From 78f56bb07450cbe1a78e02b3ab5a23e7cc1cc5e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=98=8A=E5=A4=A9?= <442384644@qq.com> Date: Wed, 21 Oct 2026 16:22:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E9=80=81=E7=A4=BC=E8=AE=A2=E5=8D=95):=20?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=80=81=E7=A4=BC/=E6=94=B6=E7=A4=BC?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E5=85=A8=E9=93=BE=E8=B7=AF=E4=B8=8E=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 订单详情按查看者裁剪:购买者隐藏收货信息、物流轨迹及确认收货/申请退款/查看物流;领取人展示收货信息、物流轨迹与确认收货/查看物流 - 领取人订单列表纳入礼物订单并只读展示,统计计数含礼物、金额仅计本人支付 - 收礼人可确认收货;修复领取成功页订单号传递与领取页礼物详情查询 - 新增 eb_order_center.gift_uid 实体列与索引并同步安装 SQL - 详情接口物流轨迹改为缓存读取 + 前端异步补齐,空轨迹短缓存 120 秒,修复送礼订单物流接口收货地址取值 --- .../v1/order/CenterOrderController.php | 167 ++++++++----- .../v1/order/StoreOrderInvoiceController.php | 2 +- backend/app/dao/order/OrderCenterDao.php | 14 ++ backend/app/model/order/OrderCenter.php | 1 + .../services/order/OrderCenterServices.php | 62 ++++- .../order/OrderCenterUserServices.php | 233 ++++++++++++++---- .../app/services/order/OrderTakeServices.php | 13 +- .../app/services/shipping/ExpressServices.php | 64 ++++- .../controller/user/OrderController.php | 5 +- .../normal/handler/NormalOrderAdapter.php | 113 +++++++++ .../OrderCenterDetailExtendListener.php | 23 +- backend/public/install/crmeb_table.sql | 2 + .../src/dynamic-pages/api/action-map.js | 16 ++ .../order/common/OrderPriceDetail.vue | 7 + .../components/order/detail/ProductCard.vue | 4 +- .../engines/OrderCreateEngine.vue | 123 ++++++--- .../engines/OrderDetailEngine.vue | 107 +++++++- .../dynamic-pages/engines/OrderListEngine.vue | 10 +- .../dynamic-pages/engines/PayStatusEngine.vue | 62 ++++- .../src/pages/goods/order_confirm/index.vue | 4 +- .../src/pages/goods/receive_gift/index.vue | 21 +- .../goods/receive_gifts_status/index.vue | 19 +- .../diyComponents/productBottom.vue | 18 +- 23 files changed, 906 insertions(+), 184 deletions(-) diff --git a/backend/app/api/controller/v1/order/CenterOrderController.php b/backend/app/api/controller/v1/order/CenterOrderController.php index 2bc7cc3ae..076c18f7c 100644 --- a/backend/app/api/controller/v1/order/CenterOrderController.php +++ b/backend/app/api/controller/v1/order/CenterOrderController.php @@ -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('订单不存在'); } diff --git a/backend/app/api/controller/v1/order/StoreOrderInvoiceController.php b/backend/app/api/controller/v1/order/StoreOrderInvoiceController.php index 76941f093..a926b5699 100644 --- a/backend/app/api/controller/v1/order/StoreOrderInvoiceController.php +++ b/backend/app/api/controller/v1/order/StoreOrderInvoiceController.php @@ -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('订单不存在'); } diff --git a/backend/app/dao/order/OrderCenterDao.php b/backend/app/dao/order/OrderCenterDao.php index 43e854a61..4d6ed8372 100644 --- a/backend/app/dao/order/OrderCenterDao.php +++ b/backend/app/dao/order/OrderCenterDao.php @@ -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] + ); + } + /** * 保存或更新记录 * 根据条件判断是新增还是更新,支持幂等性操作 diff --git a/backend/app/model/order/OrderCenter.php b/backend/app/model/order/OrderCenter.php index 48312f276..cd9096749 100644 --- a/backend/app/model/order/OrderCenter.php +++ b/backend/app/model/order/OrderCenter.php @@ -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 退款奖励累计已处理目标,含已核对短缺 diff --git a/backend/app/services/order/OrderCenterServices.php b/backend/app/services/order/OrderCenterServices.php index b507cdcea..4480111fa 100644 --- a/backend/app/services/order/OrderCenterServices.php +++ b/backend/app/services/order/OrderCenterServices.php @@ -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} / 其他兼容 diff --git a/backend/app/services/order/OrderCenterUserServices.php b/backend/app/services/order/OrderCenterUserServices.php index 0fd32aa4e..3e307b90b 100644 --- a/backend/app/services/order/OrderCenterUserServices.php +++ b/backend/app/services/order/OrderCenterUserServices.php @@ -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; } diff --git a/backend/app/services/order/OrderTakeServices.php b/backend/app/services/order/OrderTakeServices.php index 145fa58ca..ea21d606e 100644 --- a/backend/app/services/order/OrderTakeServices.php +++ b/backend/app/services/order/OrderTakeServices.php @@ -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){ diff --git a/backend/app/services/shipping/ExpressServices.php b/backend/app/services/shipping/ExpressServices.php index 69709f93d..560974d5b 100644 --- a/backend/app/services/shipping/ExpressServices.php +++ b/backend/app/services/shipping/ExpressServices.php @@ -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; } diff --git a/backend/modules/product/normal/controller/user/OrderController.php b/backend/modules/product/normal/controller/user/OrderController.php index 34bc0491c..c98004d7a 100644 --- a/backend/modules/product/normal/controller/user/OrderController.php +++ b/backend/modules/product/normal/controller/user/OrderController.php @@ -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()); diff --git a/backend/modules/product/normal/handler/NormalOrderAdapter.php b/backend/modules/product/normal/handler/NormalOrderAdapter.php index 19666a749..4562c3c5d 100644 --- a/backend/modules/product/normal/handler/NormalOrderAdapter.php +++ b/backend/modules/product/normal/handler/NormalOrderAdapter.php @@ -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; diff --git a/backend/modules/product/normal/listener/OrderCenterDetailExtendListener.php b/backend/modules/product/normal/listener/OrderCenterDetailExtendListener.php index e93a1ddec..65285946a 100644 --- a/backend/modules/product/normal/listener/OrderCenterDetailExtendListener.php +++ b/backend/modules/product/normal/listener/OrderCenterDetailExtendListener.php @@ -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']; + } } } diff --git a/backend/public/install/crmeb_table.sql b/backend/public/install/crmeb_table.sql index 65535d8a5..e5d38d411 100755 --- a/backend/public/install/crmeb_table.sql +++ b/backend/public/install/crmeb_table.sql @@ -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`), diff --git a/frontend/uni-app/src/dynamic-pages/api/action-map.js b/frontend/uni-app/src/dynamic-pages/api/action-map.js index 9742d717c..0475899b1 100644 --- a/frontend/uni-app/src/dynamic-pages/api/action-map.js +++ b/frontend/uni-app/src/dynamic-pages/api/action-map.js @@ -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 || ''; diff --git a/frontend/uni-app/src/dynamic-pages/components/order/common/OrderPriceDetail.vue b/frontend/uni-app/src/dynamic-pages/components/order/common/OrderPriceDetail.vue index fb44bdae3..e364fdf75 100644 --- a/frontend/uni-app/src/dynamic-pages/components/order/common/OrderPriceDetail.vue +++ b/frontend/uni-app/src/dynamic-pages/components/order/common/OrderPriceDetail.vue @@ -14,6 +14,11 @@ {{ $t(`可用积分`) }} {{ usableIntegral }} + + + {{ $t(`礼品附加费`) }} + ¥{{ parseFloat(giftPrice).toFixed(2) }} + {{ $t(`快递费用`) }} @@ -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 }, /** 是否显示快递费用行(快递配送关闭时隐藏) */ diff --git a/frontend/uni-app/src/dynamic-pages/components/order/detail/ProductCard.vue b/frontend/uni-app/src/dynamic-pages/components/order/detail/ProductCard.vue index 36a896a79..a7ed64c44 100644 --- a/frontend/uni-app/src/dynamic-pages/components/order/detail/ProductCard.vue +++ b/frontend/uni-app/src/dynamic-pages/components/order/detail/ProductCard.vue @@ -18,7 +18,7 @@ {{ item.store_name || '' }} {{ getSkuText(item) }} - ¥{{ item.true_price || item.price || '0.00' }} + ¥{{ item.true_price || item.price || '0.00' }} x{{ item.cart_num || 1 }} @@ -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 || {}); /** diff --git a/frontend/uni-app/src/dynamic-pages/engines/OrderCreateEngine.vue b/frontend/uni-app/src/dynamic-pages/engines/OrderCreateEngine.vue index c2f36b60b..2f7270775 100644 --- a/frontend/uni-app/src/dynamic-pages/engines/OrderCreateEngine.vue +++ b/frontend/uni-app/src/dynamic-pages/engines/OrderCreateEngine.vue @@ -17,8 +17,9 @@ + + + @@ -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} + */ +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 内部完成 +} diff --git a/frontend/uni-app/src/dynamic-pages/engines/OrderListEngine.vue b/frontend/uni-app/src/dynamic-pages/engines/OrderListEngine.vue index 3c71bb88b..147a8f8a7 100644 --- a/frontend/uni-app/src/dynamic-pages/engines/OrderListEngine.vue +++ b/frontend/uni-app/src/dynamic-pages/engines/OrderListEngine.vue @@ -44,6 +44,7 @@ {{ order.type_label }} + {{ order.is_gift_receiver ? $t('收到的礼物') : $t('礼物') }} {{ formatTime(order.add_time) }} @@ -58,7 +59,7 @@ {{ item.store_name || '' }} {{ getItemSkuText(item) }} - ¥{{ item.true_price || item.price || '0.00' }} + ¥{{ item.true_price || item.price || '0.00' }} x{{ item.cart_num || 1 }} @@ -73,7 +74,7 @@ {{ order.title || '' }} - + {{ $t(`¥`) }}{{ order.pay_price || '0.00' }} x{{ order.total_num || 0 }} @@ -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; diff --git a/frontend/uni-app/src/dynamic-pages/engines/PayStatusEngine.vue b/frontend/uni-app/src/dynamic-pages/engines/PayStatusEngine.vue index 7d8fd5639..195b047ee 100644 --- a/frontend/uni-app/src/dynamic-pages/engines/PayStatusEngine.vue +++ b/frontend/uni-app/src/dynamic-pages/engines/PayStatusEngine.vue @@ -39,6 +39,18 @@ @click="onAction(action)" >{{ $t(action.label) }} + + + + + + + @@ -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; diff --git a/frontend/uni-app/src/pages/goods/order_confirm/index.vue b/frontend/uni-app/src/pages/goods/order_confirm/index.vue index 9bb087a01..17c308715 100644 --- a/frontend/uni-app/src/pages/goods/order_confirm/index.vue +++ b/frontend/uni-app/src/pages/goods/order_confirm/index.vue @@ -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({ diff --git a/frontend/uni-app/src/pages/goods/receive_gift/index.vue b/frontend/uni-app/src/pages/goods/receive_gift/index.vue index 799bb94ff..d78be5626 100644 --- a/frontend/uni-app/src/pages/goods/receive_gift/index.vue +++ b/frontend/uni-app/src/pages/goods/receive_gift/index.vue @@ -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' }); }); } diff --git a/frontend/uni-app/src/pages/goods/receive_gifts_status/index.vue b/frontend/uni-app/src/pages/goods/receive_gifts_status/index.vue index 6dd517848..fc6e37e95 100644 --- a/frontend/uni-app/src/pages/goods/receive_gifts_status/index.vue +++ b/frontend/uni-app/src/pages/goods/receive_gifts_status/index.vue @@ -24,13 +24,15 @@ diff --git a/frontend/uni-app/src/subpackage/diyComponents/productBottom.vue b/frontend/uni-app/src/subpackage/diyComponents/productBottom.vue index ed2adfff4..aba32e6a6 100644 --- a/frontend/uni-app/src/subpackage/diyComponents/productBottom.vue +++ b/frontend/uni-app/src/subpackage/diyComponents/productBottom.vue @@ -8,7 +8,7 @@
- + { 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;