- JavaScript 内存详解 & 分析指南
前言 JavaScript 诞生于 1995 年,最初被设计用于网页内的表单验证。 这些年来 JavaScript 成长飞速,生态圈日益壮大,成为了最受程序员欢迎的开发语言之一。并且现在的 JavaScript 不再局限于网页端,已经扩展到了桌面端、移动端以及服务端。 随着大前端时代的到来,使用 JavaScript 的开发者越来越多,但是许多开发者都只停留在“会用”这个层面,而对于这门语言并没有更多的了解。 如果想要成为一名更好的 JavaScript 开发者,理解内存是一个不可忽略的关键点。 📖 本文主要包含两大部分: JavaScript 内存详解 JavaScript 内存分析指南 看完这篇文章后,相信你会对 JavaScript 的内存有比较全面的了解,并且能够拥有独自进行内存分析的能力。 🧐 话不多说,我们开始吧! 文章篇幅较长,除去代码也有 12000 字左右,需要一定的时间来阅读,但是我保证你所花费的时间都是值得的。 正文 内存(memory) 什么是内存(What is memory) 相信大家都对内存有一定的了解,我就不从盘古开天辟地开始讲了,稍微提一下。 首先,任何应用程序想要运行都离不开内存。 另外,我们提到的内存在不同的层面上有着不同的含义。 💻 硬件层面(Hardware) 在硬件层面上,内存指的是随机存取存储器。 内存是计算机重要组成部分,用来储存应用运行所需要的各种数据,CPU 能够直接与内存交换数据,保证应用能够流畅运行。 一般来说,在计算机的组成中主要有两种随机存取存储器:高速缓存(Cache)和主存储器(Main memory)。 高速缓存通常直接集成在 CPU 内部,离我们比较远,所以更多时候我们提到的(硬件)内存都是主存储器。 💡 随机存取存储器(Random Access Memory,RAM) 随机存取存储器分为静态随机存取存储器(Static Random Access Memory,SRAM)和动态随机存取存储器(Dynamic Random Access Memory,DRAM)两大类。 在速度上 SRAM 要远快于 DRAM,而 SRAM 的速度仅次于 CPU 内部的寄存器。 在现代计算机中,高速缓存使用的是 SRAM,而主存储器使用的是 DRAM。 💡 主存储器(Main memory,主存) 虽然高速缓存的速度很快,但是其存储容量很小,小到几 KB 最大也才几十 MB,根本不足以储存应用运行的数据。 我们需要一种存储容量与速度适中的存储部件,让我们在保证性能的情况下,能够同时运行几十甚至上百个应用,这也就是主存的作用。 计算机中的主存其实就是我们平时说的内存条(硬件)。 硬件内存不是我们今天的主题,所以就说这么多,想要深入了解的话可以根据上面提到关键词进行搜索。 🧩 软件层面(Software) 在软件层面上,内存通常指的是操作系统从主存中划分(抽象)出来的内存空间。 此时内存又可以分为两类:栈内存和堆内存。 接下来我将围绕 JavaScript 这门语言来对内存进行讲解。 在后面的文章中所提到的内存均指软件层面上的内存。 栈与堆(Stack & Heap) 栈内存(Stack memory) 💡 栈(Stack) 栈是一种常见的数据结构,栈只允许在结构的一端操作数据,所有数据都遵循后进先出(Last-In First-Out,LIFO)的原则。 现实生活中最贴切的的例子就是羽毛球桶,通常我们只通过球桶的一侧来进行存取,最先放进去的羽毛球只能最后被取出,而最后放进去的则会最先被取出。 栈内存之所以叫做栈内存,是因为栈内存使用了栈的结构。 栈内存是一段连续的内存空间,得益于栈结构的简单直接,栈内存的访问和操作速度都非常快。 栈内存的容量较小,主要用于存放函数调用信息和变量等数据,大量的内存分配操作会导致栈溢出(Stack overflow)。 栈内存的数据储存基本都是临时性的,数据会在使用完之后立即被回收(如函数内创建的局部变量在函数返回后就会被回收)。 简单来说:栈内存适合存放生命周期短、占用空间小且固定的数据。 [图片] 💡 栈内存的大小 栈内存由操作系统直接管理,所以栈内存的大小也由操作系统决定。 通常来说,每一条线程(Thread)都会有独立的栈内存空间,Windows 给每条线程分配的栈内存默认大小为 1MB。 堆内存(Heap memory) 💡 堆(Heap) 堆也是一种常见的数据结构,但是不在本文讨论范围内,就不多说了。 堆内存虽然名字里有个“堆”字,但是它和数据结构中的堆没半毛钱关系,就只是撞了名罢了。 堆内存是一大片内存空间,堆内存的分配是动态且不连续的,程序可以按需申请堆内存空间,但是访问速度要比栈内存慢不少。 堆内存里的数据可以长时间存在,无用的数据需要程序主动去回收,如果大量无用数据占用内存就会造成内存泄露(Memory leak)。 简单来说:堆内存适合存放生命周期长,占用空间较大或占用空间不固定的数据。 [图片] 💡 堆内存的上限 在 Node.js 中,堆内存默认上限在 64 位系统中约为 1.4 GB,在 32 位系统中约为 0.7 GB。 而在 Chrome 浏览器中,每个标签页的内存上限约为 4 GB(64 位系统)和 1 GB(32 位系统)。 💡 进程、线程与堆内存 通常来说,一个进程(Process)只会有一个堆内存,同一进程下的多个线程会共享同一个堆内存。 在 Chrome 浏览器中,一般情况下每个标签页都有单独的进程,不过在某些情况下也会出现多个标签页共享一个进程的情况。 函数调用(Function calling) 明白了栈内存与堆内存是什么后,现在让我们看看当一个函数被调用时,栈内存和堆内存会发生什么变化。 当函数被调用时,会将函数推入栈内存中,生成一个栈帧(Stack frame),栈帧可以理解为由函数的返回地址、参数和局部变量组成的一个块;当函数调用另一个函数时,又会将另一个函数也推入栈内存中,周而复始;直到最后一个函数返回,便从栈顶开始将栈内存中的元素逐个弹出,直到栈内存中不再有元素时则此次调用结束。 [图片] 上图中的内容经过了简化,剥离了栈帧和各种指针的概念,主要展示函数调用以及内存分配的大概过程。 在同一线程下(JavaScript 是单线程的),所有被执行的函数以及函数的参数和局部变量都会被推入到同一个栈内存中,这也就是大量递归会导致栈溢出(Stack overflow)的原因。 关于图中涉及到的函数内部变量内存分配的详情请接着往下看。 储存变量(Store variables) 当 JavaScript 程序运行时,在非全局作用域中产生的局部变量均储存在栈内存中。 但是,只有原始类型的变量是真正地把值储存在栈内存中。 而引用类型的变量只在栈内存中储存一个引用(reference),这个引用指向堆内存里的真正的值。 💡 原始类型(Primitive type) 原始类型又称基本类型,包括 [代码]string[代码]、[代码]number[代码]、[代码]bigint[代码]、[代码]boolean[代码]、[代码]undefined[代码]、[代码]null[代码] 和 [代码]symbol[代码](ES6 新增)。 原始类型的值被称为原始值(Primitive value)。 补充:虽然 [代码]typeof null[代码] 返回的是 [代码]'object'[代码],但是 [代码]null[代码] 真的不是对象,会出现这样的结果其实是 JavaScript 的一个 Bug~ 💡 引用类型(Reference type) 除了原始类型外,其余类型都属于引用类型,包括 [代码]Object[代码]、[代码]Array[代码]、[代码]Function[代码]、[代码]Date[代码]、[代码]RegExp[代码]、[代码]String[代码]、[代码]Number[代码]、[代码]Boolean[代码] 等等… 实际上 [代码]Object[代码] 是最基本的引用类型,其他引用类型均继承自 [代码]Object[代码]。也就是说,所有引用类型的值实际上都是对象。 引用类型的值被称为引用值(Reference value)。 🎃 简单来说 在多数情况下,原始类型的数据储存在栈内存,而引用类型的数据(对象)则储存在堆内存。 [图片] 特别注意(Attention) 全局变量以及被闭包引用的变量(即使是原始类型)均储存在堆内存中。 🌐 全局变量(Global variables) 在全局作用域下创建的所有变量都会成为全局对象(如 [代码]window[代码] 对象)的属性,也就是全局变量。 而全局对象储存在堆内存中,所以全局变量必然也会储存在堆内存中。 不要问我为什么全局对象储存在堆内存中,一会我翻脸了啊! 📦 闭包(Closures) 在函数(局部作用域)内创建的变量均为局部变量。 当一个局部变量被当前函数之外的其他函数所引用(也就是发生了逃逸),此时这个局部变量就不能随着当前函数的返回而被回收,那么这个变量就必须储存在堆内存中。 而这里的“其他函数”就是我们说的闭包,就如下面这个例子: [代码]function getCounter() { let count = 0; function counter() { return ++count; } return counter; } // closure 是一个闭包函数 // 变量 count 发生了逃逸 let closure = getCounter(); closure(); // 1 closure(); // 2 closure(); // 3 [代码] 闭包是一个非常重要且常用的概念,许多编程语言里都有闭包这个概念。这里就不详细介绍了,贴一篇阮一峰大佬的文章。 学习 JavaScript 闭包:http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html 💡 逃逸分析(Escape Analysis) 实际上,JavaScript 引擎会通过逃逸分析来决定变量是要储存在栈内存还是堆内存中。 简单来说,逃逸分析是一种用来分析变量的作用域的机制。 不可变与可变(Immutable and Mutable) 栈内存中会储存两种变量数据:原始值和对象引用。 不仅类型不同,它们在栈内存中的具体表现也不太一样。 原始值(Primitive values) 🚫 Primitive values are immutable! 前面有说到:原始类型的数据(原始值)直接储存在栈内存中。 ⑴ 当我们定义一个原始类型变量的时候,JavaScript 会在栈内存中激活一块内存来储存变量的值(原始值)。 ⑵ 当我们更改原始类型变量的值时,实际上会再激活一块新的内存来储存新的值,并将变量指向新的内存空间,而不是改变原来那块内存里的值。 ⑶ 当我们将一个原始类型变量赋值给另一个新的变量(也就是复制变量)时,也是会再激活一块新的内存,并将源变量内存里的值复制一份到新的内存里。 [图片] 🤠 总之就是:栈内存中的原始值一旦确定就不能被更改(不可变的)。 原始值的比较(Comparison) 当我们比较原始类型的变量时,会直接比较栈内存中的值,只要值相等那么它们就相等。 [代码]let a = '123'; let b = '123'; let c = '110'; let d = 123; console.log(a === b); // true console.log(a === c); // false console.log(a === d); // false [代码] 对象引用(Object references) 🧩 Object references are mutable! 前面也有说到:引用类型的变量在栈内存中储存的只是一个指向堆内存的引用。 ⑴ 当我们定义一个引用类型的变量时,JavaScript 会先在堆内存中找到一块合适的地方来储存对象,并激活一块栈内存来储存对象的引用(堆内存地址),最后将变量指向这块栈内存。 💡 所以当我们通过变量访问对象时,实际的访问过程应该是: 变量 -> 栈内存中的引用 -> 堆内存中的值 ⑵ 当我们把引用类型变量赋值给另一个变量时,会将源变量指向的栈内存中的对象引用复制到新变量的栈内存中,所以实际上只是复制了个对象引用,并没有在堆内存中生成一份新的对象。 ⑶ 而当我们给引用类型变量分配为一个新的对象时,则会直接修改变量指向的栈内存中的引用,新的引用指向堆内存中新的对象。 [图片] 🤠 总之就是:栈内存中的对象引用是可以被更改的(可变的)。 对象的比较(Comparison) 所有引用类型的值实际上都是对象。 当我们比较引用类型的变量时,实际上是在比较栈内存中的引用,只有引用相同时变量才相等。 即使是看起来完全一样的两个引用类型变量,只要他们的引用的不是同一个值,那么他们就是不一样。 [代码]// 两个变量指向的是两个不同的引用 // 虽然这两个对象看起来完全一样 // 但它们确确实实是不同的对象实例 let a = { name: 'pp' } let b = { name: 'pp' } console.log(a === b); // false // 直接赋值的方式复制的是对象的引用 let c = a; console.log(a === c); // true [代码] 对象的深拷贝(Deep copy) 当我们搞明白引用类型变量在内存中的表现时,就能清楚地理解为什么浅拷贝对象是不可靠的。 在浅拷贝中,简单的赋值只会复制对象的引用,实际上新变量和源变量引用的都是同一个对象,修改时也是修改的同一个对象,这显然不是我们想要的。 想要真正的复制一个对象,就必须新建一个对象,将源对象的属性复制过去;如果遇到引用类型的属性,那就再新建一个对象,继续复制… 此时我们就需要借助递归来实现多层次对象的复制,这也就是我们说的深拷贝。 对于任何引用类型的变量,都应该使用深拷贝来复制,除非你很确定你的目的就是复制一个引用。 内存生命周期(Memory life cycle) 通常来说,所有应用程序的内存生命周期都是基本一致的: 分配 -> 使用 -> 释放 当我们使用高级语言编写程序时,往往不会涉及到内存的分配与释放操作,因为分配与释放均已经在底层语言中实现了。 对于 JavaScript 程序来说,内存的分配与释放是由 JavaScript 引擎自动完成的(目前的 JavaScript 引擎基本都是使用 C++ 或 C 编写的)。 但是这不意味着我们就不需要在乎内存管理,了解内存的更多细节可以帮助我们写出性能更好,稳定性更高的代码。 垃圾回收(Garbage collection) 垃圾回收即我们常说的 GC(Garbage collection),也就是清除内存中不再需要的数据,释放内存空间。 由于栈内存由操作系统直接管理,所以当我们提到 GC 时指的都是堆内存的垃圾回收。 基本上现在的浏览器的 JavaScript 引擎(如 V8 和 SpiderMonkey)都实现了垃圾回收机制,引擎中的垃圾回收器(Garbage collector)会定期进行垃圾回收。 📢 紧急补课 在我们继续之前,必须先了解“可达性”和“内存泄露”这两个概念: 💡 可达性(Reachability) 在 JavaScript 中,可达性指的是一个变量是否能够直接或间接通过全局对象访问到,如果可以那么该变量就是可达的(Reachable),否则就是不可达的(Unreachable)。 [图片] 上图中的节点 9 和节点 10 均无法通过节点 1(根节点)直接或间接访问,所以它们都是不可达的,可以被安全地回收。 💡 内存泄漏(Memory leak) 内存泄露指的是程序运行时由于某种原因未能释放那些不再使用的内存,造成内存空间的浪费。 轻微的内存泄漏或许不太会对程序造成什么影响,但是一旦泄露变严重,就会开始影响程序的性能,甚至导致程序的崩溃。 垃圾回收算法(Algorithms) 垃圾回收的基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。 实际上,在回收过程中想要确定一个变量是否还有用并不简单。 直到现在也还没有一个真正完美的垃圾回收算法,接下来介绍 3 种最广为人知的垃圾回收算法。 标记-清除(Mark-and-Sweep) 标记清除算法是目前最常用的垃圾收集算法之一。 从该算法的名字上就可以看出,算法的关键就是标记与清除。 标记指的是标记变量的状态的过程,标记变量的具体方法有很多种,但是基本理念是相似的。 对于标记算法我们不需要知道所有细节,只需明白标记的基本原理即可。 需要注意的是,这个算法的效率不算高,同时会引起内存碎片化的问题。 🌰 举个栗子 当一个变量进入执行上下文时,它就会被标记为“处于上下文中”;而当变量离开执行上下文时,则会被标记为“已离开上下文”。 💡 执行上下文(Execution context) 执行上下文是 JavaScript 中非常重要的概念,简单来说的是代码执行的环境。 如果你现在对于执行上下文还不是很了解,我强烈建议你抽空专门去学习下!!! 垃圾回收器将定期扫描内存中的所有变量,将处于上下文中以及被处于上下文中的变量引用的变量的标记去除,将其余变量标记为“待删除”。 随后,垃圾回收器会清除所有带有“待删除”标记的变量,并释放它们所占用的内存。 标记-整理(Mark-Compact) 准确来说,Compact 应译为紧凑、压缩,但是在这里我觉得用“整理”更为贴切。 标记整理算法也是常用的垃圾收集算法之一。 使用标记整理算法可以解决内存碎片化的问题(通过整理),提高内存空间的可用性。 但是,该算法的标记阶段比较耗时,可能会堵塞主线程,导致程序长时间处于无响应状态。 虽然算法的名字上只有标记和整理,但这个算法通常有 3 个阶段,即标记、整理与清除。 🌰 以 V8 的标记整理算法为例 ① 首先,在标记阶段,垃圾回收器会从全局对象(根)开始,一层一层往下查询,直到标记完所有活跃的对象,那么剩下的未被标记的对象就是不可达的了。 [图片] ② 然后是整理阶段(碎片整理),垃圾回收器会将活跃的(被标记了的)对象往内存空间的一端移动,这个过程可能会改变内存中的对象的内存地址。 ③ 最后来到清除阶段,垃圾回收器会将边界后面(也就是最后一个活跃的对象后面)的对象清除,并释放它们占用的内存空间。 [图片] 引用计数(Reference counting) 引用计数算法是基于“引用计数”实现的垃圾回收算法,这是最初级但已经被弃用的垃圾回收算法。 引用计数算法需要 JavaScript 引擎在程序运行时记录每个变量被引用的次数,随后根据引用的次数来判断变量是否能够被回收。 虽然垃圾回收已不再使用引用计数算法,但是引用计数技术仍非常有用! 🌰 举个栗子 注意:垃圾回收不是即使生效的!但是在下面的例子中我们将假设回收是立即生效的,这样会更好理解~ [代码]// 下面我将 name 属性为 ππ 的对象简称为 ππ // 而 name 属性为 pp 的对象则简称为 pp // ππ 的引用:1,pp 的引用:1 let a = { name: 'ππ', z: { name: 'pp' } } // b 和 a 都指向 ππ // ππ 的引用:2,pp 的引用:1 let b = a; // x 和 a.z 都指向 pp // ππ 的引用:2,pp 的引用:2 let x = a.z; // 现在只有 b 还指向 ππ // ππ 的引用:1,pp 的引用:2 a = null; // 现在 ππ 没有任何引用了,可以被回收了 // 在 ππ 被回收后,pp 的引用也会相应减少 // ππ 的引用:0,pp 的引用:1 b = null; // 现在 pp 也可以被回收了 // ππ 的引用:0,pp 的引用:0 x = null; // 哦豁,这下全完了! [代码] 🔄 循环引用(Circular references) 引用计数算法看似很美好,但是它有一个致命的缺点,就是无法处理循环引用的情况。 在下方的例子中,当 [代码]foo()[代码] 函数执行完毕之后,对象 [代码]a[代码] 与 [代码]b[代码] 都已经离开了作用域,理论上它们都应该能够被回收才对。 但是由于它们互相引用了对方,所以垃圾回收器就认为他们都还在被引用着,导致它们哥俩永远都不会被回收,这就造成了内存泄露。 [代码]function foo() { let a = { o: null }; let b = { o: null }; a.o = b; b.o = a; } foo(); // 即使 foo 函数已经执行完毕 // 对象 a 和 b 均已离开函数作用域 // 但是 a 和 b 还在互相引用 // 那么它们这辈子都不会被回收了 // Oops!内存泄露了! [代码] V8 中的垃圾回收(GC in V8) 8️⃣ V8 V8 是一个由 Google 开源的用 C++ 编写的高性能 JavaScript 引擎。 V8 是目前最流行的 JavaScript 引擎之一,我们熟知的 Chrome 浏览器和 Node.js 等软件都在使用 V8。 在 V8 的内存管理机制中,把堆内存(Heap memory)划分成了多个区域。 [图片] 这里我们只关注这两个区域: New Space(新空间):又称 Young generation(新世代),用于储存新生成的对象,由 Minor GC 进行管理。 Old Space(旧空间):又称 Old generation(旧世代),用于储存那些在两次 GC 后仍然存活的对象,由 Major GC 进行管理。 也就是说,只要 New Space 里的对象熬过了两次 GC,就会被转移到 Old Space,变成老油条。 🧹 双管齐下 V8 内部实现了两个垃圾回收器: Minor GC(副 GC):它还有个名字叫做 Scavenger(清道夫),具体使用的是 Cheney’s Algorithm(Cheney 算法)。 Major GC(主 GC):使用的是文章前面提到的 Mark-Compact Algorithm(标记-整理算法)。 储存在 New Space 里的新生对象大多都只是临时使用的,而且 New Space 的容量比较小,为了保持内存的可用率,Minor GC 会频繁地运行。 而 Old Space 里的对象存活时间都比较长,所以 Major GC 没那么勤快,这一定程度地降低了频繁 GC 带来的性能损耗。 💥 加点魔法 我们在上方的“标记整理算法”中有提到这个算法的标记过程非常耗时,所以很容易导致应用长时间无响应。 为了提升用户体验,V8 还实现了一个名为增量标记(Incremental marking)的特性。 增量标记的要点就是把标记工作分成多个小段,夹杂在主线程(Main thread)的 JavaScript 逻辑中,这样就不会长时间阻塞主线程了。 [图片] 当然增量标记也有代价的,在增量标记过程中所有对象的变化都需要通知垃圾回收器,好让垃圾回收器能够正确地标记那些对象,这里的“通知”也是需要成本的。 另外 V8 中还有使用工作线程(Worker thread)实现的平行标记(Parallel marking)和并行标记(Concurrent marking),这里我就不再细说了~ 🤓 总结一下 为了提升性能和用户体验,V8 内部做了非常非常多的“骚操作”,本文提到的都只是冰山一角,但足以让我五体投地佩服连连! 总之就是非常 Amazing 啊~ 内存管理(Memory management) 或者说是:内存优化(Memory optimization)? 虽然我们写代码的时候一般不会直接接触内存管理,但是有一些注意事项可以让我们避免引起内存问题,甚至提升代码的性能。 全局变量(Global variable) 全局变量的访问速度远不及局部变量,应尽量避免定义非必要的全局变量。 在我们实际的项目开发中,难免会需要去定义一些全局变量,但是我们必须谨慎使用全局变量。 因为全局变量永远都是可达的,所以全局变量永远不会被回收。 🌐 还记得“可达性”这个概念吗? 因为全局变量直接挂载在全局对象上,也就是说全局变量永远都可以通过全局对象直接访问。 所以全局变量永远都是可达的,而可达的变量永远都不会被回收。 🤨 应该怎么做? 当一个全局变量不再需要用到时,记得解除其引用(置空),好让垃圾回收器可以释放这部分内存。 [代码]// 全局变量不会被回收 window.me = { name: '吴彦祖', speak: function() { console.log(`我是${this.name}`); } }; window.me.speak(); // 解除引用后才可以被回收 window.me = null; [代码] 隐藏类(HiddenClass) 实际上的隐藏类远比本文所提到的复杂,但是今天的主角不是它,所以我们点到为止。 在 V8 内部有一个叫做“隐藏类”的机制,主要用于提升对象(Object)的性能。 V8 里的每一个 JS 对象(JS Objects)都会关联一个隐藏类,隐藏类里面储存了对象的形状(特征)和属性名称到属性的映射等信息。 隐藏类内记录了每个属性的内存偏移(Memory offset),后续访问属性的时候就可以快速定位到对应属性的内存位置,从而提升对象属性的访问速度。 在我们创建对象时,拥有完全相同的特征(相同属性且相同顺序)的对象可以共享同一个隐藏类。 🤯 再想象一下 我们可以把隐藏类想象成工业生产中使用的模具,有了模具之后,产品的生产效率得到了很大的提升。 但是如果我们更改了产品的形状,那么原来的模具就不能用了,又需要制作新的模具才行。 🌰 举个栗子 在 Chrome 浏览器 Devtools 的 Console 面板中执行以下代码: [代码]// 对象 A let objectA = { id: 'A', name: '吴彦祖' }; // 对象 B let objectB = { id: 'B', name: '彭于晏' }; // 对象 C let objectC = { id: 'C', name: '刘德华', gender: '男' }; // 对象 A 和 B 拥有完全相同的特征 // 所以它们可以使用同一个隐藏类 // good! [代码] 随后在 Memory 面板打一个堆快照,通过堆快照中的 Comparison 视图可以快速找到上面创建的 3 个对象: 注:关于如何查看内存中的对象将会在文章的第二大部分中进行讲解,现在让我们专注于隐藏类。 [图片] 在上图中可以很清楚地看到对象 A 和 B 确实使用了同一个隐藏类。 而对象 C 因为多了一个 [代码]gender[代码] 属性,所以不能和前面两个对象共享隐藏类。 🧀 动态增删对象属性 一般情况下,当我们动态修改对象的特征(增删属性)时,V8 会为该对象分配一个能用的隐藏类或者创建一个新的隐藏类(新的分支)。 例如动态地给对象增加一个新的属性: 注:这种操作被称为“先创建再补充(ready-fire-aim)”。 [代码]// 增加 gender 属性 objectB.gender = '男'; // 对象 B 的特征发生了变化 // 多了一个原本没有的 gender 属性 // 导致对象 B 不能再与 A 共享隐藏类 // bad! [代码] 动态删除([代码]delete[代码])对象的属性也会导致同样的结果: [代码]// 删除 name 属性 delete objectB.name; // A:我们不一样! // bad! [代码] 不过,添加数组索引属性(Array-indexed properties)并不会有影响: 其实就是用整数作为属性名,此时 V8 会另外处理。 [代码]// 增加 1 属性 objectB[1] = '数字组引属性'; // 不影响共享隐藏类 // so far so good! [代码] 🙄 那问题来了 说了这么多,隐藏类看起来确实可以提升性能,那它和内存又有什么关系呢? 实际上,隐藏类也需要占用内存空间,这其实就是一种用空间换时间的机制。 如果由于动态增删对象属性而创建了大量隐藏类和分支,结果就是会浪费不少内存空间。 🌰 举个栗子 创建 1000 个拥有相同属性的对象,内存中只会多出 1 个隐藏类。 而创建 1000 个属性信息完全不同的对象,内存中就会多出 1000 个隐藏类。 🤔 应该怎么做? 所以,我们要尽量避免动态增删对象属性操作,应该在构造函数内就一次性声明所有需要用到的属性。 如果确实不再需要某个属性,我们可以将属性的值设为 [代码]null[代码],如下: [代码]// 将 age 属性置空 objectB.age = null; // still good! [代码] 另外,相同名称的属性尽量按照相同的顺序来声明,可以尽可能地让更多对象共享相同的隐藏类。 即使遇到不能共享隐藏类的情况,也至少可以减少隐藏类分支的产生。 其实动态增删对象属性所引起的性能问题更为关键,但因本文篇幅有限,就不再展开了。 闭包(Closure) 前面有提到:被闭包引用的变量储存在堆内存中。 这里我们再重点关注一下闭包中的内存问题,还是前面的例子: [代码]function getCounter() { let count = 0; function counter() { return ++count; } return counter; } // closure 是一个闭包函数 let closure = getCounter(); closure(); // 1 closure(); // 2 closure(); // 3 [代码] 现在只要我们一直持有变量(函数) [代码]closure[代码],那么变量 [代码]count[代码] 就不会被释放。 或许你还没有发现风险所在,不如让我们试想变量 [代码]count[代码] 不是一个数字,而是一个巨大的数组,一但这样的闭包多了,那对于内存来说就是灾难。 [代码]// 我将这个作品称为:闭包炸弹 function closureBomb() { const handsomeBoys = []; setInterval(() => { for (let i = 0; i < 100; i++) { handsomeBoys.push( { name: '陈皮皮', rank: 0 }, { name: ' 你 ', rank: 1 }, { name: '吴彦祖', rank: 2 }, { name: '彭于晏', rank: 3 }, { name: '刘德华', rank: 4 }, { name: '郭富城', rank: 5 } ); } }, 100); } closureBomb(); // 即将毁灭世界 // 💣 🌍 💥 💨 [代码] 🤔 应该怎么做? 所以,我们必须避免滥用闭包,并且谨慎使用闭包! 当不再需要时记得解除闭包函数的引用,让闭包函数以及引用的变量能够被回收。 [代码]closure = null; // 变量 count 终于得救了 [代码] 如何分析内存(Analyze) 说了这么多,那我们应该如何查看并分析程序运行时的内存情况呢? “工欲善其事,必先利其器。” 对于 Web 前端项目来说,分析内存的最佳工具非 Memory 莫属! 这里的 Memory 指的是 DevTools 中的一个工具,为了避免混淆,下面我会用“Memory 面板”或”内存面板“代称。 🔧 DevTools(开发者工具) DevTools 是浏览器里内置的一套用于 Web 开发和调试的工具。 使用 Chromuim 内核的浏览器都带有 DevTools,个人推荐使用 Chrome 或者 Edge(新)。 Memory in Devtools(内存面板) 在我们切换到 Memory 面板后,会看到以下界面(注意标注): [图片] 在这个面板中,我们可以通过 3 种方式来记录内存情况: Heap snapshot:堆快照 Allocation instrumentation on timeline:内存分配时间轴 Allocation sampling:内存分配采样 小贴士:点击面板左上角的 Collect garbage 按钮(垃圾桶图标)可以主动触发垃圾回收。 🤓 在正式开始分析内存之前,让我们先学习几个重要的概念: 💡 Shallow Size(浅层大小) 浅层大小指的是当前对象自身占用的内存大小。 浅层大小不包含自身引用的对象。 💡 Retained Size(保留大小) 保留大小指的是当前对象被 GC 回收后总共能够释放的内存大小。 换句话说,也就是当前对象自身大小加上对象直接或间接引用的其他对象的大小总和。 需要注意的是,保留大小不包含那些除了被当前对象引用之外还被全局对象直接或间接引用的对象。 Heap snapshot(堆快照) [图片] 堆快照可以记录页面当前时刻的 JS 对象以及 DOM 节点的内存分配情况。 🚀 如何开始 点击页面底部的 Take snapshot 按钮或者左上角的 ⚫ 按钮即可打一个堆快照,片刻之后就会自动展示结果。 [图片] 在堆快照结果页面中,我们可以使用 4 种不同的视图来观察内存情况: Summary:摘要视图 Comparison:比较视图 Containment:包含视图 Statistics:统计视图 默认显示 Summary 视图。 Summary(摘要视图) 摘要视图根据 Constructor(构造函数)来将对象进行分组,我们可以在 Class filter(类过滤器)中输入构造函数名称来快速筛选对象。 [图片] 页面中的几个关键词: Constructor:构造函数。 Distance:(根)距离,对象与 GC 根之间的最短距离。 Shallow Size:浅层大小,单位:Bytes(字节)。 Retained Size:保留大小,单位:Bytes(字节)。 Retainers:持有者,也就是直接引用目标对象的变量。 📌 Retainers(持有者) Retainers 栏在旧版的 Devtools 里叫做 Object’s retaining tree(对象保留树)。 Retainers 下的对象也展开为树形结构,方便我们进行引用溯源。 在视图中的构造函数列表中,有一些用“()”包裹的条目: (compiled code):已编译的代码。 (closure):闭包函数。 (array, string, number, symbol, regexp):对应类型([代码]Array[代码]、[代码]String[代码]、[代码]Number[代码]、[代码]Symbol[代码]、[代码]RegExp[代码])的数据。 (concatenated string):使用 [代码]concat()[代码] 函数拼接而成的字符串。 (sliced string):使用 [代码]slice()[代码]、[代码]substring()[代码] 等函数进行边缘切割的字符串。 (system):系统(引擎)产生的对象,如 V8 创建的 HiddenClasses(隐藏类)和 DescriptorArrays(描述符数组)等数据。 💡 DescriptorArrays(描述符数组) 描述符数组主要包含对象的属性名信息,是隐藏类的重要组成部分。 不过描述符数组内不会包含整数索引属性。 而其余没有用“()”包裹的则为全局属性和 GC 根。 另外,每个对象后面都会有一串“@”开头的数字,这是对象在内存中的唯一 ID。 小贴士:按下快捷键 Ctrl/Command + F 展示搜索栏,输入名称或 ID 即可快速查找目标对象。 💪 实践一下:实例化一个对象 ① 切换到 Console 面板,执行以下代码来实例化一个对象: [代码]function TestClass() { this.number = 123; this.string = 'abc'; this.boolean = true; this.symbol = Symbol('test'); this.undefined = undefined; this.null = null; this.object = { name: 'pp' }; this.array = [1, 2, 3]; this.getSet = { _value: 0, get value() { return this._value; }, set value(v) { this._value = v; } }; } let testObject = new TestClass(); [代码] [图片] ② 回到 Memory 面板,打一个堆快照,在 Class filter 中输入“TestClass”: 可以看到内存中有一个 [代码]TestClass[代码] 的实例,该实例的浅层大小为 80 字节,保留大小为 876 字节。 [图片] 🤔 注意到了吗? 堆快照中的 [代码]TestClass[代码] 实例的属性中少了一个名为 [代码]number[代码] 属性,这是因为堆快照不会捕捉数字属性。 💪 实践一下:创建一个字符串 ① 切换到 Console 面板,执行以下代码来创建一个字符串: [代码]// 这是一个全局变量 let testString = '我是吴彦祖'; [代码] ② 回到 Memory 面板,打一个堆快照,打开搜索栏(Ctrl/Command + F)并输入“我是吴彦祖”: [图片] Comparison(比较视图) 只有同时存在 2 个或以上的堆快照时才会出现 Comparison 选项。 比较视图用于展示两个堆快照之间的差异。 使用比较视图可以让我们快速得知在执行某个操作后的内存变化情况(如新增或减少对象)。 通过多个快照的对比还可以让我们快速判断并定位内存泄漏。 文章前面提到隐藏类的时候,就是使用了比较视图来快速查找新创建的对象。 💪 实践一下 ① 新建一个无痕(匿名)标签页并切换到 Memory 面板,打一个堆快照 Snapshot 1。 💡 为什么是无痕标签页? 普通标签页会受到浏览器扩展或者其他脚本影响,内存占用不稳定。 使用无痕窗口的标签页可以保证页面的内存相对纯净且稳定,有利于我们进行对比。 另外,建议打开窗口一段之间之后再开始测试,这样内存会比较稳定(控制变量)。 ② 切换到 Console 面板,执行以下代码来实例化一个 [代码]Foo[代码] 对象: [代码]function Foo() { this.name = 'pp'; this.age = 18; } let foo = new Foo(); [代码] ③ 回到 Memory 面板,再打一个堆快照 Snapshot 2,切换到 Comparison 视图,选择 Snapshot 1 作为 Base snapshot(基本快照),在 Class filter 中输入“Foo”: 可以看到内存中新增了一个 [代码]Foo[代码] 对象实例,分配了 52 字节内存空间,该实例的引用持有者为变量 [代码]foo[代码]。 [图片] ④ 再次切换到 Console 面板,执行以下代码来解除变量 [代码]foo[代码] 的引用: [代码]// 解除对象的引用 foo = null; [代码] ⑤ 再回到 Memory 面板,打一个堆快照 Snapshot 3,选择 Snapshot 2 作为 Base snapshot,在 Class filter 中输入“Foo”: 内存中的 [代码]Foo[代码] 对象实例已经被删除,释放了 52 字节的内存空间。 [图片] Containment(包含视图) 包含视图就是程序对象结构的“鸟瞰图(Bird’s eye view)”,允许我们通过全局对象出发,一层一层往下探索,从而了解内存的详细情况。 [图片] 包含视图中有以下几种全局对象: GC roots(GC 根) GC roots 就是 JavaScript 虚拟机的垃圾回收中实际使用的根节点。 GC 根可以由 Built-in object maps(内置对象映射)、Symbol tables(符号表)、VM thread stacks(VM 线程堆栈)、Compilation caches(编译缓存)、Handle scopes(句柄作用域)和 Global handles(全局句柄)等组成。 DOMWindow objects(DOMWindow 对象) DOMWindow objects 指的是由宿主环境(浏览器)提供的顶级对象,也就是 JavaScript 代码中的全局对象 [代码]window[代码],每个标签页都有自己的 [代码]window[代码] 对象(即使是同一窗口)。 Native objects(原生对象) Native objects 指的是那些基于 ECMAScript 标准实现的内置对象,包括 [代码]Object[代码]、[代码]Function[代码]、[代码]Array[代码]、[代码]String[代码]、[代码]Boolean[代码]、[代码]Number[代码]、[代码]Date[代码]、[代码]RegExp[代码]、[代码]Math[代码] 等对象。 💪 实践一下 ① 切换到 Console 面板,执行以下代码来创建一个构造函数 [代码]$ABC[代码]: 构造函数命名前面加个 $ 是因为这样排序的时候可以排在前面,方便找。 [代码]function $ABC() { this.name = 'pp'; } [代码] ② 切换到 Memory 面板,打一个堆快照,切换为 Containment 视图: 在当前标签页的全局对象下就可以找到我们刚刚创建的构造函数 [代码]$ABC[代码]。 [图片] Statistics(统计视图) 统计视图可以很直观地展示内存整体分配情况。 [图片] 在该视图里的空心饼图中共有 6 种颜色,各含义分别为: 红色:Code(代码) 绿色:Strings(字符串) 蓝色:JS arrays(数组) 橙色:Typed arrays(类型化数组) 紫色:System objects(系统对象) 白色:空闲内存 Allocation instrumentation on timeline(分配时间轴) [图片] 在一段时间内持续地记录内存分配(约每 50 毫秒打一张堆快照),记录完成后可以选择查看任意时间段的内存分配详情。 另外还可以勾选同时记录分配堆栈(Allocation stacks),也就是记录调用堆栈,不过这会产生额外的性能消耗。 🚀 如何开始 点击页面底部的 Start 按钮或者左上角的 ⚫ 按钮即可开始记录,记录过程中点击左上角的 🔴 按钮来结束记录,片刻之后就会自动展示结果。 💪 操作一下 ① 打开 Memory 面板,开始记录分配时间轴。 ② 切换到 Console 面板,执行以下代码: 代码效果:每隔 1 秒钟创建 100 个对象,共创建 1000 个对象。 [代码]console.log('测试开始'); let objects = []; let handler = setInterval(() => { // 每秒创建 100 个对象 for (let i = 0; i < 100; i++) { const name = `n${objects.length}`; const value = `v${objects.length}`; objects.push({ [name]: value}); } console.log(`对象数量:${objects.length}`); // 达到 1000 个后停止 if (objects.length >= 1000) { clearInterval(handler); console.log('测试结束'); } }, 1000); [代码] 😈 又是一个细节 不知道你有没有发现,在上面的代码中,我干了一件坏事。 在 for 循环创建对象时,会根据对象数组当前长度生成一个唯一的属性名和属性值。 这样一来 V8 就无法对这些对象进行优化,方便我们进行测试。 另外,如果直接使用对象数组的长度作为属性名会有惊喜~ ③ 静静等待 10 秒钟,控制台会打印出“测试结束”。 ④ 切换回 Memory 面板,停止记录,片刻之后会自动进入结果页面。 [图片] 分配时间轴结果页有 4 种视图: Summary:摘要视图 Containment:包含视图 Allocation:分配视图 Statistics:统计视图 默认显示 Summary 视图。 Summary(摘要视图) 看起来和堆快照的摘要视图很相似,主要是页面上方多了一条横向的时间轴(Timeline)。 [图片] 🧭 时间轴 时间轴中主要的 3 种线: 细横线:内存分配大小刻度线 蓝色竖线:表示内存在对应时刻被分配,最后仍然活跃 灰色竖线:表示内存在对应时刻被分配,但最后被回收 时间轴的几个操作: 鼠标移动到时间轴内任意位置,点击左键或长按左键并拖动即可选择一段时间 鼠标拖动时间段框上方的方块可以对已选择的时间段进行调整 鼠标移到已选择的时间段框内部,滑动滚轮可以调整时间范围 鼠标移到已选择的时间段框两旁,滑动滚轮即可调整时间段 双击鼠标左键即可取消选择 [图片] 在时间轴中选择要查看的时间段,即可得到该段时间的内存分配详情。 [图片] Containment(包含视图) 分配时间轴的包含视图与堆快照的包含视图是一样的,这里就不再重复介绍了。 [图片] Allocation(分配视图) 对不起各位,这玩意儿我也不知道有啥用… 打开就直接报错,我:喵喵喵? [图片] 是不是因为没人用这玩意儿,所以没人发现有问题… Statistics(统计视图) 分配时间轴的统计视图与堆快照的统计视图也是一样的,不再赘述。 [图片] Allocation sampling(分配采样) [图片] Memory 面板上的简介:使用采样方法记录内存分配。这种分析方式的性能开销最小,可以用于长时间的记录。 好家伙,这个简介有够模糊,说了跟没说似的,很有精神! 我在官方文档里没有找到任何关于分配采样的介绍,Google 上也几乎没有与之有关的信息。所以以下内容仅为个人实践得出的结果,如有不对的地方欢迎各位指出! 简单来说,通过分配采样我们可以很直观地看到代码中的每个函数(API)所分配的内存大小。 由于是采样的方式,所以结果并非百分百准确,即使每次执行相同的操作也可能会有不同的结果,但是足以让我们了解内存分配的大体情况。 ✍ 如何开始 点击页面底部的 Start 按钮或者左上角的 ⚫ 按钮即可开始记录,记录过程中点击左上角的 🔴 按钮来结束记录,片刻之后就会自动展示结果。 💪 操作一下 ① 打开 Memory 面板,开始记录分配采样。 ② 切换到 Console 面板,执行以下代码: 代码看起来有点长,其实就是 4 个函数分别以不同的方式往数组里面添加对象。 [代码]// 普通单层调用 let array_a = []; function aoo1() { for (let i = 0; i < 10000; i++) { array_a.push({ a: 'pp' }); } } aoo1(); // 两层嵌套调用 let array_b = []; function boo1() { function boo2() { for (let i = 0; i < 20000; i++) { array_b.push({ b: 'pp' }); } } boo2(); } boo1(); // 三层嵌套调用 let array_c = []; function coo1() { function coo2() { function coo3() { for (let i = 0; i < 30000; i++) { array_c.push({ c: 'pp' }); } } coo3(); } coo2(); } coo1(); // 两层嵌套多个调用 let array_d = []; function doo1() { function doo2_1() { for (let i = 0; i < 20000; i++) { array_d.push({ d: 'pp' }); } } doo2_1(); function doo2_2() { for (let i = 0; i < 20000; i++) { array_d.push({ d: 'pp' }); } } doo2_2(); } doo1(); [代码] ③ 切换回 Memory 面板,停止记录,片刻之后会自动进入结果页面。 [图片] 分配采样结果页有 3 种视图可选: Chart:图表视图 Heavy (Bottom Up):扁平视图(调用层级自下而上) Tree (Top Down):树状视图(调用层级自上而下) 这个 Heavy 我真的不知道该怎么翻译,所以我就按照具体表现来命名了。 默认会显示 Chart 视图。 Chart(图表视图) Chart 视图以图形化的表格形式展现各个函数的内存分配详情,可以选择精确到内存分配的不同阶段(以内存分配的大小为轴)。 [图片] 鼠标左键点击、拖动和双击以操作内存分配阶段轴(和时间轴一样),选择要查看的阶段范围。 [图片] 将鼠标移动到函数方块上会显示函数的内存分配详情。 [图片] 鼠标左键点击函数方块可以跳转到相应代码。 [图片] Heavy(扁平视图) Heavy 视图将函数调用层级压平,函数将以独立的个体形式展现。另外也可以展开调用层级,不过是自下而上的结构,也就是一个反向的函数调用过程。 [图片] 视图中的两种 Size(大小): Self Size:自身大小,指的是在函数内部直接分配的内存空间大小。 Total Size:总大小,指的是函数总共分配的内存空间大小,也就是包括函数内部嵌套调用的其他函数所分配的大小。 Tree(树状视图) Tree 视图以树形结构展现函数调用层级。我们可以从代码执行的源头开始自上而下逐层展开,呈现一个完整的正向的函数调用过程。 [图片] 参考资料 《JavaScript 高级程序设计(第4版)》 Memory Management:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management Visualizing memory management in V8 Engine:https://deepu.tech/memory-management-in-v8/ Trash talk: the Orinoco garbage collector:https://v8.dev/blog/trash-talk Fast properties in V8:https://v8.dev/blog/fast-properties Concurrent marking in V8:https://v8.dev/blog/concurrent-marking Chrome DevTools:https://developers.google.com/web/tools/chrome-devtools 传送门 微信推文版本 个人博客:菜鸟小栈 开源主页:陈皮皮 Eazax-CCC 游戏开发脚手架 更多分享 《为什么选择使用 TypeScript ?》 《高斯模糊 Shader》 《一文看懂 YAML》 《Cocos Creator 性能优化:DrawCall》 《互联网运营术语扫盲》 《在 Cocos Creator 里画个炫酷的雷达图》 《用 Shader 写个完美的波浪》 《在 Cocos Creator 中优雅且高效地管理弹窗》 《Cocos Creator 源码解读:引擎启动与主循环》 公众号 菜鸟小栈 😺我是陈皮皮,一个还在不断学习的游戏开发者,一个热爱分享的 Cocos Star Writer。 🎨这是我的个人公众号,专注但不仅限于游戏开发和前端技术分享。 💖每一篇原创都非常用心,你的关注就是我原创的动力! Input and output. [图片]
2021-01-13 - 新版自定义交易组件接入指引(更新售后流程相关内容)
自定义交易组件 本文主要介绍新版自定义交易组件的业务流程及各流程的注意事项,文档篇幅较长,如无需查看完整文档可以使用浏览器自带页面搜索功能进行关键字搜索(快捷键Ctrl+F )。 在开始开发前应确认以下必要前置条件是否完成: 1、开通场景经营商户号; 2、部分接口强制校验来源IP白名单,白名单配置方式: 1)服务商配置方式"点我查看" 2)普通小程序:登录小程序管理后台->开发管理->开发设置->“开发者ID”下的“IP白名单” 3、因视频号场景受控订单无微信支付回调,支付结果等事件是通过小程序消息推送进行下发的,需要登录小程序管理后台后,在「开发」-「开发设置」-「消息推送」中启用并设置消息推送配置,开发者接收到的消息需要解密, 消息加解密说明可在开发文档查询。 1、业务流程图 1.1 关键流程逻辑 [图片] 1.2 售后流程 [图片] 1.3 主要业务变动 [图片] 2、接入流程详细说明 整体接入流程 注:本文2.1-2.2为前置必做流程,完成后才可以进行接口调试 [图片] 2.1 开通自定义交易组件 2.1.1 公众平台后台开通 登录公众平台,点击功能下方的“交易组件”,点击“开通”阅读并同意开通协议后即可进行下一步操作。[图片] 开通后页面如图所示[图片] 2.1.2 接口申请开通 调用接口 “shop/register/apply” 申请开通自定义版交易组件,请求成功后将发送“法务协议确认”到管理员微信,管理员需要在“24小时内”点击该模板消息,并确认服务协议。服务协议签署成功后,开发者可以通过“获取接入状态接口(可点击跳转)”或“”事件回调“进行开通状态判断,若状态为“已开通”,开发者才可以继续调用后续接口进行调试接入。[图片][图片][图片] 2.1.3 自定义交易组件“升级版”跟升级前的自定义交易组件有什么区别,哪些接口需要进行升级? 1.新支付接口,必须走新商户号。 2.取消订单, 小程序(小程序内以及发现-小程序我的订单)和视频号双向可取消,之前只可以在小程序上取消,然后同步给视频号状态。 3.申请退款,小程序和视频号双向可申请退款。 4.申请退货退款,小程序和视频号双向可申请退货退款,之前只有小程序上操作。 5.未付款订单,小程序和视频号 可在各自订单中心重新支付,同步状态。 6.确认收货,小程序和视频号双向可确认收货。 7.同步发货状态接口更新。 2.2 开通场景经营所需商户号 视频号场景下,会通过开通商户号、补齐资料,获得一个新的电商收付通的商户号。商家在视频号内的交易资金流,需要使用这个新商户号。 准备事项:需准备营业执照、法人身份证、商户号超级管理员手机号、邮箱信息 注意事项:小程序超管将作为微信支付商户号的超级管理员,如管理员已离职、管理员曾变更过姓名未在小程序后台更新等情况,将影响商户号的签约,请确认无误后再进行商户号申请。 2.2.1 通过公众平台后台申请 在小程序官方后台自定义交易组件页面,点击商户号申请所对应的“去开通”按钮进入商户号申请页面,共计三部分(不想开商户号了,暂时不放图): 2.2.1.1 填写基础信息 此部分主要为上传营业执照、法人身份证、填写商户号超管信息 2.2.1.2验证账户小额打款/法人验证 此部分会有两种情况,如为“小额打款”验证方式,在页面输入收到的打款金额即可;如为法人验证,需要使用实名认证为法人同名微信扫码进行验证。 2.2.1.3确认账户真实性。 点击“去签署”按钮会展示签约二维码,使用超管微信进行扫码,用于确认经营意愿并签署协议,完成商户号注册。 2.2.2 通过API接口申请 调用“提交支付资质”接口,提交场景经营所需商户号申请,提交成功后,商户申请是没有事件回调的,需要服务商主动调用“状态查询接口”查询申请单进度,申请单状态同“电商收付通二级商户进件申请单状态查询”。此处不做过多描述,有问题可以留言,后续视情况补充更新。 2.2.3 开通场景经营商户常见问题 2.2.3.1 Q:新版交易组件需要重新申请商户号吗?是否可以使用原有商户号? A:不可以,新版交易组件必须要申请开通场景专用商户号 2.2.3.2 Q:新版场景专用商户号费率是多少,是否有优惠,结算周期是多久? A:商户号费率为0.6%,无费率优惠,结算周期为7+7日,即用户收货后7天后结算。 2.2.3.3 Q:申请新商户号时,最后一步签约遇到“微信实名信息与管理员信息不一致”是什么原因? A:申请新的场景专用商户号时,“超级管理员”这一项不支持修改,默认为小程序“超级管理员”实名信息,如需修改,需要为该用户前往成员管理为小程序绑定超级管理员。 2.2.3.4 Q:申请新的商户号时,为什么不能修改主体信息? A:“当前主体”这一项不支持修改,因为商户号主体必须和该小程序注册主体保持一致。 2.2.3.5 Q:通过新版自定义交易组件申请的场景专用商户号是否对跨境类小程序(自助报关)有影响? A:会,二级商户当前暂不支持自助清关接口调用,留意后续更新通知 2.2.3.6 Q:新商户号是否支持分账功能? A:即将支持,敬请期待。 2.3 上传商品并通过审核,完成商品接口调用(仅有API接入方式) 基础流程,不做过多说明,只写流程、常见错误和注意事项 2.3.1 添加商品流程: (1)首先去“获取类目详情”,与自己的系统商品类目相匹配 (2)查看对应类目是否需要行业资质,如果需要,请上传相关资质 (3)“上传相关类目资质”获得对应类目的使用权限 (4)如果商品有品牌,“上传品牌信息”获得品牌的使用权限 (5)调用“添加商品”接口将商品进行上传 2.3.2 SPU接口常见错误及注意事项 2.3.2.1 Q:自定义交易组件提交上传类目资质时报错{“errcode”: 1000009, “errmsg”: “调用的类目id不存在”} A: 调用获取类目详情,看对应““qualification_type"与"product_qualification_type"参数是否为"0”,为"0"无需上传类目资质 2.3.2.2 Q:调用自定义交易组件“添加商品”接口shop/spu/add时报错“该账号客服方式必须包含微信客服/小程序客服” {“errcode”:1040042,“errmsg”:"该账号客服方式必须包含微信客服/小程序客服”} A11:需要在MP后台配置微信客服/小程序客服后,然后通过“更新商家信息”接口更新商家信息 [图片] 调用“获取商家信息”接口应返回一下内容才为成功,“service_agent_type”字段需要同时包含0,1,2三个值 [图片] 2.3.2.3 Q:类目审核成功,但是添加商品报错,{“errcode”:1000005,“errmsg”:"该商品使用了未申请通过的类目,请先申请类目且通过后再提审商品 "} A: 检查是否取消开通过,取消开通会清空品牌类目。对于服务商,可以监听账户接入回调事件,目前只有取消开通回调。 2.3.2.4 Q:调用自定义组件上传图片接口报错{“errcode”:1070001,“errmsg”:"文件/图片为空 "} A25:检查请求报文协议,需[代码]Content-Type: multipart/form-data[代码] 2.4 调试支付校验,完成订单接口调用 2.4.1 支付流程: (1)按照"开发指引"修改基础库配置 (2)在小程序中调用"生成订单"接口生成一笔订单 (3)完成订单的支付(视频号场景需要调用生成支付参数后完成收银台的拉起,其他场景按照已有业务逻辑进行支付) (4)调用"同步订单支付结果"接口同步订单的支付结果(二级商户单无需同步支付结果,系统自动流转订单状态) 注意: 这里两个接口都需要调用,以同步订单结果为最后确认步骤,视频号场景则以支付成功作为最终确认步骤。 基础库拉起收银台接口改造后需要发版才可以生效。 完成接口调用后,点击完成,切换状态。 2.4.2 订单接口接口常见错误及注意事项 2.4.2.1 Q:二级商户号订单支付流程与原有订单支付流程有什么区别? A17:主要区别是:二级商户号订单调起支付所需参数是通过“生成支付参数”获取,无需同步支付结果;原流程调起支付是需要通过微信支付统一下单获取,需要同步支付结果。 2.4.2.2 Q:调用自定义交易组件“创建订单”接口shop/order/add时报错“不支持的发货方式” {“errcode”:1010036,“errmsg”:"不支持的发货方式“} A:视频号场景当前只支持“正常快递”方式,其他请留意后续更新。 2.4.2.3 Q:自定义交易组件申请视频号专用商户号后,唤起支付报错: “商户号该产品权限未开通” A:需要先调用“生成订单”接口,生成订单时将fund_type设为1,然后调用“生成支付参数”接口获取调取支付所需参数,不要调用微信支付统一下单接口获取调用支付参数 2.4.2.4 Q:自定义交易组件二级商户单调起支付时报错“JSAPI缺少参数total_fee” A:生成支付参数失败,没返回正确的预支付 ID,重新调用生成支付参数接口获取新的支付参数即可 2.4.2.5 Q:自定义交易组件调用同步订单支付结果时报错 {“errcode”:990022,“errmsg”:"暂不支持操作“} A:二级商户单无需同步支付结果,付款成功后系统自动流转订单状态并下发支付结果回调事件。 2.5 调试发货接口,完成物流相关接口调用 2.5.1 发货流程 (1)先调用获取快递公司列表接口获取快递公司信息 (2)调用"订单发货"接口,完成发货 2.5.2 发货接口常见错误及注意事项 注意事项: 视频号订单状态流转不可逆的,当finish_all_delivery=0时,订单状态流转到21(部分发货), 当finish_all_delivery=1时,订单状态从20(待发货)/ 21(部分发货)流转到30(待收货),部分发货时finish_all_delivery一定要传0,最后一次发货才可以传1,切记!切记!切记!!! 2.5.2.1 Q:调用自定义交易组件“创建订单”接口shop/order/add时报错“不支持的发货方式” {“errcode”:1010036,“errmsg”:"不支持的发货方式“} A:视频号场景当前只支持“正常快递”方式,其他请留意后续更新。 2.5.2.2 Q:当自定义交易组件订单只存在一个SKU,发货时误操作为部分发货,订单状态码流转为21时,在既不能确认收货、也不能退款的情况下应该如何处理? A:按照截图示例传参,即可完成发货,订单状态由21转为30,订单状态变更后就可以正常流转 注:传参时order_id与out_order_id二选一传参 [图片] 2.6 调试售后接口,完成售后接口调用 2.6.1 售后流程、超时时间及售后事件和API、回调的对应关系 看本文1.2 售后流程图 用户及商户处理售后超时时间 类型 超时时间 商家处理退款超时 48小时 商家处理退货超时 48小时 用户超时未申请平台接入 7天 用户上传退货物流超时 7天 商家超时未确认收货 10天 售后事件和API、回调的对应关系 事件 API 触发回调 说明 用户申请退款 用户提交售后申请 用户提交售后申请回调 - 用户申请退货 用户提交售后申请 用户提交售后申请回调 - 用户修改申请 用户更新售后申请 用户更新售后申请回调 - 用户取消申请 用户取消售后申请 用户取消售后申请回调 - 用户申请平台介入 无API,只能由用户在微信侧触发 暂无 - 用户超时未申请平台介入(7天) - 用户申请平台介入超时回调 - 用户上传退货物流 用户上传物流信息 用户上传退货物流回调(待商家确认收货) - 用户超时未上传退货物流(7天) - 用户上传退货物流超时回调 - 用户确认退款凭证 无API,只能由用户在微信侧触发 用户确认退款凭证回调 - 商家同意退款 同意退款 - - 商家处理退款超时(48小时) - 商家处理退款请求超时回调 - 商家同意退货 同意退货 - - 商家处理退货超时(48小时) - 商家处理退货申请超时回调 - 商家拒绝退款 拒绝售后 - - 商家拒绝退货 拒绝售后 - - 商家上传退款凭证 上传退款凭证 - 商家确认收货 同意退款 商家确认收到的货没问题后,调用同意退款API表示确认收货,如果有问题就调用拒绝售后API 商家超时未确认收货(10天) - 商家确认收货超时回调 - 平台退款成功 - 平台退款成功回调 - 平台退款失败 - 平台退款失败回调(待商家线下退款) - 平台判定用户责任 - 纠纷事件更新售后单状态回调 纠纷事件通知请见纠纷回调 平台判定商家退款 - 纠纷事件更新售后单状态回调 纠纷事件通知请见纠纷回调 平台判定商家退货 - 纠纷事件更新售后单状态回调 纠纷事件通知请见纠纷回调 2.6.2 售后接口常见错误及注意事项 注意事项 新旧接口不可混用,新售后接口无法处理旧接口订单,否则会出现很多意想不到的错误 2.6.2.1 枚举值定义 (1)AfterSalesReason 枚举值 描述 1 排错/多拍 2 不想要了 3 无快递信息 4 包裹为空 5 已拒签包裹 6 快递长时间未送达 7 与商品描述不符 8 质量问题 9 卖家发错货 10 三无产品 11 假冒产品 12 其他 (2)AfterSalesState 枚举值 描述 1 用户取消 2 商家受理退款申请中 4 商家拒绝退款 5 商家拒绝退货退款 6 待买家退货 7 售后单关闭 8 待商家收货 11 平台退款中 13 退款成功 21 平台受理退款申请中 22 平台介入处理完成 23 商家受理退货申请中 24 平台受理退货申请中 2.6.2.2 Q:调用自定义交易组件售后相关接口:“创建售后单”、“用户取消售后单”、“用户上传物流信息”、“获取售后单列表”、“获取售后单详情”、“同意退款“、”同意退货“、“拒绝售后”、“上传退款凭证”、“更新售后单”等接口时报错{“errcode”: 48001,“errmsg”: “api unauthorized”} A18:未开通视频号场景经营商户号,需要先开通场景经营商户号才可以调用。 2.6.2.3 Q:调用自定义交易组件创建售后接口ecaftersale/add时报错2747002,参数错误{“errcode”:2747002,“errmsg”:"参数错误 "} A9:1.请检查“orderamt”参数,传参金额应不含邮费。 2.新旧接口不可混合调用,新接口不支持对旧接口生成的订单创建售后。 2.6.2.4 Q:调用自定义交易组件创建售后接口ecaftersale/add时报47001错误{“errcode”:47001,“errmsg”:"data format error "} A:请检查“product_info”字段,注意对应类型为“object”。 2.6.2.5 Q:调用自定义交易组件“同意退款”接口shop/ecaftersale/acceptrefund时报错“同意退款失败” {“errcode”:9700209,“errmsg”:"同意退款失败 退款失败“} A:1.该问题是订单流转状态不对导致,请严格按照文档流程进行操作调用; 2.新旧接口混合调用也会报此错误 3.“orderamt"传参为"0” 2.6.2.6 Q:用自定义交易组件创建售后接口ecaftersale/add时是报错“售后金额不合法,大于最大可退款金额”{“errcode”:2747014,“errmsg”:" 售后金额不合法,大于最大可退款金额"} A:1.请确认请求创建售后时"orderamt"金额是否大于下单时"sku_real_price"金额 2.订单已进行部分退款 码字中 稍后回来
2022-11-16 - 【干货分享】小程序自定义交易组件开通及打通视频号场景
想要在视频号中拥有交易购物能力,必须接入交易组件,目前包含标准版交易组件和自定义版交易组件两种接入方案,以下是微信给出的对比及适用场景。 [图片] 因为有赞已经为商家小程序提供了完善的电商能力,所以自定义交易组件的方案更适合有赞商家小程序来对接微信视频号。 本文主要分享自定义交易组件开通的流程以及基于有赞场景去接入的时候遇到的一些坑,希望能帮助到大家!~ 一、总流程[图片] 二 、开通微信小程序 这个不必多说,但需要注意的是 1、必须是一个非个人主体的小程序 2、小程序类目需要符合下图要求 [图片] 三、开通微信支付后续的订单创建、同步会拉起微信的收银台完成支付,所以需要小程序开通微信支付的能力 四、检查是否已经开通标准版交易组件标准版和自定义版交易组件是二选一的,所以需要先检查是否开通了标准版交易组件 如果已经开通标准版但没有完成开店任务的话还是可以在微信后台取消使用的; 但如果连开店任务也完成的话,需要先注销(目前微信还没有开放注销功能),如果是有赞的商家的话可以联系有赞客服协助处理 五、授权自定义交易组件权限因为有赞是微信小程序的三方开发服务商,所以需要商家授权自定义交易组件权限,当然如果非服务商开发的话不用关心。 六、申请接入微信接口地址:点击跳转 http请求方式:POST https://api.weixin.qq.com/shop/register/apply?access_token=xxxxxxxxx 只需要获取到access_token后请求这个接口即可,接口是同步返回的(历史上是有异步的版本的,这块如果很早接触但没有在维护过的同学要注意了),这个接口请求失败的话可能是以下原因: access_token过期小程序类目不符合已经开通了标准版交易组件七、完成商品接口调试7.1 同步商品类目至微信审核你同步商品的三级类目id以及该类目在微信这边是否要求上传相应的类目资质,需要通过获取微信全量三级类目接口来查询 微信接口地址:点击跳转 http请求方式:POST https://api.weixin.qq.com/shop/cat/get?access_token=xxxxxxxxx 根据回参中的 qualification_type 类目资质类型 product_qualification_type 商品资质类型 来判断资质是否需要上传 根据回参中的 qualification 类目资质 product_qualification 商品资质 来查询需要上传资质 将回参中的third_cat_id记录,后续同步商品时会用 需要同步的话,就要调用上传类目资质接口,这是个异步接口,审核结果通过需要通过事件回调获取 微信接口地址:点击跳转 http请求方式:POST https://api.weixin.qq.com/shop/audit/audit_category?access_token=xxxxxxxxx 这块资质图片的字段建议先通过上传图片接口换取media_id来传入 7.2 同步品牌至微信审核如果你的同步的商品是一个品牌商品,那么在同步商品之前需要先同步品牌信息至微信审核,在审核通过的回调信息中获取到brand_id,之后同步商品时传入 微信接口地址:点击跳转 http请求方式:POST https://api.weixin.qq.com/shop/audit/audit_brand?access_token=xxxxxxxxx 同样的,这块品牌的一些证照图片的字段建议先通过上传图片接口换取media_id来传入 这里导致审核失败的几个原因: 上传证照的图片有问题,譬如有水印、大小超过2M、图片不能正常显示等证照过期 7.3 同步商品至微信审核现在就可以将调用同步商品接口完成商品同步了,也是一个异步接口,需要等审核回调 微信接口地址:点击跳转 http请求方式:POST https://api.weixin.qq.com/shop/spu/add?access_token=xxxxxxxxx 这里的tips引用下微信文档上的 注意:[代码]third_cat_id[代码]请根据获取类目接口拿到,并确定其[代码]qualification_type[代码]类目资质是否为必填,若为必填,那么要先调类目资质审核接口进行该[代码]third_cat_id[代码]的资质审核;[代码]qualification_pics[代码]请根据获取类目接口中对应[代码]third_cat_id[代码]的[代码]product_qualification_type[代码]为依据,若为必填,那么该字段需要加上该商品的资质图片;若需要上传某品牌商品,需要按照微信小商店开通规则开通对应品牌使用权限。微信小商店品牌开通规则:点击跳转,若无品牌可指定无品牌(无品牌[代码]brand_id: 2100000000[代码])。库存字段[代码]stock_num[代码]注意如果是0则无法在视频号直播上架该商品。部分特殊品类商品标题需要按照规范上传,请仔细阅读,避免审核不通过。商品标题规则:点击跳转。商品详情字段[代码]desc_info.desc[代码] [代码]desc_info.imgs[代码] 虽然非必填,但一些特殊品类仍然需要上传商品详情,请仔细阅读,避免审核不通过。商品详情规则:点击跳转。 这里应该是有赞场景下去开通自定义交易组件卡点比较严重的一环了,因为有赞的商家基数较大,商品种类较多,所以让商家自己同步一个商品至微信的话很容易被微信审核拒掉。有赞这边给到的一个方案是商家在开通自定义交易组件时,系统会帮商家创建一个简单的测试商品,因为这个商品无类目、无品牌且经验证100%通过商品审核,所以这一环的卡点也就能够被顺利疏通。 八、完成订单接口调试商品同步好后,需要对这个商品用微信支付完成一次交易,并将订单同步至微信 8.1 创建、同步订单因为自定义交易组件历史版本的原因,目前存在两套创建订单并同步至微信的方案 需要后端调用创建订单的微信api获取到ticket,然后小程序获取并通过ticket调用wx.requestPayment来拉起微信收银台(可能会废弃)小程序直接将订单信息通过wx.requestOrderPayment同步给微信,并拉起微信收银台微信这边推荐使用第二套方案 8.2 支付校验1、订单全量同步 如果选择订单全量同步到微信的话,那就不需要支付校验。 2、场景校验后同步 使用scene/check方法先去判断是否为视频号场景(这块后续可能会拓展,交易组件应该还会支持视频号以外的场景),是的话再去同步订单至微信。 这里建议在自定义交易组件接口调试过程中可以全量同步,但后续正常业务下单流程还是通过scene/check方式。因为如果你的小程序商城还支持其他支付方式的话,全量同步订单的话就会有坑,原因是无论你选择哪种创建、同步订单的方式都需要唤起微信支付,这样在原本能支持多种支付方式的场景下只能使用微信支付。 8.3 发布小程序因为创建、同步订单和支付校验有一些小程序的改动,所以小程序还是需要发布版本的。仅是开通流程的话走微信开发者工具也ok。 8.4 支付同步微信接口地址:点击跳转 http请求方式:POST https://api.weixin.qq.com/shop/order/pay?access_token=xxxxxxxxx 创建订单并支付成功后调用支付同步接口,就会将订单状态流转待发货 九、完成物流接口调试9.1 订单发货微信接口地址:点击跳转 http请求方式:POST https://api.weixin.qq.com/shop/delivery/send?access_token=xxxxxxxxx 9.2 订单收货微信接口地址:点击跳转 http请求方式:POST https://api.weixin.qq.com/shop/delivery/recieve?access_token=xxxxxxxxx 十、完成售后接口调试10.1 创建售后微信接口地址:点击跳转 http请求方式:POST https://api.weixin.qq.com/shop/aftersale/add?access_token=xxxxxxxxx 10.2 更新售后微信接口地址:点击跳转 http请求方式:POST https://api.weixin.qq.com/shop/aftersale/update?access_token=xxxxxxxxx 物流、售后接口的调试,如果只是开通流程的话,建议直接后端自动调用完成,可以减少卡点,帮助快速开通。 十一、完成接入任务11.1 获取接入状态微信接口地址:点击跳转 http请求方式:POST https://api.weixin.qq.com/shop/register/finish_access_info?access_token=xxxxxxxxx 回参中的access_info内 spu_audit_success 在商品接口调试成功后会被置为1 pay_order_success 在订单接口调试成功后会被置为1 send_delivery_success 在物流接口调试成功后会被置为1 add_aftersale_success 在售后接口调试成功后会被置为1 11.2 完成所有接入任务微信接口地址:点击跳转 http请求方式:POST https://api.weixin.qq.com/shop/register/finish_access_info?access_token=xxxxxxxxx 这个接口可以在上面获取接入状态接口返回的spu_audit_success、pay_order_success、send_delivery_success、add_aftersale_success的字段都为1时一次性调用完成所有。 当下面的任务都被完成后,自定义交易组件就已经完成接入了 6:完成spu接口,7:完成订单接口,8:完成物流接口,9:完成售后接口,10:测试完成,11:发版完成 十二、自定义交易组件接入视频号场景微信接口地址:点击跳转 http请求方式:POST https://api.weixin.qq.com/shop/register/finish_access_info?access_token=xxxxxxxxx 只需要调用场景申请接口来申请视频号场景,然后等待微信申请成功就可以完成自定义交易组件接入视频号了 十三、微信后台添加视频号推广员需要去微信后台添加一个视频号为推广员,然后就可以在这个视频号关联到已经开通自定义交易组件的小程序,关联后就可以上架已经商品同步接口调用通过的商品了。 [图片] [图片]
2021-10-27 - 视频号带货指引
介绍为了帮助无货源的视频号创作者分享商品并获取收益,微信官方提供了视频号带货能力。无须开通商店,即可从已接入视频号的带货平台带货,并在直播间和个人主页分享商品。 一、选品带货1)通过微信搜索“视频号带货” --> 找到“带货中心”的小程序; [图片] 2)进入“带货中心”,同意带货协议后,关联需要带货的视频号; [图片] 3)进入「选品中心」挑选带货商品,点击商品右下方的“带货”,即可将商品添加到视频号的商品橱窗;还可以在「搜索栏」,搜索商品名称/链接,选择商品进行带货,需要特别注意: a. 仅京东、拼多多、有赞、唯品会商品支持链接能力搜索; b. 对于魔筷、当当、苏宁等商品,建议使用商品全名进行搜索,精准匹配商品。 [图片] 二、商品管理用户可以选择在“带货中心”或“视频号商品橱窗”对带货商品进行管理,两边商品状态一致。 1)带货中心1.商品管理: 上述商品添加完成后,系统会弹出「带货成功,查看已带货商品」提示,可点击「去查看」进入带货管理页面;「首页」 --> 「我带的货」,也可进入带货管理页面; [图片] 2.商品添加&移出: 在带货管理页,点击「取消带货」可将带货商品从视频号橱窗里移出,也可进入下架商品页面将商品重新添加到视频号橱窗中; [图片] 2)视频号橱窗1.商品管理: 用户也可在视频号橱窗中查看已添加商品,具体路径为:视频号 --> 视频号个人主页 --> 「创作者中心」 --> 「商品橱窗」; [图片] 2.商品添加&移出: 在视频号商品橱窗内,选择对应的商品左滑即可将该商品移出橱窗;点击左上方列表,选择「已移出商品」,点击「加入橱窗」即可将商品重新添加到橱窗内; [图片] 三、直播带货用户在视频号个人主页可以选择「发起直播」,点击「商品」即可从商品橱窗中选择对应的商品添加到直播间内; [图片] 四、带货订单及收入管理:1)查看订单及收入明细在“带货中心”首页点击「订单」可以查看成功带货的订单详情,点击「收入」可以查看待打款与已打款的佣金明细; [图片] 2)如何查看佣金的打款时间带货佣金需要等待对应平台打款后,才能打款给商家。 可点击「收入」查看佣金,显示「预估X月20日可打款」栏有金额显示,如果填写的银行卡信息正确,到日期后就会进行打款。一般下单到最终收到佣金需要45-60天;显示「平台未打款」栏,即对应第三方平台并未打款至小商店,可查看到预计的佣金款项。3)如何绑定提现银行卡(待上线)绑定银行卡及结算功能即将上线,不影响带货收入。 五、带货结算规则:不同商品平台的结算周期及规则不同,详情请查阅以下文档: 带货中心结算规则:https://developers.weixin.qq.com/community/minihome/doc/000824b9b30e500f725c2065356801 六、其他:微信视频号直播功能禁止发布的营销信息列表:https://weixin.qq.com/cgi-bin/readtemplate?t=finder_live_forbidden_list&lang=zh_CN
2021-07-02 - 视频号一键直达小程序!
视频号作为链接“公域”和“私域”的载体,不少商家早已开启了相关的运营动作,但仅仅围绕IP打造和引流直播间两个方向,难以形成更紧密的闭环。 而在近日微信团队对视频号的功能升级中,我们注意到:在视频号主页的「服务菜单」可设置直接跳转小程序,结合之前就可添加企业微信的功能,视频号将越来越趋近于企业的“微官网”。 [图片] *图源,网络。视频号主页服务菜单展示 这也意味着,“公众号+视频号+朋友圈+私域直播+小程序+企业微信+微信支付”已经形成了微信生态的商业超级闭环。 而对于美业商家来说,小程序、企业微信和视频号的彻底打通,也将让微信生态内的私域运营有了更多可能性。接下来,让我们一起看看针对此次更新,我们如何快速响应,调整运营策略吧! 视频号主页跳转小程序 视频号跳转小程序,是在视频号主页的「服务菜单」功能实现的。 已通过企业认证的视频号,可在主页添加「服务菜单」功能,支持直接跳转小程序。主要步骤如下: 第一步:登陆「视频号助手」 账号需已完成视频号蓝V认证。 第二步:侧边菜单栏点击「设置」→「服务菜单」 [图片] 第三步:点击「添加服务」自定义配置 这里的自定义服务菜单配置,我们可以选择添加「小程序」和「企业微信」 [图片] 完成了这番操作,小程序和企业微信的入口就能够在视频号主页展示了。 而这样的展示对商家来说,能够缩短视频号的引流路径,让用户能够一键直达小程序商城,更高效地触达客户,降低公域流量的流失,且提升用户粘性。商家可通过视频号的内容运营,实现更精准、更高效获客。 使用企微推荐视频号 视频号关联企业微信和个人微信的客服功能,已经有不少商家都配置过了。但今天我们要说的,是如何通过企业微信,把视频号和视频号直播推荐给更多顾客。 让品牌和商家能够通过企业微信源源不断地向客户输出内容,实现更高频、即时地触达。具体操作步骤如下: 第一步:进入企微「工作台」点击「视频号」 亦可直接在搜索栏搜索「视频号」进入应用。 [图片] 第二步:点击「绑定视频号」 点击后搜索对应视频号绑定。注意企微主体需与视频号主体一致方可完成绑定。 [图片] 第三步:选择视频号分享,或进入对应视频分享 进入分享的视频号或单个视频,点击右上角“…”按钮,即可推荐视频号名片或具体视频给客户、客户群及客户朋友圈。 [图片] 当前这两个功能的打通,线下门店新增了流量入口,小程序商城也将承担更为重要的营销意义。
2022-03-21 - 视频号重磅更新,可以直接跳转小程序了。
近期,视频号重大更新,不用自定义交易组件也可以跳转小程序了。视频号的流量将全面导入小程序。 视频号推出了服务菜单功能,只要是认证的企业号,可以添加客服和小程序任意页面。一共可以添加6个。 需要注意的是,小程序页面路径一定要加上后缀【.html】,不然可能打开就是未知页面了。 [图片] [图片]
2022-03-02 - 视频号助手配置服务菜单无法跳转小程序
https://channels.weixin.qq.com/platform/login 扫码进入,配置服务菜单,选择小程序,输入小程序appid以及页面路径,保存。进入手机微信视频号,点击配置好的菜单,白屏。若任何路径不配置,是可以进小程序首页的。(小程序页面路径是没问题的,是开发者从微信开发者工具左下角复制出来的,视频号也是关联了当前小程序主体的)
2022-02-26 - 商家拒绝售后,某订单再次发起售后,显示:“商品不能售后,检查该商品是否有进行中的售后单”,为什么?
获取售后详情返回的结果:此时的 status 已经为 5 [图片] 外部订单号:1216502632980846000692 外部售后订单号:138165026832308460000002696
2022-04-18 - 拒绝售后单,再次发起售后,是新生成售后单还是更新售后单?视频号更新售后返回来一个未知的回调类型?
问题1 场景: 1.调用生成售后单接口,生成售后单 2.调用拒绝售后接口,拒绝售后 3.小程序端想修改售后继续发起售后,调用更新售后单接口,报错, 外部售后单号和内部售后单号不匹配 rid: 624eb74f-64089f6f-25906050 [图片] 4.若果拒绝的售后单不支持更新,此时调用生成售后单接口,生成新售后单报错, 售后金额不合法,大于最大可退款金额 rid: 624eb7e4-4143b104-5555188b [图片] 5.以上步骤都不行之后,通过视频号订单列表,进入售后详情页,有一个 修改申请按钮,此时服务单收到一个类型为aftersale_update_order的回调信息 ,文档并没有这个回调类型的说明 [图片] 6.请问小程序的售后被拒绝后,应该怎么走微信的流程,求解答,谢谢
2022-04-08 - 自定义交易组件视频号场景下单必须选正常快递,我们产品是 语言培训类目的,这个产品实际是不需要发货的?
[图片] [图片] 1.对于这样的订单后续我们该怎么处理、? 2.还有后续订单一直不发货会对结算有影响么? 哪位大佬清楚帮忙告知一下!
2022-04-02 - 自定义交易组件售后,订单发货之后,为什么退款不能退运费了?
退款参数 3 分钱,其中 2 分应该是运费,1 分应该是商品价格,这里出现了退款失败的情况 {"openid":"oMPFi5fFZm7-1kH6OzYilUMtG4Kg","order_id":"3302429977949635072","orderamt":3,"out_aftersale_id":"1281376","out_order_id":"s211979676","product_info":{"out_product_id":"101089","out_sku_id":"qjt4251cbi89kjdqtfcc3","product_cnt":1},"refund_reason":"不想要了","refund_reason_type":2,"type":1} result: {"errcode":2747014,"errmsg":" 售后金额不合法,大于最大可退款金额"} 订单信息 { "errcode": 0, "order": { "order_id": 3302429977949635072, "out_order_id": "s211979676", "openid": "oMPFi5fFZm7-1kH6OzYilUMtG4Kg", "create_time": "2022-04-06 23:41:46", "order_detail": { "product_infos": [ { "out_product_id": "101089", "out_sku_id": "qjt4251cbi89kjdqtfcc3", "product_cnt": 1, "sale_price": 1, "title": "驼鹿", "head_img": "https:\/\/store.mp.video.tencent-cloud.com\/161\/20304\/snscosdownload\/SH\/reserved\/624db4bc000c25af00000000696fb40b000000a100004f50", "path": "pages\/productDetail\/productDetail?productId=101089", "sku_attrs": [ ], "sku_real_price": 1, "can_after_sale": true } ], "pay_info": { "prepay_id": "wx0623414792947178cacc673bcc9f380000", "prepay_time": "2022-04-06 23:41:47", "transaction_id": "4200001366202204060153695897", "pay_time": "2022-04-06 23:41:55", "pay_method_type": 0, "out_trade_no": "120330242997794963507200000", "midas_order_id": "" }, "price_info": { "freight": 2, "discounted_price": 0, "final_price": 3, "order_price": 3 }, "pay_info_list": [ ], "promotion_info": { } }, "delivery_detail": { "delivery_type": 1, "delivery_list": [ { "waybill_id": "432473534547346", "delivery_id": "YTO", "product_info_list": [ { "out_product_id": "101089", "out_sku_id": "qjt4251cbi89kjdqtfcc3", "product_cnt": 1 } ] } ], "ship_done_time": "2022-04-06 23:42:47" }, "status": 30, "path": "pages\/order\/order-list", "update_time": "2022-04-06 23:42:55", "address_info": { "receiver_name": "韩冬", "detailed_address": "沪南路2688", "tel_number": "86-13085093976", "country": "中国", "province": "上海市", "city": "市辖区", "town": "浦东新区" }, "scene": 1000, "fund_type": 1, "settlement_info": { "max_handing_fee": 0, "max_unsplit_amount": 3, "settlement_list": [ { "type": 0, "uin": 3848466736, "amount": 3 } ], "handing_fee_dyn": 0, "merchant_settle": 3, "settle_status": 1 }, "expire_time": 1649263306, "aftersale_duration": 5, "receive_duration": 15 } }
2022-04-07 - 自定义版交易组件 1010004错误pay_info字段异常,请检查json及字段合法性?
自定义版交易组件 生成订单https://api.weixin.qq.com/shop/order/add 接口一直报1010004错误那位大佬帮忙看看什么问题啊? 参数对了很多遍找不出问题在哪 pay_info字段异常,请检查json及字段合法性 rid: 6239d2e7-4a950b75-0452fffc 下面是我的请求参数 {"create_time":"2022-03-22 21:43:32","out_order_id":"146","openid":"oo58i5EDIR-xWmNjXW-vyONZX41Q","path":"\/pages\/order\/orderDetail\/orderDetail?order_id=146","scene":1177,"order_detail":{"product_infos":[{"out_product_id":"245","out_sku_id":"1529","product_cnt":1,"sale_price":3900,"sku_real_price":3900,"path":"pages\/goods\/goodsDetail\/goodsDetail?goods_id=245","title":"小猪努比 NB 830便携式奶粉格 三层 三层","head_img":"https:\/\/shop-img.hanbaoxuan.com\/1624357783.jpg"}],"pay_info":{"pay_method_type":0},"price_info":{"order_price":3900,"freight":0}},"delivery_detail":{"delivery_type":1},"address_info":{"receiver_name":"liudaoning","detailed_address":"北京市市辖区东城区sadasd","tel_number":"18701519716"},"fund_type":0,"expire_time":1648043012}
2022-03-22