feat(cli): 添加命令行敏感操作门禁和数据库管理功能
- 添加 migrate、seed、reset-all 命令行子命令 - 实现管理员密码门禁机制保护敏感操作 - 添加数据库表结构迁移和种子数据初始化功能 - 实现清空并重建全部表的 reset-all 功能 - 重构种子数据只包含系统必需基础数据,移除演示数据 - 添加 RBAC 权限角色初始化的命令行支持 - 配置命令行密码验证和操作日志记录功能
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/subtle"
|
||||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
@@ -13,6 +16,8 @@ import (
|
||||
"time"
|
||||
|
||||
"bj_power_wms/internal/config"
|
||||
"bj_power_wms/internal/db"
|
||||
"bj_power_wms/internal/eventlog"
|
||||
"bj_power_wms/internal/handler"
|
||||
"bj_power_wms/internal/logdaily"
|
||||
"bj_power_wms/internal/svc"
|
||||
@@ -69,6 +74,102 @@ func openBrowser(url string) {
|
||||
_ = cmd.Start()
|
||||
}
|
||||
|
||||
// promptPassword 从终端读取一行输入(不隐藏回显,用于运维终端操作)
|
||||
func promptPassword(msg string) string {
|
||||
fmt.Print(msg)
|
||||
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
return strings.TrimRight(line, "\r\n")
|
||||
}
|
||||
|
||||
// adminGate 管理员密码门禁:敏感命令行操作(migrate/seed/reset-all)必须输入与配置
|
||||
// Cli.AdminPassword 一致的密码才放行;校验失败记录日志并终止,绝不继续执行(对齐 MES)。
|
||||
func adminGate(c config.Config) bool {
|
||||
pwd := promptPassword("请输入管理员密码以执行 [" + flag.Arg(0) + "]: ")
|
||||
if subtle.ConstantTimeCompare([]byte(pwd), []byte(c.Cli.AdminPassword)) != 1 {
|
||||
logx.Errorf("敏感命令 [%s] 因管理员密码校验失败被终止", flag.Arg(0))
|
||||
fmt.Println("管理员密码不正确,已终止该操作。")
|
||||
return false
|
||||
}
|
||||
logx.Infof("敏感命令 [%s] 已通过管理员密码校验", flag.Arg(0))
|
||||
return true
|
||||
}
|
||||
|
||||
// dbConf 从服务配置构造数据库连接配置
|
||||
func dbConf(c config.Config) db.DatabaseConf {
|
||||
return db.DatabaseConf{
|
||||
Host: c.Postgres.Host,
|
||||
Port: c.Postgres.Port,
|
||||
User: c.Postgres.User,
|
||||
Password: c.Postgres.Password,
|
||||
Dbname: c.Postgres.DBName,
|
||||
}
|
||||
}
|
||||
|
||||
// runMigrate 仅建表/补列(只增,不删任何数据)
|
||||
func runMigrate(c config.Config) {
|
||||
dbc := dbConf(c)
|
||||
if err := db.EnsureDB(dbc); err != nil {
|
||||
logx.Errorf("ensure database failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
client := db.MustNewDB(dbc)
|
||||
defer client.Close()
|
||||
if err := db.AutoMigrate(client); err != nil {
|
||||
logx.Errorf("migrate failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("migrate: 数据库结构迁移成功(只增/补列,未删除任何数据)。")
|
||||
}
|
||||
|
||||
// runSeed 对齐基础主数据 + 权限角色(不删数据;表不存在则先建)
|
||||
func runSeed(c config.Config) {
|
||||
dbc := dbConf(c)
|
||||
if err := db.EnsureDB(dbc); err != nil {
|
||||
logx.Errorf("ensure database failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
client := db.MustNewDB(dbc)
|
||||
defer client.Close()
|
||||
if err := db.AutoMigrate(client); err != nil {
|
||||
logx.Errorf("seed migrate failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := db.SeedIfEmpty(client); err != nil {
|
||||
logx.Errorf("seed failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
svcCtx := &svc.ServiceContext{EntClient: client, EventLog: eventlog.NewWriter(client)}
|
||||
p, r, b := handler.SeedRBAC(svcCtx, "system")
|
||||
fmt.Printf("seed: 基础数据与权限已对齐(新增权限 %d、角色 %d、绑定用户 %d)。\n", p, r, b)
|
||||
}
|
||||
|
||||
// runResetAll 清空并重建全部表 + 重灌全部种子(基础主数据 + 权限角色)。此操作不可恢复!
|
||||
func runResetAll(c config.Config) {
|
||||
dbc := dbConf(c)
|
||||
if err := db.EnsureDB(dbc); err != nil {
|
||||
logx.Errorf("ensure database failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := db.DropAllTables(dbc); err != nil {
|
||||
logx.Errorf("reset-all drop failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
client := db.MustNewDB(dbc)
|
||||
defer client.Close()
|
||||
if err := db.AutoMigrate(client); err != nil {
|
||||
logx.Errorf("reset-all migrate failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := db.SeedIfEmpty(client); err != nil {
|
||||
logx.Errorf("reset-all seed failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
svcCtx := &svc.ServiceContext{EntClient: client, EventLog: eventlog.NewWriter(client)}
|
||||
p, r, b := handler.SeedRBAC(svcCtx, "system")
|
||||
logx.Infof("reset-all 完成:新增权限 %d、角色 %d、绑定用户 %d", p, r, b)
|
||||
fmt.Println("reset-all: 已清空并重建全部表,写入基础主数据与权限(管理员 admin/123456)。")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
@@ -83,6 +184,35 @@ func main() {
|
||||
logx.SetWriter(logx.NewWriter(w))
|
||||
}
|
||||
|
||||
// 命令行子命令:DDL/删库/种子等敏感操作一律显式触发 + 管理员密码门禁,
|
||||
// 绝不随服务启动自动执行(对齐 MES:migrate / seed / reset-all / serve)。
|
||||
switch cmd := flag.Arg(0); cmd {
|
||||
case "reset-all":
|
||||
fmt.Println("reset-all: 即将清空并重建 WMS 全部表,此操作不可恢复!")
|
||||
if !adminGate(c) {
|
||||
os.Exit(1)
|
||||
}
|
||||
runResetAll(c)
|
||||
return
|
||||
case "seed":
|
||||
if !adminGate(c) {
|
||||
os.Exit(1)
|
||||
}
|
||||
runSeed(c)
|
||||
return
|
||||
case "migrate":
|
||||
if !adminGate(c) {
|
||||
os.Exit(1)
|
||||
}
|
||||
runMigrate(c)
|
||||
return
|
||||
case "", "serve":
|
||||
// 正常启动服务:落到下方启动逻辑
|
||||
default:
|
||||
fmt.Println("未知命令: " + cmd + ";可用命令: migrate / seed / reset-all / serve")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
staticHandler := newStaticHandler(webFS)
|
||||
|
||||
// 兜底路由:已注册的 /api/* 走正常匹配;未注册的非 /api 路径走嵌入式静态资源
|
||||
|
||||
@@ -49,6 +49,10 @@ Agv:
|
||||
Web:
|
||||
OpenBrowser: true
|
||||
|
||||
# 命令行敏感操作门禁密码(migrate/reset-all/seed 需输入,独立于后台登录账号密码)
|
||||
Cli:
|
||||
AdminPassword: "Hardman_2026"
|
||||
|
||||
# 附件存储(年/月/日/文件类型/uuid.ext,库内只存相对路径,不存绝对路径)
|
||||
# 生产环境改 RootDir: /data/bj_power/attachments
|
||||
Attachment:
|
||||
|
||||
@@ -36,12 +36,12 @@ type MesConfig struct {
|
||||
// AgvConfig 海康 AGV(RCS-2000) 调度系统对接
|
||||
// 未配置 appKey/appSecret/RCS 地址 或 EnableMock=true 时,使用 mock 客户端(模拟下发成功/到位),便于离线联调。
|
||||
type AgvConfig struct {
|
||||
EnableMock bool `json:",default=true"` // 是否模拟(未接真实 RCS 前保持 true)
|
||||
EnableMock bool `json:",default=true"` // 是否模拟(未接真实 RCS 前保持 true)
|
||||
BaseURL string `json:",default=http://127.0.0.1:1010/rcs/rtas"` // RCS 服务前缀,如 http://10.10.10.10:1010/rcs/rtas
|
||||
AppKey string `json:",default=unset"` // RCS 颁发给业务系统的应用标识
|
||||
AppSecret string `json:",default=unset"` // RCS 应用私钥
|
||||
CarrierCode string `json:",default=DEFAULT"` // 默认载具/托盘编号(选填)
|
||||
PollInterval int `json:",default=10"` // 任务状态轮询间隔(秒)
|
||||
AppKey string `json:",default=unset"` // RCS 颁发给业务系统的应用标识
|
||||
AppSecret string `json:",default=unset"` // RCS 应用私钥
|
||||
CarrierCode string `json:",default=DEFAULT"` // 默认载具/托盘编号(选填)
|
||||
PollInterval int `json:",default=10"` // 任务状态轮询间隔(秒)
|
||||
}
|
||||
|
||||
// WebConfig 前端静态资源托管配置(原库房客户端 8891 已合并进主服务)
|
||||
@@ -49,6 +49,12 @@ type WebConfig struct {
|
||||
OpenBrowser bool `json:",default=true"` // 本机开发时启动后自动打开浏览器;服务器部署设为 false
|
||||
}
|
||||
|
||||
// CliConfig 命令行敏感操作门禁密码(migrate/reset-all/seed 需输入,独立于后台登录账号密码)。
|
||||
// 与 MES 对齐:DDL/删库等高危操作一律显式触发 + 密码校验,绝不随服务启动自动执行。
|
||||
type CliConfig struct {
|
||||
AdminPassword string `json:",default=Hardman_2026"`
|
||||
}
|
||||
|
||||
// AttachmentConfig 附件存储 / 归档 / 磁盘监控配置(etc/*.yaml 的 Attachment 段)。
|
||||
// 落盘约定:<RootDir>/年/月/日/<文件类型大写>/<uuid>.<ext>,库内只存相对路径。
|
||||
type AttachmentConfig struct {
|
||||
@@ -73,6 +79,7 @@ type Config struct {
|
||||
Agv AgvConfig
|
||||
Web WebConfig
|
||||
Attachment AttachmentConfig
|
||||
Cli CliConfig
|
||||
}
|
||||
|
||||
// RedisConf Redis 连接配置
|
||||
|
||||
@@ -268,3 +268,39 @@ func applyColumnPatches(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DropAllTables 删除 public schema 下全部表(CASCADE 连带索引/序列/外键约束)。
|
||||
// 仅供命令行 reset-all 使用:清空后由 AutoMigrate 重建结构、SeedIfEmpty + handler.SeedRBAC 重灌种子。
|
||||
// 与 MES 的 db.DropAllTables 对齐(逐表 DROP 而非 DROP SCHEMA,保留 schema 权限归属)。
|
||||
func DropAllTables(c DatabaseConf) error {
|
||||
dsn := fmt.Sprintf("postgresql://%s:%s@%s:%d/%s?sslmode=disable",
|
||||
c.User, c.Password, c.Host, c.Port, c.Dbname)
|
||||
sqlDB, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
|
||||
rows, err := sqlDB.Query(`SELECT tablename FROM pg_tables WHERE schemaname='public'`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var names []string
|
||||
for rows.Next() {
|
||||
var t string
|
||||
if err := rows.Scan(&t); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
names = append(names, t)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
for _, t := range names {
|
||||
if _, err := sqlDB.Exec(`DROP TABLE IF EXISTS "` + t + `" CASCADE`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
slog.Info(fmt.Sprintf("已删除全部表 %d 张", len(names)))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,14 +4,21 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"bj_power_wms/ent"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// SeedIfEmpty 首次启动时预置基础数据(区域/账号/演示物料与库存)
|
||||
// SeedIfEmpty 首次启动/重置后预置「系统运行必需的基础主数据」,不含任何演示业务数据。
|
||||
//
|
||||
// 生产环境口径(用户裁决 2026-09-20):种子只允许写入系统跑起来所必需的固定主数据,
|
||||
// 严禁预置演示物料/演示库存/演示账号等假业务数据。具体:
|
||||
// - 区域:库房四大基础分区 Z01~Z04(库存落位的根节点,level=1);
|
||||
// - 管理员账号:admin(初始密码 123456,登录后应立即修改);其余账号由管理员在「账号管理」按需创建、分配角色;
|
||||
// - 接驳台:DOCK01~20(产线,DOCKn↔工位n)+ DOCK21(库房收货),系统预置只读主数据(dock.go 不提供增删改)。
|
||||
//
|
||||
// 权限与角色不在此:由 handler.SeedRBAC 幂等初始化(reset-all/seed 命令与 POST /api/rbac/seed 共用同一函数)。
|
||||
func SeedIfEmpty(client *ent.Client) error {
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -36,106 +43,20 @@ func SeedIfEmpty(client *ent.Client) error {
|
||||
slog.Info("seed: zones created")
|
||||
}
|
||||
|
||||
// 2. 账号(全部统一初始密码 123456)
|
||||
// 2. 管理员账号(唯一预置账号;初始密码 123456,登录后应立即修改)
|
||||
users, _ := client.User.Query().Count(ctx)
|
||||
if users == 0 {
|
||||
adminHash, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
|
||||
opHash, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
|
||||
client.User.Create().
|
||||
SetUsername("admin").
|
||||
SetPassword(string(adminHash)).
|
||||
SetRealName("系统管理员").
|
||||
SetRole("admin").
|
||||
SaveX(ctx)
|
||||
client.User.Create().
|
||||
SetUsername("store1").
|
||||
SetPassword(string(opHash)).
|
||||
SetRealName("一号库房保管员").
|
||||
SetRole("operator").
|
||||
SetDept("库房").
|
||||
SaveX(ctx)
|
||||
client.User.Create().
|
||||
SetUsername("insp1").
|
||||
SetPassword(string(opHash)).
|
||||
SetRealName("质检员一号").
|
||||
SetRole("inspector").
|
||||
SetDept("质检").
|
||||
SaveX(ctx)
|
||||
slog.Info("seed: users created (admin/123456, store1/123456, insp1/123456)")
|
||||
slog.Info("seed: admin user created (admin/123456)")
|
||||
}
|
||||
|
||||
// 3. 演示物料与库存
|
||||
materials, _ := client.Material.Query().Count(ctx)
|
||||
if materials == 0 {
|
||||
mats := []struct {
|
||||
code, name, spec, unit string
|
||||
mode int
|
||||
isBatch, isSerial bool
|
||||
}{
|
||||
{"GJ-BAN-001", "10mm钢板(结构件)", "2000x1000x10", "张", 1, true, false},
|
||||
{"GJ-GANGGUAN-002", "无缝钢管114x8", "114x8x6000", "根", 1, true, false},
|
||||
{"LJ-LUOSI-M16", "M16高强度螺栓", "M16x80 10.9级", "件", 1, true, false},
|
||||
{"JM-ZHOUCHENG-6020", "精密轴承6020", "6020-2RS", "套", 2, false, true},
|
||||
{"JM-DIANJI-750W", "伺服电机750W", "ECMA-C21307", "台", 2, false, true},
|
||||
{"BCP-JIEKOUTI", "接驳体半成品", "-", "件", 2, false, true},
|
||||
}
|
||||
for _, m := range mats {
|
||||
client.Material.Create().
|
||||
SetCode(m.code).
|
||||
SetName(m.name).
|
||||
SetNillableSpec(strPtrNil(m.spec)).
|
||||
SetNillableUnit(strPtrNil(m.unit)).
|
||||
SetManageMode(m.mode).
|
||||
SetIsBatchManaged(m.isBatch).
|
||||
SetIsSerialManaged(m.isSerial).
|
||||
SaveX(ctx)
|
||||
}
|
||||
|
||||
// 演示库存(统一 inventory 表,manage_mode 区分结构件/电气件)
|
||||
today := time.Now().Format("2006-01-02")
|
||||
// 结构件批次(已检合格)
|
||||
client.Inventory.Create().
|
||||
SetManageMode(1).
|
||||
SetMaterialCode("GJ-BAN-001").
|
||||
SetNillableMaterialName(strPtrNil("10mm钢板(结构件)")).
|
||||
SetBatchNo("B2026060100001").
|
||||
SetQuantity(120).SetLockedQty(0).
|
||||
SetNillableProductionDate(strPtrNil("2026-06-01")).
|
||||
SetNillableSupplier(strPtrNil("宝钢供应商")).
|
||||
SetQualityStatus("合格").
|
||||
SetNillableZoneCode(strPtrNil("Z02")).
|
||||
SetStatus("在库").
|
||||
SaveX(ctx)
|
||||
client.Inventory.Create().
|
||||
SetManageMode(1).
|
||||
SetMaterialCode("GJ-GANGGUAN-002").
|
||||
SetNillableMaterialName(strPtrNil("无缝钢管114x8")).
|
||||
SetBatchNo("B2026060200034").
|
||||
SetQuantity(50).SetLockedQty(0).
|
||||
SetNillableProductionDate(strPtrNil(today)).
|
||||
SetNillableSupplier(strPtrNil("天津钢管")).
|
||||
SetQualityStatus("未检").
|
||||
SetNillableZoneCode(strPtrNil("Z01")).
|
||||
SetStatus("在库").
|
||||
SaveX(ctx)
|
||||
|
||||
// 电气件 SN(在库,合格)
|
||||
for i := 1; i <= 5; i++ {
|
||||
client.Inventory.Create().
|
||||
SetManageMode(2).
|
||||
SetMaterialCode("JM-ZHOUCHENG-6020").
|
||||
SetNillableMaterialName(strPtrNil("精密轴承6020")).
|
||||
SetSnCode("SN20260601001" + itoaPad(i)).
|
||||
SetQuantity(1).SetLockedQty(0).
|
||||
SetQualityStatus("合格").
|
||||
SetNillableZoneCode(strPtrNil("Z02")).
|
||||
SetStatus("在库").
|
||||
SaveX(ctx)
|
||||
}
|
||||
slog.Info("seed: demo materials & stock created")
|
||||
}
|
||||
|
||||
// 4. 接驳台主数据 DOCK01~20(产线, DOCKn↔工位n) + DOCK21(库房)
|
||||
// 3. 接驳台主数据 DOCK01~20(产线, DOCKn↔工位n) + DOCK21(库房)
|
||||
docks, _ := client.Dock.Query().Count(ctx)
|
||||
if docks == 0 {
|
||||
for i := 1; i <= 20; i++ {
|
||||
@@ -162,31 +83,3 @@ func SeedIfEmpty(client *ent.Client) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func strPtrNil(s string) *string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
func itoaPad(n int) string {
|
||||
base := "000"
|
||||
return base[:len(base)-lenOfInt(n)] + intToStr(n)
|
||||
}
|
||||
|
||||
func lenOfInt(n int) int {
|
||||
c := 1
|
||||
for n >= 10 {
|
||||
n /= 10
|
||||
c++
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func intToStr(n int) string {
|
||||
if n < 10 {
|
||||
return string(rune('0' + n))
|
||||
}
|
||||
return intToStr(n/10) + string(rune('0'+n%10))
|
||||
}
|
||||
|
||||
@@ -115,140 +115,146 @@ var seedRoleMeta = []struct {
|
||||
{"inspector", "质检员", "质量检验相关"},
|
||||
}
|
||||
|
||||
// seedRbacHandler POST /api/rbac/seed 幂等初始化权限与角色(已存在按 code 跳过)
|
||||
func seedRbacHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
|
||||
createdP, createdR := 0, 0
|
||||
for _, p := range seedPermissions {
|
||||
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).
|
||||
// SeedRBAC 幂等初始化权限与角色(已存在按 code 跳过/补齐),返回(新增权限数, 新增角色数, 绑定历史用户数)。
|
||||
// 接收最小 ServiceContext(命令行 reset-all/seed 仅需 EntClient + EventLog),与 HTTP seedRbacHandler 共用同一逻辑,
|
||||
// 保证两条路径灌入完全一致的权限模型。operator 为操作人(HTTP 传登录名,命令行传 "system")。
|
||||
func SeedRBAC(ctx *svc.ServiceContext, operator string) (createdP, createdR, bound int) {
|
||||
for _, p := range seedPermissions {
|
||||
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).
|
||||
SetPath(p.Path).SetIcon(p.Icon).SetSort(p.Sort).
|
||||
Save(ctx0())
|
||||
if err == nil {
|
||||
createdP++
|
||||
}
|
||||
continue
|
||||
}
|
||||
codes := seedRoleCodes()
|
||||
bound := 0
|
||||
for _, m := range seedRoleMeta {
|
||||
rl, err := ctx.EntClient.Role.Query().
|
||||
Where(role.CodeEQ(m.Code)).Only(ctx0())
|
||||
if err != nil {
|
||||
// 内置角色不存在则创建
|
||||
rl, err = ctx.EntClient.Role.Create().
|
||||
SetCode(m.Code).SetName(m.Name).SetRemark(m.Remark).
|
||||
SetPermissionCodes(codes[m.Code]).
|
||||
Save(ctx0())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
createdR++
|
||||
}
|
||||
// 把「role 字符串 == 该角色 code」的历史用户绑定到 role_id(幂等,仅绑定 role_id 为空者)
|
||||
users, _ := ctx.EntClient.User.Query().
|
||||
Where(user.RoleEQ(m.Code), user.RoleIDIsNil()).All(ctx0())
|
||||
for _, u := range users {
|
||||
if _, err := ctx.EntClient.User.UpdateOneID(u.ID).SetRoleID(rl.ID).Save(ctx0()); err == nil {
|
||||
bound++
|
||||
}
|
||||
}
|
||||
_, 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 {
|
||||
createdP++
|
||||
}
|
||||
// 迁移历史角色:把“可查看的菜单”自动补上对应导出码(精细化:原全局 *: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())
|
||||
}
|
||||
// 新增菜单权限的自动补授:seed 新增了 MENU 权限码(如新上线「退库确认」),
|
||||
// 但内置角色已存在、不会被上面覆盖 → 这里把内置角色(非 admin)按 seedRoleCodes
|
||||
// 的期望集合做「并集补授」,保证新菜单对既有角色立即可见。仅对内置三角色生效,
|
||||
// 不触碰自建角色(避免影响管理员的自定义授权)。
|
||||
builtin := map[string]bool{"admin": true, "operator": true, "inspector": true}
|
||||
for _, m := range seedRoleMeta {
|
||||
if m.Code == "admin" || !builtin[m.Code] {
|
||||
continue
|
||||
}
|
||||
rl, err := ctx.EntClient.Role.Query().Where(role.CodeEQ(m.Code)).Only(ctx0())
|
||||
}
|
||||
codes := seedRoleCodes()
|
||||
for _, m := range seedRoleMeta {
|
||||
rl, err := ctx.EntClient.Role.Query().
|
||||
Where(role.CodeEQ(m.Code)).Only(ctx0())
|
||||
if err != nil {
|
||||
// 内置角色不存在则创建
|
||||
rl, err = ctx.EntClient.Role.Create().
|
||||
SetCode(m.Code).SetName(m.Name).SetRemark(m.Remark).
|
||||
SetPermissionCodes(codes[m.Code]).
|
||||
Save(ctx0())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
want := codes[m.Code]
|
||||
set := map[string]bool{}
|
||||
for _, c := range rl.PermissionCodes {
|
||||
set[c] = true
|
||||
createdR++
|
||||
}
|
||||
// 把「role 字符串 == 该角色 code」的历史用户绑定到 role_id(幂等,仅绑定 role_id 为空者)
|
||||
users, _ := ctx.EntClient.User.Query().
|
||||
Where(user.RoleEQ(m.Code), user.RoleIDIsNil()).All(ctx0())
|
||||
for _, u := range users {
|
||||
if _, err := ctx.EntClient.User.UpdateOneID(u.ID).SetRoleID(rl.ID).Save(ctx0()); err == nil {
|
||||
bound++
|
||||
}
|
||||
changed := false
|
||||
for _, c := range want {
|
||||
if !set[c] {
|
||||
set[c] = true
|
||||
}
|
||||
}
|
||||
// 迁移历史角色:把“可查看的菜单”自动补上对应导出码(精细化:原全局 *: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
|
||||
}
|
||||
merged := make([]string, 0, len(set))
|
||||
for c := range set {
|
||||
merged = append(merged, c)
|
||||
}
|
||||
_, _ = ctx.EntClient.Role.UpdateOneID(rl.ID).SetPermissionCodes(merged).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())
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
// 清理已废弃的全局 *:export 权限行(迁移后无角色引用,幂等)
|
||||
_, _ = ctx.EntClient.Permission.Delete().Where(permission.CodeEQ("*:export")).Exec(ctx0())
|
||||
newCodes := make([]string, 0, len(set))
|
||||
for c := range set {
|
||||
newCodes = append(newCodes, c)
|
||||
}
|
||||
_, _ = ctx.EntClient.Role.UpdateOneID(rl.ID).SetPermissionCodes(newCodes).Save(ctx0())
|
||||
}
|
||||
// 新增菜单权限的自动补授:seed 新增了 MENU 权限码(如新上线「退库确认」),
|
||||
// 但内置角色已存在、不会被上面覆盖 → 这里把内置角色(非 admin)按 seedRoleCodes
|
||||
// 的期望集合做「并集补授」,保证新菜单对既有角色立即可见。仅对内置三角色生效,
|
||||
// 不触碰自建角色(避免影响管理员的自定义授权)。
|
||||
builtin := map[string]bool{"admin": true, "operator": true, "inspector": true}
|
||||
for _, m := range seedRoleMeta {
|
||||
if m.Code == "admin" || !builtin[m.Code] {
|
||||
continue
|
||||
}
|
||||
rl, err := ctx.EntClient.Role.Query().Where(role.CodeEQ(m.Code)).Only(ctx0())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
want := codes[m.Code]
|
||||
set := map[string]bool{}
|
||||
for _, c := range rl.PermissionCodes {
|
||||
set[c] = true
|
||||
}
|
||||
changed := false
|
||||
for _, c := range want {
|
||||
if !set[c] {
|
||||
set[c] = true
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
merged := make([]string, 0, len(set))
|
||||
for c := range set {
|
||||
merged = append(merged, c)
|
||||
}
|
||||
_, _ = ctx.EntClient.Role.UpdateOneID(rl.ID).SetPermissionCodes(merged).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())
|
||||
|
||||
// 幂等 seed 只在发生真实变更(新增权限/角色/绑定历史用户)时记录日志,避免每次页面加载刷屏
|
||||
if createdP > 0 || createdR > 0 || bound > 0 {
|
||||
ctx.EventLog.Write(ctx0(), "rbac.seed", r.Header.Get("X-Username"), "rbac", "",
|
||||
"RBAC 种子初始化", map[string]any{"permissions": createdP, "roles": createdR, "bound": bound})
|
||||
}
|
||||
logx.Infof("rbac seed: 新增权限 %d 条、角色 %d 个、绑定历史用户 %d 个", createdP, createdR, bound)
|
||||
// 幂等 seed 只在发生真实变更(新增权限/角色/绑定历史用户)时记录日志,避免每次页面加载刷屏
|
||||
if createdP > 0 || createdR > 0 || bound > 0 {
|
||||
ctx.EventLog.Write(ctx0(), "rbac.seed", operator, "rbac", "",
|
||||
"RBAC 种子初始化", map[string]any{"permissions": createdP, "roles": createdR, "bound": bound})
|
||||
}
|
||||
logx.Infof("rbac seed: 新增权限 %d 条、角色 %d 个、绑定历史用户 %d 个", createdP, createdR, bound)
|
||||
return
|
||||
}
|
||||
|
||||
// seedRbacHandler POST /api/rbac/seed 幂等初始化权限与角色(复用 SeedRBAC,与命令行 reset-all/seed 同源)
|
||||
func seedRbacHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
|
||||
createdP, createdR, bound := SeedRBAC(ctx, r.Header.Get("X-Username"))
|
||||
ok(w, map[string]any{"createdPermissions": createdP, "createdRoles": createdR, "boundUsers": bound})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
WMS系统
|
||||
1.仓储作业-入库管理
|
||||
物料和检测单号为必输项,但是前端显示规格不一致,物料未填写时,输入框会爆红,提示请选择物料,检测单号未填写时,输入框不会爆红,但是会有弹窗提示。建议如果必填项未填写的话,输入框既爆红,又会有弹窗提示。
|
||||
2.仓储作业-入库管理
|
||||
结构件入库,电气件 SN 入库,其他入库,这三个都可以添加一个附件上传的功能,就是入库记录页面的附件上传。入库记录页面的附件上传现在存在bug,上传附件后点击查看显示{"code":401,"message":"未登录或 token 缺失"}
|
||||
3.区域维护设计的不合理,我说一下当前的展示效果,每次新建一个区域编码的时候,需要输入区域编码和区域名称,这时候问题就来了,一个区域不止一个货架号吧,但是当前页面只能新建一个货架号,然后选择第几层,选择位置号,一个货架中不止一层,每一次不止一个位置号,这样的话,现在这个区域维护页面需要显示好多重复的区域编码和区域名称,你需要思考一下这个问题,这个地方应该怎么进行优化。然后还存在一个联动问题,区域维护新建好,或者编辑一个区域之后,入库管理页面,在选择区域的时候不会显示刚刚新建的内容,需要刷新一下页面才能显示
|
||||
4.仓储作业-入库管理
|
||||
电气件SN入库把这个名称修改为电气件入库
|
||||
5.仓储作业-入库管理
|
||||
结构类入库:这部分现在存在问题,在选择物料的时候,会出现不止结构件类型的,还出现了,应该在其他入库类型的选项
|
||||
6.wms工单备料台账,存在问题,mes未实现把数据推送到wms工单备料台账,导致wms工单备料台账中没有数据,然后间接导致不能进行出库操作,不能闭环跑完整个流程
|
||||
7.仓储作业-出库管理
|
||||
出库区域选择的时候全是undefined,点击出库会有错误,参数错误: json: unknown field "outboundType"
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user