v2.0: 添加分类系统和标签系统,支持多类别多标签
This commit is contained in:
218
app.py
218
app.py
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user