进行了完整的视图拆分,同步了 merchant_ops 的修改
This commit is contained in:
108
products/views/__init__.py
Normal file
108
products/views/__init__.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
商品应用视图聚合模块(products/views/__init__.py)
|
||||
|
||||
本模块原为 products/views.py(6105 行单文件),已按功能组拆分到 products/views/ 子包。
|
||||
本文件作为聚合 re-export 入口,重新导出所有视图类与公共符号。
|
||||
外部引用(products/urls.py、jituan/views_miniapp_assets.py、jituan/views_display.py)保持不变。
|
||||
|
||||
功能组拆分:
|
||||
- product_query 商品/会员/分红 数据查询
|
||||
- yajin_payment 押金支付流程
|
||||
- jifen_payment 积分支付流程
|
||||
- huiyuan_payment 会员购买支付流程
|
||||
- chongzhi_payment 商家充值与充值抵扣(含 INTEGRAL_* / IDENTITY_CONFIG 常量)
|
||||
- shangpin_admin 商品发布与管理后台
|
||||
"""
|
||||
import logging
|
||||
|
||||
# 各功能组视图
|
||||
from .product_query import (
|
||||
ShangpinHuoquView,
|
||||
ShangpinXiangqingHuoquView,
|
||||
DashouHuiyuanList,
|
||||
GuanshiFenhongListAPI,
|
||||
AdGetAllDataView,
|
||||
HuiyuanTypeListView,
|
||||
HuiyuanRecordListView,
|
||||
ZuzhangFenhongView,
|
||||
)
|
||||
from .yajin_payment import (
|
||||
YajinGoumai,
|
||||
YajinHuitiao,
|
||||
YajinShibai,
|
||||
YajinLunxun,
|
||||
)
|
||||
from .jifen_payment import (
|
||||
JifenBuchong,
|
||||
JifenHuitiao,
|
||||
JifenShibai,
|
||||
JifenLunxun,
|
||||
)
|
||||
from .huiyuan_payment import (
|
||||
HuiyuanGoumai,
|
||||
HuiyuanHuitiao,
|
||||
HuiyuanShibai,
|
||||
HuiyuanLunxun,
|
||||
)
|
||||
from .chongzhi_payment import (
|
||||
ShangjiaChongzhi,
|
||||
ShangjiaHuitiao,
|
||||
ShangjiaShibai,
|
||||
ShangjiaChenggong,
|
||||
CzhqdyView,
|
||||
DsqrgmdhView,
|
||||
INTEGRAL_PRICE,
|
||||
INTEGRAL_INCREMENT,
|
||||
MAX_INTEGRAL,
|
||||
IDENTITY_CONFIG,
|
||||
)
|
||||
from .shangpin_admin import (
|
||||
ShangpinLeixingFabuView,
|
||||
ShangpinZhuanquFabuView,
|
||||
ShangpinFabuView,
|
||||
GuizeTupianShangchuanView,
|
||||
AdHuiyuanHqView,
|
||||
HuiyuanXiugaiView,
|
||||
AdHuiyuanFbView,
|
||||
AdShangpinHuQu,
|
||||
AdGonggaoXiuGai,
|
||||
AdZhuanquXiuGai,
|
||||
AdLunboXiuGai,
|
||||
AdLeixingXiuGai,
|
||||
AdShangpinNeiRongHuoQu,
|
||||
AdShangpinXiuGai,
|
||||
AdShangpinGuizeXiuGai,
|
||||
)
|
||||
|
||||
# 重新导出原文件从 utils.oss_utils 引入的公共符号,保持外部引用不变
|
||||
# (jituan/views_miniapp_assets.py、jituan/views_display.py 依赖)
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
|
||||
# 模块级符号(保持向后兼容)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
# product_query
|
||||
'ShangpinHuoquView', 'ShangpinXiangqingHuoquView', 'DashouHuiyuanList',
|
||||
'GuanshiFenhongListAPI', 'AdGetAllDataView',
|
||||
'HuiyuanTypeListView', 'HuiyuanRecordListView', 'ZuzhangFenhongView',
|
||||
# yajin_payment
|
||||
'YajinGoumai', 'YajinHuitiao', 'YajinShibai', 'YajinLunxun',
|
||||
# jifen_payment
|
||||
'JifenBuchong', 'JifenHuitiao', 'JifenShibai', 'JifenLunxun',
|
||||
# huiyuan_payment
|
||||
'HuiyuanGoumai', 'HuiyuanHuitiao', 'HuiyuanShibai', 'HuiyuanLunxun',
|
||||
# chongzhi_payment
|
||||
'ShangjiaChongzhi', 'ShangjiaHuitiao', 'ShangjiaShibai', 'ShangjiaChenggong',
|
||||
'CzhqdyView', 'DsqrgmdhView',
|
||||
'INTEGRAL_PRICE', 'INTEGRAL_INCREMENT', 'MAX_INTEGRAL', 'IDENTITY_CONFIG',
|
||||
# shangpin_admin
|
||||
'ShangpinLeixingFabuView', 'ShangpinZhuanquFabuView', 'ShangpinFabuView',
|
||||
'GuizeTupianShangchuanView',
|
||||
'AdHuiyuanHqView', 'HuiyuanXiugaiView', 'AdHuiyuanFbView', 'AdShangpinHuQu',
|
||||
'AdGonggaoXiuGai', 'AdZhuanquXiuGai', 'AdLunboXiuGai', 'AdLeixingXiuGai',
|
||||
'AdShangpinNeiRongHuoQu', 'AdShangpinXiuGai', 'AdShangpinGuizeXiuGai',
|
||||
# 公共符号(向后兼容)
|
||||
'validate_image', 'upload_to_oss', 'delete_from_oss',
|
||||
'logger',
|
||||
]
|
||||
1105
products/views/chongzhi_payment.py
Normal file
1105
products/views/chongzhi_payment.py
Normal file
File diff suppressed because it is too large
Load Diff
677
products/views/huiyuan_payment.py
Normal file
677
products/views/huiyuan_payment.py
Normal file
@@ -0,0 +1,677 @@
|
||||
"""会员购买支付流程视图。
|
||||
|
||||
包含会员购买参数生成、支付回调、失败处理、轮询。
|
||||
"""
|
||||
# ==================== 标准库 ====================
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import hashlib
|
||||
import requests
|
||||
import logging
|
||||
import decimal
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal, InvalidOperation, ROUND_CEILING
|
||||
import defusedxml.ElementTree as ET
|
||||
|
||||
# ==================== Django 内置 ====================
|
||||
from django.conf import settings
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Q
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from django.utils import timezone
|
||||
|
||||
# ==================== DRF 框架 ====================
|
||||
from rest_framework.views import APIView, View
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||
from rest_framework.throttling import AnonRateThrottle
|
||||
|
||||
# ==================== 项目工具函数 ====================
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from utils.money import format_yuan, quantize_yuan, yuan_to_fen
|
||||
from backend.utils import (
|
||||
update_guanshi_daily_by_action,
|
||||
update_guanshi_xufei_daily,
|
||||
update_zuzhang_daily_by_action
|
||||
)
|
||||
from users.fadan_fenhong_utils import process_fadan_fenhong
|
||||
|
||||
# ==================== 各App模型 ====================
|
||||
from ..models import (
|
||||
ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan,
|
||||
Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong
|
||||
)
|
||||
from users.models import (
|
||||
AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
from orders.models import CommissionRate
|
||||
from config.models import Gonggao, Lunbo
|
||||
|
||||
from orders.models import Penalty # 用于罚款类型
|
||||
from jituan.services.club_config import (
|
||||
get_commission_rate,
|
||||
get_commission_rate_object,
|
||||
get_huiyuan_price,
|
||||
get_huiyuan_fenchong,
|
||||
)
|
||||
from jituan.services.club_context import resolve_effective_club_id
|
||||
from jituan.services.club_write import resolve_club_id_for_write
|
||||
from jituan.services.club_user import get_payment_openid
|
||||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||
|
||||
import traceback
|
||||
|
||||
# ==================== 日志实例 ====================
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HuiyuanGoumai(APIView):
|
||||
"""
|
||||
会员充值支付参数接口
|
||||
路径:/shangpin/huiyuangm
|
||||
请求方式:POST
|
||||
权限:JWT认证
|
||||
参数:huiyuanid(会员ID)
|
||||
返回:支付参数(payParams)和订单ID(dingdanid)
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 1. 获取会员ID参数
|
||||
huiyuanid = request.data.get('huiyuanid')
|
||||
is_trial = bool(request.data.get('is_trial') or request.data.get('isTrial'))
|
||||
if not huiyuanid:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '会员ID不能为空'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2. 验证用户身份和状态
|
||||
try:
|
||||
dashou = request.user.DashouProfile
|
||||
except UserDashou.DoesNotExist:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '用户不是打手,无法购买会员'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 检查打手账号状态
|
||||
if dashou.zhanghaozhuangtai != 1:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '账号状态异常,无法进行购买'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4. 查询会员信息
|
||||
try:
|
||||
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuanid)
|
||||
except Huiyuan.DoesNotExist:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '会员信息不存在'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
club_id = resolve_effective_club_id(request, request.user)
|
||||
from jituan.services.member_recharge import validate_member_purchase
|
||||
|
||||
ok, err_msg, club_price, purchase_days = validate_member_purchase(
|
||||
request.user.UserUID, huiyuanid, club_id, is_trial,
|
||||
)
|
||||
if not ok:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': err_msg or '不可购买',
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 5. 生成订单ID(前缀CzHY表示会员充值)
|
||||
timestamp_ms = int(time.time() * 1000)
|
||||
timestamp_ns = int(time.time_ns() // 1000) % 1000000
|
||||
random_num = random.randint(0, 99)
|
||||
|
||||
dingdanid = f"CzHY{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
|
||||
|
||||
# 6. 创建充值记录订单(金额仅用本俱乐部配置)
|
||||
jine = quantize_yuan(club_price)
|
||||
# 组合订单说明:会员介绍 + 配置描述
|
||||
shuoming = f"{huiyuan.jieshao} - {settings.HUIYUAN_PAY_DESCRIPTION}"
|
||||
|
||||
chongzhi_jilu = Czjilu.query.create(
|
||||
dingdan_id=dingdanid,
|
||||
zhuangtai=9, # 未支付状态
|
||||
yonghuid=request.user.UserUID,
|
||||
jine=jine,
|
||||
leixing=1, # 充值类型:1=会员
|
||||
shuoming=shuoming,
|
||||
huiyuan_id=huiyuanid, # 存储会员ID
|
||||
club_id=club_id,
|
||||
is_trial=is_trial,
|
||||
purchase_days=int(purchase_days),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"创建会员充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, "
|
||||
f"会员{huiyuanid}, 俱乐部{club_id}, 体验={is_trial}, "
|
||||
f"金额{jine}元, 天数{purchase_days}")
|
||||
|
||||
# 7. 生成微信支付参数
|
||||
try:
|
||||
pay_openid, _ = get_payment_openid(request)
|
||||
if not pay_openid:
|
||||
chongzhi_jilu.delete()
|
||||
return Response({'code': 400, 'message': '用户openid不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
pay_params = self.generate_wechat_pay_params(
|
||||
dingdanid=dingdanid,
|
||||
jine=jine,
|
||||
openid=pay_openid,
|
||||
pay_type='huiyuan'
|
||||
)
|
||||
except Exception as e:
|
||||
# 支付参数生成失败,删除订单记录
|
||||
chongzhi_jilu.delete()
|
||||
logger.error(f"生成会员支付参数失败: {str(e)}")
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'支付参数生成失败: {str(e)}'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
# 8. 返回支付参数和订单ID
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '支付参数生成成功',
|
||||
'payParams': pay_params,
|
||||
'dingdanid': dingdanid,
|
||||
'club_id': club_id,
|
||||
'jine': format_yuan(jine),
|
||||
'is_trial': is_trial,
|
||||
'purchase_days': purchase_days,
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"会员充值接口异常: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '服务器内部错误,请稍后重试'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
|
||||
"""生成微信支付参数(完整版,无省略)"""
|
||||
# 微信支付配置 - 从settings中获取
|
||||
from jituan.services.club_write import resolve_club_id_for_write
|
||||
from jituan.services.wechat_pay import get_wechat_v2_config
|
||||
pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user))
|
||||
APPID = pay_cfg['appid']
|
||||
MCHID = pay_cfg['mch_id']
|
||||
KEY = pay_cfg['key']
|
||||
|
||||
# 根据支付类型获取对应的配置
|
||||
if pay_type == 'yajin':
|
||||
PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION
|
||||
NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL
|
||||
elif pay_type == 'jifen':
|
||||
PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION
|
||||
NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL
|
||||
elif pay_type == 'huiyuan':
|
||||
PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION
|
||||
NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL
|
||||
else:
|
||||
raise Exception('未知的支付类型')
|
||||
|
||||
# 生成随机字符串
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
|
||||
# 金额转换(元转分)
|
||||
total_fee = yuan_to_fen(jine)
|
||||
|
||||
# 交易类型(小程序支付使用JSAPI)
|
||||
trade_type = 'JSAPI'
|
||||
|
||||
# 获取客户端IP
|
||||
user_ip = self.get_client_ip()
|
||||
|
||||
# 1. 第一次签名(统一下单接口)
|
||||
sign_data = {
|
||||
'appid': APPID,
|
||||
'mch_id': MCHID,
|
||||
'nonce_str': nonce_str,
|
||||
'body': PAY_DESCRIPTION,
|
||||
'out_trade_no': dingdanid,
|
||||
'total_fee': total_fee,
|
||||
'spbill_create_ip': user_ip,
|
||||
'notify_url': NOTIFY_URL,
|
||||
'trade_type': trade_type,
|
||||
'openid': openid # 小程序支付必须传openid
|
||||
}
|
||||
|
||||
# 按照参数名ASCII码从小到大排序
|
||||
sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
|
||||
|
||||
# 拼接字符串
|
||||
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
|
||||
sign_string += f'&key={KEY}'
|
||||
|
||||
# MD5签名
|
||||
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
|
||||
|
||||
# 构造XML数据
|
||||
xml_data = f"""<xml>
|
||||
<appid>{APPID}</appid>
|
||||
<body>{PAY_DESCRIPTION}</body>
|
||||
<mch_id>{MCHID}</mch_id>
|
||||
<nonce_str>{nonce_str}</nonce_str>
|
||||
<notify_url>{NOTIFY_URL}</notify_url>
|
||||
<openid>{openid}</openid>
|
||||
<out_trade_no>{dingdanid}</out_trade_no>
|
||||
<spbill_create_ip>{user_ip}</spbill_create_ip>
|
||||
<total_fee>{total_fee}</total_fee>
|
||||
<trade_type>{trade_type}</trade_type>
|
||||
<sign>{sign}</sign>
|
||||
</xml>"""
|
||||
|
||||
# 调用微信统一下单接口(官方地址)
|
||||
response = requests.post(
|
||||
'https://api.mch.weixin.qq.com/pay/unifiedorder',
|
||||
data=xml_data.encode('utf-8'),
|
||||
headers={'Content-Type': 'application/xml'},
|
||||
timeout=10 # 设置超时
|
||||
)
|
||||
|
||||
# 解析返回的XML
|
||||
root = ET.fromstring(response.content)
|
||||
|
||||
return_code = root.find('return_code').text
|
||||
result_code = root.find('result_code').text
|
||||
|
||||
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
|
||||
# 获取prepay_id
|
||||
prepay_id = root.find('prepay_id').text
|
||||
|
||||
# 2. 第二次签名(小程序支付参数)
|
||||
timestamp = str(int(time.time()))
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
package = f'prepay_id={prepay_id}'
|
||||
sign_type = 'MD5'
|
||||
|
||||
# 生成支付签名(注意:这里key的大小写和字段名与第一次不同)
|
||||
pay_sign_data = {
|
||||
'appId': APPID, # 注意:这里是appId,不是appid
|
||||
'timeStamp': timestamp, # 注意:这里是timeStamp,不是timestamp
|
||||
'nonceStr': nonce_str,
|
||||
'package': package,
|
||||
'signType': sign_type
|
||||
}
|
||||
|
||||
sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
|
||||
pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
|
||||
pay_sign_string += f'&key={KEY}'
|
||||
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
|
||||
|
||||
# 返回前端需要的支付参数
|
||||
return {
|
||||
'appId': APPID, # 对应前端:appId
|
||||
'timeStamp': timestamp, # 对应前端:timeStamp
|
||||
'nonceStr': nonce_str, # 对应前端:nonceStr
|
||||
'package': package, # 对应前端:package
|
||||
'signType': sign_type, # 对应前端:signType
|
||||
'paySign': pay_sign # 对应前端:paySign
|
||||
}
|
||||
else:
|
||||
# 获取错误信息
|
||||
err_code = root.find('err_code')
|
||||
err_code_des = root.find('err_code_des')
|
||||
return_msg = root.find('return_msg')
|
||||
|
||||
error_details = f"return_code: {return_code}"
|
||||
if err_code is not None:
|
||||
error_details += f", err_code: {err_code.text}"
|
||||
if err_code_des is not None:
|
||||
error_details += f", err_code_des: {err_code_des.text}"
|
||||
if return_msg is not None:
|
||||
error_details += f", return_msg: {return_msg.text}"
|
||||
|
||||
raise Exception(f"微信支付下单失败: {error_details}")
|
||||
|
||||
def get_client_ip(self):
|
||||
"""获取客户端IP(完整版)"""
|
||||
request = self.request
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip if ip else '127.0.0.1'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class HuiyuanHuitiao(View):
|
||||
"""
|
||||
会员支付回调接口
|
||||
路径:/shangpin/huiyuanhuitiao
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 1. 解析微信回调XML数据
|
||||
request_body = request.body
|
||||
logger.info(f"收到会员支付回调,原始数据: {request_body.decode('utf-8')}")
|
||||
|
||||
root = ET.fromstring(request_body)
|
||||
|
||||
return_code = root.find('return_code').text
|
||||
result_code = root.find('result_code').text
|
||||
out_trade_no = root.find('out_trade_no').text
|
||||
transaction_id = root.find('transaction_id').text
|
||||
total_fee = int(root.find('total_fee').text)
|
||||
|
||||
logger.info(f"会员回调数据: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分")
|
||||
|
||||
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
|
||||
logger.warning(f"会员支付回调状态异常: {return_code}/{result_code}")
|
||||
return self.wechat_response('FAIL', '支付失败')
|
||||
|
||||
from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature
|
||||
notify_club = club_id_from_czjilu(out_trade_no)
|
||||
if not verify_wechat_v2_signature(request_body, notify_club):
|
||||
logger.error(f"会员签名验证失败: {out_trade_no}")
|
||||
return self.wechat_response('FAIL', '签名验证失败')
|
||||
|
||||
# 查询订单
|
||||
try:
|
||||
order = Czjilu.query.get(dingdan_id=out_trade_no)
|
||||
except Czjilu.DoesNotExist:
|
||||
logger.error(f"会员订单不存在: {out_trade_no}")
|
||||
return self.wechat_response('FAIL', '订单不存在')
|
||||
|
||||
if order.leixing != 1:
|
||||
logger.error(f"订单类型错误: {order.leixing}, 期望1(会员)")
|
||||
return self.wechat_response('FAIL', '订单类型错误')
|
||||
|
||||
if order.zhuangtai != 9:
|
||||
logger.warning(f"会员订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
|
||||
if order.zhuangtai == 3:
|
||||
from jituan.services.member_recharge import repair_member_entitlement_if_paid
|
||||
repair_member_entitlement_if_paid(out_trade_no)
|
||||
try:
|
||||
from decimal import Decimal
|
||||
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
|
||||
repair_wechat_income_if_missing(
|
||||
Decimal(str(order.jine)),
|
||||
getattr(order, 'club_id', None),
|
||||
biz_ref=f'cz:{out_trade_no}',
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error('会员回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True)
|
||||
return self.wechat_response('SUCCESS', 'OK')
|
||||
|
||||
wechat_amount_yuan = total_fee / 100
|
||||
order_amount_yuan = float(order.jine)
|
||||
if abs(wechat_amount_yuan - order_amount_yuan) > 0.01:
|
||||
logger.error(f"会员金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元")
|
||||
return self.wechat_response('FAIL', '金额不一致')
|
||||
|
||||
from jituan.services.member_recharge import fulfill_member_recharge, MemberRechargeError
|
||||
try:
|
||||
fulfill_member_recharge(
|
||||
out_trade_no,
|
||||
paid_amount_yuan=wechat_amount_yuan,
|
||||
source='callback',
|
||||
)
|
||||
except MemberRechargeError as exc:
|
||||
logger.error('会员回调履约失败 %s: %s', out_trade_no, exc)
|
||||
return self.wechat_response('FAIL', str(exc))
|
||||
|
||||
logger.info(f"会员支付回调处理完成: {out_trade_no}")
|
||||
return self.wechat_response('SUCCESS', 'OK')
|
||||
|
||||
except ET.ParseError as e:
|
||||
logger.error(f"会员XML解析失败: {str(e)}")
|
||||
return self.wechat_response('FAIL', '数据格式错误')
|
||||
except Exception as e:
|
||||
logger.error(f"会员支付回调处理异常: {str(e)}", exc_info=True)
|
||||
return self.wechat_response('FAIL', '处理异常')
|
||||
|
||||
# 履约逻辑见 jituan.services.member_recharge(回调与 /huiyuanlx 确认共用)
|
||||
|
||||
def verify_signature(self, xml_data):
|
||||
"""验证微信支付签名(与原代码一致)"""
|
||||
try:
|
||||
root = ET.fromstring(xml_data)
|
||||
sign = root.find('sign').text
|
||||
if not sign:
|
||||
logger.error("会员签名字段不存在")
|
||||
return False
|
||||
|
||||
params = {}
|
||||
for child in root:
|
||||
if child.tag != 'sign' and child.text is not None:
|
||||
params[child.tag] = child.text
|
||||
|
||||
sorted_params = sorted(params.items(), key=lambda x: x[0])
|
||||
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
|
||||
sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
|
||||
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
|
||||
return sign == calculated_sign
|
||||
except Exception as e:
|
||||
logger.error(f"会员验证签名异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def wechat_response(self, return_code, return_msg):
|
||||
"""返回微信回调响应"""
|
||||
xml_response = f"""<xml>
|
||||
<return_code><![CDATA[{return_code}]]></return_code>
|
||||
<return_msg><![CDATA[{return_msg}]]></return_msg>
|
||||
</xml>"""
|
||||
logger.info(f"会员返回微信响应: {return_code} - {return_msg}")
|
||||
return HttpResponse(xml_response, content_type='application/xml')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# views.py
|
||||
class HuiyuanShibai(APIView):
|
||||
"""
|
||||
会员支付失败处理接口
|
||||
路径:/shangpin/huiyuanshibai
|
||||
请求方式:POST
|
||||
权限:JWT认证
|
||||
参数:dingdanid(订单ID)
|
||||
返回:删除结果
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 1. 获取订单ID
|
||||
dingdanid = request.data.get('dingdanid')
|
||||
if not dingdanid:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单ID不能为空'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2. 查询订单(确保是当前用户的订单)
|
||||
try:
|
||||
order = Czjilu.query.get(
|
||||
dingdan_id=dingdanid,
|
||||
yonghuid=request.user.UserUID
|
||||
)
|
||||
except Czjilu.DoesNotExist:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单不存在或不属于当前用户'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 检查订单类型是否为会员(1)
|
||||
if order.leixing != 1:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单类型错误,不是会员订单'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4. 检查订单状态是否为未支付(9)
|
||||
if order.zhuangtai == 9:
|
||||
# 删除订单
|
||||
order.delete()
|
||||
logger.info(f"用户{request.user.UserUID}删除未支付会员订单: {dingdanid}")
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '会员订单已删除'
|
||||
}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '会员订单状态不是未支付,无法删除'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"会员支付失败处理异常: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '服务器内部错误'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
# views.py
|
||||
class HuiyuanLunxun(APIView):
|
||||
"""
|
||||
会员支付成功轮询接口
|
||||
路径:/shangpin/huiyuanlx
|
||||
请求方式:POST
|
||||
权限:JWT认证
|
||||
参数:dingdanid(订单ID)
|
||||
返回:订单状态和会员信息
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 1. 获取订单ID
|
||||
dingdanid = request.data.get('dingdanid')
|
||||
if not dingdanid:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单ID不能为空'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2. 查询订单(确保是当前用户的订单)
|
||||
try:
|
||||
order = Czjilu.query.get(
|
||||
dingdan_id=dingdanid,
|
||||
yonghuid=request.user.UserUID
|
||||
)
|
||||
except Czjilu.DoesNotExist:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单不存在或不属于当前用户'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 检查订单类型是否为会员(1)
|
||||
if order.leixing != 1:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单类型错误,不是会员订单'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4. 未支付:查微信并幂等履约(与回调互斥,zhuangtai!=9 则跳过)
|
||||
if order.zhuangtai == 9:
|
||||
from jituan.services.member_recharge import (
|
||||
confirm_member_recharge_paid,
|
||||
MemberRechargeError,
|
||||
)
|
||||
try:
|
||||
confirm_member_recharge_paid(dingdanid, request.user.UserUID)
|
||||
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
|
||||
except MemberRechargeError as exc:
|
||||
logger.info(f"会员确认未就绪 {dingdanid}: {exc}")
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': str(exc),
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if order.zhuangtai != 3:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '会员订单尚未支付完成'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
from jituan.services.member_recharge import repair_member_entitlement_if_paid
|
||||
repair_member_entitlement_if_paid(dingdanid)
|
||||
|
||||
# 5. 查询会员信息
|
||||
try:
|
||||
huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id)
|
||||
|
||||
# 6. 查询会员购买记录
|
||||
goumai_record = Huiyuangoumai.query.filter(
|
||||
yonghu_id=request.user.UserUID,
|
||||
huiyuan_id=order.huiyuan_id
|
||||
).first()
|
||||
|
||||
if not goumai_record:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '会员购买记录不存在'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 7. 格式化到期时间
|
||||
#from django.utils.timezone import localtime
|
||||
#daoqi_time = localtime(goumai_record.daoqi_time).strftime('%Y-%m-%d %H:%M:%S')
|
||||
# 直接格式化,不使用时区转换
|
||||
daoqi_time = goumai_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
current_user = request.user
|
||||
dashou = current_user.DashouProfile
|
||||
|
||||
logger.info(f"会员轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}, 会员{order.huiyuan_id}")
|
||||
|
||||
# 8. 返回会员信息(用于更新前端全局变量)
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '支付流程全部完成',
|
||||
'jifen': dashou.jifen,
|
||||
'huiyuan': {
|
||||
'huiyuanid': order.huiyuan_id,
|
||||
'huiyuanming': huiyuan.jieshao,
|
||||
'daoqi': daoqi_time
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except Huiyuan.DoesNotExist:
|
||||
logger.error(f"会员信息不存在: {order.huiyuan_id}")
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '会员信息不存在'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"会员轮询异常: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '服务器内部错误'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
|
||||
# views.py in shangpin app
|
||||
|
||||
|
||||
|
||||
607
products/views/jifen_payment.py
Normal file
607
products/views/jifen_payment.py
Normal file
@@ -0,0 +1,607 @@
|
||||
"""积分支付流程视图。
|
||||
|
||||
包含积分补充、充值回调、失败处理、轮询。
|
||||
"""
|
||||
# ==================== 标准库 ====================
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import hashlib
|
||||
import requests
|
||||
import logging
|
||||
import decimal
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal, InvalidOperation, ROUND_CEILING
|
||||
import defusedxml.ElementTree as ET
|
||||
|
||||
# ==================== Django 内置 ====================
|
||||
from django.conf import settings
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Q
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from django.utils import timezone
|
||||
|
||||
# ==================== DRF 框架 ====================
|
||||
from rest_framework.views import APIView, View
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||
from rest_framework.throttling import AnonRateThrottle
|
||||
|
||||
# ==================== 项目工具函数 ====================
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from utils.money import format_yuan, quantize_yuan, yuan_to_fen
|
||||
from backend.utils import (
|
||||
update_guanshi_daily_by_action,
|
||||
update_guanshi_xufei_daily,
|
||||
update_zuzhang_daily_by_action
|
||||
)
|
||||
from users.fadan_fenhong_utils import process_fadan_fenhong
|
||||
|
||||
# ==================== 各App模型 ====================
|
||||
from ..models import (
|
||||
ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan,
|
||||
Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong
|
||||
)
|
||||
from users.models import (
|
||||
AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
from orders.models import CommissionRate
|
||||
from config.models import Gonggao, Lunbo
|
||||
|
||||
from orders.models import Penalty # 用于罚款类型
|
||||
from jituan.services.club_config import (
|
||||
get_commission_rate,
|
||||
get_commission_rate_object,
|
||||
get_huiyuan_price,
|
||||
get_huiyuan_fenchong,
|
||||
)
|
||||
from jituan.services.club_context import resolve_effective_club_id
|
||||
from jituan.services.club_write import resolve_club_id_for_write
|
||||
from jituan.services.club_user import get_payment_openid
|
||||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||
|
||||
import traceback
|
||||
|
||||
# ==================== 日志实例 ====================
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class JifenBuchong(APIView):
|
||||
"""
|
||||
积分补充支付参数接口
|
||||
路径:/shangpin/jifenbc
|
||||
请求方式:POST
|
||||
权限:JWT认证
|
||||
参数:无(固定5元)
|
||||
返回:支付参数(payParams)和订单ID(dingdanid)
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 1. 验证用户身份和状态
|
||||
try:
|
||||
dashou = request.user.DashouProfile
|
||||
except UserDashou.DoesNotExist:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '用户不是打手,无法补充积分'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2. 检查打手账号状态
|
||||
if dashou.zhanghaozhuangtai != 1:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '账号状态异常,无法进行操作'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 检查积分是否已满(满分10分)
|
||||
if dashou.jifen >= 10:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '积分已满,无需补充'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4. 生成订单ID(前缀CzJF表示积分充值)
|
||||
timestamp_ms = int(time.time() * 1000)
|
||||
timestamp_ns = int(time.time_ns() // 1000) % 1000000
|
||||
random_num = random.randint(0, 99)
|
||||
|
||||
dingdanid = f"CzJF{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
|
||||
|
||||
# 5. 创建积分充值记录订单(固定5元)
|
||||
jine = 5.00 # 固定5元
|
||||
chongzhi_jilu = Czjilu.query.create(
|
||||
dingdan_id=dingdanid,
|
||||
zhuangtai=9, # 未支付状态
|
||||
yonghuid=request.user.UserUID,
|
||||
jine=jine,
|
||||
leixing=3, # 充值类型:3=积分
|
||||
shuoming=settings.JIFEN_PAY_DESCRIPTION,
|
||||
club_id=resolve_club_id_for_write(request, request.user),
|
||||
)
|
||||
|
||||
logger.info(f"创建积分充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, 金额{jine}元")
|
||||
|
||||
# 6. 生成微信支付参数(完整版)
|
||||
try:
|
||||
pay_openid, _ = get_payment_openid(request)
|
||||
if not pay_openid:
|
||||
chongzhi_jilu.delete()
|
||||
return Response({'code': 400, 'message': '用户openid不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
pay_params = self.generate_wechat_pay_params(
|
||||
dingdanid=dingdanid,
|
||||
jine=jine,
|
||||
openid=pay_openid,
|
||||
pay_type='jifen'
|
||||
)
|
||||
except Exception as e:
|
||||
# 支付参数生成失败,删除订单记录
|
||||
chongzhi_jilu.delete()
|
||||
logger.error(f"生成积分支付参数失败: {str(e)}")
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'支付参数生成失败: {str(e)}'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
# 7. 返回支付参数和订单ID
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '支付参数生成成功',
|
||||
'payParams': pay_params, # 对应前端payParams
|
||||
'dingdanid': dingdanid # 对应前端dingdanid
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"积分充值接口异常: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '服务器内部错误,请稍后重试'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
|
||||
"""生成微信支付参数(完整版,无省略)"""
|
||||
# 微信支付配置 - 从settings中获取
|
||||
from jituan.services.club_write import resolve_club_id_for_write
|
||||
from jituan.services.wechat_pay import get_wechat_v2_config
|
||||
pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user))
|
||||
APPID = pay_cfg['appid']
|
||||
MCHID = pay_cfg['mch_id']
|
||||
KEY = pay_cfg['key']
|
||||
|
||||
# 根据支付类型获取对应的配置
|
||||
if pay_type == 'yajin':
|
||||
PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION
|
||||
NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL
|
||||
elif pay_type == 'jifen':
|
||||
PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION
|
||||
NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL
|
||||
elif pay_type == 'huiyuan':
|
||||
PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION
|
||||
NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL
|
||||
else:
|
||||
raise Exception('未知的支付类型')
|
||||
|
||||
# 生成随机字符串
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
|
||||
# 金额转换(元转分)
|
||||
total_fee = yuan_to_fen(jine)
|
||||
|
||||
# 交易类型(小程序支付使用JSAPI)
|
||||
trade_type = 'JSAPI'
|
||||
|
||||
# 获取客户端IP
|
||||
user_ip = self.get_client_ip()
|
||||
|
||||
# 1. 第一次签名(统一下单接口)
|
||||
sign_data = {
|
||||
'appid': APPID,
|
||||
'mch_id': MCHID,
|
||||
'nonce_str': nonce_str,
|
||||
'body': PAY_DESCRIPTION,
|
||||
'out_trade_no': dingdanid,
|
||||
'total_fee': total_fee,
|
||||
'spbill_create_ip': user_ip,
|
||||
'notify_url': NOTIFY_URL,
|
||||
'trade_type': trade_type,
|
||||
'openid': openid # 小程序支付必须传openid
|
||||
}
|
||||
|
||||
# 按照参数名ASCII码从小到大排序
|
||||
sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
|
||||
|
||||
# 拼接字符串
|
||||
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
|
||||
sign_string += f'&key={KEY}'
|
||||
|
||||
# MD5签名
|
||||
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
|
||||
|
||||
# 构造XML数据
|
||||
xml_data = f"""<xml>
|
||||
<appid>{APPID}</appid>
|
||||
<body>{PAY_DESCRIPTION}</body>
|
||||
<mch_id>{MCHID}</mch_id>
|
||||
<nonce_str>{nonce_str}</nonce_str>
|
||||
<notify_url>{NOTIFY_URL}</notify_url>
|
||||
<openid>{openid}</openid>
|
||||
<out_trade_no>{dingdanid}</out_trade_no>
|
||||
<spbill_create_ip>{user_ip}</spbill_create_ip>
|
||||
<total_fee>{total_fee}</total_fee>
|
||||
<trade_type>{trade_type}</trade_type>
|
||||
<sign>{sign}</sign>
|
||||
</xml>"""
|
||||
|
||||
# 调用微信统一下单接口(官方地址)
|
||||
response = requests.post(
|
||||
'https://api.mch.weixin.qq.com/pay/unifiedorder',
|
||||
data=xml_data.encode('utf-8'),
|
||||
headers={'Content-Type': 'application/xml'},
|
||||
timeout=10 # 设置超时
|
||||
)
|
||||
|
||||
# 解析返回的XML
|
||||
root = ET.fromstring(response.content)
|
||||
|
||||
return_code = root.find('return_code').text
|
||||
result_code = root.find('result_code').text
|
||||
|
||||
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
|
||||
# 获取prepay_id
|
||||
prepay_id = root.find('prepay_id').text
|
||||
|
||||
# 2. 第二次签名(小程序支付参数)
|
||||
timestamp = str(int(time.time()))
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
package = f'prepay_id={prepay_id}'
|
||||
sign_type = 'MD5'
|
||||
|
||||
# 生成支付签名(注意:这里key的大小写和字段名与第一次不同)
|
||||
pay_sign_data = {
|
||||
'appId': APPID, # 注意:这里是appId,不是appid
|
||||
'timeStamp': timestamp, # 注意:这里是timeStamp,不是timestamp
|
||||
'nonceStr': nonce_str,
|
||||
'package': package,
|
||||
'signType': sign_type
|
||||
}
|
||||
|
||||
sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
|
||||
pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
|
||||
pay_sign_string += f'&key={KEY}'
|
||||
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
|
||||
|
||||
# 返回前端需要的支付参数
|
||||
return {
|
||||
'appId': APPID, # 对应前端:appId
|
||||
'timeStamp': timestamp, # 对应前端:timeStamp
|
||||
'nonceStr': nonce_str, # 对应前端:nonceStr
|
||||
'package': package, # 对应前端:package
|
||||
'signType': sign_type, # 对应前端:signType
|
||||
'paySign': pay_sign # 对应前端:paySign
|
||||
}
|
||||
else:
|
||||
# 获取错误信息
|
||||
err_code = root.find('err_code')
|
||||
err_code_des = root.find('err_code_des')
|
||||
return_msg = root.find('return_msg')
|
||||
|
||||
error_details = f"return_code: {return_code}"
|
||||
if err_code is not None:
|
||||
error_details += f", err_code: {err_code.text}"
|
||||
if err_code_des is not None:
|
||||
error_details += f", err_code_des: {err_code_des.text}"
|
||||
if return_msg is not None:
|
||||
error_details += f", return_msg: {return_msg.text}"
|
||||
|
||||
raise Exception(f"微信支付下单失败: {error_details}")
|
||||
|
||||
def get_client_ip(self):
|
||||
"""获取客户端IP(完整版)"""
|
||||
request = self.request
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip if ip else '127.0.0.1'
|
||||
|
||||
|
||||
# views.py
|
||||
class JifenHuitiao(View):
|
||||
"""
|
||||
积分支付回调接口
|
||||
路径:/shangpin/jifenhuitiao
|
||||
注意:这个接口不需要JWT认证,由微信服务器调用
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 1. 解析微信回调XML数据
|
||||
request_body = request.body
|
||||
logger.info(f"收到积分支付回调,原始数据: {request_body.decode('utf-8')}")
|
||||
|
||||
root = ET.fromstring(request_body)
|
||||
|
||||
# 获取关键字段
|
||||
return_code = root.find('return_code').text
|
||||
result_code = root.find('result_code').text
|
||||
out_trade_no = root.find('out_trade_no').text # 订单ID
|
||||
transaction_id = root.find('transaction_id').text # 微信交易号
|
||||
total_fee = int(root.find('total_fee').text) # 支付金额(分)
|
||||
|
||||
logger.info(f"积分回调数据: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分")
|
||||
|
||||
# 2. 验证基本返回状态
|
||||
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
|
||||
logger.warning(f"积分支付回调状态异常: {return_code}/{result_code}")
|
||||
return self.wechat_response('FAIL', '支付失败')
|
||||
|
||||
# 3. 验证签名
|
||||
from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature
|
||||
notify_club = club_id_from_czjilu(out_trade_no)
|
||||
if not verify_wechat_v2_signature(request_body, notify_club):
|
||||
logger.error(f"积分签名验证失败: {out_trade_no}")
|
||||
return self.wechat_response('FAIL', '签名验证失败')
|
||||
|
||||
# 4. 查询订单
|
||||
try:
|
||||
order = Czjilu.query.get(dingdan_id=out_trade_no)
|
||||
logger.info(f"找到积分订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}")
|
||||
except Czjilu.DoesNotExist:
|
||||
logger.error(f"积分订单不存在: {out_trade_no}")
|
||||
return self.wechat_response('FAIL', '订单不存在')
|
||||
|
||||
# 5. 检查订单状态是否为未支付(9)
|
||||
if order.zhuangtai != 9:
|
||||
logger.warning(f"积分订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
|
||||
try:
|
||||
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
|
||||
repair_wechat_income_if_missing(
|
||||
Decimal(str(order.jine)),
|
||||
getattr(order, 'club_id', None),
|
||||
biz_ref=f'cz:{out_trade_no}',
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error('积分回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True)
|
||||
return self.wechat_response('SUCCESS', 'OK')
|
||||
|
||||
# 6. 验证金额(固定5元)
|
||||
wechat_amount_yuan = total_fee / 100 # 转换为元
|
||||
order_amount_yuan = float(order.jine) # 订单金额(元)
|
||||
|
||||
# 验证是否是5元(允许1分钱误差)
|
||||
if abs(wechat_amount_yuan - 5.00) > 0.01:
|
||||
logger.error(f"积分金额错误: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元")
|
||||
return self.wechat_response('FAIL', '金额错误')
|
||||
|
||||
from jituan.services.czjilu_fulfill import fulfill_jifen_recharge, CzjiluFulfillError
|
||||
try:
|
||||
fulfill_jifen_recharge(out_trade_no, paid_amount_yuan=wechat_amount_yuan)
|
||||
except CzjiluFulfillError as exc:
|
||||
logger.error('积分回调履约失败 %s: %s', out_trade_no, exc)
|
||||
return self.wechat_response('FAIL', str(exc))
|
||||
|
||||
logger.info(f"积分支付回调处理完成: {out_trade_no}")
|
||||
return self.wechat_response('SUCCESS', 'OK')
|
||||
|
||||
except ET.ParseError as e:
|
||||
logger.error(f"积分XML解析失败: {str(e)}")
|
||||
return self.wechat_response('FAIL', '数据格式错误')
|
||||
except Exception as e:
|
||||
logger.error(f"积分支付回调处理异常: {str(e)}", exc_info=True)
|
||||
return self.wechat_response('FAIL', '处理异常')
|
||||
|
||||
def verify_signature(self, xml_data):
|
||||
"""验证微信支付签名"""
|
||||
try:
|
||||
root = ET.fromstring(xml_data)
|
||||
sign = root.find('sign').text
|
||||
|
||||
if not sign:
|
||||
logger.error("积分数签名字段不存在")
|
||||
return False
|
||||
|
||||
# 获取所有参数并排除sign
|
||||
params = {}
|
||||
for child in root:
|
||||
if child.tag != 'sign' and child.text is not None:
|
||||
params[child.tag] = child.text
|
||||
|
||||
# 按照参数名ASCII码从小到大排序
|
||||
sorted_params = sorted(params.items(), key=lambda x: x[0])
|
||||
|
||||
# 拼接字符串
|
||||
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
|
||||
sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
|
||||
|
||||
# 计算签名
|
||||
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
|
||||
|
||||
is_valid = sign == calculated_sign
|
||||
if not is_valid:
|
||||
logger.error(f"积分签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}")
|
||||
|
||||
return is_valid
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"积分验证签名异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def wechat_response(self, return_code, return_msg):
|
||||
"""返回微信支付回调响应"""
|
||||
xml_response = f"""<xml>
|
||||
<return_code><![CDATA[{return_code}]]></return_code>
|
||||
<return_msg><![CDATA[{return_msg}]]></return_msg>
|
||||
</xml>"""
|
||||
|
||||
logger.info(f"积分返回微信响应: {return_code} - {return_msg}")
|
||||
return HttpResponse(xml_response, content_type='application/xml')
|
||||
|
||||
|
||||
# views.py
|
||||
class JifenShibai(APIView):
|
||||
"""
|
||||
积分支付失败处理接口
|
||||
路径:/shangpin/jifenshibai
|
||||
请求方式:POST
|
||||
权限:JWT认证
|
||||
参数:dingdanid(订单ID)
|
||||
返回:删除结果
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 1. 获取订单ID
|
||||
dingdanid = request.data.get('dingdanid')
|
||||
if not dingdanid:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单ID不能为空'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2. 查询订单(确保是当前用户的订单)
|
||||
try:
|
||||
order = Czjilu.query.get(
|
||||
dingdan_id=dingdanid,
|
||||
yonghuid=request.user.UserUID
|
||||
)
|
||||
except Czjilu.DoesNotExist:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单不存在或不属于当前用户'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 检查订单类型是否为积分(3)
|
||||
if order.leixing != 3:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单类型错误,不是积分订单'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4. 检查订单状态是否为未支付(9)
|
||||
if order.zhuangtai == 9:
|
||||
# 删除订单
|
||||
order.delete()
|
||||
logger.info(f"用户{request.user.UserUID}删除未支付积分订单: {dingdanid}")
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '积分订单已删除'
|
||||
}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '积分订单状态不是未支付,无法删除'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"积分支付失败处理异常: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '服务器内部错误'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
# views.py
|
||||
class JifenLunxun(APIView):
|
||||
"""
|
||||
积分支付成功轮询接口
|
||||
路径:/shangpin/jifenlunxun
|
||||
请求方式:POST
|
||||
权限:JWT认证
|
||||
参数:dingdanid(订单ID)
|
||||
返回:订单状态和当前积分
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 1. 获取订单ID
|
||||
dingdanid = request.data.get('dingdanid')
|
||||
if not dingdanid:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单ID不能为空'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2. 查询订单(确保是当前用户的订单)
|
||||
try:
|
||||
order = Czjilu.query.get(
|
||||
dingdan_id=dingdanid,
|
||||
yonghuid=request.user.UserUID
|
||||
)
|
||||
except Czjilu.DoesNotExist:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单不存在或不属于当前用户'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 检查订单类型是否为积分(3)
|
||||
if order.leixing != 3:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单类型错误,不是积分订单'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4. 未支付则查微信并幂等履约
|
||||
if order.zhuangtai == 9:
|
||||
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
|
||||
try:
|
||||
confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=3)
|
||||
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
|
||||
except CzjiluFulfillError as exc:
|
||||
logger.info(f"积分确认未就绪 {dingdanid}: {exc}")
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': str(exc),
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if order.zhuangtai != 3:
|
||||
logger.info(f"积分订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}")
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '积分订单尚未支付完成'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 5. 获取打手积分(通过反向查询)
|
||||
try:
|
||||
dashou = request.user.DashouProfile
|
||||
jifen = dashou.jifen # 直接使用整数
|
||||
|
||||
logger.info(f"积分轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}积分为{jifen}")
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '支付流程全部完成',
|
||||
'jifen': jifen # 返回当前积分(前端需要更新全局变量)
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except UserDashou.DoesNotExist:
|
||||
logger.error(f"打手信息不存在: 用户{request.user.UserUID}")
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '打手信息不存在'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"积分轮询异常: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '服务器内部错误'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
971
products/views/product_query.py
Normal file
971
products/views/product_query.py
Normal file
@@ -0,0 +1,971 @@
|
||||
"""商品/会员/分红 数据查询视图。
|
||||
|
||||
原 products/views.py 查询类视图聚合:商品数据获取、商品详情、打手会员列表、
|
||||
管事分红记录、管理员全量数据获取、会员类型/记录查询、组长分红记录。
|
||||
"""
|
||||
# ==================== 标准库 ====================
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import hashlib
|
||||
import requests
|
||||
import logging
|
||||
import decimal
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal, InvalidOperation, ROUND_CEILING
|
||||
import defusedxml.ElementTree as ET
|
||||
|
||||
# ==================== Django 内置 ====================
|
||||
from django.conf import settings
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Q
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from django.utils import timezone
|
||||
|
||||
# ==================== DRF 框架 ====================
|
||||
from rest_framework.views import APIView, View
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||
from rest_framework.throttling import AnonRateThrottle
|
||||
|
||||
# ==================== 项目工具函数 ====================
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from utils.money import format_yuan, quantize_yuan, yuan_to_fen
|
||||
from backend.utils import (
|
||||
update_guanshi_daily_by_action,
|
||||
update_guanshi_xufei_daily,
|
||||
update_zuzhang_daily_by_action
|
||||
)
|
||||
from users.fadan_fenhong_utils import process_fadan_fenhong
|
||||
|
||||
# ==================== 各App模型 ====================
|
||||
from ..models import (
|
||||
ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan,
|
||||
Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong
|
||||
)
|
||||
from users.models import (
|
||||
AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
from orders.models import CommissionRate
|
||||
from config.models import Gonggao, Lunbo
|
||||
|
||||
from orders.models import Penalty # 用于罚款类型
|
||||
from jituan.services.club_config import (
|
||||
get_commission_rate,
|
||||
get_commission_rate_object,
|
||||
get_huiyuan_price,
|
||||
get_huiyuan_fenchong,
|
||||
)
|
||||
from jituan.services.club_context import resolve_effective_club_id
|
||||
from jituan.services.club_write import resolve_club_id_for_write
|
||||
from jituan.services.club_user import get_payment_openid
|
||||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||
|
||||
import traceback
|
||||
|
||||
# ==================== 日志实例 ====================
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ShangpinHuoquView(APIView):
|
||||
"""
|
||||
商品数据获取接口
|
||||
一次性返回商品类型、商品专区、商品列表
|
||||
"""
|
||||
|
||||
# 应用节流(限制频率)
|
||||
throttle_classes = [AnonRateThrottle]
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
处理POST请求,返回所有商品相关数据
|
||||
"""
|
||||
try:
|
||||
logger.info("收到商品数据获取请求")
|
||||
|
||||
from jituan.services.club_context import resolve_effective_club_id
|
||||
from jituan.services.club_shangpin_leixing import build_club_leixing_list_for_api
|
||||
|
||||
club_id = resolve_effective_club_id(request, getattr(request, 'user', None))
|
||||
leixing_rows = build_club_leixing_list_for_api(club_id)
|
||||
shangpinleixing_data = [
|
||||
{
|
||||
'id': row['id'],
|
||||
'jieshao': row['jieshao'],
|
||||
'tupian_url': row['tupian_url'],
|
||||
'paixu': row['paixu'],
|
||||
}
|
||||
for row in leixing_rows
|
||||
]
|
||||
|
||||
# 2. 查询商品专区(只查询可见类型下的专区,并排序)
|
||||
# 先获取可见的类型ID列表
|
||||
visible_leixing_ids = [leixing['id'] for leixing in shangpinleixing_data]
|
||||
|
||||
if visible_leixing_ids:
|
||||
zhuanqu_queryset = ShangpinZhuanqu.query.filter(
|
||||
leixing_id__in=visible_leixing_ids,
|
||||
shenhezhuangtai=1 # 🔴【添加】只筛选审核状态为2的专区
|
||||
).order_by('-paixu', 'id') # 先按paixu降序,再按id升序
|
||||
else:
|
||||
zhuanqu_queryset = ShangpinZhuanqu.query.none()
|
||||
|
||||
shangpinzhuanqu_data = []
|
||||
for zhuanqu in zhuanqu_queryset:
|
||||
shangpinzhuanqu_data.append({
|
||||
"id": zhuanqu.id,
|
||||
"mingzi": zhuanqu.mingzi,
|
||||
"leixing_id": zhuanqu.leixing_id,
|
||||
"paixu": zhuanqu.paixu
|
||||
})
|
||||
|
||||
# 3. 查询商品列表(只查询可见专区下的商品,并排序)
|
||||
# 先获取可见的专区ID列表
|
||||
visible_zhuanqu_ids = [zhuanqu['id'] for zhuanqu in shangpinzhuanqu_data]
|
||||
|
||||
if visible_zhuanqu_ids:
|
||||
# 注意:这里使用values()只获取指定字段,提高查询效率
|
||||
shangpin_queryset = Shangpin.query.filter(
|
||||
zhuanqu_id__in=visible_zhuanqu_ids,
|
||||
shenhezhuangtai=1 # 🔴【添加】只筛选审核状态为2的商品
|
||||
).values(
|
||||
'id', 'biaoqian', 'jiage', 'leixing_id',
|
||||
'zhuanqu_id', 'tupian_url', 'duiwai_xiaoliang', 'paixu'
|
||||
).order_by('-paixu', 'id') # 先按paixu降序,再按id升序
|
||||
|
||||
# 将QuerySet转换为列表
|
||||
shangpinliebiao_data = list(shangpin_queryset)
|
||||
else:
|
||||
shangpinliebiao_data = []
|
||||
|
||||
# 4. 构建返回数据
|
||||
response_data = {
|
||||
"shangpinleixing": shangpinleixing_data,
|
||||
"shangpinzhuanqu": shangpinzhuanqu_data,
|
||||
"shangpinliebiao": shangpinliebiao_data
|
||||
}
|
||||
|
||||
logger.info(f"返回数据:类型{len(shangpinleixing_data)}个,"
|
||||
f"专区{len(shangpinzhuanqu_data)}个,"
|
||||
f"商品{len(shangpinliebiao_data)}个")
|
||||
|
||||
# 5. 返回响应
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
# 记录异常
|
||||
logger.error(f"获取商品数据时发生错误:{str(e)}", exc_info=True)
|
||||
|
||||
# 返回错误响应
|
||||
error_data = {
|
||||
"error": "服务器内部错误",
|
||||
"detail": "获取商品数据失败,请稍后重试"
|
||||
}
|
||||
return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ShangpinXiangqingHuoquView(APIView):
|
||||
"""
|
||||
商品详情接口 - 极简版
|
||||
假设所有字段都存在,直接返回
|
||||
"""
|
||||
|
||||
throttle_classes = [AnonRateThrottle]
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
shangpin_id = request.data.get('shangpin_id')
|
||||
|
||||
if not shangpin_id:
|
||||
return Response({
|
||||
"code": 1,
|
||||
"msg": "商品ID不能为空",
|
||||
"data": None
|
||||
}, status=400)
|
||||
|
||||
# 直接查询,数据量小,性能没问题
|
||||
shangpin = Shangpin.query.filter(id=shangpin_id).first()
|
||||
|
||||
if not shangpin:
|
||||
return Response({
|
||||
"code": 2,
|
||||
"msg": "商品不存在",
|
||||
"data": None
|
||||
}, status=404)
|
||||
|
||||
# 直接构建数据,不处理空值
|
||||
data = {
|
||||
"tupian": shangpin.tupian_url,
|
||||
"biaoti": shangpin.biaoqian,
|
||||
"jiage": shangpin.jiage,
|
||||
"xiangqing": shangpin.jieshao,
|
||||
"xiadanxuzhi": shangpin.xiadan_xuzhi,
|
||||
"guize": shangpin.guize_tupian,
|
||||
"xiaoliang": shangpin.duiwai_xiaoliang,
|
||||
"kucun": shangpin.kucun,
|
||||
}
|
||||
|
||||
return Response({
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"接口异常: {e}")
|
||||
return Response({
|
||||
"code": 500,
|
||||
"msg": "服务器错误",
|
||||
"data": None
|
||||
}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class DashouHuiyuanList(APIView):
|
||||
"""
|
||||
获取打手会员商品列表
|
||||
路径:/shangpin/dshyhq
|
||||
请求方式:POST
|
||||
权限:JWT认证
|
||||
返回:会员商品列表,字段对应前端需求
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 1. 检查用户是否是打手(通过反向查询)
|
||||
try:
|
||||
dashou = request.user.DashouProfile
|
||||
except UserDashou.DoesNotExist:
|
||||
logger.warning(f"用户{request.user.UserUID}不是打手,尝试访问打手接口")
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '用户不是打手,无法访问此接口'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2. 检查打手账号状态
|
||||
if dashou.zhanghaozhuangtai != 1:
|
||||
logger.warning(f"打手{request.user.UserUID}账号状态异常: {dashou.zhanghaozhuangtai}")
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '账号状态异常,无法购买会员'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 仅返回本俱乐部可售会员(club_huiyuan_price 已配置且启用)
|
||||
from jituan.services.member_recharge import club_huiyuan_sellable, can_purchase_trial
|
||||
|
||||
club_id = resolve_effective_club_id(request, request.user)
|
||||
huiyuan_queryset = Huiyuan.query.all().only(
|
||||
'huiyuan_id', 'jieshao', 'jtjieshao', 'jiage',
|
||||
).order_by('jiage')
|
||||
|
||||
huiyuan_list = []
|
||||
yonghuid = request.user.UserUID
|
||||
for huiyuan in huiyuan_queryset:
|
||||
sellable, club_price, formal_days = club_huiyuan_sellable(
|
||||
club_id, huiyuan.huiyuan_id, is_trial=False,
|
||||
)
|
||||
if not sellable or not club_price:
|
||||
continue
|
||||
trial_ok, _ = can_purchase_trial(yonghuid, huiyuan.huiyuan_id, club_id)
|
||||
trial_sellable, trial_price, trial_days = club_huiyuan_sellable(
|
||||
club_id, huiyuan.huiyuan_id, is_trial=True,
|
||||
)
|
||||
huiyuan_list.append({
|
||||
'id': huiyuan.huiyuan_id,
|
||||
'mingzi': huiyuan.jieshao,
|
||||
'jiage': format_yuan(club_price),
|
||||
'formal_days': formal_days,
|
||||
'jieshao': huiyuan.jtjieshao,
|
||||
'trial_enabled': bool(trial_sellable),
|
||||
'trial_price': format_yuan(trial_price) if trial_sellable else '0.00',
|
||||
'trial_days': trial_days if trial_sellable else 0,
|
||||
'can_buy_trial': bool(trial_ok and trial_sellable),
|
||||
})
|
||||
|
||||
# 5. 返回数据
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '获取会员列表成功',
|
||||
'data': huiyuan_list,
|
||||
'club_id': club_id,
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取会员列表接口异常: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '服务器内部错误,请稍后重试'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class GuanshiFenhongListAPI(APIView):
|
||||
"""
|
||||
管事分红记录查询接口
|
||||
权限:需要JWT认证
|
||||
请求方式:POST
|
||||
分页参数:page, page_size
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 获取当前管事用户ID
|
||||
yonghuid = request.user.UserUID
|
||||
|
||||
# 获取分页参数
|
||||
page = int(request.data.get('page', 1))
|
||||
page_size = int(request.data.get('page_size', 50))
|
||||
|
||||
if page < 1:
|
||||
page = 1
|
||||
if page_size < 1 or page_size > 100: # 限制最大100条
|
||||
page_size = 50
|
||||
|
||||
# 计算偏移量
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
# 查询条件:管事ID匹配
|
||||
query = Q(guanshi=yonghuid)
|
||||
|
||||
# 先查询总数(使用分页查询优化)
|
||||
|
||||
# 查询总记录数
|
||||
zong_tiaoshu = Gsfenhong.query.filter(query).count()
|
||||
|
||||
# 查询去重后的打手数量
|
||||
chongzhi_dashou_shuliang = Gsfenhong.query.filter(query).values('dashouid').distinct().count()
|
||||
|
||||
# 查询当前页数据,按创建时间倒序排列
|
||||
fenhong_list = Gsfenhong.query.filter(query) \
|
||||
.order_by('-CreateTime') \
|
||||
.values('dashouid', 'avatar', 'nicheng', 'fenhong', 'CreateTime')[offset:offset + page_size]
|
||||
|
||||
# 格式化数据
|
||||
formatted_list = []
|
||||
for item in fenhong_list:
|
||||
formatted_list.append({
|
||||
'yonghuid': item['dashouid'], # 打手ID
|
||||
'avatar': item['avatar'] or '', # 打手头像
|
||||
'nicheng': item['nicheng'] or '未知用户', # 打手昵称
|
||||
'fenhong': str(item['fenhong']), # 分红金额
|
||||
'CreateTime': item['CreateTime'].strftime('%Y-%m-%d %H:%M:%S') if item['CreateTime'] else ''
|
||||
})
|
||||
|
||||
# 返回数据
|
||||
return Response({
|
||||
'code': 200,
|
||||
'msg': '成功',
|
||||
'data': {
|
||||
'list': formatted_list,
|
||||
'total': zong_tiaoshu, # 总记录数
|
||||
'chongzhi_dashou_shuliang': chongzhi_dashou_shuliang, # 充值打手数量(去重)
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
'has_more': len(formatted_list) == page_size # 是否还有更多数据
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查询管事分红记录失败: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'msg': f'服务器错误: {str(e)}',
|
||||
'data': {}
|
||||
})
|
||||
|
||||
|
||||
# views.py - 商品模块视图
|
||||
class AdGetAllDataView(APIView):
|
||||
"""
|
||||
获取商品类型、商品专区、会员数据的接口
|
||||
接口地址:/shangpin/adhqlx
|
||||
请求方法:POST
|
||||
权限要求:JWT Token认证 + 管理员权限
|
||||
"""
|
||||
|
||||
# 需要JWT Token认证
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
处理POST请求,返回商品相关数据
|
||||
"""
|
||||
try:
|
||||
# 1. 获取请求数据
|
||||
zhanghao = request.data.get('zhanghao')
|
||||
|
||||
if not zhanghao:
|
||||
logger.warning(f"管理员接口:未接收到账号参数,请求用户:{request.user.UserUID}")
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '缺少账号参数',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2. 获取当前登录用户
|
||||
current_user = request.user
|
||||
|
||||
# 3. 验证用户类型是否为管理员
|
||||
if current_user.UserType != 'admin':
|
||||
logger.warning(
|
||||
f"权限验证失败:用户 {current_user.UserUID} 的用户类型为 {current_user.UserType},非管理员")
|
||||
return Response({
|
||||
'code': 401,
|
||||
'message': '无管理员权限',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 4. 验证手机号与传递的账号是否一致
|
||||
if current_user.Phone != zhanghao:
|
||||
logger.warning(
|
||||
f"账号验证失败:用户 {current_user.UserUID} 的phone={current_user.Phone},传递的zhanghao={zhanghao}")
|
||||
return Response({
|
||||
'code': 401,
|
||||
'message': '账号验证失败',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 5. 验证管理员扩展表是否存在
|
||||
try:
|
||||
admin_profile = current_user.AdminProfile
|
||||
logger.info(f"管理员验证通过:用户 {current_user.UserUID},手机号 {current_user.Phone}")
|
||||
except AdminProfile.DoesNotExist:
|
||||
logger.error(
|
||||
f"管理员扩展表不存在:用户 {current_user.UserUID} 虽然user_type=admin,但没有AdminProfile记录")
|
||||
return Response({
|
||||
'code': 401,
|
||||
'message': '管理员信息不完整',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 6. 查询商品类型数据
|
||||
try:
|
||||
# 使用select_related或prefetch_related优化查询(如果需要关联查询)
|
||||
# 这里直接查询所有字段,然后提取需要的字段
|
||||
shangpinleixing_queryset = ShangpinLeixing.query.all()
|
||||
|
||||
# 转换为前端需要的格式
|
||||
shangpinleixing_list = []
|
||||
for leixing in shangpinleixing_queryset:
|
||||
leixing_data = {
|
||||
'id': leixing.id,
|
||||
'yaoqiuleixing': leixing.yaoqiuleixing if leixing.yaoqiuleixing is not None else None,
|
||||
'huiyuan_id': leixing.huiyuan_id if leixing.huiyuan_id else None,
|
||||
'yongjin': str(leixing.yongjin) if leixing.yongjin is not None else None,
|
||||
'jieshao': leixing.jieshao if leixing.jieshao else '',
|
||||
'tupian_url': leixing.tupian_url if leixing.tupian_url else ''
|
||||
}
|
||||
shangpinleixing_list.append(leixing_data)
|
||||
|
||||
#logger.info(f"成功查询到 {len(shangpinleixing_list)} 条商品类型数据")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查询商品类型数据时出错:{str(e)}")
|
||||
shangpinleixing_list = []
|
||||
|
||||
# 7. 查询商品专区数据
|
||||
try:
|
||||
# 查询所有商品专区
|
||||
zhuanqu_queryset = ShangpinZhuanqu.query.all()
|
||||
|
||||
# 转换为前端需要的格式
|
||||
zhuanqu_list = []
|
||||
for zhuanqu in zhuanqu_queryset:
|
||||
zhuanqu_data = {
|
||||
'id': zhuanqu.id,
|
||||
'mingzi': zhuanqu.mingzi if zhuanqu.mingzi else '',
|
||||
'leixing_id': zhuanqu.leixing_id
|
||||
}
|
||||
zhuanqu_list.append(zhuanqu_data)
|
||||
|
||||
# logger.info(f"成功查询到 {len(zhuanqu_list)} 条商品专区数据")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查询商品专区数据时出错:{str(e)}")
|
||||
zhuanqu_list = []
|
||||
|
||||
# 8. 查询会员数据
|
||||
try:
|
||||
# 查询所有会员
|
||||
huiyuan_queryset = Huiyuan.query.all()
|
||||
|
||||
# 转换为前端需要的格式
|
||||
huiyuan_list = []
|
||||
for huiyuan in huiyuan_queryset:
|
||||
huiyuan_data = {
|
||||
'huiyuan_id': huiyuan.huiyuan_id,
|
||||
'jieshao': huiyuan.jieshao,
|
||||
'jiage': str(huiyuan.jiage) # Decimal转为字符串
|
||||
}
|
||||
huiyuan_list.append(huiyuan_data)
|
||||
|
||||
# logger.info(f"成功查询到 {len(huiyuan_list)} 条会员数据")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查询会员数据时出错:{str(e)}")
|
||||
huiyuan_list = []
|
||||
|
||||
# 9. 检查会员列表是否为空
|
||||
if not huiyuan_list:
|
||||
logger.warning("会员列表为空,可能会影响商品发布功能")
|
||||
|
||||
# 10. 组织返回数据
|
||||
response_data = {
|
||||
'code': 0,
|
||||
'message': '获取数据成功',
|
||||
'data': {
|
||||
'shangpinleixingList': shangpinleixing_list,
|
||||
'zhuanquList': zhuanqu_list,
|
||||
'huiyuanList': huiyuan_list
|
||||
}
|
||||
}
|
||||
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
# 捕获未预期的异常
|
||||
logger.error(f"接口处理异常:{str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'服务器内部错误:{str(e)}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
# views.py - shangpin模块
|
||||
|
||||
|
||||
class HuiyuanTypeListView(APIView):
|
||||
"""
|
||||
获取会员类型列表接口
|
||||
路径:/shangpin/adhyjllxcx
|
||||
方法:POST
|
||||
权限:JWT Token + 管理员验证
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 🔥 1. 身份验证
|
||||
user = request.user
|
||||
|
||||
# 检查是否是管理员
|
||||
if not hasattr(user, 'AdminProfile'):
|
||||
return Response({
|
||||
'code': 401,
|
||||
'message': '无权限访问',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 🔥 2. 获取所有会员类型
|
||||
huiyuan_list = Huiyuan.query.all().order_by('CreateTime')
|
||||
|
||||
# 🔥 3. 构建返回数据
|
||||
huiyuan_data = []
|
||||
for huiyuan in huiyuan_list:
|
||||
huiyuan_data.append({
|
||||
'id': huiyuan.huiyuan_id, # 会员ID
|
||||
'jieshao': huiyuan.jieshao, # 会员介绍
|
||||
'jiage': str(huiyuan.jiage), # 价格
|
||||
'goumai_cishu': huiyuan.goumai_cishu, # 购买次数
|
||||
'CreateTime': huiyuan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.CreateTime else None
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'message': '获取成功',
|
||||
'data': huiyuan_data
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取会员类型列表失败: {str(e)}")
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '系统错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
|
||||
class HuiyuanRecordListView(APIView):
|
||||
"""
|
||||
获取会员记录列表接口
|
||||
路径:/shangpin/adhyjlcxhq
|
||||
方法:POST
|
||||
权限:JWT Token + 管理员验证
|
||||
参数:
|
||||
- zhanghao: 管理员账号
|
||||
- huiyuan_type: 会员类型ID
|
||||
- jiluzhuangtai: 记录状态(1=未过期, 2=已过期)
|
||||
- page: 页码
|
||||
- pagesize: 每页条数
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 🔥 1. 身份验证
|
||||
user = request.user
|
||||
|
||||
# 检查是否是管理员
|
||||
if not hasattr(user, 'AdminProfile'):
|
||||
return Response({
|
||||
'code': 401,
|
||||
'message': '无权限访问',
|
||||
'data': None
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 🔥 2. 获取参数
|
||||
zhanghao = request.data.get('zhanghao', '').strip()
|
||||
huiyuan_type = request.data.get('huiyuan_type', '').strip()
|
||||
jiluzhuangtai = request.data.get('jiluzhuangtai', 1) # 默认未过期
|
||||
page = int(request.data.get('page', 1))
|
||||
pagesize = int(request.data.get('pagesize', 5))
|
||||
|
||||
# 🔥 3. 参数验证
|
||||
if not zhanghao:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '管理员账号不能为空',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not huiyuan_type:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '会员类型不能为空',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 🔥 4. 检查会员类型是否存在
|
||||
huiyuan_obj = Huiyuan.query.filter(huiyuan_id=huiyuan_type).first()
|
||||
if not huiyuan_obj:
|
||||
return Response({
|
||||
'code': 404,
|
||||
'message': '会员类型不存在',
|
||||
'data': None
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# 🔥 5. 构建基础查询(一次性获取所有符合条件的记录ID)
|
||||
now = timezone.now()
|
||||
|
||||
# 🔥 使用原生SQL优化查询(避免N+1问题)
|
||||
with connection.cursor() as cursor:
|
||||
# 先查询符合条件的总记录数
|
||||
count_sql = """
|
||||
SELECT COUNT(*)
|
||||
FROM huiyuangoumai hg
|
||||
WHERE hg.huiyuan_id = %s
|
||||
"""
|
||||
params = [huiyuan_type]
|
||||
|
||||
if jiluzhuangtai == 1: # 未过期
|
||||
count_sql += " AND hg.daoqi_time > %s"
|
||||
params.append(now)
|
||||
else: # 已过期
|
||||
count_sql += " AND hg.daoqi_time <= %s"
|
||||
params.append(now)
|
||||
|
||||
cursor.execute(count_sql, params)
|
||||
total_count = cursor.fetchone()[0]
|
||||
|
||||
# 🔥 6. 分页查询(使用窗口函数提高性能)
|
||||
offset = (page - 1) * pagesize
|
||||
|
||||
record_sql = """
|
||||
SELECT
|
||||
hg.id,
|
||||
hg.yonghu_id,
|
||||
hg.huiyuan_id,
|
||||
hg.jieshao,
|
||||
hg.daoqi_time,
|
||||
hg.CreateTime,
|
||||
um.avatar,
|
||||
ud.nicheng
|
||||
FROM huiyuangoumai hg
|
||||
LEFT JOIN user_main um ON hg.yonghu_id = um.yonghuid
|
||||
LEFT JOIN user_dashou ud ON um.id = ud.user_id
|
||||
WHERE hg.huiyuan_id = %s
|
||||
"""
|
||||
|
||||
record_params = [huiyuan_type]
|
||||
|
||||
if jiluzhuangtai == 1: # 未过期
|
||||
record_sql += " AND hg.daoqi_time > %s"
|
||||
record_params.append(now)
|
||||
else: # 已过期
|
||||
record_sql += " AND hg.daoqi_time <= %s"
|
||||
record_params.append(now)
|
||||
|
||||
record_sql += " ORDER BY hg.CreateTime DESC LIMIT %s OFFSET %s"
|
||||
record_params.extend([pagesize, offset])
|
||||
|
||||
cursor.execute(record_sql, record_params)
|
||||
records = cursor.fetchall()
|
||||
|
||||
# 🔥 7. 处理查询结果
|
||||
record_list = []
|
||||
for record in records:
|
||||
record_list.append({
|
||||
'id': record[0],
|
||||
'yonghuid': record[1], # 用户ID
|
||||
'huiyuan_id': record[2],
|
||||
'jieshao': record[3],
|
||||
'daoqi_time': record[4].strftime('%Y-%m-%d %H:%M:%S') if record[4] else None,
|
||||
'CreateTime': record[5].strftime('%Y-%m-%d %H:%M:%S') if record[5] else None,
|
||||
'touxiang': record[6], # 头像相对URL
|
||||
'nicheng': record[7] or record[1], # 昵称或用户ID
|
||||
'yonghu_type': 2 # 🔥 默认打手类型(实际应该根据user_type判断)
|
||||
})
|
||||
|
||||
# 🔥 8. 判断是否还有更多数据
|
||||
has_more = (page * pagesize) < total_count
|
||||
|
||||
# 🔥 9. 获取会员类型统计信息(所有会员类型的当前状态总数)
|
||||
huiyuan_types = Huiyuan.query.all()
|
||||
huiyuan_stats = []
|
||||
|
||||
for huiyuan in huiyuan_types:
|
||||
# 使用COUNT查询统计
|
||||
stats_sql = """
|
||||
SELECT COUNT(*)
|
||||
FROM huiyuangoumai
|
||||
WHERE huiyuan_id = %s
|
||||
"""
|
||||
stats_params = [huiyuan.huiyuan_id]
|
||||
|
||||
if jiluzhuangtai == 1: # 未过期
|
||||
stats_sql += " AND daoqi_time > %s"
|
||||
stats_params.append(now)
|
||||
else: # 已过期
|
||||
stats_sql += " AND daoqi_time <= %s"
|
||||
stats_params.append(now)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(stats_sql, stats_params)
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
huiyuan_stats.append({
|
||||
'id': huiyuan.huiyuan_id,
|
||||
'jieshao': huiyuan.jieshao,
|
||||
'count': count
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'message': '获取成功',
|
||||
'data': {
|
||||
'list': record_list,
|
||||
'total': total_count,
|
||||
'current_page': page,
|
||||
'page_size': pagesize,
|
||||
'has_more': has_more,
|
||||
'huiyuan_stats': huiyuan_stats, # 🔥 所有会员类型的统计
|
||||
'current_huiyuan': {
|
||||
'id': huiyuan_obj.huiyuan_id,
|
||||
'jieshao': huiyuan_obj.jieshao,
|
||||
'count': total_count
|
||||
}
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except ValueError as e:
|
||||
print(f"参数错误: {str(e)}")
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '参数错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取会员记录失败: {str(e)}")
|
||||
traceback.print_exc()
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '系统错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ZuzhangFenhongView(APIView):
|
||||
"""
|
||||
组长分红记录接口
|
||||
路径:/shangpin/zuzhangfenhong
|
||||
方法:POST
|
||||
权限:JWT认证,仅限组长身份
|
||||
请求参数:
|
||||
{
|
||||
"page": 1, # 页码,默认1
|
||||
"page_size": 5 # 每页条数,默认5
|
||||
}
|
||||
返回:
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"fenhong_zonge": "1234.56", # 分红总额
|
||||
"yaoqing_guanshi": 10, # 邀请管事数
|
||||
"fenhong_cishu": 50, # 分红次数
|
||||
"total": 50, # 总记录数
|
||||
"has_more": false, # 是否有下一页
|
||||
"list": [...] # 记录列表
|
||||
}
|
||||
}
|
||||
"""
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
current_user = request.user
|
||||
|
||||
# 1. 验证组长身份
|
||||
try:
|
||||
zuzhang = UserZuzhang.query.get(user=current_user)
|
||||
except UserZuzhang.DoesNotExist:
|
||||
return Response({
|
||||
'code': 403,
|
||||
'msg': '当前用户不是组长身份',
|
||||
'data': None
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# 2. 请求参数
|
||||
page = int(request.data.get('page', 1))
|
||||
page_size = int(request.data.get('page_size', 5))
|
||||
page = max(page, 1)
|
||||
page_size = max(page_size, 1)
|
||||
|
||||
# 3. 统计数据
|
||||
# 3.1 分红总额(从组长扩展表)
|
||||
fenhong_zonge = zuzhang.fenyong_zonge
|
||||
|
||||
# 3.2 邀请管事数(统计邀请人等于当前用户ID的管事记录)
|
||||
yaoqing_guanshi = UserGuanshi.query.filter(yaoqingren=current_user.UserUID).count()
|
||||
|
||||
# 3.3 分红次数(总记录数)
|
||||
base_queryset = Gsfenhong.query.filter(zuzhang_id=current_user.UserUID).order_by('-CreateTime')
|
||||
fenhong_cishu = base_queryset.count()
|
||||
|
||||
# 4. 分页查询
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
records = base_queryset[start:end]
|
||||
|
||||
# 5. 批量获取管事用户信息(只查 boss_profile 获取昵称)
|
||||
guanshi_ids = list(set(rec.guanshi for rec in records if rec.guanshi))
|
||||
guanshi_info_map = self._get_guanshi_info(guanshi_ids)
|
||||
|
||||
# 6. 组装列表数据
|
||||
list_data = []
|
||||
for rec in records:
|
||||
# 打手信息(直接从记录中取快照)
|
||||
dashou_avatar = rec.avatar or ''
|
||||
dashou_nicheng = rec.nicheng or ''
|
||||
dashou_yonghuid = rec.dashouid or ''
|
||||
|
||||
# 管事信息
|
||||
guanshi_id = rec.guanshi
|
||||
guanshi_info = guanshi_info_map.get(guanshi_id, {'avatar': '', 'nicheng': ''})
|
||||
guanshi_avatar = guanshi_info['avatar']
|
||||
guanshi_nicheng = guanshi_info['nicheng']
|
||||
|
||||
# 组长分红金额
|
||||
fenhong = rec.zuzhang_fenhong
|
||||
if fenhong is None:
|
||||
fenhong = decimal.Decimal('0.00')
|
||||
fenhong_str = str(fenhong.quantize(decimal.Decimal('0.00')))
|
||||
|
||||
# 时间格式化
|
||||
CreateTime = rec.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if rec.CreateTime else ''
|
||||
|
||||
list_data.append({
|
||||
'dashou_avatar': dashou_avatar,
|
||||
'dashou_nicheng': dashou_nicheng,
|
||||
'dashou_yonghuid': dashou_yonghuid,
|
||||
'guanshi_avatar': guanshi_avatar,
|
||||
'guanshi_nicheng': guanshi_nicheng,
|
||||
'guanshi_yonghuid': guanshi_id,
|
||||
'fenhong': fenhong_str,
|
||||
'CreateTime': CreateTime,
|
||||
})
|
||||
|
||||
# 7. 是否有更多数据
|
||||
has_more = (end < fenhong_cishu)
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
'data': {
|
||||
'fenhong_zonge': str(fenhong_zonge.quantize(decimal.Decimal('0.00'))),
|
||||
'yaoqing_guanshi': yaoqing_guanshi,
|
||||
'fenhong_cishu': fenhong_cishu,
|
||||
'total': fenhong_cishu,
|
||||
'has_more': has_more,
|
||||
'list': list_data,
|
||||
}
|
||||
})
|
||||
|
||||
def _get_guanshi_info(self, guanshi_ids):
|
||||
"""
|
||||
批量获取管事用户的头像和昵称
|
||||
昵称从老板扩展表(boss_profile)获取,头像从主表获取
|
||||
返回字典:{用户ID: {'avatar': 相对路径, 'nicheng': 昵称}}
|
||||
"""
|
||||
if not guanshi_ids:
|
||||
return {}
|
||||
|
||||
# 查询用户主表,并预加载 boss_profile
|
||||
users = User.query.filter(UserUID__in=guanshi_ids).select_related(
|
||||
'BossProfile'
|
||||
).only(
|
||||
'UserUID', 'Avatar',
|
||||
'BossProfile__nickname'
|
||||
)
|
||||
|
||||
info = {}
|
||||
for user in users:
|
||||
avatar = user.Avatar or ''
|
||||
# 从 boss_profile 获取昵称
|
||||
if hasattr(user, 'BossProfile') and user.BossProfile:
|
||||
nicheng = user.BossProfile.nickname or ''
|
||||
else:
|
||||
# 如果没有 boss_profile,使用默认昵称
|
||||
nicheng = f"用户{user.UserUID[-4:]}"
|
||||
info[user.UserUID] = {'avatar': avatar, 'nicheng': nicheng}
|
||||
|
||||
# 对于未查询到的用户,提供默认值
|
||||
for gid in guanshi_ids:
|
||||
if gid not in info:
|
||||
info[gid] = {'avatar': '', 'nicheng': f"用户{gid[-4:]}"}
|
||||
|
||||
return info
|
||||
|
||||
|
||||
|
||||
# 常量定义(可根据需要调整)
|
||||
|
||||
|
||||
2544
products/views/shangpin_admin.py
Normal file
2544
products/views/shangpin_admin.py
Normal file
File diff suppressed because it is too large
Load Diff
613
products/views/yajin_payment.py
Normal file
613
products/views/yajin_payment.py
Normal file
@@ -0,0 +1,613 @@
|
||||
"""押金支付流程视图。
|
||||
|
||||
包含押金购买的参数生成、支付回调、失败处理、轮询。
|
||||
"""
|
||||
# ==================== 标准库 ====================
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import hashlib
|
||||
import requests
|
||||
import logging
|
||||
import decimal
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal, InvalidOperation, ROUND_CEILING
|
||||
import defusedxml.ElementTree as ET
|
||||
|
||||
# ==================== Django 内置 ====================
|
||||
from django.conf import settings
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Q
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from django.utils import timezone
|
||||
|
||||
# ==================== DRF 框架 ====================
|
||||
from rest_framework.views import APIView, View
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||
from rest_framework.throttling import AnonRateThrottle
|
||||
|
||||
# ==================== 项目工具函数 ====================
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from utils.money import format_yuan, quantize_yuan, yuan_to_fen
|
||||
from backend.utils import (
|
||||
update_guanshi_daily_by_action,
|
||||
update_guanshi_xufei_daily,
|
||||
update_zuzhang_daily_by_action
|
||||
)
|
||||
from users.fadan_fenhong_utils import process_fadan_fenhong
|
||||
|
||||
# ==================== 各App模型 ====================
|
||||
from ..models import (
|
||||
ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan,
|
||||
Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong
|
||||
)
|
||||
from users.models import (
|
||||
AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia
|
||||
)
|
||||
from users.business_models import User
|
||||
|
||||
from orders.models import CommissionRate
|
||||
from config.models import Gonggao, Lunbo
|
||||
|
||||
from orders.models import Penalty # 用于罚款类型
|
||||
from jituan.services.club_config import (
|
||||
get_commission_rate,
|
||||
get_commission_rate_object,
|
||||
get_huiyuan_price,
|
||||
get_huiyuan_fenchong,
|
||||
)
|
||||
from jituan.services.club_context import resolve_effective_club_id
|
||||
from jituan.services.club_write import resolve_club_id_for_write
|
||||
from jituan.services.club_user import get_payment_openid
|
||||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||
|
||||
import traceback
|
||||
|
||||
# ==================== 日志实例 ====================
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class YajinGoumai(APIView):
|
||||
"""
|
||||
押金充值支付参数接口
|
||||
路径:/shangpin/yajingoumai
|
||||
请求方式:POST
|
||||
权限:JWT认证
|
||||
参数:jine(金额,1-10000元)
|
||||
返回:支付参数(payParams)和订单ID(dingdanid)
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 1. 验证用户身份和状态
|
||||
try:
|
||||
dashou = request.user.DashouProfile
|
||||
except UserDashou.DoesNotExist:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '用户不是打手,无法充值押金'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if dashou.zhanghaozhuangtai != 1:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '账号状态异常,无法进行充值'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2. 获取并验证金额参数
|
||||
jine = request.data.get('jine')
|
||||
if not jine:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '请填写充值金额'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
jine = int(jine)
|
||||
except (ValueError, TypeError):
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '金额格式错误,请填写1-10000的整数'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 验证金额范围
|
||||
if jine < 1 or jine > 10000:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '充值金额必须在1-10000元之间'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 生成订单ID(使用你提供的算法,前缀改为CzYJ)
|
||||
timestamp_ms = int(time.time() * 1000)
|
||||
timestamp_ns = int(time.time_ns() // 1000) % 1000000
|
||||
random_num = random.randint(0, 99)
|
||||
|
||||
dingdanid = f"CzYJ{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
|
||||
|
||||
# 4. 创建充值记录订单
|
||||
chongzhi_jilu = Czjilu.query.create(
|
||||
dingdan_id=dingdanid,
|
||||
zhuangtai=9, # 未支付状态
|
||||
yonghuid=request.user.UserUID,
|
||||
jine=jine,
|
||||
leixing=2, # 充值类型:2=押金
|
||||
shuoming=settings.YAJIN_PAY_DESCRIPTION,
|
||||
club_id=resolve_club_id_for_write(request, request.user),
|
||||
)
|
||||
|
||||
logger.info(f"创建押金充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, 金额{jine}元")
|
||||
|
||||
# 5. 生成微信支付参数
|
||||
try:
|
||||
pay_openid, _ = get_payment_openid(request)
|
||||
if not pay_openid:
|
||||
chongzhi_jilu.delete()
|
||||
return Response({'code': 400, 'message': '用户openid不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
pay_params = self.generate_wechat_pay_params(
|
||||
dingdanid=dingdanid,
|
||||
jine=jine,
|
||||
openid=pay_openid,
|
||||
pay_type='yajin'
|
||||
)
|
||||
except Exception as e:
|
||||
# 支付参数生成失败,删除订单记录
|
||||
chongzhi_jilu.delete()
|
||||
logger.error(f"生成微信支付参数失败: {str(e)}")
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'支付参数生成失败: {str(e)}'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
# 6. 返回支付参数和订单ID
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '支付参数生成成功',
|
||||
'payParams': pay_params, # 支付参数(前端接收为payParams)
|
||||
'dingdanid': dingdanid # 订单ID(前端接收为dingdanid)
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"押金充值接口异常: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '服务器内部错误,请稍后重试'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
|
||||
"""生成微信小程序支付参数"""
|
||||
# 获取配置
|
||||
from jituan.services.club_write import resolve_club_id_for_write
|
||||
from jituan.services.wechat_pay import get_wechat_v2_config
|
||||
pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user))
|
||||
APPID = pay_cfg['appid']
|
||||
MCHID = pay_cfg['mch_id']
|
||||
KEY = pay_cfg['key']
|
||||
|
||||
# 根据支付类型获取对应的配置
|
||||
if pay_type == 'yajin':
|
||||
PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION
|
||||
NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL
|
||||
elif pay_type == 'jifen':
|
||||
PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION
|
||||
NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL
|
||||
elif pay_type == 'huiyuan':
|
||||
PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION
|
||||
NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL
|
||||
else:
|
||||
raise Exception('未知的支付类型')
|
||||
|
||||
# 生成随机字符串
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
|
||||
# 金额转换(元转分)
|
||||
total_fee = yuan_to_fen(jine)
|
||||
|
||||
# 交易类型(小程序支付使用JSAPI)
|
||||
trade_type = 'JSAPI'
|
||||
|
||||
# 获取客户端IP
|
||||
user_ip = self.get_client_ip()
|
||||
|
||||
# 1. 第一次签名(统一下单接口)
|
||||
sign_data = {
|
||||
'appid': APPID,
|
||||
'mch_id': MCHID,
|
||||
'nonce_str': nonce_str,
|
||||
'body': PAY_DESCRIPTION,
|
||||
'out_trade_no': dingdanid,
|
||||
'total_fee': total_fee,
|
||||
'spbill_create_ip': user_ip,
|
||||
'notify_url': NOTIFY_URL,
|
||||
'trade_type': trade_type,
|
||||
'openid': openid # 小程序支付必须传openid
|
||||
}
|
||||
|
||||
# 按照参数名ASCII码从小到大排序
|
||||
sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
|
||||
|
||||
# 拼接字符串
|
||||
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
|
||||
sign_string += f'&key={KEY}'
|
||||
|
||||
# MD5签名
|
||||
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
|
||||
|
||||
# 构造XML数据
|
||||
xml_data = f"""<xml>
|
||||
<appid>{APPID}</appid>
|
||||
<body>{PAY_DESCRIPTION}</body>
|
||||
<mch_id>{MCHID}</mch_id>
|
||||
<nonce_str>{nonce_str}</nonce_str>
|
||||
<notify_url>{NOTIFY_URL}</notify_url>
|
||||
<openid>{openid}</openid>
|
||||
<out_trade_no>{dingdanid}</out_trade_no>
|
||||
<spbill_create_ip>{user_ip}</spbill_create_ip>
|
||||
<total_fee>{total_fee}</total_fee>
|
||||
<trade_type>{trade_type}</trade_type>
|
||||
<sign>{sign}</sign>
|
||||
</xml>"""
|
||||
|
||||
# 调用微信统一下单接口(官方地址)
|
||||
response = requests.post(
|
||||
'https://api.mch.weixin.qq.com/pay/unifiedorder',
|
||||
data=xml_data.encode('utf-8'),
|
||||
headers={'Content-Type': 'application/xml'},
|
||||
timeout=10 # 设置超时
|
||||
)
|
||||
|
||||
# 解析返回的XML
|
||||
root = ET.fromstring(response.content)
|
||||
|
||||
return_code = root.find('return_code').text
|
||||
result_code = root.find('result_code').text
|
||||
|
||||
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
|
||||
# 获取prepay_id
|
||||
prepay_id = root.find('prepay_id').text
|
||||
|
||||
# 2. 第二次签名(小程序支付参数)
|
||||
timestamp = str(int(time.time()))
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
package = f'prepay_id={prepay_id}'
|
||||
sign_type = 'MD5'
|
||||
|
||||
# 生成支付签名(注意:这里key的大小写和字段名与第一次不同)
|
||||
pay_sign_data = {
|
||||
'appId': APPID, # 注意:这里是appId,不是appid
|
||||
'timeStamp': timestamp, # 注意:这里是timeStamp,不是timestamp
|
||||
'nonceStr': nonce_str,
|
||||
'package': package,
|
||||
'signType': sign_type
|
||||
}
|
||||
|
||||
sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
|
||||
pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
|
||||
pay_sign_string += f'&key={KEY}'
|
||||
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
|
||||
|
||||
# 返回前端需要的支付参数
|
||||
return {
|
||||
'appId': APPID, # 对应前端:appId
|
||||
'timeStamp': timestamp, # 对应前端:timeStamp
|
||||
'nonceStr': nonce_str, # 对应前端:nonceStr
|
||||
'package': package, # 对应前端:package
|
||||
'signType': sign_type, # 对应前端:signType
|
||||
'paySign': pay_sign # 对应前端:paySign
|
||||
}
|
||||
else:
|
||||
# 获取错误信息
|
||||
err_code = root.find('err_code')
|
||||
err_code_des = root.find('err_code_des')
|
||||
return_msg = root.find('return_msg')
|
||||
|
||||
error_details = f"return_code: {return_code}"
|
||||
if err_code is not None:
|
||||
error_details += f", err_code: {err_code.text}"
|
||||
if err_code_des is not None:
|
||||
error_details += f", err_code_des: {err_code_des.text}"
|
||||
if return_msg is not None:
|
||||
error_details += f", return_msg: {return_msg.text}"
|
||||
|
||||
raise Exception(f"微信支付下单失败: {error_details}")
|
||||
|
||||
def get_client_ip(self):
|
||||
"""获取客户端IP(从DRF的request对象中获取)"""
|
||||
request = self.request
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip if ip else '127.0.0.1'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class YajinHuitiao(View):
|
||||
"""
|
||||
押金支付回调接口
|
||||
路径:/shangpin/yajinhuitiao
|
||||
注意:这个接口不需要JWT认证,由微信服务器调用
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 1. 解析微信回调XML数据
|
||||
request_body = request.body
|
||||
logger.info(f"收到微信支付回调,原始数据: {request_body.decode('utf-8')}")
|
||||
|
||||
root = ET.fromstring(request_body)
|
||||
|
||||
# 获取关键字段
|
||||
return_code = root.find('return_code').text
|
||||
result_code = root.find('result_code').text
|
||||
out_trade_no = root.find('out_trade_no').text # 订单ID
|
||||
transaction_id = root.find('transaction_id').text # 微信交易号
|
||||
total_fee = int(root.find('total_fee').text) # 支付金额(分)
|
||||
|
||||
logger.info(f"回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分")
|
||||
|
||||
from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature
|
||||
notify_club = club_id_from_czjilu(out_trade_no)
|
||||
|
||||
# 2. 验证基本返回状态
|
||||
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
|
||||
logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}")
|
||||
return self.wechat_response('FAIL', '支付失败')
|
||||
|
||||
# 3. 验证签名(确保是微信发来的)
|
||||
if not verify_wechat_v2_signature(request_body, notify_club):
|
||||
logger.error(f"签名验证失败: {out_trade_no}")
|
||||
return self.wechat_response('FAIL', '签名验证失败')
|
||||
|
||||
# 4. 查询订单
|
||||
try:
|
||||
order = Czjilu.query.get(dingdan_id=out_trade_no)
|
||||
logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}")
|
||||
except Czjilu.DoesNotExist:
|
||||
logger.error(f"订单不存在: {out_trade_no}")
|
||||
return self.wechat_response('FAIL', '订单不存在')
|
||||
|
||||
# 5. 检查订单状态是否为未支付(9)
|
||||
if order.zhuangtai != 9:
|
||||
logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
|
||||
try:
|
||||
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
|
||||
repair_wechat_income_if_missing(
|
||||
Decimal(str(order.jine)),
|
||||
getattr(order, 'club_id', None),
|
||||
biz_ref=f'cz:{out_trade_no}',
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error('押金回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True)
|
||||
return self.wechat_response('SUCCESS', 'OK')
|
||||
|
||||
# 6. 验证金额(微信返回的是分,需要转换)
|
||||
wechat_amount_yuan = total_fee / 100 # 转换为元
|
||||
order_amount_yuan = float(order.jine) # 订单金额(元)
|
||||
|
||||
# 允许1分钱的误差(浮点数比较)
|
||||
if abs(wechat_amount_yuan - order_amount_yuan) > 0.01:
|
||||
logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元")
|
||||
return self.wechat_response('FAIL', '金额不一致')
|
||||
|
||||
from jituan.services.czjilu_fulfill import fulfill_yajin_recharge, CzjiluFulfillError
|
||||
try:
|
||||
fulfill_yajin_recharge(out_trade_no, paid_amount_yuan=wechat_amount_yuan)
|
||||
except CzjiluFulfillError as exc:
|
||||
logger.error('押金回调履约失败 %s: %s', out_trade_no, exc)
|
||||
return self.wechat_response('FAIL', str(exc))
|
||||
|
||||
logger.info(f"押金支付回调处理完成: {out_trade_no}")
|
||||
return self.wechat_response('SUCCESS', 'OK')
|
||||
|
||||
except ET.ParseError as e:
|
||||
logger.error(f"XML解析失败: {str(e)}")
|
||||
return self.wechat_response('FAIL', '数据格式错误')
|
||||
except Exception as e:
|
||||
logger.error(f"押金支付回调处理异常: {str(e)}", exc_info=True)
|
||||
# 返回FAIL让微信重试
|
||||
return self.wechat_response('FAIL', '处理异常')
|
||||
|
||||
def verify_signature(self, xml_data):
|
||||
"""验证微信支付签名"""
|
||||
try:
|
||||
root = ET.fromstring(xml_data)
|
||||
sign = root.find('sign').text
|
||||
|
||||
if not sign:
|
||||
logger.error("签名字段不存在")
|
||||
return False
|
||||
|
||||
# 获取所有参数并排除sign
|
||||
params = {}
|
||||
for child in root:
|
||||
if child.tag != 'sign' and child.text is not None:
|
||||
params[child.tag] = child.text
|
||||
|
||||
# 按照参数名ASCII码从小到大排序
|
||||
sorted_params = sorted(params.items(), key=lambda x: x[0])
|
||||
|
||||
# 拼接字符串
|
||||
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
|
||||
sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
|
||||
|
||||
# 计算签名
|
||||
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
|
||||
|
||||
is_valid = sign == calculated_sign
|
||||
if not is_valid:
|
||||
logger.error(f"签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}")
|
||||
|
||||
return is_valid
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证签名异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def wechat_response(self, return_code, return_msg):
|
||||
"""返回微信支付回调响应"""
|
||||
xml_response = f"""<xml>
|
||||
<return_code><![CDATA[{return_code}]]></return_code>
|
||||
<return_msg><![CDATA[{return_msg}]]></return_msg>
|
||||
</xml>"""
|
||||
|
||||
logger.info(f"返回微信响应: {return_code} - {return_msg}")
|
||||
return HttpResponse(xml_response, content_type='application/xml')
|
||||
|
||||
|
||||
|
||||
|
||||
# views.py
|
||||
class YajinShibai(APIView):
|
||||
"""
|
||||
押金支付失败处理接口
|
||||
路径:/shangpin/yajinshibai
|
||||
请求方式:POST
|
||||
权限:JWT认证
|
||||
参数:dingdanid(订单ID)
|
||||
返回:删除结果
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 1. 获取订单ID
|
||||
dingdanid = request.data.get('dingdanid')
|
||||
if not dingdanid:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单ID不能为空'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2. 查询订单(确保是当前用户的订单)
|
||||
try:
|
||||
order = Czjilu.query.get(
|
||||
dingdan_id=dingdanid,
|
||||
yonghuid=request.user.UserUID
|
||||
)
|
||||
except Czjilu.DoesNotExist:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单不存在或不属于当前用户'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 检查订单状态是否为未支付(9)
|
||||
if order.zhuangtai == 9:
|
||||
# 删除订单
|
||||
order.delete()
|
||||
logger.info(f"用户{request.user.UserUID}删除未支付订单: {dingdanid}")
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '订单已删除'
|
||||
}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单状态不是未支付,无法删除'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"押金支付失败处理异常: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '服务器内部错误'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
# views.py
|
||||
class YajinLunxun(APIView):
|
||||
"""
|
||||
押金支付成功轮询接口
|
||||
路径:/shangpin/yajinlunxun
|
||||
请求方式:POST
|
||||
权限:JWT认证
|
||||
参数:dingdanid(订单ID)
|
||||
返回:订单状态和当前押金
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# 1. 获取订单ID
|
||||
dingdanid = request.data.get('dingdanid')
|
||||
if not dingdanid:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单ID不能为空'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2. 查询订单(确保是当前用户的订单)
|
||||
try:
|
||||
order = Czjilu.query.get(
|
||||
dingdan_id=dingdanid,
|
||||
yonghuid=request.user.UserUID
|
||||
)
|
||||
except Czjilu.DoesNotExist:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单不存在或不属于当前用户'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3. 未支付则查微信并幂等履约
|
||||
if order.zhuangtai == 9:
|
||||
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
|
||||
try:
|
||||
confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=2)
|
||||
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
|
||||
except CzjiluFulfillError as exc:
|
||||
logger.info(f"押金确认未就绪 {dingdanid}: {exc}")
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': str(exc),
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if order.zhuangtai != 3:
|
||||
logger.info(f"订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}")
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '订单尚未支付完成'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4. 获取打手押金(通过反向查询,最快捷的方式)
|
||||
try:
|
||||
dashou = request.user.DashouProfile
|
||||
yajin = float(dashou.yajin) # 转为浮点数方便前端处理
|
||||
|
||||
logger.info(f"轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}押金为{yajin}元")
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '支付流程全部完成',
|
||||
'yajin': yajin # 返回当前押金(前端需要更新全局变量)
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except UserDashou.DoesNotExist:
|
||||
logger.error(f"打手信息不存在: 用户{request.user.UserUID}")
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '打手信息不存在'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"押金轮询异常: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '服务器内部错误'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
# views.py
|
||||
|
||||
|
||||
Reference in New Issue
Block a user