thread-master/apps/backend/internal/module/scout/repository/mongo.go

370 lines
11 KiB
Go

package repository
import (
"context"
libmongo "apps/backend/internal/lib/mongo"
"apps/backend/internal/module/scout/domain"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
type MonStore struct {
brands *mon.Model
products *mon.Model
active *mon.Model
posts *mon.Model
runs *mon.Model
seen *mon.Model
hw *mon.Model
crawler *mon.Model
}
func NewMonStore(uri, database string) *MonStore {
uri = libmongo.MustMongoURI(uri)
return &MonStore{
brands: mon.MustNewModel(uri, database, "scout_brands"),
products: mon.MustNewModel(uri, database, "scout_products"),
active: mon.MustNewModel(uri, database, "scout_active_brand"),
posts: mon.MustNewModel(uri, database, "scout_posts"),
runs: mon.MustNewModel(uri, database, "scout_runs"),
seen: mon.MustNewModel(uri, database, "scout_seen_identities"),
hw: mon.MustNewModel(uri, database, "scout_homework"),
crawler: mon.MustNewModel(uri, database, "scout_crawler_session"),
}
}
func (s *MonStore) SaveBrand(ctx context.Context, b *domain.Brand) error {
_, err := s.brands.ReplaceOne(ctx, bson.M{"_id": b.ID}, b, options.Replace().SetUpsert(true))
return err
}
func (s *MonStore) GetBrand(ctx context.Context, id string) (*domain.Brand, error) {
var b domain.Brand
err := s.brands.FindOne(ctx, &b, bson.M{"_id": id})
if err != nil {
if err == mon.ErrNotFound {
return nil, domain.ErrNotFound
}
return nil, err
}
return &b, nil
}
func (s *MonStore) ListBrands(ctx context.Context, ownerUID int64) ([]*domain.Brand, error) {
var list []*domain.Brand
err := s.brands.Find(ctx, &list, bson.M{"owner_uid": ownerUID})
return list, err
}
func (s *MonStore) DeleteBrand(ctx context.Context, id string) error {
res, err := s.brands.DeleteOne(ctx, bson.M{"_id": id})
if err != nil {
return err
}
if res == 0 {
return domain.ErrNotFound
}
return nil
}
func (s *MonStore) SaveProduct(ctx context.Context, p *domain.Product) error {
_, err := s.products.ReplaceOne(ctx, bson.M{"_id": p.ID}, p, options.Replace().SetUpsert(true))
return err
}
func (s *MonStore) GetProduct(ctx context.Context, id string) (*domain.Product, error) {
var p domain.Product
err := s.products.FindOne(ctx, &p, bson.M{"_id": id})
if err != nil {
if err == mon.ErrNotFound {
return nil, domain.ErrNotFound
}
return nil, err
}
return &p, nil
}
func (s *MonStore) ListProducts(ctx context.Context, ownerUID int64, brandID string) ([]*domain.Product, error) {
filter := bson.M{"owner_uid": ownerUID}
if brandID != "" {
filter["brand_id"] = brandID
}
var list []*domain.Product
err := s.products.Find(ctx, &list, filter)
return list, err
}
func (s *MonStore) DeleteProduct(ctx context.Context, id string) error {
res, err := s.products.DeleteOne(ctx, bson.M{"_id": id})
if err != nil {
return err
}
if res == 0 {
return domain.ErrNotFound
}
return nil
}
func (s *MonStore) GetActiveBrandID(ctx context.Context, ownerUID int64) (string, error) {
var a domain.ActiveBrand
err := s.active.FindOne(ctx, &a, bson.M{"_id": ownerUID})
if err != nil {
if err == mon.ErrNotFound {
return "", nil
}
return "", err
}
return a.BrandID, nil
}
func (s *MonStore) SetActiveBrandID(ctx context.Context, ownerUID int64, brandID string) error {
doc := &domain.ActiveBrand{OwnerUID: ownerUID, BrandID: brandID, UpdatedAt: domain.NowNano()}
_, err := s.active.ReplaceOne(ctx, bson.M{"_id": ownerUID}, doc, options.Replace().SetUpsert(true))
return err
}
func (s *MonStore) SavePost(ctx context.Context, p *domain.Post) error {
_, err := s.posts.ReplaceOne(ctx, bson.M{"_id": p.ID}, p, options.Replace().SetUpsert(true))
return err
}
func (s *MonStore) GetPost(ctx context.Context, id string) (*domain.Post, error) {
var p domain.Post
err := s.posts.FindOne(ctx, &p, bson.M{"_id": id})
if err != nil {
if err == mon.ErrNotFound {
return nil, domain.ErrNotFound
}
return nil, err
}
return &p, nil
}
func (s *MonStore) ListPosts(ctx context.Context, ownerUID int64, brandID string) ([]*domain.Post, error) {
filter := bson.M{"owner_uid": ownerUID}
if brandID != "" {
filter["brand_id"] = brandID
}
var list []*domain.Post
// score 是品質主排序;時間只作相同分數的 deterministic tie-breaker。
err := s.posts.Find(ctx, &list, filter, options.Find().SetSort(bson.D{
{Key: "score", Value: -1},
{Key: "posted_at", Value: -1},
{Key: "created_at", Value: -1},
{Key: "_id", Value: -1},
}))
return list, err
}
func (s *MonStore) DeletePost(ctx context.Context, id string) error {
res, err := s.posts.DeleteOne(ctx, bson.M{"_id": id})
if err != nil {
return err
}
if res == 0 {
return domain.ErrNotFound
}
return nil
}
func (s *MonStore) DeletePostsByTheme(ctx context.Context, ownerUID int64, themeKey string) error {
_, err := s.posts.DeleteMany(ctx, bson.M{"owner_uid": ownerUID, "theme_key": themeKey})
return err
}
func (s *MonStore) SaveHomework(ctx context.Context, h *domain.Homework) error {
// composite: store theme as id, filter by owner
docID := formatKey(h.OwnerUID, h.ThemeKey)
type hwDoc struct {
ID string `bson:"_id"`
*domain.Homework
}
_, err := s.hw.ReplaceOne(ctx, bson.M{"_id": docID}, &hwDoc{ID: docID, Homework: h}, options.Replace().SetUpsert(true))
return err
}
func (s *MonStore) GetHomework(ctx context.Context, ownerUID int64, themeKey string) (*domain.Homework, error) {
type hwDoc struct {
ID string `bson:"_id"`
domain.Homework `bson:",inline"`
}
var doc hwDoc
err := s.hw.FindOne(ctx, &doc, bson.M{"_id": formatKey(ownerUID, themeKey)})
if err != nil {
if err == mon.ErrNotFound {
return nil, domain.ErrNotFound
}
return nil, err
}
h := doc.Homework
return &h, nil
}
func (s *MonStore) ListHomework(ctx context.Context, ownerUID int64) ([]*domain.Homework, error) {
var list []*domain.Homework
err := s.hw.Find(ctx, &list, bson.M{"owner_uid": ownerUID})
return list, err
}
func (s *MonStore) DeleteHomework(ctx context.Context, ownerUID int64, themeKey string) error {
res, err := s.hw.DeleteOne(ctx, bson.M{"_id": formatKey(ownerUID, themeKey)})
if err != nil {
return err
}
if res == 0 {
return domain.ErrNotFound
}
return nil
}
func (s *MonStore) GetCrawlerSession(ctx context.Context, ownerUID int64) (*domain.CrawlerSession, error) {
var c domain.CrawlerSession
err := s.crawler.FindOne(ctx, &c, bson.M{"_id": ownerUID})
if err != nil {
if err == mon.ErrNotFound {
return nil, domain.ErrNotFound
}
return nil, err
}
if c.StorageStateEnc == "" {
return nil, domain.ErrNotFound
}
return &c, nil
}
func (s *MonStore) SetCrawlerSession(ctx context.Context, sess *domain.CrawlerSession) error {
_, err := s.crawler.ReplaceOne(ctx, bson.M{"_id": sess.OwnerUID}, sess, options.Replace().SetUpsert(true))
return err
}
func (s *MonStore) ClearCrawlerSession(ctx context.Context, ownerUID int64) error {
_, err := s.crawler.DeleteOne(ctx, bson.M{"_id": ownerUID})
return err
}
func (s *MonStore) CreateRun(ctx context.Context, r *domain.Run) error {
if r == nil || r.ID == "" || r.OwnerUID == 0 {
return domain.ErrValidation
}
_, err := s.runs.InsertOne(ctx, r)
return err
}
func (s *MonStore) GetRun(ctx context.Context, ownerUID int64, id string) (*domain.Run, error) {
var r domain.Run
err := s.runs.FindOne(ctx, &r, bson.M{"_id": id, "owner_uid": ownerUID})
if err != nil {
if err == mon.ErrNotFound {
return nil, domain.ErrNotFound
}
return nil, err
}
return &r, nil
}
func (s *MonStore) ListRuns(ctx context.Context, filter domain.RunFilter) (domain.RunPage, error) {
page := domain.NormalizePage(filter.Page, filter.PageSize)
q := bson.M{"owner_uid": filter.OwnerUID}
if filter.BrandID != "" {
q["brand_id"] = filter.BrandID
}
if filter.Mode != "" {
q["mode"] = filter.Mode
}
total, err := s.runs.CountDocuments(ctx, q)
if err != nil {
return domain.RunPage{}, err
}
page = page.WithTotal(total)
var list []*domain.Run
err = s.runs.Find(ctx, &list, q, options.Find().SetSort(bson.D{
{Key: "created_at", Value: -1}, {Key: "_id", Value: -1},
}).SetSkip(int64((page.Page-1)*page.PageSize)).SetLimit(int64(page.PageSize)))
return domain.RunPage{Items: list, Pagination: page}, err
}
func (s *MonStore) ListRunPosts(ctx context.Context, ownerUID int64, runID string, requestedPage, requestedSize int) (domain.RunPostPage, error) {
r, err := s.GetRun(ctx, ownerUID, runID)
if err != nil {
return domain.RunPostPage{}, err
}
page := domain.NormalizePage(requestedPage, requestedSize)
q := bson.M{"owner_uid": ownerUID, "run_id": runID}
total, err := s.posts.CountDocuments(ctx, q)
if err != nil {
return domain.RunPostPage{}, err
}
page = page.WithTotal(total)
var list []*domain.Post
err = s.posts.Find(ctx, &list, q, options.Find().SetSort(bson.D{
{Key: "score", Value: -1}, {Key: "posted_at", Value: -1}, {Key: "created_at", Value: -1}, {Key: "_id", Value: -1},
}).SetSkip(int64((page.Page-1)*page.PageSize)).SetLimit(int64(page.PageSize)))
return domain.RunPostPage{Run: r, Items: list, Pagination: page}, err
}
func (s *MonStore) ReplaceRunGuarded(ctx context.Context, ownerUID int64, id string, expected []string, replacement *domain.Run) error {
if replacement == nil || replacement.ID != id {
return domain.ErrValidation
}
filter := bson.M{"_id": id, "owner_uid": ownerUID}
if len(expected) > 0 {
filter["status"] = bson.M{"$in": expected}
}
res, err := s.runs.ReplaceOne(ctx, filter, replacement)
if err != nil {
return err
}
if res.MatchedCount == 0 {
if _, getErr := s.GetRun(ctx, ownerUID, id); getErr != nil {
return getErr
}
return domain.ErrIllegalRunStatus
}
return nil
}
func (s *MonStore) DeleteRun(ctx context.Context, ownerUID int64, id string) error {
res, err := s.runs.DeleteOne(ctx, bson.M{"_id": id, "owner_uid": ownerUID})
if err != nil {
return err
}
if res == 0 {
return domain.ErrNotFound
}
_, err = s.posts.DeleteMany(ctx, bson.M{"owner_uid": ownerUID, "run_id": id})
return err
}
func (s *MonStore) PublishRunPosts(ctx context.Context, ownerUID int64, runID string, posts []*domain.Post) error {
if _, err := s.GetRun(ctx, ownerUID, runID); err != nil {
return err
}
for _, p := range posts {
if p == nil || p.ID == "" || p.OwnerUID != ownerUID || p.RunID != runID {
return domain.ErrValidation
}
}
for _, p := range posts {
if _, err := s.posts.ReplaceOne(ctx, bson.M{"_id": p.ID, "owner_uid": ownerUID}, p, options.Replace().SetUpsert(true)); err != nil {
return err
}
}
return nil
}
func (s *MonStore) HasSeenIdentity(ctx context.Context, ownerUID int64, identity string) (bool, error) {
if identity == "" {
return false, domain.ErrValidation
}
var doc struct {
ID string `bson:"_id"`
}
err := s.seen.FindOne(ctx, &doc, bson.M{"_id": identityKey(ownerUID, identity)})
if err == mon.ErrNotFound {
return false, nil
}
return err == nil, err
}
func (s *MonStore) MarkSeenIdentity(ctx context.Context, ownerUID int64, identity, postID string, seenAt int64) error {
if identity == "" {
return domain.ErrValidation
}
doc := bson.M{"_id": identityKey(ownerUID, identity), "owner_uid": ownerUID, "identity": identity, "post_id": postID, "seen_at": seenAt}
_, err := s.seen.ReplaceOne(ctx, bson.M{"_id": identityKey(ownerUID, identity)}, doc, options.Replace().SetUpsert(true))
return err
}
func formatKey(uid int64, theme string) string {
return formatUID(uid) + "|" + theme
}
var _ domain.Repository = (*MonStore)(nil)