Files
KCGL/inventory-backend/app/models/base.py
yueli 23fc10a25f fix: Day2就绪 — 删除硬化+索引补全+全局异常处理
## base_service.py — MaterialBase删除硬化
- 新增 BomTable 引用检查(bom_usage_count)
- 物料被BOM引用时禁止删除,提示「请先解除BOM绑定」

## models/base.py + bom.py — 缺失索引补全
- material_base: company_name + is_enabled 添加 index=True
- bom_table: is_enabled 添加 index=True

## __init__.py — 全局异常处理
- @app.errorhandler(ValueError) → 400 + 自定义消息
- @app.errorhandler(404/405) → 标准错误
- @app.errorhandler(Exception) → 500 + 通用消息
  堆栈仅记录服务器日志,不泄露到前端
2026-07-16 14:14:43 +08:00

138 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# app/models/base.py
from app.extensions import db
from pgvector.sqlalchemy import Vector
import json
class MaterialBase(db.Model):
"""
基础信息表模型
对应数据库表: material_base
"""
__tablename__ = 'material_base'
# 1. 基础字段
id = db.Column(db.Integer, primary_key=True)
company_name = db.Column(db.String(255), index=True, comment='所属公司') # ★ 多租户过滤高频列
name = db.Column(db.String(255), nullable=False, index=True, comment='名称') # ★ 模糊搜索/精确定位高频列
common_name = db.Column(db.String(255), comment='俗名')
category = db.Column(db.String(100), index=True, comment='类别') # ★ 分类统计/过滤高频列
material_type = db.Column(db.String(100), index=True, comment='类型') # ★ 类型分组/过滤高频列
spec_model = db.Column(db.String(255), index=True, comment='规格型号') # ★ 模糊搜索/精确匹配高频列
unit = db.Column(db.String(50), comment='计量单位')
# 可见等级
visibility_level = db.Column(db.Integer, default=0, comment='信息可见等级')
# 链接与图片 (现在存储 JSON 字符串)
manual_link = db.Column(db.Text, comment='通用说明书')
product_image = db.Column(db.Text, comment='通用产品图')
# 备注字段(纯文本,与文件 URL 分离存储)
product_image_remark = db.Column(db.Text, default='', comment='产品图备注')
manual_link_remark = db.Column(db.Text, default='', comment='说明书备注')
# 启用状态
is_enabled = db.Column(db.Boolean, default=True, index=True, comment='是否启用') # ★ 状态过滤高频列
# 强制质检标记(采购入库时必须上传检测报告)
is_inspection_required = db.Column(db.Boolean, default=False, comment='是否强制要求质检')
# 参考价格
reference_price = db.Column(db.Numeric(10, 2), nullable=True, comment='参考价格')
# CLIP 视觉向量(用于以图搜图)
img_embedding = db.Column(Vector(512), nullable=True)
# ============================================================
# 关联关系区域
# ============================================================
# 1. 关联采购库存 (StockBuy)
stock_buys = db.relationship('StockBuy', back_populates='base', lazy='dynamic')
# 2. 关联半成品库存 (StockSemi)
stock_semis = db.relationship('StockSemi', back_populates='base', lazy='dynamic')
# 3. 关联成品库存 (StockProduct)
stock_products = db.relationship('StockProduct', back_populates='base', lazy='dynamic')
# 4. 关联服务库存 (StockService)
stock_services = db.relationship('StockService', back_populates='base', lazy='dynamic')
# 5. 关联预警设置 (MaterialWarningSetting)
warning_settings = db.relationship('MaterialWarningSetting', back_populates='material', lazy='dynamic', cascade='all, delete-orphan')
def to_dict(self):
"""
序列化方法
"""
# 辅助解析函数:将数据库存储的 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,
'companyName': self.company_name,
'name': self.name,
'commonName': self.common_name,
'category': self.category,
'type': self.material_type,
'spec': self.spec_model,
'unit': self.unit,
'visibilityLevel': self.visibility_level,
'generalManual': parse_list(self.manual_link),
'generalImage': parse_list(self.product_image),
'productImageRemark': self.product_image_remark or '',
'manualLinkRemark': self.manual_link_remark or '',
# 【核心修改】:直接返回布尔值,不再转成 1 或 0
'isEnabled': bool(self.is_enabled),
# 强制质检标记
'isInspectionRequired': bool(self.is_inspection_required),
# 参考价格
'referencePrice': float(self.reference_price) if self.reference_price is not None else None,
}
class MaterialWarningSetting(db.Model):
"""
物料预警设置表模型
对应数据库表: material_warning_settings
"""
__tablename__ = 'material_warning_settings'
id = db.Column(db.Integer, primary_key=True)
base_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False, comment='物料基础信息ID')
is_enabled = db.Column(db.Boolean, default=False, comment='是否启用预警')
yellow_threshold = db.Column(db.Numeric(10, 2), nullable=True, comment='黄色预警阈值')
red_threshold = db.Column(db.Numeric(10, 2), nullable=True, comment='红色预警阈值')
yellow_emails = db.Column(db.String(500), nullable=True, comment='黄色预警通知邮箱')
red_emails = db.Column(db.String(500), nullable=True, comment='红色预警通知邮箱')
is_ordered = db.Column(db.Boolean, default=False, comment='是否已处理采购')
last_notified_at = db.Column(db.DateTime, nullable=True, comment='上次邮件通知时间')
# 关联关系
material = db.relationship('MaterialBase', back_populates='warning_settings')
def to_dict(self):
return {
'id': self.id,
'baseId': self.base_id,
'isEnabled': bool(self.is_enabled),
'yellowThreshold': float(self.yellow_threshold) if self.yellow_threshold is not None else None,
'redThreshold': float(self.red_threshold) if self.red_threshold is not None else None,
'yellowEmails': self.yellow_emails or '',
'redEmails': self.red_emails or '',
'isOrdered': bool(self.is_ordered),
'lastNotifiedAt': self.last_notified_at.strftime('%Y-%m-%d %H:%M:%S') if self.last_notified_at else None
}