2026-09-02 14:54:54 +08:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"bj_power_wms/ent"
|
2026-09-03 14:20:37 +08:00
|
|
|
userrole "bj_power_wms/ent/role"
|
2026-09-02 14:54:54 +08:00
|
|
|
"bj_power_wms/ent/user"
|
|
|
|
|
"bj_power_wms/internal/svc"
|
|
|
|
|
|
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 密码最小长度(注册 / 改密 / 重置统一)
|
|
|
|
|
const minPasswordLen = 6
|
|
|
|
|
|
2026-09-03 14:20:37 +08:00
|
|
|
// legacyRolePermissions 旧的角色 → 权限码映射(已废弃)。
|
|
|
|
|
// 保留此常量的价值:作为「用户 role_id 为空」时的语义兜底,但此兜底直接查 roles 表
|
|
|
|
|
// (按 role 字符串匹配 code),不再依赖这段硬编码数组。本常量仅作注释参照,不再被逻辑引用。
|
|
|
|
|
|
|
|
|
|
// permissionsForRole 按角色字符串 from roles 表取权限码(数据兜底,非硬编码)。
|
|
|
|
|
// 用于 admin/operator/inspector 这类内置角色名,即使某用户未绑定 role_id 也能从数据库取到权限。
|
|
|
|
|
func permissionsForRole(ctx *svc.ServiceContext, role string) []string {
|
|
|
|
|
rl, err := ctx.EntClient.Role.Query().Where(userrole.CodeEQ(role)).Only(ctx0())
|
|
|
|
|
if err != nil || len(rl.PermissionCodes) == 0 {
|
|
|
|
|
return []string{}
|
|
|
|
|
}
|
|
|
|
|
return rl.PermissionCodes
|
2026-09-02 14:54:54 +08:00
|
|
|
}
|
|
|
|
|
|
2026-09-03 14:20:37 +08:00
|
|
|
// permissionsForUser 取用户权限码:
|
|
|
|
|
// 1. 有 role_id → 直接读该角色的 permission_codes
|
|
|
|
|
// 2. 无 role_id(历史账号) → 按 role 字符串从 roles 表查内置角色兜底
|
|
|
|
|
// 纯数据来源,不含任何硬编码权限映射。
|
|
|
|
|
func permissionsForUser(ctx *svc.ServiceContext, u *ent.User) []string {
|
|
|
|
|
if u.RoleID > 0 {
|
|
|
|
|
rl, err := ctx.EntClient.Role.Get(ctx0(), u.RoleID)
|
|
|
|
|
if err == nil && len(rl.PermissionCodes) > 0 {
|
|
|
|
|
return rl.PermissionCodes
|
|
|
|
|
}
|
2026-09-02 14:54:54 +08:00
|
|
|
}
|
2026-09-03 14:20:37 +08:00
|
|
|
return permissionsForRole(ctx, u.Role)
|
2026-09-02 14:54:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-03 18:13:59 +08:00
|
|
|
// requirePerm 按权限码判定:当前用户拥有指定 permissionCode 才放行。
|
|
|
|
|
// 用于替代 requireAdmin,使管理员也受自身角色权限码约束(精细控制)。
|
|
|
|
|
// 支持通配 *(拥有 * 视为全部放行)。seed 类系统引导接口仍用 requireAdmin。
|
|
|
|
|
func requirePerm(ctx *svc.ServiceContext, code string, next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
username := strings.TrimSpace(r.Header.Get("X-Username"))
|
|
|
|
|
if username == "" {
|
|
|
|
|
fail(w, http.StatusUnauthorized, "未登录")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
u, err := ctx.EntClient.User.Query().Where(user.UsernameEQ(username)).Only(ctx0())
|
|
|
|
|
if err != nil {
|
|
|
|
|
fail(w, http.StatusUnauthorized, "用户不存在")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !hasCode(permissionsForUser(ctx, u), code) {
|
|
|
|
|
fail(w, http.StatusForbidden, "无权限: "+code)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
next(w, r)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// hasCode 判断权限码列表是否包含指定码(支持通配 *)
|
|
|
|
|
func hasCode(codes []string, code string) bool {
|
|
|
|
|
for _, c := range codes {
|
|
|
|
|
if c == code || c == "*" {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-02 14:54:54 +08:00
|
|
|
// 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,
|
2026-09-03 14:20:37 +08:00
|
|
|
"roleId": u.RoleID,
|
2026-09-02 14:54:54 +08:00
|
|
|
"dept": u.Dept,
|
|
|
|
|
"isActive": u.IsActive,
|
2026-09-03 14:20:37 +08:00
|
|
|
"permissionCodes": permissionsForUser(ctx, u),
|
2026-09-02 14:54:54 +08:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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,
|
2026-09-03 14:20:37 +08:00
|
|
|
"roleId": u.RoleID,
|
2026-09-02 14:54:54 +08:00
|
|
|
"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 {
|
2026-09-03 18:13:59 +08:00
|
|
|
return requirePerm(ctx, "user:manage", func(w http.ResponseWriter, r *http.Request) {
|
2026-09-02 14:54:54 +08:00
|
|
|
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 {
|
2026-09-03 18:13:59 +08:00
|
|
|
return requirePerm(ctx, "user:create", func(w http.ResponseWriter, r *http.Request) {
|
2026-09-02 14:54:54 +08:00
|
|
|
var req struct {
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
Password string `json:"password"`
|
|
|
|
|
RealName string `json:"realName"`
|
|
|
|
|
Role string `json:"role"`
|
2026-09-03 14:20:37 +08:00
|
|
|
RoleID int `json:"roleId"`
|
2026-09-02 14:54:54 +08:00
|
|
|
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)
|
2026-09-03 14:20:37 +08:00
|
|
|
// role 字符串与 role_id 用同一角色语义:优先 role_id(数据化),否则按字符串回退。
|
|
|
|
|
// 若用户给的是 role_code,尝试从 roles 表反查 role_id。
|
|
|
|
|
roleID := req.RoleID
|
|
|
|
|
if roleID == 0 {
|
|
|
|
|
if rl, err := ctx.EntClient.Role.Query().Where(userrole.CodeEQ(role)).Only(ctx0()); err == nil {
|
|
|
|
|
roleID = rl.ID
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-02 14:54:54 +08:00
|
|
|
u, err := ctx.EntClient.User.Create().
|
|
|
|
|
SetUsername(req.Username).
|
|
|
|
|
SetPassword(string(hash)).
|
|
|
|
|
SetNillableRealName(strPtr(req.RealName)).
|
|
|
|
|
SetRole(role).
|
2026-09-03 14:20:37 +08:00
|
|
|
SetRoleID(roleID).
|
2026-09-02 14:54:54 +08:00
|
|
|
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 {
|
2026-09-03 18:13:59 +08:00
|
|
|
return requirePerm(ctx, "user:edit", func(w http.ResponseWriter, r *http.Request) {
|
2026-09-02 14:54:54 +08:00
|
|
|
var req struct {
|
|
|
|
|
ID int `json:"id"`
|
|
|
|
|
RealName string `json:"realName"`
|
|
|
|
|
Role string `json:"role"`
|
2026-09-03 14:20:37 +08:00
|
|
|
RoleID int `json:"roleId"`
|
2026-09-02 14:54:54 +08:00
|
|
|
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
|
|
|
|
|
}
|
2026-09-03 18:13:59 +08:00
|
|
|
// 内置管理员账号(username=admin)不允许通过管理接口修改(含禁用/改角色/改密码等)
|
|
|
|
|
if target.Username == "admin" {
|
|
|
|
|
fail(w, http.StatusBadRequest, "系统内置管理员账号不允许修改")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-09-02 14:54:54 +08:00
|
|
|
upd := target.Update()
|
|
|
|
|
if req.RealName != "" {
|
|
|
|
|
upd.SetRealName(req.RealName)
|
|
|
|
|
}
|
|
|
|
|
if req.Role != "" {
|
|
|
|
|
upd.SetRole(roleOrDefault(req.Role))
|
|
|
|
|
}
|
2026-09-03 14:20:37 +08:00
|
|
|
if req.RoleID > 0 {
|
|
|
|
|
upd.SetRoleID(req.RoleID)
|
|
|
|
|
}
|
2026-09-02 14:54:54 +08:00
|
|
|
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 {
|
2026-09-03 18:13:59 +08:00
|
|
|
return requirePerm(ctx, "user:delete", func(w http.ResponseWriter, r *http.Request) {
|
2026-09-02 14:54:54 +08:00
|
|
|
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
|
|
|
|
|
}
|
2026-09-03 18:13:59 +08:00
|
|
|
// 内置管理员账号(username=admin)不允许删除
|
|
|
|
|
if target.Username == "admin" {
|
|
|
|
|
fail(w, http.StatusBadRequest, "系统内置管理员账号不允许删除")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-09-02 14:54:54 +08:00
|
|
|
// 禁止删除最后一个管理员
|
|
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
}
|