"""转盘购票 / 抽奖 / 后台配置(独立业务域,不影响普通点单与抢单池)。 路径前缀:/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 = '' + ''.join(f'<{k}>' for k, v in params.items()) + '' 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'' f'', 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'})