|
|
@@ -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)
|
|
|
{
|