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:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user