最近对独立部署的医药商城小程序做了一些功能的后端重构,涉及佣金计算明细、药品退款流程、后台菜单结构三块。这篇文章记录一下方案设计和踩过的坑。
一、佣金计算明细:从黑盒到可审计
问题背景
原来的佣金模块只存最终金额,运营和分销商看不到计算过程。分销商问"这笔订单为什么是这个数",只能人工翻订单、翻比例、拿计算器现场算。订单量一大,客诉成本很高。
方案设计
核心思路是下单时固化计算快照,页面只做只读展示。具体做法:
下单环节计算佣金时,同步生成可读的公式描述(如"实付金额 × 分销比例")存入快照字段
收货/结算时直接读取快照写流水,不再重新计算
前端弹窗渲染快照中的公式和明细,不参与任何计算逻辑
这样做的好处是:金额可解释、计算过程不可篡改、前端只负责展示。

     * 推广订单明细列表查询
     * @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拆分展示
退款上传提示文案和按钮文案按场景区分

总结
医药电商系统的难点不在功能复杂度,而在合规约束和可解释性。佣金要能说清"为什么是这个数",退款要能证明"系统不允许违规操作",菜单要让运营"不用培训就能找到"。把这三件事做扎实,后面的药品参数、处方药管控就只是字段层面的扩展,架构上不用再动。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐