fix(billing): backfill finite image quota after free-plan rebuild

Expired paid users were rebuilt as free with period_images_limit=0, which the quota system treats as unlimited. Write the catalog quota on rebuild and repair existing finite plans on schema ensure.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-14 21:39:55 +08:00
parent 41f245c88a
commit 8cf1c772a7
4 changed files with 113 additions and 3 deletions
+26 -1
View File
@@ -57,6 +57,16 @@ async function columnExists(pool, table, column) {
return rows.length > 0;
}
async function tableExists(pool, table) {
const [rows] = await pool.query(
`SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
LIMIT 1`,
[table],
);
return rows.length > 0;
}
async function indexExists(pool, table, index) {
const [rows] = await pool.query(
`SELECT 1 FROM information_schema.STATISTICS
@@ -943,10 +953,23 @@ export async function migrateSchema(pool) {
// Back-fill free subscriptions for existing users who pre-date the subscription system.
// Idempotent: only inserts where no active subscription exists.
// period_images_limit defaults to 0 (= unlimited). Always write the catalog
// quota when the column exists, otherwise expired paid users become unlimited
// on the next Portal/Plaza start.
const hasImageLimit = await columnExists(pool, 'h5_subscriptions', 'period_images_limit');
const hasCatalog = await tableExists(pool, 'h5_plan_catalog');
const tokenSelect = hasCatalog ? 'COALESCE(c.period_tokens, 150000)' : '150000';
const imageSelect = hasCatalog ? 'COALESCE(c.period_images, 10)' : '10';
const catalogJoin = hasCatalog ? "LEFT JOIN h5_plan_catalog c ON c.plan_type = 'free'" : '';
const imageCols = hasImageLimit
? 'period_images_limit, period_images_used, period_images_bonus,'
: '';
const imageVals = hasImageLimit ? `${imageSelect}, 0, 0,` : '';
await pool.query(`
INSERT INTO h5_subscriptions
(id, user_id, plan_type, status,
period_tokens_limit, period_tokens_used,
${imageCols}
period_start, period_end, expires_at, overage_rate,
operator_id, note, created_at, updated_at)
SELECT
@@ -954,7 +977,8 @@ export async function migrateSchema(pool) {
u.id,
'free',
'active',
150000, 0,
${tokenSelect}, 0,
${imageVals}
UNIX_TIMESTAMP() * 1000,
(UNIX_TIMESTAMP() + 30 * 86400) * 1000,
(UNIX_TIMESTAMP() + 30 * 86400) * 1000,
@@ -964,6 +988,7 @@ export async function migrateSchema(pool) {
UNIX_TIMESTAMP() * 1000,
UNIX_TIMESTAMP() * 1000
FROM h5_users u
${catalogJoin}
WHERE NOT EXISTS (
SELECT 1 FROM h5_subscriptions s
WHERE s.user_id = u.id AND s.status = 'active'