This commit is contained in:
toom1996
2026-09-07 00:01:48 +08:00
parent b049104a0b
commit 74ba700598
31 changed files with 1113 additions and 606 deletions

View File

@ -19,8 +19,11 @@ import (
"fashionapi/internal/config"
"fashionapi/internal/database"
"fashionapi/internal/handler"
"fashionapi/internal/middleware"
"fashionapi/internal/pkg/hashid"
"fashionapi/internal/pkg/imgurl"
"fashionapi/internal/pkg/jwt"
"fashionapi/internal/pkg/storage"
"fashionapi/internal/repository"
"fashionapi/internal/router"
"fashionapi/internal/service"
@ -70,25 +73,61 @@ func main() {
// 5. 装配各层依赖
var (
articleRepo = repository.NewArticleRepository(db)
brandRepo = repository.NewBrandRepository(db)
userRepo = repository.NewUserRepository(db)
snapRepo = repository.NewStreetSnapRepository(db)
refreshRepo = repository.NewRefreshTokenRepository(db)
favRepo = repository.NewFavoriteRepository(db)
histRepo = repository.NewHistoryRepository(db)
articleRepo = repository.NewArticleRepository(db)
brandRepo = repository.NewBrandRepository(db)
userRepo = repository.NewUserRepository(db)
snapRepo = repository.NewStreetSnapRepository(db)
refreshRepo = repository.NewRefreshTokenRepository(db)
favRepo = repository.NewFavoriteRepository(db)
histRepo = repository.NewHistoryRepository(db)
ingestRepo = repository.NewIngestRepository(db)
reviewRepo = repository.NewReviewRepository(db)
// 跨表图片引用计数(删除图集时判定七牛孤儿文件用)
mediaRepo = repository.NewMediaRepository(db)
jwtManager = jwt.NewManager(cfg.JWT.Secret, cfg.JWT.ExpireHours)
articleSvc = service.NewArticleService(articleRepo)
// 图片 URL 拼装器:库里只存七牛 key,渲染时按 tier 拼 base_url + 样式。
imgComposer = imgurl.New(cfg.Qiniu.BaseURL, cfg.Qiniu.StyleNormal, cfg.Qiniu.StyleVip, cfg.Qiniu.LocalBase, cfg.Qiniu.AK, cfg.Qiniu.SK, cfg.Qiniu.SignTTLNormal, cfg.Qiniu.SignTTLHD)
// 图片存储:七牛云优先(若启用),失败兜底本地 uploads。
// localUp 同时实现 Uploader 与 Deleter(本地兜底删除);启用七牛时两者均为七牛实现。
localUp *storage.LocalUploader = storage.NewLocalUploader(uploadDir, cfg.Upload.URLPrefix)
imgUp storage.Uploader = localUp
imgDel storage.Deleter = localUp
articleSvc service.ArticleService // 在 ingestSvc 创建后再装配(需注入清理入队器)
brandSvc = service.NewBrandService(brandRepo)
indexSvc = service.NewIndexService(brandRepo)
snapSvc = service.NewStreetSnapService(snapRepo)
indexSvc = service.NewIndexService(brandRepo, imgComposer)
snapSvc service.StreetSnapService // 在 ingestSvc 创建后再装配(需注入清理入队器)
authSvc = service.NewAuthService(userRepo, refreshRepo, jwtManager, cfg.JWT.ExpireHours, cfg.JWT.RefreshExpireHours)
favSvc = service.NewFavoriteService(favRepo)
histSvc = service.NewHistoryService(histRepo)
// 审核:草稿列表 / 通过晋升正式表 / 驳回(后台预览始终高清)
reviewSvc = service.NewReviewService(reviewRepo, mediaRepo, imgComposer, imgDel)
)
// 爬虫入库:入队 + 异步 worker(去重 / 补季节码 / 下载图 / 写草稿表)
// 图片存储:七牛云优先(若启用),失败兜底本地 uploads。
// imgUp / imgDel 已在上方 var 块声明,这里仅在启用七牛时切换为七牛实现。
if cfg.Qiniu.Enabled {
// 注意:上传器不传 base_url(末参传空),Upload 只返回七牛 key;
// 完整可访问 URL 由 imgComposer 在读取时按 tier 拼 base_url + 样式,
// 这样既方便换 CDN 域名,又能做普通/高清分级(见 internal/pkg/imgurl)。
qu := storage.NewQiniuUploader(cfg.Qiniu.AK, cfg.Qiniu.SK, cfg.Qiniu.Bucket, cfg.Qiniu.Zone, "")
imgUp = qu
imgDel = qu
log.Printf("✓ 图片存储: 七牛云 bucket=%s zone=%s(库只存 key,渲染时拼 base_url=%s)", cfg.Qiniu.Bucket, cfg.Qiniu.Zone, cfg.Qiniu.BaseURL)
} else {
log.Printf("✓ 图片存储: 本地 %s", cfg.Upload.URLPrefix)
}
// 爬虫入库服务:注入 mediaRepo(引用计数删孤儿)+ imgDel(七牛/本地删除器)。
// 必须在 articleSvc / snapSvc 之前创建——图集服务删除时需调用它把「清理七牛」异步入队。
ingestSvc := service.NewIngestService(ingestRepo, brandRepo, mediaRepo, imgUp, imgDel, localUp)
// 图集服务:注入 ingestSvc 作为「清理七牛孤儿图」的入队器,使下架/删除异步触发存储清理。
articleSvc = service.NewArticleService(articleRepo, mediaRepo, imgComposer, imgDel, ingestSvc)
snapSvc = service.NewStreetSnapService(snapRepo, mediaRepo, imgComposer, imgDel, ingestSvc)
engine := router.New(router.Options{
Config: cfg,
JWT: jwtManager,
@ -101,6 +140,11 @@ func main() {
Health: handler.NewHealthHandler(),
})
// 5.5 装配管理后台(Backstage)handler,供下方独立引擎使用。
backstageHandler := handler.NewBackstageHandler(authSvc, brandSvc, articleSvc, snapSvc, reviewSvc, ingestSvc)
// 爬虫上报 handler(HMAC 验签入口)
ingestHandler := handler.NewIngestHandler(ingestSvc, brandSvc)
// 6. 启动对外公开 HTTP 服务(8090),并支持优雅关闭
srv := &http.Server{
Addr: ":" + cfg.Server.Port,
@ -133,6 +177,37 @@ func main() {
}
}()
// 7.5 启动管理后台(默认 8092):后台 UI + 爬虫 ingest 内部接口。
backstageEngine := router.NewBackstage(router.BackstageOptions{
Config: cfg,
JWT: jwtManager,
Backstage: backstageHandler,
UploadURLPrefix: cfg.Upload.URLPrefix,
UploadDir: uploadDir,
Ingest: ingestHandler,
IngestAuth: &middleware.IngestAuthConfig{
Secret: cfg.Ingest.Secret,
TTL: cfg.Ingest.TTLSeconds,
Nonces: ingestRepo,
},
})
// 7.6 启动爬虫入库 worker(异步处理 ingest_jobs 队列)。
// 用独立可取消 ctx,优雅关闭时随主流程一起退出。
workerCtx, workerCancel := context.WithCancel(context.Background())
go ingestSvc.Run(workerCtx, 2, 3*time.Second)
log.Printf("⚙ 入库 worker 已启动(队列 ingest_jobs)")
srvBackstage := &http.Server{
Addr: ":" + cfg.Server.BackstagePort,
Handler: backstageEngine,
}
go func() {
log.Printf("🛠 管理后台已启动: http://localhost:%s/admin", cfg.Server.BackstagePort)
if err := srvBackstage.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("✗ 管理后台启动失败: %v", err)
}
}()
// 收到中断信号后,给正在处理的请求留出完成时间再退出,
// 避免直接杀进程导致请求被截断、连接池未正常释放。
quit := make(chan os.Signal, 1)
@ -143,11 +218,15 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(),
time.Duration(cfg.Server.ShutdownTimeout)*time.Second)
defer cancel()
workerCancel() // 通知入库 worker 停止领取新任务
if err := srv.Shutdown(ctx); err != nil {
log.Printf("! 公开服务优雅关闭超时: %v", err)
}
if err := srvSSG.Shutdown(ctx); err != nil {
log.Printf("! SSG 内部服务优雅关闭超时: %v", err)
}
if err := srvBackstage.Shutdown(ctx); err != nil {
log.Printf("! 管理后台优雅关闭超时: %v", err)
}
log.Println("✓ 服务已停止")
}