#!/usr/bin/env node /** * One-off billing compensation for duplicate charges / rate overcharge. * * Usage: * node scripts/compensate-user-billing.mjs wx_mk4zzaps * node scripts/compensate-user-billing.mjs wx_mk4zzaps --apply * node scripts/compensate-user-billing.mjs wx_mk4zzaps --apply --include-rate-adjustment */ import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import mysql from 'mysql2/promise'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); function loadEnvFile(filePath) { if (!fs.existsSync(filePath)) return; for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eq = trimmed.indexOf('='); if (eq < 0) continue; const key = trimmed.slice(0, eq).trim(); const value = trimmed.slice(eq + 1).trim(); if (!process.env[key]) process.env[key] = value; } } loadEnvFile(path.join(root, '.env')); const username = process.argv[2]; const apply = process.argv.includes('--apply'); const includeRateAdjustment = process.argv.includes('--include-rate-adjustment'); if (!username) { console.error('Usage: node scripts/compensate-user-billing.mjs [--apply] [--include-rate-adjustment]'); process.exit(1); } if (!process.env.DATABASE_URL) { console.error('DATABASE_URL is not configured'); process.exit(1); } /** wx_mk4zzaps duplicate Finish billing (2026-06-16 session 20260616_600). */ const DUPLICATE_COST_CENTS = 1735; const DUPLICATE_TOKENS = 838567; /** Unique bills at 2/6 分/1k vs documented DeepSeek ×1 default (0.101/0.202 分/1k). */ const RATE_OVERCHARGE_CENTS = 1467; async function main() { const pool = mysql.createPool(process.env.DATABASE_URL); try { const [users] = await pool.query( `SELECT u.id, u.username, u.display_name, u.status, COALESCE(w.balance_cents, 0) AS balance_cents, COALESCE(w.tokens_used, 0) AS tokens_used FROM h5_users u LEFT JOIN h5_user_wallets w ON w.user_id = u.id WHERE u.username = ? LIMIT 1`, [username], ); const user = users[0]; if (!user) { console.error(`User not found: ${username}`); process.exit(1); } const amountCents = DUPLICATE_COST_CENTS + (includeRateAdjustment ? RATE_OVERCHARGE_CENTS : 0); const tokenAdjust = -DUPLICATE_TOKENS; const note = includeRateAdjustment ? `补偿:重复扣费 ¥${(DUPLICATE_COST_CENTS / 100).toFixed(2)} + 费率差额 ¥${(RATE_OVERCHARGE_CENTS / 100).toFixed(2)}` : `补偿:重复扣费 ¥${(DUPLICATE_COST_CENTS / 100).toFixed(2)}(session 20260616_600)`; console.log('User:', user.username, user.display_name ?? '', `(${user.id})`); console.log('Current status:', user.status); console.log('Current balance:', `¥${(Number(user.balance_cents) / 100).toFixed(2)}`); console.log('Current tokens_used:', Number(user.tokens_used).toLocaleString()); console.log(''); console.log('Compensation plan:'); console.log(' balance +', `¥${(amountCents / 100).toFixed(2)}`, `(${amountCents} cents)`); if (tokenAdjust !== 0) { console.log(' tokens_used', tokenAdjust.toLocaleString(), '(remove duplicate token count)'); } console.log(' note:', note); console.log(' reactivate account if suspended:', user.status === 'suspended' ? 'yes' : 'no'); if (!apply) { console.log(''); console.log('Dry run only. Re-run with --apply to execute.'); return; } const now = Date.now(); const conn = await pool.getConnection(); try { await conn.beginTransaction(); await conn.query( `INSERT INTO h5_user_wallets (user_id, balance_cents, tokens_used, updated_at) VALUES (?, ?, 0, ?) ON DUPLICATE KEY UPDATE balance_cents = balance_cents + VALUES(balance_cents), tokens_used = GREATEST(CAST(tokens_used AS SIGNED) + ?, 0), updated_at = VALUES(updated_at)`, [user.id, amountCents, now, tokenAdjust], ); await conn.query( `INSERT INTO h5_billing_ledger (user_id, type, amount_cents, tokens, note, operator_id, created_at) VALUES (?, 'adjust', ?, ?, ?, NULL, ?)`, [user.id, amountCents, tokenAdjust, note, now], ); if (user.status === 'suspended') { await conn.query(`UPDATE h5_users SET status = 'active', updated_at = ? WHERE id = ?`, [ now, user.id, ]); } await conn.commit(); } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); } const [updatedRows] = await pool.query( `SELECT u.status, COALESCE(w.balance_cents, 0) AS balance_cents, COALESCE(w.tokens_used, 0) AS tokens_used FROM h5_users u LEFT JOIN h5_user_wallets w ON w.user_id = u.id WHERE u.id = ? LIMIT 1`, [user.id], ); const updated = updatedRows[0]; console.log(''); console.log('Applied.'); console.log('New status:', updated.status); console.log('New balance:', `¥${(Number(updated.balance_cents) / 100).toFixed(2)}`); console.log('New tokens_used:', Number(updated.tokens_used).toLocaleString()); } finally { await pool.end(); } } main().catch((err) => { console.error(err); process.exit(1); });