feat(auth): integrate Aliyun SMS verification flow with docs update

Wire auth verification-code delivery to Aliyun SMS with mock fallback, config validation, and a standalone SMS smoke-test script. Update deployment checklist and system status docs with required env vars and rollout notes.

Made-with: Cursor
This commit is contained in:
2026-03-09 20:30:52 +08:00
parent 5c5fec52c1
commit d30bf95815
9 changed files with 443 additions and 11 deletions

View File

@@ -1,4 +1,4 @@
# Database
# Database
DATABASE_URL=postgresql://postgres:postgres123@localhost:5432/ai_clinical_research?schema=public
# Redis
@@ -8,6 +8,17 @@ REDIS_URL=redis://localhost:6379
JWT_SECRET=your-secret-key-change-in-production
JWT_EXPIRES_IN=7d
# SMS (Auth Verification Code)
# development: mock / production: aliyun
SMS_PROVIDER=mock
SMS_ENDPOINT=dysmsapi.aliyuncs.com
SMS_SIGN_NAME=你的短信签名
SMS_TEMPLATE_CODE_LOGIN=SMS_xxx
SMS_TEMPLATE_CODE_RESET=SMS_xxx
# 阿里云凭据AK 方式,生产建议改为 RAM 角色等无 AK 方式)
ALIBABA_CLOUD_ACCESS_KEY_ID=your-access-key-id
ALIBABA_CLOUD_ACCESS_KEY_SECRET=your-access-key-secret
# LLM API
DEEPSEEK_API_KEY=sk-7f8cc37a79fa4799860b38fc7ba2e150
DASHSCOPE_API_KEY=sk-75b4ff29a14a49e79667a331034f3298

View File

@@ -9,6 +9,8 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@alicloud/credentials": "^2.4.4",
"@alicloud/dysmsapi20170525": "^4.5.0",
"@fastify/cors": "^11.1.0",
"@fastify/jwt": "^10.0.0",
"@fastify/multipart": "^9.2.1",
@@ -63,6 +65,174 @@
"typescript": "^5.9.3"
}
},
"node_modules/@alicloud/credentials": {
"version": "2.4.4",
"resolved": "https://registry.npmmirror.com/@alicloud/credentials/-/credentials-2.4.4.tgz",
"integrity": "sha512-/eRAGSKcniLIFQ1UCpDhB/IrHUZisQ1sc65ws/c2avxUMpXwH1rWAohb76SVAUJhiF4mwvLzLJM1Mn1XL4Xe/Q==",
"license": "MIT",
"dependencies": {
"@alicloud/tea-typescript": "^1.8.0",
"httpx": "^2.3.3",
"ini": "^1.3.5",
"kitx": "^2.0.0"
}
},
"node_modules/@alicloud/darabonba-array": {
"version": "0.1.2",
"resolved": "https://registry.npmmirror.com/@alicloud/darabonba-array/-/darabonba-array-0.1.2.tgz",
"integrity": "sha512-ZPuQ+bJyjrd8XVVm55kl+ypk7OQoi1ZH/DiToaAEQaGvgEjrTcvQkg71//vUX/6cvbLIF5piQDvhrLb+lUEIPQ==",
"license": "ISC",
"dependencies": {
"@alicloud/tea-typescript": "^1.7.1"
}
},
"node_modules/@alicloud/darabonba-encode-util": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.2.tgz",
"integrity": "sha512-mlsNctkeqmR0RtgE1Rngyeadi5snLOAHBCWEtYf68d7tyKskosXDTNeZ6VCD/UfrUu4N51ItO8zlpfXiOgeg3A==",
"license": "ISC",
"dependencies": {
"moment": "^2.29.1"
}
},
"node_modules/@alicloud/darabonba-map": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/@alicloud/darabonba-map/-/darabonba-map-0.0.1.tgz",
"integrity": "sha512-2ep+G3YDvuI+dRYVlmER1LVUQDhf9kEItmVB/bbEu1pgKzelcocCwAc79XZQjTcQGFgjDycf3vH87WLDGLFMlw==",
"license": "ISC",
"dependencies": {
"@alicloud/tea-typescript": "^1.7.1"
}
},
"node_modules/@alicloud/darabonba-signature-util": {
"version": "0.0.4",
"resolved": "https://registry.npmmirror.com/@alicloud/darabonba-signature-util/-/darabonba-signature-util-0.0.4.tgz",
"integrity": "sha512-I1TtwtAnzLamgqnAaOkN0IGjwkiti//0a7/auyVThdqiC/3kyafSAn6znysWOmzub4mrzac2WiqblZKFcN5NWg==",
"license": "ISC",
"dependencies": {
"@alicloud/darabonba-encode-util": "^0.0.1"
}
},
"node_modules/@alicloud/darabonba-signature-util/node_modules/@alicloud/darabonba-encode-util": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.1.tgz",
"integrity": "sha512-Sl5vCRVAYMqwmvXpJLM9hYoCHOMsQlGxaWSGhGWulpKk/NaUBArtoO1B0yHruJf1C5uHhEJIaylYcM48icFHgw==",
"license": "ISC",
"dependencies": {
"@alicloud/tea-typescript": "^1.7.1",
"moment": "^2.29.1"
}
},
"node_modules/@alicloud/darabonba-string": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@alicloud/darabonba-string/-/darabonba-string-1.0.3.tgz",
"integrity": "sha512-NyWwrU8cAIesWk3uHL1Q7pTDTqLkCI/0PmJXC4/4A0MFNAZ9Ouq0iFBsRqvfyUujSSM+WhYLuTfakQXiVLkTMA==",
"license": "Apache-2.0",
"dependencies": {
"@alicloud/tea-typescript": "^1.5.1"
}
},
"node_modules/@alicloud/dysmsapi20170525": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/@alicloud/dysmsapi20170525/-/dysmsapi20170525-4.5.0.tgz",
"integrity": "sha512-nhKdRDLRDhTVxr7VbMbBi6UtJWmVFgwySU2ohkJ1zL7jd98DEGGy8CE/n7W44ZP9+yTBBmLhM8qW1C12kHDEIg==",
"license": "Apache-2.0",
"dependencies": {
"@alicloud/openapi-core": "^1.0.0",
"@darabonba/typescript": "^1.0.0"
}
},
"node_modules/@alicloud/endpoint-util": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/@alicloud/endpoint-util/-/endpoint-util-0.0.1.tgz",
"integrity": "sha512-+pH7/KEXup84cHzIL6UJAaPqETvln4yXlD9JzlrqioyCSaWxbug5FUobsiI6fuUOpw5WwoB3fWAtGbFnJ1K3Yg==",
"license": "Apache-2.0",
"dependencies": {
"@alicloud/tea-typescript": "^1.5.1",
"kitx": "^2.0.0"
}
},
"node_modules/@alicloud/gateway-pop": {
"version": "0.0.6",
"resolved": "https://registry.npmmirror.com/@alicloud/gateway-pop/-/gateway-pop-0.0.6.tgz",
"integrity": "sha512-KF4I+JvfYuLKc3fWeWYIZ7lOVJ9jRW0sQXdXidZn1DKZ978ncfGf7i0LBfONGk4OxvNb/HD3/0yYhkgZgPbKtA==",
"license": "ISC",
"dependencies": {
"@alicloud/credentials": "^2",
"@alicloud/darabonba-array": "^0.1.0",
"@alicloud/darabonba-encode-util": "^0.0.2",
"@alicloud/darabonba-map": "^0.0.1",
"@alicloud/darabonba-signature-util": "^0.0.4",
"@alicloud/darabonba-string": "^1.0.2",
"@alicloud/endpoint-util": "^0.0.1",
"@alicloud/gateway-spi": "^0.0.8",
"@alicloud/openapi-util": "^0.3.2",
"@alicloud/tea-typescript": "^1.7.1",
"@alicloud/tea-util": "^1.4.8"
}
},
"node_modules/@alicloud/gateway-spi": {
"version": "0.0.8",
"resolved": "https://registry.npmmirror.com/@alicloud/gateway-spi/-/gateway-spi-0.0.8.tgz",
"integrity": "sha512-KM7fu5asjxZPmrz9sJGHJeSU+cNQNOxW+SFmgmAIrITui5hXL2LB+KNRuzWmlwPjnuA2X3/keq9h6++S9jcV5g==",
"license": "ISC",
"dependencies": {
"@alicloud/credentials": "^2",
"@alicloud/tea-typescript": "^1.7.1"
}
},
"node_modules/@alicloud/openapi-core": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/@alicloud/openapi-core/-/openapi-core-1.0.7.tgz",
"integrity": "sha512-I80PQVfmlzRiXGHwutMp2zTpiqUVv8ts30nWAfksfHUSTIapk3nj9IXaPbULMPGNV6xqEyshO2bj2a+pmwc2tQ==",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"@alicloud/credentials": "^2.4.2",
"@alicloud/gateway-pop": "0.0.6",
"@alicloud/gateway-spi": "^0.0.8",
"@darabonba/typescript": "^1.0.2"
}
},
"node_modules/@alicloud/openapi-util": {
"version": "0.3.3",
"resolved": "https://registry.npmmirror.com/@alicloud/openapi-util/-/openapi-util-0.3.3.tgz",
"integrity": "sha512-vf0cQ/q8R2U7ZO88X5hDiu1yV3t/WexRj+YycWxRutkH/xVXfkmpRgps8lmNEk7Ar+0xnY8+daN2T+2OyB9F4A==",
"license": "ISC",
"dependencies": {
"@alicloud/tea-typescript": "^1.7.1",
"@alicloud/tea-util": "^1.3.0",
"kitx": "^2.1.0",
"sm3": "^1.0.3"
}
},
"node_modules/@alicloud/tea-typescript": {
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/@alicloud/tea-typescript/-/tea-typescript-1.8.0.tgz",
"integrity": "sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==",
"license": "ISC",
"dependencies": {
"@types/node": "^12.0.2",
"httpx": "^2.2.6"
}
},
"node_modules/@alicloud/tea-typescript/node_modules/@types/node": {
"version": "12.20.55",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-12.20.55.tgz",
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
"license": "MIT"
},
"node_modules/@alicloud/tea-util": {
"version": "1.4.11",
"resolved": "https://registry.npmmirror.com/@alicloud/tea-util/-/tea-util-1.4.11.tgz",
"integrity": "sha512-HyPEEQ8F0WoZegiCp7sVdrdm6eBOB+GCvGl4182u69LDFktxfirGLcAx3WExUr1zFWkq2OSmBroTwKQ4w/+Yww==",
"license": "Apache-2.0",
"dependencies": {
"@alicloud/tea-typescript": "^1.5.1",
"@darabonba/typescript": "^1.0.0",
"kitx": "^2.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz",
@@ -105,6 +275,20 @@
"kuler": "^2.0.0"
}
},
"node_modules/@darabonba/typescript": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/@darabonba/typescript/-/typescript-1.0.4.tgz",
"integrity": "sha512-icl8RGTw4DiWRpco6dVh21RS0IqrH4s/eEV36TZvz/e1+paogSZjaAgox7ByrlEuvG+bo5d8miq/dRlqiUaL/w==",
"license": "Apache License 2.0",
"dependencies": {
"@alicloud/tea-typescript": "^1.5.1",
"httpx": "^2.3.2",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"xml2js": "^0.6.2"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.10",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
@@ -3143,6 +3327,31 @@
"node": ">=8.0.0"
}
},
"node_modules/httpx": {
"version": "2.3.3",
"resolved": "https://registry.npmmirror.com/httpx/-/httpx-2.3.3.tgz",
"integrity": "sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==",
"license": "MIT",
"dependencies": {
"@types/node": "^20",
"debug": "^4.1.1"
}
},
"node_modules/httpx/node_modules/@types/node": {
"version": "20.19.37",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.37.tgz",
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/httpx/node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz",
@@ -3218,7 +3427,6 @@
"version": "1.3.8",
"resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true,
"license": "ISC"
},
"node_modules/iobuffer": {
@@ -3553,6 +3761,30 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/kitx": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/kitx/-/kitx-2.2.0.tgz",
"integrity": "sha512-tBMwe6AALTBQJb0woQDD40734NKzb0Kzi3k7wQj9ar3AbP9oqhoVrdXPh7rk2r00/glIgd0YbToIUJsnxWMiIg==",
"license": "MIT",
"dependencies": {
"@types/node": "^22.5.4"
}
},
"node_modules/kitx/node_modules/@types/node": {
"version": "22.19.15",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.15.tgz",
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/kitx/node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/kuler/-/kuler-2.0.0.tgz",
@@ -3947,6 +4179,27 @@
"obliterator": "^2.0.4"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/moment-timezone": {
"version": "0.5.48",
"resolved": "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.48.tgz",
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
"license": "MIT",
"dependencies": {
"moment": "^2.29.4"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
@@ -5249,6 +5502,12 @@
"integrity": "sha512-NvFvl1GuLZNW4U046Tfi8b26zXo8aBzgCAS2f7yVJR/fArN93mOqSA99cB9uITm92ajSz01bsu1K7SCVVjIMpQ==",
"license": "MIT"
},
"node_modules/sm3": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/sm3/-/sm3-1.0.3.tgz",
"integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==",
"license": "MIT"
},
"node_modules/sonic-boom": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/sonic-boom/-/sonic-boom-4.2.0.tgz",

View File

@@ -12,6 +12,7 @@
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio",
"prisma:seed": "tsx prisma/seed.ts",
"test:sms": "tsx scripts/test-aliyun-sms.ts",
"iit:equery:dedupe": "tsx scripts/dedupe_open_equeries.ts",
"iit:equery:dedupe:apply": "tsx scripts/dedupe_open_equeries.ts --apply",
"iit:guard:check": "tsx scripts/validate_guard_types_for_project.ts",
@@ -31,6 +32,8 @@
"author": "AI Clinical Dev Team",
"license": "ISC",
"dependencies": {
"@alicloud/credentials": "^2.4.4",
"@alicloud/dysmsapi20170525": "^4.5.0",
"@fastify/cors": "^11.1.0",
"@fastify/jwt": "^10.0.0",
"@fastify/multipart": "^9.2.1",

View File

@@ -0,0 +1,34 @@
/**
* 阿里云短信联调脚本
*
* 用法:
* npm run test:sms -- 13800138000 LOGIN
* npm run test:sms -- 13800138000 RESET_PASSWORD
*/
import { sendVerificationCodeSms } from '../src/common/sms/aliyunSms.service.js'
import { config } from '../src/config/env.js'
function usage(): never {
console.log('用法: npm run test:sms -- <手机号> [LOGIN|RESET_PASSWORD]')
process.exit(1)
}
async function main(): Promise<void> {
const phone = process.argv[2]
const type = (process.argv[3] || 'LOGIN') as 'LOGIN' | 'RESET_PASSWORD'
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) usage()
if (type !== 'LOGIN' && type !== 'RESET_PASSWORD') usage()
if (config.smsProvider !== 'aliyun') {
console.error('当前 SMS_PROVIDER 不是 aliyun请先在 .env 中设置 SMS_PROVIDER=aliyun')
process.exit(1)
}
const code = Math.floor(100000 + Math.random() * 900000).toString()
await sendVerificationCodeSms(phone, code, type)
console.log(`短信发送完成,手机号: ${phone}, 类型: ${type}, 验证码: ${code}`)
}
void main()

View File

@@ -13,6 +13,7 @@ import { prisma } from '../../config/database.js';
import { jwtService } from './jwt.service.js';
import type { JWTPayload, TokenResponse } from './jwt.service.js';
import { logger } from '../logging/index.js';
import { sendVerificationCodeSms } from '../sms/aliyunSms.service.js';
/**
* 登录请求 - 密码方式
@@ -419,7 +420,10 @@ export class AuthService {
// 4. 设置5分钟过期
const expiresAt = new Date(Date.now() + 5 * 60 * 1000);
// 5. 保存验证码
// 5. 发送短信(失败则不落库,避免生成无效验证码
await sendVerificationCodeSms(phone, code, type);
// 6. 保存验证码
await prisma.verification_codes.create({
data: {
phone,
@@ -429,10 +433,11 @@ export class AuthService {
},
});
// TODO: 实际发送短信
// 开发环境直接打印验证码
logger.info('📱 验证码已生成', { phone, code, type, expiresAt });
// 非生产环境保留本地调试日志
logger.info('📱 验证码已生成', { phone, type, expiresAt });
if (process.env.NODE_ENV !== 'production') {
console.log(`\n📱 验证码: ${code} (有效期5分钟)\n`);
}
return { expiresIn: 300 }; // 5分钟
}

View File

@@ -0,0 +1,92 @@
import { createRequire } from 'module'
import { config } from '../../config/env.js'
import { logger } from '../logging/index.js'
const require = createRequire(import.meta.url)
const dysmsClientModule = require('@alicloud/dysmsapi20170525/dist/client.js')
const credentialModule = require('@alicloud/credentials/dist/src/client.js')
const DysmsClient = dysmsClientModule.default as any
const SendSmsRequest = dysmsClientModule.SendSmsRequest as any
const Credential = credentialModule.default as any
export type SmsCodeType = 'LOGIN' | 'RESET_PASSWORD'
function resolveTemplateCode(type: SmsCodeType): string {
return type === 'RESET_PASSWORD'
? (config.smsTemplateCodeReset || config.smsTemplateCodeLogin)
: config.smsTemplateCodeLogin
}
function createClient(): any {
const credential = new Credential()
const clientConfig = {
credential,
endpoint: config.smsEndpoint || 'dysmsapi.aliyuncs.com',
}
return new DysmsClient(clientConfig as any)
}
function assertSmsConfig(type: SmsCodeType): void {
const templateCode = resolveTemplateCode(type)
if (!config.smsSignName) {
throw new Error('短信配置缺失SMS_SIGN_NAME 未设置')
}
if (!templateCode) {
throw new Error('短信配置缺失SMS_TEMPLATE_CODE_LOGIN / SMS_TEMPLATE_CODE_RESET 未设置')
}
}
export async function sendVerificationCodeSms(
phone: string,
code: string,
type: SmsCodeType,
): Promise<void> {
// 非阿里云模式:本地开发默认 mock避免阻塞联调
if (config.smsProvider !== 'aliyun') {
logger.info('[SMS] 非 aliyun 模式,跳过真实短信发送', {
phone,
type,
provider: config.smsProvider,
code: config.nodeEnv !== 'production' ? code : '[hidden]',
})
return
}
assertSmsConfig(type)
const client = createClient()
const request = new SendSmsRequest({
signName: config.smsSignName,
templateCode: resolveTemplateCode(type),
phoneNumbers: phone,
templateParam: JSON.stringify({ code }),
})
try {
const resp = await client.sendSms(request)
if (resp.body?.code !== 'OK') {
logger.error('[SMS] 阿里云短信发送失败', {
phone,
type,
code: resp.body?.code,
message: resp.body?.message,
requestId: resp.body?.requestId,
})
throw new Error(resp.body?.message || '短信发送失败')
}
logger.info('[SMS] 阿里云短信发送成功', {
phone,
type,
bizId: resp.body?.bizId,
requestId: resp.body?.requestId,
})
} catch (error: any) {
logger.error('[SMS] 阿里云短信调用异常', {
phone,
type,
error: error?.message || String(error),
recommend: error?.data?.Recommend,
})
throw new Error('短信发送失败,请稍后重试')
}
}

View File

@@ -123,6 +123,23 @@ export const config = {
/** CORS允许的源 */
corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:5173',
// ==================== 短信配置(认证模块)====================
/** 短信服务商mock | aliyun */
smsProvider: process.env.SMS_PROVIDER || (process.env.NODE_ENV === 'production' ? 'aliyun' : 'mock'),
/** 阿里云短信 Endpoint */
smsEndpoint: process.env.SMS_ENDPOINT || 'dysmsapi.aliyuncs.com',
/** 短信签名 */
smsSignName: process.env.SMS_SIGN_NAME || '',
/** 登录验证码模板 */
smsTemplateCodeLogin: process.env.SMS_TEMPLATE_CODE_LOGIN || '',
/** 找回密码验证码模板(可选,不填则复用登录模板) */
smsTemplateCodeReset: process.env.SMS_TEMPLATE_CODE_RESET || '',
// ==================== LLM API配置 ====================
/** DeepSeek API Key */
@@ -232,6 +249,14 @@ export function validateEnv(): void {
}
}
// 使用阿里云短信时,检查必要配置
if (config.smsProvider === 'aliyun') {
if (!config.smsSignName) errors.push('SMS_SIGN_NAME is required when SMS_PROVIDER=aliyun')
if (!config.smsTemplateCodeLogin) {
errors.push('SMS_TEMPLATE_CODE_LOGIN is required when SMS_PROVIDER=aliyun')
}
}
// ========== 安全配置验证 ==========
if (config.nodeEnv === 'production') {

View File

@@ -1,10 +1,11 @@
# AIclinicalresearch 系统当前状态与开发指南
> **文档版本:** v6.8
> **文档版本:** v6.9
> **创建日期:** 2025-11-28
> **维护者:** 开发团队
> **最后更新:** 2026-03-08
> **最后更新:** 2026-03-09
> **🎉 重大里程碑:**
> - **🆕 2026-03-09认证模块接入阿里云短信验证码** 登录验证码链路支持 `mock/aliyun` 双模式 + 后端短信服务封装 + 独立联调脚本(`npm run test:sms`+ 实机发送验证通过
> - **🆕 2026-03-08SSA 智能统计分析 Agent 模式 MVP 完成!** Agent 核心 Prompt 接入运营管理端PlannerAgent + CoderAgent 动态化 + 三级容灾)+ Phase 5A 防错护栏 + Prompt 全景盘点Agent 仅用 2 个 PromptQPER 11 个已归档)
> - **🆕 2026-03-07SSA Agent 通道体验优化 + Plan-and-Execute 架构设计完成!** 方案 B 左右职责分离 + 10 项 Bug 修复JWT 刷新/代码截断/重试流式/R Docker 结构化错误/进度同步/导出按钮)+ 分步执行架构评审通过(代码累加策略 + 5 项工程护栏)
> - **🆕 2026-03-05RVW V3.0 智能审稿 + ASL Deep Research 历史 + 系统稳定性增强!** RVW LLM 数据核查 + 临床评估维度 + 并行 Skill 故障隔离 + ASL 研究历史/删除 + DeepSearch S3 升级
@@ -36,7 +37,8 @@
> - **2026-01-24Protocol Agent 框架完成!** 可复用Agent框架+5阶段对话流程
> - **2026-01-22OSS 存储集成完成!** 阿里云 OSS 正式接入平台基础层
>
> **🆕 最新进展(SSA Agent MVP + RVW V3.0 2026-03-08**
> **🆕 最新进展(含认证短信集成 2026-03-09**
> - ✅ **🆕 认证短信验证码接入完成** — `sendVerificationCode` 接入阿里云短信网关(保留 `mock`+ 发送成功后再落库验证码 + 环境变量校验 + 联调脚本 `test:sms` + 实机发送验证通过
> - ✅ **🆕 SSA Agent 模式 MVP 完成** — Agent 核心 Prompt 接入运营管理端(`SSA_AGENT_PLANNER` + `SSA_AGENT_CODER` 动态化)+ 三级容灾DB→缓存→fallback+ 种子脚本幂等写入 + Prompt 全景盘点Agent 2 个 / QPER 11 个归档)
> - ✅ **🆕 SSA Agent 通道体验优化12 文件, +931/-203 行)** — 方案 B 左右职责分离 + JWT 刷新 + 代码截断修复 + 重试流式生成 + R Docker 结构化错误20+ 模式)+ Prompt 铁律 + 进度同步 + 导出按钮恢复 + ExecutingProgress 动态 UI
> - ✅ **🆕 Plan-and-Execute 分步执行架构设计完成** — 代码累加策略 + 5 项工程护栏XML 标签/AST 预检/防御性 Prompt/高保真 Schema/错误分类短路)+ 3 份架构评审报告

View File

@@ -25,6 +25,7 @@
| BE-2 | 优雅停机增强:健康检查停机时返回 503 + 30s 强制超时兜底 | `healthCheck.ts`, `health/index.ts`, `index.ts` | 重新构建镜像 | CLB 在滚动更新时不再向濒死 Pod 派发请求 |
| BE-3 | AIA 附件链路稳定性修复(上传落库 + 发送回源 + 错误分层) | `aia/services/attachmentService.ts`, `aia/services/conversationService.ts` | 重新构建镜像 | 上传阶段持久化附件文本与提取状态;发送时缓存未命中自动回源 DB 并回填,显著降低“对话中途上传附件无法识别”概率 |
| BE-4 | 生产环境缓存安全护栏:禁止 `CACHE_TYPE=memory` 启动 | `config/env.ts` | 重新构建镜像 | 防止多实例缓存不共享导致附件/会话等状态偶发丢失,符合云原生规范 |
| BE-5 | 登录验证码接入阿里云短信(保留 mock 模式) | `auth.service.ts`, `common/sms/aliyunSms.service.ts`, `config/env.ts` | 重新构建镜像 | `sendVerificationCode` 改为真实短信发送;生产建议 `SMS_PROVIDER=aliyun`,开发可继续 `mock` |
### 前端变更
@@ -49,7 +50,7 @@
| # | 变更内容 | 服务 | 变量名 | 备注 |
|---|---------|------|--------|------|
| — | *暂无* | | | |
| ENV-1 | 新增短信网关配置(登录验证码) | nodejs-backend | `SMS_PROVIDER`,`SMS_ENDPOINT`,`SMS_SIGN_NAME`,`SMS_TEMPLATE_CODE_LOGIN`,`SMS_TEMPLATE_CODE_RESET`,`ALIBABA_CLOUD_ACCESS_KEY_ID`,`ALIBABA_CLOUD_ACCESS_KEY_SECRET` | 若生产使用阿里云短信,需在 SAE 配置完整变量;`SMS_TEMPLATE_CODE_RESET` 可选(默认复用登录模板) |
### 基础设施变更