Kamixitong/app/api/product.py

432 lines
14 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.

from flask import request, jsonify, current_app
from datetime import datetime
from app import db
from app.models import Product, License, Device, Version
from . import api_bp
from .decorators import require_login, require_admin
from sqlalchemy import func, case
import traceback
import sys
from app.utils.logger import log_operation
@api_bp.route('/products', methods=['GET'])
@require_login
def get_products():
"""获取产品列表"""
try:
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 20, type=int), 100)
keyword = request.args.get('keyword', '').strip()
include_stats = request.args.get('include_stats', 'true').lower() == 'true'
query = Product.query
if keyword:
query = query.filter(
Product.product_name.like(f'%{keyword}%') |
Product.description.like(f'%{keyword}%')
)
query = query.order_by(Product.create_time.desc())
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
products = pagination.items
# 优化批量查询统计信息避免N+1查询问题
if include_stats and products:
product_ids = [p.product_id for p in products]
# 批量查询license统计
license_stats = db.session.query(
License.product_id,
func.count(License.license_id).label('total_licenses'),
func.sum(case((License.status == 1, 1), else_=0)).label('active_licenses')
).filter(License.product_id.in_(product_ids)).group_by(License.product_id).all()
license_dict = {pid: {'total': 0, 'active': 0} for pid in product_ids}
for stat in license_stats:
license_dict[stat.product_id] = {
'total': stat.total_licenses or 0,
'active': stat.active_licenses or 0
}
# 批量查询device统计
device_stats = db.session.query(
Device.product_id,
func.count(Device.device_id).label('total_devices')
).filter(
Device.product_id.in_(product_ids),
Device.status == 1
).group_by(Device.product_id).all()
device_dict = {pid: 0 for pid in product_ids}
for stat in device_stats:
device_dict[stat.product_id] = stat.total_devices or 0
# 批量查询最新版本
version_stats = db.session.query(
Version.product_id,
Version.version_num,
Version.create_time
).filter(
Version.product_id.in_(product_ids),
Version.publish_status == 1
).order_by(Version.create_time.desc()).all()
version_dict = {}
for v in version_stats:
if v.product_id not in version_dict:
version_dict[v.product_id] = v.version_num
# 为每个产品添加统计信息
for product in products:
pid = product.product_id
stats = license_dict.get(pid, {'total': 0, 'active': 0})
product._cached_stats = {
'total_licenses': stats['total'],
'active_licenses': stats['active'],
'total_devices': device_dict.get(pid, 0),
'latest_version': version_dict.get(pid)
}
products = [product.to_dict(include_stats=include_stats) for product in products]
return jsonify({
'success': True,
'data': {
'products': products,
'pagination': {
'page': page,
'per_page': per_page,
'total': pagination.total,
'pages': pagination.pages,
'has_prev': pagination.has_prev,
'has_next': pagination.has_next
}
}
})
except Exception as e:
current_app.logger.error(f"获取产品列表失败: {str(e)}")
current_app.logger.error(f"错误类型: {type(e)}")
current_app.logger.error(f"错误堆栈: {traceback.format_exc()}")
return jsonify({
'success': False,
'message': '服务器内部错误,请稍后重试'
}), 500
@api_bp.route('/products', methods=['POST'])
@require_login
def create_product():
"""创建产品"""
try:
data = request.get_json()
if not data:
return jsonify({
'success': False,
'message': '请求数据为空'
}), 400
product_name = data.get('product_name', '').strip()
description = data.get('description', '').strip()
custom_id = data.get('product_id', '').strip()
if not product_name:
return jsonify({
'success': False,
'message': '产品名称不能为空'
}), 400
# 检查自定义ID是否重复
if custom_id:
existing = Product.query.filter_by(product_id=custom_id).first()
if existing:
return jsonify({
'success': False,
'message': '产品ID已存在'
}), 400
# 创建产品
product = Product(
product_id=custom_id if custom_id else None,
product_name=product_name,
description=description,
status=1
)
db.session.add(product)
db.session.commit()
# 记录操作日志
log_operation('CREATE_PRODUCT', 'PRODUCT', product.product_id, {
'product_name': product.product_name,
'description': product.description
})
return jsonify({
'success': True,
'message': '产品创建成功',
'data': product.to_dict()
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"创建产品失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/products/<product_id>', methods=['GET'])
@require_login
def get_product(product_id):
"""获取产品详情"""
try:
product = Product.query.filter_by(product_id=product_id).first()
if not product:
return jsonify({
'success': False,
'message': '产品不存在'
}), 404
return jsonify({
'success': True,
'data': product.to_dict(include_stats=True)
})
except Exception as e:
current_app.logger.error(f"获取产品详情失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/products/<product_id>', methods=['PUT'])
@require_login
def update_product(product_id):
"""更新产品"""
try:
product = Product.query.filter_by(product_id=product_id).first()
if not product:
return jsonify({
'success': False,
'message': '产品不存在'
}), 404
data = request.get_json()
if not data:
return jsonify({
'success': False,
'message': '请求数据为空'
}), 400
if 'product_name' in data:
product.product_name = data['product_name'].strip()
if 'description' in data:
product.description = data['description'].strip()
if 'status' in data:
product.status = data['status']
db.session.commit()
# 记录操作日志
log_operation('UPDATE_PRODUCT', 'PRODUCT', product.product_id, {
'product_name': product.product_name,
'description': product.description
})
return jsonify({
'success': True,
'message': '产品更新成功',
'data': product.to_dict()
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"更新产品失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/products/<product_id>', methods=['DELETE'])
@require_login
def delete_product(product_id):
"""删除产品"""
try:
product = Product.query.filter_by(product_id=product_id).first()
if not product:
return jsonify({
'success': False,
'message': '产品不存在'
}), 404
# 检查是否有关联的卡密
license_count = product.licenses.count()
if license_count > 0:
return jsonify({
'success': False,
'message': f'产品下还有 {license_count} 个卡密,无法删除'
}), 400
db.session.delete(product)
db.session.commit()
# 记录操作日志
log_operation('DELETE_PRODUCT', 'PRODUCT', product.product_id, {
'product_name': product.product_name
})
return jsonify({
'success': True,
'message': '产品删除成功'
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"删除产品失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/products/batch', methods=['DELETE'])
@require_login
def batch_delete_products():
"""批量删除产品"""
try:
data = request.get_json()
if not data or 'product_ids' not in data:
return jsonify({
'success': False,
'message': '请求数据为空或缺少product_ids字段'
}), 400
product_ids = data['product_ids']
if not isinstance(product_ids, list) or len(product_ids) == 0:
return jsonify({
'success': False,
'message': 'product_ids必须是非空列表'
}), 400
# 查找所有要删除的产品
products = Product.query.filter(Product.product_id.in_(product_ids)).all()
if len(products) != len(product_ids):
found_ids = [p.product_id for p in products]
missing_ids = [pid for pid in product_ids if pid not in found_ids]
return jsonify({
'success': False,
'message': f'以下产品不存在: {", ".join(missing_ids)}'
}), 404
# 检查是否有产品有关联的卡密
undeletable_products = []
for product in products:
license_count = product.licenses.count()
if license_count > 0:
undeletable_products.append({
'product_id': product.product_id,
'product_name': product.product_name,
'license_count': license_count
})
if undeletable_products:
return jsonify({
'success': False,
'message': '部分产品无法删除,因为有关联的卡密',
'undeletable_products': undeletable_products
}), 400
# 批量删除产品
for product in products:
db.session.delete(product)
db.session.commit()
# 记录操作日志
product_ids = [p.product_id for p in products]
log_operation('BATCH_DELETE_PRODUCTS', 'PRODUCT', None, {
'product_ids': product_ids,
'count': len(products)
})
return jsonify({
'success': True,
'message': f'成功删除 {len(products)} 个产品'
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"批量删除产品失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/products/batch/status', methods=['PUT'])
@require_login
def batch_update_product_status():
"""批量更新产品状态"""
try:
data = request.get_json()
if not data or 'product_ids' not in data or 'status' not in data:
return jsonify({
'success': False,
'message': '请求数据为空或缺少product_ids/status字段'
}), 400
product_ids = data['product_ids']
status = data['status']
if not isinstance(product_ids, list) or len(product_ids) == 0:
return jsonify({
'success': False,
'message': 'product_ids必须是非空列表'
}), 400
if status not in [0, 1]:
return jsonify({
'success': False,
'message': 'status必须是0(禁用)或1(启用)'
}), 400
# 查找所有要更新的产品
products = Product.query.filter(Product.product_id.in_(product_ids)).all()
if len(products) != len(product_ids):
found_ids = [p.product_id for p in products]
missing_ids = [pid for pid in product_ids if pid not in found_ids]
return jsonify({
'success': False,
'message': f'以下产品不存在: {", ".join(missing_ids)}'
}), 404
# 批量更新产品状态
for product in products:
product.status = status
db.session.commit()
# 记录操作日志
product_ids = [p.product_id for p in products]
log_operation('BATCH_UPDATE_PRODUCT_STATUS', 'PRODUCT', None, {
'product_ids': product_ids,
'status': status,
'count': len(products)
})
status_name = '启用' if status == 1 else '禁用'
return jsonify({
'success': True,
'message': f'成功{status_name} {len(products)} 个产品'
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"批量更新产品状态失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500