隔离(db_migrations/phase15_warehouse_location_company.sql) - sys_warehouse_location 新增 company_name,NULL = 全局共享 - 存量 3371 行按 full_path 顶段首字母回填:Y*→IRIS(500),L*/C*→LICA(2871) - 读范围 = 本公司 + 全局;写范围 = 仅本公司 删除/改名/批量生成都加归属校验 —— 只隔离读接口等于没隔离, 否则 LICA 一条 DELETE 就能删掉 IRIS 整棵库位树 - /tree 与 /children 补上缺失的 @jwt_required() - 公司名走白名单校验,非法值 400(拼错会造出谁都看不见的库位,且无从排查) - 超管可改归属,级联整棵子树(归属撕裂会让父子可见性错乱) 改名补偿 - 级联重写子孙 full_path(原实现只改自己一行,子孙仍挂旧前缀, 而库存快照存的正是 full_path,改一次名整棵子树就和台账对不上) - 同事务改写在库台账 stock_buy/semi/product/adjustment 的快照, 以及未结束盘点会话的 scope_config.locations - 刻意不含 trans_outbound / trans_borrow 等历史单据(改历史 = 篡改操作记录), 也不含 trans_repair.customer_location(那是客户地址不是库位) 批量生成 - 起点支持三种:勾选的一批库位 / 某子树下所有末级 / 单个父级 - 已存在同名跳过 —— full_path 上有唯一约束 uniq_full_path, 不跳过会整单 500 并整体回滚,连本该新建的也一并丢失 - 数量上限改按实际创建数(各层之和 × 起点数),不再是各层相乘 (旧口径低估,3000 的上限因此能被悄悄突破) - 前端加生成结构预览:逐层数量 + 完整路径示例,边填边看 前端清理 - 删除 utils/warehouseCompany.ts 前缀白名单,WarehouseSelector 与三个 入库页改为纯渲染后端数据(隔离已下沉到后端) - 盘点页 loadLocationTree 必须带 company_name:跨域角色在后端返回 None, 不带就会拉全量、盘点范围脱离所选公司 - 盘点页切公司时清库位树缓存(原来会继续沿用上一个公司的树) - 修复批量删除只取叶子导致中间层节点残留成孤儿
201 lines
7.4 KiB
Python
201 lines
7.4 KiB
Python
# inventory-backend/app/models/system.py
|
||
from app.extensions import db, beijing_time
|
||
from werkzeug.security import generate_password_hash, check_password_hash
|
||
from datetime import datetime
|
||
|
||
|
||
# ==========================================
|
||
# 1. 系统用户表
|
||
# ==========================================
|
||
class SysUser(db.Model):
|
||
"""
|
||
系统用户表
|
||
对应数据库: sys_user
|
||
username 字段存储格式约定: "真实姓名/登录账号" (例如: 张三/zhangsan01)
|
||
"""
|
||
__tablename__ = 'sys_user'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
username = db.Column(db.String(100), nullable=False) # 存储 "张三/zhangsan"
|
||
email = db.Column(db.String(100), unique=True)
|
||
department = db.Column(db.String(100))
|
||
role = db.Column(db.String(50))
|
||
status = db.Column(db.String(20), default='active')
|
||
password_hash = db.Column(db.Text)
|
||
created_at = db.Column(db.DateTime, default=beijing_time)
|
||
|
||
def set_password(self, password):
|
||
"""生成加密密码"""
|
||
self.password_hash = generate_password_hash(password)
|
||
|
||
def check_password(self, password):
|
||
"""验证密码"""
|
||
return check_password_hash(self.password_hash, password)
|
||
|
||
def to_dict(self):
|
||
"""
|
||
序列化为字典
|
||
数据库存的是 '张三/zhangsan'
|
||
前端需要的是 '张三(zhangsan)'
|
||
"""
|
||
raw_name = self.username
|
||
real_name = ''
|
||
account_id = raw_name
|
||
|
||
# 解析存储格式: Name/ID
|
||
if '/' in raw_name:
|
||
parts = raw_name.split('/')
|
||
real_name = parts[0]
|
||
acc_id = parts[1]
|
||
# 格式化为前端展示格式: 张三(zhangsan01)
|
||
display_name = f"{real_name}({acc_id})"
|
||
# 单独提取账号ID (如果前端需要单独用)
|
||
account_id = acc_id
|
||
else:
|
||
display_name = raw_name
|
||
|
||
return {
|
||
'id': self.id,
|
||
'username': display_name, # 列表显示: 张三(zhangsan01)
|
||
'raw_username': self.username, # 原始数据
|
||
'real_name': real_name, # 真实姓名: 张三
|
||
'display_name': display_name, # 显示名: 张三(zhangsan01)
|
||
'account_id': account_id, # 纯账号ID: zhangsan01
|
||
'email': self.email,
|
||
'department': self.department,
|
||
'role': self.role,
|
||
'status': self.status,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||
}
|
||
|
||
|
||
# ==========================================
|
||
# 2. 系统日志表
|
||
# ==========================================
|
||
class SysLog(db.Model):
|
||
"""
|
||
系统操作日志表
|
||
对应数据库表: sys_log
|
||
"""
|
||
__tablename__ = 'sys_log'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
op_time = db.Column(db.DateTime, default=beijing_time)
|
||
op_user_name = db.Column(db.String(100))
|
||
op_user_id = db.Column(db.String(50))
|
||
module_name = db.Column(db.String(100))
|
||
action_type = db.Column(db.String(50))
|
||
target_table = db.Column(db.String(100))
|
||
target_id = db.Column(db.Integer)
|
||
description = db.Column(db.Text)
|
||
ip_address = db.Column(db.String(50))
|
||
|
||
def to_dict(self):
|
||
return {
|
||
'id': self.id,
|
||
'op_time': self.op_time.isoformat() if self.op_time else None,
|
||
'op_user_name': self.op_user_name,
|
||
'module_name': self.module_name,
|
||
'action_type': self.action_type,
|
||
'description': self.description
|
||
}
|
||
|
||
|
||
# ==========================================
|
||
# 3. 权限管理模型 (RBAC) - [新增]
|
||
# ==========================================
|
||
|
||
class SysMenu(db.Model):
|
||
"""系统菜单/页面表"""
|
||
__tablename__ = 'sys_menu'
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
parent_id = db.Column(db.Integer, default=0)
|
||
name = db.Column(db.String(50), nullable=False)
|
||
code = db.Column(db.String(100), unique=True, nullable=False)
|
||
path = db.Column(db.String(200))
|
||
sort_order = db.Column(db.Integer, default=0)
|
||
is_visible = db.Column(db.Boolean, default=True)
|
||
|
||
def to_dict(self):
|
||
return {
|
||
'id': self.id,
|
||
'name': self.name,
|
||
'code': self.code,
|
||
'path': self.path,
|
||
'type': 'menu' # 前端树形控件图标判断用
|
||
}
|
||
|
||
|
||
class SysElement(db.Model):
|
||
"""页面元素/列定义表"""
|
||
__tablename__ = 'sys_element'
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
menu_code = db.Column(db.String(100), db.ForeignKey('sys_menu.code'))
|
||
name = db.Column(db.String(100), nullable=False)
|
||
code = db.Column(db.String(100), nullable=False) # 如: unit_price
|
||
element_type = db.Column(db.String(20), default='column')
|
||
|
||
def to_dict(self):
|
||
return {
|
||
'id': self.id,
|
||
'name': self.name,
|
||
'code': self.code,
|
||
'menu_code': self.menu_code,
|
||
'type': 'element',
|
||
'element_type': self.element_type
|
||
}
|
||
|
||
|
||
class SysRolePermission(db.Model):
|
||
"""角色权限关联表(支持公司级别隔离)"""
|
||
__tablename__ = 'sys_role_permission'
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
role_code = db.Column(db.String(50), nullable=False)
|
||
target_code = db.Column(db.String(100), nullable=False) # menu_code 或 element_code
|
||
type = db.Column(db.String(20), nullable=False) # 'menu' 或 'element'
|
||
company_name = db.Column(db.String(255), comment='所属公司(NULL/空/System=全局模板,具体值=公司定制)')
|
||
|
||
|
||
# ==========================================
|
||
# 4. 库位管理模型
|
||
# ==========================================
|
||
class SysWarehouseLocation(db.Model):
|
||
"""
|
||
库位字典表(支持无限层级树形结构)
|
||
对应数据库: sys_warehouse_location
|
||
"""
|
||
__tablename__ = 'sys_warehouse_location'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
parent_id = db.Column(db.Integer, db.ForeignKey('sys_warehouse_location.id'), nullable=True)
|
||
name = db.Column(db.String(100), nullable=False)
|
||
full_path = db.Column(db.String(500)) # 完整路径,如 "A区/货架1/第3层"
|
||
level = db.Column(db.Integer, default=0) # 层级深度,顶级为0
|
||
is_enabled = db.Column(db.Boolean, default=True)
|
||
# [新增] 公司归属:NULL = 全局共享(两司均可见)。
|
||
# ★ 这是「归属」不是「任意公司名」,取值域只有 IRIS / LICA / NULL 三态。
|
||
# 切勿写入 'System' —— 超管的 JWT company_name 正是 'System',一旦落库,
|
||
# 这批库位对**所有**公司都不可见且无从排查。写入前统一过
|
||
# warehouse.py 的 _normalize_company()。
|
||
company_name = db.Column(db.String(50), index=True, comment='公司归属: IRIS/LICA,NULL=全局共享')
|
||
created_at = db.Column(db.DateTime, default=beijing_time)
|
||
# 注意:数据库表中没有 updated_at 字段,不要添加!
|
||
|
||
# 自关联 - 使用 backref 定义父节点的反向引用
|
||
children = db.relationship(
|
||
'SysWarehouseLocation',
|
||
backref=db.backref('parent', remote_side=[id]),
|
||
lazy='dynamic'
|
||
)
|
||
|
||
def to_dict(self):
|
||
return {
|
||
'id': self.id,
|
||
'parent_id': self.parent_id,
|
||
'name': self.name,
|
||
'full_path': self.full_path,
|
||
'level': self.level,
|
||
'is_enabled': self.is_enabled,
|
||
'company_name': self.company_name,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||
} |