feat(billing): add dual formulas, disable 10yuan gift, retry auto-renew
Memind CI / Test, build, and release guards (push) Successful in 3m43s

Turn off the low-balance 10 CNY signup bonus while keeping the 5 CNY
registration credit, split metering into independent formula A/B scopes,
and retry expired auto-renew subscriptions after recharge or hourly worker.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-22 08:45:43 +08:00
parent f57e080b49
commit 4d12ea438b
9 changed files with 474 additions and 164 deletions
+34 -8
View File
@@ -129,15 +129,19 @@ export function createUserAuth(pool, options = {}) {
const { storageRoot, publicBaseUrl } = resolveMindSpaceRuntimeConfig(h5Root, env);
const skillCatalog = listPlatformSkillCatalog(h5Root);
const defaultSignupBalanceCents = Number(options.defaultSignupBalanceCents ?? 500);
const lowBalanceGiftThresholdCents = Number(options.lowBalanceGiftThresholdCents ?? 100);
const lowBalanceGiftAmountCents = Number(options.lowBalanceGiftAmountCents ?? 1000);
const lowBalanceGiftThresholdCents = Number(
options.lowBalanceGiftThresholdCents ?? env.H5_LOW_BALANCE_GIFT_THRESHOLD_CENTS ?? 100,
);
const lowBalanceGiftAmountCents = Number(
options.lowBalanceGiftAmountCents ?? env.H5_LOW_BALANCE_GIFT_AMOUNT_CENTS ?? 0,
);
const sessionTtlMs = Number(options.sessionTtlMs ?? 7 * 24 * 60 * 60 * 1000);
const loginMaxFailures = Number(options.loginMaxFailures ?? 5);
const loginFailureWindowMs = Number(options.loginFailureWindowMs ?? 5 * 60 * 1000);
const persistSessions = options.persistSessions !== false && Boolean(pool);
let rechargeNotifier =
typeof options.onRechargeNotification === 'function' ? options.onRechargeNotification : null;
const subscriptionService = options.subscriptionService ?? null;
let subscriptionService = options.subscriptionService ?? null;
const provisionUserDataSpace = typeof options.provisionUserDataSpace === 'function'
? options.provisionUserDataSpace
: null;
@@ -190,7 +194,7 @@ export function createUserAuth(pool, options = {}) {
const getUserById = async (userId) => {
const [rows] = await pool.query(
`SELECT u.id, u.username, u.slug, u.email, u.display_name, u.role, u.status,
u.plan_type, u.workspace_root,
u.plan_type, u.workspace_root, u.billing_formula,
s.quota_bytes, s.used_bytes, s.reserved_bytes,
w.balance_cents, w.tokens_used,
(SELECT COALESCE(SUM(ABS(amount_cents)), 0)
@@ -218,6 +222,7 @@ export function createUserAuth(pool, options = {}) {
status: row.status,
planType: row.plan_type ?? 'free',
workspaceRoot: row.workspace_root,
billingFormula: row.billing_formula === 'B' ? 'B' : 'A',
balanceCents,
totalCreditCents: balanceCents + spentCents,
tokensUsed: Number(row.tokens_used ?? 0),
@@ -447,7 +452,7 @@ export function createUserAuth(pool, options = {}) {
(id, username, slug, email, display_name, salt, password_hash, password_algorithm,
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, ?, ?)`,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'user', 'active', 'free', ?, 0, NULL, ?, ?)`,
[
userId,
normalized,
@@ -900,7 +905,7 @@ export function createUserAuth(pool, options = {}) {
);
const [rows] = await pool.query(
`SELECT u.id, u.username, u.slug, u.email, u.display_name, u.role, u.status,
u.plan_type, u.workspace_root,
u.plan_type, u.workspace_root, u.billing_formula,
s.quota_bytes, s.used_bytes, s.reserved_bytes,
u.created_at, u.updated_at, w.balance_cents, w.tokens_used
FROM h5_users u
@@ -1038,6 +1043,14 @@ export function createUserAuth(pool, options = {}) {
fields.push('role = ?');
values.push(patch.role === 'admin' ? 'admin' : 'user');
}
if (patch.billingFormula !== undefined) {
const formula = String(patch.billingFormula).trim().toUpperCase();
if (!['A', 'B'].includes(formula)) {
return { ok: false, message: '计量公式只能是 A 或 B' };
}
fields.push('billing_formula = ?');
values.push(formula);
}
if (fields.length > 0) {
fields.push('updated_at = ?');
@@ -1272,6 +1285,16 @@ export function createUserAuth(pool, options = {}) {
]);
}
if (ownsConnection) await conn.commit();
if (subscriptionService?.processAutoRenewalsForUser) {
try {
await subscriptionService.processAutoRenewalsForUser(userId);
} catch (err) {
console.warn(
'Post-recharge auto-renew retry failed:',
err instanceof Error ? err.message : String(err),
);
}
}
const updated = await getUserById(userId);
if (rechargeNotifier) {
try {
@@ -1336,7 +1359,7 @@ export function createUserAuth(pool, options = {}) {
}
const tokenState = normalizeTokenState(tokenStateRaw);
const config = billingConfigService?.getEffectiveBillingConfig
? await billingConfigService.getEffectiveBillingConfig()
? await billingConfigService.getEffectiveBillingConfig({ userId })
: loadBillingConfig(env);
const normalizedRequestId = requestId ? String(requestId).trim() || null : null;
const now = Date.now();
@@ -2747,7 +2770,7 @@ export function createUserAuth(pool, options = {}) {
(id, username, slug, email, display_name, salt, password_hash, password_algorithm,
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, ?, ?)`,
VALUES (?, ?, ?, NULL, ?, ?, ?, ?, 'user', 'active', 'free', ?, 'wechat', 0, NULL, ?, ?)`,
[
userId,
normalized,
@@ -3079,6 +3102,9 @@ export function createUserAuth(pool, options = {}) {
setRechargeNotifier(callback) {
rechargeNotifier = typeof callback === 'function' ? callback : null;
},
setSubscriptionService(service) {
subscriptionService = service ?? null;
},
findWechatUserByOpenid,
getWechatAgentRoute,
upsertWechatAgentRoute,