Explorar o código

Merge branch 'fw' into dev

# Conflicts:
#	app-ghs/controllers/CustomController.php
#	biz-hd/purchase/classes/PurchaseClass.php
shish hai 1 día
pai
achega
cba10b7b2d

+ 15 - 17
app-ghs/controllers/CustomController.php

@@ -7,11 +7,9 @@ use biz\ghs\classes\GhsClass;
 use biz\shop\classes\ShopAdminClass;
 use biz\sj\classes\SjClass;
 use biz\wx\classes\WxMessageClass;
-use biz\wx\classes\WxOpenClass;
 use bizGhs\custom\classes\CustomClass;
 use bizGhs\custom\services\CustomService;
 use bizGhs\custom\services\GhsRechargeSettleService;
-use common\components\miniUtil;
 use common\components\noticeUtil;
 use common\components\qrCodeUtil;
 use bizGhs\order\classes\OrderClass;
@@ -21,8 +19,10 @@ use bizHd\shop\classes\ShopClass;
 
 //use bizHd\stat\classes\StatVisitClass;
 use bizGhs\stat\classes\StatVisitClass;
+use bizHd\wx\classes\WxOpenClass;
 use common\components\dict;
 use common\components\imgUtil;
+use common\components\miniUtil;
 use common\components\orderSn;
 use common\components\stringUtil;
 use common\components\util;
@@ -120,10 +120,9 @@ class CustomController extends BaseController
     }
 
     /**
-     * 获取收款小程序码(区别于 actionGetGatheringCode 的普通二维码,扫码直接进小程序,不用先跳浏览器)ssh 冲销单功能
-     * 用途:affirmForward.vue 提交冲销单成功后的退款凭证海报(forwardResult.vue),识别后直接打开 hdApp 小程序的
-     * admin/ghs/pay 页面查看余额明细;与 actionGetGatheringCode 生成的参数含义完全一致(id=ghsId, salt=custom.salt),
-     * 只是二维码载体从"H5链接二维码"换成了"小程序码",hdApp 端 admin/ghs/pay.vue 已同步支持从 scene 里解析这两个参数。
+     * 客户向供货商充值的小程序码(冲销返余额海报用)
+     * 跳转花店端 admin/ghs/pay;ssh 冲销单功能
+     * GET: id=customId
      */
     public function actionGetGatheringMiniCode()
     {
@@ -138,16 +137,19 @@ class CustomController extends BaseController
         }
         $ghsId = $custom->ghsId ?? 0;
         $salt = $custom->salt ?? '';
-
+        // 使用花店小程序码,路径与 H5 收款一致:admin/ghs/pay
         $merchant = WxOpenClass::getWxInfo();
         $page = 'admin/ghs/pay';
         $ptStyle = dict::getDict('ptStyle', 'hd');
-        //小程序码scene不能超过32个字符,与H5链接的query参数含义保持一致:id=ghsId, salt=custom.salt
-        $scene = "id={$ghsId}&salt={$salt}";
+        $scene = 'id=' . $ghsId . '&salt=' . $salt;
         $envVersion = miniUtil::normalizeMiniEnvVersion($get['env_version'] ?? 'release');
-        $imgUrl = miniUtil::generateUnlimitedMiniCode($merchant, $page, $scene, $ptStyle, $envVersion);
-        $imageUrl = imgUtil::groupImg($imgUrl);
-        util::success(['imgUrl' => $imageUrl]);
+        $miniCode = miniUtil::generateUnlimitedMiniCode($merchant, $page, $scene, $ptStyle, $envVersion);
+        $imageUrl = imgUtil::groupImg($miniCode);
+        util::success([
+            'imgUrl' => $imageUrl,
+            'ghsId' => $ghsId,
+            'salt' => $salt,
+        ]);
     }
 
     //供货商给客户充值和结账 ssh 20240309
@@ -161,7 +163,7 @@ class CustomController extends BaseController
         util::checkRepeatCommit($staffId, 5);
 
         $staff = $this->shopAdmin;
-        if (!isset($staff->finance) || $staff->finance == 0) {
+        if (isset($staff->finance) == false || $staff->finance == 0) {
             if ($this->shopId == 17118) {
                 //淘花里中山店,没有财务权限要能销账
                 if (!in_array($this->adminId, [17908])) {
@@ -313,10 +315,6 @@ class CustomController extends BaseController
         }
         $connection = Yii::$app->db;
         $transaction = $connection->beginTransaction();
-
-        //检查客户余额是否有问题
-        //CustomClass::checkBalance($custom);
-
         try {
             $staff = $this->shopAdmin;
             $params = ['remark' => $remark, 'rechargeType' => $rechargeType];

+ 119 - 0
app-ghs/controllers/ForwardController.php

@@ -0,0 +1,119 @@
+<?php
+/**
+ * 供货商冲销凭证接口
+ * 用途:创建/按原单列表/详情;方案A凭证,不是负销售单。ssh 冲销单功能
+ */
+namespace ghs\controllers;
+
+use bizGhs\forward\services\ForwardService;
+use bizGhs\order\classes\OrderClass;
+use bizGhs\order\classes\RefundOrderClass;
+use common\components\util;
+use Yii;
+
+class ForwardController extends BaseController
+{
+    /**
+     * 创建冲销凭证
+     * POST: orderId|refundId|customId, fundType, forwardStock, items[], amount?, remark?
+     */
+    public function actionCreate()
+    {
+        $shopAdmin = $this->shopAdmin;
+        if (!isset($shopAdmin->super) || intval($shopAdmin->super) !== 1) {
+            util::fail('请超管操作冲销');
+        }
+        util::checkRepeatCommit($this->shopAdminId, 5);
+
+        $post = Yii::$app->request->post();
+        $post['shopId'] = $this->shopId;
+        $post['mainId'] = $this->mainId;
+        $post['sjId'] = $this->sjId;
+        $post['shopAdminId'] = $this->shopAdminId;
+        $post['shopAdminName'] = $shopAdmin->name ?? '';
+
+        $connection = Yii::$app->db;
+        $transaction = $connection->beginTransaction();
+        try {
+            $result = ForwardService::create($post);
+            $transaction->commit();
+            util::success($result, '冲销成功');
+        } catch (\Exception $e) {
+            $transaction->rollBack();
+            Yii::info('冲销失败:' . $e->getMessage());
+            util::fail($e->getMessage() ?: '冲销失败');
+        }
+    }
+
+    /**
+     * 按原销售单查冲销凭证列表
+     * GET: orderId
+     */
+    public function actionListByOrder()
+    {
+        $orderId = Yii::$app->request->get('orderId', 0);
+        $data = ForwardService::listByOrder($orderId, $this->mainId);
+        util::success($data);
+    }
+
+    /**
+     * 冲销凭证详情
+     * GET: id
+     */
+    public function actionDetail()
+    {
+        $id = Yii::$app->request->get('id', 0);
+        $data = ForwardService::detail($id, $this->mainId);
+        util::success($data);
+    }
+
+    /**
+     * 原单是否可冲销(售后页预检)
+     * GET: orderId
+     */
+    public function actionCheckEligible()
+    {
+        $orderId = Yii::$app->request->get('orderId', 0);
+        $order = OrderClass::getById($orderId, true);
+        if (empty($order) || intval($order->mainId) !== intval($this->mainId)) {
+            util::fail('没有找到订单');
+        }
+        $check = ForwardService::checkEligible($order);
+        $check['hasForward'] = intval($order->hasForward ?? 0);
+        $check['forwardPrice'] = $order->forwardPrice ?? '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';
+        util::success($check);
+    }
+
+    /**
+     * 售后是否可转冲销
+     * GET: refundId
+     */
+    public function actionCheckRefundEligible()
+    {
+        $refundId = Yii::$app->request->get('refundId', 0);
+        $refund = RefundOrderClass::getById($refundId, true);
+        RefundOrderClass::valid($refund, $this->mainId);
+        if (intval($refund->status) !== RefundOrderClass::STATUS_UN_COMPLETE) {
+            util::success(['ok' => false, 'reason' => '仅待审核售后可转冲销']);
+        }
+        if (!empty($refund->forwardId)) {
+            util::success(['ok' => false, 'reason' => '该售后已转冲销']);
+        }
+        $order = OrderClass::getByCondition(['orderSn' => $refund->relateOrderSn], true);
+        if (empty($order)) {
+            util::fail('没有找到原订单');
+        }
+        $check = ForwardService::checkEligible($order);
+        $check['refundId'] = $refund->id;
+        $check['refundPrice'] = $refund->refundPrice ?? '0.00';
+        $check['payWay'] = $order->payWay ?? 0;
+        $check['onlinePay'] = $order->onlinePay ?? 0;
+        $check['orderId'] = $order->id;
+        util::success($check);
+    }
+}

+ 15 - 0
app-ghs/controllers/StatItemController.php

@@ -3,6 +3,7 @@
 namespace ghs\controllers;
 
 use biz\stat\classes\StatItemClass;
+use bizGhs\forward\classes\ForwardClass;
 use bizGhs\order\classes\OrderClass;
 use bizGhs\order\classes\OrderItemClass;
 use bizGhs\product\classes\ProductClass;
@@ -147,6 +148,20 @@ class StatItemController extends BaseController
                 }
             }
         }
+        // 方案A:花材销量扣减成功冲销明细(数量/金额;毛利按金额同口径扣减)
+        $fwdItemMap = ForwardClass::sumItemByProduct($this->mainId, $currentStartTime, $currentEndTime);
+        foreach ($fwdItemMap as $fwdPid => $fwdRow) {
+            if (!isset($stat[$fwdPid])) {
+                continue;
+            }
+            $stat[$fwdPid]['num'] = bcsub((string)($stat[$fwdPid]['num'] ?? 0), (string)$fwdRow['num'], 2);
+            $stat[$fwdPid]['amount'] = bcsub((string)($stat[$fwdPid]['amount'] ?? 0), (string)$fwdRow['amount'], 2);
+            $stat[$fwdPid]['gross'] = bcsub((string)($stat[$fwdPid]['gross'] ?? 0), (string)$fwdRow['amount'], 2);
+            $totalNum = bcsub((string)$totalNum, (string)$fwdRow['num'], 2);
+            $totalAmount = bcsub((string)$totalAmount, (string)$fwdRow['amount'], 2);
+            $totalMl = bcsub((string)$totalMl, (string)$fwdRow['amount'], 2);
+        }
+
         //增加毛利率
         foreach ($stat as $key => $val) {
             $profit = $val['gross'] ?? 0;

+ 101 - 0
app-hd/controllers/ForwardController.php

@@ -0,0 +1,101 @@
+<?php
+/**
+ * 花店冲销凭证只读接口
+ * 用途:按本侧 xhCgForward 查列表/详情,不跨读 GHS 冲销表。ssh 冲销单功能
+ */
+namespace hd\controllers;
+
+use bizHd\cg\classes\CgForwardClass;
+use bizHd\cg\classes\CgForwardItemClass;
+use bizHd\purchase\classes\PurchaseClass;
+use common\components\util;
+use Yii;
+
+class ForwardController extends BaseController
+{
+    /**
+     * 按原采购单查冲销凭证列表
+     * GET: cgId
+     */
+    public function actionListByCg()
+    {
+        $cgId = intval(Yii::$app->request->get('cgId', 0));
+        $cg = PurchaseClass::getById($cgId, true);
+        if (empty($cg) || intval($cg->mainId) !== intval($this->mainId)) {
+            util::fail('没有找到采购单');
+        }
+        $list = CgForwardClass::getAllByCondition(
+            ['cgId' => $cgId, 'status' => CgForwardClass::STATUS_SUCCESS],
+            'id DESC',
+            '*',
+            null,
+            true
+        );
+        $rows = [];
+        foreach ($list ?: [] as $row) {
+            $rows[] = [
+                'id' => $row->id,
+                'forwardSn' => $row->forwardSn,
+                'cgId' => $row->cgId,
+                'cgSn' => $row->cgSn,
+                'amount' => $row->amount,
+                'payWay' => $row->payWay,
+                'fundType' => $row->fundType,
+                'forwardStock' => $row->forwardStock,
+                'status' => $row->status,
+                'remark' => $row->remark,
+                'addTime' => $row->addTime,
+                'ghsForwardId' => $row->ghsForwardId,
+            ];
+        }
+        util::success([
+            'list' => $rows,
+            'cgId' => $cgId,
+            'forwardPrice' => $cg->forwardPrice ?? '0.00',
+            'hasForward' => intval($cg->hasForward ?? 0),
+        ]);
+    }
+
+    /**
+     * 花店侧冲销凭证详情
+     * GET: id
+     */
+    public function actionDetail()
+    {
+        $id = intval(Yii::$app->request->get('id', 0));
+        $forward = CgForwardClass::getById($id, true);
+        if (empty($forward) || intval($forward->mainId) !== intval($this->mainId)) {
+            util::fail('没有找到冲销凭证');
+        }
+        $items = CgForwardItemClass::getAllByCondition(['forwardId' => $id], null, '*', null, true);
+        $itemRows = [];
+        foreach ($items ?: [] as $it) {
+            $itemRows[] = [
+                'id' => $it->id,
+                'productId' => $it->productId,
+                'name' => $it->name,
+                'num' => $it->num,
+                'bigNum' => $it->bigNum,
+                'smallNum' => $it->smallNum,
+                'unitPrice' => $it->unitPrice,
+                'price' => $it->price,
+                'amount' => $it->amount,
+            ];
+        }
+        util::success([
+            'id' => $forward->id,
+            'forwardSn' => $forward->forwardSn,
+            'cgId' => $forward->cgId,
+            'cgSn' => $forward->cgSn,
+            'amount' => $forward->amount,
+            'payWay' => $forward->payWay,
+            'fundType' => $forward->fundType,
+            'forwardStock' => $forward->forwardStock,
+            'status' => $forward->status,
+            'remark' => $forward->remark,
+            'addTime' => $forward->addTime,
+            'ghsForwardId' => $forward->ghsForwardId,
+            'items' => $itemRows,
+        ]);
+    }
+}

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

@@ -7,6 +7,7 @@ use biz\sj\classes\SjClass;
 use bizGhs\admin\classes\AdminClass;
 use bizGhs\book\classes\BookCustomClass;
 use bizGhs\book\classes\BookItemCustomClass;
+use bizGhs\custom\classes\AccountMoneyClass;
 use bizGhs\custom\models\Custom;
 use bizGhs\ghs\classes\GhsBalanceChangeClass;
 use bizGhs\order\classes\OrderClass;
@@ -1502,4 +1503,78 @@ class CustomClass extends BaseClass
         return ['pfAmount' => $pfAmount, 'lsAmount' => $lsAmount];
     }
 
+    /**
+     * 冲销凭证返充余额:同步增加 xhGhsCustom.balance 与镜像 xhGhs.balance,并各写余额变动。
+     * 用途:方案A ForwardService 在 fundType=返余额时调用;挂账结清后再冲销时语义等同「冲减欠款」。
+     * @param $custom object 已加锁的客户模型
+     * @param $forward object 冲销凭证模型(xhGhsForward)
+     * @param $returnAmount string|float 返充金额(正数)
+     * @return string 返充后的客户余额
+     */
+    public static function forwardReturnBalance($custom, $forward, $returnAmount)
+    {
+        $returnAmount = bcadd((string)$returnAmount, '0', 2);
+        if (bccomp($returnAmount, '0', 2) <= 0) {
+            util::fail('返充金额必须大于0');
+        }
+        AccountMoneyClass::ensureCustomMoneyReady($custom, true);
+        $ghsId = $custom->ghsId ?? 0;
+        $ghs = GhsClass::getLockById($ghsId);
+        if (!empty($ghs)) {
+            AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
+            self::assertPairBalanceEqual($custom, $ghs);
+        }
+        $newBalance = bcadd((string)($custom->balance ?? '0'), $returnAmount, 2);
+        self::savePairBalanceAfterDeduct($custom, $ghs, $newBalance);
+
+        $forwardSn = $forward->forwardSn ?? '';
+        $customId = $custom->id ?? 0;
+        $capitalTypeCustom = dict::getDict('capitalType', 'ghsHelpCustomRechargeReturn', 'id');
+        $capitalTypeGhs = dict::getDict('capitalType', 'customAskGhsRechargeReturn', 'id');
+        CustomBalanceChangeClass::add([
+            'relateId' => $forward->id ?? 0,
+            'customId' => $customId,
+            'customName' => $custom->name ?? '',
+            'ptStyle' => dict::getDict('ptStyle', 'ghs'),
+            'capitalType' => $capitalTypeCustom,
+            'amount' => $returnAmount,
+            'balance' => $newBalance,
+            'io' => 1,
+            'payWay' => dict::getDict('payWay', 'balancePay'),
+            'event' => "冲销返充余额,凭证号:{$forwardSn}",
+            'sjId' => $forward->sjId ?? 0,
+            'shopId' => $forward->shopId ?? 0,
+            'mainId' => $forward->mainId ?? 0,
+            'payTime' => date('Y-m-d H:i:s'),
+            'staffId' => $forward->shopAdminId ?? 0,
+            'staffName' => $forward->shopAdminName ?? '',
+            'side' => 0,
+            'fromType' => dict::getDict('fromType', 'shop'),
+            'remark' => '',
+        ], true);
+        if (!empty($ghs)) {
+            $customShopId = $custom->shopId ?? 0;
+            $customShop = ShopClass::getById($customShopId, true);
+            GhsBalanceChangeClass::add([
+                'ghsId' => $ghs->id ?? 0,
+                'relateId' => $forward->id ?? 0,
+                'ptStyle' => dict::getDict('ptStyle', 'ghs'),
+                'capitalType' => $capitalTypeGhs,
+                'amount' => $returnAmount,
+                'balance' => $newBalance,
+                'io' => 1,
+                'side' => 0,
+                'onlinePay' => 1,
+                'payWay' => dict::getDict('payWay', 'balancePay'),
+                'fromType' => dict::getDict('fromType', 'shop'),
+                'event' => "冲销返充余额,凭证号:{$forwardSn}",
+                'sjId' => $customShop->sjId ?? ($custom->sjId ?? 0),
+                'mainId' => $customShop->mainId ?? 0,
+                'shopId' => $customShopId,
+                'remark' => '',
+            ], true);
+        }
+        return $newBalance;
+    }
+
 }

+ 113 - 0
biz-ghs/forward/classes/ForwardClass.php

@@ -0,0 +1,113 @@
+<?php
+
+namespace bizGhs\forward\classes;
+
+use bizGhs\base\classes\BaseClass;
+use Yii;
+
+/**
+ * 供货商冲销凭证 Class
+ * 职责:凭证 CRUD 辅助 + 统计汇总查询(按时间/客户/花材扣减销量与金额);ssh 冲销单功能
+ */
+class ForwardClass extends BaseClass
+{
+    public static $baseFile = '\bizGhs\forward\models\Forward';
+
+    const STATUS_PROCESSING = 0;
+    const STATUS_SUCCESS = 1;
+    const STATUS_FAIL = 2;
+
+    const FUND_ORIGINAL = 1; // 原路退
+    const FUND_BALANCE = 2;  // 返余额
+    const FUND_NONE = 3;     // 仅记账
+
+    const STOCK_RETURN = 0;  // 回库存
+    const STOCK_KEEP = 1;    // 不回库存
+
+    /**
+     * 按主体+时间汇总成功冲销金额(用于收入类统计扣减)
+     * @return string 金额字符串
+     */
+    public static function sumAmountByMainAndTime($mainId, $startTime, $endTime)
+    {
+        $sql = "SELECT COALESCE(SUM(amount),0) AS total FROM xhGhsForward
+            WHERE mainId=:mainId AND status=:status AND addTime BETWEEN :start AND :end";
+        $row = Yii::$app->db->createCommand($sql, [
+            ':mainId' => $mainId,
+            ':status' => self::STATUS_SUCCESS,
+            ':start' => $startTime,
+            ':end' => $endTime,
+        ])->queryOne();
+        return bcadd($row['total'] ?? '0', '0', 2);
+    }
+
+    /**
+     * 按客户汇总成功冲销金额(customId => amount)
+     */
+    public static function sumAmountGroupByCustom($mainId, $startTime, $endTime)
+    {
+        $sql = "SELECT customId, COALESCE(SUM(amount),0) AS total FROM xhGhsForward
+            WHERE mainId=:mainId AND status=:status AND addTime BETWEEN :start AND :end
+            GROUP BY customId";
+        $rows = Yii::$app->db->createCommand($sql, [
+            ':mainId' => $mainId,
+            ':status' => self::STATUS_SUCCESS,
+            ':start' => $startTime,
+            ':end' => $endTime,
+        ])->queryAll();
+        $map = [];
+        foreach ($rows as $row) {
+            $map[$row['customId']] = bcadd($row['total'] ?? '0', '0', 2);
+        }
+        return $map;
+    }
+
+    /**
+     * 按花材汇总成功冲销数量与金额(productId => [num, amount])
+     */
+    public static function sumItemByProduct($mainId, $startTime, $endTime)
+    {
+        $sql = "SELECT i.productId, COALESCE(SUM(i.num),0) AS num, COALESCE(SUM(i.amount),0) AS amount
+            FROM xhGhsForwardItem i
+            INNER JOIN xhGhsForward f ON f.id = i.forwardId
+            WHERE f.mainId=:mainId AND f.status=:status AND f.addTime BETWEEN :start AND :end
+            GROUP BY i.productId";
+        $rows = Yii::$app->db->createCommand($sql, [
+            ':mainId' => $mainId,
+            ':status' => self::STATUS_SUCCESS,
+            ':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;
+    }
+
+    /**
+     * 按客户汇总成功冲销花材数量合计(customId => num)
+     */
+    public static function sumItemNumGroupByCustom($mainId, $startTime, $endTime)
+    {
+        $sql = "SELECT i.customId, COALESCE(SUM(i.num),0) AS num
+            FROM xhGhsForwardItem i
+            INNER JOIN xhGhsForward f ON f.id = i.forwardId
+            WHERE f.mainId=:mainId AND f.status=:status AND f.addTime BETWEEN :start AND :end
+            GROUP BY i.customId";
+        $rows = Yii::$app->db->createCommand($sql, [
+            ':mainId' => $mainId,
+            ':status' => self::STATUS_SUCCESS,
+            ':start' => $startTime,
+            ':end' => $endTime,
+        ])->queryAll();
+        $map = [];
+        foreach ($rows as $row) {
+            $map[$row['customId']] = bcadd($row['num'] ?? '0', '0', 2);
+        }
+        return $map;
+    }
+}

+ 13 - 0
biz-ghs/forward/classes/ForwardItemClass.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace bizGhs\forward\classes;
+
+use bizGhs\base\classes\BaseClass;
+
+/**
+ * 供货商冲销凭证明细 Class;ssh 冲销单功能
+ */
+class ForwardItemClass extends BaseClass
+{
+    public static $baseFile = '\bizGhs\forward\models\ForwardItem';
+}

+ 17 - 0
biz-ghs/forward/models/Forward.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace bizGhs\forward\models;
+
+use bizGhs\base\models\Base;
+
+/**
+ * 供货商冲销凭证主表 xhGhsForward
+ * 用途:方案A冲销载体(不是负销售单),记录资金/库存处理结果;ssh 冲销单功能
+ */
+class Forward extends Base
+{
+    public static function tableName()
+    {
+        return 'xhGhsForward';
+    }
+}

+ 17 - 0
biz-ghs/forward/models/ForwardItem.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace bizGhs\forward\models;
+
+use bizGhs\base\models\Base;
+
+/**
+ * 供货商冲销凭证明细 xhGhsForwardItem
+ * 用途:按花材记录冲销数量与金额,供销量统计扣减;ssh 冲销单功能
+ */
+class ForwardItem extends Base
+{
+    public static function tableName()
+    {
+        return 'xhGhsForwardItem';
+    }
+}

+ 701 - 0
biz-ghs/forward/services/ForwardService.php

@@ -0,0 +1,701 @@
+<?php
+
+namespace bizGhs\forward\services;
+
+use biz\ghs\classes\GhsClass;
+use bizGhs\base\services\BaseService;
+use bizGhs\custom\classes\CustomClass;
+use bizGhs\forward\classes\ForwardClass;
+use bizGhs\forward\classes\ForwardItemClass;
+use bizGhs\order\classes\OrderClass;
+use bizGhs\order\classes\OrderItemClass;
+use bizGhs\order\classes\RefundOrderClass;
+use bizGhs\order\classes\RefundOrderItemClass;
+use bizGhs\product\classes\ProductClass;
+use bizGhs\stock\classes\StockRecordClass;
+use bizHd\cg\classes\CgForwardClass;
+use bizHd\cg\classes\CgForwardItemClass;
+use bizHd\cg\classes\CgRefundClass;
+use bizHd\purchase\classes\PurchaseClass;
+use bizHd\purchase\classes\PurchaseItemClass;
+use bizHd\shop\classes\ShopClass as HdShopClass;
+use common\components\dict;
+use common\components\orderSn;
+use common\components\util;
+
+/**
+ * 冲销凭证服务(方案A)
+ * 职责:同事务创建双侧凭证/明细,处理资金三分支、库存回退、原单汇总缓存、售后转凭证取消;
+ * 不插入负销售单/负采购单。ssh 冲销单功能
+ */
+class ForwardService extends BaseService
+{
+    public static $baseFile = '\bizGhs\forward\classes\ForwardClass';
+
+    /**
+     * 创建冲销凭证(唯一写入口)
+     * @param array $post 含 orderId/refundId/customId、fundType、forwardStock、items、门店操作人等
+     * @return array {forwardId, forwardSn, cgForwardId, fundType, amount, customId}
+     */
+    public static function create($post)
+    {
+        $orderId = intval($post['orderId'] ?? 0);
+        $refundId = intval($post['refundId'] ?? 0);
+        $customId = intval($post['customId'] ?? 0);
+        $fundType = intval($post['fundType'] ?? 0);
+        $forwardStock = intval($post['forwardStock'] ?? ForwardClass::STOCK_KEEP);
+        $remark = trim((string)($post['remark'] ?? ''));
+        $now = date('Y-m-d H:i:s');
+
+        $order = null;
+        $cg = null;
+        $refund = null;
+        $cgRefund = null;
+        $items = $post['items'] ?? [];
+
+        // 售后转冲销:以售后明细为准,并锁原单
+        if ($refundId > 0) {
+            $refund = RefundOrderClass::getLockById($refundId);
+            if (empty($refund)) {
+                util::fail('没有找到售后单');
+            }
+            if (intval($refund->status) !== RefundOrderClass::STATUS_UN_COMPLETE) {
+                util::fail('仅待审核售后可转冲销');
+            }
+            if (!empty($refund->forwardId)) {
+                util::fail('该售后已转冲销');
+            }
+            $relateOrderSn = $refund->relateOrderSn ?? '';
+            $orderRow = OrderClass::getByCondition(['orderSn' => $relateOrderSn], true);
+            if (empty($orderRow)) {
+                util::fail('没有找到原订单');
+            }
+            $order = OrderClass::getLockById($orderRow->id);
+            if (empty($order)) {
+                util::fail('没有找到原订单');
+            }
+            $orderId = intval($order->id);
+            $customId = intval($order->customId ?? 0);
+            $items = self::buildItemsFromRefund($refund);
+            $post['amount'] = $refund->refundPrice ?? ($post['amount'] ?? 0);
+            $cgRefundId = intval($refund->cgRefundId ?? 0);
+            if ($cgRefundId > 0) {
+                $cgRefund = CgRefundClass::getLockById($cgRefundId);
+            }
+        } elseif ($orderId > 0) {
+            $order = OrderClass::getLockById($orderId);
+            if (empty($order)) {
+                util::fail('没有找到原订单');
+            }
+            $customId = intval($order->customId ?? 0);
+        }
+
+        if ($customId <= 0) {
+            util::fail('请选择客户');
+        }
+        $custom = CustomClass::getLockById($customId);
+        if (empty($custom)) {
+            util::fail('没有找到客户');
+        }
+        $mainId = intval($post['mainId'] ?? ($order->mainId ?? ($custom->ownMainId ?? 0)));
+        if (intval($custom->ownMainId ?? 0) !== $mainId) {
+            util::fail('不是你的客户');
+        }
+
+        $hasRelate = !empty($order) || $refundId > 0;
+        if ($hasRelate) {
+            self::assertEligible($order);
+            self::assertOrderCanForward($order, $mainId);
+        }
+
+        // 解析明细与金额(仅退款无花材明细时允许空明细,仍记金额)
+        $normalizedItems = self::normalizeItems($items, $order);
+        $amount = bcadd((string)($post['amount'] ?? '0'), '0', 2);
+        if (bccomp($amount, '0', 2) <= 0) {
+            $amount = '0.00';
+            foreach ($normalizedItems as $it) {
+                $amount = bcadd($amount, $it['amount'], 2);
+            }
+        }
+        if (bccomp($amount, '0', 2) <= 0) {
+            util::fail('冲销金额必须大于0');
+        }
+        if (empty($normalizedItems) && $refundId <= 0) {
+            util::fail('请选择冲销花材');
+        }
+
+        if (!empty($order)) {
+            $forwarded = bcadd((string)($order->forwardPrice ?? '0'), '0', 2);
+            $cap = bcadd($forwarded, $amount, 2);
+            $actPrice = bcadd((string)($order->actPrice ?? '0'), '0', 2);
+            if (bccomp($cap, $actPrice, 2) === 1) {
+                util::fail('累计冲销金额不能超过订单实付金额');
+            }
+            $purchaseId = intval($order->purchaseId ?? 0);
+            if ($purchaseId > 0) {
+                $cg = PurchaseClass::getLockById($purchaseId);
+            }
+        }
+
+        $payWay = !empty($order) ? intval($order->payWay ?? 0) : intval($post['payWay'] ?? dict::getDict('payWay', 'balancePay'));
+        $fundType = self::resolveFundType($fundType, $payWay, $order, $hasRelate);
+
+        $forwardSn = orderSn::getGhsForwardSn();
+        $cgForwardSn = orderSn::getCgForwardSn();
+        $ghsId = intval($custom->ghsId ?? 0);
+        $shopId = intval($post['shopId'] ?? ($order->shopId ?? ($custom->ownShopId ?? 0)));
+        $sjId = intval($post['sjId'] ?? ($order->sjId ?? ($custom->sjId ?? 0)));
+        $shopAdminId = intval($post['shopAdminId'] ?? 0);
+        $shopAdminName = (string)($post['shopAdminName'] ?? '');
+
+        // 花店侧主体/门店
+        $hdShopId = intval($custom->shopId ?? 0);
+        $hdShop = HdShopClass::getById($hdShopId, true);
+        $hdMainId = intval($hdShop->mainId ?? 0);
+        $hdSjId = intval($hdShop->sjId ?? ($custom->sjId ?? 0));
+
+        // 处理中写 GHS 凭证
+        $ghsForward = ForwardClass::add([
+            'forwardSn' => $forwardSn,
+            'mainId' => $mainId,
+            'shopId' => $shopId,
+            'sjId' => $sjId,
+            'customId' => $customId,
+            'ghsId' => $ghsId,
+            'orderId' => $orderId,
+            'orderSn' => $order->orderSn ?? '',
+            'amount' => $amount,
+            'payWay' => $payWay,
+            'fundType' => $fundType,
+            'forwardStock' => $forwardStock,
+            'cgForwardId' => 0,
+            'refundId' => $refundId,
+            'thirdRefundNo' => '',
+            'status' => ForwardClass::STATUS_PROCESSING,
+            'remark' => $remark,
+            'shopAdminId' => $shopAdminId,
+            'shopAdminName' => $shopAdminName,
+            'addTime' => $now,
+        ], true);
+
+        foreach ($normalizedItems as $it) {
+            ForwardItemClass::add([
+                'forwardId' => $ghsForward->id,
+                'forwardSn' => $forwardSn,
+                'mainId' => $mainId,
+                'customId' => $customId,
+                'productId' => $it['productId'],
+                'orderItemId' => $it['orderItemId'],
+                'name' => $it['name'],
+                'num' => $it['num'],
+                'bigNum' => $it['bigNum'],
+                'smallNum' => $it['smallNum'],
+                'unitPrice' => $it['unitPrice'],
+                'price' => $it['price'],
+                'amount' => $it['amount'],
+                'addTime' => $now,
+            ]);
+        }
+
+        // HD 镜像凭证
+        $cgForward = CgForwardClass::add([
+            'forwardSn' => $cgForwardSn,
+            'mainId' => $hdMainId,
+            'shopId' => $hdShopId,
+            'sjId' => $hdSjId,
+            'customId' => $customId,
+            'ghsId' => $ghsId,
+            'cgId' => intval($cg->id ?? 0),
+            'cgSn' => $cg->orderSn ?? '',
+            'ghsForwardId' => $ghsForward->id,
+            'amount' => $amount,
+            'payWay' => $payWay,
+            'fundType' => $fundType,
+            'forwardStock' => $forwardStock,
+            'cgRefundId' => intval($cgRefund->id ?? 0),
+            'status' => CgForwardClass::STATUS_PROCESSING,
+            'remark' => $remark,
+            'addTime' => $now,
+        ], true);
+
+        // 互挂 id
+        $ghsForward->cgForwardId = $cgForward->id;
+        $ghsForward->save(false, ['cgForwardId']);
+
+        $cgItemMap = [];
+        if (!empty($cg)) {
+            $cgItems = PurchaseItemClass::getAllByCondition(['orderSn' => $cg->orderSn], null, '*', null, true);
+            foreach ($cgItems ?: [] as $ci) {
+                $cgItemMap[intval($ci->itemId ?? 0)] = $ci;
+            }
+        }
+        foreach ($normalizedItems as $it) {
+            $cgItemId = 0;
+            $ptItemId = intval($it['itemId'] ?? 0);
+            if ($ptItemId > 0 && isset($cgItemMap[$ptItemId])) {
+                $cgItemId = intval($cgItemMap[$ptItemId]->id ?? 0);
+            }
+            CgForwardItemClass::add([
+                'forwardId' => $cgForward->id,
+                'forwardSn' => $cgForwardSn,
+                'ghsId' => $ghsId,
+                'customId' => $customId,
+                'productId' => $it['productId'],
+                'cgItemId' => $cgItemId,
+                'name' => $it['name'],
+                'num' => $it['num'],
+                'bigNum' => $it['bigNum'],
+                'smallNum' => $it['smallNum'],
+                'unitPrice' => $it['unitPrice'],
+                'price' => $it['price'],
+                'amount' => $it['amount'],
+                'addTime' => $now,
+            ]);
+        }
+
+        // 资金分支(原单 actPrice/tkPrice 不动)
+        $thirdRefundNo = '';
+        if ($fundType === ForwardClass::FUND_ORIGINAL) {
+            if (empty($cg)) {
+                util::fail('无原采购单,无法原路退回');
+            }
+            $thirdRefundNo = PurchaseClass::forwardOriginalOnlineRefund($cg, $forwardSn, $amount, $remark ?: '冲销原路退回');
+            $ghsForward->thirdRefundNo = $thirdRefundNo;
+            $ghsForward->save(false, ['thirdRefundNo']);
+        } elseif ($fundType === ForwardClass::FUND_BALANCE) {
+            CustomClass::forwardReturnBalance($custom, $ghsForward, $amount);
+        }
+        // FUND_NONE:仅记账,不动作
+
+        // 库存:仅 GHS 仓,与资金解耦
+        if ($forwardStock === ForwardClass::STOCK_RETURN) {
+            self::returnStock($normalizedItems, $forwardSn, $custom, $shopId, $sjId, $mainId);
+        }
+
+        // 原单/采购汇总缓存
+        if (!empty($order)) {
+            $order->hasForward = 1;
+            $order->forwardPrice = bcadd((string)($order->forwardPrice ?? '0'), $amount, 2);
+            $order->save(false, ['hasForward', 'forwardPrice']);
+            self::increaseOrderItemForwardNum($normalizedItems);
+        }
+        if (!empty($cg)) {
+            $cg->hasForward = 1;
+            $cg->forwardPrice = bcadd((string)($cg->forwardPrice ?? '0'), $amount, 2);
+            $cg->save(false, ['hasForward', 'forwardPrice']);
+            self::increaseCgItemForwardNum($normalizedItems, $cgItemMap);
+        }
+
+        // 消费累计:减金额,不加笔数
+        $buyAmount = bcsub((string)($custom->buyAmount ?? '0'), $amount, 2);
+        if (bccomp($buyAmount, '0', 2) < 0) {
+            $buyAmount = '0.00';
+        }
+        $custom->buyAmount = $buyAmount;
+        $custom->save(false, ['buyAmount']);
+        if ($ghsId > 0) {
+            $ghs = GhsClass::getLockById($ghsId);
+            if (!empty($ghs) && isset($ghs->expendAmount)) {
+                $expend = bcsub((string)($ghs->expendAmount ?? '0'), $amount, 2);
+                if (bccomp($expend, '0', 2) < 0) {
+                    $expend = '0.00';
+                }
+                $ghs->expendAmount = $expend;
+                $ghs->save(false, ['expendAmount']);
+            }
+        }
+
+        // 售后转冲销:取消两侧售后并回写 forwardId(不走 passRefund)
+        if (!empty($refund)) {
+            $refund->status = RefundOrderClass::STATUS_CANCEL;
+            $refund->forwardId = $ghsForward->id;
+            $refund->remark = trim(($refund->remark ?? '') . ';已转冲销凭证' . $forwardSn, ';');
+            $refund->save(false, ['status', 'forwardId', 'remark']);
+            if (!empty($cgRefund)) {
+                $cgRefund->status = CgRefundClass::STATUS_CANCEL;
+                $cgRefund->forwardId = $cgForward->id;
+                $cgRefund->remark = trim(($cgRefund->remark ?? '') . ';已转冲销凭证' . $cgForwardSn, ';');
+                $cgRefund->save(false, ['status', 'forwardId', 'remark']);
+            }
+        }
+
+        $ghsForward->status = ForwardClass::STATUS_SUCCESS;
+        $ghsForward->save(false, ['status']);
+        $cgForward->status = CgForwardClass::STATUS_SUCCESS;
+        $cgForward->save(false, ['status']);
+
+        return [
+            'forwardId' => $ghsForward->id,
+            'forwardSn' => $forwardSn,
+            'cgForwardId' => $cgForward->id,
+            'fundType' => $fundType,
+            'amount' => $amount,
+            'customId' => $customId,
+            'thirdRefundNo' => $thirdRefundNo,
+        ];
+    }
+
+    /**
+     * 有原单时的冲销资格:非当天支付,或挂账已结清(debtPrice>0 且 remainDebtPrice=0)
+     */
+    public static function assertEligible($order)
+    {
+        if (empty($order)) {
+            util::fail('没有原订单');
+        }
+        $payTime = $order->payTime ?? ($order->addTime ?? '');
+        $notToday = true;
+        if (!empty($payTime)) {
+            $notToday = 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;
+        if (!$notToday && !$debtCleared) {
+            util::fail('当天订单请走常规售后,不可冲销');
+        }
+    }
+
+    /**
+     * 前端资格预检(不调用 util::fail,供详情页展示按钮)
+     */
+    public static function checkEligible($order)
+    {
+        if (empty($order)) {
+            return ['ok' => false, 'reason' => '没有原订单'];
+        }
+        $payTime = $order->payTime ?? ($order->addTime ?? '');
+        $notToday = true;
+        if (!empty($payTime)) {
+            $notToday = 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;
+        if (!$notToday && !$debtCleared) {
+            return ['ok' => false, 'reason' => '当天订单请走常规售后,不可冲销'];
+        }
+        return ['ok' => true, 'reason' => ''];
+    }
+
+    /**
+     * 原单基础校验:归属、状态、结账
+     */
+    protected static function assertOrderCanForward($order, $mainId)
+    {
+        if (intval($order->mainId ?? 0) !== intval($mainId)) {
+            util::fail('不是你的订单');
+        }
+        if (intval($order->status) === OrderClass::ORDER_STATUS_CANCEL) {
+            util::fail('已取消订单不能冲销');
+        }
+        if (intval($order->status) !== OrderClass::ORDER_STATUS_COMPLETE) {
+            util::fail('仅已完成订单可冲销');
+        }
+        if (!empty($order->clearId)) {
+            util::fail('订单已结账,请线下处理');
+        }
+    }
+
+    /**
+     * 按支付方式强制/校正 fundType
+     * 线上:允许原路退或返余额(互斥,由入参决定);挂账/余额强制返余额;无原单禁止原路退
+     */
+    protected static function resolveFundType($fundType, $payWay, $order, $hasRelate)
+    {
+        $wxPay = dict::getDict('payWay', 'wxPay');
+        $aliPay = dict::getDict('payWay', 'alipay');
+        $debtPay = dict::getDict('payWay', 'debtPay');
+        $balancePay = dict::getDict('payWay', 'balancePay');
+        $onlinePay = !empty($order) ? intval($order->onlinePay ?? 0) : 0;
+        $isOnline = $onlinePay == dict::getDict('onlinePay', 'yes') && in_array($payWay, [$wxPay, $aliPay], true);
+
+        if (!$hasRelate) {
+            // 自由冲销:仅返余额或仅记账
+            if ($fundType === ForwardClass::FUND_ORIGINAL) {
+                util::fail('自由冲销不能原路退回');
+            }
+            if (!in_array($fundType, [ForwardClass::FUND_BALANCE, ForwardClass::FUND_NONE], true)) {
+                $fundType = ForwardClass::FUND_BALANCE;
+            }
+            return $fundType;
+        }
+
+        if ($payWay === $debtPay || $payWay === $balancePay) {
+            return ForwardClass::FUND_BALANCE;
+        }
+        if ($isOnline) {
+            if (!in_array($fundType, [ForwardClass::FUND_ORIGINAL, ForwardClass::FUND_BALANCE, ForwardClass::FUND_NONE], true)) {
+                util::fail('请选择资金处理方式');
+            }
+            return $fundType;
+        }
+        // 现金等其它:默认返余额,也允许仅记账
+        if (!in_array($fundType, [ForwardClass::FUND_BALANCE, ForwardClass::FUND_NONE], true)) {
+            return ForwardClass::FUND_BALANCE;
+        }
+        return $fundType;
+    }
+
+    /**
+     * 从售后明细生成冲销明细
+     */
+    protected static function buildItemsFromRefund($refund)
+    {
+        $refundSn = $refund->orderSn ?? '';
+        $list = RefundOrderItemClass::getAllByCondition(['orderSn' => $refundSn], null, '*', null, true);
+        if (empty($list)) {
+            // 仅退款无明细时,用金额做一条空花材记账(仍要求前端传 items 更稳妥)
+            return [];
+        }
+        $items = [];
+        $unitSmall = dict::getDict('unitType', 'small');
+        foreach ($list as $row) {
+            $unitType = intval($row->xhUnitType ?? 0);
+            $xhNum = bcadd((string)($row->xhNum ?? '0'), '0', 2);
+            $itemNum = bcadd((string)($row->itemNum ?? '0'), '0', 2);
+            $bigNum = $unitType === $unitSmall ? '0' : $xhNum;
+            $smallNum = $unitType === $unitSmall ? $xhNum : '0';
+            $items[] = [
+                'productId' => intval($row->productId ?? 0),
+                'orderItemId' => intval($row->orderItemId ?? 0),
+                'itemId' => intval($row->itemId ?? 0),
+                'name' => (string)($row->name ?? ''),
+                'num' => $itemNum,
+                'bigNum' => $bigNum,
+                'smallNum' => $smallNum,
+                'unitPrice' => bcadd((string)($row->xhUnitPrice ?? '0'), '0', 2),
+                'price' => bcadd((string)($row->xhUnitPrice ?? '0'), '0', 2),
+                'amount' => bcadd((string)($row->xhPrice ?? '0'), '0', 2),
+            ];
+        }
+        return $items;
+    }
+
+    /**
+     * 规范化明细:补全名称/数量/金额,并校验原单明细上限
+     */
+    protected static function normalizeItems($items, $order)
+    {
+        if (empty($items) || !is_array($items)) {
+            return [];
+        }
+        $orderItemMap = [];
+        if (!empty($order)) {
+            $orderItems = OrderItemClass::getAllByCondition(['orderSn' => $order->orderSn], null, '*', null, true);
+            foreach ($orderItems ?: [] as $oi) {
+                $orderItemMap[intval($oi->id)] = $oi;
+            }
+        }
+        $result = [];
+        foreach ($items as $raw) {
+            $productId = intval($raw['productId'] ?? 0);
+            $orderItemId = intval($raw['orderItemId'] ?? 0);
+            $orderItem = $orderItemId > 0 && isset($orderItemMap[$orderItemId]) ? $orderItemMap[$orderItemId] : null;
+            if (!empty($orderItem)) {
+                $productId = intval($orderItem->productId ?? $productId);
+            }
+            if ($productId <= 0) {
+                util::fail('花材信息缺失');
+            }
+            $name = (string)($raw['name'] ?? ($orderItem->name ?? ''));
+            if ($name === '') {
+                $product = ProductClass::getById($productId, true);
+                $name = $product->name ?? '';
+            }
+            $bigNum = bcadd((string)($raw['bigNum'] ?? '0'), '0', 2);
+            $smallNum = bcadd((string)($raw['smallNum'] ?? '0'), '0', 2);
+            $ratio = bcadd((string)($orderItem->ratio ?? ($raw['ratio'] ?? '1')), '0', 2);
+            if (bccomp($ratio, '0', 2) <= 0) {
+                $ratio = '1';
+            }
+            // 有大小单位时按售后口径换算 itemNum;否则用入参 num
+            if (bccomp($bigNum, '0', 2) > 0 || bccomp($smallNum, '0', 2) > 0) {
+                $num = ProductClass::mergeItemNum($bigNum, $smallNum, $ratio);
+            } else {
+                $num = bcadd((string)($raw['num'] ?? '0'), '0', 2);
+            }
+            if (bccomp($num, '0', 2) <= 0) {
+                util::fail('冲销数量必须大于0');
+            }
+            $unitPrice = bcadd((string)($raw['unitPrice'] ?? ($raw['price'] ?? '0')), '0', 2);
+            $price = bcadd((string)($raw['price'] ?? $unitPrice), '0', 2);
+            $amount = bcadd((string)($raw['amount'] ?? '0'), '0', 2);
+            if (bccomp($amount, '0', 2) <= 0) {
+                $baseNum = bccomp($bigNum, '0', 2) > 0 ? $bigNum : $num;
+                $amount = bcmul($baseNum, $price, 2);
+            }
+            if (!empty($orderItem)) {
+                $remain = bcsub((string)($orderItem->itemNum ?? '0'), (string)($orderItem->forwardNum ?? '0'), 2);
+                if (bccomp($num, $remain, 2) === 1) {
+                    util::fail(($name ?: '花材') . '冲销数量超过可冲销量');
+                }
+            }
+            $result[] = [
+                'productId' => $productId,
+                'orderItemId' => $orderItemId,
+                'itemId' => intval($raw['itemId'] ?? ($orderItem->itemId ?? 0)),
+                'name' => $name,
+                'num' => $num,
+                'bigNum' => $bigNum,
+                'smallNum' => $smallNum,
+                'unitPrice' => $unitPrice,
+                'price' => $price,
+                'amount' => $amount,
+            ];
+        }
+        return $result;
+    }
+
+    /**
+     * 回库存并写流水(关联 forwardSn)
+     */
+    protected static function returnStock($items, $forwardSn, $custom, $shopId, $sjId, $mainId)
+    {
+        $productIds = [];
+        foreach ($items as $it) {
+            $productIds[] = intval($it['productId']);
+        }
+        $productIds = array_values(array_unique(array_filter($productIds)));
+        sort($productIds);
+        foreach ($productIds as $pid) {
+            ProductClass::getLockById($pid);
+        }
+        $customName = $custom->name ?? '';
+        foreach ($items as $it) {
+            $itemNum = $it['num'];
+            if (bccomp($itemNum, '0', 2) <= 0) {
+                continue;
+            }
+            $stockInfo = ProductClass::addStockByItemNum($it['productId'], $itemNum);
+            $product = ProductClass::getById($it['productId'], true);
+            $recordData = [
+                'relateName' => $customName,
+                'itemNum' => $itemNum,
+                'sjId' => $sjId,
+                'shopId' => $shopId,
+                'mainId' => $mainId,
+                'orderSn' => $forwardSn,
+                'itemId' => $product->itemId ?? ($it['itemId'] ?? 0),
+                'oldStock' => $stockInfo['oldStock'] ?? 0,
+                'productId' => $it['productId'],
+                'newStock' => $stockInfo['newStock'] ?? 0,
+                'io' => 1,
+            ];
+            StockRecordClass::ghsRefundAddRecord($recordData);
+        }
+    }
+
+    /**
+     * 累加原销售明细 forwardNum
+     */
+    protected static function increaseOrderItemForwardNum($items)
+    {
+        foreach ($items as $it) {
+            $orderItemId = intval($it['orderItemId'] ?? 0);
+            if ($orderItemId <= 0) {
+                continue;
+            }
+            $oi = OrderItemClass::getLockById($orderItemId);
+            if (empty($oi)) {
+                continue;
+            }
+            $oi->forwardNum = bcadd((string)($oi->forwardNum ?? '0'), $it['num'], 2);
+            $oi->save(false, ['forwardNum']);
+        }
+    }
+
+    /**
+     * 累加采购明细 forwardNum(按平台 itemId 对齐)
+     */
+    protected static function increaseCgItemForwardNum($items, $cgItemMap)
+    {
+        foreach ($items as $it) {
+            $ptItemId = intval($it['itemId'] ?? 0);
+            if ($ptItemId <= 0 || !isset($cgItemMap[$ptItemId])) {
+                continue;
+            }
+            $ci = $cgItemMap[$ptItemId];
+            $ci = PurchaseItemClass::getLockById($ci->id);
+            if (empty($ci)) {
+                continue;
+            }
+            $ci->forwardNum = bcadd((string)($ci->forwardNum ?? '0'), $it['num'], 2);
+            $ci->save(false, ['forwardNum']);
+        }
+    }
+
+    /**
+     * 按原单查冲销凭证列表(GHS 侧)
+     */
+    public static function listByOrder($orderId, $mainId)
+    {
+        $orderId = intval($orderId);
+        $order = OrderClass::getById($orderId, true);
+        if (empty($order) || intval($order->mainId) !== intval($mainId)) {
+            util::fail('没有找到订单');
+        }
+        $list = ForwardClass::getAllByCondition(
+            ['orderId' => $orderId, 'status' => ForwardClass::STATUS_SUCCESS],
+            'id DESC',
+            '*',
+            null,
+            true
+        );
+        $rows = [];
+        foreach ($list ?: [] as $row) {
+            $rows[] = self::formatForwardRow($row);
+        }
+        return ['list' => $rows, 'orderId' => $orderId, 'forwardPrice' => $order->forwardPrice ?? '0.00'];
+    }
+
+    /**
+     * 凭证详情(含明细)
+     */
+    public static function detail($forwardId, $mainId)
+    {
+        $forward = ForwardClass::getById($forwardId, true);
+        if (empty($forward) || intval($forward->mainId) !== intval($mainId)) {
+            util::fail('没有找到冲销凭证');
+        }
+        $items = ForwardItemClass::getAllByCondition(['forwardId' => $forwardId], null, '*', null, true);
+        $itemRows = [];
+        foreach ($items ?: [] as $it) {
+            $itemRows[] = [
+                'id' => $it->id,
+                'productId' => $it->productId,
+                'name' => $it->name,
+                'num' => $it->num,
+                'bigNum' => $it->bigNum,
+                'smallNum' => $it->smallNum,
+                'unitPrice' => $it->unitPrice,
+                'price' => $it->price,
+                'amount' => $it->amount,
+            ];
+        }
+        $data = self::formatForwardRow($forward);
+        $data['items'] = $itemRows;
+        return $data;
+    }
+
+    protected static function formatForwardRow($row)
+    {
+        return [
+            'id' => $row->id,
+            'forwardSn' => $row->forwardSn,
+            'orderId' => $row->orderId,
+            'orderSn' => $row->orderSn,
+            'customId' => $row->customId,
+            'amount' => $row->amount,
+            'payWay' => $row->payWay,
+            'fundType' => $row->fundType,
+            'forwardStock' => $row->forwardStock,
+            'status' => $row->status,
+            'remark' => $row->remark,
+            'shopAdminName' => $row->shopAdminName,
+            'thirdRefundNo' => $row->thirdRefundNo,
+            'addTime' => $row->addTime,
+            'cgForwardId' => $row->cgForwardId,
+        ];
+    }
+}

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

@@ -3,6 +3,7 @@
 namespace bizGhs\stat\classes;
 
 use biz\shop\classes\ShopAdminClass;
+use bizGhs\forward\classes\ForwardClass;
 use bizGhs\order\classes\OrderClass;
 use bizGhs\order\classes\OrderItemClass;
 use bizGhs\order\classes\StockOutOrderClass;
@@ -414,6 +415,17 @@ class StatKdClass extends BaseClass
                 $staffAmountList[$staffId] = ['num' => 1, 'amount' => $currentAmount, 'staffName' => $staffName];
             }
         }
+        // 方案A:渠道收入总计扣减成功冲销金额(笔数不因冲销增减)
+        $fwdAmount = ForwardClass::sumAmountByMainAndTime($mainId, $currentStartTime, $currentEndTime);
+        if (bccomp((string)$fwdAmount, '0', 2) > 0) {
+            $incomeList['system']['amount'] = bcsub((string)$incomeList['system']['amount'], (string)$fwdAmount, 2);
+            $incomeList['system']['category']['pf']['amount'] = bcsub(
+                (string)$incomeList['system']['category']['pf']['amount'],
+                (string)$fwdAmount,
+                2
+            );
+        }
+
         return ['incomeList' => $incomeList, 'staffAmountList' => $staffAmountList, 'payCodeIncome' => $payCodeIncome, 'lsAfterSale' => $lsAfterSale];
 
     }

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

@@ -8,6 +8,7 @@ use bizGhs\cg\classes\CgRefundClass;
 use bizGhs\custom\classes\CustomClass;
 use bizGhs\custom\classes\DistClass;
 use bizGhs\expend\classes\ExpendClass;
+use bizGhs\forward\classes\ForwardClass;
 use bizGhs\item\classes\ItemClassClass;
 use bizGhs\order\classes\OrderItemClass;
 use bizGhs\order\classes\PurchaseOrderClass;
@@ -210,6 +211,24 @@ class StatSaleClass extends BaseClass
             }
             $arr = arrayUtil::arraySort($arr, 'profit');
         }
+        // 方案A:成功冲销凭证扣减客户采购金额与花材数量(笔数不减)
+        $fwdAmountMap = ForwardClass::sumAmountGroupByCustom($mainId, $currentStartTime, $currentEndTime);
+        $fwdNumMap = ForwardClass::sumItemNumGroupByCustom($mainId, $currentStartTime, $currentEndTime);
+        foreach ($fwdAmountMap as $cid => $fwdAmount) {
+            if (!isset($arr[$cid])) {
+                continue;
+            }
+            $arr[$cid]['amount'] = bcsub((string)$arr[$cid]['amount'], (string)$fwdAmount, 2);
+            $arr[$cid]['profit'] = bcsub((string)$arr[$cid]['profit'], (string)$fwdAmount, 2);
+            $totalAmount = bcsub((string)$totalAmount, (string)$fwdAmount, 2);
+            $totalProfit = bcsub((string)$totalProfit, (string)$fwdAmount, 2);
+        }
+        foreach ($fwdNumMap as $cid => $fwdNum) {
+            if (!isset($arr[$cid])) {
+                continue;
+            }
+            $arr[$cid]['count'] = bcsub((string)$arr[$cid]['count'], (string)$fwdNum, 2);
+        }
         $totalAmount = floatval($totalAmount);
         return ['list' => $arr, 'totalAmount' => $totalAmount, 'totalNum' => $totalNum, 'totalCost' => $totalCost, 'totalProfit' => $totalProfit];
     }
@@ -329,6 +348,8 @@ class StatSaleClass extends BaseClass
                 }
             }
         }
+        // 方案A:批发收入扣减成功冲销金额(笔数不变)
+        $pfIncome = bcsub((string)$pfIncome, ForwardClass::sumAmountByMainAndTime($mainId, $currentStartTime, $currentEndTime), 2);
 
         //零售开单
         $payCodeIncome = 0;
@@ -961,6 +982,21 @@ class StatSaleClass extends BaseClass
                 }
             }
         }
+        // 方案A:分类销量/金额扣减成功冲销明细
+        $fwdItemMap = ForwardClass::sumItemByProduct($mainId, $currentStartTime, $currentEndTime);
+        if (!empty($fwdItemMap)) {
+            $fwdProductIds = array_keys($fwdItemMap);
+            $fwdProducts = ProductClass::getAllByCondition(['id' => ['in', $fwdProductIds]], null, 'id,classId', 'id');
+            foreach ($fwdItemMap as $fwdPid => $fwdRow) {
+                $classId = intval($fwdProducts[$fwdPid]['classId'] ?? 0);
+                if (!isset($arr[$classId])) {
+                    continue;
+                }
+                $arr[$classId]['saleNum'] = bcsub((string)($arr[$classId]['saleNum'] ?? 0), (string)$fwdRow['num'], 2);
+                $arr[$classId]['income'] = bcsub((string)($arr[$classId]['income'] ?? 0), (string)$fwdRow['amount'], 2);
+            }
+            $pfIncome = bcsub((string)$pfIncome, ForwardClass::sumAmountByMainAndTime($mainId, $currentStartTime, $currentEndTime), 2);
+        }
 
         //零售开单
         $lsIncome = 0;
@@ -1506,6 +1542,26 @@ class StatSaleClass extends BaseClass
             }
             $arr = arrayUtil::arraySort($arr, 'profit');
         }
+        // 方案A:片区统计扣减冲销(按客户归属片区归集金额与数量,笔数不变)
+        $fwdAmountMap = ForwardClass::sumAmountGroupByCustom($mainId, $currentStartTime, $currentEndTime);
+        $fwdNumMap = ForwardClass::sumItemNumGroupByCustom($mainId, $currentStartTime, $currentEndTime);
+        foreach ($fwdAmountMap as $cid => $fwdAmount) {
+            $distId = $customList[$cid]['distId'] ?? 0;
+            if (!isset($arr[$distId])) {
+                continue;
+            }
+            $arr[$distId]['amount'] = bcsub((string)$arr[$distId]['amount'], (string)$fwdAmount, 2);
+            $arr[$distId]['profit'] = bcsub((string)$arr[$distId]['profit'], (string)$fwdAmount, 2);
+            $totalAmount = bcsub((string)$totalAmount, (string)$fwdAmount, 2);
+            $totalProfit = bcsub((string)$totalProfit, (string)$fwdAmount, 2);
+        }
+        foreach ($fwdNumMap as $cid => $fwdNum) {
+            $distId = $customList[$cid]['distId'] ?? 0;
+            if (!isset($arr[$distId])) {
+                continue;
+            }
+            $arr[$distId]['count'] = bcsub((string)$arr[$distId]['count'], (string)$fwdNum, 2);
+        }
         $totalAmount = floatval($totalAmount);
         return ['list' => $arr, 'totalAmount' => $totalAmount, 'totalNum' => $totalNum, 'totalCost' => $totalCost, 'totalProfit' => $totalProfit];
     }

+ 61 - 0
biz-hd/cg/classes/CgForwardClass.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace bizHd\cg\classes;
+
+use bizGhs\base\classes\BaseClass;
+use Yii;
+
+/**
+ * 花店冲销凭证 Class
+ * 职责:HD 侧列表/详情查询与统计汇总;与 xhGhsForward 成对;ssh 冲销单功能
+ */
+class CgForwardClass extends BaseClass
+{
+    public static $baseFile = '\bizHd\cg\models\CgForward';
+
+    const STATUS_PROCESSING = 0;
+    const STATUS_SUCCESS = 1;
+    const STATUS_FAIL = 2;
+
+    /**
+     * 按供货商+时间汇总成功冲销金额(HD 采购统计扣减用)
+     */
+    public static function sumAmountByGhsAndTime($ghsId, $startTime, $endTime)
+    {
+        $sql = "SELECT COALESCE(SUM(amount),0) AS total FROM xhCgForward
+            WHERE ghsId=:ghsId AND status=:status AND addTime BETWEEN :start AND :end";
+        $row = Yii::$app->db->createCommand($sql, [
+            ':ghsId' => $ghsId,
+            ':status' => self::STATUS_SUCCESS,
+            ':start' => $startTime,
+            ':end' => $endTime,
+        ])->queryOne();
+        return bcadd($row['total'] ?? '0', '0', 2);
+    }
+
+    /**
+     * 按花材汇总成功冲销数量(ghsId 维度)
+     */
+    public static function sumItemByProduct($ghsId, $startTime, $endTime)
+    {
+        $sql = "SELECT i.productId, COALESCE(SUM(i.num),0) AS num, COALESCE(SUM(i.amount),0) AS amount
+            FROM xhCgForwardItem i
+            INNER JOIN xhCgForward f ON f.id = i.forwardId
+            WHERE f.ghsId=:ghsId AND f.status=:status AND f.addTime BETWEEN :start AND :end
+            GROUP BY i.productId";
+        $rows = Yii::$app->db->createCommand($sql, [
+            ':ghsId' => $ghsId,
+            ':status' => self::STATUS_SUCCESS,
+            ':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;
+    }
+}

+ 13 - 0
biz-hd/cg/classes/CgForwardItemClass.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace bizHd\cg\classes;
+
+use bizGhs\base\classes\BaseClass;
+
+/**
+ * 花店冲销凭证明细 Class;ssh 冲销单功能
+ */
+class CgForwardItemClass extends BaseClass
+{
+    public static $baseFile = '\bizHd\cg\models\CgForwardItem';
+}

+ 17 - 0
biz-hd/cg/models/CgForward.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace bizHd\cg\models;
+
+use bizHd\base\models\Base;
+
+/**
+ * 花店冲销凭证主表 xhCgForward
+ * 用途:与 xhGhsForward 成对镜像,HD 列表/统计读本侧表;ssh 冲销单功能
+ */
+class CgForward extends Base
+{
+    public static function tableName()
+    {
+        return 'xhCgForward';
+    }
+}

+ 17 - 0
biz-hd/cg/models/CgForwardItem.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace bizHd\cg\models;
+
+use bizHd\base\models\Base;
+
+/**
+ * 花店冲销凭证明细 xhCgForwardItem
+ * 用途:与 GHS 明细镜像,便于 HD 采购数量统计扣减;ssh 冲销单功能
+ */
+class CgForwardItem extends Base
+{
+    public static function tableName()
+    {
+        return 'xhCgForwardItem';
+    }
+}

+ 9 - 12
biz-hd/purchase/classes/PurchaseClass.php

@@ -1698,16 +1698,13 @@ class PurchaseClass extends BaseClass
     }
 
     /**
-     * 冲销单"原路退回":针对线上支付(微信/支付宝)的原采购单($cg),调用拉卡拉网关把冲销金额原路退回给付款人(花店)。
-     * 用途:GHS 侧"售后转冲销单"/"直接开冲销单+关联原单"场景下,原单是线上支付时,用户在弹框里选择"原路退回=是",
-     * 由 OrderService::createForwardOrder 调用本方法完成真实的网关退款。
-     * 说明:本方法只做网关退款调用,不修改 $cg 自身的 actPrice/realPrice/tkPrice/refund 等字段——
-     * 冲销单的金额抵销统一通过原单的 hasForward/forwardPrice 缓存字段记录,避免与这里产生重复统计。
-     * @param $cg object 原采购单模型(xhCg),调用前需由上层完成加锁(getLockById)
-     * @param $refundSn string 本次退款的唯一流水号,建议直接用冲销单自己的单号,天然保证唯一/幂等
-     * @param $refundAmount string|float 退款金额(正数)
-     * @param $remark string 退款备注(拉卡拉退款原因)
-     * @return string 第三方退款流水号(拉卡拉 trade_no),失败会直接 util::fail 中断
+     * 冲销凭证「原路退回」:对线上支付原采购单调拉卡拉网关退款。
+     * 只做网关调用,不改原单 actPrice/tkPrice(冲销金额走原单 forwardPrice 缓存);ssh 冲销单功能
+     * @param $cg object 已加锁的原采购单
+     * @param $refundSn string 退款流水号(建议用冲销凭证号)
+     * @param $refundAmount string|float 正数金额
+     * @param $remark string 退款原因
+     * @return string 第三方退款流水号
      */
     public static function forwardOriginalOnlineRefund($cg, $refundSn, $refundAmount, $remark = '')
     {
@@ -1749,12 +1746,12 @@ class PurchaseClass extends BaseClass
             'orderSn' => $cg->orderSn ?? '',
             'refundAmount' => $refundFee,
             'refundReason' => $remark,
-            'thirdNo' => $thirdNo, //必传,不然聚合收银的支付宝付款退款不会成功
+            'thirdNo' => $thirdNo,
         ];
         $response = $laResource->refund($aliParams);
         if (!isset($response['code']) || $response['code'] != 'BBS00000') {
             $errMsg = $response['msg'] ?? '退款失败';
-            noticeUtil::push("冲销原路退回失败:{$errMsg} 金额:{$refundAmount} 原单:{$cg->orderSn}", '15280215347');
+            noticeUtil::push("冲销原路退回失败:{$errMsg} 金额:{$refundAmount} 原单:{$cg->orderSn}", '15280215347');
             util::fail('原路退回失败:' . $errMsg);
         }
         return $response['resp_data']['trade_no'] ?? '';

+ 22 - 0
common/components/orderSn.php

@@ -457,6 +457,28 @@ class orderSn
         return $prefix . $id;
     }
 
+    /**
+     * 冲销凭证号(GHS/HD 可各生成一号;靠表唯一索引防撞)
+     * 不单独建 sn 表,时间+随机即可满足并发量;ssh 冲销单功能
+     */
+    public static function getGhsForwardSn()
+    {
+        $prefix = 'CX_CS';
+        if (getenv('YII_ENV', 'local') == 'production') {
+            $prefix = 'CX';
+        }
+        return $prefix . date('ymdHis') . mt_rand(1000, 9999);
+    }
+
+    public static function getCgForwardSn()
+    {
+        $prefix = 'CXH_CS';
+        if (getenv('YII_ENV', 'local') == 'production') {
+            $prefix = 'CXH';
+        }
+        return $prefix . date('ymdHis') . mt_rand(1000, 9999);
+    }
+
     //零售采购单的退款单 ssh 20219628
     public static function getCgRefundSn()
     {

+ 129 - 0
sql/forward_schema_a.sql

@@ -0,0 +1,129 @@
+-- 方案A:冲销凭证双表 + 原单汇总缓存 + 售后关联(ssh 冲销单功能)
+-- 说明:不创建负销售单/负采购单;GHS/HD 各有凭证主表+明细,互挂 id
+
+-- ========== GHS 冲销凭证 ==========
+CREATE TABLE IF NOT EXISTS `xhGhsForward` (
+  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `forwardSn` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '冲销凭证号',
+  `mainId` INT(11) NOT NULL DEFAULT 0 COMMENT '供货商主体id',
+  `shopId` INT(11) NOT NULL DEFAULT 0,
+  `sjId` INT(11) NOT NULL DEFAULT 0,
+  `customId` INT(11) NOT NULL DEFAULT 0,
+  `ghsId` INT(11) NOT NULL DEFAULT 0 COMMENT '对应 xhGhs.id(花店侧镜像客户)',
+  `orderId` INT(11) NOT NULL DEFAULT 0 COMMENT '原销售单id,0=自由冲销',
+  `orderSn` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '原销售单号',
+  `amount` DECIMAL(9,2) NOT NULL DEFAULT 0.00 COMMENT '本次冲销金额(正数)',
+  `payWay` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '原单/记账支付方式',
+  `fundType` TINYINT(4) NOT NULL DEFAULT 2 COMMENT '1原路退 2返余额 3仅记账',
+  `forwardStock` TINYINT(4) NOT NULL DEFAULT 1 COMMENT '0回库存 1不回库存',
+  `cgForwardId` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '对应 xhCgForward.id',
+  `refundId` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '来源 xhRefund.id,0=非售后转',
+  `thirdRefundNo` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '网关退款流水号',
+  `status` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '0处理中 1成功 2失败',
+  `remark` VARCHAR(255) NOT NULL DEFAULT '',
+  `shopAdminId` INT(11) NOT NULL DEFAULT 0,
+  `shopAdminName` VARCHAR(64) NOT NULL DEFAULT '',
+  `addTime` DATETIME NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_forwardSn` (`forwardSn`),
+  KEY `idx_orderId` (`orderId`),
+  KEY `idx_customId` (`customId`),
+  KEY `idx_mainId_addTime` (`mainId`,`addTime`),
+  KEY `idx_cgForwardId` (`cgForwardId`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供货商冲销凭证';
+
+CREATE TABLE IF NOT EXISTS `xhGhsForwardItem` (
+  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `forwardId` INT(11) UNSIGNED NOT NULL DEFAULT 0,
+  `forwardSn` VARCHAR(64) NOT NULL DEFAULT '',
+  `mainId` INT(11) NOT NULL DEFAULT 0,
+  `customId` INT(11) NOT NULL DEFAULT 0,
+  `productId` INT(11) NOT NULL DEFAULT 0,
+  `orderItemId` INT(11) NOT NULL DEFAULT 0 COMMENT '原单明细id,可0',
+  `name` VARCHAR(128) NOT NULL DEFAULT '',
+  `num` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '冲销数量(正数,小单位合计)',
+  `bigNum` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+  `smallNum` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+  `unitPrice` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+  `price` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '单价(大单位或提交价)',
+  `amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '行金额(正数)',
+  `addTime` DATETIME NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `idx_forwardId` (`forwardId`),
+  KEY `idx_productId` (`productId`),
+  KEY `idx_mainId_addTime` (`mainId`,`addTime`),
+  KEY `idx_customId` (`customId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供货商冲销凭证明细';
+
+-- ========== HD 冲销凭证(镜像) ==========
+CREATE TABLE IF NOT EXISTS `xhCgForward` (
+  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `forwardSn` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '花店侧冲销凭证号',
+  `mainId` INT(11) NOT NULL DEFAULT 0 COMMENT '花店主体id',
+  `shopId` INT(11) NOT NULL DEFAULT 0,
+  `sjId` INT(11) NOT NULL DEFAULT 0,
+  `customId` INT(11) NOT NULL DEFAULT 0 COMMENT '供货商视角客户id',
+  `ghsId` INT(11) NOT NULL DEFAULT 0,
+  `cgId` INT(11) NOT NULL DEFAULT 0 COMMENT '原采购单id,0=自由冲销',
+  `cgSn` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '原采购单号',
+  `ghsForwardId` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '对应 xhGhsForward.id',
+  `amount` DECIMAL(9,2) NOT NULL DEFAULT 0.00,
+  `payWay` TINYINT(4) NOT NULL DEFAULT 0,
+  `fundType` TINYINT(4) NOT NULL DEFAULT 2,
+  `forwardStock` TINYINT(4) NOT NULL DEFAULT 1,
+  `cgRefundId` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '来源 xhCgRefund.id',
+  `status` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '0处理中 1成功 2失败',
+  `remark` VARCHAR(255) NOT NULL DEFAULT '',
+  `addTime` DATETIME NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_forwardSn` (`forwardSn`),
+  KEY `idx_cgId` (`cgId`),
+  KEY `idx_ghsForwardId` (`ghsForwardId`),
+  KEY `idx_ghsId_addTime` (`ghsId`,`addTime`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='花店冲销凭证';
+
+CREATE TABLE IF NOT EXISTS `xhCgForwardItem` (
+  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `forwardId` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'xhCgForward.id',
+  `forwardSn` VARCHAR(64) NOT NULL DEFAULT '',
+  `ghsId` INT(11) NOT NULL DEFAULT 0,
+  `customId` INT(11) NOT NULL DEFAULT 0,
+  `productId` INT(11) NOT NULL DEFAULT 0,
+  `cgItemId` INT(11) NOT NULL DEFAULT 0,
+  `name` VARCHAR(128) NOT NULL DEFAULT '',
+  `num` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+  `bigNum` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+  `smallNum` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+  `unitPrice` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+  `price` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+  `amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+  `addTime` DATETIME NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `idx_forwardId` (`forwardId`),
+  KEY `idx_productId` (`productId`),
+  KEY `idx_ghsId` (`ghsId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='花店冲销凭证明细';
+
+-- ========== 原单汇总缓存 ==========
+ALTER TABLE `xhGhsOrder`
+  ADD COLUMN `hasForward` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '0无冲销 1有冲销' AFTER `tkPrice`,
+  ADD COLUMN `forwardPrice` DECIMAL(9,2) NOT NULL DEFAULT 0.00 COMMENT '累计冲销金额' AFTER `hasForward`;
+
+ALTER TABLE `xhCg`
+  ADD COLUMN `hasForward` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '0无冲销 1有冲销' AFTER `tkPrice`,
+  ADD COLUMN `forwardPrice` DECIMAL(9,2) NOT NULL DEFAULT 0.00 COMMENT '累计冲销金额' AFTER `hasForward`;
+
+ALTER TABLE `xhGhsOrderItem`
+  ADD COLUMN `forwardNum` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '已冲销数量';
+
+ALTER TABLE `xhCgItem`
+  ADD COLUMN `forwardNum` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '已冲销数量';
+
+-- ========== 售后指向本侧凭证 ==========
+ALTER TABLE `xhRefund`
+  ADD COLUMN `forwardId` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '转冲销后xhGhsForward.id,0=未转' AFTER `cgRefundId`;
+
+ALTER TABLE `xhCgRefund`
+  ADD COLUMN `forwardId` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '转冲销后xhCgForward.id,0=未转' AFTER `saleRefundId`;