diy_offer 类型参考(o_diy_offer)
o_diy_offer 购物车插件活动速查。架构与计算顺序见 promotion-discount-checkout.md §4。
与实现冲突时以 extend/diyoffers/、DiyOfferService 为准。
目录
1. 类型总表
DB type | 实现类 | 前台 data_from(常见) | 改价模式 |
|---|---|---|---|
minmaxoffer | DiyOffersMinmaxoffer | app_minmaxoffer | InlinePrice |
promotion | DiyOffersPromotion | app_limitedtimeoffer / 空 | InlinePrice |
bundlesale | DiyOffersBundlesale | app_bundlesale | AccruedDiscount |
skubundlesale | DiyOffersSkubundlesale | app_skubundlesale | AccruedDiscount |
gift | DiyOffersGift | app_gift | LineRebuild |
扩展标识(无独立 PHP 类):app_exitintent(app_id=226)、app_customeow 等,多数仍走 type=promotion 或对应插件逻辑。
2. 三种改价模式
| 模式 | 汇总进 promotion_price | 体现在订单 |
|---|---|---|
| InlinePrice | 通常 否(diy_offers.discount=0) | order_product.price / current_subtotal_price |
| AccruedDiscount | 是(diy_offers[].discount) | current_promotion_price + 行 discount_price 分摊 |
| LineRebuild | 通常 否 | 增删行、礼品 final_price=0 |
3. 各 type params 与行为
minmaxoffer
店铺级,defaultDiyOffer(['minmaxoffer']) 自动打标,不需行上 diy_offer_id。后台保存时经 DiyOffersMinmaxoffer::validateSaveData 校验后写入 o_diy_offer.params(JSON 字符串)。
params 示例(rule_type=3 时才会同时含 rule_min 与 rule_max;rule_type=1 仅存 rule_min,rule_type=2 仅存 rule_max):
{
"rule_type": 3,
"rule_min": { "amount": 100.00, "title": "最低客单价" },
"rule_max": { "amount": 500.00, "title": "最高客单价", "lock_max_order_price": 1 },
"hide_fee": 0
}顶层字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
rule_type | int | 是 | 锁定策略:1 仅锁最低客单价;2 仅锁最高客单价;3 同时锁最低与最高。决定 calDiscount 走 calMinPrice / calMaxPrice / calMaxAndMinPrice。 |
rule_min | object | rule_type 为 1 或 3 时必填 | 最低客单价规则,见下表。 |
rule_max | object | rule_type 为 2 或 3 时必填 | 最高客单价规则,见下表。rule_type=3 时 rule_max.amount 须 ≥ rule_min.amount。 |
hide_fee | int | 否,默认 0 | 保存时 intval 写入 params。当前 PHP 结账计算链路(DiyOffersMinmaxoffer::calDiscount)未读取;仅作为配置落库,供后台/主题展示控制(如是否隐藏调价说明)。 |
rule_min 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
amount | number | 是 | 最低商品小计阈值(店铺货币,两位小数语义)。计算基准为各行 price × quantity 之和(calProductTotalPrice)。当前小计低于该值时,按比例分摊把行价上调至目标总额。 |
title | string | 是 | 前台展示文案。调价后写入行 property / original_property 中 type=app_minmaxoffer 项的 value(handleProperty)。 |
rule_max 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
amount | number | 是 | 最高商品小计阈值。当前小计高于该值时,按比例分摊把行价下调至目标总额;并设 cart.has_maxoffer=true。 |
title | string | 是 | 同 rule_min.title,用于 app_minmaxoffer property 展示。 |
lock_max_order_price | int | 否,默认 0 | 非 0 时把 rule_max.amount 写入 cart.minmaxoffer_max_order_price。结账选支付方式时 PaymentService::lockMaxOrderPrice 用该上限约束 订单总价(含支付手续费等);并与 minmaxoffer_diff_price 配合修正 order.current_subtotal_price 精度。 |
运行时行为(calDiscount)
| 场景 | 行为 |
|---|---|
rule_type=1 且小计 ≥ rule_min.amount | recoverPrice 恢复原价,清除 app_minmaxoffer property |
rule_type=2 且小计 ≤ rule_max.amount | 同上;has_maxoffer=false |
rule_type=3 且小计在 [rule_min, rule_max] 内 | 不调价,恢复原价 |
| 需要调价 | applyAdjustment 按行权重分摊目标总额;分摊舍入差写入 cart.minmaxoffer_diff_price;设 cart.has_minmaxoffer=true、refreshProductData=true |
| 与其它 diy_offer | has_minmaxoffer=true 后 跳过 bundlesale、gift 等其它 diy_offer 计算 |
与 o_promotion | 满减 仍执行(minmax 改的是行价/小计,不阻断标准满减) |
分摊规则:以各行 price × quantity 为权重(0 元行用 0.01 虚拟权重);最后一行吸收舍入差,保证商品小计尽量等于目标 amount。
promotion / limitedtimeoffer
DB
type均为promotion;前台data_from常见app_limitedtimeoffer(离开意图插件app_id=226时为app_exitintent)。与o_promotion标准满减的区别见 promotion-discount-checkout.md §4.1.1。
后台保存时 params 经 json_encode 写入 o_diy_offer.params;商品/专辑范围另写入 o_diy_offer.product_range + o_diy_offer_range(与 params.type 对应,见下表)。
params 示例(指定商品 + 限时):
{
"type": "products",
"data": [
{ "id": 123, "type": "discount", "value": 10, "range": [0, 999] }
],
"show_page": ["product_detail", "cart"],
"timer": 30,
"sort": "best_selling"
}type=all_ai 额外字段示例:
{
"type": "all_ai",
"data": [{ "id": 0, "type": "reduction", "value": 5, "range": [0, 0] }],
"show_page": ["product_detail"],
"timer": 60,
"enable_fallback": 1,
"ai_cooccurrence_source": "add_to_cart",
"related_product_limit": 5
}顶层字段(params)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type | string | 是 | 优惠适用范围,与表字段 product_range 对齐:products 指定商品;collection 指定专辑;all 全店;all_ai AI 关联推荐商品页。决定 calDiscount 走 changeCartPrice 的分支(1/2/3)。 |
data | array | 是 | 优惠规则列表,元素结构见下表。products 类型按 id=product_id 逐商品匹配;collection / all / all_ai 通常取 data[0] 作为统一规则。 |
show_page | string[] | 是 | 活动展示页面路由列表,如 product_detail、cart。formatFountListData 仅当当前 $route 命中时才返回该活动。 |
timer | int | 是 | 分钟(存库单位)。前台列表接口会 × 60 转为秒;若活动 ends_at 更近则取剩余秒数。用于倒计时 property promotion_timer 的有效期。 |
sort | string | 否 | 活动商品列表排序(getProductList):title_ascending / price_ascending / price_descending / best_selling / created_descending / created_ascending。空则按近期订单购买顺序 + ES 补全。 |
enable_fallback | int | all_ai 时 | 非 0 时 AI 协同商品不足则用店铺热销等兜底凑满 related_product_limit。 |
ai_cooccurrence_source | string | all_ai 时 | 协同比对数据源:add_to_cart(默认)/ view_content / purchase。 |
related_product_limit | int | 否,默认 5 | all_ai 推荐商品数量上限。 |
data[] 元素字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | int | 是 | type=products 时为 商品 ID;type=collection 时为 专辑 ID;all / all_ai 可为占位 0。 |
type | string | 是 | 改价方式(getDiscount):definite_price 指定售价(value 为目标价);discount 百分比折扣(value 为折扣百分比,如 10 表示 10% off);reduction 直减(value 为减价金额)。 |
value | number | 是 | 与 type 配合的数值,见上。 |
range | [number, number] | 否,默认 [0,0] | 仅 collection / all / all_ai 商品列表 ES 筛选:按 variant_price_min 过滤,[min,max],0 表示不限。 |
表级字段(非 params,保存时一并提交)
| 字段 | 说明 |
|---|---|
starts_at / ends_at | 活动起止时间戳;ends_at=0 表示永久(存为 FOREVER_TIME)。status=0 或未开始时不改价。 |
product_range + ids | 与 params.type 对应,写入 o_diy_offer_range(商品 ID 或专辑 ID 列表)。 |
status | 0 关闭 / 1 开启。 |
运行时行为(calDiscount → changeCartPrice)
| 场景 | 行为 |
|---|---|
行上无 promotion_timer property | 不改价;plan_properties_id = -diy_offer_id 来自 formatCartItemInfo |
倒计时过期 / 活动 ends_at 已过 | 清 diy_offer_id、property、data_from;cartDataComposition 也会清 id |
商品不在 data 范围 / 专辑不匹配 | 同上,恢复原价 |
| 改价成功 | 直接改行 price / final_line_price(InlinePrice,diy_offers.discount=0);refreshProductData=1 |
definite_price | 行价 = value |
discount | 行价 = 原价 × (1 - value/100) |
reduction | 行价 = max(原价 - value, 0) |
bundlesale
组合销售;validateSaveData 强制 product_range=products、ends_at=永久。行上须带 diy_offer_id(通常经 POST /cart/diyoffers 加购)。
params 示例:
{
"products": [
{ "product_id": 1, "num": 2, "master": 1 },
{ "product_id": 2, "num": 1, "master": 0 }
],
"discount_type": "percentage",
"discount_value": 20,
"discount_rule": "all",
"display_rule": "all"
}顶层字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
products | array | 是,≥2 个 | 组合内商品列表,元素见下表。保存时同步写入 o_diy_offer_range。 |
discount_type | string | 是 | fix 组合指定总价;percentage 百分比折扣(discount_value 须 0 < value < 100);constant 直减金额。 |
discount_value | number | 是 | 与 discount_type 配合的目标价 / 折扣百分比 / 减价金额。 |
discount_rule | string | 否,默认 all | all:每个配置商品的购物车数量须 等于 num 才生效;partial:数量 ≥ num 即可,仅统计达标商品的金额。 |
display_rule | string | 是 | 前台展示范围:all 任意组合内商品详情页均展示;master 仅 master=1 的主商品页展示。 |
products[] 元素字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
product_id | int | 是 | 组合内商品 ID。 |
num | int | 是 | 该商品在组合中要求的数量(见 discount_rule)。 |
master | int | 否,默认 0 | 1 表示主商品;配合 display_rule=master 控制详情页是否展示活动。 |
运行时行为(calDiscount)
| 场景 | 行为 |
|---|---|
| 数量条件不满足 | calTotalDiscount 返回 0;filterCartItemOfferInfo 清除 行上 diy_offer_id / data_from |
| 条件满足 | 汇总 AccruedDiscount:diy_offers[].discount 为负值优惠额;按行 final_line_price 升序均摊到 productDiscounts |
fix | 优惠 = min(0, discount_value - 组合原价合计) |
percentage | 优惠 = -(合计 × value / 100) |
constant | 优惠 = -min(value, 合计) |
| 与标准满减 | 参与 SKU 写入 mutexPromotionVariantUniqKey,与 o_promotion 互斥 |
skubundlesale
按 组合内 SKU 总件数 匹配 packages 档位;表级有 starts_at / ends_at / status,须在有效期内且 status=1 才计算。
params 示例:
{
"products": [{ "product_id": 1 }, { "product_id": 2 }],
"packages": [
{ "num": 2, "discount_type": "percentage", "discount_value": 10 },
{ "num": 3, "discount_type": "fix", "discount_value": 50 }
]
}顶层字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
products | array | 是,≥1,≤100 | 可参与组合的商品 ID 列表(product_id)。 |
packages | array | 是 | 按 件数档位 配置的优惠方案,元素见下表。 |
products[] / packages[] 元素字段
| 路径 | 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
products[] | product_id | int | 是 | 组合候选商品。 |
packages[] | num | int | 是 | 触发档位所需的 SKU 总件数(同 diy_offer_id 下各行 quantity 之和须 等于 某档 num)。 |
packages[] | discount_type | string | 是 | 同 bundlesale:fix / percentage / constant。 |
packages[] | discount_value | number | 是 | 同 bundlesale,相对该档组合 final_line_price 合计计算。 |
运行时行为(calDiscount)
| 场景 | 行为 |
|---|---|
活动未开始 / 已结束 / status=0 | 返回 discount=0 |
总件数未命中任一 packages[].num | discount=0 |
| 命中档位 | 计算逻辑同 bundlesale;AccruedDiscount 汇总进 promotion_price |
| 条件不满足 | 保留 行上 diy_offer_id(产品需求:统计归因),discount=0;refreshProductData=true |
| 与标准满减 | 同 bundlesale,写入 mutexPromotionVariantUniqKey |
gift
满赠;触发条件看 o_diy_offer.product_range + o_diy_offer_range(保存时在 range 字段提交,不在 params JSON 内)。params 只存门槛规则与赠品池。
params 示例:
{
"discount_type": 1,
"no_limit": 0,
"rules": [
{
"condition": 100,
"product_num": 1,
"products": [{ "id": 999 }, { "id": 1000 }]
},
{
"condition": 200,
"product_num": 2,
"products": [{ "id": 1001 }]
}
]
}顶层字段(params)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
discount_type | int | 是 | 门槛计量方式:1 按 金额(适用范围内商品 final_line_price 合计);2 按 件数(quantity 合计)。 |
no_limit | int | 否,默认 0 | 0 命中 rules 中最高满足档 一次,赠送 product_num 件;1 每满 condition 送 product_num 件(floor(当前值/condition) × product_num)。 |
rules | array | 是 | 阶梯规则,按 condition 升序配置;计算时 从高到低 匹配(array_reverse)。元素见下表。 |
rules[] 元素字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
condition | number | 是 | 门槛值(金额或件数,取决于 discount_type)。 |
product_num | int | 是 | 满足该档时可赠送的 礼品件数上限(顾客在 products 池中选择)。 |
products | array | 是 | 可选赠品池,[{ "id": product_id }, ...]。 |
表级字段(非 params)
| 字段 | 说明 |
|---|---|
product_range | 触发范围:all 全店 / products 指定商品 / collection 指定专辑;对应 o_diy_offer_range 行。 |
starts_at / ends_at | 活动时间;未在有效期内 getDiyOfferGifts 返回空。 |
| 同时段冲突 | 同 product_range + 重叠 range 不可保存(validateSaveData 抛 TIME_CONFLICT)。 |
运行时行为(calDiscount → LineRebuild)
| 场景 | 行为 |
|---|---|
getDiyOfferGifts | 按 product_range 汇总购物车统计值,匹配 rules 得 productIds + 可赠 quantity |
product_range switch | 无 break fall-through(all → collection → products 累加);all 类型会叠加专辑/商品统计,排障需注意 |
| 未达门槛 | 删除该 diy_offer_id 旧 gift 行;不新增可赠礼品 |
| 达门槛 | 删旧 gift 行 → 按已选 SKU 重建;可赠数量内 unavailable=0、final_price=0;超出拆分为不可赠行 |
| 购物车 vs 结账 | 购物车阶段不可赠标记 unavailable=1;结账页不可赠恢复为普通商品(清 diy_offer_id) |
| 与其它 diy_offer | has_minmaxoffer=true 时 gift 计算被跳过 |
| 汇总 | diy_offers.discount=0;优惠体现在 0 元礼品行 与小计扣减 |
4. 叠加与互斥
| 规则 | 说明 |
|---|---|
多 diy_offer_id | 购物车 可并存;cartDiyOffers 按 id 分组分别 calDiscount |
| minmaxoffer 激活 | 禁止 其它 diy_offer(bundlesale、gift 等) |
| bundlesale / skubundlesale | 参与 SKU 移出满减池(mutexPromotionVariantUniqKey) |
| 同一行 | 一般只绑 一个 diy_offer_id |
5. 传参方式(前台 → Redis)
diy_offer 不是像 offer_from_name 那样在结账步 POST 一个 JSON 包;而是在 加购 / 改购 时把活动信息写在 购物车行 上,持久化到 Redis guestcart / customercart。
5.1 主入口:POST /cart/diyoffers
路由:app/homeapi/route/route.php → Cart/diyOffers。
请求体(JSON):
{
"action": "addtocart",
"products": [
{
"product_id": 123,
"sku_code": "abc-0-0-0-0-0-0",
"quantity": 2,
"diy_offer_id": 456,
"diy_offer_name": "捆绑活动A",
"data_from": "app_bundlesale",
"property": [],
"second": 3600,
"unmodifiable": 0,
"unavailable": 0,
"label": ""
},
{
"product_id": 789,
"sku_code": "def-0-0-0-0-0-0",
"quantity": 1,
"diy_offer_id": 999,
"diy_offer_name": "限时促销B",
"data_from": "app_limitedtimeoffer",
"property": [{ "plan_properties_id": -1, "name": "promotion_timer", "value": "30" }],
"second": 1800
}
],
"checkout_type": ""
}| 字段 | 层级 | 说明 |
|---|---|---|
action | 根 | addtocart | buynow | batchbuynow | reducecart |
products | 根 | 数组;每项 = 一行商品 + 该行绑定的 diy_offer |
diy_offer_id | 行 | o_diy_offer.id;minmaxoffer 可不传(店铺级自动) |
diy_offer_name | 行 | 展示/归因用 |
data_from | 行 | 如 app_bundlesale、app_limitedtimeoffer、app_gift |
property | 行 | 定制属性;限时促销含 promotion_timer |
second | 行 | 倒计时秒数 → ends_at = time() + second |
unmodifiable / unavailable / label | 行 | gift 等场景 |
处理链:CartService::diyOffers → cartStructBuild → DiyOfferService::formatCartItems → 写 Redis。
5.2 普通加购 POST /cart/add
不带 diy_offer_id(仅 product_id、sku_code、quantity、data_from、property)。
无活动 id 的加购 不能 通过此接口绑 diy_offer;需走 /cart/diyoffers 或带活动的专用落地页。
5.2.1 还有哪些方式会写入 / 改变 diy_offer_id?
| 方式 | 是否前台传 diy_offer_id | 写哪里 | 说明 |
|---|---|---|---|
POST /cart/diyoffers | ✅ 行上显式传 | Redis / buynow cart_data | 主路径;action 含 addtocart、buynow、batchbuynow、reducecart |
POST /cart/buynow | ❌ 无此参数 | checkoutcart cart_data | createBuynow → cartStructBuild(...) 默认 id=0 |
POST /cart/batchbuynow | ❌ | checkoutcart | 同上 |
POST /cart/add / batched | ❌ | guestcart/customercart | 同上 |
getList → minmax | ❌(店铺级自动) | 先内存,可回写 Redis | defaultDiyOffer → DiyOffersMinmaxoffer::formatCartItemInfo 设 diy_offer_id(第109行) |
getList → gift | ❌(依赖已有行) | 内存 + resetCartListData | 满赠 重建/合并赠品行,保留活动 id;回写 Redis |
getList → bundlesale | ❌ | 内存 + 可能 resetCartListData | 条件不满足时 清 行上 diy_offer_id(filterCartItemInfo) |
| 限时过期 | ❌ | 内存;行数变时 saveCartListData | cartDataComposition 清 diy_offer_id(834–837 行) |
创单 saveProducts | ❌ | o_order_product | 从 cart 行 拷贝 到订单,不是写 Redis |
结论:前台要带活动 id 加购/立即购买,应走 /cart/diyoffers(或在同一接口里 action=buynow)。
minmax / gift / bundlesale 校验 可在 无 id 或 id 已存在 的行上,在 getList 计价阶段 自动 补写、清写或回写 Redis。
5.2.2 diy_offer_id 清理时机(代码索引)
⚡⚡⚡ 三处清理逻辑,触发条件不同,写入位置也不同。
1. 活动过期(通用)— cartDataComposition 第1步
适用:所有 diy_offer 类型(含限时促销)。
触发:ends_at 已过或 status=0。
代码:cartDataComposition 内检查并清 diy_offer_id;行数变化时 saveCartListData 回写 Redis。
2. bundlesale 条件不满足 — filterCartItemInfo
适用:仅 bundlesale。
触发:购物车内商品数量/配置不满足 bundlesale 条件。
代码:filterCartItemInfo 清除行 diy_offer_id;行数变化时回写。
3. 活动已删除(任意类型)— DiyOfferService::cartDiyOffers → resetCartListData(第436–480行)
if (empty($diyOfferInfo)) {
// 活动已删除
$needResetRedis = 1;
$clearCartDiyOfferIds[] = $diyOfferId;
} else {
$diyOfferInfo['type'] == 'gift' && $needResetRedis = 1; // gift 每次都触发重建
}
// ...
if ($needResetRedis) {
$this->resetCartListData($cart, $clearCartDiyOfferIds); // 清 clearCartDiyOfferIds 里的活动
}resetCartListData(489–524行)根据 clearCartDiyOfferIds 清除对应行的 diy_offer_id、property、name 等字段,并 saveCartListData 回写 Redis。
gift 行清理不在 resetCartListData:gift 的行变化(删除旧赠品行、新增满足条件的赠品行)由 DiyOffersGift::calDiscount 直接操作 $cart['items'];resetCartListData 只保证 Redis 数据同步(gift 类型每次都触发 needResetRedis=1 是为了重建同步)。
总结对照表:
| 触发条件 | 清理位置 | 写 Redis 条件 |
|---|---|---|
| 活动过期(通用) | cartDataComposition | 行数变化时 saveCartListData |
| bundlesale 条件不满足 | filterCartItemInfo | 行数变化时 saveCartListData |
| 活动已从数据库删除 | cartDiyOffers → resetCartListData | needResetRedis=1 时调用 |
| gift 行增删 | DiyOffersGift::calDiscount 改 $cart['items'] | 外部 resetCartListData 同步 |
5.2.4 单页与标准形态算券的数据源差异
| 场景 | 数据来源 | 说明 |
|---|---|---|
标准形态 CartService::getList | Redis preCouponCode | 用户在购物车页领券后写入 Redis |
单页 CheckoutOnePageService::getCart → calCartCouponPrice | trace_info 结构体 | 用户在结算页选择地址/支付方式后,trace_info 随请求传入 |
⚡⚡⚡ 两者数据源不同:标准形态读 Redis(预券),单页读 trace_info(当前请求上下文)。
⚡⚡⚡ 只有 minmaxoffer 是后台自动打 diy_offer_id 标记的实现类,bundlesale/gift 不在此处打标记。
CartService::getList 内三段调用(524–544 行):
// 第524行 — 先算 minmaxoffer(标记 + 计价)
(new DiyOfferService())->defaultDiyOffer($return, ['minmaxoffer']);
// 第527行 — minmax 未激活时才算其它 diy_offer
if (empty($return['has_minmaxoffer'])) {
(new DiyOfferService())->cartDiyOffers($return, ['promotion','bundlesale', 'skubundlesale']);
}
// 第544行 — gift 也被 minmax 挡掉
if (empty($return['has_minmaxoffer'])) {
(new DiyOfferService())->cartDiyOffers($return, ['gift']);
}defaultDiyOffer 内部流程(DiyOfferService 699–753 行):
1. 从 Redis(defaultDiyOfferLists)读店铺级默认活动
2. 遍历 cart['items'] 每行
3. 调 $obj->formatCartItemInfo() ← 这里打 diy_offer_id 标记
4. 调 cartDiyOffers() 算价格
打标记位置:DiyOffersMinmaxoffer::formatCartItemInfo()(第109行):
$cartItem['diy_offer_id'] = $offer['id'];
$cartItem['diy_offer_name'] = $offer['name'];
$cartItem['data_from'] = 'app_minmaxoffer';
// 同时填充 property各实现类 formatCartItemInfo 行为对比:
| 实现类 | diy_offer_id | 其它 |
|---|---|---|
DiyOffersMinmaxoffer | ✅ 打标记(第109行) | 填 name、data_from、property |
DiyOffersBundlesale | ❌ 空方法,不打 | — |
DiyOffersGift | ❌ 不打 | 填 name、unmodifiable、property(gift 条目) |
添加新的后台自动计算 diy_offer(参考 minmaxoffer):
extend/diyoffers/DiyOffersXxx.php实现calDiscount+formatCartItemInfo(后者打diy_offer_id)DiyOfferService::getExtendObj()注册 case- 在
CartService::getList的defaultDiyOffer调用里加 type,或仿照第527/544行单独调cartDiyOffers
5.3 计价阶段(无需再传参)
结账 getList 时读 Redis 行上的 diy_offer_id,按 id 分组调 cartDiyOffers / defaultDiyOffer。没有单独的「应用 diy_offer」POST。
5.4 能否一次用多个?
可以,模型是「多行、多个 id」,不是「一个 JSON 里列多个 offer」。
| 方式 | 说明 |
|---|---|
| 同一请求多行 | products[] 里每行可不同 diy_offer_id(上例 A+B) |
| 购物车多行 | 不同 SKU 各绑不同活动;cartDiyOffers 按 diy_offer_id 分组分别 calDiscount |
| 结果数组 | 内存 cart.diy_offers[] 可 多条(每个产生 discount≠0 的活动一条) |
| 同一行 | 仅一个标量 diy_offer_id(见 §5.5) |
| minmaxoffer | 店铺级 全局唯一;has_minmaxoffer 时 不算 其它 diy_offer |
| bundlesale SKU | 与标准满减 互斥(mutexPromotionVariantUniqKey) |
5.5 同一行为何一般只有一个 diy_offer_id?(代码依据)
1. 行结构只有一个字段
cartStructBuild 只写入 一个 diy_offer_id(标量),无 diy_offer_ids[] 数组。
2. 计价按该字段分组
DiyOfferService::cartDiyOffers 用 $product['diy_offer_id'] 做 bucket key 分组;diy_offer_id=0 的行跳过。一行只会落入 一个 bucket。
3. 合并加购不覆盖活动 id
buildVariantUniqKey = product_id + sku_code + property(不含 diy_offer_id)。
同 key 再次加购时(diyOffers addtocart 1676–1687 行)只累加 quantity,保留原行 diy_offer_id / data_from,不会变成第二次加购的活动。
4. minmax 覆盖行 id
DiyOffersMinmaxoffer::formatCartItemInfo 执行 $cartItem['diy_offer_id'] = $offer['id'],店铺 minmax 激活后 整行归属 minmax。
5.6 同一商品能否享受多个 diy_offer?怎么处理?
不能在同一 Redis 行上并存多个 diy_offer_id。 多活动靠以下方式:
| 场景 | 处理 |
|---|---|
| 同 SKU、不同活动 | 不同 property(如限时带 promotion_timer)→ 不同 uniqueKey → 两行,各行一个 id |
| 同 key 重复加购 | 先加购的活动 id 保留(合并逻辑不更新 id) |
| gift | 新增赠品行,独立 diy_offer_id |
| 整 cart 多活动 | 多行不同 id → cart.diy_offers[] 多条 |
diy_offer + o_promotion | 可叠加(不同流水线);bundlesale 可能 mutexPromotionVariantUniqKey 排除满减 |
| minmax + 其它 diy | minmax 后 跳过 其它 cartDiyOffers(CartService 524–527 行) |
| 限时 promotion 改价 | changeCartPrice 扫整 cart,但每行用 property 首项 的负 plan_properties_id 定活动(current($v['property'])) |
没有「一行挂 id 列表、分别计多次 diy_offer」的实现。
与 offer_from_name(订单级) 对比:
| diy_offer | order_diy_offer | |
|---|---|---|
| 传参时机 | 加购 / diyoffers API | 物流步 / complete 的 offer_from_name |
| 多选形式 | products[] 多行 或 cart 多行 | 一个 JSON 对象多个 key |
| 存储 | Redis 行字段 + 计价内存 | o_order_diy_offer 表 |
6. 落库字段
| 位置 | 字段 |
|---|---|
o_order_product | diy_offer_id、diy_offer_name、data_from、price←final_price |
o_order | current_promotion_price ← AccruedDiscount 类 discount 汇总(非 order_diy_offer) |
行 discount_price 分摊:OrderPriceService::calOrderProductDiscountPrice()(bundlesale/skubundlesale 的 diy_offers[].products[].discount)。
7. 单测 / 排障要点
| 场景 | 断言 / 排查 |
|---|---|
| minmax + bundlesale | 不应同时生效;查 has_minmaxoffer |
| bundlesale 数量边界 | all vs partial;不满足应清 offer 或 discount=0 |
| 限时促销过期 | ends_at、promotion_timer property |
| gift 规则切换 | 行重建、Redis reset |
current_promotion_price 有值、promotion_id 空 | 常见为 bundlesale/skubundlesale 或订单头未写满减 id |
| buynow 加购 | formatCartItemInfo 对 bundlesale 空实现,依赖前端 data_from |
8. 代码索引
| 模块 | 路径 |
|---|---|
| 编排 | common/services/DiyOfferService.php — cartDiyOffers、defaultDiyOffer |
| 加购 | common/services/CartService.php — diyOffers、cartStructBuild |
| Handler | extend/diyoffers/DiyOffers*.php |
| 分摊 | common/services/OrderPriceService.php |