feat(送礼订单): 完善送礼/收礼订单全链路与详情性能

- 订单详情按查看者裁剪:购买者隐藏收货信息、物流轨迹及确认收货/申请退款/查看物流;领取人展示收货信息、物流轨迹与确认收货/查看物流
- 领取人订单列表纳入礼物订单并只读展示,统计计数含礼物、金额仅计本人支付
- 收礼人可确认收货;修复领取成功页订单号传递与领取页礼物详情查询
- 新增 eb_order_center.gift_uid 实体列与索引并同步安装 SQL
- 详情接口物流轨迹改为缓存读取 + 前端异步补齐,空轨迹短缓存 120 秒,修复送礼订单物流接口收货地址取值
This commit is contained in:
吴昊天 2026-10-21 16:22:36 +08:00
parent 897d4339d7
commit 78f56bb074
23 changed files with 906 additions and 184 deletions

View File

@ -130,7 +130,8 @@ class CenterOrderController
if ($ownedId === null) {
return app('json')->fail('订单不存在');
}
$result = $services->getDetailWithConfig($ownedId);
// 详情首屏不同步请求第三方物流:只读轨迹缓存,未命中由前端走 order/express 异步补齐
$result = $services->getDetailWithConfig($ownedId, false);
// 移动端订单详情不返回 timeline section,避免接口数据冗余
if (!empty($result['page_config']['sections'])) {
$result['page_config']['sections'] = array_values(
@ -139,6 +140,8 @@ class CenterOrderController
})
);
}
// 送礼订单按查看者裁剪:购买者隐藏收货信息/物流/相关按钮,领取人保留只读物流能力
$result = $services->applyGiftViewerPolicy($result, $uid);
return app('json')->success($result);
} catch (\Throwable $e) {
return app('json')->fail($e->getMessage());
@ -435,14 +438,20 @@ class CenterOrderController
$validCartInfo = $priceGroup['cartInfo'] ?? $validCartInfo;
// 组装与单品直购模式一致的 priceData,供 computed/create 读取
// 送礼订单:应付价 = 商品总价 + 礼品附加费(无运费)
$giftPrice = $priceGroup['giftPrice'] ?? 0;
$priceData = [
'totalPrice' => $priceGroup['totalPrice'] ?? 0,
'payPrice' => bcadd((string)($priceGroup['totalPrice'] ?? 0), (string)($priceGroup['storePostage'] ?? 0), 2),
'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' => $priceGroup['giftPrice'] ?? 0,
'giftPrice' => $giftPrice,
'levelPrice' => $priceGroup['levelPrice'] ?? 0,
'memberPrice' => $priceGroup['memberPrice'] ?? 0,
];
@ -569,10 +578,11 @@ class CenterOrderController
}
// 功能开关配置
// 送礼订单:收货信息由领取人填写,关闭配送/自提/上门入口
$features = [
'express_shipping' => $expressShipping, // 是否支持快递配送(所有商品均支持快递)
'store_pickup' => (bool)$storeSelfMention && $storePickupAll,// 是否支持到店自提(系统开启且所有商品均支持自提)
'door_service' => $doorServiceAll, // 是否支持上门服务(所有商品均支持上门)
'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),// 发票
@ -581,6 +591,8 @@ class CenterOrderController
'custom_form' => !empty($cartCustomForm), // 自定义表单(取主商品定义)
'params_list' => false, // 商品参数(下单页不展示)
'visitor_form' => !empty($cartVisitorFormConfig), // 游客信息表单(门票专属,取主商品定义)
'gift' => $isGift == 1, // 送礼订单标记
'gift_mark' => true, // 送礼留言输入
];
$deduction = $cartGroup['deduction'] ?? [];
@ -648,6 +660,8 @@ class CenterOrderController
$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('参数错误');
@ -726,7 +740,8 @@ class CenterOrderController
$uid,
$cartInfo,
$rebuildAddressInfo,
$shippingType
$shippingType,
$isGift
);
$cartInfo = $freshCartGroup['valid'] ?? [];
if (empty($cartInfo)) {
@ -735,16 +750,20 @@ class CenterOrderController
if (!empty($freshCartGroup['invalid'])) {
return app('json')->fail('商品库存或配送条件已变化,请返回确认页刷新');
}
// 商品信息可能已变化,必须以重建后的商品能力再次校验配送方式。
$deliveryContext = $this->validateNormalOrderDelivery(
$cartInfo,
$deliveryType,
$uid,
$addressId,
$storeId,
[],
false
);
// 送礼订单由领取人填收货信息,下单阶段不校验配送地址
$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(
@ -753,7 +772,7 @@ class CenterOrderController
$deliveryContext['address_info'],
$userInfo,
$shippingType,
0
$isGift
);
$cartGroup['cartInfo'] = $cartInfo;
$cartGroup['priceData'] = [
@ -872,7 +891,7 @@ class CenterOrderController
$computedServices = app()->make(OrderComputedServices::class);
$result = $computedServices->computedOrder(
$uid, $userInfo, $modifiedGroup, $addressId, '',
$useIntegral, 0, false, $shippingType, 0
$useIntegral, 0, false, $shippingType, $isGift
);
// 修正返回字段:total_price 用原始总价,coupon_price 用计算值
@ -959,6 +978,9 @@ class CenterOrderController
$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;
@ -1094,14 +1116,16 @@ class CenterOrderController
$cartIdsForRemove,
false,
[],
$formShippingType
$formShippingType,
$isGift
);
} else {
$freshCartGroup = $cartServices->rebuildNormalCheckoutCart(
$uid,
$cartInfo,
[],
$formShippingType
$formShippingType,
$isGift
);
}
$cartInfo = $freshCartGroup['valid'] ?? [];
@ -1124,18 +1148,24 @@ class CenterOrderController
$num = (int)($cartInfo[0]['cart_num'] ?? $num);
$productInfo = $cartInfo[0]['productInfo'] ?? $productInfo;
// 重建后再次校验商品当前配送能力,并用当前地址计算运费。
$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;
// 送礼订单由领取人填收货信息,下单阶段不校验配送地址
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);
@ -1145,7 +1175,7 @@ class CenterOrderController
$validatedAddressInfo,
$userInfo,
$formShippingType,
0
$isGift
);
$cartInfo = $priceGroup['cartInfo'] ?? $cartInfo;
$priceData = [
@ -1160,18 +1190,23 @@ class CenterOrderController
'memberPrice' => $priceGroup['memberPrice'] ?? 0,
];
} elseif ($productType === 'normal' && !$isActivityOrder) {
// 非普通重建路径(兼容旧活动/特殊缓存)仍必须校验配送信息。
$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;
// 非普通重建路径(兼容旧活动/特殊缓存)仍必须校验配送信息;送礼订单除外
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 记录)
@ -1337,7 +1372,7 @@ class CenterOrderController
$computedServices = app()->make(OrderComputedServices::class);
$computedResult = $computedServices->computedOrder(
$uid, $userInfo, $tmpCartGroup, $addressId, $payType,
$useIntegral, 0, false, $formShippingType, 0
$useIntegral, 0, false, $formShippingType, $isGift
);
$finalPayPrice = $computedResult['pay_price'] ?? 0;
$finalDeductionPrice = $computedResult['deduction_price'] ?? 0;
@ -1606,7 +1641,14 @@ class CenterOrderController
// 保存用户优惠券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('订单创建失败');
@ -2194,7 +2236,7 @@ class CenterOrderController
// 从中台表查询订单
/** @var OrderCenterUserServices $centerServices */
$centerServices = app()->make(OrderCenterUserServices::class);
$centerOrder = $centerServices->getDetailByOrderNo($uni);
$centerOrder = $centerServices->getDetailByOrderNo($uni, false);
if (!$centerOrder) {
CacheService::delete('PAY_LOCK_' . $uni);
return app('json')->fail('订单不存在');
@ -2356,7 +2398,7 @@ class CenterOrderController
case 'order':
/** @var OrderCenterUserServices $centerServices */
$centerServices = app()->make(OrderCenterUserServices::class);
$order = $centerServices->getDetailByOrderNo($orderNo);
$order = $centerServices->getDetailByOrderNo($orderNo, false);
if (!$order) {
return app('json')->fail('您支付的订单不存在');
}
@ -2372,7 +2414,12 @@ class CenterOrderController
$data['offline_postage'] = (int)sys_config('offline_postage', 0);
$data['invalid_time'] = $time;
$data['oid'] = $order['id'] ?? 0;
$data['is_gift'] = 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:
@ -2531,7 +2578,7 @@ class CenterOrderController
{
if (!strlen(trim($uni))) return app('json')->fail('参数错误');
$orderCenterServices = app()->make(OrderCenterUserServices::class);
$order = $orderCenterServices->getDetailByOrderNo($uni);
$order = $orderCenterServices->getDetailByOrderNo($uni, false);
if (!$order) return app('json')->fail('订单不存在');
// 订单详情格式化(OrderOutServices 已中台化,tidyOrder 基于中台数据)
$orderData = app()->make(OrderOutServices::class)->tidyOrder($order, true);
@ -2627,7 +2674,7 @@ class CenterOrderController
// 快照为空时回退到订单中台读取商品列表(兼容早期未填充快照的售后单)
if (empty($info) && !empty($afterSale['order_no'])) {
try {
$sourceOrder = $orderCenterServices->getDetailByOrderNo($afterSale['order_no']);
$sourceOrder = $orderCenterServices->getDetailByOrderNo($afterSale['order_no'], false);
foreach ($sourceOrder['order_items'] ?? [] as $item) {
$info[] = [
'cart_num' => $item['cart_num'] ?? 1,
@ -2664,7 +2711,8 @@ class CenterOrderController
if ($ownedId === null) {
return app('json')->fail('订单不存在');
}
$order = $orderCenterServices->getDetail($ownedId);
// 物流接口自身会查询轨迹,这里只取订单/配送信息,避免重复请求第三方
$order = $orderCenterServices->getDetail($ownedId, false);
} catch (\Throwable $e) {
return app('json')->fail('订单不存在');
}
@ -2978,7 +3026,7 @@ class CenterOrderController
}
$uid = (int)request()->uid();
$orderCenterServices = app()->make(OrderCenterUserServices::class);
$res = $orderCenterServices->receiveGift($uid, $oid, $gift_key, $shipping_type, $name, $phone, $address_id, $store_id);
$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 {
@ -2997,7 +3045,8 @@ class CenterOrderController
return app('json')->fail('缺少参数');
}
$orderCenterServices = app()->make(OrderCenterUserServices::class);
return app('json')->success($orderCenterServices->giftDetail($oid));
// 路由参数为字符串,服务层签名要求 int
return app('json')->success($orderCenterServices->giftDetail((int)$oid));
}
/**
@ -3016,7 +3065,7 @@ class CenterOrderController
try {
/** @var OrderCenterUserServices $centerServices */
$centerServices = app()->make(OrderCenterUserServices::class);
$order = $centerServices->getDetailByOrderNo($orderNo);
$order = $centerServices->getDetailByOrderNo($orderNo, false);
// 从 order_items 获取商品明细
$orderItems = $order['order_items'] ?? [];
@ -3124,7 +3173,7 @@ class CenterOrderController
/** @var OrderCenterUserServices $centerServices */
$centerServices = app()->make(OrderCenterUserServices::class);
try {
$order = $centerServices->getDetailByOrderNo($orderNo);
$order = $centerServices->getDetailByOrderNo($orderNo, false);
} catch (\Throwable $e) {
return app('json')->fail($e->getMessage());
}
@ -3165,7 +3214,7 @@ class CenterOrderController
/** @var OrderCenterUserServices $centerServices */
$centerServices = app()->make(OrderCenterUserServices::class);
try {
$order = $centerServices->getDetailByOrderNo($orderNo);
$order = $centerServices->getDetailByOrderNo($orderNo, false);
} catch (\Throwable $e) {
return app('json')->fail('订单不存在');
}

View File

@ -78,7 +78,7 @@ class StoreOrderInvoiceController
$uid = (int)$request->uid();
try {
// 订单中台统一详情(含状态、支付、配送、发票等信息)
$order = $services->getDetailByOrderNo($uni);
$order = $services->getDetailByOrderNo($uni, false);
} catch (\Throwable) {
return app('json')->fail('订单不存在');
}

View File

@ -81,6 +81,20 @@ class OrderCenterDao extends BaseDao
return $this->getModel()->where($where)->count();
}
/**
* 用户订单范围查询:本人下单的订单 + 本人领取的礼品订单
* 两个条件均为索引字段(idx_uid / idx_gift_uid),支持索引合并
* @param int $uid 用户ID
* @return mixed 返回查询对象
*/
public function userOrderQuery(int $uid)
{
return $this->getModel()->whereRaw(
'(uid = :owner_uid OR gift_uid = :gift_uid)',
['owner_uid' => $uid, 'gift_uid' => $uid]
);
}
/**
* 保存或更新记录
* 根据条件判断是新增还是更新,支持幂等性操作

View File

@ -15,6 +15,7 @@ use crmeb\traits\ModelTrait;
* 本模型保留常量别名以兼容已有代码,新代码请直接引用接口常量或从 Adapter 继承
*
* @method static $this where($field, $operator = null, $value = null)
* @property int $gift_uid 礼品领取人UID(0=未领取;领取后用于领取人订单列表/详情权限)
* @property int $pay_counted 购买次数已记账标记,与用户计数同事务提交
* @property int $take_rewards_given 收货奖励已记账标记,防止重复发奖
* @property array|string|null $refund_reward_accounted 退款奖励累计已处理目标,含已核对短缺

View File

@ -420,26 +420,28 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
/**
* 根据订单号获取订单详情
* @param string $orderNo 订单号
* @param bool $withRemoteLogistics 是否同步请求第三方物流轨迹
* @return array 返回订单详情数据
* @throws \RuntimeException 订单不存在时抛出异常
*/
public function getDetailByOrderNo(string $orderNo): array
public function getDetailByOrderNo(string $orderNo, bool $withRemoteLogistics = true): array
{
$detail = $this->dao->get(['order_no' => $orderNo], ['id']);
if (!$detail) {
throw new \RuntimeException('订单不存在');
}
return $this->getDetail((int)$detail['id']);
return $this->getDetail((int)$detail['id'], $withRemoteLogistics);
}
/**
* 获取订单详情
* 通过事件机制允许扩展模块注入详情数据
* @param int $id 订单中台记录ID
* @param bool $withRemoteLogistics 是否同步请求第三方物流轨迹(详情首屏可关闭,改由前端异步查询)
* @return array 返回订单详情数据
* @throws \RuntimeException 订单不存在时抛出异常
*/
public function getDetail(int $id): array
public function getDetail(int $id, bool $withRemoteLogistics = true): array
{
$detail = $this->dao->get(['id' => $id]);
if (!$detail) {
@ -584,7 +586,7 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
// 加载物流轨迹扩展数据(已发货订单调用物流查询驱动获取 tracks)
// 前端 LogisticsTimeline 组件据此渲染物流轨迹卡片,无需点"查看物流"单独查询
$this->loadLogisticsExtension($detail);
$this->loadLogisticsExtension($detail, $withRemoteLogistics);
// 加载订单完成态权益扩展数据(积分奖励/领券入口,系统内置能力,中台自行组装)
$this->loadOrderRewardExtension($detail);
@ -601,6 +603,24 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
return $detail;
}
/**
* 判定是否送礼订单
* extra_info.gift / gift_mark 为下单写入的送礼标记,gift_price 为兼容旧单兜底
* @param array $detail 订单详情(含 extra_info)
* @return int 1=送礼订单 0=普通订单
*/
protected function resolveIsGiftFlag(array $detail): int
{
$extra = $detail['extra_info'] ?? [];
if (!is_array($extra)) {
$extra = [];
}
if (!empty($extra['gift']) || isset($extra['gift_mark'])) {
return 1;
}
return (float)($detail['gift_price'] ?? 0) > 0 ? 1 : 0;
}
/**
* 组装价格明细数组
* 将金额实体列转为 {label, value, type} 数组格式
@ -659,6 +679,14 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
'type' => 'vip',
];
}
// 礼品附加费(送礼订单按件累计)
if (!empty($detail['gift_price']) && (float)$detail['gift_price'] > 0) {
$items[] = [
'label' => '礼品附加费',
'value' => '+' . number_format((float)$detail['gift_price'], 2, '.', ''),
'type' => 'gift',
];
}
// 价格明细扩展缝口:交易模式/产品类型插件经事件追加金额项(如服务费/上门费)
$pricingEvent = ['items' => &$items, 'detail' => $detail];
@ -775,6 +803,7 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
// 40. 配送/自提卡片(仅实物商品)
// 上门服务订单不渲染地址卡片,预约信息由 service_appointment section(118)展示
// 送礼订单的可见性由查看者身份决定(购买者隐藏、领取人展示),在 applyGiftViewerPolicy 中裁剪
if ($productType === 'normal' && !($detail['delivery']['is_door_service'] ?? false)) {
$deliveryComponent = $isPickup ? 'store_card' : 'address_card';
$deliveryProps = $isPickup
@ -1090,6 +1119,11 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
'detail_route' => $detail['detail_route'] ?? '',
'admin_detail_url'=> $detail['admin_detail_url'] ?? '',
'activity_detail_url' => $detail['activity_detail_url'] ?? '',
// 送礼订单:1=购买礼物 0=普通单;gift_uid 非 0 表示已被领取
'is_gift' => $this->resolveIsGiftFlag($detail),
'gift_uid' => (int)($detail['gift_uid'] ?? $detail['extra_info']['gift']['gift_uid'] ?? ($detail['extra_info']['gift_uid'] ?? 0)),
'gift_mark' => (string)($detail['extra_info']['gift_mark'] ?? ''),
'gift_price' => $detail['gift_price'] ?? '0.00',
];
// 时间线
@ -1357,12 +1391,13 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
* 由中台基础 section + 插件 extend_sections 自声明 section 组成
* - page_config.extensions:扩展数据(含 extra_info 深度定制数据)
* @param int $id 订单中台记录ID
* @param bool $withRemoteLogistics 是否同步请求第三方物流轨迹(首屏可关闭,由前端异步补齐)
* @return array 返回 {page_config: {sections: [...], extensions: {...}}}
*/
public function getDetailWithConfig(int $id): array
public function getDetailWithConfig(int $id, bool $withRemoteLogistics = true): array
{
// 1. 获取扁平详情数据(复用现有 getDetail 逻辑)
$flatDetail = $this->getDetail($id);
$flatDetail = $this->getDetail($id, $withRemoteLogistics);
// 2. 组装分组数据(内部使用:buildDefaultPageConfig 依赖全量分组生成 sections)
$data = $this->buildGroupedData($flatDetail);
@ -1396,16 +1431,17 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
/**
* 根据订单号获取带声明式渲染配置的订单详情
* @param string $orderNo 订单号
* @param bool $withRemoteLogistics 是否同步请求第三方物流轨迹
* @return array 返回 {page_config, data}
* @throws \RuntimeException 订单不存在时抛出异常
*/
public function getDetailWithConfigByOrderNo(string $orderNo): array
public function getDetailWithConfigByOrderNo(string $orderNo, bool $withRemoteLogistics = true): array
{
$detail = $this->dao->get(['order_no' => $orderNo], ['id']);
if (!$detail) {
throw new \RuntimeException('订单不存在');
}
return $this->getDetailWithConfig((int)$detail['id']);
return $this->getDetailWithConfig((int)$detail['id'], $withRemoteLogistics);
}
/**
@ -1647,16 +1683,17 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
/**
* 加载物流轨迹扩展数据
* 已发货且填了快递单号的订单,调用物流查询驱动获取 tracks,注入 extend.logistics
* 已发货且填了快递单号的订单,读取物流轨迹(默认允许同步查询第三方;详情首屏可只读缓存)并注入 extend.logistics
* 前端 LogisticsTimeline 组件据此渲染物流轨迹卡片,用户无需点"查看物流"单独查询
* 数据流:本方法写入 $detail['extend']['logistics']
* → buildGroupedData() 合并到 extensions['logistics']
* → page_config 中 component='timeline' + key='logistics' → 复合 key 'timeline:logistics'
* → 前端 LogisticsTimeline 渲染 delivery_name/delivery_id + OrderTimeline 遍历 tracks
* @param array $detail 订单详情数据(引用传递)
* @param bool $allowRemote 是否允许同步请求第三方物流接口;false 时仅读取已有缓存,避免详情首屏被慢请求阻塞
* @return void
*/
protected function loadLogisticsExtension(array &$detail): void
protected function loadLogisticsExtension(array &$detail, bool $allowRemote = true): void
{
// 仅处理有快递单号的订单(已发货且填写了物流单号)
$deliveryId = (string)($detail['delivery_id'] ?? '');
@ -1690,7 +1727,10 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
/** @var ExpressServices $expressServices */
$expressServices = $this->app->make(ExpressServices::class);
$rawTracks = $expressServices->query($cacheName, $deliveryId, $deliveryCode, $phone);
// 详情首屏只读缓存;轨迹按需由前端调用 order/express 异步补齐,避免第三方慢请求阻塞详情
$rawTracks = $allowRemote
? $expressServices->query($cacheName, $deliveryId, $deliveryCode, $phone)
: $expressServices->getCachedTracks($cacheName);
// 归一化轨迹字段为前端统一格式 {title, time, desc}
// 驱动返回格式差异:一号通 {time,status} / 阿里云 {time,context} / 其他兼容

View File

@ -25,15 +25,106 @@ use think\facade\Log;
class OrderCenterUserServices extends OrderCenterServices
{
/**
* 查询当前用户归属的订单中台记录 ID
* 查询当前用户可访问的订单中台记录 ID
*
* 可访问范围:
* 1. 订单归属人(uid 匹配);
* 2. 送礼订单的领取人(gift_uid 匹配)——领取礼物后需查看物流、核销与领取进度。
*
* @param string $orderNo 订单号
* @param int $uid 当前用户 ID
* @return int|null 不属于当前用户或订单不存在时返回 null
* @return int|null 无权访问或订单不存在时返回 null
*/
public function getOwnedOrderIdByNo(string $orderNo, int $uid): ?int
{
$id = $this->dao->value(['order_no' => $orderNo, 'uid' => $uid], 'id');
return $id ? (int)$id : null;
$order = $this->dao->get(['order_no' => $orderNo], ['id', 'uid', 'gift_uid', 'extra_info']);
if (!$order) {
return null;
}
$order = is_array($order) ? $order : $order->toArray();
if ((int)($order['uid'] ?? 0) === $uid) {
return (int)$order['id'];
}
// 送礼订单:领取人可查看该订单(优先实体列,历史数据回退 extra_info)
$extra = $this->parseExtraInfo($order['extra_info'] ?? null);
$giftUid = (int)($order['gift_uid'] ?? $extra['gift']['gift_uid'] ?? ($extra['gift_uid'] ?? 0));
return $giftUid > 0 && $giftUid === $uid ? (int)$order['id'] : null;
}
/**
* 送礼订单详情按查看者身份裁剪渲染配置
*
* - 购买者:隐藏收货信息、物流轨迹,以及确认收货/申请退款/查看物流按钮;
* - 领取人:只读视角,保留收货信息、物流轨迹与确认收货/查看物流按钮,
* 隐藏金额/支付/发票等下单人信息。
* 非送礼订单不做任何处理。
*
* @param array $result getDetailWithConfig 返回的 page_config 结构
* @param int $viewerUid 查看者用户 ID
* @return array 裁剪后的结构
*/
public function applyGiftViewerPolicy(array $result, int $viewerUid): array
{
$sections = $result['page_config']['sections'] ?? [];
if (empty($sections)) {
return $result;
}
// 从订单信息 section 读取送礼标识、下单人与领取人
$fields = [];
foreach ($sections as $section) {
if (($section['key'] ?? '') === 'order_info') {
$fields = $section['props']['fields'] ?? [];
break;
}
}
if ((int)($fields['is_gift'] ?? 0) !== 1) {
return $result;
}
$ownerUid = (int)($fields['uid'] ?? 0);
$giftUid = (int)($fields['gift_uid'] ?? 0);
$isReceiver = $ownerUid !== $viewerUid && $giftUid === $viewerUid;
if ($isReceiver) {
// 领取人:隐藏金额/支付/发票与下单人权益入口,保留收货信息、物流轨迹
$sections = array_values(array_filter(
$sections,
fn($section) => !in_array($section['key'] ?? '', ['pricing', 'payment', 'invoice', 'coupon_gift', 'points_reward'], true)
));
$result['page_config']['extensions']['viewer_role'] = 'gift_receiver';
$visibleActions = ['confirmReceive', 'viewLogistics'];
$hiddenActions = [];
} else {
// 购买者:不展示收货信息与物流轨迹,隐藏确认收货/申请退款/查看物流
$sections = array_values(array_filter(
$sections,
fn($section) => !in_array($section['key'] ?? '', ['delivery', 'logistics'], true)
));
$visibleActions = [];
$hiddenActions = ['confirmReceive', 'applyRefund', 'viewLogistics'];
}
// 按查看者身份过滤底部操作按钮
foreach ($sections as &$section) {
if (($section['key'] ?? '') !== 'buttons') {
continue;
}
$section['props']['buttons'] = array_values(array_filter(
$section['props']['buttons'] ?? [],
function ($button) use ($isReceiver, $visibleActions, $hiddenActions) {
$action = (string)($button['action'] ?? '');
return $isReceiver
? in_array($action, $visibleActions, true)
: !in_array($action, $hiddenActions, true);
}
));
$section['visible'] = !empty($section['props']['buttons']);
}
unset($section);
$result['page_config']['sections'] = $sections;
return $result;
}
/**
@ -48,7 +139,6 @@ class OrderCenterUserServices extends OrderCenterServices
public function getList(int $uid, array $where, int $page, int $limit): array
{
$searchWhere = [
['uid', '=', $uid],
['is_del', '=', 0],
];
// 订单大类筛选
@ -84,15 +174,18 @@ class OrderCenterUserServices extends OrderCenterServices
$searchWhere[] = ['take_time', '=', (int)$where['take_time']];
}
$list = $this->dao->selectList(
$searchWhere,
'order_no, source_type, source_id, order_category, product_type, activity_type, status_code,
// 列表范围:本人下单 + 本人领取的礼品订单(userOrderQuery 内按 uid/gift_uid 索引查询)
$list = $this->dao->userOrderQuery($uid)
->where($searchWhere)
->field('uid, gift_uid, order_no, source_type, source_id, order_category, product_type, activity_type, status_code,
total_price, pay_price, total_num, title, image,
subtitle, pay_type, shipping_type, extra_info, detail_route,
add_time, pay_time, delivery_time, take_time',
$page, $limit, 'id DESC'
);
$total = $this->dao->count($searchWhere);
add_time, pay_time, delivery_time, take_time, gift_price')
->page($page, $limit)
->order('id DESC')
->select()
->toArray();
$total = (int)$this->dao->userOrderQuery($uid)->where($searchWhere)->count();
// 批量加载扩展表子状态,供列表页按钮和扩展文案精确判断
$sourceStatusMap = $this->loadSourceStatusesForList($list);
@ -114,6 +207,18 @@ class OrderCenterUserServices extends OrderCenterServices
// type_label: 订单类型短标签(秒杀/积分/充值/网盘/卡密等),用于列表项角标展示
// 优先级:活动类型 > 订单大类 > 产品类型,各维度均通过注册中心/事件动态获取,新增类型无需改前端
$item['type_label'] = $this->resolveTypeLabel($item);
// 送礼订单标识与礼品附加费,供列表角标与金额展示
$itemExtra = $item['extra_info'] ?? [];
$item['is_gift'] = (!empty($itemExtra['gift']) || isset($itemExtra['gift_mark'])) ? 1 : 0;
$item['gift_price'] = $item['gift_price'] ?? '0.00';
// 礼物领取人视角(非下单人):只读列表,仅保留查看物流,避免出现退款/取消/删除等下单人操作
$item['is_gift_receiver'] = ((int)($item['uid'] ?? 0) !== $uid && (int)($item['gift_uid'] ?? 0) === $uid) ? 1 : 0;
if ($item['is_gift_receiver']) {
$item['order_buttons'] = array_values(array_filter(
$item['order_buttons'] ?? [],
fn($button) => ($button['action'] ?? '') === 'viewLogistics'
));
}
}
// 为 viewAfterSale 按钮批量注入 after_sale_no(与详情接口 getOrderButtonsFromHandler 同口径)
@ -319,27 +424,27 @@ class OrderCenterUserServices extends OrderCenterServices
*/
public function getUserStatistics(int $uid): array
{
// 缓存 key 带结构版本 v3 + 失效版本号:订单状态流转经 syncStatus/updateRecord 递增
// order_center:stat:ver 强制换 key 刷新(结构变更升级 v3),旧 key 靠自身 TTL 自然过期
// 缓存 key 带结构版本 v4 + 失效版本号:订单状态流转经 syncStatus/updateRecord 递增
// order_center:stat:ver 强制换 key 刷新(结构变更升级 v4:统计范围含领取的礼品订单),旧 key 靠自身 TTL 自然过期
$ver = (int) app()->cache->get('order_center:stat:ver');
$cacheKey = 'order_center:user_stat:v3:' . $uid . ':' . $ver;
$cacheKey = 'order_center:user_stat:v4:' . $uid . ':' . $ver;
return app()->cache->remember($cacheKey, function () use ($uid) {
$baseWhere = [
['uid', '=', $uid],
['is_del', '=', 0],
];
// 统计范围:本人下单 + 本人领取的礼品订单(与订单列表口径一致)
// 金额只累计本人实际支付金额,收到的礼物不计入个人消费
$baseQuery = function () use ($uid) {
return $this->dao->userOrderQuery($uid)->where('is_del', 0);
};
$amountField = 'SUM(CASE WHEN uid = ' . $uid . ' THEN pay_price ELSE 0 END) as amount';
$statusStats = $this->dao->getDb()
->where($baseWhere)
->field('status_code, COUNT(*) as count, SUM(pay_price) as amount')
$statusStats = $baseQuery()
->field('status_code, COUNT(*) as count, ' . $amountField)
->group('status_code')
->select()
->toArray();
$all = $this->dao->getDb()
->where($baseWhere)
->field('COUNT(*) as count, SUM(pay_price) as amount')
$all = $baseQuery()
->field('COUNT(*) as count, ' . $amountField)
->find();
$statusCounts = [];
@ -353,10 +458,9 @@ class OrderCenterUserServices extends OrderCenterServices
// STATUS_PROCESSING 按产品类型拆分:非实物业态=待使用(虚拟/卡密/酒店/门票等)
// 实物(normal)进行中再按发货状态细分为待发货/待收货,与订单列表 tab 及角标口径一致
$processingByType = $this->dao->getDb()
->where($baseWhere)
$processingByType = $baseQuery()
->where('status_code', OrderCenter::STATUS_PROCESSING)
->field('product_type, COUNT(*) as count, SUM(pay_price) as amount')
->field('product_type, COUNT(*) as count, ' . $amountField)
->group('product_type')
->select()
->toArray();
@ -378,20 +482,18 @@ class OrderCenterUserServices extends OrderCenterServices
}
// 实物进行中按发货状态拆分:待发货=未发货(delivery_time=0),待收货=已发货未收货
$pendingShip = $this->dao->getDb()
->where($baseWhere)
$pendingShip = $baseQuery()
->where('status_code', OrderCenter::STATUS_PROCESSING)
->where('product_type', 'normal')
->where('delivery_time', 0)
->field('COUNT(*) as count, SUM(pay_price) as amount')
->field('COUNT(*) as count, ' . $amountField)
->find();
$pendingReceipt = $this->dao->getDb()
->where($baseWhere)
$pendingReceipt = $baseQuery()
->where('status_code', OrderCenter::STATUS_PROCESSING)
->where('product_type', 'normal')
->where('delivery_time', '<>', 0)
->where('take_time', 0)
->field('COUNT(*) as count, SUM(pay_price) as amount')
->field('COUNT(*) as count, ' . $amountField)
->find();
$grouped = [
@ -540,7 +642,8 @@ class OrderCenterUserServices extends OrderCenterServices
*/
public function getFriendDetail(int $orderId, int $uid)
{
$detail = $this->getDetail($orderId);
// 代付详情不需要物流轨迹,跳过第三方物流查询
$detail = $this->getDetail($orderId, false);
if (!$detail) {
throw new ApiException('订单不存在');
}
@ -586,7 +689,8 @@ class OrderCenterUserServices extends OrderCenterServices
*/
public function refundCartInfoList(array $cartIds = [], int $id = 0)
{
$detail = $this->getDetail($id);
// 退款商品列表不需要物流轨迹,跳过第三方物流查询
$detail = $this->getDetail($id, false);
if (!$detail) {
throw new ApiException('订单不存在');
}
@ -676,7 +780,7 @@ class OrderCenterUserServices extends OrderCenterServices
throw new ApiException('参数错误');
}
$detail = $this->getDetailByOrderNo($uni);
$detail = $this->getDetailByOrderNo($uni, false);
if (!$detail || (int)($detail['uid'] ?? 0) !== $uid) {
throw new ApiException('订单不存在');
}
@ -798,7 +902,8 @@ class OrderCenterUserServices extends OrderCenterServices
*/
public function giftDetail(int $oid)
{
$detail = $this->getDetail($oid);
// 礼品详情不需要物流轨迹,跳过第三方物流查询
$detail = $this->getDetail($oid, false);
if (!$detail) {
throw new ApiException('订单不存在');
}
@ -807,8 +912,44 @@ class OrderCenterUserServices extends OrderCenterServices
$userServices = app()->make(UserServices::class);
$userInfo = $userServices->get($detail['uid']);
// 商品快照:中台 order_items 为扁平结构,旧版页面(orderGoods 组件)按 productInfo 嵌套结构渲染,
// 这里在保留扁平字段的同时补齐 productInfo 等旧版字段,供礼物领取页等旧页面直接复用
$orderItems = is_array($detail['order_items'] ?? null) ? $detail['order_items'] : [];
$cartInfo = [];
foreach ($orderItems as $item) {
if (!is_array($item)) {
continue;
}
// 已是旧版结构时直接透传
if (!empty($item['productInfo']) && is_array($item['productInfo'])) {
$cartInfo[] = $item;
continue;
}
$attr = is_array($item['attr_info'] ?? null) ? $item['attr_info'] : [];
$cartInfo[] = array_merge($item, [
'unique' => (string)($item['unique'] ?? ($attr['unique'] ?? '')),
'is_valid' => $item['is_valid'] ?? true,
'is_reply' => (int)($item['is_reply'] ?? 0),
'refund_num' => (int)($item['refund_num'] ?? 0),
'productInfo' => [
'product_id' => (int)($item['product_id'] ?? ($attr['product_id'] ?? 0)),
'store_name' => (string)($item['store_name'] ?? ''),
'image' => (string)($item['image'] ?? ($attr['image'] ?? '')),
'price' => (string)($item['price'] ?? ($attr['price'] ?? '0.00')),
'ot_price' => (string)($attr['ot_price'] ?? ($item['ot_price'] ?? '0.00')),
'store_mention' => (int)($item['store_mention'] ?? 1),
'virtual_type' => (int)($item['virtual_type'] ?? 0),
'time_id' => (int)($item['time_id'] ?? 0),
'attrInfo' => $attr ? array_merge($attr, [
'image' => (string)($attr['image'] ?? ($item['image'] ?? '')),
'price' => (string)($attr['price'] ?? ($item['price'] ?? '0.00')),
'suk' => (string)($attr['suk'] ?? ($item['sku'] ?? '')),
]) : null,
],
]);
}
// 从 extra_info 提取物流方式信息
$cartInfo = $detail['order_items'] ?? [];
$arr = [];
foreach ($cartInfo as $cartInfoItem) {
$arr[] = $cartInfoItem['productInfo']['logistics'] ?? '';
@ -840,7 +981,8 @@ class OrderCenterUserServices extends OrderCenterServices
'pay_price' => $detail['pay_price'] ?? '0.00',
'gift_key' => $giftKey,
'gift_mark' => $extraInfo['gift_mark'] ?? '',
'gift_uid' => $extraInfo['gift_uid'] ?? 0,
'gift_uid' => (int)($detail['gift_uid'] ?? $extraInfo['gift']['gift_uid'] ?? ($extraInfo['gift_uid'] ?? 0)),
'is_gift' => (!empty($extraInfo['gift']) || isset($extraInfo['gift_mark'])) ? 1 : 0,
'refund_status' => $detail['refund_status'] ?? 0,
'type' => $type,
'store_self_mention' => (int)sys_config('store_self_mention') ?? 0,
@ -908,9 +1050,12 @@ class OrderCenterUserServices extends OrderCenterServices
}
// 更新 extra_info 中的礼物和联系人信息
// gift_time 供订单详情「礼品已领」进度节点展示;本人重复领取时保留首次领取时间
$giftTime = (int)($extraInfo['gift']['gift_time'] ?? 0) ?: time();
$extraInfo['gift'] = [
'gift_uid' => $uid,
'gift_key' => $giftKey,
'gift_uid' => $uid,
'gift_key' => $giftKey,
'gift_time' => $giftTime,
];
// 联系人信息
$extraInfo['contact'] = [
@ -927,8 +1072,12 @@ class OrderCenterUserServices extends OrderCenterServices
'shipping_type' => $shippingType,
'store_id' => $storeId,
'verify_code' => $verifyCode,
// 领取人写入实体列,供用户订单列表按 gift_uid 索引查询
'gift_uid' => $uid,
'extra_info' => $this->encodeExtraInfo($extraInfo),
]);
// 领取后礼物订单进入领取人订单列表,清除统计缓存
$this->clearStatCache();
return true;
}

View File

@ -83,13 +83,24 @@ class OrderTakeServices extends BaseService
*/
public function takeOrder(string $uni, int $uid)
{
$order = $this->dao->getOne(['order_no' => $uni, 'uid' => $uid]);
$order = $this->dao->getOne(['order_no' => $uni]);
if (!$order) {
throw new ApiException('订单不存在');
}
if (is_object($order)) {
$order = $order->toArray();
}
// 订单归属人可确认收货;送礼订单的领取人也可确认收货
if ((int)($order['uid'] ?? 0) !== $uid) {
$extra = $order['extra_info'] ?? [];
if (is_string($extra)) {
$extra = json_decode($extra, true) ?: [];
}
$giftUid = (int)($order['gift_uid'] ?? $extra['gift']['gift_uid'] ?? ($extra['gift_uid'] ?? 0));
if ($giftUid !== $uid) {
throw new ApiException('订单不存在');
}
}
// 售后拦截:存在处理中售后单不可确认收货(售后引擎统一判断)
$orderIsRefund = app()->make(AfterSaleServices::class)->hasActiveByOrderId((int)$order['id']);
if($orderIsRefund){

View File

@ -195,6 +195,48 @@ class ExpressServices extends BaseService
return $this->dao->getExpressList($where, 'id,name,code,partner_id,partner_key,net,account,key,net_name', 0, 0);
}
/**
* 解析当前启用的物流查询驱动
* @return array 驱动配置;未启用或所属插件停用时返回空数组
*/
protected function resolveEnabledQueryDriver(): array
{
/** @var LogisticsDriverServices $logisticsDriverServices */
$logisticsDriverServices = app()->make(LogisticsDriverServices::class);
$enabledDriver = $logisticsDriverServices->getEnabledDriver() ?? [];
$driverKey = (string)($enabledDriver['driver_key'] ?? '');
$ownerPluginId = (string)($enabledDriver['owner_plugin_id'] ?? '');
// 插件或查询驱动关闭时,即使轨迹缓存仍存在也不返回旧数据。
if ($driverKey === '' || $ownerPluginId === '' || !ModuleStateChecker::isDriverActive($ownerPluginId)) {
return [];
}
if (!in_array($driverKey, ['aliyun_express', 'yihaotong'], true)) {
throw new \RuntimeException('物流查询驱动不支持');
}
return $enabledDriver;
}
/**
* 仅读取物流轨迹缓存,不请求第三方服务
* 用于订单详情首屏:避免第三方物流接口的慢请求阻塞详情返回
* @param string $cacheName 订单号 + 快递单号
* @return array 已缓存的轨迹数据,未命中返回空数组
*/
public function getCachedTracks(string $cacheName): array
{
try {
$enabledDriver = $this->resolveEnabledQueryDriver();
$driverKey = (string)($enabledDriver['driver_key'] ?? '');
if ($driverKey === '') {
return [];
}
$data = CacheService::get('logistics:' . $driverKey . ':' . $cacheName, null);
return is_array($data) ? $data : [];
} catch (\Throwable $e) {
return [];
}
}
/**
* 物流公司查询
* @param string $cacheName
@ -209,23 +251,21 @@ class ExpressServices extends BaseService
$com = (string)($com ?? '');
$phone = (string)($phone ?? '');
/** @var LogisticsDriverServices $logisticsDriverServices */
$logisticsDriverServices = app()->make(LogisticsDriverServices::class);
$enabledDriver = $logisticsDriverServices->getEnabledDriver() ?? [];
$enabledDriver = $this->resolveEnabledQueryDriver();
$driverKey = (string)($enabledDriver['driver_key'] ?? '');
$ownerPluginId = (string)($enabledDriver['owner_plugin_id'] ?? '');
// 插件或查询驱动关闭时,即使轨迹缓存仍存在也不返回旧数据。
if ($driverKey === '' || $ownerPluginId === '' || !ModuleStateChecker::isDriverActive($ownerPluginId)) {
if ($driverKey === '') {
return [];
}
if (!in_array($driverKey, ['aliyun_express', 'yihaotong'], true)) {
throw new \RuntimeException('物流查询驱动不支持');
}
// 按查询驱动隔离轨迹缓存,避免切换后复用其他服务商的数据。
$driverCacheName = 'logistics:' . $driverKey . ':' . $cacheName;
$resultData = CacheService::get($driverCacheName, null);
if (!is_array($resultData) || empty($resultData)) {
// 第三方无轨迹/请求失败时短缓存,避免详情页与物流页反复触发慢请求
$emptyCacheName = $driverCacheName . ':empty:' . md5($phone . '|' . $com);
if ((int)CacheService::get($emptyCacheName, 0) === 1) {
return [];
}
$data = [];
$cacheTime = 0;
switch ($driverKey) {
@ -265,10 +305,12 @@ class ExpressServices extends BaseService
default:
throw new \RuntimeException('物流查询驱动不支持');
}
// 空轨迹通常表示手机号/快递编码尚未满足验单条件,不缓存失败结果,
// 以便后续携带完整参数的查询可以重新请求第三方服务。
// 空轨迹通常表示手机号/快递编码尚未满足验单条件:按查询参数短缓存 120 秒,
// 既避免详情页/物流页反复触发第三方慢请求,又允许后续携带更完整参数时重试。
if ($data !== []) {
CacheService::set($driverCacheName, $data, $cacheTime);
} else {
CacheService::set($emptyCacheName, 1, 120);
}
return $data;
}

View File

@ -27,12 +27,15 @@ class OrderController
try {
$ownedId = $service->getOwnedOrderIdByNo($orderNo, $uid);
if ($ownedId === null) return app('json')->fail('订单不存在');
$result = $service->getDetailWithConfig($ownedId);
// 详情首屏不同步请求第三方物流:只读轨迹缓存,未命中由前端走 order/express 异步补齐
$result = $service->getDetailWithConfig($ownedId, false);
if (!empty($result['page_config']['sections'])) {
$result['page_config']['sections'] = array_values(array_filter(
$result['page_config']['sections'], fn ($s) => ($s['key'] ?? '') !== 'timeline'
));
}
// 送礼订单按查看者裁剪(与中台详情接口同口径)
$result = $service->applyGiftViewerPolicy($result, $uid);
return app('json')->success($result);
} catch (\Throwable $e) {
return app('json')->fail($e->getMessage());

View File

@ -359,6 +359,114 @@ class NormalOrderAdapter extends BaseOrderTypeAdapter
return $this->extStatusMap[$statusCode]['user_buttons'] ?? [];
}
public function getOrderButtons(array $detail): array
{
$buttons = parent::getOrderButtons($detail);
// 送礼订单:已支付且未领取时,提供「送给好友」(与申请退款同在底部按钮组)
if ($this->isGiftOrder($detail) && $this->isGiftShareable($detail)) {
array_unshift($buttons, [
'text' => '送给好友',
'action' => 'shareGift',
'primary' => true,
'params' => ['order_no' => (string)($detail['order_no'] ?? '')],
]);
}
return $buttons;
}
/**
* 是否送礼订单
*/
protected function isGiftOrder(array $detail): bool
{
$extra = $detail['extra_info'] ?? [];
if (!is_array($extra)) {
$extra = [];
}
return !empty($extra['gift']) || isset($extra['gift_mark']) || (float)($detail['gift_price'] ?? 0) > 0;
}
/**
* 送礼订单是否可分享(已支付且尚未被领取)
*/
protected function isGiftShareable(array $detail): bool
{
if ((int)($detail['pay_status'] ?? 0) !== 1) {
return false;
}
$extra = $detail['extra_info'] ?? [];
if (!is_array($extra)) {
$extra = [];
}
// 优先读取实体列 gift_uid(领取索引),历史数据回退 extra_info
$giftUid = (int)($detail['gift_uid'] ?? $extra['gift']['gift_uid'] ?? ($extra['gift_uid'] ?? 0));
return $giftUid === 0;
}
/**
* 送礼订单生命周期进度条
*
* 节点固定为:提交订单 → 支付成功 → 礼品待领 → 礼品已领 → 订单完成。
* 领取状态由实体列 gift_uid 判定(非 0 表示已被领取;历史数据回退 extra_info.gift.gift_uid),
* 领取时间取 extra_info.gift.gift_time(历史订单无该字段时留空)。
*
* @param array $detail 订单详情(含 status_code、pay_status、extra_info、add_time、pay_time、take_time 等)
* @return array 返回 {current, status, nodes}
*/
protected function buildGiftProgress(array $detail): array
{
$statusCode = (string)($detail['status_code'] ?? '');
$isCancel = $statusCode === self::STATUS_CANCELED;
$isAfterSale = $statusCode === self::STATUS_AFTER_SALE;
$isRefunded = $statusCode === self::STATUS_REFUNDED;
$isCompleted = $statusCode === self::STATUS_COMPLETED;
$extra = is_array($detail['extra_info'] ?? null) ? $detail['extra_info'] : [];
$gift = is_array($extra['gift'] ?? null) ? $extra['gift'] : [];
// 优先读取实体列 gift_uid(领取索引),历史数据回退 extra_info
$giftUid = (int)($detail['gift_uid'] ?? $gift['gift_uid'] ?? ($extra['gift_uid'] ?? 0));
$giftTime = (int)($gift['gift_time'] ?? 0);
$addTime = (int)($detail['add_time'] ?? 0);
$payTime = (int)($detail['pay_time'] ?? 0);
$takeTime = (int)($detail['take_time'] ?? 0);
// 已领取:礼品待领/礼品已领两个节点同时完成
$isClaimed = $giftUid > 0;
$nodes = [
['title' => '提交订单', 'key' => 'submit', 'time' => $addTime ? date('Y-m-d H:i:s', $addTime) : '', 'completed' => $addTime > 0],
['title' => '支付成功', 'key' => 'pay', 'time' => $payTime ? date('Y-m-d H:i:s', $payTime) : '', 'completed' => $payTime > 0 || (int)($detail['pay_status'] ?? 0) === 1],
['title' => '礼品待领', 'key' => 'gift_pending', 'time' => $isClaimed && $giftTime ? date('Y-m-d H:i:s', $giftTime) : '', 'completed' => $isClaimed],
['title' => '礼品已领', 'key' => 'gift_received', 'time' => $isClaimed && $giftTime ? date('Y-m-d H:i:s', $giftTime) : '', 'completed' => $isClaimed],
['title' => '订单完成', 'key' => 'complete', 'time' => $isCompleted && $takeTime ? date('Y-m-d H:i:s', $takeTime) : '', 'completed' => $isCompleted],
];
// 当前激活节点:取消停在提交订单、售后/退款中断在支付成功、完成后停在订单完成
if ($isCancel) {
$current = 0;
} elseif ($isRefunded || $isAfterSale) {
$current = 1;
} elseif ($isCompleted) {
$current = 4;
} elseif ($isClaimed) {
$current = 3; // 已领取,等待订单完成(收货/自动收货)
} elseif ($payTime > 0) {
$current = 2; // 已支付,等待好友领取
} else {
$current = 0; // 刚提交,未支付
}
foreach ($nodes as $i => &$node) {
$node['active'] = ($i === $current);
}
unset($node);
return [
'current' => $current,
'status' => $statusCode,
'nodes' => $nodes,
];
}
/**
* PROCESSING 状态动态按钮
*/
@ -413,6 +521,11 @@ class NormalOrderAdapter extends BaseOrderTypeAdapter
$isRefunded = $statusCode === self::STATUS_REFUNDED;
$isCompleted = $statusCode === self::STATUS_COMPLETED;
// 送礼订单:提交订单 → 支付成功 → 礼品待领 → 礼品已领 → 订单完成
if ($this->isGiftOrder($detail)) {
return $this->buildGiftProgress($detail);
}
// 判断是否为到店自提(shipping_type=2),默认走快递配送流程
$isPickup = ($detail['shipping_type'] ?? 0) == 2;

View File

@ -52,15 +52,20 @@ class OrderCenterDetailExtendListener implements ListenerInterface
$detail['contact'] = $extraInfo['contact'];
unset($extraInfo['contact']);
// 将 contact.address 对象格式化为字符串,赋值给 user_address
if (isset($detail['contact']['address']) && is_array($detail['contact']['address'])) {
$addr = $detail['contact']['address'];
$detail['user_address'] = implode('', array_filter([
$addr['province'] ?? '',
$addr['city'] ?? '',
$addr['district'] ?? '',
$addr['detail'] ?? '',
]));
// 将 contact.address 统一转为字符串赋值给 user_address
// 送礼订单领取时写入的是字符串地址,普通订单是省市区对象,两种都要兼容
if (isset($detail['contact']['address'])) {
if (is_array($detail['contact']['address'])) {
$addr = $detail['contact']['address'];
$detail['user_address'] = implode('', array_filter([
$addr['province'] ?? '',
$addr['city'] ?? '',
$addr['district'] ?? '',
$addr['detail'] ?? '',
]));
} else {
$detail['user_address'] = (string)$detail['contact']['address'];
}
}
}

View File

@ -770,6 +770,7 @@ CREATE TABLE IF NOT EXISTS `eb_order_center` (
`member_discount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '等级会员优惠金额',
`vip_discount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '付费会员优惠金额',
`gift_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '礼品费用',
`gift_uid` int NOT NULL DEFAULT '0' COMMENT '礼品领取人UID(0=未领取)',
`total_num` int NOT NULL DEFAULT '0' COMMENT '商品数量总数',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '订单标题摘要',
`image` varchar(500) NOT NULL DEFAULT '' COMMENT '缩略图',
@ -792,6 +793,7 @@ CREATE TABLE IF NOT EXISTS `eb_order_center` (
UNIQUE KEY `uk_order_no` (`order_no`),
UNIQUE KEY `uk_source_order` (`source_type`,`source_id`),
KEY `idx_uid` (`uid`),
KEY `idx_gift_uid` (`gift_uid`),
KEY `idx_order_category` (`order_category`),
KEY `idx_source_type` (`source_type`),
KEY `idx_status_code` (`status_code`),

View File

@ -76,6 +76,22 @@ export default {
},
});
},
/**
* 送给好友(送礼订单分享礼物领取链接)
* 详情页引擎通过 context.onShareGift 打开送礼弹窗;无弹窗上下文的页面跳详情页操作
*/
shareGift(order, params, context) {
if (context && typeof context.onShareGift === 'function') {
context.onShareGift(params || {});
return;
}
const orderNo = params.order_no || order.order_no || '';
if (!orderNo) {
uni.showToast({ title: i18n.t(`订单信息异常`), icon: 'none' });
return;
}
uni.navigateTo({ url: routes.orderDetail + '?order_no=' + orderNo });
},
/** 申请退款(统一走售后流程,售后页按 available_types 渲染仅退款等类型) */
applyRefund(order, params) {
const orderNo = params.order_no || order.order_no || '';

View File

@ -14,6 +14,11 @@
<view>{{ $t(`可用积分`) }}</view>
<view class="money">{{ usableIntegral }}</view>
</view>
<!-- 礼品附加费(送礼订单) -->
<view class="item acea-row row-between-wrapper" v-if="parseFloat(giftPrice) > 0">
<view>{{ $t(`礼品附加费`) }}</view>
<view class="money">¥{{ parseFloat(giftPrice).toFixed(2) }}</view>
</view>
<!-- 运费(仅支持快递配送时显示) -->
<view class="item acea-row row-between-wrapper" v-if="showPostage">
<view>{{ $t(`快递费用`) }}</view>
@ -78,6 +83,8 @@ const props = defineProps({
integralPrice: { type: [Number, String], default: 0 },
/** 积分兑换总额(非0时替换商品总价显示) */
integralTotal: { type: [Number, String], default: 0 },
/** 礼品附加费(送礼订单按件累计) */
giftPrice: { type: [Number, String], default: 0 },
/** 可用积分(积分兑换模式展示) */
usableIntegral: { type: [Number, String], default: 0 },
/** 是否显示快递费用行(快递配送关闭时隐藏) */

View File

@ -18,7 +18,7 @@
<text class="product-name">{{ item.store_name || '' }}</text>
<text v-if="getSkuText(item)" class="sku-text">{{ getSkuText(item) }}</text>
<view class="product-meta">
<text class="product-price">¥{{ item.true_price || item.price || '0.00' }}</text>
<text v-if="!isGiftReceiver" class="product-price">¥{{ item.true_price || item.price || '0.00' }}</text>
<text class="product-qty">x{{ item.cart_num || 1 }}</text>
</view>
</view>
@ -53,6 +53,8 @@ const emit = defineEmits(['click']);
const p = computed(() => (props.section && props.section.props) || {});
/** 引擎注入的全局状态 */
const ctx = computed(() => (props.section && props.section._ctx) || {});
/** 礼品领取人查看:不展示商品价格(与旧版 is_gift=2 一致) */
const isGiftReceiver = computed(() => ctx.value.viewerRole === 'gift_receiver');
/** 商品信息对象 */
const product = computed(() => p.value.product || {});
/**

View File

@ -17,8 +17,9 @@
</view>
<template v-if="!loading && !errorMsg">
<!-- 配送方式 + 收货地址,由 express_shipping / store_pickup / door_service 驱动 -->
<!-- 配送方式 + 收货地址,由 express_shipping / store_pickup / door_service 驱动;送礼订单由领取人填地址 -->
<DeliverySection
v-if="!isGift"
v-model="deliveryType"
:express-shipping="features.express_shipping"
:store-pickup="features.store_pickup"
@ -81,11 +82,22 @@
:fields="visitorFormConfig"
/>
<!-- 订单备注 -->
<!-- 订单备注 / 送礼留言 -->
<OrderRemarkInput
v-if="!isGift"
v-model="remark"
:visible="features.remark"
/>
<view v-if="isGift" class="gift-mark-card">
<view class="gift-mark-label">{{ $t('礼物留言') }}</view>
<input
v-model="giftMark"
class="gift-mark-input"
type="text"
maxlength="40"
:placeholder="$t('填写好友留言,最多40字')"
/>
</view>
<!-- 发票信息(抬头选择模式,编辑/保存走原 user_invoice_form 页面) -->
<OrderInvoicePanel
@ -112,9 +124,10 @@
:integral-price="integralPrice"
:integral-total="integralTotal"
:usable-integral="usableIntegral"
:show-postage="features.express_shipping && deliveryType === 'delivery'"
:gift-price="giftPrice"
:show-postage="!isGift && features.express_shipping && deliveryType === 'delivery'"
:door-postage="features.door_service_postage"
:show-door-postage="features.door_service && deliveryType === 'door'"
:show-door-postage="!isGift && features.door_service && deliveryType === 'door'"
/>
<!-- 底部提交栏 -->
@ -197,6 +210,10 @@ const num = ref(1);
const cartId = ref('');
const news = ref(0);
const isCartMode = ref(false);
// 送礼订单:1=购买礼物(不填地址/不计运费),2=领取礼物
const isGift = ref(0);
const giftMark = ref('');
const giftPrice = ref(0);
// 详情页 display_sections block 选中数据(票种/场次/日期),通过 URL block_data 参数透传
const selectedBlockData = ref([]);
@ -318,11 +335,14 @@ const usableIntegralDisplay = computed(() => {
const integralCheckDisabled = computed(() => usableIntegral.value <= 0 && !useIntegral.value);
const totalPriceText = computed(() => totalPrice.value || '0.00');
const payPriceText = computed(() => payPrice.value ?? '0.00');
/** 提交禁用:快递配送模式下未选地址,自提模式下未选门店,上门模式下未填完整预约信息 */
/** 提交禁用:快递配送模式下未选地址,自提模式下未选门店,上门模式下未填完整预约信息;送礼订单不校验收货信息 */
const submitDisabled = computed(() => {
if (!priceReady.value || priceComputing.value) {
return true;
}
if (isGift.value) {
return false;
}
if (features.value.express_shipping && deliveryType.value === 'delivery') {
return !addressInfo.value.real_name;
}
@ -419,6 +439,7 @@ async function loadConfirmData() {
if (activityType.value) params.activity_type = activityType.value;
if (activityId.value) params.activity_id = activityId.value;
}
if (isGift.value) params.is_gift = isGift.value;
var res = await getCenterOrderConfirm(params);
var data = res.data || {};
@ -432,7 +453,17 @@ async function loadConfirmData() {
levelPrice.value = confirmPriceGroup.levelPrice ?? data.level_price ?? 0;
memberPrice.value = confirmPriceGroup.memberPrice ?? data.member_price ?? 0;
postageDiscount.value = confirmPriceGroup.postageDiscount ?? data.postage_discount ?? 0;
giftPrice.value = confirmPriceGroup.giftPrice ?? data.gift_price ?? 0;
features.value = { ...features.value, ...(data.features || {}) };
// 送礼订单:后端 features 仍按商品配送能力下发,前端按 is_gift 隐藏地址与运费
if (isGift.value) {
features.value = {
...features.value,
express_shipping: false,
store_pickup: false,
door_service: false,
};
}
// 保存活动订单信息(含免费支付方式标识与跳转地址,由交易模式 Handler 下发)
activityInfo.value = data.activity_info || {};
// 同步发票相关配置:后端未返回时默认开启专用发票
@ -473,8 +504,8 @@ async function loadConfirmData() {
}
// 加载地址:有回跳 addressId 时加载地址详情,否则加载默认地址
// 购物车模式从地址列表选择后回跳会携带 address_id
if (features.value.express_shipping || features.value.store_pickup || features.value.door_service) {
// 购物车模式从地址列表选择后回跳会携带 address_id;送礼订单由领取人填地址,跳过
if (!isGift.value && (features.value.express_shipping || features.value.store_pickup || features.value.door_service)) {
if (addressId.value) {
await refreshAddressInfo();
} else {
@ -567,8 +598,8 @@ function ChangCoupons(e) {
*/
async function computedPrice() {
var requestSerial = ++priceRequestSerial;
// 快递运费依赖收货地址,未选择时等待地址回调再试算。
if (!orderKey.value || (features.value.express_shipping && deliveryType.value === 'delivery' && !addressId.value)) {
// 快递运费依赖收货地址,未选择时等待地址回调再试算;送礼订单不计运费,无需地址
if (!orderKey.value || (!isGift.value && features.value.express_shipping && deliveryType.value === 'delivery' && !addressId.value)) {
priceComputing.value = false;
priceReady.value = false;
priceError.value = '';
@ -593,11 +624,13 @@ async function computedPrice() {
address_id: addressId.value,
delivery_type: deliveryType.value,
store_id: deliveryType.value === 'pickup' ? (storeInfo.value.id || 0) : 0,
is_gift: isGift.value || 0,
});
if (requestSerial !== priceRequestSerial) return;
var data = res.data || {};
payPrice.value = data.pay_price ?? 0;
payPostage.value = data.pay_postage ?? 0;
giftPrice.value = data.gift_price ?? giftPrice.value;
// computed 接口返回 snake_case 价格字段,和确认接口的 priceGroup 保持同一展示口径。
levelPrice.value = data.level_price ?? data.priceGroup?.levelPrice ?? levelPrice.value;
memberPrice.value = data.member_price ?? data.priceGroup?.memberPrice ?? memberPrice.value;
@ -644,26 +677,29 @@ async function handleConfirm() {
uni.showToast({ title: i18n.global.t('金额正在计算,请稍后再试'), icon: 'none' });
return;
}
if (features.value.express_shipping && deliveryType.value === 'delivery' && !addressId.value) {
uni.showToast({ title: i18n.global.t('请选择收货地址'), icon: 'none' });
return;
}
if (features.value.store_pickup && deliveryType.value === 'pickup' && !storeInfo.value.id) {
uni.showToast({ title: i18n.global.t('请选择自提门店'), icon: 'none' });
return;
}
if (features.value.door_service && deliveryType.value === 'door') {
if (!serviceInfo.value.contact_name) {
uni.showToast({ title: i18n.global.t('请填写联系人姓名'), icon: 'none' });
// 送礼订单由领取人填收货信息,不校验地址/门店/预约
if (!isGift.value) {
if (features.value.express_shipping && deliveryType.value === 'delivery' && !addressId.value) {
uni.showToast({ title: i18n.global.t('请选择收货地址'), icon: 'none' });
return;
}
if (!serviceInfo.value.contact_phone) {
uni.showToast({ title: i18n.global.t('请填写联系电话'), icon: 'none' });
if (features.value.store_pickup && deliveryType.value === 'pickup' && !storeInfo.value.id) {
uni.showToast({ title: i18n.global.t('请选择自提门店'), icon: 'none' });
return;
}
if (!serviceInfo.value.appointment_time) {
uni.showToast({ title: i18n.global.t('请选择预约时间'), icon: 'none' });
return;
if (features.value.door_service && deliveryType.value === 'door') {
if (!serviceInfo.value.contact_name) {
uni.showToast({ title: i18n.global.t('请填写联系人姓名'), icon: 'none' });
return;
}
if (!serviceInfo.value.contact_phone) {
uni.showToast({ title: i18n.global.t('请填写联系电话'), icon: 'none' });
return;
}
if (!serviceInfo.value.appointment_time) {
uni.showToast({ title: i18n.global.t('请选择预约时间'), icon: 'none' });
return;
}
}
}
if (features.value.custom_form && customForm.value.length > 0) {
@ -687,8 +723,8 @@ async function handleConfirm() {
coupon_id: couponId.value,
use_integral: useIntegral.value ? 1 : 0,
order_data: {
address_id: !features.value.express_shipping || deliveryType.value === 'pickup' || deliveryType.value === 'door' ? 0 : addressId.value,
delivery_type: deliveryType.value,
address_id: isGift.value ? 0 : (!features.value.express_shipping || deliveryType.value === 'pickup' || deliveryType.value === 'door' ? 0 : addressId.value),
delivery_type: isGift.value ? 'delivery' : deliveryType.value,
store_id: deliveryType.value === 'pickup' ? (storeInfo.value.id || 0) : 0,
service: deliveryType.value === 'door' ? serviceInfo.value : {},
mark: remark.value,
@ -696,6 +732,8 @@ async function handleConfirm() {
visitors: (visitorFormRef.value && features.value.visitor_form) ? visitorFormRef.value.getData() : [],
block_data: selectedBlockData.value,
invoice_id: features.value.invoice ? (invoiceId.value || 0) : 0,
is_gift: isGift.value || 0,
gift_mark: isGift.value ? giftMark.value : '',
},
};
} else {
@ -710,8 +748,8 @@ async function handleConfirm() {
coupon_id: couponId.value,
use_integral: useIntegral.value ? 1 : 0,
order_data: {
address_id: !features.value.express_shipping || deliveryType.value === 'pickup' || deliveryType.value === 'door' ? 0 : addressId.value,
delivery_type: deliveryType.value,
address_id: isGift.value ? 0 : (!features.value.express_shipping || deliveryType.value === 'pickup' || deliveryType.value === 'door' ? 0 : addressId.value),
delivery_type: isGift.value ? 'delivery' : deliveryType.value,
store_id: deliveryType.value === 'pickup' ? (storeInfo.value.id || 0) : 0,
service: deliveryType.value === 'door' ? serviceInfo.value : {},
mark: remark.value,
@ -719,6 +757,8 @@ async function handleConfirm() {
visitors: (visitorFormRef.value && features.value.visitor_form) ? visitorFormRef.value.getData() : [],
block_data: selectedBlockData.value,
invoice_id: features.value.invoice ? (invoiceId.value || 0) : 0,
is_gift: isGift.value || 0,
gift_mark: isGift.value ? giftMark.value : '',
},
};
if (activityType.value) params.activity_type = activityType.value;
@ -746,7 +786,7 @@ async function handleConfirm() {
return;
}
uni.redirectTo({
url: routes.cashier + '?order_no=' + orderNo + '&from_type=order&price=' + payPrice,
url: routes.cashier + '?order_no=' + orderNo + '&from_type=order&price=' + payPrice + (isGift.value ? '&is_gift=1' : ''),
});
} catch (e) {
uni.showToast({ title: e.msg || e.message || i18n.global.t('下单失败'), icon: 'none' });
@ -953,6 +993,11 @@ function initFromOptions(options) {
cartId.value = routeQuery.cartId || '';
news.value = routeQuery.new === '1' || routeQuery.new === 1 ? 1 : 0;
isCartMode.value = !!cartId.value;
// 送礼下单:详情页「送礼物」跳转携带 is_gift=1
isGift.value = Number(routeQuery.is_gift || 0) || 0;
if (isGift.value === 1) {
giftMark.value = i18n.global.t('这是送您的一份礼物~');
}
productId.value = routeQuery.product_id || '';
skuUnique.value = routeQuery.sku_unique || '';
try {
@ -1043,4 +1088,22 @@ onBeforeUnmount(() => {
.product-card-hover {
opacity: 0.7;
}
/* 送礼留言 */
.gift-mark-card {
margin: 20rpx;
background: #fff;
border-radius: 12rpx;
padding: 24rpx 30rpx;
}
.gift-mark-label {
font-size: 28rpx;
color: #282828;
margin-bottom: 12rpx;
}
.gift-mark-input {
width: 100%;
font-size: 28rpx;
color: #282828;
}
</style>

View File

@ -27,6 +27,14 @@
/>
<!-- #endif -->
</template>
<!-- 送礼订单:分享入口由底部按钮组(与「申请退款」同组)的 shareGift 动作唤起 -->
<giftModal
:aleartStatus="giftModalShow"
:giftData="giftShareData"
@shareH5="shareH5"
@close="giftModalShow = false"
/>
<view class="mask" v-if="giftModalShow" @click="giftModalShow = false"></view>
</view>
</view>
</template>
@ -54,9 +62,11 @@ import SectionDispatcher from '../components/order/detail/SectionDispatcher.vue'
// #endif
import actionMap from '../api/action-map.js';
import { getCenterOrderDetail, takeOrder } from '@/api/order-center.js';
import { express } from '@/api/order.js';
import routes from '../api/routes.js';
import { useColorTheme } from '@/composables/useColorTheme';
import i18n from '@/utils/lang.js';
import giftModal from '@/pages/goods/order_pay_status/components/giftModal.vue';
defineOptions({ name: 'OrderDetailEngine' });
@ -72,6 +82,9 @@ const { colorStyle } = useColorTheme();
const pageConfig = ref({ sections: [], extensions: {} });
const loading = ref(true);
const confirmingReceive = ref(false);
const logisticsLoading = ref(false);
// 送礼分享
const giftModalShow = ref(false);
// ---------- computed ----------
/**
@ -107,6 +120,30 @@ const payPrice = computed(() => {
const payStatus = computed(() => {
return (sectionPropsMap.value['status'] && sectionPropsMap.value['status'].pay_status) || 0;
});
/** 查看者角色:gift_receiver 表示当前用户是礼品领取人(只读视图) */
const viewerRole = computed(() => {
return (pageConfig.value.extensions && pageConfig.value.extensions.viewer_role) || '';
});
/** 订单信息字段(送礼标识/领取人等) */
const orderInfoFields = computed(() => {
return (sectionPropsMap.value['order_info'] && sectionPropsMap.value['order_info'].fields) || {};
});
/** giftModal 分享数据 */
const giftShareData = computed(() => {
// section.props.product = { product: {title,image,items}, product_id }
var productObj = (sectionPropsMap.value['product'] && sectionPropsMap.value['product'].product) || {};
var productItems = productObj.items || [];
var first = productItems[0] || {};
return {
id: orderInfoFields.value.id || 0,
image: first.image || productObj.image || '',
title: first.store_name || first.title || productObj.title || '',
message: orderInfoFields.value.gift_mark || '',
avatar: '',
nickname: '',
code: '',
};
});
// ---------- watch ----------
// The parent page receives route options in onLoad, after this child may
@ -133,6 +170,8 @@ async function loadDetail() {
return;
}
pageConfig.value = (res.data && res.data.page_config) || { sections: [], extensions: {} };
// 详情接口首屏只返回已缓存的物流轨迹,未命中时异步补齐,避免被第三方物流慢请求阻塞
loadLogisticsTracks();
} catch (e) {
uni.showToast({ title: i18n.t('加载失败'), icon: 'none' });
} finally {
@ -140,6 +179,43 @@ async function loadDetail() {
}
}
/**
* 异步加载物流轨迹
* 详情接口不再同步请求第三方物流,仅返回已缓存轨迹;未命中时由前端按需查询并回填 section
* 失败时静默降级,仍展示快递公司 + 单号
* @return {Promise<void>}
*/
async function loadLogisticsTracks() {
if (logisticsLoading.value) return;
const sections = pageConfig.value.sections || [];
const section = sections.find(function (s) { return s.key === 'logistics'; });
if (!section) return;
// 已命中后端轨迹缓存,无需再次查询
const cached = (section.props && section.props.tracks) || [];
if (cached.length > 0) return;
const orderNo = (sectionPropsMap.value['order_info'] && sectionPropsMap.value['order_info'].fields
&& sectionPropsMap.value['order_info'].fields.order_no) || props.orderNo || '';
if (!orderNo) return;
logisticsLoading.value = true;
try {
const res = await express(orderNo);
const list = res && res.data && res.data.express && res.data.express.result
? res.data.express.result.list : null;
if (!Array.isArray(list) || list.length === 0) return;
section.props.tracks = list.map(function (track) {
return {
title: track.title || track.status || track.context || track.name || '',
time: track.time || track.ftime || '',
desc: track.desc || '',
};
});
} catch (e) {
// 物流查询失败不影响详情展示
} finally {
logisticsLoading.value = false;
}
}
// 包装页 onShow 返回时调用(评价完成/售后进度变化等场景,返回详情需看到最新状态)
defineExpose({ refresh: loadDetail });
@ -158,6 +234,7 @@ function withCtx(section) {
payStatus: payStatus.value,
isRefund: isRefund.value,
sectionPropsMap: sectionPropsMap.value,
viewerRole: viewerRole.value,
},
});
}
@ -207,11 +284,24 @@ function onButtonAction(payload) {
var orderNo = ((sectionPropsMap.value['order_info'] && sectionPropsMap.value['order_info'].fields)
&& sectionPropsMap.value['order_info'].fields.order_no)
|| (payload.params && payload.params.order_no) || '';
var context = payload.action === 'confirmReceive' ? { onConfirm: confirmReceive } : undefined;
// 需要页面内弹窗/刷新等交互的动作,通过 context 回调注入,action-map 不直接持有页面状态
var context;
if (payload.action === 'confirmReceive') {
context = { onConfirm: confirmReceive };
} else if (payload.action === 'shareGift') {
context = { onShareGift: openGiftShare };
}
handler(Object.assign({}, sectionPropsMap.value, { order_no: orderNo }), payload.params || {}, context);
}
}
/**
* 打开送礼分享弹窗(送给好友按钮,与申请退款同组)
*/
function openGiftShare() {
giftModalShow.value = true;
}
/**
* 详情页确认收货
* @param {object} payload {order_no}
@ -329,6 +419,10 @@ function goGoodCall() {
&& sectionPropsMap.value['order_info'].fields.order_no) || '';
uni.navigateTo({ url: routes.customerChat + '?orderId=' + orderNo });
}
/** H5 分享面板回调(giftModal) */
function shareH5() {
// 链接复制由 giftModal 内部完成
}
</script>
<style scoped>
@ -337,4 +431,15 @@ function goGoodCall() {
padding-bottom: calc(150rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(150rpx + env(safe-area-inset-bottom));
}
.mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
/* 低于 giftModal 的 999,避免遮罩盖住弹窗按钮 */
z-index: 998;
}
</style>

View File

@ -44,6 +44,7 @@
<view class="title acea-row row-between-wrapper">
<view class="acea-row row-middle">
<text v-if="order.type_label" class="type-tag">{{ order.type_label }}</text>
<text v-if="order.is_gift" class="type-tag gift-tag">{{ order.is_gift_receiver ? $t('收到的礼物') : $t('礼物') }}</text>
<view>{{ formatTime(order.add_time) }}</view>
</view>
<StatusBadge :status-code="order.status_code" :status-text="order.status_text" />
@ -58,7 +59,7 @@
<text class="goods-name line2">{{ item.store_name || '' }}</text>
<text v-if="getItemSkuText(item)" class="goods-sku line1">{{ getItemSkuText(item) }}</text>
<view class="goods-meta">
<text class="goods-price">¥{{ item.true_price || item.price || '0.00' }}</text>
<text v-if="!order.is_gift_receiver" class="goods-price">¥{{ item.true_price || item.price || '0.00' }}</text>
<text class="goods-num">x{{ item.cart_num || 1 }}</text>
</view>
</view>
@ -73,7 +74,7 @@
<view class="text">
<view class="title-row">
<text class="name line2">{{ order.title || '' }}</text>
<view class="money">
<view v-if="!order.is_gift_receiver" class="money">
<view>{{ $t(`¥`) }}{{ order.pay_price || '0.00' }}</view>
<view>x{{ order.total_num || 0 }}</view>
</view>
@ -426,6 +427,11 @@ defineExpose({ refresh: refreshList });
border-radius: 6rpx;
}
.my-order .list .item .title .gift-tag {
color: #ff6b00;
background: rgba(255, 107, 0, 0.1);
}
.my-order .list .item .item-info {
padding: 0 30rpx;
margin-top: 22rpx;

View File

@ -39,6 +39,18 @@
@click="onAction(action)"
>{{ $t(action.label) }}</button>
</view>
<!-- 送礼订单:支付成功后引导分享礼物(按钮样式与其它操作按钮一致) -->
<view v-if="isGiftShareVisible" @click="giftModalShow = true">
<button class="returnBnt bg-color" hover-class="none">{{ $t('送给好友') }}</button>
</view>
<!-- 蒙层置于弹窗下层,避免遮挡 giftModal -->
<view class="gift-mask" v-if="giftModalShow" @click="giftModalShow = false"></view>
<giftModal
:aleartStatus="giftModalShow"
:giftData="giftData"
@shareH5="shareH5"
@close="giftModalShow = false"
/>
<!-- 赠送优惠券 -->
<view class="coupons" v-if="couponList.length">
<view class="title acea-row row-center-wrapper">
@ -92,6 +104,7 @@ import { useAppStore } from '@/store/modules/app';
import { useColorTheme } from '@/composables/useColorTheme';
import routes from '../api/routes.js';
import i18n from '@/utils/lang.js';
import giftModal from '@/pages/goods/order_pay_status/components/giftModal.vue';
defineOptions({ name: 'PayStatusEngine' });
@ -111,6 +124,9 @@ const msg = ref('');
const payType = ref('');
/** 是否礼品订单(0 否) */
const isGift = ref(0);
// 送礼分享弹窗与数据
const giftModalShow = ref(false);
const giftData = ref({});
// 是否已完成首次查询
const loading = ref(false);
// 订单支付信息
@ -181,6 +197,13 @@ const currentActions = computed(() => {
list.push(actionMap.value.goIndex);
return list;
});
/**
* 送礼分享入口可见性:送礼订单且已支付(含线下支付成功)
*/
const isGiftShareVisible = computed(() => {
const info = order_pay_info.value || {};
return !!isGift.value && (info.pay_status === 1 || info.pay_type === 'offline');
});
/**
* 状态图标 class
* @returns {String} 图标 class 字符串
@ -291,6 +314,22 @@ function getOrderPayInfo() {
pay_price: pricingProps.pay_price || '0.00',
pay_status: statusProps.pay_status || 0,
};
// 送礼订单:URL 参数缺失时按订单详情字段兜底
const orderIsGift = Number(orderInfoFields.is_gift || 0) || (isGift.value ? 1 : 0);
isGift.value = orderIsGift;
// section.props.product = { product: {title,image,items}, product_id }
const productObj = (propsMap['product'] && propsMap['product'].product) || {};
const productItems = productObj.items || [];
const firstProduct = productItems[0] || {};
giftData.value = {
id: orderInfoFields.id || 0,
image: firstProduct.image || productObj.image || '',
title: firstProduct.store_name || firstProduct.title || productObj.title || '',
message: orderInfoFields.gift_mark || '',
avatar: '',
nickname: '',
code: '',
};
// 根据支付状态设置导航栏标题
// pay_status: 0未支付 1已支付 2部分退款 3全额退款
const isPaid = order_pay_info.value.pay_status === 1;
@ -298,8 +337,10 @@ function getOrderPayInfo() {
title: isPaid ? i18n.t('支付成功') : i18n.t('未支付'),
});
loading.value = true;
// 非礼品订单禁用分享(本引擎不支持礼品分享)
uni.hideShareMenu();
// 送礼订单允许分享领取链接;普通订单禁用分享
if (!isGift.value) {
uni.hideShareMenu();
}
getOrderCoupon();
})
.catch(() => {
@ -360,6 +401,12 @@ function goIndex() {
url: '/pages/index/index',
});
}
/**
* H5 分享面板(giftModal 回调)
*/
function shareH5() {
// 保留与旧支付结果页一致的回调占位;链接复制由 giftModal 内部完成
}
// 供壳子(pages/center/pay/status)onMounted 初始化、onShow 刷新,通过 engineRef 调用
defineExpose({
@ -437,6 +484,17 @@ defineExpose({
margin: 0 auto 20rpx auto;
}
/* 送礼分享蒙层:压在内容上、置于 giftModal(z-index 999)之下 */
.gift-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 998;
}
.coupons {
.title {
margin: 30rpx 0 25rpx 0;

View File

@ -1463,8 +1463,10 @@
store_id: system_store.value ? system_store.value.id : 0,
}
orderReceiveGift(orderId.value, data).then(res => {
// 订单详情页按订单号(order_no)跳转,不能传中台主键 id
const receivedOrderNo = giftData.value.order_no || '';
uni.reLaunch({
url: `/pages/goods/receive_gifts_status/index?status=${res.data.status}&order_id=${giftData.value.order_id}`
url: `/pages/goods/receive_gifts_status/index?status=${res.data.status}&order_no=${receivedOrderNo}`
})
}).catch(err => {
uni.showToast({

View File

@ -48,7 +48,8 @@ const image = ref('');
const store_name = ref('');
const nickname = ref('');
const giftStatusText = ref('');
const o_id = ref(0);
// 订单号:已领取后跳转订单详情使用
const orderNo = ref('');
// 原代码未在 data 声明、直接挂到实例上的字段
let gift_uid;
// 原代码 onLoad 中 this.uid = this.$store.state.app.uid 的快照
@ -74,8 +75,13 @@ onLoad((options) => {
function goGetGift() {
if (giftStatus.value == 2) {
// 已领取:查看礼物订单详情,按订单号跳转(中台主键 id 不能作为 order_no)
if (!orderNo.value) {
uni.showToast({ title: i18n.t(`订单信息异常`), icon: 'none' });
return;
}
uni.reLaunch({
url: `/pages/goods/order_details/index?is_gift=2&order_id=${o_id.value}&is_gift=2`
url: `/pages/center/order/detail/index?order_no=${orderNo.value}`
});
} else {
uni.reLaunch({
@ -89,9 +95,12 @@ function getOrderInfo() {
gift_mark.value = res.data.gift_mark;
nickname.value = res.data.nickname;
refund_status.value = res.data.refund_status;
image.value = res.data.cartInfo[0].productInfo.image;
store_name.value = res.data.cartInfo[0].productInfo.store_name;
o_id.value = res.data.order_id
// order_items 为扁平商品快照;兼容旧结构 productInfo.*
const first = (res.data.cartInfo && res.data.cartInfo[0]) || {};
const product = first.productInfo || first;
image.value = product.image || first.image || '';
store_name.value = product.store_name || first.store_name || '';
orderNo.value = res.data.order_no || '';
if (gift_uid === 0) {
giftStatus.value = 1;
giftStatusText.value = i18n.t('收下礼物');
@ -101,6 +110,8 @@ function getOrderInfo() {
} else if (gift_uid !== uidSnapshot) {
giftStatus.value = 3;
}
}).catch((err) => {
uni.showToast({ title: (err && err.msg) || i18n.t('加载失败'), icon: 'none' });
});
}
</script>

View File

@ -24,13 +24,15 @@
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import i18n from '@/utils/lang.js';
const status = ref(0);
const orderId = ref(0);
const orderNo = ref('');
onLoad((options) => {
status.value = options.status;
orderId.value = options.order_id;
// order_no 为现行参数;order_id 兼容历史链接(历史链接中携带的同样是订单号)
orderNo.value = options.order_no || options.order_id || '';
});
function goPage(type) {
@ -38,11 +40,16 @@ function goPage(type) {
uni.reLaunch({
url: '/pages/index/index'
});
} else {
uni.reLaunch({
url: `/pages/goods/order_details/index?order_id=${orderId.value}`
});
return;
}
// 订单详情按订单号跳转(中台主键 id 不能作为 order_no 使用)
if (!orderNo.value) {
uni.showToast({ title: i18n.t(`订单信息异常`), icon: 'none' });
return;
}
uni.reLaunch({
url: `/pages/center/order/detail/index?order_no=${orderNo.value}`
});
}
</script>

View File

@ -8,7 +8,7 @@
<view class="acea-row row-between-wrapper px-20 py-14" style="height: 100%">
<div class="acea-row">
<block v-if="!isCustomEntry">
<block v-for="(item_id, index) in showIcons" :key="index">
<block v-for="(item_id, index) in displayIcons" :key="index">
<navigator :render-link="false"
v-if="item_id === 3"
hover-class="none"
@ -358,6 +358,22 @@ const showIcons = computed(() => {
if (!bottomConfig.value) return [3, 1, 2, 0, 4, 5];
return bottomConfig.value.showContent.type;
});
// 送礼入口会占一席,左侧最多只保留 3 个按钮(含送礼)。
// 超出时按 首页(3) → 客服(0) → 购物车(2) 的优先级隐藏,收藏/分享保留。
// 模板仅渲染 0/1/2/3/4,占位 id 不参与配额,避免误判按钮数量。
const RENDERABLE_ICON_IDS = [3, 1, 2, 0, 4];
const displayIcons = computed(() => {
const icons = (showIcons.value || []).filter((id) => RENDERABLE_ICON_IDS.includes(id));
if (!props.is_gift) return (showIcons.value || []).slice();
const hideOrder = [3, 0, 2];
// 含送礼共 3 个 => 常规图标最多 2 个
for (const id of hideOrder) {
if (icons.length <= 2) break;
const idx = icons.indexOf(id);
if (idx > -1) icons.splice(idx, 1);
}
return icons;
});
const showCartButton = computed(() => {
if (!bottomConfig.value) return true;
return bottomConfig.value.cartButton.tabVal === 0;