$hdShopId, 'shopId' => $ghsShopId]); if (!empty($ghs)) { return $ghs; } } else { $custom = CustomClass::getByCondition(['ownShopId' => $ghsShopId, 'shopId' => $hdShopId]); if (!empty($custom)) { return $custom; } } $ghsShop = ShopClass::getShopInfo($ghsShopId); $ghsSjId = $ghsShop['sjId'] ?? 0; $ghsMainId = $ghsShop['mainId'] ?? 0; $ghsOwnPtStyle = $ghsShop['ptStyle'] ?? 1; $ghsAvatar = $ghsShop['shortAvatar'] ?? ''; $ghsDefault = $ghsShop['default'] ?? 0; $allowDebt = $ghsShop['allowDebt'] ?? 0; $needCheck = $ghsShop['needCheck'] ?? 0; $home = $ghsShop['home'] ?? 0; $homeAmount = $ghsShop['homeAmount'] ?? 0; $homeNum = $ghsShop['homeNum'] ?? 0; $passStatus = $needCheck == 1 ? 0 : 1; $live = $ghsShop['live'] ?? 1; $isHd = $ghsShop['isHd'] ?? 1; $showStock = $ghsShop['showStock'] ?? 0; $allowDebtAmount = $ghsShop['allowDebtAmount'] ?? 0; if ($isHd == 1) { $giveLevel = 1; $level = 1; } else { $giveLevel = 0; $level = 0; } $name = $ghsDefault == 1 && $ghsShop['shopName'] == '首店' ? $ghsShop['merchantName'] : $ghsShop['merchantName'] . ' ' . $ghsShop['shopName']; $py = stringUtil::py($name); $hdShopInfo = ShopClass::getShopInfo($hdShopId); $hdSjId = $hdShopInfo['sjId'] ?? 0; $hdMainId = $hdShopInfo['mainId'] ?? 0; $hdOwnPtStyle = $hdShopInfo['ptStyle'] ?? 1; $salt = stringUtil::charsShuffleLowerCase(8); $ghs = self::getByCondition(['ownShopId' => $hdShopId, 'shopId' => $ghsShopId]); if (empty($ghs)) { $data = [ 'sjId' => $ghsSjId, 'shopId' => $ghsShopId, 'mainId' => $ghsMainId, 'ownSjId' => $hdSjId, 'ownShopId' => $hdShopId, 'ownMainId' => $hdMainId, 'ownPtStyle' => $hdOwnPtStyle, 'name' => $name, 'py' => $py, 'avatar' => $ghsAvatar, 'mobile' => $ghsShop['mobile'] ?? '', 'province' => $ghsShop['province'] ?? '', 'city' => $ghsShop['city'] ?? '', 'dist' => $ghsShop['dist'] ?? '', 'address' => $ghsShop['address'] ?? '', 'floor' => $ghsShop['floor'] ?? '', 'fullAddress' => $ghsShop['fullAddress'] ?? '', 'lat' => $ghsShop['lat'] ?? '', 'long' => $ghsShop['long'] ?? '', 'live' => $live, 'giveLevel' => $giveLevel, 'showStock' => $showStock, 'salt' => $salt, 'passStatus' => $passStatus, 'home' => $home, 'homeAmount' => $homeAmount, 'homeNum' => $homeNum, ]; 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); $hdAvatar = $shop['shortAvatar'] ?? ''; $default = $shop['default'] ?? 0; $name = $default == 1 && $shop['shopName'] == '首店' ? $shop['merchantName'] : $shop['merchantName'] . ' ' . $shop['shopName']; //从分店同步客户,客户被修改的名称也要同步过来 $firstName = empty($changeName) ? $name : $changeName; $py = stringUtil::py($firstName); $secondName = $name; $mobile = $shop['mobile'] ?? ''; $uniGhsId = $shop['uniGhsId'] ?? 0; $hasManyGhs = $shop['hasManyGhs'] ?? 0; $hdShopPt = intval($shop['pt'] ?? 0); if ($uniGhsId == 0 && $hasManyGhs == 0) { ShopClass::updateById($hdShopId, ['uniGhsId' => $ghsId]); } else { //从只有一个供货商变成有多个供货商 ShopClass::updateById($hdShopId, ['hasManyGhs' => 1]); } $custom = CustomClass::getByCondition(['ownShopId' => $ghsShopId, 'shopId' => $hdShopId]); if (empty($custom)) { $customData = [ 'sjId' => $hdSjId, 'ghsId' => $ghsId, 'shopId' => $hdShopId, 'mainId' => $hdMainId, 'ownSjId' => $ghsSjId, 'ownShopId' => $ghsShopId, 'ownMainId' => $ghsMainId, 'ownPtStyle' => $ghsOwnPtStyle, 'name' => $firstName, 'originName' => $secondName, 'py' => $py, 'avatar' => $hdAvatar, 'mobile' => $mobile, 'province' => $shop['province'] ?? '', 'city' => $shop['city'] ?? '', 'dist' => $shop['dist'] ?? '', 'address' => $shop['address'] ?? '', 'fullAddress' => $shop['fullAddress'] ?? '', 'showAddress' => $shop['showAddress'] ?? '', 'long' => $shop['long'] ?? '', 'lat' => $shop['lat'] ?? '', 'debt' => $allowDebt, 'debtLimit' => $allowDebtAmount, 'live' => $live, 'level' => $level, 'showStock' => $showStock, 'salt' => $salt, 'passStatus' => $passStatus, 'home' => $home, 'homeAmount' => $homeAmount, 'homeNum' => $homeNum, 'pt' => $hdShopPt === 1 ? 1 : 0, ]; 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]); $customSjId = $custom['sjId'] ?? 0; $customSj = SjClass::getById($customSjId, true); $parentShopId = $customSj->parentShopId ?? 0; $introduce = []; if (!empty($parentShopId)) { $introduce = CustomClass::getByCondition(['ownShopId' => $ghsShopId, 'shopId' => $parentShopId], true); } //有新客户提醒供货商 $ghsShopObj = ShopClass::getById($ghsShopId, true); $customObj = CustomClass::getById($customId, true); WxMessageClass::newGhsCustomInform($ghsShopObj, $customObj, $introduce); //新客户加入app通知 //NoticeClass::newCustomNotice($ghsShop, $custom); if ($type == 1) { return $ghs; } return $custom; } //增加供货商 ssh 2021.3.29 public static function addGhs($data) { 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) { $data = self::getList('*', $where, 'inTurn DESC,addTime ASC'); $data['list'] = self::groupBaseInfo($data['list']); return $data; } //根据xhSupplier的id数组 取供货商信息 ssh 2021.1.18 public static function getGhsByIds($ids) { $list = self::getByIds($ids, null, 'id'); if (empty($list)) { return []; } $list = self::groupBaseInfo($list); return $list; } public static function groupBaseInfo($list) { if (empty($list)) { return $list; } $ids = array_column($list, 'shopId'); $shopInfo = ShopClass::getByIds($ids, null, 'id'); $mainIds = []; foreach ($shopInfo as $shop) { $mainId = $shop['mainId'] ?? 0; if (!empty($mainId)) { $mainIds[(int)$mainId] = (int)$mainId; } } $mainIds = array_values($mainIds); $founderStaffMap = []; if (!empty($mainIds)) { $founderStaffList = ShopAdminClass::getAllByCondition( ['mainId' => ['in', $mainIds], 'founder' => 2, 'delStatus' => 0], null, 'id,mainId', 'mainId', true ); foreach ($founderStaffList as $mainId => $founderStaff) { //分享批发店时,通过这个员工找到相应批发店 $founderStaffMap[$mainId] = intval($founderStaff->id ?? 0); } } foreach ($list as $key => $val) { $avatar = $val['avatar'] ?? ''; $list[$key]['shortAvatar'] = $avatar; $list[$key]['avatar'] = imgUtil::groupImg($avatar); $list[$key]['smallAvatar'] = imgUtil::groupImg($avatar) . "?x-oss-process=image/resize,m_fill,h_80,w_80"; $currentShopId = $val['shopId'] ?? 0; $currentShop = $shopInfo[$currentShopId] ?? []; //新人福利和推荐福利 $xrFl = $currentShop['xrFl'] ?? 0; $xrFlAmount = $currentShop['xrFlAmount'] ?? 0; $tjFl = $currentShop['tjFl'] ?? 0; $tjFlAmount = $currentShop['tjFlAmount'] ?? 0; $list[$key]['xrFl'] = $xrFl; $list[$key]['xrFlAmount'] = $xrFlAmount; $list[$key]['tjFl'] = $tjFl; $list[$key]['tjFlAmount'] = $tjFlAmount; $list[$key]['miniKilo'] = $currentShop['miniKilo'] ?? 0; $list[$key]['kiloFee'] = $currentShop['kiloFee'] ?? 0; $list[$key]['presell'] = $currentShop['presell'] ?? 0; $isOpen = 0; if (!empty($currentShop)) { $isOpen = ShopClass::isOpen($currentShop); } //营业状态 $list[$key]['isOpen'] = $isOpen; $list[$key]['openStartTime'] = $currentShop['openStartTime'] ?? ''; $list[$key]['openEndTime'] = $currentShop['openEndTime'] ?? ''; $list[$key]['meetNum'] = isset($currentShop['meetNum']) ? floatval($currentShop['meetNum']) : 0; $list[$key]['meetAmount'] = isset($currentShop['meetAmount']) ? floatval($currentShop['meetAmount']) : 0; $list[$key]['cutAmount'] = isset($currentShop['cutAmount']) ? floatval($currentShop['cutAmount']) : 0; $list[$key]['cutStyle'] = isset($currentShop['cutStyle']) ? floatval($currentShop['cutStyle']) : 0; $count = PurchaseClass::getCount(['ghsId' => $val['id'], 'debt' => PurchaseClass::DEBT_YES]); $list[$key]['debtNum'] = $count; $telephone = $currentShop['telephone'] ?? ''; $list[$key]['telephone'] = $telephone; $telephone2 = $currentShop['telephone2'] ?? ''; $list[$key]['telephone2'] = $telephone2; $balance = $val['balance'] ?? 0; $debtAmount = $val['debtAmount'] ?? 0; $remainDebtAmount = bcsub($debtAmount, $balance, 2); $list[$key]['remainDebtAmount'] = floatval($remainDebtAmount); //与花神的合作 $openKmCg = $currentShop['openKmCg'] ?? 0; $kmCgAppId = $currentShop['kmCgAppId'] ?? ''; $kmCgUrl = $currentShop['kmCgUrl'] ?? ''; $list[$key]['openKmCg'] = $openKmCg; $list[$key]['kmCgAppId'] = $kmCgAppId; $list[$key]['kmCgUrl'] = $kmCgUrl; $pfLevel = $currentShop['pfLevel'] ?? 0; $list[$key]['pfLevel'] = $pfLevel; $mainId = $currentShop['mainId'] ?? 0; $list[$key]['ghsShopAdminId'] = !empty($mainId) ? ($founderStaffMap[$mainId] ?? 0) : 0; if (isset($val['mobile']) == false || empty($val['mobile'])) { $mobile = $currentShop['mobile'] ?? ''; $list[$key]['mobile'] = $mobile; } } return $list; } //根据供货商信息 ssh 2021.1.18 public static function getGhsInfo($id) { $info = self::getById($id); if (empty($info)) { return []; } $list = self::groupBaseInfo([$info]); return current($list); } //待结款列表 ssh 2021.126 public static function getDebtList($where) { $sjId = $where['sjId'] ?? 0; if (empty($sjId)) { util::fail('没有找到商家'); } $data = self::getList('*', $where, 'debtAmount DESC'); $data['list'] = self::groupBaseInfo($data['list']); $asset = MerchantAssetClass::getBySjId($sjId); $data['debtAmount'] = $asset['cgDebtAmount'] ?? 0.00; $data['debtNum'] = $asset['cgDebtNum'] ?? 0; return $data; } //验证权限 ssh 2021.4.11 public static function valid($info, $shopId) { if (empty($info)) { util::fail('没有供货商'); } if ($info['ownShopId'] == $shopId) { return true; } util::fail('找不到供货商'); } // 建立附近批发店关系 public static function buildNearbyGhsRelations($shopId, $pfShopId, $toLat, $toLnt) { if (empty($toLat) || empty($toLnt)) { noticeUtil::push("批发关系建立失败:目标门店坐标为空,shopId={$shopId}"); return false; } if (!self::isValidAmapCoordinate($toLat, $toLnt)) { noticeUtil::push("批发关系建立失败:目标门店坐标非法,shopId={$shopId},lat={$toLat},lng={$toLnt}"); return false; } // 获取除虚拟外的所有批发店 $nearbyShops = ShopClass::getAllByCondition(['ptStyle' => 2, 'live' => 1, 'actTime>=' => strtotime('yesterday')], null, 'id, merchantName, lat, long'); if (empty($nearbyShops)) { return false; } //要排除的批发店,填门店id $exclude = [95258, 23667, 78556, 64112, 795, 1405, 28051, 62304, 42385, 56611, 38143, 15373, 42946, 15375, 15734, 15736, 15738, 15740, 24132, 29044, 29161, 30902, 44724, 48608, 48642, 82971, 38231, 1105, 3, 154, 208, 23580, 24713, 8249, 16070, 83690, 86726, 1596, 2167, 15007, 88085, 91064, 413, 62421]; foreach ($nearbyShops as $key => $nearbyShop) { if (in_array($nearbyShop['id'], $exclude)) { unset($nearbyShops[$key]); } } $nearbyShopOriginPairs = []; foreach ($nearbyShops as $nearbyShop) { if ($nearbyShop['id'] == $pfShopId) { continue; } if (!empty($nearbyShop['lat']) && !empty($nearbyShop['long']) && self::isValidAmapCoordinate($nearbyShop['lat'], $nearbyShop['long'])) { $nearbyShopOriginPairs[] = [ 'shop' => $nearbyShop, 'origin' => [ 'lat' => $nearbyShop['lat'], 'lng' => $nearbyShop['long'], ], ]; } else { // 发送钉钉通知 noticeUtil::push('批发商:' . $nearbyShop['merchantName'] . '(' . $nearbyShop['id'] . ')经纬度无效,lat=' . ($nearbyShop['lat'] ?? '') . ',lng=' . ($nearbyShop['long'] ?? '')); } } if (empty($nearbyShopOriginPairs)) { return false; } // 高德批量测距接口最多支持 100 个起点,需分批处理 $chunkPairs = array_chunk($nearbyShopOriginPairs, 100); $distanceShopPairs = []; foreach ($chunkPairs as $chunk) { $chunkOrigins = array_column($chunk, 'origin'); $distances = \common\components\mapUtil::calculateBatchDistance($chunkOrigins, $toLnt, $toLat); if (empty($distances)) { noticeUtil::push("批发关系建立失败:高德批量测距返回空结果,shopId={$shopId}"); continue; } foreach ($chunk as $index => $pair) { if (!isset($distances[$index]) || !is_numeric($distances[$index])) { continue; } $distanceShopPairs[] = [ 'distance' => (float)$distances[$index], 'shop' => $pair['shop'], ]; } } if (empty($distanceShopPairs)) { noticeUtil::push("批发关系建立失败:没有可用距离数据,shopId={$shopId}"); return false; } // 建立关系前按距离从小到大排序 usort($distanceShopPairs, function ($a, $b) { return $a['distance'] <=> $b['distance']; }); // 取前6个 $distanceShopPairs = array_slice($distanceShopPairs, 0, 6); // 建立关系并保存距离 foreach ($distanceShopPairs as $pair) { $nearbyShop = $pair['shop']; $ghsShopId = $nearbyShop['id']; $distance = $pair['distance'] ?? 0; // 距离大于39公里不建立关系 if ($distance > 39 * 1000) { continue; } // 调用 GhsClass::build 建立关系 (返回的 $ghs 包含 id 和 customId) $ghs = self::build($ghsShopId, $shopId); if (!empty($ghs) && $distance > 0) { $ghsId = $ghs['id']; $ghsName = $ghs['name'] ?? ''; noticeUtil::push("建立了一个关系:ghsId:" . $ghsId . ' name:' . $ghsName); self::updateById($ghsId, ['distance' => $distance]); } } return true; } /** * 高德坐标有效性校验(仅保留中国大陆范围坐标) * @param mixed $lat * @param mixed $lng * @return bool */ private static function isValidAmapCoordinate($lat, $lng) { if (!is_numeric($lat) || !is_numeric($lng)) { return false; } $lat = (float)$lat; $lng = (float)$lng; if ($lng < 72.004 || $lng > 137.8347) { return false; } if ($lat < 0.8293 || $lat > 55.8271) { return false; } return true; } }