feat: UFO 转盘购票与专区图标(不影响现有俱乐部点单)

新增转盘活动/奖品/机会表与 Czjilu leixing=7 履约;专区 tupian_url;hqsp/详情下发 shi_zhuanpan。普通点单与抢单主链路保持不变。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-22 18:59:26 +08:00
parent dacd55bb1e
commit 6a107e873d
14 changed files with 953 additions and 11 deletions

View File

@@ -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,

View File

@@ -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}')

View File

@@ -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({

View File

@@ -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 '',
}
})

View 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'})