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:
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user