OrderService.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. <?php
  2. namespace app\common\Service;
  3. use app\common\Enum\GoodsEnum;
  4. use app\common\model\Order;
  5. use app\common\model\OrderGoods;
  6. use app\common\model\OrderAction;
  7. use app\common\model\Carts;
  8. use app\common\model\Address;
  9. use app\common\model\UserCoupon;
  10. use app\common\model\Goods;
  11. use app\common\model\Sku;
  12. use app\common\model\Freight;
  13. use app\common\model\Coupon;
  14. use think\Db;
  15. use think\Exception;
  16. /**
  17. * 订单服务类
  18. * 封装订单创建相关逻辑
  19. */
  20. class OrderService
  21. {
  22. /**
  23. * 统一的创建订单方法
  24. * @param int $address_id 地址ID
  25. * @param int $user_id 用户ID
  26. * @param array $goods_list 标准化的商品列表
  27. * @param int $user_coupon_id 优惠券ID
  28. * @param string $memo 备注
  29. * @param array $cart_ids 购物车ID数组(如果是购物车模式需要清空)
  30. * @return Order
  31. * @throws Exception
  32. */
  33. public static function createOrder($address_id, $user_id, $goods_list, $user_coupon_id = 0, $memo = '', $cart_ids = [])
  34. {
  35. $address = Address::get($address_id);
  36. if (!$address || $address['user_id'] != $user_id) {
  37. throw new Exception("地址未找到");
  38. }
  39. if (empty($goods_list)) {
  40. throw new Exception("商品列表不能为空");
  41. }
  42. $config = get_addon_config('shop');
  43. $order_sn = date("Ymdhis") . sprintf("%08d", $user_id) . mt_rand(1000, 9999);
  44. // 订单主表信息
  45. $orderInfo = [
  46. 'user_id' => $user_id,
  47. 'order_sn' => $order_sn,
  48. 'address_id' => $address->id,
  49. 'province_id' => $address->province_id,
  50. 'city_id' => $address->city_id,
  51. 'area_id' => $address->area_id,
  52. 'receiver' => $address->receiver,
  53. 'mobile' => $address->mobile,
  54. 'address' => $address->address,
  55. 'zipcode' => $address->zipcode,
  56. 'goodsprice' => 0, // 商品金额 (不含运费)
  57. 'amount' => 0, // 总金额 (含运费)
  58. 'shippingfee' => 0, // 运费
  59. 'discount' => 0, // 优惠金额
  60. 'saleamount' => 0,
  61. 'memo' => $memo,
  62. 'expiretime' => time() + $config['order_timeout'], // 订单失效
  63. 'status' => 'normal'
  64. ];
  65. // 通过商品列表计算订单明细
  66. list($orderItem, $goodsList, $userCoupon) = self::computeGoods($orderInfo, $goods_list, $user_id, $address->area_id, $user_coupon_id);
  67. // 创建订单
  68. $order = self::createOrderWithTransaction($orderInfo, $orderItem, $goodsList, $userCoupon);
  69. // 如果是购物车模式,需要清空购物车
  70. if (!empty($cart_ids)) {
  71. Carts::clear($cart_ids);
  72. }
  73. return $order;
  74. }
  75. /**
  76. * 通过购物车创建订单
  77. * @param int $address_id 地址ID
  78. * @param int $user_id 用户ID
  79. * @param array $cart_ids 购物车ID数组
  80. * @param int $user_coupon_id 优惠券ID
  81. * @param string $memo 备注
  82. * @return Order
  83. * @throws Exception
  84. */
  85. public static function createOrderByCart($address_id, $user_id, $cart_ids, $user_coupon_id = 0, $memo = '')
  86. {
  87. if (empty($cart_ids)) {
  88. throw new Exception("购物车列表不能为空");
  89. }
  90. // 将购物车数据转换为标准的商品列表格式
  91. $goods_list = self::convertCartToGoodsList($cart_ids, $user_id);
  92. return self::createOrder($address_id, $user_id, $goods_list, $user_coupon_id, $memo, $cart_ids);
  93. }
  94. /**
  95. * 直接通过商品规格数量创建订单
  96. * @param int $address_id 地址ID
  97. * @param int $user_id 用户ID
  98. * @param array $goods_list 商品列表 [['goods_id'=>1, 'goods_sku_id'=>0, 'nums'=>1], ...]
  99. * @param int $user_coupon_id 优惠券ID
  100. * @param string $memo 备注
  101. * @return Order
  102. * @throws Exception
  103. */
  104. public static function createOrderByGoods($address_id, $user_id, $goods_list, $user_coupon_id = 0, $memo = '')
  105. {
  106. return self::createOrder($address_id, $user_id, $goods_list, $user_coupon_id, $memo);
  107. }
  108. /**
  109. * 根据商品列表计算订单明细
  110. * @param array $orderInfo 订单基础信息
  111. * @param array $goods_list 商品列表
  112. * @param int $user_id 用户ID
  113. * @param int $area_id 地区ID
  114. * @param int $user_coupon_id 优惠券ID
  115. * @return array
  116. * @throws Exception
  117. */
  118. protected static function computeGoods(&$orderInfo, $goods_list, $user_id, $area_id, $user_coupon_id = 0)
  119. {
  120. $config = get_addon_config('shop');
  121. $orderInfo['amount'] = 0;
  122. $orderInfo['goodsprice'] = 0;
  123. $orderInfo['shippingfee'] = 0;
  124. $orderInfo['discount'] = 0;
  125. $orderItem = [];
  126. $shippingTemp = [];
  127. $userCoupon = null;
  128. $goodsList = [];
  129. // 校验优惠券
  130. if ($user_coupon_id) {
  131. $userCouponModel = new UserCoupon();
  132. $userCoupon = $userCouponModel->checkUserOrUse($user_coupon_id, $user_id);
  133. $orderInfo['user_coupon_id'] = $user_coupon_id;
  134. }
  135. // 提取所有商品ID和SKU ID,进行批量查询
  136. $goods_ids = array_column($goods_list, 'goods_id');
  137. $sku_ids = [];
  138. $goods_without_sku = []; // 记录没有指定SKU ID的商品,需要查询默认SKU
  139. foreach ($goods_list as $index => $item) {
  140. if (isset($item['goods_sku_id']) && $item['goods_sku_id'] > 0) {
  141. $sku_ids[] = $item['goods_sku_id'];
  142. } else {
  143. // 没有指定SKU ID的商品,记录下来后续查询默认SKU
  144. $goods_without_sku[$index] = $item['goods_id'];
  145. }
  146. }
  147. // 批量查询商品信息
  148. $goodsData = [];
  149. if (!empty($goods_ids)) {
  150. $goodsCollection = Goods::with(['category', 'brand'])
  151. ->where('id', 'in', $goods_ids)
  152. ->where('status', GoodsEnum::STATUS_ON_SALE)
  153. ->select();
  154. foreach ($goodsCollection as $goods) {
  155. $goodsData[$goods->id] = $goods;
  156. }
  157. }
  158. // 批量查询SKU信息
  159. $skuData = [];
  160. $multiSpecSkuIds = []; // 用于存储多规格商品的SKU ID
  161. if (!empty($sku_ids)) {
  162. $skuCollection = Sku::where('id', 'in', $sku_ids)->select();
  163. foreach ($skuCollection as $sku) {
  164. $skuData[$sku->id] = $sku;
  165. // 过滤出有规格值的SKU ID(spec_value_ids不为空)
  166. if (!empty($sku->spec_value_ids)) {
  167. $multiSpecSkuIds[] = $sku->id;
  168. }
  169. }
  170. }
  171. // 查询没有指定SKU ID的商品的默认SKU(单规格商品或多规格商品的默认SKU)
  172. if (!empty($goods_without_sku)) {
  173. $defaultSkuCollection = Sku::where('goods_id', 'in', array_values($goods_without_sku))
  174. ->where('is_default', 1)
  175. ->select();
  176. foreach ($defaultSkuCollection as $sku) {
  177. $skuData[$sku->id] = $sku;
  178. // 更新对应的goods_list项,补充SKU ID
  179. foreach ($goods_without_sku as $list_index => $goods_id) {
  180. if ($sku->goods_id == $goods_id) {
  181. $goods_list[$list_index]['goods_sku_id'] = $sku->id;
  182. $sku_ids[] = $sku->id; // 添加到sku_ids中用于后续查询规格属性
  183. // 如果默认SKU有规格值,也加入到多规格SKU列表
  184. if (!empty($sku->spec_value_ids)) {
  185. $multiSpecSkuIds[] = $sku->id;
  186. }
  187. unset($goods_without_sku[$list_index]);
  188. break;
  189. }
  190. }
  191. }
  192. }
  193. // 批量查询规格属性字符串(只查询多规格商品的SKU)
  194. $skuAttrData = [];
  195. if (!empty($multiSpecSkuIds)) {
  196. $skuAttrData = \app\common\Service\SkuSpec::getSkuAttrs($multiSpecSkuIds);
  197. }
  198. // 验证并构建商品数据
  199. foreach ($goods_list as $item) {
  200. $goods_id = $item['goods_id'];
  201. $goods_sku_id = $item['goods_sku_id']; // 现在所有商品都应该有SKU ID
  202. $nums = $item['nums'];
  203. if ($nums <= 0) {
  204. throw new Exception("商品数量必须大于0");
  205. }
  206. // 检查商品是否存在
  207. if (!isset($goodsData[$goods_id])) {
  208. throw new Exception("商品已下架");
  209. }
  210. $goods = $goodsData[$goods_id];
  211. // 所有商品都必须有SKU(包括单规格商品的默认SKU)
  212. if (empty($skuData) || !isset($skuData[$goods_sku_id])) {
  213. throw new Exception("商品规格不存在");
  214. }
  215. $sku = $skuData[$goods_sku_id];
  216. // 验证SKU是否属于该商品
  217. if ($sku->goods_id != $goods_id) {
  218. throw new Exception("商品规格不匹配");
  219. }
  220. // 获取规格属性字符串(单规格商品的sku_attr为空)
  221. $sku_attr = $skuAttrData[$goods_sku_id] ?? '';
  222. // 构建商品对象,模拟购物车数据结构
  223. $goodsItem = (object)[
  224. 'goods_id' => $goods_id,
  225. 'goods_sku_id' => $goods_sku_id,
  226. 'nums' => $nums,
  227. 'goods' => $goods,
  228. 'sku' => $sku,
  229. 'sku_attr' => $sku_attr
  230. ];
  231. $goodsList[] = $goodsItem;
  232. }
  233. // 计算商品价格和运费(统一使用SKU进行计算)
  234. foreach ($goodsList as $item) {
  235. $goodsItemData = [];
  236. if (empty($item->goods) || empty($item->sku)) {
  237. throw new Exception("商品已下架");
  238. }
  239. // 库存验证(统一使用SKU库存)
  240. if ($item->sku->stocks < $item->nums) {
  241. throw new Exception("商品库存不足,请重新修改数量");
  242. }
  243. // 统一使用SKU数据进行计算
  244. $goodsItemData['image'] = !empty($item->sku->image) ? $item->sku->image : $item->goods->image;
  245. $goodsItemData['price'] = $item->sku->price;
  246. $goodsItemData['lineation_price'] = $item->sku->lineation_price;
  247. $goodsItemData['sku_sn'] = $item->sku->sku_sn;
  248. $amount = bcmul($item->sku->price, $item->nums, 2);
  249. $goodsItemData['amount'] = $amount;
  250. // 订单总价
  251. $orderInfo['amount'] = bcadd($orderInfo['amount'], $amount, 2);
  252. // 商品总价
  253. $orderInfo['goodsprice'] = bcadd($orderInfo['goodsprice'], $amount, 2);
  254. $freight_id = $item->goods->express_template_id;
  255. // 计算邮费【合并运费模板】
  256. if (!isset($shippingTemp[$freight_id])) {
  257. $shippingTemp[$freight_id] = [
  258. 'nums' => $item->nums,
  259. 'weight' => $item->sku->weight,
  260. 'amount' => $amount
  261. ];
  262. } else {
  263. $shippingTemp[$freight_id] = [
  264. 'nums' => bcadd($shippingTemp[$freight_id]['nums'], $item->nums, 2),
  265. 'weight' => bcadd($shippingTemp[$freight_id]['weight'], $item->sku->weight, 2),
  266. 'amount' => bcadd($shippingTemp[$freight_id]['amount'], $amount, 2)
  267. ];
  268. }
  269. // 创建订单商品数据
  270. $orderItem[] = array_merge($goodsItemData, [
  271. 'order_sn' => $orderInfo['order_sn'],
  272. 'goods_id' => $item->goods_id,
  273. 'title' => $item->goods->title,
  274. 'url' => $item->goods->url,
  275. 'nums' => $item->nums,
  276. 'goods_sku_id' => $item->goods_sku_id,
  277. 'attrdata' => $item->sku_attr,
  278. 'weight' => $item->sku->weight,
  279. 'category_id' => $item->goods->category_id,
  280. 'brand_id' => $item->goods->brand_id,
  281. ]);
  282. }
  283. // 按运费模板计算
  284. foreach ($shippingTemp as $key => $item) {
  285. $shippingfee = Freight::calculate($key, $area_id, $item['nums'], $item['weight'], $item['amount']);
  286. $orderInfo['shippingfee'] = bcadd($orderInfo['shippingfee'], $shippingfee, 2);
  287. }
  288. // 订单总价(含邮费)
  289. $orderInfo['amount'] = bcadd($orderInfo['goodsprice'], $orderInfo['shippingfee'], 2);
  290. if (!empty($userCoupon)) {
  291. // 校验优惠券
  292. $goods_ids = array_column($orderItem, 'goods_id');
  293. $category_ids = array_column($orderItem, 'category_id');
  294. $brand_ids = array_column($orderItem, 'brand_id');
  295. $couponModel = new Coupon();
  296. $coupon = $couponModel->getCoupon($userCoupon['coupon_id'])
  297. ->checkCoupon()
  298. ->checkOpen()
  299. ->checkUseTime($userCoupon['createtime'])
  300. ->checkConditionGoods($goods_ids, $user_id, $category_ids, $brand_ids);
  301. // 计算折扣金额,判断是使用不含运费,还是含运费的金额
  302. $amount = !isset($config['shippingfeecoupon']) || $config['shippingfeecoupon'] == 0 ? $orderInfo['goodsprice'] : $orderInfo['amount'];
  303. list($new_money, $coupon_money) = $coupon->doBuy($amount);
  304. // 判断优惠金额是否超出总价,超出则直接设定优惠金额为总价
  305. $orderInfo['discount'] = $coupon_money > $amount ? $amount : $coupon_money;
  306. }
  307. // 计算订单的应付金额【减去折扣】
  308. $orderInfo['saleamount'] = max(0, bcsub($orderInfo['amount'], $orderInfo['discount'], 2));
  309. $orderInfo['discount'] = bcadd($orderInfo['discount'], 0, 2);
  310. return [
  311. $orderItem,
  312. $goodsList,
  313. $userCoupon
  314. ];
  315. }
  316. /**
  317. * 在事务中创建订单
  318. * @param array $orderInfo 订单信息
  319. * @param array $orderItem 订单商品列表
  320. * @param array $goodsList 商品列表
  321. * @param object $userCoupon 优惠券
  322. * @return Order
  323. * @throws Exception
  324. */
  325. protected static function createOrderWithTransaction($orderInfo, $orderItem, $goodsList, $userCoupon)
  326. {
  327. $order = null;
  328. Db::startTrans();
  329. try {
  330. // 创建订单
  331. $order = Order::create($orderInfo, true);
  332. // 减库存
  333. foreach ($goodsList as $index => $item) {
  334. if ($item->sku) {
  335. $item->sku->setDec('stocks', $item->nums);
  336. }
  337. $item->goods->setDec("stocks", $item->nums);
  338. }
  339. // 计算单个商品折扣后的价格
  340. $saleamount = bcsub($order['saleamount'], $order['shippingfee'], 2);
  341. $saleratio = $order['goodsprice'] > 0 ? bcdiv($saleamount, $order['goodsprice'], 10) : 1;
  342. $saleremains = $saleamount;
  343. foreach ($orderItem as $index => &$item) {
  344. if (!isset($orderItem[$index + 1])) {
  345. $saleprice = $saleremains;
  346. } else {
  347. $saleprice = $order['discount'] == 0 ? bcmul($item['price'], $item['nums'], 2) : bcmul(bcmul($item['price'], $item['nums'], 2), $saleratio, 2);
  348. }
  349. $saleremains = bcsub($saleremains, $saleprice, 2);
  350. $item['realprice'] = $saleprice;
  351. }
  352. unset($item);
  353. // 创建订单商品数据
  354. foreach ($orderItem as $index => $item) {
  355. OrderGoods::create($item, true);
  356. }
  357. // 修改地址使用次数
  358. $address = Address::get($orderInfo['address_id']);
  359. if ($address) {
  360. $address->setInc('usednums');
  361. }
  362. // 优惠券已使用
  363. if (!empty($userCoupon)) {
  364. $userCoupon->save(['is_used' => 2]);
  365. }
  366. // 提交事务
  367. Db::commit();
  368. } catch (Exception $e) {
  369. Db::rollback();
  370. throw new Exception($e->getMessage());
  371. }
  372. // 记录操作
  373. OrderAction::push($orderInfo['order_sn'], '系统', '订单创建成功');
  374. // 订单应付金额为0时直接结算
  375. if ($order['saleamount'] == 0) {
  376. Order::settle($order->order_sn, 0);
  377. $order = Order::get($order->id);
  378. }
  379. return $order;
  380. }
  381. /**
  382. * 验证商品规格参数
  383. * @param array $goods_list 商品列表
  384. * @throws Exception
  385. */
  386. public static function validateGoodsList($goods_list)
  387. {
  388. if (empty($goods_list) || !is_array($goods_list)) {
  389. throw new Exception("商品列表不能为空");
  390. }
  391. foreach ($goods_list as $item) {
  392. if (!isset($item['goods_id']) || !is_numeric($item['goods_id']) || $item['goods_id'] <= 0) {
  393. throw new Exception("商品ID无效");
  394. }
  395. if (!isset($item['nums']) || !is_numeric($item['nums']) || $item['nums'] <= 0) {
  396. throw new Exception("商品数量必须大于0");
  397. }
  398. if (isset($item['goods_sku_id']) && !is_numeric($item['goods_sku_id'])) {
  399. throw new Exception("商品规格ID无效");
  400. }
  401. }
  402. }
  403. /**
  404. * 统一的订单计算方法(用于预览订单)
  405. * @param array $goods_list 标准化的商品列表
  406. * @param int $user_id 用户ID
  407. * @param int $area_id 地区ID
  408. * @param int $user_coupon_id 优惠券ID
  409. * @return array
  410. * @throws Exception
  411. */
  412. public static function calculateOrder($goods_list, $user_id, $area_id = 0, $user_coupon_id = 0)
  413. {
  414. if (empty($goods_list)) {
  415. throw new Exception("商品列表不能为空");
  416. }
  417. // 验证商品列表格式
  418. self::validateGoodsList($goods_list);
  419. $order_sn = date("Ymdhis") . sprintf("%08d", $user_id) . mt_rand(1000, 9999);
  420. // 订单基础信息
  421. $orderInfo = [
  422. 'order_sn' => $order_sn,
  423. 'goodsprice' => 0, // 商品金额 (不含运费)
  424. 'amount' => 0, // 总金额 (含运费)
  425. 'shippingfee' => 0, // 运费
  426. 'discount' => 0, // 优惠金额
  427. 'saleamount' => 0 // 应付金额
  428. ];
  429. // 计算商品明细
  430. list($orderItem, $goodsList, $userCoupon) = self::computeGoods($orderInfo, $goods_list, $user_id, $area_id, $user_coupon_id);
  431. return [
  432. 'orderItem' => $orderItem,
  433. 'goodsList' => $goodsList,
  434. 'orderInfo' => $orderInfo,
  435. 'userCoupon' => $userCoupon
  436. ];
  437. }
  438. /**
  439. * 通过商品规格计算订单明细(用于预览订单)
  440. * @param array $goods_list 商品列表
  441. * @param int $user_id 用户ID
  442. * @param int $area_id 地区ID
  443. * @param int $user_coupon_id 优惠券ID
  444. * @return array
  445. * @throws Exception
  446. */
  447. public static function calculateOrderByGoods($goods_list, $user_id, $area_id = 0, $user_coupon_id = 0)
  448. {
  449. return self::calculateOrder($goods_list, $user_id, $area_id, $user_coupon_id);
  450. }
  451. /**
  452. * 通过购物车计算订单明细(用于预览订单)
  453. * @param array $cart_ids 购物车ID列表
  454. * @param int $user_id 用户ID
  455. * @param int $area_id 地区ID
  456. * @param int $user_coupon_id 优惠券ID
  457. * @return array
  458. * @throws Exception
  459. */
  460. public static function calculateOrderByCart($cart_ids, $user_id, $area_id = 0, $user_coupon_id = 0)
  461. {
  462. if (empty($cart_ids)) {
  463. throw new Exception("购物车列表不能为空");
  464. }
  465. // 将购物车数据转换为标准的商品列表格式
  466. $goods_list = self::convertCartToGoodsList($cart_ids, $user_id);
  467. return self::calculateOrder($goods_list, $user_id, $area_id, $user_coupon_id);
  468. }
  469. /**
  470. * 将购物车数据转换为标准的商品列表格式
  471. * @param array $cart_ids 购物车ID列表
  472. * @param int $user_id 用户ID
  473. * @return array
  474. * @throws Exception
  475. */
  476. public static function convertCartToGoodsList($cart_ids, $user_id)
  477. {
  478. // 查询购物车数据
  479. $cartItems = Carts::where('id', 'in', $cart_ids)
  480. ->where('user_id', $user_id)
  481. ->select();
  482. if (empty($cartItems)) {
  483. throw new Exception("购物车数据不存在");
  484. }
  485. $goods_list = [];
  486. foreach ($cartItems as $cart) {
  487. $goods_list[] = [
  488. 'goods_id' => $cart->goods_id,
  489. 'goods_sku_id' => $cart->goods_sku_id,
  490. 'nums' => $cart->nums
  491. ];
  492. }
  493. return $goods_list;
  494. }
  495. }