升级
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
18631081161 2026-03-28 20:36:11 +08:00
parent 262de67a8e
commit 8ae6dcfa88
7 changed files with 561 additions and 7 deletions

View File

@ -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
}
}
/// <summary>
/// 绑定上下级关系(渠道商-合伙人)
/// </summary>
/// <param name="request">绑定请求</param>
/// <returns>操作结果</returns>
[HttpPost("bindParent")]
[BusinessPermission("user:update")]
public async Task<IActionResult> 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);
}
}
/// <summary>
/// 解除上下级关系
/// </summary>
/// <param name="request">请求包含用户ID</param>
/// <returns>操作结果</returns>
[HttpPost("unbindParent")]
[BusinessPermission("user:update")]
public async Task<IActionResult> 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);
}
}
/// <summary>
/// 导出用户列表
/// </summary>

View File

@ -182,6 +182,11 @@ public static class ErrorCodes
/// </summary>
public const int UserNotFound = 3301;
/// <summary>
/// 上级用户不存在
/// </summary>
public const int ParentUserNotFound = 3302;
#endregion
#region (3400-3499)

View File

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
namespace MiAssessment.Admin.Business.Models.User;
/// <summary>
/// 绑定上下级关系请求
/// </summary>
public class BindParentUserRequest
{
/// <summary>
/// 下级用户ID合伙人
/// </summary>
[Required]
public long UserId { get; set; }
/// <summary>
/// 上级用户ID渠道商
/// </summary>
[Required]
public long ParentUserId { get; set; }
}

View File

@ -58,6 +58,21 @@ public interface IUserBusinessService
/// <returns>导出数据</returns>
Task<List<UserDto>> ExportUsersAsync(UserQueryRequest request);
/// <summary>
/// 绑定上下级关系(上级必须是渠道商,下级必须是合伙人)
/// </summary>
/// <param name="userId">下级用户ID合伙人</param>
/// <param name="parentUserId">上级用户ID渠道商</param>
/// <returns>是否成功</returns>
Task<bool> BindParentUserAsync(long userId, long parentUserId);
/// <summary>
/// 解除上下级关系
/// </summary>
/// <param name="userId">用户ID</param>
/// <returns>是否成功</returns>
Task<bool> UnbindParentUserAsync(long userId);
/// <summary>
/// 删除用户(硬删除,同时清除登录记录和令牌)
/// </summary>

View File

@ -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;
}
/// <summary>
/// 判断等级变更时是否需要解除与上级的关系
/// 规则:
/// - 提升为渠道商(3):一律解除上下级关系
/// - 提升为合伙人(2):仅当上级是渠道商(3)时保留,否则解除
/// - 变为普通用户(1):保留原有关系不变
/// </summary>
private async Task<bool> 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;
}
/// <inheritdoc />
public async Task<bool> 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;
}
/// <inheritdoc />
public async Task<bool> 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;
}

View File

@ -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<ApiResponse<Blob>> {
})
}
/**
* -
* @param data
* @returns
*/
export function bindParentUser(data: BindParentUserRequest): Promise<ApiResponse<boolean>> {
return request<boolean>({
url: '/admin/user/bindParent',
method: 'post',
data
})
}
/**
*
* @param id ID
* @returns
*/
export function unbindParentUser(id: number): Promise<ApiResponse<boolean>> {
return request<boolean>({
url: '/admin/user/unbindParent',
method: 'post',
data: { id }
})
}
/**
*
* @param id ID

View File

@ -161,7 +161,7 @@
</el-table-column>
<!-- 操作 -->
<el-table-column label="操作" width="250" fixed="right" align="center">
<el-table-column label="操作" width="320" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleViewDetail(row)">
<el-icon><View /></el-icon>
@ -175,6 +175,10 @@
<el-icon><Edit /></el-icon>
等级
</el-button>
<el-button type="success" link size="small" @click="handleBindParent(row)">
<el-icon><Link /></el-icon>
上级
</el-button>
<el-button type="danger" link size="small" @click="handleDelete(row)">
<el-icon><Delete /></el-icon>
删除
@ -246,6 +250,24 @@
{{ state.userDetail.parentUserNickname || '-' }}
</el-descriptions-item>
</el-descriptions>
<div style="margin-top: 8px;">
<el-button
v-if="state.userDetail.parentUserId"
type="danger"
size="small"
@click="handleDetailUnbind"
>
解除上级关系
</el-button>
<el-button
v-else-if="state.userDetail.userLevel === USER_LEVEL.PARTNER"
type="primary"
size="small"
@click="handleBindParent(state.userDetail)"
>
绑定上级渠道商
</el-button>
</div>
</div>
<!-- 财务信息 -->
@ -348,7 +370,7 @@
<el-dialog
v-model="state.levelDialogVisible"
title="修改用户等级"
width="400px"
width="460px"
:close-on-click-modal="false"
>
<el-form
@ -371,6 +393,14 @@
/>
</el-form-item>
</el-form>
<el-alert
v-if="levelChangeWarning"
:title="levelChangeWarning"
type="warning"
show-icon
:closable="false"
style="margin-top: 8px;"
/>
<template #footer>
<el-button @click="state.levelDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="state.levelFormLoading" @click="handleLevelSubmit">
@ -378,6 +408,74 @@
</el-button>
</template>
</el-dialog>
<!-- 绑定上级对话框 -->
<el-dialog
v-model="state.bindDialogVisible"
title="管理上级关系"
width="500px"
:close-on-click-modal="false"
destroy-on-close
>
<div class="bind-info">
<el-descriptions :column="1" border>
<el-descriptions-item label="当前用户">
{{ state.bindFormData.userNickname }}{{ state.bindFormData.userUid }}
</el-descriptions-item>
<el-descriptions-item label="用户等级">
<el-tag :type="getLevelTagType(state.bindFormData.userLevel)">
{{ getLevelName(state.bindFormData.userLevel) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="当前上级">
<template v-if="state.bindFormData.currentParentNickname">
{{ state.bindFormData.currentParentNickname }}{{ state.bindFormData.currentParentUid }}
<el-button type="danger" link size="small" @click="handleUnbindParent" :loading="state.bindFormLoading" style="margin-left: 8px;">
解除关系
</el-button>
</template>
<span v-else class="no-parent">暂无上级</span>
</el-descriptions-item>
</el-descriptions>
</div>
<el-divider />
<template v-if="!state.bindFormData.currentParentNickname">
<el-alert
v-if="state.bindFormData.userLevel !== USER_LEVEL.PARTNER"
title="只有合伙人才能绑定上级渠道商"
type="info"
show-icon
:closable="false"
style="margin-bottom: 16px;"
/>
<el-form
v-else
ref="bindFormRef"
:model="state.bindFormData"
:rules="bindFormRules"
label-width="100px"
>
<el-form-item label="上级渠道商" prop="parentUid">
<el-input
v-model="state.bindFormData.parentUid"
placeholder="请输入渠道商的UID"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="state.bindFormLoading" @click="handleBindSubmit">
确认绑定
</el-button>
</el-form-item>
</el-form>
</template>
<template #footer>
<el-button @click="state.bindDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
@ -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<InstanceType<typeof import('element-plus')['ElTable']>>()
const levelFormRef = ref<FormInstance>()
const bindFormRef = ref<FormInstance>()
const dateRange = ref<[string, string] | null>(null)
// ============ State ============
@ -527,7 +641,18 @@ const state = reactive<UserPageState>({
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);
}
</style>