161 lines
4.2 KiB
TypeScript
161 lines
4.2 KiB
TypeScript
import cloud from '@lafjs/cloud'
|
||
|
||
const db = cloud.database();
|
||
|
||
const TA_RECEIVER_URL = "https://data.nika4fun.com/sync_data";
|
||
const TA_PRODUCTION_APP_ID = "87d18958cea145f29d3265470ecd3486";
|
||
const TA_TEST_APP_ID = "121591378fc1423893deb12041413eb3";
|
||
const UPLOAD_CONFIRMATION = "UPLOAD";
|
||
|
||
export default async function (ctx: FunctionContext) {
|
||
const body = ctx.body || {};
|
||
const configuredToken = String((globalThis as any).process?.env?.TA_UNIONID_SYNC_TOKEN || "");
|
||
const adminToken = String(body.adminToken || "");
|
||
|
||
if (!configuredToken) {
|
||
return fail("服务端未配置 TA_UNIONID_SYNC_TOKEN");
|
||
}
|
||
if (!adminToken || adminToken !== configuredToken) {
|
||
return fail("unauthorized");
|
||
}
|
||
|
||
const openid = typeof body.openid === "string" ? body.openid.trim() : "";
|
||
if (!openid) {
|
||
return fail("openid required");
|
||
}
|
||
|
||
const collectionName = body.gameName === "iaa" ? "usersAd" : "users";
|
||
const result = await db.collection(collectionName).where({ openid: openid }).getOne();
|
||
const user = result.data;
|
||
if (!user) {
|
||
return fail(`未在 ${collectionName} 中找到该 openid`);
|
||
}
|
||
|
||
const unionid = typeof user.unionid === "string" ? user.unionid.trim() : "";
|
||
if (!unionid) {
|
||
return fail("该用户没有有效的 unionid");
|
||
}
|
||
|
||
const taProject = resolveTaProject(user.isDebug);
|
||
if (!taProject) {
|
||
return fail("用户 isDebug 字段缺失或无效,无法安全判断数数项目");
|
||
}
|
||
|
||
const distinctId = typeof user.distinctId === "string" ? user.distinctId.trim() : "";
|
||
const preview = {
|
||
collection: collectionName,
|
||
taProject: taProject.name,
|
||
accountId: openid,
|
||
distinctId: distinctId || null,
|
||
properties: {
|
||
unionid: unionid,
|
||
},
|
||
};
|
||
|
||
if (body.confirm !== UPLOAD_CONFIRMATION) {
|
||
return {
|
||
code: 1,
|
||
data: { dryRun: true, ...preview },
|
||
msg: `预览成功;确认无误后传 confirm=${UPLOAD_CONFIRMATION} 才会上传`,
|
||
};
|
||
}
|
||
|
||
try {
|
||
const receiverResponse = await uploadUnionId(
|
||
taProject.appId,
|
||
openid,
|
||
distinctId,
|
||
unionid,
|
||
);
|
||
return {
|
||
code: 1,
|
||
data: { uploaded: true, ...preview, receiverResponse: receiverResponse },
|
||
msg: "unionid 已提交到数数",
|
||
};
|
||
} catch (error) {
|
||
const message = getErrorMessage(error);
|
||
console.error("syncUserUnionId upload failed:", message);
|
||
return fail(message);
|
||
}
|
||
}
|
||
|
||
function resolveTaProject(isDebug: any) {
|
||
const releaseFlag = normalizeBoolean(isDebug);
|
||
if (releaseFlag === null) {
|
||
return null;
|
||
}
|
||
|
||
// 当前客户端把“正式版”记录为 isDebug=true,这里与现有登录逻辑保持一致。
|
||
return releaseFlag
|
||
? { name: "production", appId: TA_PRODUCTION_APP_ID }
|
||
: { name: "test", appId: TA_TEST_APP_ID };
|
||
}
|
||
|
||
function normalizeBoolean(value: any): boolean | null {
|
||
if (value === true || value === "true" || value === 1 || value === "1") {
|
||
return true;
|
||
}
|
||
if (value === false || value === "false" || value === 0 || value === "0") {
|
||
return false;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function uploadUnionId(
|
||
appId: string,
|
||
accountId: string,
|
||
distinctId: string,
|
||
unionid: string,
|
||
) {
|
||
const data: any = {
|
||
"#account_id": accountId,
|
||
"#type": "user_set",
|
||
properties: {
|
||
unionid: unionid,
|
||
},
|
||
};
|
||
if (distinctId) {
|
||
data["#distinct_id"] = distinctId;
|
||
}
|
||
|
||
const formData = [
|
||
`appid=${encodeURIComponent(appId)}`,
|
||
`data=${encodeURIComponent(JSON.stringify(data))}`,
|
||
"debug=0",
|
||
].join("&");
|
||
|
||
const response = await cloud.fetch({
|
||
url: TA_RECEIVER_URL,
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||
},
|
||
data: formData,
|
||
timeout: 10000,
|
||
});
|
||
const receiverResponse = parseReceiverResponse(response?.data);
|
||
if (!receiverResponse || Number(receiverResponse.code) !== 0) {
|
||
throw new Error(`数数接收失败: ${JSON.stringify(receiverResponse ?? response?.data)}`);
|
||
}
|
||
return receiverResponse;
|
||
}
|
||
|
||
function parseReceiverResponse(value: any) {
|
||
if (typeof value !== "string") {
|
||
return value;
|
||
}
|
||
try {
|
||
return JSON.parse(value);
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function getErrorMessage(error: any) {
|
||
return error instanceof Error ? error.message : String(error);
|
||
}
|
||
|
||
function fail(msg: string) {
|
||
return { code: 0, data: null, msg: msg };
|
||
}
|