name: 修复购物车秒杀商品合并 overview: 修复 mallApp 购物车/结算页把同规格「秒杀花束」与「普通花束」误合并成同一商品的问题,根因是商品去重/合并的 key 生成函数缺少活动类型字段。 todos:
mallApp 购物车与结算的“同一商品”判定分为两套逻辑,二者不一致:
加购/删除定位用的是 _selectRowMatches(花束分支已考虑 activityType,能把秒杀行和普通行分开):
_selectRowMatches(element, item) {
if (!element || !item) return false;
const eleProperty = this.getItemProperty(element);
const itemProperty = this.getItemProperty(item);
if (eleProperty !== itemProperty) return false;
if (eleProperty === 0) {
// 秒杀与普通购买同商品同规格也必须分行,避免不同计价规则的购物车行被合并
if ((element.activityType || '') !== (item.activityType || '')) return false;
return String(element.id) === String(item.id)
&& String(element.specGoodsId || 0) === String(item.specGoodsId || 0);
}
...
}
勾选态 key / 合并去重 key 用的是 getSelectRowKey(花束分支没有带 activityType,是本次 bug 的根因):
getSelectRowKey(item) {
if (!item || item.id === undefined || item.id === null) return '';
const property = this.getItemProperty(item);
if (property === 0) {
return `0_${item.id}_${item.specGoodsId || 0}`;
}
if (this._shouldMergeSelectByProductId()) {
return `1_${item.id}`;
}
return `1_${item.id}_${item.classId || 0}`;
},
getSelectRowKey 被两处消费:
mallApp/src/mixins/cgProduct.js 中 _mergeDuplicateSelectRows(第 273-296 行),当 pageType === 'cg'(mallApp 购物车/结算页均为 cg)时会在 _syncMergedSelectListIfNeeded(298-306 行)里被调用,合并 Vuex 中的 selectList。mallApp/src/pages/billing/affirmMix.vue 的 onShow(第 585-604 行左右),在拿到 selectList 后再次用同一个 key 做本地合并展示:
onShow() {
this.syncShopContext();
this._syncMergedSelectListIfNeeded();
const selectList = this.$util.copyObject(this.selectList);
const mergedMap = {};
const list = [];
for (let i = 0; i < selectList.length; i++) {
const item = selectList[i];
const key = this.getSelectRowKey(item) || String(item.id);
if (mergedMap[key]) {
mergedMap[key].bigCount = Number(mergedMap[key].bigCount || 0) + Number(item.bigCount || 0);
mergedMap[key].smallCount = Number(mergedMap[key].smallCount || 0) + Number(item.smallCount || 0);
} else {
mergedMap[key] = this.$util.copyObject(item);
list.push(mergedMap[key]);
}
}
this.list = list;
...
因为秒杀行(specGoodsId=2120, activityType='seckill', price=3)和普通行(specGoodsId=2120, activityType='', price=16)算出的 getSelectRowKey 都是 0_{goodsId}_2120,两行被合并成一行:数量相加,但价格/activityType 只保留了先出现的那一行,导致最终提交给后端 POST /order/create-mix-order 的 product 数组里少了一行、且这一行的活动标记可能是错的。
后端 app-mall/controllers/OrderController.php 的 actionCreateMixOrder(958 行起)是按 product 数组逐行独立处理和核价的(1110-1256 行),并不会对 goodsId+specGoodsId 做跨行去重,秒杀单独通过 activityType === 'seckill' 判断走 Redis 核价(1198-1239 行)。也就是说后端本来就支持同一规格同时存在秒杀行和普通行,问题完全在前端合并 key 缺字段。
只需让 getSelectRowKey 的花束分支和 _selectRowMatches 保持一致,把 activityType 纳入 key:
文件:front-end/mallApp/src/mixins/cgProduct.js
getSelectRowKey(item) {
if (!item || item.id === undefined || item.id === null) return '';
const property = this.getItemProperty(item);
if (property === 0) {
return `0_${item.id}_${item.specGoodsId || 0}_${item.activityType || ''}`;
}
if (this._shouldMergeSelectByProductId()) {
return `1_${item.id}`;
}
return `1_${item.id}_${item.classId || 0}`;
},
这一处改动会自动修复三个消费点:
mallApp/src/pages/home/cart.vue 第 371-373 行 getItemKey → checkedMap 勾选态 key,不再把秒杀行和普通行的勾选状态混在一起。cgProduct.js 的 _mergeDuplicateSelectRows / _syncMergedSelectListIfNeeded,Vuex 里的 selectList 合并不再误并。affirmMix.vue 的 onShow 合并循环(585-603 行),结算页展示与提交给 createMixOrder 的 product 数组会分别保留秒杀行和普通行各自的价格、数量、activityType。不需要改动后端和 affirmGhs.vue(该页仅处理花材 property=1,本身不带活动字段,_selectRowMatches 分支不涉及秒杀问题)。
affirmMix.vue 展示两行(各自价格、各自「秒杀」标签),数量互不影响。product 数组包含两条独立记录(一条带 activityType: 'seckill',一条不带),且后端下单成功、金额正确。