CRMEB/backend/modules/product/normal/service/OrderCreateServices.php
吴昊天 6b1f41dddb fix(订单): 详情展示原运费与运费优惠
- 下单时保存 postage_discount,供订单详情还原优惠前快递运费
- 价格信息运费改为快递运费,展示原价并单独展示会员运费优惠
- 自提/上门订单运费优惠清零
2026-10-23 14:14:37 +08:00

508 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace modules\product\normal\service;
use crmeb\basic\BaseService;
use app\dao\order\OrderCenterDao;
use app\services\order\OrderCenterServices;
use app\services\order\OrderCenterUserServices;
use app\services\order\OrderComputedServices;
use app\services\order\OrderInvoiceServices;
use app\services\order\StoreCartServices;
use app\services\user\UserAddressServices;
use app\services\user\UserBillServices;
use app\services\user\UserInvoiceServices;
use crmeb\utils\SnowflakeIdGenerator;
use modules\product\normal\Handler;
/**
* 实物商品下单服务
* 下单逻辑自旧订单创建服务内化而来,模块自治:
* 校验购物车 → 价格计算 → 地址处理 → 构建订单 → 扣库存/积分 → 写扩展表 → 写中台表 → 清购物车 → 后置事件
*/
class OrderCreateServices extends BaseService
{
/**
* @var OrderCenterServices
*/
protected $centerServices;
/**
* @var Handler
*/
protected $typeHandler;
/**
* @var OrderCenterDao
*/
protected $centerDao;
/**
* 初始化
*/
protected function initialize(): void
{
$this->centerServices = $this->app->make(OrderCenterServices::class);
$this->centerDao = $this->app->make(OrderCenterDao::class);
$this->typeHandler = $this->app->make(Handler::class);
}
/**
* 创建订单
* @param int $uid
* @param string $key 缓存订单key
* @param array $userInfo
* @param int $addressId
* @param string $payType
* @param bool $useIntegral
* @param int $couponId
* @param string $mark
* @param int $shippingType 1快递 2自提
* @param string $realName
* @param string $phone
* @param int $storeId
* @param bool $news 是否立即购买
* @param array $customForm 自定义表单
* @param int $invoiceId 发票ID
* @return array
*/
public function createOrder(
int $uid,
string $key,
array $userInfo,
int $addressId = 0,
string $payType = '',
bool $useIntegral = false,
int $couponId = 0,
string $mark = '',
int $shippingType = 1,
string $realName = '',
string $phone = '',
int $storeId = 0,
bool $news = false,
array $customForm = [],
int $invoiceId = 0
): array {
// 1. 校验购物车
$cartGroup = $this->validateCart($uid, $key);
// 2. 地址校验
$addressInfo = $this->validateAddress($uid, $addressId, $shippingType, $realName, $phone, $cartGroup);
// 3. 价格计算
$priceData = $this->computePrice($uid, $userInfo, $cartGroup, $addressId, $payType, $useIntegral, $couponId, $shippingType);
// 4. 构建订单数据
$orderInfo = $this->buildOrderInfo($uid, $userInfo, $addressInfo, $cartGroup, $priceData, $payType, $couponId, $mark, $shippingType, $key);
// 5. 事务写入
$orderNo = $orderInfo['order_no'];
$cartInfo = $this->normalizeCartInfo($cartGroup['cartInfo'][0] ?? []);
$orderCenterId = 0;
$this->transaction(function () use ($uid, $orderInfo, $cartInfo, $cartGroup, $addressInfo, $useIntegral, $priceData, $orderNo, $payType, $shippingType, $storeId, $customForm, &$orderCenterId) {
// 5.1 扣减合单中的每一个 SKU。库存不足必须抛错,让事务回滚,不能静默创建无法履约的订单。
$stockItems = $cartGroup['cartInfo'] ?? [$cartInfo];
foreach ($stockItems as $stockItem) {
$stockItem = $this->normalizeCartInfo($stockItem);
$skuUnique = (string)($stockItem['product_attr_unique'] ?? ($stockItem['productInfo']['attrInfo']['unique'] ?? ''));
$productId = (int)($stockItem['product_id'] ?? $stockItem['productInfo']['id'] ?? 0);
$cartNum = (int)($stockItem['cart_num'] ?? 1);
if ($productId <= 0 || $cartNum <= 0 || !$this->typeHandler->decStock($productId, $skuUnique, $cartNum)) {
throw new \RuntimeException('商品库存不足或规格已失效');
}
}
// 5.2 积分抵扣
if ($useIntegral && !empty($priceData['usedIntegral'])) {
$this->deductIntegral($uid, $priceData, $orderNo);
}
// 5.3 先写扩展表获取 source_id,避免唯一索引冲突
$context = [
'pay_price' => $priceData['pay_price'] ?? 0,
'total_price' => $priceData['total_price'] ?? 0,
'activity_type' => $orderInfo['activity_type'] ?? '',
'shipping_type' => $shippingType,
];
$extendId = $this->typeHandler->createOrderExtend($orderNo, $cartInfo, $context);
// 生成核销码(自提订单使用,直接传中台表实体列)
$verifyCode = ($shippingType === 2 && method_exists($this->typeHandler, 'generateVerifyCode'))
? $this->typeHandler->generateVerifyCode() : '';
// 5.4 写中台订单表(使用扩展表ID作为source_id,确保唯一索引不冲突)
$this->centerServices->createRecord([
'order_no' => $orderNo,
'source_type' => $this->typeHandler->getOrderSourceType(),
'source_id' => $extendId,
'uid' => $uid,
'order_category' => 'goods',
'product_type' => $this->typeHandler->getTypeKey(),
'status_code' => 'pending_payment',
'pay_status' => 0,
'refund_status' => 0,
'total_price' => $priceData['total_price'] ?? 0,
'pay_price' => $priceData['pay_price'] ?? 0,
'pay_type' => $payType,
'coupon_price' => (string) ($priceData['coupon_price'] ?? '0.00'),
'deduction_price' => (string) ($priceData['deduction_price'] ?? '0.00'),
'use_integral_num' => (int) ($priceData['usedIntegral'] ?? 0),
'member_discount' => '0.00',
'vip_discount' => '0.00',
'gift_price' => '0.00',
'title' => $cartGroup['cartInfo'][0]['productInfo']['store_name'] ?? '',
'image' => $cartGroup['cartInfo'][0]['productInfo']['image'] ?? '',
'total_num' => $orderInfo['total_num'] ?? 1,
'store_id' => (int)$storeId,
'shipping_type' => $shippingType,
'verify_code' => $verifyCode,
'mark' => $orderInfo['mark'] ?? '',
'custom_form' => $customForm,
'extra_info' => [
'business_status' => [
'code' => 'pending_payment',
'text' => '待支付',
'detail' => '',
'extend' => new \stdClass(),
],
'delivery_type' => $shippingType === 2 ? 'pickup' : ($shippingType === 3 ? 'door' : 'express'),
'delivery' => [
'delivery_type' => $shippingType === 2 ? 'pickup' : ($shippingType === 3 ? 'door' : 'express'),
],
// 金额字段已提升为实体列;运费优惠需保留原始运费与优惠额供订单详情展示
'postage_discount' => (string)($priceData['postage_discount'] ?? '0.00'),
// 商品规格快照,供订单列表/详情页展示
'sku_info' => $this->buildSkuInfo($cartGroup['cartInfo'][0] ?? []),
// v8.1.1 商品级售后:写入 items 快照(含 cart_id),供售后引擎加载商品列表
'items' => $this->buildOrderItems($cartGroup['cartInfo'] ?? []),
'contact' => [
'name' => $addressInfo['real_name'] ?? $orderInfo['real_name'] ?? '',
'phone' => $addressInfo['phone'] ?? $orderInfo['user_phone'] ?? '',
'address' => [
'province' => $addressInfo['province'] ?? '',
'city' => $addressInfo['city'] ?? '',
'district' => $addressInfo['district'] ?? '',
'detail' => $addressInfo['detail'] ?? '',
'postal_code' => $addressInfo['postal_code'] ?? '',
],
],
],
// 普通商品订单详情统一由订单中台路由承接,避免模块写入过期插件路由。
'detail_route' => '/pages/center/order/detail/index?order_no=' . $orderNo,
]);
// 5.5 获取中台订单主键,供自动取消等异步任务使用
$centerRecord = $this->centerDao->get(['order_no' => $orderNo], ['id']);
if ($centerRecord) {
$orderCenterId = (int)($centerRecord['id'] ?? 0);
}
// 5.6 确认扩展表写入(幂等跳过,避免重复写入)
$this->typeHandler->afterOrder($orderNo, $cartInfo, $context);
// 5.7 清理购物车
$this->clearCart($uid, $orderInfo['cart_id'] ?? []);
});
// 6. 发票校验与开具
if ($invoiceId) {
app()->make(UserInvoiceServices::class)->checkInvoice((int)$invoiceId, $uid);
app()->make(OrderInvoiceServices::class)->makeUp($uid, $orderNo, (int)$invoiceId);
}
// 7. 订单创建后置事件(新中台格式)
event('OrderCreateAfter', [
'order_no' => $orderNo,
'order_id' => $orderCenterId,
'cart_info' => $cartInfo,
'cart_items' => $cartGroup['cartInfo'] ?? [$cartInfo],
'price_data' => $priceData,
'uid' => $uid,
'user_phone' => $orderInfo['user_phone'] ?? '',
'key' => $key,
'product_type' => $this->typeHandler->getTypeKey(),
]);
// 8. 外部订单推送
event('OutPush', ['order_create_push', ['order_id' => $orderCenterId]]);
return ['order_id' => $orderNo];
}
/**
* 校验购物车
* @param int $uid
* @param string $key
* @return array
*/
protected function validateCart(int $uid, string $key): array
{
if ($uid <= 0 || $key === '') {
throw new \RuntimeException('订单已过期,请刷新当前页面');
}
/** @var OrderCenterUserServices $centerUserServices */
$centerUserServices = $this->app->make(OrderCenterUserServices::class);
$cartGroup = $centerUserServices->getCacheOrderInfo($uid, $key);
if (!$cartGroup) {
throw new \RuntimeException('订单已过期,请刷新当前页面');
}
if (is_string($cartGroup)) {
$cartGroup = json_decode($cartGroup, true) ?: [];
}
// cacheOrderInfo 缓存字段为 priceData,computePrice 需要 priceGroup,做兼容
if (!isset($cartGroup['priceGroup']) && isset($cartGroup['priceData'])) {
$cartGroup['priceGroup'] = $cartGroup['priceData'];
}
if (empty($cartGroup['cartInfo']) || empty($cartGroup['priceGroup'])) {
throw new \RuntimeException('订单商品信息有误');
}
return $cartGroup;
}
/**
* 地址校验
* @param int $uid
* @param int $addressId
* @param int $shippingType
* @param string $realName
* @param string $phone
* @param array $cartGroup
* @return array
*/
protected function validateAddress(int $uid, int $addressId, int $shippingType, string $realName, string $phone, array $cartGroup): array
{
$isVirtual = !empty($cartGroup['cartInfo'][0]['productInfo']['is_virtual']);
if ($shippingType == 1 && !$isVirtual) {
if (!$addressId) {
throw new \RuntimeException('请选择收货地址');
}
$addressServices = $this->app->make(UserAddressServices::class);
$addressInfo = $addressServices->getOne(['uid' => $uid, 'id' => $addressId, 'is_del' => 0]);
if (!$addressInfo) {
throw new \RuntimeException('地址选择有误');
}
return $addressInfo->toArray();
}
if ((!$realName || !$phone) && !$isVirtual) {
throw new \RuntimeException('请填写姓名和电话');
}
return [
'real_name' => $realName,
'phone' => $phone,
'province' => '',
'city' => '',
'district' => '',
'detail' => '',
];
}
/**
* 价格计算
* @param int $uid
* @param array $userInfo
* @param array $cartGroup
* @param int $addressId
* @param string $payType
* @param bool $useIntegral
* @param int $couponId
* @param int $shippingType
* @return array
*/
protected function computePrice(int $uid, array $userInfo, array $cartGroup, int $addressId, string $payType, bool $useIntegral, int $couponId, int $shippingType): array
{
$computedServices = $this->app->make(OrderComputedServices::class);
$priceData = $computedServices->computedOrder($uid, $userInfo, $cartGroup, $addressId, $payType, $useIntegral, $couponId, true, $shippingType);
return [
'total_price' => $cartGroup['priceGroup']['totalPrice'] ?? 0,
'promotion_price' => $priceData['promotion_price'] ?? 0,
'promotion_detail' => $priceData['promotion_detail'] ?? [],
'pay_price' => $priceData['pay_price'] ?? 0,
'coupon_price' => $priceData['coupon_price'] ?? 0,
'pay_postage' => $priceData['pay_postage'] ?? 0,
// 会员运费优惠:pay_postage 是优惠后运费,需单独落库供详情页展示原运费与优惠
'postage_discount' => $priceData['storePostageDiscount'] ?? 0,
'deduction_price' => $priceData['deduction_price'] ?? 0,
'usedIntegral' => $priceData['usedIntegral'] ?? 0,
];
}
/**
* 构建订单数据
* @param int $uid
* @param array $userInfo
* @param array $addressInfo
* @param array $cartGroup
* @param array $priceData
* @param string $payType
* @param int $couponId
* @param string $mark
* @param int $shippingType
* @return array
*/
protected function buildOrderInfo(int $uid, array $userInfo, array $addressInfo, array $cartGroup, array $priceData, string $payType, int $couponId, string $mark, int $shippingType, string $key): array
{
$cartInfo = $cartGroup['cartInfo'];
$cartIds = [];
$totalNum = 0;
foreach ($cartInfo as $cart) {
$cartIds[] = $cart['id'];
$totalNum += $cart['cart_num'];
}
return [
'uid' => $uid,
'order_no' => SnowflakeIdGenerator::generate('cp'),
'unique' => $key,
'real_name' => $addressInfo['real_name'] ?? '',
'user_phone' => $addressInfo['phone'] ?? '',
'user_address' => trim(($addressInfo['province'] ?? '') . ' ' . ($addressInfo['city'] ?? '') . ' ' . ($addressInfo['district'] ?? '') . ' ' . ($addressInfo['detail'] ?? '')),
'cart_id' => $cartIds,
'total_num' => $totalNum,
'total_price' => $priceData['total_price'],
'pay_price' => $priceData['pay_price'],
'coupon_id' => $couponId,
'coupon_price' => $priceData['coupon_price'],
'pay_postage' => $priceData['pay_postage'],
'deduction_price' => $priceData['deduction_price'],
'pay_type' => $payType,
'mark' => htmlspecialchars($mark),
'shipping_type' => $shippingType,
'channel_type' => $userInfo['user_type'] ?? '',
'add_time' => time(),
];
}
/**
* 积分抵扣
* @param int $uid
* @param array $priceData
* @param string $orderNo
*/
protected function deductIntegral(int $uid, array $priceData, string $orderNo): void
{
$usedIntegral = (int)($priceData['usedIntegral'] ?? 0);
if ($usedIntegral <= 0) {
return;
}
/** @var UserBillServices $userBillServices */
$userBillServices = $this->app->make(UserBillServices::class);
$userBillServices->billIntegralIncome('deduction', $uid, [
'number' => $usedIntegral,
'deductionPrice' => $priceData['deduction_price'] ?? 0,
], $orderNo);
}
/**
* 清理购物车
* @param int $uid
* @param array $cartIds
*/
protected function clearCart(int $uid, array $cartIds): void
{
if (empty($cartIds)) {
return;
}
try {
/** @var StoreCartServices $cartServices */
$cartServices = $this->app->make(StoreCartServices::class);
$cartServices->removeUserCart($uid, $cartIds);
} catch (\Throwable $e) {
// 购物车清理失败不影响订单创建,记录日志即可
\think\facade\Log::warning('[NormalOrderCreate] clearCart failed: ' . $e->getMessage());
}
}
/**
* 构建商品规格快照
* 从购物车商品信息中提取 SKU 唯一码、规格名称、图片等,供订单列表/详情展示
* @param array $cart 购物车单项
* @return array 返回规格快照数组
*/
protected function buildSkuInfo(array $cart): array
{
if (empty($cart)) {
return [];
}
$attrInfo = $cart['productInfo']['attrInfo'] ?? [];
if (empty($attrInfo)) {
return [];
}
return [
'unique' => $attrInfo['unique'] ?? ($attrInfo['unique_code'] ?? ''),
'suk' => $attrInfo['suk'] ?? ($attrInfo['sku'] ?? ''),
'image' => $attrInfo['image'] ?? '',
'price' => $attrInfo['price'] ?? 0,
'ot_price' => $attrInfo['ot_price'] ?? 0,
];
}
/**
* 构建订单商品快照列表,存入 extra_info.items 供详情页和售后引擎使用
* v8.1.1 商品级售后:包含 cart_id 字段,供按 cart_id 选择售后范围
* 支持 multi-cart(多商品订单),遍历 cartInfo 数组构建 items 列表
* @param array $cartInfoList 购物车商品数组
* @return array
*/
protected function buildOrderItems(array $cartInfoList): array
{
$items = [];
foreach ($cartInfoList as $cart) {
if (empty($cart) || empty($cart['productInfo'])) {
continue;
}
$productInfo = $cart['productInfo'];
$attrInfo = $productInfo['attrInfo'] ?? [];
$items[] = [
// cart_id 为商品级售后主键,从购物车数据继承
'cart_id' => (int)($cart['id'] ?? $cart['cart_id'] ?? 0),
'product_id' => $productInfo['id'] ?? 0,
'image' => $productInfo['image'] ?? '',
'store_name' => $productInfo['store_name'] ?? ($productInfo['title'] ?? ''),
'price' => $attrInfo['price'] ?? ($productInfo['price'] ?? '0.00'),
'true_price' => $cart['truePrice'] ?? ($attrInfo['price'] ?? $productInfo['price'] ?? '0.00'),
'cart_num' => $cart['cart_num'] ?? 1,
'product_type' => $productInfo['product_type'] ?? 'normal',
'attr_info' => $attrInfo,
'sku' => $attrInfo['suk'] ?? '默认',
];
}
return $items;
}
/**
* 规范化购物车信息,供 Handler::afterOrder 使用
* @param array $cart
* @return array
*/
protected function normalizeCartInfo(array $cart): array
{
if (empty($cart)) {
return [];
}
$cart['product_id'] = $cart['product_id'] ?? ($cart['productInfo']['id'] ?? 0);
$cart['uid'] = $cart['uid'] ?? 0;
$cart['cart_num'] = $cart['cart_num'] ?? 1;
return $cart;
}
}