mirror of
https://github.com/crmeb/CRMEB.git
synced 2026-10-28 13:31:04 +00:00
- 确认接口 priceGroup 增加 sumPrice(规格原价 × 数量),活动订单取活动价 - 下单引擎商品卡片使用规格原价,商品总价使用 sumPrice,等级/会员优惠单独列示 - 旧版确认页商品总价按 sumPrice 展示,修复运费字段名不一致导致的 NaN - 快递费用显示优惠前金额,会员运费优惠单独展示
3381 lines
167 KiB
PHP
3381 lines
167 KiB
PHP
<?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 app\api\controller\v1\order;
|
||
|
||
use app\services\activity\coupon\StoreCouponUserServices;
|
||
use app\dao\order\OrderCenterDao;
|
||
use app\model\order\OrderCenter;
|
||
use app\services\order\OrderCenterUserServices;
|
||
use app\services\order\OrderCenterServices;
|
||
use app\services\order\OrderComputedServices;
|
||
use app\services\order\OrderOutServices;
|
||
use app\services\product\CenterProductServices;
|
||
use app\services\system\store\SystemStoreServices;
|
||
use app\services\system\SystemUserLevelServices;
|
||
use app\services\user\UserBillServices;
|
||
use app\services\user\UserAddressServices;
|
||
use app\services\user\UserServices;
|
||
use app\services\user\member\MemberCardServices;
|
||
use app\services\pay\PayServices;
|
||
use app\services\pay\PayRecordServices;
|
||
use app\services\pay\YuePayServices;
|
||
use app\services\pay\OrderPayServices;
|
||
use app\services\pay\PayCredentialService;
|
||
use app\services\driver\pay\PayChannelServices;
|
||
use app\services\order\OrderSuccessServices;
|
||
use crmeb\trade\product\ProductTypeRegistry;
|
||
use crmeb\trade\mode\TradeModeRegistry;
|
||
use app\services\activity\coupon\StoreCouponIssueServices;
|
||
use app\services\order\StoreCartServices;
|
||
use app\services\order\OrderInvoiceServices;
|
||
use app\services\order\AfterSaleServices;
|
||
use app\services\order\OrderTakeServices;
|
||
use app\services\product\product\StoreProductReplyServices;
|
||
use app\services\shipping\ExpressServices;
|
||
use crmeb\services\CacheService;
|
||
use think\facade\Log;
|
||
|
||
/**
|
||
* 中台订单用户端控制器
|
||
* 用户端:统一订单列表、订单确认、统一下单
|
||
*/
|
||
class CenterOrderController
|
||
{
|
||
/**
|
||
* 我的订单列表
|
||
* 从 eb_order_center 统一中台表读取
|
||
*/
|
||
public function list(OrderCenterUserServices $services)
|
||
{
|
||
$uid = (int) request()->uid();
|
||
// 状态筛选:接受语义化状态(pending_payment/pending_delivery/pending_use/pending_receipt/pending_comment/after_sale),
|
||
// 映射到实际 status_code,前端无需维护状态映射表
|
||
$semanticStatus = (string) request()->param('status', '');
|
||
$where = [
|
||
'order_category' => request()->param('order_category', ''),
|
||
'status_code' => $this->mapSemanticStatus($semanticStatus),
|
||
'product_type' => request()->param('product_type', ''),
|
||
];
|
||
// 待发货/待收货/待使用:均为 STATUS_PROCESSING,需按产品类型与发货状态细分
|
||
// 待发货=实物业态(normal)未发货;待收货=实物业态已发货未收货;待使用=非实物业态(virtual/cardkey/hotel/ticket 等)
|
||
if ($semanticStatus === 'pending_delivery') {
|
||
$where['product_type'] = 'normal';
|
||
$where['delivery_time'] = 0;
|
||
} elseif ($semanticStatus === 'pending_receipt') {
|
||
$where['product_type'] = 'normal';
|
||
$where['delivery_time_ne'] = 0;
|
||
$where['take_time'] = 0;
|
||
} elseif ($semanticStatus === 'pending_use') {
|
||
$where['product_type_ne'] = 'normal';
|
||
}
|
||
$page = (int) request()->param('page', 1);
|
||
$limit = (int) request()->param('limit', 10);
|
||
return app('json')->success($services->getList($uid, $where, $page, $limit));
|
||
}
|
||
|
||
/**
|
||
* 语义化状态映射到实际 status_code
|
||
* 前端传入语义化状态标识,后端映射到中台 status_code,支持逗号分隔多状态(如售后=refunding,refunded)
|
||
* 待发货(pending_delivery)/待收货(pending_receipt)/待使用(pending_use) 均映射到 STATUS_PROCESSING,
|
||
* 发货状态与产品类型过滤由 list 方法处理,此处不承担
|
||
* @param string $status 语义化状态标识
|
||
* @return string 返回 status_code,空表示不筛选(全部)
|
||
*/
|
||
protected function mapSemanticStatus(string $status): string
|
||
{
|
||
$map = [
|
||
'pending_payment' => OrderCenter::STATUS_PENDING_PAYMENT,
|
||
'pending_receipt' => OrderCenter::STATUS_PROCESSING,
|
||
// 待发货/待收货/待使用:均为进行中状态,产品类型与发货状态过滤交由 list 方法处理
|
||
'pending_delivery' => OrderCenter::STATUS_PROCESSING,
|
||
'pending_use' => OrderCenter::STATUS_PROCESSING,
|
||
// 待评价:履约完成待评价(评价后流转 completed;与统计接口 grouped.pending_comment 口径一致)
|
||
'pending_comment' => OrderCenter::STATUS_PENDING_REVIEW,
|
||
// 售后:退款中 + 已退款
|
||
'after_sale' => OrderCenter::STATUS_REFUNDING . ',' . OrderCenter::STATUS_REFUNDED,
|
||
];
|
||
return $map[$status] ?? '';
|
||
}
|
||
|
||
/**
|
||
* 我的订单统计
|
||
* 获取当前用户各状态订单数量和总金额
|
||
*/
|
||
public function stat(OrderCenterUserServices $services)
|
||
{
|
||
$uid = (int) request()->uid();
|
||
return app('json')->success($services->getUserStatistics($uid));
|
||
}
|
||
|
||
/**
|
||
* 订单详情
|
||
* v8.1 最终形态:接口只返回 {page_config: {sections, extensions}},渲染数据直填在 sections.props
|
||
* @param OrderCenterUserServices $services
|
||
* @param string $orderNo 订单号
|
||
* @return mixed
|
||
*/
|
||
public function detail(OrderCenterUserServices $services, string $orderNo)
|
||
{
|
||
$uid = (int) request()->uid();
|
||
try {
|
||
$ownedId = $services->getOwnedOrderIdByNo($orderNo, $uid);
|
||
if ($ownedId === null) {
|
||
return app('json')->fail('订单不存在');
|
||
}
|
||
// 详情首屏不同步请求第三方物流:只读轨迹缓存,未命中由前端走 order/express 异步补齐
|
||
$result = $services->getDetailWithConfig($ownedId, false);
|
||
// 移动端订单详情不返回 timeline section,避免接口数据冗余
|
||
if (!empty($result['page_config']['sections'])) {
|
||
$result['page_config']['sections'] = array_values(
|
||
array_filter($result['page_config']['sections'], function($s) {
|
||
return $s['key'] !== 'timeline';
|
||
})
|
||
);
|
||
}
|
||
// 送礼订单按查看者裁剪:购买者隐藏收货信息/物流/相关按钮,领取人保留只读物流能力
|
||
$result = $services->applyGiftViewerPolicy($result, $uid);
|
||
return app('json')->success($result);
|
||
} catch (\Throwable $e) {
|
||
return app('json')->fail($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 订单确认(展示商品信息、优惠券、积分、价格)
|
||
* 从中心商品直接购买,绕过购物车
|
||
*/
|
||
public function confirm()
|
||
{
|
||
try {
|
||
$uid = (int) request()->uid();
|
||
// 购物车模式:携带 cartId 时走多商品合并下单确认流程
|
||
$cartId = (string) request()->param('cartId', '');
|
||
if ($cartId) {
|
||
return $this->cartConfirm($uid);
|
||
}
|
||
$productId = (int) request()->param('product_id', 0);
|
||
$skuUnique = (string) request()->param('sku_unique', '');
|
||
$productType = (string) request()->param('product_type', 'normal');
|
||
$activityType = (string) request()->param('activity_type', 'normal');
|
||
$activityId = (int) request()->param('activity_id', 0);
|
||
$num = (int) request()->param('num', 1);
|
||
|
||
if (!$productId) {
|
||
return app('json')->fail('商品ID不能为空');
|
||
}
|
||
|
||
// 1. 获取交易模式处理器并校验交易规则
|
||
/** @var TradeModeRegistry $tradeModeRegistry */
|
||
$tradeModeRegistry = app()->make(TradeModeRegistry::class);
|
||
$tradeModeHandler = $tradeModeRegistry->get($activityType);
|
||
if ($activityType !== 'normal' && $activityId > 0) {
|
||
// 交易模式 Handler 统一校验:验证活动资格、限购规则
|
||
$tradeModeHandler->beforeCreateOrder($productId, $skuUnique, $num, $activityId, $uid);
|
||
$tradeModeHandler->validateOrder($productId, $skuUnique, $num, $activityId, $uid);
|
||
}
|
||
|
||
// 2. 获取基础数据并构建购物车信息
|
||
[$cartInfo, $priceData, $productInfo, $skuInfo, $registry]
|
||
= $this->buildOrderData($uid, $productId, $skuUnique, $productType, $num, $activityType, $activityId);
|
||
// 获取产品类型处理器(用于获取配送方式、自定义表单等)
|
||
$handler = $registry->get($productType);
|
||
$pricingPolicy = method_exists($handler, 'getPricingPolicy') ? $handler->getPricingPolicy() : [];
|
||
|
||
// 3. 缓存订单信息,返回 orderKey
|
||
/** @var OrderCenterUserServices $orderServices */
|
||
$orderServices = app()->make(OrderCenterUserServices::class);
|
||
$orderKey = $orderServices->cacheOrderInfo($uid, $cartInfo, $priceData, [
|
||
'product_type' => $productType,
|
||
'activity_type' => $activityType,
|
||
'activity_id' => $activityId,
|
||
'order_mode' => 'direct',
|
||
'integralRatio' => sys_config('integral_ratio', 0),
|
||
'pricing_policy' => $pricingPolicy,
|
||
], 600);
|
||
|
||
// 3. 获取用户信息
|
||
/** @var UserServices $userServices */
|
||
$userServices = app()->make(UserServices::class);
|
||
$userInfo = $userServices->getUserInfo($uid);
|
||
if (!$userInfo) {
|
||
return app('json')->fail('用户信息不存在');
|
||
}
|
||
if (is_object($userInfo)) $userInfo = $userInfo->toArray();
|
||
// 检查用户 VIP 等级
|
||
$levelServices = app()->make(\app\services\user\UserLevelServices::class);
|
||
$userLevel = $levelServices->getUerLevelInfoByUid($uid);
|
||
$userInfo['vip'] = $userLevel !== false;
|
||
if ($userInfo['vip']) {
|
||
$userInfo['vip_id'] = $userLevel['id'] ?? 0;
|
||
$userInfo['discount'] = $userLevel['discount'] ?? 0;
|
||
}
|
||
|
||
// 4. 获取用户可用积分
|
||
/** @var UserBillServices $userBillServices */
|
||
$userBillServices = app()->make(UserBillServices::class);
|
||
$usableIntegral = bcsub(
|
||
(string) ($userInfo['integral'] ?? 0),
|
||
(string) $userBillServices->getBillSum(['uid' => $uid, 'is_frozen' => 1]),
|
||
0
|
||
);
|
||
// 检查积分是否开启
|
||
$integralRatio = sys_config('integral_ratio', 0);
|
||
$integralOpen = (float) $integralRatio > 0 || $usableIntegral > 0;
|
||
|
||
// 活动订单不走优惠券自动匹配,标记前置(优惠券列表构建时使用)
|
||
$isActivityOrder = $activityType !== 'normal' && $activityId > 0;
|
||
|
||
// 5. 获取用户可用优惠券(无可用券时自动领取整单最优券并入列表,与 computed/create 自动用券同口径)
|
||
$couponList = $this->buildConfirmCouponList($uid, ['valid' => $cartInfo], $isActivityOrder, $pricingPolicy);
|
||
|
||
// 6. 获取商品详情扩展数据(自定义表单、商品参数)
|
||
$detailExtend = $handler->getDetailExtend($productId);
|
||
|
||
// 转换自定义表单格式,适配前端期望结构
|
||
$customForm = $detailExtend['custom_form'] ?? [];
|
||
$formattedCustomForm = [];
|
||
foreach ($customForm as $key => $item) {
|
||
$formattedCustomForm[] = [
|
||
'label' => $item['title'] ?? '',
|
||
'field' => 'custom_field_' . $key,
|
||
'type' => $item['label'] === 'text' ? 'input' : ($item['label'] === 'textarea' ? 'textarea' : $item['label']),
|
||
'required' => !empty($item['status']),
|
||
];
|
||
}
|
||
|
||
// 功能开关配置,由后端控制前端展示哪些功能
|
||
// 活动订单默认禁用优惠券和积分抵扣,具体由交易模式处理器控制
|
||
$activityAllowedPayTypes = [];
|
||
$activityExtraInfo = [];
|
||
$freePayType = '';
|
||
$payRedirectUrl = '';
|
||
// 活动订单信息
|
||
if ($isActivityOrder) {
|
||
$activityAllowedPayTypes = $tradeModeHandler->getAllowedPayTypes($activityId);
|
||
$activityExtraInfo = $tradeModeHandler->getOrderExtraInfo($activityId);
|
||
// 免费支付场景(如积分兑换):下发支付方式标识与跳转地址,前端不再硬编码
|
||
if ($tradeModeHandler->isFreePay($activityId)) {
|
||
$freePayType = $tradeModeHandler->getFreePayType($activityId);
|
||
$payRedirectUrl = $tradeModeHandler->getPayRedirectUrl($activityId);
|
||
}
|
||
}
|
||
// 从商品中心表读取商品级物流配置
|
||
// 从产品信息中读取配送能力
|
||
$expressShipping = $productInfo['express_shipping'] ?? false;
|
||
$storePickup = $productInfo['store_pickup'] ?? false;
|
||
$doorService = $productInfo['door_service'] ?? false;
|
||
// 上门费用:从商品 freight_config.door_service.postage 读取
|
||
$doorServicePostage = (float)($productInfo['door_service_postage'] ?? 0);
|
||
// 功能开关
|
||
$features = [
|
||
'express_shipping' => $expressShipping, // 是否支持快递配送
|
||
'store_pickup' => $storePickup, // 是否支持到店自提
|
||
'door_service' => $doorService, // 是否支持上门服务
|
||
'door_service_postage' => $doorServicePostage, // 上门服务费用(元)
|
||
'remark' => true, // 买家留言,与配送方式无关
|
||
'invoice' => (bool)sys_config('invoice_func_status', 0), // 发票,与商品类型无关
|
||
'integral' => !$isActivityOrder && $integralOpen, // 积分抵扣,与商品类型无关
|
||
'coupon' => !$isActivityOrder && !empty($couponList), // 优惠券,与商品类型无关
|
||
'custom_form' => !empty($formattedCustomForm), // 自定义表单,与商品类型无关
|
||
'params_list' => !empty($detailExtend['params_list'] ?? []), // 商品参数,与商品类型无关
|
||
'visitor_form' => !empty($detailExtend['visitor_form_config'] ?? []), // 游客信息表单(门票专属,按数量倍增)
|
||
];
|
||
|
||
$data = [
|
||
'order_key' => $orderKey,// 订单号
|
||
'product_type' => $productType,// 商品类型
|
||
'activity_type' => $activityType,// 活动类型
|
||
'activity_id' => $activityId,// 活动ID
|
||
'features' => $features,// 功能开关配置
|
||
'cartInfo' => $cartInfo,// 购物车信息
|
||
'total_price' => $priceData['totalPrice'],// 订单总金额
|
||
'pay_price' => $priceData['payPrice'],// 应付金额
|
||
'pay_postage' => $priceData['payPostage'],// 配送费
|
||
'priceGroup' => [
|
||
'totalPrice' => $priceData['totalPrice'],// 订单总金额(等级/会员优惠后)
|
||
'sumPrice' => $priceData['sumPrice'] ?? $priceData['totalPrice'],// 商品原价总额(等级/会员优惠前)
|
||
'payPostage' => $priceData['payPostage'],// 配送费
|
||
'postageDiscount' => 0,// 配送费优惠
|
||
'levelPrice' => $priceData['levelPrice'] ?? 0,// 等级优惠
|
||
'memberPrice' => $priceData['memberPrice'] ?? 0,// 付费会员优惠
|
||
'couponPrice' => 0,// 优惠券优惠
|
||
'deductionPrice' => 0,// 积分抵扣
|
||
'giftPrice' => 0,// 礼品优惠
|
||
],
|
||
'userInfo' => $userInfo,// 用户信息
|
||
'usable_integral' => $usableIntegral,// 可用积分
|
||
'integral_open' => $integralOpen,// 是否开启积分抵扣
|
||
'integral_ratio' => $integralRatio,// 积分抵扣比例
|
||
'coupon_list' => $couponList,// 优惠券列表
|
||
'custom_form' => $formattedCustomForm,// 自定义表单
|
||
'params_list' => $detailExtend['params_list'] ?? [],// 商品参数
|
||
'visitor_form_config' => $detailExtend['visitor_form_config'] ?? [],// 游客信息表单配置(门票专属)
|
||
// 活动订单额外信息
|
||
'activity_info' => $isActivityOrder ? array_merge(
|
||
$activityExtraInfo,// 活动订单额外信息
|
||
[
|
||
'allowed_pay_types' => $activityAllowedPayTypes,// 允许支付方式
|
||
'free_pay_type' => $freePayType,// 免费支付方式标识(空表示需走收银台)
|
||
'pay_redirect_url' => $payRedirectUrl,// 免费支付完成后跳转地址(含 {order_no} 占位符)
|
||
]
|
||
) : [],
|
||
];
|
||
|
||
return app('json')->success($data);
|
||
} catch (\Throwable $e) {
|
||
return app('json')->fail($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 购物车模式订单确认
|
||
* 多商品合并下单:校验所有商品产品类型一致(不同产品类型提示分开下单),
|
||
* 汇总价格并缓存,响应结构与单品直购模式保持一致
|
||
* @param int $uid 用户ID
|
||
* @return mixed
|
||
*/
|
||
protected function cartConfirm(int $uid)
|
||
{
|
||
$cartId = (string) request()->param('cartId', '');
|
||
$new = (bool) request()->param('new', false);
|
||
$addressId = (int) request()->param('address_id', 0);
|
||
$shippingType = (int) request()->param('shipping_type', 1);
|
||
$isGift = (int) request()->param('is_gift', 0);
|
||
|
||
if (!is_string($cartId) || !$cartId) {
|
||
return app('json')->fail('请提交购买的商品');
|
||
}
|
||
|
||
// 获取收货地址(用于运费模板不送达校验;自提/礼品场景无需地址)
|
||
$addr = [];
|
||
if ($shippingType != 2 && $isGift != 1) {
|
||
/** @var UserAddressServices $addressServices */
|
||
$addressServices = app()->make(UserAddressServices::class);
|
||
if ($addressId) {
|
||
$addressInfo = $addressServices->getOne(['uid' => $uid, 'id' => $addressId, 'is_del' => 0]);
|
||
if ($addressInfo) {
|
||
$addr = $addressInfo->toArray();
|
||
}
|
||
}
|
||
// 未传地址或地址已失效时,回退取默认地址
|
||
if (!$addr) {
|
||
$defaultAddr = $addressServices->getUserDefaultAddress($uid);
|
||
if ($defaultAddr) {
|
||
$addr = $defaultAddr->toArray();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取购物车数据(含价格计算、有效/无效分离、不送达校验)
|
||
/** @var StoreCartServices $cartServices */
|
||
$cartServices = app()->make(StoreCartServices::class);
|
||
$cartGroup = $cartServices->getUserProductCartListV1($uid, $cartId, $new, $addr, $shippingType, $isGift);
|
||
$validCartInfo = $cartGroup['valid'] ?? [];
|
||
if (empty($validCartInfo)) {
|
||
return app('json')->fail('暂无可购买商品,请重新选择');
|
||
}
|
||
// 合单商品类型已在前置校验中确认一致,取首件类型获取对应处理器。
|
||
$productType = (string)($validCartInfo[0]['product_type'] ?? $validCartInfo[0]['productInfo']['product_type'] ?? 'normal');
|
||
|
||
// 校验所有商品产品类型一致:相同产品类型可合并下单,不同产品类型提示分开下单
|
||
$productTypes = array_values(array_unique(array_map(function ($item) {
|
||
return (string)($item['productInfo']['product_type'] ?? 'normal');
|
||
}, $validCartInfo)));
|
||
if (count($productTypes) > 1) {
|
||
return app('json')->fail('不同产品类型的商品请分开下单');
|
||
}
|
||
$productType = $productTypes[0];
|
||
|
||
// 计算价格组(含运费模板运费、会员运费折扣),与旧版确认页结构一致
|
||
/** @var OrderComputedServices $computedServices */
|
||
$computedServices = app()->make(OrderComputedServices::class);
|
||
$storeFreePostage = (float)sys_config('store_free_postage') ?: 0;
|
||
$userInfo = app()->make(UserServices::class)->getUserInfo($uid);
|
||
if (!$userInfo) {
|
||
return app('json')->fail('用户信息不存在');
|
||
}
|
||
if (is_object($userInfo)) $userInfo = $userInfo->toArray();
|
||
$priceGroup = $computedServices->getOrderPriceGroup($storeFreePostage, $validCartInfo, $addr, $userInfo, $shippingType, $isGift);
|
||
$validCartInfo = $priceGroup['cartInfo'] ?? $validCartInfo;
|
||
|
||
// 组装与单品直购模式一致的 priceData,供 computed/create 读取
|
||
// 送礼订单:应付价 = 商品总价 + 礼品附加费(无运费)
|
||
$giftPrice = $priceGroup['giftPrice'] ?? 0;
|
||
$priceData = [
|
||
'totalPrice' => $priceGroup['totalPrice'] ?? 0,
|
||
// 原价总额:商品规格原价 × 数量之和,供确认页“商品总价”展示
|
||
'sumPrice' => $priceGroup['sumPrice'] ?? 0,
|
||
'payPrice' => bcadd(
|
||
(string)bcadd((string)($priceGroup['totalPrice'] ?? 0), (string)($priceGroup['storePostage'] ?? 0), 2),
|
||
(string)$giftPrice,
|
||
2
|
||
),
|
||
'payPostage' => $priceGroup['storePostage'] ?? 0,
|
||
'isStoreFreePostage' => $priceGroup['isStoreFreePostage'] ?? false,
|
||
'storeFreePostage' => $priceGroup['storeFreePostage'] ?? 0,
|
||
'costPrice' => $priceGroup['costPrice'] ?? 0,
|
||
'giftPrice' => $giftPrice,
|
||
'levelPrice' => $priceGroup['levelPrice'] ?? 0,
|
||
'memberPrice' => $priceGroup['memberPrice'] ?? 0,
|
||
];
|
||
/** @var ProductTypeRegistry $productTypeRegistry */
|
||
$productTypeRegistry = app()->make(ProductTypeRegistry::class);
|
||
$handler = $productTypeRegistry->get($productType);
|
||
|
||
// 缓存订单信息,返回 orderKey(create 阶段据此读取购物车数据)
|
||
/** @var OrderCenterUserServices $orderServices */
|
||
$orderServices = app()->make(OrderCenterUserServices::class);
|
||
$orderKey = $orderServices->cacheOrderInfo($uid, $validCartInfo, $priceData, [
|
||
'product_type' => $productType,
|
||
'activity_type' => 'normal',
|
||
'activity_id' => 0,
|
||
'order_mode' => 'cart',
|
||
'cart_ids' => $cartId, // 下单成功后清除购物车
|
||
'new' => $new, // 是否缓存key模式(立即购买)
|
||
'deduction' => $cartGroup['deduction'] ?? [],
|
||
'integralRatio' => sys_config('integral_ratio', 0),
|
||
'pricing_policy' => method_exists($handler, 'getPricingPolicy') ? $handler->getPricingPolicy() : [],
|
||
], 600);
|
||
|
||
// 用户 VIP 等级信息
|
||
$levelServices = app()->make(\app\services\user\UserLevelServices::class);
|
||
$userLevel = $levelServices->getUerLevelInfoByUid($uid);
|
||
$userInfo['vip'] = $userLevel !== false;
|
||
if ($userInfo['vip']) {
|
||
$userInfo['vip_id'] = $userLevel['id'] ?? 0;
|
||
$userInfo['discount'] = $userLevel['discount'] ?? 0;
|
||
}
|
||
|
||
// 可用积分
|
||
$usableIntegral = bcsub(
|
||
(string)($userInfo['integral'] ?? 0),
|
||
(string)app()->make(UserBillServices::class)->getBillSum(['uid' => $uid, 'is_frozen' => 1]),
|
||
0
|
||
);
|
||
$integralRatio = sys_config('integral_ratio', 0);
|
||
$integralOpen = (float)$integralRatio > 0 || $usableIntegral > 0;
|
||
|
||
// 可用优惠券(无可用券时自动领取整单最优券并入列表,保证与 computed 的自动选券结果一致)
|
||
$couponList = $this->buildConfirmCouponList(
|
||
$uid,
|
||
$cartGroup,
|
||
false,
|
||
method_exists($handler, 'getPricingPolicy') ? $handler->getPricingPolicy() : []
|
||
);
|
||
|
||
// 支付渠道状态
|
||
/** @var PayChannelServices $payChannelServices */
|
||
$payChannelServices = app()->make(PayChannelServices::class);
|
||
|
||
// 门店自提是否开启(系统配置开启且存在门店)
|
||
$storeSelfMention = (int)sys_config('store_self_mention');
|
||
if ($storeSelfMention) {
|
||
/** @var SystemStoreServices $storeServices */
|
||
$storeServices = app()->make(SystemStoreServices::class);
|
||
$storeCount = $storeServices->count(['type' => 0]);
|
||
$storeSelfMention = $storeSelfMention && $storeCount;
|
||
}
|
||
|
||
// 发票开关
|
||
$invoiceFunc = app()->make(\app\services\user\UserInvoiceServices::class)->invoiceFuncStatus();
|
||
|
||
// 配送方式:合单需所有商品统一支持该方式,从商品 delivery_methods 聚合(与单品直购 confirm 的 express_shipping/store_pickup/door_service 语义一致)
|
||
$expressShipping = true;
|
||
$storePickupAll = true;
|
||
$doorServiceAll = true;
|
||
foreach ($validCartInfo as $item) {
|
||
$methods = $item['productInfo']['delivery_methods'] ?? [];
|
||
if (is_string($methods)) {
|
||
$methods = json_decode($methods, true) ?: [];
|
||
}
|
||
$methods = is_array($methods) ? $methods : [];
|
||
if (!in_array('express', $methods, true)) {
|
||
$expressShipping = false;
|
||
}
|
||
if (!in_array('pickup', $methods, true)) {
|
||
$storePickupAll = false;
|
||
}
|
||
if (!in_array('door_service', $methods, true)) {
|
||
$doorServiceAll = false;
|
||
}
|
||
}
|
||
|
||
// 上门费用:取第一个支持上门服务商品的上门费用(购物车模式下按首商品计费)
|
||
$doorServicePostage = 0.00;
|
||
foreach ($validCartInfo as $item) {
|
||
if (!empty($item['productInfo']['door_service_postage'])) {
|
||
$doorServicePostage = (float)$item['productInfo']['door_service_postage'];
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 自定义表单:合单只能填一份表单,取主商品(首项)定义
|
||
// 与 create 阶段取首项 productId 的逻辑保持一致,确保前后端字段映射统一
|
||
$cartCustomForm = [];
|
||
$cartVisitorFormConfig = [];
|
||
try {
|
||
/** @var ProductTypeRegistry $registry */
|
||
$registry = app()->make(ProductTypeRegistry::class);
|
||
$handler = $registry->get($productType);
|
||
$cartProductId = (int)($validCartInfo[0]['product_id'] ?? $validCartInfo[0]['productInfo']['id'] ?? 0);
|
||
if ($cartProductId && method_exists($handler, 'getDetailExtend')) {
|
||
$detailExtend = $handler->getDetailExtend($cartProductId);
|
||
// 转换自定义表单格式,适配前端期望结构(与单品直购 confirm 一致)
|
||
$rawCustomForm = $detailExtend['custom_form'] ?? [];
|
||
foreach ($rawCustomForm as $key => $item) {
|
||
$label = $item['label'] ?? '';
|
||
$cartCustomForm[] = [
|
||
'label' => $item['title'] ?? '',
|
||
'field' => 'custom_field_' . $key,
|
||
'type' => $label === 'text' ? 'input' : ($label === 'textarea' ? 'textarea' : $label),
|
||
'required' => !empty($item['status']),
|
||
];
|
||
}
|
||
// 游客信息表单配置(门票专属,取主商品定义)
|
||
$cartVisitorFormConfig = $detailExtend['visitor_form_config'] ?? [];
|
||
}
|
||
} catch (\Throwable $e) {
|
||
// 获取自定义表单失败不影响主流程
|
||
$cartCustomForm = [];
|
||
$cartVisitorFormConfig = [];
|
||
}
|
||
|
||
// 功能开关配置
|
||
// 送礼订单:收货信息由领取人填写,关闭配送/自提/上门入口
|
||
$features = [
|
||
'express_shipping' => $isGift != 1 && $expressShipping, // 是否支持快递配送(所有商品均支持快递)
|
||
'store_pickup' => $isGift != 1 && (bool)$storeSelfMention && $storePickupAll,// 是否支持到店自提(系统开启且所有商品均支持自提)
|
||
'door_service' => $isGift != 1 && $doorServiceAll, // 是否支持上门服务(所有商品均支持上门)
|
||
'door_service_postage' => $doorServicePostage, // 上门服务费用(元)
|
||
'remark' => true, // 买家留言
|
||
'invoice' => (bool)sys_config('invoice_func_status', 0),// 发票
|
||
'integral' => $integralOpen, // 积分抵扣
|
||
'coupon' => !empty($couponList), // 优惠券
|
||
'custom_form' => !empty($cartCustomForm), // 自定义表单(取主商品定义)
|
||
'params_list' => false, // 商品参数(下单页不展示)
|
||
'visitor_form' => !empty($cartVisitorFormConfig), // 游客信息表单(门票专属,取主商品定义)
|
||
'gift' => $isGift == 1, // 送礼订单标记
|
||
'gift_mark' => true, // 送礼留言输入
|
||
];
|
||
|
||
$deduction = $cartGroup['deduction'] ?? [];
|
||
$data = [
|
||
'order_key' => $orderKey,// 订单缓存key
|
||
'product_type' => $productType,// 商品类型
|
||
'activity_type' => 'normal',// 活动类型
|
||
'activity_id' => 0,// 活动ID
|
||
'features' => $features,// 功能开关配置
|
||
'cartInfo' => $validCartInfo,// 有效购物车商品(失效商品不参与合单展示与计价)
|
||
'valid_count' => count($validCartInfo),// 有效商品数量
|
||
'total_price' => $priceGroup['totalPrice'] ?? 0,// 订单总金额
|
||
'pay_price' => $priceData['payPrice'],// 应付金额
|
||
'pay_postage' => $priceGroup['storePostage'] ?? 0,// 配送费
|
||
'priceGroup' => [
|
||
'totalPrice' => $priceGroup['totalPrice'] ?? 0,// 订单总金额(等级/会员优惠后)
|
||
'sumPrice' => $priceGroup['sumPrice'] ?? 0,// 商品原价总额(等级/会员优惠前)
|
||
'payPostage' => $priceGroup['storePostage'] ?? 0,// 配送费
|
||
'postageDiscount' => $priceGroup['storePostageDiscount'] ?? 0,// 配送费优惠
|
||
'levelPrice' => $priceGroup['levelPrice'] ?? 0,// 等级优惠
|
||
'memberPrice' => $priceGroup['memberPrice'] ?? 0,// 付费会员优惠
|
||
'couponPrice' => 0,// 优惠券优惠
|
||
'deductionPrice' => 0,// 积分抵扣
|
||
'giftPrice' => $priceGroup['giftPrice'] ?? 0,// 礼品优惠
|
||
],
|
||
'userInfo' => $userInfo,// 用户信息
|
||
'usable_integral' => $usableIntegral,// 可用积分
|
||
'integral_open' => $integralOpen,// 是否开启积分抵扣
|
||
'integral_ratio' => $integralRatio,// 积分抵扣比例
|
||
'integralRatio' => $integralRatio,// 兼容旧字段名
|
||
'coupon_list' => $couponList,// 优惠券列表
|
||
'custom_form' => $cartCustomForm,// 自定义表单(取主商品定义)
|
||
'params_list' => [],// 商品参数(下单页不展示)
|
||
'visitor_form_config' => $cartVisitorFormConfig,// 游客信息表单配置(门票专属,取主商品定义)
|
||
'virtual_type' => (($handler->getCapabilities()['order']['virtual'] ?? false) === true) ? 1 : 0,// 是否虚拟商品
|
||
'store_self_mention' => (int)$storeSelfMention,// 门店自提开关
|
||
'seckill_id' => $deduction['seckill_id'] ?? 0,// 秒杀活动ID
|
||
'combination_id' => $deduction['combination_id'] ?? 0,// 拼团活动ID
|
||
'bargain_id' => $deduction['bargain_id'] ?? 0,// 砍价活动ID
|
||
'advance_id' => $deduction['advance_id'] ?? 0,// 预售活动ID
|
||
'deduction' => !empty(array_filter($deduction)),// 是否含活动商品
|
||
'offlinePostage' => sys_config('offline_postage'),// 线下配送费
|
||
'offline_pay_status' => $payChannelServices->getChannelStatus('offline') ? 1 : 2,// 线下支付开关
|
||
'yue_pay_status' => $payChannelServices->getChannelStatus('yue') ? 1 : 2,// 余额支付渠道开关
|
||
'pay_weixin_open' => $payChannelServices->getChannelStatus('weixin'),// 微信支付开关
|
||
'ali_pay_status' => $payChannelServices->getChannelStatus('alipay'),// 支付宝开关
|
||
'friend_pay_status' => $payChannelServices->getChannelStatus('friend') ? 1 : 0,// 好友代付开关
|
||
'invoice_func' => $invoiceFunc['invoice_func'],// 发票功能开关
|
||
'special_invoice' => $invoiceFunc['special_invoice'],// 专票开关
|
||
'activity_info' => [],// 活动信息(购物车模式无活动)
|
||
];
|
||
|
||
return app('json')->success($data);
|
||
}
|
||
|
||
/**
|
||
* 构建确认页可用优惠券列表
|
||
* 用户已有可用券时直接返回;没有可用券时自动领取整单最优券并补入列表(预览即真实领取)。
|
||
* 与 computed/create 的自动用券同口径,保证 features.coupon、coupon_list 与后续自动选券结果一致,
|
||
* 避免确认页显示已用券但优惠券弹窗列表为空。
|
||
* @param int $uid 用户ID
|
||
* @param array $cartGroup 购物车分组(须含 valid 商品条目)
|
||
* @param bool $isActivityOrder 活动订单不参与优惠券
|
||
* @param array $pricingPolicy 产品类型计价策略,coupon=false 时不参与优惠券
|
||
* @return array
|
||
*/
|
||
protected function buildConfirmCouponList(int $uid, array $cartGroup, bool $isActivityOrder, array $pricingPolicy = []): array
|
||
{
|
||
if ($isActivityOrder || ($pricingPolicy['coupon'] ?? true) !== true) {
|
||
return [];
|
||
}
|
||
/** @var StoreCouponUserServices $couponServices */
|
||
$couponServices = app()->make(StoreCouponUserServices::class);
|
||
$couponList = [];
|
||
try {
|
||
$couponList = $couponServices->getUsableCouponList($uid, $cartGroup);
|
||
} catch (\Throwable $e) {
|
||
// 优惠券列表获取失败不影响确认页主流程
|
||
$couponList = [];
|
||
}
|
||
if (!empty($couponList)) {
|
||
return $couponList;
|
||
}
|
||
// 用户尚无可用券:自动领取整单最优优惠券,行结构与 getUsableCouponList 保持一致
|
||
try {
|
||
/** @var OrderComputedServices $computedServices */
|
||
$computedServices = app()->make(OrderComputedServices::class);
|
||
$autoCoupon = $computedServices->autoMatchCoupon($uid, $cartGroup['valid'] ?? [], true);
|
||
$autoRow = $couponServices->getUsableCouponRow($uid, (int)($autoCoupon['coupon_user_id'] ?? 0));
|
||
if ($autoRow) {
|
||
$couponList[] = $autoRow;
|
||
}
|
||
} catch (\Throwable $e) {
|
||
// 自动领券失败不影响确认页主流程
|
||
}
|
||
return $couponList;
|
||
}
|
||
|
||
/**
|
||
* 计算订单金额(切换优惠券/积分时重算)
|
||
*/
|
||
public function computed()
|
||
{
|
||
try {
|
||
$uid = (int) request()->uid();
|
||
$key = (string) request()->param('key', '');
|
||
$couponId = (int) request()->param('coupon_id', 0);
|
||
$useIntegral = (bool) request()->param('use_integral', false);
|
||
$addressId = (int) request()->param('address_id', 0);
|
||
$deliveryType = $this->normalizeOrderDeliveryType((string) request()->param('delivery_type', ''));
|
||
$storeId = (int) request()->param('store_id', 0);
|
||
// 送礼订单:不选地址、不计运费,累加礼品附加费
|
||
$isGift = (int) request()->param('is_gift', 0);
|
||
|
||
if (!$key) {
|
||
return app('json')->fail('参数错误');
|
||
}
|
||
|
||
/** @var OrderCenterUserServices $orderServices */
|
||
$orderServices = app()->make(OrderCenterUserServices::class);
|
||
$cartGroup = $orderServices->getCacheOrderInfo($uid, $key);
|
||
if (!$cartGroup) {
|
||
return app('json')->fail('订单已过期,请刷新页面');
|
||
}
|
||
// 价格能力以商品类型 Handler 的后端声明为准,不能信任客户端参数。
|
||
$productType = (string)($cartGroup['other']['product_type'] ?? $cartGroup['product_type'] ?? 'normal');
|
||
$handler = app()->make(ProductTypeRegistry::class)->get($productType);
|
||
$pricingPolicy = method_exists($handler, 'getPricingPolicy') ? $handler->getPricingPolicy() : [];
|
||
if (($pricingPolicy['integral'] ?? true) !== true) {
|
||
$useIntegral = false;
|
||
}
|
||
if (($pricingPolicy['coupon'] ?? true) !== true) {
|
||
$couponId = 0;
|
||
}
|
||
$isActivityOrder = (($cartGroup['activity_type'] ?? 'normal') !== 'normal')
|
||
&& (int)($cartGroup['activity_id'] ?? 0) > 0;
|
||
$cartInfo = $cartGroup['cartInfo'] ?? [];
|
||
|
||
// 重算金额必须使用当前用户上下文,积分抵扣和会员价格都依赖用户信息。
|
||
/** @var UserServices $userServices */
|
||
$userServices = app()->make(UserServices::class);
|
||
$userInfo = $userServices->getUserInfo($uid);
|
||
if (!$userInfo) {
|
||
return app('json')->fail('用户信息不存在');
|
||
}
|
||
if (is_object($userInfo)) {
|
||
$userInfo = $userInfo->toArray();
|
||
}
|
||
/** @var \app\services\user\UserLevelServices $levelServices */
|
||
$levelServices = app()->make(\app\services\user\UserLevelServices::class);
|
||
$userLevel = $levelServices->getUerLevelInfoByUid($uid);
|
||
$userInfo['vip'] = $userLevel !== false;
|
||
if ($userInfo['vip']) {
|
||
$userInfo['vip_id'] = $userLevel['id'] ?? 0;
|
||
$userInfo['discount'] = $userLevel['discount'] ?? 0;
|
||
}
|
||
|
||
// 普通商品预览也必须重新读取当前商品、SKU和用户价格,不能使用确认页旧快照。
|
||
$shippingType = $deliveryType === 'pickup' ? 2 : ($deliveryType === 'door' ? 3 : 1);
|
||
$hasCartActivity = false;
|
||
foreach ($cartInfo as $cartItem) {
|
||
if (!empty($cartItem['seckill_id'])
|
||
|| !empty($cartItem['bargain_id'])
|
||
|| !empty($cartItem['combination_id'])
|
||
|| !empty($cartItem['advance_id'])) {
|
||
$hasCartActivity = true;
|
||
break;
|
||
}
|
||
}
|
||
if ($productType === 'normal' && !$isActivityOrder && !$hasCartActivity) {
|
||
// 先取当前地址用于重建,配送能力校验放到重建后,避免旧缓存的能力字段阻断当前商品。
|
||
$rebuildAddressInfo = [];
|
||
if ($deliveryType === 'delivery' && $addressId > 0) {
|
||
/** @var UserAddressServices $addressServices */
|
||
$addressServices = app()->make(UserAddressServices::class);
|
||
$rebuildAddressInfo = $addressServices->getOne([
|
||
'uid' => $uid,
|
||
'id' => $addressId,
|
||
'is_del' => 0,
|
||
]);
|
||
if (is_object($rebuildAddressInfo)) {
|
||
$rebuildAddressInfo = $rebuildAddressInfo->toArray();
|
||
}
|
||
$rebuildAddressInfo = is_array($rebuildAddressInfo) ? $rebuildAddressInfo : [];
|
||
}
|
||
/** @var StoreCartServices $cartServices */
|
||
$cartServices = app()->make(StoreCartServices::class);
|
||
$freshCartGroup = $cartServices->rebuildNormalCheckoutCart(
|
||
$uid,
|
||
$cartInfo,
|
||
$rebuildAddressInfo,
|
||
$shippingType,
|
||
$isGift
|
||
);
|
||
$cartInfo = $freshCartGroup['valid'] ?? [];
|
||
if (empty($cartInfo)) {
|
||
return app('json')->fail('商品已失效,请返回确认页刷新');
|
||
}
|
||
if (!empty($freshCartGroup['invalid'])) {
|
||
return app('json')->fail('商品库存或配送条件已变化,请返回确认页刷新');
|
||
}
|
||
// 送礼订单由领取人填收货信息,下单阶段不校验配送地址
|
||
$deliveryContext = ['address_info' => [], 'store_info' => []];
|
||
if ($isGift != 1) {
|
||
// 商品信息可能已变化,必须以重建后的商品能力再次校验配送方式。
|
||
$deliveryContext = $this->validateNormalOrderDelivery(
|
||
$cartInfo,
|
||
$deliveryType,
|
||
$uid,
|
||
$addressId,
|
||
$storeId,
|
||
[],
|
||
false
|
||
);
|
||
}
|
||
/** @var OrderComputedServices $computedServices */
|
||
$computedServices = app()->make(OrderComputedServices::class);
|
||
$priceGroup = $computedServices->getOrderPriceGroup(
|
||
(float)sys_config('store_free_postage') ?: 0,
|
||
$cartInfo,
|
||
$deliveryContext['address_info'],
|
||
$userInfo,
|
||
$shippingType,
|
||
$isGift
|
||
);
|
||
$cartGroup['cartInfo'] = $cartInfo;
|
||
$cartGroup['priceData'] = [
|
||
'totalPrice' => $priceGroup['totalPrice'] ?? 0,
|
||
'payPrice' => bcadd((string)($priceGroup['totalPrice'] ?? 0), (string)($priceGroup['storePostage'] ?? 0), 2),
|
||
'payPostage' => $priceGroup['storePostage'] ?? 0,
|
||
'isStoreFreePostage' => $priceGroup['isStoreFreePostage'] ?? false,
|
||
'storeFreePostage' => $priceGroup['storeFreePostage'] ?? 0,
|
||
'costPrice' => $priceGroup['costPrice'] ?? 0,
|
||
'giftPrice' => $priceGroup['giftPrice'] ?? 0,
|
||
'levelPrice' => $priceGroup['levelPrice'] ?? 0,
|
||
'memberPrice' => $priceGroup['memberPrice'] ?? 0,
|
||
];
|
||
}
|
||
|
||
// 用户未选券时自动领取并使用整单最优优惠券(折扣对应真实领取的券,与商品详情到手价口径一致;失败降级为无券)
|
||
if (!$couponId && !$isActivityOrder && ($pricingPolicy['coupon'] ?? true)) {
|
||
/** @var OrderComputedServices $computedServices */
|
||
$computedServices = app()->make(OrderComputedServices::class);
|
||
$autoCoupon = $computedServices->autoMatchCoupon($uid, $cartInfo, true);
|
||
if (!empty($autoCoupon['coupon_user_id'])) {
|
||
$couponId = (int)$autoCoupon['coupon_user_id'];
|
||
}
|
||
}
|
||
|
||
// 手动校验并计算优惠券金额(不依赖 useCouponId 的 getOne 查询)
|
||
$couponPrice = 0;
|
||
$couponTitle = '';
|
||
if ($couponId) {
|
||
/** @var \app\services\activity\coupon\StoreCouponUserServices $couponServices */
|
||
$couponServices = app()->make(\app\services\activity\coupon\StoreCouponUserServices::class);
|
||
// 通过主键查询,预加载 issue 关联以获取 applicable_type
|
||
$couponInfo = $couponServices->get($couponId, ['*'], ['issue']);
|
||
if (!$couponInfo) {
|
||
return app('json')->fail('选择的优惠劵无效');
|
||
}
|
||
$couponTitle = (string)$couponInfo['coupon_title'];
|
||
// 使用 getData 获取原始值,避免 Model 访问器转换(status→"未使用"、type→"后台发放"等)
|
||
$rawStatus = $couponInfo->getData('status');
|
||
$rawIsFail = $couponInfo->getData('is_fail');
|
||
if ($rawIsFail != 0 || $rawStatus != 0) {
|
||
return app('json')->fail('选择的优惠劵无效');
|
||
}
|
||
if ($couponInfo['uid'] != $uid) {
|
||
return app('json')->fail('选择的优惠劵无效');
|
||
}
|
||
if ($couponInfo['start_time'] > time()) {
|
||
return app('json')->fail('选择的优惠劵无效');
|
||
}
|
||
if ($couponInfo['end_time'] < time()) {
|
||
return app('json')->fail('选择的优惠劵无效');
|
||
}
|
||
// 计算适用商品价格
|
||
$price = '0';
|
||
$count = 0;
|
||
// 使用 getData 获取原始 type 值(避免 getTypeAttr 访问器将整数转为中文)
|
||
$type = $couponInfo['applicable_type'] ?? $couponInfo->getData('type') ?? 0;
|
||
switch ($type) {
|
||
case 0:
|
||
case 3:
|
||
foreach ($cartInfo as $cart) {
|
||
$price = bcadd($price, bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2);
|
||
$count++;
|
||
}
|
||
break;
|
||
case 1:
|
||
$storeCategoryServices = app()->make(\app\dao\product\product\StoreCategoryDao::class);
|
||
$coupon_category = explode(',', (string)$couponInfo['category_id']);
|
||
$category_ids = $storeCategoryServices->getAllById($coupon_category);
|
||
if ($category_ids) {
|
||
$cateIds = array_column($category_ids, 'id');
|
||
foreach ($cartInfo as $cart) {
|
||
if (isset($cart['productInfo']['cate_id']) && array_intersect(explode(',', $cart['productInfo']['cate_id']), $cateIds)) {
|
||
$price = bcadd($price, bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2);
|
||
$count++;
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case 2:
|
||
foreach ($cartInfo as $cart) {
|
||
if (isset($cart['product_id']) && in_array($cart['product_id'], explode(',', $couponInfo['product_id']))) {
|
||
$price = bcadd($price, bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2);
|
||
$count++;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
if ($count && $couponInfo['use_min_price'] <= $price) {
|
||
$couponPrice = min($couponInfo['coupon_price'], $price);
|
||
} else {
|
||
return app('json')->fail('不满足优惠劵的使用条件');
|
||
}
|
||
}
|
||
|
||
// 缓存数据使用 priceData 作为 key,但 computedOrder 需要 priceGroup,做结构归一化
|
||
$modifiedGroup = $cartGroup;
|
||
$modifiedGroup['priceGroup'] = $cartGroup['priceData'] ?? [];
|
||
$modifiedGroup['other'] = [
|
||
'integralRatio' => $cartGroup['integralRatio'] ?? sys_config('integral_ratio', 0),
|
||
'pricing_policy' => $pricingPolicy,
|
||
];
|
||
// 记录原始总价,供结果修正使用
|
||
$originalTotalPrice = $modifiedGroup['priceGroup']['totalPrice'] ?? 0;
|
||
|
||
// 将优惠券金额从总价中扣除,再传给 computedOrder(不传 coupon_id)
|
||
// 这样积分抵扣在优惠后的价格上计算,顺序正确
|
||
$modifiedGroup['priceGroup']['totalPrice'] = bcsub((string)$originalTotalPrice, (string)$couponPrice, 2);
|
||
|
||
// 自提模式传入门店信息,使后续流程可获取门店相关数据
|
||
if ($storeId) {
|
||
$modifiedGroup['store_id'] = $storeId;
|
||
}
|
||
|
||
/** @var OrderComputedServices $computedServices */
|
||
$computedServices = app()->make(OrderComputedServices::class);
|
||
$result = $computedServices->computedOrder(
|
||
$uid, $userInfo, $modifiedGroup, $addressId, '',
|
||
$useIntegral, 0, false, $shippingType, $isGift
|
||
);
|
||
|
||
// 修正返回字段:total_price 用原始总价,coupon_price 用计算值
|
||
$result['total_price'] = $originalTotalPrice;
|
||
$result['coupon_price'] = $couponPrice;
|
||
// 自动用券回显:前端据此展示已选中的优惠券
|
||
$result['coupon_id'] = $couponId;
|
||
$result['coupon_title'] = $couponTitle;
|
||
|
||
// 自动领取发生在确认接口返回之后,或被自动选中并用于计价的券不在列表查询窗口内时,
|
||
// 重新查询可用券并返回给前端,按已选中的券补齐,保证金额、入口文案与弹窗列表口径一致。
|
||
$result['coupon_list'] = [];
|
||
if (!$isActivityOrder && ($pricingPolicy['coupon'] ?? true)) {
|
||
/** @var StoreCouponUserServices $couponServices */
|
||
$couponServices = app()->make(StoreCouponUserServices::class);
|
||
try {
|
||
$result['coupon_list'] = $couponServices->getUsableCouponList($uid, ['valid' => $cartInfo]);
|
||
} catch (\Throwable $e) {
|
||
// 优惠券列表刷新失败不影响金额计算和下单流程
|
||
$result['coupon_list'] = [];
|
||
}
|
||
$usableCouponIds = array_map('intval', array_column($result['coupon_list'], 'id'));
|
||
if ($couponId > 0 && !in_array($couponId, $usableCouponIds, true)) {
|
||
try {
|
||
$usedCouponRow = $couponServices->getUsableCouponRow($uid, $couponId);
|
||
if ($usedCouponRow) {
|
||
$result['coupon_list'][] = $usedCouponRow;
|
||
}
|
||
} catch (\Throwable $e) {
|
||
// 已选券回显失败不影响金额计算
|
||
}
|
||
}
|
||
}
|
||
|
||
// 自提模式强制运费为 0;上门服务模式运费替换为商品上门费用(computedPayPostage 内部仍会检查
|
||
// store_self_mention 系统配置,但产品级 store_pickup/door_service 开关已由 confirm 接口控制,
|
||
// 前端发送 delivery_type='pickup'/'door' 即表示该商品支持到店自提/上门服务)
|
||
if ($deliveryType === 'pickup') {
|
||
// 如果 computedOrder 返回了运费,需从 pay_price 中扣除
|
||
$originalPayPostage = $result['pay_postage'] ?? 0;
|
||
$result['pay_postage'] = 0;
|
||
if ($originalPayPostage > 0) {
|
||
$result['pay_price'] = bcsub((string)($result['pay_price'] ?? 0), (string)$originalPayPostage, 2);
|
||
}
|
||
} elseif ($deliveryType === 'door') {
|
||
// 上门服务:运费 = 商品上门费用(实时从数据库读取,避免 confirm 缓存快照过期导致与 create 不一致)
|
||
$doorPostage = 0.00;
|
||
if (!empty($cartInfo[0]['product_id'])) {
|
||
try {
|
||
/** @var \app\services\product\CenterProductServices $centerServices */
|
||
$centerServices = app()->make(\app\services\product\CenterProductServices::class);
|
||
$doorInfo = $centerServices->getInfo((int)$cartInfo[0]['product_id']);
|
||
$doorProductInfo = $doorInfo['productInfo'] ?? $doorInfo;
|
||
$doorPostage = (float)($doorProductInfo['door_service_postage'] ?? 0);
|
||
} catch (\Throwable $e) {
|
||
// 读取失败时按免上门费处理,不影响主流程
|
||
$doorPostage = 0.00;
|
||
}
|
||
}
|
||
$originalPayPostage = $result['pay_postage'] ?? 0;
|
||
$result['pay_postage'] = $doorPostage;
|
||
// 扣除快递运费后按上门费用重算应付金额
|
||
$basePrice = bcsub((string)($result['pay_price'] ?? 0), (string)$originalPayPostage, 2);
|
||
$result['pay_price'] = bcadd($basePrice, (string)$doorPostage, 2);
|
||
}
|
||
|
||
return app('json')->success($result);
|
||
} catch (\Throwable $e) {
|
||
return app('json')->fail($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 统一下单(中心商品直接购买,绕过购物车)
|
||
* 接收商品ID和规格,直接创建订单并返回支付信息
|
||
*/
|
||
public function create()
|
||
{
|
||
$stockDecrements = [];
|
||
$stockHandler = null;
|
||
$orderTransactionStarted = false;
|
||
$orderServices = null;
|
||
$orderCacheLockKey = '';
|
||
$orderCacheLockAcquired = false;
|
||
$orderCacheKeepLock = false;
|
||
try {
|
||
$uid = (int) request()->uid();
|
||
$productId = (int) request()->param('product_id', 0);
|
||
$skuUnique = (string) request()->param('sku_unique', '');
|
||
$productType = (string) request()->param('product_type', 'normal');
|
||
$activityType = (string) request()->param('activity_type', 'normal');
|
||
$activityId = (int) request()->param('activity_id', 0);
|
||
$formData = request()->param('order_data/a', []);
|
||
$couponId = (int) request()->param('coupon_id', 0);
|
||
$formDeliveryType = $this->normalizeOrderDeliveryType((string)($formData['delivery_type'] ?? ''));
|
||
$formShippingType = $formDeliveryType === 'pickup' ? 2 : ($formDeliveryType === 'door' ? 3 : 1);
|
||
$addressId = (int)($formData['address_id'] ?? 0);
|
||
$storeId = (int)($formData['store_id'] ?? 0);
|
||
// 送礼订单:地址由领取人填写,下单阶段不选地址、不计运费
|
||
$isGift = (int)($formData['is_gift'] ?? request()->param('is_gift', 0));
|
||
$giftMark = trim((string)($formData['gift_mark'] ?? request()->param('gift_mark', '')));
|
||
$storeInfo = [];
|
||
$validatedAddressInfo = [];
|
||
$normalDeliveryValidated = false;
|
||
// 创建订单必须携带 confirm 阶段返回的一次性凭证,避免绕过确认页及重复提交。
|
||
$key = trim((string) request()->param('key', ''));
|
||
if ($key === '') {
|
||
return app('json')->fail('订单确认凭证不能为空,请返回确认页重新提交');
|
||
}
|
||
$requestProductId = $productId;
|
||
$requestSkuUnique = $skuUnique;
|
||
$requestNum = (int) request()->param('num', 0);
|
||
$isCartMode = false;
|
||
$shouldRepriceNormal = false;
|
||
$cartIdsForRemove = '';
|
||
$cartNewMode = false;
|
||
|
||
/** @var OrderCenterUserServices $orderServices */
|
||
$orderServices = app()->make(OrderCenterUserServices::class);
|
||
$orderCacheLockKey = $orderServices->acquireOrderCacheLock($uid, $key);
|
||
if ($orderCacheLockKey === null) {
|
||
return app('json')->fail('订单已过期,请刷新页面');
|
||
}
|
||
if ($orderCacheLockKey === '') {
|
||
return app('json')->fail('订单正在创建,请勿重复提交');
|
||
}
|
||
$orderCacheLockAcquired = true;
|
||
|
||
$cartGroup = $orderServices->getCacheOrderInfo($uid, $key);
|
||
if (!$cartGroup || empty($cartGroup['cartInfo'])) {
|
||
return app('json')->fail('订单已过期,请刷新页面');
|
||
}
|
||
$cartInfo = $cartGroup['cartInfo'];
|
||
$priceData = $cartGroup['priceData'] ?? [];
|
||
$cacheMode = (string)($cartGroup['order_mode'] ?? '');
|
||
// 兼容旧缓存:存在 cart_ids/new 才视为购物车,否则按直购缓存处理。
|
||
$isCartMode = $cacheMode === 'cart'
|
||
|| ($cacheMode === '' && (array_key_exists('cart_ids', $cartGroup) || array_key_exists('new', $cartGroup)));
|
||
$activityType = (string)($cartGroup['activity_type'] ?? 'normal');
|
||
$activityId = (int)($cartGroup['activity_id'] ?? 0);
|
||
$isActivityOrder = $activityType !== 'normal' && $activityId > 0;
|
||
|
||
/** @var TradeModeRegistry $tradeModeRegistry */
|
||
$tradeModeRegistry = app()->make(TradeModeRegistry::class);
|
||
$tradeModeHandler = $tradeModeRegistry->get($activityType);
|
||
|
||
// 校验确认缓存中的商品类型,避免客户端修改 product_type 绕过 Handler。
|
||
$productTypes = array_values(array_unique(array_map(function ($item) {
|
||
return (string)($item['productInfo']['product_type'] ?? 'normal');
|
||
}, $cartInfo)));
|
||
if (count($productTypes) > 1) {
|
||
return app('json')->fail('不同产品类型的商品请分开下单');
|
||
}
|
||
$productType = (string)($cartGroup['product_type'] ?? ($productTypes[0] ?? 'normal'));
|
||
if (!in_array($productType, $productTypes, true)) {
|
||
$productType = $productTypes[0] ?? 'normal';
|
||
}
|
||
|
||
// 直购只能使用确认页缓存中的商品和规格,不能用请求参数替换订单主体。
|
||
$cachedFirstItem = $cartInfo[0] ?? [];
|
||
$productId = (int)($cachedFirstItem['product_id'] ?? $cachedFirstItem['productInfo']['id'] ?? 0);
|
||
$skuUnique = (string)($cachedFirstItem['product_attr_unique']
|
||
?? $cachedFirstItem['productInfo']['attrInfo']['unique']
|
||
?? $skuUnique);
|
||
$num = (int)($cachedFirstItem['cart_num'] ?? 1);
|
||
if (!$productId) {
|
||
return app('json')->fail('商品信息已失效,请返回确认页重新提交');
|
||
}
|
||
if (!$isCartMode) {
|
||
if ($requestProductId > 0 && $requestProductId !== $productId) {
|
||
return app('json')->fail('商品信息已变化,请返回确认页重新提交');
|
||
}
|
||
if ($requestSkuUnique !== '' && $requestSkuUnique !== $skuUnique) {
|
||
return app('json')->fail('商品规格已变化,请返回确认页重新提交');
|
||
}
|
||
if ($requestNum > 0 && $requestNum !== $num) {
|
||
return app('json')->fail('购买数量已变化,请返回确认页重新提交');
|
||
}
|
||
}
|
||
|
||
$skuInfo = [];
|
||
/** @var ProductTypeRegistry $registry */
|
||
$registry = app()->make(ProductTypeRegistry::class);
|
||
$productInfo = $cachedFirstItem['productInfo'] ?? [];
|
||
if ($isActivityOrder) {
|
||
$cartSkuUnique = (string)($cachedFirstItem['product_attr_unique']
|
||
?? $cachedFirstItem['productInfo']['attrInfo']['unique']
|
||
?? $skuUnique);
|
||
$tradeModeHandler->beforeCreateOrder($productId, $cartSkuUnique, $num, $activityId, $uid);
|
||
$tradeModeHandler->validateOrder($productId, $cartSkuUnique, $num, $activityId, $uid);
|
||
}
|
||
|
||
// 记录下单成功后需清除的购物车ID(仅数据库购物车模式需要清除)。
|
||
$cartIdsForRemove = (string)($cartGroup['cart_ids'] ?? '');
|
||
$cartNewMode = (bool)($cartGroup['new'] ?? false);
|
||
|
||
// 普通商品无活动时,创建订单前重新读取当前商品、SKU、库存和用户价格。
|
||
// 购物车数据库模式从当前购物车读取,直购/立即购买模式从确认缓存重建。
|
||
$hasCartActivity = false;
|
||
foreach ($cartInfo as $cartItem) {
|
||
if (!empty($cartItem['seckill_id'])
|
||
|| !empty($cartItem['bargain_id'])
|
||
|| !empty($cartItem['combination_id'])
|
||
|| !empty($cartItem['advance_id'])) {
|
||
$hasCartActivity = true;
|
||
break;
|
||
}
|
||
}
|
||
if ($productType === 'normal' && !$isActivityOrder && !$hasCartActivity) {
|
||
$shouldRepriceNormal = true;
|
||
}
|
||
|
||
// 用户信息必须在最终核价前实时读取,会员价、运费折扣和积分余额都依赖当前状态。
|
||
/** @var UserServices $userServices */
|
||
$userServices = app()->make(UserServices::class);
|
||
$userInfo = $userServices->getUserInfo($uid);
|
||
if (!$userInfo) {
|
||
return app('json')->fail('用户信息不存在');
|
||
}
|
||
if (is_object($userInfo)) {
|
||
$userInfo = $userInfo->toArray();
|
||
}
|
||
|
||
// 普通商品创建订单前重新读取当前商品、SKU、库存和用户价格,不能信任确认页缓存。
|
||
if ($shouldRepriceNormal) {
|
||
/** @var StoreCartServices $cartServices */
|
||
$cartServices = app()->make(StoreCartServices::class);
|
||
if ($isCartMode && !$cartNewMode) {
|
||
if ($cartIdsForRemove === '') {
|
||
return app('json')->fail('购物车商品信息已失效,请返回确认页重新提交');
|
||
}
|
||
$freshCartGroup = $cartServices->getUserProductCartListV1(
|
||
$uid,
|
||
$cartIdsForRemove,
|
||
false,
|
||
[],
|
||
$formShippingType,
|
||
$isGift
|
||
);
|
||
} else {
|
||
$freshCartGroup = $cartServices->rebuildNormalCheckoutCart(
|
||
$uid,
|
||
$cartInfo,
|
||
[],
|
||
$formShippingType,
|
||
$isGift
|
||
);
|
||
}
|
||
$cartInfo = $freshCartGroup['valid'] ?? [];
|
||
if (empty($cartInfo)) {
|
||
return app('json')->fail('商品已失效,请返回确认页重新提交');
|
||
}
|
||
if (!empty($freshCartGroup['invalid'])) {
|
||
return app('json')->fail('库存不足或商品已不可配送,请返回确认页刷新');
|
||
}
|
||
$freshProductTypes = array_values(array_unique(array_map(function ($item) {
|
||
return (string)($item['productInfo']['product_type'] ?? 'normal');
|
||
}, $cartInfo)));
|
||
if (count($freshProductTypes) !== 1 || $freshProductTypes[0] !== 'normal') {
|
||
return app('json')->fail('商品类型已变化,请返回确认页重新提交');
|
||
}
|
||
$productId = (int)($cartInfo[0]['product_id'] ?? $cartInfo[0]['productInfo']['id'] ?? $productId);
|
||
$skuUnique = (string)($cartInfo[0]['product_attr_unique']
|
||
?? $cartInfo[0]['productInfo']['attrInfo']['unique']
|
||
?? $skuUnique);
|
||
$num = (int)($cartInfo[0]['cart_num'] ?? $num);
|
||
$productInfo = $cartInfo[0]['productInfo'] ?? $productInfo;
|
||
|
||
// 送礼订单由领取人填收货信息,下单阶段不校验配送地址
|
||
if ($isGift == 1) {
|
||
$validatedAddressInfo = [];
|
||
$normalDeliveryValidated = true;
|
||
} else {
|
||
// 重建后再次校验商品当前配送能力,并用当前地址计算运费。
|
||
$deliveryContext = $this->validateNormalOrderDelivery(
|
||
$cartInfo,
|
||
$formDeliveryType,
|
||
$uid,
|
||
$addressId,
|
||
$storeId,
|
||
is_array($formData['service'] ?? null) ? $formData['service'] : []
|
||
);
|
||
$validatedAddressInfo = $deliveryContext['address_info'];
|
||
$storeInfo = $deliveryContext['store_info'];
|
||
$normalDeliveryValidated = true;
|
||
}
|
||
|
||
/** @var OrderComputedServices $computedServices */
|
||
$computedServices = app()->make(OrderComputedServices::class);
|
||
$priceGroup = $computedServices->getOrderPriceGroup(
|
||
(float)sys_config('store_free_postage') ?: 0,
|
||
$cartInfo,
|
||
$validatedAddressInfo,
|
||
$userInfo,
|
||
$formShippingType,
|
||
$isGift
|
||
);
|
||
$cartInfo = $priceGroup['cartInfo'] ?? $cartInfo;
|
||
$priceData = [
|
||
'totalPrice' => $priceGroup['totalPrice'] ?? 0,
|
||
'payPrice' => bcadd((string)($priceGroup['totalPrice'] ?? 0), (string)($priceGroup['storePostage'] ?? 0), 2),
|
||
'payPostage' => $priceGroup['storePostage'] ?? 0,
|
||
'isStoreFreePostage' => $priceGroup['isStoreFreePostage'] ?? false,
|
||
'storeFreePostage' => $priceGroup['storeFreePostage'] ?? 0,
|
||
'costPrice' => $priceGroup['costPrice'] ?? 0,
|
||
'giftPrice' => $priceGroup['giftPrice'] ?? 0,
|
||
'levelPrice' => $priceGroup['levelPrice'] ?? 0,
|
||
'memberPrice' => $priceGroup['memberPrice'] ?? 0,
|
||
];
|
||
} elseif ($productType === 'normal' && !$isActivityOrder) {
|
||
// 非普通重建路径(兼容旧活动/特殊缓存)仍必须校验配送信息;送礼订单除外
|
||
if ($isGift == 1) {
|
||
$validatedAddressInfo = [];
|
||
$normalDeliveryValidated = true;
|
||
} else {
|
||
$deliveryContext = $this->validateNormalOrderDelivery(
|
||
$cartInfo,
|
||
$formDeliveryType,
|
||
$uid,
|
||
$addressId,
|
||
$storeId,
|
||
is_array($formData['service'] ?? null) ? $formData['service'] : []
|
||
);
|
||
$validatedAddressInfo = $deliveryContext['address_info'];
|
||
$storeInfo = $deliveryContext['store_info'];
|
||
$normalDeliveryValidated = true;
|
||
}
|
||
}
|
||
|
||
// 记录实时原始总价(优惠券抵扣不影响 total_price 记录)
|
||
$originalTotalPrice = $priceData['totalPrice'] ?? 0;
|
||
|
||
// 用户未选券时自动匹配整单最优优惠券(可领取的先领取,失败降级为无券;活动订单与关闭券能力的商品类型不参与)
|
||
if (!$couponId && !$isActivityOrder) {
|
||
$couponPolicyEnabled = true;
|
||
$autoHandler = $registry->get($productType);
|
||
if (method_exists($autoHandler, 'getPricingPolicy')) {
|
||
$couponPolicyEnabled = ($autoHandler->getPricingPolicy()['coupon'] ?? true) === true;
|
||
}
|
||
if ($couponPolicyEnabled) {
|
||
$autoCoupon = app()->make(OrderComputedServices::class)->autoMatchCoupon($uid, $cartInfo, true);
|
||
if (!empty($autoCoupon['coupon_user_id'])) {
|
||
$couponId = (int)$autoCoupon['coupon_user_id'];
|
||
}
|
||
}
|
||
}
|
||
|
||
// 1.5 手动校验并计算优惠券金额
|
||
// 活动订单默认不支持优惠券
|
||
if ($couponId && !$isActivityOrder) {
|
||
/** @var \app\services\activity\coupon\StoreCouponUserServices $couponServices */
|
||
$couponServices = app()->make(\app\services\activity\coupon\StoreCouponUserServices::class);
|
||
// 预加载 issue 关联以获取 applicable_type
|
||
$couponInfo = $couponServices->get($couponId, ['*'], ['issue']);
|
||
if (!$couponInfo || $couponInfo['uid'] != $uid || $couponInfo->getData('is_fail') != 0 || $couponInfo->getData('status') != 0 || $couponInfo['start_time'] > time() || $couponInfo['end_time'] < time()) {
|
||
return app('json')->fail('选择的优惠劵无效');
|
||
}
|
||
$validPrice = '0';
|
||
$validCount = 0;
|
||
// 使用 getData 获取原始 type 值(避免 getTypeAttr 访问器将整数转为中文)
|
||
$type = $couponInfo['applicable_type'] ?? $couponInfo->getData('type') ?? 0;
|
||
switch ($type) {
|
||
case 0:
|
||
case 3:
|
||
foreach ($cartInfo as $cart) {
|
||
$validPrice = bcadd($validPrice, bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2);
|
||
$validCount++;
|
||
}
|
||
break;
|
||
case 1:
|
||
$storeCategoryServices = app()->make(\app\dao\product\product\StoreCategoryDao::class);
|
||
$couponCategory = explode(',', (string)$couponInfo['category_id']);
|
||
$categoryIds = $storeCategoryServices->getAllById($couponCategory);
|
||
if ($categoryIds) {
|
||
$cateIds = array_column($categoryIds, 'id');
|
||
foreach ($cartInfo as $cart) {
|
||
if (isset($cart['productInfo']['cate_id']) && array_intersect(explode(',', $cart['productInfo']['cate_id']), $cateIds)) {
|
||
$validPrice = bcadd($validPrice, bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2);
|
||
$validCount++;
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case 2:
|
||
foreach ($cartInfo as $cart) {
|
||
if (isset($cart['product_id']) && in_array($cart['product_id'], explode(',', $couponInfo['product_id']))) {
|
||
$validPrice = bcadd($validPrice, bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2);
|
||
$validCount++;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
if ($validCount && $couponInfo['use_min_price'] <= $validPrice) {
|
||
$couponPriceVal = min($couponInfo['coupon_price'], $validPrice);
|
||
// 将优惠券金额从总价中扣除,使 computedOrder 在优惠后价格上计算积分
|
||
$priceData['totalPrice'] = bcsub((string)$priceData['totalPrice'], (string)$couponPriceVal, 2);
|
||
} else {
|
||
return app('json')->fail('不满足优惠劵的使用条件');
|
||
}
|
||
}
|
||
|
||
// 3.1 生成订单号(统一走订单中台)
|
||
/** @var OrderCenterServices $createServices */
|
||
$createServices = app()->make(OrderCenterServices::class);
|
||
$orderNo = $createServices->getNewOrderId('cp');
|
||
|
||
// 3.2 获取收货地址信息
|
||
$realName = $formData['real_name'] ?? '';
|
||
$userPhone = $formData['phone'] ?? '';
|
||
$userAddress = '';
|
||
if ($addressId) {
|
||
/** @var \app\services\user\UserAddressServices $addressServices */
|
||
$addressServices = app()->make(\app\services\user\UserAddressServices::class);
|
||
$addressInfo = $validatedAddressInfo ?: $addressServices->getOne(['uid' => $uid, 'id' => $addressId, 'is_del' => 0]);
|
||
if ($addressInfo) {
|
||
if (is_object($addressInfo)) {
|
||
$addressInfo = $addressInfo->toArray();
|
||
}
|
||
$realName = $addressInfo['real_name'] ?? $realName;
|
||
$userPhone = $addressInfo['phone'] ?? $userPhone;
|
||
$userAddress = implode(' ', array_filter([
|
||
$addressInfo['province'] ?? '',
|
||
$addressInfo['city'] ?? '',
|
||
$addressInfo['district'] ?? '',
|
||
$addressInfo['detail'] ?? '',
|
||
]));
|
||
}
|
||
}
|
||
|
||
$payType = request()->param('pay_type', '');
|
||
|
||
// 活动订单强制使用指定支付方式
|
||
$activityAllowedPayTypes = [];
|
||
$isFreePay = false;
|
||
if ($isActivityOrder) {
|
||
$activityAllowedPayTypes = $tradeModeHandler->getAllowedPayTypes($activityId);
|
||
$isFreePay = $tradeModeHandler->isFreePay($activityId);
|
||
}
|
||
if ($isActivityOrder && !empty($activityAllowedPayTypes) && !in_array($payType, $activityAllowedPayTypes, true)) {
|
||
$payType = $activityAllowedPayTypes[0];
|
||
}
|
||
// 免费活动订单(如积分兑换)统一使用 free 支付方式
|
||
if ($isActivityOrder && $isFreePay) {
|
||
$payType = 'free';
|
||
}
|
||
|
||
// 从 Handler 获取订单源类型
|
||
$handler = $registry->get($productType);
|
||
$stockHandler = $handler;
|
||
$sourceType = $handler->getOrderSourceType();
|
||
|
||
// 转换自定义表单数据:将提交的 {"custom_field_0": "值"} 转为带原始标题的数组格式
|
||
$submittedCustomForm = $formData['custom_form'] ?? [];
|
||
if (!empty($submittedCustomForm) && is_array($submittedCustomForm) && !isset($submittedCustomForm[0])) {
|
||
// 从产品定义获取字段标题映射
|
||
$detailExtend = $handler->getDetailExtend($productId);
|
||
$productCustomForm = $detailExtend['custom_form'] ?? [];
|
||
$fieldTitleMap = [];
|
||
// 循环变量不得使用 $key:外层 $key 是订单确认缓存凭证,覆盖后 consumeOrderCache 会因类型错误崩溃
|
||
foreach ($productCustomForm as $fieldKey => $fieldItem) {
|
||
$fieldTitleMap['custom_field_' . $fieldKey] = $fieldItem['title'] ?? ('自定义字段' . ($fieldKey + 1));
|
||
}
|
||
$structured = [];
|
||
foreach ($submittedCustomForm as $field => $value) {
|
||
$structured[] = [
|
||
'label' => $field,
|
||
'title' => $fieldTitleMap[$field] ?? $field,
|
||
'value' => $value,
|
||
];
|
||
}
|
||
$formData['custom_form'] = $structured;
|
||
}
|
||
|
||
// 4. 计算最终价格(优惠券+积分抵扣+运费)
|
||
// 活动订单默认不使用积分抵扣
|
||
$useIntegral = !$isActivityOrder && (bool) request()->param('use_integral', false);
|
||
// 构建与 computed() 相同的 cartGroup 结构
|
||
$tmpCartGroup = [
|
||
'cartInfo' => $cartInfo,
|
||
'priceData' => $priceData,
|
||
'integralRatio' => sys_config('integral_ratio', 0),
|
||
];
|
||
$tmpCartGroup['priceGroup'] = $tmpCartGroup['priceData'];
|
||
$tmpCartGroup['other'] = [
|
||
'integralRatio' => $tmpCartGroup['integralRatio'],
|
||
'pricing_policy' => method_exists($handler, 'getPricingPolicy') ? $handler->getPricingPolicy() : [],
|
||
];
|
||
// totalPrice 已在第 1.5 步按优惠券扣减,传给 computedOrder(couponId=0 避免重复扣)
|
||
/** @var OrderComputedServices $computedServices */
|
||
$computedServices = app()->make(OrderComputedServices::class);
|
||
$computedResult = $computedServices->computedOrder(
|
||
$uid, $userInfo, $tmpCartGroup, $addressId, $payType,
|
||
$useIntegral, 0, false, $formShippingType, $isGift
|
||
);
|
||
$finalPayPrice = $computedResult['pay_price'] ?? 0;
|
||
$finalDeductionPrice = $computedResult['deduction_price'] ?? 0;
|
||
$finalUsedIntegral = $computedResult['usedIntegral'] ?? 0;
|
||
|
||
// 自提模式强制运费为 0;上门服务模式运费替换为商品上门费用(同上 computed 方法的处理逻辑)
|
||
$finalPayPostage = $computedResult['pay_postage'] ?? 0;
|
||
// 会员运费优惠:pay_postage 为优惠后运费,详情页需还原原运费并单独展示优惠
|
||
$finalPostageDiscount = (string)($computedResult['storePostageDiscount'] ?? '0.00');
|
||
if ($formDeliveryType === 'pickup') {
|
||
if ($finalPayPostage > 0) {
|
||
$finalPayPrice = bcsub((string)$finalPayPrice, (string)$finalPayPostage, 2);
|
||
}
|
||
$finalPayPostage = 0;
|
||
$finalPostageDiscount = '0.00';
|
||
} elseif ($formDeliveryType === 'door') {
|
||
// 上门服务:运费 = 商品上门费用(实时从数据库读取,避免缓存快照过期导致金额不一致)
|
||
$doorPostage = 0.00;
|
||
$doorProductId = (int)($cartInfo[0]['product_id'] ?? $productId ?? 0);
|
||
if ($doorProductId) {
|
||
try {
|
||
/** @var \app\services\product\CenterProductServices $centerServices */
|
||
$centerServices = app()->make(\app\services\product\CenterProductServices::class);
|
||
$doorInfo = $centerServices->getInfo($doorProductId);
|
||
$doorProductInfo = $doorInfo['productInfo'] ?? $doorInfo;
|
||
$doorPostage = (float)($doorProductInfo['door_service_postage'] ?? 0);
|
||
} catch (\Throwable $e) {
|
||
// 读取失败时按免上门费处理,不影响主流程
|
||
$doorPostage = 0.00;
|
||
}
|
||
}
|
||
if ($finalPayPostage > 0) {
|
||
$finalPayPrice = bcsub((string)$finalPayPrice, (string)$finalPayPostage, 2);
|
||
}
|
||
$finalPayPrice = bcadd((string)$finalPayPrice, (string)$doorPostage, 2);
|
||
$finalPayPostage = $doorPostage;
|
||
$finalPostageDiscount = '0.00';
|
||
}
|
||
|
||
// 5. 先写产品类型扩展表,拿到扩展表记录 id 作为中台表的 source_id
|
||
$cartInfo[0]['product_id'] = $cartInfo[0]['product_id'] ?? $productId;
|
||
$cartInfo[0]['uid'] = $uid;
|
||
|
||
// 5.0 透传 block_data(门票/酒店等 display_sections 选中数据)到 cartInfo[0]
|
||
// 由前端 order_data.block_data 传入,Handler.createOrderExtend 从中解析 visit_date/session_id/sku_id
|
||
$blockData = $formData['block_data'] ?? [];
|
||
if (!empty($blockData)) {
|
||
$cartInfo[0]['block_data'] = $blockData;
|
||
}
|
||
|
||
// 5.1 游客信息(门票专属):校验数量并 merge 到 cartInfo[0],由 Handler.createOrderExtend 存储
|
||
// 校验规则:商品开启 need_id_card 时,visitors 数量必须等于购买总数量
|
||
// 购物车合单模式:累加所有商品 cart_num(每张票对应一位游客)
|
||
$visitors = $formData['visitors'] ?? [];
|
||
if (method_exists($handler, 'getDetailExtend')) {
|
||
$visitorFormConfig = $handler->getDetailExtend($productId)['visitor_form_config'] ?? [];
|
||
if (!empty($visitorFormConfig)) {
|
||
$visitors = is_array($visitors) ? $visitors : [];
|
||
// 累加所有购物车项的 cart_num 作为总票数
|
||
$cartNum = 0;
|
||
foreach ($cartInfo as $cItem) {
|
||
$cartNum += (int)($cItem['cart_num'] ?? 0);
|
||
}
|
||
$cartNum = $cartNum > 0 ? $cartNum : 1;
|
||
if (count($visitors) !== $cartNum) {
|
||
return app('json')->fail('请完整填写游客信息');
|
||
}
|
||
// 校验每位游客必填字段(姓名 + 身份证)
|
||
foreach ($visitors as $idx => $visitor) {
|
||
$name = (string)($visitor['name'] ?? '');
|
||
$idCard = (string)($visitor['id_card'] ?? '');
|
||
if ($name === '' || $idCard === '') {
|
||
return app('json')->fail('请完整填写第' . ($idx + 1) . '位游客信息');
|
||
}
|
||
}
|
||
$cartInfo[0]['visitors'] = $visitors;
|
||
// 合单模式下 quantity 存总票数(与 visitors 数量一致)
|
||
$cartInfo[0]['cart_num'] = $cartNum;
|
||
}
|
||
}
|
||
|
||
// 自提订单:校验门店并获取门店信息
|
||
$verifyCode = '';
|
||
if ($formDeliveryType === 'pickup') {
|
||
if (!$normalDeliveryValidated) {
|
||
$storeId = (int)($formData['store_id'] ?? 0);
|
||
if ($storeId > 0) {
|
||
/** @var SystemStoreServices $storeServices */
|
||
$storeServices = app()->make(SystemStoreServices::class);
|
||
$storeInfo = $storeServices->getOne(['id' => $storeId, 'is_show' => 1, 'is_del' => 0]);
|
||
if (is_object($storeInfo)) {
|
||
$storeInfo = $storeInfo->toArray();
|
||
}
|
||
if (!$storeInfo || empty($storeInfo['id'])) {
|
||
return app('json')->fail('自提门店不存在或已停用');
|
||
}
|
||
}
|
||
}
|
||
// 生成核销码(8位随机数字)
|
||
$verifyCode = str_pad((string)random_int(0, 99999999), 8, '0', STR_PAD_LEFT);
|
||
}
|
||
|
||
// 上门服务订单:组装预约信息(联系人/电话/地址/预约时间),缺失时回退地址信息
|
||
$doorServiceInfo = [];
|
||
if ($formDeliveryType === 'door') {
|
||
$serviceData = $formData['service'] ?? [];
|
||
$doorServiceInfo = [
|
||
'contact_name' => $serviceData['contact_name'] ?? $realName,
|
||
'contact_phone' => $serviceData['contact_phone'] ?? $userPhone,
|
||
'address' => $serviceData['address'] ?? $userAddress,
|
||
'appointment_time'=> $serviceData['appointment_time'] ?? '',
|
||
];
|
||
if (empty($doorServiceInfo['contact_phone'])) {
|
||
return app('json')->fail('请填写上门服务联系电话');
|
||
}
|
||
}
|
||
$deliveryType = $formDeliveryType === 'pickup' ? 'pickup' : ($formDeliveryType === 'door' ? 'door' : 'express');
|
||
|
||
// 库存、产品扩展、中台订单须原子提交;产品钩子可能预留稀缺资源。
|
||
\think\facade\Db::startTrans();
|
||
$orderTransactionStarted = true;
|
||
// 普通实物订单在创建时锁定库存。活动订单由活动 Handler 自己管理库存,不能重复扣减。
|
||
if (!$isActivityOrder) {
|
||
foreach ($cartInfo as $cartItem) {
|
||
$itemProductId = (int)($cartItem['product_id'] ?? $cartItem['productInfo']['id'] ?? 0);
|
||
$itemNum = (int)($cartItem['cart_num'] ?? 0);
|
||
$itemSkuUnique = (string)($cartItem['product_attr_unique']
|
||
?? $cartItem['productInfo']['attrInfo']['unique']
|
||
?? '');
|
||
if ($itemProductId <= 0 || $itemNum <= 0 || !$handler->decStock($itemProductId, $itemSkuUnique, $itemNum)) {
|
||
throw new \RuntimeException('商品库存不足或规格已失效');
|
||
}
|
||
$stockDecrements[] = [
|
||
'product_id' => $itemProductId,
|
||
'sku_unique' => $itemSkuUnique,
|
||
'num' => $itemNum,
|
||
];
|
||
}
|
||
}
|
||
|
||
// 积分抵扣必须在订单事务内落流水并扣减用户积分,失败时回滚库存和订单。
|
||
if ((int)$finalUsedIntegral > 0) {
|
||
/** @var UserBillServices $userBillServices */
|
||
$userBillServices = app()->make(UserBillServices::class);
|
||
$userBillServices->billIntegralIncome('deduction', $uid, [
|
||
'number' => (int)$finalUsedIntegral,
|
||
'deductionPrice' => $finalDeductionPrice,
|
||
], $orderNo);
|
||
}
|
||
|
||
// 优惠券在订单创建事务内消费,订单后续写入失败时随事务一起回滚。
|
||
if ($couponId > 0 && !$isActivityOrder) {
|
||
/** @var StoreCouponUserServices $couponServices */
|
||
$couponServices = app()->make(StoreCouponUserServices::class);
|
||
if (!$couponServices->useCoupon($couponId)) {
|
||
throw new \RuntimeException('优惠券已被使用或已失效,请重新选择');
|
||
}
|
||
}
|
||
|
||
$extendId = 0;
|
||
if (method_exists($handler, 'createOrderExtend')) {
|
||
$extendId = $handler->createOrderExtend($orderNo, $cartInfo[0] ?? [], [
|
||
'cart_items' => $cartInfo,
|
||
'store_id' => $storeId,
|
||
'shipping_type' => $formShippingType,
|
||
'verify_code' => $verifyCode,
|
||
// 上门服务预约信息,扩展订单表可据此展示/核销
|
||
'service' => $doorServiceInfo,
|
||
]);
|
||
}
|
||
|
||
// 构建发票信息
|
||
$invoiceInfo = [];
|
||
if (!empty($formData['invoice_id'])) {
|
||
/** @var \app\services\user\UserInvoiceServices $invoiceServices */
|
||
$invoiceServices = app()->make(\app\services\user\UserInvoiceServices::class);
|
||
// 发票必须属于当前用户且处于有效状态,不能只按 ID 查询。
|
||
$invoice = $invoiceServices->checkInvoice((int)$formData['invoice_id'], $uid);
|
||
$invoiceInfo = is_object($invoice) ? $invoice->toArray() : (array)$invoice;
|
||
}
|
||
|
||
// 6. 写中台订单表 eb_order_center
|
||
/** @var OrderCenterUserServices $centerServices */
|
||
$centerServices = app()->make(OrderCenterUserServices::class);
|
||
|
||
// 组装活动相关扩展信息
|
||
$activityExtraInfo = [];
|
||
if ($isActivityOrder) {
|
||
$orderExtraInfo = $tradeModeHandler->getOrderExtraInfo($activityId);
|
||
$activityExtraInfo = array_merge(
|
||
$orderExtraInfo,
|
||
[
|
||
'activity_type' => $activityType,
|
||
'activity_id' => $activityId,
|
||
]
|
||
);
|
||
}
|
||
|
||
if (!$centerServices->createRecord([
|
||
'order_no' => $orderNo,
|
||
'source_type' => $sourceType,
|
||
'source_id' => $extendId,
|
||
'uid' => $uid,
|
||
'order_category' => 'goods',
|
||
'product_type' => $productType,
|
||
'store_id' => $storeId,
|
||
'shipping_type' => $formShippingType,
|
||
'verify_code' => $verifyCode,
|
||
'activity_type' => $activityType,
|
||
'activity_id' => $activityId,
|
||
'status_code' => 'pending_payment',
|
||
'pay_status' => 0,
|
||
'refund_status' => 0,
|
||
'total_price' => $originalTotalPrice,
|
||
'pay_price' => $finalPayPrice,
|
||
'pay_postage' => $finalPayPostage,
|
||
'pay_type' => $payType,
|
||
'coupon_price' => (string) ($couponPriceVal ?? $priceData['couponPrice'] ?? '0.00'),
|
||
'deduction_price' => (string) $finalDeductionPrice,
|
||
'use_integral_num' => $finalUsedIntegral,
|
||
'member_discount' => (string)($priceData['levelPrice'] ?? $computedResult['level_price'] ?? '0.00'),
|
||
'vip_discount' => (string)($priceData['memberPrice'] ?? $computedResult['member_price'] ?? '0.00'),
|
||
'gift_price' => (string)($priceData['giftPrice'] ?? $computedResult['gift_price'] ?? '0.00'),
|
||
'title' => $cartInfo[0]['productInfo']['store_name'] ?? '',
|
||
'image' => $cartInfo[0]['productInfo']['image'] ?? '',
|
||
// 多商品合单时聚合所有商品数量
|
||
'total_num' => array_sum(array_column($cartInfo, 'cart_num')) ?: 1,
|
||
'mark' => $formData['mark'] ?? '',
|
||
'custom_form' => $formData['custom_form'] ?? [],
|
||
'invoice' => $invoiceInfo,
|
||
'extra_info' => [
|
||
'business_status' => [
|
||
'code' => 'pending_payment',
|
||
'text' => '待支付',
|
||
'detail' => '',
|
||
'extend' => new \stdClass(),
|
||
],
|
||
'delivery_type' => $deliveryType,
|
||
'delivery' => [
|
||
'delivery_type' => $deliveryType,
|
||
],
|
||
// 金额字段已提升为实体列;运费优惠需保留供详情页还原原运费
|
||
'postage_discount' => $finalPostageDiscount,
|
||
'sku_info' => $this->buildSkuInfoForExtra($cartInfo[0] ?? []),
|
||
// 商品明细快照,供订单详情/退款页面使用
|
||
'items' => $this->buildOrderItemsForExtra($cartInfo),
|
||
// 商品ID,供订单详情页跳转商品详情使用
|
||
'product_id' => $cartInfo[0]['product_id'] ?? $cartInfo[0]['productInfo']['id'] ?? 0,
|
||
// 上门服务订单:保存预约信息,供详情页显示;快递订单保存收货联系信息
|
||
'contact' => $formDeliveryType === 'door' ? [] : [
|
||
'name' => $realName,
|
||
'phone' => $userPhone,
|
||
'address' => [
|
||
'province' => '',
|
||
'city' => '',
|
||
'district' => '',
|
||
'detail' => $userAddress,
|
||
'postal_code' => '',
|
||
],
|
||
],
|
||
// 上门服务订单:预约信息(联系人/电话/地址/预约时间)
|
||
'service' => $formDeliveryType === 'door' ? $doorServiceInfo : [],
|
||
// 自提订单:保存门店信息,供详情页显示
|
||
'store_info' => $formDeliveryType === 'pickup' && !empty($storeInfo) ? [
|
||
'store_id' => (int)$storeInfo['id'],
|
||
'store_name' => $storeInfo['name'] ?? '',
|
||
'store_phone' => $storeInfo['phone'] ?? '',
|
||
'address' => $storeInfo['address'] ?? '',
|
||
'detailed_address' => $storeInfo['detailed_address'] ?? $storeInfo['address'] ?? '',
|
||
] : [],
|
||
// 保存用户优惠券ID,取消或退款时用于恢复优惠券。
|
||
'coupon_id' => (!$isActivityOrder && $couponId > 0) ? $couponId : 0,
|
||
'activity_info' => $activityExtraInfo,
|
||
] + ($isGift == 1 ? [
|
||
// 送礼订单:留言与领取记录挂在 extra_info,领取时写入 gift.gift_uid
|
||
'gift_mark' => $giftMark,
|
||
'gift' => [
|
||
'gift_uid' => 0,
|
||
'gift_key' => '',
|
||
],
|
||
] : []),
|
||
'detail_route' => $handler->getOrderDetailRoute($orderNo),
|
||
])) {
|
||
throw new \RuntimeException('订单创建失败');
|
||
}
|
||
|
||
// 中台主键:开票申请与活动回调共用
|
||
$centerOrderId = 0;
|
||
try {
|
||
$centerOrderId = (int)app()->make(OrderCenterDao::class)->value(['order_no' => $orderNo], 'id');
|
||
} catch (\Throwable $e) {
|
||
// 查询失败不影响主流程(活动回调另有兜底)
|
||
}
|
||
|
||
// 下单勾选发票:同步创建开票申请记录,供后台发票列表与支付回写 is_pay 使用
|
||
if (!empty($invoiceInfo) && $centerOrderId > 0) {
|
||
try {
|
||
app()->make(OrderInvoiceServices::class)->createFromOrder((int)$uid, $centerOrderId, $invoiceInfo, 0);
|
||
} catch (\Throwable $e) {
|
||
\think\facade\Log::warning('下单创建开票申请失败: ' . $orderNo . ': ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
// 6.1 活动订单创建后回调
|
||
if ($isActivityOrder) {
|
||
$tradeModeHandler->afterOrderCreate($orderNo, $activityId, [
|
||
'order_no' => $orderNo,
|
||
'id' => $centerOrderId,
|
||
'uid' => $uid,
|
||
'product_type'=> $productType,
|
||
'pay_price' => $finalPayPrice,
|
||
'total_num' => array_sum(array_column($cartInfo, 'cart_num')) ?: 1,
|
||
'cartInfo' => $cartInfo,
|
||
]);
|
||
}
|
||
|
||
\think\facade\Db::commit();
|
||
$orderTransactionStarted = false;
|
||
$stockDecrements = [];
|
||
|
||
// 订单事务成功后立即消费确认缓存,不能只依赖后置事件清理。
|
||
// 消费失败时保留互斥锁直到超时,避免缓存残留导致重复创建订单。
|
||
if ($key && $orderServices && !$orderServices->consumeOrderCache($uid, $key)) {
|
||
$orderCacheKeepLock = true;
|
||
Log::warning('订单确认缓存消费失败,已保留创建锁: ' . $key);
|
||
}
|
||
|
||
// 通用入口也必须调度超时取消,否则预留库存会被未支付订单永久占用。
|
||
try {
|
||
event('OrderCreateAfter', [
|
||
'order_no' => $orderNo,
|
||
'order_id' => (int)app()->make(OrderCenterDao::class)->value(['order_no' => $orderNo], 'id'),
|
||
'uid' => $uid,
|
||
'key' => $key,
|
||
'cart_info' => $cartInfo[0] ?? [],
|
||
'cart_items' => $cartInfo,
|
||
'price_data' => $computedResult,
|
||
'product_type' => $productType,
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
\think\facade\Log::error('订单已创建,但后置任务调度失败: ' . $orderNo . ': ' . $e->getMessage());
|
||
}
|
||
|
||
// 6.2 购物车模式:下单成功后清除已下单的购物车记录(缓存/立即购买模式无需清除)
|
||
if ($isCartMode && !$cartNewMode && $cartIdsForRemove) {
|
||
try {
|
||
$cartIds = array_filter(array_map('intval', explode(',', $cartIdsForRemove)));
|
||
if ($cartIds) {
|
||
/** @var StoreCartServices $cartServices */
|
||
$cartServices = app()->make(StoreCartServices::class);
|
||
$cartServices->removeUserCart($uid, $cartIds);
|
||
}
|
||
} catch (\Throwable $e) {
|
||
// 购物车清除失败不影响下单结果
|
||
\think\facade\Log::warning('下单成功后清除购物车失败: ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
// 7. 写入订单状态日志,供 detail 接口的时间线读取
|
||
try {
|
||
/** @var \app\dao\order\OrderLogDao $logDao */
|
||
$logDao = app()->make(\app\dao\order\OrderLogDao::class);
|
||
$logDao->save([
|
||
'order_no' => $orderNo,
|
||
'uid' => $uid,
|
||
'operator_id' => $uid,
|
||
'operator_type' => 'user',
|
||
'from_status' => '',
|
||
'to_status' => 'pending_payment',
|
||
'content' => '提交订单',
|
||
'add_time' => time(),
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
// 日志写入失败不影响主流程
|
||
\think\facade\Log::warning('订单状态日志写入失败: ' . $e->getMessage());
|
||
}
|
||
|
||
return app('json')->success([
|
||
'order_no' => $orderNo,
|
||
'pay_price' => $finalPayPrice,
|
||
'order_id' => $orderNo,
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
if ($orderTransactionStarted) {
|
||
\think\facade\Db::rollback();
|
||
$stockDecrements = [];
|
||
}
|
||
// 仅补偿事务外的库存扣减,已回滚的事务不能重复加库存。
|
||
if ($stockHandler && $stockDecrements) {
|
||
foreach (array_reverse($stockDecrements) as $stock) {
|
||
try {
|
||
$stockHandler->incStock($stock['product_id'], $stock['sku_unique'], $stock['num']);
|
||
} catch (\Throwable $rollbackError) {
|
||
\think\facade\Log::error('CenterOrderController::create stock rollback failed: ' . $rollbackError->getMessage());
|
||
}
|
||
}
|
||
}
|
||
\think\facade\Log::error('CenterOrderController::create error: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine());
|
||
return app('json')->fail($e->getMessage() . ' [' . $e->getFile() . ':' . $e->getLine() . ']');
|
||
} finally {
|
||
if ($orderCacheLockAcquired && !$orderCacheKeepLock && $orderServices) {
|
||
$orderServices->releaseOrderCacheLock($orderCacheLockKey);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 规范化订单配送方式。
|
||
*
|
||
* delivery 是用户端确认页使用的快递标识,express 作为历史兼容别名;
|
||
* 其他值必须拒绝,不能默认降级为快递绕过商品配送能力校验。
|
||
*
|
||
* @param string $deliveryType 客户端配送方式
|
||
* @return string 规范化后的 delivery/pickup/door
|
||
* @throws \RuntimeException 配送方式不合法时抛出
|
||
*/
|
||
private function normalizeOrderDeliveryType(string $deliveryType): string
|
||
{
|
||
$deliveryType = trim($deliveryType);
|
||
if ($deliveryType === '' || $deliveryType === 'express') {
|
||
return 'delivery';
|
||
}
|
||
if (!in_array($deliveryType, ['delivery', 'pickup', 'door'], true)) {
|
||
throw new \RuntimeException('配送方式无效');
|
||
}
|
||
return $deliveryType;
|
||
}
|
||
|
||
/**
|
||
* 强制校验普通商品订单的配送方式及其关联数据。
|
||
*
|
||
* @param array $cartInfo 当前商品购物车快照
|
||
* @param string $deliveryType delivery/pickup/door
|
||
* @param int $uid 用户ID
|
||
* @param int $addressId 收货地址ID
|
||
* @param int $storeId 自提门店ID
|
||
* @param array $serviceData 上门服务信息
|
||
* @param bool $validateService 是否校验上门服务表单(金额计算接口不包含该表单)
|
||
* @return array{address_info: array, store_info: array}
|
||
* @throws \RuntimeException 配送方式、地址、门店或服务信息不合法时抛出
|
||
*/
|
||
private function validateNormalOrderDelivery(
|
||
array $cartInfo,
|
||
string $deliveryType,
|
||
int $uid,
|
||
int $addressId,
|
||
int $storeId,
|
||
array $serviceData = [],
|
||
bool $validateService = true
|
||
): array {
|
||
$deliveryMethod = [
|
||
'delivery' => 'express',
|
||
'pickup' => 'pickup',
|
||
'door' => 'door_service',
|
||
][$deliveryType] ?? '';
|
||
if ($deliveryMethod === '') {
|
||
throw new \RuntimeException('配送方式无效');
|
||
}
|
||
|
||
if (empty($cartInfo)) {
|
||
throw new \RuntimeException('订单商品信息已失效,请返回确认页重新提交');
|
||
}
|
||
if ($deliveryType !== 'pickup' && $storeId > 0) {
|
||
throw new \RuntimeException('当前配送方式不能选择自提门店');
|
||
}
|
||
|
||
// 合单订单必须要求每一件商品都支持用户选择的配送方式。
|
||
foreach ($cartInfo as $cartItem) {
|
||
$productInfo = $cartItem['productInfo'] ?? [];
|
||
$methods = $productInfo['delivery_methods'] ?? [];
|
||
if (is_string($methods)) {
|
||
$methods = json_decode($methods, true) ?: [];
|
||
}
|
||
$methods = is_array($methods) ? array_values(array_unique(array_filter($methods))) : [];
|
||
if (!$methods) {
|
||
if (!empty($productInfo['express_shipping'])) {
|
||
$methods[] = 'express';
|
||
}
|
||
if (!empty($productInfo['store_pickup'])) {
|
||
$methods[] = 'pickup';
|
||
}
|
||
if (!empty($productInfo['door_service'])) {
|
||
$methods[] = 'door_service';
|
||
}
|
||
}
|
||
if (!in_array($deliveryMethod, $methods, true)) {
|
||
throw new \RuntimeException('商品不支持当前配送方式');
|
||
}
|
||
}
|
||
|
||
$addressInfo = [];
|
||
$storeInfo = [];
|
||
if ($deliveryType === 'delivery') {
|
||
if ($addressId <= 0) {
|
||
throw new \RuntimeException('请选择收货地址');
|
||
}
|
||
/** @var UserAddressServices $addressServices */
|
||
$addressServices = app()->make(UserAddressServices::class);
|
||
$addressInfo = $addressServices->getOne([
|
||
'uid' => $uid,
|
||
'id' => $addressId,
|
||
'is_del' => 0,
|
||
]);
|
||
if (!$addressInfo) {
|
||
throw new \RuntimeException('收货地址不存在或已失效');
|
||
}
|
||
if (is_object($addressInfo)) {
|
||
$addressInfo = $addressInfo->toArray();
|
||
}
|
||
$addressText = implode('', array_filter([
|
||
$addressInfo['province'] ?? '',
|
||
$addressInfo['city'] ?? '',
|
||
$addressInfo['district'] ?? '',
|
||
$addressInfo['detail'] ?? '',
|
||
]));
|
||
if (empty($addressInfo['real_name']) || empty($addressInfo['phone']) || $addressText === '') {
|
||
throw new \RuntimeException('收货地址信息不完整');
|
||
}
|
||
} elseif ($deliveryType === 'pickup') {
|
||
if (!(int)sys_config('store_self_mention', 0)) {
|
||
throw new \RuntimeException('当前未开启到店自提');
|
||
}
|
||
if ($storeId <= 0) {
|
||
throw new \RuntimeException('请选择自提门店');
|
||
}
|
||
/** @var SystemStoreServices $storeServices */
|
||
$storeServices = app()->make(SystemStoreServices::class);
|
||
$storeInfo = $storeServices->getOne([
|
||
'id' => $storeId,
|
||
'is_show' => 1,
|
||
'is_del' => 0,
|
||
]);
|
||
if (!$storeInfo) {
|
||
throw new \RuntimeException('自提门店不存在或已停用');
|
||
}
|
||
if (is_object($storeInfo)) {
|
||
$storeInfo = $storeInfo->toArray();
|
||
}
|
||
} elseif ($validateService) {
|
||
if (trim((string)($serviceData['contact_name'] ?? '')) === '') {
|
||
throw new \RuntimeException('请填写上门服务联系人');
|
||
}
|
||
if (trim((string)($serviceData['contact_phone'] ?? '')) === '') {
|
||
throw new \RuntimeException('请填写上门服务联系电话');
|
||
}
|
||
if (trim((string)($serviceData['address'] ?? '')) === '') {
|
||
throw new \RuntimeException('请填写上门服务地址');
|
||
}
|
||
if (trim((string)($serviceData['appointment_time'] ?? '')) === '') {
|
||
throw new \RuntimeException('请选择上门服务时间');
|
||
}
|
||
}
|
||
|
||
return [
|
||
'address_info' => is_array($addressInfo) ? $addressInfo : [],
|
||
'store_info' => is_array($storeInfo) ? $storeInfo : [],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 构建中心商品下单的基础购物车数据和价格数据
|
||
* 提取为私有方法,供 confirm 和 create 共用
|
||
* @param int $uid 用户ID
|
||
* @param int $productId 中台商品ID
|
||
* @param string $skuUnique SKU唯一码
|
||
* @param string $productType 产品类型
|
||
* @param int $num 购买数量
|
||
* @param string $activityType 活动类型
|
||
* @param int $activityId 活动ID
|
||
* @return array [cartInfo, priceData, productInfo, skuInfo, registry]
|
||
*/
|
||
private function buildOrderData(int $uid, int $productId, string $skuUnique, string $productType, int $num, string $activityType = 'normal', int $activityId = 0): array
|
||
{
|
||
/** @var CenterProductServices $centerProductServices */
|
||
$centerProductServices = app()->make(CenterProductServices::class);
|
||
$productData = $centerProductServices->getInfo($productId);
|
||
if (!$productData) {
|
||
throw new \RuntimeException('商品不存在');
|
||
}
|
||
// CenterProductServices::getInfo 返回包含 tempList/cateList/productInfo 的数组,真实商品信息在 productInfo 键中
|
||
$productInfo = $productData['productInfo'] ?? $productData;
|
||
if (!$productInfo) {
|
||
throw new \RuntimeException('商品不存在');
|
||
}
|
||
|
||
$num = max(1, $num);
|
||
$minQty = max(1, (int)($productInfo['min_qty'] ?? 1));
|
||
if ($num < $minQty) {
|
||
throw new \RuntimeException('购买数量不能少于起购数量 ' . $minQty);
|
||
}
|
||
if (!empty($productInfo['is_limit']) && (int)($productInfo['limit_type'] ?? 0) === 1
|
||
&& $num > (int)($productInfo['limit_num'] ?? 0)) {
|
||
throw new \RuntimeException('购买数量不能超过限购数量');
|
||
}
|
||
|
||
// 优先使用产品中心记录的实际 product_type(normal/virtual/cardkey),
|
||
// 覆盖前端传入的 product_type 参数,确保不同类型的商品 SKU 能通过正确的 Handler 读取
|
||
$actualProductType = (string)($productInfo['product_type'] ?? '');
|
||
if ($actualProductType !== '') {
|
||
$productType = $actualProductType;
|
||
}
|
||
|
||
/** @var ProductTypeRegistry $registry */
|
||
$registry = app()->make(ProductTypeRegistry::class);
|
||
$handler = $registry->get($productType);
|
||
$shippingInfo = $handler->getShippingInfo($productId);
|
||
$skuInfo = $handler->getSkuInfo($productId, $skuUnique);
|
||
if (!$skuInfo) {
|
||
throw new \RuntimeException('商品规格不可用');
|
||
}
|
||
if (!$activityId && (($handler->getCapabilities()['order']['virtual'] ?? false) !== true) && (int)($skuInfo['stock'] ?? 0) < $num) {
|
||
throw new \RuntimeException('商品库存不足');
|
||
}
|
||
|
||
$truePrice = $skuInfo['price'] ?? $productInfo['price'] ?? 0;
|
||
$defaultSkuInfo = array_merge([
|
||
'weight' => 0,
|
||
'volume' => 0,
|
||
], is_array($skuInfo) ? $skuInfo : []);
|
||
// SKU 级别的重量/体积为 0 时,使用商品级别的值作为 fallback(供运费模板计算使用)
|
||
// 优先使用 getShippingInfo() 从扩展表读取的值,其次使用 productInfo
|
||
if (empty($defaultSkuInfo['weight'])) {
|
||
$defaultSkuInfo['weight'] = $shippingInfo['weight'] ?? $productInfo['weight'] ?? 0;
|
||
}
|
||
if (empty($defaultSkuInfo['volume'])) {
|
||
$defaultSkuInfo['volume'] = $shippingInfo['volume'] ?? $productInfo['volume'] ?? 0;
|
||
}
|
||
// 解析 suk JSON,构建可读的规格标签
|
||
if (!empty($defaultSkuInfo['suk'])) {
|
||
$sukParsed = json_decode($defaultSkuInfo['suk'], true);
|
||
if (is_array($sukParsed)) {
|
||
$defaultSkuInfo['suk'] = implode(' / ', array_values($sukParsed));
|
||
}
|
||
}
|
||
|
||
$isVirtual = (($handler->getCapabilities()['order']['virtual'] ?? false) === true);
|
||
|
||
// 从商品信息中读取是否支持快递配送(已在 getInfo 中解析 logistics)
|
||
$productExpressShipping = $productInfo['express_shipping'] ?? false;
|
||
|
||
// 处理图片URL
|
||
$image = $productInfo['image'] ?? '';
|
||
if ($image && !str_starts_with($image, 'http') && !str_starts_with($image, '//')) {
|
||
$image = sys_config('site_url') . $image;
|
||
}
|
||
|
||
// 分类ID可能是数组(CenterProductServices::getInfo 已解析),转字符串避免 Array to string conversion
|
||
$cateId = $productInfo['cate_id'] ?? '';
|
||
if (is_array($cateId)) {
|
||
$cateId = implode(',', $cateId);
|
||
}
|
||
|
||
$isActivityOrder = $activityType !== 'normal' && $activityId > 0;
|
||
$activityPriceInfo = [];
|
||
$activityIntegral = 0;
|
||
if ($isActivityOrder) {
|
||
/** @var TradeModeRegistry $tradeModeRegistry */
|
||
$tradeModeRegistry = app()->make(TradeModeRegistry::class);
|
||
$tradeModeHandler = $tradeModeRegistry->get($activityType);
|
||
$activityPriceInfo = $tradeModeHandler->getActivityPrice($productId, $skuUnique, $activityId);
|
||
// 活动价覆盖真实单价
|
||
if (!empty($activityPriceInfo['unit_price'])) {
|
||
$truePrice = $activityPriceInfo['unit_price'];
|
||
}
|
||
// 积分兑换场景记录积分数量
|
||
$activityIntegral = (int) ($activityPriceInfo['currency_amount'] ?? 0);
|
||
}
|
||
|
||
$cartInfo = [[
|
||
'id' => $productId,
|
||
'product_id' => $productId,
|
||
'cart_num' => $num,
|
||
'truePrice' => $truePrice,
|
||
'trueStock' => $activityPriceInfo['stock'] ?? $skuInfo['stock'] ?? 0,
|
||
'costPrice' => $skuInfo['cost'] ?? 0,
|
||
'sum_price' => $skuInfo['price'] ?? $productInfo['price'] ?? 0,
|
||
'vip_truePrice' => 0,
|
||
'price_type' => '',
|
||
'product_attr_unique' => $skuInfo['unique'] ?? '',
|
||
'type' => 0,
|
||
'seckill_id' => 0,
|
||
'bargain_id' => 0,
|
||
'combination_id' => 0,
|
||
'advance_id' => 0,
|
||
'activity_type' => $activityType,
|
||
'activity_id' => $activityId,
|
||
'activity_integral' => $activityIntegral,
|
||
'productInfo' => array_merge($productInfo, [
|
||
'weight' => $shippingInfo['weight'],
|
||
'volume' => $shippingInfo['volume'],
|
||
'give_integral' => '0',
|
||
'presale' => 0,
|
||
'is_virtual' => $isVirtual ? 1 : 0,
|
||
'gift_price' => '0.00',
|
||
'store_name' => $productInfo['name'] ?? '',
|
||
'image' => $image,
|
||
'cate_id' => (string)$cateId,
|
||
'product_type' => $productType,
|
||
'attrInfo' => $defaultSkuInfo, // 运费模板按重量/体积计算时需要
|
||
]),
|
||
]];
|
||
|
||
// 普通商品直购必须沿用购物车的会员价计算规则,确认页与创建订单使用同一用户价格。
|
||
$pricingPolicy = method_exists($handler, 'getPricingPolicy') ? $handler->getPricingPolicy() : [];
|
||
$memberDiscount = 0;
|
||
$paidMemberDiscount = 0;
|
||
$priceType = '';
|
||
if (!$isActivityOrder && $productType === 'normal') {
|
||
/** @var UserServices $userServices */
|
||
$userServices = app()->make(UserServices::class);
|
||
$userInfo = $userServices->getUserInfo($uid);
|
||
if (!$userInfo) {
|
||
throw new \RuntimeException('用户信息不存在');
|
||
}
|
||
if (is_object($userInfo)) {
|
||
$userInfo = $userInfo->toArray();
|
||
}
|
||
|
||
$discount = 100;
|
||
if (sys_config('member_func_status', 1)) {
|
||
/** @var SystemUserLevelServices $systemLevelServices */
|
||
$systemLevelServices = app()->make(SystemUserLevelServices::class);
|
||
$discount = $systemLevelServices->value([
|
||
'id' => $userInfo['level'] ?? 0,
|
||
'is_del' => 0,
|
||
'is_show' => 1,
|
||
], 'discount') ?: 100;
|
||
}
|
||
/** @var MemberCardServices $memberCardServices */
|
||
$memberCardServices = app()->make(MemberCardServices::class);
|
||
$vipStatus = $memberCardServices->isOpenMemberCard('vip_price', false)
|
||
&& (int)($userInfo['is_money_level'] ?? 0) > 0;
|
||
[$truePrice, $unitDiscount, $priceType] = $centerProductServices->setLevelPrice(
|
||
$skuInfo['price'] ?? $productInfo['price'] ?? 0,
|
||
$uid,
|
||
$userInfo,
|
||
$vipStatus,
|
||
($pricingPolicy['member_discount'] ?? true) ? $discount : 100,
|
||
($pricingPolicy['vip_discount'] ?? true) ? ($skuInfo['vip_price'] ?? 0) : 0,
|
||
($pricingPolicy['vip_discount'] ?? true) ? ($productInfo['is_vip'] ?? 0) : 0,
|
||
true
|
||
);
|
||
$cartInfo[0]['truePrice'] = $truePrice;
|
||
$cartInfo[0]['vip_truePrice'] = $unitDiscount;
|
||
$cartInfo[0]['price_type'] = $priceType;
|
||
$discountTotal = bcmul((string)$unitDiscount, (string)$num, 2);
|
||
if ($priceType === 'level') {
|
||
$memberDiscount = $discountTotal;
|
||
} elseif ($priceType === 'member') {
|
||
$paidMemberDiscount = $discountTotal;
|
||
}
|
||
}
|
||
|
||
$payPostage = 0;
|
||
// 不支持快递配送时,运费强制为0
|
||
if ($productExpressShipping) {
|
||
if ($shippingInfo['freight'] == 2 && $shippingInfo['postage'] > 0) {
|
||
// 固定运费
|
||
$payPostage = $shippingInfo['postage'] * $cartInfo[0]['cart_num'];
|
||
} elseif ($shippingInfo['freight'] == 3) {
|
||
// 运费模板:尝试使用用户默认地址计算运费
|
||
try {
|
||
/** @var UserAddressServices $addressServices */
|
||
$addressServices = app()->make(UserAddressServices::class);
|
||
$addr = $addressServices->getUserDefaultAddress($uid);
|
||
if ($addr) {
|
||
$addr = $addr->toArray();
|
||
/** @var OrderComputedServices $computedServices */
|
||
$computedServices = app()->make(OrderComputedServices::class);
|
||
$priceGroup = $computedServices->getOrderPriceGroup(0, $cartInfo, $addr, []);
|
||
$payPostage = $priceGroup['storePostage'] ?? 0;
|
||
}
|
||
} catch (\Throwable $e) {
|
||
// 地址不存在或计算失败,运费为0
|
||
}
|
||
}
|
||
}
|
||
|
||
$totalPrice = bcmul((string)$cartInfo[0]['truePrice'], (string)$cartInfo[0]['cart_num'], 2);
|
||
// 活动订单没有等级/会员折扣,原价总额取活动价;普通订单取规格原价
|
||
$sumPrice = $isActivityOrder
|
||
? $totalPrice
|
||
: bcmul((string)($skuInfo['price'] ?? $productInfo['price'] ?? 0), (string)$num, 2);
|
||
$priceData = [
|
||
'totalPrice' => $totalPrice,
|
||
// 规格原价总额(等级/会员优惠前),与 cartInfo[0].sum_price 对齐
|
||
'sumPrice' => $sumPrice,
|
||
'payPrice' => bcadd($totalPrice, (string)$payPostage, 2),
|
||
'payPostage' => $payPostage,
|
||
'isStoreFreePostage' => false,
|
||
'storeFreePostage' => 0,
|
||
'costPrice' => bcmul((string)$cartInfo[0]['costPrice'], (string)$cartInfo[0]['cart_num'], 2),
|
||
'giftPrice' => 0,
|
||
'activityIntegral' => $activityIntegral,
|
||
'levelPrice' => $memberDiscount,
|
||
'memberPrice' => $paidMemberDiscount,
|
||
];
|
||
|
||
return [$cartInfo, $priceData, $productInfo, $skuInfo, $registry];
|
||
}
|
||
|
||
/**
|
||
* 构建商品规格快照,供订单 extra_info 使用
|
||
* 从购物车商品信息中提取 SKU 唯一编码、规格名称、图片等字段
|
||
* @param array $cart 购物车单项
|
||
* @return array 返回规格快照,无规格时返回空数组
|
||
*/
|
||
private function buildSkuInfoForExtra(array $cart): array
|
||
{
|
||
$attrInfo = $cart['productInfo']['attrInfo'] ?? [];
|
||
if (empty($attrInfo)) {
|
||
return [];
|
||
}
|
||
return [
|
||
'unique' => $attrInfo['unique'] ?? '',
|
||
'suk' => $attrInfo['suk'] ?? '',
|
||
'image' => $attrInfo['image'] ?? '',
|
||
'price' => $attrInfo['price'] ?? 0,
|
||
'ot_price' => $attrInfo['ot_price'] ?? 0,
|
||
'product_id' => $cart['product_id'] ?? $cart['productInfo']['id'] ?? 0,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 构建商品明细快照,供订单 extra_info.items 使用
|
||
* 格式兼容 loadOrderItems 和退款页面所需字段
|
||
* @param array $cartInfo 购物车商品列表
|
||
* @return array 返回商品明细快照列表
|
||
*/
|
||
private function buildOrderItemsForExtra(array $cartInfo): array
|
||
{
|
||
$items = [];
|
||
foreach ($cartInfo as $index => $item) {
|
||
$items[] = [
|
||
'cart_id' => $item['id'] ?? ($index + 1),
|
||
'product_id' => $item['product_id'] ?? $item['productInfo']['id'] ?? 0,
|
||
'image' => $item['productInfo']['image'] ?? '',
|
||
'store_name' => $item['productInfo']['store_name'] ?? '',
|
||
'price' => $item['productInfo']['price'] ?? '0.00',
|
||
'true_price' => $item['truePrice'] ?? '0.00',
|
||
// 商品级购买送积分快照(eb_product_center.give_integral),收货时按数量聚合发放
|
||
'give_integral' => (float)($item['productInfo']['give_integral'] ?? 0),
|
||
'cart_num' => $item['cart_num'] ?? 1,
|
||
'product_type' => $item['productInfo']['product_type'] ?? '',
|
||
'attr_info' => $item['productInfo']['attrInfo'] ?? [],
|
||
'sku' => $item['productInfo']['attrInfo']['suk'] ?? '默认',
|
||
'postage_price' => $item['postage_price'] ?? '0.00',
|
||
];
|
||
}
|
||
return $items;
|
||
}
|
||
|
||
/**
|
||
* 订单支付
|
||
* 从中台表读取订单,调用对应支付方式完成支付
|
||
*/
|
||
public function pay(YuePayServices $yuePayServices, OrderPayServices $payServices)
|
||
{
|
||
[$uni, $paytype, $quitUrl, $type] = request()->postMore([
|
||
['uni', ''],
|
||
['paytype', ''],
|
||
['quitUrl', ''],
|
||
['type', 0]
|
||
], true);
|
||
|
||
if (!$uni) return app('json')->fail('参数错误');
|
||
|
||
$payLock = CacheService::get('PAY_LOCK_' . $uni);
|
||
if ($payLock) return app('json')->fail('订单支付中,请勿重复支付');
|
||
CacheService::set('PAY_LOCK_' . $uni, 'PAY_LOCK', 2);
|
||
|
||
// 从中台表查询订单
|
||
/** @var OrderCenterUserServices $centerServices */
|
||
$centerServices = app()->make(OrderCenterUserServices::class);
|
||
$centerOrder = $centerServices->getDetailByOrderNo($uni, false);
|
||
if (!$centerOrder) {
|
||
CacheService::delete('PAY_LOCK_' . $uni);
|
||
return app('json')->fail('订单不存在');
|
||
}
|
||
|
||
if (in_array($centerOrder['status_code'] ?? '', [OrderCenter::STATUS_CANCELED, OrderCenter::STATUS_DELETED], true)) {
|
||
return app('json')->fail('订单已经超过系统支付时间,无法支付,请重新下单');
|
||
}
|
||
if ($centerOrder['pay_status'] == 1) {
|
||
return app('json')->fail('订单已支付');
|
||
}
|
||
|
||
$uid = $type == 1 ? (int)request()->uid() : $centerOrder['uid'];
|
||
$orderNo = $centerOrder['order_no'] ?? $uni;
|
||
|
||
// 构建兼容支付服务接口的订单数组
|
||
$orderInfo = [
|
||
'id' => $centerOrder['id'] ?? 0,
|
||
'order_id' => $orderNo,
|
||
'uid' => $centerOrder['uid'],
|
||
'pay_uid' => $centerOrder['uid'],
|
||
'pay_price' => $centerOrder['pay_price'] ?? '0.00',
|
||
'pay_postage' => $centerOrder['pay_postage'] ?? '0.00',
|
||
'paid' => 0,
|
||
'is_channel' => request()->isApp() ? 0 : 1,
|
||
'unique' => $orderNo,
|
||
'is_cancel' => 0,
|
||
'is_del' => 0,
|
||
'is_system_del' => 0,
|
||
'combination_id' => 0,
|
||
'seckill_id' => 0,
|
||
'bargain_id' => 0,
|
||
'refund_status' => 0,
|
||
'real_name' => $centerOrder['extra_info']['contact']['name'] ?? '',
|
||
'storeName' => $centerOrder['title'] ?? '',
|
||
];
|
||
|
||
// 0元支付或免费活动订单
|
||
$isActivityOrder = !empty($centerOrder['activity_type']) && $centerOrder['activity_type'] !== 'normal' && (int)($centerOrder['activity_id'] ?? 0) > 0;
|
||
$activityType = $centerOrder['activity_type'] ?? 'normal';
|
||
$activityId = (int)($centerOrder['activity_id'] ?? 0);
|
||
$tradeModeHandler = null;
|
||
if ($isActivityOrder) {
|
||
/** @var TradeModeRegistry $tradeModeRegistry */
|
||
$tradeModeRegistry = app()->make(TradeModeRegistry::class);
|
||
$tradeModeHandler = $tradeModeRegistry->get($activityType);
|
||
}
|
||
|
||
$isFreePay = false;
|
||
if ($isActivityOrder && $tradeModeHandler) {
|
||
$isFreePay = $tradeModeHandler->isFreePay($activityId);
|
||
}
|
||
|
||
if (bcsub((string)$orderInfo['pay_price'], '0', 2) <= 0 || ($isActivityOrder && $isFreePay)) {
|
||
// 活动订单在 0 元支付前先执行活动支付处理(如扣减积分)
|
||
if ($isActivityOrder && $tradeModeHandler) {
|
||
try {
|
||
$extraInfo = $centerOrder['extra_info'] ?? [];
|
||
$skuUnique = $extraInfo['sku_info']['unique'] ?? '';
|
||
$tradeModeHandler->beforeOrderPay($orderNo, $activityId, $centerOrder);
|
||
$tradeModeHandler->onOrderPay($orderNo, $activityId, array_merge($centerOrder, [
|
||
'sku_unique' => $skuUnique,
|
||
]));
|
||
} catch (\Throwable $e) {
|
||
CacheService::delete('PAY_LOCK_' . $uni);
|
||
return app('json')->status('pay_error', $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/** @var OrderSuccessServices $success */
|
||
$success = app()->make(OrderSuccessServices::class);
|
||
// 免费活动订单使用 free 支付方式记录,避免被记录为余额支付
|
||
$freePayType = $isActivityOrder && $isFreePay ? 'free' : PayServices::YUE_PAY;
|
||
$payPriceStatus = $success->zeroYuanPayment($orderInfo, $uid, $freePayType);
|
||
if ($payPriceStatus) {
|
||
// 0 元支付成功后触发活动支付成功回调(如扣减活动库存)
|
||
if ($isActivityOrder && $tradeModeHandler) {
|
||
try {
|
||
$extraInfo = $centerOrder['extra_info'] ?? [];
|
||
$skuUnique = $extraInfo['sku_info']['unique'] ?? '';
|
||
$tradeModeHandler->afterOrderPay($orderNo, $activityId, array_merge($centerOrder, [
|
||
'sku_unique' => $skuUnique,
|
||
]));
|
||
} catch (\Throwable $e) {
|
||
\think\facade\Log::warning('活动支付成功回调执行失败: ' . $e->getMessage());
|
||
}
|
||
}
|
||
return app('json')->status('success', '支付成功', ['order_id' => $orderNo, 'key' => $orderNo]);
|
||
}
|
||
|
||
// 0 元支付失败时,触发活动支付失败回调回滚已扣减资源(如积分)
|
||
if ($isActivityOrder && $tradeModeHandler) {
|
||
try {
|
||
$extraInfo = $centerOrder['extra_info'] ?? [];
|
||
$skuUnique = $extraInfo['sku_info']['unique'] ?? '';
|
||
$tradeModeHandler->onOrderPayFail($orderNo, $activityId, array_merge($centerOrder, [
|
||
'sku_unique' => $skuUnique,
|
||
]));
|
||
} catch (\Throwable $e) {
|
||
\think\facade\Log::warning('活动支付失败回调执行失败: ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
return app('json')->status('pay_error', '支付失败');
|
||
}
|
||
|
||
app()->make(PayRecordServices::class)->assertChannel($orderNo, (string)$paytype);
|
||
switch ($paytype) {
|
||
case PayServices::YUE_PAY:
|
||
$pay = $yuePayServices->yueOrderPay($orderInfo, (int)request()->uid());
|
||
if ($pay['status'] === true) {
|
||
return app('json')->status('success', '余额支付成功');
|
||
}
|
||
if (is_array($pay)) {
|
||
return app('json')->status($pay['status'], $pay['msg']);
|
||
}
|
||
return app('json')->status('pay_error', $pay);
|
||
|
||
case PayServices::OFFLINE_PAY:
|
||
/** @var OrderCenterServices $centerServices */
|
||
$centerServices = app()->make(OrderCenterServices::class);
|
||
if ($centerServices->setOrderTypePayOffline($orderNo)) {
|
||
return app('json')->status('success', '订单创建成功');
|
||
}
|
||
return app('json')->status('success', '支付失败');
|
||
|
||
default:
|
||
$payOptions = array_merge(PayCredentialService::getCredentials((int)$uid, $paytype), ['quitUrl' => $quitUrl, 'payer_uid' => (int)$uid]);
|
||
$payInfo = $payServices->beforePay($orderInfo, $paytype, $payOptions);
|
||
return app('json')->status($payInfo['status'], $payInfo['payInfo']);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 订单收银台
|
||
* 返回支付渠道开关和订单基本信息
|
||
*/
|
||
public function cashier($orderNo, $type = 'order')
|
||
{
|
||
if (!$orderNo) {
|
||
return app('json')->fail('参数错误');
|
||
}
|
||
|
||
/** @var PayChannelServices $payChannelServices */
|
||
$payChannelServices = app()->make(PayChannelServices::class);
|
||
$data = [
|
||
'ali_pay_status' => $payChannelServices->getChannelStatus('alipay'),
|
||
'wechat_pay_status' => $payChannelServices->getChannelStatus('weixin') || $payChannelServices->getChannelStatus('weixin_v3'),
|
||
'offline_pay_status'=> $payChannelServices->getChannelStatus('offline'),
|
||
'friend_pay_status' => $payChannelServices->getChannelStatus('friend'),
|
||
'yue_pay_status' => $payChannelServices->getChannelStatus('yue'),
|
||
];
|
||
|
||
$data['order_id'] = $orderNo;
|
||
$data['pay_price'] = '0';
|
||
$data['now_money'] = app()->make(UserServices::class)->value(['uid' => (int)request()->uid()], 'now_money');
|
||
|
||
switch ($type) {
|
||
case 'order':
|
||
/** @var OrderCenterUserServices $centerServices */
|
||
$centerServices = app()->make(OrderCenterUserServices::class);
|
||
$order = $centerServices->getDetailByOrderNo($orderNo, false);
|
||
if (!$order) {
|
||
return app('json')->fail('您支付的订单不存在');
|
||
}
|
||
$orderCancelTime = sys_config('order_cancel_time', 0);
|
||
$time = $orderCancelTime * 60 * 60 + ($order['add_time'] ?? time());
|
||
|
||
if ($time < 0) {
|
||
$time = 0;
|
||
}
|
||
|
||
$data['pay_price'] = $order['pay_price'] ?? '0';
|
||
$data['pay_postage'] = $order['pay_postage'] ?? '0';
|
||
$data['offline_postage'] = (int)sys_config('offline_postage', 0);
|
||
$data['invalid_time'] = $time;
|
||
$data['oid'] = $order['id'] ?? 0;
|
||
// 送礼订单:支付成功后跳支付结果页并展示送礼分享
|
||
$payExtra = $order['extra_info'] ?? [];
|
||
$payExtra = is_string($payExtra) ? (json_decode($payExtra, true) ?: []) : ($payExtra ?: []);
|
||
$isGiftOrder = !empty($payExtra['gift']) || !empty($payExtra['gift_mark']) || (float)($order['gift_price'] ?? 0) > 0;
|
||
$data['is_gift'] = $isGiftOrder ? 1 : 0;
|
||
$data['gift_mark'] = (string)($payExtra['gift_mark'] ?? '');
|
||
break;
|
||
|
||
default:
|
||
return app('json')->fail('暂不支持其他类型订单支付');
|
||
}
|
||
|
||
return app('json')->success($data);
|
||
}
|
||
|
||
/**
|
||
* 商家寄件回调
|
||
* @return \think\Response
|
||
* @author 等风来
|
||
* @email 136327134@qq.com
|
||
* @date 2023/6/12
|
||
*/
|
||
public function callBack()
|
||
{
|
||
$orderDao = app()->make(OrderCenterDao::class);
|
||
$data = request()->postMore([
|
||
['type', ''],
|
||
['data', ''],
|
||
]);
|
||
$data['data'] = $this->decrypt($data['data'], sys_config('yihaotong_appsecret'));
|
||
switch ($data['type']) {
|
||
case 'detection'://检测回调地址
|
||
return \json($data['data']);
|
||
break;
|
||
case 'order_success'://下单成功
|
||
$update = [
|
||
'label' => $data['data']['label'] ?? '',
|
||
];
|
||
//韵达会异步推送单号
|
||
if (isset($data['kuaidinum'])) {
|
||
$update['delivery_id'] = $data['kuaidinum'];
|
||
}
|
||
if (isset($data['task_id'])) {
|
||
$orderDao->update(['kuaidi_task_id' => $data['task_id']], $update);
|
||
}
|
||
break;
|
||
case 'order_take'://取件
|
||
if (isset($data['data']['task_id'])) {
|
||
$orderInfo = $orderDao->get(['kuaidi_task_id' => $data['data']['task_id']]);
|
||
if (!$orderInfo) {
|
||
return app('json')->fail('订单不存在');
|
||
}
|
||
$orderDao->update(['kuaidi_task_id' => $data['data']['task_id']], [
|
||
'status' => 1,
|
||
'is_stock_up' => 0
|
||
]);
|
||
// eb_store_order_status 表已删除,状态记录已移除
|
||
}
|
||
break;
|
||
case 'order_cancel'://取消寄件
|
||
if (isset($data['data']['task_id'])) {
|
||
$orderInfo = $orderDao->get(['kuaidi_task_id' => $data['data']['task_id']]);
|
||
if (!$orderInfo) {
|
||
return app('json')->fail('订单不存在');
|
||
}
|
||
if ($orderInfo->is_stock_up && $orderInfo->status == 0) {
|
||
$orderInfo->status = 0;
|
||
$orderInfo->is_stock_up = 0;
|
||
$orderInfo->kuaidi_task_id = '';
|
||
$orderInfo->kuaidi_order_id = '';
|
||
$orderInfo->express_dump = '';
|
||
$orderInfo->kuaidi_label = '';
|
||
$orderInfo->delivery_id = '';
|
||
$orderInfo->delivery_code = '';
|
||
$orderInfo->delivery_name = '';
|
||
$orderInfo->delivery_type = '';
|
||
$orderInfo->save();
|
||
} else {
|
||
Log::error('商家寄件自动回调,订单状态不正确:', [
|
||
'kuaidi_task_id' => $data['data']['task_id']
|
||
]);
|
||
}
|
||
}
|
||
break;
|
||
case 'success'://电子发票回调
|
||
$oid = $orderDao->value(['order_id' => $data['data']['unique']], 'id');
|
||
if ($oid) {
|
||
$invoiceServices = app()->make(OrderInvoiceServices::class);
|
||
$invoiceServices->update([
|
||
'category' => 'order',
|
||
'order_id' => $oid,
|
||
], [
|
||
'unique' => $data['data']['unique'],
|
||
'invoice_type' => $data['data']['invoice_type'],
|
||
'invoice_num' => $data['data']['invoice_num'],
|
||
'invoice_serial_number' => $data['data']['invoice_serial_number'],
|
||
'is_invoice' => 1,
|
||
'invoice_time' => time()
|
||
]);
|
||
}
|
||
break;
|
||
}
|
||
|
||
return app('json')->success();
|
||
}
|
||
|
||
/**
|
||
* 解密商家寄件回调
|
||
* @param string $encryptedData
|
||
* @param string $key
|
||
* @return false|string
|
||
* @author: 吴汐
|
||
* @email: 442384644@qq.com
|
||
* @date: 2023/8/31
|
||
*/
|
||
private function decrypt(string $encryptedData, string $key)
|
||
{
|
||
$key = substr($key, 0, 32);
|
||
$decodedData = base64_decode($encryptedData);
|
||
$iv = substr($decodedData, 0, 16);
|
||
$encrypted = substr($decodedData, 16);
|
||
$decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
|
||
return json_decode($decrypted, true);
|
||
}
|
||
|
||
/**
|
||
* 订单取消 未支付的订单回退积分,回退优惠券,回退库存
|
||
* @return mixed
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
*/
|
||
public function cancel()
|
||
{
|
||
[$id] = request()->postMore([['id', 0]], true);
|
||
if (!$id) return app('json')->fail('参数错误');
|
||
$orderCenterServices = app()->make(OrderCenterUserServices::class);
|
||
if ($orderCenterServices->cancelOrder($id, (int)request()->uid()))
|
||
return app('json')->success('取消成功');
|
||
return app('json')->fail('取消失败');
|
||
}
|
||
|
||
/**
|
||
* 获取确认订单页面是否展示快递配送和到店自提
|
||
* @return mixed
|
||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||
*/
|
||
public function checkShipping()
|
||
{
|
||
[$cartId, $new] = request()->postMore(['cartId', 'new'], true);
|
||
$orderCenterServices = app()->make(OrderCenterUserServices::class);
|
||
return app('json')->success($orderCenterServices->checkShipping((int)request()->uid(), $cartId, (bool)$new));
|
||
}
|
||
|
||
/**
|
||
* 退款订单详情
|
||
* @param $uni
|
||
* @param string $cartId
|
||
* @return mixed
|
||
*/
|
||
public function refundDetail($uni, $cartId = '')
|
||
{
|
||
if (!strlen(trim($uni))) return app('json')->fail('参数错误');
|
||
$orderCenterServices = app()->make(OrderCenterUserServices::class);
|
||
$order = $orderCenterServices->getDetailByOrderNo($uni, false);
|
||
if (!$order) return app('json')->fail('订单不存在');
|
||
// 订单详情格式化(OrderOutServices 已中台化,tidyOrder 基于中台数据)
|
||
$orderData = app()->make(OrderOutServices::class)->tidyOrder($order, true);
|
||
foreach ($orderData['cartInfo'] ?? [] as $key => $cart) {
|
||
$orderData['cartInfo'][$key]['one_postage_price'] = isset($cart['postage_price']) ? bcdiv($cart['postage_price'], $cart['cart_num'], 2) : 0;
|
||
if ($cartId !== '') {
|
||
if ($cart['id'] != $cartId) {
|
||
unset($orderData['cartInfo'][$key]);
|
||
} else {
|
||
$orderData['total_num'] = $orderData['cartInfo'][$key]['cart_num'];
|
||
$orderData['pay_price'] = bcadd(bcmul($cart['truePrice'], $orderData['total_num'], 4), bcmul($orderData['total_num'], $orderData['cartInfo'][$key]['one_postage_price'], 4), 2);
|
||
}
|
||
}
|
||
}
|
||
$orderData['cartInfo'] = array_merge($orderData['cartInfo']);
|
||
return app('json')->success($orderData);
|
||
}
|
||
|
||
/**
|
||
* 订单退款理由
|
||
* @return mixed
|
||
*/
|
||
public function refundReason()
|
||
{
|
||
$reason = sys_config('stor_reason') ?: [];//退款理由
|
||
$reason = str_replace("\r\n", "\n", $reason);//防止不兼容
|
||
$reason = explode("\n", $reason);
|
||
return app('json')->success($reason);
|
||
}
|
||
|
||
/**
|
||
* 订单收货
|
||
* @param OrderTakeServices $services
|
||
* @param StoreCouponIssueServices $issueServices
|
||
* @return mixed
|
||
*/
|
||
public function take(OrderTakeServices $services, StoreCouponIssueServices $issueServices)
|
||
{
|
||
[$uni] = request()->postMore([
|
||
['uni', ''],
|
||
], true);
|
||
if (!$uni) return app('json')->fail('参数错误');
|
||
$order = $services->takeOrder($uni, (int)request()->uid());
|
||
if ($order) {
|
||
return app('json')->success('收货成功');
|
||
} else
|
||
return app('json')->fail('收货失败');
|
||
}
|
||
|
||
/**
|
||
* 订单 查看物流
|
||
* @param ExpressServices $expressServices
|
||
* @param $uni
|
||
* @param string $type
|
||
* @return mixed
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
*/
|
||
public function express(ExpressServices $expressServices, $uni, $type = '')
|
||
{
|
||
$orderCenterServices = app()->make(OrderCenterUserServices::class);
|
||
// 售后退货/补发物流查询:$uni 为售后单号,校验归属后复用 service 层物流查询能力
|
||
if ($type === 'aftersale' || $type === 'aftersale_resend') {
|
||
$afterSaleServices = app()->make(AfterSaleServices::class);
|
||
// 校验售后单归属当前用户,防止越权查询他人售后物流
|
||
$afterSale = $afterSaleServices->getOwnedByNo($uni, (int)request()->uid());
|
||
if (empty($afterSale)) {
|
||
return app('json')->fail('售后单不存在');
|
||
}
|
||
// 复用 service 层物流查询(含快递公司名称→编码解析),return=退货物流 resend=补发物流
|
||
$logisticsType = $type === 'aftersale_resend' ? 'resend' : 'return';
|
||
try {
|
||
$expressData = $afterSaleServices->queryExpress((int)$afterSale['id'], $logisticsType);
|
||
} catch (\RuntimeException $e) {
|
||
return app('json')->fail($e->getMessage());
|
||
}
|
||
// 从售后商品快照构造展示信息(物流页商品列表)
|
||
$info = [];
|
||
$snapshot = is_array($afterSale['product_snapshot']) ? $afterSale['product_snapshot'] : [];
|
||
foreach ($snapshot['items'] ?? [] as $item) {
|
||
$info[] = [
|
||
'cart_num' => $item['cart_num'] ?? 1,
|
||
'truePrice' => $item['true_price'] ?? '0.00',
|
||
'postage_price' => $item['postage_price'] ?? '0.00',
|
||
'productInfo' => [
|
||
'image' => $item['image'] ?? '',
|
||
'store_name' => $item['store_name'] ?? '',
|
||
'unit_name' => $item['unit_name'] ?? '',
|
||
],
|
||
];
|
||
}
|
||
// 快照为空时回退到订单中台读取商品列表(兼容早期未填充快照的售后单)
|
||
if (empty($info) && !empty($afterSale['order_no'])) {
|
||
try {
|
||
$sourceOrder = $orderCenterServices->getDetailByOrderNo($afterSale['order_no'], false);
|
||
foreach ($sourceOrder['order_items'] ?? [] as $item) {
|
||
$info[] = [
|
||
'cart_num' => $item['cart_num'] ?? 1,
|
||
'truePrice' => $item['true_price'] ?? $item['price'] ?? '0.00',
|
||
'postage_price' => $item['postage_price'] ?? '0.00',
|
||
'productInfo' => [
|
||
'image' => $item['image'] ?? '',
|
||
'store_name' => $item['store_name'] ?? '',
|
||
'unit_name' => $item['unit_name'] ?? '',
|
||
],
|
||
];
|
||
}
|
||
} catch (\Throwable $e) {
|
||
// 订单读取失败不阻断物流查询,商品列表留空
|
||
}
|
||
}
|
||
$orderInfo = [
|
||
'delivery_id' => $expressData['delivery_id'],
|
||
'delivery_name' => $expressData['delivery_name'],
|
||
'delivery_code' => '',
|
||
'delivery_type' => 'express',
|
||
'cartInfo' => $info,
|
||
];
|
||
return app('json')->success([
|
||
'order' => $orderInfo,
|
||
'express' => [
|
||
'result' => ['list' => $expressData['result']]
|
||
]
|
||
]);
|
||
}
|
||
// type=refund 旧退款物流分支已下线,退货/补发物流统一走上方 aftersale 分支
|
||
try {
|
||
$ownedId = $orderCenterServices->getOwnedOrderIdByNo((string)$uni, (int)request()->uid());
|
||
if ($ownedId === null) {
|
||
return app('json')->fail('订单不存在');
|
||
}
|
||
// 物流接口自身会查询轨迹,这里只取订单/配送信息,避免重复请求第三方
|
||
$order = $orderCenterServices->getDetail($ownedId, false);
|
||
} catch (\Throwable $e) {
|
||
return app('json')->fail('订单不存在');
|
||
}
|
||
// 优先读取 extra_info.delivery;旧订单可能只在详情顶层保留配送字段,需回退以兼容历史数据。
|
||
$delivery = is_array($order['extra_info']['delivery'] ?? null)
|
||
? $order['extra_info']['delivery']
|
||
: [];
|
||
foreach (['delivery_type', 'delivery_id', 'delivery_name', 'delivery_code'] as $deliveryField) {
|
||
if (!empty($delivery[$deliveryField])) {
|
||
$order[$deliveryField] = $delivery[$deliveryField];
|
||
} else {
|
||
$order[$deliveryField] = $order[$deliveryField] ?? '';
|
||
}
|
||
}
|
||
// contact 已被 getDetail 提升到顶层并从 extra_info 移除,从 $order['contact'] 读取
|
||
$contact = $order['contact'] ?? [];
|
||
// user_address 已由 getDetail 自动格式化,无需重复处理
|
||
$order['user_phone'] = $contact['phone'] ?? ($contact['user_phone'] ?? '');
|
||
$order['real_name'] = $contact['name'] ?? ($contact['real_name'] ?? '');
|
||
|
||
if ($order['delivery_type'] != 'express' || !$order['delivery_id']) return app('json')->fail('快递单号不存在');
|
||
$express = $order['delivery_id'];
|
||
$cacheName = $uni . $express;
|
||
$orderInfo = [];
|
||
$info = [];
|
||
// 从 order_items(extra_info.items 快照)读取商品列表
|
||
foreach ($order['order_items'] ?? [] as $item) {
|
||
$info[] = [
|
||
'cart_num' => $item['cart_num'] ?? 1,
|
||
'truePrice' => $item['true_price'] ?? $item['price'] ?? '0.00',
|
||
'postage_price' => $item['postage_price'] ?? '0.00',
|
||
'productInfo' => [
|
||
'image' => $item['image'] ?? '',
|
||
'store_name' => $item['store_name'] ?? '',
|
||
'unit_name' => $item['unit_name'] ?? '',
|
||
'suk' => $item['sku'] ?? '',
|
||
],
|
||
];
|
||
}
|
||
$orderInfo['delivery_id'] = $express;
|
||
$orderInfo['delivery_name'] = $order['delivery_name'];
|
||
$orderInfo['delivery_code'] = $order['delivery_code'];
|
||
$orderInfo['delivery_type'] = $order['delivery_type'];
|
||
$orderInfo['user_address'] = $order['user_address'];
|
||
$orderInfo['user_mark'] = $order['mark'];
|
||
$orderInfo['cartInfo'] = $info;
|
||
$orderInfo['user_name'] = $order['real_name'];
|
||
$orderInfo['user_phone'] = $order['user_phone'];
|
||
$orderInfo['send_address'] = $order['user_address'];
|
||
$address = explode(' ', $order['user_address']);
|
||
$orderInfo['send_city'] = isset($address[0]) && in_array($address[0], ['北京', '上海', '天津', '重庆', '香港', '澳门', '台湾']) ? $address[0] : ($address[1] ?? '');
|
||
$orderInfo['delivery_type'] = $order['delivery_type'] ?? 1;
|
||
$orderInfo['user_city'] = $orderInfo['send_city'];
|
||
$orderInfo['user_mark'] = $order['mark'] ?? '';
|
||
return app('json')->success([
|
||
'order' => $orderInfo,
|
||
'express' => [
|
||
'result' => ['list' => $expressServices->query($cacheName, $orderInfo['delivery_id'], $orderInfo['delivery_code'], $order['user_phone'])
|
||
]
|
||
]
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 订单删除
|
||
* @return mixed
|
||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||
*/
|
||
public function del()
|
||
{
|
||
[$uni] = request()->postMore([
|
||
['uni', ''],
|
||
], true);
|
||
if (!$uni) return app('json')->fail('参数错误');
|
||
$orderCenterServices = app()->make(OrderCenterUserServices::class);
|
||
$res = $orderCenterServices->removeOrder($uni, (int)request()->uid());
|
||
if ($res) {
|
||
return app('json')->success('删除成功');
|
||
} else {
|
||
return app('json')->fail('删除失败');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 订单 再次下单
|
||
* @param StoreCartServices $services
|
||
* @return mixed
|
||
*/
|
||
public function again(StoreCartServices $services)
|
||
{
|
||
[$uni] = request()->postMore([
|
||
['uni', ''],
|
||
], true);
|
||
$orderCenterServices = app()->make(OrderCenterUserServices::class);
|
||
$cateId = $orderCenterServices->againOrder($services, $uni, (int)request()->uid());
|
||
return app('json')->success(['cateId' => implode(',', $cateId)]);
|
||
}
|
||
|
||
/**
|
||
* 订单商品信息(评价页商品展示,中台化)
|
||
* @return mixed
|
||
*/
|
||
public function product()
|
||
{
|
||
[$unique] = request()->postMore([['unique', '']], true);
|
||
$item = $this->findOrderItemBySkuUnique((string)$unique);
|
||
if (!$item) return app('json')->fail('商品不存在');
|
||
// 订单号由辅助方法附带返回(_order_no)
|
||
$cartProduct = [];
|
||
$cartProduct['cart_num'] = (int)($item['cart_num'] ?? 1);
|
||
$cartProduct['productInfo']['image'] = get_thumb_water($item['image'] ?? '');
|
||
$cartProduct['productInfo']['price'] = $item['price'] ?? 0;
|
||
$cartProduct['productInfo']['store_name'] = $item['store_name'] ?? '';
|
||
if (!empty($item['attr_info'])) {
|
||
$cartProduct['productInfo']['attrInfo']['product_id'] = $item['attr_info']['product_id'] ?? ($item['product_id'] ?? 0);
|
||
$cartProduct['productInfo']['attrInfo']['suk'] = $item['sku'] ?? ($item['attr_info']['suk'] ?? '');
|
||
$cartProduct['productInfo']['attrInfo']['price'] = $item['attr_info']['price'] ?? ($item['price'] ?? 0);
|
||
$cartProduct['productInfo']['attrInfo']['image'] = get_thumb_water($item['attr_info']['image'] ?? ($item['image'] ?? ''));
|
||
}
|
||
$cartProduct['product_id'] = $item['product_id'] ?? 0;
|
||
$cartProduct['combination_id'] = $item['combination_id'] ?? 0;
|
||
$cartProduct['seckill_id'] = $item['seckill_id'] ?? 0;
|
||
$cartProduct['bargain_id'] = $item['bargain_id'] ?? 0;
|
||
// 订单号从中台快照附带返回
|
||
$cartProduct['order_id'] = $item['_order_no'] ?? '';
|
||
return app('json')->success($cartProduct);
|
||
}
|
||
|
||
/**
|
||
* 判断用户是否可对订单执行用户端操作:下单人本人或送礼订单领取人
|
||
* @param array $order 订单数据(含 uid / gift_uid / extra_info)
|
||
* @param int $uid 当前用户 ID
|
||
* @return bool
|
||
*/
|
||
protected function canOperateOrder(array $order, int $uid): bool
|
||
{
|
||
if ((int)($order['uid'] ?? 0) === $uid) {
|
||
return true;
|
||
}
|
||
$giftUid = (int)($order['gift_uid'] ?? 0);
|
||
if ($giftUid === 0) {
|
||
$extra = $order['extra_info'] ?? [];
|
||
if (is_string($extra)) {
|
||
$extra = json_decode($extra, true) ?: [];
|
||
}
|
||
$giftUid = (int)($extra['gift']['gift_uid'] ?? ($extra['gift_uid'] ?? 0));
|
||
}
|
||
return $giftUid > 0 && $giftUid === $uid;
|
||
}
|
||
|
||
/**
|
||
* 按 SKU 唯一码从中台订单快照定位商品行
|
||
* 查询订单中台的 extra_info.items,匹配 attr_info.unique(与订单评价 unique 同源)
|
||
* @param string $unique SKU 唯一码
|
||
* @param int|null $uid 限定用户(评价场景必传防越权,下单人或送礼领取人),null 不限定
|
||
* @return array|null 商品行(附 _order_no/_center_id),未找到返回 null
|
||
*/
|
||
protected function findOrderItemBySkuUnique(string $unique, ?int $uid = null): ?array
|
||
{
|
||
if ($unique === '') {
|
||
return null;
|
||
}
|
||
// 不直接按 uid 过滤:送礼订单的领取人也需要能评价(领取人 uid 不等于订单 uid)
|
||
$where = [['extra_info', 'like', '%' . $unique . '%']];
|
||
$orders = app()->make(OrderCenterDao::class)->selectList($where, 'id, uid, gift_uid, order_no, extra_info', 0, 0);
|
||
foreach ($orders as $order) {
|
||
if ($uid !== null && !$this->canOperateOrder($order, $uid)) {
|
||
continue;
|
||
}
|
||
$extra = is_string($order['extra_info']) ? json_decode($order['extra_info'], true) : ($order['extra_info'] ?? []);
|
||
foreach ($extra['items'] ?? [] as $item) {
|
||
if (($item['attr_info']['unique'] ?? '') === $unique) {
|
||
$item['_order_no'] = (string)$order['order_no'];
|
||
$item['_center_id'] = (int)$order['id'];
|
||
return $item;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 按订单号+SKU unique 定位中台订单内的评价商品条目
|
||
* 评价必须落到用户指定的那笔订单:同商品重复购买时各订单 unique 相同,仅凭 unique 反查会命中旧订单
|
||
* @param string $orderNo 中台订单号
|
||
* @param string $unique SKU 唯一码
|
||
* @param int|null $uid 限定归属用户(下单人或送礼领取人)
|
||
* @return array|null
|
||
*/
|
||
protected function findOrderItemByOrderNo(string $orderNo, string $unique, ?int $uid = null): ?array
|
||
{
|
||
if ($orderNo === '' || $unique === '') {
|
||
return null;
|
||
}
|
||
$order = app()->make(OrderCenterDao::class)->get(['order_no' => $orderNo]);
|
||
if (!$order) {
|
||
return null;
|
||
}
|
||
$order = is_object($order) ? $order->toArray() : (array)$order;
|
||
if ($uid !== null && !$this->canOperateOrder($order, $uid)) {
|
||
return null;
|
||
}
|
||
$extra = is_string($order['extra_info'] ?? null) ? json_decode($order['extra_info'], true) : ($order['extra_info'] ?? []);
|
||
foreach ($extra['items'] ?? [] as $item) {
|
||
if (($item['attr_info']['unique'] ?? '') === $unique) {
|
||
$item['_order_no'] = (string)$order['order_no'];
|
||
$item['_center_id'] = (int)$order['id'];
|
||
return $item;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 订单评价(中台化)
|
||
* @param StoreProductReplyServices $replyServices
|
||
* @return \think\Response|void
|
||
*/
|
||
public function comment(StoreProductReplyServices $replyServices)
|
||
{
|
||
// ==================== 1. 获取请求参数 ====================
|
||
$group = request()->postMore([
|
||
['unique', ''],
|
||
['comment', ''],
|
||
['pics', ''],
|
||
['product_score', 5],
|
||
['service_score', 5]
|
||
]);
|
||
|
||
// ==================== 2. 基础参数验证 ====================
|
||
$unique = $group['unique'];
|
||
if (!$unique) {
|
||
return app('json')->fail('参数错误');
|
||
}
|
||
|
||
// ==================== 3. 获取用户信息 ====================
|
||
$uid = (int)request()->uid();
|
||
$user_info = request()->user();
|
||
// StoreProductReplyServices::comment() 要求数组(取 nickname/avatar)
|
||
if (is_object($user_info)) {
|
||
$user_info = $user_info->toArray();
|
||
}
|
||
|
||
// ==================== 4. 从中台订单快照定位商品(unique 为 SKU 唯一码,限定本人订单防越权) ====================
|
||
// 页面带订单号(uni)时按订单精确定位;同商品重复购买时 unique 跨订单重复,仅凭 unique 会命中旧订单
|
||
$uni = (string)request()->param('uni', '');
|
||
$item = $uni !== ''
|
||
? $this->findOrderItemByOrderNo($uni, (string)$unique, $uid)
|
||
: $this->findOrderItemBySkuUnique((string)$unique, $uid);
|
||
if (!$item) {
|
||
return app('json')->fail('商品不存在');
|
||
}
|
||
|
||
// ==================== 5. 获取订单信息 ====================
|
||
$orderInfo = app()->make(OrderCenterDao::class)->get($item['_center_id'] ?? 0);
|
||
if (!$orderInfo) {
|
||
return app('json')->fail('订单不存在');
|
||
}
|
||
$orderInfo = $orderInfo->toArray();
|
||
|
||
// ==================== 6. 权限验证 ====================
|
||
// 下单人本人或送礼订单领取人均可评价(领取人评价时 uid 为领取人)
|
||
if (!$this->canOperateOrder($orderInfo, $uid)) {
|
||
return app('json')->fail('无权评价该订单');
|
||
}
|
||
|
||
// ==================== 7. 调用评价服务(业务逻辑和事件触发都在Service中)===================
|
||
try {
|
||
$cartInfo = [
|
||
'oid' => (int)$orderInfo['id'],
|
||
'product_id' => (int)($item['product_id'] ?? 0),
|
||
'cart_info' => [
|
||
'product_id' => (int)($item['product_id'] ?? 0),
|
||
'productInfo' => [
|
||
'attrInfo' => [
|
||
'suk' => $item['sku'] ?? ($item['attr_info']['suk'] ?? ''),
|
||
],
|
||
],
|
||
],
|
||
];
|
||
$replyServices->comment($uid, $user_info, $cartInfo, $orderInfo, $group);
|
||
} catch (\Exception $e) {
|
||
return app('json')->fail($e->getMessage());
|
||
}
|
||
|
||
// ==================== 8. 检查是否有抽奖可跳转 ====================
|
||
// 通过事件机制,由抽奖插件决定是否跳转抽奖页。若插件未安装,事件无监听者,默认 false
|
||
$lotteryResult = new \stdClass();
|
||
$lotteryResult->toLottery = false;
|
||
event('OrderCommentLotteryCheck', [$uid, $orderInfo, $lotteryResult]);
|
||
return app('json')->success(['to_lottery' => $lotteryResult->toLottery]);
|
||
}
|
||
|
||
/**
|
||
* 代付订单详情
|
||
* @return mixed
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
*/
|
||
public function friendDetail()
|
||
{
|
||
[$orderId] = request()->getMore([
|
||
['order_id', '']
|
||
], true);
|
||
$orderId = (int) $orderId;
|
||
if ($orderId <= 0) {
|
||
return app('json')->fail('订单参数错误');
|
||
}
|
||
$orderCenterServices = app()->make(OrderCenterUserServices::class);
|
||
$info = $orderCenterServices->getFriendDetail($orderId, (int) request()->uid());
|
||
return app('json')->success(compact('info'));
|
||
}
|
||
|
||
/**
|
||
* 领取礼物
|
||
* @param $oid
|
||
* @return mixed
|
||
*/
|
||
public function receiveGift($oid)
|
||
{
|
||
[$gift_key, $shipping_type, $name, $phone, $address_id, $store_id] = request()->postMore([
|
||
['gift_key', ''],
|
||
['shipping_type', 1],
|
||
['name', ''],
|
||
['phone', ''],
|
||
['address_id', 0],
|
||
['store_id', 0],
|
||
], true);
|
||
if (!$oid) {
|
||
return app('json')->fail('缺少参数');
|
||
}
|
||
if ($shipping_type == 1 && $address_id == 0) {
|
||
return app('json')->fail('请选择收货地址');
|
||
}
|
||
$uid = (int)request()->uid();
|
||
$orderCenterServices = app()->make(OrderCenterUserServices::class);
|
||
$res = $orderCenterServices->receiveGift($uid, (int)$oid, $gift_key, (int)$shipping_type, $name, $phone, (int)$address_id, (int)$store_id);
|
||
if ($res) {
|
||
return app('json')->success('领取成功', ['status' => 1]);
|
||
} else {
|
||
return app('json')->success('该礼品已经被别人领取', ['status' => 0]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 礼品详情
|
||
* @param $oid
|
||
* @return mixed
|
||
*/
|
||
public function giftDetail($oid)
|
||
{
|
||
if (!$oid) {
|
||
return app('json')->fail('缺少参数');
|
||
}
|
||
$orderCenterServices = app()->make(OrderCenterUserServices::class);
|
||
// 路由参数为字符串,服务层签名要求 int
|
||
return app('json')->success($orderCenterServices->giftDetail((int)$oid));
|
||
}
|
||
|
||
/**
|
||
* 退款中间页订单商品列表(中台化)
|
||
* 从 eb_order_center.extra_info.items 读取商品快照
|
||
* @param $orderNo
|
||
* @return mixed
|
||
*/
|
||
public function refundCartInfo($orderNo)
|
||
{
|
||
[$cart_ids] = request()->postMore([
|
||
['cart_ids', []]
|
||
], true);
|
||
|
||
// 从 eb_order_center.extra_info.items 读取(统一订单)
|
||
try {
|
||
/** @var OrderCenterUserServices $centerServices */
|
||
$centerServices = app()->make(OrderCenterUserServices::class);
|
||
$order = $centerServices->getDetailByOrderNo($orderNo, false);
|
||
|
||
// 从 order_items 获取商品明细
|
||
$orderItems = $order['order_items'] ?? [];
|
||
|
||
// order_items 为空时,从 order_center 基本信息合成一个商品项(兜底)
|
||
if (empty($orderItems)) {
|
||
$orderItems = [[
|
||
'cart_id' => 1,
|
||
'product_id' => $order['extra_info']['product_id'] ?? 0,
|
||
'image' => $order['image'] ?? '',
|
||
'store_name' => $order['title'] ?? '商品',
|
||
'price' => $order['pay_price'] ?? '0.00',
|
||
'true_price' => $order['pay_price'] ?? '0.00',
|
||
'cart_num' => $order['total_num'] ?? 1,
|
||
'product_type' => $order['product_type'] ?? 'normal',
|
||
'attr_info' => [],
|
||
'sku' => '默认',
|
||
'postage_price' => $order['pay_postage'] ?? '0.00',
|
||
]];
|
||
}
|
||
|
||
// 格式化为前端 refundCartInfo 期望的格式(与 getRefundCartList 兼容)
|
||
$list = [];
|
||
foreach ($orderItems as $index => $item) {
|
||
$cartId = (string)($item['cart_id'] ?? ($item['id'] ?? ($index + 1)));
|
||
$productInfo = [
|
||
'image' => $item['image'] ?? '',
|
||
'store_name' => $item['store_name'] ?? '',
|
||
'attrInfo' => $item['attr_info'] ?? [],
|
||
'price' => $item['price'] ?? '0.00',
|
||
];
|
||
|
||
$cartInfo = [
|
||
'productInfo' => $productInfo,
|
||
'truePrice' => $item['true_price'] ?? $item['price'] ?? '0.00',
|
||
'postage_price' => $item['postage_price'] ?? '0.00',
|
||
'cart_num' => $item['cart_num'] ?? 1,
|
||
];
|
||
|
||
$list[] = [
|
||
// BIGINT cart_id 统一以字符串返回,避免前端 JSON number 精度丢失。
|
||
'id' => $cartId,
|
||
'cart_id' => $cartId,
|
||
'cart_num' => $item['cart_num'] ?? 1,
|
||
'truePrice' => $item['true_price'] ?? $item['price'] ?? '0.00',
|
||
'postage_price' => $item['postage_price'] ?? '0.00',
|
||
'productInfo' => $productInfo,
|
||
'product_id' => $item['product_id'] ?? 0,
|
||
'cart_info' => $cartInfo,
|
||
'surplus_num' => $item['cart_num'] ?? 1,
|
||
'sku' => $item['sku'] ?? '默认',
|
||
];
|
||
}
|
||
|
||
// 如果传了 cart_ids,应用数量覆盖
|
||
if ($cart_ids) {
|
||
$cartIdsMap = array_combine(array_column($cart_ids, 'cart_id'), $cart_ids);
|
||
foreach ($list as &$item) {
|
||
if (isset($cartIdsMap[$item['cart_id']]['cart_num'])) {
|
||
$item['cart_num'] = $cartIdsMap[$item['cart_id']]['cart_num'];
|
||
$item['cart_info']['cart_num'] = $item['cart_num'];
|
||
}
|
||
}
|
||
}
|
||
|
||
return app('json')->success($list);
|
||
} catch (\Throwable $e) {
|
||
Log::error('[refundCartInfo] 从中台读取退款商品列表失败: ' . $e->getMessage());
|
||
return app('json')->success([]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取退货商品列表
|
||
* @return mixed
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
*/
|
||
public function refundCartInfoList()
|
||
{
|
||
[$cart_ids, $id] = request()->postMore([
|
||
['cart_ids', []],
|
||
['id', 0],
|
||
], true);
|
||
if (!$id) {
|
||
return app('json')->fail('参数错误');
|
||
}
|
||
$orderCenterServices = app()->make(OrderCenterUserServices::class);
|
||
return app('json')->success($orderCenterServices->refundCartInfoList((array)$cart_ids, (int)$id));
|
||
}
|
||
|
||
/**
|
||
* 获取退款表单配置
|
||
* 根据产品类型返回不同的退款表单字段配置
|
||
* @param $orderNo
|
||
* @return mixed
|
||
*/
|
||
public function refundFormConfig($orderNo)
|
||
{
|
||
if (!$orderNo) {
|
||
return app('json')->fail('参数错误');
|
||
}
|
||
$uid = (int)request()->uid();
|
||
/** @var OrderCenterUserServices $centerServices */
|
||
$centerServices = app()->make(OrderCenterUserServices::class);
|
||
try {
|
||
$order = $centerServices->getDetailByOrderNo($orderNo, false);
|
||
} catch (\Throwable $e) {
|
||
return app('json')->fail($e->getMessage());
|
||
}
|
||
// 校验订单归属当前用户
|
||
if (!isset($order['uid']) || (int)$order['uid'] !== $uid) {
|
||
return app('json')->fail('订单不存在');
|
||
}
|
||
$productType = $order['product_type'] ?? 'normal';
|
||
$formConfig = $this->getRefundFormConfigByProductType($productType);
|
||
return app('json')->success($formConfig);
|
||
}
|
||
|
||
/**
|
||
* 用户申请退款
|
||
* 退款统一走售后流程,委托 AfterSaleServices::apply(type=refund_only 仅退款)
|
||
* 各产品类型 Handler 的 getRule 校验可用性,售后单写入 eb_after_sale
|
||
* 路由保留以兼容旧前端调用,前端新版直接走 /api/after_sale/apply
|
||
* @param $orderNo
|
||
* @return mixed
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
*/
|
||
public function applyRefund($orderNo)
|
||
{
|
||
$data = request()->postMore([
|
||
['text', ''],
|
||
['refund_reason_wap_img', ''],
|
||
['refund_reason_wap_explain', ''],
|
||
['refund_type', 1],
|
||
['refund_price', 0.00],
|
||
['cart_ids', []]
|
||
]);
|
||
if ($data['text'] === '') return app('json')->fail('参数错误');
|
||
|
||
// 从 order_center 中台表读取订单
|
||
$uid = (int)request()->uid();
|
||
/** @var OrderCenterUserServices $centerServices */
|
||
$centerServices = app()->make(OrderCenterUserServices::class);
|
||
try {
|
||
$order = $centerServices->getDetailByOrderNo($orderNo, false);
|
||
} catch (\Throwable $e) {
|
||
return app('json')->fail('订单不存在');
|
||
}
|
||
if ($uid != $order['uid']) {
|
||
return app('json')->fail('订单不存在');
|
||
}
|
||
|
||
// 旧参数 refund_reason_wap_img 为逗号分隔字符串,统一转为数组
|
||
$images = [];
|
||
if (!empty($data['refund_reason_wap_img'])) {
|
||
$images = array_values(array_filter(explode(',', $data['refund_reason_wap_img'])));
|
||
}
|
||
|
||
// 委托 AfterSaleServices 走售后流程(refund_only 仅退款)
|
||
// 售后规则由各产品类型 Handler 的 getRule 校验,中台状态联动由 AfterSaleServices 内部处理
|
||
try {
|
||
/** @var AfterSaleServices $afterSaleServices */
|
||
$afterSaleServices = app()->make(AfterSaleServices::class);
|
||
$afterSaleServices->apply([
|
||
'order_no' => $orderNo,
|
||
'uid' => $uid,
|
||
'type' => 'refund_only',
|
||
'refund_price' => (float)$data['refund_price'],
|
||
'reason' => $data['text'],
|
||
'evidence_images' => $images,
|
||
]);
|
||
return app('json')->success('申请成功');
|
||
} catch (\RuntimeException $e) {
|
||
return app('json')->fail($e->getMessage());
|
||
} catch (\Throwable $e) {
|
||
return app('json')->fail($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据产品类型获取退款表单配置
|
||
* @param string $productType
|
||
* @return array
|
||
*/
|
||
protected function getRefundFormConfigByProductType(string $productType): array
|
||
{
|
||
$baseFields = [
|
||
[
|
||
'type' => 'reason',
|
||
'label' => '退款原因',
|
||
'required' => true,
|
||
'options' => [],
|
||
'show' => true,
|
||
],
|
||
[
|
||
'type' => 'textarea',
|
||
'label' => '备注说明',
|
||
'placeholder' => '填写备注信息,100字以内',
|
||
'maxlength' => 100,
|
||
'required' => true,
|
||
'show' => true,
|
||
],
|
||
];
|
||
|
||
switch ($productType) {
|
||
case 'normal':
|
||
return [
|
||
'show_select_step' => true,
|
||
'fields' => array_merge([
|
||
[
|
||
'type' => 'refund_type',
|
||
'label' => '退款类型',
|
||
'required' => true,
|
||
'options' => ['仅退款', '退货并退款'],
|
||
'show' => true,
|
||
],
|
||
[
|
||
'type' => 'refund_num',
|
||
'label' => '退货件数',
|
||
'required' => true,
|
||
'show' => true,
|
||
],
|
||
], $baseFields, [
|
||
[
|
||
'type' => 'image',
|
||
'label' => '上传图片',
|
||
'max_count' => 3,
|
||
'show' => true,
|
||
],
|
||
]),
|
||
];
|
||
case 'virtual':
|
||
case 'cardkey':
|
||
case 'coupon':
|
||
return [
|
||
'show_select_step' => false,
|
||
'fields' => $baseFields,
|
||
];
|
||
default:
|
||
return [
|
||
'show_select_step' => true,
|
||
'fields' => $baseFields,
|
||
];
|
||
}
|
||
}
|
||
}
|