first commit
This commit is contained in:
0
routes/__init__.py
Normal file
0
routes/__init__.py
Normal file
BIN
routes/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
routes/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
routes/__pycache__/api.cpython-39.pyc
Normal file
BIN
routes/__pycache__/api.cpython-39.pyc
Normal file
Binary file not shown.
BIN
routes/__pycache__/batch_api.cpython-39.pyc
Normal file
BIN
routes/__pycache__/batch_api.cpython-39.pyc
Normal file
Binary file not shown.
BIN
routes/__pycache__/db_api.cpython-39.pyc
Normal file
BIN
routes/__pycache__/db_api.cpython-39.pyc
Normal file
Binary file not shown.
BIN
routes/__pycache__/monitor_api.cpython-39.pyc
Normal file
BIN
routes/__pycache__/monitor_api.cpython-39.pyc
Normal file
Binary file not shown.
BIN
routes/__pycache__/pages.cpython-39.pyc
Normal file
BIN
routes/__pycache__/pages.cpython-39.pyc
Normal file
Binary file not shown.
433
routes/api.py
Normal file
433
routes/api.py
Normal file
@@ -0,0 +1,433 @@
|
||||
import base64
|
||||
import io
|
||||
import logging
|
||||
import time
|
||||
from urllib.parse import quote
|
||||
from flask import Blueprint, request, jsonify, Response
|
||||
from PIL import Image as PILImage
|
||||
from docx import Document
|
||||
from lib.word_signer import sign_word, _new_trace_id
|
||||
from lib.image_processor import process_image
|
||||
from lib import field_codes
|
||||
from lib.validators import validate_sign_request, validate_docx_size, validate_image_size
|
||||
from lib.config import get_limits_config, get_oa_config
|
||||
from lib import monitor
|
||||
import lib.db as db
|
||||
|
||||
api_bp = Blueprint('api', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _error_body(code, message, trace_id, **extra):
|
||||
body = {'success': False, 'code': code, 'message': message, 'trace_id': trace_id}
|
||||
if extra:
|
||||
body['data'] = extra
|
||||
return body
|
||||
|
||||
|
||||
@api_bp.route('/api/word/sign', methods=['POST'])
|
||||
def word_sign():
|
||||
log_source = '外部API'
|
||||
trace_id = _new_trace_id()
|
||||
client_ip = request.remote_addr
|
||||
start_ts = time.time()
|
||||
limits = get_limits_config()
|
||||
|
||||
monitor.register_request(
|
||||
trace_id, client_ip, '/api/word/sign',
|
||||
request_body_summary={'content_type': request.content_type}
|
||||
)
|
||||
logger.info("[请求接入] 客户端IP=%s, 接口=/api/word/sign, trace_id=%s",
|
||||
client_ip, trace_id, extra={'log_source': log_source, 'trace_id': trace_id})
|
||||
|
||||
content_type = request.content_type or ''
|
||||
|
||||
if 'application/json' in content_type:
|
||||
json_data = request.get_json(silent=True)
|
||||
if not json_data:
|
||||
logger.error("[参数校验] 请求体JSON解析失败",
|
||||
extra={'log_source': log_source, 'trace_id': trace_id})
|
||||
monitor.finish_request(trace_id, 'error', response_status=400,
|
||||
elapsed_ms=int((time.time() - start_ts) * 1000),
|
||||
error_message='INVALID_JSON')
|
||||
return jsonify(_error_body('INVALID_JSON', '请求体JSON解析失败', trace_id)), 400
|
||||
|
||||
params = json_data
|
||||
docx_base64 = params.get('docx_base64', '')
|
||||
if not docx_base64:
|
||||
logger.error("[参数校验] 缺少docx_base64字段",
|
||||
extra={'log_source': log_source, 'trace_id': trace_id})
|
||||
monitor.finish_request(trace_id, 'error', response_status=400,
|
||||
elapsed_ms=int((time.time() - start_ts) * 1000),
|
||||
error_message='MISSING_DOCX')
|
||||
return jsonify(_error_body('MISSING_DOCX', '缺少docx_base64字段', trace_id)), 400
|
||||
|
||||
try:
|
||||
docx_data = base64.b64decode(docx_base64)
|
||||
except Exception as e:
|
||||
logger.error("[参数校验] docx_base64解码失败: %s", str(e),
|
||||
extra={'log_source': log_source, 'trace_id': trace_id})
|
||||
monitor.finish_request(trace_id, 'error', response_status=400,
|
||||
elapsed_ms=int((time.time() - start_ts) * 1000),
|
||||
error_message='INVALID_BASE64')
|
||||
return jsonify(_error_body('INVALID_BASE64', f'docx_base64解码失败: {e}', trace_id)), 400
|
||||
|
||||
elif 'multipart/form-data' in content_type:
|
||||
docx_file = request.files.get('docx')
|
||||
if not docx_file:
|
||||
logger.error("[参数校验] multipart请求缺少docx文件",
|
||||
extra={'log_source': log_source, 'trace_id': trace_id})
|
||||
monitor.finish_request(trace_id, 'error', response_status=400,
|
||||
elapsed_ms=int((time.time() - start_ts) * 1000),
|
||||
error_message='MISSING_DOCX')
|
||||
return jsonify(_error_body('MISSING_DOCX', '缺少docx文件', trace_id)), 400
|
||||
docx_data = docx_file.read()
|
||||
params = {
|
||||
'docx_name': docx_file.filename or 'upload.docx',
|
||||
'match_mode': request.form.get('match_mode', 'upload'),
|
||||
'stamps': [],
|
||||
}
|
||||
import json
|
||||
try:
|
||||
params['stamps'] = json.loads(request.form.get('stamps', '[]'))
|
||||
except Exception:
|
||||
params['stamps'] = []
|
||||
|
||||
image_file = request.files.get('image')
|
||||
if image_file:
|
||||
img_bytes = image_file.read()
|
||||
valid, msg = validate_image_size(img_bytes, limits['image_max_bytes'])
|
||||
if not valid:
|
||||
logger.error("[参数校验] 上传图片过大: %s", msg,
|
||||
extra={'log_source': log_source, 'trace_id': trace_id})
|
||||
monitor.finish_request(trace_id, 'error', response_status=400,
|
||||
elapsed_ms=int((time.time() - start_ts) * 1000),
|
||||
error_message='IMAGE_TOO_LARGE')
|
||||
return jsonify(_error_body('IMAGE_TOO_LARGE', msg, trace_id)), 400
|
||||
img_b64 = base64.b64encode(img_bytes).decode('utf-8')
|
||||
for s in params['stamps']:
|
||||
s['image_base64'] = img_b64
|
||||
s['image_filename'] = image_file.filename or 'image.png'
|
||||
|
||||
for key in ['use_global_align', 'align_h', 'align_v', 'use_global_height',
|
||||
'height', 'use_global_noise', 'noise_level', 'use_global_rotate',
|
||||
'rotate_min', 'rotate_max']:
|
||||
if key in request.form:
|
||||
val = request.form[key]
|
||||
if val.lower() in ('true', 'false'):
|
||||
params[key] = val.lower() == 'true'
|
||||
else:
|
||||
try:
|
||||
if '.' in val:
|
||||
params[key] = float(val)
|
||||
else:
|
||||
params[key] = int(val)
|
||||
except ValueError:
|
||||
params[key] = val
|
||||
else:
|
||||
logger.error("[参数校验] 不支持的Content-Type: %s", content_type,
|
||||
extra={'log_source': log_source, 'trace_id': trace_id})
|
||||
monitor.finish_request(trace_id, 'error', response_status=400,
|
||||
elapsed_ms=int((time.time() - start_ts) * 1000),
|
||||
error_message='INVALID_CONTENT_TYPE')
|
||||
return jsonify(_error_body('INVALID_CONTENT_TYPE',
|
||||
f'不支持的Content-Type: {content_type}', trace_id)), 400
|
||||
|
||||
# SRS 9.1/15.3.3:docx 文档 ≤ 1 MB
|
||||
valid, msg = validate_docx_size(docx_data, limits['docx_max_bytes'])
|
||||
if not valid:
|
||||
logger.error("[参数校验] %s", msg, extra={'log_source': log_source, 'trace_id': trace_id})
|
||||
monitor.finish_request(trace_id, 'error', response_status=400,
|
||||
elapsed_ms=int((time.time() - start_ts) * 1000),
|
||||
error_message='DOCX_TOO_LARGE')
|
||||
return jsonify(_error_body('DOCX_TOO_LARGE', msg, trace_id)), 400
|
||||
|
||||
# SRS 15.3.3:upload 模式下图片大小校验
|
||||
if params.get('match_mode') == 'upload':
|
||||
for idx, s in enumerate(params.get('stamps', [])):
|
||||
b64 = s.get('image_base64') or ''
|
||||
if b64:
|
||||
try:
|
||||
img_bytes = base64.b64decode(b64)
|
||||
valid, msg = validate_image_size(img_bytes, limits['image_max_bytes'])
|
||||
if not valid:
|
||||
logger.error("[参数校验] 盖章项%d图片过大: %s", idx + 1, msg,
|
||||
extra={'log_source': log_source, 'trace_id': trace_id})
|
||||
monitor.finish_request(trace_id, 'error', response_status=400,
|
||||
elapsed_ms=int((time.time() - start_ts) * 1000),
|
||||
error_message='IMAGE_TOO_LARGE')
|
||||
return jsonify(_error_body('IMAGE_TOO_LARGE', f"盖章项{idx + 1}: {msg}", trace_id)), 400
|
||||
except Exception as e:
|
||||
monitor.finish_request(trace_id, 'error', response_status=400,
|
||||
elapsed_ms=int((time.time() - start_ts) * 1000),
|
||||
error_message='INVALID_BASE64')
|
||||
return jsonify(_error_body('INVALID_BASE64',
|
||||
f"盖章项{idx + 1}: 图片Base64解码失败", trace_id)), 400
|
||||
|
||||
errors = validate_sign_request(params)
|
||||
if errors:
|
||||
logger.error("[参数校验] 校验失败: %s", '; '.join(errors),
|
||||
extra={'log_source': log_source, 'trace_id': trace_id})
|
||||
monitor.finish_request(trace_id, 'error', response_status=400,
|
||||
elapsed_ms=int((time.time() - start_ts) * 1000),
|
||||
error_message='VALIDATION_ERROR')
|
||||
return jsonify(_error_body('VALIDATION_ERROR', '; '.join(errors), trace_id)), 400
|
||||
|
||||
logger.info("[参数校验] 校验通过", extra={'log_source': log_source, 'trace_id': trace_id})
|
||||
|
||||
# SRS 15.3.4:需要数据库的模式下 MySQL 未启动或连接失败,返回 5xx
|
||||
if params.get('match_mode') in ('id', 'name', 'auto'):
|
||||
if not db.ensure_mysql_available():
|
||||
logger.error("[数据库] MySQL 不可用,按 SRS 15.3.4 返回 5xx",
|
||||
extra={'log_source': log_source, 'trace_id': trace_id})
|
||||
monitor.finish_request(trace_id, 'error', response_status=503,
|
||||
elapsed_ms=int((time.time() - start_ts) * 1000),
|
||||
error_message='MYSQL_UNAVAILABLE')
|
||||
return jsonify(_error_body('MYSQL_UNAVAILABLE',
|
||||
'MySQL 不可用,请检查数据库配置', trace_id)), 503
|
||||
|
||||
result, error = sign_word(docx_data, params, log_source=log_source, trace_id=trace_id, client_ip=client_ip)
|
||||
|
||||
if error:
|
||||
code = error.get('code', 'INTERNAL_ERROR')
|
||||
status_code = 400 if code in ('INVALID_FILE_TYPE', 'INVALID_MATCH_MODE',
|
||||
'TOO_MANY_STAMPS', 'INVALID_DOCX',
|
||||
'DOCX_TOO_LARGE', 'IMAGE_TOO_LARGE',
|
||||
'VALIDATION_ERROR') else 500
|
||||
monitor.finish_request(trace_id, 'error', response_status=status_code,
|
||||
elapsed_ms=int((time.time() - start_ts) * 1000),
|
||||
error_message=code)
|
||||
return jsonify(error), status_code
|
||||
|
||||
filename = result['filename']
|
||||
response = Response(
|
||||
result['data'],
|
||||
mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
)
|
||||
ascii_fallback = filename.encode('ascii', errors='replace').decode('ascii')
|
||||
response.headers['Content-Disposition'] = (
|
||||
f"attachment; filename=\"{ascii_fallback}\"; "
|
||||
f"filename*=UTF-8''{quote(filename, safe='')}"
|
||||
)
|
||||
response.headers['X-SEAL-Trace-Id'] = result.get('trace_id', trace_id)
|
||||
response.headers['X-SEAL-Warnings'] = str(len(result.get('warnings', [])))
|
||||
if result.get('warnings'):
|
||||
joined = '; '.join(result['warnings'])
|
||||
encoded = quote(joined, safe='')
|
||||
encoded_bytes = encoded.encode('latin-1', errors='replace')
|
||||
while len(encoded_bytes) > 1024:
|
||||
encoded = encoded[:-1]
|
||||
encoded_bytes = encoded.encode('latin-1', errors='replace')
|
||||
response.headers['X-SEAL-Warning-Detail'] = encoded
|
||||
|
||||
elapsed_ms = int((time.time() - start_ts) * 1000)
|
||||
monitor.finish_request(
|
||||
trace_id, 'success', response_status=200, elapsed_ms=elapsed_ms,
|
||||
response_summary={'filename': filename, 'success_count': result.get('success_count', 0)},
|
||||
warnings=result.get('warnings', [])
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
def _image_data_url(data):
|
||||
"""根据魔数判断图片格式并构造 data URL。"""
|
||||
if data[:8] == b'\x89PNG\r\n\x1a\n':
|
||||
mime = 'image/png'
|
||||
elif data[:3] == b'\xff\xd8\xff':
|
||||
mime = 'image/jpeg'
|
||||
elif data[:6] in (b'GIF87a', b'GIF89a'):
|
||||
mime = 'image/gif'
|
||||
else:
|
||||
mime = 'image/png'
|
||||
return f'data:{mime};base64,' + base64.b64encode(data).decode('ascii')
|
||||
|
||||
|
||||
def _resolve_preview_image(payload):
|
||||
"""根据 payload 解析出 (image_bytes, is_signature) 或 (None, error_msg)。"""
|
||||
match_mode = payload.get('match_mode', 'upload')
|
||||
|
||||
if match_mode == 'upload':
|
||||
b64 = payload.get('image_base64') or ''
|
||||
if not b64:
|
||||
return None, 'upload 模式缺少 image_base64'
|
||||
try:
|
||||
return base64.b64decode(b64), None
|
||||
except Exception as e:
|
||||
return None, f'Base64 解码失败: {e}'
|
||||
|
||||
if not db.ensure_mysql_available():
|
||||
return None, 'MySQL 不可用,无法按人员查询'
|
||||
|
||||
is_sig_from_db = None
|
||||
if match_mode == 'id':
|
||||
image_id = (payload.get('image_id') or '').strip()
|
||||
if not image_id:
|
||||
return None, 'id 模式缺少 image_id'
|
||||
record = db.get_image_by_user_id(image_id)
|
||||
if not record:
|
||||
return None, f'未找到 image_id={image_id}'
|
||||
return record['sign_image'], None
|
||||
|
||||
if match_mode == 'name':
|
||||
name = (payload.get('image_name') or '').strip()
|
||||
if not name:
|
||||
return None, 'name 模式缺少 image_name'
|
||||
records = db.get_image_by_user_name(name)
|
||||
if not records:
|
||||
return None, f'未找到 image_name={name}'
|
||||
return records[0]['sign_image'], None
|
||||
|
||||
if match_mode == 'auto':
|
||||
marker = (payload.get('marker') or '').strip()
|
||||
if not marker:
|
||||
return None, 'auto 模式缺少 marker'
|
||||
name = marker[5:] if marker.startswith('USER_') else marker
|
||||
name = field_codes._strip_switches(name)
|
||||
if not name:
|
||||
return None, f'marker={marker} 无法解析出姓名'
|
||||
records = db.get_image_by_user_name(name)
|
||||
if not records:
|
||||
return None, f'未找到姓名={name}'
|
||||
return records[0]['sign_image'], None
|
||||
|
||||
return None, f'不支持的 match_mode: {match_mode}'
|
||||
|
||||
|
||||
def _parse_float(value, default=None, lo=None, hi=None):
|
||||
if value in (None, ''):
|
||||
return default
|
||||
try:
|
||||
v = float(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
if lo is not None and v < lo:
|
||||
return default
|
||||
if hi is not None and v > hi:
|
||||
return default
|
||||
return v
|
||||
|
||||
|
||||
def _parse_int(value, default=0, lo=None, hi=None):
|
||||
if value in (None, ''):
|
||||
return default
|
||||
try:
|
||||
v = int(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
if lo is not None and v < lo:
|
||||
return default
|
||||
if hi is not None and v > hi:
|
||||
return default
|
||||
return v
|
||||
|
||||
|
||||
@api_bp.route('/api/preview/sign-image', methods=['POST'])
|
||||
def preview_sign_image():
|
||||
"""预览:原签名图 vs 处理后效果(走完整 process_image 流水线)。"""
|
||||
log_source = '前端操作'
|
||||
payload = request.get_json(silent=True) or {}
|
||||
|
||||
image_data, err = _resolve_preview_image(payload)
|
||||
if err:
|
||||
logger.warning("[图片预览] 解析图片失败: %s", err, extra={'log_source': log_source})
|
||||
return jsonify({'success': False, 'message': err}), 400
|
||||
|
||||
is_sig = bool(payload.get('is_signature', False))
|
||||
|
||||
height_cm = _parse_float(payload.get('height_cm'), default=None, lo=0.1, hi=10.0)
|
||||
noise_level = _parse_int(payload.get('noise_level'), default=0, lo=0, hi=10)
|
||||
rotate_min = _parse_int(payload.get('rotate_min'), default=0, lo=-180, hi=180)
|
||||
rotate_max = _parse_int(payload.get('rotate_max'), default=0, lo=-180, hi=180)
|
||||
|
||||
try:
|
||||
processed = process_image(
|
||||
image_data,
|
||||
is_signature=is_sig,
|
||||
noise_level=noise_level,
|
||||
rotate_min=rotate_min,
|
||||
rotate_max=rotate_max,
|
||||
log_source=log_source,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("[图片预览] 处理失败: %s", str(e), extra={'log_source': log_source})
|
||||
return jsonify({'success': False, 'message': f'图片处理失败: {e}'}), 500
|
||||
|
||||
try:
|
||||
pimg = PILImage.open(io.BytesIO(processed))
|
||||
p_w, p_h = pimg.size
|
||||
except Exception:
|
||||
p_w, p_h = 0, 0
|
||||
|
||||
try:
|
||||
oimg = PILImage.open(io.BytesIO(image_data))
|
||||
o_w, o_h = oimg.size
|
||||
except Exception:
|
||||
o_w, o_h = 0, 0
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'original': _image_data_url(image_data),
|
||||
'processed': _image_data_url(processed),
|
||||
'original_size': len(image_data),
|
||||
'processed_size': len(processed),
|
||||
'original_dimensions': [o_w, o_h],
|
||||
'processed_dimensions': [p_w, p_h],
|
||||
'is_signature': is_sig,
|
||||
'params': {
|
||||
'height_cm': height_cm, 'noise_level': noise_level,
|
||||
'rotate_min': rotate_min, 'rotate_max': rotate_max,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@api_bp.route('/api/doc/scan-markers', methods=['POST'])
|
||||
def doc_scan_markers():
|
||||
"""扫描 docx 中所有 SET USER_<name> 标记,用于 auto 模式预览。"""
|
||||
log_source = '前端操作'
|
||||
payload = request.get_json(silent=True) or {}
|
||||
docx_b64 = payload.get('docx_base64') or ''
|
||||
if not docx_b64:
|
||||
return jsonify({'success': False, 'message': '缺少 docx_base64'}), 400
|
||||
try:
|
||||
docx_data = base64.b64decode(docx_b64)
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f'docx Base64 解码失败: {e}'}), 400
|
||||
|
||||
try:
|
||||
doc = Document(io.BytesIO(docx_data))
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f'Word 解析失败: {e}'}), 400
|
||||
|
||||
markers = field_codes.scan_user_markers(doc, log_source=log_source)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'markers': [{'marker': m['marker'], 'name': m['name']} for m in markers],
|
||||
})
|
||||
|
||||
|
||||
@api_bp.route('/api/health', methods=['GET'])
|
||||
def health():
|
||||
limits = get_limits_config()
|
||||
oa = get_oa_config()
|
||||
mysql_enabled = (db.get_active_db_type() == 'mysql')
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'mysql_enabled': mysql_enabled,
|
||||
'docx_max_bytes': limits['docx_max_bytes'],
|
||||
'image_max_bytes': limits['image_max_bytes'],
|
||||
'stamp_max_count': limits['stamp_max_count'],
|
||||
'action_timeout_seconds': oa['action_timeout_seconds'],
|
||||
'db_type': db.get_active_db_type(),
|
||||
})
|
||||
|
||||
|
||||
@api_bp.route('/api/db/init', methods=['POST'])
|
||||
def init_db_endpoint():
|
||||
try:
|
||||
ok = db.init_database(strict_mysql=False)
|
||||
if not ok:
|
||||
return jsonify({'success': False, 'message': 'MySQL 不可用'}), 500
|
||||
return jsonify({'success': True, 'message': f"数据库已初始化: {db.get_active_db_type()}"})
|
||||
except Exception as e:
|
||||
logger.error("数据库初始化失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
104
routes/batch_api.py
Normal file
104
routes/batch_api.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import logging
|
||||
import threading
|
||||
from flask import Blueprint, request, jsonify
|
||||
|
||||
from lib import batch_processor
|
||||
from lib.word_signer import _new_trace_id
|
||||
|
||||
batch_api_bp = Blueprint('batch_api', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_picker_lock = threading.Lock()
|
||||
|
||||
|
||||
@batch_api_bp.route('/api/batch/rules', methods=['GET'])
|
||||
def batch_rules_get():
|
||||
path = request.args.get('path') or None
|
||||
try:
|
||||
result = batch_processor.load_rules(path)
|
||||
return jsonify({'success': True, 'rules': result['rules'],
|
||||
'warning': result.get('warning'),
|
||||
'path': batch_processor.resolve_rules_path(path)})
|
||||
except Exception as e:
|
||||
logger.error("加载规则失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e), 'rules': []}), 500
|
||||
|
||||
|
||||
@batch_api_bp.route('/api/batch/rules', methods=['POST'])
|
||||
def batch_rules_save():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
rules = payload.get('rules') or []
|
||||
path = payload.get('path') or None
|
||||
try:
|
||||
result = batch_processor.save_rules(rules, path)
|
||||
return jsonify({'success': True, 'rules': result['rules'],
|
||||
'path': result['path'], 'warnings': result.get('warnings', [])})
|
||||
except RuntimeError as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 400
|
||||
except Exception as e:
|
||||
logger.error("保存规则失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@batch_api_bp.route('/api/batch/regex-snippets', methods=['GET'])
|
||||
def batch_snippets():
|
||||
return jsonify(batch_processor.get_regex_snippets())
|
||||
|
||||
|
||||
@batch_api_bp.route('/api/batch/rules/test', methods=['POST'])
|
||||
def batch_rules_test():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
rules = payload.get('rules') or []
|
||||
text = payload.get('text') or ''
|
||||
try:
|
||||
result = batch_processor.test_rules(rules, text)
|
||||
return jsonify({'success': True, **result})
|
||||
except Exception as e:
|
||||
logger.error("测试规则失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@batch_api_bp.route('/api/batch/pick-folder', methods=['GET'])
|
||||
def batch_pick_folder():
|
||||
title = request.args.get('title') or '选择文件夹'
|
||||
initialdir = request.args.get('initialdir') or None
|
||||
try:
|
||||
with _picker_lock:
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
root.attributes('-topmost', True)
|
||||
folder = filedialog.askdirectory(
|
||||
title=title, initialdir=initialdir, mustexist=True
|
||||
)
|
||||
root.destroy()
|
||||
return jsonify({'success': True, 'folder': folder or ''})
|
||||
except Exception as e:
|
||||
logger.error("选择目录失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e), 'folder': ''}), 500
|
||||
|
||||
|
||||
@batch_api_bp.route('/api/batch/process', methods=['POST'])
|
||||
def batch_process():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
folder = payload.get('folder')
|
||||
if not folder:
|
||||
return jsonify({'success': False, 'message': '请提供 Word 根目录路径'}), 400
|
||||
|
||||
recursive = bool(payload.get('recursive', True))
|
||||
auto_sign = bool(payload.get('auto_sign', True))
|
||||
rules = payload.get('rules') or []
|
||||
request_options = payload.get('request_options') or {}
|
||||
|
||||
trace_id = _new_trace_id()
|
||||
try:
|
||||
summary = batch_processor.process_folder(
|
||||
folder, recursive=recursive, auto_sign=auto_sign,
|
||||
rules=rules, request_options=request_options,
|
||||
log_source='批量', trace_id=trace_id,
|
||||
)
|
||||
return jsonify({'success': summary['success'], **summary, 'trace_id': trace_id})
|
||||
except Exception as e:
|
||||
logger.error("批量处理失败: %s", str(e), extra={'log_source': '批量', 'trace_id': trace_id})
|
||||
return jsonify({'success': False, 'message': str(e), 'trace_id': trace_id}), 500
|
||||
438
routes/db_api.py
Normal file
438
routes/db_api.py
Normal file
@@ -0,0 +1,438 @@
|
||||
import base64
|
||||
import logging
|
||||
from flask import Blueprint, request, jsonify, Response
|
||||
import lib.db as db
|
||||
from lib.validators import validate_user_id, validate_user_name, validate_image_size
|
||||
from lib.config import get_limits_config
|
||||
from lib.image_processor import preview_remove_white, preview_binarize
|
||||
|
||||
db_api_bp = Blueprint('db_api', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@db_api_bp.route('/api/db/list', methods=['GET'])
|
||||
def db_list():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
page_size = request.args.get('page_size', 20, type=int)
|
||||
keyword = request.args.get('keyword', '')
|
||||
try:
|
||||
result = db.query_all(page=page, page_size=page_size, keyword=keyword or None)
|
||||
return jsonify({'success': True, 'data': result})
|
||||
except Exception as e:
|
||||
logger.error("查询列表失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@db_api_bp.route('/api/db/get/<user_id>', methods=['GET'])
|
||||
def db_get(user_id):
|
||||
try:
|
||||
record = db.query_by_user_id(user_id)
|
||||
if not record:
|
||||
return jsonify({'success': False, 'message': f'未找到人员ID={user_id}的记录'}), 404
|
||||
record['is_signature'] = bool(record['is_signature'])
|
||||
record.pop('sign_image', None)
|
||||
return jsonify({'success': True, 'data': record})
|
||||
except Exception as e:
|
||||
logger.error("查询记录失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@db_api_bp.route('/api/db/image/<user_id>', methods=['GET'])
|
||||
def db_get_image(user_id):
|
||||
try:
|
||||
record = db.query_by_user_id(user_id)
|
||||
if not record:
|
||||
return jsonify({'success': False, 'message': '未找到记录'}), 404
|
||||
image_data = record['sign_image']
|
||||
return Response(image_data, mimetype='image/png')
|
||||
except Exception as e:
|
||||
logger.error("获取图片失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@db_api_bp.route('/api/db/preview/<user_id>', methods=['GET'])
|
||||
def db_preview_image(user_id):
|
||||
try:
|
||||
record = db.query_by_user_id(user_id)
|
||||
if not record:
|
||||
return jsonify({'success': False, 'message': '未找到记录'}), 404
|
||||
image_data = record['sign_image']
|
||||
is_sig = bool(record['is_signature'])
|
||||
if is_sig:
|
||||
processed = preview_binarize(image_data)
|
||||
else:
|
||||
processed = preview_remove_white(image_data)
|
||||
return Response(processed, mimetype='image/png')
|
||||
except Exception as e:
|
||||
logger.error("预览图片失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@db_api_bp.route('/api/db/add', methods=['POST'])
|
||||
def db_add():
|
||||
try:
|
||||
content_type = request.content_type or ''
|
||||
|
||||
if 'multipart/form-data' in content_type:
|
||||
user_id = request.form.get('user_id', '')
|
||||
user_name = request.form.get('user_name', '')
|
||||
is_signature = request.form.get('is_signature', '0')
|
||||
image_file = request.files.get('sign_image')
|
||||
|
||||
valid, msg = validate_user_id(user_id)
|
||||
if not valid:
|
||||
return jsonify({'success': False, 'message': msg}), 400
|
||||
valid, msg = validate_user_name(user_name)
|
||||
if not valid:
|
||||
return jsonify({'success': False, 'message': msg}), 400
|
||||
if not image_file:
|
||||
return jsonify({'success': False, 'message': '缺少签章图片'}), 400
|
||||
|
||||
image_data = image_file.read()
|
||||
is_sig = 1 if is_signature in ('1', 'true', 'True') else 0
|
||||
|
||||
db.insert_record(user_id, user_name, image_data, is_sig)
|
||||
return jsonify({'success': True, 'message': '新增成功'})
|
||||
|
||||
json_data = request.get_json(silent=True)
|
||||
if not json_data:
|
||||
return jsonify({'success': False, 'message': '无效请求数据'}), 400
|
||||
|
||||
user_id = json_data.get('user_id', '')
|
||||
user_name = json_data.get('user_name', '')
|
||||
is_signature = json_data.get('is_signature', 0)
|
||||
image_b64 = json_data.get('sign_image_base64', '')
|
||||
|
||||
valid, msg = validate_user_id(user_id)
|
||||
if not valid:
|
||||
return jsonify({'success': False, 'message': msg}), 400
|
||||
valid, msg = validate_user_name(user_name)
|
||||
if not valid:
|
||||
return jsonify({'success': False, 'message': msg}), 400
|
||||
if not image_b64:
|
||||
return jsonify({'success': False, 'message': '缺少签章图片'}), 400
|
||||
|
||||
image_data = base64.b64decode(image_b64)
|
||||
is_sig = 1 if is_signature in (1, '1', True, 'true') else 0
|
||||
|
||||
db.insert_record(user_id, user_name, image_data, is_sig)
|
||||
return jsonify({'success': True, 'message': '新增成功'})
|
||||
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 400
|
||||
except Exception as e:
|
||||
logger.error("新增记录失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@db_api_bp.route('/api/db/update/<user_id>', methods=['PUT'])
|
||||
def db_update(user_id):
|
||||
try:
|
||||
content_type = request.content_type or ''
|
||||
user_name = None
|
||||
sign_image = None
|
||||
is_signature = None
|
||||
|
||||
if 'multipart/form-data' in content_type:
|
||||
user_name = request.form.get('user_name')
|
||||
is_sig_str = request.form.get('is_signature')
|
||||
if is_sig_str is not None:
|
||||
is_signature = 1 if is_sig_str in ('1', 'true', 'True') else 0
|
||||
image_file = request.files.get('sign_image')
|
||||
if image_file:
|
||||
sign_image = image_file.read()
|
||||
else:
|
||||
json_data = request.get_json(silent=True)
|
||||
if json_data:
|
||||
user_name = json_data.get('user_name')
|
||||
is_sig_val = json_data.get('is_signature')
|
||||
if is_sig_val is not None:
|
||||
is_signature = 1 if is_sig_val in (1, '1', True, 'true') else 0
|
||||
image_b64 = json_data.get('sign_image_base64')
|
||||
if image_b64:
|
||||
sign_image = base64.b64decode(image_b64)
|
||||
|
||||
db.update_record(user_id, user_name=user_name, sign_image=sign_image, is_signature=is_signature)
|
||||
return jsonify({'success': True, 'message': '更新成功'})
|
||||
except Exception as e:
|
||||
logger.error("更新记录失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@db_api_bp.route('/api/db/delete/<user_id>', methods=['DELETE'])
|
||||
def db_delete(user_id):
|
||||
try:
|
||||
db.delete_record(user_id)
|
||||
return jsonify({'success': True, 'message': '删除成功'})
|
||||
except Exception as e:
|
||||
logger.error("删除记录失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@db_api_bp.route('/api/db/batch_import', methods=['POST'])
|
||||
def db_batch_import():
|
||||
try:
|
||||
files = request.files.getlist('files')
|
||||
auto_id = request.form.get('auto_id', '0') == '1'
|
||||
id_prefix = request.form.get('id_prefix', 'USER')
|
||||
is_signature = request.form.get('is_signature', '0')
|
||||
is_sig = 1 if is_signature in ('1', 'true', 'True') else 0
|
||||
|
||||
results = []
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
id_counter = 1
|
||||
|
||||
for f in files:
|
||||
filename = f.filename or ''
|
||||
name_part = filename.rsplit('.', 1)[0] if '.' in filename else filename
|
||||
|
||||
if auto_id:
|
||||
user_id = f"{id_prefix}{id_counter:04d}"
|
||||
id_counter += 1
|
||||
else:
|
||||
user_id = name_part
|
||||
|
||||
user_name = name_part
|
||||
image_data = f.read()
|
||||
|
||||
try:
|
||||
db.insert_record(user_id, user_name, image_data, is_sig)
|
||||
results.append({'filename': filename, 'user_id': user_id, 'status': 'success'})
|
||||
success_count += 1
|
||||
except ValueError as e:
|
||||
results.append({'filename': filename, 'user_id': user_id, 'status': 'fail', 'message': str(e)})
|
||||
fail_count += 1
|
||||
except Exception as e:
|
||||
results.append({'filename': filename, 'user_id': user_id, 'status': 'fail', 'message': str(e)})
|
||||
fail_count += 1
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'total': len(files),
|
||||
'success': success_count,
|
||||
'fail': fail_count,
|
||||
'results': results,
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error("批量导入失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SPA 兼容端点:/api/db/records* (供 templates/index.html 调用)
|
||||
# 与上方原 /api/db/* 端点并存,不互相影响。
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _records_to_spa(record):
|
||||
"""将内部记录转换为 SPA 期望的列表项格式。"""
|
||||
image_size = 0
|
||||
if record.get('sign_image'):
|
||||
try:
|
||||
image_size = len(record['sign_image'])
|
||||
except Exception:
|
||||
image_size = 0
|
||||
return {
|
||||
'user_id': record.get('user_id'),
|
||||
'user_name': record.get('user_name'),
|
||||
'is_signature': bool(record.get('is_signature')),
|
||||
'image_size': image_size,
|
||||
'create_time': str(record.get('create_time') or ''),
|
||||
'update_time': str(record.get('update_time') or ''),
|
||||
}
|
||||
|
||||
|
||||
@db_api_bp.route('/api/db/records', methods=['GET'])
|
||||
def db_records_list_spa():
|
||||
try:
|
||||
keyword = request.args.get('q') or None
|
||||
page = request.args.get('page', 1, type=int)
|
||||
page_size = request.args.get('page_size', 200, type=int)
|
||||
result = db.query_all(page=page, page_size=page_size, keyword=keyword)
|
||||
records = [_records_to_spa(r) for r in result.get('data', [])]
|
||||
return jsonify({'success': True, 'records': records,
|
||||
'total': result.get('total', len(records))})
|
||||
except Exception as e:
|
||||
logger.error("查询记录列表失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e), 'records': []}), 500
|
||||
|
||||
|
||||
@db_api_bp.route('/api/db/records', methods=['POST'])
|
||||
def db_records_add_spa():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
user_id = (payload.get('user_id') or '').strip()
|
||||
user_name = (payload.get('user_name') or '').strip()
|
||||
is_signature = payload.get('is_signature', False)
|
||||
image_b64 = payload.get('image_base64') or ''
|
||||
|
||||
valid, msg = validate_user_id(user_id)
|
||||
if not valid:
|
||||
return jsonify({'success': False, 'message': msg}), 400
|
||||
valid, msg = validate_user_name(user_name)
|
||||
if not valid:
|
||||
return jsonify({'success': False, 'message': msg}), 400
|
||||
if not image_b64:
|
||||
return jsonify({'success': False, 'message': '缺少图片'}), 400
|
||||
|
||||
try:
|
||||
image_data = base64.b64decode(image_b64)
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f'图片 Base64 解码失败: {e}'}), 400
|
||||
|
||||
limits = get_limits_config()
|
||||
valid, msg = validate_image_size(image_data, limits['image_max_bytes'])
|
||||
if not valid:
|
||||
return jsonify({'success': False, 'message': msg}), 400
|
||||
|
||||
is_sig = 1 if is_signature else 0
|
||||
try:
|
||||
db.insert_record(user_id, user_name, image_data, is_sig)
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 400
|
||||
except Exception as e:
|
||||
logger.error("新增记录失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
return jsonify({'success': True,
|
||||
'record': {'user_id': user_id, 'user_name': user_name,
|
||||
'is_signature': bool(is_sig)}})
|
||||
|
||||
|
||||
@db_api_bp.route('/api/db/records/<user_id>', methods=['PUT'])
|
||||
def db_records_update_spa(user_id):
|
||||
payload = request.get_json(silent=True) or {}
|
||||
user_name = payload.get('user_name')
|
||||
is_signature = payload.get('is_signature')
|
||||
image_b64 = payload.get('image_base64')
|
||||
|
||||
sign_image = None
|
||||
if image_b64:
|
||||
try:
|
||||
sign_image = base64.b64decode(image_b64)
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f'图片 Base64 解码失败: {e}'}), 400
|
||||
limits = get_limits_config()
|
||||
valid, msg = validate_image_size(sign_image, limits['image_max_bytes'])
|
||||
if not valid:
|
||||
return jsonify({'success': False, 'message': msg}), 400
|
||||
|
||||
if user_name is not None:
|
||||
valid, msg = validate_user_name(user_name)
|
||||
if not valid:
|
||||
return jsonify({'success': False, 'message': msg}), 400
|
||||
|
||||
is_sig_val = None
|
||||
if is_signature is not None:
|
||||
is_sig_val = 1 if is_signature else 0
|
||||
|
||||
try:
|
||||
db.update_record(user_id, user_name=user_name, sign_image=sign_image, is_signature=is_sig_val)
|
||||
except Exception as e:
|
||||
logger.error("更新记录失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
return jsonify({'success': True,
|
||||
'record': {'user_id': user_id, 'user_name': user_name,
|
||||
'is_signature': bool(is_sig_val) if is_sig_val is not None else None}})
|
||||
|
||||
|
||||
@db_api_bp.route('/api/db/records/<user_id>', methods=['DELETE'])
|
||||
def db_records_delete_spa(user_id):
|
||||
try:
|
||||
db.delete_record(user_id)
|
||||
return jsonify({'success': True})
|
||||
except Exception as e:
|
||||
logger.error("删除记录失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@db_api_bp.route('/api/db/records/import', methods=['POST'])
|
||||
def db_records_import_spa():
|
||||
"""SPA 批量导入:JSON + Base64。
|
||||
|
||||
返回结构需让前端能识别失败条目(含 index)以便保留待二次处理。
|
||||
"""
|
||||
payload = request.get_json(silent=True) or {}
|
||||
items = payload.get('items') or []
|
||||
id_prefix = (payload.get('auto_id_prefix') or 'U').strip() or 'U'
|
||||
id_start = int(payload.get('auto_id_start') or 1)
|
||||
id_padding = max(1, min(int(payload.get('auto_id_padding') or 4), 20))
|
||||
default_is_signature = bool(payload.get('default_is_signature', False))
|
||||
default_is_sig = 1 if default_is_signature else 0
|
||||
|
||||
limits = get_limits_config()
|
||||
imported = 0
|
||||
failed = 0
|
||||
failures = []
|
||||
|
||||
for idx, item in enumerate(items):
|
||||
filename = item.get('image_filename') or f"{id_prefix}{idx + 1:0{id_padding}d}.png"
|
||||
name_part = filename.rsplit('.', 1)[0] if '.' in filename else filename
|
||||
user_name = item.get('user_name') or name_part
|
||||
user_id = f"{id_prefix}{(id_start + idx):0{id_padding}d}"
|
||||
is_signature = item.get('is_signature', default_is_signature)
|
||||
is_sig = 1 if is_signature else 0
|
||||
image_b64 = item.get('image_base64') or ''
|
||||
|
||||
if not image_b64:
|
||||
failed += 1
|
||||
failures.append({'index': idx, 'code': 'MISSING_IMAGE',
|
||||
'message': '缺少图片 Base64', 'filename': filename})
|
||||
continue
|
||||
try:
|
||||
image_data = base64.b64decode(image_b64)
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
failures.append({'index': idx, 'code': 'INVALID_BASE64',
|
||||
'message': f'Base64 解码失败: {e}', 'filename': filename})
|
||||
continue
|
||||
|
||||
valid, msg = validate_image_size(image_data, limits['image_max_bytes'])
|
||||
if not valid:
|
||||
failed += 1
|
||||
failures.append({'index': idx, 'code': 'IMAGE_TOO_LARGE',
|
||||
'message': msg, 'filename': filename})
|
||||
continue
|
||||
|
||||
try:
|
||||
db.insert_record(user_id, user_name, image_data, is_sig)
|
||||
imported += 1
|
||||
except ValueError as e:
|
||||
failed += 1
|
||||
failures.append({'index': idx, 'code': 'DUPLICATE',
|
||||
'message': str(e), 'filename': filename})
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
failures.append({'index': idx, 'code': 'ERROR',
|
||||
'message': str(e), 'filename': filename})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'imported': imported,
|
||||
'failed': failed,
|
||||
'failures': failures,
|
||||
})
|
||||
|
||||
|
||||
@db_api_bp.route('/api/db/records/<user_id>/image', methods=['GET'])
|
||||
def db_records_image_spa(user_id):
|
||||
try:
|
||||
record = db.query_by_user_id(user_id)
|
||||
if not record:
|
||||
return jsonify({'success': False, 'message': '未找到记录'}), 404
|
||||
processed = request.args.get('processed') in ('1', 'true', 'True', 'yes')
|
||||
image_data = record['sign_image']
|
||||
if processed:
|
||||
if bool(record.get('is_signature')):
|
||||
payload = preview_binarize(image_data)
|
||||
else:
|
||||
payload = preview_remove_white(image_data)
|
||||
else:
|
||||
payload = image_data
|
||||
return Response(payload, mimetype='image/png')
|
||||
except Exception as e:
|
||||
logger.error("获取图片失败: %s", str(e))
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
24
routes/monitor_api.py
Normal file
24
routes/monitor_api.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import logging
|
||||
from flask import Blueprint, request, jsonify
|
||||
|
||||
from lib import monitor
|
||||
|
||||
monitor_api_bp = Blueprint('monitor_api', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@monitor_api_bp.route('/api/monitor/requests', methods=['GET'])
|
||||
def monitor_requests():
|
||||
status = request.args.get('status') or None
|
||||
keyword = request.args.get('q') or None
|
||||
limit = request.args.get('limit', 200)
|
||||
items = monitor.query_requests(status=status, keyword=keyword, limit=limit)
|
||||
return jsonify({'success': True, 'requests': items})
|
||||
|
||||
|
||||
@monitor_api_bp.route('/api/monitor/requests/<trace_id>', methods=['GET'])
|
||||
def monitor_request_detail(trace_id):
|
||||
entry = monitor.get_request(trace_id)
|
||||
if not entry:
|
||||
return jsonify({'success': False, 'message': '未找到 trace_id 对应记录'}), 404
|
||||
return jsonify({'success': True, 'request': entry})
|
||||
85
routes/pages.py
Normal file
85
routes/pages.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from flask import Blueprint, render_template, request, Response, stream_with_context, jsonify
|
||||
from lib.log_handler import subscribe, unsubscribe, get_all_logs, query_logs
|
||||
|
||||
pages_bp = Blueprint('pages', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pages_bp.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
@pages_bp.route('/db-manager')
|
||||
def db_manager():
|
||||
return render_template('db_manager.html')
|
||||
|
||||
|
||||
@pages_bp.route('/logs')
|
||||
def logs_page():
|
||||
return render_template('logs.html')
|
||||
|
||||
|
||||
@pages_bp.route('/api/logs/stream')
|
||||
def logs_stream():
|
||||
sub_id, q = subscribe()
|
||||
|
||||
def generate():
|
||||
try:
|
||||
for entry in get_all_logs():
|
||||
yield f"data: {json.dumps(entry, ensure_ascii=False)}\n\n"
|
||||
while True:
|
||||
if q:
|
||||
while q:
|
||||
entry = q.popleft()
|
||||
yield f"data: {json.dumps(entry, ensure_ascii=False)}\n\n"
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
except GeneratorExit:
|
||||
pass
|
||||
finally:
|
||||
unsubscribe(sub_id)
|
||||
|
||||
return Response(
|
||||
stream_with_context(generate()),
|
||||
mimetype='text/event-stream',
|
||||
headers={
|
||||
'Cache-Control': 'no-cache',
|
||||
'X-Accel-Buffering': 'no',
|
||||
'Connection': 'keep-alive',
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pages_bp.route('/api/logs', methods=['GET'])
|
||||
def logs_poll():
|
||||
"""SPA 兼容的轮询接口:按 after_id 增量返回,并支持 level/source/q 过滤。"""
|
||||
after_id = request.args.get('after_id', 0)
|
||||
limit = request.args.get('limit', 200)
|
||||
level = request.args.get('level') or None
|
||||
source = request.args.get('source') or None
|
||||
keyword = request.args.get('q') or None
|
||||
result = query_logs(after_id=after_id, level=level, source=source,
|
||||
keyword=keyword, limit=limit)
|
||||
return jsonify({'success': True, **result})
|
||||
|
||||
|
||||
@pages_bp.route('/api/logs/history')
|
||||
def logs_history():
|
||||
source = request.args.get('source', 'all')
|
||||
level = request.args.get('level', 'all')
|
||||
keyword = request.args.get('keyword', '')
|
||||
|
||||
logs = get_all_logs()
|
||||
|
||||
if source != 'all':
|
||||
logs = [l for l in logs if l.get('source') == source]
|
||||
if level != 'all':
|
||||
logs = [l for l in logs if l.get('level') == level]
|
||||
if keyword:
|
||||
logs = [l for l in logs if keyword.lower() in l.get('message', '').lower()]
|
||||
|
||||
return jsonify({'success': True, 'data': logs})
|
||||
Reference in New Issue
Block a user