feat: Introduce AI Brand Monitoring System V5 technical design document

- Added comprehensive technical design document for AI Brand Monitoring System V5, outlining system architecture, data models, sampling strategies, and monitoring protocols.
- Key changes include a shift to a sampling-based trend monitoring approach, updated data collection and storage strategies, and new metrics for performance evaluation.
- Implemented migration scripts to support the flattening of brand questions and versioning of question texts, ensuring historical data integrity and version control.
This commit is contained in:
2026-04-09 14:43:20 +08:00
parent 41f8e0621e
commit 36451a613d
18 changed files with 2709 additions and 234 deletions
+33 -74
View File
@@ -2,7 +2,6 @@ package app
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"strings"
@@ -276,7 +275,7 @@ func (s *BrandService) DeleteKeyword(ctx context.Context, brandID, keywordID int
return nil
}
// --- Question CRUD with versioning ---
// --- Question CRUD ---
type QuestionRequest struct {
KeywordID int64 `json:"keyword_id" binding:"required"`
@@ -284,23 +283,19 @@ type QuestionRequest struct {
}
type QuestionResponse struct {
ID int64 `json:"id"`
BrandID int64 `json:"brand_id"`
KeywordID int64 `json:"keyword_id"`
CurrentVersionID *int64 `json:"current_version_id"`
QuestionText *string `json:"question_text"`
VersionNo *int `json:"version_no"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
ID int64 `json:"id"`
BrandID int64 `json:"brand_id"`
KeywordID int64 `json:"keyword_id"`
QuestionText string `json:"question_text"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
}
func (s *BrandService) ListQuestions(ctx context.Context, brandID int64, keywordID *int64) ([]QuestionResponse, error) {
actor := auth.MustActor(ctx)
query := `
SELECT q.id, q.brand_id, q.keyword_id, q.current_version_id, q.status, q.created_at,
v.question_text, v.version_no
SELECT q.id, q.brand_id, q.keyword_id, q.question_text, q.status, q.created_at
FROM brand_questions q
LEFT JOIN brand_question_versions v ON v.id = q.current_version_id
WHERE q.brand_id = $1 AND q.tenant_id = $2 AND q.deleted_at IS NULL`
args := []interface{}{brandID, actor.TenantID}
@@ -320,7 +315,7 @@ func (s *BrandService) ListQuestions(ctx context.Context, brandID int64, keyword
for rows.Next() {
var q QuestionResponse
var ca interface{}
if err := rows.Scan(&q.ID, &q.BrandID, &q.KeywordID, &q.CurrentVersionID, &q.Status, &ca, &q.QuestionText, &q.VersionNo); err != nil {
if err := rows.Scan(&q.ID, &q.BrandID, &q.KeywordID, &q.QuestionText, &q.Status, &ca); err != nil {
return nil, response.ErrInternal(50010, "scan_failed", err.Error())
}
q.CreatedAt = fmt.Sprintf("%v", ca)
@@ -334,42 +329,29 @@ func (s *BrandService) ListQuestions(ctx context.Context, brandID int64, keyword
func (s *BrandService) CreateQuestion(ctx context.Context, brandID int64, req QuestionRequest) (*QuestionResponse, error) {
actor := auth.MustActor(ctx)
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, fmt.Errorf("begin tx: %w", err)
req.QuestionText = strings.TrimSpace(req.QuestionText)
if req.QuestionText == "" {
return nil, response.ErrBadRequest(40001, "invalid_params", "question_text is required")
}
defer func() {
_ = tx.Rollback(ctx)
}()
var questionID int64
err = tx.QueryRow(ctx, `
INSERT INTO brand_questions (tenant_id, brand_id, keyword_id, status) VALUES ($1, $2, $3, 'active') RETURNING id
`, actor.TenantID, brandID, req.KeywordID).Scan(&questionID)
var ca interface{}
err := s.pool.QueryRow(ctx, `
INSERT INTO brand_questions (tenant_id, brand_id, keyword_id, question_text, status)
VALUES ($1, $2, $3, $4, 'active')
RETURNING id, created_at
`, actor.TenantID, brandID, req.KeywordID, req.QuestionText).Scan(&questionID, &ca)
if err != nil {
return nil, response.ErrInternal(50010, "create_failed", "failed to create question")
}
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(req.QuestionText)))
var versionID int64
err = tx.QueryRow(ctx, `
INSERT INTO brand_question_versions (question_id, question_text, question_hash, version_no, is_active)
VALUES ($1, $2, $3, 1, true) RETURNING id
`, questionID, req.QuestionText, hash).Scan(&versionID)
if err != nil {
return nil, response.ErrInternal(50010, "create_version_failed", "failed to create question version")
}
_, _ = tx.Exec(ctx, `UPDATE brand_questions SET current_version_id = $1 WHERE id = $2`, versionID, questionID)
if err := tx.Commit(ctx); err != nil {
return nil, fmt.Errorf("commit: %w", err)
}
vno := 1
return &QuestionResponse{
ID: questionID, BrandID: brandID, KeywordID: req.KeywordID,
CurrentVersionID: &versionID, QuestionText: &req.QuestionText, VersionNo: &vno, Status: "active",
ID: questionID,
BrandID: brandID,
KeywordID: req.KeywordID,
QuestionText: req.QuestionText,
Status: "active",
CreatedAt: fmt.Sprintf("%v", ca),
}, nil
}
@@ -379,42 +361,19 @@ type UpdateQuestionRequest struct {
func (s *BrandService) UpdateQuestion(ctx context.Context, brandID, questionID int64, req UpdateQuestionRequest) error {
actor := auth.MustActor(ctx)
tx, err := s.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
req.QuestionText = strings.TrimSpace(req.QuestionText)
if req.QuestionText == "" {
return response.ErrBadRequest(40001, "invalid_params", "question_text is required")
}
defer func() {
_ = tx.Rollback(ctx)
}()
// Verify ownership
var exists bool
_ = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM brand_questions WHERE id = $1 AND brand_id = $2 AND tenant_id = $3 AND deleted_at IS NULL)`,
questionID, brandID, actor.TenantID).Scan(&exists)
if !exists {
tag, err := s.pool.Exec(ctx, `
UPDATE brand_questions SET question_text = $1, updated_at = NOW()
WHERE id = $2 AND brand_id = $3 AND tenant_id = $4 AND deleted_at IS NULL
`, req.QuestionText, questionID, brandID, actor.TenantID)
if err != nil || tag.RowsAffected() == 0 {
return response.ErrNotFound(40422, "question_not_found", "question not found")
}
// Deactivate current version
_, _ = tx.Exec(ctx, `UPDATE brand_question_versions SET is_active = false WHERE question_id = $1 AND is_active = true`, questionID)
// Get next version number
var maxVersion int
_ = tx.QueryRow(ctx, `SELECT COALESCE(MAX(version_no), 0) FROM brand_question_versions WHERE question_id = $1`, questionID).Scan(&maxVersion)
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(req.QuestionText)))
var versionID int64
err = tx.QueryRow(ctx, `
INSERT INTO brand_question_versions (question_id, question_text, question_hash, version_no, is_active)
VALUES ($1, $2, $3, $4, true) RETURNING id
`, questionID, req.QuestionText, hash, maxVersion+1).Scan(&versionID)
if err != nil {
return fmt.Errorf("create version: %w", err)
}
_, _ = tx.Exec(ctx, `UPDATE brand_questions SET current_version_id = $1, updated_at = NOW() WHERE id = $2`, versionID, questionID)
return tx.Commit(ctx)
return nil
}
func (s *BrandService) DeleteQuestion(ctx context.Context, brandID, questionID int64) error {
@@ -3,21 +3,11 @@ package domain
import "time"
type BrandQuestion struct {
ID int64
TenantID int64
BrandID int64
KeywordID int64
CurrentVersionID *int64
Status string
CreatedAt time.Time
}
type BrandQuestionVersion struct {
ID int64
QuestionID int64
TenantID int64
BrandID int64
KeywordID int64
QuestionText string
QuestionHash string
VersionNo int
IsActive bool
Status string
CreatedAt time.Time
}
@@ -20,6 +20,22 @@ func nullableText(value pgtype.Text) *string {
return &text
}
func nullableAnyText(value interface{}) *string {
switch typed := value.(type) {
case nil:
return nil
case string:
return &typed
case []byte:
text := string(typed)
return &text
case pgtype.Text:
return nullableText(typed)
default:
return nil
}
}
func nullableInt64(value pgtype.Int8) *int64 {
if !value.Valid {
return nil
@@ -94,59 +94,30 @@ func (q *Queries) CreateKeyword(ctx context.Context, arg CreateKeywordParams) (C
}
const createQuestion = `-- name: CreateQuestion :one
INSERT INTO brand_questions (tenant_id, brand_id, keyword_id, status)
VALUES ($1, $2, $3, 'active')
INSERT INTO brand_questions (tenant_id, brand_id, keyword_id, question_text, status)
VALUES ($1, $2, $3, $4, 'active')
RETURNING id
`
type CreateQuestionParams struct {
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
KeywordID int64 `json:"keyword_id"`
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
KeywordID int64 `json:"keyword_id"`
QuestionText string `json:"question_text"`
}
func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams) (int64, error) {
row := q.db.QueryRow(ctx, createQuestion, arg.TenantID, arg.BrandID, arg.KeywordID)
var id int64
err := row.Scan(&id)
return id, err
}
const createQuestionVersion = `-- name: CreateQuestionVersion :one
INSERT INTO brand_question_versions (question_id, question_text, question_hash, version_no, is_active)
VALUES ($1, $2, $3, $4, true)
RETURNING id
`
type CreateQuestionVersionParams struct {
QuestionID int64 `json:"question_id"`
QuestionText string `json:"question_text"`
QuestionHash string `json:"question_hash"`
VersionNo int32 `json:"version_no"`
}
func (q *Queries) CreateQuestionVersion(ctx context.Context, arg CreateQuestionVersionParams) (int64, error) {
row := q.db.QueryRow(ctx, createQuestionVersion,
arg.QuestionID,
row := q.db.QueryRow(ctx, createQuestion,
arg.TenantID,
arg.BrandID,
arg.KeywordID,
arg.QuestionText,
arg.QuestionHash,
arg.VersionNo,
)
var id int64
err := row.Scan(&id)
return id, err
}
const deactivateQuestionVersion = `-- name: DeactivateQuestionVersion :exec
UPDATE brand_question_versions SET is_active = false
WHERE question_id = $1 AND is_active = true
`
func (q *Queries) DeactivateQuestionVersion(ctx context.Context, questionID int64) error {
_, err := q.db.Exec(ctx, deactivateQuestionVersion, questionID)
return err
}
const getBrandByID = `-- name: GetBrandByID :one
SELECT id, tenant_id, name, description, status, created_at, updated_at
FROM brands
@@ -183,19 +154,6 @@ func (q *Queries) GetBrandByID(ctx context.Context, arg GetBrandByIDParams) (Get
return i, err
}
const getLatestQuestionVersionNo = `-- name: GetLatestQuestionVersionNo :one
SELECT COALESCE(MAX(version_no), 0)::INT AS max_version
FROM brand_question_versions
WHERE question_id = $1
`
func (q *Queries) GetLatestQuestionVersionNo(ctx context.Context, questionID int64) (int32, error) {
row := q.db.QueryRow(ctx, getLatestQuestionVersionNo, questionID)
var max_version int32
err := row.Scan(&max_version)
return max_version, err
}
const listBrands = `-- name: ListBrands :many
SELECT id, tenant_id, name, description, status, created_at, updated_at
FROM brands
@@ -346,10 +304,8 @@ func (q *Queries) ListKeywords(ctx context.Context, arg ListKeywordsParams) ([]L
}
const listQuestions = `-- name: ListQuestions :many
SELECT q.id, q.tenant_id, q.brand_id, q.keyword_id, q.current_version_id, q.status, q.created_at,
v.question_text, v.version_no
SELECT q.id, q.tenant_id, q.brand_id, q.keyword_id, q.question_text, q.status, q.created_at
FROM brand_questions q
LEFT JOIN brand_question_versions v ON v.id = q.current_version_id
WHERE q.brand_id = $1 AND q.tenant_id = $2 AND q.deleted_at IS NULL
AND ($3::bigint IS NULL OR q.keyword_id = $3)
ORDER BY q.created_at DESC
@@ -362,15 +318,13 @@ type ListQuestionsParams struct {
}
type ListQuestionsRow struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
KeywordID int64 `json:"keyword_id"`
CurrentVersionID pgtype.Int8 `json:"current_version_id"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
QuestionText pgtype.Text `json:"question_text"`
VersionNo pgtype.Int4 `json:"version_no"`
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
KeywordID int64 `json:"keyword_id"`
QuestionText string `json:"question_text"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([]ListQuestionsRow, error) {
@@ -387,11 +341,9 @@ func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([
&i.TenantID,
&i.BrandID,
&i.KeywordID,
&i.CurrentVersionID,
&i.QuestionText,
&i.Status,
&i.CreatedAt,
&i.QuestionText,
&i.VersionNo,
); err != nil {
return nil, err
}
@@ -584,18 +536,24 @@ func (q *Queries) UpdateKeyword(ctx context.Context, arg UpdateKeywordParams) er
return err
}
const updateQuestionCurrentVersion = `-- name: UpdateQuestionCurrentVersion :exec
UPDATE brand_questions SET current_version_id = $1, updated_at = NOW()
WHERE id = $2 AND tenant_id = $3
const updateQuestion = `-- name: UpdateQuestion :exec
UPDATE brand_questions SET question_text = $1, updated_at = NOW()
WHERE id = $2 AND brand_id = $3 AND tenant_id = $4 AND deleted_at IS NULL
`
type UpdateQuestionCurrentVersionParams struct {
VersionID pgtype.Int8 `json:"version_id"`
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
type UpdateQuestionParams struct {
QuestionText string `json:"question_text"`
ID int64 `json:"id"`
BrandID int64 `json:"brand_id"`
TenantID int64 `json:"tenant_id"`
}
func (q *Queries) UpdateQuestionCurrentVersion(ctx context.Context, arg UpdateQuestionCurrentVersionParams) error {
_, err := q.db.Exec(ctx, updateQuestionCurrentVersion, arg.VersionID, arg.ID, arg.TenantID)
func (q *Queries) UpdateQuestion(ctx context.Context, arg UpdateQuestionParams) error {
_, err := q.db.Exec(ctx, updateQuestion,
arg.QuestionText,
arg.ID,
arg.BrandID,
arg.TenantID,
)
return err
}
@@ -94,25 +94,15 @@ type BrandKeyword struct {
}
type BrandQuestion struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
KeywordID int64 `json:"keyword_id"`
CurrentVersionID pgtype.Int8 `json:"current_version_id"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
type BrandQuestionVersion struct {
ID int64 `json:"id"`
QuestionID int64 `json:"question_id"`
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
KeywordID int64 `json:"keyword_id"`
QuestionText string `json:"question_text"`
QuestionHash string `json:"question_hash"`
VersionNo int32 `json:"version_no"`
IsActive bool `json:"is_active"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
type Competitor struct {
@@ -146,6 +136,77 @@ type GenerationTask struct {
OperatorID pgtype.Int8 `json:"operator_id"`
}
type KnowledgeChunksMetum struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
KnowledgeItemID int64 `json:"knowledge_item_id"`
ChunkIndex int32 `json:"chunk_index"`
TokenCount int32 `json:"token_count"`
QdrantPointID string `json:"qdrant_point_id"`
ItemVersion int32 `json:"item_version"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type KnowledgeGroup struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
Name string `json:"name"`
ParentID pgtype.Int8 `json:"parent_id"`
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
type KnowledgeItem struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
GroupID int64 `json:"group_id"`
SourceType string `json:"source_type"`
Name string `json:"name"`
SourceUri pgtype.Text `json:"source_uri"`
StorageKey string `json:"storage_key"`
ContentText pgtype.Text `json:"content_text"`
Status string `json:"status"`
SizeBytes int64 `json:"size_bytes"`
ItemVersion int32 `json:"item_version"`
ErrorMessage pgtype.Text `json:"error_message"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
MarkdownContent pgtype.Text `json:"markdown_content"`
}
type KnowledgeParseTask struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
KnowledgeItemID int64 `json:"knowledge_item_id"`
SourceType string `json:"source_type"`
Status string `json:"status"`
ErrorMessage pgtype.Text `json:"error_message"`
StartedAt pgtype.Timestamptz `json:"started_at"`
CompletedAt pgtype.Timestamptz `json:"completed_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type MediaPlatform struct {
ID int64 `json:"id"`
PlatformID string `json:"platform_id"`
Name string `json:"name"`
Category string `json:"category"`
ShortName string `json:"short_name"`
AccentColor string `json:"accent_color"`
LoginUrl pgtype.Text `json:"login_url"`
LogoUrl pgtype.Text `json:"logo_url"`
Status string `json:"status"`
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type Plan struct {
ID int64 `json:"id"`
PlanCode string `json:"plan_code"`
@@ -157,6 +218,22 @@ type Plan struct {
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
type PlatformAccount struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"`
PlatformID string `json:"platform_id"`
PlatformUid string `json:"platform_uid"`
Nickname string `json:"nickname"`
AvatarUrl pgtype.Text `json:"avatar_url"`
Status string `json:"status"`
MetadataJson []byte `json:"metadata_json"`
LastCheckAt pgtype.Timestamptz `json:"last_check_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
type PlatformUserRole struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
@@ -166,6 +243,40 @@ type PlatformUserRole struct {
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
type PluginInstallation struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"`
InstallationKey string `json:"installation_key"`
InstallationName string `json:"installation_name"`
BrowserName pgtype.Text `json:"browser_name"`
ClientVersion pgtype.Text `json:"client_version"`
InstallationTokenHash string `json:"installation_token_hash"`
Status string `json:"status"`
LastSeenAt pgtype.Timestamptz `json:"last_seen_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
type PluginSession struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"`
PluginInstallationID pgtype.Int8 `json:"plugin_installation_id"`
ActionType string `json:"action_type"`
ResourceType pgtype.Text `json:"resource_type"`
ResourceID pgtype.Int8 `json:"resource_id"`
PlatformAccountID pgtype.Int8 `json:"platform_account_id"`
PlatformID string `json:"platform_id"`
SessionToken string `json:"session_token"`
ClientVersion pgtype.Text `json:"client_version"`
Status string `json:"status"`
ExpireAt pgtype.Timestamptz `json:"expire_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type PromptRule struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
@@ -191,6 +302,44 @@ type PromptRuleGroup struct {
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
type PromptRuleKnowledgeGroup struct {
PromptRuleID int64 `json:"prompt_rule_id"`
KnowledgeGroupID int64 `json:"knowledge_group_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type PublishBatch struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
ArticleID int64 `json:"article_id"`
InitiatorUserID int64 `json:"initiator_user_id"`
Status string `json:"status"`
PublishType string `json:"publish_type"`
CoverAssetUrl pgtype.Text `json:"cover_asset_url"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type PublishRecord struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
PublishBatchID int64 `json:"publish_batch_id"`
ArticleID int64 `json:"article_id"`
PlatformAccountID int64 `json:"platform_account_id"`
PlatformID string `json:"platform_id"`
Status string `json:"status"`
ExternalArticleID pgtype.Text `json:"external_article_id"`
ExternalArticleUrl pgtype.Text `json:"external_article_url"`
ExternalManageUrl pgtype.Text `json:"external_manage_url"`
PublishedAt pgtype.Timestamptz `json:"published_at"`
RequestPayloadJson []byte `json:"request_payload_json"`
ResponsePayloadJson []byte `json:"response_payload_json"`
ErrorMessage pgtype.Text `json:"error_message"`
RetryCount int32 `json:"retry_count"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type QuotaReservation struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
@@ -27,16 +27,13 @@ type Querier interface {
CreatePromptRule(ctx context.Context, arg CreatePromptRuleParams) (CreatePromptRuleRow, error)
CreatePromptRuleGroup(ctx context.Context, arg CreatePromptRuleGroupParams) (CreatePromptRuleGroupRow, error)
CreateQuestion(ctx context.Context, arg CreateQuestionParams) (int64, error)
CreateQuestionVersion(ctx context.Context, arg CreateQuestionVersionParams) (int64, error)
CreateQuotaReservation(ctx context.Context, arg CreateQuotaReservationParams) (int64, error)
CreateScheduleTask(ctx context.Context, arg CreateScheduleTaskParams) (CreateScheduleTaskRow, error)
DeactivateQuestionVersion(ctx context.Context, questionID int64) error
GetActivePlanForTenant(ctx context.Context, tenantID int64) (GetActivePlanForTenantRow, error)
GetArticleByID(ctx context.Context, arg GetArticleByIDParams) (GetArticleByIDRow, error)
GetArticleVersions(ctx context.Context, articleID int64) ([]GetArticleVersionsRow, error)
GetBrandByID(ctx context.Context, arg GetBrandByIDParams) (GetBrandByIDRow, error)
GetCurrentBalance(ctx context.Context, arg GetCurrentBalanceParams) (int32, error)
GetLatestQuestionVersionNo(ctx context.Context, questionID int64) (int32, error)
GetPromptRuleByID(ctx context.Context, arg GetPromptRuleByIDParams) (GetPromptRuleByIDRow, error)
GetQuotaSummary(ctx context.Context, tenantID int64) (int32, error)
GetRecentArticles(ctx context.Context, tenantID int64) ([]GetRecentArticlesRow, error)
@@ -85,7 +82,7 @@ type Querier interface {
UpdatePromptRule(ctx context.Context, arg UpdatePromptRuleParams) error
UpdatePromptRuleGroup(ctx context.Context, arg UpdatePromptRuleGroupParams) error
UpdatePromptRuleStatus(ctx context.Context, arg UpdatePromptRuleStatusParams) error
UpdateQuestionCurrentVersion(ctx context.Context, arg UpdateQuestionCurrentVersionParams) error
UpdateQuestion(ctx context.Context, arg UpdateQuestionParams) error
UpdateQuotaReservationResource(ctx context.Context, arg UpdateQuotaReservationResourceParams) error
UpdateScheduleTask(ctx context.Context, arg UpdateScheduleTaskParams) error
UpdateScheduleTaskStatus(ctx context.Context, arg UpdateScheduleTaskStatusParams) error
@@ -98,7 +98,7 @@ SELECT a.id, a.generate_status, a.publish_status, a.source_type, a.created_at,
t.template_name,
COALESCE(
NULLIF(gt.input_params_json ->> 'generation_mode', ''),
CASE WHEN a.source_type = 'custom_generation' THEN 'instant' ELSE NULL END
CASE WHEN a.source_type = 'custom_generation' THEN 'instant'::TEXT ELSE NULL::TEXT END
) AS generation_mode
FROM articles a
LEFT JOIN article_versions av ON av.id = a.current_version_id
@@ -126,7 +126,7 @@ type GetRecentArticlesRow struct {
WordCount pgtype.Int4 `json:"word_count"`
SourceLabel pgtype.Text `json:"source_label"`
TemplateName pgtype.Text `json:"template_name"`
GenerationMode pgtype.Text `json:"generation_mode"`
GenerationMode interface{} `json:"generation_mode"`
}
func (q *Queries) GetRecentArticles(ctx context.Context, tenantID int64) ([]GetRecentArticlesRow, error) {
@@ -46,36 +46,20 @@ UPDATE brand_keywords SET deleted_at = NOW(), updated_at = NOW()
WHERE brand_id = sqlc.arg(brand_id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
-- name: ListQuestions :many
SELECT q.id, q.tenant_id, q.brand_id, q.keyword_id, q.current_version_id, q.status, q.created_at,
v.question_text, v.version_no
SELECT q.id, q.tenant_id, q.brand_id, q.keyword_id, q.question_text, q.status, q.created_at
FROM brand_questions q
LEFT JOIN brand_question_versions v ON v.id = q.current_version_id
WHERE q.brand_id = sqlc.arg(brand_id) AND q.tenant_id = sqlc.arg(tenant_id) AND q.deleted_at IS NULL
AND (sqlc.narg(keyword_id)::bigint IS NULL OR q.keyword_id = sqlc.narg(keyword_id))
ORDER BY q.created_at DESC;
-- name: CreateQuestion :one
INSERT INTO brand_questions (tenant_id, brand_id, keyword_id, status)
VALUES (sqlc.arg(tenant_id), sqlc.arg(brand_id), sqlc.arg(keyword_id), 'active')
INSERT INTO brand_questions (tenant_id, brand_id, keyword_id, question_text, status)
VALUES (sqlc.arg(tenant_id), sqlc.arg(brand_id), sqlc.arg(keyword_id), sqlc.arg(question_text), 'active')
RETURNING id;
-- name: CreateQuestionVersion :one
INSERT INTO brand_question_versions (question_id, question_text, question_hash, version_no, is_active)
VALUES (sqlc.arg(question_id), sqlc.arg(question_text), sqlc.arg(question_hash), sqlc.arg(version_no), true)
RETURNING id;
-- name: UpdateQuestionCurrentVersion :exec
UPDATE brand_questions SET current_version_id = sqlc.arg(version_id), updated_at = NOW()
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
-- name: DeactivateQuestionVersion :exec
UPDATE brand_question_versions SET is_active = false
WHERE question_id = sqlc.arg(question_id) AND is_active = true;
-- name: GetLatestQuestionVersionNo :one
SELECT COALESCE(MAX(version_no), 0)::INT AS max_version
FROM brand_question_versions
WHERE question_id = sqlc.arg(question_id);
-- name: UpdateQuestion :exec
UPDATE brand_questions SET question_text = sqlc.arg(question_text), updated_at = NOW()
WHERE id = sqlc.arg(id) AND brand_id = sqlc.arg(brand_id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
-- name: SoftDeleteQuestion :exec
UPDATE brand_questions SET deleted_at = NOW(), updated_at = NOW()
@@ -16,7 +16,7 @@ SELECT a.id, a.generate_status, a.publish_status, a.source_type, a.created_at,
t.template_name,
COALESCE(
NULLIF(gt.input_params_json ->> 'generation_mode', ''),
CASE WHEN a.source_type = 'custom_generation' THEN 'instant' ELSE NULL END
CASE WHEN a.source_type = 'custom_generation' THEN 'instant'::TEXT ELSE NULL::TEXT END
) AS generation_mode
FROM articles a
LEFT JOIN article_versions av ON av.id = a.current_version_id
@@ -70,7 +70,7 @@ func (r *workspaceRepository) GetRecentArticles(ctx context.Context, tenantID in
GenerateStatus: row.GenerateStatus,
PublishStatus: row.PublishStatus,
SourceType: row.SourceType,
GenerationMode: nullableText(row.GenerationMode),
GenerationMode: nullableAnyText(row.GenerationMode),
CreatedAt: timeFromTimestamp(row.CreatedAt),
Title: nullableText(row.Title),
WordCount: intFromInt4(row.WordCount),