From 41b8a8fcfb46c2f66829bb099af604bf719b1f49 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 3 Mar 2026 05:13:16 -0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20SMS=20=E6=A8=A1=E6=9D=BF=E6=8C=89?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=88=86=E5=8F=91=20+=20=E9=98=BF=E9=87=8C?= =?UTF-8?q?=E4=BA=91=208=20=E6=A8=A1=E6=9D=BF=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构 auth-service 短信发送逻辑,从单一模板改为按验证类型分发不同模板。 变更: - SmsVerificationType 枚举新增 3 类型: IDENTITY_VERIFY, TRANSACTION_CONFIRM, PAYMENT_VERIFY - AliyunSmsProvider.getTemplateCode() 通过 TEMPLATE_ENV_MAP 按类型查找环境变量 优先使用类型专属模板,fallback 到通用 ALIYUN_SMS_TEMPLATE_CODE - 无模板配置时返回错误而非发送空模板 - 日志增加类型和模板代码,便于排查 阿里云已创建 8 个 Genex 短信模板: - SMS_501745796 注册验证码 (已通过) - SMS_501720822 登录验证码 - SMS_501735781 重置密码 - SMS_501925825 身份验证 (已通过) - SMS_501820752 交易确认 (已通过) - SMS_501855782 支付验证 (已通过) - SMS_501780799 异常登录提醒 (已通过) - SMS_501810819 账户变更通知 环境变量: - .env.example / docker-compose.yml 新增 ALIYUN_SMS_TPL_* 共 7 项 - aliyun_sms_manager.py 迁移到 CreateSmsTemplate 新 API (旧 AddSmsTemplate 已下线) Co-Authored-By: Claude Opus 4.6 --- backend/.env.example | 7 ++++ backend/docker-compose.yml | 7 ++++ backend/services/auth-service/.env.example | 9 ++++- .../entities/sms-verification.entity.ts | 3 ++ .../infrastructure/sms/aliyun-sms.provider.ts | 37 +++++++++++++++---- scripts/aliyun_sms_manager.py | 16 ++++++-- 6 files changed, 66 insertions(+), 13 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index dae86fe..a042f6d 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -62,6 +62,13 @@ ALIYUN_ACCESS_KEY_ID= ALIYUN_ACCESS_KEY_SECRET= ALIYUN_SMS_SIGN_NAME=券金融 ALIYUN_SMS_TEMPLATE_CODE= +ALIYUN_SMS_TPL_REGISTER=SMS_501745796 +ALIYUN_SMS_TPL_LOGIN=SMS_501720822 +ALIYUN_SMS_TPL_RESET_PASSWORD=SMS_501735781 +ALIYUN_SMS_TPL_CHANGE_PHONE=SMS_501925825 +ALIYUN_SMS_TPL_IDENTITY_VERIFY=SMS_501925825 +ALIYUN_SMS_TPL_TRANSACTION=SMS_501820752 +ALIYUN_SMS_TPL_PAYMENT=SMS_501855782 ALIYUN_SMS_ENDPOINT=dysmsapi.aliyuncs.com # --- Account Lockout --- diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 8936dde..1fc0aa2 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -516,6 +516,13 @@ services: - ALIYUN_ACCESS_KEY_SECRET=${ALIYUN_ACCESS_KEY_SECRET:-} - ALIYUN_SMS_SIGN_NAME=${ALIYUN_SMS_SIGN_NAME:-} - ALIYUN_SMS_TEMPLATE_CODE=${ALIYUN_SMS_TEMPLATE_CODE:-} + - ALIYUN_SMS_TPL_REGISTER=${ALIYUN_SMS_TPL_REGISTER:-SMS_501745796} + - ALIYUN_SMS_TPL_LOGIN=${ALIYUN_SMS_TPL_LOGIN:-SMS_501720822} + - ALIYUN_SMS_TPL_RESET_PASSWORD=${ALIYUN_SMS_TPL_RESET_PASSWORD:-SMS_501735781} + - ALIYUN_SMS_TPL_CHANGE_PHONE=${ALIYUN_SMS_TPL_CHANGE_PHONE:-SMS_501925825} + - ALIYUN_SMS_TPL_IDENTITY_VERIFY=${ALIYUN_SMS_TPL_IDENTITY_VERIFY:-SMS_501925825} + - ALIYUN_SMS_TPL_TRANSACTION=${ALIYUN_SMS_TPL_TRANSACTION:-SMS_501820752} + - ALIYUN_SMS_TPL_PAYMENT=${ALIYUN_SMS_TPL_PAYMENT:-SMS_501855782} - ALIYUN_SMS_ENDPOINT=${ALIYUN_SMS_ENDPOINT:-dysmsapi.aliyuncs.com} depends_on: postgres: diff --git a/backend/services/auth-service/.env.example b/backend/services/auth-service/.env.example index 0cb778d..edc803b 100644 --- a/backend/services/auth-service/.env.example +++ b/backend/services/auth-service/.env.example @@ -25,7 +25,14 @@ SMS_MAX_VERIFY_ATTEMPTS=5 # ALIYUN_ACCESS_KEY_ID= # ALIYUN_ACCESS_KEY_SECRET= # ALIYUN_SMS_SIGN_NAME=券金融 -# ALIYUN_SMS_TEMPLATE_CODE=SMS_123456789 +# ALIYUN_SMS_TEMPLATE_CODE=SMS_501745796 +# ALIYUN_SMS_TPL_REGISTER=SMS_501745796 +# ALIYUN_SMS_TPL_LOGIN=SMS_501720822 +# ALIYUN_SMS_TPL_RESET_PASSWORD=SMS_501735781 +# ALIYUN_SMS_TPL_CHANGE_PHONE=SMS_501925825 +# ALIYUN_SMS_TPL_IDENTITY_VERIFY=SMS_501925825 +# ALIYUN_SMS_TPL_TRANSACTION=SMS_501820752 +# ALIYUN_SMS_TPL_PAYMENT=SMS_501855782 # ── Kafka (optional, events silently skipped if unavailable) ── KAFKA_BROKERS=localhost:9092 diff --git a/backend/services/auth-service/src/domain/entities/sms-verification.entity.ts b/backend/services/auth-service/src/domain/entities/sms-verification.entity.ts index 90ba04d..3e166c2 100644 --- a/backend/services/auth-service/src/domain/entities/sms-verification.entity.ts +++ b/backend/services/auth-service/src/domain/entities/sms-verification.entity.ts @@ -11,6 +11,9 @@ export enum SmsVerificationType { LOGIN = 'LOGIN', RESET_PASSWORD = 'RESET_PASSWORD', CHANGE_PHONE = 'CHANGE_PHONE', + IDENTITY_VERIFY = 'IDENTITY_VERIFY', + TRANSACTION_CONFIRM = 'TRANSACTION_CONFIRM', + PAYMENT_VERIFY = 'PAYMENT_VERIFY', } @Entity('sms_verifications') diff --git a/backend/services/auth-service/src/infrastructure/sms/aliyun-sms.provider.ts b/backend/services/auth-service/src/infrastructure/sms/aliyun-sms.provider.ts index 8669a73..ec88143 100644 --- a/backend/services/auth-service/src/infrastructure/sms/aliyun-sms.provider.ts +++ b/backend/services/auth-service/src/infrastructure/sms/aliyun-sms.provider.ts @@ -5,14 +5,17 @@ import { ISmsProvider, SmsDeliveryResult } from './sms-provider.interface'; /** * 阿里云 SMS Provider * - * 参考 rwdurian identity-service 已验证的实现, - * 使用 sendSmsWithOptions + RuntimeOptions 调用阿里云短信 API。 - * * 环境变量: - * - ALIYUN_ACCESS_KEY_ID - * - ALIYUN_ACCESS_KEY_SECRET + * - ALIYUN_ACCESS_KEY_ID / ALIYUN_ACCESS_KEY_SECRET * - ALIYUN_SMS_SIGN_NAME (签名名称) - * - ALIYUN_SMS_TEMPLATE_CODE (模板代码) + * - ALIYUN_SMS_TEMPLATE_CODE (通用 fallback 模板) + * - ALIYUN_SMS_TPL_REGISTER 注册验证码 + * - ALIYUN_SMS_TPL_LOGIN 登录验证码 + * - ALIYUN_SMS_TPL_RESET_PASSWORD 重置密码 + * - ALIYUN_SMS_TPL_CHANGE_PHONE 换绑手机(复用身份验证模板) + * - ALIYUN_SMS_TPL_IDENTITY_VERIFY 身份验证 + * - ALIYUN_SMS_TPL_TRANSACTION 交易确认 + * - ALIYUN_SMS_TPL_PAYMENT 支付验证 * - ALIYUN_SMS_ENDPOINT (默认 dysmsapi.aliyuncs.com) */ @Injectable() @@ -20,6 +23,17 @@ export class AliyunSmsProvider implements ISmsProvider { private readonly logger = new Logger('AliyunSmsProvider'); private client: any; // Dysmsapi20170525 client (lazy init) + /** 类型 → 环境变量名 映射 */ + private static readonly TEMPLATE_ENV_MAP: Record = { + [SmsVerificationType.REGISTER]: 'ALIYUN_SMS_TPL_REGISTER', + [SmsVerificationType.LOGIN]: 'ALIYUN_SMS_TPL_LOGIN', + [SmsVerificationType.RESET_PASSWORD]: 'ALIYUN_SMS_TPL_RESET_PASSWORD', + [SmsVerificationType.CHANGE_PHONE]: 'ALIYUN_SMS_TPL_CHANGE_PHONE', + [SmsVerificationType.IDENTITY_VERIFY]: 'ALIYUN_SMS_TPL_IDENTITY_VERIFY', + [SmsVerificationType.TRANSACTION_CONFIRM]: 'ALIYUN_SMS_TPL_TRANSACTION', + [SmsVerificationType.PAYMENT_VERIFY]: 'ALIYUN_SMS_TPL_PAYMENT', + }; + async send( phone: string, code: string, @@ -35,6 +49,11 @@ export class AliyunSmsProvider implements ISmsProvider { const signName = process.env.ALIYUN_SMS_SIGN_NAME || '券金融'; const templateCode = this.getTemplateCode(type); + if (!templateCode) { + this.logger.error(`No SMS template configured for type: ${type}`); + return { success: false, errorMsg: `No template for type: ${type}` }; + } + // eslint-disable-next-line @typescript-eslint/no-var-requires const Dysmsapi = require('@alicloud/dysmsapi20170525'); // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -56,7 +75,7 @@ export class AliyunSmsProvider implements ISmsProvider { if (response.body?.code === 'OK') { this.logger.log( - `SMS sent: phone=${phone.slice(0, 3)}****${phone.slice(-4)} bizId=${response.body.bizId}`, + `SMS sent [${type}]: phone=${phone.slice(0, 3)}****${phone.slice(-4)} tpl=${templateCode} bizId=${response.body.bizId}`, ); return { success: true, @@ -78,7 +97,9 @@ export class AliyunSmsProvider implements ISmsProvider { } private getTemplateCode(type: SmsVerificationType): string { - return process.env.ALIYUN_SMS_TEMPLATE_CODE || ''; + const envKey = AliyunSmsProvider.TEMPLATE_ENV_MAP[type]; + const specific = envKey ? process.env[envKey] : undefined; + return specific || process.env.ALIYUN_SMS_TEMPLATE_CODE || ''; } private async getClient() { diff --git a/scripts/aliyun_sms_manager.py b/scripts/aliyun_sms_manager.py index 1638a21..bee841c 100644 --- a/scripts/aliyun_sms_manager.py +++ b/scripts/aliyun_sms_manager.py @@ -107,17 +107,23 @@ def list_templates(args): def create_template(args): client = get_client() - from alibabacloud_dysmsapi20170525.models import AddSmsTemplateRequest - req = AddSmsTemplateRequest( + # Use new CreateSmsTemplate API (AddSmsTemplate is deprecated) + from alibabacloud_dysmsapi20170525.models import CreateSmsTemplateRequest + sign_name = getattr(args, 'sign', None) or os.environ.get('ALIYUN_SMS_SIGN_NAME', '') + rule = getattr(args, 'rule', None) or '' + req = CreateSmsTemplateRequest( template_type=args.type, template_name=args.name, template_content=args.content, remark=args.remark, + related_sign_name=sign_name, + template_rule=rule if rule else None, ) - resp = client.add_sms_template(req) + resp = client.create_sms_template(req) body = resp.body if body.code == 'OK': - print(f'✅ 模板 "{args.name}" 提交成功 (Code: {body.template_code}),等待审核') + code = getattr(body, 'template_code', None) or getattr(body, 'order_id', 'N/A') + print(f'✅ 模板 "{args.name}" 提交成功 (Code/Order: {code}),等待审核') else: print(f'ERROR: {body.code} - {body.message}') @@ -246,6 +252,8 @@ def main(): p.add_argument('--content', required=True, help='模板内容,如: 您的验证码为${code}') p.add_argument('--type', type=int, default=0, help='类型: 0=验证码 1=通知 2=推广') p.add_argument('--remark', default='', help='备注说明') + p.add_argument('--sign', default='', help='关联签名名称(新API必填,默认读ALIYUN_SMS_SIGN_NAME)') + p.add_argument('--rule', default='', help='变量规则JSON,如: {"code":"numberCaptcha"}') p = sub.add_parser('delete-template', help='删除短信模板') p.add_argument('--code', required=True, help='模板Code')