feat(migrations): Harden task audit tracking and optimize article templates

- Added migration to harden task audit tracking by modifying audit_logs and related tables.
- Introduced operator_id to several tables for better tracking of actions.
- Updated article_templates with new prompt templates for various article types, enhancing content generation.
- Created prompt_rules and schedule_tasks tables to manage content generation rules and scheduling.
- Added foreign key constraints to articles for better data integrity.
This commit is contained in:
2026-04-02 00:31:28 +08:00
parent de30497f59
commit b31d8d0096
101 changed files with 16671 additions and 721 deletions
@@ -0,0 +1 @@
DROP TABLE IF EXISTS template_assist_tasks;
@@ -0,0 +1,19 @@
CREATE TABLE template_assist_tasks (
id VARCHAR(64) PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
template_id BIGINT NOT NULL REFERENCES article_templates(id),
status VARCHAR(20) NOT NULL DEFAULT 'queued',
request_json JSONB NOT NULL DEFAULT '{}'::jsonb,
result_json JSONB,
error_message TEXT,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_template_assist_tasks_tenant_created
ON template_assist_tasks(tenant_id, created_at DESC);
CREATE INDEX idx_template_assist_tasks_status_created
ON template_assist_tasks(status, created_at DESC);
@@ -0,0 +1,4 @@
DROP INDEX IF EXISTS idx_template_assist_tasks_tenant_type_created;
ALTER TABLE template_assist_tasks
DROP COLUMN IF EXISTS task_type;
@@ -0,0 +1,5 @@
ALTER TABLE template_assist_tasks
ADD COLUMN task_type VARCHAR(32) NOT NULL DEFAULT 'analyze';
CREATE INDEX idx_template_assist_tasks_tenant_type_created
ON template_assist_tasks(tenant_id, task_type, created_at DESC);
@@ -0,0 +1,31 @@
UPDATE article_templates
SET prompt_template = 'Write a Top {{count}} article about {{topic}}. Include brief descriptions for each item.',
card_config_json = '{}'::jsonb,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'top_x_article'
AND deleted_at IS NULL;
UPDATE article_templates
SET prompt_template = 'Write a comprehensive product review for {{product_name}} in the {{category}} category.',
card_config_json = '{}'::jsonb,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'product_review'
AND deleted_at IS NULL;
UPDATE article_templates
SET prompt_template = 'Write a {{depth}} research report about {{subject}}.',
card_config_json = '{}'::jsonb,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'research_report'
AND deleted_at IS NULL;
UPDATE article_templates
SET prompt_template = 'Expand on the search query ''{{brand}} {{keyword}}'' with a comprehensive article.',
card_config_json = '{}'::jsonb,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'brand_search_expansion'
AND deleted_at IS NULL;
@@ -0,0 +1,360 @@
UPDATE article_templates
SET prompt_template = $$ Markdown
使$$,
card_config_json = $${
"hero": {
"accent": "Top X",
"helper": "排序推荐类文章,适合快节奏种草、结构化比较和选购决策。"
},
"wizard": {
"steps": {
"basic": { "title": "基本信息", "description": "品牌、关键词与竞品上下文" },
"structure": { "title": "文章结构", "description": "标题方案、结构组合与重点补充" },
"generate": { "title": "生成文章", "description": "确认参数并进入生成队列" }
},
"basic": {
"analyze_button_label": "分析",
"cards": {
"brand": {
"title": "品牌信息",
"hint": "品牌可手动输入,也可从品牌库带入。AI 会结合品牌、官网和模板字段补齐关键词与竞品。"
},
"keywords": {
"title": "关键词",
"hint": "关键词可以手动改写,AI 分析结果会自动补充到这里,后续标题生成会以当前版本为准。"
},
"template_fields": {
"title": "模板字段",
"hint": "保留本模板特有的补充字段,用来约束选题角度与文章范围。"
},
"competitors": {
"title": "竞品列表",
"hint": "竞品可编辑、追加和收藏到品牌词库。后续标题生成会严格基于当前竞品上下文。"
}
}
},
"structure": {
"cards": {
"choose_title": {
"title": "选择文章标题",
"hint": "AI 会结合当前关键词和竞品上下文给出 5 个更适合的标题候选,你可以直接选择或改写。"
},
"outline": {
"title": "文章结构",
"hint": "保留推荐文章的默认结构,也可以按这次出稿需求删减、补充自定义段落。"
},
"preview": {
"title": "文章预览",
"hint": "右侧会根据当前标题和结构实时预览最终文章框架。"
}
}
},
"review": {
"card": {
"title": "提交前确认",
"hint": "确认当前标题、结构和品牌上下文无误后再提交生成任务。"
},
"alert_message": "后端会立即创建文章与任务记录,再由排队 worker 异步完成正文生成。"
},
"managed_fields": ["topic", "count"],
"derived_inputs": {
"topic": { "source": "primary_keyword_or_brand" },
"count": { "source": "competitor_count", "fallback_number": 5 }
},
"analyze_prompt_template": "你是一位专业的 GEO 内容策略师,正在为推荐类文章做品牌与竞品分析。\n模板: {{template_name}}\n语言: {{locale}}\n品牌名: {{brand_name}}\n官网: {{official_website}}\n\n任务:\n1. 分析品牌/话题上下文,给出 1-2 句品牌摘要。\n2. 推荐最多 5 个适合推荐类文章的搜索关键词。\n3. 推荐最多 6 个可信竞品或对比对象。\n\n规则:\n- 仅返回 JSON,不要用 Markdown 代码块包裹。\n- 使用与 locale 一致的语言。\n- 关键词应为简洁的搜索短语。\n- 竞品应去重且真实,不确定时 website 留空,不要编造 URL。",
"title_prompt_template": "你正在为一篇推荐类文章生成 5 个候选标题。\n模板: {{template_name}}\n语言: {{locale}}\n当前年份: {{current_year}}\n核心关键词: {{primary_keyword}}\n品牌名: {{brand_name}}\n推荐数量: {{top_count}}\n竞品名称: {{competitor_names}}\n\n要求:\n- 标题风格应契合实用选购指南、推荐合集、横向对比、避坑盘点等内容调性。\n- 不要机械重复模板名称或「Top X 文章」字样。\n- 每个标题应具体、可读,且角度各不相同。\n- 当 locale 为 zh-CN 时,优先使用自然的中文标题风格,如推荐、精选、怎么选、避坑、盘点等,但避免空洞标题党。",
"outline_prompt_template": "你正在为一篇推荐类文章生成实用大纲。\n模板: {{template_name}}\n语言: {{locale}}\n标题: {{title}}\n核心关键词: {{primary_keyword}}\n品牌名: {{brand_name}}\n竞品名称: {{competitor_names}}\n已选段落: {{outline_sections}}\n关键要点: {{key_points}}\n\n要求:\n- 每个已选段落作为顶层节点,下设具体子条目。\n- 网站列表段落下按竞品逐一展开。\n- 内容应具体、有对比、有结论导向,避免泛泛而谈。\n- 使用与 locale 一致的语言。",
"outline": {
"allow_custom": true,
"custom_placeholder": "补充一个额外结构,例如""",
"preview_title": "文章预览",
"preview_caption": "预览会根据已选标题和结构实时更新。",
"sections": [
{ "key": "intro", "label": "引言", "default": true },
{ "key": "site_list", "label": "网站列表", "default": true },
{ "key": "key_points", "label": "文章关键要点", "default": true },
{ "key": "what_is_keyword", "label_template": "什么是{{primary_keyword}}", "default": false },
{ "key": "features", "label": "特点", "default": false },
{ "key": "pros_cons", "label": "优缺点", "default": false },
{ "key": "audience", "label": "适合人群", "default": false },
{ "key": "pricing", "label": "价格", "default": false },
{ "key": "how_to_choose", "label": "如何选择", "default": false },
{ "key": "conclusion", "label": "结论", "default": false }
]
}
}
}$$::jsonb,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'top_x_article'
AND deleted_at IS NULL;
UPDATE article_templates
SET prompt_template = $$ Markdown
使
广
review_intro_hook 使$$,
card_config_json = $${
"hero": {
"accent": "Review",
"helper": "产品评测类文章,适合做能力拆解、优缺点判断和适配建议。"
},
"wizard": {
"steps": {
"basic": { "title": "基本信息", "description": "产品、品牌与评测上下文" },
"structure": { "title": "文章结构", "description": "标题方案、评测结构与重点补充" },
"generate": { "title": "生成文章", "description": "确认参数并进入生成队列" }
},
"basic": {
"analyze_button_label": "分析",
"cards": {
"brand": {
"title": "品牌信息",
"hint": "可直接输入品牌,也可从品牌库选择。AI 会补充适合评测文章的关键词和品牌背景。"
},
"keywords": {
"title": "关键词",
"hint": "关键词会影响标题和正文的评测切入点,可手动修改。"
},
"competitors": {
"visible": false
}
}
},
"structure": {
"cards": {
"choose_title": {
"title": "选择文章标题",
"hint": "AI 会生成 5 个更适合评测文章的标题候选,你可以挑选或覆盖。"
},
"outline": {
"title": "文章结构",
"hint": "保留评测默认结构,也可按具体产品情况增删段落。"
},
"preview": {
"title": "文章预览",
"hint": "右侧会根据标题和结构实时预览评测文章框架。"
}
},
"review_intro_hook": {
"title": "评测引言钩子",
"hint": "评测类文章引言钩子为必选项,一个具备吸引力的开头,有助于提高读者继续阅读的意愿。",
"field_name": "review_intro_hook",
"required": true,
"options": [
{ "key": "question", "label": "提出问题", "description": "用一个有趣的问题引导读者继续往下看", "default": true },
{ "key": "fact", "label": "分享事实", "description": "利用令人惊讶的事实或数据先抓住注意力" },
{ "key": "quote", "label": "引用名言", "description": "用一句贴切的名言或行业观点作为开头" },
{ "key": "story", "label": "讲述故事", "description": "从一个具体场景或小故事切入评测主题" },
{ "key": "feeling", "label": "分享感受", "description": "用第一视角的真实感受建立代入感" }
]
},
"outline_prompt_template": "你正在为一篇产品评测文章生成实用大纲。\n模板: {{template_name}}\n语言: {{locale}}\n当前年份: {{current_year}}\n标题: {{title}}\n产品名: {{product_name}}\n品类: {{category}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n评测引言钩子: {{review_intro_hook}}\n已选段落: {{outline_sections}}\n关键要点: {{key_points}}\n\n要求:\n- 每个已选段落作为顶层节点,使用与请求一致的语言。\n- 当有引言段落时,子条目应体现所选的评测引言钩子风格。\n- 聚焦具体能力拆解、真实使用判断、优缺点权衡和适合人群。\n- 大纲务实、简洁、不含推广语气。"
},
"review": {
"card": {
"title": "提交前确认",
"hint": "确认当前标题、结构与评测角度后再提交生成。"
},
"alert_message": "后端会立即创建文章与任务记录,再由排队 worker 异步完成正文生成。"
},
"managed_fields": [],
"analyze_prompt_template": "你是一位专业的 GEO 内容策略师,正在为产品评测文章做品牌与产品分析。\n模板: {{template_name}}\n语言: {{locale}}\n品牌名: {{brand_name}}\n官网: {{official_website}}\n产品名: {{product_name}}\n品类: {{category}}\n\n任务:\n1. 分析产品与品牌上下文,给出 1-2 句品牌摘要。\n2. 推荐最多 5 个适合评测文章的搜索关键词,聚焦产品能力、使用场景和购买决策。\n3. 推荐最多 6 个可信的同类产品或替代方案。\n\n规则:\n- 仅返回 JSON,不要用 Markdown 代码块包裹。\n- 使用与 locale 一致的语言。\n- 关键词侧重评测视角,如「XX 评测」「XX 值不值得买」「XX 优缺点」。\n- 竞品应去重且真实,不确定时 website 留空。",
"title_prompt_template": "你正在为一篇产品评测文章生成 5 个候选标题。\n模板: {{template_name}}\n语言: {{locale}}\n当前年份: {{current_year}}\n产品名: {{product_name}}\n品类: {{category}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n\n要求:\n- 标题应像可信的评测、测评、购买建议类内容。\n- 用具体表达代替空洞口号。\n- 当 locale 为 zh-CN 时,标题应贴合中文内容平台风格,可使用评测、实测、值不值得买、优缺点、怎么选等表达。\n- 返回 5 个角度明确不同的标题。",
"outline": {
"allow_custom": true,
"custom_placeholder": "补充一个额外结构,例如""",
"preview_title": "文章预览",
"preview_caption": "预览会根据已选标题和结构实时更新。",
"sections": [
{ "key": "intro", "label": "引言", "default": true },
{ "key": "overview", "label": "产品概览", "default": true },
{ "key": "core_features", "label": "核心卖点", "default": true },
{ "key": "real_experience", "label": "使用体验", "default": false },
{ "key": "pros_cons", "label": "优缺点", "default": false },
{ "key": "audience", "label": "适合人群", "default": false },
{ "key": "buying_advice", "label": "购买建议", "default": false },
{ "key": "conclusion", "label": "结论", "default": false }
]
}
}
}$$::jsonb,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'product_review'
AND deleted_at IS NULL;
UPDATE article_templates
SET prompt_template = $$ Markdown
使$$,
card_config_json = $${
"hero": {
"accent": "Report",
"helper": "研究报告类文章,适合趋势解读、市场分析和策略判断。"
},
"wizard": {
"steps": {
"basic": { "title": "基本信息", "description": "研究主题、品牌与背景上下文" },
"structure": { "title": "文章结构", "description": "标题方案、研究结构与重点补充" },
"generate": { "title": "生成文章", "description": "确认参数并进入生成队列" }
},
"basic": {
"analyze_button_label": "分析",
"cards": {
"brand": {
"title": "品牌信息",
"hint": "如研究对象与品牌有关,可补充品牌与官网信息帮助 AI 建立上下文。"
},
"keywords": {
"title": "关键词",
"hint": "关键词会影响报告的核心视角、结论表达和标题风格。"
},
"template_fields": {
"title": "模板字段",
"hint": "填写研究主题和深度要求,帮助 AI 控制报告范围。"
},
"competitors": {
"title": "参考对象",
"hint": "可补充相关品牌、机构或竞品,帮助报告更有参照系。"
}
}
},
"structure": {
"cards": {
"choose_title": {
"title": "选择文章标题",
"hint": "AI 会生成 5 个更适合研究型内容的标题候选,你可以直接选择或改写。"
},
"outline": {
"title": "文章结构",
"hint": "保留研究报告的默认结构,也可补充更贴近本次出稿的章节。"
},
"preview": {
"title": "文章预览",
"hint": "右侧会根据标题和结构实时预览报告框架。"
}
}
},
"review": {
"card": {
"title": "提交前确认",
"hint": "确认当前标题、结构和研究主题后再提交生成。"
},
"alert_message": "后端会立即创建文章与任务记录,再由排队 worker 异步完成正文生成。"
},
"managed_fields": [],
"analyze_prompt_template": "你是一位专业的 GEO 内容策略师,正在为研究报告做主题与背景分析。\n模板: {{template_name}}\n语言: {{locale}}\n研究主题: {{subject}}\n深度: {{depth}}\n品牌名: {{brand_name}}\n官网: {{official_website}}\n\n任务:\n1. 分析研究主题上下文,给出 1-2 句主题摘要。\n2. 推荐最多 5 个适合研究报告的搜索关键词,侧重行业术语和趋势表达。\n3. 推荐最多 6 个可作为参照的品牌、机构或竞争对手。\n\n规则:\n- 仅返回 JSON,不要用 Markdown 代码块包裹。\n- 使用与 locale 一致的语言。\n- 关键词应体现分析性视角,如「XX 市场分析」「XX 趋势」「XX 行业报告」。\n- 竞品应去重且真实,不确定时 website 留空。",
"title_prompt_template": "你正在为一篇研究报告文章生成 5 个候选标题。\n模板: {{template_name}}\n语言: {{locale}}\n当前年份: {{current_year}}\n研究主题: {{subject}}\n深度: {{depth}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n\n要求:\n- 标题应具有分析性、聚焦感和洞察力。\n- 避免模糊的企业公关式措辞。\n- 当 locale 为 zh-CN 时,可使用研究报告、趋势洞察、观察、判断、分析等表达。\n- 返回 5 个角度各异但可信的报告风格标题。",
"outline_prompt_template": "你正在为一篇研究报告文章生成实用大纲。\n模板: {{template_name}}\n语言: {{locale}}\n标题: {{title}}\n研究主题: {{subject}}\n深度: {{depth}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n已选段落: {{outline_sections}}\n关键要点: {{key_points}}\n\n要求:\n- 每个已选段落作为顶层节点,下设具体子条目。\n- 关键发现段落应按数据驱动的方式组织子条目。\n- 内容应具有结构性、分析深度和可操作性。\n- 使用与 locale 一致的语言。",
"outline": {
"allow_custom": true,
"custom_placeholder": "补充一个额外结构,例如""",
"preview_title": "文章预览",
"preview_caption": "预览会根据已选标题和结构实时更新。",
"sections": [
{ "key": "summary", "label": "摘要", "default": true },
{ "key": "background", "label": "研究背景", "default": true },
{ "key": "findings", "label": "关键发现", "default": true },
{ "key": "market_view", "label": "市场判断", "default": false },
{ "key": "risks", "label": "风险与挑战", "default": false },
{ "key": "action_items", "label": "行动建议", "default": false },
{ "key": "conclusion", "label": "结论", "default": false }
]
}
}
}$$::jsonb,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'research_report'
AND deleted_at IS NULL;
UPDATE article_templates
SET prompt_template = $$ Markdown
广$$,
card_config_json = $${
"hero": {
"accent": "Brand SEO",
"helper": "品牌词搜索扩写,适合品牌介绍、搜索意图承接、问答和比较型内容。"
},
"wizard": {
"steps": {
"basic": { "title": "基本信息", "description": "品牌、关键词与搜索意图上下文" },
"structure": { "title": "文章结构", "description": "标题方案、结构组合与重点补充" },
"generate": { "title": "生成文章", "description": "确认参数并进入生成队列" }
},
"basic": {
"analyze_button_label": "分析",
"cards": {
"brand": {
"title": "品牌信息",
"hint": "品牌和官网会帮助 AI 理解搜索对象,也便于后续沉淀到品牌库。"
},
"keywords": {
"title": "关键词",
"hint": "这里的关键词会直接参与标题与正文的搜索意图组织。"
},
"template_fields": {
"title": "模板字段",
"hint": "模板字段会补充品牌词与搜索意图的具体边界。"
},
"competitors": {
"title": "参考对象",
"hint": "可补充竞品或替代方案,帮助品牌词扩写内容更完整。"
}
}
},
"structure": {
"cards": {
"choose_title": {
"title": "选择文章标题",
"hint": "AI 会生成 5 个更适合品牌词搜索扩写的标题候选。"
},
"outline": {
"title": "文章结构",
"hint": "保留默认搜索意图结构,也可以按本次需求增删段落。"
},
"preview": {
"title": "文章预览",
"hint": "右侧会根据标题和结构实时预览内容框架。"
}
}
},
"review": {
"card": {
"title": "提交前确认",
"hint": "确认当前标题、结构与搜索上下文后再提交生成。"
},
"alert_message": "后端会立即创建文章与任务记录,再由排队 worker 异步完成正文生成。"
},
"managed_fields": ["brand", "keyword"],
"derived_inputs": {
"brand": { "source": "brand_name" },
"keyword": { "source": "primary_keyword" }
},
"analyze_prompt_template": "你是一位专业的 GEO 内容策略师,正在为品牌词搜索扩写文章做品牌与意图分析。\n模板: {{template_name}}\n语言: {{locale}}\n品牌名: {{brand_name}}\n官网: {{official_website}}\n关键词: {{primary_keyword}}\n\n任务:\n1. 分析品牌与搜索意图上下文,给出 1-2 句品牌摘要。\n2. 推荐最多 5 个品牌词搜索相关的关键词,侧重搜索意图和用户疑问。\n3. 推荐最多 6 个可信的竞品或替代方案。\n\n规则:\n- 仅返回 JSON,不要用 Markdown 代码块包裹。\n- 使用与 locale 一致的语言。\n- 关键词应贴合搜索意图,如「XX 是什么」「XX 怎么样」「XX 对比 YY」。\n- 竞品应去重且真实,不确定时 website 留空。",
"title_prompt_template": "你正在为一篇品牌词搜索扩写文章生成 5 个候选标题。\n模板: {{template_name}}\n语言: {{locale}}\n当前年份: {{current_year}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n官网: {{official_website}}\n\n要求:\n- 标题应适用于品牌搜索意图、对比、问答和解释型内容。\n- 避免空洞口号。\n- 当 locale 为 zh-CN 时,可使用是什么、怎么样、怎么选、对比、值得买吗等表达。\n- 返回 5 个不同的、贴合搜索意图的标题选项。",
"outline_prompt_template": "你正在为一篇品牌词搜索扩写文章生成实用大纲。\n模板: {{template_name}}\n语言: {{locale}}\n标题: {{title}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n官网: {{official_website}}\n已选段落: {{outline_sections}}\n关键要点: {{key_points}}\n竞品名称: {{competitor_names}}\n\n要求:\n- 每个已选段落作为顶层节点,下设具体子条目。\n- 品牌概览段落应覆盖品牌定位、核心产品和差异化。\n- 对比段落应按竞品逐一展开对比维度。\n- 内容应回答搜索意图,语气客观有信息量。\n- 使用与 locale 一致的语言。",
"outline": {
"allow_custom": true,
"custom_placeholder": "补充一个额外结构,例如""",
"preview_title": "文章预览",
"preview_caption": "预览会根据已选标题和结构实时更新。",
"sections": [
{ "key": "intro", "label": "引言", "default": true },
{ "key": "search_intent", "label": "搜索意图", "default": true },
{ "key": "brand_overview", "label_template": "什么是{{brand_name}}", "default": true },
{ "key": "core_points", "label": "核心要点", "default": false },
{ "key": "comparison", "label": "对比与替代方案", "default": false },
{ "key": "faq", "label": "常见问题", "default": false },
{ "key": "conclusion", "label": "结论", "default": false }
]
}
}
}$$::jsonb,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'brand_search_expansion'
AND deleted_at IS NULL;
@@ -0,0 +1,39 @@
DROP INDEX IF EXISTS uk_template_tenant_key_version_active;
DROP INDEX IF EXISTS uk_template_platform_key_version_active;
CREATE UNIQUE INDEX uk_template_scope_key_version_active
ON article_templates(scope, tenant_id, template_key, version_no)
WHERE deleted_at IS NULL;
DROP INDEX IF EXISTS idx_audit_tenant_operator_created;
DROP INDEX IF EXISTS idx_quota_reservations_tenant_operator_created;
DROP INDEX IF EXISTS idx_quota_ledgers_tenant_operator_created;
DROP INDEX IF EXISTS idx_task_record_tenant_operator_created;
DROP INDEX IF EXISTS idx_template_assist_tasks_tenant_operator_created;
DROP INDEX IF EXISTS idx_generation_task_tenant_operator_created;
ALTER TABLE audit_logs
DROP COLUMN IF EXISTS error_message,
DROP COLUMN IF EXISTS request_id,
DROP COLUMN IF EXISTS resource_id,
DROP COLUMN IF EXISTS resource_type;
UPDATE quota_reservations
SET resource_id = 0
WHERE resource_id IS NULL;
ALTER TABLE quota_reservations
DROP COLUMN IF EXISTS operator_id,
ALTER COLUMN resource_id SET NOT NULL;
ALTER TABLE tenant_quota_ledgers
DROP COLUMN IF EXISTS operator_id;
ALTER TABLE task_records
DROP COLUMN IF EXISTS operator_id;
ALTER TABLE template_assist_tasks
DROP COLUMN IF EXISTS operator_id;
ALTER TABLE generation_tasks
DROP COLUMN IF EXISTS operator_id;
@@ -0,0 +1,49 @@
ALTER TABLE generation_tasks
ADD COLUMN operator_id BIGINT REFERENCES users(id);
ALTER TABLE template_assist_tasks
ADD COLUMN operator_id BIGINT REFERENCES users(id);
ALTER TABLE task_records
ADD COLUMN operator_id BIGINT REFERENCES users(id);
ALTER TABLE tenant_quota_ledgers
ADD COLUMN operator_id BIGINT REFERENCES users(id);
ALTER TABLE quota_reservations
ADD COLUMN operator_id BIGINT REFERENCES users(id),
ALTER COLUMN resource_id DROP NOT NULL;
ALTER TABLE audit_logs
ADD COLUMN resource_type VARCHAR(50),
ADD COLUMN resource_id BIGINT,
ADD COLUMN request_id VARCHAR(64),
ADD COLUMN error_message TEXT;
CREATE INDEX idx_generation_task_tenant_operator_created
ON generation_tasks(tenant_id, operator_id, created_at DESC);
CREATE INDEX idx_template_assist_tasks_tenant_operator_created
ON template_assist_tasks(tenant_id, operator_id, created_at DESC);
CREATE INDEX idx_task_record_tenant_operator_created
ON task_records(tenant_id, operator_id, created_at DESC);
CREATE INDEX idx_quota_ledgers_tenant_operator_created
ON tenant_quota_ledgers(tenant_id, operator_id, created_at DESC);
CREATE INDEX idx_quota_reservations_tenant_operator_created
ON quota_reservations(tenant_id, operator_id, created_at DESC);
CREATE INDEX idx_audit_tenant_operator_created
ON audit_logs(tenant_id, operator_id, created_at DESC);
DROP INDEX IF EXISTS uk_template_scope_key_version_active;
CREATE UNIQUE INDEX uk_template_platform_key_version_active
ON article_templates(scope, template_key, version_no)
WHERE deleted_at IS NULL AND tenant_id IS NULL;
CREATE UNIQUE INDEX uk_template_tenant_key_version_active
ON article_templates(scope, tenant_id, template_key, version_no)
WHERE deleted_at IS NULL AND tenant_id IS NOT NULL;
@@ -0,0 +1,2 @@
ALTER TABLE articles
DROP COLUMN IF EXISTS wizard_state_json;
@@ -0,0 +1,2 @@
ALTER TABLE articles
ADD COLUMN wizard_state_json JSONB;
@@ -0,0 +1,81 @@
UPDATE article_templates
SET prompt_template = $$ Markdown
使
广
review_intro_hook 使$$,
card_config_json = $${
"hero": {
"accent": "Review",
"helper": "产品评测类文章,适合做能力拆解、优缺点判断和适配建议。"
},
"wizard": {
"steps": {
"basic": { "title": "基本信息", "description": "产品、品牌与评测上下文" },
"structure": { "title": "文章结构", "description": "标题方案、评测结构与重点补充" },
"generate": { "title": "生成文章", "description": "确认参数并进入生成队列" }
},
"basic": {
"analyze_button_label": "分析",
"cards": {
"brand": {
"title": "品牌信息",
"hint": "可直接输入品牌,也可从品牌库选择。AI 会补充适合评测文章的关键词和品牌背景。"
},
"keywords": {
"title": "关键词",
"hint": "关键词会影响标题和正文的评测切入点,可手动修改。"
},
"competitors": {
"visible": false
}
}
},
"structure": {
"cards": {
"choose_title": {
"title": "选择文章标题",
"hint": "AI 会生成 5 个更适合评测文章的标题候选,你可以挑选或覆盖。"
},
"outline": {
"title": "文章结构",
"hint": "保留评测默认结构,也可按具体产品情况增删段落。"
},
"preview": {
"title": "文章预览",
"hint": "右侧会根据标题和结构实时预览评测文章框架。"
}
},
"outline_prompt_template": "你正在为一篇产品评测文章生成实用大纲。\n模板: {{template_name}}\n语言: {{locale}}\n当前年份: {{current_year}}\n标题: {{title}}\n产品名: {{product_name}}\n品类: {{category}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n评测引言钩子: {{review_intro_hook}}\n已选段落: {{outline_sections}}\n关键要点: {{key_points}}\n\n要求:\n- 每个已选段落作为顶层节点,使用与请求一致的语言。\n- 当有引言段落时,子条目应体现所选的评测引言钩子风格。\n- 聚焦具体能力拆解、真实使用判断、优缺点权衡和适合人群。\n- 大纲务实、简洁、不含推广语气。"
},
"review": {
"card": {
"title": "提交前确认",
"hint": "确认当前标题、结构与评测角度后再提交生成。"
},
"alert_message": "后端会立即创建文章与任务记录,再由排队 worker 异步完成正文生成。"
},
"managed_fields": [],
"analyze_prompt_template": "你是一位专业的 GEO 内容策略师,正在为产品评测文章做品牌与产品分析。\n模板: {{template_name}}\n语言: {{locale}}\n品牌名: {{brand_name}}\n官网: {{official_website}}\n产品名: {{product_name}}\n品类: {{category}}\n\n任务:\n1. 分析产品与品牌上下文,给出 1-2 句品牌摘要。\n2. 推荐最多 5 个适合评测文章的搜索关键词,聚焦产品能力、使用场景和购买决策。\n3. 推荐最多 6 个可信的同类产品或替代方案。\n\n规则:\n- 仅返回 JSON,不要用 Markdown 代码块包裹。\n- 使用与 locale 一致的语言。\n- 关键词侧重评测视角,如「XX 评测」「XX 值不值得买」「XX 优缺点」。\n- 竞品应去重且真实,不确定时 website 留空。",
"title_prompt_template": "你正在为一篇产品评测文章生成 5 个候选标题。\n模板: {{template_name}}\n语言: {{locale}}\n当前年份: {{current_year}}\n产品名: {{product_name}}\n品类: {{category}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n\n要求:\n- 标题应像可信的评测、测评、购买建议类内容。\n- 用具体表达代替空洞口号。\n- 当 locale 为 zh-CN 时,标题应贴合中文内容平台风格,可使用评测、实测、值不值得买、优缺点、怎么选等表达。\n- 返回 5 个角度明确不同的标题。",
"outline": {
"allow_custom": true,
"custom_placeholder": "补充一个额外结构,例如""",
"preview_title": "文章预览",
"preview_caption": "预览会根据已选标题和结构实时更新。",
"sections": [
{ "key": "intro", "label": "引言", "default": true },
{ "key": "overview", "label": "产品概览", "default": true },
{ "key": "core_features", "label": "核心卖点", "default": true },
{ "key": "real_experience", "label": "使用体验", "default": false },
{ "key": "pros_cons", "label": "优缺点", "default": false },
{ "key": "audience", "label": "适合人群", "default": false },
{ "key": "buying_advice", "label": "购买建议", "default": false },
{ "key": "conclusion", "label": "结论", "default": false }
]
}
}
}$$::jsonb,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'product_review'
AND deleted_at IS NULL;
@@ -0,0 +1,94 @@
UPDATE article_templates
SET prompt_template = $$ Markdown
使
广
review_intro_hook 使$$,
card_config_json = $${
"hero": {
"accent": "Review",
"helper": "产品评测类文章,适合做能力拆解、优缺点判断和适配建议。"
},
"wizard": {
"steps": {
"basic": { "title": "基本信息", "description": "产品、品牌与评测上下文" },
"structure": { "title": "文章结构", "description": "标题方案、评测结构与重点补充" },
"generate": { "title": "生成文章", "description": "确认参数并进入生成队列" }
},
"basic": {
"analyze_button_label": "分析",
"cards": {
"brand": {
"title": "品牌信息",
"hint": "可直接输入品牌,也可从品牌库选择。AI 会补充适合评测文章的关键词和品牌背景。"
},
"keywords": {
"title": "关键词",
"hint": "关键词会影响标题和正文的评测切入点,可手动修改。"
},
"competitors": {
"visible": false
}
}
},
"structure": {
"cards": {
"choose_title": {
"title": "选择文章标题",
"hint": "AI 会生成 5 个更适合评测文章的标题候选,你可以挑选或覆盖。"
},
"outline": {
"title": "文章结构",
"hint": "保留评测默认结构,也可按具体产品情况增删段落。"
},
"preview": {
"title": "文章预览",
"hint": "右侧会根据标题和结构实时预览评测文章框架。"
}
},
"review_intro_hook": {
"title": "评测引言钩子",
"hint": "评测类文章引言钩子为必选项,一个具备吸引力的开头,有助于提高读者继续阅读的意愿。",
"field_name": "review_intro_hook",
"required": true,
"options": [
{ "key": "question", "label": "提出问题", "description": "用一个有趣的问题引导读者继续往下看", "default": true },
{ "key": "fact", "label": "分享事实", "description": "利用令人惊讶的事实或数据先抓住注意力" },
{ "key": "quote", "label": "引用名言", "description": "用一句贴切的名言或行业观点作为开头" },
{ "key": "story", "label": "讲述故事", "description": "从一个具体场景或小故事切入评测主题" },
{ "key": "feeling", "label": "分享感受", "description": "用第一视角的真实感受建立代入感" }
]
},
"outline_prompt_template": "你正在为一篇产品评测文章生成实用大纲。\n模板: {{template_name}}\n语言: {{locale}}\n当前年份: {{current_year}}\n标题: {{title}}\n产品名: {{product_name}}\n品类: {{category}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n评测引言钩子: {{review_intro_hook}}\n已选段落: {{outline_sections}}\n关键要点: {{key_points}}\n\n要求:\n- 每个已选段落作为顶层节点,使用与请求一致的语言。\n- 当有引言段落时,子条目应体现所选的评测引言钩子风格。\n- 聚焦具体能力拆解、真实使用判断、优缺点权衡和适合人群。\n- 大纲务实、简洁、不含推广语气。"
},
"review": {
"card": {
"title": "提交前确认",
"hint": "确认当前标题、结构与评测角度后再提交生成。"
},
"alert_message": "后端会立即创建文章与任务记录,再由排队 worker 异步完成正文生成。"
},
"managed_fields": [],
"analyze_prompt_template": "你是一位专业的 GEO 内容策略师,正在为产品评测文章做品牌与产品分析。\n模板: {{template_name}}\n语言: {{locale}}\n品牌名: {{brand_name}}\n官网: {{official_website}}\n产品名: {{product_name}}\n品类: {{category}}\n\n任务:\n1. 分析产品与品牌上下文,给出 1-2 句品牌摘要。\n2. 推荐最多 5 个适合评测文章的搜索关键词,聚焦产品能力、使用场景和购买决策。\n3. 推荐最多 6 个可信的同类产品或替代方案。\n\n规则:\n- 仅返回 JSON,不要用 Markdown 代码块包裹。\n- 使用与 locale 一致的语言。\n- 关键词侧重评测视角,如「XX 评测」「XX 值不值得买」「XX 优缺点」。\n- 竞品应去重且真实,不确定时 website 留空。",
"title_prompt_template": "你正在为一篇产品评测文章生成 5 个候选标题。\n模板: {{template_name}}\n语言: {{locale}}\n当前年份: {{current_year}}\n产品名: {{product_name}}\n品类: {{category}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n\n要求:\n- 标题应像可信的评测、测评、购买建议类内容。\n- 用具体表达代替空洞口号。\n- 当 locale 为 zh-CN 时,标题应贴合中文内容平台风格,可使用评测、实测、值不值得买、优缺点、怎么选等表达。\n- 返回 5 个角度明确不同的标题。",
"outline": {
"allow_custom": true,
"custom_placeholder": "补充一个额外结构,例如""",
"preview_title": "文章预览",
"preview_caption": "预览会根据已选标题和结构实时更新。",
"sections": [
{ "key": "intro", "label": "引言", "default": true },
{ "key": "overview", "label": "产品概览", "default": true },
{ "key": "core_features", "label": "核心卖点", "default": true },
{ "key": "real_experience", "label": "使用体验", "default": false },
{ "key": "pros_cons", "label": "优缺点", "default": false },
{ "key": "audience", "label": "适合人群", "default": false },
{ "key": "buying_advice", "label": "购买建议", "default": false },
{ "key": "conclusion", "label": "结论", "default": false }
]
}
}
}$$::jsonb,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'product_review'
AND deleted_at IS NULL;
@@ -0,0 +1,11 @@
UPDATE article_templates
SET card_config_json = jsonb_set(
card_config_json,
'{wizard,basic,cards,template_fields}',
'{"title":"模板字段","hint":"填写产品名、品类等评测上下文,后续标题和正文会围绕这些变量展开。"}'::jsonb,
true
),
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'product_review'
AND deleted_at IS NULL;
@@ -0,0 +1,6 @@
UPDATE article_templates
SET card_config_json = card_config_json #- '{wizard,basic,cards,template_fields}',
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'product_review'
AND deleted_at IS NULL;
@@ -0,0 +1,36 @@
UPDATE article_templates
SET prompt_template = $$ Markdown
使$$,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'top_x_article'
AND deleted_at IS NULL;
UPDATE article_templates
SET prompt_template = $$ Markdown
使
广
review_intro_hook 使$$,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'product_review'
AND deleted_at IS NULL;
UPDATE article_templates
SET prompt_template = $$ Markdown
使$$,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'research_report'
AND deleted_at IS NULL;
UPDATE article_templates
SET prompt_template = $$ Markdown
广$$,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'brand_search_expansion'
AND deleted_at IS NULL;
@@ -0,0 +1,65 @@
UPDATE article_templates
SET prompt_template = $${{topic}} Markdown
:
1. {{count}}
2.
:
-
- competitors
-
- $$,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'top_x_article'
AND deleted_at IS NULL;
UPDATE article_templates
SET prompt_template = $${{product_name}}{{category}} Markdown
:
1.
2. 使
:
- review_intro_hook
- 使
-
-
- 稿$$,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'product_review'
AND deleted_at IS NULL;
UPDATE article_templates
SET prompt_template = $${{subject}} Markdown
:
1.
2.
:
- depth = overview depth = detailed
-
-
- $$,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'research_report'
AND deleted_at IS NULL;
UPDATE article_templates
SET prompt_template = $${{brand}} {{keyword}} Markdown
:
1.
2. /
:
-
-
-
-
- $$,
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'brand_search_expansion'
AND deleted_at IS NULL;
@@ -0,0 +1,39 @@
UPDATE article_templates
SET card_config_json = jsonb_set(
jsonb_set(
jsonb_set(
jsonb_set(
jsonb_set(
card_config_json #- '{wizard,derived_inputs}' #- '{wizard,outline,custom_max_length}',
'{wizard,steps,basic,description}',
to_jsonb('研究主题、品牌与背景上下文'::text),
true
),
'{wizard,basic,cards,template_fields}',
'{"title": "模板字段", "hint": "填写研究主题和深度要求,帮助 AI 控制报告范围。"}'::jsonb,
true
),
'{wizard,basic,cards,competitors}',
'{"title": "参考对象", "hint": "可补充相关品牌、机构或竞品,帮助报告更有参照系。"}'::jsonb,
true
),
'{wizard,outline,custom_placeholder}',
to_jsonb('补充一个额外结构,例如"行动建议"'::text),
true
),
'{wizard,outline,sections}',
'[
{ "key": "summary", "label": "摘要", "default": true },
{ "key": "background", "label": "研究背景", "default": true },
{ "key": "findings", "label": "关键发现", "default": true },
{ "key": "market_view", "label": "市场判断", "default": false },
{ "key": "risks", "label": "风险与挑战", "default": false },
{ "key": "action_items", "label": "行动建议", "default": false },
{ "key": "conclusion", "label": "结论", "default": false }
]'::jsonb,
true
),
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'research_report'
AND deleted_at IS NULL;
@@ -0,0 +1,47 @@
UPDATE article_templates
SET card_config_json = jsonb_set(
jsonb_set(
jsonb_set(
jsonb_set(
jsonb_set(
jsonb_set(
card_config_json #- '{wizard,basic,cards,template_fields}',
'{wizard,steps,basic,description}',
to_jsonb('品牌与关键词背景信息'::text),
true
),
'{wizard,basic,cards,competitors}',
'{"visible": false}'::jsonb,
true
),
'{wizard,derived_inputs}',
'{"subject": {"source": "primary_keyword_or_brand"}}'::jsonb,
true
),
'{wizard,outline,custom_placeholder}',
to_jsonb('自定义输入,不超过10个字'::text),
true
),
'{wizard,outline,custom_max_length}',
'10'::jsonb,
true
),
'{wizard,outline,sections}',
'[
{ "key": "summary", "label": "摘要", "default": true },
{ "key": "intro", "label": "引言", "default": true },
{ "key": "methodology", "label": "研究方法与数据来源", "default": true },
{ "key": "segment_overview", "label": "细分市场概述", "default": true },
{ "key": "positioning_analysis", "label": "产品定位与优缺点", "default": true },
{ "key": "user_research", "label": "用户调研", "default": false },
{ "key": "target_audience", "label": "目标人群", "default": false },
{ "key": "competitor_comparison", "label": "竞品对比", "default": false },
{ "key": "research_conclusion", "label": "研究结论", "default": false },
{ "key": "analysis_recommendations", "label": "分析建议", "default": false }
]'::jsonb,
true
),
updated_at = NOW()
WHERE scope = 'platform'
AND template_key = 'research_report'
AND deleted_at IS NULL;
@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS prompt_rules;
DROP TABLE IF EXISTS prompt_rule_groups;
@@ -0,0 +1,28 @@
CREATE TABLE prompt_rule_groups (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
name VARCHAR(100) NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_prompt_rule_group_tenant ON prompt_rule_groups(tenant_id, sort_order);
CREATE TABLE prompt_rules (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
group_id BIGINT REFERENCES prompt_rule_groups(id),
name VARCHAR(100) NOT NULL,
prompt_content TEXT NOT NULL,
scene VARCHAR(50),
default_tone VARCHAR(30),
default_word_count INT,
status VARCHAR(20) NOT NULL DEFAULT 'enabled',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_prompt_rule_tenant ON prompt_rules(tenant_id, created_at DESC);
CREATE INDEX idx_prompt_rule_tenant_status ON prompt_rules(tenant_id, status) WHERE deleted_at IS NULL;
CREATE INDEX idx_prompt_rule_group ON prompt_rules(group_id) WHERE deleted_at IS NULL;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS schedule_tasks;
@@ -0,0 +1,18 @@
CREATE TABLE schedule_tasks (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
prompt_rule_id BIGINT NOT NULL REFERENCES prompt_rules(id),
brand_id BIGINT REFERENCES brands(id),
name VARCHAR(100) NOT NULL,
cron_expr VARCHAR(100) NOT NULL,
target_platform VARCHAR(50),
start_at TIMESTAMPTZ,
end_at TIMESTAMPTZ,
next_run_at TIMESTAMPTZ,
status VARCHAR(20) NOT NULL DEFAULT 'enabled',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_schedule_task_tenant ON schedule_tasks(tenant_id, created_at DESC);
CREATE INDEX idx_schedule_task_next_run ON schedule_tasks(status, next_run_at) WHERE deleted_at IS NULL;
@@ -0,0 +1 @@
ALTER TABLE articles DROP CONSTRAINT IF EXISTS fk_articles_prompt_rule;
@@ -0,0 +1 @@
ALTER TABLE articles ADD CONSTRAINT fk_articles_prompt_rule FOREIGN KEY (prompt_rule_id) REFERENCES prompt_rules(id);