feat: auto grant one-time low balance gift for new users

This commit is contained in:
john
2026-06-30 20:39:46 +08:00
parent ae6eaf8c3e
commit 9c653e9b16
4 changed files with 232 additions and 9 deletions
+94 -9
View File
@@ -129,6 +129,8 @@ export function createUserAuth(pool, options = {}) {
const publicBaseUrl = resolvePublicBaseUrl(env);
const skillCatalog = listPlatformSkillCatalog(h5Root);
const defaultSignupBalanceCents = Number(options.defaultSignupBalanceCents ?? 500);
const lowBalanceGiftThresholdCents = Number(options.lowBalanceGiftThresholdCents ?? 100);
const lowBalanceGiftAmountCents = Number(options.lowBalanceGiftAmountCents ?? 1000);
const sessionTtlMs = Number(options.sessionTtlMs ?? 7 * 24 * 60 * 60 * 1000);
const loginMaxFailures = Number(options.loginMaxFailures ?? 5);
const loginFailureWindowMs = Number(options.loginFailureWindowMs ?? 5 * 60 * 1000);
@@ -340,6 +342,72 @@ export function createUserAuth(pool, options = {}) {
);
};
const shouldEnableLowBalanceGift = ({ isAdmin = false, initialBalanceCents }) => {
return !isAdmin && Number(initialBalanceCents) === defaultSignupBalanceCents && defaultSignupBalanceCents > 0;
};
const grantLowBalanceGiftIfNeeded = async (conn, { userId, currentBalance, nextBalance, now }) => {
if (lowBalanceGiftAmountCents <= 0 || lowBalanceGiftThresholdCents < 0 || nextBalance > lowBalanceGiftThresholdCents) {
return { gifted: false, balanceAfter: nextBalance };
}
const [rows] = await conn.query(
`SELECT low_balance_gift_eligible, low_balance_gift_granted_at
FROM h5_users
WHERE id = ?
FOR UPDATE`,
[userId],
);
const user = rows[0];
if (!user || !Boolean(user.low_balance_gift_eligible) || user.low_balance_gift_granted_at != null) {
return { gifted: false, balanceAfter: nextBalance };
}
const giftedBalance = nextBalance + lowBalanceGiftAmountCents;
await conn.query(
`UPDATE h5_user_wallets
SET balance_cents = ?, updated_at = ?
WHERE user_id = ?`,
[giftedBalance, now, userId],
);
await conn.query(
`UPDATE h5_users
SET low_balance_gift_eligible = 0,
low_balance_gift_granted_at = ?,
status = CASE WHEN status = 'suspended' THEN 'active' ELSE status END,
updated_at = ?
WHERE id = ?`,
[now, now, userId],
);
await conn.query(
`INSERT INTO h5_billing_ledger
(user_id, type, amount_cents, tokens, note, operator_id, created_at)
VALUES (?, 'adjust', ?, 0, ?, NULL, ?)`,
[userId, lowBalanceGiftAmountCents, '新用户低余额自动赠送', now],
);
await conn.query(
`INSERT INTO h5_user_notifications
(id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at)
VALUES (?, ?, 'web', 'low_balance_gift', ?, ?, ?, 'unread', NULL, ?, ?)`,
[
crypto.randomUUID(),
userId,
'新用户额度已自动补送',
`检测到你的余额已低于 ¥${(lowBalanceGiftThresholdCents / 100).toFixed(2)},系统已自动赠送 ¥${(lowBalanceGiftAmountCents / 100).toFixed(2)} 新用户额度。本福利仅可领取一次。`,
JSON.stringify({
triggerBalanceCents: nextBalance,
previousBalanceCents: currentBalance,
giftAmountCents: lowBalanceGiftAmountCents,
thresholdCents: lowBalanceGiftThresholdCents,
}),
now,
now,
],
);
return { gifted: true, balanceAfter: giftedBalance };
};
const register = async ({ username, password, displayName, email }) => {
const normalized = normalizeUsername(username);
if (!isValidUsername(normalized)) {
@@ -364,8 +432,9 @@ export function createUserAuth(pool, options = {}) {
await conn.query(
`INSERT INTO h5_users
(id, username, slug, email, display_name, salt, password_hash, password_algorithm,
role, status, plan_type, workspace_root, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'user', 'active', 'free', ?, ?, ?)`,
role, status, plan_type, workspace_root, low_balance_gift_eligible, low_balance_gift_granted_at,
created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'user', 'active', 'free', ?, 1, NULL, ?, ?)`,
[
userId,
normalized,
@@ -815,13 +884,15 @@ export function createUserAuth(pool, options = {}) {
const now = Date.now();
const conn = await pool.getConnection();
const initialBalanceCents = Number(balanceCents ?? defaultSignupBalanceCents);
try {
await conn.beginTransaction();
await conn.query(
`INSERT INTO h5_users
(id, username, slug, email, display_name, salt, password_hash, password_algorithm,
role, status, plan_type, workspace_root, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 'free', ?, ?, ?)`,
role, status, plan_type, workspace_root, low_balance_gift_eligible, low_balance_gift_granted_at,
created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 'free', ?, ?, NULL, ?, ?)`,
[
userId,
normalized,
@@ -833,6 +904,7 @@ export function createUserAuth(pool, options = {}) {
passwordAlgorithm,
isAdmin ? 'admin' : 'user',
root,
shouldEnableLowBalanceGift({ isAdmin, initialBalanceCents }) ? 1 : 0,
now,
now,
],
@@ -840,7 +912,7 @@ export function createUserAuth(pool, options = {}) {
await conn.query(
`INSERT INTO h5_user_wallets (user_id, balance_cents, tokens_used, updated_at)
VALUES (?, ?, 0, ?)`,
[userId, Number(balanceCents ?? defaultSignupBalanceCents), now],
[userId, initialBalanceCents, now],
);
await conn.query(
`INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
@@ -1315,7 +1387,11 @@ export function createUserAuth(pool, options = {}) {
let tokensUsedAfter = null;
if (costCents > 0) {
const [walletRows] = await conn.query(
`SELECT balance_cents, tokens_used FROM h5_user_wallets WHERE user_id = ? FOR UPDATE`,
`SELECT w.balance_cents, w.tokens_used, u.status
FROM h5_user_wallets w
JOIN h5_users u ON u.id = w.user_id
WHERE w.user_id = ?
FOR UPDATE`,
[userId],
);
const wallet = walletRows[0];
@@ -1366,7 +1442,15 @@ export function createUserAuth(pool, options = {}) {
],
);
if (nextBalance <= 0) {
const lowBalanceGift = await grantLowBalanceGiftIfNeeded(conn, {
userId,
currentBalance,
nextBalance,
now,
});
balanceAfter = lowBalanceGift.balanceAfter;
if (balanceAfter <= 0) {
await conn.query(`UPDATE h5_users SET status = 'suspended', updated_at = ? WHERE id = ?`, [
now,
userId,
@@ -2425,8 +2509,9 @@ export function createUserAuth(pool, options = {}) {
await conn.query(
`INSERT INTO h5_users
(id, username, slug, email, display_name, salt, password_hash, password_algorithm,
role, status, plan_type, workspace_root, signup_source, created_at, updated_at)
VALUES (?, ?, ?, NULL, ?, ?, ?, ?, 'user', 'active', 'free', ?, 'wechat', ?, ?)`,
role, status, plan_type, workspace_root, signup_source, low_balance_gift_eligible,
low_balance_gift_granted_at, created_at, updated_at)
VALUES (?, ?, ?, NULL, ?, ?, ?, ?, 'user', 'active', 'free', ?, 'wechat', 1, NULL, ?, ?)`,
[
userId,
normalized,