Kamixitong/app/api/user.py
2025-12-12 11:35:14 +08:00

849 lines
28 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 app import db
from app.models import Product, License, Ticket, Version, Order, Package
from . import api_bp
from app.utils.logger import log_operation
from app.utils.cors_middleware import handle_preflight, cors_after
import re
from datetime import datetime
# 注册请求和响应处理器
@api_bp.before_request
def before_request():
"""在每个请求前处理"""
# 处理预检请求
preflight_response = handle_preflight()
if preflight_response:
return preflight_response
@api_bp.after_request
def after_request(response):
"""在每个请求后添加CORS头部"""
return cors_after(response)
# ==================== 产品相关接口 ====================
@api_bp.route('/user/products', methods=['GET'])
def get_user_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()
product_type = request.args.get('type') # 产品类型筛选
is_paid = request.args.get('is_paid', type=int) # 付费筛选1=付费0=免费
query = Product.query.filter_by(status=1) # 只显示启用的产品
# 关键词搜索
if keyword:
query = query.filter(
db.or_(
Product.product_name.like(f'%{keyword}%'),
Product.description.like(f'%{keyword}%')
)
)
# 产品类型筛选
if product_type:
query = query.filter(Product.product_type == product_type)
query = query.order_by(Product.create_time.desc())
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
products = []
for product in pagination.items:
product_dict = product.to_dict()
# 获取最新版本信息
latest_version = Version.query.filter_by(
product_id=product.product_id,
publish_status=1
).order_by(Version.update_time.desc(), Version.create_time.desc()).first()
product_dict['latest_version'] = latest_version.version_num if latest_version else None
# 移除不存在的is_paid字段
products.append(product_dict)
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)}")
return jsonify({
'success': False,
'message': '服务器内部错误,请稍后重试'
}), 500
@api_bp.route('/user/products/<product_id>', methods=['GET'])
def get_user_product(product_id):
"""用户端获取产品详情(无需认证)"""
try:
product = Product.query.filter_by(product_id=product_id, status=1).first()
if not product:
return jsonify({
'success': False,
'message': '产品不存在或已下架'
}), 404
product_dict = product.to_dict(include_stats=True)
# 获取最新版本信息
latest_version = Version.query.filter_by(
product_id=product_id,
publish_status=1
).order_by(Version.update_time.desc(), Version.create_time.desc()).first()
if latest_version:
product_dict['latest_version'] = latest_version.version_num
# 获取最近3条更新日志包含下载URL信息
recent_versions = Version.query.filter_by(
product_id=product_id,
publish_status=1
).order_by(Version.update_time.desc(), Version.create_time.desc()).limit(3).all()
product_dict['recent_updates'] = [
{
'version_num': v.version_num,
'update_time': v.create_time.isoformat(),
'update_log': v.update_log,
'download_url': v.download_url, # 添加下载URL信息
'file_hash': v.file_hash # 添加文件哈希信息
} for v in recent_versions
]
# 移除不存在的is_paid字段
return jsonify({
'success': True,
'data': product_dict
})
except Exception as e:
current_app.logger.error(f"获取产品详情失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
# ==================== 卡密相关接口 ====================
@api_bp.route('/user/licenses/packages', methods=['GET'])
def get_license_packages():
"""用户端获取卡密套餐(按产品筛选,无需认证)"""
try:
product_id = request.args.get('product_id')
if not product_id:
return jsonify({
'success': False,
'message': '缺少产品ID参数'
}), 400
# 验证产品是否存在且启用
product = Product.query.filter_by(product_id=product_id, status=1).first()
if not product:
return jsonify({
'success': False,
'message': '产品不存在或已下架'
}), 404
# 从套餐表获取该产品的套餐信息
from app.models.package import Package
# 获取启用的套餐
packages = Package.query.filter_by(
product_id=product_id,
status=1
).order_by(Package.sort_order).all()
# 转换为字典格式
package_list = [pkg.to_dict() for pkg in packages]
return jsonify({
'success': True,
'data': {
'product': product.to_dict(),
'packages': package_list
}
})
except Exception as e:
current_app.logger.error(f"获取卡密套餐失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/user/licenses/verify', methods=['GET'])
def user_verify_license():
"""用户端验证卡密(下载用,无需认证)"""
try:
license_key = request.args.get('license_key')
product_id = request.args.get('product_id')
if not all([license_key, product_id]):
return jsonify({
'success': False,
'message': '缺少必要参数'
}), 400
# 查找卡密
license_obj = License.query.filter_by(
license_key=license_key,
product_id=product_id
).first()
if not license_obj:
return jsonify({
'success': False,
'message': '卡密不存在'
}), 404
# 检查卡密状态
if license_obj.status != 1:
return jsonify({
'success': False,
'message': '卡密未激活或已失效'
}), 403
# 检查是否过期
if license_obj.is_expired():
return jsonify({
'success': False,
'message': '卡密已过期'
}), 403
# 获取产品信息
product = Product.query.filter_by(product_id=product_id, status=1).first()
if not product:
return jsonify({
'success': False,
'message': '产品不存在或已下架'
}), 404
# 获取下载链接(从最新版本获取)
latest_version = Version.query.filter_by(
product_id=product_id,
publish_status=1
).order_by(Version.update_time.desc(), Version.create_time.desc()).first()
return jsonify({
'success': True,
'message': '卡密验证成功',
'data': {
'license_key': license_obj.license_key,
'product_name': product.product_name,
'expire_time': license_obj.expire_time.isoformat() if license_obj.expire_time else None,
'download_url': latest_version.download_url if latest_version else None,
'can_download': True
}
})
except Exception as e:
current_app.logger.error(f"卡密验证失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
# ==================== 工单相关接口 ====================
@api_bp.route('/user/tickets', methods=['POST'])
def create_user_ticket():
"""用户端创建工单(无需认证)"""
try:
data = request.get_json()
current_app.logger.info(f"收到工单创建请求: {data}")
if not data:
current_app.logger.warning("工单创建请求数据为空")
return jsonify({'success': False, 'message': '请求数据为空'}), 400
product_id = data.get('product_id')
contact_person = data.get('contact_person', '').strip()
phone = data.get('phone', '').strip()
title = data.get('title', '').strip()
description = data.get('description', '').strip()
attachment = data.get('attachment') # 附件信息
# 验证必填字段
if not all([product_id, contact_person, phone, title, description]):
missing_fields = []
if not product_id:
missing_fields.append('product_id')
if not contact_person:
missing_fields.append('contact_person')
if not phone:
missing_fields.append('phone')
if not title:
missing_fields.append('title')
if not description:
missing_fields.append('description')
current_app.logger.warning(f"工单创建缺少必要参数: {missing_fields}")
return jsonify({'success': False, 'message': f'缺少必要参数: {", ".join(missing_fields)}'}), 400
# 验证手机号格式
if not phone or not re.match(r'^1[3-9]\d{9}$', phone):
current_app.logger.warning(f"工单创建手机号格式不正确: {phone}")
return jsonify({'success': False, 'message': '手机号格式不正确'}), 400
# 验证产品存在且启用
product = Product.query.filter_by(product_id=product_id, status=1).first()
if not product:
current_app.logger.warning(f"工单创建产品不存在或已下架: {product_id}")
return jsonify({'success': False, 'message': '产品不存在或已下架'}), 404
ticket = Ticket(
title=title,
product_id=product_id,
description=description,
contact_person=contact_person,
phone=phone,
priority=data.get('priority', 1)
# 移除了不存在的source和attachment字段
)
db.session.add(ticket)
db.session.commit()
# 记录操作日志
log_operation('CREATE_USER_TICKET', 'TICKET', ticket.ticket_id, {
'title': ticket.title,
'product_id': ticket.product_id,
'contact_person': contact_person,
'phone': phone
})
current_app.logger.info(f"工单创建成功: ticket_id={ticket.ticket_id}, ticket_number={ticket.ticket_number}")
return jsonify({
'success': True,
'message': '工单提交成功',
'data': {
'ticket_id': ticket.ticket_id,
'ticket_number': ticket.ticket_number
}
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"创建工单失败: {str(e)}", exc_info=True)
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
@api_bp.route('/user/tickets', methods=['GET'])
def get_user_tickets():
"""用户端获取工单列表(通过手机号查询,无需认证)"""
try:
phone = request.args.get('phone')
if not phone:
return jsonify({
'success': False,
'message': '缺少手机号参数'
}), 400
# 验证手机号格式
if not re.match(r'^1[3-9]\d{9}$', phone):
return jsonify({
'success': False,
'message': '手机号格式不正确'
}), 400
# 查询该手机号提交的工单
tickets = Ticket.query.filter_by(phone=phone).order_by(Ticket.create_time.desc()).all()
return jsonify({
'success': True,
'data': [ticket.to_dict() for ticket in tickets]
})
except Exception as e:
current_app.logger.error(f"获取工单列表失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/user/tickets/query', methods=['GET'])
def query_user_ticket():
"""用户端查询工单(通过工单编号+手机号,无需认证)"""
try:
ticket_number = request.args.get('ticket_number')
phone = request.args.get('phone')
if not all([ticket_number, phone]):
return jsonify({
'success': False,
'message': '缺少必要参数'
}), 400
# 验证手机号格式
if not phone or not re.match(r'^1[3-9]\d{9}$', phone):
return jsonify({
'success': False,
'message': '手机号格式不正确'
}), 400
# 查找工单
ticket = Ticket.query.filter_by(
ticket_number=ticket_number,
phone=phone
).first()
if not ticket:
return jsonify({
'success': False,
'message': '工单不存在或手机号不匹配'
}), 404
return jsonify({
'success': True,
'data': ticket.to_dict()
})
except Exception as e:
current_app.logger.error(f"查询工单失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
# ==================== 订单相关接口 ====================
@api_bp.route('/user/orders', methods=['POST'])
def create_user_order():
"""用户端创建订单(无需认证)"""
try:
data = request.get_json()
if not data:
return jsonify({
'success': False,
'message': '请求数据为空'
}), 400
product_id = data.get('product_id')
package_id = data.get('package_id')
contact_person = data.get('contact_person', '').strip()
phone = data.get('phone', '').strip()
quantity = data.get('quantity', 1)
# 验证必填字段
if not all([product_id, package_id, contact_person, phone]):
return jsonify({
'success': False,
'message': '缺少必要参数'
}), 400
# 验证手机号格式
if not phone or not re.match(r'^1[3-9]\d{9}$', phone):
return jsonify({
'success': False,
'message': '手机号格式不正确'
}), 400
# 验证数量
if not isinstance(quantity, int) or quantity < 1 or quantity > 5:
return jsonify({
'success': False,
'message': '购买数量必须在1-5之间'
}), 400
# 验证产品存在且启用
product = Product.query.filter_by(product_id=product_id, status=1).first()
if not product:
return jsonify({
'success': False,
'message': '产品不存在或已下架'
}), 404
# 验证套餐存在且启用
package = Package.query.filter_by(package_id=package_id, status=1, product_id=product_id).first()
if not package:
return jsonify({
'success': False,
'message': '套餐不存在或已下架'
}), 404
# 检查库存
if not package.has_stock():
return jsonify({
'success': False,
'message': '套餐库存不足'
}), 400
# 计算总金额
total_amount = package.price * quantity
# 创建订单
order = Order(
product_id=product_id,
package_id=package_id,
contact_person=contact_person,
phone=phone,
quantity=quantity,
amount=total_amount,
status=0 # 待支付
)
# 保存到数据库
db.session.add(order)
db.session.commit()
# 记录操作日志
log_operation('CREATE_USER_ORDER', 'ORDER', order.order_id, {
'order_number': order.order_number,
'product_id': product_id,
'package_id': package_id,
'contact_person': contact_person,
'phone': phone,
'quantity': quantity,
'amount': total_amount
})
return jsonify({
'success': True,
'message': '订单创建成功',
'data': {
'order_number': order.order_number,
'amount': total_amount
}
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"创建订单失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/user/orders/query', methods=['GET'])
def query_user_order():
"""用户端查询订单(通过手机号+订单号,无需认证)"""
try:
order_number = request.args.get('order_number')
phone = request.args.get('phone')
if not all([order_number, phone]):
return jsonify({
'success': False,
'message': '缺少必要参数'
}), 400
# 验证手机号格式
if not phone or not re.match(r'^1[3-9]\d{9}$', phone):
return jsonify({
'success': False,
'message': '手机号格式不正确'
}), 400
# 查询订单表
order = Order.query.filter_by(order_number=order_number, phone=phone).first()
if not order:
return jsonify({
'success': False,
'message': '订单不存在'
}), 404
# 返回订单信息
return jsonify({
'success': True,
'data': order.to_dict()
})
except Exception as e:
current_app.logger.error(f"查询订单失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/user/orders', methods=['GET'])
def get_user_orders():
"""用户端获取订单列表(通过手机号查询,无需认证)"""
try:
phone = request.args.get('phone')
if not phone:
return jsonify({
'success': False,
'message': '缺少手机号参数'
}), 400
# 验证手机号格式
if not re.match(r'^1[3-9]\d{9}$', phone):
return jsonify({
'success': False,
'message': '手机号格式不正确'
}), 400
# 查询该手机号的所有订单
orders = Order.query.filter_by(phone=phone).order_by(Order.create_time.desc()).all()
return jsonify({
'success': True,
'data': {
'orders': [order.to_dict() for order in orders]
}
})
except Exception as e:
current_app.logger.error(f"获取订单列表失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/user/pay/alipay', methods=['POST'])
def create_alipay_payment():
"""创建支付宝支付订单"""
try:
data = request.get_json()
if not data:
return jsonify({
'success': False,
'message': '请求数据为空'
}), 400
order_number = data.get('order_number')
payment_type = data.get('payment_type', 'pc') # pc 或 wap
if not order_number:
return jsonify({
'success': False,
'message': '缺少订单号参数'
}), 400
# 查询订单
order = Order.query.filter_by(order_number=order_number).first()
if not order:
return jsonify({
'success': False,
'message': '订单不存在'
}), 404
# 检查订单状态
if order.status != 0: # 只有待支付的订单才能支付
return jsonify({
'success': False,
'message': f'订单当前状态不允许支付,当前状态:{order.get_status_name()}'
}), 400
# 获取支付宝配置
from app.utils.alipay import AlipayHelper
alipay_helper = AlipayHelper(current_app)
# 构建支付参数
subject = f"{order.product.product_name if order.product else '软件授权'}"
notify_url = f"{request.url_root}api/v/pay/alipay1/user/notify"
return_url = f"{request.url_root}payment/result?order_number={order_number}"
# 创建支付链接
if payment_type == 'wap':
# 手机网站支付
payment_url = alipay_helper.create_wap_payment_url(
order_number=order_number,
amount=order.amount,
subject=subject,
notify_url=notify_url,
return_url=return_url
)
else:
# PC网站支付
payment_url = alipay_helper.create_payment_url(
order_number=order_number,
amount=order.amount,
subject=subject,
notify_url=notify_url,
return_url=return_url
)
# 更新订单支付方式
order.payment_method = 'alipay'
db.session.commit()
return jsonify({
'success': True,
'message': '支付链接创建成功',
'data': {
'payment_url': payment_url,
'order_number': order_number,
'amount': order.amount
}
})
except ValueError as e:
return jsonify({
'success': False,
'message': f'支付配置错误: {str(e)}'
}), 500
except Exception as e:
db.session.rollback()
current_app.logger.error(f"创建支付宝支付失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
@api_bp.route('/pay/alipay1/user/notify', methods=['POST'])
def alipay_notify():
"""支付宝异步通知回调处理"""
try:
# 获取支付宝通知数据
notify_data = request.form.to_dict()
# 记录通知日志
current_app.logger.info(f"收到支付宝异步通知: {notify_data}")
# 获取支付宝配置
from app.utils.alipay import AlipayHelper
alipay_helper = AlipayHelper(current_app)
# 验证交易状态
is_valid, trade_status = alipay_helper.verify_trade_status(notify_data)
if not is_valid:
current_app.logger.warning(f"支付宝通知验证失败: {notify_data}")
return 'fail', 400
# 获取订单号
order_number = notify_data.get('out_trade_no')
trade_no = notify_data.get('trade_no') # 支付宝交易号
total_amount = notify_data.get('total_amount')
# 查询订单
order = Order.query.filter_by(order_number=order_number).first()
if not order:
current_app.logger.error(f"订单不存在: {order_number}")
return 'fail', 404
# 检查订单状态
if order.status != 0: # 订单已处理
current_app.logger.info(f"订单已处理: {order_number}, 状态: {order.status}")
return 'success'
# 验证订单金额
if float(order.amount) != float(total_amount):
current_app.logger.error(f"订单金额不匹配: 订单{order.amount} vs 通知{total_amount}")
return 'fail', 400
# 更新订单状态
order.status = 1 # 已支付
order.payment_time = datetime.utcnow()
order.payment_method = 'alipay'
db.session.commit()
# 记录操作日志
log_operation('ALIPAY_NOTIFY', 'ORDER', order.order_id, {
'order_number': order_number,
'trade_no': trade_no,
'amount': total_amount,
'trade_status': trade_status
})
# 支付成功后生成许可证
try:
from app.utils.license_generator import LicenseGenerator
license_gen = LicenseGenerator()
license_key = license_gen.generate_license(
product_id=order.product_id,
package_id=order.package_id,
contact_person=order.contact_person,
phone=order.phone,
quantity=order.quantity
)
# 记录许可证生成日志
log_operation('GENERATE_LICENSE', 'LICENSE', None, {
'order_number': order_number,
'license_key': license_key,
'product_id': order.product_id,
'quantity': order.quantity
})
current_app.logger.info(f"订单{order_number}支付成功,已生成许可证: {license_key}")
except Exception as e:
current_app.logger.error(f"生成许可证失败: {str(e)}")
# 许可证生成失败不应该影响支付状态
# 可以添加重试机制或人工处理
current_app.logger.info(f"订单{order_number}支付成功处理完成")
return 'success'
except Exception as e:
db.session.rollback()
current_app.logger.error(f"处理支付宝异步通知失败: {str(e)}")
return 'fail', 500
# ==================== 下载相关接口 ====================
@api_bp.route('/user/downloads/check', methods=['GET'])
def check_download_permission():
"""用户端检查下载权限(无需认证)"""
try:
product_id = request.args.get('product_id')
if not product_id:
return jsonify({
'success': False,
'message': '缺少产品ID参数'
}), 400
# 验证产品存在且启用
product = Product.query.filter_by(product_id=product_id, status=1).first()
if not product:
return jsonify({
'success': False,
'message': '产品不存在或已下架'
}), 404
# 获取产品是否为付费产品
is_paid = getattr(product, 'is_paid', False)
# 获取最新版本信息
latest_version = Version.query.filter_by(
product_id=product_id,
publish_status=1
).order_by(Version.update_time.desc(), Version.create_time.desc()).first()
return jsonify({
'success': True,
'data': {
'product_id': product_id,
'product_name': product.product_name,
'is_paid': is_paid,
'latest_version': latest_version.version_num if latest_version else None,
'download_url': latest_version.download_url if latest_version else None
}
})
except Exception as e:
current_app.logger.error(f"检查下载权限失败: {str(e)}")
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500