请在以下目录中做代码研究(只读,不要修改文件):
需要回答的问题:
找到 mallApp/src/pages/home/cart.vue 完整文件内容。重点关注:
找到 mallApp/src/pages/billing/affirmMix.vue 完整文件内容。重点关注:
在 mallApp 相关的 api 封装 (src/api/**) 中找到"提交订单"、"结算预览"相关接口,看看提交给后端的数据结构里,商品项是否带有 activityType/activityId/seckillId 等字段用以区分秒杀和普通商品。
在后端 huahuibao 仓库中,找到 app-mall (或类似目录) 中处理"订单结算/下单"逻辑的 Controller 及相关的业务类 (biz-//classes),重点关注:
搜索整个 mallApp 目录下(不限于这两个文件)是否有其他地方用到类似的商品合并/去重逻辑(例如 utils 里的公共函数),这些函数被哪些页面复用。
请把找到的关键代码片段(带文件路径和行号)、以及你的分析总结,完整返回给我。这非常重要,请尽量详细并给出具体代码内容,不要只给结论。
以下是只读研究结果。核心结论先说:购物车加购能区分秒杀/普通,但结算页合并 key 漏了 activityType,因此同 goodsId + specGoodsId 的秒杀与普通会被合成一行。
cart.vue:唯一标识、去结算、加减合并文件:/Users/shishao/dnmp/www/front-end/mallApp/src/pages/home/cart.vue(约 1197 行;逻辑在 mixin cgProduct)
购物车页本身不写 dedupe key,而是:
getItemKey(item) {
return this.getSelectRowKey(item)
},
findCartRowIndex(list, item) {
if (!item || item.id === undefined || item.id === null) {
return -1
}
return list.findIndex((el) => this._selectRowMatches(el, item))
cgProduct.js 里两套逻辑不一致:
A. getSelectRowKey(勾选 map / 结算合并用)——不含 activityType:
/** 购物车行唯一键:花束用 id+property+specGoodsId,花材用 id(+classId) */
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}`;
},
花束 key = 0_{goodsId}_{specGoodsId},没有 activityType / activityId / seckillId。
B. _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);
}
if (this._shouldMergeSelectByProductId()) {
return String(element.id) === String(item.id);
}
return element.id == item.id && element.classId == item.classId;
},
加购时写入 activityType: 'seckill':
// 秒杀商品:打上活动标记与原价,用于购物车/结算页展示划线价;
// 实际下单价格由后端独立核对活动配置核价,此处价格仅用于前端展示
if (goods.activityType === 'seckill') {
row.activityType = 'seckill';
row.activityEndTime = Number(goods.activityEndTime) || 0;
row.originPrice = Number(goods.originPrice) || 0;
}
结论(购物车侧):
| 场景 | 判断字段 | 是否区分秒杀 vs 普通 |
|---|---|---|
| 加购合并 / 加减 / 删除 | property + id + specGoodsId + activityType |
是,可两行并存 |
勾选 checkedMap key |
0_id_specGoodsId(无 activityType) |
否,同规格秒杀/普通共用勾选态 |
仓库中未发现 activityId / seckillId 参与购物车行身份 |
— | — |
展示上会看 activityType === 'seckill' 打「秒杀」标(cart.vue 第 51 行)。
affirmMix goCheckout() {
const list = this.checkedCheckoutList
...
const checkoutUrl = '/pages/billing/affirmMix'
...
const navigateAffirm = () => {
this.limitBuyWarnList = []
this.setSelectInfoByType({ type: this.pageType, info: list })
this.setLimitBuyInfoByType({ type: this.pageType, info: [] })
const realHdId = this.hdId || uni.getStorageSync('hdId') || 0
this.$util.pageTo({
url: checkoutUrl,
query: { account, hdId: realHdId }
})
}
传递方式:
setSelectInfoByType({ type: 'cg', info: 勾选列表 }) —— 商品明细走 store,不是 URL。account、hdId。setLimitBuyInfoByType;本地缓存 key 形如 selectListcg_hd_{account}(persistCartToStorage / rememberProduct)。affirmMix.vue:合并逻辑与误合并原因文件:/Users/shishao/dnmp/www/front-end/mallApp/src/pages/billing/affirmMix.vue
onShow:
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;
公共合并(会先改 Vuex):
_mergeDuplicateSelectRows(list) {
...
const key = this.getSelectRowKey(ele) || String(ele.id);
if (!map.has(key)) {
...
} else {
const prev = map.get(key);
prev.bigCount = Number(prev.bigCount) + (Number(ele.bigCount) || 0);
prev.smallCount = Number(prev.smallCount) + (Number(ele.smallCount) || 0);
}
...
},
_syncMergedSelectListIfNeeded() {
if (!this._shouldMergeSelectByProductId()) return;
...
const merged = this._mergeDuplicateSelectRows(list);
if (merged.length !== list.length) {
this.setSelectInfoByType({ type: this.pageType, info: merged });
}
},
假设两行:
id=主商品, specGoodsId=2120, activityType='seckill', price=3id=主商品, specGoodsId=2120, activityType='' , price=16两者 getSelectRowKey 都是:
0_{主商品id}_2120
合并时:
_syncMergedSelectListIfNeeded() 先按该 key 合并进 Vuex;onShow 再用同一 key 累加 bigCount / smallCount;price、activityType 等),后一行数量加进去。因此会出现:
根因确认:合并 key 只用了 property + id + specGoodsId,没有把 activityType(更没有 activityId)纳入。
加购侧 _selectRowMatches 已意识到要分行,但 getSelectRowKey / 结算合并未对齐。
| 位置 | 路径 | 行号 |
|---|---|---|
| 结算页合并循环 | mallApp/src/pages/billing/affirmMix.vue |
588–603 |
| 进入页先同步合并 Vuex | 同上 + cgProduct._syncMergedSelectListIfNeeded |
affirmMix 588;mixin 298–305 |
| key 定义 | mallApp/src/mixins/cgProduct.js |
262–272 |
| Map 合并实现 | mallApp/src/mixins/cgProduct.js |
273–296 |
| 加购可区分 activityType | mallApp/src/mixins/cgProduct.js |
307–317, 475–485 |
/Users/shishao/dnmp/www/front-end/mallApp/src/api/order/index.js:
export const createOrder = data => {
return https.post('/order/create-order', data)
}
/** *
* 花束+花材合并结算下单(不影响 buy-item / create-order)
*/
export const createMixOrder = data => {
return https.post('/order/create-mix-order', data)
}
createMixOrder → POST /order/create-mix-order(affirmMix 使用)buyItem → /order/buy-item(affirmGhs)createOrder → /order/create-ordergetLimitBuyInfo、orderRelate、allDeliveryQuotes(跑腿报价) const product = (this.list || []).map((ele) => {
if (this.isBouquetItem(ele)) {
const saleId = Number(ele.specGoodsId) > 0 ? Number(ele.specGoodsId) : Number(ele.id);
const row = {
productId: saleId,
goodsId: Number(ele.goodsId || ele.id),
specGoodsId: Number(ele.specGoodsId) || 0,
num: Number(ele.bigCount) || 1,
unitType: 0,
property: 0
};
// 秒杀/团购打上活动标记,后端据此独立核对活动库存/限购/价格后再计价
if (ele.activityType === "seckill" || ele.activityType === "groupBuy") {
row.activityType = ele.activityType;
}
return row;
}
...
return { productId: ele.id, num, itemId: ele.itemId, unitType, property: 1 };
});
要点:
activityType: 'seckill' | 'groupBuy'activityId / seckillIdactivityType 取决于合并后保留的是哪一行入口:/Users/shishao/dnmp/www/huahuibao/app-mall/controllers/OrderController.php → actionCreateMixOrder(约 958 行起)
按 product JSON 逐行处理,没有用 goodsId+specGoodsId 再做跨行去重。
花束分支(约 1149–1255):
goodsId / specGoodsId / productId 解析销售单元 saleGoodsId仅当 activityType === 'seckill' 时走秒杀:
// 秒杀商品:不信任前端传的价格,独立回查 Redis 秒杀配置核实活动状态/库存/限购后再核价
$seckillRow = null;
if (strval($element['activityType'] ?? '') === 'seckill') {
$seckillRow = HomePageModuleClass::getSeckillActiveRow($mainId, $saleGoodsId);
...
}
...
if ($seckillRow) {
$unitPrice = floatval($seckillRow['price'] ?? 0);
...
} else {
$ret = \bizHd\goods\classes\GoodsClass::getFinalPrice(...);
...
}
if ($seckillRow) {
HomePageModuleClass::reserveSeckillPurchase($mainId, $saleGoodsId, $hdCustomId, $goodsNum);
}
秒杀配置查找:HomePageModuleClass::getSeckillActiveRow($mainId, $goodsId) —— 按 销售商品 id(规格 id) 匹配,无单独 activityId:
public static function getSeckillActiveRow($mainId, $goodsId)
{
$data = self::getSeckill($mainId, true);
...
if (intval($item['goodsId'] ?? 0) === $goodsId) {
return $item;
}
| 维度 | 字段 |
|---|---|
| 花材行 | productId(+ property=1) |
| 花束行 | goodsId + specGoodsId/productId → saleGoodsId |
| 秒杀 vs 普通 | activityType === 'seckill'(请求行级);无 activityId |
| 秒杀库存/限购键 | mainId + saleGoodsId(规格商品 id) |
后端本身允许同规格两行(一行带 seckill、一行不带);当前问题主要在前端合并丢掉一行后再提交。
单品立即购买 create-order 同样用顶层 activityType(约 610 行),逻辑一致。
| 函数/逻辑 | 文件 | 被谁用 |
|---|---|---|
getSelectRowKey / _mergeDuplicateSelectRows / _syncMergedSelectListIfNeeded / _selectRowMatches |
src/mixins/cgProduct.js |
混入 cart、affirmMix、affirmGhs、商品详情加购等 |
affirmMix onShow mergedMap |
pages/billing/affirmMix.vue 588–603 |
仅混合结算 |
affirmGhs onShow |
pages/billing/affirmGhs.vue 367–381 |
只用 item.id 合并,更粗,主要面向花材 |
addBouquetToCart |
mixin | goods/detail.vue → onAddCartFromPopup |
未发现独立 utils 公共 groupBy(goodsId, specGoodsId);合并集中在 cgProduct mixin + 两个结算页 onShow。
仓库内没有 activityId / seckillId 作为购物车/结算行 key;活动标识基本是字符串 activityType: 'seckill'。
加购 (_selectRowMatches 含 activityType)
→ 购物车可显示两行(秒杀 ¥3 + 普通 ¥16)
勾选 key = getSelectRowKey(无 activityType)
→ 同规格两行共享 checkedMap
去结算 → Vuex selectList + 路由 account/hdId
affirmMix onShow
→ _syncMergedSelectListIfNeeded + mergedMap
→ key = 0_id_2120(无 activityType)
→ 两行合成一行,数量累加,价格/活动标取第一行
提交 createMixOrder
→ 只剩一行 product;activityType 可能错/丢
修复方向(仅分析,未改代码): 让 getSelectRowKey(及 _mergeDuplicateSelectRows / affirmMix 合并)与 _selectRowMatches 对齐,花束 key 至少加入 activityType(例如 0_${id}_${specGoodsId}_${activityType||''}),勾选 map 同步调整。