Checkout.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2017~2024 https://www.yiovo.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
  8. // +----------------------------------------------------------------------
  9. // | Author: 萤火科技 <admin@yiovo.com>
  10. // +----------------------------------------------------------------------
  11. declare (strict_types=1);
  12. namespace app\api\service\order;
  13. use app\api\model\{
  14. User as UserModel,
  15. Goods as GoodsModel,
  16. Order as OrderModel,
  17. Setting as SettingModel,
  18. UserCoupon as UserCouponModel,
  19. };
  20. use app\api\service\{
  21. User as UserService,
  22. user\Grade as UserGradeService,
  23. coupon\GoodsDeduct as GoodsDeductService,
  24. order\PaySuccess as OrderPaySuccesService,
  25. points\GoodsDeduct as PointsDeductService,
  26. order\source\checkout\Factory as CheckoutFactory,
  27. };
  28. use app\common\enum\{
  29. Setting as SettingEnum,
  30. DiscountType as DiscountTypeEnum,
  31. payment\Method as PaymentMethodEnum,
  32. order\OrderType as OrderTypeEnum,
  33. order\OrderStatus as OrderStatusEnum,
  34. order\OrderSource as OrderSourceEnum,
  35. order\DeliveryType as DeliveryTypeEnum,
  36. };
  37. use app\common\service\{
  38. BaseService,
  39. delivery\Express as ExpressService,
  40. goods\source\Factory as StockFactory
  41. };
  42. use app\common\library\helper;
  43. use cores\exception\BaseException;
  44. /**
  45. * 订单结算台服务类
  46. * Class Checkout
  47. * @package app\api\service\order
  48. */
  49. class Checkout extends BaseService
  50. {
  51. /* $model OrderModel 订单模型 */
  52. public OrderModel $model;
  53. /* @var UserModel $user 当前用户信息 */
  54. private $user;
  55. // 订单结算商品列表
  56. private $goodsList;
  57. /**
  58. * 订单结算api参数
  59. * @var array
  60. */
  61. private array $param = [
  62. 'delivery' => null, // 配送方式
  63. 'couponId' => 0, // 用户的优惠券ID
  64. 'isUsePoints' => 0, // 是否使用积分抵扣
  65. 'remark' => '', // 买家留言
  66. ];
  67. /**
  68. * 订单结算的规则
  69. * @var array
  70. */
  71. private array $checkoutRule = [
  72. 'isUserGrade' => true, // 会员等级折扣
  73. 'isCoupon' => true, // 优惠券折扣
  74. 'isUsePoints' => true, // 积分抵扣
  75. 'isUseFullDiscount' => true, // 满额立减
  76. 'isUseFullFree' => true, // 满额包邮
  77. 'isDealer' => true, // 开启分销
  78. ];
  79. /**
  80. * 订单来源
  81. * @var array
  82. */
  83. private array $orderSource = [
  84. 'source' => OrderSourceEnum::MAIN,
  85. 'sourceId' => 0,
  86. 'sourceData' => [],
  87. ];
  88. /**
  89. * 订单结算数据
  90. * @var array
  91. */
  92. private array $orderData = [];
  93. // 是否已支付 (仅积分商品兑换时)
  94. private bool $isPaySuccess = false;
  95. /**
  96. * 构造函数
  97. * Checkout constructor.
  98. * @throws BaseException
  99. */
  100. public function __construct()
  101. {
  102. parent::__construct();
  103. $this->user = UserService::getCurrentLoginUser(true);
  104. $this->model = new OrderModel;
  105. $this->storeId = $this->getStoreId();
  106. }
  107. /**
  108. * 设置结算台请求的参数
  109. * @param $param
  110. * @return array
  111. */
  112. public function setParam($param): array
  113. {
  114. $this->param = array_merge($this->param, $param);
  115. return $this->getParam();
  116. }
  117. /**
  118. * 获取结算台请求的参数
  119. * @return array
  120. */
  121. public function getParam(): array
  122. {
  123. return $this->param;
  124. }
  125. /**
  126. * 订单结算的规则
  127. * @param $data
  128. * @return $this
  129. */
  130. public function setCheckoutRule($data): Checkout
  131. {
  132. $this->checkoutRule = \array_merge($this->checkoutRule, $data);
  133. return $this;
  134. }
  135. /**
  136. * 设置订单来源(普通订单)
  137. * @param $data
  138. * @return $this
  139. */
  140. public function setOrderSource($data): Checkout
  141. {
  142. $this->orderSource = array_merge($this->orderSource, $data);
  143. return $this;
  144. }
  145. /**
  146. * 订单确认-结算台
  147. * @param $goodsList
  148. * @return array
  149. * @throws BaseException
  150. * @throws \think\db\exception\DataNotFoundException
  151. * @throws \think\db\exception\DbException
  152. * @throws \think\db\exception\ModelNotFoundException
  153. */
  154. public function onCheckout($goodsList): array
  155. {
  156. // 订单确认-立即购买
  157. $this->goodsList = $goodsList;
  158. return $this->checkout();
  159. }
  160. /**
  161. * 订单结算台
  162. * @return array
  163. * @throws BaseException
  164. * @throws \think\db\exception\DataNotFoundException
  165. * @throws \think\db\exception\DbException
  166. * @throws \think\db\exception\ModelNotFoundException
  167. */
  168. private function checkout(): array
  169. {
  170. // 整理订单数据
  171. $this->orderData = $this->getOrderData();
  172. // 验证商品状态, 是否允许购买
  173. $this->validateGoodsList();
  174. // 订单商品总数量
  175. $orderTotalNum = (int)helper::getArrayColumnSum($this->goodsList, 'total_num');
  176. // 设置订单商品会员折扣价
  177. $this->setOrderGoodsGradeMoney();
  178. // 设置订单商品总金额(不含优惠折扣)
  179. $this->setOrderTotalPrice();
  180. // 当前用户可用的优惠券列表
  181. $couponList = $this->getCouponListByOrder((float)$this->orderData['orderTotalPrice']);
  182. // 计算优惠券抵扣
  183. $this->setOrderCouponMoney($couponList, (int)$this->param['couponId']);
  184. // 计算可用积分抵扣
  185. $this->setOrderPoints();
  186. // 计算订单商品的实际付款金额
  187. $this->setOrderGoodsPayPrice();
  188. // 设置订单配送信息
  189. $this->setDelivery();
  190. // 计算订单最终金额
  191. $this->setOrderPayPrice();
  192. // 计算订单积分赠送数量
  193. $this->setOrderPointsBonus();
  194. // 返回订单数据
  195. return \array_merge([
  196. 'goodsList' => $this->goodsList, // 商品信息
  197. 'orderTotalNum' => $orderTotalNum, // 商品总数量
  198. 'couponList' => \array_values($couponList), // 优惠券列表
  199. 'hasError' => $this->hasError(),
  200. 'errorMsg' => $this->getError(),
  201. ], $this->orderData);
  202. }
  203. /**
  204. * 设置订单配送信息
  205. * @throws \think\db\exception\DataNotFoundException
  206. * @throws \think\db\exception\DbException
  207. * @throws \think\db\exception\ModelNotFoundException
  208. */
  209. private function setDelivery()
  210. {
  211. // 设置默认配送方式
  212. if (!$this->param['delivery']) {
  213. $deliveryType = SettingModel::getItem(SettingEnum::DELIVERY)['delivery_type'];
  214. $this->param['delivery'] = current($deliveryType);
  215. }
  216. // 处理配送方式
  217. if ($this->param['delivery'] == DeliveryTypeEnum::EXPRESS) {
  218. $this->setOrderExpress();
  219. }
  220. // 判断商品是否支持配送
  221. $deliveryName = DeliveryTypeEnum::data()[$this->param['delivery']]['name'];
  222. foreach ($this->goodsList as $goods) {
  223. if ($goods['is_ind_delivery_type'] && !in_array($this->param['delivery'], $goods['delivery_type'])) {
  224. $this->setError("很抱歉,商品 [{$goods['goods_name']}] 不支持{$deliveryName}");
  225. }
  226. }
  227. }
  228. /**
  229. * 计算订单可用积分抵扣
  230. * @return void
  231. * @throws \think\db\exception\DataNotFoundException
  232. * @throws \think\db\exception\DbException
  233. * @throws \think\db\exception\ModelNotFoundException
  234. */
  235. private function setOrderPoints(): void
  236. {
  237. // 设置默认的商品积分抵扣信息
  238. $this->setDefaultGoodsPoints();
  239. // 积分设置
  240. $setting = SettingModel::getItem('points');
  241. // 条件:后台开启下单使用积分抵扣
  242. if (
  243. !$this->checkoutRule['isUsePoints']
  244. || !$setting['is_shopping_discount']
  245. ) {
  246. return;
  247. }
  248. // 条件:订单金额满足[?]元
  249. if (helper::bccomp($setting['discount']['full_order_price'], $this->orderData['orderTotalPrice']) === 1) {
  250. return;
  251. }
  252. // 计算订单商品最多可抵扣的积分数量
  253. $this->setOrderGoodsMaxPointsNum();
  254. // 订单最多可抵扣的积分总数量
  255. $maxPointsNumCount = (int)helper::getArrayColumnSum($this->goodsList, 'max_points_num');
  256. // 实际可抵扣的积分数量
  257. $actualPointsNum = min($maxPointsNumCount, $this->user['points']);
  258. if ($actualPointsNum < 1) {
  259. return;
  260. }
  261. // 计算订单商品实际抵扣的积分数量和金额
  262. $GoodsDeduct = new PointsDeductService($this->goodsList);
  263. $GoodsDeduct->setGoodsPoints($maxPointsNumCount, $actualPointsNum);
  264. // 积分抵扣总金额
  265. $orderPointsMoney = helper::getArrayColumnSum($this->goodsList, 'points_money');
  266. $this->orderData['pointsMoney'] = helper::number2($orderPointsMoney);
  267. // 积分抵扣总数量
  268. $this->orderData['pointsNum'] = $actualPointsNum;
  269. // 允许积分抵扣
  270. $this->orderData['isAllowPoints'] = true;
  271. }
  272. /**
  273. * 计算订单商品最多可抵扣的积分数量
  274. * @return void
  275. * @throws \think\db\exception\DataNotFoundException
  276. * @throws \think\db\exception\DbException
  277. * @throws \think\db\exception\ModelNotFoundException
  278. */
  279. private function setOrderGoodsMaxPointsNum(): void
  280. {
  281. // 积分设置
  282. $setting = SettingModel::getItem('points');
  283. foreach ($this->goodsList as &$goods) {
  284. // 商品不允许积分抵扣
  285. if (!$goods['is_points_discount']) continue;
  286. // 积分抵扣比例
  287. $deductionRatio = helper::bcdiv($setting['discount']['max_money_ratio'], 100);
  288. // 最多可抵扣的金额
  289. $totalPayPrice = helper::bcsubs([$goods['total_price'], $goods['coupon_money']]);
  290. $maxPointsMoney = helper::bcmul($totalPayPrice, $deductionRatio);
  291. // 最多可抵扣的积分数量
  292. $goods['max_points_num'] = helper::bcdiv($maxPointsMoney, $setting['discount']['discount_ratio'], 0);
  293. }
  294. }
  295. /**
  296. * 设置默认的商品积分抵扣信息
  297. * @return void
  298. */
  299. private function setDefaultGoodsPoints(): void
  300. {
  301. foreach ($this->goodsList as &$goods) {
  302. // 最多可抵扣的积分数量
  303. $goods['max_points_num'] = 0;
  304. // 实际抵扣的积分数量
  305. $goods['points_num'] = 0;
  306. // 实际抵扣的金额
  307. $goods['points_money'] = 0;
  308. }
  309. }
  310. /**
  311. * 整理订单数据(结算台初始化)
  312. * @return array
  313. * @throws \think\db\exception\DataNotFoundException
  314. * @throws \think\db\exception\DbException
  315. * @throws \think\db\exception\ModelNotFoundException
  316. */
  317. private function getOrderData(): array
  318. {
  319. return [
  320. // 当前订单类型
  321. 'orderType' => $this->getOrderType(),
  322. // 当前配送方式
  323. 'delivery' => $this->getDeliveryType(),
  324. // 默认地址
  325. 'address' => $this->user['address_default'],
  326. // 是否存在收货地址
  327. 'existAddress' => $this->user['address_id'] > 0,
  328. // 配送费用
  329. 'expressPrice' => '0.00',
  330. // 当前用户收货城市是否存在配送规则中
  331. 'isIntraRegion' => true,
  332. // 是否允许使用积分抵扣
  333. 'isAllowPoints' => false,
  334. // 是否使用积分抵扣
  335. 'isUsePoints' => (bool)$this->param['isUsePoints'],
  336. // 积分抵扣金额
  337. 'pointsMoney' => '0.00',
  338. // 赠送的积分数量
  339. 'pointsBonus' => 0,
  340. ];
  341. }
  342. /**
  343. * 获取当前配送方式
  344. * @return int|mixed
  345. * @throws \think\db\exception\DataNotFoundException
  346. * @throws \think\db\exception\DbException
  347. * @throws \think\db\exception\ModelNotFoundException
  348. */
  349. private function getDeliveryType()
  350. {
  351. // 系统默认的配送方式
  352. $setting = SettingModel::getItem(SettingEnum::DELIVERY)['delivery_type'];
  353. // 配送方式:实物订单
  354. if ($this->getOrderType() == OrderTypeEnum::PHYSICAL) {
  355. return $this->param['delivery'] > 0 ? $this->param['delivery'] : $setting[0];
  356. }
  357. // 配送方式:无需配送
  358. return DeliveryTypeEnum::NOTHING;
  359. }
  360. /**
  361. * 获取订单类型
  362. * @return mixed
  363. */
  364. private function getOrderType()
  365. {
  366. return $this->goodsList[0]['goods_type'];
  367. }
  368. /**
  369. * 获取订单页面中使用到的系统设置
  370. * @return array
  371. * @throws \think\db\exception\DataNotFoundException
  372. * @throws \think\db\exception\DbException
  373. * @throws \think\db\exception\ModelNotFoundException
  374. */
  375. public function getSetting(): array
  376. {
  377. // 系统支持的配送方式 (后台设置)
  378. $deliveryType = SettingModel::getItem(SettingEnum::DELIVERY)['delivery_type'];
  379. // 积分设置
  380. $pointsSetting = SettingModel::getItem(SettingEnum::POINTS);
  381. return [
  382. 'deliveryType' => $deliveryType, // 支持的配送方式
  383. 'points_name' => $pointsSetting['points_name'], // 积分名称
  384. 'points_describe' => $pointsSetting['describe'], // 积分说明
  385. ];
  386. }
  387. /**
  388. * 获取订单结算时的个人信息
  389. * @return array
  390. */
  391. public function getPersonal(): array
  392. {
  393. return [
  394. 'user_id' => $this->user['user_id'],
  395. 'balance' => $this->user['balance'],
  396. 'points' => $this->user['points'],
  397. 'address_id' => $this->user['address_id'],
  398. ];
  399. }
  400. /**
  401. * 获取订单是否已支付成功
  402. * @return bool
  403. */
  404. public function isPaySuccess(): bool
  405. {
  406. return $this->isPaySuccess;
  407. }
  408. /**
  409. * 当前用户可用的优惠券列表
  410. * @param float $orderTotalPrice 订单商品总金额
  411. * @return array
  412. * @throws \think\db\exception\DbException
  413. */
  414. private function getCouponListByOrder(float $orderTotalPrice): array
  415. {
  416. // 是否开启优惠券折扣
  417. if (!$this->checkoutRule['isCoupon']) {
  418. return [];
  419. }
  420. // 整理当前订单所有商品ID集
  421. $orderGoodsIds = helper::getArrayColumn($this->goodsList, 'goods_id');
  422. // 当前用户可用的优惠券列表
  423. $couponList = UserCouponModel::getListByOrder($this->user['user_id'], $orderTotalPrice);
  424. // 判断当前优惠券是否满足订单使用条件 (优惠券适用范围)
  425. return UserCouponModel::couponListApplyRange($couponList, $orderGoodsIds);
  426. }
  427. /**
  428. * 验证订单商品的状态
  429. * @return void
  430. */
  431. private function validateGoodsList(): void
  432. {
  433. $Checkout = CheckoutFactory::getFactory(
  434. $this->user,
  435. $this->goodsList,
  436. $this->orderSource['source']
  437. );
  438. if (!$Checkout->validateGoodsList()) {
  439. $this->setError($Checkout->getError());
  440. }
  441. }
  442. /**
  443. * 设置订单的商品总金额(不含优惠折扣)
  444. */
  445. private function setOrderTotalPrice()
  446. {
  447. // 订单商品的总金额(不含优惠券折扣)
  448. $this->orderData['orderTotalPrice'] = helper::number2(helper::getArrayColumnSum($this->goodsList, 'total_price'));
  449. }
  450. /**
  451. * 设置订单的实际支付金额(含配送费)
  452. */
  453. private function setOrderPayPrice()
  454. {
  455. // 订单金额(含优惠折扣)
  456. $this->orderData['orderPrice'] = helper::number2(helper::getArrayColumnSum($this->goodsList, 'total_pay_price'));
  457. // 订单实付款金额(订单金额 + 运费)
  458. $this->orderData['orderPayPrice'] = helper::number2(helper::bcadd($this->orderData['orderPrice'], $this->orderData['expressPrice']));
  459. }
  460. /**
  461. * 计算订单积分赠送数量
  462. * @return void
  463. * @throws \think\db\exception\DataNotFoundException
  464. * @throws \think\db\exception\DbException
  465. * @throws \think\db\exception\ModelNotFoundException
  466. */
  467. private function setOrderPointsBonus(): void
  468. {
  469. // 初始化商品积分赠送数量
  470. foreach ($this->goodsList as &$goods) {
  471. $goods['points_bonus'] = 0;
  472. }
  473. // 积分设置
  474. $setting = SettingModel::getItem('points');
  475. // 条件:后台开启开启购物送积分
  476. if (!$setting['is_shopping_gift']) {
  477. return;
  478. }
  479. // 设置商品积分赠送数量
  480. foreach ($this->goodsList as &$goods) {
  481. // 积分赠送比例
  482. $ratio = helper::bcdiv($setting['gift_ratio'], 100);
  483. // 计算抵扣积分数量
  484. $goods['points_bonus'] = !$goods['is_points_gift'] ? 0 : helper::bcmul($goods['total_pay_price'], $ratio, 0);
  485. }
  486. // 订单积分赠送数量
  487. $this->orderData['pointsBonus'] = (int)helper::getArrayColumnSum($this->goodsList, 'points_bonus');
  488. }
  489. /**
  490. * 计算订单商品的实际付款金额
  491. * @return void
  492. */
  493. private function setOrderGoodsPayPrice(): void
  494. {
  495. // 商品总价 - 优惠抵扣
  496. foreach ($this->goodsList as &$goods) {
  497. // 减去优惠券抵扣金额
  498. $value = helper::bcsubs([$goods['total_price'], $goods['coupon_money']]);
  499. // 减去积分抵扣金额
  500. if ($this->orderData['isAllowPoints'] && $this->orderData['isUsePoints']) {
  501. $value = helper::bcsub($value, $goods['points_money']);
  502. }
  503. // 订单商品的实际付款金额
  504. $goods['total_pay_price'] = helper::number2($value);
  505. }
  506. }
  507. /**
  508. * 设置订单商品会员折扣价
  509. * @return void
  510. * @throws BaseException
  511. * @throws \think\db\exception\DataNotFoundException
  512. * @throws \think\db\exception\DbException
  513. * @throws \think\db\exception\ModelNotFoundException
  514. */
  515. private function setOrderGoodsGradeMoney(): void
  516. {
  517. // 设置默认数据
  518. helper::setDataAttribute($this->goodsList, [
  519. // 标记参与会员折扣
  520. 'is_user_grade' => false,
  521. // 会员等级抵扣的金额
  522. 'grade_ratio' => 0,
  523. // 会员折扣的商品单价
  524. 'grade_goods_price' => 0,
  525. // 会员折扣的总额差
  526. 'grade_total_money' => 0,
  527. ], true);
  528. // 是否开启会员等级折扣
  529. if (!$this->checkoutRule['isUserGrade']) {
  530. return;
  531. }
  532. // 获取当前登录用户的会员等级信息
  533. $gradeInfo = UserGradeService::getCurrentGradeInfo();
  534. // 判断商品是否参与会员折扣
  535. if (empty($gradeInfo)) {
  536. return;
  537. }
  538. // 计算抵扣金额
  539. foreach ($this->goodsList as &$goods) {
  540. // 判断商品是否参与会员折扣
  541. if (!$goods['is_enable_grade']) {
  542. continue;
  543. }
  544. // 折扣比例
  545. $discountRatio = $gradeInfo['equity']['discount'];
  546. // 商品单独设置了会员折扣
  547. if ($goods['is_alone_grade'] && isset($goods['alone_grade_equity'][$this->user['grade_id']])) {
  548. $discountRatio = $goods['alone_grade_equity'][$gradeInfo['grade_id']];
  549. }
  550. if (empty($discountRatio)) {
  551. continue;
  552. }
  553. // 会员折扣后的商品总金额
  554. $gradeTotalPrice = UserGradeService::getDiscountPrice($goods['total_price'], $discountRatio);
  555. helper::setDataAttribute($goods, [
  556. 'is_user_grade' => true,
  557. 'grade_ratio' => $discountRatio,
  558. 'grade_goods_price' => UserGradeService::getDiscountPrice($goods['goods_price'], $discountRatio),
  559. 'grade_total_money' => helper::bcsub($goods['total_price'], $gradeTotalPrice),
  560. 'total_price' => $gradeTotalPrice,
  561. ], false);
  562. }
  563. }
  564. /**
  565. * 设置订单优惠券抵扣信息
  566. * @param array $couponList 当前用户可用的优惠券列表
  567. * @param int $userCouponId 当前选择的优惠券ID
  568. * @return void
  569. * @throws BaseException
  570. */
  571. private function setOrderCouponMoney(array $couponList, int $userCouponId): void
  572. {
  573. // 设置默认数据:订单信息
  574. helper::setDataAttribute($this->orderData, [
  575. 'couponId' => 0, // 用户的优惠券ID
  576. 'couponMoney' => 0, // 优惠券抵扣金额
  577. ], false);
  578. // 设置默认数据:订单商品列表
  579. helper::setDataAttribute($this->goodsList, [
  580. 'coupon_money' => 0, // 优惠券抵扣金额
  581. ], true);
  582. // 验证选择的优惠券ID是否合法
  583. if (!$this->verifyOrderCouponId($userCouponId, $couponList)) {
  584. return;
  585. }
  586. // 获取优惠券信息
  587. $couponInfo = $this->getCouponInfo($userCouponId, $couponList);
  588. // 商品列表信息格式化
  589. $goodsListTemp = helper::getArrayColumns($this->goodsList, ['goods_id', 'goods_sku_id', 'total_price']);
  590. // 计算订单商品优惠券抵扣金额
  591. $CouponMoney = new GoodsDeductService;
  592. $rangeGoodsList = $CouponMoney->setGoodsList($goodsListTemp)
  593. ->setCouponInfo($couponInfo)
  594. ->setGoodsCouponMoney()
  595. ->getRangeGoodsList();
  596. // 分配订单商品优惠券抵扣金额
  597. foreach ($this->goodsList as &$goods) {
  598. $goodsKey = "{$goods['goods_id']}-{$goods['goods_sku_id']}";
  599. if (isset($rangeGoodsList[$goodsKey])) {
  600. $goods['coupon_money'] = helper::bcdiv($rangeGoodsList[$goodsKey]['coupon_money'], 100);
  601. }
  602. }
  603. // 记录订单优惠券信息
  604. $this->orderData['couponId'] = $userCouponId;
  605. $this->orderData['couponMoney'] = helper::number2(helper::bcdiv($CouponMoney->getActualReducedMoney(), 100));
  606. }
  607. /**
  608. * 验证用户选择的优惠券ID是否合法
  609. * @param int $userCouponId
  610. * @param $couponList
  611. * @return bool
  612. * @throws BaseException
  613. */
  614. private function verifyOrderCouponId(int $userCouponId, $couponList): bool
  615. {
  616. // 是否开启优惠券折扣
  617. if (!$this->checkoutRule['isCoupon']) {
  618. return false;
  619. }
  620. // 如果没有可用的优惠券,直接返回
  621. if ($userCouponId <= 0 || empty($couponList)) {
  622. return false;
  623. }
  624. // 判断优惠券是否存在
  625. $couponInfo = $this->getCouponInfo($userCouponId, $couponList);
  626. if (!$couponInfo) {
  627. throwError('未找到优惠券信息');
  628. }
  629. // 判断优惠券适用范围是否合法
  630. if (!$couponInfo['is_apply']) {
  631. throwError($couponInfo['not_apply_info']);
  632. }
  633. return true;
  634. }
  635. /**
  636. * 查找指定的优惠券信息
  637. * @param int $userCouponId 用户优惠券ID
  638. * @param array $couponList 优惠券列表
  639. * @return false|mixed
  640. */
  641. private function getCouponInfo(int $userCouponId, array $couponList)
  642. {
  643. return helper::getArrayItemByColumn($couponList, 'user_coupon_id', $userCouponId);
  644. }
  645. /**
  646. * 订单配送-快递配送
  647. * @return void
  648. * @throws \think\db\exception\DataNotFoundException
  649. * @throws \think\db\exception\DbException
  650. * @throws \think\db\exception\ModelNotFoundException
  651. */
  652. private function setOrderExpress(): void
  653. {
  654. // 设置默认数据:配送费用
  655. helper::setDataAttribute($this->goodsList, [
  656. 'expressPrice' => 0,
  657. ], true);
  658. // 当前用户收货城市ID
  659. $cityId = $this->user['address_default'] ? (int)$this->user['address_default']['city_id'] : 0;
  660. // 初始化配送服务类
  661. $ExpressService = new ExpressService($cityId, $this->goodsList);
  662. // 验证商品是否在配送范围
  663. $isIntraRegion = $ExpressService->isIntraRegion();
  664. if ($cityId > 0 && !$isIntraRegion) {
  665. $notInRuleGoodsName = $ExpressService->getNotInRuleGoodsName();
  666. $this->setError("很抱歉,您的收货地址不在商品 [{$notInRuleGoodsName}] 的配送范围内");
  667. }
  668. // 订单总运费金额
  669. $this->orderData['isIntraRegion'] = $isIntraRegion;
  670. $this->orderData['expressPrice'] = $ExpressService->getDeliveryFee(true);
  671. }
  672. /**
  673. * 创建新订单
  674. * @param array $order 订单信息
  675. * @return bool
  676. */
  677. public function createOrder(array $order): bool
  678. {
  679. // 表单验证
  680. if (!$this->validateOrderForm($order)) {
  681. return false;
  682. }
  683. // 创建新的订单
  684. return $this->model->transaction(function () use ($order) {
  685. return $this->createOrderEvent($order);
  686. });
  687. }
  688. /**
  689. * 创建订单事件
  690. * @param $order
  691. * @return bool
  692. * @throws BaseException
  693. * @throws \think\db\exception\DataNotFoundException
  694. * @throws \think\db\exception\DbException
  695. * @throws \think\db\exception\ModelNotFoundException
  696. */
  697. private function createOrderEvent($order): bool
  698. {
  699. // 新增订单记录
  700. $this->add($order, $this->param['remark']);
  701. // 保存订单数据 (根据订单类型)
  702. $order['orderType'] == OrderTypeEnum::PHYSICAL && $this->saveOrderByPhysical($order);
  703. // 保存订单商品信息
  704. $this->saveOrderGoods($order);
  705. // 更新商品库存 (针对下单减库存的商品)
  706. $this->updateGoodsStockNum($order);
  707. // 设置优惠券使用状态
  708. $order['couponId'] > 0 && UserCouponModel::setIsUse((int)$order['couponId']);
  709. // 积分抵扣: 扣除用户可用积分
  710. if ($order['isAllowPoints'] && $order['isUsePoints'] && $order['pointsNum'] > 0) {
  711. $describe = "订单抵扣:{$this->model['order_no']}";
  712. UserModel::setIncPoints($this->user['user_id'], -$order['pointsNum'], $describe);
  713. }
  714. // 获取订单详情
  715. $detail = OrderModel::getDetail((int)$this->model['order_id'], ['goods']);
  716. return true;
  717. }
  718. /**
  719. * 保存订单数据 (实物商品)
  720. * @param $order
  721. * @return void
  722. */
  723. private function saveOrderByPhysical($order)
  724. {
  725. if ($order['delivery'] == DeliveryTypeEnum::EXPRESS) {
  726. // 记录收货地址
  727. $this->saveOrderAddress($order['address']);
  728. }
  729. }
  730. /**
  731. * 表单验证 (订单提交)
  732. * @param array $order 订单信息
  733. * @return bool
  734. */
  735. private function validateOrderForm(array $order): bool
  736. {
  737. if ($order['orderType'] == OrderTypeEnum::PHYSICAL) {
  738. return $this->validateOrderFormByPhysical($order);
  739. }
  740. return true;
  741. }
  742. /**
  743. * 表单验证:实物订单
  744. * @param array $order
  745. * @return bool
  746. */
  747. private function validateOrderFormByPhysical(array $order): bool
  748. {
  749. if ($order['delivery'] == DeliveryTypeEnum::EXPRESS) {
  750. if (empty($order['address'])) {
  751. $this->error = '您还没有选择配送地址';
  752. return false;
  753. }
  754. }
  755. return true;
  756. }
  757. /**
  758. * 当前订单是否存在和使用积分抵扣
  759. * @param $order
  760. * @return bool
  761. */
  762. private function isExistPointsDeduction($order): bool
  763. {
  764. return $order['isAllowPoints'] && $order['isUsePoints'];
  765. }
  766. /**
  767. * 新增订单记录
  768. * @param $order
  769. * @param string $remark
  770. * @return void
  771. */
  772. private function add($order, string $remark = ''): void
  773. {
  774. // 当前订单是否存在和使用积分抵扣
  775. $isExistPointsDeduction = $this->isExistPointsDeduction($order);
  776. // 订单数据
  777. $data = [
  778. 'user_id' => $this->user['user_id'],
  779. 'order_no' => $this->model->orderNo(),
  780. 'order_type' => $order['orderType'],
  781. 'total_price' => $order['orderTotalPrice'],
  782. 'order_price' => $order['orderPrice'],
  783. 'coupon_id' => $order['couponId'],
  784. 'coupon_money' => $order['couponMoney'],
  785. 'points_money' => $isExistPointsDeduction ? $order['pointsMoney'] : 0,
  786. 'points_num' => $isExistPointsDeduction ? $order['pointsNum'] : 0,
  787. 'pay_price' => $order['orderPayPrice'],
  788. 'delivery_type' => $order['delivery'],
  789. 'buyer_remark' => trim($remark),
  790. 'order_source' => $this->orderSource['source'],
  791. 'order_source_id' => $this->orderSource['sourceId'],
  792. 'order_source_data' => $this->orderSource['sourceData'],
  793. 'points_bonus' => $order['pointsBonus'],
  794. 'order_status' => OrderStatusEnum::NORMAL,
  795. 'platform' => \getPlatform(),
  796. 'store_id' => $this->storeId,
  797. ];
  798. if ($order['delivery'] == DeliveryTypeEnum::EXPRESS) {
  799. $data['express_price'] = $order['expressPrice'];
  800. }
  801. // 保存订单记录
  802. $this->model->save($data);
  803. }
  804. /**
  805. * 保存订单商品信息
  806. * @param $order
  807. * @return void
  808. */
  809. private function saveOrderGoods($order): void
  810. {
  811. // 当前订单是否存在和使用积分抵扣
  812. $isExistPointsDeduction = $this->isExistPointsDeduction($order);
  813. // 订单商品列表
  814. $goodsList = [];
  815. foreach ($order['goodsList'] as $goods) {
  816. /* @var GoodsModel $goods */
  817. $item = [
  818. 'user_id' => $this->user['user_id'],
  819. 'store_id' => $this->storeId,
  820. 'goods_id' => $goods['goods_id'],
  821. 'goods_type' => $goods['goods_type'],
  822. 'goods_name' => $goods['goods_name'],
  823. 'goods_no' => $goods['goods_no'] ?: '',
  824. 'image_id' => $this->getGoodsImageId($goods),
  825. 'deduct_stock_type' => $goods['deduct_stock_type'],
  826. 'spec_type' => $goods['spec_type'],
  827. 'goods_sku_id' => $goods['skuInfo']['goods_sku_id'],
  828. 'goods_props' => $goods['skuInfo']['goods_props'] ?: '',
  829. 'content' => $goods['content'] ?? '',
  830. 'goods_sku_no' => $goods['skuInfo']['goods_sku_no'] ?: '',
  831. 'goods_price' => $goods['skuInfo']['goods_price'],
  832. 'line_price' => $goods['skuInfo']['line_price'],
  833. 'goods_weight' => $goods['skuInfo']['goods_weight'],
  834. 'is_user_grade' => (int)$goods['is_user_grade'],
  835. 'grade_ratio' => $goods['grade_ratio'],
  836. 'grade_goods_price' => $goods['grade_goods_price'],
  837. 'grade_total_money' => $goods['grade_total_money'],
  838. 'coupon_money' => $goods['coupon_money'],
  839. 'points_money' => $isExistPointsDeduction ? $goods['points_money'] : 0,
  840. 'points_num' => $isExistPointsDeduction ? $goods['points_num'] : 0,
  841. 'points_bonus' => $goods['points_bonus'],
  842. 'total_num' => $goods['total_num'],
  843. 'total_price' => $goods['total_price'],
  844. 'total_pay_price' => $goods['total_pay_price'],
  845. ];
  846. // 记录订单商品来源ID
  847. $item['goods_source_id'] = isset($goods['goods_source_id']) ? $goods['goods_source_id'] : 0;
  848. $goodsList[] = $item;
  849. }
  850. $this->model->goods()->saveAll($goodsList) !== false;
  851. }
  852. /**
  853. * 获取订单商品的封面图(优先sku封面图)
  854. * @param $goods
  855. * @return int
  856. */
  857. private function getGoodsImageId($goods): int
  858. {
  859. return $goods['skuInfo']['image_id'] ?: (int)current($goods['goods_images'])['file_id'];
  860. }
  861. /**
  862. * 更新商品库存 (针对下单减库存的商品)
  863. * @param $order
  864. * @return void
  865. */
  866. private function updateGoodsStockNum($order): void
  867. {
  868. StockFactory::getFactory($this->model['order_source'])->updateGoodsStock($order['goodsList']);
  869. }
  870. /**
  871. * 记录收货地址
  872. * @param $address
  873. * @return void
  874. */
  875. private function saveOrderAddress($address): void
  876. {
  877. $this->model->address()->save([
  878. 'user_id' => $this->user['user_id'],
  879. 'store_id' => $this->storeId,
  880. 'name' => $address['name'],
  881. 'phone' => $address['phone'],
  882. 'province_id' => $address['province_id'],
  883. 'city_id' => $address['city_id'],
  884. 'region_id' => $address['region_id'],
  885. 'detail' => $address['detail'],
  886. ]);
  887. }
  888. }