199 lines
6.3 KiB
TypeScript
199 lines
6.3 KiB
TypeScript
import cloud from '@lafjs/cloud'
|
|
import { createHash } from 'crypto';
|
|
|
|
const db = cloud.database();
|
|
const CONFIG_COLLECTION = "sign_in_activity_config";
|
|
const CLAIM_COLLECTION = "sign_in_activity_claims";
|
|
const USERS_COLLECTION = "users";
|
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
const UTC_8_OFFSET_MS = 8 * 60 * 60 * 1000;
|
|
const REWARD_DAYS = 7;
|
|
|
|
export type SignInRewardItem = {
|
|
type: "coin" | "freeze" | "hammer" | "magic_wand" | "infinite_health" | "cat_skin";
|
|
count: number;
|
|
itemId?: number;
|
|
};
|
|
|
|
export type SignInActivityConfig = {
|
|
activityId: string;
|
|
enabled: boolean;
|
|
triggerLevel: number;
|
|
durationDays: number;
|
|
timezone?: string;
|
|
rewards: Array<{
|
|
day: number;
|
|
items: SignInRewardItem[];
|
|
}>;
|
|
updatedAt?: number;
|
|
createdAt?: number;
|
|
};
|
|
|
|
export type SignInActivityState = {
|
|
activityId: string;
|
|
startAt: number;
|
|
endAt: number;
|
|
activatedAt: number;
|
|
completedAt?: number | null;
|
|
};
|
|
|
|
export function getChinaDayStart(now = Date.now()) {
|
|
return Math.floor((now + UTC_8_OFFSET_MS) / DAY_MS) * DAY_MS - UTC_8_OFFSET_MS;
|
|
}
|
|
|
|
export function getChinaDateKey(now = Date.now()) {
|
|
return String(Math.floor((now + UTC_8_OFFSET_MS) / DAY_MS));
|
|
}
|
|
|
|
export function getUserSignInLevel(user: any) {
|
|
const level = Number(user?.levelAmount);
|
|
return Number.isFinite(level) && level > 0 ? level : 0;
|
|
}
|
|
|
|
export function normalizeSignInState(value: any): SignInActivityState | null {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
if (typeof value === "string") {
|
|
try {
|
|
value = JSON.parse(value);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
if (!value.activityId || !Number.isFinite(Number(value.startAt)) || !Number.isFinite(Number(value.endAt))) {
|
|
return null;
|
|
}
|
|
return {
|
|
activityId: String(value.activityId),
|
|
startAt: Number(value.startAt),
|
|
endAt: Number(value.endAt),
|
|
activatedAt: Number(value.activatedAt) || Number(value.startAt),
|
|
completedAt: Number(value.completedAt) || null,
|
|
};
|
|
}
|
|
|
|
export function isValidSignInConfig(config: any): config is SignInActivityConfig {
|
|
if (!config?.activityId) {
|
|
return false;
|
|
}
|
|
if (!Number.isFinite(Number(config.triggerLevel)) || Number(config.triggerLevel) < 0) {
|
|
return false;
|
|
}
|
|
if (!Number.isInteger(Number(config.durationDays)) || Number(config.durationDays) <= 0) {
|
|
return false;
|
|
}
|
|
if (!Array.isArray(config.rewards) || config.rewards.length !== REWARD_DAYS) {
|
|
return false;
|
|
}
|
|
return config.rewards.every((reward, index) =>
|
|
Number(reward?.day) === index + 1
|
|
&& Array.isArray(reward.items)
|
|
&& reward.items.length > 0
|
|
&& reward.items.every(isValidRewardItem)
|
|
);
|
|
}
|
|
|
|
function isValidRewardItem(item: any) {
|
|
const validTypes = ["coin", "freeze", "hammer", "magic_wand", "infinite_health", "cat_skin"];
|
|
const validCatId = item?.type !== "cat_skin"
|
|
|| (Number.isInteger(Number(item.itemId)) && Number(item.itemId) > 0);
|
|
return validTypes.includes(item?.type)
|
|
&& Number.isFinite(Number(item.count))
|
|
&& Number(item.count) > 0
|
|
&& validCatId
|
|
&& (item.itemId === undefined || Number.isFinite(Number(item.itemId)));
|
|
}
|
|
|
|
export async function getActiveSignInConfig(): Promise<SignInActivityConfig | null> {
|
|
try {
|
|
const result = await db.collection(CONFIG_COLLECTION).where({ enabled: true }).get();
|
|
const configs = Array.isArray(result.data) ? result.data.filter(isValidSignInConfig) : [];
|
|
if (configs.length !== 1) {
|
|
return null;
|
|
}
|
|
configs.sort((left, right) => Number(right.updatedAt || right.createdAt || 0) - Number(left.updatedAt || left.createdAt || 0));
|
|
return configs[0];
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function getSignInConfig(activityId: string): Promise<SignInActivityConfig | null> {
|
|
try {
|
|
const result = await db.collection(CONFIG_COLLECTION).where({ activityId }).getOne();
|
|
return isValidSignInConfig(result.data) ? result.data : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function activateSignInActivity(user: any, now = Date.now()) {
|
|
if (!user?._id || normalizeSignInState(user.signInActivity)) {
|
|
return { activated: false, state: normalizeSignInState(user?.signInActivity), config: null };
|
|
}
|
|
const config = await getActiveSignInConfig();
|
|
if (!config || getUserSignInLevel(user) < Number(config.triggerLevel)) {
|
|
return { activated: false, state: null, config };
|
|
}
|
|
const startAt = getChinaDayStart(now);
|
|
const state: SignInActivityState = {
|
|
activityId: config.activityId,
|
|
startAt,
|
|
endAt: startAt + Number(config.durationDays) * DAY_MS,
|
|
activatedAt: now,
|
|
completedAt: null,
|
|
};
|
|
const result = await db.collection(USERS_COLLECTION)
|
|
.where({ _id: user._id })
|
|
.update({ signInActivity: JSON.stringify(state) });
|
|
const activated = getUpdatedCount(result) > 0;
|
|
if (activated) {
|
|
user.signInActivity = state;
|
|
}
|
|
return { activated, state: activated ? state : normalizeSignInState(user.signInActivity), config };
|
|
}
|
|
|
|
function getUpdatedCount(result: any) {
|
|
const count = Number(result?.updated ?? result?.matched ?? result?.data?.updated);
|
|
return Number.isFinite(count) ? count : 1;
|
|
}
|
|
|
|
export async function getSignInClaims(uid: string, activityId: string) {
|
|
const result = await db.collection(CLAIM_COLLECTION).where({ uid, activityId }).get();
|
|
const claims = Array.isArray(result.data) ? result.data.slice() : [];
|
|
claims.sort((left, right) => Number(left.rewardDay) - Number(right.rewardDay));
|
|
return claims;
|
|
}
|
|
|
|
export function buildClaimId(uid: string, activityId: string, dateKey: string) {
|
|
return createHash("sha256").update(`${uid}:${activityId}:${dateKey}`).digest("hex");
|
|
}
|
|
|
|
export async function addSignInClaim(payload: any) {
|
|
return db.collection(CLAIM_COLLECTION).add(payload);
|
|
}
|
|
|
|
export async function markSignInCompleted(uid: string, state: SignInActivityState, completedAt: number) {
|
|
const nextState = { ...state, completedAt };
|
|
await db.collection(USERS_COLLECTION).where({ _id: uid }).update({
|
|
signInActivity: JSON.stringify(nextState),
|
|
});
|
|
return nextState;
|
|
}
|
|
|
|
export function getSignInActivityStatus(state: SignInActivityState | null, claimedCount: number, now = Date.now()) {
|
|
if (!state) {
|
|
return "locked" as const;
|
|
}
|
|
if (claimedCount >= REWARD_DAYS || state.completedAt) {
|
|
return "completed" as const;
|
|
}
|
|
if (now >= state.endAt) {
|
|
return "expired" as const;
|
|
}
|
|
return "active" as const;
|
|
}
|
|
|
|
export const SIGN_IN_REWARD_DAYS = REWARD_DAYS;
|