feat: 完成多模块功能迭代与优化
本次提交包含多模块功能更新: 1. 权限系统重构:移除硬编码admin放行逻辑,新增精细化权限控制,完善权限树形结构与用户/角色保护 2. 新增物料导入导出接口,补充台账查询条件 3. 优化列表查询排序逻辑,新增区域编码大小写不敏感搜索 4. 修复帮助抽屉重复弹出问题,补充页面权限控制 5. 新增弱密码提示逻辑,优化MES/WMS用户/角色管理权限 6. 调整路由结构,新增权限校验脚本与前端权限树重构 7. 补充数据库字段与依赖包更新
This commit is contained in:
@@ -26,6 +26,20 @@ func healthHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// isWeakPassword 弱密码判定:密码为纯数字(安全性低)。
|
||||
// 登录时不拦截此类账号(业务上仍允许登录),仅在登录响应里标记,由前端提示尽快修改。
|
||||
func isWeakPassword(pwd string) bool {
|
||||
if pwd == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range pwd {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func loginHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
@@ -67,6 +81,8 @@ func loginHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
ok(w, map[string]any{
|
||||
"token": token,
|
||||
"expireAt": expireAt,
|
||||
// 弱密码(纯数字)仅提示,不影响登录
|
||||
"weakPassword": isWeakPassword(req.Password),
|
||||
"user": map[string]any{
|
||||
"id": u.ID, "username": u.Username, "realName": u.RealName,
|
||||
"role": u.Role, "roleId": u.RoleID, "dept": u.Dept,
|
||||
|
||||
@@ -299,7 +299,7 @@ func inboundDetailsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
list, err := q.Order(ent.Desc("created_at")).
|
||||
list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
@@ -334,7 +334,7 @@ func queryInboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
list, err := q.Order(ent.Desc("created_at")).
|
||||
list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
@@ -356,7 +356,7 @@ func exportInboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
q := ctx.EntClient.InboundOrder.Query()
|
||||
q = applyInboundFilters(q, r)
|
||||
list, err := q.Order(ent.Desc("created_at")).All(ctx0())
|
||||
list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
|
||||
@@ -260,7 +260,7 @@ func queryInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
list, err := q.Order(ent.Desc("created_at")).
|
||||
list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
@@ -315,7 +315,7 @@ func exportInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
list, err := q.Order(ent.Desc("created_at")).All(ctx0())
|
||||
list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"bj_power_wms/ent"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"bj_power_wms/internal/svc"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// createMaterialHandler 创建物料档案
|
||||
@@ -164,7 +166,7 @@ func queryMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
list, err := q.Order(ent.Desc("created_at")).
|
||||
list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).
|
||||
Offset((page - 1) * pageSize).
|
||||
Limit(pageSize).
|
||||
All(ctx0())
|
||||
@@ -185,7 +187,7 @@ func queryMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
// listMaterialsHandler 下拉框全量列表(不翻页)
|
||||
func listMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := ctx.EntClient.Material.Query().Order(ent.Desc("created_at")).All(ctx0())
|
||||
list, err := ctx.EntClient.Material.Query().Order(ent.Desc("created_at"), ent.Desc("id")).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -210,3 +212,198 @@ func getMaterialHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
ok(w, m)
|
||||
}
|
||||
}
|
||||
|
||||
// exportMaterialsHandler 物料档案全量导出(xlsx),按当前筛选条件导出
|
||||
func exportMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
materialCode := r.URL.Query().Get("materialCode")
|
||||
name := r.URL.Query().Get("name")
|
||||
spec := r.URL.Query().Get("spec")
|
||||
manageMode := atoi(r.URL.Query().Get("manageMode"), 0)
|
||||
|
||||
q := ctx.EntClient.Material.Query()
|
||||
if materialCode != "" {
|
||||
q = q.Where(material.CodeContains(materialCode))
|
||||
}
|
||||
if name != "" {
|
||||
q = q.Where(material.NameContains(name))
|
||||
}
|
||||
if spec != "" {
|
||||
q = q.Where(material.SpecContains(spec))
|
||||
}
|
||||
if manageMode != 0 {
|
||||
q = q.Where(material.ManageModeEQ(manageMode))
|
||||
}
|
||||
list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
type exportRow struct {
|
||||
Code string
|
||||
Name string
|
||||
ShortName string
|
||||
Spec string
|
||||
Unit string
|
||||
ManageMode int
|
||||
Description string
|
||||
CreatedAt int64
|
||||
}
|
||||
rows := make([]exportRow, 0, len(list))
|
||||
for _, m := range list {
|
||||
rows = append(rows, exportRow{
|
||||
Code: m.Code, Name: m.Name, ShortName: m.ShortName, Spec: m.Spec,
|
||||
Unit: m.Unit, ManageMode: m.ManageMode, Description: m.Description, CreatedAt: m.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("format") == "xlsx" {
|
||||
headers := []string{"物料编码", "名称", "简称", "规格", "单位", "类型", "说明", "创建时间"}
|
||||
matrix := make([][]any, 0, len(rows))
|
||||
for _, rr := range rows {
|
||||
matrix = append(matrix, []any{
|
||||
rr.Code, rr.Name, rr.ShortName, rr.Spec, rr.Unit,
|
||||
manageModeLabel(rr.ManageMode), rr.Description, unixFmt(rr.CreatedAt),
|
||||
})
|
||||
}
|
||||
sendExcel(w, xlsxFilename("物料档案"), headers, matrix)
|
||||
return
|
||||
}
|
||||
ok(w, map[string]any{"total": len(rows), "list": rows})
|
||||
}
|
||||
}
|
||||
|
||||
// excelMaterialImportHandler 物料档案 Excel 批量导入
|
||||
// 模板(首行忽略表头):物料编码 | 名称 | 简称 | 规格 | 单位 | 类型(1结构件/2精密件) | 说明
|
||||
// 强约束:全成功或全失败(一次性全导入 or 全失败,禁止部分成功)。
|
||||
// 阶段1 逐行解析+校验(编码必填且唯一 / 名称必填 / 类型合法) → 任一错则整体拒绝并列出错误行;
|
||||
// 阶段2 全部通过 → 开启单事务整体提交,任一步失败整体回滚。
|
||||
func excelMaterialImportHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseMultipartForm(20 << 20); err != nil {
|
||||
fail(w, http.StatusBadRequest, "文件上传失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
f, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "缺少 file 字段")
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
xl, err := excelize.OpenReader(f)
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "无法解析 Excel: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer xl.Close()
|
||||
sheet := xl.GetSheetList()[0]
|
||||
rows, err := xl.GetRows(sheet)
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "读取工作表失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if len(rows) <= 1 {
|
||||
fail(w, http.StatusBadRequest, "文件无数据行(首行为表头,需至少一行数据)")
|
||||
return
|
||||
}
|
||||
|
||||
type planRow struct {
|
||||
row int
|
||||
code string
|
||||
name string
|
||||
shortName string
|
||||
spec string
|
||||
unit string
|
||||
manageMode int
|
||||
description string
|
||||
}
|
||||
plans := make([]planRow, 0, len(rows)-1)
|
||||
errs := make([]importErr, 0)
|
||||
|
||||
// 阶段1:解析 + 校验(不落库)
|
||||
for i, row := range rows {
|
||||
if i == 0 {
|
||||
continue // 表头
|
||||
}
|
||||
cell := func(idx int) string {
|
||||
if idx < len(row) {
|
||||
return strings.TrimSpace(row[idx])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
pr := planRow{row: i + 1}
|
||||
pr.code = cell(0)
|
||||
pr.name = cell(1)
|
||||
pr.shortName = cell(2)
|
||||
pr.spec = cell(3)
|
||||
pr.unit = cell(4)
|
||||
pr.manageMode = atoi(cell(5), 1)
|
||||
pr.description = cell(6)
|
||||
|
||||
if pr.code == "" {
|
||||
errs = append(errs, importErr{pr.row, pr.code, "物料编码为空"})
|
||||
continue
|
||||
}
|
||||
if pr.name == "" {
|
||||
errs = append(errs, importErr{pr.row, pr.code, "名称为空"})
|
||||
continue
|
||||
}
|
||||
if pr.manageMode != 1 && pr.manageMode != 2 {
|
||||
errs = append(errs, importErr{pr.row, pr.code, "类型必须为 1(结构件) 或 2(精密件)"})
|
||||
continue
|
||||
}
|
||||
exists, _ := ctx.EntClient.Material.Query().
|
||||
Where(material.CodeEQ(pr.code)).Exist(ctx0())
|
||||
if exists {
|
||||
errs = append(errs, importErr{pr.row, pr.code, "物料编码已存在: " + pr.code})
|
||||
continue
|
||||
}
|
||||
plans = append(plans, pr)
|
||||
}
|
||||
|
||||
// 阶段2:有错则整体拒绝,一行都不写
|
||||
if len(errs) > 0 {
|
||||
ok(w, map[string]any{"success": 0, "failed": len(errs), "errors": errs})
|
||||
return
|
||||
}
|
||||
|
||||
// 阶段3:全部通过 → 单事务整体提交(原子,全成功或全失败)
|
||||
tx, e := ctx.EntClient.Tx(ctx0())
|
||||
if e != nil {
|
||||
fail(w, http.StatusInternalServerError, "开启事务失败: "+e.Error())
|
||||
return
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
for _, pr := range plans {
|
||||
if _, e = tx.Material.Create().
|
||||
SetCode(pr.code).
|
||||
SetName(pr.name).
|
||||
SetNillableShortName(strPtr(pr.shortName)).
|
||||
SetNillableSpec(strPtr(pr.spec)).
|
||||
SetNillableUnit(strPtr(pr.unit)).
|
||||
SetNillableDescription(strPtr(pr.description)).
|
||||
SetManageMode(pr.manageMode).
|
||||
SetIsBatchManaged(pr.manageMode == 1).
|
||||
SetIsSerialManaged(pr.manageMode == 2).
|
||||
Save(ctx0()); e != nil {
|
||||
fail(w, http.StatusInternalServerError, "创建物料失败: "+e.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if e = tx.Commit(); e != nil {
|
||||
fail(w, http.StatusInternalServerError, "提交事务失败: "+e.Error())
|
||||
return
|
||||
}
|
||||
committed = true
|
||||
ok(w, map[string]any{"success": len(plans), "failed": 0, "errors": []importErr{}})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ func querySemiHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
list, err := q.Order(ent.Desc("created_at")).
|
||||
list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
@@ -158,6 +158,8 @@ func queryLedgerHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orderNo := r.URL.Query().Get("orderNo")
|
||||
materialCode := r.URL.Query().Get("materialCode")
|
||||
materialName := r.URL.Query().Get("materialName")
|
||||
status := r.URL.Query().Get("status")
|
||||
page := atoi(r.URL.Query().Get("page"), 1)
|
||||
pageSize := atoi(r.URL.Query().Get("pageSize"), 20)
|
||||
|
||||
@@ -168,12 +170,18 @@ func queryLedgerHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
if materialCode != "" {
|
||||
q = q.Where(ordermaterialledger.MaterialCodeEQ(materialCode))
|
||||
}
|
||||
if materialName != "" {
|
||||
q = q.Where(ordermaterialledger.MaterialNameContains(materialName))
|
||||
}
|
||||
if status != "" {
|
||||
q = q.Where(ordermaterialledger.StatusEQ(status))
|
||||
}
|
||||
total, err := q.Count(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
list, err := q.Order(ent.Desc("created_at")).
|
||||
list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
|
||||
@@ -260,7 +260,7 @@ func queryOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
list, err := q.Order(ent.Desc("created_at")).
|
||||
list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
@@ -489,7 +489,7 @@ func exportOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
q = q.Where(outboundorder.BoxNoContains(boxNo))
|
||||
}
|
||||
|
||||
list, err := q.Order(ent.Desc("created_at")).All(ctx0())
|
||||
list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
|
||||
@@ -76,6 +76,39 @@ func requireAdmin(next func(w http.ResponseWriter, r *http.Request)) func(w http
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// userInfoHandler GET /api/user/info 当前登录用户信息 + 权限码
|
||||
func userInfoHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -116,7 +149,7 @@ func userDTO(u *ent.User) map[string]any {
|
||||
|
||||
// listUsersHandler GET /api/user/list 管理员查看全部用户
|
||||
func listUsersHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
|
||||
return requirePerm(ctx, "user:manage", 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())
|
||||
@@ -132,7 +165,7 @@ func listUsersHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
|
||||
// createUserHandler POST /api/user/create 管理员新建账号
|
||||
func createUserHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
|
||||
return requirePerm(ctx, "user:create", func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
@@ -190,7 +223,7 @@ func createUserHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
|
||||
// updateUserHandler POST /api/user/update 管理员编辑账号(可选重置密码)
|
||||
func updateUserHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
|
||||
return requirePerm(ctx, "user:edit", func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
ID int `json:"id"`
|
||||
RealName string `json:"realName"`
|
||||
@@ -209,6 +242,11 @@ func updateUserHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
fail(w, http.StatusNotFound, "用户不存在")
|
||||
return
|
||||
}
|
||||
// 内置管理员账号(username=admin)不允许通过管理接口修改(含禁用/改角色/改密码等)
|
||||
if target.Username == "admin" {
|
||||
fail(w, http.StatusBadRequest, "系统内置管理员账号不允许修改")
|
||||
return
|
||||
}
|
||||
upd := target.Update()
|
||||
if req.RealName != "" {
|
||||
upd.SetRealName(req.RealName)
|
||||
@@ -249,7 +287,7 @@ func updateUserHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
|
||||
// deleteUserHandler POST /api/user/delete 管理员删除账号
|
||||
func deleteUserHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
|
||||
return requirePerm(ctx, "user:delete", func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
ID int `json:"id"`
|
||||
}
|
||||
@@ -267,6 +305,11 @@ func deleteUserHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
fail(w, http.StatusNotFound, "用户不存在")
|
||||
return
|
||||
}
|
||||
// 内置管理员账号(username=admin)不允许删除
|
||||
if target.Username == "admin" {
|
||||
fail(w, http.StatusBadRequest, "系统内置管理员账号不允许删除")
|
||||
return
|
||||
}
|
||||
// 禁止删除最后一个管理员
|
||||
if target.Role == "admin" {
|
||||
n, _ := ctx.EntClient.User.Query().Where(user.RoleEQ("admin")).Count(ctx0())
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ func RegisterHandlers(server *rest.Server, ctx *svc.ServiceContext) {
|
||||
{Method: http.MethodGet, Path: "/api/material/query", Handler: queryMaterialsHandler(ctx)},
|
||||
{Method: http.MethodGet, Path: "/api/material/list", Handler: listMaterialsHandler(ctx)},
|
||||
{Method: http.MethodGet, Path: "/api/material/detail", Handler: getMaterialHandler(ctx)},
|
||||
{Method: http.MethodGet, Path: "/api/material/export", Handler: exportMaterialsHandler(ctx)},
|
||||
{Method: http.MethodPost, Path: "/api/material/import", Handler: excelMaterialImportHandler(ctx)},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ func aggregateStock(ctx *svc.ServiceContext, a aggregateStockArgs) ([]*stockAggR
|
||||
q = q.Where(inventory.CreatedAtLTE(endUnix))
|
||||
}
|
||||
|
||||
all, err := q.Order(ent.Desc("created_at")).All(ctx0())
|
||||
all, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).All(ctx0())
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -290,7 +290,7 @@ func stockDetailsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
list, err := q.Order(ent.Desc("created_at")).
|
||||
list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
|
||||
@@ -271,7 +271,7 @@ func queryStocktakeHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
|
||||
if no == "" {
|
||||
orders, _ := ctx.EntClient.StocktakeOrder.Query().
|
||||
Order(ent.Desc("created_at")).Limit(100).All(ctx0())
|
||||
Order(ent.Desc("created_at"), ent.Desc("id")).Limit(100).All(ctx0())
|
||||
ok(w, map[string]any{"list": orders})
|
||||
return
|
||||
}
|
||||
@@ -284,7 +284,7 @@ func queryStocktakeHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
list, err := ctx.EntClient.StocktakeItem.Query().
|
||||
Where(stocktakeitem.StocktakeNoEQ(no)).
|
||||
Order(ent.Desc("created_at")).
|
||||
Order(ent.Desc("created_at"), ent.Desc("id")).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
|
||||
@@ -97,7 +97,8 @@ func listZonesHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
|
||||
q := ctx.EntClient.Zone.Query()
|
||||
if zoneCode != "" {
|
||||
q = q.Where(zone.ZoneCodeContains(zoneCode))
|
||||
// 区域编码搜索大小写不敏感
|
||||
q = q.Where(zone.ZoneCodeContainsFold(zoneCode))
|
||||
}
|
||||
if zoneName != "" {
|
||||
q = q.Where(zone.ZoneNameContains(zoneName))
|
||||
@@ -128,7 +129,7 @@ func listZonesHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
list, err := q.Order(ent.Desc("created_at")).
|
||||
list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).
|
||||
Offset((page - 1) * pageSize).
|
||||
Limit(pageSize).
|
||||
All(ctx0())
|
||||
|
||||
Reference in New Issue
Block a user