Fix duplicate session billing and align pricing with DeepSeek ×3.

Serialize billSessionUsage with row locks, expose rates on recharge, add compensation script and admin usage views.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
John
2026-06-16 03:10:05 -07:00
parent befeedfd37
commit 03690ee354
14 changed files with 472 additions and 52 deletions
+153
View File
@@ -0,0 +1,153 @@
#!/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 <username> [--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 ×3 default (0.302/0.605 分/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);
});