init: initial commit

This commit is contained in:
2026-10-07 09:28:43 +08:00
commit a68a4ef411
251 changed files with 38279 additions and 0 deletions
+146
View File
@@ -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("服务已停止")
}