feat: UFO 转盘购票与专区图标(不影响现有俱乐部点单)
新增转盘活动/奖品/机会表与 Czjilu leixing=7 履约;专区 tupian_url;hqsp/详情下发 shi_zhuanpan。普通点单与抢单主链路保持不变。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -57,6 +57,17 @@ from .chongzhi_payment import (
|
||||
IDENTITY_CONFIG,
|
||||
)
|
||||
from .alipay_czjilu_notify import AlipayCzjiluNotifyView, CzjiluRepairView
|
||||
from .zhuanpan_payment import (
|
||||
ZhuanpanGoumai,
|
||||
ZhuanpanLunxun,
|
||||
ZhuanpanShibai,
|
||||
ZhuanpanHuitiao,
|
||||
ZhuanpanJiangpinList,
|
||||
ZhuanpanChoujiang,
|
||||
ZhuanpanWode,
|
||||
AdZhuanpanHuodongView,
|
||||
AdZhuanpanJiangpinView,
|
||||
)
|
||||
from .shangpin_admin import (
|
||||
ShangpinLeixingFabuView,
|
||||
ShangpinZhuanquFabuView,
|
||||
|
||||
@@ -17,6 +17,7 @@ from products.models import Czjilu
|
||||
from products.czjilu_types import (
|
||||
CZJILU_LEIXING_HUIYUAN, CZJILU_LEIXING_YAJIN, CZJILU_LEIXING_JIFEN,
|
||||
CZJILU_LEIXING_SHANGJIA, CZJILU_LEIXING_KAOHE, CZJILU_LEIXING_FAKUAN,
|
||||
CZJILU_LEIXING_ZHUANPAN,
|
||||
CZJILU_PAID_STATUS, is_czjilu_fulfillable_status,
|
||||
)
|
||||
|
||||
@@ -43,6 +44,9 @@ def _fulfill_czjilu_by_leixing(out_trade_no, leixing, paid_amount, source='callb
|
||||
if leixing == CZJILU_LEIXING_FAKUAN:
|
||||
from jituan.services.czjilu_fulfill import fulfill_fakuan
|
||||
return fulfill_fakuan(out_trade_no, paid_amount_yuan=paid_amount)
|
||||
if leixing == CZJILU_LEIXING_ZHUANPAN:
|
||||
from jituan.services.czjilu_fulfill import fulfill_zhuanpan
|
||||
return fulfill_zhuanpan(out_trade_no, paid_amount_yuan=paid_amount)
|
||||
raise ValueError(f'未知 leixing={leixing}')
|
||||
|
||||
|
||||
|
||||
@@ -212,14 +212,20 @@ class ShangpinXiangqingHuoquView(APIView):
|
||||
|
||||
# 直接构建数据,不处理空值
|
||||
data = {
|
||||
"id": shangpin.id,
|
||||
"tupian": shangpin.tupian_url,
|
||||
"tupian_url": shangpin.tupian_url,
|
||||
"biaoti": shangpin.biaoqian,
|
||||
"biaoqian": shangpin.biaoqian,
|
||||
"jiage": shangpin.jiage,
|
||||
"xiangqing": shangpin.jieshao,
|
||||
"xiadanxuzhi": shangpin.xiadan_xuzhi,
|
||||
"guize": shangpin.guize_tupian,
|
||||
"xiaoliang": shangpin.duiwai_xiaoliang,
|
||||
"kucun": shangpin.kucun,
|
||||
"leixing_id": shangpin.leixing_id,
|
||||
"zhuanqu_id": shangpin.zhuanqu_id,
|
||||
"shi_zhuanpan": bool(getattr(shangpin, 'shi_zhuanpan', False)),
|
||||
}
|
||||
|
||||
return Response({
|
||||
|
||||
@@ -325,6 +325,7 @@ class ShangpinZhuanquFabuView(APIView):
|
||||
# === 2. 接收并验证字段 ===
|
||||
mingzi = request.data.get('mingzi', '').strip()
|
||||
leixing_id = request.data.get('leixing_id')
|
||||
tupian_url = (request.data.get('tupian_url') or '').strip()
|
||||
|
||||
# 2.1 验证必填字段
|
||||
if not mingzi:
|
||||
@@ -362,6 +363,7 @@ class ShangpinZhuanquFabuView(APIView):
|
||||
mingzi=mingzi,
|
||||
leixing_id=leixing_id,
|
||||
club_id=club_id,
|
||||
tupian_url=tupian_url,
|
||||
)
|
||||
|
||||
zhuanqu_obj.save()
|
||||
@@ -370,7 +372,8 @@ class ShangpinZhuanquFabuView(APIView):
|
||||
response_data = {
|
||||
'id': zhuanqu_obj.id,
|
||||
'mingzi': mingzi,
|
||||
'leixing_id': leixing_id
|
||||
'leixing_id': leixing_id,
|
||||
'tupian_url': tupian_url,
|
||||
}
|
||||
|
||||
# 返回成功响应
|
||||
@@ -1547,9 +1550,10 @@ class AdZhuanquXiuGai(APIView):
|
||||
status=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
|
||||
# 2. 获取专区ID和新的专区名称
|
||||
# 2. 获取专区ID和新的专区名称(可选图标)
|
||||
zhuanqu_id = request.data.get('zhuanqu_id')
|
||||
mingzi = request.data.get('mingzi', '').strip()
|
||||
tupian_url = request.data.get('tupian_url', None)
|
||||
|
||||
if not zhuanqu_id:
|
||||
return Response(
|
||||
@@ -1578,8 +1582,10 @@ class AdZhuanquXiuGai(APIView):
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 4. 更新专区名称
|
||||
# 4. 更新专区名称(及可选图标;未传 tupian_url 则不改图)
|
||||
zhuanqu_obj.mingzi = mingzi
|
||||
if tupian_url is not None:
|
||||
zhuanqu_obj.tupian_url = (tupian_url or '').strip()
|
||||
zhuanqu_obj.save()
|
||||
|
||||
# 5. 返回成功响应
|
||||
@@ -1589,7 +1595,8 @@ class AdZhuanquXiuGai(APIView):
|
||||
'data': {
|
||||
'id': zhuanqu_obj.id,
|
||||
'mingzi': zhuanqu_obj.mingzi,
|
||||
'leixing_id': zhuanqu_obj.leixing_id
|
||||
'leixing_id': zhuanqu_obj.leixing_id,
|
||||
'tupian_url': getattr(zhuanqu_obj, 'tupian_url', '') or '',
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
632
products/views/zhuanpan_payment.py
Normal file
632
products/views/zhuanpan_payment.py
Normal file
@@ -0,0 +1,632 @@
|
||||
"""转盘购票 / 抽奖 / 后台配置(独立业务域,不影响普通点单与抢单池)。
|
||||
|
||||
路径前缀:/shangpin/
|
||||
- zhuanpangoumai 购票下单(写 Czjilu leixing=7 + ZhuanpanJihui)
|
||||
- zhuanpanlunxun 支付复查
|
||||
- zhuanpanhuitiao 微信回调
|
||||
- zhuanpanshibai 关单
|
||||
- zhuanpanjiangpin 奖品格
|
||||
- zhuanpanchoujiang 抽奖(后端控奖)
|
||||
- zhuanpanwode 我的抽奖机会
|
||||
- adzhuanpan* 后台活动/奖品配置
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
import traceback
|
||||
from decimal import Decimal
|
||||
|
||||
import defusedxml.ElementTree as ET
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
from django.views import View
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from jituan.services.club_context import resolve_effective_club_id
|
||||
from jituan.services.club_user import get_payment_openid
|
||||
from jituan.services.club_write import resolve_club_id_for_write
|
||||
from products.czjilu_types import (
|
||||
CZJILU_LEIXING_ZHUANPAN,
|
||||
ZHUANPAN_ORDER_PREFIX,
|
||||
close_unpaid_czjilu,
|
||||
)
|
||||
from products.models import (
|
||||
Czjilu, Shangpin, ZhuanpanHuodong, ZhuanpanJiangpin, ZhuanpanJihui,
|
||||
)
|
||||
from utils.money import yuan_to_fen
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _gen_czzp_id():
|
||||
timestamp_ms = int(time.time() * 1000)
|
||||
timestamp_ns = int(time.time_ns() // 1000) % 1000000
|
||||
random_num = random.randint(0, 99)
|
||||
return f"{ZHUANPAN_ORDER_PREFIX}{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
|
||||
|
||||
|
||||
def _pick_jiangpin(jiangpin_list):
|
||||
"""按权重抽奖,返回选中的奖品对象。"""
|
||||
total = sum(max(int(j.quanzhong or 0), 0) for j in jiangpin_list)
|
||||
if total <= 0:
|
||||
return jiangpin_list[0]
|
||||
r = random.randint(1, total)
|
||||
acc = 0
|
||||
for j in jiangpin_list:
|
||||
acc += max(int(j.quanzhong or 0), 0)
|
||||
if r <= acc:
|
||||
return j
|
||||
return jiangpin_list[-1]
|
||||
|
||||
|
||||
def _create_pool_order_from_prize(jihui, jiangpin, request_user):
|
||||
"""中奖后写普通 Order Status=1 进抢池;谢谢参与则不创建。"""
|
||||
if jiangpin.shi_xiexie or not jiangpin.shangpin_id:
|
||||
return ''
|
||||
|
||||
from orders.models import Order, PlatformOrderExt
|
||||
from jituan.services.club_config import get_commission_rate_object
|
||||
from shop.utils import calculate_pingtai_and_dianpu_shouyi
|
||||
|
||||
shangpin = Shangpin.query.filter(id=jiangpin.shangpin_id).first()
|
||||
if not shangpin:
|
||||
raise ValueError('中奖商品不存在')
|
||||
|
||||
club_id = jihui.club_id
|
||||
shangpin_jiage = Decimal(str(shangpin.jiage or 0))
|
||||
if shangpin.kaioi_ewai_dashou_fencheng and shangpin.ewai_dashou_fencheng is not None:
|
||||
dashou_fencheng = Decimal(str(shangpin.ewai_dashou_fencheng))
|
||||
else:
|
||||
dashou_lilu = get_commission_rate_object(club_id, '1')
|
||||
rate = Decimal(str(getattr(dashou_lilu, 'Rate', 0) or 0))
|
||||
dashou_fencheng = shangpin_jiage * rate
|
||||
|
||||
timestamp = int(time.time())
|
||||
timestamp_ns = int(time.time_ns() // 1000) % 1000000
|
||||
random_num = random.randint(0, 99)
|
||||
dingdanid = f"PT{timestamp:011d}{timestamp_ns:06d}{random_num:02d}"
|
||||
|
||||
laoban_yonghuid = getattr(request_user, 'UserUID', '') or jihui.yonghuid
|
||||
dingdan = Order.query.create(
|
||||
OrderID=dingdanid,
|
||||
Status=1, # 已付待抢
|
||||
Amount=shangpin_jiage,
|
||||
Platform=1,
|
||||
PlayerCommission=dashou_fencheng,
|
||||
AssignedID=None,
|
||||
ProductID=shangpin.id,
|
||||
GrabRequirement=getattr(shangpin, 'yaoqiuleixing', None),
|
||||
MembershipID=getattr(shangpin, 'huiyuan_id', '') or '',
|
||||
CommissionReq=getattr(shangpin, 'yongjin', 0) or 0,
|
||||
Description=getattr(shangpin, 'jieshao', '') or '',
|
||||
Remark=(jihui.beizhu or '')[:200],
|
||||
Nickname=jihui.nicheng or '',
|
||||
ImageURL=getattr(shangpin, 'tupian_url', None) or '',
|
||||
User1ID=f"Boss{laoban_yonghuid}",
|
||||
ProductTypeID=shangpin.leixing_id,
|
||||
ClubID=club_id,
|
||||
WechatTransactionID=jihui.czjilu_dingdan_id,
|
||||
)
|
||||
|
||||
dianpu_id, pingtai_shouyi, dianpu_shouyi, shangji_dianpu_id, shangji_fenhong = \
|
||||
calculate_pingtai_and_dianpu_shouyi(
|
||||
shangpin_id=shangpin.id,
|
||||
dingdan_jiage=shangpin_jiage,
|
||||
dashou_fencheng=dashou_fencheng,
|
||||
)
|
||||
pingtaikuozhan_data = {
|
||||
'Order': dingdan,
|
||||
'BossID': laoban_yonghuid,
|
||||
'PlatformIncome': pingtai_shouyi,
|
||||
'ParentShopID': shangji_dianpu_id,
|
||||
'ParentBonus': shangji_fenhong if shangji_fenhong else Decimal('0.00'),
|
||||
'ShopIncome': dianpu_shouyi if dianpu_id else Decimal('0.00'),
|
||||
}
|
||||
if dianpu_id:
|
||||
pingtaikuozhan_data['ShopID'] = dianpu_id
|
||||
PlatformOrderExt.query.create(**pingtaikuozhan_data)
|
||||
|
||||
try:
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
dingdan_guangbo.delay({
|
||||
'OrderID': dingdanid,
|
||||
'game_type': '',
|
||||
'amount': str(shangpin_jiage),
|
||||
'order_desc': (getattr(shangpin, 'jieshao', None) or getattr(shangpin, 'biaoqian', '') or '')[:50],
|
||||
})
|
||||
except Exception as exc:
|
||||
logger.warning('转盘中奖广播失败 dingdan=%s: %s', dingdanid, exc)
|
||||
|
||||
return dingdanid
|
||||
|
||||
|
||||
class ZhuanpanGoumai(APIView):
|
||||
"""POST /shangpin/zhuanpangoumai 转盘购票。"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
huodong_id = request.data.get('huodong_id')
|
||||
shangpin_id = request.data.get('shangpin_id')
|
||||
nicheng = (request.data.get('nicheng') or '').strip()
|
||||
beizhu = (request.data.get('Remark') or request.data.get('beizhu') or '').strip()
|
||||
pay_method = (request.data.get('pay_method') or 'wechat').strip().lower()
|
||||
|
||||
if not nicheng:
|
||||
return Response({'code': 400, 'message': '请填写游戏昵称'}, status=400)
|
||||
|
||||
club_id = resolve_club_id_for_write(request, request.user)
|
||||
huodong = None
|
||||
if huodong_id:
|
||||
huodong = ZhuanpanHuodong.query.filter(
|
||||
id=int(huodong_id), club_id=club_id, qiyong=True
|
||||
).first()
|
||||
elif shangpin_id:
|
||||
huodong = ZhuanpanHuodong.query.filter(
|
||||
shangpin_id=int(shangpin_id), club_id=club_id, qiyong=True
|
||||
).first()
|
||||
|
||||
if not huodong:
|
||||
return Response({'code': 404, 'message': '转盘活动不存在或未启用'}, status=404)
|
||||
|
||||
ticket = Shangpin.query.filter(id=huodong.shangpin_id, club_id=club_id).first()
|
||||
if not ticket:
|
||||
return Response({'code': 404, 'message': '购票商品不存在'}, status=404)
|
||||
|
||||
jine = Decimal(str(ticket.jiage or 0))
|
||||
if jine <= 0:
|
||||
return Response({'code': 400, 'message': '购票价格异常'}, status=400)
|
||||
|
||||
prizes = list(ZhuanpanJiangpin.query.filter(huodong_id=huodong.id).order_by('paixu', 'id'))
|
||||
if not prizes:
|
||||
return Response({'code': 400, 'message': '活动未配置奖品'}, status=400)
|
||||
|
||||
dingdanid = _gen_czzp_id()
|
||||
with transaction.atomic():
|
||||
Czjilu.query.create(
|
||||
dingdan_id=dingdanid,
|
||||
zhuangtai=9,
|
||||
yonghuid=request.user.UserUID,
|
||||
jine=jine,
|
||||
leixing=CZJILU_LEIXING_ZHUANPAN,
|
||||
shuoming=getattr(settings, 'ZHUANPAN_PAY_DESCRIPTION', '转盘购票'),
|
||||
club_id=club_id,
|
||||
)
|
||||
ZhuanpanJihui.query.create(
|
||||
club_id=club_id,
|
||||
yonghuid=request.user.UserUID,
|
||||
huodong_id=huodong.id,
|
||||
czjilu_dingdan_id=dingdanid,
|
||||
jine=jine,
|
||||
zhuangtai=9,
|
||||
nicheng=nicheng[:50],
|
||||
beizhu=beizhu[:200],
|
||||
)
|
||||
|
||||
if pay_method == 'alipay':
|
||||
from jituan.services.alipay_pay import generate_alipay_czjilu_pay_params
|
||||
pay_params = generate_alipay_czjilu_pay_params(
|
||||
dingdanid=dingdanid,
|
||||
jine=jine,
|
||||
subject=getattr(settings, 'ZHUANPAN_PAY_DESCRIPTION', '转盘购票'),
|
||||
club_id=club_id,
|
||||
)
|
||||
else:
|
||||
pay_openid, _ = get_payment_openid(request, club_id=club_id)
|
||||
if not pay_openid:
|
||||
Czjilu.query.filter(dingdan_id=dingdanid).delete()
|
||||
ZhuanpanJihui.query.filter(czjilu_dingdan_id=dingdanid).delete()
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '当前小程序未绑定微信openid,请重新登录',
|
||||
}, status=400)
|
||||
pay_params = self._wechat_pay_params(request, dingdanid, jine, pay_openid, club_id)
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': 'ok',
|
||||
'payParams': pay_params,
|
||||
'dingdanid': dingdanid,
|
||||
'jihui_id': ZhuanpanJihui.query.filter(czjilu_dingdan_id=dingdanid).first().id,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error('转盘购票异常: %s', e, exc_info=True)
|
||||
return Response({'code': 500, 'message': f'购票失败: {e}'}, status=500)
|
||||
|
||||
def _wechat_pay_params(self, request, dingdanid, jine, openid, club_id):
|
||||
from jituan.services.wechat_pay import get_wechat_v2_config
|
||||
from jituan.constants import FUBEI_BIZ_CZJILU
|
||||
from jituan.services.mini_pay_router import maybe_create_fubei_jsapi_params
|
||||
|
||||
PAY_DESCRIPTION = getattr(settings, 'ZHUANPAN_PAY_DESCRIPTION', '转盘购票')
|
||||
NOTIFY_URL = getattr(settings, 'ZHUANPAN_PAY_NOTIFY_URL', '')
|
||||
|
||||
fb_params = maybe_create_fubei_jsapi_params(
|
||||
club_id=club_id,
|
||||
out_trade_no=dingdanid,
|
||||
amount_yuan=jine,
|
||||
body=PAY_DESCRIPTION,
|
||||
openid=openid,
|
||||
notify_url=NOTIFY_URL,
|
||||
biz_type=FUBEI_BIZ_CZJILU,
|
||||
)
|
||||
if fb_params is not None:
|
||||
return fb_params
|
||||
|
||||
pay_cfg = get_wechat_v2_config(club_id)
|
||||
APPID = pay_cfg['appid']
|
||||
MCHID = pay_cfg['mch_id']
|
||||
KEY = pay_cfg['key']
|
||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
total_fee = yuan_to_fen(jine)
|
||||
|
||||
import requests as http_requests
|
||||
params = {
|
||||
'appid': APPID,
|
||||
'mch_id': MCHID,
|
||||
'nonce_str': nonce_str,
|
||||
'body': PAY_DESCRIPTION,
|
||||
'out_trade_no': dingdanid,
|
||||
'total_fee': str(total_fee),
|
||||
'spbill_create_ip': request.META.get('REMOTE_ADDR', '127.0.0.1'),
|
||||
'notify_url': NOTIFY_URL,
|
||||
'trade_type': 'JSAPI',
|
||||
'openid': openid,
|
||||
}
|
||||
sign_str = '&'.join(f'{k}={params[k]}' for k in sorted(params) if params[k]) + f'&key={KEY}'
|
||||
params['sign'] = hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
|
||||
xml_body = '<xml>' + ''.join(f'<{k}><![CDATA[{v}]]></{k}>' for k, v in params.items()) + '</xml>'
|
||||
resp = http_requests.post('https://api.mch.weixin.qq.com/pay/unifiedorder', data=xml_body.encode('utf-8'), timeout=15)
|
||||
root = ET.fromstring(resp.content)
|
||||
if root.findtext('return_code') != 'SUCCESS' or root.findtext('result_code') != 'SUCCESS':
|
||||
raise Exception(root.findtext('err_code_des') or root.findtext('return_msg') or '统一下单失败')
|
||||
prepay_id = root.findtext('prepay_id')
|
||||
ts = str(int(time.time()))
|
||||
pay_nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
pkg = f'prepay_id={prepay_id}'
|
||||
pay_sign_src = f'appId={APPID}&nonceStr={pay_nonce}&package={pkg}&signType=MD5&timeStamp={ts}&key={KEY}'
|
||||
return {
|
||||
'timeStamp': ts,
|
||||
'nonceStr': pay_nonce,
|
||||
'package': pkg,
|
||||
'signType': 'MD5',
|
||||
'paySign': hashlib.md5(pay_sign_src.encode('utf-8')).hexdigest().upper(),
|
||||
}
|
||||
|
||||
|
||||
class ZhuanpanLunxun(APIView):
|
||||
"""POST /shangpin/zhuanpanlunxun"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
dingdanid = request.data.get('dingdanid')
|
||||
if not dingdanid:
|
||||
return Response({'code': 400, 'message': '缺少 dingdanid'}, status=400)
|
||||
try:
|
||||
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
|
||||
result = confirm_czjilu_paid(
|
||||
dingdanid, request.user.UserUID, expected_leixing=CZJILU_LEIXING_ZHUANPAN
|
||||
)
|
||||
jihui = ZhuanpanJihui.query.filter(czjilu_dingdan_id=dingdanid).first()
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': 'ok',
|
||||
'data': {
|
||||
**result,
|
||||
'jihui_id': jihui.id if jihui else None,
|
||||
'jihui_zhuangtai': jihui.zhuangtai if jihui else None,
|
||||
},
|
||||
})
|
||||
except Exception as e:
|
||||
from jituan.services.czjilu_fulfill import CzjiluFulfillError
|
||||
if isinstance(e, CzjiluFulfillError):
|
||||
return Response({'code': 400, 'message': str(e)}, status=400)
|
||||
logger.error('转盘轮询异常: %s', e, exc_info=True)
|
||||
return Response({'code': 500, 'message': str(e)}, status=500)
|
||||
|
||||
|
||||
class ZhuanpanShibai(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
dingdanid = request.data.get('dingdanid')
|
||||
if not dingdanid:
|
||||
return Response({'code': 400, 'message': '缺少 dingdanid'}, status=400)
|
||||
order = Czjilu.query.filter(
|
||||
dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=CZJILU_LEIXING_ZHUANPAN
|
||||
).first()
|
||||
if not order:
|
||||
return Response({'code': 404, 'message': '订单不存在'}, status=404)
|
||||
close_unpaid_czjilu(order, reason='用户取消转盘支付')
|
||||
jihui = ZhuanpanJihui.query.filter(czjilu_dingdan_id=dingdanid).first()
|
||||
if jihui and int(jihui.zhuangtai) == 9:
|
||||
jihui.zhuangtai = 3
|
||||
jihui.save(update_fields=['zhuangtai', 'UpdateTime'])
|
||||
return Response({'code': 200, 'message': 'ok'})
|
||||
|
||||
|
||||
class ZhuanpanHuitiao(View):
|
||||
"""微信转盘购票回调 /shangpin/zhuanpanhuitiao"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
xml_data = request.body
|
||||
root = ET.fromstring(xml_data)
|
||||
if root.findtext('return_code') != 'SUCCESS':
|
||||
return self._wx('FAIL', 'return_code')
|
||||
out_trade_no = root.findtext('out_trade_no') or ''
|
||||
total_fee = root.findtext('total_fee')
|
||||
paid = None
|
||||
if total_fee:
|
||||
paid = Decimal(total_fee) / Decimal('100')
|
||||
from jituan.services.czjilu_fulfill import fulfill_zhuanpan
|
||||
fulfill_zhuanpan(out_trade_no, paid_amount_yuan=paid)
|
||||
return self._wx('SUCCESS', 'OK')
|
||||
except Exception as e:
|
||||
logger.error('转盘微信回调异常: %s', e, exc_info=True)
|
||||
return self._wx('FAIL', 'error')
|
||||
|
||||
def _wx(self, code, msg):
|
||||
return HttpResponse(
|
||||
f'<xml><return_code><![CDATA[{code}]]></return_code>'
|
||||
f'<return_msg><![CDATA[{msg}]]></return_msg></xml>',
|
||||
content_type='application/xml',
|
||||
)
|
||||
|
||||
|
||||
class ZhuanpanJiangpinList(APIView):
|
||||
"""POST /shangpin/zhuanpanjiangpin 拉奖品格(可匿名,按 club)。"""
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
club_id = resolve_effective_club_id(request)
|
||||
huodong_id = request.data.get('huodong_id')
|
||||
shangpin_id = request.data.get('shangpin_id')
|
||||
q = ZhuanpanHuodong.query.filter(club_id=club_id, qiyong=True)
|
||||
if huodong_id:
|
||||
q = q.filter(id=int(huodong_id))
|
||||
elif shangpin_id:
|
||||
q = q.filter(shangpin_id=int(shangpin_id))
|
||||
huodong = q.first()
|
||||
if not huodong:
|
||||
return Response({'code': 404, 'message': '无可用转盘活动', 'data': None}, status=404)
|
||||
prizes = list(
|
||||
ZhuanpanJiangpin.query.filter(huodong_id=huodong.id).order_by('paixu', 'id')
|
||||
)
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': 'ok',
|
||||
'data': {
|
||||
'huodong': {
|
||||
'id': huodong.id,
|
||||
'mingzi': huodong.mingzi,
|
||||
'shangpin_id': huodong.shangpin_id,
|
||||
},
|
||||
'jiangpin': [
|
||||
{
|
||||
'id': p.id,
|
||||
'mingzi': p.mingzi,
|
||||
'tupian_url': p.tupian_url,
|
||||
'paixu': p.paixu,
|
||||
'shi_xiexie': p.shi_xiexie,
|
||||
'shangpin_id': p.shangpin_id,
|
||||
}
|
||||
for p in prizes
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
class ZhuanpanChoujiang(APIView):
|
||||
"""POST /shangpin/zhuanpanchoujiang 执行抽奖(后端控奖)。"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
jihui_id = request.data.get('jihui_id')
|
||||
if not jihui_id:
|
||||
return Response({'code': 400, 'message': '缺少 jihui_id'}, status=400)
|
||||
try:
|
||||
with transaction.atomic():
|
||||
jihui = ZhuanpanJihui.query.select_for_update().filter(
|
||||
id=int(jihui_id), yonghuid=request.user.UserUID
|
||||
).first()
|
||||
if not jihui:
|
||||
return Response({'code': 404, 'message': '抽奖机会不存在'}, status=404)
|
||||
if int(jihui.zhuangtai) != 1:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '机会未支付或已抽过',
|
||||
'data': {'zhuangtai': jihui.zhuangtai},
|
||||
}, status=400)
|
||||
|
||||
prizes = list(
|
||||
ZhuanpanJiangpin.query.filter(huodong_id=jihui.huodong_id).order_by('paixu', 'id')
|
||||
)
|
||||
if not prizes:
|
||||
return Response({'code': 400, 'message': '奖品未配置'}, status=400)
|
||||
|
||||
win = _pick_jiangpin(prizes)
|
||||
dingdan_id = ''
|
||||
if not win.shi_xiexie and win.shangpin_id:
|
||||
dingdan_id = _create_pool_order_from_prize(jihui, win, request.user)
|
||||
|
||||
jihui.zhuangtai = 2
|
||||
jihui.zhongjiang_jiangpin_id = win.id
|
||||
jihui.zhongjiang_shangpin_id = win.shangpin_id
|
||||
jihui.dingdan_id = dingdan_id or ''
|
||||
jihui.save(update_fields=[
|
||||
'zhuangtai', 'zhongjiang_jiangpin_id', 'zhongjiang_shangpin_id',
|
||||
'dingdan_id', 'UpdateTime',
|
||||
])
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': 'ok',
|
||||
'data': {
|
||||
'jihui_id': jihui.id,
|
||||
'jiangpin_id': win.id,
|
||||
'mingzi': win.mingzi,
|
||||
'tupian_url': win.tupian_url,
|
||||
'paixu': win.paixu,
|
||||
'shi_xiexie': win.shi_xiexie,
|
||||
'dingdan_id': dingdan_id,
|
||||
},
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error('抽奖异常: %s\n%s', e, traceback.format_exc())
|
||||
return Response({'code': 500, 'message': str(e)}, status=500)
|
||||
|
||||
|
||||
class ZhuanpanWode(APIView):
|
||||
"""POST /shangpin/zhuanpanwode"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
club_id = resolve_effective_club_id(request)
|
||||
qs = ZhuanpanJihui.query.filter(
|
||||
yonghuid=request.user.UserUID, club_id=club_id
|
||||
).exclude(zhuangtai=9).order_by('-id')[:50]
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': 'ok',
|
||||
'data': [
|
||||
{
|
||||
'id': j.id,
|
||||
'huodong_id': j.huodong_id,
|
||||
'zhuangtai': j.zhuangtai,
|
||||
'jine': str(j.jine),
|
||||
'nicheng': j.nicheng,
|
||||
'zhongjiang_jiangpin_id': j.zhongjiang_jiangpin_id,
|
||||
'dingdan_id': j.dingdan_id,
|
||||
'czjilu_dingdan_id': j.czjilu_dingdan_id,
|
||||
'CreateTime': j.CreateTime.isoformat() if j.CreateTime else '',
|
||||
}
|
||||
for j in qs
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
class AdZhuanpanHuodongView(APIView):
|
||||
"""后台:转盘活动 list/save。POST action=list|save|toggle"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
action = (request.data.get('action') or 'list').strip()
|
||||
club_id = resolve_effective_club_id(request)
|
||||
if action == 'list':
|
||||
rows = list(ZhuanpanHuodong.query.filter(club_id=club_id).order_by('-id'))
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': [
|
||||
{
|
||||
'id': h.id,
|
||||
'mingzi': h.mingzi,
|
||||
'shangpin_id': h.shangpin_id,
|
||||
'qiyong': h.qiyong,
|
||||
'beizhu': h.beizhu,
|
||||
}
|
||||
for h in rows
|
||||
],
|
||||
})
|
||||
if action == 'save':
|
||||
hid = request.data.get('id')
|
||||
mingzi = (request.data.get('mingzi') or '幸运转盘').strip()
|
||||
shangpin_id = int(request.data.get('shangpin_id') or 0)
|
||||
beizhu = (request.data.get('beizhu') or '').strip()
|
||||
qiyong = bool(request.data.get('qiyong', True))
|
||||
if not shangpin_id:
|
||||
return Response({'code': 1, 'msg': '需要 shangpin_id'})
|
||||
# 标记购票商品
|
||||
Shangpin.query.filter(id=shangpin_id, club_id=club_id).update(shi_zhuanpan=True)
|
||||
if hid:
|
||||
h = ZhuanpanHuodong.query.filter(id=int(hid), club_id=club_id).first()
|
||||
if not h:
|
||||
return Response({'code': 1, 'msg': '活动不存在'})
|
||||
h.mingzi = mingzi
|
||||
h.shangpin_id = shangpin_id
|
||||
h.beizhu = beizhu
|
||||
h.qiyong = qiyong
|
||||
h.save()
|
||||
else:
|
||||
h = ZhuanpanHuodong.query.create(
|
||||
club_id=club_id, mingzi=mingzi, shangpin_id=shangpin_id,
|
||||
beizhu=beizhu, qiyong=qiyong,
|
||||
)
|
||||
return Response({'code': 0, 'data': {'id': h.id}})
|
||||
if action == 'toggle':
|
||||
hid = int(request.data.get('id') or 0)
|
||||
h = ZhuanpanHuodong.query.filter(id=hid, club_id=club_id).first()
|
||||
if not h:
|
||||
return Response({'code': 1, 'msg': '不存在'})
|
||||
h.qiyong = not h.qiyong
|
||||
h.save(update_fields=['qiyong', 'UpdateTime'])
|
||||
return Response({'code': 0, 'data': {'qiyong': h.qiyong}})
|
||||
return Response({'code': 1, 'msg': '未知 action'})
|
||||
|
||||
|
||||
class AdZhuanpanJiangpinView(APIView):
|
||||
"""后台:奖品 list/save/delete。"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
action = (request.data.get('action') or 'list').strip()
|
||||
club_id = resolve_effective_club_id(request)
|
||||
huodong_id = int(request.data.get('huodong_id') or 0)
|
||||
huodong = ZhuanpanHuodong.query.filter(id=huodong_id, club_id=club_id).first()
|
||||
if not huodong:
|
||||
return Response({'code': 1, 'msg': '活动不存在或不属于当前俱乐部'})
|
||||
|
||||
if action == 'list':
|
||||
rows = list(ZhuanpanJiangpin.query.filter(huodong_id=huodong_id).order_by('paixu', 'id'))
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': [
|
||||
{
|
||||
'id': p.id,
|
||||
'mingzi': p.mingzi,
|
||||
'tupian_url': p.tupian_url,
|
||||
'shangpin_id': p.shangpin_id,
|
||||
'quanzhong': p.quanzhong,
|
||||
'paixu': p.paixu,
|
||||
'shi_xiexie': p.shi_xiexie,
|
||||
}
|
||||
for p in rows
|
||||
],
|
||||
})
|
||||
if action == 'save':
|
||||
pid = request.data.get('id')
|
||||
data = dict(
|
||||
mingzi=(request.data.get('mingzi') or '').strip() or '奖品',
|
||||
tupian_url=(request.data.get('tupian_url') or '').strip(),
|
||||
shangpin_id=request.data.get('shangpin_id') or None,
|
||||
quanzhong=int(request.data.get('quanzhong') or 1),
|
||||
paixu=int(request.data.get('paixu') or 0),
|
||||
shi_xiexie=bool(request.data.get('shi_xiexie', False)),
|
||||
)
|
||||
if data['shangpin_id'] in ('', None):
|
||||
data['shangpin_id'] = None
|
||||
else:
|
||||
data['shangpin_id'] = int(data['shangpin_id'])
|
||||
if pid:
|
||||
p = ZhuanpanJiangpin.query.filter(id=int(pid), huodong_id=huodong_id).first()
|
||||
if not p:
|
||||
return Response({'code': 1, 'msg': '奖品不存在'})
|
||||
for k, v in data.items():
|
||||
setattr(p, k, v)
|
||||
p.save()
|
||||
else:
|
||||
p = ZhuanpanJiangpin.query.create(huodong_id=huodong_id, **data)
|
||||
return Response({'code': 0, 'data': {'id': p.id}})
|
||||
if action == 'delete':
|
||||
pid = int(request.data.get('id') or 0)
|
||||
ZhuanpanJiangpin.query.filter(id=pid, huodong_id=huodong_id).delete()
|
||||
return Response({'code': 0})
|
||||
return Response({'code': 1, 'msg': '未知 action'})
|
||||
Reference in New Issue
Block a user