Sfoglia il codice sorgente

Merge branch 'forward' into dev

shish 6 ore fa
parent
commit
ff818376eb

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

@@ -7,9 +7,11 @@ 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;
@@ -117,6 +119,37 @@ class CustomController extends BaseController
         util::success(['imgUrl' => $imageUrl]);
     }
 
+    /**
+     * 获取收款小程序码(区别于 actionGetGatheringCode 的普通二维码,扫码直接进小程序,不用先跳浏览器)ssh 冲销单功能
+     * 用途:affirmForward.vue 提交冲销单成功后的退款凭证海报(forwardResult.vue),识别后直接打开 hdApp 小程序的
+     * admin/ghs/pay 页面查看余额明细;与 actionGetGatheringCode 生成的参数含义完全一致(id=ghsId, salt=custom.salt),
+     * 只是二维码载体从"H5链接二维码"换成了"小程序码",hdApp 端 admin/ghs/pay.vue 已同步支持从 scene 里解析这两个参数。
+     */
+    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不能超过32个字符,与H5链接的query参数含义保持一致:id=ghsId, salt=custom.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]);
+    }
+
     //供货商给客户充值和结账 ssh 20240309
     public function actionRecharge()
     {

+ 14 - 0
app-ghs/controllers/OrderController.php

@@ -1774,6 +1774,8 @@ class OrderController extends BaseController
      * 1. 从 selectCustom(forward=1) 自由开单:不传 relateOrderId,不关联具体原订单。
      * 2. 从售后 refund.vue 转入(原订单非当天/欠款已结清):传 relateOrderId,做累计冲销上限校验,
      *    并强制按原订单实际支付方式覆盖 payWay(不信任前端传值)。
+     * 资金处理方式(originalRefund/returnBalance)也按原单支付渠道在 OrderService::createForwardOrder 内强制/校验,
+     * 不完全信任前端传值:挂账/余额支付强制返充余额;线上支付(微信/支付宝)可二选一原路退回或返充余额。
      */
     public function actionCreateForwardOrder()
     {
@@ -1843,6 +1845,7 @@ class OrderController extends BaseController
             'actPrice' => $order->actPrice,
             'customName' => $order->customName,
             'customBalance' => $result['customBalance'],
+            'thirdRefundNo' => $result['thirdRefundNo'] ?? '',
         ]);
     }
 
@@ -2487,6 +2490,17 @@ class OrderController extends BaseController
         $shopBusiness = $shop->business ?? 0;
         $info['shopBusiness'] = $shopBusiness;
 
+        //冲销单详情页展示"原单"入口:如果当前订单本身就是一张冲销单(forward==1),
+        //反查 xhGhsOrderForward 关联记录拿到原销售单id/单号,供前端跳转查看原单。ssh 冲销单功能
+        $forward = $info['forward'] ?? 0;
+        if ($forward == 1) {
+            $relate = OrderForwardClass::getByForwardOrderId($id);
+            if (!empty($relate)) {
+                $info['relateOrderId'] = $relate['orderId'] ?? 0;
+                $info['relateOrderSn'] = $relate['orderSn'] ?? '';
+            }
+        }
+
         util::success($info);
     }
 

+ 18 - 0
app-hd/controllers/PurchaseController.php

@@ -1435,6 +1435,24 @@ class PurchaseController extends BaseController
         }
         $info['showForPay'] = $showForPay;
 
+        //冲销单详情页展示"原单"入口:如果当前采购单本身就是一张冲销单(forward==1),
+        //通过 xhCg.saleId(对应xhGhsOrder.id) 反查 xhGhsOrderForward 关联记录,
+        //拿到原销售单id后再取其 purchaseId 得到花店侧原采购单id,供前端跳转查看原单。ssh 冲销单功能
+        $forward = $info['forward'] ?? 0;
+        if ($forward == 1) {
+            $saleId = $info['saleId'] ?? 0;
+            if (!empty($saleId)) {
+                $relate = OrderForwardClass::getByForwardOrderId($saleId);
+                if (!empty($relate)) {
+                    $relateOrder = OrderClass::getById($relate['orderId'], true);
+                    if (!empty($relateOrder)) {
+                        $info['relateCgId'] = $relateOrder->purchaseId ?? 0;
+                        $info['relateOrderSn'] = $relate['orderSn'] ?? '';
+                    }
+                }
+            }
+        }
+
         util::success($info);
     }
 

+ 10 - 0
biz-ghs/order/classes/OrderForwardClass.php

@@ -43,4 +43,14 @@ class OrderForwardClass extends BaseClass
     {
         return self::getAllByCondition(['orderId' => $orderId], 'addTime DESC,id DESC', '*', null, false);
     }
+
+    /**
+     * 按冲销单id反查关联的原销售单信息(供冲销单详情页“查看原单”跳转用)
+     * 一张冲销单只会对应一条原单关联记录(由 relateOrderId 落库时写入),取第一条即可
+     * 返回普通数组(非AR对象),没有关联时返回空数组
+     */
+    public static function getByForwardOrderId($forwardOrderId)
+    {
+        return self::getByCondition(['forwardOrderId' => $forwardOrderId], false);
+    }
 }

+ 79 - 26
biz-ghs/order/services/OrderService.php

@@ -932,7 +932,7 @@ class OrderService extends BaseService
         //当天同个人同地址的订单汇总
         $prefixDate = substr($order->payTime, 0, 10);
         OrderClass::updateSameTimeIds($order, $prefixDate);
-        OrderClass::updateMergeOrder($order,0);
+        OrderClass::updateMergeOrder($order, 0);
     }
 
     //发起退款
@@ -1022,9 +1022,10 @@ class OrderService extends BaseService
      * 创建冲销单(金额为负的独立订单)
      * 设计说明:不复用极其庞杂的常规开单主链路(createNewOrder->OrderClass::addOrder),
      * 避免误伤现网所有商户的正常开单流程;只在数据落地时保持与正常订单相同的表结构,方便统一查询/统计。
-     * @param $post array 前端提交参数:customId,product(JSON数组),remark,needPrint,payWay,forwardStock,returnBalance,relateOrderId(可选)
+     * @param $post array 前端提交参数:customId,product(JSON数组),remark,needPrint,payWay,forwardStock,returnBalance,
+     *              originalRefund(可选,原路退回网关,仅线上支付+关联原单时生效),relateOrderId(可选),refundOrderId(可选)
      * @param $custom array 客户信息(CustomClass::getCustom 返回)
-     * @return array ['order'=>冲销单模型, 'customBalance'=>返充后的余额(未返充时为null)]
+     * @return array ['order'=>冲销单模型, 'customBalance'=>返充后的余额(未返充时为null), 'thirdRefundNo'=>原路退回的网关流水号(未走原路退回时为空)]
      */
     public static function createForwardOrder($post, $custom)
     {
@@ -1099,11 +1100,14 @@ class OrderService extends BaseService
 
         $forwardStock = isset($post['forwardStock']) && in_array((int)$post['forwardStock'], [0, 1], true) ? (int)$post['forwardStock'] : 1;
         $returnBalance = isset($post['returnBalance']) && (int)$post['returnBalance'] == 1 ? 1 : 0;
+        //原路退回(仅线上支付关联原单场景可用):是否调支付网关把这笔钱退回给付款人,与"返充到余额"二选一。ssh 冲销单功能
+        $originalRefund = isset($post['originalRefund']) && (int)$post['originalRefund'] == 1 ? 1 : 0;
         $payWay = isset($post['payWay']) ? (int)$post['payWay'] : dict::getDict('payWay', 'wxPay');
 
         //关联原单场景:累计冲销上限校验,并且强制按原单支付方式,不信任前端传的 payWay
         $relateOrderId = $post['relateOrderId'] ?? 0;
         $relateOrder = null;
+        $relateCg = null;
         if (!empty($relateOrderId)) {
             $relateOrder = OrderClass::getLockById($relateOrderId);
             if (empty($relateOrder)) {
@@ -1112,6 +1116,16 @@ class OrderService extends BaseService
             if ($relateOrder->mainId != $mainId || $relateOrder->customId != $customId) {
                 util::fail('原订单信息不匹配');
             }
+
+            //服务端复核"转冲销单"资格:非当天下单 或者 欠款已结清,二者满足其一才允许,不能只靠前端拦截,
+            //避免绕过前端直接调接口对"今天下的、欠款还没结清"的订单发起冲销单。ssh 冲销单功能
+            $relatePayTime = $relateOrder->payTime ?? '';
+            $notToday = empty($relatePayTime) || date('Y-m-d', strtotime($relatePayTime)) != date('Y-m-d');
+            $debtCleared = bccomp($relateOrder->debtPrice ?? 0, '0', 2) > 0 && bccomp($relateOrder->remainDebtPrice ?? 0, '0', 2) == 0;
+            if (!$notToday && !$debtCleared) {
+                util::fail('该订单不满足转冲销单条件(需为非当天订单,或欠款已结清),请走常规售后流程');
+            }
+
             $actPrice = $relateOrder->actPrice ?? 0;
             $hasForwardPrice = $relateOrder->forwardPrice ?? 0;
             if (bccomp($hasForwardPrice, $actPrice, 2) >= 0) {
@@ -1123,6 +1137,41 @@ class OrderService extends BaseService
             }
             //退款方式锁定为原单实际支付方式
             $payWay = $relateOrder->payWay ?? $payWay;
+
+            //花店侧原采购单:下面"原路退回"网关退款、以及后面同步hasForward/forwardPrice都要用到,这里统一加锁取一次
+            $relateCgId = $relateOrder->purchaseId ?? 0;
+            if (!empty($relateCgId)) {
+                $relateCg = PurchaseClass::getLockById($relateCgId);
+            }
+        }
+
+        //资金处理方式按原单支付渠道强制/校验,不完全信任前端传值:仅在"售后转冲销单"(带relateOrderId)场景生效,
+        //因为只有这种场景 payWay 才是真实原订单的支付渠道;没有关联原单的自由开冲销单,payWay 只是用户自己选的
+        //一个记账渠道说明,并不对应任何真实交易,不需要也不能做这里的强制/原路退回逻辑。
+        //挂账、余额支付:欠款本质是负余额,返充余额=冲减欠款,二者在账本层面是同一个动作,强制走"返充余额",不允许原路退回网关
+        //微信/支付宝:允许"原路退回"(真实网关退款)与"返充余额"二选一
+        if (!empty($relateOrderId)) {
+            $wxPay = dict::getDict('payWay', 'wxPay');
+            $aliPay = dict::getDict('payWay', 'alipay');
+            $debtPay = dict::getDict('payWay', 'debtPay');
+            $balancePay = dict::getDict('payWay', 'balancePay');
+            if (in_array($payWay, [$debtPay, $balancePay], true)) {
+                $returnBalance = 1;
+                $originalRefund = 0;
+            } elseif (in_array($payWay, [$wxPay, $aliPay], true)) {
+                if ($originalRefund == 1) {
+                    if (empty($relateCg)) {
+                        util::fail('没有关联原订单的真实交易信息,无法原路退回');
+                    }
+                    //原路退回和返充余额二选一,不能同时生效
+                    $returnBalance = 0;
+                }
+            } else {
+                $originalRefund = 0;
+            }
+        } else {
+            //自由开冲销单:不存在"原路退回网关"这个概念,永远不生效,防止前端异常传参
+            $originalRefund = 0;
         }
 
         //落库统一取负:金额字段为负,数量字段保持正数(代表冲销的数量)
@@ -1151,7 +1200,7 @@ class OrderService extends BaseService
 
         $orderData = [
             'mainId' => $mainId,
-            'sendNum' => '0' . date('j') . StatOrderCountClass::addOrder(ShopClass::getLockById($shopId), MainClass::getLockById($mainId)),
+            'sendNum' => '',
             'orderSn' => $orderSn,
             'customId' => $customId,
             'customName' => $custom['name'] ?? '',
@@ -1182,7 +1231,7 @@ class OrderService extends BaseService
             'status' => OrderClass::ORDER_STATUS_COMPLETE,
             'payStatus' => OrderClass::PAY_STATUS_HAS_PAY,
             'sendType' => OrderClass::SEND_TYPE_SHOP_GET,
-            'sendStatus' => 2,
+            'sendStatus' => -4,
             'sendTimeWant' => date('Y-m-d'),
             'payTime' => $now,
             'deadline' => $deadline,
@@ -1316,9 +1365,16 @@ class OrderService extends BaseService
         $ghs->expendAmount = bcadd($ghs->expendAmount ?? '0.00', $actPrice, 2);
         $ghs->save(false, ['expendAmount']);
 
-        //余额处理:returnBalance==1 加回客户余额;==0 不改 balance,只把 payWay 记在订单上作为返款渠道记录
+        //资金处理:originalRefund==1 调支付网关原路退回给付款人;returnBalance==1 加回客户余额;
+        //两者都为0时不改动任何资金字段,只把 payWay 记在订单上作为返款渠道记录。ssh 冲销单功能
         $customBalance = null;
-        if ($returnBalance == 1) {
+        $thirdRefundNo = '';
+        if ($originalRefund == 1) {
+            //原路退回:用原单对应的采购单(xhCg)真实支付信息调网关退款,用冲销单自己的orderSn做退款流水号,天然保证唯一/幂等
+            $thirdRefundNo = PurchaseClass::forwardOriginalOnlineRefund($relateCg, $orderSn, $totalAmount, '冲销单' . $orderSn . '原路退回');
+            $order->remark = trim(($order->remark ?? '') . " [原路退回成功,网关退款单号:{$thirdRefundNo}]");
+            $order->save(false, ['remark']);
+        } elseif ($returnBalance == 1) {
             $customBalance = CustomClass::forwardReturnBalance($lockCustom, $order, $totalAmount);
         }
 
@@ -1342,23 +1398,20 @@ class OrderService extends BaseService
 
             //同步花店侧原采购单(xhCg)的 hasForward/forwardPrice 与采购单项(xhCgItem)的 forwardNum,
             //供 hdApp 的 purDetails.vue/refundList.vue 展示冲销记录,与 GHS 侧原单缓存字段保持一致
-            $relateCgId = $relateOrder->purchaseId ?? 0;
-            if (!empty($relateCgId)) {
-                $relateCg = PurchaseClass::getById($relateCgId, true);
-                if (!empty($relateCg)) {
-                    $relateCg->hasForward = 1;
-                    $relateCg->forwardPrice = bcadd($relateCg->forwardPrice ?? 0, $totalAmount, 2);
-                    $relateCg->save(false, ['hasForward', 'forwardPrice']);
-
-                    $relateCgItems = PurchaseItemClass::getAllByCondition(['orderSn' => $relateCg->orderSn], null, '*', 'productId', true);
-                    foreach ($negItems as $it) {
-                        $productId = $it['productId'] ?? 0;
-                        $relateCgItem = $relateCgItems[$productId] ?? null;
-                        if (!empty($relateCgItem)) {
-                            $relateCgItem->forward = 1;
-                            $relateCgItem->forwardNum = bcadd($relateCgItem->forwardNum ?? 0, $it['num'] ?? 0, 2);
-                            $relateCgItem->save(false, ['forward', 'forwardNum']);
-                        }
+            //($relateCg 已在前面校验资金处理方式时统一加锁取过,这里直接复用,避免重复查询/重复加锁)
+            if (!empty($relateCg)) {
+                $relateCg->hasForward = 1;
+                $relateCg->forwardPrice = bcadd($relateCg->forwardPrice ?? 0, $totalAmount, 2);
+                $relateCg->save(false, ['hasForward', 'forwardPrice']);
+
+                $relateCgItems = PurchaseItemClass::getAllByCondition(['orderSn' => $relateCg->orderSn], null, '*', 'productId', true);
+                foreach ($negItems as $it) {
+                    $productId = $it['productId'] ?? 0;
+                    $relateCgItem = $relateCgItems[$productId] ?? null;
+                    if (!empty($relateCgItem)) {
+                        $relateCgItem->forward = 1;
+                        $relateCgItem->forwardNum = bcadd($relateCgItem->forwardNum ?? 0, $it['num'] ?? 0, 2);
+                        $relateCgItem->save(false, ['forward', 'forwardNum']);
                     }
                 }
             }
@@ -1396,7 +1449,7 @@ class OrderService extends BaseService
             }
         }
 
-        return ['order' => $order, 'customBalance' => $customBalance];
+        return ['order' => $order, 'customBalance' => $customBalance, 'thirdRefundNo' => $thirdRefundNo];
     }
 
-}
+}

+ 63 - 0
biz-hd/purchase/classes/PurchaseClass.php

@@ -1697,4 +1697,67 @@ 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 中断
+     */
+    public static function forwardOriginalOnlineRefund($cg, $refundSn, $refundAmount, $remark = '')
+    {
+        $onlinePay = $cg->onlinePay ?? 1;
+        $payWay = $cg->payWay ?? 0;
+        $wxPay = dict::getDict('payWay', 'wxPay');
+        $aliPay = dict::getDict('payWay', 'alipay');
+        if ($onlinePay != dict::getDict('onlinePay', 'yes') || !in_array($payWay, [$wxPay, $aliPay], true)) {
+            util::fail('原订单支付方式不支持原路退回');
+        }
+        $thirdNo = $cg->thirdNo ?? '';
+        if (empty($thirdNo)) {
+            util::fail('原订单缺少支付流水号,无法原路退回');
+        }
+        $ghsId = $cg->ghsId ?? 0;
+        $ghs = GhsClass::getLockById($ghsId);
+        if (empty($ghs)) {
+            util::fail('没有找到供货商');
+        }
+        $ghsShopId = $ghs->shopId ?? 0;
+        $ghsShop = ShopClass::getById($ghsShopId, true);
+        if (empty($ghsShop) || empty($ghsShop->lklSjNo) || empty($ghsShop->lklScanTermNo)) {
+            util::fail('供货商门店未配置收款商户信息,无法原路退回');
+        }
+        $refundFee = bcmul($refundAmount, 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);
+        $aliParams = [
+            'refundSn' => $refundSn,
+            'orderSn' => $cg->orderSn ?? '',
+            'refundAmount' => $refundFee,
+            'refundReason' => $remark,
+            'thirdNo' => $thirdNo, //必传,不然聚合收银的支付宝付款退款不会成功
+        ];
+        $response = $laResource->refund($aliParams);
+        if (!isset($response['code']) || $response['code'] != 'BBS00000') {
+            $errMsg = $response['msg'] ?? '退款失败';
+            noticeUtil::push("冲销单原路退回失败:{$errMsg} 金额:{$refundAmount} 原单:{$cg->orderSn}", '15280215347');
+            util::fail('原路退回失败:' . $errMsg);
+        }
+        return $response['resp_data']['trade_no'] ?? '';
+    }
+
 }