v2.0: 添加分类系统和标签系统,支持多类别多标签

This commit is contained in:
2026-05-23 00:51:42 +08:00
parent d3dd11ca8b
commit 87286986ea
4 changed files with 770 additions and 85 deletions

218
app.py
View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
文章编写系统 - Article Writer System
功能主题管理、素材收集、Prompt编辑、大模型生成文章
功能主题管理、素材收集、Prompt编辑、大模型生成文章、分类与标签
"""
import os
@@ -82,6 +82,31 @@ def init_db():
created_at TEXT DEFAULT (datetime('now','localtime')),
FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS categories (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
sort_order INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now','localtime'))
);
CREATE TABLE IF NOT EXISTS tags (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
created_at TEXT DEFAULT (datetime('now','localtime'))
);
CREATE TABLE IF NOT EXISTS topic_categories (
topic_id TEXT NOT NULL,
category_id TEXT NOT NULL,
PRIMARY KEY (topic_id, category_id),
FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS topic_tags (
topic_id TEXT NOT NULL,
tag_id TEXT NOT NULL,
PRIMARY KEY (topic_id, tag_id),
FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
''')
# 初始化默认配置
conn.execute('INSERT OR IGNORE INTO llm_config (id, url, api_key, model) VALUES (1, ?, ?, ?)',
@@ -104,6 +129,22 @@ def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def _topic_with_tags(conn, topic_row):
"""给 topic row 附上分类和标签信息"""
t = dict(topic_row)
cats = conn.execute(
'SELECT c.* FROM categories c JOIN topic_categories tc ON c.id=tc.category_id WHERE tc.topic_id=? ORDER BY c.sort_order',
(t['id'],)
).fetchall()
t['categories'] = [dict(c) for c in cats]
tags = conn.execute(
'SELECT tg.* FROM tags tg JOIN topic_tags tt ON tg.id=tt.tag_id WHERE tt.topic_id=? ORDER BY tg.name',
(t['id'],)
).fetchall()
t['tags'] = [dict(tg) for tg in tags]
return t
# ==================== 页面路由 ====================
@app.route('/')
@@ -113,8 +154,14 @@ def index():
'SELECT t.*, (SELECT COUNT(*) FROM materials WHERE topic_id=t.id) as mat_count '
'FROM topics t ORDER BY updated_at DESC'
).fetchall()
categories = conn.execute('SELECT * FROM categories ORDER BY sort_order, name').fetchall()
tags = conn.execute('SELECT * FROM tags ORDER BY name').fetchall()
# 附上分类和标签
topic_list = []
for t in topics:
topic_list.append(_topic_with_tags(conn, t))
conn.close()
return render_template('index.html', topics=topics)
return render_template('index.html', topics=topic_list, categories=categories, tags=tags)
@app.route('/topic/<topic_id>')
@@ -124,6 +171,7 @@ def topic_detail(topic_id):
if not topic:
conn.close()
return redirect(url_for('index'))
topic = _topic_with_tags(conn, topic)
materials = conn.execute(
'SELECT * FROM materials WHERE topic_id=? ORDER BY sort_order, created_at',
(topic_id,)
@@ -132,8 +180,11 @@ def topic_detail(topic_id):
'SELECT * FROM articles WHERE topic_id=? ORDER BY created_at DESC',
(topic_id,)
).fetchall()
categories = conn.execute('SELECT * FROM categories ORDER BY sort_order, name').fetchall()
tags = conn.execute('SELECT * FROM tags ORDER BY name').fetchall()
conn.close()
return render_template('topic.html', topic=topic, materials=materials, articles=articles)
return render_template('topic.html', topic=topic, materials=materials, articles=articles,
categories=categories, tags=tags)
# ==================== 主题API ====================
@@ -151,10 +202,29 @@ def create_topic():
'INSERT INTO topics (id, name, description, prompt, created_at, updated_at) VALUES (?,?,?,?,?,?)',
(tid, name, data.get('description', ''), data.get('prompt', ''), now, now)
)
# 关联分类
for cat_id in data.get('category_ids', []):
conn.execute('INSERT OR IGNORE INTO topic_categories (topic_id, category_id) VALUES (?,?)', (tid, cat_id))
# 关联标签如果传了已有tag id
for tag_id in data.get('tag_ids', []):
conn.execute('INSERT OR IGNORE INTO topic_tags (topic_id, tag_id) VALUES (?,?)', (tid, tag_id))
# 处理新标签(按名称创建)
for tag_name in data.get('new_tags', []):
tag_name = tag_name.strip()
if not tag_name:
continue
existing = conn.execute('SELECT id FROM tags WHERE name=?', (tag_name,)).fetchone()
if existing:
conn.execute('INSERT OR IGNORE INTO topic_tags (topic_id, tag_id) VALUES (?,?)', (tid, existing['id']))
else:
tag_id = str(uuid.uuid4())[:8]
conn.execute('INSERT INTO tags (id, name, created_at) VALUES (?,?,?)', (tag_id, tag_name, now))
conn.execute('INSERT OR IGNORE INTO topic_tags (topic_id, tag_id) VALUES (?,?)', (tid, tag_id))
conn.commit()
topic = conn.execute('SELECT * FROM topics WHERE id=?', (tid,)).fetchone()
result = _topic_with_tags(conn, topic)
conn.close()
return jsonify(dict(topic))
return jsonify(result)
@app.route('/api/topics/<topic_id>', methods=['PUT'])
@@ -174,9 +244,37 @@ def update_topic(topic_id):
values.append(topic_id)
conn.execute(f'UPDATE topics SET {",".join(fields)} WHERE id=?', values)
conn.commit()
# 更新分类关联
if 'category_ids' in data:
conn.execute('DELETE FROM topic_categories WHERE topic_id=?', (topic_id,))
for cat_id in data['category_ids']:
conn.execute('INSERT OR IGNORE INTO topic_categories (topic_id, category_id) VALUES (?,?)', (topic_id, cat_id))
# 更新标签关联
if 'tag_ids' in data or 'new_tags' in data:
conn.execute('DELETE FROM topic_tags WHERE topic_id=?', (topic_id,))
for tag_id in data.get('tag_ids', []):
conn.execute('INSERT OR IGNORE INTO topic_tags (topic_id, tag_id) VALUES (?,?)', (topic_id, tag_id))
for tag_name in data.get('new_tags', []):
tag_name = tag_name.strip()
if not tag_name:
continue
existing = conn.execute('SELECT id FROM tags WHERE name=?', (tag_name,)).fetchone()
if existing:
conn.execute('INSERT OR IGNORE INTO topic_tags (topic_id, tag_id) VALUES (?,?)', (topic_id, existing['id']))
else:
tag_id = str(uuid.uuid4())[:8]
conn.execute('INSERT INTO tags (id, name, created_at) VALUES (?,?,?)', (tag_id, tag_name, now))
conn.execute('INSERT OR IGNORE INTO topic_tags (topic_id, tag_id) VALUES (?,?)', (topic_id, tag_id))
# 清理无引用的孤立标签
conn.execute('DELETE FROM tags WHERE id NOT IN (SELECT tag_id FROM topic_tags)')
conn.commit()
topic = conn.execute('SELECT * FROM topics WHERE id=?', (topic_id,)).fetchone()
result = _topic_with_tags(conn, topic)
conn.close()
return jsonify(dict(topic))
return jsonify(result)
@app.route('/api/topics/<topic_id>', methods=['DELETE'])
@@ -190,6 +288,112 @@ def delete_topic(topic_id):
if os.path.exists(fp):
os.remove(fp)
conn.execute('DELETE FROM topics WHERE id=?', (topic_id,))
# 清理孤立标签
conn.execute('DELETE FROM tags WHERE id NOT IN (SELECT tag_id FROM topic_tags)')
conn.commit()
conn.close()
return jsonify({'ok': True})
# ==================== 分类API ====================
@app.route('/api/categories', methods=['GET'])
def list_categories():
conn = get_db()
rows = conn.execute(
'SELECT c.*, (SELECT COUNT(*) FROM topic_categories WHERE category_id=c.id) as topic_count '
'FROM categories c ORDER BY c.sort_order, c.name'
).fetchall()
conn.close()
return jsonify([dict(r) for r in rows])
@app.route('/api/categories', methods=['POST'])
def create_category():
data = request.get_json(force=True)
name = data.get('name', '').strip()
if not name:
return jsonify({'error': '分类名不能为空'}), 400
conn = get_db()
existing = conn.execute('SELECT * FROM categories WHERE name=?', (name,)).fetchone()
if existing:
conn.close()
return jsonify({'error': '分类已存在'}), 400
cid = str(uuid.uuid4())[:8]
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
conn.execute('INSERT INTO categories (id, name, sort_order, created_at) VALUES (?,?,?,?)',
(cid, name, data.get('sort_order', 0), now))
conn.commit()
cat = conn.execute('SELECT * FROM categories WHERE id=?', (cid,)).fetchone()
conn.close()
return jsonify(dict(cat))
@app.route('/api/categories/<category_id>', methods=['PUT'])
def update_category(category_id):
data = request.get_json(force=True)
conn = get_db()
fields = []
values = []
for key in ['name', 'sort_order']:
if key in data:
fields.append(f'{key}=?')
values.append(data[key])
if fields:
values.append(category_id)
conn.execute(f'UPDATE categories SET {",".join(fields)} WHERE id=?', values)
conn.commit()
cat = conn.execute('SELECT * FROM categories WHERE id=?', (category_id,)).fetchone()
conn.close()
return jsonify(dict(cat)) if cat else jsonify({'error': '未找到'}), 404
@app.route('/api/categories/<category_id>', methods=['DELETE'])
def delete_category(category_id):
conn = get_db()
conn.execute('DELETE FROM categories WHERE id=?', (category_id,))
conn.commit()
conn.close()
return jsonify({'ok': True})
# ==================== 标签API ====================
@app.route('/api/tags', methods=['GET'])
def list_tags():
conn = get_db()
rows = conn.execute(
'SELECT tg.*, (SELECT COUNT(*) FROM topic_tags WHERE tag_id=tg.id) as topic_count '
'FROM tags tg ORDER BY tg.name'
).fetchall()
conn.close()
return jsonify([dict(r) for r in rows])
@app.route('/api/tags', methods=['POST'])
def create_tag():
data = request.get_json(force=True)
name = data.get('name', '').strip()
if not name:
return jsonify({'error': '标签名不能为空'}), 400
conn = get_db()
existing = conn.execute('SELECT * FROM tags WHERE name=?', (name,)).fetchone()
if existing:
conn.close()
return jsonify(dict(existing)) # 已存在直接返回
tid = str(uuid.uuid4())[:8]
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
conn.execute('INSERT INTO tags (id, name, created_at) VALUES (?,?,?)', (tid, name, now))
conn.commit()
tag = conn.execute('SELECT * FROM tags WHERE id=?', (tid,)).fetchone()
conn.close()
return jsonify(dict(tag))
@app.route('/api/tags/<tag_id>', methods=['DELETE'])
def delete_tag(tag_id):
conn = get_db()
conn.execute('DELETE FROM tags WHERE id=?', (tag_id,))
conn.commit()
conn.close()
return jsonify({'ok': True})
@@ -304,8 +508,8 @@ def update_llm_config():
conn.execute('''
INSERT INTO llm_config (id, url, api_key, model, updated_at) VALUES (1, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET url=?, api_key=?, model=?, updated_at=?
''', (1, url, api_key, model, now, url, api_key, model, now))
ON CONFLICT(id) DO UPDATE SET url=excluded.url, api_key=excluded.api_key, model=excluded.model, updated_at=excluded.updated_at
''', (url, api_key, model, now))
conn.commit()
conn.close()
return jsonify({'ok': True})