From 8ae6dcfa88674992e39747e1aa2d4cb6ff8c96d6 Mon Sep 17 00:00:00 2001 From: 18631081161 <2088094923@qq.com> Date: Sat, 28 Mar 2026 20:36:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/UserController.cs | 60 +++- .../Models/Common/ErrorCodes.cs | 5 + .../Models/User/BindParentUserRequest.cs | 21 ++ .../Interfaces/IUserBusinessService.cs | 15 + .../Services/UserBusinessService.cs | 133 +++++++- .../admin-web/src/api/business/user.ts | 36 +++ .../src/views/business/user/index.vue | 298 +++++++++++++++++- 7 files changed, 561 insertions(+), 7 deletions(-) create mode 100644 server/MiAssessment/src/MiAssessment.Admin.Business/Models/User/BindParentUserRequest.cs diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/UserController.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/UserController.cs index 641f098..8a6aa90 100644 --- a/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/UserController.cs +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/UserController.cs @@ -1,4 +1,4 @@ -using MiAssessment.Admin.Business.Attributes; +using MiAssessment.Admin.Business.Attributes; using MiAssessment.Admin.Business.Models; using MiAssessment.Admin.Business.Models.Common; using MiAssessment.Admin.Business.Models.User; @@ -129,6 +129,64 @@ public class UserController : BusinessControllerBase } } + /// + /// 绑定上下级关系(渠道商-合伙人) + /// + /// 绑定请求 + /// 操作结果 + [HttpPost("bindParent")] + [BusinessPermission("user:update")] + public async Task BindParentUser([FromBody] BindParentUserRequest request) + { + if (!ModelState.IsValid) + { + return ValidationError("参数验证失败"); + } + + try + { + var result = await _userService.BindParentUserAsync(request.UserId, request.ParentUserId); + if (result) + { + return Ok("绑定上下级关系成功"); + } + return Error(ErrorCodes.BusinessError, "绑定失败"); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + + /// + /// 解除上下级关系 + /// + /// 请求(包含用户ID) + /// 操作结果 + [HttpPost("unbindParent")] + [BusinessPermission("user:update")] + public async Task UnbindParentUser([FromBody] DeleteRequest request) + { + if (!ModelState.IsValid) + { + return ValidationError("参数验证失败"); + } + + try + { + var result = await _userService.UnbindParentUserAsync(request.Id); + if (result) + { + return Ok("解除上下级关系成功"); + } + return Error(ErrorCodes.BusinessError, "解除失败"); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + /// /// 导出用户列表 /// diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Models/Common/ErrorCodes.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Models/Common/ErrorCodes.cs index 369a68b..0216ea6 100644 --- a/server/MiAssessment/src/MiAssessment.Admin.Business/Models/Common/ErrorCodes.cs +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Models/Common/ErrorCodes.cs @@ -182,6 +182,11 @@ public static class ErrorCodes /// public const int UserNotFound = 3301; + /// + /// 上级用户不存在 + /// + public const int ParentUserNotFound = 3302; + #endregion #region 订单模块错误 (3400-3499) diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Models/User/BindParentUserRequest.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Models/User/BindParentUserRequest.cs new file mode 100644 index 0000000..dca545b --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Models/User/BindParentUserRequest.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace MiAssessment.Admin.Business.Models.User; + +/// +/// 绑定上下级关系请求 +/// +public class BindParentUserRequest +{ + /// + /// 下级用户ID(合伙人) + /// + [Required] + public long UserId { get; set; } + + /// + /// 上级用户ID(渠道商) + /// + [Required] + public long ParentUserId { get; set; } +} diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Services/Interfaces/IUserBusinessService.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Services/Interfaces/IUserBusinessService.cs index 8fe213f..97b55e7 100644 --- a/server/MiAssessment/src/MiAssessment.Admin.Business/Services/Interfaces/IUserBusinessService.cs +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Services/Interfaces/IUserBusinessService.cs @@ -58,6 +58,21 @@ public interface IUserBusinessService /// 导出数据 Task> ExportUsersAsync(UserQueryRequest request); + /// + /// 绑定上下级关系(上级必须是渠道商,下级必须是合伙人) + /// + /// 下级用户ID(合伙人) + /// 上级用户ID(渠道商) + /// 是否成功 + Task BindParentUserAsync(long userId, long parentUserId); + + /// + /// 解除上下级关系 + /// + /// 用户ID + /// 是否成功 + Task UnbindParentUserAsync(long userId); + /// /// 删除用户(硬删除,同时清除登录记录和令牌) /// diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Services/UserBusinessService.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Services/UserBusinessService.cs index 275f7dc..1351af2 100644 --- a/server/MiAssessment/src/MiAssessment.Admin.Business/Services/UserBusinessService.cs +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Services/UserBusinessService.cs @@ -211,12 +211,143 @@ public class UserBusinessService : IUserBusinessService throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在"); } + if (userLevel < 1 || userLevel > 3) + { + throw new BusinessException(ErrorCodes.ParamError, "无效的用户等级"); + } + + var oldLevel = user.UserLevel; user.UserLevel = userLevel; user.UpdateTime = DateTime.Now; + // 等级变更时自动处理上下级关系 + if (user.ParentUserId.HasValue && oldLevel != userLevel) + { + var shouldUnbind = await ShouldUnbindParentOnLevelChange(user.ParentUserId.Value, userLevel); + if (shouldUnbind) + { + _logger.LogInformation( + "用户 {UserId} 等级从 {OldLevel} 变更为 {NewLevel},自动解除与上级 {ParentUserId} 的关系", + id, oldLevel, userLevel, user.ParentUserId.Value); + user.ParentUserId = null; + } + } + + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation("更新用户等级成功,ID: {UserId}, 等级: {OldLevel} -> {UserLevel}", id, oldLevel, userLevel); + + return true; + } + + /// + /// 判断等级变更时是否需要解除与上级的关系 + /// 规则: + /// - 提升为渠道商(3):一律解除上下级关系 + /// - 提升为合伙人(2):仅当上级是渠道商(3)时保留,否则解除 + /// - 变为普通用户(1):保留原有关系不变 + /// + private async Task ShouldUnbindParentOnLevelChange(long parentUserId, int newLevel) + { + if (newLevel == 3) + return true; + + if (newLevel == 2) + { + var parentLevel = await _dbContext.Users + .AsNoTracking() + .Where(u => u.Id == parentUserId && !u.IsDeleted) + .Select(u => u.UserLevel) + .FirstOrDefaultAsync(); + + return parentLevel != 3; + } + + return false; + } + + /// + public async Task BindParentUserAsync(long userId, long parentUserId) + { + if (userId == parentUserId) + { + throw new BusinessException(ErrorCodes.InvalidOperation, "不能绑定自己为上级"); + } + + var user = await _dbContext.Users + .Where(u => u.Id == userId && !u.IsDeleted) + .FirstOrDefaultAsync(); + + if (user == null) + { + throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在"); + } + + var parentUser = await _dbContext.Users + .AsNoTracking() + .Where(u => u.Id == parentUserId && !u.IsDeleted) + .FirstOrDefaultAsync(); + + if (parentUser == null) + { + throw new BusinessException(ErrorCodes.ParentUserNotFound, "上级用户不存在"); + } + + if (parentUser.UserLevel != 3) + { + throw new BusinessException(ErrorCodes.InvalidOperation, "上级用户必须是渠道商"); + } + + if (user.UserLevel != 2) + { + throw new BusinessException(ErrorCodes.InvalidOperation, "只有合伙人才能绑定上级渠道商"); + } + + if (user.ParentUserId.HasValue) + { + throw new BusinessException(ErrorCodes.InvalidOperation, "该用户已有上级,请先解除现有上下级关系"); + } + + // 防止循环引用:检查上级的上级是否是当前用户 + if (parentUser.ParentUserId == userId) + { + throw new BusinessException(ErrorCodes.InvalidOperation, "不能形成循环上下级关系"); + } + + user.ParentUserId = parentUserId; + user.UpdateTime = DateTime.Now; + await _dbContext.SaveChangesAsync(); - _logger.LogInformation("更新用户等级成功,ID: {UserId}, 等级: {UserLevel}", id, userLevel); + _logger.LogInformation("绑定上下级关系成功,用户 {UserId} 的上级设为 {ParentUserId}", userId, parentUserId); + + return true; + } + + /// + public async Task UnbindParentUserAsync(long userId) + { + var user = await _dbContext.Users + .Where(u => u.Id == userId && !u.IsDeleted) + .FirstOrDefaultAsync(); + + if (user == null) + { + throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在"); + } + + if (!user.ParentUserId.HasValue) + { + throw new BusinessException(ErrorCodes.InvalidOperation, "该用户没有上级,无需解除"); + } + + var oldParentId = user.ParentUserId.Value; + user.ParentUserId = null; + user.UpdateTime = DateTime.Now; + + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation("解除上下级关系成功,用户 {UserId} 与上级 {ParentUserId} 解绑", userId, oldParentId); return true; } diff --git a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/api/business/user.ts b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/api/business/user.ts index 624db68..2e79e34 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/api/business/user.ts +++ b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/api/business/user.ts @@ -93,6 +93,16 @@ export interface UpdateUserLevelRequest { userLevel: number } +/** + * 绑定上下级关系请求 + */ +export interface BindParentUserRequest { + /** 下级用户ID(合伙人) */ + userId: number + /** 上级用户ID(渠道商) */ + parentUserId: number +} + // ==================== User API ==================== /** @@ -161,6 +171,32 @@ export function exportUsers(params: UserQuery): Promise> { }) } +/** + * 绑定上下级关系(渠道商-合伙人) + * @param data 绑定请求 + * @returns 绑定结果 + */ +export function bindParentUser(data: BindParentUserRequest): Promise> { + return request({ + url: '/admin/user/bindParent', + method: 'post', + data + }) +} + +/** + * 解除上下级关系 + * @param id 用户ID + * @returns 解除结果 + */ +export function unbindParentUser(id: number): Promise> { + return request({ + url: '/admin/user/unbindParent', + method: 'post', + data: { id } + }) +} + /** * 删除用户(硬删除,同时清除登录记录和令牌) * @param id 用户ID diff --git a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/user/index.vue b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/user/index.vue index 6655fed..0773ca6 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/user/index.vue +++ b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/user/index.vue @@ -161,7 +161,7 @@ - + @@ -387,8 +485,8 @@ * @description 管理C端用户信息,支持搜索、查看详情、状态管理、等级修改、导出 * @requirements 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7 */ -import { reactive, ref, onMounted } from 'vue' -import { Search, Refresh, View, Edit, Download, User, Delete } from '@element-plus/icons-vue' +import { reactive, ref, computed, onMounted } from 'vue' +import { Search, Refresh, View, Edit, Download, User, Delete, Link } from '@element-plus/icons-vue' import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus' import { getUserList, @@ -397,6 +495,8 @@ import { updateUserLevel, exportUsers, deleteUser, + bindParentUser, + unbindParentUser, type UserItem, type UserDetail, type UserQuery @@ -461,6 +561,16 @@ interface LevelFormData { userLevel: string } +interface BindFormData { + userId: number + userNickname: string + userUid: string + userLevel: number + currentParentNickname: string + currentParentUid: string + parentUid: string +} + interface UserPageState { loading: boolean tableData: UserItemWithLoading[] @@ -481,12 +591,16 @@ interface UserPageState { subParentId: number subParentNickname: string subParentUid: string + bindDialogVisible: boolean + bindFormData: BindFormData + bindFormLoading: boolean } // ============ Refs ============ const tableRef = ref>() const levelFormRef = ref() +const bindFormRef = ref() const dateRange = ref<[string, string] | null>(null) // ============ State ============ @@ -527,7 +641,18 @@ const state = reactive({ subPageSize: 10, subParentId: 0, subParentNickname: '', - subParentUid: '' + subParentUid: '', + bindDialogVisible: false, + bindFormData: { + userId: 0, + userNickname: '', + userUid: '', + userLevel: 0, + currentParentNickname: '', + currentParentUid: '', + parentUid: '' + }, + bindFormLoading: false }) // ============ Form Rules ============ @@ -538,6 +663,26 @@ const levelFormRules: FormRules = { ] } +const bindFormRules: FormRules = { + parentUid: [ + { required: true, message: '请输入渠道商的UID', trigger: 'blur' } + ] +} + +const levelChangeWarning = computed(() => { + const newLevel = Number(state.levelFormData.userLevel) + const currentLevel = state.levelFormData.currentLevel + if (!newLevel || newLevel === currentLevel) return '' + + if (newLevel === USER_LEVEL.CHANNEL) { + return '提升为渠道商后,该用户与其上级的关系将自动解除' + } + if (newLevel === USER_LEVEL.PARTNER && currentLevel === USER_LEVEL.NORMAL) { + return '提升为合伙人后,仅当上级是渠道商时保留上下级关系,否则自动解除' + } + return '' +}) + // ============ Helper Functions ============ function getLevelTagType(level: number): 'info' | 'success' | 'warning' | 'danger' { @@ -802,6 +947,141 @@ async function handleLevelSubmit() { } } +async function handleDetailUnbind() { + if (!state.userDetail) return + + try { + await ElMessageBox.confirm( + `确认解除 "${state.userDetail.nickname}" 与上级 "${state.userDetail.parentUserNickname}" 的关系吗?`, + '解除确认', + { confirmButtonText: '确认解除', cancelButtonText: '取消', type: 'warning' } + ) + } catch { + return + } + + try { + const res = await unbindParentUser(state.userDetail.id) + if (res.code === 0) { + ElMessage.success('解除上下级关系成功') + await loadUserDetail(state.userDetail.id) + await loadUserList() + } else { + throw new Error(res.message || '解除失败') + } + } catch (error) { + const message = error instanceof Error ? error.message : '解除失败' + ElMessage.error(message) + } +} + +async function handleBindParent(row: UserItem) { + state.bindFormData = { + userId: row.id, + userNickname: row.nickname, + userUid: row.uid, + userLevel: row.userLevel, + currentParentNickname: '', + currentParentUid: '', + parentUid: '' + } + + // 获取用户详情以了解当前上级 + try { + const res = await getUserDetail(row.id) + if (res.code === 0 && res.data) { + state.bindFormData.currentParentNickname = res.data.parentUserNickname || '' + state.bindFormData.currentParentUid = res.data.parentUserUid || '' + state.bindFormData.userLevel = res.data.userLevel + } + } catch { + // ignore + } + + state.bindDialogVisible = true +} + +async function handleBindSubmit() { + if (!bindFormRef.value) return + try { + await bindFormRef.value.validate() + } catch { + return + } + + // 先通过 UID 查找渠道商用户 + state.bindFormLoading = true + try { + const searchRes = await getUserList({ + page: 1, + pageSize: 1, + uid: state.bindFormData.parentUid, + userLevel: USER_LEVEL.CHANNEL + }) + + if (searchRes.code !== 0 || !searchRes.data?.list?.length) { + ElMessage.error('未找到该UID对应的渠道商用户,请确认UID是否正确') + return + } + + const parentUser = searchRes.data.list[0] + + await ElMessageBox.confirm( + `确认将 "${state.bindFormData.userNickname}" 绑定到渠道商 "${parentUser.nickname}"(${parentUser.uid})下吗?`, + '绑定确认', + { confirmButtonText: '确认绑定', cancelButtonText: '取消', type: 'warning' } + ) + + const res = await bindParentUser({ + userId: state.bindFormData.userId, + parentUserId: parentUser.id + }) + + if (res.code === 0) { + ElMessage.success('绑定上下级关系成功') + state.bindDialogVisible = false + await loadUserList() + } else { + throw new Error(res.message || '绑定失败') + } + } catch (error) { + if (error === 'cancel' || (error instanceof Object && 'action' in error)) return + const message = error instanceof Error ? error.message : '绑定失败' + ElMessage.error(message) + } finally { + state.bindFormLoading = false + } +} + +async function handleUnbindParent() { + try { + await ElMessageBox.confirm( + `确认解除 "${state.bindFormData.userNickname}" 与上级 "${state.bindFormData.currentParentNickname}" 的关系吗?`, + '解除确认', + { confirmButtonText: '确认解除', cancelButtonText: '取消', type: 'warning' } + ) + } catch { + return + } + + state.bindFormLoading = true + try { + const res = await unbindParentUser(state.bindFormData.userId) + if (res.code === 0) { + ElMessage.success('解除上下级关系成功') + state.bindDialogVisible = false + await loadUserList() + } else { + throw new Error(res.message || '解除失败') + } + } catch (error) { + const message = error instanceof Error ? error.message : '解除失败' + ElMessage.error(message) + } finally { + state.bindFormLoading = false + } +} + async function handleExport() { state.exportLoading = true try { @@ -1007,4 +1287,12 @@ onMounted(() => { :deep(.el-descriptions) { --el-descriptions-item-bordered-label-background: var(--bg-light, #f5f7fa); } + +.bind-info { + margin-bottom: 8px; +} + +.no-parent { + color: var(--text-secondary, #909399); +}