6e0519a232
- Implemented a new BrandAssetCleanupWorker to handle the cleanup of brand-related assets after a brand is deleted. - Added SQL queries for cleaning up articles, keywords, questions, competitors, and monitoring data associated with a brand. - Introduced a new API endpoint to delete publish records. - Updated the router to include the new delete publish record endpoint. - Added tests for the BrandAssetCleanupWorker to ensure proper functionality. - Created migration scripts to support soft deletion of publish records and to add the brand asset cleanup scheduler job.
359 lines
12 KiB
Go
359 lines
12 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
sharedcache "github.com/geo-platform/tenant-api/internal/shared/cache"
|
|
)
|
|
|
|
const (
|
|
defaultReadCacheTTL = 5 * time.Minute
|
|
defaultReadCacheEmptyTTL = 1 * time.Minute
|
|
articleCacheVersionTTL = 24 * time.Hour
|
|
)
|
|
|
|
func defaultCacheTTL() time.Duration {
|
|
return defaultReadCacheTTL
|
|
}
|
|
|
|
func defaultCacheEmptyTTL() time.Duration {
|
|
return defaultReadCacheEmptyTTL
|
|
}
|
|
|
|
func digestCacheKey(value interface{}) string {
|
|
raw, err := json.Marshal(value)
|
|
if err != nil {
|
|
return "fallback"
|
|
}
|
|
|
|
sum := sha1.Sum(raw)
|
|
return hex.EncodeToString(sum[:8])
|
|
}
|
|
|
|
func deleteCacheKey(ctx context.Context, c sharedcache.Cache, key string) {
|
|
if c == nil || key == "" {
|
|
return
|
|
}
|
|
_ = c.Delete(ctx, key)
|
|
}
|
|
|
|
func deleteCachePrefix(ctx context.Context, c sharedcache.Cache, prefix string) {
|
|
if c == nil || prefix == "" {
|
|
return
|
|
}
|
|
_ = c.DeletePrefix(ctx, prefix)
|
|
}
|
|
|
|
func workspaceOverviewCachePrefix(tenantID int64) string {
|
|
return fmt.Sprintf("workspace:overview:%d:", tenantID)
|
|
}
|
|
|
|
func workspaceOverviewCacheKey(tenantID, brandID int64) string {
|
|
return fmt.Sprintf("%s%d", workspaceOverviewCachePrefix(tenantID), brandID)
|
|
}
|
|
|
|
func workspaceRecentArticlesCachePrefix(tenantID int64) string {
|
|
return fmt.Sprintf("workspace:recent_articles:%d:", tenantID)
|
|
}
|
|
|
|
func workspaceRecentArticlesCacheKey(tenantID, brandID int64) string {
|
|
return fmt.Sprintf("%s%d", workspaceRecentArticlesCachePrefix(tenantID), brandID)
|
|
}
|
|
|
|
func workspaceQuotaSummaryCacheKey(tenantID int64) string {
|
|
return fmt.Sprintf("workspace:quota_summary:%d", tenantID)
|
|
}
|
|
|
|
func workspaceTemplateCardsCacheKey(tenantID int64) string {
|
|
return fmt.Sprintf("workspace:template_cards:%d", tenantID)
|
|
}
|
|
|
|
func workspaceKolCardsCacheKey(tenantID int64) string {
|
|
return fmt.Sprintf("workspace:kol_cards:%d", tenantID)
|
|
}
|
|
|
|
func invalidateWorkspaceCaches(ctx context.Context, c sharedcache.Cache, tenantID int64) {
|
|
deleteCachePrefix(ctx, c, workspaceOverviewCachePrefix(tenantID))
|
|
deleteCachePrefix(ctx, c, workspaceRecentArticlesCachePrefix(tenantID))
|
|
deleteCacheKey(ctx, c, workspaceQuotaSummaryCacheKey(tenantID))
|
|
deleteCacheKey(ctx, c, workspaceTemplateCardsCacheKey(tenantID))
|
|
deleteCacheKey(ctx, c, workspaceKolCardsCacheKey(tenantID))
|
|
}
|
|
|
|
func brandListCacheKey(tenantID int64) string {
|
|
return fmt.Sprintf("brand:list:%d", tenantID)
|
|
}
|
|
|
|
func brandLibrarySummaryCacheKey(tenantID int64) string {
|
|
return fmt.Sprintf("brand:library_summary:%d", tenantID)
|
|
}
|
|
|
|
func tenantQuestionQuotaLockKey(tenantID int64) string {
|
|
return fmt.Sprintf("tenant:%d:question_quota", tenantID)
|
|
}
|
|
|
|
func brandDetailCacheKey(tenantID, brandID int64) string {
|
|
return fmt.Sprintf("brand:detail:%d:%d", tenantID, brandID)
|
|
}
|
|
|
|
func brandKeywordsCachePrefix(tenantID, brandID int64) string {
|
|
return fmt.Sprintf("brand:keywords:%d:%d:", tenantID, brandID)
|
|
}
|
|
|
|
func brandKeywordsCacheKey(tenantID, brandID int64) string {
|
|
return brandKeywordsCachePrefix(tenantID, brandID) + "all"
|
|
}
|
|
|
|
func brandQuestionsCachePrefix(tenantID, brandID int64) string {
|
|
return fmt.Sprintf("brand:questions:%d:%d:", tenantID, brandID)
|
|
}
|
|
|
|
func brandQuestionsCacheKey(tenantID, brandID int64, params QuestionListParams) string {
|
|
return fmt.Sprintf("%s%s", brandQuestionsCachePrefix(tenantID, brandID), digestCacheKey(struct {
|
|
KeywordID *int64 `json:"keyword_id,omitempty"`
|
|
Page int `json:"page"`
|
|
PageSize int `json:"page_size"`
|
|
Query string `json:"q,omitempty"`
|
|
}{
|
|
KeywordID: params.KeywordID,
|
|
Page: params.Page,
|
|
PageSize: params.PageSize,
|
|
Query: params.Query,
|
|
}))
|
|
}
|
|
|
|
func brandCompetitorsCachePrefix(tenantID, brandID int64) string {
|
|
return fmt.Sprintf("brand:competitors:%d:%d:", tenantID, brandID)
|
|
}
|
|
|
|
func brandCompetitorsCacheKey(tenantID, brandID int64) string {
|
|
return brandCompetitorsCachePrefix(tenantID, brandID) + "all"
|
|
}
|
|
|
|
func scheduleTaskListCachePrefix(tenantID int64) string {
|
|
return fmt.Sprintf("schedule_task:list:%d:", tenantID)
|
|
}
|
|
|
|
func scheduleTaskDetailCachePrefix(tenantID int64) string {
|
|
return fmt.Sprintf("schedule_task:detail:%d:", tenantID)
|
|
}
|
|
|
|
func invalidateBrandCaches(ctx context.Context, c sharedcache.Cache, tenantID, brandID int64) {
|
|
deleteCacheKey(ctx, c, brandDetailCacheKey(tenantID, brandID))
|
|
deleteCachePrefix(ctx, c, brandKeywordsCachePrefix(tenantID, brandID))
|
|
deleteCachePrefix(ctx, c, brandQuestionsCachePrefix(tenantID, brandID))
|
|
deleteCachePrefix(ctx, c, brandCompetitorsCachePrefix(tenantID, brandID))
|
|
deleteCacheKey(ctx, c, brandListCacheKey(tenantID))
|
|
deleteCacheKey(ctx, c, brandLibrarySummaryCacheKey(tenantID))
|
|
deleteCachePrefix(ctx, c, scheduleTaskListCachePrefix(tenantID))
|
|
deleteCachePrefix(ctx, c, scheduleTaskDetailCachePrefix(tenantID))
|
|
invalidateWorkspaceCaches(ctx, c, tenantID)
|
|
}
|
|
|
|
func InvalidateBrandDeletionCaches(ctx context.Context, c sharedcache.Cache, tenantID, brandID int64) {
|
|
invalidateBrandCaches(ctx, c, tenantID, brandID)
|
|
invalidateArticleCaches(ctx, c, tenantID, nil)
|
|
invalidatePromptRuleCaches(ctx, c, tenantID, brandID, nil)
|
|
invalidateScheduleTaskCaches(ctx, c, tenantID, nil)
|
|
}
|
|
|
|
func promptRuleGroupsCacheKey(tenantID, brandID int64) string {
|
|
return fmt.Sprintf("prompt_rule:groups:%d:%d", tenantID, brandID)
|
|
}
|
|
|
|
func promptRuleSimpleCacheKey(tenantID, brandID int64) string {
|
|
return fmt.Sprintf("prompt_rule:simple:%d:%d", tenantID, brandID)
|
|
}
|
|
|
|
func promptRuleDetailCachePrefix(tenantID, brandID int64) string {
|
|
return fmt.Sprintf("prompt_rule:detail:%d:%d:", tenantID, brandID)
|
|
}
|
|
|
|
func promptRuleDetailCacheKey(tenantID, brandID, ruleID int64) string {
|
|
return fmt.Sprintf("%s%d", promptRuleDetailCachePrefix(tenantID, brandID), ruleID)
|
|
}
|
|
|
|
func promptRuleListCachePrefix(tenantID, brandID int64) string {
|
|
return fmt.Sprintf("prompt_rule:list:%d:%d:", tenantID, brandID)
|
|
}
|
|
|
|
func promptRuleListCacheKey(tenantID, brandID int64, params interface{}) string {
|
|
return promptRuleListCachePrefix(tenantID, brandID) + digestCacheKey(params)
|
|
}
|
|
|
|
func articleListCachePrefix(tenantID int64) string {
|
|
return fmt.Sprintf("article:list:%d:", tenantID)
|
|
}
|
|
|
|
func invalidatePromptRuleCaches(ctx context.Context, c sharedcache.Cache, tenantID, brandID int64, ruleID *int64) {
|
|
deleteCacheKey(ctx, c, promptRuleGroupsCacheKey(tenantID, brandID))
|
|
deleteCacheKey(ctx, c, promptRuleSimpleCacheKey(tenantID, brandID))
|
|
deleteCachePrefix(ctx, c, promptRuleListCachePrefix(tenantID, brandID))
|
|
if ruleID != nil && *ruleID > 0 {
|
|
deleteCacheKey(ctx, c, promptRuleDetailCacheKey(tenantID, brandID, *ruleID))
|
|
} else {
|
|
deleteCachePrefix(ctx, c, promptRuleDetailCachePrefix(tenantID, brandID))
|
|
}
|
|
deleteCachePrefix(ctx, c, scheduleTaskListCachePrefix(tenantID))
|
|
deleteCachePrefix(ctx, c, scheduleTaskDetailCachePrefix(tenantID))
|
|
deleteCachePrefix(ctx, c, articleListCachePrefix(tenantID))
|
|
}
|
|
|
|
func scheduleTaskDetailCacheKey(tenantID, brandID, taskID int64) string {
|
|
return fmt.Sprintf("%s%d:%d", scheduleTaskDetailCachePrefix(tenantID), brandID, taskID)
|
|
}
|
|
|
|
func scheduleTaskListCacheKey(tenantID, brandID int64, params interface{}) string {
|
|
return fmt.Sprintf("%s%d:%s", scheduleTaskListCachePrefix(tenantID), brandID, digestCacheKey(params))
|
|
}
|
|
|
|
func invalidateScheduleTaskCaches(ctx context.Context, c sharedcache.Cache, tenantID int64, taskID *int64) {
|
|
deleteCachePrefix(ctx, c, scheduleTaskListCachePrefix(tenantID))
|
|
deleteCachePrefix(ctx, c, scheduleTaskDetailCachePrefix(tenantID))
|
|
}
|
|
|
|
func InvalidateScheduleTaskCaches(ctx context.Context, c sharedcache.Cache, tenantID int64, taskID *int64) {
|
|
invalidateScheduleTaskCaches(ctx, c, tenantID, taskID)
|
|
}
|
|
|
|
func articleDetailCachePrefix(tenantID int64) string {
|
|
return fmt.Sprintf("article:detail:%d:", tenantID)
|
|
}
|
|
|
|
func articleDetailCacheArticlePrefix(tenantID, articleID int64) string {
|
|
return fmt.Sprintf("%s%d:", articleDetailCachePrefix(tenantID), articleID)
|
|
}
|
|
|
|
func articleDetailCacheKey(tenantID, articleID int64, version string) string {
|
|
return fmt.Sprintf("%s%s", articleDetailCacheArticlePrefix(tenantID, articleID), normalizeArticleCacheVersion(version))
|
|
}
|
|
|
|
func articleListCacheKey(tenantID int64, version string, params interface{}) string {
|
|
return articleListCachePrefix(tenantID) + normalizeArticleCacheVersion(version) + ":" + digestCacheKey(params)
|
|
}
|
|
|
|
func articleCacheVersionKey(tenantID int64) string {
|
|
return fmt.Sprintf("cache:version:article:%d", tenantID)
|
|
}
|
|
|
|
func articleCacheVersion(ctx context.Context, c sharedcache.Cache, tenantID int64) string {
|
|
if c == nil || tenantID <= 0 {
|
|
return "0"
|
|
}
|
|
raw, err := c.Get(ctx, articleCacheVersionKey(tenantID))
|
|
if err != nil || len(raw) == 0 {
|
|
return "0"
|
|
}
|
|
return normalizeArticleCacheVersion(string(raw))
|
|
}
|
|
|
|
func bumpArticleCacheVersion(ctx context.Context, c sharedcache.Cache, tenantID int64) {
|
|
if c == nil || tenantID <= 0 {
|
|
return
|
|
}
|
|
version := uuid.NewString()
|
|
_ = c.Set(ctx, articleCacheVersionKey(tenantID), []byte(version), articleCacheVersionTTL)
|
|
}
|
|
|
|
func normalizeArticleCacheVersion(version string) string {
|
|
version = strings.TrimSpace(version)
|
|
if version == "" {
|
|
return "0"
|
|
}
|
|
return version
|
|
}
|
|
|
|
func invalidateArticleCaches(ctx context.Context, c sharedcache.Cache, tenantID int64, articleID *int64) {
|
|
bumpArticleCacheVersion(ctx, c, tenantID)
|
|
deleteCachePrefix(ctx, c, articleListCachePrefix(tenantID))
|
|
if articleID != nil && *articleID > 0 {
|
|
deleteCachePrefix(ctx, c, articleDetailCacheArticlePrefix(tenantID, *articleID))
|
|
} else {
|
|
deleteCachePrefix(ctx, c, articleDetailCachePrefix(tenantID))
|
|
}
|
|
invalidateWorkspaceCaches(ctx, c, tenantID)
|
|
}
|
|
|
|
func InvalidateArticleCaches(ctx context.Context, c sharedcache.Cache, tenantID int64, articleID *int64) {
|
|
invalidateArticleCaches(ctx, c, tenantID, articleID)
|
|
}
|
|
|
|
func kolPromptDetailCachePrefix(tenantID int64) string {
|
|
return fmt.Sprintf("kol:prompt:detail:%d:", tenantID)
|
|
}
|
|
|
|
func kolPromptContentCacheKey(tenantID, promptID int64) string {
|
|
return fmt.Sprintf("kol:prompt:content:%d:%d", tenantID, promptID)
|
|
}
|
|
|
|
func kolPromptListCachePrefix(tenantID int64) string {
|
|
return fmt.Sprintf("kol:prompt:list:%d:", tenantID)
|
|
}
|
|
|
|
func kolMarketplaceCachePrefix() string {
|
|
return "kol:marketplace:"
|
|
}
|
|
|
|
func kolMarketplacePackageListCachePrefix() string {
|
|
return kolMarketplaceCachePrefix() + "packages:list:"
|
|
}
|
|
|
|
func kolMarketplacePackageListCacheKey(viewerTenantID int64, params interface{}) string {
|
|
return fmt.Sprintf("%s%d:%s", kolMarketplacePackageListCachePrefix(), viewerTenantID, digestCacheKey(params))
|
|
}
|
|
|
|
func kolMarketplacePackageDetailCachePrefix() string {
|
|
return kolMarketplaceCachePrefix() + "packages:detail:"
|
|
}
|
|
|
|
func kolMarketplacePackageDetailCacheKey(viewerTenantID, packageID int64) string {
|
|
return fmt.Sprintf("%s%d:%d", kolMarketplacePackageDetailCachePrefix(), viewerTenantID, packageID)
|
|
}
|
|
|
|
func kolSubscriptionPromptListCachePrefix(tenantID int64) string {
|
|
return fmt.Sprintf("kol:subscription_prompt:list:%d:", tenantID)
|
|
}
|
|
|
|
func kolSubscriptionPromptListCacheKey(tenantID int64) string {
|
|
return kolSubscriptionPromptListCachePrefix(tenantID) + "all"
|
|
}
|
|
|
|
func kolSubscriptionPromptSchemaCachePrefix(tenantID int64) string {
|
|
return fmt.Sprintf("kol:subscription_prompt:schema:%d:", tenantID)
|
|
}
|
|
|
|
func kolSubscriptionPromptSchemaCacheKey(tenantID, subscriptionPromptID int64) string {
|
|
return fmt.Sprintf("%s%d", kolSubscriptionPromptSchemaCachePrefix(tenantID), subscriptionPromptID)
|
|
}
|
|
|
|
func InvalidateKolPublicCaches(ctx context.Context, c sharedcache.Cache) {
|
|
deleteCachePrefix(ctx, c, kolMarketplaceCachePrefix())
|
|
deleteCachePrefix(ctx, c, "kol:subscription_prompt:list:")
|
|
deleteCachePrefix(ctx, c, "kol:subscription_prompt:schema:")
|
|
deleteCachePrefix(ctx, c, "workspace:kol_cards:")
|
|
}
|
|
|
|
func invalidateKolPromptCaches(ctx context.Context, c sharedcache.Cache, tenantID int64) {
|
|
deleteCachePrefix(ctx, c, kolPromptDetailCachePrefix(tenantID))
|
|
deleteCachePrefix(ctx, c, fmt.Sprintf("kol:prompt:content:%d:", tenantID))
|
|
deleteCachePrefix(ctx, c, kolPromptListCachePrefix(tenantID))
|
|
deleteCachePrefix(ctx, c, kolSubscriptionPromptListCachePrefix(tenantID))
|
|
deleteCachePrefix(ctx, c, kolSubscriptionPromptSchemaCachePrefix(tenantID))
|
|
deleteCacheKey(ctx, c, workspaceKolCardsCacheKey(tenantID))
|
|
}
|
|
|
|
func InvalidateKolPromptCaches(ctx context.Context, c sharedcache.Cache) {
|
|
deleteCachePrefix(ctx, c, "kol:prompt:detail:")
|
|
deleteCachePrefix(ctx, c, "kol:prompt:content:")
|
|
deleteCachePrefix(ctx, c, "kol:prompt:list:")
|
|
InvalidateKolPublicCaches(ctx, c)
|
|
}
|