Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ MINIO_BUCKET=ttml-db
MINIO_USE_SSL=false

# ===== RabbitMQ =====
RABBITMQ_URL=amqp://guest:guest@localhost:5672/
RABBITMQ_URL=amqp://guest:guest@localhost:5672/amllhub
RABBITMQ_QUEUE=sync_queue
RABBITMQ_DLQ=sync_queue.dlq
RABBITMQ_NF_QUEUE=not_found_parse_queue
Expand All @@ -48,3 +48,4 @@ SYNC_LOCK_TTL=3600

# ===== NCM API =====
NCM_API_BASE=https://ncmapi.bikonoo.com
NCM_MUSIC_U=
17 changes: 0 additions & 17 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,20 +95,3 @@ jobs:
- name: 测试
run: go test -v ./...

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: '1.25'
cache-dependency-path: backend/go.sum

- name: 代码检查
uses: golangci/golangci-lint-action@v7
with:
version: v2.1.6
working-directory: backend
args: --timeout=5m

6 changes: 3 additions & 3 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ jobs:
- name: 构建
run: cargo build --release --bin ttml-worker --target ${{ matrix.target }}

- name: 测试
run: cargo test --release --target ${{ matrix.target }}

- name: 上传产物
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: ${{ matrix.binary_path }}
if-no-files-found: error

- name: 测试
run: cargo test --release --target ${{ matrix.target }}
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

# Worker
/worker/target
/worker/Cargo.lock
*.swp

# IDE
Expand Down
22 changes: 13 additions & 9 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
FROM golang:1.22-alpine AS builder
FROM golang:1.25-alpine AS builder

WORKDIR /app

# 利用缓存加速依赖下载
COPY go.mod go.sum* ./
RUN go mod download || true
COPY go.mod go.sum ./
RUN go mod download

COPY . .

# 生成 go.sum(首次构建)并编译
RUN go mod tidy && \
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/server ./cmd/server
# 编译
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/server ./cmd/server

# 运行阶段
FROM alpine:3.20

RUN apk --no-cache add ca-certificates tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone
echo "Asia/Shanghai" > /etc/timezone && \
adduser -D -u 10001 appuser

WORKDIR /app
COPY --from=builder /out/server /app/server
COPY --from=builder /app/migrations /app/migrations
COPY --from=builder --chown=appuser:appuser /out/server /app/server
COPY --from=builder --chown=appuser:appuser /app/migrations /app/migrations

EXPOSE 8080

USER appuser

HEALTHCHECK --interval=30s --timeout=5s CMD wget -qO- http://localhost:8080/health || exit 1

ENTRYPOINT ["/app/server"]
2 changes: 1 addition & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
APP := server
PKG := github.com/amll-dev/amll-hub/backend
MIGRATE_DSN ?= "postgres://ttml:ttml@localhost:5432/ttml_db?sslmode=disable"
MIGRATE_DSN ?= "postgres://$${DB_USER:-ttml}:$${DB_PASSWORD:-ttml}@localhost:5432/$${DB_NAME:-ttml_db}?sslmode=disable"

.PHONY: tidy run build test lint migrate-up migrate-down docker-build docker-help

Expand Down
39 changes: 21 additions & 18 deletions backend/cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ import (
"github.com/amll-dev/amll-hub/backend/internal/repository"
"github.com/amll-dev/amll-hub/backend/internal/router"
"github.com/amll-dev/amll-hub/backend/internal/service"
"github.com/minio/minio-go/v7"
"github.com/redis/go-redis/v9"
logrus "github.com/sirupsen/logrus"
"gorm.io/gorm"
)

// Run 启动 HTTP 服务
Expand Down Expand Up @@ -85,15 +82,18 @@ func Run() {
indexSvc := service.NewIndexService(cfg, minioClient)
notFoundSvc := service.NewNotFoundService(notFoundRepo, redisClient, mq)
onlineSearchSvc := service.NewOnlineSearchService(cfg)
cloudMusicSvc := service.NewCloudMusicService(cfg, redisClient)

// 4.1 启动无歌词服务后台任务:白名单预热 + 每周清空
appCtx, appCancel := context.WithCancel(context.Background())
defer appCancel()
{
preloadCtx, preloadCancel := context.WithTimeout(context.Background(), 30*time.Second)
if err := notFoundSvc.PreloadWhitelist(preloadCtx); err != nil {
logrus.Warnf("preload not_found whitelist: %v", err)
}
preloadCancel()
notFoundSvc.StartWeeklyClearTask()
notFoundSvc.StartWeeklyClearTask(appCtx)
}

// 5. 初始化 handler
Expand All @@ -105,16 +105,18 @@ func Run() {
indexH := handler.NewIndexHandler(indexSvc)
nfH := handler.NewNotFoundHandler(notFoundSvc)
onlineSearchH := handler.NewOnlineSearchHandler(onlineSearchSvc)
cloudMusicH := handler.NewCloudMusicHandler(cloudMusicSvc)

// 6. 启动 HTTP
r := router.New(syncH, lyricsH, searchH, batchH, statsH, indexH, nfH, onlineSearchH)
r := router.New(syncH, lyricsH, searchH, batchH, statsH, indexH, nfH, onlineSearchH, cloudMusicH)

srv := &http.Server{
Addr: ":" + cfg.HTTP.Port,
Handler: r,
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 120 * time.Second,
Addr: ":" + cfg.HTTP.Port,
Handler: r,
ReadTimeout: 30 * time.Second,
ReadHeaderTimeout: 10 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 120 * time.Second,
}

// 7. 优雅关闭
Expand All @@ -128,23 +130,24 @@ func Run() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
appCancel()
logrus.Info("shutting down server...")

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logrus.Errorf("server shutdown: %v", err)
}
_ = dbWithCtx(db)
_ = redisWithCtx(redisClient)
_ = minioWithCtx(minioClient)
if db != nil {
if sqlDB, err := db.DB(); err == nil {
_ = sqlDB.Close()
}
}
if redisClient != nil {
_ = redisClient.Close()
}
logrus.Info("server exited")
}

// dbWithCtx 占位避免 unused
func dbWithCtx(_ *gorm.DB) error { return nil }
func redisWithCtx(_ *redis.Client) error { return nil }
func minioWithCtx(_ *minio.Client) error { return nil }

// _ 占位避免 fmt 未引用
var _ = fmt.Sprintf
4 changes: 3 additions & 1 deletion backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ require (
gorm.io/gorm v1.25.11
)

require github.com/cybaka520/AMLLHub-Music-API v0.0.0-20260709020609-44a3a67cec13

require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
Expand Down Expand Up @@ -66,7 +68,7 @@ require (
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.52.0 // indirect
github.com/xiaowumin-mark/AMLX-MUSIC-API v0.0.0-20260510143522-2522b551028d // indirect
github.com/xiaowumin-mark/AMLX-MUSIC-API v0.0.0-20260510143522-2522b551028d
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.8.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cybaka520/AMLLHub-Music-API v0.0.0-20260709020609-44a3a67cec13 h1:OSYVfK/DZs0XNcjXCXgLw3Ue/S3QJcuqw2/npZ992dI=
github.com/cybaka520/AMLLHub-Music-API v0.0.0-20260709020609-44a3a67cec13/go.mod h1:ZjsbxoxibtDycatkQwQ4Bc73Vt6g943XRfrM1EKYDHQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
Expand Down
19 changes: 18 additions & 1 deletion backend/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"path/filepath"
"strings"

"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/subosito/gotenv"
)
Expand Down Expand Up @@ -90,6 +91,7 @@ type SyncConfig struct {

type NCMConfig struct {
APIBase string
MusicU string
}

type OnlineSearchConfig struct {
Expand Down Expand Up @@ -119,7 +121,9 @@ func findDotEnv() string {
// Load 从环境变量加载配置(优先加载当前目录或项目根目录的 .env 文件)
func Load() (*Config, error) {
if dotenv := findDotEnv(); dotenv != "" {
_ = gotenv.Load(dotenv)
if err := gotenv.Load(dotenv); err != nil {
logrus.Warnf("load .env file %s: %v", dotenv, err)
}
}

v := viper.New()
Expand Down Expand Up @@ -220,11 +224,24 @@ func Load() (*Config, error) {
},
NCM: NCMConfig{
APIBase: v.GetString("NCM_API_BASE"),
MusicU: v.GetString("NCM_MUSIC_U"),
},
OnlineSearch: OnlineSearchConfig{
TimeoutSec: v.GetInt("ONLINE_SEARCH_TIMEOUT_SEC"),
},
}

if os.Getenv("APP_ENV") == "production" {
if cfg.MinIO.AccessKey == "minioadmin" {
logrus.Warnf("MINIO_ACCESS_KEY is default 'minioadmin' in production")
}
if cfg.MinIO.SecretKey == "minioadmin" {
logrus.Warnf("MINIO_SECRET_KEY is default 'minioadmin' in production")
}
if strings.Contains(cfg.RabbitMQ.URL, "guest:guest") {
logrus.Warnf("RABBITMQ_URL contains default 'guest:guest' credentials in production")
}
}

return cfg, nil
}
21 changes: 18 additions & 3 deletions backend/internal/handler/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,35 @@ func (h *BatchHandler) Post(c *gin.Context) {
return
}

// 收集命中的 songID,批量查询关联数据避免 N+1
songIDs := make([]int64, 0, len(songMap))
for _, song := range songMap {
songIDs = append(songIDs, song.ID)
}
artistsMap, err := h.songRepo.GetArtistsBySongIDs(ctx, songIDs)
if err != nil {
pkg.Fail(c, http.StatusInternalServerError, 500, "批量查询艺术家失败: "+err.Error())
return
}
pmsMap, err := h.songRepo.GetPlatformMappingsBySongIDs(ctx, songIDs)
if err != nil {
pkg.Fail(c, http.StatusInternalServerError, 500, "批量查询平台映射失败: "+err.Error())
return
}

// 构造响应:保持与请求 ids 顺序一致(命中的返回,未命中的跳过)
items := make([]BatchItem, 0, len(songMap))
for _, id := range req.IDs {
song, ok := songMap[id]
if !ok {
continue
}
// 查关联数据
artists, _ := h.songRepo.GetArtistsBySongID(ctx, song.ID)
artists := artistsMap[song.ID]
artistNames := make([]string, 0, len(artists))
for _, a := range artists {
artistNames = append(artistNames, a.Name)
}
pms, _ := h.songRepo.GetPlatformMappingsBySongID(ctx, song.ID)
pms := pmsMap[song.ID]
platformIDs := map[string]string{}
for _, pm := range pms {
platformIDs[pm.Platform] = pm.PlatformID
Expand Down
Loading
Loading