2026-08-28 15:06:01 +08:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-09-20 15:38:45 +08:00
|
|
|
|
"bufio"
|
|
|
|
|
|
"crypto/subtle"
|
2026-09-07 15:55:30 +08:00
|
|
|
|
"embed"
|
2026-08-28 15:06:01 +08:00
|
|
|
|
"flag"
|
|
|
|
|
|
"fmt"
|
2026-09-07 15:55:30 +08:00
|
|
|
|
"io/fs"
|
|
|
|
|
|
"net/http"
|
2026-09-20 15:38:45 +08:00
|
|
|
|
"os"
|
2026-09-07 15:55:30 +08:00
|
|
|
|
"os/exec"
|
|
|
|
|
|
"path"
|
|
|
|
|
|
"runtime"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
2026-08-28 15:06:01 +08:00
|
|
|
|
|
|
|
|
|
|
"bj_power_wms/internal/config"
|
2026-09-20 15:38:45 +08:00
|
|
|
|
"bj_power_wms/internal/db"
|
|
|
|
|
|
"bj_power_wms/internal/eventlog"
|
2026-08-28 15:06:01 +08:00
|
|
|
|
"bj_power_wms/internal/handler"
|
|
|
|
|
|
"bj_power_wms/internal/logdaily"
|
|
|
|
|
|
"bj_power_wms/internal/svc"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/zeromicro/go-zero/core/conf"
|
|
|
|
|
|
"github.com/zeromicro/go-zero/core/logx"
|
|
|
|
|
|
"github.com/zeromicro/go-zero/rest"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-09-07 15:55:30 +08:00
|
|
|
|
// 前端静态资源(Vue 构建产物,由 frontend/ npm run build 输出到 web/static)。
|
|
|
|
|
|
// 原由 bj_power_wms_client(8891) 托管并反代 API,2026-09-07 合并进主服务:
|
|
|
|
|
|
// 一个 exe、一个端口(8890)同时提供页面与 API。
|
|
|
|
|
|
//
|
|
|
|
|
|
//go:embed all:web/static
|
|
|
|
|
|
var webFS embed.FS
|
|
|
|
|
|
|
2026-08-28 15:06:01 +08:00
|
|
|
|
var configFile = flag.String("f", "etc/bj_power_wms-api.yaml", "the config file")
|
|
|
|
|
|
|
2026-09-07 15:55:30 +08:00
|
|
|
|
// newStaticHandler 嵌入式静态文件:命中则直接返回;未命中的非 /api 路径
|
|
|
|
|
|
// 回退到 index.html(SPA history 路由),由浏览器端 Vue Router 接管。
|
|
|
|
|
|
func newStaticHandler(fsys embed.FS) http.Handler {
|
|
|
|
|
|
sub, err := fs.Sub(fsys, "web/static")
|
|
|
|
|
|
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(仅本机开发使用,服务器部署时 Web.OpenBrowser=false)
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
_ = cmd.Start()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-20 15:38:45 +08:00
|
|
|
|
// 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)。")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-28 15:06:01 +08:00
|
|
|
|
func main() {
|
|
|
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
|
|
|
|
var c config.Config
|
|
|
|
|
|
conf.MustLoad(*configFile, &c)
|
|
|
|
|
|
|
|
|
|
|
|
// 日志按天存储:logs/bj_power_wms-2026-01-02.log,保留 30 天
|
|
|
|
|
|
if c.Log.Mode == "file" {
|
|
|
|
|
|
logx.SetUp(c.Log)
|
|
|
|
|
|
w := logdaily.New(c.Log.Path, c.Name, 30)
|
|
|
|
|
|
defer w.Close()
|
|
|
|
|
|
logx.SetWriter(logx.NewWriter(w))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-20 15:38:45 +08:00
|
|
|
|
// 命令行子命令: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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-07 15:55:30 +08:00
|
|
|
|
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))
|
2026-08-28 15:06:01 +08:00
|
|
|
|
defer server.Stop()
|
|
|
|
|
|
|
|
|
|
|
|
ctx := svc.NewServiceContext(c)
|
|
|
|
|
|
|
|
|
|
|
|
handler.RegisterHandlers(server, ctx)
|
|
|
|
|
|
|
2026-09-19 10:54:15 +08:00
|
|
|
|
// 磁盘空间监控:每天 07:00 检测附件根目录剩余空间(<20% 告警、<10% 提示清理)
|
|
|
|
|
|
handler.StartDiskMonitor(ctx)
|
|
|
|
|
|
|
2026-08-28 15:06:01 +08:00
|
|
|
|
fmt.Printf("Starting bj_power_wms server at %s:%d...\n", c.Host, c.Port)
|
|
|
|
|
|
logx.Infof("Starting bj_power_wms server at %s:%d...", c.Host, c.Port)
|
2026-09-07 15:55:30 +08:00
|
|
|
|
|
|
|
|
|
|
// 服务起来后自动打开浏览器访问前端(本机开发便利;Web.OpenBrowser=false 关闭)
|
|
|
|
|
|
if c.Web.OpenBrowser {
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
time.Sleep(time.Second)
|
|
|
|
|
|
host := c.Host
|
|
|
|
|
|
if host == "0.0.0.0" || host == "" {
|
|
|
|
|
|
host = "127.0.0.1"
|
|
|
|
|
|
}
|
|
|
|
|
|
openBrowser(fmt.Sprintf("http://%s:%d", host, c.Port))
|
|
|
|
|
|
}()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-28 15:06:01 +08:00
|
|
|
|
server.Start()
|
|
|
|
|
|
}
|