采购件图像上传初实现

This commit is contained in:
dxc
2026-02-03 11:16:12 +08:00
parent efcd2d923c
commit 7fa40115d9
7 changed files with 510 additions and 91 deletions

View File

@ -3,7 +3,7 @@
from flask import Flask
from config import Config
from app.extensions import db, migrate, cors
import os
def create_app():
app = Flask(__name__)
@ -14,7 +14,8 @@ def create_app():
migrate.init_app(app, db)
# 确保跨域配置
cors.init_app(app, resources={r"/api/*": {"origins": "*"}})
# 允许 /api/ 开头的请求跨域
cors.init_app(app, resources={r"/*": {"origins": "*"}}) # 放宽跨域限制,防止图片访问被拦截
# =========================================================
# 2. 注册蓝图 (Blueprints)
@ -25,12 +26,9 @@ def create_app():
# -----------------------------------------------------
try:
# 指向聚合文件: app/api/v1/inbound/__init__.py
# 该文件里应该包含了 buy, semi, base, product 的聚合逻辑
from app.api.v1.inbound import inbound_bp
# 注册父蓝图,路由前缀为 /api/v1/inbound
# 最终路由效果:
# /api/v1/inbound + /buy/list -> /api/v1/inbound/buy/list
app.register_blueprint(inbound_bp, url_prefix='/api/v1/inbound')
print("✅ Inbound (Buy, Semi, Product, Base) 模块注册成功")
@ -39,14 +37,12 @@ def create_app():
print(f"❌ 错误: Inbound 模块导入失败: {e}")
# -----------------------------------------------------
# 2.2 注册通用打印模块 (Common Print) - [新增]
# 2.2 注册通用打印模块 (Common Print)
# -----------------------------------------------------
try:
from app.api.v1.common.print import print_bp
# 注册打印蓝图
# 前端请求地址: /common/print/preview
# 配合 baseURL=/api/v1最终对应后端: /api/v1/common/print/preview
app.register_blueprint(print_bp, url_prefix='/api/v1/common/print')
print("✅ Print (Label Printing) 模块注册成功")
@ -54,6 +50,25 @@ def create_app():
except ImportError as e:
print(f"❌ 错误: Print 模块导入失败: {e}")
# -----------------------------------------------------
# 2.3 [新增] 注册通用上传模块 (Common Upload)
# -----------------------------------------------------
try:
from app.api.v1.common.upload import upload_bp
# 【核心修改】注册方式 1: 标准路径 (对应 /api/v1/common/files/xxx)
app.register_blueprint(upload_bp, url_prefix='/api/v1/common')
# 【核心修改】注册方式 2: 兼容路径 (对应 /v1/common/files/xxx)
# 解决部分代理服务器剥离 /api 前缀导致的 404 问题
# name='upload_fallback' 防止蓝图名称冲突
app.register_blueprint(upload_bp, url_prefix='/v1/common', name='upload_fallback')
print("✅ Upload (File Storage) 模块注册成功 (双路径兼容模式)")
except ImportError as e:
print(f"❌ 错误: Upload 模块导入失败: {e}")
# =========================================================
# 3. 预加载数据模型 (解决 relationship 找不到模型的问题)
# =========================================================

View File

@ -0,0 +1,132 @@
# 文件路径: inventory-backend/app/api/v1/common/upload.py
import os
import uuid
from flask import Blueprint, request, jsonify, send_from_directory
# 定义蓝图
upload_bp = Blueprint('upload', __name__)
# =========================================================
# 配置上传路径 (核心修改:确保路径绝对准确)
# =========================================================
# 向上寻找直到找到 inventory-backend 目录,或者默认为当前文件的上级目录的...上级
# 这种方式比数 dirname 层级更稳健
def get_project_root():
"""获取项目根目录 inventory-backend"""
current_path = os.path.abspath(__file__)
# 循环向上查找,直到找到名为 inventory-backend 的目录
# 如果你的根目录名字不是 inventory-backend请修改这里的判断逻辑
# 或者直接使用相对路径回退 5 层: api/v1/common -> app -> inventory-backend
base = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_path)))))
return base
BASE_DIR = get_project_root()
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
# 允许上传的文件后缀
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'}
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def ensure_upload_folder_exists():
if not os.path.exists(UPLOAD_FOLDER):
try:
os.makedirs(UPLOAD_FOLDER)
print(f"✅ [Upload] 目录创建成功: {UPLOAD_FOLDER}")
except Exception as e:
print(f"❌ [Upload] 目录创建失败: {e}")
# ------------------------------------------------------------------
# 1. 文件上传接口
# URL: /api/v1/common/upload (POST)
# ------------------------------------------------------------------
@upload_bp.route('/upload', methods=['POST'])
def upload_file():
ensure_upload_folder_exists()
if 'file' not in request.files:
return jsonify({"code": 400, "msg": "未找到文件部分"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"code": 400, "msg": "未选择文件"}), 400
if file and allowed_file(file.filename):
try:
ext = file.filename.rsplit('.', 1)[1].lower()
new_filename = f"{uuid.uuid4().hex}.{ext}"
save_path = os.path.join(UPLOAD_FOLDER, new_filename)
file.save(save_path)
print(f"💾 [Upload] 文件已保存: {save_path}")
# 生成访问 URL
# 这里的路径必须与 __init__.py 中注册的 url_prefix + 路由匹配
file_url = f"/api/v1/common/files/{new_filename}"
return jsonify({
"code": 200,
"msg": "上传成功",
"data": {
"url": file_url,
"filename": new_filename
}
})
except Exception as e:
print(f"❌ [Upload] 保存异常: {e}")
return jsonify({"code": 500, "msg": "文件保存失败"}), 500
return jsonify({"code": 400, "msg": "不支持的文件格式"}), 400
# ------------------------------------------------------------------
# 2. 静态文件访问接口 (回显)
# URL: /api/v1/common/files/<filename>
# ------------------------------------------------------------------
@upload_bp.route('/files/<filename>')
def uploaded_file(filename):
# 打印日志帮助调试 404 问题
full_path = os.path.join(UPLOAD_FOLDER, filename)
if not os.path.exists(full_path):
print(f"❌ [File Access] 文件未找到: {full_path}")
return jsonify({"code": 404, "msg": "文件不存在"}), 404
return send_from_directory(UPLOAD_FOLDER, filename)
# ------------------------------------------------------------------
# 3. 文件删除接口 (同步删除物理文件)
# URL: /api/v1/common/files/<filename> (DELETE)
# ------------------------------------------------------------------
@upload_bp.route('/files/<filename>', methods=['DELETE'])
def delete_file(filename):
try:
# 安全处理文件名
safe_filename = os.path.basename(filename)
file_path = os.path.join(UPLOAD_FOLDER, safe_filename)
print(f"🗑️ [Delete] 尝试删除文件: {file_path}")
if os.path.exists(file_path):
os.remove(file_path)
print(f"✅ [Delete] 文件删除成功")
return jsonify({"code": 200, "msg": "文件已删除"})
else:
print(f"⚠️ [Delete] 文件不存在,无需删除")
# 即使文件不存在也返回成功,保证前端流程继续
return jsonify({"code": 200, "msg": "文件不存在或已删除"})
except Exception as e:
print(f"❌ [Delete] 删除异常: {e}")
return jsonify({"code": 500, "msg": f"删除失败: {str(e)}"}), 500

View File

@ -36,13 +36,16 @@ class StockBuy(db.Model):
exchange_rate = db.Column(db.Numeric(15, 6), default=1.0)
supplier_name = db.Column(db.String(255))
buyer_name = db.Column(db.String(100)) # 对应 SQL: buyer_name
buyer_email = db.Column(db.String(100)) # 对应 SQL: buyer_email
original_link = db.Column(db.Text) # 对应 SQL: original_link
buyer_name = db.Column(db.String(100))
buyer_email = db.Column(db.String(100))
original_link = db.Column(db.Text)
detail_link = db.Column(db.Text)
arrival_photo = db.Column(db.Text)
# [新增] 全局打印流水号 (用于跨表连续编号,对应 Sequence: global_print_seq)
# [新增] 检测报告图片路径
inspection_report = db.Column(db.Text)
# 全局打印流水号
global_print_id = db.Column(db.Integer)
# 关系定义
@ -86,7 +89,9 @@ class StockBuy(db.Model):
'detail_link': self.detail_link,
'arrival_photo': self.arrival_photo,
# [新增] 返回全局打印ID及其格式化字符串
# [新增] 返回检测报告字段
'inspection_report': self.inspection_report,
'global_print_id': self.global_print_id,
'global_print_id_str': f"{self.global_print_id:010d}" if self.global_print_id else ""
}

View File

@ -82,9 +82,8 @@ class BuyInboundService:
generated_sku = str(next_global_id).zfill(10)
# ------------------------------------------------------------------
# 3. 条码逻辑处理 (核心修改)
# 3. 条码逻辑处理
# 如果前端没传条码(barcode),则默认使用系统生成的 SKU 作为条码
# 这样保证了条码生成依据是 "自动填写的 SKU"
# ------------------------------------------------------------------
final_barcode = data.get('barcode')
if not final_barcode:
@ -93,8 +92,8 @@ class BuyInboundService:
new_stock = StockBuy(
base_id=material.id,
global_print_id=next_global_id,
sku=generated_sku, # 自动生成的SKU
barcode=final_barcode, # 如果未输入则存入SKU值
sku=generated_sku,
barcode=final_barcode,
in_date=in_date_val,
serial_number=data.get('serial_number'),
@ -114,13 +113,15 @@ class BuyInboundService:
buyer_email=data.get('purchaser_email'),
original_link=data.get('source_link'),
detail_link=data.get('detail_link'),
arrival_photo=data.get('arrival_photo')
arrival_photo=data.get('arrival_photo'),
# [新增] 保存检测报告字段
inspection_report=data.get('inspection_report')
)
db.session.add(new_stock)
db.session.commit()
# 返回创建的对象实例
return new_stock
except Exception as e:
@ -151,7 +152,10 @@ class BuyInboundService:
'exchange_rate': 'exchange_rate',
'purchaser': 'buyer_name',
'purchaser_email': 'buyer_email',
'source_link': 'original_link'
'source_link': 'original_link',
# [新增] 允许更新检测报告
'inspection_report': 'inspection_report'
}
for frontend_key, db_attr in field_mapping.items():
@ -207,7 +211,6 @@ class BuyInboundService:
@staticmethod
def get_list(page, limit, keyword=None):
try:
# 1. 查询分页数据
query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id)
if keyword:
@ -223,9 +226,6 @@ class BuyInboundService:
pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False)
# ---------------------------------------------------------------------
# 计算总库存 (聚合)
# ---------------------------------------------------------------------
current_items = pagination.items
base_ids = list(set([item.base_id for item in current_items if item.base_id]))
@ -289,6 +289,9 @@ class BuyInboundService:
'detail_link': item.detail_link,
'arrival_photo': item.arrival_photo,
# [新增] 返回检测报告
'inspection_report': item.inspection_report,
'global_print_id': item.global_print_id,
'global_print_id_str': f"{item.global_print_id:08d}" if item.global_print_id else ""
}