feat(kol): store KOL prompt content in database with object storage fallback
Add prompt_content columns to kol_prompts and kol_prompt_revisions so prompt bodies live alongside their metadata, eliminating an extra round trip to object storage for the common read path while keeping the old asset key as a fallback for legacy rows. Reads go through a singleflight-deduped, cache-backed loader that prefers the database column, falls back to the asset key, and tolerates missing objects via a new objectstorage.ErrObjectNotFound returned by both the Aliyun OSS and MinIO clients on 404/NoSuchKey responses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
||||
sharedcache "github.com/geo-platform/tenant-api/internal/shared/cache"
|
||||
@@ -68,6 +69,7 @@ type KolPromptService struct {
|
||||
subRepo repository.KolSubscriptionRepository
|
||||
asset *KolPromptAsset
|
||||
cache sharedcache.Cache
|
||||
cacheGroup singleflight.Group
|
||||
}
|
||||
|
||||
func NewKolPromptService(
|
||||
@@ -210,13 +212,6 @@ func (s *KolPromptService) Save(ctx context.Context, actor auth.Actor, promptID
|
||||
sortOrder = *input.SortOrder
|
||||
}
|
||||
|
||||
const currentRevisionNo = 1
|
||||
|
||||
assetKey, err := s.asset.Put(ctx, actor.TenantID, promptID, currentRevisionNo, input.Content)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50074, "kol_prompt_asset_store_failed", err.Error())
|
||||
}
|
||||
|
||||
schemaJSON, err := JSONEncodeSchema(input.Schema)
|
||||
if err != nil {
|
||||
return nil, response.ErrBadRequest(40065, "kol_prompt_schema_invalid", err.Error())
|
||||
@@ -229,6 +224,7 @@ func (s *KolPromptService) Save(ctx context.Context, actor auth.Actor, promptID
|
||||
|
||||
publishedRevisionNo := prompt.PublishedRevisionNo
|
||||
if prompt.Status == "active" {
|
||||
const currentRevisionNo = 1
|
||||
revNo := currentRevisionNo
|
||||
publishedRevisionNo = &revNo
|
||||
}
|
||||
@@ -239,7 +235,7 @@ func (s *KolPromptService) Save(ctx context.Context, actor auth.Actor, promptID
|
||||
Name: name,
|
||||
PlatformHint: normalizeKolOptionalString(input.PlatformHint),
|
||||
SortOrder: sortOrder,
|
||||
PromptAssetKey: assetKey,
|
||||
PromptContent: input.Content,
|
||||
SchemaJSON: schemaJSON,
|
||||
CardConfigJSON: cardConfigJSON,
|
||||
Status: prompt.Status,
|
||||
@@ -283,13 +279,6 @@ func (s *KolPromptService) Publish(ctx context.Context, actor auth.Actor, prompt
|
||||
sortOrder = *input.SortOrder
|
||||
}
|
||||
|
||||
const publishedRevisionNo = 1
|
||||
|
||||
assetKey, err := s.asset.Put(ctx, actor.TenantID, promptID, publishedRevisionNo, input.Content)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50074, "kol_prompt_asset_store_failed", err.Error())
|
||||
}
|
||||
|
||||
schemaJSON, err := JSONEncodeSchema(input.Schema)
|
||||
if err != nil {
|
||||
return nil, response.ErrBadRequest(40065, "kol_prompt_schema_invalid", err.Error())
|
||||
@@ -309,13 +298,14 @@ func (s *KolPromptService) Publish(ctx context.Context, actor auth.Actor, prompt
|
||||
promptTx := repository.NewKolPromptRepository(tx)
|
||||
subTx := repository.NewKolSubscriptionRepository(tx)
|
||||
|
||||
const publishedRevisionNo = 1
|
||||
if err := promptTx.Publish(ctx, repository.PublishKolPromptInput{
|
||||
ID: promptID,
|
||||
TenantID: actor.TenantID,
|
||||
Name: name,
|
||||
PlatformHint: normalizeKolOptionalString(input.PlatformHint),
|
||||
SortOrder: sortOrder,
|
||||
PromptAssetKey: assetKey,
|
||||
PromptContent: input.Content,
|
||||
SchemaJSON: schemaJSON,
|
||||
CardConfigJSON: cardConfigJSON,
|
||||
PublishedRevisionNo: publishedRevisionNo,
|
||||
@@ -359,14 +349,12 @@ func (s *KolPromptService) Activate(ctx context.Context, actor auth.Actor, promp
|
||||
promptTx := repository.NewKolPromptRepository(tx)
|
||||
subTx := repository.NewKolSubscriptionRepository(tx)
|
||||
|
||||
switch {
|
||||
case prompt.PromptAssetKey != nil && strings.TrimSpace(*prompt.PromptAssetKey) != "":
|
||||
if err := promptTx.SetPublishedRevision(ctx, actor.TenantID, promptID, 1, actor.UserID); err != nil {
|
||||
return nil, response.ErrInternal(50090, "kol_prompt_activate_failed", err.Error())
|
||||
}
|
||||
default:
|
||||
if !s.promptHasContent(ctx, prompt) {
|
||||
return nil, errKolPromptContentEmpty
|
||||
}
|
||||
if err := promptTx.SetPublishedRevision(ctx, actor.TenantID, promptID, 1, actor.UserID); err != nil {
|
||||
return nil, response.ErrInternal(50090, "kol_prompt_activate_failed", err.Error())
|
||||
}
|
||||
|
||||
if err := s.grantPromptToActiveSubscribers(ctx, subTx, pkg.ID, promptID); err != nil {
|
||||
return nil, err
|
||||
@@ -484,7 +472,7 @@ func (s *KolPromptService) buildPromptDetail(ctx context.Context, prompt *reposi
|
||||
UpdatedAt: prompt.UpdatedAt,
|
||||
}
|
||||
|
||||
if len(prompt.SchemaJSON) == 0 && len(prompt.CardConfigJSON) == 0 && (prompt.PromptAssetKey == nil || strings.TrimSpace(*prompt.PromptAssetKey) == "") {
|
||||
if len(prompt.SchemaJSON) == 0 && len(prompt.CardConfigJSON) == 0 && !includeContent {
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
@@ -500,8 +488,8 @@ func (s *KolPromptService) buildPromptDetail(ctx context.Context, prompt *reposi
|
||||
detail.LatestSchemaJSON = schema
|
||||
detail.LatestCardConfigJSON = cardConfig
|
||||
|
||||
if includeContent && prompt.PromptAssetKey != nil && strings.TrimSpace(*prompt.PromptAssetKey) != "" {
|
||||
content, err := s.asset.Get(ctx, *prompt.PromptAssetKey)
|
||||
if includeContent {
|
||||
content, err := s.loadPromptContent(ctx, prompt)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50087, "kol_prompt_asset_load_failed", err.Error())
|
||||
}
|
||||
@@ -511,6 +499,15 @@ func (s *KolPromptService) buildPromptDetail(ctx context.Context, prompt *reposi
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
func (s *KolPromptService) promptHasContent(ctx context.Context, prompt *repository.KolPrompt) bool {
|
||||
content, err := s.loadPromptContent(ctx, prompt)
|
||||
return err == nil && strings.TrimSpace(content) != ""
|
||||
}
|
||||
|
||||
func (s *KolPromptService) loadPromptContent(ctx context.Context, prompt *repository.KolPrompt) (string, error) {
|
||||
return loadKolPromptContent(ctx, s.cache, &s.cacheGroup, s.promptRepo, s.asset, prompt)
|
||||
}
|
||||
|
||||
func (s *KolPromptService) grantPromptToActiveSubscribers(
|
||||
ctx context.Context,
|
||||
subRepo repository.KolSubscriptionRepository,
|
||||
|
||||
Reference in New Issue
Block a user