LogisticsSyncQueryService.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. <?php
  2. namespace addons\WangDianTong\common\service;
  3. use addons\CloudStock\common\logic\order\WangDianTongLogic;
  4. use addons\WangDianTong\common\enums\WangDianTongEnum;
  5. use addons\WangDianTong\common\expand\sdk\wangdian\model\LogisticsItem;
  6. use addons\WangDianTong\common\expand\sdk\wangdian\model\LogisticsSyncAck;
  7. use addons\WangDianTong\common\expand\sdk\wangdian\model\LogisticsSyncQuery;
  8. use addons\WangDianTong\common\expand\sdk\wangdian\qjb\model\SalesLogisticsSyncGetSyncListExtItemModel;
  9. use addons\WangDianTong\common\expand\sdk\wangdian\qjb\model\SalesLogisticsSyncGetSyncListExtModel;
  10. use addons\WangDianTong\common\expand\sdk\wangdian\qjb\model\SalesLogisticsSyncUpdateModel;
  11. use addons\WangDianTong\common\expand\sdk\wangdian\qjb\model\SyncItemModel;
  12. use addons\WangDianTong\common\expand\sdk\wangdian\qjb\request\SalesLogisticsSyncGetSyncListExtRequest;
  13. use addons\WangDianTong\common\expand\sdk\wangdian\qjb\request\SalesLogisticsSyncUpdateRequest;
  14. use addons\WangDianTong\common\models\AddonsWangDianTongLogisticsCompany;
  15. use addons\WangDianTong\common\models\AddonsWangDianTongLogisticsSync;
  16. use addons\WangDianTong\common\traits\RequestRetryTrait;
  17. use common\enums\StatusEnum;
  18. use common\helpers\FormatHelper;
  19. use common\models\mall\Mall;
  20. use common\models\order\Order;
  21. use common\models\order\OrderExpress;
  22. use yii\db\Exception;
  23. use yii\helpers\Json;
  24. class LogisticsSyncQueryService
  25. {
  26. use RequestRetryTrait;
  27. public function sync()
  28. {
  29. $mall_ids = Mall::getAllMallIds();
  30. $limit = 100;
  31. foreach ($mall_ids as $mall_id) {
  32. $config = (new SettingService())->get($mall_id);
  33. if (empty($config['is_enable'])) {
  34. continue;
  35. }
  36. if (empty($config['app_version'])) {
  37. $config['app_version'] = WangDianTongEnum::DEFAULT_APP_VERSION;
  38. }
  39. // 执行
  40. // 不需要分页,因为 每次取出来就直接执行ack了
  41. $this->{$config['app_version']}($mall_id, $limit, $config);
  42. }
  43. }
  44. protected function flagship(int $mall_id, int $limit, array $config)
  45. {
  46. $page_no = 0;
  47. while (true) {
  48. $model = new SalesLogisticsSyncGetSyncListExtModel();
  49. $mParams = new SalesLogisticsSyncGetSyncListExtItemModel();
  50. $mParams->shop_no = $config['shop_no'];
  51. // $mParams->is_part_sync = 1;
  52. $mParams->is_own_platform = true;
  53. $m_params = $mParams->toArray();
  54. $model->params = $m_params;
  55. $model->page_size = $limit;
  56. $model->page_no = $page_no;
  57. $model->calc_total = 0;
  58. $req = new SalesLogisticsSyncGetSyncListExtRequest($config);
  59. $resp = $req->send($model);
  60. try {
  61. if (!empty($resp->getError())) {
  62. throw new \Exception($resp->getError());
  63. }
  64. $res = $resp->toArray();
  65. if (!isset($res['status']) || $res['status'] != StatusEnum::DISABLED) {
  66. throw new \Exception($res['message'] ?? var_export($res, true));
  67. }
  68. if (empty($res['data'])) {
  69. break;
  70. }
  71. $ack_list = [];
  72. $sync_list = [];
  73. // 执行处理
  74. foreach ($res['data'] as $k => $v) {
  75. /**
  76. * @var $logistics AddonsWangDianTongLogisticsCompany
  77. */
  78. $logistics = AddonsWangDianTongLogisticsCompany::getOne([
  79. ['express_code' => $v['logistics_code']]
  80. ]);
  81. if (empty($logistics)) {
  82. // 那么就是其他
  83. $logistics = AddonsWangDianTongLogisticsCompany::getOne([
  84. ['erp_type' => 2000]
  85. ]);
  86. }
  87. // 云库存订单
  88. if (strpos($v['tid'], "cs_") === 0) {
  89. $flag = WangDianTongLogic::syncLogistics([
  90. 'mall_id' => $mall_id,
  91. 'express_code' => $logistics->express_code,
  92. 'express_no' => $v['logistics_no'],
  93. 'id' => substr($v['tid'], 3), // 取出订单ID
  94. ], $logistics->toArray());
  95. } else { // 平台订单
  96. // 因为旺店通物流订单有拆单的功能。拆单的话需要独立处理
  97. $orderDetails = [];
  98. if (!empty($v['is_part_sync']) && !empty($v['oids'])) {
  99. $orderExplodes = explode(',', $v['oids']); // 将oids字段拆成数组
  100. $orderDetails = array_map(function ($item) {
  101. // 过滤掉前面的主订单,获取子订单ID
  102. $explode = explode('_', $item);
  103. return end($explode);
  104. }, $orderExplodes);
  105. }
  106. $order = Order::getOne([
  107. ['order_no' => $v['tid']]
  108. ], true, ['detail' => function($query) use ($orderDetails) {
  109. $query->andWhere(['is_virtual' => StatusEnum::DISABLED])->andWhere(['status' => StatusEnum::ENABLED]);
  110. if (!empty($orderDetails)) { // 如果是拆单那面就获取相应的子订单
  111. $query->andWhere(['id' => $orderDetails]);
  112. }
  113. }]);
  114. if (empty($order)) throw new \Exception("订单不存在: {$v['tid']}");
  115. if (!empty($order['detail'])) {
  116. // 发货
  117. $flag = $this->ship([
  118. 'mall_id' => $mall_id,
  119. 'express_id' => $logistics->express_id > 0 ? $logistics->express_id : 10000, // 如果没有就是其他快递公司
  120. 'logistics_name'=> $logistics->express_id > 0 ? $logistics->express_name : $logistics->erp_name,
  121. 'logistics_code'=> $v['logistics_no'],
  122. 'order_id' => $order['id'], // 取出订单ID
  123. 'memo' => $logistics->express_id == 0 || $logistics->express_id == 10000 ? "快递:{$logistics->erp_name}" : '',
  124. 'order_detail_ids' => array_column($order['detail'], 'id'),
  125. ]);
  126. $flag = is_string($flag) ? $flag : true;
  127. } else {
  128. $flag = true;
  129. }
  130. }
  131. $ackModel = new SyncItemModel();
  132. $ackModel->sync_id = $v['sync_id'];
  133. $ackModel->status = $flag === true || $flag == '订单已经发货' ? StatusEnum::DISABLED : StatusEnum::ENABLED; // 0 success, 1 fail
  134. $ackModel->error_msg = $flag === true ? '' : (is_string($flag) ? $flag : ''); // 相关描述信息,可在erp的物流同步界面看到
  135. $ack_list[] = $ackModel->toArray();
  136. $v['flag'] = $flag;
  137. $sync_list[$v['tid']] = [
  138. 'mall_id' => $mall_id,
  139. 'rec_id' => $v['sync_id'],
  140. 'ack_status'=> WangDianTongEnum::LOGISTICS_ACK_STATUS_PROCESS,
  141. 'order_id' => $order['id'] ?? $v['tid'],
  142. 'created_at' => time(),
  143. 'updated_at' => time(),
  144. 'logistics_code' => $v['logistics_no'],
  145. 'logistics_name' => $logistics->express_id > 0 ? $logistics->express_name : $logistics->erp_name,
  146. 'logistics_code_erp' => $v['logistics_no'],
  147. 'logistics_name_erp' => $v['logistics_name'],
  148. 'sync_res' => Json::encode($v)
  149. ];
  150. }
  151. // 存储同步信息
  152. $this->saveSync($sync_list);
  153. // 过滤发货不成功的
  154. $ack_list = array_filter($ack_list, function ($val) {
  155. return $val['status'] == StatusEnum::DISABLED;
  156. });
  157. // 发完后需要通知那边进行确认
  158. // ack需要在这一步,因为同步接口没有分页参数,如果当前逻辑中不做ack那么永远都是第一页?
  159. // 重试?
  160. if (!empty($ack_list)) { // 确认过滤后的数据
  161. $flag = $this->retry(function () use ($config, $ack_list) {
  162. return $this->flagshipAck($config, array_values($ack_list));
  163. }, 1);
  164. \Yii::error(__METHOD__ . " flagshipAck $flag");
  165. // if ($flag !== true) throw new \Exception($flag);
  166. // ack失败,那么就需要重试 - 如果重试还是失败那么需要怎么处理?
  167. $this->updateSync($ack_list, $flag !== true ?
  168. WangDianTongEnum::LOGISTICS_ACK_STATUS_FAIL : WangDianTongEnum::LOGISTICS_ACK_STATUS_SUCCESS,
  169. is_string($flag) ? $flag : '', 'sync_id');
  170. }
  171. $p_total = count($res['data']);
  172. if ($p_total < $limit) break;
  173. } catch (\Exception $e) {
  174. sleep(1);
  175. \Yii::error(FormatHelper::exception($e, __METHOD__));
  176. break;
  177. }
  178. $page_no++;
  179. }
  180. }
  181. protected function enterprise(int $mall_id, int $limit, array $config)
  182. {
  183. while (true) {
  184. $model = new LogisticsSyncQuery();
  185. $model->limit = $limit;
  186. $model->is_part_sync_able = 1;
  187. $model->shop_no = $config['shop_no'];
  188. $req = new \addons\WangDianTong\common\expand\sdk\wangdian\src\LogisticsSyncQuery($config);
  189. $resp = $req->send($model);
  190. try {
  191. if ($resp->getError()) {
  192. // 执行处理
  193. throw new \Exception($resp->getError());
  194. }
  195. $res = $resp->toArray();
  196. if (!isset($res['code']) || $res['code'] != StatusEnum::DISABLED) {
  197. throw new \Exception($res['message'] ?? var_export($res, true));
  198. }
  199. // 其他逻辑 - 手动发货商品
  200. if (!empty($res['trades'])) {
  201. $ack_list = [];
  202. $sync_list = [];
  203. foreach ($res['trades'] as $trade) {
  204. /**
  205. * @var $logistics AddonsWangDianTongLogisticsCompany
  206. */
  207. $logistics = AddonsWangDianTongLogisticsCompany::getOne([
  208. ['erp_type' => $trade['logistics_type']]
  209. ]);
  210. if (empty($logistics)) {
  211. // 那么就是其他
  212. $logistics = AddonsWangDianTongLogisticsCompany::getOne([
  213. ['erp_type' => 2000]
  214. ]);
  215. }
  216. // 云库存订单
  217. if (strpos($trade['tid'], "cs_") === 0) {
  218. $flag = WangDianTongLogic::syncLogistics([
  219. 'mall_id' => $mall_id,
  220. 'express_code' => $logistics->express_code,
  221. 'express_no' => $trade['logistics_no'],
  222. 'id' => substr($trade['tid'], 3), // 取出订单ID
  223. ], $logistics->toArray());
  224. } else { // 平台订单
  225. $order = Order::getOne([
  226. ['order_no' => $trade['tid']]
  227. ], true, ['detail' => function($query) {
  228. $query->andWhere(['is_virtual' => StatusEnum::DISABLED])->andWhere(['status' => StatusEnum::ENABLED]);
  229. }]);
  230. if (empty($order)) throw new \Exception("订单不存在: {$trade['tid']}");
  231. if (!empty($order['detail'])) {
  232. $flag = $this->ship([
  233. 'mall_id' => $mall_id,
  234. 'express_id' => $logistics->express_id > 0 ? $logistics->express_id : 10000, // 如果没有就是其他快递公司
  235. 'logistics_name'=> $logistics->express_id > 0 ? $logistics->express_name : $logistics->erp_name,
  236. 'logistics_code'=> $trade['logistics_no'],
  237. 'order_id' => $order['id'], // 取出订单ID
  238. 'memo' => '',
  239. 'order_detail_ids' => array_column($order['detail'], 'id'),
  240. ]);
  241. if (is_string($flag)) {
  242. \Yii::error("同步物流失败: {$order['id']} -- $flag " . __METHOD__);
  243. }
  244. $flag = is_string($flag) ? $flag : true;
  245. } else {
  246. $flag = true;
  247. }
  248. }
  249. $ackModel = new LogisticsItem();
  250. $ackModel->rec_id = $trade['rec_id'];
  251. $ackModel->status = $flag === true || $flag == '订单已经发货' ? StatusEnum::DISABLED : StatusEnum::ENABLED; // 0 success, 1 fail
  252. $ackModel->message = $flag === true ? '' : (is_string($flag) ? $flag : ''); // 相关描述信息,可在erp的物流同步界面看到
  253. $ack_list[] = $ackModel->toArray();
  254. $sync_list[$order['id'] ?? $trade['tid']] = [
  255. 'mall_id' => $mall_id,
  256. 'rec_id' => $trade['rec_id'],
  257. 'ack_status'=> WangDianTongEnum::LOGISTICS_ACK_STATUS_PROCESS,
  258. 'order_id' => $order['id'] ?? $trade['tid'],
  259. 'created_at' => time(),
  260. 'updated_at' => time(),
  261. 'logistics_code' => $trade['logistics_no'],
  262. 'logistics_name' => $logistics->express_id > 0 ? $logistics->express_name : $logistics->erp_name,
  263. 'logistics_code_erp' => $trade['logistics_no'],
  264. 'logistics_name_erp' => $trade['logistics_name'],
  265. 'sync_res' => Json::encode($trade)
  266. ];
  267. }
  268. // 存储同步信息
  269. $this->saveSync($sync_list);
  270. // 过滤发货不成功的
  271. $ack_list = array_filter($ack_list, function ($val) {
  272. return $val['status'] == StatusEnum::DISABLED;
  273. });
  274. // 发完后需要通知那边进行确认
  275. // ack需要在这一步,因为同步接口没有分页参数,如果当前逻辑中不做ack那么永远都是第一页?
  276. // 重试?
  277. if (!empty($ack_list)) { // 确认过滤后的数据
  278. $flag = $this->retry(function () use ($config, $ack_list) {
  279. return $this->ack($config, array_values($ack_list));
  280. });
  281. // if ($flag !== true) throw new \Exception($flag);
  282. // ack失败,那么就需要重试 - 如果重试还是失败那么需要怎么处理?
  283. $this->updateSync($ack_list, $flag !== true ?
  284. WangDianTongEnum::LOGISTICS_ACK_STATUS_FAIL : WangDianTongEnum::LOGISTICS_ACK_STATUS_SUCCESS, is_string($flag) ? $flag : '');
  285. }
  286. }
  287. // 如果总数小于或者等于100条那么就跳出当前循环
  288. if ($res['total_count'] <= $limit) break;
  289. } catch (\Exception $e) {
  290. sleep(1);
  291. var_dump($e->getMessage(), $e->getLine(), $e->getFile());
  292. \Yii::error(FormatHelper::exception($e, __METHOD__));
  293. break;
  294. }
  295. }
  296. }
  297. /**
  298. * @param array $data
  299. * @return OrderExpress|string
  300. */
  301. protected function ship(array $data)
  302. {
  303. try {
  304. $flag = OrderExpress::delivery([
  305. 'mall_id' => $data['mall_id'],
  306. 'shipping_type' => 1, // 0无需物流,1快递配送
  307. 'express_id' => $data['express_id'], // 快递公司ID
  308. 'id' => $data['order_id'], // 订单ID
  309. 'express_name' => $data['logistics_name'], // 快递公司名称
  310. 'express_no' => $data['logistics_code'], // 快递单号
  311. 'memo' => $data['memo'] ?? '', // 商家备注
  312. 'order_detail_ids' => $data['order_detail_ids'], // 子订单ID
  313. 'shipping_change' => null, // 是否为修改
  314. 'customer_name' => '', // 京东物流编号
  315. 'operator_name' => 'Erp发货',
  316. 'operator_id' => 0
  317. ]);
  318. if ($flag) return $flag;
  319. return OrderExpress::getStaticError();
  320. } catch (\Exception $e) {
  321. return $e->getMessage();
  322. }
  323. }
  324. /**
  325. * @param array $config
  326. * @param array $ack_list
  327. * @return string|true
  328. */
  329. protected function flagshipAck(array $config, array $ack_list)
  330. {
  331. try {
  332. $req = new SalesLogisticsSyncUpdateRequest($config);
  333. $model = new SalesLogisticsSyncUpdateModel();
  334. $model->syncList = $ack_list;
  335. $resp = $req->send($model);
  336. if ($resp->getError()) throw new \Exception($resp->getError());
  337. $res = $req->toArray();
  338. if (!isset($res['status']) || $res['status'] != StatusEnum::DISABLED) throw new \Exception($res['message'] ?? var_export($res, true));
  339. return true;
  340. } catch (\Exception $e) {
  341. return $e->getMessage();
  342. }
  343. }
  344. /**
  345. * @param array $config
  346. * @param array $ack_list
  347. * @return string|true
  348. */
  349. protected function ack(array $config, array $ack_list)
  350. {
  351. try {
  352. $model = new LogisticsSyncAck();
  353. $model->logistics_list = Json::encode($ack_list);
  354. $req = new \addons\WangDianTong\common\expand\sdk\wangdian\src\LogisticsSyncAck($config);
  355. $resp = $req->send($model);
  356. if ($resp->getError()) throw new \Exception($resp->getError());
  357. $res = $req->toArray();
  358. if (!isset($res['code']) || $res['code'] != StatusEnum::DISABLED) throw new \Exception($res['message'] ?? var_export($res, true));
  359. return true;
  360. } catch (\Exception $e) {
  361. return $e->getMessage();
  362. }
  363. }
  364. /**
  365. * 保存同步信息
  366. * @param array $sync_list
  367. * @return void
  368. * @throws Exception
  369. */
  370. protected function saveSync(array $sync_list)
  371. {
  372. $order_ids = array_keys($sync_list);
  373. $local_order_ids = AddonsWangDianTongLogisticsSync::find()->where(['order_id' => $order_ids])->select('order_id')->column();
  374. if (!empty($local_order_ids)) {
  375. foreach ($sync_list as $key => $item) {
  376. if (in_array($key, $local_order_ids)) unset($sync_list[$key]);
  377. }
  378. }
  379. if (!empty($sync_list)) {
  380. $sync_list = array_values($sync_list);
  381. AddonsWangDianTongLogisticsSync::find()
  382. ->createCommand()
  383. ->batchInsert(AddonsWangDianTongLogisticsSync::tableName(), array_keys($sync_list[0]), $sync_list)
  384. ->execute();
  385. }
  386. }
  387. /**
  388. * 同步更新
  389. * @param array $ack_list
  390. * @param int $ack_status
  391. * @param string $remark
  392. * @return void
  393. */
  394. protected function updateSync(array $ack_list, int $ack_status, string $remark = '', string $column_key = 'rec_id')
  395. {
  396. AddonsWangDianTongLogisticsSync::updateAll(['ack_status' => $ack_status, 'remark' => $remark],
  397. ['and', ['rec_id' => array_column($ack_list, $column_key)]]);
  398. }
  399. }