Files
bj_power/bj_power_mes/bj_power_mes.go
T
SunYF d9d4155de6 feat: 为MES和工作站服务添加启动后自动打开浏览器功能
优化WMS客户端URL处理逻辑,移除尾斜杠避免请求404错误
更新工作站配置默认工位号为1
新增数据库字段支持工位页签配置
删除WMS端未完成的权限树形化和范围验证测试文件
删除物料档案对齐方案文档

新增自动打开浏览器的跨平台实现,服务启动后等待1秒后打开管理后台,提升使用便捷性
2026-09-24 17:27:24 +08:00

251 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bufio"
"context"
"crypto/subtle"
"embed"
"flag"
"fmt"
"io/fs"
"net/http"
"os"
"os/exec"
"path"
"runtime"
"strings"
"time"
"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)
})
}
// openBrowser 用系统默认浏览器打开 url(Windows/macOS/Linux)
func openBrowser(url string) {
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
case "darwin":
cmd = exec.Command("open", url)
default:
cmd = exec.Command("xdg-open", url)
}
if err := cmd.Start(); err != nil {
gologx.Errorf("自动打开浏览器失败(%s): %v", url, err)
}
}
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)
// 启动后自动打开管理后台(等 1 秒让端口就绪;Host=0.0.0.0 时用 127.0.0.1 访问)
go func() {
time.Sleep(time.Second)
openBrowser(fmt.Sprintf("http://127.0.0.1:%d", c.Port))
}()
server.Start()
}