积分与余额账务系统硬切重构
This commit is contained in:
@@ -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|$)/)
|
||||
})
|
||||
Reference in New Issue
Block a user