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