feat: 完成多模块功能迭代与优化
本次提交包含多模块功能更新: 1. 权限系统重构:移除硬编码admin放行逻辑,新增精细化权限控制,完善权限树形结构与用户/角色保护 2. 新增物料导入导出接口,补充台账查询条件 3. 优化列表查询排序逻辑,新增区域编码大小写不敏感搜索 4. 修复帮助抽屉重复弹出问题,补充页面权限控制 5. 新增弱密码提示逻辑,优化MES/WMS用户/角色管理权限 6. 调整路由结构,新增权限校验脚本与前端权限树重构 7. 补充数据库字段与依赖包更新
This commit is contained in:
@@ -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{}})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user