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/', 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