fix(shop-cart): 修复购物车后端查询及前后端字段名对齐问题
- 修复后端ShopCartMapper SQL,关联查询商品和SKU信息,返回完整商品详情字段 - 新增ShopCart实体非持久化字段,映射商品名称、图片、价格、库存等 - 调整后端Controller列表方法,自动过滤当前登录用户购物车数据 - 前端ShopCart接口新增salePrice、dealerPrice字段,CartContext映射新增字段 - 修正购物车数量和选中状态字段名,前端与后端统一使用cartNum和selected - 统一更新购物车数量、选中状态的接口调用参数,避免因字段不匹配导致更新失败 - 购物车及结算页价格显示逻辑优先使用后端扁平dealerPrice字段,兼容VIP价格显示 - 各购物车相关页面调整添加购物车参数,确保字段名一致和正确传递
This commit is contained in:
@@ -55,3 +55,38 @@ adapter `/list` 接口忽略分页,一次返回全部收藏;前端 `listShop
|
|||||||
1. `updateCartNum`:`PUT /shop/shop-cart/{id}` + `{quantity}` → `PUT /shop/shop-cart` + `{id, num}`
|
1. `updateCartNum`:`PUT /shop/shop-cart/{id}` + `{quantity}` → `PUT /shop/shop-cart` + `{id, num}`
|
||||||
2. `updateCartChecked`:`PUT /shop/shop-cart/{id}` + `{selected}` → `PUT /shop/shop-cart` + `{id, checked}`(同时修正字段名 `selected`→`checked` 与 model 一致)
|
2. `updateCartChecked`:`PUT /shop/shop-cart/{id}` + `{selected}` → `PUT /shop/shop-cart` + `{id, checked}`(同时修正字段名 `selected`→`checked` 与 model 一致)
|
||||||
3. `updateCartAllChecked`:`PUT /shop/shop-cart/selected` + `{selected}` → `POST /shop/shop-cart/selected` + `{checked}`(改用 POST,自定义批量操作更可能支持 POST;CartContext 已有静默 catch 兜底)
|
3. `updateCartAllChecked`:`PUT /shop/shop-cart/selected` + `{selected}` → `POST /shop/shop-cart/selected` + `{checked}`(改用 POST,自定义批量操作更可能支持 POST;CartContext 已有静默 catch 兜底)
|
||||||
|
|
||||||
|
## 购物车后端 SQL 关联查询修复(2026-07-14)
|
||||||
|
|
||||||
|
### 问题
|
||||||
|
`GET /api/shop/shop-cart` 返回的数据没有商品图片、价格等信息。前端 `CartContext.refresh()` 已映射 `goodsName/goodsImage/skuPrice/skuSpec/stock` 等字段,但后端根本没返回这些字段。
|
||||||
|
|
||||||
|
### 根因
|
||||||
|
后端 `ShopCartMapper.xml` 的 SQL 只做 `SELECT a.* FROM shop_cart a`,没有 JOIN 商品表和 SKU 表。
|
||||||
|
|
||||||
|
### 后端修改(`guilixu-java` 项目)
|
||||||
|
1. **`ShopCart.java`**:新增 7 个 `@TableField(exist = false)` 非持久化字段:`goodsName`、`goodsImage`、`skuPrice`、`skuSpec`、`stock`、`salePrice`、`dealerPrice`;补充 `TableField` import
|
||||||
|
2. **`ShopCartMapper.xml`**:SQL 改为 `LEFT JOIN shop_goods g ON a.goods_id = g.goods_id` + `LEFT JOIN shop_goods_sku s ON a.sku_id = s.id`,SELECT 别名映射到新字段(`COALESCE(s.price, g.price) AS sku_price`、`COALESCE(s.stock, g.stock) AS stock`);同时修复 `userId/goodsId/merchantId` 从 `LIKE` 改为 `=`(原 LIKE 会导致 userId=1 匹配到 10/11/21 等)
|
||||||
|
3. **`ShopCartController.java`**:`list()` 方法自动按当前登录用户过滤(`param.setUserId(loginUser.getUserId().longValue())`)
|
||||||
|
|
||||||
|
### 前端修改
|
||||||
|
4. **`src/api/shop/shopCart/model/index.ts`**:`ShopCart` 接口新增 `salePrice`、`dealerPrice` 字段
|
||||||
|
5. **`src/contexts/CartContext.tsx`**:`CartItem` 新增 `salePrice`、`dealerPrice`;`refresh()` 映射这两个字段;`calcPrice()` VIP 优先用扁平 `i.dealerPrice`(之前只看 `i.product?.dealerPrice`)
|
||||||
|
6. **`src/pages/shop/cart.tsx`** + **`src/pages/shop/checkout.tsx`**:价格显示逻辑同步更新,优先使用扁平 `item.dealerPrice` / `item.salePrice`
|
||||||
|
|
||||||
|
## 购物车前后端字段名对齐修复(2026-07-14)
|
||||||
|
|
||||||
|
### 问题
|
||||||
|
修改购物车数量时后端报 `UPDATE shop_cart WHERE id=?`(SET 子句为空),MyBatis-Plus 找不到要更新的字段。
|
||||||
|
|
||||||
|
### 根因
|
||||||
|
前后端字段名不匹配:
|
||||||
|
- 前端发送 `num`,后端实体字段是 `cartNum` → Jackson 反序列化时 `cartNum` 为 null → 空 UPDATE
|
||||||
|
- 前端发送 `checked`,后端实体字段是 `selected` → 同样不匹配
|
||||||
|
- 列表返回 `cartNum`,前端读 `item.num` → 数量永远为默认值 1
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
1. **`src/api/shop/shopCart/model/index.ts`**:`ShopCart.num` → `cartNum`;新增 `selected` 字段;`AddToCartParam.num` → `cartNum`;`UpdateCartNumParam.num` → `cartNum`
|
||||||
|
2. **`src/api/shop/shopCart/index.ts`**:`updateCartNum` 发 `{id, cartNum}`;`updateCartChecked` 发 `{id, selected}`
|
||||||
|
3. **`src/contexts/CartContext.tsx`**:`refresh()` 读 `item.cartNum` 和 `item.selected`;`addItem()` 发 `cartNum`;`updateQuantity` 调用 `{id, cartNum}`
|
||||||
|
4. **`src/pages/shop/index.tsx`** + **`src/pages/shop/category.tsx`**:`addToCart` 参数 `num` → `cartNum`
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export async function addToCart(data: AddToCartParam) {
|
|||||||
export async function updateCartNum(data: UpdateCartNumParam) {
|
export async function updateCartNum(data: UpdateCartNumParam) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
'/shop/shop-cart',
|
'/shop/shop-cart',
|
||||||
{ id: data.id, num: data.num }
|
{ id: data.id, cartNum: data.cartNum }
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
return res.message || '更新成功';
|
return res.message || '更新成功';
|
||||||
@@ -77,7 +77,7 @@ export async function updateCartNum(data: UpdateCartNumParam) {
|
|||||||
export async function updateCartChecked(id: number, checked: boolean) {
|
export async function updateCartChecked(id: number, checked: boolean) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
'/shop/shop-cart',
|
'/shop/shop-cart',
|
||||||
{ id, checked }
|
{ id, selected: checked }
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
return res.message || '更新成功';
|
return res.message || '更新成功';
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export interface ShopCart {
|
|||||||
goodsId?: number;
|
goodsId?: number;
|
||||||
// SKU ID
|
// SKU ID
|
||||||
skuId?: number;
|
skuId?: number;
|
||||||
// 数量
|
// 数量(后端字段名 cartNum)
|
||||||
num?: number;
|
cartNum?: number;
|
||||||
// 商品名称
|
// 商品名称
|
||||||
goodsName?: string;
|
goodsName?: string;
|
||||||
// 商品图片
|
// 商品图片
|
||||||
@@ -24,6 +24,12 @@ export interface ShopCart {
|
|||||||
skuSpec?: string;
|
skuSpec?: string;
|
||||||
// 库存
|
// 库存
|
||||||
stock?: number;
|
stock?: number;
|
||||||
|
// 市场价(划线价)
|
||||||
|
salePrice?: string | number;
|
||||||
|
// 经销商价(VIP专享)
|
||||||
|
dealerPrice?: string | number;
|
||||||
|
// 是否选中(后端字段名 selected)
|
||||||
|
selected?: boolean;
|
||||||
// 租户id
|
// 租户id
|
||||||
tenantId?: number;
|
tenantId?: number;
|
||||||
// 创建时间
|
// 创建时间
|
||||||
@@ -52,8 +58,8 @@ export interface AddToCartParam {
|
|||||||
goodsId: number;
|
goodsId: number;
|
||||||
// SKU ID(单规格可不传)
|
// SKU ID(单规格可不传)
|
||||||
skuId?: number;
|
skuId?: number;
|
||||||
// 数量
|
// 数量(后端字段名 cartNum)
|
||||||
num: number;
|
cartNum: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,8 +68,8 @@ export interface AddToCartParam {
|
|||||||
export interface UpdateCartNumParam {
|
export interface UpdateCartNumParam {
|
||||||
// 购物车ID
|
// 购物车ID
|
||||||
id: number;
|
id: number;
|
||||||
// 数量
|
// 数量(后端字段名 cartNum)
|
||||||
num: number;
|
cartNum: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export interface CartItem {
|
|||||||
skuPrice?: string
|
skuPrice?: string
|
||||||
skuSpec?: string
|
skuSpec?: string
|
||||||
stock?: number
|
stock?: number
|
||||||
|
salePrice?: string | number
|
||||||
|
dealerPrice?: string | number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CartContextType {
|
interface CartContextType {
|
||||||
@@ -53,21 +55,20 @@ export const CartProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
const data = await listShopCart()
|
const data = await listShopCart()
|
||||||
// 防御性检查:确保返回的是数组
|
// 防御性检查:确保返回的是数组
|
||||||
const safeData = Array.isArray(data) ? data : []
|
const safeData = Array.isArray(data) ? data : []
|
||||||
// 转换为前端格式,默认都选中
|
// 转换为前端格式
|
||||||
const mappedItems: CartItem[] = safeData.map(item => ({
|
const mappedItems: CartItem[] = safeData.map(item => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
goodsId: item.goodsId!,
|
goodsId: item.goodsId!,
|
||||||
skuId: item.skuId,
|
skuId: item.skuId,
|
||||||
quantity: item.num || 1,
|
quantity: item.cartNum || 1,
|
||||||
goodsName: item.goodsName,
|
goodsName: item.goodsName,
|
||||||
goodsImage: item.goodsImage,
|
goodsImage: item.goodsImage,
|
||||||
skuPrice: item.skuPrice,
|
skuPrice: item.skuPrice,
|
||||||
skuSpec: item.skuSpec,
|
skuSpec: item.skuSpec,
|
||||||
stock: item.stock,
|
stock: item.stock,
|
||||||
// 后端可能返回嵌套的 product/sku 对象(对应 ShopCartItem 结构)
|
salePrice: item.salePrice,
|
||||||
product: (item as any).product,
|
dealerPrice: item.dealerPrice,
|
||||||
sku: (item as any).sku,
|
checked: item.selected !== false,
|
||||||
checked: true,
|
|
||||||
}))
|
}))
|
||||||
setItems(mappedItems)
|
setItems(mappedItems)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -83,7 +84,7 @@ export const CartProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
goodsId: product.goodsId!,
|
goodsId: product.goodsId!,
|
||||||
num: quantity,
|
cartNum: quantity,
|
||||||
} as Record<string, unknown>
|
} as Record<string, unknown>
|
||||||
// 只有多规格商品才传 skuId,且不能为 0
|
// 只有多规格商品才传 skuId,且不能为 0
|
||||||
if (sku?.id && sku.id > 0) {
|
if (sku?.id && sku.id > 0) {
|
||||||
@@ -125,7 +126,7 @@ export const CartProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
if (!item?.id) return
|
if (!item?.id) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateCartNum({ id: item.id, num: quantity })
|
await updateCartNum({ id: item.id, cartNum: quantity })
|
||||||
// 本地更新
|
// 本地更新
|
||||||
setItems(prev => prev.map(i =>
|
setItems(prev => prev.map(i =>
|
||||||
(i.goodsId === goodsId && i.skuId === skuId) ? { ...i, quantity } : i
|
(i.goodsId === goodsId && i.skuId === skuId) ? { ...i, quantity } : i
|
||||||
@@ -201,8 +202,8 @@ export const CartProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
const calcPrice = (list: CartItem[]) => {
|
const calcPrice = (list: CartItem[]) => {
|
||||||
const vip = isVipMember()
|
const vip = isVipMember()
|
||||||
return list.reduce((sum, i) => {
|
return list.reduce((sum, i) => {
|
||||||
// VIP 会员优先使用 dealerPrice
|
// VIP 会员优先使用 dealerPrice(后端关联字段 > product 嵌套对象)
|
||||||
const dealerPrice = vip ? (i.product as any)?.dealerPrice : null
|
const dealerPrice = vip ? (i.dealerPrice || (i.product as any)?.dealerPrice) : null
|
||||||
const unitPrice = dealerPrice || i.skuPrice || i.sku?.price || i.product?.salePrice || i.product?.price || 0
|
const unitPrice = dealerPrice || i.skuPrice || i.sku?.price || i.product?.salePrice || i.product?.price || 0
|
||||||
return sum + Number(unitPrice) * i.quantity
|
return sum + Number(unitPrice) * i.quantity
|
||||||
}, 0).toFixed(2)
|
}, 0).toFixed(2)
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ const CartPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
<View className="flex justify-between items-center">
|
<View className="flex justify-between items-center">
|
||||||
<Text className="text-sm font-bold text-red-500">
|
<Text className="text-sm font-bold text-red-500">
|
||||||
¥{isVipMember() && (item.product as any)?.dealerPrice ? (item.product as any).dealerPrice : (item.skuPrice || item.sku?.price || item.product?.salePrice || item.product?.price || '0')}
|
¥{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')}
|
||||||
</Text>
|
</Text>
|
||||||
<View className="flex items-center gap-3">
|
<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)}>
|
<View className="w-6 h-6 rounded bg-gray-100 flex items-center justify-center" onClick={() => updateQuantity(item.goodsId, item.skuId, item.quantity - 1)}>
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const CategoryPage: React.FC = () => {
|
|||||||
if (!requireLogin({ action: 'addToCart' })) return
|
if (!requireLogin({ action: 'addToCart' })) return
|
||||||
addToCart({
|
addToCart({
|
||||||
goodsId: product.goodsId!,
|
goodsId: product.goodsId!,
|
||||||
num: product.step || 1,
|
cartNum: product.step || 1,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
Taro.showToast({ title: '已加入购物车', icon: 'success' })
|
Taro.showToast({ title: '已加入购物车', icon: 'success' })
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
|||||||
@@ -149,9 +149,9 @@ const CheckoutPage: React.FC = () => {
|
|||||||
if (!items || items.length === 0) return 0
|
if (!items || items.length === 0) return 0
|
||||||
const vip = isVipMember()
|
const vip = isVipMember()
|
||||||
return items.reduce((sum, item) => {
|
return items.reduce((sum, item) => {
|
||||||
// VIP 会员优先使用 dealerPrice
|
// VIP 会员优先使用 dealerPrice(后端关联字段 > product 嵌套对象)
|
||||||
const dealerPrice = vip ? (item.product as any)?.dealerPrice : null
|
const dealerPrice = vip ? (item.dealerPrice || (item.product as any)?.dealerPrice) : null
|
||||||
const unitPrice = dealerPrice || item.skuPrice || item.sku?.price || item.product?.salePrice || item.product?.price || 0
|
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)
|
return sum + Number(unitPrice) * (item.quantity || item.num || 1)
|
||||||
}, 0)
|
}, 0)
|
||||||
}, [buyNowItems, selectedItems])
|
}, [buyNowItems, selectedItems])
|
||||||
@@ -368,7 +368,7 @@ const CheckoutPage: React.FC = () => {
|
|||||||
<View className='flex justify-between items-center mt-1'>
|
<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-xs text-gray-500'>x{item.quantity || item.num || 1}</Text>
|
||||||
<Text className='text-sm font-medium text-gray-800'>
|
<Text className='text-sm font-medium text-gray-800'>
|
||||||
¥{isVipMember() && (item.product as any)?.dealerPrice ? (item.product as any).dealerPrice : (item.skuPrice || item.sku?.salePrice || item.sku?.price || item.product?.salePrice || item.product?.price || '0')}
|
¥{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')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ const ShopPage: React.FC = () => {
|
|||||||
if (!requireLogin({ action: 'addToCart' })) return
|
if (!requireLogin({ action: 'addToCart' })) return
|
||||||
addToCart({
|
addToCart({
|
||||||
goodsId: product.goodsId!,
|
goodsId: product.goodsId!,
|
||||||
num: product.step || 1,
|
cartNum: product.step || 1,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
Taro.showToast({ title: '已加入购物车', icon: 'success' })
|
Taro.showToast({ title: '已加入购物车', icon: 'success' })
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
|||||||
Reference in New Issue
Block a user