feat: 完成多模块功能迭代与优化

本次提交包含多模块功能更新:
1. 权限系统重构:移除硬编码admin放行逻辑,新增精细化权限控制,完善权限树形结构与用户/角色保护
2. 新增物料导入导出接口,补充台账查询条件
3. 优化列表查询排序逻辑,新增区域编码大小写不敏感搜索
4. 修复帮助抽屉重复弹出问题,补充页面权限控制
5. 新增弱密码提示逻辑,优化MES/WMS用户/角色管理权限
6. 调整路由结构,新增权限校验脚本与前端权限树重构
7. 补充数据库字段与依赖包更新
This commit is contained in:
SunYF
2026-09-03 18:13:59 +08:00
parent 572fa975bc
commit c1fc6d32b2
47 changed files with 1761 additions and 926 deletions
+109 -37
View File
@@ -2,6 +2,7 @@ package handler
import (
"net/http"
"strings"
"bj_power_wms/ent"
"bj_power_wms/ent/permission"
@@ -20,42 +21,51 @@ type seedPerm struct {
Path string
Icon string
Sort int
// Parent 按钮所属菜单编码(BUTTON 填其所属 MENU code;导出等按钮已按菜单拆分,不再用全局 *)
Parent string
}
var seedPermissions = []seedPerm{
// ===== 菜单权限 =====
{"dashboard:view", "工作台", "MENU", "/", "HomeFilled", 10},
{"inbound:view", "入库管理", "MENU", "/inbound", "Download", 20},
{"outbound:view", "出库管理", "MENU", "/outbound", "Upload", 30},
{"inventory:view", "库存查询", "MENU", "/inventory", "Search", 40},
{"inspection:view", "质量检验", "MENU", "/inspection", "CircleCheck", 50},
{"stocktake:view", "库存盘点", "MENU", "/stocktake", "List", 60},
{"semi:view", "半成品/成品", "MENU", "/semi", "Box", 70},
{"ledger:view", "备料台账", "MENU", "/ledger", "Tickets", 80},
{"zone:view", "区域维护", "MENU", "/zone", "Location", 90},
{"material:view", "物料档案", "MENU", "/material", "Goods", 100},
{"user:manage", "账号管理", "MENU", "/users", "EditPen", 110},
{"role:manage", "角色管理", "MENU", "/roles", "Key", 120},
{"dashboard:view", "工作台", "MENU", "/", "HomeFilled", 10, ""},
{"inbound:view", "入库管理", "MENU", "/inbound", "Download", 20, ""},
{"outbound:view", "出库管理", "MENU", "/outbound", "Upload", 30, ""},
{"inventory:view", "库存查询", "MENU", "/inventory", "Search", 40, ""},
{"inspection:view", "质量检验", "MENU", "/inspection", "CircleCheck", 50, ""},
{"stocktake:view", "库存盘点", "MENU", "/stocktake", "List", 60, ""},
{"semi:view", "半成品/成品", "MENU", "/semi", "Box", 70, ""},
{"ledger:view", "备料台账", "MENU", "/ledger", "Tickets", 80, ""},
{"zone:view", "区域维护", "MENU", "/zone", "Location", 90, ""},
{"material:view", "物料档案", "MENU", "/material", "Goods", 100, ""},
{"user:manage", "账号管理", "MENU", "/users", "EditPen", 110, ""},
{"role:manage", "角色管理", "MENU", "/roles", "Key", 120, ""},
// ===== 按钮权限 =====
{"inbound:create", "批量入库", "BUTTON", "", "", 200},
{"inbound:import", "Excel导入", "BUTTON", "", "", 201},
{"outbound:create", "发起出库", "BUTTON", "", "", 210},
{"inspection:create", "录入检验", "BUTTON", "", "", 220},
{"stocktake:start", "发起盘点", "BUTTON", "", "", 230},
{"stocktake:writeback", "差异写回", "BUTTON", "", "", 231},
{"material:create", "新增物料", "BUTTON", "", "", 240},
{"material:edit", "编辑物料", "BUTTON", "", "", 241},
{"material:delete", "删除物料", "BUTTON", "", "", 242},
{"zone:create", "新增区域", "BUTTON", "", "", 250},
{"zone:edit", "编辑区域", "BUTTON", "", "", 251},
{"zone:delete", "删除区域", "BUTTON", "", "", 252},
{"user:create", "新增账号", "BUTTON", "", "", 260},
{"user:edit", "编辑账号", "BUTTON", "", "", 261},
{"user:delete", "删除账号", "BUTTON", "", "", 262},
{"role:create", "新增角色", "BUTTON", "", "", 270},
{"role:edit", "编辑角色", "BUTTON", "", "", 271},
{"role:delete", "删除角色", "BUTTON", "", "", 272},
{"*:export", "导出", "BUTTON", "", "", 280},
{"inbound:create", "批量入库", "BUTTON", "", "", 200, "inbound:view"},
{"inbound:import", "Excel导入", "BUTTON", "", "", 201, "inbound:view"},
{"inbound:export", "导出", "BUTTON", "", "", 202, "inbound:view"},
{"outbound:create", "发起出库", "BUTTON", "", "", 210, "outbound:view"},
{"outbound:export", "导出", "BUTTON", "", "", 211, "outbound:view"},
{"inspection:create", "录入检验", "BUTTON", "", "", 220, "inspection:view"},
{"inspection:export", "导出", "BUTTON", "", "", 221, "inspection:view"},
{"inventory:export", "导出", "BUTTON", "", "", 204, "inventory:view"},
{"stocktake:start", "发起盘点", "BUTTON", "", "", 230, "stocktake:view"},
{"stocktake:writeback", "差异写回", "BUTTON", "", "", 231, "stocktake:view"},
{"material:create", "新增物料", "BUTTON", "", "", 240, "material:view"},
{"material:edit", "编辑物料", "BUTTON", "", "", 241, "material:view"},
{"material:delete", "删除物料", "BUTTON", "", "", 242, "material:view"},
{"material:import", "导入", "BUTTON", "", "", 243, "material:view"},
{"material:export", "导出", "BUTTON", "", "", 244, "material:view"},
{"zone:create", "新增区域", "BUTTON", "", "", 250, "zone:view"},
{"zone:edit", "编辑区域", "BUTTON", "", "", 251, "zone:view"},
{"zone:delete", "删除区域", "BUTTON", "", "", 252, "zone:view"},
{"user:create", "新增账号", "BUTTON", "", "", 260, "user:manage"},
{"user:edit", "编辑账号", "BUTTON", "", "", 261, "user:manage"},
{"user:delete", "删除账号", "BUTTON", "", "", 262, "user:manage"},
{"role:create", "新增角色", "BUTTON", "", "", 270, "role:manage"},
{"role:edit", "编辑角色", "BUTTON", "", "", 271, "role:manage"},
{"role:delete", "删除角色", "BUTTON", "", "", 272, "role:manage"},
// 注:导出权限已按菜单拆分(inbound/outbound/inspection/inventory:export),
// 不再使用全局 *:export,以更精细地控制“谁能在哪个菜单导出”。
}
// 三角色预置权限(对齐原硬编码 rolePermissions,并补充按钮权限)
@@ -76,7 +86,8 @@ func seedRoleCodes() map[string][]string {
}
insp := []string{
"dashboard:view", "inventory:view", "inspection:view", "inspection:create",
"stocktake:view", "ledger:view", "zone:view", "material:view", "*:export",
"stocktake:view", "ledger:view", "zone:view", "material:view",
"inventory:export", "inspection:export", "material:export",
}
return map[string][]string{"admin": all, "operator": oper, "inspector": insp}
}
@@ -99,10 +110,16 @@ func seedRbacHandler(ctx *svc.ServiceContext) http.HandlerFunc {
exist, _ := ctx.EntClient.Permission.Query().
Where(permission.CodeEQ(p.Code)).Exist(ctx0())
if exist {
// 已存在:幂等补齐 parent_code(新增字段不影响已建角色权限)
_, _ = ctx.EntClient.Permission.Update().
Where(permission.CodeEQ(p.Code)).
SetParentCode(p.Parent).
Save(ctx0())
continue
}
_, err := ctx.EntClient.Permission.Create().
SetCode(p.Code).SetName(p.Name).SetType(p.Type).
SetParentCode(p.Parent).
SetPath(p.Path).SetIcon(p.Icon).SetSort(p.Sort).
Save(ctx0())
if err == nil {
@@ -134,6 +151,51 @@ func seedRbacHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
}
}
// 迁移历史角色:把“可查看的菜单”自动补上对应导出码(精细化:原全局 *:export 等价于“可见菜单均可导出”)
// 同时清理可能残留的废弃 *:export。已有角色 seed 不会覆盖,故在此补齐导出能力。
menuExport := map[string]string{}
for _, p := range seedPermissions {
if p.Type == "BUTTON" && p.Parent != "" && p.Name == "导出" {
menuExport[p.Parent] = p.Code
}
}
allRoles, _ := ctx.EntClient.Role.Query().All(ctx0())
for _, rl := range allRoles {
codes := rl.PermissionCodes
changed := false
set := map[string]bool{}
for _, c := range codes {
if c == "*:export" {
changed = true // 移除废弃全局码
continue
}
set[c] = true
}
for c := range set {
if strings.HasSuffix(c, ":view") {
// c 本身就是菜单权限码(如 inbound:view),与 menuExport 的 key 一致
if exp, ok := menuExport[c]; ok && !set[exp] {
set[exp] = true
changed = true
}
}
}
if !changed {
continue
}
newCodes := make([]string, 0, len(set))
for c := range set {
newCodes = append(newCodes, c)
}
_, _ = ctx.EntClient.Role.UpdateOneID(rl.ID).SetPermissionCodes(newCodes).Save(ctx0())
}
// 强制管理员角色始终拥有全部权限(不允许被缩减),与“admin 拥有所有权限”一致
if adminRole, aerr := ctx.EntClient.Role.Query().Where(role.CodeEQ("admin")).Only(ctx0()); aerr == nil {
_ = ctx.EntClient.Role.UpdateOneID(adminRole.ID).SetPermissionCodes(codes["admin"]).Exec(ctx0())
}
// 清理已废弃的全局 *:export 权限行(迁移后无角色引用,幂等)
_, _ = ctx.EntClient.Permission.Delete().Where(permission.CodeEQ("*:export")).Exec(ctx0())
logx.Infof("rbac seed: 新增权限 %d 条、角色 %d 个、绑定历史用户 %d 个", createdP, createdR, bound)
ok(w, map[string]any{"createdPermissions": createdP, "createdRoles": createdR, "boundUsers": bound})
})
@@ -141,19 +203,19 @@ func seedRbacHandler(ctx *svc.ServiceContext) http.HandlerFunc {
// listRolesHandler GET /api/roles 角色列表(含权限码)
func listRolesHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
return requirePerm(ctx, "role:manage", func(w http.ResponseWriter, r *http.Request) {
list, err := ctx.EntClient.Role.Query().Order(ent.Asc("id")).All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
ok(w, map[string]any{"list": list})
}
})
}
// createRoleHandler POST /api/roles 新增角色
func createRoleHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
return requirePerm(ctx, "role:create", func(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
Code string `json:"code"`
@@ -187,7 +249,7 @@ func createRoleHandler(ctx *svc.ServiceContext) http.HandlerFunc {
// updateRoleHandler POST /api/roles/update 编辑角色(含权限勾选)
func updateRoleHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
return requirePerm(ctx, "role:edit", func(w http.ResponseWriter, r *http.Request) {
var req struct {
ID int `json:"id"`
Name string `json:"name"`
@@ -198,6 +260,16 @@ func updateRoleHandler(ctx *svc.ServiceContext) http.HandlerFunc {
fail(w, http.StatusBadRequest, "参数错误")
return
}
// 内置管理员角色不允许修改其权限(admin 拥有所有权限,且不可被缩减)
target, err := ctx.EntClient.Role.Get(ctx0(), req.ID)
if err != nil {
fail(w, http.StatusNotFound, "角色不存在")
return
}
if target.Code == "admin" {
fail(w, http.StatusBadRequest, "内置管理员角色不允许修改其权限")
return
}
// 角色编码创建后不可修改(前端亦置灰),此处只改名称/备注/权限
upd := ctx.EntClient.Role.UpdateOneID(req.ID)
if req.Name != "" {
@@ -220,7 +292,7 @@ func updateRoleHandler(ctx *svc.ServiceContext) http.HandlerFunc {
// deleteRoleHandler POST /api/roles/delete 删除角色(内置三角色禁止删除)
func deleteRoleHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
return requirePerm(ctx, "role:delete", func(w http.ResponseWriter, r *http.Request) {
var req struct {
ID int `json:"id"`
}