Explorar o código

Merge branch 'master' into dev

shish hai 2 meses
pai
achega
fa45427445
Modificáronse 2 ficheiros con 87 adicións e 5 borrados
  1. 2 2
      app-pt/views/main/app.php
  2. 85 3
      biz/ghs/classes/GhsClass.php

+ 2 - 2
app-pt/views/main/app.php

@@ -235,8 +235,8 @@
             }
             // 这里可以根据appType跳转到对应的下载链接
             const downloadUrls = {
-                'xiaohuabao': 'https://api.shop.hzghd.com/1307.apk', // 销花宝APP下载链接
-                'huazhanggui': 'https://api.shop.hzghd.com/238.apk', // 花掌柜APP下载链接
+                'xiaohuabao': 'https://api.shop.hzghd.com/1310.apk', // 销花宝APP下载链接
+                'huazhanggui': 'https://api.shop.hzghd.com/245.apk', // 花掌柜APP下载链接
                 'huazhanggui_cashier': 'https://api.shop.hzghd.com/200.apk', // 花掌柜收银下载链接
                 'xiaohuabao_cashier': 'https://api.shop.hzghd.com/159.apk' // 销花宝收银下载链接
             };

+ 85 - 3
biz/ghs/classes/GhsClass.php

@@ -1,5 +1,18 @@
 <?php
 
+/**
+ * GhsClass:供货商(ghs)与客户(花店/ownShop)双向关系建立与维护的核心业务类。
+ * 主要用途:通过 build() 实现“确保 ghs 记录与 xhGhsCustom 客户记录成对存在”(get-or-create 幂等操作)。
+ * 调用方:app-hd/controllers/GhsController::actionInfo(高频入口)、app-ghs GhsController 加供货商流程、
+ * console 初始化/迁移脚本、biz-ghs/custom/services/CustomService 介绍人逻辑、buildNearbyGhsRelations 等十余处。
+ * 解决的问题:原先“先 getByCondition 为空再 addGhs/addCustom”的 check-then-insert 在并发(多设备、快速刷新、脚本重试等)
+ * 场景下,会同时看到不存在而双双 INSERT,导致 xhGhsCustom 表唯一索引 'custom'(ownShopId+shopId 组合)触发 1062 Duplicate entry。
+ * 本次小改:在两个 INSERT 执行点增加 try-add + catch 重复键异常后重新 getByCondition 的防御逻辑,
+ * 使 build 成为并发安全的幂等方法;不改调用签名、不强制要求外层事务、改动极小。
+ * 副作用说明:race 恢复时可能重复执行 shop 标记位更新和 newGhsCustomInform 通知,属于可接受的小副作用(通知幂等性由接收端或后续优化保障)。
+ * 约束:方法本身不开启事务(由调用方在需要强一致时包裹);getByCondition 为普通查询,无 FOR UPDATE。
+ */
+
 namespace biz\ghs\classes;
 
 use biz\shop\classes\ShopClass;
@@ -30,7 +43,25 @@ class GhsClass extends BaseClass
         return false;
     }
 
-    //新增客户和供货商关系 ssh 2021.4.13
+    /**
+     * 确保供货商与花店的成对关系存在(ghs 表 + xhGhsCustom 表),支持并发幂等创建。
+     * 干什么:
+     *   - type=1:返回/创建 xhGhs 记录(供货商视角),并确保对方 xhGhsCustom 也存在。
+     *   - type=2:返回/创建 xhGhsCustom 记录(客户视角)。
+     * 入参业务含义:
+     *   $ghsShopId - 批发店(供货商)shopId
+     *   $hdShopId  - 花店(ownShopId,客户归属店)shopId
+     *   $type      - 1 返回 ghs,2 返回 custom(默认1)
+     *   $changeName- 可选覆盖名称(用于从分店同步客户名称时)
+     * 返回:ghs 或 custom 的数组记录(含 id 等)
+     * 副作用:可能写入 ghs/custom 表、更新 shop.uniGhsId / hasManyGhs、触发 WxMessageClass::newGhsCustomInform 新客户通知。
+     * 关键边界与本次修改:
+     *   - 内部对 addGhs / addCustom 做了唯一键冲突防御:catch 到重复键(MySQL 1062/23000/Integrity)后,
+     *     立即重新 getByCondition 返回已存在记录,函数继续执行后续 updateById / 通知等逻辑。
+     *   - 因此 build 现在是“确保存在”语义的幂等方法,不会因并发而向上抛 1062。
+     *   - 不改变调用方任何代码;不强制事务(调用方可在外层包 beginTransaction)。
+     * 历史:ssh 2021.4.13 初版;2026-06 小改增加并发 duplicate 容错。
+     */
     public static function build($ghsShopId, $hdShopId, $type = 1, $changeName = '')
     {
         if ($type == 1) {
@@ -108,7 +139,18 @@ class GhsClass extends BaseClass
                 'homeAmount' => $homeAmount,
                 'homeNum' => $homeNum,
             ];
-            $ghs = self::addGhs($data);
+            try {
+                $ghs = self::addGhs($data);
+            } catch (\Exception $e) {
+                if (self::isDuplicateKeyError($e)) {
+                    // 并发或竞态条件下,另一请求已成功插入相同 (ownShopId, shopId) 的 ghs 记录
+                    // 捕获唯一键冲突(1062 / 23000 / Integrity constraint violation),重新查询返回已存在记录
+                    // 为什么:让 build 成为幂等操作,防止 1062 错误直接暴露给调用方(如 hd actionInfo)
+                    $ghs = self::getByCondition(['ownShopId' => $hdShopId, 'shopId' => $ghsShopId]);
+                } else {
+                    throw $e;
+                }
+            }
         }
         $ghsId = $ghs['id'] ?? 0;
         $shop = ShopClass::getShopInfo($hdShopId);
@@ -168,7 +210,19 @@ class GhsClass extends BaseClass
                 'homeNum' => $homeNum,
                 'pt' => $hdShopPt === 1 ? 1 : 0,
             ];
-            $custom = CustomClass::addCustom($customData);
+            try {
+                $custom = CustomClass::addCustom($customData);
+            } catch (\Exception $e) {
+                if (self::isDuplicateKeyError($e)) {
+                    // 这是导致原 1062 的主路径:xhGhsCustom 表 unique key 'custom'(ownShopId + shopId)
+                    // 并发场景下两个请求同时通过前面的 getByCondition 看到不存在,同时执行 addCustom
+                    // 捕获后重新 get,函数继续走 customId 赋值、update ghs.customId、发通知等
+                    // 为什么采用 catch+reget 而非外层事务+锁:最小改动、兼容现有数十处调用点、不改变事务边界假设
+                    $custom = CustomClass::getByCondition(['ownShopId' => $ghsShopId, 'shopId' => $hdShopId]);
+                } else {
+                    throw $e;
+                }
+            }
         }
         $customId = $custom['id'] ?? 0;
         self::updateById($ghsId, ['customId' => $customId]);
@@ -199,6 +253,34 @@ class GhsClass extends BaseClass
         return self::add($data);
     }
 
+    /**
+     * 判断异常是否为数据库唯一键冲突(Duplicate key / 1062 / 23000 / Integrity constraint violation)。
+     * 干什么:为 build() 等“确保存在”幂等创建逻辑提供统一的重复键识别能力。
+     * 为什么:项目中已大量使用普通 get+add 模式,且 MySQL 错误信息稳定,通过 message+code 识别即可,
+     * 避免引入新 use 或依赖具体 Exception 子类(yii\db\IntegrityException 等),保持小改动。
+     * 识别特征:code===23000 或消息包含 "Duplicate entry" / "Integrity constraint violation"。
+     * @param \Throwable|\Exception|null $e
+     * @return bool
+     */
+    protected static function isDuplicateKeyError($e)
+    {
+        if (empty($e)) {
+            return false;
+        }
+        $msg = $e->getMessage();
+        $code = $e->getCode();
+        if ($code === 23000) {
+            return true;
+        }
+        if (strpos($msg, 'Duplicate entry') !== false) {
+            return true;
+        }
+        if (strpos($msg, 'Integrity constraint violation') !== false) {
+            return true;
+        }
+        return false;
+    }
+
     //供货商列表 ssh 2021.1.24
     public static function getGhsList($where)
     {