diff --git a/.workbuddy/memory/2026-07-15.md b/.workbuddy/memory/2026-07-15.md index 8ecb81a..07698f6 100644 --- a/.workbuddy/memory/2026-07-15.md +++ b/.workbuddy/memory/2026-07-15.md @@ -138,6 +138,25 @@ - 引入:从 `@tarojs/components` 新增导入 `Button` - 注意:微信客服 Button 在开发者工具中无效,需真机预览测试 +## 订单详情页查看物流改造(显示 shopOrderDelivery) +- 需求:订单详情页点击「查看物流」跳转 `/pages/order/logistics`,原页面强制要求 URL 带 expressNo + expressCompany,但详情页只传了 orderId,导致 toast「缺少物流参数」+ 空状态 +- 后端修复(`/Users/gxwebsoft/JAVA/guilixu-java`,1 个文件): + - `ShopOrderServiceImpl.java` — `getByIdRel(orderId)` 原本只调 `baseMapper.selectListRel(param)`,没有像 `pageRel` 那样填充 `shopOrderDelivery`。新增私有方法 `fillShopOrderDelivery(order)`,在快递配送(deliveryType==0/null)且已发货(deliveryStatus>10)时调 `shopOrderDeliveryService.getByOrderId(orderId)` 查发货单,并关联 `shopExpressService.getById(expressId)` 取 expressName 填充 + - 编译验证:Maven compile 成功 +- 前端类型补充(`src/api/shop/shopOrder/model/index.ts`): + - 新增 `ShopOrderDelivery` 接口(与后端 ShopOrderDelivery 实体对应:deliveryId/orderId/deliveryMethod/expressId/expressName/sendName/sendPhone/sendAddress/expressNo/createTime 等) + - `ShopOrder` 接口新增 `shopOrderDelivery?: ShopOrderDelivery | null` 字段 +- 前端物流页改造(`src/pages/order/logistics.tsx`): + - 不再强制要求 URL 带 expressNo/expressCompany;只要求 orderId + - 调 `getShopOrder(orderId)` 加载订单(含后端填充的 shopOrderDelivery) + - 运单号优先级:URL expressNo → delivery.expressNo → order.expressNo + - 物流轨迹:仅在有 expressNo + expressCompany 代码时才查询 queryLogistics,失败不阻塞发货单展示 + - 新增「发货信息」卡:发货人/联系方式/发货地址/物流单号(可复制)/发货时间/发货方式(10手动/20无需物流/30电子面单) + - 新增「收货信息」卡:从订单读 realName/mobile/address + - 保留原物流轨迹时间线(仅在查询到时展示)和温馨提示 + - 空状态:订单接口也失败时才显示 +- 后端需重启服务才生效 + ## 修复 VIP 价格首页不显示 + 后端不认 price 字段 - **问题 1(首页)**:`src/pages/index/index.tsx`(tabBar 首页)没接入 `useVipStatus`,热销推荐价格一直显示 `product.price` 而非 `dealerPrice`。商品详情/商城列表/购物车/分类都修了,唯独漏了 tabBar 首页 - **问题 2(后端 DTO 缺字段)**:前端 `OrderGoodsItem` 已传 `price` 字段,但后端 `OrderCreateRequest.OrderGoodsItem`(嵌套静态类)**根本没有 `price` 字段**,Jackson 反序列化直接丢弃。后端 `OrderBusinessService.validateAndCalculateTotal()` 和 `saveOrderGoods()` 都用 `goods.getPrice()` 兜底 —— 这就是「前端传的对、后端处理就错」的根本原因 @@ -157,3 +176,14 @@ - `goods.salePrice` - 市场价(划掉的) - `goods.dealerPrice` - VIP 专享价 - 前端传 `dealerPrice` 到 `OrderGoodsItem.price`,后端通过该字段下单 + +## 修复订单列表"待付款"Tab 下状态显示"待发货" +- 文件:`src/components/common/OrderCard/index.tsx` +- 问题:用户切到"待付款"Tab,下方订单卡片状态文字显示"待发货",与 Tab 不一致 +- 根因:线下付款(payType=9)订单的 `payStatus=false`(数据库 pay_status=0),后端 `statusFilter=0` 的 SQL 条件 `pay_status=0 AND order_status=0` 把它归入"待付款"Tab 是合理的(待商家确认收款)。但 OrderCard 的 `getCardStatus` 因 `isOffline=true` → `!isOffline=false` → 跳过"待付款"分支,进入 `if (deliveryStatus === 10)` → 显示"待发货" +- 关键业务规则: + - 货到付款(8):后端 Controller `saveLegacy()` 下单时已 `setPayStatus(true)`,不会进入 statusFilter=0 + - 线下付款(9):保持 `payStatus=false`,会进入 statusFilter=0 + - 微信支付(1):未付款 `payStatus=false`,正常进入 statusFilter=0 +- 修复:`getCardStatus` 第 30 行去掉 `&& !isOffline` 条件,改为 `if (!payStatus && !isCod) return { text: '待付款' }`。保留 `!isCod`(货到付款的特殊兜底,虽然正常流程不会走到这里);清理掉不再使用的 `isOffline` 变量 +- 决策依据:用户认可"线下付款订单出现在待付款 Tab"是合理的,因此只改前端文案判断,不动后端 SQL diff --git a/src/api/shop/shopOrder/model/index.ts b/src/api/shop/shopOrder/model/index.ts index a78278f..549c358 100644 --- a/src/api/shop/shopOrder/model/index.ts +++ b/src/api/shop/shopOrder/model/index.ts @@ -1,6 +1,42 @@ import type { PageParam } from '@/api/index'; import type { ShopOrderGoods } from '@/api/shop/shopOrderGoods/model'; +/** + * 发货单(与后端 ShopOrderDelivery 对应) + */ +export interface ShopOrderDelivery { + // 发货单ID + deliveryId?: number; + // 订单ID + orderId?: number; + // 发货方式(10手动录入 20无需物流 30电子面单) + deliveryMethod?: number; + // 物流公司ID + expressId?: number; + // 物流公司名称(关联查询,非数据库字段) + expressName?: string; + // 发货人 + sendName?: string; + // 发货人联系方式 + sendPhone?: string; + // 发货地址 + sendAddress?: string; + // 物流单号 + expressNo?: string; + // 电子面单模板内容 + eorderHtml?: string; + // 排序号 + sortNumber?: number; + // 是否删除, 0否, 1是 + deleted?: number; + // 租户id + tenantId?: number; + // 创建时间 + createTime?: string; + // 修改时间 + updateTime?: string; +} + /** * 订单 */ @@ -163,6 +199,8 @@ export interface ShopOrder { hasTakeGift?: string; // 订单商品项 orderGoods?: ShopOrderGoods[]; + // 发货单信息(详情接口关联查询返回,仅快递配送且已发货时有值) + shopOrderDelivery?: ShopOrderDelivery | null; } /** diff --git a/src/components/common/OrderCard/index.tsx b/src/components/common/OrderCard/index.tsx index b093023..99acff2 100644 --- a/src/components/common/OrderCard/index.tsx +++ b/src/components/common/OrderCard/index.tsx @@ -15,7 +15,6 @@ interface OrderCardProps { const getCardStatus = (order: ShopOrder): { text: string; color: string } => { const { payStatus, deliveryStatus, orderStatus, payType } = order const isCod = payType === 8 // 货到付款 - const isOffline = payType === 9 // 线下付款 if (orderStatus === 2) return { text: '已取消', color: '#999' } if (orderStatus === 3) return { text: '取消中', color: '#ff7d00' } @@ -26,8 +25,9 @@ const getCardStatus = (order: ShopOrder): { text: string; color: string } => { // 终态:orderStatus=1 必须最先判断,盖过 deliveryStatus/payStatus if (orderStatus === 1) return { text: '已完成', color: '#0e932e' } - // 货到付款(8)/线下付款(9):无需在线支付,下单即进入发货流程 - if (!payStatus && !isCod && !isOffline) return { text: '待付款', color: '#ee0a24' } + // 货到付款(8):下单时后端已 setPayStatus(true),不会走到这里 + // 线下付款(9):保持 payStatus=false(待商家确认收款),应显示"待付款"与 Tab 一致 + if (!payStatus && !isCod) return { text: '待付款', color: '#ee0a24' } if (deliveryStatus === 10) return { text: '待发货', color: '#4b9cf5' } if (deliveryStatus === 20) return { text: '待收货', color: '#4b9cf5' } if (deliveryStatus === 30) return { text: '已收货', color: '#0e932e' } diff --git a/src/pages/order/logistics.tsx b/src/pages/order/logistics.tsx index d1334cb..880641f 100644 --- a/src/pages/order/logistics.tsx +++ b/src/pages/order/logistics.tsx @@ -2,8 +2,11 @@ import React, { useState, useEffect } from 'react' import { View, Text, ScrollView } from '@tarojs/components' import Taro from '@tarojs/taro' import { Button } from '@nutui/nutui-react-taro' -import { queryLogistics, formatLogisticsStatus, EXPRESS_COMPANIES } from '@/api/shop/shopLogistics' +import { queryLogistics, formatLogisticsStatus } from '@/api/shop/shopLogistics' import type { LogisticsInfo, LogisticsTrack } from '@/api/shop/shopLogistics' +import { getShopOrder } from '@/api/shop/shopOrder' +import type { ShopOrder, ShopOrderDelivery } from '@/api/shop/shopOrder/model' +import { OrderStatus } from '@/types/order' import Loading from '@/components/common/Loading' import EmptyState from '@/components/common/EmptyState' @@ -11,9 +14,18 @@ definePageConfig({ navigationBarTitleText: '物流详情', }) +// 发货方式映射 +const DELIVERY_METHOD_MAP: Record = { + 10: '手动录入', + 20: '无需物流', + 30: '电子面单', +} + const LogisticsPage: React.FC = () => { - const { orderId, expressNo, expressCompany } = Taro.getCurrentInstance().router?.params || {} + const { orderId, expressNo: urlExpressNo, expressCompany: urlExpressCompany } = Taro.getCurrentInstance().router?.params || {} const [loading, setLoading] = useState(true) + const [order, setOrder] = useState(null) + const [delivery, setDelivery] = useState(null) const [logisticsInfo, setLogisticsInfo] = useState(null) const [trackList, setTrackList] = useState([]) @@ -22,27 +34,42 @@ const LogisticsPage: React.FC = () => { }, []) const loadLogistics = async () => { + if (!orderId) { + Taro.showToast({ title: '缺少订单参数', icon: 'none' }) + setLoading(false) + return + } setLoading(true) try { - // 必须传入参数 - if (!expressNo || !expressCompany) { - Taro.showToast({ title: '缺少物流参数', icon: 'none' }) - setLoading(false) - return + // 1) 拉取订单详情(含 shopOrderDelivery,由后端 getByIdRel 填充) + let orderData: ShopOrder | null = null + try { + orderData = await getShopOrder(Number(orderId)) + } catch (err) { + console.error('加载订单信息失败', err) + } + if (orderData) { + setOrder(orderData) + setDelivery(orderData.shopOrderDelivery ?? null) } - const res = await queryLogistics({ - orderId, - expressNo, - expressCompany, - }) - if (res?.data) { - setLogisticsInfo(res.data.logisticsInfo) - setTrackList(res.data.trackList) + // 2) 决定运单号 / 物流公司:优先 URL 参数,其次订单的 shopOrderDelivery + const expressNo = urlExpressNo || orderData?.shopOrderDelivery?.expressNo || orderData?.expressNo || '' + // expressCompany 通常是快递公司代码(SF/YTO 等);shopOrderDelivery.expressName 是中文名 + const expressCompany = urlExpressCompany || '' + + // 3) 如果有运单号 + 物流公司代码,尝试查询物流轨迹(失败不阻塞发货单展示) + if (expressNo && expressCompany) { + try { + const res = await queryLogistics({ orderId, expressNo, expressCompany }) + if (res?.data) { + setLogisticsInfo(res.data.logisticsInfo || null) + setTrackList(res.data.trackList || []) + } + } catch (err) { + console.error('查询物流轨迹失败', err) + } } - } catch (err) { - console.error('加载物流信息失败', err) - Taro.showToast({ title: '加载失败', icon: 'none' }) } finally { setLoading(false) } @@ -50,49 +77,35 @@ const LogisticsPage: React.FC = () => { // 复制单号 const copyExpressNo = () => { - if (logisticsInfo?.expressNo) { - Taro.setClipboardData({ - data: logisticsInfo.expressNo, - success: () => { - Taro.showToast({ title: '单号已复制', icon: 'success' }) - }, - }) - } + const no = logisticsInfo?.expressNo || delivery?.expressNo || order?.expressNo || '' + if (!no) return + Taro.setClipboardData({ + data: no, + success: () => Taro.showToast({ title: '单号已复制', icon: 'success' }), + }) } - // 联系快递员(模拟) - const contactRider = () => { + // 联系客服 + const contactService = () => { Taro.makePhoneCall({ phoneNumber: '400-811-1111', - fail: () => { - Taro.showToast({ title: '拨打失败', icon: 'none' }) - }, + fail: () => Taro.showToast({ title: '拨打失败', icon: 'none' }), }) } // 格式化时间 - const formatTime = (time: string) => { - if (!time) return '' - const date = new Date(time) - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - return `${month}-${day} ${hours}:${minutes}` - } - - // 格式化日期 - const formatDate = (time: string) => { - if (!time) return '' - const date = new Date(time) - return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` + const formatTime = (time?: string) => { + if (!time) return '-' + return time.replace('T', ' ').slice(0, 19) } if (loading) { return } - if (!logisticsInfo) { + // 既无物流轨迹,又无发货单,又无订单 → 空状态 + const hasAnything = logisticsInfo || trackList?.length || delivery || order + if (!hasAnything) { return ( @@ -100,7 +113,30 @@ const LogisticsPage: React.FC = () => { ) } - const statusInfo = formatLogisticsStatus(logisticsInfo.status) + const statusInfo = logisticsInfo ? formatLogisticsStatus(logisticsInfo.status) : null + + // 发货状态文案:优先用 logisticsInfo.status,否则按订单 deliveryStatus 推断 + const getDeliveryStatusText = (): string => { + if (logisticsInfo?.status) return logisticsInfo.status + const ds = order?.deliveryStatus + if (ds === 30) return '部分发货' + if (ds === 20) return '已发货' + if (ds === 10) return '未发货' + if (order?.orderStatus === OrderStatus.Completed) return '已签收' + return '已发货' + } + + // 物流公司名:优先 logisticsInfo,其次 delivery.expressName + const expressCompanyName = logisticsInfo?.expressCompanyName || delivery?.expressName || '未知物流' + // 运单号:优先 logisticsInfo,其次 delivery,最后 order + const expressNo = logisticsInfo?.expressNo || delivery?.expressNo || order?.expressNo || '' + // 发货人信息 + const sendName = delivery?.sendName || order?.expressMerchantName || '-' + const sendPhone = delivery?.sendPhone || '-' + const sendAddress = delivery?.sendAddress || '-' + const deliveryMethodText = delivery?.deliveryMethod != null + ? DELIVERY_METHOD_MAP[delivery.deliveryMethod] || '-' + : (expressNo ? '手动录入' : '无需物流') return ( @@ -108,109 +144,147 @@ const LogisticsPage: React.FC = () => { {/* 快递信息卡片 */} - {/* 快递logo */} 📦 - {logisticsInfo.expressCompanyName} + {expressCompanyName} - {statusInfo.text} + {getDeliveryStatusText()} - 运单号: {logisticsInfo.expressNo} + 运单号: {expressNo || '暂无'} - - 复制 - + {expressNo ? ( + + 复制 + + ) : null} - {/* 预计送达 */} - {logisticsInfo.estimatedTime && ( - - - 预计送达: {formatDate(logisticsInfo.estimatedTime)} 24:00 前 - - - )} - - - {/* 当前状态 */} - - - - {statusInfo.icon} - - - {logisticsInfo.status} - {logisticsInfo.currentLocation && ( - - 当前位于: {logisticsInfo.currentLocation} - - )} - - 更新时间: {formatTime(logisticsInfo.updateTime)} - - + {/* 发货方式 */} + + 发货方式 + {deliveryMethodText} - {/* 收货人信息 */} - {logisticsInfo.receiverInfo && ( + {/* 发货单信息(shopOrderDelivery) */} + {delivery ? ( + + 发货信息 + + + 发货人 + {sendName} + + + 联系方式 + {sendPhone} + + + 发货地址 + {sendAddress} + + + 物流单号 + + {delivery.expressNo || '-'} + {delivery.expressNo ? ( + 复制 + ) : null} + + + {delivery.createTime ? ( + + 发货时间 + {formatTime(delivery.createTime)} + + ) : null} + + + ) : null} + + {/* 收货人信息(从订单读取) */} + {order ? ( 收货信息 - {logisticsInfo.receiverInfo.name} - {logisticsInfo.receiverInfo.phone} + {order.realName || '-'} + {order.mobile || order.phone || '-'} - {logisticsInfo.receiverInfo.address} + {order.address || '-'} - )} + ) : null} - {/* 物流轨迹 */} - - 物流详情 - - {/* 竖线 */} - + {/* 物流轨迹(仅在查询到时展示) */} + {trackList && trackList.length > 0 ? ( + + 物流详情 + + {/* 竖线 */} + - {trackList.map((item, idx) => ( - - {/* 圆点 */} - - {item.isCompleted && idx !== 0 ? ( - - ) : !item.isCompleted ? ( - - ) : null} - - - {/* 内容 */} - - - - {item.status} - - {formatTime(item.time)} + {trackList.map((item, idx) => ( + + {/* 圆点 */} + + {item.isCompleted && idx !== 0 ? ( + + ) : !item.isCompleted ? ( + + ) : null} + + + {/* 内容 */} + + + + {item.status} + + {formatTime(item.time)} + + + {item.description} + + {item.location} - - {item.description} - - {item.location} - - ))} + ))} + - + ) : null} + + {/* 当前状态(仅在查询到 logisticsInfo 时展示) */} + {logisticsInfo ? ( + + + + {statusInfo?.icon} + + + {logisticsInfo.status} + {logisticsInfo.currentLocation ? ( + + 当前位于: {logisticsInfo.currentLocation} + + ) : null} + + 更新时间: {formatTime(logisticsInfo.updateTime)} + + + + + ) : null} {/* 温馨提示 */} @@ -229,7 +303,7 @@ const LogisticsPage: React.FC = () => {