简介亲测可用的易优第三方商家插件面向易优CMS电商平台运营者与二次开发者核心是把普通会员升级为商家会员提交资料与资质管理员审核后获得商家权限可在商城发布商品、管理订单并处理客户咨询适合希望扩充商品与服务品类、为会员提供轻量创业入口的场景。压缩包共526个文件约7.47MB以JavaScript、CSS、PNG、HTM等前端与页面资源为主同时包含PHP业务代码、SQL脚本及字体图标文件兼顾后台逻辑、模板渲染与移动端展示。包内可见application、template、weapp等目录分别对应业务处理、前端模板和微信小程序对接便于理解会员转商家、商品与订单管理、多渠道运营的实现脉络。已有135人学习或下载可用于本地部署测试、功能验证与界面定制也适合作为插件结构分析和二次开发的参考。1. 会员变商家这一刀切在会员表还是商家表上后台只有一个会员列表会员想卖货只能给他开管理员账号——这是很多中小商城站跑到半年后必然撞上的一堵墙。易优第三方商家会员变商家商城插件解决的正是这一步让已有会员在前台提交入驻申请审核通过后拿到一个独立的商家后台能发自己的商品、看自己的订单平台按佣金抽成。它不是把会员改成管理员也不是简单加个 is_seller 字段就完事真正的难点在数据隔离和订单拆单这两处。适合手里已经跑着一套易优商城、想用低改造成本切到多商户模式的人。下面按我实际拆包、装表、改模板的顺序讲。2. 拆包先看骨架四类文件、五张表、三个字段的咬合关系拿到压缩包别急着往站点根目录覆盖。插件类资源最容易翻车的地方不是它功能不全而是它默认你的站点是“干净”的——表前缀没改过、会员模块没二开过、后台菜单没手工加过。只要其中一条不成立直接覆盖就是白屏。我一般的顺序是备份、看目录、对表、挂菜单、清缓存顺序颠倒一次就得回滚一次。2.1 解压到临时目录先把四类文件分清楚插件包里通常混着四类东西应用模块PHP 业务代码、静态资源js/css/图片、数据库脚本install.sql 之类、模板文件前台会员中心和后台模板各一套。它们的覆盖风险完全不同必须先拆开看再决定哪些直接覆盖、哪些手工合并。# 1. 整站备份插件改动面大后悔药先备好 cd /www/wwwroot/example.com tar -czf /backup/site_$(date %F).tar.gz ./application ./template ./public ./extend mysqldump -uroot -p你的密码 --default-character-setutf8mb4 --single-transaction shop_db /backup/db_$(date %F).sql # 2. 解压到临时目录绝不直接覆盖站点 mkdir -p /tmp/plugin cd /tmp/plugin unzip -o ~/upload/shop_seller.zip # 3. 看三层目录结构确认模块名和表前缀 find . -maxdepth 3 -type d | sort grep -rn ey_ ./ --include*.sql | head -20这三段命令的作用不一样。第一段是保命--single-transaction让 InnoDB 在不锁表的前提下导出一致性快照站点还在跑也不影响./extend一定要带上不少插件的助手函数丢在这里。第二段的-o是覆盖解压只在临时目录用别在站点目录用。第三段最关键grep ey_是在确认插件写的表前缀和你站点一致很多“装完报 1146 表不存在”的根因就在这一步没做。分完类对应关系大致是这样包内目录落到站点位置处理方式风险点application/shop、application/sellerapplication/可整体覆盖与已有模块同名会冲突public/static/pluginpublic/static/直接覆盖无sql/install.sql手工执行逐条执行不整包跑缺 IF NOT EXISTS 会报重复列template/admin、template/defaulttemplate/手工比对合并覆盖会丢二开application/admin/controller 下的公共文件application/admin/手工合并覆盖直接白屏表格里后两行是血泪经验。application/admin/下的公共控制器比如登录、首页框架、公共继承类几乎是所有插件都想改一嘴的地方整包覆盖等于把站点的后台二开全抹掉。我的做法是先diff一遍只把插件新增的方法贴进去。# 比对后台公共控制器只看差异不覆盖 diff -u ./application/admin/controller/Index.php /tmp/plugin/application/admin/controller/Index.phpdiff -u输出统一格式-开头是站点原有、开头是插件新增。逐个 hunk 看只保留插件新增的商家相关方法原有方法一行都不动。2.2 五张核心表加会员表上的三个字段这套插件的多商户模型不复杂核心就五张表商家主表入驻申请和店铺信息合一、商家店铺配置表、商品表上的商家归属一般是在原商品表加字段不新建表、订单表上的商家归属、结算表。会员表只加三个字段用来标记“这个人是不是商家”。-- 商家主表一条记录 一次入驻申请 一个店铺 CREATE TABLE IF NOT EXISTS ey_shop_seller ( seller_id int(10) unsigned NOT NULL AUTO_INCREMENT, users_id int(10) unsigned NOT NULL DEFAULT 0 COMMENT 关联 ey_users.users_id, shop_name varchar(60) NOT NULL DEFAULT COMMENT 店铺名前台展示, real_name varchar(30) NOT NULL DEFAULT COMMENT 联系人, mobile char(11) NOT NULL DEFAULT COMMENT 审核回访用, license_img varchar(255) NOT NULL DEFAULT COMMENT 执照图相对路径, commission_rate decimal(5,2) NOT NULL DEFAULT 5.00 COMMENT 平台佣金百分比, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待审 1通过 2驳回 3停用, audit_admin int(10) unsigned NOT NULL DEFAULT 0 COMMENT 审核人, add_time int(10) unsigned NOT NULL DEFAULT 0, audit_time int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (seller_id), UNIQUE KEY uk_users (users_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商家入驻主表;uk_users这个唯一索引是整套设计的支点一个会员只能有一个店铺从数据库层面堵死重复申请。commission_rate用decimal(5,2)而不是 float后面算佣金时少掉一堆精度麻烦。status里的 3停用和 2驳回要分开驳回是流程没走完停用是已经开过店被平台下线业务含义不同。会员表这边加字段前先查一遍别硬跑-- 先确认字段是否已存在避免 Duplicate column name SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA DATABASE() AND TABLE_NAME ey_users AND COLUMN_NAME IN (is_seller,seller_id,seller_status); -- 确认没有同名后才执行 ALTER TABLE ey_users ADD COLUMN is_seller tinyint(1) NOT NULL DEFAULT 0 COMMENT 0普通会员 1商家, ADD COLUMN seller_id int(10) unsigned NOT NULL DEFAULT 0 COMMENT 绑定 ey_shop_seller.seller_id, ADD COLUMN seller_status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0未申请 1待审 2通过 3驳回;三个字段里is_seller是给前台判断用的快捷位头像旁边要不要显示“商家中心”入口seller_id是真正的外键seller_status记录流程状态。冗余是有意为之商城首页、会员中心、商品详情页到处都要判断“是不是商家”每次都去 join 商家表页面一多就是 N1 查询。用冗余位换查询量在 CMS 这种模板标签到处嵌的业务里是划算的。商品表和订单表同理各加一个seller_id加索引ALTER TABLE ey_shop_goods ADD COLUMN seller_id int(10) unsigned NOT NULL DEFAULT 0, ADD KEY idx_seller (seller_id); ALTER TABLE ey_shop_order ADD COLUMN seller_id int(10) unsigned NOT NULL DEFAULT 0, ADD KEY idx_seller (seller_id);注意DEFAULT 0平台自营商品的 seller_id 就是 0。这样整套查询逻辑只有一条规则seller_id 0是平台大于 0 是某个商家不用额外加“是否自营”字段模板里判断也简单。2.3 后台菜单和权限节点顺序反了会白屏菜单和权限是安装阶段最容易漏的一步。插件包里的菜单 SQL 通常是INSERT INTO ey_admin_menu但很多人只跑建表脚本忘了跑菜单脚本结果登录后台一片空白——不是报错是当前管理员没有新菜单的权限框架直接渲染了个空框架。-- 后台菜单先插父级拿到自增ID再插子级引用它 INSERT INTO ey_admin_menu (menu_id,pid,name,controller,action,sort_order,status) VALUES (9001, 0, 商家管理, Seller, index, 60, 1); INSERT INTO ey_admin_menu (pid,name,controller,action,sort_order,status) VALUES (9001, 入驻审核, Seller, audit, 1, 1), (9001, 商家列表, Seller, lists, 2, 1), (9001, 结算管理, Settle, index, 3, 1); -- 权限节点把新控制器登记进去否则非超管看不到 INSERT INTO ey_admin_auth (title,controller,action,status) VALUES (商家管理, Seller, index, 1), (入驻审核, Seller, audit, 1), (商家列表, Seller, lists, 1);菜单表用固定menu_id是故意的方便回滚时按 ID 删干净。表名以你站点实际的为准易优不同版本后台菜单表命名有过变化跑之前先SHOW TABLES LIKE %menu%确认一下。菜单挂完必须清缓存这一步漏了前面全白做# 清运行时缓存 模板编译缓存 rm -rf ./runtime/cache/* ./runtime/temp/* ./runtime/log/* chmod -R 755 ./runtime ./public/uploadsruntime/temp是模板编译产物不清的话你改的后台模板根本不生效看到的还是旧页面很容易误判成“插件没装成功”。public/uploads给 755 是因为商家上传执照图要走这个目录权限不对会静默失败——表单提交成功但图片是空的。3. 会员升级为商家的完整链路从申请表到店铺初始化装完表只是把地基打好真正决定这套插件能不能用的是这条链路会员在前台填表、后台审核、审核通过的一瞬间完成身份转换和店铺初始化。这条链路上任何一步不是原子的就会出现“会员标记成商家了但店铺没建起来”这种半残状态用户登录进去直接报错。3.1 入驻申请表单哪些字段必须在服务端卡死前台表单看着简单实际校验分三层浏览器端体验、控制器端安全、数据库端兜底。新手常犯的错是只做第一层改个前端 disabled 就能提交空店铺名。字段类型校验规则为什么卡死shop_namevarchar(60)必填2–30 字全站唯一前台店铺页按名字做 SEO 路径mobilechar(11)必填正则^1[3-9]\d{9}$审核回访唯一联系方式license_imgvarchar(255)必填只存相对路径存绝对路径换域名全挂commission_ratedecimal(5,2)后端写死不让前台传前台传等于让商家自己定抽成users_idint从 session 取不接收表单值接收表单值等于可替他人申请最后一行是重点。users_id一定要从登录态里取绝对不要从前端 POST 里取。这类插件早期版本里十个有八个是input(users_id)直接用的等于给了一个“以任意会员身份入驻”的越权口子。// 前台提交入驻申请三件事必做 $usersId session(users_id); // 1. 身份只认 session if (empty($usersId)) return json([code0,msg请先登录]); $has Db::name(shop_seller)-where(users_id, $usersId) -whereIn(status, [0,1]) // 2. 待审或已通过都不允许再提 -count(); if ($has) return json([code0,msg已有申请在处理中]); $data [ users_id $usersId, shop_name trim(input(post.shop_name/s)), mobile input(post.mobile/s), license_img str_replace($domain, , input(post.license_img/s)), // 3. 存相对路径 status 0, add_time time(), ]; Db::name(shop_seller)-insert($data);第二段的whereIn(status,[0,1])配合数据库唯一索引形成双保险应用层挡住重复提交数据库层挡住并发提交。第三段的路径处理容易被忽略上传组件返回的往往是带域名的完整 URL直接存进库将来站点换域名或者上 CDN所有历史执照图全部 404。存相对路径/uploads/seller/2024/xxx.jpg模板里拼域名是更稳的做法。3.2 审核通过那一下事务里必须干完五件事这一节是整套插件的核心。后台点“通过”按钮需要同时完成改申请状态、改会员标记、初始化店铺配置、建默认运费模板、写审核日志。少一件商家登录后台就会撞上空数据。// 后台审核通过五步必须在一个事务里任何一步失败全部回滚 Db::startTrans(); try { $seller Db::name(shop_seller)-where(seller_id, $sellerId)-lock(true)-find(); if (!$seller || $seller[status] ! 0) { throw new \Exception(该申请不存在或已处理); } // 1. 改申请状态 Db::name(shop_seller)-where(seller_id, $sellerId)-update([ status 1, audit_admin session(admin_id), audit_time time(), ]); // 2. 改会员标记三个字段一起动别只改 is_seller Db::name(users)-where(users_id, $seller[users_id])-update([ is_seller 1, seller_id $sellerId, seller_status 2, ]); // 3. 初始化店铺配置不建这条商家后台读配置就是 null Db::name(shop_seller_config)-insert([ seller_id $sellerId, shop_banner , shop_desc $seller[shop_name], free_shipping 0, ship_fee 8.00, open_state 1, ]); // 4. 建默认运费模板商家发第一个商品时必须有模板可选 Db::name(shop_freight)-insert([ seller_id $sellerId, name 默认模板, first_piece 1, first_fee 8.00, add_piece 1, add_fee 2.00, ]); // 5. 审核日志出问题能查是谁点的 Db::name(shop_seller_log)-insert([ seller_id $sellerId, admin_id session(admin_id), content 审核通过并初始化店铺, add_time time(), ]); Db::commit(); return json([code1,msg审核通过]); } catch (\Exception $e) { Db::rollback(); return json([code0,msg$e-getMessage()]); }几个参数值得单独说。lock(true)会生成SELECT ... FOR UPDATE两人同时点审核时后一个会阻塞到前一个提交然后因为status ! 0被拦下来避免重复初始化。第 3 步的ship_fee是字符串8.00不是浮点 8.0因为字段类型是 decimalPHP 里传 float 进去某些 MySQL 版本会做四舍六入出现7.999存成7.99的问题。第 5 步的日志表看着可有可无但真出纠纷时商家说“我明明通过了”这是唯一的黑匣子。驳回路径简单得多但有个细节驳回后users.seller_status要改成 3同时不要动is_seller和seller_id。因为被驳回的会员可能之前已经通过过一个店铺后来停用的那种把seller_id清 0 会把他的历史订单归属打断。3.3 商家后台的登录态seller_id 只能从 session 来商家后台是独立入口通常是/seller.php或者index.php?mseller走的是独立登录和会员中心共用ey_users但用不同的 session key。这里最常见的错误是把seller_id放在 URL 上到处传——/seller/goods/lists?seller_id12改个数字就能看别人家的商品和订单。// 商家后台基础控制器所有子控制器都继承它 class Base extends Controller { protected $sellerId 0; protected $seller []; public function __construct() { parent::__construct(); $usersId session(seller_users_id); // 商家登录时写入的 key if (empty($usersId)) $this-redirect(seller/login/index); $seller Db::name(shop_seller) -where(users_id, $usersId)-where(status, 1)-find(); if (empty($seller)) $this-error(店铺状态异常); $this-seller $seller; $this-sellerId $seller[seller_id]; // 后续所有查询用它不接收外部传参 } }这个基类的意义在于$this-sellerId是服务端解析出来的任何前台参数都改不动它。后面所有的商品列表、订单列表、结算查询统一用where(seller_id, $this-sellerId)越权问题在这层就被掐死了。我在排查章里还会再提一次因为这真的是一线最常翻车的地方。4. 商品归属与订单拆单多商户数据隔离怎么真正落地到这一步商家能登录了、能进后台了但货还没上、单还没拆。第 4 章讲的就是这套插件最“硬”的部分商品怎么绑商家、一个订单里混着三个商家的货时怎么拆、拆完的钱怎么结算。这三件事没做对多商户商城就是个大号的自营站加几个摆设。4.1 商品挂 seller_id查询拦截放基类而不是每个方法商品表加了seller_id之后商家后台的每一次商品查询都必须带上它。有两种写法一种是在每个控制器方法里手写 where一种是在模型层做全局作用域。我推荐后者因为前者一定会漏——商家后台十几个方法总有一个忘了加。// 模型层给商品模型加商家作用域 class GoodsModel extends Model { protected $name shop_goods; // 在模型初始化时统一注入业务代码不用再管 protected static function init() { // 只对商家后台生效前台商城不走这条 if (defined(SELLER_SCOPE) SELLER_SCOPE session(seller_users_id)) { $sellerId Db::name(shop_seller) -where(users_id, session(seller_users_id)) -value(seller_id); self::addGlobalScope(seller, function ($query) use ($sellerId) { $query-where(seller_id, $sellerId); }); } } }addGlobalScope是 ThinkPHP 5.1 之后就有的能力这里的关键是SELLER_SCOPE常量——在商家后台的入口文件里定义它前台商城入口不定义。这样同一个模型前台查全量商品商家后台自动只看自己的业务代码零改动。同一种模型两种作用域避免了“前台商品列表被商家作用域误伤”这类更难查的问题。商品保存时还有一层校验不能光靠作用域// 商品编辑先验归属再落库 $goodsId input(post.goods_id/d); if ($goodsId) { $own Db::name(shop_goods) -where(goods_id, $goodsId) -where(seller_id, $this-sellerId) // 归属校验必须在 where 里 -count(); if (!$own) return json([code0,msg无权操作该商品]); }作用域解决的是“列表看到什么”这条校验解决的是“能不能改”。两者缺一不可作用域在模型层保存逻辑如果走了Db::name()原生查询就绕过去了显式 where 放在这里才是真正的最后一道闸。4.2 订单拆单父单收款、子单履约一个会员购物车里放了 A 商家的鞋和 B 商家的帽子下单后必须拆成两个子订单——因为发货、退款、售后都是分开的。但支付只能付一次所以正常做法是一张父订单负责支付和金额汇总N 张子订单负责履约靠parent_order_id关联。// 下单拆单按 seller_id 分组每组一张子订单 $cart Db::name(shop_cart)-where(users_id, $usersId)-select(); $groups []; foreach ($cart as $item) { $groups[$item[seller_id]][] $item; // seller_id 为 0 的是平台自营 } $parentSn date(YmdHis) . mt_rand(1000, 9999); Db::startTrans(); try { // 父订单只记总金额和支付状态 $parentId Db::name(shop_order)-insertGetId([ order_sn $parentSn, users_id $usersId, total_amount $totalAmount, pay_status 0, seller_id 0, // 父单不属于任何商家 is_parent 1, add_time time(), ]); foreach ($groups as $sellerId $items) { $subSn $parentSn . _ . $sellerId; // 子单号带上商家对账时一眼看出 Db::name(shop_order)-insert([ order_sn $subSn, parent_order_id $parentId, users_id $usersId, seller_id $sellerId, total_amount array_sum(array_column($items, subtotal)), pay_status 0, is_parent 0, add_time time(), ]); // 订单明细同样带上 seller_id佣金按明细行算 // ... 明细写入略 } Db::commit(); } catch (\Exception $e) { Db::rollback(); }is_parent这个字段是整套拆单逻辑的开关支付回调只处理is_parent 1的父单然后把pay_status同步给所有子单发货、收货、退款只处理is_parent 0的子单。父子单都放在同一张表里好处是订单查询不用 union坏处是所有查询都必须带is_parent条件——这是典型的用一点查询复杂度换表结构的简洁。子单号用父单号_sellerId的拼接方式看起来土但对账时极其省事拿到任意一个子单号split 一下就知道它属于哪张父单、哪个商家不用连表。4.3 佣金结算表的生成时机与金额精度佣金不能在下单时就算死必须等订单完成收货 过售后期才入账。我见过不少插件是支付成功就生成结算记录结果商家退款了还得回头冲账越做越乱。合理的设计是订单状态变“已完成”时按明细行逐条算佣金汇总进结算表。-- 结算表一条记录 一个商家一个结算周期 CREATE TABLE IF NOT EXISTS ey_shop_settle ( settle_id int(10) unsigned NOT NULL AUTO_INCREMENT, seller_id int(10) unsigned NOT NULL DEFAULT 0, order_count int(10) unsigned NOT NULL DEFAULT 0, total_amount decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT 订单总额, commission decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT 平台佣金, settle_amount decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT 商家应得, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待结算 1已结算, add_time int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (settle_id), KEY idx_seller (seller_id,status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商家结算表;金额计算全部用分整数在 PHP 里算最后写库时再除 100 转 decimal// 订单完成时算佣金先转分算完再转回元 $rate (float)$seller[commission_rate]; // 例5.00 表示 5% $amount (int)round($subtotal * 100); // 元转分四舍五入 $commission (int)round($amount * $rate / 100); // 分上算佣金 $settleAmount $amount - $commission; // 商家应得不会出现负数 Db::name(shop_settle)-insert([ seller_id $sellerId, total_amount $amount / 100, commission $commission / 100, settle_amount $settleAmount / 100, status 0, add_time time(), ]);round()放在转分之后而不是之前是为了让误差只出现一次。如果直接在元上算19.99 * 0.05浮点结果是0.9994999999999999存进 decimal 会变成1.00一百单累积下来差几块钱商家一定会来找你对账。5. 上线后才暴露的五类问题排查与避坑前面四章讲的是“怎么装对”这一章讲“装完了为什么还是不对”。有些问题在测试环境永远遇不到只有真实订单、并发点击、用户乱改 URL 的时候才会浮出来。5.1 现象装完后台一片空白也不报错原因几乎全是菜单和权限节点没挂上。后台框架会先查当前管理员的权限列表列表为空时不是报 403而是直接渲染一个空容器看起来很吓人。另外还有一种情况是application/admin/controller被整包覆盖公共方法丢了。解决分两步。先确认菜单表有没有数据SELECT * FROM ey_admin_menu WHERE name LIKE %商家%查不到就说明菜单 SQL 没跑。再确认当前账号是不是超管非超管要去权限组里勾上新节点。如果两步都对还是白屏把runtime/temp清干净再看——模板编译缓存不清改了模板等于没改。5.2 现象商家把 URL 里的 id 改成别人的能看到别家商品原因很直接查询条件用的是input(get.seller_id)或者input(get.goods_id)直接查没有叠加归属条件。这类插件早期版本几乎都有因为开发者本地测试只用一个商家账号根本发现不了。解决是把归属校验下沉到基础控制器所有子控制器统一从$this-sellerId取值控制器里禁止出现input(get.seller_id)这种写法。上线前可以自查一遍grep -rn input(get.seller_id) ./application/seller/有输出就是有口子。5.3 现象支付成功了父单显示已付款子单还是待付款原因在支付回调只更新了父单。回调拿到的是父单号如果代码里写的是where(order_sn, $sn)-update([pay_status1])只会命中一行子单原地不动商家后台永远等不到“待发货”的订单。解决是在回调里加一次级联先按父单号查出所有子单 ID再批量更新同时给每个子单记一条支付流水方便退款时按子单回滚。这段逻辑必须和父单更新放在同一个事务里否则会出现父单付了、子单没更的中间态靠人工补数据。5.4 现象佣金结算时金额差几分钱商家反复来问原因是用 float 做金额运算或者佣金在元上直接算。1850.50 的 3% 在浮点里是55.514999999999999round之后可能得 55.51 也可能是 55.52取决于 PHP 版本和运算顺序。解决是全程整数分运算读出来(int)round($v*100)算完$v/100写回。再补一条对账 SQL每天跑一次-- 结算表金额与订单明细逐单核对差额不为 0 的挑出来人工看 SELECT s.seller_id, s.settle_amount, SUM(o.settle_amount) AS detail_sum, s.settle_amount - SUM(o.settle_amount) AS diff FROM ey_shop_settle s JOIN ey_shop_order o ON o.seller_id s.seller_id WHERE o.settle_status 1 GROUP BY s.seller_id, s.settle_amount HAVING ABS(diff) 0.001;HAVING里用 0.001 而不是 0是因为 decimal 比较在极少数情况下会有末位差异卡太死会天天报假警。5.5 现象会员升级成功后原来的积分、等级、优惠券全没了原因是插件在改会员表时用了update整行覆盖把users表里原有的积分、等级、余额字段一起写成了默认值。这类 bug 极隐蔽因为会员本人往往几天后才想起来积分不对。解决很简单但必须做审核通过时只update那三个字段用数组精确指定绝不整行赋值。同时给会员表加一个简单的触发器或者日志记录记录变更前后的值。我在自己站上还加了一层审核前先SELECT出原积分行写进日志表出问题时能还原。6. 上线前的三张自检表和一次小流量探路插件跑通和插件能上线是两件事。跑通只需要功能能点上线要考虑权限、精度、并发、回滚。我在交付前固定跑一遍三张自查表谁看都能照着做。检查项检查方式通过标准越权访问用 A 商家账号改 URL 里的商品/订单 ID全部返回“无权操作”无一条能读到数据金额精度下三单含多商家商品的订单逐单核对父单金额 子单之和佣金合计误差为 0状态一致支付、发货、收货、退款各走一遍父单和所有子单状态同步无滞留中间态权限边界用普通会员账号请求商家后台接口全部跳登录无 200 返回回滚能力手动删掉新增表、恢复备份站点恢复到装插件之前无残留菜单第二张表是余额和佣金的口径确认commission_rate谁定、什么时候改、改了之后存量订单按新费率还是旧费率算。我的做法是订单落库时就把费率快照存进订单明细商家后来改费率不影响历史单这条一定要在需求文档里写死。第三张是时间表结算周期用 T7 还是 T15直接影响商家的现金流预期别等上线了再改。探路那次我用的是最笨的办法先只放开 5 个商家入驻跑满一个月盯着三个指标——子单支付同步延迟、结算差额、商家后台 500 错误数。第三周发现过一次子单同步失败原因是支付回调超时后重试第二次回调时父单已经pay_status 1代码直接 return 了子单没补上。后来改成幂等处理父单已付款但子单未同步时继续执行同步逻辑只跳过重复的流水写入。这个坑靠压测是测不出来的只能是真实流量喂出来。从那以后我每次装这类多商户插件都会先跑一遍那三张自查表尤其是越权那栏用两个商家账号互改 ID 试一遍确认没口子才敢开支付。上线前多花两小时比上线后半夜爬起来补数据划算得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?