Files
Django/products/views/zhuanpan_payment.py

902 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""转盘购票 / 抽奖 / 后台配置(独立业务域,不影响普通点单与抢单池)。
路径前缀:/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'})