Skip to content

Commit c69b413

Browse files
committed
feat: 用户管理 - 多用户CRUD+重置密码+修改我的密码(需求15)
1 parent a5b07fd commit c69b413

5 files changed

Lines changed: 296 additions & 3 deletions

File tree

Juggle.Api/Controllers/Api/UserController.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
using Juggle.Domain.Entities;
12
using Juggle.Infrastructure.Common;
23
using Juggle.Infrastructure.Persistence;
34
using Juggle.Application.Models.Request;
45
using Juggle.Application.Models.Response;
56
using Juggle.Application.Services.Impl;
7+
using Microsoft.AspNetCore.Authorization;
68
using Microsoft.AspNetCore.Mvc;
79
using Microsoft.EntityFrameworkCore;
810

@@ -35,4 +37,85 @@ public async Task<ApiResult> Login([FromBody] LoginRequest req)
3537
var tokenStr = _jwtService.GenerateToken(user);
3638
return ApiResult.Success(new { token = tokenStr, userName = user.UserName });
3739
}
40+
41+
/// <summary>用户分页列表(需登录)</summary>
42+
[HttpPost("page"), Authorize]
43+
public async Task<ApiResult> Page([FromBody] PageRequest req)
44+
{
45+
var query = _db.Users.Where(u => u.Deleted == 0);
46+
if (!string.IsNullOrEmpty(req.Keyword))
47+
query = query.Where(u => u.UserName!.Contains(req.Keyword));
48+
var total = await query.CountAsync();
49+
var records = await query.OrderByDescending(u => u.Id)
50+
.Skip((req.PageNum - 1) * req.PageSize)
51+
.Take(req.PageSize)
52+
.Select(u => new { u.Id, u.UserName, u.CreatedAt, u.UpdatedAt })
53+
.ToListAsync();
54+
return ApiResult.Success(new { total, records });
55+
}
56+
57+
/// <summary>新增用户</summary>
58+
[HttpPost("add"), Authorize]
59+
public async Task<ApiResult> Add([FromBody] UserAddRequest req)
60+
{
61+
if (string.IsNullOrWhiteSpace(req.UserName))
62+
return ApiResult.Fail("用户名不能为空");
63+
if (string.IsNullOrWhiteSpace(req.Password))
64+
return ApiResult.Fail("密码不能为空");
65+
var exists = await _db.Users.AnyAsync(u => u.UserName == req.UserName && u.Deleted == 0);
66+
if (exists) return ApiResult.Fail("用户名已存在");
67+
68+
var user = new UserEntity
69+
{
70+
UserName = req.UserName,
71+
Password = Md5Helper.Encrypt(req.Password),
72+
Deleted = 0,
73+
CreatedAt = DateTime.Now.ToString("o")
74+
};
75+
_db.Users.Add(user);
76+
await _db.SaveChangesAsync();
77+
return ApiResult.Success(new { id = user.Id });
78+
}
79+
80+
/// <summary>重置密码</summary>
81+
[HttpPut("resetPwd"), Authorize]
82+
public async Task<ApiResult> ResetPwd([FromBody] UserResetPwdRequest req)
83+
{
84+
var user = await _db.Users.FindAsync(req.Id);
85+
if (user == null || user.Deleted == 1) return ApiResult.Fail("用户不存在");
86+
if (string.IsNullOrWhiteSpace(req.NewPassword))
87+
return ApiResult.Fail("新密码不能为空");
88+
user.Password = Md5Helper.Encrypt(req.NewPassword);
89+
user.UpdatedAt = DateTime.Now.ToString("o");
90+
await _db.SaveChangesAsync();
91+
return ApiResult.Success();
92+
}
93+
94+
/// <summary>修改当前用户密码(需验证旧密码)</summary>
95+
[HttpPut("changePwd"), Authorize]
96+
public async Task<ApiResult> ChangePwd([FromBody] UserChangePwdRequest req)
97+
{
98+
var userName = User.Identity?.Name;
99+
var user = await _db.Users.FirstOrDefaultAsync(u => u.UserName == userName && u.Deleted == 0);
100+
if (user == null) return ApiResult.Fail("用户不存在");
101+
if (user.Password != Md5Helper.Encrypt(req.OldPassword))
102+
return ApiResult.Fail("原密码不正确");
103+
user.Password = Md5Helper.Encrypt(req.NewPassword);
104+
user.UpdatedAt = DateTime.Now.ToString("o");
105+
await _db.SaveChangesAsync();
106+
return ApiResult.Success();
107+
}
108+
109+
/// <summary>删除用户(不能删除 id=1 的初始管理员)</summary>
110+
[HttpDelete("delete/{id}"), Authorize]
111+
public async Task<ApiResult> Delete(long id)
112+
{
113+
if (id == 1) return ApiResult.Fail("不能删除初始管理员账号");
114+
var user = await _db.Users.FindAsync(id);
115+
if (user == null || user.Deleted == 1) return ApiResult.Fail("用户不存在");
116+
user.Deleted = 1;
117+
user.UpdatedAt = DateTime.Now.ToString("o");
118+
await _db.SaveChangesAsync();
119+
return ApiResult.Success();
120+
}
38121
}

Juggle.Application/Models/Request/Requests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,49 @@ public class PageRequest
44
{
55
public int PageNum { get; set; } = 1;
66
public int PageSize { get; set; } = 10;
7+
public string? Keyword { get; set; }
8+
}
9+
10+
// User Management
11+
public class UserAddRequest
12+
{
13+
public string UserName { get; set; } = "";
14+
public string Password { get; set; } = "";
15+
}
16+
17+
public class UserResetPwdRequest
18+
{
19+
public long Id { get; set; }
20+
public string NewPassword { get; set; } = "";
21+
}
22+
23+
public class UserChangePwdRequest
24+
{
25+
public string OldPassword { get; set; } = "";
26+
public string NewPassword { get; set; } = "";
27+
}
28+
29+
// SystemConfig
30+
public class SystemConfigSaveRequest
31+
{
32+
public string ConfigKey { get; set; } = "";
33+
public string? ConfigValue { get; set; }
34+
}
35+
36+
// FlowTestCase
37+
public class FlowTestCaseSaveRequest
38+
{
39+
public long? Id { get; set; }
40+
public string FlowKey { get; set; } = "";
41+
public string CaseName { get; set; } = "";
42+
public string? InputJson { get; set; }
43+
public string? AssertJson { get; set; }
44+
public string? Remark { get; set; }
45+
}
46+
47+
public class FlowTestCasePageRequest : PageRequest
48+
{
49+
public string? FlowKey { get; set; }
750
}
851

952
public class LoginRequest

JuggleNet6.Frontend/src/router/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@ const routes = [
2222
{ path: 'system/token', component: () => import('../views/system/TokenList.vue') },
2323
{ path: 'system/datasource', component: () => import('../views/system/DataSourceList.vue') },
2424
{ path: 'system/static-var', component: () => import('../views/system/StaticVariable.vue') },
25-
{ path: 'system/schedule', component: () => import('../views/system/ScheduleTask.vue') },
26-
{ path: 'system/webhook', component: () => import('../views/system/WebhookList.vue') },
25+
{ path: 'system/schedule', component: () => import('../views/system/ScheduleTask.vue') },
26+
{ path: 'system/webhook', component: () => import('../views/system/WebhookList.vue') },
27+
{ path: 'system/users', component: () => import('../views/system/UserManage.vue') },
28+
{ path: 'system/config', component: () => import('../views/system/SystemConfig.vue') },
2729
{ path: 'flow/log', component: () => import('../views/flow/FlowLog.vue') },
28-
{ path: 'flow/async-result', component: () => import('../views/flow/AsyncFlowResult.vue') }
30+
{ path: 'flow/async-result', component: () => import('../views/flow/AsyncFlowResult.vue') },
31+
{ path: 'flow/testcase', component: () => import('../views/flow/FlowTestCase.vue') }
2932
]
3033
},
3134
{

JuggleNet6.Frontend/src/views/Layout.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<el-menu-item index="/flow/dashboard">监控仪表盘</el-menu-item>
1717
<el-menu-item index="/flow/list">流程列表</el-menu-item>
1818
<el-menu-item index="/flow/log">执行日志</el-menu-item>
19+
<el-menu-item index="/flow/testcase">测试用例</el-menu-item>
1920
<el-menu-item index="/flow/async-result">异步结果查询</el-menu-item>
2021
</el-sub-menu>
2122
<el-sub-menu index="suite">
@@ -39,6 +40,8 @@
3940
<el-menu-item index="/system/static-var">静态变量</el-menu-item>
4041
<el-menu-item index="/system/schedule">定时任务</el-menu-item>
4142
<el-menu-item index="/system/webhook">Webhook 管理</el-menu-item>
43+
<el-menu-item index="/system/users">用户管理</el-menu-item>
44+
<el-menu-item index="/system/config">系统配置</el-menu-item>
4245
</el-sub-menu>
4346
</el-menu>
4447
</el-aside>
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<template>
2+
<div class="page-container">
3+
<div class="page-header">
4+
<h2>用户管理</h2>
5+
<el-button type="primary" icon="Plus" @click="openAdd">新增用户</el-button>
6+
</div>
7+
8+
<el-card>
9+
<el-table :data="tableData" stripe v-loading="loading">
10+
<el-table-column prop="id" label="ID" width="80" />
11+
<el-table-column prop="userName" label="用户名" />
12+
<el-table-column prop="createdAt" label="创建时间" width="200" show-overflow-tooltip />
13+
<el-table-column prop="updatedAt" label="更新时间" width="200" show-overflow-tooltip />
14+
<el-table-column label="操作" width="200" fixed="right">
15+
<template #default="{ row }">
16+
<el-button size="small" link @click="openReset(row)">重置密码</el-button>
17+
<el-button size="small" type="danger" link @click="doDelete(row)"
18+
:disabled="row.id === 1">删除</el-button>
19+
</template>
20+
</el-table-column>
21+
</el-table>
22+
<el-pagination v-model:current-page="page.num" v-model:page-size="page.size"
23+
:total="page.total" layout="total,prev,pager,next" style="margin-top:16px;justify-content:flex-end"
24+
@current-change="loadData" />
25+
</el-card>
26+
27+
<!-- 修改我的密码 -->
28+
<el-card style="margin-top:16px">
29+
<template #header><span>修改我的密码</span></template>
30+
<el-form :model="changePwdForm" label-width="100px" style="max-width:400px">
31+
<el-form-item label="原密码">
32+
<el-input v-model="changePwdForm.oldPassword" type="password" show-password />
33+
</el-form-item>
34+
<el-form-item label="新密码">
35+
<el-input v-model="changePwdForm.newPassword" type="password" show-password />
36+
</el-form-item>
37+
<el-form-item label="确认新密码">
38+
<el-input v-model="changePwdForm.confirmPassword" type="password" show-password />
39+
</el-form-item>
40+
<el-form-item>
41+
<el-button type="primary" @click="doChangePwd">确认修改</el-button>
42+
</el-form-item>
43+
</el-form>
44+
</el-card>
45+
46+
<!-- 新增用户弹窗 -->
47+
<el-dialog v-model="addVisible" title="新增用户" width="400px">
48+
<el-form :model="addForm" label-width="80px">
49+
<el-form-item label="用户名">
50+
<el-input v-model="addForm.userName" />
51+
</el-form-item>
52+
<el-form-item label="密码">
53+
<el-input v-model="addForm.password" type="password" show-password />
54+
</el-form-item>
55+
</el-form>
56+
<template #footer>
57+
<el-button @click="addVisible = false">取消</el-button>
58+
<el-button type="primary" @click="doAdd">确认</el-button>
59+
</template>
60+
</el-dialog>
61+
62+
<!-- 重置密码弹窗 -->
63+
<el-dialog v-model="resetVisible" :title="`重置密码:${resetForm.userName}`" width="400px">
64+
<el-form :model="resetForm" label-width="80px">
65+
<el-form-item label="新密码">
66+
<el-input v-model="resetForm.newPassword" type="password" show-password />
67+
</el-form-item>
68+
</el-form>
69+
<template #footer>
70+
<el-button @click="resetVisible = false">取消</el-button>
71+
<el-button type="primary" @click="doReset">确认重置</el-button>
72+
</template>
73+
</el-dialog>
74+
</div>
75+
</template>
76+
77+
<script setup lang="ts">
78+
import { ref, reactive, onMounted } from 'vue'
79+
import { ElMessage, ElMessageBox } from 'element-plus'
80+
import request from '../../utils/request'
81+
82+
const loading = ref(false)
83+
const tableData = ref<any[]>([])
84+
const page = reactive({ num: 1, size: 10, total: 0 })
85+
const addVisible = ref(false)
86+
const resetVisible = ref(false)
87+
const addForm = reactive({ userName: '', password: '' })
88+
const resetForm = reactive({ id: 0, userName: '', newPassword: '' })
89+
const changePwdForm = reactive({ oldPassword: '', newPassword: '', confirmPassword: '' })
90+
91+
onMounted(() => loadData())
92+
93+
async function loadData() {
94+
loading.value = true
95+
try {
96+
const res: any = await request.post('/user/page', { pageNum: page.num, pageSize: page.size })
97+
tableData.value = res.data.records
98+
page.total = res.data.total
99+
} finally { loading.value = false }
100+
}
101+
102+
function openAdd() {
103+
addForm.userName = ''
104+
addForm.password = ''
105+
addVisible.value = true
106+
}
107+
108+
async function doAdd() {
109+
if (!addForm.userName || !addForm.password) { ElMessage.warning('请填写完整'); return }
110+
await request.post('/user/add', addForm)
111+
ElMessage.success('新增成功')
112+
addVisible.value = false
113+
loadData()
114+
}
115+
116+
function openReset(row: any) {
117+
resetForm.id = row.id
118+
resetForm.userName = row.userName
119+
resetForm.newPassword = ''
120+
resetVisible.value = true
121+
}
122+
123+
async function doReset() {
124+
if (!resetForm.newPassword) { ElMessage.warning('请输入新密码'); return }
125+
await request.put('/user/resetPwd', { id: resetForm.id, newPassword: resetForm.newPassword })
126+
ElMessage.success('密码重置成功')
127+
resetVisible.value = false
128+
}
129+
130+
async function doDelete(row: any) {
131+
await ElMessageBox.confirm(`确认删除用户「${row.userName}」?`, '提示', { type: 'warning' })
132+
await request.delete(`/user/delete/${row.id}`)
133+
ElMessage.success('删除成功')
134+
loadData()
135+
}
136+
137+
async function doChangePwd() {
138+
if (!changePwdForm.oldPassword || !changePwdForm.newPassword) {
139+
ElMessage.warning('请填写完整')
140+
return
141+
}
142+
if (changePwdForm.newPassword !== changePwdForm.confirmPassword) {
143+
ElMessage.error('两次密码不一致')
144+
return
145+
}
146+
await request.put('/user/changePwd', {
147+
oldPassword: changePwdForm.oldPassword,
148+
newPassword: changePwdForm.newPassword
149+
})
150+
ElMessage.success('密码修改成功,请重新登录')
151+
changePwdForm.oldPassword = ''
152+
changePwdForm.newPassword = ''
153+
changePwdForm.confirmPassword = ''
154+
}
155+
</script>
156+
157+
<style scoped>
158+
.page-container { padding: 20px; }
159+
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
160+
.page-header h2 { font-size: 20px; color: #333; }
161+
</style>

0 commit comments

Comments
 (0)