feat(config): 添加用户默认配置功能(UID、昵称前缀、默认头像)

- UserConfigSetting 模型增加 default_nickname_prefix 和 default_avatar 字段
- Admin ConfigController 新增 user/get 和 user/update 接口
- 后台管理前端新增用户配置 tab 页面
- AuthService 创建用户时从配置读取默认昵称前缀和头像,支持 fallback
This commit is contained in:
zpc 2026-02-20 21:25:22 +08:00
parent d125c24cba
commit d14e96ac97
7 changed files with 612 additions and 148 deletions

View File

@ -1,4 +1,4 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
namespace MiAssessment.Admin.Business.Models.Config;
@ -630,8 +630,21 @@ public class UserConfigSetting
/// </summary>
[JsonPropertyName("uid_length")]
public string? UidLength { get; set; }
/// <summary>
/// 默认昵称前缀,如"用户",注册时自动拼接随机数字
/// </summary>
[JsonPropertyName("default_nickname_prefix")]
public string? DefaultNicknamePrefix { get; set; }
/// <summary>
/// 默认头像URL为空时使用系统生成的头像
/// </summary>
[JsonPropertyName("default_avatar")]
public string? DefaultAvatar { get; set; }
}
#endregion
#region

View File

@ -29,6 +29,7 @@ public class ConfigController : ControllerBase
public const string Upload = "uploads";
public const string Miniprogram = "miniprogram_setting";
public const string WeixinPay = "weixinpay_setting";
public const string UserConfig = "user_config";
}
public ConfigController(IAdminConfigService configService, ILogger<ConfigController> logger)
@ -284,4 +285,54 @@ public class ConfigController : ControllerBase
}
#endregion
#region
/// <summary>
/// 获取用户配置
/// </summary>
/// <returns>用户配置</returns>
[HttpGet("user/get")]
public async Task<ApiResponse<UserConfigSetting>> GetUserConfig()
{
try
{
var config = await _configService.GetConfigAsync<UserConfigSetting>(ConfigKeys.UserConfig);
return ApiResponse<UserConfigSetting>.Success(config ?? new UserConfigSetting
{
UidType = "2",
UidLength = "6",
DefaultNicknamePrefix = "用户",
DefaultAvatar = ""
});
}
catch (Exception ex)
{
_logger.LogError(ex, "获取用户配置失败");
return ApiResponse<UserConfigSetting>.Error(AdminErrorCodes.InternalError, "获取用户配置失败");
}
}
/// <summary>
/// 更新用户配置
/// </summary>
/// <param name="request">用户配置</param>
/// <returns>更新结果</returns>
[HttpPost("user/update")]
[OperationLog("配置管理", "更新用户配置")]
public async Task<ApiResponse<bool>> UpdateUserConfig([FromBody] UserConfigSetting request)
{
try
{
var result = await _configService.UpdateConfigAsync(ConfigKeys.UserConfig, request);
return ApiResponse<bool>.Success(result, "用户配置更新成功");
}
catch (Exception ex)
{
_logger.LogError(ex, "更新用户配置失败");
return ApiResponse<bool>.Error(AdminErrorCodes.InternalError, "更新用户配置失败");
}
}
#endregion
}

View File

@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
namespace MiAssessment.Admin.Models.Config;
/// <summary>
/// 用户配置
/// </summary>
public class UserConfigSetting
{
/// <summary>
/// UID类型 1真实ID 2数字ID 3随机字符和数字
/// </summary>
[JsonPropertyName("uid_type")]
public string? UidType { get; set; }
/// <summary>
/// UID长度
/// </summary>
[JsonPropertyName("uid_length")]
public string? UidLength { get; set; }
/// <summary>
/// 默认昵称前缀,如"用户",注册时自动拼接随机数字
/// </summary>
[JsonPropertyName("default_nickname_prefix")]
public string? DefaultNicknamePrefix { get; set; }
/// <summary>
/// 默认头像URL为空时使用系统生成的头像
/// </summary>
[JsonPropertyName("default_avatar")]
public string? DefaultAvatar { get; set; }
}

View File

@ -165,3 +165,40 @@ export function updateWeixinPayConfig(data: WeixinPaySetting): Promise<ApiRespon
data
})
}
// ==================== 用户配置 ====================
/**
*
*/
export interface UserConfigSetting {
/** UID类型 1真实ID 2数字ID 3随机字符和数字 */
uid_type?: string
/** UID长度 */
uid_length?: string
/** 默认昵称前缀 */
default_nickname_prefix?: string
/** 默认头像URL */
default_avatar?: string
}
/**
*
*/
export function getUserConfig(): Promise<ApiResponse<UserConfigSetting>> {
return request<UserConfigSetting>({
url: '/admin/config/user/get',
method: 'get'
})
}
/**
*
*/
export function updateUserConfig(data: UserConfigSetting): Promise<ApiResponse<boolean>> {
return request<boolean>({
url: '/admin/config/user/update',
method: 'post',
data
})
}

View File

@ -10,6 +10,9 @@
<el-tab-pane label="上传配置" name="upload">
<UploadConfig />
</el-tab-pane>
<el-tab-pane label="用户配置" name="user">
<UserConfig />
</el-tab-pane>
</el-tabs>
</div>
</template>
@ -22,6 +25,7 @@ import { ref } from 'vue'
import UploadConfig from './upload.vue'
import MiniprogramConfig from './miniprogram.vue'
import PaymentConfig from './payment.vue'
import UserConfig from './user.vue'
// Tab
const activeTab = ref('miniprogram')

View File

@ -0,0 +1,270 @@
<template>
<div class="user-config-container">
<!-- 页面标题 -->
<el-card class="page-header">
<div class="header-content">
<h2 class="page-title">用户配置</h2>
<span class="page-description">配置新用户注册时的UID生成规则默认昵称前缀和默认头像</span>
</div>
</el-card>
<!-- 配置表单 -->
<el-card v-loading="state.loading" class="config-form-card">
<el-form label-width="140px" label-position="right">
<!-- UID 配置 -->
<div class="section-title">UID 配置</div>
<el-form-item label="UID类型">
<el-radio-group v-model="state.formData.uid_type">
<el-radio value="1">真实ID</el-radio>
<el-radio value="2">数字ID</el-radio>
<el-radio value="3">随机字符和数字</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="UID长度">
<el-input-number
v-model.number="uidLengthNum"
:min="4"
:max="12"
controls-position="right"
/>
<div class="form-item-tip">新用户UID的位数建议6位</div>
</el-form-item>
<el-divider />
<!-- 默认昵称配置 -->
<div class="section-title">默认昵称</div>
<el-form-item label="昵称前缀">
<el-input
v-model="state.formData.default_nickname_prefix"
placeholder="请输入默认昵称前缀"
maxlength="10"
show-word-limit
clearable
style="width: 300px"
/>
<div class="form-item-tip">新用户注册时的昵称前缀系统会自动拼接6位随机数字"用户123456"</div>
</el-form-item>
<el-divider />
<!-- 默认头像配置 -->
<div class="section-title">默认头像</div>
<el-form-item label="默认头像">
<div class="avatar-config">
<div class="avatar-preview">
<el-image
v-if="state.formData.default_avatar"
:src="state.formData.default_avatar"
fit="cover"
class="avatar-img"
>
<template #error>
<div class="avatar-placeholder">
<el-icon :size="24"><Picture /></el-icon>
</div>
</template>
</el-image>
<div v-else class="avatar-placeholder">
<el-icon :size="24"><User /></el-icon>
<span class="placeholder-text">系统生成</span>
</div>
</div>
<div class="avatar-input">
<el-input
v-model="state.formData.default_avatar"
placeholder="请输入默认头像URL留空则使用系统自动生成"
clearable
/>
<div class="form-item-tip">填写图片URL作为所有新用户的默认头像留空则系统自动生成唯一头像</div>
</div>
</div>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<div class="form-actions">
<el-button type="primary" :loading="state.saving" @click="handleSave">保存配置</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
/**
* 用户配置页面
* @description 配置UID生成规则默认昵称前缀和默认头像
*/
import { reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Picture, User } from '@element-plus/icons-vue'
import {
getUserConfig,
updateUserConfig,
type UserConfigSetting
} from '@/api/system/config'
// State
const state = reactive({
loading: false,
saving: false,
formData: {
uid_type: '2',
uid_length: '6',
default_nickname_prefix: '用户',
default_avatar: ''
} as UserConfigSetting
})
// UID
const uidLengthNum = computed({
get: () => parseInt(state.formData.uid_length || '6') || 6,
set: (val: number) => { state.formData.uid_length = String(val) }
})
/**
* 加载配置
*/
async function loadConfig() {
state.loading = true
try {
const res = await getUserConfig()
if (res.code === 0 && res.data) {
state.formData = res.data
}
} catch (error) {
console.error('加载用户配置失败:', error)
ElMessage.error('加载配置失败')
} finally {
state.loading = false
}
}
/**
* 保存配置
*/
async function handleSave() {
state.saving = true
try {
const res = await updateUserConfig(state.formData)
if (res.code === 0) {
ElMessage.success('保存成功')
} else {
ElMessage.error(res.message || '保存失败')
}
} catch (error) {
console.error('保存用户配置失败:', error)
ElMessage.error('保存失败')
} finally {
state.saving = false
}
}
/**
* 重置配置
*/
function handleReset() {
loadConfig()
}
// Lifecycle
onMounted(() => {
loadConfig()
})
</script>
<style scoped>
.page-header {
margin-bottom: 20px;
}
.header-content {
display: flex;
align-items: baseline;
gap: 12px;
}
.page-title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.page-description {
color: #909399;
font-size: 14px;
}
.config-form-card {
margin-bottom: 20px;
}
.section-title {
font-size: 15px;
font-weight: 500;
color: #303133;
margin-bottom: 16px;
padding-left: 10px;
border-left: 3px solid #409eff;
}
.form-item-tip {
font-size: 12px;
color: #909399;
line-height: 1.4;
margin-top: 4px;
}
.avatar-config {
display: flex;
align-items: flex-start;
gap: 16px;
}
.avatar-preview {
flex-shrink: 0;
width: 80px;
height: 80px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #ebeef5;
}
.avatar-img {
width: 80px;
height: 80px;
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f5f7fa;
color: #909399;
}
.placeholder-text {
font-size: 10px;
margin-top: 4px;
}
.avatar-input {
flex: 1;
}
.form-actions {
display: flex;
justify-content: center;
gap: 12px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
}
</style>

View File

@ -1,5 +1,6 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using MiAssessment.Core.Interfaces;
using MiAssessment.Model.Data;
using MiAssessment.Model.Entities;
@ -10,7 +11,7 @@ using Microsoft.Extensions.Logging;
namespace MiAssessment.Core.Services;
/// <summary>
/// 认证服务实现
/// <EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD><EFBFBD>
/// </summary>
public class AuthService : IAuthService
{
@ -20,6 +21,7 @@ public class AuthService : IAuthService
private readonly IWechatService _wechatService;
private readonly IIpLocationService _ipLocationService;
private readonly IRedisService _redisService;
private readonly IConfigService _configService;
private readonly JwtSettings _jwtSettings;
private readonly ILogger<AuthService> _logger;
@ -28,7 +30,7 @@ public class AuthService : IAuthService
private const string SmsCodeKeyPrefix = "sms:code:";
private const int DebounceSeconds = 3;
// Refresh Token 配置
// Refresh Token <EFBFBD><EFBFBD><EFBFBD><EFBFBD>
private const int RefreshTokenLength = 64;
public AuthService(
@ -38,6 +40,7 @@ public class AuthService : IAuthService
IWechatService wechatService,
IIpLocationService ipLocationService,
IRedisService redisService,
IConfigService configService,
JwtSettings jwtSettings,
ILogger<AuthService> logger)
{
@ -47,50 +50,51 @@ public class AuthService : IAuthService
_wechatService = wechatService ?? throw new ArgumentNullException(nameof(wechatService));
_ipLocationService = ipLocationService ?? throw new ArgumentNullException(nameof(ipLocationService));
_redisService = redisService ?? throw new ArgumentNullException(nameof(redisService));
_configService = configService ?? throw new ArgumentNullException(nameof(configService));
_jwtSettings = jwtSettings ?? throw new ArgumentNullException(nameof(jwtSettings));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// 微信小程序登录
/// ΢<EFBFBD><EFBFBD>С<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼
/// Requirements: 1.1-1.8
/// </summary>
public async Task<LoginResult> WechatMiniProgramLoginAsync(string code, int? pid, string? clickId)
{
_logger.LogInformation("[AuthService] 微信登录开始,code={Code}, pid={Pid}", code, pid);
_logger.LogInformation("[AuthService] ΢<EFBFBD>ŵ<EFBFBD>¼<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD>code={Code}, pid={Pid}", code, pid);
if (string.IsNullOrWhiteSpace(code))
{
_logger.LogWarning("[AuthService] 微信登录失败code为空");
_logger.LogWarning("[AuthService] ΢<EFBFBD>ŵ<EFBFBD>¼ʧ<EFBFBD>ܣ<EFBFBD>codeΪ<EFBFBD><EFBFBD>");
return new LoginResult
{
Success = false,
ErrorMessage = "授权code不能为空"
ErrorMessage = "<EFBFBD><EFBFBD>Ȩcode<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>"
};
}
try
{
// 1.6 防抖机制 - 3秒内不允许重复登录
// 1.6 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> - 3<><33><EFBFBD>ڲ<EFBFBD><DAB2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD><D8B8><EFBFBD>¼
var debounceKey = $"{LoginDebounceKeyPrefix}wechat:{code}";
_logger.LogInformation("[AuthService] 检查防抖锁: {Key}", debounceKey);
_logger.LogInformation("[AuthService] <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {Key}", debounceKey);
var lockAcquired = await _redisService.TryAcquireLockAsync(debounceKey, "1", TimeSpan.FromSeconds(DebounceSeconds));
if (!lockAcquired)
{
_logger.LogWarning("[AuthService] 防抖触发,拒绝重复登录请求: {Code}", code);
_logger.LogWarning("[AuthService] <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܾ<EFBFBD><EFBFBD>ظ<EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {Code}", code);
return new LoginResult
{
Success = false,
ErrorMessage = "请勿频繁登录"
ErrorMessage = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼"
};
}
_logger.LogInformation("[AuthService] 防抖锁获取成功");
_logger.LogInformation("[AuthService] <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ<EFBFBD>ɹ<EFBFBD>");
// 1.1 调用微信API获取openid和unionid
_logger.LogInformation("[AuthService] 开始调用微信API获取openid...");
// 1.1 <EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD><EFBFBD>API<EFBFBD><EFBFBD>ȡopenid<EFBFBD><EFBFBD>unionid
_logger.LogInformation("[AuthService] <EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD><EFBFBD>API<EFBFBD><EFBFBD>ȡopenid...");
var wechatResult = await _wechatService.GetOpenIdAsync(code);
_logger.LogInformation("[AuthService] 微信API调用完成Success={Success}, OpenId={OpenId}, UnionId={UnionId}, Error={Error}",
_logger.LogInformation("[AuthService] ΢<EFBFBD><EFBFBD>API<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɣ<EFBFBD>Success={Success}, OpenId={OpenId}, UnionId={UnionId}, Error={Error}",
wechatResult.Success,
wechatResult.OpenId ?? "null",
wechatResult.UnionId ?? "null",
@ -98,89 +102,89 @@ public class AuthService : IAuthService
if (!wechatResult.Success)
{
_logger.LogWarning("[AuthService] 微信API调用失败: {Error}", wechatResult.ErrorMessage);
_logger.LogWarning("[AuthService] ΢<EFBFBD><EFBFBD>API<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><EFBFBD>: {Error}", wechatResult.ErrorMessage);
return new LoginResult
{
Success = false,
ErrorMessage = wechatResult.ErrorMessage ?? "登录失败,请稍后重试"
ErrorMessage = wechatResult.ErrorMessage ?? "<EFBFBD><EFBFBD>¼ʧ<EFBFBD>ܣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ժ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"
};
}
var openId = wechatResult.OpenId!;
var unionId = wechatResult.UnionId;
// 1.2 查找用户 - 优先通过unionid查找其次通过openid查找
// 1.2 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD> - <20><><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8>unionid<69><64><EFBFBD>ң<EFBFBD><D2A3><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8>openid<69><64><EFBFBD><EFBFBD>
User? user = null;
if (!string.IsNullOrWhiteSpace(unionId))
{
_logger.LogInformation("[AuthService] 尝试通过unionid查找用户: {UnionId}", unionId);
_logger.LogInformation("[AuthService] <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><EFBFBD>unionid<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>: {UnionId}", unionId);
user = await _userService.GetUserByUnionIdAsync(unionId);
_logger.LogInformation("[AuthService] unionid查找结果: {Found}", user != null ? $"找到用户ID={user.Id}" : "未找到");
_logger.LogInformation("[AuthService] unionid<EFBFBD><EFBFBD><EFBFBD>ҽ<EFBFBD><EFBFBD>: {Found}", user != null ? $"<22>ҵ<EFBFBD><D2B5>û<EFBFBD>ID={user.Id}" : <>ҵ<EFBFBD>");
}
if (user == null)
{
_logger.LogInformation("[AuthService] 尝试通过openid查找用户: {OpenId}", openId);
_logger.LogInformation("[AuthService] <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><EFBFBD>openid<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>: {OpenId}", openId);
user = await _userService.GetUserByOpenIdAsync(openId);
_logger.LogInformation("[AuthService] openid查找结果: {Found}", user != null ? $"找到用户ID={user.Id}" : "未找到");
_logger.LogInformation("[AuthService] openid<EFBFBD><EFBFBD><EFBFBD>ҽ<EFBFBD><EFBFBD>: {Found}", user != null ? $"<22>ҵ<EFBFBD><D2B5>û<EFBFBD>ID={user.Id}" : <>ҵ<EFBFBD>");
}
if (user == null)
{
// 1.3 用户不存在,创建新用户
_logger.LogInformation("[AuthService] 用户不存在,开始创建新用户...");
// 1.3 <EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>
_logger.LogInformation("[AuthService] <EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڣ<EFBFBD><EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>...");
var createDto = new CreateUserDto
{
OpenId = openId,
UnionId = unionId,
Nickname = $"用户{Random.Shared.Next(100000, 999999)}",
Headimg = GenerateDefaultAvatar(openId),
Nickname = await GetDefaultNicknameAsync(),
Headimg = await GetDefaultAvatarAsync(openId),
Pid = pid ?? 0
};
user = await _userService.CreateUserAsync(createDto);
_logger.LogInformation("[AuthService] 新用户创建成功: UserId={UserId}, OpenId={OpenId}", user.Id, openId);
_logger.LogInformation("[AuthService] <EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɹ<EFBFBD>: UserId={UserId}, OpenId={OpenId}", user.Id, openId);
}
else
{
// 1.4 用户存在更新unionid如果之前为空
// 1.4 <EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>unionid<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֮ǰΪ<EFBFBD>գ<EFBFBD>
if (string.IsNullOrWhiteSpace(user.UnionId) && !string.IsNullOrWhiteSpace(unionId))
{
_logger.LogInformation("[AuthService] 更新用户unionid: UserId={UserId}", user.Id);
_logger.LogInformation("[AuthService] <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>unionid: UserId={UserId}", user.Id);
await _userService.UpdateUserAsync(user.Id, new UpdateUserDto { UnionId = unionId });
_logger.LogInformation("[AuthService] unionid更新成功");
_logger.LogInformation("[AuthService] unionid<EFBFBD><EFBFBD><EFBFBD>³ɹ<EFBFBD>");
}
}
// 1.5 生成双 TokenAccess Token + Refresh Token
_logger.LogInformation("[AuthService] 开始生成双 Token: UserId={UserId}", user.Id);
// 1.5 <EFBFBD><EFBFBD><EFBFBD><EFBFBD>˫ Token<65><6E>Access Token + Refresh Token<65><6E>
_logger.LogInformation("[AuthService] <EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>˫ Token: UserId={UserId}", user.Id);
var loginResponse = await GenerateLoginResponseAsync(user, null);
_logger.LogInformation("[AuthService] 双 Token 生成成功AccessToken长度={Length}", loginResponse.AccessToken?.Length ?? 0);
_logger.LogInformation("[AuthService] ˫ Token <20><><EFBFBD>ɳɹ<C9B3><C9B9><EFBFBD>AccessToken<65><6E><EFBFBD><EFBFBD>={Length}", loginResponse.AccessToken?.Length ?? 0);
_logger.LogInformation("[AuthService] 微信登录成功: UserId={UserId}", user.Id);
_logger.LogInformation("[AuthService] ΢<EFBFBD>ŵ<EFBFBD>¼<EFBFBD>ɹ<EFBFBD>: UserId={UserId}", user.Id);
return new LoginResult
{
Success = true,
Token = loginResponse.AccessToken, // 兼容旧版
Token = loginResponse.AccessToken, // <EFBFBD><EFBFBD><EFBFBD>ݾɰ<EFBFBD>
UserId = user.Id,
LoginResponse = loginResponse
};
}
catch (Exception ex)
{
_logger.LogError(ex, "[AuthService] 微信登录异常: code={Code}, Message={Message}, StackTrace={StackTrace}",
_logger.LogError(ex, "[AuthService] ΢<EFBFBD>ŵ<EFBFBD>¼<EFBFBD>: code={Code}, Message={Message}, StackTrace={StackTrace}",
code, ex.Message, ex.StackTrace);
return new LoginResult
{
Success = false,
ErrorMessage = "网络故障,请稍后再试"
ErrorMessage = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ժ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"
};
}
}
/// <summary>
/// 手机号验证码登录
/// <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD>¼
/// Requirements: 2.1-2.7
/// </summary>
public async Task<LoginResult> MobileLoginAsync(string mobile, string code, int? pid, string? clickId)
@ -190,7 +194,7 @@ public class AuthService : IAuthService
return new LoginResult
{
Success = false,
ErrorMessage = "手机号不能为空"
ErrorMessage = "<EFBFBD>ֻ<EFBFBD><EFBFBD>Ų<EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>"
};
}
@ -199,13 +203,13 @@ public class AuthService : IAuthService
return new LoginResult
{
Success = false,
ErrorMessage = "验证码不能为空"
ErrorMessage = "<EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>"
};
}
try
{
// 2.6 防抖机制 - 3秒内不允许重复登录
// 2.6 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> - 3<><33><EFBFBD>ڲ<EFBFBD><DAB2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD><D8B8><EFBFBD>¼
var debounceKey = $"{LoginDebounceKeyPrefix}mobile:{mobile}";
var lockAcquired = await _redisService.TryAcquireLockAsync(debounceKey, "1", TimeSpan.FromSeconds(DebounceSeconds));
if (!lockAcquired)
@ -214,11 +218,11 @@ public class AuthService : IAuthService
return new LoginResult
{
Success = false,
ErrorMessage = "请勿频繁登录"
ErrorMessage = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼"
};
}
// 2.1 从Redis获取并验证验证码
// 2.1 <EFBFBD><EFBFBD>Redis<EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD>
var smsCodeKey = $"{SmsCodeKeyPrefix}{mobile}";
var storedCode = await _redisService.GetStringAsync(smsCodeKey);
@ -228,24 +232,24 @@ public class AuthService : IAuthService
return new LoginResult
{
Success = false,
ErrorMessage = "验证码错误"
ErrorMessage = "<EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"
};
}
// 2.2 验证码验证通过后删除Redis中的验证码
// 2.2 <EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɾ<EFBFBD><EFBFBD>Redis<EFBFBD>е<EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD>
await _redisService.DeleteAsync(smsCodeKey);
// 查找用户
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>
var user = await _userService.GetUserByMobileAsync(mobile);
if (user == null)
{
// 2.3 用户不存在,创建新用户
// 2.3 <EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>
var createDto = new CreateUserDto
{
Mobile = mobile,
Nickname = $"用户{Random.Shared.Next(100000, 999999)}",
Headimg = GenerateDefaultAvatar(mobile),
Nickname = await GetDefaultNicknameAsync(),
Headimg = await GetDefaultAvatarAsync(mobile),
Pid = pid ?? 0
};
@ -253,7 +257,7 @@ public class AuthService : IAuthService
_logger.LogInformation("New user created via mobile login: UserId={UserId}, Mobile={Mobile}", user.Id, MaskMobile(mobile));
}
// 2.4 生成双 TokenAccess Token + Refresh Token
// 2.4 <EFBFBD><EFBFBD><EFBFBD><EFBFBD>˫ Token<65><6E>Access Token + Refresh Token<65><6E>
var loginResponse = await GenerateLoginResponseAsync(user, null);
_logger.LogInformation("Mobile login successful: UserId={UserId}", user.Id);
@ -261,7 +265,7 @@ public class AuthService : IAuthService
return new LoginResult
{
Success = true,
Token = loginResponse.AccessToken, // 兼容旧版
Token = loginResponse.AccessToken, // <EFBFBD><EFBFBD><EFBFBD>ݾɰ<EFBFBD>
UserId = user.Id,
LoginResponse = loginResponse
};
@ -272,58 +276,58 @@ public class AuthService : IAuthService
return new LoginResult
{
Success = false,
ErrorMessage = "网络故障,请稍后再试"
ErrorMessage = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ժ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"
};
}
}
/// <summary>
/// 验证码绑定手机号
/// <EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
/// Requirements: 5.1-5.5
/// </summary>
public async Task<BindMobileResponse> BindMobileAsync(long userId, string mobile, string code)
{
if (string.IsNullOrWhiteSpace(mobile))
{
throw new ArgumentException("手机号不能为空", nameof(mobile));
throw new ArgumentException("<EFBFBD>ֻ<EFBFBD><EFBFBD>Ų<EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>", nameof(mobile));
}
if (string.IsNullOrWhiteSpace(code))
{
throw new ArgumentException("验证码不能为空", nameof(code));
throw new ArgumentException("<EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>", nameof(code));
}
// 5.1 验证短信验证码
// 5.1 <EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD>
var smsCodeKey = $"{SmsCodeKeyPrefix}{mobile}";
var storedCode = await _redisService.GetStringAsync(smsCodeKey);
if (string.IsNullOrWhiteSpace(storedCode) || storedCode != code)
{
_logger.LogWarning("SMS code verification failed for bind mobile: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile));
throw new InvalidOperationException("验证码错误");
throw new InvalidOperationException("<EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
// 验证码验证通过后删除
// <EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɾ<EFBFBD><EFBFBD>
await _redisService.DeleteAsync(smsCodeKey);
// 获取当前用户
// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD>
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("用户不存在");
throw new InvalidOperationException("<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
// 检查手机号是否已被其他用户绑定
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD>
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// 5.2 手机号已被其他用户绑定,需要合并账户
// 5.2 <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD>󶨣<EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD>ϲ<EFBFBD><EFBFBD>˻<EFBFBD>
return await MergeAccountsAsync(currentUser, existingUser);
}
// 5.4 手机号未被绑定,直接更新当前用户的手机号
// 5.4 <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD><EFBFBD><EFBFBD>󶨣<EFBFBD>ֱ<EFBFBD>Ӹ<EFBFBD><EFBFBD>µ<EFBFBD>ǰ<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
await _userService.UpdateUserAsync(userId, new UpdateUserDto { Mobile = mobile });
_logger.LogInformation("Mobile bound successfully: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile));
@ -331,43 +335,43 @@ public class AuthService : IAuthService
}
/// <summary>
/// 微信授权绑定手机号
/// ΢<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ȩ<EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
/// Requirements: 5.1-5.5
/// </summary>
public async Task<BindMobileResponse> WechatBindMobileAsync(long userId, string wechatCode)
{
if (string.IsNullOrWhiteSpace(wechatCode))
{
throw new ArgumentException("微信授权code不能为空", nameof(wechatCode));
throw new ArgumentException("΢<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ȩcode<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>", nameof(wechatCode));
}
// 调用微信API获取手机号
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD><EFBFBD>API<EFBFBD><EFBFBD>ȡ<EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
var mobileResult = await _wechatService.GetMobileAsync(wechatCode);
if (!mobileResult.Success || string.IsNullOrWhiteSpace(mobileResult.Mobile))
{
_logger.LogWarning("WeChat get mobile failed: UserId={UserId}, Error={Error}", userId, mobileResult.ErrorMessage);
throw new InvalidOperationException(mobileResult.ErrorMessage ?? "获取手机号失败");
throw new InvalidOperationException(mobileResult.ErrorMessage ?? "<EFBFBD><EFBFBD>ȡ<EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><EFBFBD>");
}
var mobile = mobileResult.Mobile;
// 获取当前用户
// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD>
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("用户不存在");
throw new InvalidOperationException("<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
// 检查手机号是否已被其他用户绑定
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD>
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// 5.2 手机号已被其他用户绑定,需要合并账户
// 5.2 <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD>󶨣<EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD>ϲ<EFBFBD><EFBFBD>˻<EFBFBD>
return await MergeAccountsAsync(currentUser, existingUser);
}
// 5.4 手机号未被绑定,直接更新当前用户的手机号
// 5.4 <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD><EFBFBD><EFBFBD>󶨣<EFBFBD>ֱ<EFBFBD>Ӹ<EFBFBD><EFBFBD>µ<EFBFBD>ǰ<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
await _userService.UpdateUserAsync(userId, new UpdateUserDto { Mobile = mobile });
_logger.LogInformation("Mobile bound via WeChat successfully: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile));
@ -376,7 +380,7 @@ public class AuthService : IAuthService
/// <summary>
/// 记录登录信息
/// <EFBFBD><EFBFBD>¼<EFBFBD><EFBFBD>¼<EFBFBD><EFBFBD>Ϣ
/// Requirements: 6.1, 6.3, 6.4
/// </summary>
public async Task<RecordLoginResponse> RecordLoginAsync(long userId, string? device, string? deviceInfo)
@ -384,17 +388,17 @@ public class AuthService : IAuthService
var user = await _userService.GetUserByIdAsync(userId);
if (user == null)
{
throw new InvalidOperationException("用户不存在");
throw new InvalidOperationException("<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
try
{
// 获取客户端IP这里使用空字符串作为占位符实际IP应从Controller传入
// <EFBFBD><EFBFBD>ȡ<EFBFBD>ͻ<EFBFBD><EFBFBD><EFBFBD>IP<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD>ÿ<EFBFBD><EFBFBD>ַ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊռλ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD><EFBFBD>IPӦ<EFBFBD><EFBFBD>Controller<EFBFBD><EFBFBD><EFBFBD>
var clientIp = deviceInfo ?? string.Empty;
var now = DateTime.Now;
// 6.1 记录登录日志
// 6.1 <EFBFBD><EFBFBD>¼<EFBFBD><EFBFBD>¼<EFBFBD><EFBFBD>־
var loginLog = new UserLoginLog
{
UserId = userId,
@ -408,7 +412,7 @@ public class AuthService : IAuthService
await _dbContext.UserLoginLogs.AddAsync(loginLog);
// 更新用户最后登录时间
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼ʱ<EFBFBD><EFBFBD>
user.LastLoginTime = now;
user.LastLoginIp = clientIp;
_dbContext.Users.Update(user);
@ -417,7 +421,7 @@ public class AuthService : IAuthService
_logger.LogInformation("Login recorded: UserId={UserId}, Device={Device}, IP={IP}", userId, device, clientIp);
// 6.4 返回用户的uid、昵称和头像
// 6.4 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD>uid<EFBFBD><EFBFBD><EFBFBD>dzƺ<EFBFBD>ͷ<EFBFBD><EFBFBD>
return new RecordLoginResponse
{
Uid = user.Uid,
@ -433,33 +437,33 @@ public class AuthService : IAuthService
}
/// <summary>
/// H5绑定手机号(无需验证码)
/// H5<EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD>ţ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>
/// Requirements: 13.1
/// </summary>
public async Task<BindMobileResponse> BindMobileH5Async(long userId, string mobile)
{
if (string.IsNullOrWhiteSpace(mobile))
{
throw new ArgumentException("手机号不能为空", nameof(mobile));
throw new ArgumentException("<EFBFBD>ֻ<EFBFBD><EFBFBD>Ų<EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>", nameof(mobile));
}
// 获取当前用户
// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD>
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("用户不存在");
throw new InvalidOperationException("<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
// 检查手机号是否已被其他用户绑定
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD>
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// 手机号已被其他用户绑定,需要合并账户
// <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD>󶨣<EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD>ϲ<EFBFBD><EFBFBD>˻<EFBFBD>
return await MergeAccountsAsync(currentUser, existingUser);
}
// 手机号未被绑定,直接更新当前用户的手机号
// <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD><EFBFBD><EFBFBD>󶨣<EFBFBD>ֱ<EFBFBD>Ӹ<EFBFBD><EFBFBD>µ<EFBFBD>ǰ<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
await _userService.UpdateUserAsync(userId, new UpdateUserDto { Mobile = mobile });
_logger.LogInformation("H5 Mobile bound successfully: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile));
@ -467,7 +471,7 @@ public class AuthService : IAuthService
}
/// <summary>
/// 账号注销
/// <EFBFBD>˺<EFBFBD>ע<EFBFBD><EFBFBD>
/// Requirements: 7.1-7.3
/// </summary>
public async Task LogOffAsync(long userId, int type)
@ -475,23 +479,23 @@ public class AuthService : IAuthService
var user = await _userService.GetUserByIdAsync(userId);
if (user == null)
{
throw new InvalidOperationException("用户不存在");
throw new InvalidOperationException("<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
try
{
// 7.1 记录注销请求日志
var action = type == 0 ? "注销账号" : "取消注销";
// 7.1 <EFBFBD><EFBFBD>¼ע<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־
var action = type == 0 ? "ע<EFBFBD><EFBFBD><EFBFBD>˺<EFBFBD>" : <><C8A1>ע<EFBFBD><D7A2>";
_logger.LogInformation("User log off request: UserId={UserId}, Type={Type}, Action={Action}", userId, type, action);
// 这里可以添加更多的注销逻辑,比如:
// - 将用户状态设置为已注销
// - 清理用户相关的缓存
// - 发送通知等
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD><EFBFBD>߼<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// - <EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>״̬<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD>
// - <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD>صĻ<EFBFBD><EFBFBD><EFBFBD>
// - <EFBFBD><EFBFBD><EFBFBD><EFBFBD>֪ͨ<EFBFBD><EFBFBD>
if (type == 0)
{
// 注销账号 - 可以设置用户状态为禁用
// ע<EFBFBD><EFBFBD><EFBFBD>˺<EFBFBD> - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>״̬Ϊ<CCAC><CEAA><EFBFBD><EFBFBD>
user.Status = 0;
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync();
@ -499,14 +503,14 @@ public class AuthService : IAuthService
}
else if (type == 1)
{
// 取消注销 - 恢复用户状态
// ȡ<EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD> - <20>ָ<EFBFBD><D6B8>û<EFBFBD>״̬
user.Status = 1;
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("User account reactivated: UserId={UserId}", userId);
}
// 7.2 返回注销成功的消息(通过不抛出异常来表示成功)
// 7.2 <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD><EFBFBD>ɹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ<EFBFBD><EFBFBD>ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>׳<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʾ<EFBFBD>ɹ<EFBFBD><EFBFBD><EFBFBD>
}
catch (Exception ex)
{
@ -519,24 +523,24 @@ public class AuthService : IAuthService
#region Refresh Token Methods
/// <summary>
/// 生成 Refresh Token 并存储到数据库
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Refresh Token <20><><EFBFBD><EFBFBD><E6B4A2><EFBFBD><EFBFBD><EFBFBD>ݿ<EFBFBD>
/// Requirements: 1.4, 1.5, 4.1
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="ipAddress">客户端 IP 地址</param>
/// <returns>生成的 Refresh Token 明文</returns>
/// <param name="userId"><EFBFBD>û<EFBFBD>ID</param>
/// <param name="ipAddress"><EFBFBD>ͻ<EFBFBD><EFBFBD><EFBFBD> IP <20><>ַ</param>
/// <returns><EFBFBD><EFBFBD><EFBFBD>ɵ<EFBFBD> Refresh Token <20><><EFBFBD><EFBFBD></returns>
private async Task<string> GenerateRefreshTokenAsync(long userId, string? ipAddress)
{
// 生成随机 Refresh Token
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Refresh Token
var refreshToken = GenerateSecureRandomString(RefreshTokenLength);
// 计算 SHA256 哈希值用于存储
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> SHA256 <20><>ϣֵ<CFA3><D6B5><EFBFBD>ڴ洢
var tokenHash = ComputeSha256Hash(refreshToken);
// 计算过期时间7天
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD>䣨7<EFBFBD>
var expiresAt = DateTime.Now.AddDays(_jwtSettings.RefreshTokenExpirationDays);
// 创建数据库记录
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݿ<EFBFBD><EFBFBD>¼
var userRefreshToken = new UserRefreshToken
{
UserId = userId,
@ -555,21 +559,21 @@ public class AuthService : IAuthService
}
/// <summary>
/// 生成登录响应(包含双 Token
/// <EFBFBD><EFBFBD><EFBFBD>ɵ<EFBFBD>¼<EFBFBD><EFBFBD>Ӧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˫ Token<65><6E>
/// Requirements: 1.1, 1.2, 1.3, 1.4, 1.5
/// </summary>
/// <param name="user">用户实体</param>
/// <param name="ipAddress">客户端 IP 地址</param>
/// <returns>登录响应</returns>
/// <param name="user"><EFBFBD>û<EFBFBD>ʵ<EFBFBD><EFBFBD></param>
/// <param name="ipAddress"><EFBFBD>ͻ<EFBFBD><EFBFBD><EFBFBD> IP <20><>ַ</param>
/// <returns><EFBFBD><EFBFBD>¼<EFBFBD><EFBFBD>Ӧ</returns>
private async Task<LoginResponse> GenerateLoginResponseAsync(User user, string? ipAddress)
{
// 生成 Access Token (JWT)
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Access Token (JWT)
var accessToken = _jwtService.GenerateToken(user);
// 生成 Refresh Token 并存储
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Refresh Token <20><><EFBFBD>
var refreshToken = await GenerateRefreshTokenAsync(user.Id, ipAddress);
// 计算 Access Token 过期时间(秒)
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Access Token <20><><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD>
var expiresIn = _jwtSettings.ExpirationMinutes * 60;
return new LoginResponse
@ -582,7 +586,7 @@ public class AuthService : IAuthService
}
/// <summary>
/// 刷新 Token
/// ˢ<EFBFBD><EFBFBD> Token
/// Requirements: 2.1-2.6
/// </summary>
public async Task<RefreshTokenResult> RefreshTokenAsync(string refreshToken, string? ipAddress)
@ -590,15 +594,15 @@ public class AuthService : IAuthService
if (string.IsNullOrWhiteSpace(refreshToken))
{
_logger.LogWarning("Refresh token is empty");
return RefreshTokenResult.Fail("刷新令牌不能为空");
return RefreshTokenResult.Fail("ˢ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʋ<EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>");
}
try
{
// 计算 Token 哈希值
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token <20><>ϣֵ
var tokenHash = ComputeSha256Hash(refreshToken);
// 查找 Token 记录
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token <20><>¼
var storedToken = await _dbContext.UserRefreshTokens
.Include(t => t.User)
.FirstOrDefaultAsync(t => t.TokenHash == tokenHash);
@ -606,47 +610,47 @@ public class AuthService : IAuthService
if (storedToken == null)
{
_logger.LogWarning("Refresh token not found: {TokenHash}", tokenHash.Substring(0, 8) + "...");
return RefreshTokenResult.Fail("无效的刷新令牌");
return RefreshTokenResult.Fail("<EFBFBD><EFBFBD>Ч<EFBFBD><EFBFBD>ˢ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
// 检查是否已过期
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><EFBFBD>ѹ<EFBFBD><EFBFBD><EFBFBD>
if (storedToken.IsExpired)
{
_logger.LogWarning("Refresh token expired for user {UserId}", storedToken.UserId);
return RefreshTokenResult.Fail("刷新令牌已过期");
return RefreshTokenResult.Fail("ˢ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѹ<EFBFBD><EFBFBD><EFBFBD>");
}
// 检查是否已撤销
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><EFBFBD>ѳ<EFBFBD><EFBFBD><EFBFBD>
if (storedToken.IsRevoked)
{
_logger.LogWarning("Refresh token revoked for user {UserId}", storedToken.UserId);
return RefreshTokenResult.Fail("刷新令牌已失效");
return RefreshTokenResult.Fail("ˢ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧЧ");
}
// 检查用户是否存在且有效
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD>Ƿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ч
var user = storedToken.User;
if (user == null)
{
_logger.LogWarning("User not found for refresh token");
return RefreshTokenResult.Fail("用户不存在");
return RefreshTokenResult.Fail("<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
if (user.Status == 0)
{
_logger.LogWarning("User {UserId} is disabled", user.Id);
return RefreshTokenResult.Fail("账号已被禁用");
return RefreshTokenResult.Fail("<EFBFBD>˺<EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
// Token 轮换:生成新的 Refresh Token
// Token <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>µ<EFBFBD> Refresh Token
var newRefreshToken = GenerateSecureRandomString(RefreshTokenLength);
var newTokenHash = ComputeSha256Hash(newRefreshToken);
// 撤销旧 Token 并记录关联关系
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token <20><><EFBFBD><EFBFBD>¼<EFBFBD><C2BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϵ
storedToken.RevokedAt = DateTime.Now;
storedToken.RevokedByIp = ipAddress;
storedToken.ReplacedByToken = newTokenHash;
// 创建新的 Token 记录
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>µ<EFBFBD> Token <20><>¼
var newUserRefreshToken = new UserRefreshToken
{
UserId = user.Id,
@ -659,7 +663,7 @@ public class AuthService : IAuthService
await _dbContext.UserRefreshTokens.AddAsync(newUserRefreshToken);
await _dbContext.SaveChangesAsync();
// 生成新的 Access Token
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>µ<EFBFBD> Access Token
var accessToken = _jwtService.GenerateToken(user);
var expiresIn = _jwtSettings.ExpirationMinutes * 60;
@ -676,12 +680,12 @@ public class AuthService : IAuthService
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing token");
return RefreshTokenResult.Fail("刷新令牌失败,请稍后重试");
return RefreshTokenResult.Fail("ˢ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD>ܣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ժ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
}
/// <summary>
/// 撤销 Token
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token
/// Requirements: 4.4
/// </summary>
public async Task RevokeTokenAsync(string refreshToken, string? ipAddress)
@ -694,10 +698,10 @@ public class AuthService : IAuthService
try
{
// 计算 Token 哈希值
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token <20><>ϣֵ
var tokenHash = ComputeSha256Hash(refreshToken);
// 查找 Token 记录
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token <20><>¼
var storedToken = await _dbContext.UserRefreshTokens
.FirstOrDefaultAsync(t => t.TokenHash == tokenHash);
@ -707,14 +711,14 @@ public class AuthService : IAuthService
return;
}
// 如果已经撤销,直接返回
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ѿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֱ<EFBFBD>ӷ<EFBFBD><EFBFBD><EFBFBD>
if (storedToken.IsRevoked)
{
_logger.LogInformation("Refresh token already revoked");
return;
}
// 撤销 Token
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token
storedToken.RevokedAt = DateTime.Now;
storedToken.RevokedByIp = ipAddress;
@ -730,14 +734,14 @@ public class AuthService : IAuthService
}
/// <summary>
/// 撤销用户的所有 Token
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token
/// Requirements: 4.4
/// </summary>
public async Task RevokeAllUserTokensAsync(long userId, string? ipAddress)
{
try
{
// 查找用户所有有效的 Token
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ч<EFBFBD><EFBFBD> Token
var activeTokens = await _dbContext.UserRefreshTokens
.Where(t => t.UserId == userId && t.RevokedAt == null)
.ToListAsync();
@ -771,7 +775,7 @@ public class AuthService : IAuthService
#region Private Helper Methods
/// <summary>
/// 合并账户 - 将当前用户的openid迁移到手机号用户
/// <EFBFBD>ϲ<EFBFBD><EFBFBD>˻<EFBFBD> - <20><><EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD><C3BB><EFBFBD>openidǨ<64>Ƶ<EFBFBD><C6B5>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD>û<EFBFBD>
/// </summary>
private async Task<BindMobileResponse> MergeAccountsAsync(User currentUser, User mobileUser)
{
@ -781,7 +785,7 @@ public class AuthService : IAuthService
_logger.LogInformation("Merging accounts: CurrentUserId={CurrentUserId}, MobileUserId={MobileUserId}",
currentUser.Id, mobileUser.Id);
// 5.2 将当前用户的openid迁移到手机号用户
// 5.2 <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD>openidǨ<EFBFBD>Ƶ<EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>
if (!string.IsNullOrWhiteSpace(currentUser.OpenId))
{
mobileUser.OpenId = currentUser.OpenId;
@ -793,13 +797,13 @@ public class AuthService : IAuthService
mobileUser.UpdateTime = DateTime.Now;
_dbContext.Users.Update(mobileUser);
// 删除当前用户
// ɾ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD>
_dbContext.Users.Remove(currentUser);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
// 5.3 生成新的token
// 5.3 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>µ<EFBFBD>token
var newToken = _jwtService.GenerateToken(mobileUser);
_logger.LogInformation("Accounts merged successfully: NewUserId={NewUserId}", mobileUser.Id);
@ -816,18 +820,70 @@ public class AuthService : IAuthService
}
/// <summary>
/// 生成默认头像URL
/// 获取用户默认昵称(从配置读取前缀)
/// </summary>
private async Task<string> GetDefaultNicknameAsync()
{
try
{
var configJson = await _configService.GetConfigValueAsync("user_config");
if (!string.IsNullOrEmpty(configJson))
{
var config = JsonSerializer.Deserialize<JsonElement>(configJson);
if (config.TryGetProperty("default_nickname_prefix", out var prefixEl) &&
prefixEl.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(prefixEl.GetString()))
{
return $"{prefixEl.GetString()}{Random.Shared.Next(100000, 999999)}";
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "读取用户默认昵称配置失败,使用默认值");
}
return $"用户{Random.Shared.Next(100000, 999999)}";
}
/// <summary>
/// 获取用户默认头像(从配置读取,为空则自动生成)
/// </summary>
private async Task<string> GetDefaultAvatarAsync(string seed)
{
try
{
var configJson = await _configService.GetConfigValueAsync("user_config");
if (!string.IsNullOrEmpty(configJson))
{
var config = JsonSerializer.Deserialize<JsonElement>(configJson);
if (config.TryGetProperty("default_avatar", out var avatarEl) &&
avatarEl.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(avatarEl.GetString()))
{
return avatarEl.GetString()!;
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "读取用户默认头像配置失败,使用系统生成");
}
return GenerateDefaultAvatar(seed);
}
/// <summary>
/// <20><><EFBFBD><EFBFBD>Ĭ<EFBFBD><C4AC>ͷ<EFBFBD><CDB7>URL
/// </summary>
private static string GenerateDefaultAvatar(string seed)
{
// 使用种子生成一个简单的默认头像URL
// 实际项目中可以使用Identicon库或其他头像生成服务
// ʹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD><EFBFBD>򵥵<EFBFBD>Ĭ<EFBFBD><EFBFBD>ͷ<EFBFBD><EFBFBD>URL
// ʵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD>п<EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD>Identicon<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͷ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɷ<EFBFBD><EFBFBD><EFBFBD>
var hash = ComputeMd5(seed);
return $"https://api.dicebear.com/7.x/identicon/svg?seed={hash}";
}
/// <summary>
/// 计算MD5哈希
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>MD5<EFBFBD><EFBFBD>ϣ
/// </summary>
private static string ComputeMd5(string input)
{
@ -837,7 +893,7 @@ public class AuthService : IAuthService
}
/// <summary>
/// 生成随机字符串
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ<EFBFBD><EFBFBD><EFBFBD>
/// </summary>
private static string GenerateRandomString(int length)
{
@ -851,7 +907,7 @@ public class AuthService : IAuthService
}
/// <summary>
/// 脱敏手机号
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
/// </summary>
private static string MaskMobile(string mobile)
{
@ -862,7 +918,7 @@ public class AuthService : IAuthService
}
/// <summary>
/// 获取年份中的周数
/// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>е<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// </summary>
private static int GetWeekOfYear(DateTime date)
{
@ -871,7 +927,7 @@ public class AuthService : IAuthService
}
/// <summary>
/// 计算 SHA256 哈希值
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> SHA256 <20><>ϣֵ
/// Requirements: 4.1
/// </summary>
private static string ComputeSha256Hash(string input)
@ -882,7 +938,7 @@ public class AuthService : IAuthService
}
/// <summary>
/// 生成安全的随机字符串(用于 Refresh Token
/// <EFBFBD><EFBFBD><EFBFBD>ɰ<EFBFBD>ȫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Refresh Token<65><6E>
/// </summary>
private static string GenerateSecureRandomString(int length)
{