This commit is contained in:
“shengyudong”
2026-01-06 14:18:39 +08:00
commit 5a384b694e
10345 changed files with 2050918 additions and 0 deletions

1293
release/30/article_routes.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,377 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
文章自动分配作者脚本
监控数据库中status为pending_review且author_id=0的文章自动分配作者
"""
import os
import sys
import time
import json
import logging
import requests
from typing import Dict, List
import traceback
# 添加项目根目录到Python路径
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from database_config import get_db_manager
from log_config import setup_logger
# 配置日志记录器,支持按日期切割和控制台输出
logger = setup_logger(
name='assign_authors_to_articles',
log_file='logs/assign_authors_to_articles.log',
error_log_file='logs/assign_authors_to_articles_error.log',
level=logging.INFO,
console_output=True
)
# 配置常量
BASE_URL = "http://127.0.0.1:8216"
SLEEP_INTERVAL = 5 # 监控间隔(秒),可配置
class AssignAuthorsToArticles:
def __init__(self):
# API配置
self.base_url = BASE_URL
# 认证信息
self.jwt_token = None
# 使用统一的数据库管理器
self.db_manager = get_db_manager()
# 登录配置 - 使用雇员账号
self.login_credentials = {
'username': '13621242430',
'password': 'admin123'
}
# 禁用代理
self.proxies = {
'http': None,
'https': None
}
logger.info("AssignAuthorsToArticles 初始化完成")
def log_to_database(self, level: str, message: str, details: str = None):
"""记录日志到数据库ai_logs表"""
try:
with self.db_manager.get_cursor() as cursor:
# 映射日志级别到数据库状态
status_map = {
'INFO': 'success',
'WARNING': 'warning',
'ERROR': 'error'
}
status = status_map.get(level, 'success')
sql = """
INSERT INTO ai_logs (user_id, action, description, status, error_message, created_at)
VALUES (%s, %s, %s, %s, %s, NOW())
"""
cursor.execute(sql, (None, 'assign_authors', message, status, details))
logger.info(f"日志已记录到数据库: {level} - {message}")
except Exception as e:
logger.error(f"记录日志到数据库失败: {e}")
def login_and_get_jwt_token(self) -> bool:
"""登录获取JWT token"""
try:
login_url = f"{self.base_url}/api/auth/login"
login_data = {
"username": self.login_credentials['username'],
"password": self.login_credentials['password']
}
logger.info(f"尝试登录: {login_data['username']}")
self.log_to_database('INFO', f"尝试登录用户: {login_data['username']}")
response = requests.post(
login_url,
json=login_data,
headers={'Content-Type': 'application/json'},
proxies=self.proxies
)
if response.status_code == 200:
result = response.json()
if result.get('code') == 200:
self.jwt_token = result['data']['token']
logger.info("JWT token获取成功")
self.log_to_database('INFO', "JWT token获取成功")
return True
else:
error_msg = f"登录失败: {result.get('message', '未知错误')}"
logger.error(error_msg)
self.log_to_database('ERROR', error_msg, json.dumps(result))
return False
else:
error_msg = f"登录请求失败: {response.status_code}"
logger.error(error_msg)
self.log_to_database('ERROR', error_msg, response.text)
return False
except Exception as e:
error_msg = f"登录异常: {e}"
logger.error(error_msg)
self.log_to_database('ERROR', error_msg, traceback.format_exc())
return False
def get_pending_articles(self) -> List[Dict]:
"""获取待分配作者的文章enterprise_id=1, status=pending_review, author_id=0"""
try:
with self.db_manager.get_cursor() as cursor:
sql = """
SELECT id, title, batch_id, enterprise_id, product_id, status,
author_id, created_at, updated_at
FROM ai_articles
WHERE enterprise_id = 1
AND status = 'pending_review'
AND author_id = 0
ORDER BY created_at ASC
LIMIT 1000
"""
cursor.execute(sql)
results = cursor.fetchall()
if results:
logger.info(f"查询到 {len(results)} 篇待分配作者的文章")
for result in results:
logger.info(f"待分配文章 - ID: {result['id']}, 标题: {result['title']}, 状态: {result['status']}")
else:
logger.info("未查询到待分配作者的文章")
return results
except Exception as e:
error_msg = f"查询待分配文章异常: {e}"
logger.error(error_msg)
self.log_to_database('ERROR', error_msg, traceback.format_exc())
return []
def get_active_authors(self) -> List[Dict]:
"""获取活跃作者enterprise_id=1, status=active, phone!=空, created_user_id>0"""
try:
with self.db_manager.get_cursor() as cursor:
sql = """
SELECT id, author_name, phone, app_id, app_token,
department_id, department_name, status, created_at
FROM ai_authors
WHERE enterprise_id = 1
AND status = 'active'
AND phone IS NOT NULL AND phone != ''
AND created_user_id > 0
ORDER BY created_at ASC
LIMIT 1000
"""
cursor.execute(sql)
results = cursor.fetchall()
if results:
logger.info(f"查询到 {len(results)} 个活跃作者")
for result in results:
logger.info(f"活跃作者 - ID: {result['id']}, 姓名: {result['author_name']}, 手机: {result['phone']}")
else:
logger.info("未查询到活跃作者")
return results
except Exception as e:
error_msg = f"查询活跃作者异常: {e}"
logger.error(error_msg)
self.log_to_database('ERROR', error_msg, traceback.format_exc())
return []
def assign_authors_to_articles_batch(self, article_ids: List[int], author_ids: List[int]) -> bool:
"""调用批量分配接口:/api/articles/batch-published-account-cycle"""
try:
if not article_ids or not author_ids:
logger.warning("文章ID或作者ID列表为空跳过分配")
return False
# 确保有JWT token
if not self.jwt_token:
logger.warning("JWT token缺失尝试重新登录")
if not self.login_and_get_jwt_token():
logger.error("重新登录失败")
return False
assign_url = f"{self.base_url}/api/articles/batch-published-account-cycle"
headers = {
'Authorization': f'Bearer {self.jwt_token}',
'Content-Type': 'application/json'
}
data = {
'article_ids': article_ids,
'author_ids': author_ids
}
logger.info(f"开始批量分配作者 - 文章数: {len(article_ids)}, 作者数: {len(author_ids)}")
logger.info(f"文章ID: {article_ids}")
logger.info(f"作者ID: {author_ids}")
self.log_to_database('INFO', f"开始批量分配作者",
f"文章数: {len(article_ids)}, 作者数: {len(author_ids)}")
response = requests.post(
assign_url,
json=data,
headers=headers,
timeout=60,
proxies=self.proxies
)
logger.info(f"批量分配响应状态码: {response.status_code}")
if response.status_code == 200:
result = response.json()
logger.info(f"批量分配响应: {result}")
if result.get('code') == 200:
data = result.get('data', {})
success_msg = f"批量分配成功 - 成功: {data.get('published_count')}, 失败: {data.get('failed_count')}"
logger.info(success_msg)
self.log_to_database('INFO', success_msg, json.dumps(result, ensure_ascii=False))
return True
else:
error_msg = f"批量分配失败: {result.get('message', '未知错误')}"
logger.error(error_msg)
self.log_to_database('ERROR', error_msg, json.dumps(result, ensure_ascii=False))
return False
elif response.status_code == 401:
# Token过期重新登录后重试
logger.warning("JWT token过期(401),尝试重新登录")
if self.login_and_get_jwt_token():
logger.info("重新登录成功,重试批量分配")
headers['Authorization'] = f'Bearer {self.jwt_token}'
retry_response = requests.post(
assign_url,
json=data,
headers=headers,
timeout=60,
proxies=self.proxies
)
if retry_response.status_code == 200:
retry_result = retry_response.json()
if retry_result.get('code') == 200:
data = retry_result.get('data', {})
success_msg = f"重试批量分配成功 - 成功: {data.get('published_count')}, 失败: {data.get('failed_count')}"
logger.info(success_msg)
self.log_to_database('INFO', success_msg, json.dumps(retry_result, ensure_ascii=False))
return True
error_msg = f"重试批量分配失败: {retry_response.status_code}"
logger.error(error_msg)
self.log_to_database('ERROR', error_msg, retry_response.text)
return False
else:
logger.error("重新登录失败")
return False
else:
error_msg = f"批量分配请求失败: {response.status_code}"
logger.error(error_msg)
self.log_to_database('ERROR', error_msg, response.text)
return False
except Exception as e:
error_msg = f"批量分配异常: {e}"
logger.error(error_msg)
self.log_to_database('ERROR', error_msg, traceback.format_exc())
return False
def run(self):
"""主运行逻辑"""
try:
logger.info("="*60)
logger.info("开始执行文章自动分配作者任务")
logger.info("="*60)
self.log_to_database('INFO', "开始执行文章自动分配作者任务")
# 1. 获取待分配作者的文章
logger.info("步骤1: 获取待分配作者的文章")
articles = self.get_pending_articles()
if not articles:
logger.info("没有待分配作者的文章,任务结束")
self.log_to_database('INFO', "没有待分配作者的文章,任务结束")
return True
# 2. 获取活跃作者
logger.info("步骤2: 获取活跃作者")
authors = self.get_active_authors()
if not authors:
logger.error("没有活跃作者,无法分配")
self.log_to_database('ERROR', "没有活跃作者,无法分配")
return False
# 3. 批量分配作者
logger.info("步骤3: 批量分配作者")
article_ids = [article['id'] for article in articles]
author_ids = [author['id'] for author in authors]
logger.info(f"准备分配 {len(article_ids)} 篇文章给 {len(author_ids)} 个作者")
self.log_to_database('INFO', f"准备分配文章",
f"文章数: {len(article_ids)}, 作者数: {len(author_ids)}")
success = self.assign_authors_to_articles_batch(article_ids, author_ids)
if success:
logger.info("="*60)
logger.info("文章自动分配作者任务完成")
logger.info("="*60)
self.log_to_database('INFO', "文章自动分配作者任务完成")
return True
else:
logger.error("文章自动分配作者任务失败")
self.log_to_database('ERROR', "文章自动分配作者任务失败")
return False
except Exception as e:
error_msg = f"运行任务异常: {e}"
logger.error(error_msg)
self.log_to_database('ERROR', error_msg, traceback.format_exc())
return False
def main():
"""主函数 - 持续监控模式"""
generator = AssignAuthorsToArticles()
logger.info("="*60)
logger.info(f"启动文章自动分配作者监控服务")
logger.info(f"监控间隔: {SLEEP_INTERVAL}")
logger.info("="*60)
# 首次登录获取JWT token
logger.info("首次登录获取JWT token")
if not generator.login_and_get_jwt_token():
logger.error("首次登录失败,程序退出")
generator.log_to_database('ERROR', "首次登录失败,程序退出")
return
while True:
try:
# 执行文章自动分配作者任务
generator.run()
# 等待下一次检查
logger.info(f"等待 {SLEEP_INTERVAL} 秒后进行下一次检查...")
time.sleep(SLEEP_INTERVAL)
except KeyboardInterrupt:
logger.info("="*60)
logger.info("收到中断信号,停止监控服务")
logger.info("="*60)
generator.log_to_database('INFO', '监控服务手动停止', 'KeyboardInterrupt')
break
except Exception as e:
logger.error(f"程序运行异常: {e}")
generator.log_to_database('ERROR', f'程序运行异常: {e}', traceback.format_exc())
logger.info(f"等待 {SLEEP_INTERVAL} 秒后重试...")
time.sleep(SLEEP_INTERVAL)
if __name__ == "__main__":
main()

693
release/30/author_routes.py Normal file
View File

@@ -0,0 +1,693 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""作者管理接口"""
from flask import Blueprint, request, jsonify, g
import logging
from datetime import datetime
from auth_utils import require_auth, require_role, AuthUtils
from database_config import get_db_manager
# 导入统一日志配置
from log_config import setup_article_server_logger
# 使用统一的日志记录器
logger = logging.getLogger('article_server')
# 创建蓝图
author_bp = Blueprint('author', __name__, url_prefix='/api/authors')
@author_bp.route('', methods=['GET'])
@require_auth
def get_authors():
"""获取作者列表(分页)"""
try:
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 10))
offset = (page - 1) * size
# 搜索参数
search = request.args.get('search', '').strip()
department = request.args.get('department', '').strip()
status = request.args.get('status', '').strip()
channel = request.args.get('channel', '').strip()
db_manager = get_db_manager()
# 构建查询条件
where_conditions = []
params = []
if search:
where_conditions.append("(author_name LIKE %s OR hospital LIKE %s OR title LIKE %s OR id = %s)")
search_param = f"%{search}%"
params.extend([search_param, search_param, search_param, search])
if department:
where_conditions.append("department = %s")
params.append(department)
if status:
where_conditions.append("status = %s")
params.append(status)
if channel:
where_conditions.append("channel = %s")
params.append(channel)
where_clause = ""
if where_conditions:
where_clause = "WHERE " + " AND ".join(where_conditions)
# 查询科室总数(用于分页)
dept_count_sql = f"SELECT COUNT(DISTINCT department_id) as total FROM ai_authors {where_clause}"
dept_count_result = db_manager.execute_query(dept_count_sql, params)
total_departments = dept_count_result[0]['total']
# 先获取分页的科室ID列表
dept_sql = f"""SELECT DISTINCT department_id, department_name
FROM ai_authors {where_clause}
ORDER BY department_id ASC
LIMIT %s OFFSET %s"""
dept_params = params + [size, offset]
departments = db_manager.execute_query(dept_sql, dept_params)
# 如果没有科室数据,直接返回空结果
if not departments:
department_list = []
else:
# 获取这些科室的所有作者
dept_ids = [str(dept['department_id']) for dept in departments]
dept_id_placeholders = ','.join(['%s'] * len(dept_ids))
# 构建包含科室ID过滤的查询条件
author_where_conditions = where_conditions.copy()
author_where_conditions.append(f"department_id IN ({dept_id_placeholders})")
author_where_clause = "WHERE " + " AND ".join(author_where_conditions)
author_sql = f"""SELECT id as author_id, author_name,
department_id, department_name, toutiao_cookie, toutiao_images_cookie
FROM ai_authors {author_where_clause}
ORDER BY department_id ASC, id ASC"""
author_params = params + dept_ids
authors = db_manager.execute_query(author_sql, author_params)
# 按科室分组
department_dict = {}
for dept in departments:
dept_id = dept['department_id']
department_dict[dept_id] = {
'department_id': dept_id,
'department_name': dept['department_name'],
'department_list': []
}
# 将作者分配到对应科室
for author in authors:
dept_id = author['department_id']
if dept_id in department_dict:
department_dict[dept_id]['department_list'].append(author)
# 转换为数组格式,保持科室顺序
department_list = [department_dict[dept['department_id']] for dept in departments]
return jsonify({
'code': 200,
'data': {
'list': department_list,
'page': page,
'size': size,
'total': total_departments
},
'message': '获取成功',
'timestamp': int(datetime.now().timestamp() * 1000)
})
except Exception as e:
logger.error(f"获取作者列表失败: {e}")
return jsonify({'code': 500, 'message': f'获取失败: {str(e)}'}), 500
@author_bp.route('/list', methods=['GET'])
@require_auth
def get_list_authors():
"""获取作者列表(分页,基于用户关联关系)"""
try:
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 10))
offset = (page - 1) * size
# 搜索参数
search = request.args.get('search', '').strip()
department = request.args.get('department', '').strip()
status = request.args.get('status', '').strip()
channel = request.args.get('channel', '').strip()
db_manager = get_db_manager()
# 获取当前登录用户
current_user = AuthUtils.get_current_user()
logger.info(f"[get_authors] 获取到的用户信息: {current_user}")
if not current_user:
logger.error("[get_authors] 未获取到用户信息")
return jsonify({'code': 401, 'message': '未登录或用户信息缺失'}), 401
user_id = current_user.get('user_id')
logger.info(f"[get_authors] 提取的user_id: {user_id}")
if not user_id:
logger.error(f"[get_authors] user_id缺失current_user内容: {current_user}")
return jsonify({'code': 401, 'message': '用户ID缺失'}), 401
# Step 1: 先查出当前用户关联的作者ID
user_authors_sql = "SELECT author_id FROM ai_user_authors WHERE user_id = %s"
logger.info(f"[get_authors] 执行查询: {user_authors_sql}, 参数: {[user_id]}")
user_authors = db_manager.execute_query(user_authors_sql, [user_id])
logger.info(f"[get_authors] 查询结果: {user_authors}")
author_ids = [str(row['author_id']) for row in user_authors]
logger.info(f"[get_authors] 提取的author_ids: {author_ids}")
if not author_ids:
# 用户没有绑定任何作者
logger.info(f"[get_authors] 用户{user_id}没有绑定任何作者")
return jsonify({
'code': 200,
'data': {
'list': [],
'page': page,
'size': size,
'total': 0
},
'message': '获取成功',
'timestamp': int(datetime.now().timestamp() * 1000)
})
# Step 2: 构建查询条件(限定 author_id 范围)
where_conditions = ["id IN (" + ",".join(["%s"] * len(author_ids)) + ")"]
params = author_ids
if search:
where_conditions.append("(author_name LIKE %s OR hospital LIKE %s OR title LIKE %s)")
search_param = f"%{search}%"
params.extend([search_param, search_param, search_param])
if department:
where_conditions.append("department = %s")
params.append(department)
if status:
where_conditions.append("status = %s")
params.append(status)
if channel:
where_conditions.append("channel = %s")
params.append(channel)
where_clause = "WHERE " + " AND ".join(where_conditions)
# Step 3: 查询科室总数(用于分页)
dept_count_sql = f"SELECT COUNT(DISTINCT department_id) as total FROM ai_authors {where_clause}"
dept_count_result = db_manager.execute_query(dept_count_sql, params)
total_departments = dept_count_result[0]['total'] if dept_count_result else 0
# Step 4: 获取分页的科室ID列表
dept_sql = f"""SELECT DISTINCT department_id, department_name
FROM ai_authors {where_clause}
ORDER BY department_id ASC
LIMIT %s OFFSET %s"""
dept_params = params + [size, offset]
departments = db_manager.execute_query(dept_sql, dept_params)
if not departments:
department_list = []
else:
# Step 5: 获取这些科室下的所有作者
dept_ids = [str(dept['department_id']) for dept in departments]
dept_id_placeholders = ','.join(['%s'] * len(dept_ids))
author_where_conditions = where_conditions.copy()
author_where_conditions.append(f"department_id IN ({dept_id_placeholders})")
author_where_clause = "WHERE " + " AND ".join(author_where_conditions)
author_sql = f"""SELECT id as author_id, author_name,
department_id, department_name
FROM ai_authors {author_where_clause}
ORDER BY department_id ASC, id ASC"""
author_params = params + dept_ids
authors = db_manager.execute_query(author_sql, author_params)
# Step 6: 按科室分组
department_dict = {}
for dept in departments:
dept_id = dept['department_id']
department_dict[dept_id] = {
'department_id': dept_id,
'department_name': dept['department_name'],
'department_list': []
}
for author in authors:
dept_id = author['department_id']
if dept_id in department_dict:
department_dict[dept_id]['department_list'].append(author)
department_list = [department_dict[dept['department_id']] for dept in departments]
return jsonify({
'code': 200,
'data': {
'list': department_list,
'page': page,
'size': size,
'total': total_departments
},
'message': '获取成功',
'timestamp': int(datetime.now().timestamp() * 1000)
})
except Exception as e:
import traceback
logger.error(f"获取作者列表失败: {e}")
logger.error(f"错误堆栈: {traceback.format_exc()}")
return jsonify({'code': 500, 'message': f'获取失败: {str(e)}'}), 500
@author_bp.route('/<int:author_id>', methods=['GET'])
@require_auth
def get_author(author_id):
"""获取单个作者详情"""
try:
db_manager = get_db_manager()
sql = """SELECT id, author_name, department, department_name, title, hospital, specialty, toutiao_images_cookie,
introduction, avatar_url, status, toutiao_cookie, created_at, updated_at
FROM ai_authors WHERE id = %s"""
author = db_manager.execute_query(sql, (author_id,))
if not author:
return jsonify({'code': 404, 'message': '作者不存在'}), 404
return jsonify({
'code': 200,
'data': author[0],
'message': '获取成功',
'timestamp': int(datetime.now().timestamp() * 1000)
})
except Exception as e:
logger.error(f"获取作者详情失败: {e}")
return jsonify({'code': 500, 'message': f'获取失败: {str(e)}'}), 500
@author_bp.route('', methods=['POST'])
@require_auth
@require_role(['admin', 'editor'])
def create_author():
"""创建新作者"""
try:
data = request.get_json()
logger.info(f"[创建作者] 接收到的数据: {data}")
# 必填字段验证
author_name = data.get('author_name', '').strip()
app_id = data.get('app_id', '').strip()
app_token = data.get('app_token', '').strip()
department_name = data.get('department_name', '').strip()
toutiao_cookie = data.get('toutiao_cookie', '').strip()
toutiao_images_cookie = data.get('toutiao_images_cookie', '').strip()
channel = data.get('channel', 1)
logger.info(f"[创建作者] 字段值 - author_name: '{author_name}', app_id: '{app_id}', app_token: '{app_token}', department_name: '{department_name}'")
if not author_name:
logger.warning(f"[创建作者] 作者姓名为空")
return jsonify({'code': 400, 'message': '作者姓名不能为空'}), 400
if not app_id:
logger.warning(f"[创建作者] app_id为空")
return jsonify({'code': 400, 'message': 'app_id不能为空'}), 400
if not app_token:
logger.warning(f"[创建作者] app_token为空")
return jsonify({'code': 400, 'message': 'app_token不能为空'}), 400
if not department_name:
logger.warning(f"[创建作者] department_name为空")
return jsonify({'code': 400, 'message': '科室不能为空'}), 400
if channel == 2 and not toutiao_cookie:
logger.warning(f"[创建作者] toutiao_cookie为空")
return jsonify({'code': 400, 'message': 'toutiao_cookie不能为空'}), 400
if channel == 2 and not toutiao_images_cookie:
logger.warning(f"[创建作者] toutiao_images_cookie为空")
return jsonify({'code': 400, 'message': 'toutiao_images_cookie不能为空'}), 400
# 兼容旧字段
department = data.get('department', department_name).strip()
# 可选字段
title = data.get('title', '').strip()
hospital = data.get('hospital', '').strip()
specialty = data.get('specialty', '').strip()
introduction = data.get('introduction', '').strip()
avatar_url = data.get('avatar_url', '').strip()
status = data.get('status', 'active')
# 状态验证
if status not in ['active', 'inactive']:
return jsonify({'code': 400, 'message': '状态值无效'}), 400
db_manager = get_db_manager()
# (1) 判断author_name唯一否则报错
check_author_name_sql = "SELECT id FROM ai_authors WHERE author_name = %s"
existing_author_name = db_manager.execute_query(check_author_name_sql, (author_name,))
if existing_author_name:
logger.warning(f"[创建作者] 作者姓名已存在: {author_name}")
return jsonify({'code': 400, 'message': f'作者姓名 "{author_name}" 已存在'}), 400
# (2) 判断app_id唯一否则报错
check_app_id_sql = "SELECT id FROM ai_authors WHERE app_id = %s"
existing_app_id = db_manager.execute_query(check_app_id_sql, (app_id,))
if existing_app_id:
logger.warning(f"[创建作者] app_id已存在: {app_id}")
return jsonify({'code': 400, 'message': f'app_id "{app_id}" 已存在'}), 400
# (3) 判断app_token唯一否则报错
check_app_token_sql = "SELECT id FROM ai_authors WHERE app_token = %s"
existing_app_token = db_manager.execute_query(check_app_token_sql, (app_token,))
if existing_app_token:
logger.warning(f"[创建作者] app_token已存在: {app_token}")
return jsonify({'code': 400, 'message': f'app_token "{app_token}" 已存在'}), 400
# (4) department_name处理逻辑
# 查询ai_departments表中是否存在该科室
check_dept_sql = "SELECT id FROM ai_departments WHERE department_name = %s"
existing_dept = db_manager.execute_query(check_dept_sql, (department_name,))
if existing_dept:
# 科室存在获取department_id
department_id = existing_dept[0]['id']
logger.info(f"[创建作者] 科室已存在使用现有ID: {department_id}")
else:
# 科室不存在,插入新科室
insert_dept_sql = "INSERT INTO ai_departments (department_name) VALUES (%s)"
department_id = db_manager.execute_insert(insert_dept_sql, (department_name,))
logger.info(f"[创建作者] 创建新科室ID: {department_id}")
# 创建作者
insert_sql = """INSERT INTO ai_authors
(author_name, app_id, app_token, department_name, department, department_id, title, hospital, specialty, toutiao_cookie, introduction, avatar_url, status, channel, toutiao_images_cookie)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"""
author_id = db_manager.execute_insert(insert_sql,
(author_name, app_id, app_token, department_name, department, department_id, title, hospital,
specialty, toutiao_cookie, introduction, avatar_url, status, channel, toutiao_images_cookie))
# 查询新作者
select_sql = """SELECT id, author_name, app_id, app_token, department_name, department, department_id, title, hospital, specialty,
toutiao_cookie, introduction, avatar_url, status, created_at, updated_at, channel
FROM ai_authors WHERE id = %s"""
new_author = db_manager.execute_query(select_sql, (author_id,))
logger.info(f"[创建作者] 成功创建作者ID: {author_id}, 科室ID: {department_id}")
return jsonify({
'code': 200,
'data': new_author[0],
'message': '创建成功',
'timestamp': int(datetime.now().timestamp() * 1000)
}), 201
except Exception as e:
logger.error(f"创建作者失败: {e}")
return jsonify({'code': 500, 'message': f'创建失败: {str(e)}'}), 500
@author_bp.route('/<int:author_id>', methods=['PUT'])
@require_auth
@require_role(['admin', 'editor'])
def update_author(author_id):
"""更新作者信息"""
try:
data = request.get_json()
# 必填字段验证
author_name = data.get('author_name', '').strip()
department_name = data.get('department_name', '').strip()
if not author_name:
return jsonify({'code': 400, 'message': '作者姓名不能为空'}), 400
if not department_name:
return jsonify({'code': 400, 'message': '科室不能为空'}), 400
# 可选字段
title = data.get('title', '').strip()
hospital = data.get('hospital', '').strip()
specialty = data.get('specialty', '').strip()
introduction = data.get('introduction', '').strip()
avatar_url = data.get('avatar_url', '').strip()
status = data.get('status', 'active')
toutiao_cookie = data.get('toutiao_cookie', '')
toutiao_images_cookie = data.get('toutiao_images_cookie', '')
# 状态验证
if status not in ['active', 'inactive']:
return jsonify({'code': 400, 'message': '状态值无效'}), 400
db_manager = get_db_manager()
# 检查存在
check_sql = "SELECT id FROM ai_authors WHERE id = %s"
existing = db_manager.execute_query(check_sql, (author_id,))
if not existing:
return jsonify({'code': 404, 'message': '作者不存在'}), 404
# 检查重复(同科室同姓名,排除自己)
duplicate_sql = "SELECT id FROM ai_authors WHERE author_name = %s AND department_name = %s AND id != %s"
duplicate = db_manager.execute_query(duplicate_sql, (author_name, department_name, author_id))
if duplicate:
return jsonify({'code': 400, 'message': '该科室已存在同名作者'}), 400
# (4) department_name处理逻辑
# 查询ai_departments表中是否存在该科室
check_dept_sql = "SELECT id FROM ai_departments WHERE department_name = %s"
existing_dept = db_manager.execute_query(check_dept_sql, (department_name,))
if existing_dept:
# 科室存在获取department_id
department_id = existing_dept[0]['id']
logger.info(f"[更新作者] 科室已存在使用现有ID: {department_id}")
else:
# 科室不存在,插入新科室
insert_dept_sql = "INSERT INTO ai_departments (department_name) VALUES (%s)"
department_id = db_manager.execute_insert(insert_dept_sql, (department_name,))
logger.info(f"[更新作者] 创建新科室ID: {department_id}")
# 更新作者
update_sql = """UPDATE ai_authors SET
author_name = %s, department_name = %s, department_id = %s, title = %s,
hospital = %s, specialty = %s, introduction = %s, avatar_url = %s,
status = %s, toutiao_cookie = %s, toutiao_images_cookie = %s,
updated_at = CURRENT_TIMESTAMP
WHERE id = %s"""
db_manager.execute_update(update_sql,
(author_name, department_name, department_id, title,
hospital, specialty, introduction, avatar_url,
status, toutiao_cookie, toutiao_images_cookie, author_id))
# 查询更新后作者
select_sql = """SELECT id, author_name, department_name, title, hospital, specialty,
introduction, toutiao_cookie, toutiao_images_cookie, avatar_url, status, created_at, updated_at
FROM ai_authors WHERE id = %s"""
updated_author = db_manager.execute_query(select_sql, (author_id,))
return jsonify({
'code': 200,
'data': updated_author[0],
'message': '更新成功',
'timestamp': int(datetime.now().timestamp() * 1000)
})
except Exception as e:
logger.error(f"更新作者失败: {e}")
return jsonify({'code': 500, 'message': f'更新失败: {str(e)}'}), 500
@author_bp.route('/<int:author_id>', methods=['DELETE'])
@require_auth
@require_role(['admin'])
def delete_author(author_id):
"""删除作者"""
try:
db_manager = get_db_manager()
# 检查存在
check_sql = "SELECT id, author_name FROM ai_authors WHERE id = %s"
existing = db_manager.execute_query(check_sql, (author_id,))
if not existing:
return jsonify({'code': 404, 'message': '作者不存在'}), 404
author_name = existing[0]['author_name']
# 检查引用(检查是否有文章引用该作者)
article_check_sql = "SELECT COUNT(*) as count FROM ai_articles WHERE author_name = %s"
article_count = db_manager.execute_query(article_check_sql, (author_name,))
if article_count[0]['count'] > 0:
return jsonify({'code': 400, 'message': f'作者被文章引用,无法删除'}), 400
# 删除作者
delete_sql = "DELETE FROM ai_authors WHERE id = %s"
db_manager.execute_update(delete_sql, (author_id,))
return jsonify({
'code': 200,
'message': '删除成功',
'timestamp': int(datetime.now().timestamp() * 1000)
})
except Exception as e:
logger.error(f"删除作者失败: {e}")
return jsonify({'code': 500, 'message': f'删除失败: {str(e)}'}), 500
@author_bp.route('/departments', methods=['GET'])
@require_auth
def get_author_departments():
"""获取所有作者科室列表"""
try:
db_manager = get_db_manager()
sql = "SELECT DISTINCT department FROM ai_authors WHERE department IS NOT NULL AND department != '' ORDER BY department"
departments = db_manager.execute_query(sql)
department_list = [dept['department'] for dept in departments]
return jsonify({
'code': 200,
'data': department_list,
'message': '获取成功',
'timestamp': int(datetime.now().timestamp() * 1000)
})
except Exception as e:
logger.error(f"获取科室列表失败: {e}")
return jsonify({'code': 500, 'message': f'获取失败: {str(e)}'}), 500
@author_bp.route('/search', methods=['GET'])
@require_auth
def search_authors():
"""搜索作者(用于下拉选择等)"""
try:
keyword = request.args.get('keyword', '').strip()
limit = int(request.args.get('limit', 20))
if not keyword:
return jsonify({
'code': 200,
'data': [],
'message': '获取成功',
'timestamp': int(datetime.now().timestamp() * 1000)
})
db_manager = get_db_manager()
sql = """SELECT id, author_name, department, title, hospital
FROM ai_authors
WHERE status = 'active' AND (author_name LIKE %s OR department LIKE %s)
ORDER BY author_name LIMIT %s"""
search_param = f"%{keyword}%"
authors = db_manager.execute_query(sql, (search_param, search_param, limit))
return jsonify({
'code': 200,
'data': authors,
'message': '获取成功',
'timestamp': int(datetime.now().timestamp() * 1000)
})
except Exception as e:
logger.error(f"搜索作者失败: {e}")
return jsonify({'code': 500, 'message': f'搜索失败: {str(e)}'}), 500
@author_bp.route('/detail_list', methods=['GET'])
@require_auth
def get_detail_list_authors():
"""获取作者详细列表分页直接查询ai_authors表不返回app_token字段"""
try:
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 10))
offset = (page - 1) * size
# 搜索参数
search = request.args.get('search', '').strip()
department = request.args.get('department', '').strip()
status = request.args.get('status', '').strip()
channel = request.args.get('channel', '')
db_manager = get_db_manager()
# 获取当前登录用户(仅用于权限验证)
current_user = AuthUtils.get_current_user()
logger.info(f"[get_detail_list_authors] 获取到的用户信息: {current_user}")
if not current_user:
logger.error("[get_detail_list_authors] 未获取到用户信息")
return jsonify({'code': 401, 'message': '未登录或用户信息缺失'}), 401
# 构建查询条件
where_conditions = []
params = []
if search:
where_conditions.append("(author_name LIKE %s OR hospital LIKE %s OR title LIKE %s OR department_name LIKE %s)")
search_param = f"%{search}%"
params.extend([search_param, search_param, search_param, search_param])
if department:
where_conditions.append("department = %s")
params.append(department)
if status:
where_conditions.append("status = %s")
params.append(status)
if channel:
where_conditions.append("channel = %s")
params.append(channel)
# 构建WHERE子句
where_clause = ""
if where_conditions:
where_clause = "WHERE " + " AND ".join(where_conditions)
# 查询作者总数(用于分页)
count_sql = f"SELECT COUNT(*) as total FROM ai_authors {where_clause}"
logger.info(f"[get_detail_list_authors] 执行计数查询: {count_sql}, 参数: {params}")
count_result = db_manager.execute_query(count_sql, params)
total_authors = count_result[0]['total'] if count_result else 0
logger.info(f"[get_detail_list_authors] 查询到的总数: {total_authors}")
# 查询作者详细信息排除app_token字段
author_sql = f"""SELECT id as author_id, author_name, app_id,
department_id, department_name, department, toutiao_cookie,
title, hospital, specialty, introduction,
avatar_url, status, created_at, updated_at
FROM ai_authors {where_clause}
ORDER BY id DESC
LIMIT %s OFFSET %s"""
author_params = params + [size, offset]
logger.info(f"[get_detail_list_authors] 执行作者查询: {author_sql}, 参数: {author_params}")
authors = db_manager.execute_query(author_sql, author_params)
logger.info(f"[get_detail_list_authors] 查询到的作者数量: {len(authors)}")
# 处理时间戳格式
for author in authors:
# 确保时间戳格式正确
if author.get('created_at'):
author['created_at'] = int(author['created_at'].timestamp() * 1000)
if author.get('updated_at'):
author['updated_at'] = int(author['updated_at'].timestamp() * 1000)
return jsonify({
'code': 200,
'data': {
'list': authors,
'page': page,
'size': size,
'total': total_authors
},
'message': '获取成功',
'timestamp': int(datetime.now().timestamp() * 1000)
})
except Exception as e:
import traceback
logger.error(f"获取作者详细列表失败: {e}")
logger.error(f"错误堆栈: {traceback.format_exc()}")
return jsonify({'code': 500, 'message': f'获取失败: {str(e)}'}), 500

View File

@@ -0,0 +1,234 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
图文动态批量生产系统 v2.0
Flask后端API服务器 - 总入口
"""
from flask import Flask, request, jsonify, redirect
from flask_cors import CORS
import logging
import time
import os
from datetime import datetime, timedelta
from logging.handlers import TimedRotatingFileHandler
import json # Added for json.dumps
# 导入统一日志配置
from log_config import setup_article_server_logger
# 导入数据库配置
from database_config import get_db_manager, DB_CONFIG
# 导入各个接口模块
from auth_routes import auth_bp
from user_routes import user_bp
from log_routes import log_bp
from enterprise_routes import enterprise_bp
from employee_routes import employee_bp
from product_routes import product_bp
from image_routes import image_bp
from prompt_routes import prompt_bp
from article_routes import article_bp
from statistics_routes import statistics_bp
from dashboard_routes import dashboard_bp
from author_routes import author_bp
# 导入认证工具
from auth_utils import AuthUtils, require_auth
# 创建Flask应用
app = Flask(__name__, static_folder='public', static_url_path='')
# 初始化日志系统
logger = setup_article_server_logger()
# 配置CORS - 允许所有域名的跨域请求
CORS(app,
origins=["http://127.0.0.1:8324", "http://localhost:8324", "http://127.0.0.1:3000", "http://localhost:3000", "*"],
methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization", "Accept", "X-Requested-With"],
supports_credentials=True,
max_age=3600)
# 注册蓝图
app.register_blueprint(auth_bp)
app.register_blueprint(user_bp)
app.register_blueprint(log_bp)
app.register_blueprint(enterprise_bp)
app.register_blueprint(employee_bp)
app.register_blueprint(product_bp)
app.register_blueprint(image_bp)
app.register_blueprint(prompt_bp)
app.register_blueprint(article_bp)
app.register_blueprint(statistics_bp)
app.register_blueprint(dashboard_bp)
app.register_blueprint(author_bp)
# 请求前拦截器 - 记录所有API访问
@app.before_request
def log_request_info():
"""记录每个请求的基本信息"""
client_ip = request.environ.get('HTTP_X_FORWARDED_FOR', request.environ.get('REMOTE_ADDR', '未知'))
user_agent = request.headers.get('User-Agent', '未知')
logger.info(f"[API访问] {request.method} {request.path} - IP: {client_ip} - User-Agent: {user_agent[:100]}")
# 记录请求参数GET请求
if request.args:
logger.debug(f"[请求参数] GET参数: {dict(request.args)}")
# CORS已经通过CORS(app)配置不需要额外的OPTIONS处理
# 请求后拦截器 - 记录响应状态
@app.after_request
def log_response_info(response):
"""记录响应状态"""
client_ip = request.environ.get('HTTP_X_FORWARDED_FOR', request.environ.get('REMOTE_ADDR', '未知'))
logger.info(f"[API响应] {request.method} {request.path} - IP: {client_ip} - 状态码: {response.status_code}")
return response
@app.route('/health', methods=['GET'])
def health_check():
"""健康检查接口"""
try:
# 检查数据库连接
db_manager = get_db_manager()
db_manager.execute_query("SELECT 1")
return jsonify({
'code': 200,
'message': '服务正常',
'data': {
'status': 'healthy',
'timestamp': int(datetime.now().timestamp() * 1000),
'version': '2.0'
}
})
except Exception as e:
logger.error(f"健康检查失败: {e}")
return jsonify({
'code': 500,
'message': '服务异常',
'data': {
'status': 'unhealthy',
'error': str(e),
'timestamp': int(datetime.now().timestamp() * 1000)
}
}), 500
@app.route('/', methods=['GET'])
def index():
"""根路径接口"""
return jsonify({
'code': 200,
'message': '万花筒API服务正常运行',
'data': {
'version': '1.0',
'service': 'wht_api'
}
})
@app.errorhandler(404)
def not_found(error):
"""404错误处理"""
return jsonify({
'code': 404,
'message': '接口不存在',
'data': None,
'timestamp': int(datetime.now().timestamp() * 1000)
}), 404
@app.errorhandler(405)
def method_not_allowed(error):
"""405错误处理"""
return jsonify({
'code': 405,
'message': '请求方法不允许',
'data': None,
'timestamp': int(datetime.now().timestamp() * 1000)
}), 405
@app.errorhandler(500)
def internal_error(error):
"""500错误处理"""
import traceback
error_traceback = traceback.format_exc()
logger.error(f"服务器内部错误: {error}")
logger.error(f"错误堆栈: {error_traceback}")
return jsonify({
'code': 500,
'message': '服务器内部错误',
'data': None,
'timestamp': int(datetime.now().timestamp() * 1000)
}), 500
# 开发环境启动入口
if __name__ == '__main__':
print("=" * 60)
print("万花筒API服务 v1.0 启动中...")
print("⚠️ 警告: 当前使用开发服务器,不建议在生产环境中使用")
print("⚠️ 生产环境请使用: waitress 等WSGI服务器")
print("-" * 60)
print("服务地址: http://127.0.0.1:8216")
print("健康检查: http://127.0.0.1:8216/health")
print("-" * 60)
print("接口模块:")
print(" - 用户认证: /api/v1/auth")
print(" - 企业管理: /api/v1/enterprises")
print(" - 员工管理: /api/v1/employees")
print(" - 产品管理: /api/v1/products")
print(" - 图片库管理: /api/v1/images")
print(" - 提示词管理: /api/v1/prompts")
print(" - 文案库管理: /api/v1/articles")
print(" - 数据统计: /api/v1/statistics")
print(" - 工作台: /api/v1/dashboard")
print(" - 用户管理: /api/users")
print(" - 日志管理: /api/log")
print("-" * 60)
print("日志配置信息:")
print(f" 主日志文件: logs/article_server.log")
print(f" 错误日志文件: logs/article_error.log")
print(f" 日志切割: 每日午夜自动切割")
print(f" 日志保留: 主日志3天错误日志9天")
print("-" * 60)
print("数据库配置:")
print(f" 主机: 127.0.0.1:3306")
print(f" 数据库: ai_wht")
print(f" 用户: root")
print("-" * 60)
print("生产部署命令:")
print(" waitress-serve --host=0.0.0.0 --port=8216 flask_wht_server_api:app")
print("=" * 60)
try:
# 测试数据库连接
logger.info("测试数据库连接...")
db_manager = get_db_manager()
db_manager.execute_query("SELECT 1")
logger.info("数据库连接成功")
# 启动开发服务器
logger.warning("使用Flask开发服务器启动仅用于开发测试")
app.run(
host='0.0.0.0',
port=8216,
debug=False,
threaded=True
)
except KeyboardInterrupt:
logger.info("收到中断信号,正在关闭服务器...")
print("\n服务器已关闭")
except Exception as e:
logger.error(f"服务器启动失败: {str(e)}", exc_info=True)
print(f"服务器启动失败: {str(e)}")
finally:
logger.info("万花筒API服务已停止")
print("日志已保存到 logs/ 目录")
# 生产环境初始化(当作为模块导入时执行)
else:
logger.info('万花筒API服务模块已加载')