From c1fc6d32b22f62ac4ae382fcd079fec8a1802fc9 Mon Sep 17 00:00:00 2001 From: SunYF <123@hard_man.com> Date: Thu, 3 Sep 2026 18:13:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=A4=9A=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=8A=9F=E8=83=BD=E8=BF=AD=E4=BB=A3=E4=B8=8E=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交包含多模块功能更新: 1. 权限系统重构:移除硬编码admin放行逻辑,新增精细化权限控制,完善权限树形结构与用户/角色保护 2. 新增物料导入导出接口,补充台账查询条件 3. 优化列表查询排序逻辑,新增区域编码大小写不敏感搜索 4. 修复帮助抽屉重复弹出问题,补充页面权限控制 5. 新增弱密码提示逻辑,优化MES/WMS用户/角色管理权限 6. 调整路由结构,新增权限校验脚本与前端权限树重构 7. 补充数据库字段与依赖包更新 --- bj_power_mes/frontend/src/pages/Rbac.vue | 15 +- bj_power_mes/internal/eventlog/eventlog.go | 2 +- bj_power_mes/internal/logic/auth.go | 25 +- bj_power_mes/public/index.html | 2 +- bj_power_wms/e2e_perm_tree.py | 58 ++ bj_power_wms/ent/migrate/schema.go | 1 + bj_power_wms/ent/mutation.go | 75 ++- bj_power_wms/ent/permission.go | 13 +- bj_power_wms/ent/permission/permission.go | 12 + bj_power_wms/ent/permission/where.go | 80 +++ bj_power_wms/ent/permission_create.go | 87 +++ bj_power_wms/ent/permission_update.go | 62 ++ bj_power_wms/ent/runtime.go | 18 +- bj_power_wms/go.mod | 1 + bj_power_wms/go.sum | 2 + bj_power_wms/internal/handler/auth.go | 16 + bj_power_wms/internal/handler/inbound.go | 6 +- bj_power_wms/internal/handler/inspection.go | 4 +- bj_power_wms/internal/handler/material.go | 201 +++++- bj_power_wms/internal/handler/misc.go | 12 +- bj_power_wms/internal/handler/outbound.go | 4 +- bj_power_wms/internal/handler/rbac.go | 51 +- bj_power_wms/internal/handler/rbac_admin.go | 146 +++-- bj_power_wms/internal/handler/routes.go | 2 + bj_power_wms/internal/handler/stock.go | 4 +- bj_power_wms/internal/handler/stocktake.go | 4 +- bj_power_wms/internal/handler/zone.go | 5 +- bj_power_wms/schema/permission.go | 2 + .../frontend/src/components/PageHelp.vue | 14 +- bj_power_wms_client/frontend/src/help.js | 52 +- .../frontend/src/layouts/MainLayout.vue | 3 +- .../frontend/src/pages/BaseData.vue | 102 +++- .../frontend/src/pages/Inbound.vue | 571 ++++++++++++++---- .../frontend/src/pages/InboundCreate.vue | 309 ---------- .../frontend/src/pages/Inspection.vue | 352 ++++++++--- .../frontend/src/pages/InspectionQuery.vue | 192 ------ .../frontend/src/pages/Inventory.vue | 5 +- .../frontend/src/pages/Ledger.vue | 61 +- .../frontend/src/pages/Login.vue | 8 + .../frontend/src/pages/Outbound.vue | 5 +- .../frontend/src/pages/RoleManage.vue | 74 ++- .../frontend/src/pages/Semi.vue | 2 +- .../frontend/src/pages/UserManage.vue | 6 +- .../frontend/src/router/index.js | 6 +- .../frontend/src/utils/perm.js | 5 +- bj_power_wms_client/web/static/index.html | 4 +- e2e_rbac.py | 6 +- 47 files changed, 1761 insertions(+), 926 deletions(-) create mode 100644 bj_power_wms/e2e_perm_tree.py delete mode 100644 bj_power_wms_client/frontend/src/pages/InboundCreate.vue delete mode 100644 bj_power_wms_client/frontend/src/pages/InspectionQuery.vue diff --git a/bj_power_mes/frontend/src/pages/Rbac.vue b/bj_power_mes/frontend/src/pages/Rbac.vue index 4786a4e..bb2482b 100644 --- a/bj_power_mes/frontend/src/pages/Rbac.vue +++ b/bj_power_mes/frontend/src/pages/Rbac.vue @@ -27,8 +27,8 @@ @@ -65,17 +65,17 @@ - + - + - + @@ -113,6 +113,9 @@ const tab = ref('users') const users = ref([]), roles = ref([]), perms = ref([]) const u = reactive({ show: false, id: 0, username: '', password: '', name: '', roleId: 0, status: 'ENABLED', canLoginWorkstation: false, workstationPassword: '', stations: [] }) +// 系统内置管理员账号(username=admin / 角色 SUPER_ADMIN)不允许被修改或删除 +const isAdminUser = (row) => row && (row.username === 'admin' || row.roleCode === 'SUPER_ADMIN') + async function loadAll() { users.value = (await request.get('/users')) || [] roles.value = (await request.get('/roles')) || [] @@ -145,7 +148,7 @@ async function saveUser() { ElMessage.success('保存成功'); u.show = false; loadAll() } async function delUser(row) { - if (row.id === 1) return ElMessage.warning('系统管理员不可删除') + if (isAdminUser(row)) return ElMessage.warning('系统管理员账号不可删除') await ElMessageBox.confirm('确认删除该用户?', '提示', { type: 'warning' }) await request.delete(`/users/${row.id}`); loadAll() } diff --git a/bj_power_mes/internal/eventlog/eventlog.go b/bj_power_mes/internal/eventlog/eventlog.go index 107ec6d..3cc5045 100644 --- a/bj_power_mes/internal/eventlog/eventlog.go +++ b/bj_power_mes/internal/eventlog/eventlog.go @@ -52,7 +52,7 @@ func (w *Writer) Query(ctx context.Context, eventType, workOrderNo, operator str if err != nil { return nil, 0, err } - list, err := q.Order(ent.Desc(eventlog.FieldCreatedAt)). + list, err := q.Order(ent.Desc(eventlog.FieldCreatedAt), ent.Desc(eventlog.FieldID)). Offset((page - 1) * pageSize).Limit(pageSize).All(ctx) if err != nil { return nil, 0, err diff --git a/bj_power_mes/internal/logic/auth.go b/bj_power_mes/internal/logic/auth.go index 7d9929d..cb085e5 100644 --- a/bj_power_mes/internal/logic/auth.go +++ b/bj_power_mes/internal/logic/auth.go @@ -355,6 +355,15 @@ func (s *Service) CreateUser(ctx context.Context, req UserReq) error { } func (s *Service) UpdateUser(ctx context.Context, req UserReq) error { + // 内置管理员账号(SUPER_ADMIN)不允许通过管理接口修改(含禁用/改角色/改密码等) + if usr, e := s.ctx.EntClient.User.Get(ctx, req.Id); e == nil { + if usr.Username == "admin" { + return errors.New("系统内置管理员账号不允许修改") + } + if rl, e2 := s.ctx.EntClient.Role.Get(ctx, usr.RoleId); e2 == nil && rl.Code == "SUPER_ADMIN" { + return errors.New("系统内置管理员账号不允许修改") + } + } u := s.ctx.EntClient.User.UpdateOneID(req.Id).SetRoleId(req.RoleId).SetName(req.Name). SetCanLoginWorkstation(req.CanLoginWorkstation) if req.Password != "" { @@ -413,8 +422,16 @@ func (s *Service) UserStations(ctx context.Context, userId int) []int { } func (s *Service) DeleteUser(ctx context.Context, id int) error { - if id == 1 { - return errors.New("不能删除超级管理员") + usr, err := s.ctx.EntClient.User.Get(ctx, id) + if err != nil { + return errors.New("用户不存在") + } + // 内置管理员账号(SUPER_ADMIN)不允许删除 + if usr.Username == "admin" { + return errors.New("系统内置管理员账号不允许删除") + } + if rl, e2 := s.ctx.EntClient.Role.Get(ctx, usr.RoleId); e2 == nil && rl.Code == "SUPER_ADMIN" { + return errors.New("系统内置管理员账号不允许删除") } return s.ctx.EntClient.User.DeleteOneID(id).Exec(ctx) } @@ -465,6 +482,10 @@ func (s *Service) CreateRole(ctx context.Context, req RoleReq) error { } func (s *Service) UpdateRole(ctx context.Context, req RoleReq) error { + // 内置管理员角色(SUPER_ADMIN)不允许修改其权限(admin 拥有所有权限,且不可被缩减) + if rl, e := s.ctx.EntClient.Role.Get(ctx, req.Id); e == nil && rl.Code == "SUPER_ADMIN" { + return errors.New("内置管理员角色不允许修改其权限") + } return s.ctx.EntClient.Role.UpdateOneID(req.Id). SetName(req.Name).SetCode(req.Code).SetRemark(req.Remark). SetPermissionCodes(req.PermissionCodes).Exec(ctx) diff --git a/bj_power_mes/public/index.html b/bj_power_mes/public/index.html index ca98666..a6cb1ae 100644 --- a/bj_power_mes/public/index.html +++ b/bj_power_mes/public/index.html @@ -6,7 +6,7 @@ MES 产线控制 - + diff --git a/bj_power_wms/e2e_perm_tree.py b/bj_power_wms/e2e_perm_tree.py new file mode 100644 index 0000000..2fcc627 --- /dev/null +++ b/bj_power_wms/e2e_perm_tree.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +"""验证:权限树形化(parent_code) - seed 回填 + /permissions 返回父子结构""" +import json, urllib.request, urllib.error + +BASE = "http://127.0.0.1:8902" +TOKEN = None + +def req(method, path, payload=None): + url = BASE + path + data = json.dumps(payload).encode() if payload is not None else None + r = urllib.request.Request(url, data=data, method=method) + r.add_header("Content-Type", "application/json") + if TOKEN: + r.add_header("Authorization", "Bearer " + TOKEN) + try: + with urllib.request.urlopen(r, timeout=20) as resp: + return json.loads(resp.read().decode()) + except urllib.error.HTTPError as e: + return {"_http": e.code, "_body": e.read().decode()[:300]} + +def main(): + global TOKEN + d = req("POST", "/api/auth/login", {"username": "admin", "password": "123456"}) + TOKEN = (d.get("data") or {}).get("token") + assert TOKEN, f"登录失败: {d}" + print("✓ 登录成功") + + # 执行 seed 回填 parent_code + sd = req("POST", "/api/rbac/seed") + print("✓ seed 完成:", sd.get("data") or sd) + + # 拉取权限,校验 parent_code + pl = req("GET", "/api/permissions") + listp = (pl.get("data") or {}).get("list") or [] + menus = [p for p in listp if p["type"] == "MENU"] + buttons = [p for p in listp if p["type"] == "BUTTON"] + print(f"权限总数 {len(listp)}:菜单 {len(menus)},按钮 {len(buttons)}") + + # 校验每个按钮的 parent_code 是否为有效菜单 code 或为空(全局) + menu_codes = {m["code"] for m in menus} + bad = [p for p in buttons if p.get("parentCode") and p["parentCode"] not in menu_codes] + global_btns = [p for p in buttons if not p.get("parentCode")] + print(f" 按钮中全局(无父)数: {len(global_btns)} -> {[p['code'] for p in global_btns]}") + print(f" 按钮 parent_code 有误(指向不存在菜单): {[p['code'] for p in bad]}") + assert not bad, "存在 parent_code 指向无效菜单" + print("✓ 所有按钮 parent_code 均有效(或为全局空)") + + # 展示各菜单下挂的按钮(模拟前端树) + for m in menus: + kids = [p["name"] for p in buttons if p.get("parentCode") == m["code"]] + print(f" [{m['name']}] 挂按钮: {kids if kids else '(无)'}") + + passed = not bad and len(global_btns) >= 1 + print("\n==== 验证 %s ====" % ("通过" if passed else "失败")) + return passed + +if __name__ == "__main__": + raise SystemExit(0 if main() else 1) \ No newline at end of file diff --git a/bj_power_wms/ent/migrate/schema.go b/bj_power_wms/ent/migrate/schema.go index e8a2d5b..7cbb333 100644 --- a/bj_power_wms/ent/migrate/schema.go +++ b/bj_power_wms/ent/migrate/schema.go @@ -370,6 +370,7 @@ var ( {Name: "code", Type: field.TypeString, Unique: true, Size: 64}, {Name: "name", Type: field.TypeString, Size: 64}, {Name: "type", Type: field.TypeString, Size: 20, Default: "MENU"}, + {Name: "parent_code", Type: field.TypeString, Nullable: true, Size: 64, Default: ""}, {Name: "path", Type: field.TypeString, Nullable: true, Size: 128, Default: ""}, {Name: "icon", Type: field.TypeString, Nullable: true, Size: 64, Default: ""}, {Name: "sort", Type: field.TypeInt, Nullable: true, Default: 0}, diff --git a/bj_power_wms/ent/mutation.go b/bj_power_wms/ent/mutation.go index 9cd11e6..9f71a0c 100644 --- a/bj_power_wms/ent/mutation.go +++ b/bj_power_wms/ent/mutation.go @@ -9883,6 +9883,7 @@ type PermissionMutation struct { code *string name *string _type *string + parent_code *string _path *string icon *string sort *int @@ -10104,6 +10105,55 @@ func (m *PermissionMutation) ResetType() { m._type = nil } +// SetParentCode sets the "parent_code" field. +func (m *PermissionMutation) SetParentCode(s string) { + m.parent_code = &s +} + +// ParentCode returns the value of the "parent_code" field in the mutation. +func (m *PermissionMutation) ParentCode() (r string, exists bool) { + v := m.parent_code + if v == nil { + return + } + return *v, true +} + +// OldParentCode returns the old "parent_code" field's value of the Permission entity. +// If the Permission object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *PermissionMutation) OldParentCode(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldParentCode is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldParentCode requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldParentCode: %w", err) + } + return oldValue.ParentCode, nil +} + +// ClearParentCode clears the value of the "parent_code" field. +func (m *PermissionMutation) ClearParentCode() { + m.parent_code = nil + m.clearedFields[permission.FieldParentCode] = struct{}{} +} + +// ParentCodeCleared returns if the "parent_code" field was cleared in this mutation. +func (m *PermissionMutation) ParentCodeCleared() bool { + _, ok := m.clearedFields[permission.FieldParentCode] + return ok +} + +// ResetParentCode resets all changes to the "parent_code" field. +func (m *PermissionMutation) ResetParentCode() { + m.parent_code = nil + delete(m.clearedFields, permission.FieldParentCode) +} + // SetPath sets the "path" field. func (m *PermissionMutation) SetPath(s string) { m._path = &s @@ -10467,7 +10517,7 @@ func (m *PermissionMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *PermissionMutation) Fields() []string { - fields := make([]string, 0, 9) + fields := make([]string, 0, 10) if m.code != nil { fields = append(fields, permission.FieldCode) } @@ -10477,6 +10527,9 @@ func (m *PermissionMutation) Fields() []string { if m._type != nil { fields = append(fields, permission.FieldType) } + if m.parent_code != nil { + fields = append(fields, permission.FieldParentCode) + } if m._path != nil { fields = append(fields, permission.FieldPath) } @@ -10509,6 +10562,8 @@ func (m *PermissionMutation) Field(name string) (ent.Value, bool) { return m.Name() case permission.FieldType: return m.GetType() + case permission.FieldParentCode: + return m.ParentCode() case permission.FieldPath: return m.Path() case permission.FieldIcon: @@ -10536,6 +10591,8 @@ func (m *PermissionMutation) OldField(ctx context.Context, name string) (ent.Val return m.OldName(ctx) case permission.FieldType: return m.OldType(ctx) + case permission.FieldParentCode: + return m.OldParentCode(ctx) case permission.FieldPath: return m.OldPath(ctx) case permission.FieldIcon: @@ -10578,6 +10635,13 @@ func (m *PermissionMutation) SetField(name string, value ent.Value) error { } m.SetType(v) return nil + case permission.FieldParentCode: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetParentCode(v) + return nil case permission.FieldPath: v, ok := value.(string) if !ok { @@ -10689,6 +10753,9 @@ func (m *PermissionMutation) AddField(name string, value ent.Value) error { // mutation. func (m *PermissionMutation) ClearedFields() []string { var fields []string + if m.FieldCleared(permission.FieldParentCode) { + fields = append(fields, permission.FieldParentCode) + } if m.FieldCleared(permission.FieldPath) { fields = append(fields, permission.FieldPath) } @@ -10715,6 +10782,9 @@ func (m *PermissionMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *PermissionMutation) ClearField(name string) error { switch name { + case permission.FieldParentCode: + m.ClearParentCode() + return nil case permission.FieldPath: m.ClearPath() return nil @@ -10744,6 +10814,9 @@ func (m *PermissionMutation) ResetField(name string) error { case permission.FieldType: m.ResetType() return nil + case permission.FieldParentCode: + m.ResetParentCode() + return nil case permission.FieldPath: m.ResetPath() return nil diff --git a/bj_power_wms/ent/permission.go b/bj_power_wms/ent/permission.go index 3072b27..b85cce3 100644 --- a/bj_power_wms/ent/permission.go +++ b/bj_power_wms/ent/permission.go @@ -22,6 +22,8 @@ type Permission struct { Name string `json:"name,omitempty"` // 类型 MENU菜单/BUTTON按钮 Type string `json:"type,omitempty"` + // 所属菜单编码(BUTTON 填其所属 MENU 的 code;全局按钮填空) + ParentCode string `json:"parentCode,omitempty"` // 前端路由(菜单类) Path string `json:"path,omitempty"` // 图标(菜单类) @@ -44,7 +46,7 @@ func (*Permission) scanValues(columns []string) ([]any, error) { switch columns[i] { case permission.FieldID, permission.FieldSort, permission.FieldCreatedAt, permission.FieldUpdatedAt: values[i] = new(sql.NullInt64) - case permission.FieldCode, permission.FieldName, permission.FieldType, permission.FieldPath, permission.FieldIcon, permission.FieldRemark: + case permission.FieldCode, permission.FieldName, permission.FieldType, permission.FieldParentCode, permission.FieldPath, permission.FieldIcon, permission.FieldRemark: values[i] = new(sql.NullString) default: values[i] = new(sql.UnknownType) @@ -85,6 +87,12 @@ func (_m *Permission) assignValues(columns []string, values []any) error { } else if value.Valid { _m.Type = value.String } + case permission.FieldParentCode: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field parent_code", values[i]) + } else if value.Valid { + _m.ParentCode = value.String + } case permission.FieldPath: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field path", values[i]) @@ -166,6 +174,9 @@ func (_m *Permission) String() string { builder.WriteString("type=") builder.WriteString(_m.Type) builder.WriteString(", ") + builder.WriteString("parent_code=") + builder.WriteString(_m.ParentCode) + builder.WriteString(", ") builder.WriteString("path=") builder.WriteString(_m.Path) builder.WriteString(", ") diff --git a/bj_power_wms/ent/permission/permission.go b/bj_power_wms/ent/permission/permission.go index caf7068..dfab14e 100644 --- a/bj_power_wms/ent/permission/permission.go +++ b/bj_power_wms/ent/permission/permission.go @@ -17,6 +17,8 @@ const ( FieldName = "name" // FieldType holds the string denoting the type field in the database. FieldType = "type" + // FieldParentCode holds the string denoting the parent_code field in the database. + FieldParentCode = "parent_code" // FieldPath holds the string denoting the path field in the database. FieldPath = "path" // FieldIcon holds the string denoting the icon field in the database. @@ -39,6 +41,7 @@ var Columns = []string{ FieldCode, FieldName, FieldType, + FieldParentCode, FieldPath, FieldIcon, FieldSort, @@ -66,6 +69,10 @@ var ( DefaultType string // TypeValidator is a validator for the "type" field. It is called by the builders before save. TypeValidator func(string) error + // DefaultParentCode holds the default value on creation for the "parent_code" field. + DefaultParentCode string + // ParentCodeValidator is a validator for the "parent_code" field. It is called by the builders before save. + ParentCodeValidator func(string) error // DefaultPath holds the default value on creation for the "path" field. DefaultPath string // PathValidator is a validator for the "path" field. It is called by the builders before save. @@ -109,6 +116,11 @@ func ByType(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldType, opts...).ToFunc() } +// ByParentCode orders the results by the parent_code field. +func ByParentCode(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldParentCode, opts...).ToFunc() +} + // ByPath orders the results by the path field. func ByPath(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldPath, opts...).ToFunc() diff --git a/bj_power_wms/ent/permission/where.go b/bj_power_wms/ent/permission/where.go index 0b4c16c..ffff4ca 100644 --- a/bj_power_wms/ent/permission/where.go +++ b/bj_power_wms/ent/permission/where.go @@ -68,6 +68,11 @@ func Type(v string) predicate.Permission { return predicate.Permission(sql.FieldEQ(FieldType, v)) } +// ParentCode applies equality check predicate on the "parent_code" field. It's identical to ParentCodeEQ. +func ParentCode(v string) predicate.Permission { + return predicate.Permission(sql.FieldEQ(FieldParentCode, v)) +} + // Path applies equality check predicate on the "path" field. It's identical to PathEQ. func Path(v string) predicate.Permission { return predicate.Permission(sql.FieldEQ(FieldPath, v)) @@ -293,6 +298,81 @@ func TypeContainsFold(v string) predicate.Permission { return predicate.Permission(sql.FieldContainsFold(FieldType, v)) } +// ParentCodeEQ applies the EQ predicate on the "parent_code" field. +func ParentCodeEQ(v string) predicate.Permission { + return predicate.Permission(sql.FieldEQ(FieldParentCode, v)) +} + +// ParentCodeNEQ applies the NEQ predicate on the "parent_code" field. +func ParentCodeNEQ(v string) predicate.Permission { + return predicate.Permission(sql.FieldNEQ(FieldParentCode, v)) +} + +// ParentCodeIn applies the In predicate on the "parent_code" field. +func ParentCodeIn(vs ...string) predicate.Permission { + return predicate.Permission(sql.FieldIn(FieldParentCode, vs...)) +} + +// ParentCodeNotIn applies the NotIn predicate on the "parent_code" field. +func ParentCodeNotIn(vs ...string) predicate.Permission { + return predicate.Permission(sql.FieldNotIn(FieldParentCode, vs...)) +} + +// ParentCodeGT applies the GT predicate on the "parent_code" field. +func ParentCodeGT(v string) predicate.Permission { + return predicate.Permission(sql.FieldGT(FieldParentCode, v)) +} + +// ParentCodeGTE applies the GTE predicate on the "parent_code" field. +func ParentCodeGTE(v string) predicate.Permission { + return predicate.Permission(sql.FieldGTE(FieldParentCode, v)) +} + +// ParentCodeLT applies the LT predicate on the "parent_code" field. +func ParentCodeLT(v string) predicate.Permission { + return predicate.Permission(sql.FieldLT(FieldParentCode, v)) +} + +// ParentCodeLTE applies the LTE predicate on the "parent_code" field. +func ParentCodeLTE(v string) predicate.Permission { + return predicate.Permission(sql.FieldLTE(FieldParentCode, v)) +} + +// ParentCodeContains applies the Contains predicate on the "parent_code" field. +func ParentCodeContains(v string) predicate.Permission { + return predicate.Permission(sql.FieldContains(FieldParentCode, v)) +} + +// ParentCodeHasPrefix applies the HasPrefix predicate on the "parent_code" field. +func ParentCodeHasPrefix(v string) predicate.Permission { + return predicate.Permission(sql.FieldHasPrefix(FieldParentCode, v)) +} + +// ParentCodeHasSuffix applies the HasSuffix predicate on the "parent_code" field. +func ParentCodeHasSuffix(v string) predicate.Permission { + return predicate.Permission(sql.FieldHasSuffix(FieldParentCode, v)) +} + +// ParentCodeIsNil applies the IsNil predicate on the "parent_code" field. +func ParentCodeIsNil() predicate.Permission { + return predicate.Permission(sql.FieldIsNull(FieldParentCode)) +} + +// ParentCodeNotNil applies the NotNil predicate on the "parent_code" field. +func ParentCodeNotNil() predicate.Permission { + return predicate.Permission(sql.FieldNotNull(FieldParentCode)) +} + +// ParentCodeEqualFold applies the EqualFold predicate on the "parent_code" field. +func ParentCodeEqualFold(v string) predicate.Permission { + return predicate.Permission(sql.FieldEqualFold(FieldParentCode, v)) +} + +// ParentCodeContainsFold applies the ContainsFold predicate on the "parent_code" field. +func ParentCodeContainsFold(v string) predicate.Permission { + return predicate.Permission(sql.FieldContainsFold(FieldParentCode, v)) +} + // PathEQ applies the EQ predicate on the "path" field. func PathEQ(v string) predicate.Permission { return predicate.Permission(sql.FieldEQ(FieldPath, v)) diff --git a/bj_power_wms/ent/permission_create.go b/bj_power_wms/ent/permission_create.go index 92745a3..524b743 100644 --- a/bj_power_wms/ent/permission_create.go +++ b/bj_power_wms/ent/permission_create.go @@ -47,6 +47,20 @@ func (_c *PermissionCreate) SetNillableType(v *string) *PermissionCreate { return _c } +// SetParentCode sets the "parent_code" field. +func (_c *PermissionCreate) SetParentCode(v string) *PermissionCreate { + _c.mutation.SetParentCode(v) + return _c +} + +// SetNillableParentCode sets the "parent_code" field if the given value is not nil. +func (_c *PermissionCreate) SetNillableParentCode(v *string) *PermissionCreate { + if v != nil { + _c.SetParentCode(*v) + } + return _c +} + // SetPath sets the "path" field. func (_c *PermissionCreate) SetPath(v string) *PermissionCreate { _c.mutation.SetPath(v) @@ -170,6 +184,10 @@ func (_c *PermissionCreate) defaults() { v := permission.DefaultType _c.mutation.SetType(v) } + if _, ok := _c.mutation.ParentCode(); !ok { + v := permission.DefaultParentCode + _c.mutation.SetParentCode(v) + } if _, ok := _c.mutation.Path(); !ok { v := permission.DefaultPath _c.mutation.SetPath(v) @@ -222,6 +240,11 @@ func (_c *PermissionCreate) check() error { return &ValidationError{Name: "type", err: fmt.Errorf(`ent: validator failed for field "Permission.type": %w`, err)} } } + if v, ok := _c.mutation.ParentCode(); ok { + if err := permission.ParentCodeValidator(v); err != nil { + return &ValidationError{Name: "parent_code", err: fmt.Errorf(`ent: validator failed for field "Permission.parent_code": %w`, err)} + } + } if v, ok := _c.mutation.Path(); ok { if err := permission.PathValidator(v); err != nil { return &ValidationError{Name: "path", err: fmt.Errorf(`ent: validator failed for field "Permission.path": %w`, err)} @@ -282,6 +305,10 @@ func (_c *PermissionCreate) createSpec() (*Permission, *sqlgraph.CreateSpec) { _spec.SetField(permission.FieldType, field.TypeString, value) _node.Type = value } + if value, ok := _c.mutation.ParentCode(); ok { + _spec.SetField(permission.FieldParentCode, field.TypeString, value) + _node.ParentCode = value + } if value, ok := _c.mutation.Path(); ok { _spec.SetField(permission.FieldPath, field.TypeString, value) _node.Path = value @@ -394,6 +421,24 @@ func (u *PermissionUpsert) UpdateType() *PermissionUpsert { return u } +// SetParentCode sets the "parent_code" field. +func (u *PermissionUpsert) SetParentCode(v string) *PermissionUpsert { + u.Set(permission.FieldParentCode, v) + return u +} + +// UpdateParentCode sets the "parent_code" field to the value that was provided on create. +func (u *PermissionUpsert) UpdateParentCode() *PermissionUpsert { + u.SetExcluded(permission.FieldParentCode) + return u +} + +// ClearParentCode clears the value of the "parent_code" field. +func (u *PermissionUpsert) ClearParentCode() *PermissionUpsert { + u.SetNull(permission.FieldParentCode) + return u +} + // SetPath sets the "path" field. func (u *PermissionUpsert) SetPath(v string) *PermissionUpsert { u.Set(permission.FieldPath, v) @@ -590,6 +635,27 @@ func (u *PermissionUpsertOne) UpdateType() *PermissionUpsertOne { }) } +// SetParentCode sets the "parent_code" field. +func (u *PermissionUpsertOne) SetParentCode(v string) *PermissionUpsertOne { + return u.Update(func(s *PermissionUpsert) { + s.SetParentCode(v) + }) +} + +// UpdateParentCode sets the "parent_code" field to the value that was provided on create. +func (u *PermissionUpsertOne) UpdateParentCode() *PermissionUpsertOne { + return u.Update(func(s *PermissionUpsert) { + s.UpdateParentCode() + }) +} + +// ClearParentCode clears the value of the "parent_code" field. +func (u *PermissionUpsertOne) ClearParentCode() *PermissionUpsertOne { + return u.Update(func(s *PermissionUpsert) { + s.ClearParentCode() + }) +} + // SetPath sets the "path" field. func (u *PermissionUpsertOne) SetPath(v string) *PermissionUpsertOne { return u.Update(func(s *PermissionUpsert) { @@ -969,6 +1035,27 @@ func (u *PermissionUpsertBulk) UpdateType() *PermissionUpsertBulk { }) } +// SetParentCode sets the "parent_code" field. +func (u *PermissionUpsertBulk) SetParentCode(v string) *PermissionUpsertBulk { + return u.Update(func(s *PermissionUpsert) { + s.SetParentCode(v) + }) +} + +// UpdateParentCode sets the "parent_code" field to the value that was provided on create. +func (u *PermissionUpsertBulk) UpdateParentCode() *PermissionUpsertBulk { + return u.Update(func(s *PermissionUpsert) { + s.UpdateParentCode() + }) +} + +// ClearParentCode clears the value of the "parent_code" field. +func (u *PermissionUpsertBulk) ClearParentCode() *PermissionUpsertBulk { + return u.Update(func(s *PermissionUpsert) { + s.ClearParentCode() + }) +} + // SetPath sets the "path" field. func (u *PermissionUpsertBulk) SetPath(v string) *PermissionUpsertBulk { return u.Update(func(s *PermissionUpsert) { diff --git a/bj_power_wms/ent/permission_update.go b/bj_power_wms/ent/permission_update.go index 34394ce..0bd758a 100644 --- a/bj_power_wms/ent/permission_update.go +++ b/bj_power_wms/ent/permission_update.go @@ -69,6 +69,26 @@ func (_u *PermissionUpdate) SetNillableType(v *string) *PermissionUpdate { return _u } +// SetParentCode sets the "parent_code" field. +func (_u *PermissionUpdate) SetParentCode(v string) *PermissionUpdate { + _u.mutation.SetParentCode(v) + return _u +} + +// SetNillableParentCode sets the "parent_code" field if the given value is not nil. +func (_u *PermissionUpdate) SetNillableParentCode(v *string) *PermissionUpdate { + if v != nil { + _u.SetParentCode(*v) + } + return _u +} + +// ClearParentCode clears the value of the "parent_code" field. +func (_u *PermissionUpdate) ClearParentCode() *PermissionUpdate { + _u.mutation.ClearParentCode() + return _u +} + // SetPath sets the "path" field. func (_u *PermissionUpdate) SetPath(v string) *PermissionUpdate { _u.mutation.SetPath(v) @@ -247,6 +267,11 @@ func (_u *PermissionUpdate) check() error { return &ValidationError{Name: "type", err: fmt.Errorf(`ent: validator failed for field "Permission.type": %w`, err)} } } + if v, ok := _u.mutation.ParentCode(); ok { + if err := permission.ParentCodeValidator(v); err != nil { + return &ValidationError{Name: "parent_code", err: fmt.Errorf(`ent: validator failed for field "Permission.parent_code": %w`, err)} + } + } if v, ok := _u.mutation.Path(); ok { if err := permission.PathValidator(v); err != nil { return &ValidationError{Name: "path", err: fmt.Errorf(`ent: validator failed for field "Permission.path": %w`, err)} @@ -286,6 +311,12 @@ func (_u *PermissionUpdate) sqlSave(ctx context.Context) (_node int, err error) if value, ok := _u.mutation.GetType(); ok { _spec.SetField(permission.FieldType, field.TypeString, value) } + if value, ok := _u.mutation.ParentCode(); ok { + _spec.SetField(permission.FieldParentCode, field.TypeString, value) + } + if _u.mutation.ParentCodeCleared() { + _spec.ClearField(permission.FieldParentCode, field.TypeString) + } if value, ok := _u.mutation.Path(); ok { _spec.SetField(permission.FieldPath, field.TypeString, value) } @@ -387,6 +418,26 @@ func (_u *PermissionUpdateOne) SetNillableType(v *string) *PermissionUpdateOne { return _u } +// SetParentCode sets the "parent_code" field. +func (_u *PermissionUpdateOne) SetParentCode(v string) *PermissionUpdateOne { + _u.mutation.SetParentCode(v) + return _u +} + +// SetNillableParentCode sets the "parent_code" field if the given value is not nil. +func (_u *PermissionUpdateOne) SetNillableParentCode(v *string) *PermissionUpdateOne { + if v != nil { + _u.SetParentCode(*v) + } + return _u +} + +// ClearParentCode clears the value of the "parent_code" field. +func (_u *PermissionUpdateOne) ClearParentCode() *PermissionUpdateOne { + _u.mutation.ClearParentCode() + return _u +} + // SetPath sets the "path" field. func (_u *PermissionUpdateOne) SetPath(v string) *PermissionUpdateOne { _u.mutation.SetPath(v) @@ -578,6 +629,11 @@ func (_u *PermissionUpdateOne) check() error { return &ValidationError{Name: "type", err: fmt.Errorf(`ent: validator failed for field "Permission.type": %w`, err)} } } + if v, ok := _u.mutation.ParentCode(); ok { + if err := permission.ParentCodeValidator(v); err != nil { + return &ValidationError{Name: "parent_code", err: fmt.Errorf(`ent: validator failed for field "Permission.parent_code": %w`, err)} + } + } if v, ok := _u.mutation.Path(); ok { if err := permission.PathValidator(v); err != nil { return &ValidationError{Name: "path", err: fmt.Errorf(`ent: validator failed for field "Permission.path": %w`, err)} @@ -634,6 +690,12 @@ func (_u *PermissionUpdateOne) sqlSave(ctx context.Context) (_node *Permission, if value, ok := _u.mutation.GetType(); ok { _spec.SetField(permission.FieldType, field.TypeString, value) } + if value, ok := _u.mutation.ParentCode(); ok { + _spec.SetField(permission.FieldParentCode, field.TypeString, value) + } + if _u.mutation.ParentCodeCleared() { + _spec.ClearField(permission.FieldParentCode, field.TypeString) + } if value, ok := _u.mutation.Path(); ok { _spec.SetField(permission.FieldPath, field.TypeString, value) } diff --git a/bj_power_wms/ent/runtime.go b/bj_power_wms/ent/runtime.go index dc639ca..13aa7d7 100644 --- a/bj_power_wms/ent/runtime.go +++ b/bj_power_wms/ent/runtime.go @@ -212,34 +212,40 @@ func init() { permission.DefaultType = permissionDescType.Default.(string) // permission.TypeValidator is a validator for the "type" field. It is called by the builders before save. permission.TypeValidator = permissionDescType.Validators[0].(func(string) error) + // permissionDescParentCode is the schema descriptor for parent_code field. + permissionDescParentCode := permissionFields[3].Descriptor() + // permission.DefaultParentCode holds the default value on creation for the parent_code field. + permission.DefaultParentCode = permissionDescParentCode.Default.(string) + // permission.ParentCodeValidator is a validator for the "parent_code" field. It is called by the builders before save. + permission.ParentCodeValidator = permissionDescParentCode.Validators[0].(func(string) error) // permissionDescPath is the schema descriptor for path field. - permissionDescPath := permissionFields[3].Descriptor() + permissionDescPath := permissionFields[4].Descriptor() // permission.DefaultPath holds the default value on creation for the path field. permission.DefaultPath = permissionDescPath.Default.(string) // permission.PathValidator is a validator for the "path" field. It is called by the builders before save. permission.PathValidator = permissionDescPath.Validators[0].(func(string) error) // permissionDescIcon is the schema descriptor for icon field. - permissionDescIcon := permissionFields[4].Descriptor() + permissionDescIcon := permissionFields[5].Descriptor() // permission.DefaultIcon holds the default value on creation for the icon field. permission.DefaultIcon = permissionDescIcon.Default.(string) // permission.IconValidator is a validator for the "icon" field. It is called by the builders before save. permission.IconValidator = permissionDescIcon.Validators[0].(func(string) error) // permissionDescSort is the schema descriptor for sort field. - permissionDescSort := permissionFields[5].Descriptor() + permissionDescSort := permissionFields[6].Descriptor() // permission.DefaultSort holds the default value on creation for the sort field. permission.DefaultSort = permissionDescSort.Default.(int) // permissionDescRemark is the schema descriptor for remark field. - permissionDescRemark := permissionFields[6].Descriptor() + permissionDescRemark := permissionFields[7].Descriptor() // permission.DefaultRemark holds the default value on creation for the remark field. permission.DefaultRemark = permissionDescRemark.Default.(string) // permission.RemarkValidator is a validator for the "remark" field. It is called by the builders before save. permission.RemarkValidator = permissionDescRemark.Validators[0].(func(string) error) // permissionDescCreatedAt is the schema descriptor for created_at field. - permissionDescCreatedAt := permissionFields[7].Descriptor() + permissionDescCreatedAt := permissionFields[8].Descriptor() // permission.DefaultCreatedAt holds the default value on creation for the created_at field. permission.DefaultCreatedAt = permissionDescCreatedAt.Default.(func() int64) // permissionDescUpdatedAt is the schema descriptor for updated_at field. - permissionDescUpdatedAt := permissionFields[8].Descriptor() + permissionDescUpdatedAt := permissionFields[9].Descriptor() // permission.DefaultUpdatedAt holds the default value on creation for the updated_at field. permission.DefaultUpdatedAt = permissionDescUpdatedAt.Default.(func() int64) roleFields := schema.Role{}.Fields() diff --git a/bj_power_wms/go.mod b/bj_power_wms/go.mod index abc447e..2900a03 100644 --- a/bj_power_wms/go.mod +++ b/bj_power_wms/go.mod @@ -32,6 +32,7 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/compress v1.17.11 // indirect + github.com/lib/pq v1.12.3 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect diff --git a/bj_power_wms/go.sum b/bj_power_wms/go.sum index 8ba5523..54c3357 100644 --- a/bj_power_wms/go.sum +++ b/bj_power_wms/go.sum @@ -112,6 +112,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= diff --git a/bj_power_wms/internal/handler/auth.go b/bj_power_wms/internal/handler/auth.go index 893398c..1f84cf5 100644 --- a/bj_power_wms/internal/handler/auth.go +++ b/bj_power_wms/internal/handler/auth.go @@ -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, diff --git a/bj_power_wms/internal/handler/inbound.go b/bj_power_wms/internal/handler/inbound.go index ad3ae90..a90366c 100644 --- a/bj_power_wms/internal/handler/inbound.go +++ b/bj_power_wms/internal/handler/inbound.go @@ -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 diff --git a/bj_power_wms/internal/handler/inspection.go b/bj_power_wms/internal/handler/inspection.go index 324962f..a7456eb 100644 --- a/bj_power_wms/internal/handler/inspection.go +++ b/bj_power_wms/internal/handler/inspection.go @@ -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 diff --git a/bj_power_wms/internal/handler/material.go b/bj_power_wms/internal/handler/material.go index 94a88e6..63f7a9a 100644 --- a/bj_power_wms/internal/handler/material.go +++ b/bj_power_wms/internal/handler/material.go @@ -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{}}) + } +} diff --git a/bj_power_wms/internal/handler/misc.go b/bj_power_wms/internal/handler/misc.go index 1e5bdcd..21845b5 100644 --- a/bj_power_wms/internal/handler/misc.go +++ b/bj_power_wms/internal/handler/misc.go @@ -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()) diff --git a/bj_power_wms/internal/handler/outbound.go b/bj_power_wms/internal/handler/outbound.go index 0f2c97e..d2a70da 100644 --- a/bj_power_wms/internal/handler/outbound.go +++ b/bj_power_wms/internal/handler/outbound.go @@ -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 diff --git a/bj_power_wms/internal/handler/rbac.go b/bj_power_wms/internal/handler/rbac.go index abb38ae..b773681 100644 --- a/bj_power_wms/internal/handler/rbac.go +++ b/bj_power_wms/internal/handler/rbac.go @@ -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()) diff --git a/bj_power_wms/internal/handler/rbac_admin.go b/bj_power_wms/internal/handler/rbac_admin.go index 98b62da..066efbb 100644 --- a/bj_power_wms/internal/handler/rbac_admin.go +++ b/bj_power_wms/internal/handler/rbac_admin.go @@ -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"` } diff --git a/bj_power_wms/internal/handler/routes.go b/bj_power_wms/internal/handler/routes.go index 5bbe3b5..efeb11b 100644 --- a/bj_power_wms/internal/handler/routes.go +++ b/bj_power_wms/internal/handler/routes.go @@ -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)}, }, ) diff --git a/bj_power_wms/internal/handler/stock.go b/bj_power_wms/internal/handler/stock.go index 076c315..6a56878 100644 --- a/bj_power_wms/internal/handler/stock.go +++ b/bj_power_wms/internal/handler/stock.go @@ -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()) diff --git a/bj_power_wms/internal/handler/stocktake.go b/bj_power_wms/internal/handler/stocktake.go index 8d696e1..8b54d41 100644 --- a/bj_power_wms/internal/handler/stocktake.go +++ b/bj_power_wms/internal/handler/stocktake.go @@ -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()) diff --git a/bj_power_wms/internal/handler/zone.go b/bj_power_wms/internal/handler/zone.go index a246c3e..fb4ea93 100644 --- a/bj_power_wms/internal/handler/zone.go +++ b/bj_power_wms/internal/handler/zone.go @@ -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()) diff --git a/bj_power_wms/schema/permission.go b/bj_power_wms/schema/permission.go index 8165e30..293f3d8 100644 --- a/bj_power_wms/schema/permission.go +++ b/bj_power_wms/schema/permission.go @@ -26,6 +26,8 @@ func (Permission) Fields() []ent.Field { field.String("code").Unique().MaxLen(64).Comment("权限编码,如 inbound:view"), field.String("name").MaxLen(64).Comment("名称"), field.String("type").Default("MENU").MaxLen(20).Comment("类型 MENU菜单/BUTTON按钮"), + field.String("parent_code").Default("").MaxLen(64).Optional(). + Comment("所属菜单编码(BUTTON 填其所属 MENU 的 code;全局按钮填空)"), field.String("path").Default("").MaxLen(128).Optional().Comment("前端路由(菜单类)"), field.String("icon").Default("").MaxLen(64).Optional().Comment("图标(菜单类)"), field.Int("sort").Default(0).Optional().Comment("排序"), diff --git a/bj_power_wms_client/frontend/src/components/PageHelp.vue b/bj_power_wms_client/frontend/src/components/PageHelp.vue index f8dd24d..0caec54 100644 --- a/bj_power_wms_client/frontend/src/components/PageHelp.vue +++ b/bj_power_wms_client/frontend/src/components/PageHelp.vue @@ -51,13 +51,21 @@ - + diff --git a/bj_power_wms_client/frontend/src/pages/Inbound.vue b/bj_power_wms_client/frontend/src/pages/Inbound.vue index a97db18..3bc6a81 100644 --- a/bj_power_wms_client/frontend/src/pages/Inbound.vue +++ b/bj_power_wms_client/frontend/src/pages/Inbound.vue @@ -1,27 +1,198 @@ + \ No newline at end of file diff --git a/bj_power_wms_client/frontend/src/pages/InboundCreate.vue b/bj_power_wms_client/frontend/src/pages/InboundCreate.vue deleted file mode 100644 index 53b1890..0000000 --- a/bj_power_wms_client/frontend/src/pages/InboundCreate.vue +++ /dev/null @@ -1,309 +0,0 @@ - - - - - diff --git a/bj_power_wms_client/frontend/src/pages/Inspection.vue b/bj_power_wms_client/frontend/src/pages/Inspection.vue index cc99270..75611e3 100644 --- a/bj_power_wms_client/frontend/src/pages/Inspection.vue +++ b/bj_power_wms_client/frontend/src/pages/Inspection.vue @@ -1,11 +1,19 @@ + \ No newline at end of file diff --git a/bj_power_wms_client/frontend/src/pages/InspectionQuery.vue b/bj_power_wms_client/frontend/src/pages/InspectionQuery.vue deleted file mode 100644 index 0378f2d..0000000 --- a/bj_power_wms_client/frontend/src/pages/InspectionQuery.vue +++ /dev/null @@ -1,192 +0,0 @@ - - - - - \ No newline at end of file diff --git a/bj_power_wms_client/frontend/src/pages/Inventory.vue b/bj_power_wms_client/frontend/src/pages/Inventory.vue index 64d4b33..d4ce974 100644 --- a/bj_power_wms_client/frontend/src/pages/Inventory.vue +++ b/bj_power_wms_client/frontend/src/pages/Inventory.vue @@ -4,6 +4,7 @@ import { onMounted, reactive, ref } from 'vue' import request from '../utils/request' import { fmtTime } from '../utils/format' import { exportXlsxFetch } from '../utils/export' +import { can } from '../utils/perm' import PageHelp from '../components/PageHelp.vue' import { helpInventory } from '../help' import { useRouter } from 'vue-router' @@ -219,7 +220,7 @@ onMounted(() => { 查询 - 导出 + 导出 @@ -353,7 +354,7 @@ onMounted(() => { - + diff --git a/bj_power_wms_client/frontend/src/pages/Ledger.vue b/bj_power_wms_client/frontend/src/pages/Ledger.vue index d1ca10a..fb474a9 100644 --- a/bj_power_wms_client/frontend/src/pages/Ledger.vue +++ b/bj_power_wms_client/frontend/src/pages/Ledger.vue @@ -8,9 +8,10 @@ import { helpLedger } from '../help' const orderNo = ref('') const materialCode = ref('') +const materialName = ref('') +const status = ref('') const loading = ref(false) const queried = ref(false) -const usingDemo = ref(false) const rows = ref([]) const total = ref(0) const page = reactive({ current: 1, size: 20 }) @@ -19,25 +20,6 @@ function gapOf(row) { return Math.max((row.totalQty || 0) - (row.outQty || 0), 0) } -// 当真实台账为空(或无符合条件数据)时,注入一组演示记录,方便查看展示效果 -function demoRows() { - const now = Date.now() - const mk = (order, code, name, totalQty, outQty, status, remark, days) => ({ - orderNo: order, materialCode: code, materialName: name, - totalQty, outQty, status, remark, isDemo: true, createdAt: now - days * 86400000 - }) - return [ - mk('WO20260903001', 'ST-A100', '结构件-A型立柱', 240, 240, '领料完结', '已全部领料', 0), - mk('WO20260903001', 'ST-B200', '结构件-B型横梁', 180, 120, '进行中', '余量 60 待领', 0), - mk('WO20260903001', 'ST-C300', '结构件-C型底座', 90, 90, '领料完结', '', 1), - mk('WO20260902017', 'PR-S100', '精密件-伺服电机主轴', 56, 36, '进行中', '余量 20 待领', 1), - mk('WO20260902017', 'PR-S200', '精密件-编码器', 56, 0, '进行中', '尚未开始领料', 2), - mk('WO20260902018', 'ST-D400', '结构件-D型支撑', 300, 210, '进行中', '余量 90 待领', 2), - mk('WO20260901045', 'PR-S300', '精密件-减速箱体', 24, 24, '领料完结', '', 5), - mk('WO20260901045', 'ST-E500', '结构件-E型端盖', 60, 40, '进行中', '', 5) - ] -} - async function load() { loading.value = true try { @@ -45,21 +27,14 @@ async function load() { params: { orderNo: orderNo.value.trim() || undefined, materialCode: materialCode.value.trim() || undefined, + materialName: materialName.value.trim() || undefined, + status: status.value || undefined, page: page.current, pageSize: page.size } }) - const real = data?.list || [] - if (real.length) { - rows.value = real - total.value = data?.total || 0 - usingDemo.value = false - } else { - // 无真实台账 → 展示演示数据(标注),便于查看效果 - rows.value = demoRows() - total.value = rows.value.length - usingDemo.value = true - } + rows.value = data?.list || [] + total.value = data?.total || 0 queried.value = true } finally { loading.value = false @@ -84,11 +59,21 @@ function statusTag(s) { + style="width:200px" @keyup.enter="search" /> + style="width:160px" @keyup.enter="search" /> + + + + + + + + + 查询 @@ -99,12 +84,12 @@ function statusTag(s) { + - - + + @@ -135,10 +120,10 @@ function statusTag(s) { - + \ No newline at end of file + diff --git a/bj_power_wms_client/frontend/src/pages/Login.vue b/bj_power_wms_client/frontend/src/pages/Login.vue index f5944f1..e37ddf9 100644 --- a/bj_power_wms_client/frontend/src/pages/Login.vue +++ b/bj_power_wms_client/frontend/src/pages/Login.vue @@ -27,6 +27,14 @@ async function onSubmit() { ElMessage.success('登录成功') const redirect = route.query.redirect router.push(redirect && typeof redirect === 'string' ? redirect : '/') + // 弱密码(纯数字)允许登录,仅提示尽快修改 + if (data.weakPassword) { + ElMessage.warning({ + message: '当前密码为纯数字,安全性较低,建议尽快修改密码', + duration: 8000, + showClose: true + }) + } } finally { loading.value = false } diff --git a/bj_power_wms_client/frontend/src/pages/Outbound.vue b/bj_power_wms_client/frontend/src/pages/Outbound.vue index a556486..04971d7 100644 --- a/bj_power_wms_client/frontend/src/pages/Outbound.vue +++ b/bj_power_wms_client/frontend/src/pages/Outbound.vue @@ -6,6 +6,7 @@ import request from '../utils/request' import { getRealName } from '../utils/auth' import { fmtTime } from '../utils/format' import { exportXlsxFetch } from '../utils/export' +import { can } from '../utils/perm' import PageHelp from '../components/PageHelp.vue' import { helpOutbound } from '../help' @@ -386,7 +387,7 @@ onMounted(loadDicts) 查询 重置 - 导出 + 导出 @@ -430,7 +431,7 @@ onMounted(loadDicts) - + \ No newline at end of file diff --git a/bj_power_wms_client/frontend/src/pages/Semi.vue b/bj_power_wms_client/frontend/src/pages/Semi.vue index 0a14727..170a046 100644 --- a/bj_power_wms_client/frontend/src/pages/Semi.vue +++ b/bj_power_wms_client/frontend/src/pages/Semi.vue @@ -228,7 +228,7 @@ function search() { - +