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 @@ + + + + + + + + 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 @@ --> @@ -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) {