积分与余额账务系统硬切重构

This commit is contained in:
2026-08-24 15:59:11 +08:00
parent ca12226b39
commit 5cae5b5c93
159 changed files with 7275 additions and 4718 deletions
@@ -0,0 +1,170 @@
const test = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const root = path.resolve(__dirname, '..', '..')
function sourceFiles(dir) {
const out = []
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.name === 'build' || entry.name === 'vendor' || entry.name === 'node_modules') continue
const full = path.join(dir, entry.name)
if (entry.isDirectory()) out.push(...sourceFiles(full))
else if (/\.(php|js|vue|cjs)$/.test(entry.name)) out.push(full)
}
return out
}
test('old asset implementations and compatibility aliases are absent', () => {
assert.equal(fs.existsSync(path.join(root, 'src', 'Credits', 'Credits.php')), false)
assert.equal(fs.existsSync(path.join(root, 'src', 'Credits', 'Balance.php')), false)
assert.equal(fs.existsSync(path.join(root, 'src', 'Payment', 'Rebate.php')), false)
assert.equal(fs.existsSync(path.join(root, 'src', 'Admin', 'Native', 'PayRebate.php')), false)
assert.equal(fs.existsSync(path.join(root, 'assets', 'admin', 'src', 'pages', 'payment', 'PaymentRebate.vue')), false)
assert.equal(fs.existsSync(path.join(root, 'assets', 'admin', 'src', 'pages', 'payment', 'components', 'RebateDetailDrawer.vue')), false)
const files = [
...sourceFiles(path.join(root, 'src')),
...sourceFiles(path.join(root, 'inc')),
...sourceFiles(path.join(root, 'assets', 'js')),
...sourceFiles(path.join(root, 'templates')),
]
const forbidden = [
/aksrt_credits(?:\b|_)/,
/['"]aksrt_balance['"]/,
/\bcredit_log\b/,
/\bbalance_log\b/,
/aksrt_credits_(?:added|deducted)/,
/aksrt_balance_(?:added|deducted)/,
/credits_(?:added|deducted)/,
/points_pass_exchange_/,
/legacy_task_points/,
/transfer_refund|recovery_failed/,
/\bPayRebate\b/,
/\bPaymentRebate\b/,
/\bRebateDetailDrawer\b/,
/\brebate_log\b/,
/\/payment\/rebate(?:\b|\/)/,
/\bpay_rebate\b/,
/aksrtpay_rebate_page/,
]
const violations = []
for (const file of files) {
const text = fs.readFileSync(file, 'utf8')
for (const pattern of forbidden) {
if (pattern.test(text)) violations.push(`${path.relative(root, file)}: ${pattern}`)
}
}
assert.deepEqual(violations, [])
})
test('asset tables are InnoDB and operation keys are unique', () => {
const schema = fs.readFileSync(path.join(root, 'src', 'Core', 'DBMigratorPart2.php'), 'utf8')
for (const table of ['asset_account', 'asset_transaction', 'asset_entry']) {
assert.match(schema, new RegExp(`CREATE TABLE \\{\\$prefix\\}aksrt_${table} \\([\\s\\S]*?\\) ENGINE=InnoDB`))
}
assert.match(schema, /UNIQUE KEY uniq_operation_key \(operation_key\)/)
assert.match(schema, /UNIQUE KEY uniq_transaction_line \(transaction_id, line_no\)/)
})
test('user asset submissions send idempotency keys', () => {
const request = fs.readFileSync(path.join(root, 'assets', 'js', 'core', 'request.js'), 'utf8')
assert.match(request, /createIdempotencyKey/)
for (const file of ['credits.js', 'reward.js', 'vip-purchase-coordinator.js', 'cms-download.js']) {
const source = fs.readFileSync(path.join(root, 'assets', 'js', file), 'utf8')
assert.match(source, /Idempotency-Key/, `${file} must send Idempotency-Key`)
}
})
test('core asset paths never use floating-point money helpers', () => {
const files = [
'src/Ledger/AssetAmount.php',
'src/Ledger/LedgerCommand.php',
'src/Ledger/LedgerService.php',
'src/Credits/BalanceService.php',
'src/Credits/BalanceServiceCharge.php',
'src/Credits/BalanceServiceRest.php',
'src/Payment/BalanceGateway.php',
'src/Payment/ChargeCard.php',
'src/Payment/CommissionLedger.php',
'src/Payment/Revenue.php',
'src/Payment/Withdrawal.php',
'src/Admin/UserController.php',
'src/Content/PayRest.php',
'src/Content/VipServicePurchase.php',
]
const forbidden = [/\(float\)/, /floatval\s*\(/, /round\s*\(/, /number_format\s*\(/]
const violations = []
for (const relative of files) {
const text = fs.readFileSync(path.join(root, relative), 'utf8')
for (const pattern of forbidden) {
if (pattern.test(text)) violations.push(`${relative}: ${pattern}`)
}
}
assert.deepEqual(violations, [])
})
test('content preview is read-only while purchase requires an operation key', () => {
const source = fs.readFileSync(path.join(root, 'src', 'Content', 'PayRest.php'), 'utf8')
const previewStart = source.indexOf('public function rest_preview')
const purchaseStart = source.indexOf('public function rest_purchase')
const purchaseEnd = source.indexOf('public function rest_order_status', purchaseStart)
assert.ok(previewStart >= 0 && purchaseStart > previewStart && purchaseEnd > purchaseStart)
assert.doesNotMatch(source.slice(previewStart, purchaseStart), /OperationKey::from_request/)
assert.match(source.slice(purchaseStart, purchaseEnd), /OperationKey::from_request\(\$request, 'content-purchase'/)
})
test('native financial actions and REST adjustments carry UUID operation keys', () => {
const income = fs.readFileSync(path.join(root, 'src', 'Admin', 'Native', 'PayIncome.php'), 'utf8')
const withdrawals = fs.readFileSync(path.join(root, 'src', 'Admin', 'Native', 'PayWithdrawals.php'), 'utf8')
const points = fs.readFileSync(path.join(root, 'src', 'User', 'CreditsAdmin.php'), 'utf8')
const balance = fs.readFileSync(path.join(root, 'src', 'User', 'BalanceAdmin.php'), 'utf8')
const restUsers = fs.readFileSync(path.join(root, 'src', 'Admin', 'UserController.php'), 'utf8')
for (const source of [income, withdrawals, points, balance]) {
assert.match(source, /wp_generate_uuid4\(\)/)
assert.match(source, /wp_is_uuid\(/)
}
assert.match(points, /PHP_INT_MAX/)
assert.match(points, /preg_match\('\/\^-\?/)
assert.match(points, /strcmp\(\$unsigned_delta, \$max_integer\)/)
assert.match(points, /current_user_can\('edit_user', \$user_id\)/)
assert.match(balance, /current_user_can\('edit_user', \$user_id\)/)
assert.match(restUsers, /OperationKey::from_request\(\$request, 'admin-adjustment'/)
assert.match(restUsers, /current_user_can\('edit_user', \$user_id\)/)
})
test('charge cards have no legacy input or metadata aliases', () => {
const source = fs.readFileSync(path.join(root, 'src', 'Payment', 'ChargeCard.php'), 'utf8')
assert.doesNotMatch(source, /\$params\[['"]code['"]\]/)
assert.doesNotMatch(source, /\$data\[['"]code['"]\]/)
assert.doesNotMatch(source, /\$raw\[['"]vip_days['"]\]/)
assert.doesNotMatch(source, /\$raw\[['"]credits['"]\]/)
assert.match(source, /\$params\['password'\]/)
assert.match(source, /\$raw\['days'\]/)
assert.match(source, /\$raw\['points'\]/)
})
test('VIP card redemption is one locked transaction with bounded retry and replay checks', () => {
const source = fs.readFileSync(path.join(root, 'src', 'Payment', 'ChargeCard.php'), 'utf8')
const start = source.indexOf('private function redeem_vip_card')
const end = source.indexOf('private function vip_card_replay_result', start)
const method = source.slice(start, end)
assert.ok(start >= 0 && end > start)
assert.match(method, /for \(\$attempt = 1; \$attempt <= 3; \$attempt\+\+\)/)
assert.match(method, /START TRANSACTION/)
assert.match(method, /FOR UPDATE/)
assert.match(method, /request_key = %s FOR UPDATE/)
assert.match(method, /COMMIT/)
assert.match(method, /ROLLBACK/)
assert.match(method, /\[1205, 1213\]/)
assert.match(method, /vip_card_replay_result/)
})
test('admin ledger keeps cash minor values as decimal strings', () => {
const controller = fs.readFileSync(path.join(root, 'src', 'Admin', 'UserController.php'), 'utf8')
const page = fs.readFileSync(path.join(root, 'assets', 'admin', 'src', 'pages', 'CmsCredits.vue'), 'utf8')
assert.match(controller, /asset_minor_for_response/)
assert.match(controller, /decimal_from_minor/)
assert.doesNotMatch(page, /\/\s*100(?:\D|$)/)
})