6d99d762da
Introduce AI image generation with chat shortcut and agent API, improve MindSpace chat-to-page save resolution, and seed Plaza covers with production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
5.4 KiB
JavaScript
154 lines
5.4 KiB
JavaScript
#!/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 ×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);
|
||
});
|