438 lines
17 KiB
Python
438 lines
17 KiB
Python
|
|
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
|