Files
past-go/past-go/cmd/server/main.go
T

157 lines
4.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.
// 我们past后端(Go 版):API 与 PHP 版 backend/api/index.php 完全兼容。
package main
import (
"context"
"flag"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
_ "time/tzdata" // Windows 没有系统时区库,内嵌 tzdata 保证 Asia/Shanghai 可用
"past/internal/config"
"past/internal/geo"
"past/internal/handler"
"past/internal/httpapi"
"past/internal/notify"
"past/internal/store"
"past/internal/upload"
"past/internal/wechat"
"past/web"
)
// version 由构建时 -ldflags 注入
var version = "dev"
func main() {
cfgPath := flag.String("config", "config.yaml", "配置文件路径")
showVer := flag.Bool("version", false, "显示版本")
svcCmd := flag.String("service", "", "Windows 服务管理: install | uninstall | start | stop")
flag.Parse()
if *showVer {
fmt.Println("past-server", version)
return
}
if *svcCmd != "" {
if err := serviceControl(*svcCmd, *cfgPath); err != nil {
fmt.Fprintln(os.Stderr, "错误:", err)
os.Exit(1)
}
return
}
// 作为 Windows 服务运行时,工作目录是 System32:配置文件相对路径按可执行文件所在目录解析
if !filepath.IsAbs(*cfgPath) {
if exe, err := os.Executable(); err == nil {
if _, err := os.Stat(*cfgPath); err != nil {
*cfgPath = filepath.Join(filepath.Dir(exe), *cfgPath)
}
}
}
if runAsServiceIfNeeded(func(stop <-chan struct{}) { serve(*cfgPath, stop) }) {
return
}
// 控制台模式:Ctrl+C / SIGTERM 触发优雅退出
stop := make(chan struct{})
go func() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
close(stop)
}()
serve(*cfgPath, stop)
}
// serve 启动全部组件并阻塞到 stop 关闭
func serve(cfgPath string, stop <-chan struct{}) {
// 与 PHP date_default_timezone_set('Asia/Shanghai') 一致
if loc, err := time.LoadLocation("Asia/Shanghai"); err == nil {
time.Local = loc
}
cfg, err := config.Load(cfgPath)
if err != nil {
slog.Error("配置加载失败", "err", err)
os.Exit(1)
}
// 确保必要目录存在(对应 config.php 中的 $dirs)
for _, d := range []string{"data", "data/tmp", "uploads", "uploads/avatars", "uploads/couples", "uploads/users"} {
os.MkdirAll(filepath.Join(cfg.DataDir, d), 0755)
}
// 日志:stdout + data/go-server.log(Windows 服务没有控制台,文件日志是主要途径)
logOut := io.Writer(os.Stdout)
if f, err := os.OpenFile(filepath.Join(cfg.LogDir(), "go-server.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil {
logOut = io.MultiWriter(os.Stdout, f)
defer f.Close()
}
slog.SetDefault(slog.New(slog.NewTextHandler(logOut, &slog.HandlerOptions{Level: slog.LevelInfo})))
db, err := store.Open(cfg.DSN())
if err != nil {
slog.Error("数据库连接失败", "err", err)
os.Exit(1)
}
db.UploadDir = cfg.UploadDir()
db.NumericAsString = cfg.PHPCompat < "8.1"
if err := db.InitSchema(); err != nil {
slog.Error("数据库初始化失败", "err", err)
os.Exit(1)
}
if err := db.MigrateSmokingRecords(); err != nil {
slog.Error("抽烟库存迁移失败", "err", err)
os.Exit(1)
}
if err := db.AdvanceUnbindings(); err != nil {
slog.Error("解绑状态处理失败", "err", err)
os.Exit(1)
}
go db.RunUnbindingWorker()
wx := wechat.New(cfg.Wechat.AppID, cfg.Wechat.Secret, cfg.Wechat.NotifyTemplateID, cfg.DataDir, db)
app := &httpapi.App{
Cfg: cfg,
DB: db,
Wechat: wx,
Geo: geo.New(cfg.TencentMapKey),
Admin: httpapi.NewAdminSessions(),
}
uploader := &upload.Uploader{Dir: cfg.UploadDir(), MaxSize: cfg.MaxUploadMB * 1024 * 1024, Checker: wx}
router := httpapi.NewRouter(app, web.AdminFS())
handler.Register(router, uploader)
slog.Info("已注册 action", "count", len(router.Actions()))
if cfg.NotifyIntervalMin > 0 {
go notify.Run(db, wx, time.Duration(cfg.NotifyIntervalMin)*time.Minute)
}
srv := &http.Server{
Addr: cfg.Listen,
Handler: router.Engine(cfg.Debug),
ReadHeaderTimeout: 30 * time.Second,
ReadTimeout: 180 * time.Second, // 大文件上传
WriteTimeout: 180 * time.Second,
IdleTimeout: 120 * time.Second,
}
go func() {
slog.Info("服务启动", "version", version, "listen", cfg.Listen, "data_dir", cfg.DataDir)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("服务异常退出", "err", err)
os.Exit(1)
}
}()
<-stop
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
srv.Shutdown(ctx)
slog.Info("服务已停止")
}