shish 1 månad sedan
förälder
incheckning
f7035ef338

+ 34 - 0
app-ghs/controllers/CustomController.php

@@ -21,9 +21,11 @@ use bizHd\shop\classes\ShopClass;
 use bizGhs\stat\classes\StatVisitClass;
 use common\components\dict;
 use common\components\imgUtil;
+use common\components\miniUtil;
 use common\components\orderSn;
 use common\components\stringUtil;
 use common\components\util;
+use bizHd\wx\classes\WxOpenClass;
 use Yii;
 
 class CustomController extends BaseController
@@ -117,6 +119,38 @@ class CustomController extends BaseController
         util::success(['imgUrl' => $imageUrl]);
     }
 
+    /**
+     * 客户向供货商充值的小程序码(隔天冲销返余额海报用)
+     * GET: id=customId
+     */
+    public function actionGetGatheringMiniCode()
+    {
+        $get = Yii::$app->request->get();
+        $id = $get['id'] ?? 0;
+        $custom = CustomClass::getById($id, true);
+        if (empty($custom)) {
+            util::fail('没有找到客户');
+        }
+        if ($custom->ownMainId != $this->mainId) {
+            util::fail('不是你的客户哈');
+        }
+        $ghsId = $custom->ghsId ?? 0;
+        $salt = $custom->salt ?? '';
+        $merchant = WxOpenClass::getWxInfo();
+        $page = 'admin/ghs/pay';
+        $ptStyle = dict::getDict('ptStyle', 'hd');
+        $scene = 'id=' . $ghsId . '&salt=' . $salt;
+        $envVersion = miniUtil::normalizeMiniEnvVersion($get['env_version'] ?? 'release');
+        $miniCode = miniUtil::generateUnlimitedMiniCode($merchant, $page, $scene, $ptStyle, $envVersion);
+        $imageUrl = imgUtil::groupImg($miniCode);
+        util::success([
+            'imgUrl' => $imageUrl,
+            'ghsId' => $ghsId,
+            'salt' => $salt,
+            'balance' => bcadd((string)($custom->balance ?? '0'), '0', 2),
+        ]);
+    }
+
     //供货商给客户充值和结账 ssh 20240309
     public function actionRecharge()
     {

+ 46 - 2
app-ghs/controllers/RefundController.php

@@ -6,6 +6,7 @@ use bizGhs\notify\classes\NotifyClass;
 use bizGhs\order\classes\CheckOrderClass;
 use bizGhs\order\classes\RefundOrderItemClass;
 use bizGhs\order\services\RefundOrderService;
+use bizGhs\order\services\NextDayRefundService;
 use bizGhs\shop\classes\ShopClass;
 use bizHd\cg\classes\CgRefundClass;
 use bizHd\cg\classes\CgRefundItemClass;
@@ -25,6 +26,30 @@ use bizGhs\order\classes\StockWastageOrderClass;
 class RefundController extends BaseController
 {
 
+    /**
+     * 原单是否可走隔天冲销(售后页预检)
+     * GET: orderId
+     */
+    public function actionCheckNextDayEligible()
+    {
+        $orderId = Yii::$app->request->get('orderId', 0);
+        $order = OrderClass::getById($orderId, true);
+        if (empty($order) || intval($order->mainId) !== intval($this->mainId)) {
+            util::fail('没有找到订单');
+        }
+        $check = NextDayRefundService::checkEligible($order);
+        $check['hasNextDay'] = bccomp((string)($order->nextDayTkPrice ?? '0'), '0', 2) > 0 ? 1 : 0;
+        $check['nextDayTkPrice'] = $order->nextDayTkPrice ?? '0.00';
+        $check['tkPrice'] = $order->tkPrice ?? '0.00';
+        $check['actPrice'] = $order->actPrice ?? '0.00';
+        $check['payWay'] = $order->payWay ?? 0;
+        $check['onlinePay'] = $order->onlinePay ?? 0;
+        $check['debtPrice'] = $order->debtPrice ?? '0.00';
+        $check['remainDebtPrice'] = $order->remainDebtPrice ?? '0.00';
+        $check['clearId'] = intval($order->clearId ?? 0);
+        util::success($check);
+    }
+
     //取消的订单退钱,特殊情况的操作:商家取消订单时,客户同时付款成功了,造成的流程冲突,需要退钱 ssh 20250429
     public function actionRefundMoney()
     {
@@ -690,7 +715,25 @@ class RefundController extends BaseController
                     RefundOrderService::passRefund($refundInfo);
 
                     $transaction->commit();
-                    return $respond;
+                    // 隔天冲销:返回海报/结果页所需字段
+                    $refundInfo = RefundOrderClass::getById($refundId, true);
+                    $order = OrderClass::getById($id, true);
+                    $custom = null;
+                    if (!empty($order)) {
+                        $custom = \bizGhs\custom\classes\CustomClass::getById(intval($order->customId ?? 0), true);
+                    }
+                    return [
+                        'id' => $refundId,
+                        'orderSn' => $refundInfo->orderSn ?? '',
+                        'sameDay' => intval($refundInfo->sameDay ?? 1),
+                        'fundType' => intval($refundInfo->fundType ?? 0),
+                        'refundPrice' => $refundInfo->refundPrice ?? 0,
+                        'customId' => intval($order->customId ?? 0),
+                        'customName' => $custom->name ?? ($order->customName ?? ''),
+                        'customBalance' => bcadd((string)($custom->balance ?? '0'), '0', 2),
+                        'orderId' => intval($id),
+                        'nextDayTkPrice' => $order->nextDayTkPrice ?? '0.00',
+                    ];
                 } catch (\Exception $exception) {
                     if ($transaction->isActive) {
                         $transaction->rollBack();
@@ -704,7 +747,8 @@ class RefundController extends BaseController
             if (util::isDbConcurrencyException($exception)) {
                 util::fail('系统繁忙中,请稍后再试');
             } else {
-                util::fail('操作失败');
+                // 透出业务失败原因,便于隔天冲销排错
+                util::fail($exception->getMessage() ?: '操作失败');
             }
         }
     }

+ 46 - 0
biz-ghs/custom/classes/CustomClass.php

@@ -695,6 +695,52 @@ class CustomClass extends BaseClass
         return $cgOrderSn !== '' ? $cgOrderSn : $saleOrderSn;
     }
 
+    /**
+     * 隔天冲销返充余额:成对账户加余额,不改订单 actPrice;流水挂售后单。
+     * 调用方须已 lockAccountPair。
+     */
+    public static function nextDayRefundReturnBalance($custom, $ghs, $refund, $order)
+    {
+        AccountMoneyClass::ensureCustomMoneyReady($custom, true);
+        if (!empty($ghs)) {
+            AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
+            self::assertPairBalanceEqual($custom, $ghs);
+        }
+        $refundPrice = bcadd((string)($refund->refundPrice ?? '0'), '0', 2);
+        if (bccomp($refundPrice, '0', 2) <= 0) {
+            util::fail('返充金额必须大于0');
+        }
+        $newBalance = bcadd((string)($custom->balance ?? '0'), $refundPrice, 2);
+        self::savePairBalanceAfterDeduct($custom, $ghs, $newBalance);
+
+        $refundSn = $refund->orderSn ?? '';
+        $saleOrderSn = $order->orderSn ?? '';
+        $capitalTypeCustom = dict::getDict('capitalType', 'ghsHelpCustomRechargeReturn', 'id');
+        CustomBalanceChangeClass::add([
+            'relateId' => $refund->id ?? 0,
+            'customId' => $custom->id ?? 0,
+            'customName' => $custom->name ?? '',
+            'ptStyle' => dict::getDict('ptStyle', 'ghs'),
+            'capitalType' => $capitalTypeCustom,
+            'amount' => $refundPrice,
+            'balance' => $newBalance,
+            'io' => 1,
+            'payWay' => dict::getDict('payWay', 'balancePay'),
+            'event' => "隔天冲销返充余额,售后单:{$refundSn},原单:{$saleOrderSn}",
+            'sjId' => $refund->sjId ?? 0,
+            'shopId' => $refund->shopId ?? 0,
+            'mainId' => $order->mainId ?? 0,
+            'payTime' => date('Y-m-d H:i:s'),
+            'staffId' => $refund->shopAdminId ?? 0,
+            'staffName' => $refund->shopAdminName ?? '',
+            'side' => 0,
+            'fromType' => dict::getDict('fromType', 'shop'),
+            'remark' => '',
+        ], true);
+        self::addGhsRefundBalanceChange($custom, $ghs, $refund, $order, $refundPrice, $newBalance);
+        return $newBalance;
+    }
+
     /**
      * 花店采购余额付售后:成对账户原路退回(与 payToChangeBalance 对称)。
      * 调用方须已 lockAccountPair;同时写客户余额明细与 xhGhs 余额明细(hdApp 可查)。

+ 12 - 1
biz-ghs/order/classes/RefundOrderClass.php

@@ -38,6 +38,10 @@ class RefundOrderClass extends BaseClass
     const REFUND_TYPE_MONEY_GOOD = 1; //退款退货
     const REFUND_TYPE_MONEY = 2; // 仅退款
 
+    /** 1当天售后(默认) 0隔天冲销 */
+    const SAME_DAY_YES = 1;
+    const SAME_DAY_NO = 0;
+
     //打印退款的小票 ssh 20241001
     public static function printRefund($respond)
     {
@@ -240,7 +244,14 @@ class RefundOrderClass extends BaseClass
         $main->totalRefund = $totalRefund;
         $main->save();
 
-        //记录总的退款金额
+        // 隔天冲销:累计 nextDayTkPrice,不改 actPrice;资金按 fundType
+        $sameDay = intval($refund->sameDay ?? self::SAME_DAY_YES);
+        if ($sameDay === self::SAME_DAY_NO) {
+            \bizGhs\order\services\NextDayRefundService::applyGhsAmountAndFund($refund, $order);
+            return;
+        }
+
+        //记录当天累计退款金额(影响 actPrice)
         $currentTkPrice = bcadd($order->tkPrice, $refundPrice, 2);
         $order->tkPrice = $currentTkPrice;
         $order->refund = OrderClass::REFUND_YES;

+ 269 - 0
biz-ghs/order/services/NextDayRefundService.php

@@ -0,0 +1,269 @@
+<?php
+/**
+ * 隔天售后(冲销)辅助服务
+ * 用途:资格校验、资金方式解析、隔天通过后的金额缓存与返余额;不建独立冲销表。
+ * 对应:xhRefund/xhCgRefund.sameDay=0,订单 nextDayTkPrice 累计且不改 actPrice。
+ */
+namespace bizGhs\order\services;
+
+use bizGhs\custom\classes\CustomClass;
+use bizGhs\custom\services\GhsRechargeSettleService;
+use bizGhs\order\classes\OrderClass;
+use bizGhs\order\classes\RefundOrderClass;
+use bizHd\cg\classes\CgRefundClass;
+use bizHd\purchase\classes\PurchaseClass;
+use common\components\dict;
+use common\components\util;
+use Yii;
+
+class NextDayRefundService
+{
+    const SAME_DAY_YES = 1;
+    const SAME_DAY_NO = 0;
+
+    const FUND_UNUSED = 0;
+    const FUND_ORIGINAL = 1;
+    const FUND_BALANCE = 2;
+    const FUND_NONE = 3;
+
+    /**
+     * 原单是否可走隔天冲销(即原「冲销」路径):
+     * - 非当天支付;或
+     * - 挂账已结清;或
+     * - 已结账单(当天已结账也按冲销处理,不走普通售后)
+     * @return array{ok:bool,reason:string,sameDay:int}
+     */
+    public static function checkEligible($order)
+    {
+        if (empty($order)) {
+            return ['ok' => false, 'reason' => '没有原订单', 'sameDay' => self::SAME_DAY_YES];
+        }
+        $payTime = $order->payTime ?? ($order->addTime ?? '');
+        $isToday = false;
+        if (!empty($payTime) && $payTime !== '0000-00-00 00:00:00') {
+            $isToday = date('Y-m-d', strtotime($payTime)) === date('Y-m-d');
+        }
+        $debtPrice = bcadd((string)($order->debtPrice ?? '0'), '0', 2);
+        $remainDebt = bcadd((string)($order->remainDebtPrice ?? '0'), '0', 2);
+        $debtCleared = bccomp($debtPrice, '0', 2) > 0 && bccomp($remainDebt, '0', 2) === 0;
+        // 已结账:当天也强制走冲销(sameDay=0),与隔天同一套资金/金额逻辑
+        $orderCleared = !empty($order->clearId);
+
+        if ($isToday && !$debtCleared && !$orderCleared) {
+            return [
+                'ok' => false,
+                'reason' => '当天未结账订单请走普通售后;已结账/挂账结清/隔天可冲销',
+                'sameDay' => self::SAME_DAY_YES,
+            ];
+        }
+        return [
+            'ok' => true,
+            'reason' => '',
+            'sameDay' => self::SAME_DAY_NO,
+            'suggestFundBalance' => true,
+            'orderCleared' => $orderCleared ? 1 : 0,
+        ];
+    }
+
+    /**
+     * 创建时决定是否必须按隔天冲销:已结账单不可走当天售后。
+     */
+    public static function mustUseNextDay($order)
+    {
+        return !empty($order) && !empty($order->clearId);
+    }
+
+    /**
+     * 解析隔天资金方式:挂账/余额强制返余额;在线可选原路/返余额/仅记账。
+     */
+    public static function resolveFundType($fundType, $order, $hasRelateOrder = true)
+    {
+        $fundType = intval($fundType);
+        $payWay = intval($order->payWay ?? 0);
+        $onlinePay = intval($order->onlinePay ?? dict::getDict('onlinePay', 'not'));
+        $debtPay = dict::getDict('payWay', 'debtPay');
+        $balancePay = dict::getDict('payWay', 'balancePay');
+        $wxPay = dict::getDict('payWay', 'wxPay');
+        $aliPay = dict::getDict('payWay', 'alipay');
+        $isOnline = $onlinePay == dict::getDict('onlinePay', 'yes') && in_array($payWay, [$wxPay, $aliPay], true);
+
+        if (!$hasRelateOrder) {
+            if ($fundType === self::FUND_ORIGINAL) {
+                util::fail('自由冲销不能原路退回');
+            }
+            if (!in_array($fundType, [self::FUND_BALANCE, self::FUND_NONE], true)) {
+                return self::FUND_BALANCE;
+            }
+            return $fundType;
+        }
+        if ($payWay === $debtPay || $payWay === $balancePay) {
+            return self::FUND_BALANCE;
+        }
+        if ($isOnline) {
+            if (!in_array($fundType, [self::FUND_ORIGINAL, self::FUND_BALANCE, self::FUND_NONE], true)) {
+                util::fail('请选择资金处理方式');
+            }
+            return $fundType;
+        }
+        if (!in_array($fundType, [self::FUND_BALANCE, self::FUND_NONE], true)) {
+            return self::FUND_BALANCE;
+        }
+        return $fundType;
+    }
+
+    /**
+     * GHS 侧隔天通过:累计 nextDayTkPrice,按 fundType 处理资金,不改 actPrice。
+     * @return string 返余额后的客户余额(非返余额则为当前余额)
+     */
+    public static function applyGhsAmountAndFund($refund, $order)
+    {
+        $refundPrice = bcadd((string)($refund->refundPrice ?? '0'), '0', 2);
+        $order->nextDayTkPrice = bcadd((string)($order->nextDayTkPrice ?? '0'), $refundPrice, 2);
+        $order->refund = OrderClass::REFUND_YES;
+        $order->save(false, ['nextDayTkPrice', 'refund']);
+
+        $customId = intval($order->customId ?? 0);
+        $custom = CustomClass::getLockById($customId);
+        if (empty($custom)) {
+            util::fail('没有找到客户');
+        }
+        $buyAmount = bcsub((string)($custom->buyAmount ?? '0'), $refundPrice, 2);
+        if (bccomp($buyAmount, '0', 2) < 0) {
+            $buyAmount = '0.00';
+        }
+        $custom->buyAmount = $buyAmount;
+        $custom->save(false, ['buyAmount']);
+
+        $fundType = intval($refund->fundType ?? self::FUND_UNUSED);
+        $balance = bcadd((string)($custom->balance ?? '0'), '0', 2);
+        if ($fundType === self::FUND_BALANCE) {
+            $pair = GhsRechargeSettleService::lockAccountPair($custom);
+            CustomClass::nextDayRefundReturnBalance($pair['custom'], $pair['ghs'], $refund, $order);
+            $custom = CustomClass::getById($customId, true);
+            $balance = bcadd((string)($custom->balance ?? '0'), '0', 2);
+        }
+        // FUND_ORIGINAL:网关退款在采购侧 CgRefundClass 执行;FUND_NONE:仅记账
+        return $balance;
+    }
+
+    /**
+     * HD 采购侧隔天通过:累计 nextDayTkPrice;原路退走拉卡拉,不改 actPrice。
+     */
+    public static function applyCgAmountAndFund($cgRefund, $cg)
+    {
+        $refundPrice = bcadd((string)($cgRefund->refundPrice ?? '0'), '0', 2);
+        $cg->nextDayTkPrice = bcadd((string)($cg->nextDayTkPrice ?? '0'), $refundPrice, 2);
+        $cg->refund = PurchaseClass::REFUND_YES;
+        $cg->save(false, ['nextDayTkPrice', 'refund']);
+
+        $fundType = intval($cgRefund->fundType ?? self::FUND_UNUSED);
+        if ($fundType === self::FUND_ORIGINAL) {
+            CgRefundClass::nextDayOriginalOnlineRefund($cgRefund, $cg);
+        }
+
+        $ghsId = intval($cg->ghsId ?? 0);
+        if ($ghsId > 0) {
+            $ghs = \bizHd\ghs\classes\GhsClass::getLockById($ghsId);
+            if (!empty($ghs) && isset($ghs->expendAmount)) {
+                $expend = bcsub((string)($ghs->expendAmount ?? '0'), $refundPrice, 2);
+                if (bccomp($expend, '0', 2) < 0) {
+                    $expend = '0.00';
+                }
+                $ghs->expendAmount = $expend;
+                $ghs->save(false, ['expendAmount']);
+            }
+        }
+    }
+
+    /**
+     * 按时段汇总成功隔天售后金额(收入/销量统计扣减用)
+     */
+    public static function sumAmountByMainAndTime($mainId, $startTime, $endTime)
+    {
+        $sql = "SELECT COALESCE(SUM(refundPrice),0) AS total FROM xhRefund
+            WHERE mainId=:mainId AND status=:status AND sameDay=:sameDay
+              AND passTime BETWEEN :start AND :end";
+        $row = Yii::$app->db->createCommand($sql, [
+            ':mainId' => $mainId,
+            ':status' => RefundOrderClass::STATUS_COMPLETE,
+            ':sameDay' => self::SAME_DAY_NO,
+            ':start' => $startTime,
+            ':end' => $endTime,
+        ])->queryOne();
+        return bcadd($row['total'] ?? '0', '0', 2);
+    }
+
+    /**
+     * 渠道收入对冲:拉取隔天成功售后及原单支付字段
+     */
+    public static function listForChannelIncomeDeduct($mainId, $startTime, $endTime)
+    {
+        $sql = "SELECT r.refundPrice AS amount, r.fundType, o.id AS orderId,
+                o.debtPrice, o.remainDebtPrice, o.onlinePay, o.payWay AS orderPayWay, o.payWay
+            FROM xhRefund r
+            LEFT JOIN xhGhsOrder o ON o.orderSn = r.relateOrderSn
+            WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
+              AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end";
+        $rows = Yii::$app->db->createCommand($sql, [
+            ':mainId' => $mainId,
+            ':status' => RefundOrderClass::STATUS_COMPLETE,
+            ':sameDay' => self::SAME_DAY_NO,
+            ':start' => $startTime,
+            ':end' => $endTime,
+        ])->queryAll();
+        return $rows ?: [];
+    }
+
+    /**
+     * 按客户汇总隔天退款金额
+     */
+    public static function sumAmountGroupByCustom($mainId, $startTime, $endTime)
+    {
+        $sql = "SELECT r.customId, COALESCE(SUM(r.refundPrice),0) AS total FROM xhRefund r
+            WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
+              AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end
+            GROUP BY r.customId";
+        $rows = Yii::$app->db->createCommand($sql, [
+            ':mainId' => $mainId,
+            ':status' => RefundOrderClass::STATUS_COMPLETE,
+            ':sameDay' => self::SAME_DAY_NO,
+            ':start' => $startTime,
+            ':end' => $endTime,
+        ])->queryAll();
+        $map = [];
+        foreach ($rows as $row) {
+            $map[$row['customId']] = bcadd($row['total'] ?? '0', '0', 2);
+        }
+        return $map;
+    }
+
+    /**
+     * 按花材汇总隔天退货数量与金额(退货退款明细)
+     */
+    public static function sumItemByProduct($mainId, $startTime, $endTime)
+    {
+        $sql = "SELECT i.productId, COALESCE(SUM(i.xhNum),0) AS num, COALESCE(SUM(i.xhPrice),0) AS amount
+            FROM xhRefundItem i
+            INNER JOIN xhRefund r ON r.orderSn = i.orderSn
+            WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
+              AND r.refundType=:rtype
+              AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end
+            GROUP BY i.productId";
+        $rows = Yii::$app->db->createCommand($sql, [
+            ':mainId' => $mainId,
+            ':status' => RefundOrderClass::STATUS_COMPLETE,
+            ':sameDay' => self::SAME_DAY_NO,
+            ':rtype' => RefundOrderClass::REFUND_TYPE_MONEY_GOOD,
+            ':start' => $startTime,
+            ':end' => $endTime,
+        ])->queryAll();
+        $map = [];
+        foreach ($rows as $row) {
+            $map[$row['productId']] = [
+                'num' => bcadd($row['num'] ?? '0', '0', 2),
+                'amount' => bcadd($row['amount'] ?? '0', '0', 2),
+            ];
+        }
+        return $map;
+    }
+}

+ 15 - 2
biz-ghs/order/services/OrderService.php

@@ -934,8 +934,16 @@ class OrderService extends BaseService
         if (empty($order)) {
             util::fail('没有找到订单');
         }
-        if (isset($order->clearId) && !empty($order->clearId)) {
-            util::fail('已经结账,不能再售后');
+        $sameDay = intval($post['sameDay'] ?? \bizGhs\order\classes\RefundOrderClass::SAME_DAY_YES);
+        // 已结账单强制走冲销(sameDay=0),不再拦截为「不能售后」
+        if (\bizGhs\order\services\NextDayRefundService::mustUseNextDay($order)) {
+            $sameDay = \bizGhs\order\classes\RefundOrderClass::SAME_DAY_NO;
+            $post['sameDay'] = $sameDay;
+        }
+        // 仅「当天普通售后」仍拦截已结账
+        if ($sameDay === \bizGhs\order\classes\RefundOrderClass::SAME_DAY_YES
+            && isset($order->clearId) && !empty($order->clearId)) {
+            util::fail('已经结账,请按冲销处理');
         }
         if ($order->debt != 1 && $order->addTime < '2023-10-17 00:00:00') {
             util::fail('10月17日前订单无法申请退款');
@@ -998,6 +1006,9 @@ class OrderService extends BaseService
             'cause' => $cause,
             'refundSendCost' => $refundSendCost,
             'refundPackCost' => $refundPackCost,
+            // 与销售侧售后成对写入隔天标记
+            'sameDay' => $saleRefund->sameDay ?? \bizGhs\order\classes\RefundOrderClass::SAME_DAY_YES,
+            'fundType' => $saleRefund->fundType ?? 0,
         ];
         $cgRefund = CgRefundService::addRefund($cgData, $cg);
         $cgRefundId = $cgRefund->id;
@@ -1005,6 +1016,8 @@ class OrderService extends BaseService
         $saleRefund->cgRefundId = $cgRefundId;
         $saleRefund->save();
         $cgRefund->saleRefundId = $saleRefundId;
+        $cgRefund->sameDay = intval($saleRefund->sameDay ?? 1);
+        $cgRefund->fundType = intval($saleRefund->fundType ?? 0);
         $cgRefund->save();
 
         return $saleRefund;

+ 36 - 3
biz-ghs/order/services/RefundOrderService.php

@@ -16,6 +16,7 @@ use bizGhs\order\classes\OrderClass;
 use bizGhs\order\classes\OrderItemClass;
 use bizGhs\order\classes\RefundOrderClass;
 use bizGhs\order\classes\RefundOrderItemClass;
+use bizGhs\order\services\NextDayRefundService;
 use bizGhs\shop\classes\MainClass;
 use bizGhs\shop\classes\ShopMoneyClass;
 use bizGhs\stock\classes\StockRecordClass;
@@ -54,7 +55,9 @@ class RefundOrderService extends BaseService
         if ($order->status == OrderClass::ORDER_STATUS_CANCEL) {
             util::fail("已取消订单,不能申请售后");
         }
-        if (isset($order->clearId) && !empty($order->clearId)) {
+        // 隔天冲销允许已结账单;当天售后仍拦截结账
+        $sameDay = intval($refund->sameDay ?? RefundOrderClass::SAME_DAY_YES);
+        if ($sameDay === RefundOrderClass::SAME_DAY_YES && isset($order->clearId) && !empty($order->clearId)) {
             util::fail('订单已结账,请驳回,并在线下完成售后');
         }
         $currentOrderId = $order->id;
@@ -66,7 +69,17 @@ class RefundOrderService extends BaseService
         if (strtotime($addTime) <= strtotime('2022-03-30 00:00:00')) {
             util::fail('历史订单,不能发起退款哦');
         }
-        if (bccomp($refund->refundPrice, $order->realPrice, 2) == 1) {
+        // 隔天冲销不减 actPrice,上限用 actPrice - 已隔天累计;当天仍对比 realPrice
+        if ($sameDay === RefundOrderClass::SAME_DAY_NO) {
+            $remainNext = bcsub(
+                (string)($order->actPrice ?? '0'),
+                (string)($order->nextDayTkPrice ?? '0'),
+                2
+            );
+            if (bccomp((string)$refund->refundPrice, $remainNext, 2) === 1) {
+                util::fail('冲销金额超过可冲销余额');
+            }
+        } elseif (bccomp($refund->refundPrice, $order->realPrice, 2) == 1) {
             util::fail("退款金额超过订单金额");
         }
         $refund->passTime = date("Y-m-d H:i:s");
@@ -86,7 +99,8 @@ class RefundOrderService extends BaseService
                 }
             }
             $currentClearId = OrderCgClearClass::getPendingClearIdByOrderId($currentOrderId, $customId);
-            if ($currentClearId > 0) {
+            // 隔天冲销不要求先取消结账单
+            if ($currentClearId > 0 && $sameDay === RefundOrderClass::SAME_DAY_YES) {
 				//有结账单要取消才能售后
                 util::success(['clearId' => $currentClearId, 'error' => 'hasUnClearOrder']);
             }
@@ -221,6 +235,25 @@ class RefundOrderService extends BaseService
         $data['customNamePy'] = $order->customNamePy??'';
         $data['customMobile'] = $order->customMobile??'';
         $data['orderTime'] = $order->payTime??'';
+
+        // 隔天冲销:sameDay=0 + fundType;已结账单强制冲销路径(即使当天)
+        $sameDay = intval($post['sameDay'] ?? RefundOrderClass::SAME_DAY_YES);
+        if (NextDayRefundService::mustUseNextDay($order)) {
+            $sameDay = RefundOrderClass::SAME_DAY_NO;
+        }
+        if ($sameDay === RefundOrderClass::SAME_DAY_NO) {
+            $check = NextDayRefundService::checkEligible($order);
+            if (empty($check['ok'])) {
+                util::fail($check['reason'] ?? '当前订单不可隔天冲销');
+            }
+            $fundType = NextDayRefundService::resolveFundType($post['fundType'] ?? 0, $order, true);
+            $data['sameDay'] = RefundOrderClass::SAME_DAY_NO;
+            $data['fundType'] = $fundType;
+        } else {
+            $data['sameDay'] = RefundOrderClass::SAME_DAY_YES;
+            $data['fundType'] = NextDayRefundService::FUND_UNUSED;
+        }
+
         $refund = RefundOrderClass::add($data, true);
         return $refund;
     }

+ 66 - 0
biz-ghs/stat/classes/StatKdClass.php

@@ -3,6 +3,7 @@
 namespace bizGhs\stat\classes;
 
 use biz\shop\classes\ShopAdminClass;
+use bizGhs\order\services\NextDayRefundService;
 use bizGhs\order\classes\OrderClass;
 use bizGhs\order\classes\OrderItemClass;
 use bizGhs\order\classes\StockOutOrderClass;
@@ -410,8 +411,73 @@ class StatKdClass extends BaseClass
                 $staffAmountList[$staffId] = ['num' => 1, 'amount' => $currentAmount, 'staffName' => $staffName];
             }
         }
+        // 隔天冲销:按原单入账渠道扣减金额(只减金额不减笔数)
+        self::deductNextDayFromChannelIncome($incomeList, $mainId, $currentStartTime, $currentEndTime);
+
         return ['incomeList' => $incomeList, 'staffAmountList' => $staffAmountList, 'payCodeIncome' => $payCodeIncome, 'lsAfterSale' => $lsAfterSale];
 
     }
 
+    /**
+     * 渠道收入对冲隔天冲销金额(挂账已结扣 debtClear 等)
+     */
+    protected static function deductNextDayFromChannelIncome(&$incomeList, $mainId, $startTime, $endTime)
+    {
+        $rows = NextDayRefundService::listForChannelIncomeDeduct($mainId, $startTime, $endTime);
+        if (empty($rows)) {
+            return;
+        }
+        foreach ($rows as $row) {
+            $amount = bcadd((string)($row['amount'] ?? '0'), '0', 2);
+            if (bccomp($amount, '0', 2) <= 0) {
+                continue;
+            }
+            $channel = self::resolveNextDayIncomeChannel($row);
+            if (empty($channel) || !isset($incomeList[$channel])) {
+                $channel = 'other';
+            }
+            $incomeList[$channel]['amount'] = bcsub((string)$incomeList[$channel]['amount'], $amount, 2);
+            if (isset($incomeList[$channel]['category']['pf'])) {
+                $incomeList[$channel]['category']['pf']['amount'] = bcsub(
+                    (string)$incomeList[$channel]['category']['pf']['amount'],
+                    $amount,
+                    2
+                );
+            }
+        }
+    }
+
+    /** 与批发开单入账分支一致,映射渠道 key */
+    protected static function resolveNextDayIncomeChannel($row)
+    {
+        $debtPrice = bcadd((string)($row['debtPrice'] ?? '0'), '0', 2);
+        $remainDebt = bcadd((string)($row['remainDebtPrice'] ?? '0'), '0', 2);
+        $orderId = intval($row['orderId'] ?? 0);
+        $payWay = intval($row['orderPayWay'] ?? ($row['payWay'] ?? 0));
+        $onlinePay = intval($row['onlinePay'] ?? dict::getDict('onlinePay', 'not'));
+
+        if ($orderId > 0 && bccomp($debtPrice, '0', 2) > 0) {
+            return bccomp($remainDebt, '0', 2) > 0 ? 'debt' : 'debtClear';
+        }
+        if ($onlinePay == dict::getDict('onlinePay', 'yes')) {
+            return 'system';
+        }
+        switch ($payWay) {
+            case dict::getDict('payWay', 'wxPay'):
+                return 'kfWx';
+            case dict::getDict('payWay', 'alipay'):
+                return 'aliPay';
+            case dict::getDict('payWay', 'cash'):
+                return 'cash';
+            case dict::getDict('payWay', 'balancePay'):
+                return 'balancePay';
+            case dict::getDict('payWay', 'bankCard'):
+                return 'bankCard';
+            case dict::getDict('payWay', 'debtPay'):
+                return 'debtClear';
+            default:
+                return 'other';
+        }
+    }
+
 }

+ 18 - 0
biz-ghs/stat/classes/StatSaleClass.php

@@ -13,6 +13,7 @@ use bizGhs\order\classes\OrderItemClass;
 use bizGhs\order\classes\PurchaseOrderClass;
 use bizGhs\order\classes\PurchaseOrderItemClass;
 use bizGhs\order\classes\RefundOrderClass;
+use bizGhs\order\services\NextDayRefundService;
 use bizGhs\order\classes\StockWastageOrderClass;
 use bizGhs\product\classes\ProductClass;
 use bizGhs\staff\classes\SalaryClass;
@@ -200,6 +201,17 @@ class StatSaleClass extends BaseClass
                     ];
                 }
             }
+            // 隔天冲销扣客户采购金额(笔数不减)
+            $fwdMap = NextDayRefundService::sumAmountGroupByCustom($mainId, $currentStartTime, $currentEndTime);
+            foreach ($fwdMap as $cid => $amt) {
+                if (!isset($arr[$cid])) {
+                    continue;
+                }
+                $arr[$cid]['amount'] = bcsub((string)$arr[$cid]['amount'], (string)$amt, 2);
+                $arr[$cid]['profit'] = bcsub((string)$arr[$cid]['profit'], (string)$amt, 2);
+                $totalAmount = bcsub((string)$totalAmount, (string)$amt, 2);
+                $totalProfit = bcsub((string)$totalProfit, (string)$amt, 2);
+            }
             $arr = arrayUtil::arraySort($arr, 'profit');
         }
         $totalAmount = floatval($totalAmount);
@@ -317,6 +329,12 @@ class StatSaleClass extends BaseClass
                 $totalOrderNum++;
             }
         }
+        // 隔天冲销扣减批发收入(笔数不变)
+        $pfIncome = bcsub(
+            (string)$pfIncome,
+            NextDayRefundService::sumAmountByMainAndTime($mainId, $currentStartTime, $currentEndTime),
+            2
+        );
 
         //零售开单
         $payCodeIncome = 0;

+ 65 - 1
biz-hd/cg/classes/CgRefundClass.php

@@ -36,6 +36,63 @@ class CgRefundClass extends BaseClass
     const REFUND_TYPE_MONEY_GOOD = 1; //退款退货
     const REFUND_TYPE_MONEY = 2; // 仅退款
 
+    const SAME_DAY_YES = 1;
+    const SAME_DAY_NO = 0;
+
+    /**
+     * 隔天冲销原路退:走拉卡拉退款,不改采购单 actPrice
+     */
+    public static function nextDayOriginalOnlineRefund($cgRefund, $cg)
+    {
+        $onlinePay = $cg->onlinePay ?? 1;
+        $payWay = $cg->payWay ?? 0;
+        if ($onlinePay != dict::getDict('onlinePay', 'yes')) {
+            util::fail('非在线支付无法原路退回');
+        }
+        if ($payWay != dict::getDict('payWay', 'wxPay') && $payWay != dict::getDict('payWay', 'alipay')) {
+            util::fail('支付方式不支持原路退回');
+        }
+        $ghsId = $cg->ghsId ?? 0;
+        $ghs = GhsClass::getById($ghsId, true);
+        if (empty($ghs)) {
+            util::fail('没有找到供货商');
+        }
+        $ghsShopId = $ghs->shopId ?? 0;
+        $ghsShop = ShopClass::getById($ghsShopId, true);
+        if (empty($ghsShop)) {
+            util::fail('没有找到供货商的门店');
+        }
+        $refundPrice = bcadd((string)($cgRefund->refundPrice ?? '0'), '0', 2);
+        $refundFee = bcmul($refundPrice, 100);
+        $merchantPrivateKeyPath = Yii::getAlias("@vendor/lakala") . '/production/api_private_key.pem';
+        $lklCertificatePath = Yii::getAlias("@vendor/lakala") . '/production/lkl-apigw-v1.cer';
+        $params = [
+            'appid' => 'OP00002119',
+            'serial_no' => '018b08cfddbd',
+            'merchant_no' => $ghsShop->lklSjNo,
+            'term_no' => $ghsShop->lklScanTermNo,
+            'merchantPrivateKeyPath' => $merchantPrivateKeyPath,
+            'lklCertificatePath' => $lklCertificatePath,
+        ];
+        $laResource = new Lakala($params);
+        $refundOrderSn = $cgRefund->orderSn ?? orderSn::getCgRefundSn();
+        $aliParams = [
+            'refundSn' => $refundOrderSn,
+            'orderSn' => $cg->orderSn ?? '',
+            'refundAmount' => $refundFee,
+            'refundReason' => '隔天冲销原路退回',
+            'thirdNo' => $cg->thirdNo ?? '',
+        ];
+        $response = $laResource->refund($aliParams);
+        if (!isset($response['code']) || $response['code'] != 'BBS00000') {
+            $errMsg = $response['msg'] ?? '';
+            util::fail('退款失败:' . $errMsg);
+        }
+        $refundTradeNo = isset($response['resp_data']['trade_no']) ? $response['resp_data']['trade_no'] : '';
+        $cgRefund->thirdRefundNo = $refundTradeNo;
+        $cgRefund->save(false, ['thirdRefundNo']);
+    }
+
     public static function onlyRefundMoney($ghsShop, $cg)
     {
         $refundOrderSn = orderSn::getCgRefundSn();
@@ -237,7 +294,14 @@ class CgRefundClass extends BaseClass
             }
         }
 
-        //记录总的退款金额
+        // 隔天冲销:累计 nextDayTkPrice,不改 actPrice;原路退在此发起
+        $sameDay = intval($cgRefund->sameDay ?? self::SAME_DAY_YES);
+        if ($sameDay === self::SAME_DAY_NO) {
+            \bizGhs\order\services\NextDayRefundService::applyCgAmountAndFund($cgRefund, $cg);
+            return;
+        }
+
+        //记录当天累计退款金额(影响 actPrice)
         $currentTkPrice = bcadd($cg->tkPrice, $refundPrice, 2);
         $cg->tkPrice = $currentTkPrice;
         $cg->refund = PurchaseClass::REFUND_YES;

+ 3 - 0
biz-hd/cg/services/CgRefundService.php

@@ -186,6 +186,9 @@ class CgRefundService extends BaseService
         $data['bigNum'] = $bigNum;
         $data['smallNum'] = $smallNum;
         $data['cause'] = $cause;
+        // 隔天冲销与销售侧成对:sameDay/fundType 由上游传入
+        $data['sameDay'] = intval($post['sameDay'] ?? CgRefundClass::SAME_DAY_YES);
+        $data['fundType'] = intval($post['fundType'] ?? 0);
         $cgRefund = CgRefundClass::add($data, true);
 
         if ($refundType == CgRefundClass::REFUND_TYPE_MONEY_GOOD) {