diff --git a/a_long_dianjing/settings.py b/a_long_dianjing/settings.py
index 84c4ceb..4134c24 100644
--- a/a_long_dianjing/settings.py
+++ b/a_long_dianjing/settings.py
@@ -291,6 +291,8 @@ SHANGJIA_CZ_MIN_AMOUNT = 1
SHANGJIA_CZ_MAX_AMOUNT = 10000
FAKUAN_PAY_DESCRIPTION = '罚款缴纳'
FAKUAN_PAY_NOTIFY_URL = 'https://www.abas.asia/hqhd/yonghu/fk_huitiao'
+ZHUANPAN_PAY_DESCRIPTION = '转盘购票'
+ZHUANPAN_PAY_NOTIFY_URL = 'https://www.abas.asia/hqhd/shangpin/zhuanpanhuitiao'
# 支付宝 Czjilu 充值统一回调(会员/押金/积分/商家/考核/罚款)
# 与订单 alipay-notify 分离;密钥仍用 app_secrets.ALIPAY_PROD,此处只写死回调地址
diff --git a/backend/views/products.py b/backend/views/products.py
index 6f30038..83e17a2 100644
--- a/backend/views/products.py
+++ b/backend/views/products.py
@@ -199,6 +199,7 @@ class GetProductBaseDataView(APIView):
'shenhezhuangtai': z.shenhezhuangtai,
'product_count': product_count,
'club_id': getattr(z, 'club_id', '') or '',
+ 'tupian_url': getattr(z, 'tupian_url', '') or '',
})
# 获取所有会员(只需 ID、介绍、价格)
@@ -449,6 +450,7 @@ class GetProductListView(APIView):
'paixu': z.paixu,
'shenhezhuangtai': z.shenhezhuangtai,
'product_count': product_count,
+ 'tupian_url': getattr(z, 'tupian_url', '') or '',
})
# 获取所有会员(只需 ID、介绍、价格)
diff --git a/jituan/services/czjilu_fulfill.py b/jituan/services/czjilu_fulfill.py
index 945a0c4..07136f8 100644
--- a/jituan/services/czjilu_fulfill.py
+++ b/jituan/services/czjilu_fulfill.py
@@ -27,6 +27,64 @@ class CzjiluFulfillError(Exception):
self.code = code
+@transaction.atomic
+def fulfill_zhuanpan(dingdan_id, paid_amount_yuan=None):
+ """转盘购票履约:Czjilu 标已付 + 抽奖机会改为待抽。绝不创建抢单池 Order。"""
+ from products.czjilu_types import CZJILU_LEIXING_ZHUANPAN, CZJILU_PAID_STATUS
+ from products.models import ZhuanpanJihui
+
+ try:
+ order = Czjilu.query.select_for_update().get(dingdan_id=dingdan_id)
+ except Czjilu.DoesNotExist:
+ raise CzjiluFulfillError('订单不存在', 'not_found')
+
+ if order.leixing != CZJILU_LEIXING_ZHUANPAN:
+ raise CzjiluFulfillError('订单类型不是转盘购票', 'type_error')
+
+ if order.zhuangtai == CZJILU_PAID_STATUS:
+ return {'already_done': True, 'dingdan_id': dingdan_id, 'zhuangtai': CZJILU_PAID_STATUS}
+
+ if not is_czjilu_fulfillable_status(order.zhuangtai):
+ return {'already_done': True, 'dingdan_id': dingdan_id, 'zhuangtai': order.zhuangtai}
+
+ amount = Decimal(str(order.jine))
+ if paid_amount_yuan is not None:
+ paid = Decimal(str(paid_amount_yuan))
+ if abs(paid - amount) > Decimal('0.01'):
+ raise CzjiluFulfillError(f'支付金额{paid}与订单{amount}不一致', 'amount_mismatch')
+
+ jihui = ZhuanpanJihui.query.select_for_update().filter(
+ czjilu_dingdan_id=dingdan_id
+ ).first()
+ if not jihui:
+ raise CzjiluFulfillError('转盘机会单不存在', 'not_found')
+
+ order.zhuangtai = CZJILU_PAID_STATUS
+ order.save(update_fields=['zhuangtai'])
+
+ if int(jihui.zhuangtai) == 9:
+ jihui.zhuangtai = 1 # 已付待抽
+ jihui.save(update_fields=['zhuangtai', 'UpdateTime'])
+
+ try:
+ from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
+ repair_wechat_income_if_missing(
+ amount,
+ getattr(order, 'club_id', None) or CLUB_ID_DEFAULT,
+ biz_ref=f'cz:{dingdan_id}',
+ )
+ except Exception as exc:
+ logger.error('转盘购票补记收支失败 dingdan=%s: %s', dingdan_id, exc, exc_info=True)
+
+ logger.info('转盘购票履约完成 dingdan=%s jihui=%s', dingdan_id, jihui.id)
+ return {
+ 'already_done': False,
+ 'dingdan_id': dingdan_id,
+ 'zhuangtai': CZJILU_PAID_STATUS,
+ 'jihui_id': jihui.id,
+ }
+
+
@transaction.atomic
def fulfill_yajin_recharge(dingdan_id, paid_amount_yuan=None):
"""押金充值幂等履约:仅待履约状态(9/用户关单2)时入账;已付(3)直接跳过防多加。"""
@@ -422,6 +480,8 @@ def confirm_czjilu_paid(dingdan_id, yonghuid, expected_leixing):
return fulfill_kaohe(dingdan_id, paid_amount_yuan=paid)
if expected_leixing == 6:
return fulfill_fakuan(dingdan_id, paid_amount_yuan=paid)
+ if expected_leixing == 7:
+ return fulfill_zhuanpan(dingdan_id, paid_amount_yuan=paid)
raise CzjiluFulfillError('不支持的充值类型', 'type_error')
diff --git a/orders/views/payment.py b/orders/views/payment.py
index b22fb23..b033302 100644
--- a/orders/views/payment.py
+++ b/orders/views/payment.py
@@ -813,6 +813,14 @@ class CreateOrderView(APIView):
if not shangpin:
return Response({'code': 4, 'msg': '商品不存在', 'data': None})
+ # 转盘购票商品禁止走普通点单,避免支付后直接进抢池
+ if getattr(shangpin, 'shi_zhuanpan', False):
+ return Response({
+ 'code': 4,
+ 'msg': '该商品为转盘购票,请使用转盘下单',
+ 'data': None,
+ })
+
is_valid, err_msg, _ = validate_shangpin_and_dianpu(shangpin_id)
if not is_valid:
diff --git a/products/czjilu_types.py b/products/czjilu_types.py
index 74800c3..a9acf20 100644
--- a/products/czjilu_types.py
+++ b/products/czjilu_types.py
@@ -9,6 +9,9 @@ CZJILU_LEIXING_JIFEN = 3
CZJILU_LEIXING_SHANGJIA = 4
CZJILU_LEIXING_KAOHE = 5
CZJILU_LEIXING_FAKUAN = 6
+CZJILU_LEIXING_ZHUANPAN = 7 # 转盘购票(履约只写抽奖机会,不进抢单池)
+
+ZHUANPAN_ORDER_PREFIX = 'CzZP'
CZJILU_PAID_STATUS = 3
# 用户取消/关闭:保留流水,不物理删除;若官方已付款,回调/查单仍可履约入账(防少加)
@@ -118,7 +121,8 @@ def looks_like_czjilu_out_trade_no(out_trade_no):
if not s:
return False
prefixes = (
- 'CzHY', 'CzYJ', 'CzJF', 'CzSJ', 'KH', 'FK',
+ 'CzHY', 'CzYJ', 'CzJF', 'CzSJ', 'KH', 'FK', 'CzZP',
getattr(settings, 'SHANGJIA_CZ_PREFIX', 'CzSJ') or 'CzSJ',
+ ZHUANPAN_ORDER_PREFIX,
)
return any(s.startswith(p) for p in prefixes if p)
diff --git a/products/migrations/0013_zhuanpan_and_zhuanqu_icon.py b/products/migrations/0013_zhuanpan_and_zhuanqu_icon.py
new file mode 100644
index 0000000..c352bbd
--- /dev/null
+++ b/products/migrations/0013_zhuanpan_and_zhuanqu_icon.py
@@ -0,0 +1,102 @@
+# Generated manually for UFO turntable + zone icon
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('products', '0012_shangpin_zhuanqu_club_id'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='shangpinzhuanqu',
+ name='tupian_url',
+ field=models.CharField(
+ blank=True, default='', max_length=500, null=True,
+ verbose_name='专区图标URL',
+ ),
+ ),
+ migrations.AddField(
+ model_name='shangpin',
+ name='shi_zhuanpan',
+ field=models.BooleanField(
+ db_index=True, default=False, verbose_name='是否转盘购票商品',
+ ),
+ ),
+ migrations.CreateModel(
+ name='ZhuanpanHuodong',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('club_id', models.CharField(db_index=True, max_length=16, verbose_name='俱乐部')),
+ ('mingzi', models.CharField(default='幸运转盘', max_length=64, verbose_name='活动名')),
+ ('shangpin_id', models.PositiveIntegerField(db_index=True, help_text='对应 Shangpin.id,且该商品建议 shi_zhuanpan=True', verbose_name='购票商品ID')),
+ ('qiyong', models.BooleanField(db_index=True, default=True, verbose_name='是否启用')),
+ ('beizhu', models.CharField(blank=True, default='', max_length=200, verbose_name='备注')),
+ ('CreateTime', models.DateTimeField(auto_now_add=True)),
+ ('UpdateTime', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'verbose_name': '转盘活动',
+ 'verbose_name_plural': '转盘活动',
+ 'db_table': 'zhuanpan_huodong',
+ },
+ ),
+ migrations.CreateModel(
+ name='ZhuanpanJiangpin',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('huodong_id', models.PositiveIntegerField(db_index=True, verbose_name='活动ID')),
+ ('mingzi', models.CharField(max_length=64, verbose_name='奖品名')),
+ ('tupian_url', models.CharField(blank=True, default='', max_length=500, verbose_name='奖品图')),
+ ('shangpin_id', models.PositiveIntegerField(blank=True, db_index=True, null=True, verbose_name='中奖后履约商品ID')),
+ ('quanzhong', models.PositiveIntegerField(default=1, verbose_name='中奖权重')),
+ ('paixu', models.PositiveSmallIntegerField(default=0, verbose_name='格子序号')),
+ ('shi_xiexie', models.BooleanField(default=False, verbose_name='是否谢谢参与')),
+ ('CreateTime', models.DateTimeField(auto_now_add=True)),
+ ('UpdateTime', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'verbose_name': '转盘奖品',
+ 'verbose_name_plural': '转盘奖品',
+ 'db_table': 'zhuanpan_jiangpin',
+ },
+ ),
+ migrations.CreateModel(
+ name='ZhuanpanJihui',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('club_id', models.CharField(db_index=True, max_length=16, verbose_name='俱乐部')),
+ ('yonghuid', models.CharField(db_index=True, max_length=32, verbose_name='用户UID')),
+ ('huodong_id', models.PositiveIntegerField(db_index=True, verbose_name='活动ID')),
+ ('czjilu_dingdan_id', models.CharField(db_index=True, max_length=64, unique=True, verbose_name='支付流水单号')),
+ ('jine', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='购票金额')),
+ ('zhuangtai', models.PositiveSmallIntegerField(db_index=True, default=9, verbose_name='状态')),
+ ('nicheng', models.CharField(blank=True, default='', max_length=50, verbose_name='游戏昵称')),
+ ('beizhu', models.CharField(blank=True, default='', max_length=200, verbose_name='备注')),
+ ('zhongjiang_jiangpin_id', models.PositiveIntegerField(blank=True, null=True, verbose_name='中奖奖品ID')),
+ ('zhongjiang_shangpin_id', models.PositiveIntegerField(blank=True, null=True, verbose_name='中奖商品ID')),
+ ('dingdan_id', models.CharField(blank=True, db_index=True, default='', max_length=32, verbose_name='中奖后生成的订单号')),
+ ('CreateTime', models.DateTimeField(auto_now_add=True)),
+ ('UpdateTime', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'verbose_name': '转盘抽奖机会',
+ 'verbose_name_plural': '转盘抽奖机会',
+ 'db_table': 'zhuanpan_jihui',
+ },
+ ),
+ migrations.AddIndex(
+ model_name='zhuanpanhuodong',
+ index=models.Index(fields=['club_id', 'qiyong'], name='zhuanpan_hu_club_id_qiyong_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='zhuanpanjiangpin',
+ index=models.Index(fields=['huodong_id', 'paixu'], name='zhuanpan_ji_huodong_paixu_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='zhuanpanjihui',
+ index=models.Index(fields=['club_id', 'yonghuid', 'zhuangtai'], name='zhuanpan_ji_club_yonghu_zt_idx'),
+ ),
+ ]
diff --git a/products/models.py b/products/models.py
index a1111b8..ed6382e 100644
--- a/products/models.py
+++ b/products/models.py
@@ -59,6 +59,10 @@ class Shangpin(QModel):
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
help_text='公共商品按俱乐部隔离;历史数据默认星阙 xq',
)
+ # 转盘购票商品标记:True 时走转盘支付,不走普通点单进抢池;默认 False,xq/xzj 不受影响
+ shi_zhuanpan = models.BooleanField(
+ default=False, db_index=True, verbose_name='是否转盘购票商品',
+ )
class Meta:
db_table = 'shangpin'
@@ -129,7 +133,11 @@ class ShangpinZhuanqu(QModel):
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
help_text='公共专区按俱乐部隔离;历史数据默认星阙 xq',
)
-
+ # UFO 等点单端专区横滑图标(相对 OSS 路径或完整 URL);xq 可不填
+ tupian_url = models.CharField(
+ max_length=500, null=True, blank=True, default='',
+ verbose_name='专区图标URL',
+ )
class Meta:
db_table = 'shangpin_zhuanqu'
@@ -388,4 +396,85 @@ class ShangpinMeiriTongji(QModel):
def save(self, *args, **kwargs):
self.clean()
- super().save(*args, **kwargs)
\ No newline at end of file
+ super().save(*args, **kwargs)
+
+
+# ===========================================================================
+# 转盘单(UFO 等俱乐部可选能力;未配置活动则接口无数据,不影响 xq/xzj)
+# ===========================================================================
+
+class ZhuanpanHuodong(QModel):
+ """转盘活动:一个俱乐部可有多个;购票价格取关联商品 jiage。"""
+ id = models.AutoField(primary_key=True)
+ club_id = models.CharField(max_length=16, db_index=True, verbose_name='俱乐部')
+ mingzi = models.CharField(max_length=64, default='幸运转盘', verbose_name='活动名')
+ shangpin_id = models.PositiveIntegerField(
+ db_index=True, verbose_name='购票商品ID',
+ help_text='对应 Shangpin.id,且该商品建议 shi_zhuanpan=True',
+ )
+ qiyong = models.BooleanField(default=True, db_index=True, verbose_name='是否启用')
+ beizhu = models.CharField(max_length=200, blank=True, default='', verbose_name='备注')
+ CreateTime = models.DateTimeField(auto_now_add=True)
+ UpdateTime = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ db_table = 'zhuanpan_huodong'
+ verbose_name = '转盘活动'
+ verbose_name_plural = verbose_name
+ indexes = [models.Index(fields=['club_id', 'qiyong'])]
+
+
+class ZhuanpanJiangpin(QModel):
+ """转盘奖品格:权重越高越易中;shi_xiexie=True 表示谢谢参与(不生成订单)。"""
+ id = models.AutoField(primary_key=True)
+ huodong_id = models.PositiveIntegerField(db_index=True, verbose_name='活动ID')
+ mingzi = models.CharField(max_length=64, verbose_name='奖品名')
+ tupian_url = models.CharField(max_length=500, blank=True, default='', verbose_name='奖品图')
+ shangpin_id = models.PositiveIntegerField(
+ null=True, blank=True, db_index=True, verbose_name='中奖后履约商品ID',
+ )
+ quanzhong = models.PositiveIntegerField(default=1, verbose_name='中奖权重')
+ paixu = models.PositiveSmallIntegerField(default=0, verbose_name='格子序号')
+ shi_xiexie = models.BooleanField(default=False, verbose_name='是否谢谢参与')
+ CreateTime = models.DateTimeField(auto_now_add=True)
+ UpdateTime = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ db_table = 'zhuanpan_jiangpin'
+ verbose_name = '转盘奖品'
+ verbose_name_plural = verbose_name
+ indexes = [models.Index(fields=['huodong_id', 'paixu'])]
+
+
+class ZhuanpanJihui(QModel):
+ """转盘抽奖机会单:支付成功后待抽,不进抢单池;抽中后再写 Order。
+
+ zhuangtai: 9=待支付 1=已付待抽 2=已抽 3=关闭/作废
+ """
+ id = models.AutoField(primary_key=True)
+ club_id = models.CharField(max_length=16, db_index=True, verbose_name='俱乐部')
+ yonghuid = models.CharField(max_length=32, db_index=True, verbose_name='用户UID')
+ huodong_id = models.PositiveIntegerField(db_index=True, verbose_name='活动ID')
+ czjilu_dingdan_id = models.CharField(
+ max_length=64, unique=True, db_index=True, verbose_name='支付流水单号',
+ )
+ jine = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='购票金额')
+ zhuangtai = models.PositiveSmallIntegerField(default=9, db_index=True, verbose_name='状态')
+ nicheng = models.CharField(max_length=50, blank=True, default='', verbose_name='游戏昵称')
+ beizhu = models.CharField(max_length=200, blank=True, default='', verbose_name='备注')
+ zhongjiang_jiangpin_id = models.PositiveIntegerField(null=True, blank=True, verbose_name='中奖奖品ID')
+ zhongjiang_shangpin_id = models.PositiveIntegerField(null=True, blank=True, verbose_name='中奖商品ID')
+ dingdan_id = models.CharField(
+ max_length=32, blank=True, default='', db_index=True,
+ verbose_name='中奖后生成的订单号',
+ )
+ CreateTime = models.DateTimeField(auto_now_add=True)
+ UpdateTime = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ db_table = 'zhuanpan_jihui'
+ verbose_name = '转盘抽奖机会'
+ verbose_name_plural = verbose_name
+ indexes = [
+ models.Index(fields=['club_id', 'yonghuid', 'zhuangtai']),
+ ]
\ No newline at end of file
diff --git a/products/urls.py b/products/urls.py
index 835e4b5..bff1e57 100644
--- a/products/urls.py
+++ b/products/urls.py
@@ -9,7 +9,9 @@ from .views import ShangpinHuoquView, ShangpinXiangqingHuoquView, DashouHuiyuanL
AdHuiyuanFbView, AdShangpinHuQu, AdLeixingXiuGai, AdLunboXiuGai, AdZhuanquXiuGai, AdGonggaoXiuGai, \
AdShangpinGuizeXiuGai, AdShangpinXiuGai, AdShangpinNeiRongHuoQu, \
HuiyuanTypeListView, HuiyuanRecordListView, ZuzhangFenhongView, CzhqdyView, DsqrgmdhView, \
- AlipayCzjiluNotifyView, CzjiluRepairView
+ AlipayCzjiluNotifyView, CzjiluRepairView, \
+ ZhuanpanGoumai, ZhuanpanLunxun, ZhuanpanShibai, ZhuanpanHuitiao, ZhuanpanJiangpinList, \
+ ZhuanpanChoujiang, ZhuanpanWode, AdZhuanpanHuodongView, AdZhuanpanJiangpinView
urlpatterns = [
#path('shangpinhuoqu/', ShangpinHuoquView.as_view(), name='获取商品数据商品类型和专区'),
@@ -39,6 +41,16 @@ urlpatterns = [
path('alipay-czjilu-notify/', csrf_exempt(AlipayCzjiluNotifyView.as_view()), name='支付宝Czjilu充值统一回调'),
# 客服补单:官方查单确认后幂等履约
path('czjilu-budan', CzjiluRepairView.as_view(), name='充值单客服补单'),
+ # 转盘购票 / 抽奖(独立域,不影响普通点单)
+ path('zhuanpangoumai', ZhuanpanGoumai.as_view(), name='转盘购票'),
+ path('zhuanpanlunxun', ZhuanpanLunxun.as_view(), name='转盘支付轮询'),
+ path('zhuanpanshibai', ZhuanpanShibai.as_view(), name='转盘支付失败'),
+ path('zhuanpanhuitiao', csrf_exempt(ZhuanpanHuitiao.as_view()), name='转盘微信回调'),
+ path('zhuanpanjiangpin', ZhuanpanJiangpinList.as_view(), name='转盘奖品格'),
+ path('zhuanpanchoujiang', ZhuanpanChoujiang.as_view(), name='转盘抽奖'),
+ path('zhuanpanwode', ZhuanpanWode.as_view(), name='我的转盘机会'),
+ path('adzhuanpanhd', AdZhuanpanHuodongView.as_view(), name='后台转盘活动'),
+ path('adzhuanpanjp', AdZhuanpanJiangpinView.as_view(), name='后台转盘奖品'),
#path('sjhuitiao', ShangjiaHuitiao.as_view(), name='商家支付官方回调'), # 商家支付回调
path('sjshibai', ShangjiaShibai.as_view(), name='商家支付失败'), # 商家支付失败处理
path('sjcg', ShangjiaChenggong.as_view(), name='商家支付成功'), # 商家支付成功轮询
diff --git a/products/views/__init__.py b/products/views/__init__.py
index 6f7f297..9e0031d 100644
--- a/products/views/__init__.py
+++ b/products/views/__init__.py
@@ -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,
diff --git a/products/views/alipay_czjilu_notify.py b/products/views/alipay_czjilu_notify.py
index b44d898..f1cdf46 100644
--- a/products/views/alipay_czjilu_notify.py
+++ b/products/views/alipay_czjilu_notify.py
@@ -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}')
diff --git a/products/views/product_query.py b/products/views/product_query.py
index ce34002..6e8654d 100644
--- a/products/views/product_query.py
+++ b/products/views/product_query.py
@@ -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({
diff --git a/products/views/shangpin_admin.py b/products/views/shangpin_admin.py
index 7063cde..46cadac 100644
--- a/products/views/shangpin_admin.py
+++ b/products/views/shangpin_admin.py
@@ -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 '',
}
})
diff --git a/products/views/zhuanpan_payment.py b/products/views/zhuanpan_payment.py
new file mode 100644
index 0000000..7fafd62
--- /dev/null
+++ b/products/views/zhuanpan_payment.py
@@ -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 = '' + ''.join(f'<{k}>{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.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'})
diff --git a/shop/views/shop_base_views.py b/shop/views/shop_base_views.py
index 602ad6f..d3e97f3 100644
--- a/shop/views/shop_base_views.py
+++ b/shop/views/shop_base_views.py
@@ -202,7 +202,8 @@ class ShopGoodsView(APIView):
'id': zq.id,
'mingzi': zq.mingzi,
'leixing_id': zq.dianpu_leixing_id, # 对应店铺类型ID
- 'paixu': zq.paixu
+ 'paixu': zq.paixu,
+ 'tupian_url': getattr(zq, 'tupian_url', None) or '',
})
# 3. 商品:根据店铺、店铺类型、专区,未封禁、已上架、平台审核通过、原有审核状态为正常
@@ -296,7 +297,8 @@ class ShopGoodsView(APIView):
'id': zq.id,
'mingzi': zq.mingzi,
'leixing_id': zq.leixing_id,
- 'paixu': zq.paixu
+ 'paixu': zq.paixu,
+ 'tupian_url': getattr(zq, 'tupian_url', None) or '',
})
# 3. 商品:当前俱乐部 + 公共
@@ -311,11 +313,12 @@ class ShopGoodsView(APIView):
club_id=club_id,
).values(
'id', 'biaoqian', 'jiage', 'tupian_url', 'duiwai_xiaoliang', 'paixu',
- 'leixing_id', 'zhuanqu_id'
+ 'leixing_id', 'zhuanqu_id', 'shi_zhuanpan'
).order_by('-paixu', 'id')
shangpin_data = list(shangpin_queryset)
for sp in shangpin_data:
sp['xiaoliang'] = sp.get('duiwai_xiaoliang') or 0
+ sp['shi_zhuanpan'] = bool(sp.get('shi_zhuanpan'))
else:
shangpin_data = []