feat: 新增签到、宠物、期刊绑定、补偿任务等业务模块,优化微信登录流程
本次提交完成了多个核心业务模块的开发与优化: 1. 宠物模块:新增宠物实体、服务接口与实现,支持创建默认宠物、激活、喂养、进化以及喂养记录查询 2. 签到模块:新增签到实体、服务接口、控制器以及相关DTO,支持用户签到和签到信息查询,新增成长值奖励字段 3. 期刊绑定模块:新增用户期刊关联实体、服务接口与控制器,支持扫码绑定期刊、解绑和查询绑定列表 4. 补偿任务模块:新增补偿任务实体、服务接口与实现,用于处理业务失败后的异步重试补偿 5. 优化微信登录流程:拆分登录与快捷登录接口,支持手机号获取,新增首次登录自动创建默认宠物逻辑 6. 调整基础路由与实体状态:修改微信控制器路由前缀,更新宠物状态枚举与默认值
This commit is contained in:
@ -15,27 +15,26 @@ namespace QYZH.InteractiveMagazine.Service;
|
||||
/// <summary>
|
||||
/// 微信小程序认证服务实现
|
||||
/// </summary>
|
||||
public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfiguration configuration, ILogger<WeChatAuthService> logger) : BaseRepository<Users>, IWeChatAuthService
|
||||
public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfiguration configuration, ILogger<WeChatAuthService> logger, IPetService petService) : BaseRepository<Users>, IWeChatAuthService
|
||||
{
|
||||
private const string TokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token";
|
||||
private const string AccessTokenCacheKey = "InteractiveMagazine:WeChat:AccessToken";
|
||||
private const string Code2SessionUrl = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
|
||||
private const string GetAccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
|
||||
private const string GetPhoneNumberUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={0}";
|
||||
|
||||
/// <summary>
|
||||
/// 微信小程序一键登录
|
||||
/// 微信小程序登录(首次创建用户,非首次直接登录)
|
||||
/// </summary>
|
||||
/// <param name="input">登录输入(含微信 code)</param>
|
||||
/// <returns>登录结果(含 Token 和该 OpenId 下的用户列表)</returns>
|
||||
public async Task<WeChatLoginOutput> LoginAsync(WeChatLoginInput input)
|
||||
{
|
||||
logger.LogInformation("微信小程序登录尝试");
|
||||
logger.LogInformation("微信小程序登录");
|
||||
|
||||
// 参数校验
|
||||
if (string.IsNullOrWhiteSpace(input.Code))
|
||||
{
|
||||
throw new BusinessException("微信登录凭证 code 不能为空", 400);
|
||||
}
|
||||
|
||||
// 获取微信配置
|
||||
var weChatSettings = GetWeChatSettings();
|
||||
|
||||
// 调用微信 code2session 接口
|
||||
@ -56,16 +55,26 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
|
||||
|
||||
if (users.Count == 0)
|
||||
{
|
||||
// 首次登录,创建新用户
|
||||
// 首次登录,获取手机号(如果传入了 PhoneCode)
|
||||
string? phone = null;
|
||||
if (!string.IsNullOrWhiteSpace(input.PhoneCode))
|
||||
{
|
||||
phone = await GetPhoneNumberAsync(weChatSettings, input.PhoneCode);
|
||||
logger.LogInformation("获取手机号成功,OpenId: {OpenId}, Phone: {Phone}", wxResponse.OpenId, phone);
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
var newUser = new Users
|
||||
{
|
||||
Name = $"wx_{wxResponse.OpenId[^8..]}",
|
||||
OpenId = wxResponse.OpenId,
|
||||
UnionId = wxResponse.UnionId,
|
||||
Phone = phone,
|
||||
Type = "Normal",
|
||||
Status = "Active",
|
||||
GrowthPoints = 0,
|
||||
Points = 0
|
||||
Points = 0,
|
||||
IsLastOnline = true
|
||||
};
|
||||
|
||||
var insertResult = await usersRepository.Insertable(newUser).ExecuteReturnIdentityAsync();
|
||||
@ -78,29 +87,73 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
|
||||
newUser.Id = insertResult;
|
||||
users.Add(newUser);
|
||||
logger.LogInformation("微信新用户创建成功,UserId: {UserId}, OpenId: {OpenId}", newUser.Id, wxResponse.OpenId);
|
||||
|
||||
// 为新用户创建默认宠物(未激活状态)
|
||||
try
|
||||
{
|
||||
await petService.CreateDefaultPetAsync(newUser.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "新用户创建默认宠物失败,UserId: {UserId}", newUser.Id);
|
||||
// 宠物创建失败不阻断注册流程
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 非首次登录,如果传入了 PhoneCode 则更新该 OpenId 下所有用户的手机号
|
||||
if (!string.IsNullOrWhiteSpace(input.PhoneCode))
|
||||
{
|
||||
var phone = await GetPhoneNumberAsync(weChatSettings, input.PhoneCode);
|
||||
if (!string.IsNullOrWhiteSpace(phone))
|
||||
{
|
||||
await usersRepository.Context.Updateable<Users>()
|
||||
.SetColumns(u => u.Phone == phone)
|
||||
.Where(u => u.OpenId == wxResponse.OpenId && !u.IsDeleted)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
foreach (var u in users)
|
||||
{
|
||||
u.Phone = phone;
|
||||
}
|
||||
|
||||
logger.LogInformation("更新 OpenId: {OpenId} 下所有用户手机号成功,Phone: {Phone}", wxResponse.OpenId, phone);
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("微信登录成功,OpenId: {OpenId} 下存在 {Count} 个用户", wxResponse.OpenId, users.Count);
|
||||
}
|
||||
|
||||
// 使用第一个用户生成 JWT Token
|
||||
var primaryUser = users.First();
|
||||
var jwtSettings = GetJwtSettings();
|
||||
var token = JwtHelper.GenerateToken((long)primaryUser.Id, primaryUser.Name, jwtSettings);
|
||||
// 构建登录输出
|
||||
return await BuildLoginOutputAsync(wxResponse.OpenId, users);
|
||||
}
|
||||
|
||||
// 缓存 Token 到 Redis
|
||||
await RedisHelper.StringSetAsync($"{TokenKeyPrefix}:{primaryUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
|
||||
/// <summary>
|
||||
/// 微信小程序快捷登录(通过 OpenId 直接登录,用户需已存在)
|
||||
/// </summary>
|
||||
public async Task<WeChatLoginOutput> QuickLoginAsync(WeChatQuickLoginInput input)
|
||||
{
|
||||
logger.LogInformation("微信快捷登录,OpenId: {OpenId}", input.OpenId);
|
||||
|
||||
// 映射用户列表
|
||||
var userOutputs = users.Select(MapUserToOutput).ToList();
|
||||
|
||||
return new WeChatLoginOutput
|
||||
if (string.IsNullOrWhiteSpace(input.OpenId))
|
||||
{
|
||||
Token = token,
|
||||
OpenId = wxResponse.OpenId,
|
||||
Users = userOutputs
|
||||
};
|
||||
throw new BusinessException("OpenId 不能为空", 400);
|
||||
}
|
||||
|
||||
// 查询该 OpenId 下的所有用户
|
||||
var users = await usersRepository.Context.Queryable<Users>()
|
||||
.Where(u => u.OpenId == input.OpenId && !u.IsDeleted)
|
||||
.ToListAsync();
|
||||
|
||||
if (users.Count == 0)
|
||||
{
|
||||
logger.LogWarning("快捷登录失败,OpenId: {OpenId} 下无用户", input.OpenId);
|
||||
throw new BusinessException("未找到该微信账号关联的用户,请先完成注册", 404);
|
||||
}
|
||||
|
||||
logger.LogInformation("快捷登录成功,OpenId: {OpenId} 下存在 {Count} 个用户", input.OpenId, users.Count);
|
||||
|
||||
return await BuildLoginOutputAsync(input.OpenId, users);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -193,6 +246,90 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建登录输出(生成 Token + 映射用户列表)
|
||||
/// </summary>
|
||||
private async Task<WeChatLoginOutput> BuildLoginOutputAsync(string openId, List<Users> users)
|
||||
{
|
||||
// 优先使用 IsLastOnline 的用户,否则取第一个
|
||||
var primaryUser = users.FirstOrDefault(u => u.IsLastOnline) ?? users.First();
|
||||
|
||||
var jwtSettings = GetJwtSettings();
|
||||
var token = JwtHelper.GenerateToken((long)primaryUser.Id, primaryUser.Name, jwtSettings);
|
||||
|
||||
// 缓存 Token 到 Redis
|
||||
await RedisHelper.StringSetAsync($"{TokenKeyPrefix}:{primaryUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
|
||||
|
||||
var userOutputs = users.Select(MapUserToOutput).ToList();
|
||||
|
||||
return new WeChatLoginOutput
|
||||
{
|
||||
Token = token,
|
||||
OpenId = openId,
|
||||
Users = userOutputs
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取微信 access_token(带 Redis 缓存)
|
||||
/// </summary>
|
||||
private async Task<string> GetAccessTokenAsync(WeChatSettings settings)
|
||||
{
|
||||
// 先从 Redis 缓存获取
|
||||
var cachedToken = await RedisHelper.StringGetAsync(AccessTokenCacheKey);
|
||||
if (!string.IsNullOrWhiteSpace(cachedToken))
|
||||
{
|
||||
return cachedToken;
|
||||
}
|
||||
|
||||
// 缓存未命中,调用微信接口获取
|
||||
var url = string.Format(GetAccessTokenUrl, settings.AppId, settings.AppSecret);
|
||||
var response = await HttpHelper.GetAsync<WxAccessTokenResponse>(url);
|
||||
|
||||
if (response == null || response.ErrCode != 0 || string.IsNullOrWhiteSpace(response.AccessToken))
|
||||
{
|
||||
var errMsg = response?.ErrMsg ?? "未知错误";
|
||||
logger.LogError("获取微信 access_token 失败,errcode: {ErrCode}, errmsg: {ErrMsg}", response?.ErrCode, errMsg);
|
||||
throw new BusinessException("微信服务请求失败,请稍后重试", 500);
|
||||
}
|
||||
|
||||
// 缓存 access_token,提前 5 分钟过期(微信默认 7200 秒)
|
||||
var expiresIn = response.ExpiresIn > 300 ? response.ExpiresIn - 300 : response.ExpiresIn;
|
||||
await RedisHelper.StringSetAsync(AccessTokenCacheKey, response.AccessToken, TimeSpan.FromSeconds(expiresIn));
|
||||
|
||||
logger.LogInformation("获取微信 access_token 成功,有效期: {ExpiresIn} 秒", expiresIn);
|
||||
return response.AccessToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 phone_code 获取微信用户手机号
|
||||
/// </summary>
|
||||
private async Task<string?> GetPhoneNumberAsync(WeChatSettings settings, string phoneCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var accessToken = await GetAccessTokenAsync(settings);
|
||||
var url = string.Format(GetPhoneNumberUrl, accessToken);
|
||||
var response = await HttpHelper.PostAsync<WxPhoneNumberResponse>(url, new { code = phoneCode });
|
||||
|
||||
if (response == null || response.ErrCode != 0 || response.PhoneInfo == null)
|
||||
{
|
||||
var errMsg = response?.ErrMsg ?? "未知错误";
|
||||
logger.LogWarning("获取手机号失败,errcode: {ErrCode}, errmsg: {ErrMsg}", response?.ErrCode, errMsg);
|
||||
// 获取手机号失败不阻断登录流程,仅记录日志
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.PhoneInfo.PurePhoneNumber ?? response.PhoneInfo.PhoneNumber;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "调用微信获取手机号接口异常");
|
||||
// 获取手机号失败不阻断登录流程
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户实体映射为输出 DTO
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user