From d30bf958153257a9ed3e10d71757e7a7ee82e5d6 Mon Sep 17 00:00:00 2001 From: HaHafeng Date: Mon, 9 Mar 2026 20:30:52 +0800 Subject: [PATCH] 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 --- backend/.env.example | 13 +- backend/package-lock.json | 261 +++++++++++++++++- backend/package.json | 3 + backend/scripts/test-aliyun-sms.ts | 34 +++ backend/src/common/auth/auth.service.ts | 15 +- backend/src/common/sms/aliyunSms.service.ts | 92 ++++++ backend/src/config/env.ts | 25 ++ .../00-系统当前状态与开发指南.md | 8 +- docs/05-部署文档/03-待部署变更清单.md | 3 +- 9 files changed, 443 insertions(+), 11 deletions(-) create mode 100644 backend/scripts/test-aliyun-sms.ts create mode 100644 backend/src/common/sms/aliyunSms.service.ts diff --git a/backend/.env.example b/backend/.env.example index b93c5d29..7f3d9353 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -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 diff --git a/backend/package-lock.json b/backend/package-lock.json index 413bafde..0827b46a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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", diff --git a/backend/package.json b/backend/package.json index fbe743e4..f67bd09e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/scripts/test-aliyun-sms.ts b/backend/scripts/test-aliyun-sms.ts new file mode 100644 index 00000000..e8b0f4ca --- /dev/null +++ b/backend/scripts/test-aliyun-sms.ts @@ -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 { + 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() + diff --git a/backend/src/common/auth/auth.service.ts b/backend/src/common/auth/auth.service.ts index 6de6f30f..8bfc2af3 100644 --- a/backend/src/common/auth/auth.service.ts +++ b/backend/src/common/auth/auth.service.ts @@ -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 }); - console.log(`\n📱 验证码: ${code} (有效期5分钟)\n`); + // 非生产环境保留本地调试日志 + logger.info('📱 验证码已生成', { phone, type, expiresAt }); + if (process.env.NODE_ENV !== 'production') { + console.log(`\n📱 验证码: ${code} (有效期5分钟)\n`); + } return { expiresIn: 300 }; // 5分钟 } diff --git a/backend/src/common/sms/aliyunSms.service.ts b/backend/src/common/sms/aliyunSms.service.ts new file mode 100644 index 00000000..7e76466e --- /dev/null +++ b/backend/src/common/sms/aliyunSms.service.ts @@ -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 { + // 非阿里云模式:本地开发默认 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('短信发送失败,请稍后重试') + } +} + diff --git a/backend/src/config/env.ts b/backend/src/config/env.ts index 69faf90c..8828b993 100644 --- a/backend/src/config/env.ts +++ b/backend/src/config/env.ts @@ -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') { diff --git a/docs/00-系统总体设计/00-系统当前状态与开发指南.md b/docs/00-系统总体设计/00-系统当前状态与开发指南.md index 721b363f..cec9af6e 100644 --- a/docs/00-系统总体设计/00-系统当前状态与开发指南.md +++ b/docs/00-系统总体设计/00-系统当前状态与开发指南.md @@ -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-08:SSA 智能统计分析 Agent 模式 MVP 完成!** Agent 核心 Prompt 接入运营管理端(PlannerAgent + CoderAgent 动态化 + 三级容灾)+ Phase 5A 防错护栏 + Prompt 全景盘点(Agent 仅用 2 个 Prompt,QPER 11 个已归档) > - **🆕 2026-03-07:SSA Agent 通道体验优化 + Plan-and-Execute 架构设计完成!** 方案 B 左右职责分离 + 10 项 Bug 修复(JWT 刷新/代码截断/重试流式/R Docker 结构化错误/进度同步/导出按钮)+ 分步执行架构评审通过(代码累加策略 + 5 项工程护栏) > - **🆕 2026-03-05:RVW V3.0 智能审稿 + ASL Deep Research 历史 + 系统稳定性增强!** RVW LLM 数据核查 + 临床评估维度 + 并行 Skill 故障隔离 + ASL 研究历史/删除 + DeepSearch S3 升级 @@ -36,7 +37,8 @@ > - **2026-01-24:Protocol Agent 框架完成!** 可复用Agent框架+5阶段对话流程 > - **2026-01-22:OSS 存储集成完成!** 阿里云 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 份架构评审报告 diff --git a/docs/05-部署文档/03-待部署变更清单.md b/docs/05-部署文档/03-待部署变更清单.md index 43a06a7e..7f7fb9a9 100644 --- a/docs/05-部署文档/03-待部署变更清单.md +++ b/docs/05-部署文档/03-待部署变更清单.md @@ -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` 可选(默认复用登录模板) | ### 基础设施变更