diff --git a/.crmeb/skills/crmeb-order-dev/references/order-aftersale.md b/.crmeb/skills/crmeb-order-dev/references/order-aftersale.md index 75ed30b33..359bc98fe 100644 --- a/.crmeb/skills/crmeb-order-dev/references/order-aftersale.md +++ b/.crmeb/skills/crmeb-order-dev/references/order-aftersale.md @@ -72,6 +72,8 @@ AfterSaleServices::refund - Handler 中的资金/资源副作用一律经 `RefundEffectContextInterface::once()` 登记去重,禁止直接裸写。 - 中台 `refund_status`(0 无 / 1 申请中 / 2 已退)由 Adapter `resolveRefundStatus` 推导,退款完成时 `syncStatus` 同步。 +**资金流水(`eb_capital_flow`)只记真实资金进出**:判定统一入口为 `PayServices::isCashChannel()`(第三方在线渠道 + 线下支付;余额 `yue`、后台代付、0 元单一律 false)。`CapitalFlowServices::setFlow()` 内部已做拦截,退款链路在调用前还要排除 `refund_type=balance` / `refund_method=balance` 的站内结转,即「余额支付订单退款」「原路退款但改退余额」都不产生现金流出,不记流水。 + > **现状说明**:`OrderRefundCreateAfter / OrderRefundRefuse / OrderRefundCancelAfter` 的模块监听器已注册但服务层触发点待接线(见 [订单生命周期·事件地图](order-lifecycle.md#三订单事件地图)),相关后置逻辑目前挂在 `AfterSaleServices` 内部回调(`lifecycle->afterApply/afterAudit`)。 ## 五、自定义售后 Handler 开发步骤 diff --git a/backend/app/services/order/RefundEffectServices.php b/backend/app/services/order/RefundEffectServices.php index 38ddfec1a..863b200ad 100644 --- a/backend/app/services/order/RefundEffectServices.php +++ b/backend/app/services/order/RefundEffectServices.php @@ -9,6 +9,7 @@ use app\dao\order\RefundEffectDao; use app\dao\order\RefundOrderDao; use app\model\order\OrderCenter; use app\model\order\RefundOrder; +use app\services\pay\PayServices; use app\services\statistic\CapitalFlowServices; use app\services\user\UserBillServices; use app\services\user\UserMoneyServices; @@ -380,7 +381,12 @@ class RefundEffectServices extends BaseService implements RefundEffectContextInt }); } - if (bccomp($amount, '0.00', 2) > 0) { + // 资金流水只记录真实资金流出:退回余额(refund_method=balance)或余额支付订单退款 + // 属于站内结转,钱没有离开平台,不记流水 + $flowPayType = (string)($order['pay_type'] ?? ($refundOrder['pay_channel'] ?? '')); + /** @var PayServices $payServices */ + $payServices = $this->app->make(PayServices::class); + if (!$localBalance && bccomp($amount, '0.00', 2) > 0 && $payServices->isCashChannel($flowPayType)) { $this->once('capital_flow', (string)$refundOrder['refund_no'], ['amount' => $amount], function () use ($order, $amount): void { /** @var UserServices $userServices */ $userServices = $this->app->make(UserServices::class); diff --git a/backend/app/services/order/RefundExecutorServices.php b/backend/app/services/order/RefundExecutorServices.php index c37986ea5..a539dc58e 100644 --- a/backend/app/services/order/RefundExecutorServices.php +++ b/backend/app/services/order/RefundExecutorServices.php @@ -97,7 +97,7 @@ class RefundExecutorServices extends BaseService } // 3. 资本流水 + 开票标记 - $this->writeFlow($order, $refundPrice); + $this->writeFlow($order, $refundPrice, $context); $this->markInvoiceRefunded($order); return [ @@ -459,20 +459,26 @@ class RefundExecutorServices extends BaseService /** * 写资本流水(refund 类型) + * 只有真实资金流出才记账:退回用户余额属于站内结转,余额支付订单退款同理,均不记流水 * @param array $order 中台订单数据 * @param float $refundPrice 退款金额 + * @param array $context 退款上下文(refund_type: original/balance) * @return void */ - protected function writeFlow(array $order, float $refundPrice): void + protected function writeFlow(array $order, float $refundPrice, array $context = []): void { if ($refundPrice <= 0) { return; } + // 退款到余额:钱没离开平台,只是从订单转为用户余额,不产生资金流出 + if (($context['refund_type'] ?? 'original') === 'balance') { + return; + } $payType = (string)($order['pay_type'] ?? ''); - // 是否真实资金渠道由 PayServices 动态判定(第三方渠道来自驱动注册中心,yue/offline 为系统常量渠道) + // 是否真实资金渠道由 PayServices 动态判定(第三方渠道来自驱动注册中心,offline 为系统常量渠道) /** @var PayServices $payServices */ $payServices = app()->make(PayServices::class); - if (!$payServices->isRealPaymentChannel($payType)) { + if (!$payServices->isCashChannel($payType)) { return; } /** @var UserServices $userServices */ diff --git a/backend/app/services/pay/PayServices.php b/backend/app/services/pay/PayServices.php index e3d5018b4..d49ceceec 100644 --- a/backend/app/services/pay/PayServices.php +++ b/backend/app/services/pay/PayServices.php @@ -337,8 +337,30 @@ class PayServices */ public function isRealPaymentChannel(string $payType): bool { - // 系统固有渠道:余额支付 / 线下支付,不随驱动扩展,常量判定合法 - if ($payType === self::YUE_PAY || $payType === self::OFFLINE_PAY) { + // 余额支付:站内虚拟资产结算,属于"有结算但无资金流",在此方法中视为真实支付 + if ($payType === self::YUE_PAY) { + return true; + } + return $this->isCashChannel($payType); + } + + /** + * 判断支付方式是否产生真实资金进出(现金收付渠道) + * 包含:第三方在线支付渠道(由驱动注册中心动态获取)+ 线下支付 + * 排除:余额支付(站内虚拟资产,充值后消费不再产生现金流动)、 + * 空支付方式(0 元单)、后台代付等无资金流动的结算方式 + * 资金流水(eb_capital_flow)只允许记录本方法判定为 true 的支付方式 + * @param string $payType 支付方式标识 + * @return bool + */ + public function isCashChannel(string $payType): bool + { + $payType = trim($payType); + if ($payType === '' || $payType === self::YUE_PAY) { + return false; + } + // 线下支付为系统固有渠道,不随驱动扩展,常量判定合法 + if ($payType === self::OFFLINE_PAY) { return true; } // 第三方渠道:从驱动注册中心动态获取 diff --git a/backend/app/services/statistic/CapitalFlowServices.php b/backend/app/services/statistic/CapitalFlowServices.php index b89362d70..047edb371 100644 --- a/backend/app/services/statistic/CapitalFlowServices.php +++ b/backend/app/services/statistic/CapitalFlowServices.php @@ -33,8 +33,12 @@ class CapitalFlowServices extends BaseService /** * 添加资金流水 - * @param $orderInfo - * @param string $type + * 资金流水记录的是平台真实资金进出(第三方在线收款、线下收银、充值及对应退款), + * 余额支付、后台代付、0 元单等站内虚拟结算不产生现金流动,一律不记账。 + * 该判定放在此处作为唯一出口,避免各业务分支各自判断导致漏过滤。 + * @param array $orderInfo 业务订单信息 + * @param string $type 流水类型 + * @return bool 是否写入流水 */ public function setFlow($orderInfo, $type = '') { @@ -97,12 +101,19 @@ class CapitalFlowServices extends BaseService $data['pay_type'] = $orderInfo['pay_type']; break; default: - break; + // 未知业务类型不记账,避免写入无金额无单号的空流水 + return false; } - $data['nickname'] = $orderInfo['nickname']; - $data['phone'] = $orderInfo['phone']; + // 非现金渠道(余额支付/后台代付/0 元单等)无真实资金流,不记流水 + /** @var PayServices $payServices */ + $payServices = app()->make(PayServices::class); + if (!$payServices->isCashChannel((string)($data['pay_type'] ?? ''))) { + return false; + } + $data['nickname'] = $orderInfo['nickname'] ?? ''; + $data['phone'] = $orderInfo['phone'] ?? ''; $data['add_time'] = time(); - $this->dao->save($data); + return (bool)$this->dao->save($data); } /**