gcx/scripts/aliyun_sms_manager.py

310 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
阿里云 SMS 短信管理工具
功能: 签名管理、模板管理、发送记录查询、发送测试
依赖: pip install alibabacloud-dysmsapi20170525
"""
import argparse
import json
import sys
import os
from datetime import datetime, timedelta
def get_client():
from alibabacloud_dysmsapi20170525.client import Client
from alibabacloud_tea_openapi.models import Config
ak_id = os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_ID', '')
ak_secret = os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_SECRET', '')
if not ak_id or not ak_secret:
print('ERROR: Set ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET')
sys.exit(1)
config = Config(
access_key_id=ak_id,
access_key_secret=ak_secret,
endpoint='dysmsapi.aliyuncs.com',
)
return Client(config)
# ──────────────── 签名管理 ────────────────
def list_signs(args):
client = get_client()
from alibabacloud_dysmsapi20170525.models import QuerySmsSignListRequest
req = QuerySmsSignListRequest(page_index=args.page, page_size=args.size)
resp = client.query_sms_sign_list(req)
body = resp.body
if body.code != 'OK':
print(f'ERROR: {body.code} - {body.message}')
return
signs = body.sms_sign_list or []
print(f'\n📋 SMS 签名列表 (共 {len(signs)} 个)\n')
print(f'{"签名名称":<20} {"审核状态":<14} {"创建日期":<22} {"业务类型"}')
print('-' * 80)
status_map = {
'AUDIT_STATE_INIT': '审核中',
'AUDIT_STATE_PASS': '✅ 已通过',
'AUDIT_STATE_NOT_PASS': '❌ 被驳回',
'AUDIT_STATE_CANCEL': '已取消',
}
for s in signs:
st = status_map.get(s.audit_status, str(s.audit_status))
biz = s.business_type or ''
print(f'{s.sign_name:<20} {st:<14} {s.create_date:<22} {biz}')
def create_sign(args):
client = get_client()
from alibabacloud_dysmsapi20170525.models import AddSmsSignRequest
req = AddSmsSignRequest(
sign_name=args.name,
sign_source=args.source,
remark=args.remark,
)
resp = client.add_sms_sign(req)
body = resp.body
if body.code == 'OK':
print(f'✅ 签名 "{args.name}" 提交成功,等待审核')
else:
print(f'ERROR: {body.code} - {body.message}')
def delete_sign(args):
client = get_client()
from alibabacloud_dysmsapi20170525.models import DeleteSmsSignRequest
req = DeleteSmsSignRequest(sign_name=args.name)
resp = client.delete_sms_sign(req)
body = resp.body
if body.code == 'OK':
print(f'✅ 签名 "{args.name}" 已删除')
else:
print(f'ERROR: {body.code} - {body.message}')
# ──────────────── 模板管理 ────────────────
def list_templates(args):
client = get_client()
from alibabacloud_dysmsapi20170525.models import QuerySmsTemplateListRequest
req = QuerySmsTemplateListRequest(page_index=args.page, page_size=args.size)
resp = client.query_sms_template_list(req)
body = resp.body
if body.code != 'OK':
print(f'ERROR: {body.code} - {body.message}')
return
templates = body.sms_template_list or []
print(f'\n📋 SMS 模板列表 (共 {len(templates)} 个)\n')
print(f'{"模板Code":<20} {"模板名称":<20} {"类型":<8} {"审核状态":<14} {"内容"}')
print('-' * 100)
status_map = {
'AUDIT_STATE_INIT': '审核中',
'AUDIT_STATE_PASS': '✅ 通过',
'AUDIT_STATE_NOT_PASS': '❌ 驳回',
'AUDIT_STATE_CANCEL': '已取消',
}
type_map = {0: '验证码', 1: '通知', 2: '推广', 3: '国际', 6: '数字短信', 7: '其他'}
for t in templates:
st = status_map.get(t.audit_status, str(t.audit_status))
tp = type_map.get(t.template_type, str(t.template_type))
content = (t.template_content or '')[:40]
print(f'{t.template_code:<20} {t.template_name:<20} {tp:<8} {st:<14} {content}')
def create_template(args):
client = get_client()
# 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.create_sms_template(req)
body = resp.body
if body.code == 'OK':
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}')
def delete_template(args):
client = get_client()
from alibabacloud_dysmsapi20170525.models import DeleteSmsTemplateRequest
req = DeleteSmsTemplateRequest(template_code=args.code)
resp = client.delete_sms_template(req)
body = resp.body
if body.code == 'OK':
print(f'✅ 模板 {args.code} 已删除')
else:
print(f'ERROR: {body.code} - {body.message}')
# ──────────────── 发送记录查询 ────────────────
def query_send_details(args):
client = get_client()
from alibabacloud_dysmsapi20170525.models import QuerySendDetailsRequest
phone = args.phone.lstrip('+86')
send_date = args.date or datetime.now().strftime('%Y%m%d')
req = QuerySendDetailsRequest(
phone_number=phone,
send_date=send_date,
page_size=args.size,
current_page=args.page,
)
resp = client.query_send_details(req)
body = resp.body
if body.code != 'OK':
print(f'ERROR: {body.code} - {body.message}')
return
details = body.sms_send_detail_dtos.sms_send_detail_dto if body.sms_send_detail_dtos else []
print(f'\n📋 发送记录 (手机: {args.phone}, 日期: {send_date}, 共 {body.total_count} 条)\n')
print(f'{"发送时间":<22} {"状态":<8} {"模板Code":<18} {"内容"}')
print('-' * 90)
status_map = {1: '等待', 2: '失败', 3: '✅ 成功'}
for d in details:
st = status_map.get(d.send_status, str(d.send_status))
content = (d.content or '')[:40]
print(f'{d.send_date:<22} {st:<8} {d.template_code:<18} {content}')
# ──────────────── 发送统计 ────────────────
def query_send_statistics(args):
client = get_client()
from alibabacloud_dysmsapi20170525.models import QuerySendStatisticsRequest
today = datetime.now()
start = args.start or (today - timedelta(days=args.days)).strftime('%Y%m%d')
end = args.end or today.strftime('%Y%m%d')
req = QuerySendStatisticsRequest(
is_globe=1,
start_date=start,
end_date=end,
page_index=1,
page_size=50,
)
resp = client.query_send_statistics(req)
body = resp.body
if body.code != 'OK':
print(f'ERROR: {body.code} - {body.message}')
return
data = body.data
items = data.target_list or [] if data else []
total_sent = sum(i.total_count for i in items)
total_success = sum(i.responded_success_count for i in items)
total_fail = sum(i.responded_fail_count for i in items)
print(f'\n📋 SMS 发送统计 ({start} ~ {end})\n')
print(f'{"日期":<12} {"发送":<8} {"成功":<8} {"失败":<8}')
print('-' * 40)
for i in items:
print(f'{i.send_date:<12} {i.total_count:<8} {i.responded_success_count:<8} {i.responded_fail_count:<8}')
print('-' * 40)
print(f'{"合计":<12} {total_sent:<8} {total_success:<8} {total_fail:<8}')
print(f'\n 注: SMS 为按量付费,无资源包余额概念')
# ──────────────── 发送测试 ────────────────
def send_sms(args):
client = get_client()
from alibabacloud_dysmsapi20170525.models import SendSmsRequest
from alibabacloud_tea_util.models import RuntimeOptions
phone = args.phone
if not phone.startswith('+'):
phone = '+86' + phone.lstrip('0')
req = SendSmsRequest(
phone_numbers=phone,
sign_name=args.sign or os.environ.get('ALIYUN_SMS_SIGN_NAME', ''),
template_code=args.template or os.environ.get('ALIYUN_SMS_TEMPLATE_CODE', ''),
template_param=json.dumps({'code': args.code}) if args.code else None,
)
runtime = RuntimeOptions(connect_timeout=15000, read_timeout=15000)
resp = client.send_sms_with_options(req, runtime)
body = resp.body
if body.code == 'OK':
print(f'✅ 短信发送成功: phone={phone} bizId={body.biz_id}')
else:
print(f'ERROR: {body.code} - {body.message}')
# ──────────────── CLI ────────────────
def main():
parser = argparse.ArgumentParser(description='阿里云 SMS 管理工具')
sub = parser.add_subparsers(dest='command')
# 签名
p = sub.add_parser('list-signs', help='列出所有短信签名')
p.add_argument('--page', type=int, default=1)
p.add_argument('--size', type=int, default=50)
p = sub.add_parser('create-sign', help='创建短信签名')
p.add_argument('--name', required=True, help='签名名称')
p.add_argument('--source', type=int, default=0, help='来源: 0=企事业 1=工信部 2=商标 3=APP 4=网站 5=公众号 6=小程序 7=电商')
p.add_argument('--remark', default='', help='备注说明')
p = sub.add_parser('delete-sign', help='删除短信签名')
p.add_argument('--name', required=True, help='签名名称')
# 模板
p = sub.add_parser('list-templates', help='列出所有短信模板')
p.add_argument('--page', type=int, default=1)
p.add_argument('--size', type=int, default=50)
p = sub.add_parser('create-template', help='创建短信模板')
p.add_argument('--name', required=True, help='模板名称')
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')
# 发送记录
p = sub.add_parser('query', help='查询发送记录')
p.add_argument('--phone', required=True, help='手机号')
p.add_argument('--date', help='日期 YYYYMMDD (默认今天)')
p.add_argument('--page', type=int, default=1)
p.add_argument('--size', type=int, default=20)
# 发送统计
p = sub.add_parser('stats', help='查询发送统计')
p.add_argument('--days', type=int, default=30, help='最近N天 (默认30)')
p.add_argument('--start', help='起始日期 YYYYMMDD')
p.add_argument('--end', help='结束日期 YYYYMMDD')
# 发送测试
p = sub.add_parser('send', help='发送短信 (测试)')
p.add_argument('--phone', required=True, help='手机号')
p.add_argument('--sign', help='签名 (默认取环境变量)')
p.add_argument('--template', help='模板Code (默认取环境变量)')
p.add_argument('--code', help='验证码内容')
args = parser.parse_args()
if not args.command:
parser.print_help()
return
cmds = {
'list-signs': list_signs,
'create-sign': create_sign,
'delete-sign': delete_sign,
'list-templates': list_templates,
'create-template': create_template,
'delete-template': delete_template,
'query': query_send_details,
'stats': query_send_statistics,
'send': send_sms,
}
try:
cmds[args.command](args)
except Exception as e:
err = str(e)
if 'NoPermission' in err or '403' in err:
print(f'\n❌ 权限不足: RAM 子账号缺少对应 API 权限')
print(f' 请在阿里云 RAM 控制台添加 AliyunDysmsFullAccess 策略')
print(f' 控制台: https://ram.console.aliyun.com')
else:
print(f'ERROR: {e}')
if __name__ == '__main__':
main()