From 038743f3f28dcc1298d37162241f7ed17e1b0bff Mon Sep 17 00:00:00 2001 From: "COMPUTER\\EDY" <249929363@qq.com> Date: Wed, 30 Sep 2026 22:01:37 +0800 Subject: [PATCH] fix: disable starter pack server purchase guard --- .../laf-cloud/functions/limitedTimeEvent.ts | 15 +--- server/laf-cloud/functions/login.ts | 11 +-- server/laf-cloud/functions/wx/KeFuInfo.ts | 14 ++- server/laf-cloud/functions/wx/checkIos.ts | 8 +- .../laf-cloud/functions/wx/getOrderReward.ts | 3 +- server/laf-cloud/functions/wx/getPayInfo.ts | 5 +- .../laf-cloud/functions/wx/iosgetPayInfo.ts | 13 +-- .../laf-cloud/functions/wx/iosorderPaySig.ts | 10 +-- server/laf-cloud/functions/wx/orderPaySig.ts | 10 +-- server/laf-cloud/functions/wx/payCallBack.ts | 10 +-- server/laf-cloud/starterPack.README.md | 6 +- .../laf-cloud/tests/login-ios-reborn.test.mjs | 16 ++-- server/laf-cloud/tests/rookie-gift.test.mjs | 89 ++++++++++--------- server/laf-cloud/tests/starter-pack.test.mjs | 22 ++--- 14 files changed, 88 insertions(+), 144 deletions(-) diff --git a/server/laf-cloud/functions/limitedTimeEvent.ts b/server/laf-cloud/functions/limitedTimeEvent.ts index 8e0c43e..9b6dca7 100644 --- a/server/laf-cloud/functions/limitedTimeEvent.ts +++ b/server/laf-cloud/functions/limitedTimeEvent.ts @@ -185,21 +185,10 @@ export function starterPackRound(user: any): number { : Number(user?.starter_pack) > 0 || Number(user?.starter_packState) === 1 ? 1 : 0; } -export function starterPackOrderSnapshot(user: any, itemid: string) { +export function starterPackOrderMetadata(itemid: string) { if (!/^starter_pack(?:_[2-5])?$/.test(itemid)) return {}; - if (!user || !Number.isFinite(Number(user.levelAmount)) || Number(user.levelAmount) < 15 - || Number(user.starter_packState) === 1 || !(Number(user.starter_pack) > Date.now())) { - throw new Error('本轮礼包未开放或已购买'); - } - const offer = user.starterPackOffer ?? starterPackPatch(user, Date.now())?.starterPackOffer; - if (!offer || offer.productId !== itemid || offer.expiresAt !== Number(user.starter_pack)) { - throw new Error('礼包档位已更新,请重新打开礼包'); - } return { - starterPackOffer: offer, - starterPackBuyRound: Math.max(1, starterPackRound(user)), - starterPackExpiresAt: Math.max(0, Number(user?.starter_pack) || 0), - rewardVersion: offer.rewardVersion, + rewardVersion: STARTER_REWARD_VERSION, activityType: 'lucky', }; } diff --git a/server/laf-cloud/functions/login.ts b/server/laf-cloud/functions/login.ts index da130b5..7af042e 100644 --- a/server/laf-cloud/functions/login.ts +++ b/server/laf-cloud/functions/login.ts @@ -484,16 +484,7 @@ async function getIosOrder(openid, ctx) { case "starter_pack_3": case "starter_pack_4": case "starter_pack_5": { - const order = res.data[i], user = res1.data; - const legacyCycle = (!user.starterPackOffer || user.starterPackOffer.legacy === true) && Number(order.time) >= Number(user.starter_pack) - 2 * 86400000 - && Number(order.time) <= Number(user.starter_pack); - const expiry = order.starterPackExpiresAt ?? (legacyCycle ? user.starter_pack : null); - if (expiry != null && Number(expiry) === Number(user.starter_pack) - && (order.starterPackBuyRound == null || Number(order.starterPackBuyRound) === (Number(user.starterPackRound) || 1))) { - await db.collection("users").where({ _id: user._id, starter_pack: user.starter_pack, - starterPackRound: user.starterPackRound ?? null }).update({ starter_packState: 1 }); - } - await db.collection("users").where({ _id: user._id }).update({ pay_user: true }); + await db.collection("users").where({ _id: res1.data._id }).update({ starter_packState: 1, pay_user: true }); break; } default: diff --git a/server/laf-cloud/functions/wx/KeFuInfo.ts b/server/laf-cloud/functions/wx/KeFuInfo.ts index cf253fc..7cdf872 100644 --- a/server/laf-cloud/functions/wx/KeFuInfo.ts +++ b/server/laf-cloud/functions/wx/KeFuInfo.ts @@ -186,13 +186,11 @@ async function PostImg(OpenId, outTradeNo, name, count, price, passInput: any = if (!token) { return { code: 0, data: null, msg: "token获取失败" }; } - const giftUser = /^starter_pack(?:_[2-5])?$/.test(name) ? (await db.collection("users").where({ openid: OpenId }).getOne()).data : null; - let giftSnapshot: any = {}; - try { - if (String(name).startsWith("starter_pack") && !/^starter_pack(?:_[2-5])?$/.test(name)) throw new Error("无效的礼包商品"); - giftSnapshot = StarterPack.starterPackOrderSnapshot(giftUser, name); - if (giftSnapshot.starterPackOffer) { count = 1; price = giftSnapshot.starterPackOffer.priceFen; } - } catch (error) { await PostMsg(OpenId, error.message); return; } + if (String(name).startsWith("starter_pack") && !/^starter_pack(?:_[2-5])?$/.test(name)) { + await PostMsg(OpenId, "无效的礼包商品"); + return; + } + const giftMetadata = StarterPack.starterPackOrderMetadata(name); const url = `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${token}`; const nonceStr = crypto.randomBytes(16).toString('hex'); const timestamp = Math.floor(Date.now() / 1000).toString(); @@ -226,7 +224,7 @@ async function PostImg(OpenId, outTradeNo, name, count, price, passInput: any = } await db.collection("iosOrder").add({ ...passFields, - ...giftSnapshot, + ...giftMetadata, openid: OpenId, outTradeNo: outTradeNo, itemid: name, diff --git a/server/laf-cloud/functions/wx/checkIos.ts b/server/laf-cloud/functions/wx/checkIos.ts index 22ecd7d..01e6df1 100644 --- a/server/laf-cloud/functions/wx/checkIos.ts +++ b/server/laf-cloud/functions/wx/checkIos.ts @@ -25,12 +25,8 @@ export default async function (ctx: FunctionContext) { openid: res.data.openid, outTradeNo: res.data.outTradeNo, itemid: res.data.itemid, - ...(res.data.starterPackBuyRound != null ? { starterPackBuyRound: res.data.starterPackBuyRound } : {}), - ...(res.data.starterPackExpiresAt != null ? { starterPackExpiresAt: res.data.starterPackExpiresAt } : {}), - ...(res.data.starterPackOffer ? { - starterPackOffer: res.data.starterPackOffer, - activityType: res.data.activityType, rewardVersion: res.data.rewardVersion, - } : {}), + ...(res.data.activityType ? { activityType: res.data.activityType } : {}), + ...(res.data.rewardVersion ? { rewardVersion: res.data.rewardVersion } : {}), itemCount: res.data.itemCount, goodsPrice: res.data.goodsPrice, time: res.data.time, diff --git a/server/laf-cloud/functions/wx/getOrderReward.ts b/server/laf-cloud/functions/wx/getOrderReward.ts index af603c7..09c6675 100644 --- a/server/laf-cloud/functions/wx/getOrderReward.ts +++ b/server/laf-cloud/functions/wx/getOrderReward.ts @@ -31,7 +31,6 @@ export default async function (ctx: FunctionContext) { } } return { code: 1, data: "ok", msg: "成功", ...(/^starter_pack(?:_[2-5])?$/.test(order.data.itemid) ? { - starterPackOffer: order.data.starterPackOffer, itemid: order.data.itemid, - starterPackBuyRound: order.data.starterPackBuyRound, + itemid: order.data.itemid, } : {}) }; } diff --git a/server/laf-cloud/functions/wx/getPayInfo.ts b/server/laf-cloud/functions/wx/getPayInfo.ts index 8fdcd6b..8a42b66 100644 --- a/server/laf-cloud/functions/wx/getPayInfo.ts +++ b/server/laf-cloud/functions/wx/getPayInfo.ts @@ -23,10 +23,7 @@ export default async function (ctx: FunctionContext) { if (res.data.openid !== user.data.openid) { return { code: 0, data: null, msg: "充值订单不属于当前玩家" }; } - const starterData = /^starter_pack(?:_[2-5])?$/.test(res.data.itemid) ? { - starterPackOffer: res.data.starterPackOffer, itemid: res.data.itemid, - starterPackBuyRound: res.data.starterPackBuyRound, - } : {}; + const starterData = /^starter_pack(?:_[2-5])?$/.test(res.data.itemid) ? { itemid: res.data.itemid } : {}; if (res.data.state == 1 || res.data.state == 2) { // Retry only events already captured by the payment callback; do not backfill // historical completed orders using today's remaining time. diff --git a/server/laf-cloud/functions/wx/iosgetPayInfo.ts b/server/laf-cloud/functions/wx/iosgetPayInfo.ts index 8cf0eaa..b13a4c8 100644 --- a/server/laf-cloud/functions/wx/iosgetPayInfo.ts +++ b/server/laf-cloud/functions/wx/iosgetPayInfo.ts @@ -31,8 +31,7 @@ export default async function (ctx: FunctionContext) { } let payment_name = ret.data?.itemid; const starterGift = /^starter_pack(?:_[2-5])?$/.test(payment_name); - const starterData = starterGift ? { starterPackOffer: ret.data.starterPackOffer, - itemid: payment_name, starterPackBuyRound: ret.data.starterPackBuyRound } : {}; + const starterData = starterGift ? { itemid: payment_name } : {}; if (starterGift) { const owner = uid ? (await db.collection("users").where({ _id: uid }).getOne()).data : null; if (!owner || owner.openid !== ret.data.openid) return { code: 0, data: null, msg: "充值订单不属于当前玩家" }; @@ -95,15 +94,7 @@ export default async function (ctx: FunctionContext) { case "starter_pack_3": case "starter_pack_4": case "starter_pack_5": { - const legacyCycle = (!res1.data.starterPackOffer || res1.data.starterPackOffer.legacy === true) && Number(ret.data.time) >= Number(res1.data.starter_pack) - 2 * 86400000 - && Number(ret.data.time) <= Number(res1.data.starter_pack); - const expiry = ret.data.starterPackExpiresAt ?? (legacyCycle ? res1.data.starter_pack : null); - if (expiry != null && Number(expiry) === Number(res1.data.starter_pack) - && (ret.data.starterPackBuyRound == null || Number(ret.data.starterPackBuyRound) === (Number(res1.data.starterPackRound) || 1))) { - await db.collection("users").where({ _id: uid, starter_pack: res1.data.starter_pack, - starterPackRound: res1.data.starterPackRound ?? null }).update({ starter_packState: 1 }); - } - await db.collection("users").where({ _id: uid }).update({ pay_user: true }); + await db.collection("users").where({ _id: uid }).update({ starter_packState: 1, pay_user: true }); break; } default: diff --git a/server/laf-cloud/functions/wx/iosorderPaySig.ts b/server/laf-cloud/functions/wx/iosorderPaySig.ts index 1f08765..a59c26e 100644 --- a/server/laf-cloud/functions/wx/iosorderPaySig.ts +++ b/server/laf-cloud/functions/wx/iosorderPaySig.ts @@ -42,12 +42,10 @@ export default async function (ctx: FunctionContext) { if (user.data == null) { return { code: 0, data: null, msg: "未获取到玩家数据" }; } - let starterFields: any = {}; - try { - if (String(itemid).startsWith("starter_pack") && !/^starter_pack(?:_[2-5])?$/.test(itemid)) throw new Error("无效的礼包商品"); - starterFields = StarterPack.starterPackOrderSnapshot(user.data, itemid); - if (starterFields.starterPackOffer) { itemCount = 1; itemPrice = starterFields.starterPackOffer.priceFen; } - } catch (error) { return { code: 0, data: null, msg: error.message }; } + if (String(itemid).startsWith("starter_pack") && !/^starter_pack(?:_[2-5])?$/.test(itemid)) { + return { code: 0, data: null, msg: "无效的礼包商品" }; + } + const starterFields = StarterPack.starterPackOrderMetadata(itemid); const jungleTier = JungleConfig.getJunglePaidTierByProductId(itemid); let passFields: any = {}; try { diff --git a/server/laf-cloud/functions/wx/orderPaySig.ts b/server/laf-cloud/functions/wx/orderPaySig.ts index eb7ca73..f930742 100644 --- a/server/laf-cloud/functions/wx/orderPaySig.ts +++ b/server/laf-cloud/functions/wx/orderPaySig.ts @@ -42,12 +42,10 @@ export default async function (ctx: FunctionContext) { if (user.data == null) { return { code: 0, data: null, msg: "未获取到玩家数据" }; } - let starterFields: any = {}; - try { - if (String(itemid).startsWith("starter_pack") && !/^starter_pack(?:_[2-5])?$/.test(itemid)) throw new Error("无效的礼包商品"); - starterFields = StarterPack.starterPackOrderSnapshot(user.data, itemid); - if (starterFields.starterPackOffer) { itemCount = 1; itemPrice = starterFields.starterPackOffer.priceFen; } - } catch (error) { return { code: 0, data: null, msg: error.message }; } + if (String(itemid).startsWith("starter_pack") && !/^starter_pack(?:_[2-5])?$/.test(itemid)) { + return { code: 0, data: null, msg: "无效的礼包商品" }; + } + const starterFields = StarterPack.starterPackOrderMetadata(itemid); const jungleTier = JungleConfig.getJunglePaidTierByProductId(itemid); let passFields: any = {}; try { diff --git a/server/laf-cloud/functions/wx/payCallBack.ts b/server/laf-cloud/functions/wx/payCallBack.ts index faf8833..d18e1b1 100644 --- a/server/laf-cloud/functions/wx/payCallBack.ts +++ b/server/laf-cloud/functions/wx/payCallBack.ts @@ -112,15 +112,7 @@ export default async function (ctx: FunctionContext) { } if (/^starter_pack(?:_[2-5])?$/.test(order.itemid)) { - // An earlier round may finish paying after a newer offer has opened. - const legacyCycle = (!user.starterPackOffer || user.starterPackOffer.legacy === true) && Number(order.time) >= Number(user.starter_pack) - 2 * 86400000 - && Number(order.time) <= Number(user.starter_pack); - const expiry = order.starterPackExpiresAt ?? (legacyCycle ? user.starter_pack : null); - if (expiry != null && Number(expiry) === Number(user.starter_pack) - && (order.starterPackBuyRound == null || Number(order.starterPackBuyRound) === (Number(user.starterPackRound) || 1))) { - await db.collection("users").where({ _id: user._id, starter_pack: user.starter_pack, - starterPackRound: user.starterPackRound ?? null }).update({ starter_packState: 1 }); - } + userUpdate.starter_packState = 1; } if (!user.pay_user) { userUpdate.pay_user = true; diff --git a/server/laf-cloud/starterPack.README.md b/server/laf-cloud/starterPack.README.md index cedb796..29f4250 100644 --- a/server/laf-cloud/starterPack.README.md +++ b/server/laf-cloud/starterPack.README.md @@ -30,13 +30,13 @@ ## 支付及兼容 -现有下单接口仅对上述五个 SKU 校验当前有效轮次、未购买状态与商品匹配,并固定服务端报价和数量 1。订单保存不含奖励内容的 `starterPackOffer`、`starterPackBuyRound`、`starterPackExpiresAt`、`activityType`、`rewardVersion`,旧 iOS 转单继续携带这些字段。 +当前临时关闭新手礼包的服务端档位价格、轮次和购买资格校验。五个有效 SKU 使用客户端提交的价格与数量创建订单;`starter_pack_1` 等无效 SKU 仍拒绝。订单不保存 `starterPackOffer`、`starterPackBuyRound` 或 `starterPackExpiresAt`,只保留画像统计使用的 `activityType` 和 `rewardVersion`。 -新订单到期后不可创建;到期前的有效订单可继续原支付流程。回调只关闭所属轮次,不关闭后开的礼包。查单携带商品和轮次快照;`getOrderReward` 仍返回 `data="ok"`,礼包快照位于响应顶层。前端只按订单商品 ID 读取本地固定奖励,不读取后端奖励字段。登录和账号迁移保留轮次及报价。 +支付回调和登录补单确认任一新手礼包 SKU 后,直接把当前礼包标记为已购买,不匹配订单轮次。查单和 `getOrderReward` 只返回订单商品 ID,前端按商品 ID 读取本地固定奖励。活动展示本身仍保留当前轮次及报价,用于客户端展示和触发下一轮。 保留原跨设备并发下单、客户端发奖及旧 iOS 响应中断的边界,未新增订单占位锁或服务端发奖事务。礼包待支付订单不再按原通用 3 小时期限清理,以免删除仍可能支付的旧单;关闭但未成功的礼包订单也会保留,不因此发奖。退款不在本次范围内。 -`rookie_gift` 支付成功埋点扩展至五个 SKU,沿用订单级去重;轮次和剩余秒数取原订单快照。 +`rookie_gift` 支付成功埋点扩展至五个 SKU,沿用订单级去重;新订单不保存活动轮次时按现有保守兼容逻辑推算轮次和剩余秒数。 ## 发布与验证 diff --git a/server/laf-cloud/tests/login-ios-reborn.test.mjs b/server/laf-cloud/tests/login-ios-reborn.test.mjs index 4440a59..8b9a983 100644 --- a/server/laf-cloud/tests/login-ios-reborn.test.mjs +++ b/server/laf-cloud/tests/login-ios-reborn.test.mjs @@ -56,25 +56,21 @@ test('disabled gold miner orders never enter legacy iOS recovery', async () => { }); for (const itemid of ['starter_pack', 'starter_pack_2', 'starter_pack_3', 'starter_pack_4', 'starter_pack_5']) { - test(`login preserves lucky offer metadata and recovers ${itemid} through original delivery`, async () => { - const offer = { version: 3, round: 2, productId: itemid, expiresAt: Date.now() + 10000, - priceFen: 1200, reward: { coin: 4500, hammer: 3, freeze: 3, magic_wand: 3, infinite_health: 2700 } }; - const { user, order, recover } = fixture(itemid, { - starterPackOffer: offer, starterPackBuyRound: 2, starterPackExpiresAt: offer.expiresAt, - }); - Object.assign(user, { starterPackOffer: offer, starterPackRound: 2, starter_pack: offer.expiresAt }); + test(`login recovers ${itemid} without order offer or cycle metadata`, async () => { + const { user, order, recover } = fixture(itemid); const pending = await recover(user.openid, {}); assert.equal(user.starter_packState, 1); assert.equal(user.pay_user, true); assert.equal(order.state, 1); - assert.deepEqual(pending[0].starterPackOffer, offer); + assert.equal(pending[0].itemid, itemid); assert.equal(pending[0].starterPackOffer, undefined); + assert.equal(pending[0].starterPackBuyRound, undefined); assert.equal(pending[0].starterPackExpiresAt, undefined); assert.deepEqual(await recover(user.openid, {}), []); }); } -test('login payment for an earlier lucky round never closes the current offer', async () => { +test('login payment closes the current offer without matching an order round', async () => { const { user, order, recover } = fixture('starter_pack_3', { starterPackBuyRound: 2, starterPackExpiresAt: 1000 }); Object.assign(user, { starterPackRound: 3, starter_pack: 2000, starterPackOffer: { version: 3, round: 3 } }); assert.equal((await recover(user.openid, {})).length, 1); - assert.equal(user.starter_packState, undefined); assert.equal(user.pay_user, true); assert.equal(order.state, 1); + assert.equal(user.starter_packState, 1); assert.equal(user.pay_user, true); assert.equal(order.state, 1); }); test('login closes a matching migrated legacy cycle for an older order without an offer snapshot', async () => { diff --git a/server/laf-cloud/tests/rookie-gift.test.mjs b/server/laf-cloud/tests/rookie-gift.test.mjs index e9dde46..83a9d51 100644 --- a/server/laf-cloud/tests/rookie-gift.test.mjs +++ b/server/laf-cloud/tests/rookie-gift.test.mjs @@ -53,7 +53,7 @@ registerHooks({ return next(specifier, context); } }); -const { reportRookieGift, starterPackOrderSnapshot } = await import('../functions/limitedTimeEvent.ts'); +const { reportRookieGift, starterPackOrderMetadata } = await import('../functions/limitedTimeEvent.ts'); const { default: callback } = await import('../functions/wx/payCallBack.ts'); const { default: query } = await import('../functions/wx/getPayInfo.ts'); const { default: iosQuery } = await import('../functions/wx/iosgetPayInfo.ts'); @@ -73,7 +73,8 @@ function reset() { tables.users.push(user); return user; } function order(user, extra = {}) { - const row = { outTradeNo: 'wcx_test', openid: 'o', itemid: 'starter_pack', itemCount: 1, goodsPrice: 300, state: 0, time: NOW - 10000, ...starterPackOrderSnapshot(user, 'starter_pack'), ...extra }; + const row = { outTradeNo: 'wcx_test', openid: 'o', itemid: 'starter_pack', itemCount: 1, goodsPrice: 300, + state: 0, time: NOW - 10000, ...starterPackOrderMetadata('starter_pack'), ...extra }; tables.order.push(row); return row; } const ctx = body => ({ body, headers: {}, socket: { remoteAddress: '127.0.0.1' } }); @@ -87,83 +88,88 @@ function setOffer(user, productId, priceFen) { return user.starterPackOffer; } -test('each lucky tier uses the server offer price, quantity and tier snapshot on native payment', async () => { +test('each lucky tier keeps the submitted price and quantity without an offer or cycle snapshot', async () => { for (const create of [androidOrder, nativeIosOrder]) { for (const [productId, priceFen] of [['starter_pack', 300], ['starter_pack_2', 600], ['starter_pack_3', 1200], ['starter_pack_4', 1800], ['starter_pack_5', 3000]]) { const user = reset(), offer = setOffer(user, productId, priceFen); const result = await create(ctx({ uid: 'u', itemid: productId, itemPrice: 1, itemCount: 9 })); assert.equal(result.code, 1); const saved = tables.order[0]; - assert.equal(saved.goodsPrice, priceFen); assert.equal(saved.itemCount, 1); - assert.deepEqual(saved.starterPackOffer, offer); - assert.equal(saved.starterPackOffer.reward, undefined); + assert.equal(saved.goodsPrice, 1); assert.equal(saved.itemCount, 9); + assert.equal(saved.starterPackOffer, undefined); + assert.equal(saved.starterPackBuyRound, undefined); assert.equal(saved.starterPackExpiresAt, undefined); assert.equal(saved.activityType, 'lucky'); assert.equal(saved.rewardVersion, offer.rewardVersion); const signed = JSON.parse(result.data.signData); - assert.equal(signed.productId, productId); assert.equal(signed.goodsPrice, priceFen); assert.equal(signed.buyQuantity, 1); + assert.equal(signed.productId, productId); assert.equal(signed.goodsPrice, 1); assert.equal(signed.buyQuantity, 9); } } }); -test('lucky native creation rejects a mismatched tier, invalid suffix, expired or purchased offer', async () => { - for (const create of [androidOrder, nativeIosOrder]) for (const scenario of ['mismatch', 'suffix', 'expired', 'purchased']) { +test('lucky native creation accepts any valid tier without offer eligibility checks', async () => { + for (const create of [androidOrder, nativeIosOrder]) for (const scenario of ['mismatch', 'expired', 'purchased']) { const user = reset(); setOffer(user, 'starter_pack_3', 1200); if (scenario === 'expired') { user.starter_pack = NOW; user.starterPackOffer.expiresAt = NOW; } if (scenario === 'purchased') user.starter_packState = 1; - const itemid = scenario === 'mismatch' ? 'starter_pack_2' : scenario === 'suffix' ? 'starter_pack_1' : 'starter_pack_3'; - assert.equal((await create(ctx({ uid: 'u', itemid, itemPrice: 1200, itemCount: 1 }))).code, 0); + const itemid = scenario === 'mismatch' ? 'starter_pack_2' : 'starter_pack_3'; + assert.equal((await create(ctx({ uid: 'u', itemid, itemPrice: 1200, itemCount: 1 }))).code, 1); + assert.equal(tables.order.length, 1); + } + for (const create of [androidOrder, nativeIosOrder]) { + reset(); + assert.equal((await create(ctx({ uid: 'u', itemid: 'starter_pack_1', itemPrice: 300, itemCount: 1 }))).code, 0); assert.equal(tables.order.length, 0); } }); -test('lucky query and original reward acknowledgement return the purchased snapshot after expiry', async () => { +test('lucky query and reward acknowledgement return only the purchased SKU', async () => { const user = reset(), offer = setOffer(user, 'starter_pack_4', 1800); const row = { outTradeNo: 'wcx_test', openid: user.openid, itemid: offer.productId, goodsPrice: offer.priceFen, - itemCount: 1, state: 1, ...starterPackOrderSnapshot(user, offer.productId) }; + itemCount: 1, state: 1, ...starterPackOrderMetadata(offer.productId) }; tables.order.push(row); now += 2 * DAY; const result = await query(ctx({ uid: 'u', outTradeNo: row.outTradeNo })); assert.equal(result.data.pay_state, 2); assert.equal(result.data.itemid, offer.productId); - assert.deepEqual(result.data.starterPackOffer, offer); + assert.equal(result.data.starterPackOffer, undefined); const claimed = await claimOrder(ctx({ uid: 'u', outTradeNo: row.outTradeNo })); - assert.equal(claimed.data, 'ok'); assert.deepEqual(claimed.starterPackOffer, offer); assert.equal(row.state, 2); + assert.equal(claimed.data, 'ok'); assert.equal(claimed.itemid, offer.productId); assert.equal(row.state, 2); const done = await query(ctx({ uid: 'u', outTradeNo: row.outTradeNo })); - assert.equal(done.data.pay_state, 1); assert.deepEqual(done.data.starterPackOffer, offer); + assert.equal(done.data.pay_state, 1); assert.equal(done.data.itemid, offer.productId); }); -test('lucky callback marks only its own round, while historical orders still confirm payment', async () => { +test('lucky callback closes the current offer without matching an order round', async () => { for (const older of [false, true]) { const user = reset(), offer = setOffer(user, 'starter_pack_5', 3000); const row = { outTradeNo: 'wcx_test', openid: 'o', itemid: offer.productId, goodsPrice: 3000, - itemCount: 1, time: NOW, state: 0, ...starterPackOrderSnapshot(user, offer.productId) }; + itemCount: 1, time: NOW, state: 0, ...starterPackOrderMetadata(offer.productId) }; tables.order.push(row); if (older) { user.starter_pack += DAY; user.starterPackRound++; setOffer(user, 'starter_pack_2', 600); } assert.equal((await callback(notification())).ErrCode, 0); assert.equal(row.state, 1); - assert.equal(user.starter_packState === 1, !older); assert.equal(user.pay_user, true); + assert.equal(user.starter_packState, 1); assert.equal(user.pay_user, true); } const user = reset(), row = order(user); - delete row.starterPackOffer; delete row.starterPackExpiresAt; delete row.starterPackBuyRound; user.starterPackRound++; user.starter_pack += DAY; setOffer(user, 'starter_pack_2', 600); assert.equal((await callback(notification())).ErrCode, 0); - assert.notEqual(user.starter_packState, 1); assert.equal(row.state, 1); + assert.equal(user.starter_packState, 1); assert.equal(row.state, 1); const result = await query(ctx({ uid: 'u', outTradeNo: row.outTradeNo })); assert.equal(result.data.starterPackOffer, undefined); }); -test('legacy iOS lucky tiers preserve the locked offer through creation, transfer and paid query', async () => { +test('legacy iOS lucky tiers keep submitted price and SKU without an offer snapshot', async () => { const user = reset(), offer = setOffer(user, 'starter_pack_3', 1200); shared.set('wxaccess_token', 'synthetic'); provider = { prepay_id: 'mock-prepay' }; await customerService(ctx({ FromUserName: 'o', SessionFrom: JSON.stringify({ tpye: 'ios', outTradeNo: 'tier-ios', propName: offer.productId, count: 9, price: 1 }) })); - assert.equal(tables.iosOrder[0].goodsPrice, 1200); assert.equal(tables.iosOrder[0].itemCount, 1); - assert.deepEqual(tables.iosOrder[0].starterPackOffer, offer); + assert.equal(tables.iosOrder[0].goodsPrice, 1); assert.equal(tables.iosOrder[0].itemCount, 9); + assert.equal(tables.iosOrder[0].starterPackOffer, undefined); + assert.equal(tables.iosOrder[0].activityType, 'lucky'); assert.equal(await transfer(ctx({ openid: 'o', time: user.iosTime, outTradeNo: 'tier-ios' })), true); - assert.deepEqual(tables.order[0].starterPackOffer, offer); + assert.equal(tables.order[0].starterPackOffer, undefined); assert.equal(tables.order[0].activityType, 'lucky'); user.starter_pack += DAY; user.starterPackRound++; setOffer(user, 'starter_pack_5', 3000); provider = { trade_state: 'SUCCESS', success_time: new Date(NOW).toISOString() }; const paid = await iosQuery(ctx({ uid: 'u', outTradeNo: 'tier-ios' })); - assert.equal(paid.code, 1); assert.deepEqual(paid.data.starterPackOffer, offer); - assert.notEqual(user.starter_packState, 1); assert.equal(tables.order[0].state, 2); + assert.equal(paid.code, 1); assert.equal(paid.data.itemid, offer.productId); + assert.equal(user.starter_packState, 1); assert.equal(tables.order[0].state, 2); const done = await iosQuery(ctx({ uid: 'u', outTradeNo: 'tier-ios' })); - assert.equal(done.msg, '已经获取到奖励'); assert.deepEqual(done.data.starterPackOffer, offer); + assert.equal(done.msg, '已经获取到奖励'); assert.equal(done.data.itemid, offer.productId); }); test('old orders without snapshots close only a matching migrated legacy offer', async () => { @@ -181,23 +187,23 @@ test('old orders without snapshots close only a matching migrated legacy offer', } }); -test('native Android and iOS orders snapshot the server cycle, ignoring client analytics values', async () => { +test('native Android and iOS orders do not retain client or server cycle fields', async () => { for (const create of [androidOrder, nativeIosOrder]) { const user = reset(); const result = await create(ctx({ uid: 'u', itemid: 'starter_pack', itemCount: 1, itemPrice: 300, starterPackBuyRound: 99, starterPackExpiresAt: 1 })); assert.equal(result.code, 1); - assert.equal(tables.order[0].starterPackBuyRound, 3); - assert.equal(tables.order[0].starterPackExpiresAt, user.starter_pack); + assert.equal(tables.order[0].starterPackBuyRound, undefined); + assert.equal(tables.order[0].starterPackExpiresAt, undefined); assert.equal(events.length, 0); } }); -test('native callback reports the paid order cycle and integer seconds, not a later reactivation', async () => { +test('native callback without a cycle snapshot uses the conservative analytics fallback', async () => { const user = reset(), row = order(user); user.starter_pack = NOW + 2 * DAY; user.starterPackRound = 4; assert.equal((await callback(notification())).ErrCode, 0); - assert.equal(row.state, 1); assert.notEqual(user.starter_packState, 1); assert.equal(user.pay_user, true); - assert.deepEqual(events[0].properties, { order_id: 'wcx_test', buy_round: 3, time_left: 3599 }); + assert.equal(row.state, 1); assert.equal(user.starter_packState, 1); assert.equal(user.pay_user, true); + assert.deepEqual(events[0].properties, { order_id: 'wcx_test', buy_round: 1, time_left: 0 }); assert.equal(events[0].event, 'rookie_gift'); assert.equal(events[0].accountId, 'o'); assert.equal(events[0].distinctId, 'd'); assert.equal(events[0].firstCheckId, 'wcx_test'); }); @@ -216,7 +222,7 @@ test('concurrent and repeated callbacks and paid queries keep a single dedup key test('payment at or beyond expiry still succeeds and reports zero remaining seconds', async () => { for (const expiry of [NOW, NOW - 1000]) { - const user = reset(); order(user, { starterPackExpiresAt: expiry }); + const user = reset(); user.starter_pack = expiry; order(user); assert.equal((await callback(notification())).ErrCode, 0); assert.equal(events[0].properties.time_left, 0); } @@ -227,7 +233,7 @@ test('unpaid queries and other products never send rookie_gift', async () => { assert.equal((await query(ctx({ uid: 'u', outTradeNo: row.outTradeNo }))).code, 0); assert.equal(events.length, 0); row.itemid = 'gold_1'; await callback(notification()); assert.equal(events.length, 0); - assert.deepEqual(starterPackOrderSnapshot(user, 'gold_1'), {}); + assert.deepEqual(starterPackOrderMetadata('gold_1'), {}); }); test('SDK failure cannot block payment; a later query retries the original snapshot', async () => { @@ -254,23 +260,22 @@ test('existing ThinkingData debug routing is retained and account ID works witho test('legacy pending orders use cycle one; missing historical deadline falls back to zero', async () => { const user = reset(); delete user.starterPackRound; - const row = order(user); delete row.starterPackBuyRound; delete row.starterPackExpiresAt; + const row = order(user); await reportRookieGift(row, user, NOW); assert.equal(events[0].properties.buy_round, 1); assert.equal(events[0].properties.time_left, 3599); reset(); const older = order(user, { time: NOW - 5 * DAY }); - delete older.starterPackBuyRound; delete older.starterPackExpiresAt; await reportRookieGift(older, user, NOW); assert.equal(events[0].properties.time_left, 0); }); -test('legacy iOS creation and transfer preserve cycle snapshot even if the user later changes', async () => { +test('legacy iOS creation and transfer omit cycle snapshots', async () => { const user = reset(); shared.set('wxaccess_token', 'synthetic'); provider = { prepay_id: 'mock-prepay' }; await customerService(ctx({ FromUserName: 'o', SessionFrom: JSON.stringify({ tpye: 'ios', outTradeNo: 'old-ios', propName: 'starter_pack', count: 1, price: 300 }) })); assert.equal(tables.iosOrder.length, 1); - assert.equal(tables.iosOrder[0].starterPackBuyRound, 3); - const expiry = user.starter_pack; user.starter_pack = NOW + 2 * DAY; user.starterPackRound = 4; + assert.equal(tables.iosOrder[0].starterPackBuyRound, undefined); + user.starter_pack = NOW + 2 * DAY; user.starterPackRound = 4; assert.equal(await transfer(ctx({ openid: 'o', time: user.iosTime, outTradeNo: 'old-ios' })), true); - assert.equal(tables.order[0].starterPackBuyRound, 3); assert.equal(tables.order[0].starterPackExpiresAt, expiry); + assert.equal(tables.order[0].starterPackBuyRound, undefined); assert.equal(tables.order[0].starterPackExpiresAt, undefined); assert.equal(events.length, 0); }); diff --git a/server/laf-cloud/tests/starter-pack.test.mjs b/server/laf-cloud/tests/starter-pack.test.mjs index e2c4047..9422f17 100644 --- a/server/laf-cloud/tests/starter-pack.test.mjs +++ b/server/laf-cloud/tests/starter-pack.test.mjs @@ -39,7 +39,7 @@ registerHooks({ return next(url, context); }, }); -const { default: activity, starterPackPatch, starterPackOrderSnapshot } = await import('../functions/limitedTimeEvent.ts'); +const { default: activity, starterPackPatch, starterPackOrderMetadata } = await import('../functions/limitedTimeEvent.ts'); const { PAYMENT_PROFILE_RULE } = await import('../functions/paymentProfile.ts'); const { starterPackReward } = await import('../../../assets/Script/module/Config/StarterPack.ts'); function reset(patch = {}) { @@ -134,7 +134,6 @@ test('a displayed offer stays fixed when payment identity and daily factors chan user.pay_user = true; profile(user, 100000, 100000); now += 3600000; await read(); await trigger(); assert.deepEqual(user.starterPackOffer, offer); - assert.equal(starterPackOrderSnapshot(user, 'starter_pack').starterPackOffer.priceFen, 300); }); test('legacy active offers retain their original deadline and use the client 3 yuan reward', async () => { @@ -231,18 +230,13 @@ test('form-encoded failure counts and automatic flags match the client transport } }); -test('order snapshots bind a valid current SKU, price and cycle without reward data', async () => { - const user = reset({ pay_user: true }); profile(user, 3000); await trigger(); - const snapshot = starterPackOrderSnapshot(user, 'starter_pack_5'); - assert.equal(snapshot.starterPackOffer.priceFen, 3000); - assert.equal(snapshot.starterPackOffer.reward, undefined); - assert.equal(snapshot.activityType, 'lucky'); - assert.equal(snapshot.rewardVersion, PAYMENT_PROFILE_RULE.starterPackRewardVersion); - assert.throws(() => starterPackOrderSnapshot(user, 'starter_pack'), /档位/); - user.starter_packState = 1; assert.throws(() => starterPackOrderSnapshot(user, 'starter_pack_5'), /已购买/); - user.starter_packState = 0; now = user.starter_pack; - assert.throws(() => starterPackOrderSnapshot(user, 'starter_pack_5'), /未开放/); - assert.deepEqual(starterPackOrderSnapshot(user, 'other_product'), {}); +test('payment metadata keeps factor labels without price, round or eligibility checks', () => { + for (const itemid of ['starter_pack', 'starter_pack_2', 'starter_pack_3', 'starter_pack_4', 'starter_pack_5']) { + assert.deepEqual(starterPackOrderMetadata(itemid), { + activityType: 'lucky', rewardVersion: PAYMENT_PROFILE_RULE.starterPackRewardVersion, + }); + } + assert.deepEqual(starterPackOrderMetadata('other_product'), {}); }); test('formula ignores rejection state and historical successful price', () => {