package main import ( "bufio" "context" "crypto/subtle" "embed" "flag" "fmt" "io/fs" "net/http" "os" "path" "strings" "bj_power_mes/common/logx" "bj_power_mes/internal/config" "bj_power_mes/internal/db" "bj_power_mes/internal/eventlog" "bj_power_mes/internal/handler" "bj_power_mes/internal/logic" "bj_power_mes/internal/svc" "github.com/zeromicro/go-zero/core/conf" gologx "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/rest" ) // 前端静态资源(Vue 构建产物:frontend/ npm run build 输出到 public/)。 // 8888 一个端口同时提供管理后台页面与 API。 // //go:embed all:public var webFS embed.FS var configFile = flag.String("f", "etc/bj_power_mes-api.yaml", "the config file") // 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 一致的密码才放行;校验失败记录日志并终止,绝不继续执行。 func adminGate(c config.Config) bool { pwd := promptPassword("请输入管理员密码以执行 [" + flag.Arg(0) + "]: ") if subtle.ConstantTimeCompare([]byte(pwd), []byte(c.Cli.AdminPassword)) != 1 { gologx.Errorf("敏感命令 [%s] 因管理员密码校验失败被终止", flag.Arg(0)) fmt.Println("管理员密码不正确,已终止该操作。") return false } gologx.Infof("敏感命令 [%s] 已通过管理员密码校验", flag.Arg(0)) return true } // newSvc 最小 ServiceContext(CLI 场景:只连库 + 事件日志写入器) func newSvc(c config.Config) *svc.ServiceContext { client, _ := db.MustNewDB(c.Database) return &svc.ServiceContext{ Config: c, EntClient: client, EventLog: eventlog.NewWriter(client), } } // runMigrate 建库(若不存在)+ 建表/补列/清理旧结构(幂等,不删业务数据) func runMigrate(c config.Config) { if err := db.EnsureDB(c.Database); err != nil { gologx.Errorf("ensure database failed: %v", err) os.Exit(1) } client, sqlDB := db.MustNewDB(c.Database) defer client.Close() defer sqlDB.Close() if err := db.EnsureSchema(client, sqlDB); err != nil { gologx.Errorf("migrate failed: %v", err) os.Exit(1) } fmt.Println("migrate: 数据库结构迁移成功(建表/补列/清理退役结构,未删除业务数据)。") } // runSeed 建库建表(若不存在)+ 灌基础种子(工位/接驳台/权限等,不删数据) func runSeed(c config.Config) { if err := db.EnsureDB(c.Database); err != nil { gologx.Errorf("ensure database failed: %v", err) os.Exit(1) } client, sqlDB := db.MustNewDB(c.Database) defer client.Close() defer sqlDB.Close() if err := db.EnsureSchema(client, sqlDB); err != nil { gologx.Errorf("seed migrate failed: %v", err) os.Exit(1) } s := logic.New(newSvc(c)) if err := s.Seed(context.Background()); err != nil { gologx.Errorf("seed failed: %v", err) os.Exit(1) } fmt.Println("seed: 基础数据已对齐(工位/接驳台/权限等)。") } // runResetAll 清空并重建全部表 + 重灌全部种子。此操作不可恢复! func runResetAll(c config.Config) { if err := db.EnsureDB(c.Database); err != nil { gologx.Errorf("ensure database failed: %v", err) os.Exit(1) } if err := db.DropAllTables(c.Database); err != nil { gologx.Errorf("reset-all drop failed: %v", err) os.Exit(1) } client, sqlDB := db.MustNewDB(c.Database) defer client.Close() defer sqlDB.Close() if err := db.EnsureSchema(client, sqlDB); err != nil { gologx.Errorf("reset-all migrate failed: %v", err) os.Exit(1) } s := logic.New(newSvc(c)) if err := s.Seed(context.Background()); err != nil { gologx.Errorf("reset-all seed failed: %v", err) os.Exit(1) } fmt.Println("reset-all: 已清空并重建全部表,写入基础数据(管理员 admin/123456)。") } // newStaticHandler 嵌入式静态文件:命中则直接返回;未命中的非 /api 路径 // 回退到 index.html(SPA history 路由),由浏览器端 Vue Router 接管。 func newStaticHandler(fsys embed.FS) http.Handler { sub, err := fs.Sub(fsys, "public") if err != nil { panic(fmt.Sprintf("嵌入静态目录不可用: %v", err)) } fileServer := http.FileServer(http.FS(sub)) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(path.Clean(r.URL.Path), "/") if name == "" { name = "index.html" } if f, openErr := sub.Open(name); openErr == nil { f.Close() fileServer.ServeHTTP(w, r) return } r2 := r.Clone(r.Context()) r2.URL.Path = "/" fileServer.ServeHTTP(w, r2) }) } func main() { flag.Parse() var c config.Config conf.MustLoad(*configFile, &c) // 按天日志:logs/bj_power_mes-2026-01-02.log,保留 30 天(go-zero logx Writer 适配 slog) if c.Logger.Filename != "" { logger := logx.NewLogger(c.Logger) logx.SetDefault(logger) gologx.SetWriter(logger) } // 命令行子命令:DDL/删库/种子等敏感操作一律显式触发 + 管理员密码门禁, // 绝不随服务启动自动执行(migrate / seed / reset-all / serve)。 switch cmd := flag.Arg(0); cmd { case "reset-all": fmt.Println("reset-all: 即将清空并重建 MES 全部表,此操作不可恢复!") 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 路径走嵌入式静态资源 //(/api 下未注册的路径仍返回 404,不回退页面) notFound := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/api" { http.NotFound(w, r) return } staticHandler.ServeHTTP(w, r) }) server := rest.MustNewServer(c.RestConf, rest.WithNotFoundHandler(notFound)) defer server.Stop() ctx := svc.NewServiceContext(c) handler.RegisterRoutes(server, ctx) handler.RegisterProductionRoutes(server, ctx) // 磁盘空间监控:每天 DiskCheckHour 点检测附件根目录剩余空间 handler.StartDiskMonitor(ctx) // 事件日志保留期清理:每小时检查,保留天数在「基础设置」页配置(0=不清理) handler.StartEventLogRetention(ctx) fmt.Printf("Starting bj_power_mes server at %s:%d...\n", c.Host, c.Port) gologx.Infof("Starting bj_power_mes server at %s:%d...", c.Host, c.Port) server.Start() }