针对于上传图片以及借库还库和出库选单进行更改
This commit is contained in:
@ -5,21 +5,18 @@ import uuid
|
||||
from flask import Blueprint, request, jsonify, send_from_directory
|
||||
|
||||
# 定义蓝图
|
||||
# 注意:在 app/__init__.py 或类似入口文件中,注册此蓝图时 url_prefix 通常应为 '/api/v1/common'
|
||||
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
|
||||
# 向上回退直到找到根目录,根据你的目录结构可能需要调整层级
|
||||
# 假设结构: inventory-backend/app/api/v1/common/upload.py (回退5层)
|
||||
base = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_path)))))
|
||||
return base
|
||||
|
||||
@ -28,7 +25,7 @@ BASE_DIR = get_project_root()
|
||||
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
|
||||
|
||||
# 允许上传的文件后缀
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'}
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'pdf', 'doc', 'docx', 'xls', 'xlsx'}
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
@ -47,7 +44,7 @@ def ensure_upload_folder_exists():
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 1. 文件上传接口
|
||||
# URL: /api/v1/common/upload (POST)
|
||||
# 完整 URL: /api/v1/common/upload (POST)
|
||||
# ------------------------------------------------------------------
|
||||
@upload_bp.route('/upload', methods=['POST'])
|
||||
def upload_file():
|
||||
@ -63,6 +60,7 @@ def upload_file():
|
||||
|
||||
if file and allowed_file(file.filename):
|
||||
try:
|
||||
# 获取后缀并生成唯一文件名
|
||||
ext = file.filename.rsplit('.', 1)[1].lower()
|
||||
new_filename = f"{uuid.uuid4().hex}.{ext}"
|
||||
|
||||
@ -71,8 +69,9 @@ def upload_file():
|
||||
|
||||
print(f"💾 [Upload] 文件已保存: {save_path}")
|
||||
|
||||
# 生成访问 URL
|
||||
# 这里的路径必须与 __init__.py 中注册的 url_prefix + 路由匹配
|
||||
# 生成访问 URL (返回给前端的相对路径)
|
||||
# 前端展示时通常拼接 baseURL,或者直接使用此路径访问
|
||||
# 这里的 /api/v1/common 需与蓝图注册路径一致
|
||||
file_url = f"/api/v1/common/files/{new_filename}"
|
||||
|
||||
return jsonify({
|
||||
@ -92,13 +91,13 @@ def upload_file():
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2. 静态文件访问接口 (回显)
|
||||
# URL: /api/v1/common/files/<filename>
|
||||
# 完整 URL: /api/v1/common/files/<filename> (GET)
|
||||
# ------------------------------------------------------------------
|
||||
@upload_bp.route('/files/<filename>')
|
||||
@upload_bp.route('/files/<filename>', methods=['GET'])
|
||||
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
|
||||
|
||||
@ -107,12 +106,12 @@ def uploaded_file(filename):
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 3. 文件删除接口 (同步删除物理文件)
|
||||
# URL: /api/v1/common/files/<filename> (DELETE)
|
||||
# 完整 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)
|
||||
|
||||
@ -124,7 +123,7 @@ def delete_file(filename):
|
||||
return jsonify({"code": 200, "msg": "文件已删除"})
|
||||
else:
|
||||
print(f"⚠️ [Delete] 文件不存在,无需删除")
|
||||
# 即使文件不存在也返回成功,保证前端流程继续
|
||||
# 即使文件不存在也返回成功,保证前端逻辑闭环
|
||||
return jsonify({"code": 200, "msg": "文件不存在或已删除"})
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
# app/models/base.py
|
||||
from app.extensions import db
|
||||
import json
|
||||
|
||||
class MaterialBase(db.Model):
|
||||
"""
|
||||
@ -11,7 +12,7 @@ class MaterialBase(db.Model):
|
||||
# 1. 基础字段
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(255), nullable=False, comment='名称')
|
||||
common_name = db.Column(db.String(255), comment='俗名') # ✅ 新增字段
|
||||
common_name = db.Column(db.String(255), comment='俗名')
|
||||
category = db.Column(db.String(100), comment='类别')
|
||||
material_type = db.Column(db.String(100), comment='类型')
|
||||
spec_model = db.Column(db.String(255), comment='规格型号')
|
||||
@ -20,7 +21,7 @@ class MaterialBase(db.Model):
|
||||
# 可见等级
|
||||
visibility_level = db.Column(db.Integer, default=0, comment='信息可见等级')
|
||||
|
||||
# 链接与图片
|
||||
# 链接与图片 (现在存储 JSON 字符串)
|
||||
manual_link = db.Column(db.Text, comment='通用说明书')
|
||||
product_image = db.Column(db.Text, comment='通用产品图')
|
||||
|
||||
@ -44,16 +45,29 @@ class MaterialBase(db.Model):
|
||||
"""
|
||||
序列化方法
|
||||
"""
|
||||
# 辅助解析函数:将数据库存储的 JSON 字符串转为 List
|
||||
def parse_list(json_str):
|
||||
if not json_str:
|
||||
return []
|
||||
try:
|
||||
# 兼容旧数据:如果不是 JSON 格式(比如是单个 URL),则包装成 list
|
||||
if not json_str.startswith('['):
|
||||
return [json_str]
|
||||
return json.loads(json_str)
|
||||
except:
|
||||
return []
|
||||
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'commonName': self.common_name, # ✅ 序列化新增字段
|
||||
'commonName': self.common_name,
|
||||
'category': self.category,
|
||||
'type': self.material_type, # 前端字段映射
|
||||
'spec': self.spec_model, # 前端字段映射
|
||||
'type': self.material_type,
|
||||
'spec': self.spec_model,
|
||||
'unit': self.unit,
|
||||
'visibilityLevel': self.visibility_level,
|
||||
'generalManual': self.manual_link,
|
||||
'generalImage': self.product_image,
|
||||
# 修改:解析为列表返回
|
||||
'generalManual': parse_list(self.manual_link),
|
||||
'generalImage': parse_list(self.product_image),
|
||||
'isEnabled': 1 if self.is_enabled else 0,
|
||||
}
|
||||
@ -6,6 +6,7 @@ from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.semi import StockSemi
|
||||
from sqlalchemy import or_
|
||||
import traceback
|
||||
import json
|
||||
|
||||
|
||||
class MaterialBaseService:
|
||||
@ -24,7 +25,6 @@ class MaterialBaseService:
|
||||
if not keyword:
|
||||
return []
|
||||
|
||||
# ✅ 搜索范围增加 common_name (俗名)
|
||||
query = MaterialBase.query.filter(
|
||||
MaterialBase.is_enabled == True,
|
||||
or_(
|
||||
@ -39,7 +39,7 @@ class MaterialBaseService:
|
||||
results.append({
|
||||
'id': item.id,
|
||||
'name': item.name,
|
||||
'commonName': item.common_name, # ✅ 返回俗名
|
||||
'commonName': item.common_name,
|
||||
'spec': item.spec_model,
|
||||
'category': item.category,
|
||||
'unit': item.unit,
|
||||
@ -63,7 +63,6 @@ class MaterialBaseService:
|
||||
# 1. 关键词模糊搜索 (名称 或 俗名 或 规格型号)
|
||||
if filters.get('keyword'):
|
||||
kw = f"%{filters['keyword']}%"
|
||||
# ✅ 增加俗名搜索
|
||||
query = query.filter(or_(
|
||||
MaterialBase.name.ilike(kw),
|
||||
MaterialBase.common_name.ilike(kw),
|
||||
@ -100,8 +99,7 @@ class MaterialBaseService:
|
||||
if not data.get('name') or not data.get('spec'):
|
||||
raise ValueError("名称和规格型号不能为空")
|
||||
|
||||
# 1. 查重 (名称+规格型号 唯一)
|
||||
# 注意:俗名不参与唯一性校验,允许重复或为空
|
||||
# 1. 查重
|
||||
exist = MaterialBase.query.filter_by(
|
||||
name=data['name'],
|
||||
spec_model=data['spec']
|
||||
@ -109,17 +107,18 @@ class MaterialBaseService:
|
||||
if exist:
|
||||
raise ValueError(f"已存在相同名称和规格的数据 (ID: {exist.id})")
|
||||
|
||||
# 2. 创建对象
|
||||
# 2. 创建对象 (列表转JSON字符串)
|
||||
new_material = MaterialBase(
|
||||
name=data['name'],
|
||||
common_name=data.get('commonName'), # ✅ 读取俗名
|
||||
common_name=data.get('commonName'),
|
||||
spec_model=data['spec'],
|
||||
category=data.get('category'),
|
||||
material_type=data.get('type'),
|
||||
unit=data.get('unit'),
|
||||
visibility_level=data.get('visibilityLevel'),
|
||||
manual_link=data.get('generalManual'),
|
||||
product_image=data.get('generalImage'),
|
||||
# 修改:将列表 dumps 为字符串
|
||||
manual_link=json.dumps(data.get('generalManual', [])),
|
||||
product_image=json.dumps(data.get('generalImage', [])),
|
||||
is_enabled=True if data.get('isEnabled', 1) == 1 else False
|
||||
)
|
||||
|
||||
@ -141,14 +140,18 @@ class MaterialBaseService:
|
||||
|
||||
# 更新字段
|
||||
if 'name' in data: material.name = data['name']
|
||||
if 'commonName' in data: material.common_name = data['commonName'] # ✅ 更新俗名
|
||||
if 'commonName' in data: material.common_name = data['commonName']
|
||||
if 'spec' in data: material.spec_model = data['spec']
|
||||
if 'category' in data: material.category = data['category']
|
||||
if 'type' in data: material.material_type = data['type']
|
||||
if 'unit' in data: material.unit = data['unit']
|
||||
if 'visibilityLevel' in data: material.visibility_level = data['visibilityLevel']
|
||||
if 'generalManual' in data: material.manual_link = data['generalManual']
|
||||
if 'generalImage' in data: material.product_image = data['generalImage']
|
||||
|
||||
# 修改:将列表 dumps 为字符串
|
||||
if 'generalManual' in data:
|
||||
material.manual_link = json.dumps(data['generalManual'])
|
||||
if 'generalImage' in data:
|
||||
material.product_image = json.dumps(data['generalImage'])
|
||||
|
||||
if 'isEnabled' in data:
|
||||
material.is_enabled = bool(int(data['isEnabled']))
|
||||
@ -170,12 +173,8 @@ class MaterialBaseService:
|
||||
if not material:
|
||||
raise ValueError("数据不存在")
|
||||
|
||||
# 1. 依赖检查:采购入库引用
|
||||
buy_usage_count = StockBuy.query.filter_by(base_id=m_id).count()
|
||||
|
||||
# 2. 依赖检查:半成品入库引用
|
||||
semi_usage_count = StockSemi.query.filter_by(base_id=m_id).count()
|
||||
|
||||
total_usage = buy_usage_count + semi_usage_count
|
||||
|
||||
if total_usage > 0:
|
||||
@ -186,7 +185,6 @@ class MaterialBaseService:
|
||||
f"请先清理相关库存或仅‘禁用’此条目。"
|
||||
)
|
||||
|
||||
# 3. 执行删除
|
||||
db.session.delete(material)
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user