diff --git a/docs/refactor/production-launch-evidence.template.json b/docs/refactor/production-launch-evidence.template.json index 4c5c082f..a0f763e5 100644 --- a/docs/refactor/production-launch-evidence.template.json +++ b/docs/refactor/production-launch-evidence.template.json @@ -54,6 +54,19 @@ "requireAdminTokens": true } }, + { + "id": "auth.sms-pnvs-remote-smoke", + "status": "pass", + "command": "SMS_SMOKE_API_BASE_URL=https://api.example.com SMS_SMOKE_TENANT_ID=00000000-0000-0000-0000-000000000001 SMS_SMOKE_PHONE=replace-with-real-phone SMS_SMOKE_ORIGIN=https://admin.example.com npm run smoke:sms-login:remote -- --json > docs/refactor/launch-artifacts/sms-pnvs-remote-smoke.json", + "completedAt": "2026-06-30T10:12:00+08:00", + "artifact": "launch-artifacts/sms-pnvs-remote-smoke.json", + "summary": { + "failed": 0, + "provider": "aliyun-pnvs", + "loginVerified": true, + "authMe": true + } + }, { "id": "rls.tenant-isolation", "status": "pass", diff --git a/docs/refactor/web-launch-acceptance-checklist.md b/docs/refactor/web-launch-acceptance-checklist.md index 61e44874..35297c59 100644 --- a/docs/refactor/web-launch-acceptance-checklist.md +++ b/docs/refactor/web-launch-acceptance-checklist.md @@ -110,7 +110,7 @@ SMS_SMOKE_API_BASE_URL=https://api.tjszsb.com \ SMS_SMOKE_TENANT_ID=00000000-0000-0000-0000-000000000001 \ SMS_SMOKE_PHONE=13800138000 \ SMS_SMOKE_ORIGIN=https://admin.tjszsb.com \ -npm run smoke:sms-login:remote +npm run smoke:sms-login:remote -- --json > docs/refactor/launch-artifacts/sms-pnvs-remote-smoke.json ``` PNVS 只验收手机号登录/换绑验证码链路。催缴、营销、CRM 等非验证码短信不应接 PNVS;平台催缴通知仍按 webhook 类通知渠道验收。 diff --git a/scripts/deploy/README.md b/scripts/deploy/README.md index 8e3bf5b6..efa081e1 100644 --- a/scripts/deploy/README.md +++ b/scripts/deploy/README.md @@ -335,14 +335,14 @@ npm run readiness:production ```bash npm run readiness:production:db npm run smoke:auth:remote -SMS_SMOKE_API_BASE_URL=https://api.tjszsb.com SMS_SMOKE_TENANT_ID=00000000-0000-0000-0000-000000000001 SMS_SMOKE_PHONE=replace-with-real-phone SMS_SMOKE_ORIGIN=https://admin.tjszsb.com npm run smoke:sms-login:remote +SMS_SMOKE_API_BASE_URL=https://api.tjszsb.com SMS_SMOKE_TENANT_ID=00000000-0000-0000-0000-000000000001 SMS_SMOKE_PHONE=replace-with-real-phone SMS_SMOKE_ORIGIN=https://admin.tjszsb.com npm run smoke:sms-login:remote -- --json > docs/refactor/launch-artifacts/sms-pnvs-remote-smoke.json npm run perf:api:local ``` 如果要同时验证手机号绑定也走 PNVS provider verification,准备一个未绑定测试手机号后执行: ```bash -SMS_SMOKE_API_BASE_URL=https://api.tjszsb.com SMS_SMOKE_TENANT_ID=00000000-0000-0000-0000-000000000001 SMS_SMOKE_PHONE=replace-with-login-phone SMS_SMOKE_BIND_PHONE=replace-with-bind-phone SMS_SMOKE_ORIGIN=https://admin.tjszsb.com npm run smoke:sms-login:remote +SMS_SMOKE_API_BASE_URL=https://api.tjszsb.com SMS_SMOKE_TENANT_ID=00000000-0000-0000-0000-000000000001 SMS_SMOKE_PHONE=replace-with-login-phone SMS_SMOKE_BIND_PHONE=replace-with-bind-phone SMS_SMOKE_ORIGIN=https://admin.tjszsb.com npm run smoke:sms-login:remote -- --json > docs/refactor/launch-artifacts/sms-pnvs-remote-smoke.json ``` 压测必须在目标云服务器、目标数据库参数、目标对象存储和目标 Nginx 配置下重新计算,本地 Windows 压测数据只能作为开发参考。 diff --git a/scripts/production-launch-gate-test.js b/scripts/production-launch-gate-test.js index 3487c2a7..471b64e7 100644 --- a/scripts/production-launch-gate-test.js +++ b/scripts/production-launch-gate-test.js @@ -119,6 +119,29 @@ assert.ok( 'missing artifact should be reported as a blocker', ); +const missingSmsPnvsSmoke = runGate(tempDir => { + const evidence = createEvidence(tempDir); + evidence.checks = evidence.checks.filter(item => item.id !== 'auth.sms-pnvs-remote-smoke'); + return evidence; +}); +assert.notEqual(missingSmsPnvsSmoke.status, 0, 'missing PNVS SMS smoke should fail launch gate'); +assert.ok( + missingSmsPnvsSmoke.payload.checks?.some(item => item.id === 'check.auth.sms-pnvs-remote-smoke' && item.status === 'blocker'), + 'missing PNVS SMS smoke should be reported as a blocker', +); + +const wrongSmsProviderSmoke = runGate(tempDir => { + const evidence = createEvidence(tempDir); + const item = evidence.checks.find(check => check.id === 'auth.sms-pnvs-remote-smoke'); + item.summary.provider = 'aliyun'; + return evidence; +}); +assert.notEqual(wrongSmsProviderSmoke.status, 0, 'non-PNVS SMS smoke should fail launch gate'); +assert.ok( + wrongSmsProviderSmoke.payload.checks?.some(item => item.id === 'check.auth.sms-pnvs-remote-smoke.summary' && item.status === 'blocker'), + 'non-PNVS SMS smoke should be reported as a blocker', +); + const wrongMigrationProfile = runGate(tempDir => { const evidence = createEvidence(tempDir); const item = evidence.checks.find(check => check.id === 'migration.pb-production-dry-run'); diff --git a/scripts/production-launch-gate.js b/scripts/production-launch-gate.js index 8c1db992..1a85f6de 100644 --- a/scripts/production-launch-gate.js +++ b/scripts/production-launch-gate.js @@ -38,6 +38,17 @@ const gateChecks = [ commandIncludes: 'smoke:auth:remote', summary: { failed: 0, requireAdminTokens: true }, }, + { + id: 'auth.sms-pnvs-remote-smoke', + label: 'Remote Aliyun PNVS SMS login smoke', + commandIncludes: 'smoke:sms-login:remote', + summary: { + failed: 0, + provider: 'aliyun-pnvs', + loginVerified: true, + authMe: true, + }, + }, { id: 'rls.tenant-isolation', label: 'Runtime tenant RLS isolation', diff --git a/scripts/remote-sms-login-smoke-test.js b/scripts/remote-sms-login-smoke-test.js index cec9c516..1e79a6fb 100644 --- a/scripts/remote-sms-login-smoke-test.js +++ b/scripts/remote-sms-login-smoke-test.js @@ -96,6 +96,11 @@ try { assert.equal(result.sessionToken, 'session-token'); assert.equal(result.boundPhone, bindPhone); + assert.equal(result.summary.provider, 'aliyun-pnvs'); + assert.equal(result.summary.loginVerified, true); + assert.equal(result.summary.authMe, true); + assert.equal(result.summary.bindProvider, 'aliyun-pnvs'); + assert.equal(result.summary.bindVerified, true); assert.equal(seen.some(item => item.path === '/api/auth/sms/send' && item.tenantId === tenantId), true); assert.equal(seen.some(item => item.path === '/api/auth/sms/verify' && item.origin === 'https://admin.tjszsb.com'), true); assert.equal(seen.some(item => item.path === '/api/auth/me'), true); diff --git a/scripts/remote-sms-login-smoke.js b/scripts/remote-sms-login-smoke.js index 66470900..0c947e4e 100644 --- a/scripts/remote-sms-login-smoke.js +++ b/scripts/remote-sms-login-smoke.js @@ -13,6 +13,13 @@ function envNumber(env, key, fallback) { return Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback; } +function parseArgs(argv) { + return { + json: argv.includes('--json'), + quiet: argv.includes('--quiet'), + }; +} + function normalizeBaseUrl(value) { return value.replace(/\/+$/, ''); } @@ -137,6 +144,7 @@ async function runRemoteSmsLoginSmoke(inputConfig, options = {}) { if (!options.quiet) { console.log(`PASS sms.send provider=${send.payload?.item?.provider || 'unknown'} expireIn=${send.payload?.expireIn || '-'} cooldown=${send.payload?.cooldown || '-'}`); } + config.sendProvider = send.payload?.item?.provider || ''; } const code = await promptCode(config); @@ -160,6 +168,7 @@ async function runRemoteSmsLoginSmoke(inputConfig, options = {}) { } let boundPhone = null; + let bindProvider = ''; if (config.bindPhone) { const sendBind = await requestJson(config, '/api/auth/sms/send', { method: 'POST', @@ -167,8 +176,9 @@ async function runRemoteSmsLoginSmoke(inputConfig, options = {}) { body: { phone: config.bindPhone, purpose: 'bind_phone' }, }); assertOk('sms.bind.send', sendBind); + bindProvider = sendBind.payload?.item?.provider || ''; if (!options.quiet) { - console.log(`PASS sms.bind.send provider=${sendBind.payload?.item?.provider || 'unknown'} phone=${maskPhone(config.bindPhone)}`); + console.log(`PASS sms.bind.send provider=${bindProvider || 'unknown'} phone=${maskPhone(config.bindPhone)}`); } const bindCode = await promptBindCode(config); @@ -190,13 +200,30 @@ async function runRemoteSmsLoginSmoke(inputConfig, options = {}) { sessionToken: token, user: verify.payload?.user || verify.payload?.item || null, boundPhone, + summary: { + failed: 0, + provider: config.sendProvider || 'unknown', + loginVerified: true, + authMe: !config.skipMe, + bindProvider: bindProvider || undefined, + bindVerified: Boolean(boundPhone), + phoneMasked: maskPhone(config.phone), + bindPhoneMasked: config.bindPhone ? maskPhone(config.bindPhone) : undefined, + }, }; } async function main() { + const options = parseArgs(process.argv.slice(2)); try { - await runRemoteSmsLoginSmoke(); + const result = await runRemoteSmsLoginSmoke(null, { quiet: options.quiet || options.json }); + if (options.json) console.log(JSON.stringify(result.summary, null, 2)); } catch (error) { + if (options.json) { + console.log(JSON.stringify({ failed: 1, error: error.message }, null, 2)); + process.exitCode = 1; + return; + } console.error(error.message); if (error.response?.payload) console.error(JSON.stringify(error.response.payload, null, 2)); if (/Missing required remote SMS smoke env/.test(error.message)) { @@ -212,6 +239,7 @@ Optional: SMS_SMOKE_BIND_PHONE= SMS_SMOKE_BIND_CODE= SMS_SMOKE_SKIP_SEND=true + --json `); } process.exitCode = 1;