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}>' 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 = []