2026-03-10 16:11:22 +08:00
|
|
|
|
using AlibabaCloud.OpenApiClient.Models;
|
|
|
|
|
|
using AlibabaCloud.SDK.Dysmsapi20170525;
|
|
|
|
|
|
using AlibabaCloud.SDK.Dysmsapi20170525.Models;
|
|
|
|
|
|
using Furion.DependencyInjection;
|
|
|
|
|
|
using Furion.FriendlyException;
|
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
|
using SqlSugar;
|
|
|
|
|
|
using Tea;
|
|
|
|
|
|
using YD_Event.Core.Entities;
|
|
|
|
|
|
using YD_Event.Core.Options;
|
|
|
|
|
|
|
|
|
|
|
|
namespace YD_Event.Application.Sms;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 阿里云短信服务实现
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class AliyunSmsService : ITransient
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly SmsOptions _options;
|
|
|
|
|
|
private readonly ILogger<AliyunSmsService> _logger;
|
|
|
|
|
|
private readonly ISqlSugarRepository<SysSmsLog> _smsLogRepository;
|
|
|
|
|
|
private AlibabaCloud.SDK.Dysmsapi20170525.Client _client;
|
|
|
|
|
|
|
|
|
|
|
|
public AliyunSmsService(IOptions<SmsOptions> options,
|
|
|
|
|
|
ILogger<AliyunSmsService> logger,
|
|
|
|
|
|
ISqlSugarRepository<SysSmsLog> smsLogRepository)
|
|
|
|
|
|
{
|
|
|
|
|
|
_options = options.Value;
|
|
|
|
|
|
_logger = logger;
|
|
|
|
|
|
_smsLogRepository = smsLogRepository;
|
|
|
|
|
|
InitClient();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 初始化阿里云短信客户端
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void InitClient()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(_options.AccessKeyId) || string.IsNullOrWhiteSpace(_options.AccessKeySecret))
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning("阿里云短信配置缺失:AccessKeyId或AccessKeySecret为空");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var config = new Config
|
|
|
|
|
|
{
|
|
|
|
|
|
AccessKeyId = _options.AccessKeyId,
|
|
|
|
|
|
AccessKeySecret = _options.AccessKeySecret,
|
|
|
|
|
|
Endpoint = _options.Endpoint
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
_client = new AlibabaCloud.SDK.Dysmsapi20170525.Client(config);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogError(ex, "阿里云短信客户端初始化失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送验证码短信
|
|
|
|
|
|
/// </summary>
|
2026-03-14 11:25:39 +08:00
|
|
|
|
public async Task SendVerificationCode(string phone, string code)
|
2026-03-10 16:11:22 +08:00
|
|
|
|
{
|
|
|
|
|
|
// 校验配置
|
|
|
|
|
|
ValidateConfig();
|
|
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(_options.VerificationCodeTemplateCode))
|
|
|
|
|
|
throw Oops.Bah("短信验证码模板未配置");
|
|
|
|
|
|
|
|
|
|
|
|
var param = JsonConvert.SerializeObject(new { code = code });
|
2026-03-14 11:25:39 +08:00
|
|
|
|
await SendSms(phone, _options.VerificationCodeTemplateCode, param);
|
2026-03-10 16:11:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-24 00:07:15 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送检录通知短信;未配置检录模板时返回 false 由调用方降级处理。
|
2026-10-08 16:13:04 +08:00
|
|
|
|
/// 模板示例:您报名的${event}正在检录,当前检录至${current},您所在第${order}场次,请提前前往检录区做好准备。单位报名多个场次时 order 可为“1、3、4、5”。
|
2026-09-21 15:07:54 +08:00
|
|
|
|
/// current 参数格式:{项目名称}第{x}场次。
|
2026-08-24 00:07:15 +08:00
|
|
|
|
/// </summary>
|
2026-10-08 16:13:04 +08:00
|
|
|
|
public async Task<bool> SendCheckinNotice(string phone, string eventName, string currentSession, string sessionOrders)
|
2026-08-24 00:07:15 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(_options.CheckinNoticeTemplateCode))
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogInformation("检录通知模板未配置,跳过短信发送:{Phone}", phone);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-17 18:14:50 +08:00
|
|
|
|
var param = JsonConvert.SerializeObject(new
|
|
|
|
|
|
{
|
|
|
|
|
|
@event = eventName,
|
|
|
|
|
|
current = currentSession,
|
2026-10-08 16:13:04 +08:00
|
|
|
|
order = sessionOrders
|
2026-09-17 18:14:50 +08:00
|
|
|
|
});
|
2026-08-24 00:07:15 +08:00
|
|
|
|
await SendSms(phone, _options.CheckinNoticeTemplateCode, param);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 16:11:22 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送短信核心方法
|
|
|
|
|
|
/// </summary>
|
2026-03-14 11:25:39 +08:00
|
|
|
|
public async Task SendSms(string phone, string templateCode, string templateParam)
|
2026-03-10 16:11:22 +08:00
|
|
|
|
{
|
|
|
|
|
|
// 1. 参数校验
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(phone)) throw Oops.Bah("手机号不能为空");
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(templateCode)) throw Oops.Bah("短信模板不能为空");
|
|
|
|
|
|
|
|
|
|
|
|
ValidateConfig();
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 构造请求
|
|
|
|
|
|
var request = new SendSmsRequest
|
|
|
|
|
|
{
|
|
|
|
|
|
PhoneNumbers = phone,
|
|
|
|
|
|
SignName = _options.SignName,
|
|
|
|
|
|
TemplateCode = templateCode,
|
|
|
|
|
|
TemplateParam = templateParam
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-20 00:55:24 +08:00
|
|
|
|
// 3. 准备运行时配置 (包括重试机制)
|
|
|
|
|
|
var runtime = new AlibabaCloud.TeaUtil.Models.RuntimeOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
Autoretry = true,
|
|
|
|
|
|
MaxAttempts = 3 // 异常重试最多3次
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 准备日志实体
|
2026-03-10 16:11:22 +08:00
|
|
|
|
var log = new SysSmsLog
|
|
|
|
|
|
{
|
|
|
|
|
|
Phone = phone,
|
|
|
|
|
|
TemplateCode = templateCode,
|
|
|
|
|
|
TemplateParam = templateParam,
|
|
|
|
|
|
CreatedTime = DateTime.Now
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-03-20 00:55:24 +08:00
|
|
|
|
// 5. 发送请求
|
|
|
|
|
|
var response = await _client.SendSmsWithOptionsAsync(request, runtime);
|
2026-03-10 16:11:22 +08:00
|
|
|
|
|
2026-03-20 00:55:24 +08:00
|
|
|
|
// 6. 处理响应
|
2026-03-10 16:11:22 +08:00
|
|
|
|
log.ResponseCode = response.Body.Code;
|
|
|
|
|
|
log.ResponseMessage = response.Body.Message;
|
|
|
|
|
|
log.RequestId = response.Body.RequestId;
|
|
|
|
|
|
log.Success = response.Body.Code == "OK";
|
|
|
|
|
|
|
|
|
|
|
|
if (!log.Success)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning($"短信发送失败:{phone}, Code: {response.Body.Code}, Message: {response.Body.Message}");
|
|
|
|
|
|
throw Oops.Bah($"短信发送失败:{response.Body.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (TeaException error)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 阿里云SDK异常
|
|
|
|
|
|
log.Success = false;
|
|
|
|
|
|
log.Exception = $"Code: {error.Code}, Message: {error.Message}, Data: {JsonConvert.SerializeObject(error.Data)}";
|
2026-03-20 00:55:24 +08:00
|
|
|
|
_logger.LogError(error, "阿里云短信发送异常: {Message}, Recommend: {Recommend}", error.Message, error.Data != null && error.Data.Contains("Recommend") ? error.Data["Recommend"] : "");
|
2026-03-10 16:11:22 +08:00
|
|
|
|
throw Oops.Bah("短信服务异常,请稍后重试");
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 其他异常(网络等)
|
|
|
|
|
|
log.Success = false;
|
|
|
|
|
|
log.Exception = ex.Message;
|
|
|
|
|
|
_logger.LogError(ex, "短信发送未知异常");
|
|
|
|
|
|
throw Oops.Bah("短信发送失败,请检查网络或联系管理员");
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
2026-03-20 00:55:24 +08:00
|
|
|
|
// 7. 日志持久化
|
|
|
|
|
|
if (_smsLogRepository != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _smsLogRepository.InsertAsync(log);
|
|
|
|
|
|
}
|
2026-03-10 16:11:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 校验配置是否有效
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void ValidateConfig()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_client == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
InitClient();
|
|
|
|
|
|
if (_client == null)
|
|
|
|
|
|
throw Oops.Bah("短信服务未初始化,请检查AccessKeyId和AccessKeySecret配置");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(_options.SignName))
|
|
|
|
|
|
throw Oops.Bah("短信签名(SignName)未配置");
|
|
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(_options.VerificationCodeTemplateCode))
|
|
|
|
|
|
throw Oops.Bah("验证码模板(VerificationCodeTemplateCode)未配置");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|