feat: 平台配置表扩展配图/字数字段,微信公众号正文插入配图,合规检查从DB读规则

- PlatformConfig模型新增requires_image、image_count_min/max、image_width/height、min_words/max_words
- schemas.py同步PlatformConfigBase/Create/Update/Response新字段
- initial_data.py为三大平台填充初始值(微信需配图、字数800-1500等)
- database.py添加新列ALTER TABLE迁移
- platforms.html重写编辑弹窗(正确字段名+配图/字数设置)
- writer.py微信公众号文章正文h1后插入<img>占位
- compliance_checker.py接受platform_config参数,从DB读取规则替代硬编码
- compliance_optimizer.py启动时加载DB平台配置传入checker
This commit is contained in:
Yuzhiran Dev
2026-05-17 23:08:43 +08:00
parent 5674114599
commit 41b0f694ee
9 changed files with 174 additions and 51 deletions
+7
View File
@@ -53,6 +53,13 @@ def init_db():
for table, col, typ in [
("users", "org_id", "VARCHAR DEFAULT 'default'"),
("topics", "org_id", "VARCHAR DEFAULT 'default'"),
("platform_configs", "requires_image", "BOOLEAN DEFAULT FALSE"),
("platform_configs", "image_count_min", "INTEGER DEFAULT 0"),
("platform_configs", "image_count_max", "INTEGER DEFAULT 0"),
("platform_configs", "image_width", "INTEGER DEFAULT 0"),
("platform_configs", "image_height", "INTEGER DEFAULT 0"),
("platform_configs", "min_words", "INTEGER DEFAULT 0"),
("platform_configs", "max_words", "INTEGER DEFAULT 0"),
]:
try:
conn.execute(text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {col} {typ}"))
+33 -11
View File
@@ -81,11 +81,18 @@ def import_initial_data():
"icon": "🔍",
"default_format": "长文深度分析,1500-3000字,有数据支撑",
"compliance_rules": {
"max_length": 50000,
"requires_authentication": False,
"sensitive_words": ["敏感词示例1", "敏感词示例2"]
"max_title_len": 100,
"allowed_tags": ["科技", "生活", "职场", "教育", "可持续", "AI", "远程工作", "个人成长"],
"forbidden_patterns": ["加微信", "私聊", "付费咨询", "点击领取"]
},
"is_active": True
"is_active": True,
"requires_image": False,
"image_count_min": 0,
"image_count_max": 0,
"image_width": 0,
"image_height": 0,
"min_words": 1500,
"max_words": 3000
},
{
"platform": "wechat",
@@ -93,10 +100,18 @@ def import_initial_data():
"icon": "💚",
"default_format": "公众号图文,800-1500字,亲切口语化",
"compliance_rules": {
"max_length": 20000,
"requires_authentication": True
"max_title_len": 32,
"allowed_tags": ["科技", "生活", "职场", "教育", "可持续", "AI", "远程工作", "个人成长"],
"forbidden_patterns": ["诱导分享", "朋友圈", "转发群"]
},
"is_active": True
"is_active": True,
"requires_image": True,
"image_count_min": 1,
"image_count_max": 3,
"image_width": 1080,
"image_height": 1080,
"min_words": 800,
"max_words": 1500
},
{
"platform": "xiaohongshu",
@@ -104,11 +119,18 @@ def import_initial_data():
"icon": "📕",
"default_format": "图文笔记,300-800字,emoji+标签",
"compliance_rules": {
"max_length": 1000,
"requires_tags": True,
"max_tags": 10
"max_title_len": 50,
"allowed_tags": ["生活方式", "可持续", "AI", "个人成长", "极简", "环保"],
"forbidden_patterns": ["私信", "加群", "导流"]
},
"is_active": True
"is_active": True,
"requires_image": True,
"image_count_min": 3,
"image_count_max": 6,
"image_width": 1080,
"image_height": 1440,
"min_words": 300,
"max_words": 800
}
]
for p in platforms:
+14
View File
@@ -361,6 +361,13 @@ class PlatformConfig(Base):
compliance_rules = Column(JSON, default=dict)
default_format = Column(Text, nullable=True)
is_active = Column(Boolean, default=True)
requires_image = Column(Boolean, default=False)
image_count_min = Column(Integer, default=0)
image_count_max = Column(Integer, default=0)
image_width = Column(Integer, default=0)
image_height = Column(Integer, default=0)
min_words = Column(Integer, default=0)
max_words = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
@@ -375,6 +382,13 @@ class PlatformConfig(Base):
"compliance_rules": self.compliance_rules or {},
"default_format": self.default_format,
"is_active": self.is_active,
"requires_image": self.requires_image,
"image_count_min": self.image_count_min,
"image_count_max": self.image_count_max,
"image_width": self.image_width,
"image_height": self.image_height,
"min_words": self.min_words,
"max_words": self.max_words,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
+14
View File
@@ -263,6 +263,13 @@ class PlatformConfigBase(BaseModel):
compliance_rules: Dict[str, Any] = {}
default_format: Optional[str] = None
is_active: bool = True
requires_image: bool = False
image_count_min: int = 0
image_count_max: int = 0
image_width: int = 0
image_height: int = 0
min_words: int = 0
max_words: int = 0
class PlatformConfigCreate(PlatformConfigBase):
@@ -278,6 +285,13 @@ class PlatformConfigUpdate(BaseModel):
compliance_rules: Optional[Dict[str, Any]] = None
default_format: Optional[str] = None
is_active: Optional[bool] = None
requires_image: Optional[bool] = None
image_count_min: Optional[int] = None
image_count_max: Optional[int] = None
image_width: Optional[int] = None
image_height: Optional[int] = None
min_words: Optional[int] = None
max_words: Optional[int] = None
class PlatformConfigResponse(PlatformConfigBase):
+50 -24
View File
@@ -78,52 +78,74 @@
<span class="platform-meta-label">平台标识</span>
<span class="platform-meta-value">{{ p.platform }}</span>
</div>
<div v-if="p.platform_name" class="platform-meta-item">
<div class="platform-meta-item">
<span class="platform-meta-label">平台名称</span>
<span class="platform-meta-value">{{ p.platform_name }}</span>
<span class="platform-meta-value">{{ p.name }}</span>
</div>
<div v-if="p.min_words || p.max_words" class="platform-meta-item">
<span class="platform-meta-label">字数限制</span>
<span class="platform-meta-value">{{ p.min_words || 0 }} - {{ p.max_words || '不限' }}</span>
</div>
<div class="platform-meta-item">
<span class="platform-meta-label">需要配图</span>
<span class="platform-meta-value">{{ p.requires_image ? '是' : '否' }}</span>
</div>
</div>
<div v-if="p.format_rules && Object.keys(p.format_rules).length > 0" class="rules-section">
<div class="rules-title"><el-icon style="vertical-align:-2px;"><IconTopic /></el-icon> 格式规则</div>
<div v-for="(rule, key) in p.format_rules" :key="key" class="rule-item">
<div v-if="p.format_template && Object.keys(p.format_template).length > 0" class="rules-section">
<div class="rules-title"><el-icon style="vertical-align:-2px;"><IconTopic /></el-icon> 格式模板</div>
<div v-for="(rule, key) in p.format_template" :key="key" class="rule-item">
<strong>{{ key }}:</strong> {{ typeof rule === 'object' ? JSON.stringify(rule) : rule }}
</div>
</div>
<div v-if="p.compliance_rules && p.compliance_rules.length > 0" class="rules-section">
<div v-if="p.compliance_rules && Object.keys(p.compliance_rules).length > 0" class="rules-section">
<div class="rules-title"><el-icon style="vertical-align:-2px;"><IconWarning /></el-icon> 合规规则</div>
<div v-for="(rule, idx) in p.compliance_rules" :key="idx" class="rule-item">{{ rule }}</div>
<div v-for="(val, key) in p.compliance_rules" :key="key" class="rule-item">
<strong>{{ key }}:</strong> {{ typeof val === 'object' ? JSON.stringify(val) : val }}
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<el-dialog v-model="showEditDialog" :title="isEditing ? '编辑平台' : '新增平台'" width="600px">
<el-form :model="platformForm" label-width="100px">
<el-dialog v-model="showEditDialog" :title="isEditing ? '编辑平台' : '新增平台'" width="650px">
<el-form :model="platformForm" label-width="120px">
<el-form-item label="平台标识" :required="!isEditing">
<el-input v-model="platformForm.platform" :disabled="isEditing" placeholder="如: zhihu, wechat, xiaohongshu"></el-input>
</el-form-item>
<el-form-item label="平台名称">
<el-input v-model="platformForm.platform_name" placeholder="如: 知乎"></el-input>
<el-input v-model="platformForm.name" placeholder="如: 知乎"></el-input>
</el-form-item>
<el-form-item label="启用状态">
<el-switch v-model="platformForm.is_active"></el-switch>
</el-form-item>
<el-form-item label="标题模板">
<el-input v-model="platformForm.title_template" placeholder="如: 【{{tag}}】{{title}}"></el-input>
<el-form-item label="默认格式">
<el-input v-model="platformForm.default_format" type="textarea" :rows="2" placeholder="如: 公众号图文,800-1500字,亲切口语化"></el-input>
</el-form-item>
<el-form-item label="正文模板">
<el-input v-model="platformForm.body_template" type="textarea" :rows="4" placeholder="如: {{content}}"></el-input>
<el-divider content-position="left">字数与配图</el-divider>
<el-form-item label="最少字数">
<el-input-number v-model="platformForm.min_words" :min="0" :max="100000"></el-input-number>
</el-form-item>
<el-form-item label="字数限制">
<el-input-number v-model="platformForm.min_words" :min="0" placeholder="最少"></el-input-number>
<span style="margin: 0 8px;">-</span>
<el-input-number v-model="platformForm.max_words" :min="0" placeholder="最多"></el-input-number>
<el-form-item label="最多字数">
<el-input-number v-model="platformForm.max_words" :min="0" :max="100000"></el-input-number>
</el-form-item>
<el-form-item label="需要配图">
<el-switch v-model="platformForm.requires_image"></el-switch>
</el-form-item>
<template v-if="platformForm.requires_image">
<el-form-item label="最少图片数">
<el-input-number v-model="platformForm.image_count_min" :min="0" :max="20"></el-input-number>
</el-form-item>
<el-form-item label="最多图片数">
<el-input-number v-model="platformForm.image_count_max" :min="0" :max="20"></el-input-number>
</el-form-item>
<el-form-item label="图片宽度(px)">
<el-input-number v-model="platformForm.image_width" :min="0" :max="4096" :step="1"></el-input-number>
</el-form-item>
<el-form-item label="图片高度(px)">
<el-input-number v-model="platformForm.image_height" :min="0" :max="4096" :step="1"></el-input-number>
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click="showEditDialog = false">取消</el-button>
@@ -147,7 +169,7 @@ const PlatformsApp = {
showEditDialog: false,
isEditing: false,
saving: false,
platformForm: { platform: '', platform_name: '', is_active: true, title_template: '', body_template: '', min_words: null, max_words: null }
platformForm: { platform: '', name: '', is_active: true, default_format: '', requires_image: false, image_count_min: 0, image_count_max: 0, image_width: 0, image_height: 0, min_words: 0, max_words: 0 }
}
},
methods: {
@@ -188,12 +210,16 @@ const PlatformsApp = {
this.isEditing = true;
this.platformForm = {
platform: p.platform,
platform_name: p.platform_name || '',
name: p.name || '',
is_active: p.is_active,
title_template: p.title_template || '',
body_template: p.body_template || '',
min_words: p.min_words,
max_words: p.max_words
default_format: p.default_format || '',
requires_image: p.requires_image || false,
image_count_min: p.image_count_min || 0,
image_count_max: p.image_count_max || 0,
image_width: p.image_width || 0,
image_height: p.image_height || 0,
min_words: p.min_words || 0,
max_words: p.max_words || 0
};
this.showEditDialog = true;
},