//go:build windows package main import ( "embed" "errors" "flag" "fmt" "io/fs" "log/slog" "net/http" "os" "os/exec" "os/signal" "path/filepath" "syscall" "time" "bj_power_mes/internal/handler" "bj_power_mes/internal/middleware" xhttp "bj_power_mes/common/httpx" xlog "bj_power_mes/common/logx" "bj_power_mes/internal/config" "bj_power_mes/internal/svc" "github.com/dromara/carbon/v2" "github.com/getlantern/systray" "github.com/pkg/browser" "github.com/zeromicro/go-zero/core/conf" "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/rest" "github.com/zeromicro/go-zero/rest/httpx" "golang.org/x/sys/windows" ) const AppName = "bj_power_mes" var configFile = flag.String("f", "etc/bj_power_mes-api.yaml", "the config file") var AppPath, _ = filepath.Abs(os.Args[0]) var baseUrl string //go:embed public var public embed.FS //go:embed app.ico var icon []byte func main() { flag.Parse() // 业务影响:所有时间显示使用北京时? carbon.SetDefault(carbon.Default{ Layout: carbon.DateTimeLayout, Timezone: carbon.PRC, Locale: "zh-CN", WeekStartsAt: carbon.Monday, }) var c config.Config conf.MustLoad(*configFile, &c) baseUrl = fmt.Sprintf("http://127.0.0.1:%d", c.Port) logger := xlog.NewLogger(c.Logger) xlog.SetDefault(logger) logx.SetWriter(logger) // 业务影响:防止同时启动多个实例,避免端口冲突?PLC 连接冲突 mutex, err := createMutex(AppName) if err != nil { if errors.Is(err, ErrAlreadyExists) { _ = browser.OpenURL(baseUrl) return } MessageBox("错误", err.Error()) return } defer procCloseHandle.Call(mutex) // 业务影响:首次运行时自动创建桌面和开始菜单快捷方? createShortcuts() // 业务影响:自动写入注册表开机启动,无需用户手动勾? if err := setAutoStart(AppName, AppPath); err != nil { slog.Warn("开机启动设置失? " + err.Error()) } // 业务影响:初始化数据库连接、PLC 连接 svcCtx := svc.NewServiceContext(c) spaHandler := xhttp.NewNotFoundHandler(http.FS(mustSub(public, "public"))) server := rest.MustNewServer( c.RestConf, rest.WithCors("*"), rest.WithNotFoundHandler(spaHandler), ) logx.DisableStat() logx.SetWriter(logger) defer server.Stop() handler.RegisterHandlers(server, svcCtx) // 写操作审计中间件(兜底覆盖系统管理类 CRUD 等所有写接口? server.Use(middleware.WriteAudit(svcCtx)) // 业务影响:统一错误响应格式,前端不用处理多种错误格? httpx.SetValidator(xhttp.NewValidator()) httpx.SetOkHandler(xhttp.OkJsonCtx) httpx.SetErrorHandlerCtx(xhttp.ErrorCtx) fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) slog.Info(fmt.Sprintf("Starting server at %s:%d...", c.Host, c.Port)) go server.Start() // 等待服务器启动完成后再打开浏览器,避免 404 go func() { for i := 0; i < 50; i++ { resp, err := http.Get(baseUrl) if err == nil { resp.Body.Close() _ = browser.OpenURL(baseUrl) return } time.Sleep(200 * time.Millisecond) } slog.Warn("server 启动超时,跳过自动打开浏览?) }() // 业务影响:Windows 后台运行,通过托盘图标控制退?打开网页 systray.Run(onReady, func() { slog.Info("onExit") }) } func mustSub(fsFS embed.FS, dir string) fs.FS { sub, err := fs.Sub(fsFS, dir) if err != nil { panic(err) } return sub } // onReady 系统托盘:用户可打开网页、切换开机自启(需管理员权限)、退出程?func onReady() { systray.SetIcon(icon) systray.SetTemplateIcon(icon, icon) systray.SetTitle("北京电力管控系统") systray.SetTooltip("北京电力管控系统") mOpen := systray.AddMenuItem("打开管理页面", "") systray.AddSeparator() mAutoStart := systray.AddMenuItem("开机启?, "") mQuit := systray.AddMenuItem("退?, "退出服?) // 业务影响:读取注册表恢复上次的开机启动设置状? if isAutoStartEnabled(AppName) { mAutoStart.Check() } signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) go func() { for { select { case s := <-signalChan: slog.Info("receive signal: " + s.String()) systray.Quit() return case <-mQuit.ClickedCh: systray.Quit() return case <-mOpen.ClickedCh: err := browser.OpenURL(baseUrl) if err != nil { logx.Error(err.Error()) } case <-mAutoStart.ClickedCh: if mAutoStart.Checked() { if err := removeAutoStart(AppName); err != nil { if errors.Is(err, windows.ERROR_ACCESS_DENIED) { MessageBox("错误", "请用管理员权限运行此程序?) } else { MessageBox("错误", err.Error()) } slog.Error("设置失败?" + err.Error()) } else { mAutoStart.Uncheck() slog.Info("已经成功取消自动运行?) } } else { if err := setAutoStart(AppName, AppPath); err != nil { if errors.Is(err, windows.ERROR_ACCESS_DENIED) { MessageBox("错误", "请用管理员权限运行此程序?) } else { MessageBox("错误", err.Error()) } slog.Error("设置失败?" + err.Error()) } else { mAutoStart.Check() slog.Info("已经成功设置为自动运行?) } } } } }() } // createShortcuts 首次运行时自动创建桌面和开始菜单快捷方式?// 如果快捷方式已存在则跳过,创建失败不阻塞程序启动?func createShortcuts() { shortcutName := "北京电力管控系统" exePath, err := os.Executable() if err != nil { return } exeDir := filepath.Dir(exePath) iconPath := filepath.Join(exeDir, "app.ico") // 桌面快捷方式 desktopDir, _ := os.UserHomeDir() desktopPath := filepath.Join(desktopDir, "Desktop", shortcutName+".lnk") makeShortcut(desktopPath, exePath, exeDir, iconPath) // 开始菜单快捷方? startMenuPath := filepath.Join(os.Getenv("APPDATA"), "Microsoft", "Windows", "Start Menu", "Programs", shortcutName+".lnk") makeShortcut(startMenuPath, exePath, exeDir, iconPath) } // makeShortcut 通过 PowerShell 创建 .lnk 快捷方式?// 如果文件已存在则跳过,创建失败不阻塞?func makeShortcut(shortcutPath, targetPath, workDir, iconPath string) { if _, err := os.Stat(shortcutPath); err == nil { return // 已存在,跳过 } script := fmt.Sprintf( `$s=(New-Object -ComObject WScript.Shell).CreateShortcut('%s');$s.TargetPath='%s';$s.WorkingDirectory='%s';$s.IconLocation='%s';$s.Save()`, shortcutPath, targetPath, workDir, iconPath, ) cmd := exec.Command("powershell", "-ExecutionPolicy", "Bypass", "-NoProfile", "-Command", script) _ = cmd.Run() // 创建失败不阻塞启?}