feat: 新增账号管理功能,完善权限控制与用户信息同步

1.  新增用户管理后台接口与前端页面,支持管理员增删改查用户账号
2.  新增RBAC权限控制系统,按角色分配菜单与操作权限
3.  优化登录响应与用户信息接口,返回完整权限码与用户数据
4.  新增密码安全校验,提示纯数字密码风险
5.  移除废弃的注册接口,统一用户管理逻辑
6.  更新前端路由与侧边栏权限过滤逻辑
This commit is contained in:
SunYF
2026-09-02 14:54:54 +08:00
parent 8694162d94
commit 665ff257dc
9 changed files with 517 additions and 74 deletions
+4 -54
View File
@@ -67,64 +67,14 @@ func loginHandler(ctx *svc.ServiceContext) http.HandlerFunc {
ok(w, map[string]any{
"token": token,
"expireAt": expireAt,
"user": map[string]any{"id": u.ID, "username": u.Username, "realName": u.RealName, "role": u.Role},
"user": map[string]any{
"id": u.ID, "username": u.Username, "realName": u.RealName,
"role": u.Role, "dept": u.Dept, "permissionCodes": permissionsForRole(u.Role),
},
})
}
}
func registerHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
RealName string `json:"realName"`
Role string `json:"role"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
return
}
if req.Username == "" || req.Password == "" {
fail(w, http.StatusBadRequest, "用户名和密码必填")
return
}
if len(req.Password) < 6 {
fail(w, http.StatusBadRequest, "密码至少 6 位")
return
}
exists, _ := ctx.EntClient.User.Query().Where(user.UsernameEQ(req.Username)).Exist(ctx0())
if exists {
fail(w, http.StatusConflict, "用户名已存在")
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
u, err := ctx.EntClient.User.Create().
SetUsername(req.Username).
SetPassword(string(hash)).
SetNillableRealName(strPtr(req.RealName)).
SetRole(roleOrDefault(req.Role)).
Save(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
ok(w, map[string]any{"id": u.ID, "username": u.Username})
}
}
func roleOrDefault(r string) string {
switch r {
case "admin", "operator", "inspector":
return r
default:
return "operator"
}
}
// changePasswordHandler POST /api/user/change-password(需登录 JWT
// 当前登录用户修改自己的密码;登录人由中间件写入的 X-Username 头确定。
func changePasswordHandler(ctx *svc.ServiceContext) http.HandlerFunc {
+3
View File
@@ -3,6 +3,7 @@ package handler
import (
"errors"
"net/http"
"strconv"
"strings"
"time"
@@ -104,6 +105,8 @@ func authInterceptor(ctx *svc.ServiceContext) func(next http.HandlerFunc) http.H
}
renewIfNearExpiry(ctx, w, claims)
r.Header.Set("X-Username", claims.Username)
r.Header.Set("X-Role", claims.Role)
r.Header.Set("X-UserID", strconv.Itoa(claims.UserID))
next.ServeHTTP(w, r)
})
}
+266
View File
@@ -0,0 +1,266 @@
package handler
import (
"net/http"
"strconv"
"strings"
"bj_power_wms/ent"
"bj_power_wms/ent/user"
"bj_power_wms/internal/svc"
"golang.org/x/crypto/bcrypt"
)
// 密码最小长度(注册 / 改密 / 重置统一)
const minPasswordLen = 6
// rolePermissions 角色 → 菜单/功能权限码。
// 与前端 MainLayout 的菜单 code 一一对应;admin 拥有全部权限(含 user:manage)。
var rolePermissions = map[string][]string{
"admin": {
"dashboard:view", "inbound:view", "outbound:view", "inventory:view",
"inspection:view", "stocktake:view", "semi:view", "package:view",
"ledger:view", "zone:view", "material:view", "user:manage",
},
"operator": {
"dashboard:view", "inbound:view", "outbound:view", "inventory:view",
"stocktake:view", "semi:view", "package:view",
"ledger:view", "zone:view", "material:view",
},
"inspector": {
"dashboard:view", "inspection:view", "inventory:view",
"stocktake:view", "ledger:view", "zone:view", "material:view",
},
}
// permissionsForRole 取角色对应的权限码列表(未知角色按 operator 处理)
func permissionsForRole(role string) []string {
if p, ok := rolePermissions[role]; ok {
return p
}
return rolePermissions["operator"]
}
// roleOrDefault 规范化角色值,非法值回退 operator
func roleOrDefault(r string) string {
switch r {
case "admin", "operator", "inspector":
return r
default:
return "operator"
}
}
// roleFromRequest 取当前登录用户角色(鉴权中间件已在 X-Role 写入)
func roleFromRequest(r *http.Request) string {
return strings.TrimSpace(r.Header.Get("X-Role"))
}
// uidFromRequest 取当前登录用户ID(鉴权中间件已在 X-UserID 写入)
func uidFromRequestWMS(r *http.Request) int {
n, _ := strconv.Atoi(strings.TrimSpace(r.Header.Get("X-UserID")))
return n
}
// requireAdmin 仅管理员可访问(用于账号管理类接口)
func requireAdmin(next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if roleFromRequest(r) != "admin" {
fail(w, http.StatusForbidden, "仅管理员可操作")
return
}
next(w, r)
}
}
// userInfoHandler GET /api/user/info 当前登录用户信息 + 权限码
func userInfoHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
username := r.Header.Get("X-Username")
u, err := ctx.EntClient.User.Query().Where(user.UsernameEQ(username)).Only(ctx0())
if err != nil {
fail(w, http.StatusUnauthorized, "用户不存在")
return
}
ok(w, map[string]any{
"id": u.ID,
"username": u.Username,
"realName": u.RealName,
"role": u.Role,
"dept": u.Dept,
"isActive": u.IsActive,
"permissionCodes": permissionsForRole(u.Role),
})
}
}
// userDTO 脱敏后的用户视图(不含密码哈希)
func userDTO(u *ent.User) map[string]any {
return map[string]any{
"id": u.ID,
"username": u.Username,
"realName": u.RealName,
"role": u.Role,
"dept": u.Dept,
"phone": u.Phone,
"isActive": u.IsActive,
"lastLoginAt": u.LastLoginAt,
"createdAt": u.CreatedAt,
}
}
// listUsersHandler GET /api/user/list 管理员查看全部用户
func listUsersHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
list, err := ctx.EntClient.User.Query().Order(ent.Asc("id")).All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
out := make([]map[string]any, 0, len(list))
for _, u := range list {
out = append(out, userDTO(u))
}
ok(w, map[string]any{"list": out})
})
}
// createUserHandler POST /api/user/create 管理员新建账号
func createUserHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
RealName string `json:"realName"`
Role string `json:"role"`
Dept string `json:"dept"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
return
}
if req.Username == "" || req.Password == "" {
fail(w, http.StatusBadRequest, "用户名和密码必填")
return
}
if len(req.Password) < minPasswordLen {
fail(w, http.StatusBadRequest, "密码长度至少 6 位")
return
}
exists, _ := ctx.EntClient.User.Query().Where(user.UsernameEQ(req.Username)).Exist(ctx0())
if exists {
fail(w, http.StatusConflict, "用户名已存在")
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
fail(w, http.StatusInternalServerError, "密码加密失败")
return
}
role := roleOrDefault(req.Role)
u, err := ctx.EntClient.User.Create().
SetUsername(req.Username).
SetPassword(string(hash)).
SetNillableRealName(strPtr(req.RealName)).
SetRole(role).
SetNillableDept(strPtr(req.Dept)).
Save(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
ok(w, userDTO(u))
})
}
// updateUserHandler POST /api/user/update 管理员编辑账号(可选重置密码)
func updateUserHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
var req struct {
ID int `json:"id"`
RealName string `json:"realName"`
Role string `json:"role"`
Dept string `json:"dept"`
IsActive *bool `json:"isActive"`
Password string `json:"password"`
}
if err := parseJSON(r, &req); err != nil || req.ID <= 0 {
fail(w, http.StatusBadRequest, "参数错误")
return
}
target, err := ctx.EntClient.User.Get(ctx0(), req.ID)
if err != nil {
fail(w, http.StatusNotFound, "用户不存在")
return
}
upd := target.Update()
if req.RealName != "" {
upd.SetRealName(req.RealName)
}
if req.Role != "" {
upd.SetRole(roleOrDefault(req.Role))
}
if req.Dept != "" {
upd.SetNillableDept(strPtr(req.Dept))
}
if req.IsActive != nil {
upd.SetIsActive(*req.IsActive)
}
// 可选重置密码:填写且 >=6 位才生效
if req.Password != "" {
if len(req.Password) < minPasswordLen {
fail(w, http.StatusBadRequest, "密码长度至少 6 位")
return
}
hash, e2 := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if e2 != nil {
fail(w, http.StatusInternalServerError, "密码加密失败")
return
}
upd.SetPassword(string(hash))
}
u, err := upd.Save(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
ok(w, userDTO(u))
})
}
// deleteUserHandler POST /api/user/delete 管理员删除账号
func deleteUserHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
var req struct {
ID int `json:"id"`
}
if err := parseJSON(r, &req); err != nil || req.ID <= 0 {
fail(w, http.StatusBadRequest, "参数错误")
return
}
// 禁止删除自己
if uidFromRequestWMS(r) == req.ID {
fail(w, http.StatusBadRequest, "不能删除当前登录账号")
return
}
target, err := ctx.EntClient.User.Get(ctx0(), req.ID)
if err != nil {
fail(w, http.StatusNotFound, "用户不存在")
return
}
// 禁止删除最后一个管理员
if target.Role == "admin" {
n, _ := ctx.EntClient.User.Query().Where(user.RoleEQ("admin")).Count(ctx0())
if n <= 1 {
fail(w, http.StatusBadRequest, "至少保留一个管理员账号")
return
}
}
if err := ctx.EntClient.User.DeleteOneID(req.ID).Exec(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
ok(w, nil)
})
}
+5 -1
View File
@@ -13,7 +13,6 @@ func RegisterHandlers(server *rest.Server, ctx *svc.ServiceContext) {
[]rest.Route{
{Method: http.MethodGet, Path: "/api/health", Handler: healthHandler(ctx)},
{Method: http.MethodPost, Path: "/api/auth/login", Handler: loginHandler(ctx)},
{Method: http.MethodPost, Path: "/api/auth/register", Handler: registerHandler(ctx)},
},
)
@@ -23,6 +22,11 @@ func RegisterHandlers(server *rest.Server, ctx *svc.ServiceContext) {
server.AddRoutes(
[]rest.Route{
{Method: http.MethodPost, Path: "/api/user/change-password", Handler: changePasswordHandler(ctx)},
{Method: http.MethodGet, Path: "/api/user/info", Handler: userInfoHandler(ctx)},
{Method: http.MethodGet, Path: "/api/user/list", Handler: listUsersHandler(ctx)},
{Method: http.MethodPost, Path: "/api/user/create", Handler: createUserHandler(ctx)},
{Method: http.MethodPost, Path: "/api/user/update", Handler: updateUserHandler(ctx)},
{Method: http.MethodPost, Path: "/api/user/delete", Handler: deleteUserHandler(ctx)},
},
)