Checkout.php 32 KB

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