2026-1-6
This commit is contained in:
1293
release/30/article_routes.py
Normal file
1293
release/30/article_routes.py
Normal file
File diff suppressed because it is too large
Load Diff
377
release/30/assign_authors_to_articles.py
Normal file
377
release/30/assign_authors_to_articles.py
Normal 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
693
release/30/author_routes.py
Normal 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
|
||||
234
release/30/flask_wht_server_api.py
Normal file
234
release/30/flask_wht_server_api.py
Normal 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服务模块已加载')
|
||||
Reference in New Issue
Block a user