thread-master/backend/internal/model/publish_guard/usecase/usecase.go

172 lines
6.0 KiB
Go
Raw Permalink Normal View History

package usecase
import (
"context"
"strings"
"time"
"haixun-backend/internal/library/clock"
app "haixun-backend/internal/library/errors"
"haixun-backend/internal/library/errors/code"
"haixun-backend/internal/model/publish_guard/domain/entity"
domrepo "haixun-backend/internal/model/publish_guard/domain/repository"
domusecase "haixun-backend/internal/model/publish_guard/domain/usecase"
threadsaccountdomain "haixun-backend/internal/model/threads_account/domain/usecase"
"github.com/google/uuid"
)
type guardUseCase struct {
repo domrepo.Repository
threadsAccount threadsaccountdomain.UseCase
}
func NewUseCase(repo domrepo.Repository, threadsAccount threadsaccountdomain.UseCase) domusecase.UseCase {
return &guardUseCase{repo: repo, threadsAccount: threadsAccount}
}
func (u *guardUseCase) Get(ctx context.Context, tenantID, ownerUID, accountID string) (*domusecase.PolicySummary, error) {
if err := u.require(ctx, tenantID, ownerUID, accountID); err != nil {
return nil, err
}
item, err := u.repo.Get(ctx, tenantID, ownerUID, accountID)
if err != nil {
if strings.Contains(err.Error(), "not found") {
item = defaultPolicy(tenantID, ownerUID, accountID)
} else {
return nil, err
}
}
out := toSummary(item)
return &out, nil
}
func (u *guardUseCase) Upsert(ctx context.Context, req domusecase.UpsertRequest) (*domusecase.PolicySummary, error) {
if err := u.require(ctx, req.TenantID, req.OwnerUID, req.AccountID); err != nil {
return nil, err
}
item := defaultPolicy(req.TenantID, req.OwnerUID, req.AccountID)
item.Enabled = req.Enabled
if req.MaxDailyPosts > 0 {
item.MaxDailyPosts = req.MaxDailyPosts
}
if req.MinIntervalMinutes > 0 {
item.MinIntervalMinutes = req.MinIntervalMinutes
}
if req.ConsecutiveFailurePauseLimit > 0 {
item.ConsecutiveFailurePauseLimit = req.ConsecutiveFailurePauseLimit
}
if req.Paused != nil {
item.Paused = *req.Paused
}
item.PausedReason = strings.TrimSpace(req.PausedReason)
item.UpdateAt = clock.NowUnixNano()
saved, err := u.repo.Upsert(ctx, item)
if err != nil {
return nil, err
}
out := toSummary(saved)
return &out, nil
}
func (u *guardUseCase) Resume(ctx context.Context, tenantID, ownerUID, accountID string) (*domusecase.PolicySummary, error) {
policy, err := u.Get(ctx, tenantID, ownerUID, accountID)
if err != nil {
return nil, err
}
paused := false
return u.Upsert(ctx, domusecase.UpsertRequest{
TenantID: tenantID, OwnerUID: ownerUID, AccountID: accountID, Enabled: policy.Enabled,
MaxDailyPosts: policy.MaxDailyPosts, MinIntervalMinutes: policy.MinIntervalMinutes,
ConsecutiveFailurePauseLimit: policy.ConsecutiveFailurePauseLimit,
Paused: &paused,
})
}
func (u *guardUseCase) Pause(ctx context.Context, tenantID, ownerUID, accountID, reason string) (*domusecase.PolicySummary, error) {
policy, err := u.Get(ctx, tenantID, ownerUID, accountID)
if err != nil {
return nil, err
}
paused := true
return u.Upsert(ctx, domusecase.UpsertRequest{
TenantID: tenantID, OwnerUID: ownerUID, AccountID: accountID, Enabled: policy.Enabled,
MaxDailyPosts: policy.MaxDailyPosts, MinIntervalMinutes: policy.MinIntervalMinutes,
ConsecutiveFailurePauseLimit: policy.ConsecutiveFailurePauseLimit,
Paused: &paused, PausedReason: reason,
})
}
func (u *guardUseCase) Check(ctx context.Context, req domusecase.CheckRequest) (*domusecase.CheckResult, error) {
policy, err := u.Get(ctx, req.TenantID, req.OwnerUID, req.AccountID)
if err != nil {
return nil, err
}
if !policy.Enabled {
return &domusecase.CheckResult{Allowed: true}, nil
}
now := req.Now
if now <= 0 {
now = clock.NowUnixNano()
}
if policy.Paused {
return &domusecase.CheckResult{Allowed: false, Reason: firstNonEmpty(policy.PausedReason, "發文已暫停")}, nil
}
if policy.MaxDailyPosts > 0 && req.PublishedToday >= int64(policy.MaxDailyPosts) {
return &domusecase.CheckResult{Allowed: false, DelayUntil: now + int64(time.Hour), Reason: "已達每日發文上限"}, nil
}
if policy.MinIntervalMinutes > 0 && req.LastPublishedAt > 0 {
minNext := req.LastPublishedAt + int64(time.Duration(policy.MinIntervalMinutes)*time.Minute)
if minNext > now {
return &domusecase.CheckResult{Allowed: false, DelayUntil: minNext, Reason: "未達最小發文間隔"}, nil
}
}
if policy.ConsecutiveFailurePauseLimit > 0 && req.FailureStreak >= policy.ConsecutiveFailurePauseLimit {
return &domusecase.CheckResult{Allowed: false, Reason: "連續失敗達上限,已暫停帳號發文", ShouldPause: true}, nil
}
return &domusecase.CheckResult{Allowed: true}, nil
}
func (u *guardUseCase) require(ctx context.Context, tenantID, ownerUID, accountID string) error {
if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" {
return app.For(code.Auth).AuthUnauthorized("missing actor")
}
if strings.TrimSpace(accountID) == "" {
return app.For(code.ThreadsAccount).InputMissingRequired("account_id is required")
}
if u.threadsAccount != nil {
_, err := u.threadsAccount.Get(ctx, tenantID, ownerUID, accountID)
return err
}
return nil
}
func defaultPolicy(tenantID, ownerUID, accountID string) *entity.Policy {
now := clock.NowUnixNano()
return &entity.Policy{
ID: uuid.NewString(), TenantID: strings.TrimSpace(tenantID), OwnerUID: strings.TrimSpace(ownerUID), AccountID: strings.TrimSpace(accountID),
Enabled: true, MaxDailyPosts: 6, MinIntervalMinutes: 90, ConsecutiveFailurePauseLimit: 3,
CreateAt: now, UpdateAt: now,
}
}
func toSummary(item *entity.Policy) domusecase.PolicySummary {
if item == nil {
return domusecase.PolicySummary{}
}
return domusecase.PolicySummary{
AccountID: item.AccountID, Enabled: item.Enabled, MaxDailyPosts: item.MaxDailyPosts,
MinIntervalMinutes: item.MinIntervalMinutes, ConsecutiveFailurePauseLimit: item.ConsecutiveFailurePauseLimit,
Paused: item.Paused, PausedReason: item.PausedReason, UpdateAt: item.UpdateAt,
}
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}