From d5e2f6066aa47f9335c1888944c89e899d0aea1b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=90=B4=E6=98=8A=E5=A4=A9?= <442384644@qq.com>
Date: Thu, 22 Oct 2026 10:45:50 +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=E7=A4=BC=E7=89=A9=E4=BF=A1=E6=81=AF=E3=80=81?=
=?UTF-8?q?=E9=A2=86=E5=8F=96=E4=BA=BA=E6=93=8D=E4=BD=9C=E4=B8=8E=E8=AE=A2?=
=?UTF-8?q?=E5=8D=95=E9=87=91=E9=A2=9D=E5=B1=95=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 订单详情新增礼物信息卡:送礼人查看领取人,领取人查看赠送人,含礼物留言与领取时间
- 领取人按订单流程可见确认收货、查看物流、去评价,评价接口权限放开至领取人
- 商品信息卡增加标题,商品金额与商品总额按规格原价(attr_info.price)展示
- 订单列表商品图放大、规格文字与图片底部对齐,修复次级按钮双层边框
---
.../v1/order/CenterOrderController.php | 42 +++++--
.../services/order/OrderCenterServices.php | 92 ++++++++++++--
.../order/OrderCenterUserServices.php | 24 +++-
.../normal/handler/NormalOrderAdapter.php | 2 +
.../components/order/detail/GiftInfoCard.vue | 113 ++++++++++++++++++
.../components/order/detail/ProductCard.vue | 56 ++++++---
.../order/detail/SectionDispatcher.vue | 2 +
.../dynamic-pages/engines/OrderListEngine.vue | 40 +++++--
8 files changed, 318 insertions(+), 53 deletions(-)
create mode 100644 frontend/uni-app/src/dynamic-pages/components/order/detail/GiftInfoCard.vue
diff --git a/backend/app/api/controller/v1/order/CenterOrderController.php b/backend/app/api/controller/v1/order/CenterOrderController.php
index 076c18f7c..dcfb86237 100644
--- a/backend/app/api/controller/v1/order/CenterOrderController.php
+++ b/backend/app/api/controller/v1/order/CenterOrderController.php
@@ -2841,11 +2841,33 @@ class CenterOrderController
return app('json')->success($cartProduct);
}
+ /**
+ * 判断用户是否可对订单执行用户端操作:下单人本人或送礼订单领取人
+ * @param array $order 订单数据(含 uid / gift_uid / extra_info)
+ * @param int $uid 当前用户 ID
+ * @return bool
+ */
+ protected function canOperateOrder(array $order, int $uid): bool
+ {
+ if ((int)($order['uid'] ?? 0) === $uid) {
+ return true;
+ }
+ $giftUid = (int)($order['gift_uid'] ?? 0);
+ if ($giftUid === 0) {
+ $extra = $order['extra_info'] ?? [];
+ if (is_string($extra)) {
+ $extra = json_decode($extra, true) ?: [];
+ }
+ $giftUid = (int)($extra['gift']['gift_uid'] ?? ($extra['gift_uid'] ?? 0));
+ }
+ return $giftUid > 0 && $giftUid === $uid;
+ }
+
/**
* 按 SKU 唯一码从中台订单快照定位商品行
* 查询订单中台的 extra_info.items,匹配 attr_info.unique(与订单评价 unique 同源)
* @param string $unique SKU 唯一码
- * @param int|null $uid 限定用户(评价场景必传防越权),null 不限定
+ * @param int|null $uid 限定用户(评价场景必传防越权,下单人或送礼领取人),null 不限定
* @return array|null 商品行(附 _order_no/_center_id),未找到返回 null
*/
protected function findOrderItemBySkuUnique(string $unique, ?int $uid = null): ?array
@@ -2853,12 +2875,13 @@ class CenterOrderController
if ($unique === '') {
return null;
}
+ // 不直接按 uid 过滤:送礼订单的领取人也需要能评价(领取人 uid 不等于订单 uid)
$where = [['extra_info', 'like', '%' . $unique . '%']];
- if ($uid !== null) {
- $where[] = ['uid', '=', $uid];
- }
- $orders = app()->make(OrderCenterDao::class)->selectList($where, 'id, order_no, extra_info', 0, 0);
+ $orders = app()->make(OrderCenterDao::class)->selectList($where, 'id, uid, gift_uid, order_no, extra_info', 0, 0);
foreach ($orders as $order) {
+ if ($uid !== null && !$this->canOperateOrder($order, $uid)) {
+ continue;
+ }
$extra = is_string($order['extra_info']) ? json_decode($order['extra_info'], true) : ($order['extra_info'] ?? []);
foreach ($extra['items'] ?? [] as $item) {
if (($item['attr_info']['unique'] ?? '') === $unique) {
@@ -2876,7 +2899,7 @@ class CenterOrderController
* 评价必须落到用户指定的那笔订单:同商品重复购买时各订单 unique 相同,仅凭 unique 反查会命中旧订单
* @param string $orderNo 中台订单号
* @param string $unique SKU 唯一码
- * @param int|null $uid 限定归属用户
+ * @param int|null $uid 限定归属用户(下单人或送礼领取人)
* @return array|null
*/
protected function findOrderItemByOrderNo(string $orderNo, string $unique, ?int $uid = null): ?array
@@ -2889,7 +2912,7 @@ class CenterOrderController
return null;
}
$order = is_object($order) ? $order->toArray() : (array)$order;
- if ($uid !== null && (int)($order['uid'] ?? 0) !== $uid) {
+ if ($uid !== null && !$this->canOperateOrder($order, $uid)) {
return null;
}
$extra = is_string($order['extra_info'] ?? null) ? json_decode($order['extra_info'], true) : ($order['extra_info'] ?? []);
@@ -2951,8 +2974,9 @@ class CenterOrderController
$orderInfo = $orderInfo->toArray();
// ==================== 6. 权限验证 ====================
- if ($uid != $orderInfo['uid']) {
- return app('json')->fail('不是您自己的订单,无法评价');
+ // 下单人本人或送礼订单领取人均可评价(领取人评价时 uid 为领取人)
+ if (!$this->canOperateOrder($orderInfo, $uid)) {
+ return app('json')->fail('无权评价该订单');
}
// ==================== 7. 调用评价服务(业务逻辑和事件触发都在Service中)===================
diff --git a/backend/app/services/order/OrderCenterServices.php b/backend/app/services/order/OrderCenterServices.php
index 4480111fa..b15d0c2f4 100644
--- a/backend/app/services/order/OrderCenterServices.php
+++ b/backend/app/services/order/OrderCenterServices.php
@@ -449,10 +449,21 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
}
$detail = $detail->toArray();
$detail['extra_info'] = $this->parseExtraInfo($detail['extra_info']);
- // 下单用户昵称/绑定电话(eb_user),供详情"用户信息"区展示
- $detailUser = User::where('uid', (int)$detail['uid'])->field('nickname,phone')->find();
+ // 下单用户昵称/绑定电话/头像(eb_user),供详情"用户信息"与送礼信息区展示
+ $detailUser = User::where('uid', (int)$detail['uid'])->field('nickname,phone,avatar')->find();
$detail['nickname'] = $detailUser['nickname'] ?? '';
$detail['user_phone'] = $detailUser['phone'] ?? '';
+ $detail['user_avatar'] = set_file_url((string)($detailUser['avatar'] ?? ''));
+ // 送礼订单:领取人信息(gift_uid 非 0 时),供送礼人查看"谁领取了礼物"
+ $giftUid = (int)($detail['gift_uid'] ?? $detail['extra_info']['gift']['gift_uid'] ?? ($detail['extra_info']['gift_uid'] ?? 0));
+ if ($giftUid > 0) {
+ $receiver = User::where('uid', $giftUid)->field('nickname,avatar')->find();
+ $detail['gift_receiver'] = [
+ 'uid' => $giftUid,
+ 'nickname' => (string)($receiver['nickname'] ?? ''),
+ 'avatar' => set_file_url((string)($receiver['avatar'] ?? '')),
+ ];
+ }
// 解析 JSON 字段
if (!empty($detail['custom_form'])) {
$detail['custom_form'] = $this->parseExtraInfo($detail['custom_form']);
@@ -621,20 +632,51 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
return (float)($detail['gift_price'] ?? 0) > 0 ? 1 : 0;
}
+ /**
+ * 计算订单商品原价总额(规格价 × 数量)
+ * total_price 为优惠后金额,价格明细中的“商品总额”需使用原价基数,才能与各项优惠对平
+ * @param array $detail 订单详情数据
+ * @return string 金额字符串(两位小数)
+ */
+ protected function resolveOriginalGoodsTotal(array $detail): string
+ {
+ $total = '0.00';
+ foreach (($detail['order_items'] ?? []) as $item) {
+ if (!is_array($item)) {
+ continue;
+ }
+ $price = is_array($item['attr_info'] ?? null) ? ($item['attr_info']['price'] ?? '') : '';
+ // 订单快照 item.price 可能是商品级价格,规格金额以 attr_info.price 为准
+ if ($price === '' || $price === null) {
+ $price = $item['price'] ?? '';
+ }
+ if ($price === '' || !is_numeric($price)) {
+ continue;
+ }
+ $num = max(1, (int)($item['cart_num'] ?? 1));
+ $total = bcadd($total, bcmul((string)$price, (string)$num, 2), 2);
+ }
+ if (bccomp($total, '0', 2) <= 0) {
+ $total = number_format((float)($detail['total_price'] ?? 0), 2, '.', '');
+ }
+ return $total;
+ }
+
/**
* 组装价格明细数组
* 将金额实体列转为 {label, value, type} 数组格式
* 金额字段为中台实体列(价格链规范固定);插件可通过 OrderPricingExtend 事件追加金额项
* @param array $detail 订单详情数据
- * @return array 返回 {items: [...], pay_price: string}
+ * @return array 返回 {items: [...], pay_price: string, original_total: string}
*/
protected function buildPricingItems(array $detail): array
{
$items = [];
- // 商品总额(始终存在)
+ // 商品总额按规格原价 × 数量汇总(total_price 是优惠后金额,不能作为明细基数)
+ $originalTotal = $this->resolveOriginalGoodsTotal($detail);
$items[] = [
'label' => '商品总额',
- 'value' => number_format((float)($detail['total_price'] ?? 0), 2, '.', ''),
+ 'value' => $originalTotal,
'type' => 'subtotal',
];
@@ -693,8 +735,9 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
event('OrderPricingExtend', $pricingEvent);
return [
- 'items' => $items,
- 'pay_price' => number_format((float)($detail['pay_price'] ?? 0), 2, '.', ''),
+ 'items' => $items,
+ 'pay_price' => number_format((float)($detail['pay_price'] ?? 0), 2, '.', ''),
+ 'original_total' => $originalTotal,
];
}
@@ -824,6 +867,16 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
];
}
+ // 45. 礼物信息(送礼订单):赠送人/领取人展示由查看者身份决定,在 applyGiftViewerPolicy 中填充
+ $gift = $detail['gift'] ?? null;
+ $sections[] = [
+ 'key' => 'gift',
+ 'component' => 'gift_info',
+ 'props' => ['gift' => $gift ?? []],
+ 'visible' => !empty($gift),
+ 'order' => 45,
+ ];
+
// 50. 商品卡片(始终存在)
// product_id 供前端引擎跳转商品详情(goProductDetail)使用
$product = $detail['product'] ?? [];
@@ -1083,7 +1136,8 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
$pricing = [
'items' => $pricingData['items'],
'pay_price' => $pricingData['pay_price'],
- 'total_price' => $detail['total_price'] ?? '0.00',
+ // 展示口径:商品总额按规格原价 × 数量汇总(原始 total_price 为优惠后金额,保留在详情顶层供业务计算)
+ 'total_price' => $pricingData['original_total'] ?? ($detail['total_price'] ?? '0.00'),
'pay_postage' => $detail['pay_postage'] ?? '0.00',
'refund_price' => $detail['refund_price'] ?? '0.00',
'coupon_price' => $detail['coupon_price'] ?? '0.00',
@@ -1232,6 +1286,25 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
// 原始 extra_info(供深度定制用)
$extensions['extra_info'] = $detail['extra_info'] ?? [];
+ // 送礼订单信息:赠送人 / 领取人 / 留言 / 领取时间,供详情礼物卡片按查看者展示
+ $gift = null;
+ if ($this->resolveIsGiftFlag($detail) === 1) {
+ $extraInfo = $detail['extra_info'] ?? [];
+ $giftUid = (int)($detail['gift_uid'] ?? $extraInfo['gift']['gift_uid'] ?? ($extraInfo['gift_uid'] ?? 0));
+ $gift = [
+ 'is_gift' => 1,
+ 'gift_uid' => $giftUid,
+ 'gift_mark' => (string)($extraInfo['gift_mark'] ?? ''),
+ 'gift_time' => (int)($extraInfo['gift']['gift_time'] ?? 0),
+ 'sender' => [
+ 'uid' => (int)($detail['uid'] ?? 0),
+ 'nickname' => (string)($detail['nickname'] ?? ''),
+ 'avatar' => (string)($detail['user_avatar'] ?? ''),
+ ],
+ 'receiver' => $detail['gift_receiver'] ?? null,
+ ];
+ }
+
// 返回分组结构:仅输出有值的分组,空分组(null/空数组)不下发,避免响应占位
$result = [
'status' => $status,
@@ -1240,6 +1313,9 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
'order_info' => $orderInfo,
'extensions' => $extensions,
];
+ if ($gift !== null) {
+ $result['gift'] = $gift;
+ }
if (!empty($buttons)) {
$result['buttons'] = $buttons;
}
diff --git a/backend/app/services/order/OrderCenterUserServices.php b/backend/app/services/order/OrderCenterUserServices.php
index 3e307b90b..261d595df 100644
--- a/backend/app/services/order/OrderCenterUserServices.php
+++ b/backend/app/services/order/OrderCenterUserServices.php
@@ -93,7 +93,7 @@ class OrderCenterUserServices extends OrderCenterServices
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'];
+ $visibleActions = ['confirmReceive', 'viewLogistics', 'goComment'];
$hiddenActions = [];
} else {
// 购买者:不展示收货信息与物流轨迹,隐藏确认收货/申请退款/查看物流
@@ -105,9 +105,22 @@ class OrderCenterUserServices extends OrderCenterServices
$hiddenActions = ['confirmReceive', 'applyRefund', 'viewLogistics'];
}
- // 按查看者身份过滤底部操作按钮
+ // 按查看者身份处理礼物信息与底部操作按钮
foreach ($sections as &$section) {
- if (($section['key'] ?? '') !== 'buttons') {
+ $sectionKey = (string)($section['key'] ?? '');
+ // 礼物信息:送礼人展示领取人,领取人展示赠送人
+ if ($sectionKey === 'gift') {
+ $gift = $section['props']['gift'] ?? [];
+ if ($isReceiver) {
+ $section['props']['role'] = 'receiver';
+ $section['props']['display'] = $gift['sender'] ?? null;
+ } else {
+ $section['props']['role'] = 'sender';
+ $section['props']['display'] = $gift['receiver'] ?? null;
+ }
+ continue;
+ }
+ if ($sectionKey !== 'buttons') {
continue;
}
$section['props']['buttons'] = array_values(array_filter(
@@ -216,7 +229,7 @@ class OrderCenterUserServices extends OrderCenterServices
if ($item['is_gift_receiver']) {
$item['order_buttons'] = array_values(array_filter(
$item['order_buttons'] ?? [],
- fn($button) => ($button['action'] ?? '') === 'viewLogistics'
+ fn($button) => in_array($button['action'] ?? '', ['confirmReceive', 'viewLogistics', 'goComment'], true)
));
}
}
@@ -348,7 +361,8 @@ class OrderCenterUserServices extends OrderCenterServices
}
// 订单大类文案:从 OrderTypeRegistry 动态获取,各适配器注册即生效
- if ($orderCategory) {
+ // 普通商品订单已有「实物商品」角标,不再重复展示“商品订单”占位文案
+ if ($orderCategory && $orderCategory !== 'goods') {
$categoryMap = OrderTypeRegistry::getCategoryMap();
if (isset($categoryMap[$orderCategory])) {
return $categoryMap[$orderCategory];
diff --git a/backend/modules/product/normal/handler/NormalOrderAdapter.php b/backend/modules/product/normal/handler/NormalOrderAdapter.php
index 4562c3c5d..6c3c5818a 100644
--- a/backend/modules/product/normal/handler/NormalOrderAdapter.php
+++ b/backend/modules/product/normal/handler/NormalOrderAdapter.php
@@ -192,6 +192,7 @@ class NormalOrderAdapter extends BaseOrderTypeAdapter
])
->userButtons([
Button::primary('去评价', 'goComment'),
+ Button::make('查看物流', 'viewLogistics'),
Button::make('申请售后', 'applyAfterSale'),
])
->filter([['pt_normal_order.status', '=', self::EXT_STATUS_PENDING_REVIEW]]),
@@ -202,6 +203,7 @@ class NormalOrderAdapter extends BaseOrderTypeAdapter
->label('已完成')
->adminButtons([])
->userButtons([
+ Button::make('查看物流', 'viewLogistics'),
Button::make('申请售后', 'applyAfterSale'),
Button::primary('再次购买', 'repurchase'),
])
diff --git a/frontend/uni-app/src/dynamic-pages/components/order/detail/GiftInfoCard.vue b/frontend/uni-app/src/dynamic-pages/components/order/detail/GiftInfoCard.vue
new file mode 100644
index 000000000..c4d416d90
--- /dev/null
+++ b/frontend/uni-app/src/dynamic-pages/components/order/detail/GiftInfoCard.vue
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ displayName }}
+ {{ $t('赠您一份礼物,请查收!') }}
+
+
+ {{ displayName }}
+ {{ $t('已领取您的礼物') }}
+
+
+ {{ $t('等待好友领取') }}
+
+
+
+
+ {{ $t('礼物留言') }}:
+ {{ gift.gift_mark }}
+
+
+
+
+
+
+
+
+
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 a7ed64c44..b9f3dafb0 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
@@ -6,23 +6,26 @@
-->
-
-
-
-
- {{ item.store_name || '' }}
- {{ getSkuText(item) }}
-
- ¥{{ item.true_price || item.price || '0.00' }}
- x{{ item.cart_num || 1 }}
+ {{ $t('商品信息') }}
+
+
+
+
+
+ {{ item.store_name || '' }}
+ {{ getSkuText(item) }}
+
+ ¥{{ getItemPrice(item) }}
+ x{{ item.cart_num || 1 }}
+
+
-
@@ -71,11 +74,24 @@ const productList = computed(() => {
image: product.value.image || '',
store_name: product.value.title || '',
cart_num: product.value.total_num || 0,
- true_price: ctx.value.payPrice || '0.00',
+ price: product.value.price || ctx.value.payPrice || '0.00',
product_id: p.value.product_id || 0,
}];
});
+/**
+ * 商品金额:展示规格原价(true_price 为优惠后单价,不作为商品金额展示)
+ * @param {object} item 商品项
+ * @return {string} 金额
+ */
+function getItemPrice(item) {
+ var attr = item.attr_info || {};
+ // 订单快照 item.price 可能是商品级价格,规格金额以 attr_info.price 为准
+ var val = attr.price || item.price || '0.00';
+ var num = parseFloat(val);
+ return isNaN(num) ? String(val) : num.toFixed(2);
+}
+
/**
* 提取规格文本
* @param {object} item 商品项
@@ -100,9 +116,11 @@ function goDetail(item) {