902 lines
37 KiB
Python
902 lines
37 KiB
Python
"""转盘购票 / 抽奖 / 后台配置(独立业务域,不影响普通点单与抢单池)。
|
||
|
||
路径前缀:/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, ShangpinLeixing, ShangpinZhuanqu,
|
||
ZhuanpanHuodong, ZhuanpanJiangpin, ZhuanpanJihui, ZhuanpanKongjiang,
|
||
)
|
||
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):
|
||
"""按权重抽奖(仅库存未耗尽:kucun==-1 或不限 / kucun>0)。"""
|
||
pool = [j for j in jiangpin_list if int(getattr(j, 'kucun', -1)) != 0]
|
||
if not pool:
|
||
raise ValueError('奖品库存已全部抽完')
|
||
total = sum(max(int(j.quanzhong or 0), 0) for j in pool)
|
||
if total <= 0:
|
||
return pool[0]
|
||
r = random.randint(1, total)
|
||
acc = 0
|
||
for j in pool:
|
||
acc += max(int(j.quanzhong or 0), 0)
|
||
if r <= acc:
|
||
return j
|
||
return pool[-1]
|
||
|
||
|
||
def _consume_jiangpin_stock(jiangpin):
|
||
"""普通抽中或控奖命中后扣库存;-1 不限不扣;0 不扣(控奖可强制)。"""
|
||
k = int(getattr(jiangpin, 'kucun', -1))
|
||
if k > 0:
|
||
jiangpin.kucun = k - 1
|
||
jiangpin.save(update_fields=['kucun', 'UpdateTime'])
|
||
|
||
|
||
def _take_kongjiang(club_id, huodong_id, yonghuid):
|
||
"""取一条可用控奖并扣减次数;返回奖品对象或 None。"""
|
||
row = (
|
||
ZhuanpanKongjiang.query.select_for_update()
|
||
.filter(
|
||
club_id=club_id,
|
||
huodong_id=huodong_id,
|
||
yonghuid=yonghuid,
|
||
qiyong=True,
|
||
cishu__gt=0,
|
||
)
|
||
.order_by('id')
|
||
.first()
|
||
)
|
||
if not row:
|
||
return None
|
||
prize = ZhuanpanJiangpin.query.filter(id=row.jiangpin_id, huodong_id=huodong_id).first()
|
||
if not prize:
|
||
row.qiyong = False
|
||
row.save(update_fields=['qiyong', 'UpdateTime'])
|
||
return None
|
||
row.cishu = max(int(row.cishu or 0) - 1, 0)
|
||
if row.cishu <= 0:
|
||
row.qiyong = False
|
||
row.save(update_fields=['cishu', 'qiyong', 'UpdateTime'])
|
||
return prize
|
||
|
||
|
||
def _serialize_huodong_row(h, ticket=None):
|
||
if ticket is None:
|
||
ticket = Shangpin.query.filter(id=h.shangpin_id).first()
|
||
return {
|
||
'id': h.id,
|
||
'mingzi': h.mingzi,
|
||
'shangpin_id': h.shangpin_id,
|
||
'qiyong': h.qiyong,
|
||
'beizhu': h.beizhu,
|
||
'biaoqian': (ticket.biaoqian if ticket else '') or h.mingzi,
|
||
'jiage': str(ticket.jiage) if ticket else '0',
|
||
'tupian_url': (ticket.tupian_url if ticket else '') or '',
|
||
'leixing_id': ticket.leixing_id if ticket else None,
|
||
'zhuanqu_id': ticket.zhuanqu_id if ticket else None,
|
||
'shangjia_zhuangtai': bool(ticket.shangjia_zhuangtai) if ticket else False,
|
||
}
|
||
|
||
|
||
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)
|
||
from jituan.services.alipay_pay import alipay_pay_method_block_reason
|
||
_alipay_block = alipay_pay_method_block_reason(pay_method, club_id)
|
||
if _alipay_block:
|
||
return Response({'code': 400, 'message': _alipay_block}, status=400)
|
||
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.select_for_update()
|
||
.filter(huodong_id=jihui.huodong_id)
|
||
.order_by('paixu', 'id')
|
||
)
|
||
if not prizes:
|
||
return Response({'code': 400, 'message': '奖品未配置'}, status=400)
|
||
|
||
# 1) 控奖优先 2) 权重+库存
|
||
win = _take_kongjiang(jihui.club_id, jihui.huodong_id, jihui.yonghuid)
|
||
if win is None:
|
||
win = _pick_jiangpin(prizes)
|
||
_consume_jiangpin_stock(win)
|
||
|
||
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 / create_product / save / toggle / meta。
|
||
|
||
运营不再手填购票商品编号:创建时直接填名称/价格/类型,自动建 Shangpin+Huodong。
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
action = (request.data.get('action') or 'list').strip()
|
||
club_id = resolve_effective_club_id(request)
|
||
|
||
if action == 'meta':
|
||
types = list(
|
||
ShangpinLeixing.query.order_by('id').values('id', 'jieshao')
|
||
)
|
||
zones = list(
|
||
ShangpinZhuanqu.query.filter(club_id=club_id)
|
||
.order_by('-paixu', 'id')
|
||
.values('id', 'mingzi', 'leixing_id')
|
||
)
|
||
# 履约商品:本俱乐部非转盘购票商品
|
||
goods = list(
|
||
Shangpin.query.filter(club_id=club_id, shi_zhuanpan=False)
|
||
.order_by('-paixu', '-id')
|
||
.values('id', 'biaoqian', 'jiage', 'tupian_url', 'leixing_id')[:500]
|
||
)
|
||
for g in goods:
|
||
g['jiage'] = str(g.get('jiage') or 0)
|
||
return Response({
|
||
'code': 0,
|
||
'data': {'leixing': types, 'zhuanqu': zones, 'goods': goods},
|
||
})
|
||
|
||
if action == 'list':
|
||
rows = list(ZhuanpanHuodong.query.filter(club_id=club_id).order_by('-id'))
|
||
sp_ids = [h.shangpin_id for h in rows]
|
||
tickets = {
|
||
s.id: s
|
||
for s in Shangpin.query.filter(id__in=sp_ids, club_id=club_id)
|
||
} if sp_ids else {}
|
||
return Response({
|
||
'code': 0,
|
||
'data': [_serialize_huodong_row(h, tickets.get(h.shangpin_id)) for h in rows],
|
||
})
|
||
|
||
if action == 'create_product':
|
||
biaoqian = (request.data.get('biaoqian') or request.data.get('mingzi') or '').strip()
|
||
mingzi = (request.data.get('mingzi') or biaoqian or '幸运转盘').strip()
|
||
try:
|
||
jiage = Decimal(str(request.data.get('jiage') or 0))
|
||
except Exception:
|
||
return Response({'code': 1, 'msg': '价格格式错误'})
|
||
if not biaoqian:
|
||
return Response({'code': 1, 'msg': '请填写转盘商品名称'})
|
||
if jiage <= 0:
|
||
return Response({'code': 1, 'msg': '价格必须大于 0'})
|
||
leixing_id = request.data.get('leixing_id')
|
||
if not leixing_id:
|
||
return Response({'code': 1, 'msg': '请选择商品类型'})
|
||
try:
|
||
leixing_id = int(leixing_id)
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 1, 'msg': '商品类型无效'})
|
||
zhuanqu_id = request.data.get('zhuanqu_id') or None
|
||
if zhuanqu_id in ('', None):
|
||
zhuanqu_id = None
|
||
else:
|
||
zhuanqu_id = int(zhuanqu_id)
|
||
tupian_url = (request.data.get('tupian_url') or '').strip()
|
||
beizhu = (request.data.get('beizhu') or '').strip()
|
||
qiyong = bool(request.data.get('qiyong', True))
|
||
|
||
with transaction.atomic():
|
||
ticket = Shangpin.query.create(
|
||
biaoqian=biaoqian[:45],
|
||
jiage=jiage,
|
||
kucun=99999,
|
||
leixing_id=leixing_id,
|
||
zhuanqu_id=zhuanqu_id,
|
||
jieshao=beizhu or '转盘购票商品',
|
||
tupian_url=tupian_url,
|
||
club_id=club_id,
|
||
shi_zhuanpan=True,
|
||
shangjia_zhuangtai=True,
|
||
fengjin_zhuangtai=True,
|
||
shenhezhuangtai=1,
|
||
paixu=0,
|
||
)
|
||
h = ZhuanpanHuodong.query.create(
|
||
club_id=club_id,
|
||
mingzi=mingzi[:64],
|
||
shangpin_id=ticket.id,
|
||
beizhu=beizhu,
|
||
qiyong=qiyong,
|
||
)
|
||
return Response({'code': 0, 'data': _serialize_huodong_row(h, ticket)})
|
||
|
||
if action == 'save':
|
||
hid = request.data.get('id')
|
||
if not hid:
|
||
return Response({'code': 1, 'msg': '缺少 id,新建请用 create_product'})
|
||
h = ZhuanpanHuodong.query.filter(id=int(hid), club_id=club_id).first()
|
||
if not h:
|
||
return Response({'code': 1, 'msg': '转盘不存在'})
|
||
ticket = Shangpin.query.filter(id=h.shangpin_id, club_id=club_id).first()
|
||
mingzi = (request.data.get('mingzi') or h.mingzi).strip()
|
||
biaoqian = (request.data.get('biaoqian') or (ticket.biaoqian if ticket else mingzi) or mingzi).strip()
|
||
beizhu = request.data.get('beizhu')
|
||
if beizhu is None:
|
||
beizhu = h.beizhu
|
||
else:
|
||
beizhu = str(beizhu).strip()
|
||
qiyong = bool(request.data.get('qiyong', h.qiyong))
|
||
tupian_url = request.data.get('tupian_url')
|
||
jiage_raw = request.data.get('jiage')
|
||
|
||
h.mingzi = mingzi[:64]
|
||
h.beizhu = beizhu
|
||
h.qiyong = qiyong
|
||
h.save()
|
||
if ticket:
|
||
ticket.biaoqian = biaoqian[:45]
|
||
ticket.shi_zhuanpan = True
|
||
if tupian_url is not None:
|
||
ticket.tupian_url = str(tupian_url).strip()
|
||
if jiage_raw is not None and str(jiage_raw).strip() != '':
|
||
try:
|
||
jv = Decimal(str(jiage_raw))
|
||
if jv > 0:
|
||
ticket.jiage = jv
|
||
except Exception:
|
||
return Response({'code': 1, 'msg': '价格格式错误'})
|
||
ticket.save()
|
||
return Response({'code': 0, 'data': _serialize_huodong_row(h, ticket)})
|
||
|
||
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'])
|
||
ticket = Shangpin.query.filter(id=h.shangpin_id, club_id=club_id).first()
|
||
if ticket:
|
||
ticket.shangjia_zhuangtai = h.qiyong
|
||
ticket.save(update_fields=['shangjia_zhuangtai', 'UpdateTime'])
|
||
return Response({'code': 0, 'data': {'qiyong': h.qiyong}})
|
||
|
||
return Response({'code': 1, 'msg': '未知 action'})
|
||
|
||
|
||
class AdZhuanpanJiangpinView(APIView):
|
||
"""后台:奖品 list/save/delete(含库存;履约商品用下拉 id,不手填语义)。"""
|
||
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'))
|
||
fulfill_ids = [p.shangpin_id for p in rows if p.shangpin_id]
|
||
name_map = {}
|
||
if fulfill_ids:
|
||
name_map = {
|
||
s.id: s.biaoqian
|
||
for s in Shangpin.query.filter(id__in=fulfill_ids, club_id=club_id)
|
||
}
|
||
return Response({
|
||
'code': 0,
|
||
'data': [
|
||
{
|
||
'id': p.id,
|
||
'mingzi': p.mingzi,
|
||
'tupian_url': p.tupian_url,
|
||
'shangpin_id': p.shangpin_id,
|
||
'shangpin_biaoqian': name_map.get(p.shangpin_id, '') if p.shangpin_id else '',
|
||
'quanzhong': p.quanzhong,
|
||
'kucun': int(getattr(p, 'kucun', -1)),
|
||
'paixu': p.paixu,
|
||
'shi_xiexie': p.shi_xiexie,
|
||
}
|
||
for p in rows
|
||
],
|
||
})
|
||
if action == 'save':
|
||
pid = request.data.get('id')
|
||
kucun_raw = request.data.get('kucun', -1)
|
||
try:
|
||
kucun = int(kucun_raw)
|
||
except (TypeError, ValueError):
|
||
kucun = -1
|
||
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),
|
||
kucun=kucun,
|
||
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'])
|
||
# 履约商品必须属于本俱乐部且不能是转盘购票商品
|
||
ful = Shangpin.query.filter(id=data['shangpin_id'], club_id=club_id).first()
|
||
if not ful:
|
||
return Response({'code': 1, 'msg': '履约商品不存在'})
|
||
if getattr(ful, 'shi_zhuanpan', False):
|
||
return Response({'code': 1, 'msg': '不能把转盘购票商品设为履约商品'})
|
||
if data['shi_xiexie']:
|
||
data['shangpin_id'] = None
|
||
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()
|
||
ZhuanpanKongjiang.query.filter(jiangpin_id=pid, huodong_id=huodong_id).delete()
|
||
return Response({'code': 0})
|
||
return Response({'code': 1, 'msg': '未知 action'})
|
||
|
||
|
||
class AdZhuanpanKongjiangView(APIView):
|
||
"""后台控奖:指定用户必中某奖若干次。"""
|
||
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(
|
||
ZhuanpanKongjiang.query.filter(huodong_id=huodong_id, club_id=club_id)
|
||
.order_by('-id')[:200]
|
||
)
|
||
jp_ids = [r.jiangpin_id for r in rows]
|
||
jp_map = {
|
||
p.id: p.mingzi
|
||
for p in ZhuanpanJiangpin.query.filter(id__in=jp_ids, huodong_id=huodong_id)
|
||
} if jp_ids else {}
|
||
return Response({
|
||
'code': 0,
|
||
'data': [
|
||
{
|
||
'id': r.id,
|
||
'yonghuid': r.yonghuid,
|
||
'jiangpin_id': r.jiangpin_id,
|
||
'jiangpin_mingzi': jp_map.get(r.jiangpin_id, ''),
|
||
'cishu': r.cishu,
|
||
'qiyong': r.qiyong,
|
||
'beizhu': r.beizhu,
|
||
}
|
||
for r in rows
|
||
],
|
||
})
|
||
if action == 'save':
|
||
yonghuid = str(request.data.get('yonghuid') or '').strip()
|
||
jiangpin_id = int(request.data.get('jiangpin_id') or 0)
|
||
cishu = int(request.data.get('cishu') or 1)
|
||
beizhu = (request.data.get('beizhu') or '').strip()
|
||
qiyong = bool(request.data.get('qiyong', True))
|
||
if not yonghuid:
|
||
return Response({'code': 1, 'msg': '请填写用户 UID'})
|
||
if cishu < 1:
|
||
return Response({'code': 1, 'msg': '次数至少为 1'})
|
||
prize = ZhuanpanJiangpin.query.filter(id=jiangpin_id, huodong_id=huodong_id).first()
|
||
if not prize:
|
||
return Response({'code': 1, 'msg': '奖品不存在'})
|
||
kid = request.data.get('id')
|
||
if kid:
|
||
row = ZhuanpanKongjiang.query.filter(
|
||
id=int(kid), huodong_id=huodong_id, club_id=club_id
|
||
).first()
|
||
if not row:
|
||
return Response({'code': 1, 'msg': '控奖记录不存在'})
|
||
row.yonghuid = yonghuid
|
||
row.jiangpin_id = jiangpin_id
|
||
row.cishu = cishu
|
||
row.beizhu = beizhu
|
||
row.qiyong = qiyong
|
||
row.save()
|
||
else:
|
||
row = ZhuanpanKongjiang.query.create(
|
||
club_id=club_id,
|
||
huodong_id=huodong_id,
|
||
yonghuid=yonghuid,
|
||
jiangpin_id=jiangpin_id,
|
||
cishu=cishu,
|
||
beizhu=beizhu,
|
||
qiyong=qiyong,
|
||
)
|
||
return Response({'code': 0, 'data': {'id': row.id}})
|
||
if action == 'delete':
|
||
kid = int(request.data.get('id') or 0)
|
||
ZhuanpanKongjiang.query.filter(
|
||
id=kid, huodong_id=huodong_id, club_id=club_id
|
||
).delete()
|
||
return Response({'code': 0})
|
||
return Response({'code': 1, 'msg': '未知 action'})
|