Ver código fonte

fix(lakala): 完善进件审核后的终端同步与授权状态刷新

- 平台提交成功后自动刷新审核结果,并为终端同步增加有限重试
- 过滤空终端号并保留已有值,修复缺失通知中的字段拼写错误
- 接入 V2 子商户授权查询,同步微信、中花汇及支付宝授权状态
- 补充接口异常、数据缺失与异步刷新时间日志

API: 平台审核提交后可返回自动刷新的申请详情,同步重试会增加请求耗时
Database: 更新既有申请审核状态和账户授权状态,避免空终端号覆盖已有值
shizhongqi 12 horas atrás
pai
commit
63f4ff3af6

+ 69 - 23
biz-ghs/lakala/classes/LakalaAccountClass.php

@@ -65,7 +65,8 @@ class LakalaAccountClass extends BaseClass
             if (empty($account)) {
                 return true;
             }
-            if ($account->wxAuthorizeState == 1 && $account->zfbAuthorizeState == 1) {
+            // 三个通道都已认证才跳过;否则中花汇会停在库里的旧值
+            if ($account->wxAuthorizeState == 1 && $account->zfbAuthorizeState == 1 && $account->wxZhhAuthorizeState == 1) {
                 return true;
             }
 
@@ -73,12 +74,15 @@ class LakalaAccountClass extends BaseClass
             $scanTermNo = $account->scanTermNo ?: '';
             $b2bTermNo = $account->b2bTermNo ?: '';
             if ($merchantNo === '' || $scanTermNo === '' || $b2bTermNo === '') {
+                Yii::warning("拉卡拉数据缺失:merchantNo={$merchantNo}, scanTermNo={$scanTermNo}, b2bTermNo={$b2bTermNo}");
                 return true;
             }
 
             $client = new LakalaOnboardingClient();
-            $account->wxAuthorizeState = self::authorizeState($client, $merchantNo, 'WXZF');
-            $account->zfbAuthorizeState = self::authorizeState($client, $merchantNo, 'ZFBZF');
+            $re = self::authorizeState($client, $merchantNo);
+            $account->wxAuthorizeState = $re['wxAuthorizeState'];
+            $account->wxZhhAuthorizeState = $re['wxZhhAuthorizeState'];
+            $account->zfbAuthorizeState = $re['zfbAuthorizeState'];
             $account->updateTime = date('Y-m-d H:i:s');
             if (!$account->save()) {
                 throw new \RuntimeException(json_encode($account->getErrors(), JSON_UNESCAPED_UNICODE));
@@ -90,30 +94,72 @@ class LakalaAccountClass extends BaseClass
         }
     }
 
-    private static function authorizeState($client, $merchantNo, $registerType)
+    private static function authorizeState($client, $merchantNo)
     {
-        $response = $client->query('/api/v3/tkbs/open_merchant_register_status_query', [
-            'merchant_no' => $merchantNo,
-            'register_type' => $registerType,
-        ], 'both');
-        Yii::info(json_encode($response));
-        $data = $response['data'] ?? [];
-
-        if (isset($data['authorize_state']) && ($data['authorize_state'] == 'AUTHORIZE_STATE_UNAUTHORIZED' || $data['authorize_state'] == 'UNAUTHORIZED')) {
-            Yii::info('Lakala '.$merchantNo. ' '.$registerType. ' authorize state is UNAUTHORIZED');
-            // 未认证
-            return 0;
+        $params = [
+            "version" => "1.0",
+            "orderNo" => date('YmdHis') . rand(10000000, 99999999), // 14位年月日时(24小时制)分秒+8位的随机数(不重复)
+            "orgCode" => $client->orgCode(),
+            "merCupNo" => $merchantNo,
+        ];
+
+        $response = $client->callV2('/api/v2/mms/openApi/querySubMerInfo', $params);
+        $list = $response['data']['list'] ?? [];
+
+        $wxStates = [];
+        $zfbState = 0;
+        $seen = [];
+        foreach ($list as $item) {
+            $registerType = strtoupper((string)($item['registerType'] ?? ''));
+            $registerChannel = strtoupper((string)($item['registerChannel'] ?? ''));
+            if ($registerChannel !== 'UNIONPAY' || !in_array($registerType, ['WXZF', 'ZFBZF'], true)) {
+                continue;
+            }
+            $subMchId = (string)($item['subMchId'] ?? '');
+            if ($subMchId === '') {
+                continue;
+            }
+            $dedupeKey = $registerType . ':' . $subMchId;
+            if (isset($seen[$dedupeKey])) {
+                continue;
+            }
+            $seen[$dedupeKey] = true;
+
+            $tradeMode = $registerType === 'WXZF' ? 'WECHAT' : 'ALIPAY';
+            $authorized = self::merchantAuthState($client, $merchantNo, $tradeMode, $subMchId);
+            if ($registerType === 'WXZF') {
+                $wxStates[] = $authorized;
+            } elseif ($authorized === 1) {
+                $zfbState = 1;
+            }
         }
 
-        if (isset($data['authorize_state']) && ($data['authorize_state'] == 'AUTHORIZE_STATE_AUTHORIZED' || $data['authorize_state'] == 'AUTHORIZED')) {
-            Yii::info('Lakala '.$merchantNo. ' '.$registerType. ' authorize state is AUTHORIZED');
-            // 已认证
-            return 1;
-        }
+        return [
+            'wxAuthorizeState' => $wxStates[0] ?? 0,
+            'wxZhhAuthorizeState' => $wxStates[1] ?? 0,
+            'zfbAuthorizeState' => $zfbState,
+        ];
+    }
 
-        // 其他状态,默认未认证
-        Yii::error('Lakala '.$merchantNo. ' '.$registerType. ' authorize state is UNKNOWN');
-        return 0;
+    private static function merchantAuthState($client, $merchantNo, $tradeMode, $subMchId)
+    {
+        try {
+            $response = $client->callV2('/api/v2/mms/sme/mrchAuthStateQuery', [
+                'tradeMode' => $tradeMode,
+                'subMerchantId' => $subMchId,
+                'merchantNo' => $merchantNo,
+            ]);
+            $checkResult = strtoupper((string)($response['data']['checkResult'] ?? ''));
+            if (in_array($checkResult, ['AUTHORIZE_STATE_AUTHORIZED', 'AUTHORIZED'], true)) {
+                return 1;
+            }
+            return 0;
+        } catch (\Throwable $e) {
+            Yii::warning('Lakala merchant auth state query failed, merchantNo=' . $merchantNo
+                . ', tradeMode=' . $tradeMode . ', subMerchantId=' . $subMchId . ': ' . $e->getMessage());
+            // 查询失败不等于未认证,交由刷新入口终止本次写入,保留已有状态。
+            throw $e;
+        }
     }
 
     private static function releaseAuthorizeRefreshLocks(array $accountIds)

+ 137 - 44
biz-ghs/lakala/services/LakalaAccountService.php

@@ -352,6 +352,11 @@ class LakalaAccountService
         $application->save();
     }
 
+    /**
+     * 平台审核通过:把申请提交到拉卡拉;拉卡拉审核通过后自动刷新回写商户号/终端。
+     * 入参 id 为进件申请主键;成功时返回申请详情。
+     * 花掌柜申请转交 biz-hd,避免沿用销花宝 0.33% 费率。
+     */
     public static function platformApprove($id)
     {
         $application = LakalaApplicationClass::getById($id, true);
@@ -365,12 +370,66 @@ class LakalaAccountService
         if ($application->status !== LakalaApplicationClass::STATUS_PLATFORM_AUDIT) {
             util::fail('仅后台审核中的申请可以提交拉卡拉');
         }
-        return self::submit(
+        $result = self::submit(
             $application->id,
             $application->mainId,
             $application->shopId,
             self::decode($application->formData)
         );
+        if (empty($result)) {
+            return $result;
+        }
+        // 提交成功后按拉卡拉审核结果自动刷新,最多 3 次直到本地 APPROVED。
+        return self::refreshAfterPlatformSubmit(
+            $application->id,
+            $application->mainId,
+            $application->shopId,
+            $result
+        );
+    }
+
+    /**
+     * 平台提交拉卡拉成功后,自动调用与客户端「刷新状态」相同的 refresh。
+     * 拉卡拉审核通过后回写商户号/终端;最多请求 3 次,直到 xhLakalaApplication.status=APPROVED。
+     * 拉卡拉驳回或刷新失败只记日志并返回已有提交结果,不阻断本次审核提交。
+     */
+    private static function refreshAfterPlatformSubmit($id, $mainId, $shopId, $fallback)
+    {
+        $result = $fallback;
+        $maxAttempts = 3;
+        $retryInterval = 3;
+        for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
+            try {
+                $application = LakalaApplicationClass::getById($id, true);
+                if (empty($application)) {
+                    return $result;
+                }
+                // 已认证通过则无需再刷;拉卡拉已驳回也不会变成 APPROVED,停止重试。
+                if ($application->status === LakalaApplicationClass::STATUS_APPROVED
+                    || $application->status === LakalaApplicationClass::STATUS_REJECT) {
+                    return $result;
+                }
+                // 没有拉卡拉查询主键时 refresh 会直接失败退出,提交后若尚未回写则跳过。
+                $canRefresh = $application->type === LakalaApplicationClass::TYPE_SETTLEMENT_CHANGE
+                    ? !empty($application->reviewRelatedId)
+                    : (!empty($application->customerNo) || !empty($application->merchantNo));
+                if (!$canRefresh) {
+                    return $result;
+                }
+                $result = self::refresh($application->id, $application->mainId, $application->shopId);
+                $application = LakalaApplicationClass::getById($id, true);
+                if (!empty($application) && ($application->status === LakalaApplicationClass::STATUS_APPROVED
+                    || $application->status === LakalaApplicationClass::STATUS_REJECT)) {
+                    return $result;
+                }
+            } catch (\Throwable $e) {
+                Yii::warning('Lakala platformApprove auto refresh attempt ' . $attempt . ' failed: ' . $e->getMessage());
+            }
+            if ($attempt < $maxAttempts) {
+                sleep($retryInterval);
+            }
+        }
+        return $result;
     }
 
     public static function refresh($id, $mainId, $shopId)
@@ -386,7 +445,7 @@ class LakalaAccountService
         $client = new LakalaOnboardingClient();
         $response = $client->query('/api/v3/tkbs/open_merchant_info', [
             'customer_no' => $application->customerNo,
-        ], 'both');
+        ], 'both'); //获取商户信息
         $customer = $response['data']['customer'];
         $status = $customer['customer_status'];
         if (in_array($status, ['REJECT', 'REVIEW_FAIL'], true)) {
@@ -526,44 +585,68 @@ class LakalaAccountService
 
     private static function syncTerms($application, $account, $client)
     {
-        $operations = LakalaOperationClass::getAllByCondition(
-            ['applicationId' => $application->id, 'operationType' => 'add_term'],
-            null,
-            '*',
-            null,
-            true
-        );
-        foreach ($operations as $operation) {
-            if (!empty($operation->reviewRelatedId) && !in_array($operation->status, ['PASS', 'FAILED'], true)) {
-                $review = $client->query('/api/v3/tkbs/customer_update_review', ['review_related_id' => $operation->reviewRelatedId]);
-                $operation->status = $review['data']['review_pass'] ?? $operation->status;
-                $operation->responseData = self::encode($review);
-                $operation->errorMessage = $review['data']['review_result'] ?? '';
-                $operation->updateTime = date('Y-m-d H:i:s');
-                $operation->save();
+        $maxAttempts = 3;
+        $retryInterval = 3;
+        for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
+            try {
+                $operations = LakalaOperationClass::getAllByCondition(
+                    ['applicationId' => $application->id, 'operationType' => 'add_term'],
+                    null,
+                    '*',
+                    null,
+                    true
+                );
+                foreach ($operations as $operation) {
+                    if (!empty($operation->reviewRelatedId) && !in_array($operation->status, ['PASS', 'FAILED'], true)) {
+                        $review = $client->query('/api/v3/tkbs/customer_update_review', ['review_related_id' => $operation->reviewRelatedId]);
+                        $operation->status = $review['data']['review_pass'] ?? $operation->status;
+                        $operation->responseData = self::encode($review);
+                        $operation->errorMessage = $review['data']['review_result'] ?? '';
+                        $operation->updateTime = date('Y-m-d H:i:s');
+                        $operation->save();
+                    }
+                }
+
+                $res = $client->query('/api/v3/tkbs/open_merchant_info', [
+                    'customer_no' => $account->customerNo ?: $application->customerNo,
+                    'merchant_no' => $account->merchantNo ?: $application->merchantNo,
+                ], 'both');
+                $merchantInfo = $res['data'];
+                $customer = $merchantInfo['customer'];
+                $terms = self::merchantInfoTermMap($merchantInfo);
+                $merchantNo = $customer['merchant_no'] ?? '';
+                if ($merchantNo !== '') {
+                    $application->merchantNo = $merchantNo;
+                    $account->merchantNo = $merchantNo;
+                }
+                self::assignAccountTermNo($account, 'scanTermNo', $terms['scanTermNo'][0] ?? '');
+                self::assignAccountTermNo($account, 'b2bTermNo', $terms['b2BTermNo'][0] ?? '');
+                $account->updateTime = date('Y-m-d H:i:s');
+                $account->save();
+                $application->save();
+                if ($account->scanTermNo != '' && $account->b2bTermNo != '') {
+                    LakalaAccountClass::dispatchAuthorizeStatesRefresh([$account->id]);
+                    return;
+                } else {
+
+                }
+            } catch (\Throwable $e) {
+                Yii::warning('Unable to sync Lakala merchant info terms (attempt ' . $attempt . '): ' . $e->getMessage());
             }
-        }
-        try {
-            $res = $client->query('/api/v3/tkbs/open_merchant_info', [
-                'customer_no' => $account->customerNo ?: $application->customerNo,
-                'merchant_no' => $account->merchantNo ?: $application->merchantNo,
-            ], 'both');
-            $merchantInfo = $res['data'];
-            $customer = $merchantInfo['customer'];
-            $terms = self::merchantInfoTermMap($merchantInfo);
-            $merchantNo = $customer['merchant_no'] ?? '';
-            if ($merchantNo !== '') {
-                $application->merchantNo = $merchantNo;
-                $account->merchantNo = $merchantNo;
+            if ($attempt < $maxAttempts) {
+                sleep($retryInterval);
             }
-            $account->scanTermNo = $terms['scanTermNo'][0] ?? $account->scanTermNo;
-            $account->b2bTermNo = $terms['b2BTermNo'][0] ?? $account->b2bTermNo;
-            $account->updateTime = date('Y-m-d H:i:s');
-            $account->save();
-            $application->save();
-            LakalaAccountClass::dispatchAuthorizeStatesRefresh([$account->id]);
-        } catch (\Throwable $e) {
-            Yii::warning('Unable to sync Lakala merchant info terms: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * scanTermNo / b2bTermNo 为 NOT NULL,拉卡拉尚未返回终端号时不能写成 null,否则 save 会 1048。
+     */
+    private static function assignAccountTermNo($account, $attribute, $termNo)
+    {
+        $termNo = trim((string)$termNo);
+        if ($termNo !== '') {
+            $account->$attribute = $termNo;
         }
     }
 
@@ -933,10 +1016,14 @@ class LakalaAccountService
     private static function version($application, array $request, array $response)
     {
         LakalaApplicationVersionClass::add([
-            'applicationId' => $application->id, 'mainId' => $application->mainId,
-            'versionNo' => $application->versionNo, 'type' => $application->type,
-            'formData' => $application->formData, 'requestData' => self::encode($request),
-            'responseData' => self::encode($response), 'createTime' => date('Y-m-d H:i:s'),
+            'applicationId' => $application->id,
+            'mainId' => $application->mainId,
+            'versionNo' => $application->versionNo,
+            'type' => $application->type,
+            'formData' => $application->formData,
+            'requestData' => self::encode($request),
+            'responseData' => self::encode($response),
+            'createTime' => date('Y-m-d H:i:s'),
         ]);
     }
 
@@ -973,13 +1060,19 @@ class LakalaAccountService
         $terminalInfos = $value['terminal_info'] ?? [];
         foreach ($terminalInfos as $term) {
             if (($term['term_type_name'] ?? '') == '专业化扫码') {
-                $result['scanTermNo'][] = $term['active_no_vo_list'][0]['term_no'] ?? '';
+                $termNo = trim((string)($term['active_no_vo_list'][0]['term_no'] ?? ''));
+                if ($termNo !== '') {
+                    $result['scanTermNo'][] = $termNo;
+                }
             }
             if (($term['term_type_name'] ?? '') == '聚合收银台') {
                 $list = $term['active_no_vo_list'] ?? [];
                 foreach ($list as $item) {
                     if (($item['busi_type_code'] ?? '') == 'QR_CODE_CARD') {
-                        $result['b2BTermNo'][] = $item['term_no'] ?? '';
+                        $termNo = trim((string)($item['term_no'] ?? ''));
+                        if ($termNo !== '') {
+                            $result['b2BTermNo'][] = $termNo;
+                        }
                     }
                 }
             }

+ 1 - 0
biz-hd/lakala/classes/LakalaAccountClass.php

@@ -74,6 +74,7 @@ class LakalaAccountClass extends BaseClass
             $scanTermNo = $account->scanTermNo ?: '';
             $b2bTermNo = $account->b2bTermNo ?: '';
             if ($merchantNo === '' || $scanTermNo === '' || $b2bTermNo === '') {
+                Yii::warning("拉卡拉数据缺失:merchantNo={$merchantNo}, scanTermNo={$scanTermNo}, b2bTermNo={$b2bTermNo}");
                 return true;
             }
 

+ 137 - 39
biz-hd/lakala/services/LakalaAccountService.php

@@ -9,6 +9,7 @@ use bizHd\lakala\classes\LakalaOperationClass;
 use bizHd\merchant\classes\ShopClass;
 use common\components\imgUtil;
 use common\components\lakala\LakalaOnboardingClient;
+use common\components\noticeUtil;
 use common\components\oss;
 use common\components\stringUtil;
 use common\components\util;
@@ -359,6 +360,10 @@ class LakalaAccountService
         $application->save();
     }
 
+    /**
+     * 平台审核通过:把申请提交到拉卡拉;拉卡拉审核通过后自动刷新回写商户号/终端。
+     * 入参 id 为进件申请主键;成功时返回申请详情。
+     */
     public static function platformApprove($id)
     {
         $application = LakalaApplicationClass::getById($id, true);
@@ -368,12 +373,66 @@ class LakalaAccountService
         if ($application->status !== LakalaApplicationClass::STATUS_PLATFORM_AUDIT) {
             util::fail('仅后台审核中的申请可以提交拉卡拉');
         }
-        return self::submit(
+        $result = self::submit(
             $application->id,
             $application->mainId,
             $application->shopId,
             self::decode($application->formData)
         );
+        if (empty($result)) {
+            return $result;
+        }
+        // 提交成功后按拉卡拉审核结果自动刷新,最多 3 次直到本地 APPROVED。
+        return self::refreshAfterPlatformSubmit(
+            $application->id,
+            $application->mainId,
+            $application->shopId,
+            $result
+        );
+    }
+
+    /**
+     * 平台提交拉卡拉成功后,自动调用与客户端「刷新状态」相同的 refresh。
+     * 拉卡拉审核通过后回写商户号/终端;最多请求 3 次,直到 xhLakalaApplication.status=APPROVED。
+     * 拉卡拉驳回或刷新失败只记日志并返回已有提交结果,不阻断本次审核提交。
+     */
+    private static function refreshAfterPlatformSubmit($id, $mainId, $shopId, $fallback)
+    {
+        $result = $fallback;
+        $maxAttempts = 3;
+        $retryInterval = 3;
+        for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
+            try {
+                $application = LakalaApplicationClass::getById($id, true);
+                if (empty($application)) {
+                    return $result;
+                }
+                // 已认证通过则无需再刷;拉卡拉已驳回也不会变成 APPROVED,停止重试。
+                if ($application->status === LakalaApplicationClass::STATUS_APPROVED
+                    || $application->status === LakalaApplicationClass::STATUS_REJECT) {
+                    return $result;
+                }
+                // 没有拉卡拉查询主键时 refresh 会直接失败退出,提交后若尚未回写则跳过。
+                $canRefresh = $application->type === LakalaApplicationClass::TYPE_SETTLEMENT_CHANGE
+                    ? !empty($application->reviewRelatedId)
+                    : (!empty($application->customerNo) || !empty($application->merchantNo));
+                if (!$canRefresh) {
+                    return $result;
+                }
+                $result = self::refresh($application->id, $application->mainId, $application->shopId);
+                $application = LakalaApplicationClass::getById($id, true);
+                if (!empty($application) && ($application->status === LakalaApplicationClass::STATUS_APPROVED
+                    || $application->status === LakalaApplicationClass::STATUS_REJECT)) {
+                    return $result;
+                }
+            } catch (\Throwable $e) {
+                Yii::warning('Lakala platformApprove auto refresh attempt ' . $attempt . ' failed: ' . $e->getMessage());
+            }
+            if ($attempt < $maxAttempts) {
+                sleep($retryInterval);
+            }
+        }
+        return $result;
     }
 
     public static function refresh($id, $mainId, $shopId)
@@ -529,44 +588,77 @@ class LakalaAccountService
 
     private static function syncTerms($application, $account, $client)
     {
-        $operations = LakalaOperationClass::getAllByCondition(
-            ['applicationId' => $application->id, 'operationType' => 'add_term'],
-            null,
-            '*',
-            null,
-            true
-        );
-        foreach ($operations as $operation) {
-            if (!empty($operation->reviewRelatedId) && !in_array($operation->status, ['PASS', 'FAILED'], true)) {
-                $review = $client->query('/api/v3/tkbs/customer_update_review', ['review_related_id' => $operation->reviewRelatedId]);
-                $operation->status = $review['data']['review_pass'] ?? $operation->status;
-                $operation->responseData = self::encode($review);
-                $operation->errorMessage = $review['data']['review_result'] ?? '';
-                $operation->updateTime = date('Y-m-d H:i:s');
-                $operation->save();
+        $maxAttempts = 3;
+        $retryInterval = 3;
+        for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
+            try {
+                $operations = LakalaOperationClass::getAllByCondition(
+                    ['applicationId' => $application->id, 'operationType' => 'add_term'],
+                    null,
+                    '*',
+                    null,
+                    true
+                );
+                foreach ($operations as $operation) {
+                    if (!empty($operation->reviewRelatedId) && !in_array($operation->status, ['PASS', 'FAILED'], true)) {
+                        $review = $client->query('/api/v3/tkbs/customer_update_review', ['review_related_id' => $operation->reviewRelatedId]);
+                        $operation->status = $review['data']['review_pass'] ?? $operation->status;
+                        $operation->responseData = self::encode($review);
+                        $operation->errorMessage = $review['data']['review_result'] ?? '';
+                        $operation->updateTime = date('Y-m-d H:i:s');
+                        $operation->save();
+                    }
+                }
+
+                $res = $client->query('/api/v3/tkbs/open_merchant_info', [
+                    'customer_no' => $account->customerNo ?: $application->customerNo,
+                    'merchant_no' => $account->merchantNo ?: $application->merchantNo,
+                ], 'both');
+                $merchantInfo = $res['data'];
+                $customer = $merchantInfo['customer'];
+                $terms = self::merchantInfoTermMap($merchantInfo);
+                $merchantNo = $customer['merchant_no'] ?? '';
+                if ($merchantNo !== '') {
+                    $application->merchantNo = $merchantNo;
+                    $account->merchantNo = $merchantNo;
+                }
+                self::assignAccountTermNo($account, 'scanTermNo', $terms['scanTermNo'][0] ?? '');
+                self::assignAccountTermNo($account, 'b2bTermNo', $terms['b2BTermNo'][0] ?? '');
+                $account->updateTime = date('Y-m-d H:i:s');
+                $account->save();
+                $application->save();
+                if ($account->scanTermNo != '' && $account->b2bTermNo != '') {
+                    LakalaAccountClass::dispatchAuthorizeStatesRefresh([$account->id]);
+                    return;
+                } else {
+                    if ($account->scanTermNo == '') {
+                        //“专业化扫码” --- “业务信息(扫码)” ---  终端号  ----> 填入到 lklScanTermNo
+                        $errMsg = '专业化扫码 --- 业务信息(扫码)--- 终端号lklScanTermNo 缺失';
+                    }
+                    if ($account->b2bTermNo == '') {
+                        //“聚合收银台” --- “业务信息(扫码)” --- 终端号   ----> 填入到 lklB2BTermNo
+                        $errMsg = '聚合收银台 --- 业务信息(扫码) --- 终端号 lklB2BTermNo 缺失';
+                    }
+                    noticeUtil::push($errMsg);
+                    Yii::error($errMsg);
+                }
+            } catch (\Throwable $e) {
+                Yii::warning('Unable to sync Lakala merchant info terms (attempt ' . $attempt . '): ' . $e->getMessage());
             }
-        }
-        try {
-            $res = $client->query('/api/v3/tkbs/open_merchant_info', [
-                'customer_no' => $account->customerNo ?: $application->customerNo,
-                'merchant_no' => $account->merchantNo ?: $application->merchantNo,
-            ], 'both');
-            $merchantInfo = $res['data'];
-            $customer = $merchantInfo['customer'];
-            $terms = self::merchantInfoTermMap($merchantInfo);
-            $merchantNo = $customer['merchant_no'] ?? '';
-            if ($merchantNo !== '') {
-                $application->merchantNo = $merchantNo;
-                $account->merchantNo = $merchantNo;
+            if ($attempt < $maxAttempts) {
+                sleep($retryInterval);
             }
-            $account->scanTermNo = $terms['scanTermNo'][0] ?? $account->scanTermNo;
-            $account->b2bTermNo = $terms['b2BTermNo'][0] ?? $account->b2bTermNo;
-            $account->updateTime = date('Y-m-d H:i:s');
-            $account->save();
-            $application->save();
-            LakalaAccountClass::dispatchAuthorizeStatesRefresh([$account->id]);
-        } catch (\Throwable $e) {
-            Yii::warning('Unable to sync Lakala merchant info terms: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * scanTermNo / b2bTermNo 为 NOT NULL,拉卡拉尚未返回终端号时不能写成 null,否则 save 会 1048。
+     */
+    private static function assignAccountTermNo($account, $attribute, $termNo)
+    {
+        $termNo = trim((string)$termNo);
+        if ($termNo !== '') {
+            $account->$attribute = $termNo;
         }
     }
 
@@ -972,13 +1064,19 @@ class LakalaAccountService
         $terminalInfos = $value['terminal_info'] ?? [];
         foreach ($terminalInfos as $term) {
             if (($term['term_type_name'] ?? '') == '专业化扫码') {
-                $result['scanTermNo'][] = $term['active_no_vo_list'][0]['term_no'] ?? '';
+                $termNo = trim((string)($term['active_no_vo_list'][0]['term_no'] ?? ''));
+                if ($termNo !== '') {
+                    $result['scanTermNo'][] = $termNo;
+                }
             }
             if (($term['term_type_name'] ?? '') == '聚合收银台') {
                 $list = $term['active_no_vo_list'] ?? [];
                 foreach ($list as $item) {
                     if (($item['busi_type_code'] ?? '') == 'QR_CODE_CARD') {
-                        $result['b2BTermNo'][] = $item['term_no'] ?? '';
+                        $termNo = trim((string)($item['term_no'] ?? ''));
+                        if ($termNo !== '') {
+                            $result['b2BTermNo'][] = $termNo;
+                        }
                     }
                 }
             }

+ 46 - 7
common/components/lakala/LakalaOnboardingClient.php

@@ -2,6 +2,9 @@
 
 namespace common\components\lakala;
 
+use Lakala\OpenAPISDK\V2\Api\V2LakalaApi;
+use Lakala\OpenAPISDK\V2\V2Configuration;
+use Lakala\OpenAPISDK\V2\Model\V2ModelRequest;
 use Lakala\OpenAPISDK\V3\Api\LakalaApi;
 use Lakala\OpenAPISDK\V3\Configuration;
 use Lakala\OpenAPISDK\V3\Model\ModelRequest;
@@ -13,6 +16,7 @@ use Yii;
 class LakalaOnboardingClient
 {
     private $config;
+    private $sdkParams;
     private $orgCode;
     private $userNo;
 
@@ -50,7 +54,7 @@ class LakalaOnboardingClient
         }
         $this->orgCode = (string)$params['org_code'];
         $this->userNo = (string)$params['user_no'];
-        $this->config = new Configuration([
+        $this->sdkParams = [
             'app_debug' => !$production,
             'host_test' => 'https://test.wsmsd.cn/sit',
             'host_pro' => 'https://s2.lakala.com',
@@ -59,7 +63,8 @@ class LakalaOnboardingClient
             'sm4_key' => $params['sm4_key'],
             'merchant_private_key_path' => $params['merchant_private_key_path'],
             'lkl_certificate_path' => $params['lkl_certificate_path'],
-        ]);
+        ];
+        $this->config = new Configuration($this->sdkParams);
     }
 
     public function orgCode()
@@ -91,11 +96,34 @@ class LakalaOnboardingClient
                 'raw' => $raw,
             ];
         } catch (\Throwable $e) {
-            Yii::error('Lakala onboarding ' . $path . ': ' . $e->getMessage());
-            //检测 $e 有没有getResponseHeaders方法
-            if (method_exists($e, 'getResponseHeaders')) {
-                Yii::error('Lakala onboarding ' . $path . ' headers error: ' . json_encode($e->getResponseHeaders()));
+            self::logCallError($path, $e);
+            throw new \RuntimeException('拉卡拉接口请求失败:' . $e->getMessage());
+        }
+    }
+
+    /**
+     * v2 开放接口使用 reqData 报文,不能走 v3 的 req_data 封装。
+     */
+    public function callV2($path, array $data, $mode = 'none')
+    {
+        $api = new V2LakalaApi(new V2Configuration($this->sdkParams), $mode);
+        $request = new V2ModelRequest();
+        $request->setReqData($data);
+        try {
+            $response = $api->tradeApi($path, $request);
+            $raw = json_decode($response->getOriginalText(), true);
+            if ($response->getRetCode() !== '000000') {
+                throw new \RuntimeException($response->getRetMsg() ?: '拉卡拉接口请求失败');
             }
+
+            return [
+                'code' => $response->getRetCode(),
+                'msg' => $response->getRetMsg(),
+                'data' => json_decode(json_encode($response->getRespData()), true),
+                'raw' => $raw,
+            ];
+        } catch (\Throwable $e) {
+            self::logCallError($path, $e);
             throw new \RuntimeException('拉卡拉接口请求失败:' . $e->getMessage());
         }
     }
@@ -103,7 +131,7 @@ class LakalaOnboardingClient
     public function upload($base64, $imgType, $ocr = false)
     {
         $base = $base64;
-        
+
         return $this->call('/api/v3/tkbs/customer/file/upload', [
             'file_base64' => $base,
             'img_type' => $imgType,
@@ -125,4 +153,15 @@ class LakalaOnboardingClient
         $data['org_code'] = $data['org_code'] ?? $this->orgCode;
         return $this->call($path, $data, $mode);
     }
+
+    private static function logCallError($path, \Throwable $e)
+    {
+        Yii::error('Lakala onboarding ' . $path . ': ' . $e->getMessage());
+        if (method_exists($e, 'getResponseHeaders')) {
+            Yii::error('Lakala onboarding ' . $path . ' headers error: ' . json_encode($e->getResponseHeaders()));
+        }
+        if (method_exists($e, 'getResponseBody')) {
+            Yii::error('Lakala onboarding ' . $path . ' body error: ' . json_encode($e->getResponseBody()));
+        }
+    }
 }

+ 1 - 1
console/controllers/LakalaAccountController.php

@@ -40,7 +40,7 @@ class LakalaAccountController extends Controller
                 $failed++;
             }
         }
-        $this->stdout('Lakala authorize refresh completed: total=' . count($ids) . ', failed=' . $failed . ', accountIds=' . $accountIds . PHP_EOL);
+        $this->stdout(date('Y-m-d H:i:s') . ' Lakala authorize refresh completed: total=' . count($ids) . ', failed=' . $failed . ', accountIds=' . $accountIds . PHP_EOL);
         return $failed === 0 ? ExitCode::OK : ExitCode::UNSPECIFIED_ERROR;
     }
 }