server/laf-cloud/functions/wx/payCallBack.ts
2026-09-24 17:59:52 +08:00

292 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as StarterPackModule from "@/limitedTimeEvent";
const StarterPack: any = StarterPackModule as any;
import * as GoldMinerPaymentModule from "@/goldMiner/payment";
const GoldMinerPayment: any = GoldMinerPaymentModule;
import cloud from '@lafjs/cloud';
import * as crypto from 'crypto';
import * as JungleConfigModule from "@/jungleConfig";
const JungleConfig: any = JungleConfigModule as any;
const db = cloud.database();
const ThinkingData = require('thinkingdata-node');
export default async function (ctx: FunctionContext) {
let data: any;
try {
data = JSON.parse(ctx.body?.MiniGame?.Payload || "");
} catch {
console.error("微信发货回调 Payload 解析失败");
return { "ErrCode": -1, "ErrMsg": "Invalid Payload" };
}
const outTradeNo = String(data.OutTradeNo || "");
const paymentAppEnv = process.env.PAYMENT_APP_ENV || "production";
if (!["production", "test"].includes(paymentAppEnv)) {
return { ErrCode: -1, ErrMsg: "Invalid Payment Environment" };
}
const testOrder = outTradeNo.startsWith("wct_");
if (testOrder) {
// Both deployments verify the original signed payload, including whitespace.
if (!validTestPayment(ctx.body, data)) {
console.error("测试订单支付回调验签失败", { orderSuffix: outTradeNo.slice(-6) });
return { ErrCode: -1, ErrMsg: "Invalid Payment Signature" };
}
if (paymentAppEnv === "production") return forwardTestPayment(ctx);
} else if (paymentAppEnv === "test") {
return { ErrCode: -1, ErrMsg: "Order Environment Mismatch" };
}
const orderResult = await db.collection("order").where({ outTradeNo }).getOne();
const order = orderResult.data;
if (!order) {
console.error("微信发货回调未找到订单:" + outTradeNo);
return { "ErrCode": -1, "ErrMsg": "Order Not Found" };
}
if (testOrder && (order.paymentAppEnv !== "test"
|| data.OpenId !== order.openid
|| data.GoodsInfo?.ProductId !== order.itemid
|| Number(data.GoodsInfo?.Quantity) !== Number(order.itemCount)
|| Number(data.GoodsInfo?.OrigPrice) !== Number(order.goodsPrice))) {
return { ErrCode: -1, ErrMsg: "Order Mismatch" };
}
if (order.goldMiner || String(order.itemid || "").startsWith("gold_miner") || order.itemid === 'pay_test') {
try {
await GoldMinerPayment.confirmNativePayment(ctx.body, data, order);
if (order.state === 0) {
try {
const paidUser = await db.collection("users").where({ _id: order.goldMiner.uid }).getOne();
if (paidUser.data) await sead(order, paidUser.data, getClientIp(ctx));
} catch { console.error("黄金矿工 payment 埋点待核对", outTradeNo.slice(-6)); }
}
return { ErrCode: 0, ErrMsg: "Success" };
} catch {
console.error("黄金矿工支付确认待重试", outTradeNo.slice(-6));
return { ErrCode: -1, ErrMsg: "Gold Miner fulfillment pending" };
}
}
const jungleTier = JungleConfig.getJunglePaidTierByProductId(order.itemid);
if (jungleTier) {
const callbackProductId = String(data.GoodsInfo?.ProductId || "");
const callbackQuantity = Number(data.GoodsInfo?.Quantity);
const callbackOriginalPrice = Number(data.GoodsInfo?.OrigPrice);
if ((data.OpenId && data.OpenId !== order.openid)
|| (callbackProductId && callbackProductId !== order.itemid)
|| (Number.isFinite(callbackQuantity) && callbackQuantity !== Number(order.itemCount))
|| (Number.isFinite(callbackOriginalPrice) && callbackOriginalPrice !== Number(order.goodsPrice))) {
console.error("Jungle 支付回调与订单信息不一致:" + outTradeNo);
return { "ErrCode": -1, "ErrMsg": "Order Mismatch" };
}
}
const userResult = await db.collection("users").where({ openid: order.openid }).getOne();
const user = userResult.data;
if (!user) {
console.error("微信发货回调未找到玩家:" + order.openid);
return { "ErrCode": -1, "ErrMsg": "User Not Found" };
}
// The native notification has no verified payment-success timestamp; freeze
// first server confirmation time, independently of the client reward time.
const rookieGiftPaidAt = Date.now();
const firstPaidCallback = order.state === 0;
if (order.state !== 2) {
await db.collection("order").where({ outTradeNo }).update({
state: 1,
chargeTime: new Date(Date.now() + 8 * 3600000),
});
}
const userUpdate: any = {};
let jungleUnlockedFromLocked = false;
if (jungleTier) {
const storedState = JungleConfig.normalizeStoredJungleState(user.jungleTreasureState);
const wasLocked = Number(storedState?.tierStates?.[jungleTier.id - 1]) === 0;
const unlockResult = JungleConfig.unlockJungleTierByProductId(
user.jungleTreasureState,
order.itemid,
outTradeNo,
);
if (unlockResult.ok && unlockResult.changed && unlockResult.state) {
userUpdate.jungleTreasureState = JungleConfig.serializeJungleState(unlockResult.state);
jungleUnlockedFromLocked = wasLocked
&& Number(unlockResult.state.tierStates?.[jungleTier.id - 1]) === 1;
}
}
if (order.itemid === "starter_pack") {
userUpdate.starter_packState = 1;
}
if (!user.pay_user) {
userUpdate.pay_user = true;
}
if (Object.keys(userUpdate).length > 0) {
await db.collection("users").where({ _id: user._id }).update(userUpdate);
}
await StarterPack.reportRookieGift(order, user, rookieGiftPaidAt);
if (firstPaidCallback) {
await sead(order, user, getClientIp(ctx));
}
if (jungleTier && jungleUnlockedFromLocked) {
await db.collection("order").where({ outTradeNo }).update({
state: 2,
getTime: new Date(Date.now() + 8 * 3600000),
});
}
// let query={
// signature: '1c1b5c1c72058436e2f1214e6aeafd258e50acd4',
// timestamp: '1751291889',
// nonce: '3122546041'
// }
/**
* ToUserName: 'gh_1092576a6ce7',
FromUserName: 'orm8I7IA-pMesygmTc5HPufGZHvA',
CreateTime: 1751292233,
MsgType: 'event',
MiniGame: {
Payload: '{"OpenId":"orm8I7BRTjhdQ3Mz9V9cg1jzTUcQ","OutTradeNo":"wcx_92778691620421751292218408","WeChatPayInfo":{"MchOrderNo":"20250630714915544","TransactionId":"4200002678202506307565098431"},"Env":0,"GoodsInfo":{"ProductId":"10011","Quantity":1,"ZoneId":"1","OrigPrice":100,"ActualPrice":100,"Attach":"购买道具","OrderSource":1},"PayChannel":0}',
PayEventSig: 'ecdaf472c239894706e28ab941d2aebf727b5bed45fe4073c645e922b6eefc1a',
IsMock: false
},
Event: 'minigame_game_pay_goods_deliver_notify'
*/
//return ctx.query.echostr;
return { "ErrCode": 0, "ErrMsg": "Success" };
}
function validTestPayment(body: any, data: any): boolean {
const key = process.env.WX_MIDAS_PAY_SIGN_KEY;
const signature = body?.MiniGame?.PayEventSig;
if (!key || body?.Event !== "minigame_game_pay_goods_deliver_notify"
|| data.Env !== 0 || body.MiniGame.IsMock === true
|| typeof signature !== "string" || !/^[a-fA-F0-9]{64}$/.test(signature)) return false;
const expected = crypto.createHmac("sha256", key)
.update(body.Event + "&" + body.MiniGame.Payload).digest();
return crypto.timingSafeEqual(expected, Buffer.from(signature, "hex"));
}
async function forwardTestPayment(ctx: FunctionContext) {
// Fixed server-owned target. No client URL and no fallback to production DB.
if (ctx.headers?.["x-payment-forwarded"]) {
return { ErrCode: -1, ErrMsg: "Payment Forward Loop" };
}
try {
const response = await cloud.fetch({
url: process.env.TEST_WX_PAY_NOTIFY_URL,
method: "POST",
data: ctx.body,
headers: { "content-type": "application/json", "x-payment-forwarded": "1" },
timeout: 3000,
maxRedirects: 0,
});
if (response.status === 200 && response.data?.ErrCode === 0) {
return { ErrCode: 0, ErrMsg: "Success" };
}
} catch {
// Avoid logging an HTTP error object that may contain the payment payload.
console.error("测试支付回调转发失败,等待微信重试");
}
return { ErrCode: -1, ErrMsg: "Test Payment Forward Failed" };
}
async function sead1(outTradeNo) {
let res = await db.collection("order").where({ outTradeNo: outTradeNo }).getOne();
if (res.data.state == 1) {
return {
code: 1, data: {
pay_state: 2
}, msg: "成功"
};
} else if (res.data.state == 2) {
let userInfo = await db.collection("users").where({ openid: res.data.openid }).getOne();
await sead(outTradeNo, userInfo.data, "")
console.log("1");
return {
code: 1, data: {
pay_state: 1,
}, msg: "已领取奖励"
}
} else {
return {
code: 1, data: {
pay_state: 1,
}, msg: "充值未成功"
}
};
}
const getClientIp = (req) => {
return (
req.headers['x-forwarded-for']?.split(',')[0]?.trim() || // 代理链中的第一个 IP
req.headers['x-real-ip'] || // Nginx 配置的 real-ip
req.socket.remoteAddress // 原始 IP(可能是代理)
);
};
async function sead(order, userInfo, ip) {
db.collection("susu").add({
order: order,
time: Date.now()
});
if (!userInfo.distinctId) {
console.log("未找到distinctId无法上传数数")
return;
}
let id = '95993f9ab6f1402a87abe5147827e5e0'; // 正式
if (userInfo.isDebug != "true") {
id = '40e3d5c5f2af49a4a074205564ce5dbb';
}
let teSDK = ThinkingData.initWithBatchMode(id, 'https://data.nika4fun.com/sync_data', {
dryRun: false, // report data to TE or not
deviceId: "123456789",//设备唯一标识
});
let trackEvent = {
accountId: order.openid,
distinctId: userInfo.distinctId,//访客id
event: 'payment',
time: new Date(Date.now() + 8 * 3600000),
ip: ip,
properties: {
order_id: order.outTradeNo,
pay_amount: order.goodsPrice,
payment_name: order.itemid,
payment_num: order.itemCount,
payment_type: order.type == "ios" ? "ios" : "Android",
is_on_table: Number(userInfo.addLevel) >= 5,
current_level: userInfo.levelAmount, //当前关卡等级 number
current_health: userInfo.healthAmount, //当前体力值
tmp_coin: userInfo.coinAmount,//当前金币
version: "1.6",
user_id: userInfo._id, //用户id
pay_user: userInfo.pay_user,
register_time: userInfo.register_time,
abTest_layer_one: getABTestGroupValue(userInfo.abTests, "layer_1"),
abTest_layer_two: getABTestGroupValue(userInfo.abTests, "layer_2"),
abTest_layer_three: getABTestGroupValue(userInfo.abTests, "layer_3")
},
callback(e) {
if (e) {
}
}
};
teSDK.track(trackEvent);
teSDK.close();
return;
}
function getABTestGroupValue(abTests: any, layerKey: string) {
if (!abTests) {
return 0;
}
if (typeof abTests === "string") {
try {
abTests = JSON.parse(abTests);
} catch {
return 0;
}
}
const groupMap = { A: 1, B: 2, C: 3, D: 4, E: 5 };
const group = String(abTests[layerKey]?.group || "").toUpperCase();
return groupMap[group] || 0;
}