数据源:百度百科开放接口 BaikeLemmaCardApi(无反爬,返回结构化 JSON) 与「历史上的今天」(维基)完全解耦,维基代码原样保留可随时恢复: 服务端: - WikiEntry 增加 image(配图)和 extra(结构化字段 JSON)字段 - 新建 BaikeTermSyncLog 独立日志表(不污染维基 WikiSyncLog) - 新建 BaikeTermSyncService 独立同步服务(24 节气,间隔 1.2s) - 启动时缺详情自动补齐,之后按配置间隔定时增量刷新 - 管理 API:GET/POST /admin/api/wiki/term-sync* 管理后台: - 百科条目 Tab 顶部加节气同步卡片(X/24 + 状态 + 同步按钮) - 条目编辑器加配图 URL(含预览)和节气扩展字段编辑 小程序: - 百科页展开详情图文展示:配图 + 简介 + 物候/习俗/养生等分块 本地验证:24/24 节气抓取成功,image/extra 全部填充,维基数据不受影响
175 lines
5.6 KiB
Go
175 lines
5.6 KiB
Go
package main
|
||
|
||
import (
|
||
"embed"
|
||
"io/fs"
|
||
"log"
|
||
"net/http"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/gouki/lunar-server/internal/config"
|
||
"github.com/gouki/lunar-server/internal/handler"
|
||
"github.com/gouki/lunar-server/internal/middleware"
|
||
"github.com/gouki/lunar-server/internal/service"
|
||
"github.com/joho/godotenv"
|
||
)
|
||
|
||
// 版本信息由 CI 通过 ldflags 注入:
|
||
// -X main.Version=... -X main.BuildTime=... -X main.CommitSha=...
|
||
var (
|
||
Version = "dev"
|
||
BuildTime = "unknown"
|
||
CommitSha = "unknown"
|
||
)
|
||
|
||
// 内嵌图片资源(许愿树背景图等),随二进制一起部署
|
||
//
|
||
//go:embed assets/images
|
||
var assetsFS embed.FS
|
||
|
||
func main() {
|
||
// 加载本地环境变量文件(生产环境由容器注入,忽略缺失;../.env.local 兼容从 server/ 目录启动)
|
||
_ = godotenv.Load(".env.local", "../.env.local", ".env")
|
||
|
||
// 将构建时注入的版本信息传递给 handler
|
||
handler.Version = Version
|
||
handler.BuildTime = BuildTime
|
||
handler.CommitSha = CommitSha
|
||
log.Printf("lunar-server %s (commit %s, built at %s)", Version, CommitSha, BuildTime)
|
||
|
||
// 加载配置
|
||
cfg := config.Load()
|
||
|
||
// 初始化数据库
|
||
if err := config.InitDB(cfg); err != nil {
|
||
log.Fatalf("Failed to initialize database: %v", err)
|
||
}
|
||
|
||
// 启动机器人许愿定时任务
|
||
wishService := service.NewWishService()
|
||
wishService.StartRobotWishJob()
|
||
|
||
// 启动维基百科同步定时任务(历史上的今天;首次启动数据为空时自动全量抓取)
|
||
handler.WikiSync = service.NewWikiSyncService(config.GetDB())
|
||
handler.WikiSync.StartScheduler(cfg.Wiki.SyncIntervalDays, cfg.Wiki.SyncHour)
|
||
|
||
// 启动节气详情同步定时任务(百度百科;独立于维基同步,首次启动缺详情时自动补齐)
|
||
handler.BaikeTermSync = service.NewBaikeTermSyncService(config.GetDB())
|
||
handler.BaikeTermSync.StartScheduler(cfg.Wiki.SyncIntervalDays, cfg.Wiki.SyncHour)
|
||
|
||
// 设置运行模式
|
||
if cfg.Server.Env == "production" {
|
||
gin.SetMode(gin.ReleaseMode)
|
||
}
|
||
|
||
// 创建路由(gin.Default 自带访问日志与 Recovery)
|
||
r := gin.Default()
|
||
|
||
// 中间件
|
||
r.Use(middleware.CORS())
|
||
|
||
// 静态文件与管理后台模板
|
||
r.Static("/static", "./web/static")
|
||
r.StaticFile("/", "./web/index.html")
|
||
r.LoadHTMLFiles("./web/index.html")
|
||
|
||
// 内嵌图片资源(许愿树背景图等)
|
||
if imagesFS, err := fs.Sub(assetsFS, "assets/images"); err == nil {
|
||
r.GET("/images/*path", gin.WrapH(http.StripPrefix("/images", http.FileServer(http.FS(imagesFS)))))
|
||
}
|
||
|
||
// API 路由
|
||
api := r.Group("/api")
|
||
{
|
||
// 版本信息(无需认证)
|
||
api.GET("/version", handler.GetVersion)
|
||
|
||
// 用户相关
|
||
user := api.Group("/user")
|
||
{
|
||
user.POST("/login", handler.UserLogin)
|
||
user.POST("/logout", handler.UserLogout)
|
||
user.GET("/profile", middleware.Auth(), handler.GetUserProfile)
|
||
user.PUT("/profile", middleware.Auth(), handler.UpdateUserProfile)
|
||
user.POST("/auth", handler.WechatAuth)
|
||
// 生辰档案(八字绑定,每用户免费 3 个)
|
||
user.GET("/birth-profiles", middleware.Auth(), handler.GetBirthProfiles)
|
||
user.POST("/birth-profiles", middleware.Auth(), handler.CreateBirthProfile)
|
||
user.DELETE("/birth-profiles/:id", middleware.Auth(), handler.DeleteBirthProfile)
|
||
}
|
||
|
||
// 支付相关
|
||
pay := api.Group("/pay")
|
||
{
|
||
pay.POST("/create", middleware.Auth(), handler.CreateOrder)
|
||
pay.POST("/notify", handler.PayNotify)
|
||
pay.GET("/status/:orderId", handler.GetPayStatus)
|
||
}
|
||
|
||
// 订单相关
|
||
order := api.Group("/order")
|
||
{
|
||
order.GET("/list", middleware.Auth(), handler.GetOrderList)
|
||
order.GET("/:id", middleware.Auth(), handler.GetOrderDetail)
|
||
order.POST("/cancel/:id", middleware.Auth(), handler.CancelOrder)
|
||
}
|
||
|
||
// 许愿相关
|
||
wish := api.Group("/wish")
|
||
{
|
||
wish.GET("/trees", handler.GetWishTrees)
|
||
wish.GET("/tree", handler.GetWishTree)
|
||
wish.GET("/tree/:id/wishes", handler.GetTreeWishes)
|
||
wish.POST("/create", middleware.Auth(), handler.CreateWish)
|
||
wish.GET("/list", handler.GetWishList)
|
||
wish.GET("/:id", handler.GetWishDetail)
|
||
wish.DELETE("/:id", middleware.Auth(), handler.DeleteWish)
|
||
wish.GET("/products", handler.GetWishProducts)
|
||
}
|
||
|
||
// 百科/历史上的今天(无需认证)
|
||
wiki := api.Group("/wiki")
|
||
{
|
||
wiki.GET("/entries", handler.GetWikiEntries)
|
||
wiki.GET("/on-this-day", handler.GetOnThisDay)
|
||
}
|
||
}
|
||
|
||
// 管理后台路由(Inertia.js)
|
||
admin := r.Group("/admin")
|
||
{
|
||
// 登录/登出接口无需认证;未配置 ADMIN_PASSWORD 时登录返回 503
|
||
admin.POST("/login", handler.AdminLogin)
|
||
admin.POST("/logout", handler.AdminLogout)
|
||
|
||
authed := admin.Group("")
|
||
authed.Use(middleware.AdminAuth())
|
||
{
|
||
authed.GET("/", handler.AdminDashboard)
|
||
authed.GET("/users", handler.AdminUsers)
|
||
authed.GET("/orders", handler.AdminOrders)
|
||
authed.GET("/wishes", handler.AdminWishes)
|
||
authed.GET("/wiki", handler.AdminWiki)
|
||
authed.GET("/settings", handler.AdminSettings)
|
||
|
||
// 百科管理 API
|
||
authed.GET("/api/wiki/entries", handler.AdminWikiEntries)
|
||
authed.POST("/api/wiki/entries", handler.AdminWikiEntryCreate)
|
||
authed.PUT("/api/wiki/entries/:id", handler.AdminWikiEntryUpdate)
|
||
authed.DELETE("/api/wiki/entries/:id", handler.AdminWikiEntryDelete)
|
||
authed.GET("/api/wiki/sync-status", handler.AdminWikiSyncStatus)
|
||
authed.POST("/api/wiki/sync", handler.AdminWikiSyncTrigger)
|
||
// 节气详情同步(百度百科,独立于维基)
|
||
authed.GET("/api/wiki/term-sync-status", handler.AdminTermSyncStatus)
|
||
authed.POST("/api/wiki/term-sync", handler.AdminTermSyncTrigger)
|
||
}
|
||
}
|
||
|
||
// 启动服务器
|
||
addr := ":" + cfg.Server.Port
|
||
log.Printf("Server starting on %s", addr)
|
||
if err := r.Run(addr); err != nil {
|
||
log.Fatalf("Failed to start server: %v", err)
|
||
}
|
||
}
|