
最近对独立部署的医药商城 小程序做了一些功能的后端重构,涉及佣金计算明细、药品退款流程、后台菜单结构三块。这篇文章记录一下方案设计和踩过的坑。
注:本次修改全部使用 codebuddy二开,先读取整个项目的逻辑,再喂给AI,让AI提出疑问,解答完所有的疑问,再让AI开始改代码,基本上准确率能达到80%意思,后续测试简单修改,即可。
一、佣金计算明细:从黑盒到可审计
问题背景
原来的佣金模块只存最终金额,运营和分销商看不到计算过程。分销商问"这笔订单为什么是这个数",只能人工翻订单、翻比例、拿计算器现场算。订单量一大,客诉成本很高。
方案设计
核心思路是下单时固化计算快照,页面只做只读展示。具体做法:
下单环节计算佣金时,同步生成可读的公式描述(如"实付金额 × 分销比例")存入快照字段
收货/结算时直接读取快照写流水,不再重新计算
前端弹窗渲染快照中的公式和明细,不参与任何计算逻辑
这样做的好处是:金额可解释、计算过程不可篡改、前端只负责展示。
* 推广订单明细列表查询
* @param array $where
* @param string $nickname
* @param int $page
* @param int $limit
* @return array
*/
public function getCommissionOrderList(array $where, string $nickname, int $page, int $limit): array
{
$query = $this->getModel()->alias('o')
->where(function ($q) {
$q->where('o.spread_uid', '>', 0)
->whereOr('o.brokerage_status', '<>', '');
});
if (!empty($where['brokerage_status'])) {
$query->where('o.brokerage_status', $where['brokerage_status']);
}
if (!empty($where['brokerage_status_not_normal'])) {
$query->where('o.brokerage_status', '<>', 'normal')->where('o.brokerage_status', '<>', '');
}
if (!empty($where['order_id'])) {
$query->where('o.order_id', 'like', '%' . $where['order_id'] . '%');
}
// 分销员昵称搜索:通过 user 表关联
if (!empty($nickname)) {
$query->join('eb_user u', 'o.spread_uid = u.uid', 'LEFT')
->where('u.nickname', 'like', '%' . $nickname . '%');
}
return $query->field('o.id,o.order_id,o.spread_uid,o.one_brokerage,o.pay_price,o.brokerage_status')
->page($page, $limit)
->order('o.id', 'desc')
->select()
->toArray();
}
/**
* 推广订单明细总数
* @param array $where
* @param string $nickname
* @return int
*/
public function getCommissionOrderCount(array $where, string $nickname): int
{
$query = $this->getModel()->alias('o')
->where(function ($q) {
$q->where('o.spread_uid', '>', 0)
->whereOr('o.brokerage_status', '<>', '');
});
if (!empty($where['brokerage_status'])) {
$query->where('o.brokerage_status', $where['brokerage_status']);
}
if (!empty($where['brokerage_status_not_normal'])) {
$query->where('o.brokerage_status', '<>', 'normal')->where('o.brokerage_status', '<>', '');
}
if (!empty($where['order_id'])) {
$query->where('o.order_id', 'like', '%' . $where['order_id'] . '%');
}
if (!empty($nickname)) {
$query->join('eb_user u', 'o.spread_uid = u.uid', 'LEFT')
->where('u.nickname', 'like', '%' . $nickname . '%');
}
return $query->count();
}
/**
* 推广订单明细导出(不分页)
* @param array $where
* @param string $nickname
* @return array
*/
public function getCommissionOrderListAll(array $where, string $nickname): array
{
$query = $this->getModel()->alias('o')
->where(function ($q) {
$q->where('o.spread_uid', '>', 0)
->whereOr('o.brokerage_status', '<>', '');
});
if (!empty($where['brokerage_status'])) {
$query->where('o.brokerage_status', $where['brokerage_status']);
}
if (!empty($where['brokerage_status_not_normal'])) {
$query->where('o.brokerage_status', '<>', 'normal')->where('o.brokerage_status', '<>', '');
}
if (!empty($where['order_id'])) {
$query->where('o.order_id', 'like', '%' . $where['order_id'] . '%');
}
if (!empty($nickname)) {
$query->join('eb_user u', 'o.spread_uid = u.uid', 'LEFT')
->where('u.nickname', 'like', '%' . $nickname . '%');
}
return $query->field('o.id,o.order_id,o.spread_uid,o.one_brokerage,o.pay_price,o.brokerage_status')
->order('o.id', 'desc')
->select()
->toArray();
}
踩坑记录
快照字段最初放在订单表,后来发现退款场景需要回看历史比例,拆到独立的佣金明细表更合理
分销比例如果支持后台随时调整,必须取"下单时的比例"而非"当前比例",否则历史订单会被静默改算
二、退款流程:药品订单强制整单退货
问题背景
药品电商的退款和普通电商不同,有两个硬约束:一是已发货必须退货退款(货不回来钱不退),二是不能单品退(防止拆单套利)。原来的实现里前端可以控制退款类型和商品选择,存在绕过风险。
方案设计
核心原则:退款权限是服务端状态机,不是UI 开关。
未发货状态:仅退款,原逻辑不变
已发货状态:后端强制校验refund_type=2(退货退款),且cart_ids必须覆盖整单有效商品
前端对已发货订单锁死退款方式切换 ,渲染合规提醒条
* 整单退款强校验:仅允许整单退款,禁止单品/部分退款
* 有效商品集合:订单 order_id 匹配、剩余可退(surplus_num>0)的全部 cart 行(含赠品,不过滤 pid)
* 校验通过后强制清空 cart_ids,走 applyRefund 整单分支,确保佣金正确置 0
* @param mixed $order 订单信息(Model 对象或数组均可)
* @param array $cart_ids 提交的退款商品集合(引用,校验通过将被置空以走整单分支)
* @throws ApiException
*/
protected function checkWholeOrderRefund($order, array &$cart_ids)
{
// 订单可能为 Model 对象或数组,统一转数组便于访问
if (is_object($order) && method_exists($order, 'toArray')) {
$order = $order->toArray();
}
// 空集合:后台/客服整单退款入口,直接放行
if (empty($cart_ids)) {
return;
}
/** @var StoreOrderCartInfoServices $cartInfoServices */
$cartInfoServices = app()->make(StoreOrderCartInfoServices::class);
// 有效集合:该订单下剩余可退(surplus_num>0)的全部 cart 行,含赠品,不过滤 pid;以主键 id 为键
$validCarts = $cartInfoServices->getCartColunm(['oid' => $order['id']], 'id,cart_num,surplus_num,refund_num', 'id');
$validIds = [];
$surplusMap = [];
foreach ($validCarts as $cid => $cart) {
if (($cart['surplus_num'] ?? 0) > 0) {
$validIds[] = $cid;
$surplusMap[$cid] = (int)$cart['surplus_num'];
}
}
$submitIds = array_column($cart_ids, 'cart_id');
// 提交集合必须与有效集合完全匹配(顺序无关),防止漏退/部分退(含赠品)
if (count($submitIds) !== count($validIds)
|| array_diff($submitIds, $validIds)
|| array_diff($validIds, $submitIds)) {
throw new ApiException('本订单仅支持整单退款,请选择全部商品申请退款。');
}
// 每项申请退款数量必须等于该行剩余可退数量(强制整件全退,不允许同一商品仅退部分数量)
$cartMap = array_column($cart_ids, 'cart_num', 'cart_id');
foreach ($validIds as $cid) {
if ((int)($cartMap[$cid] ?? 0) !== $surplusMap[$cid]) {
throw new ApiException('本订单仅支持整单退款,请选择全部商品申请退款。');
}
}
// 校验通过:强制按整单方式退款(清空,走 applyRefund 整单分支,佣金正确置 0)
$cart_ids = [];
}
关键校验点
delivery_status判断必须在服务端做,不能信任前端传入的状态
cart_id集合校验用排序后比对,防止漏选或多选
校验失败直接抛异常,不走"前端disabled所以不会触发"的逻辑
三、后台菜单迁移:推广订单明细独立化
问题背景
推广订单明细原来挂在佣金记录页面的el-tab里,运营反馈"找不到入口"。本质问题是菜单树设计不合理——一个叶子菜单承载了两个业务含义。
改法
前端抽独立组件,复用原接口,业务零改动
路由新增子项,权限标识独立
菜单表通过SQL直接插入新节点,清缓存生效
{
path: 'finance/commission_order',
name: `${pre}commissionOrder`,
meta: {
auth: ['finance-finance-commission-order'],
title: '推广订单明细',
},
component: () => import('@/pages/finance/commissionOrder/index'),
},
{
经验
系统菜单是数据库驱动的,界面上配不了完整结构。遇到"页面藏太深"的问题,优先改菜单树pid关系,别在组件层面打补丁。
四、顺带修的几个小问题
cart_id列与主键id不匹配导致的整单校验误判
多商品订单名称拼接后单元格溢出,前端split拆分展示
退款上传提示文案和按钮文案按场景区分

总结
医药电商系统的难点不在功能复杂度,而在合规约束和可解释性。佣金要能说清"为什么是这个数",退款要能证明"系统不允许违规操作",菜单要让运营"不用培训就能找到"。把这三件事做扎实,后面的药品参数、处方药管控就只是字段层面的扩展,架构上不用再动。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。