diff --git a/Taskfile.yaml b/Taskfile.yaml
index c27514bbe..f3874badb 100644
--- a/Taskfile.yaml
+++ b/Taskfile.yaml
@@ -85,7 +85,7 @@ tasks:
test-all:
desc: Run all tests.
cmds:
- - go test -timeout 10s -tags test ./...
+ - go test -v -timeout 10s -tags test github.com/bangumi/server/internal/tag/...
env:
TEST_MYSQL: "1"
TEST_REDIS: "1"
diff --git a/canal/canal.go b/canal/canal.go
index 50ac7f1b8..3b57ec5e6 100644
--- a/canal/canal.go
+++ b/canal/canal.go
@@ -59,7 +59,7 @@ func Main() error {
// driver and connector
fx.Provide(
- driver.NewMysqlConnectionPool,
+ driver.NewMysqlSqlDB,
driver.NewRedisClient, logger.Copy, cache.NewRedisCache,
subject.NewMysqlRepo, search.New, session.NewMysqlRepo, session.New,
driver.NewS3,
diff --git a/cmd/archive/main.go b/cmd/archive/main.go
index fd23fbc79..047b6a79d 100644
--- a/cmd/archive/main.go
+++ b/cmd/archive/main.go
@@ -80,7 +80,7 @@ func start(out string) {
err := fx.New(
fx.NopLogger,
fx.Provide(
- driver.NewMysqlConnectionPool, dal.NewDB,
+ driver.NewMysqlSqlDB, dal.NewGormDB,
config.NewAppConfig, logger.Copy,
@@ -210,7 +210,7 @@ type Subject struct {
type Tag struct {
Name string `json:"name"`
- Count int `json:"count"`
+ Count uint `json:"count"`
}
func exportSubjects(q *query.Query, w io.Writer) {
diff --git a/cmd/gen/gorm/main.go b/cmd/gen/gorm/main.go
index 643aa85d4..baccbecba 100644
--- a/cmd/gen/gorm/main.go
+++ b/cmd/gen/gorm/main.go
@@ -92,12 +92,12 @@ func main() {
panic("failed to read config: " + err.Error())
}
- conn, err := driver.NewMysqlConnectionPool(c)
+ conn, err := driver.NewMysqlSqlDB(c)
if err != nil {
panic(err)
}
- db, err := dal.NewDB(conn, c)
+ db, err := dal.NewGormDB(conn, c)
if err != nil {
panic(err)
}
diff --git a/cmd/web/cmd.go b/cmd/web/cmd.go
index 7127a0561..4e3ff73b2 100644
--- a/cmd/web/cmd.go
+++ b/cmd/web/cmd.go
@@ -41,6 +41,7 @@ import (
"github.com/bangumi/server/internal/revision"
"github.com/bangumi/server/internal/search"
"github.com/bangumi/server/internal/subject"
+ "github.com/bangumi/server/internal/tag"
"github.com/bangumi/server/internal/timeline"
"github.com/bangumi/server/internal/user"
"github.com/bangumi/server/web"
@@ -65,7 +66,8 @@ func start() error {
fx.Provide(
config.AppConfigReader(config.AppTypeHTTP),
driver.NewRedisClientWithMetrics, // redis
- driver.NewMysqlConnectionPool, // mysql
+ driver.NewMysqlSqlDB, // mysql
+ driver.NewRueidisClient,
func() *resty.Client {
httpClient := resty.New().SetJSONEscapeHTML(false)
httpClient.JSONUnmarshal = json.Unmarshal
@@ -74,6 +76,8 @@ func start() error {
},
),
+ fx.Invoke(dal.SetupMetrics),
+
dal.Module,
fx.Provide(
@@ -87,6 +91,8 @@ func start() error {
dam.New, subject.NewMysqlRepo, subject.NewCachedRepo, person.NewMysqlRepo,
+ tag.NewCachedRepo, tag.NewMysqlRepo,
+
auth.NewService, person.NewService, search.New,
),
diff --git a/dal/fx.go b/dal/fx.go
index 465467229..1f06120bb 100644
--- a/dal/fx.go
+++ b/dal/fx.go
@@ -27,7 +27,7 @@ import (
var Module = fx.Module("dal",
fx.Provide(
- NewDB,
+ NewGormDB,
query.Use,
NewMysqlTransaction,
func(db *sql.DB) *sqlx.DB {
diff --git a/dal/metrics.go b/dal/metrics.go
index 041d6120f..4702c7a0b 100644
--- a/dal/metrics.go
+++ b/dal/metrics.go
@@ -22,7 +22,7 @@ import (
"gorm.io/gorm"
)
-func setupMetrics(db *gorm.DB, conn *sql.DB) error {
+func SetupMetrics(db *gorm.DB, conn *sql.DB) error {
var DatabaseQuery = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "chii_db_execute_total",
diff --git a/dal/new.go b/dal/new.go
index bef0f170b..6166a1447 100644
--- a/dal/new.go
+++ b/dal/new.go
@@ -27,7 +27,7 @@ import (
"github.com/bangumi/server/internal/pkg/logger"
)
-func NewDB(conn *sql.DB, c config.AppConfig) (*gorm.DB, error) {
+func NewGormDB(conn *sql.DB, c config.AppConfig) (*gorm.DB, error) {
var gLog gormLogger.Interface
if c.Debug.Gorm {
logger.Info("enable gorm debug mode, will log all sql")
@@ -51,9 +51,5 @@ func NewDB(conn *sql.DB, c config.AppConfig) (*gorm.DB, error) {
return nil, errgo.Wrap(err, "create dal")
}
- if err = setupMetrics(db, conn); err != nil {
- return nil, errgo.Wrap(err, "setup metrics")
- }
-
return db, nil
}
diff --git a/dal/new_test.go b/dal/new_test.go
index 54f0645cc..7245ea6bc 100644
--- a/dal/new_test.go
+++ b/dal/new_test.go
@@ -31,9 +31,9 @@ func TestNewDB(t *testing.T) {
cfg, err := config.NewAppConfig()
require.NoError(t, err)
- conn, err := driver.NewMysqlConnectionPool(cfg)
+ conn, err := driver.NewMysqlSqlDB(cfg)
require.NoError(t, err)
- db, err := dal.NewDB(conn, cfg)
+ db, err := dal.NewGormDB(conn, cfg)
require.NoError(t, err)
err = db.Exec("select 0;").Error
diff --git a/go.mod b/go.mod
index 912ef457b..44dae6b6b 100644
--- a/go.mod
+++ b/go.mod
@@ -25,6 +25,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0
github.com/prometheus/client_golang v1.20.4
github.com/redis/go-redis/v9 v9.6.1
+ github.com/redis/rueidis v1.0.47
github.com/samber/lo v1.47.0
github.com/segmentio/kafka-go v0.4.47
github.com/spf13/cobra v1.8.1
diff --git a/go.sum b/go.sum
index 47325d807..63e43c185 100644
--- a/go.sum
+++ b/go.sum
@@ -338,8 +338,8 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y=
-github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
+github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
+github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
@@ -400,6 +400,8 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn
github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps=
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
+github.com/redis/rueidis v1.0.47 h1:41UdeXOo4eJuW+cfpUJuLtVGyO0QJY3A2rEYgJWlfHs=
+github.com/redis/rueidis v1.0.47/go.mod h1:by+34b0cFXndxtYmPAHpoTHO5NkosDlBvhexoTURIxM=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
@@ -513,6 +515,8 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
diff --git a/internal/cachekey/cachekey.go b/internal/cachekey/cachekey.go
index 319cc3b23..2f424a728 100644
--- a/internal/cachekey/cachekey.go
+++ b/internal/cachekey/cachekey.go
@@ -54,3 +54,7 @@ func Index(id model.IndexID) string {
func User(id model.UserID) string {
return resPrefix + "user:" + strconv.FormatUint(uint64(id), 10)
}
+
+func SubjectMetaTag(id model.SubjectID) string {
+ return resPrefix + "subject:meta-tags:" + strconv.FormatUint(uint64(id), 10)
+}
diff --git a/internal/collections/infra/mysql_repo.go b/internal/collections/infra/mysql_repo.go
index a408e44ce..e45df0bd4 100644
--- a/internal/collections/infra/mysql_repo.go
+++ b/internal/collections/infra/mysql_repo.go
@@ -333,8 +333,8 @@ func (r mysqlRepo) reCountSubjectTags(ctx context.Context, tx *query.Query,
return errgo.Trace(err)
}
- var count = make(map[string]int)
- var countMap = make(map[string]uint32)
+ var count = make(map[string]uint)
+ var countMap = make(map[string]uint)
for _, tag := range tagList {
if !dam.ValidateTag(tag.Tag.Name) {
@@ -342,7 +342,7 @@ func (r mysqlRepo) reCountSubjectTags(ctx context.Context, tx *query.Query,
}
count[tag.Tag.Name]++
- countMap[tag.Tag.Name] = tag.Tag.Results
+ countMap[tag.Tag.Name] = uint(tag.Tag.Results)
}
var phpTags = make([]subject.Tag, 0, len(count))
@@ -351,7 +351,7 @@ func (r mysqlRepo) reCountSubjectTags(ctx context.Context, tx *query.Query,
phpTags = append(phpTags, subject.Tag{
Name: lo.ToPtr(name),
Count: c,
- TotalCount: int(countMap[name]),
+ TotalCount: uint(countMap[name]),
})
}
diff --git a/internal/model/subject.go b/internal/model/subject.go
index 577c9e55f..626de64ee 100644
--- a/internal/model/subject.go
+++ b/internal/model/subject.go
@@ -17,8 +17,9 @@ package model
const subjectLocked = 2
type Tag struct {
- Name string
- Count int
+ Name string
+ Count uint
+ TotalCount uint
}
type Subject struct {
diff --git a/internal/pkg/cache/noop.go b/internal/pkg/cache/noop.go
index 491f46aab..8dc204b32 100644
--- a/internal/pkg/cache/noop.go
+++ b/internal/pkg/cache/noop.go
@@ -17,6 +17,8 @@ package cache
import (
"context"
"time"
+
+ "github.com/redis/rueidis"
)
func NewNoop() RedisCache {
@@ -36,3 +38,7 @@ func (n noop) Set(context.Context, string, any, time.Duration) error {
func (n noop) Del(context.Context, ...string) error {
return nil
}
+
+func (n noop) mget(ctx context.Context, key []string) rueidis.RedisResult {
+ return rueidis.RedisResult{}
+}
diff --git a/internal/pkg/cache/redis.go b/internal/pkg/cache/redis.go
index 16746dd5f..9806f94d1 100644
--- a/internal/pkg/cache/redis.go
+++ b/internal/pkg/cache/redis.go
@@ -19,6 +19,7 @@ import (
"time"
"github.com/redis/go-redis/v9"
+ "github.com/redis/rueidis"
"github.com/trim21/errgo"
"go.uber.org/zap"
@@ -34,15 +35,18 @@ type RedisCache interface {
Get(ctx context.Context, key string, value any) (bool, error)
Set(ctx context.Context, key string, value any, ttl time.Duration) error
Del(ctx context.Context, keys ...string) error
+
+ mget(ctx context.Context, key []string) rueidis.RedisResult
}
// NewRedisCache create a redis backed cache.
-func NewRedisCache(cli *redis.Client) RedisCache {
- return redisCache{r: cli}
+func NewRedisCache(cli *redis.Client, ru rueidis.Client) RedisCache {
+ return redisCache{r: cli, ru: ru}
}
type redisCache struct {
- r *redis.Client
+ r *redis.Client
+ ru rueidis.Client
}
func (c redisCache) Get(ctx context.Context, key string, value any) (bool, error) {
@@ -66,6 +70,14 @@ func (c redisCache) Get(ctx context.Context, key string, value any) (bool, error
return true, nil
}
+func (c redisCache) mget(ctx context.Context, keys []string) rueidis.RedisResult {
+ return c.ru.Do(ctx, c.ru.B().Mget().Key(keys...).Build())
+}
+
+func MGet[T any](c RedisCache, ctx context.Context, keys []string, value *[]T) error {
+ return rueidis.DecodeSliceOfJSON(c.mget(ctx, keys), value)
+}
+
func (c redisCache) Set(ctx context.Context, key string, value any, ttl time.Duration) error {
b, err := marshalBytes(value)
if err != nil {
diff --git a/internal/pkg/driver/mysql.go b/internal/pkg/driver/mysql.go
index 480ec3a54..03b65a5de 100644
--- a/internal/pkg/driver/mysql.go
+++ b/internal/pkg/driver/mysql.go
@@ -31,7 +31,8 @@ import (
var setLoggerOnce = sync.Once{}
-func NewMysqlConnectionPool(c config.AppConfig) (*sql.DB, error) {
+//nolint:stylecheck
+func NewMysqlSqlDB(c config.AppConfig) (*sql.DB, error) {
setLoggerOnce.Do(func() {
_ = mysql.SetLogger(logger.StdAt(zap.ErrorLevel))
})
diff --git a/internal/pkg/driver/redis.go b/internal/pkg/driver/redis.go
index 9eb4e3eaf..ea5d378fa 100644
--- a/internal/pkg/driver/redis.go
+++ b/internal/pkg/driver/redis.go
@@ -16,9 +16,12 @@ package driver
import (
"context"
+ "fmt"
+ "net/url"
"time"
"github.com/redis/go-redis/v9"
+ "github.com/redis/rueidis"
"github.com/trim21/errgo"
"go.uber.org/zap"
@@ -63,3 +66,21 @@ func NewRedisClientWithMetrics(c config.AppConfig) (*redis.Client, error) {
return cli, nil
}
+
+func NewRueidisClient(c config.AppConfig) (rueidis.Client, error) {
+ u, err := url.Parse(c.RedisURL)
+ if err != nil {
+ return nil, err
+ }
+
+ password, _ := u.User.Password()
+ cli, err := rueidis.NewClient(rueidis.ClientOption{
+ InitAddress: []string{fmt.Sprintf("%s:%s", u.Hostname(), u.Port())},
+ Password: password,
+ })
+ if err != nil {
+ return cli, err
+ }
+
+ return cli, nil
+}
diff --git a/internal/pkg/test/fx.go b/internal/pkg/test/fx.go
new file mode 100644
index 000000000..209897f57
--- /dev/null
+++ b/internal/pkg/test/fx.go
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see
+
+package test
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+
+ "github.com/bangumi/server/config"
+ "github.com/bangumi/server/dal"
+ "github.com/bangumi/server/internal/pkg/cache"
+ "github.com/bangumi/server/internal/pkg/driver"
+)
+
+func Fx(t *testing.T, target ...fx.Option) {
+ t.Helper()
+ err := fx.New(
+ append(target, fx.NopLogger,
+
+ // driver and connector
+ fx.Provide(
+ config.AppConfigReader(config.AppTypeHTTP),
+ driver.NewRedisClient, // redis
+ driver.NewRueidisClient, // redis
+ driver.NewMysqlSqlDB, // mysql
+ func() *resty.Client {
+ httpClient := resty.New().SetJSONEscapeHTML(false)
+ httpClient.JSONUnmarshal = json.Unmarshal
+ httpClient.JSONMarshal = json.Marshal
+ return httpClient
+ },
+ ),
+
+ dal.Module,
+
+ fx.Provide(cache.NewRedisCache, zap.NewNop),
+ )...,
+ ).Err()
+
+ require.NoError(t, err)
+}
diff --git a/internal/pkg/test/gorm.go b/internal/pkg/test/gorm.go
index 7c90a4eb2..0424f24af 100644
--- a/internal/pkg/test/gorm.go
+++ b/internal/pkg/test/gorm.go
@@ -56,7 +56,7 @@ func GetGorm(tb testing.TB) *gorm.DB {
func newGorm(tb testing.TB, c config.AppConfig) (*gorm.DB, error) {
tb.Helper()
- conn, err := driver.NewMysqlConnectionPool(c)
+ conn, err := driver.NewMysqlSqlDB(c)
if err != nil {
return nil, errgo.Wrap(err, "sql.Open")
}
diff --git a/internal/subject/mysq_repository_compat.go b/internal/subject/mysq_repository_compat.go
index a1d761142..b3c2ef2b4 100644
--- a/internal/subject/mysq_repository_compat.go
+++ b/internal/subject/mysq_repository_compat.go
@@ -24,8 +24,8 @@ import (
type Tag struct {
Name *string `php:"tag_name"`
- Count int `php:"result,string"`
- TotalCount int `php:"tag_results,string"`
+ Count uint `php:"result,string"`
+ TotalCount uint `php:"tag_results,string"`
}
func ParseTags(b []byte) ([]model.Tag, error) {
diff --git a/internal/tag/cache_repo.go b/internal/tag/cache_repo.go
new file mode 100644
index 000000000..71570723e
--- /dev/null
+++ b/internal/tag/cache_repo.go
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see
+
+package tag
+
+import (
+ "context"
+ "time"
+
+ "github.com/samber/lo"
+ "github.com/trim21/errgo"
+ "go.uber.org/zap"
+
+ "github.com/bangumi/server/internal/cachekey"
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/pkg/cache"
+)
+
+func NewCachedRepo(c cache.RedisCache, r Repo, log *zap.Logger) CachedRepo {
+ return cacheRepo{cache: c, repo: r, log: log.Named("subject.CachedRepo")}
+}
+
+var _ CachedRepo = cacheRepo{}
+
+type cacheRepo struct {
+ cache cache.RedisCache
+ repo Repo
+ log *zap.Logger
+}
+
+type cachedTags struct {
+ ID model.SubjectID
+ Tags []Tag
+}
+
+func (r cacheRepo) Get(ctx context.Context, id model.SubjectID) ([]Tag, error) {
+ var key = cachekey.SubjectMetaTag(id)
+
+ var s cachedTags
+ ok, err := r.cache.Get(ctx, key, &s)
+ if err != nil {
+ return s.Tags, errgo.Wrap(err, "cache.Get")
+ }
+
+ if ok {
+ return s.Tags, nil
+ }
+
+ tags, err := r.repo.Get(ctx, id)
+ if err != nil {
+ return tags, err
+ }
+
+ if e := r.cache.Set(ctx, key, cachedTags{
+ ID: id,
+ Tags: tags,
+ }, time.Minute); e != nil {
+ r.log.Error("can't set response to cache", zap.Error(e))
+ }
+
+ return tags, nil
+}
+
+func (r cacheRepo) GetByIDs(ctx context.Context, ids []model.SubjectID) (map[model.SubjectID][]Tag, error) {
+ var tags []cachedTags
+ err := cache.MGet(r.cache, ctx, lo.Map(ids, func(item model.SubjectID, index int) string {
+ return cachekey.SubjectMetaTag(item)
+ }), &tags)
+ if err != nil {
+ return nil, errgo.Wrap(err, "cache.MGet")
+ }
+
+ result := make(map[model.SubjectID][]Tag, len(ids))
+ for _, tag := range tags {
+ result[tag.ID] = tag.Tags
+ }
+
+ var missing = make([]model.SubjectID, 0, len(ids))
+ for _, id := range ids {
+ if _, ok := result[id]; !ok {
+ missing = append(missing, id)
+ }
+ }
+
+ missingFromCache, err := r.repo.GetByIDs(ctx, missing)
+ if err != nil {
+ return nil, err
+ }
+ for id, tag := range missingFromCache {
+ result[id] = tag
+ err = r.cache.Set(ctx, cachekey.SubjectMetaTag(id), cachedTags{
+ ID: id,
+ Tags: tag,
+ }, time.Hour)
+ if err != nil {
+ return nil, errgo.Wrap(err, "cache.Set")
+ }
+ }
+
+ return result, nil
+}
diff --git a/internal/tag/cache_repo_test.go b/internal/tag/cache_repo_test.go
new file mode 100644
index 000000000..cbb61927b
--- /dev/null
+++ b/internal/tag/cache_repo_test.go
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see
+
+package tag_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "go.uber.org/fx"
+
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/pkg/test"
+ "github.com/bangumi/server/internal/tag"
+)
+
+func getCacheRepo(t *testing.T) tag.CachedRepo {
+ t.Helper()
+
+ var r tag.CachedRepo
+
+ test.Fx(t, fx.Provide(tag.NewCachedRepo, tag.NewMysqlRepo), fx.Populate(&r))
+
+ return r
+}
+
+func TestCacheGet(t *testing.T) {
+ test.RequireEnv(t, test.EnvMysql, test.EnvRedis)
+ t.Parallel()
+
+ repo := getCacheRepo(t)
+
+ _, err := repo.Get(context.Background(), 8)
+ require.NoError(t, err)
+}
+
+func TestCacheGetTags(t *testing.T) {
+ test.RequireEnv(t, test.EnvMysql, test.EnvRedis)
+ t.Parallel()
+
+ repo := getCacheRepo(t)
+
+ _, err := repo.GetByIDs(context.Background(), []model.SubjectID{1, 2, 8})
+ require.NoError(t, err)
+}
diff --git a/internal/subject/repo2.go b/internal/tag/domain.go
similarity index 54%
rename from internal/subject/repo2.go
rename to internal/tag/domain.go
index 32f36ee71..375a7e1ce 100644
--- a/internal/subject/repo2.go
+++ b/internal/tag/domain.go
@@ -12,4 +12,36 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see
-package subject
+package tag
+
+import (
+ "context"
+
+ "github.com/bangumi/server/internal/model"
+)
+
+// CatSubject 条目tag.
+const CatSubject = 0
+
+// CatMeta 官方tag.
+const CatMeta = 3
+
+type Tag struct {
+ Name string
+ Count uint
+ // TotalCount count for all tags including all subject
+ TotalCount uint
+}
+
+type CachedRepo interface {
+ read
+}
+
+type Repo interface {
+ read
+}
+
+type read interface {
+ Get(ctx context.Context, id model.SubjectID) ([]Tag, error)
+ GetByIDs(ctx context.Context, ids []model.SubjectID) (map[model.SubjectID][]Tag, error)
+}
diff --git a/internal/tag/mysql_repo.go b/internal/tag/mysql_repo.go
new file mode 100644
index 000000000..475bb5cb8
--- /dev/null
+++ b/internal/tag/mysql_repo.go
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see
+
+package tag
+
+import (
+ "context"
+
+ "github.com/jmoiron/sqlx"
+ "go.uber.org/zap"
+
+ "github.com/bangumi/server/dal/query"
+ "github.com/bangumi/server/internal/model"
+)
+
+func NewMysqlRepo(q *query.Query, log *zap.Logger, db *sqlx.DB) (Repo, error) {
+ return mysqlRepo{q: q, log: log.Named("tag.mysqlRepo"), db: db}, nil
+}
+
+type mysqlRepo struct {
+ q *query.Query
+ log *zap.Logger
+ db *sqlx.DB
+}
+
+func (r mysqlRepo) Get(ctx context.Context, id model.SubjectID) ([]Tag, error) {
+ var s []struct {
+ Tid uint `db:"tlt_tid"`
+ Name string `db:"tag_name"`
+ TotalCount uint `db:"tag_results"`
+ }
+
+ err := r.db.SelectContext(ctx, &s, `
+ select tlt_tid, tag_name, tag_results
+ from chii_tag_neue_list
+ inner join chii_tag_neue_index on tlt_tid = tag_id and tlt_type = tag_type
+ where tlt_uid = 0 and tag_cat = ? and tlt_mid = ?
+ `, CatSubject, id)
+ if err != nil {
+ return nil, err
+ }
+
+ tags := make([]Tag, len(s))
+ for i, t := range s {
+ tags[i] = Tag{
+ Name: t.Name,
+ TotalCount: t.TotalCount,
+ }
+ }
+
+ return tags, nil
+}
+
+func (r mysqlRepo) GetByIDs(ctx context.Context, ids []model.SubjectID) (map[model.SubjectID][]Tag, error) {
+ var s []struct {
+ Tid uint `db:"tlt_tid"`
+ Name string `db:"tag_name"`
+ TotalCount uint `db:"tag_results"`
+ Mid model.SubjectID `db:"tlt_mid"`
+ }
+
+ q, v, err := sqlx.In(`
+ select tlt_tid, tag_name, tag_results, tlt_mid
+ from chii_tag_neue_list
+ inner join chii_tag_neue_index on tlt_tid = tag_id and tlt_type = tag_type
+ where tlt_uid = 0 and tag_cat = ? and tlt_mid IN (?)
+ `, CatSubject, ids)
+ if err != nil {
+ return nil, err
+ }
+
+ err = r.db.SelectContext(ctx, &s, q, v...)
+ if err != nil {
+ return nil, err
+ }
+
+ tags := make(map[model.SubjectID][]Tag, len(s))
+ for _, t := range s {
+ tags[t.Mid] = append(tags[t.Mid], Tag{
+ Name: t.Name,
+ TotalCount: t.TotalCount,
+ })
+ }
+
+ return tags, nil
+}
diff --git a/internal/tag/mysql_repo_test.go b/internal/tag/mysql_repo_test.go
new file mode 100644
index 000000000..31cc1585d
--- /dev/null
+++ b/internal/tag/mysql_repo_test.go
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see
+
+package tag_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/jmoiron/sqlx"
+ "github.com/samber/lo"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+
+ "github.com/bangumi/server/dal/query"
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/pkg/test"
+ "github.com/bangumi/server/internal/tag"
+)
+
+func getRepo(t *testing.T) tag.Repo {
+ t.Helper()
+ q := query.Use(test.GetGorm(t))
+ repo, err := tag.NewMysqlRepo(q, zap.NewNop(), sqlx.NewDb(lo.Must(q.DB().DB()), "mysql"))
+ require.NoError(t, err)
+
+ return repo
+}
+
+func TestGet(t *testing.T) {
+ test.RequireEnv(t, test.EnvMysql)
+ t.Parallel()
+
+ repo := getRepo(t)
+
+ _, err := repo.Get(context.Background(), 8)
+ require.NoError(t, err)
+}
+
+func TestGetTags(t *testing.T) {
+ test.RequireEnv(t, test.EnvMysql)
+ t.Parallel()
+
+ repo := getRepo(t)
+
+ _, err := repo.GetByIDs(context.Background(), []model.SubjectID{1, 2, 8})
+ require.NoError(t, err)
+}
diff --git a/web/handler/subject/browse.go b/web/handler/subject/browse.go
index ebfb7b5f3..2968a425b 100644
--- a/web/handler/subject/browse.go
+++ b/web/handler/subject/browse.go
@@ -60,7 +60,7 @@ func (h Subject) Browse(c echo.Context) error {
}
data := make([]res.SubjectV0, 0, len(subjects))
for _, s := range subjects {
- data = append(data, convertModelSubject(s, 0))
+ data = append(data, convertModelSubject(s, 0, nil))
}
return c.JSON(http.StatusOK, res.Paged{Data: data, Total: count, Limit: page.Limit, Offset: page.Offset})
diff --git a/web/handler/subject/get.go b/web/handler/subject/get.go
index e7f0f623a..178842db5 100644
--- a/web/handler/subject/get.go
+++ b/web/handler/subject/get.go
@@ -20,6 +20,7 @@ import (
"net/http"
"github.com/labstack/echo/v4"
+ "github.com/samber/lo"
"github.com/trim21/errgo"
"go.uber.org/zap"
@@ -31,6 +32,7 @@ import (
"github.com/bangumi/server/internal/pkg/logger"
"github.com/bangumi/server/internal/pkg/null"
"github.com/bangumi/server/internal/subject"
+ "github.com/bangumi/server/internal/tag"
"github.com/bangumi/server/pkg/vars"
"github.com/bangumi/server/pkg/wiki"
"github.com/bangumi/server/web/accessor"
@@ -66,7 +68,12 @@ func (h Subject) Get(c echo.Context) error {
return errgo.Wrap(err, "episode.Count")
}
- return c.JSON(http.StatusOK, convertModelSubject(s, totalEpisode))
+ metaTags, err := h.tag.Get(c.Request().Context(), s.ID)
+ if err != nil {
+ return err
+ }
+
+ return c.JSON(http.StatusOK, convertModelSubject(s, totalEpisode, metaTags))
}
func platformString(s model.Subject) *string {
@@ -114,7 +121,7 @@ func (h Subject) GetImage(c echo.Context) error {
return c.Redirect(http.StatusFound, l)
}
-func convertModelSubject(s model.Subject, totalEpisode int64) res.SubjectV0 {
+func convertModelSubject(s model.Subject, totalEpisode int64, metaTags []tag.Tag) res.SubjectV0 {
return res.SubjectV0{
TotalEpisodes: totalEpisode,
ID: s.ID,
@@ -128,6 +135,13 @@ func convertModelSubject(s model.Subject, totalEpisode int64) res.SubjectV0 {
Volumes: s.Volumes,
Redirect: s.Redirect,
Eps: s.Eps,
+ MetaTags: lo.Map(metaTags, func(item tag.Tag, index int) res.SubjectTag {
+ return res.SubjectTag{
+ Name: item.Name,
+ Count: item.Count,
+ TotalCont: item.TotalCount,
+ }
+ }),
Tags: slice.Map(s.Tags, func(tag model.Tag) res.SubjectTag {
return res.SubjectTag{
Name: tag.Name,
diff --git a/web/handler/subject/subject.go b/web/handler/subject/subject.go
index d7cbf6055..aaa64c008 100644
--- a/web/handler/subject/subject.go
+++ b/web/handler/subject/subject.go
@@ -21,6 +21,7 @@ import (
"github.com/bangumi/server/internal/episode"
"github.com/bangumi/server/internal/person"
"github.com/bangumi/server/internal/subject"
+ "github.com/bangumi/server/internal/tag"
)
type Subject struct {
@@ -28,6 +29,7 @@ type Subject struct {
episode episode.Repo
personRepo person.Repo
subject subject.Repo
+ tag tag.CachedRepo
c character.Repo
}
@@ -37,6 +39,7 @@ func New(
personRepo person.Repo,
c character.Repo,
episode episode.Repo,
+ tag tag.CachedRepo,
) (Subject, error) {
return Subject{
c: c,
@@ -44,6 +47,7 @@ func New(
personRepo: personRepo,
subject: subject,
person: p,
+ tag: tag,
}, nil
}
diff --git a/web/res/subject.go b/web/res/subject.go
index ba3bde9d4..8fb78cfb1 100644
--- a/web/res/subject.go
+++ b/web/res/subject.go
@@ -29,8 +29,9 @@ const defaultShortSummaryLength = 120
type v0wiki = []any
type SubjectTag struct {
- Name string `json:"name"`
- Count int `json:"count"`
+ Name string `json:"name"`
+ Count uint `json:"count"`
+ TotalCont uint `json:"total_cont"`
}
type SubjectV0 struct {
@@ -47,6 +48,7 @@ type SubjectV0 struct {
Collection SubjectCollectionStat `json:"collection"`
ID model.SubjectID `json:"id"`
Eps uint32 `json:"eps"`
+ MetaTags []SubjectTag `json:"meta_tags"`
Volumes uint32 `json:"volumes"`
Series bool `json:"series"`
Locked bool `json:"locked"`