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')