feat(auth): SMS 模板按类型分发 + 阿里云 8 模板配置
重构 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 <noreply@anthropic.com>
This commit is contained in:
parent
bd6ecaa0fd
commit
41b8a8fcfb
|
|
@ -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 ---
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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, string> = {
|
||||
[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() {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Reference in New Issue