From 4018dbfbfcbeb7cccdde2fb99117ab7e52f3c9a7 Mon Sep 17 00:00:00 2001 From: "COMPUTER\\EDY" <249929363@qq.com> Date: Mon, 21 Sep 2026 17:13:35 +0800 Subject: [PATCH] feat: add per-run reborn gift price tiers --- assets/Script/JiaZai.ts | 6 +- assets/Script/Map.ts | 15 ++ assets/Script/Revive.ts | 128 +++++++---- assets/Script/module/Pay/Utils.ts | 4 +- assets/Script/module/Tool/GameTool.ts | 10 +- assets/shop/script/shop.ts | 2 +- server/laf-cloud/functions/login.ts | 7 +- server/laf-cloud/functions/wx/KeFuInfo.ts | 7 + .../laf-cloud/functions/wx/getOrderReward.ts | 9 + .../laf-cloud/functions/wx/iosgetPayInfo.ts | 16 +- .../laf-cloud/functions/wx/iosorderPaySig.ts | 5 + server/laf-cloud/functions/wx/orderPaySig.ts | 5 + server/laf-cloud/rebornGift-tiers.md | 28 +++ tools/test-reborn-gift.cjs | 215 ++++++++++++++++++ 14 files changed, 405 insertions(+), 52 deletions(-) create mode 100644 server/laf-cloud/rebornGift-tiers.md create mode 100644 tools/test-reborn-gift.cjs diff --git a/assets/Script/JiaZai.ts b/assets/Script/JiaZai.ts index d9a04f2d..21053785 100644 --- a/assets/Script/JiaZai.ts +++ b/assets/Script/JiaZai.ts @@ -2648,7 +2648,7 @@ export default class JiaZai extends cc.Component { if (order.itemid == "gold_1" || order.itemid == "gold_2" || order.itemid == "gold_3" || order.itemid == "gold_4" || order.itemid == "gold_5" || order.itemid == "gold_6" || order.itemid == "unlimited_health_bundle_10" || order.itemid == "unlimited_health_bundle_20" - || order.itemid == "unlimited_health_bundle_30" || order.itemid == "month_Card" || order.itemid == "reborn_Gift" + || order.itemid == "unlimited_health_bundle_30" || order.itemid == "month_Card" || /^reborn_Gift(?:_[2-5])?$/.test(order.itemid) || order.itemid == "starter_pack" || order.itemid == "battlepass" ) { if (canUpdateHome()) this.openLoad(); @@ -2678,7 +2678,7 @@ export default class JiaZai extends cc.Component { if (canUpdateHome()) this.openLoad(); this.buyMonthCard(productId, order.outTradeNo); } - else if (order.itemid == "reborn_Gift") { + else if (/^reborn_Gift(?:_[2-5])?$/.test(order.itemid)) { coinTemp = 1000; } else if (order.itemid == "starter_pack") { @@ -2695,7 +2695,7 @@ export default class JiaZai extends cc.Component { let title = "充值补发奖励金币:" + coinTemp; // MiniGameSdk.API.showToast(title); if (order.itemid != "month_Card") - cc.fx.GameTool.shopBuy(productId, true, productId === "starter_pack" ? { outTradeNo: order.outTradeNo } : null); + cc.fx.GameTool.shopBuy(productId, true, productId === "starter_pack" || /^reborn_Gift(?:_[2-5])?$/.test(productId) ? { outTradeNo: order.outTradeNo } : null); // console.log("充值成功获得金币"); let price = parseInt(order.goodsPrice) || 0; // const dataSuccess = { diff --git a/assets/Script/Map.ts b/assets/Script/Map.ts index 408ae08b..a5aae858 100644 --- a/assets/Script/Map.ts +++ b/assets/Script/Map.ts @@ -283,6 +283,21 @@ export default class MapConroler extends cc.Component { // 6. 获取场景管理器引用 // 7. 获取城市排行榜数据 // ============================================ + // 本局成功购买的礼包次数,与金币复活次数 review 独立;重建关卡时归零。 + rebornGiftCount: number = 0; + private rebornGiftOrders: Set = new Set(); + + recordRebornGiftPurchase(order: string) { + if (!order || this.rebornGiftOrders.has(order)) return; + this.rebornGiftOrders.add(order); + this.rebornGiftCount++; + } + + getRebornGiftProduct() { + const tier = Math.min(this.rebornGiftCount, 4); + return { id: tier === 0 ? "reborn_Gift" : "reborn_Gift_" + (tier + 1), price: [600, 800, 1000, 1200, 1500][tier] }; + } + onLoad() { // 测试开关开启时,每次进关都从十连胜开始。 if (cc.fx.GameConfig.forceRainbowWinStreakB) { diff --git a/assets/Script/Revive.ts b/assets/Script/Revive.ts index 11d3f7d9..cb3a179f 100644 --- a/assets/Script/Revive.ts +++ b/assets/Script/Revive.ts @@ -25,6 +25,9 @@ export default class Revive extends cc.Component { private iosPrice: number = 0; private iosProductId: string = ""; private iosCount: number = 1; + private iosMap: MapConroler = null; + private iosUid: string = ""; + private checkingIosOrder: boolean = false; public home: number = 0; onLoad() { @@ -36,12 +39,18 @@ export default class Revive extends cc.Component { if (cc.sys.platform === cc.sys.WECHAT_GAME || cc.sys.platform === cc.sys.BYTEDANCE_GAME) { console.log("从后台进入前台订单号:", cc.fx.GameConfig.GM_INFO.iosReviveOrder); if (cc.fx.GameConfig.GM_INFO.iosReviveOrder != null && cc.fx.GameConfig.GM_INFO.iosReviveOrder != "") { + if (this.checkingIosOrder) return; + this.checkingIosOrder = true; console.log("有苹果订单号,开始轮训"); const iosReviveOrder = cc.fx.GameConfig.GM_INFO.iosReviveOrder; this.openLoad(); - this.btn_Touch = true; + this.btn_Touch = false; + const purchaseMap = this.iosMap; + const purchaseUid = this.iosUid || cc.fx.GameConfig.GM_INFO.uid; Utils.getIosPayInfo(iosReviveOrder, (data) => { + this.checkingIosOrder = false; + if (purchaseUid !== cc.fx.GameConfig.GM_INFO.uid) return; console.log("获得轮训结果:", data); const iosID = data.data?.payment_name || this.iosProductId; let iosAmount = data.data?.goodsPrice || this.iosPrice; @@ -55,12 +64,10 @@ export default class Revive extends cc.Component { this.btn_Touch = true; console.log("_________正式发货"); MiniGameSdk.API.showToast("充值成功"); - cc.fx.GameTool.shopBuy(iosID, false); - this.revivew() - if (iosID == "reborn_Gift") { - this.buyGift(); - } - cc.fx.GameConfig.GM_INFO.iosReviveOrder = null; + this.completePurchase(iosID, iosReviveOrder, purchaseMap, purchaseUid); + Utils.setPayInfo(() => {}, iosReviveOrder); + if (cc.fx.GameConfig.GM_INFO.iosReviveOrder === iosReviveOrder) + cc.fx.GameConfig.GM_INFO.iosReviveOrder = null; //console.log("充值成功获得金币"); this.updateGameCoin(); @@ -81,7 +88,8 @@ export default class Revive extends cc.Component { if (iosID != undefined && iosID != "" && iosID != null) { cc.fx.GameTool.shushu_Track("payment_fail", dataFail); } - cc.fx.GameConfig.GM_INFO.iosReviveOrder = null; + if (cc.fx.GameConfig.GM_INFO.iosReviveOrder === iosReviveOrder) + cc.fx.GameConfig.GM_INFO.iosReviveOrder = null; } else if (data.code == 2) { this.closeLoad(); @@ -112,10 +120,34 @@ export default class Revive extends cc.Component { return; } this.node.active = true; + this.updatePrice(); this.addListener(); this.btn_Touch = true; } + private updatePrice() { + const map = MapConroler._instance; + const price = this.node.getChildByName("price"); + const label = price && price.getComponent(cc.Label); + if (!map || !label) return; + // 固定数字右边缘,让两位数向左展开,保留旁边独立的“元”节点。 + price.x += price.width * (1 - price.anchorX); + price.anchorX = 1; + label.string = String(map.getRebornGiftProduct().price / 100); + } + + private completePurchase(productId: string, order: string, purchaseMap: MapConroler, uid: string) { + if (uid !== cc.fx.GameConfig.GM_INFO.uid || !order || !/^reborn_Gift(?:_[2-5])?$/.test(productId)) return; + const currentRun = purchaseMap && cc.isValid(purchaseMap, true) && MapConroler._instance === purchaseMap; + if (currentRun) purchaseMap.recordRebornGiftPurchase(order); + if (cc.fx.GameTool.shopBuy(productId, false, { outTradeNo: order }) === false) return; + this.buyGift(); + // 旧局订单仍发奖励,但不能复活新局或提高新局的礼包价格。 + if (currentRun) { + this.revivew(purchaseMap); + } + } + addListener() { if (cc.sys.platform === cc.sys.WECHAT_GAME || cc.sys.platform === cc.sys.BYTEDANCE_GAME) { if (this.onShowListener) { @@ -141,9 +173,8 @@ export default class Revive extends cc.Component { // update (dt) {} } //发奖励 - revivew() { + revivew(map: MapConroler) { cc.fx.GameConfig.GM_INFO.revive = 0; - const map = MapConroler && MapConroler._instance; if (map) map.setPropNum(); const jiazaiNode = cc.find("Canvas"); // 假设 JiaZai 挂在 Canvas 节点 const jiazaiComp = jiazaiNode && jiazaiNode.getComponent(JiaZai); @@ -166,7 +197,7 @@ export default class Revive extends cc.Component { this.node.active = false; } }, 200); - if (map && cc.isValid(map)) map.runRewiveCopy(false); + if (map && cc.isValid(map)) map.runRewiveCopy(); } buyProduct(customData) { // cc.fx.GameTool.shopBuy("revive", false); @@ -174,17 +205,19 @@ export default class Revive extends cc.Component { if (!this.btn_Touch) { return; } - this.btn_Touch = false; - const productId = "reborn_Gift"; - let id = "10011"; - let price = 100; - let count = 1; - id = productId; - switch (productId) { - case "reborn_Gift": - price = 600; - break; + if (cc.fx.GameConfig.GM_INFO.iosReviveOrder) { + this.onShow(); + return; } + const purchaseMap = MapConroler._instance; + if (!purchaseMap || !cc.isValid(purchaseMap, true)) return; + const purchaseUid = cc.fx.GameConfig.GM_INFO.uid; + this.btn_Touch = false; + const product = purchaseMap.getRebornGiftProduct(); + const productId = product.id; + const id = productId; + const price = product.price; + const count = 1; console.log("获得商品id:", id, count, price); // 判断设备系统 let systemType = "Android"; @@ -203,8 +236,7 @@ export default class Revive extends cc.Component { // MiniGameSdk.API.showToast("IOS系统暂不支持支付"); // this.btn_Touch = true; this.openLoad(); - this.btn_Touch = true; - this.iosOldPay(price, productId); + this.iosOldPay(price, productId, purchaseMap, purchaseUid); } else { // MiniGameSdk.API.showToast("充值成功"); @@ -220,9 +252,9 @@ export default class Revive extends cc.Component { // }, 500); this.openLoad(); - this.btn_Touch = true; console.log("7.14_____________________", "调用充值接口"); Utils.buyProp(id, count, price, systemType, productId, (res) => { + if (purchaseUid !== cc.fx.GameConfig.GM_INFO.uid) return; console.log("获得充值结果", res); if (res == null) { MiniGameSdk.API.showToast("充值失败"); @@ -243,7 +275,7 @@ export default class Revive extends cc.Component { console.log("错误原因:", res.errMsg); if (res.errCode == 16) { // console.log("走ios旧支付"); - this.iosOldPay(price, productId); + this.iosOldPay(price, productId, purchaseMap, purchaseUid); return; } MiniGameSdk.API.showToast("充值失败"); @@ -266,7 +298,15 @@ export default class Revive extends cc.Component { return; } else { + const orderNumber = res.outTradeNo; + if (!orderNumber) { + this.btn_Touch = true; + this.closeLoad(); + MiniGameSdk.API.showToast("未获取到充值订单,请稍后重试"); + return; + } Utils.getPayInfo((data) => { + if (purchaseUid !== cc.fx.GameConfig.GM_INFO.uid) return; console.log("7.14_______________充值成功,准备轮训"); console.log("获得轮训结果:", data); this.closeLoad(); @@ -274,7 +314,7 @@ export default class Revive extends cc.Component { this.btn_Touch = true; MiniGameSdk.API.showToast("取消充值"); const dataFail2 = { - outTradeNo: Utils.outTradeNo, + outTradeNo: orderNumber, pay_amount: price, payment_name: productId, payment_num: 1, @@ -284,7 +324,8 @@ export default class Revive extends cc.Component { cc.fx.GameTool.shushu_Track("payment_fail", dataFail2); } else if (data.data.pay_state == 2) { - this.btn_Touch = true; + if (cc.isValid(purchaseMap, true) && MapConroler._instance === purchaseMap) + purchaseMap.recordRebornGiftPurchase(orderNumber); // const dataSuccess = { // outTradeNo: Utils.outTradeNo, // pay_amount: price, @@ -294,41 +335,40 @@ export default class Revive extends cc.Component { // } // cc.fx.GameTool.shushu_Track("payment", dataSuccess); let name = "购买金币道具:" + productId; - console.log("引力付费透传", price, Utils.outTradeNo, name); - MiniGameSdk.API.yinli_Pay(price, Utils.outTradeNo, name) + console.log("引力付费透传", price, orderNumber, name); + MiniGameSdk.API.yinli_Pay(price, orderNumber, name) console.log("7.14_______________充值成功,轮训成功,准备发货"); + this.completePurchase(productId, orderNumber, purchaseMap, purchaseUid); Utils.setPayInfo( (res) => { + if (purchaseUid !== cc.fx.GameConfig.GM_INFO.uid) return; + this.btn_Touch = true; console.log("设置轮训结果:", res); if (res.code === 1) { console.log("7.14_________正式发货"); MiniGameSdk.API.showToast("充值成功"); - cc.fx.GameTool.shopBuy(productId, false); - this.revivew(); - if (productId == "reborn_Gift") { - this.buyGift(); - } //console.log("充值成功获得金币"); } else { - MiniGameSdk.API.showToast("网络异常,充值奖励将在登录后再次发放"); + MiniGameSdk.API.showToast("充值已到账,订单确认将在登录后重试"); const dataFail4 = { - outTradeNo: Utils.outTradeNo, + outTradeNo: orderNumber, pay_amount: price, payment_name: productId, payment_num: 1, type: systemType, - fail_reason: "成功付款,但是发货时请求服务器失败,充值成功未发货", + fail_reason: "充值已到账,确认发货请求失败", } cc.fx.GameTool.shushu_Track("payment_fail", dataFail4); } this.updateGameCoin(); - }, Utils.outTradeNo) + if (cc.isValid(this, true)) this.updatePrice(); + }, orderNumber) } else { const dataFail3 = { - outTradeNo: Utils.outTradeNo, + outTradeNo: orderNumber, pay_amount: price, payment_name: productId, payment_num: 1, @@ -339,18 +379,20 @@ export default class Revive extends cc.Component { this.btn_Touch = true; this.updateGameCoin(); } - }) + }, orderNumber) } }); } } openLoad() { + if (!cc.isValid(this, true)) return; const canvas = cc.find("Canvas"); const loading = canvas && canvas.getChildByName("Loading"); if (loading) LoadingCatAnimation.play(loading); } closeLoad() { + if (!cc.isValid(this, true)) return; const canvas = cc.find("Canvas"); const loading = canvas && canvas.getChildByName("Loading"); if (loading) LoadingCatAnimation.stop(loading); @@ -407,7 +449,7 @@ export default class Revive extends cc.Component { this.onShow(); } - iosOldPay(price, productId) { + iosOldPay(price, productId, purchaseMap: MapConroler, uid: string) { // 旧版本ios支付 let iosPayInfo = { price: price, @@ -417,6 +459,8 @@ export default class Revive extends cc.Component { this.iosPrice = price; this.iosProductId = productId; this.iosCount = 1; + this.iosMap = purchaseMap; + this.iosUid = uid; console.log("准备跳客服回话:"); console.log(this.iosProductId); Utils.GoKEFu(iosPayInfo, (res) => { @@ -425,6 +469,8 @@ export default class Revive extends cc.Component { } else { console.log("客服回话失败"); + this.btn_Touch = true; + cc.fx.GameConfig.GM_INFO.iosReviveOrder = null; this.closeLoad(); } }); diff --git a/assets/Script/module/Pay/Utils.ts b/assets/Script/module/Pay/Utils.ts index b8124459..7ea957dc 100644 --- a/assets/Script/module/Pay/Utils.ts +++ b/assets/Script/module/Pay/Utils.ts @@ -253,7 +253,7 @@ export default class Utils { cc.fx.GameConfig.GM_INFO.iosMonthOrder = null; cc.fx.GameConfig.GM_INFO.iosMonthOrder = order; } - else if (iosPayInfo.payment_name == "reborn_Gift") { + else if (/^reborn_Gift(?:_[2-5])?$/.test(iosPayInfo.payment_name)) { cc.fx.GameConfig.GM_INFO.iosReviveOrder = null; cc.fx.GameConfig.GM_INFO.iosReviveOrder = order; } @@ -336,7 +336,7 @@ export default class Utils { const orderNow = order; console.log("请求uid:" + Utils.uid); console.log("outTradeNo:" + orderNow); - Utils.POST("wx/iosgetPayInfo", { outTradeNo: orderNow, uid: cc.fx.GameConfig.GM_INFO.uid }, res => { + Utils.POST("wx/iosgetPayInfo", { outTradeNo: orderNow, uid: cc.fx.GameConfig.GM_INFO.uid, rebornGiftVersion: 1 }, res => { console.log("查询字符结果IOS"); console.log(res); // 清除超时定时器 diff --git a/assets/Script/module/Tool/GameTool.ts b/assets/Script/module/Tool/GameTool.ts index cc402457..48a3d25e 100644 --- a/assets/Script/module/Tool/GameTool.ts +++ b/assets/Script/module/Tool/GameTool.ts @@ -999,6 +999,9 @@ var GameTool = { //商城购买 shopBuy(productId, compensate, orderContext: any = null) { + const rebornOrderKey = /^reborn_Gift(?:_[2-5])?$/.test(productId) && orderContext?.outTradeNo + ? "RebornGiftGranted_" + cc.fx.GameConfig.GM_INFO.uid + "_" + orderContext.outTradeNo : ""; + if (rebornOrderKey && cc.fx.StorageMessage.getStorage(rebornOrderKey)) return false; const starterOrderKey = productId === "starter_pack" && orderContext?.outTradeNo ? "StarterPackGranted_" + cc.fx.GameConfig.GM_INFO.uid + "_" + orderContext.outTradeNo : ""; if (starterOrderKey && cc.fx.StorageMessage.getStorage(starterOrderKey)) return; @@ -1234,9 +1237,13 @@ var GameTool = { ] break; case "reborn_Gift": + case "reborn_Gift_2": + case "reborn_Gift_3": + case "reborn_Gift_4": + case "reborn_Gift_5": cc.fx.GameTool.changeCoin(1000); coin = 1000; - price = 600; + price = { reborn_Gift: 600, reborn_Gift_2: 800, reborn_Gift_3: 1000, reborn_Gift_4: 1200, reborn_Gift_5: 1500 }[productId]; rewardData = [ { type: "coin", count: coin }, { type: "freeze", count: 1 }, @@ -1250,6 +1257,7 @@ var GameTool = { "price": price } this.getShopProp(propData3, compensate); + if (rebornOrderKey) cc.fx.StorageMessage.setStorage(rebornOrderKey, true); break; case "starter_pack": { const reward = starterPackReward(); diff --git a/assets/shop/script/shop.ts b/assets/shop/script/shop.ts index c9eb3c62..a85cb264 100644 --- a/assets/shop/script/shop.ts +++ b/assets/shop/script/shop.ts @@ -393,7 +393,7 @@ export default class NewClass extends cc.Component { const iosID = data.data?.payment_name || this.iosProductId; let iosAmount = data.data?.goodsPrice || this.iosPrice; iosAmount = parseInt(iosAmount); - if (iosID == "reborn_Gift" || iosID == "month_Card" || iosID == "starter_pack") { + if (/^reborn_Gift(?:_[2-5])?$/.test(iosID) || iosID == "month_Card" || iosID == "starter_pack") { console.log("商城检测到是月卡返回"); return; } diff --git a/server/laf-cloud/functions/login.ts b/server/laf-cloud/functions/login.ts index c501f2a3..e7e7a775 100644 --- a/server/laf-cloud/functions/login.ts +++ b/server/laf-cloud/functions/login.ts @@ -476,6 +476,10 @@ async function getIosOrder(openid, ctx) { await db.collection("users").where({ _id: res1.data._id }).update({ pay_user: true }); break; case "reborn_Gift": + case "reborn_Gift_2": + case "reborn_Gift_3": + case "reborn_Gift_4": + case "reborn_Gift_5": db.collection("users").where({ _id: res1.data._id }).update({ rebornGiftTime: Date.now(), rebornGiftCount: 0, pay_user: true }); break; case "starter_pack": @@ -486,7 +490,8 @@ async function getIosOrder(openid, ctx) { break; } // 月卡补单确认成功后进入待客户端补发状态。 - if (res.data[i].itemid === "month_Card" || jungleTier || /^battlepass(?:_30|_12)?$/.test(res.data[i].itemid)) { + if (res.data[i].itemid === "month_Card" || jungleTier || /^battlepass(?:_30|_12)?$/.test(res.data[i].itemid) + || /^reborn_Gift(?:_[2-5])?$/.test(res.data[i].itemid)) { await db.collection("order").where({ outTradeNo: res.data[i].outTradeNo }).update({ state: 1 }); } } else { diff --git a/server/laf-cloud/functions/wx/KeFuInfo.ts b/server/laf-cloud/functions/wx/KeFuInfo.ts index 292dff75..58960294 100644 --- a/server/laf-cloud/functions/wx/KeFuInfo.ts +++ b/server/laf-cloud/functions/wx/KeFuInfo.ts @@ -102,6 +102,8 @@ async function frontsign(data) { return signature } const propName = { + reborn_Gift: "复活礼包6元", reborn_Gift_2: "复活礼包8元", reborn_Gift_3: "复活礼包10元", + reborn_Gift_4: "复活礼包12元", reborn_Gift_5: "复活礼包15元", battlepass: '基础战令', battlepass_30: '高级战令', battlepass_12: '战令升级', gold_1: "金币包1", gold_2: "金币包2", @@ -151,6 +153,11 @@ async function PostMsg(OpenId, msg) { return "ok" } async function PostImg(OpenId, outTradeNo, name, count, price, passInput: any = {}) { + const rebornPrice = { reborn_Gift: 600, reborn_Gift_2: 800, reborn_Gift_3: 1000, reborn_Gift_4: 1200, reborn_Gift_5: 1500 }; + if (/^reborn_Gift(?:_[2-5])?$/.test(name)) { + count = 1; + price = rebornPrice[name]; + } let passFields: any = {}; if (Object.prototype.hasOwnProperty.call(PASS_PRICES, name)) { const owner = await db.collection('users').where({ openid: OpenId }).getOne(); diff --git a/server/laf-cloud/functions/wx/getOrderReward.ts b/server/laf-cloud/functions/wx/getOrderReward.ts index 089d55c2..106acc87 100644 --- a/server/laf-cloud/functions/wx/getOrderReward.ts +++ b/server/laf-cloud/functions/wx/getOrderReward.ts @@ -10,6 +10,15 @@ export default async function (ctx: FunctionContext) { if (!order.data) { return { code: 0, data: null, msg: "未获取到充值订单" }; } + if (/^reborn_Gift(?:_[2-5])?$/.test(order.data.itemid)) { + const user = await db.collection("users").where({ _id: ctx.body.uid }).getOne(); + if (!user.data || user.data.openid !== order.data.openid) { + return { code: 0, data: null, msg: "充值订单不属于当前玩家" }; + } + if (order.data.state !== 1 && order.data.state !== 2) { + return { code: 0, data: null, msg: "充值尚未成功" }; + } + } // 月卡由 monthlyCard 在成功加时后完成订单,避免两次请求之间断线导致无法补发。 if (isUpgradeOrder(order.data)) return { code: 0, msg: '请在新版战令中完成发放' }; if (order.data.itemid !== "month_Card") { diff --git a/server/laf-cloud/functions/wx/iosgetPayInfo.ts b/server/laf-cloud/functions/wx/iosgetPayInfo.ts index 5cfab3cf..56d81f90 100644 --- a/server/laf-cloud/functions/wx/iosgetPayInfo.ts +++ b/server/laf-cloud/functions/wx/iosgetPayInfo.ts @@ -24,6 +24,9 @@ export default async function (ctx: FunctionContext) { return { code: 0, data: null, msg: "未获取到充值信息" }; } let payment_name = ret.data?.itemid; + const rebornGift = /^reborn_Gift(?:_[2-5])?$/.test(payment_name); + const rebornPending = rebornGift && Number(ctx.body.rebornGiftVersion) === 1; + if (rebornGift && !uid) return { code: 0, data: null, msg: "未获取到玩家数据" }; if (isUpgradeOrder(ret.data) && ret.data.state === 2) return { code: 1, data: { payment_name, goodsPrice: ret.data.goodsPrice } }; if (res && payment_name === "starter_pack") { try { @@ -45,6 +48,9 @@ export default async function (ctx: FunctionContext) { if (!res1.data) { return { code: 0, data: null, msg: "未获取到玩家数据" }; } + if (rebornGift && ret.data.openid !== res1.data.openid) { + return { code: 0, data: null, msg: "充值订单不属于当前玩家" }; + } paidUser = res1.data; if (!iosOrder) { iosOrder = {}; @@ -65,6 +71,10 @@ export default async function (ctx: FunctionContext) { await db.collection("users").where({ _id: uid }).update({ pay_user: true }); break; case "reborn_Gift": + case "reborn_Gift_2": + case "reborn_Gift_3": + case "reborn_Gift_4": + case "reborn_Gift_5": db.collection("users").where({ _id: uid }).update({ rebornGiftTime: Date.now(), rebornGiftCount: 0, pay_user: true }); break; case "starter_pack": @@ -75,11 +85,11 @@ export default async function (ctx: FunctionContext) { break; } } - // 月卡等待 monthlyCard 完成加时后再进入 state=2,其他商品保持原逻辑。 + // 新版复活礼包等待客户端去重发奖后确认;旧客户端保持直接完成订单。 await db.collection("order").where({ outTradeNo: outTradeNo, ...(isUpgradeOrder(ret.data) ? { state: ret.data.state } : {}) }).update({ - state: payment_name === "month_Card" || isUpgradeOrder(ret.data) ? 1 : 2, + state: payment_name === "month_Card" || isUpgradeOrder(ret.data) || rebornPending ? 1 : 2, chargeTime: new Date(Date.now() + 8 * 3600000), - ...(payment_name === "month_Card" || isUpgradeOrder(ret.data) ? {} : { getTime: new Date(Date.now() + 8 * 3600000) }), + ...(payment_name === "month_Card" || isUpgradeOrder(ret.data) || rebornPending ? {} : { getTime: new Date(Date.now() + 8 * 3600000) }), }); diff --git a/server/laf-cloud/functions/wx/iosorderPaySig.ts b/server/laf-cloud/functions/wx/iosorderPaySig.ts index d2ac1d92..989336dd 100644 --- a/server/laf-cloud/functions/wx/iosorderPaySig.ts +++ b/server/laf-cloud/functions/wx/iosorderPaySig.ts @@ -17,6 +17,11 @@ export default async function (ctx: FunctionContext) { let itemid = ctx.body.itemid; let itemCount = ctx.body.itemCount; let itemPrice = ctx.body.itemPrice; + const rebornPrice = { reborn_Gift: 600, reborn_Gift_2: 800, reborn_Gift_3: 1000, reborn_Gift_4: 1200, reborn_Gift_5: 1500 }; + if (/^reborn_Gift(?:_[2-5])?$/.test(itemid)) { + itemCount = 1; + itemPrice = rebornPrice[itemid]; + } let user = await db.collection("users").where({ _id: uid }).getOne(); if (user.data == null) { return { code: 0, data: null, msg: "未获取到玩家数据" }; diff --git a/server/laf-cloud/functions/wx/orderPaySig.ts b/server/laf-cloud/functions/wx/orderPaySig.ts index 9e9d5058..b056a6a9 100644 --- a/server/laf-cloud/functions/wx/orderPaySig.ts +++ b/server/laf-cloud/functions/wx/orderPaySig.ts @@ -17,6 +17,11 @@ export default async function (ctx: FunctionContext) { let itemid = ctx.body.itemid; let itemCount = ctx.body.itemCount; let itemPrice = ctx.body.itemPrice; + const rebornPrice = { reborn_Gift: 600, reborn_Gift_2: 800, reborn_Gift_3: 1000, reborn_Gift_4: 1200, reborn_Gift_5: 1500 }; + if (/^reborn_Gift(?:_[2-5])?$/.test(itemid)) { + itemCount = 1; + itemPrice = rebornPrice[itemid]; + } let user = await db.collection("users").where({ _id: uid }).getOne(); if (user.data == null) { return { code: 0, data: null, msg: "未获取到玩家数据" }; diff --git a/server/laf-cloud/rebornGift-tiers.md b/server/laf-cloud/rebornGift-tiers.md new file mode 100644 index 00000000..78404941 --- /dev/null +++ b/server/laf-cloud/rebornGift-tiers.md @@ -0,0 +1,28 @@ +# 当局复活礼包阶梯 + +| 本局成功购买次数(购买前) | 商品 ID | 价格(分) | +| --- | --- | --- | +| 0 | reborn_Gift | 600 | +| 1 | reborn_Gift_2 | 800 | +| 2 | reborn_Gift_3 | 1000 | +| 3 | reborn_Gift_4 | 1200 | +| 4 及以上 | reborn_Gift_5 | 1500 | + +每档维持 1000 金币、冰冻/锤子/魔棒各 1,以及当前局复活加 30 秒。主线、助力和无尽模式共用规则。 + +## 计数与补单 + +- `Map.rebornGiftCount` 只记录本局已确认支付的订单,以订单号去重;新建关卡的 Map 时归零。切后台不重建 Map,因此保留次数。 +- 金币复活继续使用独立的 `GM_INFO.review`;礼包购买不读写它。 +- 回调捕获下单时的 Map。若已换局,仍发金币/道具,但不复活新局、不递增新局次数。 +- `GameTool.shopBuy` 沿用现有发奖方式,为复活礼包增加按用户及订单号保存的去重标记,登录补单也使用同一标记。 +- 新版旧 iOS 客服支付查询携带 `rebornGiftVersion: 1`,服务器先标记待领取,客户端发奖后再确认;旧客户端保持原有完成方式。 +- 不新增对局登记接口。云端旧 `rebornGiftCount` 仍返回 0、保持不限购;当局阶梯由 Map 管理,服务器按商品 ID 强制对应价格和数量 1,不独立校验当局购买序号。 + +## 发布范围 + +先发布云函数:`wx/orderPaySig`、`wx/iosorderPaySig`、`wx/KeFuInfo`、`wx/iosgetPayInfo`、`wx/getOrderReward`、`login`,再发布客户端。支付平台须存在上述五个商品,仓库无法验证平台配置。 + +客户端直接更新 `Lose/Revive/price` 的数字文本,保留现有字体及独立的“元”节点;数字固定右边缘,两位数向左展开。 + +验证命令:`node --test tools/test-reborn-gift.cjs`(仓库根目录)。测试使用模拟订单,不会发起真实支付。 diff --git a/tools/test-reborn-gift.cjs b/tools/test-reborn-gift.cjs new file mode 100644 index 00000000..8c2b8dcf --- /dev/null +++ b/tools/test-reborn-gift.cjs @@ -0,0 +1,215 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const crypto = require('node:crypto'); +const { loadMembers, loadModule } = require('./new-player-ab-test-support.cjs'); + +const products = ['reborn_Gift', 'reborn_Gift_2', 'reborn_Gift_3', 'reborn_Gift_4', 'reborn_Gift_5']; +const prices = [600, 800, 1000, 1200, 1500]; +const MapController = loadMembers('assets/Script/Map.ts', [ + 'rebornGiftCount', 'rebornGiftOrders', 'recordRebornGiftPurchase', 'getRebornGiftProduct', +]); + +function fixture() { + const storage = new Map(), calls = { rewards: [], revived: [], acknowledgements: [], products: [], props: [] }; + const info = { uid: 'u', coin: 100, review: 3, iosCanPay: true }; + const map = new MapController(); + const cc = { + fx: { GameConfig: { GM_INFO: info }, StorageMessage: { + getStorage: key => storage.get(key), setStorage: (key, value) => storage.set(key, value), + } }, + sys: { platform: 1, WECHAT_GAME: 1, BYTEDANCE_GAME: 2 }, + isValid: value => !!value && !value.destroyed, find: () => null, + }; + MapController._instance = map; + map.SceneManager = { openRewardWindow: rewards => calls.rewards.push(rewards) }; + const MiniGameSdk = { API: { showToast() {}, yinli_Pay() {}, shushu_SetSuperProperties() {} } }; + const Tool = loadMembers('assets/Script/module/Tool/GameTool.ts', ['shopBuy'], { cc, MapConroler: MapController, MiniGameSdk }); + cc.fx.GameTool = Object.assign(new Tool(), { + changeCoin: amount => { info.coin += amount; }, + getShopProp: props => calls.props.push(props), shushu_Track() {}, + }); + const Utils = { + outTradeNo: 'unrelated-order', + buyProp: (id, count, price, platform, product, callback) => { calls.products.push({ id, count, price }); calls.pay = callback; }, + getPayInfo: (callback, order) => { calls.poll = callback; calls.polledOrder = order; }, + getIosPayInfo: (order, callback) => { calls.iosPoll = callback; calls.polledOrder = order; }, + setPayInfo: (callback, order) => { calls.acknowledgements.push(order); calls.ack = callback; }, + GoKEFu: (data, callback) => { calls.iosRequest = data; info.iosReviveOrder = 'ios-order'; callback('success'); }, + }; + const Revive = loadMembers('assets/Script/Revive.ts', [ + 'iosMap', 'iosUid', 'checkingIosOrder', 'iosPrice', 'iosProductId', 'iosCount', + 'buyProduct', 'onShow', 'iosOldPay', 'completePurchase', + ], { cc, MapConroler: MapController, MiniGameSdk, Utils, wx: { getSystemInfoSync: () => ({ platform: 'android' }) }, console: { log() {}, error() {} } }); + const revive = Object.assign(new Revive(), { + btn_Touch: true, openLoad() {}, closeLoad() {}, buyGift() {}, updateGameCoin() {}, updatePrice() {}, openConfirmBox() {}, + revivew: source => calls.revived.push(source), + }); + return { map, revive, info, calls, cc, Utils, newMap: () => { + const next = new MapController(); next.SceneManager = map.SceneManager; MapController._instance = next; return next; + } }; +} + +test('one run advances through five prices, caps at 15 yuan and leaves coin revival untouched', () => { + const f = fixture(); + for (let i = 0; i < 8; i++) { + const tier = Math.min(i, 4), offer = f.map.getRebornGiftProduct(); + assert.equal(offer.id, products[tier]); assert.equal(offer.price, prices[tier]); + f.revive.completePurchase(offer.id, `order-${i}`, f.map, 'u'); + assert.equal(f.map.rebornGiftCount, i + 1); + assert.equal(f.calls.props[i].price, prices[tier]); + assert.equal(f.calls.props[i].freeze, 1); assert.equal(f.calls.props[i].hammer, 1); assert.equal(f.calls.props[i].magic_wand, 1); + } + assert.equal(f.info.coin, 8100); assert.equal(f.info.review, 3); + assert.equal(f.calls.revived.length, 8); + assert.equal(f.newMap().getRebornGiftProduct().price, 600); +}); + +test('duplicate callbacks and login compensation grant each order only once', () => { + const f = fixture(); + f.revive.completePurchase('reborn_Gift', 'same', f.map, 'u'); + f.revive.completePurchase('reborn_Gift', 'same', f.map, 'u'); + f.cc.fx.GameTool.shopBuy('reborn_Gift', true, { outTradeNo: 'same' }); + assert.equal(f.info.coin, 1100); assert.equal(f.map.rebornGiftCount, 1); assert.equal(f.calls.revived.length, 1); + for (const product of products) f.cc.fx.GameTool.shopBuy(product, true, { outTradeNo: product }); + assert.equal(f.info.coin, 6100); assert.equal(f.map.rebornGiftCount, 1); +}); + +test('old-run payment grants rewards without reviving or advancing a new run; changed account is ignored', () => { + const f = fixture(); const next = f.newMap(); + f.revive.completePurchase('reborn_Gift_3', 'late', f.map, 'u'); + assert.equal(f.info.coin, 1100); assert.equal(next.rebornGiftCount, 0); assert.equal(f.calls.revived.length, 0); + f.info.uid = 'other'; f.revive.completePurchase('reborn_Gift_4', 'other-account', f.map, 'u'); + assert.equal(f.info.coin, 1100); +}); + +test('ordinary payment captures its order and run, blocks repeated clicks and counts verified success once', () => { + const f = fixture(); + f.revive.buyProduct(null); f.revive.buyProduct(null); + assert.equal(f.calls.products.length, 1); assert.equal(f.map.rebornGiftCount, 0); + f.calls.pay({ outTradeNo: 'paid-order' }); assert.equal(f.calls.polledOrder, 'paid-order'); + f.calls.poll({ code: 1, data: { pay_state: 2 } }); + assert.equal(f.map.rebornGiftCount, 1); assert.equal(f.info.coin, 1100); + assert.deepEqual(f.calls.acknowledgements, ['paid-order']); + f.calls.ack({ code: 1 }); assert.equal(f.map.rebornGiftCount, 1); +}); + +test('cancelled, failed and unconfirmed payments keep the six-yuan tier', () => { + for (const response of [null, { err: true, errCode: -2 }, { err: true, errCode: -1 }]) { + const f = fixture(); f.revive.buyProduct(null); f.calls.pay(response); + assert.equal(f.map.rebornGiftCount, 0); assert.equal(f.revive.btn_Touch, true); + } + const f = fixture(); f.revive.buyProduct(null); f.calls.pay({ outTradeNo: 'unknown' }); + f.calls.poll({ code: 0, data: { pay_state: -1 } }); + assert.equal(f.map.rebornGiftCount, 0); assert.equal(f.info.coin, 100); + const missing = fixture(); missing.revive.buyProduct(null); missing.calls.pay({}); + assert.equal(missing.calls.polledOrder, undefined); assert.equal(missing.revive.btn_Touch, true); +}); + +test('iOS background return keeps the run; overlapping polls and late callbacks do not double grant', () => { + const f = fixture(); f.map.recordRebornGiftPurchase('earlier'); + f.revive.iosOldPay(800, 'reborn_Gift_2', f.map, 'u'); + assert.equal(f.calls.iosRequest.price, 800); + f.revive.onShow(); const poll = f.calls.iosPoll; f.revive.onShow(); assert.equal(f.calls.iosPoll, poll); + poll({ code: 1, data: { payment_name: 'reborn_Gift_2', goodsPrice: 800 } }); + assert.equal(f.map.rebornGiftCount, 2); assert.equal(f.info.coin, 1100); + assert.deepEqual(f.calls.acknowledgements, ['ios-order']); + f.info.iosReviveOrder = 'late-ios'; const next = f.newMap(); f.revive.onShow(); + f.calls.iosPoll({ code: 1, data: { payment_name: 'reborn_Gift_3', goodsPrice: 1000 } }); + assert.equal(next.rebornGiftCount, 0); assert.equal(f.info.coin, 2100); assert.equal(f.calls.revived.length, 1); +}); + +test('price node updates the amount on every opening, including 10/12/15 yuan', () => { + const data = JSON.parse(require('node:fs').readFileSync('assets/lose/prefab/Lose.prefab', 'utf8')); + const node = data.find(n => n.__type__ === 'cc.Node' && n._name === 'price'); + assert.ok(node._components.some(ref => data[ref.__id__].__type__ === 'cc.Label')); + assert.ok(data[data.findIndex(n => n === node)]._parent); + assert.ok(data[node._parent.__id__]._children.some(ref => data[ref.__id__]._name === 'yuan')); + const label = {}, price = { x: node._trs.array[0], width: node._contentSize.width, anchorX: node._anchorPoint.x, getComponent: () => label }; + const right = price.x + price.width * (1 - price.anchorX); + const map = new MapController(); MapController._instance = map; + const cc = { Label: {} }; + const Panel = loadMembers('assets/Script/Revive.ts', ['updatePrice'], { cc, MapConroler: MapController }); + const panel = new Panel(); panel.node = { getChildByName: name => name === 'price' ? price : null }; + for (let i = 0; i < 7; i++) { + map.rebornGiftCount = i; panel.updatePrice(); + assert.equal(label.string, String(prices[Math.min(i, 4)] / 100)); + assert.equal(price.x, right); assert.equal(price.anchorX, 1); + price.width = label.string.length * 44; + } +}); + +function serverFixture() { + const state = { orders: [], user: { _id: 'u', openid: 'o', session_key: 'session' }, requests: [] }; + const matches = (row, query) => Object.entries(query).every(([key, value]) => row[key] === value); + const db = { collection: name => ({ + add: async row => { state.orders.push({ ...row, collection: name }); return {}; }, + where: query => ({ + getOne: async () => ({ data: (name === 'users' ? [state.user] : state.orders).find(row => matches(row, query)) }), + update: async values => { (name === 'users' ? [state.user] : state.orders).filter(row => matches(row, query)).forEach(row => Object.assign(row, values)); return {}; }, + }), + }) }; + const cloud = { database: () => db, shared: { get: key => key === 'wxaccess_token' ? 'token' : {}, set() {} }, storage: { bucket: () => ({ readFile: async () => ({ Body: { transformToString: async () => 'key' } }) }) } }; + const deps = { + '@lafjs/cloud': { default: cloud }, '@/limitedTimeEvent': { starterPackOrderSnapshot: () => ({}), reportRookieGift: async () => {} }, + '@/passCheckUpgrade': { passOrderFields: () => ({}), PASS_PRICES: {}, isUpgradeOrder: () => false }, + '@/jungleConfig': { getJunglePaidTierByProductId: () => null }, + 'thinkingdata-node': {}, + crypto: { ...crypto, default: { ...crypto, createSign: () => ({ update() {}, sign: () => 'signature' }) } }, + }; + const globals = { process: { env: { PAYMENT_APP_ENV: 'production', WX_MIDAS_PAY_SIGN_KEY: 'test-key' } }, console: { log() {}, error() {} }, + fetch: async (url, options) => { state.requests.push({ url, body: options?.body && JSON.parse(options.body) }); return { ok: true, json: async () => ({ prepay_id: 'prepay', errcode: 0, trade_state: 'SUCCESS' }) }; }, + }; + return { state, load: file => loadModule(file, deps, globals).default }; +} + +test('Android and iOS signing enforce all five server prices and quantity one', async () => { + for (const file of ['orderPaySig', 'iosorderPaySig']) { + const f = serverFixture(), create = f.load(`server/laf-cloud/functions/wx/${file}.ts`); + for (let i = 0; i < products.length; i++) { + const result = await create({ body: { uid: 'u', itemid: products[i], itemPrice: 1, itemCount: 99 } }); + assert.equal(result.code, 1); + const signed = JSON.parse(result.data.signData); + assert.equal(signed.goodsPrice, prices[i]); assert.equal(signed.buyQuantity, 1); + assert.equal(f.state.orders[i].goodsPrice, prices[i]); + } + } +}); + +test('legacy iOS customer service enforces the same prices and names', async () => { + const f = serverFixture(), create = f.load('server/laf-cloud/functions/wx/KeFuInfo.ts'); + for (let i = 0; i < products.length; i++) { + await create({ body: { FromUserName: 'o', SessionFrom: JSON.stringify({ tpye: 'ios', propName: products[i], price: 1, count: 99, outTradeNo: `ios-${i}` }) } }); + assert.equal(f.state.orders[i].goodsPrice, prices[i]); assert.equal(f.state.orders[i].itemCount, 1); + const payment = f.state.requests.filter(r => r.url.includes('/v3/pay/transactions/jsapi'))[i]; + assert.equal(payment.body.amount.total, prices[i]); + } +}); + +test('reborn acknowledgement rejects unpaid or foreign orders and tolerates retries', async () => { + const f = serverFixture(), acknowledge = f.load('server/laf-cloud/functions/wx/getOrderReward.ts'); + const order = { outTradeNo: 'paid', itemid: 'reborn_Gift_5', openid: 'o', state: 0 }; f.state.orders.push(order); + const request = () => acknowledge({ body: { outTradeNo: 'paid', uid: 'u' } }); + assert.equal((await request()).code, 0); assert.equal(order.state, 0); + order.state = 1; order.openid = 'other'; assert.equal((await request()).code, 0); + order.openid = 'o'; assert.equal((await request()).code, 1); assert.equal(order.state, 2); + assert.equal((await request()).code, 1); +}); + +test('legacy iOS confirms all tiers but leaves new-client rewards pending until acknowledgement', async () => { + for (const version of [undefined, 1]) for (let i = 0; i < products.length; i++) { + const f = serverFixture(), poll = f.load('server/laf-cloud/functions/wx/iosgetPayInfo.ts'); + const order = { outTradeNo: 'ios', itemid: products[i], goodsPrice: prices[i], openid: 'o', state: 0 }; + f.state.orders.push(order); + const result = await poll({ body: { uid: 'u', outTradeNo: 'ios', rebornGiftVersion: version }, headers: {}, socket: {} }); + assert.equal(result.code, 1); assert.equal(result.data.goodsPrice, prices[i]); + assert.equal(order.state, version === 1 ? 1 : 2); + assert.equal(f.state.user.rebornGiftCount, 0, 'legacy global limit stays disabled'); + if (version === 1) { + const again = await poll({ body: { uid: 'u', outTradeNo: 'ios', rebornGiftVersion: 1 }, headers: {}, socket: {} }); + assert.equal(again.code, 1); assert.equal(order.state, 1); + const acknowledge = f.load('server/laf-cloud/functions/wx/getOrderReward.ts'); + assert.equal((await acknowledge({ body: { uid: 'u', outTradeNo: 'ios' } })).code, 1); + assert.equal(order.state, 2); + } + } +});