CloudStockAgentOrderService.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. <?php
  2. namespace addons\CloudStock\common\services;
  3. use addons\CloudStock\common\enums\CloudStockEnum;
  4. use addons\CloudStock\common\enums\PaymentTradeItemRefundEnum;
  5. use addons\CloudStock\common\logic\order\OrderLimitLogic;
  6. use addons\CloudStock\common\models\CloudStockAgentOrder;
  7. use addons\CloudStock\common\models\CloudStockAgentOrderRefund;
  8. use common\enums\AddonsEnum;
  9. use common\enums\AppEnum;
  10. use common\enums\PayTypeEnum;
  11. use common\enums\PreviewTypeEnum;
  12. use common\enums\StatusEnum;
  13. use common\globals\GlobalInvoke;
  14. use common\helpers\FormatHelper;
  15. use common\logic\order\OrderFormLogic;
  16. use common\logic\order\OrderInitDataLogic;
  17. use common\logic\order\OrderMarketingLogic;
  18. use common\models\common\PaymentTrade;
  19. use common\models\goods\GoodsAttr;
  20. use common\models\order\Order;
  21. use common\models\order\OrderRefund;
  22. use Exception;
  23. use RuntimeException;
  24. use Yii;
  25. class CloudStockAgentOrderService extends BaseService
  26. {
  27. /**
  28. * 验证商品
  29. *
  30. * @param CloudStockAgentCartService $cartService
  31. * @param CloudStockLevelService $levelService
  32. * @param CloudStockAgentGoodsService $goodsService
  33. * @param array $goodsGoodsNumList
  34. * @param bool $isBuy
  35. *
  36. * @return bool
  37. */
  38. public function checkLimit(
  39. CloudStockAgentCartService $cartService,
  40. CloudStockLevelService $levelService,
  41. CloudStockAgentGoodsService $goodsService,
  42. array $goodsGoodsNumList = [],
  43. bool $isBuy = true
  44. ): bool
  45. {
  46. // 验证代理权重允许最大和最少提货的数量
  47. if (!$cartService->checkLevelFillOnceMinAndMax($levelService, $goodsGoodsNumList, $isBuy)) {
  48. $this->setError($cartService->getError());
  49. return false;
  50. }
  51. // 验证商品库存
  52. if (!$goodsService->checkGoodsStockByGoodsIds($goodsGoodsNumList)) {
  53. $this->setError($goodsService->getError());
  54. return false;
  55. }
  56. // 验证商品数据
  57. if (!$goodsService->checkGoodsDataByGoodsIds(array_column($goodsGoodsNumList, 'goods_id'))) {
  58. $this->setError($goodsService->getError());
  59. return false;
  60. }
  61. $configs = \Yii::$app->services->setting->addons->get($this->mall_id, AddonsEnum::ADDONS_NAME_CLOUD_STOCK, 'basic');
  62. $isEnableOrderNumLimit = $configs['is_enable_order_num_limit'] ?? 0;
  63. $orderLimitType = $configs['order_limit_type'] ?? 2;
  64. $orderLimitNum = $configs['order_limit_num'] ?? 0;
  65. if (!$isEnableOrderNumLimit) {
  66. return true;
  67. }
  68. switch ($orderLimitType) {
  69. case 1: // 限制kg
  70. if ($cartService->getCartWeight($goodsGoodsNumList) > $orderLimitNum) {
  71. $this->setError(sprintf('超出最大可自提重量, 最大可自提重量为:%.2fkg', $orderLimitNum));
  72. return false;
  73. }
  74. break;
  75. case 2: // 限制件数
  76. if ($cartService->getCartNum() > $orderLimitNum) {
  77. $this->setError(sprintf('超出最大可自提件数, 最大可自提:%d件', $orderLimitNum));
  78. return false;
  79. }
  80. break;
  81. }
  82. return true;
  83. }
  84. /**
  85. * 获取计算运费from
  86. *
  87. * @param CloudStockAgentGoodsService $goodsService
  88. * @param int $mallId
  89. * @param int $userId
  90. * @param int $addrId
  91. * @param array $goodsAndNumList
  92. *
  93. * @return mixed
  94. */
  95. public function getForm(
  96. CloudStockAgentGoodsService $goodsService,
  97. int $mallId,
  98. int $userId,
  99. int $addrId,
  100. array $goodsAndNumList
  101. )
  102. {
  103. // 计算运费
  104. $orderFormLogic = new OrderFormLogic();
  105. $form = $orderFormLogic->getDefaultform($mallId, $userId);
  106. $freightAttr = [];
  107. $goodsIds = array_column($goodsAndNumList, 'goods_id');
  108. $goodsAttrMap = $goodsService->getGoodsAttrMap($goodsIds);
  109. foreach ($goodsAndNumList as $orderDetail) {
  110. if (!isset($goodsAttrMap[$orderDetail['goods_id']])) {
  111. throw new \RuntimeException(sprintf('商品异常,ID:%d', $orderDetail['goods_id']));
  112. }
  113. $freightAttr[] = ['attr_id' => $goodsAttrMap[$orderDetail['goods_id']], 'num' => $orderDetail['num']];
  114. }
  115. if (empty($freightAttr)) {
  116. throw new \RuntimeException('当前订单不允许修改地址');
  117. }
  118. $form->attributes = [
  119. 'type' => PreviewTypeEnum::FREIGHT,
  120. 'data' => [
  121. 'shipping_type' => \common\enums\ShippingTypeEnum::LOGISTICS,
  122. 'freight_attr' => $freightAttr,
  123. 'address_id' => $addrId
  124. ]
  125. ];
  126. $form['action'] = AppEnum::ACTION_CREATE;
  127. // 初始化订单数据
  128. $orderInitDataLogic = new OrderInitDataLogic();
  129. $form = $orderInitDataLogic->execute($form, $form->type);
  130. if ($form === false) throw new \RuntimeException($orderInitDataLogic->getError());
  131. // 购买限制验证
  132. $orderLimitLogic = new OrderLimitLogic();
  133. $form = $orderLimitLogic->run($form);
  134. if ($form === false) throw new RuntimeException($orderLimitLogic->getError());
  135. // 营销减免活动
  136. $orderMarketingLogic = new OrderMarketingLogic();
  137. $form = $orderMarketingLogic->run($form, true);
  138. if ($form === false) throw new \RuntimeException($orderMarketingLogic->getError());
  139. if (empty($form['group_order_goods']) || !is_array($form['group_order_goods'])) {
  140. throw new \RuntimeException('订单异常');
  141. }
  142. if (empty($form['group_order_goods'])) {
  143. throw new \RuntimeException('订单异常');
  144. }
  145. return $form;
  146. }
  147. /**
  148. * 得到运费
  149. *
  150. * @param CloudStockAgentGoodsService $goodsService
  151. * @param int $mall_id
  152. * @param int $user_id
  153. * @param int $addr_id
  154. * @param array $goodsAndNumListData
  155. * @param object|null $form
  156. *
  157. * @return float
  158. */
  159. public function getFreight(
  160. CloudStockAgentGoodsService $goodsService,
  161. int $mall_id,
  162. int $user_id,
  163. int $addr_id,
  164. array $goodsAndNumListData,
  165. object $form = null
  166. ): float
  167. {
  168. if (is_null($form)) {
  169. $form = $this->getForm($goodsService, $mall_id, $user_id, $addr_id, $goodsAndNumListData);
  170. }
  171. if (!isset($form['marketing']['deduct']['shipping_fee']['money'])) {
  172. throw new \RuntimeException('订单异常');
  173. }
  174. return $form['marketing']['deduct']['shipping_fee']['money'];
  175. }
  176. /**
  177. * @param CloudStockAgentOrder $order
  178. *
  179. * @return bool
  180. * @throws Exception
  181. */
  182. public function shippingRefunds(CloudStockAgentOrder $order): bool
  183. {
  184. // 不需要退
  185. if ($order->pay_money <= 0 || $order->is_shipping_refunded || empty($order->trade_no)) {
  186. return true;
  187. }
  188. // 已退款或退款中 直接返回
  189. if (!$this->isAllowApplyRefund($order->id)) {
  190. return true;
  191. }
  192. // 查出流水表
  193. $paymentTrade = PaymentTrade::findOne([
  194. 'out_trade_no' => $order->trade_no,
  195. 'mall_id' => $this->mall_id,
  196. 'is_pay' => StatusEnum::ENABLED
  197. ]);
  198. if (empty($paymentTrade)) {
  199. $this->setError('未查询到支付记录');
  200. return false;
  201. }
  202. if (empty($paymentTrade->pay_fee)) {
  203. return true;
  204. }
  205. $baseLogData = [
  206. 'method' => __METHOD__,
  207. 'agent_order_id' => $order->id
  208. ];
  209. // 创建退款订单
  210. try {
  211. [
  212. 'cloudStockAgentOrderRefund' => $cloudStockAgentOrderRefund,
  213. 'mallOrder' => $mallOrder,
  214. 'mallOrderRefund' => $mallOrderRefund
  215. ] = $this->createOrderRefund($paymentTrade, $order);
  216. } catch (Exception $e) {
  217. Yii::$app->custom->logs(FormatHelper::exception($e, '创建售后订单失败'), $baseLogData);
  218. $this->setError('退款失败,请稍后重试!');
  219. return false;
  220. }
  221. // 执行退款
  222. $defaultErrMsg = '自提订单退款失败';
  223. try {
  224. $refundRes = $this->refund($paymentTrade, $order, $cloudStockAgentOrderRefund, $mallOrderRefund);
  225. } catch (Exception $e) {
  226. $refundRes = false;
  227. $refundErrorMsg = FormatHelper::exception($e, $defaultErrMsg);
  228. }
  229. // 保存打款结果
  230. if (!$this->saveRefundRes($order, $cloudStockAgentOrderRefund, $refundRes)) {
  231. Yii::$app->custom->logs(
  232. '取消自提修改打款状态失败',
  233. array_merge(
  234. $baseLogData,
  235. ['agent_order_refund_id' => $cloudStockAgentOrderRefund->id, 'refundRes' => $refundRes]
  236. )
  237. );
  238. $this->setError($refundRes ? '打款成功,修改打款状态失败!' : '退款失败,请稍后重试!');
  239. return false;
  240. }
  241. if (!$refundRes) {
  242. Yii::$app->custom->logs(
  243. $refundErrorMsg ?? $defaultErrMsg,
  244. array_merge(
  245. $baseLogData,
  246. ['agent_order_refund_id' => $cloudStockAgentOrderRefund->id, 'refundRes' => false]
  247. )
  248. );
  249. $this->setError('退款失败,请稍后重试!');
  250. return false;
  251. }
  252. return true;
  253. }
  254. /**
  255. * 是否可以申请退款
  256. *
  257. * @param int $orderId
  258. *
  259. * @return bool
  260. */
  261. public function isAllowApplyRefund(int $orderId): bool
  262. {
  263. $refund = CloudStockAgentOrderRefund::find()
  264. ->where([
  265. 'order_id' => $orderId,
  266. 'order_type' => PaymentTradeItemRefundEnum::ORDER_TYPE_AGENT_ORDER,
  267. 'status' => StatusEnum::ENABLED,
  268. 'mall_id' => $this->mall_id
  269. ])
  270. ->select('is_refund')
  271. ->asArray()
  272. ->one();
  273. return empty($refund) || $refund['is_refund'] == StatusEnum::DELETE;
  274. }
  275. /**
  276. * 保存打款结果
  277. *
  278. * @param CloudStockAgentOrder $order
  279. * @param CloudStockAgentOrderRefund $refund
  280. * @param bool $refundRes 结果
  281. *
  282. * @return bool
  283. */
  284. public function saveRefundRes(CloudStockAgentOrder $order, CloudStockAgentOrderRefund $refund, bool $refundRes): bool
  285. {
  286. // 保存打款结果
  287. if ($refundRes) {
  288. $refund->is_refund = StatusEnum::ENABLED;
  289. $refund->refund_at = time();
  290. $order->is_shipping_refunded = StatusEnum::ENABLED;
  291. } else {
  292. $refund->is_refund = StatusEnum::DELETE;
  293. }
  294. return $refund->save() && $order->save();
  295. }
  296. /**
  297. * 创建售后数据
  298. *
  299. * @param PaymentTrade $paymentTrade
  300. * @param CloudStockAgentOrder $order
  301. *
  302. * @return array
  303. * @throws Exception
  304. */
  305. public function createOrderRefund(PaymentTrade $paymentTrade, CloudStockAgentOrder $order): array
  306. {
  307. $t = Yii::$app->db->beginTransaction();
  308. try {
  309. // 创建退款表
  310. $cloudStockAgentOrderRefund = CloudStockAgentOrderRefund::setData([
  311. 'mall_id' => $this->mall_id,
  312. 'order_id' => $order->id,
  313. 'order_type' => PaymentTradeItemRefundEnum::ORDER_TYPE_AGENT_ORDER,
  314. 'user_id' => $order->user_id,
  315. 'order_no' => Order::generateOrderNo('csaor'),
  316. 'refund_price' => $paymentTrade->pay_fee,
  317. 'reality_refund_price' => $paymentTrade->pay_fee,
  318. ]);
  319. if (empty($cloudStockAgentOrderRefund->id)) {
  320. throw new RuntimeException('生成退款单号失败');
  321. }
  322. $mallOrder = new Order();
  323. $mallOrder->mall_id = $this->mall_id;
  324. $mallOrderRefund = new OrderRefund();
  325. $mallOrderRefund->mall_id = $this->mall_id;
  326. $mallOrderRefund->id = $cloudStockAgentOrderRefund->id;
  327. $mallOrderRefund->order_id = $cloudStockAgentOrderRefund->order_id;
  328. $mallOrderRefund->order_no = $cloudStockAgentOrderRefund->order_no;
  329. $mallOrderRefund->refund_price = $cloudStockAgentOrderRefund->refund_price;
  330. $mallOrderRefund->reality_refund_price = $cloudStockAgentOrderRefund->reality_refund_price;
  331. $paymentTrade->addRefundItem($mallOrderRefund, $mallOrder, CloudStockEnum::ADDONS_NAME);
  332. $t->commit();
  333. return compact('cloudStockAgentOrderRefund', 'mallOrder', 'mallOrderRefund');
  334. } catch (Exception $e) {
  335. $t->rollBack();
  336. throw $e;
  337. }
  338. }
  339. /**
  340. * 退款
  341. *
  342. * @param PaymentTrade $paymentTrade
  343. * @param CloudStockAgentOrder $order
  344. * @param CloudStockAgentOrderRefund $refund
  345. *
  346. * @return bool
  347. * @throws Exception
  348. */
  349. public function refund(PaymentTrade $paymentTrade, CloudStockAgentOrder $order, CloudStockAgentOrderRefund $refund, OrderRefund $mallOrderRefund): bool
  350. {
  351. $params = [
  352. 'refund_price' => $refund->reality_refund_price,
  353. ];
  354. $list = $paymentTrade->getRefundFeeList($refund->id, CloudStockEnum::ADDONS_NAME);
  355. foreach ($list as $item) {
  356. if (in_array($item->pay_type, [PayTypeEnum::BALANCE, PayTypeEnum::SCORE, PayTypeEnum::JOINPAY, PayTypeEnum::ALI, PayTypeEnum::JOINPAY_FAST_PAY, PayTypeEnum::WECHAT, PayTypeEnum::SANDPAY_FAST_PAY])) {
  357. $refund_price = $item->getRefundFee($params, $refund);
  358. // 退款金额需要大于0 组合支付金额过小的情况下 分配子订单退款金额可能为0
  359. if ($refund_price > 0) {
  360. //原路返回
  361. $res = Yii::$app->services->pay->refund(
  362. $refund->refund_price,
  363. $refund_price,
  364. $paymentTrade->out_trade_no,
  365. $order,
  366. '订单取消自提退款',
  367. CloudStockEnum::ADDONS_NAME,
  368. 'cloud_stock_cancel_pickup_refund',
  369. $item->pay_type
  370. );
  371. if ($res === false) {
  372. throw new RuntimeException('退款失败');
  373. }
  374. }
  375. } elseif ( // 当res返回为false 说明没有相对于的退款方式
  376. $res = GlobalInvoke::exec(GlobalInvoke::INVOKE_ORDER_REFUND, $this->mall_id, [
  377. 'id' => $refund->id,
  378. 'mall_id' => $refund->mall_id,
  379. 'refund' => $mallOrderRefund,
  380. 'params' => $params,
  381. 'order_no' => $order['order_no'],
  382. 'refund_order_no' => $refund['order_no'],
  383. 'payment_trade_id' => $item->getTradeId(),
  384. 'refund_price' => $item->getRefundFee($params, $refund),
  385. 'refund_source' => CloudStockEnum::ADDONS_NAME,
  386. 'pay_type' => $item->pay_type,
  387. ])
  388. ) {
  389. // 当res不为空 进此判断
  390. list($boolean, $msg) = $res;
  391. if ($boolean === false) {
  392. $this->setError($msg);
  393. return false;
  394. }
  395. }
  396. }
  397. // 设置为已退款
  398. $paymentTrade->setRefund($refund->id, CloudStockEnum::ADDONS_NAME);
  399. return true;
  400. }
  401. /**
  402. * 修改备注
  403. *
  404. * @param int $id
  405. * @param string $remark
  406. * @param bool $isSeller 是否是卖家备注
  407. * @param callable|null $callback 数据验证等
  408. *
  409. * @return bool
  410. */
  411. public function modifyRemark(int $id, string $remark, bool $isSeller = false, callable $callback = null): bool
  412. {
  413. $order = CloudStockAgentOrder::findOne([
  414. 'id' => $id,
  415. 'mall_id' => $this->mall_id,
  416. 'status' => StatusEnum::ENABLED
  417. ]);
  418. if (empty($order)) {
  419. $this->setError('订单不存在');
  420. return false;
  421. }
  422. if ($callback !== null) {
  423. try {
  424. $callback($order);
  425. } catch (Exception $e) {
  426. $this->setError($e->getMessage());
  427. return false;
  428. }
  429. }
  430. if ($isSeller) {
  431. $order->seller_remark = $remark;
  432. } else {
  433. $order->remark = $remark;
  434. }
  435. return $order->save();
  436. }
  437. /**
  438. * 修改收货地址
  439. *
  440. * @param array $params
  441. * @param bool $isCheckOnlyOnce 是否需要验证只允许修改一次
  442. * @param callable|null $callback 自定义验证函数
  443. *
  444. * @return bool
  445. */
  446. public function modifyAddress(array $params, bool $isCheckOnlyOnce = false, callable $callback = null): bool
  447. {
  448. $order = CloudStockAgentOrder::find()
  449. ->where([
  450. 'mall_id' => $this->mall_id,
  451. 'id' => $params['id'],
  452. 'send_status' => [CloudStockEnum::PICK_ORDER_STATUS0, CloudStockEnum::PICK_ORDER_STATUS3]
  453. ])
  454. ->with(['details'])
  455. ->one();
  456. if (empty($order)) {
  457. $this->setError('当前订单不允许修改收货地址');
  458. return false;
  459. }
  460. if ($callback !== null) {
  461. try {
  462. $callback($order, $params);
  463. } catch (Exception $e) {
  464. $this->setError($e->getMessage());
  465. return false;
  466. }
  467. }
  468. // 验证只允许修改一次地址
  469. if ($isCheckOnlyOnce && $order->is_delivery_address_modified) {
  470. $this->setError('当前订单已修改过收货地址');
  471. return false;
  472. }
  473. $params['is_delivery_address_modified'] = 1;
  474. if (!$order->load($params, '') || !$order->save()) {
  475. Yii::$app->custom->logs(
  476. '修改自提订单收货地址失败',
  477. ['order' => $order->toArray(), 'err_msg' => $order->getErrorMessage()]
  478. );
  479. $this->setError('修改地址失败');
  480. return false;
  481. }
  482. return true;
  483. }
  484. /**
  485. * 验证新地址的运费
  486. *
  487. * @param CloudStockAgentOrder $order
  488. * @param int $addressId
  489. *
  490. * @return array
  491. */
  492. public function checkNewAddressFreight(CloudStockAgentOrder $order, int $addressId): array
  493. {
  494. $goodsService = new CloudStockAgentGoodsService(['mall_id' => $this->mall_id]);
  495. $form = $this->getForm($goodsService, $this->mall_id, $order->user_id, $addressId, $order['details']);
  496. if (!empty(\Yii::$app->services->setting->addons->get($this->mall_id, AddonsEnum::ADDONS_NAME_CLOUD_STOCK, 'basic.is_enable_freight_charges'))) {
  497. if (empty($order['details'])) {
  498. throw new RuntimeException('当前订单不允许修改收货地址');
  499. }
  500. try {
  501. if (bccomp($this->getFreight($goodsService, $this->mall_id, $order->user_id, $addressId, $order['details'], $form), $order->pay_money, 2) > 0) {
  502. throw new RuntimeException('当前地址运费超过原地址运费,不允许修改');
  503. }
  504. } catch (Exception $e) {
  505. throw new RuntimeException($e instanceof RuntimeException ? $e->getMessage() : '修改收货地址失败,请稍后重试!');
  506. }
  507. }
  508. $groupOrderGoods = reset($form['group_order_goods'])['mch_order_info'] ?? [];
  509. if (empty($groupOrderGoods)) {
  510. throw new RuntimeException('修改收货地址失败,请稍后重试!');
  511. }
  512. return [
  513. 'name' => $groupOrderGoods['receiver_name'],
  514. 'mobile' => $groupOrderGoods['mobile'],
  515. 'address' => str_replace(' ', '', $groupOrderGoods['region_name']) . $groupOrderGoods['address'],
  516. 'province_id' => $groupOrderGoods['province'],
  517. 'city_id' => $groupOrderGoods['city'],
  518. 'district_id' => $groupOrderGoods['area'],
  519. ];
  520. }
  521. }