2026-07-16 14:00:28 +08:00
|
|
|
|
"""
|
|
|
|
|
|
批量导入 API
|
|
|
|
|
|
|
|
|
|
|
|
端点:
|
|
|
|
|
|
GET /api/v1/import/template?type=material|bom → 下载Excel模板
|
|
|
|
|
|
POST /api/v1/import/preview → Dry-Run预览+验证
|
|
|
|
|
|
POST /api/v1/import/execute → 确认导入执行
|
|
|
|
|
|
"""
|
|
|
|
|
|
from flask import Blueprint, request, jsonify, send_file, current_app
|
|
|
|
|
|
from flask_jwt_extended import jwt_required
|
|
|
|
|
|
from app.utils.decorators import permission_required
|
|
|
|
|
|
from app.services.import_service import (
|
|
|
|
|
|
generate_template,
|
|
|
|
|
|
preview_material, preview_bom,
|
|
|
|
|
|
execute_material_import, execute_bom_import,
|
|
|
|
|
|
)
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
|
|
|
|
import_bp = Blueprint('import_bp', __name__, url_prefix='/api/v1/import')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@import_bp.route('/template', methods=['GET'])
|
|
|
|
|
|
@jwt_required()
|
|
|
|
|
|
@permission_required('material_list')
|
|
|
|
|
|
def download_template():
|
|
|
|
|
|
"""下载导入模板"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
import_type = request.args.get('type', 'material').strip()
|
|
|
|
|
|
if import_type not in ('material', 'bom'):
|
|
|
|
|
|
return jsonify({'code': 400, 'msg': 'type 必须为 material 或 bom'}), 400
|
|
|
|
|
|
|
|
|
|
|
|
file_stream = generate_template(import_type)
|
|
|
|
|
|
filename = f"{'基础信息' if import_type == 'material' else 'BOM表'}_导入模板.xlsx"
|
|
|
|
|
|
|
|
|
|
|
|
return send_file(
|
|
|
|
|
|
file_stream,
|
|
|
|
|
|
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
|
|
|
|
as_attachment=True,
|
|
|
|
|
|
download_name=filename
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
current_app.logger.error(f'下载模板失败: {str(e)}')
|
|
|
|
|
|
return jsonify({'code': 500, 'msg': str(e)}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@import_bp.route('/preview', methods=['POST'])
|
|
|
|
|
|
@jwt_required()
|
|
|
|
|
|
@permission_required('material_list:operation')
|
|
|
|
|
|
def preview_import():
|
2026-08-03 15:53:15 +08:00
|
|
|
|
"""Dry-Run 预览 + 验证(不写入数据库)
|
|
|
|
|
|
|
|
|
|
|
|
FormData:
|
|
|
|
|
|
type: 'material' | 'bom'
|
|
|
|
|
|
file: Excel 文件
|
|
|
|
|
|
mode: 'skip' (默认) | 'update' — update 模式下 DB 重复行显示为「将被更新」而非错误
|
|
|
|
|
|
"""
|
2026-07-16 14:00:28 +08:00
|
|
|
|
try:
|
|
|
|
|
|
import_type = request.form.get('type', 'material').strip()
|
2026-08-03 15:53:15 +08:00
|
|
|
|
mode = request.form.get('mode', 'skip').strip()
|
2026-07-16 14:00:28 +08:00
|
|
|
|
file = request.files.get('file')
|
|
|
|
|
|
if not file:
|
|
|
|
|
|
return jsonify({'code': 400, 'msg': '请上传文件'}), 400
|
|
|
|
|
|
if import_type not in ('material', 'bom'):
|
|
|
|
|
|
return jsonify({'code': 400, 'msg': 'type 必须为 material 或 bom'}), 400
|
|
|
|
|
|
|
|
|
|
|
|
file_stream = file.read()
|
|
|
|
|
|
from io import BytesIO
|
|
|
|
|
|
|
|
|
|
|
|
if import_type == 'material':
|
2026-08-03 15:53:15 +08:00
|
|
|
|
results = preview_material(BytesIO(file_stream), mode=mode)
|
2026-07-16 14:00:28 +08:00
|
|
|
|
else:
|
|
|
|
|
|
results = preview_bom(BytesIO(file_stream))
|
|
|
|
|
|
|
2026-08-03 15:53:15 +08:00
|
|
|
|
success_count = sum(1 for r in results if r['status'] in ('success', 'update'))
|
2026-07-16 14:00:28 +08:00
|
|
|
|
error_count = sum(1 for r in results if r['status'] == 'error')
|
|
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
'code': 200, 'msg': '预览完成',
|
|
|
|
|
|
'data': {
|
|
|
|
|
|
'rows': results,
|
|
|
|
|
|
'total': len(results),
|
|
|
|
|
|
'success_count': success_count,
|
|
|
|
|
|
'error_count': error_count,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
return jsonify({'code': 500, 'msg': f'预览失败: {str(e)}'}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@import_bp.route('/execute', methods=['POST'])
|
|
|
|
|
|
@jwt_required()
|
|
|
|
|
|
@permission_required('material_list:operation')
|
|
|
|
|
|
def execute_import():
|
2026-08-03 15:53:15 +08:00
|
|
|
|
"""确认导入执行
|
|
|
|
|
|
|
|
|
|
|
|
Body:
|
|
|
|
|
|
type: 'material' | 'bom'
|
|
|
|
|
|
rows: 待导入行列表
|
|
|
|
|
|
mode: 'skip' (跳过重复,默认) | 'update' (覆盖更新已有记录)
|
|
|
|
|
|
"""
|
2026-07-16 14:00:28 +08:00
|
|
|
|
try:
|
|
|
|
|
|
data = request.get_json() or {}
|
|
|
|
|
|
import_type = data.get('type', 'material').strip()
|
|
|
|
|
|
rows = data.get('rows', [])
|
2026-08-03 15:53:15 +08:00
|
|
|
|
mode = data.get('mode', 'skip').strip()
|
2026-07-16 14:00:28 +08:00
|
|
|
|
|
|
|
|
|
|
if not rows:
|
|
|
|
|
|
return jsonify({'code': 400, 'msg': '导入数据不能为空'}), 400
|
|
|
|
|
|
if import_type not in ('material', 'bom'):
|
|
|
|
|
|
return jsonify({'code': 400, 'msg': 'type 必须为 material 或 bom'}), 400
|
2026-08-03 15:53:15 +08:00
|
|
|
|
if mode not in ('skip', 'update'):
|
|
|
|
|
|
return jsonify({'code': 400, 'msg': 'mode 必须为 skip 或 update'}), 400
|
2026-07-16 14:00:28 +08:00
|
|
|
|
|
2026-08-03 15:53:15 +08:00
|
|
|
|
# 仅导入状态为 success 或 update 的行,error 行不参与导入
|
|
|
|
|
|
valid_rows = [r for r in rows if r.get('status') in ('success', 'update')]
|
2026-07-16 14:00:28 +08:00
|
|
|
|
if not valid_rows:
|
2026-08-03 15:53:15 +08:00
|
|
|
|
return jsonify({'code': 400, 'msg': '没有可导入的有效数据'}), 400
|
2026-07-16 14:00:28 +08:00
|
|
|
|
|
|
|
|
|
|
if import_type == 'material':
|
2026-08-03 15:53:15 +08:00
|
|
|
|
result = execute_material_import(valid_rows, mode=mode)
|
2026-07-16 14:00:28 +08:00
|
|
|
|
else:
|
|
|
|
|
|
result = execute_bom_import(valid_rows)
|
|
|
|
|
|
|
|
|
|
|
|
code = 200 if result['success'] else 400
|
|
|
|
|
|
return jsonify({'code': code, 'msg': result['msg'], 'data': result})
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
return jsonify({'code': 500, 'msg': f'导入失败: {str(e)}'}), 500
|