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:
@@ -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
|
||||
|
||||
261
backend/package-lock.json
generated
261
backend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
34
backend/scripts/test-aliyun-sms.ts
Normal file
34
backend/scripts/test-aliyun-sms.ts
Normal 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()
|
||||
|
||||
@@ -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分钟
|
||||
}
|
||||
|
||||
92
backend/src/common/sms/aliyunSms.service.ts
Normal file
92
backend/src/common/sms/aliyunSms.service.ts
Normal 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('短信发送失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 份架构评审报告
|
||||
|
||||
@@ -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` 可选(默认复用登录模板) |
|
||||
|
||||
### 基础设施变更
|
||||
|
||||
|
||||
Reference in New Issue
Block a user