diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 55fb7c2ad..56f80f84a 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -24,9 +24,12 @@ on:
- "**.go.json"
- "etc/Dockerfile"
+env:
+ GOTOOLCHAIN: "local"
+
jobs:
docker:
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -46,7 +49,6 @@ jobs:
key: go-cache-122-${{ hashFiles('**/go.sum') }}-build
restore-keys: |
go-cache-122-${{ hashFiles('**/go.sum') }}-
- go-cache-122-
- name: Install Task
uses: arduino/setup-task@v2
diff --git a/.github/workflows/lint-review.yaml b/.github/workflows/lint-review.yaml
index 5d22de19f..eba0e7e82 100644
--- a/.github/workflows/lint-review.yaml
+++ b/.github/workflows/lint-review.yaml
@@ -8,6 +8,9 @@ on:
permissions:
contents: read
+env:
+ GOTOOLCHAIN: "local"
+
jobs:
lint:
runs-on: ubuntu-latest
@@ -29,4 +32,4 @@ jobs:
- run: gofmt -w -s .
- - uses: autofix-ci/action@ea32e3a12414e6d3183163c3424a7d7a8631ad84
+ - uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 872eb6ce2..cdd1cabac 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -22,9 +22,12 @@ on:
- ".golangci.yaml"
- ".github/workflows/lint.yaml"
+env:
+ GOTOOLCHAIN: "local"
+
jobs:
lint:
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -43,13 +46,10 @@ jobs:
key: go-cache-122-${{ hashFiles('**/go.sum') }}-lint
restore-keys: |
go-cache-122-${{ hashFiles('**/go.sum') }}-
- go-cache-122-
- run: go get -t ./...
- name: Run linters
- uses: golangci/golangci-lint-action@v4
+ uses: golangci/golangci-lint-action@v6
with:
- version: v1.57.2
- skip-pkg-cache: true
- skip-build-cache: true
+ version: v1.61.0
diff --git a/.github/workflows/release-docker.yaml b/.github/workflows/release-docker.yaml
index 00aa61915..e9fbd9837 100644
--- a/.github/workflows/release-docker.yaml
+++ b/.github/workflows/release-docker.yaml
@@ -4,17 +4,20 @@ on:
push:
tags:
- "v*.*.*"
- branches:
- - master
+ branches-ignore:
+ - "renovate/**"
permissions:
packages: write
+env:
+ GOTOOLCHAIN: "local"
+
jobs:
docker:
name: "docker"
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
env:
IMAGE: "ghcr.io/${{ github.repository_owner }}/chii"
@@ -36,7 +39,6 @@ jobs:
key: go-cache-122-${{ hashFiles('**/go.sum') }}-build
restore-keys: |
go-cache-122-${{ hashFiles('**/go.sum') }}-
- go-cache-122-
- run: echo "SHA=${GITHUB_REF##*/}" >> $GITHUB_ENV
if: "${{ startsWith(github.ref, 'refs/tags/') }}"
@@ -63,6 +65,7 @@ jobs:
images: ${{ env.IMAGE }}
tags: |
type=semver,event=tag,pattern=v{{version}}
+ type=raw,value={{commit_date 'YYYY-MM-DD'}}-{{sha}}
type=ref,event=branch
type=ref,event=branch,suffix=-${{ env.SHA }}
@@ -74,7 +77,7 @@ jobs:
password: ${{ github.token }}
- name: Build Final Docker Image
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
with:
context: ./
provenance: false
diff --git a/.github/workflows/release-openapi.yaml b/.github/workflows/release-openapi.yaml
index 6c4ac250e..aa78ef435 100644
--- a/.github/workflows/release-openapi.yaml
+++ b/.github/workflows/release-openapi.yaml
@@ -2,18 +2,21 @@ name: Release(openapi)
on:
push:
- tags:
- - "v*.*.*"
+ branches:
+ - master
workflow_dispatch:
+env:
+ GOTOOLCHAIN: "local"
+
jobs:
openapi:
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
- node-version: 16
+ node-version: 20
- run: npm ci
- run: npm run build
@@ -26,7 +29,7 @@ jobs:
- run: cp ./dist/v0.yaml ./api/open-api/v0.yaml
- name: Create Pull Request
- uses: peter-evans/create-pull-request@v6
+ uses: peter-evans/create-pull-request@v7
with:
path: api
token: ${{ secrets.PAT }}
@@ -34,18 +37,3 @@ jobs:
push-to-fork: Trim21-bot/api
branch: "update-upstream"
author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
-
- - uses: actions/checkout@v4
- with:
- repository: "bangumi/dev-docs"
- path: dev-docs
-# - run: cp ./dist/private.yaml ./dev-docs/api.yaml
-# - name: Create Pull Request
-# uses: peter-evans/create-pull-request@v4
-# with:
-# path: dev-docs
-# token: ${{ secrets.PAT }}
-# title: Update Openapi Specification from bangumi/server
-# push-to-fork: Trim21-bot/dev-docs
-# branch: "update-upstream"
-# author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index af87e55df..dffbf0816 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -5,9 +5,12 @@ on:
tags:
- "v*.*.*"
+env:
+ GOTOOLCHAIN: "local"
+
jobs:
github:
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
diff --git a/.github/workflows/test-openapi.yaml b/.github/workflows/test-openapi.yaml
index e14032bc4..7eb0242c7 100644
--- a/.github/workflows/test-openapi.yaml
+++ b/.github/workflows/test-openapi.yaml
@@ -18,6 +18,9 @@ on:
- "package-lock.json"
- ".github/workflows/test-openapi.yaml"
+env:
+ GOTOOLCHAIN: "local"
+
jobs:
test:
runs-on: "${{ matrix.os }}"
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 9cd675630..20ada4382 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -22,12 +22,15 @@ on:
- "**.go"
- "**.go.json"
+env:
+ GOTOOLCHAIN: "local"
+
jobs:
test:
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
steps:
- run: git clone https://github.com/bangumi/dev-env $HOME/dev-env
- - run: cd ~/dev-env && docker-compose up -d
+ - run: cd ~/dev-env && docker compose up -d
- uses: actions/checkout@v4
with:
@@ -53,7 +56,6 @@ jobs:
key: go-cache-122-${{ hashFiles('**/go.sum') }}-test
restore-keys: |
go-cache-122-${{ hashFiles('**/go.sum') }}-
- go-cache-122-
- run: go get -t ./...
@@ -70,7 +72,7 @@ jobs:
MYSQL_DB: bangumi
REDIS_URI: "redis://:redis-pass@127.0.0.1:6379/0"
- - uses: codecov/codecov-action@v4
+ - uses: codecov/codecov-action@v5
with:
files: coverage.out
token: ${{ secrets.CODECOV_TOKEN }} # required
diff --git a/.golangci.yaml b/.golangci.yaml
index 12d004952..380125165 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -18,9 +18,7 @@ run:
# If false (default) - golangci-lint acquires file lock on start.
allow-parallel-runners: true
- skip-files: []
-
- go: "1.22"
+ go: "1.23"
# output configuration options
output:
@@ -106,8 +104,6 @@ linters-settings:
- standard # Captures all standard packages if they do not match another section.
- default # Contains all imports that could not be matched to another section type.
- prefix(github.com/bangumi/server) # Groups all imports with the specified Prefix.
- sectionSeparators:
- - newLine
depguard:
rules:
@@ -131,7 +127,7 @@ linters-settings:
- pkg: "github.com/golang/mock"
desc: 'use "github.com/stretchr/testify/mock" and "github.com/vektra/mockery"'
- gomnd:
+ mnd:
# settings:
# mnd:
# the list of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description.
@@ -141,8 +137,8 @@ linters-settings:
- operation
- return
- assign
- ignored-functions: strconv\..*,time\..*,make,math\..*,strings\..*
- ignored-numbers: 1,2,3,10,100,1000,10000
+ ignored-functions: [strconv\..*, time\..*, make, math\..*, strings\..*]
+ ignored-numbers: ["1", "2", "3", "10", "100", "1000", "10000"]
gosimple:
# Select the Go version to target. The default is '1.13'.
@@ -181,7 +177,7 @@ linters-settings:
checks: ["all"]
stylecheck:
- # Select the Go version to target. The default is '1.13'.
+ # Select the Go version to target. The default is '1.13'.
testpackage:
# regexp pattern to skip files
@@ -203,12 +199,9 @@ linters-settings:
nlreturn:
block-size: 3
- ifshort:
- # Maximum length of vars declaration measured in number of lines, after which linter won't suggest using short syntax.
- # Has higher priority than max-decl-chars.
- max-decl-lines: 1
- # Maximum length of vars declaration measured in number of characters, after which linter won't suggest using short syntax.
- max-decl-chars: 30
+ gosec:
+ excludes:
+ - G115
tagliatelle:
# Check the struck tag name case.
@@ -236,8 +229,9 @@ linters:
- errchkjson
- errname
- errorlint
+ # https://github.com/golangci/golangci-lint/issues/5065
- exhaustive
- - exportloopref
+ - copyloopvar
- forbidigo
- forcetypeassert
- funlen
@@ -250,9 +244,8 @@ linters:
- gocritic
- gocyclo
- godot
- - goerr113
- gofmt
- - gomnd
+ - mnd
- gomoddirectives
- gomodguard
- goprintffuncname
@@ -319,7 +312,7 @@ issues:
- source: 'var .* sync\.Once'
linters: [gochecknoglobals]
- - linters: [goerr113, errorlint]
+ - linters: [err113, errorlint]
source: "if err == redis.Nil {"
# https://github.com/kunwardeep/paralleltest/issues/8
diff --git a/Taskfile.yaml b/Taskfile.yaml
index 5f8d4ed0b..c27514bbe 100644
--- a/Taskfile.yaml
+++ b/Taskfile.yaml
@@ -27,7 +27,7 @@ tasks:
generates:
- ./dist/chii.exe
cmds:
- - go build -ldflags '-w -s' -trimpath -o dist/chii.exe main.go
+ - go build -trimpath -o dist/chii.exe main.go
env:
CGO_ENABLED: "0"
@@ -35,7 +35,7 @@ tasks:
silent: true
desc: Run 'golangci-lint'
cmds:
- - golangci-lint run --fix
+ - golangci-lint --path-prefix "{{ .TASKFILE_DIR }}" run --fix
test:
desc: Run mocked tests, need nothing.
@@ -44,6 +44,31 @@ tasks:
env:
CGO_ENABLED: "0"
+ web:
+ desc: Run Web Server
+ aliases:
+ - serve
+ - server
+ cmds:
+ - go run main.go --config config.yaml web
+
+ consumer:
+ desc: Run Kafka Consumer
+ aliases:
+ - canal
+ cmds:
+ - go run main.go canal --config config.yaml
+
+ openapi-test:
+ desc: Test OpenAPI Schema
+ cmds:
+ - npm run test
+
+ openapi:
+ desc: Build OpenAPI Schema
+ cmds:
+ - npm run build
+
bench:
desc: Run benchmark
cmds:
diff --git a/canal/canal.go b/canal/canal.go
index db51a2d5a..9ad7ea470 100644
--- a/canal/canal.go
+++ b/canal/canal.go
@@ -26,16 +26,19 @@ import (
"github.com/bangumi/server/config"
"github.com/bangumi/server/dal"
+ "github.com/bangumi/server/internal/character"
+ "github.com/bangumi/server/internal/person"
"github.com/bangumi/server/internal/pkg/cache"
"github.com/bangumi/server/internal/pkg/driver"
"github.com/bangumi/server/internal/pkg/logger"
"github.com/bangumi/server/internal/pkg/sys"
"github.com/bangumi/server/internal/search"
"github.com/bangumi/server/internal/subject"
+ "github.com/bangumi/server/internal/tag"
"github.com/bangumi/server/web/session"
)
-const groupID = "my-group"
+const groupID = "go-canal"
var errNoTopic = fmt.Errorf("missing search events topic")
@@ -50,16 +53,6 @@ func Main() error {
return errNoTopic
}
- var opt fx.Option
- switch cfg.Canal.Broker {
- case "redis":
- opt = fx.Provide(newRedisStream)
- case "kafka":
- opt = fx.Provide(newKafkaStream)
- default:
- return fmt.Errorf("broker not supported, only support redis/kafka as debezium broker") // nolint: goerr113
- }
-
var h *eventHandler
di := fx.New(
fx.NopLogger,
@@ -69,15 +62,18 @@ func Main() error {
// driver and connector
fx.Provide(
- driver.NewMysqlConnectionPool,
- driver.NewRedisClient, logger.Copy, cache.NewRedisCache,
- subject.NewMysqlRepo, search.New, session.NewMysqlRepo, session.New,
+ driver.NewMysqlSqlDB,
+ driver.NewRueidisClient, logger.Copy, cache.NewRedisCache,
+ subject.NewMysqlRepo, character.NewMysqlRepo, person.NewMysqlRepo,
+ search.New, session.NewMysqlRepo, session.New,
driver.NewS3,
+ tag.NewCachedRepo,
+ tag.NewMysqlRepo,
newEventHandler,
),
- opt,
+ fx.Provide(newKafkaStream),
fx.Populate(&h),
)
diff --git a/canal/event.go b/canal/event.go
index 7af69f9a7..4d7fc3d0f 100644
--- a/canal/event.go
+++ b/canal/event.go
@@ -19,8 +19,8 @@ import (
"encoding/json"
"sync/atomic"
- "github.com/aws/aws-sdk-go/service/s3"
- "github.com/redis/go-redis/v9"
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/redis/rueidis"
"github.com/trim21/errgo"
"go.uber.org/zap"
@@ -28,6 +28,7 @@ import (
"github.com/bangumi/server/internal/model"
"github.com/bangumi/server/internal/pkg/logger/log"
"github.com/bangumi/server/internal/search"
+ "github.com/bangumi/server/internal/tag"
"github.com/bangumi/server/web/session"
)
@@ -35,10 +36,11 @@ func newEventHandler(
log *zap.Logger,
appConfig config.AppConfig,
session session.Manager,
- redis *redis.Client,
+ redis rueidis.Client,
stream Stream,
+ tag tag.Repo,
search search.Client,
- s3 *s3.S3,
+ s3 *s3.Client,
) *eventHandler {
return &eventHandler{
redis: redis,
@@ -48,6 +50,7 @@ func newEventHandler(
s3: s3,
stream: stream,
log: log.Named("eventHandler"),
+ tag: tag,
}
}
@@ -58,18 +61,19 @@ type eventHandler struct {
log *zap.Logger
search search.Client
stream Stream
- s3 *s3.S3 // optional, check nil before use
- redis *redis.Client
+ s3 *s3.Client // optional, check nil before use
+ redis rueidis.Client
+ tag tag.Repo
}
func (e *eventHandler) start() error {
ee := e.stream.Read(context.Background(), func(msg Msg) error {
- e.log.Debug("new message", zap.String("stream", msg.Stream), zap.String("id", msg.ID))
+ e.log.Debug("new message", zap.String("topic", msg.Topic), zap.String("id", msg.ID))
err := e.onMessage(msg.Key, msg.Value)
if err != nil {
e.log.Error("failed to handle stream msg",
- zap.Error(err), zap.String("stream", msg.Stream), zap.String("id", msg.ID))
+ zap.Error(err), zap.String("stream", msg.Topic), zap.String("id", msg.ID))
return errgo.Trace(err)
}
@@ -85,10 +89,10 @@ func (e *eventHandler) Close() error {
return nil
}
-func (e *eventHandler) OnUserPasswordChange(id model.UserID) error {
+func (e *eventHandler) OnUserPasswordChange(ctx context.Context, id model.UserID) error {
e.log.Info("user change password", log.User(id))
- if err := e.session.RevokeUser(context.Background(), id); err != nil {
+ if err := e.session.RevokeUser(ctx, id); err != nil {
e.log.Error("failed to revoke user", log.User(id), zap.Error(err))
return errgo.Wrap(err, "session.RevokeUser")
}
@@ -103,26 +107,29 @@ func (e *eventHandler) onMessage(key, value []byte) error {
return nil
}
- var k messageKey
- if err := json.Unmarshal(key, &k); err != nil {
+ var p Payload
+ if err := json.Unmarshal(value, &p); err != nil {
+ e.log.Warn("failed to parse kafka value", zap.String("table", p.Source.Table), zap.Error(err))
return nil
}
- var v messageValue
- if err := json.Unmarshal(value, &v); err != nil {
- return nil
- }
+ e.log.Debug("new message", zap.String("table", p.Source.Table))
- e.log.Debug("new message", zap.String("table", v.Payload.Source.Table))
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
var err error
- switch v.Payload.Source.Table {
+ switch p.Source.Table {
case "chii_subject_fields":
- err = e.OnSubjectField(k.Payload, v.Payload)
+ err = e.OnSubjectField(ctx, key, p)
case "chii_subjects":
- err = e.OnSubject(k.Payload, v.Payload)
+ err = e.OnSubject(ctx, key, p)
+ case "chii_characters":
+ err = e.OnCharacter(ctx, key, p)
+ case "chii_persons":
+ err = e.OnPerson(ctx, key, p)
case "chii_members":
- err = e.OnUserChange(k.Payload, v.Payload)
+ err = e.OnUserChange(ctx, key, p)
}
return err
@@ -138,15 +145,7 @@ const (
// https://debezium.io/documentation/reference/connectors/mysql.html
// Table 9. Overview of change event basic content
-type messageKey struct {
- Payload json.RawMessage `json:"payload"`
-}
-
-type messageValue struct {
- Payload payload `json:"payload"`
-}
-
-type payload struct {
+type Payload struct {
Before json.RawMessage `json:"before"`
After json.RawMessage `json:"after"`
Source source `json:"source"`
diff --git a/canal/ifce.go b/canal/ifce.go
index 1de12767c..892d546db 100644
--- a/canal/ifce.go
+++ b/canal/ifce.go
@@ -19,10 +19,10 @@ import (
)
type Msg struct {
- ID string
- Stream string
- Key []byte
- Value []byte
+ ID string
+ Topic string
+ Key []byte
+ Value []byte
}
type Stream interface {
diff --git a/canal/on_character.go b/canal/on_character.go
new file mode 100644
index 000000000..525d1b7d8
--- /dev/null
+++ b/canal/on_character.go
@@ -0,0 +1,45 @@
+package canal
+
+import (
+ "context"
+ "encoding/json"
+
+ "github.com/trim21/errgo"
+ "go.uber.org/zap"
+
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/search"
+)
+
+type CharacterKey struct {
+ ID model.CharacterID `json:"crt_id"`
+}
+
+func (e *eventHandler) OnCharacter(ctx context.Context, key json.RawMessage, payload Payload) error {
+ var k CharacterKey
+ if err := json.Unmarshal(key, &k); err != nil {
+ return err
+ }
+ return e.onCharacterChange(ctx, k.ID, payload.Op)
+}
+
+func (e *eventHandler) onCharacterChange(ctx context.Context, characterID model.CharacterID, op string) error {
+ switch op {
+ case opCreate:
+ if err := e.search.EventAdded(ctx, characterID, search.SearchTargetCharacter); err != nil {
+ return errgo.Wrap(err, "search.OnCharacterAdded")
+ }
+ case opUpdate, opSnapshot:
+ if err := e.search.EventUpdate(ctx, characterID, search.SearchTargetCharacter); err != nil {
+ return errgo.Wrap(err, "search.OnCharacterUpdate")
+ }
+ case opDelete:
+ if err := e.search.EventDelete(ctx, characterID, search.SearchTargetCharacter); err != nil {
+ return errgo.Wrap(err, "search.OnCharacterDelete")
+ }
+ default:
+ e.log.Warn("unexpected operator", zap.String("op", op))
+ }
+
+ return nil
+}
diff --git a/canal/on_person.go b/canal/on_person.go
new file mode 100644
index 000000000..f5f75b4fa
--- /dev/null
+++ b/canal/on_person.go
@@ -0,0 +1,44 @@
+package canal
+
+import (
+ "context"
+ "encoding/json"
+
+ "github.com/trim21/errgo"
+ "go.uber.org/zap"
+
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/search"
+)
+
+type PersonKey struct {
+ ID model.PersonID `json:"prsn_id"`
+}
+
+func (e *eventHandler) OnPerson(ctx context.Context, key json.RawMessage, payload Payload) error {
+ var k PersonKey
+ if err := json.Unmarshal(key, &k); err != nil {
+ return err
+ }
+ return e.onPersonChange(ctx, k.ID, payload.Op)
+}
+
+func (e *eventHandler) onPersonChange(ctx context.Context, personID model.PersonID, op string) error {
+ switch op {
+ case opCreate:
+ if err := e.search.EventAdded(ctx, personID, search.SearchTargetPerson); err != nil {
+ return errgo.Wrap(err, "search.OnPersonAdded")
+ }
+ case opUpdate, opSnapshot:
+ if err := e.search.EventUpdate(ctx, personID, search.SearchTargetPerson); err != nil {
+ return errgo.Wrap(err, "search.OnPersonUpdate")
+ }
+ case opDelete:
+ if err := e.search.EventDelete(ctx, personID, search.SearchTargetPerson); err != nil {
+ return errgo.Wrap(err, "search.OnPersonDelete")
+ }
+ default:
+ e.log.Warn("unexpected operator", zap.String("op", op))
+ }
+ return nil
+}
diff --git a/canal/on_subject.go b/canal/on_subject.go
index 214f9ddfa..5ecf0b406 100644
--- a/canal/on_subject.go
+++ b/canal/on_subject.go
@@ -19,47 +19,55 @@ import (
"encoding/json"
"github.com/trim21/errgo"
+ "go.uber.org/zap"
"github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/search"
)
-func (e *eventHandler) OnSubject(key json.RawMessage, payload payload) error {
+type SubjectKey struct {
+ ID model.SubjectID `json:"subject_id"`
+}
+
+type SubjectFieldKey struct {
+ ID model.SubjectID `json:"field_sid"`
+}
+
+func (e *eventHandler) OnSubject(ctx context.Context, key json.RawMessage, payload Payload) error {
var k SubjectKey
if err := json.Unmarshal(key, &k); err != nil {
- return nil
+ return err
}
- return e.onSubjectChange(k.ID, payload.Op)
+ return e.onSubjectChange(ctx, k.ID, payload.Op)
}
-func (e *eventHandler) OnSubjectField(key json.RawMessage, payload payload) error {
+func (e *eventHandler) OnSubjectField(ctx context.Context, key json.RawMessage, payload Payload) error {
var k SubjectFieldKey
if err := json.Unmarshal(key, &k); err != nil {
- return nil
+ return err
}
- return e.onSubjectChange(k.ID, payload.Op)
+ return e.onSubjectChange(ctx, k.ID, payload.Op)
}
-func (e *eventHandler) onSubjectChange(subjectID model.SubjectID, op string) error {
+func (e *eventHandler) onSubjectChange(ctx context.Context, subjectID model.SubjectID, op string) error {
switch op {
- case opCreate, opUpdate, opSnapshot:
- if err := e.search.OnSubjectUpdate(context.TODO(), subjectID); err != nil {
+ case opCreate:
+ if err := e.search.EventAdded(ctx, subjectID, search.SearchTargetSubject); err != nil {
+ return errgo.Wrap(err, "search.OnSubjectAdded")
+ }
+ case opUpdate, opSnapshot:
+ if err := e.search.EventUpdate(ctx, subjectID, search.SearchTargetSubject); err != nil {
return errgo.Wrap(err, "search.OnSubjectUpdate")
}
case opDelete:
- if err := e.search.OnSubjectDelete(context.TODO(), subjectID); err != nil {
+ if err := e.search.EventDelete(ctx, subjectID, search.SearchTargetSubject); err != nil {
return errgo.Wrap(err, "search.OnSubjectDelete")
}
+ default:
+ e.log.Warn("unexpected operator", zap.String("op", op))
}
return nil
}
-
-type SubjectKey struct {
- ID model.SubjectID `json:"subject_id"`
-}
-
-type SubjectFieldKey struct {
- ID model.SubjectID `json:"field_sid"`
-}
diff --git a/canal/on_user.go b/canal/on_user.go
index 34c08902e..5447ae2ef 100644
--- a/canal/on_user.go
+++ b/canal/on_user.go
@@ -20,8 +20,11 @@ import (
"encoding/json"
"fmt"
"strings"
+ "time"
- "github.com/aws/aws-sdk-go/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/redis/rueidis"
"github.com/samber/lo"
"github.com/trim21/errgo"
"go.uber.org/zap"
@@ -30,7 +33,7 @@ import (
"github.com/bangumi/server/internal/pkg/logger/log"
)
-func (e *eventHandler) OnUserChange(key json.RawMessage, payload payload) error {
+func (e *eventHandler) OnUserChange(ctx context.Context, key json.RawMessage, payload Payload) error {
var k UserKey
if err := json.Unmarshal(key, &k); err != nil {
e.log.Error("failed to unmarshal json", zap.Error(err))
@@ -51,17 +54,19 @@ func (e *eventHandler) OnUserChange(key json.RawMessage, payload payload) error
}
if before.Password != after.Password {
- err := e.OnUserPasswordChange(k.ID)
+ err := e.OnUserPasswordChange(ctx, k.ID)
if err != nil {
e.log.Error("failed to clear cache", zap.Error(err))
}
}
if before.NewNotify != after.NewNotify {
- e.redis.Publish(context.Background(), fmt.Sprintf("event-user-notify-%d", k.ID), redisUserChannel{
- UserID: k.ID,
- NewNotify: after.NewNotify,
- })
+ e.redis.Do(ctx, e.redis.B().Publish().
+ Channel(fmt.Sprintf("event-user-notify-%d", k.ID)).
+ Message(rueidis.JSON(redisUserChannel{
+ UserID: k.ID,
+ NewNotify: after.NewNotify,
+ })).Build())
}
if before.Avatar != after.Avatar {
@@ -70,14 +75,14 @@ func (e *eventHandler) OnUserChange(key json.RawMessage, payload payload) error
}
e.log.Debug("clear user avatar cache", log.User(k.ID))
- go e.clearImageCache(after.Avatar)
+ go e.clearImageCache(context.Background(), after.Avatar)
}
}
return nil
}
-func (e *eventHandler) clearImageCache(avatar string) {
+func (e *eventHandler) clearImageCache(ctx context.Context, avatar string) {
p, q, ok := strings.Cut(avatar, "?")
if !ok {
p = avatar
@@ -91,34 +96,37 @@ func (e *eventHandler) clearImageCache(avatar string) {
e.log.Debug("clear image for prefix", zap.String("avatar", avatar), zap.String("prefix", p))
- err := e.s3.ListObjectsV2PagesWithContext(context.Background(),
- &s3.ListObjectsV2Input{Bucket: &e.config.S3ImageResizeBucket, Prefix: &p},
- func(output *s3.ListObjectsV2Output, b bool) bool {
- if len(output.Contents) == 0 {
- return false
- }
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
- _, err := e.s3.DeleteObjects(&s3.DeleteObjectsInput{
- Bucket: &e.config.S3ImageResizeBucket,
- Delete: &s3.Delete{
- Objects: lo.Map(output.Contents, func(item *s3.Object, index int) *s3.ObjectIdentifier {
- return &s3.ObjectIdentifier{
- Key: item.Key,
- }
- }),
- },
- })
+ pages := s3.NewListObjectsV2Paginator(
+ e.s3,
+ &s3.ListObjectsV2Input{Bucket: &e.config.S3ImageResizeBucket, Prefix: &p},
+ )
- if err != nil {
- e.log.Error("failed to clear s3 cached image", zap.Error(err))
- }
+ for pages.HasMorePages() {
+ output, err := pages.NextPage(ctx)
+ if err != nil {
+ break
+ }
- return true
- },
- )
+ if len(output.Contents) == 0 {
+ break
+ }
- if err != nil {
- e.log.Error("failed to clear s3 cached image", zap.Error(err))
+ _, err = e.s3.DeleteObjects(ctx, &s3.DeleteObjectsInput{
+ Bucket: &e.config.S3ImageResizeBucket,
+ Delete: &types.Delete{
+ Objects: lo.Map(output.Contents, func(item types.Object, index int) types.ObjectIdentifier {
+ return types.ObjectIdentifier{
+ Key: item.Key,
+ }
+ }),
+ },
+ })
+ if err != nil {
+ e.log.Error("failed to clear s3 cached image", zap.Error(err))
+ }
}
}
diff --git a/canal/stream/redis_stream.go b/canal/stream/redis_stream.go
deleted file mode 100644
index bc3198e06..000000000
--- a/canal/stream/redis_stream.go
+++ /dev/null
@@ -1,183 +0,0 @@
-// 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 stream
-
-import (
- "context"
- "errors"
- "time"
-
- "github.com/redis/go-redis/v9"
-)
-
-// A Message is a consumed message from a redis stream.
-type Message struct {
- Stream string
- ID string
- Values map[string]any
-}
-
-type config struct {
- group string
- consumer string
- streams []string // list of streams and ids, e.g. stream1 stream2 id1 id2
- count int64
- block time.Duration
- noAck bool
-}
-
-// An Option modifies the config.
-type Option func(*config)
-
-// WithStream adds a stream to the consumer.
-func WithStream(stream string) Option {
- return func(cfg *config) {
- cfg.streams = append(cfg.streams, stream)
- }
-}
-
-// WithCount sets the count for the config.
-func WithCount(cnt int64) Option {
- return func(cfg *config) {
- cfg.count = cnt
- }
-}
-
-// WithBlock sets the block field of the config.
-func WithBlock(duration time.Duration) Option {
- return func(cfg *config) {
- cfg.block = duration
- }
-}
-
-// WithNoAck sets the noAck field of the config.
-func WithNoAck(noAck bool) Option {
- return func(cfg *config) {
- cfg.noAck = noAck
- }
-}
-
-// A Consumer consumes messages from a stream.
-type Consumer struct {
- client *redis.Client
- cfg *config
- lastIDs map[string]string
-}
-
-// New creates a new consumer.
-func New(client *redis.Client, group, consumer string, options ...Option) *Consumer {
- cfg := &config{
- group: group,
- consumer: consumer,
- }
- for _, opt := range options {
- opt(cfg)
- }
- lastIDs := make(map[string]string)
- for _, stream := range cfg.streams {
- lastIDs[stream] = "0-0"
- }
-
- return &Consumer{
- client: client,
- cfg: cfg,
- lastIDs: lastIDs,
- }
-}
-
-// Read reads messages from the stream.
-func (c *Consumer) Read(ctx context.Context) ([]Message, error) {
- for {
- streams := make([]string, 0, len(c.cfg.streams)*2)
- streams = append(streams, c.cfg.streams...)
- for _, stream := range c.cfg.streams {
- streams = append(streams, c.lastIDs[stream])
- }
-
- cmd := c.client.XReadGroup(ctx, &redis.XReadGroupArgs{
- Group: c.cfg.group,
- Consumer: c.cfg.consumer,
- Streams: streams,
- Count: c.cfg.count,
- Block: c.cfg.block,
- NoAck: c.cfg.noAck,
- })
-
- vals, err := cmd.Result()
- if err == redis.Nil {
- if c.cfg.block >= 0 {
- continue
- }
-
- return nil, nil //nolint:revive
- } else if err != nil {
- return nil, err
- }
-
- allLatest := true
- for _, lastID := range c.lastIDs {
- if lastID != ">" {
- allLatest = false
- }
- }
-
- var msgs []Message
- for _, stream := range vals {
- if len(stream.Messages) == 0 {
- c.lastIDs[stream.Stream] = ">"
- }
- for _, msg := range stream.Messages {
- msgs = append(msgs, Message{
- Stream: stream.Stream,
- ID: msg.ID,
- Values: msg.Values,
- })
- c.lastIDs[stream.Stream] = msg.ID
- }
- }
- if len(msgs) > 0 || allLatest {
- return msgs, nil
- }
- }
-}
-
-// Ack acknowledges the messages.
-func (c *Consumer) Ack(ctx context.Context, msgs ...Message) error {
- if len(msgs) == 0 {
- return nil
- }
-
- if len(msgs) == 1 {
- msg := msgs[0]
- return errors.Join(
- c.client.XAck(ctx, msg.Stream, c.cfg.group, msg.ID).Err(),
- c.client.XDel(ctx, msg.Stream, msg.ID).Err(),
- )
- }
-
- ids := map[string][]string{}
- for _, msg := range msgs {
- ids[msg.Stream] = append(ids[msg.Stream], msg.ID)
- }
-
- _, err := c.client.Pipelined(ctx, func(p redis.Pipeliner) error {
- for stream, msgIDs := range ids {
- p.XAck(ctx, stream, c.cfg.group, msgIDs...)
- p.XDel(ctx, stream, msgIDs...)
- }
- return nil
- })
- return err
-}
diff --git a/canal/stream_kafka.go b/canal/stream_kafka.go
index 5dbbd83b4..8166a9761 100644
--- a/canal/stream_kafka.go
+++ b/canal/stream_kafka.go
@@ -74,10 +74,10 @@ func (s *kafkaStream) Read(ctx context.Context, onMessage func(msg Msg) error) e
s.log.Debug("new message", zap.String("topic", msg.Topic))
m := Msg{
- ID: strconv.FormatInt(msg.Offset, 10),
- Stream: msg.Topic,
- Key: msg.Key,
- Value: msg.Value,
+ ID: strconv.FormatInt(msg.Offset, 10),
+ Topic: msg.Topic,
+ Key: msg.Key,
+ Value: msg.Value,
}
if err := onMessage(m); err != nil {
diff --git a/canal/stream_redis.go b/canal/stream_redis.go
deleted file mode 100644
index d5dd39ac8..000000000
--- a/canal/stream_redis.go
+++ /dev/null
@@ -1,143 +0,0 @@
-// 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 canal
-
-import (
- "context"
- "reflect"
- "sync/atomic"
- "time"
-
- "github.com/redis/go-redis/v9"
- "github.com/samber/lo"
- "github.com/trim21/errgo"
- "go.uber.org/zap"
-
- "github.com/bangumi/server/canal/stream"
- "github.com/bangumi/server/config"
- "github.com/bangumi/server/internal/pkg/logger"
-)
-
-func newRedisStream(cfg config.AppConfig, redisClient *redis.Client) (Stream, error) {
- var ch = make(chan Msg, 1)
-
- reader := stream.New(
- redisClient,
- groupID,
- "canal",
- lo.Map(cfg.Canal.Topics, func(item string, _ int) stream.Option {
- return stream.WithStream(item)
- })...,
- )
-
- r := &redisStream{
- log: logger.Named("canal.search.stream"),
- ch: ch,
- cfg: cfg,
- redis: redisClient,
- reader: reader,
- }
-
- for _, s := range r.cfg.Canal.Topics {
- var infos, err = r.redis.XInfoGroups(context.Background(), s).Result()
- if err != nil {
- if err.Error() != "ERR no such key" {
- return nil, errgo.Trace(err)
- }
- }
-
- groups := lo.SliceToMap(infos, func(item redis.XInfoGroup) (string, bool) {
- return item.Name, true
- })
-
- if !groups[groupID] {
- err := r.redis.XGroupCreateMkStream(context.Background(), s, groupID, "$").Err()
- if err != nil {
- return nil, errgo.Trace(err)
- }
- }
- }
-
- return r, nil
-}
-
-type redisStream struct {
- log *zap.Logger
- redis *redis.Client
- ch chan Msg
- cfg config.AppConfig
- closed atomic.Bool
- reader *stream.Consumer
-}
-
-func (r *redisStream) Read(ctx context.Context, onMessage func(msg Msg) error) error {
- for {
- if r.closed.Load() {
- return nil
- }
-
- rr, err := r.reader.Read(ctx)
- if err != nil {
- r.log.Error("failed to read new messages", zap.Error(err))
- time.Sleep(time.Second)
- continue
- }
-
- for _, msg := range rr {
- r.log.Debug("new message", zap.String("id", msg.ID), zap.String("s", msg.Stream))
-
- rawKey := msg.Values["key"]
- rawValue := msg.Values["value"]
-
- if rawKey == nil || rawValue == nil {
- _ = r.reader.Ack(context.Background(), msg)
- continue
- }
-
- value, ok := rawValue.(string)
- if !ok {
- r.log.Error("failed to handle event", zap.String("id", msg.ID),
- zap.String("value-type", reflect.TypeOf(rawKey).String()))
- _ = r.reader.Ack(context.Background(), msg)
- continue
- }
-
- key, ok := rawKey.(string)
- if !ok {
- r.log.Error("failed to handle event", zap.String("id", msg.ID),
- zap.String("key-type", reflect.TypeOf(rawKey).String()))
- _ = r.reader.Ack(context.Background(), msg)
- continue
- }
-
- if err := onMessage(Msg{ID: msg.ID, Stream: msg.Stream, Key: []byte(key), Value: []byte(value)}); err != nil {
- return errgo.Trace(err)
- }
-
- if err := r.reader.Ack(ctx, msg); err != nil {
- return errgo.Trace(err)
- }
- }
- }
-}
-
-func (r *redisStream) Close() error {
- r.closed.Store(true)
- return nil
-}
-
-func (r *redisStream) Ack(ctx context.Context, msg Msg) error {
- return r.reader.Ack(ctx, stream.Message{Stream: msg.Stream, ID: msg.ID})
-}
diff --git a/cmd/archive/main.go b/cmd/archive/main.go
index ca504be95..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,
@@ -180,6 +180,14 @@ type Score struct {
Field10 uint32 `json:"10"`
}
+type Favorite struct {
+ Wish uint32 `json:"wish"`
+ Done uint32 `json:"done"`
+ Doing uint32 `json:"doing"`
+ OnHold uint32 `json:"on_hold"`
+ Dropped uint32 `json:"dropped"`
+}
+
type Subject struct {
ID model.SubjectID `json:"id"`
Type model.SubjectType `json:"type"`
@@ -190,15 +198,19 @@ type Subject struct {
Summary string `json:"summary"`
Nsfw bool `json:"nsfw"`
- Tags []Tag `json:"tags"`
- Score float64 `json:"score"`
- ScoreDetails Score `json:"score_details"`
- Rank uint32 `json:"rank"`
+ Tags []Tag `json:"tags"`
+ Score float64 `json:"score"`
+ ScoreDetails Score `json:"score_details"`
+ Rank uint32 `json:"rank"`
+ Date string `json:"date"`
+ Favorite Favorite `json:"favorite"`
+
+ Series bool `json:"series"`
}
type Tag struct {
Name string `json:"name"`
- Count int `json:"count"`
+ Count uint `json:"count"`
}
func exportSubjects(q *query.Query, w io.Writer) {
@@ -219,7 +231,7 @@ func exportSubjects(q *query.Query, w io.Writer) {
return tags[i].Count >= tags[j].Count
})
- tags = lo.Filter(lo.Slice(tags, 0, 11), func(item model.Tag, index int) bool { //nolint:gomnd
+ tags = lo.Filter(lo.Slice(tags, 0, 11), func(item model.Tag, index int) bool { //nolint:mnd
return utf8.RuneCountInString(item.Name) < 10 || item.Count >= 10
})
@@ -238,12 +250,17 @@ func exportSubjects(q *query.Query, w io.Writer) {
6*f.Rate6+7*f.Rate7+8*f.Rate8+9*f.Rate9+10*f.Rate10) / float64(total)
}
+ encodedDate := ""
+ if !subject.Fields.Date.IsZero() {
+ encodedDate = subject.Fields.Date.Format("2006-01-02")
+ }
+
encode(w, Subject{
ID: subject.ID,
Type: subject.TypeID,
- Name: subject.Name,
- NameCN: subject.NameCN,
- Infobox: subject.Infobox,
+ Name: string(subject.Name),
+ NameCN: string(subject.NameCN),
+ Infobox: string(subject.Infobox),
Platform: subject.Platform,
Summary: subject.Summary,
Nsfw: subject.Nsfw,
@@ -262,18 +279,29 @@ func exportSubjects(q *query.Query, w io.Writer) {
Field9: subject.Fields.Rate9,
Field10: subject.Fields.Rate10,
},
+ Date: encodedDate,
+ Series: subject.Series,
+ Favorite: Favorite{
+ Wish: subject.Wish,
+ Done: subject.Done,
+ Doing: subject.Doing,
+ OnHold: subject.OnHold,
+ Dropped: subject.Dropped,
+ },
})
}
}
}
type Person struct {
- ID model.PersonID `json:"id"`
- Name string `json:"name"`
- Type uint8 `json:"type"`
- Career []string `json:"career"`
- Infobox string `json:"infobox"`
- Summary string `json:"summary"`
+ ID model.PersonID `json:"id"`
+ Name string `json:"name"`
+ Type uint8 `json:"type"`
+ Career []string `json:"career"`
+ Infobox string `json:"infobox"`
+ Summary string `json:"summary"`
+ Comments uint32 `json:"comments"`
+ Collects uint32 `json:"collects"`
}
func exportPersons(q *query.Query, w io.Writer) {
@@ -286,12 +314,14 @@ func exportPersons(q *query.Query, w io.Writer) {
for _, p := range persons {
encode(w, Person{
- ID: p.ID,
- Name: p.Name,
- Type: p.Type,
- Career: careers(p),
- Infobox: p.Infobox,
- Summary: p.Summary,
+ ID: p.ID,
+ Name: p.Name,
+ Type: p.Type,
+ Career: careers(p),
+ Infobox: p.Infobox,
+ Summary: p.Summary,
+ Comments: p.Comment,
+ Collects: p.Collects,
})
}
}
@@ -336,11 +366,13 @@ func careers(p *dao.Person) []string {
}
type Character struct {
- ID model.CharacterID `json:"id"`
- Role uint8 `json:"role"`
- Name string `json:"name"`
- Infobox string `json:"infobox"`
- Summary string `json:"summary"`
+ ID model.CharacterID `json:"id"`
+ Role uint8 `json:"role"`
+ Name string `json:"name"`
+ Infobox string `json:"infobox"`
+ Summary string `json:"summary"`
+ Comments uint32 `json:"comments"`
+ Collects uint32 `json:"collects"`
}
func exportCharacters(q *query.Query, w io.Writer) {
@@ -353,11 +385,13 @@ func exportCharacters(q *query.Query, w io.Writer) {
for _, c := range characters {
encode(w, Character{
- ID: c.ID,
- Name: c.Name,
- Role: c.Role,
- Infobox: c.Infobox,
- Summary: c.Summary,
+ ID: c.ID,
+ Name: c.Name,
+ Role: c.Role,
+ Infobox: c.Infobox,
+ Summary: c.Summary,
+ Comments: c.Comment,
+ Collects: c.Collects,
})
}
}
@@ -370,6 +404,7 @@ type Episode struct {
Description string `json:"description"`
AirDate string `json:"airdate"`
Disc uint8 `json:"disc"`
+ Duration string `json:"duration"`
SubjectID model.SubjectID `json:"subject_id"`
Sort float32 `json:"sort"`
Type episode.Type `json:"type"`
@@ -394,6 +429,7 @@ func exportEpisodes(q *query.Query, w io.Writer) {
NameCn: e.NameCn,
Sort: e.Sort,
SubjectID: e.SubjectID,
+ Duration: e.Duration,
Description: e.Desc,
Type: e.Type,
AirDate: e.Airdate,
@@ -407,7 +443,7 @@ type SubjectRelation struct {
SubjectID model.SubjectID `json:"subject_id"`
RelationType uint16 `json:"relation_type"`
RelatedSubjectID model.SubjectID `json:"related_subject_id"`
- Order uint8 `json:"order"`
+ Order uint16 `json:"order"`
}
func exportSubjectRelations(q *query.Query, w io.Writer) {
@@ -459,7 +495,7 @@ type SubjectCharacter struct {
CharacterID model.CharacterID `json:"character_id"`
SubjectID model.SubjectID `json:"subject_id"`
Type uint8 `json:"type"`
- Order uint8 `json:"order"`
+ Order uint16 `json:"order"`
}
func exportSubjectCharacterRelations(q *query.Query, w io.Writer) {
diff --git a/cmd/gen/gorm/main.go b/cmd/gen/gorm/main.go
index d53ae03a4..baccbecba 100644
--- a/cmd/gen/gorm/main.go
+++ b/cmd/gen/gorm/main.go
@@ -24,6 +24,7 @@ NOTICE:
package main
import (
+ "path/filepath"
"strings"
"gorm.io/gen"
@@ -54,13 +55,14 @@ func DeprecatedFiled(s string) gen.ModelOpt {
}
const createdTime = "CreatedTime"
+const updateTime = "UpdatedTime"
// generate code.
func main() {
g := gen.NewGenerator(gen.Config{
- OutPath: "./dal/query/",
- OutFile: "./gen.go",
- ModelPkgPath: "./dao/",
+ OutPath: filepath.Clean("./dal/query/"),
+ OutFile: "gen.go",
+ ModelPkgPath: "dao",
WithUnitTest: false,
// if you want the nullable field generation property to be pointer type, set FieldNullable true
@@ -78,6 +80,7 @@ func main() {
"github.com/bangumi/server/dal/utiltype",
"gorm.io/plugin/soft_delete",
)
+
g.WithJSONTagNameStrategy(func(_ string) string {
return ""
})
@@ -89,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)
}
@@ -157,10 +160,33 @@ func main() {
modelField := g.GenerateModelAs("chii_memberfields", "MemberField",
gen.FieldType("uid", userIDTypeString),
gen.FieldType("privacy", "[]byte"),
+ gen.FieldIgnore("index_sort"),
+ gen.FieldIgnore("user_agent"),
+ gen.FieldIgnore("ignorepm"),
+ gen.FieldIgnore("groupterms"),
+ gen.FieldIgnore("authstr"),
+ gen.FieldIgnoreReg("^(homepage|reg_source|invite_num|email_verified|reset_password_dateline|reset_password_token)$"),
+ gen.FieldIgnoreReg("^(reset_password_force|email_verify_dateline|email_verify_token|email_verify_score)$"),
)
modelMember := g.GenerateModelAs("chii_members", "Member",
gen.FieldRename("uid", "ID"),
+ // gen.FieldIgnore("password_crypt"),
+ gen.FieldIgnore("secques"),
+ gen.FieldIgnore("gender"),
+ gen.FieldIgnore("adminid"),
+ gen.FieldIgnore("regip"),
+ gen.FieldIgnore("lastip"),
+
+ // gen.FieldIgnore("email"),
+ gen.FieldIgnore("bday"),
+ gen.FieldIgnore("styleid"),
+ gen.FieldIgnore("newsletter"),
+ gen.FieldIgnore("ukagaka_settings"),
+ gen.FieldIgnore("username_lock"),
+ gen.FieldIgnore("invited"),
+ gen.FieldIgnore("img_chart"),
+
gen.FieldType("uid", userIDTypeString),
gen.FieldType("sign", "utiltype.HTMLEscapedString"),
gen.FieldType("regdate", "int64"),
@@ -210,6 +236,19 @@ func main() {
gen.FieldTrimPrefix("interest_"),
))
+ g.ApplyBasic(g.GenerateModelAs("chii_person_collects", "PersonCollect",
+ gen.FieldTrimPrefix("prsn_clt_"),
+ gen.FieldType("prsn_clt_id", "uint32"),
+ gen.FieldType("prsn_clt_cat", "string"),
+ gen.FieldType("prsn_clt_uid", userIDTypeString),
+ gen.FieldType("prsn_clt_mid", "uint32"),
+ gen.FieldType("prsn_clt_dateline", "uint32"),
+ gen.FieldRename("prsn_clt_cat", "Category"),
+ gen.FieldRename("prsn_clt_uid", "UserID"),
+ gen.FieldRename("prsn_clt_mid", "TargetID"),
+ gen.FieldRename("prsn_clt_dateline", createdTime),
+ ))
+
g.ApplyBasic(g.GenerateModelAs("chii_index", "Index",
gen.FieldTrimPrefix("idx_"),
gen.FieldType("idx_id", "uint32"),
@@ -303,6 +342,9 @@ func main() {
gen.FieldRename("subject_collect", "Done"),
gen.FieldRename("field_infobox", "infobox"),
gen.FieldType("subject_id", subjectIDTypeString),
+ gen.FieldType("subject_name", "utiltype.HTMLEscapedString"),
+ gen.FieldType("field_infobox", "utiltype.HTMLEscapedString"),
+ gen.FieldType("subject_name_cn", "utiltype.HTMLEscapedString"),
gen.FieldType("subject_ban", "uint8"),
gen.FieldType("subject_type_id", subjectTypeIDTypeString),
gen.FieldType("subject_airtime", "uint8"),
@@ -481,6 +523,24 @@ func main() {
gen.FieldRename("msg_rdeleted", "DeletedByReceiver"),
))
+ modelTagIndex := g.GenerateModelAs("chii_tag_neue_index", "TagIndex",
+ gen.FieldTrimPrefix("tag_"),
+ gen.FieldRename("tag_dateline", createdTime),
+ gen.FieldRename("tag_lasttouch", updateTime),
+ gen.FieldType("tag_type", "uint8"),
+ )
+
+ g.ApplyBasic(modelTagIndex)
+
+ g.ApplyBasic(g.GenerateModelAs("chii_tag_neue_list", "TagList",
+ gen.FieldTrimPrefix("tlt_"),
+ gen.FieldRename("tlt_dateline", createdTime),
+
+ gen.FieldRelate(field.HasOne, "Tag", modelTagIndex, &field.RelateConfig{
+ GORMTag: field.GormTag{"foreignKey": []string{"tag_id"}, "references": []string{"tlt_tid"}},
+ }),
+ ))
+
// execute the action of code generation
g.Execute()
}
diff --git a/cmd/web/cmd.go b/cmd/web/cmd.go
index 7127a0561..739a44e01 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"
@@ -64,8 +65,8 @@ func start() error {
// driver and connector
fx.Provide(
config.AppConfigReader(config.AppTypeHTTP),
- driver.NewRedisClientWithMetrics, // redis
- driver.NewMysqlConnectionPool, // mysql
+ driver.NewRueidisClient, // redis
+ driver.NewMysqlSqlDB, // mysql
func() *resty.Client {
httpClient := resty.New().SetJSONEscapeHTML(false)
httpClient.JSONUnmarshal = json.Unmarshal
@@ -74,18 +75,21 @@ func start() error {
},
),
+ fx.Invoke(dal.SetupMetrics),
+
dal.Module,
fx.Provide(
logger.Copy, cache.NewRedisCache,
- character.NewMysqlRepo,
-
user.NewMysqlRepo,
index.NewMysqlRepo, auth.NewMysqlRepo, episode.NewMysqlRepo, revision.NewMysqlRepo, infra.NewMysqlRepo,
timeline.NewMysqlRepo, pm.NewMysqlRepo, notification.NewMysqlRepo,
- dam.New, subject.NewMysqlRepo, subject.NewCachedRepo, person.NewMysqlRepo,
+ dam.New, subject.NewMysqlRepo, subject.NewCachedRepo,
+ character.NewMysqlRepo, person.NewMysqlRepo,
+
+ tag.NewCachedRepo, tag.NewMysqlRepo,
auth.NewService, person.NewService, search.New,
),
diff --git a/config.example.yaml b/config.example.yaml
index e342a4aba..4e125cfed 100644
--- a/config.example.yaml
+++ b/config.example.yaml
@@ -23,6 +23,8 @@ canal:
topics: # kafka topic of redis stream keys
- debezium.bangumi.chii_subject_fields
- debezium.bangumi.chii_subjects
+ - debezium.bangumi.chii_characters
+ - debezium.bangumi.chii_persons
- debezium.bangumi.chii_members
search:
diff --git a/config/config.go b/config/config.go
index 32b7b510f..cd98eb246 100644
--- a/config/config.go
+++ b/config/config.go
@@ -68,9 +68,9 @@ type AppConfig struct {
DisableWords string `yaml:"disable_words"`
BannedDomain string `yaml:"banned_domain"`
- // "http://localhost:2379"
- EtcdAddr string `yaml:"etcd_addr" env:"ETCD_ADDR"`
- EtcdNamespace string `yaml:"etcd_namespace" env:"ETCD_NAMESPACE" env-default:"/chii/services"`
+ // a timeline microservice listen domain
+ SrvTimelineDomain string `yaml:"srv_timeline_domain" env:"SRV_TIMELINE_DOMAIN"`
+ SrvTimelinePort uint16 `yaml:"srv_timeline_port" env:"SRV_TIMELINE_PORT"`
S3EntryPoint string `yaml:"s3_entry_point" env:"S3_ENTRY_POINT"`
S3AccessKey string `yaml:"s3_access_key" env:"S3_ACCESS_KEY"`
diff --git a/dal/dao/chii_crt_subject_index.gen.go b/dal/dao/chii_crt_subject_index.gen.go
index 09671e218..960048ca9 100644
--- a/dal/dao/chii_crt_subject_index.gen.go
+++ b/dal/dao/chii_crt_subject_index.gen.go
@@ -13,7 +13,7 @@ type CharacterSubjects struct {
SubjectTypeID uint8 `gorm:"column:subject_type_id;type:tinyint(4) unsigned;not null" json:""`
CrtType uint8 `gorm:"column:crt_type;type:tinyint(4) unsigned;not null;comment:主角,配角" json:""` // 主角,配角
CtrAppearEps string `gorm:"column:ctr_appear_eps;type:mediumtext;not null;comment:可选,角色出场的的章节" json:""` // 可选,角色出场的的章节
- CrtOrder uint8 `gorm:"column:crt_order;type:tinyint(3) unsigned;not null" json:""`
+ CrtOrder uint16 `gorm:"column:crt_order;type:smallint(6) unsigned;not null" json:""`
Character Character `gorm:"foreignKey:crt_id;references:crt_id" json:"character"`
Subject Subject `gorm:"foreignKey:subject_id;references:subject_id" json:"subject"`
}
diff --git a/dal/dao/chii_person_collects.gen.go b/dal/dao/chii_person_collects.gen.go
new file mode 100644
index 000000000..19c9b72e8
--- /dev/null
+++ b/dal/dao/chii_person_collects.gen.go
@@ -0,0 +1,21 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package dao
+
+const TableNamePersonCollect = "chii_person_collects"
+
+// PersonCollect 人物收藏
+type PersonCollect struct {
+ ID uint32 `gorm:"column:prsn_clt_id;type:mediumint(8) unsigned;primaryKey;autoIncrement:true" json:""`
+ Category string `gorm:"column:prsn_clt_cat;type:enum('prsn','crt');not null" json:""`
+ TargetID uint32 `gorm:"column:prsn_clt_mid;type:mediumint(8) unsigned;not null" json:""`
+ UserID uint32 `gorm:"column:prsn_clt_uid;type:mediumint(8) unsigned;not null" json:""`
+ CreatedTime uint32 `gorm:"column:prsn_clt_dateline;type:int(10) unsigned;not null" json:""`
+}
+
+// TableName PersonCollect's table name
+func (*PersonCollect) TableName() string {
+ return TableNamePersonCollect
+}
diff --git a/dal/dao/chii_subject_relations.gen.go b/dal/dao/chii_subject_relations.gen.go
index dd09b6c40..08628db94 100644
--- a/dal/dao/chii_subject_relations.gen.go
+++ b/dal/dao/chii_subject_relations.gen.go
@@ -14,7 +14,7 @@ type SubjectRelation struct {
RelatedSubjectID uint32 `gorm:"column:rlt_related_subject_id;type:mediumint(8) unsigned;primaryKey;comment:关联目标 ID" json:""` // 关联目标 ID
RelatedSubjectTypeID uint8 `gorm:"column:rlt_related_subject_type_id;type:tinyint(3) unsigned;not null;comment:关联目标类型" json:""` // 关联目标类型
ViceVersa bool `gorm:"column:rlt_vice_versa;type:tinyint(1) unsigned;primaryKey" json:""`
- Order uint8 `gorm:"column:rlt_order;type:tinyint(3) unsigned;not null;comment:关联排序" json:""` // 关联排序
+ Order uint16 `gorm:"column:rlt_order;type:smallint(6) unsigned;not null;comment:关联排序" json:""` // 关联排序
Subject Subject `gorm:"foreignKey:rlt_related_subject_id;references:subject_id" json:"subject"`
}
diff --git a/dal/dao/chii_subject_revisions.gen.go b/dal/dao/chii_subject_revisions.gen.go
index 8c1ad4d55..57360bb8f 100644
--- a/dal/dao/chii_subject_revisions.gen.go
+++ b/dal/dao/chii_subject_revisions.gen.go
@@ -8,21 +8,22 @@ const TableNameSubjectRevision = "chii_subject_revisions"
// SubjectRevision mapped from table
type SubjectRevision struct {
- ID uint32 `gorm:"column:rev_id;type:mediumint(8) unsigned;primaryKey;autoIncrement:true" json:""`
- Type uint8 `gorm:"column:rev_type;type:tinyint(3) unsigned;not null;default:1;comment:修订类型" json:""` // 修订类型
- SubjectID uint32 `gorm:"column:rev_subject_id;type:mediumint(8) unsigned;not null" json:""`
- TypeID uint16 `gorm:"column:rev_type_id;type:smallint(6) unsigned;not null" json:""`
- CreatorID uint32 `gorm:"column:rev_creator;type:mediumint(8) unsigned;not null" json:""`
- Dateline uint32 `gorm:"column:rev_dateline;type:int(10) unsigned;not null" json:""`
- Name string `gorm:"column:rev_name;type:varchar(80);not null" json:""`
- NameCN string `gorm:"column:rev_name_cn;type:varchar(80);not null" json:""`
- FieldInfobox string `gorm:"column:rev_field_infobox;type:mediumtext;not null" json:""`
- FieldSummary string `gorm:"column:rev_field_summary;type:mediumtext;not null" json:""`
- VoteField string `gorm:"column:rev_vote_field;type:mediumtext;not null" json:""`
- FieldEps uint32 `gorm:"column:rev_field_eps;type:mediumint(8) unsigned;not null" json:""`
- EditSummary string `gorm:"column:rev_edit_summary;type:varchar(200);not null" json:""`
- Platform uint16 `gorm:"column:rev_platform;type:smallint(6) unsigned;not null" json:""`
- Subject Subject `gorm:"foreignKey:rev_subject_id;references:subject_id" json:"subject"`
+ ID uint32 `gorm:"column:rev_id;type:mediumint(8) unsigned;primaryKey;autoIncrement:true" json:""`
+ Type uint8 `gorm:"column:rev_type;type:tinyint(3) unsigned;not null;default:1;comment:修订类型" json:""` // 修订类型
+ SubjectID uint32 `gorm:"column:rev_subject_id;type:mediumint(8) unsigned;not null" json:""`
+ TypeID uint16 `gorm:"column:rev_type_id;type:smallint(6) unsigned;not null" json:""`
+ CreatorID uint32 `gorm:"column:rev_creator;type:mediumint(8) unsigned;not null" json:""`
+ Dateline uint32 `gorm:"column:rev_dateline;type:int(10) unsigned;not null" json:""`
+ Name string `gorm:"column:rev_name;type:varchar(80);not null" json:""`
+ NameCN string `gorm:"column:rev_name_cn;type:varchar(80);not null" json:""`
+ FieldInfobox string `gorm:"column:rev_field_infobox;type:mediumtext;not null" json:""`
+ FieldMetaTags string `gorm:"column:rev_field_meta_tags;type:mediumtext;not null" json:""`
+ FieldSummary string `gorm:"column:rev_field_summary;type:mediumtext;not null" json:""`
+ VoteField string `gorm:"column:rev_vote_field;type:mediumtext;not null" json:""`
+ FieldEps uint32 `gorm:"column:rev_field_eps;type:mediumint(8) unsigned;not null" json:""`
+ EditSummary string `gorm:"column:rev_edit_summary;type:varchar(200);not null" json:""`
+ Platform uint16 `gorm:"column:rev_platform;type:smallint(6) unsigned;not null" json:""`
+ Subject Subject `gorm:"foreignKey:rev_subject_id;references:subject_id" json:"subject"`
}
// TableName SubjectRevision's table name
diff --git a/dal/dao/chii_subjects.gen.go b/dal/dao/chii_subjects.gen.go
index 47e26d911..f345bd642 100644
--- a/dal/dao/chii_subjects.gen.go
+++ b/dal/dao/chii_subjects.gen.go
@@ -4,36 +4,41 @@
package dao
+import (
+ "github.com/bangumi/server/dal/utiltype"
+)
+
const TableNameSubject = "chii_subjects"
// Subject mapped from table
type Subject struct {
- ID uint32 `gorm:"column:subject_id;type:mediumint(8) unsigned;primaryKey;autoIncrement:true" json:""`
- TypeID uint8 `gorm:"column:subject_type_id;type:smallint(6) unsigned;not null" json:""`
- Name string `gorm:"column:subject_name;type:varchar(80);not null" json:""`
- NameCN string `gorm:"column:subject_name_cn;type:varchar(80);not null" json:""`
- UID string `gorm:"column:subject_uid;type:varchar(20);not null;comment:isbn / imdb" json:""` // isbn / imdb
- Creator uint32 `gorm:"column:subject_creator;type:mediumint(8) unsigned;not null" json:""`
- Dateline uint32 `gorm:"column:subject_dateline;type:int(10) unsigned;not null" json:""`
- Image string `gorm:"column:subject_image;type:varchar(255);not null" json:""`
- Platform uint16 `gorm:"column:subject_platform;type:smallint(6) unsigned;not null" json:""`
- Infobox string `gorm:"column:field_infobox;type:mediumtext;not null" json:""`
- Summary string `gorm:"column:field_summary;type:mediumtext;not null;comment:summary" json:""` // summary
- Field5 string `gorm:"column:field_5;type:mediumtext;not null;comment:author summary" json:""` // author summary
- Volumes uint32 `gorm:"column:field_volumes;type:mediumint(8) unsigned;not null;comment:卷数" json:""` // 卷数
- Eps uint32 `gorm:"column:field_eps;type:mediumint(8) unsigned;not null" json:""`
- Wish uint32 `gorm:"column:subject_wish;type:mediumint(8) unsigned;not null" json:""`
- Done uint32 `gorm:"column:subject_collect;type:mediumint(8) unsigned;not null" json:""`
- Doing uint32 `gorm:"column:subject_doing;type:mediumint(8) unsigned;not null" json:""`
- OnHold uint32 `gorm:"column:subject_on_hold;type:mediumint(8) unsigned;not null;comment:搁置人数" json:""` // 搁置人数
- Dropped uint32 `gorm:"column:subject_dropped;type:mediumint(8) unsigned;not null;comment:抛弃人数" json:""` // 抛弃人数
- Series bool `gorm:"column:subject_series;type:tinyint(1) unsigned;not null" json:""`
- SeriesEntry uint32 `gorm:"column:subject_series_entry;type:mediumint(8) unsigned;not null" json:""`
- IdxCn string `gorm:"column:subject_idx_cn;type:varchar(1);not null" json:""`
- Airtime uint8 `gorm:"column:subject_airtime;type:tinyint(1) unsigned;not null" json:""`
- Nsfw bool `gorm:"column:subject_nsfw;type:tinyint(1);not null" json:""`
- Ban uint8 `gorm:"column:subject_ban;type:tinyint(1) unsigned;not null" json:""`
- Fields SubjectField `gorm:"foreignKey:subject_id;references:field_sid" json:"fields"`
+ ID uint32 `gorm:"column:subject_id;type:mediumint(8) unsigned;primaryKey;autoIncrement:true" json:""`
+ TypeID uint8 `gorm:"column:subject_type_id;type:smallint(6) unsigned;not null" json:""`
+ Name utiltype.HTMLEscapedString `gorm:"column:subject_name;type:varchar(512);not null" json:""`
+ NameCN utiltype.HTMLEscapedString `gorm:"column:subject_name_cn;type:varchar(512);not null" json:""`
+ UID string `gorm:"column:subject_uid;type:varchar(20);not null;comment:isbn / imdb" json:""` // isbn / imdb
+ Creator uint32 `gorm:"column:subject_creator;type:mediumint(8) unsigned;not null" json:""`
+ Dateline uint32 `gorm:"column:subject_dateline;type:int(10) unsigned;not null" json:""`
+ Image string `gorm:"column:subject_image;type:varchar(255);not null" json:""`
+ Platform uint16 `gorm:"column:subject_platform;type:smallint(6) unsigned;not null" json:""`
+ Infobox utiltype.HTMLEscapedString `gorm:"column:field_infobox;type:mediumtext;not null" json:""`
+ FieldMetaTags string `gorm:"column:field_meta_tags;type:mediumtext;not null" json:""`
+ Summary string `gorm:"column:field_summary;type:mediumtext;not null;comment:summary" json:""` // summary
+ Field5 string `gorm:"column:field_5;type:mediumtext;not null;comment:author summary" json:""` // author summary
+ Volumes uint32 `gorm:"column:field_volumes;type:mediumint(8) unsigned;not null;comment:卷数" json:""` // 卷数
+ Eps uint32 `gorm:"column:field_eps;type:mediumint(8) unsigned;not null" json:""`
+ Wish uint32 `gorm:"column:subject_wish;type:mediumint(8) unsigned;not null" json:""`
+ Done uint32 `gorm:"column:subject_collect;type:mediumint(8) unsigned;not null" json:""`
+ Doing uint32 `gorm:"column:subject_doing;type:mediumint(8) unsigned;not null" json:""`
+ OnHold uint32 `gorm:"column:subject_on_hold;type:mediumint(8) unsigned;not null;comment:搁置人数" json:""` // 搁置人数
+ Dropped uint32 `gorm:"column:subject_dropped;type:mediumint(8) unsigned;not null;comment:抛弃人数" json:""` // 抛弃人数
+ Series bool `gorm:"column:subject_series;type:tinyint(1) unsigned;not null" json:""`
+ SeriesEntry uint32 `gorm:"column:subject_series_entry;type:mediumint(8) unsigned;not null" json:""`
+ IdxCn string `gorm:"column:subject_idx_cn;type:varchar(1);not null" json:""`
+ Airtime uint8 `gorm:"column:subject_airtime;type:tinyint(1) unsigned;not null" json:""`
+ Nsfw bool `gorm:"column:subject_nsfw;type:tinyint(1);not null" json:""`
+ Ban uint8 `gorm:"column:subject_ban;type:tinyint(1) unsigned;not null" json:""`
+ Fields SubjectField `gorm:"foreignKey:subject_id;references:field_sid" json:"fields"`
}
// TableName Subject's table name
diff --git a/dal/dao/chii_tag_neue_index.gen.go b/dal/dao/chii_tag_neue_index.gen.go
new file mode 100644
index 000000000..142365250
--- /dev/null
+++ b/dal/dao/chii_tag_neue_index.gen.go
@@ -0,0 +1,23 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package dao
+
+const TableNameTagIndex = "chii_tag_neue_index"
+
+// TagIndex mapped from table
+type TagIndex struct {
+ ID uint32 `gorm:"column:tag_id;type:mediumint(8) unsigned;primaryKey;autoIncrement:true" json:""`
+ Name string `gorm:"column:tag_name;type:varchar(30);not null" json:""`
+ Cat int8 `gorm:"column:tag_cat;type:tinyint(3);not null;comment:0=条目 1=日志 2=天窗" json:""` // 0=条目 1=日志 2=天窗
+ Type uint8 `gorm:"column:tag_type;type:tinyint(3);not null" json:""`
+ Results uint32 `gorm:"column:tag_results;type:mediumint(8) unsigned;not null" json:""`
+ CreatedTime uint32 `gorm:"column:tag_dateline;type:int(10) unsigned;not null" json:""`
+ UpdatedTime uint32 `gorm:"column:tag_lasttouch;type:int(10) unsigned;not null" json:""`
+}
+
+// TableName TagIndex's table name
+func (*TagIndex) TableName() string {
+ return TableNameTagIndex
+}
diff --git a/dal/dao/chii_tag_neue_list.gen.go b/dal/dao/chii_tag_neue_list.gen.go
new file mode 100644
index 000000000..7b6499b7b
--- /dev/null
+++ b/dal/dao/chii_tag_neue_list.gen.go
@@ -0,0 +1,23 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package dao
+
+const TableNameTagList = "chii_tag_neue_list"
+
+// TagList mapped from table
+type TagList struct {
+ Tid uint32 `gorm:"column:tlt_tid;type:mediumint(8) unsigned;not null" json:""`
+ UID uint32 `gorm:"column:tlt_uid;type:mediumint(8) unsigned;not null" json:""`
+ Cat uint8 `gorm:"column:tlt_cat;type:tinyint(3) unsigned;not null" json:""`
+ Type uint8 `gorm:"column:tlt_type;type:tinyint(3) unsigned;not null" json:""`
+ Mid uint32 `gorm:"column:tlt_mid;type:mediumint(8) unsigned;not null" json:""`
+ CreatedTime uint32 `gorm:"column:tlt_dateline;type:int(10) unsigned;not null" json:""`
+ Tag TagIndex `gorm:"foreignKey:tag_id;references:tlt_tid" json:"tag"`
+}
+
+// TableName TagList's table name
+func (*TagList) TableName() string {
+ return TableNameTagList
+}
diff --git a/dal/fx.go b/dal/fx.go
index 3810e1785..1f06120bb 100644
--- a/dal/fx.go
+++ b/dal/fx.go
@@ -17,6 +17,9 @@
package dal
import (
+ "database/sql"
+
+ "github.com/jmoiron/sqlx"
"go.uber.org/fx"
"github.com/bangumi/server/dal/query"
@@ -24,8 +27,11 @@ import (
var Module = fx.Module("dal",
fx.Provide(
- NewDB,
+ NewGormDB,
query.Use,
NewMysqlTransaction,
+ func(db *sql.DB) *sqlx.DB {
+ return sqlx.NewDb(db, "mysql")
+ },
),
)
diff --git a/dal/log.go b/dal/log.go
index ed29dabb9..5f56bb7b1 100644
--- a/dal/log.go
+++ b/dal/log.go
@@ -82,7 +82,7 @@ func (l *metricsLog) Trace(_ context.Context, begin time.Time, fc func() (sql st
l.h.Observe(elapsed.Seconds())
switch {
- case err != nil && !errors.Is(err, gorm.ErrRecordNotFound):
+ case err != nil && !errors.Is(err, gorm.ErrRecordNotFound) && !errors.Is(err, context.Canceled):
sql, rows := fc()
l.log.Error("gorm error", zap.String("sql", sql), zap.Error(err),
zap.Duration("duration", elapsed), zap.Int64("rows", rows))
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 35de32e5a..6166a1447 100644
--- a/dal/new.go
+++ b/dal/new.go
@@ -16,10 +16,9 @@ package dal
import (
"database/sql"
- "log"
- "os"
"github.com/trim21/errgo"
+ "go.uber.org/zap"
"gorm.io/driver/mysql"
"gorm.io/gorm"
gormLogger "gorm.io/gorm/logger"
@@ -28,12 +27,12 @@ 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")
gLog = gormLogger.New(
- log.New(os.Stdout, "\r\n", log.LstdFlags),
+ logger.StdAt(zap.DebugLevel),
gormLogger.Config{
LogLevel: gormLogger.Info,
IgnoreRecordNotFoundError: true,
@@ -52,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/dal/query/chii_crt_subject_index.gen.go b/dal/query/chii_crt_subject_index.gen.go
index 10be0b8b0..0f18a66a0 100644
--- a/dal/query/chii_crt_subject_index.gen.go
+++ b/dal/query/chii_crt_subject_index.gen.go
@@ -32,7 +32,7 @@ func newCharacterSubjects(db *gorm.DB, opts ...gen.DOOption) characterSubjects {
_characterSubjects.SubjectTypeID = field.NewUint8(tableName, "subject_type_id")
_characterSubjects.CrtType = field.NewUint8(tableName, "crt_type")
_characterSubjects.CtrAppearEps = field.NewString(tableName, "ctr_appear_eps")
- _characterSubjects.CrtOrder = field.NewUint8(tableName, "crt_order")
+ _characterSubjects.CrtOrder = field.NewUint16(tableName, "crt_order")
_characterSubjects.Character = characterSubjectsHasOneCharacter{
db: db.Session(&gorm.Session{}),
@@ -69,7 +69,7 @@ type characterSubjects struct {
SubjectTypeID field.Uint8
CrtType field.Uint8 // 主角,配角
CtrAppearEps field.String // 可选,角色出场的的章节
- CrtOrder field.Uint8
+ CrtOrder field.Uint16
Character characterSubjectsHasOneCharacter
Subject characterSubjectsHasOneSubject
@@ -94,7 +94,7 @@ func (c *characterSubjects) updateTableName(table string) *characterSubjects {
c.SubjectTypeID = field.NewUint8(table, "subject_type_id")
c.CrtType = field.NewUint8(table, "crt_type")
c.CtrAppearEps = field.NewString(table, "ctr_appear_eps")
- c.CrtOrder = field.NewUint8(table, "crt_order")
+ c.CrtOrder = field.NewUint16(table, "crt_order")
c.fillFieldMap()
diff --git a/dal/query/chii_person_collects.gen.go b/dal/query/chii_person_collects.gen.go
new file mode 100644
index 000000000..bd7753700
--- /dev/null
+++ b/dal/query/chii_person_collects.gen.go
@@ -0,0 +1,348 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+ "context"
+
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+ "gorm.io/gorm/schema"
+
+ "gorm.io/gen"
+ "gorm.io/gen/field"
+
+ "gorm.io/plugin/dbresolver"
+
+ "github.com/bangumi/server/dal/dao"
+)
+
+func newPersonCollect(db *gorm.DB, opts ...gen.DOOption) personCollect {
+ _personCollect := personCollect{}
+
+ _personCollect.personCollectDo.UseDB(db, opts...)
+ _personCollect.personCollectDo.UseModel(&dao.PersonCollect{})
+
+ tableName := _personCollect.personCollectDo.TableName()
+ _personCollect.ALL = field.NewAsterisk(tableName)
+ _personCollect.ID = field.NewUint32(tableName, "prsn_clt_id")
+ _personCollect.Category = field.NewString(tableName, "prsn_clt_cat")
+ _personCollect.TargetID = field.NewUint32(tableName, "prsn_clt_mid")
+ _personCollect.UserID = field.NewUint32(tableName, "prsn_clt_uid")
+ _personCollect.CreatedTime = field.NewUint32(tableName, "prsn_clt_dateline")
+
+ _personCollect.fillFieldMap()
+
+ return _personCollect
+}
+
+// personCollect 人物收藏
+type personCollect struct {
+ personCollectDo personCollectDo
+
+ ALL field.Asterisk
+ ID field.Uint32
+ Category field.String
+ TargetID field.Uint32
+ UserID field.Uint32
+ CreatedTime field.Uint32
+
+ fieldMap map[string]field.Expr
+}
+
+func (p personCollect) Table(newTableName string) *personCollect {
+ p.personCollectDo.UseTable(newTableName)
+ return p.updateTableName(newTableName)
+}
+
+func (p personCollect) As(alias string) *personCollect {
+ p.personCollectDo.DO = *(p.personCollectDo.As(alias).(*gen.DO))
+ return p.updateTableName(alias)
+}
+
+func (p *personCollect) updateTableName(table string) *personCollect {
+ p.ALL = field.NewAsterisk(table)
+ p.ID = field.NewUint32(table, "prsn_clt_id")
+ p.Category = field.NewString(table, "prsn_clt_cat")
+ p.TargetID = field.NewUint32(table, "prsn_clt_mid")
+ p.UserID = field.NewUint32(table, "prsn_clt_uid")
+ p.CreatedTime = field.NewUint32(table, "prsn_clt_dateline")
+
+ p.fillFieldMap()
+
+ return p
+}
+
+func (p *personCollect) WithContext(ctx context.Context) *personCollectDo {
+ return p.personCollectDo.WithContext(ctx)
+}
+
+func (p personCollect) TableName() string { return p.personCollectDo.TableName() }
+
+func (p personCollect) Alias() string { return p.personCollectDo.Alias() }
+
+func (p personCollect) Columns(cols ...field.Expr) gen.Columns {
+ return p.personCollectDo.Columns(cols...)
+}
+
+func (p *personCollect) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+ _f, ok := p.fieldMap[fieldName]
+ if !ok || _f == nil {
+ return nil, false
+ }
+ _oe, ok := _f.(field.OrderExpr)
+ return _oe, ok
+}
+
+func (p *personCollect) fillFieldMap() {
+ p.fieldMap = make(map[string]field.Expr, 5)
+ p.fieldMap["prsn_clt_id"] = p.ID
+ p.fieldMap["prsn_clt_cat"] = p.Category
+ p.fieldMap["prsn_clt_mid"] = p.TargetID
+ p.fieldMap["prsn_clt_uid"] = p.UserID
+ p.fieldMap["prsn_clt_dateline"] = p.CreatedTime
+}
+
+func (p personCollect) clone(db *gorm.DB) personCollect {
+ p.personCollectDo.ReplaceConnPool(db.Statement.ConnPool)
+ return p
+}
+
+func (p personCollect) replaceDB(db *gorm.DB) personCollect {
+ p.personCollectDo.ReplaceDB(db)
+ return p
+}
+
+type personCollectDo struct{ gen.DO }
+
+func (p personCollectDo) Debug() *personCollectDo {
+ return p.withDO(p.DO.Debug())
+}
+
+func (p personCollectDo) WithContext(ctx context.Context) *personCollectDo {
+ return p.withDO(p.DO.WithContext(ctx))
+}
+
+func (p personCollectDo) ReadDB() *personCollectDo {
+ return p.Clauses(dbresolver.Read)
+}
+
+func (p personCollectDo) WriteDB() *personCollectDo {
+ return p.Clauses(dbresolver.Write)
+}
+
+func (p personCollectDo) Session(config *gorm.Session) *personCollectDo {
+ return p.withDO(p.DO.Session(config))
+}
+
+func (p personCollectDo) Clauses(conds ...clause.Expression) *personCollectDo {
+ return p.withDO(p.DO.Clauses(conds...))
+}
+
+func (p personCollectDo) Returning(value interface{}, columns ...string) *personCollectDo {
+ return p.withDO(p.DO.Returning(value, columns...))
+}
+
+func (p personCollectDo) Not(conds ...gen.Condition) *personCollectDo {
+ return p.withDO(p.DO.Not(conds...))
+}
+
+func (p personCollectDo) Or(conds ...gen.Condition) *personCollectDo {
+ return p.withDO(p.DO.Or(conds...))
+}
+
+func (p personCollectDo) Select(conds ...field.Expr) *personCollectDo {
+ return p.withDO(p.DO.Select(conds...))
+}
+
+func (p personCollectDo) Where(conds ...gen.Condition) *personCollectDo {
+ return p.withDO(p.DO.Where(conds...))
+}
+
+func (p personCollectDo) Order(conds ...field.Expr) *personCollectDo {
+ return p.withDO(p.DO.Order(conds...))
+}
+
+func (p personCollectDo) Distinct(cols ...field.Expr) *personCollectDo {
+ return p.withDO(p.DO.Distinct(cols...))
+}
+
+func (p personCollectDo) Omit(cols ...field.Expr) *personCollectDo {
+ return p.withDO(p.DO.Omit(cols...))
+}
+
+func (p personCollectDo) Join(table schema.Tabler, on ...field.Expr) *personCollectDo {
+ return p.withDO(p.DO.Join(table, on...))
+}
+
+func (p personCollectDo) LeftJoin(table schema.Tabler, on ...field.Expr) *personCollectDo {
+ return p.withDO(p.DO.LeftJoin(table, on...))
+}
+
+func (p personCollectDo) RightJoin(table schema.Tabler, on ...field.Expr) *personCollectDo {
+ return p.withDO(p.DO.RightJoin(table, on...))
+}
+
+func (p personCollectDo) Group(cols ...field.Expr) *personCollectDo {
+ return p.withDO(p.DO.Group(cols...))
+}
+
+func (p personCollectDo) Having(conds ...gen.Condition) *personCollectDo {
+ return p.withDO(p.DO.Having(conds...))
+}
+
+func (p personCollectDo) Limit(limit int) *personCollectDo {
+ return p.withDO(p.DO.Limit(limit))
+}
+
+func (p personCollectDo) Offset(offset int) *personCollectDo {
+ return p.withDO(p.DO.Offset(offset))
+}
+
+func (p personCollectDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *personCollectDo {
+ return p.withDO(p.DO.Scopes(funcs...))
+}
+
+func (p personCollectDo) Unscoped() *personCollectDo {
+ return p.withDO(p.DO.Unscoped())
+}
+
+func (p personCollectDo) Create(values ...*dao.PersonCollect) error {
+ if len(values) == 0 {
+ return nil
+ }
+ return p.DO.Create(values)
+}
+
+func (p personCollectDo) CreateInBatches(values []*dao.PersonCollect, batchSize int) error {
+ return p.DO.CreateInBatches(values, batchSize)
+}
+
+// Save : !!! underlying implementation is different with GORM
+// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
+func (p personCollectDo) Save(values ...*dao.PersonCollect) error {
+ if len(values) == 0 {
+ return nil
+ }
+ return p.DO.Save(values)
+}
+
+func (p personCollectDo) First() (*dao.PersonCollect, error) {
+ if result, err := p.DO.First(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.PersonCollect), nil
+ }
+}
+
+func (p personCollectDo) Take() (*dao.PersonCollect, error) {
+ if result, err := p.DO.Take(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.PersonCollect), nil
+ }
+}
+
+func (p personCollectDo) Last() (*dao.PersonCollect, error) {
+ if result, err := p.DO.Last(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.PersonCollect), nil
+ }
+}
+
+func (p personCollectDo) Find() ([]*dao.PersonCollect, error) {
+ result, err := p.DO.Find()
+ return result.([]*dao.PersonCollect), err
+}
+
+func (p personCollectDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*dao.PersonCollect, err error) {
+ buf := make([]*dao.PersonCollect, 0, batchSize)
+ err = p.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+ defer func() { results = append(results, buf...) }()
+ return fc(tx, batch)
+ })
+ return results, err
+}
+
+func (p personCollectDo) FindInBatches(result *[]*dao.PersonCollect, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+ return p.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (p personCollectDo) Attrs(attrs ...field.AssignExpr) *personCollectDo {
+ return p.withDO(p.DO.Attrs(attrs...))
+}
+
+func (p personCollectDo) Assign(attrs ...field.AssignExpr) *personCollectDo {
+ return p.withDO(p.DO.Assign(attrs...))
+}
+
+func (p personCollectDo) Joins(fields ...field.RelationField) *personCollectDo {
+ for _, _f := range fields {
+ p = *p.withDO(p.DO.Joins(_f))
+ }
+ return &p
+}
+
+func (p personCollectDo) Preload(fields ...field.RelationField) *personCollectDo {
+ for _, _f := range fields {
+ p = *p.withDO(p.DO.Preload(_f))
+ }
+ return &p
+}
+
+func (p personCollectDo) FirstOrInit() (*dao.PersonCollect, error) {
+ if result, err := p.DO.FirstOrInit(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.PersonCollect), nil
+ }
+}
+
+func (p personCollectDo) FirstOrCreate() (*dao.PersonCollect, error) {
+ if result, err := p.DO.FirstOrCreate(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.PersonCollect), nil
+ }
+}
+
+func (p personCollectDo) FindByPage(offset int, limit int) (result []*dao.PersonCollect, count int64, err error) {
+ result, err = p.Offset(offset).Limit(limit).Find()
+ if err != nil {
+ return
+ }
+
+ if size := len(result); 0 < limit && 0 < size && size < limit {
+ count = int64(size + offset)
+ return
+ }
+
+ count, err = p.Offset(-1).Limit(-1).Count()
+ return
+}
+
+func (p personCollectDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+ count, err = p.Count()
+ if err != nil {
+ return
+ }
+
+ err = p.Offset(offset).Limit(limit).Scan(result)
+ return
+}
+
+func (p personCollectDo) Scan(result interface{}) (err error) {
+ return p.DO.Scan(result)
+}
+
+func (p personCollectDo) Delete(models ...*dao.PersonCollect) (result gen.ResultInfo, err error) {
+ return p.DO.Delete(models)
+}
+
+func (p *personCollectDo) withDO(do gen.Dao) *personCollectDo {
+ p.DO = *do.(*gen.DO)
+ return p
+}
diff --git a/dal/query/chii_subject_relations.gen.go b/dal/query/chii_subject_relations.gen.go
index 01f786f63..06591e68c 100644
--- a/dal/query/chii_subject_relations.gen.go
+++ b/dal/query/chii_subject_relations.gen.go
@@ -33,7 +33,7 @@ func newSubjectRelation(db *gorm.DB, opts ...gen.DOOption) subjectRelation {
_subjectRelation.RelatedSubjectID = field.NewUint32(tableName, "rlt_related_subject_id")
_subjectRelation.RelatedSubjectTypeID = field.NewUint8(tableName, "rlt_related_subject_type_id")
_subjectRelation.ViceVersa = field.NewBool(tableName, "rlt_vice_versa")
- _subjectRelation.Order = field.NewUint8(tableName, "rlt_order")
+ _subjectRelation.Order = field.NewUint16(tableName, "rlt_order")
_subjectRelation.Subject = subjectRelationHasOneSubject{
db: db.Session(&gorm.Session{}),
@@ -61,7 +61,7 @@ type subjectRelation struct {
RelatedSubjectID field.Uint32 // 关联目标 ID
RelatedSubjectTypeID field.Uint8 // 关联目标类型
ViceVersa field.Bool
- Order field.Uint8 // 关联排序
+ Order field.Uint16 // 关联排序
Subject subjectRelationHasOneSubject
fieldMap map[string]field.Expr
@@ -85,7 +85,7 @@ func (s *subjectRelation) updateTableName(table string) *subjectRelation {
s.RelatedSubjectID = field.NewUint32(table, "rlt_related_subject_id")
s.RelatedSubjectTypeID = field.NewUint8(table, "rlt_related_subject_type_id")
s.ViceVersa = field.NewBool(table, "rlt_vice_versa")
- s.Order = field.NewUint8(table, "rlt_order")
+ s.Order = field.NewUint16(table, "rlt_order")
s.fillFieldMap()
diff --git a/dal/query/chii_subject_revisions.gen.go b/dal/query/chii_subject_revisions.gen.go
index 0d50fdc81..93c7f4927 100644
--- a/dal/query/chii_subject_revisions.gen.go
+++ b/dal/query/chii_subject_revisions.gen.go
@@ -36,6 +36,7 @@ func newSubjectRevision(db *gorm.DB, opts ...gen.DOOption) subjectRevision {
_subjectRevision.Name = field.NewString(tableName, "rev_name")
_subjectRevision.NameCN = field.NewString(tableName, "rev_name_cn")
_subjectRevision.FieldInfobox = field.NewString(tableName, "rev_field_infobox")
+ _subjectRevision.FieldMetaTags = field.NewString(tableName, "rev_field_meta_tags")
_subjectRevision.FieldSummary = field.NewString(tableName, "rev_field_summary")
_subjectRevision.VoteField = field.NewString(tableName, "rev_vote_field")
_subjectRevision.FieldEps = field.NewUint32(tableName, "rev_field_eps")
@@ -60,22 +61,23 @@ func newSubjectRevision(db *gorm.DB, opts ...gen.DOOption) subjectRevision {
type subjectRevision struct {
subjectRevisionDo subjectRevisionDo
- ALL field.Asterisk
- ID field.Uint32
- Type field.Uint8 // 修订类型
- SubjectID field.Uint32
- TypeID field.Uint16
- CreatorID field.Uint32
- Dateline field.Uint32
- Name field.String
- NameCN field.String
- FieldInfobox field.String
- FieldSummary field.String
- VoteField field.String
- FieldEps field.Uint32
- EditSummary field.String
- Platform field.Uint16
- Subject subjectRevisionBelongsToSubject
+ ALL field.Asterisk
+ ID field.Uint32
+ Type field.Uint8 // 修订类型
+ SubjectID field.Uint32
+ TypeID field.Uint16
+ CreatorID field.Uint32
+ Dateline field.Uint32
+ Name field.String
+ NameCN field.String
+ FieldInfobox field.String
+ FieldMetaTags field.String
+ FieldSummary field.String
+ VoteField field.String
+ FieldEps field.Uint32
+ EditSummary field.String
+ Platform field.Uint16
+ Subject subjectRevisionBelongsToSubject
fieldMap map[string]field.Expr
}
@@ -101,6 +103,7 @@ func (s *subjectRevision) updateTableName(table string) *subjectRevision {
s.Name = field.NewString(table, "rev_name")
s.NameCN = field.NewString(table, "rev_name_cn")
s.FieldInfobox = field.NewString(table, "rev_field_infobox")
+ s.FieldMetaTags = field.NewString(table, "rev_field_meta_tags")
s.FieldSummary = field.NewString(table, "rev_field_summary")
s.VoteField = field.NewString(table, "rev_vote_field")
s.FieldEps = field.NewUint32(table, "rev_field_eps")
@@ -134,7 +137,7 @@ func (s *subjectRevision) GetFieldByName(fieldName string) (field.OrderExpr, boo
}
func (s *subjectRevision) fillFieldMap() {
- s.fieldMap = make(map[string]field.Expr, 15)
+ s.fieldMap = make(map[string]field.Expr, 16)
s.fieldMap["rev_id"] = s.ID
s.fieldMap["rev_type"] = s.Type
s.fieldMap["rev_subject_id"] = s.SubjectID
@@ -144,6 +147,7 @@ func (s *subjectRevision) fillFieldMap() {
s.fieldMap["rev_name"] = s.Name
s.fieldMap["rev_name_cn"] = s.NameCN
s.fieldMap["rev_field_infobox"] = s.FieldInfobox
+ s.fieldMap["rev_field_meta_tags"] = s.FieldMetaTags
s.fieldMap["rev_field_summary"] = s.FieldSummary
s.fieldMap["rev_vote_field"] = s.VoteField
s.fieldMap["rev_field_eps"] = s.FieldEps
diff --git a/dal/query/chii_subjects.gen.go b/dal/query/chii_subjects.gen.go
index c89c7275d..53b6a059f 100644
--- a/dal/query/chii_subjects.gen.go
+++ b/dal/query/chii_subjects.gen.go
@@ -29,14 +29,15 @@ func newSubject(db *gorm.DB, opts ...gen.DOOption) subject {
_subject.ALL = field.NewAsterisk(tableName)
_subject.ID = field.NewUint32(tableName, "subject_id")
_subject.TypeID = field.NewUint8(tableName, "subject_type_id")
- _subject.Name = field.NewString(tableName, "subject_name")
- _subject.NameCN = field.NewString(tableName, "subject_name_cn")
+ _subject.Name = field.NewField(tableName, "subject_name")
+ _subject.NameCN = field.NewField(tableName, "subject_name_cn")
_subject.UID = field.NewString(tableName, "subject_uid")
_subject.Creator = field.NewUint32(tableName, "subject_creator")
_subject.Dateline = field.NewUint32(tableName, "subject_dateline")
_subject.Image = field.NewString(tableName, "subject_image")
_subject.Platform = field.NewUint16(tableName, "subject_platform")
- _subject.Infobox = field.NewString(tableName, "field_infobox")
+ _subject.Infobox = field.NewField(tableName, "field_infobox")
+ _subject.FieldMetaTags = field.NewString(tableName, "field_meta_tags")
_subject.Summary = field.NewString(tableName, "field_summary")
_subject.Field5 = field.NewString(tableName, "field_5")
_subject.Volumes = field.NewUint32(tableName, "field_volumes")
@@ -66,33 +67,34 @@ func newSubject(db *gorm.DB, opts ...gen.DOOption) subject {
type subject struct {
subjectDo subjectDo
- ALL field.Asterisk
- ID field.Uint32
- TypeID field.Uint8
- Name field.String
- NameCN field.String
- UID field.String // isbn / imdb
- Creator field.Uint32
- Dateline field.Uint32
- Image field.String
- Platform field.Uint16
- Infobox field.String
- Summary field.String // summary
- Field5 field.String // author summary
- Volumes field.Uint32 // 卷数
- Eps field.Uint32
- Wish field.Uint32
- Done field.Uint32
- Doing field.Uint32
- OnHold field.Uint32 // 搁置人数
- Dropped field.Uint32 // 抛弃人数
- Series field.Bool
- SeriesEntry field.Uint32
- IdxCn field.String
- Airtime field.Uint8
- Nsfw field.Bool
- Ban field.Uint8
- Fields subjectHasOneFields
+ ALL field.Asterisk
+ ID field.Uint32
+ TypeID field.Uint8
+ Name field.Field
+ NameCN field.Field
+ UID field.String // isbn / imdb
+ Creator field.Uint32
+ Dateline field.Uint32
+ Image field.String
+ Platform field.Uint16
+ Infobox field.Field
+ FieldMetaTags field.String
+ Summary field.String // summary
+ Field5 field.String // author summary
+ Volumes field.Uint32 // 卷数
+ Eps field.Uint32
+ Wish field.Uint32
+ Done field.Uint32
+ Doing field.Uint32
+ OnHold field.Uint32 // 搁置人数
+ Dropped field.Uint32 // 抛弃人数
+ Series field.Bool
+ SeriesEntry field.Uint32
+ IdxCn field.String
+ Airtime field.Uint8
+ Nsfw field.Bool
+ Ban field.Uint8
+ Fields subjectHasOneFields
fieldMap map[string]field.Expr
}
@@ -111,14 +113,15 @@ func (s *subject) updateTableName(table string) *subject {
s.ALL = field.NewAsterisk(table)
s.ID = field.NewUint32(table, "subject_id")
s.TypeID = field.NewUint8(table, "subject_type_id")
- s.Name = field.NewString(table, "subject_name")
- s.NameCN = field.NewString(table, "subject_name_cn")
+ s.Name = field.NewField(table, "subject_name")
+ s.NameCN = field.NewField(table, "subject_name_cn")
s.UID = field.NewString(table, "subject_uid")
s.Creator = field.NewUint32(table, "subject_creator")
s.Dateline = field.NewUint32(table, "subject_dateline")
s.Image = field.NewString(table, "subject_image")
s.Platform = field.NewUint16(table, "subject_platform")
- s.Infobox = field.NewString(table, "field_infobox")
+ s.Infobox = field.NewField(table, "field_infobox")
+ s.FieldMetaTags = field.NewString(table, "field_meta_tags")
s.Summary = field.NewString(table, "field_summary")
s.Field5 = field.NewString(table, "field_5")
s.Volumes = field.NewUint32(table, "field_volumes")
@@ -158,7 +161,7 @@ func (s *subject) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
func (s *subject) fillFieldMap() {
- s.fieldMap = make(map[string]field.Expr, 26)
+ s.fieldMap = make(map[string]field.Expr, 27)
s.fieldMap["subject_id"] = s.ID
s.fieldMap["subject_type_id"] = s.TypeID
s.fieldMap["subject_name"] = s.Name
@@ -169,6 +172,7 @@ func (s *subject) fillFieldMap() {
s.fieldMap["subject_image"] = s.Image
s.fieldMap["subject_platform"] = s.Platform
s.fieldMap["field_infobox"] = s.Infobox
+ s.fieldMap["field_meta_tags"] = s.FieldMetaTags
s.fieldMap["field_summary"] = s.Summary
s.fieldMap["field_5"] = s.Field5
s.fieldMap["field_volumes"] = s.Volumes
diff --git a/dal/query/chii_tag_neue_index.gen.go b/dal/query/chii_tag_neue_index.gen.go
new file mode 100644
index 000000000..630c6d284
--- /dev/null
+++ b/dal/query/chii_tag_neue_index.gen.go
@@ -0,0 +1,351 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+ "context"
+
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+ "gorm.io/gorm/schema"
+
+ "gorm.io/gen"
+ "gorm.io/gen/field"
+
+ "gorm.io/plugin/dbresolver"
+
+ "github.com/bangumi/server/dal/dao"
+)
+
+func newTagIndex(db *gorm.DB, opts ...gen.DOOption) tagIndex {
+ _tagIndex := tagIndex{}
+
+ _tagIndex.tagIndexDo.UseDB(db, opts...)
+ _tagIndex.tagIndexDo.UseModel(&dao.TagIndex{})
+
+ tableName := _tagIndex.tagIndexDo.TableName()
+ _tagIndex.ALL = field.NewAsterisk(tableName)
+ _tagIndex.ID = field.NewUint32(tableName, "tag_id")
+ _tagIndex.Name = field.NewString(tableName, "tag_name")
+ _tagIndex.Cat = field.NewInt8(tableName, "tag_cat")
+ _tagIndex.Type = field.NewUint8(tableName, "tag_type")
+ _tagIndex.Results = field.NewUint32(tableName, "tag_results")
+ _tagIndex.CreatedTime = field.NewUint32(tableName, "tag_dateline")
+ _tagIndex.UpdatedTime = field.NewUint32(tableName, "tag_lasttouch")
+
+ _tagIndex.fillFieldMap()
+
+ return _tagIndex
+}
+
+type tagIndex struct {
+ tagIndexDo tagIndexDo
+
+ ALL field.Asterisk
+ ID field.Uint32
+ Name field.String
+ Cat field.Int8 // 0=条目 1=日志 2=天窗
+ Type field.Uint8
+ Results field.Uint32
+ CreatedTime field.Uint32
+ UpdatedTime field.Uint32
+
+ fieldMap map[string]field.Expr
+}
+
+func (t tagIndex) Table(newTableName string) *tagIndex {
+ t.tagIndexDo.UseTable(newTableName)
+ return t.updateTableName(newTableName)
+}
+
+func (t tagIndex) As(alias string) *tagIndex {
+ t.tagIndexDo.DO = *(t.tagIndexDo.As(alias).(*gen.DO))
+ return t.updateTableName(alias)
+}
+
+func (t *tagIndex) updateTableName(table string) *tagIndex {
+ t.ALL = field.NewAsterisk(table)
+ t.ID = field.NewUint32(table, "tag_id")
+ t.Name = field.NewString(table, "tag_name")
+ t.Cat = field.NewInt8(table, "tag_cat")
+ t.Type = field.NewUint8(table, "tag_type")
+ t.Results = field.NewUint32(table, "tag_results")
+ t.CreatedTime = field.NewUint32(table, "tag_dateline")
+ t.UpdatedTime = field.NewUint32(table, "tag_lasttouch")
+
+ t.fillFieldMap()
+
+ return t
+}
+
+func (t *tagIndex) WithContext(ctx context.Context) *tagIndexDo { return t.tagIndexDo.WithContext(ctx) }
+
+func (t tagIndex) TableName() string { return t.tagIndexDo.TableName() }
+
+func (t tagIndex) Alias() string { return t.tagIndexDo.Alias() }
+
+func (t tagIndex) Columns(cols ...field.Expr) gen.Columns { return t.tagIndexDo.Columns(cols...) }
+
+func (t *tagIndex) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+ _f, ok := t.fieldMap[fieldName]
+ if !ok || _f == nil {
+ return nil, false
+ }
+ _oe, ok := _f.(field.OrderExpr)
+ return _oe, ok
+}
+
+func (t *tagIndex) fillFieldMap() {
+ t.fieldMap = make(map[string]field.Expr, 7)
+ t.fieldMap["tag_id"] = t.ID
+ t.fieldMap["tag_name"] = t.Name
+ t.fieldMap["tag_cat"] = t.Cat
+ t.fieldMap["tag_type"] = t.Type
+ t.fieldMap["tag_results"] = t.Results
+ t.fieldMap["tag_dateline"] = t.CreatedTime
+ t.fieldMap["tag_lasttouch"] = t.UpdatedTime
+}
+
+func (t tagIndex) clone(db *gorm.DB) tagIndex {
+ t.tagIndexDo.ReplaceConnPool(db.Statement.ConnPool)
+ return t
+}
+
+func (t tagIndex) replaceDB(db *gorm.DB) tagIndex {
+ t.tagIndexDo.ReplaceDB(db)
+ return t
+}
+
+type tagIndexDo struct{ gen.DO }
+
+func (t tagIndexDo) Debug() *tagIndexDo {
+ return t.withDO(t.DO.Debug())
+}
+
+func (t tagIndexDo) WithContext(ctx context.Context) *tagIndexDo {
+ return t.withDO(t.DO.WithContext(ctx))
+}
+
+func (t tagIndexDo) ReadDB() *tagIndexDo {
+ return t.Clauses(dbresolver.Read)
+}
+
+func (t tagIndexDo) WriteDB() *tagIndexDo {
+ return t.Clauses(dbresolver.Write)
+}
+
+func (t tagIndexDo) Session(config *gorm.Session) *tagIndexDo {
+ return t.withDO(t.DO.Session(config))
+}
+
+func (t tagIndexDo) Clauses(conds ...clause.Expression) *tagIndexDo {
+ return t.withDO(t.DO.Clauses(conds...))
+}
+
+func (t tagIndexDo) Returning(value interface{}, columns ...string) *tagIndexDo {
+ return t.withDO(t.DO.Returning(value, columns...))
+}
+
+func (t tagIndexDo) Not(conds ...gen.Condition) *tagIndexDo {
+ return t.withDO(t.DO.Not(conds...))
+}
+
+func (t tagIndexDo) Or(conds ...gen.Condition) *tagIndexDo {
+ return t.withDO(t.DO.Or(conds...))
+}
+
+func (t tagIndexDo) Select(conds ...field.Expr) *tagIndexDo {
+ return t.withDO(t.DO.Select(conds...))
+}
+
+func (t tagIndexDo) Where(conds ...gen.Condition) *tagIndexDo {
+ return t.withDO(t.DO.Where(conds...))
+}
+
+func (t tagIndexDo) Order(conds ...field.Expr) *tagIndexDo {
+ return t.withDO(t.DO.Order(conds...))
+}
+
+func (t tagIndexDo) Distinct(cols ...field.Expr) *tagIndexDo {
+ return t.withDO(t.DO.Distinct(cols...))
+}
+
+func (t tagIndexDo) Omit(cols ...field.Expr) *tagIndexDo {
+ return t.withDO(t.DO.Omit(cols...))
+}
+
+func (t tagIndexDo) Join(table schema.Tabler, on ...field.Expr) *tagIndexDo {
+ return t.withDO(t.DO.Join(table, on...))
+}
+
+func (t tagIndexDo) LeftJoin(table schema.Tabler, on ...field.Expr) *tagIndexDo {
+ return t.withDO(t.DO.LeftJoin(table, on...))
+}
+
+func (t tagIndexDo) RightJoin(table schema.Tabler, on ...field.Expr) *tagIndexDo {
+ return t.withDO(t.DO.RightJoin(table, on...))
+}
+
+func (t tagIndexDo) Group(cols ...field.Expr) *tagIndexDo {
+ return t.withDO(t.DO.Group(cols...))
+}
+
+func (t tagIndexDo) Having(conds ...gen.Condition) *tagIndexDo {
+ return t.withDO(t.DO.Having(conds...))
+}
+
+func (t tagIndexDo) Limit(limit int) *tagIndexDo {
+ return t.withDO(t.DO.Limit(limit))
+}
+
+func (t tagIndexDo) Offset(offset int) *tagIndexDo {
+ return t.withDO(t.DO.Offset(offset))
+}
+
+func (t tagIndexDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *tagIndexDo {
+ return t.withDO(t.DO.Scopes(funcs...))
+}
+
+func (t tagIndexDo) Unscoped() *tagIndexDo {
+ return t.withDO(t.DO.Unscoped())
+}
+
+func (t tagIndexDo) Create(values ...*dao.TagIndex) error {
+ if len(values) == 0 {
+ return nil
+ }
+ return t.DO.Create(values)
+}
+
+func (t tagIndexDo) CreateInBatches(values []*dao.TagIndex, batchSize int) error {
+ return t.DO.CreateInBatches(values, batchSize)
+}
+
+// Save : !!! underlying implementation is different with GORM
+// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
+func (t tagIndexDo) Save(values ...*dao.TagIndex) error {
+ if len(values) == 0 {
+ return nil
+ }
+ return t.DO.Save(values)
+}
+
+func (t tagIndexDo) First() (*dao.TagIndex, error) {
+ if result, err := t.DO.First(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.TagIndex), nil
+ }
+}
+
+func (t tagIndexDo) Take() (*dao.TagIndex, error) {
+ if result, err := t.DO.Take(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.TagIndex), nil
+ }
+}
+
+func (t tagIndexDo) Last() (*dao.TagIndex, error) {
+ if result, err := t.DO.Last(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.TagIndex), nil
+ }
+}
+
+func (t tagIndexDo) Find() ([]*dao.TagIndex, error) {
+ result, err := t.DO.Find()
+ return result.([]*dao.TagIndex), err
+}
+
+func (t tagIndexDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*dao.TagIndex, err error) {
+ buf := make([]*dao.TagIndex, 0, batchSize)
+ err = t.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+ defer func() { results = append(results, buf...) }()
+ return fc(tx, batch)
+ })
+ return results, err
+}
+
+func (t tagIndexDo) FindInBatches(result *[]*dao.TagIndex, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+ return t.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (t tagIndexDo) Attrs(attrs ...field.AssignExpr) *tagIndexDo {
+ return t.withDO(t.DO.Attrs(attrs...))
+}
+
+func (t tagIndexDo) Assign(attrs ...field.AssignExpr) *tagIndexDo {
+ return t.withDO(t.DO.Assign(attrs...))
+}
+
+func (t tagIndexDo) Joins(fields ...field.RelationField) *tagIndexDo {
+ for _, _f := range fields {
+ t = *t.withDO(t.DO.Joins(_f))
+ }
+ return &t
+}
+
+func (t tagIndexDo) Preload(fields ...field.RelationField) *tagIndexDo {
+ for _, _f := range fields {
+ t = *t.withDO(t.DO.Preload(_f))
+ }
+ return &t
+}
+
+func (t tagIndexDo) FirstOrInit() (*dao.TagIndex, error) {
+ if result, err := t.DO.FirstOrInit(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.TagIndex), nil
+ }
+}
+
+func (t tagIndexDo) FirstOrCreate() (*dao.TagIndex, error) {
+ if result, err := t.DO.FirstOrCreate(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.TagIndex), nil
+ }
+}
+
+func (t tagIndexDo) FindByPage(offset int, limit int) (result []*dao.TagIndex, count int64, err error) {
+ result, err = t.Offset(offset).Limit(limit).Find()
+ if err != nil {
+ return
+ }
+
+ if size := len(result); 0 < limit && 0 < size && size < limit {
+ count = int64(size + offset)
+ return
+ }
+
+ count, err = t.Offset(-1).Limit(-1).Count()
+ return
+}
+
+func (t tagIndexDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+ count, err = t.Count()
+ if err != nil {
+ return
+ }
+
+ err = t.Offset(offset).Limit(limit).Scan(result)
+ return
+}
+
+func (t tagIndexDo) Scan(result interface{}) (err error) {
+ return t.DO.Scan(result)
+}
+
+func (t tagIndexDo) Delete(models ...*dao.TagIndex) (result gen.ResultInfo, err error) {
+ return t.DO.Delete(models)
+}
+
+func (t *tagIndexDo) withDO(do gen.Dao) *tagIndexDo {
+ t.DO = *do.(*gen.DO)
+ return t
+}
diff --git a/dal/query/chii_tag_neue_list.gen.go b/dal/query/chii_tag_neue_list.gen.go
new file mode 100644
index 000000000..ee820e9ef
--- /dev/null
+++ b/dal/query/chii_tag_neue_list.gen.go
@@ -0,0 +1,425 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+ "context"
+
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+ "gorm.io/gorm/schema"
+
+ "gorm.io/gen"
+ "gorm.io/gen/field"
+
+ "gorm.io/plugin/dbresolver"
+
+ "github.com/bangumi/server/dal/dao"
+)
+
+func newTagList(db *gorm.DB, opts ...gen.DOOption) tagList {
+ _tagList := tagList{}
+
+ _tagList.tagListDo.UseDB(db, opts...)
+ _tagList.tagListDo.UseModel(&dao.TagList{})
+
+ tableName := _tagList.tagListDo.TableName()
+ _tagList.ALL = field.NewAsterisk(tableName)
+ _tagList.Tid = field.NewUint32(tableName, "tlt_tid")
+ _tagList.UID = field.NewUint32(tableName, "tlt_uid")
+ _tagList.Cat = field.NewUint8(tableName, "tlt_cat")
+ _tagList.Type = field.NewUint8(tableName, "tlt_type")
+ _tagList.Mid = field.NewUint32(tableName, "tlt_mid")
+ _tagList.CreatedTime = field.NewUint32(tableName, "tlt_dateline")
+ _tagList.Tag = tagListHasOneTag{
+ db: db.Session(&gorm.Session{}),
+
+ RelationField: field.NewRelation("Tag", "dao.TagIndex"),
+ }
+
+ _tagList.fillFieldMap()
+
+ return _tagList
+}
+
+type tagList struct {
+ tagListDo tagListDo
+
+ ALL field.Asterisk
+ Tid field.Uint32
+ UID field.Uint32
+ Cat field.Uint8
+ Type field.Uint8
+ Mid field.Uint32
+ CreatedTime field.Uint32
+ Tag tagListHasOneTag
+
+ fieldMap map[string]field.Expr
+}
+
+func (t tagList) Table(newTableName string) *tagList {
+ t.tagListDo.UseTable(newTableName)
+ return t.updateTableName(newTableName)
+}
+
+func (t tagList) As(alias string) *tagList {
+ t.tagListDo.DO = *(t.tagListDo.As(alias).(*gen.DO))
+ return t.updateTableName(alias)
+}
+
+func (t *tagList) updateTableName(table string) *tagList {
+ t.ALL = field.NewAsterisk(table)
+ t.Tid = field.NewUint32(table, "tlt_tid")
+ t.UID = field.NewUint32(table, "tlt_uid")
+ t.Cat = field.NewUint8(table, "tlt_cat")
+ t.Type = field.NewUint8(table, "tlt_type")
+ t.Mid = field.NewUint32(table, "tlt_mid")
+ t.CreatedTime = field.NewUint32(table, "tlt_dateline")
+
+ t.fillFieldMap()
+
+ return t
+}
+
+func (t *tagList) WithContext(ctx context.Context) *tagListDo { return t.tagListDo.WithContext(ctx) }
+
+func (t tagList) TableName() string { return t.tagListDo.TableName() }
+
+func (t tagList) Alias() string { return t.tagListDo.Alias() }
+
+func (t tagList) Columns(cols ...field.Expr) gen.Columns { return t.tagListDo.Columns(cols...) }
+
+func (t *tagList) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+ _f, ok := t.fieldMap[fieldName]
+ if !ok || _f == nil {
+ return nil, false
+ }
+ _oe, ok := _f.(field.OrderExpr)
+ return _oe, ok
+}
+
+func (t *tagList) fillFieldMap() {
+ t.fieldMap = make(map[string]field.Expr, 7)
+ t.fieldMap["tlt_tid"] = t.Tid
+ t.fieldMap["tlt_uid"] = t.UID
+ t.fieldMap["tlt_cat"] = t.Cat
+ t.fieldMap["tlt_type"] = t.Type
+ t.fieldMap["tlt_mid"] = t.Mid
+ t.fieldMap["tlt_dateline"] = t.CreatedTime
+
+}
+
+func (t tagList) clone(db *gorm.DB) tagList {
+ t.tagListDo.ReplaceConnPool(db.Statement.ConnPool)
+ return t
+}
+
+func (t tagList) replaceDB(db *gorm.DB) tagList {
+ t.tagListDo.ReplaceDB(db)
+ return t
+}
+
+type tagListHasOneTag struct {
+ db *gorm.DB
+
+ field.RelationField
+}
+
+func (a tagListHasOneTag) Where(conds ...field.Expr) *tagListHasOneTag {
+ if len(conds) == 0 {
+ return &a
+ }
+
+ exprs := make([]clause.Expression, 0, len(conds))
+ for _, cond := range conds {
+ exprs = append(exprs, cond.BeCond().(clause.Expression))
+ }
+ a.db = a.db.Clauses(clause.Where{Exprs: exprs})
+ return &a
+}
+
+func (a tagListHasOneTag) WithContext(ctx context.Context) *tagListHasOneTag {
+ a.db = a.db.WithContext(ctx)
+ return &a
+}
+
+func (a tagListHasOneTag) Session(session *gorm.Session) *tagListHasOneTag {
+ a.db = a.db.Session(session)
+ return &a
+}
+
+func (a tagListHasOneTag) Model(m *dao.TagList) *tagListHasOneTagTx {
+ return &tagListHasOneTagTx{a.db.Model(m).Association(a.Name())}
+}
+
+type tagListHasOneTagTx struct{ tx *gorm.Association }
+
+func (a tagListHasOneTagTx) Find() (result *dao.TagIndex, err error) {
+ return result, a.tx.Find(&result)
+}
+
+func (a tagListHasOneTagTx) Append(values ...*dao.TagIndex) (err error) {
+ targetValues := make([]interface{}, len(values))
+ for i, v := range values {
+ targetValues[i] = v
+ }
+ return a.tx.Append(targetValues...)
+}
+
+func (a tagListHasOneTagTx) Replace(values ...*dao.TagIndex) (err error) {
+ targetValues := make([]interface{}, len(values))
+ for i, v := range values {
+ targetValues[i] = v
+ }
+ return a.tx.Replace(targetValues...)
+}
+
+func (a tagListHasOneTagTx) Delete(values ...*dao.TagIndex) (err error) {
+ targetValues := make([]interface{}, len(values))
+ for i, v := range values {
+ targetValues[i] = v
+ }
+ return a.tx.Delete(targetValues...)
+}
+
+func (a tagListHasOneTagTx) Clear() error {
+ return a.tx.Clear()
+}
+
+func (a tagListHasOneTagTx) Count() int64 {
+ return a.tx.Count()
+}
+
+type tagListDo struct{ gen.DO }
+
+func (t tagListDo) Debug() *tagListDo {
+ return t.withDO(t.DO.Debug())
+}
+
+func (t tagListDo) WithContext(ctx context.Context) *tagListDo {
+ return t.withDO(t.DO.WithContext(ctx))
+}
+
+func (t tagListDo) ReadDB() *tagListDo {
+ return t.Clauses(dbresolver.Read)
+}
+
+func (t tagListDo) WriteDB() *tagListDo {
+ return t.Clauses(dbresolver.Write)
+}
+
+func (t tagListDo) Session(config *gorm.Session) *tagListDo {
+ return t.withDO(t.DO.Session(config))
+}
+
+func (t tagListDo) Clauses(conds ...clause.Expression) *tagListDo {
+ return t.withDO(t.DO.Clauses(conds...))
+}
+
+func (t tagListDo) Returning(value interface{}, columns ...string) *tagListDo {
+ return t.withDO(t.DO.Returning(value, columns...))
+}
+
+func (t tagListDo) Not(conds ...gen.Condition) *tagListDo {
+ return t.withDO(t.DO.Not(conds...))
+}
+
+func (t tagListDo) Or(conds ...gen.Condition) *tagListDo {
+ return t.withDO(t.DO.Or(conds...))
+}
+
+func (t tagListDo) Select(conds ...field.Expr) *tagListDo {
+ return t.withDO(t.DO.Select(conds...))
+}
+
+func (t tagListDo) Where(conds ...gen.Condition) *tagListDo {
+ return t.withDO(t.DO.Where(conds...))
+}
+
+func (t tagListDo) Order(conds ...field.Expr) *tagListDo {
+ return t.withDO(t.DO.Order(conds...))
+}
+
+func (t tagListDo) Distinct(cols ...field.Expr) *tagListDo {
+ return t.withDO(t.DO.Distinct(cols...))
+}
+
+func (t tagListDo) Omit(cols ...field.Expr) *tagListDo {
+ return t.withDO(t.DO.Omit(cols...))
+}
+
+func (t tagListDo) Join(table schema.Tabler, on ...field.Expr) *tagListDo {
+ return t.withDO(t.DO.Join(table, on...))
+}
+
+func (t tagListDo) LeftJoin(table schema.Tabler, on ...field.Expr) *tagListDo {
+ return t.withDO(t.DO.LeftJoin(table, on...))
+}
+
+func (t tagListDo) RightJoin(table schema.Tabler, on ...field.Expr) *tagListDo {
+ return t.withDO(t.DO.RightJoin(table, on...))
+}
+
+func (t tagListDo) Group(cols ...field.Expr) *tagListDo {
+ return t.withDO(t.DO.Group(cols...))
+}
+
+func (t tagListDo) Having(conds ...gen.Condition) *tagListDo {
+ return t.withDO(t.DO.Having(conds...))
+}
+
+func (t tagListDo) Limit(limit int) *tagListDo {
+ return t.withDO(t.DO.Limit(limit))
+}
+
+func (t tagListDo) Offset(offset int) *tagListDo {
+ return t.withDO(t.DO.Offset(offset))
+}
+
+func (t tagListDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *tagListDo {
+ return t.withDO(t.DO.Scopes(funcs...))
+}
+
+func (t tagListDo) Unscoped() *tagListDo {
+ return t.withDO(t.DO.Unscoped())
+}
+
+func (t tagListDo) Create(values ...*dao.TagList) error {
+ if len(values) == 0 {
+ return nil
+ }
+ return t.DO.Create(values)
+}
+
+func (t tagListDo) CreateInBatches(values []*dao.TagList, batchSize int) error {
+ return t.DO.CreateInBatches(values, batchSize)
+}
+
+// Save : !!! underlying implementation is different with GORM
+// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
+func (t tagListDo) Save(values ...*dao.TagList) error {
+ if len(values) == 0 {
+ return nil
+ }
+ return t.DO.Save(values)
+}
+
+func (t tagListDo) First() (*dao.TagList, error) {
+ if result, err := t.DO.First(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.TagList), nil
+ }
+}
+
+func (t tagListDo) Take() (*dao.TagList, error) {
+ if result, err := t.DO.Take(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.TagList), nil
+ }
+}
+
+func (t tagListDo) Last() (*dao.TagList, error) {
+ if result, err := t.DO.Last(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.TagList), nil
+ }
+}
+
+func (t tagListDo) Find() ([]*dao.TagList, error) {
+ result, err := t.DO.Find()
+ return result.([]*dao.TagList), err
+}
+
+func (t tagListDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*dao.TagList, err error) {
+ buf := make([]*dao.TagList, 0, batchSize)
+ err = t.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+ defer func() { results = append(results, buf...) }()
+ return fc(tx, batch)
+ })
+ return results, err
+}
+
+func (t tagListDo) FindInBatches(result *[]*dao.TagList, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+ return t.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (t tagListDo) Attrs(attrs ...field.AssignExpr) *tagListDo {
+ return t.withDO(t.DO.Attrs(attrs...))
+}
+
+func (t tagListDo) Assign(attrs ...field.AssignExpr) *tagListDo {
+ return t.withDO(t.DO.Assign(attrs...))
+}
+
+func (t tagListDo) Joins(fields ...field.RelationField) *tagListDo {
+ for _, _f := range fields {
+ t = *t.withDO(t.DO.Joins(_f))
+ }
+ return &t
+}
+
+func (t tagListDo) Preload(fields ...field.RelationField) *tagListDo {
+ for _, _f := range fields {
+ t = *t.withDO(t.DO.Preload(_f))
+ }
+ return &t
+}
+
+func (t tagListDo) FirstOrInit() (*dao.TagList, error) {
+ if result, err := t.DO.FirstOrInit(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.TagList), nil
+ }
+}
+
+func (t tagListDo) FirstOrCreate() (*dao.TagList, error) {
+ if result, err := t.DO.FirstOrCreate(); err != nil {
+ return nil, err
+ } else {
+ return result.(*dao.TagList), nil
+ }
+}
+
+func (t tagListDo) FindByPage(offset int, limit int) (result []*dao.TagList, count int64, err error) {
+ result, err = t.Offset(offset).Limit(limit).Find()
+ if err != nil {
+ return
+ }
+
+ if size := len(result); 0 < limit && 0 < size && size < limit {
+ count = int64(size + offset)
+ return
+ }
+
+ count, err = t.Offset(-1).Limit(-1).Count()
+ return
+}
+
+func (t tagListDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+ count, err = t.Count()
+ if err != nil {
+ return
+ }
+
+ err = t.Offset(offset).Limit(limit).Scan(result)
+ return
+}
+
+func (t tagListDo) Scan(result interface{}) (err error) {
+ return t.DO.Scan(result)
+}
+
+func (t tagListDo) Delete(models ...*dao.TagList) (result gen.ResultInfo, err error) {
+ return t.DO.Delete(models)
+}
+
+func (t *tagListDo) withDO(do gen.Dao) *tagListDo {
+ t.DO = *do.(*gen.DO)
+ return t
+}
diff --git a/internal/subject/repo2.go b/dal/query/export_db.go
similarity index 87%
rename from internal/subject/repo2.go
rename to dal/query/export_db.go
index 32f36ee71..bac903af6 100644
--- a/internal/subject/repo2.go
+++ b/dal/query/export_db.go
@@ -12,4 +12,12 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see
-package subject
+package query
+
+import (
+ "gorm.io/gorm"
+)
+
+func (q *Query) DB() *gorm.DB {
+ return q.db
+}
diff --git a/dal/query/gen.go b/dal/query/gen.go
index 7d1e11c49..d24e02f71 100644
--- a/dal/query/gen.go
+++ b/dal/query/gen.go
@@ -32,6 +32,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
Notification: newNotification(db, opts...),
NotificationField: newNotificationField(db, opts...),
Person: newPerson(db, opts...),
+ PersonCollect: newPersonCollect(db, opts...),
PersonField: newPersonField(db, opts...),
PersonSubjects: newPersonSubjects(db, opts...),
PrivateMessage: newPrivateMessage(db, opts...),
@@ -42,6 +43,8 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
SubjectField: newSubjectField(db, opts...),
SubjectRelation: newSubjectRelation(db, opts...),
SubjectRevision: newSubjectRevision(db, opts...),
+ TagIndex: newTagIndex(db, opts...),
+ TagList: newTagList(db, opts...),
UserGroup: newUserGroup(db, opts...),
WebSession: newWebSession(db, opts...),
}
@@ -64,6 +67,7 @@ type Query struct {
Notification notification
NotificationField notificationField
Person person
+ PersonCollect personCollect
PersonField personField
PersonSubjects personSubjects
PrivateMessage privateMessage
@@ -74,6 +78,8 @@ type Query struct {
SubjectField subjectField
SubjectRelation subjectRelation
SubjectRevision subjectRevision
+ TagIndex tagIndex
+ TagList tagList
UserGroup userGroup
WebSession webSession
}
@@ -97,6 +103,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
Notification: q.Notification.clone(db),
NotificationField: q.NotificationField.clone(db),
Person: q.Person.clone(db),
+ PersonCollect: q.PersonCollect.clone(db),
PersonField: q.PersonField.clone(db),
PersonSubjects: q.PersonSubjects.clone(db),
PrivateMessage: q.PrivateMessage.clone(db),
@@ -107,6 +114,8 @@ func (q *Query) clone(db *gorm.DB) *Query {
SubjectField: q.SubjectField.clone(db),
SubjectRelation: q.SubjectRelation.clone(db),
SubjectRevision: q.SubjectRevision.clone(db),
+ TagIndex: q.TagIndex.clone(db),
+ TagList: q.TagList.clone(db),
UserGroup: q.UserGroup.clone(db),
WebSession: q.WebSession.clone(db),
}
@@ -137,6 +146,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
Notification: q.Notification.replaceDB(db),
NotificationField: q.NotificationField.replaceDB(db),
Person: q.Person.replaceDB(db),
+ PersonCollect: q.PersonCollect.replaceDB(db),
PersonField: q.PersonField.replaceDB(db),
PersonSubjects: q.PersonSubjects.replaceDB(db),
PrivateMessage: q.PrivateMessage.replaceDB(db),
@@ -147,6 +157,8 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
SubjectField: q.SubjectField.replaceDB(db),
SubjectRelation: q.SubjectRelation.replaceDB(db),
SubjectRevision: q.SubjectRevision.replaceDB(db),
+ TagIndex: q.TagIndex.replaceDB(db),
+ TagList: q.TagList.replaceDB(db),
UserGroup: q.UserGroup.replaceDB(db),
WebSession: q.WebSession.replaceDB(db),
}
@@ -167,6 +179,7 @@ type queryCtx struct {
Notification *notificationDo
NotificationField *notificationFieldDo
Person *personDo
+ PersonCollect *personCollectDo
PersonField *personFieldDo
PersonSubjects *personSubjectsDo
PrivateMessage *privateMessageDo
@@ -177,6 +190,8 @@ type queryCtx struct {
SubjectField *subjectFieldDo
SubjectRelation *subjectRelationDo
SubjectRevision *subjectRevisionDo
+ TagIndex *tagIndexDo
+ TagList *tagListDo
UserGroup *userGroupDo
WebSession *webSessionDo
}
@@ -197,6 +212,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
Notification: q.Notification.WithContext(ctx),
NotificationField: q.NotificationField.WithContext(ctx),
Person: q.Person.WithContext(ctx),
+ PersonCollect: q.PersonCollect.WithContext(ctx),
PersonField: q.PersonField.WithContext(ctx),
PersonSubjects: q.PersonSubjects.WithContext(ctx),
PrivateMessage: q.PrivateMessage.WithContext(ctx),
@@ -207,6 +223,8 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
SubjectField: q.SubjectField.WithContext(ctx),
SubjectRelation: q.SubjectRelation.WithContext(ctx),
SubjectRevision: q.SubjectRevision.WithContext(ctx),
+ TagIndex: q.TagIndex.WithContext(ctx),
+ TagList: q.TagList.WithContext(ctx),
UserGroup: q.UserGroup.WithContext(ctx),
WebSession: q.WebSession.WithContext(ctx),
}
diff --git a/domain/relation.go b/domain/relation.go
index 6240c906f..ac9b6714c 100644
--- a/domain/relation.go
+++ b/domain/relation.go
@@ -29,6 +29,7 @@ type SubjectPersonRelation struct {
PersonID model.PersonID
SubjectID model.SubjectID
+ Eps string
}
type SubjectCharacterRelation struct {
diff --git a/etc/Dockerfile b/etc/Dockerfile
index c318a35b6..76bc339a6 100644
--- a/etc/Dockerfile
+++ b/etc/Dockerfile
@@ -1,4 +1,4 @@
-FROM gcr.io/distroless/static@sha256:41972110a1c1a5c0b6adb283e8aa092c43c31f7c5d79b8656fbffff2c3e61f05
+FROM gcr.io/distroless/static@sha256:5c7e2b465ac6a2a4e5f4f7f722ce43b147dabe87cb21ac6c4007ae5178a1fa58
ENTRYPOINT ["/app/chii.exe"]
diff --git a/etc/mock.task.yaml b/etc/mock.task.yaml
index 57ef2ef06..47292fda1 100644
--- a/etc/mock.task.yaml
+++ b/etc/mock.task.yaml
@@ -6,6 +6,7 @@ vars:
tasks:
all:
cmds:
+ - task: Tag
- task: session-manager
- task: session-repo
- task: cache
@@ -98,6 +99,19 @@ tasks:
MOCK_STRUCT: "AuthService"
INTERFACE: Service
+ "Tag":
+ sources:
+ - internal/tag/domain.go
+ - ./internal/pkg/tools/go.mod
+ generates:
+ - internal/mocks/TagRepo.go
+ cmds:
+ - task: base-mock
+ vars:
+ SRC_DIR: ./internal/tag
+ INTERFACE: "Repo"
+ MOCK_STRUCT: "TagRepo"
+
"CharacterRepo":
sources:
- ./internal/character/domain/.go
diff --git a/generated/proto/go/api/v1/timeline.pb.go b/generated/proto/go/api/v1/timeline.pb.go
index 864c35ffd..2728da773 100644
--- a/generated/proto/go/api/v1/timeline.pb.go
+++ b/generated/proto/go/api/v1/timeline.pb.go
@@ -2,7 +2,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
-// protoc-gen-go v1.30.0
+// protoc-gen-go v1.34.2
// protoc (unknown)
// source: api/v1/timeline.proto
@@ -787,7 +787,7 @@ func file_api_v1_timeline_proto_rawDescGZIP() []byte {
}
var file_api_v1_timeline_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
-var file_api_v1_timeline_proto_goTypes = []interface{}{
+var file_api_v1_timeline_proto_goTypes = []any{
(*HelloRequest)(nil), // 0: api.v1.HelloRequest
(*HelloResponse)(nil), // 1: api.v1.HelloResponse
(*SubjectCollectResponse)(nil), // 2: api.v1.SubjectCollectResponse
@@ -825,7 +825,7 @@ func file_api_v1_timeline_proto_init() {
return
}
if !protoimpl.UnsafeEnabled {
- file_api_v1_timeline_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ file_api_v1_timeline_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*HelloRequest); i {
case 0:
return &v.state
@@ -837,7 +837,7 @@ func file_api_v1_timeline_proto_init() {
return nil
}
}
- file_api_v1_timeline_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ file_api_v1_timeline_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*HelloResponse); i {
case 0:
return &v.state
@@ -849,7 +849,7 @@ func file_api_v1_timeline_proto_init() {
return nil
}
}
- file_api_v1_timeline_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ file_api_v1_timeline_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*SubjectCollectResponse); i {
case 0:
return &v.state
@@ -861,7 +861,7 @@ func file_api_v1_timeline_proto_init() {
return nil
}
}
- file_api_v1_timeline_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ file_api_v1_timeline_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*SubjectProgressResponse); i {
case 0:
return &v.state
@@ -873,7 +873,7 @@ func file_api_v1_timeline_proto_init() {
return nil
}
}
- file_api_v1_timeline_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ file_api_v1_timeline_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*EpisodeCollectResponse); i {
case 0:
return &v.state
@@ -885,7 +885,7 @@ func file_api_v1_timeline_proto_init() {
return nil
}
}
- file_api_v1_timeline_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ file_api_v1_timeline_proto_msgTypes[5].Exporter = func(v any, i int) any {
switch v := v.(*Subject); i {
case 0:
return &v.state
@@ -897,7 +897,7 @@ func file_api_v1_timeline_proto_init() {
return nil
}
}
- file_api_v1_timeline_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ file_api_v1_timeline_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*Episode); i {
case 0:
return &v.state
@@ -909,7 +909,7 @@ func file_api_v1_timeline_proto_init() {
return nil
}
}
- file_api_v1_timeline_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+ file_api_v1_timeline_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*SubjectCollectRequest); i {
case 0:
return &v.state
@@ -921,7 +921,7 @@ func file_api_v1_timeline_proto_init() {
return nil
}
}
- file_api_v1_timeline_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+ file_api_v1_timeline_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*EpisodeCollectRequest); i {
case 0:
return &v.state
@@ -933,7 +933,7 @@ func file_api_v1_timeline_proto_init() {
return nil
}
}
- file_api_v1_timeline_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+ file_api_v1_timeline_proto_msgTypes[9].Exporter = func(v any, i int) any {
switch v := v.(*SubjectProgressRequest); i {
case 0:
return &v.state
diff --git a/generated/proto/go/api/v1/timeline_grpc.pb.go b/generated/proto/go/api/v1/timeline_grpc.pb.go
index e41f6a4fa..1ad4df8cd 100644
--- a/generated/proto/go/api/v1/timeline_grpc.pb.go
+++ b/generated/proto/go/api/v1/timeline_grpc.pb.go
@@ -2,7 +2,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
-// - protoc-gen-go-grpc v1.3.0
+// - protoc-gen-go-grpc v1.5.1
// - protoc (unknown)
// source: api/v1/timeline.proto
@@ -17,8 +17,8 @@ import (
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
-// Requires gRPC-Go v1.32.0 or later.
-const _ = grpc.SupportPackageIsVersion7
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
const (
TimeLineService_Hello_FullMethodName = "/api.v1.TimeLineService/Hello"
@@ -47,8 +47,9 @@ func NewTimeLineServiceClient(cc grpc.ClientConnInterface) TimeLineServiceClient
}
func (c *timeLineServiceClient) Hello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(HelloResponse)
- err := c.cc.Invoke(ctx, TimeLineService_Hello_FullMethodName, in, out, opts...)
+ err := c.cc.Invoke(ctx, TimeLineService_Hello_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -56,8 +57,9 @@ func (c *timeLineServiceClient) Hello(ctx context.Context, in *HelloRequest, opt
}
func (c *timeLineServiceClient) SubjectCollect(ctx context.Context, in *SubjectCollectRequest, opts ...grpc.CallOption) (*SubjectCollectResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SubjectCollectResponse)
- err := c.cc.Invoke(ctx, TimeLineService_SubjectCollect_FullMethodName, in, out, opts...)
+ err := c.cc.Invoke(ctx, TimeLineService_SubjectCollect_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -65,8 +67,9 @@ func (c *timeLineServiceClient) SubjectCollect(ctx context.Context, in *SubjectC
}
func (c *timeLineServiceClient) SubjectProgress(ctx context.Context, in *SubjectProgressRequest, opts ...grpc.CallOption) (*SubjectProgressResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SubjectProgressResponse)
- err := c.cc.Invoke(ctx, TimeLineService_SubjectProgress_FullMethodName, in, out, opts...)
+ err := c.cc.Invoke(ctx, TimeLineService_SubjectProgress_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -74,8 +77,9 @@ func (c *timeLineServiceClient) SubjectProgress(ctx context.Context, in *Subject
}
func (c *timeLineServiceClient) EpisodeCollect(ctx context.Context, in *EpisodeCollectRequest, opts ...grpc.CallOption) (*EpisodeCollectResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(EpisodeCollectResponse)
- err := c.cc.Invoke(ctx, TimeLineService_EpisodeCollect_FullMethodName, in, out, opts...)
+ err := c.cc.Invoke(ctx, TimeLineService_EpisodeCollect_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -84,7 +88,7 @@ func (c *timeLineServiceClient) EpisodeCollect(ctx context.Context, in *EpisodeC
// TimeLineServiceServer is the server API for TimeLineService service.
// All implementations must embed UnimplementedTimeLineServiceServer
-// for forward compatibility
+// for forward compatibility.
type TimeLineServiceServer interface {
// Debug function
Hello(context.Context, *HelloRequest) (*HelloResponse, error)
@@ -94,9 +98,12 @@ type TimeLineServiceServer interface {
mustEmbedUnimplementedTimeLineServiceServer()
}
-// UnimplementedTimeLineServiceServer must be embedded to have forward compatible implementations.
-type UnimplementedTimeLineServiceServer struct {
-}
+// UnimplementedTimeLineServiceServer must be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedTimeLineServiceServer struct{}
func (UnimplementedTimeLineServiceServer) Hello(context.Context, *HelloRequest) (*HelloResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Hello not implemented")
@@ -111,6 +118,7 @@ func (UnimplementedTimeLineServiceServer) EpisodeCollect(context.Context, *Episo
return nil, status.Errorf(codes.Unimplemented, "method EpisodeCollect not implemented")
}
func (UnimplementedTimeLineServiceServer) mustEmbedUnimplementedTimeLineServiceServer() {}
+func (UnimplementedTimeLineServiceServer) testEmbeddedByValue() {}
// UnsafeTimeLineServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to TimeLineServiceServer will
@@ -120,6 +128,13 @@ type UnsafeTimeLineServiceServer interface {
}
func RegisterTimeLineServiceServer(s grpc.ServiceRegistrar, srv TimeLineServiceServer) {
+ // If the following call pancis, it indicates UnimplementedTimeLineServiceServer was
+ // embedded by pointer and is nil. This will cause panics if an
+ // unimplemented method is ever invoked, so we test this at initialization
+ // time to prevent it from happening at runtime later due to I/O.
+ if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+ t.testEmbeddedByValue()
+ }
s.RegisterService(&TimeLineService_ServiceDesc, srv)
}
diff --git a/go.mod b/go.mod
index 403d2617d..7904b9973 100644
--- a/go.mod
+++ b/go.mod
@@ -1,103 +1,109 @@
module github.com/bangumi/server
-go 1.22
-
-toolchain go1.22.2
+go 1.23.2
require (
github.com/avast/retry-go/v4 v4.6.0
- github.com/aws/aws-sdk-go v1.51.32
+ github.com/aws/aws-sdk-go-v2 v1.32.5
+ github.com/aws/aws-sdk-go-v2/credentials v1.17.46
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0
+ github.com/bytedance/sonic v1.12.5
github.com/davecgh/go-spew v1.1.1
github.com/elliotchance/phpserialize v1.4.0
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
- github.com/go-playground/validator/v10 v10.20.0
- github.com/go-redis/redismock/v9 v9.2.0
- github.com/go-resty/resty/v2 v2.12.0
+ github.com/go-playground/validator/v10 v10.23.0
+ github.com/go-resty/resty/v2 v2.16.2
github.com/go-sql-driver/mysql v1.8.1
+ github.com/google/uuid v1.6.0
github.com/ilyakaznacheev/cleanenv v1.5.0
github.com/jarcoal/httpmock v1.3.1
+ github.com/jmoiron/sqlx v1.4.0
github.com/labstack/echo/v4 v4.12.0
github.com/mattn/go-colorable v0.1.13
- github.com/meilisearch/meilisearch-go v0.26.2
+ github.com/meilisearch/meilisearch-go v0.29.0
github.com/mitchellh/mapstructure v1.5.0
- github.com/prometheus/client_golang v1.19.0
- github.com/redis/go-redis/v9 v9.5.1
- github.com/samber/lo v1.39.0
+ github.com/prometheus/client_golang v1.20.5
+ github.com/redis/rueidis v1.0.49
+ github.com/samber/lo v1.47.0
github.com/segmentio/kafka-go v0.4.47
- github.com/spf13/cobra v1.8.0
+ github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
- github.com/stretchr/testify v1.9.0
- github.com/trim21/errgo v0.0.2
- github.com/trim21/go-phpserialize v0.0.21
- github.com/trim21/go-redis-prometheus v0.0.0
+ github.com/stretchr/testify v1.10.0
+ github.com/trim21/errgo v0.0.3
+ github.com/trim21/go-phpserialize v0.1.0-alpha.5
github.com/trim21/htest v0.0.4
- github.com/trim21/pkg v0.0.3
- go.etcd.io/etcd/client/v3 v3.5.13
- go.uber.org/fx v1.21.1
+ github.com/trim21/pkg v0.0.4
+ go.uber.org/fx v1.23.0
go.uber.org/zap v1.27.0
- golang.org/x/crypto v0.22.0
- google.golang.org/grpc v1.63.2
- google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
- google.golang.org/protobuf v1.34.0
+ golang.org/x/crypto v0.29.0
+ golang.org/x/text v0.20.0
+ google.golang.org/grpc v1.68.0
+ google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1
+ google.golang.org/protobuf v1.35.2
gopkg.in/yaml.v3 v3.0.1
- gorm.io/driver/mysql v1.5.6
+ gorm.io/driver/mysql v1.5.7
gorm.io/gen v0.3.26
- gorm.io/gorm v1.25.10
- gorm.io/plugin/dbresolver v1.5.1
+ gorm.io/gorm v1.25.12
+ gorm.io/plugin/dbresolver v1.5.3
gorm.io/plugin/soft_delete v1.2.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
- github.com/BurntSushi/toml v1.2.1 // indirect
- github.com/andybalholm/brotli v1.0.6 // indirect
+ github.com/BurntSushi/toml v1.4.0 // indirect
+ github.com/andybalholm/brotli v1.1.0 // indirect
+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 // indirect
+ github.com/aws/smithy-go v1.22.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
- github.com/cespare/xxhash/v2 v2.2.0 // indirect
- github.com/coreos/go-semver v0.3.0 // indirect
- github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 // indirect
- github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
- github.com/fsnotify/fsnotify v1.6.0 // indirect
- github.com/gabriel-vasile/mimetype v1.4.3 // indirect
- github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/bytedance/sonic/loader v0.2.0 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/cloudwego/base64x v0.1.4 // indirect
+ github.com/cloudwego/iasm v0.2.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
- github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
- github.com/golang/protobuf v1.5.4 // indirect
+ github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
- github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
- github.com/klauspost/compress v1.16.7 // indirect
+ github.com/klauspost/compress v1.17.10 // indirect
+ github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/pierrec/lz4/v4 v4.1.18 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/prometheus/client_model v0.5.0 // indirect
- github.com/prometheus/common v0.48.0 // indirect
- github.com/prometheus/procfs v0.12.0 // indirect
+ github.com/prometheus/client_model v0.6.1 // indirect
+ github.com/prometheus/common v0.60.0 // indirect
+ github.com/prometheus/procfs v0.15.1 // indirect
+ github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
- github.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
- go.etcd.io/etcd/api/v3 v3.5.13 // indirect
- go.etcd.io/etcd/client/pkg/v3 v3.5.13 // indirect
- go.uber.org/dig v1.17.1 // indirect
- go.uber.org/multierr v1.10.0 // indirect
- golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
- golang.org/x/mod v0.14.0 // indirect
- golang.org/x/net v0.24.0 // indirect
- golang.org/x/sys v0.19.0 // indirect
- golang.org/x/text v0.14.0 // indirect
- golang.org/x/time v0.5.0 // indirect
- golang.org/x/tools v0.17.0 // indirect
- google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
- gorm.io/datatypes v1.2.0 // indirect
+ go.uber.org/dig v1.18.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect
+ golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
+ golang.org/x/mod v0.21.0 // indirect
+ golang.org/x/net v0.30.0 // indirect
+ golang.org/x/sync v0.9.0 // indirect
+ golang.org/x/sys v0.27.0 // indirect
+ golang.org/x/time v0.7.0 // indirect
+ golang.org/x/tools v0.26.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f // indirect
+ gorm.io/datatypes v1.2.2 // indirect
gorm.io/hints v1.1.2 // indirect
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
)
diff --git a/go.sum b/go.sum
index 541d8f38a..e9657e3c9 100644
--- a/go.sum
+++ b/go.sum
@@ -1,204 +1,101 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
-github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
-github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
-github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
-github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
-github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
-github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
-github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
-github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
-github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
-github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
-github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
-github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
+github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA=
github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE=
-github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
-github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
-github.com/aws/aws-sdk-go v1.51.32 h1:A6mPui7QP4mwmovyzgtdedbRbNur1Iu0/El7hBWNHms=
-github.com/aws/aws-sdk-go v1.51.32/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
-github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo=
+github.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 h1:JX70yGKLj25+lMC5Yyh8wBtvB01GDilyRuJvXJ4piD0=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24/go.mod h1:+Ln60j9SUTD0LEwnhEB0Xhg61DHqplBrbZpLgyjoEHg=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 h1:gvZOjQKPxFXy1ft3QnEyXmT+IqneM9QAUWlM3r0mfqw=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5/go.mod h1:DLWnfvIcm9IET/mmjdxeXbBKmTCm0ZB8p1za9BVteM8=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 h1:P1doBzv5VEg1ONxnJss1Kh5ZG/ewoIE4MQtKKc6Crgg=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5/go.mod h1:NOP+euMW7W3Ukt28tAxPuoWao4rhhqJD3QEBk7oCg7w=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0 h1:Q2ax8S21clKOnHhhr933xm3JxdJebql+R7aNo7p7GBQ=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0/go.mod h1:ralv4XawHjEMaHOWnTFushl0WRqim/gQWesAMF6hTow=
+github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
+github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/bsm/ginkgo/v2 v2.5.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
-github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
-github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
-github.com/bsm/gomega v1.20.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATsbwk=
-github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
-github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
-github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
-github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
-github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
-github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
-github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
-github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27wIbmOGUs7RIbVgPEjb31ehTVniDwPGXyMxm5U=
-github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
+github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
+github.com/bytedance/sonic v1.12.5 h1:hoZxY8uW+mT+OpkcUWw4k0fDINtOcVavEsGfzwzFU/w=
+github.com/bytedance/sonic v1.12.5/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
+github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+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/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
-github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
-github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
-github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
-github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
-github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
-github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elliotchance/phpserialize v1.4.0 h1:cAp/9+KSnEbUC8oYCE32n2n84BeW8HOY3HMDI8hG2OY=
github.com/elliotchance/phpserialize v1.4.0/go.mod h1:gt7XX9+ETUcLXbtTKEuyrqW3lcLUAeS/AnGZ2e49TZs=
-github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
-github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
-github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
-github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
-github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
-github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
-github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
+github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
+github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
+github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
-github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
-github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw=
-github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0=
-github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA=
-github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0=
-github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
-github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
+github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
+github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
-github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
-github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
-github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
+github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
-github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
-github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
-github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
-github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
-github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
-github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
-github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
-github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
-github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
-github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
-github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
-github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
-github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
-github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
-github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
-github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
-github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-github.com/jackc/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA=
-github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
+github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
+github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
+github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -207,499 +104,209 @@ github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
-github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
-github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
-github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
-github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
-github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
-github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
-github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
-github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
-github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
-github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
-github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
-github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
+github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
-github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
-github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
-github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
-github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
-github.com/meilisearch/meilisearch-go v0.26.2 h1:3gTlmiV1dHHumVUhYdJbvh3camiNiyqQ1hNveVsU2OE=
-github.com/meilisearch/meilisearch-go v0.26.2/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
+github.com/meilisearch/meilisearch-go v0.29.0 h1:HZ9NEKN59USINQ/DXJge/aaXq8IrsKbXGTdAoBaaDz4=
+github.com/meilisearch/meilisearch-go v0.29.0/go.mod h1:2cRCAn4ddySUsFfNDLVPod/plRibQsJkXF/4gLhxbOk=
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
-github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
-github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
-github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
-github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
-github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
-github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
-github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
-github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
-github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
-github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
-github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
-github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
-github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
-github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
-github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
-github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-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/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=
-github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
-github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
-github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
-github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
-github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
-github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
-github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
-github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
-github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
-github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
-github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
+github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
-github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
+github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
+github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
-github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
-github.com/prometheus/client_golang v1.8.0/go.mod h1:O9VU6huf47PktckDQfMTX0Y8tY0/7TSWwj+ITvv0TnM=
-github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
-github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
-github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
-github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
-github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
-github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
-github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
-github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
-github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
-github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
-github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps=
-github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
-github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
-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.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
-github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
+github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA=
+github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/redis/rueidis v1.0.49 h1:uhjMcQ663R8st3saoo85VV9Ce37zfvRXiveZcBrS3YQ=
+github.com/redis/rueidis v1.0.49/go.mod h1:by+34b0cFXndxtYmPAHpoTHO5NkosDlBvhexoTURIxM=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
-github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
-github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
-github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
+github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0=
github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
-github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
-github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
-github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
-github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
-github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
-github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
-github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
-github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/trim21/errgo v0.0.2 h1:OlaTR0PaSzBGFgBMEhwwTtnpN2q03WNNr3YossUCHCY=
-github.com/trim21/errgo v0.0.2/go.mod h1:T9MN4yD51VA68yqmod0e6z4ZjTBd8buD0aCFjfQ+hUM=
-github.com/trim21/go-phpserialize v0.0.21 h1:/RaseAU8bDOVJRzmogTD/WNLjcqKJlGueKTs5eF1FJE=
-github.com/trim21/go-phpserialize v0.0.21/go.mod h1:vNqVtkwoZtQuK3VMD0KoytIGUEmofWvujEwxvPKjHyo=
-github.com/trim21/go-redis-prometheus v0.0.0 h1:9svVIZkKaDGE1bSRbtTQdsKBzW+QEWfwc35QIF8JsfA=
-github.com/trim21/go-redis-prometheus v0.0.0/go.mod h1:UTXPI/fofnsXF9/X/WtvwhAdbSOBVjLg17xDjQj9VgU=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/trim21/errgo v0.0.3 h1:q0cUPTs+4c5NxByA4f0HUGRvlNyBlUtdxFiTKQdTE68=
+github.com/trim21/errgo v0.0.3/go.mod h1:AH1KzogdvSkSPXbZq9QAuqSt1L1Eu5W8eYK32zPYv9s=
+github.com/trim21/go-phpserialize v0.1.0-alpha.5 h1:bMsUpfwAgPggQzDKdafNBvkPWDCMfzlvH30MWzI/SYg=
+github.com/trim21/go-phpserialize v0.1.0-alpha.5/go.mod h1:/3zMYuOzpcKOevwP3ZN0WxdVRaB3CzJh5T2i41QPgRQ=
github.com/trim21/htest v0.0.4 h1:dDIzKNdIClgtB158DlO+Xf0sfwNycmx3kfo/FJuY+eE=
github.com/trim21/htest v0.0.4/go.mod h1:W+zaYAGCBqx38eMrMGvXrALnbcXR6OBtZiRiHahgo+E=
-github.com/trim21/pkg v0.0.3 h1:uAqfoFmmYiIMOSretKj8/tvrQs3KG57020Ff0cx8UtE=
-github.com/trim21/pkg v0.0.3/go.mod h1:JrRIFidkCLeuU5j0vBP5ZN0NOp2JavagHZNr4D3AH6Q=
-github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
-github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/trim21/pkg v0.0.4 h1:0nYODKdqNUzmUaPFvqSiR420u2uXQgIYyVyiNfH7olc=
+github.com/trim21/pkg v0.0.4/go.mod h1:edl6xdqBOJrhMuIGvcY2lg5L9cqp/hVuwHRM/kdzbMg=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d h1:xS9QTPgKl9ewGsAOPc+xW7DeStJDqYPfisDmeSCcbco=
-github.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
-github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
-github.com/volatiletech/null/v9 v9.0.0 h1:JCdlHEiSRVxOi7/MABiEfdsqmuj9oTV20Ao7VvZ0JkE=
-github.com/volatiletech/null/v9 v9.0.0/go.mod h1:zRFghPVahaiIMRXiUJrc6gsoG83Cm3ZoAfSTw7VHGQc=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
-github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
-go.etcd.io/etcd/api/v3 v3.5.13 h1:8WXU2/NBge6AUF1K1gOexB6e07NgsN1hXK0rSTtgSp4=
-go.etcd.io/etcd/api/v3 v3.5.13/go.mod h1:gBqlqkcMMZMVTMm4NDZloEVJzxQOQIls8splbqBDa0c=
-go.etcd.io/etcd/client/pkg/v3 v3.5.13 h1:RVZSAnWWWiI5IrYAXjQorajncORbS0zI48LQlE2kQWg=
-go.etcd.io/etcd/client/pkg/v3 v3.5.13/go.mod h1:XxHT4u1qU12E2+po+UVPrEeL94Um6zL58ppuJWXSAB8=
-go.etcd.io/etcd/client/v3 v3.5.13 h1:o0fHTNJLeO0MyVbc7I3fsCf6nrOqn5d+diSarKnB2js=
-go.etcd.io/etcd/client/v3 v3.5.13/go.mod h1:cqiAeY8b5DEEcpxvgWKsbLIWNM/8Wy2xJSDMtioMcoI=
-go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
-go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
-go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
-go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc=
-go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
-go.uber.org/fx v1.21.1 h1:RqBh3cYdzZS0uqwVeEjOX2p73dddLpym315myy/Bpb0=
-go.uber.org/fx v1.21.1/go.mod h1:HT2M7d7RHo+ebKGh9NRcrsrHHfpZ60nW3QRubMRfv48=
+go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw=
+go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
+go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg=
+go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
-go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
-go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
-go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
-go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
-go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
-go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 h1:lGdhQUN/cnWdSH3291CUuxSEqc+AsGTiDxPP3r2J0l4=
+go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
-golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
-golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
-golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
-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=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
+golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
+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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
-golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
+golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
-golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
-golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
-golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
+golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
-golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
+golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
-golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
-golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
+golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
+golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
+golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
-golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
+golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
+golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
-google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
-google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0=
-google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
-google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
-google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
-google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA=
-google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
-google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f h1:cUMEy+8oS78BWIH9OWazBkzbr090Od9tWBNtZHkOhf0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
+google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
+google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
+google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
+google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
+google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
+google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
-gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
-gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco=
-gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04=
-gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
-gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
-gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
+gorm.io/datatypes v1.2.2 h1:sdn7ZmG4l7JWtMDUb3L98f2Ym7CO5F8mZLlrQJMfF9g=
+gorm.io/datatypes v1.2.2/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI=
+gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
+gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
@@ -711,24 +318,17 @@ gorm.io/gen v0.3.26 h1:sFf1j7vNStimPRRAtH4zz5NiHM+1dr6eA9aaRdplyhY=
gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE=
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.23.0/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
-gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
-gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
-gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
-gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
+gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gorm.io/hints v1.1.2 h1:b5j0kwk5p4+3BtDtYqqfY+ATSxjj+6ptPgVveuynn9o=
gorm.io/hints v1.1.2/go.mod h1:/ARdpUHAtyEMCh5NNi3tI7FsGh+Cj/MIUlvNxCNCFWg=
-gorm.io/plugin/dbresolver v1.5.1 h1:s9Dj9f7r+1rE3nx/Ywzc85nXptUEaeOO0pt27xdopM8=
-gorm.io/plugin/dbresolver v1.5.1/go.mod h1:l4Cn87EHLEYuqUncpEeTC2tTJQkjngPSD+lo8hIvcT0=
+gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU=
+gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
gorm.io/plugin/soft_delete v1.2.1 h1:qx9D/c4Xu6w5KT8LviX8DgLcB9hkKl6JC9f44Tj7cGU=
gorm.io/plugin/soft_delete v1.2.1/go.mod h1:Zv7vQctOJTGOsJ/bWgrN1n3od0GBAZgnLjEx+cApLGk=
-honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
-sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
-sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
diff --git a/internal/auth/mysql_repository.go b/internal/auth/mysql_repository.go
index f477fde9f..806743f75 100644
--- a/internal/auth/mysql_repository.go
+++ b/internal/auth/mysql_repository.go
@@ -16,11 +16,13 @@ package auth
import (
"context"
+ "database/sql"
"encoding/json"
"errors"
"strconv"
"time"
+ "github.com/jmoiron/sqlx"
"github.com/trim21/errgo"
"go.uber.org/zap"
"gorm.io/gorm"
@@ -32,14 +34,20 @@ import (
"github.com/bangumi/server/internal/pkg/gstr"
"github.com/bangumi/server/internal/pkg/logger"
"github.com/bangumi/server/internal/pkg/random"
+ "github.com/bangumi/server/internal/user"
)
-func NewMysqlRepo(q *query.Query, log *zap.Logger) Repo {
- return mysqlRepo{q: q, log: log.Named("auth.mysqlRepo")}
+func NewMysqlRepo(q *query.Query, log *zap.Logger, db *sqlx.DB) Repo {
+ return mysqlRepo{
+ q: q,
+ log: log.Named("auth.mysqlRepo"),
+ db: db,
+ }
}
type mysqlRepo struct {
q *query.Query
+ db *sqlx.DB
log *zap.Logger
}
@@ -50,7 +58,6 @@ func (m mysqlRepo) GetByEmail(ctx context.Context, email string) (UserInfo, []by
return UserInfo{}, nil, gerr.ErrNotFound
}
- m.log.Error("unexpected error happened", zap.Error(err))
return UserInfo{}, nil, errgo.Wrap(err, "gorm")
}
@@ -62,16 +69,17 @@ func (m mysqlRepo) GetByEmail(ctx context.Context, email string) (UserInfo, []by
}
func (m mysqlRepo) GetByToken(ctx context.Context, token string) (UserInfo, error) {
- access, err := m.q.AccessToken.WithContext(ctx).
- Where(m.q.AccessToken.AccessToken.Eq(token), m.q.AccessToken.ExpiredAt.Gte(time.Now())).
- First()
+ var access struct {
+ UserID string `db:"user_id"`
+ }
+ err := m.db.GetContext(ctx, &access,
+ `select user_id from chii_oauth_access_tokens
+ where access_token = BINARY ? and expires > ? limit 1`, token, time.Now())
if err != nil {
- if errors.Is(err, gorm.ErrRecordNotFound) {
+ if errors.Is(err, sql.ErrNoRows) {
return UserInfo{}, gerr.ErrNotFound
}
- m.log.Error("unexpected error happened", zap.Error(err))
-
return UserInfo{}, errgo.Wrap(err, "gorm")
}
@@ -81,24 +89,25 @@ func (m mysqlRepo) GetByToken(ctx context.Context, token string) (UserInfo, erro
return UserInfo{}, errgo.Wrap(err, "parsing user id")
}
- u, err := m.q.Member.WithContext(ctx).Where(m.q.Member.ID.Eq(id)).Take()
- if err != nil {
- if errors.Is(err, gorm.ErrRecordNotFound) {
- m.log.Error("can't find user of access token",
- zap.String("token", token), zap.String("uid", access.UserID))
+ var u struct {
+ Regdate int64
+ GroupID user.GroupID
+ }
+ err = m.db.QueryRowContext(ctx, `select regdate, groupid from chii_members where uid = ? limit 1`, id).
+ Scan(&u.Regdate, &u.GroupID)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
return UserInfo{}, gerr.ErrNotFound
}
- m.log.Error("unexpected error happened", zap.Error(err))
-
return UserInfo{}, errgo.Wrap(err, "gorm")
}
return UserInfo{
RegTime: time.Unix(u.Regdate, 0),
- ID: u.ID,
- GroupID: u.Groupid,
+ ID: id,
+ GroupID: u.GroupID,
}, nil
}
@@ -109,8 +118,6 @@ func (m mysqlRepo) GetPermission(ctx context.Context, groupID uint8) (Permission
m.log.Error("can't find permission for group", zap.Uint8("user_group_id", groupID))
return Permission{}, nil
}
-
- m.log.Error("unexpected error", zap.Error(err))
return Permission{}, errgo.Wrap(err, "dal")
}
@@ -159,7 +166,6 @@ func (m mysqlRepo) CreateAccessToken(
Info: infoByte,
})
if err != nil {
- m.log.Error("unexpected error happened", zap.Error(err))
return "", errgo.Wrap(err, "dal")
}
@@ -176,7 +182,6 @@ func (m mysqlRepo) ListAccessToken(ctx context.Context, userID model.UserID) ([]
Where(m.q.AccessToken.UserID.Eq(strconv.FormatUint(uint64(userID), 10)),
m.q.AccessToken.ExpiredAt.Gte(time.Now())).Find()
if err != nil {
- m.log.Error("unexpected error happened", zap.Error(err))
return nil, errgo.Wrap(err, "dal")
}
diff --git a/internal/auth/mysql_repository_test.go b/internal/auth/mysql_repository_test.go
index daa1f7a3a..0320de7ac 100644
--- a/internal/auth/mysql_repository_test.go
+++ b/internal/auth/mysql_repository_test.go
@@ -17,9 +17,12 @@ package auth_test
import (
"context"
"strconv"
+ "strings"
"testing"
"time"
+ "github.com/jmoiron/sqlx"
+ "github.com/samber/lo"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
@@ -34,7 +37,7 @@ import (
func getRepo(t *testing.T) (auth.Repo, *query.Query) {
t.Helper()
q := query.Use(test.GetGorm(t))
- repo := auth.NewMysqlRepo(q, zap.NewNop())
+ repo := auth.NewMysqlRepo(q, zap.NewNop(), sqlx.NewDb(lo.Must(q.DB().DB()), "mysql"))
return repo, q
}
@@ -61,6 +64,16 @@ func TestMysqlRepo_GetByToken(t *testing.T) {
require.EqualValues(t, 382951, u.ID)
}
+func TestMysqlRepo_GetByToken_case_sensitive(t *testing.T) {
+ test.RequireEnv(t, "mysql")
+ t.Parallel()
+
+ repo, _ := getRepo(t)
+
+ _, err := repo.GetByToken(context.Background(), strings.ToUpper("a_development_access_token"))
+ require.ErrorIs(t, err, gerr.ErrNotFound)
+}
+
func TestMysqlRepo_GetByToken_expired(t *testing.T) {
test.RequireEnv(t, "mysql")
t.Parallel()
diff --git a/internal/cachekey/cachekey.go b/internal/cachekey/cachekey.go
index 99ce587f6..524fe1ded 100644
--- a/internal/cachekey/cachekey.go
+++ b/internal/cachekey/cachekey.go
@@ -35,6 +35,14 @@ func Subject(id model.SubjectID) string {
return resPrefix + "subject:" + strconv.FormatUint(uint64(id), 10)
}
+func SubjectBrowse(s string, limit, offset int) string {
+ return resPrefix + "subject::browse:" + s + ":" + strconv.Itoa(limit) + ":" + strconv.Itoa(offset)
+}
+
+func SubjectBrowseCount(s string) string {
+ return resPrefix + "subject::browse:" + s + "::count"
+}
+
func Episode(id model.EpisodeID) string {
return resPrefix + "episode:" + strconv.FormatUint(uint64(id), 10)
}
@@ -46,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 "chii:v0:subject:meta-tags:" + strconv.FormatUint(uint64(id), 10)
+}
diff --git a/internal/character/mysql_repository.go b/internal/character/mysql_repository.go
index 47bc3640c..543615ac1 100644
--- a/internal/character/mysql_repository.go
+++ b/internal/character/mysql_repository.go
@@ -47,7 +47,6 @@ func (r mysqlRepo) Get(ctx context.Context, id model.CharacterID) (model.Charact
return model.Character{}, gerr.ErrCharacterNotFound
}
- r.log.Error("unexpected error happened", zap.Error(err))
return model.Character{}, errgo.Wrap(err, "dal")
}
@@ -60,7 +59,6 @@ func (r mysqlRepo) GetByIDs(
records, err := r.q.Character.WithContext(ctx).Preload(r.q.Character.Fields).
Where(r.q.Character.ID.In(slice.ToUint32(ids)...)).Find()
if err != nil {
- r.log.Error("unexpected error happened", zap.Error(err))
return nil, errgo.Wrap(err, "dal")
}
@@ -80,7 +78,6 @@ func (r mysqlRepo) GetPersonRelated(
Order(r.q.Cast.SubjectID).
Find()
if err != nil {
- r.log.Error("unexpected error happened", zap.Error(err))
return nil, errgo.Wrap(err, "dal")
}
@@ -103,7 +100,6 @@ func (r mysqlRepo) GetSubjectRelated(
Where(r.q.CharacterSubjects.SubjectID.Eq(subjectID)).
Order(r.q.CharacterSubjects.CharacterID).Find()
if err != nil {
- r.log.Error("unexpected error happened", zap.Error(err))
return nil, errgo.Wrap(err, "dal")
}
@@ -137,7 +133,6 @@ func (r mysqlRepo) GetSubjectRelationByIDs(
Find()
if err != nil {
- r.log.Error("unexpected error happened", zap.Error(err))
return nil, errgo.Wrap(err, "dal")
}
diff --git a/internal/collections/domain.go b/internal/collections/domain.go
index cad4d91a5..c37a327d2 100644
--- a/internal/collections/domain.go
+++ b/internal/collections/domain.go
@@ -24,7 +24,7 @@ import (
"github.com/bangumi/server/internal/pkg/null"
)
-type Repo interface {
+type Repo interface { //nolint:interfacebloat
// WithQuery is used to replace repo's query to txn
WithQuery(query *query.Query) Repo
CountSubjectCollections(
@@ -74,6 +74,34 @@ type Repo interface {
episodeIDs []model.EpisodeID, collection collection.EpisodeCollection,
at time.Time,
) (collection.UserSubjectEpisodesCollection, error)
+
+ GetPersonCollection(
+ ctx context.Context, userID model.UserID,
+ cat collection.PersonCollectCategory, targetID model.PersonID,
+ ) (collection.UserPersonCollection, error)
+
+ AddPersonCollection(
+ ctx context.Context, userID model.UserID,
+ cat collection.PersonCollectCategory, targetID model.PersonID,
+ ) error
+
+ RemovePersonCollection(
+ ctx context.Context, userID model.UserID,
+ cat collection.PersonCollectCategory, targetID model.PersonID,
+ ) error
+
+ CountPersonCollections(
+ ctx context.Context,
+ userID model.UserID,
+ cat collection.PersonCollectCategory,
+ ) (int64, error)
+
+ ListPersonCollection(
+ ctx context.Context,
+ userID model.UserID,
+ cat collection.PersonCollectCategory,
+ limit, offset int,
+ ) ([]collection.UserPersonCollection, error)
}
type Update struct {
diff --git a/internal/collections/domain/collection/model.go b/internal/collections/domain/collection/model.go
index f2f229ba4..ec1b0fb07 100644
--- a/internal/collections/domain/collection/model.go
+++ b/internal/collections/domain/collection/model.go
@@ -51,3 +51,18 @@ type UserEpisodeCollection struct {
}
type UserSubjectEpisodesCollection map[model.EpisodeID]UserEpisodeCollection
+
+type PersonCollectCategory string
+
+const (
+ PersonCollectCategoryPerson PersonCollectCategory = "prsn"
+ PersonCollectCategoryCharacter PersonCollectCategory = "crt"
+)
+
+type UserPersonCollection struct {
+ ID uint32
+ Category string
+ TargetID model.PersonID
+ UserID model.UserID
+ CreatedAt time.Time
+}
diff --git a/internal/collections/domain/collection/subject.go b/internal/collections/domain/collection/subject.go
index 8fc8227ae..3534aca95 100644
--- a/internal/collections/domain/collection/subject.go
+++ b/internal/collections/domain/collection/subject.go
@@ -19,6 +19,7 @@ import (
"github.com/samber/lo"
"github.com/trim21/errgo"
+ "golang.org/x/text/unicode/norm"
"github.com/bangumi/server/domain/gerr"
"github.com/bangumi/server/internal/model"
@@ -118,7 +119,7 @@ func (s *Subject) UpdateComment(comment string) error {
return gerr.ErrInvisibleChar
}
- s.comment = comment
+ s.comment = norm.NFKC.String(comment)
return nil
}
@@ -135,6 +136,10 @@ func (s *Subject) UpdateTags(tags []string) error {
return gerr.ErrInvisibleChar
}
+ for i, tag := range tags {
+ tags[i] = norm.NFKC.String(tag)
+ }
+
s.tags = lo.Uniq(tags)
return nil
diff --git a/internal/collections/infra/mysql_repo.go b/internal/collections/infra/mysql_repo.go
index f2eb6c362..204920c42 100644
--- a/internal/collections/infra/mysql_repo.go
+++ b/internal/collections/infra/mysql_repo.go
@@ -15,9 +15,11 @@
package infra
import (
+ "cmp"
"context"
"errors"
"fmt"
+ "slices"
"sort"
"strings"
"time"
@@ -29,6 +31,7 @@ import (
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/gorm"
+ "gorm.io/gorm/clause"
"github.com/bangumi/server/dal/dao"
"github.com/bangumi/server/dal/query"
@@ -37,6 +40,7 @@ import (
"github.com/bangumi/server/internal/collections"
"github.com/bangumi/server/internal/collections/domain/collection"
"github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/pkg/dam"
"github.com/bangumi/server/internal/pkg/gstr"
"github.com/bangumi/server/internal/subject"
)
@@ -128,6 +132,7 @@ func (r mysqlRepo) DeleteSubjectCollection(
return err
}
+//nolint:funlen
func (r mysqlRepo) updateOrCreateSubjectCollection(
ctx context.Context,
userID model.UserID,
@@ -152,31 +157,230 @@ func (r mysqlRepo) updateOrCreateSubjectCollection(
original := *collectionSubject
+ relatedTags := slices.Clone(original.Tags())
+ slices.Sort(relatedTags)
+
// Update subject collection
s, err := update(ctx, collectionSubject)
if err != nil {
return errgo.Trace(err)
}
+ originalCollection := *obj
+
if err = r.updateSubjectCollection(obj, &original, s, at, ip, created); err != nil {
return errgo.Trace(err)
}
- T := r.q.SubjectCollection
- if created {
- err = errgo.Trace(T.WithContext(ctx).Create(obj))
- } else {
- err = errgo.Trace(T.WithContext(ctx).Save(obj))
- }
+ newTags := slices.Clone(s.Tags())
+ slices.Sort(newTags)
+
+ err = r.q.Transaction(func(tx *query.Query) error {
+ sanitizedTags, txErr := r.updateUserTags(ctx, tx, userID, subject, at, s)
+ if txErr != nil {
+ return errgo.Trace(txErr)
+ }
+
+ sort.Strings(sanitizedTags)
+
+ obj.Tag = strings.Join(sanitizedTags, " ")
+ return errgo.Trace(r.q.SubjectCollection.WithContext(ctx).Clauses(clause.OnConflict{UpdateAll: true}).Create(obj))
+ })
if err != nil {
return err
}
+ if slices.Equal(relatedTags, newTags) {
+ relatedTags = nil
+ } else {
+ relatedTags = lo.Uniq(append(relatedTags, newTags...))
+ }
+
+ if len(relatedTags) != 0 {
+ err = r.q.Transaction(func(tx *query.Query) error {
+ return r.reCountSubjectTags(ctx, tx, subject, relatedTags)
+ })
+ if err != nil {
+ return errgo.Trace(err)
+ }
+ }
+
r.updateSubject(ctx, subject.ID)
+
+ if obj.Rate != originalCollection.Rate {
+ if err := r.reCountSubjectRate(ctx, subject.ID, originalCollection.Rate, obj.Rate); err != nil {
+ r.log.Error("failed to update collection counts", zap.Error(err), zap.Uint32("subject_id", subject.ID))
+ }
+ }
+
return nil
}
+//nolint:funlen
+func (r mysqlRepo) updateUserTags(ctx context.Context,
+ q *query.Query,
+ userID model.UserID, subject model.Subject,
+ at time.Time, s *collection.Subject) ([]string, error) {
+ tx := q.WithContext(ctx)
+
+ if (len(s.Tags())) == 0 {
+ _, err := tx.TagList.Where(q.TagList.UID.Eq(userID),
+ q.TagList.Mid.Eq(subject.ID),
+ q.TagList.Cat.Eq(model.TagCatSubject)).Delete()
+ return nil, errgo.Trace(err)
+ }
+
+ tags, err := tx.TagIndex.Select().
+ Where(q.TagIndex.Name.In(s.Tags()...), q.TagIndex.Cat.Eq(model.TagCatSubject),
+ q.TagIndex.Type.Eq(subject.TypeID)).Find()
+ if err != nil {
+ return nil, errgo.Trace(err)
+ }
+
+ var existsTags = lo.SliceToMap(tags, func(item *dao.TagIndex) (string, bool) {
+ return strings.ToLower(item.Name), true
+ })
+
+ var missingTags []string
+ for _, tag := range s.Tags() {
+ if !existsTags[strings.ToLower(tag)] {
+ missingTags = append(missingTags, tag)
+ }
+ }
+
+ if len(missingTags) > 0 {
+ r.log.Info("create missing tags", zap.Strings("missing_tags", missingTags))
+ err = tx.TagIndex.Create(lo.Map(missingTags, func(item string, index int) *dao.TagIndex {
+ return &dao.TagIndex{
+ Name: item,
+ Cat: model.TagCatSubject,
+ Type: subject.TypeID,
+ Results: 1,
+ CreatedTime: uint32(at.Unix()),
+ UpdatedTime: uint32(at.Unix()),
+ }
+ })...)
+
+ if err != nil {
+ return nil, errgo.Trace(err)
+ }
+ }
+
+ tags, err = tx.TagIndex.Select().
+ Where(q.TagIndex.Name.In(s.Tags()...), q.TagIndex.Cat.Eq(model.TagCatSubject),
+ q.TagIndex.Type.Eq(subject.TypeID)).Find()
+ if err != nil {
+ return nil, errgo.Trace(err)
+ }
+
+ err = tx.TagList.Clauses(clause.OnConflict{DoNothing: true}).
+ Create(lo.Map(tags, func(item *dao.TagIndex, index int) *dao.TagList {
+ return &dao.TagList{
+ Tid: item.ID,
+ UID: s.User(),
+ Cat: model.TagCatSubject,
+ Type: subject.TypeID,
+ Mid: subject.ID,
+ CreatedTime: uint32(at.Unix()),
+ }
+ })...)
+ if err != nil {
+ return nil, errgo.Trace(err)
+ }
+
+ return lo.Map(tags, func(item *dao.TagIndex, index int) string {
+ return item.Name
+ }), nil
+}
+
+//nolint:funlen
+func (r mysqlRepo) reCountSubjectTags(ctx context.Context, tx *query.Query,
+ s model.Subject, relatedTags []string) error {
+ tagIndexs, err := tx.WithContext(ctx).TagIndex.Select().
+ Where(
+ tx.TagIndex.Cat.Eq(model.TagCatSubject),
+ tx.TagIndex.Name.In(relatedTags...),
+ tx.TagIndex.Type.Eq(s.TypeID),
+ ).Order(tx.TagIndex.ID.Asc()).Find()
+ if err != nil {
+ return err
+ }
+
+ // tag in (...) 操作不区分大小写,使用第一次出现的 tag
+ // 比如 {name="Galgame"} {name="galgame"} 在过滤后会只保留 {name="Galgame"}
+ seen := make(map[string]bool)
+ tagIndexs = lo.Filter(tagIndexs, func(item *dao.TagIndex, index int) bool {
+ n := strings.ToLower(item.Name)
+ if seen[n] {
+ return false
+ }
+ seen[n] = true
+ return true
+ })
+
+ db := tx.DB().WithContext(ctx)
+
+ err = db.Exec(`
+ update chii_tag_neue_index as ti
+ set tag_results = (
+ select count(1)
+ from chii_tag_neue_list as tl
+ where tl.tlt_cat = ti.tag_cat AND tl.tlt_tid = ti.tag_id and tl.tlt_type = ti.tag_type
+ )
+ where ti.tag_cat = ? AND ti.tag_type = ? AND ti.tag_id IN ?
+ `, model.TagCatSubject, s.TypeID,
+ lo.Uniq(lo.Map(tagIndexs, func(item *dao.TagIndex, index int) uint32 {
+ return item.ID
+ })),
+ ).Error
+ if err != nil {
+ return errgo.Trace(err)
+ }
+
+ tagList, err := tx.WithContext(ctx).TagList.Preload(tx.TagList.Tag).
+ Where(tx.TagList.Cat.Eq(model.TagCatSubject), tx.TagList.Mid.Eq(s.ID)).Find()
+ if err != nil {
+ return errgo.Trace(err)
+ }
+
+ var count = make(map[string]uint)
+ var countMap = make(map[string]uint)
+
+ for _, tag := range tagList {
+ if !dam.ValidateTag(tag.Tag.Name) {
+ continue
+ }
+
+ count[tag.Tag.Name]++
+ countMap[tag.Tag.Name] = uint(tag.Tag.Results)
+ }
+
+ var phpTags = make([]subject.Tag, 0, len(count))
+
+ for name, c := range count {
+ phpTags = append(phpTags, subject.Tag{
+ Name: lo.ToPtr(name),
+ Count: c,
+ TotalCount: countMap[name],
+ })
+ }
+
+ slices.SortFunc(phpTags, func(a, b subject.Tag) int {
+ return -cmp.Compare(a.Count, b.Count)
+ })
+
+ newTag, err := phpserialize.Marshal(lo.Slice(phpTags, 0, 30)) //nolint:mnd
+ if err != nil {
+ return errgo.Wrap(err, "php.Marshal")
+ }
+
+ _, err = tx.WithContext(ctx).SubjectField.Where(r.q.SubjectField.Sid.Eq(s.ID)).
+ UpdateSimple(r.q.SubjectField.Tags.Value(newTag))
+
+ return errgo.Wrap(err, "failed to update subject field")
+}
+
// 根据新旧 collection.Subject 状态
// 更新 dao.SubjectCollection.
func (r mysqlRepo) updateSubjectCollection(
@@ -274,7 +478,6 @@ func (r mysqlRepo) ListSubjectCollection(
collections, err := q.Find()
if err != nil {
- r.log.Error("unexpected error happened", zap.Error(err))
return nil, errgo.Wrap(err, "dal")
}
@@ -350,12 +553,8 @@ func (r mysqlRepo) GetSubjectEpisodesCollection(
}
func (r mysqlRepo) updateSubject(ctx context.Context, subjectID model.SubjectID) {
- if err := r.updateSubjectTags(ctx, subjectID); err != nil {
- r.log.Error("failed to update subject tags", zap.Error(err))
- }
-
if err := r.reCountSubjectCollection(ctx, subjectID); err != nil {
- r.log.Error("failed to update collection counts", zap.Error(err))
+ r.log.Error("failed to update collection counts", zap.Error(err), zap.Uint32("subject_id", subjectID))
}
}
@@ -365,90 +564,103 @@ func (r mysqlRepo) reCountSubjectCollection(ctx context.Context, subjectID model
Total uint32 `gorm:"total"`
}
- err := r.q.SubjectCollection.WithContext(ctx).
- Select(r.q.SubjectCollection.Type.As("type"), r.q.SubjectCollection.Type.Count().As("total")).
- Group(r.q.SubjectCollection.Type).
- Where(r.q.SubjectCollection.SubjectID.Eq(subjectID)).Group(r.q.SubjectCollection.Type).Scan(&counts)
- if err != nil {
- return errgo.Wrap(err, "dal")
- }
+ return r.q.Transaction(func(tx *query.Query) error {
+ err := tx.DB().WithContext(ctx).Raw(`
+ select interest_type as type, count(interest_type) as total from chii_subject_interests
+ where interest_subject_id = ?
+ group by interest_type
+ `, subjectID).Scan(&counts).Error
+ if err != nil {
+ return errgo.Wrap(err, "dal")
+ }
- var updater = make([]field.AssignExpr, 0, 5)
+ var updater = make([]field.AssignExpr, 0, 5)
+ for _, count := range counts {
+ switch collection.SubjectCollection(count.Type) {
+ case collection.SubjectCollectionAll:
+ continue
- for _, count := range counts {
- switch collection.SubjectCollection(count.Type) { //nolint:exhaustive
- case collection.SubjectCollectionDropped:
- updater = append(updater, r.q.Subject.Dropped.Value(count.Total))
+ case collection.SubjectCollectionDropped:
+ updater = append(updater, r.q.Subject.Dropped.Value(count.Total))
- case collection.SubjectCollectionWish:
- updater = append(updater, r.q.Subject.Wish.Value(count.Total))
+ case collection.SubjectCollectionWish:
+ updater = append(updater, r.q.Subject.Wish.Value(count.Total))
- case collection.SubjectCollectionDoing:
- updater = append(updater, r.q.Subject.Doing.Value(count.Total))
+ case collection.SubjectCollectionDoing:
+ updater = append(updater, r.q.Subject.Doing.Value(count.Total))
- case collection.SubjectCollectionOnHold:
- updater = append(updater, r.q.Subject.OnHold.Value(count.Total))
+ case collection.SubjectCollectionOnHold:
+ updater = append(updater, r.q.Subject.OnHold.Value(count.Total))
- case collection.SubjectCollectionDone:
- updater = append(updater, r.q.Subject.Done.Value(count.Total))
+ case collection.SubjectCollectionDone:
+ updater = append(updater, r.q.Subject.Done.Value(count.Total))
+ }
}
- }
- _, err = r.q.Subject.WithContext(ctx).Where(r.q.Subject.ID.Eq(subjectID)).UpdateSimple(updater...)
- if err != nil {
- return errgo.Wrap(err, "dal")
- }
+ _, err = tx.Subject.WithContext(ctx).Where(r.q.Subject.ID.Eq(subjectID)).UpdateSimple(updater...)
+ if err != nil {
+ return errgo.Wrap(err, "dal")
+ }
- return nil
+ return nil
+ })
}
-func (r mysqlRepo) updateSubjectTags(ctx context.Context, subjectID model.SubjectID) error {
- collections, err := r.q.SubjectCollection.WithContext(ctx).
- Where(
- r.q.SubjectCollection.SubjectID.Eq(subjectID),
- r.q.SubjectCollection.Private.Neq(uint8(collection.CollectPrivacyBan)),
- ).Find()
- if err != nil {
- return errgo.Wrap(err, "failed to get all collection")
- }
+//nolint:mnd,gocyclo
+func (r mysqlRepo) reCountSubjectRate(ctx context.Context, subjectID model.SubjectID, before uint8, after uint8) error {
+ return r.q.Transaction(func(tx *query.Query) error {
+ var counts = make(map[uint8]uint32, 2)
+
+ for _, rate := range []uint8{before, after} {
+ var count uint32
+ if rate != 0 {
+ err := tx.DB().WithContext(ctx).Raw(`
+ select count(*) from chii_subject_interests
+ where interest_subject_id = ? and interest_private = 0 and interest_rate = ?
+ `, subjectID, rate).Scan(&count).Error
+ if err != nil {
+ return errgo.Wrap(err, "dal")
+ }
+
+ counts[rate] = count
+ }
+ }
- var tags = make(map[string]int)
- for _, collection := range collections {
- for _, s := range strings.Split(collection.Tag, " ") {
- if s == "" {
+ var updater = make([]field.AssignExpr, 0, 2)
+ for rate, total := range counts {
+ switch rate {
+ case 0:
continue
+ case 1:
+ updater = append(updater, tx.SubjectField.Rate1.Value(total))
+ case 2:
+ updater = append(updater, tx.SubjectField.Rate2.Value(total))
+ case 3:
+ updater = append(updater, tx.SubjectField.Rate3.Value(total))
+ case 4:
+ updater = append(updater, tx.SubjectField.Rate4.Value(total))
+ case 5:
+ updater = append(updater, tx.SubjectField.Rate5.Value(total))
+ case 6:
+ updater = append(updater, tx.SubjectField.Rate6.Value(total))
+ case 7:
+ updater = append(updater, tx.SubjectField.Rate7.Value(total))
+ case 8:
+ updater = append(updater, tx.SubjectField.Rate8.Value(total))
+ case 9:
+ updater = append(updater, tx.SubjectField.Rate9.Value(total))
+ case 10:
+ updater = append(updater, tx.SubjectField.Rate10.Value(total))
}
- tags[s]++
}
- }
-
- var phpTags = make([]subject.Tag, 0, len(tags))
-
- for name, count := range tags {
- name := name
- phpTags = append(phpTags, subject.Tag{
- Name: &name,
- Count: count,
- })
- }
- sort.Slice(phpTags, func(i, j int) bool {
- if phpTags[i].Count != phpTags[j].Count {
- return phpTags[i].Count > phpTags[j].Count
+ _, err := tx.SubjectField.WithContext(ctx).Where(r.q.SubjectField.Sid.Eq(subjectID)).UpdateSimple(updater...)
+ if err != nil {
+ return errgo.Wrap(err, "dal")
}
- return *phpTags[i].Name > *phpTags[j].Name
+ return nil
})
-
- newTag, err := phpserialize.Marshal(lo.Slice(phpTags, 0, 30)) //nolint:gomnd
- if err != nil {
- return errgo.Wrap(err, "php.Marshal")
- }
-
- _, err = r.q.SubjectField.WithContext(ctx).Where(r.q.SubjectField.Sid.Eq(subjectID)).
- UpdateSimple(r.q.SubjectField.Tags.Value(newTag))
-
- return errgo.Wrap(err, "failed to update subject field")
}
func (r mysqlRepo) updateCollectionTime(obj *dao.SubjectCollection,
@@ -472,6 +684,148 @@ func (r mysqlRepo) updateCollectionTime(obj *dao.SubjectCollection,
return nil
}
+func (r mysqlRepo) GetPersonCollection(
+ ctx context.Context, userID model.UserID,
+ cat collection.PersonCollectCategory, targetID model.PersonID,
+) (collection.UserPersonCollection, error) {
+ c, err := r.q.PersonCollect.WithContext(ctx).
+ Where(r.q.PersonCollect.UserID.Eq(userID), r.q.PersonCollect.Category.Eq(string(cat)),
+ r.q.PersonCollect.TargetID.Eq(targetID)).Take()
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return collection.UserPersonCollection{}, gerr.ErrNotFound
+ }
+ return collection.UserPersonCollection{}, errgo.Wrap(err, "dal")
+ }
+
+ return collection.UserPersonCollection{
+ ID: c.ID,
+ Category: c.Category,
+ TargetID: c.TargetID,
+ UserID: c.UserID,
+ CreatedAt: time.Unix(int64(c.CreatedTime), 0),
+ }, nil
+}
+
+func (r mysqlRepo) AddPersonCollection(
+ ctx context.Context, userID model.UserID,
+ cat collection.PersonCollectCategory, targetID model.PersonID,
+) error {
+ collect := &dao.PersonCollect{
+ UserID: userID,
+ Category: string(cat),
+ TargetID: targetID,
+ CreatedTime: uint32(time.Now().Unix()),
+ }
+ err := r.q.Transaction(func(tx *query.Query) error {
+ switch cat {
+ case collection.PersonCollectCategoryCharacter:
+ if _, err := tx.Character.WithContext(ctx).Where(
+ tx.Character.ID.Eq(targetID)).UpdateSimple(tx.Character.Collects.Add(1)); err != nil {
+ r.log.Error("failed to update character collects", zap.Error(err))
+ return err
+ }
+ case collection.PersonCollectCategoryPerson:
+ if _, err := tx.Person.WithContext(ctx).Where(
+ tx.Person.ID.Eq(targetID)).UpdateSimple(tx.Person.Collects.Add(1)); err != nil {
+ r.log.Error("failed to update person collects", zap.Error(err))
+ return err
+ }
+ }
+ if err := tx.PersonCollect.WithContext(ctx).Create(collect); err != nil {
+ r.log.Error("failed to create person collection record", zap.Error(err))
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ return errgo.Wrap(err, "dal")
+ }
+ return nil
+}
+
+func (r mysqlRepo) RemovePersonCollection(
+ ctx context.Context, userID model.UserID,
+ cat collection.PersonCollectCategory, targetID model.PersonID,
+) error {
+ err := r.q.Transaction(func(tx *query.Query) error {
+ switch cat {
+ case collection.PersonCollectCategoryCharacter:
+ if _, err := tx.Character.WithContext(ctx).Where(
+ tx.Character.ID.Eq(targetID)).UpdateSimple(tx.Character.Collects.Sub(1)); err != nil {
+ r.log.Error("failed to update character collects", zap.Error(err))
+ return err
+ }
+ case collection.PersonCollectCategoryPerson:
+ if _, err := tx.Person.WithContext(ctx).Where(
+ tx.Person.ID.Eq(targetID)).UpdateSimple(tx.Person.Collects.Sub(1)); err != nil {
+ r.log.Error("failed to update person collects", zap.Error(err))
+ return err
+ }
+ }
+ _, err := tx.PersonCollect.WithContext(ctx).Where(
+ tx.PersonCollect.UserID.Eq(userID),
+ tx.PersonCollect.Category.Eq(string(cat)),
+ tx.PersonCollect.TargetID.Eq(targetID),
+ ).Delete()
+ if err != nil {
+ r.log.Error("failed to delete person collection record", zap.Error(err))
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ return errgo.Wrap(err, "dal")
+ }
+
+ return nil
+}
+
+func (r mysqlRepo) CountPersonCollections(
+ ctx context.Context,
+ userID model.UserID,
+ cat collection.PersonCollectCategory,
+) (int64, error) {
+ q := r.q.PersonCollect.WithContext(ctx).
+ Where(r.q.PersonCollect.UserID.Eq(userID), r.q.PersonCollect.Category.Eq(string(cat)))
+
+ c, err := q.Count()
+ if err != nil {
+ return 0, errgo.Wrap(err, "dal")
+ }
+
+ return c, nil
+}
+
+func (r mysqlRepo) ListPersonCollection(
+ ctx context.Context,
+ userID model.UserID,
+ cat collection.PersonCollectCategory,
+ limit, offset int,
+) ([]collection.UserPersonCollection, error) {
+ q := r.q.PersonCollect.WithContext(ctx).
+ Order(r.q.PersonCollect.CreatedTime.Desc()).
+ Where(r.q.PersonCollect.UserID.Eq(userID), r.q.PersonCollect.Category.Eq(string(cat))).Limit(limit).Offset(offset)
+
+ collections, err := q.Find()
+ if err != nil {
+ return nil, errgo.Wrap(err, "dal")
+ }
+
+ var results = make([]collection.UserPersonCollection, len(collections))
+ for i, c := range collections {
+ results[i] = collection.UserPersonCollection{
+ ID: c.ID,
+ Category: c.Category,
+ TargetID: c.TargetID,
+ UserID: c.UserID,
+ CreatedAt: time.Unix(int64(c.CreatedTime), 0),
+ }
+ }
+
+ return results, nil
+}
+
func (r mysqlRepo) UpdateEpisodeCollection(
ctx context.Context,
userID model.UserID,
@@ -535,7 +889,8 @@ func (r mysqlRepo) createEpisodeCollection(
}
table := r.q.EpCollection
- err = table.WithContext(ctx).Where(table.UserID.Eq(userID), table.SubjectID.Eq(subjectID)).Create(&dao.EpCollection{
+ err = table.WithContext(ctx).Clauses(clause.OnConflict{DoNothing: true}).
+ Where(table.UserID.Eq(userID), table.SubjectID.Eq(subjectID)).Create(&dao.EpCollection{
UserID: userID,
SubjectID: subjectID,
Status: bytes,
diff --git a/internal/collections/infra/mysql_repo_compat.go b/internal/collections/infra/mysql_repo_compat.go
index bd95aba89..c4c5435d1 100644
--- a/internal/collections/infra/mysql_repo_compat.go
+++ b/internal/collections/infra/mysql_repo_compat.go
@@ -33,8 +33,10 @@ type mysqlEpCollection map[model.EpisodeID]mysqlEpCollectionItem
func deserializePhpEpStatus(phpSerialized []byte) (mysqlEpCollection, error) {
var e map[model.EpisodeID]mysqlEpCollectionItem
- if err := phpserialize.Unmarshal(phpSerialized, &e); err != nil {
- return nil, errgo.Wrap(err, "php deserialize")
+ if len(phpSerialized) != 0 {
+ if err := phpserialize.Unmarshal(phpSerialized, &e); err != nil {
+ return nil, errgo.Wrap(err, "php deserialize")
+ }
}
return e, nil
diff --git a/internal/collections/infra/mysql_repo_test.go b/internal/collections/infra/mysql_repo_test.go
index 57887082c..6a1c42950 100644
--- a/internal/collections/infra/mysql_repo_test.go
+++ b/internal/collections/infra/mysql_repo_test.go
@@ -19,11 +19,13 @@ import (
"testing"
"time"
+ "github.com/samber/lo"
"github.com/stretchr/testify/require"
"github.com/trim21/go-phpserialize"
"go.uber.org/zap"
"gorm.io/gen/field"
"gorm.io/gorm"
+ "gorm.io/gorm/clause"
"github.com/bangumi/server/dal/dao"
"github.com/bangumi/server/dal/query"
@@ -32,11 +34,13 @@ import (
"github.com/bangumi/server/internal/collections/infra"
"github.com/bangumi/server/internal/model"
"github.com/bangumi/server/internal/pkg/test"
+ subject2 "github.com/bangumi/server/internal/subject"
)
func getRepo(t *testing.T) (collections.Repo, *query.Query) {
t.Helper()
q := test.GetQuery(t)
+
repo, err := infra.NewMysqlRepo(q, zap.NewNop())
require.NoError(t, err)
@@ -139,12 +143,12 @@ func TestMysqlRepo_ListSubjectCollection(t *testing.T) {
require.NoError(t, err)
}
- for i := 0; i < 2; i++ {
+ for i := uint32(0); i < 2; i++ {
err = q.SubjectCollection.
WithContext(context.Background()).
Create(&dao.SubjectCollection{
UserID: uid,
- SubjectID: model.SubjectID(200 + i),
+ SubjectID: 200 + i,
SubjectType: model.SubjectTypeGame,
UpdatedTime: uint32(time.Now().Unix()),
})
@@ -209,12 +213,24 @@ func TestMysqlRepo_UpdateOrCreateSubjectCollection(t *testing.T) {
repo, q := getRepo(t)
table := q.SubjectCollection
+ err := q.Subject.WithContext(context.TODO()).Clauses(clause.OnConflict{DoNothing: true}).
+ Where(q.Subject.ID.Eq(sid)).Create(&dao.Subject{ID: sid})
+ require.NoError(t, err)
+
+ err = q.SubjectField.WithContext(context.TODO()).Clauses(clause.OnConflict{DoNothing: true}).
+ Where(q.Subject.ID.Eq(sid)).Create(&dao.SubjectField{Sid: sid, Tags: []byte("")})
+ require.NoError(t, err)
+
+ t.Cleanup(func() {
+ lo.Must(q.Subject.WithContext(context.TODO()).Where(q.Subject.ID.Eq(sid)).Delete())
+ lo.Must(q.SubjectField.WithContext(context.TODO()).Where(q.SubjectField.Sid.Eq(sid)).Delete())
+ })
+
test.RunAndCleanup(t, func() {
- _, err := table.WithContext(context.TODO()).Where(field.Or(table.SubjectID.Eq(sid), table.UserID.Eq(uid))).Delete()
- require.NoError(t, err)
+ lo.Must(table.WithContext(context.TODO()).Where(field.Or(table.SubjectID.Eq(sid), table.UserID.Eq(uid))).Delete())
})
- err := table.WithContext(context.Background()).Create(
+ err = table.WithContext(context.Background()).Create(
&dao.SubjectCollection{
UserID: uid, SubjectID: sid + 1, Rate: 8, Type: uint8(collection.SubjectCollectionDoing),
},
@@ -247,6 +263,7 @@ func TestMysqlRepo_UpdateOrCreateSubjectCollection(t *testing.T) {
s.UpdateType(collection.SubjectCollectionDropped)
require.NoError(t, s.UpdateComment("c"))
require.NoError(t, s.UpdateRate(1))
+ require.NoError(t, s.UpdateTags([]string{"1", "2", "3"}))
return s, nil
})
require.NoError(t, err)
@@ -287,6 +304,14 @@ func TestMysqlRepo_UpdateOrCreateSubjectCollection(t *testing.T) {
require.NoError(t, err)
require.EqualValues(t, 8, r.Rate)
+
+ s, err := q.WithContext(context.Background()).Subject.Preload(q.Subject.Fields).Where(q.Subject.ID.Eq(sid)).First()
+ require.NoError(t, err)
+
+ tags, err := subject2.ParseTags(s.Fields.Tags)
+ require.NoError(t, err)
+
+ require.Len(t, tags, 3)
}
func TestMysqlRepo_UpdateSubjectCollection(t *testing.T) {
@@ -540,3 +565,197 @@ func TestMysqlRepo_UpdateEpisodeCollection_create_ep_status(t *testing.T) {
require.Contains(t, m, uint32(2))
require.EqualValues(t, collection.EpisodeCollectionDone, m[2].Type)
}
+
+func TestMysqlRepo_GetPersonCollect(t *testing.T) {
+ test.RequireEnv(t, test.EnvMysql)
+ t.Parallel()
+
+ const uid model.UserID = 39000
+ const cat = "prsn"
+ const mid model.PersonID = 12000
+
+ repo, q := getRepo(t)
+ test.RunAndCleanup(t, func() {
+ _, err := q.PersonCollect.WithContext(context.TODO()).Where(q.PersonCollect.UserID.Eq(uid)).Delete()
+ require.NoError(t, err)
+ })
+
+ err := q.PersonCollect.WithContext(context.Background()).Create(&dao.PersonCollect{
+ UserID: uid,
+ Category: cat,
+ TargetID: mid,
+ CreatedTime: uint32(time.Now().Unix()),
+ })
+ require.NoError(t, err)
+
+ r, err := repo.GetPersonCollection(context.Background(), uid, cat, mid)
+ require.NoError(t, err)
+ require.Equal(t, uid, r.UserID)
+ require.Equal(t, mid, r.TargetID)
+ require.Equal(t, cat, r.Category)
+}
+
+func TestMysqlRepo_AddPersonCollect(t *testing.T) {
+ test.RequireEnv(t, test.EnvMysql)
+ t.Parallel()
+
+ const uid model.UserID = 40000
+ const cat = "prsn"
+ const mid model.PersonID = 13000
+ const collects uint32 = 10
+
+ repo, q := getRepo(t)
+ table := q.PersonCollect
+ test.RunAndCleanup(t, func() {
+ _, err := table.WithContext(context.TODO()).Where(table.UserID.Eq(uid)).Delete()
+ require.NoError(t, err)
+ _, err = q.Person.WithContext(context.TODO()).Where(q.Person.ID.Eq(mid)).Delete()
+ require.NoError(t, err)
+ })
+
+ err := q.Person.WithContext(context.Background()).Create(&dao.Person{
+ ID: mid,
+ Collects: collects,
+ })
+ require.NoError(t, err)
+
+ err = repo.AddPersonCollection(context.Background(), uid, cat, mid)
+ require.NoError(t, err)
+
+ r, err := table.WithContext(context.TODO()).Where(table.UserID.Eq(uid)).Take()
+ require.NoError(t, err)
+ require.NotZero(t, r.ID)
+
+ p, err := q.Person.WithContext(context.Background()).Where(q.Person.ID.Eq(mid)).Take()
+ require.NoError(t, err)
+ require.Equal(t, collects+1, p.Collects)
+}
+
+func TestMysqlRepo_RemovePersonCollect(t *testing.T) {
+ test.RequireEnv(t, test.EnvMysql)
+ t.Parallel()
+
+ const uid model.UserID = 41000
+ const cat = "prsn"
+ const mid model.PersonID = 14000
+ const collects uint32 = 10
+
+ repo, q := getRepo(t)
+ test.RunAndCleanup(t, func() {
+ _, err := q.PersonCollect.WithContext(context.TODO()).Where(q.PersonCollect.UserID.Eq(uid)).Delete()
+ require.NoError(t, err)
+ _, err = q.Person.WithContext(context.TODO()).Where(q.Person.ID.Eq(mid)).Delete()
+ require.NoError(t, err)
+ })
+
+ err := q.Person.WithContext(context.Background()).Create(&dao.Person{
+ ID: mid,
+ Collects: collects,
+ })
+ require.NoError(t, err)
+ err = q.PersonCollect.WithContext(context.Background()).Create(&dao.PersonCollect{
+ UserID: uid,
+ Category: cat,
+ TargetID: mid,
+ CreatedTime: uint32(time.Now().Unix()),
+ })
+ require.NoError(t, err)
+
+ r, err := q.PersonCollect.WithContext(context.TODO()).Where(q.PersonCollect.UserID.Eq(uid)).Take()
+ require.NoError(t, err)
+ require.NotZero(t, r.ID)
+
+ err = repo.RemovePersonCollection(context.Background(), uid, cat, mid)
+ require.NoError(t, err)
+
+ _, err = q.PersonCollect.WithContext(context.TODO()).Where(q.PersonCollect.UserID.Eq(uid)).Take()
+ require.ErrorIs(t, err, gorm.ErrRecordNotFound)
+
+ p, err := q.Person.WithContext(context.Background()).Where(q.Person.ID.Eq(mid)).Take()
+ require.NoError(t, err)
+ require.Equal(t, collects-1, p.Collects)
+}
+
+func TestMysqlRepo_CountPersonCollections(t *testing.T) {
+ t.Parallel()
+ test.RequireEnv(t, test.EnvMysql)
+
+ const uid model.UserID = 42000
+ const cat = "prsn"
+
+ repo, q := getRepo(t)
+ test.RunAndCleanup(t, func() {
+ _, err := q.PersonCollect.
+ WithContext(context.Background()).
+ Where(q.PersonCollect.UserID.Eq(uid)).
+ Delete()
+ require.NoError(t, err)
+ })
+
+ for i := 0; i < 5; i++ {
+ err := q.PersonCollect.
+ WithContext(context.Background()).
+ Create(&dao.PersonCollect{
+ UserID: uid,
+ TargetID: model.PersonID(i + 100),
+ Category: cat,
+ CreatedTime: uint32(time.Now().Unix()),
+ })
+ require.NoError(t, err)
+ }
+
+ count, err := repo.CountPersonCollections(context.Background(), uid, cat)
+ require.NoError(t, err)
+ require.EqualValues(t, 5, count)
+}
+
+func TestMysqlRepo_ListPersonCollection(t *testing.T) {
+ t.Parallel()
+ test.RequireEnv(t, test.EnvMysql)
+
+ const uid model.UserID = 43000
+ const cat = "prsn"
+
+ repo, q := getRepo(t)
+
+ var err error
+ test.RunAndCleanup(t, func() {
+ _, err = q.PersonCollect.
+ WithContext(context.Background()).
+ Where(q.PersonCollect.UserID.Eq(uid)).
+ Delete()
+ require.NoError(t, err)
+ })
+
+ data, err := repo.ListPersonCollection(context.Background(), uid, collection.PersonCollectCategory(cat), 5, 0)
+ require.NoError(t, err)
+ require.Len(t, data, 0)
+
+ for i := 0; i < 5; i++ {
+ err = q.PersonCollect.
+ WithContext(context.Background()).
+ Create(&dao.PersonCollect{
+ UserID: uid,
+ TargetID: model.PersonID(i + 100),
+ Category: cat,
+ CreatedTime: uint32(time.Now().Unix()),
+ })
+ require.NoError(t, err)
+ }
+
+ for i := 0; i < 2; i++ {
+ err = q.PersonCollect.
+ WithContext(context.Background()).
+ Create(&dao.PersonCollect{
+ UserID: uid,
+ TargetID: model.PersonID(i + 200),
+ Category: cat,
+ CreatedTime: uint32(time.Now().Unix()),
+ })
+ require.NoError(t, err)
+ }
+
+ data, err = repo.ListPersonCollection(context.Background(), uid, collection.PersonCollectCategory(cat), 5, 0)
+ require.NoError(t, err)
+ require.Len(t, data, 5)
+}
diff --git a/internal/index/mysql_repository_test.go b/internal/index/mysql_repository_test.go
index 2aef0a569..d57e2b288 100644
--- a/internal/index/mysql_repository_test.go
+++ b/internal/index/mysql_repository_test.go
@@ -189,9 +189,8 @@ func TestMysqlRepo_DeleteIndex2(t *testing.T) {
err := repo.New(ctx, index)
require.NoError(t, err)
- for i := 10; i < 20; i++ {
- _, err = repo.AddOrUpdateIndexSubject(ctx, index.ID, model.SubjectID(i),
- uint32(i), fmt.Sprintf("comment %d", i))
+ for i := uint32(10); i < 20; i++ {
+ _, err = repo.AddOrUpdateIndexSubject(ctx, index.ID, i, i, fmt.Sprintf("comment %d", i))
require.NoError(t, err)
}
@@ -292,9 +291,8 @@ func TestMysqlRepo_DeleteIndexSubject(t *testing.T) {
require.NotEqual(t, 0, index.ID)
require.NoError(t, err)
- for i := 10; i < 20; i++ {
- _, err = repo.AddOrUpdateIndexSubject(ctx, index.ID, model.SubjectID(i),
- uint32(i), fmt.Sprintf("comment %d", i))
+ for i := uint32(10); i < 20; i++ {
+ _, err = repo.AddOrUpdateIndexSubject(ctx, index.ID, i, i, fmt.Sprintf("comment %d", i))
require.NoError(t, err)
}
diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go
index 410f90e5c..9756532f9 100644
--- a/internal/metrics/metrics.go
+++ b/internal/metrics/metrics.go
@@ -12,7 +12,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see
-//nolint:gomnd,gochecknoinits,gochecknoglobals
+//nolint:mnd,gochecknoinits,gochecknoglobals
package metrics
import (
@@ -45,7 +45,10 @@ var RequestHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
0.200,
0.300,
0.500,
- 1.000,
+ 1,
+ 2,
+ 5,
+ 10,
},
})
@@ -68,6 +71,9 @@ var SQLHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
0.200,
0.300,
0.500,
- 1.000,
+ 1,
+ 2,
+ 5,
+ 10,
},
})
diff --git a/internal/mocks/CollectionRepo.go b/internal/mocks/CollectionRepo.go
index 8bbf921d5..c7ce9f663 100644
--- a/internal/mocks/CollectionRepo.go
+++ b/internal/mocks/CollectionRepo.go
@@ -30,6 +30,113 @@ func (_m *CollectionRepo) EXPECT() *CollectionRepo_Expecter {
return &CollectionRepo_Expecter{mock: &_m.Mock}
}
+// AddPersonCollection provides a mock function with given fields: ctx, userID, cat, targetID
+func (_m *CollectionRepo) AddPersonCollection(ctx context.Context, userID uint32, cat collection.PersonCollectCategory, targetID uint32) error {
+ ret := _m.Called(ctx, userID, cat, targetID)
+
+ if len(ret) == 0 {
+ panic("no return value specified for AddPersonCollection")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(context.Context, uint32, collection.PersonCollectCategory, uint32) error); ok {
+ r0 = rf(ctx, userID, cat, targetID)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// CollectionRepo_AddPersonCollection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddPersonCollection'
+type CollectionRepo_AddPersonCollection_Call struct {
+ *mock.Call
+}
+
+// AddPersonCollection is a helper method to define mock.On call
+// - ctx context.Context
+// - userID uint32
+// - cat collection.PersonCollectCategory
+// - targetID uint32
+func (_e *CollectionRepo_Expecter) AddPersonCollection(ctx interface{}, userID interface{}, cat interface{}, targetID interface{}) *CollectionRepo_AddPersonCollection_Call {
+ return &CollectionRepo_AddPersonCollection_Call{Call: _e.mock.On("AddPersonCollection", ctx, userID, cat, targetID)}
+}
+
+func (_c *CollectionRepo_AddPersonCollection_Call) Run(run func(ctx context.Context, userID uint32, cat collection.PersonCollectCategory, targetID uint32)) *CollectionRepo_AddPersonCollection_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(context.Context), args[1].(uint32), args[2].(collection.PersonCollectCategory), args[3].(uint32))
+ })
+ return _c
+}
+
+func (_c *CollectionRepo_AddPersonCollection_Call) Return(_a0 error) *CollectionRepo_AddPersonCollection_Call {
+ _c.Call.Return(_a0)
+ return _c
+}
+
+func (_c *CollectionRepo_AddPersonCollection_Call) RunAndReturn(run func(context.Context, uint32, collection.PersonCollectCategory, uint32) error) *CollectionRepo_AddPersonCollection_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// CountPersonCollections provides a mock function with given fields: ctx, userID, cat
+func (_m *CollectionRepo) CountPersonCollections(ctx context.Context, userID uint32, cat collection.PersonCollectCategory) (int64, error) {
+ ret := _m.Called(ctx, userID, cat)
+
+ if len(ret) == 0 {
+ panic("no return value specified for CountPersonCollections")
+ }
+
+ var r0 int64
+ var r1 error
+ if rf, ok := ret.Get(0).(func(context.Context, uint32, collection.PersonCollectCategory) (int64, error)); ok {
+ return rf(ctx, userID, cat)
+ }
+ if rf, ok := ret.Get(0).(func(context.Context, uint32, collection.PersonCollectCategory) int64); ok {
+ r0 = rf(ctx, userID, cat)
+ } else {
+ r0 = ret.Get(0).(int64)
+ }
+
+ if rf, ok := ret.Get(1).(func(context.Context, uint32, collection.PersonCollectCategory) error); ok {
+ r1 = rf(ctx, userID, cat)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// CollectionRepo_CountPersonCollections_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CountPersonCollections'
+type CollectionRepo_CountPersonCollections_Call struct {
+ *mock.Call
+}
+
+// CountPersonCollections is a helper method to define mock.On call
+// - ctx context.Context
+// - userID uint32
+// - cat collection.PersonCollectCategory
+func (_e *CollectionRepo_Expecter) CountPersonCollections(ctx interface{}, userID interface{}, cat interface{}) *CollectionRepo_CountPersonCollections_Call {
+ return &CollectionRepo_CountPersonCollections_Call{Call: _e.mock.On("CountPersonCollections", ctx, userID, cat)}
+}
+
+func (_c *CollectionRepo_CountPersonCollections_Call) Run(run func(ctx context.Context, userID uint32, cat collection.PersonCollectCategory)) *CollectionRepo_CountPersonCollections_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(context.Context), args[1].(uint32), args[2].(collection.PersonCollectCategory))
+ })
+ return _c
+}
+
+func (_c *CollectionRepo_CountPersonCollections_Call) Return(_a0 int64, _a1 error) *CollectionRepo_CountPersonCollections_Call {
+ _c.Call.Return(_a0, _a1)
+ return _c
+}
+
+func (_c *CollectionRepo_CountPersonCollections_Call) RunAndReturn(run func(context.Context, uint32, collection.PersonCollectCategory) (int64, error)) *CollectionRepo_CountPersonCollections_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
// CountSubjectCollections provides a mock function with given fields: ctx, userID, subjectType, collectionType, showPrivate
func (_m *CollectionRepo) CountSubjectCollections(ctx context.Context, userID uint32, subjectType uint8, collectionType collection.SubjectCollection, showPrivate bool) (int64, error) {
ret := _m.Called(ctx, userID, subjectType, collectionType, showPrivate)
@@ -138,6 +245,65 @@ func (_c *CollectionRepo_DeleteSubjectCollection_Call) RunAndReturn(run func(con
return _c
}
+// GetPersonCollection provides a mock function with given fields: ctx, userID, cat, targetID
+func (_m *CollectionRepo) GetPersonCollection(ctx context.Context, userID uint32, cat collection.PersonCollectCategory, targetID uint32) (collection.UserPersonCollection, error) {
+ ret := _m.Called(ctx, userID, cat, targetID)
+
+ if len(ret) == 0 {
+ panic("no return value specified for GetPersonCollection")
+ }
+
+ var r0 collection.UserPersonCollection
+ var r1 error
+ if rf, ok := ret.Get(0).(func(context.Context, uint32, collection.PersonCollectCategory, uint32) (collection.UserPersonCollection, error)); ok {
+ return rf(ctx, userID, cat, targetID)
+ }
+ if rf, ok := ret.Get(0).(func(context.Context, uint32, collection.PersonCollectCategory, uint32) collection.UserPersonCollection); ok {
+ r0 = rf(ctx, userID, cat, targetID)
+ } else {
+ r0 = ret.Get(0).(collection.UserPersonCollection)
+ }
+
+ if rf, ok := ret.Get(1).(func(context.Context, uint32, collection.PersonCollectCategory, uint32) error); ok {
+ r1 = rf(ctx, userID, cat, targetID)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// CollectionRepo_GetPersonCollection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPersonCollection'
+type CollectionRepo_GetPersonCollection_Call struct {
+ *mock.Call
+}
+
+// GetPersonCollection is a helper method to define mock.On call
+// - ctx context.Context
+// - userID uint32
+// - cat collection.PersonCollectCategory
+// - targetID uint32
+func (_e *CollectionRepo_Expecter) GetPersonCollection(ctx interface{}, userID interface{}, cat interface{}, targetID interface{}) *CollectionRepo_GetPersonCollection_Call {
+ return &CollectionRepo_GetPersonCollection_Call{Call: _e.mock.On("GetPersonCollection", ctx, userID, cat, targetID)}
+}
+
+func (_c *CollectionRepo_GetPersonCollection_Call) Run(run func(ctx context.Context, userID uint32, cat collection.PersonCollectCategory, targetID uint32)) *CollectionRepo_GetPersonCollection_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(context.Context), args[1].(uint32), args[2].(collection.PersonCollectCategory), args[3].(uint32))
+ })
+ return _c
+}
+
+func (_c *CollectionRepo_GetPersonCollection_Call) Return(_a0 collection.UserPersonCollection, _a1 error) *CollectionRepo_GetPersonCollection_Call {
+ _c.Call.Return(_a0, _a1)
+ return _c
+}
+
+func (_c *CollectionRepo_GetPersonCollection_Call) RunAndReturn(run func(context.Context, uint32, collection.PersonCollectCategory, uint32) (collection.UserPersonCollection, error)) *CollectionRepo_GetPersonCollection_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
// GetSubjectCollection provides a mock function with given fields: ctx, userID, subjectID
func (_m *CollectionRepo) GetSubjectCollection(ctx context.Context, userID uint32, subjectID uint32) (collection.UserSubjectCollection, error) {
ret := _m.Called(ctx, userID, subjectID)
@@ -256,6 +422,68 @@ func (_c *CollectionRepo_GetSubjectEpisodesCollection_Call) RunAndReturn(run fun
return _c
}
+// ListPersonCollection provides a mock function with given fields: ctx, userID, cat, limit, offset
+func (_m *CollectionRepo) ListPersonCollection(ctx context.Context, userID uint32, cat collection.PersonCollectCategory, limit int, offset int) ([]collection.UserPersonCollection, error) {
+ ret := _m.Called(ctx, userID, cat, limit, offset)
+
+ if len(ret) == 0 {
+ panic("no return value specified for ListPersonCollection")
+ }
+
+ var r0 []collection.UserPersonCollection
+ var r1 error
+ if rf, ok := ret.Get(0).(func(context.Context, uint32, collection.PersonCollectCategory, int, int) ([]collection.UserPersonCollection, error)); ok {
+ return rf(ctx, userID, cat, limit, offset)
+ }
+ if rf, ok := ret.Get(0).(func(context.Context, uint32, collection.PersonCollectCategory, int, int) []collection.UserPersonCollection); ok {
+ r0 = rf(ctx, userID, cat, limit, offset)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).([]collection.UserPersonCollection)
+ }
+ }
+
+ if rf, ok := ret.Get(1).(func(context.Context, uint32, collection.PersonCollectCategory, int, int) error); ok {
+ r1 = rf(ctx, userID, cat, limit, offset)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// CollectionRepo_ListPersonCollection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPersonCollection'
+type CollectionRepo_ListPersonCollection_Call struct {
+ *mock.Call
+}
+
+// ListPersonCollection is a helper method to define mock.On call
+// - ctx context.Context
+// - userID uint32
+// - cat collection.PersonCollectCategory
+// - limit int
+// - offset int
+func (_e *CollectionRepo_Expecter) ListPersonCollection(ctx interface{}, userID interface{}, cat interface{}, limit interface{}, offset interface{}) *CollectionRepo_ListPersonCollection_Call {
+ return &CollectionRepo_ListPersonCollection_Call{Call: _e.mock.On("ListPersonCollection", ctx, userID, cat, limit, offset)}
+}
+
+func (_c *CollectionRepo_ListPersonCollection_Call) Run(run func(ctx context.Context, userID uint32, cat collection.PersonCollectCategory, limit int, offset int)) *CollectionRepo_ListPersonCollection_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(context.Context), args[1].(uint32), args[2].(collection.PersonCollectCategory), args[3].(int), args[4].(int))
+ })
+ return _c
+}
+
+func (_c *CollectionRepo_ListPersonCollection_Call) Return(_a0 []collection.UserPersonCollection, _a1 error) *CollectionRepo_ListPersonCollection_Call {
+ _c.Call.Return(_a0, _a1)
+ return _c
+}
+
+func (_c *CollectionRepo_ListPersonCollection_Call) RunAndReturn(run func(context.Context, uint32, collection.PersonCollectCategory, int, int) ([]collection.UserPersonCollection, error)) *CollectionRepo_ListPersonCollection_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
// ListSubjectCollection provides a mock function with given fields: ctx, userID, subjectType, collectionType, showPrivate, limit, offset
func (_m *CollectionRepo) ListSubjectCollection(ctx context.Context, userID uint32, subjectType uint8, collectionType collection.SubjectCollection, showPrivate bool, limit int, offset int) ([]collection.UserSubjectCollection, error) {
ret := _m.Called(ctx, userID, subjectType, collectionType, showPrivate, limit, offset)
@@ -320,6 +548,55 @@ func (_c *CollectionRepo_ListSubjectCollection_Call) RunAndReturn(run func(conte
return _c
}
+// RemovePersonCollection provides a mock function with given fields: ctx, userID, cat, targetID
+func (_m *CollectionRepo) RemovePersonCollection(ctx context.Context, userID uint32, cat collection.PersonCollectCategory, targetID uint32) error {
+ ret := _m.Called(ctx, userID, cat, targetID)
+
+ if len(ret) == 0 {
+ panic("no return value specified for RemovePersonCollection")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(context.Context, uint32, collection.PersonCollectCategory, uint32) error); ok {
+ r0 = rf(ctx, userID, cat, targetID)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// CollectionRepo_RemovePersonCollection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemovePersonCollection'
+type CollectionRepo_RemovePersonCollection_Call struct {
+ *mock.Call
+}
+
+// RemovePersonCollection is a helper method to define mock.On call
+// - ctx context.Context
+// - userID uint32
+// - cat collection.PersonCollectCategory
+// - targetID uint32
+func (_e *CollectionRepo_Expecter) RemovePersonCollection(ctx interface{}, userID interface{}, cat interface{}, targetID interface{}) *CollectionRepo_RemovePersonCollection_Call {
+ return &CollectionRepo_RemovePersonCollection_Call{Call: _e.mock.On("RemovePersonCollection", ctx, userID, cat, targetID)}
+}
+
+func (_c *CollectionRepo_RemovePersonCollection_Call) Run(run func(ctx context.Context, userID uint32, cat collection.PersonCollectCategory, targetID uint32)) *CollectionRepo_RemovePersonCollection_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(context.Context), args[1].(uint32), args[2].(collection.PersonCollectCategory), args[3].(uint32))
+ })
+ return _c
+}
+
+func (_c *CollectionRepo_RemovePersonCollection_Call) Return(_a0 error) *CollectionRepo_RemovePersonCollection_Call {
+ _c.Call.Return(_a0)
+ return _c
+}
+
+func (_c *CollectionRepo_RemovePersonCollection_Call) RunAndReturn(run func(context.Context, uint32, collection.PersonCollectCategory, uint32) error) *CollectionRepo_RemovePersonCollection_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
// UpdateEpisodeCollection provides a mock function with given fields: ctx, userID, subjectID, episodeIDs, _a4, at
func (_m *CollectionRepo) UpdateEpisodeCollection(ctx context.Context, userID uint32, subjectID uint32, episodeIDs []uint32, _a4 collection.EpisodeCollection, at time.Time) (collection.UserSubjectEpisodesCollection, error) {
ret := _m.Called(ctx, userID, subjectID, episodeIDs, _a4, at)
diff --git a/internal/mocks/RedisCache.go b/internal/mocks/RedisCache.go
index fce7469dd..6a08e0216 100644
--- a/internal/mocks/RedisCache.go
+++ b/internal/mocks/RedisCache.go
@@ -141,6 +141,54 @@ func (_c *RedisCache_Get_Call) RunAndReturn(run func(context.Context, string, in
return _c
}
+// MGet provides a mock function with given fields: ctx, key, result
+func (_m *RedisCache) MGet(ctx context.Context, key []string, result interface{}) error {
+ ret := _m.Called(ctx, key, result)
+
+ if len(ret) == 0 {
+ panic("no return value specified for MGet")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(context.Context, []string, interface{}) error); ok {
+ r0 = rf(ctx, key, result)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// RedisCache_MGet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MGet'
+type RedisCache_MGet_Call struct {
+ *mock.Call
+}
+
+// MGet is a helper method to define mock.On call
+// - ctx context.Context
+// - key []string
+// - result interface{}
+func (_e *RedisCache_Expecter) MGet(ctx interface{}, key interface{}, result interface{}) *RedisCache_MGet_Call {
+ return &RedisCache_MGet_Call{Call: _e.mock.On("MGet", ctx, key, result)}
+}
+
+func (_c *RedisCache_MGet_Call) Run(run func(ctx context.Context, key []string, result interface{})) *RedisCache_MGet_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(context.Context), args[1].([]string), args[2].(interface{}))
+ })
+ return _c
+}
+
+func (_c *RedisCache_MGet_Call) Return(_a0 error) *RedisCache_MGet_Call {
+ _c.Call.Return(_a0)
+ return _c
+}
+
+func (_c *RedisCache_MGet_Call) RunAndReturn(run func(context.Context, []string, interface{}) error) *RedisCache_MGet_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
// Set provides a mock function with given fields: ctx, key, value, ttl
func (_m *RedisCache) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
ret := _m.Called(ctx, key, value, ttl)
diff --git a/internal/mocks/SearchClient.go b/internal/mocks/SearchClient.go
index cdf74f078..41241d903 100644
--- a/internal/mocks/SearchClient.go
+++ b/internal/mocks/SearchClient.go
@@ -7,6 +7,8 @@ import (
echo "github.com/labstack/echo/v4"
mock "github.com/stretchr/testify/mock"
+
+ search "github.com/bangumi/server/internal/search"
)
// SearchClient is an autogenerated mock type for the Client type
@@ -54,17 +56,17 @@ func (_c *SearchClient_Close_Call) RunAndReturn(run func()) *SearchClient_Close_
return _c
}
-// Handle provides a mock function with given fields: c
-func (_m *SearchClient) Handle(c echo.Context) error {
- ret := _m.Called(c)
+// EventAdded provides a mock function with given fields: ctx, id, target
+func (_m *SearchClient) EventAdded(ctx context.Context, id uint32, target search.SearchTarget) error {
+ ret := _m.Called(ctx, id, target)
if len(ret) == 0 {
- panic("no return value specified for Handle")
+ panic("no return value specified for EventAdded")
}
var r0 error
- if rf, ok := ret.Get(0).(func(echo.Context) error); ok {
- r0 = rf(c)
+ if rf, ok := ret.Get(0).(func(context.Context, uint32, search.SearchTarget) error); ok {
+ r0 = rf(ctx, id, target)
} else {
r0 = ret.Error(0)
}
@@ -72,45 +74,47 @@ func (_m *SearchClient) Handle(c echo.Context) error {
return r0
}
-// SearchClient_Handle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Handle'
-type SearchClient_Handle_Call struct {
+// SearchClient_EventAdded_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EventAdded'
+type SearchClient_EventAdded_Call struct {
*mock.Call
}
-// Handle is a helper method to define mock.On call
-// - c echo.Context
-func (_e *SearchClient_Expecter) Handle(c interface{}) *SearchClient_Handle_Call {
- return &SearchClient_Handle_Call{Call: _e.mock.On("Handle", c)}
+// EventAdded is a helper method to define mock.On call
+// - ctx context.Context
+// - id uint32
+// - target search.SearchTarget
+func (_e *SearchClient_Expecter) EventAdded(ctx interface{}, id interface{}, target interface{}) *SearchClient_EventAdded_Call {
+ return &SearchClient_EventAdded_Call{Call: _e.mock.On("EventAdded", ctx, id, target)}
}
-func (_c *SearchClient_Handle_Call) Run(run func(c echo.Context)) *SearchClient_Handle_Call {
+func (_c *SearchClient_EventAdded_Call) Run(run func(ctx context.Context, id uint32, target search.SearchTarget)) *SearchClient_EventAdded_Call {
_c.Call.Run(func(args mock.Arguments) {
- run(args[0].(echo.Context))
+ run(args[0].(context.Context), args[1].(uint32), args[2].(search.SearchTarget))
})
return _c
}
-func (_c *SearchClient_Handle_Call) Return(_a0 error) *SearchClient_Handle_Call {
+func (_c *SearchClient_EventAdded_Call) Return(_a0 error) *SearchClient_EventAdded_Call {
_c.Call.Return(_a0)
return _c
}
-func (_c *SearchClient_Handle_Call) RunAndReturn(run func(echo.Context) error) *SearchClient_Handle_Call {
+func (_c *SearchClient_EventAdded_Call) RunAndReturn(run func(context.Context, uint32, search.SearchTarget) error) *SearchClient_EventAdded_Call {
_c.Call.Return(run)
return _c
}
-// OnSubjectDelete provides a mock function with given fields: ctx, id
-func (_m *SearchClient) OnSubjectDelete(ctx context.Context, id uint32) error {
- ret := _m.Called(ctx, id)
+// EventDelete provides a mock function with given fields: ctx, id, target
+func (_m *SearchClient) EventDelete(ctx context.Context, id uint32, target search.SearchTarget) error {
+ ret := _m.Called(ctx, id, target)
if len(ret) == 0 {
- panic("no return value specified for OnSubjectDelete")
+ panic("no return value specified for EventDelete")
}
var r0 error
- if rf, ok := ret.Get(0).(func(context.Context, uint32) error); ok {
- r0 = rf(ctx, id)
+ if rf, ok := ret.Get(0).(func(context.Context, uint32, search.SearchTarget) error); ok {
+ r0 = rf(ctx, id, target)
} else {
r0 = ret.Error(0)
}
@@ -118,46 +122,47 @@ func (_m *SearchClient) OnSubjectDelete(ctx context.Context, id uint32) error {
return r0
}
-// SearchClient_OnSubjectDelete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnSubjectDelete'
-type SearchClient_OnSubjectDelete_Call struct {
+// SearchClient_EventDelete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EventDelete'
+type SearchClient_EventDelete_Call struct {
*mock.Call
}
-// OnSubjectDelete is a helper method to define mock.On call
+// EventDelete is a helper method to define mock.On call
// - ctx context.Context
// - id uint32
-func (_e *SearchClient_Expecter) OnSubjectDelete(ctx interface{}, id interface{}) *SearchClient_OnSubjectDelete_Call {
- return &SearchClient_OnSubjectDelete_Call{Call: _e.mock.On("OnSubjectDelete", ctx, id)}
+// - target search.SearchTarget
+func (_e *SearchClient_Expecter) EventDelete(ctx interface{}, id interface{}, target interface{}) *SearchClient_EventDelete_Call {
+ return &SearchClient_EventDelete_Call{Call: _e.mock.On("EventDelete", ctx, id, target)}
}
-func (_c *SearchClient_OnSubjectDelete_Call) Run(run func(ctx context.Context, id uint32)) *SearchClient_OnSubjectDelete_Call {
+func (_c *SearchClient_EventDelete_Call) Run(run func(ctx context.Context, id uint32, target search.SearchTarget)) *SearchClient_EventDelete_Call {
_c.Call.Run(func(args mock.Arguments) {
- run(args[0].(context.Context), args[1].(uint32))
+ run(args[0].(context.Context), args[1].(uint32), args[2].(search.SearchTarget))
})
return _c
}
-func (_c *SearchClient_OnSubjectDelete_Call) Return(_a0 error) *SearchClient_OnSubjectDelete_Call {
+func (_c *SearchClient_EventDelete_Call) Return(_a0 error) *SearchClient_EventDelete_Call {
_c.Call.Return(_a0)
return _c
}
-func (_c *SearchClient_OnSubjectDelete_Call) RunAndReturn(run func(context.Context, uint32) error) *SearchClient_OnSubjectDelete_Call {
+func (_c *SearchClient_EventDelete_Call) RunAndReturn(run func(context.Context, uint32, search.SearchTarget) error) *SearchClient_EventDelete_Call {
_c.Call.Return(run)
return _c
}
-// OnSubjectUpdate provides a mock function with given fields: ctx, id
-func (_m *SearchClient) OnSubjectUpdate(ctx context.Context, id uint32) error {
- ret := _m.Called(ctx, id)
+// EventUpdate provides a mock function with given fields: ctx, id, target
+func (_m *SearchClient) EventUpdate(ctx context.Context, id uint32, target search.SearchTarget) error {
+ ret := _m.Called(ctx, id, target)
if len(ret) == 0 {
- panic("no return value specified for OnSubjectUpdate")
+ panic("no return value specified for EventUpdate")
}
var r0 error
- if rf, ok := ret.Get(0).(func(context.Context, uint32) error); ok {
- r0 = rf(ctx, id)
+ if rf, ok := ret.Get(0).(func(context.Context, uint32, search.SearchTarget) error); ok {
+ r0 = rf(ctx, id, target)
} else {
r0 = ret.Error(0)
}
@@ -165,31 +170,79 @@ func (_m *SearchClient) OnSubjectUpdate(ctx context.Context, id uint32) error {
return r0
}
-// SearchClient_OnSubjectUpdate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnSubjectUpdate'
-type SearchClient_OnSubjectUpdate_Call struct {
+// SearchClient_EventUpdate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EventUpdate'
+type SearchClient_EventUpdate_Call struct {
*mock.Call
}
-// OnSubjectUpdate is a helper method to define mock.On call
+// EventUpdate is a helper method to define mock.On call
// - ctx context.Context
// - id uint32
-func (_e *SearchClient_Expecter) OnSubjectUpdate(ctx interface{}, id interface{}) *SearchClient_OnSubjectUpdate_Call {
- return &SearchClient_OnSubjectUpdate_Call{Call: _e.mock.On("OnSubjectUpdate", ctx, id)}
+// - target search.SearchTarget
+func (_e *SearchClient_Expecter) EventUpdate(ctx interface{}, id interface{}, target interface{}) *SearchClient_EventUpdate_Call {
+ return &SearchClient_EventUpdate_Call{Call: _e.mock.On("EventUpdate", ctx, id, target)}
}
-func (_c *SearchClient_OnSubjectUpdate_Call) Run(run func(ctx context.Context, id uint32)) *SearchClient_OnSubjectUpdate_Call {
+func (_c *SearchClient_EventUpdate_Call) Run(run func(ctx context.Context, id uint32, target search.SearchTarget)) *SearchClient_EventUpdate_Call {
_c.Call.Run(func(args mock.Arguments) {
- run(args[0].(context.Context), args[1].(uint32))
+ run(args[0].(context.Context), args[1].(uint32), args[2].(search.SearchTarget))
})
return _c
}
-func (_c *SearchClient_OnSubjectUpdate_Call) Return(_a0 error) *SearchClient_OnSubjectUpdate_Call {
+func (_c *SearchClient_EventUpdate_Call) Return(_a0 error) *SearchClient_EventUpdate_Call {
+ _c.Call.Return(_a0)
+ return _c
+}
+
+func (_c *SearchClient_EventUpdate_Call) RunAndReturn(run func(context.Context, uint32, search.SearchTarget) error) *SearchClient_EventUpdate_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// Handle provides a mock function with given fields: c, target
+func (_m *SearchClient) Handle(c echo.Context, target search.SearchTarget) error {
+ ret := _m.Called(c, target)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Handle")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(echo.Context, search.SearchTarget) error); ok {
+ r0 = rf(c, target)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// SearchClient_Handle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Handle'
+type SearchClient_Handle_Call struct {
+ *mock.Call
+}
+
+// Handle is a helper method to define mock.On call
+// - c echo.Context
+// - target search.SearchTarget
+func (_e *SearchClient_Expecter) Handle(c interface{}, target interface{}) *SearchClient_Handle_Call {
+ return &SearchClient_Handle_Call{Call: _e.mock.On("Handle", c, target)}
+}
+
+func (_c *SearchClient_Handle_Call) Run(run func(c echo.Context, target search.SearchTarget)) *SearchClient_Handle_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(echo.Context), args[1].(search.SearchTarget))
+ })
+ return _c
+}
+
+func (_c *SearchClient_Handle_Call) Return(_a0 error) *SearchClient_Handle_Call {
_c.Call.Return(_a0)
return _c
}
-func (_c *SearchClient_OnSubjectUpdate_Call) RunAndReturn(run func(context.Context, uint32) error) *SearchClient_OnSubjectUpdate_Call {
+func (_c *SearchClient_Handle_Call) RunAndReturn(run func(echo.Context, search.SearchTarget) error) *SearchClient_Handle_Call {
_c.Call.Return(run)
return _c
}
diff --git a/internal/mocks/SubjectCachedRepo.go b/internal/mocks/SubjectCachedRepo.go
index 867524561..48c1ce036 100644
--- a/internal/mocks/SubjectCachedRepo.go
+++ b/internal/mocks/SubjectCachedRepo.go
@@ -26,6 +26,124 @@ func (_m *SubjectCachedRepo) EXPECT() *SubjectCachedRepo_Expecter {
return &SubjectCachedRepo_Expecter{mock: &_m.Mock}
}
+// Browse provides a mock function with given fields: ctx, filter, limit, offset
+func (_m *SubjectCachedRepo) Browse(ctx context.Context, filter subject.BrowseFilter, limit int, offset int) ([]model.Subject, error) {
+ ret := _m.Called(ctx, filter, limit, offset)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Browse")
+ }
+
+ var r0 []model.Subject
+ var r1 error
+ if rf, ok := ret.Get(0).(func(context.Context, subject.BrowseFilter, int, int) ([]model.Subject, error)); ok {
+ return rf(ctx, filter, limit, offset)
+ }
+ if rf, ok := ret.Get(0).(func(context.Context, subject.BrowseFilter, int, int) []model.Subject); ok {
+ r0 = rf(ctx, filter, limit, offset)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).([]model.Subject)
+ }
+ }
+
+ if rf, ok := ret.Get(1).(func(context.Context, subject.BrowseFilter, int, int) error); ok {
+ r1 = rf(ctx, filter, limit, offset)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// SubjectCachedRepo_Browse_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Browse'
+type SubjectCachedRepo_Browse_Call struct {
+ *mock.Call
+}
+
+// Browse is a helper method to define mock.On call
+// - ctx context.Context
+// - filter subject.BrowseFilter
+// - limit int
+// - offset int
+func (_e *SubjectCachedRepo_Expecter) Browse(ctx interface{}, filter interface{}, limit interface{}, offset interface{}) *SubjectCachedRepo_Browse_Call {
+ return &SubjectCachedRepo_Browse_Call{Call: _e.mock.On("Browse", ctx, filter, limit, offset)}
+}
+
+func (_c *SubjectCachedRepo_Browse_Call) Run(run func(ctx context.Context, filter subject.BrowseFilter, limit int, offset int)) *SubjectCachedRepo_Browse_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(context.Context), args[1].(subject.BrowseFilter), args[2].(int), args[3].(int))
+ })
+ return _c
+}
+
+func (_c *SubjectCachedRepo_Browse_Call) Return(_a0 []model.Subject, _a1 error) *SubjectCachedRepo_Browse_Call {
+ _c.Call.Return(_a0, _a1)
+ return _c
+}
+
+func (_c *SubjectCachedRepo_Browse_Call) RunAndReturn(run func(context.Context, subject.BrowseFilter, int, int) ([]model.Subject, error)) *SubjectCachedRepo_Browse_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// Count provides a mock function with given fields: ctx, filter
+func (_m *SubjectCachedRepo) Count(ctx context.Context, filter subject.BrowseFilter) (int64, error) {
+ ret := _m.Called(ctx, filter)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Count")
+ }
+
+ var r0 int64
+ var r1 error
+ if rf, ok := ret.Get(0).(func(context.Context, subject.BrowseFilter) (int64, error)); ok {
+ return rf(ctx, filter)
+ }
+ if rf, ok := ret.Get(0).(func(context.Context, subject.BrowseFilter) int64); ok {
+ r0 = rf(ctx, filter)
+ } else {
+ r0 = ret.Get(0).(int64)
+ }
+
+ if rf, ok := ret.Get(1).(func(context.Context, subject.BrowseFilter) error); ok {
+ r1 = rf(ctx, filter)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// SubjectCachedRepo_Count_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Count'
+type SubjectCachedRepo_Count_Call struct {
+ *mock.Call
+}
+
+// Count is a helper method to define mock.On call
+// - ctx context.Context
+// - filter subject.BrowseFilter
+func (_e *SubjectCachedRepo_Expecter) Count(ctx interface{}, filter interface{}) *SubjectCachedRepo_Count_Call {
+ return &SubjectCachedRepo_Count_Call{Call: _e.mock.On("Count", ctx, filter)}
+}
+
+func (_c *SubjectCachedRepo_Count_Call) Run(run func(ctx context.Context, filter subject.BrowseFilter)) *SubjectCachedRepo_Count_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(context.Context), args[1].(subject.BrowseFilter))
+ })
+ return _c
+}
+
+func (_c *SubjectCachedRepo_Count_Call) Return(_a0 int64, _a1 error) *SubjectCachedRepo_Count_Call {
+ _c.Call.Return(_a0, _a1)
+ return _c
+}
+
+func (_c *SubjectCachedRepo_Count_Call) RunAndReturn(run func(context.Context, subject.BrowseFilter) (int64, error)) *SubjectCachedRepo_Count_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
// Get provides a mock function with given fields: ctx, id, filter
func (_m *SubjectCachedRepo) Get(ctx context.Context, id uint32, filter subject.Filter) (model.Subject, error) {
ret := _m.Called(ctx, id, filter)
diff --git a/internal/mocks/SubjectRepo.go b/internal/mocks/SubjectRepo.go
index 84c36ee4b..8ec7b2338 100644
--- a/internal/mocks/SubjectRepo.go
+++ b/internal/mocks/SubjectRepo.go
@@ -26,6 +26,124 @@ func (_m *SubjectRepo) EXPECT() *SubjectRepo_Expecter {
return &SubjectRepo_Expecter{mock: &_m.Mock}
}
+// Browse provides a mock function with given fields: ctx, filter, limit, offset
+func (_m *SubjectRepo) Browse(ctx context.Context, filter subject.BrowseFilter, limit int, offset int) ([]model.Subject, error) {
+ ret := _m.Called(ctx, filter, limit, offset)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Browse")
+ }
+
+ var r0 []model.Subject
+ var r1 error
+ if rf, ok := ret.Get(0).(func(context.Context, subject.BrowseFilter, int, int) ([]model.Subject, error)); ok {
+ return rf(ctx, filter, limit, offset)
+ }
+ if rf, ok := ret.Get(0).(func(context.Context, subject.BrowseFilter, int, int) []model.Subject); ok {
+ r0 = rf(ctx, filter, limit, offset)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).([]model.Subject)
+ }
+ }
+
+ if rf, ok := ret.Get(1).(func(context.Context, subject.BrowseFilter, int, int) error); ok {
+ r1 = rf(ctx, filter, limit, offset)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// SubjectRepo_Browse_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Browse'
+type SubjectRepo_Browse_Call struct {
+ *mock.Call
+}
+
+// Browse is a helper method to define mock.On call
+// - ctx context.Context
+// - filter subject.BrowseFilter
+// - limit int
+// - offset int
+func (_e *SubjectRepo_Expecter) Browse(ctx interface{}, filter interface{}, limit interface{}, offset interface{}) *SubjectRepo_Browse_Call {
+ return &SubjectRepo_Browse_Call{Call: _e.mock.On("Browse", ctx, filter, limit, offset)}
+}
+
+func (_c *SubjectRepo_Browse_Call) Run(run func(ctx context.Context, filter subject.BrowseFilter, limit int, offset int)) *SubjectRepo_Browse_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(context.Context), args[1].(subject.BrowseFilter), args[2].(int), args[3].(int))
+ })
+ return _c
+}
+
+func (_c *SubjectRepo_Browse_Call) Return(_a0 []model.Subject, _a1 error) *SubjectRepo_Browse_Call {
+ _c.Call.Return(_a0, _a1)
+ return _c
+}
+
+func (_c *SubjectRepo_Browse_Call) RunAndReturn(run func(context.Context, subject.BrowseFilter, int, int) ([]model.Subject, error)) *SubjectRepo_Browse_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// Count provides a mock function with given fields: ctx, filter
+func (_m *SubjectRepo) Count(ctx context.Context, filter subject.BrowseFilter) (int64, error) {
+ ret := _m.Called(ctx, filter)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Count")
+ }
+
+ var r0 int64
+ var r1 error
+ if rf, ok := ret.Get(0).(func(context.Context, subject.BrowseFilter) (int64, error)); ok {
+ return rf(ctx, filter)
+ }
+ if rf, ok := ret.Get(0).(func(context.Context, subject.BrowseFilter) int64); ok {
+ r0 = rf(ctx, filter)
+ } else {
+ r0 = ret.Get(0).(int64)
+ }
+
+ if rf, ok := ret.Get(1).(func(context.Context, subject.BrowseFilter) error); ok {
+ r1 = rf(ctx, filter)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// SubjectRepo_Count_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Count'
+type SubjectRepo_Count_Call struct {
+ *mock.Call
+}
+
+// Count is a helper method to define mock.On call
+// - ctx context.Context
+// - filter subject.BrowseFilter
+func (_e *SubjectRepo_Expecter) Count(ctx interface{}, filter interface{}) *SubjectRepo_Count_Call {
+ return &SubjectRepo_Count_Call{Call: _e.mock.On("Count", ctx, filter)}
+}
+
+func (_c *SubjectRepo_Count_Call) Run(run func(ctx context.Context, filter subject.BrowseFilter)) *SubjectRepo_Count_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(context.Context), args[1].(subject.BrowseFilter))
+ })
+ return _c
+}
+
+func (_c *SubjectRepo_Count_Call) Return(_a0 int64, _a1 error) *SubjectRepo_Count_Call {
+ _c.Call.Return(_a0, _a1)
+ return _c
+}
+
+func (_c *SubjectRepo_Count_Call) RunAndReturn(run func(context.Context, subject.BrowseFilter) (int64, error)) *SubjectRepo_Count_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
// Get provides a mock function with given fields: ctx, id, filter
func (_m *SubjectRepo) Get(ctx context.Context, id uint32, filter subject.Filter) (model.Subject, error) {
ret := _m.Called(ctx, id, filter)
diff --git a/internal/mocks/TagRepo.go b/internal/mocks/TagRepo.go
new file mode 100644
index 000000000..2aec09a0e
--- /dev/null
+++ b/internal/mocks/TagRepo.go
@@ -0,0 +1,155 @@
+// Code generated by mockery v2.43.0. DO NOT EDIT.
+
+package mocks
+
+import (
+ context "context"
+
+ tag "github.com/bangumi/server/internal/tag"
+ mock "github.com/stretchr/testify/mock"
+)
+
+// TagRepo is an autogenerated mock type for the Repo type
+type TagRepo struct {
+ mock.Mock
+}
+
+type TagRepo_Expecter struct {
+ mock *mock.Mock
+}
+
+func (_m *TagRepo) EXPECT() *TagRepo_Expecter {
+ return &TagRepo_Expecter{mock: &_m.Mock}
+}
+
+// Get provides a mock function with given fields: ctx, id
+func (_m *TagRepo) Get(ctx context.Context, id uint32) ([]tag.Tag, error) {
+ ret := _m.Called(ctx, id)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Get")
+ }
+
+ var r0 []tag.Tag
+ var r1 error
+ if rf, ok := ret.Get(0).(func(context.Context, uint32) ([]tag.Tag, error)); ok {
+ return rf(ctx, id)
+ }
+ if rf, ok := ret.Get(0).(func(context.Context, uint32) []tag.Tag); ok {
+ r0 = rf(ctx, id)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).([]tag.Tag)
+ }
+ }
+
+ if rf, ok := ret.Get(1).(func(context.Context, uint32) error); ok {
+ r1 = rf(ctx, id)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// TagRepo_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'
+type TagRepo_Get_Call struct {
+ *mock.Call
+}
+
+// Get is a helper method to define mock.On call
+// - ctx context.Context
+// - id uint32
+func (_e *TagRepo_Expecter) Get(ctx interface{}, id interface{}) *TagRepo_Get_Call {
+ return &TagRepo_Get_Call{Call: _e.mock.On("Get", ctx, id)}
+}
+
+func (_c *TagRepo_Get_Call) Run(run func(ctx context.Context, id uint32)) *TagRepo_Get_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(context.Context), args[1].(uint32))
+ })
+ return _c
+}
+
+func (_c *TagRepo_Get_Call) Return(_a0 []tag.Tag, _a1 error) *TagRepo_Get_Call {
+ _c.Call.Return(_a0, _a1)
+ return _c
+}
+
+func (_c *TagRepo_Get_Call) RunAndReturn(run func(context.Context, uint32) ([]tag.Tag, error)) *TagRepo_Get_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// GetByIDs provides a mock function with given fields: ctx, ids
+func (_m *TagRepo) GetByIDs(ctx context.Context, ids []uint32) (map[uint32][]tag.Tag, error) {
+ ret := _m.Called(ctx, ids)
+
+ if len(ret) == 0 {
+ panic("no return value specified for GetByIDs")
+ }
+
+ var r0 map[uint32][]tag.Tag
+ var r1 error
+ if rf, ok := ret.Get(0).(func(context.Context, []uint32) (map[uint32][]tag.Tag, error)); ok {
+ return rf(ctx, ids)
+ }
+ if rf, ok := ret.Get(0).(func(context.Context, []uint32) map[uint32][]tag.Tag); ok {
+ r0 = rf(ctx, ids)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(map[uint32][]tag.Tag)
+ }
+ }
+
+ if rf, ok := ret.Get(1).(func(context.Context, []uint32) error); ok {
+ r1 = rf(ctx, ids)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// TagRepo_GetByIDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByIDs'
+type TagRepo_GetByIDs_Call struct {
+ *mock.Call
+}
+
+// GetByIDs is a helper method to define mock.On call
+// - ctx context.Context
+// - ids []uint32
+func (_e *TagRepo_Expecter) GetByIDs(ctx interface{}, ids interface{}) *TagRepo_GetByIDs_Call {
+ return &TagRepo_GetByIDs_Call{Call: _e.mock.On("GetByIDs", ctx, ids)}
+}
+
+func (_c *TagRepo_GetByIDs_Call) Run(run func(ctx context.Context, ids []uint32)) *TagRepo_GetByIDs_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(context.Context), args[1].([]uint32))
+ })
+ return _c
+}
+
+func (_c *TagRepo_GetByIDs_Call) Return(_a0 map[uint32][]tag.Tag, _a1 error) *TagRepo_GetByIDs_Call {
+ _c.Call.Return(_a0, _a1)
+ return _c
+}
+
+func (_c *TagRepo_GetByIDs_Call) RunAndReturn(run func(context.Context, []uint32) (map[uint32][]tag.Tag, error)) *TagRepo_GetByIDs_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// NewTagRepo creates a new instance of TagRepo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewTagRepo(t interface {
+ mock.TestingT
+ Cleanup(func())
+}) *TagRepo {
+ mock := &TagRepo{}
+ mock.Mock.Test(t)
+
+ t.Cleanup(func() { mock.AssertExpectations(t) })
+
+ return mock
+}
diff --git a/internal/mocks/UserRepo.go b/internal/mocks/UserRepo.go
index ee0c909d2..cea8aa675 100644
--- a/internal/mocks/UserRepo.go
+++ b/internal/mocks/UserRepo.go
@@ -385,6 +385,63 @@ func (_c *UserRepo_GetFriends_Call) RunAndReturn(run func(context.Context, uint3
return _c
}
+// GetFullUser provides a mock function with given fields: ctx, userID
+func (_m *UserRepo) GetFullUser(ctx context.Context, userID uint32) (user.FullUser, error) {
+ ret := _m.Called(ctx, userID)
+
+ if len(ret) == 0 {
+ panic("no return value specified for GetFullUser")
+ }
+
+ var r0 user.FullUser
+ var r1 error
+ if rf, ok := ret.Get(0).(func(context.Context, uint32) (user.FullUser, error)); ok {
+ return rf(ctx, userID)
+ }
+ if rf, ok := ret.Get(0).(func(context.Context, uint32) user.FullUser); ok {
+ r0 = rf(ctx, userID)
+ } else {
+ r0 = ret.Get(0).(user.FullUser)
+ }
+
+ if rf, ok := ret.Get(1).(func(context.Context, uint32) error); ok {
+ r1 = rf(ctx, userID)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// UserRepo_GetFullUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFullUser'
+type UserRepo_GetFullUser_Call struct {
+ *mock.Call
+}
+
+// GetFullUser is a helper method to define mock.On call
+// - ctx context.Context
+// - userID uint32
+func (_e *UserRepo_Expecter) GetFullUser(ctx interface{}, userID interface{}) *UserRepo_GetFullUser_Call {
+ return &UserRepo_GetFullUser_Call{Call: _e.mock.On("GetFullUser", ctx, userID)}
+}
+
+func (_c *UserRepo_GetFullUser_Call) Run(run func(ctx context.Context, userID uint32)) *UserRepo_GetFullUser_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(context.Context), args[1].(uint32))
+ })
+ return _c
+}
+
+func (_c *UserRepo_GetFullUser_Call) Return(_a0 user.FullUser, _a1 error) *UserRepo_GetFullUser_Call {
+ _c.Call.Return(_a0, _a1)
+ return _c
+}
+
+func (_c *UserRepo_GetFullUser_Call) RunAndReturn(run func(context.Context, uint32) (user.FullUser, error)) *UserRepo_GetFullUser_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
// NewUserRepo creates a new instance of UserRepo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewUserRepo(t interface {
diff --git a/internal/model/relation.go b/internal/model/relation.go
index 63589d551..b997cda9a 100644
--- a/internal/model/relation.go
+++ b/internal/model/relation.go
@@ -24,6 +24,7 @@ type SubjectPersonRelation struct {
Person Person
Subject Subject
TypeID uint16
+ Eps string
}
type SubjectCharacterRelation struct {
diff --git a/internal/model/subject.go b/internal/model/subject.go
index 577c9e55f..256dc8e02 100644
--- a/internal/model/subject.go
+++ b/internal/model/subject.go
@@ -17,14 +17,16 @@ package model
const subjectLocked = 2
type Tag struct {
- Name string
- Count int
+ Name string
+ Count uint
+ TotalCount uint
}
type Subject struct {
Image string
Summary string
Name string
+ MetaTags string
Date string // first release date
NameCN string
Infobox string
diff --git a/internal/pkg/random/string_internal_test.go b/internal/model/tag.go
similarity index 71%
rename from internal/pkg/random/string_internal_test.go
rename to internal/model/tag.go
index 166cfb5ee..becc2f209 100644
--- a/internal/pkg/random/string_internal_test.go
+++ b/internal/model/tag.go
@@ -12,17 +12,10 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see
-package random
+package model
-import (
- "testing"
+// TagCatSubject 条目tag.
+const TagCatSubject = 0
- "github.com/stretchr/testify/require"
-)
-
-func TestConst(t *testing.T) {
- t.Parallel()
-
- require.EqualValues(t, len(base62Chars), base62CharsLength)
- require.EqualValues(t, 255-(256%len(base62Chars)), base62MaxByte)
-}
+// TagCatMeta 官方tag.
+const TagCatMeta = 3
diff --git a/internal/person/mysql_repository.go b/internal/person/mysql_repository.go
index d14a8284e..95eccc14d 100644
--- a/internal/person/mysql_repository.go
+++ b/internal/person/mysql_repository.go
@@ -45,8 +45,6 @@ func (r mysqlRepo) Get(ctx context.Context, id model.PersonID) (model.Person, er
return model.Person{}, gerr.ErrNotFound
}
- r.log.Error("unexpected error happened", zap.Error(err))
-
return model.Person{}, errgo.Wrap(err, "dal")
}
@@ -59,7 +57,6 @@ func (r mysqlRepo) GetSubjectRelated(
relations, err := r.q.PersonSubjects.WithContext(ctx).
Where(r.q.PersonSubjects.SubjectID.Eq(subjectID)).Find()
if err != nil {
- r.log.Error("unexpected error happened", zap.Error(err))
return nil, errgo.Wrap(err, "dal")
}
@@ -68,6 +65,7 @@ func (r mysqlRepo) GetSubjectRelated(
rel[i] = domain.SubjectPersonRelation{
PersonID: relation.PersonID,
TypeID: relation.PrsnPosition,
+ Eps: relation.PrsnAppearEps,
}
}
@@ -83,7 +81,6 @@ func (r mysqlRepo) GetCharacterRelated(
Order(r.q.Cast.PersonID).
Find()
if err != nil {
- r.log.Error("unexpected error happened", zap.Error(err))
return nil, errgo.Wrap(err, "dal")
}
@@ -103,7 +100,6 @@ func (r mysqlRepo) GetByIDs(ctx context.Context, ids []model.PersonID) (map[mode
u, err := r.q.Person.WithContext(ctx).Joins(r.q.Person.Fields).
Where(r.q.Person.ID.In(ids...)).Find()
if err != nil {
- r.log.Error("unexpected error happened", zap.Error(err))
return nil, errgo.Wrap(err, "dal")
}
diff --git a/internal/person/service.go b/internal/person/service.go
index 7c7b5ead2..83d2948d9 100644
--- a/internal/person/service.go
+++ b/internal/person/service.go
@@ -65,6 +65,7 @@ func (s service) GetSubjectRelated(
Person: persons[rel.PersonID],
Subject: sub,
TypeID: rel.TypeID,
+ Eps: rel.Eps,
}
}
diff --git a/internal/pkg/cache/noop.go b/internal/pkg/cache/noop.go
index 491f46aab..0555f9eaf 100644
--- a/internal/pkg/cache/noop.go
+++ b/internal/pkg/cache/noop.go
@@ -36,3 +36,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, result any) error {
+ return nil
+}
diff --git a/internal/pkg/cache/redis.go b/internal/pkg/cache/redis.go
index 16746dd5f..3ba23db54 100644
--- a/internal/pkg/cache/redis.go
+++ b/internal/pkg/cache/redis.go
@@ -16,9 +16,10 @@ package cache
import (
"context"
+ "reflect"
"time"
- "github.com/redis/go-redis/v9"
+ "github.com/redis/rueidis"
"github.com/trim21/errgo"
"go.uber.org/zap"
@@ -34,31 +35,41 @@ 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 result should be `*[]T`
+ // for example
+ //
+ // var s []struct{}
+ // cache.MGet(ctx, keys, &s)
+ MGet(ctx context.Context, key []string, result any) error
}
// NewRedisCache create a redis backed cache.
-func NewRedisCache(cli *redis.Client) RedisCache {
- return redisCache{r: cli}
+func NewRedisCache(ru rueidis.Client) RedisCache {
+ return redisCache{ru: ru}
}
type redisCache struct {
- r *redis.Client
+ ru rueidis.Client
}
func (c redisCache) Get(ctx context.Context, key string, value any) (bool, error) {
- raw, err := c.r.Get(ctx, key).Bytes()
- if err != nil {
- if err == redis.Nil {
- return false, nil
- }
-
+ result := c.ru.Do(ctx, c.ru.B().Get().Key(key).Build())
+ if err := result.NonRedisError(); err != nil {
return false, errgo.Wrap(err, "redis get")
}
+ raw, err := result.AsBytes()
+ // redis.Nil
+ if err != nil {
+ return false, nil
+ }
+
err = unmarshalBytes(raw, value)
if err != nil {
logger.Warn("can't unmarshal redis cached data as json", zap.String("key", key))
- c.r.Del(ctx, key)
+
+ c.ru.Do(ctx, c.ru.B().Del().Key(key).Build())
return false, nil
}
@@ -66,13 +77,57 @@ func (c redisCache) Get(ctx context.Context, key string, value any) (bool, error
return true, nil
}
-func (c redisCache) Set(ctx context.Context, key string, value any, ttl time.Duration) error {
- b, err := marshalBytes(value)
+// MGet result should be `*[]T`
+// for example
+//
+// var s []struct{}
+// cache.MGet(ctx, keys, &s)
+func (c redisCache) MGet(ctx context.Context, keys []string, result any) error {
+ results, err := c.ru.Do(ctx, c.ru.B().Mget().Key(keys...).Build()).ToArray()
if err != nil {
- return err
+ //nolint:errorlint
+ if err == rueidis.Nil {
+ return nil
+ }
+
+ return errgo.Wrap(err, "redis mget")
+ }
+
+ // no more ideal way to do this
+ // reflect.ValueOf(*[]T)
+ rv := reflect.ValueOf(result)
+ if !(rv.Kind() == reflect.Ptr && rv.Elem().Kind() == reflect.Slice) {
+ panic("only allow *[]T as input")
+ }
+
+ // reflect.ValueOf([]T)
+ vv := rv.Elem()
+
+ vv.Set(reflect.MakeSlice(rv.Elem().Type(), 0, len(results)))
+ elementType := rv.Elem().Type().Elem()
+ for _, message := range results {
+ var v = reflect.New(elementType)
+
+ //nolint:errorlint
+ if message.Error() == rueidis.Nil {
+ continue
+ }
+
+ e := message.DecodeJSON(v.Interface())
+ if e != nil {
+ logger.Warn("unexpected failure when decoding json", zap.Error(e))
+ continue
+ }
+
+ reflect.Append(vv, v.Elem())
}
- if err := c.r.Set(ctx, key, b, ttl).Err(); err != nil {
+ return nil
+}
+
+func (c redisCache) Set(ctx context.Context, key string, value any, ttl time.Duration) error {
+ err := c.ru.Do(ctx, c.ru.B().Set().Key(key).Value(rueidis.JSON(value)).Ex(ttl).Build()).Error()
+ if err != nil {
return errgo.Wrap(err, "redis set")
}
@@ -80,6 +135,6 @@ func (c redisCache) Set(ctx context.Context, key string, value any, ttl time.Dur
}
func (c redisCache) Del(ctx context.Context, keys ...string) error {
- err := c.r.Del(ctx, keys...).Err()
+ err := c.ru.Do(ctx, c.ru.B().Del().Key(keys...).Build()).Error()
return errgo.Wrap(err, "redis.Del")
}
diff --git a/internal/pkg/cache/redis_test.go b/internal/pkg/cache/redis_test.go
index 6d0f89a06..897432311 100644
--- a/internal/pkg/cache/redis_test.go
+++ b/internal/pkg/cache/redis_test.go
@@ -16,11 +16,10 @@ package cache_test
import (
"context"
- "encoding/json"
+ "fmt"
"testing"
"time"
- redismock "github.com/go-redis/redismock/v9"
"github.com/stretchr/testify/require"
"github.com/bangumi/server/internal/pkg/cache"
@@ -32,106 +31,15 @@ type RedisCacheTestItem struct {
I int
}
-func mockedCache() (cache.RedisCache, redismock.ClientMock) {
- db, mock := redismock.NewClientMock()
- c := cache.NewRedisCache(db)
-
- return c, mock
-}
-
-func TestRedisCache_Set(t *testing.T) {
- t.Parallel()
- var key = t.Name() + "redis_key"
- c, mock := mockedCache()
- mock.Regexp().ExpectSet(key, `.*`, time.Hour).SetVal("OK")
-
- value := RedisCacheTestItem{
- S: "sss",
- I: 2,
- }
-
- require.NoError(t, c.Set(context.TODO(), key, value, time.Hour))
-
- if err := mock.ExpectationsWereMet(); err != nil {
- t.Error(err)
- }
-}
-
-func TestRedisCache_Get_Nil(t *testing.T) {
- t.Parallel()
-
- var key = t.Name() + "redis_key"
- c, mock := mockedCache()
- mock.Regexp().ExpectGet(key).RedisNil()
-
- var result RedisCacheTestItem
-
- ok, err := c.Get(context.TODO(), key, &result)
- require.NoError(t, err)
- require.False(t, ok)
-
- if err := mock.ExpectationsWereMet(); err != nil {
- t.Error(err)
- }
-}
-
-func TestRedisCache_Get_Cached(t *testing.T) {
- t.Parallel()
-
- var key = t.Name() + "redis_key"
- value := RedisCacheTestItem{
- S: "sss",
- I: 2,
- }
-
- c, mock := mockedCache()
- encoded, err := json.Marshal(value)
- require.NoError(t, err)
-
- mock.Regexp().ExpectGet(key).SetVal(string(encoded))
-
- var result RedisCacheTestItem
-
- ok, err := c.Get(context.TODO(), key, &result)
- require.NoError(t, err)
- require.True(t, ok)
-
- require.Equal(t, value, result)
-
- if err := mock.ExpectationsWereMet(); err != nil {
- t.Error(err)
- }
-}
-
-func TestRedisCache_Get_Broken(t *testing.T) {
- t.Parallel()
-
- var key = t.Name() + "redis_key"
- c, mock := mockedCache()
-
- mock.Regexp().ExpectGet(key).SetVal("some random broken content")
- mock.Regexp().ExpectDel(key).SetVal(1)
-
- var result RedisCacheTestItem
-
- ok, err := c.Get(context.TODO(), key, &result)
- require.NoError(t, err)
- require.False(t, ok)
-
- if err := mock.ExpectationsWereMet(); err != nil {
- t.Error(err)
- }
-}
-
func TestRedisCache_Real(t *testing.T) {
t.Parallel()
var key = t.Name() + "redis_key"
- db := test.GetRedis(t)
- db.Del(context.TODO(), key)
+ r := test.GetRedis(t)
+ require.NoError(t, r.Do(context.TODO(), r.B().Del().Key(key).Build()).Error())
- c := cache.NewRedisCache(db)
+ c := cache.NewRedisCache(r)
var data = RedisCacheTestItem{S: "ss", I: 5}
require.NoError(t, c.Set(context.TODO(), key, data, time.Hour))
@@ -146,16 +54,16 @@ func TestRedisCache_Real(t *testing.T) {
func TestRedisCache_Del(t *testing.T) {
t.Parallel()
- var key = t.Name() + "redis_test "
+ var key = fmt.Sprintln(t.Name(), "redis_test", time.Now())
- db := test.GetRedis(t)
- require.NoError(t, db.Set(context.Background(), key, "", 0).Err())
+ r := test.GetRedis(t)
+ require.NoError(t, r.Do(context.TODO(), r.B().Set().Key(key).Value("").Build()).Error())
- c := cache.NewRedisCache(db)
+ c := cache.NewRedisCache(r)
require.NoError(t, c.Del(context.Background(), key))
- v, err := db.Exists(context.Background(), key).Result()
+ exist, err := r.Do(context.TODO(), r.B().Exists().Key(key).Build()).AsBool()
require.NoError(t, err)
- require.True(t, v == 0)
+ require.False(t, exist)
}
diff --git a/internal/pkg/cache/serialize.go b/internal/pkg/cache/serialize.go
index 5110d84ae..6ca0cd2b9 100644
--- a/internal/pkg/cache/serialize.go
+++ b/internal/pkg/cache/serialize.go
@@ -20,15 +20,6 @@ import (
"github.com/trim21/errgo"
)
-func marshalBytes(v any) ([]byte, error) {
- b, err := json.Marshal(v)
- if err != nil {
- return nil, errgo.Wrap(err, "json.Marshal")
- }
-
- return b, nil
-}
-
func unmarshalBytes(b []byte, v any) error {
err := json.Unmarshal(b, v)
if err != nil {
diff --git a/internal/pkg/dam/dam.go b/internal/pkg/dam/dam.go
index 81eb59dd0..719577802 100644
--- a/internal/pkg/dam/dam.go
+++ b/internal/pkg/dam/dam.go
@@ -16,7 +16,6 @@ package dam
import (
"regexp"
- "strings"
"unicode"
"github.com/trim21/errgo"
@@ -34,21 +33,21 @@ func New(c config.AppConfig) (Dam, error) {
var cc Dam
var err error
if c.NsfwWord != "" {
- cc.nsfwWord, err = regexp.Compile(c.NsfwWord)
+ cc.nsfwWord, err = regexp.Compile("(?i)" + c.NsfwWord)
if err != nil {
return Dam{}, errgo.Wrap(err, "nsfw_word")
}
}
if c.DisableWords != "" {
- cc.disableWord, err = regexp.Compile(c.DisableWords)
+ cc.disableWord, err = regexp.Compile("(?i)" + c.DisableWords)
if err != nil {
return Dam{}, errgo.Wrap(err, "disable_words")
}
}
if c.BannedDomain != "" {
- cc.bannedDomain, err = regexp.Compile(c.BannedDomain)
+ cc.bannedDomain, err = regexp.Compile("(?i)" + c.BannedDomain)
if err != nil {
return Dam{}, errgo.Wrap(err, "banned_domain")
}
@@ -65,8 +64,6 @@ func (d Dam) NeedReview(text string) bool {
return false
}
- text = strings.ToLower(text)
-
return d.disableWord.MatchString(text)
}
@@ -96,3 +93,26 @@ func AllPrintableChar(text string) bool {
return true
}
+
+var ZeroWidthPattern = regexp.MustCompile(`[^\t\r\n\p{L}\p{M}\p{N}\p{P}\p{S}\p{Z}]`)
+var ExtraSpacePattern = regexp.MustCompile("[\u3000 ]")
+
+func ValidateTag(t string) bool {
+ if len(t) == 0 {
+ return false
+ }
+
+ if !AllPrintableChar(t) {
+ return false
+ }
+
+ if ZeroWidthPattern.MatchString(t) {
+ return false
+ }
+
+ if ExtraSpacePattern.MatchString(t) {
+ return false
+ }
+
+ return true
+}
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..8b943d7ba 100644
--- a/internal/pkg/driver/redis.go
+++ b/internal/pkg/driver/redis.go
@@ -15,51 +15,30 @@
package driver
import (
- "context"
- "time"
+ "fmt"
+ "net/url"
- "github.com/redis/go-redis/v9"
- "github.com/trim21/errgo"
- "go.uber.org/zap"
+ "github.com/redis/rueidis"
"github.com/bangumi/server/config"
- "github.com/bangumi/server/internal/metrics"
- "github.com/bangumi/server/internal/pkg/logger"
)
-const defaultRedisPoolSize = 4
-
-// NewRedisClient create a redis client
-// use [test.GetRedis] in tests.
-func NewRedisClient(c config.AppConfig) (*redis.Client, error) {
- redisOptions, err := redis.ParseURL(c.RedisURL)
+func NewRueidisClient(c config.AppConfig) (rueidis.Client, error) {
+ u, err := url.Parse(c.RedisURL)
if err != nil {
- logger.Fatal("redis: failed to parse redis url", zap.String("url", c.RedisURL))
- }
-
- if redisOptions.PoolSize == 0 {
- redisOptions.PoolSize = defaultRedisPoolSize
+ return nil, err
}
- cli := redis.NewClient(redisOptions)
-
- ctx, cancel := context.WithTimeout(context.Background(), time.Second)
- defer cancel()
-
- if err := cli.Ping(ctx).Err(); err != nil {
- return nil, errgo.Wrap(err, "redis: failed to ping")
- }
-
- return cli, nil
-}
-
-func NewRedisClientWithMetrics(c config.AppConfig) (*redis.Client, error) {
- cli, err := NewRedisClient(c)
+ password, _ := u.User.Password()
+ cli, err := rueidis.NewClient(rueidis.ClientOption{
+ InitAddress: []string{fmt.Sprintf("%s:%s", u.Hostname(), u.Port())},
+ Password: password,
+ // 1<<2 = 4 connection for each node
+ PipelineMultiplex: 2,
+ })
if err != nil {
return cli, err
}
- cli.AddHook(metrics.RedisHook(cli.Options().Addr))
-
return cli, nil
}
diff --git a/internal/pkg/driver/s3.go b/internal/pkg/driver/s3.go
index 97499fe68..84a943349 100644
--- a/internal/pkg/driver/s3.go
+++ b/internal/pkg/driver/s3.go
@@ -15,30 +15,24 @@
package driver
import (
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/credentials"
- "github.com/aws/aws-sdk-go/aws/session"
- "github.com/aws/aws-sdk-go/service/s3"
- "github.com/samber/lo"
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/credentials"
+ "github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/bangumi/server/config"
)
-func NewS3(c config.AppConfig) (*s3.S3, error) {
+func NewS3(c config.AppConfig) (*s3.Client, error) {
if c.S3EntryPoint == "" {
return nil, nil //nolint:nilnil
}
- cred := credentials.NewStaticCredentials(c.S3AccessKey, c.S3SecretKey, "")
- s := lo.Must(session.NewSession(&aws.Config{
- Credentials: cred,
- Endpoint: &c.S3EntryPoint,
- Region: lo.ToPtr("us-east-1"),
- DisableSSL: lo.ToPtr(true),
- S3ForcePathStyle: lo.ToPtr(true),
- }))
-
- svc := s3.New(s)
+ svc := s3.New(s3.Options{
+ BaseEndpoint: aws.String(c.S3EntryPoint),
+ Region: "us-east-1",
+ UsePathStyle: true,
+ Credentials: credentials.NewStaticCredentialsProvider(c.S3AccessKey, c.S3SecretKey, ""),
+ })
return svc, nil
}
diff --git a/internal/pkg/generic/set/set_test.go b/internal/pkg/generic/set/set_test.go
index 5c8034303..cad2a3b41 100644
--- a/internal/pkg/generic/set/set_test.go
+++ b/internal/pkg/generic/set/set_test.go
@@ -43,7 +43,6 @@ func TestFromSlice(t *testing.T) {
{"init without values", []string{}},
}
for _, tc := range testcases {
- tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
s := set.FromSlice[string](tc.input)
diff --git a/internal/pkg/gstr/parse.go b/internal/pkg/gstr/parse.go
index 7b637447a..84287ed13 100644
--- a/internal/pkg/gstr/parse.go
+++ b/internal/pkg/gstr/parse.go
@@ -20,14 +20,38 @@ import (
"github.com/trim21/errgo"
)
+func ParseInt8(s string) (int8, error) {
+ v, err := strconv.ParseInt(s, 10, 8)
+
+ return int8(v), errgo.Wrap(err, "strconv")
+}
+
func ParseUint8(s string) (uint8, error) {
v, err := strconv.ParseUint(s, 10, 8)
return uint8(v), errgo.Wrap(err, "strconv")
}
+func ParseUint16(s string) (uint16, error) {
+ v, err := strconv.ParseUint(s, 10, 16)
+
+ return uint16(v), errgo.Wrap(err, "strconv")
+}
+
+func ParseInt32(s string) (int32, error) {
+ v, err := strconv.ParseInt(s, 10, 32)
+
+ return int32(v), errgo.Wrap(err, "strconv")
+}
+
func ParseUint32(s string) (uint32, error) {
v, err := strconv.ParseUint(s, 10, 32)
return uint32(v), errgo.Wrap(err, "strconv")
}
+
+func ParseBool(s string) (bool, error) {
+ v, err := strconv.ParseBool(s)
+
+ return v, errgo.Wrap(err, "strconv")
+}
diff --git a/internal/pkg/logger/ctx.go b/internal/pkg/logger/ctx.go
index 6fcc9694f..ca71d5442 100644
--- a/internal/pkg/logger/ctx.go
+++ b/internal/pkg/logger/ctx.go
@@ -23,17 +23,22 @@ import (
// https://github.com/uber-go/zap/issues/654
+// make RequestKey and unique.
+type key string
+
//nolint:gochecknoglobals
-var RequestKey = &struct{}{}
+const RequestKey key = "logger.contextKey"
type RequestTrace struct {
IP string
ReqID string
+ Path string
}
func (r *RequestTrace) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddString("ip", r.IP)
enc.AddString("request-id", r.ReqID)
+ enc.AddString("path", r.Path)
return nil
}
diff --git a/internal/pkg/random/string.go b/internal/pkg/random/string.go
index fb1524822..600cb653b 100644
--- a/internal/pkg/random/string.go
+++ b/internal/pkg/random/string.go
@@ -28,8 +28,8 @@ var p = pool.New(func() *bufio.Reader {
// we may never need to change these values.
const base62Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
-const base62CharsLength = 62 // len(base62Chars)
-const base62MaxByte byte = 255 - (256 % base62CharsLength) //nolint:gomnd
+const base62CharsLength = byte(len(base62Chars))
+const base62MaxByte = byte(255 - (256 % len(base62Chars))) //nolint:mnd
// Base62String generate a cryptographically secure base62 string in given length.
// Will panic if it can't read from 'crypto/rand'.
@@ -39,7 +39,7 @@ func Base62String(length int) string {
b := make([]byte, length)
// storage for random bytes.
- r := make([]byte, length+(length/4)) //nolint:gomnd
+ r := make([]byte, length+(length/4)) //nolint:mnd
i := 0
for {
diff --git a/internal/pkg/test/fx.go b/internal/pkg/test/fx.go
new file mode 100644
index 000000000..6db211382
--- /dev/null
+++ b/internal/pkg/test/fx.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 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(tb testing.TB, target ...fx.Option) {
+ tb.Helper()
+ err := fx.New(
+ append(target, fx.NopLogger,
+
+ // driver and connector
+ fx.Provide(
+ config.AppConfigReader(config.AppTypeHTTP),
+ 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(tb, err)
+}
diff --git a/internal/pkg/test/gorm.go b/internal/pkg/test/gorm.go
index 83985d504..0424f24af 100644
--- a/internal/pkg/test/gorm.go
+++ b/internal/pkg/test/gorm.go
@@ -44,7 +44,7 @@ func GetQuery(tb testing.TB) *query.Query {
func GetGorm(tb testing.TB) *gorm.DB {
tb.Helper()
- RequireEnv(tb, EnvRedis)
+ RequireEnv(tb, EnvMysql)
cfg, err := config.NewAppConfig()
require.NoError(tb, err)
@@ -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/pkg/test/redis.go b/internal/pkg/test/redis.go
index 1ae557634..ab61e0969 100644
--- a/internal/pkg/test/redis.go
+++ b/internal/pkg/test/redis.go
@@ -17,22 +17,15 @@ package test
import (
"testing"
- "github.com/redis/go-redis/v9"
- "github.com/stretchr/testify/require"
-
- "github.com/bangumi/server/config"
- "github.com/bangumi/server/internal/pkg/driver"
+ "github.com/redis/rueidis"
+ "go.uber.org/fx"
)
-func GetRedis(tb testing.TB) *redis.Client {
+func GetRedis(tb testing.TB) (r rueidis.Client) {
tb.Helper()
-
RequireEnv(tb, EnvRedis)
- cfg, err := config.NewAppConfig()
- require.NoError(tb, err)
- db, err := driver.NewRedisClient(cfg)
- require.NoError(tb, err)
+ Fx(tb, fx.Populate(&r))
- return db
+ return
}
diff --git a/internal/pkg/test/web.go b/internal/pkg/test/web.go
index 1984e3b15..79cca3023 100644
--- a/internal/pkg/test/web.go
+++ b/internal/pkg/test/web.go
@@ -43,6 +43,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"
@@ -56,6 +57,7 @@ type Mock struct {
PersonRepo person.Repo
CharacterRepo character.Repo
AuthRepo auth.Repo
+ TagRepo tag.Repo
AuthService auth.Service
EpisodeRepo episode.Repo
UserRepo user.Repo
@@ -108,6 +110,7 @@ func GetWebApp(tb testing.TB, m Mock) *echo.Echo {
MockNoticationRepo(m.NotificationRepo),
MockSessionManager(m.SessionManager),
MockTimeLineSrv(m.TimeLineSrv),
+ MockTagRepo(m.TagRepo),
// don't need a default mock for these repositories.
fx.Provide(func() collections.Repo { return m.CollectionRepo }),
@@ -237,6 +240,18 @@ func MockAuthRepo(m auth.Repo) fx.Option {
return fx.Provide(func() auth.Repo { return m })
}
+func MockTagRepo(m tag.Repo) fx.Option {
+ if m == nil {
+ mocker := &mocks.TagRepo{}
+ mocker.EXPECT().Get(mock.Anything, mock.Anything).Return([]tag.Tag{}, nil)
+ mocker.EXPECT().GetByIDs(mock.Anything, mock.Anything).Return(map[model.SubjectID][]tag.Tag{}, nil)
+
+ m = mocker
+ }
+
+ return fx.Provide(func() tag.Repo { return m }, func() tag.CachedRepo { return m })
+}
+
func MockAuthService(m auth.Service) fx.Option {
if m == nil {
return fx.Provide(auth.NewService)
diff --git a/internal/revision/mysql_repo_episode.go b/internal/revision/mysql_repo_episode.go
index 291098469..a47827f67 100644
--- a/internal/revision/mysql_repo_episode.go
+++ b/internal/revision/mysql_repo_episode.go
@@ -76,7 +76,7 @@ func (r mysqlRepo) GetEpisodeRelated(ctx context.Context, id model.RevisionID) (
r.log.Error("can't find revision text", zap.Uint32("id", revision.TextID))
return model.EpisodeRevision{}, gerr.ErrNotFound
}
- r.log.Error("unexpected error happened", zap.Error(err))
+
return model.EpisodeRevision{}, errgo.Wrap(err, "dal")
}
diff --git a/internal/revision/mysql_repository.go b/internal/revision/mysql_repository.go
index 37b975c3d..99b7b337d 100644
--- a/internal/revision/mysql_repository.go
+++ b/internal/revision/mysql_repository.go
@@ -84,7 +84,7 @@ func (r mysqlRepo) GetPersonRelated(ctx context.Context, id model.RevisionID) (m
if errors.Is(err, gorm.ErrRecordNotFound) {
return model.PersonRevision{}, gerr.ErrNotFound
}
- r.log.Error("unexpected error happened", zap.Error(err))
+
return model.PersonRevision{}, errgo.Wrap(err, "dal")
}
data, err := r.q.RevisionText.WithContext(ctx).
@@ -95,7 +95,6 @@ func (r mysqlRepo) GetPersonRelated(ctx context.Context, id model.RevisionID) (m
return model.PersonRevision{}, gerr.ErrNotFound
}
- r.log.Error("unexpected error happened", zap.Error(err))
return model.PersonRevision{}, errgo.Wrap(err, "dal")
}
return convertPersonRevisionDao(revision, data), nil
@@ -148,7 +147,7 @@ func (r mysqlRepo) GetCharacterRelated(ctx context.Context, id model.RevisionID)
r.log.Error("can't find revision text", zap.Uint32("id", revision.TextID))
return model.CharacterRevision{}, gerr.ErrNotFound
}
- r.log.Error("unexpected error happened", zap.Error(err))
+
return model.CharacterRevision{}, errgo.Wrap(err, "dal")
}
return convertCharacterRevisionDao(revision, data), nil
@@ -199,7 +198,6 @@ func (r mysqlRepo) GetSubjectRelated(ctx context.Context, id model.RevisionID) (
return model.SubjectRevision{}, gerr.ErrNotFound
}
- r.log.Error("unexpected error happened", zap.Error(err))
return model.SubjectRevision{}, errgo.Wrap(err, "dal")
}
return convertSubjectRevisionDao(revision, true), nil
diff --git a/internal/search/character/client.go b/internal/search/character/client.go
new file mode 100644
index 000000000..83e926243
--- /dev/null
+++ b/internal/search/character/client.go
@@ -0,0 +1,110 @@
+package character
+
+import (
+ "context"
+ "fmt"
+ "reflect"
+ "strconv"
+
+ "github.com/meilisearch/meilisearch-go"
+ "github.com/trim21/errgo"
+ "github.com/trim21/pkg/queue"
+ "go.uber.org/zap"
+
+ "github.com/bangumi/server/config"
+ "github.com/bangumi/server/dal/query"
+ "github.com/bangumi/server/internal/character"
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/search/searcher"
+)
+
+const (
+ idx = "characters"
+)
+
+func New(
+ cfg config.AppConfig,
+ meili meilisearch.ServiceManager,
+ repo character.Repo,
+ log *zap.Logger,
+ query *query.Query,
+) (searcher.Searcher, error) {
+ if repo == nil {
+ return nil, fmt.Errorf("nil characterRepo")
+ }
+ c := &client{
+ meili: meili,
+ repo: repo,
+ index: meili.Index(idx),
+ log: log.Named("search").With(zap.String("index", idx)),
+ q: query,
+ }
+
+ if cfg.AppType != config.AppTypeCanal {
+ return c, nil
+ }
+
+ return c, c.canalInit(cfg)
+}
+
+type client struct {
+ repo character.Repo
+ index meilisearch.IndexManager
+
+ meili meilisearch.ServiceManager
+ log *zap.Logger
+ q *query.Query
+
+ queue *queue.Batched[searcher.Document]
+}
+
+func (c *client) Close() {
+ if c.queue != nil {
+ c.queue.Close()
+ }
+}
+
+func (c *client) canalInit(cfg config.AppConfig) error {
+ if err := searcher.ValidateConfigs(cfg); err != nil {
+ return errgo.Wrap(err, "validate search config")
+ }
+ c.queue = searcher.NewBatchQueue(cfg, c.log, c.index)
+ searcher.RegisterQueueMetrics(idx, c.queue)
+ shouldCreateIndex, err := searcher.NeedFirstRun(c.meili, idx)
+ if err != nil {
+ return err
+ }
+ if shouldCreateIndex {
+ go c.firstRun()
+ }
+ return nil
+}
+
+//nolint:funlen
+func (c *client) firstRun() {
+ c.log.Info("search initialize")
+ rt := reflect.TypeOf(document{})
+ searcher.InitIndex(c.log, c.meili, idx, rt, rankRule())
+
+ ctx := context.Background()
+
+ maxItem, err := c.q.Character.WithContext(ctx).Limit(1).Order(c.q.Character.ID.Desc()).Take()
+ if err != nil {
+ c.log.Fatal("failed to get current max id", zap.Error(err))
+ return
+ }
+
+ c.log.Info(fmt.Sprintf("run full search index with max %s id %d", idx, maxItem.ID))
+
+ width := len(strconv.Itoa(int(maxItem.ID)))
+ for i := model.CharacterID(1); i <= maxItem.ID; i++ {
+ if i%10000 == 0 {
+ c.log.Info(fmt.Sprintf("progress %*d/%d", width, i, maxItem.ID))
+ }
+
+ err := c.OnUpdate(ctx, i)
+ if err != nil {
+ c.log.Error("error when updating", zap.Error(err))
+ }
+ }
+}
diff --git a/internal/search/character/doc.go b/internal/search/character/doc.go
new file mode 100644
index 000000000..1e1a23de8
--- /dev/null
+++ b/internal/search/character/doc.go
@@ -0,0 +1,51 @@
+package character
+
+import (
+ "strconv"
+
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/search/searcher"
+ "github.com/bangumi/server/pkg/wiki"
+)
+
+type document struct {
+ ID model.CharacterID `json:"id"`
+ Name string `json:"name" searchable:"true"`
+ Aliases []string `json:"aliases,omitempty" searchable:"true"`
+ Comment uint32 `json:"comment" sortable:"true"`
+ Collect uint32 `json:"collect" sortable:"true"`
+ NSFW bool `json:"nsfw" filterable:"true"`
+}
+
+func (d *document) GetID() string {
+ return strconv.FormatUint(uint64(d.ID), 10)
+}
+
+func rankRule() *[]string {
+ return &[]string{
+ // 相似度最优先
+ "exactness",
+ "words",
+ "typo",
+ "proximity",
+ "attribute",
+ "sort",
+ "id:asc",
+ "comment:desc",
+ "collect:desc",
+ "nsfw:asc",
+ }
+}
+
+func extract(c *model.Character) searcher.Document {
+ w := wiki.ParseOmitError(c.Infobox)
+
+ return &document{
+ ID: c.ID,
+ Name: c.Name,
+ Aliases: searcher.ExtractAliases(w),
+ Comment: c.CommentCount,
+ Collect: c.CollectCount,
+ NSFW: c.NSFW,
+ }
+}
diff --git a/internal/search/character/event.go b/internal/search/character/event.go
new file mode 100644
index 000000000..10c3dad66
--- /dev/null
+++ b/internal/search/character/event.go
@@ -0,0 +1,57 @@
+package character
+
+import (
+ "context"
+ "errors"
+ "strconv"
+
+ "github.com/trim21/errgo"
+
+ "github.com/bangumi/server/domain/gerr"
+ "github.com/bangumi/server/internal/model"
+)
+
+func (c *client) OnAdded(ctx context.Context, id model.CharacterID) error {
+ s, err := c.repo.Get(ctx, id)
+ if err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return nil
+ }
+ return errgo.Wrap(err, "characterRepo.Get")
+ }
+
+ if s.Redirect != 0 {
+ return c.OnDelete(ctx, id)
+ }
+
+ extracted := extract(&s)
+
+ _, err = c.index.UpdateDocumentsWithContext(ctx, extracted, "id")
+ return err
+}
+
+func (c *client) OnUpdate(ctx context.Context, id model.CharacterID) error {
+ s, err := c.repo.Get(ctx, id)
+ if err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return nil
+ }
+ return errgo.Wrap(err, "characterRepo.Get")
+ }
+
+ if s.Redirect != 0 {
+ return c.OnDelete(ctx, id)
+ }
+
+ extracted := extract(&s)
+
+ c.queue.Push(extracted)
+
+ return nil
+}
+
+func (c *client) OnDelete(ctx context.Context, id model.CharacterID) error {
+ _, err := c.index.DeleteDocumentWithContext(ctx, strconv.FormatUint(uint64(id), 10))
+
+ return errgo.Wrap(err, "search")
+}
diff --git a/internal/search/character/handle.go b/internal/search/character/handle.go
new file mode 100644
index 000000000..660b2704e
--- /dev/null
+++ b/internal/search/character/handle.go
@@ -0,0 +1,128 @@
+package character
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "github.com/labstack/echo/v4"
+ "github.com/meilisearch/meilisearch-go"
+ "github.com/trim21/errgo"
+
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/pkg/generic/slice"
+ "github.com/bangumi/server/internal/pkg/null"
+ "github.com/bangumi/server/web/accessor"
+ "github.com/bangumi/server/web/req"
+ "github.com/bangumi/server/web/res"
+)
+
+const defaultLimit = 10
+const maxLimit = 20
+
+type Req struct {
+ Keyword string `json:"keyword"`
+ Filter ReqFilter `json:"filter"`
+}
+
+type ReqFilter struct { //nolint:musttag
+ NSFW null.Bool `json:"nsfw"`
+}
+
+type hit struct {
+ ID model.CharacterID `json:"id"`
+}
+
+//nolint:funlen
+func (c *client) Handle(ctx echo.Context) error {
+ auth := accessor.GetFromCtx(ctx)
+ q, err := req.GetPageQuerySoftLimit(ctx, defaultLimit, maxLimit)
+ if err != nil {
+ return err
+ }
+
+ var r Req
+ if err = json.NewDecoder(ctx.Request().Body).Decode(&r); err != nil {
+ return res.JSONError(ctx, err)
+ }
+
+ if !auth.AllowNSFW() {
+ r.Filter.NSFW = null.Bool{Set: true, Value: false}
+ }
+
+ result, err := c.doSearch(r.Keyword, filterToMeiliFilter(r.Filter), q.Limit, q.Offset)
+ if err != nil {
+ return errgo.Wrap(err, "search")
+ }
+
+ var hits []hit
+ if err = json.Unmarshal(result.Hits, &hits); err != nil {
+ return errgo.Wrap(err, "json.Unmarshal")
+ }
+ ids := slice.Map(hits, func(h hit) model.SubjectID { return h.ID })
+
+ characters, err := c.repo.GetByIDs(ctx.Request().Context(), ids)
+ if err != nil {
+ return errgo.Wrap(err, "characterRepo.GetByIDs")
+ }
+
+ var data = make([]res.CharacterV0, 0, len(characters))
+ for _, id := range ids {
+ s, ok := characters[id]
+ if !ok {
+ continue
+ }
+ character := res.ConvertModelCharacter(s)
+ data = append(data, character)
+ }
+
+ return ctx.JSON(http.StatusOK, res.Paged{
+ Data: data,
+ Total: result.EstimatedTotalHits,
+ Limit: q.Limit,
+ Offset: q.Offset,
+ })
+}
+
+func (c *client) doSearch(
+ words string,
+ filter [][]string,
+ limit, offset int,
+) (*meiliSearchResponse, error) {
+ if limit == 0 {
+ limit = 10
+ } else if limit > 50 {
+ limit = 50
+ }
+
+ raw, err := c.index.SearchRaw(words, &meilisearch.SearchRequest{
+ Offset: int64(offset),
+ Limit: int64(limit),
+ Filter: filter,
+ })
+ if err != nil {
+ return nil, errgo.Wrap(err, "meilisearch search")
+ }
+
+ var r meiliSearchResponse
+ if err := json.Unmarshal(*raw, &r); err != nil {
+ return nil, errgo.Wrap(err, "json.Unmarshal")
+ }
+
+ return &r, nil
+}
+
+type meiliSearchResponse struct {
+ Hits json.RawMessage `json:"hits"`
+ EstimatedTotalHits int64 `json:"estimatedTotalHits"` //nolint:tagliatelle
+}
+
+func filterToMeiliFilter(req ReqFilter) [][]string {
+ var filter = make([][]string, 0, 1)
+
+ if req.NSFW.Set {
+ filter = append(filter, []string{fmt.Sprintf("nsfw = %t", req.NSFW.Value)})
+ }
+
+ return filter
+}
diff --git a/internal/search/client.go b/internal/search/client.go
deleted file mode 100644
index d1fe8afcc..000000000
--- a/internal/search/client.go
+++ /dev/null
@@ -1,320 +0,0 @@
-// 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 search
-
-import (
- "context"
- "errors"
- "fmt"
- "net/url"
- "os"
- "reflect"
- "strconv"
- "strings"
- "time"
-
- "github.com/avast/retry-go/v4"
- "github.com/meilisearch/meilisearch-go"
- "github.com/prometheus/client_golang/prometheus"
- "github.com/samber/lo"
- "github.com/trim21/errgo"
- "github.com/trim21/pkg/queue"
- "go.uber.org/zap"
-
- "github.com/bangumi/server/config"
- "github.com/bangumi/server/dal/query"
- "github.com/bangumi/server/domain/gerr"
- "github.com/bangumi/server/internal/model"
- "github.com/bangumi/server/internal/subject"
-)
-
-// New provide a search app is AppConfig.MeiliSearchURL is empty string, return nope search client.
-//
-// see `MeiliSearchURL` and `MeiliSearchKey` in [config.AppConfig].
-func New(
- cfg config.AppConfig,
- subjectRepo subject.Repo,
- log *zap.Logger,
- query *query.Query,
-) (Client, error) {
- if cfg.Search.MeiliSearch.URL == "" {
- return NoopClient{}, nil
- }
-
- if subjectRepo == nil {
- panic("nil SubjectRepo")
- }
- if _, err := url.Parse(cfg.Search.MeiliSearch.URL); err != nil {
- return nil, errgo.Wrap(err, "url.Parse")
- }
-
- meili := meilisearch.NewClient(meilisearch.ClientConfig{
- Host: cfg.Search.MeiliSearch.URL,
- APIKey: cfg.Search.MeiliSearch.Key,
- Timeout: cfg.Search.MeiliSearch.Timeout,
- })
-
- if _, err := meili.GetVersion(); err != nil {
- return nil, errgo.Wrap(err, "meilisearch")
- }
-
- c := &client{
- meili: meili,
- q: query,
- subject: "subjects",
- subjectIndex: meili.Index("subjects"),
- log: log.Named("search"),
- subjectRepo: subjectRepo,
- }
-
- if cfg.AppType != config.AppTypeCanal {
- return c, nil
- }
-
- return c, c.canalInit(cfg)
-}
-
-func (c *client) canalInit(cfg config.AppConfig) error {
- if cfg.Search.SearchBatchSize <= 0 {
- // nolint: goerr113
- return fmt.Errorf("config.SearchBatchSize should >= 0, current %d", cfg.Search.SearchBatchSize)
- }
-
- if cfg.Search.SearchBatchInterval <= 0 {
- // nolint: goerr113
- return fmt.Errorf("config.SearchBatchInterval should >= 0, current %d", cfg.Search.SearchBatchInterval)
- }
-
- c.queue = queue.NewBatchedDedupe[subjectIndex](
- c.sendBatch,
- cfg.Search.SearchBatchSize,
- cfg.Search.SearchBatchInterval,
- func(items []subjectIndex) []subjectIndex {
- // lo.UniqBy 会保留第一次出现的元素,reverse 之后会保留新的数据
- return lo.UniqBy(lo.Reverse(items), func(item subjectIndex) model.SubjectID {
- return item.ID
- })
- },
- )
-
- prometheus.DefaultRegisterer.MustRegister(
- prometheus.NewGaugeFunc(
- prometheus.GaugeOpts{
- Namespace: "chii",
- Name: "meilisearch_queue_batch",
- Help: "meilisearch update queue batch size",
- },
- func() float64 {
- return float64(c.queue.Len())
- },
- ))
-
- shouldCreateIndex, err := c.needFirstRun()
- if err != nil {
- return err
- }
-
- if shouldCreateIndex {
- go c.firstRun()
- }
-
- return nil
-}
-
-type client struct {
- subjectRepo subject.Repo
- meili *meilisearch.Client
- q *query.Query
- subjectIndex *meilisearch.Index
- log *zap.Logger
- subject string
- queue *queue.Batched[subjectIndex]
-}
-
-func (c *client) Close() {
- if c.queue != nil {
- c.queue.Close()
- }
-}
-
-// OnSubjectUpdate is the hook called by canal.
-func (c *client) OnSubjectUpdate(ctx context.Context, id model.SubjectID) error {
- s, err := c.subjectRepo.Get(ctx, id, subject.Filter{})
- if err != nil {
- if errors.Is(err, gerr.ErrNotFound) {
- return c.DeleteSubject(ctx, id)
- }
- return errgo.Wrap(err, "subjectRepo.Get")
- }
-
- extracted := extractSubject(&s)
-
- c.queue.Push(extracted)
-
- return nil
-}
-
-// OnSubjectDelete is the hook called by canal.
-func (c *client) OnSubjectDelete(_ context.Context, id model.SubjectID) error {
- _, err := c.subjectIndex.DeleteDocument(strconv.FormatUint(uint64(id), 10))
-
- return errgo.Wrap(err, "search")
-}
-
-// UpsertSubject add subject to search backend.
-func (c *client) sendBatch(items []subjectIndex) {
- c.log.Debug("send batch to meilisearch", zap.Int("len", len(items)))
- err := retry.Do(
- func() error {
- _, err := c.subjectIndex.UpdateDocuments(items, "id")
- return err
- },
- retry.OnRetry(func(n uint, err error) {
- c.log.Warn("failed to send batch", zap.Uint("attempt", n), zap.Error(err))
- }),
-
- retry.DelayType(retry.BackOffDelay),
- retry.Delay(time.Microsecond*100),
- retry.Attempts(5), //nolint:gomnd
- retry.RetryIf(func(err error) bool {
- return errors.As(err, &meilisearch.Error{})
- }),
- )
-
- if err != nil {
- c.log.Error("failed to send batch", zap.Error(err))
- }
-}
-
-func (c *client) DeleteSubject(_ context.Context, id model.SubjectID) error {
- _, err := c.subjectIndex.Delete(strconv.FormatUint(uint64(id), 10))
-
- return errgo.Wrap(err, "delete")
-}
-
-func (c *client) needFirstRun() (bool, error) {
- if os.Getenv("CHII_SEARCH_INIT") == "true" {
- return true, nil
- }
-
- index, err := c.meili.GetIndex("subjects")
- if err != nil {
- var e *meilisearch.Error
- if errors.As(err, &e) {
- return true, nil
- }
- return false, errgo.Wrap(err, "get subjects index")
- }
-
- stat, err := index.GetStats()
- if err != nil {
- return false, errgo.Wrap(err, "get subjects index stats")
- }
-
- return stat.NumberOfDocuments == 0, nil
-}
-
-//nolint:funlen
-func (c *client) firstRun() {
- c.log.Info("search initialize")
- _, err := c.meili.CreateIndex(&meilisearch.IndexConfig{
- Uid: "subjects",
- PrimaryKey: "id",
- })
- if err != nil {
- c.log.Fatal("failed to create search subject index", zap.Error(err))
- return
- }
-
- subjectIndex := c.meili.Index("subjects")
-
- c.log.Info("set sortable attributes", zap.Strings("attributes", *getAttributes("sortable")))
- _, err = subjectIndex.UpdateSortableAttributes(getAttributes("sortable"))
- if err != nil {
- c.log.Fatal("failed to update search index sortable attributes", zap.Error(err))
- return
- }
-
- c.log.Info("set filterable attributes", zap.Strings("attributes", *getAttributes("filterable")))
- _, err = subjectIndex.UpdateFilterableAttributes(getAttributes("filterable"))
- if err != nil {
- c.log.Fatal("failed to update search index filterable attributes", zap.Error(err))
- return
- }
-
- c.log.Info("set searchable attributes", zap.Strings("attributes", *searchAbleAttribute()))
- _, err = subjectIndex.UpdateSearchableAttributes(searchAbleAttribute())
- if err != nil {
- c.log.Fatal("failed to update search index searchable attributes", zap.Error(err))
- return
- }
-
- c.log.Info("set ranking rules", zap.Strings("rule", *rankRule()))
- _, err = subjectIndex.UpdateRankingRules(rankRule())
- if err != nil {
- c.log.Fatal("failed to update search index searchable attributes", zap.Error(err))
- return
- }
-
- ctx := context.Background()
-
- maxSubject, err := c.q.Subject.WithContext(ctx).Limit(1).Order(c.q.Subject.ID.Desc()).Take()
- if err != nil {
- c.log.Fatal("failed to get current max subject id", zap.Error(err))
- return
- }
-
- c.log.Info(fmt.Sprintf("run full search index with max subject id %d", maxSubject.ID))
-
- width := len(strconv.Itoa(int(maxSubject.ID)))
- for i := model.SubjectID(1); i < maxSubject.ID; i++ {
- if i%10000 == 0 {
- c.log.Info(fmt.Sprintf("progress %*d/%d", width, i, maxSubject.ID))
- }
-
- err := c.OnSubjectUpdate(ctx, i)
- if err != nil {
- c.log.Error("error when updating subject", zap.Error(err))
- }
- }
-}
-
-func getAttributes(tag string) *[]string {
- rt := reflect.TypeOf(subjectIndex{})
- var s []string
- for i := 0; i < rt.NumField(); i++ {
- t, ok := rt.Field(i).Tag.Lookup(tag)
- if !ok {
- continue
- }
-
- if t != "true" {
- continue
- }
-
- s = append(s, getJSONFieldName(rt.Field(i)))
- }
-
- return &s
-}
-
-func getJSONFieldName(f reflect.StructField) string {
- t := f.Tag.Get("json")
- if t == "" {
- return f.Name
- }
-
- return strings.Split(t, ",")[0]
-}
diff --git a/internal/search/extractor.common.go b/internal/search/extractor.common.go
deleted file mode 100644
index 7f63e83e0..000000000
--- a/internal/search/extractor.common.go
+++ /dev/null
@@ -1,56 +0,0 @@
-// 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 search
-
-import (
- "github.com/bangumi/server/internal/model"
- "github.com/bangumi/server/pkg/wiki"
-)
-
-func heat(s *model.Subject) uint32 {
- return s.OnHold + s.Doing + s.Dropped + s.Wish + s.Collect
-}
-
-func extractNames(s *model.Subject, w wiki.Wiki) []string {
- var names = make([]string, 0, 3)
- names = append(names, s.Name)
- if s.NameCN != "" {
- names = append(names, s.NameCN)
- }
-
- for _, field := range w.Fields {
- if field.Key == "别名" {
- names = append(names, getValues(field)...)
- }
- }
-
- return names
-}
-
-func getValues(f wiki.Field) []string {
- if f.Null {
- return nil
- }
-
- if !f.Array {
- return []string{f.Value}
- }
-
- var s = make([]string, len(f.Values))
- for i, value := range f.Values {
- s[i] = value.Value
- }
- return s
-}
diff --git a/internal/search/noop.go b/internal/search/noop.go
index 0a929174b..56ca35a8e 100644
--- a/internal/search/noop.go
+++ b/internal/search/noop.go
@@ -19,8 +19,6 @@ import (
"net/http"
"github.com/labstack/echo/v4"
-
- "github.com/bangumi/server/internal/model"
)
var _ Client = NoopClient{}
@@ -28,15 +26,19 @@ var _ Client = NoopClient{}
type NoopClient struct {
}
-func (n NoopClient) Handle(c echo.Context) error {
+func (n NoopClient) Handle(c echo.Context, _ SearchTarget) error {
return c.String(http.StatusOK, "search is not enable")
}
-func (n NoopClient) OnSubjectUpdate(_ context.Context, _ model.SubjectID) error {
+func (n NoopClient) EventAdded(ctx context.Context, _ uint32, _ SearchTarget) error {
+ return nil
+}
+
+func (n NoopClient) EventUpdate(_ context.Context, _ uint32, _ SearchTarget) error {
return nil
}
-func (n NoopClient) OnSubjectDelete(_ context.Context, _ model.SubjectID) error {
+func (n NoopClient) EventDelete(_ context.Context, _ uint32, _ SearchTarget) error {
return nil
}
diff --git a/internal/search/person/client.go b/internal/search/person/client.go
new file mode 100644
index 000000000..a7a454005
--- /dev/null
+++ b/internal/search/person/client.go
@@ -0,0 +1,110 @@
+package person
+
+import (
+ "context"
+ "fmt"
+ "reflect"
+ "strconv"
+
+ "github.com/meilisearch/meilisearch-go"
+ "github.com/trim21/errgo"
+ "github.com/trim21/pkg/queue"
+ "go.uber.org/zap"
+
+ "github.com/bangumi/server/config"
+ "github.com/bangumi/server/dal/query"
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/person"
+ "github.com/bangumi/server/internal/search/searcher"
+)
+
+const (
+ idx = "persons"
+)
+
+func New(
+ cfg config.AppConfig,
+ meili meilisearch.ServiceManager,
+ repo person.Repo,
+ log *zap.Logger,
+ query *query.Query,
+) (searcher.Searcher, error) {
+ if repo == nil {
+ return nil, fmt.Errorf("nil personRepo")
+ }
+ c := &client{
+ meili: meili,
+ repo: repo,
+ index: meili.Index("persons"),
+ log: log.Named("search").With(zap.String("index", idx)),
+ q: query,
+ }
+
+ if cfg.AppType != config.AppTypeCanal {
+ return c, nil
+ }
+
+ return c, c.canalInit(cfg)
+}
+
+type client struct {
+ repo person.Repo
+ index meilisearch.IndexManager
+
+ meili meilisearch.ServiceManager
+ log *zap.Logger
+ q *query.Query
+
+ queue *queue.Batched[searcher.Document]
+}
+
+func (c *client) Close() {
+ if c.queue != nil {
+ c.queue.Close()
+ }
+}
+
+func (c *client) canalInit(cfg config.AppConfig) error {
+ if err := searcher.ValidateConfigs(cfg); err != nil {
+ return errgo.Wrap(err, "validate search config")
+ }
+ c.queue = searcher.NewBatchQueue(cfg, c.log, c.index)
+ searcher.RegisterQueueMetrics(idx, c.queue)
+ shouldCreateIndex, err := searcher.NeedFirstRun(c.meili, idx)
+ if err != nil {
+ return err
+ }
+ if shouldCreateIndex {
+ go c.firstRun()
+ }
+ return nil
+}
+
+//nolint:funlen
+func (c *client) firstRun() {
+ c.log.Info("search initialize")
+ rt := reflect.TypeOf(document{})
+ searcher.InitIndex(c.log, c.meili, idx, rt, rankRule())
+
+ ctx := context.Background()
+
+ maxItem, err := c.q.Person.WithContext(ctx).Limit(1).Order(c.q.Person.ID.Desc()).Take()
+ if err != nil {
+ c.log.Fatal("failed to get current max id", zap.Error(err))
+ return
+ }
+
+ c.log.Info(fmt.Sprintf("run full search index with max %s id %d", idx, maxItem.ID))
+
+ width := len(strconv.Itoa(int(maxItem.ID)))
+ for i := model.PersonID(1); i <= maxItem.ID; i++ {
+ if i%10000 == 0 {
+ c.log.Info(fmt.Sprintf("progress %*d/%d", width, i, maxItem.ID))
+ }
+
+ err := c.OnUpdate(ctx, i)
+ if err != nil {
+ c.log.Error("error when updating", zap.Error(err))
+ }
+ }
+}
diff --git a/internal/search/person/doc.go b/internal/search/person/doc.go
new file mode 100644
index 000000000..c19e3a7ef
--- /dev/null
+++ b/internal/search/person/doc.go
@@ -0,0 +1,49 @@
+package person
+
+import (
+ "strconv"
+
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/search/searcher"
+ "github.com/bangumi/server/pkg/wiki"
+)
+
+type document struct {
+ ID model.PersonID `json:"id"`
+ Name string `json:"name" searchable:"true"`
+ Aliases []string `json:"aliases,omitempty" searchable:"true"`
+ Comment uint32 `json:"comment" sortable:"true"`
+ Collect uint32 `json:"collect" sortable:"true"`
+ Career []string `json:"career,omitempty" filterable:"true"`
+}
+
+func (d *document) GetID() string {
+ return strconv.FormatUint(uint64(d.ID), 10)
+}
+
+func rankRule() *[]string {
+ return &[]string{
+ // 相似度最优先
+ "exactness",
+ "words",
+ "typo",
+ "proximity",
+ "attribute",
+ "sort",
+ "id:asc",
+ "comment:desc",
+ "collect:desc",
+ }
+}
+
+func extract(c *model.Person) searcher.Document {
+ w := wiki.ParseOmitError(c.Infobox)
+
+ return &document{
+ ID: c.ID,
+ Name: c.Name,
+ Aliases: searcher.ExtractAliases(w),
+ Comment: c.CommentCount,
+ Collect: c.CollectCount,
+ }
+}
diff --git a/internal/search/person/event.go b/internal/search/person/event.go
new file mode 100644
index 000000000..467c2fdf2
--- /dev/null
+++ b/internal/search/person/event.go
@@ -0,0 +1,57 @@
+package person
+
+import (
+ "context"
+ "errors"
+ "strconv"
+
+ "github.com/trim21/errgo"
+
+ "github.com/bangumi/server/domain/gerr"
+ "github.com/bangumi/server/internal/model"
+)
+
+func (c *client) OnAdded(ctx context.Context, id model.PersonID) error {
+ s, err := c.repo.Get(ctx, id)
+ if err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return nil
+ }
+ return errgo.Wrap(err, "characterRepo.Get")
+ }
+
+ if s.Redirect != 0 {
+ return c.OnDelete(ctx, id)
+ }
+
+ extracted := extract(&s)
+
+ _, err = c.index.UpdateDocumentsWithContext(ctx, extracted, "id")
+ return err
+}
+
+func (c *client) OnUpdate(ctx context.Context, id model.PersonID) error {
+ s, err := c.repo.Get(ctx, id)
+ if err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return nil
+ }
+ return errgo.Wrap(err, "characterRepo.Get")
+ }
+
+ if s.Redirect != 0 {
+ return c.OnDelete(ctx, id)
+ }
+
+ extracted := extract(&s)
+
+ c.queue.Push(extracted)
+
+ return nil
+}
+
+func (c *client) OnDelete(ctx context.Context, id model.PersonID) error {
+ _, err := c.index.DeleteDocumentWithContext(ctx, strconv.FormatUint(uint64(id), 10))
+
+ return errgo.Wrap(err, "search")
+}
diff --git a/internal/search/person/handle.go b/internal/search/person/handle.go
new file mode 100644
index 000000000..3f4bf1023
--- /dev/null
+++ b/internal/search/person/handle.go
@@ -0,0 +1,121 @@
+package person
+
+import (
+ "encoding/json"
+ "net/http"
+ "strconv"
+
+ "github.com/labstack/echo/v4"
+ "github.com/meilisearch/meilisearch-go"
+ "github.com/trim21/errgo"
+
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/pkg/generic/slice"
+ "github.com/bangumi/server/web/req"
+ "github.com/bangumi/server/web/res"
+)
+
+const defaultLimit = 10
+const maxLimit = 20
+
+type Req struct {
+ Keyword string `json:"keyword"`
+ Filter ReqFilter `json:"filter"`
+}
+
+type ReqFilter struct { //nolint:musttag
+ Careers []string `json:"meta_tags"` // and
+}
+
+type hit struct {
+ ID model.PersonID `json:"id"`
+}
+
+//nolint:funlen
+func (c *client) Handle(ctx echo.Context) error {
+ q, err := req.GetPageQuerySoftLimit(ctx, defaultLimit, maxLimit)
+ if err != nil {
+ return err
+ }
+
+ var r Req
+ if err = json.NewDecoder(ctx.Request().Body).Decode(&r); err != nil {
+ return res.JSONError(ctx, err)
+ }
+
+ result, err := c.doSearch(r.Keyword, filterToMeiliFilter(r.Filter), q.Limit, q.Offset)
+ if err != nil {
+ return errgo.Wrap(err, "search")
+ }
+
+ var hits []hit
+ if err = json.Unmarshal(result.Hits, &hits); err != nil {
+ return errgo.Wrap(err, "json.Unmarshal")
+ }
+ ids := slice.Map(hits, func(h hit) model.SubjectID { return h.ID })
+
+ persons, err := c.repo.GetByIDs(ctx.Request().Context(), ids)
+ if err != nil {
+ return errgo.Wrap(err, "personRepo.GetByIDs")
+ }
+
+ var data = make([]res.PersonV0, 0, len(persons))
+ for _, id := range ids {
+ s, ok := persons[id]
+ if !ok {
+ continue
+ }
+ person := res.ConvertModelPerson(s)
+ data = append(data, person)
+ }
+
+ return ctx.JSON(http.StatusOK, res.Paged{
+ Data: data,
+ Total: result.EstimatedTotalHits,
+ Limit: q.Limit,
+ Offset: q.Offset,
+ })
+}
+
+func (c *client) doSearch(
+ words string,
+ filter [][]string,
+ limit, offset int,
+) (*meiliSearchResponse, error) {
+ if limit == 0 {
+ limit = 10
+ } else if limit > 50 {
+ limit = 50
+ }
+
+ raw, err := c.index.SearchRaw(words, &meilisearch.SearchRequest{
+ Offset: int64(offset),
+ Limit: int64(limit),
+ Filter: filter,
+ })
+ if err != nil {
+ return nil, errgo.Wrap(err, "meilisearch search")
+ }
+
+ var r meiliSearchResponse
+ if err := json.Unmarshal(*raw, &r); err != nil {
+ return nil, errgo.Wrap(err, "json.Unmarshal")
+ }
+
+ return &r, nil
+}
+
+type meiliSearchResponse struct {
+ Hits json.RawMessage `json:"hits"`
+ EstimatedTotalHits int64 `json:"estimatedTotalHits"` //nolint:tagliatelle
+}
+
+func filterToMeiliFilter(req ReqFilter) [][]string {
+ var filter = make([][]string, 0, len(req.Careers))
+
+ for _, career := range req.Careers {
+ filter = append(filter, []string{"career = " + strconv.Quote(career)})
+ }
+
+ return filter
+}
diff --git a/internal/search/search.go b/internal/search/search.go
new file mode 100644
index 000000000..8c80027be
--- /dev/null
+++ b/internal/search/search.go
@@ -0,0 +1,150 @@
+// 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 search
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/labstack/echo/v4"
+ "github.com/meilisearch/meilisearch-go"
+ "github.com/trim21/errgo"
+ "go.uber.org/zap"
+
+ "github.com/bangumi/server/config"
+ "github.com/bangumi/server/dal/query"
+ "github.com/bangumi/server/internal/character"
+ "github.com/bangumi/server/internal/person"
+ characterSearcher "github.com/bangumi/server/internal/search/character"
+ personSearcher "github.com/bangumi/server/internal/search/person"
+ "github.com/bangumi/server/internal/search/searcher"
+ subjectSearcher "github.com/bangumi/server/internal/search/subject"
+ "github.com/bangumi/server/internal/subject"
+)
+
+type SearchTarget string
+
+const (
+ SearchTargetSubject SearchTarget = "subject"
+ SearchTargetCharacter SearchTarget = "character"
+ SearchTargetPerson SearchTarget = "person"
+)
+
+type Client interface {
+ Handle(c echo.Context, target SearchTarget) error
+ Close()
+
+ EventAdded(ctx context.Context, id uint32, target SearchTarget) error
+ EventUpdate(ctx context.Context, id uint32, target SearchTarget) error
+ EventDelete(ctx context.Context, id uint32, target SearchTarget) error
+}
+
+type Handler interface {
+ Handle(c echo.Context, target SearchTarget) error
+}
+
+type Search struct {
+ searchers map[SearchTarget]searcher.Searcher
+}
+
+// New provide a search app is AppConfig.MeiliSearchURL is empty string, return nope search client.
+//
+// see `MeiliSearchURL` and `MeiliSearchKey` in [config.AppConfig].
+func New(
+ cfg config.AppConfig,
+ subjectRepo subject.Repo,
+ characterRepo character.Repo,
+ personRepo person.Repo,
+ log *zap.Logger,
+ query *query.Query,
+) (Client, error) {
+ if cfg.Search.MeiliSearch.URL == "" {
+ return NoopClient{}, nil
+ }
+ if _, err := url.Parse(cfg.Search.MeiliSearch.URL); err != nil {
+ return nil, errgo.Wrap(err, "url.Parse")
+ }
+ meili := meilisearch.New(
+ cfg.Search.MeiliSearch.URL,
+ meilisearch.WithAPIKey(cfg.Search.MeiliSearch.Key),
+ meilisearch.WithCustomClient(&http.Client{Timeout: cfg.Search.MeiliSearch.Timeout}),
+ )
+ if _, err := meili.Version(); err != nil {
+ return nil, errgo.Wrap(err, "meilisearch")
+ }
+
+ subject, err := subjectSearcher.New(cfg, meili, subjectRepo, log, query)
+ if err != nil {
+ return nil, errgo.Wrap(err, "subject search")
+ }
+ character, err := characterSearcher.New(cfg, meili, characterRepo, log, query)
+ if err != nil {
+ return nil, errgo.Wrap(err, "character search")
+ }
+ person, err := personSearcher.New(cfg, meili, personRepo, log, query)
+ if err != nil {
+ return nil, errgo.Wrap(err, "person search")
+ }
+
+ searchers := map[SearchTarget]searcher.Searcher{
+ SearchTargetSubject: subject,
+ SearchTargetCharacter: character,
+ SearchTargetPerson: person,
+ }
+ s := &Search{
+ searchers: searchers,
+ }
+ return s, nil
+}
+
+func (s *Search) Handle(c echo.Context, target SearchTarget) error {
+ searcher := s.searchers[target]
+ if searcher == nil {
+ return fmt.Errorf("searcher not found for %s", target)
+ }
+ return searcher.Handle(c)
+}
+
+func (s *Search) EventAdded(ctx context.Context, id uint32, target SearchTarget) error {
+ searcher := s.searchers[target]
+ if searcher == nil {
+ return fmt.Errorf("searcher not found for %s", target)
+ }
+ return searcher.OnAdded(ctx, id)
+}
+
+func (s *Search) EventUpdate(ctx context.Context, id uint32, target SearchTarget) error {
+ searcher := s.searchers[target]
+ if searcher == nil {
+ return fmt.Errorf("searcher not found for %s", target)
+ }
+ return searcher.OnUpdate(ctx, id)
+}
+
+func (s *Search) EventDelete(ctx context.Context, id uint32, target SearchTarget) error {
+ searcher := s.searchers[target]
+ if searcher == nil {
+ return fmt.Errorf("searcher not found for %s", target)
+ }
+ return searcher.OnDelete(ctx, id)
+}
+
+func (s *Search) Close() {
+ for _, searcher := range s.searchers {
+ searcher.Close()
+ }
+}
diff --git a/internal/search/searcher/client.go b/internal/search/searcher/client.go
new file mode 100644
index 000000000..d18498cb5
--- /dev/null
+++ b/internal/search/searcher/client.go
@@ -0,0 +1,231 @@
+package searcher
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "reflect"
+ "strings"
+ "time"
+
+ "github.com/avast/retry-go/v4"
+ "github.com/labstack/echo/v4"
+ "github.com/meilisearch/meilisearch-go"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/samber/lo"
+ "github.com/trim21/errgo"
+ "github.com/trim21/pkg/queue"
+ "go.uber.org/zap"
+
+ "github.com/bangumi/server/config"
+ "github.com/bangumi/server/pkg/wiki"
+)
+
+type Searcher interface {
+ Handle(c echo.Context) error
+
+ Close()
+
+ OnAdded(ctx context.Context, id uint32) error
+ OnUpdate(ctx context.Context, id uint32) error
+ OnDelete(ctx context.Context, id uint32) error
+}
+
+type Document interface {
+ GetID() string
+}
+
+func NeedFirstRun(meili meilisearch.ServiceManager, idx string) (bool, error) {
+ if os.Getenv("CHII_SEARCH_INIT") == "true" {
+ return true, nil
+ }
+
+ index, err := meili.GetIndex(idx)
+ if err != nil {
+ var e *meilisearch.Error
+ if errors.As(err, &e) {
+ return true, nil
+ }
+ return false, errgo.Wrap(err, fmt.Sprintf("get index %s", idx))
+ }
+
+ stat, err := index.GetStats()
+ if err != nil {
+ return false, errgo.Wrap(err, fmt.Sprintf("get index %s stats", idx))
+ }
+
+ return stat.NumberOfDocuments == 0, nil
+}
+
+func ValidateConfigs(cfg config.AppConfig) error {
+ if cfg.Search.SearchBatchSize <= 0 {
+ // nolint: goerr113
+ return fmt.Errorf("config.SearchBatchSize should >= 0, current %d", cfg.Search.SearchBatchSize)
+ }
+
+ if cfg.Search.SearchBatchInterval <= 0 {
+ // nolint: goerr113
+ return fmt.Errorf("config.SearchBatchInterval should >= 0, current %d", cfg.Search.SearchBatchInterval)
+ }
+
+ return nil
+}
+
+func ExtractAliases(w wiki.Wiki) []string {
+ aliases := []string{}
+ for _, field := range w.Fields {
+ if field.Key == "中文名" {
+ aliases = append(aliases, GetWikiValues(field)...)
+ }
+ if field.Key == "简体中文名" {
+ aliases = append(aliases, GetWikiValues(field)...)
+ }
+ }
+ for _, field := range w.Fields {
+ if field.Key == "别名" {
+ aliases = append(aliases, GetWikiValues(field)...)
+ }
+ }
+ return aliases
+}
+
+func GetWikiValues(f wiki.Field) []string {
+ if f.Null {
+ return nil
+ }
+
+ if !f.Array {
+ return []string{f.Value}
+ }
+
+ var s = make([]string, len(f.Values))
+ for i, value := range f.Values {
+ s[i] = value.Value
+ }
+ return s
+}
+
+func NewSendBatch(log *zap.Logger, index meilisearch.IndexManager) func([]Document) {
+ return func(items []Document) {
+ log.Debug("send batch to meilisearch", zap.Int("len", len(items)))
+ err := retry.Do(
+ func() error {
+ _, err := index.UpdateDocuments(items, "id")
+ return err
+ },
+ retry.OnRetry(func(n uint, err error) {
+ log.Warn("failed to send batch", zap.Uint("attempt", n), zap.Error(err))
+ }),
+ retry.DelayType(retry.BackOffDelay),
+ retry.Delay(time.Microsecond*100),
+ retry.Attempts(5), //nolint:mnd
+ retry.RetryIf(func(err error) bool {
+ var r = &meilisearch.Error{}
+ return errors.As(err, &r)
+ }),
+ )
+ if err != nil {
+ log.Error("failed to send batch", zap.Error(err))
+ }
+ }
+}
+
+func NewDedupeFunc() func([]Document) []Document {
+ return func(items []Document) []Document {
+ // lo.UniqBy 会保留第一次出现的元素,reverse 之后会保留新的数据
+ return lo.UniqBy(lo.Reverse(items), func(item Document) string {
+ return item.GetID()
+ })
+ }
+}
+
+func NewBatchQueue(cfg config.AppConfig, log *zap.Logger, index meilisearch.IndexManager) *queue.Batched[Document] {
+ return queue.NewBatchedDedupe(
+ NewSendBatch(log, index),
+ cfg.Search.SearchBatchSize,
+ cfg.Search.SearchBatchInterval,
+ NewDedupeFunc(),
+ )
+}
+
+func RegisterQueueMetrics(idx string, queue *queue.Batched[Document]) {
+ prometheus.DefaultRegisterer.MustRegister(
+ prometheus.NewGaugeFunc(
+ prometheus.GaugeOpts{
+ Namespace: "chii",
+ Name: "meilisearch_queue_batch",
+ Help: "meilisearch update queue batch size",
+ ConstLabels: prometheus.Labels{
+ "index": idx,
+ },
+ },
+ func() float64 {
+ return float64(queue.Len())
+ },
+ ))
+}
+
+func GetAttributes(rt reflect.Type, tag string) *[]string {
+ var s []string
+ for i := 0; i < rt.NumField(); i++ {
+ t, ok := rt.Field(i).Tag.Lookup(tag)
+ if !ok {
+ continue
+ }
+ if t != "true" {
+ continue
+ }
+ s = append(s, getJSONFieldName(rt.Field(i)))
+ }
+ return &s
+}
+
+func getJSONFieldName(f reflect.StructField) string {
+ t := f.Tag.Get("json")
+ if t == "" {
+ return f.Name
+ }
+ return strings.Split(t, ",")[0]
+}
+
+func InitIndex(log *zap.Logger, meili meilisearch.ServiceManager, idx string, rt reflect.Type, rankRule *[]string) {
+ _, err := meili.CreateIndex(&meilisearch.IndexConfig{
+ Uid: idx,
+ PrimaryKey: "id",
+ })
+ if err != nil {
+ log.Fatal("failed to create search index", zap.Error(err))
+ return
+ }
+
+ index := meili.Index(idx)
+
+ log.Info("set sortable attributes", zap.Strings("attributes", *GetAttributes(rt, "sortable")))
+ _, err = index.UpdateSortableAttributes(GetAttributes(rt, "sortable"))
+ if err != nil {
+ log.Fatal("failed to update search index sortable attributes", zap.Error(err))
+ return
+ }
+
+ log.Info("set filterable attributes", zap.Strings("attributes", *GetAttributes(rt, "filterable")))
+ _, err = index.UpdateFilterableAttributes(GetAttributes(rt, "filterable"))
+ if err != nil {
+ log.Fatal("failed to update search index filterable attributes", zap.Error(err))
+ return
+ }
+
+ log.Info("set searchable attributes", zap.Strings("attributes", *GetAttributes(rt, "searchable")))
+ _, err = index.UpdateSearchableAttributes(GetAttributes(rt, "searchable"))
+ if err != nil {
+ log.Fatal("failed to update search index searchable attributes", zap.Error(err))
+ return
+ }
+
+ log.Info("set ranking rules", zap.Strings("rule", *rankRule))
+ _, err = index.UpdateRankingRules(rankRule)
+ if err != nil {
+ log.Fatal("failed to update search index searchable attributes", zap.Error(err))
+ return
+ }
+}
diff --git a/internal/search/subject/client.go b/internal/search/subject/client.go
new file mode 100644
index 000000000..b7c899740
--- /dev/null
+++ b/internal/search/subject/client.go
@@ -0,0 +1,111 @@
+package subject
+
+import (
+ "context"
+ "fmt"
+ "reflect"
+ "strconv"
+
+ "github.com/meilisearch/meilisearch-go"
+ "github.com/trim21/errgo"
+ "github.com/trim21/pkg/queue"
+ "go.uber.org/zap"
+
+ "github.com/bangumi/server/config"
+ "github.com/bangumi/server/dal/query"
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/search/searcher"
+ "github.com/bangumi/server/internal/subject"
+)
+
+const (
+ idx = "subjects"
+)
+
+func New(
+ cfg config.AppConfig,
+ meili meilisearch.ServiceManager,
+ repo subject.Repo,
+ log *zap.Logger,
+ query *query.Query,
+) (searcher.Searcher, error) {
+ if repo == nil {
+ return nil, fmt.Errorf("nil subjectRepo")
+ }
+ c := &client{
+ meili: meili,
+ repo: repo,
+ index: meili.Index(idx),
+ log: log.Named("search").With(zap.String("index", idx)),
+ q: query,
+ }
+
+ if cfg.AppType != config.AppTypeCanal {
+ return c, nil
+ }
+
+ return c, c.canalInit(cfg)
+}
+
+type client struct {
+ repo subject.Repo
+ index meilisearch.IndexManager
+
+ meili meilisearch.ServiceManager
+ log *zap.Logger
+ q *query.Query
+
+ queue *queue.Batched[searcher.Document]
+}
+
+func (c *client) Close() {
+ if c.queue != nil {
+ c.queue.Close()
+ }
+}
+
+func (c *client) canalInit(cfg config.AppConfig) error {
+ if err := searcher.ValidateConfigs(cfg); err != nil {
+ return errgo.Wrap(err, "validate search config")
+ }
+ c.queue = searcher.NewBatchQueue(cfg, c.log, c.index)
+ searcher.RegisterQueueMetrics(idx, c.queue)
+
+ shouldCreateIndex, err := searcher.NeedFirstRun(c.meili, idx)
+ if err != nil {
+ return err
+ }
+ if shouldCreateIndex {
+ go c.firstRun()
+ }
+ return nil
+}
+
+//nolint:funlen
+func (c *client) firstRun() {
+ c.log.Info("search initialize")
+ rt := reflect.TypeOf(document{})
+ searcher.InitIndex(c.log, c.meili, idx, rt, rankRule())
+
+ ctx := context.Background()
+
+ maxItem, err := c.q.Subject.WithContext(ctx).Limit(1).Order(c.q.Subject.ID.Desc()).Take()
+ if err != nil {
+ c.log.Fatal("failed to get current max id", zap.Error(err))
+ return
+ }
+
+ c.log.Info(fmt.Sprintf("run full search index with max %s id %d", idx, maxItem.ID))
+
+ width := len(strconv.Itoa(int(maxItem.ID)))
+ for i := model.SubjectID(1); i <= maxItem.ID; i++ {
+ if i%10000 == 0 {
+ c.log.Info(fmt.Sprintf("progress %*d/%d", width, i, maxItem.ID))
+ }
+
+ err := c.OnUpdate(ctx, i)
+ if err != nil {
+ c.log.Error("error when updating", zap.Error(err))
+ }
+ }
+}
diff --git a/internal/search/index.go b/internal/search/subject/doc.go
similarity index 74%
rename from internal/search/index.go
rename to internal/search/subject/doc.go
index a48641970..ec7b083a4 100644
--- a/internal/search/index.go
+++ b/internal/search/subject/doc.go
@@ -12,12 +12,14 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see
-package search
+package subject
import (
"strconv"
+ "strings"
"github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/search/searcher"
"github.com/bangumi/server/pkg/wiki"
)
@@ -25,11 +27,12 @@ import (
// 使用 `filterable:"true"`, `sortable:"true"`
// 两种 tag 来设置是否可以被索引和排序.
// 搜索字段因为带有排序,所以定义在 [search.searchAbleAttribute] 中.
-type subjectIndex struct {
+type document struct {
ID model.SubjectID `json:"id"`
- Summary string `json:"summary"`
Tag []string `json:"tag,omitempty" filterable:"true"`
- Name []string `json:"name"`
+ MetaTags []string `json:"meta_tag" filterable:"true"`
+ Name string `json:"name" searchable:"true"`
+ Aliases []string `json:"aliases,omitempty" searchable:"true"`
Date int `json:"date,omitempty" filterable:"true" sortable:"true"`
Score float64 `json:"score" filterable:"true" sortable:"true"`
PageRank float64 `json:"page_rank" sortable:"true"`
@@ -40,14 +43,8 @@ type subjectIndex struct {
NSFW bool `json:"nsfw" filterable:"true"`
}
-func searchAbleAttribute() *[]string {
- return &[]string{
- "name",
- "summary",
- "tag",
- "type",
- "id",
- }
+func (d *document) GetID() string {
+ return strconv.FormatUint(uint64(d.ID), 10)
}
func rankRule() *[]string {
@@ -68,7 +65,11 @@ func rankRule() *[]string {
}
}
-func extractSubject(s *model.Subject) subjectIndex {
+func heat(s *model.Subject) uint32 {
+ return s.OnHold + s.Doing + s.Dropped + s.Wish + s.Collect
+}
+
+func extract(s *model.Subject) searcher.Document {
tags := s.Tags
w := wiki.ParseOmitError(s.Infobox)
@@ -80,11 +81,12 @@ func extractSubject(s *model.Subject) subjectIndex {
tagNames[i] = tag.Name
}
- return subjectIndex{
+ return &document{
ID: s.ID,
- Name: extractNames(s, w),
+ Name: s.Name,
+ Aliases: extractAliases(s, w),
+ MetaTags: strings.Split(s.MetaTags, " "),
Tag: tagNames,
- Summary: s.Summary,
NSFW: s.NSFW,
Type: s.TypeID,
Date: parseDateVal(s.Date),
@@ -96,6 +98,21 @@ func extractSubject(s *model.Subject) subjectIndex {
}
}
+func extractAliases(s *model.Subject, w wiki.Wiki) []string {
+ var aliases = make([]string, 0, 2)
+ if s.NameCN != "" {
+ aliases = append(aliases, s.NameCN)
+ }
+
+ for _, field := range w.Fields {
+ if field.Key == "别名" {
+ aliases = append(aliases, searcher.GetWikiValues(field)...)
+ }
+ }
+
+ return aliases
+}
+
func parseDateVal(date string) int {
if len(date) < 10 {
return 0
diff --git a/internal/search/extract_internal_test.go b/internal/search/subject/doc_internal_test.go
similarity index 98%
rename from internal/search/extract_internal_test.go
rename to internal/search/subject/doc_internal_test.go
index f84874568..6af854169 100644
--- a/internal/search/extract_internal_test.go
+++ b/internal/search/subject/doc_internal_test.go
@@ -12,7 +12,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see
-package search
+package subject
import (
"testing"
diff --git a/internal/search/subject/event.go b/internal/search/subject/event.go
new file mode 100644
index 000000000..268338403
--- /dev/null
+++ b/internal/search/subject/event.go
@@ -0,0 +1,58 @@
+package subject
+
+import (
+ "context"
+ "errors"
+ "strconv"
+
+ "github.com/trim21/errgo"
+
+ "github.com/bangumi/server/domain/gerr"
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/subject"
+)
+
+func (c *client) OnAdded(ctx context.Context, id model.SubjectID) error {
+ s, err := c.repo.Get(ctx, id, subject.Filter{})
+ if err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return nil
+ }
+ return errgo.Wrap(err, "subjectRepo.Get")
+ }
+
+ if s.Redirect != 0 || s.Ban != 0 {
+ return c.OnDelete(ctx, id)
+ }
+
+ extracted := extract(&s)
+
+ _, err = c.index.UpdateDocumentsWithContext(ctx, extracted, "id")
+ return err
+}
+
+func (c *client) OnUpdate(ctx context.Context, id model.SubjectID) error {
+ s, err := c.repo.Get(ctx, id, subject.Filter{})
+ if err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return nil
+ }
+ return errgo.Wrap(err, "subjectRepo.Get")
+ }
+
+ if s.Redirect != 0 || s.Ban != 0 {
+ return c.OnDelete(ctx, id)
+ }
+
+ extracted := extract(&s)
+
+ c.queue.Push(extracted)
+
+ return nil
+}
+
+func (c *client) OnDelete(ctx context.Context, id model.SubjectID) error {
+ _, err := c.index.DeleteDocumentWithContext(ctx, strconv.FormatUint(uint64(id), 10))
+
+ return errgo.Wrap(err, "search")
+}
diff --git a/internal/search/handle.go b/internal/search/subject/handle.go
similarity index 58%
rename from internal/search/handle.go
rename to internal/search/subject/handle.go
index 4404407a8..bc524a8e8 100644
--- a/internal/search/handle.go
+++ b/internal/search/subject/handle.go
@@ -13,10 +13,9 @@
// along with this program. If not, see
// Package search 基于 meilisearch 提供搜索功能
-package search
+package subject
import (
- "context"
"encoding/json"
"fmt"
"net/http"
@@ -25,32 +24,23 @@ import (
"github.com/labstack/echo/v4"
"github.com/meilisearch/meilisearch-go"
+ "github.com/samber/lo"
"github.com/trim21/errgo"
"github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/pkg/compat"
"github.com/bangumi/server/internal/pkg/generic/slice"
"github.com/bangumi/server/internal/pkg/null"
"github.com/bangumi/server/internal/subject"
+ "github.com/bangumi/server/internal/tag"
+ "github.com/bangumi/server/pkg/wiki"
"github.com/bangumi/server/web/accessor"
"github.com/bangumi/server/web/req"
"github.com/bangumi/server/web/res"
)
-type Client interface {
- Handler
- OnSubjectUpdate(ctx context.Context, id model.SubjectID) error
- Close()
- OnSubjectDelete(ctx context.Context, id model.SubjectID) error
-}
-
-// Handler
-// TODO: 想个办法挪到 web 里面去.
-type Handler interface {
- Handle(c echo.Context) error
-}
-
-const defaultLimit = 50
-const maxLimit = 200
+const defaultLimit = 10
+const maxLimit = 20
type Req struct {
Keyword string `json:"keyword"`
@@ -59,12 +49,15 @@ type Req struct {
}
type ReqFilter struct { //nolint:musttag
- Type []model.SubjectType `json:"type"` // or
- Tag []string `json:"tag"` // and
- AirDate []string `json:"air_date"` // and
- Score []string `json:"rating"` // and
- Rank []string `json:"rank"` // and
- NSFW null.Bool `json:"nsfw"`
+ Type []model.SubjectType `json:"type"` // or
+ Tag []string `json:"tag"` // and
+ AirDate []string `json:"air_date"` // and
+ Score []string `json:"rating"` // and
+ Rank []string `json:"rank"` // and
+ MetaTags []string `json:"meta_tags"` // and
+
+ // if NSFW subject is enabled
+ NSFW null.Bool `json:"nsfw"`
}
type hit struct {
@@ -72,21 +65,32 @@ type hit struct {
}
type ReponseSubject struct {
- Date string `json:"date"`
- Image string `json:"image"`
- Type uint8 `json:"type"`
- Summary string `json:"summary"`
- Name string `json:"name"`
- NameCN string `json:"name_cn"`
- Tags []res.SubjectTag `json:"tags"`
- Score float64 `json:"score"`
- ID model.SubjectID `json:"id"`
- Rank uint32 `json:"rank"`
+ Date *string `json:"date"`
+ Platform *string `json:"platform"`
+ Images res.SubjectImages `json:"images"`
+ Image string `json:"image"`
+ Summary string `json:"summary"`
+ Name string `json:"name"`
+ NameCN string `json:"name_cn"`
+ Tags []res.SubjectTag `json:"tags"`
+ Infobox res.V0wiki `json:"infobox"`
+ Rating res.Rating `json:"rating"`
+ Collection res.SubjectCollectionStat `json:"collection"`
+ ID model.SubjectID `json:"id"`
+ Eps uint32 `json:"eps"`
+ MetaTags []string `json:"meta_tags"`
+ Volumes uint32 `json:"volumes"`
+ Series bool `json:"series"`
+ Locked bool `json:"locked"`
+ NSFW bool `json:"nsfw"`
+ TypeID model.SubjectType `json:"type"`
+ Redirect model.SubjectID `json:"-"`
}
+//nolint:funlen
func (c *client) Handle(ctx echo.Context) error {
auth := accessor.GetFromCtx(ctx)
- q, err := req.GetPageQuery(ctx, defaultLimit, maxLimit)
+ q, err := req.GetPageQuerySoftLimit(ctx, defaultLimit, maxLimit)
if err != nil {
return err
}
@@ -97,7 +101,7 @@ func (c *client) Handle(ctx echo.Context) error {
}
if !auth.AllowNSFW() {
- r.Filter.NSFW = null.New(false)
+ r.Filter.NSFW = null.Bool{Set: true, Value: false}
}
result, err := c.doSearch(r.Keyword, filterToMeiliFilter(r.Filter), r.Sort, q.Limit, q.Offset)
@@ -111,29 +115,29 @@ func (c *client) Handle(ctx echo.Context) error {
}
ids := slice.Map(hits, func(h hit) model.SubjectID { return h.ID })
- subjects, err := c.subjectRepo.GetByIDs(ctx.Request().Context(), ids, subject.Filter{NSFW: r.Filter.NSFW})
+ subjects, err := c.repo.GetByIDs(ctx.Request().Context(), ids, subject.Filter{})
if err != nil {
return errgo.Wrap(err, "subjectRepo.GetByIDs")
}
- data := slice.Map(ids, func(id model.SubjectID) ReponseSubject {
- s := subjects[id]
-
- return ReponseSubject{
- Date: s.Date,
- Image: res.SubjectImage(s.Image).Large,
- Type: s.TypeID,
- Summary: s.Summary,
- Name: s.Name,
- NameCN: s.NameCN,
- Tags: slice.Map(s.Tags, func(item model.Tag) res.SubjectTag {
- return res.SubjectTag{Name: item.Name, Count: item.Count}
- }),
- Score: s.Rating.Score,
- ID: s.ID,
- Rank: s.Rating.Rank,
+ var data = make([]ReponseSubject, 0, len(subjects))
+ for _, id := range ids {
+ s, ok := subjects[id]
+ if !ok {
+ continue
}
- })
+ var metaTags []tag.Tag
+
+ for _, t := range strings.Split(s.MetaTags, " ") {
+ if t == "" {
+ continue
+ }
+ metaTags = append(metaTags, tag.Tag{Name: t, Count: 1})
+ }
+
+ subject := toResponseSubject(s, metaTags)
+ data = append(data, subject)
+ }
return ctx.JSON(http.StatusOK, res.Paged{
Data: data,
@@ -168,7 +172,7 @@ func (c *client) doSearch(
return nil, res.BadRequest("sort not supported")
}
- raw, err := c.subjectIndex.SearchRaw(words, &meilisearch.SearchRequest{
+ raw, err := c.index.SearchRaw(words, &meilisearch.SearchRequest{
Offset: int64(offset),
Limit: int64(limit),
Filter: filter,
@@ -205,8 +209,13 @@ func filterToMeiliFilter(req ReqFilter) [][]string {
return fmt.Sprintf("type = %d", s)
}))
}
+
if req.NSFW.Set {
- filter = append(filter, []string{"nsfw = " + strconv.FormatBool(req.NSFW.Value)})
+ filter = append(filter, []string{fmt.Sprintf("nsfw = %t", req.NSFW.Value)})
+ }
+
+ for _, tag := range req.MetaTags {
+ filter = append(filter, []string{"meta_tag = " + strconv.Quote(tag)})
}
// AND
@@ -301,3 +310,58 @@ func isDigitsOnly(s string) bool {
}
return true
}
+
+func toResponseSubject(s model.Subject, metaTags []tag.Tag) ReponseSubject {
+ images := res.SubjectImage(s.Image)
+ return ReponseSubject{
+ ID: s.ID,
+ Image: images.Large,
+ Images: images,
+ Summary: s.Summary,
+ Name: s.Name,
+ Platform: res.PlatformString(s),
+ NameCN: s.NameCN,
+ Date: null.NilString(s.Date),
+ Infobox: compat.V0Wiki(wiki.ParseOmitError(s.Infobox).NonZero()),
+ Volumes: s.Volumes,
+ Redirect: s.Redirect,
+ Eps: s.Eps,
+ MetaTags: lo.Map(metaTags, func(item tag.Tag, index int) string {
+ return item.Name
+ }),
+ Tags: slice.Map(s.Tags, func(tag model.Tag) res.SubjectTag {
+ return res.SubjectTag{
+ Name: tag.Name,
+ Count: tag.Count,
+ }
+ }),
+ Collection: res.SubjectCollectionStat{
+ OnHold: s.OnHold,
+ Wish: s.Wish,
+ Dropped: s.Dropped,
+ Collect: s.Collect,
+ Doing: s.Doing,
+ },
+ TypeID: s.TypeID,
+ Series: s.Series,
+ Locked: s.Locked(),
+ NSFW: s.NSFW,
+ Rating: res.Rating{
+ Rank: s.Rating.Rank,
+ Total: s.Rating.Total,
+ Count: res.Count{
+ Field1: s.Rating.Count.Field1,
+ Field2: s.Rating.Count.Field2,
+ Field3: s.Rating.Count.Field3,
+ Field4: s.Rating.Count.Field4,
+ Field5: s.Rating.Count.Field5,
+ Field6: s.Rating.Count.Field6,
+ Field7: s.Rating.Count.Field7,
+ Field8: s.Rating.Count.Field8,
+ Field9: s.Rating.Count.Field9,
+ Field10: s.Rating.Count.Field10,
+ },
+ Score: s.Rating.Score,
+ },
+ }
+}
diff --git a/internal/search/handle_internal_test.go b/internal/search/subject/handle_internal_test.go
similarity index 92%
rename from internal/search/handle_internal_test.go
rename to internal/search/subject/handle_internal_test.go
index 17f2bb668..5eb9e839f 100644
--- a/internal/search/handle_internal_test.go
+++ b/internal/search/subject/handle_internal_test.go
@@ -12,7 +12,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see
-package search
+package subject
import (
"testing"
@@ -27,10 +27,11 @@ func Test_ReqFilterToMeiliFilter(t *testing.T) {
actual := filterToMeiliFilter(ReqFilter{
Tag: []string{"a", "b"},
- NSFW: null.Bool{Set: false},
+ NSFW: null.Bool{Set: true, Value: false},
})
require.Equal(t, [][]string{
+ {`nsfw = false`},
{`tag = "a"`},
{`tag = "b"`},
}, actual)
diff --git a/internal/search/index_internal_test.go b/internal/search/subject/index_internal_test.go
similarity index 77%
rename from internal/search/index_internal_test.go
rename to internal/search/subject/index_internal_test.go
index a9d218441..1da67e001 100644
--- a/internal/search/index_internal_test.go
+++ b/internal/search/subject/index_internal_test.go
@@ -12,20 +12,24 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see
-package search
+package subject
import (
+ "reflect"
"sort"
"testing"
"github.com/stretchr/testify/require"
+
+ "github.com/bangumi/server/internal/search/searcher"
)
func TestIndexFilter(t *testing.T) {
t.Parallel()
- actual := *(getAttributes("filterable"))
- expected := []string{"date", "score", "rank", "type", "nsfw", "tag"}
+ rt := reflect.TypeOf(document{})
+ actual := *(searcher.GetAttributes(rt, "filterable"))
+ expected := []string{"date", "meta_tag", "score", "rank", "type", "nsfw", "tag"}
sort.Strings(expected)
sort.Strings(actual)
diff --git a/internal/subject/cache_repo.go b/internal/subject/cache_repo.go
index 72ff9da52..c172a87b3 100644
--- a/internal/subject/cache_repo.go
+++ b/internal/subject/cache_repo.go
@@ -39,6 +39,11 @@ type cacheRepo struct {
log *zap.Logger
}
+const (
+ browseCacheTTLFirst = 24 * time.Hour
+ browseCacheTTLOther = time.Hour
+)
+
func (r cacheRepo) Get(ctx context.Context, id model.SubjectID, filter Filter) (model.Subject, error) {
var key = cachekey.Subject(id)
@@ -71,6 +76,66 @@ func (r cacheRepo) GetByIDs(
return r.repo.GetByIDs(ctx, ids, filter)
}
+func (r cacheRepo) Count(ctx context.Context, filter BrowseFilter) (int64, error) {
+ hash, err := filter.Hash()
+ if err != nil {
+ return 0, err
+ }
+ key := cachekey.SubjectBrowseCount(hash)
+
+ var s int64
+ ok, err := r.cache.Get(ctx, key, &s)
+ if err != nil {
+ return s, errgo.Wrap(err, "cache.Get")
+ }
+ if ok {
+ return s, nil
+ }
+
+ s, err = r.repo.Count(ctx, filter)
+ if err != nil {
+ return s, err
+ }
+ if e := r.cache.Set(ctx, key, s, 10*time.Minute); e != nil {
+ r.log.Error("can't set response to cache", zap.Error(e))
+ }
+
+ return s, nil
+}
+
+func (r cacheRepo) Browse(
+ ctx context.Context, filter BrowseFilter, limit, offset int,
+) ([]model.Subject, error) {
+ hash, err := filter.Hash()
+ if err != nil {
+ return nil, err
+ }
+ key := cachekey.SubjectBrowse(hash, limit, offset)
+
+ var subjects []model.Subject
+ ok, err := r.cache.Get(ctx, key, &subjects)
+ if err != nil {
+ return nil, errgo.Wrap(err, "cache.Get")
+ }
+ if ok {
+ return subjects, nil
+ }
+
+ subjects, err = r.repo.Browse(ctx, filter, limit, offset)
+ if err != nil {
+ return nil, err
+ }
+ ttl := browseCacheTTLFirst
+ if offset > 0 {
+ ttl = browseCacheTTLOther
+ }
+ if e := r.cache.Set(ctx, key, subjects, ttl); e != nil {
+ r.log.Error("can't set response to cache", zap.Error(e))
+ }
+
+ return subjects, nil
+}
+
func (r cacheRepo) GetPersonRelated(
ctx context.Context, personID model.PersonID,
) ([]domain.SubjectPersonRelation, error) {
diff --git a/internal/subject/domain.go b/internal/subject/domain.go
index 2fe2c3e8e..cc47e95eb 100644
--- a/internal/subject/domain.go
+++ b/internal/subject/domain.go
@@ -16,6 +16,8 @@ package subject
import (
"context"
+ "fmt"
+ "hash/fnv"
"github.com/bangumi/server/domain"
"github.com/bangumi/server/internal/model"
@@ -23,10 +25,49 @@ import (
)
type Filter struct {
- // if nsfw subject are allowed
NSFW null.Bool
}
+type BrowseFilter struct {
+ NSFW null.Bool
+ Type uint8
+ Category null.Uint16
+ Series null.Bool
+ Platform null.String
+ Sort null.String
+ Year null.Int32
+ Month null.Int8
+}
+
+func (f BrowseFilter) Hash() (string, error) {
+ h := fnv.New64a()
+
+ fmt.Fprintf(h, "type:%v", f.Type)
+ if f.NSFW.Set {
+ fmt.Fprintf(h, "nsfw:%v", f.NSFW)
+ }
+ if f.Category.Set {
+ fmt.Fprintf(h, "category:%v", f.Category)
+ }
+ if f.Series.Set {
+ fmt.Fprintf(h, "series:%v", f.Series)
+ }
+ if f.Platform.Set {
+ fmt.Fprintf(h, "platform:%v", f.Platform)
+ }
+ if f.Sort.Set {
+ fmt.Fprintf(h, "sort:%v", f.Sort)
+ }
+ if f.Year.Set {
+ fmt.Fprintf(h, "year:%v", f.Year)
+ }
+ if f.Month.Set {
+ fmt.Fprintf(h, "month:%v", f.Month)
+ }
+
+ return fmt.Sprintf("%x", h.Sum64()), nil
+}
+
type Repo interface {
read
}
@@ -40,6 +81,9 @@ type read interface {
Get(ctx context.Context, id model.SubjectID, filter Filter) (model.Subject, error)
GetByIDs(ctx context.Context, ids []model.SubjectID, filter Filter) (map[model.SubjectID]model.Subject, error)
+ Count(ctx context.Context, filter BrowseFilter) (int64, error)
+ Browse(ctx context.Context, filter BrowseFilter, limit, offset int) ([]model.Subject, error)
+
GetPersonRelated(ctx context.Context, personID model.PersonID) ([]domain.SubjectPersonRelation, error)
GetCharacterRelated(ctx context.Context, characterID model.CharacterID) ([]domain.SubjectCharacterRelation, error)
GetSubjectRelated(ctx context.Context, subjectID model.SubjectID) ([]domain.SubjectInternalRelation, error)
diff --git a/internal/subject/mysq_repository_compat.go b/internal/subject/mysq_repository_compat.go
index 01a700445..b3c2ef2b4 100644
--- a/internal/subject/mysq_repository_compat.go
+++ b/internal/subject/mysq_repository_compat.go
@@ -23,15 +23,18 @@ import (
)
type Tag struct {
- Name *string `php:"tag_name"`
- Count int `php:"result,string"`
+ Name *string `php:"tag_name"`
+ Count uint `php:"result,string"`
+ TotalCount uint `php:"tag_results,string"`
}
func ParseTags(b []byte) ([]model.Tag, error) {
var tags []Tag
- err := phpserialize.Unmarshal(b, &tags)
- if err != nil {
- return nil, errgo.Wrap(err, "ParseTags: phpserialize.Unmarshal")
+ if len(b) != 0 {
+ err := phpserialize.Unmarshal(b, &tags)
+ if err != nil {
+ return nil, errgo.Wrap(err, "ParseTags: phpserialize.Unmarshal")
+ }
}
return slice.MapFilter(tags, func(item Tag) (model.Tag, bool) {
diff --git a/internal/subject/mysql_repository.go b/internal/subject/mysql_repository.go
index a76a9baa7..3e2a44179 100644
--- a/internal/subject/mysql_repository.go
+++ b/internal/subject/mysql_repository.go
@@ -26,6 +26,7 @@ import (
"github.com/bangumi/server/dal/dao"
"github.com/bangumi/server/dal/query"
+ "github.com/bangumi/server/dal/utiltype"
"github.com/bangumi/server/domain"
"github.com/bangumi/server/domain/gerr"
"github.com/bangumi/server/internal/model"
@@ -53,7 +54,6 @@ func (r mysqlRepo) Get(ctx context.Context, id model.SubjectID, filter Filter) (
return model.Subject{}, fmt.Errorf("%w: %d", gerr.ErrNotFound, id)
}
- r.log.Error("unexpected error happened", zap.Error(err))
return model.Subject{}, errgo.Wrap(err, "dal")
}
@@ -75,12 +75,13 @@ func ConvertDao(s *dao.Subject) (model.Subject, error) {
Redirect: s.Fields.Redirect,
Date: date,
ID: s.ID,
- Name: s.Name,
- NameCN: s.NameCN,
+ Name: string(s.Name),
+ NameCN: string(s.NameCN),
+ MetaTags: s.FieldMetaTags,
TypeID: s.TypeID,
Image: s.Image,
PlatformID: s.Platform,
- Infobox: s.Infobox,
+ Infobox: string(s.Infobox),
Summary: s.Summary,
Volumes: s.Volumes,
Eps: s.Eps,
@@ -136,8 +137,6 @@ func (r mysqlRepo) GetPersonRelated(
Joins(r.q.PersonSubjects.Person).
Where(r.q.PersonSubjects.PersonID.Eq(personID)).Find()
if err != nil {
- r.log.Error("unexpected error happened", zap.Error(err))
-
return nil, errgo.Wrap(err, "dal")
}
@@ -161,7 +160,6 @@ func (r mysqlRepo) GetCharacterRelated(
Joins(r.q.CharacterSubjects.Subject).
Where(r.q.CharacterSubjects.CharacterID.Eq(characterID)).Find()
if err != nil {
- r.log.Error("unexpected error happened", zap.Error(err))
return nil, errgo.Wrap(err, "dal")
}
@@ -185,7 +183,6 @@ func (r mysqlRepo) GetSubjectRelated(
Joins(r.q.SubjectRelation.Subject).Where(r.q.SubjectRelation.SubjectID.Eq(subjectID)).
Order(r.q.SubjectRelation.Order).Find()
if err != nil {
- r.log.Error("unexpected error happened", zap.Error(err))
return nil, errgo.Wrap(err, "dal")
}
@@ -215,7 +212,6 @@ func (r mysqlRepo) GetByIDs(
records, err := q.Find()
if err != nil {
- r.log.Error("unexpected error happened", zap.Error(err))
return nil, errgo.Wrap(err, "dal")
}
@@ -231,6 +227,93 @@ func (r mysqlRepo) GetByIDs(
return result, nil
}
+func (r mysqlRepo) Count(
+ ctx context.Context,
+ filter BrowseFilter) (int64, error) {
+ q := r.q.Subject.WithContext(ctx).Joins(r.q.Subject.Fields).Join(
+ r.q.SubjectField, r.q.Subject.ID.EqCol(r.q.SubjectField.Sid),
+ ).Where(r.q.Subject.TypeID.Eq(filter.Type))
+ if filter.NSFW.Set {
+ q = q.Where(r.q.Subject.Nsfw.Is(filter.NSFW.Value))
+ }
+ if filter.Category.Set {
+ q = q.Where(r.q.Subject.Platform.Eq(filter.Category.Value))
+ }
+ if filter.Series.Set {
+ q = q.Where(r.q.Subject.Series.Is(filter.Series.Value))
+ }
+ if filter.Platform.Set {
+ q = q.Where(r.q.Subject.Infobox.Like(utiltype.HTMLEscapedString(fmt.Sprintf("%%[%s]%%", filter.Platform.Value))))
+ }
+ if filter.Year.Set {
+ q = q.Where(r.q.SubjectField.Year.Eq(filter.Year.Value))
+ }
+ if filter.Month.Set {
+ q = q.Where(r.q.SubjectField.Mon.Eq(filter.Month.Value))
+ }
+
+ if filter.Sort.Set {
+ switch filter.Sort.Value {
+ case "date":
+ q = q.Order(r.q.SubjectField.Date.Desc())
+ case "rank":
+ q = q.Where(r.q.SubjectField.Rank.Gt(0)).Order(r.q.SubjectField.Rank)
+ }
+ }
+
+ return q.Count()
+}
+
+func (r mysqlRepo) Browse(
+ ctx context.Context, filter BrowseFilter, limit, offset int,
+) ([]model.Subject, error) {
+ q := r.q.Subject.WithContext(ctx).Joins(r.q.Subject.Fields).Join(
+ r.q.SubjectField, r.q.Subject.ID.EqCol(r.q.SubjectField.Sid),
+ ).Where(r.q.Subject.TypeID.Eq(filter.Type))
+ if filter.NSFW.Set {
+ q = q.Where(r.q.Subject.Nsfw.Is(filter.NSFW.Value))
+ }
+ if filter.Category.Set {
+ q = q.Where(r.q.Subject.Platform.Eq(filter.Category.Value))
+ }
+ if filter.Series.Set {
+ q = q.Where(r.q.Subject.Series.Is(filter.Series.Value))
+ }
+ if filter.Platform.Set {
+ q = q.Where(r.q.Subject.Infobox.Like(utiltype.HTMLEscapedString(fmt.Sprintf("%%[%s]%%", filter.Platform.Value))))
+ }
+ if filter.Year.Set {
+ q = q.Where(r.q.SubjectField.Year.Eq(filter.Year.Value))
+ }
+ if filter.Month.Set {
+ q = q.Where(r.q.SubjectField.Mon.Eq(filter.Month.Value))
+ }
+
+ if filter.Sort.Set {
+ switch filter.Sort.Value {
+ case "date":
+ q = q.Order(r.q.SubjectField.Date.Desc())
+ case "rank":
+ q = q.Where(r.q.SubjectField.Rank.Gt(0)).Order(r.q.SubjectField.Rank)
+ }
+ }
+
+ subjects, err := q.Limit(limit).Offset(offset).Find()
+ if err != nil {
+ return nil, errgo.Wrap(err, "dal")
+ }
+
+ var result = make([]model.Subject, len(subjects))
+ for i, subject := range subjects {
+ result[i], err = ConvertDao(subject)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return result, nil
+}
+
func (r mysqlRepo) GetActors(
ctx context.Context,
subjectID model.SubjectID,
@@ -241,7 +324,6 @@ func (r mysqlRepo) GetActors(
Order(r.q.Cast.PersonID).
Find()
if err != nil {
- r.log.Error("unexpected error happened", zap.Error(err))
return nil, errgo.Wrap(err, "dal")
}
diff --git a/internal/subject/mysql_repository_test.go b/internal/subject/mysql_repository_test.go
index 3dfc5b4fd..c6a767a99 100644
--- a/internal/subject/mysql_repository_test.go
+++ b/internal/subject/mysql_repository_test.go
@@ -62,6 +62,64 @@ func TestMysqlRepo_Get_filter(t *testing.T) {
require.ErrorIs(t, err, gerr.ErrNotFound)
}
+func TestBrowse(t *testing.T) {
+ test.RequireEnv(t, test.EnvMysql)
+ t.Parallel()
+
+ repo := getRepo(t)
+
+ filter := subject.BrowseFilter{
+ Type: 2,
+ }
+ s, err := repo.Browse(context.Background(), filter, 30, 0)
+ require.NoError(t, err)
+ require.Equal(t, 12, len(s))
+
+ filter = subject.BrowseFilter{
+ Type: 1,
+ Category: null.New(uint16(1003)),
+ }
+ s, err = repo.Browse(context.Background(), filter, 30, 0)
+ require.NoError(t, err)
+ require.Equal(t, 2, len(s))
+
+ filter = subject.BrowseFilter{
+ Type: 2,
+ Year: null.New(int32(2008)),
+ }
+ s, err = repo.Browse(context.Background(), filter, 30, 0)
+ require.NoError(t, err)
+ require.Equal(t, 2, len(s))
+
+ filter = subject.BrowseFilter{
+ Type: 3,
+ Sort: null.New("rank"),
+ }
+ s, err = repo.Browse(context.Background(), filter, 30, 0)
+ require.NoError(t, err)
+ require.Equal(t, 8, len(s))
+ require.Equal(t, model.SubjectID(20), s[0].ID)
+ require.Equal(t, model.SubjectID(17), s[1].ID)
+ require.Equal(t, model.SubjectID(16), s[2].ID)
+ require.Equal(t, model.SubjectID(15), s[3].ID)
+ require.Equal(t, model.SubjectID(406604), s[4].ID)
+ require.Equal(t, model.SubjectID(19), s[5].ID)
+ require.Equal(t, model.SubjectID(315957), s[6].ID)
+ require.Equal(t, model.SubjectID(18), s[7].ID)
+
+ filter = subject.BrowseFilter{
+ Type: 4,
+ Platform: null.New("PS3"),
+ Sort: null.New("date"),
+ }
+ s, err = repo.Browse(context.Background(), filter, 30, 0)
+ require.NoError(t, err)
+ require.Equal(t, 3, len(s))
+ require.Equal(t, model.SubjectID(7), s[0].ID)
+ require.Equal(t, model.SubjectID(6), s[1].ID)
+ require.Equal(t, model.SubjectID(13), s[2].ID)
+}
+
func TestMysqlRepo_GetByIDs(t *testing.T) {
test.RequireEnv(t, test.EnvMysql)
t.Parallel()
diff --git a/internal/tag/cache_repo.go b/internal/tag/cache_repo.go
new file mode 100644
index 000000000..4cbc3ff0f
--- /dev/null
+++ b/internal/tag/cache_repo.go
@@ -0,0 +1,150 @@
+// 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/prometheus/client_golang/prometheus"
+ "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"
+)
+
+const cacheTTL = time.Hour * 24
+
+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
+}
+
+//nolint:gochecknoglobals
+var CachedCount = prometheus.NewCounter(prometheus.CounterOpts{
+ Subsystem: "chii",
+ Name: "query_cached_count_total",
+ Help: "cached sql query count total",
+ ConstLabels: map[string]string{"repo": "meta_tags"},
+})
+
+//nolint:gochecknoglobals
+var TotalCount = prometheus.NewCounter(prometheus.CounterOpts{
+ Subsystem: "chii",
+ Name: "query_count_total",
+ Help: "sql query count total",
+ ConstLabels: map[string]string{"repo": "meta_tags"},
+})
+
+//nolint:gochecknoinits
+func init() {
+ prometheus.MustRegister(CachedCount)
+ prometheus.MustRegister(TotalCount)
+}
+
+// also need to change version in [cachekey.SubjectMetaTag] if schema is changed.
+
+func (r cacheRepo) Get(ctx context.Context, id model.SubjectID) ([]Tag, error) {
+ TotalCount.Add(1)
+ 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 {
+ CachedCount.Add(1)
+ 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}, cacheTTL); 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) {
+ result := make(map[model.SubjectID][]Tag, len(ids))
+ if len(ids) == 0 {
+ return result, nil
+ }
+
+ TotalCount.Add(float64(len(ids)))
+
+ var tags []cachedTags
+
+ err := r.cache.MGet(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")
+ }
+
+ CachedCount.Add(float64(len(tags)))
+ 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)
+ }
+ }
+
+ if len(missing) == 0 {
+ return result, nil
+ }
+
+ 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,
+ }, cacheTTL)
+ 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/metrics/redis.go b/internal/tag/domain.go
similarity index 56%
rename from internal/metrics/redis.go
rename to internal/tag/domain.go
index 12fa990bb..375a7e1ce 100644
--- a/internal/metrics/redis.go
+++ b/internal/tag/domain.go
@@ -12,17 +12,36 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see
-package metrics
+package tag
import (
- "github.com/redis/go-redis/v9"
- redisprom "github.com/trim21/go-redis-prometheus"
+ "context"
+
+ "github.com/bangumi/server/internal/model"
)
-func RedisHook(instance string) redis.Hook {
- return redisprom.NewHook(
- redisprom.WithNamespace("chii"),
- redisprom.WithDurationBuckets([]float64{.001, .002, .003, .004, .005, .0075, .01, .05, .1, .3, .5, .75, 1, 2}),
- redisprom.WithInstanceName(instance),
- )
+// 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..a171efcf0
--- /dev/null
+++ b/internal/tag/mysql_repo.go
@@ -0,0 +1,105 @@
+// 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
+ 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
+ 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,
+ })
+ }
+
+ // set empty slice for subjects without tags
+ // this help we cache them.
+ for _, id := range ids {
+ if _, ok := tags[id]; !ok {
+ tags[id] = []Tag{}
+ }
+ }
+
+ 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/internal/timeline/grpc.go b/internal/timeline/grpc.go
index b096c7ba5..b86f94e45 100644
--- a/internal/timeline/grpc.go
+++ b/internal/timeline/grpc.go
@@ -20,8 +20,6 @@ import (
"time"
"github.com/trim21/errgo"
- clientv3 "go.etcd.io/etcd/client/v3"
- "go.etcd.io/etcd/client/v3/naming/resolver"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
@@ -130,26 +128,13 @@ func (m grpcClient) ChangeEpisodeStatus(
}
func newGrpcClient(cfg config.AppConfig) (pb.TimeLineServiceClient, error) {
- if cfg.EtcdAddr == "" {
- logger.Info("no etcd, using nope timeline service")
+ if cfg.SrvTimelineDomain == "" || cfg.SrvTimelinePort == 0 {
+ logger.Info("no srv_timeline_domain and srv_timeline_port, using nope timeline service")
return noopClient{}, nil
}
- logger.Info("using etcd to discovery timeline services " + cfg.EtcdAddr)
-
- cli, err := clientv3.NewFromURL(cfg.EtcdAddr)
- if err != nil {
- return nil, errgo.Wrap(err, "etcd new client")
- }
-
- etcdResolver, err := resolver.NewBuilder(cli)
- if err != nil {
- return nil, errgo.Wrap(err, "etcd grpc resolver")
- }
-
- conn, err := grpc.Dial(
- fmt.Sprintf("etcd:///%s/timeline", cfg.EtcdNamespace),
- grpc.WithResolvers(etcdResolver),
+ conn, err := grpc.NewClient(
+ fmt.Sprintf("dns:///%s:%d", cfg.SrvTimelineDomain, cfg.SrvTimelinePort),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
diff --git a/internal/user/domain.go b/internal/user/domain.go
index d547004c0..c34c2fdd6 100644
--- a/internal/user/domain.go
+++ b/internal/user/domain.go
@@ -21,6 +21,9 @@ import (
)
type Repo interface {
+ // GetFullUser find a user by uid.
+ GetFullUser(ctx context.Context, userID model.UserID) (FullUser, error)
+
// GetByID find a user by uid.
GetByID(ctx context.Context, userID model.UserID) (User, error)
// GetByName find a user by username.
diff --git a/internal/user/model.go b/internal/user/model.go
index fc79bece2..cb700cf6b 100644
--- a/internal/user/model.go
+++ b/internal/user/model.go
@@ -22,6 +22,19 @@ import (
"github.com/bangumi/server/internal/model"
)
+// FullUser is for current user or admin only.
+type FullUser struct {
+ RegistrationTime time.Time
+ NickName string
+ Avatar string
+ Sign string
+ UserName string
+ ID model.UserID
+ UserGroup GroupID
+ TimeOffset int8
+ Email string
+}
+
type GroupID = uint8
// User is visible for everyone.
@@ -65,9 +78,11 @@ type PrivacySettings struct {
func (settings *PrivacySettings) Unmarshal(s []byte) {
rawMap := make(map[PrivacySettingsField]ReceiveFilter, 4)
- err := phpserialize.Unmarshal(s, &rawMap)
- if err != nil {
- return
+ if len(s) != 0 {
+ err := phpserialize.Unmarshal(s, &rawMap)
+ if err != nil {
+ return
+ }
}
settings.ReceivePrivateMessage = rawMap[PrivacyReceivePrivateMessage]
diff --git a/internal/user/mysql_repository.go b/internal/user/mysql_repository.go
index d288f6504..a85365a79 100644
--- a/internal/user/mysql_repository.go
+++ b/internal/user/mysql_repository.go
@@ -17,6 +17,7 @@ package user
import (
"context"
"errors"
+ "strconv"
"time"
"github.com/trim21/errgo"
@@ -40,14 +41,51 @@ type mysqlRepo struct {
log *zap.Logger
}
+func (m mysqlRepo) GetFullUser(ctx context.Context, userID model.UserID) (FullUser, error) {
+ u, err := m.q.Member.WithContext(ctx).Where(m.q.Member.ID.Eq(userID)).Take()
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return FullUser{}, gerr.ErrUserNotFound
+ }
+ return FullUser{}, errgo.Wrap(err, "dal")
+ }
+
+ return FullUser{
+ UserName: u.Username,
+ NickName: u.Nickname,
+ UserGroup: u.Groupid,
+ Avatar: u.Avatar,
+ Sign: string(u.Sign),
+ ID: u.ID,
+ RegistrationTime: time.Unix(u.Regdate, 0),
+ TimeOffset: parseTimeOffset(u.Timeoffset),
+ Email: u.Email,
+ }, nil
+}
+
+// default time zone GMT+8.
+const defaultTimeOffset = 8
+
+func parseTimeOffset(s string) int8 {
+ switch s {
+ case "", "9999":
+ return defaultTimeOffset
+ }
+
+ v, err := strconv.ParseInt(s, 10, 8)
+ if err != nil {
+ return defaultTimeOffset
+ }
+
+ return int8(v)
+}
+
func (m mysqlRepo) GetByID(ctx context.Context, userID model.UserID) (User, error) {
u, err := m.q.Member.WithContext(ctx).Where(m.q.Member.ID.Eq(userID)).Take()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return User{}, gerr.ErrUserNotFound
}
-
- m.log.Error("unexpected error happened", zap.Error(err))
return User{}, errgo.Wrap(err, "dal")
}
@@ -60,8 +98,6 @@ func (m mysqlRepo) GetByName(ctx context.Context, username string) (User, error)
if errors.Is(err, gorm.ErrRecordNotFound) {
return User{}, gerr.ErrUserNotFound
}
-
- m.log.Error("unexpected error happened", zap.Error(err))
return User{}, errgo.Wrap(err, "dal")
}
@@ -71,7 +107,6 @@ func (m mysqlRepo) GetByName(ctx context.Context, username string) (User, error)
func (m mysqlRepo) GetByIDs(ctx context.Context, ids []model.UserID) (map[model.UserID]User, error) {
u, err := m.q.Member.WithContext(ctx).Where(m.q.Member.ID.In(ids...)).Find()
if err != nil {
- m.log.Error("unexpected error happened", zap.Error(err))
return nil, errgo.Wrap(err, "dal")
}
diff --git a/main.go b/main.go
index 7c046eaab..b89fe5333 100644
--- a/main.go
+++ b/main.go
@@ -17,11 +17,14 @@ package main
import (
"fmt"
+ "github.com/google/uuid"
+
"github.com/bangumi/server/cmd"
"github.com/bangumi/server/internal/pkg/logger"
)
func main() {
+ uuid.EnableRandPool()
if err := cmd.Root.Execute(); err != nil {
logger.Fatal("failed to start app:\n" + fmt.Sprintf("\n%+v", err))
}
diff --git a/openapi/components/subject_cat_anime.yaml b/openapi/components/subject_cat_anime.yaml
new file mode 100644
index 000000000..42ca9c232
--- /dev/null
+++ b/openapi/components/subject_cat_anime.yaml
@@ -0,0 +1,31 @@
+title: SubjectAnimeCategory
+example: 1
+enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ - 5
+type: integer
+description: |-
+ 动画类型
+ - `0` 为 其他
+ - `1` 为 TV
+ - `2` 为 OVA
+ - `3` 为 Movie
+ - `5` 为 WEB
+x-ms-enum:
+ name: SubjectAnimeCategory
+ modelAsString: false
+ values:
+ - Other
+ - TV
+ - OVA
+ - Movie
+ - WEB
+x-enum-varnames:
+ - Other
+ - TV
+ - OVA
+ - Movie
+ - WEB
diff --git a/openapi/components/subject_cat_book.yaml b/openapi/components/subject_cat_book.yaml
new file mode 100644
index 000000000..0c5cb8bd7
--- /dev/null
+++ b/openapi/components/subject_cat_book.yaml
@@ -0,0 +1,27 @@
+title: SubjectBookCategory
+example: 1001
+enum:
+ - 0
+ - 1001
+ - 1002
+ - 1003
+type: integer
+description: |-
+ 书籍类型
+ - `0` 为 其他
+ - `1001` 为 漫画
+ - `1002` 为 小说
+ - `1003` 为 画集
+x-ms-enum:
+ name: SubjectBookCategory
+ modelAsString: false
+ values:
+ - Other
+ - Comic
+ - Novel
+ - Illustration
+x-enum-varnames:
+ - Other
+ - Comic
+ - Novel
+ - Illustration
diff --git a/openapi/components/subject_cat_game.yaml b/openapi/components/subject_cat_game.yaml
new file mode 100644
index 000000000..ec1eeaafa
--- /dev/null
+++ b/openapi/components/subject_cat_game.yaml
@@ -0,0 +1,31 @@
+title: SubjectGameCategory
+example: 4001
+enum:
+ - 0
+ - 4001
+ - 4003
+ - 4002
+ - 4005
+type: integer
+description: |-
+ 游戏类型
+ - `0` 为 其他
+ - `4001` 为 游戏
+ - `4002` 为 软件
+ - `4003` 为 扩展包
+ - `4005` 为 桌游
+x-ms-enum:
+ name: SubjectGameCategory
+ modelAsString: false
+ values:
+ - Other
+ - Games
+ - Software
+ - DLC
+ - Tabletop
+x-enum-varnames:
+ - Other
+ - Games
+ - Software
+ - DLC
+ - Tabletop
diff --git a/openapi/components/subject_cat_real.yaml b/openapi/components/subject_cat_real.yaml
new file mode 100644
index 000000000..317d4291b
--- /dev/null
+++ b/openapi/components/subject_cat_real.yaml
@@ -0,0 +1,43 @@
+title: SubjectRealCategory
+example: 6
+enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ - 6001
+ - 6002
+ - 6003
+ - 6004
+type: integer
+description: |-
+ 电影类型
+ - `0` 为 其他
+ - `1` 为 日剧
+ - `2` 为 欧美剧
+ - `3` 为 华语剧
+ - `6001` 为 电视剧
+ - `6002` 为 电影
+ - `6003` 为 演出
+ - `6004` 为 综艺
+x-ms-enum:
+ name: SubjectRealCategory
+ modelAsString: false
+ values:
+ - Other
+ - JP
+ - EN
+ - CN
+ - TV
+ - Movie
+ - Live
+ - Show
+x-enum-varnames:
+ - Other
+ - JP
+ - EN
+ - CN
+ - TV
+ - Movie
+ - Live
+ - Show
diff --git a/openapi/components/subject_v0.yaml b/openapi/components/subject_v0.yaml
index 7bf9df61e..d6ce9392d 100644
--- a/openapi/components/subject_v0.yaml
+++ b/openapi/components/subject_v0.yaml
@@ -8,8 +8,10 @@ required:
- nsfw
- locked
- platform
+ - meta_tags
- volumes
- eps
+ - series
- total_episodes
- rating
- images
@@ -34,6 +36,10 @@ properties:
summary:
title: Summary
type: string
+ series:
+ title: Series
+ type: boolean
+ description: 是否为书籍系列的主条目
nsfw:
title: Nsfw
type: boolean
@@ -47,7 +53,7 @@ properties:
platform:
title: Platform
type: string
- description: TV, Web, 欧美剧, PS4...
+ description: TV, Web, 欧美剧, DLC...
images:
$ref: "./subject_image.yaml"
infobox:
@@ -132,5 +138,10 @@ properties:
dropped:
title: Dropped
type: integer
+ meta_tags:
+ description: 由维基人维护的 tag
+ type: array
+ items:
+ type: string
tags:
$ref: "./subject_tags.yaml"
diff --git a/openapi/components/subject_v0_slim.yaml b/openapi/components/subject_v0_slim.yaml
index 8a47772c9..048929646 100644
--- a/openapi/components/subject_v0_slim.yaml
+++ b/openapi/components/subject_v0_slim.yaml
@@ -52,8 +52,12 @@ properties:
type: integer
score:
description: 分数
- title: Total
+ title: Score
type: number
+ rank:
+ description: 排名
+ title: Rank
+ type: integer
tags:
description: 前 10 个 tag
diff --git a/openapi/components/subject_collection.yaml b/openapi/components/user_subject_collection.yaml
similarity index 100%
rename from openapi/components/subject_collection.yaml
rename to openapi/components/user_subject_collection.yaml
diff --git a/openapi/components/subject_collection_modify_payload.yaml b/openapi/components/user_subject_collection_modify_payload.yaml
similarity index 100%
rename from openapi/components/subject_collection_modify_payload.yaml
rename to openapi/components/user_subject_collection_modify_payload.yaml
diff --git a/openapi/v0.yaml b/openapi/v0.yaml
index 8cc637044..99cef4c12 100644
--- a/openapi/v0.yaml
+++ b/openapi/v0.yaml
@@ -33,11 +33,6 @@ paths:
不同筛选条件之间为 `且`
-
- 由于目前 meilisearch 的一些问题,条目排名更新并不会触发搜索数据更新,所以条目排名可能是过期数据。
-
- 希望未来版本的 meilisearch 能解决相关的问题。
-
parameters:
- name: limit
in: query
@@ -86,6 +81,14 @@ paths:
items:
$ref: "#/components/schemas/SubjectType"
description: 条目类型,参照 `SubjectType` enum,多值之间为 `或` 的关系。
+ meta_tags:
+ type: array
+ items:
+ type: string
+ example:
+ - 童年
+ - 原创
+ description: 公共标签。多个值之间为 `且` 关系。可以用 `-` 排除标签。比如 `-科幻` 可以排除科幻标签。
tag:
type: array
items:
@@ -128,73 +131,202 @@ paths:
`true` 只会返回 R18 条目。
`false` 只会返回非 R18 条目。
+ responses:
+ 200:
+ description: 返回搜索结果
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/Paged_Subject"
+
+ "/v0/search/characters":
+ post:
+ tags:
+ - 角色
+ summary: 角色搜索
+ operationId: searchCharacters
+ description: |
+ ## 实验性 API, 本 schema 和实际的 API 行为都可能随时发生改动
+
+ 目前支持的筛选条件包括:
+ - `nsfw`: 使用 `include` 包含NSFW搜索结果。默认排除搜索NSFW条目。无权限情况下忽略此选项,不会返回NSFW条目。
+
+ parameters:
+ - name: limit
+ in: query
+ description: 分页参数
+ required: false
+ schema:
+ type: integer
+ - name: offset
+ in: query
+ description: 分页参数
+ required: false
+ schema:
+ type: integer
+ requestBody:
+ content:
+ "application/json":
+ schema:
+ type: object
+ required:
+ - keyword
+ properties:
+ keyword:
+ type: string
+ filter:
+ type: object
+ description: 不同条件之间是 `且` 的关系
+ properties:
+ nsfw:
+ type: boolean
+ description: |
+ 无权限的用户会直接忽略此字段,不会返回 R18 角色。
+
+ 默认或者 `null` 会返回包含 R18 的所有搜索结果。
+
+ `true` 只会返回 R18 角色。
+ `false` 只会返回非 R18 角色。
responses:
200:
description: 返回搜索结果
content:
application/json:
schema:
- description: 用户信息
- type: object
- properties:
- total:
- description: 搜索结果数量
- type: integer
- example: 100
- limit:
- description: 当前分页参数
- type: integer
- example: 100
- offset:
- description: 当前分页参数
- type: integer
- example: 100
- data:
- type: array
- items:
- type: object
- required:
- - score
- - id
- - rank
- - tags
- - name
- - name_cn
- - image
- - date
- - summary
- properties:
- id:
- description: 条目ID
- type: integer
- example: 8
- type:
- $ref: "#/components/schemas/SubjectType"
- "date":
- "type": "string"
- description: 上映/开播/连载开始日期,可能为空字符串
- "image":
- "type": "string"
- format: url
- description: 封面
- summary:
- type: string
- description: 条目描述
- "name":
- "type": "string"
- description: 条目原名
- "name_cn":
- "type": "string"
- description: 条目中文名
- "tags":
- $ref: "#/components/schemas/SubjectTags"
- "score":
- description: 评分
- "type": "number"
- "rank":
- description: 排名
- "type": "integer"
+ "$ref": "#/components/schemas/Paged_Character"
+
+ "/v0/search/persons":
+ post:
+ tags:
+ - 人物
+ summary: 人物搜索
+ operationId: searchPersons
+ description: |
+ ## 实验性 API, 本 schema 和实际的 API 行为都可能随时发生改动
+
+ 目前支持的筛选条件包括:
+ - `career`: 职业,可以多次出现。`且` 关系。
+
+ 不同筛选条件之间为 `且`
+
+ parameters:
+ - name: limit
+ in: query
+ description: 分页参数
+ required: false
+ schema:
+ type: integer
+ - name: offset
+ in: query
+ description: 分页参数
+ required: false
+ schema:
+ type: integer
+ requestBody:
+ content:
+ "application/json":
+ schema:
+ type: object
+ required:
+ - keyword
+ properties:
+ keyword:
+ type: string
+ filter:
+ type: object
+ description: 不同条件之间是 `且` 的关系
+ properties:
+ career:
+ type: array
+ items:
+ type: string
+ example:
+ - artist
+ - director
+ description: 职业,可以多次出现。多值之间为 `且` 关系。
+ responses:
+ 200:
+ description: 返回搜索结果
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/Paged_Person"
+
+ "/v0/subjects":
+ get:
+ tags:
+ - 条目
+ summary: 浏览条目
+ description: 第一页会 cache 24h,之后会 cache 1h
+ operationId: getSubjects
+ parameters:
+ - name: type
+ in: query
+ description: 条目类型
+ required: true
+ schema:
+ $ref: "#/components/schemas/SubjectType"
+ - name: cat
+ in: query
+ description: 条目分类,参照 `SubjectCategory` enum
+ required: false
+ schema:
+ $ref: "#/components/schemas/SubjectCategory"
+ - name: series
+ in: query
+ description: 是否系列,仅对书籍类型的条目有效
+ required: false
+ schema:
+ type: boolean
+ - name: platform
+ in: query
+ description: 平台,仅对游戏类型的条目有效
+ required: false
+ schema:
+ type: string
+ - name: sort
+ in: query
+ description: 排序,枚举值 {date|rank}
+ required: false
+ schema:
+ title: Sort Order
+ type: string
+ - name: year
+ in: query
+ description: 年份
+ required: false
+ schema:
+ type: integer
+ - name: month
+ in: query
+ description: 月份
+ required: false
+ schema:
+ type: integer
+ - $ref: "#/components/parameters/default_query_limit"
+ - $ref: "#/components/parameters/default_query_offset"
+ responses:
+ "200":
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/Paged_Subject"
+ "400":
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ "404":
+ description: Not Found
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ security:
+ - OptionalHTTPBearer: []
"/v0/subjects/{subject_id}":
get:
@@ -460,7 +592,7 @@ paths:
content:
application/json:
schema:
- "$ref": "#/components/schemas/CharacterDetail"
+ "$ref": "#/components/schemas/Character"
"404":
description: Not Found
content:
@@ -571,6 +703,70 @@ paths:
application/json:
schema:
"$ref": "#/components/schemas/ErrorDetail"
+ "/v0/characters/{character_id}/collect":
+ post:
+ tags:
+ - 角色
+ summary: Collect character for current user
+ operationId: collectCharacterByCharacterIdAndUserId
+ description: 为当前用户收藏角色
+ parameters:
+ - $ref: "#/components/parameters/path_character_id"
+ responses:
+ "204":
+ description: Successful Response
+ "400":
+ description: character ID not valid
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ "401":
+ description: not authorized
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ "404":
+ description: 角色不存在
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ security:
+ - HTTPBearer: []
+ delete:
+ tags:
+ - 角色
+ summary: Uncollect character for current user
+ operationId: uncollectCharacterByCharacterIdAndUserId
+ description: 为当前用户取消收藏角色
+ parameters:
+ - $ref: "#/components/parameters/path_character_id"
+ responses:
+ "204":
+ description: Successful Response
+ "400":
+ description: character ID not valid
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ "401":
+ description: not authorized
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ "404":
+ description: 角色不存在
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ security:
+ - HTTPBearer: []
+
"/v0/persons/{person_id}":
get:
tags:
@@ -690,12 +886,75 @@ paths:
application/json:
schema:
"$ref": "#/components/schemas/ErrorDetail"
- "400":
- description: Validation Error
+ "400":
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ "/v0/persons/{person_id}/collect":
+ post:
+ tags:
+ - 人物
+ summary: Collect person for current user
+ operationId: collectPersonByPersonIdAndUserId
+ description: 为当前用户收藏人物
+ parameters:
+ - $ref: "#/components/parameters/path_person_id"
+ responses:
+ "204":
+ description: Successful Response
+ "400":
+ description: person ID not valid
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ "401":
+ description: not authorized
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ "404":
+ description: 人物不存在
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ security:
+ - OptionalHTTPBearer: []
+ delete:
+ tags:
+ - 人物
+ summary: Uncollect person for current user
+ operationId: uncollectPersonByPersonIdAndUserId
+ description: 为当前用户取消收藏人物
+ parameters:
+ - $ref: "#/components/parameters/path_person_id"
+ responses:
+ "204":
+ description: Successful Response
+ "400":
+ description: person ID not valid
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ "401":
+ description: not authorized
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ "404":
+ description: 人物不存在
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorDetail"
+ security:
+ - OptionalHTTPBearer: []
"/v0/users/{username}":
get:
@@ -780,7 +1039,24 @@ paths:
content:
application/json:
schema:
- "$ref": "#/components/schemas/User"
+ allOf:
+ - "$ref": "#/components/schemas/User"
+ - required:
+ - email
+ - reg_time
+ - type: object
+ properties:
+ email:
+ description: "用户绑定的邮箱地址"
+ type: string
+ format: email
+ reg_time:
+ description: "用户注册时间。比如 2017-12-03T08:51:16+08:00"
+ type: string
+ format: date-time
+ time_offset:
+ description: "用户设置的时区偏移,以小时为单位。比如 GMT+8(shanghai/beijing)为 8"
+ type: integer
"403":
description: unauthorized
content:
@@ -847,8 +1123,8 @@ paths:
get:
tags:
- 收藏
- summary: 获取用户单个收藏
- description: 获取对应用户的收藏,查看私有收藏需要access token。
+ summary: 获取用户单个条目收藏
+ description: 获取对应用户的收藏,查看私有收藏需要 access token
operationId: getUserCollection
parameters:
- $ref: "#/components/parameters/path_username"
@@ -878,7 +1154,7 @@ paths:
post:
tags:
- 收藏
- summary: 新增或修改用户单个收藏
+ summary: 新增或修改用户单个条目收藏
description: |
修改条目收藏状态, 如果不存在则创建,如果存在则修改
@@ -1177,6 +1453,106 @@ paths:
security:
- HTTPBearer: []
+ "/v0/users/{username}/collections/-/characters":
+ get:
+ tags:
+ - 收藏
+ summary: 获取用户角色收藏列表
+ operationId: getUserCharacterCollections
+ parameters:
+ - $ref: "#/components/parameters/path_username"
+ responses:
+ "200":
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/Paged_UserCharacterCollection"
+ "404":
+ description: 用户不存在
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ "/v0/users/{username}/collections/-/characters/{character_id}":
+ get:
+ tags:
+ - 收藏
+ summary: 获取用户单个角色收藏信息
+ operationId: getUserCharacterCollection
+ parameters:
+ - $ref: "#/components/parameters/path_username"
+ - $ref: "#/components/parameters/path_character_id"
+ responses:
+ "200":
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/UserCharacterCollection"
+ "400":
+ description: character ID not valid
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ "404":
+ description: 用户或角色不存在
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+
+ "/v0/users/{username}/collections/-/persons":
+ get:
+ tags:
+ - 收藏
+ summary: 获取用户人物收藏列表
+ operationId: getUserPersonCollections
+ parameters:
+ - $ref: "#/components/parameters/path_username"
+ responses:
+ "200":
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/Paged_UserPersonCollection"
+ "404":
+ description: 用户不存在
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ "/v0/users/{username}/collections/-/persons/{person_id}":
+ get:
+ tags:
+ - 收藏
+ summary: 获取用户单个人物收藏信息
+ operationId: getUserPersonCollection
+ parameters:
+ - $ref: "#/components/parameters/path_username"
+ - $ref: "#/components/parameters/path_person_id"
+ responses:
+ "200":
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/UserPersonCollection"
+ "400":
+ description: person ID not valid
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+ "404":
+ description: 用户或人物不存在
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/ErrorDetail"
+
"/v0/revisions/persons":
get:
tags:
@@ -1744,8 +2120,8 @@ components:
- B
- AB
- O
- CharacterDetail:
- title: CharacterDetail
+ Character:
+ title: Character
required:
- id
- name
@@ -1816,6 +2192,7 @@ components:
- name
- type
- subject_id
+ - subject_type
- subject_name
- subject_name_cn
type: object
@@ -1840,6 +2217,8 @@ components:
subject_id:
title: Subject ID
type: integer
+ subject_type:
+ $ref: "#/components/schemas/SubjectType"
subject_name:
title: Subject Name
type: string
@@ -2114,9 +2493,7 @@ components:
title: ID
type: integer
type:
- title: Type
- type: integer
- description: "`0` 本篇,`1` SP,`2` OP,`3` ED"
+ $ref: "#/components/schemas/EpType"
name:
title: Name
type: string
@@ -2285,6 +2662,72 @@ components:
Page:
$ref: "./components/page.yaml"
+ Paged_Subject:
+ title: Paged[Subject]
+ type: object
+ properties:
+ total:
+ title: Total
+ type: integer
+ default: 0
+ limit:
+ title: Limit
+ type: integer
+ default: 0
+ offset:
+ title: Offset
+ type: integer
+ default: 0
+ data:
+ title: Data
+ type: array
+ items:
+ "$ref": "#/components/schemas/Subject"
+ default: []
+ Paged_Character:
+ title: Paged[Character]
+ type: object
+ properties:
+ total:
+ title: Total
+ type: integer
+ default: 0
+ limit:
+ title: Limit
+ type: integer
+ default: 0
+ offset:
+ title: Offset
+ type: integer
+ default: 0
+ data:
+ title: Data
+ type: array
+ items:
+ "$ref": "#/components/schemas/Character"
+ default: []
+ Paged_Person:
+ title: Paged[Person]
+ type: object
+ properties:
+ total:
+ title: Total
+ type: integer
+ default: 0
+ limit:
+ title: Limit
+ type: integer
+ default: 0
+ offset:
+ title: Offset
+ type: integer
+ default: 0
+ data:
+ title: Data
+ type: array
+ items:
+ "$ref": "#/components/schemas/Person"
+ default: []
Paged_Episode:
title: Paged[Episode]
type: object
@@ -2373,6 +2816,50 @@ components:
items:
"$ref": "#/components/schemas/UserSubjectCollection"
default: []
+ Paged_UserCharacterCollection:
+ title: Paged[UserCharacterCollection]
+ type: object
+ properties:
+ total:
+ title: Total
+ type: integer
+ default: 0
+ limit:
+ title: Limit
+ type: integer
+ default: 0
+ offset:
+ title: Offset
+ type: integer
+ default: 0
+ data:
+ title: Data
+ type: array
+ items:
+ "$ref": "#/components/schemas/UserCharacterCollection"
+ default: []
+ Paged_UserPersonCollection:
+ title: Paged[UserPersonCollection]
+ type: object
+ properties:
+ total:
+ title: Total
+ type: integer
+ default: 0
+ limit:
+ title: Limit
+ type: integer
+ default: 0
+ offset:
+ title: Offset
+ type: integer
+ default: 0
+ data:
+ title: Data
+ type: array
+ items:
+ "$ref": "#/components/schemas/UserPersonCollection"
+ default: []
Person:
title: Person
required:
@@ -2430,6 +2917,7 @@ components:
- name
- type
- subject_id
+ - subject_type
- subject_name
- subject_name_cn
type: object
@@ -2453,6 +2941,8 @@ components:
subject_id:
title: Subject ID
type: integer
+ subject_type:
+ $ref: "#/components/schemas/SubjectType"
subject_name:
title: Subject Name
type: string
@@ -2623,6 +3113,7 @@ components:
- type
- career
- relation
+ - eps
type: object
properties:
id:
@@ -2649,6 +3140,75 @@ components:
relation:
title: Relation
type: string
+ eps:
+ title: Eps
+ type: string
+ description: 参与章节/曲目
+ UserCharacterCollection:
+ title: UserCharacterCollection
+ required:
+ - id
+ - name
+ - type
+ - created_at
+ type: object
+ properties:
+ id:
+ title: ID
+ type: integer
+ name:
+ title: Name
+ type: string
+ type:
+ type: integer
+ allOf:
+ - "$ref": "#/components/schemas/CharacterType"
+ description: 角色,机体,舰船,组织...
+ images:
+ title: Images
+ type: object
+ allOf:
+ - "$ref": "#/components/schemas/PersonImages"
+ description: object with some size of images, this object maybe `null`
+ created_at:
+ title: Created At
+ type: string
+ format: date-time
+ UserPersonCollection:
+ title: UserPersonCollection
+ required:
+ - id
+ - name
+ - type
+ - career
+ - created_at
+ type: object
+ properties:
+ id:
+ title: ID
+ type: integer
+ name:
+ title: Name
+ type: string
+ type:
+ type: integer
+ allOf:
+ - "$ref": "#/components/schemas/PersonType"
+ description: "`1`, `2`, `3` 表示 `个人`, `公司`, `组合`"
+ career:
+ type: array
+ items:
+ "$ref": "#/components/schemas/PersonCareer"
+ images:
+ title: Images
+ type: object
+ allOf:
+ - "$ref": "#/components/schemas/PersonImages"
+ description: object with some size of images, this object maybe `null`
+ created_at:
+ title: Created At
+ type: string
+ format: date-time
Revision:
title: Revision
required:
@@ -2694,23 +3254,41 @@ components:
$ref: "./components/subject_tags.yaml"
SubjectType:
$ref: "./components/subject_type.yaml"
+ SubjectBookCategory:
+ $ref: "./components/subject_cat_book.yaml"
+ SubjectAnimeCategory:
+ $ref: "./components/subject_cat_anime.yaml"
+ SubjectGameCategory:
+ $ref: "./components/subject_cat_game.yaml"
+ SubjectRealCategory:
+ $ref: "./components/subject_cat_real.yaml"
+ SubjectCategory:
+ anyOf:
+ - $ref: "#/components/schemas/SubjectBookCategory"
+ - $ref: "#/components/schemas/SubjectAnimeCategory"
+ - $ref: "#/components/schemas/SubjectGameCategory"
+ - $ref: "#/components/schemas/SubjectRealCategory"
UserSubjectCollection:
- $ref: "./components/subject_collection.yaml"
+ $ref: "./components/user_subject_collection.yaml"
UserSubjectCollectionModifyPayload:
- $ref: "./components/subject_collection_modify_payload.yaml"
+ $ref: "./components/user_subject_collection_modify_payload.yaml"
UserEpisodeCollection:
$ref: "./components/get-user-episodes-collection.yaml"
v0_RelatedSubject:
title: RelatedSubject
required:
- id
+ - type
- staff
+ - name
- name_cn
type: object
properties:
id:
title: ID
type: integer
+ type:
+ $ref: "#/components/schemas/SubjectType"
staff:
title: Staff
type: string
diff --git a/package-lock.json b/package-lock.json
index be510e9b1..8b8d05ba4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,27 +1,27 @@
{
"name": "chii",
- "version": "0.33.19",
+ "version": "0.34.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "chii",
- "version": "0.33.19",
+ "version": "0.34.0",
"dependencies": {
- "@apidevtools/json-schema-ref-parser": "^11.6.1",
+ "@apidevtools/json-schema-ref-parser": "^11.7.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21"
},
"devDependencies": {
"colors": "^1.4.0",
"oas-validator": "^5.0.8",
- "prettier": "^3.2.5"
+ "prettier": "^3.4.1"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
- "version": "11.6.1",
- "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.6.1.tgz",
- "integrity": "sha512-DxjgKBCoyReu4p5HMvpmgSOfRhhBcuf5V5soDDRgOTZMwsA4KSFzol1abFZgiCTE11L2kKGca5Md9GwDdXVBwQ==",
+ "version": "11.7.2",
+ "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz",
+ "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.15",
@@ -294,9 +294,9 @@
}
},
"node_modules/prettier": {
- "version": "3.2.5",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
- "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz",
+ "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
@@ -444,9 +444,9 @@
},
"dependencies": {
"@apidevtools/json-schema-ref-parser": {
- "version": "11.6.1",
- "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.6.1.tgz",
- "integrity": "sha512-DxjgKBCoyReu4p5HMvpmgSOfRhhBcuf5V5soDDRgOTZMwsA4KSFzol1abFZgiCTE11L2kKGca5Md9GwDdXVBwQ==",
+ "version": "11.7.2",
+ "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz",
+ "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==",
"requires": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.15",
@@ -664,9 +664,9 @@
}
},
"prettier": {
- "version": "3.2.5",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
- "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz",
+ "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==",
"dev": true
},
"reftools": {
diff --git a/package.json b/package.json
index 068e5c511..c49a7c45d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "chii",
- "version": "0.33.19",
+ "version": "0.34.0",
"description": "tools to bundle openapi spec, not used in our server",
"private": true,
"scripts": {
@@ -12,14 +12,14 @@
"printWidth": 120
},
"dependencies": {
- "@apidevtools/json-schema-ref-parser": "^11.6.1",
+ "@apidevtools/json-schema-ref-parser": "^11.7.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21"
},
"devDependencies": {
"colors": "^1.4.0",
"oas-validator": "^5.0.8",
- "prettier": "^3.2.5"
+ "prettier": "^3.4.1"
},
"nodemonConfig": {
"restartable": "rs",
diff --git a/pkg/duration/duration_test.go b/pkg/duration/duration_test.go
index 3f87e5cd6..a05a2014e 100644
--- a/pkg/duration/duration_test.go
+++ b/pkg/duration/duration_test.go
@@ -64,7 +64,6 @@ func TestParse(t *testing.T) {
}
for _, tc := range testcases {
- tc := tc
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
actual, err := duration.Parse(tc.Input)
diff --git a/pkg/vars/staff.go.json b/pkg/vars/staff.go.json
index 198929657..abfba3be4 100644
--- a/pkg/vars/staff.go.json
+++ b/pkg/vars/staff.go.json
@@ -53,6 +53,18 @@
"EN": "Original Character Design",
"JP": "キャラクター原案",
"RDF": ""
+ },
+ "2010": {
+ "CN": "脚本",
+ "EN": "",
+ "JP": "シナリオ",
+ "RDF": ""
+ },
+ "2011": {
+ "CN": "文库",
+ "EN": "",
+ "JP": "文庫",
+ "RDF": ""
}
},
"2": {
@@ -635,9 +647,34 @@
"RDF": ""
},
"3011": {
- "en": "O.P.",
- "cn": "出版方",
- "jp": "音楽出版社"
+ "CN": "出版方",
+ "EN": "O.P.",
+ "JP": "音楽出版社",
+ "RDF": ""
+ },
+ "3012": {
+ "CN": "母带制作",
+ "EN": "Mastering",
+ "JP": "",
+ "RDF": ""
+ },
+ "3013": {
+ "CN": "混音",
+ "EN": "Mixing",
+ "JP": "",
+ "RDF": ""
+ },
+ "3014": {
+ "CN": "乐器",
+ "EN": "Instrument",
+ "JP": "",
+ "RDF": ""
+ },
+ "3015": {
+ "CN": "声乐",
+ "EN": "Vocal",
+ "JP": "",
+ "RDF": ""
}
},
"4": {
@@ -942,6 +979,12 @@
"EN": "Production",
"JP": "製作 製作スタジオ",
"RDF": ""
+ },
+ "4019": {
+ "CN": "出品",
+ "EN": "",
+ "JP": "配給",
+ "RDF": ""
}
}
}
diff --git a/pkg/wiki/spec_test.go b/pkg/wiki/spec_test.go
index e3c9c0f0b..c030c4b5d 100644
--- a/pkg/wiki/spec_test.go
+++ b/pkg/wiki/spec_test.go
@@ -88,7 +88,6 @@ func TestAgainstInvalidSpec(t *testing.T) {
for _, file := range files {
// name := file.Name()
- file := file
t.Run(file.Name(), func(t *testing.T) {
t.Parallel()
raw, err := os.ReadFile(filepath.Join(caseRoot, file.Name()))
diff --git a/readme.md b/readme.md
index 91016cd9e..cc8ad863e 100644
--- a/readme.md
+++ b/readme.md
@@ -1,11 +1,8 @@
新后端服务器。
-![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/Bangumi/server?style=flat-square)
-[![Codecov](https://img.shields.io/codecov/c/github/Bangumi/server?style=flat-square)](https://app.codecov.io/gh/Bangumi/server)
-
## Requirements
-- [Go 1.22](https://go.dev/)
+- ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/Bangumi/server?style=flat-square)
- [go-task](https://taskfile.dev/installation/),使用 `task` 查看所有的构建目标。
- [golangci-lint](https://golangci-lint.run/),使用 `task lint` 运行 linter。
@@ -78,13 +75,13 @@ ORM: [GORM](https://github.com/go-gorm/gorm) 和 [GORM Gen](https://github.com/g
启动 HTTP server
```shell
-go run main.go --config config.yaml web
+task web
```
启动 kafka consumer
```shell
-go run main.go canal --config config.yaml
+task consumer
```
### 后端环境
diff --git a/web/dev.go b/web/dev.go
index 8d3097a8e..786f6aa68 100644
--- a/web/dev.go
+++ b/web/dev.go
@@ -15,9 +15,6 @@
package web
import (
- "net/http"
- "net/http/pprof"
-
"github.com/labstack/echo/v4"
"github.com/bangumi/server/internal/pkg/random"
@@ -33,11 +30,3 @@ func genFakeRequestID(next echo.HandlerFunc) echo.HandlerFunc {
return next(c)
}
}
-
-func addProfile(app *echo.Echo) {
- app.GET("/debug/pprof/cmdline", echo.WrapHandler(http.HandlerFunc(pprof.Cmdline)))
- app.GET("/debug/pprof/profile", echo.WrapHandler(http.HandlerFunc(pprof.Profile)))
- app.GET("/debug/pprof/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol)))
- app.GET("/debug/pprof/trace", echo.WrapHandler(http.HandlerFunc(pprof.Trace)))
- app.Any("/debug/pprof/", echo.WrapHandler(http.HandlerFunc(pprof.Index)))
-}
diff --git a/web/error.go b/web/error.go
index 94daeff37..fa57db47a 100644
--- a/web/error.go
+++ b/web/error.go
@@ -15,6 +15,7 @@
package web
import (
+ "context"
"errors"
"net/http"
@@ -36,11 +37,14 @@ func globalNotFoundHandler(c echo.Context) error {
})
}
+//nolint:funlen
func getDefaultErrorHandler() echo.HTTPErrorHandler {
var log = logger.Named("http.err").
WithOptions(zap.AddStacktrace(zapcore.PanicLevel), zap.WithCaller(false))
return func(err error, c echo.Context) {
+ reqID := c.Request().Header.Get(cf.HeaderRequestID)
+
{
var e res.HTTPError
if errors.As(err, &e) {
@@ -48,6 +52,7 @@ func getDefaultErrorHandler() echo.HTTPErrorHandler {
_ = c.JSON(e.Code, res.Error{
Title: http.StatusText(e.Code),
Description: e.Msg,
+ RequestID: reqID,
Details: util.Detail(c),
})
return
@@ -60,25 +65,45 @@ func getDefaultErrorHandler() echo.HTTPErrorHandler {
log.Error("unexpected echo error",
zap.Int("code", e.Code),
zap.Any("message", e.Message),
- zap.String("path", c.Request().URL.Path),
- zap.String("query", c.Request().URL.RawQuery),
- zap.String("cf-ray", c.Request().Header.Get(cf.HeaderRequestID)),
+ zap.String("request_method", c.Request().Method),
+ zap.String("request_uri", c.Request().URL.Path),
+ zap.String("request_query", c.Request().URL.RawQuery),
+ zap.String("request_id", reqID),
)
_ = c.JSON(http.StatusInternalServerError, res.Error{
Title: http.StatusText(e.Code),
Description: e.Error(),
+ RequestID: reqID,
Details: util.DetailWithErr(c, err),
})
return
}
}
+ if errors.Is(err, context.Canceled) {
+ log.Error("request timeout",
+ zap.String("message", err.Error()),
+ zap.String("request_method", c.Request().Method),
+ zap.String("request_uri", c.Request().URL.Path),
+ zap.String("request_query", c.Request().URL.RawQuery),
+ zap.String("request_id", reqID),
+ )
+
+ _ = c.JSON(http.StatusInternalServerError, res.Error{
+ Title: "request timeout",
+ Description: "request timeout",
+ RequestID: c.Request().Header.Get(cf.HeaderRequestID),
+ })
+ return
+ }
+
log.Error("unexpected error",
zap.Error(err),
- zap.String("path", c.Path()),
- zap.String("query", c.Request().URL.RawQuery),
- zap.String("cf-ray", c.Request().Header.Get(cf.HeaderRequestID)),
+ zap.String("request_method", c.Request().Method),
+ zap.String("request_uri", c.Request().URL.Path),
+ zap.String("request_query", c.Request().URL.RawQuery),
+ zap.String("request_id", reqID),
)
// unexpected error, return internal server error
diff --git a/web/handler/character/character.go b/web/handler/character/character.go
index 2d42cfa2f..eacf54fd4 100644
--- a/web/handler/character/character.go
+++ b/web/handler/character/character.go
@@ -20,62 +20,36 @@ import (
"github.com/bangumi/server/config"
"github.com/bangumi/server/ctrl"
"github.com/bangumi/server/internal/character"
- "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/collections"
"github.com/bangumi/server/internal/person"
- "github.com/bangumi/server/internal/pkg/compat"
- "github.com/bangumi/server/internal/pkg/null"
"github.com/bangumi/server/internal/subject"
- "github.com/bangumi/server/pkg/wiki"
- "github.com/bangumi/server/web/res"
)
type Character struct {
- ctrl ctrl.Ctrl
- person person.Service
- c character.Repo
- subject subject.Repo
- log *zap.Logger
- cfg config.AppConfig
+ ctrl ctrl.Ctrl
+ person person.Service
+ character character.Repo
+ subject subject.Repo
+ collect collections.Repo
+ log *zap.Logger
+ cfg config.AppConfig
}
func New(
- p person.Service,
+ person person.Service,
ctrl ctrl.Ctrl,
- c character.Repo,
+ character character.Repo,
subject subject.Repo,
+ collect collections.Repo,
log *zap.Logger,
) (Character, error) {
return Character{
- ctrl: ctrl,
- c: c,
- subject: subject,
- person: p,
- log: log.Named("handler.Character"),
- cfg: config.AppConfig{},
+ ctrl: ctrl,
+ character: character,
+ subject: subject,
+ person: person,
+ collect: collect,
+ log: log.Named("handler.Character"),
+ cfg: config.AppConfig{},
}, nil
}
-
-func convertModelCharacter(s model.Character) res.CharacterV0 {
- img := res.PersonImage(s.Image)
-
- return res.CharacterV0{
- ID: s.ID,
- Type: s.Type,
- Name: s.Name,
- NSFW: s.NSFW,
- Images: img,
- Summary: s.Summary,
- Infobox: compat.V0Wiki(wiki.ParseOmitError(s.Infobox).NonZero()),
- Gender: null.NilString(res.GenderMap[s.FieldGender]),
- BloodType: null.NilUint8(s.FieldBloodType),
- BirthYear: null.NilUint16(s.FieldBirthYear),
- BirthMon: null.NilUint8(s.FieldBirthMon),
- BirthDay: null.NilUint8(s.FieldBirthDay),
- Stat: res.Stat{
- Comments: s.CommentCount,
- Collects: s.CollectCount,
- },
- Redirect: s.Redirect,
- Locked: s.Locked,
- }
-}
diff --git a/web/handler/character/character_test.go b/web/handler/character/character_test.go
index 305c2b06a..ddf5cff3d 100644
--- a/web/handler/character/character_test.go
+++ b/web/handler/character/character_test.go
@@ -92,7 +92,6 @@ func TestCharacter_GetImage(t *testing.T) {
app := test.GetWebApp(t, test.Mock{CharacterRepo: m})
for _, imageType := range []string{"large", "grid", "medium", "small"} {
- imageType := imageType
t.Run(imageType, func(t *testing.T) {
t.Parallel()
diff --git a/web/handler/character/collect.go b/web/handler/character/collect.go
new file mode 100644
index 000000000..f8d6c60b6
--- /dev/null
+++ b/web/handler/character/collect.go
@@ -0,0 +1,90 @@
+// 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 character
+
+import (
+ "errors"
+
+ "github.com/labstack/echo/v4"
+
+ "github.com/bangumi/server/domain/gerr"
+ "github.com/bangumi/server/internal/collections/domain/collection"
+ "github.com/bangumi/server/web/accessor"
+ "github.com/bangumi/server/web/req"
+ "github.com/bangumi/server/web/res"
+)
+
+func (h Character) CollectCharacter(c echo.Context) error {
+ cid, err := req.ParseID(c.Param("id"))
+ if err != nil {
+ return err
+ }
+ uid := accessor.GetFromCtx(c).ID
+ return h.collectCharacter(c, cid, uid)
+}
+
+func (h Character) UncollectCharacter(c echo.Context) error {
+ cid, err := req.ParseID(c.Param("id"))
+ if err != nil {
+ return err
+ }
+ uid := accessor.GetFromCtx(c).ID
+ return h.uncollectCharacter(c, cid, uid)
+}
+
+func (h Character) collectCharacter(c echo.Context, cid uint32, uid uint32) error {
+ ctx := c.Request().Context()
+ // check if the character exists
+ if _, err := h.character.Get(ctx, cid); err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return res.ErrNotFound
+ }
+ return res.InternalError(c, err, "get character error")
+ }
+ // check if the user has collected the character
+ if _, err := h.collect.GetPersonCollection(ctx, uid, collection.PersonCollectCategoryCharacter, cid); err == nil {
+ return nil // already collected
+ } else if !errors.Is(err, gerr.ErrNotFound) {
+ return res.InternalError(c, err, "get character collect error")
+ }
+ // add the collect
+ if err := h.collect.AddPersonCollection(ctx, uid, collection.PersonCollectCategoryCharacter, cid); err != nil {
+ return res.InternalError(c, err, "add character collect failed")
+ }
+ return nil
+}
+
+func (h Character) uncollectCharacter(c echo.Context, cid uint32, uid uint32) error {
+ ctx := c.Request().Context()
+ // check if the character exists
+ if _, err := h.character.Get(ctx, cid); err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return res.ErrNotFound
+ }
+ return res.InternalError(c, err, "get character error")
+ }
+ // check if the user has collected the character
+ if _, err := h.collect.GetPersonCollection(ctx, uid, collection.PersonCollectCategoryCharacter, cid); err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return res.NotFound("character not collected")
+ }
+ return res.InternalError(c, err, "get character collect error")
+ }
+ // remove the collect
+ if err := h.collect.RemovePersonCollection(ctx, uid, collection.PersonCollectCategoryCharacter, cid); err != nil {
+ return res.InternalError(c, err, "remove character collect failed")
+ }
+ return nil
+}
diff --git a/web/handler/character/get.go b/web/handler/character/get.go
index 5def90ca7..8e9fde474 100644
--- a/web/handler/character/get.go
+++ b/web/handler/character/get.go
@@ -36,7 +36,7 @@ func (h Character) Get(c echo.Context) error {
return err
}
- r, err := h.c.Get(c.Request().Context(), id)
+ r, err := h.character.Get(c.Request().Context(), id)
if err != nil {
if errors.Is(err, gerr.ErrNotFound) {
return res.ErrNotFound
@@ -53,7 +53,7 @@ func (h Character) Get(c echo.Context) error {
return res.ErrNotFound
}
- return c.JSON(http.StatusOK, convertModelCharacter(r))
+ return c.JSON(http.StatusOK, res.ConvertModelCharacter(r))
}
func (h Character) GetImage(c echo.Context) error {
@@ -62,7 +62,7 @@ func (h Character) GetImage(c echo.Context) error {
return err
}
- p, err := h.c.Get(c.Request().Context(), id)
+ p, err := h.character.Get(c.Request().Context(), id)
if err != nil {
if errors.Is(err, gerr.ErrNotFound) {
return res.ErrNotFound
diff --git a/web/handler/character/get_related_persons.go b/web/handler/character/get_related_persons.go
index d974d485b..d82bc8c18 100644
--- a/web/handler/character/get_related_persons.go
+++ b/web/handler/character/get_related_persons.go
@@ -33,7 +33,7 @@ func (h Character) GetRelatedPersons(c echo.Context) error {
return err
}
- _, err = h.c.Get(c.Request().Context(), id)
+ _, err = h.character.Get(c.Request().Context(), id)
if err != nil {
if errors.Is(err, gerr.ErrNotFound) {
return res.ErrNotFound
@@ -63,6 +63,7 @@ func (h Character) GetRelatedPersons(c echo.Context) error {
Type: cast.Person.Type,
Images: res.PersonImage(cast.Person.Image),
SubjectID: cast.Subject.ID,
+ SubjectType: cast.Subject.TypeID,
SubjectName: cast.Subject.Name,
SubjectNameCn: cast.Subject.NameCN,
Staff: res.CharacterStaffString(mSubjectRelations[cast.Subject.ID]),
diff --git a/web/handler/character/get_related_subjects.go b/web/handler/character/get_related_subjects.go
index 9ed59b9c4..ce228d015 100644
--- a/web/handler/character/get_related_subjects.go
+++ b/web/handler/character/get_related_subjects.go
@@ -50,6 +50,7 @@ func (h Character) GetRelatedSubjects(c echo.Context) error {
s := relation.Subject
response[i] = res.CharacterRelatedSubject{
ID: s.ID,
+ Type: s.TypeID,
Name: s.Name,
NameCn: s.NameCN,
Staff: res.CharacterStaffString(relation.TypeID),
@@ -64,7 +65,7 @@ func (h Character) getCharacterRelatedSubjects(
ctx context.Context,
characterID model.CharacterID,
) (model.Character, []model.SubjectCharacterRelation, error) {
- character, err := h.c.Get(ctx, characterID)
+ character, err := h.character.Get(ctx, characterID)
if err != nil {
return model.Character{}, nil, errgo.Wrap(err, "get character")
}
diff --git a/web/handler/person/collect.go b/web/handler/person/collect.go
new file mode 100644
index 000000000..d1ec9fa62
--- /dev/null
+++ b/web/handler/person/collect.go
@@ -0,0 +1,92 @@
+// 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 person
+
+import (
+ "errors"
+
+ "github.com/labstack/echo/v4"
+
+ "github.com/bangumi/server/domain/gerr"
+ "github.com/bangumi/server/internal/collections/domain/collection"
+ "github.com/bangumi/server/web/accessor"
+ "github.com/bangumi/server/web/req"
+ "github.com/bangumi/server/web/res"
+)
+
+func (h Person) CollectPerson(c echo.Context) error {
+ pid, err := req.ParseID(c.Param("id"))
+ if err != nil {
+ return err
+ }
+
+ uid := accessor.GetFromCtx(c).ID
+ return h.collectPerson(c, pid, uid)
+}
+
+func (h Person) UncollectPerson(c echo.Context) error {
+ pid, err := req.ParseID(c.Param("id"))
+ if err != nil {
+ return err
+ }
+
+ uid := accessor.GetFromCtx(c).ID
+ return h.uncollectPerson(c, pid, uid)
+}
+
+func (h Person) collectPerson(c echo.Context, pid uint32, uid uint32) error {
+ ctx := c.Request().Context()
+ // check if the person exists
+ if _, err := h.person.Get(ctx, pid); err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return res.ErrNotFound
+ }
+ return res.InternalError(c, err, "get person error")
+ }
+ // check if the user has collected the person
+ if _, err := h.collect.GetPersonCollection(ctx, uid, collection.PersonCollectCategoryPerson, pid); err == nil {
+ return nil // already collected
+ } else if !errors.Is(err, gerr.ErrNotFound) {
+ return res.InternalError(c, err, "get person collect error")
+ }
+ // add the collect
+ if err := h.collect.AddPersonCollection(ctx, uid, collection.PersonCollectCategoryPerson, pid); err != nil {
+ return res.InternalError(c, err, "add person collect failed")
+ }
+ return nil
+}
+
+func (h Person) uncollectPerson(c echo.Context, pid uint32, uid uint32) error {
+ ctx := c.Request().Context()
+ // check if the person exists
+ if _, err := h.person.Get(ctx, pid); err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return res.ErrNotFound
+ }
+ return res.InternalError(c, err, "get person error")
+ }
+ // check if the user has collected the person
+ if _, err := h.collect.GetPersonCollection(ctx, uid, collection.PersonCollectCategoryPerson, pid); err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return res.NotFound("person not collected")
+ }
+ return res.InternalError(c, err, "get person collect error")
+ }
+ // remove the collect
+ if err := h.collect.RemovePersonCollection(ctx, uid, collection.PersonCollectCategoryPerson, pid); err != nil {
+ return res.InternalError(c, err, "remove person collect failed")
+ }
+ return nil
+}
diff --git a/web/handler/person/get_related_characters.go b/web/handler/person/get_related_characters.go
index 098a38b96..60a530fb6 100644
--- a/web/handler/person/get_related_characters.go
+++ b/web/handler/person/get_related_characters.go
@@ -43,7 +43,6 @@ func (h Person) GetRelatedCharacters(c echo.Context) error {
}
return errgo.Wrap(err, "failed to get person")
}
-
if r.Redirect != 0 {
return res.ErrNotFound
}
@@ -60,7 +59,7 @@ func (h Person) GetRelatedCharacters(c echo.Context) error {
SubjectID: relation.Subject.ID,
}
}
- subjectRelations, err := h.c.GetSubjectRelationByIDs(c.Request().Context(), compositeIDs)
+ subjectRelations, err := h.character.GetSubjectRelationByIDs(c.Request().Context(), compositeIDs)
if err != nil {
return errgo.Wrap(err, "CharacterRepo.GetRelations")
}
@@ -84,6 +83,7 @@ func (h Person) GetRelatedCharacters(c echo.Context) error {
Type: rel.Character.Type,
Images: res.PersonImage(rel.Character.Image),
SubjectID: rel.Subject.ID,
+ SubjectType: rel.Subject.TypeID,
SubjectName: rel.Subject.Name,
SubjectNameCn: rel.Subject.NameCN,
Staff: res.CharacterStaffString(subjectTypeID),
@@ -96,7 +96,7 @@ func (h Person) GetRelatedCharacters(c echo.Context) error {
func (h Person) getPersonRelatedCharacters(
ctx context.Context, personID model.PersonID,
) ([]model.PersonCharacterRelation, error) {
- relations, err := h.c.GetPersonRelated(ctx, personID)
+ relations, err := h.character.GetPersonRelated(ctx, personID)
if err != nil {
return nil, errgo.Wrap(err, "CharacterRepo.GetPersonRelated")
}
@@ -112,7 +112,7 @@ func (h Person) getPersonRelatedCharacters(
subjectIDs[i] = relation.SubjectID
}
- characters, err := h.c.GetByIDs(ctx, characterIDs)
+ characters, err := h.character.GetByIDs(ctx, characterIDs)
if err != nil {
return nil, errgo.Wrap(err, "CharacterRepo.GetByIDs")
}
diff --git a/web/handler/person/get_related_subjects.go b/web/handler/person/get_related_subjects.go
index 062002c80..89aada773 100644
--- a/web/handler/person/get_related_subjects.go
+++ b/web/handler/person/get_related_subjects.go
@@ -60,6 +60,7 @@ func (h Person) GetRelatedSubjects(c echo.Context) error {
for i, relation := range relations {
response[i] = res.PersonRelatedSubject{
SubjectID: relation.Subject.ID,
+ Type: relation.Subject.TypeID,
Staff: vars.StaffMap[relation.Subject.TypeID][relation.TypeID].String(),
Name: relation.Subject.Name,
NameCn: relation.Subject.NameCN,
diff --git a/web/handler/person/person.go b/web/handler/person/person.go
index 3146859ae..a087c9cef 100644
--- a/web/handler/person/person.go
+++ b/web/handler/person/person.go
@@ -17,27 +17,31 @@ package person
import (
"github.com/bangumi/server/ctrl"
"github.com/bangumi/server/internal/character"
+ "github.com/bangumi/server/internal/collections"
"github.com/bangumi/server/internal/person"
"github.com/bangumi/server/internal/subject"
)
type Person struct {
- person person.Repo
- c character.Repo
- ctrl ctrl.Ctrl
- subject subject.Repo
+ ctrl ctrl.Ctrl
+ person person.Repo
+ character character.Repo
+ subject subject.Repo
+ collect collections.Repo
}
func New(
ctrl ctrl.Ctrl,
person person.Repo,
subject subject.Repo,
- c character.Repo,
+ character character.Repo,
+ collect collections.Repo,
) (Person, error) {
return Person{
- person: person,
- ctrl: ctrl,
- c: c,
- subject: subject,
+ ctrl: ctrl,
+ person: person,
+ character: character,
+ subject: subject,
+ collect: collect,
}, nil
}
diff --git a/web/handler/person/person_test.go b/web/handler/person/person_test.go
index 64dd45f24..073e5b00f 100644
--- a/web/handler/person/person_test.go
+++ b/web/handler/person/person_test.go
@@ -61,7 +61,6 @@ func TestPerson_GetImage(t *testing.T) {
app := test.GetWebApp(t, test.Mock{PersonRepo: m})
for _, imageType := range []string{"small", "grid", "large", "medium"} {
- imageType := imageType
t.Run(imageType, func(t *testing.T) {
t.Parallel()
diff --git a/web/handler/revision_test.go b/web/handler/revision_test.go
index 13c54d233..88690293d 100644
--- a/web/handler/revision_test.go
+++ b/web/handler/revision_test.go
@@ -62,7 +62,6 @@ func TestHandler_ListPersonRevision_Bad_ID(t *testing.T) {
badIDs := []string{"-1", "a", "0"}
for _, id := range badIDs {
- id := id
t.Run(id, func(t *testing.T) {
t.Parallel()
@@ -122,7 +121,6 @@ func TestHandler_ListSubjectRevision_Bad_ID(t *testing.T) {
badIDs := []string{"-1", "a", "0"}
for _, id := range badIDs {
- id := id
t.Run(id, func(t *testing.T) {
t.Parallel()
diff --git a/web/handler/search.go b/web/handler/search.go
index 38895045f..cf0aeec0c 100644
--- a/web/handler/search.go
+++ b/web/handler/search.go
@@ -16,8 +16,18 @@ package handler
import (
"github.com/labstack/echo/v4"
+
+ "github.com/bangumi/server/internal/search"
)
-func (h Handler) Search(c echo.Context) error {
- return h.search.Handle(c) //nolint:wrapcheck
+func (h Handler) SearchSubjects(c echo.Context) error {
+ return h.search.Handle(c, search.SearchTargetSubject) //nolint:wrapcheck
+}
+
+func (h Handler) SearchCharacters(c echo.Context) error {
+ return h.search.Handle(c, search.SearchTargetCharacter) //nolint:wrapcheck
+}
+
+func (h Handler) SearchPersons(c echo.Context) error {
+ return h.search.Handle(c, search.SearchTargetPerson) //nolint:wrapcheck
}
diff --git a/web/handler/subject/browse.go b/web/handler/subject/browse.go
new file mode 100644
index 000000000..1e08f763d
--- /dev/null
+++ b/web/handler/subject/browse.go
@@ -0,0 +1,160 @@
+// 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 subject
+
+import (
+ "net/http"
+
+ "github.com/labstack/echo/v4"
+ "github.com/trim21/errgo"
+
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/pkg/gstr"
+ "github.com/bangumi/server/internal/pkg/null"
+ "github.com/bangumi/server/internal/subject"
+ "github.com/bangumi/server/web/accessor"
+ "github.com/bangumi/server/web/req"
+ "github.com/bangumi/server/web/res"
+)
+
+func (h Subject) Browse(c echo.Context) error {
+ page, err := req.GetPageQuery(c, req.DefaultPageLimit, req.DefaultMaxPageLimit)
+ if err != nil {
+ return err
+ }
+
+ filter, err := parseBrowseQuery(c)
+ if err != nil {
+ return err
+ }
+
+ count, err := h.subject.Count(c.Request().Context(), *filter)
+ if err != nil {
+ return errgo.Wrap(err, "failed to count subjects")
+ }
+
+ if count == 0 {
+ return c.JSON(http.StatusOK, res.Paged{
+ Data: []res.SubjectV0{}, Total: count, Limit: page.Limit, Offset: page.Offset})
+ }
+
+ if err = page.Check(count); err != nil {
+ return err
+ }
+
+ subjects, err := h.subject.Browse(c.Request().Context(), *filter, page.Limit, page.Offset)
+ if err != nil {
+ return errgo.Wrap(err, "failed to browse subjects")
+ }
+ ids := make([]model.SubjectID, 0, len(subjects))
+ for _, s := range subjects {
+ ids = append(ids, s.ID)
+ }
+ tags, err := h.tag.GetByIDs(c.Request().Context(), ids)
+ if err != nil {
+ return errgo.Wrap(err, "failed to get tags")
+ }
+
+ data := make([]res.SubjectV0, 0, len(subjects))
+ for _, s := range subjects {
+ metaTags := tags[s.ID]
+ data = append(data, res.ToSubjectV0(s, 0, metaTags))
+ }
+
+ return c.JSON(http.StatusOK, res.Paged{Data: data, Total: count, Limit: page.Limit, Offset: page.Offset})
+}
+
+func parseBrowseQuery(c echo.Context) (*subject.BrowseFilter, error) {
+ filter := subject.BrowseFilter{}
+ u := accessor.GetFromCtx(c)
+ filter.NSFW = null.Bool{Value: false, Set: !u.AllowNSFW()}
+ if stype, err := req.ParseSubjectType(c.QueryParam("type")); err != nil {
+ return nil, res.BadRequest(err.Error())
+ } else {
+ filter.Type = stype
+ }
+ if catStr := c.QueryParam("cat"); catStr != "" {
+ if cat, err := req.ParseSubjectCategory(filter.Type, catStr); err != nil {
+ return nil, res.BadRequest(err.Error())
+ } else {
+ filter.Category = null.Uint16{Value: cat, Set: true}
+ }
+ }
+ if filter.Type == model.SubjectTypeBook {
+ if seriesStr := c.QueryParam("series"); seriesStr != "" {
+ if series, err := gstr.ParseBool(seriesStr); err != nil {
+ return nil, res.BadRequest(err.Error())
+ } else {
+ filter.Series = null.Bool{Value: series, Set: true}
+ }
+ }
+ }
+ if filter.Type == model.SubjectTypeGame {
+ if platform := c.QueryParam("platform"); platform != "" {
+ // TODO: check if platform is valid
+ filter.Platform = null.String{Value: platform, Set: true}
+ }
+ }
+ if sort := c.QueryParam("sort"); sort != "" {
+ switch sort {
+ case "rank", "date":
+ filter.Sort = null.String{Value: sort, Set: true}
+ default:
+ return nil, res.BadRequest("unknown sort: " + sort)
+ }
+ }
+ if year, err := GetYearQuery(c); err != nil {
+ return nil, err
+ } else {
+ filter.Year = year
+ }
+ if month, err := GetMonthQuery(c); err != nil {
+ return nil, err
+ } else {
+ filter.Month = month
+ }
+
+ return &filter, nil
+}
+
+func GetYearQuery(c echo.Context) (null.Int32, error) {
+ yearStr := c.QueryParam("year")
+ if yearStr == "" {
+ return null.Int32{}, nil
+ }
+ if year, err := gstr.ParseInt32(yearStr); err != nil {
+ return null.Int32{}, res.BadRequest(err.Error())
+ } else {
+ if year < 1900 || year > 3000 {
+ return null.Int32{}, res.BadRequest("invalid year: " + yearStr)
+ }
+ return null.Int32{Value: year, Set: true}, nil
+ }
+}
+
+func GetMonthQuery(c echo.Context) (null.Int8, error) {
+ monthStr := c.QueryParam("month")
+ if monthStr == "" {
+ return null.Int8{}, nil
+ }
+ if month, err := gstr.ParseInt8(monthStr); err != nil {
+ return null.Int8{}, res.BadRequest(err.Error())
+ } else {
+ if month < 1 || month > 12 {
+ return null.Int8{}, res.BadRequest("invalid month: " + monthStr)
+ }
+ return null.Int8{Value: month, Set: true}, nil
+ }
+}
diff --git a/web/handler/subject/get.go b/web/handler/subject/get.go
index 6e57bfbc4..4aa9179b7 100644
--- a/web/handler/subject/get.go
+++ b/web/handler/subject/get.go
@@ -21,18 +21,13 @@ import (
"github.com/labstack/echo/v4"
"github.com/trim21/errgo"
- "go.uber.org/zap"
"github.com/bangumi/server/domain/gerr"
"github.com/bangumi/server/internal/episode"
"github.com/bangumi/server/internal/model"
- "github.com/bangumi/server/internal/pkg/compat"
- "github.com/bangumi/server/internal/pkg/generic/slice"
- "github.com/bangumi/server/internal/pkg/logger"
"github.com/bangumi/server/internal/pkg/null"
"github.com/bangumi/server/internal/subject"
"github.com/bangumi/server/pkg/vars"
- "github.com/bangumi/server/pkg/wiki"
"github.com/bangumi/server/web/accessor"
"github.com/bangumi/server/web/req"
"github.com/bangumi/server/web/res"
@@ -66,24 +61,12 @@ func (h Subject) Get(c echo.Context) error {
return errgo.Wrap(err, "episode.Count")
}
- return c.JSON(http.StatusOK, convertModelSubject(s, totalEpisode))
-}
-
-func platformString(s model.Subject) *string {
- platform, ok := vars.PlatformMap[s.TypeID][s.PlatformID]
- if !ok && s.TypeID != 0 {
- logger.Warn("unknown platform",
- zap.Uint32("subject", s.ID),
- zap.Uint8("type", s.TypeID),
- zap.Uint16("platform", s.PlatformID),
- )
-
- return nil
+ metaTags, err := h.tag.Get(c.Request().Context(), s.ID)
+ if err != nil {
+ return err
}
- v := platform.String()
-
- return &v
+ return c.JSON(http.StatusOK, res.ToSubjectV0(s, totalEpisode, metaTags))
}
func (h Subject) GetImage(c echo.Context) error {
@@ -114,56 +97,6 @@ func (h Subject) GetImage(c echo.Context) error {
return c.Redirect(http.StatusFound, l)
}
-func convertModelSubject(s model.Subject, totalEpisode int64) res.SubjectV0 {
- return res.SubjectV0{
- TotalEpisodes: totalEpisode,
- ID: s.ID,
- Image: res.SubjectImage(s.Image),
- Summary: s.Summary,
- Name: s.Name,
- Platform: platformString(s),
- NameCN: s.NameCN,
- Date: null.NilString(s.Date),
- Infobox: compat.V0Wiki(wiki.ParseOmitError(s.Infobox).NonZero()),
- Volumes: s.Volumes,
- Redirect: s.Redirect,
- Eps: s.Eps,
- Tags: slice.Map(s.Tags, func(tag model.Tag) res.SubjectTag {
- return res.SubjectTag{
- Name: tag.Name,
- Count: tag.Count,
- }
- }),
- Collection: res.SubjectCollectionStat{
- OnHold: s.OnHold,
- Wish: s.Wish,
- Dropped: s.Dropped,
- Collect: s.Collect,
- Doing: s.Doing,
- },
- TypeID: s.TypeID,
- Locked: s.Locked(),
- NSFW: s.NSFW,
- Rating: res.Rating{
- Rank: s.Rating.Rank,
- Total: s.Rating.Total,
- Count: res.Count{
- Field1: s.Rating.Count.Field1,
- Field2: s.Rating.Count.Field2,
- Field3: s.Rating.Count.Field3,
- Field4: s.Rating.Count.Field4,
- Field5: s.Rating.Count.Field5,
- Field6: s.Rating.Count.Field6,
- Field7: s.Rating.Count.Field7,
- Field8: s.Rating.Count.Field8,
- Field9: s.Rating.Count.Field9,
- Field10: s.Rating.Count.Field10,
- },
- Score: s.Rating.Score,
- },
- }
-}
-
func readableRelation(destSubjectType model.SubjectType, relation uint16) string {
var r, ok = vars.RelationMap[destSubjectType][relation]
if !ok || relation == 1 {
diff --git a/web/handler/subject/get_test.go b/web/handler/subject/get_test.go
index 8f9b1fc91..ee531eaea 100644
--- a/web/handler/subject/get_test.go
+++ b/web/handler/subject/get_test.go
@@ -31,6 +31,7 @@ import (
"github.com/bangumi/server/internal/pkg/null"
"github.com/bangumi/server/internal/pkg/test"
"github.com/bangumi/server/internal/subject"
+ "github.com/bangumi/server/internal/tag"
"github.com/bangumi/server/web/accessor"
subjectHandler "github.com/bangumi/server/web/handler/subject"
"github.com/bangumi/server/web/internal/ctxkey"
@@ -56,7 +57,10 @@ func TestSubject_Get(t *testing.T) {
ep := mocks.NewEpisodeRepo(t)
ep.EXPECT().Count(mock.Anything, subjectID, mock.Anything).Return(3, nil)
- s, err := subjectHandler.New(nil, m, nil, nil, ep)
+ tagRepo := mocks.NewTagRepo(t)
+ tagRepo.EXPECT().Get(mock.Anything, mock.Anything).Return([]tag.Tag{}, nil)
+
+ s, err := subjectHandler.New(nil, m, nil, nil, ep, tagRepo)
require.NoError(t, err)
s.Routes(g)
@@ -134,7 +138,6 @@ func TestSubject_Get_bad_id(t *testing.T) {
app := test.GetWebApp(t, test.Mock{SubjectRepo: m})
for _, path := range []string{"/v0/subjects/0", "/v0/subjects/-1", "/v0/subjects/a"} {
- path := path
t.Run(path, func(t *testing.T) {
t.Parallel()
@@ -154,7 +157,6 @@ func TestSubject_GetImage_302(t *testing.T) {
app := test.GetWebApp(t, test.Mock{SubjectRepo: m})
for _, imageType := range []string{"small", "grid", "large", "medium", "common"} {
- imageType := imageType
t.Run(imageType, func(t *testing.T) {
t.Parallel()
diff --git a/web/handler/subject/related_persons.go b/web/handler/subject/related_persons.go
index af1d96196..cee74ad32 100644
--- a/web/handler/subject/related_persons.go
+++ b/web/handler/subject/related_persons.go
@@ -62,6 +62,7 @@ func (h Subject) GetRelatedPersons(c echo.Context) error {
Career: rel.Person.Careers(),
Type: rel.Person.Type,
ID: rel.Person.ID,
+ Eps: rel.Eps,
}
}
diff --git a/web/handler/subject/subject.go b/web/handler/subject/subject.go
index b1eec2e71..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,10 +47,12 @@ func New(
personRepo: personRepo,
subject: subject,
person: p,
+ tag: tag,
}, nil
}
func (h *Subject) Routes(g *echo.Group) {
+ g.GET("/subjects", h.Browse)
g.GET("/subjects/:id", h.Get)
g.GET("/subjects/:id/image", h.GetImage)
g.GET("/subjects/:id/persons", h.GetRelatedPersons)
diff --git a/web/handler/user/collection_test.go b/web/handler/user/collection_test.go
index cc54dfeb1..e25c85dcf 100644
--- a/web/handler/user/collection_test.go
+++ b/web/handler/user/collection_test.go
@@ -130,3 +130,151 @@ func TestUser_ListSubjectCollection_other_user(t *testing.T) {
Get(fmt.Sprintf("/v0/users/%s/collections/%d", username, subjectID)).
ExpectCode(http.StatusNotFound)
}
+
+func TestUser_GetPersonCollection(t *testing.T) {
+ t.Parallel()
+ const username = "ni"
+ const userID model.UserID = 7
+ const personID model.PersonID = 9
+
+ m := mocks.NewUserRepo(t)
+ m.EXPECT().GetByName(mock.Anything, username).Return(user.User{ID: userID, UserName: username}, nil)
+ c := mocks.NewCollectionRepo(t)
+ c.EXPECT().GetPersonCollection(mock.Anything, userID, mock.Anything, mock.Anything).
+ Return(collection.UserPersonCollection{UserID: userID, Category: "prsn", TargetID: personID}, nil)
+
+ p := mocks.NewPersonRepo(t)
+ p.EXPECT().Get(mock.Anything, personID).Return(model.Person{
+ ID: personID,
+ Name: "v",
+ }, nil)
+
+ app := test.GetWebApp(t, test.Mock{UserRepo: m, CollectionRepo: c, PersonRepo: p})
+
+ var r res.PersonCollection
+ resp := htest.New(t, app).
+ Get(fmt.Sprintf("/v0/users/%s/collections/-/persons/%d", username, personID)).
+ JSON(&r).
+ ExpectCode(http.StatusOK)
+
+ require.Equal(t, personID, r.ID, resp.BodyString())
+ require.Equal(t, "v", r.Name, resp.BodyString())
+}
+
+func TestUser_ListPersonCollection(t *testing.T) {
+ t.Parallel()
+ const username = "ni"
+ const userID model.UserID = 7
+ const personID model.PersonID = 9
+
+ m := mocks.NewUserRepo(t)
+ m.EXPECT().GetByName(mock.Anything, username).Return(user.User{ID: userID, UserName: username}, nil)
+
+ c := mocks.NewCollectionRepo(t)
+ c.EXPECT().ListPersonCollection(mock.Anything, userID, mock.Anything, 10, 0).
+ Return([]collection.UserPersonCollection{{UserID: userID, Category: "prsn", TargetID: personID}}, nil)
+ c.EXPECT().CountPersonCollections(mock.Anything, userID, mock.Anything).
+ Return(1, nil)
+
+ p := mocks.NewPersonRepo(t)
+ p.EXPECT().GetByIDs(mock.Anything, mock.Anything).Return(map[model.PersonID]model.Person{
+ personID: {ID: personID, Name: "v"},
+ }, nil)
+
+ app := test.GetWebApp(t, test.Mock{UserRepo: m, CollectionRepo: c, PersonRepo: p})
+
+ var r struct {
+ Data json.RawMessage `json:"data"`
+ Total int64 `json:"total"`
+ Limit int `json:"limit"`
+ Offset int `json:"offset"`
+ }
+
+ resp := htest.New(t, app).
+ Query("limit", "10").
+ Get(fmt.Sprintf("/v0/users/%s/collections/-/persons", username)).
+ JSON(&r).
+ ExpectCode(http.StatusOK)
+
+ var data []res.PersonCollection
+ require.NoError(t, json.Unmarshal(r.Data, &data))
+
+ require.Len(t, data, 1)
+
+ require.Equal(t, personID, data[0].ID, resp.BodyString())
+ require.Equal(t, "v", data[0].Name, resp.BodyString())
+}
+
+func TestUser_GetCharacterCollection(t *testing.T) {
+ t.Parallel()
+ const username = "ni"
+ const userID model.UserID = 7
+ const characterID model.CharacterID = 9
+
+ m := mocks.NewUserRepo(t)
+ m.EXPECT().GetByName(mock.Anything, username).Return(user.User{ID: userID, UserName: username}, nil)
+ c := mocks.NewCollectionRepo(t)
+ c.EXPECT().GetPersonCollection(mock.Anything, userID, mock.Anything, mock.Anything).
+ Return(collection.UserPersonCollection{UserID: userID, Category: "crt", TargetID: characterID}, nil)
+
+ p := mocks.NewCharacterRepo(t)
+ p.EXPECT().Get(mock.Anything, characterID).Return(model.Character{
+ ID: characterID,
+ Name: "v",
+ }, nil)
+
+ app := test.GetWebApp(t, test.Mock{UserRepo: m, CollectionRepo: c, CharacterRepo: p})
+
+ var r res.PersonCollection
+ resp := htest.New(t, app).
+ Get(fmt.Sprintf("/v0/users/%s/collections/-/characters/%d", username, characterID)).
+ JSON(&r).
+ ExpectCode(http.StatusOK)
+
+ require.Equal(t, characterID, r.ID, resp.BodyString())
+ require.Equal(t, "v", r.Name, resp.BodyString())
+}
+
+func TestUser_ListCharacterCollection(t *testing.T) {
+ t.Parallel()
+ const username = "ni"
+ const userID model.UserID = 7
+ const characterID model.CharacterID = 9
+
+ m := mocks.NewUserRepo(t)
+ m.EXPECT().GetByName(mock.Anything, username).Return(user.User{ID: userID, UserName: username}, nil)
+
+ c := mocks.NewCollectionRepo(t)
+ c.EXPECT().ListPersonCollection(mock.Anything, userID, mock.Anything, 10, 0).
+ Return([]collection.UserPersonCollection{{UserID: userID, Category: "crt", TargetID: characterID}}, nil)
+ c.EXPECT().CountPersonCollections(mock.Anything, userID, mock.Anything).
+ Return(1, nil)
+
+ p := mocks.NewCharacterRepo(t)
+ p.EXPECT().GetByIDs(mock.Anything, mock.Anything).Return(map[model.CharacterID]model.Character{
+ characterID: {ID: characterID, Name: "v"},
+ }, nil)
+
+ app := test.GetWebApp(t, test.Mock{UserRepo: m, CollectionRepo: c, CharacterRepo: p})
+
+ var r struct {
+ Data json.RawMessage `json:"data"`
+ Total int64 `json:"total"`
+ Limit int `json:"limit"`
+ Offset int `json:"offset"`
+ }
+
+ resp := htest.New(t, app).
+ Query("limit", "10").
+ Get(fmt.Sprintf("/v0/users/%s/collections/-/characters", username)).
+ JSON(&r).
+ ExpectCode(http.StatusOK)
+
+ var data []res.PersonCollection
+ require.NoError(t, json.Unmarshal(r.Data, &data))
+
+ require.Len(t, data, 1)
+
+ require.Equal(t, characterID, data[0].ID, resp.BodyString())
+ require.Equal(t, "v", data[0].Name, resp.BodyString())
+}
diff --git a/web/handler/user/get_character_collection.go b/web/handler/user/get_character_collection.go
new file mode 100644
index 000000000..42657e601
--- /dev/null
+++ b/web/handler/user/get_character_collection.go
@@ -0,0 +1,81 @@
+// 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 user
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/labstack/echo/v4"
+ "github.com/trim21/errgo"
+
+ "github.com/bangumi/server/domain/gerr"
+ "github.com/bangumi/server/internal/collections/domain/collection"
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/web/req"
+ "github.com/bangumi/server/web/res"
+)
+
+func (h User) GetCharacterCollection(c echo.Context) error {
+ username := c.Param("username")
+ if username == "" {
+ return res.BadRequest("missing require parameters `username`")
+ }
+
+ characterID, err := req.ParseID(c.Param("character_id"))
+ if err != nil {
+ return err
+ }
+
+ return h.getCharacterCollection(c, username, characterID)
+}
+
+func (h User) getCharacterCollection(c echo.Context, username string, characterID model.CharacterID) error {
+ const notFoundMessage = "character is not collected by user"
+
+ character, err := h.character.Get(c.Request().Context(), characterID)
+ if err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return res.ErrNotFound
+ }
+
+ return errgo.Wrap(err, "failed to get character")
+ }
+
+ u, err := h.user.GetByName(c.Request().Context(), username)
+ if err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return res.NotFound("user doesn't exist or has been removed")
+ }
+
+ return errgo.Wrap(err, "failed to get user by name")
+ }
+
+ collect, err := h.collect.GetPersonCollection(
+ c.Request().Context(),
+ u.ID,
+ collection.PersonCollectCategoryCharacter,
+ characterID,
+ )
+ if err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return res.NotFound(notFoundMessage)
+ }
+
+ return errgo.Wrap(err, "failed to get user's character collection")
+ }
+
+ return c.JSON(http.StatusOK, res.ConvertModelCharacterCollection(collect, character))
+}
diff --git a/web/handler/user/get_person_collection.go b/web/handler/user/get_person_collection.go
new file mode 100644
index 000000000..1240cb8a4
--- /dev/null
+++ b/web/handler/user/get_person_collection.go
@@ -0,0 +1,77 @@
+// 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 user
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/labstack/echo/v4"
+ "github.com/trim21/errgo"
+
+ "github.com/bangumi/server/domain/gerr"
+ "github.com/bangumi/server/internal/collections/domain/collection"
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/web/req"
+ "github.com/bangumi/server/web/res"
+)
+
+func (h User) GetPersonCollection(c echo.Context) error {
+ username := c.Param("username")
+ if username == "" {
+ return res.BadRequest("missing require parameters `username`")
+ }
+
+ personID, err := req.ParseID(c.Param("person_id"))
+ if err != nil {
+ return err
+ }
+
+ return h.getPersonCollection(c, username, personID)
+}
+
+func (h User) getPersonCollection(c echo.Context, username string, personID model.PersonID) error {
+ const notFoundMessage = "person is not collected by user"
+
+ person, err := h.person.Get(c.Request().Context(), personID)
+ if err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return res.ErrNotFound
+ }
+
+ return errgo.Wrap(err, "failed to get person")
+ }
+
+ u, err := h.user.GetByName(c.Request().Context(), username)
+ if err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return res.NotFound("user doesn't exist or has been removed")
+ }
+
+ return errgo.Wrap(err, "failed to get user by name")
+ }
+
+ collect, err := h.collect.GetPersonCollection(
+ c.Request().Context(), u.ID, collection.PersonCollectCategoryPerson, personID)
+ if err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return res.NotFound(notFoundMessage)
+ }
+
+ return errgo.Wrap(err, "failed to get person collect")
+ }
+
+ return c.JSON(http.StatusOK, res.ConvertModelPersonCollection(collect, person))
+}
diff --git a/web/handler/user/list_character_collections.go b/web/handler/user/list_character_collections.go
new file mode 100644
index 000000000..38309bc53
--- /dev/null
+++ b/web/handler/user/list_character_collections.go
@@ -0,0 +1,103 @@
+// 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 user
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/labstack/echo/v4"
+ "github.com/trim21/errgo"
+
+ "github.com/bangumi/server/domain/gerr"
+ "github.com/bangumi/server/internal/collections/domain/collection"
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/pkg/generic/slice"
+ "github.com/bangumi/server/internal/user"
+ "github.com/bangumi/server/web/req"
+ "github.com/bangumi/server/web/res"
+)
+
+func (h User) ListCharacterCollection(c echo.Context) error {
+ page, err := req.GetPageQuery(c, req.DefaultPageLimit, req.DefaultMaxPageLimit)
+ if err != nil {
+ return err
+ }
+
+ username := c.Param("username")
+ if username == "" {
+ return res.BadRequest("missing require parameters `username`")
+ }
+
+ u, err := h.user.GetByName(c.Request().Context(), username)
+ if err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return res.NotFound("user doesn't exist or has been removed")
+ }
+
+ return errgo.Wrap(err, "user.GetByName")
+ }
+
+ return h.listCharacterCollection(c, u, page)
+}
+
+func (h User) listCharacterCollection(c echo.Context, u user.User, page req.PageQuery) error {
+ count, err := h.collect.CountPersonCollections(c.Request().Context(), u.ID, collection.PersonCollectCategoryCharacter)
+ if err != nil {
+ return errgo.Wrap(err, "failed to count user's character collections")
+ }
+
+ if count == 0 {
+ return c.JSON(http.StatusOK, res.Paged{Data: []int{}, Total: count, Limit: page.Limit, Offset: page.Offset})
+ }
+
+ if err = page.Check(count); err != nil {
+ return err
+ }
+
+ cols, err := h.collect.ListPersonCollection(
+ c.Request().Context(),
+ u.ID, collection.PersonCollectCategoryCharacter,
+ page.Limit, page.Offset)
+ if err != nil {
+ return errgo.Wrap(err, "failed to list user's person collections")
+ }
+
+ characterIDs := slice.Map(cols, func(item collection.UserPersonCollection) model.PersonID {
+ return item.TargetID
+ })
+
+ characterMap, err := h.character.GetByIDs(c.Request().Context(), characterIDs)
+ if err != nil {
+ return errgo.Wrap(err, "failed to get persons")
+ }
+
+ var data = make([]res.PersonCollection, 0, len(cols))
+
+ for _, col := range cols {
+ character, ok := characterMap[col.TargetID]
+ if !ok {
+ continue
+ }
+ data = append(data, res.ConvertModelCharacterCollection(col, character))
+ }
+
+ return c.JSON(http.StatusOK, res.Paged{
+ Data: data,
+ Total: count,
+ Limit: page.Limit,
+ Offset: page.Offset,
+ })
+}
diff --git a/web/handler/user/list_person_collections.go b/web/handler/user/list_person_collections.go
new file mode 100644
index 000000000..b48f1da4d
--- /dev/null
+++ b/web/handler/user/list_person_collections.go
@@ -0,0 +1,102 @@
+// 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 user
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/labstack/echo/v4"
+ "github.com/trim21/errgo"
+
+ "github.com/bangumi/server/domain/gerr"
+ "github.com/bangumi/server/internal/collections/domain/collection"
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/pkg/generic/slice"
+ "github.com/bangumi/server/internal/user"
+ "github.com/bangumi/server/web/req"
+ "github.com/bangumi/server/web/res"
+)
+
+func (h User) ListPersonCollection(c echo.Context) error {
+ page, err := req.GetPageQuery(c, req.DefaultPageLimit, req.DefaultMaxPageLimit)
+ if err != nil {
+ return err
+ }
+
+ username := c.Param("username")
+ if username == "" {
+ return res.BadRequest("missing require parameters `username`")
+ }
+
+ u, err := h.user.GetByName(c.Request().Context(), username)
+ if err != nil {
+ if errors.Is(err, gerr.ErrNotFound) {
+ return res.NotFound("user doesn't exist or has been removed")
+ }
+
+ return errgo.Wrap(err, "user.GetByName")
+ }
+
+ return h.listPersonCollection(c, u, page)
+}
+
+func (h User) listPersonCollection(c echo.Context, u user.User, page req.PageQuery) error {
+ count, err := h.collect.CountPersonCollections(c.Request().Context(), u.ID, collection.PersonCollectCategoryPerson)
+ if err != nil {
+ return errgo.Wrap(err, "failed to count user's person collections")
+ }
+
+ if count == 0 {
+ return c.JSON(http.StatusOK, res.Paged{Data: []int{}, Total: count, Limit: page.Limit, Offset: page.Offset})
+ }
+
+ if err = page.Check(count); err != nil {
+ return err
+ }
+
+ cols, err := h.collect.ListPersonCollection(
+ c.Request().Context(), u.ID, collection.PersonCollectCategoryPerson,
+ page.Limit, page.Offset)
+ if err != nil {
+ return errgo.Wrap(err, "failed to list user's person collections")
+ }
+
+ personIDs := slice.Map(cols, func(item collection.UserPersonCollection) model.PersonID {
+ return item.TargetID
+ })
+
+ personMap, err := h.person.GetByIDs(c.Request().Context(), personIDs)
+ if err != nil {
+ return errgo.Wrap(err, "failed to get persons")
+ }
+
+ var data = make([]res.PersonCollection, 0, len(cols))
+
+ for _, col := range cols {
+ person, ok := personMap[col.TargetID]
+ if !ok {
+ continue
+ }
+ data = append(data, res.ConvertModelPersonCollection(col, person))
+ }
+
+ return c.JSON(http.StatusOK, res.Paged{
+ Data: data,
+ Total: count,
+ Limit: page.Limit,
+ Offset: page.Offset,
+ })
+}
diff --git a/web/handler/user/me.go b/web/handler/user/me.go
index b0c48d45f..ca4fecc8e 100644
--- a/web/handler/user/me.go
+++ b/web/handler/user/me.go
@@ -16,32 +16,50 @@ package user
import (
"net/http"
+ "time"
"github.com/labstack/echo/v4"
"github.com/trim21/errgo"
+ "github.com/bangumi/server/internal/model"
"github.com/bangumi/server/web/accessor"
"github.com/bangumi/server/web/res"
)
+type CurrentUser struct {
+ Avatar res.Avatar `json:"avatar"`
+ Sign string `json:"sign"`
+ URL string `json:"url"`
+ Username string `json:"username"`
+ Nickname string `json:"nickname"`
+ ID model.UserID `json:"id"`
+ UserGroup uint8 `json:"user_group"`
+ RegistrationTime time.Time `json:"reg_time"`
+ Email string `json:"email"`
+ TimeOffset int8 `json:"time_offset"`
+}
+
func (h User) GetCurrent(c echo.Context) error {
u := accessor.GetFromCtx(c)
if !u.Login || u.ID == 0 {
return res.Unauthorized("need Login")
}
- user, err := h.user.GetByID(c.Request().Context(), u.ID)
+ user, err := h.user.GetFullUser(c.Request().Context(), u.ID)
if err != nil {
return errgo.Wrap(err, "failed to get user")
}
- return c.JSON(http.StatusOK, res.User{
- ID: user.ID,
- URL: "https://bgm.tv/user/" + user.UserName,
- Username: user.UserName,
- Nickname: user.NickName,
- UserGroup: user.UserGroup,
- Avatar: res.UserAvatar(user.Avatar),
- Sign: user.Sign,
+ return c.JSON(http.StatusOK, CurrentUser{
+ ID: user.ID,
+ URL: "https://bgm.tv/user/" + user.UserName,
+ Username: user.UserName,
+ Nickname: user.NickName,
+ UserGroup: user.UserGroup,
+ Avatar: res.UserAvatar(user.Avatar),
+ Sign: user.Sign,
+ RegistrationTime: user.RegistrationTime,
+ Email: user.Email,
+ TimeOffset: user.TimeOffset,
})
}
diff --git a/web/handler/user/patch_subject_collection.go b/web/handler/user/patch_subject_collection.go
index 56f196edb..557ae90bd 100644
--- a/web/handler/user/patch_subject_collection.go
+++ b/web/handler/user/patch_subject_collection.go
@@ -84,6 +84,10 @@ func (h User) updateOrCreateSubjectCollection(
return errgo.Wrap(err, "query.GetSubject")
}
+ if s.Ban != 0 {
+ return res.NotFound("subject locked or merged")
+ }
+
if s.TypeID != model.SubjectTypeBook {
if r.VolStatus.Set || r.EpStatus.Set {
return res.BadRequest("can't set 'vol_status' or 'ep_status' on non-book subject")
diff --git a/web/handler/user/patch_subject_collection_test.go b/web/handler/user/patch_subject_collection_test.go
index 25c59234d..86c3ab3b2 100644
--- a/web/handler/user/patch_subject_collection_test.go
+++ b/web/handler/user/patch_subject_collection_test.go
@@ -80,14 +80,14 @@ func TestUser_PatchSubjectCollection(t *testing.T) {
"type": 2,
"private": true,
"rate": 8,
- "tags": []string{"q", "vv"},
+ "tags": []string{"qq", "vv"},
}).
Patch(fmt.Sprintf("/v0/users/-/collections/%d", sid)).
ExpectCode(http.StatusNoContent)
require.Equal(t, collection.CollectPrivacySelf, s.Privacy())
require.Equal(t, "1 test_content 2", s.Comment())
- require.EqualValues(t, []string{"q", "vv"}, s.Tags())
+ require.EqualValues(t, []string{"qq", "vv"}, s.Tags())
require.EqualValues(t, 8, s.Rate())
}
@@ -118,7 +118,7 @@ func TestUser_PatchToNonExistsSubjectCollection(t *testing.T) {
"type": 1,
"private": true,
"rate": 8,
- "tags": []string{"q", "vv"},
+ "tags": []string{"qq", "vv"},
}).
Patch(fmt.Sprintf("/v0/users/-/collections/%d", sid)).
ExpectCode(http.StatusNotFound)
diff --git a/web/handler/user/post_subject_collection_test.go b/web/handler/user/post_subject_collection_test.go
index c4a9cb99f..32dc5ea6b 100644
--- a/web/handler/user/post_subject_collection_test.go
+++ b/web/handler/user/post_subject_collection_test.go
@@ -78,14 +78,14 @@ func TestUser_PostSubjectCollection(t *testing.T) {
"type": 2,
"private": true,
"rate": 8,
- "tags": []string{"q", "vv"},
+ "tags": []string{"qq", "vv"},
}).
Post(fmt.Sprintf("/v0/users/-/collections/%d", sid)).
ExpectCode(http.StatusAccepted)
require.Equal(t, collection.CollectPrivacySelf, s.Privacy())
require.Equal(t, "1 test_content 2", s.Comment())
- require.EqualValues(t, []string{"q", "vv"}, s.Tags())
+ require.EqualValues(t, []string{"qq", "vv"}, s.Tags())
require.EqualValues(t, 8, s.Rate())
}
@@ -151,12 +151,12 @@ func TestUser_PostSubjectCollectionPartialData(t *testing.T) {
htest.New(t, app).
Header(echo.HeaderAuthorization, "Bearer t").
BodyJSON(map[string]any{
- "tags": []string{"q", "vv"},
+ "tags": []string{"qq", "vv"},
}).
Post(fmt.Sprintf("/v0/users/-/collections/%d", sid)).
ExpectCode(http.StatusAccepted)
- require.EqualValues(t, []string{"q", "vv"}, s.Tags())
+ require.EqualValues(t, []string{"qq", "vv"}, s.Tags())
}
func TestUser_PostSubjectCollection_badID(t *testing.T) {
diff --git a/web/handler/user/user.go b/web/handler/user/user.go
index 59236045f..a85733224 100644
--- a/web/handler/user/user.go
+++ b/web/handler/user/user.go
@@ -19,6 +19,7 @@ import (
"github.com/bangumi/server/config"
"github.com/bangumi/server/ctrl"
+ "github.com/bangumi/server/internal/character"
"github.com/bangumi/server/internal/collections"
"github.com/bangumi/server/internal/episode"
"github.com/bangumi/server/internal/person"
@@ -27,18 +28,20 @@ import (
)
type User struct {
- ctrl ctrl.Ctrl
- episode episode.Repo
- person person.Service
- collect collections.Repo
- subject subject.CachedRepo
- log *zap.Logger
- user user.Repo
- cfg config.AppConfig
+ ctrl ctrl.Ctrl
+ episode episode.Repo
+ character character.Repo
+ person person.Repo
+ collect collections.Repo
+ subject subject.CachedRepo
+ log *zap.Logger
+ user user.Repo
+ cfg config.AppConfig
}
func New(
- p person.Service,
+ person person.Repo,
+ character character.Repo,
user user.Repo,
ctrl ctrl.Ctrl,
subject subject.Repo,
@@ -47,13 +50,14 @@ func New(
log *zap.Logger,
) (User, error) {
return User{
- ctrl: ctrl,
- episode: episode,
- collect: collect,
- subject: subject,
- user: user,
- person: p,
- log: log.Named("handler.User"),
- cfg: config.AppConfig{},
+ ctrl: ctrl,
+ episode: episode,
+ collect: collect,
+ subject: subject,
+ user: user,
+ person: person,
+ character: character,
+ log: log.Named("handler.User"),
+ cfg: config.AppConfig{},
}, nil
}
diff --git a/web/handler/user/user_test.go b/web/handler/user/user_test.go
index 31a8d8a7f..9201023cf 100644
--- a/web/handler/user/user_test.go
+++ b/web/handler/user/user_test.go
@@ -36,7 +36,7 @@ func TestUser_Get(t *testing.T) {
const uid model.UserID = 7
u := mocks.NewUserRepo(t)
- u.EXPECT().GetByID(mock.Anything, uid).Return(user.User{ID: uid}, nil)
+ u.EXPECT().GetFullUser(mock.Anything, uid).Return(user.FullUser{ID: uid}, nil)
a := mocks.NewAuthRepo(t)
a.EXPECT().GetByToken(mock.Anything, "token").Return(auth.UserInfo{ID: uid}, nil)
@@ -101,7 +101,6 @@ func TestUser_GetAvatar_302(t *testing.T) {
app := test.GetWebApp(t, test.Mock{UserRepo: m})
for _, imageType := range []string{"large", "medium", "small"} {
- imageType := imageType
t.Run(imageType, func(t *testing.T) {
t.Parallel()
diff --git a/web/json.go b/web/json.go
index 7f3262e5e..484f8a8ed 100644
--- a/web/json.go
+++ b/web/json.go
@@ -15,8 +15,8 @@
package web
import (
- "encoding/json"
-
+ "github.com/bytedance/sonic/decoder"
+ "github.com/bytedance/sonic/encoder"
"github.com/labstack/echo/v4"
)
@@ -26,7 +26,7 @@ type jsonSerializer struct {
}
func (j jsonSerializer) Serialize(c echo.Context, i any, indent string) error {
- enc := json.NewEncoder(c.Response())
+ enc := encoder.NewStreamEncoder(c.Response())
if indent != "" {
enc.SetIndent("", indent)
}
@@ -34,7 +34,7 @@ func (j jsonSerializer) Serialize(c echo.Context, i any, indent string) error {
}
func (j jsonSerializer) Deserialize(c echo.Context, i any) error {
- dec := json.NewDecoder(c.Request().Body)
+ dec := decoder.NewStreamDecoder(c.Request().Body)
dec.DisallowUnknownFields()
return dec.Decode(i)
}
diff --git a/web/new.go b/web/new.go
index c532821fb..97b902520 100644
--- a/web/new.go
+++ b/web/new.go
@@ -19,10 +19,12 @@ import (
"fmt"
"net"
"net/http"
+ "net/http/pprof"
"strconv"
"strings"
"time"
+ "github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -78,6 +80,13 @@ func New() *echo.Echo {
}
})
+ app.GET("/metrics", echo.WrapHandler(promhttp.Handler()))
+ app.GET("/debug/pprof/cmdline", echo.WrapHandler(http.HandlerFunc(pprof.Cmdline)))
+ app.GET("/debug/pprof/profile", echo.WrapHandler(http.HandlerFunc(pprof.Profile)))
+ app.GET("/debug/pprof/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol)))
+ app.GET("/debug/pprof/trace", echo.WrapHandler(http.HandlerFunc(pprof.Trace)))
+ app.GET("/debug/pprof/*", echo.WrapHandler(http.HandlerFunc(pprof.Index)))
+
if env.Development {
app.Use(genFakeRequestID)
}
@@ -91,14 +100,20 @@ func New() *echo.Echo {
app.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
+ ctx, cancel := context.WithTimeout(context.WithoutCancel(c.Request().Context()), time.Minute)
+ defer cancel()
+
reqID := c.Request().Header.Get(cf.HeaderRequestID)
- reqIP := c.RealIP()
- c.SetRequest(c.Request().
- WithContext(context.WithValue(context.Background(), logger.RequestKey, &logger.RequestTrace{
- IP: reqIP,
- ReqID: reqID,
- })))
+ if reqID == "" {
+ reqID = uuid.Must(uuid.NewV7()).String()
+ }
+
+ c.SetRequest(c.Request().WithContext(context.WithValue(ctx, logger.RequestKey, &logger.RequestTrace{
+ IP: c.RealIP(),
+ ReqID: reqID,
+ Path: c.Request().RequestURI,
+ })))
return next(c)
}
@@ -106,10 +121,6 @@ func New() *echo.Echo {
app.Use(recovery.New())
- app.GET("/metrics", echo.WrapHandler(promhttp.Handler()))
-
- addProfile(app)
-
app.GET("/openapi", func(c echo.Context) error {
return c.Redirect(http.StatusFound, "/openapi/")
})
diff --git a/web/req/auth_test.go b/web/req/auth_test.go
index 2cb9fedda..d519d114f 100644
--- a/web/req/auth_test.go
+++ b/web/req/auth_test.go
@@ -32,7 +32,6 @@ func TestLoginPass(t *testing.T) {
}
validate := validator.New()
for i, login := range testCase {
- login := login
t.Run(fmt.Sprintf("success %d", i), func(t *testing.T) {
t.Parallel()
require.NoError(t, validate.Struct(login))
@@ -50,7 +49,6 @@ func TestLoginErr(t *testing.T) {
}
validate := validator.New()
for i, login := range testCase {
- login := login
t.Run(fmt.Sprintf("fail %d", i), func(t *testing.T) {
t.Parallel()
require.Error(t, validate.Struct(login))
diff --git a/web/req/collection.go b/web/req/collection.go
index 4387fd3d6..f0e5d6aac 100644
--- a/web/req/collection.go
+++ b/web/req/collection.go
@@ -15,10 +15,12 @@
package req
import (
+ "fmt"
"strings"
"unicode/utf8"
"github.com/samber/lo"
+ "golang.org/x/text/unicode/norm"
"github.com/bangumi/server/internal/collections/domain/collection"
"github.com/bangumi/server/internal/pkg/dam"
@@ -47,18 +49,33 @@ func (v *SubjectEpisodeCollectionPatch) Validate() error {
}
}
- if len(v.Tags) > 0 {
- if !lo.EveryBy(v.Tags, dam.AllPrintableChar) {
- return res.BadRequest("invisible character are included in tags")
+ if v.Tags != nil {
+ if len(v.Tags) > 10 {
+ return res.BadRequest("最多允许 10 个标签")
+ }
+
+ v.Tags = lo.Map(v.Tags, func(item string, index int) string {
+ return norm.NFKC.String(item)
+ })
+ }
+
+ for _, tag := range v.Tags {
+ if utf8.RuneCountInString(tag) < 2 {
+ return res.BadRequest("tag 最短为两个字")
+ }
+
+ if !dam.ValidateTag(tag) {
+ return res.BadRequest(fmt.Sprintf("invalid tag: %q", tag))
}
}
if v.Comment.Set {
+ v.Comment.Value = norm.NFKC.String(v.Comment.Value)
+ v.Comment.Value = strings.TrimSpace(v.Comment.Value)
if !dam.AllPrintableChar(v.Comment.Value) {
return res.BadRequest("invisible character are included in comment")
}
- v.Comment.Value = strings.TrimSpace(v.Comment.Value)
if utf8.RuneCountInString(v.Comment.Value) > 380 {
return res.BadRequest("comment too long, only allow less equal than 380 characters")
}
diff --git a/web/req/page.go b/web/req/page.go
index c0e1de94b..18e719ac1 100644
--- a/web/req/page.go
+++ b/web/req/page.go
@@ -35,6 +35,42 @@ func (q PageQuery) Check(count int64) error {
return nil
}
+// GetPageQuerySoftLimit apply soft limit on query without error.
+func GetPageQuerySoftLimit(c echo.Context, defaultLimit int, maxLimit int) (PageQuery, error) {
+ q := PageQuery{Limit: defaultLimit}
+ var err error
+
+ raw := c.QueryParam("limit")
+ if raw != "" {
+ q.Limit, err = strconv.Atoi(raw)
+ if err != nil {
+ return q, res.BadRequest("can't parse query args limit as int: " + strconv.Quote(raw))
+ }
+
+ if q.Limit <= 0 {
+ return q, res.BadRequest("limit should be greater than zero")
+ }
+
+ if q.Limit > maxLimit {
+ q.Limit = maxLimit
+ }
+ }
+
+ raw = c.QueryParam("offset")
+ if raw != "" {
+ q.Offset, err = strconv.Atoi(raw)
+ if err != nil {
+ return q, res.BadRequest("can't parse query args offset as int: " + strconv.Quote(raw))
+ }
+
+ if q.Offset < 0 {
+ return q, res.BadRequest("offset should be greater than or equal to 0")
+ }
+ }
+
+ return q, nil
+}
+
func GetPageQuery(c echo.Context, defaultLimit int, maxLimit int) (PageQuery, error) {
q := PageQuery{Limit: defaultLimit}
var err error
diff --git a/web/req/query_parse.go b/web/req/query_parse.go
index e9a71c456..63a0fa085 100644
--- a/web/req/query_parse.go
+++ b/web/req/query_parse.go
@@ -22,6 +22,7 @@ import (
"github.com/bangumi/server/internal/model"
"github.com/bangumi/server/internal/pkg/gstr"
"github.com/bangumi/server/internal/pm"
+ "github.com/bangumi/server/pkg/vars"
"github.com/bangumi/server/web/res"
)
@@ -47,6 +48,24 @@ func ParseSubjectType(s string) (model.SubjectType, error) {
return 0, res.BadRequest(strconv.Quote(s) + " is not a valid subject type")
}
+func ParseSubjectCategory(stype model.SubjectType, s string) (uint16, error) {
+ if s == "" {
+ return 0, res.BadRequest("subject category is empty")
+ }
+ platforms, ok := vars.PlatformMap[stype]
+ if !ok {
+ return 0, res.BadRequest("bad subject type: " + strconv.Quote(s))
+ }
+ v, err := gstr.ParseUint16(s)
+ if err != nil {
+ return 0, res.BadRequest("bad subject category: " + strconv.Quote(s))
+ }
+ if _, ok := platforms[v]; !ok {
+ return 0, res.BadRequest("bad subject category: " + strconv.Quote(s))
+ }
+ return v, nil
+}
+
func ParseID(s string) (model.CharacterID, error) {
if s == "" {
return 0, errMissingID
diff --git a/web/res/character.go b/web/res/character.go
index 703b55d37..4acdd7e04 100644
--- a/web/res/character.go
+++ b/web/res/character.go
@@ -14,7 +14,12 @@
package res
-import "github.com/bangumi/server/internal/model"
+import (
+ "github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/pkg/compat"
+ "github.com/bangumi/server/internal/pkg/null"
+ "github.com/bangumi/server/pkg/wiki"
+)
type CharacterV0 struct {
BirthMon *uint8 `json:"birth_mon"`
@@ -25,7 +30,7 @@ type CharacterV0 struct {
Images PersonImages `json:"images"`
Summary string `json:"summary"`
Name string `json:"name"`
- Infobox v0wiki `json:"infobox"`
+ Infobox V0wiki `json:"infobox"`
Stat Stat `json:"stat"`
ID model.CharacterID `json:"id"`
Redirect model.CharacterID `json:"-"`
@@ -52,3 +57,28 @@ func CharacterStaffString(i uint8) string {
return ""
}
+
+func ConvertModelCharacter(s model.Character) CharacterV0 {
+ img := PersonImage(s.Image)
+
+ return CharacterV0{
+ ID: s.ID,
+ Type: s.Type,
+ Name: s.Name,
+ NSFW: s.NSFW,
+ Images: img,
+ Summary: s.Summary,
+ Infobox: compat.V0Wiki(wiki.ParseOmitError(s.Infobox).NonZero()),
+ Gender: null.NilString(GenderMap[s.FieldGender]),
+ BloodType: null.NilUint8(s.FieldBloodType),
+ BirthYear: null.NilUint16(s.FieldBirthYear),
+ BirthMon: null.NilUint8(s.FieldBirthMon),
+ BirthDay: null.NilUint8(s.FieldBirthDay),
+ Stat: Stat{
+ Comments: s.CommentCount,
+ Collects: s.CollectCount,
+ },
+ Redirect: s.Redirect,
+ Locked: s.Locked,
+ }
+}
diff --git a/web/res/collection.go b/web/res/collection.go
index 7cb639ec9..84534d55b 100644
--- a/web/res/collection.go
+++ b/web/res/collection.go
@@ -51,3 +51,33 @@ func ConvertModelSubjectCollection(c collection.UserSubjectCollection, subject S
Subject: subject,
}
}
+
+type PersonCollection struct {
+ ID uint32 `json:"id"`
+ Type uint8 `json:"type"`
+ Name string `json:"name"`
+ Images PersonImages `json:"images"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+func ConvertModelPersonCollection(c collection.UserPersonCollection, person model.Person) PersonCollection {
+ img := PersonImage(person.Image)
+ return PersonCollection{
+ ID: person.ID,
+ Type: person.Type,
+ Name: person.Name,
+ Images: img,
+ CreatedAt: c.CreatedAt,
+ }
+}
+
+func ConvertModelCharacterCollection(c collection.UserPersonCollection, character model.Character) PersonCollection {
+ img := PersonImage(character.Image)
+ return PersonCollection{
+ ID: character.ID,
+ Type: character.Type,
+ Name: character.Name,
+ Images: img,
+ CreatedAt: c.CreatedAt,
+ }
+}
diff --git a/web/res/error.go b/web/res/error.go
index 1b34a81a4..94c1e5260 100644
--- a/web/res/error.go
+++ b/web/res/error.go
@@ -22,6 +22,7 @@ import (
"github.com/labstack/echo/v4"
+ "github.com/bangumi/server/web/req/cf"
"github.com/bangumi/server/web/util"
)
@@ -31,6 +32,7 @@ var ErrNotFound = NewError(http.StatusNotFound, "resource can't be found in the
type Error struct {
Title string `json:"title"`
Details any `json:"details,omitempty"`
+ RequestID string `json:"request_id,omitempty"`
Description string `json:"description"`
}
@@ -77,6 +79,7 @@ func InternalError(c echo.Context, err error, message string) error {
return c.JSON(http.StatusInternalServerError, Error{
Title: "Internal Server Error",
Description: message,
+ RequestID: c.Request().Header.Get(cf.HeaderRequestID),
Details: util.DetailWithErr(c, err),
})
}
diff --git a/web/res/image.go b/web/res/image.go
index 3b5fc5c0d..7f870bec2 100644
--- a/web/res/image.go
+++ b/web/res/image.go
@@ -93,7 +93,7 @@ func PersonImage(s string) PersonImages {
Large: "https://lain.bgm.tv/pic/crt/l/" + s,
Small: "https://lain.bgm.tv/r/100/pic/crt/l/" + s,
- Grid: "https://lain.bgm.tv/r/200/pic/crt/l/" + s,
+ Grid: "https://lain.bgm.tv/pic/crt/g/" + s,
Medium: "https://lain.bgm.tv/r/400/pic/crt/l/" + s,
}
}
diff --git a/web/res/person.go b/web/res/person.go
index f08c8f101..8c5410e31 100644
--- a/web/res/person.go
+++ b/web/res/person.go
@@ -34,7 +34,7 @@ type PersonV0 struct {
Summary string `json:"summary"`
Name string `json:"name"`
Img string `json:"img"`
- Infobox v0wiki `json:"infobox"`
+ Infobox V0wiki `json:"infobox"`
Career []string `json:"career"`
Stat Stat `json:"stat"`
Redirect model.PersonID `json:"-"`
diff --git a/web/res/subject.go b/web/res/subject.go
index b9a26aae5..c7bf3ab01 100644
--- a/web/res/subject.go
+++ b/web/res/subject.go
@@ -18,40 +18,50 @@ import (
"time"
"github.com/samber/lo"
+ "go.uber.org/zap"
"github.com/bangumi/server/internal/model"
+ "github.com/bangumi/server/internal/pkg/compat"
"github.com/bangumi/server/internal/pkg/generic/slice"
"github.com/bangumi/server/internal/pkg/gstr"
+ "github.com/bangumi/server/internal/pkg/logger"
+ "github.com/bangumi/server/internal/pkg/null"
+ "github.com/bangumi/server/internal/tag"
+ "github.com/bangumi/server/pkg/vars"
+ "github.com/bangumi/server/pkg/wiki"
)
const defaultShortSummaryLength = 120
-type v0wiki = []any
+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 {
Date *string `json:"date"`
Platform *string `json:"platform"`
- Image SubjectImages `json:"images"`
+ Images SubjectImages `json:"images"`
Summary string `json:"summary"`
Name string `json:"name"`
NameCN string `json:"name_cn"`
Tags []SubjectTag `json:"tags"`
- Infobox v0wiki `json:"infobox"`
+ Infobox V0wiki `json:"infobox"`
Rating Rating `json:"rating"`
TotalEpisodes int64 `json:"total_episodes" doc:"episodes count in database"`
Collection SubjectCollectionStat `json:"collection"`
ID model.SubjectID `json:"id"`
Eps uint32 `json:"eps"`
+ MetaTags []string `json:"meta_tags"`
Volumes uint32 `json:"volumes"`
- Redirect model.SubjectID `json:"-"`
+ Series bool `json:"series"`
Locked bool `json:"locked"`
NSFW bool `json:"nsfw"`
TypeID model.SubjectType `json:"type"`
+ Redirect model.SubjectID `json:"-"`
}
type SlimSubjectV0 struct {
@@ -98,6 +108,75 @@ func ToSlimSubjectV0(s model.Subject) SlimSubjectV0 {
}
}
+func PlatformString(s model.Subject) *string {
+ platform, ok := vars.PlatformMap[s.TypeID][s.PlatformID]
+ if !ok && s.TypeID != 0 {
+ logger.Warn("unknown platform",
+ zap.Uint32("subject", s.ID),
+ zap.Uint8("type", s.TypeID),
+ zap.Uint16("platform", s.PlatformID),
+ )
+
+ return nil
+ }
+ v := platform.String()
+ return &v
+}
+
+func ToSubjectV0(s model.Subject, totalEpisode int64, metaTags []tag.Tag) SubjectV0 {
+ return SubjectV0{
+ TotalEpisodes: totalEpisode,
+ ID: s.ID,
+ Images: SubjectImage(s.Image),
+ Summary: s.Summary,
+ Name: s.Name,
+ Platform: PlatformString(s),
+ NameCN: s.NameCN,
+ Date: null.NilString(s.Date),
+ Infobox: compat.V0Wiki(wiki.ParseOmitError(s.Infobox).NonZero()),
+ Volumes: s.Volumes,
+ Redirect: s.Redirect,
+ Eps: s.Eps,
+ MetaTags: lo.Map(metaTags, func(item tag.Tag, index int) string {
+ return item.Name
+ }),
+ Tags: slice.Map(s.Tags, func(tag model.Tag) SubjectTag {
+ return SubjectTag{
+ Name: tag.Name,
+ Count: tag.Count,
+ }
+ }),
+ Collection: SubjectCollectionStat{
+ OnHold: s.OnHold,
+ Wish: s.Wish,
+ Dropped: s.Dropped,
+ Collect: s.Collect,
+ Doing: s.Doing,
+ },
+ TypeID: s.TypeID,
+ Series: s.Series,
+ Locked: s.Locked(),
+ NSFW: s.NSFW,
+ Rating: Rating{
+ Rank: s.Rating.Rank,
+ Total: s.Rating.Total,
+ Count: Count{
+ Field1: s.Rating.Count.Field1,
+ Field2: s.Rating.Count.Field2,
+ Field3: s.Rating.Count.Field3,
+ Field4: s.Rating.Count.Field4,
+ Field5: s.Rating.Count.Field5,
+ Field6: s.Rating.Count.Field6,
+ Field7: s.Rating.Count.Field7,
+ Field8: s.Rating.Count.Field8,
+ Field9: s.Rating.Count.Field9,
+ Field10: s.Rating.Count.Field10,
+ },
+ Score: s.Rating.Score,
+ },
+ }
+}
+
type SubjectCollectionStat struct {
OnHold uint32 `json:"on_hold"`
Dropped uint32 `json:"dropped"`
@@ -131,11 +210,12 @@ type Rating struct {
}
type PersonRelatedSubject struct {
- Staff string `json:"staff"`
- Name string `json:"name"`
- NameCn string `json:"name_cn"`
- Image string `json:"image"`
- SubjectID model.SubjectID `json:"id"`
+ Staff string `json:"staff"`
+ Name string `json:"name"`
+ NameCn string `json:"name_cn"`
+ Image string `json:"image"`
+ Type model.SubjectType `json:"type"`
+ SubjectID model.SubjectID `json:"id"`
}
type PersonRelatedCharacter struct {
@@ -143,6 +223,7 @@ type PersonRelatedCharacter struct {
Name string `json:"name"`
SubjectName string `json:"subject_name"`
SubjectNameCn string `json:"subject_name_cn"`
+ SubjectType model.SubjectType `json:"subject_type"`
SubjectID model.SubjectID `json:"subject_id"`
Staff string `json:"staff"`
ID model.CharacterID `json:"id"`
@@ -150,22 +231,24 @@ type PersonRelatedCharacter struct {
}
type CharacterRelatedPerson struct {
- Images PersonImages `json:"images"`
- Name string `json:"name"`
- SubjectName string `json:"subject_name"`
- SubjectNameCn string `json:"subject_name_cn"`
- SubjectID model.SubjectID `json:"subject_id"`
- Staff string `json:"staff"`
- ID model.PersonID `json:"id"`
- Type uint8 `json:"type" doc:"person type"`
+ Images PersonImages `json:"images"`
+ Name string `json:"name"`
+ SubjectName string `json:"subject_name"`
+ SubjectNameCn string `json:"subject_name_cn"`
+ SubjectType model.SubjectType `json:"subject_type"`
+ SubjectID model.SubjectID `json:"subject_id"`
+ Staff string `json:"staff"`
+ ID model.PersonID `json:"id"`
+ Type uint8 `json:"type" doc:"person type"`
}
type CharacterRelatedSubject struct {
- Staff string `json:"staff"`
- Name string `json:"name"`
- NameCn string `json:"name_cn"`
- Image string `json:"image"`
- ID model.SubjectID `json:"id"`
+ Staff string `json:"staff"`
+ Name string `json:"name"`
+ NameCn string `json:"name_cn"`
+ Image string `json:"image"`
+ Type model.SubjectType `json:"type"`
+ ID model.SubjectID `json:"id"`
}
type SubjectRelatedSubject struct {
@@ -193,6 +276,7 @@ type SubjectRelatedPerson struct {
Career []string `json:"career"`
Type uint8 `json:"type"`
ID model.PersonID `json:"id" doc:"person ID"`
+ Eps string `json:"eps" doc:"episodes participated"`
}
type Actor struct {
@@ -212,7 +296,7 @@ type IndexSubjectV0 struct {
Name string `json:"name"`
NameCN string `json:"name_cn"`
Comment string `json:"comment"`
- Infobox v0wiki `json:"infobox"`
+ Infobox V0wiki `json:"infobox"`
ID model.SubjectID `json:"id"`
TypeID model.SubjectType `json:"type"`
}
diff --git a/web/routes.go b/web/routes.go
index d68263fee..f908c44a2 100644
--- a/web/routes.go
+++ b/web/routes.go
@@ -58,7 +58,9 @@ func AddRouters(
v0 := app.Group("/v0", common.MiddlewareAccessTokenAuth)
- v0.POST("/search/subjects", h.Search)
+ v0.POST("/search/subjects", h.SearchSubjects)
+ v0.POST("/search/characters", h.SearchCharacters)
+ v0.POST("/search/persons", h.SearchPersons)
subjectHandler.Routes(v0)
@@ -66,18 +68,28 @@ func AddRouters(
v0.GET("/persons/:id/image", personHandler.GetImage)
v0.GET("/persons/:id/subjects", personHandler.GetRelatedSubjects)
v0.GET("/persons/:id/characters", personHandler.GetRelatedCharacters)
+ v0.POST("/persons/:id/collect", personHandler.CollectPerson, mw.NeedLogin)
+ // TODO: wait for soft delete
+ // v0.DELETE("/persons/:id/collect", personHandler.UncollectPerson, mw.NeedLogin)
+
v0.GET("/characters/:id", characterHandler.Get)
v0.GET("/characters/:id/image", characterHandler.GetImage)
v0.GET("/characters/:id/subjects", characterHandler.GetRelatedSubjects)
v0.GET("/characters/:id/persons", characterHandler.GetRelatedPersons)
+ v0.POST("/characters/:id/collect", characterHandler.CollectCharacter, mw.NeedLogin)
+ // TODO: wait for soft delete
+ // v0.DELETE("/characters/:id/collect", characterHandler.UncollectCharacter, mw.NeedLogin)
+
v0.GET("/episodes/:id", h.GetEpisode)
v0.GET("/episodes", h.ListEpisode)
// echo 中间件从前往后运行按顺序
v0.GET("/me", userHandler.GetCurrent)
v0.GET("/users/:username", userHandler.Get)
+ v0.GET("/users/:username/avatar", userHandler.GetAvatar)
v0.GET("/users/:username/collections", userHandler.ListSubjectCollection)
v0.GET("/users/:username/collections/:subject_id", userHandler.GetSubjectCollection)
+
v0.GET("/users/-/collections/-/episodes/:episode_id", userHandler.GetEpisodeCollection, mw.NeedLogin)
v0.PUT("/users/-/collections/-/episodes/:episode_id", userHandler.PutEpisodeCollection, req.JSON, mw.NeedLogin)
v0.GET("/users/-/collections/:subject_id/episodes", userHandler.GetSubjectEpisodeCollection, mw.NeedLogin)
@@ -87,7 +99,10 @@ func AddRouters(
v0.PATCH("/users/-/collections/:subject_id/episodes",
userHandler.PatchEpisodeCollectionBatch, req.JSON, mw.NeedLogin)
- v0.GET("/users/:username/avatar", userHandler.GetAvatar)
+ v0.GET("/users/:username/collections/-/characters", userHandler.ListCharacterCollection)
+ v0.GET("/users/:username/collections/-/characters/:character_id", userHandler.GetCharacterCollection)
+ v0.GET("/users/:username/collections/-/persons", userHandler.ListPersonCollection)
+ v0.GET("/users/:username/collections/-/persons/:person_id", userHandler.GetPersonCollection)
{
i := indexHandler