修改登录退出逻辑
This commit is contained in:
@ -2,7 +2,6 @@
|
||||
|
||||
from flask import Flask
|
||||
from config import Config
|
||||
# 【修改】增加 jwt 引入
|
||||
from app.extensions import db, migrate, cors, jwt
|
||||
import os
|
||||
|
||||
@ -16,36 +15,44 @@ def create_app():
|
||||
# =========================================================
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
jwt.init_app(app) # 初始化 JWT
|
||||
|
||||
# 【新增】初始化 JWT (用于 Token 认证)
|
||||
jwt.init_app(app)
|
||||
|
||||
# 确保跨域配置
|
||||
# 允许 /api/ 开头的请求跨域
|
||||
cors.init_app(app, resources={r"/*": {"origins": "*"}})
|
||||
# 允许所有 /api/ 开头的请求跨域,支持 credentials
|
||||
cors.init_app(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True)
|
||||
|
||||
# =========================================================
|
||||
# 2. 注册蓝图 (Blueprints)
|
||||
# ---------------------------------------------------------
|
||||
# 注意:为了解决前端请求不带 /v1 导致的 404 错误,
|
||||
# 下面的模块都采用了 "双重注册" 策略:
|
||||
# 1. 标准地址: /api/v1/...
|
||||
# 2. 兼容地址: /api/... (name 参数必须不同)
|
||||
# =========================================================
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 2.0 [新增] 注册权限与认证模块 (Auth) - 最关键修复
|
||||
# 2.0 注册权限与认证模块 (Auth)
|
||||
# -----------------------------------------------------
|
||||
try:
|
||||
from app.api.v1.auth import auth_bp
|
||||
# 前端请求地址: /api/v1/auth/login
|
||||
# 标准
|
||||
app.register_blueprint(auth_bp, url_prefix='/api/v1/auth')
|
||||
print("✅ Auth (System & Login) 模块注册成功")
|
||||
# 兼容 (防止前端忘记写 v1)
|
||||
app.register_blueprint(auth_bp, url_prefix='/api/auth', name='auth_legacy')
|
||||
print("✅ Auth 模块注册成功")
|
||||
except ImportError as e:
|
||||
print(f"❌ 错误: Auth 模块导入失败: {e}")
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 2.1 注册入库聚合模块 (Inbound)
|
||||
# 2.1 注册入库聚合模块 (Inbound) - 【核心修复点】
|
||||
# -----------------------------------------------------
|
||||
try:
|
||||
from app.api.v1.inbound import inbound_bp
|
||||
# 标准: /api/v1/inbound/base/list
|
||||
app.register_blueprint(inbound_bp, url_prefix='/api/v1/inbound')
|
||||
print("✅ Inbound (Buy, Semi, Product, Base) 模块注册成功")
|
||||
|
||||
# 兼容: /api/inbound/base/list (修复前端 404)
|
||||
app.register_blueprint(inbound_bp, url_prefix='/api/inbound', name='inbound_legacy')
|
||||
print("✅ Inbound 模块注册成功 (已启用兼容模式: /api/inbound)")
|
||||
except ImportError as e:
|
||||
print(f"❌ 错误: Inbound 模块导入失败: {e}")
|
||||
|
||||
@ -55,7 +62,8 @@ def create_app():
|
||||
try:
|
||||
from app.api.v1.common.print import print_bp
|
||||
app.register_blueprint(print_bp, url_prefix='/api/v1/common/print')
|
||||
print("✅ Print (Label Printing) 模块注册成功")
|
||||
app.register_blueprint(print_bp, url_prefix='/api/common/print', name='print_legacy')
|
||||
print("✅ Print 模块注册成功")
|
||||
except ImportError as e:
|
||||
print(f"❌ 错误: Print 模块导入失败: {e}")
|
||||
|
||||
@ -64,51 +72,44 @@ def create_app():
|
||||
# -----------------------------------------------------
|
||||
try:
|
||||
from app.api.v1.common.upload import upload_bp
|
||||
# 注册方式 1: 标准路径
|
||||
app.register_blueprint(upload_bp, url_prefix='/api/v1/common')
|
||||
# 注册方式 2: 兼容路径 (防止反向代理剥离 /api)
|
||||
app.register_blueprint(upload_bp, url_prefix='/v1/common', name='upload_fallback')
|
||||
print("✅ Upload (File Storage) 模块注册成功 (双路径兼容模式)")
|
||||
app.register_blueprint(upload_bp, url_prefix='/api/common', name='upload_legacy')
|
||||
print("✅ Upload 模块注册成功")
|
||||
except ImportError as e:
|
||||
print(f"❌ 错误: Upload 模块导入失败: {e}")
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 2.4 [新增] 注册业务操作模块 (Transactions)
|
||||
# 2.4 注册业务操作模块 (Transactions)
|
||||
# -----------------------------------------------------
|
||||
try:
|
||||
# 对应 borrow, return, scrap 等操作
|
||||
from app.api.v1.transactions import trans_bp
|
||||
app.register_blueprint(trans_bp, url_prefix='/api/v1/trans')
|
||||
print("✅ Transactions (Borrow, Return, Scrap) 模块注册成功")
|
||||
app.register_blueprint(trans_bp, url_prefix='/api/trans', name='trans_legacy')
|
||||
print("✅ Transactions 模块注册成功")
|
||||
except ImportError as e:
|
||||
# 如果文件还没写好,这里会报错,但不影响主程序启动
|
||||
print(f"⚠️ 警告: Transaction 模块导入失败 (如果是新建项目可忽略): {e}")
|
||||
# 允许模块不存在时不崩溃
|
||||
print(f"⚠️ 提示: Transaction 模块尚未创建或导入失败: {e}")
|
||||
|
||||
# =========================================================
|
||||
# 3. 预加载数据模型 (解决 relationship 找不到模型的问题)
|
||||
# 3. 预加载数据模型
|
||||
# =========================================================
|
||||
with app.app_context():
|
||||
try:
|
||||
# 1. 基础物料
|
||||
# 基础与库存模型
|
||||
from app.models.base import MaterialBase
|
||||
# 2. 采购入库
|
||||
from app.models.inbound.buy import StockBuy
|
||||
# 3. 半成品入库
|
||||
from app.models.inbound.semi import StockSemi
|
||||
# 4. 成品入库
|
||||
from app.models.inbound.product import StockProduct
|
||||
|
||||
# 【新增】5. 系统用户 (关键:确保创建 user 表)
|
||||
# 系统与业务模型
|
||||
from app.models.system import SysUser, SysLog
|
||||
|
||||
# 【新增】6. 业务流水
|
||||
from app.models.transaction import TransBorrow, TransRepair, TransScrap
|
||||
|
||||
# 开发环境自动建表 (根据之前的对话,强烈建议在容器第一次启动时开启或手动调用)
|
||||
# 首次运行时可取消注释自动建表 (但在生产环境建议使用 flask db upgrade)
|
||||
# db.create_all()
|
||||
|
||||
except ImportError as e:
|
||||
print(f"⚠️ 模型预加载失败: {e}")
|
||||
print(f"⚠️ 模型预加载部分失败 (检查是否缺少文件): {e}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 模型预加载发生未知错误: {e}")
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# app/api/v1/auth.py
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from flask_jwt_extended import jwt_required, get_jwt
|
||||
from app.services.auth_service import AuthService
|
||||
|
||||
@ -10,13 +10,38 @@ auth_bp = Blueprint('auth', __name__)
|
||||
def login():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'msg': '无效的请求数据'}), 400
|
||||
|
||||
if not data.get('username') or not data.get('password'):
|
||||
return jsonify({'msg': '请输入用户名和密码'}), 400
|
||||
|
||||
# 调用 Service 层逻辑
|
||||
result = AuthService.login(data)
|
||||
return jsonify({'msg': '登录成功', 'data': result}), 200
|
||||
|
||||
# [关键修复]
|
||||
# 前端 store 代码写的是: token.value = res.data.access_token
|
||||
# 所以我们这里不能把 access_token 包裹在 data 字段里,
|
||||
# 而是应该直接合并返回,或者让前端去 data.data 里面取。
|
||||
# 为了不改前端,我们这里做解构返回:
|
||||
|
||||
response_data = {
|
||||
'msg': '登录成功',
|
||||
'access_token': result.get('access_token'),
|
||||
'user': result.get('user')
|
||||
}
|
||||
|
||||
return jsonify(response_data), 200
|
||||
|
||||
except ValueError as ve:
|
||||
# 捕获已知的业务错误(如密码错误、用户不存在)
|
||||
return jsonify({'msg': str(ve)}), 401
|
||||
except Exception as e:
|
||||
return jsonify({'msg': str(e)}), 401
|
||||
# [关键修复] 打印详细报错到控制台,方便排查 500 错误
|
||||
# (例如数据库连接失败、表不存在等)
|
||||
current_app.logger.error(f"Login Failed Error: {str(e)}")
|
||||
# 生产环境不建议直接把 error 返回给前端,但调试阶段很有用
|
||||
return jsonify({'msg': f'服务器内部错误: {str(e)}'}), 500
|
||||
|
||||
|
||||
# 新增:创建用户 (替代了原来的注册)
|
||||
@ -30,8 +55,13 @@ def create_user():
|
||||
claims = get_jwt()
|
||||
operator_role = claims.get('role')
|
||||
|
||||
# 增加一个简单的权限判断(可选)
|
||||
if operator_role not in ['super_admin', 'supervisor']:
|
||||
return jsonify({'msg': '权限不足,无法创建用户'}), 403
|
||||
|
||||
result = AuthService.create_user(data, operator_role)
|
||||
return jsonify({'msg': '用户创建成功', 'data': result}), 201
|
||||
|
||||
except Exception as e:
|
||||
# 这里虽然返回 400,但实际可能包含 403 的含义,具体看前端处理
|
||||
current_app.logger.error(f"User Create Failed: {str(e)}")
|
||||
return jsonify({'msg': str(e)}), 400
|
||||
@ -1,8 +1,12 @@
|
||||
from flask import Blueprint, jsonify
|
||||
|
||||
# 定义蓝图,名字叫 'transactions'
|
||||
# 定义蓝图
|
||||
# 注意:这个变量名 trans_bp 必须与 app/__init__.py 中注册时引用的名字一致
|
||||
trans_bp = Blueprint('transactions', __name__)
|
||||
|
||||
@trans_bp.route('/test', methods=['GET'])
|
||||
def test_transaction():
|
||||
return jsonify({"message": "Transaction module is working"})
|
||||
"""
|
||||
测试接口:用于验证 Transaction 模块是否加载成功
|
||||
"""
|
||||
return jsonify({"message": "Transaction module is working", "status": "success"})
|
||||
@ -10,7 +10,8 @@ class MaterialBase(db.Model):
|
||||
|
||||
# 1. 基础字段
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(255), nullable=False, comment='基础信息名称')
|
||||
name = db.Column(db.String(255), nullable=False, 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='规格型号')
|
||||
@ -46,6 +47,7 @@ class MaterialBase(db.Model):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'commonName': self.common_name, # ✅ 序列化新增字段
|
||||
'category': self.category,
|
||||
'type': self.material_type, # 前端字段映射
|
||||
'spec': self.spec_model, # 前端字段映射
|
||||
|
||||
@ -1,66 +1,96 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# 1. 借用表
|
||||
class TransBorrow(db.Model):
|
||||
__tablename__ = 'trans_borrow'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
sku = db.Column(db.String(100))
|
||||
sku = db.Column(db.String(100), index=True) # 加索引优化查询
|
||||
source_table = db.Column(db.String(50))
|
||||
stock_id = db.Column(db.Integer)
|
||||
quantity = db.Column(db.Numeric(19, 4))
|
||||
|
||||
borrow_time = db.Column(db.DateTime, default=datetime.now)
|
||||
expected_return_time = db.Column(db.DateTime)
|
||||
|
||||
borrower_name = db.Column(db.String(100))
|
||||
actual_return_time = db.Column(db.DateTime)
|
||||
approver_name = db.Column(db.String(100))
|
||||
status = db.Column(db.String(20))
|
||||
|
||||
# 状态:borrowed(借出), returned(已还), overdue(逾期)
|
||||
status = db.Column(db.String(20), default='borrowed')
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'sku': self.sku,
|
||||
'quantity': float(self.quantity) if self.quantity else 0,
|
||||
'borrower_name': self.borrower_name,
|
||||
'borrow_time': self.borrow_time.strftime('%Y-%m-%d %H:%M:%S') if self.borrow_time else None,
|
||||
'status': self.status
|
||||
}
|
||||
|
||||
|
||||
# 2. 维修表
|
||||
class TransRepair(db.Model):
|
||||
__tablename__ = 'trans_repair'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
sku = db.Column(db.String(100))
|
||||
sku = db.Column(db.String(100), index=True)
|
||||
source_table = db.Column(db.String(50))
|
||||
stock_id = db.Column(db.Integer)
|
||||
|
||||
arrival_date = db.Column(db.Date)
|
||||
expected_repair_time = db.Column(db.String(100))
|
||||
shipping_date = db.Column(db.Date)
|
||||
|
||||
is_self_made = db.Column(db.Boolean, default=False)
|
||||
related_product_id = db.Column(db.Integer)
|
||||
related_contract_id = db.Column(db.String(100))
|
||||
|
||||
repair_manager = db.Column(db.String(100))
|
||||
fault_description = db.Column(db.Text)
|
||||
repair_result = db.Column(db.Text)
|
||||
|
||||
cost_price = db.Column(db.Numeric(19, 4))
|
||||
sale_price = db.Column(db.Numeric(19, 4))
|
||||
|
||||
def to_dict(self):
|
||||
return {'id': self.id, 'sku': self.sku, 'status': 'repaired' if self.repair_result else 'pending'}
|
||||
return {
|
||||
'id': self.id,
|
||||
'sku': self.sku,
|
||||
'status': 'repaired' if self.repair_result else 'pending',
|
||||
'manager': self.repair_manager
|
||||
}
|
||||
|
||||
|
||||
# 3. 报废表
|
||||
class TransScrap(db.Model):
|
||||
__tablename__ = 'trans_scrap'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
sku = db.Column(db.String(100))
|
||||
sku = db.Column(db.String(100), index=True)
|
||||
source_table = db.Column(db.String(50))
|
||||
stock_id = db.Column(db.Integer)
|
||||
quantity = db.Column(db.Numeric(19, 4))
|
||||
|
||||
reason = db.Column(db.Text)
|
||||
operator_name = db.Column(db.String(100))
|
||||
operation_time = db.Column(db.DateTime, default=datetime.now)
|
||||
|
||||
approver_name = db.Column(db.String(100))
|
||||
approval_status = db.Column(db.String(20))
|
||||
approval_status = db.Column(db.String(20), default='pending') # pending, approved, rejected
|
||||
|
||||
cost_at_scrap = db.Column(db.Numeric(19, 4))
|
||||
total_loss = db.Column(db.Numeric(19, 4))
|
||||
|
||||
def to_dict(self):
|
||||
return {'id': self.id, 'sku': self.sku, 'total_loss': float(self.total_loss) if self.total_loss else 0}
|
||||
return {
|
||||
'id': self.id,
|
||||
'sku': self.sku,
|
||||
'quantity': float(self.quantity) if self.quantity else 0,
|
||||
'total_loss': float(self.total_loss) if self.total_loss else 0,
|
||||
'reason': self.reason
|
||||
}
|
||||
@ -2,12 +2,8 @@
|
||||
|
||||
from app.extensions import db
|
||||
from app.models.base import MaterialBase
|
||||
|
||||
# ==============================================================================
|
||||
# ✅ 正确的引用方式
|
||||
# ==============================================================================
|
||||
from app.models.inbound.buy import StockBuy # 引用采购库存模型
|
||||
from app.models.inbound.semi import StockSemi # 引用半成品库存模型
|
||||
from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.semi import StockSemi
|
||||
from sqlalchemy import or_
|
||||
import traceback
|
||||
|
||||
@ -28,11 +24,12 @@ class MaterialBaseService:
|
||||
if not keyword:
|
||||
return []
|
||||
|
||||
# 搜索名称或规格型号,且必须是启用的
|
||||
# ✅ 搜索范围增加 common_name (俗名)
|
||||
query = MaterialBase.query.filter(
|
||||
MaterialBase.is_enabled == True,
|
||||
or_(
|
||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||
MaterialBase.common_name.ilike(f'%{keyword}%'),
|
||||
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||
)
|
||||
).limit(20)
|
||||
@ -42,6 +39,7 @@ class MaterialBaseService:
|
||||
results.append({
|
||||
'id': item.id,
|
||||
'name': item.name,
|
||||
'commonName': item.common_name, # ✅ 返回俗名
|
||||
'spec': item.spec_model,
|
||||
'category': item.category,
|
||||
'unit': item.unit,
|
||||
@ -62,12 +60,14 @@ class MaterialBaseService:
|
||||
query = MaterialBase.query
|
||||
|
||||
if filters:
|
||||
# 1. 关键词模糊搜索 (名称 或 规格型号)
|
||||
# 1. 关键词模糊搜索 (名称 或 俗名 或 规格型号)
|
||||
if filters.get('keyword'):
|
||||
kw = f"%{filters['keyword']}%"
|
||||
# ✅ 增加俗名搜索
|
||||
query = query.filter(or_(
|
||||
MaterialBase.name.like(kw),
|
||||
MaterialBase.spec_model.like(kw)
|
||||
MaterialBase.name.ilike(kw),
|
||||
MaterialBase.common_name.ilike(kw),
|
||||
MaterialBase.spec_model.ilike(kw)
|
||||
))
|
||||
|
||||
# 2. 精确筛选
|
||||
@ -101,6 +101,7 @@ class MaterialBaseService:
|
||||
raise ValueError("名称和规格型号不能为空")
|
||||
|
||||
# 1. 查重 (名称+规格型号 唯一)
|
||||
# 注意:俗名不参与唯一性校验,允许重复或为空
|
||||
exist = MaterialBase.query.filter_by(
|
||||
name=data['name'],
|
||||
spec_model=data['spec']
|
||||
@ -111,6 +112,7 @@ class MaterialBaseService:
|
||||
# 2. 创建对象
|
||||
new_material = MaterialBase(
|
||||
name=data['name'],
|
||||
common_name=data.get('commonName'), # ✅ 读取俗名
|
||||
spec_model=data['spec'],
|
||||
category=data.get('category'),
|
||||
material_type=data.get('type'),
|
||||
@ -139,6 +141,7 @@ class MaterialBaseService:
|
||||
|
||||
# 更新字段
|
||||
if 'name' in data: material.name = data['name']
|
||||
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']
|
||||
@ -161,7 +164,6 @@ class MaterialBaseService:
|
||||
def delete_material(m_id):
|
||||
"""
|
||||
删除基础信息 (带依赖检查)
|
||||
✅ 已升级:同时检查采购库(Buy)和半成品库(Semi)
|
||||
"""
|
||||
try:
|
||||
material = MaterialBase.query.get(m_id)
|
||||
|
||||
Reference in New Issue
Block a user