init: initial commit
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
// 我们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.NumericAsString = cfg.PHPCompat < "8.1"
|
||||
if err := db.InitSchema(); err != nil {
|
||||
slog.Error("数据库初始化失败", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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("服务已停止")
|
||||
}
|
||||
Reference in New Issue
Block a user