feat: auto grant one-time low balance gift for new users
This commit is contained in:
+94
-9
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user