From e47a0d3d3e08f1a0e7b4f5d45f391ad0322b614d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 15 Jul 2026 02:45:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(shop):=20=E4=BC=98=E5=8C=96VIP=E4=BB=B7?= =?UTF-8?q?=E6=A0=BC=E6=94=AF=E6=8C=81=E5=8F=8A=E7=BA=BF=E4=B8=8B=E4=BB=98?= =?UTF-8?q?=E6=AC=BE=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 结算页新增线下付款异常友好提示,避免支付配置中appid缺失错误 - 后端修复支付类型判断,防止线下付款仍试图创建微信支付订单 - 结算页默认支付方式调整为线下付款,货到付款选项注释 - 新增门店订单“修改金额”功能,支持弹窗输入新金额和修改原因 - 订单修改支持追加操作记录,接口复用updateShopOrder,无需新接口 - 重构多处页面和组件,使用响应式useVipStatus替代同步isVipMember缓存 - 新增OrderGoodsItem.price字段,下单时传VIP dealerPrice确保价格准确 - 购物车、商品详情、分类页、SkuSelector、商品卡片等支持动态VIP价格展示 - 修改购物车上下文calcPrice函数,响应VIP状态变更自动更新价格显示 - 提升前端VIP状态管理一致性,防止过期缓存导致VIP特权价格显示异常 - 验证构建通过,后端服务重启后生效线下付款修复和订单修改功能 --- .workbuddy/memory/2026-07-15.md | 37 +- .workbuddy/memory/MEMORY.md | 5 +- models.json | 316 ++++++++++++++++++ src/api/shop/shopOrder/index.ts | 4 +- src/api/shop/shopOrder/model/index.ts | 2 + src/components/business/SkuSelector/index.tsx | 8 +- src/components/common/ProductCard/index.tsx | 8 +- src/contexts/CartContext.tsx | 6 +- src/pages/shop/cart.tsx | 8 +- src/pages/shop/category.tsx | 8 +- src/pages/shop/checkout.tsx | 39 ++- src/pages/shop/index.tsx | 8 +- src/pages/shop/product-detail.tsx | 14 +- src/pages/store/orders/index.tsx | 134 +++++++- 14 files changed, 556 insertions(+), 41 deletions(-) create mode 100644 models.json diff --git a/.workbuddy/memory/2026-07-15.md b/.workbuddy/memory/2026-07-15.md index 5ca8dd9..40a00f0 100644 --- a/.workbuddy/memory/2026-07-15.md +++ b/.workbuddy/memory/2026-07-15.md @@ -1,6 +1,41 @@ -# 2026-07-15 工作日志 +## 线下付款下单失败排查 +- 文件:`src/pages/shop/checkout.tsx`、`src/api/shop/shopOrder/index.ts` +- 现象:结算页选择线下付款(payType=9)下单时报错:`创建支付订单失败:创建微信支付订单失败:支付配置中应用ID为null或空` +- 原因:前端传参正确,但后端 `POST /shop/shop-order` 在 payType=9 时仍尝试创建微信支付订单,因未配置 appId 失败 +- 前端处理: + - `checkout.tsx` 增加线下付款支付配置类错误的友好提示 + - `createOrder` 返回类型放宽为 `WxPayResult | null`,兼容非微信支付场景 +- 后端修复(`/Users/gxwebsoft/JAVA/guilixu-java`,3 个文件): + 1. `OrderBusinessService.java` — `createOrder()` 中 Integer 比较从 `==` 改为 `.equals()`,避免装箱陷阱导致 payType=8/9 拦截失效;微信支付分支前加日志 + 2. `ShopOrderServiceImpl.java` — `createWxOrder()` 方法开头增加 payType 守卫,非微信支付(1/102)直接抛异常,不走 JSAPI 分支 + 3. `ShopOrderController.java` — `saveLegacy()` 方法增加 payType 检查,货到付款/线下付款/余额支付跳过 createWxOrder +- 验证:Maven compile 成功,无编译错误 +- 注意:后端服务需重启才能生效 ## 结算页支付方式调整 - 文件:`src/pages/shop/checkout.tsx` - 注释掉货到付款(id=8)选项,只保留线下付款(id=9) - 默认支付方式从 8(货到付款) 改为 9(线下付款) + +## 门店订单修改金额功能 +- 文件:`src/pages/store/orders/index.tsx` +- 新增 OpType `'editPrice'`,在订单卡片操作区增加橙色「修改金额」按钮 +- 仅对可操作订单(非已完成/非已关闭)显示 +- 弹窗包含:当前实付展示、新金额输入(digit)、修改原因(必填 textarea) +- 提交调用 `updateShopOrder({ orderId, payPrice, comments })`,金额变更记录追加到 comments +- 复用现有 `updateShopOrder` API(PUT `/shop/shop-order`),无需新增接口 + +## 修复 VIP 会员价格显示和下单问题 +- **根因**:`useVipStatus` hook 已定义但从未被任何页面使用,所有页面直接调用 `isVipMember()` 读取同步缓存。该缓存仅在「个人中心」页刷新,导致 VIP 会员访问商品详情/购物车/结算页时缓存为 stale false,不显示 dealerPrice +- **修复文件**(共 9 个): + 1. `src/api/shop/shopOrder/model/index.ts` — `OrderGoodsItem` 新增 `price?: string` 字段,下单时传 VIP 单价 + 2. `src/pages/shop/product-detail.tsx` — 引入 `useVipStatus`,用响应式 `isVip` 替换 `isVipMember()` + 3. `src/pages/shop/checkout.tsx` — 引入 `useVipStatus`,`goodsPrice` useMemo 依赖加 `isVip`;下单时 `OrderGoodsItem` 传 `price` 字段 + 4. `src/pages/shop/index.tsx` — 引入 `useVipStatus`,替换 `isVipMember()` + 5. `src/pages/shop/category.tsx` — 引入 `useVipStatus`,替换 `isVipMember()` + 6. `src/pages/shop/cart.tsx` — 引入 `useVipStatus`,替换 `isVipMember()` + 7. `src/contexts/CartContext.tsx` — CartProvider 引入 `useVipStatus`,`calcPrice` 用响应式 `isVip` + 8. `src/components/business/SkuSelector/index.tsx` — 引入 `useVipStatus`,替换 `isVipMember()` + 9. `src/components/common/ProductCard/index.tsx` — 引入 `useVipStatus`,替换 `isVipMember()` +- **原理**:`useVipStatus` 在组件挂载时异步调用 `checkAndCacheVipStatus` 查询后端并更新缓存+React state,state 变化触发重渲染,使所有价格显示/计算自动更新为 dealerPrice +- 验证:`npx taro build --type weapp` 构建成功 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index 6324ff4..b3f7dc7 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -18,7 +18,10 @@ - 用户在 `vip-upgrade` 页面提交门店名称和地址申请 - 门店店员在 `vip-review` 页面审核(通过/驳回) - VIP 会员特权:商品显示 `dealerPrice` 价格,下单使用该价格结算 -- VIP 状态判断:`isVipMember()` 从 localStorage 缓存读取,`checkAndCacheVipStatus(userId)` 异步更新 +- VIP 状态判断:`isVipMember()` 从 localStorage 缓存读取(同步),`checkAndCacheVipStatus(userId)` 异步更新 +- **响应式 VIP 状态**:使用 `useVipStatus` hook(`src/hooks/useVipStatus.ts`),组件挂载时异步校验并更新缓存 + React state,isVip 变化触发重渲染 +- **已接入 `useVipStatus` 的页面/组件**:商品详情、结算、商城首页、分类页、购物车页、CartContext、SkuSelector、ProductCard +- `OrderGoodsItem` 接口新增 `price` 字段,VIP 下单时传 dealerPrice 给后端 ## 商品价格字段约定 - `product.price` - 到手价(主价格) diff --git a/models.json b/models.json new file mode 100644 index 0000000..dd72e29 --- /dev/null +++ b/models.json @@ -0,0 +1,316 @@ +{ + "models": [ + { + "id": "auto", + "name": "AuTo", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 128000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "deepseek-v4-pro", + "name": "Deepseek-v4-pro", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 1000000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "deepseek-v4-flash", + "name": "Deepseek-v4-flash", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 1000000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "deepseek-v3-2-volc", + "name": "Deepseek-v3-2-volc", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 128000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "deepseek-v3-1", + "name": "Deepseek-v3-1", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 128000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "deepseek-v3-0324", + "name": "Deepseek-v3-0324", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 128000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "deepseek-r1", + "name": "Deepseek-r1", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 128000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "glm-5.2", + "name": "GLM-5.2", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 1000000, + "maxOutputTokens": 128000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "glm-5.1", + "name": "GLM-5.1", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 200000, + "maxOutputTokens": 128000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "glm-5.0", + "name": "GLM-5.0", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 200000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "glm-5.0-turbo", + "name": "GLM-5.0-Turbo", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 200000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "glm-5v-turbo", + "name": "GLM-5v-Turbo", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 200000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "glm-4.7", + "name": "GLM-4.7", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 128000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "glm-4.6", + "name": "GLM-4.6", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 128000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "minimax-m3", + "name": "Minimax-M3", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 1000000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "minimax-m2.7", + "name": "Minimax-M2.7", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 128000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "minimax-m2.5", + "name": "Minimax-M2.5", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 128000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "kimi-k2.7-code", + "name": "Kimi-K2.7-Code", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 256000, + "maxOutputTokens": 20000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "kimi-k2.6", + "name": "Kimi-K2.6", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 256000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "kimi-k2.5", + "name": "Kimi-K2.5", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 256000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "hy3-preview", + "name": "HY-3-Preview", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 200000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "hy3", + "name": "HY-3", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 256000, + "maxOutputTokens": 192000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "hunyuan-chat", + "name": "HY-Chat", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 128000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + }, + { + "id": "hunyuan-2.0-thinking", + "name": "HY-2.0-Thinking", + "vendor": "Custom", + "url": "https://api.getaigate.com/v1/chat/completions", + "apiKey": "sk-aigate-2335f1670e169eb855c88f6ed58862cb", + "maxInputTokens": 200000, + "maxOutputTokens": 32000, + "supportsToolCall": true, + "supportsImages": true, + "supportsReasoning": true, + "useCustomProtocol": false + } + ] +} \ No newline at end of file diff --git a/src/api/shop/shopOrder/index.ts b/src/api/shop/shopOrder/index.ts index 95fdd26..f01a76d 100644 --- a/src/api/shop/shopOrder/index.ts +++ b/src/api/shop/shopOrder/index.ts @@ -154,7 +154,9 @@ export async function prepayShopOrder(data: OrderPrepayRequest) { */ export async function createOrder(data: OrderCreateRequest) { // Java 后端期望 camelCase 字段,直接传 - const res = await request.post>( + // 注意:货到付款(payType=8)和线下付款(payType=9)时,后端不应创建微信支付订单, + // 理论上返回 data 为 null 或订单信息;微信支付场景才会返回 WxPayResult。 + const res = await request.post>( '/shop/shop-order', data ); diff --git a/src/api/shop/shopOrder/model/index.ts b/src/api/shop/shopOrder/model/index.ts index 06d10a8..8bf545f 100644 --- a/src/api/shop/shopOrder/model/index.ts +++ b/src/api/shop/shopOrder/model/index.ts @@ -169,6 +169,8 @@ export interface OrderGoodsItem { quantity: number; skuId?: number; specInfo?: string; + // 单价(VIP 会员传 dealerPrice,普通用户不传由后端按商品价计算) + price?: string; } /** diff --git a/src/components/business/SkuSelector/index.tsx b/src/components/business/SkuSelector/index.tsx index dccdc7d..3e04e2b 100644 --- a/src/components/business/SkuSelector/index.tsx +++ b/src/components/business/SkuSelector/index.tsx @@ -4,7 +4,7 @@ import Taro from '@tarojs/taro' import type { ShopGoods, ShopGoodsSku } from '@/api/shop/shopGoods/model' import type { ShopGoodsSpec } from '@/api/shop/shopGoodsSpec/model' import Price from '@/components/common/Price' -import { isVipMember } from '@/utils/vip' +import { useVipStatus } from '@/hooks/useVipStatus' import { getCompressedImageUrl } from '@/utils/image' interface SkuSelectorProps { @@ -49,6 +49,8 @@ const SkuSelector: React.FC = ({ const [quantity, setQuantity] = useState(1) const [showOverlay, setShowOverlay] = useState(false) const [slideUp, setSlideUp] = useState(false) + // VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染 + const { isVip } = useVipStatus() // 控制动画 useEffect(() => { @@ -157,7 +159,7 @@ const SkuSelector: React.FC = ({ // 单规格商品(无SKU列表 或 SKU列表为空数组) if ((!product?.goodsSkus || product.goodsSkus.length === 0) && product) { // VIP 会员使用 dealerPrice 作为结算价 - const vipPrice = isVipMember() && product.dealerPrice ? product.dealerPrice : undefined + const vipPrice = isVip && product.dealerPrice ? product.dealerPrice : undefined const fakeSku: ShopGoodsSku = { id: 0, goodsId: product.goodsId!, @@ -197,7 +199,7 @@ const SkuSelector: React.FC = ({ // - product.price / sku.price : 实际"到手价"(详情页主价格) // - product.dealerPrice : VIP 会员专享价 // VIP 会员优先使用 dealerPrice - const vipPrice = isVipMember() ? (product?.dealerPrice || selectedSku?.price) : null + const vipPrice = isVip ? (product?.dealerPrice || selectedSku?.price) : null const currentPrice = vipPrice || selectedSku?.price || product?.price || selectedSku?.salePrice || product?.salePrice || '0' const currentStock = selectedSku?.stock ?? product?.stock ?? 0 const currentImage = selectedSku?.image || product?.image || (product?.files?.split(',')[0]) || '' diff --git a/src/components/common/ProductCard/index.tsx b/src/components/common/ProductCard/index.tsx index 0b340ee..ba2c104 100644 --- a/src/components/common/ProductCard/index.tsx +++ b/src/components/common/ProductCard/index.tsx @@ -3,7 +3,7 @@ import { View, Text, Image } from '@tarojs/components' import Taro from '@tarojs/taro' import Price from '../Price' import { isGuest } from '@/utils/auth' -import { isVipMember } from '@/utils/vip' +import { useVipStatus } from '@/hooks/useVipStatus' import { getCompressedImageUrl } from '@/utils/image' import type { ShopGoods } from '@/api/shop/shopGoods/model' @@ -13,6 +13,8 @@ interface ProductCardProps { } const ProductCard: React.FC = ({ product, onClick }) => { + // VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染 + const { isVip } = useVipStatus() const handleClick = () => { if (onClick) { onClick() @@ -38,8 +40,8 @@ const ProductCard: React.FC = ({ product, onClick }) => { - - {isVipMember() && product.dealerPrice ? ( + + {isVip && product.dealerPrice ? ( // VIP 用户显示原价划掉 ¥{product.price} diff --git a/src/contexts/CartContext.tsx b/src/contexts/CartContext.tsx index 6f90d6c..61bf610 100644 --- a/src/contexts/CartContext.tsx +++ b/src/contexts/CartContext.tsx @@ -2,7 +2,7 @@ import React, { createContext, useContext, useState, useCallback, type ReactNode import Taro from '@tarojs/taro' import { listShopCart, addToCart, updateCartNum, removeShopCart, removeBatchShopCart, updateCartAllChecked, updateCartChecked } from '@/api/shop/shopCart' import type { ShopGoods, ShopGoodsSku } from '@/api/shop/shopGoods/model' -import { isVipMember } from '@/utils/vip' +import { useVipStatus } from '@/hooks/useVipStatus' export interface CartItem { id?: number @@ -47,6 +47,8 @@ const CartContext = createContext(undefined) export const CartProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [items, setItems] = useState([]) const [loading, setLoading] = useState(false) + // VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染使 calcPrice 重新计算 + const { isVip } = useVipStatus() // 刷新购物车数据 const refresh = useCallback(async () => { @@ -200,7 +202,7 @@ export const CartProvider: React.FC<{ children: ReactNode }> = ({ children }) => }, [items]) const calcPrice = (list: CartItem[]) => { - const vip = isVipMember() + const vip = isVip return list.reduce((sum, i) => { // VIP 会员优先使用 dealerPrice(后端关联字段 > product 嵌套对象) const dealerPrice = vip ? (i.dealerPrice || (i.product as any)?.dealerPrice) : null diff --git a/src/pages/shop/cart.tsx b/src/pages/shop/cart.tsx index 06b4035..784705a 100644 --- a/src/pages/shop/cart.tsx +++ b/src/pages/shop/cart.tsx @@ -10,7 +10,7 @@ import Loading from '@/components/common/Loading' import { isGuest } from '@/utils/auth' import { getCompressedImageUrl } from '@/utils/image' import { requireLogin } from '@/utils/login-guard' -import { isVipMember } from '@/utils/vip' +import { useVipStatus } from '@/hooks/useVipStatus' definePageConfig({ navigationBarTitleText: '购物车', @@ -19,6 +19,8 @@ definePageConfig({ const CartPage: React.FC = () => { const { items, selectedCount, selectedPrice, updateQuantity, toggleSelect, selectAll, removeItem, refresh, loading, removeSelected, addItem } = useCartContext() const { isLoggedIn } = useUserContext() + // VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染 + const { isVip } = useVipStatus() const [recommendGoods, setRecommendGoods] = useState([]) const [recommendPage, setRecommendPage] = useState(1) const [refreshingRecommend, setRefreshingRecommend] = useState(false) @@ -189,7 +191,7 @@ const CartPage: React.FC = () => { )} - ¥{isVipMember() && (item.dealerPrice || (item.product as any)?.dealerPrice) ? (item.dealerPrice || (item.product as any).dealerPrice) : (item.skuPrice || item.sku?.price || item.salePrice || item.product?.salePrice || item.product?.price || '0')} + ¥{isVip && (item.dealerPrice || (item.product as any)?.dealerPrice) ? (item.dealerPrice || (item.product as any).dealerPrice) : (item.skuPrice || item.sku?.price || item.salePrice || item.product?.salePrice || item.product?.price || '0')} updateQuantity(item.goodsId, item.skuId, item.quantity - 1)}> @@ -238,7 +240,7 @@ const CartPage: React.FC = () => { - ¥{isVipMember() && goods.dealerPrice ? goods.dealerPrice : (goods.salePrice || goods.price || '0')} + ¥{isVip && goods.dealerPrice ? goods.dealerPrice : (goods.salePrice || goods.price || '0')} { const { addItem } = useCartContext() + // VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染 + const { isVip } = useVipStatus() const [categories, setCategories] = useState([]) const [activeId, setActiveId] = useState() @@ -126,7 +128,7 @@ const CategoryPage: React.FC = () => { /** 获取显示价格 */ const getDisplayPrice = (product: ShopGoods) => { - if (isVipMember() && product.dealerPrice) return product.dealerPrice + if (isVip && product.dealerPrice) return product.dealerPrice return product.price || '0' } @@ -242,7 +244,7 @@ const CategoryPage: React.FC = () => { /{item.unitName} ) : null} {/* 原价划掉 */} - {isVipMember() && item.dealerPrice ? ( + {isVip && item.dealerPrice ? ( ¥{item.price} diff --git a/src/pages/shop/checkout.tsx b/src/pages/shop/checkout.tsx index 9d4ebd4..d2113df 100644 --- a/src/pages/shop/checkout.tsx +++ b/src/pages/shop/checkout.tsx @@ -13,7 +13,7 @@ import { listShopGoods } from '@/api/shop/shopGoods' import type { ShopGoods } from '@/api/shop/shopGoods/model' import type { ShopUserCoupon } from '@/api/shop/shopUserCoupon/model' import type { OrderGoodsItem, OrderCreateRequest } from '@/api/shop/shopOrder/model' -import { isVipMember } from '@/utils/vip' +import { useVipStatus } from '@/hooks/useVipStatus' import { getCompressedImageUrl } from '@/utils/image' // 满减门槛配置 @@ -124,6 +124,9 @@ const CheckoutPage: React.FC = () => { const [submitting, setSubmitting] = useState(false) const [rushBuyProducts, setRushBuyProducts] = useState([]) + // VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染 + const { isVip } = useVipStatus() + // 支付方式相关状态 const [payType, setPayType] = useState(9) // 9: 线下付款(默认),8: 货到付款(已注释) @@ -153,14 +156,14 @@ const CheckoutPage: React.FC = () => { // 计算金额 const goodsPrice = useMemo(() => { if (!items || items.length === 0) return 0 - const vip = isVipMember() + const vip = isVip return items.reduce((sum, item) => { // VIP 会员优先使用 dealerPrice(后端关联字段 > product 嵌套对象) const dealerPrice = vip ? (item.dealerPrice || (item.product as any)?.dealerPrice) : null const unitPrice = dealerPrice || item.skuPrice || item.sku?.price || item.salePrice || item.product?.salePrice || item.product?.price || 0 return sum + Number(unitPrice) * (item.quantity || item.num || 1) }, 0) - }, [buyNowItems, selectedItems]) + }, [buyNowItems, selectedItems, isVip]) const couponDiscount = selectedCoupon?.reducePrice ? Number(selectedCoupon.reducePrice) : 0 const totalPrice = Math.max(0, goodsPrice - couponDiscount).toFixed(2) @@ -262,12 +265,18 @@ const CheckoutPage: React.FC = () => { setSubmitting(true) try { // 构建商品列表 - const goodsItems: OrderGoodsItem[] = items.map(item => ({ - goodsId: item.goodsId, - skuId: item.skuId && item.skuId > 0 ? item.skuId : undefined, - quantity: item.quantity || item.num || 1, - specInfo: item.skuSpec || item.sku?.sku || item.product?.specName, - })) + const goodsItems: OrderGoodsItem[] = items.map(item => { + // VIP 会员优先使用 dealerPrice 作为下单单价 + const dealerPrice = isVip ? (item.dealerPrice || (item.product as any)?.dealerPrice) : null + const unitPrice = dealerPrice || undefined + return { + goodsId: item.goodsId, + skuId: item.skuId && item.skuId > 0 ? item.skuId : undefined, + quantity: item.quantity || item.num || 1, + specInfo: item.skuSpec || item.sku?.sku || item.product?.specName, + price: unitPrice ? String(unitPrice) : undefined, + } + }) // 货到付款(8):后端会自动设置 payStatus=true,无需前端传递 payStatus/payTime // 线下付款(9):后端会设置 payStatus=false,等待商家确认收款后才设为 true @@ -295,7 +304,15 @@ const CheckoutPage: React.FC = () => { Taro.switchTab({ url: '/pages/order/list' }) }, 1500) } catch (err: any) { - Taro.showToast({ title: err.message || '提交失败', icon: 'none' }) + const message = err.message || '提交失败' + // 线下付款(payType=9)下单时,后端如果仍尝试创建微信支付订单, + // 会因支付配置缺失应用ID而报错。这里给出更明确的提示。 + if (payType === 9 && /应用ID|appid|appId|支付配置|微信支付订单|创建支付订单失败/.test(message)) { + Taro.showToast({ title: '线下支付暂不可用:微信支付配置缺失', icon: 'none', duration: 3000 }) + console.error('[checkout] 线下付款下单失败,后端支付配置问题:', message) + } else { + Taro.showToast({ title: message, icon: 'none' }) + } } finally { setSubmitting(false) } @@ -367,7 +384,7 @@ const CheckoutPage: React.FC = () => { x{item.quantity || item.num || 1} - ¥{isVipMember() && (item.dealerPrice || (item.product as any)?.dealerPrice) ? (item.dealerPrice || (item.product as any).dealerPrice) : (item.skuPrice || item.sku?.salePrice || item.sku?.price || item.salePrice || item.product?.salePrice || item.product?.price || '0')} + ¥{isVip && (item.dealerPrice || (item.product as any)?.dealerPrice) ? (item.dealerPrice || (item.product as any).dealerPrice) : (item.skuPrice || item.sku?.salePrice || item.sku?.price || item.salePrice || item.product?.salePrice || item.product?.price || '0')} diff --git a/src/pages/shop/index.tsx b/src/pages/shop/index.tsx index 5e331eb..a8028ae 100644 --- a/src/pages/shop/index.tsx +++ b/src/pages/shop/index.tsx @@ -7,7 +7,7 @@ import { useCartContext } from '@/contexts/CartContext' import type { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model' import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model' import { isGuest } from '@/utils/auth' -import { isVipMember } from '@/utils/vip' +import { useVipStatus } from '@/hooks/useVipStatus' import { getCompressedImageUrl } from '@/utils/image' import { requireLogin } from '@/utils/login-guard' import { useShare } from '@/hooks/useShare' @@ -38,6 +38,8 @@ const SORT_TABS: { key: SortKey; label: string }[] = [ const ShopPage: React.FC = () => { const { addItem, totalCount } = useCartContext() + // VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染 + const { isVip } = useVipStatus() const [categoryList, setCategoryList] = useState(PRESET_CATEGORIES) const [activeCategory, setActiveCategory] = useState(0) @@ -140,7 +142,7 @@ const ShopPage: React.FC = () => { /** 获取显示价格 */ const getDisplayPrice = (product: ShopGoods) => { - if (isVipMember() && product.dealerPrice) return product.dealerPrice + if (isVip && product.dealerPrice) return product.dealerPrice return product.price || '0' } @@ -304,7 +306,7 @@ const ShopPage: React.FC = () => { { const [isFavorite, setIsFavorite] = useState(false) const { addItem, totalCount, refresh } = useCartContext() const { isLoggedIn } = useUserContext() + // VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染 + const { isVip } = useVipStatus() // 底部操作栏高度约 90px(含按钮 + 安全区),动态计算 ScrollView 可用高度 const scrollHeight = useScrollHeight(44) @@ -72,7 +74,7 @@ const ProductDetailPage: React.FC = () => { cover: product.image, title: product.name || product.goodsName || '优质商品推荐', price: - product.dealerPrice && isVipMember() ? product.dealerPrice : product.price, + product.dealerPrice && isVip ? product.dealerPrice : product.price, page: 'pages/shop/product-detail', }) .then(setPosterPath) @@ -89,7 +91,7 @@ const ProductDetailPage: React.FC = () => { // VIP 会员显示 dealerPrice,普通用户显示 price const getDisplayPrice = (): string => { - if (isVipMember() && product?.dealerPrice) { + if (isVip && product?.dealerPrice) { return product.dealerPrice } return product?.price || '0' @@ -172,7 +174,7 @@ const ProductDetailPage: React.FC = () => { } } else { // VIP 会员使用 dealerPrice 作为结算价 - const vipPrice = isVipMember() && product.dealerPrice ? product.dealerPrice : undefined + const vipPrice = isVip && product.dealerPrice ? product.dealerPrice : undefined const buyNowData = [{ goodsId: product.goodsId!, skuId: sku?.id || 0, @@ -292,7 +294,7 @@ const ProductDetailPage: React.FC = () => { 到手价 {/* VIP 会员价 */} - {!isGuest() && isVipMember() && Number(product.dealerPrice) > 0 ? ( + {!isGuest() && isVip && Number(product.dealerPrice) > 0 ? ( VIP专享 ¥{product.dealerPrice} @@ -304,7 +306,7 @@ const ProductDetailPage: React.FC = () => { )} {/* 会员价 */} - {!isGuest() && !isVipMember() && Number(product.memberStorePrice) > 0 && product.memberStorePrice !== product.price && ( + {!isGuest() && !isVip && Number(product.memberStorePrice) > 0 && product.memberStorePrice !== product.price && ( 会员价: ¥{product.memberStorePrice} diff --git a/src/pages/store/orders/index.tsx b/src/pages/store/orders/index.tsx index 6a6e9f1..27682c1 100644 --- a/src/pages/store/orders/index.tsx +++ b/src/pages/store/orders/index.tsx @@ -1,5 +1,5 @@ import React, {useState, useEffect, useCallback, useRef} from 'react' -import {View, Text, Image, ScrollView} from '@tarojs/components' +import {View, Text, Image, ScrollView, Input, Textarea} from '@tarojs/components' import Taro, {useDidShow} from '@tarojs/taro' import {pageShopOrder, updateShopOrder, removeShopOrder} from '@/api/shop/shopOrder' import type {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model' @@ -24,16 +24,18 @@ const TABS: { key: TabKey; label: string; params: Partial[] }[] ] // ─── 操作类型 ───────────────────────────────────────────────────── -type OpType = 'pay' | 'complete' +type OpType = 'pay' | 'complete' | 'editPrice' const OP_LABEL: Record = { pay: '已收款', complete: '已完成', + editPrice: '金额', } const OP_DESC: Record = { pay: '变更支付状态为已付款', complete: '同时变更支付状态为已付款、收货状态为已收货、订单状态为已完成', + editPrice: '修改订单实付金额', } // ─── 图片上传 ───────────────────────────────────────────────────── @@ -119,6 +121,8 @@ function isOrderActionable(order: ShopOrder): boolean { function getOrderActions(order: ShopOrder): { label: string; type: OpType }[] { if (!isOrderActionable(order)) return [] const actions: { label: string; type: OpType }[] = [] + // 修改金额(门店权限) + actions.push({label: '修改金额', type: 'editPrice'}) // 货到付款:未付款 / 已付款 都直接"确认完成", // 确认完成会同时设置 payStatus=true,无需单独的"确认收款"按钮 if (order.orderStatus !== 1 && order.orderStatus !== 2) { @@ -144,6 +148,11 @@ export default function StoreOrdersPage() { const [proofImages, setProofImages] = useState([]) const [submitting, setSubmitting] = useState(false) + // 修改金额弹窗状态 + const [showEditPriceModal, setShowEditPriceModal] = useState(false) + const [editPayPrice, setEditPayPrice] = useState('') + const [editReason, setEditReason] = useState('') + const pageSize = 10 const loadingRef = useRef(false) @@ -246,6 +255,57 @@ export default function StoreOrdersPage() { setProofImages([]) } + /** 打开修改金额弹窗 */ + const openEditPriceModal = (order: ShopOrder) => { + setCurrentOrder(order) + setEditPayPrice(String(order.payPrice || order.totalPrice || '')) + setEditReason('') + setShowEditPriceModal(true) + } + + /** 关闭修改金额弹窗 */ + const closeEditPriceModal = () => { + setShowEditPriceModal(false) + setCurrentOrder(null) + setEditPayPrice('') + setEditReason('') + } + + /** 提交修改金额 */ + const submitEditPrice = async () => { + if (!currentOrder) return + + const newPrice = parseFloat(editPayPrice) + if (isNaN(newPrice) || newPrice < 0) { + Taro.showToast({title: '请输入有效的金额', icon: 'none'}) + return + } + if (!editReason.trim()) { + Taro.showToast({title: '请输入修改原因', icon: 'none'}) + return + } + + setSubmitting(true) + try { + const oldPrice = currentOrder.payPrice || currentOrder.totalPrice || '0' + const changeRecord = `【门店修改金额】原实付¥${oldPrice} → 新实付¥${newPrice.toFixed(2)},原因:${editReason.trim()}` + + await updateShopOrder({ + orderId: currentOrder.orderId, + payPrice: newPrice.toFixed(2), + comments: (currentOrder.comments || '') + `\n${changeRecord}`, + } as ShopOrder) + + Taro.showToast({title: '修改成功', icon: 'success'}) + closeEditPriceModal() + loadOrders(activeTab, 1) + } catch (e: any) { + Taro.showToast({title: e.message || '修改失败', icon: 'none'}) + } finally { + setSubmitting(false) + } + } + /** 选择并上传凭证图片 */ const chooseProofImage = () => { const maxCount = 3 @@ -445,12 +505,14 @@ export default function StoreOrdersPage() { className={`px-4 py-2 rounded-lg ${ act.type === 'complete' ? 'bg-green-500' + : act.type === 'editPrice' + ? 'bg-orange-500' : 'border border-blue-500' }`} - onClick={() => openModal(order, act.type)} + onClick={() => act.type === 'editPrice' ? openEditPriceModal(order) : openModal(order, act.type)} > {act.label} ))} @@ -594,6 +656,70 @@ export default function StoreOrdersPage() { )} + + {/* 修改金额弹窗 */} + {showEditPriceModal && currentOrder && ( + + {/* 遮罩 */} + + {/* 弹窗内容 */} + + + 修改订单金额 + + + {/* 订单信息 */} + + 订单号:{currentOrder.orderNo} + + 当前实付:¥{currentOrder.payPrice || currentOrder.totalPrice} + + + + {/* 新金额输入 */} + + 新实付金额(元) + setEditPayPrice(e.detail.value)} + placeholder='请输入新的实付金额' + /> + + + {/* 修改原因 */} + + 修改原因(必填) +