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:
2026-05-24 01:47:51 +08:00
parent 075e282c76
commit db95b8e4ee
12 changed files with 225 additions and 58 deletions
@@ -22,6 +22,7 @@ type KolPrompt struct {
PublishedRevisionNo *int
DraftRevisionNo *int
PromptAssetKey *string
PromptContent *string
SchemaJSON []byte
CardConfigJSON []byte
CreatedBy int64
@@ -61,7 +62,8 @@ type SaveKolPromptInput struct {
Name string
PlatformHint *string
SortOrder int
PromptAssetKey string
PromptAssetKey *string
PromptContent string
SchemaJSON []byte
CardConfigJSON []byte
Status string
@@ -75,7 +77,8 @@ type PublishKolPromptInput struct {
Name string
PlatformHint *string
SortOrder int
PromptAssetKey string
PromptAssetKey *string
PromptContent string
SchemaJSON []byte
CardConfigJSON []byte
PublishedRevisionNo int
@@ -85,6 +88,7 @@ type PublishKolPromptInput struct {
type KolPromptRepository interface {
Create(ctx context.Context, input CreateKolPromptInput) (*KolPrompt, error)
GetByID(ctx context.Context, tenantID, id int64) (*KolPrompt, error)
GetContentByID(ctx context.Context, tenantID, id int64) (*string, error)
ListByPackage(ctx context.Context, tenantID, packageID int64) ([]KolPrompt, error)
CountByPackages(ctx context.Context, tenantID int64, packageIDs []int64) (map[int64]int, error)
ListActiveByPackage(ctx context.Context, tenantID, packageID int64) ([]ActiveKolPrompt, error)
@@ -116,6 +120,7 @@ func mapKolPromptRow(
publishedRevisionNo pgtype.Int4,
draftRevisionNo pgtype.Int4,
promptAssetKey pgtype.Text,
promptContent pgtype.Text,
schemaJSON []byte,
cardConfigJSON []byte,
createdBy int64,
@@ -134,6 +139,7 @@ func mapKolPromptRow(
PublishedRevisionNo: nullableIntFromInt4(publishedRevisionNo),
DraftRevisionNo: nullableIntFromInt4(draftRevisionNo),
PromptAssetKey: nullableText(promptAssetKey),
PromptContent: nullableText(promptContent),
SchemaJSON: schemaJSON,
CardConfigJSON: cardConfigJSON,
CreatedBy: createdBy,
@@ -155,6 +161,7 @@ func scanKolPrompt(scanner interface{ Scan(...any) error }) (*KolPrompt, error)
publishedRevisionNo pgtype.Int4
draftRevisionNo pgtype.Int4
promptAssetKey pgtype.Text
promptContent pgtype.Text
schemaJSON []byte
cardConfigJSON []byte
createdBy int64
@@ -174,6 +181,7 @@ func scanKolPrompt(scanner interface{ Scan(...any) error }) (*KolPrompt, error)
&publishedRevisionNo,
&draftRevisionNo,
&promptAssetKey,
&promptContent,
&schemaJSON,
&cardConfigJSON,
&createdBy,
@@ -195,6 +203,7 @@ func scanKolPrompt(scanner interface{ Scan(...any) error }) (*KolPrompt, error)
publishedRevisionNo,
draftRevisionNo,
promptAssetKey,
promptContent,
schemaJSON,
cardConfigJSON,
createdBy,
@@ -222,7 +231,7 @@ func (r *kolPromptRepository) Create(ctx context.Context, input CreateKolPromptI
func (r *kolPromptRepository) GetByID(ctx context.Context, tenantID, id int64) (*KolPrompt, error) {
row := r.db.QueryRow(ctx, `
SELECT id, tenant_id, package_id, name, platform_hint, status, sort_order,
published_revision_no, draft_revision_no, prompt_asset_key, schema_json, card_config_json,
published_revision_no, draft_revision_no, prompt_asset_key, NULL::text AS prompt_content, schema_json, card_config_json,
created_by, updated_by, created_at, updated_at
FROM kol_prompts
WHERE id = $1
@@ -240,10 +249,28 @@ WHERE id = $1
return prompt, nil
}
func (r *kolPromptRepository) GetContentByID(ctx context.Context, tenantID, id int64) (*string, error) {
var content pgtype.Text
err := r.db.QueryRow(ctx, `
SELECT prompt_content
FROM kol_prompts
WHERE id = $1
AND tenant_id = $2
AND deleted_at IS NULL
`, id, tenantID).Scan(&content)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return nullableText(content), nil
}
func (r *kolPromptRepository) ListByPackage(ctx context.Context, tenantID, packageID int64) ([]KolPrompt, error) {
rows, err := r.db.Query(ctx, `
SELECT id, tenant_id, package_id, name, platform_hint, status, sort_order,
published_revision_no, draft_revision_no, prompt_asset_key, schema_json, card_config_json,
published_revision_no, draft_revision_no, prompt_asset_key, NULL::text AS prompt_content, schema_json, card_config_json,
created_by, updated_by, created_at, updated_at
FROM kol_prompts
WHERE package_id = $1
@@ -346,16 +373,17 @@ SET name = $1,
platform_hint = $2,
sort_order = $3,
prompt_asset_key = $4,
schema_json = $5::jsonb,
card_config_json = $6::jsonb,
status = $7,
published_revision_no = $8,
updated_by = $9,
prompt_content = $5,
schema_json = $6::jsonb,
card_config_json = $7::jsonb,
status = $8,
published_revision_no = $9,
updated_by = $10,
updated_at = NOW()
WHERE id = $10
AND tenant_id = $11
WHERE id = $11
AND tenant_id = $12
AND deleted_at IS NULL
`, input.Name, pgText(input.PlatformHint), int32(input.SortOrder), input.PromptAssetKey, input.SchemaJSON, input.CardConfigJSON, input.Status, publishedRevisionNo, input.UpdatedBy, input.ID, input.TenantID)
`, input.Name, pgText(input.PlatformHint), int32(input.SortOrder), pgText(input.PromptAssetKey), input.PromptContent, input.SchemaJSON, input.CardConfigJSON, input.Status, publishedRevisionNo, input.UpdatedBy, input.ID, input.TenantID)
return err
}
@@ -366,17 +394,18 @@ SET name = $1,
platform_hint = $2,
sort_order = $3,
prompt_asset_key = $4,
schema_json = $5::jsonb,
card_config_json = $6::jsonb,
published_revision_no = $7,
prompt_content = $5,
schema_json = $6::jsonb,
card_config_json = $7::jsonb,
published_revision_no = $8,
draft_revision_no = NULL,
status = 'active',
updated_by = $8,
updated_by = $9,
updated_at = NOW()
WHERE id = $9
AND tenant_id = $10
WHERE id = $10
AND tenant_id = $11
AND deleted_at IS NULL
`, input.Name, pgText(input.PlatformHint), int32(input.SortOrder), input.PromptAssetKey, input.SchemaJSON, input.CardConfigJSON, input.PublishedRevisionNo, input.UpdatedBy, input.ID, input.TenantID)
`, input.Name, pgText(input.PlatformHint), int32(input.SortOrder), pgText(input.PromptAssetKey), input.PromptContent, input.SchemaJSON, input.CardConfigJSON, input.PublishedRevisionNo, input.UpdatedBy, input.ID, input.TenantID)
return err
}