2026-06-16 03:10:05 -07:00
|
|
|
|
#!/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;
|
2026-06-16 16:29:19 -07:00
|
|
|
|
/** Unique bills at 2/6 分/1k vs documented DeepSeek ×1 default (0.101/0.202 分/1k). */
|
2026-06-16 03:10:05 -07:00
|
|
|
|
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);
|
|
|
|
|
|
});
|