Checkout.php 34 KB

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