feat(shop): 优化VIP价格支持及线下付款兼容

- 结算页新增线下付款异常友好提示,避免支付配置中appid缺失错误
- 后端修复支付类型判断,防止线下付款仍试图创建微信支付订单
- 结算页默认支付方式调整为线下付款,货到付款选项注释
- 新增门店订单“修改金额”功能,支持弹窗输入新金额和修改原因
- 订单修改支持追加操作记录,接口复用updateShopOrder,无需新接口
- 重构多处页面和组件,使用响应式useVipStatus替代同步isVipMember缓存
- 新增OrderGoodsItem.price字段,下单时传VIP dealerPrice确保价格准确
- 购物车、商品详情、分类页、SkuSelector、商品卡片等支持动态VIP价格展示
- 修改购物车上下文calcPrice函数,响应VIP状态变更自动更新价格显示
- 提升前端VIP状态管理一致性,防止过期缓存导致VIP特权价格显示异常
- 验证构建通过,后端服务重启后生效线下付款修复和订单修改功能
This commit is contained in:
2026-07-15 02:45:19 +08:00
parent 747a2a1d3a
commit e47a0d3d3e
14 changed files with 556 additions and 41 deletions

View File

@@ -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` APIPUT `/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 statestate 变化触发重渲染,使所有价格显示/计算自动更新为 dealerPrice
- 验证:`npx taro build --type weapp` 构建成功

View File

@@ -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 stateisVip 变化触发重渲染
- **已接入 `useVipStatus` 的页面/组件**商品详情、结算、商城首页、分类页、购物车页、CartContext、SkuSelector、ProductCard
- `OrderGoodsItem` 接口新增 `price` 字段VIP 下单时传 dealerPrice 给后端
## 商品价格字段约定
- `product.price` - 到手价(主价格)

316
models.json Normal file
View File

@@ -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
}
]
}

View File

@@ -154,7 +154,9 @@ export async function prepayShopOrder(data: OrderPrepayRequest) {
*/
export async function createOrder(data: OrderCreateRequest) {
// Java 后端期望 camelCase 字段,直接传
const res = await request.post<ApiResult<WxPayResult>>(
// 注意:货到付款(payType=8)和线下付款(payType=9)时,后端不应创建微信支付订单,
// 理论上返回 data 为 null 或订单信息;微信支付场景才会返回 WxPayResult。
const res = await request.post<ApiResult<WxPayResult | null>>(
'/shop/shop-order',
data
);

View File

@@ -169,6 +169,8 @@ export interface OrderGoodsItem {
quantity: number;
skuId?: number;
specInfo?: string;
// 单价VIP 会员传 dealerPrice普通用户不传由后端按商品价计算
price?: string;
}
/**

View File

@@ -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<SkuSelectorProps> = ({
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<SkuSelectorProps> = ({
// 单规格商品无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<SkuSelectorProps> = ({
// - 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]) || ''

View File

@@ -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<ProductCardProps> = ({ product, onClick }) => {
// VIP 状态异步校验并更新缓存isVip 变化时触发重渲染
const { isVip } = useVipStatus()
const handleClick = () => {
if (onClick) {
onClick()
@@ -38,8 +40,8 @@ const ProductCard: React.FC<ProductCardProps> = ({ product, onClick }) => {
</Text>
<View className='flex items-end justify-between mt-2'>
<View className='flex-1'>
<Price price={isVipMember() && product.dealerPrice ? product.dealerPrice : (product.price || '0')} size='small' loginMask />
{isVipMember() && product.dealerPrice ? (
<Price price={isVip && product.dealerPrice ? product.dealerPrice : (product.price || '0')} size='small' loginMask />
{isVip && product.dealerPrice ? (
// VIP 用户显示原价划掉
<Text className='text-xs text-gray-400 line-through ml-1'>
¥{product.price}

View File

@@ -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<CartContextType | undefined>(undefined)
export const CartProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [items, setItems] = useState<CartItem[]>([])
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

View File

@@ -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<ShopGoods[]>([])
const [recommendPage, setRecommendPage] = useState(1)
const [refreshingRecommend, setRefreshingRecommend] = useState(false)
@@ -189,7 +191,7 @@ const CartPage: React.FC = () => {
)}
<View className="flex justify-between items-center">
<Text className="text-sm font-bold text-red-500">
¥{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')}
</Text>
<View className="flex items-center gap-3">
<View className="w-6 h-6 rounded bg-gray-100 flex items-center justify-center" onClick={() => updateQuantity(item.goodsId, item.skuId, item.quantity - 1)}>
@@ -238,7 +240,7 @@ const CartPage: React.FC = () => {
</Text>
<View className="flex items-center justify-between mt-1">
<Text className="text-xs font-bold text-red-500">
¥{isVipMember() && goods.dealerPrice ? goods.dealerPrice : (goods.salePrice || goods.price || '0')}
¥{isVip && goods.dealerPrice ? goods.dealerPrice : (goods.salePrice || goods.price || '0')}
</Text>
<View
className="w-5 h-5 rounded-full flex items-center justify-center"

View File

@@ -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 LoadMore from '@/components/common/LoadMore'
@@ -29,6 +29,8 @@ const SORT_TABS: { key: SortKey; label: string }[] = [
const CategoryPage: React.FC = () => {
const { addItem } = useCartContext()
// VIP 状态异步校验并更新缓存isVip 变化时触发重渲染
const { isVip } = useVipStatus()
const [categories, setCategories] = useState<ShopGoodsCategory[]>([])
const [activeId, setActiveId] = useState<number | undefined>()
@@ -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 = () => {
<Text className='text-xs text-gray-400'>/{item.unitName}</Text>
) : null}
{/* 原价划掉 */}
{isVipMember() && item.dealerPrice ? (
{isVip && item.dealerPrice ? (
<Text className='text-xs text-gray-400 line-through ml-1'>
¥{item.price}
</Text>

View File

@@ -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<ShopGoods[]>([])
// VIP 状态异步校验并更新缓存isVip 变化时触发重渲染
const { isVip } = useVipStatus()
// 支付方式相关状态
const [payType, setPayType] = useState<number>(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 => ({
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 = () => {
<View className='flex justify-between items-center mt-1'>
<Text className='text-xs text-gray-500'>x{item.quantity || item.num || 1}</Text>
<Text className='text-sm font-medium text-gray-800'>
¥{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')}
</Text>
</View>
</View>

View File

@@ -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<any[]>(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 = () => {
<Price
price={getDisplayPrice(item)}
original={
isVipMember() && item.dealerPrice
isVip && item.dealerPrice
? item.price
: item.salePrice && item.salePrice !== item.price
? item.salePrice

View File

@@ -18,7 +18,7 @@ import { useShare } from '@/hooks/useShare'
import SharePoster, { SharePosterHandle } from '@/components/SharePoster'
import { isGuest } from '@/utils/auth'
import { requireLogin } from '@/utils/login-guard'
import { isVipMember } from '@/utils/vip'
import { useVipStatus } from '@/hooks/useVipStatus'
definePageConfig({
navigationBarTitleText: '商品详情',
@@ -36,6 +36,8 @@ const ProductDetailPage: 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 = () => {
<Price price={getDisplayPrice()} size='large' color='#ee0a24' loginMask />
<Tag></Tag>
{/* VIP 会员价 */}
{!isGuest() && isVipMember() && Number(product.dealerPrice) > 0 ? (
{!isGuest() && isVip && Number(product.dealerPrice) > 0 ? (
<View className='ml-auto inline-flex items-center gap-1 bg-amber-50 rounded px-2 py-1'>
<Text className='text-xs text-amber-600 font-medium'>VIP专享</Text>
<Text className='text-sm text-amber-700 font-bold'>¥{product.dealerPrice}</Text>
@@ -304,7 +306,7 @@ const ProductDetailPage: React.FC = () => {
)}
</View>
{/* 会员价 */}
{!isGuest() && !isVipMember() && Number(product.memberStorePrice) > 0 && product.memberStorePrice !== product.price && (
{!isGuest() && !isVip && Number(product.memberStorePrice) > 0 && product.memberStorePrice !== product.price && (
<View className='mt-2 inline-block bg-orange-50 rounded px-2 py-1'>
<Text className='text-xs text-orange-500'>: ¥{product.memberStorePrice}</Text>
</View>

View File

@@ -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<ShopOrderParam>[] }[]
]
// ─── 操作类型 ─────────────────────────────────────────────────────
type OpType = 'pay' | 'complete'
type OpType = 'pay' | 'complete' | 'editPrice'
const OP_LABEL: Record<OpType, string> = {
pay: '已收款',
complete: '已完成',
editPrice: '金额',
}
const OP_DESC: Record<OpType, string> = {
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<string[]>([])
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)}
>
<Text className={`text-sm ${
act.type === 'complete' ? 'text-white' : 'text-blue-500'
act.type === 'complete' || act.type === 'editPrice' ? 'text-white' : 'text-blue-500'
}`}>{act.label}</Text>
</View>
))}
@@ -594,6 +656,70 @@ export default function StoreOrdersPage() {
</View>
</View>
)}
{/* 修改金额弹窗 */}
{showEditPriceModal && currentOrder && (
<View className='fixed inset-0 z-50 flex items-end justify-center'>
{/* 遮罩 */}
<View className='absolute inset-0 bg-black/50' onClick={closeEditPriceModal}/>
{/* 弹窗内容 */}
<View className='relative bg-white rounded-t-2xl w-full px-5 pt-6 pb-10'>
<Text className='text-lg font-medium text-gray-800 text-center mb-5 block'>
</Text>
{/* 订单信息 */}
<View className='bg-gray-50 rounded-xl p-4 mb-5'>
<Text className='text-sm text-gray-700 block'>{currentOrder.orderNo}</Text>
<Text className='text-sm text-gray-700 mt-1 block'>
<Text className='text-red-500 font-medium'>¥{currentOrder.payPrice || currentOrder.totalPrice}</Text>
</Text>
</View>
{/* 新金额输入 */}
<View className='mb-5'>
<Text className='text-sm text-gray-600 mb-2 block'></Text>
<Input
type='digit'
className='w-full px-4 py-3 rounded-xl border border-gray-200 text-lg text-gray-800'
value={editPayPrice}
onInput={(e) => setEditPayPrice(e.detail.value)}
placeholder='请输入新的实付金额'
/>
</View>
{/* 修改原因 */}
<View className='mb-6'>
<Text className='text-sm text-gray-600 mb-2 block'></Text>
<Textarea
className='w-full px-4 py-3 rounded-xl border border-gray-200 text-sm text-gray-800'
style={{minHeight: '60px'}}
value={editReason}
onInput={(e) => setEditReason(e.detail.value)}
placeholder='请输入修改原因,如:商品缺货调整、协商降价等'
maxlength={100}
/>
</View>
{/* 操作按钮 */}
<View className='flex gap-3'>
<View className='flex-1 py-3 rounded-xl bg-gray-100 text-center' onClick={closeEditPriceModal}>
<Text className='text-sm text-gray-600'></Text>
</View>
<View
className='flex-1 py-3 rounded-xl bg-orange-500 text-center flex items-center justify-center'
onClick={submitting ? undefined : submitEditPrice}
>
{submitting ? (
<Text className='text-sm text-white'>...</Text>
) : (
<Text className='text-sm text-white'></Text>
)}
</View>
</View>
</View>
</View>
)}
</View>
)
}