Files
bj_power/bj_power_wms/bj_power_wms.go
T
SunYF 5f68e77d5f feat(cli): 添加命令行敏感操作门禁和数据库管理功能
- 添加 migrate、seed、reset-all 命令行子命令
- 实现管理员密码门禁机制保护敏感操作
- 添加数据库表结构迁移和种子数据初始化功能
- 实现清空并重建全部表的 reset-all 功能
- 重构种子数据只包含系统必需基础数据,移除演示数据
- 添加 RBAC 权限角色初始化的命令行支持
- 配置命令行密码验证和操作日志记录功能
2026-09-20 15:38:45 +08:00

255 lines
7.6 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"
"crypto/subtle"
"embed"
"flag"
"fmt"
"io/fs"
"net/http"
"os"
"os/exec"
"path"
"runtime"
"strings"
"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"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/rest"
)
// 前端静态资源(Vue 构建产物,由 frontend/ npm run build 输出到 web/static)。
// 原由 bj_power_wms_client(8891) 托管并反代 API2026-09-07 合并进主服务:
// 一个 exe、一个端口(8890)同时提供页面与 API。
//
//go:embed all:web/static
var webFS embed.FS
var configFile = flag.String("f", "etc/bj_power_wms-api.yaml", "the config file")
// newStaticHandler 嵌入式静态文件:命中则直接返回;未命中的非 /api 路径
// 回退到 index.htmlSPA 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()
}
// 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()
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))
}
// 命令行子命令:DDL/删库/种子等敏感操作一律显式触发 + 管理员密码门禁,
// 绝不随服务启动自动执行(对齐 MESmigrate / 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 路径走嵌入式静态资源
//(/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.RegisterHandlers(server, ctx)
// 磁盘空间监控:每天 07:00 检测附件根目录剩余空间(<20% 告警、<10% 提示清理)
handler.StartDiskMonitor(ctx)
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)
// 服务起来后自动打开浏览器访问前端(本机开发便利;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))
}()
}
server.Start()
}