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

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

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

View File

@@ -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此处只写死回调地址

View File

@@ -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、介绍、价格

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,6 +59,10 @@ class Shangpin(QModel):
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
help_text='公共商品按俱乐部隔离;历史数据默认星阙 xq',
)
# 转盘购票商品标记True 时走转盘支付,不走普通点单进抢池;默认 Falsexq/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 路径或完整 URLxq 可不填
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)
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']),
]

View File

@@ -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='商家支付成功'), # 商家支付成功轮询

View File

@@ -57,6 +57,17 @@ from .chongzhi_payment import (
IDENTITY_CONFIG,
)
from .alipay_czjilu_notify import AlipayCzjiluNotifyView, CzjiluRepairView
from .zhuanpan_payment import (
ZhuanpanGoumai,
ZhuanpanLunxun,
ZhuanpanShibai,
ZhuanpanHuitiao,
ZhuanpanJiangpinList,
ZhuanpanChoujiang,
ZhuanpanWode,
AdZhuanpanHuodongView,
AdZhuanpanJiangpinView,
)
from .shangpin_admin import (
ShangpinLeixingFabuView,
ShangpinZhuanquFabuView,

View File

@@ -17,6 +17,7 @@ from products.models import Czjilu
from products.czjilu_types import (
CZJILU_LEIXING_HUIYUAN, CZJILU_LEIXING_YAJIN, CZJILU_LEIXING_JIFEN,
CZJILU_LEIXING_SHANGJIA, CZJILU_LEIXING_KAOHE, CZJILU_LEIXING_FAKUAN,
CZJILU_LEIXING_ZHUANPAN,
CZJILU_PAID_STATUS, is_czjilu_fulfillable_status,
)
@@ -43,6 +44,9 @@ def _fulfill_czjilu_by_leixing(out_trade_no, leixing, paid_amount, source='callb
if leixing == CZJILU_LEIXING_FAKUAN:
from jituan.services.czjilu_fulfill import fulfill_fakuan
return fulfill_fakuan(out_trade_no, paid_amount_yuan=paid_amount)
if leixing == CZJILU_LEIXING_ZHUANPAN:
from jituan.services.czjilu_fulfill import fulfill_zhuanpan
return fulfill_zhuanpan(out_trade_no, paid_amount_yuan=paid_amount)
raise ValueError(f'未知 leixing={leixing}')

View File

@@ -212,14 +212,20 @@ class ShangpinXiangqingHuoquView(APIView):
# 直接构建数据,不处理空值
data = {
"id": shangpin.id,
"tupian": shangpin.tupian_url,
"tupian_url": shangpin.tupian_url,
"biaoti": shangpin.biaoqian,
"biaoqian": shangpin.biaoqian,
"jiage": shangpin.jiage,
"xiangqing": shangpin.jieshao,
"xiadanxuzhi": shangpin.xiadan_xuzhi,
"guize": shangpin.guize_tupian,
"xiaoliang": shangpin.duiwai_xiaoliang,
"kucun": shangpin.kucun,
"leixing_id": shangpin.leixing_id,
"zhuanqu_id": shangpin.zhuanqu_id,
"shi_zhuanpan": bool(getattr(shangpin, 'shi_zhuanpan', False)),
}
return Response({

View File

@@ -325,6 +325,7 @@ class ShangpinZhuanquFabuView(APIView):
# === 2. 接收并验证字段 ===
mingzi = request.data.get('mingzi', '').strip()
leixing_id = request.data.get('leixing_id')
tupian_url = (request.data.get('tupian_url') or '').strip()
# 2.1 验证必填字段
if not mingzi:
@@ -362,6 +363,7 @@ class ShangpinZhuanquFabuView(APIView):
mingzi=mingzi,
leixing_id=leixing_id,
club_id=club_id,
tupian_url=tupian_url,
)
zhuanqu_obj.save()
@@ -370,7 +372,8 @@ class ShangpinZhuanquFabuView(APIView):
response_data = {
'id': zhuanqu_obj.id,
'mingzi': mingzi,
'leixing_id': leixing_id
'leixing_id': leixing_id,
'tupian_url': tupian_url,
}
# 返回成功响应
@@ -1547,9 +1550,10 @@ class AdZhuanquXiuGai(APIView):
status=status.HTTP_401_UNAUTHORIZED
)
# 2. 获取专区ID和新的专区名称
# 2. 获取专区ID和新的专区名称(可选图标)
zhuanqu_id = request.data.get('zhuanqu_id')
mingzi = request.data.get('mingzi', '').strip()
tupian_url = request.data.get('tupian_url', None)
if not zhuanqu_id:
return Response(
@@ -1578,8 +1582,10 @@ class AdZhuanquXiuGai(APIView):
status=status.HTTP_400_BAD_REQUEST
)
# 4. 更新专区名称
# 4. 更新专区名称(及可选图标;未传 tupian_url 则不改图)
zhuanqu_obj.mingzi = mingzi
if tupian_url is not None:
zhuanqu_obj.tupian_url = (tupian_url or '').strip()
zhuanqu_obj.save()
# 5. 返回成功响应
@@ -1589,7 +1595,8 @@ class AdZhuanquXiuGai(APIView):
'data': {
'id': zhuanqu_obj.id,
'mingzi': zhuanqu_obj.mingzi,
'leixing_id': zhuanqu_obj.leixing_id
'leixing_id': zhuanqu_obj.leixing_id,
'tupian_url': getattr(zhuanqu_obj, 'tupian_url', '') or '',
}
})

View File

@@ -0,0 +1,632 @@
"""转盘购票 / 抽奖 / 后台配置(独立业务域,不影响普通点单与抢单池)。
路径前缀:/shangpin/
- zhuanpangoumai 购票下单(写 Czjilu leixing=7 + ZhuanpanJihui
- zhuanpanlunxun 支付复查
- zhuanpanhuitiao 微信回调
- zhuanpanshibai 关单
- zhuanpanjiangpin 奖品格
- zhuanpanchoujiang 抽奖(后端控奖)
- zhuanpanwode 我的抽奖机会
- adzhuanpan* 后台活动/奖品配置
"""
from __future__ import annotations
import hashlib
import logging
import random
import string
import time
import traceback
from decimal import Decimal
import defusedxml.ElementTree as ET
from django.conf import settings
from django.db import transaction
from django.http import HttpResponse
from django.views import View
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from jituan.services.club_context import resolve_effective_club_id
from jituan.services.club_user import get_payment_openid
from jituan.services.club_write import resolve_club_id_for_write
from products.czjilu_types import (
CZJILU_LEIXING_ZHUANPAN,
ZHUANPAN_ORDER_PREFIX,
close_unpaid_czjilu,
)
from products.models import (
Czjilu, Shangpin, ZhuanpanHuodong, ZhuanpanJiangpin, ZhuanpanJihui,
)
from utils.money import yuan_to_fen
logger = logging.getLogger(__name__)
def _gen_czzp_id():
timestamp_ms = int(time.time() * 1000)
timestamp_ns = int(time.time_ns() // 1000) % 1000000
random_num = random.randint(0, 99)
return f"{ZHUANPAN_ORDER_PREFIX}{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
def _pick_jiangpin(jiangpin_list):
"""按权重抽奖,返回选中的奖品对象。"""
total = sum(max(int(j.quanzhong or 0), 0) for j in jiangpin_list)
if total <= 0:
return jiangpin_list[0]
r = random.randint(1, total)
acc = 0
for j in jiangpin_list:
acc += max(int(j.quanzhong or 0), 0)
if r <= acc:
return j
return jiangpin_list[-1]
def _create_pool_order_from_prize(jihui, jiangpin, request_user):
"""中奖后写普通 Order Status=1 进抢池;谢谢参与则不创建。"""
if jiangpin.shi_xiexie or not jiangpin.shangpin_id:
return ''
from orders.models import Order, PlatformOrderExt
from jituan.services.club_config import get_commission_rate_object
from shop.utils import calculate_pingtai_and_dianpu_shouyi
shangpin = Shangpin.query.filter(id=jiangpin.shangpin_id).first()
if not shangpin:
raise ValueError('中奖商品不存在')
club_id = jihui.club_id
shangpin_jiage = Decimal(str(shangpin.jiage or 0))
if shangpin.kaioi_ewai_dashou_fencheng and shangpin.ewai_dashou_fencheng is not None:
dashou_fencheng = Decimal(str(shangpin.ewai_dashou_fencheng))
else:
dashou_lilu = get_commission_rate_object(club_id, '1')
rate = Decimal(str(getattr(dashou_lilu, 'Rate', 0) or 0))
dashou_fencheng = shangpin_jiage * rate
timestamp = int(time.time())
timestamp_ns = int(time.time_ns() // 1000) % 1000000
random_num = random.randint(0, 99)
dingdanid = f"PT{timestamp:011d}{timestamp_ns:06d}{random_num:02d}"
laoban_yonghuid = getattr(request_user, 'UserUID', '') or jihui.yonghuid
dingdan = Order.query.create(
OrderID=dingdanid,
Status=1, # 已付待抢
Amount=shangpin_jiage,
Platform=1,
PlayerCommission=dashou_fencheng,
AssignedID=None,
ProductID=shangpin.id,
GrabRequirement=getattr(shangpin, 'yaoqiuleixing', None),
MembershipID=getattr(shangpin, 'huiyuan_id', '') or '',
CommissionReq=getattr(shangpin, 'yongjin', 0) or 0,
Description=getattr(shangpin, 'jieshao', '') or '',
Remark=(jihui.beizhu or '')[:200],
Nickname=jihui.nicheng or '',
ImageURL=getattr(shangpin, 'tupian_url', None) or '',
User1ID=f"Boss{laoban_yonghuid}",
ProductTypeID=shangpin.leixing_id,
ClubID=club_id,
WechatTransactionID=jihui.czjilu_dingdan_id,
)
dianpu_id, pingtai_shouyi, dianpu_shouyi, shangji_dianpu_id, shangji_fenhong = \
calculate_pingtai_and_dianpu_shouyi(
shangpin_id=shangpin.id,
dingdan_jiage=shangpin_jiage,
dashou_fencheng=dashou_fencheng,
)
pingtaikuozhan_data = {
'Order': dingdan,
'BossID': laoban_yonghuid,
'PlatformIncome': pingtai_shouyi,
'ParentShopID': shangji_dianpu_id,
'ParentBonus': shangji_fenhong if shangji_fenhong else Decimal('0.00'),
'ShopIncome': dianpu_shouyi if dianpu_id else Decimal('0.00'),
}
if dianpu_id:
pingtaikuozhan_data['ShopID'] = dianpu_id
PlatformOrderExt.query.create(**pingtaikuozhan_data)
try:
from orders.notice_tasks import dingdan_guangbo
dingdan_guangbo.delay({
'OrderID': dingdanid,
'game_type': '',
'amount': str(shangpin_jiage),
'order_desc': (getattr(shangpin, 'jieshao', None) or getattr(shangpin, 'biaoqian', '') or '')[:50],
})
except Exception as exc:
logger.warning('转盘中奖广播失败 dingdan=%s: %s', dingdanid, exc)
return dingdanid
class ZhuanpanGoumai(APIView):
"""POST /shangpin/zhuanpangoumai 转盘购票。"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
huodong_id = request.data.get('huodong_id')
shangpin_id = request.data.get('shangpin_id')
nicheng = (request.data.get('nicheng') or '').strip()
beizhu = (request.data.get('Remark') or request.data.get('beizhu') or '').strip()
pay_method = (request.data.get('pay_method') or 'wechat').strip().lower()
if not nicheng:
return Response({'code': 400, 'message': '请填写游戏昵称'}, status=400)
club_id = resolve_club_id_for_write(request, request.user)
huodong = None
if huodong_id:
huodong = ZhuanpanHuodong.query.filter(
id=int(huodong_id), club_id=club_id, qiyong=True
).first()
elif shangpin_id:
huodong = ZhuanpanHuodong.query.filter(
shangpin_id=int(shangpin_id), club_id=club_id, qiyong=True
).first()
if not huodong:
return Response({'code': 404, 'message': '转盘活动不存在或未启用'}, status=404)
ticket = Shangpin.query.filter(id=huodong.shangpin_id, club_id=club_id).first()
if not ticket:
return Response({'code': 404, 'message': '购票商品不存在'}, status=404)
jine = Decimal(str(ticket.jiage or 0))
if jine <= 0:
return Response({'code': 400, 'message': '购票价格异常'}, status=400)
prizes = list(ZhuanpanJiangpin.query.filter(huodong_id=huodong.id).order_by('paixu', 'id'))
if not prizes:
return Response({'code': 400, 'message': '活动未配置奖品'}, status=400)
dingdanid = _gen_czzp_id()
with transaction.atomic():
Czjilu.query.create(
dingdan_id=dingdanid,
zhuangtai=9,
yonghuid=request.user.UserUID,
jine=jine,
leixing=CZJILU_LEIXING_ZHUANPAN,
shuoming=getattr(settings, 'ZHUANPAN_PAY_DESCRIPTION', '转盘购票'),
club_id=club_id,
)
ZhuanpanJihui.query.create(
club_id=club_id,
yonghuid=request.user.UserUID,
huodong_id=huodong.id,
czjilu_dingdan_id=dingdanid,
jine=jine,
zhuangtai=9,
nicheng=nicheng[:50],
beizhu=beizhu[:200],
)
if pay_method == 'alipay':
from jituan.services.alipay_pay import generate_alipay_czjilu_pay_params
pay_params = generate_alipay_czjilu_pay_params(
dingdanid=dingdanid,
jine=jine,
subject=getattr(settings, 'ZHUANPAN_PAY_DESCRIPTION', '转盘购票'),
club_id=club_id,
)
else:
pay_openid, _ = get_payment_openid(request, club_id=club_id)
if not pay_openid:
Czjilu.query.filter(dingdan_id=dingdanid).delete()
ZhuanpanJihui.query.filter(czjilu_dingdan_id=dingdanid).delete()
return Response({
'code': 400,
'message': '当前小程序未绑定微信openid请重新登录',
}, status=400)
pay_params = self._wechat_pay_params(request, dingdanid, jine, pay_openid, club_id)
return Response({
'code': 200,
'message': 'ok',
'payParams': pay_params,
'dingdanid': dingdanid,
'jihui_id': ZhuanpanJihui.query.filter(czjilu_dingdan_id=dingdanid).first().id,
})
except Exception as e:
logger.error('转盘购票异常: %s', e, exc_info=True)
return Response({'code': 500, 'message': f'购票失败: {e}'}, status=500)
def _wechat_pay_params(self, request, dingdanid, jine, openid, club_id):
from jituan.services.wechat_pay import get_wechat_v2_config
from jituan.constants import FUBEI_BIZ_CZJILU
from jituan.services.mini_pay_router import maybe_create_fubei_jsapi_params
PAY_DESCRIPTION = getattr(settings, 'ZHUANPAN_PAY_DESCRIPTION', '转盘购票')
NOTIFY_URL = getattr(settings, 'ZHUANPAN_PAY_NOTIFY_URL', '')
fb_params = maybe_create_fubei_jsapi_params(
club_id=club_id,
out_trade_no=dingdanid,
amount_yuan=jine,
body=PAY_DESCRIPTION,
openid=openid,
notify_url=NOTIFY_URL,
biz_type=FUBEI_BIZ_CZJILU,
)
if fb_params is not None:
return fb_params
pay_cfg = get_wechat_v2_config(club_id)
APPID = pay_cfg['appid']
MCHID = pay_cfg['mch_id']
KEY = pay_cfg['key']
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
total_fee = yuan_to_fen(jine)
import requests as http_requests
params = {
'appid': APPID,
'mch_id': MCHID,
'nonce_str': nonce_str,
'body': PAY_DESCRIPTION,
'out_trade_no': dingdanid,
'total_fee': str(total_fee),
'spbill_create_ip': request.META.get('REMOTE_ADDR', '127.0.0.1'),
'notify_url': NOTIFY_URL,
'trade_type': 'JSAPI',
'openid': openid,
}
sign_str = '&'.join(f'{k}={params[k]}' for k in sorted(params) if params[k]) + f'&key={KEY}'
params['sign'] = hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
xml_body = '<xml>' + ''.join(f'<{k}><![CDATA[{v}]]></{k}>' for k, v in params.items()) + '</xml>'
resp = http_requests.post('https://api.mch.weixin.qq.com/pay/unifiedorder', data=xml_body.encode('utf-8'), timeout=15)
root = ET.fromstring(resp.content)
if root.findtext('return_code') != 'SUCCESS' or root.findtext('result_code') != 'SUCCESS':
raise Exception(root.findtext('err_code_des') or root.findtext('return_msg') or '统一下单失败')
prepay_id = root.findtext('prepay_id')
ts = str(int(time.time()))
pay_nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
pkg = f'prepay_id={prepay_id}'
pay_sign_src = f'appId={APPID}&nonceStr={pay_nonce}&package={pkg}&signType=MD5&timeStamp={ts}&key={KEY}'
return {
'timeStamp': ts,
'nonceStr': pay_nonce,
'package': pkg,
'signType': 'MD5',
'paySign': hashlib.md5(pay_sign_src.encode('utf-8')).hexdigest().upper(),
}
class ZhuanpanLunxun(APIView):
"""POST /shangpin/zhuanpanlunxun"""
permission_classes = [IsAuthenticated]
def post(self, request):
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({'code': 400, 'message': '缺少 dingdanid'}, status=400)
try:
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
result = confirm_czjilu_paid(
dingdanid, request.user.UserUID, expected_leixing=CZJILU_LEIXING_ZHUANPAN
)
jihui = ZhuanpanJihui.query.filter(czjilu_dingdan_id=dingdanid).first()
return Response({
'code': 200,
'message': 'ok',
'data': {
**result,
'jihui_id': jihui.id if jihui else None,
'jihui_zhuangtai': jihui.zhuangtai if jihui else None,
},
})
except Exception as e:
from jituan.services.czjilu_fulfill import CzjiluFulfillError
if isinstance(e, CzjiluFulfillError):
return Response({'code': 400, 'message': str(e)}, status=400)
logger.error('转盘轮询异常: %s', e, exc_info=True)
return Response({'code': 500, 'message': str(e)}, status=500)
class ZhuanpanShibai(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({'code': 400, 'message': '缺少 dingdanid'}, status=400)
order = Czjilu.query.filter(
dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=CZJILU_LEIXING_ZHUANPAN
).first()
if not order:
return Response({'code': 404, 'message': '订单不存在'}, status=404)
close_unpaid_czjilu(order, reason='用户取消转盘支付')
jihui = ZhuanpanJihui.query.filter(czjilu_dingdan_id=dingdanid).first()
if jihui and int(jihui.zhuangtai) == 9:
jihui.zhuangtai = 3
jihui.save(update_fields=['zhuangtai', 'UpdateTime'])
return Response({'code': 200, 'message': 'ok'})
class ZhuanpanHuitiao(View):
"""微信转盘购票回调 /shangpin/zhuanpanhuitiao"""
def post(self, request):
try:
xml_data = request.body
root = ET.fromstring(xml_data)
if root.findtext('return_code') != 'SUCCESS':
return self._wx('FAIL', 'return_code')
out_trade_no = root.findtext('out_trade_no') or ''
total_fee = root.findtext('total_fee')
paid = None
if total_fee:
paid = Decimal(total_fee) / Decimal('100')
from jituan.services.czjilu_fulfill import fulfill_zhuanpan
fulfill_zhuanpan(out_trade_no, paid_amount_yuan=paid)
return self._wx('SUCCESS', 'OK')
except Exception as e:
logger.error('转盘微信回调异常: %s', e, exc_info=True)
return self._wx('FAIL', 'error')
def _wx(self, code, msg):
return HttpResponse(
f'<xml><return_code><![CDATA[{code}]]></return_code>'
f'<return_msg><![CDATA[{msg}]]></return_msg></xml>',
content_type='application/xml',
)
class ZhuanpanJiangpinList(APIView):
"""POST /shangpin/zhuanpanjiangpin 拉奖品格(可匿名,按 club"""
permission_classes = [AllowAny]
def post(self, request):
club_id = resolve_effective_club_id(request)
huodong_id = request.data.get('huodong_id')
shangpin_id = request.data.get('shangpin_id')
q = ZhuanpanHuodong.query.filter(club_id=club_id, qiyong=True)
if huodong_id:
q = q.filter(id=int(huodong_id))
elif shangpin_id:
q = q.filter(shangpin_id=int(shangpin_id))
huodong = q.first()
if not huodong:
return Response({'code': 404, 'message': '无可用转盘活动', 'data': None}, status=404)
prizes = list(
ZhuanpanJiangpin.query.filter(huodong_id=huodong.id).order_by('paixu', 'id')
)
return Response({
'code': 200,
'message': 'ok',
'data': {
'huodong': {
'id': huodong.id,
'mingzi': huodong.mingzi,
'shangpin_id': huodong.shangpin_id,
},
'jiangpin': [
{
'id': p.id,
'mingzi': p.mingzi,
'tupian_url': p.tupian_url,
'paixu': p.paixu,
'shi_xiexie': p.shi_xiexie,
'shangpin_id': p.shangpin_id,
}
for p in prizes
],
},
})
class ZhuanpanChoujiang(APIView):
"""POST /shangpin/zhuanpanchoujiang 执行抽奖(后端控奖)。"""
permission_classes = [IsAuthenticated]
def post(self, request):
jihui_id = request.data.get('jihui_id')
if not jihui_id:
return Response({'code': 400, 'message': '缺少 jihui_id'}, status=400)
try:
with transaction.atomic():
jihui = ZhuanpanJihui.query.select_for_update().filter(
id=int(jihui_id), yonghuid=request.user.UserUID
).first()
if not jihui:
return Response({'code': 404, 'message': '抽奖机会不存在'}, status=404)
if int(jihui.zhuangtai) != 1:
return Response({
'code': 400,
'message': '机会未支付或已抽过',
'data': {'zhuangtai': jihui.zhuangtai},
}, status=400)
prizes = list(
ZhuanpanJiangpin.query.filter(huodong_id=jihui.huodong_id).order_by('paixu', 'id')
)
if not prizes:
return Response({'code': 400, 'message': '奖品未配置'}, status=400)
win = _pick_jiangpin(prizes)
dingdan_id = ''
if not win.shi_xiexie and win.shangpin_id:
dingdan_id = _create_pool_order_from_prize(jihui, win, request.user)
jihui.zhuangtai = 2
jihui.zhongjiang_jiangpin_id = win.id
jihui.zhongjiang_shangpin_id = win.shangpin_id
jihui.dingdan_id = dingdan_id or ''
jihui.save(update_fields=[
'zhuangtai', 'zhongjiang_jiangpin_id', 'zhongjiang_shangpin_id',
'dingdan_id', 'UpdateTime',
])
return Response({
'code': 200,
'message': 'ok',
'data': {
'jihui_id': jihui.id,
'jiangpin_id': win.id,
'mingzi': win.mingzi,
'tupian_url': win.tupian_url,
'paixu': win.paixu,
'shi_xiexie': win.shi_xiexie,
'dingdan_id': dingdan_id,
},
})
except Exception as e:
logger.error('抽奖异常: %s\n%s', e, traceback.format_exc())
return Response({'code': 500, 'message': str(e)}, status=500)
class ZhuanpanWode(APIView):
"""POST /shangpin/zhuanpanwode"""
permission_classes = [IsAuthenticated]
def post(self, request):
club_id = resolve_effective_club_id(request)
qs = ZhuanpanJihui.query.filter(
yonghuid=request.user.UserUID, club_id=club_id
).exclude(zhuangtai=9).order_by('-id')[:50]
return Response({
'code': 200,
'message': 'ok',
'data': [
{
'id': j.id,
'huodong_id': j.huodong_id,
'zhuangtai': j.zhuangtai,
'jine': str(j.jine),
'nicheng': j.nicheng,
'zhongjiang_jiangpin_id': j.zhongjiang_jiangpin_id,
'dingdan_id': j.dingdan_id,
'czjilu_dingdan_id': j.czjilu_dingdan_id,
'CreateTime': j.CreateTime.isoformat() if j.CreateTime else '',
}
for j in qs
],
})
class AdZhuanpanHuodongView(APIView):
"""后台:转盘活动 list/save。POST action=list|save|toggle"""
permission_classes = [IsAuthenticated]
def post(self, request):
action = (request.data.get('action') or 'list').strip()
club_id = resolve_effective_club_id(request)
if action == 'list':
rows = list(ZhuanpanHuodong.query.filter(club_id=club_id).order_by('-id'))
return Response({
'code': 0,
'data': [
{
'id': h.id,
'mingzi': h.mingzi,
'shangpin_id': h.shangpin_id,
'qiyong': h.qiyong,
'beizhu': h.beizhu,
}
for h in rows
],
})
if action == 'save':
hid = request.data.get('id')
mingzi = (request.data.get('mingzi') or '幸运转盘').strip()
shangpin_id = int(request.data.get('shangpin_id') or 0)
beizhu = (request.data.get('beizhu') or '').strip()
qiyong = bool(request.data.get('qiyong', True))
if not shangpin_id:
return Response({'code': 1, 'msg': '需要 shangpin_id'})
# 标记购票商品
Shangpin.query.filter(id=shangpin_id, club_id=club_id).update(shi_zhuanpan=True)
if hid:
h = ZhuanpanHuodong.query.filter(id=int(hid), club_id=club_id).first()
if not h:
return Response({'code': 1, 'msg': '活动不存在'})
h.mingzi = mingzi
h.shangpin_id = shangpin_id
h.beizhu = beizhu
h.qiyong = qiyong
h.save()
else:
h = ZhuanpanHuodong.query.create(
club_id=club_id, mingzi=mingzi, shangpin_id=shangpin_id,
beizhu=beizhu, qiyong=qiyong,
)
return Response({'code': 0, 'data': {'id': h.id}})
if action == 'toggle':
hid = int(request.data.get('id') or 0)
h = ZhuanpanHuodong.query.filter(id=hid, club_id=club_id).first()
if not h:
return Response({'code': 1, 'msg': '不存在'})
h.qiyong = not h.qiyong
h.save(update_fields=['qiyong', 'UpdateTime'])
return Response({'code': 0, 'data': {'qiyong': h.qiyong}})
return Response({'code': 1, 'msg': '未知 action'})
class AdZhuanpanJiangpinView(APIView):
"""后台:奖品 list/save/delete。"""
permission_classes = [IsAuthenticated]
def post(self, request):
action = (request.data.get('action') or 'list').strip()
club_id = resolve_effective_club_id(request)
huodong_id = int(request.data.get('huodong_id') or 0)
huodong = ZhuanpanHuodong.query.filter(id=huodong_id, club_id=club_id).first()
if not huodong:
return Response({'code': 1, 'msg': '活动不存在或不属于当前俱乐部'})
if action == 'list':
rows = list(ZhuanpanJiangpin.query.filter(huodong_id=huodong_id).order_by('paixu', 'id'))
return Response({
'code': 0,
'data': [
{
'id': p.id,
'mingzi': p.mingzi,
'tupian_url': p.tupian_url,
'shangpin_id': p.shangpin_id,
'quanzhong': p.quanzhong,
'paixu': p.paixu,
'shi_xiexie': p.shi_xiexie,
}
for p in rows
],
})
if action == 'save':
pid = request.data.get('id')
data = dict(
mingzi=(request.data.get('mingzi') or '').strip() or '奖品',
tupian_url=(request.data.get('tupian_url') or '').strip(),
shangpin_id=request.data.get('shangpin_id') or None,
quanzhong=int(request.data.get('quanzhong') or 1),
paixu=int(request.data.get('paixu') or 0),
shi_xiexie=bool(request.data.get('shi_xiexie', False)),
)
if data['shangpin_id'] in ('', None):
data['shangpin_id'] = None
else:
data['shangpin_id'] = int(data['shangpin_id'])
if pid:
p = ZhuanpanJiangpin.query.filter(id=int(pid), huodong_id=huodong_id).first()
if not p:
return Response({'code': 1, 'msg': '奖品不存在'})
for k, v in data.items():
setattr(p, k, v)
p.save()
else:
p = ZhuanpanJiangpin.query.create(huodong_id=huodong_id, **data)
return Response({'code': 0, 'data': {'id': p.id}})
if action == 'delete':
pid = int(request.data.get('id') or 0)
ZhuanpanJiangpin.query.filter(id=pid, huodong_id=huodong_id).delete()
return Response({'code': 0})
return Response({'code': 1, 'msg': '未知 action'})

View File

@@ -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 = []