feat: UFO 转盘购票与专区图标(不影响现有俱乐部点单)
新增转盘活动/奖品/机会表与 Czjilu leixing=7 履约;专区 tupian_url;hqsp/详情下发 shi_zhuanpan。普通点单与抢单主链路保持不变。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,此处只写死回调地址
|
||||
|
||||
@@ -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、介绍、价格)
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
102
products/migrations/0013_zhuanpan_and_zhuanqu_icon.py
Normal file
102
products/migrations/0013_zhuanpan_and_zhuanqu_icon.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
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']),
|
||||
]
|
||||
@@ -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='商家支付成功'), # 商家支付成功轮询
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}')
|
||||
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 '',
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
632
products/views/zhuanpan_payment.py
Normal file
632
products/views/zhuanpan_payment.py
Normal 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'})
|
||||
@@ -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 = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user