2026-07-13 01:15:30 +00:00
|
|
|
|
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
|
2026-08-13 02:22:24 +00:00
|
|
|
|
runs *mon.Model
|
|
|
|
|
|
seen *mon.Model
|
2026-07-13 01:15:30 +00:00
|
|
|
|
hw *mon.Model
|
|
|
|
|
|
crawler *mon.Model
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewMonStore(uri, database string) *MonStore {
|
|
|
|
|
|
uri = libmongo.MustMongoURI(uri)
|
|
|
|
|
|
return &MonStore{
|
2026-07-13 08:59:13 +00:00
|
|
|
|
brands: mon.MustNewModel(uri, database, "scout_brands"),
|
2026-07-13 01:15:30 +00:00
|
|
|
|
products: mon.MustNewModel(uri, database, "scout_products"),
|
2026-07-13 08:59:13 +00:00
|
|
|
|
active: mon.MustNewModel(uri, database, "scout_active_brand"),
|
|
|
|
|
|
posts: mon.MustNewModel(uri, database, "scout_posts"),
|
2026-08-13 02:22:24 +00:00
|
|
|
|
runs: mon.MustNewModel(uri, database, "scout_runs"),
|
|
|
|
|
|
seen: mon.MustNewModel(uri, database, "scout_seen_identities"),
|
2026-07-13 08:59:13 +00:00
|
|
|
|
hw: mon.MustNewModel(uri, database, "scout_homework"),
|
|
|
|
|
|
crawler: mon.MustNewModel(uri, database, "scout_crawler_session"),
|
2026-07-13 01:15:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-08-13 02:22:24 +00:00
|
|
|
|
// score 是品質主排序;時間只作相同分數的 deterministic tie-breaker。
|
2026-08-03 07:58:07 +00:00
|
|
|
|
err := s.posts.Find(ctx, &list, filter, options.Find().SetSort(bson.D{
|
2026-08-13 02:22:24 +00:00
|
|
|
|
{Key: "score", Value: -1},
|
2026-08-03 07:58:07 +00:00
|
|
|
|
{Key: "posted_at", Value: -1},
|
|
|
|
|
|
{Key: "created_at", Value: -1},
|
2026-08-13 02:22:24 +00:00
|
|
|
|
{Key: "_id", Value: -1},
|
2026-08-03 07:58:07 +00:00
|
|
|
|
}))
|
2026-07-13 01:15:30 +00:00
|
|
|
|
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 {
|
2026-07-13 08:59:13 +00:00
|
|
|
|
ID string `bson:"_id"`
|
2026-07-13 01:15:30 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-07-13 08:59:13 +00:00
|
|
|
|
if c.StorageStateEnc == "" {
|
2026-07-13 01:15:30 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 02:22:24 +00:00
|
|
|
|
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)
|
2026-08-19 02:01:15 +00:00
|
|
|
|
// 與 memory store 一致的可見性屏障:run 成功前的候選是暫存資料,不得外流。
|
|
|
|
|
|
if r.Status != domain.RunSucceeded {
|
|
|
|
|
|
return domain.RunPostPage{Run: r, Items: []*domain.Post{}, Pagination: page.WithTotal(0)}, nil
|
|
|
|
|
|
}
|
2026-08-13 02:22:24 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 01:15:30 +00:00
|
|
|
|
func formatKey(uid int64, theme string) string {
|
|
|
|
|
|
return formatUID(uid) + "|" + theme
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var _ domain.Repository = (*MonStore)(nil)
|