From 21bb129c5d8ccb0d8a7ec06c6544a9e93855a293 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sun, 14 Jun 2026 16:39:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=86=E6=96=87=E4=BB=B6=E5=A4=B9=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E4=BB=8E=E6=8B=BC=E9=9F=B3=E6=94=B9=E4=B8=BA=E8=8B=B1?= =?UTF-8?q?=E6=96=87=EF=BC=8C=E6=95=B4=E7=90=86=E4=BA=86=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=EF=BC=8C=E5=B0=86=E5=AF=BC=E5=85=A5=E8=AF=AD?= =?UTF-8?q?=E5=8F=A5=E8=BF=81=E7=A7=BB=E5=88=B0=E4=BA=86=E5=A4=B4=E9=83=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- a_long_dianjing/celery.py | 31 +- a_long_dianjing/celery_app.py | 20 +- a_long_dianjing/settings.py | 658 +- {houtai => backend}/_init_.py | 0 {houtai => backend}/apps.py | 2 +- .../migrations/0001_initial.py | 0 ...zhangritongji_shangjiaritongji_and_more.py | 0 .../migrations/0003_tixianritongji.py | 0 {dengji => backend/migrations}/__init__.py | 0 {houtai => backend}/migrations/_init_.py | 0 {houtai => backend}/models.py | 0 {houtai => backend}/serializers.py | 2 +- {houtai => backend}/urls.py | 0 {houtai => backend}/utils.py | 6 +- {houtai => backend}/view.py | 24571 +++++++------- {dengji/migrations => config}/__init__.py | 0 {dengji => config}/admin.py | 0 {peizhi => config}/apps.py | 0 {peizhi => config}/migrations/0001_initial.py | 0 .../0002_shangjiamoban_shangjialianjie.py | 0 .../0003_tupianpeizhi_fangwen_cishu.py | 0 .../migrations/0004_tixianquotadefault.py | 0 ...ordertypemapping_dailydispatchstat_club.py | 0 .../0006_clubconfig_accountpermission.py | 0 ...ilydispatchstat_claimed_amount_and_more.py | 0 .../0008_clubconfig_storage_bucket_name.py | 0 .../0009_dailypayoutstat_dailyincomestat.py | 0 ...ge_popupimage_popupconfig_page_and_more.py | 0 .../migrations/0011_withdrawconfig.py | 0 ...0012_shangjiamoban_chenghao_id_and_more.py | 0 ...jialianjie_previous_dingdan_id_and_more.py | 0 {dingdan => config/migrations}/__init__.py | 0 {peizhi => config}/models.py | 17 +- {peizhi => config}/serializers.py | 0 {peizhi => config}/tasks.py | 3 +- {dengji => config}/tests.py | 0 {peizhi => config}/urls.py | 2 +- {peizhi => config}/views.py | 8937 ++---- dingdan/signals.py | 614 - gunicorn.conf.py | 26 +- {dingdan/migrations => orders}/__init__.py | 0 {dingdan => orders}/admin.py | 0 {dingdan => orders}/apps.py | 2 +- {dingdan => orders}/cross_platform_tasks.py | 0 .../migrations/0001_initial.py | 0 ...dan_auto_expire_at_dingdan_auto_task_id.py | 0 ..._pending_dispatch_dingdan_status_8_time.py | 0 .../0004_alter_tuikuanjilu_chuliid.py | 0 .../0005_alter_chufajilu_chuliid.py | 0 .../migrations/0006_chufatupian.py | 0 .../migrations/0007_chufajilu_ssliyou.py | 0 ...ngdan_is_cross_dingdan_partner_order_id.py | 0 ...ingtai_clkf_dingdanpingtai_jjjs_biaoshi.py | 0 ...remove_dingdanpingtai_clkf_dingdan_clkf.py | 0 .../migrations/0011_dingdan_tkly.py | 0 ..._dingdan_wechat_transaction_id_and_more.py | 0 .../0013_alter_chufajilu_qingqiuid.py | 0 ...n_dispatch_type_dingdan_partner_club_id.py | 0 ...esettlement_orderdashouhistory_and_more.py | 0 ...alter_presettlement_dingdan_id_and_more.py | 0 .../0017_dingdanpingtai_dianpu_id_and_more.py | 0 .../0018_dingdanshangjia_partner_yonghu_id.py | 0 ...ngdanpingtai_shangji_dianpu_id_and_more.py | 0 .../migrations/0020_fadanfenhong_fadan.py | 0 .../migrations/0021_fadanshensutupian.py | 0 ...rtner_club_id_fadan_shenqingren_shenfen.py | 0 .../0023_fadanshensutupian_yongtu.py | 0 ...4_fadan_chulizhe_fadan_chulizhe_shenfen.py | 0 .../0025_alter_fadan_shenqingren_shenfen.py | 0 ...lter_fadan_shenqingren_shenfen_and_more.py | 0 .../migrations/0027_dingdanpeihu.py | 0 ...28_dingdanshangjia_shangyige_dingdan_id.py | 0 .../0029_fadanfenhong_is_individual_rate.py | 0 .../0030_fadan_shenqingren_fenhong_jine.py | 0 .../0031_dingdan_guanshi_fencheng.py | 0 {houtai => orders}/migrations/__init__.py | 0 {dingdan => orders}/models.py | 1 + .../notice_tasks.py | 140 +- orders/signals.py | 102 + {dingdan => orders}/tasks.py | 16 +- {dingdan => orders}/tests.py | 0 {dingdan => orders}/urls.py | 4 - {dingdan => orders}/utils.py | 47 +- {dingdan => orders}/views.py | 744 +- {peizhi => products}/__init__.py | 0 {peizhi => products}/admin.py | 0 {shangpin => products}/apps.py | 0 .../migrations/0001_initial.py | 0 ..._huiyuangoumai_unique_together_and_more.py | 0 ...in_paixu_shangpinleixing_paixu_and_more.py | 0 .../0004_shangpin_shenhezhuangtai_and_more.py | 0 .../0005_alter_gsfenhong_dashouid.py | 0 ...iyuan_id_huiyuan_zuzhangfc_duocifenhong.py | 0 ...duoci_fenho_guanshi_b3de53_idx_and_more.py | 0 ...ng_zuzhang_fenhong_gsfenhong_zuzhang_id.py | 0 ..._shangpin_ewai_dashou_fencheng_and_more.py | 0 .../0010_shangpinleixing_is_cross_enabled.py | 0 ...dianpu_shangpin_dianpu_leixing_and_more.py | 0 .../migrations/0012_shangpinmeiritongji.py | 0 .../0013_shangpinleixing_bankuai.py | 0 .../migrations/0014_huiyuan_bankuai.py | 0 .../0015_gsfenhong_fenhong_leixing.py | 0 {peizhi => products}/migrations/__init__.py | 0 {shangpin => products}/models.py | 0 {peizhi => products}/tests.py | 0 {shangpin => products}/urls.py | 1 - {shangpin => products}/utils.py | 2 +- {shangpin => products}/views.py | 2172 +- {shangdian => rank}/__init__.py | 0 {shangdian => rank}/admin.py | 0 {dengji => rank}/apps.py | 0 {dengji => rank}/migrations/0001_initial.py | 0 .../0002_alter_dashoubiaoxian_dingdan.py | 0 .../0003_alter_dashoubiaoxian_bankuai.py | 0 .../migrations/0004_dingdanbiaoqian.py | 0 {dengji => rank}/migrations/0005_fix_state.py | 0 .../migrations/0006_shenhejilu.py | 0 .../migrations/0007_kaoheguanbankuai.py | 0 ...guize_shenhejilu_dashou_beizhu_and_more.py | 0 ...inpai_alter_shenhejilu_jilu_id_and_more.py | 0 .../migrations/0010_kaohejilufeiyong.py | 0 .../migrations/0011_kaohepaytemp.py | 0 {shangdian => rank}/migrations/__init__.py | 0 {dengji => rank}/models.py | 2 +- {dengji => rank}/services.py | 0 {shangdian => rank}/tests.py | 0 {dengji => rank}/urls.py | 0 {dengji => rank}/utils.py | 38 +- {dengji => rank}/views.py | 58 +- rp.py | 80 + {shangpin => shop}/__init__.py | 0 {shangpin => shop}/admin.py | 0 {shangdian => shop}/apps.py | 0 .../migrations/0001_initial.py | 0 .../0002_alter_yonghupingzheng_yonghu.py | 0 ...eizhi_dianpu_kaiqi_meiri_xiane_and_more.py | 0 .../0004_dianpushangpinshenheshezhi.py | 0 ...dianpu_fenhong_choucheng_feilv_and_more.py | 0 ...imeiritongji_gei_shangji_cishu_and_more.py | 0 {shangpin => shop}/migrations/__init__.py | 0 {shangdian => shop}/models.py | 0 {shangpin => shop}/tests.py | 0 {shangdian => shop}/urls.py | 2 +- {shangdian => shop}/utils.py | 6 +- {shangdian => shop}/views.py | 159 +- {yonghu => users}/__init__.py | 0 {yonghu => users}/admin.py | 0 {yonghu => users}/apps.py | 0 {yonghu => users}/fadan_fenhong_utils.py | 4 +- {yonghu => users}/migrations/0001_initial.py | 0 .../migrations/0002_adminprofile_yaoqingma.py | 0 .../migrations/0003_rankingrecord.py | 0 .../migrations/0004_alter_usermain_avatar.py | 0 ...05_officialaccountuser_usermain_unionid.py | 0 .../0006_alter_officialaccountuser_unionid.py | 0 {yonghu => users}/migrations/0007_userkefu.py | 0 ...0008_userguanshi_fenghong_erci_and_more.py | 0 ...ne_userdashou_jinritixian_jine_and_more.py | 0 .../0010_userguanshi_invite_qrcode_url.py | 0 .../0011_usermain_shoujihao_renzheng.py | 0 .../migrations/0012_usershenheguan.py | 0 .../0013_usershenheguan_is_renzheng.py | 0 ...ianautorecord_tixianshenhejilu_and_more.py | 0 {yonghu => users}/migrations/__init__.py | 0 {yonghu => users}/models.py | 0 {yonghu => users}/paihang_views.py | 4 +- {yonghu => users}/ranking_tasks.py | 2 +- {yonghu => users}/tasks.py | 2 +- {yonghu => users}/tests.py | 0 {yonghu => users}/tixian_shenhe_services.py | 25 +- {yonghu => users}/tixian_shenhe_views.py | 11 +- {yonghu => users}/urls.py | 0 {yonghu => users}/views.py | 26443 ++++++++-------- utils/chat_utils.py | 6 +- utils/fadan_utils.py | 2 +- utils/invitationcode_utils.py | 56 + utils/weixin_broadcast.py | 2 +- utils/yaoqingma_utils.py | 89 - 178 files changed, 29249 insertions(+), 35862 deletions(-) rename {houtai => backend}/_init_.py (100%) rename {houtai => backend}/apps.py (86%) rename {houtai => backend}/migrations/0001_initial.py (100%) rename {houtai => backend}/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py (100%) rename {houtai => backend}/migrations/0003_tixianritongji.py (100%) rename {dengji => backend/migrations}/__init__.py (100%) rename {houtai => backend}/migrations/_init_.py (100%) rename {houtai => backend}/models.py (100%) rename {houtai => backend}/serializers.py (95%) rename {houtai => backend}/urls.py (100%) rename {houtai => backend}/utils.py (99%) rename {houtai => backend}/view.py (96%) rename {dengji/migrations => config}/__init__.py (100%) rename {dengji => config}/admin.py (100%) rename {peizhi => config}/apps.py (100%) rename {peizhi => config}/migrations/0001_initial.py (100%) rename {peizhi => config}/migrations/0002_shangjiamoban_shangjialianjie.py (100%) rename {peizhi => config}/migrations/0003_tupianpeizhi_fangwen_cishu.py (100%) rename {peizhi => config}/migrations/0004_tixianquotadefault.py (100%) rename {peizhi => config}/migrations/0005_ordertypemapping_dailydispatchstat_club.py (100%) rename {peizhi => config}/migrations/0006_clubconfig_accountpermission.py (100%) rename {peizhi => config}/migrations/0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more.py (100%) rename {peizhi => config}/migrations/0008_clubconfig_storage_bucket_name.py (100%) rename {peizhi => config}/migrations/0009_dailypayoutstat_dailyincomestat.py (100%) rename {peizhi => config}/migrations/0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more.py (100%) rename {peizhi => config}/migrations/0011_withdrawconfig.py (100%) rename {peizhi => config}/migrations/0012_shangjiamoban_chenghao_id_and_more.py (100%) rename {peizhi => config}/migrations/0013_shangjialianjie_previous_dingdan_id_and_more.py (100%) rename {dingdan => config/migrations}/__init__.py (100%) rename {peizhi => config}/models.py (99%) rename {peizhi => config}/serializers.py (100%) rename {peizhi => config}/tasks.py (98%) rename {dengji => config}/tests.py (100%) rename {peizhi => config}/urls.py (99%) rename {peizhi => config}/views.py (59%) delete mode 100644 dingdan/signals.py rename {dingdan/migrations => orders}/__init__.py (100%) rename {dingdan => orders}/admin.py (100%) rename {dingdan => orders}/apps.py (88%) rename {dingdan => orders}/cross_platform_tasks.py (100%) rename {dingdan => orders}/migrations/0001_initial.py (100%) rename {dingdan => orders}/migrations/0002_dingdan_auto_expire_at_dingdan_auto_task_id.py (100%) rename {dingdan => orders}/migrations/0003_dingdan_pending_dispatch_dingdan_status_8_time.py (100%) rename {dingdan => orders}/migrations/0004_alter_tuikuanjilu_chuliid.py (100%) rename {dingdan => orders}/migrations/0005_alter_chufajilu_chuliid.py (100%) rename {dingdan => orders}/migrations/0006_chufatupian.py (100%) rename {dingdan => orders}/migrations/0007_chufajilu_ssliyou.py (100%) rename {dingdan => orders}/migrations/0008_dingdan_is_cross_dingdan_partner_order_id.py (100%) rename {dingdan => orders}/migrations/0009_dingdanpingtai_clkf_dingdanpingtai_jjjs_biaoshi.py (100%) rename {dingdan => orders}/migrations/0010_remove_dingdanpingtai_clkf_dingdan_clkf.py (100%) rename {dingdan => orders}/migrations/0011_dingdan_tkly.py (100%) rename {dingdan => orders}/migrations/0012_dingdan_wechat_transaction_id_and_more.py (100%) rename {dingdan => orders}/migrations/0013_alter_chufajilu_qingqiuid.py (100%) rename {dingdan => orders}/migrations/0014_dingdan_dispatch_type_dingdan_partner_club_id.py (100%) rename {dingdan => orders}/migrations/0015_presettlement_orderdashouhistory_and_more.py (100%) rename {dingdan => orders}/migrations/0016_alter_presettlement_dingdan_id_and_more.py (100%) rename {dingdan => orders}/migrations/0017_dingdanpingtai_dianpu_id_and_more.py (100%) rename {dingdan => orders}/migrations/0018_dingdanshangjia_partner_yonghu_id.py (100%) rename {dingdan => orders}/migrations/0019_dingdanpingtai_shangji_dianpu_id_and_more.py (100%) rename {dingdan => orders}/migrations/0020_fadanfenhong_fadan.py (100%) rename {dingdan => orders}/migrations/0021_fadanshensutupian.py (100%) rename {dingdan => orders}/migrations/0022_fadan_partner_club_id_fadan_shenqingren_shenfen.py (100%) rename {dingdan => orders}/migrations/0023_fadanshensutupian_yongtu.py (100%) rename {dingdan => orders}/migrations/0024_fadan_chulizhe_fadan_chulizhe_shenfen.py (100%) rename {dingdan => orders}/migrations/0025_alter_fadan_shenqingren_shenfen.py (100%) rename {dingdan => orders}/migrations/0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more.py (100%) rename {dingdan => orders}/migrations/0027_dingdanpeihu.py (100%) rename {dingdan => orders}/migrations/0028_dingdanshangjia_shangyige_dingdan_id.py (100%) rename {dingdan => orders}/migrations/0029_fadanfenhong_is_individual_rate.py (100%) rename {dingdan => orders}/migrations/0030_fadan_shenqingren_fenhong_jine.py (100%) rename {dingdan => orders}/migrations/0031_dingdan_guanshi_fencheng.py (100%) rename {houtai => orders}/migrations/__init__.py (100%) rename {dingdan => orders}/models.py (99%) rename dingdan/tongzhi_tasks.py => orders/notice_tasks.py (94%) create mode 100644 orders/signals.py rename {dingdan => orders}/tasks.py (96%) rename {dingdan => orders}/tests.py (100%) rename {dingdan => orders}/urls.py (99%) rename {dingdan => orders}/utils.py (94%) rename {dingdan => orders}/views.py (90%) rename {peizhi => products}/__init__.py (100%) rename {peizhi => products}/admin.py (100%) rename {shangpin => products}/apps.py (100%) rename {shangpin => products}/migrations/0001_initial.py (100%) rename {shangpin => products}/migrations/0002_alter_huiyuangoumai_unique_together_and_more.py (100%) rename {shangpin => products}/migrations/0003_shangpin_paixu_shangpinleixing_paixu_and_more.py (100%) rename {shangpin => products}/migrations/0004_shangpin_shenhezhuangtai_and_more.py (100%) rename {shangpin => products}/migrations/0005_alter_gsfenhong_dashouid.py (100%) rename {shangpin => products}/migrations/0006_gsfenhong_huiyuan_id_huiyuan_zuzhangfc_duocifenhong.py (100%) rename {shangpin => products}/migrations/0007_remove_duocifenhong_duoci_fenho_guanshi_b3de53_idx_and_more.py (100%) rename {shangpin => products}/migrations/0008_gsfenhong_zuzhang_fenhong_gsfenhong_zuzhang_id.py (100%) rename {shangpin => products}/migrations/0009_shangpin_ewai_dashou_fencheng_and_more.py (100%) rename {shangpin => products}/migrations/0010_shangpinleixing_is_cross_enabled.py (100%) rename {shangpin => products}/migrations/0011_shangpin_dianpu_shangpin_dianpu_leixing_and_more.py (100%) rename {shangpin => products}/migrations/0012_shangpinmeiritongji.py (100%) rename {shangpin => products}/migrations/0013_shangpinleixing_bankuai.py (100%) rename {shangpin => products}/migrations/0014_huiyuan_bankuai.py (100%) rename {shangpin => products}/migrations/0015_gsfenhong_fenhong_leixing.py (100%) rename {peizhi => products}/migrations/__init__.py (100%) rename {shangpin => products}/models.py (100%) rename {peizhi => products}/tests.py (100%) rename {shangpin => products}/urls.py (99%) rename {shangpin => products}/utils.py (99%) rename {shangpin => products}/views.py (75%) rename {shangdian => rank}/__init__.py (100%) rename {shangdian => rank}/admin.py (100%) rename {dengji => rank}/apps.py (100%) rename {dengji => rank}/migrations/0001_initial.py (100%) rename {dengji => rank}/migrations/0002_alter_dashoubiaoxian_dingdan.py (100%) rename {dengji => rank}/migrations/0003_alter_dashoubiaoxian_bankuai.py (100%) rename {dengji => rank}/migrations/0004_dingdanbiaoqian.py (100%) rename {dengji => rank}/migrations/0005_fix_state.py (100%) rename {dengji => rank}/migrations/0006_shenhejilu.py (100%) rename {dengji => rank}/migrations/0007_kaoheguanbankuai.py (100%) rename {dengji => rank}/migrations/0008_chenghao_kaohe_guize_shenhejilu_dashou_beizhu_and_more.py (100%) rename {dengji => rank}/migrations/0009_chenghao_kaioi_jinpai_alter_shenhejilu_jilu_id_and_more.py (100%) rename {dengji => rank}/migrations/0010_kaohejilufeiyong.py (100%) rename {dengji => rank}/migrations/0011_kaohepaytemp.py (100%) rename {shangdian => rank}/migrations/__init__.py (100%) rename {dengji => rank}/models.py (99%) rename {dengji => rank}/services.py (100%) rename {shangdian => rank}/tests.py (100%) rename {dengji => rank}/urls.py (100%) rename {dengji => rank}/utils.py (95%) rename {dengji => rank}/views.py (96%) create mode 100644 rp.py rename {shangpin => shop}/__init__.py (100%) rename {shangpin => shop}/admin.py (100%) rename {shangdian => shop}/apps.py (100%) rename {shangdian => shop}/migrations/0001_initial.py (100%) rename {shangdian => shop}/migrations/0002_alter_yonghupingzheng_yonghu.py (100%) rename {shangdian => shop}/migrations/0003_dianpumorenpeizhi_dianpu_kaiqi_meiri_xiane_and_more.py (100%) rename {shangdian => shop}/migrations/0004_dianpushangpinshenheshezhi.py (100%) rename {shangdian => shop}/migrations/0005_dianpu_fenhong_choucheng_feilv_and_more.py (100%) rename {shangdian => shop}/migrations/0006_dianpushouzhimeiritongji_gei_shangji_cishu_and_more.py (100%) rename {shangpin => shop}/migrations/__init__.py (100%) rename {shangdian => shop}/models.py (100%) rename {shangpin => shop}/tests.py (100%) rename {shangdian => shop}/urls.py (94%) rename {shangdian => shop}/utils.py (99%) rename {shangdian => shop}/views.py (97%) rename {yonghu => users}/__init__.py (100%) rename {yonghu => users}/admin.py (100%) rename {yonghu => users}/apps.py (100%) rename {yonghu => users}/fadan_fenhong_utils.py (98%) rename {yonghu => users}/migrations/0001_initial.py (100%) rename {yonghu => users}/migrations/0002_adminprofile_yaoqingma.py (100%) rename {yonghu => users}/migrations/0003_rankingrecord.py (100%) rename {yonghu => users}/migrations/0004_alter_usermain_avatar.py (100%) rename {yonghu => users}/migrations/0005_officialaccountuser_usermain_unionid.py (100%) rename {yonghu => users}/migrations/0006_alter_officialaccountuser_unionid.py (100%) rename {yonghu => users}/migrations/0007_userkefu.py (100%) rename {yonghu => users}/migrations/0008_userguanshi_fenghong_erci_and_more.py (100%) rename {yonghu => users}/migrations/0009_userdashou_ewai_xiane_userdashou_jinritixian_jine_and_more.py (100%) rename {yonghu => users}/migrations/0010_userguanshi_invite_qrcode_url.py (100%) rename {yonghu => users}/migrations/0011_usermain_shoujihao_renzheng.py (100%) rename {yonghu => users}/migrations/0012_usershenheguan.py (100%) rename {yonghu => users}/migrations/0013_usershenheguan_is_renzheng.py (100%) rename {yonghu => users}/migrations/0014_tixianautorecord_tixianshenhejilu_and_more.py (100%) rename {yonghu => users}/migrations/__init__.py (100%) rename {yonghu => users}/models.py (100%) rename {yonghu => users}/paihang_views.py (98%) rename {yonghu => users}/ranking_tasks.py (99%) rename {yonghu => users}/tasks.py (98%) rename {yonghu => users}/tests.py (100%) rename {yonghu => users}/tixian_shenhe_services.py (99%) rename {yonghu => users}/tixian_shenhe_views.py (98%) rename {yonghu => users}/urls.py (100%) rename {yonghu => users}/views.py (96%) create mode 100644 utils/invitationcode_utils.py delete mode 100644 utils/yaoqingma_utils.py diff --git a/a_long_dianjing/celery.py b/a_long_dianjing/celery.py index 7927a29..d98cbfb 100644 --- a/a_long_dianjing/celery.py +++ b/a_long_dianjing/celery.py @@ -1,12 +1,6 @@ """ 阿龙电竞 - Celery定时任务主配置 -生产环境就绪版本,支持精确延时任务和周期性任务 """ - - - - - import os from celery import Celery from celery.schedules import crontab @@ -26,14 +20,9 @@ app.config_from_object('django.conf:settings', namespace='CELERY') #app.autodiscover_tasks() app.autodiscover_tasks(['yonghu.tasks', 'yonghu.ranking_tasks', 'dingdan.tasks', 'peizhi.tasks']) - - # 配置周期性任务(Celery Beat Schedule) app.conf.beat_schedule = { - - - # 月榜数据转移(每月最后一天23:50执行) 'yuebang_zhuanyi': { 'task': 'yonghu.ranking_tasks.zhuanyi_yuebang', @@ -41,8 +30,6 @@ app.conf.beat_schedule = { 'options': {'queue': 'periodic_tasks', 'priority': 7}, }, - - # 日榜数据转移(每天23:55执行,在清零前) 'ribang_zhuanyi': { 'task': 'yonghu.ranking_tasks.zhuanyi_ribang', @@ -51,8 +38,6 @@ app.conf.beat_schedule = { 'options': {'queue': 'periodic_tasks', 'priority': 7}, # 优先级高于清零任务 }, - - # 清理旧历史数据(每月1号凌晨1点执行) 'qingli_jiulishuju': { 'task': 'yonghu.ranking_tasks.qingli_jiulishuju', @@ -60,21 +45,13 @@ app.conf.beat_schedule = { 'options': {'queue': 'periodic_tasks', 'priority': 4}, }, - - - - - - - - # 🔥 补偿检查任务(每5分钟运行) + # 补偿检查任务(每5分钟运行) 'check_order_expire_task': { 'task': 'dingdan.tasks.check_order_expire_task', 'schedule': crontab(minute='*/5000'), 'options': {'queue': 'order_tasks', 'priority': 3}, }, - # 1. 每日凌晨0点执行 - 清零任务 'daily_reset_task': { 'task': 'yonghu.tasks.daily_reset_task', @@ -99,10 +76,6 @@ app.conf.beat_schedule = { 'kwargs': {} }, - - - - # 4. 每天凌晨0点5分清理收支记录 'daily_sz_reset_task': { 'task': 'peizhi.tasks.daily_sz_reset_task', @@ -197,4 +170,4 @@ app.conf.task_acks_late = True app.conf.worker_disable_rate_limits = True # 结果过期时间 -app.conf.result_expires = 3600 # 任务结果保留1小时 \ No newline at end of file +app.conf.result_expires = 3600 # 任务结果保留1小时 diff --git a/a_long_dianjing/celery_app.py b/a_long_dianjing/celery_app.py index 10629c2..140f6e0 100644 --- a/a_long_dianjing/celery_app.py +++ b/a_long_dianjing/celery_app.py @@ -1,10 +1,10 @@ -# celery_app.py -import os -from celery import Celery - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'a_long_dianjing.settings') - -app = Celery('a_long_dianjing') -app.config_from_object('django.conf:settings', namespace='CELERY') -# 手动导入任务模块,避免与已有 tasks.py 冲突 -app.autodiscover_tasks(['dingdan.tongzhi_tasks']) \ No newline at end of file +# celery_app.py +import os +from celery import Celery + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'a_long_dianjing.settings') + +app = Celery('a_long_dianjing') +app.config_from_object('django.conf:settings', namespace='CELERY') +# 手动导入任务模块,避免与已有 tasks.py 冲突 +app.autodiscover_tasks(['dingdan.notice_tasks']) diff --git a/a_long_dianjing/settings.py b/a_long_dianjing/settings.py index 60bb888..2e6ab89 100644 --- a/a_long_dianjing/settings.py +++ b/a_long_dianjing/settings.py @@ -1,329 +1,329 @@ -""" -Django settings for a_long_dianjing project. -""" -from pathlib import Path -from datetime import timedelta -import os - -# 从密钥配置文件导入敏感信息 -try: - import app_secrets -except ImportError: - raise ImportError("缺少 appapp_secrets.py 密钥配置文件,请参照项目说明创建") - -BASE_DIR = Path(__file__).resolve().parent.parent - -SECRET_KEY = app_secrets.SECRET_KEY -DEBUG = False -ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'www.abas.asia'] - -# ==================== 应用配置 ==================== -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'rest_framework', - 'rest_framework_simplejwt', - 'corsheaders', - 'django_celery_results', - 'celery', - 'yonghu', - 'dingdan', - 'shangpin', - 'peizhi', - 'houtai', - 'utils', - 'shangdian', - 'dengji', -] - -MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'a_long_dianjing.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR / 'templates'], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'a_long_dianjing.wsgi.application' - -# ==================== 数据库 ==================== -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': app_secrets.DATABASE_NAME, - 'USER': app_secrets.DATABASE_USER, - 'HOST': app_secrets.DATABASE_HOST, - 'PASSWORD': app_secrets.DATABASE_PASSWORD, - 'PORT': app_secrets.DATABASE_PORT, - 'OPTIONS': { - 'charset': 'utf8mb4', - 'init_command': "SET time_zone='+08:00'", - } - } -} - -# ==================== 密码验证 ==================== -AUTH_PASSWORD_VALIDATORS = [ - {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, - {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, - {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, - {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, -] - -# ==================== 国际化 ==================== -LANGUAGE_CODE = 'zh-Hans' -TIME_ZONE = 'Asia/Shanghai' -USE_I18N = True -USE_TZ = False - -STATIC_URL = 'static/' -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -# ==================== JWT 配置 ==================== -SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(days=36500), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=36500), - 'ROTATE_REFRESH_TOKENS': False, - 'BLACKLIST_AFTER_ROTATION': False, - 'UPDATE_LAST_LOGIN': False, - 'ALGORITHM': 'HS256', - 'SIGNING_KEY': SECRET_KEY, - 'VERIFYING_KEY': None, - 'AUDIENCE': None, - 'ISSUER': None, - 'JWK_URL': None, - 'LEEWAY': 0, - 'AUTH_HEADER_TYPES': ('Bearer',), - 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', - 'USER_ID_FIELD': 'yonghuid', - 'USER_ID_CLAIM': 'yonghuid', - 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule', - 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), - 'TOKEN_TYPE_CLAIM': 'token_type', - 'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser', - 'JTI_CLAIM': 'jti', -} - -AUTHENTICATION_BACKENDS = [ - 'django.contrib.auth.backends.ModelBackend', -] - -AUTH_USER_MODEL = 'yonghu.UserMain' - -# ==================== DRF 配置 ==================== -REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework_simplejwt.authentication.JWTAuthentication', - ), - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', - ), - 'DEFAULT_THROTTLE_CLASSES': [ - 'rest_framework.throttling.AnonRateThrottle', - 'rest_framework.throttling.UserRateThrottle', - ], - 'DEFAULT_THROTTLE_RATES': { - 'anon': '100/minute', - 'user': '1000/minute', - }, - 'EXCEPTION_HANDLER': 'utils.exception_handler.custom_exception_handler', - 'DEFAULT_RENDERER_CLASSES': [ - 'rest_framework.renderers.JSONRenderer', - ], -} - -# ==================== H5 域名 ==================== -H5_DOMAIN = 'https://www.abas.asia:666' - -# ==================== CORS 配置 ==================== -CORS_ALLOWED_ORIGINS = [ - "http://localhost:8080", - "http://127.0.0.1:8080", - "https://www.abas.asia", - "https://www.abas.asia:666", - "https://www.abas.asia:1234", - "https://www.abas.asia:963", - "http://localhost:5174", - "http://localhost:5173", - "http://localhost:3000", - "http://localhost:3001", -] -CORS_ALLOW_CREDENTIALS = True -CORS_ALLOW_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") -CORS_ALLOW_HEADERS = [ - "accept", "accept-encoding", "authorization", "content-type", - "dnt", "origin", "user-agent", "x-csrftoken", "x-requested-with", -] - -# ==================== 腾讯云 COS ==================== -COS_SECRET_ID = app_secrets.COS_SECRET_ID -COS_SECRET_KEY = app_secrets.COS_SECRET_KEY -COS_REGION = app_secrets.COS_REGION -COS_BUCKET = app_secrets.COS_BUCKET -COS_SCHEME = 'https' -COS_DOMAIN = f'https://{COS_BUCKET}.cos.{COS_REGION}.myqcloud.com' -COS_APPID = app_secrets.COS_APPID -COS_STS_DURATION = 7200 - -# ==================== 微信小程序 & 服务号 ==================== -WEIXIN_APPID = app_secrets.WEIXIN_APPID -WEIXIN_SECRET = app_secrets.WEIXIN_SECRET -WEIXIN_MCHID = app_secrets.WEIXIN_MCHID -WEIXIN_SHANGHUMIYAO = app_secrets.WEIXIN_SHANGHUMIYAO -WEIXIN_OFFICIAL_APPID = app_secrets.WEIXIN_OFFICIAL_APPID -WEIXIN_OFFICIAL_SECRET = app_secrets.WEIXIN_OFFICIAL_SECRET -WEIXIN_OFFICIAL_TOKEN = app_secrets.WEIXIN_OFFICIAL_TOKEN -WEIXIN_OFFICIAL_ENCODING_AES_KEY = app_secrets.WEIXIN_OFFICIAL_ENCODING_AES_KEY -WEIXIN_TEMPLATE_ID = app_secrets.WEIXIN_TEMPLATE_ID -WEIXIN_TEMPLATE_MAX_PER_MINUTE = 950 -WEIXIN_TEMPLATE_BATCH_SIZE = 100 -WEIXIN_CERT_PATH = app_secrets.WEIXIN_CERT_PATH -WEIXIN_KEY_PATH = app_secrets.WEIXIN_KEY_PATH - -# ==================== 微信支付回调地址 ==================== -WEIXIN_NOTIFY_URL = 'https://www.abas.asia/hqhd/dingdan/pay-notify/' -WEIXIN_PAY_DESCRIPTION = "网络服务" -YAJIN_PAY_DESCRIPTION = "打手押金充值" -YAJIN_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/yajinhuitiao" -JIFEN_PAY_DESCRIPTION = "打手积分补充" -JIFEN_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/jifenhuitiao" -HUIYUAN_PAY_DESCRIPTION = "打手会员充值" -HUIYUAN_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/huiyuanhuitiao" -KAOHE_PAY_DESCRIPTION = '考核金牌-缴纳考核费' -KAOHE_PAY_NOTIFY_URL = 'https://www.abas.asia/hqhd/yonghu/kh_huitiao' -KAOHE_PAY_DEFAULT_FEE = 5.00 -SHANGJIA_CZ_PREFIX = "CzSJ" -SHANGJIA_CZ_PAY_DESCRIPTION = "阿龙电竞-商家余额充值" -SHANGJIA_CZ_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/sjhuitiao" -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' - -# ==================== 订单ID前缀 ==================== -ORDER_PREFIX = { - 'yajin': 'CzYJ', - 'jifen': 'CzJF', - 'huiyuan': 'CzHY', -} - -# ==================== 腾讯云 IM ==================== -TENCENT_IM_CONFIG = { - 'SDK_APP_ID': 0, - 'SECRET_KEY': '', -} - -# ==================== 阿里云短信 ==================== -ALIYUN_SMS_CONFIG = { - 'ACCESS_KEY_ID': '', - 'ACCESS_KEY_SECRET': '', - 'SIGN_NAME': '', - 'TEMPLATE_CODE': '', - 'REGION_ID': 'cn-hangzhou', -} - -# ==================== GoEasy IM ==================== -GOEASY_APPKEY = app_secrets.GOEASY_APPKEY -GOEASY_SECRET = app_secrets.GOEASY_SECRET -GOEASY_REST_URL = 'https://rest-hz.goeasy.io/v2/im/message' - -# ==================== Celery 配置 ==================== -REDIS_HOST = app_secrets.REDIS_HOST -REDIS_PORT = app_secrets.REDIS_PORT -REDIS_PASSWORD = app_secrets.REDIS_PASSWORD -REDIS_DB_CELERY = app_secrets.REDIS_DB_CELERY -REDIS_DB_RESULT = app_secrets.REDIS_DB_RESULT - -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.redis.RedisCache', - 'LOCATION': f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/2', - 'TIMEOUT': 300, - 'OPTIONS': {} - } -} - -if REDIS_PASSWORD: - CELERY_BROKER_URL = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_CELERY}' - CELERY_RESULT_BACKEND = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_RESULT}' -else: - CELERY_BROKER_URL = f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_CELERY}' - CELERY_RESULT_BACKEND = f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_RESULT}' - -CELERY_ACCEPT_CONTENT = ['json'] -CELERY_TASK_SERIALIZER = 'json' -CELERY_RESULT_SERIALIZER = 'json' -CELERY_TIMEZONE = 'Asia/Shanghai' -CELERY_ENABLE_UTC = True -CELERY_IMPORTS = ('dingdan.tongzhi_tasks',) -CELERY_TASK_TIME_LIMIT = 300 -CELERY_TASK_SOFT_TIME_LIMIT = 240 -CELERY_WORKER_CONCURRENCY = 4 -CELERY_WORKER_PREFETCH_MULTIPLIER = 1 -CELERY_TASK_ACKS_LATE = True -CELERY_WORKER_DISABLE_RATE_LIMITS = True -CELERY_TASK_DEFAULT_QUEUE = 'default' -CELERY_TASK_QUEUES = { - 'default': {'exchange': 'default', 'routing_key': 'default'}, - 'order_tasks': {'exchange': 'order_tasks', 'routing_key': 'order_tasks'}, - 'periodic_tasks': {'exchange': 'periodic_tasks', 'routing_key': 'periodic_tasks'}, -} - -# ==================== 定时任务时间配置 ==================== -ORDER_EXPIRE_SECONDS = 48 * 60 * 60 # 48小时 = 172800秒 -DAILY_TASK_HOUR = 0 -DAILY_TASK_MINUTE = 30 -MONTHLY_TASK_DAY = 1 -MONTHLY_TASK_HOUR = 0 -MONTHLY_TASK_MINUTE = 0 -ORDER_CHECK_INTERVAL = 5 - -# ==================== 跨平台订单互通 ==================== -CROSS_PLATFORM_TOKEN = app_secrets.CROSS_PLATFORM_TOKEN -CROSS_PLATFORM_SYNC_TOKEN = app_secrets.CROSS_PLATFORM_SYNC_TOKEN -PARTNER_BASE_URL = app_secrets.PARTNER_BASE_URL -PARTNER_SYNC_ORDER_URL = '/dingdan/partner_sync_order' -PARTNER_CHECK_CLAIM_URL = '/dingdan/partner_check_claim' -PARTNER_NOTIFY_CLAIM_URL = '/dingdan/partner_notify_claim' -PARTNER_REFUND_NOTIFY_URL = '/dingdan/partner_refund_notify' -AGENT_SHANGJIA_ID = app_secrets.AGENT_SHANGJIA_ID -CROSS_PLATFORM_MEMBER_ID = app_secrets.CROSS_PLATFORM_MEMBER_ID -CROSS_PLATFORM_GAME_TYPE_ID = 1 -CROSS_PLATFORM_WHITELIST = app_secrets.CROSS_PLATFORM_WHITELIST -SPECIAL_GUANSHI_IDS = app_secrets.SPECIAL_GUANSHI_IDS - -# ==================== 微信支付 V3 ==================== -WECHAT_PAY_V3_CONFIG = { - 'MCHID': app_secrets.WECHAT_PAY_V3_MCHID, - 'APPID': app_secrets.WECHAT_PAY_V3_APPID, - 'API_V3_KEY': app_secrets.WECHAT_PAY_V3_API_V3_KEY, - 'PRIVATE_KEY_PATH': app_secrets.WECHAT_PAY_V3_PRIVATE_KEY_PATH, - 'CERT_SERIAL_NO': app_secrets.WECHAT_PAY_V3_CERT_SERIAL_NO, - 'PLATFORM_CERT_DIR': app_secrets.WECHAT_PAY_V3_PLATFORM_CERT_DIR, - 'NOTIFY_URL': 'https://www.abas.asia/hqhd/yonghu/tixiancallback', - 'TRANSFER_SCENE_ID': '1005', -} +""" +Django settings for a_long_dianjing project. +""" +from pathlib import Path +from datetime import timedelta +import os + +# 从密钥配置文件导入敏感信息 +try: + import app_secrets +except ImportError: + raise ImportError("缺少 appapp_secrets.py 密钥配置文件,请参照项目说明创建") + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = app_secrets.SECRET_KEY +DEBUG = False +ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'www.abas.asia'] + +# ==================== 应用配置 ==================== +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'rest_framework_simplejwt', + 'corsheaders', + 'django_celery_results', + 'celery', + 'yonghu', + 'dingdan', + 'shangpin', + 'peizhi', + 'houtai', + 'utils', + 'shangdian', + 'dengji', +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'a_long_dianjing.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'a_long_dianjing.wsgi.application' + +# ==================== 数据库 ==================== +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': app_secrets.DATABASE_NAME, + 'USER': app_secrets.DATABASE_USER, + 'HOST': app_secrets.DATABASE_HOST, + 'PASSWORD': app_secrets.DATABASE_PASSWORD, + 'PORT': app_secrets.DATABASE_PORT, + 'OPTIONS': { + 'charset': 'utf8mb4', + 'init_command': "SET time_zone='+08:00'", + } + } +} + +# ==================== 密码验证 ==================== +AUTH_PASSWORD_VALIDATORS = [ + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, +] + +# ==================== 国际化 ==================== +LANGUAGE_CODE = 'zh-Hans' +TIME_ZONE = 'Asia/Shanghai' +USE_I18N = True +USE_TZ = False + +STATIC_URL = 'static/' +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# ==================== JWT 配置 ==================== +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(days=36500), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=36500), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': False, + 'UPDATE_LAST_LOGIN': False, + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUDIENCE': None, + 'ISSUER': None, + 'JWK_URL': None, + 'LEEWAY': 0, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', + 'USER_ID_FIELD': 'yonghuid', + 'USER_ID_CLAIM': 'yonghuid', + 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule', + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', + 'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser', + 'JTI_CLAIM': 'jti', +} + +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', +] + +AUTH_USER_MODEL = 'yonghu.UserMain' + +# ==================== DRF 配置 ==================== +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ), + 'DEFAULT_THROTTLE_CLASSES': [ + 'rest_framework.throttling.AnonRateThrottle', + 'rest_framework.throttling.UserRateThrottle', + ], + 'DEFAULT_THROTTLE_RATES': { + 'anon': '100/minute', + 'user': '1000/minute', + }, + 'EXCEPTION_HANDLER': 'utils.exception_handler.custom_exception_handler', + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + ], +} + +# ==================== H5 域名 ==================== +H5_DOMAIN = 'https://www.abas.asia:666' + +# ==================== CORS 配置 ==================== +CORS_ALLOWED_ORIGINS = [ + "http://localhost:8080", + "http://127.0.0.1:8080", + "https://www.abas.asia", + "https://www.abas.asia:666", + "https://www.abas.asia:1234", + "https://www.abas.asia:963", + "http://localhost:5174", + "http://localhost:5173", + "http://localhost:3000", + "http://localhost:3001", +] +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOW_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") +CORS_ALLOW_HEADERS = [ + "accept", "accept-encoding", "authorization", "content-type", + "dnt", "origin", "user-agent", "x-csrftoken", "x-requested-with", +] + +# ==================== 腾讯云 COS ==================== +COS_SECRET_ID = app_secrets.COS_SECRET_ID +COS_SECRET_KEY = app_secrets.COS_SECRET_KEY +COS_REGION = app_secrets.COS_REGION +COS_BUCKET = app_secrets.COS_BUCKET +COS_SCHEME = 'https' +COS_DOMAIN = f'https://{COS_BUCKET}.cos.{COS_REGION}.myqcloud.com' +COS_APPID = app_secrets.COS_APPID +COS_STS_DURATION = 7200 + +# ==================== 微信小程序 & 服务号 ==================== +WEIXIN_APPID = app_secrets.WEIXIN_APPID +WEIXIN_SECRET = app_secrets.WEIXIN_SECRET +WEIXIN_MCHID = app_secrets.WEIXIN_MCHID +WEIXIN_SHANGHUMIYAO = app_secrets.WEIXIN_SHANGHUMIYAO +WEIXIN_OFFICIAL_APPID = app_secrets.WEIXIN_OFFICIAL_APPID +WEIXIN_OFFICIAL_SECRET = app_secrets.WEIXIN_OFFICIAL_SECRET +WEIXIN_OFFICIAL_TOKEN = app_secrets.WEIXIN_OFFICIAL_TOKEN +WEIXIN_OFFICIAL_ENCODING_AES_KEY = app_secrets.WEIXIN_OFFICIAL_ENCODING_AES_KEY +WEIXIN_TEMPLATE_ID = app_secrets.WEIXIN_TEMPLATE_ID +WEIXIN_TEMPLATE_MAX_PER_MINUTE = 950 +WEIXIN_TEMPLATE_BATCH_SIZE = 100 +WEIXIN_CERT_PATH = app_secrets.WEIXIN_CERT_PATH +WEIXIN_KEY_PATH = app_secrets.WEIXIN_KEY_PATH + +# ==================== 微信支付回调地址 ==================== +WEIXIN_NOTIFY_URL = 'https://www.abas.asia/hqhd/dingdan/pay-notify/' +WEIXIN_PAY_DESCRIPTION = "网络服务" +YAJIN_PAY_DESCRIPTION = "打手押金充值" +YAJIN_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/yajinhuitiao" +JIFEN_PAY_DESCRIPTION = "打手积分补充" +JIFEN_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/jifenhuitiao" +HUIYUAN_PAY_DESCRIPTION = "打手会员充值" +HUIYUAN_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/huiyuanhuitiao" +KAOHE_PAY_DESCRIPTION = '考核金牌-缴纳考核费' +KAOHE_PAY_NOTIFY_URL = 'https://www.abas.asia/hqhd/yonghu/kh_huitiao' +KAOHE_PAY_DEFAULT_FEE = 5.00 +SHANGJIA_CZ_PREFIX = "CzSJ" +SHANGJIA_CZ_PAY_DESCRIPTION = "阿龙电竞-商家余额充值" +SHANGJIA_CZ_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/sjhuitiao" +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' + +# ==================== 订单ID前缀 ==================== +ORDER_PREFIX = { + 'yajin': 'CzYJ', + 'jifen': 'CzJF', + 'huiyuan': 'CzHY', +} + +# ==================== 腾讯云 IM ==================== +TENCENT_IM_CONFIG = { + 'SDK_APP_ID': 0, + 'SECRET_KEY': '', +} + +# ==================== 阿里云短信 ==================== +ALIYUN_SMS_CONFIG = { + 'ACCESS_KEY_ID': '', + 'ACCESS_KEY_SECRET': '', + 'SIGN_NAME': '', + 'TEMPLATE_CODE': '', + 'REGION_ID': 'cn-hangzhou', +} + +# ==================== GoEasy IM ==================== +GOEASY_APPKEY = app_secrets.GOEASY_APPKEY +GOEASY_SECRET = app_secrets.GOEASY_SECRET +GOEASY_REST_URL = 'https://rest-hz.goeasy.io/v2/im/message' + +# ==================== Celery 配置 ==================== +REDIS_HOST = app_secrets.REDIS_HOST +REDIS_PORT = app_secrets.REDIS_PORT +REDIS_PASSWORD = app_secrets.REDIS_PASSWORD +REDIS_DB_CELERY = app_secrets.REDIS_DB_CELERY +REDIS_DB_RESULT = app_secrets.REDIS_DB_RESULT + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/2', + 'TIMEOUT': 300, + 'OPTIONS': {} + } +} + +if REDIS_PASSWORD: + CELERY_BROKER_URL = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_CELERY}' + CELERY_RESULT_BACKEND = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_RESULT}' +else: + CELERY_BROKER_URL = f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_CELERY}' + CELERY_RESULT_BACKEND = f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_RESULT}' + +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_RESULT_SERIALIZER = 'json' +CELERY_TIMEZONE = 'Asia/Shanghai' +CELERY_ENABLE_UTC = True +CELERY_IMPORTS = ('dingdan.notice_tasks',) +CELERY_TASK_TIME_LIMIT = 300 +CELERY_TASK_SOFT_TIME_LIMIT = 240 +CELERY_WORKER_CONCURRENCY = 4 +CELERY_WORKER_PREFETCH_MULTIPLIER = 1 +CELERY_TASK_ACKS_LATE = True +CELERY_WORKER_DISABLE_RATE_LIMITS = True +CELERY_TASK_DEFAULT_QUEUE = 'default' +CELERY_TASK_QUEUES = { + 'default': {'exchange': 'default', 'routing_key': 'default'}, + 'order_tasks': {'exchange': 'order_tasks', 'routing_key': 'order_tasks'}, + 'periodic_tasks': {'exchange': 'periodic_tasks', 'routing_key': 'periodic_tasks'}, +} + +# ==================== 定时任务时间配置 ==================== +ORDER_EXPIRE_SECONDS = 48 * 60 * 60 # 48小时 = 172800秒 +DAILY_TASK_HOUR = 0 +DAILY_TASK_MINUTE = 30 +MONTHLY_TASK_DAY = 1 +MONTHLY_TASK_HOUR = 0 +MONTHLY_TASK_MINUTE = 0 +ORDER_CHECK_INTERVAL = 5 + +# ==================== 跨平台订单互通 ==================== +CROSS_PLATFORM_TOKEN = app_secrets.CROSS_PLATFORM_TOKEN +CROSS_PLATFORM_SYNC_TOKEN = app_secrets.CROSS_PLATFORM_SYNC_TOKEN +PARTNER_BASE_URL = app_secrets.PARTNER_BASE_URL +PARTNER_SYNC_ORDER_URL = '/dingdan/partner_sync_order' +PARTNER_CHECK_CLAIM_URL = '/dingdan/partner_check_claim' +PARTNER_NOTIFY_CLAIM_URL = '/dingdan/partner_notify_claim' +PARTNER_REFUND_NOTIFY_URL = '/dingdan/partner_refund_notify' +AGENT_SHANGJIA_ID = app_secrets.AGENT_SHANGJIA_ID +CROSS_PLATFORM_MEMBER_ID = app_secrets.CROSS_PLATFORM_MEMBER_ID +CROSS_PLATFORM_GAME_TYPE_ID = 1 +CROSS_PLATFORM_WHITELIST = app_secrets.CROSS_PLATFORM_WHITELIST +SPECIAL_GUANSHI_IDS = app_secrets.SPECIAL_GUANSHI_IDS + +# ==================== 微信支付 V3 ==================== +WECHAT_PAY_V3_CONFIG = { + 'MCHID': app_secrets.WECHAT_PAY_V3_MCHID, + 'APPID': app_secrets.WECHAT_PAY_V3_APPID, + 'API_V3_KEY': app_secrets.WECHAT_PAY_V3_API_V3_KEY, + 'PRIVATE_KEY_PATH': app_secrets.WECHAT_PAY_V3_PRIVATE_KEY_PATH, + 'CERT_SERIAL_NO': app_secrets.WECHAT_PAY_V3_CERT_SERIAL_NO, + 'PLATFORM_CERT_DIR': app_secrets.WECHAT_PAY_V3_PLATFORM_CERT_DIR, + 'NOTIFY_URL': 'https://www.abas.asia/hqhd/yonghu/tixiancallback', + 'TRANSFER_SCENE_ID': '1005', +} diff --git a/houtai/_init_.py b/backend/_init_.py similarity index 100% rename from houtai/_init_.py rename to backend/_init_.py diff --git a/houtai/apps.py b/backend/apps.py similarity index 86% rename from houtai/apps.py rename to backend/apps.py index bc21450..f64e2fd 100644 --- a/houtai/apps.py +++ b/backend/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class HoutaiConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'houtai' \ No newline at end of file + name = 'houtai' diff --git a/houtai/migrations/0001_initial.py b/backend/migrations/0001_initial.py similarity index 100% rename from houtai/migrations/0001_initial.py rename to backend/migrations/0001_initial.py diff --git a/houtai/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py b/backend/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py similarity index 100% rename from houtai/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py rename to backend/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py diff --git a/houtai/migrations/0003_tixianritongji.py b/backend/migrations/0003_tixianritongji.py similarity index 100% rename from houtai/migrations/0003_tixianritongji.py rename to backend/migrations/0003_tixianritongji.py diff --git a/dengji/__init__.py b/backend/migrations/__init__.py similarity index 100% rename from dengji/__init__.py rename to backend/migrations/__init__.py diff --git a/houtai/migrations/_init_.py b/backend/migrations/_init_.py similarity index 100% rename from houtai/migrations/_init_.py rename to backend/migrations/_init_.py diff --git a/houtai/models.py b/backend/models.py similarity index 100% rename from houtai/models.py rename to backend/models.py diff --git a/houtai/serializers.py b/backend/serializers.py similarity index 95% rename from houtai/serializers.py rename to backend/serializers.py index 68c7b37..8c1f8fd 100644 --- a/houtai/serializers.py +++ b/backend/serializers.py @@ -2,7 +2,7 @@ #小程序弹窗序列化器 from rest_framework import serializers -from peizhi.models import PopupPage, PopupConfig, PopupImage +from config.models import PopupPage, PopupConfig, PopupImage class PopupImageSerializer(serializers.ModelSerializer): """图片序列化器(输出用)""" diff --git a/houtai/urls.py b/backend/urls.py similarity index 100% rename from houtai/urls.py rename to backend/urls.py diff --git a/houtai/utils.py b/backend/utils.py similarity index 99% rename from houtai/utils.py rename to backend/utils.py index 95fc5bb..3299e0b 100644 --- a/houtai/utils.py +++ b/backend/utils.py @@ -11,6 +11,8 @@ from django.db.models import F from datetime import date from decimal import Decimal + + logger = logging.getLogger(__name__) @@ -217,10 +219,6 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')): 根据操作行为更新管事每日统计(邀请打手、充值会员、订单/押金分红) action: 1 = 邀请打手, 2 = 充值会员, 3 = 商家订单分红, 4 = 押金分红 """ - from django.db import transaction - from django.db.models import F - from datetime import date - today = date.today() with transaction.atomic(): diff --git a/houtai/view.py b/backend/view.py similarity index 96% rename from houtai/view.py rename to backend/view.py index f902651..49af8c9 100644 --- a/houtai/view.py +++ b/backend/view.py @@ -1,12509 +1,12064 @@ -import hmac -import threading -import traceback -from calendar import monthrange - -from django.contrib.auth.hashers import make_password -from django.db.models import Q, Count -from django.core.paginator import Paginator - - -from houtai.utils import update_dashou_daily_by_action -from dingdan.models import Lilubiao - -import uuid - -from rest_framework.parsers import JSONParser, MultiPartParser, FormParser - -from utils.oss_utils import upload_to_oss, validate_image, delete_from_oss -from shangpin.models import Shangpin, ShangpinLeixing, ShangpinZhuanqu,Huiyuan - - -import hashlib -from django.db.models import Q, Sum, F -from rest_framework import status - -from houtai.models import Role, Permission, RolePermission, UserRole, TixianRiTongji -from houtai.utils import verify_kefu_permission -import xmltodict - -from django.conf import settings - -from dingdan.models import Dashoutupian, Chufajilu, PreSettlement, OrderDashouHistory -# apps/houtai/views.py 继续添加 - -from django.views.decorators.csrf import csrf_exempt -from django.utils.decorators import method_decorator - -from houtai.models import Role - -from yonghu.models import UserMain, UserGuanshi, UserBoss, UserZuzhang - -from shangpin.models import DuociFenhong - -import time -import logging -import requests -from datetime import date, datetime, timedelta -from django.utils import timezone - -from decimal import Decimal - -from django.core.exceptions import ObjectDoesNotExist -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated, AllowAny - -from rest_framework.response import Response -from rest_framework.parsers import JSONParser - -from dingdan.utils import update_daily_dispatch_stat -from shangpin.models import ShangpinLeixing, Huiyuangoumai, Huiyuan -from yonghu.models import UserMain, UserKefu, UserDashou, Xiugaijilu, UserShangjia -from peizhi.models import AccountPermission, ClubConfig, Club, DailyDispatchStat, OrderTypeMapping, TixianQuotaDefault -from dingdan.models import Dingdan, Tuikuanjilu, CrossPlatformOrderData - -# views.py -import os -from peizhi.models import PopupPage, PopupConfig, PopupImage -from .serializers import PopupPageSerializer, PopupConfigSerializer, PopupImageSerializer - - -logger = logging.getLogger(__name__) - - -class GetClubConfigView(APIView): - """ - 获取对方俱乐部配置列表(用于跨平台选择) - 权限要求:客服身份 + 权限代码1且开启 - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - """ - 处理 POST 请求,返回对方俱乐部配置数据(is_self=0) - """ - try: - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '003aa' not in permissions: - return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) - - # 5. 查询所有对方俱乐部(is_self=0),只取必要字段 - club_configs = ClubConfig.objects.filter( - is_self=0 # 只返回对方俱乐部 - ).only( - 'club_id', - 'club_nickname', - 'club_avatar', - 'storage_bucket_domain' # 如果前端需要存储桶域名,一并返回 - ) - - # 构造返回数据列表 - data_list = [] - for club in club_configs: - data_list.append({ - 'club_id': club.club_id, - 'club_nickname': club.club_nickname or club.club_id, # 若昵称为空,用ID代替 - 'club_avatar': club.club_avatar or '', # 可能为空 - 'storage_bucket_domain': club.storage_bucket_domain or '' # 可选 - }) - - return Response({ - 'code': 0, - 'msg': 'success', - 'data': data_list - }) - - except Exception as e: - logger.error(f"获取俱乐部配置失败: {str(e)}") - return Response({ - 'code': 500, - 'msg': '服务器内部错误' - }) - - - - - -class GetCrossOrderListView(APIView): - """ - 获取跨平台订单列表(客服后台) - 权限要求:客服身份 + 权限代码1且开启 - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - """ - 处理 POST 请求,返回分页订单数据及统计信息 - """ - try: - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '003aa' not in permissions: - return Response({'code': 403, 'msg': '您无权查看俱乐部订单'}) - - # 5. 获取请求参数 - data = request.data - - - - - # 分页参数 - page = int(data.get('page', 1)) - page_size = int(data.get('pageSize', 20)) - - # 查询参数 - leixing = data.get('leixing') # 订单类型ID - zhuangtai = data.get('zhuangtai') # 状态字符串,如 "1,7" - club_id = data.get('club_id') # 对方俱乐部ID(精确) - direction = data.get('direction') # send/receive/all - dingdan_id = data.get('dingdan_id') # 订单ID模糊搜索 - club_id_search = data.get('club_id_search') # 对方俱乐部ID模糊搜索 - date = data.get('date') # 日期 YYYY-MM-DD - dashou_id = data.get('dashou_id') # 打手ID(接单打手ID) - clkf = data.get('clkf') # 处理客服 - - # 6. 构建基础查询(跨平台订单) - base_q = Q(is_cross=1) - - # 添加筛选条件(非空且非全部) - if leixing and leixing != '': - base_q &= Q(leixing_id=leixing) - - if zhuangtai and zhuangtai != 'all': - # 解析逗号分隔的状态码 - status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip().isdigit()] - if status_list: - base_q &= Q(zhuangtai__in=status_list) - - if club_id and club_id != '': - base_q &= Q(partner_club_id=club_id) - - # 方向映射 - if direction == 'send': - base_q &= Q(dispatch_type=1) - elif direction == 'receive': - base_q &= Q(dispatch_type=2) - # direction == 'all' 时不加方向条件 - - if dingdan_id: - base_q &= Q(dingdan_id__icontains=dingdan_id) - - if club_id_search: - base_q &= Q(partner_club_id__icontains=club_id_search) - - if date: - # 按日期精确过滤(假设 create_time 是带时区的 DateTimeField) - base_q &= Q(create_time__date=date) - - if dashou_id: - base_q &= Q(jiedan_dashou_id=dashou_id) - - if clkf: - base_q &= Q(clkf__icontains=clkf) - - # 7. 统计各状态数量(基于相同过滤条件) - # 获取所有符合条件的订单的状态分组统计 - status_counts = Dingdan.objects.filter(base_q).values('zhuangtai').annotate(count=Count('id')).order_by() - - # 转换为字典 {状态码: 数量} - status_map = {item['zhuangtai']: item['count'] for item in status_counts} - - # 定义状态列表(与前端一致) - status_list_map = { - 'all': 0, # 稍后填充总数量 - '1,7': 0, - '2': 0, - '8': 0, - '3': 0, - '4': 0, - '5': 0, - '6': 0, - } - # 填充总数 - status_list_map['all'] = sum(status_map.values()) - # 填充单个状态 - for code, cnt in status_map.items(): - if code == 1 or code == 7: - status_list_map['1,7'] += cnt - elif code in status_list_map: - status_list_map[str(code)] = cnt - # 其他状态暂不处理 - - # 构建 stats 统计 - total_orders = status_list_map['all'] - completed_orders = status_map.get(3, 0) - refund_orders = status_map.get(5, 0) - pending_orders = status_map.get(8, 0) + status_map.get(4, 0) - - stats = { - 'totalOrders': total_orders, - 'completedOrders': completed_orders, - 'refundOrders': refund_orders, - 'pendingOrders': pending_orders, - } - - # 8. 分页获取订单列表 - # 按创建时间倒序,保证分页稳定 - orders_qs = Dingdan.objects.filter(base_q).order_by('-create_time') - - # 使用 Paginator 分页 - paginator = Paginator(orders_qs, page_size) - current_page = paginator.get_page(page) - order_list = current_page.object_list - - # 只返回前端需要的字段,减少数据传输 - data_list = [] - for order in order_list: - data_list.append({ - 'dingdan_id': order.dingdan_id, - 'nicheng': order.nicheng or '', - 'jieshao': order.jieshao or '', - 'jiage': float(order.jine) if order.jine else 0, - 'zhuangtai': order.zhuangtai, - }) - - # 9. 返回结果 - return Response({ - 'code': 0, - 'msg': 'success', - 'data': { - 'list': data_list, - 'total': total_orders, - 'statusCounts': status_list_map, - 'stats': stats, - } - }) - - except Exception as e: - logger.error(f"获取跨平台订单列表失败: {str(e)}") - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - - - -class GetCrossOrderDetailView(APIView): - """ - 客服获取跨平台订单详情接口 - 请求:POST /houtai/hqkptddxq - 参数:{"username": "客服手机号", "dingdan_id": "订单ID"} - 认证:JWT,权限码1 - 返回:code=0 + 订单详情数据(包含俱乐部信息、打手图片、处罚记录等) - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - start_time = time.time() - - # 1. 获取参数 - username = request.data.get('username', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - if not username or not dingdan_id: - return Response({'code': 400, 'msg': '参数不完整'}) - - try: - # 2. 身份验证 - current_user = request.user - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '003aa' not in permissions: - return Response({'code': 403, 'msg': '您无权查看俱乐部订单'}) - - # 4. 查询订单 - dingdan_obj = Dingdan.objects.select_related( - 'pingtai_kuozhan', - 'shangjia_kuozhan' - ).get(dingdan_id=dingdan_id) - - # 5. 构建基础响应数据 - response_data = { - 'dingdan_id': dingdan_obj.dingdan_id or '', - 'zhuangtai': dingdan_obj.zhuangtai or 0, - 'fadanpingtai': dingdan_obj.fadan_pingtai or 0, - 'dispatch_type': dingdan_obj.dispatch_type or 0, # 1=我方派单, 2=对方派单 - 'is_cross': dingdan_obj.is_cross or 0, - 'partner_order_id': dingdan_obj.partner_order_id or '', - 'partner_club_id': dingdan_obj.partner_club_id or '', - 'jine': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00, - 'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00, - 'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '', - 'dashou_liuyan': dingdan_obj.dashou_liuyan or '', - 'zhiding_id': dingdan_obj.zhiding_id or '', - 'shangpin_id': dingdan_obj.shangpin_id or '', - 'leixing': dingdan_obj.leixing_id or 0, - 'tupian_url': dingdan_obj.tupian or '', - 'jieshao': dingdan_obj.jieshao or '', - 'beizhu': dingdan_obj.beizhu or '', - 'tkly': dingdan_obj.tkly or '', - 'nicheng': dingdan_obj.nicheng or '', - 'clkf': dingdan_obj.clkf or '', - 'chuangjianshijian': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.create_time else '', - 'genggaishijian': dingdan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.update_time else '', - } - - # 6. 扩展表数据 - if dingdan_obj.fadan_pingtai == 1: - if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan: - response_data['pingtai_kuozhan'] = { - 'laoban_id': dingdan_obj.pingtai_kuozhan.laoban_id or '', - 'laoban_pingjia': dingdan_obj.pingtai_kuozhan.laoban_pingjia or '', - } - else: - response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''} - elif dingdan_obj.fadan_pingtai == 2: - if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan: - shangjia = dingdan_obj.shangjia_kuozhan - response_data['shangjia_kuozhan'] = { - 'shangjia_id': shangjia.shangjia_id or '', - 'sjnicheng': shangjia.sjnicheng or '', - 'shangjia_pingjia': shangjia.shangjia_pingjia or '', - 'cfliyou': shangjia.cfliyou or '', - 'sqzhuangtai': shangjia.sqzhuangtai if shangjia.sqzhuangtai is not None else 0, - 'bhliyou': shangjia.bhliyou or '', - } - else: - response_data['shangjia_kuozhan'] = { - 'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '', - 'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': '' - } - - # ========== 核心逻辑:判断派单/接单关系 ========== - dispatch_type = dingdan_obj.dispatch_type # 1=我方派单, 2=对方派单 - is_cross = dingdan_obj.is_cross - partner_club_id = dingdan_obj.partner_club_id - partner_order_id = dingdan_obj.partner_order_id - - # 获取我方俱乐部配置(用于本地存储桶域名) - my_club_config = ClubConfig.objects.filter(is_self=1).first() - my_storage_domain = my_club_config.storage_bucket_domain if my_club_config else '' - - # 初始化字段 - response_data['club_info'] = None # 对方俱乐部信息 - response_data['partner_images'] = [] # 打手提交图片(对方平台打手或我方打手) - response_data['punishment_info'] = {} # 处罚信息 - response_data['is_punished'] = False # 是否已处罚(用于前端禁用按钮) - response_data['receive_side'] = '' # 接单方:'self' 我方接单,'partner' 对方接单 - response_data['dispatch_side'] = '' # 派单方:'self' 我方派单,'partner' 对方派单 - - # 设置派单方 - if dispatch_type == 1: - response_data['dispatch_side'] = 'self' - elif dispatch_type == 2: - response_data['dispatch_side'] = 'partner' - else: - # 兼容旧数据:根据 partner_club_id 或 partner_order_id 推断 - if partner_club_id or partner_order_id: - response_data['dispatch_side'] = 'partner' - else: - response_data['dispatch_side'] = 'self' - - # 情况1:对方派单(对方平台发单,我方打手接单) - if response_data['dispatch_side'] == 'partner': - # 接单方为我方 - response_data['receive_side'] = 'self' - - # 获取对方俱乐部信息(用于展示) - if partner_club_id: - partner_config = ClubConfig.objects.filter(club_id=partner_club_id, is_self=0).first() - if partner_config: - response_data['club_info'] = { - 'club_id': partner_config.club_id, - 'club_nickname': partner_config.club_nickname or partner_config.club_id, - 'club_avatar': partner_config.club_avatar or '', - } - else: - response_data['club_info'] = {'club_id': partner_club_id, 'club_nickname': '', 'club_avatar': ''} - else: - response_data['club_info'] = None - - # 查询我方打手提交的图片(本地) - dashou_images = Dashoutupian.objects.filter( - dingdan_id=dingdan_id, - dashou=dingdan_obj.jiedan_dashou_id - ).values_list('tupian', flat=True) - images = [] - for rel_path in dashou_images: - if rel_path: - if rel_path.startswith('http'): - images.append(rel_path) - else: - images.append(my_storage_domain + rel_path) - response_data['partner_images'] = images - - # 查询我方打手处罚记录 - punish_records = Chufajilu.objects.filter( - dingdan_id=dingdan_id, - dashouid=dingdan_obj.jiedan_dashou_id - ).order_by('-create_time') - if punish_records.exists(): - latest = punish_records.first() - response_data['punishment_info'] = { - 'cfliyou': latest.cfliyou or '', - 'sqzhuangtai': latest.sqzhuangtai or 0, - 'bhliyou': latest.bhliyou or '', - 'ssliyou': latest.ssliyou or '', - 'jifen': latest.jifen or 0, - } - response_data['is_punished'] = True - else: - response_data['punishment_info'] = {} - response_data['is_punished'] = False - - # 情况2:我方派单(我方平台发单) - else: - response_data['dispatch_side'] = 'self' - # 根据是否有对方订单ID/俱乐部ID判断接单方 - if partner_club_id or partner_order_id: - # 有对方信息,说明订单派给了对方平台,对方接单 - response_data['receive_side'] = 'partner' - - # 获取对方俱乐部信息 - if partner_club_id: - partner_config = ClubConfig.objects.filter(club_id=partner_club_id, is_self=0).first() - if partner_config: - response_data['club_info'] = { - 'club_id': partner_config.club_id, - 'club_nickname': partner_config.club_nickname or partner_config.club_id, - 'club_avatar': partner_config.club_avatar or '', - } - else: - response_data['club_info'] = {'club_id': partner_club_id, 'club_nickname': '', 'club_avatar': ''} - - # 需要调用对方接口获取图片和处罚记录 - if partner_club_id and partner_order_id: - # 获取对方服务器域名 - if my_club_config: - self_club_id = my_club_config.club_id - else: - logger.error(f"无法获取本俱乐部ID,订单ID: {dingdan_id}") - return Response({'code': 500, 'msg': '系统配置错误,请联系管理员'}) - - try: - club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) - partner_domain = club_rel.partner_domain - except ObjectDoesNotExist: - logger.error(f"未找到合作关系: {self_club_id} <-> {partner_club_id}") - return Response({'code': 500, 'msg': '未找到对方平台配置'}) - - # 调用对方接口,最多重试3次 - url = partner_domain.rstrip('/') + '/houtai/kpthqtp' - payload = { - 'order_id': partner_order_id, - 'club_id': self_club_id, - } - timeout = 3 - retries = 3 - success = False - for attempt in range(retries): - try: - resp = requests.post(url, json=payload, timeout=timeout) - if resp.status_code == 200: - resp_json = resp.json() - if resp_json.get('code') == 0: - data = resp_json.get('data', {}) - response_data['partner_images'] = data.get('images', []) - response_data['punishment_info'] = data.get('punishment', {}) - response_data['is_punished'] = bool(response_data['punishment_info'].get('cfliyou')) - success = True - break - else: - logger.warning(f"对方接口返回错误 (尝试{attempt+1}): {resp_json.get('msg')}") - else: - logger.warning(f"对方接口HTTP错误 (尝试{attempt+1}): {resp.status_code}") - except requests.exceptions.Timeout: - logger.warning(f"请求对方接口超时 (尝试{attempt+1}): {url}") - except Exception as e: - logger.warning(f"请求对方接口异常 (尝试{attempt+1}): {str(e)}") - if not success: - # 三次失败,返回部分数据,但提示前端对方数据获取失败 - response_data['partner_images'] = [] - response_data['punishment_info'] = {} - response_data['is_punished'] = False - response_data['partner_data_error'] = '获取对方平台数据失败,请稍后重试' - else: - # 对方信息不完整,无法获取数据 - response_data['partner_images'] = [] - response_data['punishment_info'] = {} - response_data['is_punished'] = False - response_data['partner_data_error'] = '对方订单信息不完整' - else: - # 无对方信息,说明我方派单且我方接单 - response_data['receive_side'] = 'self' - # 我方打手提交图片(本地) - dashou_images = Dashoutupian.objects.filter( - dingdan_id=dingdan_id, - dashou=dingdan_obj.jiedan_dashou_id - ).values_list('tupian', flat=True) - images = [] - for rel_path in dashou_images: - if rel_path: - if rel_path.startswith('http'): - images.append(rel_path) - else: - images.append(my_storage_domain + rel_path) - response_data['partner_images'] = images - - # 处罚记录 - punish_records = Chufajilu.objects.filter( - dingdan_id=dingdan_id, - dashouid=dingdan_obj.jiedan_dashou_id - ).order_by('-create_time') - if punish_records.exists(): - latest = punish_records.first() - response_data['punishment_info'] = { - 'cfliyou': latest.cfliyou or '', - 'sqzhuangtai': latest.sqzhuangtai or 0, - 'bhliyou': latest.bhliyou or '', - 'ssliyou': latest.ssliyou or '', - 'jifen': latest.jifen or 0, - } - response_data['is_punished'] = True - else: - response_data['punishment_info'] = {} - response_data['is_punished'] = False - - # 如果是我方派单且我方接单,对方俱乐部信息为空 - response_data['club_info'] = None - - # 7. 查询退款理由 - try: - tuikuan_record = Tuikuanjilu.objects.filter( - dingdan_id=dingdan_id - ).order_by('-create_time').first() - response_data['tuikuan_liyou'] = tuikuan_record.liyou if tuikuan_record else '' - except Exception as e: - logger.warning(f"查询退款记录失败: {str(e)}") - response_data['tuikuan_liyou'] = '' - - # ========== 新增:查询更换打手生成的新订单信息 ========== - try: - from peizhi.models import ShangjiaLianjie - new_lianjie = ShangjiaLianjie.objects.filter( - previous_dingdan_id=dingdan_id - ).order_by('-create_time').first() - if new_lianjie: - response_data['new_dingdan_id'] = new_lianjie.dingdan_id or '' - response_data['new_lianjie'] = new_lianjie.lianjie or '' - else: - response_data['new_dingdan_id'] = '' - response_data['new_lianjie'] = '' - except Exception as e: - logger.warning(f"查询新订单链接失败: {str(e)}") - response_data['new_dingdan_id'] = '' - response_data['new_lianjie'] = '' - # ===================================================== - - elapsed = time.time() - start_time - logger.info(f"获取跨平台订单详情接口耗时: {elapsed:.3f}s, 订单ID: {dingdan_id}") - return Response({'code': 0, 'data': response_data}) - - except Dingdan.DoesNotExist: - logger.warning(f"订单不存在: {dingdan_id}") - return Response({'code': 404, 'msg': '订单不存在'}) - except Exception as e: - logger.error(f"获取跨平台订单详情接口异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - -class CrossPlatformRefundView(APIView): - """ - 跨平台订单退款接口(同时支持本地订单和跨平台订单) - 请求:POST /houtai/kptk - 参数:{ - "username": "客服账号ID", - "dingdan_id": "订单ID", - "tuikuan_liyou": "退款理由(可选)" - } - 认证:JWT - 返回:code=0 成功,其他失败 - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - username = request.data.get('username', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() - - if not username or not dingdan_id: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 身份验证 - current_user = request.user - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '003aa' not in permissions: - return Response({'code': 403, 'msg': '您无权处理订单'}) - - # 4. 查询订单(带扩展表) - try: - order = Dingdan.objects.select_related( - 'pingtai_kuozhan', - 'shangjia_kuozhan' - ).get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - logger.warning(f"订单不存在: {dingdan_id}") - return Response({'code': 404, 'msg': '订单不存在'}) - - # 5. 订单状态校验(只有 1,7,2,8,4 可退款) - allowed_statuses = [1, 7, 2, 8, 4] - if order.zhuangtai not in allowed_statuses: - logger.warning(f"订单状态不允许退款: {order.zhuangtai}") - return Response({'code': 400, 'msg': '订单状态不允许退款'}) - - # 6. 根据派单方向分流 - dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单 - - # ---------- 情况一:对方派单(我方打手接对方的单)---------- - if dispatch_type == 2: - # 对方派单,资金在对方平台,我方只处理打手状态和订单状态 - with transaction.atomic(): - # 更新订单状态为已退款 - order.zhuangtai = 5 - order.clkf = current_user.phone - if tuikuan_liyou: - order.tkly = tuikuan_liyou - order.save() - - # 更新打手数据(接单打手) - dashou_id = order.jiedan_dashou_id - if dashou_id: - try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.tuikuanliang += 1 - dashou_profile.zhuangtai = 1 # 设为空闲 - dashou_profile.save() - except (UserMain.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"对方派单退款:打手 {dashou_id} 不存在,跳过更新") - - # 更新退款记录(记录操作) - try: - refund_record, created = Tuikuanjilu.objects.get_or_create( - dingdan_id=dingdan_id, - defaults={ - 'dashouid': dashou_id or '', - 'qingqiuid': '', - 'chuliid': current_user.phone, - 'liyou': tuikuan_liyou or '客服退款(对方派单)', - 'sqzhuangtai': 1, - 'jine': order.jine or 0, - 'dashou_fencheng': order.dashou_fencheng or 0, - 'jieshao': order.jieshao or '', - 'beizhu': '对方派单退款', - 'nicheng': order.nicheng or '', - } - ) - if not created: - refund_record.sqzhuangtai = 1 - refund_record.chuliid = current_user.phone - if tuikuan_liyou: - refund_record.liyou = tuikuan_liyou - refund_record.save() - except Exception as e: - logger.error(f"更新退款记录失败: {str(e)}") - - # 🔥 新增:调用打手每日统计(成交) - try: - update_dashou_daily_by_action( - yonghuid=order.jiedan_dashou_id, - amount=order.dashou_fencheng, - action=3 # 3 表示退款(退款) - ) - logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - # 不影响主流程 - - - # ✅ 新方法(行为驱动,更安全) - from houtai.utils import update_shangjia_daily - shangjia_ext = order.shangjia_kuozhan - shangjia_id = shangjia_ext.shangjia_id - update_shangjia_daily( - yonghuid=shangjia_id, - amount=Decimal(str(order.jine)), - action=3 # 3 = 退款 - ) - - # 更新客服统计 - kefu.jinrichuli += 1 - kefu.jinyuechuli += 1 - kefu.zongchuli += 1 - kefu.save() - - logger.info(f"对方派单退款成功,订单 {dingdan_id}") - return Response({'code': 0, 'msg': '退款成功'}) - - # ---------- 情况二:我方派单(订单由我方发出)---------- - # 需要进一步判断是本地订单还是跨平台订单 - is_cross = order.is_cross - partner_club_id = order.partner_club_id - partner_order_id = order.partner_order_id - - # 子情况2.1:本地订单(非跨平台) - if not is_cross or not partner_club_id: - # 按原有逻辑处理:平台订单或商家订单 - if order.fadan_pingtai == 1: - # 平台订单退款(调用微信支付) - return self._refund_platform_order(order, current_user, tuikuan_liyou, kefu) - elif order.fadan_pingtai == 2: - # 商家订单退款(直接退余额) - return self._refund_merchant_order(order, current_user, tuikuan_liyou, kefu) - else: - return Response({'code': 400, 'msg': '未知的发单平台类型'}) - - # 子情况2.2:跨平台订单(我方派单给对方) - else: - # 必须先执行本地资金退款(因为钱在我方平台) - # 根据发单平台类型执行退款 - local_refund_success = False - try: - with transaction.atomic(): - if order.fadan_pingtai == 1: - # 平台订单:调用微信退款 - refund_result = self._call_wechat_refund(order) - if refund_result['code'] != 0: - raise Exception(refund_result['msg']) - # 微信退款成功,更新订单状态 - order.zhuangtai = 5 - order.clkf = current_user.phone - if tuikuan_liyou: - order.tkly = tuikuan_liyou - order.save() - local_refund_success = True - elif order.fadan_pingtai == 2: - # 商家订单:退余额给商家 - self._refund_to_merchant(order, current_user.phone, tuikuan_liyou) - order.zhuangtai = 5 - order.clkf = current_user.phone - if tuikuan_liyou: - order.tkly = tuikuan_liyou - order.save() - local_refund_success = True - else: - return Response({'code': 400, 'msg': '未知的发单平台类型'}) - - # 更新打手数据(接单打手) - dashou_id = order.jiedan_dashou_id - if dashou_id: - try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.tuikuanliang += 1 - dashou_profile.zhuangtai = 1 - dashou_profile.save() - except (UserMain.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"跨平台退款:打手 {dashou_id} 不存在,跳过更新") - - # 更新退款记录 - self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou) - - # 更新客服统计 - kefu.jinrichuli += 1 - kefu.jinyuechuli += 1 - kefu.zongchuli += 1 - kefu.save() - - except Exception as e: - logger.error(f"跨平台订单本地退款失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': f'本地退款失败: {str(e)}'}) - - # 本地退款成功后,通知对方平台 - if local_refund_success: - # 查询对方平台域名 - try: - # 获取我方俱乐部ID - my_club_config = ClubConfig.objects.filter(is_self=1).first() - if not my_club_config: - logger.error("未配置我方俱乐部信息") - # 本地已成功,仅记录错误,不影响返回 - else: - self_club_id = my_club_config.club_id - club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) - partner_domain = club_rel.partner_domain - - # 调用对方退款通知接口 - notify_url = partner_domain.rstrip('/') + '/houtai/dfddtk' - payload = { - 'order_id': partner_order_id, # 对方订单ID - 'club_id': self_club_id, # 我方俱乐部ID - 'status': 5, # 通知对方我方已退款 - 'reason': tuikuan_liyou - } - # 设置超时3秒,不阻塞主流程 - try: - resp = requests.post(notify_url, json=payload, timeout=3) - if resp.status_code == 200: - resp_json = resp.json() - if resp_json.get('code') == 0: - # 更新跨平台订单扩展表 - cross_data, _ = CrossPlatformOrderData.objects.get_or_create( - dingdan_id=dingdan_id, - defaults={ - 'partner_order_id': partner_order_id, - 'partner_club_id': partner_club_id, - 'partner_order_status': resp_json.get('data', {}).get('partner_status', 0), - 'partner_dashou_id': resp_json.get('data', {}).get('partner_dashou_id', ''), - 'partner_amount': order.jine or 0, - } - ) - if not _: - cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0) - cross_data.save() - else: - logger.warning(f"对方平台返回错误: {resp_json.get('msg')}") - else: - logger.warning(f"对方平台接口HTTP错误: {resp.status_code}") - except requests.exceptions.Timeout: - logger.error(f"通知对方平台超时: {notify_url}") - except Exception as e: - logger.error(f"通知对方平台异常: {str(e)}") - except Exception as e: - logger.error(f"查询对方平台配置失败: {str(e)}") - - return Response({'code': 0, 'msg': '退款成功'}) - - # ========== 辅助方法 ========== - def _refund_platform_order(self, order, current_user, tuikuan_liyou, kefu): - """平台订单退款(调用微信支付)""" - # 调用微信退款 - refund_result = self._call_wechat_refund(order) - if refund_result['code'] != 0: - return Response({'code': 400, 'msg': refund_result['msg']}) - - with transaction.atomic(): - order.zhuangtai = 5 - order.clkf = current_user.phone - if tuikuan_liyou: - order.tkly = tuikuan_liyou - order.save() - - # 更新打手 - dashou_id = order.jiedan_dashou_id - if dashou_id: - try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.tuikuanliang += 1 - dashou_profile.zhuangtai = 1 - dashou_profile.save() - except (UserMain.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"平台退款:打手 {dashou_id} 不存在,跳过更新") - - # 更新退款记录 - self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou) - - is_cross = order.is_cross - partner_club_id = order.partner_club_id - partner_order_id = order.partner_order_id - - if not is_cross or not partner_order_id: - - # 🔥 新增:调用打手每日统计(成交) - try: - update_dashou_daily_by_action( - yonghuid=order.jiedan_dashou_id, - amount=order.dashou_fencheng, - action=3 # 3 表示退款(退款) - ) - logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - # 不影响主流程 - else: - pass - - # 客服统计 - kefu.jinrichuli += 1 - kefu.jinyuechuli += 1 - kefu.zongchuli += 1 - kefu.save() - - logger.info(f"平台订单退款成功: {order.dingdan_id}") - return Response({'code': 0, 'msg': '退款成功'}) - - def _refund_merchant_order(self, order, current_user, tuikuan_liyou, kefu): - """商家订单退款(退余额给商家)""" - # 获取商家ID - try: - shangjia_ext = order.shangjia_kuozhan - shangjia_id = shangjia_ext.shangjia_id - except ObjectDoesNotExist: - return Response({'code': 400, 'msg': '订单商家信息不存在'}) - - with transaction.atomic(): - # 订单状态改已退款 - order.zhuangtai = 5 - order.clkf = current_user.phone - if tuikuan_liyou: - order.tkly = tuikuan_liyou - order.save() - - # 退余额给商家 - try: - shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) - shangjia_profile = shangjia_user.shop_profile - shangjia_profile.tuikuan += 1 - shangjia_profile.yue += order.jine or 0 - shangjia_profile.save() - except (UserMain.DoesNotExist, ObjectDoesNotExist): - logger.error(f"商家退款:商家 {shangjia_id} 不存在,无法退余额") - raise Exception("商家账户不存在") - - # 更新打手 - dashou_id = order.jiedan_dashou_id - if dashou_id: - try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.tuikuanliang += 1 - dashou_profile.zhuangtai = 1 - dashou_profile.save() - except (UserMain.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"商家退款:打手 {dashou_id} 不存在,跳过更新") - - # 更新退款记录 - self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou) - - - is_cross = order.is_cross - partner_club_id = order.partner_club_id - partner_order_id = order.partner_order_id - - if not is_cross or not partner_order_id: - - # 🔥 新增:调用打手每日统计(成交) - try: - update_dashou_daily_by_action( - yonghuid=order.jiedan_dashou_id, - amount=order.dashou_fencheng, - action=3 # 3 表示退款(退款) - ) - logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - # 不影响主流程 - else: - pass - - # ✅ 新方法(行为驱动,更安全) - from houtai.utils import update_shangjia_daily - update_shangjia_daily( - yonghuid=shangjia_id, - amount=Decimal(str(order.jine)), - action=3 # 3 = 退款 - ) - - # 客服统计 - kefu.jinrichuli += 1 - kefu.jinyuechuli += 1 - kefu.zongchuli += 1 - kefu.save() - - logger.info(f"商家订单退款成功: {order.dingdan_id}") - return Response({'code': 0, 'msg': '退款成功'}) - - def _call_wechat_refund(self, order): - """调用微信支付退款接口,返回 {'code':0, 'msg':'success'} 或错误信息""" - # 获取微信支付配置 - appid = getattr(settings, 'WEIXIN_APPID', '') - mch_id = getattr(settings, 'WEIXIN_MCHID', '') - key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') - cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '') - key_path = getattr(settings, 'WEIXIN_KEY_PATH', '') - - if not all([appid, mch_id, key, cert_path, key_path]): - missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v] - logger.error(f"微信支付配置缺失: {missing}") - return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'} - - # 生成退款单号 - out_refund_no = self._generate_refund_no(order.dingdan_id) - total_fee = int(float(order.jine or 0) * 100) - refund_fee = total_fee - refund_desc = '客服退款' - - params = { - 'appid': appid, - 'mch_id': mch_id, - 'nonce_str': self._generate_nonce_str(), - 'out_refund_no': out_refund_no, - 'total_fee': total_fee, - 'refund_fee': refund_fee, - 'refund_desc': refund_desc, - } - # 优先使用微信交易号 - if order.wechat_transaction_id: - params['transaction_id'] = order.wechat_transaction_id - else: - params['out_trade_no'] = order.dingdan_id - - # 签名 - sorted_keys = sorted(params.keys()) - stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys]) - stringSignTemp = f"{stringA}&key={key}" - sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() - params['sign'] = sign - - xml_data = self._dict_to_xml(params) - url = 'https://api.mch.weixin.qq.com/secapi/pay/refund' - - try: - response = requests.post(url, data=xml_data, cert=(cert_path, key_path), timeout=10) - result = xmltodict.parse(response.content)['xml'] - logger.info(f"微信退款响应: {result}") - except Exception as e: - logger.error(f"微信退款请求异常: {str(e)}", exc_info=True) - return {'code': 500, 'msg': '微信退款请求异常'} - - if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': - return {'code': 0, 'msg': 'success'} - else: - err_msg = result.get('err_code_des', result.get('return_msg', '未知错误')) - logger.warning(f"微信退款失败: {err_msg}") - return {'code': 400, 'msg': err_msg} - - def _refund_to_merchant(self, order, clkf_phone, tuikuan_liyou): - """给商家退余额(事务内调用,不再独立事务)""" - try: - shangjia_ext = order.shangjia_kuozhan - shangjia_id = shangjia_ext.shangjia_id - except ObjectDoesNotExist: - raise Exception("订单商家信息不存在") - - try: - shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) - shangjia_profile = shangjia_user.shop_profile - shangjia_profile.tuikuan += 1 - shangjia_profile.yue += order.jine or 0 - shangjia_profile.save() - except (UserMain.DoesNotExist, ObjectDoesNotExist): - logger.error(f"商家退款:商家 {shangjia_id} 不存在") - raise Exception("商家账户不存在") - - def _update_refund_record(self, order, dashou_id, chuli_phone, tuikuan_liyou): - """更新退款记录表""" - try: - #更新支出记录 - if order.fadan_pingtai == 1: - from dingdan.utils import update_daily_payout - update_daily_payout(order.jine) - - # ========== 新增:商品和店铺每日统计 ========== - try: - from shangdian.utils import update_dianpu_daily_stat - from shangpin.utils import update_shangpin_daily_stat - # 商品每日统计(只要有商品ID就执行) - if order.shangpin_id: - # 从扩展表获取已计算好的收益值(下单时已存储) - pingtai_shouyi = Decimal('0.00') - dianpu_shouyi = Decimal('0.00') - if hasattr(order, 'pingtai_kuozhan'): - pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') - dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - - update_shangpin_daily_stat( - shangpin_id=order.shangpin_id, - caozuo_leixing=3, # 3 = 退款 - dingdan_jiage=order.jine, - pingtai_shouyi=pingtai_shouyi, - dianpu_zongshouyi=dianpu_shouyi - ) - logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") - - # 店铺每日统计(仅当存在店铺ID时执行) - dianpu_id = None - if hasattr(order, 'pingtai_kuozhan'): - dianpu_id = order.pingtai_kuozhan.dianpu_id - if dianpu_id: - dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - update_dianpu_daily_stat( - dianpu_id=dianpu_id, - caozuo_leixing=3, # 3 = 退款 - dingdan_jiage=order.jine, - dianpu_shouyi=dianpu_shouyi - ) - logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") - except Exception as e: - logger.error(f"商品/店铺统计更新失败: {e}") - logger.error(traceback.format_exc()) - # 统计失败不影响主流程 - # ============================================= - - - refund_record, created = Tuikuanjilu.objects.get_or_create( - dingdan_id=order.dingdan_id, - defaults={ - 'dashouid': dashou_id or '', - 'qingqiuid': '', - 'chuliid': chuli_phone, - 'liyou': tuikuan_liyou or '客服退款', - 'sqzhuangtai': 1, - 'jine': order.jine or 0, - 'dashou_fencheng': order.dashou_fencheng or 0, - 'jieshao': order.jieshao or '', - 'beizhu': '客服退款', - 'nicheng': order.nicheng or '', - } - ) - if not created: - refund_record.sqzhuangtai = 1 - refund_record.chuliid = chuli_phone - if tuikuan_liyou: - refund_record.liyou = tuikuan_liyou - refund_record.save() - except Exception as e: - logger.error(f"更新退款记录异常: {str(e)}") - - def _generate_nonce_str(self): - return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - - def _generate_refund_no(self, dingdan_id): - timestamp = str(int(time.time())) - rand = ''.join(random.choices('0123456789', k=4)) - return f"{dingdan_id}REF{timestamp}{rand}" - - def _dict_to_xml(self, data): - xml = [''] - for k, v in data.items(): - xml.append(f'<{k}>{v}') - xml.append('') - return ''.join(xml) - - - - -class PartnerGetOrderDataView(APIView): - """ - 对方平台调用,获取我方订单的打手图片和处罚记录 - 请求:POST /houtai/kpthqtp - 参数:{ - "order_id": "对方平台的订单ID", - "club_id": "对方俱乐部ID" - } - 无需认证 - 返回:code=0 + images列表 + punishment信息 - """ - permission_classes = [AllowAny] - parser_classes = [JSONParser] - - def post(self, request): - # token = request.data.get('token') - # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): - # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200) - try: - # 1. 获取参数 - partner_order_id = request.data.get('order_id', '').strip() - partner_club_id = request.data.get('club_id', '').strip() - if not partner_order_id or not partner_club_id: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 查询我方存储桶域名(自己俱乐部的配置) - my_club_config = ClubConfig.objects.filter(is_self=1).first() - if not my_club_config: - logger.error("未配置我方俱乐部存储桶域名") - return Response({'code': 500, 'msg': '系统配置错误'}) - storage_domain = my_club_config.storage_bucket_domain or '' - - # 3. 查询我方订单(通过对方订单ID和对方俱乐部ID) - try: - order = Dingdan.objects.get( - dingdan_id=partner_order_id, - partner_club_id=partner_club_id - ) - except Dingdan.DoesNotExist: - logger.warning(f"订单不存在: partner_order_id={partner_order_id}, partner_club_id={partner_club_id}") - return Response({'code': 404, 'msg': '订单不存在'}) - - dingdan_id = order.dingdan_id - dashou_id = order.jiedan_dashou_id - - # 4. 查询打手图片(绝对URL) - images = [] - if dashou_id: - dashou_images = Dashoutupian.objects.filter( - dingdan_id=dingdan_id, - dashou=dashou_id - ).values_list('tupian', flat=True) - for rel_path in dashou_images: - if rel_path: - if rel_path.startswith('http'): - images.append(rel_path) - else: - images.append(storage_domain + rel_path) - - # 5. 查询处罚记录(最新一条) - punishment = { - 'cfliyou': '', - 'sqzhuangtai': 0, - 'bhliyou': '', - 'ssliyou': '', - 'jifen': 0 - } - if dashou_id: - punish_record = Chufajilu.objects.filter( - dingdan_id=dingdan_id, - dashouid=dashou_id - ).order_by('-create_time').first() - if punish_record: - punishment = { - 'cfliyou': punish_record.cfliyou or '', - 'sqzhuangtai': punish_record.sqzhuangtai or 0, - 'bhliyou': punish_record.bhliyou or '', - 'ssliyou': punish_record.ssliyou or '', - 'jifen': punish_record.jifen or 0, - } - - # 6. 返回数据 - return Response({ - 'code': 0, - 'msg': 'success', - 'data': { - 'images': images, - 'punishment': punishment - } - }) - - except Exception as e: - logger.error(f"对方获取订单数据接口异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - -# ========== 新增:对方平台退款通知接口 ========== -class PartnerRefundNotifyView(APIView): - """ - 接收对方平台推送的订单状态变更通知(完成/退款),同步更新我方数据。 - 请求:POST /houtai/dfddtk - 参数: - { - "order_id": "对方平台订单ID(实际为我方订单ID)", - "club_id": "对方俱乐部ID", - "status": 3 或 5, - "reason": "理由(可选)" - } - 返回:code=0 + data包含我方订单当前状态和打手ID - - 核心职责: - 1. 更新跨平台订单扩展表 - 2. 对于对方派单(我方打手接对方的单): - a. 状态3(完成)→ 更新订单为已完成,创建预结算(如无) - b. 状态5(退款)→ 更新订单为已退款,将预结算待结算改为已拒绝 - c. 在此过程中,若订单原状态为“进行中(2)”,则将打手状态重置为空闲(1) - """ - - permission_classes = [AllowAny] - parser_classes = [JSONParser] - - def post(self, request): - # token = request.data.get('token') - # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): - # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200) - # 1. 参数校验(事务外执行) - partner_order_id = request.data.get('order_id', '').strip() - partner_club_id = request.data.get('club_id', '').strip() - partner_status = request.data.get('status') - reason = request.data.get('reason', '').strip() - - if not partner_order_id or not partner_club_id: - return Response({'code': 400, 'msg': '参数不完整'}) - if partner_status not in (3, 5): - return Response({'code': 400, 'msg': '不支持的状态码'}) - - # 2. 核心业务在事务内执行 - try: - with transaction.atomic(): - # 2.1 锁定订单行(必须事务内) - try: - order = Dingdan.objects.select_for_update().get( - dingdan_id=partner_order_id, - partner_club_id=partner_club_id - ) - except Dingdan.DoesNotExist: - logger.warning(f"通知订单不存在: order_id={partner_order_id}, club_id={partner_club_id}") - return Response({'code': 404, 'msg': '订单不存在'}) - - my_dingdan_id = order.dingdan_id - dashou_id = order.jiedan_dashou_id or '' - - # 记录原订单状态,用于后续打手状态重置 - original_order_status = order.zhuangtai - - # 2.2 更新跨平台扩展表(记录对方状态) - cross_data, created = CrossPlatformOrderData.objects.get_or_create( - dingdan_id=my_dingdan_id, - defaults={ - 'partner_order_id': partner_order_id, - 'partner_club_id': partner_club_id, - 'partner_order_status': partner_status, - 'partner_dashou_id': '', - 'partner_amount': order.jine or 0, - } - ) - if not created: - cross_data.partner_order_status = partner_status - cross_data.save(update_fields=['partner_order_status', 'update_time']) - - # 2.3 仅处理对方派单(dispatch_type == 2) - if order.dispatch_type == 2: - # ========== 打手状态恢复逻辑 ========== - # 如果原订单状态为“进行中(2)”,且存在接单打手,则将打手状态改为空闲(1) - if original_order_status == 2 and dashou_id: - try: - # 通过打手ID查用户主表,再反向获取打手扩展表 - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.zhuangtai = 1 # 状态置为空闲 - dashou_profile.save(update_fields=['zhuangtai']) - logger.info(f"打手 {dashou_id} 状态已重置为1(空闲)") - except UserMain.DoesNotExist: - logger.warning(f"打手用户不存在: {dashou_id}") - except UserDashou.DoesNotExist: - logger.warning(f"打手扩展信息不存在: {dashou_id}") - # 🔥 新增:调用打手每日统计(成交) - if partner_status ==3: - - try: - update_dashou_daily_by_action( - yonghuid=order.jiedan_dashou_id, - amount=order.dashou_fencheng, - action=2 # 3 表示退款(退款) - ) - logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - if partner_status ==5: - try: - update_dashou_daily_by_action( - yonghuid=order.jiedan_dashou_id, - amount=order.dashou_fencheng, - action=3 # 3 表示退款(退款) - ) - logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - - # ========== 情况A:对方完成(status=3) ========== - if partner_status == 3: - order.zhuangtai = 3 - - order.clkf = partner_club_id # 处理人(对方俱乐部ID) - order.save(update_fields=['zhuangtai', 'update_time','clkf']) - - if dashou_id: - yjs, yjs_created = PreSettlement.objects.get_or_create( - dingdan_id=my_dingdan_id, - dashou_id=dashou_id, - defaults={ - 'partner_order_id': partner_order_id, - 'partner_club_id': partner_club_id, - 'amount': order.dashou_fencheng or 0, - 'order_amount': order.jine or 0, - 'status': 0, # 待结算 - 'operator': '', - } - ) - if yjs_created: - logger.info(f"预结算创建成功: {my_dingdan_id}, 打手 {dashou_id}, 金额 {order.dashou_fencheng}") - else: - logger.info(f"预结算已存在,跳过: {my_dingdan_id}, 打手 {dashou_id}") - else: - logger.warning(f"订单 {my_dingdan_id} 无接单打手,跳过预结算创建") - - # ========== 情况B:对方退款(status=5) ========== - elif partner_status == 5: - order.zhuangtai = 5 - order.tkly = reason if reason else '' # 退款理由 - order.clkf = partner_club_id # 处理人(对方俱乐部ID) - order.save(update_fields=['zhuangtai', 'update_time','clkf','tkly']) - - if dashou_id: - try: - yjs = PreSettlement.objects.get( - dingdan_id=my_dingdan_id, - dashou_id=dashou_id - ) - if yjs.status == 0: # 待结算 → 已拒绝 - yjs.status = 2 - yjs.process_time = timezone.now() - yjs.save(update_fields=['status', 'process_time', 'update_time']) - logger.info(f"预结算状态已改为已拒绝: {my_dingdan_id}") - except PreSettlement.DoesNotExist: - logger.info(f"订单 {my_dingdan_id} 无预结算记录,无需修改") - - logger.info(f"对方通知处理完成: dingdan_id={my_dingdan_id}, partner_status={partner_status}") - - # 返回结果 - return Response({ - 'code': 0, - 'msg': 'success', - 'data': { - 'partner_status': order.zhuangtai, - 'partner_dashou_id': dashou_id - } - }) - - except Exception as e: - logger.error(f"对方通知接口异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - - - - -from houtai.utils import update_dashou_daily_by_action - -class CrossPlatformForceCompleteView(APIView): - """ - 跨平台订单强制结单接口 - 请求:POST /houtai/kpqzjd - 参数:{"username": "客服账号ID", "dingdan_id": "订单ID"} - 认证:JWT - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - username = request.data.get('username', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - if not username or not dingdan_id: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 身份验证 - current_user = request.user - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '003aa' not in permissions: - return Response({'code': 403, 'msg': '您无权查处理订单'}) - - # 4. 查询订单并加锁 - try: - with transaction.atomic(): - order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) - - # 5. 校验订单状态必须为8(结算中) - if order.zhuangtai not in (2,8): - return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'}) - - # 获取关键字段 - dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单 - is_cross = order.is_cross # 0=本地,1=跨平台 - partner_club_id = order.partner_club_id - partner_order_id = order.partner_order_id - dashou_id = order.jiedan_dashou_id - dashou_fencheng = order.dashou_fencheng or Decimal('0') - jine = order.jine or Decimal('0') - fadan_pingtai = order.fadan_pingtai # 1=平台,2=商家 - - # ========== 修复点1:完善跨平台判断 ========== - # 跨平台且被对方打手接单的条件:is_cross==1 且 (partner_club_id 或 partner_order_id 有值) - is_cross_claimed_by_partner = (is_cross == 1 and (partner_club_id or partner_order_id)) - # 如果是跨平台订单但两个字段都为空,则按本地订单处理(即自家打手接单) - - # ========== 修复点2:根据方向处理 ========== - # ---------- 对方派单(我方打手接对方单)---------- - if dispatch_type == 2: - # 创建预结算记录(如果已存在则更新) - pre_settlement, created = PreSettlement.objects.get_or_create( - dingdan_id=dingdan_id, - defaults={ - 'partner_order_id': partner_order_id, - 'partner_club_id': partner_club_id, - 'dashou_id': dashou_id, - 'amount': dashou_fencheng, - 'order_amount': jine, - 'status': 0, # 待结算 - 'operator': current_user.phone, - } - ) - if not created: - pre_settlement.partner_order_id = partner_order_id - pre_settlement.partner_club_id = partner_club_id - pre_settlement.dashou_id = dashou_id - pre_settlement.amount = dashou_fencheng - pre_settlement.order_amount = jine - pre_settlement.status = 0 - pre_settlement.process_time = None - pre_settlement.operator = current_user.phone - pre_settlement.save() - - # 更新订单状态 - order.zhuangtai = 3 - order.clkf = current_user.phone - order.save() - - # 更新每日统计表(对方派单方向,使用订单金额) - if partner_club_id: - # 修复点3:使用公共统计函数 - update_daily_dispatch_stat( - direction=2, - partner_club_id=partner_club_id, - success_amount=jine, - success_count=1 - ) - else: - # 如果没有俱乐部ID,使用默认000001 - update_daily_dispatch_stat( - direction=2, - partner_club_id='000001', - success_amount=jine, - success_count=1 - ) - - # 通知对方平台订单已完成(修复点4:移到事务外) - if partner_club_id and partner_order_id: - transaction.on_commit( - lambda: self._notify_partner_order_status( - partner_club_id=partner_club_id, - partner_order_id=partner_order_id, - new_status=3, - current_user=current_user - ) - ) - - # ---------- 我方派单 ---------- - elif dispatch_type == 1: - # 判断是否为跨平台订单且被对方打手接单(即 partner_club_id 或 partner_order_id 有值) - if is_cross_claimed_by_partner: - # 跨平台订单:我方派单给外平台打手 - # 修复点5:处理只有 partner_order_id 没有 partner_club_id 的情况 - real_partner_club_id = partner_club_id or '000001' - - # 更新订单状态 - order.zhuangtai = 3 - order.clkf = current_user.phone - order.save() - - # 更新每日统计表(我方派单方向,使用打手分成金额) - update_daily_dispatch_stat( - direction=1, - partner_club_id=real_partner_club_id, - success_amount=dashou_fencheng, - success_count=1 - ) - - - # ========== 新增:商品和店铺每日统计 ========== - if order.fadan_pingtai == 1: - try: - from shangdian import update_dianpu_daily_stat - from shangpin import update_shangpin_daily_stat - # 商品每日统计(只要有商品ID就执行) - if order.shangpin_id: - # 从扩展表获取已计算好的收益值(下单时已存储) - pingtai_shouyi = Decimal('0.00') - dianpu_shouyi = Decimal('0.00') - if hasattr(order, 'pingtai_kuozhan'): - pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') - dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - - update_shangpin_daily_stat( - shangpin_id=order.shangpin_id, - caozuo_leixing=3, # 3 = 退款 - dingdan_jiage=order.jine, - pingtai_shouyi=pingtai_shouyi, - dianpu_zongshouyi=dianpu_shouyi - ) - logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") - - # 店铺每日统计(仅当存在店铺ID时执行) - dianpu_id = None - if hasattr(order, 'pingtai_kuozhan'): - dianpu_id = order.pingtai_kuozhan.dianpu_id - if dianpu_id: - dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - update_dianpu_daily_stat( - dianpu_id=dianpu_id, - caozuo_leixing=3, # 3 = 退款 - dingdan_jiage=order.jine, - dianpu_shouyi=dianpu_shouyi - ) - logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") - except Exception as e: - logger.error(f"商品/店铺统计更新失败: {e}") - logger.error(traceback.format_exc()) - # 统计失败不影响主流程 - # ============================================= - - # 通知对方平台订单已完成(异步) - if partner_order_id: - transaction.on_commit( - lambda: self._notify_partner_order_status( - partner_club_id=real_partner_club_id, - partner_order_id=partner_order_id, - new_status=3, - current_user=current_user - ) - ) - else: - # 本地订单(非跨平台 或 跨平台但实际是自家打手接单),按原有逻辑给自家打手结算 - if not dashou_id: - return Response({'code': 400, 'msg': '订单无接单打手'}) - if dashou_fencheng <= 0: - return Response({'code': 400, 'msg': '打手分成金额无效'}) - - # ========== 新增:商品和店铺每日统计 ========== - if order.fadan_pingtai == 1: - try: - from shangdian.utils import update_dianpu_daily_stat - from shangpin.utils import update_shangpin_daily_stat - # 商品每日统计(只要有商品ID就执行) - if order.shangpin_id: - # 从扩展表获取已计算好的收益值(下单时已存储) - pingtai_shouyi = Decimal('0.00') - dianpu_shouyi = Decimal('0.00') - if hasattr(order, 'pingtai_kuozhan'): - pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') - dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - - update_shangpin_daily_stat( - shangpin_id=order.shangpin_id, - caozuo_leixing=3, # 3 = 退款 - dingdan_jiage=order.jine, - pingtai_shouyi=pingtai_shouyi, - dianpu_zongshouyi=dianpu_shouyi - ) - logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") - - # 店铺每日统计(仅当存在店铺ID时执行) - dianpu_id = None - if hasattr(order, 'pingtai_kuozhan'): - dianpu_id = order.pingtai_kuozhan.dianpu_id - if dianpu_id: - dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - update_dianpu_daily_stat( - dianpu_id=dianpu_id, - caozuo_leixing=3, # 3 = 退款 - dingdan_jiage=order.jine, - dianpu_shouyi=dianpu_shouyi - ) - logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") - except Exception as e: - logger.error(f"商品/店铺统计更新失败: {e}") - logger.error(traceback.format_exc()) - # 统计失败不影响主流程 - # ============================================= - - # 更新打手余额和统计 - try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.chengjiaozongliang += 1 - dashou_profile.yue += dashou_fencheng - dashou_profile.zonge += dashou_fencheng - dashou_profile.jinrishouyi += dashou_fencheng - dashou_profile.jinyueshouyi += dashou_fencheng - dashou_profile.save() - except (UserMain.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"本地订单结算:打手 {dashou_id} 不存在,跳过") - - # 如果是商家订单,更新商家成交单量 - if fadan_pingtai == 2: - try: - shangjia_ext = order.shangjia_kuozhan - if shangjia_ext and shangjia_ext.shangjia_id: - shangjia_user = UserMain.objects.get(yonghuid=shangjia_ext.shangjia_id) - shangjia_profile = shangjia_user.shop_profile - shangjia_profile.chengjiao += 1 - shangjia_profile.save() - except Exception as e: - logger.warning(f"商家订单更新失败: {str(e)}") - - # 更新订单状态 - order.zhuangtai = 3 - order.clkf = current_user.phone - order.save() - - # 本地订单不涉及跨平台统计,无需更新 DailyDispatchStat - - else: - return Response({'code': 400, 'msg': '未知的派单方向'}) - - - if dispatch_type == 1 and is_cross_claimed_by_partner: - pass - # 判断是否为跨平台订单且被对方打手接单(即 partner_club_id 或 partner_order_id 有值) - elif dispatch_type == 2: - pass - else: - - - # 🔥 新增:调用打手每日统计(成交) - try: - update_dashou_daily_by_action( - yonghuid=order.jiedan_dashou_id, - amount=dashou_fencheng, - action=2 # 2 表示成交(结算) - ) - logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - # 不影响主流程 - - if order.fadan_pingtai == 2: - shangjia_ext = order.shangjia_kuozhan - # ✅ 新方法(行为驱动,更安全) - from houtai.utils import update_shangjia_daily - update_shangjia_daily( - yonghuid=shangjia_ext.shangjia_id, - amount=Decimal(str(order.jine)), - action=2 # 2 = 结算 - ) - else: - pass - - # 7. 更新客服统计 - kefu.jinrichuli += 1 - kefu.jinyuechuli += 1 - kefu.zongchuli += 1 - kefu.save() - - # 8. 记录操作日志 - logger.info(f"强制结单成功,订单 {dingdan_id},方向 {dispatch_type},客服 {username}") - - return Response({'code': 0, 'msg': '强制结单成功'}) - - except Dingdan.DoesNotExist: - logger.warning(f"订单不存在: {dingdan_id}") - return Response({'code': 404, 'msg': '订单不存在'}) - except Exception as e: - logger.error(f"强制结单接口异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - # ========== 辅助方法 ========== - def _notify_partner_order_status(self, partner_club_id, partner_order_id, new_status, current_user): - """ - 通知对方平台订单状态变更(已在事务外调用) - """ - try: - # 获取我方俱乐部ID - my_club_config = ClubConfig.objects.filter(is_self=1).first() - if not my_club_config: - logger.error("未配置我方俱乐部信息,无法通知对方") - return - self_club_id = my_club_config.club_id - - # 查询对方服务器域名 - club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) - partner_domain = club_rel.partner_domain - if not partner_domain: - logger.error(f"对方俱乐部 {partner_club_id} 未配置域名") - return - - url = partner_domain.rstrip('/') + '/houtai/dfddtk' - payload = { - 'order_id': partner_order_id, - 'club_id': self_club_id, - 'status': new_status - } - resp = requests.post(url, json=payload, timeout=3) - if resp.status_code == 200: - resp_json = resp.json() - if resp_json.get('code') == 0: - # 更新跨平台订单扩展表中的对方状态 - cross_data, _ = CrossPlatformOrderData.objects.get_or_create( - dingdan_id=partner_order_id, - defaults={ - 'partner_order_id': partner_order_id, - 'partner_club_id': partner_club_id, - 'partner_order_status': new_status, - 'partner_dashou_id': '', - 'partner_amount': Decimal('0') - } - ) - if not _: - cross_data.partner_order_status = new_status - cross_data.save(update_fields=['partner_order_status']) - logger.info(f"通知对方订单状态成功: {partner_order_id} -> {new_status}") - else: - logger.warning(f"对方接口返回错误: {resp_json.get('msg')}") - else: - logger.warning(f"对方接口HTTP错误: {resp.status_code}") - except requests.exceptions.Timeout: - logger.error(f"通知对方平台超时: {partner_club_id}") - except Exception as e: - logger.error(f"通知对方平台异常: {str(e)}") - - - - -class CrossPlatformTransferHallView(APIView): - """ - 跨平台订单转移大厅接口 - 请求:POST /houtai/kpzydt - 参数:{"username": "客服账号ID", "dingdan_id": "订单ID"} - 认证:JWT - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - username = request.data.get('username', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - if not username or not dingdan_id: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 客服身份验证 - current_user = request.user - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '003aa' not in permissions: - return Response({'code': 403, 'msg': '您无权处理俱乐部订单'}) - - # 4. 查询订单并加锁 - try: - with transaction.atomic(): - order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) - - # 5. 状态校验 - if order.zhuangtai not in [2, 8, 4]: - return Response({'code': 400, 'msg': f'订单状态不允许转移大厅,当前状态: {order.zhuangtai}'}) - - # 获取关键字段 - is_cross = order.is_cross - dispatch_type = order.dispatch_type - partner_club_id = order.partner_club_id - partner_order_id = order.partner_order_id - current_dashou_id = order.jiedan_dashou_id - zhiding_id = order.zhiding_id - - # ========== 判断是否需要通知对方平台 ========== - # 条件:跨平台 且 我方派单(dispatch_type=1) 且 (partner_club_id 或 partner_order_id 有值) - need_notify = (is_cross == 1 and (partner_club_id or partner_order_id)) - - # ========== 判断是否需要修改本地打手状态 ========== - # 当接单打手是我方打手时,才需要修改状态(改为空闲1) - # 我方打手的情况: - # 1. 非跨平台订单 (is_cross != 1) - # 2. 跨平台且对方派单 (dispatch_type == 2) —— 此时打手是我方 - # 3. 跨平台且我方派单但没有对方信息 (即 need_notify == False) —— 此时打手也是我方 - need_update_dashou_status = (current_dashou_id and - (is_cross != 1 or dispatch_type == 2 or not need_notify)) - - # ========== 记录打手历史(无论打手是谁,只要有ID就记录) ========== - if current_dashou_id: - last_history = OrderDashouHistory.objects.filter( - dingdan_id=dingdan_id - ).order_by('-times').first() - next_times = (last_history.times + 1) if last_history else 1 - OrderDashouHistory.objects.create( - dingdan_id=dingdan_id, - dashou_id=current_dashou_id, - times=next_times, - operator=username - ) - - # ========== 修改本地打手状态(仅当是我方打手时) ========== - if need_update_dashou_status: - try: - dashou_profile = UserDashou.objects.get(user__yonghuid=current_dashou_id) - dashou_profile.zhuangtai = 1 - dashou_profile.save() - logger.info(f"打手 {current_dashou_id} 状态改为空闲") - except ObjectDoesNotExist: - logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新") - - # ========== 更新订单状态(清空打手,改为1或7) ========== - if zhiding_id: - order.zhuangtai = 7 # 指定中 - else: - order.zhuangtai = 1 # 下单中 - order.jiedan_dashou_id = None - order.clkf = username - order.save() - - # ========== 如果需要通知对方平台,异步发送通知 ========== - if need_notify: - # 确定对方俱乐部ID(如果没有则用默认000001) - real_partner_club_id = partner_club_id if partner_club_id else '000001' - real_partner_order_id = partner_order_id if partner_order_id else dingdan_id - - # 获取我方俱乐部ID - my_club_config = ClubConfig.objects.filter(is_self=1).first() - if my_club_config: - self_club_id = my_club_config.club_id - # 查询对方域名 - try: - club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=real_partner_club_id) - partner_domain = club_rel.partner_domain - if partner_domain: - url = partner_domain.rstrip('/') + '/houtai/kpttzzydt' - payload = { - 'order_id': real_partner_order_id, - 'club_id': self_club_id - } - - def notify(): - try: - resp = requests.post(url, json=payload, timeout=3) - if resp.status_code == 200: - resp_json = resp.json() - if resp_json.get('code') == 0: - # 更新跨平台订单扩展表中的对方状态 - cross_data, created = CrossPlatformOrderData.objects.get_or_create( - dingdan_id=dingdan_id, - defaults={ - 'partner_order_id': real_partner_order_id, - 'partner_club_id': real_partner_club_id, - 'partner_order_status': resp_json.get('data', {}).get('partner_status', 0), - 'partner_dashou_id': '', - 'partner_amount': order.jine or 0, - } - ) - if not created: - cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0) - cross_data.save(update_fields=['partner_order_status']) - logger.info(f"转移大厅通知对方成功: {dingdan_id}") - - # ========== 新增代码开始 ========== - # 对方成功转移大厅,清空我方订单中的对方相关信息 - # ========== 新增代码开始 ========== - # 只有我方派单时才清空对方平台信息,对方派单则保留 - if dispatch_type == 1: - Dingdan.objects.filter(dingdan_id=dingdan_id).update( - partner_club_id=None, - partner_order_id=None, - jiedan_dashou_id=None, - zhuangtai=1 - ) - logger.info(f"我方派单,已清空订单 {dingdan_id} 的对方信息") - else: - # 对方派单,只清空打手并恢复状态,保留对方信息 - Dingdan.objects.filter(dingdan_id=dingdan_id).update( - jiedan_dashou_id=None, - zhuangtai=1 - ) - logger.info(f"对方派单,只清空打手,保留对方信息,订单 {dingdan_id}") - # ========== 新增代码结束 ========== - # ========== 新增代码结束 ========== - - else: - logger.warning(f"对方平台返回错误: {resp_json.get('msg')}") - else: - logger.warning(f"对方接口HTTP错误: {resp.status_code}") - except Exception as e: - logger.error(f"转移大厅通知对方异常: {str(e)}") - - threading.Thread(target=notify).start() - except Club.DoesNotExist: - logger.error(f"未找到对方俱乐部配置: partner_club_id={real_partner_club_id}") - except Exception as e: - logger.error(f"查询对方平台配置失败: {str(e)}") - - # ========== 更新客服统计 ========== - kefu.jinrichuli += 1 - kefu.jinyuechuli += 1 - kefu.zongchuli += 1 - kefu.save() - - return Response({'code': 0, 'msg': '转移大厅成功'}) - - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}) - except Exception as e: - logger.error(f"转移大厅接口异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - -class PartnerTransferHallNotifyView(APIView): - """ - 对方平台转移大厅通知接口 - 对方调用此接口通知我方订单已转移大厅,我方同步更新订单状态 - 请求:POST /houtai/kpttzzydt - 参数:{ - "order_id": "对方平台的订单ID", - "club_id": "对方俱乐部ID" - } - 无需认证 - 返回:{ - "code": 0, - "msg": "success", - "data": { - "partner_status": 1 # 我方订单当前状态 - } - } - """ - parser_classes = [JSONParser] - permission_classes = [AllowAny] - - def post(self, request): - # token = request.data.get('token') - # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): - # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200) - try: - # 1. 获取参数 - partner_order_id = request.data.get('order_id', '').strip() - partner_club_id = request.data.get('club_id', '').strip() - if not partner_order_id or not partner_club_id: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 查询我方订单 - try: - order = Dingdan.objects.get( - dingdan_id=partner_order_id, - partner_club_id=partner_club_id - ) - except Dingdan.DoesNotExist: - logger.warning(f"对方转移大厅通知:订单不存在 partner_order_id={partner_order_id}, partner_club_id={partner_club_id}") - return Response({'code': 404, 'msg': '订单不存在'}) - - # 3. 加锁处理 - with transaction.atomic(): - order = Dingdan.objects.select_for_update().get(id=order.id) - - current_status = order.zhuangtai - current_dashou_id = order.jiedan_dashou_id - - # 4. 根据订单状态处理 - if current_status == 3: - # 已完成状态,需要检查预结算记录 - try: - pre_settlement = PreSettlement.objects.select_for_update().get( - dingdan_id=order.dingdan_id, - dashou_id=current_dashou_id - ) - # 如果预结算状态是0(待结算),改为2(已拒绝) - if pre_settlement.status == 0: - pre_settlement.status = 2 - pre_settlement.process_time = timezone.now() - pre_settlement.operator = partner_club_id # 记录操作方 - pre_settlement.save() - logger.info(f"预结算记录已拒绝: {order.dingdan_id}") - else: - logger.info(f"预结算记录已是其他状态 {pre_settlement.status},不重复修改") - except PreSettlement.DoesNotExist: - logger.warning(f"订单 {order.dingdan_id} 已完成但无预结算记录,无法转移大厅") - return Response({'code': 400, 'msg': '订单已完成且无预结算记录,无法转移大厅'}) - - # 5. 记录打手历史(如果有接单打手) - if current_dashou_id: - last_history = OrderDashouHistory.objects.filter( - dingdan_id=order.dingdan_id - ).order_by('-times').first() - next_times = (last_history.times + 1) if last_history else 1 - OrderDashouHistory.objects.create( - dingdan_id=order.dingdan_id, - dashou_id=current_dashou_id, - times=next_times, - operator=partner_club_id # 记录操作方 - ) - # 将打手状态改为空闲 - try: - dashou_profile = UserDashou.objects.get(user__yonghuid=current_dashou_id) - dashou_profile.zhuangtai = 1 - dashou_profile.save() - - # 打手退款每日统计 - if order.dispatch_type == 2: - try: - update_dashou_daily_by_action( - yonghuid=current_dashou_id, - amount=dashou_profile.dashou_fencheng, - action=3 # 退款 - ) - except Exception as e: - logger.error(f"记录打手统计错误: {str(e)}") - except ObjectDoesNotExist: - logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新") - - # 6. 更新订单状态为1(下单中)或7(指定中) - # 7. 根据派单方向清理对方信息 - if order.dispatch_type == 1: - # 我方派单:清空对方俱乐部ID和订单ID - order.partner_club_id = None - order.partner_order_id = None - # 对方派单时保留对方信息,不清空 - - # 8. 更新订单状态 - if order.zhiding_id: - order.zhuangtai = 7 # 指定中 - else: - order.zhuangtai = 1 # 下单中 - - - order.jiedan_dashou_id = None - order.clkf = partner_club_id # 记录操作方 - order.save() - - logger.info(f"对方转移大厅通知成功: 订单 {order.dingdan_id},新状态 {order.zhuangtai}") - - # 7. 返回我方订单状态 - return Response({ - 'code': 0, - 'msg': 'success', - 'data': { - 'partner_status': order.zhuangtai - } - }) - - except Exception as e: - logger.error(f"对方转移大厅通知接口异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - - - - -logger = logging.getLogger(__name__) - - -# ========== 1. 获取俱乐部设置数据接口 ========== -class GetSettingsView(APIView): - """ - 获取跨平台设置页面数据 - 请求:POST /houtai/hqkptsz - 参数:{"username": "客服账号ID"} - 权限:权限码2且开启 - 返回:包含自己俱乐部信息(含客服字段)和合作俱乐部列表 - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 客服身份验证 - current_user = request.user - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '004aa' not in permissions: - return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) - - # 4. 查询数据 - try: - # 4.1 自己俱乐部配置(is_self=1) - self_config = ClubConfig.objects.filter(is_self=1).first() - if not self_config: - logger.error("未找到自己俱乐部配置") - return Response({'code': 500, 'msg': '系统配置错误'}) - - self_club = { - 'club_id': self_config.club_id, - 'club_nickname': self_config.club_nickname, - 'club_avatar': self_config.club_avatar, - 'backend_domain': self_config.backend_domain, - 'chat_api_key': self_config.chat_api_key, - 'chat_api_id': self_config.chat_api_id, - 'customer_service_api_url': self_config.customer_service_api_url, - 'customer_service_corp_id': self_config.customer_service_corp_id, - 'storage_bucket_domain': self_config.storage_bucket_domain, - } - - # 4.2 合作俱乐部列表(Club 表中本俱乐部ID = self_config.club_id) - partner_records = Club.objects.filter(club_id=self_config.club_id) - partner_clubs = [] - for record in partner_records: - partner_config = ClubConfig.objects.filter( - club_id=record.partner_club_id, - is_self=0 - ).first() - partner_clubs.append({ - 'club_id': record.partner_club_id, - 'club_nickname': partner_config.club_nickname if partner_config else record.partner_club_id, - 'club_avatar': partner_config.club_avatar if partner_config else '', - 'is_interop_enabled': record.is_interop_enabled, - 'is_receive_enabled': record.is_receive_enabled, - }) - - return Response({'code': 0, 'data': {'self_club': self_club, 'partner_clubs': partner_clubs}}) - - except Exception as e: - logger.error(f"获取设置失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - -# ========== 2. 获取订单类型映射接口 ========== -class GetMappingsView(APIView): - """ - 获取指定合作俱乐部的订单类型映射 - 请求:POST /houtai/hqqtjlbsj - 参数:{"username": "客服账号ID", "partner_club_id": "对方俱乐部ID"} - 权限:权限码2 - 返回:对方订单类型列表、我方订单类型列表、当前映射关系 - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - username = request.data.get('username', '').strip() - partner_club_id = request.data.get('partner_club_id', '').strip() - if not username or not partner_club_id: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 客服身份验证 - current_user = request.user - - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '004aa' not in permissions: - return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) - - try: - # 4. 获取我方所有商品类型(订单类型) - our_types = ShangpinLeixing.objects.all().values('id', 'jieshao', 'tupian_url') - our_type_list = [ - {'type_id': t['id'], 'type_name': t['jieshao'], 'icon': t['tupian_url']} - for t in our_types - ] - - # 5. 获取对方订单类型(从映射表去重) - mappings_query = OrderTypeMapping.objects.filter(partner_club_id=partner_club_id) - partner_types_dict = {} - for m in mappings_query: - partner_types_dict[m.partner_type_id] = { - 'type_id': m.partner_type_id, - 'type_name': m.partner_type_name, - 'icon': '' - } - partner_type_list = list(partner_types_dict.values()) - - # 6. 获取当前映射关系 - mapping_list = [ - {'partner_type_id': m.partner_type_id, 'our_type_id': m.our_type_id} - for m in mappings_query - ] - - return Response({ - 'code': 0, - 'data': { - 'partner_types': partner_type_list, - 'our_types': our_type_list, - 'mappings': mapping_list - } - }) - - except Exception as e: - logger.error(f"获取映射失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - -# ========== 3. 保存设置接口(自己俱乐部 + 合作俱乐部开关)========== -class UpdateSettingsView(APIView): - """ - 保存俱乐部设置(自己俱乐部信息 + 合作俱乐部开关) - 请求:POST /houtai/xgjlbsj - 参数:{ - "username": "客服账号ID", - "self_club": { - "club_nickname": "...", - "backend_domain": "...", - "chat_api_key": "...", - "chat_api_id": "...", - "customer_service_api_url": "...", - "customer_service_corp_id": "...", - "storage_bucket_domain": "..." - }, - "partner_updates": [ - {"club_id": "654321", "is_interop_enabled": true, "is_receive_enabled": false} - ] - } - 权限:权限码2 - 返回:{"code": 0, "msg": "success"} - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 客服身份验证 - current_user = request.user - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '004aa' not in permissions: - return Response({'code': 403, 'msg': '您无权修改俱乐部配置'}) - - # 4. 获取自己俱乐部当前配置 - try: - self_config = ClubConfig.objects.get(is_self=1) - except ObjectDoesNotExist: - logger.error("未找到自己俱乐部配置") - return Response({'code': 500, 'msg': '系统配置错误'}) - - # 5. 更新自己俱乐部配置(包含客服字段) - self_updates = request.data.get('self_club', {}) - if self_updates: - allowed_fields = [ - 'club_nickname', 'backend_domain', 'chat_api_key', 'chat_api_id', - 'customer_service_api_url', 'customer_service_corp_id', - 'storage_bucket_domain' - ] - fields_to_update = [] - for field in allowed_fields: - if field in self_updates: - setattr(self_config, field, self_updates[field]) - fields_to_update.append(field) - if fields_to_update: - self_config.save(update_fields=fields_to_update) - - # 6. 更新合作俱乐部开关 - partner_updates = request.data.get('partner_updates', []) - for update in partner_updates: - club_id = update.get('club_id') - if not club_id: - continue - try: - partner_record = Club.objects.get(club_id=self_config.club_id, partner_club_id=club_id) - if 'is_interop_enabled' in update: - partner_record.is_interop_enabled = update['is_interop_enabled'] - if 'is_receive_enabled' in update: - partner_record.is_receive_enabled = update['is_receive_enabled'] - partner_record.save(update_fields=['is_interop_enabled', 'is_receive_enabled']) - except ObjectDoesNotExist: - logger.warning(f"合作俱乐部记录不存在: {club_id}") - - # 7. 异步通知所有合作俱乐部(如果自己数据有修改) - if self_updates: - self._notify_all_partners(self_config, self_updates) - - return Response({'code': 0, 'msg': 'success'}) - - def _notify_all_partners(self, self_config, updates): - """异步通知所有合作俱乐部更新我方配置""" - partner_records = Club.objects.filter(club_id=self_config.club_id) - if not partner_records: - return - - data_to_send = { - 'club_id': self_config.club_id, - 'updates': updates - } - - def notify(partner_domain): - try: - url = partner_domain.rstrip('/') + '/houtai/xgzjsjcs' - resp = requests.post(url, json=data_to_send, timeout=3) - if resp.status_code != 200: - logger.warning(f"通知 {partner_domain} 失败,状态码 {resp.status_code}") - except Exception as e: - logger.error(f"通知 {partner_domain} 异常: {str(e)}") - - for record in partner_records: - domain = record.partner_domain - if domain: - threading.Thread(target=notify, args=(domain,)).start() - - - - - -class UpdateMappingsView(APIView): - """ - 保存指定合作俱乐部的订单类型映射 - 请求:POST /houtai/xgdfddys - 参数:{ - "username": "客服手机号", - "partner_club_id": "对方俱乐部ID", - "mappings": [ - {"partner_type_id": 1, "our_type_id": 10}, - {"partner_type_id": 2, "our_type_id": 11} - ] - } - 权限:权限码2 - 返回:{"code": 0, "msg": "success"} - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - username = request.data.get('username', '').strip() - partner_club_id = request.data.get('partner_club_id', '').strip() - mappings = request.data.get('mappings', []) - - if not username or not partner_club_id: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 客服身份验证 - current_user = request.user - - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '004aa' not in permissions: - return Response({'code': 403, 'msg': '您无权修改俱乐部配置'}) - - try: - with transaction.atomic(): - # 4. 获取当前该俱乐部所有现有映射(按 partner_type_id 索引) - existing_mappings = { - m.partner_type_id: m - for m in OrderTypeMapping.objects.filter(partner_club_id=partner_club_id) - } - - # 5. 处理前端传来的映射(更新或新增) - processed_partner_ids = set() - for mapping in mappings: - partner_type_id = mapping.get('partner_type_id') - our_type_id = mapping.get('our_type_id') - if partner_type_id is None or our_type_id is None: - continue - processed_partner_ids.add(partner_type_id) - - # 获取我方类型名称 - our_type_name = '' - try: - our_type = ShangpinLeixing.objects.get(id=our_type_id) - our_type_name = our_type.jieshao - except ObjectDoesNotExist: - logger.warning(f"我方订单类型不存在: {our_type_id}") - - # 查找是否存在该对方类型的映射 - if partner_type_id in existing_mappings: - # 更新现有记录 - mapping_obj = existing_mappings[partner_type_id] - mapping_obj.our_type_id = our_type_id - mapping_obj.our_type_name = our_type_name - mapping_obj.save(update_fields=['our_type_id', 'our_type_name']) - # 从字典中移除,以便后续知道哪些被删除了 - del existing_mappings[partner_type_id] - else: - # 新增记录 - # 对方类型名称:优先从现有映射中查找(可能被其他俱乐部共享?不,每个俱乐部独立) - # 由于该对方类型没有映射过,我们暂时无法获取名称,可留空或使用默认值。 - # 但为保证数据完整性,我们尝试从已有记录中查找(如果该俱乐部之前删除过又重建,则可能丢失名称) - # 更稳妥的方式是前端传递 partner_type_name,但当前未要求,因此此处留空并记录警告。 - partner_type_name = '' - logger.warning(f"新增映射时对方类型名称缺失: partner_club_id={partner_club_id}, partner_type_id={partner_type_id}") - - OrderTypeMapping.objects.create( - partner_club_id=partner_club_id, - partner_type_id=partner_type_id, - partner_type_name=partner_type_name, - our_type_id=our_type_id, - our_type_name=our_type_name - ) - - # 6. 删除前端未传入的映射(即用户取消的映射) - for partner_type_id, mapping_obj in existing_mappings.items(): - mapping_obj.delete() - logger.info(f"删除映射: partner_club_id={partner_club_id}, partner_type_id={partner_type_id}") - - return Response({'code': 0, 'msg': 'success'}) - - except Exception as e: - logger.error(f"保存映射失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) -# ========== 5. 接收对方平台修改数据接口 ========== -class PartnerUpdateSelfConfigView(APIView): - """ - 接收对方平台修改自己俱乐部配置的通知 - 请求:POST /houtai/xgzjsjcs - 参数:{ - "club_id": "对方俱乐部ID", - "updates": { - "club_nickname": "新昵称", - "backend_domain": "新域名", - "chat_api_key": "新key", - "chat_api_id": "新id", - "customer_service_api_url": "新URL", - "customer_service_corp_id": "新企业ID", - "storage_bucket_domain": "新存储桶" - } - } - 无需认证,对方平台可信调用 - 返回:{"code": 0, "msg": "success"} - """ - parser_classes = [JSONParser] - - def post(self, request): - # token = request.data.get('token') - # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): - # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200) - # 1. 获取参数 - club_id = request.data.get('club_id', '').strip() - updates = request.data.get('updates', {}) - - if not club_id or not updates: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 查询对方俱乐部的配置记录(is_self=0) - try: - config = ClubConfig.objects.get(club_id=club_id, is_self=0) - except ClubConfig.DoesNotExist: - logger.warning(f"对方俱乐部配置不存在: {club_id}") - return Response({'code': 404, 'msg': '俱乐部不存在'}) - - # 3. 允许修改的字段白名单(包含客服字段) - allowed_fields = [ - 'club_nickname', - 'backend_domain', - 'chat_api_key', - 'chat_api_id', - 'customer_service_api_url', - 'customer_service_corp_id', - 'storage_bucket_domain' - ] - - # 4. 更新字段 - with transaction.atomic(): - fields_updated = [] - for field in allowed_fields: - if field in updates: - new_value = updates[field] - setattr(config, field, new_value) - fields_updated.append(field) - if fields_updated: - config.save(update_fields=fields_updated) - logger.info(f"对方俱乐部 {club_id} 配置已更新: {fields_updated}") - else: - logger.info(f"对方俱乐部 {club_id} 无有效字段更新") - - return Response({'code': 0, 'msg': 'success'}) - - - - -# apps/houtai/views.py - - - - - -# ========== 1. 获取合作俱乐部列表接口(完整) ========== -class GetPartnerClubsView(APIView): - """ - 获取我方合作俱乐部列表(用于统计页面) - 请求:POST /houtai/kpthqpt - 参数:{"username": "客服手机号"} - 权限:权限码2且开启 - 返回:{"code":0, "data":[{"club_id":"xxx","club_nickname":"xxx","club_avatar":"xxx"}]} - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 客服身份验证 - current_user = request.user - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '004bb' not in permissions: - return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) - - # 4. 获取我方俱乐部ID - try: - self_config = ClubConfig.objects.get(is_self=1) - except ObjectDoesNotExist: - logger.error("未找到自己俱乐部配置") - return Response({'code': 500, 'msg': '系统配置错误'}) - - # 5. 查询所有合作俱乐部 - try: - records = Club.objects.filter(club_id=self_config.club_id) - result = [] - for record in records: - partner_config = ClubConfig.objects.filter( - club_id=record.partner_club_id, - is_self=0 - ).first() - result.append({ - 'club_id': record.partner_club_id, - 'club_nickname': partner_config.club_nickname if partner_config else record.partner_club_id, - 'club_avatar': partner_config.club_avatar if partner_config else '', - }) - return Response({'code': 0, 'data': result}) - except Exception as e: - logger.error(f"查询合作俱乐部失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - -# ========== 2. 获取统计数据接口(完整) ========== - - - - - -class GetStatsView(APIView): - """ - 获取派单统计数据(按日或按月) - 请求:POST /houtai/hqpfsj - 参数:{ - "username": "客服手机号", - "club_id": "对方俱乐部ID", - "direction": 1, # 1=我方派单,2=对方派单 - "granularity": "day", # "day" 或 "month" - # 趋势图参数(可选,不传则使用默认值): - "trend_year_month": "YYYY-MM", # 按日时表示月份,缺省当前月 - "trend_year": "YYYY", # 按月时表示年份,缺省当前年 - # KPI参数(可选,不传则使用默认值): - "date": "YYYY-MM-DD", # 按日时表示日期,缺省当天 - "year_month": "YYYY-MM" # 按月时表示月份,缺省当前月 - } - 权限:权限码2 - 返回:{ - "code": 0, - "data": { - "summary": { ... }, - "chart": { ... } - } - } - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取基础参数 - username = request.data.get('username', '').strip() - club_id = request.data.get('club_id', '').strip() - direction = request.data.get('direction') - granularity = request.data.get('granularity') - - # 基础参数校验 - if not username: - return Response({'code': 400, 'msg': 'username 不能为空'}) - if not club_id: - return Response({'code': 400, 'msg': 'club_id 不能为空'}) - if direction is None: - return Response({'code': 400, 'msg': 'direction 不能为空'}) - if granularity not in ('day', 'month'): - return Response({'code': 400, 'msg': 'granularity 必须是 day 或 month'}) - try: - direction = int(direction) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': 'direction 必须为整数'}) - - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '004bb' not in permissions: - return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) - - # 4. 获取当前时间用于默认值 - now = datetime.now() - - # 5. 处理趋势图参数(默认当前月/年) - if granularity == 'day': - trend_year_month = request.data.get('trend_year_month') - if not trend_year_month: - trend_year_month = now.strftime('%Y-%m') - try: - year, month = map(int, trend_year_month.split('-')) - start_date = datetime(year, month, 1).date() - _, days_in_month = monthrange(year, month) - end_date = datetime(year, month, days_in_month).date() - except (ValueError, TypeError): - # 如果格式错误,回退到当前月 - year, month = now.year, now.month - start_date = datetime(year, month, 1).date() - _, days_in_month = monthrange(year, month) - end_date = datetime(year, month, days_in_month).date() - else: - trend_year = request.data.get('trend_year') - if not trend_year: - trend_year = str(now.year) - try: - year = int(trend_year) - except (ValueError, TypeError): - year = now.year - start_date = datetime(year, 1, 1).date() - end_date = datetime(year, 12, 31).date() - - # 6. 处理KPI参数(默认当天/当月) - if granularity == 'day': - date_str = request.data.get('date') - if not date_str: - date_str = now.strftime('%Y-%m-%d') - try: - target_date = datetime.strptime(date_str, '%Y-%m-%d').date() - except (ValueError, TypeError): - target_date = now.date() - else: - year_month_str = request.data.get('year_month') - if not year_month_str: - year_month_str = now.strftime('%Y-%m') - try: - year, month = map(int, year_month_str.split('-')) - start_date_kpi = datetime(year, month, 1).date() - _, days_in_month = monthrange(year, month) - end_date_kpi = datetime(year, month, days_in_month).date() - except (ValueError, TypeError): - year, month = now.year, now.month - start_date_kpi = datetime(year, month, 1).date() - _, days_in_month = monthrange(year, month) - end_date_kpi = datetime(year, month, days_in_month).date() - - # 7. 查询趋势图数据 - response_data = { - 'summary': {'dispatch_count': 0, 'dispatch_amount': 0.0, 'success_count': 0, 'success_amount': 0.0}, - 'chart': {'labels': [], 'dispatch_counts': [], 'dispatch_amounts': [], 'success_counts': [], 'success_amounts': []} - } - - try: - if granularity == 'day': - qs = DailyDispatchStat.objects.filter( - partner_club_id=club_id, - direction=direction, - date__gte=start_date, - date__lte=end_date - ).order_by('date') - data_dict = {} - for item in qs: - data_dict[item.date] = { - 'dispatch_count': item.dispatch_count, - 'dispatch_amount': float(item.dispatch_amount), - 'success_count': item.success_count, - 'success_amount': float(item.success_amount), - } - labels = [] - dispatch_counts = [] - dispatch_amounts = [] - success_counts = [] - success_amounts = [] - current = start_date - while current <= end_date: - labels.append(current.strftime('%d')) - day_data = data_dict.get(current, {}) - dispatch_counts.append(day_data.get('dispatch_count', 0)) - dispatch_amounts.append(day_data.get('dispatch_amount', 0.0)) - success_counts.append(day_data.get('success_count', 0)) - success_amounts.append(day_data.get('success_amount', 0.0)) - current += timedelta(days=1) - response_data['chart'] = { - 'labels': labels, - 'dispatch_counts': dispatch_counts, - 'dispatch_amounts': dispatch_amounts, - 'success_counts': success_counts, - 'success_amounts': success_amounts, - } - else: # month - qs = DailyDispatchStat.objects.filter( - partner_club_id=club_id, - direction=direction, - date__gte=start_date, - date__lte=end_date - ).order_by('date') - month_totals = {m: {'dispatch_count': 0, 'dispatch_amount': 0.0, 'success_count': 0, 'success_amount': 0.0} - for m in range(1, 13)} - for item in qs: - m = item.date.month - month_totals[m]['dispatch_count'] += item.dispatch_count - month_totals[m]['dispatch_amount'] += float(item.dispatch_amount) - month_totals[m]['success_count'] += item.success_count - month_totals[m]['success_amount'] += float(item.success_amount) - labels = [] - dispatch_counts = [] - dispatch_amounts = [] - success_counts = [] - success_amounts = [] - for m in range(1, 13): - labels.append(f"{m}月") - dispatch_counts.append(month_totals[m]['dispatch_count']) - dispatch_amounts.append(month_totals[m]['dispatch_amount']) - success_counts.append(month_totals[m]['success_count']) - success_amounts.append(month_totals[m]['success_amount']) - response_data['chart'] = { - 'labels': labels, - 'dispatch_counts': dispatch_counts, - 'dispatch_amounts': dispatch_amounts, - 'success_counts': success_counts, - 'success_amounts': success_amounts, - } - - # 8. 查询KPI数据 - if granularity == 'day': - try: - stat = DailyDispatchStat.objects.get( - partner_club_id=club_id, - direction=direction, - date=target_date - ) - response_data['summary'] = { - 'dispatch_count': stat.dispatch_count, - 'dispatch_amount': float(stat.dispatch_amount), - 'success_count': stat.success_count, - 'success_amount': float(stat.success_amount), - } - except DailyDispatchStat.DoesNotExist: - response_data['summary'] = { - 'dispatch_count': 0, - 'dispatch_amount': 0.0, - 'success_count': 0, - 'success_amount': 0.0, - } - else: # month - qs_kpi = DailyDispatchStat.objects.filter( - partner_club_id=club_id, - direction=direction, - date__gte=start_date_kpi, - date__lte=end_date_kpi - ) - summary = qs_kpi.aggregate( - dispatch_count=Sum('dispatch_count'), - dispatch_amount=Sum('dispatch_amount'), - success_count=Sum('success_count'), - success_amount=Sum('success_amount') - ) - response_data['summary'] = { - 'dispatch_count': summary['dispatch_count'] or 0, - 'dispatch_amount': float(summary['dispatch_amount'] or 0.0), - 'success_count': summary['success_count'] or 0, - 'success_amount': float(summary['success_amount'] or 0.0), - } - - return Response({'code': 0, 'data': response_data}) - - except Exception as e: - logger.error(f"获取统计数据失败: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - - -class PunishDashouView(APIView): - """ - 客服处罚打手接口 - 请求:POST /houtai/kpcf - 参数:{ - "username": "客服手机号", - "dingdan_id": "订单ID", - "reason": "处罚原因(可选)" - } - 认证:JWT,权限码1 - 返回:code=0 + 处罚结果 - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - start_time = time.time() - - # 1. 获取参数 - username = request.data.get('username', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - reason = request.data.get('reason', '').strip() - - if not username or not dingdan_id: - return Response({'code': 400, 'msg': '参数不完整'}) - - try: - # 2. 身份验证 - current_user = request.user - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '003aa' not in permissions: - return Response({'code': 403, 'msg': '您无权处罚'}) - - # 4. 查询订单并加锁 - with transaction.atomic(): - order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) - - # 5. 订单状态校验(允许处罚的状态:1,2,3,4,5,6,7,8,即除已完成以外的状态?这里宽松处理) - if order.zhuangtai not in [1,2,3,4,5,6,7,8]: - return Response({'code': 400, 'msg': f'订单状态 {order.zhuangtai} 不允许处罚'}) - - # 6. 判断是否已处罚(避免重复) - existing_punish = Chufajilu.objects.filter( - dingdan_id=dingdan_id, - dashouid=order.jiedan_dashou_id - ).exists() - if existing_punish: - return Response({'code': 400, 'msg': '该打手已被处罚过'}) - - # 7. 区分本地处罚还是通知对方平台 - is_cross = order.is_cross - dispatch_type = order.dispatch_type - partner_club_id = order.partner_club_id - partner_order_id = order.partner_order_id - dashou_id = order.jiedan_dashou_id - - # 如果没有接单打手ID,无法处罚 - if not dashou_id: - return Response({'code': 400, 'msg': '订单无接单打手,无法处罚'}) - - # 本地处罚(处罚我方打手) - def local_punish(): - # 扣除打手积分(假设每次扣5分) - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.jifen -= 5 - dashou_profile.save() - - # 创建处罚记录 - Chufajilu.objects.create( - dingdan_id=dingdan_id, - dashouid=dashou_id, - qingqiuid='', # 请求处罚ID,可空 - chuliid=username, - cfliyou=reason or '', - sqzhuangtai=1, # 成功 - bhliyou='', - ssliyou='', - jifen=5, - ) - logger.info(f"本地处罚打手成功: 订单 {dingdan_id}, 打手 {dashou_id}, 扣分5") - - # 情况A:对方派单(dispatch_type=2)或 本地订单 - if dispatch_type == 2 or not is_cross: - # 直接本地处罚 - local_punish() - return Response({'code': 0, 'msg': '处罚成功'}) - - # 情况B:我方派单且有对方信息(需要通知对方平台) - elif dispatch_type == 1 and partner_club_id and partner_order_id: - # 需要通知对方平台处罚(对方平台打手) - # 获取我方俱乐部ID - my_club_config = ClubConfig.objects.filter(is_self=1).first() - if not my_club_config: - logger.error("未找到我方俱乐部配置") - return Response({'code': 500, 'msg': '系统配置错误'}) - self_club_id = my_club_config.club_id - - # 获取对方服务器域名 - try: - club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) - partner_domain = club_rel.partner_domain - except ObjectDoesNotExist: - logger.error(f"未找到合作关系: {self_club_id} <-> {partner_club_id}") - return Response({'code': 500, 'msg': '未找到对方平台配置'}) - - # 调用对方平台处罚接口,最多重试2次 - url = partner_domain.rstrip('/') + '/houtai/kptcfds' - payload = { - 'order_id': partner_order_id, # 对方平台的订单ID - 'club_id': self_club_id, - 'reason': reason, - 'operator': username - } - timeout = 3 - retries = 2 - success = False - for attempt in range(retries): - try: - resp = requests.post(url, json=payload, timeout=timeout) - if resp.status_code == 200: - resp_json = resp.json() - if resp_json.get('code') == 0: - success = True - logger.info(f"通知对方平台处罚成功: {dingdan_id}") - # 可选:在本平台记录处罚记录(标记已通知对方,但不扣分) - Chufajilu.objects.create( - dingdan_id=dingdan_id, - dashouid=dashou_id, - qingqiuid='', - chuliid=username, - cfliyou=reason or '', - sqzhuangtai=1, - bhliyou='', - ssliyou='', - jifen=0, # 对方扣分,本方不扣 - ) - break - else: - logger.warning(f"对方平台返回错误 (尝试{attempt+1}): {resp_json.get('msg')}") - else: - logger.warning(f"对方平台HTTP错误 (尝试{attempt+1}): {resp.status_code}") - except requests.exceptions.Timeout: - logger.warning(f"请求对方平台超时 (尝试{attempt+1}): {url}") - except Exception as e: - logger.warning(f"请求对方平台异常 (尝试{attempt+1}): {str(e)}") - if not success: - return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'}) - - return Response({'code': 0, 'msg': '处罚成功'}) - - else: - # 其他情况(例如我方派单但缺少对方信息),视为本地订单,处罚我方打手 - local_punish() - return Response({'code': 0, 'msg': '处罚成功'}) - - except Dingdan.DoesNotExist: - logger.warning(f"订单不存在: {dingdan_id}") - return Response({'code': 404, 'msg': '订单不存在'}) - except Exception as e: - logger.error(f"处罚接口异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - - - - -@method_decorator(csrf_exempt, name='dispatch') -class PartnerPunishNotifyView(APIView): - """ - 接收对方平台处罚通知接口 - """ - authentication_classes = [] - permission_classes = [AllowAny] - parser_classes = [JSONParser] - - def post(self, request): - # token = request.data.get('token') - # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): - # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200) - try: - order_id = request.data.get('order_id', '').strip() - club_id = request.data.get('club_id', '').strip() - reason = request.data.get('reason', '').strip() - operator = request.data.get('operator', '').strip() - - if not order_id or not club_id: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 事务开始 - with transaction.atomic(): - # 查询订单并加锁 - try: - order = Dingdan.objects.select_for_update().get(dingdan_id=order_id) - except Dingdan.DoesNotExist: - logger.warning(f"处罚通知:订单不存在 {order_id}") - return Response({'code': 404, 'msg': '订单不存在'}) - - dashou_id = order.jiedan_dashou_id - if not dashou_id: - logger.warning(f"订单 {order_id} 无接单打手,无法处罚") - return Response({'code': 400, 'msg': '订单无接单打手'}) - - # 检查是否已处罚 - existing = Chufajilu.objects.filter( - dingdan_id=order_id, - dashouid=dashou_id - ).exists() - if existing: - logger.info(f"订单 {order_id} 打手 {dashou_id} 已被处罚过,跳过") - return Response({'code': 0, 'msg': 'already punished'}) - - # 扣除积分 - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.jifen -= 5 - dashou_profile.save() - - # 创建处罚记录 - Chufajilu.objects.create( - dingdan_id=order_id, - dashouid=dashou_id, - qingqiuid='', - chuliid=operator, - cfliyou=reason or '', - sqzhuangtai=0, - bhliyou='', - ssliyou='', - jifen=5, - ) - logger.info(f"对方平台处罚成功: 订单 {order_id}, 打手 {dashou_id}") - - return Response({'code': 0, 'msg': 'success'}) - - except Exception as e: - logger.error(f"对方处罚通知接口异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - - - - - - -# ==================== 接口1:获取俱乐部列表(权限码2) ==================== -class GetClubConfigForPreSettlementView(APIView): - """ - 获取对方俱乐部配置列表(用于预结算页面) - 权限要求:客服身份 + 权限代码2且开启 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - # 1. 校验客服身份 - try: - kefu = UserKefu.objects.select_related('user').get(user=user) - except ObjectDoesNotExist: - return Response({'code': 403, 'msg': '身份错误,非客服账号'}) - if kefu.zhuangtai != 1: - return Response({'code': 403, 'msg': '客服账号已被禁用'}) - - # 2. 获取请求参数中的 username 并校验 - data = request.data - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '004aa' not in permissions: - return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) - - # 4. 查询所有对方俱乐部(is_self=0) - club_configs = ClubConfig.objects.filter(is_self=0).only( - 'club_id', 'club_nickname', 'club_avatar', 'storage_bucket_domain' - ) - data_list = [] - for club in club_configs: - data_list.append({ - 'club_id': club.club_id, - 'club_nickname': club.club_nickname or club.club_id, - 'club_avatar': club.club_avatar or '', - 'storage_bucket_domain': club.storage_bucket_domain or '' - }) - return Response({'code': 0, 'msg': 'success', 'data': data_list}) - - except Exception as e: - logger.error(f"获取俱乐部配置失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - -# ==================== 接口2:获取预结算订单列表 ==================== -class GetPreSettlementDataView(APIView): - """获取预结算订单列表(支持筛选、分页、统计)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - # 1. 客服身份校验 - try: - kefu = UserKefu.objects.select_related('user').get(user=user) - except ObjectDoesNotExist: - return Response({'code': 403, 'msg': '身份错误,非客服账号'}) - if kefu.zhuangtai != 1: - return Response({'code': 403, 'msg': '客服账号已被禁用'}) - - data = request.data - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限(003aa) - if '004aa' not in permissions: - return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) - - # 2. 获取筛选参数 - status_filter = data.get('status_filter') # 0=待处理, 1=已处理(包含已拒绝) - club_id = data.get('club_id') - dingdan_id = data.get('dingdan_id') - partner_order_id = data.get('partner_order_id') - dashou_id = data.get('dashou_id') - clkf = data.get('clkf') - date_str = data.get('date') # 创建日期 YYYY-MM-DD - page = int(data.get('page', 1)) - page_size = int(data.get('pageSize', 20)) - - # 3. 构建查询集 - queryset = PreSettlement.objects.all() - - # 状态筛选:0待处理,1已处理(status=1或2) - if status_filter == 0: - queryset = queryset.filter(status=0) - elif status_filter == 1: - queryset = queryset.filter(status__in=[1, 2]) - # 如果status_filter为其他值(如'all'),则不筛选 - - if club_id: - queryset = queryset.filter(partner_club_id=club_id) - if dingdan_id: - queryset = queryset.filter(dingdan_id__icontains=dingdan_id) - if partner_order_id: - queryset = queryset.filter(partner_order_id__icontains=partner_order_id) - if dashou_id: - queryset = queryset.filter(dashou_id__icontains=dashou_id) - if clkf: - queryset = queryset.filter(operator__icontains=clkf) - if date_str: - try: - target_date = datetime.strptime(date_str, '%Y-%m-%d').date() - queryset = queryset.filter(create_time__date=target_date) - except ValueError: - pass - - # 4. 统计数据(待处理总数量和总金额、已处理总数量) - pending_queryset = PreSettlement.objects.filter(status=0) - # 应用相同的筛选条件(除了状态) - if club_id: - pending_queryset = pending_queryset.filter(partner_club_id=club_id) - if dingdan_id: - pending_queryset = pending_queryset.filter(dingdan_id__icontains=dingdan_id) - if partner_order_id: - pending_queryset = pending_queryset.filter(partner_order_id__icontains=partner_order_id) - if dashou_id: - pending_queryset = pending_queryset.filter(dashou_id__icontains=dashou_id) - if clkf: - pending_queryset = pending_queryset.filter(operator__icontains=clkf) - if date_str: - pending_queryset = pending_queryset.filter(create_time__date=target_date) - - pending_count = pending_queryset.count() - pending_amount = pending_queryset.aggregate(total=Sum('order_amount'))['total'] or Decimal('0.00') - - processed_count = PreSettlement.objects.filter(status__in=[1, 2]) - if club_id: - processed_count = processed_count.filter(partner_club_id=club_id) - if dingdan_id: - processed_count = processed_count.filter(dingdan_id__icontains=dingdan_id) - if partner_order_id: - processed_count = processed_count.filter(partner_order_id__icontains=partner_order_id) - if dashou_id: - processed_count = processed_count.filter(dashou_id__icontains=dashou_id) - if clkf: - processed_count = processed_count.filter(operator__icontains=clkf) - if date_str: - processed_count = processed_count.filter(create_time__date=target_date) - processed_count = processed_count.count() - - # 5. 分页 - total = queryset.count() - start = (page - 1) * page_size - end = start + page_size - items = queryset.order_by('-create_time')[start:end] - - # 6. 构造返回数据 - list_data = [] - for item in items: - list_data.append({ - 'dingdan_id': item.dingdan_id, - 'partner_order_id': item.partner_order_id, - 'partner_club_id': item.partner_club_id, - 'dashou_id': item.dashou_id, - 'amount': float(item.amount), - 'order_amount': float(item.order_amount), - 'operator': item.operator, - 'status': item.status, - 'status_text': dict(PreSettlement.STATUS_CHOICES).get(item.status, '未知'), - 'create_time': item.create_time.strftime('%Y-%m-%d %H:%M:%S') if item.create_time else '', - 'process_time': item.process_time.strftime('%Y-%m-%d %H:%M:%S') if item.process_time else '', - }) - - return Response({ - 'code': 0, - 'msg': 'success', - 'data': { - 'list': list_data, - 'total': total, - 'stats': { - 'pending_count': pending_count, - 'pending_amount': float(pending_amount), - 'processed_count': processed_count - } - } - }) - - except Exception as e: - logger.error(f"获取预结算数据失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - -# ==================== 接口3:打款/拒绝操作 ==================== -class PreSettlementActionView(APIView): - """预结算打款/拒绝操作(原子事务,保证资金安全)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - # 1. 客服身份校验 - try: - kefu = UserKefu.objects.select_related('user').get(user=user) - except ObjectDoesNotExist: - return Response({'code': 403, 'msg': '身份错误,非客服账号'}) - if kefu.zhuangtai != 1: - return Response({'code': 403, 'msg': '客服账号已被禁用'}) - - data = request.data - - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否有跨平台管理权限() - if '004aa' not in permissions: - return Response({'code': 403, 'msg': '您无权处理预结算打款'}) - - dingdan_id = data.get('dingdan_id') - action = data.get('action') # 'pay' 或 'reject' - - if not dingdan_id or action not in ['pay', 'reject']: - return Response({'code': 400, 'msg': '参数错误'}) - - with transaction.atomic(): - # 锁定预结算记录(行锁) - pre_settlement = PreSettlement.objects.select_for_update().get(dingdan_id=dingdan_id) - - # 只有状态为0(待处理)才能操作 - if pre_settlement.status != 0: - return Response({'code': 400, 'msg': '该记录已处理,不可重复操作'}) - - if action == 'pay': - # 打款操作:增加打手余额及相关统计 - dashou_id = pre_settlement.dashou_id - amount = pre_settlement.amount - - # 查询打手扩展表 - try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - except (UserMain.DoesNotExist, ObjectDoesNotExist): - return Response({'code': 404, 'msg': '打手不存在'}) - - # 使用 F 表达式原子更新余额和统计字段(防止并发覆盖) - dashou_profile.yue = F('yue') + amount - dashou_profile.zonge = F('zonge') + amount - dashou_profile.jinrishouyi = F('jinrishouyi') + amount - dashou_profile.jinyueshouyi = F('jinyueshouyi') + amount - dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1 - dashou_profile.save(update_fields=['yue', 'zonge', 'jinrishouyi', 'jinyueshouyi', 'chengjiaozongliang']) - - # 更新预结算状态为已结算 - pre_settlement.status = 1 - pre_settlement.process_time = timezone.now() - pre_settlement.operator = user.phone - pre_settlement.save() - - # ---------- 打手每日统计(成交) ---------- - - try: - - - - from houtai.utils import update_dashou_daily_by_action - update_dashou_daily_by_action( - yonghuid=dashou_id, - amount=amount, - action=2 # 2 表示成交(结算) - ) - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - - - logger.info(f"预结算打款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {amount}") - - logger.info(f"预结算打款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {amount}") - - elif action == 'reject': - # 拒绝操作:仅修改状态,不涉及资金变动 - pre_settlement.status = 2 - pre_settlement.process_time = timezone.now() - pre_settlement.operator = user.phone - pre_settlement.save() - logger.info(f"预结算拒绝: 订单 {dingdan_id}") - - # ---------- 打手每日统计(成交) ---------- - dashou_id = pre_settlement.dashou_id - amount = pre_settlement.amount - try: - dashou_id = pre_settlement.dashou_id - amount = pre_settlement.amount - from houtai.utils import update_dashou_daily_by_action - update_dashou_daily_by_action( - yonghuid=dashou_id, - amount=amount, - action=3 # 3 表示退款(退款) - ) - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - logger.info(f"预结算拒绝: 订单 {dingdan_id}") - - return Response({'code': 0, 'msg': '操作成功'}) - - except PreSettlement.DoesNotExist: - return Response({'code': 404, 'msg': '预结算记录不存在'}) - except Exception as e: - logger.error(f"预结算操作异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - - - - -class GetRolePermissionView(APIView): - """ - 获取所有角色、权限以及角色关联的用户数 - 权限要求:用户必须拥有 000001 权限 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 1. 权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 2. 检查是否有 000001 权限 - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权访问此页面'}) - - # 3. 获取所有权限 - all_perms = Permission.objects.all().values('perm_code', 'perm_name', 'description') - permissions_list = list(all_perms) - - # 4. 获取所有角色,并统计每个角色下的用户数和关联的权限 - roles = Role.objects.all() - roles_data = [] - for role in roles: - # 统计该角色下的用户数 - user_count = UserRole.objects.filter(role=role).count() - # 获取该角色绑定的权限 - role_perms = RolePermission.objects.filter(role=role).select_related('permission') - perms_of_role = [ - { - 'perm_code': rp.permission.perm_code, - 'perm_name': rp.permission.perm_name, - 'description': rp.permission.description - } - for rp in role_perms - ] - roles_data.append({ - 'role_code': role.role_code, - 'role_name': role.role_name, - 'description': role.description, - 'user_count': user_count, - 'permissions': perms_of_role - }) - - return Response({ - 'code': 0, - 'data': { - 'roles': roles_data, - 'permissions': permissions_list - } - }) - - -class ModifyRolePermissionView(APIView): - """ - 通用接口:支持删除角色、为角色添加权限、移除角色权限 - 操作类型通过 action 字段区分 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username', '').strip() - action = request.data.get('action') # delete_role, add_permission, remove_permission - if not username or not action: - return Response({'code': 400, 'msg': '缺少必要参数'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权进行此操作'}) - - # 获取角色(需要角色编码) - role_code = request.data.get('role_code') - if not role_code: - return Response({'code': 400, 'msg': '缺少角色编码'}) - - try: - role = Role.objects.get(role_code=role_code) - except Role.DoesNotExist: - return Response({'code': 404, 'msg': '角色不存在'}) - - # 超级管理员角色(role_code='000001')的特殊限制 - if role.role_code == '000001': - if action in ['delete_role']: - return Response({'code': 403, 'msg': '超级管理员角色不能删除'}) - # 修改名称/描述(本接口不涉及名称描述修改,但若后续扩展需限制) - - # 根据 action 处理 - if action == 'delete_role': - # 删除角色:同时删除角色权限关联、用户角色关联 - with transaction.atomic(): - RolePermission.objects.filter(role=role).delete() - UserRole.objects.filter(role=role).delete() - role.delete() - return Response({'code': 0, 'msg': '角色删除成功'}) - - elif action == 'add_permission': - perm_code = request.data.get('perm_code') - if not perm_code: - return Response({'code': 400, 'msg': '缺少权限编码'}) - # 超级管理员角色不能添加 000001 权限(但实际它已拥有,这里防御) - if role.role_code == '000001' and perm_code == '000001': - return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'}) - try: - perm = Permission.objects.get(perm_code=perm_code) - except Permission.DoesNotExist: - return Response({'code': 404, 'msg': '权限不存在'}) - # 检查是否已存在 - if RolePermission.objects.filter(role=role, permission=perm).exists(): - return Response({'code': 400, 'msg': '该角色已拥有此权限'}) - RolePermission.objects.create(role=role, permission=perm) - return Response({'code': 0, 'msg': '权限添加成功'}) - - elif action == 'remove_permission': - perm_code = request.data.get('perm_code') - if not perm_code: - return Response({'code': 400, 'msg': '缺少权限编码'}) - # 超级管理员角色不能删除 000001 权限 - if role.role_code == '000001' and perm_code == '000001': - return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'}) - try: - perm = Permission.objects.get(perm_code=perm_code) - except Permission.DoesNotExist: - return Response({'code': 404, 'msg': '权限不存在'}) - deleted = RolePermission.objects.filter(role=role, permission=perm).delete() - if deleted[0] == 0: - return Response({'code': 400, 'msg': '该角色未拥有此权限'}) - return Response({'code': 0, 'msg': '权限移除成功'}) - - else: - return Response({'code': 400, 'msg': '无效的 action'}) - - -import random -import string -from django.db import transaction - -class AddRoleView(APIView): - """ - 添加角色:自动生成角色编码,前端提供角色名称、描述、选择的权限列表 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username', '').strip() - role_name = request.data.get('role_name', '').strip() - description = request.data.get('description', '').strip() - perm_codes = request.data.get('perm_codes', []) # 列表 - - if not username or not role_name: - return Response({'code': 400, 'msg': '缺少必要参数'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权进行此操作'}) - - # 检查角色名称是否已存在 - if Role.objects.filter(role_name=role_name).exists(): - return Response({'code': 400, 'msg': '角色名称已存在'}) - - # 生成唯一角色编码(格式:ROLE_ + 8位随机字母数字) - while True: - random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) - role_code = f"ROLE_{random_str}" - if not Role.objects.filter(role_code=role_code).exists(): - break - - # 创建角色 - with transaction.atomic(): - role = Role.objects.create( - role_code=role_code, - role_name=role_name, - description=description, - - ) - # 绑定权限(过滤掉不存在的权限编码) - for pc in perm_codes: - # 不允许添加 000001 权限(此权限仅供超级管理员) - if pc == '000001': - continue - try: - perm = Permission.objects.get(perm_code=pc) - RolePermission.objects.create(role=role, permission=perm) - except Permission.DoesNotExist: - # 忽略不存在的权限,或记录日志 - logger.warning(f"添加角色时忽略不存在的权限编码: {pc}") - pass - - return Response({'code': 0, 'msg': '角色添加成功', 'data': {'role_code': role_code}}) - - -import logging -import traceback -from django.core.paginator import Paginator -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from django.db.models import Exists, OuterRef # 若不再使用可移除,此处保留以备后用 - -# 请根据项目实际路径导入 verify_kefu_permission -from .utils import verify_kefu_permission - -logger = logging.getLogger(__name__) - -class GetAdminUserListView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权访问此页面'}) - - phone = request.data.get('phone', '').strip() - nicheng = request.data.get('nicheng', '').strip() - role_code = request.data.get('roleCode', '').strip() - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('pageSize', 20)) - - users = UserMain.objects.filter( - user_type='kefu', - kefu_profile__isnull=False - ).select_related('kefu_profile') - - if phone: - users = users.filter(phone__icontains=phone) - if nicheng: - users = users.filter(kefu_profile__nicheng__icontains=nicheng) - - # 关键修正:先求值为列表,再传入 phone__in - if role_code: - user_ids_with_role = list( - UserRole.objects.filter(role__role_code=role_code) - .values_list('account_id', flat=True) - ) - users = users.filter(phone__in=user_ids_with_role) - - users = users.order_by('-create_time') - paginator = Paginator(users, page_size) - page_obj = paginator.get_page(page) - - data_list = [] - for user in page_obj: - user_roles = UserRole.objects.filter(account_id=user.phone).select_related('role') - roles_data = [ - {'role_code': ur.role.role_code, 'role_name': ur.role.role_name} - for ur in user_roles - ] - data_list.append({ - 'phone': user.phone, - 'nicheng': user.kefu_profile.nicheng, - 'roles': roles_data, - 'status': user.kefu_profile.zhuangtai, - 'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(user, 'create_time') else '', - 'update_time': user.update_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(user, 'update_time') else '', - }) - - return Response({ - 'code': 0, - 'data': { - 'list': data_list, - 'total': paginator.count, - 'page': page, - 'pageSize': page_size - } - }) - - except Exception as e: - logger.error(f"GetAdminUserListView 异常: {e}\n{traceback.format_exc()}") - return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}) - - - -class GetAdminRolesView(APIView): - """获取所有角色(用于筛选和分配)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权访问此页面'}) - - roles = Role.objects.all().values('role_code', 'role_name') - return Response({ - 'code': 0, - 'data': { - 'roles': list(roles) - } - }) - - - - - - -'''class GetAdminUserListView(APIView): - """获取后台用户列表(支持分页、筛选)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权访问此页面'}) - - # 筛选条件 - phone = request.data.get('phone', '').strip() - nicheng = request.data.get('nicheng', '').strip() - role_code = request.data.get('roleCode', '').strip() - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('pageSize', 20)) - - # 基础查询:user_type='kefu' 且存在客服扩展表 - users = UserMain.objects.filter(user_type='kefu', kefu_profile__isnull=False).select_related('kefu_profile') - if phone: - users = users.filter(phone__icontains=phone) - if nicheng: - users = users.filter(kefu_profile__nicheng__icontains=nicheng) - if role_code: - # 筛选拥有该角色的用户 - user_ids_with_role = UserRole.objects.filter(role__role_code=role_code).values_list('account_id', flat=True) - users = users.filter(phone__in=user_ids_with_role) - - # 排序 - users = users.order_by('-create_time') - - # 分页 - paginator = Paginator(users, page_size) - page_obj = paginator.get_page(page) - - # 构建返回数据 - data_list = [] - for user in page_obj: - # 获取用户的所有角色 - user_roles = UserRole.objects.filter(account_id=user.phone).select_related('role') - roles_data = [{'role_code': ur.role.role_code, 'role_name': ur.role.role_name} for ur in user_roles] - data_list.append({ - 'phone': user.phone, - 'nicheng': user.kefu_profile.nicheng, - 'roles': roles_data, - 'status': user.kefu_profile.zhuangtai, - 'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S'), - 'update_time': user.update_time.strftime('%Y-%m-%d %H:%M:%S'), - }) - - return Response({ - 'code': 0, - 'data': { - 'list': data_list, - 'total': paginator.count, - 'page': page, - 'pageSize': page_size - } - })''' - - - - - -class ModifyAdminUserView(APIView): - """修改后台用户信息(昵称、密码、二级密码、状态、角色)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username', '').strip() - action = request.data.get('action') - if not username or not action: - return Response({'code': 400, 'msg': '缺少参数'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权进行此操作'}) - - target_phone = request.data.get('phone', '').strip() - if not target_phone: - return Response({'code': 400, 'msg': '缺少目标用户账号'}) - - try: - target_user = UserMain.objects.get(phone=target_phone, user_type='kefu') - target_kefu = target_user.kefu_profile - except UserMain.DoesNotExist: - return Response({'code': 404, 'msg': '用户不存在'}) - except UserKefu.DoesNotExist: - return Response({'code': 404, 'msg': '用户扩展信息缺失'}) - - with transaction.atomic(): - if action == 'update_user_info': - # 修改昵称、状态、密码、二级密码 - nicheng = request.data.get('nicheng') - status = request.data.get('status') - password = request.data.get('password') - erjimima = request.data.get('erjimima') - if nicheng is not None: - target_kefu.nicheng = nicheng - if status is not None: - target_kefu.zhuangtai = int(status) - if password: - if len(password) < 6: - return Response({'code': 400, 'msg': '密码至少6位'}) - target_user.password = password - if erjimima: - if len(erjimima) < 6: - return Response({'code': 400, 'msg': '二级密码至少6位'}) - target_kefu.erjimima = erjimima # 假设二级密码也需要加密 - target_user.save() - target_kefu.save() - return Response({'code': 0, 'msg': '修改成功'}) - - elif action == 'add_user_roles': - role_codes = request.data.get('role_codes', []) - if not role_codes: - return Response({'code': 400, 'msg': '请选择角色'}) - for rc in role_codes: - try: - role = Role.objects.get(role_code=rc) - except Role.DoesNotExist: - continue - UserRole.objects.get_or_create(account_id=target_phone, role=role) - return Response({'code': 0, 'msg': '角色添加成功'}) - - elif action == 'remove_user_role': - role_code = request.data.get('role_code') - if not role_code: - return Response({'code': 400, 'msg': '缺少角色编码'}) - try: - role = Role.objects.get(role_code=role_code) - except Role.DoesNotExist: - return Response({'code': 404, 'msg': '角色不存在'}) - UserRole.objects.filter(account_id=target_phone, role=role).delete() - return Response({'code': 0, 'msg': '角色移除成功'}) - - else: - return Response({'code': 400, 'msg': '无效的action'}) - - - - - -class AddAdminUserView(APIView): - """添加后台用户(客服账号)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权进行此操作'}) - - phone = request.data.get('phone', '').strip() - password = request.data.get('password', '').strip() - erjimima = request.data.get('erjimima', '').strip() - nicheng = request.data.get('nicheng', '').strip() - role_codes = request.data.get('role_codes', []) - - if not all([phone, password, erjimima, nicheng]): - return Response({'code': 400, 'msg': '请填写完整信息'}) - if len(password) < 6 or len(erjimima) < 6: - return Response({'code': 400, 'msg': '密码和二级密码至少6位'}) - - # 校验手机号是否已被占用(且 user_type='kefu') - if UserMain.objects.filter(phone=phone, user_type='kefu').exists(): - return Response({'code': 400, 'msg': '该账号已存在'}) - - # 生成唯一的 yonghuid(7位数字) - while True: - yonghuid = str(random.randint(1000000, 9999999)) - if not UserMain.objects.filter(yonghuid=yonghuid).exists(): - break - - # 生成唯一的 openid(格式:admin_{时间戳}_{随机6位}) - timestamp = str(int(time.time() * 1000)) - random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) - openid = f"admin_{timestamp}_{random_suffix}" - while UserMain.objects.filter(openid=openid).exists(): - random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) - openid = f"admin_{timestamp}_{random_suffix}" - - with transaction.atomic(): - user_main = UserMain.objects.create( - yonghuid=yonghuid, - openid=openid, - phone=phone, - password=password, - user_type='kefu' - ) - # 创建客服扩展表(后台用户扩展) - UserKefu.objects.create( - user=user_main, - nicheng=nicheng, - erjimima=erjimima, - zhuangtai=1 - ) - # 分配角色 - for rc in role_codes: - try: - role = Role.objects.get(role_code=rc) - UserRole.objects.create(account_id=phone, role=role) - except Role.DoesNotExist: - pass - - return Response({'code': 0, 'msg': '添加成功'}) - - - - - - - - -class KefuGetDashouListView(APIView): - """ - 客服获取打手列表接口(仅打手类型) - 需要拥有以下任一权限:001aa,001bb,001cc,001dd,001ee,001ff,001gg - 请求:POST /yonghu/kefuhqdslb - 参数: - username: 客服手机号(用于权限校验) - keyword: 搜索关键词(可选,搜索用户ID或昵称) - page: 页码(从1开始) - page_size: 每页数量 - 返回: - code: 0 - data: { - list: [打手对象], - has_more: bool, - total: int, // 总打手数 - valid_dashou_count: int // 有效打手数(有有效会员) - } - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数(前端字段为 username) - username = request.data.get('username', '').strip() - keyword = request.data.get('keyword', '').strip() - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - except ValueError: - return Response({'code': 400, 'msg': '分页参数错误'}) - - if not username: - return Response({'code': 401, 'msg': '认证失败'}) - - # 2. 公共权限校验(验证客服身份,获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否拥有打手管理相关权限(至少一个) - required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看打手列表'}) - - # 4. 构建打手查询集(仅打手类型) - dashou_qs = UserDashou.objects.select_related('user').all() - - # 关键词筛选(用户ID或昵称) - if keyword: - dashou_qs = dashou_qs.filter( - Q(user__yonghuid__icontains=keyword) | - Q(nicheng__icontains=keyword) - ) - - # 5. 统计打手总数 - total_dashou = dashou_qs.count() - - # 6. 统计有效打手数(有会员且未过期) - dashou_user_ids = dashou_qs.values_list('user__yonghuid', flat=True) - now = timezone.now() - valid_user_ids = Huiyuangoumai.objects.filter( - yonghu_id__in=dashou_user_ids, - huiyuan_zhuangtai=1, - daoqi_time__gt=now - ).values_list('yonghu_id', flat=True).distinct() - valid_dashou_count = valid_user_ids.count() - - # 7. 分页 - offset = (page - 1) * page_size - dashou_list = dashou_qs.order_by('-create_time')[offset:offset + page_size] - - # 8. 构建返回数据 - result = [] - for dashou in dashou_list: - user = dashou.user - result.append({ - 'yonghuid': user.yonghuid, - 'avatar': user.avatar or '', - 'zaixianzhuangtai': dashou.zaixianzhuangtai, - 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, - 'nicheng': dashou.nicheng or '', - 'zhuangtai': dashou.zhuangtai, - }) - - has_more = (offset + len(dashou_list)) < total_dashou - - return Response({ - 'code': 0, - 'data': { - 'list': result, - 'has_more': has_more, - 'total': total_dashou, - 'valid_dashou_count': valid_dashou_count, - } - }) - - - -class KefuGetDashouDetailView(APIView): - """ - 客服获取打手详情接口 - 请求:POST /yonghu/kefuhqdsxq - 参数:{ - "username": "客服手机号", - "uid": "打手ID (yonghuid)" - } - 认证:JWT - 返回:code=0 + 打手信息 + 会员列表 + 所有会员类型 + 客服权限列表 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - username = request.data.get('phone', '').strip() - uid = request.data.get('uid', '').strip() - - if not username or not uid: - return Response({'code': 400, 'msg': '缺少参数'}) - - # 2. 公共权限校验(验证客服身份,获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 3. 检查是否拥有打手管理相关权限(至少一个) - required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看打手详情'}) - - # 4. 查询打手主表和扩展表 - try: - user_main = UserMain.objects.select_related('dashou_profile').get( - yonghuid=uid - ) - except UserMain.DoesNotExist: - return Response({'code': 404, 'msg': '打手不存在'}) - - try: - dashou = user_main.dashou_profile - except ObjectDoesNotExist: - # 用户存在但未注册打手身份,返回基本信息 - user_info = { - 'yonghuid': user_main.yonghuid, - 'avatar': user_main.avatar or '', - 'zaixianzhuangtai': 0, - 'zhanghaozhuangtai': 0, - 'zhuangtai': 0, - 'nicheng': '', - 'chenghao': '', - 'dianhua': '', - 'wechat': '', - 'phone': user_main.phone or '', - 'create_time': user_main.create_time, - 'ip': user_main.ip or '', - 'jiedanzongliang': 0, - 'chengjiaozongliang': 0, - 'tuikuanliang': 0, - 'jinrijiedan': 0, - 'yue': 0.00, - 'zonge': 0.00, - 'jifen': 0, - 'yajin': 0.00, - 'jinrishouyi': 0.00, - 'jinyueshouyi': 0.00, - 'yaoqingren': '', - 'jieshao': '', - } - member_list = [] - # 仍然需要返回 all_members 和 permissions - all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao')) - return Response({ - 'code': 0, - 'data': { - 'user_info': user_info, - 'member_list': member_list, - 'all_members': all_members, - 'permissions': permissions, - } - }) - - # 5. 构建打手信息 - user_info = { - 'yonghuid': user_main.yonghuid, - 'avatar': user_main.avatar or '', - 'zaixianzhuangtai': dashou.zaixianzhuangtai, - 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, - 'zhuangtai': dashou.zhuangtai, - 'nicheng': dashou.nicheng or '', - 'chenghao': dashou.chenghao or '', - 'dianhua': dashou.dianhua or '', - 'wechat': dashou.wechat or '', - 'phone': user_main.phone or '', - 'create_time': user_main.create_time, - 'ip': user_main.ip or '', - 'jiedanzongliang': dashou.jiedanzongliang, - 'chengjiaozongliang': dashou.chengjiaozongliang, - 'tuikuanliang': dashou.tuikuanliang, - 'jinrijiedan': dashou.jinrijiedan, - 'yue': float(dashou.yue) if dashou.yue else 0.00, - 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, - 'jifen': dashou.jifen, - 'yajin': float(dashou.yajin) if dashou.yajin else 0.00, - 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00, - 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00, - 'yaoqingren': dashou.yaoqingren or '', - 'jieshao': dashou.jieshao or '', - } - - # 6. 查询打手已拥有的会员(未过期) - member_records = Huiyuangoumai.objects.filter( - yonghu_id=uid - ).order_by('-create_time') - member_list = [] - for record in member_records: - # 检查是否过期 - if hasattr(record, 'jiance_shifou_daoqi'): - is_expired = record.jiance_shifou_daoqi() - else: - is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True - - if not is_expired: - try: - huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id) - huiyuan_name = huiyuan.jieshao - except Huiyuan.DoesNotExist: - huiyuan_name = '未知会员' - member_list.append({ - 'id': record.id, - 'huiyuan_name': huiyuan_name, - 'daoqi_time': record.daoqi_time, - 'is_active': record.huiyuan_zhuangtai == 1, - 'huiyuan_id': record.huiyuan_id, # 用于删除操作 - }) - - # 7. 查询所有会员类型(用于前端添加会员) - all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao')) - - return Response({ - 'code': 0, - 'data': { - 'user_info': user_info, - 'member_list': member_list, - 'all_members': all_members, - 'permissions': permissions, # 返回当前客服的权限列表 - } - }) - - - - -'''class KefuGetDashouDetailView(APIView): - """ - 客服获取打手详情接口 - 请求:POST /yonghu/kefuhqdsxq - 参数:{ - "phone": "客服手机号", - "uid": "打手ID (yonghuid)" - } - 认证:JWT - 返回:code=0 + 打手信息 + 会员列表 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - username = request.data.get('phone', '').strip() - uid = request.data.get('uid', '').strip() - - if not username or not uid: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 客服身份验证 - current_user = request.user - - # 2. 公共权限校验(验证客服身份,获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否拥有打手管理相关权限(至少一个) - required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看打手列表'}) - - # 3. 查询打手主表和扩展表 - try: - # 使用 select_related 预加载 dashou_profile,减少数据库查询 - user_main = UserMain.objects.select_related('dashou_profile').get( - yonghuid=uid - ) - except UserMain.DoesNotExist: - return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND) - - # 验证用户类型是否为打手(可选,但建议验证) - # 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册) - try: - dashou = user_main.dashou_profile - except ObjectDoesNotExist: - # 用户存在但未注册打手身份,返回基本信息 - user_info = { - 'yonghuid': user_main.yonghuid, - 'avatar': user_main.avatar or '', - 'zaixianzhuangtai': 0, - 'zhanghaozhuangtai': 0, - 'zhuangtai': 0, - 'nicheng': '', - 'chenghao': '', - 'dianhua': '', - 'wechat': '', - 'phone': user_main.phone or '', - 'create_time': user_main.create_time, - 'ip': user_main.ip or '', - 'jiedanzongliang': 0, - 'chengjiaozongliang': 0, - 'tuikuanliang': 0, - 'jinrijiedan': 0, - 'yue': 0.00, - 'zonge': 0.00, - 'jifen': 0, - 'yajin': 0.00, - 'jinrishouyi': 0.00, - 'jinyueshouyi': 0.00, - 'yaoqingren': '', - 'jieshao': '', - } - member_list = [] - return Response({ - 'code': 0, - 'data': { - 'user_info': user_info, - 'member_list': member_list - } - }) - - # 4. 构建打手信息 - user_info = { - 'yonghuid': user_main.yonghuid, - 'avatar': user_main.avatar or '', - 'zaixianzhuangtai': dashou.zaixianzhuangtai, - 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, - 'zhuangtai': dashou.zhuangtai, - 'nicheng': dashou.nicheng or '', - 'chenghao': dashou.chenghao or '', - 'dianhua': dashou.dianhua or '', - 'wechat': dashou.wechat or '', - 'phone': user_main.phone or '', - 'create_time': user_main.create_time, - 'ip': user_main.ip or '', - 'jiedanzongliang': dashou.jiedanzongliang, - 'chengjiaozongliang': dashou.chengjiaozongliang, - 'tuikuanliang': dashou.tuikuanliang, - 'jinrijiedan': dashou.jinrijiedan, - 'yue': float(dashou.yue) if dashou.yue else 0.00, - 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, - 'jifen': dashou.jifen, - 'yajin': float(dashou.yajin) if dashou.yajin else 0.00, - 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00, - 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00, - 'yaoqingren': dashou.yaoqingren or '', - 'jieshao': dashou.jieshao or '', - } - - # 5. 查询打手会员信息(仅未过期) - member_records = Huiyuangoumai.objects.filter( - yonghu_id=uid - ).order_by('-create_time') - - member_list = [] - for record in member_records: - # 调用模型方法检查是否过期 - if hasattr(record, 'jiance_shifou_daoqi'): - is_expired = record.jiance_shifou_daoqi() - else: - # 如果方法不存在,简单判断 - is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True - - if not is_expired: - # 获取会员详情 - try: - huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id) - huiyuan_name = huiyuan.jieshao - except Huiyuan.DoesNotExist: - huiyuan_name = '未知会员' - - member_list.append({ - 'id': record.id, - 'huiyuan_name': huiyuan_name, - 'daoqi_time': record.daoqi_time, - 'is_active': record.huiyuan_zhuangtai == 1, - }) - - return Response({ - 'code': 0, - 'data': { - 'user_info': user_info, - 'member_list': member_list - } - })''' - - - - - - -class KefuUpdateDashouView(APIView): - """ - 客服修改打手信息接口 - 支持操作: - - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb) - - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb) - - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd) - - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee) - - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee) - - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee) - - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg) - - gai_chenghao : 修改打手称号(升级/降级金牌) (权限001hh) - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - dashou_id = request.data.get('dashou_id', '').strip() - caozuo = request.data.get('caozuo', '').strip() - value = request.data.get('value') # 金额、积分、天数、称号等 - huiyuan_id = request.data.get('huiyuan_id', '').strip() - - if not username or not dashou_id or not caozuo: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 3. 查询打手 - try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - except UserMain.DoesNotExist: - return Response({'code': 404, 'msg': '打手不存在'}) - except UserDashou.DoesNotExist: - return Response({'code': 404, 'msg': '打手扩展信息缺失'}) - - with transaction.atomic(): - try: - before = { - 'yue': dashou_profile.yue, - 'yajin': dashou_profile.yajin, - 'jifen': dashou_profile.jifen, - 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai, - 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, - 'zhuangtai': dashou_profile.zhuangtai, - 'chenghao': dashou_profile.chenghao, - } - - # ---------- 余额操作 ---------- - if caozuo == 'jian_yue': - if '001aa' not in permissions: - return Response({'code': 403, 'msg': '无权限减打手余额'}) - amount = Decimal(str(value)) - if dashou_profile.yue < amount: - return Response({'code': 400, 'msg': '余额不足'}) - dashou_profile.yue -= amount - dashou_profile.save() - after = dashou_profile.yue - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - xiugaitijiao=after, - xiugaitijiaoq=before['yue'], - ) - return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after)}}) - - elif caozuo == 'jia_yue': - if '001bb' not in permissions: - return Response({'code': 403, 'msg': '无权限加打手余额'}) - amount = Decimal(str(value)) - dashou_profile.yue += amount - dashou_profile.save() - after = dashou_profile.yue - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - xiugaitijiao=after, - xiugaitijiaoq=before['yue'], - ) - return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after)}}) - - # ---------- 押金操作 ---------- - elif caozuo == 'jian_yajin': - if '001aa' not in permissions: - return Response({'code': 403, 'msg': '无权限减打手押金'}) - amount = Decimal(str(value)) - if dashou_profile.yajin < amount: - return Response({'code': 400, 'msg': '押金不足'}) - dashou_profile.yajin -= amount - dashou_profile.save() - after = dashou_profile.yajin - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - yajin=after, - yyajin=before['yajin'], - ) - return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after)}}) - - elif caozuo == 'jia_yajin': - if '001bb' not in permissions: - return Response({'code': 403, 'msg': '无权限加打手押金'}) - amount = Decimal(str(value)) - dashou_profile.yajin += amount - dashou_profile.save() - after = dashou_profile.yajin - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - yajin=after, - yyajin=before['yajin'], - ) - return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after)}}) - - # ---------- 积分操作 ---------- - elif caozuo == 'jian_jifen': - if '001cc' not in permissions: - return Response({'code': 403, 'msg': '无权限减打手积分'}) - jifen = int(value) - if dashou_profile.jifen < jifen: - return Response({'code': 400, 'msg': '积分不足'}) - dashou_profile.jifen -= jifen - dashou_profile.save() - after = dashou_profile.jifen - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - jifen=after, - yjifen=before['jifen'], - ) - return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after}}) - - elif caozuo == 'jia_jifen': - if '001dd' not in permissions: - return Response({'code': 403, 'msg': '无权限加打手积分'}) - jifen = int(value) - dashou_profile.jifen += jifen - dashou_profile.save() - after = dashou_profile.jifen - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - jifen=after, - yjifen=before['jifen'], - ) - return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after}}) - - # ---------- 状态修改 ---------- - elif caozuo == 'gai_zhanghaozhuangtai': - if '001ee' not in permissions: - return Response({'code': 403, 'msg': '无权限修改打手账号状态'}) - new_status = int(value) - if new_status not in [0, 1]: - return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'}) - dashou_profile.zhanghaozhuangtai = new_status - dashou_profile.save() - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - qitashuoming=f'修改账号状态为{new_status}', - ) - return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}}) - - elif caozuo == 'gai_zaixianzhuangtai': - if '001ee' not in permissions: - return Response({'code': 403, 'msg': '无权限修改打手在线状态'}) - new_status = int(value) - if new_status not in [0, 1]: - return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'}) - dashou_profile.zaixianzhuangtai = new_status - dashou_profile.save() - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - qitashuoming=f'修改在线状态为{new_status}', - ) - return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}}) - - elif caozuo == 'gai_zhuangtai': - if '001ee' not in permissions: - return Response({'code': 403, 'msg': '无权限修改打手接单状态'}) - new_status = int(value) - if new_status not in [0, 1]: - return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'}) - dashou_profile.zhuangtai = new_status - dashou_profile.save() - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - qitashuoming=f'修改接单状态为{new_status}', - ) - return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}}) - - # ---------- 会员操作 ---------- - elif caozuo == 'jia_huiyuan': - if '001ff' not in permissions: - return Response({'code': 403, 'msg': '无权限给打手加会员'}) - if not huiyuan_id: - return Response({'code': 400, 'msg': '缺少会员ID'}) - days = int(value) if value else 30 - try: - huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id) - except Huiyuan.DoesNotExist: - return Response({'code': 404, 'msg': '会员类型不存在'}) - record, created = Huiyuangoumai.objects.select_for_update().get_or_create( - yonghu_id=dashou_id, - huiyuan_id=huiyuan_id, - defaults={ - 'huiyuan_zhuangtai': 1, - 'jieshao': huiyuan.jieshao, - 'daoqi_time': timezone.now() + timedelta(days=days), - } - ) - if not created: - new_daoqi = record.daoqi_time + timedelta(days=days) - record.daoqi_time = new_daoqi - record.huiyuan_zhuangtai = 1 - record.save() - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - huiyuants=days, - huiyuan_id=huiyuan_id, - qitashuoming=f'加会员 {huiyuan_id},增加{days}天', - ) - return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}}) - - elif caozuo == 'jian_huiyuan': - if '001gg' not in permissions: - return Response({'code': 403, 'msg': '无权限给打手减会员'}) - if not huiyuan_id: - return Response({'code': 400, 'msg': '缺少会员ID'}) - try: - record = Huiyuangoumai.objects.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id) - except Huiyuangoumai.DoesNotExist: - return Response({'code': 404, 'msg': '该打手未购买此会员'}) - record.delete() - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - huiyuan_id=huiyuan_id, - qitashuoming=f'移除会员 {huiyuan_id}', - ) - return Response({'code': 0, 'msg': '减会员成功'}) - - # ---------- 称号修改(金牌打手) ---------- - elif caozuo == 'gai_chenghao': - if '001hh' not in permissions: - return Response({'code': 403, 'msg': '无权限修改打手称号'}) - new_chenghao = str(value).strip() - if not new_chenghao: - return Response({'code': 400, 'msg': '称号不能为空'}) - # 可限制合法值,这里只记录日志 - dashou_profile.chenghao = new_chenghao - dashou_profile.save() - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - qitashuoming=f'修改称号:{before["chenghao"] or "无"} → {new_chenghao}', - ) - return Response({'code': 0, 'msg': '修改称号成功', 'data': {'new_chenghao': new_chenghao}}) - - else: - return Response({'code': 400, 'msg': '未知操作类型'}) - - except Exception as e: - logger.error(f"修改打手信息异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - - -class KefuGetShangjiaListView(APIView): - """ - 客服/管理员获取商家列表接口 - 需要拥有以下任一权限:1100a, 1100b - 请求:POST /houtai/hqsjgllb - 参数: - username: 客服手机号(用于权限校验,必填) - keyword: 搜索关键词(可选,搜索商家用户ID或昵称) - balance_op: 余额比较符,gte(>=) / lte(<=)(可选) - balance_value: 余额数值(可选,需与balance_op同时存在) - order_count_op: 派单量比较符,gte(>=) / lte(<=)(可选) - order_count_value: 派单量数值(可选,需与order_count_op同时存在) - status: 商家状态,1=正常,2=禁用(可选) - page: 页码,从1开始,默认1 - page_size: 每页数量,默认20 - 返回: - code: 0 成功,其他失败 - data: { - list: [商家对象], - total: int, // 总商家数 - } - """ - permission_classes = [] # 不依赖全局认证,手动校验 - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - username = request.data.get('username', '').strip() - keyword = request.data.get('keyword', '').strip() - balance_op = request.data.get('balance_op', '').strip().lower() - balance_value_str = request.data.get('balance_value') - order_count_op = request.data.get('order_count_op', '').strip().lower() - order_count_value_str = request.data.get('order_count_value') - status_str = request.data.get('status') - - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - if page < 1 or page_size < 1: - raise ValueError - except ValueError: - return Response({'code': 400, 'msg': '分页参数错误'}) - - if not username: - return Response({'code': 401, 'msg': '认证失败,缺少username'}) - - # 2. 公共权限校验(复用现有函数 verify_kefu_permission) - # 假设 verify_kefu_permission 定义在某个公共模块,这里引入 - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查商家管理权限(1100a 或 1100b) - required_perms = {'1100a', '1100b'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看商家列表'}) - - # 4. 构建商家查询集(商家扩展表 + 关联用户主表) - shangjia_qs = UserShangjia.objects.select_related('user').all() - - # 关键词筛选(用户ID或昵称) - if keyword: - shangjia_qs = shangjia_qs.filter( - Q(user__yonghuid__icontains=keyword) | - Q(nicheng__icontains=keyword) - ) - - # 余额筛选 - if balance_op and balance_value_str is not None and balance_value_str != '': - try: - balance_value = Decimal(str(balance_value_str)) - if balance_op == 'gte': - shangjia_qs = shangjia_qs.filter(yue__gte=balance_value) - elif balance_op == 'lte': - shangjia_qs = shangjia_qs.filter(yue__lte=balance_value) - else: - return Response({'code': 400, 'msg': '余额比较符只支持gte或lte'}) - except Exception: - return Response({'code': 400, 'msg': '余额数值格式错误'}) - - # 派单总量筛选(fabu字段) - if order_count_op and order_count_value_str is not None and order_count_value_str != '': - try: - order_count_value = int(order_count_value_str) - if order_count_op == 'gte': - shangjia_qs = shangjia_qs.filter(fabu__gte=order_count_value) - elif order_count_op == 'lte': - shangjia_qs = shangjia_qs.filter(fabu__lte=order_count_value) - else: - return Response({'code': 400, 'msg': '派单量比较符只支持gte或lte'}) - except Exception: - return Response({'code': 400, 'msg': '派单量数值必须为整数'}) - - # 商家状态筛选 - if status_str is not None and status_str != '': - try: - status_val = int(status_str) - if status_val not in [1, 2]: - return Response({'code': 400, 'msg': '商家状态值无效,应为1或2'}) - shangjia_qs = shangjia_qs.filter(zhuangtai=status_val) - except ValueError: - return Response({'code': 400, 'msg': '商家状态格式错误'}) - - # 5. 统计总商家数(应用筛选后) - total = shangjia_qs.count() - - # 6. 分页 - offset = (page - 1) * page_size - shangjia_list = shangjia_qs.order_by('-create_time')[offset:offset + page_size] - - # 7. 构建返回数据 - result = [] - for shangjia in shangjia_list: - user = shangjia.user - result.append({ - 'yonghuid': user.yonghuid, - 'avatar': user.avatar or '', - 'nicheng': shangjia.nicheng or '', - 'yue': str(shangjia.yue), # 转换为字符串,避免前端精度问题 - 'fabu': shangjia.fabu, - 'chengjiao': shangjia.chengjiao, - 'zhuangtai': shangjia.zhuangtai, # 1=正常,2=禁用 - }) - - return Response({ - 'code': 0, - 'data': { - 'list': result, - 'total': total, - } - }) - - - - - - -# yonghu/views.py 中添加 - -class KefuGetShangjiaDetailView(APIView): - """ - 客服/管理员获取商家详情接口 - 需要权限:1100a 或 1100b - 请求:POST /houtai/hthqsjxq - 参数: - username: 客服手机号(用于权限校验,必填) - yonghuid: 商家用户ID(必填) - 返回: - code: 0 成功 - data: 商家所有字段(含主表头像等) - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - yonghuid = request.data.get('yonghuid', '').strip() - - if not username: - return Response({'code': 401, 'msg': '认证失败,缺少username'}) - if not yonghuid: - return Response({'code': 400, 'msg': '缺少商家ID'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - required_perms = {'1100a', '1100b'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看商家详情'}) - - # 查询商家 - try: - shangjia = UserShangjia.objects.select_related('user').get(user__yonghuid=yonghuid) - except UserShangjia.DoesNotExist: - return Response({'code': 404, 'msg': '商家不存在'}) - - user = shangjia.user - - data = { - 'yonghuid': user.yonghuid, - 'avatar': user.avatar or '', - 'nicheng': shangjia.nicheng, - 'zhuangtai': shangjia.zhuangtai, # 1正常,2禁用 - 'dianhua': shangjia.dianhua, - 'wechat': shangjia.wechat, - 'yue': str(shangjia.yue), - 'fabu': shangjia.fabu, - 'chengjiao': shangjia.chengjiao, - 'tuikuan': shangjia.tuikuan, - 'jinridingdan': shangjia.jinridingdan, - 'jinriliushui': str(shangjia.jinriliushui), - 'jinyuedingdan': shangjia.jinyuedingdan, - 'jinyueliushui': str(shangjia.jinyueliushui), - 'create_time': shangjia.create_time.isoformat() if shangjia.create_time else None, - } - - return Response({'code': 0, 'data': data}) - - - - - - - -class KefuUpdateShangjiaView(APIView): - """ - 客服/管理员修改商家信息(余额、状态) - POST /houtai/xgsjxx - 参数: - username, yonghuid, action - action可选:add_balance, sub_balance, ban, unban - amount(add_balance/sub_balance时必填) - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - yonghuid = request.data.get('yonghuid', '').strip() - action = request.data.get('action', '').strip().lower() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not yonghuid: - return Response({'code': 400, 'msg': '缺少商家ID'}) - if action not in ('add_balance', 'sub_balance', 'ban', 'unban'): - return Response({'code': 400, 'msg': 'action参数无效'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 操作权限校验 - if action in ('add_balance', 'ban', 'unban') and '1100a' not in permissions: - return Response({'code': 403, 'msg': '无权限执行此操作(需要1100a)'}) - if action == 'sub_balance' and '1100b' not in permissions: - return Response({'code': 403, 'msg': '无权限减少商家余额(需要1100b)'}) - - # 使用事务,select_for_update 必须在事务内 - with transaction.atomic(): - try: - shangjia = UserShangjia.objects.select_for_update().get(user__yonghuid=yonghuid) - except UserShangjia.DoesNotExist: - return Response({'code': 404, 'msg': '商家不存在'}) - - # 增加余额 - if action == 'add_balance': - amount_str = request.data.get('amount') - try: - amount = Decimal(str(amount_str)) - if amount <= 0 or amount > 1000000: - return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) - except: - return Response({'code': 400, 'msg': '金额格式错误'}) - new_yue = shangjia.yue + amount - if new_yue > 999999999.99: - return Response({'code': 400, 'msg': '增加后余额超出上限'}) - shangjia.yue = new_yue - shangjia.save(update_fields=['yue']) - return Response({'code': 0, 'msg': '余额增加成功', 'data': {'new_balance': str(shangjia.yue)}}) - - # 减少余额 - if action == 'sub_balance': - amount_str = request.data.get('amount') - try: - amount = Decimal(str(amount_str)) - if amount <= 0 or amount > 1000000: - return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) - except: - return Response({'code': 400, 'msg': '金额格式错误'}) - if shangjia.yue < amount: - return Response({'code': 400, 'msg': '余额不足'}) - shangjia.yue = shangjia.yue - amount - shangjia.save(update_fields=['yue']) - return Response({'code': 0, 'msg': '余额减少成功', 'data': {'new_balance': str(shangjia.yue)}}) - - # 封禁 - if action == 'ban': - if shangjia.zhuangtai == 2: - return Response({'code': 400, 'msg': '商家已是封禁状态'}) - shangjia.zhuangtai = 2 - shangjia.save(update_fields=['zhuangtai']) - return Response({'code': 0, 'msg': '商家已封禁', 'data': {'zhuangtai': 2}}) - - # 解封 - if action == 'unban': - if shangjia.zhuangtai == 1: - return Response({'code': 400, 'msg': '商家已是正常状态'}) - shangjia.zhuangtai = 1 - shangjia.save(update_fields=['zhuangtai']) - return Response({'code': 0, 'msg': '商家已解封', 'data': {'zhuangtai': 1}}) - - - - - - - -class GetProductBaseDataView(APIView): - """ - 获取商品基础数据(类型、专区、会员) - 权限:2200a - POST /houtai/htsphqjcsx - 参数:username - 返回: - code:0, data: { - type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...], - zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...], - member_list: [ {huiyuan_id, jieshao, jiage}, ...] - } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 获取所有商品类型(按 paixu 升序) - type_qs = ShangpinLeixing.objects.all().order_by('paixu') - type_list = [] - for t in type_qs: - # 🔥 修改:统计该类型下的公共商品数量(店铺为空且店铺商品类型为空) - product_count = Shangpin.objects.filter( - leixing_id=t.id, - dianpu__isnull=True, # 所属店铺为空 - dianpu_leixing__isnull=True # 店铺商品类型为空 - ).count() - type_list.append({ - 'id': t.id, - 'jieshao': t.jieshao or '', - 'tupian_url': t.tupian_url or '', - 'paixu': t.paixu, - 'shenhezhuangtai': t.shenhezhuangtai, - 'product_count': product_count, - }) - - # 获取所有专区(按 paixu 升序) - zone_qs = ShangpinZhuanqu.objects.all().order_by('paixu') - zone_list = [] - for z in zone_qs: - # 🔥 修改:统计该专区下的公共商品数量(店铺为空且店铺商品类型为空) - product_count = Shangpin.objects.filter( - zhuanqu_id=z.id, - dianpu__isnull=True, # 所属店铺为空 - dianpu_leixing__isnull=True # 店铺商品类型为空 - ).count() - zone_list.append({ - 'id': z.id, - 'mingzi': z.mingzi or '', - 'leixing_id': z.leixing_id, - 'paixu': z.paixu, - 'shenhezhuangtai': z.shenhezhuangtai, - 'product_count': product_count, - }) - - # 获取所有会员(只需 ID、介绍、价格) - member_list = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage')) - - return Response({ - 'code': 0, - 'data': { - 'type_list': type_list, - 'zone_list': zone_list, - 'member_list': member_list, - } - }) - - - -class GetProductListView(APIView): - """ - 获取商品列表(支持筛选、排序) - 权限:2200a - POST /houtai/hthqsplb - 参数: - username: 必填 - leixing_id: 类型ID(可选) - zhuanqu_id: 专区ID(可选) - keyword: 搜索关键词(商品标题/介绍) - sales_op: 销量比较符 gte/lte - sales_value: 销量数值 - stock_op: 库存比较符 gte/lte - stock_value: 库存数值 - sort_field: 排序字段 zhenshi_xiaoliang / jiage - sort_order: asc/desc - page: 页码(默认1) - page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留) - 返回: - code:0, data: { list: [...], total } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 获取参数 - leixing_id = request.data.get('leixing_id') - zhuanqu_id = request.data.get('zhuanqu_id') - keyword = request.data.get('keyword', '').strip() - sales_op = request.data.get('sales_op', '').strip().lower() - sales_value = request.data.get('sales_value') - stock_op = request.data.get('stock_op', '').strip().lower() - stock_value = request.data.get('stock_value') - sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage - sort_order = request.data.get('sort_order', 'desc').lower() - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 1000)) - if page < 1 or page_size < 1: - raise ValueError - except: - return Response({'code': 400, 'msg': '分页参数错误'}) - - # 🔥 修改:构建查询集,增加公共商品筛选(店铺为空且店铺商品类型为空) - qs = Shangpin.objects.filter( - dianpu__isnull=True, # 所属店铺为空 - dianpu_leixing__isnull=True # 店铺商品类型为空 - ) - - if leixing_id: - try: - leixing_id = int(leixing_id) - qs = qs.filter(leixing_id=leixing_id) - except: - pass - if zhuanqu_id: - try: - zhuanqu_id = int(zhuanqu_id) - qs = qs.filter(zhuanqu_id=zhuanqu_id) - except: - pass - if keyword: - qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword)) - - # 销量筛选 - if sales_op and sales_value is not None: - try: - sales_val = int(sales_value) - if sales_op == 'gte': - qs = qs.filter(zhenshi_xiaoliang__gte=sales_val) - elif sales_op == 'lte': - qs = qs.filter(zhenshi_xiaoliang__lte=sales_val) - except: - pass - - # 库存筛选 - if stock_op and stock_value is not None: - try: - stock_val = int(stock_value) - if stock_op == 'gte': - qs = qs.filter(kucun__gte=stock_val) - elif stock_op == 'lte': - qs = qs.filter(kucun__lte=stock_val) - except: - pass - - # 排序 - if sort_field in ('zhenshi_xiaoliang', 'jiage'): - if sort_order == 'asc': - qs = qs.order_by(sort_field) - else: - qs = qs.order_by(f'-{sort_field}') - else: - qs = qs.order_by('-create_time') # 默认按创建时间倒序 - - # 分页 - total = qs.count() - offset = (page - 1) * page_size - products = qs[offset:offset + page_size] - - # 构建返回数据 - product_list = [] - for p in products: - product_list.append({ - 'id': p.id, - 'biaoqian': p.biaoqian or '', - 'jiage': str(p.jiage), - 'kucun': p.kucun, - 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, - 'tupian_url': p.tupian_url or '', - 'jieshao': p.jieshao or '', - }) - - return Response({ - 'code': 0, - 'data': { - 'list': product_list, - 'total': total, - } - }) - - - - -'''class GetProductBaseDataView(APIView): - """ - 获取商品基础数据(类型、专区、会员) - 权限:2200a - POST /houtai/htsphqjcsx - 参数:username - 返回: - code:0, data: { - type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...], - zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...], - member_list: [ {huiyuan_id, jieshao, jiage}, ...] - } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 获取所有商品类型(按 paixu 升序) - type_qs = ShangpinLeixing.objects.all().order_by('paixu') - type_list = [] - for t in type_qs: - # 统计该类型下的商品数量(忽略审核状态?通常统计所有商品,可根据需求调整) - product_count = Shangpin.objects.filter(leixing_id=t.id).count() - type_list.append({ - 'id': t.id, - 'jieshao': t.jieshao or '', - 'tupian_url': t.tupian_url or '', - 'paixu': t.paixu, - 'shenhezhuangtai': t.shenhezhuangtai, - 'product_count': product_count, - }) - - # 获取所有专区(按 paixu 升序) - zone_qs = ShangpinZhuanqu.objects.all().order_by('paixu') - zone_list = [] - for z in zone_qs: - product_count = Shangpin.objects.filter(zhuanqu_id=z.id).count() - zone_list.append({ - 'id': z.id, - 'mingzi': z.mingzi or '', - 'leixing_id': z.leixing_id, - 'paixu': z.paixu, - 'shenhezhuangtai': z.shenhezhuangtai, - 'product_count': product_count, - }) - - # 获取所有会员(只需 ID、介绍、价格) - member_list = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage')) - - return Response({ - 'code': 0, - 'data': { - 'type_list': type_list, - 'zone_list': zone_list, - 'member_list': member_list, - } - }) - - -class GetProductListView(APIView): - """ - 获取商品列表(支持筛选、排序) - 权限:2200a - POST /houtai/hthqsplb - 参数: - username: 必填 - leixing_id: 类型ID(可选) - zhuanqu_id: 专区ID(可选) - keyword: 搜索关键词(商品标题/介绍) - sales_op: 销量比较符 gte/lte - sales_value: 销量数值 - stock_op: 库存比较符 gte/lte - stock_value: 库存数值 - sort_field: 排序字段 zhenshi_xiaoliang / jiage - sort_order: asc/desc - page: 页码(默认1) - page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留) - 返回: - code:0, data: { list: [...], total } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 获取参数 - leixing_id = request.data.get('leixing_id') - zhuanqu_id = request.data.get('zhuanqu_id') - keyword = request.data.get('keyword', '').strip() - sales_op = request.data.get('sales_op', '').strip().lower() - sales_value = request.data.get('sales_value') - stock_op = request.data.get('stock_op', '').strip().lower() - stock_value = request.data.get('stock_value') - sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage - sort_order = request.data.get('sort_order', 'desc').lower() - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 1000)) - if page < 1 or page_size < 1: - raise ValueError - except: - return Response({'code': 400, 'msg': '分页参数错误'}) - - # 构建查询集 - qs = Shangpin.objects.all() - - if leixing_id: - try: - leixing_id = int(leixing_id) - qs = qs.filter(leixing_id=leixing_id) - except: - pass - if zhuanqu_id: - try: - zhuanqu_id = int(zhuanqu_id) - qs = qs.filter(zhuanqu_id=zhuanqu_id) - except: - pass - if keyword: - qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword)) - - # 销量筛选 - if sales_op and sales_value is not None: - try: - sales_val = int(sales_value) - if sales_op == 'gte': - qs = qs.filter(zhenshi_xiaoliang__gte=sales_val) - elif sales_op == 'lte': - qs = qs.filter(zhenshi_xiaoliang__lte=sales_val) - except: - pass - - # 库存筛选 - if stock_op and stock_value is not None: - try: - stock_val = int(stock_value) - if stock_op == 'gte': - qs = qs.filter(kucun__gte=stock_val) - elif stock_op == 'lte': - qs = qs.filter(kucun__lte=stock_val) - except: - pass - - # 排序 - if sort_field in ('zhenshi_xiaoliang', 'jiage'): - if sort_order == 'asc': - qs = qs.order_by(sort_field) - else: - qs = qs.order_by(f'-{sort_field}') - else: - qs = qs.order_by('-create_time') # 默认按创建时间倒序 - - # 分页 - total = qs.count() - offset = (page - 1) * page_size - products = qs[offset:offset + page_size] - - # 构建返回数据 - product_list = [] - for p in products: - product_list.append({ - 'id': p.id, - 'biaoqian': p.biaoqian or '', - 'jiage': str(p.jiage), - 'kucun': p.kucun, - 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, - 'tupian_url': p.tupian_url or '', - 'jieshao': p.jieshao or '', - }) - - return Response({ - 'code': 0, - 'data': { - 'list': product_list, - 'total': total, - } - })''' - - -class SaveProductOrderView(APIView): - """ - 保存商品类型或专区的排序 - 权限:2200a - POST /houtai/htsppaixu - 参数: - username: 必填 - type: 'leixing' 或 'zhuanqu' - leixing_id: 当 type='zhuanqu' 时必填(用于限定该类型下的专区) - items: [ {id, paixu}, ... ] # 每个项目的新排序值(从1开始) - 返回: - code:0 成功 - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - sort_type = request.data.get('type', '').strip().lower() - if sort_type not in ('leixing', 'zhuanqu'): - return Response({'code': 400, 'msg': 'type参数必须为leixing或zhuanqu'}) - - items = request.data.get('items') - if not isinstance(items, list) or len(items) == 0: - return Response({'code': 400, 'msg': 'items参数无效'}) - - # 校验每个 item 的格式 - for item in items: - if not isinstance(item, dict) or 'id' not in item or 'paixu' not in item: - return Response({'code': 400, 'msg': 'items内每项需包含id和paixu'}) - try: - item['id'] = int(item['id']) - item['paixu'] = int(item['paixu']) - if item['paixu'] < 1: - return Response({'code': 400, 'msg': 'paixu必须为正整数'}) - except: - return Response({'code': 400, 'msg': 'id或paixu格式错误'}) - - with transaction.atomic(): - if sort_type == 'leixing': - # 更新商品类型的排序 - for item in items: - ShangpinLeixing.objects.filter(id=item['id']).update(paixu=item['paixu']) - else: # zhuanqu - leixing_id = request.data.get('leixing_id') - if not leixing_id: - return Response({'code': 400, 'msg': '更新专区排序时必须提供leixing_id'}) - try: - leixing_id = int(leixing_id) - except: - return Response({'code': 400, 'msg': 'leixing_id格式错误'}) - # 可选:检查该专区是否属于该类型,但不强制,由前端保证 - for item in items: - ShangpinZhuanqu.objects.filter(id=item['id']).update(paixu=item['paixu']) - - return Response({'code': 0, 'msg': '排序保存成功'}) - - -class AddProductView(APIView): - """ - 添加商品(含图片上传) - 权限:2200a - POST /houtai/htfbsp - 参数:multipart/form-data,包含所有商品字段及图片文件 - 字段说明见代码 - """ - permission_classes = [] - parser_classes = [MultiPartParser, FormParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 获取表单字段 - biaoqian = request.data.get('biaoqian', '').strip() - jiage_str = request.data.get('jiage') - kucun_str = request.data.get('kucun') - duiwai_xiaoliang_str = request.data.get('duiwai_xiaoliang', '0') - leixing_id_str = request.data.get('leixing_id') - zhuanqu_id_str = request.data.get('zhuanqu_id') - yaoqiuleixing_str = request.data.get('yaoqiuleixing', '1') - huiyuan_id = request.data.get('huiyuan_id', '').strip() - yongjin_str = request.data.get('yongjin', '0') - fencheng_type = request.data.get('fencheng_type', 'default') - ewai_dashou_fencheng_str = request.data.get('ewai_dashou_fencheng', '0') - jieshao = request.data.get('jieshao', '').strip() - xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip() - # 图片文件 - image_file = request.FILES.get('file') # 前端上传时字段名为 'file' - - # 必填字段校验 - if not biaoqian: - return Response({'code': 400, 'msg': '商品标题不能为空'}) - try: - jiage = Decimal(jiage_str) - if jiage <= 0: - return Response({'code': 400, 'msg': '价格必须大于0'}) - except: - return Response({'code': 400, 'msg': '价格格式错误'}) - try: - kucun = int(kucun_str) - if kucun < 0: - return Response({'code': 400, 'msg': '库存不能为负数'}) - except: - return Response({'code': 400, 'msg': '库存格式错误'}) - try: - duiwai_xiaoliang = int(duiwai_xiaoliang_str) - except: - duiwai_xiaoliang = 0 - try: - leixing_id = int(leixing_id_str) - except: - return Response({'code': 400, 'msg': '商品类型ID无效'}) - try: - zhuanqu_id = int(zhuanqu_id_str) - except: - return Response({'code': 400, 'msg': '商品专区ID无效'}) - try: - yaoqiuleixing = int(yaoqiuleixing_str) - if yaoqiuleixing not in (1, 2): - raise ValueError - except: - return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'}) - - # 校验类型和专区的存在性及关联性 - try: - leixing_obj = ShangpinLeixing.objects.get(id=leixing_id) - except ShangpinLeixing.DoesNotExist: - return Response({'code': 400, 'msg': '商品类型不存在'}) - try: - zhuanqu_obj = ShangpinZhuanqu.objects.get(id=zhuanqu_id) - if zhuanqu_obj.leixing_id != leixing_id: - return Response({'code': 400, 'msg': '专区不属于该商品类型'}) - except ShangpinZhuanqu.DoesNotExist: - return Response({'code': 400, 'msg': '商品专区不存在'}) - - # 处理抢单要求相关字段 - huiyuan_id_val = None - yongjin_val = None - if yaoqiuleixing == 1: - # 会员要求 - if not huiyuan_id: - # 未传递时,尝试从商品类型中获取默认会员ID - huiyuan_id_val = leixing_obj.huiyuan_id - if not huiyuan_id_val: - return Response({'code': 400, 'msg': '未指定会员且商品类型无默认会员'}) - else: - huiyuan_id_val = huiyuan_id - # 验证会员是否存在 - if not Huiyuan.objects.filter(huiyuan_id=huiyuan_id_val).exists(): - return Response({'code': 400, 'msg': '指定的会员不存在'}) - else: - # 佣金要求 - try: - yongjin_val = Decimal(yongjin_str) - if yongjin_val < 0: - return Response({'code': 400, 'msg': '佣金不能为负数'}) - except: - # 若未传递或无效,尝试从商品类型中获取默认佣金 - if leixing_obj.yongjin is not None: - yongjin_val = leixing_obj.yongjin - else: - return Response({'code': 400, 'msg': '佣金格式错误且类型无默认佣金'}) - - # 处理打手分成方式 - kaioi_ewai = False - ewai_fencheng = Decimal('0') - if fencheng_type == 'custom': - try: - ewai_fencheng = Decimal(ewai_dashou_fencheng_str) - if ewai_fencheng < 0: - return Response({'code': 400, 'msg': '自定义分成金额不能为负数'}) - kaioi_ewai = True - except: - return Response({'code': 400, 'msg': '自定义分成金额格式错误'}) - - # 图片上传处理 - tupian_url = '' - if image_file: - # 验证图片 - valid, msg = validate_image(image_file) - if not valid: - return Response({'code': 400, 'msg': f'图片无效:{msg}'}) - # 生成唯一文件名 - ext = image_file.name.split('.')[-1].lower() - if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: - return Response({'code': 400, 'msg': '不支持的图片格式'}) - timestamp = datetime.now().strftime('%Y%m%d%H%M%S') - random_str = uuid.uuid4().hex[:8] - filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}" - # 上传到 OSS - uploaded_url = upload_to_oss(image_file, filename) - if not uploaded_url: - return Response({'code': 500, 'msg': '图片上传失败,请稍后重试'}) - tupian_url = filename # 存储相对路径,前端拼接域名 - else: - return Response({'code': 400, 'msg': '商品图片不能为空'}) - - # 创建商品 - try: - with transaction.atomic(): - product = Shangpin.objects.create( - biaoqian=biaoqian, - jiage=jiage, - kucun=kucun, - leixing_id=leixing_id, - zhuanqu_id=zhuanqu_id, - zhenshi_xiaoliang=0, # 初始真实销量为0 - duiwai_xiaoliang=duiwai_xiaoliang, - jieshao=jieshao, - xiadan_xuzhi=xiadan_xuzhi, - tupian_url=tupian_url, - yaoqiuleixing=yaoqiuleixing, - huiyuan_id=huiyuan_id_val, - yongjin=yongjin_val, - kaioi_ewai_dashou_fencheng=kaioi_ewai, - ewai_dashou_fencheng=ewai_fencheng, - paixu=0, # 新商品默认排序最后 - shenhezhuangtai=1, # 正常状态 - ) - except Exception as e: - logger.error(f"创建商品失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '创建商品失败,请稍后重试'}) - - return Response({ - 'code': 0, - 'msg': '商品发布成功', - 'data': { - 'id': product.id, - 'tupian_url': tupian_url, - } - }) - - -# houtai/views_product.py 中添加 - -class DeleteProductView(APIView): - """ - 删除商品(物理删除,同时删除OSS图片) - 权限:2200a - POST /houtai/htscsp - 参数: - username: 客服/管理员账号 - id: 商品ID - 返回: - code:0 成功 - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - product_id = request.data.get('id') - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not product_id: - return Response({'code': 400, 'msg': '缺少商品ID'}) - try: - product_id = int(product_id) - except: - return Response({'code': 400, 'msg': '商品ID格式错误'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 查询商品是否存在 - try: - product = Shangpin.objects.get(id=product_id) - except Shangpin.DoesNotExist: - return Response({'code': 404, 'msg': '商品不存在'}) - - # 先删除OSS上的图片 - tupian_url = product.tupian_url - if tupian_url: - # 调用已有的 delete_from_oss 函数 - from utils.oss_utils import delete_from_oss - delete_success = delete_from_oss(tupian_url) - if not delete_success: - logger.warning(f"删除商品图片失败: {tupian_url}, 商品ID: {product_id}") - # 注意:即使图片删除失败,也应继续删除数据库记录,避免数据不一致 - # 但可以记录日志供后续人工处理 - - # 删除数据库记录 - product.delete() - - logger.info(f"商品删除成功: id={product_id}, 图片路径={tupian_url}, 操作人={username}") - return Response({'code': 0, 'msg': '商品删除成功'}) - - - - - - - -class GetProductDetailView(APIView): - """ - 获取商品详情(包含商品类型、专区、会员列表) - 权限:2200a - POST /houtai/htsphqxq - 参数: - username: 客服/管理员账号 - id: 商品ID - 返回: - code:0, data: { product, type_list, zone_list, member_list } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - product_id = request.data.get('id') - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not product_id: - return Response({'code': 400, 'msg': '缺少商品ID'}) - try: - product_id = int(product_id) - except ValueError: - return Response({'code': 400, 'msg': '商品ID格式错误'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 查询商品 - try: - product = Shangpin.objects.get(id=product_id) - except Shangpin.DoesNotExist: - return Response({'code': 404, 'msg': '商品不存在'}) - - # 查询所有商品类型、专区、会员(用于前端下拉选择) - type_list = list(ShangpinLeixing.objects.all().values( - 'id', 'jieshao', 'tupian_url', 'shenhezhuangtai' - )) - zone_list = list(ShangpinZhuanqu.objects.all().values( - 'id', 'mingzi', 'leixing_id', 'shenhezhuangtai' - )) - member_list = list(Huiyuan.objects.all().values( - 'huiyuan_id', 'jieshao', 'jiage' - )) - - # 构建商品返回数据 - product_data = { - 'id': product.id, - 'biaoqian': product.biaoqian, - 'jiage': str(product.jiage), - 'kucun': product.kucun, - 'leixing_id': product.leixing_id, - 'zhuanqu_id': product.zhuanqu_id, - 'zhenshi_xiaoliang': product.zhenshi_xiaoliang, - 'duiwai_xiaoliang': product.duiwai_xiaoliang, - 'jieshao': product.jieshao, - 'xiadan_xuzhi': product.xiadan_xuzhi, - 'guize_tupian': product.guize_tupian, - 'yaoqiuleixing': product.yaoqiuleixing, - 'huiyuan_id': product.huiyuan_id, - 'yongjin': str(product.yongjin) if product.yongjin is not None else None, - 'tupian_url': product.tupian_url, - 'kaioi_ewai_dashou_fencheng': product.kaioi_ewai_dashou_fencheng, - 'ewai_dashou_fencheng': str(product.ewai_dashou_fencheng) if product.ewai_dashou_fencheng is not None else '0', - 'create_time': product.create_time.isoformat() if product.create_time else None, - 'shenhezhuangtai': product.shenhezhuangtai, - } - - return Response({ - 'code': 0, - 'data': { - 'product': product_data, - 'type_list': type_list, - 'zone_list': zone_list, - 'member_list': member_list, - } - }) - - - -class UpdateProductView(APIView): - """ - 修改商品(支持商品图片和规则图片的更新) - 权限:2200a - POST /houtai/bcspxg - """ - permission_classes = [] - parser_classes = [MultiPartParser, FormParser] - - def post(self, request): - username = request.data.get('username', '').strip() - product_id = request.data.get('id') - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not product_id: - return Response({'code': 400, 'msg': '缺少商品ID'}) - try: - product_id = int(product_id) - except ValueError: - return Response({'code': 400, 'msg': '商品ID格式错误'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 使用事务,将 select_for_update 放在事务内 - with transaction.atomic(): - try: - product = Shangpin.objects.select_for_update().get(id=product_id) - except Shangpin.DoesNotExist: - return Response({'code': 404, 'msg': '商品不存在'}) - - # ---------- 1. 基础字段 ---------- - biaoqian = request.data.get('biaoqian', '').strip() - if not biaoqian: - return Response({'code': 400, 'msg': '商品标题不能为空'}) - - try: - jiage = Decimal(str(request.data.get('jiage', 0))) - if jiage <= 0: - return Response({'code': 400, 'msg': '价格必须大于0'}) - except: - return Response({'code': 400, 'msg': '价格格式错误'}) - - try: - kucun = int(request.data.get('kucun', 0)) - if kucun < 0: - return Response({'code': 400, 'msg': '库存不能为负数'}) - except: - return Response({'code': 400, 'msg': '库存格式错误'}) - - try: - duiwai_xiaoliang = int(request.data.get('duiwai_xiaoliang', 0)) - except: - duiwai_xiaoliang = 0 - - try: - leixing_id = int(request.data.get('leixing_id')) - except: - return Response({'code': 400, 'msg': '商品类型ID无效'}) - - try: - zhuanqu_id = int(request.data.get('zhuanqu_id')) - except: - return Response({'code': 400, 'msg': '商品专区ID无效'}) - - try: - yaoqiuleixing = int(request.data.get('yaoqiuleixing', 1)) - if yaoqiuleixing not in (1, 2): - raise ValueError - except: - return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'}) - - try: - shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) - except: - shenhezhuangtai = 1 - - # 校验类型和专区存在性 - if not ShangpinLeixing.objects.filter(id=leixing_id).exists(): - return Response({'code': 400, 'msg': '商品类型不存在'}) - if not ShangpinZhuanqu.objects.filter(id=zhuanqu_id, leixing_id=leixing_id).exists(): - return Response({'code': 400, 'msg': '专区不存在或不属于该类型'}) - - # ---------- 2. 会员/佣金 ---------- - huiyuan_id = None - yongjin = None - if yaoqiuleixing == 1: - huiyuan_id = request.data.get('huiyuan_id', '').strip() or None - if huiyuan_id and not Huiyuan.objects.filter(huiyuan_id=huiyuan_id).exists(): - return Response({'code': 400, 'msg': '指定的会员不存在'}) - else: - try: - yongjin = Decimal(str(request.data.get('yongjin', 0))) - if yongjin < 0: - return Response({'code': 400, 'msg': '佣金不能为负数'}) - except: - yongjin = Decimal('0') - - # ---------- 3. 打手分成 ---------- - kaioi_ewai = request.data.get('kaioi_ewai_dashou_fencheng') == 'true' - ewai_fencheng = Decimal('0') - if kaioi_ewai: - try: - ewai_fencheng = Decimal(str(request.data.get('ewai_dashou_fencheng', 0))) - if ewai_fencheng < 0: - return Response({'code': 400, 'msg': '分成金额不能为负数'}) - except: - return Response({'code': 400, 'msg': '分成金额格式错误'}) - - jieshao = request.data.get('jieshao', '').strip() - xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip() - - # ---------- 4. 商品图片更新 ---------- - tupian_url = product.tupian_url - tupian_file = request.FILES.get('tupian_file') - if tupian_file: - valid, msg = validate_image(tupian_file) - if not valid: - return Response({'code': 400, 'msg': f'商品图片无效:{msg}'}) - ext = tupian_file.name.split('.')[-1].lower() - if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: - return Response({'code': 400, 'msg': '不支持的图片格式'}) - timestamp = datetime.now().strftime('%Y%m%d%H%M%S') - random_str = uuid.uuid4().hex[:8] - new_filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}" - uploaded_url = upload_to_oss(tupian_file, new_filename) - if not uploaded_url: - return Response({'code': 500, 'msg': '商品图片上传失败'}) - if product.tupian_url: - delete_from_oss(product.tupian_url) - tupian_url = new_filename - - # ---------- 5. 规则图片更新 ---------- - guize_tupian = product.guize_tupian - guize_file = request.FILES.get('guize_file') - if guize_file: - valid, msg = validate_image(guize_file) - if not valid: - return Response({'code': 400, 'msg': f'规则图片无效:{msg}'}) - ext = guize_file.name.split('.')[-1].lower() - if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: - return Response({'code': 400, 'msg': '不支持的图片格式'}) - timestamp = datetime.now().strftime('%Y%m%d%H%M%S') - random_str = uuid.uuid4().hex[:8] - new_filename = f"shangpin/guizetupian/{timestamp}_{random_str}.{ext}" - uploaded_url = upload_to_oss(guize_file, new_filename) - if not uploaded_url: - return Response({'code': 500, 'msg': '规则图片上传失败'}) - if product.guize_tupian: - delete_from_oss(product.guize_tupian) - guize_tupian = new_filename - - # ---------- 6. 更新数据库 ---------- - product.biaoqian = biaoqian - product.jiage = jiage - product.kucun = kucun - product.duiwai_xiaoliang = duiwai_xiaoliang - product.leixing_id = leixing_id - product.zhuanqu_id = zhuanqu_id - product.yaoqiuleixing = yaoqiuleixing - product.huiyuan_id = huiyuan_id - product.yongjin = yongjin - product.kaioi_ewai_dashou_fencheng = kaioi_ewai - product.ewai_dashou_fencheng = ewai_fencheng - product.jieshao = jieshao - product.xiadan_xuzhi = xiadan_xuzhi - product.tupian_url = tupian_url - product.guize_tupian = guize_tupian - product.shenhezhuangtai = shenhezhuangtai - product.save() - - return Response({'code': 0, 'msg': '商品修改成功', 'data': {'id': product.id}}) - - - - - -class GetMemberListView(APIView): - """ - 获取会员列表 - 权限:3300a - POST /houtai/hthqhylb - 参数:username - 返回:code:0, data: { list: [...] } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '3300a' not in permissions: - return Response({'code': 403, 'msg': '无权限查看会员列表'}) - - members = Huiyuan.objects.all().order_by('-create_time') - member_list = [] - for m in members: - member_list.append({ - 'huiyuan_id': m.huiyuan_id, - 'jieshao': m.jieshao, - 'jtjieshao': m.jtjieshao, - 'jiage': str(m.jiage), - 'guanshifc': str(m.guanshifc), - 'zuzhangfc': str(m.zuzhangfc), - 'goumai_cishu': m.goumai_cishu, - 'create_time': m.create_time.isoformat() if m.create_time else None, - 'update_time': m.update_time.isoformat() if m.update_time else None, - }) - return Response({'code': 0, 'data': {'list': member_list}}) - - -class UpdateMemberView(APIView): - """ - 修改会员信息 - 权限:3300a - POST /houtai/htxghyxx - 参数: - username, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao - 返回:code:0 成功 - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - huiyuan_id = request.data.get('huiyuan_id', '').strip() - jieshao = request.data.get('jieshao', '').strip() - jiage = request.data.get('jiage') - guanshifc = request.data.get('guanshifc') - zuzhangfc = request.data.get('zuzhangfc') - jtjieshao = request.data.get('jtjieshao', '').strip() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not huiyuan_id: - return Response({'code': 400, 'msg': '缺少会员ID'}) - if not jieshao: - return Response({'code': 400, 'msg': '会员名称不能为空'}) - - try: - jiage = Decimal(str(jiage)) - guanshifc = Decimal(str(guanshifc)) - zuzhangfc = Decimal(str(zuzhangfc)) - if jiage < 0 or guanshifc < 0 or zuzhangfc < 0: - raise ValueError - except: - return Response({'code': 400, 'msg': '金额格式错误'}) - - if guanshifc + zuzhangfc > jiage: - return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '3300a' not in permissions: - return Response({'code': 403, 'msg': '无权限修改会员'}) - - with transaction.atomic(): - try: - member = Huiyuan.objects.select_for_update().get(huiyuan_id=huiyuan_id) - except Huiyuan.DoesNotExist: - return Response({'code': 404, 'msg': '会员不存在'}) - - member.jieshao = jieshao - member.jiage = jiage - member.guanshifc = guanshifc - member.zuzhangfc = zuzhangfc - member.jtjieshao = jtjieshao - member.save() - - return Response({'code': 0, 'msg': '修改成功'}) - - - - -import random -import string - -class AddMemberView(APIView): - """ - 添加会员 - 权限:3300a - POST /houtai/httjhy - 参数: - username, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao - 返回:code:0, data: { huiyuan_id } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - jieshao = request.data.get('jieshao', '').strip() - jiage = request.data.get('jiage') - guanshifc = request.data.get('guanshifc') - zuzhangfc = request.data.get('zuzhangfc') - jtjieshao = request.data.get('jtjieshao', '').strip() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not jieshao: - return Response({'code': 400, 'msg': '会员名称不能为空'}) - - try: - jiage = Decimal(str(jiage)) - guanshifc = Decimal(str(guanshifc)) - zuzhangfc = Decimal(str(zuzhangfc)) - if jiage <= 0 or guanshifc < 0 or zuzhangfc < 0: - raise ValueError - except: - return Response({'code': 400, 'msg': '金额格式错误'}) - - if guanshifc + zuzhangfc > jiage: - return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '3300a' not in permissions: - return Response({'code': 403, 'msg': '无权限添加会员'}) - - # 生成唯一6位会员ID(数字+字母) - def generate_huiyuan_id(): - while True: - new_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) - if not Huiyuan.objects.filter(huiyuan_id=new_id).exists(): - return new_id - - with transaction.atomic(): - new_id = generate_huiyuan_id() - member = Huiyuan.objects.create( - huiyuan_id=new_id, - jieshao=jieshao, - jiage=jiage, - guanshifc=guanshifc, - zuzhangfc=zuzhangfc, - jtjieshao=jtjieshao, - goumai_cishu=0 - ) - - return Response({'code': 0, 'msg': '添加成功', 'data': {'huiyuan_id': member.huiyuan_id}}) - - - - - -class GetGuanliListView(APIView): - """ - 获取管事列表 - 权限:4400a,4400b,4400c,4400d,4400e,4400f 任一 - POST /houtai/hthqgslb - 参数: - username, keyword(可选), balance_op/balance_value, commission_op/commission_value, - invite_count_op/invite_count_value, status, page, page_size - 返回: - code:0, data: { list: [...], total } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - # 权限校验(需要任一权限) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看管事列表'}) - - # 分页参数 - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - if page < 1 or page_size < 1: - raise ValueError - except: - return Response({'code': 400, 'msg': '分页参数错误'}) - - # 筛选参数 - keyword = request.data.get('keyword', '').strip() - balance_op = request.data.get('balance_op', '').strip().lower() - balance_value = request.data.get('balance_value') - commission_op = request.data.get('commission_op', '').strip().lower() - commission_value = request.data.get('commission_value') - invite_count_op = request.data.get('invite_count_op', '').strip().lower() - invite_count_value = request.data.get('invite_count_value') - status = request.data.get('status') - - # 构建查询:从 UserMain 开始,关联 UserGuanshi 和 UserBoss(左连接获取昵称) - qs = UserMain.objects.filter( - guanshi_profile__isnull=False # 确保是管事 - ).select_related('guanshi_profile').select_related('boss_profile') - - # 关键词搜索(用户ID 或 昵称来自 boss_profile.nickname) - if keyword: - qs = qs.filter( - Q(yonghuid__icontains=keyword) | - Q(boss_profile__nickname__icontains=keyword) - ) - - # 余额筛选(使用 guanshi_profile.yue) - if balance_op and balance_value is not None and balance_value != '': - try: - balance_val = Decimal(str(balance_value)) - if balance_op == 'gte': - qs = qs.filter(guanshi_profile__yue__gte=balance_val) - elif balance_op == 'lte': - qs = qs.filter(guanshi_profile__yue__lte=balance_val) - else: - return Response({'code': 400, 'msg': '余额比较符无效'}) - except: - return Response({'code': 400, 'msg': '余额数值格式错误'}) - - # 分佣总额筛选(guanshi_profile.chongzhifenrun) - if commission_op and commission_value is not None and commission_value != '': - try: - commission_val = Decimal(str(commission_value)) - if commission_op == 'gte': - qs = qs.filter(guanshi_profile__chongzhifenrun__gte=commission_val) - elif commission_op == 'lte': - qs = qs.filter(guanshi_profile__chongzhifenrun__lte=commission_val) - else: - return Response({'code': 400, 'msg': '分佣比较符无效'}) - except: - return Response({'code': 400, 'msg': '分佣数值格式错误'}) - - # 邀请打手总数筛选(guanshi_profile.yaogingshuliang) - if invite_count_op and invite_count_value is not None and invite_count_value != '': - try: - invite_val = int(invite_count_value) - if invite_count_op == 'gte': - qs = qs.filter(guanshi_profile__yaogingshuliang__gte=invite_val) - elif invite_count_op == 'lte': - qs = qs.filter(guanshi_profile__yaogingshuliang__lte=invite_val) - else: - return Response({'code': 400, 'msg': '邀请数量比较符无效'}) - except: - return Response({'code': 400, 'msg': '邀请数量格式错误'}) - - # 状态筛选 - if status is not None and status != '': - try: - status_val = int(status) - if status_val not in (0, 1): - raise ValueError - qs = qs.filter(guanshi_profile__zhuangtai=status_val) - except: - return Response({'code': 400, 'msg': '状态值无效'}) - - # 计算总数 - total = qs.count() - - # 分页 - offset = (page - 1) * page_size - users = qs.order_by('-create_time')[offset:offset + page_size] - - # 构建返回数据 - result = [] - for user in users: - guanshi = user.guanshi_profile - boss = user.boss_profile if hasattr(user, 'boss_profile') else None - result.append({ - 'yonghuid': user.yonghuid, - 'avatar': user.avatar or '', - 'nickname': boss.nickname if boss else '', - 'zhuangtai': guanshi.zhuangtai, - 'yaogingshuliang': guanshi.yaogingshuliang, - 'yue': str(guanshi.yue), - 'chongzhifenrun': str(guanshi.chongzhifenrun), - }) - - return Response({'code': 0, 'data': {'list': result, 'total': total}}) - - - - - - - - -class GetGuanliDetailView(APIView): - """ - 获取管事详情 - 权限:4400a,4400b,4400c,4400d,4400e,4400f 任一 - POST /houtai/hthqgsxq - 参数:username, yonghuid - 返回: - code:0, data: { - base: {...}, - balance: {...}, - permanent_enabled, permanent_amount, - custom_first_dividend: {}, - extra_dividend_map: {}, - all_members: [] - } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - yonghuid = request.data.get('yonghuid', '').strip() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not yonghuid: - return Response({'code': 400, 'msg': '缺少用户ID'}) - - # 权限校验(需要任一权限) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看管事详情'}) - - # 查询用户主表和管事扩展表 - try: - user = UserMain.objects.select_related('guanshi_profile', 'boss_profile').get(yonghuid=yonghuid) - guanshi = user.guanshi_profile - boss = user.boss_profile if hasattr(user, 'boss_profile') else None - except UserMain.DoesNotExist: - return Response({'code': 404, 'msg': '管事不存在'}) - except UserGuanshi.DoesNotExist: - return Response({'code': 404, 'msg': '用户不是管事'}) - - # 基础信息 - base_data = { - 'yonghuid': user.yonghuid, - 'nickname': boss.nickname if boss else '', - 'avatar': user.avatar or '', - 'dianhua': guanshi.dianhua or '', - 'wechat': guanshi.wechat or '', - 'zhuangtai': guanshi.zhuangtai, - 'yaoqingma': guanshi.yaoqingma, - 'yaoqingren': guanshi.yaoqingren, - 'invite_qrcode_url': guanshi.invite_qrcode_url or '', - 'yaogingshuliang': guanshi.yaogingshuliang, - 'jinrichongzhi': guanshi.jinrichongzhi, - 'jinyuechongzhi': guanshi.jinyuechongzhi, - 'chongzhifenrun': str(guanshi.chongzhifenrun), - 'create_time': guanshi.create_time.isoformat() if guanshi.create_time else None, - } - - # 余额提现相关 - balance_data = { - 'yue': str(guanshi.yue), - 'jinritixian_jine': str(guanshi.jinritixian_jine), - 'last_tixian_date': guanshi.last_tixian_date.isoformat() if guanshi.last_tixian_date else None, - 'kaioi_ewai_xiane': guanshi.kaioi_ewai_xiane, - 'ewai_xiane': str(guanshi.ewai_xiane), - } - - # 永久分红(二次分红) - permanent_enabled = guanshi.fenghong_erci_enabled - permanent_amount = str(guanshi.fenghong_erci) if guanshi.fenghong_erci else '0' - - # 获取所有会员(用于前端选择) - all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage', 'guanshifc')) - for m in all_members: - m['jiage'] = str(m['jiage']) - m['guanshifc'] = str(m['guanshifc']) - - # 获取多次分红配置(按次数和会员组织) - # 查询该管事的所有多次分红记录 - duoci_records = DuociFenhong.objects.filter(yonghuid=yonghuid).order_by('cishu') - custom_first = {} # 首次分红定制(cishu=1) - extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } } - - for record in duoci_records: - # 获取会员信息 - member = Huiyuan.objects.filter(huiyuan_id=record.huiyuan).first() - if not member: - continue - member_info = { - 'jieshao': member.jieshao, - 'jiage': str(member.jiage), - } - if record.cishu == 1: - custom_first[record.huiyuan] = float(record.guanshi_fenhong) - else: - times = record.cishu - if times not in extra_map: - extra_map[times] = {} - extra_map[times][record.huiyuan] = { - 'guanshi_fenhong': float(record.guanshi_fenhong), - **member_info - } - - # 注意:首次分红定制只存储有定制记录的会员,没有定制的会员使用默认值(前端通过会员表展示) - # 额外次数分红按次数返回 - - return Response({ - 'code': 0, - 'data': { - 'base': base_data, - 'balance': balance_data, - 'permanent_enabled': permanent_enabled, - 'permanent_amount': permanent_amount, - 'custom_first_dividend': custom_first, - 'extra_dividend_map': extra_map, - 'all_members': all_members, - } - }) - - - - - -class UpdateGuanliView(APIView): - """ - 修改管事信息(基础信息、余额、提现限额、分红策略) - 权限细分: - - 修改状态(封禁/解封)及昵称/电话/微信:需要4400a - - 增加余额:需要4400c - - 减少余额:需要4400b - - 修改提现限额(kaioi_ewai_xiane/ewai_xiane):需要4400e - - 修改分红相关(永久分红、定制首次、额外次数):需要4400f - POST /houtai/htxggsxx - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - yonghuid = request.data.get('yonghuid', '').strip() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not yonghuid: - return Response({'code': 400, 'msg': '缺少用户ID'}) - - # 权限校验(先获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 查询管事对象(不在事务外加锁,事务内加锁) - try: - user = UserMain.objects.get(yonghuid=yonghuid) - guanshi = user.guanshi_profile - boss = user.boss_profile if hasattr(user, 'boss_profile') else None - except UserMain.DoesNotExist: - return Response({'code': 404, 'msg': '用户不存在'}) - except UserGuanshi.DoesNotExist: - return Response({'code': 404, 'msg': '用户不是管事'}) - - # 开始事务,内部使用 select_for_update - with transaction.atomic(): - # 重新获取加锁对象 - user = UserMain.objects.select_for_update().get(yonghuid=yonghuid) - guanshi = user.guanshi_profile - boss = user.boss_profile if hasattr(user, 'boss_profile') else None - - # ---------- 1. 基础信息修改(需要4400a)---------- - nickname = request.data.get('nickname') - dianhua = request.data.get('dianhua') - wechat = request.data.get('wechat') - zhuangtai = request.data.get('zhuangtai') - avatar = request.data.get('avatar') - - if any(v is not None for v in [nickname, dianhua, wechat, zhuangtai, avatar]): - if '4400a' not in permissions: - return Response({'code': 403, 'msg': '无权限修改管事基础信息(需要4400a)'}) - # 处理昵称(在老板扩展表中) - if nickname is not None: - if not boss: - boss = UserBoss.objects.create(user=user, nickname=nickname) - else: - boss.nickname = nickname - boss.save() - # 处理其他字段 - if dianhua is not None: - guanshi.dianhua = dianhua - if wechat is not None: - guanshi.wechat = wechat - if zhuangtai is not None: - try: - zt = int(zhuangtai) - if zt in (0, 1): - guanshi.zhuangtai = zt - except: - pass - if avatar is not None: - user.avatar = avatar - user.save() - guanshi.save() - - # ---------- 2. 余额修改(加:4400c;减:4400b)---------- - new_yue = request.data.get('yue') - if new_yue is not None: - try: - new_yue = Decimal(str(new_yue)) - if new_yue < 0: - return Response({'code': 400, 'msg': '余额不能为负数'}) - except: - return Response({'code': 400, 'msg': '余额格式错误'}) - - old_yue = guanshi.yue - if new_yue > old_yue: - if '4400c' not in permissions: - return Response({'code': 403, 'msg': '无权限增加余额(需要4400c)'}) - elif new_yue < old_yue: - if '4400b' not in permissions: - return Response({'code': 403, 'msg': '无权限减少余额(需要4400b)'}) - guanshi.yue = new_yue - guanshi.save() - - # ---------- 3. 提现限额修改(需要4400e)---------- - kaioi_ewai = request.data.get('kaioi_ewai_xiane') - ewai_val = request.data.get('ewai_xiane') - if kaioi_ewai is not None or ewai_val is not None: - if '4400e' not in permissions: - return Response({'code': 403, 'msg': '无权限修改提现限额(需要4400e)'}) - if kaioi_ewai is not None: - guanshi.kaioi_ewai_xiane = bool(kaioi_ewai) - if ewai_val is not None: - try: - guanshi.ewai_xiane = Decimal(str(ewai_val)) - except: - return Response({'code': 400, 'msg': '额外限额金额格式错误'}) - guanshi.save() - - # ---------- 4. 分红相关修改(需要4400f)---------- - perm_enabled = request.data.get('permanent_enabled') - perm_amount = request.data.get('permanent_amount') - custom_first = request.data.get('custom_first_dividend') - extra_map = request.data.get('extra_dividend_map') - - if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]): - if '4400f' not in permissions: - return Response({'code': 403, 'msg': '无权限修改分红策略(需要4400f)'}) - - # 4.1 永久分红 - if perm_enabled is not None: - guanshi.fenghong_erci_enabled = bool(perm_enabled) - if perm_amount is not None: - try: - guanshi.fenghong_erci = Decimal(str(perm_amount)) - except: - return Response({'code': 400, 'msg': '永久分红金额格式错误'}) - guanshi.save() - - # 4.2 首次定制分红(cishu=1) - if custom_first is not None and isinstance(custom_first, dict): - # 获取当前所有首次定制记录 - existing_first = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu=1) - # 前端传来的定制字典 {会员ID: 金额} - for huiyuan_id, amount in custom_first.items(): - try: - amount = Decimal(str(amount)) - except: - continue - if amount <= 0: - # 金额为0或负数表示删除定制 - DuociFenhong.objects.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() - else: - DuociFenhong.objects.update_or_create( - yonghuid=yonghuid, - huiyuan=huiyuan_id, - cishu=1, - defaults={ - 'guanshi_fenhong': amount, - 'zuzhang_fenhong': Decimal('0') - } - ) - # 删除那些在前端字典中不存在的首次定制记录(即已取消定制) - for record in existing_first: - if record.huiyuan not in custom_first: - record.delete() - - # 4.3 额外次数分红(cishu >= 2) - if extra_map is not None and isinstance(extra_map, dict): - # 收集需要保留的记录 - keep_set = set() - for times_str, members in extra_map.items(): - try: - cishu = int(times_str) - if cishu < 2: - continue - except: - continue - if not isinstance(members, dict): - continue - for huiyuan_id, data in members.items(): - if not isinstance(data, dict): - continue - amount = data.get('guanshi_fenhong') - if amount is None: - continue - try: - amount = Decimal(str(amount)) - except: - continue - keep_set.add((cishu, huiyuan_id)) - # 更新或创建 - DuociFenhong.objects.update_or_create( - yonghuid=yonghuid, - huiyuan=huiyuan_id, - cishu=cishu, - defaults={ - 'guanshi_fenhong': amount, - 'zuzhang_fenhong': Decimal('0') - } - ) - # 删除不在 keep_set 中的额外次数记录 - existing_extra = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu__gte=2) - for record in existing_extra: - if (record.cishu, record.huiyuan) not in keep_set: - record.delete() - - return Response({'code': 0, 'msg': '修改成功'}) - - - - - - - - - -class GetWithdrawSettingsView(APIView): - """ - 获取提现设置(手续费利率、每日限额、当日已提现总额) - 权限:需要 5500a 或 5500b 或 5500c 任一 - POST /houtai/hthqtxpz - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - # 需要5500a/b/c任一 - if not any(p in permissions for p in ('5500a', '5500b', '5500c')): - return Response({'code': 403, 'msg': '无权限查看提现设置'}) - - # 1. 手续费利率(Lilubiao:5打手/6管事/8组长/9审核官/10商家/11打手押金) - rate_map = {} - for leixing, code in [(1, '5'), (2, '6'), (3, '8'), (4, '9'), (5, '11'), (6, '10')]: - obj = Lilubiao.objects.filter(fadanpingtai=code).first() - rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0 - - # 1b. 订单/押金分红费率(Lilubiao:1平台订单/3商家订单/12押金管事/13商家订单管事分红) - order_rate_map = {} - for leixing, code in [(1, '1'), (3, '3'), (12, '12'), (13, '13')]: - obj = Lilubiao.objects.filter(fadanpingtai=code).first() - order_rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0 - - # 2. 个人每日限额 (leixing=1,2,3) - quota_map = {} - for leixing in [1, 2, 3]: - obj = TixianQuotaDefault.objects.filter(leixing=leixing).first() - quota_map[leixing] = float(obj.default_quota) if obj else 20.0 - - # 3. 每日总限额(提现类型1~6 → 配置表4~9) - total_limit_map = {} - for withdraw_leixing, quota_leixing in [(1, 4), (2, 5), (3, 6), (4, 7), (5, 8), (6, 9)]: - obj = TixianQuotaDefault.objects.filter(leixing=quota_leixing).first() - total_limit_map[withdraw_leixing] = float(obj.default_quota) if obj else 0.0 - - # 4. 当日已提现总额(按提现类型 leixing 1~6) - today = date.today() - today_totals = {i: 0.0 for i in range(1, 7)} - records = TixianRiTongji.objects.filter(riqi=today) - for rec in records: - if rec.leixing in today_totals: - today_totals[rec.leixing] = float(rec.total_amount) - - # 🆕 获取提现模式(自动1 / 手动2),默认2 - from peizhi.models import WithdrawConfig # 确保导入模型 - config = WithdrawConfig.objects.filter(id=1).first() - withdraw_mode = config.mode if config else 2 - - return Response({ - 'code': 0, - 'data': { - 'withdraw_mode': withdraw_mode, - 'rates': rate_map, - 'order_rates': order_rate_map, - 'quotas': quota_map, - 'total_limits': total_limit_map, - 'today_totals': today_totals, - } - }) - - -class UpdateWithdrawSettingsView(APIView): - """ - 修改提现设置(权限细分): - - 修改打手相关(利率、个人限额、总限额)需要5500a - - 修改管事相关需要5500b - - 修改组长相关需要5500c - 注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算), - 后端不直接修改 TixianRiTongji,该表由提现流程自动累加。 - POST /houtai/htxgtxsz - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 权限映射:要修改的角色需要对应的权限 - # 前端传来 rates, quotas, total_limits 三个对象,每个包含 1,2,3 - rates = request.data.get('rates', {}) - order_rates = request.data.get('order_rates', request.data.get('commission_rates', {})) - quotas = request.data.get('quotas', {}) - total_limits = request.data.get('total_limits', {}) - - # 角色类型映射 - role_perms = {1: '5500a', 2: '5500b', 3: '5500c'} - # 利率代码映射(1-3 有水缸限额;4审核官/5打手押金/6商家余额仅费率) - rate_code_map = {1: '5', 2: '6', 3: '8', 4: '9', 5: '11', 6: '10'} - # 订单/押金分红(1平台订单/3商家订单/12押金管事/13商家订单管事分红) - order_rate_code_map = {1: '1', 3: '3', 12: '12', 13: '13'} - # 总限额:提现类型 → TixianQuotaDefault.leixing(4~9) - total_code_map = {1: 4, 2: 5, 3: 6, 4: 7, 5: 8, 6: 9} - - def _rate_key_present(data, role): - return (str(role) in data and data[str(role)] is not None) or \ - (role in data and data[role] is not None) - - with transaction.atomic(): - # 遍历三个角色(限额相关) - for role in [1, 2, 3]: - perm_needed = role_perms[role] - if perm_needed not in permissions: - if _rate_key_present(rates, role) or \ - (str(role) in quotas and quotas[str(role)] is not None) or \ - (role in quotas and quotas.get(role) is not None) or \ - (str(role) in total_limits and total_limits[str(role)] is not None) or \ - (role in total_limits and total_limits.get(role) is not None): - return Response({'code': 403, 'msg': f'无权限修改{["","打手","管事","组长"][role]}相关设置(需要{perm_needed})'}) - - # 其他提现手续费(审核官/打手押金/商家余额):需 5500a/b/c 任一 - extra_role_names = {4: '考核官', 5: '打手押金', 6: '商家余额'} - for role in [4, 5, 6]: - if _rate_key_present(rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')): - return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}提现手续费率'}) - if (str(role) in total_limits and total_limits[str(role)] is not None) or \ - (role in total_limits and total_limits.get(role) is not None): - if not any(p in permissions for p in ('5500a', '5500b', '5500c')): - return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}总限额'}) - - # 订单/押金分红费率:需 5500a/b/c 任一 - for role in [1, 3, 12, 13]: - if _rate_key_present(order_rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')): - order_names = { - 1: '平台订单打手分红', 3: '商家订单打手分红', - 12: '押金管事分红', 13: '管事订单分红', - } - return Response({'code': 403, 'msg': f'无权限修改{order_names[role]}费率'}) - - # 1. 修改手续费利率 - for role in [1, 2, 3, 4, 5, 6]: - val = rates.get(str(role), rates.get(role)) - if val is not None: - rate = Decimal(str(val)) - code = rate_code_map[role] - obj, created = Lilubiao.objects.select_for_update().get_or_create( - fadanpingtai=code, - defaults={'lilu': rate} - ) - if not created: - obj.lilu = rate - obj.save() - - # 1b. 修改订单/押金分红费率 - for role in [1, 3, 12, 13]: - val = order_rates.get(str(role), order_rates.get(role)) - if val is not None: - rate = Decimal(str(val)) - code = order_rate_code_map[role] - obj, created = Lilubiao.objects.select_for_update().get_or_create( - fadanpingtai=code, - defaults={'lilu': rate} - ) - if not created: - obj.lilu = rate - obj.save() - - # 2. 修改个人每日限额 - for role in [1, 2, 3]: - if str(role) in quotas: - val = quotas[str(role)] - if val is not None: - quota = Decimal(str(val)) - obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create( - leixing=role, - defaults={'default_quota': quota} - ) - if not created: - obj.default_quota = quota - obj.save() - - # 3. 修改每日总限额(1打手/2管事/3组长/4考核官/5押金/6商家) - for role in [1, 2, 3, 4, 5, 6]: - val = total_limits.get(str(role), total_limits.get(role)) - if val is not None: - limit = Decimal(str(val)) - code = total_code_map[role] - obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create( - leixing=code, - defaults={'default_quota': limit} - ) - if not created: - obj.default_quota = limit - obj.save() - - # 🆕 修改提现模式(有5500a/b/c任一即可) - from peizhi.models import WithdrawConfig - withdraw_mode = request.data.get('withdraw_mode') - if withdraw_mode is not None: - if not any(p in permissions for p in ('5500a', '5500b', '5500c')): - return Response({'code': 403, 'msg': '无权限修改提现模式'}) - # 更新或创建 id=1 的配置 - WithdrawConfig.objects.update_or_create( - id=1, - defaults={'mode': int(withdraw_mode)} - ) - - # 注意:TixianRiTongji 表由提现流程自动更新,此处不直接修改 - return Response({'code': 0, 'msg': '设置修改成功'}) - - - -class GetZuzhangListView(APIView): - """ - 组长列表接口(分页 + 多条件筛选) - 路由:POST /houtai/hthqzzlb - 权限:需要登录,且拥有 6600a~6600e 任一权限 - """ - permission_classes = [IsAuthenticated] - - @transaction.atomic # 保证查询的一致性(可选,对只读查询影响不大但无坏处) - def post(self, request): - # 1. 获取前端传递的 username(用于防越权) - frontend_username = request.data.get('username', '') - - # 2. 调用公共校验方法,验证身份并获取权限列表 - result = verify_kefu_permission(request, username_from_frontend=frontend_username) - - # 重要:verify_kefu_permission 返回格式为 (user_obj, permissions_list) 或 (None, Response) - # 如果第二个元素是 Response 对象,说明校验失败,直接返回该 Response - if isinstance(result, tuple) and len(result) == 2: - user_obj, second = result - if isinstance(second, Response): - return second # 校验失败,直接返回错误响应 - else: - permissions = second # 正常情况:permissions 是列表 - else: - # 防止意外返回格式,视为失败 - return Response({'code': 500, 'msg': '权限校验模块异常'}) - - # 3. 检查是否拥有组长管理所需权限(6600a ~ 6600e 任一) - required_perms = ['6600a', '6600b', '6600c', '6600d', '6600e'] - if not any(perm in permissions for perm in required_perms): - logger.warning(f"用户 {request.user.phone} 尝试访问组长列表但缺少必要权限,已有权限: {permissions}") - return Response({'code': 403, 'msg': '您无权访问组长管理页面'}) - - # 4. 提取请求参数(分页 + 筛选) - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - except ValueError: - page, page_size = 1, 20 - - keyword = request.data.get('keyword', '').strip() - status = request.data.get('status') # 1=正常, 0=禁用 - invite_count_op = request.data.get('invite_count_op') # gte / lte - invite_count_value = request.data.get('invite_count_value') - amount_op = request.data.get('amount_op') - amount_value = request.data.get('amount_value') - commission_op = request.data.get('commission_op') - commission_value = request.data.get('commission_value') - - # 5. 构建基础 QuerySet(预加载关联表) - queryset = UserZuzhang.objects.select_related('user', 'user__boss_profile').all() - - # 6. 应用筛选条件(使用 ORM 安全过滤) - if keyword: - queryset = queryset.filter( - Q(user__yonghuid__icontains=keyword) | - Q(user__boss_profile__nickname__icontains=keyword) - ) - - if status is not None and status != '': - try: - queryset = queryset.filter(zhuangtai=int(status)) - except ValueError: - pass - - # 邀请总数筛选 - if invite_count_value not in (None, ''): - try: - val = float(invite_count_value) - op = invite_count_op if invite_count_op in ('gte', 'lte') else 'gte' - queryset = queryset.filter(**{f'yaoqing_zongshu__{op}': val}) - except (ValueError, TypeError): - pass - - # 可提现金额筛选 - if amount_value not in (None, ''): - try: - val = float(amount_value) - op = amount_op if amount_op in ('gte', 'lte') else 'gte' - queryset = queryset.filter(**{f'ketixian_jine__{op}': val}) - except (ValueError, TypeError): - pass - - # 分佣总额筛选 - if commission_value not in (None, ''): - try: - val = float(commission_value) - op = commission_op if commission_op in ('gte', 'lte') else 'gte' - queryset = queryset.filter(**{f'fenyong_zonge__{op}': val}) - except (ValueError, TypeError): - pass - - # 7. 总数统计 - total = queryset.count() - - # 8. 分页 - offset = (page - 1) * page_size - paged_queryset = queryset[offset:offset + page_size] - - # 9. 构造返回数据 - data_list = [] - for zu in paged_queryset: - user = zu.user - nickname = '' - if hasattr(user, 'boss_profile') and user.boss_profile: - nickname = user.boss_profile.nickname or '' - - data_list.append({ - 'yonghuid': user.yonghuid, - 'avatar': user.avatar or '', - 'nickname': nickname, - 'fenyong_zonge': float(zu.fenyong_zonge), - 'ketixian_jine': float(zu.ketixian_jine), - 'yaoqing_zongshu': zu.yaoqing_zongshu, - 'zhuangtai': zu.zhuangtai, - }) - - return Response({ - 'code': 0, - 'msg': 'success', - 'data': { - 'list': data_list, - 'total': total, - } - }) - - - - - - - - - - -class GetZuzhangDetailView(APIView): - """ - 获取组长详情 - 权限:6600a,6600b,6600c,6600d,6600e 任一 - POST /houtai/hthqzzxq - 参数:username, yonghuid - 返回: - code:0, data: { - base: {...}, - balance: {...}, - permanent_enabled, permanent_amount, - custom_first_dividend: {}, - extra_dividend_map: {}, - all_members: [] - } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - yonghuid = request.data.get('yonghuid', '').strip() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not yonghuid: - return Response({'code': 400, 'msg': '缺少用户ID'}) - - # 权限校验(需要任一权限) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - required_perms = {'6600a', '6600b', '6600c', '6600d', '6600e'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看组长详情'}) - - # 查询组长主表、扩展表、老板扩展表(昵称) - try: - user = UserMain.objects.select_related('zuzhang_profile', 'boss_profile').get(yonghuid=yonghuid) - zuzhang = user.zuzhang_profile - boss = user.boss_profile if hasattr(user, 'boss_profile') else None - except UserMain.DoesNotExist: - return Response({'code': 404, 'msg': '组长不存在'}) - except UserZuzhang.DoesNotExist: - return Response({'code': 404, 'msg': '用户不是组长'}) - - # 基础信息 - base_data = { - 'yonghuid': user.yonghuid, - 'nickname': boss.nickname if boss else '', - 'avatar': user.avatar or '', - 'dianhua': '', # 组长表无电话字段?组长扩展表没有电话/微信,但前端可能需要,可从UserMain关联?实际上组长表无电话字段,这里留空或从其他表获取 - 'wechat': '', - 'zhuangtai': zuzhang.zhuangtai, - 'yaoqingma': zuzhang.yaoqingma, - 'yaoqingren': '', # 组长表没有邀请人字段 - 'haibao_url': zuzhang.haibao_url or '', - 'yaoqing_zongshu': zuzhang.yaoqing_zongshu, - 'jinri_fenyong': str(zuzhang.jinri_fenyong), - 'jinyue_fenyong': str(zuzhang.jinyue_fenyong), - 'fenyong_zonge': str(zuzhang.fenyong_zonge), - 'create_time': zuzhang.create_time.isoformat() if zuzhang.create_time else None, - } - - # 余额提现相关 - balance_data = { - 'ketixian_jine': str(zuzhang.ketixian_jine), - 'jinri_tixian': str(zuzhang.jinri_tixian), - 'last_tixian_time': zuzhang.last_tixian_time.isoformat() if zuzhang.last_tixian_time else None, - 'kaioi_ewai_tixian': zuzhang.kaioi_ewai_tixian, - 'ewai_tixian_xiane': str(zuzhang.ewai_tixian_xiane), - } - - # 永久分红(额外分红开关和金额) - permanent_enabled = zuzhang.kaioi_ewai_fenhong - permanent_amount = str(zuzhang.ewai_fenhong_jine) if zuzhang.ewai_fenhong_jine else '0' - - # 获取所有会员(用于前端选择) - all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage', 'zuzhangfc')) - for m in all_members: - m['jiage'] = str(m['jiage']) - m['zuzhangfc'] = str(m['zuzhangfc']) - - # 获取多次分红配置(按次数和会员组织) - duoci_records = DuociFenhong.objects.filter(yonghuid=yonghuid).order_by('cishu') - custom_first = {} # 首次分红定制(cishu=1) - extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } } - - for record in duoci_records: - # 获取会员信息 - member = Huiyuan.objects.filter(huiyuan_id=record.huiyuan).first() - if not member: - continue - member_info = { - 'jieshao': member.jieshao, - 'jiage': str(member.jiage), - } - if record.cishu == 1: - custom_first[record.huiyuan] = float(record.zuzhang_fenhong) - else: - times = record.cishu - if times not in extra_map: - extra_map[times] = {} - extra_map[times][record.huiyuan] = { - 'zuzhang_fenhong': float(record.zuzhang_fenhong), - **member_info - } - - return Response({ - 'code': 0, - 'data': { - 'base': base_data, - 'balance': balance_data, - 'permanent_enabled': permanent_enabled, - 'permanent_amount': permanent_amount, - 'custom_first_dividend': custom_first, - 'extra_dividend_map': extra_map, - 'all_members': all_members, - } - }) - - - - -class UpdateZuzhangView(APIView): - """ - 修改组长信息(基础信息、余额、提现限额、分红策略) - 权限细分: - - 增加可提现金额:6600a - - 减少可提现金额:6600b - - 修改状态(封禁/解封):6600c - - 修改提现限额(kaioi_ewai_tixian/ewai_tixian_xiane):6600d - - 修改分红相关(永久分红、定制首次、额外次数):6600e - POST /houtai/htxgzzxx - 参数: - username, yonghuid, - nickname, avatar, dianhua, wechat, zhuangtai, # 基础 - ketixian_jine, kaioi_ewai_tixian, ewai_tixian_xiane, # 余额提现 - permanent_enabled, permanent_amount, # 永久分红 - custom_first_dividend: {}, # 首次定制分红 {会员ID: 金额} - extra_dividend_map: {} # 额外次数分红 {次数: {会员ID: {zuzhang_fenhong}}} - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - yonghuid = request.data.get('yonghuid', '').strip() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not yonghuid: - return Response({'code': 400, 'msg': '缺少用户ID'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 查询组长对象 - try: - user = UserMain.objects.get(yonghuid=yonghuid) - zuzhang = user.zuzhang_profile - boss = user.boss_profile if hasattr(user, 'boss_profile') else None - except UserMain.DoesNotExist: - return Response({'code': 404, 'msg': '用户不存在'}) - except UserZuzhang.DoesNotExist: - return Response({'code': 404, 'msg': '用户不是组长'}) - - # 获取原始数据(用于对比金额变化) - old_ketixian_jine = zuzhang.ketixian_jine - - # 开始事务 - with transaction.atomic(): - # ---------- 1. 基础信息修改(需要6600c?按需求,基础信息修改通常用6600c,但为了安全,暂不校验,如有需要可加)---------- - nickname = request.data.get('nickname', '').strip() - avatar = request.data.get('avatar', '').strip() - dianhua = request.data.get('dianhua', '').strip() - wechat = request.data.get('wechat', '').strip() - zhuangtai = request.data.get('zhuangtai') - # 状态修改需要6600c - if zhuangtai is not None: - if '6600c' not in permissions: - return Response({'code': 403, 'msg': '无权限修改组长状态(需要6600c)'}) - try: - zt = int(zhuangtai) - if zt in (0, 1): - zuzhang.zhuangtai = zt - except: - pass - if nickname: - if not boss: - boss = UserBoss.objects.create(user=user, nickname=nickname) - else: - boss.nickname = nickname - boss.save() - if avatar: - user.avatar = avatar - user.save() - # 组长表没有电话/微信字段,如需存储可暂存到UserMain或忽略 - # 这里不处理 - - # ---------- 2. 可提现金额修改(加:6600a;减:6600b)---------- - new_ketixian_jine = request.data.get('ketixian_jine') - if new_ketixian_jine is not None: - try: - new_val = Decimal(str(new_ketixian_jine)) - if new_val < 0: - return Response({'code': 400, 'msg': '可提现金额不能为负数'}) - except: - return Response({'code': 400, 'msg': '金额格式错误'}) - if new_val > old_ketixian_jine: - if '6600a' not in permissions: - return Response({'code': 403, 'msg': '无权限增加可提现金额(需要6600a)'}) - elif new_val < old_ketixian_jine: - if '6600b' not in permissions: - return Response({'code': 403, 'msg': '无权限减少可提现金额(需要6600b)'}) - zuzhang.ketixian_jine = new_val - zuzhang.save(update_fields=['ketixian_jine']) - - # ---------- 3. 提现限额修改(需要6600d)---------- - kaioi_ewai = request.data.get('kaioi_ewai_tixian') - ewai_val = request.data.get('ewai_tixian_xiane') - if kaioi_ewai is not None or ewai_val is not None: - if '6600d' not in permissions: - return Response({'code': 403, 'msg': '无权限修改提现限额(需要6600d)'}) - if kaioi_ewai is not None: - zuzhang.kaioi_ewai_tixian = bool(kaioi_ewai) - if ewai_val is not None: - try: - zuzhang.ewai_tixian_xiane = Decimal(str(ewai_val)) - except: - return Response({'code': 400, 'msg': '额外限额金额格式错误'}) - zuzhang.save() - - # ---------- 4. 分红相关修改(需要6600e)---------- - perm_enabled = request.data.get('permanent_enabled') - perm_amount = request.data.get('permanent_amount') - custom_first = request.data.get('custom_first_dividend') - extra_map = request.data.get('extra_dividend_map') - - if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]): - if '6600e' not in permissions: - return Response({'code': 403, 'msg': '无权限修改分红策略(需要6600e)'}) - - # 4.1 永久分红(额外分红开关和金额) - if perm_enabled is not None: - zuzhang.kaioi_ewai_fenhong = bool(perm_enabled) - if perm_amount is not None: - try: - zuzhang.ewai_fenhong_jine = Decimal(str(perm_amount)) - except: - return Response({'code': 400, 'msg': '永久分红金额格式错误'}) - zuzhang.save() - - # 4.2 首次定制分红(cishu=1) - if custom_first is not None and isinstance(custom_first, dict): - existing_first = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu=1) - for huiyuan_id, amount in custom_first.items(): - try: - amount = Decimal(str(amount)) - except: - continue - if amount <= 0: - # 删除定制(恢复默认) - DuociFenhong.objects.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() - else: - DuociFenhong.objects.update_or_create( - yonghuid=yonghuid, - huiyuan=huiyuan_id, - cishu=1, - defaults={ - 'zuzhang_fenhong': amount, - 'guanshi_fenhong': Decimal('0') # 组长表只关心组长分红 - } - ) - # 删除那些在前端字典中不存在的首次定制记录(即已取消定制) - for record in existing_first: - if record.huiyuan not in custom_first: - record.delete() - - # 4.3 额外次数分红(cishu >= 2) - if extra_map is not None and isinstance(extra_map, dict): - keep_set = set() - for times_str, members in extra_map.items(): - try: - cishu = int(times_str) - if cishu < 2: - continue - except: - continue - if not isinstance(members, dict): - continue - for huiyuan_id, data in members.items(): - if not isinstance(data, dict): - continue - amount = data.get('zuzhang_fenhong') - if amount is None: - continue - try: - amount = Decimal(str(amount)) - except: - continue - keep_set.add((cishu, huiyuan_id)) - DuociFenhong.objects.update_or_create( - yonghuid=yonghuid, - huiyuan=huiyuan_id, - cishu=cishu, - defaults={ - 'zuzhang_fenhong': amount, - 'guanshi_fenhong': Decimal('0') - } - ) - # 删除不在 keep_set 中的额外次数记录 - existing_extra = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu__gte=2) - for record in existing_extra: - if (record.cishu, record.huiyuan) not in keep_set: - record.delete() - - # 保存组长其他可能修改的字段(例如电话、微信没有,忽略) - - return Response({'code': 0, 'msg': '修改成功'}) - - - - -class GetProductTypeZoneView(APIView): - """ - 获取商品类型、专区及会员列表 - 权限:需要拥有 7007a - 请求:POST /houtai/hthqsplxzq - 参数:{"username": "客服手机号"} - 返回: - code: 0 - data: { - type_list: [{id, jieshao, tupian_url, yaoqiuleixing, huiyuan_id, yongjin, shenhezhuangtai, is_cross_enabled, paixu, product_count}, ...], - zone_list: [{id, mingzi, leixing_id, shenhezhuangtai, paixu}, ...], - member_list: [{huiyuan_id, jieshao, jiage}, ...] - } - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '7007a' not in permissions: - return Response({'code': 403, 'msg': '您无权访问此页面'}) - - # 查询所有商品类型(含专区数量) - types = ShangpinLeixing.objects.all().order_by('-paixu') # paixu 越大越靠前 - type_list = [] - for t in types: - # 统计该类型下的专区数量 - zone_count = ShangpinZhuanqu.objects.filter(leixing_id=t.id).count() - type_list.append({ - 'id': t.id, - 'jieshao': t.jieshao or '', - 'tupian_url': t.tupian_url or '', - 'yaoqiuleixing': t.yaoqiuleixing, - 'huiyuan_id': t.huiyuan_id or '', - 'yongjin': float(t.yongjin) if t.yongjin is not None else None, - 'shenhezhuangtai': t.shenhezhuangtai, - 'is_cross_enabled': t.is_cross_enabled, - 'paixu': t.paixu, - 'product_count': zone_count, - }) - - # 查询所有专区 - zones = ShangpinZhuanqu.objects.all().order_by('-paixu') - zone_list = [ - { - 'id': z.id, - 'mingzi': z.mingzi or '', - 'leixing_id': z.leixing_id, - 'shenhezhuangtai': z.shenhezhuangtai, - 'paixu': z.paixu, - } - for z in zones - ] - - # 查询所有会员(只返回 id、介绍、价格) - members = Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage') - member_list = [ - { - 'huiyuan_id': m['huiyuan_id'], - 'jieshao': m['jieshao'], - 'jiage': float(m['jiage']), - } - for m in members - ] - - return Response({ - 'code': 0, - 'data': { - 'type_list': type_list, - 'zone_list': zone_list, - 'member_list': member_list, - } - }) - - - - - - - - -'''class ModifyProductTypeZoneView(APIView): - """ - 统一修改商品类型/专区(增、删、改) - 权限:需要拥有 7007a - 请求:POST /houtai/htxgsplx - 支持 multipart/form-data 和 application/json - 参数(通用): - username: 客服账号 - type: 'leixing' 或 'zhuanqu' - action: 'add' / 'update' / 'delete' - 类型专用参数: - - add/update: id (update时必填), jieshao, tupian_url(可不上传), file(图片文件), yaoqiuleixing, huiyuan_id, yongjin, shenhezhuangtai, is_cross_enabled, sync_to_products (可选) - - delete: id, delete_products (布尔) - 专区专用参数: - - add/update: id (update时必填), mingzi, leixing_id, shenhezhuangtai - - delete: id, delete_products (布尔) - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser, MultiPartParser, FormParser] - - def post(self, request): - username = request.data.get('username', '').strip() - obj_type = request.data.get('type') # 'leixing' 或 'zhuanqu' - action = request.data.get('action') # 'add', 'update', 'delete' - - if not username or not obj_type or not action: - return Response({'code': 400, 'msg': '缺少必要参数'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '7007a' not in permissions: - return Response({'code': 403, 'msg': '您无权进行此操作'}) - - if obj_type == 'leixing': - return self.handle_leixing(request, action) - elif obj_type == 'zhuanqu': - return self.handle_zhuanqu(request, action) - else: - return Response({'code': 400, 'msg': '无效的type参数'}) - - # ---------- 商品类型处理 ---------- - def handle_leixing(self, request, action): - if action == 'add': - return self.add_leixing(request) - elif action == 'update': - return self.update_leixing(request) - elif action == 'delete': - return self.delete_leixing(request) - else: - return Response({'code': 400, 'msg': '无效的action'}) - - def add_leixing(self, request): - # 获取表单字段 - jieshao = request.data.get('jieshao', '').strip() - yaoqiuleixing = request.data.get('yaoqiuleixing') - huiyuan_id = request.data.get('huiyuan_id', '').strip() or None - yongjin = request.data.get('yongjin') - shenhezhuangtai = request.data.get('shenhezhuangtai', 1) - is_cross_enabled = request.data.get('is_cross_enabled', False) - image_file = request.FILES.get('file') # 上传的图片文件 - - if not jieshao: - return Response({'code': 400, 'msg': '类型名称不能为空'}) - if yaoqiuleixing not in [1, 2]: - return Response({'code': 400, 'msg': '抢单要求类型无效'}) - if not image_file: - return Response({'code': 400, 'msg': '请上传类型图片'}) - - # 校验图片 - try: - from utils.oss_utils import validate_image - valid, msg = validate_image(image_file) - if not valid: - return Response({'code': 400, 'msg': msg}) - except ImportError: - pass - - # 上传图片到OSS - ext = image_file.name.split('.')[-1].lower() - new_filename = f"{uuid.uuid4().hex}.{ext}" - oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}" - url = upload_to_oss(image_file, oss_path) - if not url: - return Response({'code': 500, 'msg': '图片上传失败'}) - - # 创建类型 - try: - with transaction.atomic(): - leixing = ShangpinLeixing.objects.create( - jieshao=jieshao, - tupian_url=oss_path, # 存储相对路径 - yaoqiuleixing=yaoqiuleixing, - huiyuan_id=huiyuan_id, - yongjin=Decimal(yongjin) if yongjin else None, - shenhezhuangtai=int(shenhezhuangtai), - is_cross_enabled=is_cross_enabled in [True, 'true', '1'], - paixu=0 # 新添加的类型排序默认0,前端可再调整 - ) - return Response({'code': 0, 'msg': '添加成功', 'data': {'id': leixing.id}}) - except Exception as e: - logger.error(f"添加类型失败: {e}", exc_info=True) - # 删除已上传的图片 - delete_from_oss(oss_path) - return Response({'code': 500, 'msg': '数据库错误'}) - - def update_leixing(self, request): - type_id = request.data.get('id') - if not type_id: - return Response({'code': 400, 'msg': '缺少类型ID'}) - try: - leixing = ShangpinLeixing.objects.get(id=type_id) - except ShangpinLeixing.DoesNotExist: - return Response({'code': 404, 'msg': '类型不存在'}) - - # 收集修改字段 - changes = {} - jieshao = request.data.get('jieshao') - if jieshao is not None and jieshao != leixing.jieshao: - changes['jieshao'] = jieshao.strip() - - yaoqiuleixing = request.data.get('yaoqiuleixing') - if yaoqiuleixing is not None and int(yaoqiuleixing) != leixing.yaoqiuleixing: - changes['yaoqiuleixing'] = int(yaoqiuleixing) - - huiyuan_id = request.data.get('huiyuan_id', '').strip() or None - if huiyuan_id != leixing.huiyuan_id: - changes['huiyuan_id'] = huiyuan_id - - yongjin = request.data.get('yongjin') - if yongjin is not None: - new_yongjin = Decimal(yongjin) if yongjin else None - if new_yongjin != leixing.yongjin: - changes['yongjin'] = new_yongjin - - shenhezhuangtai = request.data.get('shenhezhuangtai') - if shenhezhuangtai is not None and int(shenhezhuangtai) != leixing.shenhezhuangtai: - changes['shenhezhuangtai'] = int(shenhezhuangtai) - - is_cross = request.data.get('is_cross_enabled') - if is_cross is not None: - new_cross = is_cross in [True, 'true', '1'] - if new_cross != leixing.is_cross_enabled: - changes['is_cross_enabled'] = new_cross - - # 图片处理 - image_file = request.FILES.get('file') - old_image_path = leixing.tupian_url - if image_file: - # 验证并上传新图片 - try: - from utils.oss_utils import validate_image - valid, msg = validate_image(image_file) - if not valid: - return Response({'code': 400, 'msg': msg}) - except ImportError: - pass - ext = image_file.name.split('.')[-1].lower() - new_filename = f"{uuid.uuid4().hex}.{ext}" - oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}" - url = upload_to_oss(image_file, oss_path) - if not url: - return Response({'code': 500, 'msg': '图片上传失败'}) - changes['tupian_url'] = oss_path - - if not changes and not image_file: - return Response({'code': 400, 'msg': '未做任何修改'}) - - # 执行更新 - sync_to_products = request.data.get('sync_to_products') in [True, 'true', '1'] - try: - with transaction.atomic(): - # 更新类型 - for field, value in changes.items(): - setattr(leixing, field, value) - leixing.save() - - # 如果修改了抢单要求相关字段且需要同步商品 - if sync_to_products and ('yaoqiuleixing' in changes or 'huiyuan_id' in changes or 'yongjin' in changes): - # 更新所有关联此类型的商品 - product_updates = {} - if 'yaoqiuleixing' in changes: - product_updates['yaoqiuleixing'] = changes['yaoqiuleixing'] - if 'huiyuan_id' in changes: - product_updates['huiyuan_id'] = changes['huiyuan_id'] - if 'yongjin' in changes: - product_updates['yongjin'] = changes['yongjin'] - Shangpin.objects.filter(leixing_id=type_id).update(**product_updates) - - # 删除旧图片(新图片上传成功后才删除) - if image_file and old_image_path: - delete_from_oss(old_image_path) - - return Response({'code': 0, 'msg': '修改成功'}) - except Exception as e: - logger.error(f"更新类型失败: {e}", exc_info=True) - # 如果上传了新图片,尝试删除 - if image_file and 'oss_path' in locals(): - delete_from_oss(oss_path) - return Response({'code': 500, 'msg': '修改失败'}) - - def delete_leixing(self, request): - type_id = request.data.get('id') - if not type_id: - return Response({'code': 400, 'msg': '缺少类型ID'}) - delete_products = request.data.get('delete_products') in [True, 'true', '1'] - - try: - leixing = ShangpinLeixing.objects.get(id=type_id) - except ShangpinLeixing.DoesNotExist: - return Response({'code': 404, 'msg': '类型不存在'}) - - with transaction.atomic(): - # 删除该类型下的所有专区 - zones = ShangpinZhuanqu.objects.filter(leixing_id=type_id) - if delete_products: - # 删除专区下的所有商品 - for zone in zones: - Shangpin.objects.filter(zhuanqu_id=zone.id).delete() - # 删除所有专区 - zones.delete() - # 删除该类型下的所有商品(可能还有直接关联类型但无专区的商品) - Shangpin.objects.filter(leixing_id=type_id).delete() - else: - # 只删除专区,不删除商品 - zones.delete() - # 商品保持原样(leixing_id 保留,但类型已删除,可能导致数据不一致,按需求不处理) - # 也可以将商品的 leixing_id 置空 - Shangpin.objects.filter(leixing_id=type_id).update(leixing_id=None) - - # 删除类型本身 - leixing.delete() - - # 删除类型图片 - if leixing.tupian_url: - delete_from_oss(leixing.tupian_url) - - return Response({'code': 0, 'msg': '删除成功'}) - - # ---------- 商品专区处理 ---------- - def handle_zhuanqu(self, request, action): - if action == 'add': - return self.add_zhuanqu(request) - elif action == 'update': - return self.update_zhuanqu(request) - elif action == 'delete': - return self.delete_zhuanqu(request) - else: - return Response({'code': 400, 'msg': '无效的action'}) - - def add_zhuanqu(self, request): - mingzi = request.data.get('mingzi', '').strip() - leixing_id = request.data.get('leixing_id') - shenhezhuangtai = request.data.get('shenhezhuangtai', 1) - - if not mingzi: - return Response({'code': 400, 'msg': '专区名称不能为空'}) - if not leixing_id: - return Response({'code': 400, 'msg': '请选择所属类型'}) - # 校验类型是否存在 - if not ShangpinLeixing.objects.filter(id=leixing_id).exists(): - return Response({'code': 404, 'msg': '所属类型不存在'}) - - try: - with transaction.atomic(): - zhuanqu = ShangpinZhuanqu.objects.create( - mingzi=mingzi, - leixing_id=int(leixing_id), - shenhezhuangtai=int(shenhezhuangtai), - paixu=0 - ) - return Response({'code': 0, 'msg': '添加成功', 'data': {'id': zhuanqu.id}}) - except Exception as e: - logger.error(f"添加专区失败: {e}", exc_info=True) - return Response({'code': 500, 'msg': '数据库错误'}) - - def update_zhuanqu(self, request): - zone_id = request.data.get('id') - if not zone_id: - return Response({'code': 400, 'msg': '缺少专区ID'}) - try: - zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id) - except ShangpinZhuanqu.DoesNotExist: - return Response({'code': 404, 'msg': '专区不存在'}) - - changes = {} - mingzi = request.data.get('mingzi') - if mingzi is not None and mingzi != zhuanqu.mingzi: - changes['mingzi'] = mingzi.strip() - leixing_id = request.data.get('leixing_id') - if leixing_id is not None and int(leixing_id) != zhuanqu.leixing_id: - # 校验新类型存在 - if not ShangpinLeixing.objects.filter(id=leixing_id).exists(): - return Response({'code': 404, 'msg': '新所属类型不存在'}) - changes['leixing_id'] = int(leixing_id) - shenhezhuangtai = request.data.get('shenhezhuangtai') - if shenhezhuangtai is not None and int(shenhezhuangtai) != zhuanqu.shenhezhuangtai: - changes['shenhezhuangtai'] = int(shenhezhuangtai) - - if not changes: - return Response({'code': 400, 'msg': '未做任何修改'}) - - try: - with transaction.atomic(): - for field, value in changes.items(): - setattr(zhuanqu, field, value) - zhuanqu.save() - return Response({'code': 0, 'msg': '修改成功'}) - except Exception as e: - logger.error(f"更新专区失败: {e}", exc_info=True) - return Response({'code': 500, 'msg': '修改失败'}) - - def delete_zhuanqu(self, request): - zone_id = request.data.get('id') - if not zone_id: - return Response({'code': 400, 'msg': '缺少专区ID'}) - delete_products = request.data.get('delete_products') in [True, 'true', '1'] - - try: - zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id) - except ShangpinZhuanqu.DoesNotExist: - return Response({'code': 404, 'msg': '专区不存在'}) - - with transaction.atomic(): - if delete_products: - Shangpin.objects.filter(zhuanqu_id=zone_id).delete() - else: - Shangpin.objects.filter(zhuanqu_id=zone_id).update(zhuanqu_id=None) - zhuanqu.delete() - - return Response({'code': 0, 'msg': '删除成功'})''' - -class ModifyProductTypeZoneView(APIView): - """ - 统一修改商品类型/专区(增、删、改) - 权限:需要拥有 7007a - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser, MultiPartParser, FormParser] - - def post(self, request): - username = request.data.get('username', '').strip() - obj_type = request.data.get('type') # 'leixing' 或 'zhuanqu' - action = request.data.get('action') # 'add', 'update', 'delete' - - if not username or not obj_type or not action: - return Response({'code': 400, 'msg': '缺少必要参数'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '7007a' not in permissions: - return Response({'code': 403, 'msg': '您无权进行此操作'}) - - if obj_type == 'leixing': - return self.handle_leixing(request, action) - elif obj_type == 'zhuanqu': - return self.handle_zhuanqu(request, action) - else: - return Response({'code': 400, 'msg': '无效的type参数'}) - - # ---------- 商品类型处理 ---------- - def handle_leixing(self, request, action): - if action == 'add': - return self.add_leixing(request) - elif action == 'update': - return self.update_leixing(request) - elif action == 'delete': - return self.delete_leixing(request) - else: - return Response({'code': 400, 'msg': '无效的action'}) - - def add_leixing(self, request): - # 获取并校验字段 - jieshao = request.data.get('jieshao', '').strip() - if not jieshao: - return Response({'code': 400, 'msg': '类型名称不能为空'}) - - # 强制转为整数 - try: - yaoqiuleixing = int(request.data.get('yaoqiuleixing', 0)) - if yaoqiuleixing not in [1, 2]: - raise ValueError - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'}) - - try: - shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) - if shenhezhuangtai not in [1, 2]: - raise ValueError - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) - - is_cross_enabled = request.data.get('is_cross_enabled', False) - is_cross_enabled = is_cross_enabled in [True, 'true', '1', 'on'] - - huiyuan_id = request.data.get('huiyuan_id', '').strip() or None - yongjin = request.data.get('yongjin') - if yongjin is not None and yongjin != '': - try: - yongjin = Decimal(str(yongjin)) - except: - return Response({'code': 400, 'msg': '佣金金额格式错误'}) - else: - yongjin = None - - # 图片校验 - image_file = request.FILES.get('file') - if not image_file: - return Response({'code': 400, 'msg': '请上传类型图片'}) - - # 验证图片 - try: - from utils.oss_utils import validate_image - valid, msg = validate_image(image_file) - if not valid: - return Response({'code': 400, 'msg': msg}) - except ImportError: - pass - - # 上传图片 - ext = image_file.name.split('.')[-1].lower() - new_filename = f"{uuid.uuid4().hex}.{ext}" - oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}" - url = upload_to_oss(image_file, oss_path) - if not url: - return Response({'code': 500, 'msg': '图片上传失败'}) - - # 创建类型 - try: - with transaction.atomic(): - leixing = ShangpinLeixing.objects.create( - jieshao=jieshao, - tupian_url=oss_path, - yaoqiuleixing=yaoqiuleixing, - huiyuan_id=huiyuan_id, - yongjin=yongjin, - shenhezhuangtai=shenhezhuangtai, - is_cross_enabled=is_cross_enabled, - paixu=0 - ) - return Response({'code': 0, 'msg': '添加成功', 'data': {'id': leixing.id}}) - except Exception as e: - logger.error(f"添加类型失败: {e}", exc_info=True) - delete_from_oss(oss_path) - return Response({'code': 500, 'msg': '数据库错误'}) - - def update_leixing(self, request): - type_id = request.data.get('id') - if not type_id: - return Response({'code': 400, 'msg': '缺少类型ID'}) - try: - leixing = ShangpinLeixing.objects.get(id=type_id) - except ShangpinLeixing.DoesNotExist: - return Response({'code': 404, 'msg': '类型不存在'}) - - changes = {} - # 名称 - jieshao = request.data.get('jieshao') - if jieshao is not None and jieshao.strip() != leixing.jieshao: - changes['jieshao'] = jieshao.strip() - - # 抢单要求类型(强制转整数) - yaoqiuleixing = request.data.get('yaoqiuleixing') - if yaoqiuleixing is not None: - try: - new_val = int(yaoqiuleixing) - if new_val not in [1, 2]: - raise ValueError - if new_val != leixing.yaoqiuleixing: - changes['yaoqiuleixing'] = new_val - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'}) - - # 会员ID - huiyuan_id = request.data.get('huiyuan_id', '').strip() or None - if huiyuan_id != leixing.huiyuan_id: - changes['huiyuan_id'] = huiyuan_id - - # 佣金金额 - yongjin = request.data.get('yongjin') - if yongjin is not None: - if yongjin == '': - new_val = None - else: - try: - new_val = Decimal(str(yongjin)) - except: - return Response({'code': 400, 'msg': '佣金金额格式错误'}) - if new_val != leixing.yongjin: - changes['yongjin'] = new_val - - # 审核状态 - shenhezhuangtai = request.data.get('shenhezhuangtai') - if shenhezhuangtai is not None: - try: - new_val = int(shenhezhuangtai) - if new_val not in [1, 2]: - raise ValueError - if new_val != leixing.shenhezhuangtai: - changes['shenhezhuangtai'] = new_val - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) - - # 跨平台互通 - is_cross = request.data.get('is_cross_enabled') - if is_cross is not None: - new_cross = is_cross in [True, 'true', '1', 'on'] - if new_cross != leixing.is_cross_enabled: - changes['is_cross_enabled'] = new_cross - - # 图片处理 - image_file = request.FILES.get('file') - old_image_path = leixing.tupian_url - if image_file: - try: - from utils.oss_utils import validate_image - valid, msg = validate_image(image_file) - if not valid: - return Response({'code': 400, 'msg': msg}) - except ImportError: - pass - ext = image_file.name.split('.')[-1].lower() - new_filename = f"{uuid.uuid4().hex}.{ext}" - oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}" - url = upload_to_oss(image_file, oss_path) - if not url: - return Response({'code': 500, 'msg': '图片上传失败'}) - changes['tupian_url'] = oss_path - - if not changes and not image_file: - return Response({'code': 400, 'msg': '未做任何修改'}) - - sync_to_products = request.data.get('sync_to_products') in [True, 'true', '1', 'on'] - try: - with transaction.atomic(): - for field, value in changes.items(): - setattr(leixing, field, value) - leixing.save() - - # 同步商品 - if sync_to_products and ('yaoqiuleixing' in changes or 'huiyuan_id' in changes or 'yongjin' in changes): - product_updates = {} - if 'yaoqiuleixing' in changes: - product_updates['yaoqiuleixing'] = changes['yaoqiuleixing'] - if 'huiyuan_id' in changes: - product_updates['huiyuan_id'] = changes['huiyuan_id'] - if 'yongjin' in changes: - product_updates['yongjin'] = changes['yongjin'] - if product_updates: - Shangpin.objects.filter(leixing_id=type_id).update(**product_updates) - - # 删除旧图片 - if image_file and old_image_path: - delete_from_oss(old_image_path) - - return Response({'code': 0, 'msg': '修改成功'}) - except Exception as e: - logger.error(f"更新类型失败: {e}", exc_info=True) - if image_file and 'oss_path' in locals(): - delete_from_oss(oss_path) - return Response({'code': 500, 'msg': '修改失败'}) - - def delete_leixing(self, request): - type_id = request.data.get('id') - if not type_id: - return Response({'code': 400, 'msg': '缺少类型ID'}) - delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on'] - - try: - leixing = ShangpinLeixing.objects.get(id=type_id) - except ShangpinLeixing.DoesNotExist: - return Response({'code': 404, 'msg': '类型不存在'}) - - with transaction.atomic(): - # 删除该类型下的所有专区 - zones = ShangpinZhuanqu.objects.filter(leixing_id=type_id) - if delete_products: - # 删除专区下的所有商品 - for zone in zones: - Shangpin.objects.filter(zhuanqu_id=zone.id).delete() - zones.delete() - # 删除直接关联该类型的商品 - Shangpin.objects.filter(leixing_id=type_id).delete() - else: - zones.delete() - # 仅清除商品的类型关联 - Shangpin.objects.filter(leixing_id=type_id).update(leixing_id=None) - - leixing.delete() - - if leixing.tupian_url: - delete_from_oss(leixing.tupian_url) - - return Response({'code': 0, 'msg': '删除成功'}) - - # ---------- 商品专区处理 ---------- - def handle_zhuanqu(self, request, action): - if action == 'add': - return self.add_zhuanqu(request) - elif action == 'update': - return self.update_zhuanqu(request) - elif action == 'delete': - return self.delete_zhuanqu(request) - else: - return Response({'code': 400, 'msg': '无效的action'}) - - def add_zhuanqu(self, request): - mingzi = request.data.get('mingzi', '').strip() - leixing_id = request.data.get('leixing_id') - if not mingzi: - return Response({'code': 400, 'msg': '专区名称不能为空'}) - if not leixing_id: - return Response({'code': 400, 'msg': '请选择所属类型'}) - - try: - leixing_id = int(leixing_id) - if not ShangpinLeixing.objects.filter(id=leixing_id).exists(): - return Response({'code': 404, 'msg': '所属类型不存在'}) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '所属类型ID无效'}) - - try: - shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) - if shenhezhuangtai not in [1, 2]: - raise ValueError - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) - - try: - with transaction.atomic(): - zhuanqu = ShangpinZhuanqu.objects.create( - mingzi=mingzi, - leixing_id=leixing_id, - shenhezhuangtai=shenhezhuangtai, - paixu=0 - ) - return Response({'code': 0, 'msg': '添加成功', 'data': {'id': zhuanqu.id}}) - except Exception as e: - logger.error(f"添加专区失败: {e}", exc_info=True) - return Response({'code': 500, 'msg': '数据库错误'}) - - def update_zhuanqu(self, request): - zone_id = request.data.get('id') - if not zone_id: - return Response({'code': 400, 'msg': '缺少专区ID'}) - try: - zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id) - except ShangpinZhuanqu.DoesNotExist: - return Response({'code': 404, 'msg': '专区不存在'}) - - changes = {} - mingzi = request.data.get('mingzi') - if mingzi is not None and mingzi.strip() != zhuanqu.mingzi: - changes['mingzi'] = mingzi.strip() - - leixing_id = request.data.get('leixing_id') - if leixing_id is not None: - try: - new_id = int(leixing_id) - if not ShangpinLeixing.objects.filter(id=new_id).exists(): - return Response({'code': 404, 'msg': '新所属类型不存在'}) - if new_id != zhuanqu.leixing_id: - changes['leixing_id'] = new_id - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '所属类型ID无效'}) - - shenhezhuangtai = request.data.get('shenhezhuangtai') - if shenhezhuangtai is not None: - try: - new_val = int(shenhezhuangtai) - if new_val not in [1, 2]: - raise ValueError - if new_val != zhuanqu.shenhezhuangtai: - changes['shenhezhuangtai'] = new_val - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) - - if not changes: - return Response({'code': 400, 'msg': '未做任何修改'}) - - try: - with transaction.atomic(): - for field, value in changes.items(): - setattr(zhuanqu, field, value) - zhuanqu.save() - return Response({'code': 0, 'msg': '修改成功'}) - except Exception as e: - logger.error(f"更新专区失败: {e}", exc_info=True) - return Response({'code': 500, 'msg': '修改失败'}) - - def delete_zhuanqu(self, request): - zone_id = request.data.get('id') - if not zone_id: - return Response({'code': 400, 'msg': '缺少专区ID'}) - delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on'] - - try: - zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id) - except ShangpinZhuanqu.DoesNotExist: - return Response({'code': 404, 'msg': '专区不存在'}) - - with transaction.atomic(): - if delete_products: - Shangpin.objects.filter(zhuanqu_id=zone_id).delete() - else: - Shangpin.objects.filter(zhuanqu_id=zone_id).update(zhuanqu_id=None) - zhuanqu.delete() - - return Response({'code': 0, 'msg': '删除成功'}) - - -# 继续在 houtai/views_product.py 中添加 -class GetRateView(APIView): - """ - 获取打手分成费率(平台订单和商家订单) - 权限:需要拥有 7007b - 请求:POST /houtai/hthqfhpz - 参数:{"username": "客服手机号"} - 返回:{"code":0, "data": {"platform_rate": xx, "merchant_rate": xx}} - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '7007b' not in permissions: - return Response({'code': 403, 'msg': '您无权查看费率'}) - - from dingdan.models import Lilubiao - try: - platform_rate_obj = Lilubiao.objects.get(fadanpingtai='1') - merchant_rate_obj = Lilubiao.objects.get(fadanpingtai='3') - except Lilubiao.DoesNotExist as e: - logger.error(f"费率记录缺失: {e}") - return Response({'code': 500, 'msg': '费率配置缺失'}) - - return Response({ - 'code': 0, - 'data': { - 'platform_rate': float(platform_rate_obj.lilu) if platform_rate_obj.lilu else 0, - 'merchant_rate': float(merchant_rate_obj.lilu) if merchant_rate_obj.lilu else 0, - } - }) - - -class ModifyRateView(APIView): - """ - 修改打手分成费率 - 权限:需要拥有 7007b - 请求:POST /houtai/htxgfl - 参数:{"username": "客服手机号", "type": "platform" / "merchant", "value": 费率(百分比数字)} - 返回:{"code":0, "msg":"修改成功"} - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - rate_type = request.data.get('type') - value = request.data.get('value') - - if not username or not rate_type or value is None: - return Response({'code': 400, 'msg': '缺少参数'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '7007b' not in permissions: - return Response({'code': 403, 'msg': '您无权修改费率'}) - - try: - rate_value = Decimal(str(value)) - except: - return Response({'code': 400, 'msg': '费率值格式错误'}) - - - if rate_type == 'platform': - fadanpingtai = '1' - elif rate_type == 'merchant': - fadanpingtai = '3' - else: - return Response({'code': 400, 'msg': '无效的type参数'}) - - try: - with transaction.atomic(): - rate_obj = Lilubiao.objects.select_for_update().get(fadanpingtai=fadanpingtai) - rate_obj.lilu = rate_value - rate_obj.save() - except Lilubiao.DoesNotExist: - return Response({'code': 404, 'msg': '费率记录不存在'}) - except Exception as e: - logger.error(f"修改费率失败: {e}", exc_info=True) - return Response({'code': 500, 'msg': '修改失败'}) - - return Response({'code': 0, 'msg': '修改成功'}) - - - - - - -# 权限码(根据你要求,使用 '8080a') -REQUIRED_PERMISSION = '8080a' -class PopupNoticeListAPIView(APIView): - """ - 获取所有弹窗配置(页面 + 弹窗 + 图片) - URL: POST /houtai/hthqtcxx - 请求参数: { "username": "客服账号" } - 响应格式: { "code": 0, "data": { "pages": [...] } } - 注意:所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段区分 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 获取前端传递的 username(用于防越权) - username_frontend = request.data.get('username') - # 2. 调用公共权限验证方法,返回 (客服对象, 权限列表) - kefu, permissions = verify_kefu_permission(request, username_frontend) - if kefu is None: - # 身份验证失败,返回业务码 403,但 HTTP 状态码仍是 200 - return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) - - # 3. 检查是否拥有所需权限码 '8080a' - if REQUIRED_PERMISSION not in permissions: - return Response({'code': 403, 'msg': '您没有权限访问弹窗管理功能'}) - - # 4. 查询所有页面,并预加载关联的弹窗和图片(避免N+1查询) - pages = PopupPage.objects.all().prefetch_related('popups__images').order_by('id') - - # 5. 序列化输出 - serializer = PopupPageSerializer(pages, many=True) - return Response({'code': 0, 'data': {'pages': serializer.data}}) - - -class PopupNoticeModifyAPIView(APIView): - """ - 统一修改接口:支持页面、弹窗、图片的增删改 - URL: POST /houtai/htxgtcxx - 请求中必须包含 action 字段,用于区分操作类型。 - 所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段体现。 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # ---------- 1. 权限验证 ---------- - username_frontend = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username_frontend) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) - if REQUIRED_PERMISSION not in permissions: - return Response({'code': 403, 'msg': '您没有权限操作弹窗管理'}) - - # ---------- 2. 获取 action 并分发 ---------- - action = request.data.get('action') - if not action: - return Response({'code': 400, 'msg': '缺少action参数'}) - - try: - if action == 'add_page': - return self.add_page(request) - elif action == 'update_page': - return self.update_page(request) - elif action == 'delete_page': - return self.delete_page(request) - elif action == 'add_popup': - return self.add_popup(request) - elif action == 'update_popup': - return self.update_popup(request) - elif action == 'delete_popup': - return self.delete_popup(request) - elif action == 'add_image': - return self.add_image(request) - elif action == 'update_image': - return self.update_image(request) - elif action == 'delete_image': - return self.delete_image(request) - else: - return Response({'code': 400, 'msg': f'未知的action: {action}'}) - except Exception as e: - # 捕获未预期的异常,返回服务器错误 - import traceback - traceback.print_exc() - return Response({'code': 500, 'msg': f'服务器错误: {str(e)}'}) - - # ==================== 页面相关操作 ==================== - - def add_page(self, request): - """添加新页面""" - data = request.data - # 必填字段校验 - if not data.get('page_key') or not data.get('name'): - return Response({'code': 400, 'msg': '缺少 page_key 或 name'}) - # 唯一性校验 - if PopupPage.objects.filter(page_key=data['page_key']).exists(): - return Response({'code': 400, 'msg': '页面标识已存在'}) - - page = PopupPage.objects.create( - page_key=data['page_key'], - name=data['name'], - description=data.get('description', ''), - is_active=data.get('is_active', True), - ignore_user_mute=data.get('ignore_user_mute', False) - ) - return Response({'code': 0, 'msg': '添加成功', 'data': {'id': page.id}}) - - def update_page(self, request): - """更新页面信息""" - page_id = request.data.get('id') - if not page_id: - return Response({'code': 400, 'msg': '缺少页面id'}) - try: - page = PopupPage.objects.get(id=page_id) - except PopupPage.DoesNotExist: - return Response({'code': 404, 'msg': '页面不存在'}) - - # 更新允许修改的字段 - page.page_key = request.data.get('page_key', page.page_key) - page.name = request.data.get('name', page.name) - page.description = request.data.get('description', page.description) - page.is_active = request.data.get('is_active', page.is_active) - page.ignore_user_mute = request.data.get('ignore_user_mute', page.ignore_user_mute) - page.save() - return Response({'code': 0, 'msg': '更新成功'}) - - def delete_page(self, request): - """删除页面,可级联删除其下所有弹窗和图片""" - page_id = request.data.get('id') - cascade = request.data.get('cascade_delete_popups', True) # 默认级联 - if not page_id: - return Response({'code': 400, 'msg': '缺少页面id'}) - try: - page = PopupPage.objects.get(id=page_id) - except PopupPage.DoesNotExist: - return Response({'code': 404, 'msg': '页面不存在'}) - - if cascade: - # 级联删除:使用事务保证原子性 - with transaction.atomic(): - popups = page.popups.all() - for popup in popups: - # 删除弹窗下的所有图片文件 - for img in popup.images.all(): - if img.image_url: - delete_from_oss(img.image_url) - popups.delete() - page.delete() - else: - # 非级联删除:如果页面下还有弹窗,则拒绝删除 - if page.popups.exists(): - return Response({'code': 400, 'msg': '该页面下存在弹窗,请先删除弹窗或使用级联删除'}) - page.delete() - - return Response({'code': 0, 'msg': '删除成功'}) - - # ==================== 弹窗相关操作 ==================== - - def add_popup(self, request): - """为指定页面添加新弹窗""" - data = request.data - page_id = data.get('page_id') - popup_id = data.get('popup_id') - if not page_id or not popup_id: - return Response({'code': 400, 'msg': '缺少 page_id 或 popup_id'}) - try: - page = PopupPage.objects.get(id=page_id) - except PopupPage.DoesNotExist: - return Response({'code': 404, 'msg': '页面不存在'}) - - # 同一页面下 popup_id 必须唯一 - if PopupConfig.objects.filter(page=page, popup_id=popup_id).exists(): - return Response({'code': 400, 'msg': '该页面下已存在相同的弹窗ID'}) - - popup = PopupConfig.objects.create( - page=page, - popup_id=popup_id, - title=data.get('title', ''), - description=data.get('description', ''), - strategy_type=data.get('strategy_type', 'daily'), - max_count=data.get('max_count', 1), - reset_interval=data.get('reset_interval', 'day'), - duration=data.get('duration', 0), - force_even_muted=data.get('force_even_muted', False), - sort_order=data.get('sort_order', 0), - is_active=data.get('is_active', True), - start_time=data.get('start_time') or None, - end_time=data.get('end_time') or None - ) - return Response({'code': 0, 'msg': '添加成功', 'data': {'id': popup.id}}) - - def update_popup(self, request): - """更新弹窗信息""" - popup_id = request.data.get('id') - if not popup_id: - return Response({'code': 400, 'msg': '缺少弹窗id'}) - try: - popup = PopupConfig.objects.get(id=popup_id) - except PopupConfig.DoesNotExist: - return Response({'code': 404, 'msg': '弹窗不存在'}) - - # 更新弹窗字段 - popup.popup_id = request.data.get('popup_id', popup.popup_id) - popup.title = request.data.get('title', popup.title) - popup.description = request.data.get('description', popup.description) - popup.strategy_type = request.data.get('strategy_type', popup.strategy_type) - popup.max_count = request.data.get('max_count', popup.max_count) - popup.reset_interval = request.data.get('reset_interval', popup.reset_interval) - popup.duration = request.data.get('duration', popup.duration) - popup.force_even_muted = request.data.get('force_even_muted', popup.force_even_muted) - popup.sort_order = request.data.get('sort_order', popup.sort_order) - popup.is_active = request.data.get('is_active', popup.is_active) - popup.start_time = request.data.get('start_time') or None - popup.end_time = request.data.get('end_time') or None - popup.save() - return Response({'code': 0, 'msg': '更新成功'}) - - def delete_popup(self, request): - """删除弹窗,同时删除其下所有图片(包括OSS文件)""" - popup_id = request.data.get('id') - if not popup_id: - return Response({'code': 400, 'msg': '缺少弹窗id'}) - try: - popup = PopupConfig.objects.get(id=popup_id) - except PopupConfig.DoesNotExist: - return Response({'code': 404, 'msg': '弹窗不存在'}) - - with transaction.atomic(): - for img in popup.images.all(): - if img.image_url: - delete_from_oss(img.image_url) - popup.images.all().delete() - popup.delete() - return Response({'code': 0, 'msg': '删除成功'}) - - # ==================== 图片相关操作 ==================== - - def _upload_image_file(self, file_obj, page_key): - """ - 内部方法:将图片上传到OSS指定目录 - 目录: a_long/tanchuang/tanchuang/ - 文件名: {page_key}_{时间戳}_{uuid8}.{ext} - 返回: 相对路径(例如 a_long/tanchuang/tanchuang/dashouzhongxin_20250419120000_abc12345.jpg) - """ - # 验证图片格式和大小 - valid, msg = validate_image(file_obj) - if not valid: - raise ValueError(msg) - - # 生成唯一文件名 - ext = os.path.splitext(file_obj.name)[1].lower() - timestamp = datetime.now().strftime('%Y%m%d%H%M%S') - unique_id = str(uuid.uuid4())[:8] - filename = f"{page_key}_{timestamp}_{unique_id}{ext}" - relative_path = f"a_long/tanchuang/tanchuang/{filename}" - - # 上传到OSS - url = upload_to_oss(file_obj, relative_path) - if not url: - raise ValueError("图片上传失败") - return relative_path - - def add_image(self, request): - """为指定弹窗添加图片(支持上传文件)""" - popup_id = request.data.get('popup_id') - if not popup_id: - return Response({'code': 400, 'msg': '缺少 popup_id'}) - try: - popup = PopupConfig.objects.get(id=popup_id) - except PopupConfig.DoesNotExist: - return Response({'code': 404, 'msg': '弹窗不存在'}) - - # 获取上传的文件 - file_obj = request.FILES.get('file') - if not file_obj: - return Response({'code': 400, 'msg': '未提供图片文件'}) - - try: - page_key = popup.page.page_key - relative_path = self._upload_image_file(file_obj, page_key) - except ValueError as e: - return Response({'code': 400, 'msg': str(e)}) - - # 保存图片记录 - image = PopupImage.objects.create( - popup_config=popup, - image_url=relative_path, - text=request.data.get('text', ''), - sort_order=request.data.get('sort_order', 0) - ) - return Response({'code': 0, 'msg': '添加成功', 'data': {'id': image.id}}) - - def update_image(self, request): - """更新图片信息(可替换图片文件、修改文字、排序)""" - image_id = request.data.get('id') - if not image_id: - return Response({'code': 400, 'msg': '缺少图片id'}) - try: - image = PopupImage.objects.get(id=image_id) - except PopupImage.DoesNotExist: - return Response({'code': 404, 'msg': '图片不存在'}) - - # 更新文字和排序 - image.text = request.data.get('text', image.text) - image.sort_order = request.data.get('sort_order', image.sort_order) - - # 如果提供了新图片文件,则替换 - file_obj = request.FILES.get('file') - if file_obj: - try: - page_key = image.popup_config.page.page_key - new_path = self._upload_image_file(file_obj, page_key) - except ValueError as e: - return Response({'code': 400, 'msg': str(e)}) - # 删除旧图片 - if image.image_url: - delete_from_oss(image.image_url) - image.image_url = new_path - - image.save() - return Response({'code': 0, 'msg': '更新成功'}) - - def delete_image(self, request): - """删除图片(同时删除OSS文件)""" - image_id = request.data.get('id') - if not image_id: - return Response({'code': 400, 'msg': '缺少图片id'}) - try: - image = PopupImage.objects.get(id=image_id) - except PopupImage.DoesNotExist: - return Response({'code': 404, 'msg': '图片不存在'}) - - if image.image_url: - delete_from_oss(image.image_url) - image.delete() - return Response({'code': 0, 'msg': '删除成功'}) - - - - -from shangdian.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng -class ShopListView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 调用公共权限验证(与您给的示例完全一致) - username_frontend = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username_frontend) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) - - # 2. 检查是否拥有所需权限(999a~999e 任意一个) - required_perms = {'999a', '999b', '999c', '999d', '999e'} - if not required_perms.intersection(set(permissions)): - return Response({'code': 403, 'msg': '您没有权限访问店铺管理功能'}) - - # 3. 解析请求参数 - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 10)) - dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip() - zhuangtai = request.data.get('zhuangtai') - lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip() - user_id = request.data.get('user_id', '').strip() - ketixian_yue_min = request.data.get('ketixian_yue_min') - ketixian_yue_max = request.data.get('ketixian_yue_max') - zhengqu_zonge_min = request.data.get('zhengqu_zonge_min') - zhengqu_zonge_max = request.data.get('zhengqu_zonge_max') - - # 4. 构建查询集(先查店铺,预加载关联凭证) - queryset = Dianpu.objects.select_related('pingzheng').all() - - if dianpu_mingcheng: - queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng) - if zhuangtai is not None: - queryset = queryset.filter(zhuangtai=zhuangtai) - if lianxi_dianhua: - queryset = queryset.filter(lianxi_dianhua__icontains=lianxi_dianhua) - if user_id: - queryset = queryset.filter(pingzheng__yonghu=user_id) - - # 金额范围过滤 - if ketixian_yue_min is not None: - queryset = queryset.filter(ketixian_yue__gte=ketixian_yue_min) - if ketixian_yue_max is not None: - queryset = queryset.filter(ketixian_yue__lte=ketixian_yue_max) - if zhengqu_zonge_min is not None: - queryset = queryset.filter(zhengqu_zonge__gte=zhengqu_zonge_min) - if zhengqu_zonge_max is not None: - queryset = queryset.filter(zhengqu_zonge__lte=zhengqu_zonge_max) - - # 5. 分页 - paginator = Paginator(queryset, page_size) - page_obj = paginator.get_page(page) - - # 6. 构造返回数据 - shop_list = [] - for dianpu in page_obj: - pingzheng = dianpu.pingzheng - shop_list.append({ - 'id': dianpu.id, - 'dianpu_mingcheng': dianpu.dianpu_mingcheng, - 'dianpu_touxiang': dianpu.dianpu_touxiang or '', - 'lianxi_dianhua': dianpu.lianxi_dianhua or '', - 'weixinhao': dianpu.weixinhao or '', - 'zhuangtai': dianpu.zhuangtai, - 'bangding_yonghushu': dianpu.bangding_yonghushu, - 'erweima_url': dianpu.erweima_url or '', - 'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng, - 'shouyi_choucheng_feilv': dianpu.shouyi_choucheng_feilv or 0, - 'ketixian_yue': dianpu.ketixian_yue, - 'zhengqu_zonge': dianpu.zhengqu_zonge, - 'meiri_tixian_xiane': dianpu.meiri_tixian_xiane or 0, - 'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane, - 'pingzheng__yonghu': pingzheng.yonghu, # 用户ID - 'pingzheng__zhanghao': pingzheng.zhanghao, # 登录账号 - }) - - # 获取默认配置(ID=1,不存在则创建) - default_config, _ = DianpuMorenPeizhi.objects.get_or_create(id=1, defaults={ - 'moren_choucheng_feilv': 0.1, - 'moren_tixian_xiane': 1000.00 - }) - - return Response({ - 'code': 0, - 'msg': 'success', - 'data': { - 'list': shop_list, - 'total': paginator.count, - 'default_config': { - 'moren_choucheng_feilv': default_config.moren_choucheng_feilv, - 'moren_tixian_xiane': default_config.moren_tixian_xiane, - } - } - }) - -from django.db import transaction, IntegrityError -class ShopModifyView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 身份验证 - username_frontend = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username_frontend) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) - - # 转换权限为集合方便判断 - user_perms = set(permissions) - action = request.data.get('action', '') - - # 2. 根据 action 分派任务 - if action == 'update_dianpu': - return self._update_dianpu(request, user_perms) - elif action == 'add_dianpu': - return self._add_dianpu(request, user_perms) - elif action == 'update_default': - return self._update_default(request, user_perms) - else: - return Response({'code': 400, 'msg': '未知操作类型'}) - - def _update_dianpu(self, request, user_perms): - """修改店铺信息,精确权限控制""" - dianpu_id = request.data.get('dianpu_id') - if not dianpu_id: - return Response({'code': 400, 'msg': '缺少店铺ID'}) - - try: - with transaction.atomic(): - dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id) - - # 基础信息修改 (999e) - if any(k in request.data for k in ['dianpu_mingcheng', 'lianxi_dianhua', 'weixinhao']): - if '999e' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改店铺基本信息(999e)'}) - if 'dianpu_mingcheng' in request.data: - dianpu.dianpu_mingcheng = request.data['dianpu_mingcheng'] - if 'lianxi_dianhua' in request.data: - dianpu.lianxi_dianhua = request.data['lianxi_dianhua'] - if 'weixinhao' in request.data: - dianpu.weixinhao = request.data['weixinhao'] - - # 店铺状态 (999a) - if 'zhuangtai' in request.data: - if '999a' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改店铺状态(999a)'}) - new_status = int(request.data['zhuangtai']) - if new_status not in (0, 1): - return Response({'code': 400, 'msg': '店铺状态无效'}) - dianpu.zhuangtai = new_status - - # 可提现余额 (999b) - if 'ketixian_yue' in request.data: - if '999b' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改可提现余额(999b)'}) - dianpu.ketixian_yue = request.data['ketixian_yue'] - - # 提现限额相关 (999c) - if 'kaiqi_meiri_xiane' in request.data or 'meiri_tixian_xiane' in request.data: - if '999c' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改提现限额配置(999c)'}) - if 'kaiqi_meiri_xiane' in request.data: - dianpu.kaiqi_meiri_xiane = request.data['kaiqi_meiri_xiane'] - if 'meiri_tixian_xiane' in request.data: - dianpu.meiri_tixian_xiane = request.data['meiri_tixian_xiane'] - - # 抽成相关 (999d) - if 'kaiqi_shouyi_choucheng' in request.data or 'shouyi_choucheng_feilv' in request.data: - if '999d' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改抽成配置(999d)'}) - if 'kaiqi_shouyi_choucheng' in request.data: - dianpu.kaiqi_shouyi_choucheng = request.data['kaiqi_shouyi_choucheng'] - if 'shouyi_choucheng_feilv' in request.data: - dianpu.shouyi_choucheng_feilv = request.data['shouyi_choucheng_feilv'] - - dianpu.save() # Django 会自动判断更新哪些字段 - return Response({'code': 0, 'msg': '店铺信息更新成功'}) - - except Dianpu.DoesNotExist: - return Response({'code': 404, 'msg': '店铺不存在'}) - except Exception as e: - logger.exception("修改店铺失败") - return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) - - def _add_dianpu(self, request, user_perms): - """添加店铺,需要权限 999b""" - if '999b' not in user_perms: - return Response({'code': 403, 'msg': '无权限添加店铺(999b)'}) - - yonghu_id = request.data.get('yonghu_id') - zhanghao = request.data.get('zhanghao') - mima = request.data.get('mima') - dianpu_mingcheng = request.data.get('dianpu_mingcheng') - - if not all([yonghu_id, zhanghao, mima, dianpu_mingcheng]): - return Response({'code': 400, 'msg': '必填字段缺失(用户ID、账号、密码、店铺名称)'}) - - # 检查用户是否存在 - if not UserMain.objects.filter(yonghuid=yonghu_id).exists(): - return Response({'code': 404, 'msg': '用户不存在'}) - - # 检查凭证是否已存在 - if YonghuPingzheng.objects.filter(zhanghao=zhanghao).exists(): - return Response({'code': 400, 'msg': '登录账号已存在'}) - if YonghuPingzheng.objects.filter(yonghu=yonghu_id).exists(): - return Response({'code': 400, 'msg': '该用户ID已有店铺凭证'}) - - try: - with transaction.atomic(): - pingzheng = YonghuPingzheng.objects.create( - yonghu=yonghu_id, - zhanghao=zhanghao, - mima=mima, - is_active=True - ) - dianpu = Dianpu.objects.create( - pingzheng=pingzheng, - dianpu_mingcheng=dianpu_mingcheng, - lianxi_dianhua=request.data.get('lianxi_dianhua', ''), - weixinhao=request.data.get('weixinhao', ''), - kaiqi_shouyi_choucheng=request.data.get('kaiqi_shouyi_choucheng', False), - shouyi_choucheng_feilv=request.data.get('shouyi_choucheng_feilv', 0), - kaiqi_meiri_xiane=request.data.get('kaiqi_meiri_xiane', False), - meiri_tixian_xiane=request.data.get('meiri_tixian_xiane', 0), - ) - return Response({'code': 0, 'msg': '店铺创建成功', 'dianpu_id': dianpu.id}) - except IntegrityError as e: - logger.error("创建店铺违反唯一性约束: %s", e) - return Response({'code': 400, 'msg': '数据冲突,请检查账号或用户ID'}) - except Exception as e: - logger.exception("创建店铺失败") - return Response({'code': 500, 'msg': '创建失败,请稍后重试'}) - - def _update_default(self, request, user_perms): - """修改默认配置,按修改内容校验权限""" - feilv = request.data.get('moren_choucheng_feilv') - xiane = request.data.get('moren_tixian_xiane') - modify_commission = feilv is not None - modify_withdraw = xiane is not None - - if modify_commission and '999d' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改默认抽成费率(999d)'}) - if modify_withdraw and '999c' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'}) - - try: - config, _ = DianpuMorenPeizhi.objects.get_or_create(id=1) - if modify_commission: - config.moren_choucheng_feilv = feilv - if modify_withdraw: - config.moren_tixian_xiane = xiane - config.save(update_fields=(['moren_choucheng_feilv'] if modify_commission else []) + - (['moren_tixian_xiane'] if modify_withdraw else [])) - return Response({'code': 0, 'msg': '默认配置更新成功'}) - except Exception as e: - logger.exception("更新默认配置失败") - return Response({'code': 500, 'msg': '更新失败'}) - - - - - -# houtai/views.py (完整新增接口,可直接追加到文件末尾) -import logging -from django.db import models # Q 查询必须导入 -from django.core.paginator import Paginator -from django.db import transaction -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from shangpin.models import ShangpinLeixing # 公共商品类型 -from shangdian.models import ( - Dianpu, ShangpinLeixingDianpu, - DianpuShangpinShenheShezhi, YonghuPingzheng -) -from .utils import verify_kefu_permission # 已提供的公共身份校验方法 - -logger = logging.getLogger(__name__) - -# 店铺商品管理所需权限组(拥有任意一个即可访问列表类接口) -SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd'] - - -# ==================== 1. 公共商品类型 + 审核模式 ==================== -class ShopPublicTypeAndAuditView(APIView): - """ - 返回:①正常状态的公共商品类型列表 ②全局商品审核模式开关 - 前端请求路径:/houtai/htdphqsplx - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # ---------- 身份与权限校验 ---------- - username = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) - - # 拥有 1199ab/1199abc/1199abd 任一个即可访问 - if not set(permissions).intersection(SHOP_PRODUCT_PERMS): - return Response({'code': 403, 'msg': '无权访问店铺商品管理功能'}) - - # ---------- 查询公共商品类型(仅正常审核状态的) ---------- - try: - # shenhezhuangtai = 1 表示正常 (根据模型默认值) - types_qs = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values( - 'id', 'jieshao', 'tupian_url', 'yaoqiuleixing', 'huiyuan_id', 'yongjin', 'is_cross_enabled' - ) - type_list = [] - for t in types_qs: - type_list.append({ - 'id': t['id'], - 'jieshao': t['jieshao'] or '', - 'tupian_url': t['tupian_url'] or '', - 'yaoqiuleixing': t['yaoqiuleixing'], - 'huiyuan_id': t['huiyuan_id'], - 'yongjin': t['yongjin'], - 'is_cross_enabled': t['is_cross_enabled'], - }) - except Exception as e: - logger.exception("查询公共商品类型失败") - return Response({'code': 500, 'msg': '服务器错误,请稍后重试'}) - - # ---------- 获取审核模式配置 ---------- - audit_config = DianpuShangpinShenheShezhi.objects.filter(id=1).first() - kaiqi_shenhe = audit_config.kaiqi_shenhe if audit_config else False - - return Response({ - 'code': 0, - 'data': { - 'public_types': type_list, - 'kaiqi_shenhe': kaiqi_shenhe - } - }) - - -# ==================== 2. 店铺列表(支持搜索、筛选、分页) ==================== -class ShopListForProductView(APIView): - """ - 返回店铺列表,用于商品管理页面选择店铺。 - 支持:关键词搜索(店铺ID/名称/用户ID)、店铺状态筛选、分页。 - 前端请求路径:/houtai/htdphqdpys - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败'}) - if not set(permissions).intersection(SHOP_PRODUCT_PERMS): - return Response({'code': 403, 'msg': '无权访问店铺列表'}) - - # ---------- 安全解析分页参数 ---------- - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 10)) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '分页参数格式错误'}) - - # 搜索关键词(字符串,ORM 自动防注入) - keyword = str(request.data.get('keyword', '')).strip() - # 店铺状态筛选(1正常,0封禁,不传则全部) - zhuangtai = request.data.get('zhuangtai') - - # ---------- 构建查询集(关联凭证表获取用户ID) ---------- - shops = Dianpu.objects.select_related('pingzheng').all() - - # 状态筛选:只接受 0 或 1 - if zhuangtai is not None: - try: - zhuangtai = int(zhuangtai) - if zhuangtai in [0, 1]: - shops = shops.filter(zhuangtai=zhuangtai) - else: - return Response({'code': 400, 'msg': '状态参数只能为0或1'}) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '状态参数必须为整数'}) - - # 关键词搜索(店铺ID/名称/用户ID) - if keyword: - shops = shops.filter( - models.Q(id__icontains=keyword) | - models.Q(dianpu_mingcheng__icontains=keyword) | - models.Q(pingzheng__yonghu__icontains=keyword) - ) - - # ---------- 分页 ---------- - paginator = Paginator(shops, page_size) - page_obj = paginator.get_page(page) - - shop_list = [] - for shop in page_obj: - shop_list.append({ - 'id': shop.id, - 'dianpu_mingcheng': shop.dianpu_mingcheng, - 'zhuangtai': shop.zhuangtai, - 'yonghu_id': shop.pingzheng.yonghu if shop.pingzheng else '', - 'lianxi_dianhua': shop.lianxi_dianhua or '', - 'weixinhao': shop.weixinhao or '', - }) - - return Response({ - 'code': 0, - 'data': { - 'list': shop_list, - 'total': paginator.count - } - }) - - -# ==================== 3. 店铺商品类型映射列表(含筛选、待审核计数) ==================== -class ShopProductTypeMappingView(APIView): - """ - 返回店铺商品类型映射(ShangpinLeixingDianpu)列表。 - 支持:店铺ID、公共类型ID、上架/封禁/审核状态筛选,关键字搜索(介绍/ID/店铺名/公共类型名)。 - 每次请求同时返回“待审核总数”(shenhe_zhuangtai=False 的记录数)。 - 前端请求路径:/houtai/htdphqyssplx - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败'}) - if not set(permissions).intersection(SHOP_PRODUCT_PERMS): - return Response({'code': 403, 'msg': '无权访问商品类型映射'}) - - # ---------- 安全解析参数 ---------- - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 10)) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '分页参数格式错误'}) - - keyword = str(request.data.get('keyword', '')).strip() - shop_id = request.data.get('shop_id') - public_type_id = request.data.get('public_type_id') - - # 布尔型筛选字段处理:None 表示不筛选,否则转为布尔值 - def parse_bool(val): - if val is None: - return None - return bool(val) - - shangjia = parse_bool(request.data.get('shangjia_zhuangtai')) - fengjin = parse_bool(request.data.get('fengjin_zhuangtai')) - shenhe = parse_bool(request.data.get('shenhe_zhuangtai')) - - # ---------- 查询(连表店铺和公共类型) ---------- - mappings = ShangpinLeixingDianpu.objects.select_related('dianpu', 'gonggong_leixing').all() - - # 筛选条件:均为参数化,完全防注入 - if shop_id is not None: - mappings = mappings.filter(dianpu_id=int(shop_id)) - if public_type_id is not None: - mappings = mappings.filter(gonggong_leixing_id=int(public_type_id)) - if shangjia is not None: - mappings = mappings.filter(shangjia_zhuangtai=shangjia) - if fengjin is not None: - mappings = mappings.filter(fengjin_zhuangtai=fengjin) - if shenhe is not None: - mappings = mappings.filter(shenhe_zhuangtai=shenhe) - - # 关键字搜索(介绍、ID、店铺名、公共类型名) - if keyword: - mappings = mappings.filter( - models.Q(jieshao__icontains=keyword) | - models.Q(id__icontains=keyword) | - models.Q(dianpu__dianpu_mingcheng__icontains=keyword) | - models.Q(gonggong_leixing__jieshao__icontains=keyword) - ) - - # ---------- 待审核总数(不受分页影响) ---------- - pending_total = mappings.filter(shenhe_zhuangtai=False).count() - - # ---------- 分页 ---------- - paginator = Paginator(mappings, page_size) - page_obj = paginator.get_page(page) - - mapping_list = [] - for m in page_obj: - mapping_list.append({ - 'id': m.id, - 'dianpu_id': m.dianpu_id, - 'dianpu_name': m.dianpu.dianpu_mingcheng if m.dianpu else '', - 'gonggong_leixing_id': m.gonggong_leixing_id, - 'gonggong_leixing_name': m.gonggong_leixing.jieshao if m.gonggong_leixing else '', - 'jieshao': m.jieshao, - 'tupian_url': m.tupian_url or '', - 'paixu': m.paixu, - 'shangjia_zhuangtai': m.shangjia_zhuangtai, - 'fengjin_zhuangtai': m.fengjin_zhuangtai, - 'shenhe_zhuangtai': m.shenhe_zhuangtai, - }) - - return Response({ - 'code': 0, - 'data': { - 'list': mapping_list, - 'total': paginator.count, - 'pending_total': pending_total - } - }) - - -# ==================== 统一修改接口(已修正事务) ==================== -class ShopProductModifyView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败'}) - - user_perms = set(permissions) - action = request.data.get('action') - - if action == 'update_audit_mode': - return self._update_audit_mode(request, user_perms) - elif action == 'update_mapping_status': - return self._update_mapping_status(request, user_perms) - else: - return Response({'code': 400, 'msg': '未知操作'}) - - @transaction.atomic # ✅ 事务修复点 - def _update_audit_mode(self, request, user_perms): - if '1199ab' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改审核模式'}) - - kaiqi = request.data.get('kaiqi_shenhe') - if kaiqi is None: - return Response({'code': 400, 'msg': '缺少参数 kaiqi_shenhe'}) - - try: - config, _ = DianpuShangpinShenheShezhi.objects.select_for_update().get_or_create(id=1) - config.kaiqi_shenhe = bool(kaiqi) - config.save(update_fields=['kaiqi_shenhe']) - return Response({'code': 0, 'msg': '审核模式已更新'}) - except Exception as e: - logger.exception("更新审核模式失败") - return Response({'code': 500, 'msg': '服务器错误'}) - - @transaction.atomic - def _update_mapping_status(self, request, user_perms): - mapping_id = request.data.get('mapping_id') - if not mapping_id: - return Response({'code': 400, 'msg': '缺少 mapping_id'}) - - try: - mapping = ShangpinLeixingDianpu.objects.select_for_update().get(id=int(mapping_id)) - except (ShangpinLeixingDianpu.DoesNotExist, ValueError): - return Response({'code': 404, 'msg': '映射不存在'}) - - fengjin = request.data.get('fengjin_zhuangtai') - shangjia = request.data.get('shangjia_zhuangtai') - shenhe = request.data.get('shenhe_zhuangtai') - - # 权限判断 - if fengjin is not None: - if fengjin: - if '1199abc' not in user_perms: - return Response({'code': 403, 'msg': '无权限封禁'}) - else: - if '1199abd' not in user_perms: - return Response({'code': 403, 'msg': '无权限解封'}) - mapping.fengjin_zhuangtai = bool(fengjin) - - if shangjia is not None: - if not shangjia: - if '1199abc' not in user_perms: - return Response({'code': 403, 'msg': '无权限下架'}) - else: - if '1199abd' not in user_perms: - return Response({'code': 403, 'msg': '无权限上架'}) - mapping.shangjia_zhuangtai = bool(shangjia) - - if shenhe is not None: - if shenhe: - if '1199abd' not in user_perms: - return Response({'code': 403, 'msg': '无权限审核通过'}) - mapping.shenhe_zhuangtai = bool(shenhe) - - mapping.save() - return Response({'code': 0, 'msg': '修改成功'}) - - - - - - -# 商品管理相关权限组(拥有任意一个即可查看列表) -PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd', '1199abe'] - - -class ProductListView(APIView): - """ - 商品列表接口,支持多条件筛选与分页。 - 请求路径: /houtai/hqhtdpsplx - 权限要求: 1199ab / 1199abc / 1199abd / 1199abe 任一即可 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # ----- 身份与权限验证 ----- - username = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败'}) - - if not set(permissions).intersection(PRODUCT_PERMS): - return Response({'code': 403, 'msg': '无商品管理权限'}) - - # ----- 安全解析分页与筛选参数 ----- - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 15)) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '分页参数格式错误'}) - - keyword = str(request.data.get('keyword', '')).strip() - shop_id = request.data.get('shop_id') # 店铺ID (可选) - shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型映射ID (可选) - public_type_id = request.data.get('public_type_id') # 公共商品类型ID (可选) - - # 布尔型状态筛选 - def parse_bool(val): - if val is None: - return None - return bool(val) - - shenhe = parse_bool(request.data.get('shenhe_zhuangtai')) - shangjia = parse_bool(request.data.get('shangjia_zhuangtai')) - fengjin = parse_bool(request.data.get('fengjin_zhuangtai')) - - # ----- 构建查询(预加载店铺和店铺商品类型) ----- - products = Shangpin.objects.select_related('dianpu', 'dianpu_leixing').all() - - # 按店铺筛选 - if shop_id is not None: - try: - products = products.filter(dianpu_id=int(shop_id)) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': 'shop_id 格式错误'}) - - # 按店铺商品类型映射筛选 - if shop_product_type_id is not None: - try: - products = products.filter(dianpu_leixing_id=int(shop_product_type_id)) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'}) - - # 按公共商品类型筛选(leixing_id 存储的是公共类型的ID) - if public_type_id is not None: - try: - products = products.filter(leixing_id=int(public_type_id)) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': 'public_type_id 格式错误'}) - - # 平台审核状态筛选 (BooleanField) - if shenhe is not None: - products = products.filter(shenhe_zhuangtai=shenhe) - - # 上架状态筛选 - if shangjia is not None: - products = products.filter(shangjia_zhuangtai=shangjia) - - # 封禁状态筛选 - if fengjin is not None: - products = products.filter(fengjin_zhuangtai=fengjin) - - # 关键词搜索:商品ID或商品标题(搜索具有模糊性,使用icontains) - if keyword: - # 尝试纯数字可能是商品ID,使用精确匹配与标题模糊搜索 - products = products.filter( - models.Q(id__icontains=keyword) | - models.Q(biaoqian__icontains=keyword) - ) - - # ----- 获取待审核总数(平台审核未通过的数量) ----- - pending_total = products.filter(shenhe_zhuangtai=False).count() - - # ----- 分页 ----- - paginator = Paginator(products, page_size) - page_obj = paginator.get_page(page) - - # ----- 构造返回数据(批量获取公共类型名称,避免N+1) ----- - # 提取所有公共商品类型ID - type_ids = {p.leixing_id for p in page_obj if p.leixing_id is not None} - public_type_map = {} - if type_ids: - public_types = ShangpinLeixing.objects.filter(id__in=type_ids).values('id', 'jieshao') - public_type_map = {t['id']: t['jieshao'] for t in public_types} - - product_list = [] - for p in page_obj: - product_list.append({ - 'id': p.id, - 'biaoqian': p.biaoqian or '', - 'jiage': p.jiage, - 'kucun': p.kucun, - 'leixing_id': p.leixing_id, - 'public_type_name': public_type_map.get(p.leixing_id, ''), - 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, - 'duiwai_xiaoliang': p.duiwai_xiaoliang, - 'jieshao': p.jieshao or '', - 'xiadan_xuzhi': p.xiadan_xuzhi or '', - 'guize_tupian': p.guize_tupian or '', - 'yaoqiuleixing': p.yaoqiuleixing, - 'huiyuan_id': p.huiyuan_id or '', - 'yongjin': p.yongjin, - 'kaioi_ewai_dashou_fencheng': p.kaioi_ewai_dashou_fencheng, - 'ewai_dashou_fencheng': p.ewai_dashou_fencheng, - 'create_time': p.create_time.isoformat() if p.create_time else '', - 'update_time': p.update_time.isoformat() if p.update_time else '', - 'paixu': p.paixu, - 'dianpu_id': p.dianpu_id, - 'dianpu_name': p.dianpu.dianpu_mingcheng if p.dianpu else '', - 'dianpu_leixing_id': p.dianpu_leixing_id, - 'dianpu_leixing_name': p.dianpu_leixing.jieshao if p.dianpu_leixing else '', - 'shangjia_zhuangtai': p.shangjia_zhuangtai, - 'fengjin_zhuangtai': p.fengjin_zhuangtai, - 'shenhe_zhuangtai': p.shenhe_zhuangtai, - }) - - return Response({ - 'code': 0, - 'data': { - 'list': product_list, - 'total': paginator.count, - 'pending_total': pending_total, # 待审核商品总数 - } - }) - - -class ProductModifyView(APIView): - """ - 商品修改接口,根据修改内容细粒度鉴权。 - 请求路径: /houtai/htxgdpspsj - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败'}) - - user_perms = set(permissions) - action = request.data.get('action', '') - - if action != 'update_product': - return Response({'code': 400, 'msg': '未知操作'}) - - return self._update_product(request, user_perms) - - @transaction.atomic - def _update_product(self, request, user_perms): - """执行商品修改,严格校验权限""" - product_id = request.data.get('product_id') - if not product_id: - return Response({'code': 400, 'msg': '缺少商品ID'}) - - try: - # 行级锁防止并发修改 - product = Shangpin.objects.select_for_update().get(id=int(product_id)) - except (Shangpin.DoesNotExist, ValueError): - return Response({'code': 404, 'msg': '商品不存在'}) - - # 需要修改的字段 - fields_to_update = {} - # 将要修改的状态标志用于后续权限判断 - status_changes = { - 'shangjia': request.data.get('shangjia_zhuangtai'), - 'fengjin': request.data.get('fengjin_zhuangtai'), - 'shenhe': request.data.get('shenhe_zhuangtai'), - } - - # ---------- 权限分类检查 ---------- - # 1. 封禁/下架操作 (消极操作) -> 1199abc - if any(status_changes.get(k) is not None and status_changes[k] == False for k in ['shangjia', 'shenhe']): - if '1199abc' not in user_perms: - return Response({'code': 403, 'msg': '无权限执行下架/封禁/驳回审核操作(1199abc)'}) - if status_changes.get('fengjin') is not None and status_changes['fengjin'] == True: - if '1199abc' not in user_perms: - return Response({'code': 403, 'msg': '无权限执行封禁操作(1199abc)'}) - - # 2. 解封/上架/过审 (积极操作) -> 1199abd - if any(status_changes.get(k) is not None and status_changes[k] == True for k in ['shangjia', 'shenhe']): - if '1199abd' not in user_perms: - return Response({'code': 403, 'msg': '无权限执行上架/解封/审核通过操作(1199abd)'}) - if status_changes.get('fengjin') is not None and status_changes['fengjin'] == False: - if '1199abd' not in user_perms: - return Response({'code': 403, 'msg': '无权限执行解封操作(1199abd)'}) - - # 3. 修改其他数据(非状态字段)-> 1199abe - # 除了上架、封禁、审核之外的字段都属于“其他数据” - other_fields = [k for k in request.data.keys() if k not in ['action', 'product_id', 'username', - 'shangjia_zhuangtai', 'fengjin_zhuangtai', - 'shenhe_zhuangtai']] - if other_fields and '1199abe' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改商品基本信息(1199abe)'}) - - # ---------- 应用修改 ---------- - # 允许修改的字段白名单(防止注入) - allowed_fields = [ - 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'duiwai_xiaoliang', - 'jieshao', 'xiadan_xuzhi', 'yaoqiuleixing', 'yongjin', - 'kaioi_ewai_dashou_fencheng', 'ewai_dashou_fencheng', - 'shangjia_zhuangtai', 'fengjin_zhuangtai', 'shenhe_zhuangtai' - ] - - for field in allowed_fields: - value = request.data.get(field) - if value is not None: - # 简单类型校验(根据字段类型) - if field in ['jiage', 'kucun', 'duiwai_xiaoliang', 'yongjin', 'ewai_dashou_fencheng', 'paixu']: - try: - value = float(value) if 'jiage' in field or 'yongjin' in field or 'ewai' in field else int(value) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': f'字段 {field} 值格式错误'}) - setattr(product, field, value) - - product.save(update_fields=allowed_fields) - return Response({'code': 0, 'msg': '商品修改成功'}) - - - - -# 与前端约定的聊天权限码 -CHAT_PERM_CODES = ['abca1', 'baac2', 'cabc3', 'cb3a2', 'bcaa4'] - -class KefuChatPermissionsView(APIView): - """ - 获取客服聊天权限及GoEasy配置 - POST /houtai/kfhqltqx - Body: { "phone": "客服手机号" } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 从前端获取传递的账号,用于防越权校验 - username_from_frontend = request.data.get('phone', None) - - # 2. 调用公共验证方法。 - # 返回 (kefu_obj, permissions) 或 (None, Response) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - - # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值(错误Response) - if kefu_obj is None: - return permissions - - # ---------- 验证通过,permissions 已经是权限码列表 ---------- - # 4. 检查是否拥有至少一个聊天权限 - has_chat_perm = any(p in CHAT_PERM_CODES for p in permissions) - - # 5. 若有聊天权限,查询我方俱乐部的 chat_api_key - goeasy_appkey = '' - if has_chat_perm: - config = ClubConfig.objects.filter(is_self=1).first() - if config: - goeasy_appkey = config.chat_api_key or '' - - # 6. 返回数据 - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'permissions': permissions, - 'goeasy_appkey': goeasy_appkey - } - }) - - - - -from rest_framework.permissions import AllowAny - - - -from dingdan.models import Dingdan, Fadan, CrossPlatformOrderData # 按实际路径 -class FineApplyView(APIView): - """ - 客服申请罚款接口(金额罚款,非积分) - 路径:POST /houtai/kffkdssq - 参数: - username: 客服手机号 - dingdan_id: 订单ID - chufaliyou: 罚款原因 - fakuanjine: 罚款金额(元) - yingxiang_qiangdan: 1影响抢单 / 0不影响抢单 - shenqingren_shenfen: 操作人身份 (1客服/2售后/3管理员等) - 权限:JWT + 权限码99933abs - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 提取参数 - username = request.data.get('username', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - chufaliyou = request.data.get('chufaliyou', '').strip() - fakuanjine = request.data.get('fakuanjine', 0) - yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) - shenqingren_shenfen = request.data.get('shenqingren_shenfen', 2) - - if not all([username, dingdan_id, chufaliyou, fakuanjine]): - return Response({'code': 400, 'msg': '参数不完整'}) - - try: - # 2. 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '99933abs' not in permissions: - return Response({'code': 403, 'msg': '您无罚款权限'}) - - # 3. 获取订单(无需 select_for_update,因不修改订单) - try: - order = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}) - - dashou_id = order.jiedan_dashou_id - if not dashou_id: - return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'}) - - # 4. 重复罚单检查 - if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists(): - return Response({'code': 400, 'msg': '该打手已被罚款过'}) - - # 5. 跨平台相关字段 - partner_order_id = order.partner_order_id or '' - partner_club_id = order.partner_club_id or '' - dispatch_type = order.dispatch_type or 0 - - # 本地创建罚单的可复用函数 - def create_local_fadan(): - Fadan.objects.create( - beichufa_id=dashou_id, - shenqing_chufa=username, - shenfen=1, # 1 代表打手 - chufaliyou=chufaliyou, - fakuanjine=fakuanjine, - guanliandingdan_id=dingdan_id, - zhuangtai=1, # 待缴纳 - yingxiang_qiangdan=yingxiang_qiangdan, - shenqingren_shenfen=shenqingren_shenfen, - partner_club_id='', - ) - - # 6. 判断:没有对方订单ID,或对方派单 → 本地处罚 - if not partner_order_id or dispatch_type == 2: - with transaction.atomic(): - create_local_fadan() - logger.info(f"本地罚款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {fakuanjine}") - return Response({'code': 0, 'msg': '罚款已生成'}) - - # 7. 我方派单且有对方订单ID → 通知对方平台 - # 获取我方俱乐部ID - try: - my_club = ClubConfig.objects.filter(is_self=1).first() - if not my_club: - return Response({'code': 500, 'msg': '系统配置错误'}) - self_club_id = my_club.club_id - club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) - partner_domain = club_rel.partner_domain - except ObjectDoesNotExist: - return Response({'code': 500, 'msg': '对方平台信息未配置'}) - - url = partner_domain.rstrip('/') + '/houtai/kffkdssq_notify' - payload = { - 'order_id': partner_order_id, - 'club_id': self_club_id, - 'chufaliyou': chufaliyou, - 'fakuanjine': fakuanjine, - 'yingxiang_qiangdan': yingxiang_qiangdan, - 'shenqingren_shenfen': shenqingren_shenfen, - 'operator': username, - } - timeout = 3 - retries = 2 - success = False - for attempt in range(retries): - try: - resp = requests.post(url, json=payload, timeout=timeout) - if resp.status_code == 200 and resp.json().get('code') == 0: - success = True - break - except Exception: - continue - - if not success: - return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'}) - - # 通知成功后,本地也记录一笔(标记为跨平台) - with transaction.atomic(): - Fadan.objects.create( - beichufa_id=dashou_id, - shenqing_chufa=username, - shenfen=1, - chufaliyou=chufaliyou, - fakuanjine=fakuanjine, - guanliandingdan_id=dingdan_id, - zhuangtai=1, - yingxiang_qiangdan=yingxiang_qiangdan, - shenqingren_shenfen=shenqingren_shenfen, - partner_club_id=partner_club_id, - ) - logger.info(f"跨平台罚款通知成功: {dingdan_id}") - return Response({'code': 0, 'msg': '罚款已提交至对方平台'}) - - except Exception as e: - logger.error(f"罚款申请异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - - - -class PartnerFineNotifyView(APIView): - """ - 接收对方平台罚款通知(无认证) - 路径:POST /houtai/kffkdssq_notify - 参数: - order_id: 对方平台的订单ID(需通过这个找到我方订单) - club_id: 对方俱乐部ID - chufaliyou: 罚款原因 - fakuanjine: 罚款金额 - yingxiang_qiangdan: 1影响抢单 / 0不影响 - shenqingren_shenfen: 申请处罚人身份 - operator: 操作人标识 - """ - authentication_classes = [] - permission_classes = [AllowAny] - parser_classes = [JSONParser] - - def post(self, request): - # token = request.data.get('token') - # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): - # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200) - try: - order_id = request.data.get('order_id', '').strip() # 对方平台订单ID - club_id = request.data.get('club_id', '').strip() - chufaliyou = request.data.get('chufaliyou', '').strip() - fakuanjine = request.data.get('fakuanjine', 0) - yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) - shenqingren_shenfen = request.data.get('shenqingren_shenfen', 2) - operator = request.data.get('operator', '').strip() - - if not all([order_id, chufaliyou]): - return Response({'code': 400, 'msg': '参数不完整'}) - - # 通过对方订单ID查找我方订单 - try: - cross = CrossPlatformOrderData.objects.get(partner_order_id=order_id) - my_dingdan_id = cross.dingdan_id - order = Dingdan.objects.get(dingdan_id=my_dingdan_id) - dashou_id = order.jiedan_dashou_id - except CrossPlatformOrderData.DoesNotExist: - return Response({'code': 404, 'msg': '未找到关联订单'}) - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '我方订单不存在'}) - - if not dashou_id: - return Response({'code': 400, 'msg': '订单无接单打手'}) - - # 重复检查 - if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=my_dingdan_id).exists(): - return Response({'code': 0, 'msg': '已存在罚单'}) - - # 创建罚单 - Fadan.objects.create( - beichufa_id=dashou_id, - shenqing_chufa=operator, - shenfen=1, - chufaliyou=chufaliyou, - fakuanjine=fakuanjine, - guanliandingdan_id=my_dingdan_id, - zhuangtai=1, - yingxiang_qiangdan=yingxiang_qiangdan, - shenqingren_shenfen=shenqingren_shenfen, - partner_club_id=club_id, - ) - logger.info(f"接收对方罚款成功: 订单 {my_dingdan_id}, 打手 {dashou_id}") - return Response({'code': 0, 'msg': 'success'}) - - except Exception as e: - logger.error(f"接收罚款通知异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - - - - - -from dingdan.models import Fadan, FadanShensuTupian -# 权限码到身份的映射 -PERMISSION_TO_SHENFEN = { - '66693a': 1, # 打手 - '66693b': 3, # 商家 - '66693c': 2, # 管事 - '66694c': 4, # 组长 -} - -# 身份对应的用户扩展关系名 -SHENFEN_PROFILE_MAP = { - 1: 'dashou_profile', - 2: 'guanshi_profile', - 3: 'shop_profile', - 4: 'zuzhang_profile', -} - - -def check_fadan_permission(permissions, shenfen): - """检查是否有处理指定身份罚单的权限""" - perm_code = {1: '66693a', 2: '66693c', 3: '66693b', 4: '66694c'}.get(shenfen) - return perm_code in permissions - - -class FaKuanTongJiView(APIView): - """ - 罚款统计接口 - POST /houtai/htglyhqcfsltj - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '参数不完整'}) - - try: - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 至少有一个罚款管理权限 - if not any(p in permissions for p in PERMISSION_TO_SHENFEN.keys()): - return Response({'code': 403, 'msg': '无权限查看罚款统计'}) - - stats = Fadan.objects.aggregate( - total=Count('id'), - daijiaona=Count('id', filter=Q(zhuangtai=1)), - shensuzhong=Count('id', filter=Q(zhuangtai=3)), - yijiaona=Count('id', filter=Q(zhuangtai=2)), - yibohui=Count('id', filter=Q(zhuangtai=4)), - ) - data = {k: v or 0 for k, v in stats.items()} - return Response({'code': 0, 'msg': '成功', 'data': data}) - except Exception as e: - logger.error(f"罚款统计异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '系统繁忙'}) - - -class FaKuanLieBiaoView(APIView): - """ - 罚款列表接口 - POST /houtai/hthqfklb - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '参数不完整'}) - - try: - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if not any(p in permissions for p in PERMISSION_TO_SHENFEN.keys()): - return Response({'code': 403, 'msg': '无权限查看罚款列表'}) - - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - if page_size > 100: - page_size = 100 - - qs = Fadan.objects.all() - - # 状态筛选 - zhuangtai = request.data.get('zhuangtai') - if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]: - qs = qs.filter(zhuangtai=int(zhuangtai)) - - # 被处罚人ID - beichufa_id = request.data.get('beichufa_id', '').strip() - if beichufa_id: - qs = qs.filter(beichufa_id=beichufa_id) - - # 被处罚人身份 - beichufa_shenfen = request.data.get('beichufa_shenfen') - if beichufa_shenfen is not None and int(beichufa_shenfen) in [1, 2, 3, 4]: - qs = qs.filter(shenfen=int(beichufa_shenfen)) - - # 模糊搜索(订单ID、被罚人ID、申请人ID) - sousuo = request.data.get('sousuo', '').strip() - if sousuo: - qs = qs.filter( - Q(guanliandingdan_id__icontains=sousuo) | - Q(beichufa_id__icontains=sousuo) | - Q(shenqing_chufa__icontains=sousuo) - ) - - qs = qs.order_by('-create_time') - paginator = Paginator(qs, page_size) - try: - page_obj = paginator.page(page) - except Exception: - return Response({ - 'code': 0, 'msg': '成功', - 'data': {'list': [], 'total': 0, 'page': page, 'page_size': page_size} - }) - - records = list(page_obj) - fadan_ids = [r.id for r in records] - - # 批量获取图片并分组 - zhengju_map = {} - shensu_map = {} - if fadan_ids: - tupians = FadanShensuTupian.objects.filter( - fadan_id__in=fadan_ids - ).values('fadan_id', 'tupian_url', 'yongtu') - for t in tupians: - fid = t['fadan_id'] - url = t['tupian_url'] - if t['yongtu'] == 1: - zhengju_map.setdefault(fid, []).append(url) - else: - shensu_map.setdefault(fid, []).append(url) - - # 组装列表数据 - results = [] - for r in records: - results.append({ - 'id': r.id, - 'beichufa_id': r.beichufa_id, - 'shenfen': r.shenfen, - 'shenfen_text': dict(Fadan._meta.get_field('shenfen').choices).get(r.shenfen, ''), - 'shenqing_chufa': r.shenqing_chufa or '', - 'shenqingren_shenfen': r.shenqingren_shenfen, - 'chufaliyou': r.chufaliyou or '', - 'fakuanjine': str(r.fakuanjine), - 'guanliandingdan_id': r.guanliandingdan_id or '', - 'zhuangtai': r.zhuangtai, - 'yingxiang_qiangdan': r.yingxiang_qiangdan, - 'shensuliyou': r.shensuliyou or '', - 'bohuiliyou': r.bohuiliyou or '', - 'chulizhe': r.chulizhe or '', - 'chulizhe_shenfen': r.chulizhe_shenfen, - 'zhengju_tupian': zhengju_map.get(r.id, []), - 'shensu_tupian': shensu_map.get(r.id, []), - 'create_time': r.create_time.strftime('%Y-%m-%d %H:%M:%S') if r.create_time else '', - 'update_time': r.update_time.strftime('%Y-%m-%d %H:%M:%S') if r.update_time else '', - }) - - return Response({ - 'code': 0, 'msg': '成功', - 'data': { - 'list': results, - 'total': paginator.count, - 'page': page, - 'page_size': page_size, - } - }) - except Exception as e: - logger.error(f"罚款列表异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '系统繁忙'}) - - -class FaKuanChuLiView(APIView): - """ - 处理罚款申诉(同意/拒绝) - POST /houtai/glyclfk - """ - permission_classes = [IsAuthenticated] - parser_classes = [MultiPartParser, FormParser, JSONParser] - - def post(self, request): - try: - username = request.data.get('username', '').strip() - fadan_id = request.data.get('fadan_id') - action = request.data.get('action', '').strip() - chuli_liyou = request.data.get('chuli_liyou', '').strip() - - if not all([username, fadan_id, action]): - return Response({'code': 400, 'msg': '参数不完整'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - with transaction.atomic(): - try: - fadan = Fadan.objects.select_for_update().get(id=fadan_id) - except Fadan.DoesNotExist: - return Response({'code': 404, 'msg': '罚单不存在'}) - - if fadan.zhuangtai != 3: - return Response({'code': 400, 'msg': '当前状态不可处理'}) - - if not check_fadan_permission(permissions, fadan.shenfen): - return Response({'code': 403, 'msg': '无权限处理该类罚单'}) - - # 处理上传的图片 - uploaded_urls = [] - files = request.FILES.getlist('tupian') or [] - if not files: - single = request.FILES.get('tupian') - if single: - files = [single] - for file in files: - valid, msg = validate_image(file) - if not valid: - return Response({'code': 400, 'msg': msg}) - ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' - file_name = f"chufa_{fadan_id}_{int(time.time() * 1000)}.{ext}" - file_path = f"chufatupian/kechufatupian/{file_name}" - url = upload_to_oss(file, file_path) - if not url: - return Response({'code': 500, 'msg': '图片上传失败'}) - uploaded_urls.append(file_path) - - # 更新状态 - if action == 'agree': # 同意申诉 → 驳回处罚 - fadan.zhuangtai = 4 - fadan.bohuiliyou = chuli_liyou - elif action == 'reject': # 拒绝申诉 → 回到待缴纳 - fadan.zhuangtai = 1 - fadan.bohuiliyou = chuli_liyou - else: - return Response({'code': 400, 'msg': '无效的操作类型'}) - - # 记录处理者(使用 request.user 的 phone) - fadan.chulizhe = request.user.phone - fadan.chulizhe_shenfen = 2 # 这里假设处理者是售后身份,可根据实际业务获取 - fadan.save() - - # 保存图片(证据图片,用途1) - for relative_url in uploaded_urls: - FadanShensuTupian.objects.create( - fadan_id=fadan_id, - beichufa_id=fadan.beichufa_id, - tupian_url=relative_url, - yongtu=1 - ) - - logger.info(f"罚款处理成功: {fadan_id}, 操作: {action}") - return Response({'code': 0, 'msg': '处理成功'}) - - except Exception as e: - logger.error(f"罚款处理异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '系统繁忙'}) - - -class FaKuanChuangJianView(APIView): - """ - 管理员创建罚款 - POST /houtai/htfksc - """ - permission_classes = [IsAuthenticated] - parser_classes = [MultiPartParser, FormParser, JSONParser] - - def post(self, request): - try: - username = request.data.get('username', '').strip() - beichufa_id = request.data.get('beichufa_id', '').strip() - shenfen = request.data.get('shenfen') - chufaliyou = request.data.get('chufaliyou', '').strip() - fakuanjine = request.data.get('fakuanjine') - yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) - - if not all([username, beichufa_id, shenfen, chufaliyou, fakuanjine]): - return Response({'code': 400, 'msg': '参数不完整'}) - - try: - shenfen = int(shenfen) - fakuanjine = float(fakuanjine) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': '参数格式错误'}) - - if shenfen not in [1, 2, 3, 4]: - return Response({'code': 400, 'msg': '无效的被罚人身份'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - if not check_fadan_permission(permissions, shenfen): - return Response({'code': 403, 'msg': '无权限创建该类罚单'}) - - # 验证用户是否存在对应的扩展表 - try: - user = UserMain.objects.get(yonghuid=beichufa_id) - profile_attr = SHENFEN_PROFILE_MAP.get(shenfen) - if not profile_attr or not hasattr(user, profile_attr): - return Response({'code': 400, 'msg': '该用户不是所选身份'}) - except UserMain.DoesNotExist: - return Response({'code': 404, 'msg': '用户不存在'}) - - # 处理图片上传 - uploaded_urls = [] - files = request.FILES.getlist('tupian') or [] - if not files: - single = request.FILES.get('tupian') - if single: - files = [single] - for file in files: - valid, msg = validate_image(file) - if not valid: - return Response({'code': 400, 'msg': msg}) - ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' - file_name = f"chufa_{int(time.time() * 1000)}.{ext}" - file_path = f"chufatupian/kechufatupian/{file_name}" - url = upload_to_oss(file, file_path) - if not url: - return Response({'code': 500, 'msg': '图片上传失败'}) - uploaded_urls.append(file_path) - - with transaction.atomic(): - fadan = Fadan.objects.create( - beichufa_id=beichufa_id, - shenfen=shenfen, - shenqing_chufa=request.user.phone, # 申请人(客服)手机号 - shenqingren_shenfen=2, # 售后身份 - chufaliyou=chufaliyou, - fakuanjine=fakuanjine, - yingxiang_qiangdan=yingxiang_qiangdan, - zhuangtai=1, # 待缴纳 - ) - for relative_url in uploaded_urls: - FadanShensuTupian.objects.create( - fadan=fadan, - beichufa_id=beichufa_id, - tupian_url=relative_url, - yongtu=1 - ) - - logger.info(f"罚款创建成功: {fadan.id}") - return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': fadan.id}}) - - except Exception as e: - logger.error(f"创建罚款异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '系统繁忙'}) - - - - - -from dengji.models import Bankuai - - - -class HqbkxxView(APIView): - """ - 获取板块配置所需全部数据 - POST /houtai/hqbkxx - Body: { "phone": "管理员手机号" } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 从前端获取传递的账号,用于防越权校验 - username_from_frontend = request.data.get('phone', None) - - # 2. 调用公共验证方法 - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - - # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值 - if kefu_obj is None: - return permissions - - # ---------- 验证通过 ---------- - # 4. 检查是否有板块管理权限 - if 'bankuai' not in permissions: - return Response({'code': 403, 'msg': '无板块管理权限'}, status=403) - - # 5. 查询所有板块 - bankuai_list = [] - for bk in Bankuai.objects.all(): - bankuai_list.append({ - 'bankuai_id': bk.bankuai_id, - 'mingcheng': bk.mingcheng, - 'create_time': bk.create_time.strftime('%Y-%m-%d %H:%M:%S') if bk.create_time else '' - }) - - # 6. 查询所有商品类型 - shangpin_leixing_list = [] - for sp in ShangpinLeixing.objects.all(): - shangpin_leixing_list.append({ - 'id': sp.id, - 'jieshao': sp.jieshao, - 'tupian_url': sp.tupian_url or '', - 'bankuai_id': sp.bankuai_id - }) - - # 7. 查询所有会员 - huiyuan_list = [] - for hy in Huiyuan.objects.all(): - huiyuan_list.append({ - 'huiyuan_id': hy.huiyuan_id, - 'jieshao': hy.jieshao, - 'bankuai_id': hy.bankuai_id - }) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'bankuai': bankuai_list, - 'shangpin_leixing': shangpin_leixing_list, - 'huiyuan': huiyuan_list - } - }) - - -class BkxgView(APIView): - """ - 板块增删改统一接口 - POST /houtai/bkxg - Body: { - "phone": "管理员手机号", - "action": "create | update | delete | add_items | remove_items", - "bankuai_id": int, - "bankuai_name": str, - "item_type": "shangpin_leixing | huiyuan", - "item_ids": [1,2,3] - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 从前端获取传递的账号 - username_from_frontend = request.data.get('phone', None) - - # 2. 调用公共验证方法 - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - - # 3. 验证失败直接返回 - if kefu_obj is None: - return permissions - - # 4. 检查板块管理权限 - if 'bankuai' not in permissions: - return Response({'code': 403, 'msg': '无板块管理权限'}, status=403) - - # 5. 获取操作类型 - action = request.data.get('action') - if not action: - return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400) - - # ==================== 创建板块 ==================== - if action == 'create': - bankuai_name = request.data.get('bankuai_name', '').strip() - if not bankuai_name: - return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400) - if Bankuai.objects.filter(mingcheng=bankuai_name).exists(): - return Response({'code': 1, 'msg': '板块名称已存在'}, status=400) - - shangpin_ids = request.data.get('shangpin_ids', []) - huiyuan_ids = request.data.get('huiyuan_ids', []) - - with transaction.atomic(): - new_bk = Bankuai.objects.create(mingcheng=bankuai_name) - if shangpin_ids: - ShangpinLeixing.objects.filter( - id__in=shangpin_ids, - bankuai__isnull=True - ).update(bankuai=new_bk) - if huiyuan_ids: - Huiyuan.objects.filter( - huiyuan_id__in=huiyuan_ids, - bankuai__isnull=True - ).update(bankuai=new_bk) - - return Response({ - 'code': 0, - 'msg': '创建成功', - 'data': {'bankuai_id': new_bk.bankuai_id} - }) - - # ==================== 以下操作需要 bankuai_id ==================== - bankuai_id = request.data.get('bankuai_id') - if not bankuai_id: - return Response({'code': 1, 'msg': '缺少 bankuai_id'}, status=400) - - try: - bk = Bankuai.objects.get(bankuai_id=bankuai_id) - except Bankuai.DoesNotExist: - return Response({'code': 1, 'msg': '板块不存在'}, status=404) - - # ==================== 修改板块名称 ==================== - if action == 'update': - new_name = request.data.get('bankuai_name', '').strip() - if not new_name: - return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400) - if Bankuai.objects.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists(): - return Response({'code': 1, 'msg': '板块名称已存在'}, status=400) - bk.mingcheng = new_name - bk.save() - return Response({'code': 0, 'msg': '修改成功'}) - - # ==================== 删除板块 ==================== - elif action == 'delete': - with transaction.atomic(): - ShangpinLeixing.objects.filter(bankuai=bk).update(bankuai=None) - Huiyuan.objects.filter(bankuai=bk).update(bankuai=None) - bk.delete() - return Response({'code': 0, 'msg': '删除成功'}) - - # ==================== 添加绑定 ==================== - elif action == 'add_items': - item_type = request.data.get('item_type') - item_ids = request.data.get('item_ids', []) - if not item_type or not item_ids: - return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400) - - if item_type == 'shangpin_leixing': - ShangpinLeixing.objects.filter( - id__in=item_ids, - bankuai__isnull=True - ).update(bankuai=bk) - elif item_type == 'huiyuan': - Huiyuan.objects.filter( - huiyuan_id__in=item_ids, - bankuai__isnull=True - ).update(bankuai=bk) - else: - return Response({'code': 1, 'msg': '无效的 item_type'}, status=400) - return Response({'code': 0, 'msg': '添加成功'}) - - # ==================== 移除绑定 ==================== - elif action == 'remove_items': - item_type = request.data.get('item_type') - item_ids = request.data.get('item_ids', []) - if not item_type or not item_ids: - return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400) - - if item_type == 'shangpin_leixing': - ShangpinLeixing.objects.filter( - id__in=item_ids, - bankuai=bk - ).update(bankuai=None) - elif item_type == 'huiyuan': - Huiyuan.objects.filter( - huiyuan_id__in=item_ids, - bankuai=bk - ).update(bankuai=None) - else: - return Response({'code': 1, 'msg': '无效的 item_type'}, status=400) - return Response({'code': 0, 'msg': '移除成功'}) - - else: - return Response({'code': 1, 'msg': '未知操作'}, status=400) - - - - - -from rest_framework.permissions import IsAuthenticated -from rest_framework.views import APIView -from rest_framework.response import Response -from django.db.models import Sum -from django.utils import timezone -from datetime import date, timedelta -from peizhi.models import ( - DailyIncomeStat, DailyPayoutStat -) -from shangpin.models import ( - Czjilu,Huiyuangoumai -) -from .utils import verify_kefu_permission - - - -class CaiwuView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request): - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - - if kefu_obj is None: - return permissions - - if 'caiwu' not in permissions: - return Response({'code': 403, 'msg': '无财务查看权限'}, status=403) - - today = date.today() - today_start = datetime.combine(today, datetime.min.time()) - today_end = today_start + timedelta(days=1) - - # 今日收入 - today_income = DailyIncomeStat.objects.filter(date=today).first() - today_income_amount = float(today_income.total_amount) if today_income else 0.00 - today_income_count = today_income.total_count if today_income else 0 - - # 今日支出 - today_payout = DailyPayoutStat.objects.filter(date=today).first() - today_payout_amount = float(today_payout.total_amount) if today_payout else 0.00 - today_payout_count = today_payout.total_count if today_payout else 0 - - # 今日订单(已付款及之后的状态) - today_orders = Dingdan.objects.filter( - create_time__gte=today_start, - create_time__lt=today_end, - zhuangtai__in=[1,2,3,4,5,6,7,8] - ) - today_order_count = today_orders.count() - today_order_amount = float(today_orders.aggregate(total=Sum('jine'))['total'] or 0.00) - - # 今日成交(zhuangtai=3) - today_completed = Dingdan.objects.filter( - create_time__gte=today_start, - create_time__lt=today_end, - zhuangtai=3 - ) - today_completed_count = today_completed.count() - today_completed_amount = float(today_completed.aggregate(total=Sum('jine'))['total'] or 0.00) - - # 今日退款(zhuangtai=5) - today_tuikuan = Dingdan.objects.filter( - create_time__gte=today_start, - create_time__lt=today_end, - zhuangtai=5 - ) - today_tuikuan_count = today_tuikuan.count() - today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('jine'))['total'] or 0.00) - - # 今日充值(会员充值,leixing=1, zhuangtai=3) - today_chongzhi = Czjilu.objects.filter( - create_time__gte=today_start, - create_time__lt=today_end, - leixing=1, - zhuangtai=3 - ) - today_chongzhi_count = today_chongzhi.count() - today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00) - - # 今日新增会员:从购买记录表统计今天创建的用户(去重) - new_huiyuan_user_ids = Huiyuangoumai.objects.filter( - create_time__gte=today_start, - create_time__lt=today_end - ).values_list('yonghu_id', flat=True).distinct() - today_new_huiyuan = len(new_huiyuan_user_ids) - - # 新增会员充值总额(这些用户今天的会员充值金额) - today_new_huiyuan_amount = 0.0 - if new_huiyuan_user_ids: - amount_sum = Czjilu.objects.filter( - create_time__gte=today_start, - create_time__lt=today_end, - yonghuid__in=new_huiyuan_user_ids, - leixing=1, - zhuangtai=3 - ).aggregate(total=Sum('jine')) - today_new_huiyuan_amount = float(amount_sum['total'] or 0.0) - - # 各角色可提现余额总和 - all_guanshi_yue = float(UserGuanshi.objects.aggregate(total=Sum('yue'))['total'] or 0.00) - all_dashou_yue = float(UserDashou.objects.aggregate(total=Sum('yue'))['total'] or 0.00) - all_zuzhang_yue = float(UserZuzhang.objects.aggregate(total=Sum('ketixian_jine'))['total'] or 0.00) - all_shangjia_yue = float(UserShangjia.objects.aggregate(total=Sum('yue'))['total'] or 0.00) - - # 平台总收支及利润 - total_income = float(DailyIncomeStat.objects.aggregate(total=Sum('total_amount'))['total'] or 0.00) - total_payout = float(DailyPayoutStat.objects.aggregate(total=Sum('total_amount'))['total'] or 0.00) - platform_profit = round(total_income - total_payout, 2) - - # 近30天每日收支明细 - daily_stats = [] - income_qs = DailyIncomeStat.objects.order_by('-date')[:30] - payout_dict = { - str(p.date): float(p.total_amount) - for p in DailyPayoutStat.objects.filter(date__gte=today - timedelta(days=30)) - } - for inc in income_qs: - d = str(inc.date) - daily_stats.append({ - 'date': d, - 'income': float(inc.total_amount), - 'income_count': inc.total_count, - 'payout': payout_dict.get(d, 0.00), - 'profit': round(float(inc.total_amount) - payout_dict.get(d, 0.00), 2) - }) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'today_income': round(today_income_amount, 2), - 'today_income_count': today_income_count, - 'today_payout': round(today_payout_amount, 2), - 'today_payout_count': today_payout_count, - 'today_order_count': today_order_count, - 'today_order_amount': round(today_order_amount, 2), - 'today_completed_count': today_completed_count, - 'today_completed_amount': round(today_completed_amount, 2), - 'today_tuikuan_count': today_tuikuan_count, - 'today_tuikuan_amount': round(today_tuikuan_amount, 2), - 'today_chongzhi_count': today_chongzhi_count, - 'today_chongzhi_amount': round(today_chongzhi_amount, 2), - 'today_new_huiyuan': today_new_huiyuan, - 'today_new_huiyuan_amount': round(today_new_huiyuan_amount, 2), - 'all_guanshi_yue': round(all_guanshi_yue, 2), - 'all_dashou_yue': round(all_dashou_yue, 2), - 'all_zuzhang_yue': round(all_zuzhang_yue, 2), - 'all_shangjia_yue': round(all_shangjia_yue, 2), - 'total_income': round(total_income, 2), - 'total_payout': round(total_payout, 2), - 'platform_profit': platform_profit, - 'daily_stats': daily_stats - } - }) - - - - - -class CwhybkhqView(APIView): - """ - 获取所有板块及对应会员列表 - POST /houtai/cwhybkhq - Body: { "phone": "管理员手机号" } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - - if 'huiyuancaiwu' not in permissions: - return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403) - - bankuai_list = [] - for bk in Bankuai.objects.all(): - members = Huiyuan.objects.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id') - bankuai_list.append({ - 'bankuai_id': bk.bankuai_id, - 'mingcheng': bk.mingcheng, - 'members': list(members) - }) - - return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list}) - except Exception as e: - logger.exception("CwhybkhqView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - -from django.db.models.functions import TruncDate, TruncMonth, TruncYear -from shangpin.models import Czjilu, Gsfenhong # 请根据实际路径调整 -class HybkjtsjView(APIView): - """ - 会员详细统计数据 - POST /houtai/hybkjtsj - Body: { - "phone": "管理员手机号", - "huiyuan_id": "会员ID", - "granularity": "day" | "month" | "year", - "year": 2026, - "month": 5, - "include_guanshi": true, - "include_zuzhang": true, - "summary_date": "2026-05-16" - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - - if 'huiyuancaiwu' not in permissions: - return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403) - - huiyuan_id = request.data.get('huiyuan_id') - granularity = request.data.get('granularity', 'day') - year = request.data.get('year') - month = request.data.get('month') - include_guanshi = request.data.get('include_guanshi', False) - include_zuzhang = request.data.get('include_zuzhang', False) - summary_date = request.data.get('summary_date') - - if not huiyuan_id: - return Response({'code': 1, 'msg': '缺少会员ID'}, status=400) - - # 🔧 直接使用 date.today() 避免模块名冲突 - from datetime import date, datetime, timedelta - now = date.today() - - # ---------- 构造时间范围 ---------- - if granularity == 'day': - if not year or not month: - return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) - start_date = date(year, month, 1) - if month == 12: - end_date = date(year + 1, 1, 1) - else: - end_date = date(year, month + 1, 1) - trunc_func = TruncDate('create_time') - if not summary_date: - summary_date = now.strftime('%Y-%m-%d') - summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() - elif granularity == 'month': - if not year: - return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncMonth('create_time') - if not summary_date: - summary_date = now.strftime('%Y-%m') - summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() - elif granularity == 'year': - year = year or now.year - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncYear('create_time') - if not summary_date: - summary_date = str(year) - summary_q = date(year, 1, 1) - else: - return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) - - # 生成 naive datetime(避免 USE_TZ=False 报错) - start_dt = datetime.combine(start_date, datetime.min.time()) - end_dt = datetime.combine(end_date, datetime.min.time()) - - # ---------- 充值过滤 ---------- - chongzhi_filter = { - 'huiyuan_id': huiyuan_id, # 会员ID筛选 - 'leixing': 1, - 'zhuangtai': 3, - 'create_time__gte': start_dt, - 'create_time__lt': end_dt - } - - # ---------- 分红过滤 ---------- - fenhong_filter = { - 'huiyuan_id': huiyuan_id, # 分红表会员ID筛选 - 'create_time__gte': start_dt, - 'create_time__lt': end_dt - } - - # 充值分组查询 - chongzhi_qs = Czjilu.objects.filter(**chongzhi_filter) \ - .annotate(period=trunc_func) \ - .values('period') \ - .annotate( - chongzhi_count=Count('id'), - chongzhi_amount=Sum('jine') - ).order_by('period') - - # 分红分组查询 - fenhong_qs = None - if include_guanshi or include_zuzhang: - fenhong_agg = {} - if include_guanshi: - fenhong_agg['guanshi_amount'] = Sum('fenhong') - if include_zuzhang: - fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong') - fenhong_qs = Gsfenhong.objects.filter(**fenhong_filter) \ - .annotate(period=trunc_func) \ - .values('period') \ - .annotate(**fenhong_agg) \ - .order_by('period') - - # 合并数据 - data_map = {} - for item in chongzhi_qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - data_map[period_str] = { - 'chongzhi_count': item['chongzhi_count'], - 'chongzhi_amount': float(item['chongzhi_amount'] or 0) - } - - if fenhong_qs: - for item in fenhong_qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - if period_str not in data_map: - data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0} - if include_guanshi: - data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0) - if include_zuzhang: - data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0) - - time_series = [] - for period_str, vals in data_map.items(): - guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0 - zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0 - vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2) - vals['date'] = period_str - time_series.append(vals) - time_series.sort(key=lambda x: x['date']) - - # ---------- 汇总数据 ---------- - summary_chongzhi_filter = chongzhi_filter.copy() - if granularity == 'day': - summary_chongzhi_filter['create_time__date'] = summary_q - elif granularity == 'month': - summary_chongzhi_filter['create_time__year'] = summary_q.year - summary_chongzhi_filter['create_time__month'] = summary_q.month - else: - summary_chongzhi_filter['create_time__year'] = summary_q.year - - summary_chongzhi = Czjilu.objects.filter(**summary_chongzhi_filter).aggregate( - total_count=Count('id'), - total_amount=Sum('jine') - ) - total_chongzhi_count = summary_chongzhi['total_count'] or 0 - total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0) - - total_guanshi = 0.0 - total_zuzhang = 0.0 - if include_guanshi or include_zuzhang: - summary_fenhong_filter = fenhong_filter.copy() - if granularity == 'day': - summary_fenhong_filter['create_time__date'] = summary_q - elif granularity == 'month': - summary_fenhong_filter['create_time__year'] = summary_q.year - summary_fenhong_filter['create_time__month'] = summary_q.month - else: - summary_fenhong_filter['create_time__year'] = summary_q.year - - fenhong_agg = {} - if include_guanshi: - fenhong_agg['guanshi_sum'] = Sum('fenhong') - if include_zuzhang: - fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong') - summary_fenhong = Gsfenhong.objects.filter(**summary_fenhong_filter).aggregate(**fenhong_agg) - total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0) - total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0) - - total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'time_series': time_series, - 'summary': { - 'total_chongzhi_count': total_chongzhi_count, - 'total_chongzhi_amount': total_chongzhi_amount, - 'total_guanshi_fenhong': total_guanshi, - 'total_zuzhang_fenhong': total_zuzhang, - 'total_shouyi': total_shouyi - } - } - }) - except Exception as e: - logger.exception("HybkjtsjView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - - -class SzxxView(APIView): - """ - 每日收支详细统计数据 - POST /houtai/szxx - Body: { - "phone": "管理员手机号", - "granularity": "day" | "month" | "year", - "year": 2026, // day/month 时必填 - "month": 5, // day 时必填 - "summary_date": "2026-05-16" // 可选,格式随颗粒度 - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 避免模块名冲突,局部导入 - from datetime import date, datetime - - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - - if 'caiwu' not in permissions: - return Response({'code': 403, 'msg': '无财务查看权限'}, status=403) - - granularity = request.data.get('granularity', 'day') - year = request.data.get('year') - month = request.data.get('month') - summary_date = request.data.get('summary_date') - - now = date.today() - - # ---------- 时间范围与分组字段 ---------- - if granularity == 'day': - if not year or not month: - return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) - # 分组字段 - group_field = 'day' - # 过滤条件 - income_filter = {'year': year, 'month': month} - payout_filter = {'year': year, 'month': month} - # 默认汇总日期 - if not summary_date: - summary_date = now.strftime('%Y-%m-%d') - summary_parts = summary_date.split('-') - summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1]), 'day': int(summary_parts[2])} - # 用于构造返回的日期字符串 - date_format = '%Y-%m-%d' - - elif granularity == 'month': - if not year: - return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) - group_field = 'month' - income_filter = {'year': year} - payout_filter = {'year': year} - if not summary_date: - summary_date = now.strftime('%Y-%m') - summary_parts = summary_date.split('-') - summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1])} - date_format = '%Y-%m' - - elif granularity == 'year': - group_field = 'year' - income_filter = {} - payout_filter = {} - if not summary_date: - summary_date = str(now.year) - summary_filter = {'year': int(summary_date)} - date_format = '%Y' - else: - return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) - - # ---------- 收入分组查询 ---------- - income_qs = DailyIncomeStat.objects.filter(**income_filter) \ - .values(group_field) \ - .annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) \ - .order_by(group_field) - - # ---------- 支出分组查询 ---------- - payout_qs = DailyPayoutStat.objects.filter(**payout_filter) \ - .values(group_field) \ - .annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) \ - .order_by(group_field) - - # ---------- 合并数据 ---------- - data_map = {} - for item in income_qs: - key = item[group_field] - data_map[key] = { - 'income': float(item['total_income'] or 0), - 'income_count': item['total_count'] or 0, - 'payout': 0.0, - 'payout_count': 0 - } - - for item in payout_qs: - key = item[group_field] - if key not in data_map: - data_map[key] = {'income': 0.0, 'income_count': 0, 'payout': 0.0, 'payout_count': 0} - data_map[key]['payout'] = float(item['total_payout'] or 0) - data_map[key]['payout_count'] = item['total_count'] or 0 - - time_series = [] - for key, vals in data_map.items(): - # 构造日期字符串 - if granularity == 'day': - date_str = f"{year}-{month:02d}-{key:02d}" - elif granularity == 'month': - date_str = f"{year}-{key:02d}" - else: - date_str = str(key) - profit = round(vals['income'] - vals['payout'], 2) - time_series.append({ - 'date': date_str, - 'income': vals['income'], - 'payout': vals['payout'], - 'profit': profit - }) - time_series.sort(key=lambda x: x['date']) - - # ---------- 汇总数据 ---------- - income_summary = DailyIncomeStat.objects.filter(**summary_filter).aggregate( - total_income=Sum('total_amount'), total_count=Sum('total_count') - ) - payout_summary = DailyPayoutStat.objects.filter(**summary_filter).aggregate( - total_payout=Sum('total_amount'), total_count=Sum('total_count') - ) - total_income = float(income_summary['total_income'] or 0) - total_payout = float(payout_summary['total_payout'] or 0) - total_profit = round(total_income - total_payout, 2) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'time_series': time_series, - 'summary': { - 'total_income': total_income, - 'total_income_count': income_summary['total_count'] or 0, - 'total_payout': total_payout, - 'total_payout_count': payout_summary['total_count'] or 0, - 'total_profit': total_profit - } - } - }) - - except Exception as e: - logger.exception("SzxxView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - - - - - -class CwddhqlxView(APIView): - """ - 获取订单类型(商品类型)用于筛选 - POST /houtai/cwddhqlx - Body: { "phone": "管理员手机号" } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - - if 'dingdancaiwu' not in permissions: - return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403) - - # 获取非审核状态(shenhezhuangtai=1)的商品类型,只返回ID和名称 - types = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values('id', 'jieshao') - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': list(types) - }) - except Exception as e: - logger.exception("CwddhqlxView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - -class CwhqjtddsjView(APIView): - """ - 订单详细统计数据(修正版) - POST /houtai/cwhqjtddsj - Body: { - "phone": "管理员手机号", - "granularity": "day" | "month" | "year", - "year": 2026, - "month": 5, - "type_id": 1, # 商品类型ID,0表示全部 - "order_source": 1, # 1=平台单,2=商家单,0=全部 - "summary_date": "2026-05-16" - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - if 'dingdancaiwu' not in permissions: - return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403) - - granularity = request.data.get('granularity', 'day') - year = request.data.get('year') - month = request.data.get('month') - type_id = request.data.get('type_id', 0) - order_source = request.data.get('order_source', 0) - summary_date = request.data.get('summary_date') - - now = date.today() - - # ---------- 时间范围 ---------- - if granularity == 'day': - if not year or not month: - return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) - start_date = date(year, month, 1) - end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) - trunc_func = TruncDate('create_time') - if not summary_date: - summary_date = now.strftime('%Y-%m-%d') - summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() - elif granularity == 'month': - if not year: - return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncMonth('create_time') - if not summary_date: - summary_date = now.strftime('%Y-%m') - summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() - elif granularity == 'year': - year = year or now.year - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncYear('create_time') - if not summary_date: - summary_date = str(year) - summary_q = date(year, 1, 1) - else: - return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) - - start_dt = datetime.combine(start_date, datetime.min.time()) - end_dt = datetime.combine(end_date, datetime.min.time()) - - # ---------- 基础过滤 ---------- - filters = { - 'create_time__gte': start_dt, - 'create_time__lt': end_dt, - 'zhuangtai__in': [1, 2, 3, 4, 5, 6, 7, 8] # 只统计有效订单状态 - } - if int(type_id) != 0: - filters['leixing_id'] = type_id - if int(order_source) in [1, 2]: - filters['fadan_pingtai'] = order_source - - # ---------- 时间序列分组查询 ---------- - qs = Dingdan.objects.filter(**filters) \ - .annotate(period=trunc_func) \ - .values('period') \ - .annotate( - # 订单总量(所有符合条件的状态) - order_count=Count('id'), - # 订单总额 - order_amount=Sum('jine'), - # 成交单量 (zhuangtai=3) - success_count=Count('id', filter=Q(zhuangtai=3)), - # 成交订单总额 - success_amount=Sum('jine', filter=Q(zhuangtai=3)), - # 退款单量 (zhuangtai=5) - refund_count=Count('id', filter=Q(zhuangtai=5)), - # 打手分成总额(仅成交订单) - dashou_fencheng_sum=Sum('dashou_fencheng', filter=Q(zhuangtai=3)), - # 平台成交订单的店铺分红 - dianpu_fenhong_sum=Sum('pingtai_kuozhan__dianpu_shouyi', - filter=Q(zhuangtai=3, fadan_pingtai=1)), - # 成交订单中:平台单金额、商家单金额、各自打手分成 - platform_success_amount=Sum('jine', filter=Q(zhuangtai=3, fadan_pingtai=1)), - merchant_success_amount=Sum('jine', filter=Q(zhuangtai=3, fadan_pingtai=2)), - platform_dashou=Sum('dashou_fencheng', filter=Q(zhuangtai=3, fadan_pingtai=1)), - merchant_dashou=Sum('dashou_fencheng', filter=Q(zhuangtai=3, fadan_pingtai=2)), - ).order_by('period') - - time_series = [] - for item in qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - - # 平台收益计算 - platform_profit = (item['platform_success_amount'] or 0) \ - - (item['platform_dashou'] or 0) \ - - (item['dianpu_fenhong_sum'] or 0) - merchant_profit = (item['merchant_success_amount'] or 0) \ - - (item['merchant_dashou'] or 0) - total_profit = platform_profit + merchant_profit - - time_series.append({ - 'date': period_str, - 'order_count': item['order_count'], - 'order_amount': float(item['order_amount'] or 0), - 'success_count': item['success_count'], - 'success_amount': float(item['success_amount'] or 0), - 'refund_count': item['refund_count'], - 'dashou_fencheng': float(item['dashou_fencheng_sum'] or 0), - 'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0), - 'profit': round(total_profit, 2) - }) - - # ---------- 汇总数据 ---------- - summary_qs = Dingdan.objects.filter(**filters) - total_order_count = summary_qs.count() - total_order_amount = summary_qs.aggregate(s=Sum('jine'))['s'] or 0 - total_success_count = summary_qs.filter(zhuangtai=3).count() - total_success_amount = summary_qs.filter(zhuangtai=3).aggregate(s=Sum('jine'))['s'] or 0 - total_refund_count = summary_qs.filter(zhuangtai=5).count() - total_dashou = summary_qs.filter(zhuangtai=3).aggregate(s=Sum('dashou_fencheng'))['s'] or 0 - total_dianpu_fenhong = summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate( - s=Sum('pingtai_kuozhan__dianpu_shouyi'))['s'] or 0 - - platform_amount = summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(s=Sum('jine'))['s'] or 0 - merchant_amount = summary_qs.filter(zhuangtai=3, fadan_pingtai=2).aggregate(s=Sum('jine'))['s'] or 0 - platform_dashou_sum = \ - summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(s=Sum('dashou_fencheng'))['s'] or 0 - merchant_dashou_sum = \ - summary_qs.filter(zhuangtai=3, fadan_pingtai=2).aggregate(s=Sum('dashou_fencheng'))['s'] or 0 - - profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong - profit_merchant = merchant_amount - merchant_dashou_sum - total_profit = profit_platform + profit_merchant - - summary = { - 'total_order_count': total_order_count, - 'total_order_amount': round(float(total_order_amount), 2), - 'total_success_count': total_success_count, - 'total_success_amount': round(float(total_success_amount), 2), - 'total_refund_count': total_refund_count, - 'total_dashou_fencheng': round(float(total_dashou), 2), - 'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2), - 'total_profit': round(total_profit, 2) - } - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'time_series': time_series, - 'summary': summary - } - }) - except Exception as e: - logger.exception("CwhqjtddsjView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - - - - - - - -from dingdan.models import Fadan, FadanFenhong # 罚单和罚款分红表,根据实际路径调整 -import logging -from datetime import date, datetime -from django.db.models import Count, Sum, Q - - - -from django.db.models.functions import TruncDate, TruncMonth, TruncYear - - -class CwqtczhqView(APIView): - """ - 其他充值/罚款数据统计(非会员充值) - POST /houtai/cwqtczhq - Body: { - "phone": "管理员手机号", - "granularity": "day" | "month" | "year", - "year": 2026, - "month": 5, // day时必填 - "types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款 - "summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY) - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - if 'qtczcaiwu' not in permissions: - return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403) - - granularity = request.data.get('granularity', 'day') - year = request.data.get('year') - month = request.data.get('month') - types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部 - summary_date = request.data.get('summary_date') - - if isinstance(types, list) and len(types) == 0: - types = [2, 3, 4, 5, 6] - - now = date.today() - - # ---------- 时间范围(用于曲线图) ---------- - if granularity == 'day': - if not year or not month: - return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) - start_date = date(year, month, 1) - end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) - trunc_func = TruncDate('create_time') - # 默认汇总日期 - if not summary_date: - summary_date = now.strftime('%Y-%m-%d') - summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() - elif granularity == 'month': - if not year: - return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncMonth('create_time') - if not summary_date: - summary_date = now.strftime('%Y-%m') - summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() - elif granularity == 'year': - year = year or now.year - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncYear('create_time') - if not summary_date: - summary_date = str(year) - summary_q = date(year, 1, 1) - else: - return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) - - start_dt = datetime.combine(start_date, datetime.min.time()) - end_dt = datetime.combine(end_date, datetime.min.time()) - - # ---------- 构建时间序列 ---------- - time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} } - - def ensure_date(ds): - if ds not in time_series_map: - time_series_map[ds] = {} - return time_series_map[ds] - - # 1. 处理充值记录(Czjilu)—— 完全不变 - cz_types = [t for t in types if t in [2, 3, 4, 5]] - if cz_types: - cz_qs = Czjilu.objects.filter( - create_time__gte=start_dt, - create_time__lt=end_dt, - leixing__in=cz_types, - zhuangtai=3 # 只统计支付成功的 - ).annotate( - period=trunc_func - ).values('period', 'leixing').annotate( - total_count=Count('id'), - total_amount=Sum('jine') - ).order_by('period', 'leixing') - - for item in cz_qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - lx = item['leixing'] - amount = float(item['total_amount'] or 0) - count = item['total_count'] - # 收益:积分(3)收益为金额本身,其他为0 - profit = amount if lx == 3 else 0.0 - - day_data = ensure_date(period_str) - day_data[str(lx)] = { - 'count': count, - 'amount': amount, - 'profit': profit, - 'fenhong_count': 0, # 充值类型无分红 - 'fenhong_amount': 0.0 - } - - # 2. 处理罚款(Fadan)—— 修改:直接从 Fadan 表获取分红总额和分红笔数 - if 6 in types: - # 使用一次聚合查询,同时获取罚款笔数、罚款金额、分红总额、分红笔数 - penalty_qs = Fadan.objects.filter( - create_time__gte=start_dt, - create_time__lt=end_dt, - zhuangtai=2 # 已缴纳 - ).annotate( - period=trunc_func - ).values('period').annotate( - penalty_count=Count('id'), - penalty_amount=Sum('fakuanjine'), - # 直接使用罚单表中的申请人分红金额字段 - fenhong_amount=Sum('shenqingren_fenhong_jine'), - fenhong_count=Count('id', filter=Q(shenqingren_fenhong_jine__gt=0)) - ).order_by('period') - - for item in penalty_qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - day_data = ensure_date(period_str) - penalty_amt = float(item['penalty_amount'] or 0) - fenhong_amt = float(item['fenhong_amount'] or 0) - profit = round(penalty_amt - fenhong_amt, 2) - day_data['6'] = { - 'count': item['penalty_count'], - 'amount': penalty_amt, - 'profit': profit, - 'fenhong_count': item['fenhong_count'], - 'fenhong_amount': fenhong_amt - } - - # 构造有序时间序列列表 - time_series = [] - for period_str in sorted(time_series_map.keys()): - entry = {'date': period_str, 'types': {}} - for type_code in sorted(time_series_map[period_str].keys()): - entry['types'][type_code] = time_series_map[period_str][type_code] - time_series.append(entry) - - # ---------- 汇总指定日期(summary)—— 完全保留原逻辑,仅将罚款部分改为从 Fadan 表直接取分红 ---------- - summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0} - - # 汇总充值类型(完全不变) - for lx in cz_types: - filter_kwargs = { - 'leixing': lx, - 'zhuangtai': 3, - } - if granularity == 'day': - filter_kwargs['create_time__date'] = summary_q - elif granularity == 'month': - filter_kwargs['create_time__year'] = summary_q.year - filter_kwargs['create_time__month'] = summary_q.month - else: - filter_kwargs['create_time__year'] = summary_q.year - agg = Czjilu.objects.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine')) - count = agg['count'] or 0 - amount = float(agg['amount'] or 0) - profit = amount if lx == 3 else 0.0 - summary_result['by_type'][str(lx)] = { - 'count': count, - 'amount': round(amount, 2), - 'profit': round(profit, 2), - 'fenhong_count': 0, - 'fenhong_amount': 0.0 - } - summary_result['total_count'] += count - summary_result['total_amount'] += amount - summary_result['total_profit'] += profit - - # 汇总罚款(修改:从 Fadan 表直接取分红,不再查询 FadanFenhong) - if 6 in types: - penalty_filter = { - 'zhuangtai': 2, - } - if granularity == 'day': - penalty_filter['create_time__date'] = summary_q - elif granularity == 'month': - penalty_filter['create_time__year'] = summary_q.year - penalty_filter['create_time__month'] = summary_q.month - else: - penalty_filter['create_time__year'] = summary_q.year - - # 一次聚合:罚款笔数、金额、分红总额、分红笔数 - agg = Fadan.objects.filter(**penalty_filter).aggregate( - count=Count('id'), - amount=Sum('fakuanjine'), - fenhong_amount=Sum('shenqingren_fenhong_jine'), - fenhong_count=Count('id', filter=Q(shenqingren_fenhong_jine__gt=0)) - ) - penalty_count = agg['count'] or 0 - penalty_amount = float(agg['amount'] or 0) - fh_amount = float(agg['fenhong_amount'] or 0) - fh_count = agg['fenhong_count'] or 0 - profit = round(penalty_amount - fh_amount, 2) - - summary_result['by_type']['6'] = { - 'count': penalty_count, - 'amount': round(penalty_amount, 2), - 'profit': profit, - 'fenhong_count': fh_count, - 'fenhong_amount': round(fh_amount, 2) - } - summary_result['total_count'] += penalty_count - summary_result['total_amount'] += penalty_amount - summary_result['total_profit'] += profit - - summary_result['total_amount'] = round(summary_result['total_amount'], 2) - summary_result['total_profit'] = round(summary_result['total_profit'], 2) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'time_series': time_series, - 'summary': summary_result - } - }) - except Exception as e: - logger.exception("CwqtczhqView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - -'''class CwqtczhqView(APIView): - """ - 其他充值/罚款数据统计(非会员充值) - POST /houtai/cwqtczhq - Body: { - "phone": "管理员手机号", - "granularity": "day" | "month" | "year", - "year": 2026, - "month": 5, // day时必填 - "types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款 - "summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY) - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - if 'qtczcaiwu' not in permissions: - return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403) - - granularity = request.data.get('granularity', 'day') - year = request.data.get('year') - month = request.data.get('month') - types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部 - summary_date = request.data.get('summary_date') - - if isinstance(types, list) and len(types) == 0: - types = [2, 3, 4, 5, 6] - - now = date.today() - - # ---------- 时间范围(用于曲线图) ---------- - if granularity == 'day': - if not year or not month: - return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) - start_date = date(year, month, 1) - end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) - trunc_func = TruncDate('create_time') - # 默认汇总日期 - if not summary_date: - summary_date = now.strftime('%Y-%m-%d') - summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() - elif granularity == 'month': - if not year: - return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncMonth('create_time') - if not summary_date: - summary_date = now.strftime('%Y-%m') - summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() - elif granularity == 'year': - year = year or now.year - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncYear('create_time') - if not summary_date: - summary_date = str(year) - summary_q = date(year, 1, 1) - else: - return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) - - start_dt = datetime.combine(start_date, datetime.min.time()) - end_dt = datetime.combine(end_date, datetime.min.time()) - - # ---------- 构建时间序列 ---------- - time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} } - - def ensure_date(ds): - if ds not in time_series_map: - time_series_map[ds] = {} - return time_series_map[ds] - - # 1. 处理充值记录(Czjilu) - cz_types = [t for t in types if t in [2, 3, 4, 5]] - if cz_types: - cz_qs = Czjilu.objects.filter( - create_time__gte=start_dt, - create_time__lt=end_dt, - leixing__in=cz_types, - zhuangtai=3 # 只统计支付成功的 - ).annotate( - period=trunc_func - ).values('period', 'leixing').annotate( - total_count=Count('id'), - total_amount=Sum('jine') - ).order_by('period', 'leixing') - - for item in cz_qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - lx = item['leixing'] - amount = float(item['total_amount'] or 0) - count = item['total_count'] - # 收益:积分(3)收益为金额本身,其他为0 - profit = amount if lx == 3 else 0.0 - - day_data = ensure_date(period_str) - day_data[str(lx)] = { - 'count': count, - 'amount': amount, - 'profit': profit, - 'fenhong_count': 0, # 充值类型无分红 - 'fenhong_amount': 0.0 - } - - # 2. 处理罚款(Fadan)—— 包含分红 - if 6 in types: - # 罚款记录 - penalty_qs = Fadan.objects.filter( - create_time__gte=start_dt, - create_time__lt=end_dt, - zhuangtai=2 # 已缴纳 - ).annotate( - period=trunc_func - ).values('period').annotate( - penalty_count=Count('id'), - penalty_amount=Sum('fakuanjine') - ).order_by('period') - - # 分红记录 - fenhong_qs = FadanFenhong.objects.filter( - create_time__gte=start_dt, - create_time__lt=end_dt - ).annotate( - period=trunc_func - ).values('period').annotate( - fh_count=Count('id'), - fh_amount=Sum('ketixian_fenhong') - ).order_by('period') - - # 合并 - for item in penalty_qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - day_data = ensure_date(period_str) - day_data['6'] = { - 'count': item['penalty_count'], - 'amount': float(item['penalty_amount'] or 0), - 'profit': 0.0, # 先设为0,后续计算 - 'fenhong_count': 0, - 'fenhong_amount': 0.0 - } - - for item in fenhong_qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - day_data = ensure_date(period_str) - if '6' not in day_data: - day_data['6'] = {'count': 0, 'amount': 0.0, 'profit': 0.0, 'fenhong_count': 0, 'fenhong_amount': 0.0} - day_data['6']['fenhong_count'] += item['fh_count'] - day_data['6']['fenhong_amount'] += float(item['fh_amount'] or 0) - - # 计算罚款收益 = 罚款金额 - 分红金额 - for period_str, day_data in time_series_map.items(): - if '6' in day_data: - penalty = day_data['6'] - penalty['profit'] = round(penalty['amount'] - penalty['fenhong_amount'], 2) - - # 构造有序时间序列列表 - time_series = [] - for period_str in sorted(time_series_map.keys()): - entry = {'date': period_str, 'types': {}} - for type_code in sorted(time_series_map[period_str].keys()): - entry['types'][type_code] = time_series_map[period_str][type_code] - time_series.append(entry) - - # ---------- 汇总指定日期 ---------- - summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0} - - # 汇总充值类型 - for lx in cz_types: - filter_kwargs = { - 'leixing': lx, - 'zhuangtai': 3, - } - if granularity == 'day': - filter_kwargs['create_time__date'] = summary_q - elif granularity == 'month': - filter_kwargs['create_time__year'] = summary_q.year - filter_kwargs['create_time__month'] = summary_q.month - else: - filter_kwargs['create_time__year'] = summary_q.year - agg = Czjilu.objects.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine')) - count = agg['count'] or 0 - amount = float(agg['amount'] or 0) - profit = amount if lx == 3 else 0.0 - summary_result['by_type'][str(lx)] = { - 'count': count, - 'amount': round(amount, 2), - 'profit': round(profit, 2), - 'fenhong_count': 0, - 'fenhong_amount': 0.0 - } - summary_result['total_count'] += count - summary_result['total_amount'] += amount - summary_result['total_profit'] += profit - - # 汇总罚款 - if 6 in types: - penalty_filter = { - 'zhuangtai': 2, - } - fenhong_filter = {} - if granularity == 'day': - penalty_filter['create_time__date'] = summary_q - fenhong_filter['create_time__date'] = summary_q - elif granularity == 'month': - penalty_filter['create_time__year'] = summary_q.year - penalty_filter['create_time__month'] = summary_q.month - fenhong_filter['create_time__year'] = summary_q.year - fenhong_filter['create_time__month'] = summary_q.month - else: - penalty_filter['create_time__year'] = summary_q.year - fenhong_filter['create_time__year'] = summary_q.year - - penalty_agg = Fadan.objects.filter(**penalty_filter).aggregate( - count=Count('id'), amount=Sum('fakuanjine')) - penalty_count = penalty_agg['count'] or 0 - penalty_amount = float(penalty_agg['amount'] or 0) - - fenhong_agg = FadanFenhong.objects.filter(**fenhong_filter).aggregate( - fh_count=Count('id'), fh_amount=Sum('ketixian_fenhong')) - fh_count = fenhong_agg['fh_count'] or 0 - fh_amount = float(fenhong_agg['fh_amount'] or 0) - - profit = round(penalty_amount - fh_amount, 2) - summary_result['by_type']['6'] = { - 'count': penalty_count, - 'amount': round(penalty_amount, 2), - 'profit': profit, - 'fenhong_count': fh_count, - 'fenhong_amount': round(fh_amount, 2) - } - summary_result['total_count'] += penalty_count - summary_result['total_amount'] += penalty_amount - summary_result['total_profit'] += profit - - summary_result['total_amount'] = round(summary_result['total_amount'], 2) - summary_result['total_profit'] = round(summary_result['total_profit'], 2) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'time_series': time_series, - 'summary': summary_result - } - }) - except Exception as e: - logger.exception("CwqtczhqView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)''' - - - - - -# ==================== 后端接口 ==================== -# 文件:views.py (添加以下两个视图类) - -from dengji.models import ( - Chenghao, KaoheCishuFeiyong, Bankuai, - YonghuChenghao, ShenheJilu, # 可能不需要用到但导入无妨 -) - - - -class KhpzhqView(APIView): - """ - 获取所有考核配置数据(称号列表、板块、费用) - POST /houtai/khpzhq - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - if 'kaohepeizhi' not in permissions: - return Response({'code': 403, 'msg': '无考核配置权限'}, status=403) - - bankuai_list = list(Bankuai.objects.values('bankuai_id', 'mingcheng')) - - chenghao_qs = Chenghao.objects.prefetch_related('kaohecishufeiyong_set').all() - chenghao_list = [] - for ch in chenghao_qs: - # 获取费用:按次数排序,转为 {cishu: feiyong} 字典 - feiyong_dict = { - item.cishu: float(item.feiyong) - for item in ch.kaohecishufeiyong_set.order_by('cishu') - } - max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0 - - chenghao_list.append({ - 'id': ch.id, - 'mingcheng': ch.mingcheng, - 'leixing': ch.leixing, - 'leixing_name': ch.get_leixing_display(), - 'bankuai_id': ch.bankuai_id, - 'tubiao_url': ch.tubiao_url or '', - 'texiao_miaoshu': ch.texiao_miaoshu or '', - 'create_time': ch.create_time.strftime('%Y-%m-%d %H:%M:%S') if ch.create_time else '', - 'kaohe_guize': ch.kaohe_guize or '', - 'kaioi_jinpai': ch.kaioi_jinpai, - 'feiyong': feiyong_dict, # {1:100.0, 2:200.0} - 'max_cishu': max_cishu - }) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'bankuai': bankuai_list, - 'chenghao': chenghao_list - } - }) - except Exception as e: - logger.exception("KhpzhqView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - -class ChzsgcView(APIView): - """ - 称号(标签)增删改查统一接口 - POST /houtai/chzsgc - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - if 'kaohepeizhi' not in permissions: - return Response({'code': 403, 'msg': '无考核配置权限'}, status=403) - - action = request.data.get('action') - if not action: - return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400) - - # ========== 创建称号 ========== - if action == 'create': - mingcheng = request.data.get('mingcheng', '').strip() - leixing = request.data.get('leixing', 'dashou') - bankuai_id = request.data.get('bankuai_id') - texiao_miaoshu = request.data.get('texiao_miaoshu', '') - kaohe_guize = request.data.get('kaohe_guize', '') - kaioi_jinpai = request.data.get('kaioi_jinpai', False) - feiyong_list = request.data.get('feiyong_list', []) - - if not mingcheng: - return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400) - if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']: - return Response({'code': 1, 'msg': '无效的称号类型'}, status=400) - if Chenghao.objects.filter(mingcheng=mingcheng).exists(): - return Response({'code': 1, 'msg': '称号名称已存在'}, status=400) - - with transaction.atomic(): - ch = Chenghao.objects.create( - mingcheng=mingcheng, - leixing=leixing, - bankuai_id=bankuai_id, - texiao_miaoshu=texiao_miaoshu, - kaohe_guize=kaohe_guize, - kaioi_jinpai=kaioi_jinpai - ) - # 处理费用:前端传来 [{cishu:1, feiyong:100}, ...] - if feiyong_list: - for item in feiyong_list: - cishu = int(item.get('cishu', 1)) - feiyong = float(item.get('feiyong', 0)) - KaoheCishuFeiyong.objects.update_or_create( - chenghao=ch, cishu=cishu, - defaults={'feiyong': feiyong} - ) - else: - # 默认添加第一次免费 - KaoheCishuFeiyong.objects.create(chenghao=ch, cishu=1, feiyong=0) - - return Response({'code': 0, 'msg': '创建成功', 'data': {'chenghao_id': ch.id}}) - - # ========== 修改称号 ========== - elif action == 'update': - ch_id = request.data.get('chenghao_id') - if not ch_id: - return Response({'code': 1, 'msg': '缺少称号ID'}, status=400) - try: - ch = Chenghao.objects.get(id=ch_id) - except Chenghao.DoesNotExist: - return Response({'code': 1, 'msg': '称号不存在'}, status=404) - - mingcheng = request.data.get('mingcheng', ch.mingcheng).strip() - leixing = request.data.get('leixing', ch.leixing) - bankuai_id = request.data.get('bankuai_id', ch.bankuai_id) - texiao_miaoshu = request.data.get('texiao_miaoshu', ch.texiao_miaoshu) - kaohe_guize = request.data.get('kaohe_guize', ch.kaohe_guize) - kaioi_jinpai = request.data.get('kaioi_jinpai', ch.kaioi_jinpai) - - if not mingcheng: - return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400) - if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']: - return Response({'code': 1, 'msg': '无效的称号类型'}, status=400) - if Chenghao.objects.filter(mingcheng=mingcheng).exclude(id=ch_id).exists(): - return Response({'code': 1, 'msg': '称号名称已存在'}, status=400) - - ch.mingcheng = mingcheng - ch.leixing = leixing - ch.bankuai_id = bankuai_id - ch.texiao_miaoshu = texiao_miaoshu - ch.kaohe_guize = kaohe_guize - ch.kaioi_jinpai = kaioi_jinpai - ch.save() - - # 如果前端传递了 feiyong_list,则全量替换费用 - feiyong_list = request.data.get('feiyong_list') - if feiyong_list is not None: - with transaction.atomic(): - ch.kaohecishufeiyong_set.all().delete() - for item in feiyong_list: - KaoheCishuFeiyong.objects.create( - chenghao=ch, - cishu=int(item.get('cishu', 1)), - feiyong=float(item.get('feiyong', 0)) - ) - - return Response({'code': 0, 'msg': '修改成功'}) - - # ========== 删除称号 ========== - elif action == 'delete': - ch_id = request.data.get('chenghao_id') - if not ch_id: - return Response({'code': 1, 'msg': '缺少称号ID'}, status=400) - try: - ch = Chenghao.objects.get(id=ch_id) - except Chenghao.DoesNotExist: - return Response({'code': 1, 'msg': '称号不存在'}, status=404) - ch.delete() - return Response({'code': 0, 'msg': '删除成功'}) - - # ========== 添加/修改某次费用 ========== - elif action == 'add_feiyong': - ch_id = request.data.get('chenghao_id') - cishu = request.data.get('cishu') - feiyong = request.data.get('feiyong', 0) - if not ch_id or not cishu: - return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400) - try: - ch = Chenghao.objects.get(id=ch_id) - except Chenghao.DoesNotExist: - return Response({'code': 1, 'msg': '称号不存在'}, status=404) - if int(cishu) < 1: - return Response({'code': 1, 'msg': '次数必须>=1'}, status=400) - KaoheCishuFeiyong.objects.update_or_create( - chenghao=ch, cishu=cishu, - defaults={'feiyong': feiyong} - ) - return Response({'code': 0, 'msg': '添加/修改费用成功'}) - - # ========== 删除某次费用 ========== - elif action == 'remove_feiyong': - ch_id = request.data.get('chenghao_id') - cishu = request.data.get('cishu') - if not ch_id or not cishu: - return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400) - if int(cishu) == 1: - return Response({'code': 1, 'msg': '不能删除第1次考核费用'}, status=400) - try: - ch = Chenghao.objects.get(id=ch_id) - except Chenghao.DoesNotExist: - return Response({'code': 1, 'msg': '称号不存在'}, status=404) - KaoheCishuFeiyong.objects.filter(chenghao=ch, cishu=cishu).delete() - return Response({'code': 0, 'msg': '删除成功'}) - - # ========== 修改某次费用(等价于add) ========== - elif action == 'update_feiyong': - return self.add_feiyong(request) # 复用逻辑 - - else: - return Response({'code': 1, 'msg': '未知操作'}, status=400) - - except Exception as e: - logger.exception("ChzsgcView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - -# ==================== 后端 views.py ==================== -# 请确保已有以下导入,如果没有请在文件头部添加 - -from yonghu.models import ( - UserMain, UserShenheguan -) - -from dengji.models import ( - KaoheguanBankuai, Bankuai -) - - -class KhgglView(APIView): - """ - 考核官管理数据接口(列表、筛选、分页) - POST /houtai/khggl - Body: { - "phone": "管理员手机号", - "page": 1, // 可选,默认1 - "page_size": 15, // 可选,默认15 - "yonghuid": "1234567", // 按用户ID筛选,可选 - "zhuangtai": 1, // 账号状态 0=禁用,1=正常,可选 - "shenhe_zhuangtai": 0, // 审核官状态 0=空闲,1=审核中,可选 - "bankuai_name": "王者" // 板块名称模糊筛选,可选 - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - - if 'kaohepeizhi' not in permissions: - return Response({'code': 403, 'msg': '无考核管理权限'}, status=403) - - # 分页参数 - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 15)) - if page < 1: page = 1 - if page_size < 1: page_size = 15 - - # 筛选条件 - filter_yonghuid = request.data.get('yonghuid', '').strip() - filter_zhuangtai = request.data.get('zhuangtai') - filter_shenhe_zhuangtai = request.data.get('shenhe_zhuangtai') - filter_bankuai_name = request.data.get('bankuai_name', '').strip() - - # 基础查询:所有审核官,预加载用户及打手扩展表 - queryset = UserShenheguan.objects.select_related('user__dashou_profile') - - if filter_yonghuid: - queryset = queryset.filter(user__yonghuid=filter_yonghuid) - if filter_zhuangtai is not None: - queryset = queryset.filter(zhuangtai=filter_zhuangtai) - if filter_shenhe_zhuangtai is not None: - queryset = queryset.filter(shenhe_zhuangtai=filter_shenhe_zhuangtai) - - # 如果填写了板块名称,筛选出关联该板块的审核官 - if filter_bankuai_name: - bankuai_ids = Bankuai.objects.filter( - mingcheng__icontains=filter_bankuai_name - ).values_list('bankuai_id', flat=True) - shenheguan_ids = KaoheguanBankuai.objects.filter( - bankuai_id__in=bankuai_ids - ).values_list('kaoheguan_id', flat=True) - queryset = queryset.filter(user__yonghuid__in=shenheguan_ids) - - total = queryset.count() - start = (page - 1) * page_size - end = start + page_size - shenheguan_list = queryset.order_by('-create_time')[start:end] - - # ---------- 预计算每个板块的打手数量 ---------- - bankuai_dashou_count_map = {} - all_banquai = Bankuai.objects.all() - for bk in all_banquai: - tag_ids = Chenghao.objects.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True) - count = YonghuChenghao.objects.filter( - chenghao_id__in=tag_ids, - yonghu__dashou_profile__isnull=False - ).values('yonghu').distinct().count() - bankuai_dashou_count_map[bk.bankuai_id] = count - - # ---------- 新增:板块标签统计 ---------- - bankuai_tags_data = [] - for bk in all_banquai: - tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou') - tag_list = [] - for tag in tags: - user_count = YonghuChenghao.objects.filter(chenghao=tag).values('yonghu').distinct().count() - tag_list.append({ - 'id': tag.id, - 'name': tag.mingcheng, - 'user_count': user_count, - }) - bankuai_tags_data.append({ - 'bankuai_id': bk.bankuai_id, - 'mingcheng': bk.mingcheng, - 'tags': tag_list, - }) - - # 构造返回数据(审核官列表) - data_list = [] - for sg in shenheguan_list: - user = sg.user - # 获取该审核官关联的板块 - bk_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=user.yonghuid).select_related('bankuai') - bankuai_info = [] - for bk in bk_qs: - bid = bk.bankuai.bankuai_id - bankuai_info.append({ - 'bankuai_id': bid, - 'mingcheng': bk.bankuai.mingcheng, - 'dashou_count': bankuai_dashou_count_map.get(bid, 0) - }) - - dashou_nick = user.dashou_profile.nicheng if hasattr(user, 'dashou_profile') and user.dashou_profile else '' - - data_list.append({ - 'yonghuid': user.yonghuid, - 'avatar': user.avatar or '', - 'phone': user.phone or '', - 'zhuangtai': sg.zhuangtai, - 'shenhe_zhuangtai': sg.shenhe_zhuangtai, - 'shenhe_zongshu': sg.shenhe_zongshu, - 'tongguo_zongshu': sg.tongguo_zongshu, - 'yue': float(sg.yue), - 'zonge': float(sg.zonge), - 'create_time': sg.create_time.strftime('%Y-%m-%d %H:%M:%S') if sg.create_time else '', - 'update_time': sg.update_time.strftime('%Y-%m-%d %H:%M:%S') if sg.update_time else '', - 'bankuai': bankuai_info, - 'nicheng': dashou_nick, - 'is_renzheng': sg.is_renzheng, - }) - - all_bankuai = list(Bankuai.objects.values('bankuai_id', 'mingcheng')) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'total': total, - 'page': page, - 'page_size': page_size, - 'list': data_list, - 'all_bankuai': all_bankuai, - 'bankuai_tags': bankuai_tags_data, # 新增板块标签统计 - } - }) - - except Exception as e: - logger.exception("KhgglView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - -class ShgxgsjView(APIView): - """ - 审核官信息修改(余额、状态、板块关联) - POST /houtai/shgxgsj - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - if 'kaohepeizhi' not in permissions: - return Response({'code': 403, 'msg': '无考核管理权限'}, status=403) - - yonghuid = request.data.get('yonghuid') - if not yonghuid: - return Response({'code': 1, 'msg': '缺少用户ID'}, status=400) - - try: - shenheguan = UserShenheguan.objects.select_related('user').get(user__yonghuid=yonghuid) - except UserShenheguan.DoesNotExist: - return Response({'code': 1, 'msg': '审核官不存在'}, status=404) - - action = request.data.get('action') - - if action == 'update_info': - if 'yue' in request.data: - shenheguan.yue = float(request.data.get('yue', 0)) - if 'zhuangtai' in request.data: - zt = int(request.data.get('zhuangtai')) - if zt not in [0, 1]: - return Response({'code': 1, 'msg': '无效的账号状态'}, status=400) - shenheguan.zhuangtai = zt - if 'shenhe_zhuangtai' in request.data: - szt = int(request.data.get('shenhe_zhuangtai')) - if szt not in [0, 1]: - return Response({'code': 1, 'msg': '无效的审核官状态'}, status=400) - shenheguan.shenhe_zhuangtai = szt - # 新增认证状态编辑 - if 'is_renzheng' in request.data: - shenheguan.is_renzheng = bool(request.data.get('is_renzheng')) - shenheguan.save() - return Response({'code': 0, 'msg': '修改成功'}) - - elif action == 'add_bankuai': - bankuai_id = request.data.get('bankuai_id') - if not bankuai_id: - return Response({'code': 1, 'msg': '缺少板块ID'}, status=400) - if not Bankuai.objects.filter(bankuai_id=bankuai_id).exists(): - return Response({'code': 1, 'msg': '板块不存在'}, status=400) - if KaoheguanBankuai.objects.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists(): - return Response({'code': 1, 'msg': '已关联该板块'}, status=400) - KaoheguanBankuai.objects.create(kaoheguan_id=yonghuid, bankuai_id=bankuai_id) - return Response({'code': 0, 'msg': '板块关联成功'}) - - elif action == 'remove_bankuai': - bankuai_id = request.data.get('bankuai_id') - if not bankuai_id: - return Response({'code': 1, 'msg': '缺少板块ID'}, status=400) - deleted, _ = KaoheguanBankuai.objects.filter( - kaoheguan_id=yonghuid, bankuai_id=bankuai_id - ).delete() - if not deleted: - return Response({'code': 1, 'msg': '未找到该板块关联'}, status=400) - return Response({'code': 0, 'msg': '板块移除成功'}) - - else: - return Response({'code': 1, 'msg': '未知操作'}, status=400) - - except Exception as e: - logger.exception("ShgxgsjView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - - - -import secrets -from dingdan.models import Dingdan, DingdanShangjia -from dingdan.tongzhi_tasks import dingdan_guangbo - -from peizhi.models import ShangjiaLianjie - -class ZxkfghdsView(APIView): - """ - 客服更换打手接口 - 路径:POST /houtai/zxkfghds - 功能: - 1. 将当前订单状态改为已退款(5),释放接单打手(若有) - 2. 如需跨平台通知,则调用对方平台退款通知接口 - 3. 基于原订单信息生成全新订单(状态直接为 1,已替老板填写) - 4. 生成新的派单链接,并标记为已使用 - 5. 新链接记录上一个链接ID和上一个订单ID - 6. 【关键】不操作商家余额、退款次数、每日统计,因新订单是替换而非新增扣款 - 权限:需要'003aa'(跨平台管理)权限,与退款接口一致 - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # ========== 1. 参数提取 ========== - username = request.data.get('username', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - if not username or not dingdan_id: - logger.warning("更换打手参数不完整") - return Response({'code': 400, 'msg': '参数不完整'}) - - # ========== 2. 权限校验(与跨平台退款相同) ========== - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 返回错误响应 - if '003aa' not in permissions: - return Response({'code': 403, 'msg': '您无权处理订单'}) - - current_user = request.user - - # ========== 3. 查询原订单及关联信息 ========== - try: - old_order = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - logger.warning(f"订单不存在: {dingdan_id}") - return Response({'code': 404, 'msg': '订单不存在'}) - - # 仅允许状态为 2(进行中)或 8(结算中)更换打手 - if old_order.zhuangtai not in [1,7,2, 8]: - return Response({'code': 400, 'msg': '当前订单状态不允许更换打手'}) - - # 本接口仅处理商家发单(fadan_pingtai=2) - if old_order.fadan_pingtai != 2: - return Response({'code': 400, 'msg': '仅支持商家发单的更换操作'}) - - # 获取原链接(用于模板ID等) - try: - old_lianjie = ShangjiaLianjie.objects.get(dingdan_id=dingdan_id) - except ShangjiaLianjie.DoesNotExist: - logger.error(f"原链接记录不存在,订单: {dingdan_id}") - return Response({'code': 500, 'msg': '链接数据异常'}) - - # 商家信息(仅用于记录关联,不操作余额) - try: - shangjia_ext = old_order.shangjia_kuozhan - shangjia_id = shangjia_ext.shangjia_id - # 获取商家昵称用于新订单扩展表 - shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) - shangjia_nicheng = shangjia_user.shop_profile.nicheng or f"商家{shangjia_id}" - except (ObjectDoesNotExist, UserShangjia.DoesNotExist): - logger.error(f"商家信息缺失,订单: {dingdan_id}") - return Response({'code': 500, 'msg': '商家数据异常'}) - - # ========== 4. 判断是否需要跨平台退款通知 ========== - is_cross = old_order.is_cross - partner_club_id = old_order.partner_club_id - partner_order_id = old_order.partner_order_id - need_notify_partner = is_cross and partner_club_id and partner_order_id - - # ========== 5. 退款处理(仅打手侧,不碰商家) ========== - with transaction.atomic(): - # 5.1 订单状态改为已退款 - old_order.zhuangtai = 5 - old_order.clkf = current_user.phone - old_order.save() - - # 5.2 处理接单打手(若有) - dashou_id = old_order.jiedan_dashou_id - if dashou_id: - try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.tuikuanliang += 1 # 退款次数+1 - dashou_profile.zhuangtai = 1 # 设为空闲 - dashou_profile.save() - except (UserMain.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"打手 {dashou_id} 不存在,跳过更新") - - # 5.3 更新退款记录表 - Tuikuanjilu.objects.update_or_create( - dingdan_id=dingdan_id, - defaults={ - 'dashouid': dashou_id or '', - 'qingqiuid': '', - 'chuliid': current_user.phone, - 'liyou': '客服更换打手退款', - 'sqzhuangtai': 1, - 'jine': old_order.jine or 0, - 'dashou_fencheng': old_order.dashou_fencheng or 0, - 'jieshao': old_order.jieshao or '', - 'beizhu': '更换打手自动退款', - 'nicheng': old_order.nicheng or '', - } - ) - - # 5.4 打手每日统计(退款) - if dashou_id: - try: - update_dashou_daily_by_action( - yonghuid=dashou_id, - amount=old_order.dashou_fencheng, - action=3 # 3=退款 - ) - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - - # 5.5 客服处理统计 - kefu.jinrichuli += 1 - kefu.jinyuechuli += 1 - kefu.zongchuli += 1 - kefu.save() - - # 5.6 跨平台退款通知(需要时) - if need_notify_partner: - try: - # 获取我方俱乐部ID - my_club_config = ClubConfig.objects.filter(is_self=1).first() - if not my_club_config: - raise Exception("我方俱乐部配置不存在") - self_club_id = my_club_config.club_id - club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) - partner_domain = club_rel.partner_domain - - notify_url = partner_domain.rstrip('/') + '/houtai/dfddtk' - payload = { - 'order_id': partner_order_id, - 'club_id': self_club_id, - 'status': 5, - 'reason': '客服更换打手退款' - } - - # 调用对方接口(必须成功,否则整个事务回滚) - resp = requests.post(notify_url, json=payload, timeout=5) - if resp.status_code == 200: - resp_json = resp.json() - if resp_json.get('code') != 0: - transaction.set_rollback(True) - logger.error(f"对方平台退款通知失败: {resp_json.get('msg')}") - return Response({'code': 500, 'msg': f'对方平台通知失败: {resp_json.get("msg")}'}) - else: - transaction.set_rollback(True) - logger.error(f"对方平台接口HTTP错误: {resp.status_code}") - return Response({'code': 500, 'msg': '通知对方平台失败'}) - - # 更新跨平台扩展表(如果存在) - from dingdan.models import CrossPlatformOrderData - CrossPlatformOrderData.objects.update_or_create( - dingdan_id=dingdan_id, - defaults={ - 'partner_order_id': partner_order_id, - 'partner_club_id': partner_club_id, - 'partner_order_status': 5, - } - ) - except Exception as e: - transaction.set_rollback(True) - logger.error(f"通知对方平台异常: {e}") - return Response({'code': 500, 'msg': f'通知对方平台失败: {str(e)}'}) - - # 至此,原订单已退款,事务提交 - - # ========== 6. 生成新订单和链接(不扣商家余额) ========== - try: - with transaction.atomic(): - # ---- 6.1 生成新的订单ID和Token ---- - def generate_dingdan_id(): - timestamp = int(time.time() * 1000) - process_id = os.getpid() % 1000 - random_num = random.randint(1000, 9999) - return f"SJ{timestamp}{process_id:03d}{random_num}" - - def generate_secure_token(dd_id, uid): - timestamp = int(time.time() * 1_000_000) - salt = secrets.token_hex(8) - raw = f"{timestamp}:{dd_id}:{uid}:{salt}" - token = hashlib.sha256(raw.encode()).hexdigest()[:24] - if ShangjiaLianjie.objects.filter(lianjie_token=token).exists(): - return generate_secure_token(dd_id, uid) - return token - - def generate_link_url(token): - base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/') - return f"{base}/order/{token}" - - new_dingdan_id = generate_dingdan_id() - secure_token = generate_secure_token(new_dingdan_id, shangjia_id) - - # ---- 6.2 创建新订单(复制核心数据,状态直接为1) ---- - new_order = Dingdan.objects.create( - dingdan_id=new_dingdan_id, - zhuangtai=1, # 下单中 - fadan_pingtai=2, - jine=old_order.jine, - dashou_fencheng=old_order.dashou_fencheng, - leixing_id=old_order.leixing_id, - yaoqiuleixing=old_order.yaoqiuleixing, - huiyuan_id=old_order.huiyuan_id, - yongjin=old_order.yongjin, - jieshao=old_order.jieshao, - nicheng=old_order.nicheng, # 继承游戏昵称 - beizhu=old_order.beizhu, - user1_id=old_order.user1_id, # 商家标识 - tupian=old_order.tupian, - # 注意:不继承 zhiding_id、jiedan_dashou_id - ) - - # ---- 6.3 创建商家扩展表 ---- - DingdanShangjia.objects.create( - dingdan=new_order, - shangjia_id=shangjia_id, - sjnicheng=shangjia_nicheng, - ) - - # ---- 6.4 生成新链接并关联旧链接信息 ---- - link_url = generate_link_url(secure_token) - new_lianjie = ShangjiaLianjie.objects.create( - yonghu_id=shangjia_id, - leixing_id=old_lianjie.leixing_id, - moban_id=old_lianjie.moban_id, - lianjie=link_url, - lianjie_token=secure_token, - dingdan_id=new_dingdan_id, - is_used=True, # 直接标记已使用 - youxiao_shijian=timezone.now() + timezone.timedelta(days=1), - previous_lianjie_id=old_lianjie.id, # 记录旧链接ID - previous_dingdan_id=old_order.dingdan_id, # 记录旧订单ID - ) - - # ---- 6.5 异步通知(广播新订单、跨平台同步) ---- - try: - order_info = { - 'dingdan_id': new_dingdan_id, - 'game_type': self._get_game_type_name(new_order.leixing_id), - 'amount': str(new_order.jine), - 'order_desc': (new_order.jieshao or '')[:50], - } - dingdan_guangbo.delay(order_info) - except Exception as e: - logger.error(f"广播通知失败: {e}") - - # 跨平台同步(与客户填写接口保持一致) - try: - from dingdan.utils import sync_order_to_partners - sync_order_to_partners(new_dingdan_id) - except Exception as e: - logger.error(f"跨平台同步失败: {e}") - - # 注意:此处不更新商家余额、发布量、每日统计,因为这是替换订单 - - logger.info(f"更换打手成功:旧订单{dingdan_id}→退款,新订单{new_dingdan_id}已生成") - - return Response({ - 'code': 0, - 'msg': '更换成功', - 'data': { - 'new_dingdan_id': new_dingdan_id, - 'new_lianjie': link_url, - } - }) - - except Exception as e: - logger.error(f"生成新订单事务失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '生成新订单失败'}) - - def _get_game_type_name(self, leixing_id): - """根据类型ID获取游戏类型名称""" - try: - from shangpin.models import ShangpinLeixing - gt = ShangpinLeixing.objects.filter(id=leixing_id).first() - return gt.jieshao if gt else '游戏订单' - except Exception: - return '游戏订单' - - - - - - -import logging - -from django.conf import settings -from django.db.models import ObjectDoesNotExist -from rest_framework import status -from rest_framework.parsers import JSONParser -from rest_framework.response import Response -from rest_framework.views import APIView - -from dingdan.models import Dingdan - -logger = logging.getLogger('houtai') - - -class KptxwztjbView(APIView): - """ - 跨平台订单状态同步接口(对方平台调用) - 请求:POST /houtai/kptxwztjb - 参数: - - token: 同步专用令牌(明文) - - partner_order_id: 我方订单ID(在对方眼中是partner_order_id,实际查本库dingdan_id) - - our_order_id: 对方订单ID(在本库中为partner_order_id) - - club_id: 俱乐部ID(可选验证) - 返回: - code=0 成功,data.status 为订单状态 - 其他 code 为错误 - 注意:此接口仅用于查询对方订单状态,不做任何修改 - """ - - authentication_classes = [] # 无需JWT等认证 - permission_classes = [] # 公开接口 - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - token = request.data.get('token', '').strip() - partner_order_id = request.data.get('partner_order_id', '').strip() - our_order_id = request.data.get('our_order_id', '').strip() - club_id = request.data.get('club_id', '').strip() - - if not all([token, partner_order_id, our_order_id]): - logger.warning("跨平台状态同步参数缺失") - return Response({'code': 400, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) - - # 2. 验证同步令牌 - SYNC_TOKEN = getattr(settings, 'CROSS_PLATFORM_SYNC_TOKEN', None) - if not SYNC_TOKEN or token != SYNC_TOKEN: - logger.warning(f"无效的同步令牌: {token[:8]}...") - return Response({'code': 403, 'msg': '无效令牌'}, status=status.HTTP_403_FORBIDDEN) - - # 3. 查询订单 - # partner_order_id 在对方视角是“我方订单ID”,在本库中应匹配 dingdan_id - try: - order = Dingdan.objects.get(dingdan_id=partner_order_id) - except Dingdan.DoesNotExist: - logger.warning(f"订单不存在, partner_order_id: {partner_order_id}") - return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) - - # 4. 验证订单关联关系:本库中的 partner_order_id 应等于传入的 our_order_id - if order.partner_order_id != our_order_id: - logger.warning( - f"订单关联不匹配: partner_order_id={order.partner_order_id}, our_order_id={our_order_id}" - ) - return Response({'code': 400, 'msg': '订单关联信息不匹配'}, status=status.HTTP_400_BAD_REQUEST) - - - - # 6. 返回订单状态 - status_code = order.zhuangtai - logger.info( - f"跨平台状态查询成功, dingdan_id={order.dingdan_id}, status={status_code}" - ) - return Response({ - 'code': 0, - 'msg': 'success', - 'data': { - 'dingdan_id': order.dingdan_id, - 'status': status_code, - } +import hmac +import threading +import traceback +import uuid +import hashlib +import xmltodict +import time +import logging +import requests +import os +import secrets +import random +import string +from calendar import monthrange +from decimal import Decimal +from datetime import date, datetime, timedelta +from django.utils import timezone +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.core.paginator import Paginator +from django.db import models, transaction, IntegrityError +from django.db.models import Q, Count, Sum, F, Exists, OuterRef +from django.db.models.functions import TruncDate, TruncMonth, TruncYear +from django.contrib.auth.hashers import make_password +from django.views.decorators.csrf import csrf_exempt +from django.utils.decorators import method_decorator + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.parsers import JSONParser, MultiPartParser, FormParser + +# 工具类导入 +from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss +from backend.utils import ( + update_dashou_daily_by_action, + update_shangjia_daily +) +from orders.utils import update_daily_dispatch_stat, update_daily_payout, sync_order_to_partners +from shop.utils import update_dianpu_daily_stat +from products.utils import update_shangpin_daily_stat +from orders.notice_tasks import dingdan_guangbo +from .utils import verify_kefu_permission + +# models 集中导入 +## backend +from backend.models import Role, Permission, RolePermission, UserRole, TixianRiTongji + +## users +from users.models import ( + UserMain, UserGuanshi, UserBoss, UserZuzhang, + UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan +) + +## orders +from orders.models import ( + Lilubiao, Dashoutupian, Chufajilu, PreSettlement, + OrderDashouHistory, Dingdan, Tuikuanjilu, CrossPlatformOrderData, + DingdanShangjia, Fadan, FadanShensuTupian, FadanFenhong +) + +## shop +from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi + +## products +from products.models import ( + Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan, + DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong +) + +## config +from config.models import ( + WithdrawConfig, ShangjiaLianjie, AccountPermission, ClubConfig, + Club, DailyDispatchStat, OrderTypeMapping, TixianQuotaDefault, + DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage +) + +## rank +from rank.models import ( + KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong, + YonghuChenghao, ShenheJilu +) + +# 序列化器 +from .serializers import PopupPageSerializer, PopupConfigSerializer, PopupImageSerializer + +# 全局常量、日志对象 +logger = logging.getLogger('houtai') +SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd'] + + + +class GetClubConfigView(APIView): + """ + 获取对方俱乐部配置列表(用于跨平台选择) + 权限要求:客服身份 + 权限代码1且开启 + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + """ + 处理 POST 请求,返回对方俱乐部配置数据(is_self=0) + """ + try: + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) + + # 5. 查询所有对方俱乐部(is_self=0),只取必要字段 + club_configs = ClubConfig.objects.filter( + is_self=0 # 只返回对方俱乐部 + ).only( + 'club_id', + 'club_nickname', + 'club_avatar', + 'storage_bucket_domain' # 如果前端需要存储桶域名,一并返回 + ) + + # 构造返回数据列表 + data_list = [] + for club in club_configs: + data_list.append({ + 'club_id': club.club_id, + 'club_nickname': club.club_nickname or club.club_id, # 若昵称为空,用ID代替 + 'club_avatar': club.club_avatar or '', # 可能为空 + 'storage_bucket_domain': club.storage_bucket_domain or '' # 可选 + }) + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': data_list + }) + + except Exception as e: + logger.error(f"获取俱乐部配置失败: {str(e)}") + return Response({ + 'code': 500, + 'msg': '服务器内部错误' + }) + + + + + +class GetCrossOrderListView(APIView): + """ + 获取跨平台订单列表(客服后台) + 权限要求:客服身份 + 权限代码1且开启 + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + """ + 处理 POST 请求,返回分页订单数据及统计信息 + """ + try: + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部订单'}) + + # 5. 获取请求参数 + data = request.data + + + + + # 分页参数 + page = int(data.get('page', 1)) + page_size = int(data.get('pageSize', 20)) + + # 查询参数 + leixing = data.get('leixing') # 订单类型ID + zhuangtai = data.get('zhuangtai') # 状态字符串,如 "1,7" + club_id = data.get('club_id') # 对方俱乐部ID(精确) + direction = data.get('direction') # send/receive/all + dingdan_id = data.get('dingdan_id') # 订单ID模糊搜索 + club_id_search = data.get('club_id_search') # 对方俱乐部ID模糊搜索 + date = data.get('date') # 日期 YYYY-MM-DD + dashou_id = data.get('dashou_id') # 打手ID(接单打手ID) + clkf = data.get('clkf') # 处理客服 + + # 6. 构建基础查询(跨平台订单) + base_q = Q(is_cross=1) + + # 添加筛选条件(非空且非全部) + if leixing and leixing != '': + base_q &= Q(leixing_id=leixing) + + if zhuangtai and zhuangtai != 'all': + # 解析逗号分隔的状态码 + status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip().isdigit()] + if status_list: + base_q &= Q(zhuangtai__in=status_list) + + if club_id and club_id != '': + base_q &= Q(partner_club_id=club_id) + + # 方向映射 + if direction == 'send': + base_q &= Q(dispatch_type=1) + elif direction == 'receive': + base_q &= Q(dispatch_type=2) + # direction == 'all' 时不加方向条件 + + if dingdan_id: + base_q &= Q(dingdan_id__icontains=dingdan_id) + + if club_id_search: + base_q &= Q(partner_club_id__icontains=club_id_search) + + if date: + # 按日期精确过滤(假设 create_time 是带时区的 DateTimeField) + base_q &= Q(create_time__date=date) + + if dashou_id: + base_q &= Q(jiedan_dashou_id=dashou_id) + + if clkf: + base_q &= Q(clkf__icontains=clkf) + + # 7. 统计各状态数量(基于相同过滤条件) + # 获取所有符合条件的订单的状态分组统计 + status_counts = Dingdan.objects.filter(base_q).values('zhuangtai').annotate(count=Count('id')).order_by() + + # 转换为字典 {状态码: 数量} + status_map = {item['zhuangtai']: item['count'] for item in status_counts} + + # 定义状态列表(与前端一致) + status_list_map = { + 'all': 0, # 稍后填充总数量 + '1,7': 0, + '2': 0, + '8': 0, + '3': 0, + '4': 0, + '5': 0, + '6': 0, + } + # 填充总数 + status_list_map['all'] = sum(status_map.values()) + # 填充单个状态 + for code, cnt in status_map.items(): + if code == 1 or code == 7: + status_list_map['1,7'] += cnt + elif code in status_list_map: + status_list_map[str(code)] = cnt + # 其他状态暂不处理 + + # 构建 stats 统计 + total_orders = status_list_map['all'] + completed_orders = status_map.get(3, 0) + refund_orders = status_map.get(5, 0) + pending_orders = status_map.get(8, 0) + status_map.get(4, 0) + + stats = { + 'totalOrders': total_orders, + 'completedOrders': completed_orders, + 'refundOrders': refund_orders, + 'pendingOrders': pending_orders, + } + + # 8. 分页获取订单列表 + # 按创建时间倒序,保证分页稳定 + orders_qs = Dingdan.objects.filter(base_q).order_by('-create_time') + + # 使用 Paginator 分页 + paginator = Paginator(orders_qs, page_size) + current_page = paginator.get_page(page) + order_list = current_page.object_list + + # 只返回前端需要的字段,减少数据传输 + data_list = [] + for order in order_list: + data_list.append({ + 'dingdan_id': order.dingdan_id, + 'nicheng': order.nicheng or '', + 'jieshao': order.jieshao or '', + 'jiage': float(order.jine) if order.jine else 0, + 'zhuangtai': order.zhuangtai, + }) + + # 9. 返回结果 + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'list': data_list, + 'total': total_orders, + 'statusCounts': status_list_map, + 'stats': stats, + } + }) + + except Exception as e: + logger.error(f"获取跨平台订单列表失败: {str(e)}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + +class GetCrossOrderDetailView(APIView): + """ + 客服获取跨平台订单详情接口 + 请求:POST /houtai/hqkptddxq + 参数:{"username": "客服手机号", "dingdan_id": "订单ID"} + 认证:JWT,权限码1 + 返回:code=0 + 订单详情数据(包含俱乐部信息、打手图片、处罚记录等) + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + start_time = time.time() + + # 1. 获取参数 + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + if not username or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + # 2. 身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部订单'}) + + # 4. 查询订单 + dingdan_obj = Dingdan.objects.select_related( + 'pingtai_kuozhan', + 'shangjia_kuozhan' + ).get(dingdan_id=dingdan_id) + + # 5. 构建基础响应数据 + response_data = { + 'dingdan_id': dingdan_obj.dingdan_id or '', + 'zhuangtai': dingdan_obj.zhuangtai or 0, + 'fadanpingtai': dingdan_obj.fadan_pingtai or 0, + 'dispatch_type': dingdan_obj.dispatch_type or 0, # 1=我方派单, 2=对方派单 + 'is_cross': dingdan_obj.is_cross or 0, + 'partner_order_id': dingdan_obj.partner_order_id or '', + 'partner_club_id': dingdan_obj.partner_club_id or '', + 'jine': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00, + 'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00, + 'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '', + 'dashou_liuyan': dingdan_obj.dashou_liuyan or '', + 'zhiding_id': dingdan_obj.zhiding_id or '', + 'shangpin_id': dingdan_obj.shangpin_id or '', + 'leixing': dingdan_obj.leixing_id or 0, + 'tupian_url': dingdan_obj.tupian or '', + 'jieshao': dingdan_obj.jieshao or '', + 'beizhu': dingdan_obj.beizhu or '', + 'tkly': dingdan_obj.tkly or '', + 'nicheng': dingdan_obj.nicheng or '', + 'clkf': dingdan_obj.clkf or '', + 'chuangjianshijian': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.create_time else '', + 'genggaishijian': dingdan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.update_time else '', + } + + # 6. 扩展表数据 + if dingdan_obj.fadan_pingtai == 1: + if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan: + response_data['pingtai_kuozhan'] = { + 'laoban_id': dingdan_obj.pingtai_kuozhan.laoban_id or '', + 'laoban_pingjia': dingdan_obj.pingtai_kuozhan.laoban_pingjia or '', + } + else: + response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''} + elif dingdan_obj.fadan_pingtai == 2: + if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan: + shangjia = dingdan_obj.shangjia_kuozhan + response_data['shangjia_kuozhan'] = { + 'shangjia_id': shangjia.shangjia_id or '', + 'sjnicheng': shangjia.sjnicheng or '', + 'shangjia_pingjia': shangjia.shangjia_pingjia or '', + 'cfliyou': shangjia.cfliyou or '', + 'sqzhuangtai': shangjia.sqzhuangtai if shangjia.sqzhuangtai is not None else 0, + 'bhliyou': shangjia.bhliyou or '', + } + else: + response_data['shangjia_kuozhan'] = { + 'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '', + 'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': '' + } + + # ========== 核心逻辑:判断派单/接单关系 ========== + dispatch_type = dingdan_obj.dispatch_type # 1=我方派单, 2=对方派单 + is_cross = dingdan_obj.is_cross + partner_club_id = dingdan_obj.partner_club_id + partner_order_id = dingdan_obj.partner_order_id + + # 获取我方俱乐部配置(用于本地存储桶域名) + my_club_config = ClubConfig.objects.filter(is_self=1).first() + my_storage_domain = my_club_config.storage_bucket_domain if my_club_config else '' + + # 初始化字段 + response_data['club_info'] = None # 对方俱乐部信息 + response_data['partner_images'] = [] # 打手提交图片(对方平台打手或我方打手) + response_data['punishment_info'] = {} # 处罚信息 + response_data['is_punished'] = False # 是否已处罚(用于前端禁用按钮) + response_data['receive_side'] = '' # 接单方:'self' 我方接单,'partner' 对方接单 + response_data['dispatch_side'] = '' # 派单方:'self' 我方派单,'partner' 对方派单 + + # 设置派单方 + if dispatch_type == 1: + response_data['dispatch_side'] = 'self' + elif dispatch_type == 2: + response_data['dispatch_side'] = 'partner' + else: + # 兼容旧数据:根据 partner_club_id 或 partner_order_id 推断 + if partner_club_id or partner_order_id: + response_data['dispatch_side'] = 'partner' + else: + response_data['dispatch_side'] = 'self' + + # 情况1:对方派单(对方平台发单,我方打手接单) + if response_data['dispatch_side'] == 'partner': + # 接单方为我方 + response_data['receive_side'] = 'self' + + # 获取对方俱乐部信息(用于展示) + if partner_club_id: + partner_config = ClubConfig.objects.filter(club_id=partner_club_id, is_self=0).first() + if partner_config: + response_data['club_info'] = { + 'club_id': partner_config.club_id, + 'club_nickname': partner_config.club_nickname or partner_config.club_id, + 'club_avatar': partner_config.club_avatar or '', + } + else: + response_data['club_info'] = {'club_id': partner_club_id, 'club_nickname': '', 'club_avatar': ''} + else: + response_data['club_info'] = None + + # 查询我方打手提交的图片(本地) + dashou_images = Dashoutupian.objects.filter( + dingdan_id=dingdan_id, + dashou=dingdan_obj.jiedan_dashou_id + ).values_list('tupian', flat=True) + images = [] + for rel_path in dashou_images: + if rel_path: + if rel_path.startswith('http'): + images.append(rel_path) + else: + images.append(my_storage_domain + rel_path) + response_data['partner_images'] = images + + # 查询我方打手处罚记录 + punish_records = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + dashouid=dingdan_obj.jiedan_dashou_id + ).order_by('-create_time') + if punish_records.exists(): + latest = punish_records.first() + response_data['punishment_info'] = { + 'cfliyou': latest.cfliyou or '', + 'sqzhuangtai': latest.sqzhuangtai or 0, + 'bhliyou': latest.bhliyou or '', + 'ssliyou': latest.ssliyou or '', + 'jifen': latest.jifen or 0, + } + response_data['is_punished'] = True + else: + response_data['punishment_info'] = {} + response_data['is_punished'] = False + + # 情况2:我方派单(我方平台发单) + else: + response_data['dispatch_side'] = 'self' + # 根据是否有对方订单ID/俱乐部ID判断接单方 + if partner_club_id or partner_order_id: + # 有对方信息,说明订单派给了对方平台,对方接单 + response_data['receive_side'] = 'partner' + + # 获取对方俱乐部信息 + if partner_club_id: + partner_config = ClubConfig.objects.filter(club_id=partner_club_id, is_self=0).first() + if partner_config: + response_data['club_info'] = { + 'club_id': partner_config.club_id, + 'club_nickname': partner_config.club_nickname or partner_config.club_id, + 'club_avatar': partner_config.club_avatar or '', + } + else: + response_data['club_info'] = {'club_id': partner_club_id, 'club_nickname': '', 'club_avatar': ''} + + # 需要调用对方接口获取图片和处罚记录 + if partner_club_id and partner_order_id: + # 获取对方服务器域名 + if my_club_config: + self_club_id = my_club_config.club_id + else: + logger.error(f"无法获取本俱乐部ID,订单ID: {dingdan_id}") + return Response({'code': 500, 'msg': '系统配置错误,请联系管理员'}) + + try: + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain + except ObjectDoesNotExist: + logger.error(f"未找到合作关系: {self_club_id} <-> {partner_club_id}") + return Response({'code': 500, 'msg': '未找到对方平台配置'}) + + # 调用对方接口,最多重试3次 + url = partner_domain.rstrip('/') + '/houtai/kpthqtp' + payload = { + 'order_id': partner_order_id, + 'club_id': self_club_id, + } + timeout = 3 + retries = 3 + success = False + for attempt in range(retries): + try: + resp = requests.post(url, json=payload, timeout=timeout) + if resp.status_code == 200: + resp_json = resp.json() + if resp_json.get('code') == 0: + data = resp_json.get('data', {}) + response_data['partner_images'] = data.get('images', []) + response_data['punishment_info'] = data.get('punishment', {}) + response_data['is_punished'] = bool(response_data['punishment_info'].get('cfliyou')) + success = True + break + else: + logger.warning(f"对方接口返回错误 (尝试{attempt+1}): {resp_json.get('msg')}") + else: + logger.warning(f"对方接口HTTP错误 (尝试{attempt+1}): {resp.status_code}") + except requests.exceptions.Timeout: + logger.warning(f"请求对方接口超时 (尝试{attempt+1}): {url}") + except Exception as e: + logger.warning(f"请求对方接口异常 (尝试{attempt+1}): {str(e)}") + if not success: + # 三次失败,返回部分数据,但提示前端对方数据获取失败 + response_data['partner_images'] = [] + response_data['punishment_info'] = {} + response_data['is_punished'] = False + response_data['partner_data_error'] = '获取对方平台数据失败,请稍后重试' + else: + # 对方信息不完整,无法获取数据 + response_data['partner_images'] = [] + response_data['punishment_info'] = {} + response_data['is_punished'] = False + response_data['partner_data_error'] = '对方订单信息不完整' + else: + # 无对方信息,说明我方派单且我方接单 + response_data['receive_side'] = 'self' + # 我方打手提交图片(本地) + dashou_images = Dashoutupian.objects.filter( + dingdan_id=dingdan_id, + dashou=dingdan_obj.jiedan_dashou_id + ).values_list('tupian', flat=True) + images = [] + for rel_path in dashou_images: + if rel_path: + if rel_path.startswith('http'): + images.append(rel_path) + else: + images.append(my_storage_domain + rel_path) + response_data['partner_images'] = images + + # 处罚记录 + punish_records = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + dashouid=dingdan_obj.jiedan_dashou_id + ).order_by('-create_time') + if punish_records.exists(): + latest = punish_records.first() + response_data['punishment_info'] = { + 'cfliyou': latest.cfliyou or '', + 'sqzhuangtai': latest.sqzhuangtai or 0, + 'bhliyou': latest.bhliyou or '', + 'ssliyou': latest.ssliyou or '', + 'jifen': latest.jifen or 0, + } + response_data['is_punished'] = True + else: + response_data['punishment_info'] = {} + response_data['is_punished'] = False + + # 如果是我方派单且我方接单,对方俱乐部信息为空 + response_data['club_info'] = None + + # 7. 查询退款理由 + try: + tuikuan_record = Tuikuanjilu.objects.filter( + dingdan_id=dingdan_id + ).order_by('-create_time').first() + response_data['tuikuan_liyou'] = tuikuan_record.liyou if tuikuan_record else '' + except Exception as e: + logger.warning(f"查询退款记录失败: {str(e)}") + response_data['tuikuan_liyou'] = '' + + # ========== 新增:查询更换打手生成的新订单信息 ========== + try: + new_lianjie = ShangjiaLianjie.objects.filter( + previous_dingdan_id=dingdan_id + ).order_by('-create_time').first() + if new_lianjie: + response_data['new_dingdan_id'] = new_lianjie.dingdan_id or '' + response_data['new_lianjie'] = new_lianjie.lianjie or '' + else: + response_data['new_dingdan_id'] = '' + response_data['new_lianjie'] = '' + except Exception as e: + logger.warning(f"查询新订单链接失败: {str(e)}") + response_data['new_dingdan_id'] = '' + response_data['new_lianjie'] = '' + # ===================================================== + + elapsed = time.time() - start_time + logger.info(f"获取跨平台订单详情接口耗时: {elapsed:.3f}s, 订单ID: {dingdan_id}") + return Response({'code': 0, 'data': response_data}) + + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {dingdan_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + except Exception as e: + logger.error(f"获取跨平台订单详情接口异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + +class CrossPlatformRefundView(APIView): + """ + 跨平台订单退款接口(同时支持本地订单和跨平台订单) + 请求:POST /houtai/kptk + 参数:{ + "username": "客服账号ID", + "dingdan_id": "订单ID", + "tuikuan_liyou": "退款理由(可选)" + } + 认证:JWT + 返回:code=0 成功,其他失败 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() + + if not username or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权处理订单'}) + + # 4. 查询订单(带扩展表) + try: + order = Dingdan.objects.select_related( + 'pingtai_kuozhan', + 'shangjia_kuozhan' + ).get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {dingdan_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + + # 5. 订单状态校验(只有 1,7,2,8,4 可退款) + allowed_statuses = [1, 7, 2, 8, 4] + if order.zhuangtai not in allowed_statuses: + logger.warning(f"订单状态不允许退款: {order.zhuangtai}") + return Response({'code': 400, 'msg': '订单状态不允许退款'}) + + # 6. 根据派单方向分流 + dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单 + + # ---------- 情况一:对方派单(我方打手接对方的单)---------- + if dispatch_type == 2: + # 对方派单,资金在对方平台,我方只处理打手状态和订单状态 + with transaction.atomic(): + # 更新订单状态为已退款 + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + + # 更新打手数据(接单打手) + dashou_id = order.jiedan_dashou_id + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 # 设为空闲 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"对方派单退款:打手 {dashou_id} 不存在,跳过更新") + + # 更新退款记录(记录操作) + try: + refund_record, created = Tuikuanjilu.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'dashouid': dashou_id or '', + 'qingqiuid': '', + 'chuliid': current_user.phone, + 'liyou': tuikuan_liyou or '客服退款(对方派单)', + 'sqzhuangtai': 1, + 'jine': order.jine or 0, + 'dashou_fencheng': order.dashou_fencheng or 0, + 'jieshao': order.jieshao or '', + 'beizhu': '对方派单退款', + 'nicheng': order.nicheng or '', + } + ) + if not created: + refund_record.sqzhuangtai = 1 + refund_record.chuliid = current_user.phone + if tuikuan_liyou: + refund_record.liyou = tuikuan_liyou + refund_record.save() + except Exception as e: + logger.error(f"更新退款记录失败: {str(e)}") + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=3 # 3 表示退款(退款) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + + + # ✅ 新方法(行为驱动,更安全) + shangjia_ext = order.shangjia_kuozhan + shangjia_id = shangjia_ext.shangjia_id + update_shangjia_daily( + yonghuid=shangjia_id, + amount=Decimal(str(order.jine)), + action=3 # 3 = 退款 + ) + + # 更新客服统计 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + logger.info(f"对方派单退款成功,订单 {dingdan_id}") + return Response({'code': 0, 'msg': '退款成功'}) + + # ---------- 情况二:我方派单(订单由我方发出)---------- + # 需要进一步判断是本地订单还是跨平台订单 + is_cross = order.is_cross + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + + # 子情况2.1:本地订单(非跨平台) + if not is_cross or not partner_club_id: + # 按原有逻辑处理:平台订单或商家订单 + if order.fadan_pingtai == 1: + # 平台订单退款(调用微信支付) + return self._refund_platform_order(order, current_user, tuikuan_liyou, kefu) + elif order.fadan_pingtai == 2: + # 商家订单退款(直接退余额) + return self._refund_merchant_order(order, current_user, tuikuan_liyou, kefu) + else: + return Response({'code': 400, 'msg': '未知的发单平台类型'}) + + # 子情况2.2:跨平台订单(我方派单给对方) + else: + # 必须先执行本地资金退款(因为钱在我方平台) + # 根据发单平台类型执行退款 + local_refund_success = False + try: + with transaction.atomic(): + if order.fadan_pingtai == 1: + # 平台订单:调用微信退款 + refund_result = self._call_wechat_refund(order) + if refund_result['code'] != 0: + raise Exception(refund_result['msg']) + # 微信退款成功,更新订单状态 + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + local_refund_success = True + elif order.fadan_pingtai == 2: + # 商家订单:退余额给商家 + self._refund_to_merchant(order, current_user.phone, tuikuan_liyou) + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + local_refund_success = True + else: + return Response({'code': 400, 'msg': '未知的发单平台类型'}) + + # 更新打手数据(接单打手) + dashou_id = order.jiedan_dashou_id + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"跨平台退款:打手 {dashou_id} 不存在,跳过更新") + + # 更新退款记录 + self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou) + + # 更新客服统计 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + except Exception as e: + logger.error(f"跨平台订单本地退款失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': f'本地退款失败: {str(e)}'}) + + # 本地退款成功后,通知对方平台 + if local_refund_success: + # 查询对方平台域名 + try: + # 获取我方俱乐部ID + my_club_config = ClubConfig.objects.filter(is_self=1).first() + if not my_club_config: + logger.error("未配置我方俱乐部信息") + # 本地已成功,仅记录错误,不影响返回 + else: + self_club_id = my_club_config.club_id + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain + + # 调用对方退款通知接口 + notify_url = partner_domain.rstrip('/') + '/houtai/dfddtk' + payload = { + 'order_id': partner_order_id, # 对方订单ID + 'club_id': self_club_id, # 我方俱乐部ID + 'status': 5, # 通知对方我方已退款 + 'reason': tuikuan_liyou + } + # 设置超时3秒,不阻塞主流程 + try: + resp = requests.post(notify_url, json=payload, timeout=3) + if resp.status_code == 200: + resp_json = resp.json() + if resp_json.get('code') == 0: + # 更新跨平台订单扩展表 + cross_data, _ = CrossPlatformOrderData.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id, + 'partner_order_status': resp_json.get('data', {}).get('partner_status', 0), + 'partner_dashou_id': resp_json.get('data', {}).get('partner_dashou_id', ''), + 'partner_amount': order.jine or 0, + } + ) + if not _: + cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0) + cross_data.save() + else: + logger.warning(f"对方平台返回错误: {resp_json.get('msg')}") + else: + logger.warning(f"对方平台接口HTTP错误: {resp.status_code}") + except requests.exceptions.Timeout: + logger.error(f"通知对方平台超时: {notify_url}") + except Exception as e: + logger.error(f"通知对方平台异常: {str(e)}") + except Exception as e: + logger.error(f"查询对方平台配置失败: {str(e)}") + + return Response({'code': 0, 'msg': '退款成功'}) + + # ========== 辅助方法 ========== + def _refund_platform_order(self, order, current_user, tuikuan_liyou, kefu): + """平台订单退款(调用微信支付)""" + # 调用微信退款 + refund_result = self._call_wechat_refund(order) + if refund_result['code'] != 0: + return Response({'code': 400, 'msg': refund_result['msg']}) + + with transaction.atomic(): + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + + # 更新打手 + dashou_id = order.jiedan_dashou_id + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"平台退款:打手 {dashou_id} 不存在,跳过更新") + + # 更新退款记录 + self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou) + + is_cross = order.is_cross + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + + if not is_cross or not partner_order_id: + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=3 # 3 表示退款(退款) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + else: + pass + + # 客服统计 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + logger.info(f"平台订单退款成功: {order.dingdan_id}") + return Response({'code': 0, 'msg': '退款成功'}) + + def _refund_merchant_order(self, order, current_user, tuikuan_liyou, kefu): + """商家订单退款(退余额给商家)""" + # 获取商家ID + try: + shangjia_ext = order.shangjia_kuozhan + shangjia_id = shangjia_ext.shangjia_id + except ObjectDoesNotExist: + return Response({'code': 400, 'msg': '订单商家信息不存在'}) + + with transaction.atomic(): + # 订单状态改已退款 + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + + # 退余额给商家 + try: + shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) + shangjia_profile = shangjia_user.shop_profile + shangjia_profile.tuikuan += 1 + shangjia_profile.yue += order.jine or 0 + shangjia_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.error(f"商家退款:商家 {shangjia_id} 不存在,无法退余额") + raise Exception("商家账户不存在") + + # 更新打手 + dashou_id = order.jiedan_dashou_id + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"商家退款:打手 {dashou_id} 不存在,跳过更新") + + # 更新退款记录 + self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou) + + + is_cross = order.is_cross + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + + if not is_cross or not partner_order_id: + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=3 # 3 表示退款(退款) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + else: + pass + + # ✅ 新方法(行为驱动,更安全) + update_shangjia_daily( + yonghuid=shangjia_id, + amount=Decimal(str(order.jine)), + action=3 # 3 = 退款 + ) + + # 客服统计 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + logger.info(f"商家订单退款成功: {order.dingdan_id}") + return Response({'code': 0, 'msg': '退款成功'}) + + def _call_wechat_refund(self, order): + """调用微信支付退款接口,返回 {'code':0, 'msg':'success'} 或错误信息""" + # 获取微信支付配置 + appid = getattr(settings, 'WEIXIN_APPID', '') + mch_id = getattr(settings, 'WEIXIN_MCHID', '') + key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') + cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '') + key_path = getattr(settings, 'WEIXIN_KEY_PATH', '') + + if not all([appid, mch_id, key, cert_path, key_path]): + missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v] + logger.error(f"微信支付配置缺失: {missing}") + return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'} + + # 生成退款单号 + out_refund_no = self._generate_refund_no(order.dingdan_id) + total_fee = int(float(order.jine or 0) * 100) + refund_fee = total_fee + refund_desc = '客服退款' + + params = { + 'appid': appid, + 'mch_id': mch_id, + 'nonce_str': self._generate_nonce_str(), + 'out_refund_no': out_refund_no, + 'total_fee': total_fee, + 'refund_fee': refund_fee, + 'refund_desc': refund_desc, + } + # 优先使用微信交易号 + if order.wechat_transaction_id: + params['transaction_id'] = order.wechat_transaction_id + else: + params['out_trade_no'] = order.dingdan_id + + # 签名 + sorted_keys = sorted(params.keys()) + stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys]) + stringSignTemp = f"{stringA}&key={key}" + sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() + params['sign'] = sign + + xml_data = self._dict_to_xml(params) + url = 'https://api.mch.weixin.qq.com/secapi/pay/refund' + + try: + response = requests.post(url, data=xml_data, cert=(cert_path, key_path), timeout=10) + result = xmltodict.parse(response.content)['xml'] + logger.info(f"微信退款响应: {result}") + except Exception as e: + logger.error(f"微信退款请求异常: {str(e)}", exc_info=True) + return {'code': 500, 'msg': '微信退款请求异常'} + + if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': + return {'code': 0, 'msg': 'success'} + else: + err_msg = result.get('err_code_des', result.get('return_msg', '未知错误')) + logger.warning(f"微信退款失败: {err_msg}") + return {'code': 400, 'msg': err_msg} + + def _refund_to_merchant(self, order, clkf_phone, tuikuan_liyou): + """给商家退余额(事务内调用,不再独立事务)""" + try: + shangjia_ext = order.shangjia_kuozhan + shangjia_id = shangjia_ext.shangjia_id + except ObjectDoesNotExist: + raise Exception("订单商家信息不存在") + + try: + shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) + shangjia_profile = shangjia_user.shop_profile + shangjia_profile.tuikuan += 1 + shangjia_profile.yue += order.jine or 0 + shangjia_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.error(f"商家退款:商家 {shangjia_id} 不存在") + raise Exception("商家账户不存在") + + def _update_refund_record(self, order, dashou_id, chuli_phone, tuikuan_liyou): + """更新退款记录表""" + try: + #更新支出记录 + if order.fadan_pingtai == 1: + update_daily_payout(order.jine) + + # ========== 新增:商品和店铺每日统计 ========== + try: + # 商品每日统计(只要有商品ID就执行) + if order.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(order, 'pingtai_kuozhan'): + pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=order.shangpin_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + dianpu_id = None + if hasattr(order, 'pingtai_kuozhan'): + dianpu_id = order.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + + refund_record, created = Tuikuanjilu.objects.get_or_create( + dingdan_id=order.dingdan_id, + defaults={ + 'dashouid': dashou_id or '', + 'qingqiuid': '', + 'chuliid': chuli_phone, + 'liyou': tuikuan_liyou or '客服退款', + 'sqzhuangtai': 1, + 'jine': order.jine or 0, + 'dashou_fencheng': order.dashou_fencheng or 0, + 'jieshao': order.jieshao or '', + 'beizhu': '客服退款', + 'nicheng': order.nicheng or '', + } + ) + if not created: + refund_record.sqzhuangtai = 1 + refund_record.chuliid = chuli_phone + if tuikuan_liyou: + refund_record.liyou = tuikuan_liyou + refund_record.save() + except Exception as e: + logger.error(f"更新退款记录异常: {str(e)}") + + def _generate_nonce_str(self): + return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + def _generate_refund_no(self, dingdan_id): + timestamp = str(int(time.time())) + rand = ''.join(random.choices('0123456789', k=4)) + return f"{dingdan_id}REF{timestamp}{rand}" + + def _dict_to_xml(self, data): + xml = [''] + for k, v in data.items(): + xml.append(f'<{k}>{v}') + xml.append('') + return ''.join(xml) + + + + +class PartnerGetOrderDataView(APIView): + """ + 对方平台调用,获取我方订单的打手图片和处罚记录 + 请求:POST /houtai/kpthqtp + 参数:{ + "order_id": "对方平台的订单ID", + "club_id": "对方俱乐部ID" + } + 无需认证 + 返回:code=0 + images列表 + punishment信息 + """ + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + # token = request.data.get('token') + # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): + # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200) + try: + # 1. 获取参数 + partner_order_id = request.data.get('order_id', '').strip() + partner_club_id = request.data.get('club_id', '').strip() + if not partner_order_id or not partner_club_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 查询我方存储桶域名(自己俱乐部的配置) + my_club_config = ClubConfig.objects.filter(is_self=1).first() + if not my_club_config: + logger.error("未配置我方俱乐部存储桶域名") + return Response({'code': 500, 'msg': '系统配置错误'}) + storage_domain = my_club_config.storage_bucket_domain or '' + + # 3. 查询我方订单(通过对方订单ID和对方俱乐部ID) + try: + order = Dingdan.objects.get( + dingdan_id=partner_order_id, + partner_club_id=partner_club_id + ) + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: partner_order_id={partner_order_id}, partner_club_id={partner_club_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + + dingdan_id = order.dingdan_id + dashou_id = order.jiedan_dashou_id + + # 4. 查询打手图片(绝对URL) + images = [] + if dashou_id: + dashou_images = Dashoutupian.objects.filter( + dingdan_id=dingdan_id, + dashou=dashou_id + ).values_list('tupian', flat=True) + for rel_path in dashou_images: + if rel_path: + if rel_path.startswith('http'): + images.append(rel_path) + else: + images.append(storage_domain + rel_path) + + # 5. 查询处罚记录(最新一条) + punishment = { + 'cfliyou': '', + 'sqzhuangtai': 0, + 'bhliyou': '', + 'ssliyou': '', + 'jifen': 0 + } + if dashou_id: + punish_record = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + dashouid=dashou_id + ).order_by('-create_time').first() + if punish_record: + punishment = { + 'cfliyou': punish_record.cfliyou or '', + 'sqzhuangtai': punish_record.sqzhuangtai or 0, + 'bhliyou': punish_record.bhliyou or '', + 'ssliyou': punish_record.ssliyou or '', + 'jifen': punish_record.jifen or 0, + } + + # 6. 返回数据 + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'images': images, + 'punishment': punishment + } + }) + + except Exception as e: + logger.error(f"对方获取订单数据接口异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + +# ========== 新增:对方平台退款通知接口 ========== +class PartnerRefundNotifyView(APIView): + """ + 接收对方平台推送的订单状态变更通知(完成/退款),同步更新我方数据。 + 请求:POST /houtai/dfddtk + 参数: + { + "order_id": "对方平台订单ID(实际为我方订单ID)", + "club_id": "对方俱乐部ID", + "status": 3 或 5, + "reason": "理由(可选)" + } + 返回:code=0 + data包含我方订单当前状态和打手ID + + 核心职责: + 1. 更新跨平台订单扩展表 + 2. 对于对方派单(我方打手接对方的单): + a. 状态3(完成)→ 更新订单为已完成,创建预结算(如无) + b. 状态5(退款)→ 更新订单为已退款,将预结算待结算改为已拒绝 + c. 在此过程中,若订单原状态为“进行中(2)”,则将打手状态重置为空闲(1) + """ + + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + # token = request.data.get('token') + # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): + # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200) + # 1. 参数校验(事务外执行) + partner_order_id = request.data.get('order_id', '').strip() + partner_club_id = request.data.get('club_id', '').strip() + partner_status = request.data.get('status') + reason = request.data.get('reason', '').strip() + + if not partner_order_id or not partner_club_id: + return Response({'code': 400, 'msg': '参数不完整'}) + if partner_status not in (3, 5): + return Response({'code': 400, 'msg': '不支持的状态码'}) + + # 2. 核心业务在事务内执行 + try: + with transaction.atomic(): + # 2.1 锁定订单行(必须事务内) + try: + order = Dingdan.objects.select_for_update().get( + dingdan_id=partner_order_id, + partner_club_id=partner_club_id + ) + except Dingdan.DoesNotExist: + logger.warning(f"通知订单不存在: order_id={partner_order_id}, club_id={partner_club_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + + my_dingdan_id = order.dingdan_id + dashou_id = order.jiedan_dashou_id or '' + + # 记录原订单状态,用于后续打手状态重置 + original_order_status = order.zhuangtai + + # 2.2 更新跨平台扩展表(记录对方状态) + cross_data, created = CrossPlatformOrderData.objects.get_or_create( + dingdan_id=my_dingdan_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id, + 'partner_order_status': partner_status, + 'partner_dashou_id': '', + 'partner_amount': order.jine or 0, + } + ) + if not created: + cross_data.partner_order_status = partner_status + cross_data.save(update_fields=['partner_order_status', 'update_time']) + + # 2.3 仅处理对方派单(dispatch_type == 2) + if order.dispatch_type == 2: + # ========== 打手状态恢复逻辑 ========== + # 如果原订单状态为“进行中(2)”,且存在接单打手,则将打手状态改为空闲(1) + if original_order_status == 2 and dashou_id: + try: + # 通过打手ID查用户主表,再反向获取打手扩展表 + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.zhuangtai = 1 # 状态置为空闲 + dashou_profile.save(update_fields=['zhuangtai']) + logger.info(f"打手 {dashou_id} 状态已重置为1(空闲)") + except UserMain.DoesNotExist: + logger.warning(f"打手用户不存在: {dashou_id}") + except UserDashou.DoesNotExist: + logger.warning(f"打手扩展信息不存在: {dashou_id}") + # 🔥 新增:调用打手每日统计(成交) + if partner_status ==3: + + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=2 # 3 表示退款(退款) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + if partner_status ==5: + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=3 # 3 表示退款(退款) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + + # ========== 情况A:对方完成(status=3) ========== + if partner_status == 3: + order.zhuangtai = 3 + + order.clkf = partner_club_id # 处理人(对方俱乐部ID) + order.save(update_fields=['zhuangtai', 'update_time','clkf']) + + if dashou_id: + yjs, yjs_created = PreSettlement.objects.get_or_create( + dingdan_id=my_dingdan_id, + dashou_id=dashou_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id, + 'amount': order.dashou_fencheng or 0, + 'order_amount': order.jine or 0, + 'status': 0, # 待结算 + 'operator': '', + } + ) + if yjs_created: + logger.info(f"预结算创建成功: {my_dingdan_id}, 打手 {dashou_id}, 金额 {order.dashou_fencheng}") + else: + logger.info(f"预结算已存在,跳过: {my_dingdan_id}, 打手 {dashou_id}") + else: + logger.warning(f"订单 {my_dingdan_id} 无接单打手,跳过预结算创建") + + # ========== 情况B:对方退款(status=5) ========== + elif partner_status == 5: + order.zhuangtai = 5 + order.tkly = reason if reason else '' # 退款理由 + order.clkf = partner_club_id # 处理人(对方俱乐部ID) + order.save(update_fields=['zhuangtai', 'update_time','clkf','tkly']) + + if dashou_id: + try: + yjs = PreSettlement.objects.get( + dingdan_id=my_dingdan_id, + dashou_id=dashou_id + ) + if yjs.status == 0: # 待结算 → 已拒绝 + yjs.status = 2 + yjs.process_time = timezone.now() + yjs.save(update_fields=['status', 'process_time', 'update_time']) + logger.info(f"预结算状态已改为已拒绝: {my_dingdan_id}") + except PreSettlement.DoesNotExist: + logger.info(f"订单 {my_dingdan_id} 无预结算记录,无需修改") + + logger.info(f"对方通知处理完成: dingdan_id={my_dingdan_id}, partner_status={partner_status}") + + # 返回结果 + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'partner_status': order.zhuangtai, + 'partner_dashou_id': dashou_id + } + }) + + except Exception as e: + logger.error(f"对方通知接口异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + +class CrossPlatformForceCompleteView(APIView): + """ + 跨平台订单强制结单接口 + 请求:POST /houtai/kpqzjd + 参数:{"username": "客服账号ID", "dingdan_id": "订单ID"} + 认证:JWT + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + if not username or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查处理订单'}) + + # 4. 查询订单并加锁 + try: + with transaction.atomic(): + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + + # 5. 校验订单状态必须为8(结算中) + if order.zhuangtai not in (2,8): + return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'}) + + # 获取关键字段 + dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单 + is_cross = order.is_cross # 0=本地,1=跨平台 + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + dashou_id = order.jiedan_dashou_id + dashou_fencheng = order.dashou_fencheng or Decimal('0') + jine = order.jine or Decimal('0') + fadan_pingtai = order.fadan_pingtai # 1=平台,2=商家 + + # ========== 修复点1:完善跨平台判断 ========== + # 跨平台且被对方打手接单的条件:is_cross==1 且 (partner_club_id 或 partner_order_id 有值) + is_cross_claimed_by_partner = (is_cross == 1 and (partner_club_id or partner_order_id)) + # 如果是跨平台订单但两个字段都为空,则按本地订单处理(即自家打手接单) + + # ========== 修复点2:根据方向处理 ========== + # ---------- 对方派单(我方打手接对方单)---------- + if dispatch_type == 2: + # 创建预结算记录(如果已存在则更新) + pre_settlement, created = PreSettlement.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id, + 'dashou_id': dashou_id, + 'amount': dashou_fencheng, + 'order_amount': jine, + 'status': 0, # 待结算 + 'operator': current_user.phone, + } + ) + if not created: + pre_settlement.partner_order_id = partner_order_id + pre_settlement.partner_club_id = partner_club_id + pre_settlement.dashou_id = dashou_id + pre_settlement.amount = dashou_fencheng + pre_settlement.order_amount = jine + pre_settlement.status = 0 + pre_settlement.process_time = None + pre_settlement.operator = current_user.phone + pre_settlement.save() + + # 更新订单状态 + order.zhuangtai = 3 + order.clkf = current_user.phone + order.save() + + # 更新每日统计表(对方派单方向,使用订单金额) + if partner_club_id: + # 修复点3:使用公共统计函数 + update_daily_dispatch_stat( + direction=2, + partner_club_id=partner_club_id, + success_amount=jine, + success_count=1 + ) + else: + # 如果没有俱乐部ID,使用默认000001 + update_daily_dispatch_stat( + direction=2, + partner_club_id='000001', + success_amount=jine, + success_count=1 + ) + + # 通知对方平台订单已完成(修复点4:移到事务外) + if partner_club_id and partner_order_id: + transaction.on_commit( + lambda: self._notify_partner_order_status( + partner_club_id=partner_club_id, + partner_order_id=partner_order_id, + new_status=3, + current_user=current_user + ) + ) + + # ---------- 我方派单 ---------- + elif dispatch_type == 1: + # 判断是否为跨平台订单且被对方打手接单(即 partner_club_id 或 partner_order_id 有值) + if is_cross_claimed_by_partner: + # 跨平台订单:我方派单给外平台打手 + # 修复点5:处理只有 partner_order_id 没有 partner_club_id 的情况 + real_partner_club_id = partner_club_id or '000001' + + # 更新订单状态 + order.zhuangtai = 3 + order.clkf = current_user.phone + order.save() + + # 更新每日统计表(我方派单方向,使用打手分成金额) + update_daily_dispatch_stat( + direction=1, + partner_club_id=real_partner_club_id, + success_amount=dashou_fencheng, + success_count=1 + ) + + + # ========== 新增:商品和店铺每日统计 ========== + if order.fadan_pingtai == 1: + try: + # 商品每日统计(只要有商品ID就执行) + if order.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(order, 'pingtai_kuozhan'): + pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=order.shangpin_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + dianpu_id = None + if hasattr(order, 'pingtai_kuozhan'): + dianpu_id = order.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + # 通知对方平台订单已完成(异步) + if partner_order_id: + transaction.on_commit( + lambda: self._notify_partner_order_status( + partner_club_id=real_partner_club_id, + partner_order_id=partner_order_id, + new_status=3, + current_user=current_user + ) + ) + else: + # 本地订单(非跨平台 或 跨平台但实际是自家打手接单),按原有逻辑给自家打手结算 + if not dashou_id: + return Response({'code': 400, 'msg': '订单无接单打手'}) + if dashou_fencheng <= 0: + return Response({'code': 400, 'msg': '打手分成金额无效'}) + + # ========== 新增:商品和店铺每日统计 ========== + if order.fadan_pingtai == 1: + try: + # 商品每日统计(只要有商品ID就执行) + if order.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(order, 'pingtai_kuozhan'): + pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=order.shangpin_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + dianpu_id = None + if hasattr(order, 'pingtai_kuozhan'): + dianpu_id = order.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + # 更新打手余额和统计 + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.chengjiaozongliang += 1 + dashou_profile.yue += dashou_fencheng + dashou_profile.zonge += dashou_fencheng + dashou_profile.jinrishouyi += dashou_fencheng + dashou_profile.jinyueshouyi += dashou_fencheng + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"本地订单结算:打手 {dashou_id} 不存在,跳过") + + # 如果是商家订单,更新商家成交单量 + if fadan_pingtai == 2: + try: + shangjia_ext = order.shangjia_kuozhan + if shangjia_ext and shangjia_ext.shangjia_id: + shangjia_user = UserMain.objects.get(yonghuid=shangjia_ext.shangjia_id) + shangjia_profile = shangjia_user.shop_profile + shangjia_profile.chengjiao += 1 + shangjia_profile.save() + except Exception as e: + logger.warning(f"商家订单更新失败: {str(e)}") + + # 更新订单状态 + order.zhuangtai = 3 + order.clkf = current_user.phone + order.save() + + # 本地订单不涉及跨平台统计,无需更新 DailyDispatchStat + + else: + return Response({'code': 400, 'msg': '未知的派单方向'}) + + + if dispatch_type == 1 and is_cross_claimed_by_partner: + pass + # 判断是否为跨平台订单且被对方打手接单(即 partner_club_id 或 partner_order_id 有值) + elif dispatch_type == 2: + pass + else: + + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=dashou_fencheng, + action=2 # 2 表示成交(结算) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + + if order.fadan_pingtai == 2: + shangjia_ext = order.shangjia_kuozhan + # ✅ 新方法(行为驱动,更安全) + update_shangjia_daily( + yonghuid=shangjia_ext.shangjia_id, + amount=Decimal(str(order.jine)), + action=2 # 2 = 结算 + ) + else: + pass + + # 7. 更新客服统计 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + # 8. 记录操作日志 + logger.info(f"强制结单成功,订单 {dingdan_id},方向 {dispatch_type},客服 {username}") + + return Response({'code': 0, 'msg': '强制结单成功'}) + + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {dingdan_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + except Exception as e: + logger.error(f"强制结单接口异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + # ========== 辅助方法 ========== + def _notify_partner_order_status(self, partner_club_id, partner_order_id, new_status, current_user): + """ + 通知对方平台订单状态变更(已在事务外调用) + """ + try: + # 获取我方俱乐部ID + my_club_config = ClubConfig.objects.filter(is_self=1).first() + if not my_club_config: + logger.error("未配置我方俱乐部信息,无法通知对方") + return + self_club_id = my_club_config.club_id + + # 查询对方服务器域名 + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain + if not partner_domain: + logger.error(f"对方俱乐部 {partner_club_id} 未配置域名") + return + + url = partner_domain.rstrip('/') + '/houtai/dfddtk' + payload = { + 'order_id': partner_order_id, + 'club_id': self_club_id, + 'status': new_status + } + resp = requests.post(url, json=payload, timeout=3) + if resp.status_code == 200: + resp_json = resp.json() + if resp_json.get('code') == 0: + # 更新跨平台订单扩展表中的对方状态 + cross_data, _ = CrossPlatformOrderData.objects.get_or_create( + dingdan_id=partner_order_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id, + 'partner_order_status': new_status, + 'partner_dashou_id': '', + 'partner_amount': Decimal('0') + } + ) + if not _: + cross_data.partner_order_status = new_status + cross_data.save(update_fields=['partner_order_status']) + logger.info(f"通知对方订单状态成功: {partner_order_id} -> {new_status}") + else: + logger.warning(f"对方接口返回错误: {resp_json.get('msg')}") + else: + logger.warning(f"对方接口HTTP错误: {resp.status_code}") + except requests.exceptions.Timeout: + logger.error(f"通知对方平台超时: {partner_club_id}") + except Exception as e: + logger.error(f"通知对方平台异常: {str(e)}") + + + + +class CrossPlatformTransferHallView(APIView): + """ + 跨平台订单转移大厅接口 + 请求:POST /houtai/kpzydt + 参数:{"username": "客服账号ID", "dingdan_id": "订单ID"} + 认证:JWT + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + if not username or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 客服身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权处理俱乐部订单'}) + + # 4. 查询订单并加锁 + try: + with transaction.atomic(): + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + + # 5. 状态校验 + if order.zhuangtai not in [2, 8, 4]: + return Response({'code': 400, 'msg': f'订单状态不允许转移大厅,当前状态: {order.zhuangtai}'}) + + # 获取关键字段 + is_cross = order.is_cross + dispatch_type = order.dispatch_type + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + current_dashou_id = order.jiedan_dashou_id + zhiding_id = order.zhiding_id + + # ========== 判断是否需要通知对方平台 ========== + # 条件:跨平台 且 我方派单(dispatch_type=1) 且 (partner_club_id 或 partner_order_id 有值) + need_notify = (is_cross == 1 and (partner_club_id or partner_order_id)) + + # ========== 判断是否需要修改本地打手状态 ========== + # 当接单打手是我方打手时,才需要修改状态(改为空闲1) + # 我方打手的情况: + # 1. 非跨平台订单 (is_cross != 1) + # 2. 跨平台且对方派单 (dispatch_type == 2) —— 此时打手是我方 + # 3. 跨平台且我方派单但没有对方信息 (即 need_notify == False) —— 此时打手也是我方 + need_update_dashou_status = (current_dashou_id and + (is_cross != 1 or dispatch_type == 2 or not need_notify)) + + # ========== 记录打手历史(无论打手是谁,只要有ID就记录) ========== + if current_dashou_id: + last_history = OrderDashouHistory.objects.filter( + dingdan_id=dingdan_id + ).order_by('-times').first() + next_times = (last_history.times + 1) if last_history else 1 + OrderDashouHistory.objects.create( + dingdan_id=dingdan_id, + dashou_id=current_dashou_id, + times=next_times, + operator=username + ) + + # ========== 修改本地打手状态(仅当是我方打手时) ========== + if need_update_dashou_status: + try: + dashou_profile = UserDashou.objects.get(user__yonghuid=current_dashou_id) + dashou_profile.zhuangtai = 1 + dashou_profile.save() + logger.info(f"打手 {current_dashou_id} 状态改为空闲") + except ObjectDoesNotExist: + logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新") + + # ========== 更新订单状态(清空打手,改为1或7) ========== + if zhiding_id: + order.zhuangtai = 7 # 指定中 + else: + order.zhuangtai = 1 # 下单中 + order.jiedan_dashou_id = None + order.clkf = username + order.save() + + # ========== 如果需要通知对方平台,异步发送通知 ========== + if need_notify: + # 确定对方俱乐部ID(如果没有则用默认000001) + real_partner_club_id = partner_club_id if partner_club_id else '000001' + real_partner_order_id = partner_order_id if partner_order_id else dingdan_id + + # 获取我方俱乐部ID + my_club_config = ClubConfig.objects.filter(is_self=1).first() + if my_club_config: + self_club_id = my_club_config.club_id + # 查询对方域名 + try: + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=real_partner_club_id) + partner_domain = club_rel.partner_domain + if partner_domain: + url = partner_domain.rstrip('/') + '/houtai/kpttzzydt' + payload = { + 'order_id': real_partner_order_id, + 'club_id': self_club_id + } + + def notify(): + try: + resp = requests.post(url, json=payload, timeout=3) + if resp.status_code == 200: + resp_json = resp.json() + if resp_json.get('code') == 0: + # 更新跨平台订单扩展表中的对方状态 + cross_data, created = CrossPlatformOrderData.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'partner_order_id': real_partner_order_id, + 'partner_club_id': real_partner_club_id, + 'partner_order_status': resp_json.get('data', {}).get('partner_status', 0), + 'partner_dashou_id': '', + 'partner_amount': order.jine or 0, + } + ) + if not created: + cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0) + cross_data.save(update_fields=['partner_order_status']) + logger.info(f"转移大厅通知对方成功: {dingdan_id}") + + # ========== 新增代码开始 ========== + # 对方成功转移大厅,清空我方订单中的对方相关信息 + # ========== 新增代码开始 ========== + # 只有我方派单时才清空对方平台信息,对方派单则保留 + if dispatch_type == 1: + Dingdan.objects.filter(dingdan_id=dingdan_id).update( + partner_club_id=None, + partner_order_id=None, + jiedan_dashou_id=None, + zhuangtai=1 + ) + logger.info(f"我方派单,已清空订单 {dingdan_id} 的对方信息") + else: + # 对方派单,只清空打手并恢复状态,保留对方信息 + Dingdan.objects.filter(dingdan_id=dingdan_id).update( + jiedan_dashou_id=None, + zhuangtai=1 + ) + logger.info(f"对方派单,只清空打手,保留对方信息,订单 {dingdan_id}") + # ========== 新增代码结束 ========== + # ========== 新增代码结束 ========== + + else: + logger.warning(f"对方平台返回错误: {resp_json.get('msg')}") + else: + logger.warning(f"对方接口HTTP错误: {resp.status_code}") + except Exception as e: + logger.error(f"转移大厅通知对方异常: {str(e)}") + + threading.Thread(target=notify).start() + except Club.DoesNotExist: + logger.error(f"未找到对方俱乐部配置: partner_club_id={real_partner_club_id}") + except Exception as e: + logger.error(f"查询对方平台配置失败: {str(e)}") + + # ========== 更新客服统计 ========== + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + return Response({'code': 0, 'msg': '转移大厅成功'}) + + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}) + except Exception as e: + logger.error(f"转移大厅接口异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + +class PartnerTransferHallNotifyView(APIView): + """ + 对方平台转移大厅通知接口 + 对方调用此接口通知我方订单已转移大厅,我方同步更新订单状态 + 请求:POST /houtai/kpttzzydt + 参数:{ + "order_id": "对方平台的订单ID", + "club_id": "对方俱乐部ID" + } + 无需认证 + 返回:{ + "code": 0, + "msg": "success", + "data": { + "partner_status": 1 # 我方订单当前状态 + } + } + """ + parser_classes = [JSONParser] + permission_classes = [AllowAny] + + def post(self, request): + # token = request.data.get('token') + # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): + # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200) + try: + # 1. 获取参数 + partner_order_id = request.data.get('order_id', '').strip() + partner_club_id = request.data.get('club_id', '').strip() + if not partner_order_id or not partner_club_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 查询我方订单 + try: + order = Dingdan.objects.get( + dingdan_id=partner_order_id, + partner_club_id=partner_club_id + ) + except Dingdan.DoesNotExist: + logger.warning(f"对方转移大厅通知:订单不存在 partner_order_id={partner_order_id}, partner_club_id={partner_club_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + + # 3. 加锁处理 + with transaction.atomic(): + order = Dingdan.objects.select_for_update().get(id=order.id) + + current_status = order.zhuangtai + current_dashou_id = order.jiedan_dashou_id + + # 4. 根据订单状态处理 + if current_status == 3: + # 已完成状态,需要检查预结算记录 + try: + pre_settlement = PreSettlement.objects.select_for_update().get( + dingdan_id=order.dingdan_id, + dashou_id=current_dashou_id + ) + # 如果预结算状态是0(待结算),改为2(已拒绝) + if pre_settlement.status == 0: + pre_settlement.status = 2 + pre_settlement.process_time = timezone.now() + pre_settlement.operator = partner_club_id # 记录操作方 + pre_settlement.save() + logger.info(f"预结算记录已拒绝: {order.dingdan_id}") + else: + logger.info(f"预结算记录已是其他状态 {pre_settlement.status},不重复修改") + except PreSettlement.DoesNotExist: + logger.warning(f"订单 {order.dingdan_id} 已完成但无预结算记录,无法转移大厅") + return Response({'code': 400, 'msg': '订单已完成且无预结算记录,无法转移大厅'}) + + # 5. 记录打手历史(如果有接单打手) + if current_dashou_id: + last_history = OrderDashouHistory.objects.filter( + dingdan_id=order.dingdan_id + ).order_by('-times').first() + next_times = (last_history.times + 1) if last_history else 1 + OrderDashouHistory.objects.create( + dingdan_id=order.dingdan_id, + dashou_id=current_dashou_id, + times=next_times, + operator=partner_club_id # 记录操作方 + ) + # 将打手状态改为空闲 + try: + dashou_profile = UserDashou.objects.get(user__yonghuid=current_dashou_id) + dashou_profile.zhuangtai = 1 + dashou_profile.save() + + # 打手退款每日统计 + if order.dispatch_type == 2: + try: + update_dashou_daily_by_action( + yonghuid=current_dashou_id, + amount=dashou_profile.dashou_fencheng, + action=3 # 退款 + ) + except Exception as e: + logger.error(f"记录打手统计错误: {str(e)}") + except ObjectDoesNotExist: + logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新") + + # 6. 更新订单状态为1(下单中)或7(指定中) + # 7. 根据派单方向清理对方信息 + if order.dispatch_type == 1: + # 我方派单:清空对方俱乐部ID和订单ID + order.partner_club_id = None + order.partner_order_id = None + # 对方派单时保留对方信息,不清空 + + # 8. 更新订单状态 + if order.zhiding_id: + order.zhuangtai = 7 # 指定中 + else: + order.zhuangtai = 1 # 下单中 + + + order.jiedan_dashou_id = None + order.clkf = partner_club_id # 记录操作方 + order.save() + + logger.info(f"对方转移大厅通知成功: 订单 {order.dingdan_id},新状态 {order.zhuangtai}") + + # 7. 返回我方订单状态 + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'partner_status': order.zhuangtai + } + }) + + except Exception as e: + logger.error(f"对方转移大厅通知接口异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + + +logger = logging.getLogger(__name__) + + +# ========== 1. 获取俱乐部设置数据接口 ========== +class GetSettingsView(APIView): + """ + 获取跨平台设置页面数据 + 请求:POST /houtai/hqkptsz + 参数:{"username": "客服账号ID"} + 权限:权限码2且开启 + 返回:包含自己俱乐部信息(含客服字段)和合作俱乐部列表 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 客服身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) + + # 4. 查询数据 + try: + # 4.1 自己俱乐部配置(is_self=1) + self_config = ClubConfig.objects.filter(is_self=1).first() + if not self_config: + logger.error("未找到自己俱乐部配置") + return Response({'code': 500, 'msg': '系统配置错误'}) + + self_club = { + 'club_id': self_config.club_id, + 'club_nickname': self_config.club_nickname, + 'club_avatar': self_config.club_avatar, + 'backend_domain': self_config.backend_domain, + 'chat_api_key': self_config.chat_api_key, + 'chat_api_id': self_config.chat_api_id, + 'customer_service_api_url': self_config.customer_service_api_url, + 'customer_service_corp_id': self_config.customer_service_corp_id, + 'storage_bucket_domain': self_config.storage_bucket_domain, + } + + # 4.2 合作俱乐部列表(Club 表中本俱乐部ID = self_config.club_id) + partner_records = Club.objects.filter(club_id=self_config.club_id) + partner_clubs = [] + for record in partner_records: + partner_config = ClubConfig.objects.filter( + club_id=record.partner_club_id, + is_self=0 + ).first() + partner_clubs.append({ + 'club_id': record.partner_club_id, + 'club_nickname': partner_config.club_nickname if partner_config else record.partner_club_id, + 'club_avatar': partner_config.club_avatar if partner_config else '', + 'is_interop_enabled': record.is_interop_enabled, + 'is_receive_enabled': record.is_receive_enabled, + }) + + return Response({'code': 0, 'data': {'self_club': self_club, 'partner_clubs': partner_clubs}}) + + except Exception as e: + logger.error(f"获取设置失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + +# ========== 2. 获取订单类型映射接口 ========== +class GetMappingsView(APIView): + """ + 获取指定合作俱乐部的订单类型映射 + 请求:POST /houtai/hqqtjlbsj + 参数:{"username": "客服账号ID", "partner_club_id": "对方俱乐部ID"} + 权限:权限码2 + 返回:对方订单类型列表、我方订单类型列表、当前映射关系 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + partner_club_id = request.data.get('partner_club_id', '').strip() + if not username or not partner_club_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 客服身份验证 + current_user = request.user + + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) + + try: + # 4. 获取我方所有商品类型(订单类型) + our_types = ShangpinLeixing.objects.all().values('id', 'jieshao', 'tupian_url') + our_type_list = [ + {'type_id': t['id'], 'type_name': t['jieshao'], 'icon': t['tupian_url']} + for t in our_types + ] + + # 5. 获取对方订单类型(从映射表去重) + mappings_query = OrderTypeMapping.objects.filter(partner_club_id=partner_club_id) + partner_types_dict = {} + for m in mappings_query: + partner_types_dict[m.partner_type_id] = { + 'type_id': m.partner_type_id, + 'type_name': m.partner_type_name, + 'icon': '' + } + partner_type_list = list(partner_types_dict.values()) + + # 6. 获取当前映射关系 + mapping_list = [ + {'partner_type_id': m.partner_type_id, 'our_type_id': m.our_type_id} + for m in mappings_query + ] + + return Response({ + 'code': 0, + 'data': { + 'partner_types': partner_type_list, + 'our_types': our_type_list, + 'mappings': mapping_list + } + }) + + except Exception as e: + logger.error(f"获取映射失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + +# ========== 3. 保存设置接口(自己俱乐部 + 合作俱乐部开关)========== +class UpdateSettingsView(APIView): + """ + 保存俱乐部设置(自己俱乐部信息 + 合作俱乐部开关) + 请求:POST /houtai/xgjlbsj + 参数:{ + "username": "客服账号ID", + "self_club": { + "club_nickname": "...", + "backend_domain": "...", + "chat_api_key": "...", + "chat_api_id": "...", + "customer_service_api_url": "...", + "customer_service_corp_id": "...", + "storage_bucket_domain": "..." + }, + "partner_updates": [ + {"club_id": "654321", "is_interop_enabled": true, "is_receive_enabled": false} + ] + } + 权限:权限码2 + 返回:{"code": 0, "msg": "success"} + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 客服身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004aa' not in permissions: + return Response({'code': 403, 'msg': '您无权修改俱乐部配置'}) + + # 4. 获取自己俱乐部当前配置 + try: + self_config = ClubConfig.objects.get(is_self=1) + except ObjectDoesNotExist: + logger.error("未找到自己俱乐部配置") + return Response({'code': 500, 'msg': '系统配置错误'}) + + # 5. 更新自己俱乐部配置(包含客服字段) + self_updates = request.data.get('self_club', {}) + if self_updates: + allowed_fields = [ + 'club_nickname', 'backend_domain', 'chat_api_key', 'chat_api_id', + 'customer_service_api_url', 'customer_service_corp_id', + 'storage_bucket_domain' + ] + fields_to_update = [] + for field in allowed_fields: + if field in self_updates: + setattr(self_config, field, self_updates[field]) + fields_to_update.append(field) + if fields_to_update: + self_config.save(update_fields=fields_to_update) + + # 6. 更新合作俱乐部开关 + partner_updates = request.data.get('partner_updates', []) + for update in partner_updates: + club_id = update.get('club_id') + if not club_id: + continue + try: + partner_record = Club.objects.get(club_id=self_config.club_id, partner_club_id=club_id) + if 'is_interop_enabled' in update: + partner_record.is_interop_enabled = update['is_interop_enabled'] + if 'is_receive_enabled' in update: + partner_record.is_receive_enabled = update['is_receive_enabled'] + partner_record.save(update_fields=['is_interop_enabled', 'is_receive_enabled']) + except ObjectDoesNotExist: + logger.warning(f"合作俱乐部记录不存在: {club_id}") + + # 7. 异步通知所有合作俱乐部(如果自己数据有修改) + if self_updates: + self._notify_all_partners(self_config, self_updates) + + return Response({'code': 0, 'msg': 'success'}) + + def _notify_all_partners(self, self_config, updates): + """异步通知所有合作俱乐部更新我方配置""" + partner_records = Club.objects.filter(club_id=self_config.club_id) + if not partner_records: + return + + data_to_send = { + 'club_id': self_config.club_id, + 'updates': updates + } + + def notify(partner_domain): + try: + url = partner_domain.rstrip('/') + '/houtai/xgzjsjcs' + resp = requests.post(url, json=data_to_send, timeout=3) + if resp.status_code != 200: + logger.warning(f"通知 {partner_domain} 失败,状态码 {resp.status_code}") + except Exception as e: + logger.error(f"通知 {partner_domain} 异常: {str(e)}") + + for record in partner_records: + domain = record.partner_domain + if domain: + threading.Thread(target=notify, args=(domain,)).start() + + + + + +class UpdateMappingsView(APIView): + """ + 保存指定合作俱乐部的订单类型映射 + 请求:POST /houtai/xgdfddys + 参数:{ + "username": "客服手机号", + "partner_club_id": "对方俱乐部ID", + "mappings": [ + {"partner_type_id": 1, "our_type_id": 10}, + {"partner_type_id": 2, "our_type_id": 11} + ] + } + 权限:权限码2 + 返回:{"code": 0, "msg": "success"} + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + partner_club_id = request.data.get('partner_club_id', '').strip() + mappings = request.data.get('mappings', []) + + if not username or not partner_club_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 客服身份验证 + current_user = request.user + + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004aa' not in permissions: + return Response({'code': 403, 'msg': '您无权修改俱乐部配置'}) + + try: + with transaction.atomic(): + # 4. 获取当前该俱乐部所有现有映射(按 partner_type_id 索引) + existing_mappings = { + m.partner_type_id: m + for m in OrderTypeMapping.objects.filter(partner_club_id=partner_club_id) + } + + # 5. 处理前端传来的映射(更新或新增) + processed_partner_ids = set() + for mapping in mappings: + partner_type_id = mapping.get('partner_type_id') + our_type_id = mapping.get('our_type_id') + if partner_type_id is None or our_type_id is None: + continue + processed_partner_ids.add(partner_type_id) + + # 获取我方类型名称 + our_type_name = '' + try: + our_type = ShangpinLeixing.objects.get(id=our_type_id) + our_type_name = our_type.jieshao + except ObjectDoesNotExist: + logger.warning(f"我方订单类型不存在: {our_type_id}") + + # 查找是否存在该对方类型的映射 + if partner_type_id in existing_mappings: + # 更新现有记录 + mapping_obj = existing_mappings[partner_type_id] + mapping_obj.our_type_id = our_type_id + mapping_obj.our_type_name = our_type_name + mapping_obj.save(update_fields=['our_type_id', 'our_type_name']) + # 从字典中移除,以便后续知道哪些被删除了 + del existing_mappings[partner_type_id] + else: + # 新增记录 + # 对方类型名称:优先从现有映射中查找(可能被其他俱乐部共享?不,每个俱乐部独立) + # 由于该对方类型没有映射过,我们暂时无法获取名称,可留空或使用默认值。 + # 但为保证数据完整性,我们尝试从已有记录中查找(如果该俱乐部之前删除过又重建,则可能丢失名称) + # 更稳妥的方式是前端传递 partner_type_name,但当前未要求,因此此处留空并记录警告。 + partner_type_name = '' + logger.warning(f"新增映射时对方类型名称缺失: partner_club_id={partner_club_id}, partner_type_id={partner_type_id}") + + OrderTypeMapping.objects.create( + partner_club_id=partner_club_id, + partner_type_id=partner_type_id, + partner_type_name=partner_type_name, + our_type_id=our_type_id, + our_type_name=our_type_name + ) + + # 6. 删除前端未传入的映射(即用户取消的映射) + for partner_type_id, mapping_obj in existing_mappings.items(): + mapping_obj.delete() + logger.info(f"删除映射: partner_club_id={partner_club_id}, partner_type_id={partner_type_id}") + + return Response({'code': 0, 'msg': 'success'}) + + except Exception as e: + logger.error(f"保存映射失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) +# ========== 5. 接收对方平台修改数据接口 ========== +class PartnerUpdateSelfConfigView(APIView): + """ + 接收对方平台修改自己俱乐部配置的通知 + 请求:POST /houtai/xgzjsjcs + 参数:{ + "club_id": "对方俱乐部ID", + "updates": { + "club_nickname": "新昵称", + "backend_domain": "新域名", + "chat_api_key": "新key", + "chat_api_id": "新id", + "customer_service_api_url": "新URL", + "customer_service_corp_id": "新企业ID", + "storage_bucket_domain": "新存储桶" + } + } + 无需认证,对方平台可信调用 + 返回:{"code": 0, "msg": "success"} + """ + parser_classes = [JSONParser] + + def post(self, request): + # token = request.data.get('token') + # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): + # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200) + # 1. 获取参数 + club_id = request.data.get('club_id', '').strip() + updates = request.data.get('updates', {}) + + if not club_id or not updates: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 查询对方俱乐部的配置记录(is_self=0) + try: + config = ClubConfig.objects.get(club_id=club_id, is_self=0) + except ClubConfig.DoesNotExist: + logger.warning(f"对方俱乐部配置不存在: {club_id}") + return Response({'code': 404, 'msg': '俱乐部不存在'}) + + # 3. 允许修改的字段白名单(包含客服字段) + allowed_fields = [ + 'club_nickname', + 'backend_domain', + 'chat_api_key', + 'chat_api_id', + 'customer_service_api_url', + 'customer_service_corp_id', + 'storage_bucket_domain' + ] + + # 4. 更新字段 + with transaction.atomic(): + fields_updated = [] + for field in allowed_fields: + if field in updates: + new_value = updates[field] + setattr(config, field, new_value) + fields_updated.append(field) + if fields_updated: + config.save(update_fields=fields_updated) + logger.info(f"对方俱乐部 {club_id} 配置已更新: {fields_updated}") + else: + logger.info(f"对方俱乐部 {club_id} 无有效字段更新") + + return Response({'code': 0, 'msg': 'success'}) + + + + +# apps/houtai/views.py + + + + + +# ========== 1. 获取合作俱乐部列表接口(完整) ========== +class GetPartnerClubsView(APIView): + """ + 获取我方合作俱乐部列表(用于统计页面) + 请求:POST /houtai/kpthqpt + 参数:{"username": "客服手机号"} + 权限:权限码2且开启 + 返回:{"code":0, "data":[{"club_id":"xxx","club_nickname":"xxx","club_avatar":"xxx"}]} + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 客服身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004bb' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) + + # 4. 获取我方俱乐部ID + try: + self_config = ClubConfig.objects.get(is_self=1) + except ObjectDoesNotExist: + logger.error("未找到自己俱乐部配置") + return Response({'code': 500, 'msg': '系统配置错误'}) + + # 5. 查询所有合作俱乐部 + try: + records = Club.objects.filter(club_id=self_config.club_id) + result = [] + for record in records: + partner_config = ClubConfig.objects.filter( + club_id=record.partner_club_id, + is_self=0 + ).first() + result.append({ + 'club_id': record.partner_club_id, + 'club_nickname': partner_config.club_nickname if partner_config else record.partner_club_id, + 'club_avatar': partner_config.club_avatar if partner_config else '', + }) + return Response({'code': 0, 'data': result}) + except Exception as e: + logger.error(f"查询合作俱乐部失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + +# ========== 2. 获取统计数据接口(完整) ========== + + + + + +class GetStatsView(APIView): + """ + 获取派单统计数据(按日或按月) + 请求:POST /houtai/hqpfsj + 参数:{ + "username": "客服手机号", + "club_id": "对方俱乐部ID", + "direction": 1, # 1=我方派单,2=对方派单 + "granularity": "day", # "day" 或 "month" + # 趋势图参数(可选,不传则使用默认值): + "trend_year_month": "YYYY-MM", # 按日时表示月份,缺省当前月 + "trend_year": "YYYY", # 按月时表示年份,缺省当前年 + # KPI参数(可选,不传则使用默认值): + "date": "YYYY-MM-DD", # 按日时表示日期,缺省当天 + "year_month": "YYYY-MM" # 按月时表示月份,缺省当前月 + } + 权限:权限码2 + 返回:{ + "code": 0, + "data": { + "summary": { ... }, + "chart": { ... } + } + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取基础参数 + username = request.data.get('username', '').strip() + club_id = request.data.get('club_id', '').strip() + direction = request.data.get('direction') + granularity = request.data.get('granularity') + + # 基础参数校验 + if not username: + return Response({'code': 400, 'msg': 'username 不能为空'}) + if not club_id: + return Response({'code': 400, 'msg': 'club_id 不能为空'}) + if direction is None: + return Response({'code': 400, 'msg': 'direction 不能为空'}) + if granularity not in ('day', 'month'): + return Response({'code': 400, 'msg': 'granularity 必须是 day 或 month'}) + try: + direction = int(direction) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': 'direction 必须为整数'}) + + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004bb' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) + + # 4. 获取当前时间用于默认值 + now = datetime.now() + + # 5. 处理趋势图参数(默认当前月/年) + if granularity == 'day': + trend_year_month = request.data.get('trend_year_month') + if not trend_year_month: + trend_year_month = now.strftime('%Y-%m') + try: + year, month = map(int, trend_year_month.split('-')) + start_date = datetime(year, month, 1).date() + _, days_in_month = monthrange(year, month) + end_date = datetime(year, month, days_in_month).date() + except (ValueError, TypeError): + # 如果格式错误,回退到当前月 + year, month = now.year, now.month + start_date = datetime(year, month, 1).date() + _, days_in_month = monthrange(year, month) + end_date = datetime(year, month, days_in_month).date() + else: + trend_year = request.data.get('trend_year') + if not trend_year: + trend_year = str(now.year) + try: + year = int(trend_year) + except (ValueError, TypeError): + year = now.year + start_date = datetime(year, 1, 1).date() + end_date = datetime(year, 12, 31).date() + + # 6. 处理KPI参数(默认当天/当月) + if granularity == 'day': + date_str = request.data.get('date') + if not date_str: + date_str = now.strftime('%Y-%m-%d') + try: + target_date = datetime.strptime(date_str, '%Y-%m-%d').date() + except (ValueError, TypeError): + target_date = now.date() + else: + year_month_str = request.data.get('year_month') + if not year_month_str: + year_month_str = now.strftime('%Y-%m') + try: + year, month = map(int, year_month_str.split('-')) + start_date_kpi = datetime(year, month, 1).date() + _, days_in_month = monthrange(year, month) + end_date_kpi = datetime(year, month, days_in_month).date() + except (ValueError, TypeError): + year, month = now.year, now.month + start_date_kpi = datetime(year, month, 1).date() + _, days_in_month = monthrange(year, month) + end_date_kpi = datetime(year, month, days_in_month).date() + + # 7. 查询趋势图数据 + response_data = { + 'summary': {'dispatch_count': 0, 'dispatch_amount': 0.0, 'success_count': 0, 'success_amount': 0.0}, + 'chart': {'labels': [], 'dispatch_counts': [], 'dispatch_amounts': [], 'success_counts': [], 'success_amounts': []} + } + + try: + if granularity == 'day': + qs = DailyDispatchStat.objects.filter( + partner_club_id=club_id, + direction=direction, + date__gte=start_date, + date__lte=end_date + ).order_by('date') + data_dict = {} + for item in qs: + data_dict[item.date] = { + 'dispatch_count': item.dispatch_count, + 'dispatch_amount': float(item.dispatch_amount), + 'success_count': item.success_count, + 'success_amount': float(item.success_amount), + } + labels = [] + dispatch_counts = [] + dispatch_amounts = [] + success_counts = [] + success_amounts = [] + current = start_date + while current <= end_date: + labels.append(current.strftime('%d')) + day_data = data_dict.get(current, {}) + dispatch_counts.append(day_data.get('dispatch_count', 0)) + dispatch_amounts.append(day_data.get('dispatch_amount', 0.0)) + success_counts.append(day_data.get('success_count', 0)) + success_amounts.append(day_data.get('success_amount', 0.0)) + current += timedelta(days=1) + response_data['chart'] = { + 'labels': labels, + 'dispatch_counts': dispatch_counts, + 'dispatch_amounts': dispatch_amounts, + 'success_counts': success_counts, + 'success_amounts': success_amounts, + } + else: # month + qs = DailyDispatchStat.objects.filter( + partner_club_id=club_id, + direction=direction, + date__gte=start_date, + date__lte=end_date + ).order_by('date') + month_totals = {m: {'dispatch_count': 0, 'dispatch_amount': 0.0, 'success_count': 0, 'success_amount': 0.0} + for m in range(1, 13)} + for item in qs: + m = item.date.month + month_totals[m]['dispatch_count'] += item.dispatch_count + month_totals[m]['dispatch_amount'] += float(item.dispatch_amount) + month_totals[m]['success_count'] += item.success_count + month_totals[m]['success_amount'] += float(item.success_amount) + labels = [] + dispatch_counts = [] + dispatch_amounts = [] + success_counts = [] + success_amounts = [] + for m in range(1, 13): + labels.append(f"{m}月") + dispatch_counts.append(month_totals[m]['dispatch_count']) + dispatch_amounts.append(month_totals[m]['dispatch_amount']) + success_counts.append(month_totals[m]['success_count']) + success_amounts.append(month_totals[m]['success_amount']) + response_data['chart'] = { + 'labels': labels, + 'dispatch_counts': dispatch_counts, + 'dispatch_amounts': dispatch_amounts, + 'success_counts': success_counts, + 'success_amounts': success_amounts, + } + + # 8. 查询KPI数据 + if granularity == 'day': + try: + stat = DailyDispatchStat.objects.get( + partner_club_id=club_id, + direction=direction, + date=target_date + ) + response_data['summary'] = { + 'dispatch_count': stat.dispatch_count, + 'dispatch_amount': float(stat.dispatch_amount), + 'success_count': stat.success_count, + 'success_amount': float(stat.success_amount), + } + except DailyDispatchStat.DoesNotExist: + response_data['summary'] = { + 'dispatch_count': 0, + 'dispatch_amount': 0.0, + 'success_count': 0, + 'success_amount': 0.0, + } + else: # month + qs_kpi = DailyDispatchStat.objects.filter( + partner_club_id=club_id, + direction=direction, + date__gte=start_date_kpi, + date__lte=end_date_kpi + ) + summary = qs_kpi.aggregate( + dispatch_count=Sum('dispatch_count'), + dispatch_amount=Sum('dispatch_amount'), + success_count=Sum('success_count'), + success_amount=Sum('success_amount') + ) + response_data['summary'] = { + 'dispatch_count': summary['dispatch_count'] or 0, + 'dispatch_amount': float(summary['dispatch_amount'] or 0.0), + 'success_count': summary['success_count'] or 0, + 'success_amount': float(summary['success_amount'] or 0.0), + } + + return Response({'code': 0, 'data': response_data}) + + except Exception as e: + logger.error(f"获取统计数据失败: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + +class PunishDashouView(APIView): + """ + 客服处罚打手接口 + 请求:POST /houtai/kpcf + 参数:{ + "username": "客服手机号", + "dingdan_id": "订单ID", + "reason": "处罚原因(可选)" + } + 认证:JWT,权限码1 + 返回:code=0 + 处罚结果 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + start_time = time.time() + + # 1. 获取参数 + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + reason = request.data.get('reason', '').strip() + + if not username or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + # 2. 身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权处罚'}) + + # 4. 查询订单并加锁 + with transaction.atomic(): + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + + # 5. 订单状态校验(允许处罚的状态:1,2,3,4,5,6,7,8,即除已完成以外的状态?这里宽松处理) + if order.zhuangtai not in [1,2,3,4,5,6,7,8]: + return Response({'code': 400, 'msg': f'订单状态 {order.zhuangtai} 不允许处罚'}) + + # 6. 判断是否已处罚(避免重复) + existing_punish = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + dashouid=order.jiedan_dashou_id + ).exists() + if existing_punish: + return Response({'code': 400, 'msg': '该打手已被处罚过'}) + + # 7. 区分本地处罚还是通知对方平台 + is_cross = order.is_cross + dispatch_type = order.dispatch_type + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + dashou_id = order.jiedan_dashou_id + + # 如果没有接单打手ID,无法处罚 + if not dashou_id: + return Response({'code': 400, 'msg': '订单无接单打手,无法处罚'}) + + # 本地处罚(处罚我方打手) + def local_punish(): + # 扣除打手积分(假设每次扣5分) + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.jifen -= 5 + dashou_profile.save() + + # 创建处罚记录 + Chufajilu.objects.create( + dingdan_id=dingdan_id, + dashouid=dashou_id, + qingqiuid='', # 请求处罚ID,可空 + chuliid=username, + cfliyou=reason or '', + sqzhuangtai=1, # 成功 + bhliyou='', + ssliyou='', + jifen=5, + ) + logger.info(f"本地处罚打手成功: 订单 {dingdan_id}, 打手 {dashou_id}, 扣分5") + + # 情况A:对方派单(dispatch_type=2)或 本地订单 + if dispatch_type == 2 or not is_cross: + # 直接本地处罚 + local_punish() + return Response({'code': 0, 'msg': '处罚成功'}) + + # 情况B:我方派单且有对方信息(需要通知对方平台) + elif dispatch_type == 1 and partner_club_id and partner_order_id: + # 需要通知对方平台处罚(对方平台打手) + # 获取我方俱乐部ID + my_club_config = ClubConfig.objects.filter(is_self=1).first() + if not my_club_config: + logger.error("未找到我方俱乐部配置") + return Response({'code': 500, 'msg': '系统配置错误'}) + self_club_id = my_club_config.club_id + + # 获取对方服务器域名 + try: + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain + except ObjectDoesNotExist: + logger.error(f"未找到合作关系: {self_club_id} <-> {partner_club_id}") + return Response({'code': 500, 'msg': '未找到对方平台配置'}) + + # 调用对方平台处罚接口,最多重试2次 + url = partner_domain.rstrip('/') + '/houtai/kptcfds' + payload = { + 'order_id': partner_order_id, # 对方平台的订单ID + 'club_id': self_club_id, + 'reason': reason, + 'operator': username + } + timeout = 3 + retries = 2 + success = False + for attempt in range(retries): + try: + resp = requests.post(url, json=payload, timeout=timeout) + if resp.status_code == 200: + resp_json = resp.json() + if resp_json.get('code') == 0: + success = True + logger.info(f"通知对方平台处罚成功: {dingdan_id}") + # 可选:在本平台记录处罚记录(标记已通知对方,但不扣分) + Chufajilu.objects.create( + dingdan_id=dingdan_id, + dashouid=dashou_id, + qingqiuid='', + chuliid=username, + cfliyou=reason or '', + sqzhuangtai=1, + bhliyou='', + ssliyou='', + jifen=0, # 对方扣分,本方不扣 + ) + break + else: + logger.warning(f"对方平台返回错误 (尝试{attempt+1}): {resp_json.get('msg')}") + else: + logger.warning(f"对方平台HTTP错误 (尝试{attempt+1}): {resp.status_code}") + except requests.exceptions.Timeout: + logger.warning(f"请求对方平台超时 (尝试{attempt+1}): {url}") + except Exception as e: + logger.warning(f"请求对方平台异常 (尝试{attempt+1}): {str(e)}") + if not success: + return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'}) + + return Response({'code': 0, 'msg': '处罚成功'}) + + else: + # 其他情况(例如我方派单但缺少对方信息),视为本地订单,处罚我方打手 + local_punish() + return Response({'code': 0, 'msg': '处罚成功'}) + + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {dingdan_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + except Exception as e: + logger.error(f"处罚接口异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + + +@method_decorator(csrf_exempt, name='dispatch') +class PartnerPunishNotifyView(APIView): + """ + 接收对方平台处罚通知接口 + """ + authentication_classes = [] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + # token = request.data.get('token') + # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): + # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200) + try: + order_id = request.data.get('order_id', '').strip() + club_id = request.data.get('club_id', '').strip() + reason = request.data.get('reason', '').strip() + operator = request.data.get('operator', '').strip() + + if not order_id or not club_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 事务开始 + with transaction.atomic(): + # 查询订单并加锁 + try: + order = Dingdan.objects.select_for_update().get(dingdan_id=order_id) + except Dingdan.DoesNotExist: + logger.warning(f"处罚通知:订单不存在 {order_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + + dashou_id = order.jiedan_dashou_id + if not dashou_id: + logger.warning(f"订单 {order_id} 无接单打手,无法处罚") + return Response({'code': 400, 'msg': '订单无接单打手'}) + + # 检查是否已处罚 + existing = Chufajilu.objects.filter( + dingdan_id=order_id, + dashouid=dashou_id + ).exists() + if existing: + logger.info(f"订单 {order_id} 打手 {dashou_id} 已被处罚过,跳过") + return Response({'code': 0, 'msg': 'already punished'}) + + # 扣除积分 + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.jifen -= 5 + dashou_profile.save() + + # 创建处罚记录 + Chufajilu.objects.create( + dingdan_id=order_id, + dashouid=dashou_id, + qingqiuid='', + chuliid=operator, + cfliyou=reason or '', + sqzhuangtai=0, + bhliyou='', + ssliyou='', + jifen=5, + ) + logger.info(f"对方平台处罚成功: 订单 {order_id}, 打手 {dashou_id}") + + return Response({'code': 0, 'msg': 'success'}) + + except Exception as e: + logger.error(f"对方处罚通知接口异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + + + + +# ==================== 接口1:获取俱乐部列表(权限码2) ==================== +class GetClubConfigForPreSettlementView(APIView): + """ + 获取对方俱乐部配置列表(用于预结算页面) + 权限要求:客服身份 + 权限代码2且开启 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + # 1. 校验客服身份 + try: + kefu = UserKefu.objects.select_related('user').get(user=user) + except ObjectDoesNotExist: + return Response({'code': 403, 'msg': '身份错误,非客服账号'}) + if kefu.zhuangtai != 1: + return Response({'code': 403, 'msg': '客服账号已被禁用'}) + + # 2. 获取请求参数中的 username 并校验 + data = request.data + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) + + # 4. 查询所有对方俱乐部(is_self=0) + club_configs = ClubConfig.objects.filter(is_self=0).only( + 'club_id', 'club_nickname', 'club_avatar', 'storage_bucket_domain' + ) + data_list = [] + for club in club_configs: + data_list.append({ + 'club_id': club.club_id, + 'club_nickname': club.club_nickname or club.club_id, + 'club_avatar': club.club_avatar or '', + 'storage_bucket_domain': club.storage_bucket_domain or '' + }) + return Response({'code': 0, 'msg': 'success', 'data': data_list}) + + except Exception as e: + logger.error(f"获取俱乐部配置失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + +# ==================== 接口2:获取预结算订单列表 ==================== +class GetPreSettlementDataView(APIView): + """获取预结算订单列表(支持筛选、分页、统计)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + # 1. 客服身份校验 + try: + kefu = UserKefu.objects.select_related('user').get(user=user) + except ObjectDoesNotExist: + return Response({'code': 403, 'msg': '身份错误,非客服账号'}) + if kefu.zhuangtai != 1: + return Response({'code': 403, 'msg': '客服账号已被禁用'}) + + data = request.data + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) + + # 2. 获取筛选参数 + status_filter = data.get('status_filter') # 0=待处理, 1=已处理(包含已拒绝) + club_id = data.get('club_id') + dingdan_id = data.get('dingdan_id') + partner_order_id = data.get('partner_order_id') + dashou_id = data.get('dashou_id') + clkf = data.get('clkf') + date_str = data.get('date') # 创建日期 YYYY-MM-DD + page = int(data.get('page', 1)) + page_size = int(data.get('pageSize', 20)) + + # 3. 构建查询集 + queryset = PreSettlement.objects.all() + + # 状态筛选:0待处理,1已处理(status=1或2) + if status_filter == 0: + queryset = queryset.filter(status=0) + elif status_filter == 1: + queryset = queryset.filter(status__in=[1, 2]) + # 如果status_filter为其他值(如'all'),则不筛选 + + if club_id: + queryset = queryset.filter(partner_club_id=club_id) + if dingdan_id: + queryset = queryset.filter(dingdan_id__icontains=dingdan_id) + if partner_order_id: + queryset = queryset.filter(partner_order_id__icontains=partner_order_id) + if dashou_id: + queryset = queryset.filter(dashou_id__icontains=dashou_id) + if clkf: + queryset = queryset.filter(operator__icontains=clkf) + if date_str: + try: + target_date = datetime.strptime(date_str, '%Y-%m-%d').date() + queryset = queryset.filter(create_time__date=target_date) + except ValueError: + pass + + # 4. 统计数据(待处理总数量和总金额、已处理总数量) + pending_queryset = PreSettlement.objects.filter(status=0) + # 应用相同的筛选条件(除了状态) + if club_id: + pending_queryset = pending_queryset.filter(partner_club_id=club_id) + if dingdan_id: + pending_queryset = pending_queryset.filter(dingdan_id__icontains=dingdan_id) + if partner_order_id: + pending_queryset = pending_queryset.filter(partner_order_id__icontains=partner_order_id) + if dashou_id: + pending_queryset = pending_queryset.filter(dashou_id__icontains=dashou_id) + if clkf: + pending_queryset = pending_queryset.filter(operator__icontains=clkf) + if date_str: + pending_queryset = pending_queryset.filter(create_time__date=target_date) + + pending_count = pending_queryset.count() + pending_amount = pending_queryset.aggregate(total=Sum('order_amount'))['total'] or Decimal('0.00') + + processed_count = PreSettlement.objects.filter(status__in=[1, 2]) + if club_id: + processed_count = processed_count.filter(partner_club_id=club_id) + if dingdan_id: + processed_count = processed_count.filter(dingdan_id__icontains=dingdan_id) + if partner_order_id: + processed_count = processed_count.filter(partner_order_id__icontains=partner_order_id) + if dashou_id: + processed_count = processed_count.filter(dashou_id__icontains=dashou_id) + if clkf: + processed_count = processed_count.filter(operator__icontains=clkf) + if date_str: + processed_count = processed_count.filter(create_time__date=target_date) + processed_count = processed_count.count() + + # 5. 分页 + total = queryset.count() + start = (page - 1) * page_size + end = start + page_size + items = queryset.order_by('-create_time')[start:end] + + # 6. 构造返回数据 + list_data = [] + for item in items: + list_data.append({ + 'dingdan_id': item.dingdan_id, + 'partner_order_id': item.partner_order_id, + 'partner_club_id': item.partner_club_id, + 'dashou_id': item.dashou_id, + 'amount': float(item.amount), + 'order_amount': float(item.order_amount), + 'operator': item.operator, + 'status': item.status, + 'status_text': dict(PreSettlement.STATUS_CHOICES).get(item.status, '未知'), + 'create_time': item.create_time.strftime('%Y-%m-%d %H:%M:%S') if item.create_time else '', + 'process_time': item.process_time.strftime('%Y-%m-%d %H:%M:%S') if item.process_time else '', + }) + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'list': list_data, + 'total': total, + 'stats': { + 'pending_count': pending_count, + 'pending_amount': float(pending_amount), + 'processed_count': processed_count + } + } + }) + + except Exception as e: + logger.error(f"获取预结算数据失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + +# ==================== 接口3:打款/拒绝操作 ==================== +class PreSettlementActionView(APIView): + """预结算打款/拒绝操作(原子事务,保证资金安全)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + # 1. 客服身份校验 + try: + kefu = UserKefu.objects.select_related('user').get(user=user) + except ObjectDoesNotExist: + return Response({'code': 403, 'msg': '身份错误,非客服账号'}) + if kefu.zhuangtai != 1: + return Response({'code': 403, 'msg': '客服账号已被禁用'}) + + data = request.data + + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限() + if '004aa' not in permissions: + return Response({'code': 403, 'msg': '您无权处理预结算打款'}) + + dingdan_id = data.get('dingdan_id') + action = data.get('action') # 'pay' 或 'reject' + + if not dingdan_id or action not in ['pay', 'reject']: + return Response({'code': 400, 'msg': '参数错误'}) + + with transaction.atomic(): + # 锁定预结算记录(行锁) + pre_settlement = PreSettlement.objects.select_for_update().get(dingdan_id=dingdan_id) + + # 只有状态为0(待处理)才能操作 + if pre_settlement.status != 0: + return Response({'code': 400, 'msg': '该记录已处理,不可重复操作'}) + + if action == 'pay': + # 打款操作:增加打手余额及相关统计 + dashou_id = pre_settlement.dashou_id + amount = pre_settlement.amount + + # 查询打手扩展表 + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + except (UserMain.DoesNotExist, ObjectDoesNotExist): + return Response({'code': 404, 'msg': '打手不存在'}) + + # 使用 F 表达式原子更新余额和统计字段(防止并发覆盖) + dashou_profile.yue = F('yue') + amount + dashou_profile.zonge = F('zonge') + amount + dashou_profile.jinrishouyi = F('jinrishouyi') + amount + dashou_profile.jinyueshouyi = F('jinyueshouyi') + amount + dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1 + dashou_profile.save(update_fields=['yue', 'zonge', 'jinrishouyi', 'jinyueshouyi', 'chengjiaozongliang']) + + # 更新预结算状态为已结算 + pre_settlement.status = 1 + pre_settlement.process_time = timezone.now() + pre_settlement.operator = user.phone + pre_settlement.save() + + # ---------- 打手每日统计(成交) ---------- + + try: + update_dashou_daily_by_action( + yonghuid=dashou_id, + amount=amount, + action=2 # 2 表示成交(结算) + ) + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + + + logger.info(f"预结算打款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {amount}") + + logger.info(f"预结算打款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {amount}") + + elif action == 'reject': + # 拒绝操作:仅修改状态,不涉及资金变动 + pre_settlement.status = 2 + pre_settlement.process_time = timezone.now() + pre_settlement.operator = user.phone + pre_settlement.save() + logger.info(f"预结算拒绝: 订单 {dingdan_id}") + + # ---------- 打手每日统计(成交) ---------- + dashou_id = pre_settlement.dashou_id + amount = pre_settlement.amount + try: + dashou_id = pre_settlement.dashou_id + amount = pre_settlement.amount + update_dashou_daily_by_action( + yonghuid=dashou_id, + amount=amount, + action=3 # 3 表示退款(退款) + ) + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + logger.info(f"预结算拒绝: 订单 {dingdan_id}") + + return Response({'code': 0, 'msg': '操作成功'}) + + except PreSettlement.DoesNotExist: + return Response({'code': 404, 'msg': '预结算记录不存在'}) + except Exception as e: + logger.error(f"预结算操作异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + + +class GetRolePermissionView(APIView): + """ + 获取所有角色、权限以及角色关联的用户数 + 权限要求:用户必须拥有 000001 权限 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 1. 权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 2. 检查是否有 000001 权限 + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权访问此页面'}) + + # 3. 获取所有权限 + all_perms = Permission.objects.all().values('perm_code', 'perm_name', 'description') + permissions_list = list(all_perms) + + # 4. 获取所有角色,并统计每个角色下的用户数和关联的权限 + roles = Role.objects.all() + roles_data = [] + for role in roles: + # 统计该角色下的用户数 + user_count = UserRole.objects.filter(role=role).count() + # 获取该角色绑定的权限 + role_perms = RolePermission.objects.filter(role=role).select_related('permission') + perms_of_role = [ + { + 'perm_code': rp.permission.perm_code, + 'perm_name': rp.permission.perm_name, + 'description': rp.permission.description + } + for rp in role_perms + ] + roles_data.append({ + 'role_code': role.role_code, + 'role_name': role.role_name, + 'description': role.description, + 'user_count': user_count, + 'permissions': perms_of_role + }) + + return Response({ + 'code': 0, + 'data': { + 'roles': roles_data, + 'permissions': permissions_list + } + }) + + +class ModifyRolePermissionView(APIView): + """ + 通用接口:支持删除角色、为角色添加权限、移除角色权限 + 操作类型通过 action 字段区分 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + action = request.data.get('action') # delete_role, add_permission, remove_permission + if not username or not action: + return Response({'code': 400, 'msg': '缺少必要参数'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + # 获取角色(需要角色编码) + role_code = request.data.get('role_code') + if not role_code: + return Response({'code': 400, 'msg': '缺少角色编码'}) + + try: + role = Role.objects.get(role_code=role_code) + except Role.DoesNotExist: + return Response({'code': 404, 'msg': '角色不存在'}) + + # 超级管理员角色(role_code='000001')的特殊限制 + if role.role_code == '000001': + if action in ['delete_role']: + return Response({'code': 403, 'msg': '超级管理员角色不能删除'}) + # 修改名称/描述(本接口不涉及名称描述修改,但若后续扩展需限制) + + # 根据 action 处理 + if action == 'delete_role': + # 删除角色:同时删除角色权限关联、用户角色关联 + with transaction.atomic(): + RolePermission.objects.filter(role=role).delete() + UserRole.objects.filter(role=role).delete() + role.delete() + return Response({'code': 0, 'msg': '角色删除成功'}) + + elif action == 'add_permission': + perm_code = request.data.get('perm_code') + if not perm_code: + return Response({'code': 400, 'msg': '缺少权限编码'}) + # 超级管理员角色不能添加 000001 权限(但实际它已拥有,这里防御) + if role.role_code == '000001' and perm_code == '000001': + return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'}) + try: + perm = Permission.objects.get(perm_code=perm_code) + except Permission.DoesNotExist: + return Response({'code': 404, 'msg': '权限不存在'}) + # 检查是否已存在 + if RolePermission.objects.filter(role=role, permission=perm).exists(): + return Response({'code': 400, 'msg': '该角色已拥有此权限'}) + RolePermission.objects.create(role=role, permission=perm) + return Response({'code': 0, 'msg': '权限添加成功'}) + + elif action == 'remove_permission': + perm_code = request.data.get('perm_code') + if not perm_code: + return Response({'code': 400, 'msg': '缺少权限编码'}) + # 超级管理员角色不能删除 000001 权限 + if role.role_code == '000001' and perm_code == '000001': + return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'}) + try: + perm = Permission.objects.get(perm_code=perm_code) + except Permission.DoesNotExist: + return Response({'code': 404, 'msg': '权限不存在'}) + deleted = RolePermission.objects.filter(role=role, permission=perm).delete() + if deleted[0] == 0: + return Response({'code': 400, 'msg': '该角色未拥有此权限'}) + return Response({'code': 0, 'msg': '权限移除成功'}) + + else: + return Response({'code': 400, 'msg': '无效的 action'}) + + +class AddRoleView(APIView): + """ + 添加角色:自动生成角色编码,前端提供角色名称、描述、选择的权限列表 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + role_name = request.data.get('role_name', '').strip() + description = request.data.get('description', '').strip() + perm_codes = request.data.get('perm_codes', []) # 列表 + + if not username or not role_name: + return Response({'code': 400, 'msg': '缺少必要参数'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + # 检查角色名称是否已存在 + if Role.objects.filter(role_name=role_name).exists(): + return Response({'code': 400, 'msg': '角色名称已存在'}) + + # 生成唯一角色编码(格式:ROLE_ + 8位随机字母数字) + while True: + random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) + role_code = f"ROLE_{random_str}" + if not Role.objects.filter(role_code=role_code).exists(): + break + + # 创建角色 + with transaction.atomic(): + role = Role.objects.create( + role_code=role_code, + role_name=role_name, + description=description, + + ) + # 绑定权限(过滤掉不存在的权限编码) + for pc in perm_codes: + # 不允许添加 000001 权限(此权限仅供超级管理员) + if pc == '000001': + continue + try: + perm = Permission.objects.get(perm_code=pc) + RolePermission.objects.create(role=role, permission=perm) + except Permission.DoesNotExist: + # 忽略不存在的权限,或记录日志 + logger.warning(f"添加角色时忽略不存在的权限编码: {pc}") + pass + + return Response({'code': 0, 'msg': '角色添加成功', 'data': {'role_code': role_code}}) + + + +class GetAdminUserListView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权访问此页面'}) + + phone = request.data.get('phone', '').strip() + nicheng = request.data.get('nicheng', '').strip() + role_code = request.data.get('roleCode', '').strip() + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('pageSize', 20)) + + users = UserMain.objects.filter( + user_type='kefu', + kefu_profile__isnull=False + ).select_related('kefu_profile') + + if phone: + users = users.filter(phone__icontains=phone) + if nicheng: + users = users.filter(kefu_profile__nicheng__icontains=nicheng) + + # 关键修正:先求值为列表,再传入 phone__in + if role_code: + user_ids_with_role = list( + UserRole.objects.filter(role__role_code=role_code) + .values_list('account_id', flat=True) + ) + users = users.filter(phone__in=user_ids_with_role) + + users = users.order_by('-create_time') + paginator = Paginator(users, page_size) + page_obj = paginator.get_page(page) + + data_list = [] + for user in page_obj: + user_roles = UserRole.objects.filter(account_id=user.phone).select_related('role') + roles_data = [ + {'role_code': ur.role.role_code, 'role_name': ur.role.role_name} + for ur in user_roles + ] + data_list.append({ + 'phone': user.phone, + 'nicheng': user.kefu_profile.nicheng, + 'roles': roles_data, + 'status': user.kefu_profile.zhuangtai, + 'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(user, 'create_time') else '', + 'update_time': user.update_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(user, 'update_time') else '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': data_list, + 'total': paginator.count, + 'page': page, + 'pageSize': page_size + } + }) + + except Exception as e: + logger.error(f"GetAdminUserListView 异常: {e}\n{traceback.format_exc()}") + return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}) + + + +class GetAdminRolesView(APIView): + """获取所有角色(用于筛选和分配)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权访问此页面'}) + + roles = Role.objects.all().values('role_code', 'role_name') + return Response({ + 'code': 0, + 'data': { + 'roles': list(roles) + } + }) + + + + + + +'''class GetAdminUserListView(APIView): + """获取后台用户列表(支持分页、筛选)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权访问此页面'}) + + # 筛选条件 + phone = request.data.get('phone', '').strip() + nicheng = request.data.get('nicheng', '').strip() + role_code = request.data.get('roleCode', '').strip() + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('pageSize', 20)) + + # 基础查询:user_type='kefu' 且存在客服扩展表 + users = UserMain.objects.filter(user_type='kefu', kefu_profile__isnull=False).select_related('kefu_profile') + if phone: + users = users.filter(phone__icontains=phone) + if nicheng: + users = users.filter(kefu_profile__nicheng__icontains=nicheng) + if role_code: + # 筛选拥有该角色的用户 + user_ids_with_role = UserRole.objects.filter(role__role_code=role_code).values_list('account_id', flat=True) + users = users.filter(phone__in=user_ids_with_role) + + # 排序 + users = users.order_by('-create_time') + + # 分页 + paginator = Paginator(users, page_size) + page_obj = paginator.get_page(page) + + # 构建返回数据 + data_list = [] + for user in page_obj: + # 获取用户的所有角色 + user_roles = UserRole.objects.filter(account_id=user.phone).select_related('role') + roles_data = [{'role_code': ur.role.role_code, 'role_name': ur.role.role_name} for ur in user_roles] + data_list.append({ + 'phone': user.phone, + 'nicheng': user.kefu_profile.nicheng, + 'roles': roles_data, + 'status': user.kefu_profile.zhuangtai, + 'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S'), + 'update_time': user.update_time.strftime('%Y-%m-%d %H:%M:%S'), + }) + + return Response({ + 'code': 0, + 'data': { + 'list': data_list, + 'total': paginator.count, + 'page': page, + 'pageSize': page_size + } + })''' + + + + + +class ModifyAdminUserView(APIView): + """修改后台用户信息(昵称、密码、二级密码、状态、角色)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + action = request.data.get('action') + if not username or not action: + return Response({'code': 400, 'msg': '缺少参数'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + target_phone = request.data.get('phone', '').strip() + if not target_phone: + return Response({'code': 400, 'msg': '缺少目标用户账号'}) + + try: + target_user = UserMain.objects.get(phone=target_phone, user_type='kefu') + target_kefu = target_user.kefu_profile + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + except UserKefu.DoesNotExist: + return Response({'code': 404, 'msg': '用户扩展信息缺失'}) + + with transaction.atomic(): + if action == 'update_user_info': + # 修改昵称、状态、密码、二级密码 + nicheng = request.data.get('nicheng') + status = request.data.get('status') + password = request.data.get('password') + erjimima = request.data.get('erjimima') + if nicheng is not None: + target_kefu.nicheng = nicheng + if status is not None: + target_kefu.zhuangtai = int(status) + if password: + if len(password) < 6: + return Response({'code': 400, 'msg': '密码至少6位'}) + target_user.password = password + if erjimima: + if len(erjimima) < 6: + return Response({'code': 400, 'msg': '二级密码至少6位'}) + target_kefu.erjimima = erjimima # 假设二级密码也需要加密 + target_user.save() + target_kefu.save() + return Response({'code': 0, 'msg': '修改成功'}) + + elif action == 'add_user_roles': + role_codes = request.data.get('role_codes', []) + if not role_codes: + return Response({'code': 400, 'msg': '请选择角色'}) + for rc in role_codes: + try: + role = Role.objects.get(role_code=rc) + except Role.DoesNotExist: + continue + UserRole.objects.get_or_create(account_id=target_phone, role=role) + return Response({'code': 0, 'msg': '角色添加成功'}) + + elif action == 'remove_user_role': + role_code = request.data.get('role_code') + if not role_code: + return Response({'code': 400, 'msg': '缺少角色编码'}) + try: + role = Role.objects.get(role_code=role_code) + except Role.DoesNotExist: + return Response({'code': 404, 'msg': '角色不存在'}) + UserRole.objects.filter(account_id=target_phone, role=role).delete() + return Response({'code': 0, 'msg': '角色移除成功'}) + + else: + return Response({'code': 400, 'msg': '无效的action'}) + + + + + +class AddAdminUserView(APIView): + """添加后台用户(客服账号)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + phone = request.data.get('phone', '').strip() + password = request.data.get('password', '').strip() + erjimima = request.data.get('erjimima', '').strip() + nicheng = request.data.get('nicheng', '').strip() + role_codes = request.data.get('role_codes', []) + + if not all([phone, password, erjimima, nicheng]): + return Response({'code': 400, 'msg': '请填写完整信息'}) + if len(password) < 6 or len(erjimima) < 6: + return Response({'code': 400, 'msg': '密码和二级密码至少6位'}) + + # 校验手机号是否已被占用(且 user_type='kefu') + if UserMain.objects.filter(phone=phone, user_type='kefu').exists(): + return Response({'code': 400, 'msg': '该账号已存在'}) + + # 生成唯一的 yonghuid(7位数字) + while True: + yonghuid = str(random.randint(1000000, 9999999)) + if not UserMain.objects.filter(yonghuid=yonghuid).exists(): + break + + # 生成唯一的 openid(格式:admin_{时间戳}_{随机6位}) + timestamp = str(int(time.time() * 1000)) + random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) + openid = f"admin_{timestamp}_{random_suffix}" + while UserMain.objects.filter(openid=openid).exists(): + random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) + openid = f"admin_{timestamp}_{random_suffix}" + + with transaction.atomic(): + user_main = UserMain.objects.create( + yonghuid=yonghuid, + openid=openid, + phone=phone, + password=password, + user_type='kefu' + ) + # 创建客服扩展表(后台用户扩展) + UserKefu.objects.create( + user=user_main, + nicheng=nicheng, + erjimima=erjimima, + zhuangtai=1 + ) + # 分配角色 + for rc in role_codes: + try: + role = Role.objects.get(role_code=rc) + UserRole.objects.create(account_id=phone, role=role) + except Role.DoesNotExist: + pass + + return Response({'code': 0, 'msg': '添加成功'}) + + + + + + + + +class KefuGetDashouListView(APIView): + """ + 客服获取打手列表接口(仅打手类型) + 需要拥有以下任一权限:001aa,001bb,001cc,001dd,001ee,001ff,001gg + 请求:POST /yonghu/kefuhqdslb + 参数: + username: 客服手机号(用于权限校验) + keyword: 搜索关键词(可选,搜索用户ID或昵称) + page: 页码(从1开始) + page_size: 每页数量 + 返回: + code: 0 + data: { + list: [打手对象], + has_more: bool, + total: int, // 总打手数 + valid_dashou_count: int // 有效打手数(有有效会员) + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数(前端字段为 username) + username = request.data.get('username', '').strip() + keyword = request.data.get('keyword', '').strip() + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + except ValueError: + return Response({'code': 400, 'msg': '分页参数错误'}) + + if not username: + return Response({'code': 401, 'msg': '认证失败'}) + + # 2. 公共权限校验(验证客服身份,获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否拥有打手管理相关权限(至少一个) + required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看打手列表'}) + + # 4. 构建打手查询集(仅打手类型) + dashou_qs = UserDashou.objects.select_related('user').all() + + # 关键词筛选(用户ID或昵称) + if keyword: + dashou_qs = dashou_qs.filter( + Q(user__yonghuid__icontains=keyword) | + Q(nicheng__icontains=keyword) + ) + + # 5. 统计打手总数 + total_dashou = dashou_qs.count() + + # 6. 统计有效打手数(有会员且未过期) + dashou_user_ids = dashou_qs.values_list('user__yonghuid', flat=True) + now = timezone.now() + valid_user_ids = Huiyuangoumai.objects.filter( + yonghu_id__in=dashou_user_ids, + huiyuan_zhuangtai=1, + daoqi_time__gt=now + ).values_list('yonghu_id', flat=True).distinct() + valid_dashou_count = valid_user_ids.count() + + # 7. 分页 + offset = (page - 1) * page_size + dashou_list = dashou_qs.order_by('-create_time')[offset:offset + page_size] + + # 8. 构建返回数据 + result = [] + for dashou in dashou_list: + user = dashou.user + result.append({ + 'yonghuid': user.yonghuid, + 'avatar': user.avatar or '', + 'zaixianzhuangtai': dashou.zaixianzhuangtai, + 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, + 'nicheng': dashou.nicheng or '', + 'zhuangtai': dashou.zhuangtai, + }) + + has_more = (offset + len(dashou_list)) < total_dashou + + return Response({ + 'code': 0, + 'data': { + 'list': result, + 'has_more': has_more, + 'total': total_dashou, + 'valid_dashou_count': valid_dashou_count, + } + }) + + + +class KefuGetDashouDetailView(APIView): + """ + 客服获取打手详情接口 + 请求:POST /yonghu/kefuhqdsxq + 参数:{ + "username": "客服手机号", + "uid": "打手ID (yonghuid)" + } + 认证:JWT + 返回:code=0 + 打手信息 + 会员列表 + 所有会员类型 + 客服权限列表 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('phone', '').strip() + uid = request.data.get('uid', '').strip() + + if not username or not uid: + return Response({'code': 400, 'msg': '缺少参数'}) + + # 2. 公共权限校验(验证客服身份,获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 3. 检查是否拥有打手管理相关权限(至少一个) + required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看打手详情'}) + + # 4. 查询打手主表和扩展表 + try: + user_main = UserMain.objects.select_related('dashou_profile').get( + yonghuid=uid + ) + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '打手不存在'}) + + try: + dashou = user_main.dashou_profile + except ObjectDoesNotExist: + # 用户存在但未注册打手身份,返回基本信息 + user_info = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar or '', + 'zaixianzhuangtai': 0, + 'zhanghaozhuangtai': 0, + 'zhuangtai': 0, + 'nicheng': '', + 'chenghao': '', + 'dianhua': '', + 'wechat': '', + 'phone': user_main.phone or '', + 'create_time': user_main.create_time, + 'ip': user_main.ip or '', + 'jiedanzongliang': 0, + 'chengjiaozongliang': 0, + 'tuikuanliang': 0, + 'jinrijiedan': 0, + 'yue': 0.00, + 'zonge': 0.00, + 'jifen': 0, + 'yajin': 0.00, + 'jinrishouyi': 0.00, + 'jinyueshouyi': 0.00, + 'yaoqingren': '', + 'jieshao': '', + } + member_list = [] + # 仍然需要返回 all_members 和 permissions + all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao')) + return Response({ + 'code': 0, + 'data': { + 'user_info': user_info, + 'member_list': member_list, + 'all_members': all_members, + 'permissions': permissions, + } + }) + + # 5. 构建打手信息 + user_info = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar or '', + 'zaixianzhuangtai': dashou.zaixianzhuangtai, + 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, + 'zhuangtai': dashou.zhuangtai, + 'nicheng': dashou.nicheng or '', + 'chenghao': dashou.chenghao or '', + 'dianhua': dashou.dianhua or '', + 'wechat': dashou.wechat or '', + 'phone': user_main.phone or '', + 'create_time': user_main.create_time, + 'ip': user_main.ip or '', + 'jiedanzongliang': dashou.jiedanzongliang, + 'chengjiaozongliang': dashou.chengjiaozongliang, + 'tuikuanliang': dashou.tuikuanliang, + 'jinrijiedan': dashou.jinrijiedan, + 'yue': float(dashou.yue) if dashou.yue else 0.00, + 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, + 'jifen': dashou.jifen, + 'yajin': float(dashou.yajin) if dashou.yajin else 0.00, + 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00, + 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00, + 'yaoqingren': dashou.yaoqingren or '', + 'jieshao': dashou.jieshao or '', + } + + # 6. 查询打手已拥有的会员(未过期) + member_records = Huiyuangoumai.objects.filter( + yonghu_id=uid + ).order_by('-create_time') + member_list = [] + for record in member_records: + # 检查是否过期 + if hasattr(record, 'jiance_shifou_daoqi'): + is_expired = record.jiance_shifou_daoqi() + else: + is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True + + if not is_expired: + try: + huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id) + huiyuan_name = huiyuan.jieshao + except Huiyuan.DoesNotExist: + huiyuan_name = '未知会员' + member_list.append({ + 'id': record.id, + 'huiyuan_name': huiyuan_name, + 'daoqi_time': record.daoqi_time, + 'is_active': record.huiyuan_zhuangtai == 1, + 'huiyuan_id': record.huiyuan_id, # 用于删除操作 + }) + + # 7. 查询所有会员类型(用于前端添加会员) + all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao')) + + return Response({ + 'code': 0, + 'data': { + 'user_info': user_info, + 'member_list': member_list, + 'all_members': all_members, + 'permissions': permissions, # 返回当前客服的权限列表 + } + }) + + + + +'''class KefuGetDashouDetailView(APIView): + """ + 客服获取打手详情接口 + 请求:POST /yonghu/kefuhqdsxq + 参数:{ + "phone": "客服手机号", + "uid": "打手ID (yonghuid)" + } + 认证:JWT + 返回:code=0 + 打手信息 + 会员列表 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('phone', '').strip() + uid = request.data.get('uid', '').strip() + + if not username or not uid: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 客服身份验证 + current_user = request.user + + # 2. 公共权限校验(验证客服身份,获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否拥有打手管理相关权限(至少一个) + required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看打手列表'}) + + # 3. 查询打手主表和扩展表 + try: + # 使用 select_related 预加载 dashou_profile,减少数据库查询 + user_main = UserMain.objects.select_related('dashou_profile').get( + yonghuid=uid + ) + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 验证用户类型是否为打手(可选,但建议验证) + # 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册) + try: + dashou = user_main.dashou_profile + except ObjectDoesNotExist: + # 用户存在但未注册打手身份,返回基本信息 + user_info = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar or '', + 'zaixianzhuangtai': 0, + 'zhanghaozhuangtai': 0, + 'zhuangtai': 0, + 'nicheng': '', + 'chenghao': '', + 'dianhua': '', + 'wechat': '', + 'phone': user_main.phone or '', + 'create_time': user_main.create_time, + 'ip': user_main.ip or '', + 'jiedanzongliang': 0, + 'chengjiaozongliang': 0, + 'tuikuanliang': 0, + 'jinrijiedan': 0, + 'yue': 0.00, + 'zonge': 0.00, + 'jifen': 0, + 'yajin': 0.00, + 'jinrishouyi': 0.00, + 'jinyueshouyi': 0.00, + 'yaoqingren': '', + 'jieshao': '', + } + member_list = [] + return Response({ + 'code': 0, + 'data': { + 'user_info': user_info, + 'member_list': member_list + } + }) + + # 4. 构建打手信息 + user_info = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar or '', + 'zaixianzhuangtai': dashou.zaixianzhuangtai, + 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, + 'zhuangtai': dashou.zhuangtai, + 'nicheng': dashou.nicheng or '', + 'chenghao': dashou.chenghao or '', + 'dianhua': dashou.dianhua or '', + 'wechat': dashou.wechat or '', + 'phone': user_main.phone or '', + 'create_time': user_main.create_time, + 'ip': user_main.ip or '', + 'jiedanzongliang': dashou.jiedanzongliang, + 'chengjiaozongliang': dashou.chengjiaozongliang, + 'tuikuanliang': dashou.tuikuanliang, + 'jinrijiedan': dashou.jinrijiedan, + 'yue': float(dashou.yue) if dashou.yue else 0.00, + 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, + 'jifen': dashou.jifen, + 'yajin': float(dashou.yajin) if dashou.yajin else 0.00, + 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00, + 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00, + 'yaoqingren': dashou.yaoqingren or '', + 'jieshao': dashou.jieshao or '', + } + + # 5. 查询打手会员信息(仅未过期) + member_records = Huiyuangoumai.objects.filter( + yonghu_id=uid + ).order_by('-create_time') + + member_list = [] + for record in member_records: + # 调用模型方法检查是否过期 + if hasattr(record, 'jiance_shifou_daoqi'): + is_expired = record.jiance_shifou_daoqi() + else: + # 如果方法不存在,简单判断 + is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True + + if not is_expired: + # 获取会员详情 + try: + huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id) + huiyuan_name = huiyuan.jieshao + except Huiyuan.DoesNotExist: + huiyuan_name = '未知会员' + + member_list.append({ + 'id': record.id, + 'huiyuan_name': huiyuan_name, + 'daoqi_time': record.daoqi_time, + 'is_active': record.huiyuan_zhuangtai == 1, + }) + + return Response({ + 'code': 0, + 'data': { + 'user_info': user_info, + 'member_list': member_list + } + })''' + + + + + + +class KefuUpdateDashouView(APIView): + """ + 客服修改打手信息接口 + 支持操作: + - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb) + - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb) + - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd) + - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee) + - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee) + - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee) + - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg) + - gai_chenghao : 修改打手称号(升级/降级金牌) (权限001hh) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + dashou_id = request.data.get('dashou_id', '').strip() + caozuo = request.data.get('caozuo', '').strip() + value = request.data.get('value') # 金额、积分、天数、称号等 + huiyuan_id = request.data.get('huiyuan_id', '').strip() + + if not username or not dashou_id or not caozuo: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 3. 查询打手 + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '打手不存在'}) + except UserDashou.DoesNotExist: + return Response({'code': 404, 'msg': '打手扩展信息缺失'}) + + with transaction.atomic(): + try: + before = { + 'yue': dashou_profile.yue, + 'yajin': dashou_profile.yajin, + 'jifen': dashou_profile.jifen, + 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai, + 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, + 'zhuangtai': dashou_profile.zhuangtai, + 'chenghao': dashou_profile.chenghao, + } + + # ---------- 余额操作 ---------- + if caozuo == 'jian_yue': + if '001aa' not in permissions: + return Response({'code': 403, 'msg': '无权限减打手余额'}) + amount = Decimal(str(value)) + if dashou_profile.yue < amount: + return Response({'code': 400, 'msg': '余额不足'}) + dashou_profile.yue -= amount + dashou_profile.save() + after = dashou_profile.yue + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + xiugaitijiao=after, + xiugaitijiaoq=before['yue'], + ) + return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after)}}) + + elif caozuo == 'jia_yue': + if '001bb' not in permissions: + return Response({'code': 403, 'msg': '无权限加打手余额'}) + amount = Decimal(str(value)) + dashou_profile.yue += amount + dashou_profile.save() + after = dashou_profile.yue + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + xiugaitijiao=after, + xiugaitijiaoq=before['yue'], + ) + return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after)}}) + + # ---------- 押金操作 ---------- + elif caozuo == 'jian_yajin': + if '001aa' not in permissions: + return Response({'code': 403, 'msg': '无权限减打手押金'}) + amount = Decimal(str(value)) + if dashou_profile.yajin < amount: + return Response({'code': 400, 'msg': '押金不足'}) + dashou_profile.yajin -= amount + dashou_profile.save() + after = dashou_profile.yajin + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + yajin=after, + yyajin=before['yajin'], + ) + return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after)}}) + + elif caozuo == 'jia_yajin': + if '001bb' not in permissions: + return Response({'code': 403, 'msg': '无权限加打手押金'}) + amount = Decimal(str(value)) + dashou_profile.yajin += amount + dashou_profile.save() + after = dashou_profile.yajin + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + yajin=after, + yyajin=before['yajin'], + ) + return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after)}}) + + # ---------- 积分操作 ---------- + elif caozuo == 'jian_jifen': + if '001cc' not in permissions: + return Response({'code': 403, 'msg': '无权限减打手积分'}) + jifen = int(value) + if dashou_profile.jifen < jifen: + return Response({'code': 400, 'msg': '积分不足'}) + dashou_profile.jifen -= jifen + dashou_profile.save() + after = dashou_profile.jifen + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + jifen=after, + yjifen=before['jifen'], + ) + return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after}}) + + elif caozuo == 'jia_jifen': + if '001dd' not in permissions: + return Response({'code': 403, 'msg': '无权限加打手积分'}) + jifen = int(value) + dashou_profile.jifen += jifen + dashou_profile.save() + after = dashou_profile.jifen + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + jifen=after, + yjifen=before['jifen'], + ) + return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after}}) + + # ---------- 状态修改 ---------- + elif caozuo == 'gai_zhanghaozhuangtai': + if '001ee' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手账号状态'}) + new_status = int(value) + if new_status not in [0, 1]: + return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'}) + dashou_profile.zhanghaozhuangtai = new_status + dashou_profile.save() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + qitashuoming=f'修改账号状态为{new_status}', + ) + return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}}) + + elif caozuo == 'gai_zaixianzhuangtai': + if '001ee' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手在线状态'}) + new_status = int(value) + if new_status not in [0, 1]: + return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'}) + dashou_profile.zaixianzhuangtai = new_status + dashou_profile.save() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + qitashuoming=f'修改在线状态为{new_status}', + ) + return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}}) + + elif caozuo == 'gai_zhuangtai': + if '001ee' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手接单状态'}) + new_status = int(value) + if new_status not in [0, 1]: + return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'}) + dashou_profile.zhuangtai = new_status + dashou_profile.save() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + qitashuoming=f'修改接单状态为{new_status}', + ) + return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}}) + + # ---------- 会员操作 ---------- + elif caozuo == 'jia_huiyuan': + if '001ff' not in permissions: + return Response({'code': 403, 'msg': '无权限给打手加会员'}) + if not huiyuan_id: + return Response({'code': 400, 'msg': '缺少会员ID'}) + days = int(value) if value else 30 + try: + huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response({'code': 404, 'msg': '会员类型不存在'}) + record, created = Huiyuangoumai.objects.select_for_update().get_or_create( + yonghu_id=dashou_id, + huiyuan_id=huiyuan_id, + defaults={ + 'huiyuan_zhuangtai': 1, + 'jieshao': huiyuan.jieshao, + 'daoqi_time': timezone.now() + timedelta(days=days), + } + ) + if not created: + new_daoqi = record.daoqi_time + timedelta(days=days) + record.daoqi_time = new_daoqi + record.huiyuan_zhuangtai = 1 + record.save() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + huiyuants=days, + huiyuan_id=huiyuan_id, + qitashuoming=f'加会员 {huiyuan_id},增加{days}天', + ) + return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}}) + + elif caozuo == 'jian_huiyuan': + if '001gg' not in permissions: + return Response({'code': 403, 'msg': '无权限给打手减会员'}) + if not huiyuan_id: + return Response({'code': 400, 'msg': '缺少会员ID'}) + try: + record = Huiyuangoumai.objects.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id) + except Huiyuangoumai.DoesNotExist: + return Response({'code': 404, 'msg': '该打手未购买此会员'}) + record.delete() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + huiyuan_id=huiyuan_id, + qitashuoming=f'移除会员 {huiyuan_id}', + ) + return Response({'code': 0, 'msg': '减会员成功'}) + + # ---------- 称号修改(金牌打手) ---------- + elif caozuo == 'gai_chenghao': + if '001hh' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手称号'}) + new_chenghao = str(value).strip() + if not new_chenghao: + return Response({'code': 400, 'msg': '称号不能为空'}) + # 可限制合法值,这里只记录日志 + dashou_profile.chenghao = new_chenghao + dashou_profile.save() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + qitashuoming=f'修改称号:{before["chenghao"] or "无"} → {new_chenghao}', + ) + return Response({'code': 0, 'msg': '修改称号成功', 'data': {'new_chenghao': new_chenghao}}) + + else: + return Response({'code': 400, 'msg': '未知操作类型'}) + + except Exception as e: + logger.error(f"修改打手信息异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + +class KefuGetShangjiaListView(APIView): + """ + 客服/管理员获取商家列表接口 + 需要拥有以下任一权限:1100a, 1100b + 请求:POST /houtai/hqsjgllb + 参数: + username: 客服手机号(用于权限校验,必填) + keyword: 搜索关键词(可选,搜索商家用户ID或昵称) + balance_op: 余额比较符,gte(>=) / lte(<=)(可选) + balance_value: 余额数值(可选,需与balance_op同时存在) + order_count_op: 派单量比较符,gte(>=) / lte(<=)(可选) + order_count_value: 派单量数值(可选,需与order_count_op同时存在) + status: 商家状态,1=正常,2=禁用(可选) + page: 页码,从1开始,默认1 + page_size: 每页数量,默认20 + 返回: + code: 0 成功,其他失败 + data: { + list: [商家对象], + total: int, // 总商家数 + } + """ + permission_classes = [] # 不依赖全局认证,手动校验 + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + keyword = request.data.get('keyword', '').strip() + balance_op = request.data.get('balance_op', '').strip().lower() + balance_value_str = request.data.get('balance_value') + order_count_op = request.data.get('order_count_op', '').strip().lower() + order_count_value_str = request.data.get('order_count_value') + status_str = request.data.get('status') + + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + if page < 1 or page_size < 1: + raise ValueError + except ValueError: + return Response({'code': 400, 'msg': '分页参数错误'}) + + if not username: + return Response({'code': 401, 'msg': '认证失败,缺少username'}) + + # 2. 公共权限校验(复用现有函数 verify_kefu_permission) + # 假设 verify_kefu_permission 定义在某个公共模块,这里引入 + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查商家管理权限(1100a 或 1100b) + required_perms = {'1100a', '1100b'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看商家列表'}) + + # 4. 构建商家查询集(商家扩展表 + 关联用户主表) + shangjia_qs = UserShangjia.objects.select_related('user').all() + + # 关键词筛选(用户ID或昵称) + if keyword: + shangjia_qs = shangjia_qs.filter( + Q(user__yonghuid__icontains=keyword) | + Q(nicheng__icontains=keyword) + ) + + # 余额筛选 + if balance_op and balance_value_str is not None and balance_value_str != '': + try: + balance_value = Decimal(str(balance_value_str)) + if balance_op == 'gte': + shangjia_qs = shangjia_qs.filter(yue__gte=balance_value) + elif balance_op == 'lte': + shangjia_qs = shangjia_qs.filter(yue__lte=balance_value) + else: + return Response({'code': 400, 'msg': '余额比较符只支持gte或lte'}) + except Exception: + return Response({'code': 400, 'msg': '余额数值格式错误'}) + + # 派单总量筛选(fabu字段) + if order_count_op and order_count_value_str is not None and order_count_value_str != '': + try: + order_count_value = int(order_count_value_str) + if order_count_op == 'gte': + shangjia_qs = shangjia_qs.filter(fabu__gte=order_count_value) + elif order_count_op == 'lte': + shangjia_qs = shangjia_qs.filter(fabu__lte=order_count_value) + else: + return Response({'code': 400, 'msg': '派单量比较符只支持gte或lte'}) + except Exception: + return Response({'code': 400, 'msg': '派单量数值必须为整数'}) + + # 商家状态筛选 + if status_str is not None and status_str != '': + try: + status_val = int(status_str) + if status_val not in [1, 2]: + return Response({'code': 400, 'msg': '商家状态值无效,应为1或2'}) + shangjia_qs = shangjia_qs.filter(zhuangtai=status_val) + except ValueError: + return Response({'code': 400, 'msg': '商家状态格式错误'}) + + # 5. 统计总商家数(应用筛选后) + total = shangjia_qs.count() + + # 6. 分页 + offset = (page - 1) * page_size + shangjia_list = shangjia_qs.order_by('-create_time')[offset:offset + page_size] + + # 7. 构建返回数据 + result = [] + for shangjia in shangjia_list: + user = shangjia.user + result.append({ + 'yonghuid': user.yonghuid, + 'avatar': user.avatar or '', + 'nicheng': shangjia.nicheng or '', + 'yue': str(shangjia.yue), # 转换为字符串,避免前端精度问题 + 'fabu': shangjia.fabu, + 'chengjiao': shangjia.chengjiao, + 'zhuangtai': shangjia.zhuangtai, # 1=正常,2=禁用 + }) + + return Response({ + 'code': 0, + 'data': { + 'list': result, + 'total': total, + } + }) + + + + + + +# yonghu/views.py 中添加 + +class KefuGetShangjiaDetailView(APIView): + """ + 客服/管理员获取商家详情接口 + 需要权限:1100a 或 1100b + 请求:POST /houtai/hthqsjxq + 参数: + username: 客服手机号(用于权限校验,必填) + yonghuid: 商家用户ID(必填) + 返回: + code: 0 成功 + data: 商家所有字段(含主表头像等) + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '认证失败,缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少商家ID'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + required_perms = {'1100a', '1100b'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看商家详情'}) + + # 查询商家 + try: + shangjia = UserShangjia.objects.select_related('user').get(user__yonghuid=yonghuid) + except UserShangjia.DoesNotExist: + return Response({'code': 404, 'msg': '商家不存在'}) + + user = shangjia.user + + data = { + 'yonghuid': user.yonghuid, + 'avatar': user.avatar or '', + 'nicheng': shangjia.nicheng, + 'zhuangtai': shangjia.zhuangtai, # 1正常,2禁用 + 'dianhua': shangjia.dianhua, + 'wechat': shangjia.wechat, + 'yue': str(shangjia.yue), + 'fabu': shangjia.fabu, + 'chengjiao': shangjia.chengjiao, + 'tuikuan': shangjia.tuikuan, + 'jinridingdan': shangjia.jinridingdan, + 'jinriliushui': str(shangjia.jinriliushui), + 'jinyuedingdan': shangjia.jinyuedingdan, + 'jinyueliushui': str(shangjia.jinyueliushui), + 'create_time': shangjia.create_time.isoformat() if shangjia.create_time else None, + } + + return Response({'code': 0, 'data': data}) + + + + + + + +class KefuUpdateShangjiaView(APIView): + """ + 客服/管理员修改商家信息(余额、状态) + POST /houtai/xgsjxx + 参数: + username, yonghuid, action + action可选:add_balance, sub_balance, ban, unban + amount(add_balance/sub_balance时必填) + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + action = request.data.get('action', '').strip().lower() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少商家ID'}) + if action not in ('add_balance', 'sub_balance', 'ban', 'unban'): + return Response({'code': 400, 'msg': 'action参数无效'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 操作权限校验 + if action in ('add_balance', 'ban', 'unban') and '1100a' not in permissions: + return Response({'code': 403, 'msg': '无权限执行此操作(需要1100a)'}) + if action == 'sub_balance' and '1100b' not in permissions: + return Response({'code': 403, 'msg': '无权限减少商家余额(需要1100b)'}) + + # 使用事务,select_for_update 必须在事务内 + with transaction.atomic(): + try: + shangjia = UserShangjia.objects.select_for_update().get(user__yonghuid=yonghuid) + except UserShangjia.DoesNotExist: + return Response({'code': 404, 'msg': '商家不存在'}) + + # 增加余额 + if action == 'add_balance': + amount_str = request.data.get('amount') + try: + amount = Decimal(str(amount_str)) + if amount <= 0 or amount > 1000000: + return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + new_yue = shangjia.yue + amount + if new_yue > 999999999.99: + return Response({'code': 400, 'msg': '增加后余额超出上限'}) + shangjia.yue = new_yue + shangjia.save(update_fields=['yue']) + return Response({'code': 0, 'msg': '余额增加成功', 'data': {'new_balance': str(shangjia.yue)}}) + + # 减少余额 + if action == 'sub_balance': + amount_str = request.data.get('amount') + try: + amount = Decimal(str(amount_str)) + if amount <= 0 or amount > 1000000: + return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + if shangjia.yue < amount: + return Response({'code': 400, 'msg': '余额不足'}) + shangjia.yue = shangjia.yue - amount + shangjia.save(update_fields=['yue']) + return Response({'code': 0, 'msg': '余额减少成功', 'data': {'new_balance': str(shangjia.yue)}}) + + # 封禁 + if action == 'ban': + if shangjia.zhuangtai == 2: + return Response({'code': 400, 'msg': '商家已是封禁状态'}) + shangjia.zhuangtai = 2 + shangjia.save(update_fields=['zhuangtai']) + return Response({'code': 0, 'msg': '商家已封禁', 'data': {'zhuangtai': 2}}) + + # 解封 + if action == 'unban': + if shangjia.zhuangtai == 1: + return Response({'code': 400, 'msg': '商家已是正常状态'}) + shangjia.zhuangtai = 1 + shangjia.save(update_fields=['zhuangtai']) + return Response({'code': 0, 'msg': '商家已解封', 'data': {'zhuangtai': 1}}) + + + + + + + +class GetProductBaseDataView(APIView): + """ + 获取商品基础数据(类型、专区、会员) + 权限:2200a + POST /houtai/htsphqjcsx + 参数:username + 返回: + code:0, data: { + type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...], + zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...], + member_list: [ {huiyuan_id, jieshao, jiage}, ...] + } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取所有商品类型(按 paixu 升序) + type_qs = ShangpinLeixing.objects.all().order_by('paixu') + type_list = [] + for t in type_qs: + # 🔥 修改:统计该类型下的公共商品数量(店铺为空且店铺商品类型为空) + product_count = Shangpin.objects.filter( + leixing_id=t.id, + dianpu__isnull=True, # 所属店铺为空 + dianpu_leixing__isnull=True # 店铺商品类型为空 + ).count() + type_list.append({ + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'paixu': t.paixu, + 'shenhezhuangtai': t.shenhezhuangtai, + 'product_count': product_count, + }) + + # 获取所有专区(按 paixu 升序) + zone_qs = ShangpinZhuanqu.objects.all().order_by('paixu') + zone_list = [] + for z in zone_qs: + # 🔥 修改:统计该专区下的公共商品数量(店铺为空且店铺商品类型为空) + product_count = Shangpin.objects.filter( + zhuanqu_id=z.id, + dianpu__isnull=True, # 所属店铺为空 + dianpu_leixing__isnull=True # 店铺商品类型为空 + ).count() + zone_list.append({ + 'id': z.id, + 'mingzi': z.mingzi or '', + 'leixing_id': z.leixing_id, + 'paixu': z.paixu, + 'shenhezhuangtai': z.shenhezhuangtai, + 'product_count': product_count, + }) + + # 获取所有会员(只需 ID、介绍、价格) + member_list = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage')) + + return Response({ + 'code': 0, + 'data': { + 'type_list': type_list, + 'zone_list': zone_list, + 'member_list': member_list, + } + }) + + + +class GetProductListView(APIView): + """ + 获取商品列表(支持筛选、排序) + 权限:2200a + POST /houtai/hthqsplb + 参数: + username: 必填 + leixing_id: 类型ID(可选) + zhuanqu_id: 专区ID(可选) + keyword: 搜索关键词(商品标题/介绍) + sales_op: 销量比较符 gte/lte + sales_value: 销量数值 + stock_op: 库存比较符 gte/lte + stock_value: 库存数值 + sort_field: 排序字段 zhenshi_xiaoliang / jiage + sort_order: asc/desc + page: 页码(默认1) + page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留) + 返回: + code:0, data: { list: [...], total } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取参数 + leixing_id = request.data.get('leixing_id') + zhuanqu_id = request.data.get('zhuanqu_id') + keyword = request.data.get('keyword', '').strip() + sales_op = request.data.get('sales_op', '').strip().lower() + sales_value = request.data.get('sales_value') + stock_op = request.data.get('stock_op', '').strip().lower() + stock_value = request.data.get('stock_value') + sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage + sort_order = request.data.get('sort_order', 'desc').lower() + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 1000)) + if page < 1 or page_size < 1: + raise ValueError + except: + return Response({'code': 400, 'msg': '分页参数错误'}) + + # 🔥 修改:构建查询集,增加公共商品筛选(店铺为空且店铺商品类型为空) + qs = Shangpin.objects.filter( + dianpu__isnull=True, # 所属店铺为空 + dianpu_leixing__isnull=True # 店铺商品类型为空 + ) + + if leixing_id: + try: + leixing_id = int(leixing_id) + qs = qs.filter(leixing_id=leixing_id) + except: + pass + if zhuanqu_id: + try: + zhuanqu_id = int(zhuanqu_id) + qs = qs.filter(zhuanqu_id=zhuanqu_id) + except: + pass + if keyword: + qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword)) + + # 销量筛选 + if sales_op and sales_value is not None: + try: + sales_val = int(sales_value) + if sales_op == 'gte': + qs = qs.filter(zhenshi_xiaoliang__gte=sales_val) + elif sales_op == 'lte': + qs = qs.filter(zhenshi_xiaoliang__lte=sales_val) + except: + pass + + # 库存筛选 + if stock_op and stock_value is not None: + try: + stock_val = int(stock_value) + if stock_op == 'gte': + qs = qs.filter(kucun__gte=stock_val) + elif stock_op == 'lte': + qs = qs.filter(kucun__lte=stock_val) + except: + pass + + # 排序 + if sort_field in ('zhenshi_xiaoliang', 'jiage'): + if sort_order == 'asc': + qs = qs.order_by(sort_field) + else: + qs = qs.order_by(f'-{sort_field}') + else: + qs = qs.order_by('-create_time') # 默认按创建时间倒序 + + # 分页 + total = qs.count() + offset = (page - 1) * page_size + products = qs[offset:offset + page_size] + + # 构建返回数据 + product_list = [] + for p in products: + product_list.append({ + 'id': p.id, + 'biaoqian': p.biaoqian or '', + 'jiage': str(p.jiage), + 'kucun': p.kucun, + 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, + 'tupian_url': p.tupian_url or '', + 'jieshao': p.jieshao or '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': product_list, + 'total': total, + } + }) + + + + +'''class GetProductBaseDataView(APIView): + """ + 获取商品基础数据(类型、专区、会员) + 权限:2200a + POST /houtai/htsphqjcsx + 参数:username + 返回: + code:0, data: { + type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...], + zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...], + member_list: [ {huiyuan_id, jieshao, jiage}, ...] + } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取所有商品类型(按 paixu 升序) + type_qs = ShangpinLeixing.objects.all().order_by('paixu') + type_list = [] + for t in type_qs: + # 统计该类型下的商品数量(忽略审核状态?通常统计所有商品,可根据需求调整) + product_count = Shangpin.objects.filter(leixing_id=t.id).count() + type_list.append({ + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'paixu': t.paixu, + 'shenhezhuangtai': t.shenhezhuangtai, + 'product_count': product_count, + }) + + # 获取所有专区(按 paixu 升序) + zone_qs = ShangpinZhuanqu.objects.all().order_by('paixu') + zone_list = [] + for z in zone_qs: + product_count = Shangpin.objects.filter(zhuanqu_id=z.id).count() + zone_list.append({ + 'id': z.id, + 'mingzi': z.mingzi or '', + 'leixing_id': z.leixing_id, + 'paixu': z.paixu, + 'shenhezhuangtai': z.shenhezhuangtai, + 'product_count': product_count, + }) + + # 获取所有会员(只需 ID、介绍、价格) + member_list = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage')) + + return Response({ + 'code': 0, + 'data': { + 'type_list': type_list, + 'zone_list': zone_list, + 'member_list': member_list, + } + }) + + +class GetProductListView(APIView): + """ + 获取商品列表(支持筛选、排序) + 权限:2200a + POST /houtai/hthqsplb + 参数: + username: 必填 + leixing_id: 类型ID(可选) + zhuanqu_id: 专区ID(可选) + keyword: 搜索关键词(商品标题/介绍) + sales_op: 销量比较符 gte/lte + sales_value: 销量数值 + stock_op: 库存比较符 gte/lte + stock_value: 库存数值 + sort_field: 排序字段 zhenshi_xiaoliang / jiage + sort_order: asc/desc + page: 页码(默认1) + page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留) + 返回: + code:0, data: { list: [...], total } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取参数 + leixing_id = request.data.get('leixing_id') + zhuanqu_id = request.data.get('zhuanqu_id') + keyword = request.data.get('keyword', '').strip() + sales_op = request.data.get('sales_op', '').strip().lower() + sales_value = request.data.get('sales_value') + stock_op = request.data.get('stock_op', '').strip().lower() + stock_value = request.data.get('stock_value') + sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage + sort_order = request.data.get('sort_order', 'desc').lower() + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 1000)) + if page < 1 or page_size < 1: + raise ValueError + except: + return Response({'code': 400, 'msg': '分页参数错误'}) + + # 构建查询集 + qs = Shangpin.objects.all() + + if leixing_id: + try: + leixing_id = int(leixing_id) + qs = qs.filter(leixing_id=leixing_id) + except: + pass + if zhuanqu_id: + try: + zhuanqu_id = int(zhuanqu_id) + qs = qs.filter(zhuanqu_id=zhuanqu_id) + except: + pass + if keyword: + qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword)) + + # 销量筛选 + if sales_op and sales_value is not None: + try: + sales_val = int(sales_value) + if sales_op == 'gte': + qs = qs.filter(zhenshi_xiaoliang__gte=sales_val) + elif sales_op == 'lte': + qs = qs.filter(zhenshi_xiaoliang__lte=sales_val) + except: + pass + + # 库存筛选 + if stock_op and stock_value is not None: + try: + stock_val = int(stock_value) + if stock_op == 'gte': + qs = qs.filter(kucun__gte=stock_val) + elif stock_op == 'lte': + qs = qs.filter(kucun__lte=stock_val) + except: + pass + + # 排序 + if sort_field in ('zhenshi_xiaoliang', 'jiage'): + if sort_order == 'asc': + qs = qs.order_by(sort_field) + else: + qs = qs.order_by(f'-{sort_field}') + else: + qs = qs.order_by('-create_time') # 默认按创建时间倒序 + + # 分页 + total = qs.count() + offset = (page - 1) * page_size + products = qs[offset:offset + page_size] + + # 构建返回数据 + product_list = [] + for p in products: + product_list.append({ + 'id': p.id, + 'biaoqian': p.biaoqian or '', + 'jiage': str(p.jiage), + 'kucun': p.kucun, + 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, + 'tupian_url': p.tupian_url or '', + 'jieshao': p.jieshao or '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': product_list, + 'total': total, + } + })''' + + +class SaveProductOrderView(APIView): + """ + 保存商品类型或专区的排序 + 权限:2200a + POST /houtai/htsppaixu + 参数: + username: 必填 + type: 'leixing' 或 'zhuanqu' + leixing_id: 当 type='zhuanqu' 时必填(用于限定该类型下的专区) + items: [ {id, paixu}, ... ] # 每个项目的新排序值(从1开始) + 返回: + code:0 成功 + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + sort_type = request.data.get('type', '').strip().lower() + if sort_type not in ('leixing', 'zhuanqu'): + return Response({'code': 400, 'msg': 'type参数必须为leixing或zhuanqu'}) + + items = request.data.get('items') + if not isinstance(items, list) or len(items) == 0: + return Response({'code': 400, 'msg': 'items参数无效'}) + + # 校验每个 item 的格式 + for item in items: + if not isinstance(item, dict) or 'id' not in item or 'paixu' not in item: + return Response({'code': 400, 'msg': 'items内每项需包含id和paixu'}) + try: + item['id'] = int(item['id']) + item['paixu'] = int(item['paixu']) + if item['paixu'] < 1: + return Response({'code': 400, 'msg': 'paixu必须为正整数'}) + except: + return Response({'code': 400, 'msg': 'id或paixu格式错误'}) + + with transaction.atomic(): + if sort_type == 'leixing': + # 更新商品类型的排序 + for item in items: + ShangpinLeixing.objects.filter(id=item['id']).update(paixu=item['paixu']) + else: # zhuanqu + leixing_id = request.data.get('leixing_id') + if not leixing_id: + return Response({'code': 400, 'msg': '更新专区排序时必须提供leixing_id'}) + try: + leixing_id = int(leixing_id) + except: + return Response({'code': 400, 'msg': 'leixing_id格式错误'}) + # 可选:检查该专区是否属于该类型,但不强制,由前端保证 + for item in items: + ShangpinZhuanqu.objects.filter(id=item['id']).update(paixu=item['paixu']) + + return Response({'code': 0, 'msg': '排序保存成功'}) + + +class AddProductView(APIView): + """ + 添加商品(含图片上传) + 权限:2200a + POST /houtai/htfbsp + 参数:multipart/form-data,包含所有商品字段及图片文件 + 字段说明见代码 + """ + permission_classes = [] + parser_classes = [MultiPartParser, FormParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取表单字段 + biaoqian = request.data.get('biaoqian', '').strip() + jiage_str = request.data.get('jiage') + kucun_str = request.data.get('kucun') + duiwai_xiaoliang_str = request.data.get('duiwai_xiaoliang', '0') + leixing_id_str = request.data.get('leixing_id') + zhuanqu_id_str = request.data.get('zhuanqu_id') + yaoqiuleixing_str = request.data.get('yaoqiuleixing', '1') + huiyuan_id = request.data.get('huiyuan_id', '').strip() + yongjin_str = request.data.get('yongjin', '0') + fencheng_type = request.data.get('fencheng_type', 'default') + ewai_dashou_fencheng_str = request.data.get('ewai_dashou_fencheng', '0') + jieshao = request.data.get('jieshao', '').strip() + xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip() + # 图片文件 + image_file = request.FILES.get('file') # 前端上传时字段名为 'file' + + # 必填字段校验 + if not biaoqian: + return Response({'code': 400, 'msg': '商品标题不能为空'}) + try: + jiage = Decimal(jiage_str) + if jiage <= 0: + return Response({'code': 400, 'msg': '价格必须大于0'}) + except: + return Response({'code': 400, 'msg': '价格格式错误'}) + try: + kucun = int(kucun_str) + if kucun < 0: + return Response({'code': 400, 'msg': '库存不能为负数'}) + except: + return Response({'code': 400, 'msg': '库存格式错误'}) + try: + duiwai_xiaoliang = int(duiwai_xiaoliang_str) + except: + duiwai_xiaoliang = 0 + try: + leixing_id = int(leixing_id_str) + except: + return Response({'code': 400, 'msg': '商品类型ID无效'}) + try: + zhuanqu_id = int(zhuanqu_id_str) + except: + return Response({'code': 400, 'msg': '商品专区ID无效'}) + try: + yaoqiuleixing = int(yaoqiuleixing_str) + if yaoqiuleixing not in (1, 2): + raise ValueError + except: + return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'}) + + # 校验类型和专区的存在性及关联性 + try: + leixing_obj = ShangpinLeixing.objects.get(id=leixing_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 400, 'msg': '商品类型不存在'}) + try: + zhuanqu_obj = ShangpinZhuanqu.objects.get(id=zhuanqu_id) + if zhuanqu_obj.leixing_id != leixing_id: + return Response({'code': 400, 'msg': '专区不属于该商品类型'}) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 400, 'msg': '商品专区不存在'}) + + # 处理抢单要求相关字段 + huiyuan_id_val = None + yongjin_val = None + if yaoqiuleixing == 1: + # 会员要求 + if not huiyuan_id: + # 未传递时,尝试从商品类型中获取默认会员ID + huiyuan_id_val = leixing_obj.huiyuan_id + if not huiyuan_id_val: + return Response({'code': 400, 'msg': '未指定会员且商品类型无默认会员'}) + else: + huiyuan_id_val = huiyuan_id + # 验证会员是否存在 + if not Huiyuan.objects.filter(huiyuan_id=huiyuan_id_val).exists(): + return Response({'code': 400, 'msg': '指定的会员不存在'}) + else: + # 佣金要求 + try: + yongjin_val = Decimal(yongjin_str) + if yongjin_val < 0: + return Response({'code': 400, 'msg': '佣金不能为负数'}) + except: + # 若未传递或无效,尝试从商品类型中获取默认佣金 + if leixing_obj.yongjin is not None: + yongjin_val = leixing_obj.yongjin + else: + return Response({'code': 400, 'msg': '佣金格式错误且类型无默认佣金'}) + + # 处理打手分成方式 + kaioi_ewai = False + ewai_fencheng = Decimal('0') + if fencheng_type == 'custom': + try: + ewai_fencheng = Decimal(ewai_dashou_fencheng_str) + if ewai_fencheng < 0: + return Response({'code': 400, 'msg': '自定义分成金额不能为负数'}) + kaioi_ewai = True + except: + return Response({'code': 400, 'msg': '自定义分成金额格式错误'}) + + # 图片上传处理 + tupian_url = '' + if image_file: + # 验证图片 + valid, msg = validate_image(image_file) + if not valid: + return Response({'code': 400, 'msg': f'图片无效:{msg}'}) + # 生成唯一文件名 + ext = image_file.name.split('.')[-1].lower() + if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: + return Response({'code': 400, 'msg': '不支持的图片格式'}) + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + random_str = uuid.uuid4().hex[:8] + filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}" + # 上传到 OSS + uploaded_url = upload_to_oss(image_file, filename) + if not uploaded_url: + return Response({'code': 500, 'msg': '图片上传失败,请稍后重试'}) + tupian_url = filename # 存储相对路径,前端拼接域名 + else: + return Response({'code': 400, 'msg': '商品图片不能为空'}) + + # 创建商品 + try: + with transaction.atomic(): + product = Shangpin.objects.create( + biaoqian=biaoqian, + jiage=jiage, + kucun=kucun, + leixing_id=leixing_id, + zhuanqu_id=zhuanqu_id, + zhenshi_xiaoliang=0, # 初始真实销量为0 + duiwai_xiaoliang=duiwai_xiaoliang, + jieshao=jieshao, + xiadan_xuzhi=xiadan_xuzhi, + tupian_url=tupian_url, + yaoqiuleixing=yaoqiuleixing, + huiyuan_id=huiyuan_id_val, + yongjin=yongjin_val, + kaioi_ewai_dashou_fencheng=kaioi_ewai, + ewai_dashou_fencheng=ewai_fencheng, + paixu=0, # 新商品默认排序最后 + shenhezhuangtai=1, # 正常状态 + ) + except Exception as e: + logger.error(f"创建商品失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '创建商品失败,请稍后重试'}) + + return Response({ + 'code': 0, + 'msg': '商品发布成功', + 'data': { + 'id': product.id, + 'tupian_url': tupian_url, + } + }) + + +# houtai/views_product.py 中添加 + +class DeleteProductView(APIView): + """ + 删除商品(物理删除,同时删除OSS图片) + 权限:2200a + POST /houtai/htscsp + 参数: + username: 客服/管理员账号 + id: 商品ID + 返回: + code:0 成功 + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + product_id = request.data.get('id') + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not product_id: + return Response({'code': 400, 'msg': '缺少商品ID'}) + try: + product_id = int(product_id) + except: + return Response({'code': 400, 'msg': '商品ID格式错误'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 查询商品是否存在 + try: + product = Shangpin.objects.get(id=product_id) + except Shangpin.DoesNotExist: + return Response({'code': 404, 'msg': '商品不存在'}) + + # 先删除OSS上的图片 + tupian_url = product.tupian_url + if tupian_url: + # 调用已有的 delete_from_oss 函数 + delete_success = delete_from_oss(tupian_url) + if not delete_success: + logger.warning(f"删除商品图片失败: {tupian_url}, 商品ID: {product_id}") + # 注意:即使图片删除失败,也应继续删除数据库记录,避免数据不一致 + # 但可以记录日志供后续人工处理 + + # 删除数据库记录 + product.delete() + + logger.info(f"商品删除成功: id={product_id}, 图片路径={tupian_url}, 操作人={username}") + return Response({'code': 0, 'msg': '商品删除成功'}) + + + + + + + +class GetProductDetailView(APIView): + """ + 获取商品详情(包含商品类型、专区、会员列表) + 权限:2200a + POST /houtai/htsphqxq + 参数: + username: 客服/管理员账号 + id: 商品ID + 返回: + code:0, data: { product, type_list, zone_list, member_list } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + product_id = request.data.get('id') + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not product_id: + return Response({'code': 400, 'msg': '缺少商品ID'}) + try: + product_id = int(product_id) + except ValueError: + return Response({'code': 400, 'msg': '商品ID格式错误'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 查询商品 + try: + product = Shangpin.objects.get(id=product_id) + except Shangpin.DoesNotExist: + return Response({'code': 404, 'msg': '商品不存在'}) + + # 查询所有商品类型、专区、会员(用于前端下拉选择) + type_list = list(ShangpinLeixing.objects.all().values( + 'id', 'jieshao', 'tupian_url', 'shenhezhuangtai' + )) + zone_list = list(ShangpinZhuanqu.objects.all().values( + 'id', 'mingzi', 'leixing_id', 'shenhezhuangtai' + )) + member_list = list(Huiyuan.objects.all().values( + 'huiyuan_id', 'jieshao', 'jiage' + )) + + # 构建商品返回数据 + product_data = { + 'id': product.id, + 'biaoqian': product.biaoqian, + 'jiage': str(product.jiage), + 'kucun': product.kucun, + 'leixing_id': product.leixing_id, + 'zhuanqu_id': product.zhuanqu_id, + 'zhenshi_xiaoliang': product.zhenshi_xiaoliang, + 'duiwai_xiaoliang': product.duiwai_xiaoliang, + 'jieshao': product.jieshao, + 'xiadan_xuzhi': product.xiadan_xuzhi, + 'guize_tupian': product.guize_tupian, + 'yaoqiuleixing': product.yaoqiuleixing, + 'huiyuan_id': product.huiyuan_id, + 'yongjin': str(product.yongjin) if product.yongjin is not None else None, + 'tupian_url': product.tupian_url, + 'kaioi_ewai_dashou_fencheng': product.kaioi_ewai_dashou_fencheng, + 'ewai_dashou_fencheng': str(product.ewai_dashou_fencheng) if product.ewai_dashou_fencheng is not None else '0', + 'create_time': product.create_time.isoformat() if product.create_time else None, + 'shenhezhuangtai': product.shenhezhuangtai, + } + + return Response({ + 'code': 0, + 'data': { + 'product': product_data, + 'type_list': type_list, + 'zone_list': zone_list, + 'member_list': member_list, + } + }) + + + +class UpdateProductView(APIView): + """ + 修改商品(支持商品图片和规则图片的更新) + 权限:2200a + POST /houtai/bcspxg + """ + permission_classes = [] + parser_classes = [MultiPartParser, FormParser] + + def post(self, request): + username = request.data.get('username', '').strip() + product_id = request.data.get('id') + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not product_id: + return Response({'code': 400, 'msg': '缺少商品ID'}) + try: + product_id = int(product_id) + except ValueError: + return Response({'code': 400, 'msg': '商品ID格式错误'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 使用事务,将 select_for_update 放在事务内 + with transaction.atomic(): + try: + product = Shangpin.objects.select_for_update().get(id=product_id) + except Shangpin.DoesNotExist: + return Response({'code': 404, 'msg': '商品不存在'}) + + # ---------- 1. 基础字段 ---------- + biaoqian = request.data.get('biaoqian', '').strip() + if not biaoqian: + return Response({'code': 400, 'msg': '商品标题不能为空'}) + + try: + jiage = Decimal(str(request.data.get('jiage', 0))) + if jiage <= 0: + return Response({'code': 400, 'msg': '价格必须大于0'}) + except: + return Response({'code': 400, 'msg': '价格格式错误'}) + + try: + kucun = int(request.data.get('kucun', 0)) + if kucun < 0: + return Response({'code': 400, 'msg': '库存不能为负数'}) + except: + return Response({'code': 400, 'msg': '库存格式错误'}) + + try: + duiwai_xiaoliang = int(request.data.get('duiwai_xiaoliang', 0)) + except: + duiwai_xiaoliang = 0 + + try: + leixing_id = int(request.data.get('leixing_id')) + except: + return Response({'code': 400, 'msg': '商品类型ID无效'}) + + try: + zhuanqu_id = int(request.data.get('zhuanqu_id')) + except: + return Response({'code': 400, 'msg': '商品专区ID无效'}) + + try: + yaoqiuleixing = int(request.data.get('yaoqiuleixing', 1)) + if yaoqiuleixing not in (1, 2): + raise ValueError + except: + return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'}) + + try: + shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) + except: + shenhezhuangtai = 1 + + # 校验类型和专区存在性 + if not ShangpinLeixing.objects.filter(id=leixing_id).exists(): + return Response({'code': 400, 'msg': '商品类型不存在'}) + if not ShangpinZhuanqu.objects.filter(id=zhuanqu_id, leixing_id=leixing_id).exists(): + return Response({'code': 400, 'msg': '专区不存在或不属于该类型'}) + + # ---------- 2. 会员/佣金 ---------- + huiyuan_id = None + yongjin = None + if yaoqiuleixing == 1: + huiyuan_id = request.data.get('huiyuan_id', '').strip() or None + if huiyuan_id and not Huiyuan.objects.filter(huiyuan_id=huiyuan_id).exists(): + return Response({'code': 400, 'msg': '指定的会员不存在'}) + else: + try: + yongjin = Decimal(str(request.data.get('yongjin', 0))) + if yongjin < 0: + return Response({'code': 400, 'msg': '佣金不能为负数'}) + except: + yongjin = Decimal('0') + + # ---------- 3. 打手分成 ---------- + kaioi_ewai = request.data.get('kaioi_ewai_dashou_fencheng') == 'true' + ewai_fencheng = Decimal('0') + if kaioi_ewai: + try: + ewai_fencheng = Decimal(str(request.data.get('ewai_dashou_fencheng', 0))) + if ewai_fencheng < 0: + return Response({'code': 400, 'msg': '分成金额不能为负数'}) + except: + return Response({'code': 400, 'msg': '分成金额格式错误'}) + + jieshao = request.data.get('jieshao', '').strip() + xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip() + + # ---------- 4. 商品图片更新 ---------- + tupian_url = product.tupian_url + tupian_file = request.FILES.get('tupian_file') + if tupian_file: + valid, msg = validate_image(tupian_file) + if not valid: + return Response({'code': 400, 'msg': f'商品图片无效:{msg}'}) + ext = tupian_file.name.split('.')[-1].lower() + if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: + return Response({'code': 400, 'msg': '不支持的图片格式'}) + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + random_str = uuid.uuid4().hex[:8] + new_filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}" + uploaded_url = upload_to_oss(tupian_file, new_filename) + if not uploaded_url: + return Response({'code': 500, 'msg': '商品图片上传失败'}) + if product.tupian_url: + delete_from_oss(product.tupian_url) + tupian_url = new_filename + + # ---------- 5. 规则图片更新 ---------- + guize_tupian = product.guize_tupian + guize_file = request.FILES.get('guize_file') + if guize_file: + valid, msg = validate_image(guize_file) + if not valid: + return Response({'code': 400, 'msg': f'规则图片无效:{msg}'}) + ext = guize_file.name.split('.')[-1].lower() + if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: + return Response({'code': 400, 'msg': '不支持的图片格式'}) + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + random_str = uuid.uuid4().hex[:8] + new_filename = f"shangpin/guizetupian/{timestamp}_{random_str}.{ext}" + uploaded_url = upload_to_oss(guize_file, new_filename) + if not uploaded_url: + return Response({'code': 500, 'msg': '规则图片上传失败'}) + if product.guize_tupian: + delete_from_oss(product.guize_tupian) + guize_tupian = new_filename + + # ---------- 6. 更新数据库 ---------- + product.biaoqian = biaoqian + product.jiage = jiage + product.kucun = kucun + product.duiwai_xiaoliang = duiwai_xiaoliang + product.leixing_id = leixing_id + product.zhuanqu_id = zhuanqu_id + product.yaoqiuleixing = yaoqiuleixing + product.huiyuan_id = huiyuan_id + product.yongjin = yongjin + product.kaioi_ewai_dashou_fencheng = kaioi_ewai + product.ewai_dashou_fencheng = ewai_fencheng + product.jieshao = jieshao + product.xiadan_xuzhi = xiadan_xuzhi + product.tupian_url = tupian_url + product.guize_tupian = guize_tupian + product.shenhezhuangtai = shenhezhuangtai + product.save() + + return Response({'code': 0, 'msg': '商品修改成功', 'data': {'id': product.id}}) + + + + + +class GetMemberListView(APIView): + """ + 获取会员列表 + 权限:3300a + POST /houtai/hthqhylb + 参数:username + 返回:code:0, data: { list: [...] } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '3300a' not in permissions: + return Response({'code': 403, 'msg': '无权限查看会员列表'}) + + members = Huiyuan.objects.all().order_by('-create_time') + member_list = [] + for m in members: + member_list.append({ + 'huiyuan_id': m.huiyuan_id, + 'jieshao': m.jieshao, + 'jtjieshao': m.jtjieshao, + 'jiage': str(m.jiage), + 'guanshifc': str(m.guanshifc), + 'zuzhangfc': str(m.zuzhangfc), + 'goumai_cishu': m.goumai_cishu, + 'create_time': m.create_time.isoformat() if m.create_time else None, + 'update_time': m.update_time.isoformat() if m.update_time else None, + }) + return Response({'code': 0, 'data': {'list': member_list}}) + + +class UpdateMemberView(APIView): + """ + 修改会员信息 + 权限:3300a + POST /houtai/htxghyxx + 参数: + username, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao + 返回:code:0 成功 + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + huiyuan_id = request.data.get('huiyuan_id', '').strip() + jieshao = request.data.get('jieshao', '').strip() + jiage = request.data.get('jiage') + guanshifc = request.data.get('guanshifc') + zuzhangfc = request.data.get('zuzhangfc') + jtjieshao = request.data.get('jtjieshao', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not huiyuan_id: + return Response({'code': 400, 'msg': '缺少会员ID'}) + if not jieshao: + return Response({'code': 400, 'msg': '会员名称不能为空'}) + + try: + jiage = Decimal(str(jiage)) + guanshifc = Decimal(str(guanshifc)) + zuzhangfc = Decimal(str(zuzhangfc)) + if jiage < 0 or guanshifc < 0 or zuzhangfc < 0: + raise ValueError + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + + if guanshifc + zuzhangfc > jiage: + return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '3300a' not in permissions: + return Response({'code': 403, 'msg': '无权限修改会员'}) + + with transaction.atomic(): + try: + member = Huiyuan.objects.select_for_update().get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response({'code': 404, 'msg': '会员不存在'}) + + member.jieshao = jieshao + member.jiage = jiage + member.guanshifc = guanshifc + member.zuzhangfc = zuzhangfc + member.jtjieshao = jtjieshao + member.save() + + return Response({'code': 0, 'msg': '修改成功'}) + + + +class AddMemberView(APIView): + """ + 添加会员 + 权限:3300a + POST /houtai/httjhy + 参数: + username, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao + 返回:code:0, data: { huiyuan_id } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + jieshao = request.data.get('jieshao', '').strip() + jiage = request.data.get('jiage') + guanshifc = request.data.get('guanshifc') + zuzhangfc = request.data.get('zuzhangfc') + jtjieshao = request.data.get('jtjieshao', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not jieshao: + return Response({'code': 400, 'msg': '会员名称不能为空'}) + + try: + jiage = Decimal(str(jiage)) + guanshifc = Decimal(str(guanshifc)) + zuzhangfc = Decimal(str(zuzhangfc)) + if jiage <= 0 or guanshifc < 0 or zuzhangfc < 0: + raise ValueError + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + + if guanshifc + zuzhangfc > jiage: + return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '3300a' not in permissions: + return Response({'code': 403, 'msg': '无权限添加会员'}) + + # 生成唯一6位会员ID(数字+字母) + def generate_huiyuan_id(): + while True: + new_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) + if not Huiyuan.objects.filter(huiyuan_id=new_id).exists(): + return new_id + + with transaction.atomic(): + new_id = generate_huiyuan_id() + member = Huiyuan.objects.create( + huiyuan_id=new_id, + jieshao=jieshao, + jiage=jiage, + guanshifc=guanshifc, + zuzhangfc=zuzhangfc, + jtjieshao=jtjieshao, + goumai_cishu=0 + ) + + return Response({'code': 0, 'msg': '添加成功', 'data': {'huiyuan_id': member.huiyuan_id}}) + + + + + +class GetGuanliListView(APIView): + """ + 获取管事列表 + 权限:4400a,4400b,4400c,4400d,4400e,4400f 任一 + POST /houtai/hthqgslb + 参数: + username, keyword(可选), balance_op/balance_value, commission_op/commission_value, + invite_count_op/invite_count_value, status, page, page_size + 返回: + code:0, data: { list: [...], total } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验(需要任一权限) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看管事列表'}) + + # 分页参数 + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + if page < 1 or page_size < 1: + raise ValueError + except: + return Response({'code': 400, 'msg': '分页参数错误'}) + + # 筛选参数 + keyword = request.data.get('keyword', '').strip() + balance_op = request.data.get('balance_op', '').strip().lower() + balance_value = request.data.get('balance_value') + commission_op = request.data.get('commission_op', '').strip().lower() + commission_value = request.data.get('commission_value') + invite_count_op = request.data.get('invite_count_op', '').strip().lower() + invite_count_value = request.data.get('invite_count_value') + status = request.data.get('status') + + # 构建查询:从 UserMain 开始,关联 UserGuanshi 和 UserBoss(左连接获取昵称) + qs = UserMain.objects.filter( + guanshi_profile__isnull=False # 确保是管事 + ).select_related('guanshi_profile').select_related('boss_profile') + + # 关键词搜索(用户ID 或 昵称来自 boss_profile.nickname) + if keyword: + qs = qs.filter( + Q(yonghuid__icontains=keyword) | + Q(boss_profile__nickname__icontains=keyword) + ) + + # 余额筛选(使用 guanshi_profile.yue) + if balance_op and balance_value is not None and balance_value != '': + try: + balance_val = Decimal(str(balance_value)) + if balance_op == 'gte': + qs = qs.filter(guanshi_profile__yue__gte=balance_val) + elif balance_op == 'lte': + qs = qs.filter(guanshi_profile__yue__lte=balance_val) + else: + return Response({'code': 400, 'msg': '余额比较符无效'}) + except: + return Response({'code': 400, 'msg': '余额数值格式错误'}) + + # 分佣总额筛选(guanshi_profile.chongzhifenrun) + if commission_op and commission_value is not None and commission_value != '': + try: + commission_val = Decimal(str(commission_value)) + if commission_op == 'gte': + qs = qs.filter(guanshi_profile__chongzhifenrun__gte=commission_val) + elif commission_op == 'lte': + qs = qs.filter(guanshi_profile__chongzhifenrun__lte=commission_val) + else: + return Response({'code': 400, 'msg': '分佣比较符无效'}) + except: + return Response({'code': 400, 'msg': '分佣数值格式错误'}) + + # 邀请打手总数筛选(guanshi_profile.yaogingshuliang) + if invite_count_op and invite_count_value is not None and invite_count_value != '': + try: + invite_val = int(invite_count_value) + if invite_count_op == 'gte': + qs = qs.filter(guanshi_profile__yaogingshuliang__gte=invite_val) + elif invite_count_op == 'lte': + qs = qs.filter(guanshi_profile__yaogingshuliang__lte=invite_val) + else: + return Response({'code': 400, 'msg': '邀请数量比较符无效'}) + except: + return Response({'code': 400, 'msg': '邀请数量格式错误'}) + + # 状态筛选 + if status is not None and status != '': + try: + status_val = int(status) + if status_val not in (0, 1): + raise ValueError + qs = qs.filter(guanshi_profile__zhuangtai=status_val) + except: + return Response({'code': 400, 'msg': '状态值无效'}) + + # 计算总数 + total = qs.count() + + # 分页 + offset = (page - 1) * page_size + users = qs.order_by('-create_time')[offset:offset + page_size] + + # 构建返回数据 + result = [] + for user in users: + guanshi = user.guanshi_profile + boss = user.boss_profile if hasattr(user, 'boss_profile') else None + result.append({ + 'yonghuid': user.yonghuid, + 'avatar': user.avatar or '', + 'nickname': boss.nickname if boss else '', + 'zhuangtai': guanshi.zhuangtai, + 'yaogingshuliang': guanshi.yaogingshuliang, + 'yue': str(guanshi.yue), + 'chongzhifenrun': str(guanshi.chongzhifenrun), + }) + + return Response({'code': 0, 'data': {'list': result, 'total': total}}) + + + + + + + + +class GetGuanliDetailView(APIView): + """ + 获取管事详情 + 权限:4400a,4400b,4400c,4400d,4400e,4400f 任一 + POST /houtai/hthqgsxq + 参数:username, yonghuid + 返回: + code:0, data: { + base: {...}, + balance: {...}, + permanent_enabled, permanent_amount, + custom_first_dividend: {}, + extra_dividend_map: {}, + all_members: [] + } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少用户ID'}) + + # 权限校验(需要任一权限) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看管事详情'}) + + # 查询用户主表和管事扩展表 + try: + user = UserMain.objects.select_related('guanshi_profile', 'boss_profile').get(yonghuid=yonghuid) + guanshi = user.guanshi_profile + boss = user.boss_profile if hasattr(user, 'boss_profile') else None + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '管事不存在'}) + except UserGuanshi.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是管事'}) + + # 基础信息 + base_data = { + 'yonghuid': user.yonghuid, + 'nickname': boss.nickname if boss else '', + 'avatar': user.avatar or '', + 'dianhua': guanshi.dianhua or '', + 'wechat': guanshi.wechat or '', + 'zhuangtai': guanshi.zhuangtai, + 'yaoqingma': guanshi.yaoqingma, + 'yaoqingren': guanshi.yaoqingren, + 'invite_qrcode_url': guanshi.invite_qrcode_url or '', + 'yaogingshuliang': guanshi.yaogingshuliang, + 'jinrichongzhi': guanshi.jinrichongzhi, + 'jinyuechongzhi': guanshi.jinyuechongzhi, + 'chongzhifenrun': str(guanshi.chongzhifenrun), + 'create_time': guanshi.create_time.isoformat() if guanshi.create_time else None, + } + + # 余额提现相关 + balance_data = { + 'yue': str(guanshi.yue), + 'jinritixian_jine': str(guanshi.jinritixian_jine), + 'last_tixian_date': guanshi.last_tixian_date.isoformat() if guanshi.last_tixian_date else None, + 'kaioi_ewai_xiane': guanshi.kaioi_ewai_xiane, + 'ewai_xiane': str(guanshi.ewai_xiane), + } + + # 永久分红(二次分红) + permanent_enabled = guanshi.fenghong_erci_enabled + permanent_amount = str(guanshi.fenghong_erci) if guanshi.fenghong_erci else '0' + + # 获取所有会员(用于前端选择) + all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage', 'guanshifc')) + for m in all_members: + m['jiage'] = str(m['jiage']) + m['guanshifc'] = str(m['guanshifc']) + + # 获取多次分红配置(按次数和会员组织) + # 查询该管事的所有多次分红记录 + duoci_records = DuociFenhong.objects.filter(yonghuid=yonghuid).order_by('cishu') + custom_first = {} # 首次分红定制(cishu=1) + extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } } + + for record in duoci_records: + # 获取会员信息 + member = Huiyuan.objects.filter(huiyuan_id=record.huiyuan).first() + if not member: + continue + member_info = { + 'jieshao': member.jieshao, + 'jiage': str(member.jiage), + } + if record.cishu == 1: + custom_first[record.huiyuan] = float(record.guanshi_fenhong) + else: + times = record.cishu + if times not in extra_map: + extra_map[times] = {} + extra_map[times][record.huiyuan] = { + 'guanshi_fenhong': float(record.guanshi_fenhong), + **member_info + } + + # 注意:首次分红定制只存储有定制记录的会员,没有定制的会员使用默认值(前端通过会员表展示) + # 额外次数分红按次数返回 + + return Response({ + 'code': 0, + 'data': { + 'base': base_data, + 'balance': balance_data, + 'permanent_enabled': permanent_enabled, + 'permanent_amount': permanent_amount, + 'custom_first_dividend': custom_first, + 'extra_dividend_map': extra_map, + 'all_members': all_members, + } + }) + + + + + +class UpdateGuanliView(APIView): + """ + 修改管事信息(基础信息、余额、提现限额、分红策略) + 权限细分: + - 修改状态(封禁/解封)及昵称/电话/微信:需要4400a + - 增加余额:需要4400c + - 减少余额:需要4400b + - 修改提现限额(kaioi_ewai_xiane/ewai_xiane):需要4400e + - 修改分红相关(永久分红、定制首次、额外次数):需要4400f + POST /houtai/htxggsxx + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少用户ID'}) + + # 权限校验(先获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 查询管事对象(不在事务外加锁,事务内加锁) + try: + user = UserMain.objects.get(yonghuid=yonghuid) + guanshi = user.guanshi_profile + boss = user.boss_profile if hasattr(user, 'boss_profile') else None + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + except UserGuanshi.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是管事'}) + + # 开始事务,内部使用 select_for_update + with transaction.atomic(): + # 重新获取加锁对象 + user = UserMain.objects.select_for_update().get(yonghuid=yonghuid) + guanshi = user.guanshi_profile + boss = user.boss_profile if hasattr(user, 'boss_profile') else None + + # ---------- 1. 基础信息修改(需要4400a)---------- + nickname = request.data.get('nickname') + dianhua = request.data.get('dianhua') + wechat = request.data.get('wechat') + zhuangtai = request.data.get('zhuangtai') + avatar = request.data.get('avatar') + + if any(v is not None for v in [nickname, dianhua, wechat, zhuangtai, avatar]): + if '4400a' not in permissions: + return Response({'code': 403, 'msg': '无权限修改管事基础信息(需要4400a)'}) + # 处理昵称(在老板扩展表中) + if nickname is not None: + if not boss: + boss = UserBoss.objects.create(user=user, nickname=nickname) + else: + boss.nickname = nickname + boss.save() + # 处理其他字段 + if dianhua is not None: + guanshi.dianhua = dianhua + if wechat is not None: + guanshi.wechat = wechat + if zhuangtai is not None: + try: + zt = int(zhuangtai) + if zt in (0, 1): + guanshi.zhuangtai = zt + except: + pass + if avatar is not None: + user.avatar = avatar + user.save() + guanshi.save() + + # ---------- 2. 余额修改(加:4400c;减:4400b)---------- + new_yue = request.data.get('yue') + if new_yue is not None: + try: + new_yue = Decimal(str(new_yue)) + if new_yue < 0: + return Response({'code': 400, 'msg': '余额不能为负数'}) + except: + return Response({'code': 400, 'msg': '余额格式错误'}) + + old_yue = guanshi.yue + if new_yue > old_yue: + if '4400c' not in permissions: + return Response({'code': 403, 'msg': '无权限增加余额(需要4400c)'}) + elif new_yue < old_yue: + if '4400b' not in permissions: + return Response({'code': 403, 'msg': '无权限减少余额(需要4400b)'}) + guanshi.yue = new_yue + guanshi.save() + + # ---------- 3. 提现限额修改(需要4400e)---------- + kaioi_ewai = request.data.get('kaioi_ewai_xiane') + ewai_val = request.data.get('ewai_xiane') + if kaioi_ewai is not None or ewai_val is not None: + if '4400e' not in permissions: + return Response({'code': 403, 'msg': '无权限修改提现限额(需要4400e)'}) + if kaioi_ewai is not None: + guanshi.kaioi_ewai_xiane = bool(kaioi_ewai) + if ewai_val is not None: + try: + guanshi.ewai_xiane = Decimal(str(ewai_val)) + except: + return Response({'code': 400, 'msg': '额外限额金额格式错误'}) + guanshi.save() + + # ---------- 4. 分红相关修改(需要4400f)---------- + perm_enabled = request.data.get('permanent_enabled') + perm_amount = request.data.get('permanent_amount') + custom_first = request.data.get('custom_first_dividend') + extra_map = request.data.get('extra_dividend_map') + + if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]): + if '4400f' not in permissions: + return Response({'code': 403, 'msg': '无权限修改分红策略(需要4400f)'}) + + # 4.1 永久分红 + if perm_enabled is not None: + guanshi.fenghong_erci_enabled = bool(perm_enabled) + if perm_amount is not None: + try: + guanshi.fenghong_erci = Decimal(str(perm_amount)) + except: + return Response({'code': 400, 'msg': '永久分红金额格式错误'}) + guanshi.save() + + # 4.2 首次定制分红(cishu=1) + if custom_first is not None and isinstance(custom_first, dict): + # 获取当前所有首次定制记录 + existing_first = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu=1) + # 前端传来的定制字典 {会员ID: 金额} + for huiyuan_id, amount in custom_first.items(): + try: + amount = Decimal(str(amount)) + except: + continue + if amount <= 0: + # 金额为0或负数表示删除定制 + DuociFenhong.objects.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() + else: + DuociFenhong.objects.update_or_create( + yonghuid=yonghuid, + huiyuan=huiyuan_id, + cishu=1, + defaults={ + 'guanshi_fenhong': amount, + 'zuzhang_fenhong': Decimal('0') + } + ) + # 删除那些在前端字典中不存在的首次定制记录(即已取消定制) + for record in existing_first: + if record.huiyuan not in custom_first: + record.delete() + + # 4.3 额外次数分红(cishu >= 2) + if extra_map is not None and isinstance(extra_map, dict): + # 收集需要保留的记录 + keep_set = set() + for times_str, members in extra_map.items(): + try: + cishu = int(times_str) + if cishu < 2: + continue + except: + continue + if not isinstance(members, dict): + continue + for huiyuan_id, data in members.items(): + if not isinstance(data, dict): + continue + amount = data.get('guanshi_fenhong') + if amount is None: + continue + try: + amount = Decimal(str(amount)) + except: + continue + keep_set.add((cishu, huiyuan_id)) + # 更新或创建 + DuociFenhong.objects.update_or_create( + yonghuid=yonghuid, + huiyuan=huiyuan_id, + cishu=cishu, + defaults={ + 'guanshi_fenhong': amount, + 'zuzhang_fenhong': Decimal('0') + } + ) + # 删除不在 keep_set 中的额外次数记录 + existing_extra = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu__gte=2) + for record in existing_extra: + if (record.cishu, record.huiyuan) not in keep_set: + record.delete() + + return Response({'code': 0, 'msg': '修改成功'}) + + + + + + + + + +class GetWithdrawSettingsView(APIView): + """ + 获取提现设置(手续费利率、每日限额、当日已提现总额) + 权限:需要 5500a 或 5500b 或 5500c 任一 + POST /houtai/hthqtxpz + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + # 需要5500a/b/c任一 + if not any(p in permissions for p in ('5500a', '5500b', '5500c')): + return Response({'code': 403, 'msg': '无权限查看提现设置'}) + + # 1. 手续费利率(Lilubiao:5打手/6管事/8组长/9审核官/10商家/11打手押金) + rate_map = {} + for leixing, code in [(1, '5'), (2, '6'), (3, '8'), (4, '9'), (5, '11'), (6, '10')]: + obj = Lilubiao.objects.filter(fadanpingtai=code).first() + rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0 + + # 1b. 订单/押金分红费率(Lilubiao:1平台订单/3商家订单/12押金管事/13商家订单管事分红) + order_rate_map = {} + for leixing, code in [(1, '1'), (3, '3'), (12, '12'), (13, '13')]: + obj = Lilubiao.objects.filter(fadanpingtai=code).first() + order_rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0 + + # 2. 个人每日限额 (leixing=1,2,3) + quota_map = {} + for leixing in [1, 2, 3]: + obj = TixianQuotaDefault.objects.filter(leixing=leixing).first() + quota_map[leixing] = float(obj.default_quota) if obj else 20.0 + + # 3. 每日总限额(提现类型1~6 → 配置表4~9) + total_limit_map = {} + for withdraw_leixing, quota_leixing in [(1, 4), (2, 5), (3, 6), (4, 7), (5, 8), (6, 9)]: + obj = TixianQuotaDefault.objects.filter(leixing=quota_leixing).first() + total_limit_map[withdraw_leixing] = float(obj.default_quota) if obj else 0.0 + + # 4. 当日已提现总额(按提现类型 leixing 1~6) + today = date.today() + today_totals = {i: 0.0 for i in range(1, 7)} + records = TixianRiTongji.objects.filter(riqi=today) + for rec in records: + if rec.leixing in today_totals: + today_totals[rec.leixing] = float(rec.total_amount) + + # 🆕 获取提现模式(自动1 / 手动2),默认2 + config = WithdrawConfig.objects.filter(id=1).first() + withdraw_mode = config.mode if config else 2 + + return Response({ + 'code': 0, + 'data': { + 'withdraw_mode': withdraw_mode, + 'rates': rate_map, + 'order_rates': order_rate_map, + 'quotas': quota_map, + 'total_limits': total_limit_map, + 'today_totals': today_totals, + } + }) + + +class UpdateWithdrawSettingsView(APIView): + """ + 修改提现设置(权限细分): + - 修改打手相关(利率、个人限额、总限额)需要5500a + - 修改管事相关需要5500b + - 修改组长相关需要5500c + 注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算), + 后端不直接修改 TixianRiTongji,该表由提现流程自动累加。 + POST /houtai/htxgtxsz + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 权限映射:要修改的角色需要对应的权限 + # 前端传来 rates, quotas, total_limits 三个对象,每个包含 1,2,3 + rates = request.data.get('rates', {}) + order_rates = request.data.get('order_rates', request.data.get('commission_rates', {})) + quotas = request.data.get('quotas', {}) + total_limits = request.data.get('total_limits', {}) + + # 角色类型映射 + role_perms = {1: '5500a', 2: '5500b', 3: '5500c'} + # 利率代码映射(1-3 有水缸限额;4审核官/5打手押金/6商家余额仅费率) + rate_code_map = {1: '5', 2: '6', 3: '8', 4: '9', 5: '11', 6: '10'} + # 订单/押金分红(1平台订单/3商家订单/12押金管事/13商家订单管事分红) + order_rate_code_map = {1: '1', 3: '3', 12: '12', 13: '13'} + # 总限额:提现类型 → TixianQuotaDefault.leixing(4~9) + total_code_map = {1: 4, 2: 5, 3: 6, 4: 7, 5: 8, 6: 9} + + def _rate_key_present(data, role): + return (str(role) in data and data[str(role)] is not None) or \ + (role in data and data[role] is not None) + + with transaction.atomic(): + # 遍历三个角色(限额相关) + for role in [1, 2, 3]: + perm_needed = role_perms[role] + if perm_needed not in permissions: + if _rate_key_present(rates, role) or \ + (str(role) in quotas and quotas[str(role)] is not None) or \ + (role in quotas and quotas.get(role) is not None) or \ + (str(role) in total_limits and total_limits[str(role)] is not None) or \ + (role in total_limits and total_limits.get(role) is not None): + return Response({'code': 403, 'msg': f'无权限修改{["","打手","管事","组长"][role]}相关设置(需要{perm_needed})'}) + + # 其他提现手续费(审核官/打手押金/商家余额):需 5500a/b/c 任一 + extra_role_names = {4: '考核官', 5: '打手押金', 6: '商家余额'} + for role in [4, 5, 6]: + if _rate_key_present(rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')): + return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}提现手续费率'}) + if (str(role) in total_limits and total_limits[str(role)] is not None) or \ + (role in total_limits and total_limits.get(role) is not None): + if not any(p in permissions for p in ('5500a', '5500b', '5500c')): + return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}总限额'}) + + # 订单/押金分红费率:需 5500a/b/c 任一 + for role in [1, 3, 12, 13]: + if _rate_key_present(order_rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')): + order_names = { + 1: '平台订单打手分红', 3: '商家订单打手分红', + 12: '押金管事分红', 13: '管事订单分红', + } + return Response({'code': 403, 'msg': f'无权限修改{order_names[role]}费率'}) + + # 1. 修改手续费利率 + for role in [1, 2, 3, 4, 5, 6]: + val = rates.get(str(role), rates.get(role)) + if val is not None: + rate = Decimal(str(val)) + code = rate_code_map[role] + obj, created = Lilubiao.objects.select_for_update().get_or_create( + fadanpingtai=code, + defaults={'lilu': rate} + ) + if not created: + obj.lilu = rate + obj.save() + + # 1b. 修改订单/押金分红费率 + for role in [1, 3, 12, 13]: + val = order_rates.get(str(role), order_rates.get(role)) + if val is not None: + rate = Decimal(str(val)) + code = order_rate_code_map[role] + obj, created = Lilubiao.objects.select_for_update().get_or_create( + fadanpingtai=code, + defaults={'lilu': rate} + ) + if not created: + obj.lilu = rate + obj.save() + + # 2. 修改个人每日限额 + for role in [1, 2, 3]: + if str(role) in quotas: + val = quotas[str(role)] + if val is not None: + quota = Decimal(str(val)) + obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create( + leixing=role, + defaults={'default_quota': quota} + ) + if not created: + obj.default_quota = quota + obj.save() + + # 3. 修改每日总限额(1打手/2管事/3组长/4考核官/5押金/6商家) + for role in [1, 2, 3, 4, 5, 6]: + val = total_limits.get(str(role), total_limits.get(role)) + if val is not None: + limit = Decimal(str(val)) + code = total_code_map[role] + obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create( + leixing=code, + defaults={'default_quota': limit} + ) + if not created: + obj.default_quota = limit + obj.save() + + # 🆕 修改提现模式(有5500a/b/c任一即可) + withdraw_mode = request.data.get('withdraw_mode') + if withdraw_mode is not None: + if not any(p in permissions for p in ('5500a', '5500b', '5500c')): + return Response({'code': 403, 'msg': '无权限修改提现模式'}) + # 更新或创建 id=1 的配置 + WithdrawConfig.objects.update_or_create( + id=1, + defaults={'mode': int(withdraw_mode)} + ) + + # 注意:TixianRiTongji 表由提现流程自动更新,此处不直接修改 + return Response({'code': 0, 'msg': '设置修改成功'}) + + + +class GetZuzhangListView(APIView): + """ + 组长列表接口(分页 + 多条件筛选) + 路由:POST /houtai/hthqzzlb + 权限:需要登录,且拥有 6600a~6600e 任一权限 + """ + permission_classes = [IsAuthenticated] + + @transaction.atomic # 保证查询的一致性(可选,对只读查询影响不大但无坏处) + def post(self, request): + # 1. 获取前端传递的 username(用于防越权) + frontend_username = request.data.get('username', '') + + # 2. 调用公共校验方法,验证身份并获取权限列表 + result = verify_kefu_permission(request, username_from_frontend=frontend_username) + + # 重要:verify_kefu_permission 返回格式为 (user_obj, permissions_list) 或 (None, Response) + # 如果第二个元素是 Response 对象,说明校验失败,直接返回该 Response + if isinstance(result, tuple) and len(result) == 2: + user_obj, second = result + if isinstance(second, Response): + return second # 校验失败,直接返回错误响应 + else: + permissions = second # 正常情况:permissions 是列表 + else: + # 防止意外返回格式,视为失败 + return Response({'code': 500, 'msg': '权限校验模块异常'}) + + # 3. 检查是否拥有组长管理所需权限(6600a ~ 6600e 任一) + required_perms = ['6600a', '6600b', '6600c', '6600d', '6600e'] + if not any(perm in permissions for perm in required_perms): + logger.warning(f"用户 {request.user.phone} 尝试访问组长列表但缺少必要权限,已有权限: {permissions}") + return Response({'code': 403, 'msg': '您无权访问组长管理页面'}) + + # 4. 提取请求参数(分页 + 筛选) + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + except ValueError: + page, page_size = 1, 20 + + keyword = request.data.get('keyword', '').strip() + status = request.data.get('status') # 1=正常, 0=禁用 + invite_count_op = request.data.get('invite_count_op') # gte / lte + invite_count_value = request.data.get('invite_count_value') + amount_op = request.data.get('amount_op') + amount_value = request.data.get('amount_value') + commission_op = request.data.get('commission_op') + commission_value = request.data.get('commission_value') + + # 5. 构建基础 QuerySet(预加载关联表) + queryset = UserZuzhang.objects.select_related('user', 'user__boss_profile').all() + + # 6. 应用筛选条件(使用 ORM 安全过滤) + if keyword: + queryset = queryset.filter( + Q(user__yonghuid__icontains=keyword) | + Q(user__boss_profile__nickname__icontains=keyword) + ) + + if status is not None and status != '': + try: + queryset = queryset.filter(zhuangtai=int(status)) + except ValueError: + pass + + # 邀请总数筛选 + if invite_count_value not in (None, ''): + try: + val = float(invite_count_value) + op = invite_count_op if invite_count_op in ('gte', 'lte') else 'gte' + queryset = queryset.filter(**{f'yaoqing_zongshu__{op}': val}) + except (ValueError, TypeError): + pass + + # 可提现金额筛选 + if amount_value not in (None, ''): + try: + val = float(amount_value) + op = amount_op if amount_op in ('gte', 'lte') else 'gte' + queryset = queryset.filter(**{f'ketixian_jine__{op}': val}) + except (ValueError, TypeError): + pass + + # 分佣总额筛选 + if commission_value not in (None, ''): + try: + val = float(commission_value) + op = commission_op if commission_op in ('gte', 'lte') else 'gte' + queryset = queryset.filter(**{f'fenyong_zonge__{op}': val}) + except (ValueError, TypeError): + pass + + # 7. 总数统计 + total = queryset.count() + + # 8. 分页 + offset = (page - 1) * page_size + paged_queryset = queryset[offset:offset + page_size] + + # 9. 构造返回数据 + data_list = [] + for zu in paged_queryset: + user = zu.user + nickname = '' + if hasattr(user, 'boss_profile') and user.boss_profile: + nickname = user.boss_profile.nickname or '' + + data_list.append({ + 'yonghuid': user.yonghuid, + 'avatar': user.avatar or '', + 'nickname': nickname, + 'fenyong_zonge': float(zu.fenyong_zonge), + 'ketixian_jine': float(zu.ketixian_jine), + 'yaoqing_zongshu': zu.yaoqing_zongshu, + 'zhuangtai': zu.zhuangtai, + }) + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'list': data_list, + 'total': total, + } + }) + + + + + + + + + + +class GetZuzhangDetailView(APIView): + """ + 获取组长详情 + 权限:6600a,6600b,6600c,6600d,6600e 任一 + POST /houtai/hthqzzxq + 参数:username, yonghuid + 返回: + code:0, data: { + base: {...}, + balance: {...}, + permanent_enabled, permanent_amount, + custom_first_dividend: {}, + extra_dividend_map: {}, + all_members: [] + } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少用户ID'}) + + # 权限校验(需要任一权限) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + required_perms = {'6600a', '6600b', '6600c', '6600d', '6600e'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看组长详情'}) + + # 查询组长主表、扩展表、老板扩展表(昵称) + try: + user = UserMain.objects.select_related('zuzhang_profile', 'boss_profile').get(yonghuid=yonghuid) + zuzhang = user.zuzhang_profile + boss = user.boss_profile if hasattr(user, 'boss_profile') else None + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '组长不存在'}) + except UserZuzhang.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是组长'}) + + # 基础信息 + base_data = { + 'yonghuid': user.yonghuid, + 'nickname': boss.nickname if boss else '', + 'avatar': user.avatar or '', + 'dianhua': '', # 组长表无电话字段?组长扩展表没有电话/微信,但前端可能需要,可从UserMain关联?实际上组长表无电话字段,这里留空或从其他表获取 + 'wechat': '', + 'zhuangtai': zuzhang.zhuangtai, + 'yaoqingma': zuzhang.yaoqingma, + 'yaoqingren': '', # 组长表没有邀请人字段 + 'haibao_url': zuzhang.haibao_url or '', + 'yaoqing_zongshu': zuzhang.yaoqing_zongshu, + 'jinri_fenyong': str(zuzhang.jinri_fenyong), + 'jinyue_fenyong': str(zuzhang.jinyue_fenyong), + 'fenyong_zonge': str(zuzhang.fenyong_zonge), + 'create_time': zuzhang.create_time.isoformat() if zuzhang.create_time else None, + } + + # 余额提现相关 + balance_data = { + 'ketixian_jine': str(zuzhang.ketixian_jine), + 'jinri_tixian': str(zuzhang.jinri_tixian), + 'last_tixian_time': zuzhang.last_tixian_time.isoformat() if zuzhang.last_tixian_time else None, + 'kaioi_ewai_tixian': zuzhang.kaioi_ewai_tixian, + 'ewai_tixian_xiane': str(zuzhang.ewai_tixian_xiane), + } + + # 永久分红(额外分红开关和金额) + permanent_enabled = zuzhang.kaioi_ewai_fenhong + permanent_amount = str(zuzhang.ewai_fenhong_jine) if zuzhang.ewai_fenhong_jine else '0' + + # 获取所有会员(用于前端选择) + all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage', 'zuzhangfc')) + for m in all_members: + m['jiage'] = str(m['jiage']) + m['zuzhangfc'] = str(m['zuzhangfc']) + + # 获取多次分红配置(按次数和会员组织) + duoci_records = DuociFenhong.objects.filter(yonghuid=yonghuid).order_by('cishu') + custom_first = {} # 首次分红定制(cishu=1) + extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } } + + for record in duoci_records: + # 获取会员信息 + member = Huiyuan.objects.filter(huiyuan_id=record.huiyuan).first() + if not member: + continue + member_info = { + 'jieshao': member.jieshao, + 'jiage': str(member.jiage), + } + if record.cishu == 1: + custom_first[record.huiyuan] = float(record.zuzhang_fenhong) + else: + times = record.cishu + if times not in extra_map: + extra_map[times] = {} + extra_map[times][record.huiyuan] = { + 'zuzhang_fenhong': float(record.zuzhang_fenhong), + **member_info + } + + return Response({ + 'code': 0, + 'data': { + 'base': base_data, + 'balance': balance_data, + 'permanent_enabled': permanent_enabled, + 'permanent_amount': permanent_amount, + 'custom_first_dividend': custom_first, + 'extra_dividend_map': extra_map, + 'all_members': all_members, + } + }) + + + + +class UpdateZuzhangView(APIView): + """ + 修改组长信息(基础信息、余额、提现限额、分红策略) + 权限细分: + - 增加可提现金额:6600a + - 减少可提现金额:6600b + - 修改状态(封禁/解封):6600c + - 修改提现限额(kaioi_ewai_tixian/ewai_tixian_xiane):6600d + - 修改分红相关(永久分红、定制首次、额外次数):6600e + POST /houtai/htxgzzxx + 参数: + username, yonghuid, + nickname, avatar, dianhua, wechat, zhuangtai, # 基础 + ketixian_jine, kaioi_ewai_tixian, ewai_tixian_xiane, # 余额提现 + permanent_enabled, permanent_amount, # 永久分红 + custom_first_dividend: {}, # 首次定制分红 {会员ID: 金额} + extra_dividend_map: {} # 额外次数分红 {次数: {会员ID: {zuzhang_fenhong}}} + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少用户ID'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 查询组长对象 + try: + user = UserMain.objects.get(yonghuid=yonghuid) + zuzhang = user.zuzhang_profile + boss = user.boss_profile if hasattr(user, 'boss_profile') else None + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + except UserZuzhang.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是组长'}) + + # 获取原始数据(用于对比金额变化) + old_ketixian_jine = zuzhang.ketixian_jine + + # 开始事务 + with transaction.atomic(): + # ---------- 1. 基础信息修改(需要6600c?按需求,基础信息修改通常用6600c,但为了安全,暂不校验,如有需要可加)---------- + nickname = request.data.get('nickname', '').strip() + avatar = request.data.get('avatar', '').strip() + dianhua = request.data.get('dianhua', '').strip() + wechat = request.data.get('wechat', '').strip() + zhuangtai = request.data.get('zhuangtai') + # 状态修改需要6600c + if zhuangtai is not None: + if '6600c' not in permissions: + return Response({'code': 403, 'msg': '无权限修改组长状态(需要6600c)'}) + try: + zt = int(zhuangtai) + if zt in (0, 1): + zuzhang.zhuangtai = zt + except: + pass + if nickname: + if not boss: + boss = UserBoss.objects.create(user=user, nickname=nickname) + else: + boss.nickname = nickname + boss.save() + if avatar: + user.avatar = avatar + user.save() + # 组长表没有电话/微信字段,如需存储可暂存到UserMain或忽略 + # 这里不处理 + + # ---------- 2. 可提现金额修改(加:6600a;减:6600b)---------- + new_ketixian_jine = request.data.get('ketixian_jine') + if new_ketixian_jine is not None: + try: + new_val = Decimal(str(new_ketixian_jine)) + if new_val < 0: + return Response({'code': 400, 'msg': '可提现金额不能为负数'}) + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + if new_val > old_ketixian_jine: + if '6600a' not in permissions: + return Response({'code': 403, 'msg': '无权限增加可提现金额(需要6600a)'}) + elif new_val < old_ketixian_jine: + if '6600b' not in permissions: + return Response({'code': 403, 'msg': '无权限减少可提现金额(需要6600b)'}) + zuzhang.ketixian_jine = new_val + zuzhang.save(update_fields=['ketixian_jine']) + + # ---------- 3. 提现限额修改(需要6600d)---------- + kaioi_ewai = request.data.get('kaioi_ewai_tixian') + ewai_val = request.data.get('ewai_tixian_xiane') + if kaioi_ewai is not None or ewai_val is not None: + if '6600d' not in permissions: + return Response({'code': 403, 'msg': '无权限修改提现限额(需要6600d)'}) + if kaioi_ewai is not None: + zuzhang.kaioi_ewai_tixian = bool(kaioi_ewai) + if ewai_val is not None: + try: + zuzhang.ewai_tixian_xiane = Decimal(str(ewai_val)) + except: + return Response({'code': 400, 'msg': '额外限额金额格式错误'}) + zuzhang.save() + + # ---------- 4. 分红相关修改(需要6600e)---------- + perm_enabled = request.data.get('permanent_enabled') + perm_amount = request.data.get('permanent_amount') + custom_first = request.data.get('custom_first_dividend') + extra_map = request.data.get('extra_dividend_map') + + if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]): + if '6600e' not in permissions: + return Response({'code': 403, 'msg': '无权限修改分红策略(需要6600e)'}) + + # 4.1 永久分红(额外分红开关和金额) + if perm_enabled is not None: + zuzhang.kaioi_ewai_fenhong = bool(perm_enabled) + if perm_amount is not None: + try: + zuzhang.ewai_fenhong_jine = Decimal(str(perm_amount)) + except: + return Response({'code': 400, 'msg': '永久分红金额格式错误'}) + zuzhang.save() + + # 4.2 首次定制分红(cishu=1) + if custom_first is not None and isinstance(custom_first, dict): + existing_first = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu=1) + for huiyuan_id, amount in custom_first.items(): + try: + amount = Decimal(str(amount)) + except: + continue + if amount <= 0: + # 删除定制(恢复默认) + DuociFenhong.objects.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() + else: + DuociFenhong.objects.update_or_create( + yonghuid=yonghuid, + huiyuan=huiyuan_id, + cishu=1, + defaults={ + 'zuzhang_fenhong': amount, + 'guanshi_fenhong': Decimal('0') # 组长表只关心组长分红 + } + ) + # 删除那些在前端字典中不存在的首次定制记录(即已取消定制) + for record in existing_first: + if record.huiyuan not in custom_first: + record.delete() + + # 4.3 额外次数分红(cishu >= 2) + if extra_map is not None and isinstance(extra_map, dict): + keep_set = set() + for times_str, members in extra_map.items(): + try: + cishu = int(times_str) + if cishu < 2: + continue + except: + continue + if not isinstance(members, dict): + continue + for huiyuan_id, data in members.items(): + if not isinstance(data, dict): + continue + amount = data.get('zuzhang_fenhong') + if amount is None: + continue + try: + amount = Decimal(str(amount)) + except: + continue + keep_set.add((cishu, huiyuan_id)) + DuociFenhong.objects.update_or_create( + yonghuid=yonghuid, + huiyuan=huiyuan_id, + cishu=cishu, + defaults={ + 'zuzhang_fenhong': amount, + 'guanshi_fenhong': Decimal('0') + } + ) + # 删除不在 keep_set 中的额外次数记录 + existing_extra = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu__gte=2) + for record in existing_extra: + if (record.cishu, record.huiyuan) not in keep_set: + record.delete() + + # 保存组长其他可能修改的字段(例如电话、微信没有,忽略) + + return Response({'code': 0, 'msg': '修改成功'}) + + + + +class GetProductTypeZoneView(APIView): + """ + 获取商品类型、专区及会员列表 + 权限:需要拥有 7007a + 请求:POST /houtai/hthqsplxzq + 参数:{"username": "客服手机号"} + 返回: + code: 0 + data: { + type_list: [{id, jieshao, tupian_url, yaoqiuleixing, huiyuan_id, yongjin, shenhezhuangtai, is_cross_enabled, paixu, product_count}, ...], + zone_list: [{id, mingzi, leixing_id, shenhezhuangtai, paixu}, ...], + member_list: [{huiyuan_id, jieshao, jiage}, ...] + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '7007a' not in permissions: + return Response({'code': 403, 'msg': '您无权访问此页面'}) + + # 查询所有商品类型(含专区数量) + types = ShangpinLeixing.objects.all().order_by('-paixu') # paixu 越大越靠前 + type_list = [] + for t in types: + # 统计该类型下的专区数量 + zone_count = ShangpinZhuanqu.objects.filter(leixing_id=t.id).count() + type_list.append({ + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'yaoqiuleixing': t.yaoqiuleixing, + 'huiyuan_id': t.huiyuan_id or '', + 'yongjin': float(t.yongjin) if t.yongjin is not None else None, + 'shenhezhuangtai': t.shenhezhuangtai, + 'is_cross_enabled': t.is_cross_enabled, + 'paixu': t.paixu, + 'product_count': zone_count, + }) + + # 查询所有专区 + zones = ShangpinZhuanqu.objects.all().order_by('-paixu') + zone_list = [ + { + 'id': z.id, + 'mingzi': z.mingzi or '', + 'leixing_id': z.leixing_id, + 'shenhezhuangtai': z.shenhezhuangtai, + 'paixu': z.paixu, + } + for z in zones + ] + + # 查询所有会员(只返回 id、介绍、价格) + members = Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage') + member_list = [ + { + 'huiyuan_id': m['huiyuan_id'], + 'jieshao': m['jieshao'], + 'jiage': float(m['jiage']), + } + for m in members + ] + + return Response({ + 'code': 0, + 'data': { + 'type_list': type_list, + 'zone_list': zone_list, + 'member_list': member_list, + } + }) + + + + +class ModifyProductTypeZoneView(APIView): + """ + 统一修改商品类型/专区(增、删、改) + 权限:需要拥有 7007a + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser, MultiPartParser, FormParser] + + def post(self, request): + username = request.data.get('username', '').strip() + obj_type = request.data.get('type') # 'leixing' 或 'zhuanqu' + action = request.data.get('action') # 'add', 'update', 'delete' + + if not username or not obj_type or not action: + return Response({'code': 400, 'msg': '缺少必要参数'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '7007a' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + if obj_type == 'leixing': + return self.handle_leixing(request, action) + elif obj_type == 'zhuanqu': + return self.handle_zhuanqu(request, action) + else: + return Response({'code': 400, 'msg': '无效的type参数'}) + + # ---------- 商品类型处理 ---------- + def handle_leixing(self, request, action): + if action == 'add': + return self.add_leixing(request) + elif action == 'update': + return self.update_leixing(request) + elif action == 'delete': + return self.delete_leixing(request) + else: + return Response({'code': 400, 'msg': '无效的action'}) + + def add_leixing(self, request): + # 获取并校验字段 + jieshao = request.data.get('jieshao', '').strip() + if not jieshao: + return Response({'code': 400, 'msg': '类型名称不能为空'}) + + # 强制转为整数 + try: + yaoqiuleixing = int(request.data.get('yaoqiuleixing', 0)) + if yaoqiuleixing not in [1, 2]: + raise ValueError + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'}) + + try: + shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) + if shenhezhuangtai not in [1, 2]: + raise ValueError + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) + + is_cross_enabled = request.data.get('is_cross_enabled', False) + is_cross_enabled = is_cross_enabled in [True, 'true', '1', 'on'] + + huiyuan_id = request.data.get('huiyuan_id', '').strip() or None + yongjin = request.data.get('yongjin') + if yongjin is not None and yongjin != '': + try: + yongjin = Decimal(str(yongjin)) + except: + return Response({'code': 400, 'msg': '佣金金额格式错误'}) + else: + yongjin = None + + # 图片校验 + image_file = request.FILES.get('file') + if not image_file: + return Response({'code': 400, 'msg': '请上传类型图片'}) + + # 验证图片 + try: + valid, msg = validate_image(image_file) + if not valid: + return Response({'code': 400, 'msg': msg}) + except ImportError: + pass + + # 上传图片 + ext = image_file.name.split('.')[-1].lower() + new_filename = f"{uuid.uuid4().hex}.{ext}" + oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}" + url = upload_to_oss(image_file, oss_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + + # 创建类型 + try: + with transaction.atomic(): + leixing = ShangpinLeixing.objects.create( + jieshao=jieshao, + tupian_url=oss_path, + yaoqiuleixing=yaoqiuleixing, + huiyuan_id=huiyuan_id, + yongjin=yongjin, + shenhezhuangtai=shenhezhuangtai, + is_cross_enabled=is_cross_enabled, + paixu=0 + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': leixing.id}}) + except Exception as e: + logger.error(f"添加类型失败: {e}", exc_info=True) + delete_from_oss(oss_path) + return Response({'code': 500, 'msg': '数据库错误'}) + + def update_leixing(self, request): + type_id = request.data.get('id') + if not type_id: + return Response({'code': 400, 'msg': '缺少类型ID'}) + try: + leixing = ShangpinLeixing.objects.get(id=type_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 404, 'msg': '类型不存在'}) + + changes = {} + # 名称 + jieshao = request.data.get('jieshao') + if jieshao is not None and jieshao.strip() != leixing.jieshao: + changes['jieshao'] = jieshao.strip() + + # 抢单要求类型(强制转整数) + yaoqiuleixing = request.data.get('yaoqiuleixing') + if yaoqiuleixing is not None: + try: + new_val = int(yaoqiuleixing) + if new_val not in [1, 2]: + raise ValueError + if new_val != leixing.yaoqiuleixing: + changes['yaoqiuleixing'] = new_val + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'}) + + # 会员ID + huiyuan_id = request.data.get('huiyuan_id', '').strip() or None + if huiyuan_id != leixing.huiyuan_id: + changes['huiyuan_id'] = huiyuan_id + + # 佣金金额 + yongjin = request.data.get('yongjin') + if yongjin is not None: + if yongjin == '': + new_val = None + else: + try: + new_val = Decimal(str(yongjin)) + except: + return Response({'code': 400, 'msg': '佣金金额格式错误'}) + if new_val != leixing.yongjin: + changes['yongjin'] = new_val + + # 审核状态 + shenhezhuangtai = request.data.get('shenhezhuangtai') + if shenhezhuangtai is not None: + try: + new_val = int(shenhezhuangtai) + if new_val not in [1, 2]: + raise ValueError + if new_val != leixing.shenhezhuangtai: + changes['shenhezhuangtai'] = new_val + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) + + # 跨平台互通 + is_cross = request.data.get('is_cross_enabled') + if is_cross is not None: + new_cross = is_cross in [True, 'true', '1', 'on'] + if new_cross != leixing.is_cross_enabled: + changes['is_cross_enabled'] = new_cross + + # 图片处理 + image_file = request.FILES.get('file') + old_image_path = leixing.tupian_url + if image_file: + try: + valid, msg = validate_image(image_file) + if not valid: + return Response({'code': 400, 'msg': msg}) + except ImportError: + pass + ext = image_file.name.split('.')[-1].lower() + new_filename = f"{uuid.uuid4().hex}.{ext}" + oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}" + url = upload_to_oss(image_file, oss_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + changes['tupian_url'] = oss_path + + if not changes and not image_file: + return Response({'code': 400, 'msg': '未做任何修改'}) + + sync_to_products = request.data.get('sync_to_products') in [True, 'true', '1', 'on'] + try: + with transaction.atomic(): + for field, value in changes.items(): + setattr(leixing, field, value) + leixing.save() + + # 同步商品 + if sync_to_products and ('yaoqiuleixing' in changes or 'huiyuan_id' in changes or 'yongjin' in changes): + product_updates = {} + if 'yaoqiuleixing' in changes: + product_updates['yaoqiuleixing'] = changes['yaoqiuleixing'] + if 'huiyuan_id' in changes: + product_updates['huiyuan_id'] = changes['huiyuan_id'] + if 'yongjin' in changes: + product_updates['yongjin'] = changes['yongjin'] + if product_updates: + Shangpin.objects.filter(leixing_id=type_id).update(**product_updates) + + # 删除旧图片 + if image_file and old_image_path: + delete_from_oss(old_image_path) + + return Response({'code': 0, 'msg': '修改成功'}) + except Exception as e: + logger.error(f"更新类型失败: {e}", exc_info=True) + if image_file and 'oss_path' in locals(): + delete_from_oss(oss_path) + return Response({'code': 500, 'msg': '修改失败'}) + + def delete_leixing(self, request): + type_id = request.data.get('id') + if not type_id: + return Response({'code': 400, 'msg': '缺少类型ID'}) + delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on'] + + try: + leixing = ShangpinLeixing.objects.get(id=type_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 404, 'msg': '类型不存在'}) + + with transaction.atomic(): + # 删除该类型下的所有专区 + zones = ShangpinZhuanqu.objects.filter(leixing_id=type_id) + if delete_products: + # 删除专区下的所有商品 + for zone in zones: + Shangpin.objects.filter(zhuanqu_id=zone.id).delete() + zones.delete() + # 删除直接关联该类型的商品 + Shangpin.objects.filter(leixing_id=type_id).delete() + else: + zones.delete() + # 仅清除商品的类型关联 + Shangpin.objects.filter(leixing_id=type_id).update(leixing_id=None) + + leixing.delete() + + if leixing.tupian_url: + delete_from_oss(leixing.tupian_url) + + return Response({'code': 0, 'msg': '删除成功'}) + + # ---------- 商品专区处理 ---------- + def handle_zhuanqu(self, request, action): + if action == 'add': + return self.add_zhuanqu(request) + elif action == 'update': + return self.update_zhuanqu(request) + elif action == 'delete': + return self.delete_zhuanqu(request) + else: + return Response({'code': 400, 'msg': '无效的action'}) + + def add_zhuanqu(self, request): + mingzi = request.data.get('mingzi', '').strip() + leixing_id = request.data.get('leixing_id') + if not mingzi: + return Response({'code': 400, 'msg': '专区名称不能为空'}) + if not leixing_id: + return Response({'code': 400, 'msg': '请选择所属类型'}) + + try: + leixing_id = int(leixing_id) + if not ShangpinLeixing.objects.filter(id=leixing_id).exists(): + return Response({'code': 404, 'msg': '所属类型不存在'}) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '所属类型ID无效'}) + + try: + shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) + if shenhezhuangtai not in [1, 2]: + raise ValueError + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) + + try: + with transaction.atomic(): + zhuanqu = ShangpinZhuanqu.objects.create( + mingzi=mingzi, + leixing_id=leixing_id, + shenhezhuangtai=shenhezhuangtai, + paixu=0 + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': zhuanqu.id}}) + except Exception as e: + logger.error(f"添加专区失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '数据库错误'}) + + def update_zhuanqu(self, request): + zone_id = request.data.get('id') + if not zone_id: + return Response({'code': 400, 'msg': '缺少专区ID'}) + try: + zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 404, 'msg': '专区不存在'}) + + changes = {} + mingzi = request.data.get('mingzi') + if mingzi is not None and mingzi.strip() != zhuanqu.mingzi: + changes['mingzi'] = mingzi.strip() + + leixing_id = request.data.get('leixing_id') + if leixing_id is not None: + try: + new_id = int(leixing_id) + if not ShangpinLeixing.objects.filter(id=new_id).exists(): + return Response({'code': 404, 'msg': '新所属类型不存在'}) + if new_id != zhuanqu.leixing_id: + changes['leixing_id'] = new_id + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '所属类型ID无效'}) + + shenhezhuangtai = request.data.get('shenhezhuangtai') + if shenhezhuangtai is not None: + try: + new_val = int(shenhezhuangtai) + if new_val not in [1, 2]: + raise ValueError + if new_val != zhuanqu.shenhezhuangtai: + changes['shenhezhuangtai'] = new_val + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) + + if not changes: + return Response({'code': 400, 'msg': '未做任何修改'}) + + try: + with transaction.atomic(): + for field, value in changes.items(): + setattr(zhuanqu, field, value) + zhuanqu.save() + return Response({'code': 0, 'msg': '修改成功'}) + except Exception as e: + logger.error(f"更新专区失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '修改失败'}) + + def delete_zhuanqu(self, request): + zone_id = request.data.get('id') + if not zone_id: + return Response({'code': 400, 'msg': '缺少专区ID'}) + delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on'] + + try: + zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 404, 'msg': '专区不存在'}) + + with transaction.atomic(): + if delete_products: + Shangpin.objects.filter(zhuanqu_id=zone_id).delete() + else: + Shangpin.objects.filter(zhuanqu_id=zone_id).update(zhuanqu_id=None) + zhuanqu.delete() + + return Response({'code': 0, 'msg': '删除成功'}) + + +# 继续在 houtai/views_product.py 中添加 +class GetRateView(APIView): + """ + 获取打手分成费率(平台订单和商家订单) + 权限:需要拥有 7007b + 请求:POST /houtai/hthqfhpz + 参数:{"username": "客服手机号"} + 返回:{"code":0, "data": {"platform_rate": xx, "merchant_rate": xx}} + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '7007b' not in permissions: + return Response({'code': 403, 'msg': '您无权查看费率'}) + + try: + platform_rate_obj = Lilubiao.objects.get(fadanpingtai='1') + merchant_rate_obj = Lilubiao.objects.get(fadanpingtai='3') + except Lilubiao.DoesNotExist as e: + logger.error(f"费率记录缺失: {e}") + return Response({'code': 500, 'msg': '费率配置缺失'}) + + return Response({ + 'code': 0, + 'data': { + 'platform_rate': float(platform_rate_obj.lilu) if platform_rate_obj.lilu else 0, + 'merchant_rate': float(merchant_rate_obj.lilu) if merchant_rate_obj.lilu else 0, + } + }) + + +class ModifyRateView(APIView): + """ + 修改打手分成费率 + 权限:需要拥有 7007b + 请求:POST /houtai/htxgfl + 参数:{"username": "客服手机号", "type": "platform" / "merchant", "value": 费率(百分比数字)} + 返回:{"code":0, "msg":"修改成功"} + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + rate_type = request.data.get('type') + value = request.data.get('value') + + if not username or not rate_type or value is None: + return Response({'code': 400, 'msg': '缺少参数'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '7007b' not in permissions: + return Response({'code': 403, 'msg': '您无权修改费率'}) + + try: + rate_value = Decimal(str(value)) + except: + return Response({'code': 400, 'msg': '费率值格式错误'}) + + + if rate_type == 'platform': + fadanpingtai = '1' + elif rate_type == 'merchant': + fadanpingtai = '3' + else: + return Response({'code': 400, 'msg': '无效的type参数'}) + + try: + with transaction.atomic(): + rate_obj = Lilubiao.objects.select_for_update().get(fadanpingtai=fadanpingtai) + rate_obj.lilu = rate_value + rate_obj.save() + except Lilubiao.DoesNotExist: + return Response({'code': 404, 'msg': '费率记录不存在'}) + except Exception as e: + logger.error(f"修改费率失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '修改失败'}) + + return Response({'code': 0, 'msg': '修改成功'}) + + + + + + +# 权限码(根据你要求,使用 '8080a') +REQUIRED_PERMISSION = '8080a' +class PopupNoticeListAPIView(APIView): + """ + 获取所有弹窗配置(页面 + 弹窗 + 图片) + URL: POST /houtai/hthqtcxx + 请求参数: { "username": "客服账号" } + 响应格式: { "code": 0, "data": { "pages": [...] } } + 注意:所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段区分 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取前端传递的 username(用于防越权) + username_frontend = request.data.get('username') + # 2. 调用公共权限验证方法,返回 (客服对象, 权限列表) + kefu, permissions = verify_kefu_permission(request, username_frontend) + if kefu is None: + # 身份验证失败,返回业务码 403,但 HTTP 状态码仍是 200 + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + + # 3. 检查是否拥有所需权限码 '8080a' + if REQUIRED_PERMISSION not in permissions: + return Response({'code': 403, 'msg': '您没有权限访问弹窗管理功能'}) + + # 4. 查询所有页面,并预加载关联的弹窗和图片(避免N+1查询) + pages = PopupPage.objects.all().prefetch_related('popups__images').order_by('id') + + # 5. 序列化输出 + serializer = PopupPageSerializer(pages, many=True) + return Response({'code': 0, 'data': {'pages': serializer.data}}) + + +class PopupNoticeModifyAPIView(APIView): + """ + 统一修改接口:支持页面、弹窗、图片的增删改 + URL: POST /houtai/htxgtcxx + 请求中必须包含 action 字段,用于区分操作类型。 + 所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段体现。 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ---------- 1. 权限验证 ---------- + username_frontend = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username_frontend) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + if REQUIRED_PERMISSION not in permissions: + return Response({'code': 403, 'msg': '您没有权限操作弹窗管理'}) + + # ---------- 2. 获取 action 并分发 ---------- + action = request.data.get('action') + if not action: + return Response({'code': 400, 'msg': '缺少action参数'}) + + try: + if action == 'add_page': + return self.add_page(request) + elif action == 'update_page': + return self.update_page(request) + elif action == 'delete_page': + return self.delete_page(request) + elif action == 'add_popup': + return self.add_popup(request) + elif action == 'update_popup': + return self.update_popup(request) + elif action == 'delete_popup': + return self.delete_popup(request) + elif action == 'add_image': + return self.add_image(request) + elif action == 'update_image': + return self.update_image(request) + elif action == 'delete_image': + return self.delete_image(request) + else: + return Response({'code': 400, 'msg': f'未知的action: {action}'}) + except Exception as e: + # 捕获未预期的异常,返回服务器错误 + traceback.print_exc() + return Response({'code': 500, 'msg': f'服务器错误: {str(e)}'}) + + # ==================== 页面相关操作 ==================== + + def add_page(self, request): + """添加新页面""" + data = request.data + # 必填字段校验 + if not data.get('page_key') or not data.get('name'): + return Response({'code': 400, 'msg': '缺少 page_key 或 name'}) + # 唯一性校验 + if PopupPage.objects.filter(page_key=data['page_key']).exists(): + return Response({'code': 400, 'msg': '页面标识已存在'}) + + page = PopupPage.objects.create( + page_key=data['page_key'], + name=data['name'], + description=data.get('description', ''), + is_active=data.get('is_active', True), + ignore_user_mute=data.get('ignore_user_mute', False) + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': page.id}}) + + def update_page(self, request): + """更新页面信息""" + page_id = request.data.get('id') + if not page_id: + return Response({'code': 400, 'msg': '缺少页面id'}) + try: + page = PopupPage.objects.get(id=page_id) + except PopupPage.DoesNotExist: + return Response({'code': 404, 'msg': '页面不存在'}) + + # 更新允许修改的字段 + page.page_key = request.data.get('page_key', page.page_key) + page.name = request.data.get('name', page.name) + page.description = request.data.get('description', page.description) + page.is_active = request.data.get('is_active', page.is_active) + page.ignore_user_mute = request.data.get('ignore_user_mute', page.ignore_user_mute) + page.save() + return Response({'code': 0, 'msg': '更新成功'}) + + def delete_page(self, request): + """删除页面,可级联删除其下所有弹窗和图片""" + page_id = request.data.get('id') + cascade = request.data.get('cascade_delete_popups', True) # 默认级联 + if not page_id: + return Response({'code': 400, 'msg': '缺少页面id'}) + try: + page = PopupPage.objects.get(id=page_id) + except PopupPage.DoesNotExist: + return Response({'code': 404, 'msg': '页面不存在'}) + + if cascade: + # 级联删除:使用事务保证原子性 + with transaction.atomic(): + popups = page.popups.all() + for popup in popups: + # 删除弹窗下的所有图片文件 + for img in popup.images.all(): + if img.image_url: + delete_from_oss(img.image_url) + popups.delete() + page.delete() + else: + # 非级联删除:如果页面下还有弹窗,则拒绝删除 + if page.popups.exists(): + return Response({'code': 400, 'msg': '该页面下存在弹窗,请先删除弹窗或使用级联删除'}) + page.delete() + + return Response({'code': 0, 'msg': '删除成功'}) + + # ==================== 弹窗相关操作 ==================== + + def add_popup(self, request): + """为指定页面添加新弹窗""" + data = request.data + page_id = data.get('page_id') + popup_id = data.get('popup_id') + if not page_id or not popup_id: + return Response({'code': 400, 'msg': '缺少 page_id 或 popup_id'}) + try: + page = PopupPage.objects.get(id=page_id) + except PopupPage.DoesNotExist: + return Response({'code': 404, 'msg': '页面不存在'}) + + # 同一页面下 popup_id 必须唯一 + if PopupConfig.objects.filter(page=page, popup_id=popup_id).exists(): + return Response({'code': 400, 'msg': '该页面下已存在相同的弹窗ID'}) + + popup = PopupConfig.objects.create( + page=page, + popup_id=popup_id, + title=data.get('title', ''), + description=data.get('description', ''), + strategy_type=data.get('strategy_type', 'daily'), + max_count=data.get('max_count', 1), + reset_interval=data.get('reset_interval', 'day'), + duration=data.get('duration', 0), + force_even_muted=data.get('force_even_muted', False), + sort_order=data.get('sort_order', 0), + is_active=data.get('is_active', True), + start_time=data.get('start_time') or None, + end_time=data.get('end_time') or None + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': popup.id}}) + + def update_popup(self, request): + """更新弹窗信息""" + popup_id = request.data.get('id') + if not popup_id: + return Response({'code': 400, 'msg': '缺少弹窗id'}) + try: + popup = PopupConfig.objects.get(id=popup_id) + except PopupConfig.DoesNotExist: + return Response({'code': 404, 'msg': '弹窗不存在'}) + + # 更新弹窗字段 + popup.popup_id = request.data.get('popup_id', popup.popup_id) + popup.title = request.data.get('title', popup.title) + popup.description = request.data.get('description', popup.description) + popup.strategy_type = request.data.get('strategy_type', popup.strategy_type) + popup.max_count = request.data.get('max_count', popup.max_count) + popup.reset_interval = request.data.get('reset_interval', popup.reset_interval) + popup.duration = request.data.get('duration', popup.duration) + popup.force_even_muted = request.data.get('force_even_muted', popup.force_even_muted) + popup.sort_order = request.data.get('sort_order', popup.sort_order) + popup.is_active = request.data.get('is_active', popup.is_active) + popup.start_time = request.data.get('start_time') or None + popup.end_time = request.data.get('end_time') or None + popup.save() + return Response({'code': 0, 'msg': '更新成功'}) + + def delete_popup(self, request): + """删除弹窗,同时删除其下所有图片(包括OSS文件)""" + popup_id = request.data.get('id') + if not popup_id: + return Response({'code': 400, 'msg': '缺少弹窗id'}) + try: + popup = PopupConfig.objects.get(id=popup_id) + except PopupConfig.DoesNotExist: + return Response({'code': 404, 'msg': '弹窗不存在'}) + + with transaction.atomic(): + for img in popup.images.all(): + if img.image_url: + delete_from_oss(img.image_url) + popup.images.all().delete() + popup.delete() + return Response({'code': 0, 'msg': '删除成功'}) + + # ==================== 图片相关操作 ==================== + + def _upload_image_file(self, file_obj, page_key): + """ + 内部方法:将图片上传到OSS指定目录 + 目录: a_long/tanchuang/tanchuang/ + 文件名: {page_key}_{时间戳}_{uuid8}.{ext} + 返回: 相对路径(例如 a_long/tanchuang/tanchuang/dashouzhongxin_20250419120000_abc12345.jpg) + """ + # 验证图片格式和大小 + valid, msg = validate_image(file_obj) + if not valid: + raise ValueError(msg) + + # 生成唯一文件名 + ext = os.path.splitext(file_obj.name)[1].lower() + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + unique_id = str(uuid.uuid4())[:8] + filename = f"{page_key}_{timestamp}_{unique_id}{ext}" + relative_path = f"a_long/tanchuang/tanchuang/{filename}" + + # 上传到OSS + url = upload_to_oss(file_obj, relative_path) + if not url: + raise ValueError("图片上传失败") + return relative_path + + def add_image(self, request): + """为指定弹窗添加图片(支持上传文件)""" + popup_id = request.data.get('popup_id') + if not popup_id: + return Response({'code': 400, 'msg': '缺少 popup_id'}) + try: + popup = PopupConfig.objects.get(id=popup_id) + except PopupConfig.DoesNotExist: + return Response({'code': 404, 'msg': '弹窗不存在'}) + + # 获取上传的文件 + file_obj = request.FILES.get('file') + if not file_obj: + return Response({'code': 400, 'msg': '未提供图片文件'}) + + try: + page_key = popup.page.page_key + relative_path = self._upload_image_file(file_obj, page_key) + except ValueError as e: + return Response({'code': 400, 'msg': str(e)}) + + # 保存图片记录 + image = PopupImage.objects.create( + popup_config=popup, + image_url=relative_path, + text=request.data.get('text', ''), + sort_order=request.data.get('sort_order', 0) + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': image.id}}) + + def update_image(self, request): + """更新图片信息(可替换图片文件、修改文字、排序)""" + image_id = request.data.get('id') + if not image_id: + return Response({'code': 400, 'msg': '缺少图片id'}) + try: + image = PopupImage.objects.get(id=image_id) + except PopupImage.DoesNotExist: + return Response({'code': 404, 'msg': '图片不存在'}) + + # 更新文字和排序 + image.text = request.data.get('text', image.text) + image.sort_order = request.data.get('sort_order', image.sort_order) + + # 如果提供了新图片文件,则替换 + file_obj = request.FILES.get('file') + if file_obj: + try: + page_key = image.popup_config.page.page_key + new_path = self._upload_image_file(file_obj, page_key) + except ValueError as e: + return Response({'code': 400, 'msg': str(e)}) + # 删除旧图片 + if image.image_url: + delete_from_oss(image.image_url) + image.image_url = new_path + + image.save() + return Response({'code': 0, 'msg': '更新成功'}) + + def delete_image(self, request): + """删除图片(同时删除OSS文件)""" + image_id = request.data.get('id') + if not image_id: + return Response({'code': 400, 'msg': '缺少图片id'}) + try: + image = PopupImage.objects.get(id=image_id) + except PopupImage.DoesNotExist: + return Response({'code': 404, 'msg': '图片不存在'}) + + if image.image_url: + delete_from_oss(image.image_url) + image.delete() + return Response({'code': 0, 'msg': '删除成功'}) + + + +class ShopListView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 调用公共权限验证(与您给的示例完全一致) + username_frontend = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username_frontend) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + + # 2. 检查是否拥有所需权限(999a~999e 任意一个) + required_perms = {'999a', '999b', '999c', '999d', '999e'} + if not required_perms.intersection(set(permissions)): + return Response({'code': 403, 'msg': '您没有权限访问店铺管理功能'}) + + # 3. 解析请求参数 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 10)) + dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip() + zhuangtai = request.data.get('zhuangtai') + lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip() + user_id = request.data.get('user_id', '').strip() + ketixian_yue_min = request.data.get('ketixian_yue_min') + ketixian_yue_max = request.data.get('ketixian_yue_max') + zhengqu_zonge_min = request.data.get('zhengqu_zonge_min') + zhengqu_zonge_max = request.data.get('zhengqu_zonge_max') + + # 4. 构建查询集(先查店铺,预加载关联凭证) + queryset = Dianpu.objects.select_related('pingzheng').all() + + if dianpu_mingcheng: + queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng) + if zhuangtai is not None: + queryset = queryset.filter(zhuangtai=zhuangtai) + if lianxi_dianhua: + queryset = queryset.filter(lianxi_dianhua__icontains=lianxi_dianhua) + if user_id: + queryset = queryset.filter(pingzheng__yonghu=user_id) + + # 金额范围过滤 + if ketixian_yue_min is not None: + queryset = queryset.filter(ketixian_yue__gte=ketixian_yue_min) + if ketixian_yue_max is not None: + queryset = queryset.filter(ketixian_yue__lte=ketixian_yue_max) + if zhengqu_zonge_min is not None: + queryset = queryset.filter(zhengqu_zonge__gte=zhengqu_zonge_min) + if zhengqu_zonge_max is not None: + queryset = queryset.filter(zhengqu_zonge__lte=zhengqu_zonge_max) + + # 5. 分页 + paginator = Paginator(queryset, page_size) + page_obj = paginator.get_page(page) + + # 6. 构造返回数据 + shop_list = [] + for dianpu in page_obj: + pingzheng = dianpu.pingzheng + shop_list.append({ + 'id': dianpu.id, + 'dianpu_mingcheng': dianpu.dianpu_mingcheng, + 'dianpu_touxiang': dianpu.dianpu_touxiang or '', + 'lianxi_dianhua': dianpu.lianxi_dianhua or '', + 'weixinhao': dianpu.weixinhao or '', + 'zhuangtai': dianpu.zhuangtai, + 'bangding_yonghushu': dianpu.bangding_yonghushu, + 'erweima_url': dianpu.erweima_url or '', + 'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng, + 'shouyi_choucheng_feilv': dianpu.shouyi_choucheng_feilv or 0, + 'ketixian_yue': dianpu.ketixian_yue, + 'zhengqu_zonge': dianpu.zhengqu_zonge, + 'meiri_tixian_xiane': dianpu.meiri_tixian_xiane or 0, + 'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane, + 'pingzheng__yonghu': pingzheng.yonghu, # 用户ID + 'pingzheng__zhanghao': pingzheng.zhanghao, # 登录账号 + }) + + # 获取默认配置(ID=1,不存在则创建) + default_config, _ = DianpuMorenPeizhi.objects.get_or_create(id=1, defaults={ + 'moren_choucheng_feilv': 0.1, + 'moren_tixian_xiane': 1000.00 + }) + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'list': shop_list, + 'total': paginator.count, + 'default_config': { + 'moren_choucheng_feilv': default_config.moren_choucheng_feilv, + 'moren_tixian_xiane': default_config.moren_tixian_xiane, + } + } + }) + + +class ShopModifyView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 身份验证 + username_frontend = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username_frontend) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + + # 转换权限为集合方便判断 + user_perms = set(permissions) + action = request.data.get('action', '') + + # 2. 根据 action 分派任务 + if action == 'update_dianpu': + return self._update_dianpu(request, user_perms) + elif action == 'add_dianpu': + return self._add_dianpu(request, user_perms) + elif action == 'update_default': + return self._update_default(request, user_perms) + else: + return Response({'code': 400, 'msg': '未知操作类型'}) + + def _update_dianpu(self, request, user_perms): + """修改店铺信息,精确权限控制""" + dianpu_id = request.data.get('dianpu_id') + if not dianpu_id: + return Response({'code': 400, 'msg': '缺少店铺ID'}) + + try: + with transaction.atomic(): + dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id) + + # 基础信息修改 (999e) + if any(k in request.data for k in ['dianpu_mingcheng', 'lianxi_dianhua', 'weixinhao']): + if '999e' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改店铺基本信息(999e)'}) + if 'dianpu_mingcheng' in request.data: + dianpu.dianpu_mingcheng = request.data['dianpu_mingcheng'] + if 'lianxi_dianhua' in request.data: + dianpu.lianxi_dianhua = request.data['lianxi_dianhua'] + if 'weixinhao' in request.data: + dianpu.weixinhao = request.data['weixinhao'] + + # 店铺状态 (999a) + if 'zhuangtai' in request.data: + if '999a' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改店铺状态(999a)'}) + new_status = int(request.data['zhuangtai']) + if new_status not in (0, 1): + return Response({'code': 400, 'msg': '店铺状态无效'}) + dianpu.zhuangtai = new_status + + # 可提现余额 (999b) + if 'ketixian_yue' in request.data: + if '999b' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改可提现余额(999b)'}) + dianpu.ketixian_yue = request.data['ketixian_yue'] + + # 提现限额相关 (999c) + if 'kaiqi_meiri_xiane' in request.data or 'meiri_tixian_xiane' in request.data: + if '999c' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改提现限额配置(999c)'}) + if 'kaiqi_meiri_xiane' in request.data: + dianpu.kaiqi_meiri_xiane = request.data['kaiqi_meiri_xiane'] + if 'meiri_tixian_xiane' in request.data: + dianpu.meiri_tixian_xiane = request.data['meiri_tixian_xiane'] + + # 抽成相关 (999d) + if 'kaiqi_shouyi_choucheng' in request.data or 'shouyi_choucheng_feilv' in request.data: + if '999d' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改抽成配置(999d)'}) + if 'kaiqi_shouyi_choucheng' in request.data: + dianpu.kaiqi_shouyi_choucheng = request.data['kaiqi_shouyi_choucheng'] + if 'shouyi_choucheng_feilv' in request.data: + dianpu.shouyi_choucheng_feilv = request.data['shouyi_choucheng_feilv'] + + dianpu.save() # Django 会自动判断更新哪些字段 + return Response({'code': 0, 'msg': '店铺信息更新成功'}) + + except Dianpu.DoesNotExist: + return Response({'code': 404, 'msg': '店铺不存在'}) + except Exception as e: + logger.exception("修改店铺失败") + return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) + + def _add_dianpu(self, request, user_perms): + """添加店铺,需要权限 999b""" + if '999b' not in user_perms: + return Response({'code': 403, 'msg': '无权限添加店铺(999b)'}) + + yonghu_id = request.data.get('yonghu_id') + zhanghao = request.data.get('zhanghao') + mima = request.data.get('mima') + dianpu_mingcheng = request.data.get('dianpu_mingcheng') + + if not all([yonghu_id, zhanghao, mima, dianpu_mingcheng]): + return Response({'code': 400, 'msg': '必填字段缺失(用户ID、账号、密码、店铺名称)'}) + + # 检查用户是否存在 + if not UserMain.objects.filter(yonghuid=yonghu_id).exists(): + return Response({'code': 404, 'msg': '用户不存在'}) + + # 检查凭证是否已存在 + if YonghuPingzheng.objects.filter(zhanghao=zhanghao).exists(): + return Response({'code': 400, 'msg': '登录账号已存在'}) + if YonghuPingzheng.objects.filter(yonghu=yonghu_id).exists(): + return Response({'code': 400, 'msg': '该用户ID已有店铺凭证'}) + + try: + with transaction.atomic(): + pingzheng = YonghuPingzheng.objects.create( + yonghu=yonghu_id, + zhanghao=zhanghao, + mima=mima, + is_active=True + ) + dianpu = Dianpu.objects.create( + pingzheng=pingzheng, + dianpu_mingcheng=dianpu_mingcheng, + lianxi_dianhua=request.data.get('lianxi_dianhua', ''), + weixinhao=request.data.get('weixinhao', ''), + kaiqi_shouyi_choucheng=request.data.get('kaiqi_shouyi_choucheng', False), + shouyi_choucheng_feilv=request.data.get('shouyi_choucheng_feilv', 0), + kaiqi_meiri_xiane=request.data.get('kaiqi_meiri_xiane', False), + meiri_tixian_xiane=request.data.get('meiri_tixian_xiane', 0), + ) + return Response({'code': 0, 'msg': '店铺创建成功', 'dianpu_id': dianpu.id}) + except IntegrityError as e: + logger.error("创建店铺违反唯一性约束: %s", e) + return Response({'code': 400, 'msg': '数据冲突,请检查账号或用户ID'}) + except Exception as e: + logger.exception("创建店铺失败") + return Response({'code': 500, 'msg': '创建失败,请稍后重试'}) + + def _update_default(self, request, user_perms): + """修改默认配置,按修改内容校验权限""" + feilv = request.data.get('moren_choucheng_feilv') + xiane = request.data.get('moren_tixian_xiane') + modify_commission = feilv is not None + modify_withdraw = xiane is not None + + if modify_commission and '999d' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改默认抽成费率(999d)'}) + if modify_withdraw and '999c' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'}) + + try: + config, _ = DianpuMorenPeizhi.objects.get_or_create(id=1) + if modify_commission: + config.moren_choucheng_feilv = feilv + if modify_withdraw: + config.moren_tixian_xiane = xiane + config.save(update_fields=(['moren_choucheng_feilv'] if modify_commission else []) + + (['moren_tixian_xiane'] if modify_withdraw else [])) + return Response({'code': 0, 'msg': '默认配置更新成功'}) + except Exception as e: + logger.exception("更新默认配置失败") + return Response({'code': 500, 'msg': '更新失败'}) + + + + + + + + +# ==================== 1. 公共商品类型 + 审核模式 ==================== +class ShopPublicTypeAndAuditView(APIView): + """ + 返回:①正常状态的公共商品类型列表 ②全局商品审核模式开关 + 前端请求路径:/houtai/htdphqsplx + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ---------- 身份与权限校验 ---------- + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + + # 拥有 1199ab/1199abc/1199abd 任一个即可访问 + if not set(permissions).intersection(SHOP_PRODUCT_PERMS): + return Response({'code': 403, 'msg': '无权访问店铺商品管理功能'}) + + # ---------- 查询公共商品类型(仅正常审核状态的) ---------- + try: + # shenhezhuangtai = 1 表示正常 (根据模型默认值) + types_qs = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values( + 'id', 'jieshao', 'tupian_url', 'yaoqiuleixing', 'huiyuan_id', 'yongjin', 'is_cross_enabled' + ) + type_list = [] + for t in types_qs: + type_list.append({ + 'id': t['id'], + 'jieshao': t['jieshao'] or '', + 'tupian_url': t['tupian_url'] or '', + 'yaoqiuleixing': t['yaoqiuleixing'], + 'huiyuan_id': t['huiyuan_id'], + 'yongjin': t['yongjin'], + 'is_cross_enabled': t['is_cross_enabled'], + }) + except Exception as e: + logger.exception("查询公共商品类型失败") + return Response({'code': 500, 'msg': '服务器错误,请稍后重试'}) + + # ---------- 获取审核模式配置 ---------- + audit_config = DianpuShangpinShenheShezhi.objects.filter(id=1).first() + kaiqi_shenhe = audit_config.kaiqi_shenhe if audit_config else False + + return Response({ + 'code': 0, + 'data': { + 'public_types': type_list, + 'kaiqi_shenhe': kaiqi_shenhe + } + }) + + +# ==================== 2. 店铺列表(支持搜索、筛选、分页) ==================== +class ShopListForProductView(APIView): + """ + 返回店铺列表,用于商品管理页面选择店铺。 + 支持:关键词搜索(店铺ID/名称/用户ID)、店铺状态筛选、分页。 + 前端请求路径:/houtai/htdphqdpys + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + if not set(permissions).intersection(SHOP_PRODUCT_PERMS): + return Response({'code': 403, 'msg': '无权访问店铺列表'}) + + # ---------- 安全解析分页参数 ---------- + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 10)) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '分页参数格式错误'}) + + # 搜索关键词(字符串,ORM 自动防注入) + keyword = str(request.data.get('keyword', '')).strip() + # 店铺状态筛选(1正常,0封禁,不传则全部) + zhuangtai = request.data.get('zhuangtai') + + # ---------- 构建查询集(关联凭证表获取用户ID) ---------- + shops = Dianpu.objects.select_related('pingzheng').all() + + # 状态筛选:只接受 0 或 1 + if zhuangtai is not None: + try: + zhuangtai = int(zhuangtai) + if zhuangtai in [0, 1]: + shops = shops.filter(zhuangtai=zhuangtai) + else: + return Response({'code': 400, 'msg': '状态参数只能为0或1'}) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '状态参数必须为整数'}) + + # 关键词搜索(店铺ID/名称/用户ID) + if keyword: + shops = shops.filter( + models.Q(id__icontains=keyword) | + models.Q(dianpu_mingcheng__icontains=keyword) | + models.Q(pingzheng__yonghu__icontains=keyword) + ) + + # ---------- 分页 ---------- + paginator = Paginator(shops, page_size) + page_obj = paginator.get_page(page) + + shop_list = [] + for shop in page_obj: + shop_list.append({ + 'id': shop.id, + 'dianpu_mingcheng': shop.dianpu_mingcheng, + 'zhuangtai': shop.zhuangtai, + 'yonghu_id': shop.pingzheng.yonghu if shop.pingzheng else '', + 'lianxi_dianhua': shop.lianxi_dianhua or '', + 'weixinhao': shop.weixinhao or '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': shop_list, + 'total': paginator.count + } + }) + + +# ==================== 3. 店铺商品类型映射列表(含筛选、待审核计数) ==================== +class ShopProductTypeMappingView(APIView): + """ + 返回店铺商品类型映射(ShangpinLeixingDianpu)列表。 + 支持:店铺ID、公共类型ID、上架/封禁/审核状态筛选,关键字搜索(介绍/ID/店铺名/公共类型名)。 + 每次请求同时返回“待审核总数”(shenhe_zhuangtai=False 的记录数)。 + 前端请求路径:/houtai/htdphqyssplx + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + if not set(permissions).intersection(SHOP_PRODUCT_PERMS): + return Response({'code': 403, 'msg': '无权访问商品类型映射'}) + + # ---------- 安全解析参数 ---------- + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 10)) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '分页参数格式错误'}) + + keyword = str(request.data.get('keyword', '')).strip() + shop_id = request.data.get('shop_id') + public_type_id = request.data.get('public_type_id') + + # 布尔型筛选字段处理:None 表示不筛选,否则转为布尔值 + def parse_bool(val): + if val is None: + return None + return bool(val) + + shangjia = parse_bool(request.data.get('shangjia_zhuangtai')) + fengjin = parse_bool(request.data.get('fengjin_zhuangtai')) + shenhe = parse_bool(request.data.get('shenhe_zhuangtai')) + + # ---------- 查询(连表店铺和公共类型) ---------- + mappings = ShangpinLeixingDianpu.objects.select_related('dianpu', 'gonggong_leixing').all() + + # 筛选条件:均为参数化,完全防注入 + if shop_id is not None: + mappings = mappings.filter(dianpu_id=int(shop_id)) + if public_type_id is not None: + mappings = mappings.filter(gonggong_leixing_id=int(public_type_id)) + if shangjia is not None: + mappings = mappings.filter(shangjia_zhuangtai=shangjia) + if fengjin is not None: + mappings = mappings.filter(fengjin_zhuangtai=fengjin) + if shenhe is not None: + mappings = mappings.filter(shenhe_zhuangtai=shenhe) + + # 关键字搜索(介绍、ID、店铺名、公共类型名) + if keyword: + mappings = mappings.filter( + models.Q(jieshao__icontains=keyword) | + models.Q(id__icontains=keyword) | + models.Q(dianpu__dianpu_mingcheng__icontains=keyword) | + models.Q(gonggong_leixing__jieshao__icontains=keyword) + ) + + # ---------- 待审核总数(不受分页影响) ---------- + pending_total = mappings.filter(shenhe_zhuangtai=False).count() + + # ---------- 分页 ---------- + paginator = Paginator(mappings, page_size) + page_obj = paginator.get_page(page) + + mapping_list = [] + for m in page_obj: + mapping_list.append({ + 'id': m.id, + 'dianpu_id': m.dianpu_id, + 'dianpu_name': m.dianpu.dianpu_mingcheng if m.dianpu else '', + 'gonggong_leixing_id': m.gonggong_leixing_id, + 'gonggong_leixing_name': m.gonggong_leixing.jieshao if m.gonggong_leixing else '', + 'jieshao': m.jieshao, + 'tupian_url': m.tupian_url or '', + 'paixu': m.paixu, + 'shangjia_zhuangtai': m.shangjia_zhuangtai, + 'fengjin_zhuangtai': m.fengjin_zhuangtai, + 'shenhe_zhuangtai': m.shenhe_zhuangtai, + }) + + return Response({ + 'code': 0, + 'data': { + 'list': mapping_list, + 'total': paginator.count, + 'pending_total': pending_total + } + }) + + +# ==================== 统一修改接口(已修正事务) ==================== +class ShopProductModifyView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + + user_perms = set(permissions) + action = request.data.get('action') + + if action == 'update_audit_mode': + return self._update_audit_mode(request, user_perms) + elif action == 'update_mapping_status': + return self._update_mapping_status(request, user_perms) + else: + return Response({'code': 400, 'msg': '未知操作'}) + + @transaction.atomic # ✅ 事务修复点 + def _update_audit_mode(self, request, user_perms): + if '1199ab' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改审核模式'}) + + kaiqi = request.data.get('kaiqi_shenhe') + if kaiqi is None: + return Response({'code': 400, 'msg': '缺少参数 kaiqi_shenhe'}) + + try: + config, _ = DianpuShangpinShenheShezhi.objects.select_for_update().get_or_create(id=1) + config.kaiqi_shenhe = bool(kaiqi) + config.save(update_fields=['kaiqi_shenhe']) + return Response({'code': 0, 'msg': '审核模式已更新'}) + except Exception as e: + logger.exception("更新审核模式失败") + return Response({'code': 500, 'msg': '服务器错误'}) + + @transaction.atomic + def _update_mapping_status(self, request, user_perms): + mapping_id = request.data.get('mapping_id') + if not mapping_id: + return Response({'code': 400, 'msg': '缺少 mapping_id'}) + + try: + mapping = ShangpinLeixingDianpu.objects.select_for_update().get(id=int(mapping_id)) + except (ShangpinLeixingDianpu.DoesNotExist, ValueError): + return Response({'code': 404, 'msg': '映射不存在'}) + + fengjin = request.data.get('fengjin_zhuangtai') + shangjia = request.data.get('shangjia_zhuangtai') + shenhe = request.data.get('shenhe_zhuangtai') + + # 权限判断 + if fengjin is not None: + if fengjin: + if '1199abc' not in user_perms: + return Response({'code': 403, 'msg': '无权限封禁'}) + else: + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限解封'}) + mapping.fengjin_zhuangtai = bool(fengjin) + + if shangjia is not None: + if not shangjia: + if '1199abc' not in user_perms: + return Response({'code': 403, 'msg': '无权限下架'}) + else: + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限上架'}) + mapping.shangjia_zhuangtai = bool(shangjia) + + if shenhe is not None: + if shenhe: + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限审核通过'}) + mapping.shenhe_zhuangtai = bool(shenhe) + + mapping.save() + return Response({'code': 0, 'msg': '修改成功'}) + + + + + + +# 商品管理相关权限组(拥有任意一个即可查看列表) +PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd', '1199abe'] + + +class ProductListView(APIView): + """ + 商品列表接口,支持多条件筛选与分页。 + 请求路径: /houtai/hqhtdpsplx + 权限要求: 1199ab / 1199abc / 1199abd / 1199abe 任一即可 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ----- 身份与权限验证 ----- + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + + if not set(permissions).intersection(PRODUCT_PERMS): + return Response({'code': 403, 'msg': '无商品管理权限'}) + + # ----- 安全解析分页与筛选参数 ----- + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 15)) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '分页参数格式错误'}) + + keyword = str(request.data.get('keyword', '')).strip() + shop_id = request.data.get('shop_id') # 店铺ID (可选) + shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型映射ID (可选) + public_type_id = request.data.get('public_type_id') # 公共商品类型ID (可选) + + # 布尔型状态筛选 + def parse_bool(val): + if val is None: + return None + return bool(val) + + shenhe = parse_bool(request.data.get('shenhe_zhuangtai')) + shangjia = parse_bool(request.data.get('shangjia_zhuangtai')) + fengjin = parse_bool(request.data.get('fengjin_zhuangtai')) + + # ----- 构建查询(预加载店铺和店铺商品类型) ----- + products = Shangpin.objects.select_related('dianpu', 'dianpu_leixing').all() + + # 按店铺筛选 + if shop_id is not None: + try: + products = products.filter(dianpu_id=int(shop_id)) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': 'shop_id 格式错误'}) + + # 按店铺商品类型映射筛选 + if shop_product_type_id is not None: + try: + products = products.filter(dianpu_leixing_id=int(shop_product_type_id)) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'}) + + # 按公共商品类型筛选(leixing_id 存储的是公共类型的ID) + if public_type_id is not None: + try: + products = products.filter(leixing_id=int(public_type_id)) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': 'public_type_id 格式错误'}) + + # 平台审核状态筛选 (BooleanField) + if shenhe is not None: + products = products.filter(shenhe_zhuangtai=shenhe) + + # 上架状态筛选 + if shangjia is not None: + products = products.filter(shangjia_zhuangtai=shangjia) + + # 封禁状态筛选 + if fengjin is not None: + products = products.filter(fengjin_zhuangtai=fengjin) + + # 关键词搜索:商品ID或商品标题(搜索具有模糊性,使用icontains) + if keyword: + # 尝试纯数字可能是商品ID,使用精确匹配与标题模糊搜索 + products = products.filter( + models.Q(id__icontains=keyword) | + models.Q(biaoqian__icontains=keyword) + ) + + # ----- 获取待审核总数(平台审核未通过的数量) ----- + pending_total = products.filter(shenhe_zhuangtai=False).count() + + # ----- 分页 ----- + paginator = Paginator(products, page_size) + page_obj = paginator.get_page(page) + + # ----- 构造返回数据(批量获取公共类型名称,避免N+1) ----- + # 提取所有公共商品类型ID + type_ids = {p.leixing_id for p in page_obj if p.leixing_id is not None} + public_type_map = {} + if type_ids: + public_types = ShangpinLeixing.objects.filter(id__in=type_ids).values('id', 'jieshao') + public_type_map = {t['id']: t['jieshao'] for t in public_types} + + product_list = [] + for p in page_obj: + product_list.append({ + 'id': p.id, + 'biaoqian': p.biaoqian or '', + 'jiage': p.jiage, + 'kucun': p.kucun, + 'leixing_id': p.leixing_id, + 'public_type_name': public_type_map.get(p.leixing_id, ''), + 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, + 'duiwai_xiaoliang': p.duiwai_xiaoliang, + 'jieshao': p.jieshao or '', + 'xiadan_xuzhi': p.xiadan_xuzhi or '', + 'guize_tupian': p.guize_tupian or '', + 'yaoqiuleixing': p.yaoqiuleixing, + 'huiyuan_id': p.huiyuan_id or '', + 'yongjin': p.yongjin, + 'kaioi_ewai_dashou_fencheng': p.kaioi_ewai_dashou_fencheng, + 'ewai_dashou_fencheng': p.ewai_dashou_fencheng, + 'create_time': p.create_time.isoformat() if p.create_time else '', + 'update_time': p.update_time.isoformat() if p.update_time else '', + 'paixu': p.paixu, + 'dianpu_id': p.dianpu_id, + 'dianpu_name': p.dianpu.dianpu_mingcheng if p.dianpu else '', + 'dianpu_leixing_id': p.dianpu_leixing_id, + 'dianpu_leixing_name': p.dianpu_leixing.jieshao if p.dianpu_leixing else '', + 'shangjia_zhuangtai': p.shangjia_zhuangtai, + 'fengjin_zhuangtai': p.fengjin_zhuangtai, + 'shenhe_zhuangtai': p.shenhe_zhuangtai, + }) + + return Response({ + 'code': 0, + 'data': { + 'list': product_list, + 'total': paginator.count, + 'pending_total': pending_total, # 待审核商品总数 + } + }) + + +class ProductModifyView(APIView): + """ + 商品修改接口,根据修改内容细粒度鉴权。 + 请求路径: /houtai/htxgdpspsj + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + + user_perms = set(permissions) + action = request.data.get('action', '') + + if action != 'update_product': + return Response({'code': 400, 'msg': '未知操作'}) + + return self._update_product(request, user_perms) + + @transaction.atomic + def _update_product(self, request, user_perms): + """执行商品修改,严格校验权限""" + product_id = request.data.get('product_id') + if not product_id: + return Response({'code': 400, 'msg': '缺少商品ID'}) + + try: + # 行级锁防止并发修改 + product = Shangpin.objects.select_for_update().get(id=int(product_id)) + except (Shangpin.DoesNotExist, ValueError): + return Response({'code': 404, 'msg': '商品不存在'}) + + # 需要修改的字段 + fields_to_update = {} + # 将要修改的状态标志用于后续权限判断 + status_changes = { + 'shangjia': request.data.get('shangjia_zhuangtai'), + 'fengjin': request.data.get('fengjin_zhuangtai'), + 'shenhe': request.data.get('shenhe_zhuangtai'), + } + + # ---------- 权限分类检查 ---------- + # 1. 封禁/下架操作 (消极操作) -> 1199abc + if any(status_changes.get(k) is not None and status_changes[k] == False for k in ['shangjia', 'shenhe']): + if '1199abc' not in user_perms: + return Response({'code': 403, 'msg': '无权限执行下架/封禁/驳回审核操作(1199abc)'}) + if status_changes.get('fengjin') is not None and status_changes['fengjin'] == True: + if '1199abc' not in user_perms: + return Response({'code': 403, 'msg': '无权限执行封禁操作(1199abc)'}) + + # 2. 解封/上架/过审 (积极操作) -> 1199abd + if any(status_changes.get(k) is not None and status_changes[k] == True for k in ['shangjia', 'shenhe']): + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限执行上架/解封/审核通过操作(1199abd)'}) + if status_changes.get('fengjin') is not None and status_changes['fengjin'] == False: + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限执行解封操作(1199abd)'}) + + # 3. 修改其他数据(非状态字段)-> 1199abe + # 除了上架、封禁、审核之外的字段都属于“其他数据” + other_fields = [k for k in request.data.keys() if k not in ['action', 'product_id', 'username', + 'shangjia_zhuangtai', 'fengjin_zhuangtai', + 'shenhe_zhuangtai']] + if other_fields and '1199abe' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改商品基本信息(1199abe)'}) + + # ---------- 应用修改 ---------- + # 允许修改的字段白名单(防止注入) + allowed_fields = [ + 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'duiwai_xiaoliang', + 'jieshao', 'xiadan_xuzhi', 'yaoqiuleixing', 'yongjin', + 'kaioi_ewai_dashou_fencheng', 'ewai_dashou_fencheng', + 'shangjia_zhuangtai', 'fengjin_zhuangtai', 'shenhe_zhuangtai' + ] + + for field in allowed_fields: + value = request.data.get(field) + if value is not None: + # 简单类型校验(根据字段类型) + if field in ['jiage', 'kucun', 'duiwai_xiaoliang', 'yongjin', 'ewai_dashou_fencheng', 'paixu']: + try: + value = float(value) if 'jiage' in field or 'yongjin' in field or 'ewai' in field else int(value) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': f'字段 {field} 值格式错误'}) + setattr(product, field, value) + + product.save(update_fields=allowed_fields) + return Response({'code': 0, 'msg': '商品修改成功'}) + + + + +# 与前端约定的聊天权限码 +CHAT_PERM_CODES = ['abca1', 'baac2', 'cabc3', 'cb3a2', 'bcaa4'] + +class KefuChatPermissionsView(APIView): + """ + 获取客服聊天权限及GoEasy配置 + POST /houtai/kfhqltqx + Body: { "phone": "客服手机号" } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 从前端获取传递的账号,用于防越权校验 + username_from_frontend = request.data.get('phone', None) + + # 2. 调用公共验证方法。 + # 返回 (kefu_obj, permissions) 或 (None, Response) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + + # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值(错误Response) + if kefu_obj is None: + return permissions + + # ---------- 验证通过,permissions 已经是权限码列表 ---------- + # 4. 检查是否拥有至少一个聊天权限 + has_chat_perm = any(p in CHAT_PERM_CODES for p in permissions) + + # 5. 若有聊天权限,查询我方俱乐部的 chat_api_key + goeasy_appkey = '' + if has_chat_perm: + config = ClubConfig.objects.filter(is_self=1).first() + if config: + goeasy_appkey = config.chat_api_key or '' + + # 6. 返回数据 + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'permissions': permissions, + 'goeasy_appkey': goeasy_appkey + } + }) + + + + + +class FineApplyView(APIView): + """ + 客服申请罚款接口(金额罚款,非积分) + 路径:POST /houtai/kffkdssq + 参数: + username: 客服手机号 + dingdan_id: 订单ID + chufaliyou: 罚款原因 + fakuanjine: 罚款金额(元) + yingxiang_qiangdan: 1影响抢单 / 0不影响抢单 + shenqingren_shenfen: 操作人身份 (1客服/2售后/3管理员等) + 权限:JWT + 权限码99933abs + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 提取参数 + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + chufaliyou = request.data.get('chufaliyou', '').strip() + fakuanjine = request.data.get('fakuanjine', 0) + yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) + shenqingren_shenfen = request.data.get('shenqingren_shenfen', 2) + + if not all([username, dingdan_id, chufaliyou, fakuanjine]): + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + # 2. 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '99933abs' not in permissions: + return Response({'code': 403, 'msg': '您无罚款权限'}) + + # 3. 获取订单(无需 select_for_update,因不修改订单) + try: + order = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}) + + dashou_id = order.jiedan_dashou_id + if not dashou_id: + return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'}) + + # 4. 重复罚单检查 + if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists(): + return Response({'code': 400, 'msg': '该打手已被罚款过'}) + + # 5. 跨平台相关字段 + partner_order_id = order.partner_order_id or '' + partner_club_id = order.partner_club_id or '' + dispatch_type = order.dispatch_type or 0 + + # 本地创建罚单的可复用函数 + def create_local_fadan(): + Fadan.objects.create( + beichufa_id=dashou_id, + shenqing_chufa=username, + shenfen=1, # 1 代表打手 + chufaliyou=chufaliyou, + fakuanjine=fakuanjine, + guanliandingdan_id=dingdan_id, + zhuangtai=1, # 待缴纳 + yingxiang_qiangdan=yingxiang_qiangdan, + shenqingren_shenfen=shenqingren_shenfen, + partner_club_id='', + ) + + # 6. 判断:没有对方订单ID,或对方派单 → 本地处罚 + if not partner_order_id or dispatch_type == 2: + with transaction.atomic(): + create_local_fadan() + logger.info(f"本地罚款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {fakuanjine}") + return Response({'code': 0, 'msg': '罚款已生成'}) + + # 7. 我方派单且有对方订单ID → 通知对方平台 + # 获取我方俱乐部ID + try: + my_club = ClubConfig.objects.filter(is_self=1).first() + if not my_club: + return Response({'code': 500, 'msg': '系统配置错误'}) + self_club_id = my_club.club_id + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain + except ObjectDoesNotExist: + return Response({'code': 500, 'msg': '对方平台信息未配置'}) + + url = partner_domain.rstrip('/') + '/houtai/kffkdssq_notify' + payload = { + 'order_id': partner_order_id, + 'club_id': self_club_id, + 'chufaliyou': chufaliyou, + 'fakuanjine': fakuanjine, + 'yingxiang_qiangdan': yingxiang_qiangdan, + 'shenqingren_shenfen': shenqingren_shenfen, + 'operator': username, + } + timeout = 3 + retries = 2 + success = False + for attempt in range(retries): + try: + resp = requests.post(url, json=payload, timeout=timeout) + if resp.status_code == 200 and resp.json().get('code') == 0: + success = True + break + except Exception: + continue + + if not success: + return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'}) + + # 通知成功后,本地也记录一笔(标记为跨平台) + with transaction.atomic(): + Fadan.objects.create( + beichufa_id=dashou_id, + shenqing_chufa=username, + shenfen=1, + chufaliyou=chufaliyou, + fakuanjine=fakuanjine, + guanliandingdan_id=dingdan_id, + zhuangtai=1, + yingxiang_qiangdan=yingxiang_qiangdan, + shenqingren_shenfen=shenqingren_shenfen, + partner_club_id=partner_club_id, + ) + logger.info(f"跨平台罚款通知成功: {dingdan_id}") + return Response({'code': 0, 'msg': '罚款已提交至对方平台'}) + + except Exception as e: + logger.error(f"罚款申请异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + +class PartnerFineNotifyView(APIView): + """ + 接收对方平台罚款通知(无认证) + 路径:POST /houtai/kffkdssq_notify + 参数: + order_id: 对方平台的订单ID(需通过这个找到我方订单) + club_id: 对方俱乐部ID + chufaliyou: 罚款原因 + fakuanjine: 罚款金额 + yingxiang_qiangdan: 1影响抢单 / 0不影响 + shenqingren_shenfen: 申请处罚人身份 + operator: 操作人标识 + """ + authentication_classes = [] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + # token = request.data.get('token') + # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): + # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200) + try: + order_id = request.data.get('order_id', '').strip() # 对方平台订单ID + club_id = request.data.get('club_id', '').strip() + chufaliyou = request.data.get('chufaliyou', '').strip() + fakuanjine = request.data.get('fakuanjine', 0) + yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) + shenqingren_shenfen = request.data.get('shenqingren_shenfen', 2) + operator = request.data.get('operator', '').strip() + + if not all([order_id, chufaliyou]): + return Response({'code': 400, 'msg': '参数不完整'}) + + # 通过对方订单ID查找我方订单 + try: + cross = CrossPlatformOrderData.objects.get(partner_order_id=order_id) + my_dingdan_id = cross.dingdan_id + order = Dingdan.objects.get(dingdan_id=my_dingdan_id) + dashou_id = order.jiedan_dashou_id + except CrossPlatformOrderData.DoesNotExist: + return Response({'code': 404, 'msg': '未找到关联订单'}) + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '我方订单不存在'}) + + if not dashou_id: + return Response({'code': 400, 'msg': '订单无接单打手'}) + + # 重复检查 + if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=my_dingdan_id).exists(): + return Response({'code': 0, 'msg': '已存在罚单'}) + + # 创建罚单 + Fadan.objects.create( + beichufa_id=dashou_id, + shenqing_chufa=operator, + shenfen=1, + chufaliyou=chufaliyou, + fakuanjine=fakuanjine, + guanliandingdan_id=my_dingdan_id, + zhuangtai=1, + yingxiang_qiangdan=yingxiang_qiangdan, + shenqingren_shenfen=shenqingren_shenfen, + partner_club_id=club_id, + ) + logger.info(f"接收对方罚款成功: 订单 {my_dingdan_id}, 打手 {dashou_id}") + return Response({'code': 0, 'msg': 'success'}) + + except Exception as e: + logger.error(f"接收罚款通知异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + + + + +# 权限码到身份的映射 +PERMISSION_TO_SHENFEN = { + '66693a': 1, # 打手 + '66693b': 3, # 商家 + '66693c': 2, # 管事 + '66694c': 4, # 组长 +} + +# 身份对应的用户扩展关系名 +SHENFEN_PROFILE_MAP = { + 1: 'dashou_profile', + 2: 'guanshi_profile', + 3: 'shop_profile', + 4: 'zuzhang_profile', +} + + +def check_fadan_permission(permissions, shenfen): + """检查是否有处理指定身份罚单的权限""" + perm_code = {1: '66693a', 2: '66693c', 3: '66693b', 4: '66694c'}.get(shenfen) + return perm_code in permissions + + +class FaKuanTongJiView(APIView): + """ + 罚款统计接口 + POST /houtai/htglyhqcfsltj + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 至少有一个罚款管理权限 + if not any(p in permissions for p in PERMISSION_TO_SHENFEN.keys()): + return Response({'code': 403, 'msg': '无权限查看罚款统计'}) + + stats = Fadan.objects.aggregate( + total=Count('id'), + daijiaona=Count('id', filter=Q(zhuangtai=1)), + shensuzhong=Count('id', filter=Q(zhuangtai=3)), + yijiaona=Count('id', filter=Q(zhuangtai=2)), + yibohui=Count('id', filter=Q(zhuangtai=4)), + ) + data = {k: v or 0 for k, v in stats.items()} + return Response({'code': 0, 'msg': '成功', 'data': data}) + except Exception as e: + logger.error(f"罚款统计异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + +class FaKuanLieBiaoView(APIView): + """ + 罚款列表接口 + POST /houtai/hthqfklb + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if not any(p in permissions for p in PERMISSION_TO_SHENFEN.keys()): + return Response({'code': 403, 'msg': '无权限查看罚款列表'}) + + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + if page_size > 100: + page_size = 100 + + qs = Fadan.objects.all() + + # 状态筛选 + zhuangtai = request.data.get('zhuangtai') + if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]: + qs = qs.filter(zhuangtai=int(zhuangtai)) + + # 被处罚人ID + beichufa_id = request.data.get('beichufa_id', '').strip() + if beichufa_id: + qs = qs.filter(beichufa_id=beichufa_id) + + # 被处罚人身份 + beichufa_shenfen = request.data.get('beichufa_shenfen') + if beichufa_shenfen is not None and int(beichufa_shenfen) in [1, 2, 3, 4]: + qs = qs.filter(shenfen=int(beichufa_shenfen)) + + # 模糊搜索(订单ID、被罚人ID、申请人ID) + sousuo = request.data.get('sousuo', '').strip() + if sousuo: + qs = qs.filter( + Q(guanliandingdan_id__icontains=sousuo) | + Q(beichufa_id__icontains=sousuo) | + Q(shenqing_chufa__icontains=sousuo) + ) + + qs = qs.order_by('-create_time') + paginator = Paginator(qs, page_size) + try: + page_obj = paginator.page(page) + except Exception: + return Response({ + 'code': 0, 'msg': '成功', + 'data': {'list': [], 'total': 0, 'page': page, 'page_size': page_size} + }) + + records = list(page_obj) + fadan_ids = [r.id for r in records] + + # 批量获取图片并分组 + zhengju_map = {} + shensu_map = {} + if fadan_ids: + tupians = FadanShensuTupian.objects.filter( + fadan_id__in=fadan_ids + ).values('fadan_id', 'tupian_url', 'yongtu') + for t in tupians: + fid = t['fadan_id'] + url = t['tupian_url'] + if t['yongtu'] == 1: + zhengju_map.setdefault(fid, []).append(url) + else: + shensu_map.setdefault(fid, []).append(url) + + # 组装列表数据 + results = [] + for r in records: + results.append({ + 'id': r.id, + 'beichufa_id': r.beichufa_id, + 'shenfen': r.shenfen, + 'shenfen_text': dict(Fadan._meta.get_field('shenfen').choices).get(r.shenfen, ''), + 'shenqing_chufa': r.shenqing_chufa or '', + 'shenqingren_shenfen': r.shenqingren_shenfen, + 'chufaliyou': r.chufaliyou or '', + 'fakuanjine': str(r.fakuanjine), + 'guanliandingdan_id': r.guanliandingdan_id or '', + 'zhuangtai': r.zhuangtai, + 'yingxiang_qiangdan': r.yingxiang_qiangdan, + 'shensuliyou': r.shensuliyou or '', + 'bohuiliyou': r.bohuiliyou or '', + 'chulizhe': r.chulizhe or '', + 'chulizhe_shenfen': r.chulizhe_shenfen, + 'zhengju_tupian': zhengju_map.get(r.id, []), + 'shensu_tupian': shensu_map.get(r.id, []), + 'create_time': r.create_time.strftime('%Y-%m-%d %H:%M:%S') if r.create_time else '', + 'update_time': r.update_time.strftime('%Y-%m-%d %H:%M:%S') if r.update_time else '', + }) + + return Response({ + 'code': 0, 'msg': '成功', + 'data': { + 'list': results, + 'total': paginator.count, + 'page': page, + 'page_size': page_size, + } + }) + except Exception as e: + logger.error(f"罚款列表异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + +class FaKuanChuLiView(APIView): + """ + 处理罚款申诉(同意/拒绝) + POST /houtai/glyclfk + """ + permission_classes = [IsAuthenticated] + parser_classes = [MultiPartParser, FormParser, JSONParser] + + def post(self, request): + try: + username = request.data.get('username', '').strip() + fadan_id = request.data.get('fadan_id') + action = request.data.get('action', '').strip() + chuli_liyou = request.data.get('chuli_liyou', '').strip() + + if not all([username, fadan_id, action]): + return Response({'code': 400, 'msg': '参数不完整'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + with transaction.atomic(): + try: + fadan = Fadan.objects.select_for_update().get(id=fadan_id) + except Fadan.DoesNotExist: + return Response({'code': 404, 'msg': '罚单不存在'}) + + if fadan.zhuangtai != 3: + return Response({'code': 400, 'msg': '当前状态不可处理'}) + + if not check_fadan_permission(permissions, fadan.shenfen): + return Response({'code': 403, 'msg': '无权限处理该类罚单'}) + + # 处理上传的图片 + uploaded_urls = [] + files = request.FILES.getlist('tupian') or [] + if not files: + single = request.FILES.get('tupian') + if single: + files = [single] + for file in files: + valid, msg = validate_image(file) + if not valid: + return Response({'code': 400, 'msg': msg}) + ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' + file_name = f"chufa_{fadan_id}_{int(time.time() * 1000)}.{ext}" + file_path = f"chufatupian/kechufatupian/{file_name}" + url = upload_to_oss(file, file_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + uploaded_urls.append(file_path) + + # 更新状态 + if action == 'agree': # 同意申诉 → 驳回处罚 + fadan.zhuangtai = 4 + fadan.bohuiliyou = chuli_liyou + elif action == 'reject': # 拒绝申诉 → 回到待缴纳 + fadan.zhuangtai = 1 + fadan.bohuiliyou = chuli_liyou + else: + return Response({'code': 400, 'msg': '无效的操作类型'}) + + # 记录处理者(使用 request.user 的 phone) + fadan.chulizhe = request.user.phone + fadan.chulizhe_shenfen = 2 # 这里假设处理者是售后身份,可根据实际业务获取 + fadan.save() + + # 保存图片(证据图片,用途1) + for relative_url in uploaded_urls: + FadanShensuTupian.objects.create( + fadan_id=fadan_id, + beichufa_id=fadan.beichufa_id, + tupian_url=relative_url, + yongtu=1 + ) + + logger.info(f"罚款处理成功: {fadan_id}, 操作: {action}") + return Response({'code': 0, 'msg': '处理成功'}) + + except Exception as e: + logger.error(f"罚款处理异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + +class FaKuanChuangJianView(APIView): + """ + 管理员创建罚款 + POST /houtai/htfksc + """ + permission_classes = [IsAuthenticated] + parser_classes = [MultiPartParser, FormParser, JSONParser] + + def post(self, request): + try: + username = request.data.get('username', '').strip() + beichufa_id = request.data.get('beichufa_id', '').strip() + shenfen = request.data.get('shenfen') + chufaliyou = request.data.get('chufaliyou', '').strip() + fakuanjine = request.data.get('fakuanjine') + yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) + + if not all([username, beichufa_id, shenfen, chufaliyou, fakuanjine]): + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + shenfen = int(shenfen) + fakuanjine = float(fakuanjine) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': '参数格式错误'}) + + if shenfen not in [1, 2, 3, 4]: + return Response({'code': 400, 'msg': '无效的被罚人身份'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + if not check_fadan_permission(permissions, shenfen): + return Response({'code': 403, 'msg': '无权限创建该类罚单'}) + + # 验证用户是否存在对应的扩展表 + try: + user = UserMain.objects.get(yonghuid=beichufa_id) + profile_attr = SHENFEN_PROFILE_MAP.get(shenfen) + if not profile_attr or not hasattr(user, profile_attr): + return Response({'code': 400, 'msg': '该用户不是所选身份'}) + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + + # 处理图片上传 + uploaded_urls = [] + files = request.FILES.getlist('tupian') or [] + if not files: + single = request.FILES.get('tupian') + if single: + files = [single] + for file in files: + valid, msg = validate_image(file) + if not valid: + return Response({'code': 400, 'msg': msg}) + ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' + file_name = f"chufa_{int(time.time() * 1000)}.{ext}" + file_path = f"chufatupian/kechufatupian/{file_name}" + url = upload_to_oss(file, file_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + uploaded_urls.append(file_path) + + with transaction.atomic(): + fadan = Fadan.objects.create( + beichufa_id=beichufa_id, + shenfen=shenfen, + shenqing_chufa=request.user.phone, # 申请人(客服)手机号 + shenqingren_shenfen=2, # 售后身份 + chufaliyou=chufaliyou, + fakuanjine=fakuanjine, + yingxiang_qiangdan=yingxiang_qiangdan, + zhuangtai=1, # 待缴纳 + ) + for relative_url in uploaded_urls: + FadanShensuTupian.objects.create( + fadan=fadan, + beichufa_id=beichufa_id, + tupian_url=relative_url, + yongtu=1 + ) + + logger.info(f"罚款创建成功: {fadan.id}") + return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': fadan.id}}) + + except Exception as e: + logger.error(f"创建罚款异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + + + + + + + + +class HqbkxxView(APIView): + """ + 获取板块配置所需全部数据 + POST /houtai/hqbkxx + Body: { "phone": "管理员手机号" } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 从前端获取传递的账号,用于防越权校验 + username_from_frontend = request.data.get('phone', None) + + # 2. 调用公共验证方法 + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + + # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值 + if kefu_obj is None: + return permissions + + # ---------- 验证通过 ---------- + # 4. 检查是否有板块管理权限 + if 'bankuai' not in permissions: + return Response({'code': 403, 'msg': '无板块管理权限'}, status=403) + + # 5. 查询所有板块 + bankuai_list = [] + for bk in Bankuai.objects.all(): + bankuai_list.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'create_time': bk.create_time.strftime('%Y-%m-%d %H:%M:%S') if bk.create_time else '' + }) + + # 6. 查询所有商品类型 + shangpin_leixing_list = [] + for sp in ShangpinLeixing.objects.all(): + shangpin_leixing_list.append({ + 'id': sp.id, + 'jieshao': sp.jieshao, + 'tupian_url': sp.tupian_url or '', + 'bankuai_id': sp.bankuai_id + }) + + # 7. 查询所有会员 + huiyuan_list = [] + for hy in Huiyuan.objects.all(): + huiyuan_list.append({ + 'huiyuan_id': hy.huiyuan_id, + 'jieshao': hy.jieshao, + 'bankuai_id': hy.bankuai_id + }) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'bankuai': bankuai_list, + 'shangpin_leixing': shangpin_leixing_list, + 'huiyuan': huiyuan_list + } + }) + + +class BkxgView(APIView): + """ + 板块增删改统一接口 + POST /houtai/bkxg + Body: { + "phone": "管理员手机号", + "action": "create | update | delete | add_items | remove_items", + "bankuai_id": int, + "bankuai_name": str, + "item_type": "shangpin_leixing | huiyuan", + "item_ids": [1,2,3] + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 从前端获取传递的账号 + username_from_frontend = request.data.get('phone', None) + + # 2. 调用公共验证方法 + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + + # 3. 验证失败直接返回 + if kefu_obj is None: + return permissions + + # 4. 检查板块管理权限 + if 'bankuai' not in permissions: + return Response({'code': 403, 'msg': '无板块管理权限'}, status=403) + + # 5. 获取操作类型 + action = request.data.get('action') + if not action: + return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400) + + # ==================== 创建板块 ==================== + if action == 'create': + bankuai_name = request.data.get('bankuai_name', '').strip() + if not bankuai_name: + return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400) + if Bankuai.objects.filter(mingcheng=bankuai_name).exists(): + return Response({'code': 1, 'msg': '板块名称已存在'}, status=400) + + shangpin_ids = request.data.get('shangpin_ids', []) + huiyuan_ids = request.data.get('huiyuan_ids', []) + + with transaction.atomic(): + new_bk = Bankuai.objects.create(mingcheng=bankuai_name) + if shangpin_ids: + ShangpinLeixing.objects.filter( + id__in=shangpin_ids, + bankuai__isnull=True + ).update(bankuai=new_bk) + if huiyuan_ids: + Huiyuan.objects.filter( + huiyuan_id__in=huiyuan_ids, + bankuai__isnull=True + ).update(bankuai=new_bk) + + return Response({ + 'code': 0, + 'msg': '创建成功', + 'data': {'bankuai_id': new_bk.bankuai_id} + }) + + # ==================== 以下操作需要 bankuai_id ==================== + bankuai_id = request.data.get('bankuai_id') + if not bankuai_id: + return Response({'code': 1, 'msg': '缺少 bankuai_id'}, status=400) + + try: + bk = Bankuai.objects.get(bankuai_id=bankuai_id) + except Bankuai.DoesNotExist: + return Response({'code': 1, 'msg': '板块不存在'}, status=404) + + # ==================== 修改板块名称 ==================== + if action == 'update': + new_name = request.data.get('bankuai_name', '').strip() + if not new_name: + return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400) + if Bankuai.objects.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists(): + return Response({'code': 1, 'msg': '板块名称已存在'}, status=400) + bk.mingcheng = new_name + bk.save() + return Response({'code': 0, 'msg': '修改成功'}) + + # ==================== 删除板块 ==================== + elif action == 'delete': + with transaction.atomic(): + ShangpinLeixing.objects.filter(bankuai=bk).update(bankuai=None) + Huiyuan.objects.filter(bankuai=bk).update(bankuai=None) + bk.delete() + return Response({'code': 0, 'msg': '删除成功'}) + + # ==================== 添加绑定 ==================== + elif action == 'add_items': + item_type = request.data.get('item_type') + item_ids = request.data.get('item_ids', []) + if not item_type or not item_ids: + return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400) + + if item_type == 'shangpin_leixing': + ShangpinLeixing.objects.filter( + id__in=item_ids, + bankuai__isnull=True + ).update(bankuai=bk) + elif item_type == 'huiyuan': + Huiyuan.objects.filter( + huiyuan_id__in=item_ids, + bankuai__isnull=True + ).update(bankuai=bk) + else: + return Response({'code': 1, 'msg': '无效的 item_type'}, status=400) + return Response({'code': 0, 'msg': '添加成功'}) + + # ==================== 移除绑定 ==================== + elif action == 'remove_items': + item_type = request.data.get('item_type') + item_ids = request.data.get('item_ids', []) + if not item_type or not item_ids: + return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400) + + if item_type == 'shangpin_leixing': + ShangpinLeixing.objects.filter( + id__in=item_ids, + bankuai=bk + ).update(bankuai=None) + elif item_type == 'huiyuan': + Huiyuan.objects.filter( + huiyuan_id__in=item_ids, + bankuai=bk + ).update(bankuai=None) + else: + return Response({'code': 1, 'msg': '无效的 item_type'}, status=400) + return Response({'code': 0, 'msg': '移除成功'}) + + else: + return Response({'code': 1, 'msg': '未知操作'}, status=400) + + + + + + + + + +class CaiwuView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + + if kefu_obj is None: + return permissions + + if 'caiwu' not in permissions: + return Response({'code': 403, 'msg': '无财务查看权限'}, status=403) + + today = date.today() + today_start = datetime.combine(today, datetime.min.time()) + today_end = today_start + timedelta(days=1) + + # 今日收入 + today_income = DailyIncomeStat.objects.filter(date=today).first() + today_income_amount = float(today_income.total_amount) if today_income else 0.00 + today_income_count = today_income.total_count if today_income else 0 + + # 今日支出 + today_payout = DailyPayoutStat.objects.filter(date=today).first() + today_payout_amount = float(today_payout.total_amount) if today_payout else 0.00 + today_payout_count = today_payout.total_count if today_payout else 0 + + # 今日订单(已付款及之后的状态) + today_orders = Dingdan.objects.filter( + create_time__gte=today_start, + create_time__lt=today_end, + zhuangtai__in=[1,2,3,4,5,6,7,8] + ) + today_order_count = today_orders.count() + today_order_amount = float(today_orders.aggregate(total=Sum('jine'))['total'] or 0.00) + + # 今日成交(zhuangtai=3) + today_completed = Dingdan.objects.filter( + create_time__gte=today_start, + create_time__lt=today_end, + zhuangtai=3 + ) + today_completed_count = today_completed.count() + today_completed_amount = float(today_completed.aggregate(total=Sum('jine'))['total'] or 0.00) + + # 今日退款(zhuangtai=5) + today_tuikuan = Dingdan.objects.filter( + create_time__gte=today_start, + create_time__lt=today_end, + zhuangtai=5 + ) + today_tuikuan_count = today_tuikuan.count() + today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('jine'))['total'] or 0.00) + + # 今日充值(会员充值,leixing=1, zhuangtai=3) + today_chongzhi = Czjilu.objects.filter( + create_time__gte=today_start, + create_time__lt=today_end, + leixing=1, + zhuangtai=3 + ) + today_chongzhi_count = today_chongzhi.count() + today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00) + + # 今日新增会员:从购买记录表统计今天创建的用户(去重) + new_huiyuan_user_ids = Huiyuangoumai.objects.filter( + create_time__gte=today_start, + create_time__lt=today_end + ).values_list('yonghu_id', flat=True).distinct() + today_new_huiyuan = len(new_huiyuan_user_ids) + + # 新增会员充值总额(这些用户今天的会员充值金额) + today_new_huiyuan_amount = 0.0 + if new_huiyuan_user_ids: + amount_sum = Czjilu.objects.filter( + create_time__gte=today_start, + create_time__lt=today_end, + yonghuid__in=new_huiyuan_user_ids, + leixing=1, + zhuangtai=3 + ).aggregate(total=Sum('jine')) + today_new_huiyuan_amount = float(amount_sum['total'] or 0.0) + + # 各角色可提现余额总和 + all_guanshi_yue = float(UserGuanshi.objects.aggregate(total=Sum('yue'))['total'] or 0.00) + all_dashou_yue = float(UserDashou.objects.aggregate(total=Sum('yue'))['total'] or 0.00) + all_zuzhang_yue = float(UserZuzhang.objects.aggregate(total=Sum('ketixian_jine'))['total'] or 0.00) + all_shangjia_yue = float(UserShangjia.objects.aggregate(total=Sum('yue'))['total'] or 0.00) + + # 平台总收支及利润 + total_income = float(DailyIncomeStat.objects.aggregate(total=Sum('total_amount'))['total'] or 0.00) + total_payout = float(DailyPayoutStat.objects.aggregate(total=Sum('total_amount'))['total'] or 0.00) + platform_profit = round(total_income - total_payout, 2) + + # 近30天每日收支明细 + daily_stats = [] + income_qs = DailyIncomeStat.objects.order_by('-date')[:30] + payout_dict = { + str(p.date): float(p.total_amount) + for p in DailyPayoutStat.objects.filter(date__gte=today - timedelta(days=30)) + } + for inc in income_qs: + d = str(inc.date) + daily_stats.append({ + 'date': d, + 'income': float(inc.total_amount), + 'income_count': inc.total_count, + 'payout': payout_dict.get(d, 0.00), + 'profit': round(float(inc.total_amount) - payout_dict.get(d, 0.00), 2) + }) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'today_income': round(today_income_amount, 2), + 'today_income_count': today_income_count, + 'today_payout': round(today_payout_amount, 2), + 'today_payout_count': today_payout_count, + 'today_order_count': today_order_count, + 'today_order_amount': round(today_order_amount, 2), + 'today_completed_count': today_completed_count, + 'today_completed_amount': round(today_completed_amount, 2), + 'today_tuikuan_count': today_tuikuan_count, + 'today_tuikuan_amount': round(today_tuikuan_amount, 2), + 'today_chongzhi_count': today_chongzhi_count, + 'today_chongzhi_amount': round(today_chongzhi_amount, 2), + 'today_new_huiyuan': today_new_huiyuan, + 'today_new_huiyuan_amount': round(today_new_huiyuan_amount, 2), + 'all_guanshi_yue': round(all_guanshi_yue, 2), + 'all_dashou_yue': round(all_dashou_yue, 2), + 'all_zuzhang_yue': round(all_zuzhang_yue, 2), + 'all_shangjia_yue': round(all_shangjia_yue, 2), + 'total_income': round(total_income, 2), + 'total_payout': round(total_payout, 2), + 'platform_profit': platform_profit, + 'daily_stats': daily_stats + } + }) + + + + + +class CwhybkhqView(APIView): + """ + 获取所有板块及对应会员列表 + POST /houtai/cwhybkhq + Body: { "phone": "管理员手机号" } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'huiyuancaiwu' not in permissions: + return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403) + + bankuai_list = [] + for bk in Bankuai.objects.all(): + members = Huiyuan.objects.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id') + bankuai_list.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'members': list(members) + }) + + return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list}) + except Exception as e: + logger.exception("CwhybkhqView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + +class HybkjtsjView(APIView): + """ + 会员详细统计数据 + POST /houtai/hybkjtsj + Body: { + "phone": "管理员手机号", + "huiyuan_id": "会员ID", + "granularity": "day" | "month" | "year", + "year": 2026, + "month": 5, + "include_guanshi": true, + "include_zuzhang": true, + "summary_date": "2026-05-16" + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'huiyuancaiwu' not in permissions: + return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403) + + huiyuan_id = request.data.get('huiyuan_id') + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + include_guanshi = request.data.get('include_guanshi', False) + include_zuzhang = request.data.get('include_zuzhang', False) + summary_date = request.data.get('summary_date') + + if not huiyuan_id: + return Response({'code': 1, 'msg': '缺少会员ID'}, status=400) + + # 🔧 直接使用 date.today() 避免模块名冲突 + now = date.today() + + # ---------- 构造时间范围 ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + start_date = date(year, month, 1) + if month == 12: + end_date = date(year + 1, 1, 1) + else: + end_date = date(year, month + 1, 1) + trunc_func = TruncDate('create_time') + if not summary_date: + summary_date = now.strftime('%Y-%m-%d') + summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncMonth('create_time') + if not summary_date: + summary_date = now.strftime('%Y-%m') + summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() + elif granularity == 'year': + year = year or now.year + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncYear('create_time') + if not summary_date: + summary_date = str(year) + summary_q = date(year, 1, 1) + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + # 生成 naive datetime(避免 USE_TZ=False 报错) + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date, datetime.min.time()) + + # ---------- 充值过滤 ---------- + chongzhi_filter = { + 'huiyuan_id': huiyuan_id, # 会员ID筛选 + 'leixing': 1, + 'zhuangtai': 3, + 'create_time__gte': start_dt, + 'create_time__lt': end_dt + } + + # ---------- 分红过滤 ---------- + fenhong_filter = { + 'huiyuan_id': huiyuan_id, # 分红表会员ID筛选 + 'create_time__gte': start_dt, + 'create_time__lt': end_dt + } + + # 充值分组查询 + chongzhi_qs = Czjilu.objects.filter(**chongzhi_filter) \ + .annotate(period=trunc_func) \ + .values('period') \ + .annotate( + chongzhi_count=Count('id'), + chongzhi_amount=Sum('jine') + ).order_by('period') + + # 分红分组查询 + fenhong_qs = None + if include_guanshi or include_zuzhang: + fenhong_agg = {} + if include_guanshi: + fenhong_agg['guanshi_amount'] = Sum('fenhong') + if include_zuzhang: + fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong') + fenhong_qs = Gsfenhong.objects.filter(**fenhong_filter) \ + .annotate(period=trunc_func) \ + .values('period') \ + .annotate(**fenhong_agg) \ + .order_by('period') + + # 合并数据 + data_map = {} + for item in chongzhi_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + data_map[period_str] = { + 'chongzhi_count': item['chongzhi_count'], + 'chongzhi_amount': float(item['chongzhi_amount'] or 0) + } + + if fenhong_qs: + for item in fenhong_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + if period_str not in data_map: + data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0} + if include_guanshi: + data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0) + if include_zuzhang: + data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0) + + time_series = [] + for period_str, vals in data_map.items(): + guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0 + zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0 + vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2) + vals['date'] = period_str + time_series.append(vals) + time_series.sort(key=lambda x: x['date']) + + # ---------- 汇总数据 ---------- + summary_chongzhi_filter = chongzhi_filter.copy() + if granularity == 'day': + summary_chongzhi_filter['create_time__date'] = summary_q + elif granularity == 'month': + summary_chongzhi_filter['create_time__year'] = summary_q.year + summary_chongzhi_filter['create_time__month'] = summary_q.month + else: + summary_chongzhi_filter['create_time__year'] = summary_q.year + + summary_chongzhi = Czjilu.objects.filter(**summary_chongzhi_filter).aggregate( + total_count=Count('id'), + total_amount=Sum('jine') + ) + total_chongzhi_count = summary_chongzhi['total_count'] or 0 + total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0) + + total_guanshi = 0.0 + total_zuzhang = 0.0 + if include_guanshi or include_zuzhang: + summary_fenhong_filter = fenhong_filter.copy() + if granularity == 'day': + summary_fenhong_filter['create_time__date'] = summary_q + elif granularity == 'month': + summary_fenhong_filter['create_time__year'] = summary_q.year + summary_fenhong_filter['create_time__month'] = summary_q.month + else: + summary_fenhong_filter['create_time__year'] = summary_q.year + + fenhong_agg = {} + if include_guanshi: + fenhong_agg['guanshi_sum'] = Sum('fenhong') + if include_zuzhang: + fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong') + summary_fenhong = Gsfenhong.objects.filter(**summary_fenhong_filter).aggregate(**fenhong_agg) + total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0) + total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0) + + total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': { + 'total_chongzhi_count': total_chongzhi_count, + 'total_chongzhi_amount': total_chongzhi_amount, + 'total_guanshi_fenhong': total_guanshi, + 'total_zuzhang_fenhong': total_zuzhang, + 'total_shouyi': total_shouyi + } + } + }) + except Exception as e: + logger.exception("HybkjtsjView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + + +class SzxxView(APIView): + """ + 每日收支详细统计数据 + POST /houtai/szxx + Body: { + "phone": "管理员手机号", + "granularity": "day" | "month" | "year", + "year": 2026, // day/month 时必填 + "month": 5, // day 时必填 + "summary_date": "2026-05-16" // 可选,格式随颗粒度 + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 避免模块名冲突,局部导入 + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'caiwu' not in permissions: + return Response({'code': 403, 'msg': '无财务查看权限'}, status=403) + + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + summary_date = request.data.get('summary_date') + + now = date.today() + + # ---------- 时间范围与分组字段 ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + # 分组字段 + group_field = 'day' + # 过滤条件 + income_filter = {'year': year, 'month': month} + payout_filter = {'year': year, 'month': month} + # 默认汇总日期 + if not summary_date: + summary_date = now.strftime('%Y-%m-%d') + summary_parts = summary_date.split('-') + summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1]), 'day': int(summary_parts[2])} + # 用于构造返回的日期字符串 + date_format = '%Y-%m-%d' + + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + group_field = 'month' + income_filter = {'year': year} + payout_filter = {'year': year} + if not summary_date: + summary_date = now.strftime('%Y-%m') + summary_parts = summary_date.split('-') + summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1])} + date_format = '%Y-%m' + + elif granularity == 'year': + group_field = 'year' + income_filter = {} + payout_filter = {} + if not summary_date: + summary_date = str(now.year) + summary_filter = {'year': int(summary_date)} + date_format = '%Y' + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + # ---------- 收入分组查询 ---------- + income_qs = DailyIncomeStat.objects.filter(**income_filter) \ + .values(group_field) \ + .annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) \ + .order_by(group_field) + + # ---------- 支出分组查询 ---------- + payout_qs = DailyPayoutStat.objects.filter(**payout_filter) \ + .values(group_field) \ + .annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) \ + .order_by(group_field) + + # ---------- 合并数据 ---------- + data_map = {} + for item in income_qs: + key = item[group_field] + data_map[key] = { + 'income': float(item['total_income'] or 0), + 'income_count': item['total_count'] or 0, + 'payout': 0.0, + 'payout_count': 0 + } + + for item in payout_qs: + key = item[group_field] + if key not in data_map: + data_map[key] = {'income': 0.0, 'income_count': 0, 'payout': 0.0, 'payout_count': 0} + data_map[key]['payout'] = float(item['total_payout'] or 0) + data_map[key]['payout_count'] = item['total_count'] or 0 + + time_series = [] + for key, vals in data_map.items(): + # 构造日期字符串 + if granularity == 'day': + date_str = f"{year}-{month:02d}-{key:02d}" + elif granularity == 'month': + date_str = f"{year}-{key:02d}" + else: + date_str = str(key) + profit = round(vals['income'] - vals['payout'], 2) + time_series.append({ + 'date': date_str, + 'income': vals['income'], + 'payout': vals['payout'], + 'profit': profit + }) + time_series.sort(key=lambda x: x['date']) + + # ---------- 汇总数据 ---------- + income_summary = DailyIncomeStat.objects.filter(**summary_filter).aggregate( + total_income=Sum('total_amount'), total_count=Sum('total_count') + ) + payout_summary = DailyPayoutStat.objects.filter(**summary_filter).aggregate( + total_payout=Sum('total_amount'), total_count=Sum('total_count') + ) + total_income = float(income_summary['total_income'] or 0) + total_payout = float(payout_summary['total_payout'] or 0) + total_profit = round(total_income - total_payout, 2) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': { + 'total_income': total_income, + 'total_income_count': income_summary['total_count'] or 0, + 'total_payout': total_payout, + 'total_payout_count': payout_summary['total_count'] or 0, + 'total_profit': total_profit + } + } + }) + + except Exception as e: + logger.exception("SzxxView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + + + + + +class CwddhqlxView(APIView): + """ + 获取订单类型(商品类型)用于筛选 + POST /houtai/cwddhqlx + Body: { "phone": "管理员手机号" } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'dingdancaiwu' not in permissions: + return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403) + + # 获取非审核状态(shenhezhuangtai=1)的商品类型,只返回ID和名称 + types = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values('id', 'jieshao') + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': list(types) + }) + except Exception as e: + logger.exception("CwddhqlxView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + +class CwhqjtddsjView(APIView): + """ + 订单详细统计数据(修正版) + POST /houtai/cwhqjtddsj + Body: { + "phone": "管理员手机号", + "granularity": "day" | "month" | "year", + "year": 2026, + "month": 5, + "type_id": 1, # 商品类型ID,0表示全部 + "order_source": 1, # 1=平台单,2=商家单,0=全部 + "summary_date": "2026-05-16" + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'dingdancaiwu' not in permissions: + return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403) + + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + type_id = request.data.get('type_id', 0) + order_source = request.data.get('order_source', 0) + summary_date = request.data.get('summary_date') + + now = date.today() + + # ---------- 时间范围 ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + start_date = date(year, month, 1) + end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) + trunc_func = TruncDate('create_time') + if not summary_date: + summary_date = now.strftime('%Y-%m-%d') + summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncMonth('create_time') + if not summary_date: + summary_date = now.strftime('%Y-%m') + summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() + elif granularity == 'year': + year = year or now.year + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncYear('create_time') + if not summary_date: + summary_date = str(year) + summary_q = date(year, 1, 1) + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date, datetime.min.time()) + + # ---------- 基础过滤 ---------- + filters = { + 'create_time__gte': start_dt, + 'create_time__lt': end_dt, + 'zhuangtai__in': [1, 2, 3, 4, 5, 6, 7, 8] # 只统计有效订单状态 + } + if int(type_id) != 0: + filters['leixing_id'] = type_id + if int(order_source) in [1, 2]: + filters['fadan_pingtai'] = order_source + + # ---------- 时间序列分组查询 ---------- + qs = Dingdan.objects.filter(**filters) \ + .annotate(period=trunc_func) \ + .values('period') \ + .annotate( + # 订单总量(所有符合条件的状态) + order_count=Count('id'), + # 订单总额 + order_amount=Sum('jine'), + # 成交单量 (zhuangtai=3) + success_count=Count('id', filter=Q(zhuangtai=3)), + # 成交订单总额 + success_amount=Sum('jine', filter=Q(zhuangtai=3)), + # 退款单量 (zhuangtai=5) + refund_count=Count('id', filter=Q(zhuangtai=5)), + # 打手分成总额(仅成交订单) + dashou_fencheng_sum=Sum('dashou_fencheng', filter=Q(zhuangtai=3)), + # 平台成交订单的店铺分红 + dianpu_fenhong_sum=Sum('pingtai_kuozhan__dianpu_shouyi', + filter=Q(zhuangtai=3, fadan_pingtai=1)), + # 成交订单中:平台单金额、商家单金额、各自打手分成 + platform_success_amount=Sum('jine', filter=Q(zhuangtai=3, fadan_pingtai=1)), + merchant_success_amount=Sum('jine', filter=Q(zhuangtai=3, fadan_pingtai=2)), + platform_dashou=Sum('dashou_fencheng', filter=Q(zhuangtai=3, fadan_pingtai=1)), + merchant_dashou=Sum('dashou_fencheng', filter=Q(zhuangtai=3, fadan_pingtai=2)), + ).order_by('period') + + time_series = [] + for item in qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + + # 平台收益计算 + platform_profit = (item['platform_success_amount'] or 0) \ + - (item['platform_dashou'] or 0) \ + - (item['dianpu_fenhong_sum'] or 0) + merchant_profit = (item['merchant_success_amount'] or 0) \ + - (item['merchant_dashou'] or 0) + total_profit = platform_profit + merchant_profit + + time_series.append({ + 'date': period_str, + 'order_count': item['order_count'], + 'order_amount': float(item['order_amount'] or 0), + 'success_count': item['success_count'], + 'success_amount': float(item['success_amount'] or 0), + 'refund_count': item['refund_count'], + 'dashou_fencheng': float(item['dashou_fencheng_sum'] or 0), + 'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0), + 'profit': round(total_profit, 2) + }) + + # ---------- 汇总数据 ---------- + summary_qs = Dingdan.objects.filter(**filters) + total_order_count = summary_qs.count() + total_order_amount = summary_qs.aggregate(s=Sum('jine'))['s'] or 0 + total_success_count = summary_qs.filter(zhuangtai=3).count() + total_success_amount = summary_qs.filter(zhuangtai=3).aggregate(s=Sum('jine'))['s'] or 0 + total_refund_count = summary_qs.filter(zhuangtai=5).count() + total_dashou = summary_qs.filter(zhuangtai=3).aggregate(s=Sum('dashou_fencheng'))['s'] or 0 + total_dianpu_fenhong = summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate( + s=Sum('pingtai_kuozhan__dianpu_shouyi'))['s'] or 0 + + platform_amount = summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(s=Sum('jine'))['s'] or 0 + merchant_amount = summary_qs.filter(zhuangtai=3, fadan_pingtai=2).aggregate(s=Sum('jine'))['s'] or 0 + platform_dashou_sum = \ + summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(s=Sum('dashou_fencheng'))['s'] or 0 + merchant_dashou_sum = \ + summary_qs.filter(zhuangtai=3, fadan_pingtai=2).aggregate(s=Sum('dashou_fencheng'))['s'] or 0 + + profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong + profit_merchant = merchant_amount - merchant_dashou_sum + total_profit = profit_platform + profit_merchant + + summary = { + 'total_order_count': total_order_count, + 'total_order_amount': round(float(total_order_amount), 2), + 'total_success_count': total_success_count, + 'total_success_amount': round(float(total_success_amount), 2), + 'total_refund_count': total_refund_count, + 'total_dashou_fencheng': round(float(total_dashou), 2), + 'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2), + 'total_profit': round(total_profit, 2) + } + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': summary + } + }) + except Exception as e: + logger.exception("CwhqjtddsjView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + + + + + + + +class CwqtczhqView(APIView): + """ + 其他充值/罚款数据统计(非会员充值) + POST /houtai/cwqtczhq + Body: { + "phone": "管理员手机号", + "granularity": "day" | "month" | "year", + "year": 2026, + "month": 5, // day时必填 + "types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款 + "summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY) + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'qtczcaiwu' not in permissions: + return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403) + + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部 + summary_date = request.data.get('summary_date') + + if isinstance(types, list) and len(types) == 0: + types = [2, 3, 4, 5, 6] + + now = date.today() + + # ---------- 时间范围(用于曲线图) ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + start_date = date(year, month, 1) + end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) + trunc_func = TruncDate('create_time') + # 默认汇总日期 + if not summary_date: + summary_date = now.strftime('%Y-%m-%d') + summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncMonth('create_time') + if not summary_date: + summary_date = now.strftime('%Y-%m') + summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() + elif granularity == 'year': + year = year or now.year + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncYear('create_time') + if not summary_date: + summary_date = str(year) + summary_q = date(year, 1, 1) + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date, datetime.min.time()) + + # ---------- 构建时间序列 ---------- + time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} } + + def ensure_date(ds): + if ds not in time_series_map: + time_series_map[ds] = {} + return time_series_map[ds] + + # 1. 处理充值记录(Czjilu)—— 完全不变 + cz_types = [t for t in types if t in [2, 3, 4, 5]] + if cz_types: + cz_qs = Czjilu.objects.filter( + create_time__gte=start_dt, + create_time__lt=end_dt, + leixing__in=cz_types, + zhuangtai=3 # 只统计支付成功的 + ).annotate( + period=trunc_func + ).values('period', 'leixing').annotate( + total_count=Count('id'), + total_amount=Sum('jine') + ).order_by('period', 'leixing') + + for item in cz_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + lx = item['leixing'] + amount = float(item['total_amount'] or 0) + count = item['total_count'] + # 收益:积分(3)收益为金额本身,其他为0 + profit = amount if lx == 3 else 0.0 + + day_data = ensure_date(period_str) + day_data[str(lx)] = { + 'count': count, + 'amount': amount, + 'profit': profit, + 'fenhong_count': 0, # 充值类型无分红 + 'fenhong_amount': 0.0 + } + + # 2. 处理罚款(Fadan)—— 修改:直接从 Fadan 表获取分红总额和分红笔数 + if 6 in types: + # 使用一次聚合查询,同时获取罚款笔数、罚款金额、分红总额、分红笔数 + penalty_qs = Fadan.objects.filter( + create_time__gte=start_dt, + create_time__lt=end_dt, + zhuangtai=2 # 已缴纳 + ).annotate( + period=trunc_func + ).values('period').annotate( + penalty_count=Count('id'), + penalty_amount=Sum('fakuanjine'), + # 直接使用罚单表中的申请人分红金额字段 + fenhong_amount=Sum('shenqingren_fenhong_jine'), + fenhong_count=Count('id', filter=Q(shenqingren_fenhong_jine__gt=0)) + ).order_by('period') + + for item in penalty_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + day_data = ensure_date(period_str) + penalty_amt = float(item['penalty_amount'] or 0) + fenhong_amt = float(item['fenhong_amount'] or 0) + profit = round(penalty_amt - fenhong_amt, 2) + day_data['6'] = { + 'count': item['penalty_count'], + 'amount': penalty_amt, + 'profit': profit, + 'fenhong_count': item['fenhong_count'], + 'fenhong_amount': fenhong_amt + } + + # 构造有序时间序列列表 + time_series = [] + for period_str in sorted(time_series_map.keys()): + entry = {'date': period_str, 'types': {}} + for type_code in sorted(time_series_map[period_str].keys()): + entry['types'][type_code] = time_series_map[period_str][type_code] + time_series.append(entry) + + # ---------- 汇总指定日期(summary)—— 完全保留原逻辑,仅将罚款部分改为从 Fadan 表直接取分红 ---------- + summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0} + + # 汇总充值类型(完全不变) + for lx in cz_types: + filter_kwargs = { + 'leixing': lx, + 'zhuangtai': 3, + } + if granularity == 'day': + filter_kwargs['create_time__date'] = summary_q + elif granularity == 'month': + filter_kwargs['create_time__year'] = summary_q.year + filter_kwargs['create_time__month'] = summary_q.month + else: + filter_kwargs['create_time__year'] = summary_q.year + agg = Czjilu.objects.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine')) + count = agg['count'] or 0 + amount = float(agg['amount'] or 0) + profit = amount if lx == 3 else 0.0 + summary_result['by_type'][str(lx)] = { + 'count': count, + 'amount': round(amount, 2), + 'profit': round(profit, 2), + 'fenhong_count': 0, + 'fenhong_amount': 0.0 + } + summary_result['total_count'] += count + summary_result['total_amount'] += amount + summary_result['total_profit'] += profit + + # 汇总罚款(修改:从 Fadan 表直接取分红,不再查询 FadanFenhong) + if 6 in types: + penalty_filter = { + 'zhuangtai': 2, + } + if granularity == 'day': + penalty_filter['create_time__date'] = summary_q + elif granularity == 'month': + penalty_filter['create_time__year'] = summary_q.year + penalty_filter['create_time__month'] = summary_q.month + else: + penalty_filter['create_time__year'] = summary_q.year + + # 一次聚合:罚款笔数、金额、分红总额、分红笔数 + agg = Fadan.objects.filter(**penalty_filter).aggregate( + count=Count('id'), + amount=Sum('fakuanjine'), + fenhong_amount=Sum('shenqingren_fenhong_jine'), + fenhong_count=Count('id', filter=Q(shenqingren_fenhong_jine__gt=0)) + ) + penalty_count = agg['count'] or 0 + penalty_amount = float(agg['amount'] or 0) + fh_amount = float(agg['fenhong_amount'] or 0) + fh_count = agg['fenhong_count'] or 0 + profit = round(penalty_amount - fh_amount, 2) + + summary_result['by_type']['6'] = { + 'count': penalty_count, + 'amount': round(penalty_amount, 2), + 'profit': profit, + 'fenhong_count': fh_count, + 'fenhong_amount': round(fh_amount, 2) + } + summary_result['total_count'] += penalty_count + summary_result['total_amount'] += penalty_amount + summary_result['total_profit'] += profit + + summary_result['total_amount'] = round(summary_result['total_amount'], 2) + summary_result['total_profit'] = round(summary_result['total_profit'], 2) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': summary_result + } + }) + except Exception as e: + logger.exception("CwqtczhqView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + +'''class CwqtczhqView(APIView): + """ + 其他充值/罚款数据统计(非会员充值) + POST /houtai/cwqtczhq + Body: { + "phone": "管理员手机号", + "granularity": "day" | "month" | "year", + "year": 2026, + "month": 5, // day时必填 + "types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款 + "summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY) + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'qtczcaiwu' not in permissions: + return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403) + + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部 + summary_date = request.data.get('summary_date') + + if isinstance(types, list) and len(types) == 0: + types = [2, 3, 4, 5, 6] + + now = date.today() + + # ---------- 时间范围(用于曲线图) ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + start_date = date(year, month, 1) + end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) + trunc_func = TruncDate('create_time') + # 默认汇总日期 + if not summary_date: + summary_date = now.strftime('%Y-%m-%d') + summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncMonth('create_time') + if not summary_date: + summary_date = now.strftime('%Y-%m') + summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() + elif granularity == 'year': + year = year or now.year + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncYear('create_time') + if not summary_date: + summary_date = str(year) + summary_q = date(year, 1, 1) + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date, datetime.min.time()) + + # ---------- 构建时间序列 ---------- + time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} } + + def ensure_date(ds): + if ds not in time_series_map: + time_series_map[ds] = {} + return time_series_map[ds] + + # 1. 处理充值记录(Czjilu) + cz_types = [t for t in types if t in [2, 3, 4, 5]] + if cz_types: + cz_qs = Czjilu.objects.filter( + create_time__gte=start_dt, + create_time__lt=end_dt, + leixing__in=cz_types, + zhuangtai=3 # 只统计支付成功的 + ).annotate( + period=trunc_func + ).values('period', 'leixing').annotate( + total_count=Count('id'), + total_amount=Sum('jine') + ).order_by('period', 'leixing') + + for item in cz_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + lx = item['leixing'] + amount = float(item['total_amount'] or 0) + count = item['total_count'] + # 收益:积分(3)收益为金额本身,其他为0 + profit = amount if lx == 3 else 0.0 + + day_data = ensure_date(period_str) + day_data[str(lx)] = { + 'count': count, + 'amount': amount, + 'profit': profit, + 'fenhong_count': 0, # 充值类型无分红 + 'fenhong_amount': 0.0 + } + + # 2. 处理罚款(Fadan)—— 包含分红 + if 6 in types: + # 罚款记录 + penalty_qs = Fadan.objects.filter( + create_time__gte=start_dt, + create_time__lt=end_dt, + zhuangtai=2 # 已缴纳 + ).annotate( + period=trunc_func + ).values('period').annotate( + penalty_count=Count('id'), + penalty_amount=Sum('fakuanjine') + ).order_by('period') + + # 分红记录 + fenhong_qs = FadanFenhong.objects.filter( + create_time__gte=start_dt, + create_time__lt=end_dt + ).annotate( + period=trunc_func + ).values('period').annotate( + fh_count=Count('id'), + fh_amount=Sum('ketixian_fenhong') + ).order_by('period') + + # 合并 + for item in penalty_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + day_data = ensure_date(period_str) + day_data['6'] = { + 'count': item['penalty_count'], + 'amount': float(item['penalty_amount'] or 0), + 'profit': 0.0, # 先设为0,后续计算 + 'fenhong_count': 0, + 'fenhong_amount': 0.0 + } + + for item in fenhong_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + day_data = ensure_date(period_str) + if '6' not in day_data: + day_data['6'] = {'count': 0, 'amount': 0.0, 'profit': 0.0, 'fenhong_count': 0, 'fenhong_amount': 0.0} + day_data['6']['fenhong_count'] += item['fh_count'] + day_data['6']['fenhong_amount'] += float(item['fh_amount'] or 0) + + # 计算罚款收益 = 罚款金额 - 分红金额 + for period_str, day_data in time_series_map.items(): + if '6' in day_data: + penalty = day_data['6'] + penalty['profit'] = round(penalty['amount'] - penalty['fenhong_amount'], 2) + + # 构造有序时间序列列表 + time_series = [] + for period_str in sorted(time_series_map.keys()): + entry = {'date': period_str, 'types': {}} + for type_code in sorted(time_series_map[period_str].keys()): + entry['types'][type_code] = time_series_map[period_str][type_code] + time_series.append(entry) + + # ---------- 汇总指定日期 ---------- + summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0} + + # 汇总充值类型 + for lx in cz_types: + filter_kwargs = { + 'leixing': lx, + 'zhuangtai': 3, + } + if granularity == 'day': + filter_kwargs['create_time__date'] = summary_q + elif granularity == 'month': + filter_kwargs['create_time__year'] = summary_q.year + filter_kwargs['create_time__month'] = summary_q.month + else: + filter_kwargs['create_time__year'] = summary_q.year + agg = Czjilu.objects.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine')) + count = agg['count'] or 0 + amount = float(agg['amount'] or 0) + profit = amount if lx == 3 else 0.0 + summary_result['by_type'][str(lx)] = { + 'count': count, + 'amount': round(amount, 2), + 'profit': round(profit, 2), + 'fenhong_count': 0, + 'fenhong_amount': 0.0 + } + summary_result['total_count'] += count + summary_result['total_amount'] += amount + summary_result['total_profit'] += profit + + # 汇总罚款 + if 6 in types: + penalty_filter = { + 'zhuangtai': 2, + } + fenhong_filter = {} + if granularity == 'day': + penalty_filter['create_time__date'] = summary_q + fenhong_filter['create_time__date'] = summary_q + elif granularity == 'month': + penalty_filter['create_time__year'] = summary_q.year + penalty_filter['create_time__month'] = summary_q.month + fenhong_filter['create_time__year'] = summary_q.year + fenhong_filter['create_time__month'] = summary_q.month + else: + penalty_filter['create_time__year'] = summary_q.year + fenhong_filter['create_time__year'] = summary_q.year + + penalty_agg = Fadan.objects.filter(**penalty_filter).aggregate( + count=Count('id'), amount=Sum('fakuanjine')) + penalty_count = penalty_agg['count'] or 0 + penalty_amount = float(penalty_agg['amount'] or 0) + + fenhong_agg = FadanFenhong.objects.filter(**fenhong_filter).aggregate( + fh_count=Count('id'), fh_amount=Sum('ketixian_fenhong')) + fh_count = fenhong_agg['fh_count'] or 0 + fh_amount = float(fenhong_agg['fh_amount'] or 0) + + profit = round(penalty_amount - fh_amount, 2) + summary_result['by_type']['6'] = { + 'count': penalty_count, + 'amount': round(penalty_amount, 2), + 'profit': profit, + 'fenhong_count': fh_count, + 'fenhong_amount': round(fh_amount, 2) + } + summary_result['total_count'] += penalty_count + summary_result['total_amount'] += penalty_amount + summary_result['total_profit'] += profit + + summary_result['total_amount'] = round(summary_result['total_amount'], 2) + summary_result['total_profit'] = round(summary_result['total_profit'], 2) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': summary_result + } + }) + except Exception as e: + logger.exception("CwqtczhqView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)''' + + + + + +# ==================== 后端接口 ==================== +# 文件:views.py (添加以下两个视图类) + + + +class KhpzhqView(APIView): + """ + 获取所有考核配置数据(称号列表、板块、费用) + POST /houtai/khpzhq + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'kaohepeizhi' not in permissions: + return Response({'code': 403, 'msg': '无考核配置权限'}, status=403) + + bankuai_list = list(Bankuai.objects.values('bankuai_id', 'mingcheng')) + + chenghao_qs = Chenghao.objects.prefetch_related('kaohecishufeiyong_set').all() + chenghao_list = [] + for ch in chenghao_qs: + # 获取费用:按次数排序,转为 {cishu: feiyong} 字典 + feiyong_dict = { + item.cishu: float(item.feiyong) + for item in ch.kaohecishufeiyong_set.order_by('cishu') + } + max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0 + + chenghao_list.append({ + 'id': ch.id, + 'mingcheng': ch.mingcheng, + 'leixing': ch.leixing, + 'leixing_name': ch.get_leixing_display(), + 'bankuai_id': ch.bankuai_id, + 'tubiao_url': ch.tubiao_url or '', + 'texiao_miaoshu': ch.texiao_miaoshu or '', + 'create_time': ch.create_time.strftime('%Y-%m-%d %H:%M:%S') if ch.create_time else '', + 'kaohe_guize': ch.kaohe_guize or '', + 'kaioi_jinpai': ch.kaioi_jinpai, + 'feiyong': feiyong_dict, # {1:100.0, 2:200.0} + 'max_cishu': max_cishu + }) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'bankuai': bankuai_list, + 'chenghao': chenghao_list + } + }) + except Exception as e: + logger.exception("KhpzhqView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + +class ChzsgcView(APIView): + """ + 称号(标签)增删改查统一接口 + POST /houtai/chzsgc + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'kaohepeizhi' not in permissions: + return Response({'code': 403, 'msg': '无考核配置权限'}, status=403) + + action = request.data.get('action') + if not action: + return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400) + + # ========== 创建称号 ========== + if action == 'create': + mingcheng = request.data.get('mingcheng', '').strip() + leixing = request.data.get('leixing', 'dashou') + bankuai_id = request.data.get('bankuai_id') + texiao_miaoshu = request.data.get('texiao_miaoshu', '') + kaohe_guize = request.data.get('kaohe_guize', '') + kaioi_jinpai = request.data.get('kaioi_jinpai', False) + feiyong_list = request.data.get('feiyong_list', []) + + if not mingcheng: + return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400) + if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']: + return Response({'code': 1, 'msg': '无效的称号类型'}, status=400) + if Chenghao.objects.filter(mingcheng=mingcheng).exists(): + return Response({'code': 1, 'msg': '称号名称已存在'}, status=400) + + with transaction.atomic(): + ch = Chenghao.objects.create( + mingcheng=mingcheng, + leixing=leixing, + bankuai_id=bankuai_id, + texiao_miaoshu=texiao_miaoshu, + kaohe_guize=kaohe_guize, + kaioi_jinpai=kaioi_jinpai + ) + # 处理费用:前端传来 [{cishu:1, feiyong:100}, ...] + if feiyong_list: + for item in feiyong_list: + cishu = int(item.get('cishu', 1)) + feiyong = float(item.get('feiyong', 0)) + KaoheCishuFeiyong.objects.update_or_create( + chenghao=ch, cishu=cishu, + defaults={'feiyong': feiyong} + ) + else: + # 默认添加第一次免费 + KaoheCishuFeiyong.objects.create(chenghao=ch, cishu=1, feiyong=0) + + return Response({'code': 0, 'msg': '创建成功', 'data': {'chenghao_id': ch.id}}) + + # ========== 修改称号 ========== + elif action == 'update': + ch_id = request.data.get('chenghao_id') + if not ch_id: + return Response({'code': 1, 'msg': '缺少称号ID'}, status=400) + try: + ch = Chenghao.objects.get(id=ch_id) + except Chenghao.DoesNotExist: + return Response({'code': 1, 'msg': '称号不存在'}, status=404) + + mingcheng = request.data.get('mingcheng', ch.mingcheng).strip() + leixing = request.data.get('leixing', ch.leixing) + bankuai_id = request.data.get('bankuai_id', ch.bankuai_id) + texiao_miaoshu = request.data.get('texiao_miaoshu', ch.texiao_miaoshu) + kaohe_guize = request.data.get('kaohe_guize', ch.kaohe_guize) + kaioi_jinpai = request.data.get('kaioi_jinpai', ch.kaioi_jinpai) + + if not mingcheng: + return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400) + if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']: + return Response({'code': 1, 'msg': '无效的称号类型'}, status=400) + if Chenghao.objects.filter(mingcheng=mingcheng).exclude(id=ch_id).exists(): + return Response({'code': 1, 'msg': '称号名称已存在'}, status=400) + + ch.mingcheng = mingcheng + ch.leixing = leixing + ch.bankuai_id = bankuai_id + ch.texiao_miaoshu = texiao_miaoshu + ch.kaohe_guize = kaohe_guize + ch.kaioi_jinpai = kaioi_jinpai + ch.save() + + # 如果前端传递了 feiyong_list,则全量替换费用 + feiyong_list = request.data.get('feiyong_list') + if feiyong_list is not None: + with transaction.atomic(): + ch.kaohecishufeiyong_set.all().delete() + for item in feiyong_list: + KaoheCishuFeiyong.objects.create( + chenghao=ch, + cishu=int(item.get('cishu', 1)), + feiyong=float(item.get('feiyong', 0)) + ) + + return Response({'code': 0, 'msg': '修改成功'}) + + # ========== 删除称号 ========== + elif action == 'delete': + ch_id = request.data.get('chenghao_id') + if not ch_id: + return Response({'code': 1, 'msg': '缺少称号ID'}, status=400) + try: + ch = Chenghao.objects.get(id=ch_id) + except Chenghao.DoesNotExist: + return Response({'code': 1, 'msg': '称号不存在'}, status=404) + ch.delete() + return Response({'code': 0, 'msg': '删除成功'}) + + # ========== 添加/修改某次费用 ========== + elif action == 'add_feiyong': + ch_id = request.data.get('chenghao_id') + cishu = request.data.get('cishu') + feiyong = request.data.get('feiyong', 0) + if not ch_id or not cishu: + return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400) + try: + ch = Chenghao.objects.get(id=ch_id) + except Chenghao.DoesNotExist: + return Response({'code': 1, 'msg': '称号不存在'}, status=404) + if int(cishu) < 1: + return Response({'code': 1, 'msg': '次数必须>=1'}, status=400) + KaoheCishuFeiyong.objects.update_or_create( + chenghao=ch, cishu=cishu, + defaults={'feiyong': feiyong} + ) + return Response({'code': 0, 'msg': '添加/修改费用成功'}) + + # ========== 删除某次费用 ========== + elif action == 'remove_feiyong': + ch_id = request.data.get('chenghao_id') + cishu = request.data.get('cishu') + if not ch_id or not cishu: + return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400) + if int(cishu) == 1: + return Response({'code': 1, 'msg': '不能删除第1次考核费用'}, status=400) + try: + ch = Chenghao.objects.get(id=ch_id) + except Chenghao.DoesNotExist: + return Response({'code': 1, 'msg': '称号不存在'}, status=404) + KaoheCishuFeiyong.objects.filter(chenghao=ch, cishu=cishu).delete() + return Response({'code': 0, 'msg': '删除成功'}) + + # ========== 修改某次费用(等价于add) ========== + elif action == 'update_feiyong': + return self.add_feiyong(request) # 复用逻辑 + + else: + return Response({'code': 1, 'msg': '未知操作'}, status=400) + + except Exception as e: + logger.exception("ChzsgcView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + +# ==================== 后端 views.py ==================== +# 请确保已有以下导入,如果没有请在文件头部添加 + + + + +class KhgglView(APIView): + """ + 考核官管理数据接口(列表、筛选、分页) + POST /houtai/khggl + Body: { + "phone": "管理员手机号", + "page": 1, // 可选,默认1 + "page_size": 15, // 可选,默认15 + "yonghuid": "1234567", // 按用户ID筛选,可选 + "zhuangtai": 1, // 账号状态 0=禁用,1=正常,可选 + "shenhe_zhuangtai": 0, // 审核官状态 0=空闲,1=审核中,可选 + "bankuai_name": "王者" // 板块名称模糊筛选,可选 + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'kaohepeizhi' not in permissions: + return Response({'code': 403, 'msg': '无考核管理权限'}, status=403) + + # 分页参数 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 15)) + if page < 1: page = 1 + if page_size < 1: page_size = 15 + + # 筛选条件 + filter_yonghuid = request.data.get('yonghuid', '').strip() + filter_zhuangtai = request.data.get('zhuangtai') + filter_shenhe_zhuangtai = request.data.get('shenhe_zhuangtai') + filter_bankuai_name = request.data.get('bankuai_name', '').strip() + + # 基础查询:所有审核官,预加载用户及打手扩展表 + queryset = UserShenheguan.objects.select_related('user__dashou_profile') + + if filter_yonghuid: + queryset = queryset.filter(user__yonghuid=filter_yonghuid) + if filter_zhuangtai is not None: + queryset = queryset.filter(zhuangtai=filter_zhuangtai) + if filter_shenhe_zhuangtai is not None: + queryset = queryset.filter(shenhe_zhuangtai=filter_shenhe_zhuangtai) + + # 如果填写了板块名称,筛选出关联该板块的审核官 + if filter_bankuai_name: + bankuai_ids = Bankuai.objects.filter( + mingcheng__icontains=filter_bankuai_name + ).values_list('bankuai_id', flat=True) + shenheguan_ids = KaoheguanBankuai.objects.filter( + bankuai_id__in=bankuai_ids + ).values_list('kaoheguan_id', flat=True) + queryset = queryset.filter(user__yonghuid__in=shenheguan_ids) + + total = queryset.count() + start = (page - 1) * page_size + end = start + page_size + shenheguan_list = queryset.order_by('-create_time')[start:end] + + # ---------- 预计算每个板块的打手数量 ---------- + bankuai_dashou_count_map = {} + all_banquai = Bankuai.objects.all() + for bk in all_banquai: + tag_ids = Chenghao.objects.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True) + count = YonghuChenghao.objects.filter( + chenghao_id__in=tag_ids, + yonghu__dashou_profile__isnull=False + ).values('yonghu').distinct().count() + bankuai_dashou_count_map[bk.bankuai_id] = count + + # ---------- 新增:板块标签统计 ---------- + bankuai_tags_data = [] + for bk in all_banquai: + tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou') + tag_list = [] + for tag in tags: + user_count = YonghuChenghao.objects.filter(chenghao=tag).values('yonghu').distinct().count() + tag_list.append({ + 'id': tag.id, + 'name': tag.mingcheng, + 'user_count': user_count, + }) + bankuai_tags_data.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'tags': tag_list, + }) + + # 构造返回数据(审核官列表) + data_list = [] + for sg in shenheguan_list: + user = sg.user + # 获取该审核官关联的板块 + bk_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=user.yonghuid).select_related('bankuai') + bankuai_info = [] + for bk in bk_qs: + bid = bk.bankuai.bankuai_id + bankuai_info.append({ + 'bankuai_id': bid, + 'mingcheng': bk.bankuai.mingcheng, + 'dashou_count': bankuai_dashou_count_map.get(bid, 0) + }) + + dashou_nick = user.dashou_profile.nicheng if hasattr(user, 'dashou_profile') and user.dashou_profile else '' + + data_list.append({ + 'yonghuid': user.yonghuid, + 'avatar': user.avatar or '', + 'phone': user.phone or '', + 'zhuangtai': sg.zhuangtai, + 'shenhe_zhuangtai': sg.shenhe_zhuangtai, + 'shenhe_zongshu': sg.shenhe_zongshu, + 'tongguo_zongshu': sg.tongguo_zongshu, + 'yue': float(sg.yue), + 'zonge': float(sg.zonge), + 'create_time': sg.create_time.strftime('%Y-%m-%d %H:%M:%S') if sg.create_time else '', + 'update_time': sg.update_time.strftime('%Y-%m-%d %H:%M:%S') if sg.update_time else '', + 'bankuai': bankuai_info, + 'nicheng': dashou_nick, + 'is_renzheng': sg.is_renzheng, + }) + + all_bankuai = list(Bankuai.objects.values('bankuai_id', 'mingcheng')) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'total': total, + 'page': page, + 'page_size': page_size, + 'list': data_list, + 'all_bankuai': all_bankuai, + 'bankuai_tags': bankuai_tags_data, # 新增板块标签统计 + } + }) + + except Exception as e: + logger.exception("KhgglView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + +class ShgxgsjView(APIView): + """ + 审核官信息修改(余额、状态、板块关联) + POST /houtai/shgxgsj + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'kaohepeizhi' not in permissions: + return Response({'code': 403, 'msg': '无考核管理权限'}, status=403) + + yonghuid = request.data.get('yonghuid') + if not yonghuid: + return Response({'code': 1, 'msg': '缺少用户ID'}, status=400) + + try: + shenheguan = UserShenheguan.objects.select_related('user').get(user__yonghuid=yonghuid) + except UserShenheguan.DoesNotExist: + return Response({'code': 1, 'msg': '审核官不存在'}, status=404) + + action = request.data.get('action') + + if action == 'update_info': + if 'yue' in request.data: + shenheguan.yue = float(request.data.get('yue', 0)) + if 'zhuangtai' in request.data: + zt = int(request.data.get('zhuangtai')) + if zt not in [0, 1]: + return Response({'code': 1, 'msg': '无效的账号状态'}, status=400) + shenheguan.zhuangtai = zt + if 'shenhe_zhuangtai' in request.data: + szt = int(request.data.get('shenhe_zhuangtai')) + if szt not in [0, 1]: + return Response({'code': 1, 'msg': '无效的审核官状态'}, status=400) + shenheguan.shenhe_zhuangtai = szt + # 新增认证状态编辑 + if 'is_renzheng' in request.data: + shenheguan.is_renzheng = bool(request.data.get('is_renzheng')) + shenheguan.save() + return Response({'code': 0, 'msg': '修改成功'}) + + elif action == 'add_bankuai': + bankuai_id = request.data.get('bankuai_id') + if not bankuai_id: + return Response({'code': 1, 'msg': '缺少板块ID'}, status=400) + if not Bankuai.objects.filter(bankuai_id=bankuai_id).exists(): + return Response({'code': 1, 'msg': '板块不存在'}, status=400) + if KaoheguanBankuai.objects.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists(): + return Response({'code': 1, 'msg': '已关联该板块'}, status=400) + KaoheguanBankuai.objects.create(kaoheguan_id=yonghuid, bankuai_id=bankuai_id) + return Response({'code': 0, 'msg': '板块关联成功'}) + + elif action == 'remove_bankuai': + bankuai_id = request.data.get('bankuai_id') + if not bankuai_id: + return Response({'code': 1, 'msg': '缺少板块ID'}, status=400) + deleted, _ = KaoheguanBankuai.objects.filter( + kaoheguan_id=yonghuid, bankuai_id=bankuai_id + ).delete() + if not deleted: + return Response({'code': 1, 'msg': '未找到该板块关联'}, status=400) + return Response({'code': 0, 'msg': '板块移除成功'}) + + else: + return Response({'code': 1, 'msg': '未知操作'}, status=400) + + except Exception as e: + logger.exception("ShgxgsjView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + + +class ZxkfghdsView(APIView): + """ + 客服更换打手接口 + 路径:POST /houtai/zxkfghds + 功能: + 1. 将当前订单状态改为已退款(5),释放接单打手(若有) + 2. 如需跨平台通知,则调用对方平台退款通知接口 + 3. 基于原订单信息生成全新订单(状态直接为 1,已替老板填写) + 4. 生成新的派单链接,并标记为已使用 + 5. 新链接记录上一个链接ID和上一个订单ID + 6. 【关键】不操作商家余额、退款次数、每日统计,因新订单是替换而非新增扣款 + 权限:需要'003aa'(跨平台管理)权限,与退款接口一致 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # ========== 1. 参数提取 ========== + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + if not username or not dingdan_id: + logger.warning("更换打手参数不完整") + return Response({'code': 400, 'msg': '参数不完整'}) + + # ========== 2. 权限校验(与跨平台退款相同) ========== + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 返回错误响应 + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权处理订单'}) + + current_user = request.user + + # ========== 3. 查询原订单及关联信息 ========== + try: + old_order = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {dingdan_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + + # 仅允许状态为 2(进行中)或 8(结算中)更换打手 + if old_order.zhuangtai not in [1,7,2, 8]: + return Response({'code': 400, 'msg': '当前订单状态不允许更换打手'}) + + # 本接口仅处理商家发单(fadan_pingtai=2) + if old_order.fadan_pingtai != 2: + return Response({'code': 400, 'msg': '仅支持商家发单的更换操作'}) + + # 获取原链接(用于模板ID等) + try: + old_lianjie = ShangjiaLianjie.objects.get(dingdan_id=dingdan_id) + except ShangjiaLianjie.DoesNotExist: + logger.error(f"原链接记录不存在,订单: {dingdan_id}") + return Response({'code': 500, 'msg': '链接数据异常'}) + + # 商家信息(仅用于记录关联,不操作余额) + try: + shangjia_ext = old_order.shangjia_kuozhan + shangjia_id = shangjia_ext.shangjia_id + # 获取商家昵称用于新订单扩展表 + shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) + shangjia_nicheng = shangjia_user.shop_profile.nicheng or f"商家{shangjia_id}" + except (ObjectDoesNotExist, UserShangjia.DoesNotExist): + logger.error(f"商家信息缺失,订单: {dingdan_id}") + return Response({'code': 500, 'msg': '商家数据异常'}) + + # ========== 4. 判断是否需要跨平台退款通知 ========== + is_cross = old_order.is_cross + partner_club_id = old_order.partner_club_id + partner_order_id = old_order.partner_order_id + need_notify_partner = is_cross and partner_club_id and partner_order_id + + # ========== 5. 退款处理(仅打手侧,不碰商家) ========== + with transaction.atomic(): + # 5.1 订单状态改为已退款 + old_order.zhuangtai = 5 + old_order.clkf = current_user.phone + old_order.save() + + # 5.2 处理接单打手(若有) + dashou_id = old_order.jiedan_dashou_id + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 # 退款次数+1 + dashou_profile.zhuangtai = 1 # 设为空闲 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"打手 {dashou_id} 不存在,跳过更新") + + # 5.3 更新退款记录表 + Tuikuanjilu.objects.update_or_create( + dingdan_id=dingdan_id, + defaults={ + 'dashouid': dashou_id or '', + 'qingqiuid': '', + 'chuliid': current_user.phone, + 'liyou': '客服更换打手退款', + 'sqzhuangtai': 1, + 'jine': old_order.jine or 0, + 'dashou_fencheng': old_order.dashou_fencheng or 0, + 'jieshao': old_order.jieshao or '', + 'beizhu': '更换打手自动退款', + 'nicheng': old_order.nicheng or '', + } + ) + + # 5.4 打手每日统计(退款) + if dashou_id: + try: + update_dashou_daily_by_action( + yonghuid=dashou_id, + amount=old_order.dashou_fencheng, + action=3 # 3=退款 + ) + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + + # 5.5 客服处理统计 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + # 5.6 跨平台退款通知(需要时) + if need_notify_partner: + try: + # 获取我方俱乐部ID + my_club_config = ClubConfig.objects.filter(is_self=1).first() + if not my_club_config: + raise Exception("我方俱乐部配置不存在") + self_club_id = my_club_config.club_id + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain + + notify_url = partner_domain.rstrip('/') + '/houtai/dfddtk' + payload = { + 'order_id': partner_order_id, + 'club_id': self_club_id, + 'status': 5, + 'reason': '客服更换打手退款' + } + + # 调用对方接口(必须成功,否则整个事务回滚) + resp = requests.post(notify_url, json=payload, timeout=5) + if resp.status_code == 200: + resp_json = resp.json() + if resp_json.get('code') != 0: + transaction.set_rollback(True) + logger.error(f"对方平台退款通知失败: {resp_json.get('msg')}") + return Response({'code': 500, 'msg': f'对方平台通知失败: {resp_json.get("msg")}'}) + else: + transaction.set_rollback(True) + logger.error(f"对方平台接口HTTP错误: {resp.status_code}") + return Response({'code': 500, 'msg': '通知对方平台失败'}) + + # 更新跨平台扩展表(如果存在) + CrossPlatformOrderData.objects.update_or_create( + dingdan_id=dingdan_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id, + 'partner_order_status': 5, + } + ) + except Exception as e: + transaction.set_rollback(True) + logger.error(f"通知对方平台异常: {e}") + return Response({'code': 500, 'msg': f'通知对方平台失败: {str(e)}'}) + + # 至此,原订单已退款,事务提交 + + # ========== 6. 生成新订单和链接(不扣商家余额) ========== + try: + with transaction.atomic(): + # ---- 6.1 生成新的订单ID和Token ---- + def generate_dingdan_id(): + timestamp = int(time.time() * 1000) + process_id = os.getpid() % 1000 + random_num = random.randint(1000, 9999) + return f"SJ{timestamp}{process_id:03d}{random_num}" + + def generate_secure_token(dd_id, uid): + timestamp = int(time.time() * 1_000_000) + salt = secrets.token_hex(8) + raw = f"{timestamp}:{dd_id}:{uid}:{salt}" + token = hashlib.sha256(raw.encode()).hexdigest()[:24] + if ShangjiaLianjie.objects.filter(lianjie_token=token).exists(): + return generate_secure_token(dd_id, uid) + return token + + def generate_link_url(token): + base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/') + return f"{base}/order/{token}" + + new_dingdan_id = generate_dingdan_id() + secure_token = generate_secure_token(new_dingdan_id, shangjia_id) + + # ---- 6.2 创建新订单(复制核心数据,状态直接为1) ---- + new_order = Dingdan.objects.create( + dingdan_id=new_dingdan_id, + zhuangtai=1, # 下单中 + fadan_pingtai=2, + jine=old_order.jine, + dashou_fencheng=old_order.dashou_fencheng, + leixing_id=old_order.leixing_id, + yaoqiuleixing=old_order.yaoqiuleixing, + huiyuan_id=old_order.huiyuan_id, + yongjin=old_order.yongjin, + jieshao=old_order.jieshao, + nicheng=old_order.nicheng, # 继承游戏昵称 + beizhu=old_order.beizhu, + user1_id=old_order.user1_id, # 商家标识 + tupian=old_order.tupian, + # 注意:不继承 zhiding_id、jiedan_dashou_id + ) + + # ---- 6.3 创建商家扩展表 ---- + DingdanShangjia.objects.create( + dingdan=new_order, + shangjia_id=shangjia_id, + sjnicheng=shangjia_nicheng, + ) + + # ---- 6.4 生成新链接并关联旧链接信息 ---- + link_url = generate_link_url(secure_token) + new_lianjie = ShangjiaLianjie.objects.create( + yonghu_id=shangjia_id, + leixing_id=old_lianjie.leixing_id, + moban_id=old_lianjie.moban_id, + lianjie=link_url, + lianjie_token=secure_token, + dingdan_id=new_dingdan_id, + is_used=True, # 直接标记已使用 + youxiao_shijian=timezone.now() + timezone.timedelta(days=1), + previous_lianjie_id=old_lianjie.id, # 记录旧链接ID + previous_dingdan_id=old_order.dingdan_id, # 记录旧订单ID + ) + + # ---- 6.5 异步通知(广播新订单、跨平台同步) ---- + try: + order_info = { + 'dingdan_id': new_dingdan_id, + 'game_type': self._get_game_type_name(new_order.leixing_id), + 'amount': str(new_order.jine), + 'order_desc': (new_order.jieshao or '')[:50], + } + dingdan_guangbo.delay(order_info) + except Exception as e: + logger.error(f"广播通知失败: {e}") + + # 跨平台同步(与客户填写接口保持一致) + try: + sync_order_to_partners(new_dingdan_id) + except Exception as e: + logger.error(f"跨平台同步失败: {e}") + + # 注意:此处不更新商家余额、发布量、每日统计,因为这是替换订单 + + logger.info(f"更换打手成功:旧订单{dingdan_id}→退款,新订单{new_dingdan_id}已生成") + + return Response({ + 'code': 0, + 'msg': '更换成功', + 'data': { + 'new_dingdan_id': new_dingdan_id, + 'new_lianjie': link_url, + } + }) + + except Exception as e: + logger.error(f"生成新订单事务失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '生成新订单失败'}) + + def _get_game_type_name(self, leixing_id): + """根据类型ID获取游戏类型名称""" + try: + gt = ShangpinLeixing.objects.filter(id=leixing_id).first() + return gt.jieshao if gt else '游戏订单' + except Exception: + return '游戏订单' + + + + + + + + + +class KptxwztjbView(APIView): + """ + 跨平台订单状态同步接口(对方平台调用) + 请求:POST /houtai/kptxwztjb + 参数: + - token: 同步专用令牌(明文) + - partner_order_id: 我方订单ID(在对方眼中是partner_order_id,实际查本库dingdan_id) + - our_order_id: 对方订单ID(在本库中为partner_order_id) + - club_id: 俱乐部ID(可选验证) + 返回: + code=0 成功,data.status 为订单状态 + 其他 code 为错误 + 注意:此接口仅用于查询对方订单状态,不做任何修改 + """ + + authentication_classes = [] # 无需JWT等认证 + permission_classes = [] # 公开接口 + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + token = request.data.get('token', '').strip() + partner_order_id = request.data.get('partner_order_id', '').strip() + our_order_id = request.data.get('our_order_id', '').strip() + club_id = request.data.get('club_id', '').strip() + + if not all([token, partner_order_id, our_order_id]): + logger.warning("跨平台状态同步参数缺失") + return Response({'code': 400, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) + + # 2. 验证同步令牌 + SYNC_TOKEN = getattr(settings, 'CROSS_PLATFORM_SYNC_TOKEN', None) + if not SYNC_TOKEN or token != SYNC_TOKEN: + logger.warning(f"无效的同步令牌: {token[:8]}...") + return Response({'code': 403, 'msg': '无效令牌'}, status=status.HTTP_403_FORBIDDEN) + + # 3. 查询订单 + # partner_order_id 在对方视角是“我方订单ID”,在本库中应匹配 dingdan_id + try: + order = Dingdan.objects.get(dingdan_id=partner_order_id) + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在, partner_order_id: {partner_order_id}") + return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 4. 验证订单关联关系:本库中的 partner_order_id 应等于传入的 our_order_id + if order.partner_order_id != our_order_id: + logger.warning( + f"订单关联不匹配: partner_order_id={order.partner_order_id}, our_order_id={our_order_id}" + ) + return Response({'code': 400, 'msg': '订单关联信息不匹配'}, status=status.HTTP_400_BAD_REQUEST) + + + + # 6. 返回订单状态 + status_code = order.zhuangtai + logger.info( + f"跨平台状态查询成功, dingdan_id={order.dingdan_id}, status={status_code}" + ) + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'dingdan_id': order.dingdan_id, + 'status': status_code, + } }) \ No newline at end of file diff --git a/dengji/migrations/__init__.py b/config/__init__.py similarity index 100% rename from dengji/migrations/__init__.py rename to config/__init__.py diff --git a/dengji/admin.py b/config/admin.py similarity index 100% rename from dengji/admin.py rename to config/admin.py diff --git a/peizhi/apps.py b/config/apps.py similarity index 100% rename from peizhi/apps.py rename to config/apps.py diff --git a/peizhi/migrations/0001_initial.py b/config/migrations/0001_initial.py similarity index 100% rename from peizhi/migrations/0001_initial.py rename to config/migrations/0001_initial.py diff --git a/peizhi/migrations/0002_shangjiamoban_shangjialianjie.py b/config/migrations/0002_shangjiamoban_shangjialianjie.py similarity index 100% rename from peizhi/migrations/0002_shangjiamoban_shangjialianjie.py rename to config/migrations/0002_shangjiamoban_shangjialianjie.py diff --git a/peizhi/migrations/0003_tupianpeizhi_fangwen_cishu.py b/config/migrations/0003_tupianpeizhi_fangwen_cishu.py similarity index 100% rename from peizhi/migrations/0003_tupianpeizhi_fangwen_cishu.py rename to config/migrations/0003_tupianpeizhi_fangwen_cishu.py diff --git a/peizhi/migrations/0004_tixianquotadefault.py b/config/migrations/0004_tixianquotadefault.py similarity index 100% rename from peizhi/migrations/0004_tixianquotadefault.py rename to config/migrations/0004_tixianquotadefault.py diff --git a/peizhi/migrations/0005_ordertypemapping_dailydispatchstat_club.py b/config/migrations/0005_ordertypemapping_dailydispatchstat_club.py similarity index 100% rename from peizhi/migrations/0005_ordertypemapping_dailydispatchstat_club.py rename to config/migrations/0005_ordertypemapping_dailydispatchstat_club.py diff --git a/peizhi/migrations/0006_clubconfig_accountpermission.py b/config/migrations/0006_clubconfig_accountpermission.py similarity index 100% rename from peizhi/migrations/0006_clubconfig_accountpermission.py rename to config/migrations/0006_clubconfig_accountpermission.py diff --git a/peizhi/migrations/0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more.py b/config/migrations/0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more.py similarity index 100% rename from peizhi/migrations/0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more.py rename to config/migrations/0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more.py diff --git a/peizhi/migrations/0008_clubconfig_storage_bucket_name.py b/config/migrations/0008_clubconfig_storage_bucket_name.py similarity index 100% rename from peizhi/migrations/0008_clubconfig_storage_bucket_name.py rename to config/migrations/0008_clubconfig_storage_bucket_name.py diff --git a/peizhi/migrations/0009_dailypayoutstat_dailyincomestat.py b/config/migrations/0009_dailypayoutstat_dailyincomestat.py similarity index 100% rename from peizhi/migrations/0009_dailypayoutstat_dailyincomestat.py rename to config/migrations/0009_dailypayoutstat_dailyincomestat.py diff --git a/peizhi/migrations/0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more.py b/config/migrations/0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more.py similarity index 100% rename from peizhi/migrations/0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more.py rename to config/migrations/0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more.py diff --git a/peizhi/migrations/0011_withdrawconfig.py b/config/migrations/0011_withdrawconfig.py similarity index 100% rename from peizhi/migrations/0011_withdrawconfig.py rename to config/migrations/0011_withdrawconfig.py diff --git a/peizhi/migrations/0012_shangjiamoban_chenghao_id_and_more.py b/config/migrations/0012_shangjiamoban_chenghao_id_and_more.py similarity index 100% rename from peizhi/migrations/0012_shangjiamoban_chenghao_id_and_more.py rename to config/migrations/0012_shangjiamoban_chenghao_id_and_more.py diff --git a/peizhi/migrations/0013_shangjialianjie_previous_dingdan_id_and_more.py b/config/migrations/0013_shangjialianjie_previous_dingdan_id_and_more.py similarity index 100% rename from peizhi/migrations/0013_shangjialianjie_previous_dingdan_id_and_more.py rename to config/migrations/0013_shangjialianjie_previous_dingdan_id_and_more.py diff --git a/dingdan/__init__.py b/config/migrations/__init__.py similarity index 100% rename from dingdan/__init__.py rename to config/migrations/__init__.py diff --git a/peizhi/models.py b/config/models.py similarity index 99% rename from peizhi/models.py rename to config/models.py index 1715daf..c4ac910 100644 --- a/peizhi/models.py +++ b/config/models.py @@ -1,5 +1,7 @@ from django.db import models + + class Lunbo(models.Model): tupian_url = models.CharField(max_length=500, null=True, blank=True, verbose_name='轮播图片URL') leixing = models.IntegerField(null=True, blank=True, verbose_name='图片类型:1:轮播2:个人中心背景') @@ -66,9 +68,6 @@ class Szjilu(models.Model): -# models/shangjia_moban.py -from django.db import models - class ShangjiaMoban(models.Model): """商家订单模板表 - 存储商家创建的订单模板""" @@ -119,8 +118,6 @@ class ShangjiaMoban(models.Model): return f"模板{self.id}-{self.yonghu_id}" -# models/shangjia_lianjie.py -from django.db import models class ShangjiaLianjie(models.Model): @@ -274,10 +271,6 @@ class WithdrawConfig(models.Model): - -# apps/club/models.py -from django.db import models - class Club(models.Model): """合作俱乐部/平台信息表""" # 自己的俱乐部ID(6位字符) @@ -384,8 +377,6 @@ class ClubConfig(models.Model): return f"{self.club_nickname or self.club_id} ({'我方' if self.is_self else '对方'})" -from django.db import models - class AccountPermission(models.Model): """账号权限表:记录每个账号拥有的权限及开关状态""" account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID') @@ -521,10 +512,6 @@ class DailyPayoutStat(models.Model): - - -from django.db import models - class PopupPage(models.Model): page_key = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='页面标识') name = models.CharField(max_length=100, verbose_name='页面名称') diff --git a/peizhi/serializers.py b/config/serializers.py similarity index 100% rename from peizhi/serializers.py rename to config/serializers.py diff --git a/peizhi/tasks.py b/config/tasks.py similarity index 98% rename from peizhi/tasks.py rename to config/tasks.py index f49f474..ee1118a 100644 --- a/peizhi/tasks.py +++ b/config/tasks.py @@ -3,7 +3,6 @@ 处理收支记录表的每日清零 固定使用ID=1的记录 """ - from celery import shared_task from django.db import transaction from django.utils import timezone @@ -12,7 +11,7 @@ from decimal import Decimal import logging # 导入模型 -from peizhi.models import Szjilu +from config.models import Szjilu from utils.celery_utils import log_task_execution, rollback_on_failure logger = logging.getLogger(__name__) diff --git a/dengji/tests.py b/config/tests.py similarity index 100% rename from dengji/tests.py rename to config/tests.py diff --git a/peizhi/urls.py b/config/urls.py similarity index 99% rename from peizhi/urls.py rename to config/urls.py index 3dc61e6..0459e8e 100644 --- a/peizhi/urls.py +++ b/config/urls.py @@ -37,4 +37,4 @@ urlpatterns = [ path('yhbdsjh', CheckPhoneAuthView.as_view(), name='判断是否需要手机号绑定'), path('hqsjlslj', ShangjiaLianjieListView.as_view(), name='商家获取生成链接'), -] \ No newline at end of file +] diff --git a/peizhi/views.py b/config/views.py similarity index 59% rename from peizhi/views.py rename to config/views.py index f213cf4..f0cb6db 100644 --- a/peizhi/views.py +++ b/config/views.py @@ -1,5563 +1,3376 @@ -# peizhi/views.py -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.throttling import AnonRateThrottle -from django.db import models -from peizhi.models import Gonggao, Lunbo -import logging -from rest_framework.permissions import AllowAny # 新增这行 - -# 配置日志 -logger = logging.getLogger(__name__) - - -# peizhi/views.py 或新建 views_config.py -import logging -from rest_framework.views import APIView - -from rest_framework.permissions import AllowAny -from rest_framework.parsers import JSONParser -from .models import ClubConfig - - -class GetDynamicConfigView(APIView): - - - permission_classes = [AllowAny] - parser_classes = [JSONParser] - - def post(self, request): - try: - # 查询我方俱乐部配置(is_self=1),取第一条(通常只有一条) - club_config = ClubConfig.objects.filter(is_self=1).only( - 'storage_bucket_domain', - 'storage_bucket_name', - 'chat_api_key', - 'customer_service_api_url', - 'customer_service_corp_id' - ).first() - - # 默认值(防止空数据导致前端报错) - cos_bucket = '' - cos_region = 'ap-shanghai' - cos_oss_url = '' - goeasy_host = 'hangzhou.goeasy.io' - goeasy_appkey = '' - kefu_link = '' - kefu_enterprise_id = '' - morentouxiang = 'beijing/morentouxiang.jpg' - dashouguize = 'a_long/dashouguize.jpg' - - if club_config: - # 存储桶配置 - bucket = club_config.storage_bucket_name or '' - region = 'ap-shanghai' # 若需要动态,可单独存储 - cos_oss_url = club_config.storage_bucket_domain or '' - if cos_oss_url and not cos_oss_url.endswith('/'): - cos_oss_url += '/' - cos_bucket = bucket - cos_region = region - - # GoEasy 配置 - if club_config.chat_api_key: - goeasy_appkey = club_config.chat_api_key - # host 如果需要可动态,这里固定或从其他字段获取 - - # 客服配置 - if club_config.customer_service_api_url: - kefu_link = club_config.customer_service_api_url - if club_config.customer_service_corp_id: - kefu_enterprise_id = club_config.customer_service_corp_id - - # 其他配置可在此扩展 - - # 构造返回数据 - data = { - "code": 0, - "data": { - "cos": { - "bucket": cos_bucket, - "region": cos_region, - "ossImageUrl": cos_oss_url, - "uploadPathPrefix": "order/" - }, - "goEasy": { - "host": goeasy_host, - "appkey": goeasy_appkey - }, - "otherConfig": { - "morentouxiang": morentouxiang, - "dashouguize": dashouguize - }, - "kefu": { - "link": kefu_link, - "enterpriseId": kefu_enterprise_id - } - } - } - return Response(data) - - except Exception as e: - logger.error(f"获取动态配置失败: {str(e)}", exc_info=True) - return Response({"code": 500, "msg": "服务器内部错误"}, status=500) - - - - - - -class ShangpinGonggaoView(APIView): - """ - 商品公告和轮播图接口 - 获取类型为1的公告和轮播图 - """ - - # 应用节流(限制频率) - throttle_classes = [AnonRateThrottle] - # 新增:允许所有用户访问(不需要认证) - permission_classes = [AllowAny] - - def post(self, request): - """ - 处理POST请求,返回公告和轮播图数据 - - 请求: POST /api/peizhi/shangpingonggao/ - 返回: { - "shangpingonggao": "公告内容字符串", - "shangpinlunbo": ["轮播图URL1", "轮播图URL2"] - } - """ - try: - logger.info("收到商品公告和轮播图请求") - - # 1. 查询公告(类型为1) - gonggao_obj = Gonggao.objects.filter(leixing=1).first() - - # 获取公告内容,如果没有则返回空字符串 - gonggao_content = gonggao_obj.jieshao if gonggao_obj else "" - - # 2. 查询轮播图(类型为1,按显示顺序排序) - lunbo_queryset = Lunbo.objects.filter(leixing=1) - - # 提取轮播图URL列表 - lunbo_urls = [lunbo.tupian_url for lunbo in lunbo_queryset] - - # 3. 构建返回数据 - response_data = { - "shangpingonggao": gonggao_content, - "shangpinlunbo": lunbo_urls - } - - logger.info(f"返回数据:公告长度{len(gonggao_content)},轮播图数量{len(lunbo_urls)}") - - # 4. 返回响应 - return Response(response_data, status=status.HTTP_200_OK) - - except Exception as e: - # 记录异常 - logger.error(f"获取商品公告和轮播图时发生错误:{str(e)}") - - # 返回错误响应 - error_data = { - "error": "服务器内部错误", - "detail": "获取数据失败,请稍后重试" - } - return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - -# peizhi/views.py -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status, permissions -from rest_framework.parsers import MultiPartParser, FormParser -from django.db import transaction -from yonghu.models import UserMain, AdminProfile -from .models import Tupianpeizhi, Qunpeizhi -from dingdan.models import Lilubiao -import json -import os -import uuid -from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image - -class AdminConfigQueryView(APIView): - """ - 管理员获取系统配置接口 - URL: /peizhi/adpzhq - 方法: POST - 权限: JWT Token认证 + 管理员权限 - 参数: zhanghao (管理员账号) - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - # 获取前端传递的账号 - zhanghao = request.data.get('zhanghao') - if not zhanghao: - return Response({ - 'code': 1, - 'msg': '账号不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 获取当前用户(UserMain实例) - user_main = request.user - - # 验证账号:比较user_main的phone与前端传递的zhanghao - if user_main.phone != zhanghao: - return Response({ - 'code': 1, - 'msg': '账号不匹配', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 验证用户类型是否为管理员 - if user_main.user_type != 'admin': - return Response({ - 'code': 1, - 'msg': '无权限', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 验证管理员扩展表是否存在 - try: - admin_profile = user_main.admin_profile - except AdminProfile.DoesNotExist: - return Response({ - 'code': 1, - 'msg': '无权限', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 查询图片配置 - rule_image = Tupianpeizhi.objects.filter(leixing=1).first() - default_avatar = Tupianpeizhi.objects.filter(leixing=2).first() - - # 查询群配置 - qun_configs = Qunpeizhi.objects.all() - qq_groups = [] - for config in qun_configs: - qq_groups.append({ - 'peizhiid': config.peizhiid, - 'neirong': config.neirong if config.neirong else '', - 'qunid': config.qunid if config.qunid else '', - 'jieshao': config.jieshao if config.jieshao else '' - }) - - # 查询利率配置 - platform_rate_obj = Lilubiao.objects.filter(fadanpingtai='1').first() - shop_rate_obj = Lilubiao.objects.filter(fadanpingtai='3').first() - # 🔴【新增】打手提现抽成利率(字符5) - dashou_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='5').first() - # 🔴【新增】管事提现抽成利率(字符6) - guanshi_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='6').first() - - # 构建返回数据 - data = { - 'images': { - 'rule_image': rule_image.tupian if rule_image and rule_image.tupian else '', - 'default_avatar': default_avatar.tupian if default_avatar and default_avatar.tupian else '' - }, - 'qq_groups': qq_groups, - 'rates': { - 'platform_rate': float(platform_rate_obj.lilu) if platform_rate_obj and platform_rate_obj.lilu else 0.0, - 'shop_rate': float(shop_rate_obj.lilu) if shop_rate_obj and shop_rate_obj.lilu else 0.0, - # 🔴【新增】打手提现抽成利率 - 'dashou_withdraw_rate': float( - dashou_withdraw_obj.lilu) if dashou_withdraw_obj and dashou_withdraw_obj.lilu else 0.0, - # 🔴【新增】管事提现抽成利率 - 'guanshi_withdraw_rate': float( - guanshi_withdraw_obj.lilu) if guanshi_withdraw_obj and guanshi_withdraw_obj.lilu else 0.0 - - } - } - - return Response({ - 'code': 0, - 'msg': 'success', - 'data': data - }) - - except Exception as e: - return Response({ - 'code': 99, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -class AdminUploadImageView(APIView): - """ - 管理员上传配置图片接口 - URL: /peizhi/adxgtp - 方法: POST - 权限: JWT Token认证 + 管理员权限 - 参数: zhanghao, leixing (1:打手规则图片, 2:默认头像图片), image (文件) - """ - permission_classes = [permissions.IsAuthenticated] - parser_classes = (MultiPartParser, FormParser) - - def post(self, request): - try: - zhanghao = request.data.get('zhanghao') - leixing = request.data.get('leixing') - image_file = request.FILES.get('image') - - if not zhanghao: - return Response({ - 'code': 1, - 'msg': '账号不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if not leixing: - return Response({ - 'code': 2, - 'msg': '图片类型不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if not image_file: - return Response({ - 'code': 3, - 'msg': '请选择图片文件', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证用户权限 - user_main = request.user - if user_main.phone != zhanghao: - return Response({ - 'code': 1, - 'msg': '账号不匹配', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - if user_main.user_type != 'admin': - return Response({ - 'code': 1, - 'msg': '无权限', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - try: - admin_profile = user_main.admin_profile - except AdminProfile.DoesNotExist: - return Response({ - 'code': 1, - 'msg': '无权限', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 验证图片类型 - if leixing not in ['1', '2']: - return Response({ - 'code': 4, - 'msg': '图片类型错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证图片文件 - is_valid, error_msg = validate_image(image_file) - if not is_valid: - return Response({ - 'code': 5, - 'msg': f'图片验证失败: {error_msg}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 根据类型确定固定相对URL和文件名 - if leixing == '1': - # 打手规则图片 - fixed_relative_url = 'a_long/dashouguize.jpg' - oss_file_name = 'dashouguize.jpg' - else: - # 默认头像图片 - fixed_relative_url = 'a_long/morentouxiang.jpg' - oss_file_name = 'morentouxiang.jpg' - - # 构建OSS文件路径 - oss_file_path = fixed_relative_url # a_long/dashouguize.jpg 或 a_long/morentouxiang.jpg - - # 上传到OSS(覆盖原有文件) - image_file.seek(0) # 重置文件指针 - full_url = upload_to_oss(image_file, oss_file_path) - - if not full_url: - return Response({ - 'code': 6, - 'msg': '图片上传到存储桶失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 更新数据库 - with transaction.atomic(): - # 获取或创建图片配置记录 - tupian_peizhi, created = Tupianpeizhi.objects.get_or_create(leixing=int(leixing)) - tupian_peizhi.tupian = fixed_relative_url - tupian_peizhi.save() - - return Response({ - 'code': 0, - 'msg': '图片上传成功', - 'data': { - 'image_url': fixed_relative_url - } - }) - - except Exception as e: - return Response({ - 'code': 99, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -class AdminUpdateConfigView(APIView): - """ - 管理员更新系统配置接口(QQ群、利率)- 已修复 - URL: /peizhi/adupdate - 方法: POST - 权限: JWT Token认证 + 管理员权限 - 参数: - - zhanghao: 管理员账号 - - qq_groups: [{"peizhiid": 1, "neirong": "", "qunid": "", "jieshao": ""}, ...] (可选) - - rates: {"platform_rate": 0.1, "shop_rate": 0.2} (可选) - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - zhanghao = request.data.get('zhanghao') - if not zhanghao: - return Response({ - 'code': 1, - 'msg': '账号不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证用户权限 - user_main = request.user - if user_main.phone != zhanghao: - return Response({ - 'code': 1, - 'msg': '账号不匹配', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - if user_main.user_type != 'admin': - return Response({ - 'code': 1, - 'msg': '无权限', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - try: - admin_profile = user_main.admin_profile - except AdminProfile.DoesNotExist: - return Response({ - 'code': 1, - 'msg': '无权限', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - response_data = {} - - # 🔴 修复:更新QQ群配置 - 确保根据peizhiid正确更新对应的记录 - qq_groups = request.data.get('qq_groups') - if qq_groups and isinstance(qq_groups, list): - updated_groups = [] - for group_data in qq_groups: - peizhiid = group_data.get('peizhiid') - if not peizhiid: - continue - - # 🔴 修复:使用filter()确保查询到正确的记录 - qun_configs = Qunpeizhi.objects.filter(peizhiid=peizhiid) - - if qun_configs.exists(): - # 如果存在多个相同peizhiid的记录,取第一个(应该只有一个) - qun_config = qun_configs.first() - - # 更新字段 - neirong = group_data.get('neirong') - qunid = group_data.get('qunid') - jieshao = group_data.get('jieshao') - - if neirong is not None: - qun_config.neirong = neirong or '' - if qunid is not None: - qun_config.qunid = qunid or '' - if jieshao is not None: - qun_config.jieshao = jieshao or '' - - qun_config.save() - else: - # 如果不存在,创建新的(使用提供的peizhiid) - qun_config = Qunpeizhi.objects.create( - peizhiid=peizhiid, - neirong=group_data.get('neirong', '') or '', - qunid=group_data.get('qunid', '') or '', - jieshao=group_data.get('jieshao', '') or '' - ) - - updated_groups.append({ - 'peizhiid': qun_config.peizhiid, - 'neirong': qun_config.neirong, - 'qunid': qun_config.qunid, - 'jieshao': qun_config.jieshao - }) - - if updated_groups: - response_data['qq_groups'] = updated_groups - - # 🔴 修复:更新利率配置 - 确保更新现有记录而不是创建新记录 - rates = request.data.get('rates') - if rates and isinstance(rates, dict): - updated_rates = {} - - # 平台发单利率(类型1,fadanpingtai='1') - platform_rate = rates.get('platform_rate') - if platform_rate is not None: - try: - platform_rate_float = float(platform_rate) - if 0 <= platform_rate_float <= 1: - # 🔴 修复:查询现有记录,如果不存在则创建 - platform_rate_obj = Lilubiao.objects.filter(fadanpingtai='1').first() - - if platform_rate_obj: - # 更新现有记录 - platform_rate_obj.lilu = platform_rate_float - platform_rate_obj.save() - else: - # 创建新记录 - platform_rate_obj = Lilubiao.objects.create( - lilu=platform_rate_float, - fadanpingtai='1' - ) - - updated_rates['platform_rate'] = platform_rate_float - else: - return Response({ - 'code': 3, - 'msg': '平台发单利率必须在0-1之间', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - except ValueError: - return Response({ - 'code': 4, - 'msg': '平台发单利率格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 商家发单利率(类型2,fadanpingtai='2') - shop_rate = rates.get('shop_rate') - if shop_rate is not None: - try: - shop_rate_float = float(shop_rate) - if 0 <= shop_rate_float <= 1: - # 🔴 修复:查询现有记录,如果不存在则创建 - shop_rate_obj = Lilubiao.objects.filter(fadanpingtai='3').first() - - if shop_rate_obj: - # 更新现有记录 - shop_rate_obj.lilu = shop_rate_float - shop_rate_obj.save() - else: - # 创建新记录 - shop_rate_obj = Lilubiao.objects.create( - lilu=shop_rate_float, - fadanpingtai='3' - ) - - updated_rates['shop_rate'] = shop_rate_float - else: - return Response({ - 'code': 5, - 'msg': '商家发单利率必须在0-1之间', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - except ValueError: - return Response({ - 'code': 6, - 'msg': '商家发单利率格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - - - - # 🔴【新增】打手提现抽成利率(类型5,fadanpingtai='5') - dashou_withdraw_rate = rates.get('dashou_withdraw_rate') - if dashou_withdraw_rate is not None: - try: - dashou_withdraw_rate_float = float(dashou_withdraw_rate) - if 0 <= dashou_withdraw_rate_float <= 1: - # 查询现有记录,如果不存在则创建 - dashou_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='5').first() - - if dashou_withdraw_obj: - # 更新现有记录 - dashou_withdraw_obj.lilu = dashou_withdraw_rate_float - dashou_withdraw_obj.save() - else: - # 创建新记录 - dashou_withdraw_obj = Lilubiao.objects.create( - lilu=dashou_withdraw_rate_float, - fadanpingtai='5' - ) - - updated_rates['dashou_withdraw_rate'] = dashou_withdraw_rate_float - else: - return Response({ - 'code': 7, # 注意:这里需要调整错误码,不要重复 - 'msg': '打手提现抽成利率必须在0-1之间', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - except ValueError: - return Response({ - 'code': 8, - 'msg': '打手提现抽成利率格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 🔴【新增】管事提现抽成利率(类型6,fadanpingtai='6') - guanshi_withdraw_rate = rates.get('guanshi_withdraw_rate') - if guanshi_withdraw_rate is not None: - try: - guanshi_withdraw_rate_float = float(guanshi_withdraw_rate) - if 0 <= guanshi_withdraw_rate_float <= 1: - # 查询现有记录,如果不存在则创建 - guanshi_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='6').first() - - if guanshi_withdraw_obj: - # 更新现有记录 - guanshi_withdraw_obj.lilu = guanshi_withdraw_rate_float - guanshi_withdraw_obj.save() - else: - # 创建新记录 - guanshi_withdraw_obj = Lilubiao.objects.create( - lilu=guanshi_withdraw_rate_float, - fadanpingtai='6' - ) - - updated_rates['guanshi_withdraw_rate'] = guanshi_withdraw_rate_float - else: - return Response({ - 'code': 9, - 'msg': '管事提现抽成利率必须在0-1之间', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - except ValueError: - return Response({ - 'code': 10, - 'msg': '管事提现抽成利率格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if updated_rates: - response_data['rates'] = updated_rates - - # 如果没有任何更新 - if not response_data: - return Response({ - 'code': 7, - 'msg': '没有需要更新的数据', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - return Response({ - 'code': 0, - 'msg': '更新成功', - 'data': response_data - }) - - except Exception as e: - return Response({ - 'code': 99, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - -'''class AdminUpdateConfigView(APIView): - """ - 管理员更新系统配置接口(QQ群、利率) - URL: /peizhi/adupdate - 方法: POST - 权限: JWT Token认证 + 管理员权限 - 参数: - - zhanghao: 管理员账号 - - qq_groups: [{"peizhiid": 1, "neirong": "", "qunid": "", "jieshao": ""}, ...] (可选) - - rates: {"platform_rate": 0.1, "shop_rate": 0.2} (可选) - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - zhanghao = request.data.get('zhanghao') - if not zhanghao: - return Response({ - 'code': 1, - 'msg': '账号不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证用户权限 - user_main = request.user - if user_main.phone != zhanghao: - return Response({ - 'code': 1, - 'msg': '账号不匹配', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - if user_main.user_type != 'admin': - return Response({ - 'code': 1, - 'msg': '无权限', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - try: - admin_profile = user_main.admin_profile - except AdminProfile.DoesNotExist: - return Response({ - 'code': 1, - 'msg': '无权限', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - response_data = {} - - # 更新QQ群配置 - qq_groups = request.data.get('qq_groups') - if qq_groups and isinstance(qq_groups, list): - updated_groups = [] - for group_data in qq_groups: - peizhiid = group_data.get('peizhiid') - if not peizhiid: - continue - - # 查找配置 - qun_config = Qunpeizhi.objects.filter(peizhiid=peizhiid).first() - if not qun_config: - # 如果不存在,创建新的 - qun_config = Qunpeizhi(peizhiid=peizhiid) - - # 更新字段(peizhiid不可改) - qun_config.neirong = group_data.get('neirong', qun_config.neirong) or '' - qun_config.qunid = group_data.get('qunid', qun_config.qunid) or '' - qun_config.jieshao = group_data.get('jieshao', qun_config.jieshao) or '' - qun_config.save() - - updated_groups.append({ - 'peizhiid': qun_config.peizhiid, - 'neirong': qun_config.neirong, - 'qunid': qun_config.qunid, - 'jieshao': qun_config.jieshao - }) - - if updated_groups: - response_data['qq_groups'] = updated_groups - - # 更新利率配置 - rates = request.data.get('rates') - if rates and isinstance(rates, dict): - updated_rates = {} - - # 平台发单利率(类型1) - platform_rate = rates.get('platform_rate') - if platform_rate is not None: - try: - platform_rate_float = float(platform_rate) - if 0 <= platform_rate_float <= 1: - platform_rate_obj = Lilubiao.objects.filter(fadanpingtai='1').first() - if not platform_rate_obj: - platform_rate_obj = Lilubiao.objects.create( - lilu=platform_rate_float, - fadanpingtai='1' - ) - else: - platform_rate_obj.lilu = platform_rate_float - platform_rate_obj.save() - - updated_rates['platform_rate'] = platform_rate_float - else: - return Response({ - 'code': 3, - 'msg': '平台发单利率必须在0-1之间', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - except ValueError: - return Response({ - 'code': 4, - 'msg': '平台发单利率格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 商家发单利率(类型2) - shop_rate = rates.get('shop_rate') - if shop_rate is not None: - try: - shop_rate_float = float(shop_rate) - if 0 <= shop_rate_float <= 1: - shop_rate_obj = Lilubiao.objects.filter(fadanpingtai='2').first() - if not shop_rate_obj: - shop_rate_obj = Lilubiao.objects.create( - lilu=shop_rate_float, - fadanpingtai='3' - ) - else: - shop_rate_obj.lilu = shop_rate_float - shop_rate_obj.save() - - updated_rates['shop_rate'] = shop_rate_float - else: - return Response({ - 'code': 5, - 'msg': '商家发单利率必须在0-1之间', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - except ValueError: - return Response({ - 'code': 6, - 'msg': '商家发单利率格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if updated_rates: - response_data['rates'] = updated_rates - - # 如果没有任何更新 - if not response_data: - return Response({ - 'code': 7, - 'msg': '没有需要更新的数据', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - return Response({ - 'code': 0, - 'msg': '更新成功', - 'data': response_data - }) - - except Exception as e: - return Response({ - 'code': 99, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' - -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from django.db.models import Q -from decimal import Decimal -import logging - -from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia, UserGuanshi - -logger = logging.getLogger(__name__) - - -class DashouPaihangView(APIView): - """ - 排行榜接口 - 路径:/peizhi/dsph - 方法:POST - 认证:JWT Token认证 - - 请求参数: - - leixing: 1=今日收入,2=今月收入 - - yonghu_leixing: 1=打手,2=管事,3=商家 - - 返回数据: - - code: 0=成功,其他=失败 - - msg: 提示信息 - - data: { - xianshi: True/False, # 是否展示真实数据 - paihang_list: [ # 排行榜列表 - { - uid: '用户ID', - touxiang: '头像相对URL', - nicheng: '用户昵称', - jine: 收入金额, - mingci: 排名 - }, - ... - ] - } - """ - - # =============== 可配置参数 =============== - # 每次查询返回的最大记录数(可修改) - ZUI_DAO_CHA_XUN_SHU_LIANG = 50 - - # 最少需要多少人才能展示(可修改) - ZUI_SHAO_REN_SHU_YAO_QIU = 1 - - # 第一名最低金额要求(单位:元)(可修改) - DI_YI_MING_ZUI_DI_JIN_E = Decimal('10.00') - - # 管事第一名最低数量要求(可修改) - DI_YI_MING_ZUI_DI_SHU_LIANG = 1 - # =============== 可配置参数结束 =============== - - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 获取请求参数 - 修复参数类型问题 - leixing_str = request.data.get('leixing', '1') - yonghu_leixing_str = request.data.get('yonghu_leixing', '1') - - # 转换为整数 - try: - leixing = int(leixing_str) - except (ValueError, TypeError): - leixing = 1 - - try: - yonghu_leixing = int(yonghu_leixing_str) - except (ValueError, TypeError): - yonghu_leixing = 1 - - # 参数验证 - if leixing not in [1, 2]: - return Response({ - 'code': 400, - 'msg': '参数错误:leixing必须为1(今日)或2(今月)', - 'data': {'xianshi': False} - }) - - if yonghu_leixing not in [1, 2, 3]: - return Response({ - 'code': 400, - 'msg': '参数错误:yonghu_leixing必须为1(打手)、2(管事)或3(商家)', - 'data': {'xianshi': False} - }) - - # 根据用户类型调用不同的查询方法 - if yonghu_leixing == 1: - # 打手排行榜 - paihang_list, xianshi_flag = self.chaxun_dashou_paihang(leixing) - elif yonghu_leixing == 2: - # 管事排行榜 - paihang_list, xianshi_flag = self.chaxun_guanshi_paihang(leixing) - else: - # 商家排行榜 - paihang_list, xianshi_flag = self.chaxun_shangjia_paihang(leixing) - - # 返回结果 - return Response({ - 'code': 0, - 'msg': '获取成功', - 'data': { - 'xianshi': xianshi_flag, - 'paihang_list': paihang_list - } - }) - - except Exception as e: - logger.error(f"排行榜查询失败: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'msg': '服务器内部错误', - 'data': {'xianshi': False} - }) - - def chaxun_dashou_paihang(self, leixing): - """ - 查询打手排行榜 - :param leixing: 1=今日收入,2=今月收入 - :return: (排行榜列表, 是否展示标志) - """ - # 确定查询的字段 - if leixing == 1: - # 今日收入 - shouyi_ziduan = 'jinrishouyi' - else: - # 今月收入 - shouyi_ziduan = 'jinyueshouyi' - - # 查询条件:收入大于0,打手账号状态正常(1=正常) - # 注意:打手表中的zhanghaozhuangtai字段 - chaxun_tiaojian = Q(**{f'{shouyi_ziduan}__gt': 0}) & Q(zhanghaozhuangtai=1) - - # 查询打手扩展表,并关联用户主表 - dashou_list = UserDashou.objects.select_related('user').filter( - chaxun_tiaojian - ).order_by(f'-{shouyi_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] - - # 检查是否满足展示条件 - if len(dashou_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU: - # 人数不足,不展示 - return [], False - - # 检查第一名金额是否达到最低要求 - if leixing == 1: - di_yi_ming_jine = dashou_list[0].jinrishouyi - else: - di_yi_ming_jine = dashou_list[0].jinyueshouyi - - if di_yi_ming_jine < self.DI_YI_MING_ZUI_DI_JIN_E: - # 第一名金额不足,不展示 - return [], False - - # 构建返回数据 - paihang_list = [] - for index, dashou in enumerate(dashou_list): - # 获取用户主表信息 - user_main = dashou.user - - # 打手的昵称直接使用打手扩展表的nicheng字段 - dashou_nicheng = dashou.nicheng or '未设置昵称' - - # 获取头像(相对URL) - touxiang_url = user_main.avatar or '' - - # 获取收入金额 - if leixing == 1: - shouyi_jine = dashou.jinrishouyi - else: - shouyi_jine = dashou.jinyueshouyi - - paihang_list.append({ - 'uid': user_main.yonghuid, - 'touxiang': touxiang_url, - 'nicheng': dashou_nicheng, - 'jine': float(shouyi_jine), # 转换为float方便前端处理 - 'mingci': index + 1 - }) - - return paihang_list, True - - def chaxun_guanshi_paihang(self, leixing): - """ - 查询管事排行榜 - :param leixing: 1=今日,2=今月 - :return: (排行榜列表, 是否展示标志) - """ - # 确定查询的字段 - if leixing == 1: - # 今日充值打手数量 - chaxun_ziduan = 'jinrichongzhi' - else: - # 今月充值打手数量 - chaxun_ziduan = 'jinyuechongzhi' - - # 查询条件:数量大于0,管事状态正常(状态1为正常) - # 注意:管手表中的zhuangtai字段 - chaxun_tiaojian = Q(**{f'{chaxun_ziduan}__gt': 0}) & Q(zhuangtai=1) - - # 查询管事扩展表,并关联用户主表 - guanshi_list = UserGuanshi.objects.select_related('user').filter( - chaxun_tiaojian - ).order_by(f'-{chaxun_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] - - # 检查是否满足展示条件 - if len(guanshi_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU: - # 人数不足,不展示 - return [], False - - # 检查第一名数量是否达到最低要求 - if leixing == 1: - di_yi_ming_shuliang = guanshi_list[0].jinrichongzhi - else: - di_yi_ming_shuliang = guanshi_list[0].jinyuechongzhi - - if di_yi_ming_shuliang < self.DI_YI_MING_ZUI_DI_SHU_LIANG: - # 第一名数量不足,不展示 - return [], False - - # 构建返回数据 - paihang_list = [] - for index, guanshi in enumerate(guanshi_list): - # 获取用户主表信息 - user_main = guanshi.user - - # 管事的昵称需要查询老板扩展表 - boss_profile = UserBoss.objects.filter(user=user_main).first() - guanshi_nicheng = boss_profile.nickname if boss_profile else '未设置昵称' - - # 获取头像(相对URL) - touxiang_url = user_main.avatar or '' - - # 获取数量 - if leixing == 1: - chongzhi_shuliang = guanshi.jinrichongzhi - else: - chongzhi_shuliang = guanshi.jinyuechongzhi - - paihang_list.append({ - 'uid': user_main.yonghuid, - 'touxiang': touxiang_url, - 'nicheng': guanshi_nicheng, - 'jine': float(chongzhi_shuliang), # 这里jine字段实际存储的是数量 - 'mingci': index + 1 - }) - - return paihang_list, True - - def chaxun_shangjia_paihang(self, leixing): - """ - 查询商家排行榜 - :param leixing: 1=今日流水,2=今月流水 - :return: (排行榜列表, 是否展示标志) - """ - # 确定查询的字段 - if leixing == 1: - # 今日流水 - liushui_ziduan = 'jinriliushui' - else: - # 今月流水 - liushui_ziduan = 'jinyueliushui' - - # 查询条件:流水大于0,商家状态正常(状态1为正常) - # 注意:商家表中的zhuangtai字段 - chaxun_tiaojian = Q(**{f'{liushui_ziduan}__gt': 0}) & Q(zhuangtai=1) - - # 查询商家扩展表,并关联用户主表 - shangjia_list = UserShangjia.objects.select_related('user').filter( - chaxun_tiaojian - ).order_by(f'-{liushui_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] - - # 检查是否满足展示条件 - if len(shangjia_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU: - # 人数不足,不展示 - return [], False - - # 检查第一名金额是否达到最低要求 - if leixing == 1: - di_yi_ming_liushui = shangjia_list[0].jinriliushui - else: - di_yi_ming_liushui = shangjia_list[0].jinyueliushui - - if di_yi_ming_liushui < self.DI_YI_MING_ZUI_DI_JIN_E: - # 第一名流水不足,不展示 - return [], False - - # 构建返回数据 - paihang_list = [] - for index, shangjia in enumerate(shangjia_list): - # 获取用户主表信息 - user_main = shangjia.user - - # 商家的昵称需要查询老板扩展表 - boss_profile = UserBoss.objects.filter(user=user_main).first() - shangjia_nicheng = boss_profile.nickname if boss_profile else '未设置昵称' - - # 获取头像(相对URL) - touxiang_url = user_main.avatar or '' - - # 获取流水金额 - if leixing == 1: - liushui_jine = shangjia.jinriliushui - else: - liushui_jine = shangjia.jinyueliushui - - paihang_list.append({ - 'uid': user_main.yonghuid, - 'touxiang': touxiang_url, - 'nicheng': shangjia_nicheng, - 'jine': float(liushui_jine), - 'mingci': index + 1 - }) - - return paihang_list, True - - - - - - - - - - -# views.py -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from django.db import connection -from django.db.models import Q -import time - - -class GetAdminZhanghaoView(APIView): - """ - 获取管理员账号列表接口 - 路径:/peizhi/adzhhq - 方法:POST - 权限:JWT Token + 管理员验证 - 参数: - - zhanghao: 当前登录的管理员账号(字符串) - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 🔥 1. 身份验证(100%按表模型) - user = request.user - - # 获取当前用户的手机号(CharField,字符串类型) - current_phone = getattr(user, 'phone', '') - if not current_phone: - return Response({ - 'code': 401, - 'message': '用户手机号信息不完整', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 检查前端传递的账号是否匹配(都是字符串) - frontend_zhanghao = request.data.get('zhanghao', '').strip() - if frontend_zhanghao != current_phone: - return Response({ - 'code': 403, - 'message': '账号不匹配,无权限访问', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 🔥 2. 验证是否为管理员(user_type是CharField,值是'admin'字符串) - user_type = getattr(user, 'user_type', '') - if user_type != 'admin': - # 检查是否有管理员扩展表 - if not hasattr(user, 'admin_profile'): - return Response({ - 'code': 403, - 'message': '无管理员权限', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 🔥 3. 查询管理员账号列表(全部按字符串查询) - with connection.cursor() as cursor: - # 🔥 查询所有管理员账号(user_type='admin' 字符串) - # 🔥 phone和yonghuid都是CharField,xiugaiid也是CharField - sql = """ - SELECT - um.id, - um.phone, - um.yonghuid, - um.user_type, - COALESCE(COUNT(xjl.id), 0) as jilushuliang - FROM user_main um - LEFT JOIN xiugaijilu xjl ON um.phone = xjl.xiugaiid - WHERE um.user_type = 'admin' - GROUP BY um.id, um.phone, um.yonghuid, um.user_type - ORDER BY um.create_time DESC - """ - - cursor.execute(sql) - results = cursor.fetchall() - - # 🔥 4. 处理查询结果(全部保持字符串类型) - zhanghao_list = [] - for row in results: - zhanghao_list.append({ - #'id': row[0], - 'id': str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串 - 'zhanghao':str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串 - 'yonghuid': str(row[2]) if row[2] else '', # 🔥 保持字符串 - 'user_type': str(row[3]) if row[3] else '', # 🔥 保持字符串 - 'jilushuliang': int(row[4]) # 只有这个是数字 - }) - - return Response({ - 'code': 0, - 'message': '获取成功', - 'data': zhanghao_list - }, status=status.HTTP_200_OK) - - except Exception as e: - print(f"获取管理员账号列表失败: {str(e)}") - import traceback - traceback.print_exc() - return Response({ - 'code': 500, - 'message': '系统错误', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -class GetXiugaiJiluView(APIView): - """ - 获取修改记录列表接口 - 路径:/peizhi/adhqxg - 方法:POST - 权限:JWT Token + 管理员验证 - 参数: - - zhanghao: 当前登录的管理员账号(字符串) - - xiugaizhe_zhanghaoid: 选中的管理员账号ID(字符串,对应phone或yonghuid) - - page: 页码(整数) - - pagesize: 每页条数(整数) - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 🔥 1. 身份验证(100%按表模型) - user = request.user - - # 获取当前用户的手机号(CharField,字符串) - current_phone = getattr(user, 'phone', '') - if not current_phone: - return Response({ - 'code': 401, - 'message': '用户手机号信息不完整', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 检查前端传递的账号是否匹配(字符串比较) - frontend_zhanghao = request.data.get('zhanghao', '') - if not isinstance(frontend_zhanghao, str): - frontend_zhanghao = str(frontend_zhanghao) - frontend_zhanghao = frontend_zhanghao.strip() - - if frontend_zhanghao != current_phone: - return Response({ - 'code': 403, - 'message': '账号不匹配,无权限访问', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 🔥 2. 验证是否为管理员(user_type='admin'字符串) - user_type = getattr(user, 'user_type', '') - if user_type != 'admin': - if not hasattr(user, 'admin_profile'): - return Response({ - 'code': 403, - 'message': '无管理员权限', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 🔥 3. 获取参数(100%按实际类型) - # 🔥 xiugaizhe_zhanghaoid 是字符串(对应xiugaiid字段,CharField) - xiugaizhe_zhanghaoid_param = request.data.get('xiugaizhe_zhanghaoid', '') - - - # 🔥 强制转为字符串(因为数据库是CharField) - if xiugaizhe_zhanghaoid_param is None: - xiugaizhe_zhanghaoid = '' - else: - # 无论如何都转为字符串 - xiugaizhe_zhanghaoid = str(xiugaizhe_zhanghaoid_param).strip() - - - # 🔥 分页参数转为整数 - try: - page = int(request.data.get('page', 1)) - except: - page = 1 - - try: - pagesize = int(request.data.get('pagesize', 10)) - except: - pagesize = 10 - - # 🔥 4. 参数验证 - if not xiugaizhe_zhanghaoid: - return Response({ - 'code': 400, - 'message': '修改者账号ID不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if page < 1: - page = 1 - if pagesize < 1 or pagesize > 50: - pagesize = 10 - - # 🔥 5. 高效查询(100%按表字段类型) - with connection.cursor() as cursor: - # 🔥 5.1 查询总记录数(xiugaiid是CharField,用字符串查询) - count_sql = """ - SELECT COUNT(*) - FROM xiugaijilu - WHERE xiugaiid = %s - """ - cursor.execute(count_sql, [xiugaizhe_zhanghaoid]) - total_count = cursor.fetchone()[0] - - # 🔥 5.2 计算分页 - offset = (page - 1) * pagesize - - # 🔥 5.3 查询当前页数据(使用参数化查询,防止SQL注入) - data_sql = """ - SELECT - xjl.id, - xjl.yonghuid, -- CharField - xjl.xiugaiid, -- CharField - xjl.leixing, -- IntegerField - xjl.xiugaitijiao, -- DecimalField - xjl.xiugaitijiaoq, -- DecimalField - xjl.jifen, -- IntegerField - xjl.yjifen, -- IntegerField - xjl.yajin, -- DecimalField - xjl.yyajin, -- DecimalField - xjl.shangjiayue, -- DecimalField - xjl.shangjiayueq, -- DecimalField - xjl.guanshiyue, -- DecimalField - xjl.guanshiyueq, -- DecimalField - xjl.huiyuants, -- IntegerField - xjl.huiyuan_id, -- CharField - xjl.qitashuoming, -- TextField - xjl.create_time, -- DateTimeField - um.avatar -- CharField - FROM xiugaijilu xjl - LEFT JOIN user_main um ON xjl.yonghuid = um.yonghuid - WHERE xjl.xiugaiid = %s - ORDER BY xjl.create_time DESC - LIMIT %s OFFSET %s - """ - - cursor.execute(data_sql, [xiugaizhe_zhanghaoid, pagesize, offset]) - records = cursor.fetchall() - - # 🔥 6. 处理查询结果(严格按照字段类型处理) - jilu_list = [] - for record in records: - # 获取修改者账号信息(都是字符串) - xiugaizhe_zhanghao = '' - with connection.cursor() as cursor2: - # 🔥 phone和yonghuid都是CharField,用字符串查询 - cursor2.execute( - "SELECT phone, yonghuid FROM user_main WHERE phone = %s OR yonghuid = %s LIMIT 1", - [xiugaizhe_zhanghaoid, xiugaizhe_zhanghaoid] - ) - xiugaizhe_info = cursor2.fetchone() - if xiugaizhe_info: - # 🔥 都是字符串 - xiugaizhe_zhanghao = str(xiugaizhe_info[0]) if xiugaizhe_info[0] else str(xiugaizhe_info[1]) if \ - xiugaizhe_info[1] else xiugaizhe_zhanghaoid - - # 获取会员名称(如果有) - huiyuan_mingzi = '' - if record[15]: # huiyuan_id是CharField - with connection.cursor() as cursor3: - cursor3.execute( - "SELECT jieshao FROM huiyuan WHERE huiyuan_id = %s LIMIT 1", - [str(record[15])] # 🔥 转为字符串查询 - ) - huiyuan_info = cursor3.fetchone() - if huiyuan_info: - huiyuan_mingzi = str(huiyuan_info[0]) if huiyuan_info[0] else '' - - # 🔥 严格按照前端字段名映射,保持正确类型 - jilu_data = { - # 基础字段(字符串) - 'id': record[0], - 'beixiugai_yonghuid': str(record[1]) if record[1] else '', # yonghuid - 'xiugaizhe_id': str(record[2]) if record[2] else '', # xiugaiid - 'xiugaizhe_zhanghao': xiugaizhe_zhanghao, # 字符串 - 'beixiugai_yonghuleixing': record[3], # leixing是整数 - - # 打手字段(Decimal转float,Integer转int) - 'xiugaihou_dashoutixianjine': float(record[4]) if record[4] is not None else 0.00, - 'xiugaiqian_dashoutixianjine': float(record[5]) if record[5] is not None else 0.00, - 'xiugaihou_dashoujifen': int(record[6]) if record[6] is not None else 0, - 'xiugaiqian_dashoujifen': int(record[7]) if record[7] is not None else 0, - 'xiugaihou_dashouyajin': float(record[8]) if record[8] is not None else 0.00, - 'xiugaiqian_dashouyajin': float(record[9]) if record[9] is not None else 0.00, - - # 商家字段 - 'xiugaihou_shangjiayue': float(record[10]) if record[10] is not None else 0.00, - 'xiugaiqian_shangjiayue': float(record[11]) if record[11] is not None else 0.00, - - # 管事字段 - 'xiugaihou_guanshiyue': float(record[12]) if record[12] is not None else 0.00, - 'xiugaiqian_guanshiyue': float(record[13]) if record[13] is not None else 0.00, - - # 会员字段 - 'tianjia_huiyuantianshu': int(record[14]) if record[14] is not None else 0, - 'huiyuan_id': str(record[15]) if record[15] else '', # 字符串 - 'huiyuan_mingzi': huiyuan_mingzi, # 字符串 - - # 其他字段 - 'qita_shuoming': str(record[16]) if record[16] else '', # 字符串 - 'xiugai_time': record[17].strftime('%Y-%m-%d %H:%M:%S') if record[17] else None, - 'touxiang': str(record[18]) if record[18] else '' # avatar是字符串 - } - - jilu_list.append(jilu_data) - - # 🔥 7. 分页控制 - queried_records = len(jilu_list) - has_more = False - - if queried_records == pagesize: - total_queried = (page - 1) * pagesize + queried_records - has_more = total_queried < total_count - - return Response({ - 'code': 0, - 'message': '获取成功', - 'data': { - 'list': jilu_list, - 'zongshuliang': total_count, - 'dangqian_page': page, - 'page_size': pagesize, - 'has_more': has_more, - 'shijichaxundediaoshuliang': queried_records - } - }, status=status.HTTP_200_OK) - - except ValueError as e: - print(f"参数错误: {str(e)}") - return Response({ - 'code': 400, - 'message': '参数错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - except Exception as e: - print(f"获取修改记录失败: {str(e)}") - import traceback - traceback.print_exc() - return Response({ - 'code': 500, - 'message': '系统错误', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - - - - - - - -# views/shangjia_moban.py -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from django.db import transaction -from decimal import Decimal -import re - -from .models import ShangjiaMoban -from shangpin.models import ShangpinLeixing # 假设商品模型路径 - - -'''class ShangjiaTianjiaMobanView(APIView): - """ - 商家添加订单模板接口 - POST /peizhi/sjtjddmb - 权限: JWT认证用户 - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取用户ID - user = request.user - yonghu_id = user.yonghuid - - # 2. 验证并获取请求数据 - leixing_id = request.data.get('shangpinTypeId') - jieshao = request.data.get('jieshao', '').strip() - jiage = request.data.get('jiage', '') - - # 3. 验证必填字段 - if not all([leixing_id, jieshao, jiage]): - return Response({ - 'code': 400, - 'msg': '缺少必填字段', - 'data': {} - }, status=400) - - # 4. 验证价格格式 - try: - jiage_decimal = Decimal(jiage) - if jiage_decimal <= 0 or jiage_decimal > 10000: - return Response({ - 'code': 400, - 'msg': '价格必须在0.1-10000元之间', - 'data': {} - }, status=400) - except (ValueError, TypeError): - return Response({ - 'code': 400, - 'msg': '价格格式错误', - 'data': {} - }, status=400) - - # 5. 验证介绍长度 - if len(jieshao) > 100: - return Response({ - 'code': 400, - 'msg': '订单介绍不能超过100字', - 'data': {} - }, status=400) - - # 6. 验证商品类型是否存在且可用 - try: - # 使用select_related优化查询,只获取需要的字段 - shangpin_obj = ShangpinLeixing.objects.filter( - id=leixing_id, - yaoqiuleixing=1 # 假设1为正常状态 - ).only('id', 'yaoqiuleixing').first() - - if not shangpin_obj: - return Response({ - 'code': 404, - 'msg': '商品类型不存在或已禁用', - 'data': {} - }, status=404) - - # 检查抢单要求类型 - if shangpin_obj.yaoqiuleixing != 1: - return Response({ - 'code': 400, - 'msg': '该商品类型不支持创建模板', - 'data': {} - }, status=400) - - except ShangpinLeixing.DoesNotExist: - return Response({ - 'code': 404, - 'msg': '商品类型不存在', - 'data': {} - }, status=404) - - # 7. 创建模板(使用事务保证数据一致性) - with transaction.atomic(): - # 检查是否已存在相同介绍(根据unique_together约束) - existing_moban = ShangjiaMoban.objects.filter( - yonghu_id=yonghu_id, - leixing_id=leixing_id, - moban_jieshao=jieshao, - #zhuangtai=1 # 只检查正常状态的模板 - ).exists() - - if existing_moban: - return Response({ - 'code': 400, - 'msg': '已存在相同介绍的模板', - 'data': {} - }, status=400) - - # 创建新模板 - moban = ShangjiaMoban.objects.create( - yonghu_id=yonghu_id, - leixing_id=leixing_id, - moban_jieshao=jieshao, - jiage=jiage_decimal, - fabu_shuliang=0, - #zhuangtai=1 - ) - - # 8. 返回成功响应(与前端字段对应) - return Response({ - 'code': 200, - 'msg': '模板创建成功', - 'data': { - 'mobanId': moban.id, # 模板ID - 'shangpinTypeId': leixing_id, # 商品类型ID - 'jieshao': moban.moban_jieshao, # 模板介绍 - 'jiage': str(moban.jiage), # 价格(转为字符串) - 'fabushuliang': moban.fabu_shuliang, # 发布数量 - 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S') # 创建时间 - } - }) - - except Exception as e: - # 记录错误日志 - import logging - logger = logging.getLogger(__name__) - logger.error(f"创建商家模板失败: {str(e)}", exc_info=True) - - return Response({ - 'code': 500, - 'msg': '服务器内部错误', - 'data': {} - }, status=500) - - -# views/shangjia_moban.py -from django.core.paginator import Paginator -from django.db.models import Q - - -class ShangjiaMobanListView(APIView): - """ - 商家订单模板列表接口 - POST /peizhi/sjddmblb - 权限: JWT认证 - 功能: 支持普通分页和搜索筛选 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - yonghu_id = user.yonghuid - - # 获取参数 - leixing_id = request.data.get('shangpinTypeId') - if not leixing_id: - return Response({'code': 400, 'msg': '商品类型ID不能为空', 'data': {}}, status=400) - - get_type = int(request.data.get('getType', 2)) - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('pageSize', 50)) - - # 搜索参数 - keyword = request.data.get('keyword', '') - min_price = request.data.get('minPrice', '') - # 修复 labelId 可能为整数,统一转为整数,避免 .strip() 错误 - label_id = request.data.get('labelId', None) - if label_id is not None: - try: - label_id = int(label_id) - except (ValueError, TypeError): - label_id = None - - # 构建基础查询 - query = Q(yonghu_id=yonghu_id, leixing_id=leixing_id) - - # 搜索模式处理 - if get_type == 1: - if keyword and keyword.strip(): - keyword = keyword.strip() - # 先尝试按模板ID精确搜索,同时按介绍模糊搜索 - try: - moban_id_int = int(keyword) - query &= Q(id=moban_id_int) | Q(moban_jieshao__icontains=keyword) - except ValueError: - query &= Q(moban_jieshao__icontains=keyword) - if min_price: - try: - min_p = Decimal(str(min_price)) - query &= Q(jiage__gte=min_p) - except: - pass - if label_id: - query &= Q(chenghao_id=label_id) - - # 搜索模式不分页,返回所有匹配结果 - moban_list = ShangjiaMoban.objects.filter(query).order_by('-id').values( - 'id', 'leixing_id', 'moban_jieshao', 'jiage', 'fabu_shuliang', - 'yongjin_qiangdan', 'yongjin_jiage', 'chenghao_id', 'create_time' - ) - - # 批量获取称号名称 - chenghao_ids = [m['chenghao_id'] for m in moban_list if m.get('chenghao_id')] - chenghao_map = {} - if chenghao_ids: - chs = Chenghao.objects.filter(id__in=chenghao_ids).values('id', 'mingcheng') - chenghao_map = {ch['id']: ch['mingcheng'] for ch in chs} - - formatted = [] - for item in moban_list: - formatted.append({ - 'mobanId': item['id'], - 'shangpinTypeId': item['leixing_id'], - 'jieshao': item['moban_jieshao'], - 'jiage': str(item['jiage']), - 'fabushuliang': item['fabu_shuliang'], - 'commissionEnabled': item['yongjin_qiangdan'], - 'commissionValue': str(item['yongjin_jiage']) if item['yongjin_jiage'] is not None else None, - 'labelId': item['chenghao_id'], - 'labelName': chenghao_map.get(item['chenghao_id'], ''), - 'createTime': item['create_time'].strftime('%Y-%m-%d %H:%M:%S') if item['create_time'] else '' - }) - - return Response({ - 'code': 200, - 'msg': '搜索成功', - 'data': { - 'list': formatted, - 'total': len(formatted), - 'hasMore': False - } - }) - - # 普通分页模式 - queryset = ShangjiaMoban.objects.filter(query).order_by('-id') - paginator = Paginator(queryset, page_size) - if page < 1 or page > paginator.num_pages: - return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400) - - current_page = paginator.page(page) - moban_list = current_page.object_list.values( - 'id', 'leixing_id', 'moban_jieshao', 'jiage', 'fabu_shuliang', - 'yongjin_qiangdan', 'yongjin_jiage', 'chenghao_id', 'create_time' - ) - - # 批量获取称号名称 - chenghao_ids = [m['chenghao_id'] for m in moban_list if m.get('chenghao_id')] - chenghao_map = {} - if chenghao_ids: - chs = Chenghao.objects.filter(id__in=chenghao_ids).values('id', 'mingcheng') - chenghao_map = {ch['id']: ch['mingcheng'] for ch in chs} - - formatted = [] - for item in moban_list: - formatted.append({ - 'mobanId': item['id'], - 'shangpinTypeId': item['leixing_id'], - 'jieshao': item['moban_jieshao'], - 'jiage': str(item['jiage']), - 'fabushuliang': item['fabu_shuliang'], - 'commissionEnabled': item['yongjin_qiangdan'], - 'commissionValue': str(item['yongjin_jiage']) if item['yongjin_jiage'] is not None else None, - 'labelId': item['chenghao_id'], - 'labelName': chenghao_map.get(item['chenghao_id'], ''), - 'createTime': item['create_time'].strftime('%Y-%m-%d %H:%M:%S') if item['create_time'] else '' - }) - - has_more = current_page.has_next() - # 如果当前页数据不足page_size,强制hasMore=False - if len(formatted) < page_size: - has_more = False - - return Response({ - 'code': 200, - 'msg': '获取成功', - 'data': { - 'list': formatted, - 'total': paginator.count, - 'hasMore': has_more, - 'currentPage': page, - 'pageSize': page_size, - 'totalPages': paginator.num_pages - } - }) - - except Exception as e: - logger.error(f"获取商家模板列表失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) - - -# views/shangjia_moban.py -class ShangjiaGengxinMobanView(APIView): - """ - 商家更新订单模板接口 - POST /peizhi/sjddmbgx - 权限: JWT认证用户 - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取用户ID - user = request.user - yonghu_id = user.yonghuid - - # 2. 获取请求参数 - moban_id = request.data.get('mobanId') - leixing_id = request.data.get('shangpinTypeId') - new_jieshao = request.data.get('jieshao', '').strip() - new_jiage = request.data.get('jiage', '') - - # 3. 验证必填字段 - if not all([moban_id, leixing_id]): - return Response({ - 'code': 400, - 'msg': '模板ID和商品类型ID不能为空', - 'data': {} - }, status=400) - - # 4. 检查是否有修改内容 - if not new_jieshao and not new_jiage: - return Response({ - 'code': 400, - 'msg': '没有修改内容', - 'data': {} - }, status=400) - - # 5. 验证修改内容 - update_fields = {} - - # 验证介绍 - if new_jieshao: - if len(new_jieshao) > 100: - return Response({ - 'code': 400, - 'msg': '订单介绍不能超过100字', - 'data': {} - }, status=400) - update_fields['moban_jieshao'] = new_jieshao - - # 验证价格 - if new_jiage: - try: - jiage_decimal = Decimal(new_jiage) - if jiage_decimal <= 0 or jiage_decimal > 10000: - return Response({ - 'code': 400, - 'msg': '价格必须在0.1-10000元之间', - 'data': {} - }, status=400) - update_fields['jiage'] = jiage_decimal - except (ValueError, TypeError): - return Response({ - 'code': 400, - 'msg': '价格格式错误', - 'data': {} - }, status=400) - - # 6. 查询模板是否存在且属于当前用户 - try: - moban = ShangjiaMoban.objects.get( - id=moban_id, - yonghu_id=yonghu_id, - leixing_id=leixing_id, - #zhuangtai=1 # 只修改正常状态的模板 - ) - except ShangjiaMoban.DoesNotExist: - return Response({ - 'code': 404, - 'msg': '模板不存在或无权修改', - 'data': {} - }, status=404) - - # 7. 检查介绍是否重复(如果修改了介绍) - if 'moban_jieshao' in update_fields: - existing_moban = ShangjiaMoban.objects.filter( - yonghu_id=yonghu_id, - leixing_id=leixing_id, - moban_jieshao=update_fields['moban_jieshao'], - #zhuangtai=1 - ).exclude(id=moban_id).exists() # 排除自身 - - if existing_moban: - return Response({ - 'code': 400, - 'msg': '已存在相同介绍的模板', - 'data': {} - }, status=400) - - # 8. 更新模板(使用update_fields优化,只更新修改的字段) - with transaction.atomic(): - for field, value in update_fields.items(): - setattr(moban, field, value) - moban.save(update_fields=list(update_fields.keys())) - - # 9. 获取更新后的数据 - updated_moban = ShangjiaMoban.objects.filter(id=moban_id).values( - 'id', 'leixing_id', 'moban_jieshao', 'jiage', 'fabu_shuliang', 'update_time' - ).first() - - # 10. 返回成功响应 - return Response({ - 'code': 200, - 'msg': '模板修改成功', - 'data': { - 'mobanId': updated_moban['id'], - 'shangpinTypeId': updated_moban['leixing_id'], - 'jieshao': updated_moban['moban_jieshao'], - 'jiage': str(updated_moban['jiage']), - 'fabushuliang': updated_moban['fabu_shuliang'], - 'updateTime': updated_moban['update_time'].strftime('%Y-%m-%d %H:%M:%S') if updated_moban[ - 'update_time'] else '' - } - }) - - except Exception as e: - # 记录错误日志 - import logging - logger = logging.getLogger(__name__) - logger.error(f"修改商家模板失败: {str(e)}", exc_info=True) - - return Response({ - 'code': 500, - 'msg': '服务器内部错误', - 'data': {} - }, status=500) - - -# views/shangjia_moban.py -# views/shangjia_moban.py -class ShangjiaShanchuMobanView(APIView): - """ - 删除模板 - POST /peizhi/sjddmbsc - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - yonghu_id = user.yonghuid - - moban_id = request.data.get('mobanId') - leixing_id = request.data.get('shangpinTypeId') - - if not all([moban_id, leixing_id]): - return Response({'code': 400, 'msg': '参数缺失', 'data': {}}, status=400) - - try: - moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=yonghu_id, leixing_id=leixing_id) - except ShangjiaMoban.DoesNotExist: - return Response({'code': 404, 'msg': '模板不存在', 'data': {}}, status=404) - - # 强制硬删除 - moban.delete() - - return Response({'code': 200, 'msg': '删除成功', 'data': {'mobanId': moban_id, 'shangpinTypeId': leixing_id}}) - except Exception as e: - import logging - logger = logging.getLogger(__name__) - logger.error(f"删除模板失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) - - -import logging - -from peizhi.models import ShangjiaMoban, ShangjiaLianjie -from shangpin.models import ShangpinLeixing -from dingdan.models import Dingdan, DingdanShangjia -# 假设利率表模型路径(根据你的项目结构调整) -from dingdan.models import Lilubiao - -logger = logging.getLogger(__name__) - -# views/peizhi/sjljhq.py -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework import status -from django.db import transaction -from django.db.models import F -from django.conf import settings -from django.utils import timezone -import time -import random -import os -import logging -import hashlib -import secrets -from decimal import Decimal - -logger = logging.getLogger(__name__) - - -class ShangjiaGenerateLinkView(APIView): - """商家生成发单链接接口""" - permission_classes = [IsAuthenticated] # JWT Token认证 - - def post(self, request): - # 1. 记录请求日志 - logger.info(f"商家生成链接请求 - 用户ID: {request.user.yonghuid}, 数据: {request.data}") - - # 2. 获取前端参数 - data = request.data - shangpin_type_id = data.get('shangpinTypeId') # 商品类型ID - moban_id = data.get('mobanId') # 模板ID - - # 3. 基本参数验证 - if not all([shangpin_type_id, moban_id]): - logger.warning(f"参数缺失 - shangpin_type_id: {shangpin_type_id}, moban_id: {moban_id}") - return Response({ - 'code': 400, - 'msg': '缺少必填参数:订单类型ID和模板ID', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - try: - shangpin_type_id = int(shangpin_type_id) - moban_id = int(moban_id) - except ValueError: - return Response({ - 'code': 400, - 'msg': '参数格式错误,必须是数字', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 获取商家扩展信息(使用反向查询,最高效的方式) - try: - shangjia = request.user.shop_profile - except UserShangjia.DoesNotExist: - logger.error(f"商家扩展信息不存在 - 用户ID: {request.user.yonghuid}") - return Response({ - 'code': 403, - 'msg': '您不是商家或商家信息未完善', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 5. 验证商家状态 - if shangjia.zhuangtai != 1: - logger.warning(f"商家状态异常 - 用户ID: {request.user.yonghuid}, 状态: {shangjia.zhuangtai}") - return Response({ - 'code': 403, - 'msg': '商家账号状态异常,请联系管理员', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 6. 查询商品类型信息 - try: - shangpin_leixing = ShangpinLeixing.objects.get(id=shangpin_type_id) - except ShangpinLeixing.DoesNotExist: - logger.error(f"商品类型不存在 - 类型ID: {shangpin_type_id}") - return Response({ - 'code': 400, - 'msg': '商品类型不存在', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 提取商品类型信息 - yaoqiuleixing = shangpin_leixing.yaoqiuleixing - huiyuan_id = shangpin_leixing.huiyuan_id if shangpin_leixing.huiyuan_id else '' - # yongjin 字段暂时不需要用到 - - # 7. 查询商家订单模板(使用组合查询,最高效) - try: - moban = ShangjiaMoban.objects.get( - yonghu_id=request.user.yonghuid, - leixing_id=shangpin_type_id, - id=moban_id - ) - except ShangjiaMoban.DoesNotExist: - logger.error( - f"商家模板不存在 - 用户ID: {request.user.yonghuid}, 类型ID: {shangpin_type_id}, 模板ID: {moban_id}") - return Response({ - 'code': 400, - 'msg': '订单模板不存在', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 获取模板价格和介绍 - jiage = moban.jiage - dingdan_jieshao = moban.moban_jieshao - - # 8. 验证商家余额 - if jiage > shangjia.yue: - logger.warning(f"商家余额不足 - 用户ID: {request.user.yonghuid}, 余额: {shangjia.yue}, 价格: {jiage}") - return Response({ - 'code': 400, - 'msg': f'余额不足,当前余额{shangjia.yue}元', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 9. 查询利率(复用商家派单接口逻辑) - try: - lilu_obj = Lilubiao.objects.get(fadanpingtai='3') - lilu = float(lilu_obj.lilu) - if lilu <= 0: - lilu = 1 # 默认值 - logger.info(f"获取到利率 - 平台: 3, 利率: {lilu}") - except Lilubiao.DoesNotExist: - logger.warning("利率表不存在,使用默认利率1") - lilu = 1 - - # 10. 计算打手分成 - # 方法1:最安全的方式,兼容所有情况 - dashou_fencheng = Decimal(str(round(float(jiage) * lilu, 2))) - logger.info(f"计算打手分成 - 价格: {jiage}, 利率: {lilu}, 分成: {dashou_fencheng}") - - # 11. 生成订单ID(复用商家派单接口的逻辑) - def generate_dingdan_id(): - """生成唯一订单ID""" - timestamp = int(time.time() * 1000) - process_id = os.getpid() % 1000 - random_num = random.randint(1000, 9999) - return f"SJ{timestamp}{process_id:03d}{random_num}" - - # 🔴🆕【新增】生成安全Token的函数 - def generate_secure_token(dingdan_id, shangjia_id): - """ - 生成安全的Token - 算法:时间戳 + 订单ID + 商家ID + 随机数 → SHA256 → 截取前24位 - """ - timestamp = int(time.time() * 1_000_000) # 纳秒级精度 - random_salt = secrets.token_hex(8) # 16位随机盐值 - - # 组合原始字符串 - raw_string = f"{timestamp}:{dingdan_id}:{shangjia_id}:{random_salt}" - - # 使用SHA256哈希 - hash_obj = hashlib.sha256(raw_string.encode('utf-8')) - token = hash_obj.hexdigest()[:24] # 取前24位作为Token - - # 确保唯一性(理论上极小概率重复,但还是要检查) - try: - if ShangjiaLianjie.objects.filter(lianjie_token=token).exists(): - # 如果重复,重新生成(极低概率) - return generate_secure_token(dingdan_id, shangjia_id) - except: - pass - - return token - - # 🔴🆕【修改】生成链接函数 - 使用Token而不是订单ID - def generate_link_url(token): - """生成安全的H5页面链接 - 使用Token而不是订单ID""" - base_url = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com') - # 确保base_url以/结尾 - if not base_url.endswith('/'): - base_url += '/' - # 🔴🆕【关键修改】生成使用Token的链接 - return f"{base_url}order/{token}" - - # 13. 使用事务确保数据一致性(这是最关键的步骤!) - try: - with transaction.atomic(): - # 生成订单ID - dingdan_id = generate_dingdan_id() - - # 🔴🆕【新增】生成安全Token - secure_token = generate_secure_token(dingdan_id, request.user.yonghuid) - - # 创建订单主表(状态为13:灰度状态,等待老板填写) - dingdan = Dingdan.objects.create( - dingdan_id=dingdan_id, - zhuangtai=13, # 🔴 核心:状态13表示灰度状态,等待老板填写 - fadan_pingtai=2, # 商家发单 - jine=jiage, - dashou_fencheng=dashou_fencheng, - # 以下字段留空,等待老板填写 - jiedan_dashou_id='', - dashou_liuyan='', - zhiding_id='', - shangpin_id=0, # 暂时设为0 - leixing_id=shangpin_type_id, - yaoqiuleixing=yaoqiuleixing if yaoqiuleixing else 1, - huiyuan_id=huiyuan_id, - yongjin=None, # 暂时不需要 - jieshao=dingdan_jieshao[:2000], # 限制长度 - beizhu='', # 备注为空,等待老板填写 - nicheng='', # 🔴 游戏昵称为空,等待老板填写 - tupian=request.user.avatar if request.user.avatar else '', - user1_id=f"Sj{request.user.yonghuid}", # 商家标识 - user2_id='', # 打手标识为空 - ) - logger.info(f"创建订单成功 - 订单ID: {dingdan_id}") - - # 创建商家订单扩展表 - DingdanShangjia.objects.create( - dingdan=dingdan, - shangjia_id=request.user.yonghuid, - sjnicheng=shangjia.nicheng if shangjia.nicheng else f"商家{request.user.yonghuid}", - shangjia_pingjia='', - cfliyou='', - sqzhuangtai=0, - bhliyou='' - ) - logger.info(f"创建商家订单扩展表成功 - 订单ID: {dingdan_id}") - - # 🔴 核心:更新商家统计信息和余额(使用F表达式,避免并发问题) - UserShangjia.objects.filter(id=shangjia.id).update( - fabu=F('fabu') + 1, - yue=F('yue') - jiage, # 🔴 扣除余额 - jinridingdan=F('jinridingdan') + 1, - jinriliushui=F('jinriliushui') + jiage, - jinyuedingdan=F('jinyuedingdan') + 1, - jinyueliushui=F('jinyueliushui') + jiage, - ) - logger.info(f"更新商家统计信息成功 - 用户ID: {request.user.yonghuid}, 扣除金额: {jiage}") - - # 🔴🆕【修改】生成链接 - 使用Token而不是订单ID - link_url = generate_link_url(secure_token) - logger.info(f"生成链接 - Token: {secure_token}, 链接: {link_url}") - - # 🔴🆕【修改】保存链接到商家链接表 - 添加Token字段 - lianjie_obj = ShangjiaLianjie.objects.create( - yonghu_id=request.user.yonghuid, - leixing_id=shangpin_type_id, - moban_id=moban_id, - lianjie=link_url, - lianjie_token=secure_token, # 🔴🆕【新增】保存Token - dingdan_id=dingdan_id, - is_used=False, # 未使用 - youxiao_shijian=timezone.now() + timezone.timedelta(days=1) # 24小时有效 - ) - logger.info(f"保存链接到数据库成功 - 链接ID: {lianjie_obj.id}, Token: {secure_token}") - - # 更新模板发布数量 - ShangjiaMoban.objects.filter(id=moban_id).update( - fabu_shuliang=F('fabu_shuliang') + 1, - ) - logger.info(f"更新模板发布数量成功 - 模板ID: {moban_id}") - - # 重新获取商家最新数据(因为余额已更新) - shangjia.refresh_from_db() - - # 记录成功日志 - logger.info( - f"商家生成链接成功 - 用户ID: {request.user.yonghuid}, 订单ID: {dingdan_id}, Token: {secure_token}, 链接: {link_url}") - - # 14. 返回成功响应(字段必须与前端对应) - return Response({ - 'code': 200, - 'msg': '链接生成成功', - 'data': { - 'linkUrl': link_url, # 🔴 前端需要这个字段 - 'newBalance': str(shangjia.yue), # 🔴 返回新的余额给前端更新 - 'dingdanId': dingdan_id, - 'dingdanJieshao': dingdan_jieshao, - 'jiage': str(jiage), - 'youxiaoShijian': lianjie_obj.youxiao_shijian.strftime( - '%Y-%m-%d %H:%M:%S') if lianjie_obj.youxiao_shijian else '', - # 以下字段可根据前端需要添加 - 'jinriliushui': str(shangjia.jinriliushui), - 'jinyueliushui': str(shangjia.jinyueliushui), - 'jinridingdan': shangjia.jinridingdan, - 'jinyuedingdan': shangjia.jinyuedingdan - } - }, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f"生成链接失败 - 用户ID: {request.user.yonghuid}, 错误: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'msg': '系统错误,生成链接失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' - - - - - - -from django.core.paginator import Paginator -from django.db.models import Q, Prefetch -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from peizhi.models import ShangjiaMoban -from dengji.models import Chenghao - -class ShangjiaMobanListView(APIView): - """ - 商家订单模板列表接口 - POST /peizhi/sjddmblb - 权限: JWT认证 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - yonghu_id = user.yonghuid - - leixing_id = request.data.get('shangpinTypeId') - if not leixing_id: - return Response({'code': 400, 'msg': '商品类型ID不能为空', 'data': {}}, status=400) - - get_type = int(request.data.get('getType', 2)) - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('pageSize', 50)) - - keyword = request.data.get('keyword', '') - min_price = request.data.get('minPrice', '') - label_id = request.data.get('labelId', None) - if label_id is not None: - try: - label_id = int(label_id) - except (ValueError, TypeError): - label_id = None - - query = Q(yonghu_id=yonghu_id, leixing_id=leixing_id) - - if get_type == 1: # 搜索模式 - if keyword: - keyword = str(keyword).strip() - try: - mid = int(keyword) - query &= Q(id=mid) | Q(moban_jieshao__icontains=keyword) - except ValueError: - query &= Q(moban_jieshao__icontains=keyword) - if min_price: - try: - query &= Q(jiage__gte=Decimal(str(min_price))) - except: - pass - if label_id: - query &= Q(chenghao_id=label_id) - - mobans = ShangjiaMoban.objects.filter(query).order_by('-id') - formatted = [] - for moban in mobans: - texiao_json = '' - label_name = '' - if moban.chenghao_id: - try: - ch = Chenghao.objects.get(id=moban.chenghao_id) - label_name = ch.mingcheng - texiao_json = ch.texiao_miaoshu or '' - except Chenghao.DoesNotExist: - pass - formatted.append({ - 'mobanId': moban.id, - 'shangpinTypeId': moban.leixing_id, - 'jieshao': moban.moban_jieshao, - 'jiage': str(moban.jiage), - 'fabushuliang': moban.fabu_shuliang, - 'commissionEnabled': moban.yongjin_qiangdan, - 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None, - 'labelId': moban.chenghao_id, - 'labelName': label_name, - 'texiaoJson': texiao_json, - 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S') if moban.create_time else '' - }) - return Response({'code': 200, 'msg': '搜索成功', 'data': {'list': formatted, 'total': len(formatted), 'hasMore': False}}) - - # 分页模式 - queryset = ShangjiaMoban.objects.filter(query).order_by('-id') - paginator = Paginator(queryset, page_size) - if page < 1 or page > paginator.num_pages: - return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400) - - current_page = paginator.page(page) - formatted = [] - for moban in current_page.object_list: - texiao_json = '' - label_name = '' - if moban.chenghao_id: - try: - ch = Chenghao.objects.get(id=moban.chenghao_id) - label_name = ch.mingcheng - texiao_json = ch.texiao_miaoshu or '' - except Chenghao.DoesNotExist: - pass - formatted.append({ - 'mobanId': moban.id, - 'shangpinTypeId': moban.leixing_id, - 'jieshao': moban.moban_jieshao, - 'jiage': str(moban.jiage), - 'fabushuliang': moban.fabu_shuliang, - 'commissionEnabled': moban.yongjin_qiangdan, - 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None, - 'labelId': moban.chenghao_id, - 'labelName': label_name, - 'texiaoJson': texiao_json, - 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S') if moban.create_time else '' - }) - - has_more = current_page.has_next() - if len(formatted) < page_size: - has_more = False - - return Response({ - 'code': 200, - 'msg': '获取成功', - 'data': { - 'list': formatted, - 'total': paginator.count, - 'hasMore': has_more, - 'currentPage': page, - 'pageSize': page_size, - 'totalPages': paginator.num_pages - } - }) - - except Exception as e: - logger.error(f"获取模板列表失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) - -from dengji.models import Chenghao - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from django.db.models import Prefetch -class ShangjiaTianjiaMobanView(APIView): - """ - 商家添加订单模板接口 - POST /peizhi/sjtjddmb - 权限: JWT认证 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取当前商家用户ID - user = request.user - yonghu_id = user.yonghuid - - # 2. 接收并清洗参数,避免类型错误 - leixing_id = request.data.get('shangpinTypeId') - jieshao = request.data.get('jieshao', '').strip() if request.data.get('jieshao') else '' - jiage = request.data.get('jiage', '') - # 佣金相关 - commission_enabled = request.data.get('commissionEnabled', False) - commission_value = request.data.get('commissionValue', None) - # 标签ID(称号ID) - label_id = request.data.get('labelId', None) - - # 3. 必填验证 - if not leixing_id or not jieshao or not jiage: - logger.warning(f"参数缺失 - leixing_id:{leixing_id}, jieshao:{jieshao}, jiage:{jiage}") - return Response({ - 'code': 400, - 'msg': '缺少必填字段(订单类型、介绍、价格)', - 'data': {} - }, status=400) - - # 4. 价格格式验证 - try: - jiage_decimal = Decimal(str(jiage)) - if jiage_decimal <= 0: - return Response({'code': 400, 'msg': '价格必须大于0', 'data': {}}, status=400) - if jiage_decimal > 10000: - return Response({'code': 400, 'msg': '价格不能超过10000元', 'data': {}}, status=400) - except (InvalidOperation, ValueError, TypeError): - return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400) - - # 5. 介绍长度验证 - if len(jieshao) > 100: - return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400) - - # 6. 佣金验证(若启用) - yongjin_qiangdan = False - yongjin_jiage = None - if commission_enabled: - yongjin_qiangdan = True - if not commission_value: - return Response({'code': 400, 'msg': '开启佣金时,佣金金额不能为空', 'data': {}}, status=400) - try: - comm_decimal = Decimal(str(commission_value)) - if comm_decimal <= 0: - return Response({'code': 400, 'msg': '佣金必须大于0', 'data': {}}, status=400) - yongjin_jiage = comm_decimal - except (InvalidOperation, ValueError, TypeError): - return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400) - - # 7. 标签ID清洗(前端可能传数字或字符串) - cleaned_label_id = None - if label_id is not None and str(label_id).strip(): - try: - cleaned_label_id = int(label_id) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': '标签ID格式错误', 'data': {}}, status=400) - - # 8. 验证商品类型是否存在且可用 - try: - shangpin_obj = ShangpinLeixing.objects.get( - id=leixing_id, - shenhezhuangtai=1 # 审核通过 - ) - except ShangpinLeixing.DoesNotExist: - return Response({'code': 404, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=404) - - # 9. 检查同一用户、同一类型下是否已存在相同介绍的模板 - if ShangjiaMoban.objects.filter( - yonghu_id=yonghu_id, - leixing_id=leixing_id, - moban_jieshao=jieshao - ).exists(): - return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400) - - # 10. 执行数据库创建(事务) - with transaction.atomic(): - moban = ShangjiaMoban.objects.create( - yonghu_id=yonghu_id, - leixing_id=leixing_id, - moban_jieshao=jieshao, - jiage=jiage_decimal, - fabu_shuliang=0, - yongjin_qiangdan=yongjin_qiangdan, - yongjin_jiage=yongjin_jiage, - chenghao_id=cleaned_label_id - ) - - # 11. 返回成功数据(字段与前端对齐) - return Response({ - 'code': 200, - 'msg': '模板创建成功', - 'data': { - 'mobanId': moban.id, - 'shangpinTypeId': leixing_id, - 'jieshao': moban.moban_jieshao, - 'jiage': str(moban.jiage), - 'fabushuliang': moban.fabu_shuliang, - 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S'), - 'commissionEnabled': moban.yongjin_qiangdan, - 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None, - 'labelId': moban.chenghao_id - } - }) - - except Exception as e: - # 打印完整错误堆栈到日志 - logger.error(f"添加模板失败: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'msg': '服务器内部错误,添加模板失败,请联系管理员', - 'data': {} - }, status=500) - - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from peizhi.models import ShangjiaMoban - -class ShangjiaShanchuMobanView(APIView): - """ - 删除模板 - POST /peizhi/sjddmbsc - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - yonghu_id = user.yonghuid - - moban_id = request.data.get('mobanId') - leixing_id = request.data.get('shangpinTypeId') - - if not all([moban_id, leixing_id]): - return Response({'code': 400, 'msg': '参数缺失', 'data': {}}, status=400) - - try: - moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=yonghu_id, leixing_id=leixing_id) - except ShangjiaMoban.DoesNotExist: - return Response({'code': 404, 'msg': '模板不存在', 'data': {}}, status=404) - - # 强制硬删除 - moban.delete() - - return Response({'code': 200, 'msg': '删除成功', 'data': {'mobanId': moban_id, 'shangpinTypeId': leixing_id}}) - except Exception as e: - import logging - logger = logging.getLogger(__name__) - logger.error(f"删除模板失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) - - - - -from decimal import Decimal, InvalidOperation -from django.db import transaction -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from peizhi.models import ShangjiaMoban - -class ShangjiaGengxinMobanView(APIView): - """ - 商家更新订单模板接口 - POST /peizhi/sjddmbgx - 权限: JWT认证 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - yonghu_id = user.yonghuid - - moban_id = request.data.get('mobanId') - leixing_id = request.data.get('shangpinTypeId') - new_jieshao = request.data.get('jieshao', '').strip() - new_jiage = request.data.get('jiage', '').strip() - # 新增字段 - commission_enabled = request.data.get('commissionEnabled') - commission_value = request.data.get('commissionValue') - label_id = request.data.get('labelId') - - if not all([moban_id, leixing_id]): - return Response({'code': 400, 'msg': '模板ID和商品类型ID不能为空', 'data': {}}, status=400) - - # 查询模板 - try: - moban = ShangjiaMoban.objects.get( - id=moban_id, - yonghu_id=yonghu_id, - leixing_id=leixing_id - ) - except ShangjiaMoban.DoesNotExist: - return Response({'code': 404, 'msg': '模板不存在或无权修改', 'data': {}}, status=404) - - update_fields = [] - - # 修改介绍 - if new_jieshao: - if len(new_jieshao) > 100: - return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400) - if new_jieshao != moban.moban_jieshao: - moban.moban_jieshao = new_jieshao - update_fields.append('moban_jieshao') - - # 修改价格 - if new_jiage: - try: - jiage_decimal = Decimal(new_jiage) - if jiage_decimal <= 0 or jiage_decimal > 10000: - return Response({'code': 400, 'msg': '价格必须在0.1-10000元之间', 'data': {}}, status=400) - if jiage_decimal != moban.jiage: - moban.jiage = jiage_decimal - update_fields.append('jiage') - except (InvalidOperation, ValueError, TypeError): - return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400) - - # 修改标签 - if label_id is not None: # 允许传空字符串或不传,传0表示清空 - new_label = int(label_id) if label_id else None - if new_label != moban.chenghao_id: - moban.chenghao_id = new_label - update_fields.append('chenghao_id') - - # 修改佣金设置 - if commission_enabled is not None: - enabled = bool(commission_enabled) - if enabled: - if not commission_value: - return Response({'code': 400, 'msg': '佣金价格不能为空', 'data': {}}, status=400) - try: - comm_decimal = Decimal(commission_value) - if comm_decimal < 0.1 or comm_decimal > 50: - return Response({'code': 400, 'msg': '佣金需在0.1-50元之间', 'data': {}}, status=400) - if moban.yongjin_qiangdan != True or moban.yongjin_jiage != comm_decimal: - moban.yongjin_qiangdan = True - moban.yongjin_jiage = comm_decimal - update_fields.extend(['yongjin_qiangdan', 'yongjin_jiage']) - except (InvalidOperation, ValueError, TypeError): - return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400) - else: - if moban.yongjin_qiangdan != False or moban.yongjin_jiage is not None: - moban.yongjin_qiangdan = False - moban.yongjin_jiage = None - update_fields.extend(['yongjin_qiangdan', 'yongjin_jiage']) - - if not update_fields: - return Response({'code': 400, 'msg': '没有修改内容', 'data': {}}, status=400) - - # 检查介绍唯一性 - if 'moban_jieshao' in update_fields: - if ShangjiaMoban.objects.filter( - yonghu_id=yonghu_id, - leixing_id=leixing_id, - moban_jieshao=moban.moban_jieshao - ).exclude(id=moban_id).exists(): - return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400) - - with transaction.atomic(): - moban.save(update_fields=update_fields) - - # 返回最新数据 - return Response({ - 'code': 200, - 'msg': '模板修改成功', - 'data': { - 'mobanId': moban.id, - 'shangpinTypeId': moban.leixing_id, - 'jieshao': moban.moban_jieshao, - 'jiage': str(moban.jiage), - 'fabushuliang': moban.fabu_shuliang, - 'commissionEnabled': moban.yongjin_qiangdan, - 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage else None, - 'labelId': moban.chenghao_id, - 'updateTime': moban.update_time.strftime('%Y-%m-%d %H:%M:%S') - } - }) - - except Exception as e: - import logging - logger = logging.getLogger(__name__) - logger.error(f"修改商家模板失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) - - - -from dengji.models import DingdanBiaoqian - - - -import random # 必须导入 -import secrets - -import os -import time -import random # 必须导入 -import hashlib -import secrets -import logging -from decimal import Decimal -from django.conf import settings -from django.db import transaction -from django.db.models import F -from django.utils import timezone -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework import status -from yonghu.models import UserShangjia - -from shangpin.models import ShangpinLeixing - -from dengji.models import Chenghao - -logger = logging.getLogger(__name__) - -class ShangjiaGenerateLinkView(APIView): - """商家生成发单链接接口(支持模板佣金/标签)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - # 获取参数 - shangpin_type_id = request.data.get('shangpinTypeId') - moban_id = request.data.get('mobanId') - if not all([shangpin_type_id, moban_id]): - return Response({'code': 400, 'msg': '缺少参数', 'data': {}}, status=400) - - try: - shangpin_type_id = int(shangpin_type_id) - moban_id = int(moban_id) - except ValueError: - return Response({'code': 400, 'msg': '参数格式错误', 'data': {}}, status=400) - - # 商家信息 - try: - shangjia = user.shop_profile - except UserShangjia.DoesNotExist: - return Response({'code': 403, 'msg': '商家信息未完善', 'data': {}}, status=403) - if shangjia.zhuangtai != 1: - return Response({'code': 403, 'msg': '商家状态异常', 'data': {}}, status=403) - - # 商品类型 - try: - shangpin_leixing = ShangpinLeixing.objects.get(id=shangpin_type_id, shenhezhuangtai=1) - except ShangpinLeixing.DoesNotExist: - return Response({'code': 400, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=400) - - # 查询模板 - try: - moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=user.yonghuid, leixing_id=shangpin_type_id) - except ShangjiaMoban.DoesNotExist: - return Response({'code': 400, 'msg': '模板不存在', 'data': {}}, status=400) - - jiage = moban.jiage - if jiage > shangjia.yue: - return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元', 'data': {}}, status=400) - - # 利率 - try: - lilu = float(Lilubiao.objects.get(fadanpingtai='3').lilu) or 1.0 - except Lilubiao.DoesNotExist: - lilu = 1.0 - dashou_fencheng = Decimal(str(round(float(jiage) * lilu, 2))) - - # 抢单要求:根据模板是否启用佣金 - chenghao_id = moban.chenghao_id - if moban.yongjin_qiangdan and moban.yongjin_jiage is not None: - final_yaoqiuleixing = 2 # 佣金模式 - final_yongjin = moban.yongjin_jiage - final_huiyuan_id = '' - else: - final_yaoqiuleixing = shangpin_leixing.yaoqiuleixing or 1 - final_yongjin = None - final_huiyuan_id = shangpin_leixing.huiyuan_id or '' - - # 获取标签名称 - label_name = '' - if chenghao_id: - try: - label_name = Chenghao.objects.get(id=chenghao_id).mingcheng - except Chenghao.DoesNotExist: - pass - - # 生成ID和Token - def generate_dingdan_id(): - timestamp = int(time.time() * 1000) - process_id = os.getpid() % 1000 - random_num = random.randint(1000, 9999) # 注意需要 import random - return f"SJ{timestamp}{process_id:03d}{random_num}" - - def generate_secure_token(dd_id, uid): - timestamp = int(time.time() * 1_000_000) - salt = secrets.token_hex(8) - raw = f"{timestamp}:{dd_id}:{uid}:{salt}" - token = hashlib.sha256(raw.encode()).hexdigest()[:24] - # 确保唯一性(极小概率重复再试一次) - if ShangjiaLianjie.objects.filter(lianjie_token=token).exists(): - return generate_secure_token(dd_id, uid) - return token - - def generate_link_url(token): - base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/') - return f"{base}/order/{token}" - - with transaction.atomic(): - dingdan_id = generate_dingdan_id() - secure_token = generate_secure_token(dingdan_id, user.yonghuid) - - # 创建订单主表 - dingdan = Dingdan.objects.create( - dingdan_id=dingdan_id, - zhuangtai=13, - fadan_pingtai=2, - jine=jiage, - dashou_fencheng=dashou_fencheng, - leixing_id=shangpin_type_id, - yaoqiuleixing=final_yaoqiuleixing, - huiyuan_id=final_huiyuan_id, - yongjin=final_yongjin, - jieshao=moban.moban_jieshao[:2000], - user1_id=f"Sj{user.yonghuid}", - tupian=user.avatar or '', - ) - - # 商家扩展表 - DingdanShangjia.objects.create( - dingdan=dingdan, - shangjia_id=user.yonghuid, - sjnicheng=shangjia.nicheng or f"商家{user.yonghuid}", - ) - - # 关联标签 - if chenghao_id: - try: - ch = Chenghao.objects.get(id=chenghao_id) - DingdanBiaoqian.objects.create(dingdan=dingdan, chenghao=ch) - except Chenghao.DoesNotExist: - logger.warning(f"称号{chenghao_id}不存在,跳过关联") - - # 更新商家余额和统计 - UserShangjia.objects.filter(id=shangjia.id).update( - fabu=F('fabu') + 1, - yue=F('yue') - jiage, - jinridingdan=F('jinridingdan') + 1, - jinriliushui=F('jinriliushui') + jiage, - jinyuedingdan=F('jinyuedingdan') + 1, - jinyueliushui=F('jinyueliushui') + jiage, - ) - - # 生成链接并保存 - link_url = generate_link_url(secure_token) - lianjie_obj = ShangjiaLianjie.objects.create( - yonghu_id=user.yonghuid, - leixing_id=shangpin_type_id, - moban_id=moban_id, - lianjie=link_url, - lianjie_token=secure_token, - dingdan_id=dingdan_id, - is_used=False, - youxiao_shijian=timezone.now() + timezone.timedelta(days=1) - ) - - # 更新模板发布数量 - ShangjiaMoban.objects.filter(id=moban_id).update(fabu_shuliang=F('fabu_shuliang') + 1) - shangjia.refresh_from_db() - - logger.info(f"链接生成成功 - 订单ID: {dingdan_id}, 标签: {label_name}") - - return Response({ - 'code': 200, - 'msg': '链接生成成功', - 'data': { - 'linkUrl': link_url, - 'newBalance': str(shangjia.yue), - 'dingdanId': dingdan_id, - 'dingdanJieshao': moban.moban_jieshao, - 'jiage': str(jiage), - 'labelId': chenghao_id, - 'labelName': label_name, - 'youxiaoShijian': lianjie_obj.youxiao_shijian.strftime('%Y-%m-%d %H:%M:%S'), - } - }) - - except Exception as e: - logger.error(f"生成链接事务失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '系统错误,生成链接失败', 'data': {}}, status=500) - - -from django.core.paginator import Paginator -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from peizhi.models import ShangjiaLianjie - -class ShangjiaLianjieListView(APIView): - """获取已生成的链接列表(支持筛选是否使用)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - yonghu_id = user.yonghuid - - moban_id = request.data.get('mobanId') - used = int(request.data.get('used', 0)) # 0未使用,1已使用 - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('pageSize', 5)) - page_size = min(page_size, 50) - - if not moban_id: - return Response({'code': 400, 'msg': '模板ID不能为空', 'data': {}}, status=400) - - # 校验模板归属 - try: - moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=yonghu_id) - except ShangjiaMoban.DoesNotExist: - return Response({'code': 404, 'msg': '模板不存在或无权查看', 'data': {}}, status=404) - - links = ShangjiaLianjie.objects.filter( - yonghu_id=yonghu_id, - moban_id=moban_id, - is_used=(used == 1) - ).order_by('-create_time') - - paginator = Paginator(links, page_size) - if page < 1 or page > paginator.num_pages: - return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400) - - current_page = paginator.page(page) - data_list = [] - for link in current_page.object_list: - data_list.append({ - 'id': link.id, - 'lianjie': link.lianjie, - 'lianjie_token': link.lianjie_token, - 'is_used': link.is_used, - 'dingdan_id': link.dingdan_id, - 'youxiao_shijian': link.youxiao_shijian.strftime('%Y-%m-%d %H:%M:%S') if link.youxiao_shijian else '', - 'create_time': link.create_time.strftime('%Y-%m-%d %H:%M:%S') - }) - - return Response({ - 'code': 200, - 'msg': '获取成功', - 'data': { - 'list': data_list, - 'total': paginator.count, - 'hasMore': current_page.has_next(), - 'currentPage': page, - 'pageSize': page_size - } - }) - - except Exception as e: - import logging - logger = logging.getLogger(__name__) - logger.error(f"获取链接列表失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) - - - - -# utils/ip_security.py -import logging -import re -from django.utils import timezone -from django.core.cache import cache -from rest_framework.throttling import SimpleRateThrottle - -logger = logging.getLogger(__name__) - - -class IPUtils: - """IP工具类""" - - @staticmethod - def get_client_ip(request): - """ - 获取客户端真实IP,处理代理情况 - 优先级: X-Real-IP > X-Forwarded-For > remote_addr - """ - ip = None - - try: - # 1. 尝试从 X-Real-IP 获取 - x_real_ip = request.META.get('HTTP_X_REAL_IP') - if x_real_ip: - ip = x_real_ip.strip() - logger.info(f"从 X-Real-IP 获取IP: {ip}") - return ip - - # 2. 尝试从 X-Forwarded-For 获取(处理代理链) - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - # 可能有多个代理IP,第一个是真实IP - ips = x_forwarded_for.split(',') - for possible_ip in ips: - possible_ip = possible_ip.strip() - if possible_ip and possible_ip.lower() != 'unknown': - ip = possible_ip - logger.info(f"从 X-Forwarded-For 获取IP: {ip}") - return ip - - # 3. 使用 remote_addr - ip = request.META.get('REMOTE_ADDR', '') - logger.info(f"从 REMOTE_ADDR 获取IP: {ip}") - - except Exception as e: - logger.error(f"获取IP失败: {str(e)}", exc_info=True) - ip = '0.0.0.0' - - return ip - - @staticmethod - def validate_ip(ip): - """验证IP格式是否合法""" - if not ip: - return False - - # IPv4正则 - ipv4_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' - - # IPv6正则(简化版) - ipv6_pattern = r'^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$' - - return bool(re.match(ipv4_pattern, ip) or re.match(ipv6_pattern, ip)) - - @staticmethod - def is_private_ip(ip): - """判断是否为内网IP""" - if not ip: - return False - - # 内网IP段 - private_ranges = [ - ('10.0.0.0', '10.255.255.255'), - ('172.16.0.0', '172.31.255.255'), - ('192.168.0.0', '192.168.255.255'), - ('127.0.0.0', '127.255.255.255'), - ] - - try: - # 将IP转换为整数比较 - def ip_to_int(ip): - parts = ip.split('.') - return (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3]) - - ip_num = ip_to_int(ip) - for start, end in private_ranges: - if ip_to_int(start) <= ip_num <= ip_to_int(end): - return True - except: - pass - - return False - - -class OrderLinkThrottle(SimpleRateThrottle): - """订单链接访问频率限制""" - - scope = 'order_link' - rate = '10/minute' # 每分钟最多10次 - - def get_cache_key(self, request, view): - # 基于IP和接口路径进行限制 - ip = IPUtils.get_client_ip(request) - if not ip or IPUtils.is_private_ip(ip): - # 内网IP或不合法IP,使用默认 - ip = 'default' - - return f'throttle_{view.__class__.__name__}_{ip}' - - -# views/peizhi/khhqddlj.py -import logging -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from django.utils import timezone -from django.db.models import Q -from .models import ShangjiaLianjie -from dingdan.models import Dingdan, DingdanShangjia -from yonghu.models import UserMain, UserShangjia - - -logger = logging.getLogger(__name__) -# views.py - KehuGetDingdanLianjieView 修改部分 - - -from peizhi.models import ClubConfig - - - - -'''class KehuGetDingdanLianjieView(APIView): - """ - 客户获取订单链接信息接口 - 路径:GET /peizhi/khhqddlj?token=xxx - 返回字段包括:订单信息、打手标识、商家标识、俱乐部配置(客服链接、存储桶、GoEasy AppKey等) - """ - - throttle_classes = [OrderLinkThrottle] - permission_classes = [] # 无需认证,任何人都可访问 - - def get(self, request): - """ - 处理GET请求,获取订单链接信息 - """ - # 1. 记录请求开始 - logger.info("=== 客户获取订单链接信息接口开始 ===") - - # 2. 获取客户端真实IP - client_ip = IPUtils.get_client_ip(request) - logger.info(f"客户端IP: {client_ip}") - - # 3. 验证IP合法性 - if not IPUtils.validate_ip(client_ip): - logger.warning(f"IP格式不合法: {client_ip}") - return Response({ - 'code': 400, - 'msg': '网络连接异常', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 获取Token参数 - token = request.query_params.get('token', '') - if not token: - logger.warning(f"Token参数缺失 - IP: {client_ip}") - return Response({ - 'code': 400, - 'msg': '缺少链接参数', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - logger.info(f"接收到Token: {token[:8]}...") - - # 5. 查询链接记录 - try: - lianjie_obj = ShangjiaLianjie.objects.select_related().get(lianjie_token=token) - logger.info(f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}") - except ShangjiaLianjie.DoesNotExist: - logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}") - return Response({ - 'code': 404, - 'msg': '链接不存在或已失效', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - logger.error(f"查询链接异常: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'msg': '系统错误,请稍后重试', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 6. 更新访问者IP(无论是否已使用,都记录最新访问IP) - try: - lianjie_obj.shiyongzhe_ip = client_ip - lianjie_obj.save(update_fields=['shiyongzhe_ip']) - logger.info(f"更新访问者IP成功: {client_ip}") - except Exception as e: - logger.warning(f"更新IP失败: {str(e)}") - # 继续处理,不影响主要逻辑 - - # 7. 检查链接有效期 - """now = timezone.now() - if lianjie_obj.youxiao_shijian and lianjie_obj.youxiao_shijian < now: - logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.youxiao_shijian}, 当前: {now}") - return Response({ - 'code': 400, - 'msg': '链接已过期', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST)""" - - # 8. 准备返回数据 - response_data = { - 'is_used': lianjie_obj.is_used, - 'dingdan_id': lianjie_obj.dingdan_id or '', - 'youxiao_shijian': lianjie_obj.youxiao_shijian.strftime( - '%Y-%m-%d %H:%M:%S') if lianjie_obj.youxiao_shijian else '', - } - - # 9. 获取订单信息 - if lianjie_obj.dingdan_id: - try: - # 查询订单主表 - dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id) - - # 获取商家昵称 - shangjia_nicheng = '' - try: - user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id) - shangjia_obj = user_main.shop_profile - shangjia_nicheng = shangjia_obj.nicheng - except (UserMain.DoesNotExist, UserShangjia.DoesNotExist): - logger.warning(f"商家信息不存在 - 用户ID: {lianjie_obj.yonghu_id}") - - # ========== 新增:确定俱乐部配置 ========== - # 默认头像(相对路径) - default_avatar = 'a_long/morentouxiang.jpg' - # 初始化配置字段默认值 - kefu_link = '' - oss_url = '' - goeasy_appkey = '' - - # 判断是否为跨平台订单 - is_cross = dingdan_obj.is_cross if hasattr(dingdan_obj, 'is_cross') else 0 - - # 默认使用我方配置 - use_self_config = True - target_club_id = None - - if is_cross == 1: - # 跨平台订单 - # 优先使用 partner_club_id - if dingdan_obj.partner_club_id: - target_club_id = dingdan_obj.partner_club_id - use_self_config = False - logger.info(f"跨平台订单,使用对方俱乐部ID: {target_club_id}") - # 其次,如果有对方订单ID但没有俱乐部ID,使用默认俱乐部ID 000001 - elif dingdan_obj.partner_order_id: - target_club_id = '000001' - use_self_config = False - logger.info(f"跨平台订单有对方订单ID但无俱乐部ID,使用默认俱乐部ID: {target_club_id}") - else: - # 无任何对方信息,降级为我方配置 - use_self_config = True - logger.warning(f"订单标记跨平台但无对方俱乐部ID和对方订单ID,使用我方配置") - else: - # 非跨平台订单 - use_self_config = True - - # 获取俱乐部配置 - club_config = None - if use_self_config: - # 查询我方俱乐部配置 (is_self=1) - try: - club_config = ClubConfig.objects.filter(is_self=1).first() - if club_config: - logger.info(f"使用我方俱乐部配置,club_id={club_config.club_id}") - else: - logger.warning("未找到我方俱乐部配置(is_self=1)") - except Exception as e: - logger.error(f"查询我方俱乐部配置异常: {str(e)}", exc_info=True) - else: - # 使用确定的俱乐部ID查询对方配置 - if target_club_id: - try: - club_config = ClubConfig.objects.filter(club_id=target_club_id).first() - if club_config: - logger.info(f"使用对方俱乐部配置,club_id={target_club_id}") - else: - logger.warning(f"未找到对方俱乐部配置,club_id={target_club_id}") - except Exception as e: - logger.error(f"查询对方俱乐部配置异常: {str(e)}", exc_info=True) - - if club_config: - kefu_link = club_config.customer_service_api_url or '' - oss_url = club_config.storage_bucket_domain or '' - goeasy_appkey = club_config.chat_api_key or '' - - # ========== 构建订单信息 ========== - dingdan_info = { - 'jieshao': dingdan_obj.jieshao or '', - 'jiage': str(dingdan_obj.jine) if dingdan_obj.jine is not None else '0.00', # 金额字段名是 jine - 'shangjia_mingcheng': shangjia_nicheng, - 'zhuangtai': dingdan_obj.zhuangtai, - 'is_cross': is_cross, - # 新增配置字段 - 'kefu_link': kefu_link, - 'oss_url': oss_url, - 'default_avatar': default_avatar, - 'goeasy_appkey': goeasy_appkey, - } - - # 如果已使用,返回已填写的信息 - if lianjie_obj.is_used: - dingdan_info.update({ - 'youxi_nicheng': dingdan_obj.nicheng or '', - 'zhiding_dashou': dingdan_obj.zhiding_id or '', - 'beizhu': dingdan_obj.beizhu or '', - }) - - # 获取聊天相关标识 - # 9.1 获取接单打手ID - jiedan_dashou_id = dingdan_obj.jiedan_dashou_id - dashou_biaoshi = f'Ds{jiedan_dashou_id}' if jiedan_dashou_id else '' - - # 9.2 获取商家标识 - shangjia_biaoshi = '' - try: - shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').filter( - dingdan__dingdan_id=lianjie_obj.dingdan_id - ).first() - - if shangjia_kuozhan and shangjia_kuozhan.shangjia_id: - shangjia_biaoshi = f'Sj{shangjia_kuozhan.shangjia_id}' - logger.info(f"生成商家标识: {shangjia_biaoshi}") - else: - logger.info(f"订单 {lianjie_obj.dingdan_id} 没有商家订单扩展信息") - except Exception as e: - logger.error(f"查询商家订单扩展异常: {str(e)}", exc_info=True) - - # 添加聊天标识 - dingdan_info.update({ - 'dashou_biaoshi': dashou_biaoshi, - 'shangjia_biaoshi': shangjia_biaoshi, - 'you_dashou': bool(dashou_biaoshi), - }) - - response_data.update(dingdan_info) - - except Dingdan.DoesNotExist: - logger.error(f"订单不存在 - 订单ID: {lianjie_obj.dingdan_id}") - response_data.update({ - 'jieshao': '', - 'jiage': '0.00', - 'shangjia_mingcheng': '', - 'zhuangtai': None, - 'dashou_biaoshi': '', - 'shangjia_biaoshi': '', - 'you_dashou': False, - 'is_cross': 0, - 'kefu_link': '', - 'oss_url': '', - 'default_avatar': 'a_long/morentouxiang.jpg', - 'goeasy_appkey': '', - }) - except Exception as e: - logger.error(f"获取订单信息异常: {str(e)}", exc_info=True) - response_data.update({ - 'jieshao': '', - 'jiage': '0.00', - 'shangjia_mingcheng': '', - 'zhuangtai': None, - 'dashou_biaoshi': '', - 'shangjia_biaoshi': '', - 'you_dashou': False, - 'is_cross': 0, - 'kefu_link': '', - 'oss_url': '', - 'default_avatar': 'a_long/morentouxiang.jpg', - 'goeasy_appkey': '', - }) - else: - # 如果订单ID为空 - logger.warning(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}") - response_data.update({ - 'jieshao': '', - 'jiage': '0.00', - 'shangjia_mingcheng': '', - 'zhuangtai': None, - 'dashou_biaoshi': '', - 'shangjia_biaoshi': '', - 'you_dashou': False, - 'is_cross': 0, - 'kefu_link': '', - 'oss_url': '', - 'default_avatar': 'a_long/morentouxiang.jpg', - 'goeasy_appkey': '', - }) - - # 10. 判断订单状态是否允许修改 - if not lianjie_obj.is_used and response_data.get('zhuangtai') == 13: - response_data['allow_modify'] = True - else: - response_data['allow_modify'] = False - - # 11. 记录成功日志并返回 - logger.info( - f"返回订单信息成功 - Token: {token[:8]}..., IP: {client_ip}, " - f"是否允许修改: {response_data['allow_modify']}, " - f"是否有打手: {response_data.get('you_dashou', False)}" - ) - - return Response({ - 'code': 200, - 'msg': '成功', - 'data': response_data - }, status=status.HTTP_200_OK)''' - - - - - - - -class KehuGetDingdanLianjieView(APIView): - """ - 客户获取订单链接信息接口 - 路径:GET /peizhi/khhqddlj?token=xxx - 返回字段包括:订单信息、打手标识、商家标识、俱乐部配置(客服链接、存储桶、GoEasy AppKey等) - """ - - throttle_classes = [OrderLinkThrottle] - permission_classes = [] # 无需认证,任何人都可访问 - - def get(self, request): - """ - 处理GET请求,获取订单链接信息 - """ - # 1. 记录请求开始 - logger.info("=== 客户获取订单链接信息接口开始 ===") - - # 2. 获取客户端真实IP - client_ip = IPUtils.get_client_ip(request) - logger.info(f"客户端IP: {client_ip}") - - # 3. 验证IP合法性 - if not IPUtils.validate_ip(client_ip): - logger.warning(f"IP格式不合法: {client_ip}") - return Response({ - 'code': 400, - 'msg': '网络连接异常', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 获取Token参数 - token = request.query_params.get('token', '') - if not token: - logger.warning(f"Token参数缺失 - IP: {client_ip}") - return Response({ - 'code': 400, - 'msg': '缺少链接参数', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - logger.info(f"接收到Token: {token[:8]}...") - - # 5. 查询链接记录 - try: - lianjie_obj = ShangjiaLianjie.objects.select_related().get(lianjie_token=token) - logger.info(f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}") - except ShangjiaLianjie.DoesNotExist: - logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}") - return Response({ - 'code': 404, - 'msg': '链接不存在或已失效', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - logger.error(f"查询链接异常: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'msg': '系统错误,请稍后重试', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 6. 更新访问者IP(无论是否已使用,都记录最新访问IP) - try: - lianjie_obj.shiyongzhe_ip = client_ip - lianjie_obj.save(update_fields=['shiyongzhe_ip']) - logger.info(f"更新访问者IP成功: {client_ip}") - except Exception as e: - logger.warning(f"更新IP失败: {str(e)}") - # 继续处理,不影响主要逻辑 - - # 7. 检查链接有效期 - '''now = timezone.now() - if lianjie_obj.youxiao_shijian and lianjie_obj.youxiao_shijian < now: - logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.youxiao_shijian}, 当前: {now}") - return Response({ - 'code': 400, - 'msg': '链接已过期', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST)''' - - # 8. 准备返回数据 - response_data = { - 'is_used': lianjie_obj.is_used, - 'dingdan_id': lianjie_obj.dingdan_id or '', - 'youxiao_shijian': lianjie_obj.youxiao_shijian.strftime( - '%Y-%m-%d %H:%M:%S') if lianjie_obj.youxiao_shijian else '', - } - - # 9. 获取订单信息 - if lianjie_obj.dingdan_id: - try: - # 查询订单主表 - dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id) - - # 获取商家昵称 - shangjia_nicheng = '' - try: - user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id) - shangjia_obj = user_main.shop_profile - shangjia_nicheng = shangjia_obj.nicheng - except (UserMain.DoesNotExist, UserShangjia.DoesNotExist): - logger.warning(f"商家信息不存在 - 用户ID: {lianjie_obj.yonghu_id}") - - # ========== 新增:确定俱乐部配置 ========== - # 默认头像(相对路径) - default_avatar = 'a_long/morentouxiang.jpg' - # 初始化配置字段默认值 - kefu_link = '' - oss_url = '' - goeasy_appkey = '' - - # 判断是否为跨平台订单(保留,但不影响配置选择) - is_cross = dingdan_obj.is_cross if hasattr(dingdan_obj, 'is_cross') else 0 - - # 始终使用我方俱乐部配置 - try: - club_config = ClubConfig.objects.filter(is_self=1).first() - if club_config: - logger.info(f"使用我方俱乐部配置,club_id={club_config.club_id}") - kefu_link = club_config.customer_service_api_url or '' - oss_url = club_config.storage_bucket_domain or '' - goeasy_appkey = club_config.chat_api_key or '' - else: - logger.warning("未找到我方俱乐部配置(is_self=1)") - except Exception as e: - logger.error(f"查询我方俱乐部配置异常: {str(e)}", exc_info=True) - - # ========== 构建订单信息 ========== - dingdan_info = { - 'jieshao': dingdan_obj.jieshao or '', - 'jiage': str(dingdan_obj.jine) if dingdan_obj.jine is not None else '0.00', # 金额字段名是 jine - 'shangjia_mingcheng': shangjia_nicheng, - 'zhuangtai': dingdan_obj.zhuangtai, - 'is_cross': is_cross, - # 新增配置字段 - 'kefu_link': kefu_link, - 'oss_url': oss_url, - 'default_avatar': default_avatar, - 'goeasy_appkey': goeasy_appkey, - # 🆕 前端用于连接GoEasy的自身用户ID(固定B前缀+商家ID) - 'self_user_id': f"B{lianjie_obj.yonghu_id}" if lianjie_obj.yonghu_id else '', - } - - # 如果已使用,返回已填写的信息 - if lianjie_obj.is_used: - dingdan_info.update({ - 'youxi_nicheng': dingdan_obj.nicheng or '', - 'zhiding_dashou': dingdan_obj.zhiding_id or '', - 'beizhu': dingdan_obj.beizhu or '', - }) - - # 获取聊天相关标识 - # 9.1 获取接单打手ID - jiedan_dashou_id = dingdan_obj.jiedan_dashou_id - dashou_biaoshi = f'Ds{jiedan_dashou_id}' if jiedan_dashou_id else '' - - # 9.2 获取商家标识 - shangjia_biaoshi = '' - try: - shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').filter( - dingdan__dingdan_id=lianjie_obj.dingdan_id - ).first() - - if shangjia_kuozhan and shangjia_kuozhan.shangjia_id: - shangjia_biaoshi = f'Sj{shangjia_kuozhan.shangjia_id}' - logger.info(f"生成商家标识: {shangjia_biaoshi}") - else: - logger.info(f"订单 {lianjie_obj.dingdan_id} 没有商家订单扩展信息") - except Exception as e: - logger.error(f"查询商家订单扩展异常: {str(e)}", exc_info=True) - - # 添加聊天标识 - dingdan_info.update({ - 'dashou_biaoshi': dashou_biaoshi, - 'shangjia_biaoshi': shangjia_biaoshi, - 'you_dashou': bool(dashou_biaoshi), - }) - - response_data.update(dingdan_info) - - except Dingdan.DoesNotExist: - logger.error(f"订单不存在 - 订单ID: {lianjie_obj.dingdan_id}") - response_data.update({ - 'jieshao': '', - 'jiage': '0.00', - 'shangjia_mingcheng': '', - 'zhuangtai': None, - 'dashou_biaoshi': '', - 'shangjia_biaoshi': '', - 'you_dashou': False, - 'is_cross': 0, - 'kefu_link': '', - 'oss_url': '', - 'default_avatar': 'a_long/morentouxiang.jpg', - 'goeasy_appkey': '', - 'self_user_id': '', - }) - except Exception as e: - logger.error(f"获取订单信息异常: {str(e)}", exc_info=True) - response_data.update({ - 'jieshao': '', - 'jiage': '0.00', - 'shangjia_mingcheng': '', - 'zhuangtai': None, - 'dashou_biaoshi': '', - 'shangjia_biaoshi': '', - 'you_dashou': False, - 'is_cross': 0, - 'kefu_link': '', - 'oss_url': '', - 'default_avatar': 'a_long/morentouxiang.jpg', - 'goeasy_appkey': '', - 'self_user_id': '', - }) - else: - # 如果订单ID为空 - logger.warning(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}") - response_data.update({ - 'jieshao': '', - 'jiage': '0.00', - 'shangjia_mingcheng': '', - 'zhuangtai': None, - 'dashou_biaoshi': '', - 'shangjia_biaoshi': '', - 'you_dashou': False, - 'is_cross': 0, - 'kefu_link': '', - 'oss_url': '', - 'default_avatar': 'a_long/morentouxiang.jpg', - 'goeasy_appkey': '', - 'self_user_id': '', - }) - - # 10. 判断订单状态是否允许修改 - if not lianjie_obj.is_used and response_data.get('zhuangtai') == 13: - response_data['allow_modify'] = True - else: - response_data['allow_modify'] = False - - # 11. 记录成功日志并返回 - logger.info( - f"返回订单信息成功 - Token: {token[:8]}..., IP: {client_ip}, " - f"是否允许修改: {response_data['allow_modify']}, " - f"是否有打手: {response_data.get('you_dashou', False)}" - ) - - return Response({ - 'code': 200, - 'msg': '成功', - 'data': response_data - }, status=status.HTTP_200_OK) - - - - - -import threading -from utils.weixin_broadcast import WeixinBroadcastSender -from dingdan.utils import sync_order_to_partners -logger = logging.getLogger(__name__) - - - -class KehuTianxieDingdanView(APIView): - """ - 客户填写订单信息接口 - 路径:POST /peizhi/kehutx - """ - - # 添加频率限制(比查询接口更严格) - throttle_classes = [OrderLinkThrottle] - permission_classes = [] # 无需认证 - - def post(self, request): - """ - 处理POST请求,填写订单信息 - """ - # 1. 记录请求开始 - logger.info("=== 客户填写订单信息接口开始 ===") - - # 2. 获取客户端真实IP - client_ip = IPUtils.get_client_ip(request) - logger.info(f"客户端IP: {client_ip}") - - # 3. 验证IP合法性 - if not IPUtils.validate_ip(client_ip): - logger.warning(f"IP格式不合法: {client_ip}") - return Response({ - 'code': 400, - 'msg': '网络连接异常', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 获取请求参数 - data = request.data - token = data.get('token', '') - youxi_nicheng = data.get('youxi_nicheng', '').strip() - zhiding_dashou = data.get('zhiding_dashou', '').strip() # 可选 - beizhu = data.get('beizhu', '').strip() # 可选 - - - # 4.0 临时关闭指定功能(如需开启,请删除或注释掉下面两行) - #if zhiding_dashou: - #return Response({'code': 8, 'msg': '指定功能尚未开启,请不要填写指定打手ID', 'data': None}) - #if beizhu: - #return Response({'code': 8, 'msg': '备注功能尚未开启,请不要填写备注', 'data': None}) - - - logger.info( - f"请求参数 - Token: {token[:8]}..., 昵称: {youxi_nicheng[:10]}..., 指定打手: {zhiding_dashou}, 备注长度: {len(beizhu)}") - - # 5. 验证必填参数 - if not token: - logger.warning(f"Token参数缺失 - IP: {client_ip}") - return Response({ - 'code': 400, - 'msg': '缺少链接参数', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if not youxi_nicheng: - logger.warning(f"游戏昵称为空 - IP: {client_ip}") - return Response({ - 'code': 400, - 'msg': '游戏昵称不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 6. 验证游戏昵称格式 - if len(youxi_nicheng) > 50: - logger.warning(f"游戏昵称过长 - IP: {client_ip}, 长度: {len(youxi_nicheng)}") - return Response({ - 'code': 400, - 'msg': '游戏昵称不能超过50个字符', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 过滤危险字符 - danger_chars = ['<', '>', "'", '"', '&', ';', '|'] - for char in danger_chars: - if char in youxi_nicheng: - logger.warning(f"游戏昵称包含危险字符 - IP: {client_ip}, 字符: {char}") - return Response({ - 'code': 400, - 'msg': '昵称包含非法字符', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 7. 验证指定打手ID格式(如果提供) - if zhiding_dashou and len(zhiding_dashou) > 32: - logger.warning(f"指定打手ID过长 - IP: {client_ip}, 长度: {len(zhiding_dashou)}") - return Response({ - 'code': 400, - 'msg': '指定打手ID不能超过32个字符', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 8. 验证备注长度 - if len(beizhu) > 500: - logger.warning(f"备注过长 - IP: {client_ip}, 长度: {len(beizhu)}") - return Response({ - 'code': 400, - 'msg': '备注不能超过500个字符', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 9. 查询链接记录 - try: - lianjie_obj = ShangjiaLianjie.objects.get(lianjie_token=token) - logger.info( - f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}, 是否已使用: {lianjie_obj.is_used}") - except ShangjiaLianjie.DoesNotExist: - logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}") - return Response({ - 'code': 404, - 'msg': '链接不存在或已失效', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 10. 检查链接有效期 - now = timezone.now() - if lianjie_obj.youxiao_shijian and lianjie_obj.youxiao_shijian < now: - logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.youxiao_shijian}") - return Response({ - 'code': 400, - 'msg': '链接已过期', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 11. 检查链接是否已使用 - if lianjie_obj.is_used: - logger.warning(f"链接已使用 - 链接ID: {lianjie_obj.id}, IP: {client_ip}") - return Response({ - 'code': 400, - 'msg': '链接已被使用,无法重复提交', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 12. 验证指定打手(如果需要) - zhuangtai_xin = 1 # 默认普通订单状态 - if zhiding_dashou: - # 验证打手是否存在且有效 - try: - # 先查询用户主表 - dashou_user = UserMain.objects.get(yonghuid=zhiding_dashou) - logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}") - - # 通过反向关系获取打手扩展信息 - try: - dashou_profile = dashou_user.dashou_profile - - # 检查打手状态(zhuangtai) - if dashou_profile.zhuangtai != 1: - logger.warning(f"打手状态异常 - 打手ID: {zhiding_dashou}, 状态: {dashou_profile.zhuangtai}") - return Response({ - 'code': 400, - 'msg': '指定打手当前无法接单', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 检查打手账号状态(zhanghaozhuangtai) - if dashou_profile.zhanghaozhuangtai != 1: - logger.warning( - f"打手账号状态异常 - 打手ID: {zhiding_dashou}, 账号状态: {dashou_profile.zhanghaozhuangtai}") - return Response({ - 'code': 400, - 'msg': '指定打手账号异常', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证通过,设置为指定订单状态 - zhuangtai_xin = 7 # 指定打手订单状态 - logger.info(f"指定打手验证通过 - 打手ID: {zhiding_dashou}, 昵称: {dashou_profile.nicheng}") - - except UserDashou.DoesNotExist: - logger.warning(f"打手扩展信息不存在 - 用户ID: {zhiding_dashou}") - return Response({ - 'code': 400, - 'msg': '指定打手信息不完整', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - except UserMain.DoesNotExist: - logger.warning(f"指定打手用户不存在 - 用户ID: {zhiding_dashou}") - return Response({ - 'code': 400, - 'msg': '指定打手不存在', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 13. 使用事务确保数据一致性 - try: - with transaction.atomic(): - # 更新链接记录 - lianjie_obj.shiyongzhe_ip = client_ip - lianjie_obj.is_used = True - lianjie_obj.save(update_fields=['shiyongzhe_ip', 'is_used']) - logger.info(f"更新链接记录成功 - 链接ID: {lianjie_obj.id}, 设置为已使用") - - # 查询订单 - if not lianjie_obj.dingdan_id: - logger.error(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}") - raise ValueError("订单ID为空") - - dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id) - - # 检查订单状态是否为13(灰色状态) - if dingdan_obj.zhuangtai != 13: - logger.warning( - f"订单状态不允许修改 - 订单ID: {dingdan_obj.dingdan_id}, 当前状态: {dingdan_obj.zhuangtai}") - # 回滚事务 - transaction.set_rollback(True) - return Response({ - 'code': 400, - 'msg': '订单状态异常,无法修改', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 更新订单信息 - dingdan_obj.nicheng = youxi_nicheng # 游戏昵称 - dingdan_obj.beizhu = beizhu if beizhu else '' # 备注 - dingdan_obj.zhuangtai = zhuangtai_xin # 新状态:1或7 - - # 如果是指定打手,设置zhiding_id - if zhiding_dashou and zhuangtai_xin == 7: - dingdan_obj.zhiding_id = zhiding_dashou - - dingdan_obj.save() - logger.info( - f"更新订单成功 - 订单ID: {dingdan_obj.dingdan_id}, 新状态: {zhuangtai_xin}, 指定打手: {zhiding_dashou or '无'}") - - # 🔴【添加】在订单更新成功后,异步发送微信服务号通知(原有) - #🟢【7. 新增】支付成功后的广播通知(异步,不阻塞主流程) - from dingdan.tongzhi_tasks import dingdan_guangbo - - - # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用) - order_info = { - 'dingdan_id': dingdan_obj.dingdan_id, - 'game_type': self._get_game_type_name(dingdan_obj.leixing_id), - 'amount': str(dingdan_obj.jine), - 'order_desc': (dingdan_obj.jieshao or '')[:50], - } - dingdan_guangbo.delay(order_info) # 瞬间返回,不阻塞请求 - - # 获取商家信息用于返回 - try: - user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id) - shangjia_obj = user_main.shop_profile - shangjia_mingcheng = shangjia_obj.nicheng - except: - shangjia_mingcheng = f"商家{lianjie_obj.yonghu_id}" - logger.warning(f"获取商家昵称失败,使用默认 - 用户ID: {lianjie_obj.yonghu_id}") - - # ========== 🆕 新增:判断是否需要跨平台同步 ========== - # 获取商家ID(从链接记录) - shangjia_user_id = lianjie_obj.yonghu_id - # 跨平台条件:商家ID在白名单、订单为普通状态(zhuangtai=1)、要求类型为1、会员ID等于配置值 - sync_order_to_partners(dingdan_obj.dingdan_id) - ''' - if (shangjia_user_id in settings.CROSS_PLATFORM_WHITELIST and - dingdan_obj.zhuangtai == 1 and # 非指定单 - dingdan_obj.yaoqiuleixing == 1 and - dingdan_obj.huiyuan_id in ['VC7CTU', 'VUXP8D', 'VEX5NJ'] - ): - - # 异步发送通知(使用独立模块) - import threading # 若文件顶部已导入可省略,此处保留确保独立运行 - from dingdan.cross_platform_tasks import send_partner_sync_order - thread = threading.Thread( - target=send_partner_sync_order, - args=( - dingdan_obj.dingdan_id, - str(dingdan_obj.jine), - str(dingdan_obj.dashou_fencheng), - dingdan_obj.jieshao, - dingdan_obj.beizhu, - dingdan_obj.create_time.isoformat(), - dingdan_obj.nicheng - - ) - ) - thread.daemon = True - thread.start() - logger.info(f"跨平台客户填写订单异步任务已启动: {dingdan_obj.dingdan_id}")''' - # ========== 🆕 新增结束 ========== - - # 14. 返回成功响应 - response_data = { - #'dingdan_id': dingdan_obj.dingdan_id, - #'create_time': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S'), - #'shangpin_mingcheng': dingdan_obj.jieshao[:50] if dingdan_obj.jieshao else '', # 截取前50字符 - #'jiage': str(dingdan_obj.jine) if dingdan_obj.jine else '0.00', - 'shangjia_mingcheng': shangjia_mingcheng, - 'youxi_nicheng': youxi_nicheng, - 'zhuangtai': zhuangtai_xin, - 'zhuangtai_text': '普通订单' if zhuangtai_xin == 1 else '指定打手订单' - } - - logger.info(f"订单提交成功 - 订单ID: {dingdan_obj.dingdan_id}, IP: {client_ip}") - - # ✅ 新方法(行为驱动,更安全) - from houtai.utils import update_shangjia_daily - update_shangjia_daily( - yonghuid=lianjie_obj.yonghu_id, - amount=Decimal(str(dingdan_obj.jine)), - action=1 # 1 = 派发 - ) - - return Response({ - 'code': 200, - 'msg': '提交成功', - 'data': response_data - }, status=status.HTTP_200_OK) - - except Dingdan.DoesNotExist: - logger.error(f"订单不存在 - 订单ID: {lianjie_obj.dingdan_id}") - return Response({ - 'code': 500, - 'msg': '订单数据异常', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - except Exception as e: - logger.error(f"处理订单异常: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'msg': '系统错误,请稍后重试', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def _get_game_type_name(self, leixing_id): - try: - from shangpin.models import ShangpinLeixing - gt = ShangpinLeixing.objects.filter(id=leixing_id).first() - return gt.jieshao if gt else '游戏订单' - except Exception as e: - logger.error(f"获取游戏类型名称异常: {e}") - return '游戏订单' - - def send_order_notification_async(self, dingdan, youxi_nicheng, zhiding_dashou, zhuangtai): - """异步发送订单通知(客户填写订单后)""" - try: - # 避免阻塞主线程,使用线程异步执行 - def notification_task(): - try: - # 获取游戏类型名称 - try: - leixing_obj = ShangpinLeixing.objects.get(id=dingdan.leixing_id) - game_type = leixing_obj.jieshao - except: - game_type = '未知游戏' - - # 构建订单信息 - order_info = { - 'dingdan_id': dingdan.dingdan_id, - 'game_type': game_type, - 'amount': str(dingdan.jine), - 'order_desc': dingdan.jieshao[:50] if dingdan.jieshao else '新订单', - 'youxi_nicheng': youxi_nicheng, - 'order_type': '指定订单' if zhuangtai == 7 else '普通订单', - 'zhiding_dashou': zhiding_dashou or '' - } - - # 调用微信广播发送器 - sender = WeixinBroadcastSender() - result = sender.broadcast_order(order_info) - - if result['success']: - logger.info(f"【客户填写订单】微信通知发送成功: {result['msg']}") - else: - logger.warning(f"【客户填写订单】微信通知发送失败: {result['msg']}") - - except Exception as e: - logger.error(f"【客户填写订单】发送通知任务异常: {str(e)}") - - # 启动异步线程 - thread = threading.Thread(target=notification_task) - thread.daemon = True - thread.start() - - except Exception as e: - logger.error(f"【客户填写订单】启动通知线程异常: {str(e)}") - - - - - -'''class KehuTianxieDingdanView(APIView): - """ - 客户填写订单信息接口 - 路径:POST /peizhi/kehutx - """ - - # 添加频率限制(比查询接口更严格) - throttle_classes = [OrderLinkThrottle] - permission_classes = [] # 无需认证 - - def post(self, request): - """ - 处理POST请求,填写订单信息 - """ - # 1. 记录请求开始 - logger.info("=== 客户填写订单信息接口开始 ===") - - # 2. 获取客户端真实IP - client_ip = IPUtils.get_client_ip(request) - logger.info(f"客户端IP: {client_ip}") - - # 3. 验证IP合法性 - if not IPUtils.validate_ip(client_ip): - logger.warning(f"IP格式不合法: {client_ip}") - return Response({ - 'code': 400, - 'msg': '网络连接异常', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 获取请求参数 - data = request.data - token = data.get('token', '') - youxi_nicheng = data.get('youxi_nicheng', '').strip() - zhiding_dashou = data.get('zhiding_dashou', '').strip() # 可选 - beizhu = data.get('beizhu', '').strip() # 可选 - - logger.info( - f"请求参数 - Token: {token[:8]}..., 昵称: {youxi_nicheng[:10]}..., 指定打手: {zhiding_dashou}, 备注长度: {len(beizhu)}") - - # 5. 验证必填参数 - if not token: - logger.warning(f"Token参数缺失 - IP: {client_ip}") - return Response({ - 'code': 400, - 'msg': '缺少链接参数', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if not youxi_nicheng: - logger.warning(f"游戏昵称为空 - IP: {client_ip}") - return Response({ - 'code': 400, - 'msg': '游戏昵称不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 6. 验证游戏昵称格式 - if len(youxi_nicheng) > 50: - logger.warning(f"游戏昵称过长 - IP: {client_ip}, 长度: {len(youxi_nicheng)}") - return Response({ - 'code': 400, - 'msg': '游戏昵称不能超过50个字符', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 过滤危险字符 - danger_chars = ['<', '>', "'", '"', '&', ';', '|'] - for char in danger_chars: - if char in youxi_nicheng: - logger.warning(f"游戏昵称包含危险字符 - IP: {client_ip}, 字符: {char}") - return Response({ - 'code': 400, - 'msg': '昵称包含非法字符', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 7. 验证指定打手ID格式(如果提供) - if zhiding_dashou and len(zhiding_dashou) > 32: - logger.warning(f"指定打手ID过长 - IP: {client_ip}, 长度: {len(zhiding_dashou)}") - return Response({ - 'code': 400, - 'msg': '指定打手ID不能超过32个字符', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 8. 验证备注长度 - if len(beizhu) > 500: - logger.warning(f"备注过长 - IP: {client_ip}, 长度: {len(beizhu)}") - return Response({ - 'code': 400, - 'msg': '备注不能超过500个字符', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 9. 查询链接记录 - try: - lianjie_obj = ShangjiaLianjie.objects.get(lianjie_token=token) - logger.info( - f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}, 是否已使用: {lianjie_obj.is_used}") - except ShangjiaLianjie.DoesNotExist: - logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}") - return Response({ - 'code': 404, - 'msg': '链接不存在或已失效', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 10. 检查链接有效期 - now = timezone.now() - if lianjie_obj.youxiao_shijian and lianjie_obj.youxiao_shijian < now: - logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.youxiao_shijian}") - return Response({ - 'code': 400, - 'msg': '链接已过期', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 11. 检查链接是否已使用 - if lianjie_obj.is_used: - logger.warning(f"链接已使用 - 链接ID: {lianjie_obj.id}, IP: {client_ip}") - return Response({ - 'code': 400, - 'msg': '链接已被使用,无法重复提交', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 12. 验证指定打手(如果需要) - zhuangtai_xin = 1 # 默认普通订单状态 - if zhiding_dashou: - # 验证打手是否存在且有效 - try: - # 先查询用户主表 - dashou_user = UserMain.objects.get(yonghuid=zhiding_dashou) - logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}") - - # 通过反向关系获取打手扩展信息 - try: - dashou_profile = dashou_user.dashou_profile - - # 检查打手状态(zhuangtai) - if dashou_profile.zhuangtai != 1: - logger.warning(f"打手状态异常 - 打手ID: {zhiding_dashou}, 状态: {dashou_profile.zhuangtai}") - return Response({ - 'code': 400, - 'msg': '指定打手当前无法接单', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 检查打手账号状态(zhanghaozhuangtai) - if dashou_profile.zhanghaozhuangtai != 1: - logger.warning( - f"打手账号状态异常 - 打手ID: {zhiding_dashou}, 账号状态: {dashou_profile.zhanghaozhuangtai}") - return Response({ - 'code': 400, - 'msg': '指定打手账号异常', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - - - # 验证通过,设置为指定订单状态 - zhuangtai_xin = 7 # 指定打手订单状态 - logger.info(f"指定打手验证通过 - 打手ID: {zhiding_dashou}, 昵称: {dashou_profile.nicheng}") - - except UserDashou.DoesNotExist: - logger.warning(f"打手扩展信息不存在 - 用户ID: {zhiding_dashou}") - return Response({ - 'code': 400, - 'msg': '指定打手信息不完整', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - except UserMain.DoesNotExist: - logger.warning(f"指定打手用户不存在 - 用户ID: {zhiding_dashou}") - return Response({ - 'code': 400, - 'msg': '指定打手不存在', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 13. 使用事务确保数据一致性 - try: - with transaction.atomic(): - # 更新链接记录 - lianjie_obj.shiyongzhe_ip = client_ip - lianjie_obj.is_used = True - lianjie_obj.save(update_fields=['shiyongzhe_ip', 'is_used']) - logger.info(f"更新链接记录成功 - 链接ID: {lianjie_obj.id}, 设置为已使用") - - # 查询订单 - if not lianjie_obj.dingdan_id: - logger.error(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}") - raise ValueError("订单ID为空") - - dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id) - - # 检查订单状态是否为13(灰色状态) - if dingdan_obj.zhuangtai != 13: - logger.warning( - f"订单状态不允许修改 - 订单ID: {dingdan_obj.dingdan_id}, 当前状态: {dingdan_obj.zhuangtai}") - # 回滚事务 - transaction.set_rollback(True) - return Response({ - 'code': 400, - 'msg': '订单状态异常,无法修改', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 更新订单信息 - dingdan_obj.nicheng = youxi_nicheng # 游戏昵称 - dingdan_obj.beizhu = beizhu if beizhu else '' # 备注 - dingdan_obj.zhuangtai = zhuangtai_xin # 新状态:1或7 - - # 如果是指定打手,设置zhiding_id - if zhiding_dashou and zhuangtai_xin == 7: - dingdan_obj.zhiding_id = zhiding_dashou - - dingdan_obj.save() - logger.info( - f"更新订单成功 - 订单ID: {dingdan_obj.dingdan_id}, 新状态: {zhuangtai_xin}, 指定打手: {zhiding_dashou or '无'}") - - # 🔴【添加】在订单更新成功后,异步发送微信服务号通知 - self.send_order_notification_async(dingdan_obj, youxi_nicheng, zhiding_dashou, zhuangtai_xin) - - # 获取商家信息用于返回 - try: - user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id) - shangjia_obj = user_main.shop_profile - shangjia_mingcheng = shangjia_obj.nicheng - except: - shangjia_mingcheng = f"商家{lianjie_obj.yonghu_id}" - logger.warning(f"获取商家昵称失败,使用默认 - 用户ID: {lianjie_obj.yonghu_id}") - - # 14. 返回成功响应 - response_data = { - #'dingdan_id': dingdan_obj.dingdan_id, - #'create_time': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S'), - #'shangpin_mingcheng': dingdan_obj.jieshao[:50] if dingdan_obj.jieshao else '', # 截取前50字符 - #'jiage': str(dingdan_obj.jine) if dingdan_obj.jine else '0.00', - 'shangjia_mingcheng': shangjia_mingcheng, - 'youxi_nicheng': youxi_nicheng, - 'zhuangtai': zhuangtai_xin, - 'zhuangtai_text': '普通订单' if zhuangtai_xin == 1 else '指定打手订单' - } - - logger.info(f"订单提交成功 - 订单ID: {dingdan_obj.dingdan_id}, IP: {client_ip}") - - return Response({ - 'code': 200, - 'msg': '提交成功', - 'data': response_data - }, status=status.HTTP_200_OK) - - except Dingdan.DoesNotExist: - logger.error(f"订单不存在 - 订单ID: {lianjie_obj.dingdan_id}") - return Response({ - 'code': 500, - 'msg': '订单数据异常', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - except Exception as e: - logger.error(f"处理订单异常: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'msg': '系统错误,请稍后重试', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def send_order_notification_async(self, dingdan, youxi_nicheng, zhiding_dashou, zhuangtai): - """异步发送订单通知(客户填写订单后)""" - try: - # 避免阻塞主线程,使用线程异步执行 - def notification_task(): - try: - # 获取游戏类型名称 - try: - leixing_obj = ShangpinLeixing.objects.get(id=dingdan.leixing_id) - game_type = leixing_obj.jieshao - except: - game_type = '未知游戏' - - # 构建订单信息 - order_info = { - 'dingdan_id': dingdan.dingdan_id, - 'game_type': game_type, - 'amount': str(dingdan.jine), - 'order_desc': dingdan.jieshao[:50] if dingdan.jieshao else '新订单', - 'youxi_nicheng': youxi_nicheng, - 'order_type': '指定订单' if zhuangtai == 7 else '普通订单', - 'zhiding_dashou': zhiding_dashou or '' - } - - # 调用微信广播发送器 - sender = WeixinBroadcastSender() - result = sender.broadcast_order(order_info) - - if result['success']: - logger.info(f"【客户填写订单】微信通知发送成功: {result['msg']}") - else: - logger.warning(f"【客户填写订单】微信通知发送失败: {result['msg']}") - - except Exception as e: - logger.error(f"【客户填写订单】发送通知任务异常: {str(e)}") - - # 启动异步线程 - thread = threading.Thread(target=notification_task) - thread.daemon = True - thread.start() - - except Exception as e: - logger.error(f"【客户填写订单】启动通知线程异常: {str(e)}")''' - - - - - - - - - - - - - - - - - - - - - - - - - - -from django.db.models import Max, F - - -class GuanZhuAListView(APIView): - """ - 获取关注阿龙页面图片列表 - POST /peizhi/gzal - 需要JWT认证 - 返回: - { - "code": 0, - "msg": "success", - "data": { - "images": ["相对URL1", "相对URL2", ...], - "last_update": "2025-03-16 12:34:56", - "visit_count": 123 - } - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 查询类型为3的所有记录 - qs = Tupianpeizhi.objects.filter(leixing=3) - if not qs.exists(): - return Response({ - 'code': 0, - 'msg': 'success', - 'data': { - 'images': [], - 'last_update': None, - 'visit_count': 0 - } - }, status=status.HTTP_200_OK) - - # 获取所有图片的相对URL - images = list(qs.values_list('tupian', flat=True)) - - # 获取最新更新时间(取所有记录的update_time最大值) - last_update = qs.aggregate(Max('update_time'))['update_time__max'] - last_update_str = last_update.strftime('%Y-%m-%d %H:%M:%S') if last_update else None - - # 原子性增加访问次数 - # 先更新所有记录,再获取更新后的最大次数 - qs.update(fangwen_cishu=F('fangwen_cishu') + 1) - visit_count = qs.aggregate(Max('fangwen_cishu'))['fangwen_cishu__max'] or 0 - - return Response({ - 'code': 0, - 'msg': 'success', - 'data': { - 'images': images, - 'last_update': last_update_str, - 'visit_count': visit_count - } - }, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f"获取关注阿龙图片列表失败: {e}", exc_info=True) - return Response({ - 'code': 1, - 'msg': '服务器内部错误', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - -# views.py -import io -import time - -from yonghu.models import UserGuanshi -from utils.oss_utils import upload_to_oss, delete_from_oss -from utils.yaoqingma_utils import chuangjianYaoqingma, yanzhengYaoqingmaWeiyixing -import requests - -logger = logging.getLogger(__name__) - - - -import urllib.parse - - -#不用缓存,一天2000次 -class GuanshiQRCodeView(APIView): - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - logger.info("===== 开始生成管事二维码 =====") - current_user = request.user - logger.info(f"当前用户ID: {current_user.yonghuid}") - - try: - try: - guanshi = current_user.guanshi_profile - logger.info(f"管事扩展表查询成功,状态: {guanshi.zhuangtai}") - except UserGuanshi.DoesNotExist: - logger.warning(f"用户 {current_user.yonghuid} 不是管事身份") - return Response( - {'code': 403, 'message': '当前用户不是管事身份', 'data': None}, - status=status.HTTP_403_FORBIDDEN - ) - - if guanshi.zhuangtai != 1: - logger.warning(f"用户 {current_user.yonghuid} 管事状态异常: {guanshi.zhuangtai}") - return Response( - {'code': 403, 'message': '管事账号状态异常', 'data': None}, - status=status.HTTP_403_FORBIDDEN - ) - - invite_code = guanshi.yaoqingma - if not invite_code: - logger.info(f"用户 {current_user.yonghuid} 无邀请码,开始生成") - invite_code = chuangjianYaoqingma(str(current_user.yonghuid)) - if not yanzhengYaoqingmaWeiyixing(invite_code): - logger.error(f"邀请码唯一性校验失败,用户: {current_user.yonghuid}") - return Response( - {'code': 500, 'message': '邀请码生成失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - guanshi.yaoqingma = invite_code - guanshi.save(update_fields=['yaoqingma']) - logger.info(f"邀请码生成成功: {invite_code}") - else: - logger.info(f"使用已有邀请码: {invite_code}") - - # 直接获取新的 access_token,不缓存 - access_token = self.get_fresh_access_token() - if not access_token: - logger.error("获取 access_token 失败") - return Response( - {'code': 500, 'message': '获取微信access_token失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - logger.info(f"获取到 access_token (前10位): {access_token[:10]}...") - - encoded_invite = urllib.parse.quote(invite_code, safe='') - page_path = f'pages/dashouduan/dashouduan?inviteCode={encoded_invite}' - wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}' - post_data = { - 'path': page_path, - 'width': 430, - 'auto_color': False, - 'line_color': {'r': 0, 'g': 0, 'b': 0} - } - logger.info(f"调用微信接口生成小程序码,path: {page_path}") - - try: - wx_resp = requests.post(wx_url, json=post_data, timeout=10) - logger.info(f"微信接口响应状态码: {wx_resp.status_code}") - except Exception as e: - logger.error(f"请求微信接口异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '调用微信服务失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - if wx_resp.status_code != 200: - error_info = {} - if wx_resp.headers.get('content-type') == 'application/json': - try: - error_info = wx_resp.json() - except: - pass - errmsg = error_info.get('errmsg', '未知错误') - logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}") - if 'errcode' in error_info: - logger.error(f"微信错误码: {error_info.get('errcode')}") - return Response( - {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - content_type = wx_resp.headers.get('content-type', '') - if 'image' not in content_type: - try: - error_info = wx_resp.json() - errmsg = error_info.get('errmsg', '未知错误') - logger.error(f"微信接口返回非图片内容: {errmsg}") - return Response( - {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - except: - logger.error("微信接口返回未知内容,无法解析为JSON") - return Response( - {'code': 500, 'message': '生成二维码失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - timestamp = int(time.time()) - filename = f"{current_user.yonghuid}_{timestamp}.png" - oss_path = f"erweima/guanshi/{filename}" - file_obj = io.BytesIO(wx_resp.content) - logger.info(f"开始上传图片到COS: {oss_path}") - - try: - full_url = upload_to_oss(file_obj, oss_path) - logger.info(f"上传成功,完整URL: {full_url}") - except Exception as e: - logger.error(f"上传到COS异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '图片上传失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - if not full_url: - logger.error("upload_to_oss 返回了空 URL") - return Response( - {'code': 500, 'message': '图片上传失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - cos_domain = getattr(settings, 'COS_DOMAIN', '') - if cos_domain and full_url.startswith(cos_domain): - relative_path = full_url[len(cos_domain):].lstrip('/') - else: - relative_path = oss_path - logger.info(f"相对路径: {relative_path}") - - old_url = guanshi.invite_qrcode_url - if old_url: - logger.info(f"删除旧二维码: {old_url}") - try: - delete_from_oss(old_url) - except Exception as e: - logger.warning(f"删除旧二维码失败(不影响主流程): {str(e)}") - - with transaction.atomic(): - guanshi.invite_qrcode_url = relative_path - guanshi.save(update_fields=['invite_qrcode_url']) - logger.info("数据库更新成功") - - return Response({ - 'code': 0, - 'message': 'success', - 'data': {'url': relative_path} - }) - - except Exception as e: - logger.error(f"生成二维码过程中发生未捕获异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - def get_fresh_access_token(self): - """ - 直接请求微信接口获取新的 access_token,不缓存,保证最新。 - """ - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') - if not appid or not secret: - logger.error("微信小程序配置缺失: WEIXIN_APPID 或 WEIXIN_SECRET 未设置") - return None - - url = 'https://api.weixin.qq.com/cgi-bin/token' - params = { - 'grant_type': 'client_credential', - 'appid': appid, - 'secret': secret - } - try: - logger.info("正在请求微信接口获取最新 access_token...") - resp = requests.get(url, params=params, timeout=5) - data = resp.json() - new_token = data.get('access_token') - if new_token: - logger.info("成功获取最新 access_token") - return new_token - else: - errcode = data.get('errcode', '未知') - errmsg = data.get('errmsg', '未知错误') - logger.error(f"获取 access_token 失败,errcode={errcode}, errmsg={errmsg}") - return None - except Exception as e: - logger.error(f"请求微信 access_token 接口异常: {str(e)}", exc_info=True) - return None - -#用了缓存 -'''class GuanshiQRCodeView(APIView): - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - logger.info("===== 开始生成管事二维码 =====") - current_user = request.user - logger.info(f"当前用户ID: {current_user.yonghuid}") - - try: - try: - guanshi = current_user.guanshi_profile - logger.info(f"管事扩展表查询成功,状态: {guanshi.zhuangtai}") - except UserGuanshi.DoesNotExist: - logger.warning(f"用户 {current_user.yonghuid} 不是管事身份") - return Response( - {'code': 403, 'message': '当前用户不是管事身份', 'data': None}, - status=status.HTTP_403_FORBIDDEN - ) - - if guanshi.zhuangtai != 1: - logger.warning(f"用户 {current_user.yonghuid} 管事状态异常: {guanshi.zhuangtai}") - return Response( - {'code': 403, 'message': '管事账号状态异常', 'data': None}, - status=status.HTTP_403_FORBIDDEN - ) - - invite_code = guanshi.yaoqingma - if not invite_code: - logger.info(f"用户 {current_user.yonghuid} 无邀请码,开始生成") - invite_code = chuangjianYaoqingma(str(current_user.yonghuid)) - if not yanzhengYaoqingmaWeiyixing(invite_code): - logger.error(f"邀请码唯一性校验失败,用户: {current_user.yonghuid}") - return Response( - {'code': 500, 'message': '邀请码生成失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - guanshi.yaoqingma = invite_code - guanshi.save(update_fields=['yaoqingma']) - logger.info(f"邀请码生成成功: {invite_code}") - else: - logger.info(f"使用已有邀请码: {invite_code}") - - access_token = self.get_cached_access_token() - if not access_token: - logger.error("获取 access_token 失败") - return Response( - {'code': 500, 'message': '获取微信access_token失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - logger.info(f"获取到 access_token (前10位): {access_token[:10]}...") - - encoded_invite = urllib.parse.quote(invite_code, safe='') - page_path = f'pages/dashouduan/dashouduan?inviteCode={encoded_invite}' - wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}' - post_data = { - 'path': page_path, - 'width': 430, - 'auto_color': False, - 'line_color': {'r': 0, 'g': 0, 'b': 0} - } - logger.info(f"调用微信接口生成小程序码,path: {page_path}") - - try: - wx_resp = requests.post(wx_url, json=post_data, timeout=10) - logger.info(f"微信接口响应状态码: {wx_resp.status_code}") - except Exception as e: - logger.error(f"请求微信接口异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '调用微信服务失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - if wx_resp.status_code != 200: - error_info = {} - if wx_resp.headers.get('content-type') == 'application/json': - try: - error_info = wx_resp.json() - except: - pass - errmsg = error_info.get('errmsg', '未知错误') - logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}") - if 'errcode' in error_info: - logger.error(f"微信错误码: {error_info.get('errcode')}") - return Response( - {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - content_type = wx_resp.headers.get('content-type', '') - if 'image' not in content_type: - try: - error_info = wx_resp.json() - errmsg = error_info.get('errmsg', '未知错误') - logger.error(f"微信接口返回非图片内容: {errmsg}") - return Response( - {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - except: - logger.error("微信接口返回未知内容,无法解析为JSON") - return Response( - {'code': 500, 'message': '生成二维码失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - timestamp = int(time.time()) - filename = f"{current_user.yonghuid}_{timestamp}.png" - oss_path = f"erweima/guanshi/{filename}" - file_obj = io.BytesIO(wx_resp.content) - logger.info(f"开始上传图片到COS: {oss_path}") - - try: - full_url = upload_to_oss(file_obj, oss_path) - logger.info(f"上传成功,完整URL: {full_url}") - except Exception as e: - logger.error(f"上传到COS异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '图片上传失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - if not full_url: - logger.error("upload_to_oss 返回了空 URL") - return Response( - {'code': 500, 'message': '图片上传失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - cos_domain = getattr(settings, 'COS_DOMAIN', '') - if cos_domain and full_url.startswith(cos_domain): - relative_path = full_url[len(cos_domain):].lstrip('/') - else: - relative_path = oss_path - logger.info(f"相对路径: {relative_path}") - - old_url = guanshi.invite_qrcode_url - if old_url: - logger.info(f"删除旧二维码: {old_url}") - try: - delete_from_oss(old_url) - except Exception as e: - logger.warning(f"删除旧二维码失败(不影响主流程): {str(e)}") - - with transaction.atomic(): - guanshi.invite_qrcode_url = relative_path - guanshi.save(update_fields=['invite_qrcode_url']) - logger.info("数据库更新成功") - - return Response({ - 'code': 0, - 'message': 'success', - 'data': {'url': relative_path} - }) - - except Exception as e: - logger.error(f"生成二维码过程中发生未捕获异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - def get_cached_access_token(self): - """获取微信全局 access_token,使用 cache.add 实现互斥锁""" - cache_key = 'wechat_access_token' - lock_key = 'wechat_access_token_lock' - lock_timeout = 10 - - # 先尝试读取缓存 - token = cache.get(cache_key) - if token: - logger.info("从缓存中读取到 access_token") - return token - - # 尝试获取锁(原子操作) - lock_acquired = cache.add(lock_key, '1', timeout=lock_timeout) - if not lock_acquired: - # 未获得锁,等待其他进程刷新 - import time - time.sleep(0.2) - token = cache.get(cache_key) - if token: - logger.info("其他进程已刷新 token,使用缓存值") - return token - # 等待几秒再试 - for _ in range(5): - time.sleep(0.2) - token = cache.get(cache_key) - if token: - return token - logger.warning("获取锁失败且缓存中无 token,返回 None") - return None - - # 获得锁,执行刷新 - try: - token = cache.get(cache_key) - if token: - logger.info("加锁后再次读取到缓存中的 token") - return token - - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') - if not appid or not secret: - logger.error("微信小程序配置缺失: WEIXIN_APPID 或 WEIXIN_SECRET 未设置") - return None - - url = 'https://api.weixin.qq.com/cgi-bin/token' - params = { - 'grant_type': 'client_credential', - 'appid': appid, - 'secret': secret - } - try: - logger.info("正在请求微信接口获取 access_token...") - resp = requests.get(url, params=params, timeout=5) - data = resp.json() - new_token = data.get('access_token') - if new_token: - cache.set(cache_key, new_token, timeout=7100) - logger.info("成功获取新 access_token,已缓存") - return new_token - else: - errcode = data.get('errcode', '未知') - errmsg = data.get('errmsg', '未知错误') - logger.error(f"获取 access_token 失败,errcode={errcode}, errmsg={errmsg}") - return None - except Exception as e: - logger.error(f"请求微信 access_token 接口异常: {str(e)}", exc_info=True) - return None - finally: - cache.delete(lock_key)''' - # ==================== 修改结束 ==================== - - - - -from yonghu.models import UserZuzhang - - - - - - - -logger = logging.getLogger(__name__) - - -class ZuzhangHaibaoView(APIView): - """ - 组长专属海报二维码生成接口 - 路径:/peizhi/zuzhanghb - 方法:POST - 权限:JWT认证,仅限组长身份 - 返回: - { - "code": 0, - "message": "success", - "data": { - "url": "erweima/zuzhang/1000001_1234567890.png", - "yaoqingma": "ABC123XYZ" - } - } - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - current_user = request.user - - # 1. 验证组长身份 - try: - zuzhang = current_user.zuzhang_profile - except UserZuzhang.DoesNotExist: - return Response({ - 'code': 403, - 'message': '当前用户不是组长身份', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 2. 验证组长账号状态 - if zuzhang.zhuangtai != 1: - return Response({ - 'code': 403, - 'message': '组长账号状态异常,无法生成海报', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 3. 获取或生成邀请码 - invite_code = zuzhang.yaoqingma - if not invite_code: - invite_code = chuangjianYaoqingma(str(current_user.yonghuid)) - if not yanzhengYaoqingmaWeiyixing(invite_code): - logger.error(f"组长邀请码生成失败,用户: {current_user.yonghuid}") - return Response({ - 'code': 500, - 'message': '邀请码生成失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - zuzhang.yaoqingma = invite_code - zuzhang.save(update_fields=['yaoqingma']) - - # 4. 获取微信 access_token - access_token = self.get_cached_access_token() - if not access_token: - return Response({ - 'code': 500, - 'message': '获取微信access_token失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # ===== 修改点:使用 A 接口(getwxacode) ===== - # 对邀请码进行 URL 编码,防止特殊字符破坏路径 - encoded_invite = urllib.parse.quote(invite_code, safe='') - # 构造带参数的完整路径(请根据实际页面调整) - page_path = f'pages/guanshiduan/guanshiduan?inviteCode={encoded_invite}' - - wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}' - post_data = { - 'path': page_path, - 'width': 430, - 'auto_color': False, - 'line_color': {'r': 0, 'g': 0, 'b': 0} - } - - try: - wx_resp = requests.post(wx_url, json=post_data, timeout=10) - except Exception as e: - logger.error(f"请求微信接口失败: {e}") - return Response({ - 'code': 500, - 'message': '调用微信服务失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 处理微信返回结果 - if wx_resp.status_code != 200: - error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {} - errmsg = error_info.get('errmsg', '未知错误') - logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}") - return Response({ - 'code': 500, - 'message': f'生成二维码失败: {errmsg}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - content_type = wx_resp.headers.get('content-type', '') - if 'image' not in content_type: - try: - error_info = wx_resp.json() - errmsg = error_info.get('errmsg', '未知错误') - logger.error(f"微信接口返回非图片内容: {errmsg}") - return Response({ - 'code': 500, - 'message': f'生成二维码失败: {errmsg}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - except: - logger.error("微信接口返回未知内容") - return Response({ - 'code': 500, - 'message': '生成二维码失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 5. 上传到腾讯云 COS - timestamp = int(time.time()) - filename = f"{current_user.yonghuid}_{timestamp}.png" - oss_path = f"erweima/zuzhang/{filename}" - file_obj = io.BytesIO(wx_resp.content) - - try: - full_url = upload_to_oss(file_obj, oss_path) - except Exception as e: - logger.error(f"上传到COS失败: {e}") - return Response({ - 'code': 500, - 'message': '图片上传失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - if not full_url: - return Response({ - 'code': 500, - 'message': '图片上传失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 6. 获取相对路径 - cos_domain = getattr(settings, 'COS_DOMAIN', '') - if cos_domain and full_url.startswith(cos_domain): - relative_path = full_url[len(cos_domain):].lstrip('/') - else: - relative_path = oss_path - - # 7. 删除旧二维码文件 - old_url = zuzhang.haibao_url - if old_url: - delete_from_oss(old_url) - - # 8. 更新数据库中的 haibao_url 字段 - with transaction.atomic(): - zuzhang.haibao_url = relative_path - zuzhang.save(update_fields=['haibao_url']) - - # 9. 返回邀请码和二维码相对路径 - return Response({ - 'code': 0, - 'message': 'success', - 'data': { - 'url': relative_path, - 'yaoqingma': invite_code - } - }) - - def get_cached_access_token(self): - """从缓存获取微信 access_token""" - cache_key = 'wx_mini_access_token' - token = cache.get(cache_key) - if token: - return token - - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') - if not appid or not secret: - logger.error("微信小程序配置缺失") - return None - - url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}' - try: - resp = requests.get(url, timeout=5) - data = resp.json() - token = data.get('access_token') - expires_in = data.get('expires_in', 7200) - if token: - cache.set(cache_key, token, 7000) - return token - except Exception as e: - logger.error(f"获取微信access_token失败: {e}") - return None -'''class ZuzhangHaibaoView(APIView): - """ - 组长专属海报二维码生成接口 - 路径:/peizhi/zuzhanghb - 方法:POST - 权限:JWT认证,仅限组长身份 - 返回: - { - "code": 0, - "message": "success", - "data": { - "url": "erweima/zuzhang/1000001_1234567890.png", # 相对路径 - "yaoqingma": "ABC123XYZ" # 组长邀请码 - } - } - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - current_user = request.user - - # 1. 验证组长身份 - try: - zuzhang = current_user.zuzhang_profile - except UserZuzhang.DoesNotExist: - return Response({ - 'code': 403, - 'message': '当前用户不是组长身份', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 2. 验证组长账号状态(zhuangtai=1正常) - if zuzhang.zhuangtai != 1: - return Response({ - 'code': 403, - 'message': '组长账号状态异常,无法生成海报', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 3. 获取或生成邀请码(yaoqingma字段) - invite_code = zuzhang.yaoqingma - if not invite_code: - # 生成唯一邀请码(使用已有工具函数) - invite_code = chuangjianYaoqingma(str(current_user.yonghuid)) - if not yanzhengYaoqingmaWeiyixing(invite_code): - logger.error(f"组长邀请码生成失败,用户: {current_user.yonghuid}") - return Response({ - 'code': 500, - 'message': '邀请码生成失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - # 更新到数据库 - zuzhang.yaoqingma = invite_code - zuzhang.save(update_fields=['yaoqingma']) - - # 4. 获取微信 access_token(使用缓存) - access_token = self.get_cached_access_token() - if not access_token: - return Response({ - 'code': 500, - 'message': '获取微信access_token失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # ========== 调用微信B接口:getUnlimited ========== - # 页面路径:打手注册页(组长邀请打手) - page_path = 'pages/guanshiduan/guanshiduan' - # scene参数:直接放入邀请码(需确保字符符合微信要求) - scene = invite_code # 假设邀请码只包含字母数字 - - wx_url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}' - post_data = { - 'scene': scene, - 'page': page_path, - 'width': 430, - 'auto_color': False, - 'line_color': {'r': 0, 'g': 0, 'b': 0} - } - - try: - wx_resp = requests.post(wx_url, json=post_data, timeout=10) - except Exception as e: - logger.error(f"请求微信接口失败: {e}") - return Response({ - 'code': 500, - 'message': '调用微信服务失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 处理微信返回结果 - if wx_resp.status_code != 200: - error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {} - errmsg = error_info.get('errmsg', '未知错误') - logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}") - return Response({ - 'code': 500, - 'message': f'生成二维码失败: {errmsg}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - content_type = wx_resp.headers.get('content-type', '') - if 'image' not in content_type: - # 可能是JSON错误 - try: - error_info = wx_resp.json() - errmsg = error_info.get('errmsg', '未知错误') - logger.error(f"微信接口返回非图片内容: {errmsg}") - return Response({ - 'code': 500, - 'message': f'生成二维码失败: {errmsg}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - except: - logger.error("微信接口返回未知内容") - return Response({ - 'code': 500, - 'message': '生成二维码失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 5. 上传二维码到腾讯云COS - timestamp = int(time.time()) - filename = f"{current_user.yonghuid}_{timestamp}.png" - oss_path = f"erweima/zuzhang/{filename}" # 组长专用文件夹 - file_obj = io.BytesIO(wx_resp.content) - - try: - full_url = upload_to_oss(file_obj, oss_path) - except Exception as e: - logger.error(f"上传到COS失败: {e}") - return Response({ - 'code': 500, - 'message': '图片上传失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - if not full_url: - return Response({ - 'code': 500, - 'message': '图片上传失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 6. 获取相对路径(用于前端拼接) - cos_domain = getattr(settings, 'COS_DOMAIN', '') - if cos_domain and full_url.startswith(cos_domain): - relative_path = full_url[len(cos_domain):].lstrip('/') - else: - relative_path = oss_path - - # 7. 删除旧的二维码文件(haibao_url字段) - old_url = zuzhang.haibao_url - if old_url: - delete_from_oss(old_url) - - # 8. 更新数据库中的haibao_url字段 - with transaction.atomic(): - zuzhang.haibao_url = relative_path - zuzhang.save(update_fields=['haibao_url']) - - # 9. 返回邀请码和二维码相对路径 - return Response({ - 'code': 0, - 'message': 'success', - 'data': { - 'url': relative_path, - 'yaoqingma': invite_code - } - }) - - def get_cached_access_token(self): - """从缓存获取微信access_token(与管事接口共用)""" - cache_key = 'wx_mini_access_token' - token = cache.get(cache_key) - if token: - return token - - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') - if not appid or not secret: - logger.error("微信小程序配置缺失") - return None - - url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}' - try: - resp = requests.get(url, timeout=5) - data = resp.json() - token = data.get('access_token') - expires_in = data.get('expires_in', 7200) - if token: - cache.set(cache_key, token, 7000) - return token - except Exception as e: - logger.error(f"获取微信access_token失败: {e}") - return None''' - - - - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework import status -from django.utils import timezone -from django.db.models import Q -from .models import PopupPage, PopupConfig -from .serializers import PopupConfigSerializer - - -class PopupConfigView(APIView): - """ - 获取指定页面的弹窗配置 - 请求格式:POST { "pageKey": "dashouzhongxin" } - 返回格式:{ "code": 0, "data": { "serverTime": "...", "popups": [...] } } - """ - permission_classes = [IsAuthenticated] # JWT 认证 - - def post(self, request): - # 1. 获取当前用户(已通过JWT认证,可直接使用) - user = request.user - if not user or not user.id: - return Response({ - 'code': 401, - 'msg': '用户未登录或认证失败' - }, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 获取参数 pageKey - page_key = request.data.get('pageKey') - if not page_key: - return Response({ - 'code': 400, - 'msg': '缺少参数 pageKey' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 查询页面配置 - try: - popup_page = PopupPage.objects.get(page_key=page_key, is_active=True) - except PopupPage.DoesNotExist: - # 该页面没有配置弹窗,返回空 - return Response({ - 'code': 0, - 'data': { - 'serverTime': timezone.now().isoformat(), - 'popups': [] - } - }) - - # 4. 查询该页面下所有启用的弹窗(且在有效时间范围内) - now = timezone.now() - popups = PopupConfig.objects.filter( - page=popup_page, - is_active=True, - start_time__lte=now, - end_time__gte=now - ).prefetch_related('images') # 预加载图片,避免N+1查询 - # 按 sort_order 排序(model Meta 已定义) - - # 5. 序列化 - serializer = PopupConfigSerializer(popups, many=True) - - # 6. 返回数据 - return Response({ - 'code': 0, - 'data': { - 'serverTime': now.isoformat(), - 'ignore_user_mute': popup_page.ignore_user_mute, # 新增 - 'popups': serializer.data - } - }) - - -from .models import WithdrawConfig - -class GetWithdrawModeView(APIView): - """ - 获取提现模式配置 - 前端请求 /peizhi/hqtxzsym - 返回格式: { "code": 0, "data": { "mode": 1 or 2 } } - """ - permission_classes = [IsAuthenticated] # JWT 认证 - - def post(self, request, *args, **kwargs): - # 尝试获取 ID=1 的记录 - config = WithdrawConfig.objects.filter(id=1).first() - # 如果存在则取实际值,否则默认 2(手动提现) - mode = config.mode if config else 2 - - return Response({ - 'code': 0, - 'data': { - 'mode': mode - } - }) - - - - -from shangpin.models import ( - Huiyuangoumai -) - - -class CheckPhoneAuthView(APIView): - """ - 判断当前登录用户是否需要强制手机号认证 - - 规则: - - 已认证过手机号(shoujihao_renzheng=True) → 不需要 - - 未认证时,检查用户角色及其业务指标是否达到阈值: - 打手:有会员购买记录(Huiyuangoumai 中存在记录) - 商家:发布订单总数(fabu)>= 5 - 管事:充值分佣总额(chongzhifenrun)>= 15 - 组长:分佣总额(fenyong_zonge)>= 2 - 老板:消费总额(zonge)>= 20 - - 满足任一角色的阈值 → 需要认证 - - 否则 → 不需要认证 - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - user = request.user # UserMain 实例 - - # 已经认证过手机号,直接放行 - if user.shoujihao_renzheng: - return Response({'need_auth': False}) - - # 检查各角色是否满足阈值条件 - if self._dashou_meets_threshold(user): - return Response({'need_auth': True}) - if self._shangjia_meets_threshold(user): - return Response({'need_auth': True}) - if self._guanshi_meets_threshold(user): - return Response({'need_auth': True}) - if self._zuzhang_meets_threshold(user): - return Response({'need_auth': True}) - if self._boss_meets_threshold(user): - return Response({'need_auth': True}) - - # 都不满足,无需认证 - return Response({'need_auth': False}) - - # ==================== 各角色阈值判断方法 ==================== - - def _dashou_meets_threshold(self, user): - """ - 打手条件:存在会员购买记录(Huiyuangoumai 表中有该用户的记录) - """ - try: - # 先确认用户是打手 - UserDashou.objects.get(user=user) - except UserDashou.DoesNotExist: - return False - # 检查是否有任意会员购买记录 - return Huiyuangoumai.objects.filter(yonghu_id=user.yonghuid).exists() - - def _shangjia_meets_threshold(self, user): - """商家条件:发布订单总数 fabu >= 5""" - try: - shop = UserShangjia.objects.get(user=user) - except UserShangjia.DoesNotExist: - return False - return shop.fabu >= 5 - - def _guanshi_meets_threshold(self, user): - """管事条件:充值分佣总额 chongzhifenrun >= 15""" - try: - guanshi = UserGuanshi.objects.get(user=user) - except UserGuanshi.DoesNotExist: - return False - return guanshi.chongzhifenrun >= 15 - - def _zuzhang_meets_threshold(self, user): - """组长条件:分佣总额 fenyong_zonge >= 2""" - try: - zuzhang = UserZuzhang.objects.get(user=user) - except UserZuzhang.DoesNotExist: - return False - return zuzhang.fenyong_zonge >= 2 - - def _boss_meets_threshold(self, user): - """老板条件:消费总额 zonge >= 20""" - try: - boss = UserBoss.objects.get(user=user) - except UserBoss.DoesNotExist: - return False +import io +import os +import json +import re +import time +import random +import secrets +import hashlib +import threading +import urllib.parse +from decimal import Decimal, InvalidOperation + +from django.conf import settings +from django.db import models, transaction, connection +from django.db.models import Q, F, Max, Prefetch +from django.core.cache import cache +from django.core.paginator import Paginator +from django.utils import timezone + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status, permissions +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.parsers import JSONParser, MultiPartParser, FormParser +from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle + +from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image +from utils.weixin_broadcast import WeixinBroadcastSender +# from utils.ip_security import * +from Django.utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode + +from backend.utils import update_shangjia_daily +from orders.utils import sync_order_to_partners + +from .models import ( + Gonggao, Lunbo, ClubConfig, Tupianpeizhi, Qunpeizhi, + ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig +) + +from users.models import ( + UserMain, AdminProfile, UserDashou, UserBoss, + UserShangjia, UserGuanshi, UserZuzhang +) + +from orders.models import Lilubiao, Dingdan, DingdanShangjia +from orders.notice_tasks import dingdan_guangbo + +from rank.models import Chenghao, DingdanBiaoqian + +from products.models import ShangpinLeixing, Huiyuangoumai + +from .serializers import PopupConfigSerializer +import traceback +import requests + +import logging +logger = logging.getLogger(__name__) + + + +class GetDynamicConfigView(APIView): + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + try: + # 查询我方俱乐部配置(is_self=1),取第一条(通常只有一条) + club_config = ClubConfig.objects.filter(is_self=1).only( + 'storage_bucket_domain', + 'storage_bucket_name', + 'chat_api_key', + 'customer_service_api_url', + 'customer_service_corp_id' + ).first() + + # 默认值(防止空数据导致前端报错) + cos_bucket = '' + cos_region = 'ap-shanghai' + cos_oss_url = '' + goeasy_host = 'hangzhou.goeasy.io' + goeasy_appkey = '' + kefu_link = '' + kefu_enterprise_id = '' + morentouxiang = 'beijing/morentouxiang.jpg' + dashouguize = 'a_long/dashouguize.jpg' + + if club_config: + # 存储桶配置 + bucket = club_config.storage_bucket_name or '' + region = 'ap-shanghai' # 若需要动态,可单独存储 + cos_oss_url = club_config.storage_bucket_domain or '' + if cos_oss_url and not cos_oss_url.endswith('/'): + cos_oss_url += '/' + cos_bucket = bucket + cos_region = region + + # GoEasy 配置 + if club_config.chat_api_key: + goeasy_appkey = club_config.chat_api_key + # host 如果需要可动态,这里固定或从其他字段获取 + + # 客服配置 + if club_config.customer_service_api_url: + kefu_link = club_config.customer_service_api_url + if club_config.customer_service_corp_id: + kefu_enterprise_id = club_config.customer_service_corp_id + + # 其他配置可在此扩展 + + # 构造返回数据 + data = { + "code": 0, + "data": { + "cos": { + "bucket": cos_bucket, + "region": cos_region, + "ossImageUrl": cos_oss_url, + "uploadPathPrefix": "order/" + }, + "goEasy": { + "host": goeasy_host, + "appkey": goeasy_appkey + }, + "otherConfig": { + "morentouxiang": morentouxiang, + "dashouguize": dashouguize + }, + "kefu": { + "link": kefu_link, + "enterpriseId": kefu_enterprise_id + } + } + } + return Response(data) + + except Exception as e: + logger.error(f"获取动态配置失败: {str(e)}", exc_info=True) + return Response({"code": 500, "msg": "服务器内部错误"}, status=500) + + + + +class ShangpinGonggaoView(APIView): + """ + 商品公告和轮播图接口 + 获取类型为1的公告和轮播图 + """ + # 应用节流(限制频率) + throttle_classes = [AnonRateThrottle] + # 新增:允许所有用户访问(不需要认证) + permission_classes = [AllowAny] + def post(self, request): + """ + 处理POST请求,返回公告和轮播图数据 + + 请求: POST /api/peizhi/shangpingonggao/ + 返回: { + "shangpingonggao": "公告内容字符串", + "shangpinlunbo": ["轮播图URL1", "轮播图URL2"] + } + """ + try: + logger.info("收到商品公告和轮播图请求") + # 1. 查询公告(类型为1) + gonggao_obj = Gonggao.objects.filter(leixing=1).first() + # 获取公告内容,如果没有则返回空字符串 + gonggao_content = gonggao_obj.jieshao if gonggao_obj else "" + # 2. 查询轮播图(类型为1,按显示顺序排序) + lunbo_queryset = Lunbo.objects.filter(leixing=1) + # 提取轮播图URL列表 + lunbo_urls = [lunbo.tupian_url for lunbo in lunbo_queryset] + # 3. 构建返回数据 + response_data = { + "shangpingonggao": gonggao_content, + "shangpinlunbo": lunbo_urls + } + logger.info(f"返回数据:公告长度{len(gonggao_content)},轮播图数量{len(lunbo_urls)}") + # 4. 返回响应 + return Response(response_data, status=status.HTTP_200_OK) + except Exception as e: + # 记录异常 + logger.error(f"获取商品公告和轮播图时发生错误:{str(e)}") + # 返回错误响应 + error_data = { + "error": "服务器内部错误", + "detail": "获取数据失败,请稍后重试" + } + return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + +class AdminConfigQueryView(APIView): + """ + 管理员获取系统配置接口 + URL: /peizhi/adpzhq + 方法: POST + 权限: JWT Token认证 + 管理员权限 + 参数: zhanghao (管理员账号) + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + # 获取前端传递的账号 + zhanghao = request.data.get('zhanghao') + if not zhanghao: + return Response({ + 'code': 1, + 'msg': '账号不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 获取当前用户(UserMain实例) + user_main = request.user + + # 验证账号:比较user_main的phone与前端传递的zhanghao + if user_main.phone != zhanghao: + return Response({ + 'code': 1, + 'msg': '账号不匹配', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 验证用户类型是否为管理员 + if user_main.user_type != 'admin': + return Response({ + 'code': 1, + 'msg': '无权限', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 验证管理员扩展表是否存在 + try: + admin_profile = user_main.admin_profile + except AdminProfile.DoesNotExist: + return Response({ + 'code': 1, + 'msg': '无权限', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 查询图片配置 + rule_image = Tupianpeizhi.objects.filter(leixing=1).first() + default_avatar = Tupianpeizhi.objects.filter(leixing=2).first() + + # 查询群配置 + qun_configs = Qunpeizhi.objects.all() + qq_groups = [] + for config in qun_configs: + qq_groups.append({ + 'peizhiid': config.peizhiid, + 'neirong': config.neirong if config.neirong else '', + 'qunid': config.qunid if config.qunid else '', + 'jieshao': config.jieshao if config.jieshao else '' + }) + + # 查询利率配置 + platform_rate_obj = Lilubiao.objects.filter(fadanpingtai='1').first() + shop_rate_obj = Lilubiao.objects.filter(fadanpingtai='3').first() + # 🔴【新增】打手提现抽成利率(字符5) + dashou_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='5').first() + # 🔴【新增】管事提现抽成利率(字符6) + guanshi_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='6').first() + + # 构建返回数据 + data = { + 'images': { + 'rule_image': rule_image.tupian if rule_image and rule_image.tupian else '', + 'default_avatar': default_avatar.tupian if default_avatar and default_avatar.tupian else '' + }, + 'qq_groups': qq_groups, + 'rates': { + 'platform_rate': float(platform_rate_obj.lilu) if platform_rate_obj and platform_rate_obj.lilu else 0.0, + 'shop_rate': float(shop_rate_obj.lilu) if shop_rate_obj and shop_rate_obj.lilu else 0.0, + # 🔴【新增】打手提现抽成利率 + 'dashou_withdraw_rate': float( + dashou_withdraw_obj.lilu) if dashou_withdraw_obj and dashou_withdraw_obj.lilu else 0.0, + # 🔴【新增】管事提现抽成利率 + 'guanshi_withdraw_rate': float( + guanshi_withdraw_obj.lilu) if guanshi_withdraw_obj and guanshi_withdraw_obj.lilu else 0.0 + + } + } + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': data + }) + + except Exception as e: + return Response({ + 'code': 99, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class AdminUploadImageView(APIView): + """ + 管理员上传配置图片接口 + URL: /peizhi/adxgtp + 方法: POST + 权限: JWT Token认证 + 管理员权限 + 参数: zhanghao, leixing (1:打手规则图片, 2:默认头像图片), image (文件) + """ + permission_classes = [permissions.IsAuthenticated] + parser_classes = (MultiPartParser, FormParser) + + def post(self, request): + try: + zhanghao = request.data.get('zhanghao') + leixing = request.data.get('leixing') + image_file = request.FILES.get('image') + + if not zhanghao: + return Response({ + 'code': 1, + 'msg': '账号不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not leixing: + return Response({ + 'code': 2, + 'msg': '图片类型不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not image_file: + return Response({ + 'code': 3, + 'msg': '请选择图片文件', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证用户权限 + user_main = request.user + if user_main.phone != zhanghao: + return Response({ + 'code': 1, + 'msg': '账号不匹配', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + if user_main.user_type != 'admin': + return Response({ + 'code': 1, + 'msg': '无权限', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + try: + admin_profile = user_main.admin_profile + except AdminProfile.DoesNotExist: + return Response({ + 'code': 1, + 'msg': '无权限', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 验证图片类型 + if leixing not in ['1', '2']: + return Response({ + 'code': 4, + 'msg': '图片类型错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证图片文件 + is_valid, error_msg = validate_image(image_file) + if not is_valid: + return Response({ + 'code': 5, + 'msg': f'图片验证失败: {error_msg}', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 根据类型确定固定相对URL和文件名 + if leixing == '1': + # 打手规则图片 + fixed_relative_url = 'a_long/dashouguize.jpg' + oss_file_name = 'dashouguize.jpg' + else: + # 默认头像图片 + fixed_relative_url = 'a_long/morentouxiang.jpg' + oss_file_name = 'morentouxiang.jpg' + + # 构建OSS文件路径 + oss_file_path = fixed_relative_url # a_long/dashouguize.jpg 或 a_long/morentouxiang.jpg + + # 上传到OSS(覆盖原有文件) + image_file.seek(0) # 重置文件指针 + full_url = upload_to_oss(image_file, oss_file_path) + + if not full_url: + return Response({ + 'code': 6, + 'msg': '图片上传到存储桶失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 更新数据库 + with transaction.atomic(): + # 获取或创建图片配置记录 + tupian_peizhi, created = Tupianpeizhi.objects.get_or_create(leixing=int(leixing)) + tupian_peizhi.tupian = fixed_relative_url + tupian_peizhi.save() + + return Response({ + 'code': 0, + 'msg': '图片上传成功', + 'data': { + 'image_url': fixed_relative_url + } + }) + + except Exception as e: + return Response({ + 'code': 99, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class AdminUpdateConfigView(APIView): + """ + 管理员更新系统配置接口(QQ群、利率)- 已修复 + URL: /peizhi/adupdate + 方法: POST + 权限: JWT Token认证 + 管理员权限 + 参数: + - zhanghao: 管理员账号 + - qq_groups: [{"peizhiid": 1, "neirong": "", "qunid": "", "jieshao": ""}, ...] (可选) + - rates: {"platform_rate": 0.1, "shop_rate": 0.2} (可选) + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + zhanghao = request.data.get('zhanghao') + if not zhanghao: + return Response({ + 'code': 1, + 'msg': '账号不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证用户权限 + user_main = request.user + if user_main.phone != zhanghao: + return Response({ + 'code': 1, + 'msg': '账号不匹配', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + if user_main.user_type != 'admin': + return Response({ + 'code': 1, + 'msg': '无权限', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + try: + admin_profile = user_main.admin_profile + except AdminProfile.DoesNotExist: + return Response({ + 'code': 1, + 'msg': '无权限', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + response_data = {} + + # 🔴 修复:更新QQ群配置 - 确保根据peizhiid正确更新对应的记录 + qq_groups = request.data.get('qq_groups') + if qq_groups and isinstance(qq_groups, list): + updated_groups = [] + for group_data in qq_groups: + peizhiid = group_data.get('peizhiid') + if not peizhiid: + continue + + # 🔴 修复:使用filter()确保查询到正确的记录 + qun_configs = Qunpeizhi.objects.filter(peizhiid=peizhiid) + + if qun_configs.exists(): + # 如果存在多个相同peizhiid的记录,取第一个(应该只有一个) + qun_config = qun_configs.first() + + # 更新字段 + neirong = group_data.get('neirong') + qunid = group_data.get('qunid') + jieshao = group_data.get('jieshao') + + if neirong is not None: + qun_config.neirong = neirong or '' + if qunid is not None: + qun_config.qunid = qunid or '' + if jieshao is not None: + qun_config.jieshao = jieshao or '' + + qun_config.save() + else: + # 如果不存在,创建新的(使用提供的peizhiid) + qun_config = Qunpeizhi.objects.create( + peizhiid=peizhiid, + neirong=group_data.get('neirong', '') or '', + qunid=group_data.get('qunid', '') or '', + jieshao=group_data.get('jieshao', '') or '' + ) + + updated_groups.append({ + 'peizhiid': qun_config.peizhiid, + 'neirong': qun_config.neirong, + 'qunid': qun_config.qunid, + 'jieshao': qun_config.jieshao + }) + + if updated_groups: + response_data['qq_groups'] = updated_groups + + # 🔴 修复:更新利率配置 - 确保更新现有记录而不是创建新记录 + rates = request.data.get('rates') + if rates and isinstance(rates, dict): + updated_rates = {} + + # 平台发单利率(类型1,fadanpingtai='1') + platform_rate = rates.get('platform_rate') + if platform_rate is not None: + try: + platform_rate_float = float(platform_rate) + if 0 <= platform_rate_float <= 1: + # 🔴 修复:查询现有记录,如果不存在则创建 + platform_rate_obj = Lilubiao.objects.filter(fadanpingtai='1').first() + + if platform_rate_obj: + # 更新现有记录 + platform_rate_obj.lilu = platform_rate_float + platform_rate_obj.save() + else: + # 创建新记录 + platform_rate_obj = Lilubiao.objects.create( + lilu=platform_rate_float, + fadanpingtai='1' + ) + + updated_rates['platform_rate'] = platform_rate_float + else: + return Response({ + 'code': 3, + 'msg': '平台发单利率必须在0-1之间', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + except ValueError: + return Response({ + 'code': 4, + 'msg': '平台发单利率格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 商家发单利率(类型2,fadanpingtai='2') + shop_rate = rates.get('shop_rate') + if shop_rate is not None: + try: + shop_rate_float = float(shop_rate) + if 0 <= shop_rate_float <= 1: + # 🔴 修复:查询现有记录,如果不存在则创建 + shop_rate_obj = Lilubiao.objects.filter(fadanpingtai='3').first() + + if shop_rate_obj: + # 更新现有记录 + shop_rate_obj.lilu = shop_rate_float + shop_rate_obj.save() + else: + # 创建新记录 + shop_rate_obj = Lilubiao.objects.create( + lilu=shop_rate_float, + fadanpingtai='3' + ) + + updated_rates['shop_rate'] = shop_rate_float + else: + return Response({ + 'code': 5, + 'msg': '商家发单利率必须在0-1之间', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + except ValueError: + return Response({ + 'code': 6, + 'msg': '商家发单利率格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + + + + # 🔴【新增】打手提现抽成利率(类型5,fadanpingtai='5') + dashou_withdraw_rate = rates.get('dashou_withdraw_rate') + if dashou_withdraw_rate is not None: + try: + dashou_withdraw_rate_float = float(dashou_withdraw_rate) + if 0 <= dashou_withdraw_rate_float <= 1: + # 查询现有记录,如果不存在则创建 + dashou_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='5').first() + + if dashou_withdraw_obj: + # 更新现有记录 + dashou_withdraw_obj.lilu = dashou_withdraw_rate_float + dashou_withdraw_obj.save() + else: + # 创建新记录 + dashou_withdraw_obj = Lilubiao.objects.create( + lilu=dashou_withdraw_rate_float, + fadanpingtai='5' + ) + + updated_rates['dashou_withdraw_rate'] = dashou_withdraw_rate_float + else: + return Response({ + 'code': 7, # 注意:这里需要调整错误码,不要重复 + 'msg': '打手提现抽成利率必须在0-1之间', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + except ValueError: + return Response({ + 'code': 8, + 'msg': '打手提现抽成利率格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 🔴【新增】管事提现抽成利率(类型6,fadanpingtai='6') + guanshi_withdraw_rate = rates.get('guanshi_withdraw_rate') + if guanshi_withdraw_rate is not None: + try: + guanshi_withdraw_rate_float = float(guanshi_withdraw_rate) + if 0 <= guanshi_withdraw_rate_float <= 1: + # 查询现有记录,如果不存在则创建 + guanshi_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='6').first() + + if guanshi_withdraw_obj: + # 更新现有记录 + guanshi_withdraw_obj.lilu = guanshi_withdraw_rate_float + guanshi_withdraw_obj.save() + else: + # 创建新记录 + guanshi_withdraw_obj = Lilubiao.objects.create( + lilu=guanshi_withdraw_rate_float, + fadanpingtai='6' + ) + + updated_rates['guanshi_withdraw_rate'] = guanshi_withdraw_rate_float + else: + return Response({ + 'code': 9, + 'msg': '管事提现抽成利率必须在0-1之间', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + except ValueError: + return Response({ + 'code': 10, + 'msg': '管事提现抽成利率格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if updated_rates: + response_data['rates'] = updated_rates + + # 如果没有任何更新 + if not response_data: + return Response({ + 'code': 7, + 'msg': '没有需要更新的数据', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + return Response({ + 'code': 0, + 'msg': '更新成功', + 'data': response_data + }) + + except Exception as e: + return Response({ + 'code': 99, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + +class DashouPaihangView(APIView): + """ + 排行榜接口 + 路径:/peizhi/dsph + 方法:POST + 认证:JWT Token认证 + + 请求参数: + - leixing: 1=今日收入,2=今月收入 + - yonghu_leixing: 1=打手,2=管事,3=商家 + + 返回数据: + - code: 0=成功,其他=失败 + - msg: 提示信息 + - data: { + xianshi: True/False, # 是否展示真实数据 + paihang_list: [ # 排行榜列表 + { + uid: '用户ID', + touxiang: '头像相对URL', + nicheng: '用户昵称', + jine: 收入金额, + mingci: 排名 + }, + ... + ] + } + """ + + # =============== 可配置参数 =============== + # 每次查询返回的最大记录数(可修改) + ZUI_DAO_CHA_XUN_SHU_LIANG = 50 + + # 最少需要多少人才能展示(可修改) + ZUI_SHAO_REN_SHU_YAO_QIU = 1 + + # 第一名最低金额要求(单位:元)(可修改) + DI_YI_MING_ZUI_DI_JIN_E = Decimal('10.00') + + # 管事第一名最低数量要求(可修改) + DI_YI_MING_ZUI_DI_SHU_LIANG = 1 + # =============== 可配置参数结束 =============== + + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 获取请求参数 - 修复参数类型问题 + leixing_str = request.data.get('leixing', '1') + yonghu_leixing_str = request.data.get('yonghu_leixing', '1') + + # 转换为整数 + try: + leixing = int(leixing_str) + except (ValueError, TypeError): + leixing = 1 + + try: + yonghu_leixing = int(yonghu_leixing_str) + except (ValueError, TypeError): + yonghu_leixing = 1 + + # 参数验证 + if leixing not in [1, 2]: + return Response({ + 'code': 400, + 'msg': '参数错误:leixing必须为1(今日)或2(今月)', + 'data': {'xianshi': False} + }) + + if yonghu_leixing not in [1, 2, 3]: + return Response({ + 'code': 400, + 'msg': '参数错误:yonghu_leixing必须为1(打手)、2(管事)或3(商家)', + 'data': {'xianshi': False} + }) + + # 根据用户类型调用不同的查询方法 + if yonghu_leixing == 1: + # 打手排行榜 + paihang_list, xianshi_flag = self.chaxun_dashou_paihang(leixing) + elif yonghu_leixing == 2: + # 管事排行榜 + paihang_list, xianshi_flag = self.chaxun_guanshi_paihang(leixing) + else: + # 商家排行榜 + paihang_list, xianshi_flag = self.chaxun_shangjia_paihang(leixing) + + # 返回结果 + return Response({ + 'code': 0, + 'msg': '获取成功', + 'data': { + 'xianshi': xianshi_flag, + 'paihang_list': paihang_list + } + }) + + except Exception as e: + logger.error(f"排行榜查询失败: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'msg': '服务器内部错误', + 'data': {'xianshi': False} + }) + + def chaxun_dashou_paihang(self, leixing): + """ + 查询打手排行榜 + :param leixing: 1=今日收入,2=今月收入 + :return: (排行榜列表, 是否展示标志) + """ + # 确定查询的字段 + if leixing == 1: + # 今日收入 + shouyi_ziduan = 'jinrishouyi' + else: + # 今月收入 + shouyi_ziduan = 'jinyueshouyi' + + # 查询条件:收入大于0,打手账号状态正常(1=正常) + # 注意:打手表中的zhanghaozhuangtai字段 + chaxun_tiaojian = Q(**{f'{shouyi_ziduan}__gt': 0}) & Q(zhanghaozhuangtai=1) + + # 查询打手扩展表,并关联用户主表 + dashou_list = UserDashou.objects.select_related('user').filter( + chaxun_tiaojian + ).order_by(f'-{shouyi_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] + + # 检查是否满足展示条件 + if len(dashou_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU: + # 人数不足,不展示 + return [], False + + # 检查第一名金额是否达到最低要求 + if leixing == 1: + di_yi_ming_jine = dashou_list[0].jinrishouyi + else: + di_yi_ming_jine = dashou_list[0].jinyueshouyi + + if di_yi_ming_jine < self.DI_YI_MING_ZUI_DI_JIN_E: + # 第一名金额不足,不展示 + return [], False + + # 构建返回数据 + paihang_list = [] + for index, dashou in enumerate(dashou_list): + # 获取用户主表信息 + user_main = dashou.user + + # 打手的昵称直接使用打手扩展表的nicheng字段 + dashou_nicheng = dashou.nicheng or '未设置昵称' + + # 获取头像(相对URL) + touxiang_url = user_main.avatar or '' + + # 获取收入金额 + if leixing == 1: + shouyi_jine = dashou.jinrishouyi + else: + shouyi_jine = dashou.jinyueshouyi + + paihang_list.append({ + 'uid': user_main.yonghuid, + 'touxiang': touxiang_url, + 'nicheng': dashou_nicheng, + 'jine': float(shouyi_jine), # 转换为float方便前端处理 + 'mingci': index + 1 + }) + + return paihang_list, True + + def chaxun_guanshi_paihang(self, leixing): + """ + 查询管事排行榜 + :param leixing: 1=今日,2=今月 + :return: (排行榜列表, 是否展示标志) + """ + # 确定查询的字段 + if leixing == 1: + # 今日充值打手数量 + chaxun_ziduan = 'jinrichongzhi' + else: + # 今月充值打手数量 + chaxun_ziduan = 'jinyuechongzhi' + + # 查询条件:数量大于0,管事状态正常(状态1为正常) + # 注意:管手表中的zhuangtai字段 + chaxun_tiaojian = Q(**{f'{chaxun_ziduan}__gt': 0}) & Q(zhuangtai=1) + + # 查询管事扩展表,并关联用户主表 + guanshi_list = UserGuanshi.objects.select_related('user').filter( + chaxun_tiaojian + ).order_by(f'-{chaxun_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] + + # 检查是否满足展示条件 + if len(guanshi_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU: + # 人数不足,不展示 + return [], False + + # 检查第一名数量是否达到最低要求 + if leixing == 1: + di_yi_ming_shuliang = guanshi_list[0].jinrichongzhi + else: + di_yi_ming_shuliang = guanshi_list[0].jinyuechongzhi + + if di_yi_ming_shuliang < self.DI_YI_MING_ZUI_DI_SHU_LIANG: + # 第一名数量不足,不展示 + return [], False + + # 构建返回数据 + paihang_list = [] + for index, guanshi in enumerate(guanshi_list): + # 获取用户主表信息 + user_main = guanshi.user + + # 管事的昵称需要查询老板扩展表 + boss_profile = UserBoss.objects.filter(user=user_main).first() + guanshi_nicheng = boss_profile.nickname if boss_profile else '未设置昵称' + + # 获取头像(相对URL) + touxiang_url = user_main.avatar or '' + + # 获取数量 + if leixing == 1: + chongzhi_shuliang = guanshi.jinrichongzhi + else: + chongzhi_shuliang = guanshi.jinyuechongzhi + + paihang_list.append({ + 'uid': user_main.yonghuid, + 'touxiang': touxiang_url, + 'nicheng': guanshi_nicheng, + 'jine': float(chongzhi_shuliang), # 这里jine字段实际存储的是数量 + 'mingci': index + 1 + }) + + return paihang_list, True + + def chaxun_shangjia_paihang(self, leixing): + """ + 查询商家排行榜 + :param leixing: 1=今日流水,2=今月流水 + :return: (排行榜列表, 是否展示标志) + """ + # 确定查询的字段 + if leixing == 1: + # 今日流水 + liushui_ziduan = 'jinriliushui' + else: + # 今月流水 + liushui_ziduan = 'jinyueliushui' + + # 查询条件:流水大于0,商家状态正常(状态1为正常) + # 注意:商家表中的zhuangtai字段 + chaxun_tiaojian = Q(**{f'{liushui_ziduan}__gt': 0}) & Q(zhuangtai=1) + + # 查询商家扩展表,并关联用户主表 + shangjia_list = UserShangjia.objects.select_related('user').filter( + chaxun_tiaojian + ).order_by(f'-{liushui_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] + + # 检查是否满足展示条件 + if len(shangjia_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU: + # 人数不足,不展示 + return [], False + + # 检查第一名金额是否达到最低要求 + if leixing == 1: + di_yi_ming_liushui = shangjia_list[0].jinriliushui + else: + di_yi_ming_liushui = shangjia_list[0].jinyueliushui + + if di_yi_ming_liushui < self.DI_YI_MING_ZUI_DI_JIN_E: + # 第一名流水不足,不展示 + return [], False + + # 构建返回数据 + paihang_list = [] + for index, shangjia in enumerate(shangjia_list): + # 获取用户主表信息 + user_main = shangjia.user + + # 商家的昵称需要查询老板扩展表 + boss_profile = UserBoss.objects.filter(user=user_main).first() + shangjia_nicheng = boss_profile.nickname if boss_profile else '未设置昵称' + + # 获取头像(相对URL) + touxiang_url = user_main.avatar or '' + + # 获取流水金额 + if leixing == 1: + liushui_jine = shangjia.jinriliushui + else: + liushui_jine = shangjia.jinyueliushui + + paihang_list.append({ + 'uid': user_main.yonghuid, + 'touxiang': touxiang_url, + 'nicheng': shangjia_nicheng, + 'jine': float(liushui_jine), + 'mingci': index + 1 + }) + + return paihang_list, True + + + + + + + + + + +class GetAdminZhanghaoView(APIView): + """ + 获取管理员账号列表接口 + 路径:/peizhi/adzhhq + 方法:POST + 权限:JWT Token + 管理员验证 + 参数: + - zhanghao: 当前登录的管理员账号(字符串) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 🔥 1. 身份验证(100%按表模型) + user = request.user + + # 获取当前用户的手机号(CharField,字符串类型) + current_phone = getattr(user, 'phone', '') + if not current_phone: + return Response({ + 'code': 401, + 'message': '用户手机号信息不完整', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 检查前端传递的账号是否匹配(都是字符串) + frontend_zhanghao = request.data.get('zhanghao', '').strip() + if frontend_zhanghao != current_phone: + return Response({ + 'code': 403, + 'message': '账号不匹配,无权限访问', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 🔥 2. 验证是否为管理员(user_type是CharField,值是'admin'字符串) + user_type = getattr(user, 'user_type', '') + if user_type != 'admin': + # 检查是否有管理员扩展表 + if not hasattr(user, 'admin_profile'): + return Response({ + 'code': 403, + 'message': '无管理员权限', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 🔥 3. 查询管理员账号列表(全部按字符串查询) + with connection.cursor() as cursor: + # 🔥 查询所有管理员账号(user_type='admin' 字符串) + # 🔥 phone和yonghuid都是CharField,xiugaiid也是CharField + sql = """ + SELECT + um.id, + um.phone, + um.yonghuid, + um.user_type, + COALESCE(COUNT(xjl.id), 0) as jilushuliang + FROM user_main um + LEFT JOIN xiugaijilu xjl ON um.phone = xjl.xiugaiid + WHERE um.user_type = 'admin' + GROUP BY um.id, um.phone, um.yonghuid, um.user_type + ORDER BY um.create_time DESC + """ + + cursor.execute(sql) + results = cursor.fetchall() + + # 🔥 4. 处理查询结果(全部保持字符串类型) + zhanghao_list = [] + for row in results: + zhanghao_list.append({ + #'id': row[0], + 'id': str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串 + 'zhanghao':str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串 + 'yonghuid': str(row[2]) if row[2] else '', # 🔥 保持字符串 + 'user_type': str(row[3]) if row[3] else '', # 🔥 保持字符串 + 'jilushuliang': int(row[4]) # 只有这个是数字 + }) + + return Response({ + 'code': 0, + 'message': '获取成功', + 'data': zhanghao_list + }, status=status.HTTP_200_OK) + + except Exception as e: + print(f"获取管理员账号列表失败: {str(e)}") + traceback.print_exc() + return Response({ + 'code': 500, + 'message': '系统错误', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class GetXiugaiJiluView(APIView): + """ + 获取修改记录列表接口 + 路径:/peizhi/adhqxg + 方法:POST + 权限:JWT Token + 管理员验证 + 参数: + - zhanghao: 当前登录的管理员账号(字符串) + - xiugaizhe_zhanghaoid: 选中的管理员账号ID(字符串,对应phone或yonghuid) + - page: 页码(整数) + - pagesize: 每页条数(整数) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 🔥 1. 身份验证(100%按表模型) + user = request.user + + # 获取当前用户的手机号(CharField,字符串) + current_phone = getattr(user, 'phone', '') + if not current_phone: + return Response({ + 'code': 401, + 'message': '用户手机号信息不完整', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 检查前端传递的账号是否匹配(字符串比较) + frontend_zhanghao = request.data.get('zhanghao', '') + if not isinstance(frontend_zhanghao, str): + frontend_zhanghao = str(frontend_zhanghao) + frontend_zhanghao = frontend_zhanghao.strip() + + if frontend_zhanghao != current_phone: + return Response({ + 'code': 403, + 'message': '账号不匹配,无权限访问', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 🔥 2. 验证是否为管理员(user_type='admin'字符串) + user_type = getattr(user, 'user_type', '') + if user_type != 'admin': + if not hasattr(user, 'admin_profile'): + return Response({ + 'code': 403, + 'message': '无管理员权限', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 🔥 3. 获取参数(100%按实际类型) + # 🔥 xiugaizhe_zhanghaoid 是字符串(对应xiugaiid字段,CharField) + xiugaizhe_zhanghaoid_param = request.data.get('xiugaizhe_zhanghaoid', '') + + + # 🔥 强制转为字符串(因为数据库是CharField) + if xiugaizhe_zhanghaoid_param is None: + xiugaizhe_zhanghaoid = '' + else: + # 无论如何都转为字符串 + xiugaizhe_zhanghaoid = str(xiugaizhe_zhanghaoid_param).strip() + + + # 🔥 分页参数转为整数 + try: + page = int(request.data.get('page', 1)) + except: + page = 1 + + try: + pagesize = int(request.data.get('pagesize', 10)) + except: + pagesize = 10 + + # 🔥 4. 参数验证 + if not xiugaizhe_zhanghaoid: + return Response({ + 'code': 400, + 'message': '修改者账号ID不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if page < 1: + page = 1 + if pagesize < 1 or pagesize > 50: + pagesize = 10 + + # 🔥 5. 高效查询(100%按表字段类型) + with connection.cursor() as cursor: + # 🔥 5.1 查询总记录数(xiugaiid是CharField,用字符串查询) + count_sql = """ + SELECT COUNT(*) + FROM xiugaijilu + WHERE xiugaiid = %s + """ + cursor.execute(count_sql, [xiugaizhe_zhanghaoid]) + total_count = cursor.fetchone()[0] + + # 🔥 5.2 计算分页 + offset = (page - 1) * pagesize + + # 🔥 5.3 查询当前页数据(使用参数化查询,防止SQL注入) + data_sql = """ + SELECT + xjl.id, + xjl.yonghuid, -- CharField + xjl.xiugaiid, -- CharField + xjl.leixing, -- IntegerField + xjl.xiugaitijiao, -- DecimalField + xjl.xiugaitijiaoq, -- DecimalField + xjl.jifen, -- IntegerField + xjl.yjifen, -- IntegerField + xjl.yajin, -- DecimalField + xjl.yyajin, -- DecimalField + xjl.shangjiayue, -- DecimalField + xjl.shangjiayueq, -- DecimalField + xjl.guanshiyue, -- DecimalField + xjl.guanshiyueq, -- DecimalField + xjl.huiyuants, -- IntegerField + xjl.huiyuan_id, -- CharField + xjl.qitashuoming, -- TextField + xjl.create_time, -- DateTimeField + um.avatar -- CharField + FROM xiugaijilu xjl + LEFT JOIN user_main um ON xjl.yonghuid = um.yonghuid + WHERE xjl.xiugaiid = %s + ORDER BY xjl.create_time DESC + LIMIT %s OFFSET %s + """ + + cursor.execute(data_sql, [xiugaizhe_zhanghaoid, pagesize, offset]) + records = cursor.fetchall() + + # 🔥 6. 处理查询结果(严格按照字段类型处理) + jilu_list = [] + for record in records: + # 获取修改者账号信息(都是字符串) + xiugaizhe_zhanghao = '' + with connection.cursor() as cursor2: + # 🔥 phone和yonghuid都是CharField,用字符串查询 + cursor2.execute( + "SELECT phone, yonghuid FROM user_main WHERE phone = %s OR yonghuid = %s LIMIT 1", + [xiugaizhe_zhanghaoid, xiugaizhe_zhanghaoid] + ) + xiugaizhe_info = cursor2.fetchone() + if xiugaizhe_info: + # 🔥 都是字符串 + xiugaizhe_zhanghao = str(xiugaizhe_info[0]) if xiugaizhe_info[0] else str(xiugaizhe_info[1]) if \ + xiugaizhe_info[1] else xiugaizhe_zhanghaoid + + # 获取会员名称(如果有) + huiyuan_mingzi = '' + if record[15]: # huiyuan_id是CharField + with connection.cursor() as cursor3: + cursor3.execute( + "SELECT jieshao FROM huiyuan WHERE huiyuan_id = %s LIMIT 1", + [str(record[15])] # 🔥 转为字符串查询 + ) + huiyuan_info = cursor3.fetchone() + if huiyuan_info: + huiyuan_mingzi = str(huiyuan_info[0]) if huiyuan_info[0] else '' + + # 🔥 严格按照前端字段名映射,保持正确类型 + jilu_data = { + # 基础字段(字符串) + 'id': record[0], + 'beixiugai_yonghuid': str(record[1]) if record[1] else '', # yonghuid + 'xiugaizhe_id': str(record[2]) if record[2] else '', # xiugaiid + 'xiugaizhe_zhanghao': xiugaizhe_zhanghao, # 字符串 + 'beixiugai_yonghuleixing': record[3], # leixing是整数 + + # 打手字段(Decimal转float,Integer转int) + 'xiugaihou_dashoutixianjine': float(record[4]) if record[4] is not None else 0.00, + 'xiugaiqian_dashoutixianjine': float(record[5]) if record[5] is not None else 0.00, + 'xiugaihou_dashoujifen': int(record[6]) if record[6] is not None else 0, + 'xiugaiqian_dashoujifen': int(record[7]) if record[7] is not None else 0, + 'xiugaihou_dashouyajin': float(record[8]) if record[8] is not None else 0.00, + 'xiugaiqian_dashouyajin': float(record[9]) if record[9] is not None else 0.00, + + # 商家字段 + 'xiugaihou_shangjiayue': float(record[10]) if record[10] is not None else 0.00, + 'xiugaiqian_shangjiayue': float(record[11]) if record[11] is not None else 0.00, + + # 管事字段 + 'xiugaihou_guanshiyue': float(record[12]) if record[12] is not None else 0.00, + 'xiugaiqian_guanshiyue': float(record[13]) if record[13] is not None else 0.00, + + # 会员字段 + 'tianjia_huiyuantianshu': int(record[14]) if record[14] is not None else 0, + 'huiyuan_id': str(record[15]) if record[15] else '', # 字符串 + 'huiyuan_mingzi': huiyuan_mingzi, # 字符串 + + # 其他字段 + 'qita_shuoming': str(record[16]) if record[16] else '', # 字符串 + 'xiugai_time': record[17].strftime('%Y-%m-%d %H:%M:%S') if record[17] else None, + 'touxiang': str(record[18]) if record[18] else '' # avatar是字符串 + } + + jilu_list.append(jilu_data) + + # 🔥 7. 分页控制 + queried_records = len(jilu_list) + has_more = False + + if queried_records == pagesize: + total_queried = (page - 1) * pagesize + queried_records + has_more = total_queried < total_count + + return Response({ + 'code': 0, + 'message': '获取成功', + 'data': { + 'list': jilu_list, + 'zongshuliang': total_count, + 'dangqian_page': page, + 'page_size': pagesize, + 'has_more': has_more, + 'shijichaxundediaoshuliang': queried_records + } + }, status=status.HTTP_200_OK) + + except ValueError as e: + print(f"参数错误: {str(e)}") + return Response({ + 'code': 400, + 'message': '参数错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + print(f"获取修改记录失败: {str(e)}") + traceback.print_exc() + return Response({ + 'code': 500, + 'message': '系统错误', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + + + + +class ShangjiaMobanListView(APIView): + """ + 商家订单模板列表接口 + POST /peizhi/sjddmblb + 权限: JWT认证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + yonghu_id = user.yonghuid + + leixing_id = request.data.get('shangpinTypeId') + if not leixing_id: + return Response({'code': 400, 'msg': '商品类型ID不能为空', 'data': {}}, status=400) + + get_type = int(request.data.get('getType', 2)) + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('pageSize', 50)) + + keyword = request.data.get('keyword', '') + min_price = request.data.get('minPrice', '') + label_id = request.data.get('labelId', None) + if label_id is not None: + try: + label_id = int(label_id) + except (ValueError, TypeError): + label_id = None + + query = Q(yonghu_id=yonghu_id, leixing_id=leixing_id) + + if get_type == 1: # 搜索模式 + if keyword: + keyword = str(keyword).strip() + try: + mid = int(keyword) + query &= Q(id=mid) | Q(moban_jieshao__icontains=keyword) + except ValueError: + query &= Q(moban_jieshao__icontains=keyword) + if min_price: + try: + query &= Q(jiage__gte=Decimal(str(min_price))) + except: + pass + if label_id: + query &= Q(chenghao_id=label_id) + + mobans = ShangjiaMoban.objects.filter(query).order_by('-id') + formatted = [] + for moban in mobans: + texiao_json = '' + label_name = '' + if moban.chenghao_id: + try: + ch = Chenghao.objects.get(id=moban.chenghao_id) + label_name = ch.mingcheng + texiao_json = ch.texiao_miaoshu or '' + except Chenghao.DoesNotExist: + pass + formatted.append({ + 'mobanId': moban.id, + 'shangpinTypeId': moban.leixing_id, + 'jieshao': moban.moban_jieshao, + 'jiage': str(moban.jiage), + 'fabushuliang': moban.fabu_shuliang, + 'commissionEnabled': moban.yongjin_qiangdan, + 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None, + 'labelId': moban.chenghao_id, + 'labelName': label_name, + 'texiaoJson': texiao_json, + 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S') if moban.create_time else '' + }) + return Response({'code': 200, 'msg': '搜索成功', 'data': {'list': formatted, 'total': len(formatted), 'hasMore': False}}) + + # 分页模式 + queryset = ShangjiaMoban.objects.filter(query).order_by('-id') + paginator = Paginator(queryset, page_size) + if page < 1 or page > paginator.num_pages: + return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400) + + current_page = paginator.page(page) + formatted = [] + for moban in current_page.object_list: + texiao_json = '' + label_name = '' + if moban.chenghao_id: + try: + ch = Chenghao.objects.get(id=moban.chenghao_id) + label_name = ch.mingcheng + texiao_json = ch.texiao_miaoshu or '' + except Chenghao.DoesNotExist: + pass + formatted.append({ + 'mobanId': moban.id, + 'shangpinTypeId': moban.leixing_id, + 'jieshao': moban.moban_jieshao, + 'jiage': str(moban.jiage), + 'fabushuliang': moban.fabu_shuliang, + 'commissionEnabled': moban.yongjin_qiangdan, + 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None, + 'labelId': moban.chenghao_id, + 'labelName': label_name, + 'texiaoJson': texiao_json, + 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S') if moban.create_time else '' + }) + + has_more = current_page.has_next() + if len(formatted) < page_size: + has_more = False + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'list': formatted, + 'total': paginator.count, + 'hasMore': has_more, + 'currentPage': page, + 'pageSize': page_size, + 'totalPages': paginator.num_pages + } + }) + + except Exception as e: + logger.error(f"获取模板列表失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) + + + + + +class ShangjiaTianjiaMobanView(APIView): + """ + 商家添加订单模板接口 + POST /peizhi/sjtjddmb + 权限: JWT认证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取当前商家用户ID + user = request.user + yonghu_id = user.yonghuid + + # 2. 接收并清洗参数,避免类型错误 + leixing_id = request.data.get('shangpinTypeId') + jieshao = request.data.get('jieshao', '').strip() if request.data.get('jieshao') else '' + jiage = request.data.get('jiage', '') + # 佣金相关 + commission_enabled = request.data.get('commissionEnabled', False) + commission_value = request.data.get('commissionValue', None) + # 标签ID(称号ID) + label_id = request.data.get('labelId', None) + + # 3. 必填验证 + if not leixing_id or not jieshao or not jiage: + logger.warning(f"参数缺失 - leixing_id:{leixing_id}, jieshao:{jieshao}, jiage:{jiage}") + return Response({ + 'code': 400, + 'msg': '缺少必填字段(订单类型、介绍、价格)', + 'data': {} + }, status=400) + + # 4. 价格格式验证 + try: + jiage_decimal = Decimal(str(jiage)) + if jiage_decimal <= 0: + return Response({'code': 400, 'msg': '价格必须大于0', 'data': {}}, status=400) + if jiage_decimal > 10000: + return Response({'code': 400, 'msg': '价格不能超过10000元', 'data': {}}, status=400) + except (InvalidOperation, ValueError, TypeError): + return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400) + + # 5. 介绍长度验证 + if len(jieshao) > 100: + return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400) + + # 6. 佣金验证(若启用) + yongjin_qiangdan = False + yongjin_jiage = None + if commission_enabled: + yongjin_qiangdan = True + if not commission_value: + return Response({'code': 400, 'msg': '开启佣金时,佣金金额不能为空', 'data': {}}, status=400) + try: + comm_decimal = Decimal(str(commission_value)) + if comm_decimal <= 0: + return Response({'code': 400, 'msg': '佣金必须大于0', 'data': {}}, status=400) + yongjin_jiage = comm_decimal + except (InvalidOperation, ValueError, TypeError): + return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400) + + # 7. 标签ID清洗(前端可能传数字或字符串) + cleaned_label_id = None + if label_id is not None and str(label_id).strip(): + try: + cleaned_label_id = int(label_id) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': '标签ID格式错误', 'data': {}}, status=400) + + # 8. 验证商品类型是否存在且可用 + try: + shangpin_obj = ShangpinLeixing.objects.get( + id=leixing_id, + shenhezhuangtai=1 # 审核通过 + ) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 404, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=404) + + # 9. 检查同一用户、同一类型下是否已存在相同介绍的模板 + if ShangjiaMoban.objects.filter( + yonghu_id=yonghu_id, + leixing_id=leixing_id, + moban_jieshao=jieshao + ).exists(): + return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400) + + # 10. 执行数据库创建(事务) + with transaction.atomic(): + moban = ShangjiaMoban.objects.create( + yonghu_id=yonghu_id, + leixing_id=leixing_id, + moban_jieshao=jieshao, + jiage=jiage_decimal, + fabu_shuliang=0, + yongjin_qiangdan=yongjin_qiangdan, + yongjin_jiage=yongjin_jiage, + chenghao_id=cleaned_label_id + ) + + # 11. 返回成功数据(字段与前端对齐) + return Response({ + 'code': 200, + 'msg': '模板创建成功', + 'data': { + 'mobanId': moban.id, + 'shangpinTypeId': leixing_id, + 'jieshao': moban.moban_jieshao, + 'jiage': str(moban.jiage), + 'fabushuliang': moban.fabu_shuliang, + 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S'), + 'commissionEnabled': moban.yongjin_qiangdan, + 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None, + 'labelId': moban.chenghao_id + } + }) + + except Exception as e: + # 打印完整错误堆栈到日志 + logger.error(f"添加模板失败: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'msg': '服务器内部错误,添加模板失败,请联系管理员', + 'data': {} + }, status=500) + + + + +class ShangjiaShanchuMobanView(APIView): + """ + 删除模板 + POST /peizhi/sjddmbsc + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + yonghu_id = user.yonghuid + + moban_id = request.data.get('mobanId') + leixing_id = request.data.get('shangpinTypeId') + + if not all([moban_id, leixing_id]): + return Response({'code': 400, 'msg': '参数缺失', 'data': {}}, status=400) + + try: + moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=yonghu_id, leixing_id=leixing_id) + except ShangjiaMoban.DoesNotExist: + return Response({'code': 404, 'msg': '模板不存在', 'data': {}}, status=404) + + # 强制硬删除 + moban.delete() + + return Response({'code': 200, 'msg': '删除成功', 'data': {'mobanId': moban_id, 'shangpinTypeId': leixing_id}}) + except Exception as e: + logger.error(f"删除模板失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) + + + + + + +class ShangjiaGengxinMobanView(APIView): + """ + 商家更新订单模板接口 + POST /peizhi/sjddmbgx + 权限: JWT认证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + yonghu_id = user.yonghuid + + moban_id = request.data.get('mobanId') + leixing_id = request.data.get('shangpinTypeId') + new_jieshao = request.data.get('jieshao', '').strip() + new_jiage = request.data.get('jiage', '').strip() + # 新增字段 + commission_enabled = request.data.get('commissionEnabled') + commission_value = request.data.get('commissionValue') + label_id = request.data.get('labelId') + + if not all([moban_id, leixing_id]): + return Response({'code': 400, 'msg': '模板ID和商品类型ID不能为空', 'data': {}}, status=400) + + # 查询模板 + try: + moban = ShangjiaMoban.objects.get( + id=moban_id, + yonghu_id=yonghu_id, + leixing_id=leixing_id + ) + except ShangjiaMoban.DoesNotExist: + return Response({'code': 404, 'msg': '模板不存在或无权修改', 'data': {}}, status=404) + + update_fields = [] + + # 修改介绍 + if new_jieshao: + if len(new_jieshao) > 100: + return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400) + if new_jieshao != moban.moban_jieshao: + moban.moban_jieshao = new_jieshao + update_fields.append('moban_jieshao') + + # 修改价格 + if new_jiage: + try: + jiage_decimal = Decimal(new_jiage) + if jiage_decimal <= 0 or jiage_decimal > 10000: + return Response({'code': 400, 'msg': '价格必须在0.1-10000元之间', 'data': {}}, status=400) + if jiage_decimal != moban.jiage: + moban.jiage = jiage_decimal + update_fields.append('jiage') + except (InvalidOperation, ValueError, TypeError): + return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400) + + # 修改标签 + if label_id is not None: # 允许传空字符串或不传,传0表示清空 + new_label = int(label_id) if label_id else None + if new_label != moban.chenghao_id: + moban.chenghao_id = new_label + update_fields.append('chenghao_id') + + # 修改佣金设置 + if commission_enabled is not None: + enabled = bool(commission_enabled) + if enabled: + if not commission_value: + return Response({'code': 400, 'msg': '佣金价格不能为空', 'data': {}}, status=400) + try: + comm_decimal = Decimal(commission_value) + if comm_decimal < 0.1 or comm_decimal > 50: + return Response({'code': 400, 'msg': '佣金需在0.1-50元之间', 'data': {}}, status=400) + if moban.yongjin_qiangdan != True or moban.yongjin_jiage != comm_decimal: + moban.yongjin_qiangdan = True + moban.yongjin_jiage = comm_decimal + update_fields.extend(['yongjin_qiangdan', 'yongjin_jiage']) + except (InvalidOperation, ValueError, TypeError): + return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400) + else: + if moban.yongjin_qiangdan != False or moban.yongjin_jiage is not None: + moban.yongjin_qiangdan = False + moban.yongjin_jiage = None + update_fields.extend(['yongjin_qiangdan', 'yongjin_jiage']) + + if not update_fields: + return Response({'code': 400, 'msg': '没有修改内容', 'data': {}}, status=400) + + # 检查介绍唯一性 + if 'moban_jieshao' in update_fields: + if ShangjiaMoban.objects.filter( + yonghu_id=yonghu_id, + leixing_id=leixing_id, + moban_jieshao=moban.moban_jieshao + ).exclude(id=moban_id).exists(): + return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400) + + with transaction.atomic(): + moban.save(update_fields=update_fields) + + # 返回最新数据 + return Response({ + 'code': 200, + 'msg': '模板修改成功', + 'data': { + 'mobanId': moban.id, + 'shangpinTypeId': moban.leixing_id, + 'jieshao': moban.moban_jieshao, + 'jiage': str(moban.jiage), + 'fabushuliang': moban.fabu_shuliang, + 'commissionEnabled': moban.yongjin_qiangdan, + 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage else None, + 'labelId': moban.chenghao_id, + 'updateTime': moban.update_time.strftime('%Y-%m-%d %H:%M:%S') + } + }) + + except Exception as e: + logger = logging.getLogger(__name__) + logger.error(f"修改商家模板失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) + + + + + +class ShangjiaGenerateLinkView(APIView): + """商家生成发单链接接口(支持模板佣金/标签)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + # 获取参数 + shangpin_type_id = request.data.get('shangpinTypeId') + moban_id = request.data.get('mobanId') + if not all([shangpin_type_id, moban_id]): + return Response({'code': 400, 'msg': '缺少参数', 'data': {}}, status=400) + + try: + shangpin_type_id = int(shangpin_type_id) + moban_id = int(moban_id) + except ValueError: + return Response({'code': 400, 'msg': '参数格式错误', 'data': {}}, status=400) + + # 商家信息 + try: + shangjia = user.shop_profile + except UserShangjia.DoesNotExist: + return Response({'code': 403, 'msg': '商家信息未完善', 'data': {}}, status=403) + if shangjia.zhuangtai != 1: + return Response({'code': 403, 'msg': '商家状态异常', 'data': {}}, status=403) + + # 商品类型 + try: + shangpin_leixing = ShangpinLeixing.objects.get(id=shangpin_type_id, shenhezhuangtai=1) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 400, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=400) + + # 查询模板 + try: + moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=user.yonghuid, leixing_id=shangpin_type_id) + except ShangjiaMoban.DoesNotExist: + return Response({'code': 400, 'msg': '模板不存在', 'data': {}}, status=400) + + jiage = moban.jiage + if jiage > shangjia.yue: + return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元', 'data': {}}, status=400) + + # 利率 + try: + lilu = float(Lilubiao.objects.get(fadanpingtai='3').lilu) or 1.0 + except Lilubiao.DoesNotExist: + lilu = 1.0 + dashou_fencheng = Decimal(str(round(float(jiage) * lilu, 2))) + + # 抢单要求:根据模板是否启用佣金 + chenghao_id = moban.chenghao_id + if moban.yongjin_qiangdan and moban.yongjin_jiage is not None: + final_yaoqiuleixing = 2 # 佣金模式 + final_yongjin = moban.yongjin_jiage + final_huiyuan_id = '' + else: + final_yaoqiuleixing = shangpin_leixing.yaoqiuleixing or 1 + final_yongjin = None + final_huiyuan_id = shangpin_leixing.huiyuan_id or '' + + # 获取标签名称 + label_name = '' + if chenghao_id: + try: + label_name = Chenghao.objects.get(id=chenghao_id).mingcheng + except Chenghao.DoesNotExist: + pass + + # 生成ID和Token + def generate_dingdan_id(): + timestamp = int(time.time() * 1000) + process_id = os.getpid() % 1000 + random_num = random.randint(1000, 9999) + return f"SJ{timestamp}{process_id:03d}{random_num}" + + def generate_secure_token(dd_id, uid): + timestamp = int(time.time() * 1_000_000) + salt = secrets.token_hex(8) + raw = f"{timestamp}:{dd_id}:{uid}:{salt}" + token = hashlib.sha256(raw.encode()).hexdigest()[:24] + # 确保唯一性(极小概率重复再试一次) + if ShangjiaLianjie.objects.filter(lianjie_token=token).exists(): + return generate_secure_token(dd_id, uid) + return token + + def generate_link_url(token): + base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/') + return f"{base}/order/{token}" + + with transaction.atomic(): + dingdan_id = generate_dingdan_id() + secure_token = generate_secure_token(dingdan_id, user.yonghuid) + + # 创建订单主表 + dingdan = Dingdan.objects.create( + dingdan_id=dingdan_id, + zhuangtai=13, + fadan_pingtai=2, + jine=jiage, + dashou_fencheng=dashou_fencheng, + leixing_id=shangpin_type_id, + yaoqiuleixing=final_yaoqiuleixing, + huiyuan_id=final_huiyuan_id, + yongjin=final_yongjin, + jieshao=moban.moban_jieshao[:2000], + user1_id=f"Sj{user.yonghuid}", + tupian=user.avatar or '', + ) + + # 商家扩展表 + DingdanShangjia.objects.create( + dingdan=dingdan, + shangjia_id=user.yonghuid, + sjnicheng=shangjia.nicheng or f"商家{user.yonghuid}", + ) + + # 关联标签 + if chenghao_id: + try: + ch = Chenghao.objects.get(id=chenghao_id) + DingdanBiaoqian.objects.create(dingdan=dingdan, chenghao=ch) + except Chenghao.DoesNotExist: + logger.warning(f"称号{chenghao_id}不存在,跳过关联") + + # 更新商家余额和统计 + UserShangjia.objects.filter(id=shangjia.id).update( + fabu=F('fabu') + 1, + yue=F('yue') - jiage, + jinridingdan=F('jinridingdan') + 1, + jinriliushui=F('jinriliushui') + jiage, + jinyuedingdan=F('jinyuedingdan') + 1, + jinyueliushui=F('jinyueliushui') + jiage, + ) + + # 生成链接并保存 + link_url = generate_link_url(secure_token) + lianjie_obj = ShangjiaLianjie.objects.create( + yonghu_id=user.yonghuid, + leixing_id=shangpin_type_id, + moban_id=moban_id, + lianjie=link_url, + lianjie_token=secure_token, + dingdan_id=dingdan_id, + is_used=False, + youxiao_shijian=timezone.now() + timezone.timedelta(days=1) + ) + + # 更新模板发布数量 + ShangjiaMoban.objects.filter(id=moban_id).update(fabu_shuliang=F('fabu_shuliang') + 1) + shangjia.refresh_from_db() + + logger.info(f"链接生成成功 - 订单ID: {dingdan_id}, 标签: {label_name}") + + return Response({ + 'code': 200, + 'msg': '链接生成成功', + 'data': { + 'linkUrl': link_url, + 'newBalance': str(shangjia.yue), + 'dingdanId': dingdan_id, + 'dingdanJieshao': moban.moban_jieshao, + 'jiage': str(jiage), + 'labelId': chenghao_id, + 'labelName': label_name, + 'youxiaoShijian': lianjie_obj.youxiao_shijian.strftime('%Y-%m-%d %H:%M:%S'), + } + }) + + except Exception as e: + logger.error(f"生成链接事务失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '系统错误,生成链接失败', 'data': {}}, status=500) + + + + +class ShangjiaLianjieListView(APIView): + """获取已生成的链接列表(支持筛选是否使用)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + yonghu_id = user.yonghuid + + moban_id = request.data.get('mobanId') + used = int(request.data.get('used', 0)) # 0未使用,1已使用 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('pageSize', 5)) + page_size = min(page_size, 50) + + if not moban_id: + return Response({'code': 400, 'msg': '模板ID不能为空', 'data': {}}, status=400) + + # 校验模板归属 + try: + moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=yonghu_id) + except ShangjiaMoban.DoesNotExist: + return Response({'code': 404, 'msg': '模板不存在或无权查看', 'data': {}}, status=404) + + links = ShangjiaLianjie.objects.filter( + yonghu_id=yonghu_id, + moban_id=moban_id, + is_used=(used == 1) + ).order_by('-create_time') + + paginator = Paginator(links, page_size) + if page < 1 or page > paginator.num_pages: + return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400) + + current_page = paginator.page(page) + data_list = [] + for link in current_page.object_list: + data_list.append({ + 'id': link.id, + 'lianjie': link.lianjie, + 'lianjie_token': link.lianjie_token, + 'is_used': link.is_used, + 'dingdan_id': link.dingdan_id, + 'youxiao_shijian': link.youxiao_shijian.strftime('%Y-%m-%d %H:%M:%S') if link.youxiao_shijian else '', + 'create_time': link.create_time.strftime('%Y-%m-%d %H:%M:%S') + }) + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'list': data_list, + 'total': paginator.count, + 'hasMore': current_page.has_next(), + 'currentPage': page, + 'pageSize': page_size + } + }) + + except Exception as e: + logger.error(f"获取链接列表失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) + + + + + + +class IPUtils: + """IP工具类""" + + @staticmethod + def get_client_ip(request): + """ + 获取客户端真实IP,处理代理情况 + 优先级: X-Real-IP > X-Forwarded-For > remote_addr + """ + ip = None + + try: + # 1. 尝试从 X-Real-IP 获取 + x_real_ip = request.META.get('HTTP_X_REAL_IP') + if x_real_ip: + ip = x_real_ip.strip() + logger.info(f"从 X-Real-IP 获取IP: {ip}") + return ip + + # 2. 尝试从 X-Forwarded-For 获取(处理代理链) + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + # 可能有多个代理IP,第一个是真实IP + ips = x_forwarded_for.split(',') + for possible_ip in ips: + possible_ip = possible_ip.strip() + if possible_ip and possible_ip.lower() != 'unknown': + ip = possible_ip + logger.info(f"从 X-Forwarded-For 获取IP: {ip}") + return ip + + # 3. 使用 remote_addr + ip = request.META.get('REMOTE_ADDR', '') + logger.info(f"从 REMOTE_ADDR 获取IP: {ip}") + + except Exception as e: + logger.error(f"获取IP失败: {str(e)}", exc_info=True) + ip = '0.0.0.0' + + return ip + + @staticmethod + def validate_ip(ip): + """验证IP格式是否合法""" + if not ip: + return False + + # IPv4正则 + ipv4_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' + + # IPv6正则(简化版) + ipv6_pattern = r'^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$' + + return bool(re.match(ipv4_pattern, ip) or re.match(ipv6_pattern, ip)) + + @staticmethod + def is_private_ip(ip): + """判断是否为内网IP""" + if not ip: + return False + + # 内网IP段 + private_ranges = [ + ('10.0.0.0', '10.255.255.255'), + ('172.16.0.0', '172.31.255.255'), + ('192.168.0.0', '192.168.255.255'), + ('127.0.0.0', '127.255.255.255'), + ] + + try: + # 将IP转换为整数比较 + def ip_to_int(ip): + parts = ip.split('.') + return (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3]) + + ip_num = ip_to_int(ip) + for start, end in private_ranges: + if ip_to_int(start) <= ip_num <= ip_to_int(end): + return True + except: + pass + + return False + + +class OrderLinkThrottle(SimpleRateThrottle): + """订单链接访问频率限制""" + + scope = 'order_link' + rate = '10/minute' # 每分钟最多10次 + + def get_cache_key(self, request, view): + # 基于IP和接口路径进行限制 + ip = IPUtils.get_client_ip(request) + if not ip or IPUtils.is_private_ip(ip): + # 内网IP或不合法IP,使用默认 + ip = 'default' + + return f'throttle_{view.__class__.__name__}_{ip}' + + + + + + + + +class KehuGetDingdanLianjieView(APIView): + """ + 客户获取订单链接信息接口 + 路径:GET /peizhi/khhqddlj?token=xxx + 返回字段包括:订单信息、打手标识、商家标识、俱乐部配置(客服链接、存储桶、GoEasy AppKey等) + """ + + throttle_classes = [OrderLinkThrottle] + permission_classes = [] # 无需认证,任何人都可访问 + + def get(self, request): + """ + 处理GET请求,获取订单链接信息 + """ + # 1. 记录请求开始 + logger.info("=== 客户获取订单链接信息接口开始 ===") + + # 2. 获取客户端真实IP + client_ip = IPUtils.get_client_ip(request) + logger.info(f"客户端IP: {client_ip}") + + # 3. 验证IP合法性 + if not IPUtils.validate_ip(client_ip): + logger.warning(f"IP格式不合法: {client_ip}") + return Response({ + 'code': 400, + 'msg': '网络连接异常', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 获取Token参数 + token = request.query_params.get('token', '') + if not token: + logger.warning(f"Token参数缺失 - IP: {client_ip}") + return Response({ + 'code': 400, + 'msg': '缺少链接参数', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + logger.info(f"接收到Token: {token[:8]}...") + + # 5. 查询链接记录 + try: + lianjie_obj = ShangjiaLianjie.objects.select_related().get(lianjie_token=token) + logger.info(f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}") + except ShangjiaLianjie.DoesNotExist: + logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}") + return Response({ + 'code': 404, + 'msg': '链接不存在或已失效', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"查询链接异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'msg': '系统错误,请稍后重试', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 6. 更新访问者IP(无论是否已使用,都记录最新访问IP) + try: + lianjie_obj.shiyongzhe_ip = client_ip + lianjie_obj.save(update_fields=['shiyongzhe_ip']) + logger.info(f"更新访问者IP成功: {client_ip}") + except Exception as e: + logger.warning(f"更新IP失败: {str(e)}") + # 继续处理,不影响主要逻辑 + + # 7. 检查链接有效期 + '''now = timezone.now() + if lianjie_obj.youxiao_shijian and lianjie_obj.youxiao_shijian < now: + logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.youxiao_shijian}, 当前: {now}") + return Response({ + 'code': 400, + 'msg': '链接已过期', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST)''' + + # 8. 准备返回数据 + response_data = { + 'is_used': lianjie_obj.is_used, + 'dingdan_id': lianjie_obj.dingdan_id or '', + 'youxiao_shijian': lianjie_obj.youxiao_shijian.strftime( + '%Y-%m-%d %H:%M:%S') if lianjie_obj.youxiao_shijian else '', + } + + # 9. 获取订单信息 + if lianjie_obj.dingdan_id: + try: + # 查询订单主表 + dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id) + + # 获取商家昵称 + shangjia_nicheng = '' + try: + user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id) + shangjia_obj = user_main.shop_profile + shangjia_nicheng = shangjia_obj.nicheng + except (UserMain.DoesNotExist, UserShangjia.DoesNotExist): + logger.warning(f"商家信息不存在 - 用户ID: {lianjie_obj.yonghu_id}") + + # ========== 新增:确定俱乐部配置 ========== + # 默认头像(相对路径) + default_avatar = 'a_long/morentouxiang.jpg' + # 初始化配置字段默认值 + kefu_link = '' + oss_url = '' + goeasy_appkey = '' + + # 判断是否为跨平台订单(保留,但不影响配置选择) + is_cross = dingdan_obj.is_cross if hasattr(dingdan_obj, 'is_cross') else 0 + + # 始终使用我方俱乐部配置 + try: + club_config = ClubConfig.objects.filter(is_self=1).first() + if club_config: + logger.info(f"使用我方俱乐部配置,club_id={club_config.club_id}") + kefu_link = club_config.customer_service_api_url or '' + oss_url = club_config.storage_bucket_domain or '' + goeasy_appkey = club_config.chat_api_key or '' + else: + logger.warning("未找到我方俱乐部配置(is_self=1)") + except Exception as e: + logger.error(f"查询我方俱乐部配置异常: {str(e)}", exc_info=True) + + # ========== 构建订单信息 ========== + dingdan_info = { + 'jieshao': dingdan_obj.jieshao or '', + 'jiage': str(dingdan_obj.jine) if dingdan_obj.jine is not None else '0.00', # 金额字段名是 jine + 'shangjia_mingcheng': shangjia_nicheng, + 'zhuangtai': dingdan_obj.zhuangtai, + 'is_cross': is_cross, + # 新增配置字段 + 'kefu_link': kefu_link, + 'oss_url': oss_url, + 'default_avatar': default_avatar, + 'goeasy_appkey': goeasy_appkey, + # 前端用于连接GoEasy的自身用户ID(固定B前缀+商家ID) + 'self_user_id': f"B{lianjie_obj.yonghu_id}" if lianjie_obj.yonghu_id else '', + } + + # 如果已使用,返回已填写的信息 + if lianjie_obj.is_used: + dingdan_info.update({ + 'youxi_nicheng': dingdan_obj.nicheng or '', + 'zhiding_dashou': dingdan_obj.zhiding_id or '', + 'beizhu': dingdan_obj.beizhu or '', + }) + + # 获取聊天相关标识 + # 9.1 获取接单打手ID + jiedan_dashou_id = dingdan_obj.jiedan_dashou_id + dashou_biaoshi = f'Ds{jiedan_dashou_id}' if jiedan_dashou_id else '' + + # 9.2 获取商家标识 + shangjia_biaoshi = '' + try: + shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').filter( + dingdan__dingdan_id=lianjie_obj.dingdan_id + ).first() + + if shangjia_kuozhan and shangjia_kuozhan.shangjia_id: + shangjia_biaoshi = f'Sj{shangjia_kuozhan.shangjia_id}' + logger.info(f"生成商家标识: {shangjia_biaoshi}") + else: + logger.info(f"订单 {lianjie_obj.dingdan_id} 没有商家订单扩展信息") + except Exception as e: + logger.error(f"查询商家订单扩展异常: {str(e)}", exc_info=True) + + # 添加聊天标识 + dingdan_info.update({ + 'dashou_biaoshi': dashou_biaoshi, + 'shangjia_biaoshi': shangjia_biaoshi, + 'you_dashou': bool(dashou_biaoshi), + }) + + response_data.update(dingdan_info) + + except Dingdan.DoesNotExist: + logger.error(f"订单不存在 - 订单ID: {lianjie_obj.dingdan_id}") + response_data.update({ + 'jieshao': '', + 'jiage': '0.00', + 'shangjia_mingcheng': '', + 'zhuangtai': None, + 'dashou_biaoshi': '', + 'shangjia_biaoshi': '', + 'you_dashou': False, + 'is_cross': 0, + 'kefu_link': '', + 'oss_url': '', + 'default_avatar': 'a_long/morentouxiang.jpg', + 'goeasy_appkey': '', + 'self_user_id': '', + }) + except Exception as e: + logger.error(f"获取订单信息异常: {str(e)}", exc_info=True) + response_data.update({ + 'jieshao': '', + 'jiage': '0.00', + 'shangjia_mingcheng': '', + 'zhuangtai': None, + 'dashou_biaoshi': '', + 'shangjia_biaoshi': '', + 'you_dashou': False, + 'is_cross': 0, + 'kefu_link': '', + 'oss_url': '', + 'default_avatar': 'a_long/morentouxiang.jpg', + 'goeasy_appkey': '', + 'self_user_id': '', + }) + else: + # 如果订单ID为空 + logger.warning(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}") + response_data.update({ + 'jieshao': '', + 'jiage': '0.00', + 'shangjia_mingcheng': '', + 'zhuangtai': None, + 'dashou_biaoshi': '', + 'shangjia_biaoshi': '', + 'you_dashou': False, + 'is_cross': 0, + 'kefu_link': '', + 'oss_url': '', + 'default_avatar': 'a_long/morentouxiang.jpg', + 'goeasy_appkey': '', + 'self_user_id': '', + }) + + # 10. 判断订单状态是否允许修改 + if not lianjie_obj.is_used and response_data.get('zhuangtai') == 13: + response_data['allow_modify'] = True + else: + response_data['allow_modify'] = False + + # 11. 记录成功日志并返回 + logger.info( + f"返回订单信息成功 - Token: {token[:8]}..., IP: {client_ip}, " + f"是否允许修改: {response_data['allow_modify']}, " + f"是否有打手: {response_data.get('you_dashou', False)}" + ) + + return Response({ + 'code': 200, + 'msg': '成功', + 'data': response_data + }, status=status.HTTP_200_OK) + + + + + + + +class KehuTianxieDingdanView(APIView): + """ + 客户填写订单信息接口 + 路径:POST /peizhi/kehutx + """ + + # 添加频率限制(比查询接口更严格) + throttle_classes = [OrderLinkThrottle] + permission_classes = [] # 无需认证 + + def post(self, request): + """ + 处理POST请求,填写订单信息 + """ + # 1. 记录请求开始 + logger.info("=== 客户填写订单信息接口开始 ===") + + # 2. 获取客户端真实IP + client_ip = IPUtils.get_client_ip(request) + logger.info(f"客户端IP: {client_ip}") + + # 3. 验证IP合法性 + if not IPUtils.validate_ip(client_ip): + logger.warning(f"IP格式不合法: {client_ip}") + return Response({ + 'code': 400, + 'msg': '网络连接异常', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 获取请求参数 + data = request.data + token = data.get('token', '') + youxi_nicheng = data.get('youxi_nicheng', '').strip() + zhiding_dashou = data.get('zhiding_dashou', '').strip() # 可选 + beizhu = data.get('beizhu', '').strip() # 可选 + + + # 4.0 临时关闭指定功能(如需开启,请删除或注释掉下面两行) + #if zhiding_dashou: + #return Response({'code': 8, 'msg': '指定功能尚未开启,请不要填写指定打手ID', 'data': None}) + #if beizhu: + #return Response({'code': 8, 'msg': '备注功能尚未开启,请不要填写备注', 'data': None}) + + + logger.info( + f"请求参数 - Token: {token[:8]}..., 昵称: {youxi_nicheng[:10]}..., 指定打手: {zhiding_dashou}, 备注长度: {len(beizhu)}") + + # 5. 验证必填参数 + if not token: + logger.warning(f"Token参数缺失 - IP: {client_ip}") + return Response({ + 'code': 400, + 'msg': '缺少链接参数', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not youxi_nicheng: + logger.warning(f"游戏昵称为空 - IP: {client_ip}") + return Response({ + 'code': 400, + 'msg': '游戏昵称不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 6. 验证游戏昵称格式 + if len(youxi_nicheng) > 50: + logger.warning(f"游戏昵称过长 - IP: {client_ip}, 长度: {len(youxi_nicheng)}") + return Response({ + 'code': 400, + 'msg': '游戏昵称不能超过50个字符', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 过滤危险字符 + danger_chars = ['<', '>', "'", '"', '&', ';', '|'] + for char in danger_chars: + if char in youxi_nicheng: + logger.warning(f"游戏昵称包含危险字符 - IP: {client_ip}, 字符: {char}") + return Response({ + 'code': 400, + 'msg': '昵称包含非法字符', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 7. 验证指定打手ID格式(如果提供) + if zhiding_dashou and len(zhiding_dashou) > 32: + logger.warning(f"指定打手ID过长 - IP: {client_ip}, 长度: {len(zhiding_dashou)}") + return Response({ + 'code': 400, + 'msg': '指定打手ID不能超过32个字符', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 8. 验证备注长度 + if len(beizhu) > 500: + logger.warning(f"备注过长 - IP: {client_ip}, 长度: {len(beizhu)}") + return Response({ + 'code': 400, + 'msg': '备注不能超过500个字符', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 9. 查询链接记录 + try: + lianjie_obj = ShangjiaLianjie.objects.get(lianjie_token=token) + logger.info( + f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}, 是否已使用: {lianjie_obj.is_used}") + except ShangjiaLianjie.DoesNotExist: + logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}") + return Response({ + 'code': 404, + 'msg': '链接不存在或已失效', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 10. 检查链接有效期 + now = timezone.now() + if lianjie_obj.youxiao_shijian and lianjie_obj.youxiao_shijian < now: + logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.youxiao_shijian}") + return Response({ + 'code': 400, + 'msg': '链接已过期', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 11. 检查链接是否已使用 + if lianjie_obj.is_used: + logger.warning(f"链接已使用 - 链接ID: {lianjie_obj.id}, IP: {client_ip}") + return Response({ + 'code': 400, + 'msg': '链接已被使用,无法重复提交', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 12. 验证指定打手(如果需要) + zhuangtai_xin = 1 # 默认普通订单状态 + if zhiding_dashou: + # 验证打手是否存在且有效 + try: + # 先查询用户主表 + dashou_user = UserMain.objects.get(yonghuid=zhiding_dashou) + logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}") + + # 通过反向关系获取打手扩展信息 + try: + dashou_profile = dashou_user.dashou_profile + + # 检查打手状态(zhuangtai) + if dashou_profile.zhuangtai != 1: + logger.warning(f"打手状态异常 - 打手ID: {zhiding_dashou}, 状态: {dashou_profile.zhuangtai}") + return Response({ + 'code': 400, + 'msg': '指定打手当前无法接单', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 检查打手账号状态(zhanghaozhuangtai) + if dashou_profile.zhanghaozhuangtai != 1: + logger.warning( + f"打手账号状态异常 - 打手ID: {zhiding_dashou}, 账号状态: {dashou_profile.zhanghaozhuangtai}") + return Response({ + 'code': 400, + 'msg': '指定打手账号异常', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证通过,设置为指定订单状态 + zhuangtai_xin = 7 # 指定打手订单状态 + logger.info(f"指定打手验证通过 - 打手ID: {zhiding_dashou}, 昵称: {dashou_profile.nicheng}") + + except UserDashou.DoesNotExist: + logger.warning(f"打手扩展信息不存在 - 用户ID: {zhiding_dashou}") + return Response({ + 'code': 400, + 'msg': '指定打手信息不完整', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + except UserMain.DoesNotExist: + logger.warning(f"指定打手用户不存在 - 用户ID: {zhiding_dashou}") + return Response({ + 'code': 400, + 'msg': '指定打手不存在', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 13. 使用事务确保数据一致性 + try: + with transaction.atomic(): + # 更新链接记录 + lianjie_obj.shiyongzhe_ip = client_ip + lianjie_obj.is_used = True + lianjie_obj.save(update_fields=['shiyongzhe_ip', 'is_used']) + logger.info(f"更新链接记录成功 - 链接ID: {lianjie_obj.id}, 设置为已使用") + + # 查询订单 + if not lianjie_obj.dingdan_id: + logger.error(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}") + raise ValueError("订单ID为空") + + dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id) + + # 检查订单状态是否为13(灰色状态) + if dingdan_obj.zhuangtai != 13: + logger.warning( + f"订单状态不允许修改 - 订单ID: {dingdan_obj.dingdan_id}, 当前状态: {dingdan_obj.zhuangtai}") + # 回滚事务 + transaction.set_rollback(True) + return Response({ + 'code': 400, + 'msg': '订单状态异常,无法修改', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 更新订单信息 + dingdan_obj.nicheng = youxi_nicheng # 游戏昵称 + dingdan_obj.beizhu = beizhu if beizhu else '' # 备注 + dingdan_obj.zhuangtai = zhuangtai_xin # 新状态:1或7 + + # 如果是指定打手,设置zhiding_id + if zhiding_dashou and zhuangtai_xin == 7: + dingdan_obj.zhiding_id = zhiding_dashou + + dingdan_obj.save() + logger.info( + f"更新订单成功 - 订单ID: {dingdan_obj.dingdan_id}, 新状态: {zhuangtai_xin}, 指定打手: {zhiding_dashou or '无'}") + + # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用) + order_info = { + 'dingdan_id': dingdan_obj.dingdan_id, + 'game_type': self._get_game_type_name(dingdan_obj.leixing_id), + 'amount': str(dingdan_obj.jine), + 'order_desc': (dingdan_obj.jieshao or '')[:50], + } + dingdan_guangbo.delay(order_info) # 瞬间返回,不阻塞请求 + + # 获取商家信息用于返回 + try: + user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id) + shangjia_obj = user_main.shop_profile + shangjia_mingcheng = shangjia_obj.nicheng + except: + shangjia_mingcheng = f"商家{lianjie_obj.yonghu_id}" + logger.warning(f"获取商家昵称失败,使用默认 - 用户ID: {lianjie_obj.yonghu_id}") + + # ========== 新增:判断是否需要跨平台同步 ========== + # 获取商家ID(从链接记录) + shangjia_user_id = lianjie_obj.yonghu_id + # 跨平台条件:商家ID在白名单、订单为普通状态(zhuangtai=1)、要求类型为1、会员ID等于配置值 + sync_order_to_partners(dingdan_obj.dingdan_id) + # ========== 新增结束 ========== + + # 14. 返回成功响应 + response_data = { + #'dingdan_id': dingdan_obj.dingdan_id, + #'create_time': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S'), + #'shangpin_mingcheng': dingdan_obj.jieshao[:50] if dingdan_obj.jieshao else '', # 截取前50字符 + #'jiage': str(dingdan_obj.jine) if dingdan_obj.jine else '0.00', + 'shangjia_mingcheng': shangjia_mingcheng, + 'youxi_nicheng': youxi_nicheng, + 'zhuangtai': zhuangtai_xin, + 'zhuangtai_text': '普通订单' if zhuangtai_xin == 1 else '指定打手订单' + } + + logger.info(f"订单提交成功 - 订单ID: {dingdan_obj.dingdan_id}, IP: {client_ip}") + + # 新方法(行为驱动,更安全) + update_shangjia_daily( + yonghuid=lianjie_obj.yonghu_id, + amount=Decimal(str(dingdan_obj.jine)), + action=1 # 1 = 派发 + ) + + return Response({ + 'code': 200, + 'msg': '提交成功', + 'data': response_data + }, status=status.HTTP_200_OK) + + except Dingdan.DoesNotExist: + logger.error(f"订单不存在 - 订单ID: {lianjie_obj.dingdan_id}") + return Response({ + 'code': 500, + 'msg': '订单数据异常', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + except Exception as e: + logger.error(f"处理订单异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'msg': '系统错误,请稍后重试', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def _get_game_type_name(self, leixing_id): + try: + gt = ShangpinLeixing.objects.filter(id=leixing_id).first() + return gt.jieshao if gt else '游戏订单' + except Exception as e: + logger.error(f"获取游戏类型名称异常: {e}") + return '游戏订单' + + def send_order_notification_async(self, dingdan, youxi_nicheng, zhiding_dashou, zhuangtai): + """异步发送订单通知(客户填写订单后)""" + try: + # 避免阻塞主线程,使用线程异步执行 + def notification_task(): + try: + # 获取游戏类型名称 + try: + leixing_obj = ShangpinLeixing.objects.get(id=dingdan.leixing_id) + game_type = leixing_obj.jieshao + except: + game_type = '未知游戏' + + # 构建订单信息 + order_info = { + 'dingdan_id': dingdan.dingdan_id, + 'game_type': game_type, + 'amount': str(dingdan.jine), + 'order_desc': dingdan.jieshao[:50] if dingdan.jieshao else '新订单', + 'youxi_nicheng': youxi_nicheng, + 'order_type': '指定订单' if zhuangtai == 7 else '普通订单', + 'zhiding_dashou': zhiding_dashou or '' + } + + # 调用微信广播发送器 + sender = WeixinBroadcastSender() + result = sender.broadcast_order(order_info) + + if result['success']: + logger.info(f"【客户填写订单】微信通知发送成功: {result['msg']}") + else: + logger.warning(f"【客户填写订单】微信通知发送失败: {result['msg']}") + + except Exception as e: + logger.error(f"【客户填写订单】发送通知任务异常: {str(e)}") + + # 启动异步线程 + thread = threading.Thread(target=notification_task) + thread.daemon = True + thread.start() + + except Exception as e: + logger.error(f"【客户填写订单】启动通知线程异常: {str(e)}") + + + + + + +class GuanZhuAListView(APIView): + """ + 获取关注阿龙页面图片列表 + POST /peizhi/gzal + 需要JWT认证 + 返回: + { + "code": 0, + "msg": "success", + "data": { + "images": ["相对URL1", "相对URL2", ...], + "last_update": "2025-03-16 12:34:56", + "visit_count": 123 + } + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 查询类型为3的所有记录 + qs = Tupianpeizhi.objects.filter(leixing=3) + if not qs.exists(): + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'images': [], + 'last_update': None, + 'visit_count': 0 + } + }, status=status.HTTP_200_OK) + + # 获取所有图片的相对URL + images = list(qs.values_list('tupian', flat=True)) + + # 获取最新更新时间(取所有记录的update_time最大值) + last_update = qs.aggregate(Max('update_time'))['update_time__max'] + last_update_str = last_update.strftime('%Y-%m-%d %H:%M:%S') if last_update else None + + # 原子性增加访问次数 + # 先更新所有记录,再获取更新后的最大次数 + qs.update(fangwen_cishu=F('fangwen_cishu') + 1) + visit_count = qs.aggregate(Max('fangwen_cishu'))['fangwen_cishu__max'] or 0 + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'images': images, + 'last_update': last_update_str, + 'visit_count': visit_count + } + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"获取关注阿龙图片列表失败: {e}", exc_info=True) + return Response({ + 'code': 1, + 'msg': '服务器内部错误', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + +#不用缓存,一天2000次 +class GuanshiQRCodeView(APIView): + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + logger.info("===== 开始生成管事二维码 =====") + current_user = request.user + logger.info(f"当前用户ID: {current_user.yonghuid}") + + try: + try: + guanshi = current_user.guanshi_profile + logger.info(f"管事扩展表查询成功,状态: {guanshi.zhuangtai}") + except UserGuanshi.DoesNotExist: + logger.warning(f"用户 {current_user.yonghuid} 不是管事身份") + return Response( + {'code': 403, 'message': '当前用户不是管事身份', 'data': None}, + status=status.HTTP_403_FORBIDDEN + ) + + if guanshi.zhuangtai != 1: + logger.warning(f"用户 {current_user.yonghuid} 管事状态异常: {guanshi.zhuangtai}") + return Response( + {'code': 403, 'message': '管事账号状态异常', 'data': None}, + status=status.HTTP_403_FORBIDDEN + ) + + invite_code = guanshi.yaoqingma + if not invite_code: + logger.info(f"用户 {current_user.yonghuid} 无邀请码,开始生成") + invite_code = CreateInvitationCode(str(current_user.yonghuid)) + if not VerifyInvitationCode(invite_code): + logger.error(f"邀请码唯一性校验失败,用户: {current_user.yonghuid}") + return Response( + {'code': 500, 'message': '邀请码生成失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + guanshi.yaoqingma = invite_code + guanshi.save(update_fields=['yaoqingma']) + logger.info(f"邀请码生成成功: {invite_code}") + else: + logger.info(f"使用已有邀请码: {invite_code}") + + # 直接获取新的 access_token,不缓存 + access_token = self.get_fresh_access_token() + if not access_token: + logger.error("获取 access_token 失败") + return Response( + {'code': 500, 'message': '获取微信access_token失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + logger.info(f"获取到 access_token (前10位): {access_token[:10]}...") + + encoded_invite = urllib.parse.quote(invite_code, safe='') + page_path = f'pages/dashouduan/dashouduan?inviteCode={encoded_invite}' + wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}' + post_data = { + 'path': page_path, + 'width': 430, + 'auto_color': False, + 'line_color': {'r': 0, 'g': 0, 'b': 0} + } + logger.info(f"调用微信接口生成小程序码,path: {page_path}") + + try: + wx_resp = requests.post(wx_url, json=post_data, timeout=10) + logger.info(f"微信接口响应状态码: {wx_resp.status_code}") + except Exception as e: + logger.error(f"请求微信接口异常: {str(e)}", exc_info=True) + return Response( + {'code': 500, 'message': '调用微信服务失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + if wx_resp.status_code != 200: + error_info = {} + if wx_resp.headers.get('content-type') == 'application/json': + try: + error_info = wx_resp.json() + except: + pass + errmsg = error_info.get('errmsg', '未知错误') + logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}") + if 'errcode' in error_info: + logger.error(f"微信错误码: {error_info.get('errcode')}") + return Response( + {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + content_type = wx_resp.headers.get('content-type', '') + if 'image' not in content_type: + try: + error_info = wx_resp.json() + errmsg = error_info.get('errmsg', '未知错误') + logger.error(f"微信接口返回非图片内容: {errmsg}") + return Response( + {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + except: + logger.error("微信接口返回未知内容,无法解析为JSON") + return Response( + {'code': 500, 'message': '生成二维码失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + timestamp = int(time.time()) + filename = f"{current_user.yonghuid}_{timestamp}.png" + oss_path = f"erweima/guanshi/{filename}" + file_obj = io.BytesIO(wx_resp.content) + logger.info(f"开始上传图片到COS: {oss_path}") + + try: + full_url = upload_to_oss(file_obj, oss_path) + logger.info(f"上传成功,完整URL: {full_url}") + except Exception as e: + logger.error(f"上传到COS异常: {str(e)}", exc_info=True) + return Response( + {'code': 500, 'message': '图片上传失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + if not full_url: + logger.error("upload_to_oss 返回了空 URL") + return Response( + {'code': 500, 'message': '图片上传失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + cos_domain = getattr(settings, 'COS_DOMAIN', '') + if cos_domain and full_url.startswith(cos_domain): + relative_path = full_url[len(cos_domain):].lstrip('/') + else: + relative_path = oss_path + logger.info(f"相对路径: {relative_path}") + + old_url = guanshi.invite_qrcode_url + if old_url: + logger.info(f"删除旧二维码: {old_url}") + try: + delete_from_oss(old_url) + except Exception as e: + logger.warning(f"删除旧二维码失败(不影响主流程): {str(e)}") + + with transaction.atomic(): + guanshi.invite_qrcode_url = relative_path + guanshi.save(update_fields=['invite_qrcode_url']) + logger.info("数据库更新成功") + + return Response({ + 'code': 0, + 'message': 'success', + 'data': {'url': relative_path} + }) + + except Exception as e: + logger.error(f"生成二维码过程中发生未捕获异常: {str(e)}", exc_info=True) + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + def get_fresh_access_token(self): + """ + 直接请求微信接口获取新的 access_token,不缓存,保证最新。 + """ + appid = getattr(settings, 'WEIXIN_APPID', '') + secret = getattr(settings, 'WEIXIN_SECRET', '') + if not appid or not secret: + logger.error("微信小程序配置缺失: WEIXIN_APPID 或 WEIXIN_SECRET 未设置") + return None + + url = 'https://api.weixin.qq.com/cgi-bin/token' + params = { + 'grant_type': 'client_credential', + 'appid': appid, + 'secret': secret + } + try: + logger.info("正在请求微信接口获取最新 access_token...") + resp = requests.get(url, params=params, timeout=5) + data = resp.json() + new_token = data.get('access_token') + if new_token: + logger.info("成功获取最新 access_token") + return new_token + else: + errcode = data.get('errcode', '未知') + errmsg = data.get('errmsg', '未知错误') + logger.error(f"获取 access_token 失败,errcode={errcode}, errmsg={errmsg}") + return None + except Exception as e: + logger.error(f"请求微信 access_token 接口异常: {str(e)}", exc_info=True) + return None + + + + + + + +class ZuzhangHaibaoView(APIView): + """ + 组长专属海报二维码生成接口 + 路径:/peizhi/zuzhanghb + 方法:POST + 权限:JWT认证,仅限组长身份 + 返回: + { + "code": 0, + "message": "success", + "data": { + "url": "erweima/zuzhang/1000001_1234567890.png", + "yaoqingma": "ABC123XYZ" + } + } + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + current_user = request.user + + # 1. 验证组长身份 + try: + zuzhang = current_user.zuzhang_profile + except UserZuzhang.DoesNotExist: + return Response({ + 'code': 403, + 'message': '当前用户不是组长身份', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 2. 验证组长账号状态 + if zuzhang.zhuangtai != 1: + return Response({ + 'code': 403, + 'message': '组长账号状态异常,无法生成海报', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 3. 获取或生成邀请码 + invite_code = zuzhang.yaoqingma + if not invite_code: + invite_code = CreateInvitationCode(str(current_user.yonghuid)) + if not VerifyInvitationCode(invite_code): + logger.error(f"组长邀请码生成失败,用户: {current_user.yonghuid}") + return Response({ + 'code': 500, + 'message': '邀请码生成失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + zuzhang.yaoqingma = invite_code + zuzhang.save(update_fields=['yaoqingma']) + + # 4. 获取微信 access_token + access_token = self.get_cached_access_token() + if not access_token: + return Response({ + 'code': 500, + 'message': '获取微信access_token失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # ===== 修改点:使用 A 接口(getwxacode) ===== + # 对邀请码进行 URL 编码,防止特殊字符破坏路径 + encoded_invite = urllib.parse.quote(invite_code, safe='') + # 构造带参数的完整路径(请根据实际页面调整) + page_path = f'pages/guanshiduan/guanshiduan?inviteCode={encoded_invite}' + + wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}' + post_data = { + 'path': page_path, + 'width': 430, + 'auto_color': False, + 'line_color': {'r': 0, 'g': 0, 'b': 0} + } + + try: + wx_resp = requests.post(wx_url, json=post_data, timeout=10) + except Exception as e: + logger.error(f"请求微信接口失败: {e}") + return Response({ + 'code': 500, + 'message': '调用微信服务失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 处理微信返回结果 + if wx_resp.status_code != 200: + error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {} + errmsg = error_info.get('errmsg', '未知错误') + logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}") + return Response({ + 'code': 500, + 'message': f'生成二维码失败: {errmsg}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + content_type = wx_resp.headers.get('content-type', '') + if 'image' not in content_type: + try: + error_info = wx_resp.json() + errmsg = error_info.get('errmsg', '未知错误') + logger.error(f"微信接口返回非图片内容: {errmsg}") + return Response({ + 'code': 500, + 'message': f'生成二维码失败: {errmsg}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + except: + logger.error("微信接口返回未知内容") + return Response({ + 'code': 500, + 'message': '生成二维码失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 5. 上传到腾讯云 COS + timestamp = int(time.time()) + filename = f"{current_user.yonghuid}_{timestamp}.png" + oss_path = f"erweima/zuzhang/{filename}" + file_obj = io.BytesIO(wx_resp.content) + + try: + full_url = upload_to_oss(file_obj, oss_path) + except Exception as e: + logger.error(f"上传到COS失败: {e}") + return Response({ + 'code': 500, + 'message': '图片上传失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + if not full_url: + return Response({ + 'code': 500, + 'message': '图片上传失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 6. 获取相对路径 + cos_domain = getattr(settings, 'COS_DOMAIN', '') + if cos_domain and full_url.startswith(cos_domain): + relative_path = full_url[len(cos_domain):].lstrip('/') + else: + relative_path = oss_path + + # 7. 删除旧二维码文件 + old_url = zuzhang.haibao_url + if old_url: + delete_from_oss(old_url) + + # 8. 更新数据库中的 haibao_url 字段 + with transaction.atomic(): + zuzhang.haibao_url = relative_path + zuzhang.save(update_fields=['haibao_url']) + + # 9. 返回邀请码和二维码相对路径 + return Response({ + 'code': 0, + 'message': 'success', + 'data': { + 'url': relative_path, + 'yaoqingma': invite_code + } + }) + + def get_cached_access_token(self): + """从缓存获取微信 access_token""" + cache_key = 'wx_mini_access_token' + token = cache.get(cache_key) + if token: + return token + + appid = getattr(settings, 'WEIXIN_APPID', '') + secret = getattr(settings, 'WEIXIN_SECRET', '') + if not appid or not secret: + logger.error("微信小程序配置缺失") + return None + + url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}' + try: + resp = requests.get(url, timeout=5) + data = resp.json() + token = data.get('access_token') + expires_in = data.get('expires_in', 7200) + if token: + cache.set(cache_key, token, 7000) + return token + except Exception as e: + logger.error(f"获取微信access_token失败: {e}") + return None + + + + +class PopupConfigView(APIView): + """ + 获取指定页面的弹窗配置 + 请求格式:POST { "pageKey": "dashouzhongxin" } + 返回格式:{ "code": 0, "data": { "serverTime": "...", "popups": [...] } } + """ + permission_classes = [IsAuthenticated] # JWT 认证 + + def post(self, request): + # 1. 获取当前用户(已通过JWT认证,可直接使用) + user = request.user + if not user or not user.id: + return Response({ + 'code': 401, + 'msg': '用户未登录或认证失败' + }, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 获取参数 pageKey + page_key = request.data.get('pageKey') + if not page_key: + return Response({ + 'code': 400, + 'msg': '缺少参数 pageKey' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 查询页面配置 + try: + popup_page = PopupPage.objects.get(page_key=page_key, is_active=True) + except PopupPage.DoesNotExist: + # 该页面没有配置弹窗,返回空 + return Response({ + 'code': 0, + 'data': { + 'serverTime': timezone.now().isoformat(), + 'popups': [] + } + }) + + # 4. 查询该页面下所有启用的弹窗(且在有效时间范围内) + now = timezone.now() + popups = PopupConfig.objects.filter( + page=popup_page, + is_active=True, + start_time__lte=now, + end_time__gte=now + ).prefetch_related('images') # 预加载图片,避免N+1查询 + # 按 sort_order 排序(model Meta 已定义) + + # 5. 序列化 + serializer = PopupConfigSerializer(popups, many=True) + + # 6. 返回数据 + return Response({ + 'code': 0, + 'data': { + 'serverTime': now.isoformat(), + 'ignore_user_mute': popup_page.ignore_user_mute, # 新增 + 'popups': serializer.data + } + }) + + + + +class GetWithdrawModeView(APIView): + """ + 获取提现模式配置 + 前端请求 /peizhi/hqtxzsym + 返回格式: { "code": 0, "data": { "mode": 1 or 2 } } + """ + permission_classes = [IsAuthenticated] # JWT 认证 + + def post(self, request, *args, **kwargs): + # 尝试获取 ID=1 的记录 + config = WithdrawConfig.objects.filter(id=1).first() + # 如果存在则取实际值,否则默认 2(手动提现) + mode = config.mode if config else 2 + + return Response({ + 'code': 0, + 'data': { + 'mode': mode + } + }) + + + + + +class CheckPhoneAuthView(APIView): + """ + 判断当前登录用户是否需要强制手机号认证 + + 规则: + - 已认证过手机号(shoujihao_renzheng=True) → 不需要 + - 未认证时,检查用户角色及其业务指标是否达到阈值: + 打手:有会员购买记录(Huiyuangoumai 中存在记录) + 商家:发布订单总数(fabu)>= 5 + 管事:充值分佣总额(chongzhifenrun)>= 15 + 组长:分佣总额(fenyong_zonge)>= 2 + 老板:消费总额(zonge)>= 20 + - 满足任一角色的阈值 → 需要认证 + - 否则 → 不需要认证 + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + user = request.user # UserMain 实例 + + # 已经认证过手机号,直接放行 + if user.shoujihao_renzheng: + return Response({'need_auth': False}) + + # 检查各角色是否满足阈值条件 + if self._dashou_meets_threshold(user): + return Response({'need_auth': True}) + if self._shangjia_meets_threshold(user): + return Response({'need_auth': True}) + if self._guanshi_meets_threshold(user): + return Response({'need_auth': True}) + if self._zuzhang_meets_threshold(user): + return Response({'need_auth': True}) + if self._boss_meets_threshold(user): + return Response({'need_auth': True}) + + # 都不满足,无需认证 + return Response({'need_auth': False}) + + # ==================== 各角色阈值判断方法 ==================== + + def _dashou_meets_threshold(self, user): + """ + 打手条件:存在会员购买记录(Huiyuangoumai 表中有该用户的记录) + """ + try: + # 先确认用户是打手 + UserDashou.objects.get(user=user) + except UserDashou.DoesNotExist: + return False + # 检查是否有任意会员购买记录 + return Huiyuangoumai.objects.filter(yonghu_id=user.yonghuid).exists() + + def _shangjia_meets_threshold(self, user): + """商家条件:发布订单总数 fabu >= 5""" + try: + shop = UserShangjia.objects.get(user=user) + except UserShangjia.DoesNotExist: + return False + return shop.fabu >= 5 + + def _guanshi_meets_threshold(self, user): + """管事条件:充值分佣总额 chongzhifenrun >= 15""" + try: + guanshi = UserGuanshi.objects.get(user=user) + except UserGuanshi.DoesNotExist: + return False + return guanshi.chongzhifenrun >= 15 + + def _zuzhang_meets_threshold(self, user): + """组长条件:分佣总额 fenyong_zonge >= 2""" + try: + zuzhang = UserZuzhang.objects.get(user=user) + except UserZuzhang.DoesNotExist: + return False + return zuzhang.fenyong_zonge >= 2 + + def _boss_meets_threshold(self, user): + """老板条件:消费总额 zonge >= 20""" + try: + boss = UserBoss.objects.get(user=user) + except UserBoss.DoesNotExist: + return False return boss.zonge >= 20 \ No newline at end of file diff --git a/dingdan/signals.py b/dingdan/signals.py deleted file mode 100644 index b34f571..0000000 --- a/dingdan/signals.py +++ /dev/null @@ -1,614 +0,0 @@ -""" -阿龙电竞 - 信号处理器(修复Redis导入问题) -""" - - - - - - - - - -# dingdan/signals.py -""" -阿龙电竞 - 订单信号处理器(生产稳定版) -经过验证,100%可靠 -""" - -import sys -import importlib -import logging -from django.db.models.signals import pre_save -from django.dispatch import receiver -from django.conf import settings -from django.utils import timezone - -logger = logging.getLogger(__name__) - -# 🔧 关键修复:确保redis模块正确加载 -def ensure_redis_loaded(): - """确保redis模块正确加载,避免导入冲突""" - if 'redis' not in sys.modules: - # 如果redis模块未加载,尝试导入 - try: - import redis - #logger.debug("✅ Redis模块正常加载") - except ImportError: - #logger.warning("⚠️ Redis模块未安装,尝试自动安装") - import subprocess - subprocess.call([sys.executable, "-m", "pip", "install", "redis==4.5.4", "-q"]) - import redis - - # 确保redis.Redis属性存在 - import redis - if redis.Redis is None: - #logger.warning("⚠️ Redis.Redis属性为None,强制重新加载模块") - importlib.reload(redis) - - return redis - -# 预加载redis模块 -ensure_redis_loaded() - -@receiver(pre_save, sender='dingdan.Dingdan') -def handle_order_status_8(sender, instance, **kwargs): - """ - 订单状态变为8时,提交定时任务 - """ - # 跳过新增的订单 - if instance.pk is None: - return - - try: - # 动态导入,避免循环依赖 - from dingdan.models import Dingdan - - # 获取旧状态 - old = Dingdan.objects.filter(pk=instance.pk).values_list('zhuangtai', flat=True).first() - - # 状态从 非8 变为 8 - if old != 8 and instance.zhuangtai == 8: - #logger.info(f"📅 订单 {instance.dingdan_id} 状态变为8,提交定时任务") - - # 设置时间标记 - instance.status_8_time = timezone.now() - instance.pending_dispatch = True - - # 使用配置中的超时时间 - expire_seconds = getattr(settings, 'ORDER_EXPIRE_SECONDS', 48 * 60 * 60) - - # 提交Celery任务 - try: - from dingdan.tasks import process_expired_order - - task_result = process_expired_order.apply_async( - args=[instance.dingdan_id], - countdown=expire_seconds, - queue='order_tasks', - priority=9 - ) - - #logger.info(f"✅ 任务提交成功,订单: {instance.dingdan_id}, 任务ID: {task_result.id}") - - # 保存任务信息 - instance.auto_task_id = task_result.id - instance.auto_expire_at = timezone.now() + timezone.timedelta(seconds=expire_seconds) - - except Exception as e: - logger.error(f"❌ 任务提交失败: {e}") - instance.pending_dispatch = True - instance.auto_task_id = f"failed_{timezone.now().timestamp()}" - - # 状态从 8 变为 非8 - elif old == 8 and instance.zhuangtai != 8: - instance.status_8_time = None - instance.pending_dispatch = False - instance.auto_task_id = '' - logger.info(f"📅 订单 {instance.dingdan_id} 状态离开8") - - except Exception as e: - logger.error(f"信号处理失败: {e}") - - - - - - - - - - - - - - - - - - - -# 🔥 关键修复:在最开始强制正确导入redis模块 -'''import sys - -# 1. 如果redis模块已加载但有问题,先移除 -if 'redis' in sys.modules: - print(f"🔧 移除已加载的redis模块: {sys.modules['redis']}") - del sys.modules['redis'] - # 同时移除相关模块 - for key in list(sys.modules.keys()): - if key.startswith('redis.') or key == 'redis': - del sys.modules[key] - -# 2. 重新导入redis模块 -try: - import importlib - redis = importlib.import_module('redis') - print(f"✅ 重新导入redis成功: {redis}") - print(f" Redis类: {redis.Redis}") - print(f" Redis类是否为None: {redis.Redis is None}") - - # 验证Redis类是否可用 - if redis.Redis is None: - print("❌ Redis类为None,尝试从源码级别修复") - # 强制重新加载 - import importlib.util - import os - - # 找到redis包路径 - import pkgutil - redis_spec = pkgutil.find_loader('redis') - if redis_spec: - print(f" Redis包路径: {redis_spec.origin}") - # 强制重新加载 - importlib.invalidate_caches() - redis = importlib.import_module('redis') - importlib.reload(redis) - print(f" Redis类(重载后): {redis.Redis}") - -except Exception as e: - print(f"❌ 导入redis失败: {e}") - import traceback - traceback.print_exc() - # 尝试直接安装 - print("尝试直接安装redis包...") - import subprocess - subprocess.call([sys.executable, "-m", "pip", "install", "redis==4.5.4", "-q"]) - -# 3. 确保kombu.transport.redis也能正确导入 -try: - import kombu.transport.redis - print(f"✅ kombu.transport.redis导入成功") -except Exception as e: - print(f"❌ kombu.transport.redis导入失败: {e}") - # 修复:先导入redis,再导入kombu.transport.redis - if 'redis' not in sys.modules: - import redis - # 重新导入 - import importlib - if 'kombu.transport.redis' in sys.modules: - del sys.modules['kombu.transport.redis'] - kombu_transport_redis = importlib.import_module('kombu.transport.redis') - print(f"✅ 重新导入kombu.transport.redis成功") - -# 4. 继续原来的信号处理器代码 -import logging -from django.db.models.signals import pre_save -from django.dispatch import receiver -from django.utils import timezone - -logger = logging.getLogger(__name__) - -@receiver(pre_save, sender='dingdan.Dingdan') -def order_status_8_handler_fixed(sender, instance, **kwargs): - """ - 修复Redis导入问题后的信号处理器 - """ - if instance.pk is None: - return - - try: - # 动态导入,避免启动时循环导入 - from dingdan.models import Dingdan - - # 获取旧状态 - old = Dingdan.objects.filter(pk=instance.pk).values_list('zhuangtai', flat=True).first() - - # 状态从 非8 变为 8 - if old != 8 and instance.zhuangtai == 8: - logger.info(f"📅 订单 {instance.dingdan_id} 状态变8") - - # 设置时间标记 - instance.status_8_time = timezone.now() - instance.pending_dispatch = True - - # 获取超时时间 - expire_seconds = 60 # 测试用60秒 - - # 🔥 再次验证redis模块 - try: - import redis - logger.info(f"🔍 信号处理器内redis模块状态: {redis.Redis}") - if redis.Redis is None: - logger.error("❌ 信号处理器内redis.Redis为None!") - # 使用备用方案 - instance.auto_task_id = f"redis_none_{timezone.now().timestamp()}" - return - except Exception as e: - logger.error(f"❌ 信号处理器内验证redis失败: {e}") - instance.auto_task_id = f"redis_check_failed_{timezone.now().timestamp()}" - return - - # 提交Celery任务 - try: - from dingdan.tasks import process_expired_order - - logger.info(f"🔧 开始提交任务,订单: {instance.dingdan_id}") - - # 使用更安全的方式提交任务 - from celery import current_app - - # 验证current_app的backend - if hasattr(current_app, 'backend'): - logger.info(f"✅ current_app.backend存在: {current_app.backend}") - else: - logger.error("❌ current_app没有backend属性") - - task_result = process_expired_order.apply_async( - args=[instance.dingdan_id], - countdown=expire_seconds, - queue='order_tasks', - priority=9 - ) - - logger.info(f"✅ Celery任务提交成功,订单: {instance.dingdan_id}, 任务ID: {task_result.id}") - - # 保存任务信息 - instance.auto_task_id = task_result.id - instance.auto_expire_at = timezone.now() + timezone.timedelta(seconds=expire_seconds) - - except Exception as e: - logger.error(f"❌ Celery任务提交失败: {type(e).__name__}: {e}") - - # 特别检查Redis相关错误 - if "'NoneType' object has no attribute 'Redis'" in str(e): - logger.error("💥 检测到Redis类为None错误") - logger.error("💥 尝试从当前环境重新导入redis模块") - - # 尝试修复 - try: - import importlib - import redis as redis_module - importlib.reload(redis_module) - logger.info(f"💥 重新加载redis模块后: {redis_module.Redis}") - except Exception as reload_e: - logger.error(f"💥 重新加载失败: {reload_e}") - - # 标记为待调度,由调度器处理 - instance.pending_dispatch = True - instance.auto_task_id = f"failed_{timezone.now().timestamp()}" - - # 状态从 8 变为 非8 - elif old == 8 and instance.zhuangtai != 8: - instance.status_8_time = None - instance.pending_dispatch = False - instance.auto_task_id = '' - logger.info(f"📅 订单 {instance.dingdan_id} 状态离开8") - - except Exception as e: - logger.error(f"订单状态变更处理失败,订单ID: {getattr(instance, 'dingdan_id', '未知')}, 错误: {e}")''' - - - - - - - - - - - - - - - - - - - - - - - - -'''from django.db.models.signals import pre_save -from django.dispatch import receiver -from django.conf import settings -from django.utils import timezone -import logging -import threading -import time - -from dingdan.models import Dingdan - -logger = logging.getLogger(__name__) - - -# 在你的 dingdan/tasks.py 文件末尾添加以下代码 - -from django.db.models.signals import pre_save -from django.dispatch import receiver -from django.utils import timezone -import logging - -from dingdan.models import Dingdan - -logger = logging.getLogger(__name__) - -@receiver(pre_save, sender=Dingdan) -def handle_order_status_change(sender, instance, **kwargs): - """ - 订单状态变化监听器(纯净标记版) - 严格遵循原始逻辑,仅更新数据库标记。 - """ - # 1. 跳过新建的订单 - if instance.pk is None: - return - - # 2. 获取数据库中此订单的旧状态 - try: - old_status = Dingdan.objects.filter(pk=instance.pk).values_list('zhuangtai', flat=True).first() - except Dingdan.DoesNotExist: - return # 旧订单不存在,不处理 - except Exception as e: - logger.error(f"获取订单 {instance.pk} 旧状态失败: {e},跳过信号处理") - return # 出错时静默退出,绝不阻塞订单保存 - - old_status = old_status or 0 # 防None - - # 3. 【核心逻辑】状态从 非8 变为 8 - if old_status != 8 and instance.zhuangtai == 8: - instance.pending_dispatch = True # 标记为待处理 - instance.status_8_time = timezone.now() # 记录变8时间 - # 清空可能存在的旧任务信息(安全起见) - instance.auto_task_id = '' - instance.auto_expire_at = None - logger.info(f"📝 订单 {instance.dingdan_id} 状态变8,已标记为待调度。") - - # 4. 【核心逻辑】状态从 8 变为 非8 - elif old_status == 8 and instance.zhuangtai != 8: - # 清除所有调度标记和任务信息 - instance.pending_dispatch = False - instance.status_8_time = None - instance.auto_task_id = '' - instance.auto_expire_at = None - logger.info(f"📝 订单 {instance.dingdan_id} 状态离开8,已清除所有调度标记。")''' - - -from django.db.models.signals import pre_save -from django.dispatch import receiver -from django.utils import timezone -from datetime import timedelta -from celery.result import AsyncResult -import logging - -from dingdan.models import Dingdan -from dingdan.tasks import process_expired_order # 确保导入你的任务函数 - -'''logger = logging.getLogger(__name__) -@receiver(pre_save, sender=Dingdan) -def handle_order_status_change(sender, instance, **kwargs): - """ - 监听订单状态变化(稳定生产版)- 修复配置问题 - """ - # 1. 跳过新建的订单(无旧状态可比) - if instance.pk is None: - return - - try: - old = sender.objects.get(pk=instance.pk) - except sender.DoesNotExist: - return - # dingdan/signals.py - 修改状态变8的逻辑部分 - - - # 2. 状态变成 8:提交任务 - if old.zhuangtai != 8 and instance.zhuangtai == 8: - from django.conf import settings - from celery import current_app - - # 🔥【核心修复】确保信号上下文中的Celery应用有正确配置 - # 直接从你的Django settings中获取Redis连接字符串,并设置给current_app - if hasattr(settings, 'CELERY_BROKER_URL'): - current_app.conf.update(broker_url=settings.CELERY_BROKER_URL) - else: - # 如果settings里没有,就按你的配置拼接(和celery.py里一致) - redis_password = getattr(settings, 'REDIS_PASSWORD', 'Dujieduze5.3') - redis_host = getattr(settings, 'REDIS_HOST', '172.19.0.3') - redis_port = getattr(settings, 'REDIS_PORT', 6379) - redis_db_celery = getattr(settings, 'REDIS_DB_CELERY', 0) - broker_url = f'redis://:{redis_password}@{redis_host}:{redis_port}/{redis_db_celery}' - current_app.conf.update(broker_url=broker_url) - - countdown_seconds = getattr(settings, 'ORDER_EXPIRE_SECONDS', 172800) # 默认48小时 - - try: - # 现在再提交任务 - job = process_expired_order.apply_async( - args=[instance.dingdan_id], - countdown=countdown_seconds, - queue='order_tasks', - priority=9 - ) - # ✅ 核心:将任务ID和计划时间记录到当前实例 - instance.auto_task_id = job.id - instance.auto_expire_at = timezone.now() + timedelta(seconds=countdown_seconds) - logger.info(f"✅ 订单 {instance.dingdan_id} 状态变8,已提交延时任务,ID: {job.id}") - except Exception as e: - logger.error(f"❌ 提交订单 {instance.dingdan_id} 的延时任务失败: {str(e)}") - return # 处理完毕,直接返回 - - # 3. 状态从 8 被改成别的 -> 尝试撤销任务 (这部分代码不变) - if old.zhuangtai == 8 and instance.zhuangtai != 8: - if old.auto_task_id: - try: - AsyncResult(old.auto_task_id).revoke() - logger.info(f"订单 {instance.dingdan_id} 状态离开8,已撤销原任务: {old.auto_task_id}") - except Exception as e: - logger.warning(f"撤销订单 {instance.dingdan_id} 的任务失败: {str(e)}") - instance.auto_task_id = '' - instance.auto_expire_at = None''' - - - -'''def try_start_celery_task_async(dingdan_id, countdown): - """ - 异步尝试启动Celery任务(在独立线程中运行) - 如果Redis不可用,立即失败,不影响主流程 - """ - def _async_task(): - try: - # 设置超时时间(500毫秒),避免长时间等待 - import socket - socket.setdefaulttimeout(0.5) - - from dingdan.tasks import process_expired_order - from celery.exceptions import TimeoutError - - # 尝试启动任务,设置极短的超时 - task = process_expired_order.apply_async( - args=[dingdan_id], - countdown=countdown, - queue='order_tasks', - priority=9, - expires=countdown + 3600 # 任务1小时后过期,避免堆积 - ) - - # 不等待任务结果,立即返回 - logger.info(f"订单 {dingdan_id} 异步任务已提交(任务ID: {task.id})") - - except Exception as e: - # 记录警告,但不要影响主流程 - logger.warning(f"订单 {dingdan_id} 异步任务提交失败: {str(e)}") - # 这个失败不影响订单保存,系统有补偿机制 - - # 启动异步线程(后台运行,不阻塞主流程) - thread = threading.Thread(target=_async_task) - thread.daemon = True # 设置为守护线程,主线程退出时会自动结束 - thread.start() - -@receiver(pre_save, sender=Dingdan) -def handle_order_status_change_lightweight(sender, instance, **kwargs): - print(f"🚨【信号触发】订单ID: {getattr(instance, 'dingdan_id', 'N/A')}, 新状态: {instance.zhuangtai}") - """ - 轻量级订单状态变化监听器 - 设计原则: - 1. 绝对不阻塞订单保存 - 2. 不依赖外部服务可用性 - 3. 只记录,不执行 - """ - try: - # 1. 快速检查:如果是新建订单,直接返回 - if instance.pk is None: - return - - # 2. 快速获取旧状态(使用数据库当前值,不加载完整对象) - try: - old_status = Dingdan.objects.filter(pk=instance.pk).values_list('zhuangtai', flat=True).first() - except Exception: - old_status = None - - # 3. 状态变化判断 - if old_status is None or old_status != instance.zhuangtai: - # 状态发生变化,记录到订单扩展字段或日志(但不要阻塞) - try: - # 这里只是示例,实际可以根据需要记录状态变化历史 - pass - except: - pass # 即使记录失败也不影响主流程 - - # 4. 特殊状态处理:状态变为8 - if old_status != 8 and instance.zhuangtai == 8: - # 4.1 立即记录日志(不等待) - logger.info(f"订单 {instance.dingdan_id} 状态变为8") - - # 4.2 异步尝试启动定时任务(不阻塞主流程) - # 使用线程池或直接新线程,确保不阻塞 - try: - # 启动异步线程尝试提交Celery任务 - if hasattr(settings, 'CELERY_BROKER_URL') and settings.CELERY_BROKER_URL: - # 在独立线程中尝试,完全不阻塞 - threading.Thread( - target=try_start_celery_task_async, - args=(instance.dingdan_id, settings.ORDER_EXPIRE_SECONDS), - daemon=True - ).start() - except Exception as e: - # 即使线程启动失败也不影响主流程 - logger.warning(f"启动异步任务线程失败: {str(e)}") - - # 5. 状态从8变为其他 - elif old_status == 8 and instance.zhuangtai != 8: - # 状态不再是8,记录日志即可 - # 定时任务执行时会检查状态,如果不是8会自动跳过 - logger.info(f"订单 {instance.dingdan_id} 状态从8变为{instance.zhuangtai}") - - except Exception as e: - # 捕获所有异常,确保不影响订单保存 - # 只记录错误,不抛出异常 - logger.error(f"信号处理异常(已捕获,不影响订单): {str(e)}")''' - - -'''from django.db.models.signals import pre_save -from django.dispatch import receiver -from django.conf import settings -import logging - -from dingdan.models import Dingdan -from dingdan.tasks import process_expired_order - -logger = logging.getLogger(__name__) - -@receiver(pre_save, sender=Dingdan) -def handle_order_status_change(sender, instance, **kwargs): - """ - 监听订单状态变化 - 当状态变为8时:启动48小时延时任务 - 当状态从8变为其他时:任务执行时会自动跳过,无需取消 - """ - try: - # 如果是新建订单,没有旧记录,直接返回 - if instance.pk is None: - return - - # 获取数据库中旧的订单记录 - old_instance = Dingdan.objects.get(pk=instance.pk) - - # ============ 状态从非8变为8 ============ - if old_instance.zhuangtai != 8 and instance.zhuangtai == 8: - # 启动48小时后的延时任务(countdown单位:秒) - task = process_expired_order.apply_async( - args=[instance.dingdan_id], - countdown=settings.ORDER_EXPIRE_SECONDS, # 172800秒 = 48小时 - queue='order_tasks', - priority=9 # 高优先级 - ) - - logger.info( - f"订单 {instance.dingdan_id} 状态变为8,已启动48小时延时任务," - f"任务ID: {task.id}" - ) - - # 记录任务启动成功(可选,可以存储到订单表或日志表) - - # ============ 状态从8变为其他 ============ - # 我们不需要取消任务,因为任务执行时会检查状态是否为8 - # 如果不是状态8,任务会自动跳过处理 - - except Dingdan.DoesNotExist: - # 新订单,无需处理 - pass - except Exception as e: - logger.error(f"处理订单状态变化信号失败,订单ID: {instance.dingdan_id if hasattr(instance, 'dingdan_id') else '未知'}, 错误: {str(e)}") - # 不抛出异常,避免影响订单保存的主流程 - # 信号处理失败不影响订单保存,有周期性检查作为备用方案''' \ No newline at end of file diff --git a/gunicorn.conf.py b/gunicorn.conf.py index 1e0e690..e9550b9 100644 --- a/gunicorn.conf.py +++ b/gunicorn.conf.py @@ -1,28 +1,28 @@ # /opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.443/django/gunicorn.conf.py -# 🔥 绑定的IP和端口(根据你的Nginx配置,后端在8001端口) +# 绑定的IP和端口(根据你的Nginx配置,后端在8001端口) bind = "127.0.0.1:8001" -# 🔥 工作进程数(根据CPU核心数调整,建议:CPU核心数*2+1) +# 工作进程数(根据CPU核心数调整,建议:CPU核心数*2+1) # 查看CPU核心数:cat /proc/cpuinfo | grep "processor" | wc -l workers = 4 -# 🔥 工作模式(使用gevent支持异步,更高效) +# 工作模式(使用gevent支持异步,更高效) #worker_class = "gevent" worker_class = "sync" # 改成这个 -# 🔥 每个worker的最大请求数(防止内存泄漏) +# 每个worker的最大请求数(防止内存泄漏) max_requests = 1000 max_requests_jitter = 50 # 随机抖动,避免所有worker同时重启 -# 🔥 超时时间(秒) +# 超时时间(秒) timeout = 30 keepalive = 2 -# 🔥 预加载应用(减少内存占用,加快请求处理) +# 预加载应用(减少内存占用,加快请求处理) preload_app = True -# 🔥 日志配置 +# 日志配置 # 访问日志:输出到标准输出,便于OnePanel查看 accesslog = "-" # 错误日志:输出到标准错误 @@ -30,23 +30,23 @@ errorlog = "-" # 日志级别 loglevel = "info" -# 🔥 进程名称(便于识别) +# 进程名称(便于识别) proc_name = "a_long_dianjing_gunicorn" -# 🔥 工作进程的用户/组(安全考虑,用非root用户运行) +# 工作进程的用户/组(安全考虑,用非root用户运行) # user = "www-data" # group = "www-data" -# 🔥 工作进程临时目录(避免权限问题) +# 工作进程临时目录(避免权限问题) worker_tmp_dir = "/dev/shm" -# 🔥 限制请求数据大小(防止大文件攻击) +# 限制请求数据大小(防止大文件攻击) limit_request_line = 4094 limit_request_fields = 100 limit_request_field_size = 8190 -# 🔥 优雅退出时间 +# 优雅退出时间 graceful_timeout = 30 -# 🔥 进程守护(如果用systemd管理,这里设为False) +# 进程守护(如果用systemd管理,这里设为False) daemon = False \ No newline at end of file diff --git a/dingdan/migrations/__init__.py b/orders/__init__.py similarity index 100% rename from dingdan/migrations/__init__.py rename to orders/__init__.py diff --git a/dingdan/admin.py b/orders/admin.py similarity index 100% rename from dingdan/admin.py rename to orders/admin.py diff --git a/dingdan/apps.py b/orders/apps.py similarity index 88% rename from dingdan/apps.py rename to orders/apps.py index 0a14712..789b4b7 100644 --- a/dingdan/apps.py +++ b/orders/apps.py @@ -7,4 +7,4 @@ class DingdanConfig(AppConfig): def ready(self): # 导入信号处理,确保信号被注册 - import dingdan.signals \ No newline at end of file + import orders.signals diff --git a/dingdan/cross_platform_tasks.py b/orders/cross_platform_tasks.py similarity index 100% rename from dingdan/cross_platform_tasks.py rename to orders/cross_platform_tasks.py diff --git a/dingdan/migrations/0001_initial.py b/orders/migrations/0001_initial.py similarity index 100% rename from dingdan/migrations/0001_initial.py rename to orders/migrations/0001_initial.py diff --git a/dingdan/migrations/0002_dingdan_auto_expire_at_dingdan_auto_task_id.py b/orders/migrations/0002_dingdan_auto_expire_at_dingdan_auto_task_id.py similarity index 100% rename from dingdan/migrations/0002_dingdan_auto_expire_at_dingdan_auto_task_id.py rename to orders/migrations/0002_dingdan_auto_expire_at_dingdan_auto_task_id.py diff --git a/dingdan/migrations/0003_dingdan_pending_dispatch_dingdan_status_8_time.py b/orders/migrations/0003_dingdan_pending_dispatch_dingdan_status_8_time.py similarity index 100% rename from dingdan/migrations/0003_dingdan_pending_dispatch_dingdan_status_8_time.py rename to orders/migrations/0003_dingdan_pending_dispatch_dingdan_status_8_time.py diff --git a/dingdan/migrations/0004_alter_tuikuanjilu_chuliid.py b/orders/migrations/0004_alter_tuikuanjilu_chuliid.py similarity index 100% rename from dingdan/migrations/0004_alter_tuikuanjilu_chuliid.py rename to orders/migrations/0004_alter_tuikuanjilu_chuliid.py diff --git a/dingdan/migrations/0005_alter_chufajilu_chuliid.py b/orders/migrations/0005_alter_chufajilu_chuliid.py similarity index 100% rename from dingdan/migrations/0005_alter_chufajilu_chuliid.py rename to orders/migrations/0005_alter_chufajilu_chuliid.py diff --git a/dingdan/migrations/0006_chufatupian.py b/orders/migrations/0006_chufatupian.py similarity index 100% rename from dingdan/migrations/0006_chufatupian.py rename to orders/migrations/0006_chufatupian.py diff --git a/dingdan/migrations/0007_chufajilu_ssliyou.py b/orders/migrations/0007_chufajilu_ssliyou.py similarity index 100% rename from dingdan/migrations/0007_chufajilu_ssliyou.py rename to orders/migrations/0007_chufajilu_ssliyou.py diff --git a/dingdan/migrations/0008_dingdan_is_cross_dingdan_partner_order_id.py b/orders/migrations/0008_dingdan_is_cross_dingdan_partner_order_id.py similarity index 100% rename from dingdan/migrations/0008_dingdan_is_cross_dingdan_partner_order_id.py rename to orders/migrations/0008_dingdan_is_cross_dingdan_partner_order_id.py diff --git a/dingdan/migrations/0009_dingdanpingtai_clkf_dingdanpingtai_jjjs_biaoshi.py b/orders/migrations/0009_dingdanpingtai_clkf_dingdanpingtai_jjjs_biaoshi.py similarity index 100% rename from dingdan/migrations/0009_dingdanpingtai_clkf_dingdanpingtai_jjjs_biaoshi.py rename to orders/migrations/0009_dingdanpingtai_clkf_dingdanpingtai_jjjs_biaoshi.py diff --git a/dingdan/migrations/0010_remove_dingdanpingtai_clkf_dingdan_clkf.py b/orders/migrations/0010_remove_dingdanpingtai_clkf_dingdan_clkf.py similarity index 100% rename from dingdan/migrations/0010_remove_dingdanpingtai_clkf_dingdan_clkf.py rename to orders/migrations/0010_remove_dingdanpingtai_clkf_dingdan_clkf.py diff --git a/dingdan/migrations/0011_dingdan_tkly.py b/orders/migrations/0011_dingdan_tkly.py similarity index 100% rename from dingdan/migrations/0011_dingdan_tkly.py rename to orders/migrations/0011_dingdan_tkly.py diff --git a/dingdan/migrations/0012_dingdan_wechat_transaction_id_and_more.py b/orders/migrations/0012_dingdan_wechat_transaction_id_and_more.py similarity index 100% rename from dingdan/migrations/0012_dingdan_wechat_transaction_id_and_more.py rename to orders/migrations/0012_dingdan_wechat_transaction_id_and_more.py diff --git a/dingdan/migrations/0013_alter_chufajilu_qingqiuid.py b/orders/migrations/0013_alter_chufajilu_qingqiuid.py similarity index 100% rename from dingdan/migrations/0013_alter_chufajilu_qingqiuid.py rename to orders/migrations/0013_alter_chufajilu_qingqiuid.py diff --git a/dingdan/migrations/0014_dingdan_dispatch_type_dingdan_partner_club_id.py b/orders/migrations/0014_dingdan_dispatch_type_dingdan_partner_club_id.py similarity index 100% rename from dingdan/migrations/0014_dingdan_dispatch_type_dingdan_partner_club_id.py rename to orders/migrations/0014_dingdan_dispatch_type_dingdan_partner_club_id.py diff --git a/dingdan/migrations/0015_presettlement_orderdashouhistory_and_more.py b/orders/migrations/0015_presettlement_orderdashouhistory_and_more.py similarity index 100% rename from dingdan/migrations/0015_presettlement_orderdashouhistory_and_more.py rename to orders/migrations/0015_presettlement_orderdashouhistory_and_more.py diff --git a/dingdan/migrations/0016_alter_presettlement_dingdan_id_and_more.py b/orders/migrations/0016_alter_presettlement_dingdan_id_and_more.py similarity index 100% rename from dingdan/migrations/0016_alter_presettlement_dingdan_id_and_more.py rename to orders/migrations/0016_alter_presettlement_dingdan_id_and_more.py diff --git a/dingdan/migrations/0017_dingdanpingtai_dianpu_id_and_more.py b/orders/migrations/0017_dingdanpingtai_dianpu_id_and_more.py similarity index 100% rename from dingdan/migrations/0017_dingdanpingtai_dianpu_id_and_more.py rename to orders/migrations/0017_dingdanpingtai_dianpu_id_and_more.py diff --git a/dingdan/migrations/0018_dingdanshangjia_partner_yonghu_id.py b/orders/migrations/0018_dingdanshangjia_partner_yonghu_id.py similarity index 100% rename from dingdan/migrations/0018_dingdanshangjia_partner_yonghu_id.py rename to orders/migrations/0018_dingdanshangjia_partner_yonghu_id.py diff --git a/dingdan/migrations/0019_dingdanpingtai_shangji_dianpu_id_and_more.py b/orders/migrations/0019_dingdanpingtai_shangji_dianpu_id_and_more.py similarity index 100% rename from dingdan/migrations/0019_dingdanpingtai_shangji_dianpu_id_and_more.py rename to orders/migrations/0019_dingdanpingtai_shangji_dianpu_id_and_more.py diff --git a/dingdan/migrations/0020_fadanfenhong_fadan.py b/orders/migrations/0020_fadanfenhong_fadan.py similarity index 100% rename from dingdan/migrations/0020_fadanfenhong_fadan.py rename to orders/migrations/0020_fadanfenhong_fadan.py diff --git a/dingdan/migrations/0021_fadanshensutupian.py b/orders/migrations/0021_fadanshensutupian.py similarity index 100% rename from dingdan/migrations/0021_fadanshensutupian.py rename to orders/migrations/0021_fadanshensutupian.py diff --git a/dingdan/migrations/0022_fadan_partner_club_id_fadan_shenqingren_shenfen.py b/orders/migrations/0022_fadan_partner_club_id_fadan_shenqingren_shenfen.py similarity index 100% rename from dingdan/migrations/0022_fadan_partner_club_id_fadan_shenqingren_shenfen.py rename to orders/migrations/0022_fadan_partner_club_id_fadan_shenqingren_shenfen.py diff --git a/dingdan/migrations/0023_fadanshensutupian_yongtu.py b/orders/migrations/0023_fadanshensutupian_yongtu.py similarity index 100% rename from dingdan/migrations/0023_fadanshensutupian_yongtu.py rename to orders/migrations/0023_fadanshensutupian_yongtu.py diff --git a/dingdan/migrations/0024_fadan_chulizhe_fadan_chulizhe_shenfen.py b/orders/migrations/0024_fadan_chulizhe_fadan_chulizhe_shenfen.py similarity index 100% rename from dingdan/migrations/0024_fadan_chulizhe_fadan_chulizhe_shenfen.py rename to orders/migrations/0024_fadan_chulizhe_fadan_chulizhe_shenfen.py diff --git a/dingdan/migrations/0025_alter_fadan_shenqingren_shenfen.py b/orders/migrations/0025_alter_fadan_shenqingren_shenfen.py similarity index 100% rename from dingdan/migrations/0025_alter_fadan_shenqingren_shenfen.py rename to orders/migrations/0025_alter_fadan_shenqingren_shenfen.py diff --git a/dingdan/migrations/0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more.py b/orders/migrations/0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more.py similarity index 100% rename from dingdan/migrations/0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more.py rename to orders/migrations/0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more.py diff --git a/dingdan/migrations/0027_dingdanpeihu.py b/orders/migrations/0027_dingdanpeihu.py similarity index 100% rename from dingdan/migrations/0027_dingdanpeihu.py rename to orders/migrations/0027_dingdanpeihu.py diff --git a/dingdan/migrations/0028_dingdanshangjia_shangyige_dingdan_id.py b/orders/migrations/0028_dingdanshangjia_shangyige_dingdan_id.py similarity index 100% rename from dingdan/migrations/0028_dingdanshangjia_shangyige_dingdan_id.py rename to orders/migrations/0028_dingdanshangjia_shangyige_dingdan_id.py diff --git a/dingdan/migrations/0029_fadanfenhong_is_individual_rate.py b/orders/migrations/0029_fadanfenhong_is_individual_rate.py similarity index 100% rename from dingdan/migrations/0029_fadanfenhong_is_individual_rate.py rename to orders/migrations/0029_fadanfenhong_is_individual_rate.py diff --git a/dingdan/migrations/0030_fadan_shenqingren_fenhong_jine.py b/orders/migrations/0030_fadan_shenqingren_fenhong_jine.py similarity index 100% rename from dingdan/migrations/0030_fadan_shenqingren_fenhong_jine.py rename to orders/migrations/0030_fadan_shenqingren_fenhong_jine.py diff --git a/dingdan/migrations/0031_dingdan_guanshi_fencheng.py b/orders/migrations/0031_dingdan_guanshi_fencheng.py similarity index 100% rename from dingdan/migrations/0031_dingdan_guanshi_fencheng.py rename to orders/migrations/0031_dingdan_guanshi_fencheng.py diff --git a/houtai/migrations/__init__.py b/orders/migrations/__init__.py similarity index 100% rename from houtai/migrations/__init__.py rename to orders/migrations/__init__.py diff --git a/dingdan/models.py b/orders/models.py similarity index 99% rename from dingdan/models.py rename to orders/models.py index f9c89da..ceee486 100644 --- a/dingdan/models.py +++ b/orders/models.py @@ -1,5 +1,6 @@ from django.db import models + class Dingdan(models.Model): """订单主表""" # 核心编号:唯一且不可为空[citation:3][citation:9] diff --git a/dingdan/tongzhi_tasks.py b/orders/notice_tasks.py similarity index 94% rename from dingdan/tongzhi_tasks.py rename to orders/notice_tasks.py index 874c4b7..c250246 100644 --- a/dingdan/tongzhi_tasks.py +++ b/orders/notice_tasks.py @@ -1,71 +1,71 @@ -# dingdan/tongzhi_tasks.py -import logging -from concurrent.futures import ThreadPoolExecutor, as_completed -from a_long_dianjing.celery_app import app -from yonghu.models import OfficialAccountUser -from utils.weixin_broadcast import WeixinBroadcastSender - -logger = logging.getLogger('weixin_broadcast') - - -@app.task(bind=True, max_retries=3, default_retry_delay=60) -def dingdan_guangbo(self, order_info): - """ - 异步并发广播订单模板消息给所有关注服务号的用户 - order_info 字典字段示例: - { - 'dingdan_id': 'PT123456', - 'game_type': '王者荣耀', - 'amount': '88.00', - 'order_desc': '星耀上王者' - } - """ - sender = WeixinBroadcastSender() - template_data = sender.build_order_message_data(order_info) - order_id = order_info.get('dingdan_id') - - # 查询所有已关注用户 - try: - openids = list( - OfficialAccountUser.objects - .filter(is_subscribed=True) - .values_list('official_openid', flat=True) - ) - except Exception as e: - logger.error(f'查询订阅用户失败: {e}') - raise self.retry(exc=e) - - if not openids: - return {'success': True, 'msg': '无订阅用户'} - - total = len(openids) - logger.info(f'📊 Celery 广播订单 {order_id} 开始,目标人数: {total}') - - success = 0 - fail = 0 - - # 20 个线程并发发送,微信接口耗时 200-500ms,1000 人约 15 秒完成 - with ThreadPoolExecutor(max_workers=20) as executor: - future_map = { - executor.submit(sender.send_to_user, openid, template_data, order_id): openid - for openid in openids - } - for future in as_completed(future_map): - openid = future_map[future] - try: - res = future.result(timeout=30) - if res.get('success'): - success += 1 - else: - fail += 1 - except Exception as e: - fail += 1 - logger.error(f'发送异常 {openid[:10]}...: {e}') - - logger.info(f'🏁 广播完成:成功 {success},失败 {fail},总计 {total}') - return { - 'success': True, - 'total': total, - 'success_count': success, - 'fail_count': fail +# dingdan/notice_tasks.py +import logging +from concurrent.futures import ThreadPoolExecutor, as_completed +from a_long_dianjing.celery_app import app +from users.models import OfficialAccountUser +from utils.weixin_broadcast import WeixinBroadcastSender + +logger = logging.getLogger('weixin_broadcast') + + +@app.task(bind=True, max_retries=3, default_retry_delay=60) +def dingdan_guangbo(self, order_info): + """ + 异步并发广播订单模板消息给所有关注服务号的用户 + order_info 字典字段示例: + { + 'dingdan_id': 'PT123456', + 'game_type': '王者荣耀', + 'amount': '88.00', + 'order_desc': '星耀上王者' + } + """ + sender = WeixinBroadcastSender() + template_data = sender.build_order_message_data(order_info) + order_id = order_info.get('dingdan_id') + + # 查询所有已关注用户 + try: + openids = list( + OfficialAccountUser.objects + .filter(is_subscribed=True) + .values_list('official_openid', flat=True) + ) + except Exception as e: + logger.error(f'查询订阅用户失败: {e}') + raise self.retry(exc=e) + + if not openids: + return {'success': True, 'msg': '无订阅用户'} + + total = len(openids) + logger.info(f'📊 Celery 广播订单 {order_id} 开始,目标人数: {total}') + + success = 0 + fail = 0 + + # 20 个线程并发发送,微信接口耗时 200-500ms,1000 人约 15 秒完成 + with ThreadPoolExecutor(max_workers=20) as executor: + future_map = { + executor.submit(sender.send_to_user, openid, template_data, order_id): openid + for openid in openids + } + for future in as_completed(future_map): + openid = future_map[future] + try: + res = future.result(timeout=30) + if res.get('success'): + success += 1 + else: + fail += 1 + except Exception as e: + fail += 1 + logger.error(f'发送异常 {openid[:10]}...: {e}') + + logger.info(f'🏁 广播完成:成功 {success},失败 {fail},总计 {total}') + return { + 'success': True, + 'total': total, + 'success_count': success, + 'fail_count': fail } \ No newline at end of file diff --git a/orders/signals.py b/orders/signals.py new file mode 100644 index 0000000..5055f15 --- /dev/null +++ b/orders/signals.py @@ -0,0 +1,102 @@ +""" +阿龙电竞 - 订单信号处理器(生产稳定版) +经过验证,100%可靠 +""" + +import sys +import importlib +import logging +from django.db.models.signals import pre_save +from django.dispatch import receiver +from django.conf import settings +from django.utils import timezone + +logger = logging.getLogger(__name__) + +# 🔧 关键修复:确保redis模块正确加载 +def ensure_redis_loaded(): + """确保redis模块正确加载,避免导入冲突""" + if 'redis' not in sys.modules: + # 如果redis模块未加载,尝试导入 + try: + import redis + #logger.debug("✅ Redis模块正常加载") + except ImportError: + #logger.warning("⚠️ Redis模块未安装,尝试自动安装") + import subprocess + subprocess.call([sys.executable, "-m", "pip", "install", "redis==4.5.4", "-q"]) + import redis + + # 确保redis.Redis属性存在 + import redis + if redis.Redis is None: + #logger.warning("⚠️ Redis.Redis属性为None,强制重新加载模块") + importlib.reload(redis) + + return redis + +# 预加载redis模块 +ensure_redis_loaded() + +@receiver(pre_save, sender='dingdan.Dingdan') +def handle_order_status_8(sender, instance, **kwargs): + """ + 订单状态变为8时,提交定时任务 + """ + # 跳过新增的订单 + if instance.pk is None: + return + + try: + # 动态导入,避免循环依赖 + from orders.models import Dingdan + + # 获取旧状态 + old = Dingdan.objects.filter(pk=instance.pk).values_list('zhuangtai', flat=True).first() + + # 状态从 非8 变为 8 + if old != 8 and instance.zhuangtai == 8: + #logger.info(f"📅 订单 {instance.dingdan_id} 状态变为8,提交定时任务") + + # 设置时间标记 + instance.status_8_time = timezone.now() + instance.pending_dispatch = True + + # 使用配置中的超时时间 + expire_seconds = getattr(settings, 'ORDER_EXPIRE_SECONDS', 48 * 60 * 60) + + # 提交Celery任务 + try: + from orders.tasks import process_expired_order + + task_result = process_expired_order.apply_async( + args=[instance.dingdan_id], + countdown=expire_seconds, + queue='order_tasks', + priority=9 + ) + + #logger.info(f"✅ 任务提交成功,订单: {instance.dingdan_id}, 任务ID: {task_result.id}") + + # 保存任务信息 + instance.auto_task_id = task_result.id + instance.auto_expire_at = timezone.now() + timezone.timedelta(seconds=expire_seconds) + + except Exception as e: + logger.error(f"❌ 任务提交失败: {e}") + instance.pending_dispatch = True + instance.auto_task_id = f"failed_{timezone.now().timestamp()}" + + # 状态从 8 变为 非8 + elif old == 8 and instance.zhuangtai != 8: + instance.status_8_time = None + instance.pending_dispatch = False + instance.auto_task_id = '' + logger.info(f"📅 订单 {instance.dingdan_id} 状态离开8") + + except Exception as e: + logger.error(f"信号处理失败: {e}") + + + + diff --git a/dingdan/tasks.py b/orders/tasks.py similarity index 96% rename from dingdan/tasks.py rename to orders/tasks.py index c079059..8a71d45 100644 --- a/dingdan/tasks.py +++ b/orders/tasks.py @@ -14,8 +14,8 @@ from decimal import Decimal import logging # 导入模型 - 确保路径正确 -from dingdan.models import Dingdan, DingdanShangjia -from yonghu.models import UserMain, UserDashou, UserShangjia +from orders.models import Dingdan +from users.models import UserMain, UserDashou, UserShangjia from utils.celery_utils import safe_decimal_operation, log_task_execution, rollback_on_failure logger = logging.getLogger(__name__) @@ -25,9 +25,6 @@ logger = logging.getLogger(__name__) - - - @shared_task(bind=True, max_retries=3, default_retry_delay=60) def process_expired_order(self, dingdan_id): """ @@ -43,13 +40,6 @@ def process_expired_order(self, dingdan_id): order = Dingdan.objects.select_for_update().get( dingdan_id=dingdan_id ) - - - - # 其余代码不变... - # ... 保持原有代码 - - # 再次检查订单状态是否为8 if order.zhuangtai != 8: @@ -124,7 +114,7 @@ def process_expired_order(self, dingdan_id): if fadan_pingtai == 1: try: - from dingdan.utils import update_platform_profit # 导入函数 + from orders.utils import update_platform_profit # 导入函数 update_platform_profit(dingdan_id) except Exception as e: # 平台收益更新失败不影响主流程,但记录日志 diff --git a/dingdan/tests.py b/orders/tests.py similarity index 100% rename from dingdan/tests.py rename to orders/tests.py diff --git a/dingdan/urls.py b/orders/urls.py similarity index 99% rename from dingdan/urls.py rename to orders/urls.py index ef2c3d9..9a39f63 100644 --- a/dingdan/urls.py +++ b/orders/urls.py @@ -65,8 +65,6 @@ urlpatterns = [ - - path('partner_sync_order', csrf_exempt(PartnerSyncOrderView.as_view()), name='东道主派单接口'), path('partner_check_claim', csrf_exempt(PartnerCheckClaimView.as_view()), name='对方抢单询问接口'), @@ -86,6 +84,4 @@ urlpatterns = [ - - ] \ No newline at end of file diff --git a/dingdan/utils.py b/orders/utils.py similarity index 94% rename from dingdan/utils.py rename to orders/utils.py index 0c64ed8..69d97ba 100644 --- a/dingdan/utils.py +++ b/orders/utils.py @@ -1,21 +1,25 @@ - import threading import requests - -from django.conf import settings -from peizhi.models import Club -from shangpin.models import ShangpinLeixing -from dingdan.models import Dingdan # 根据实际路径导入 - +import time import logging from datetime import date +from decimal import Decimal +from django.conf import settings from django.db import transaction from django.db.models import F -from peizhi.models import DailyDispatchStat # 假设模型在 peizhi 应用中 + +from config.models import Club, DailyDispatchStat, DailyIncomeStat, DailyPayoutStat +from orders.models import Dingdan, DingdanPingtai, DingdanShangjia, Lilubiao +from products.models import ShangpinLeixing, Gsfenhong +from users.models import UserDashou, UserGuanshi + +from backend.utils import update_guanshi_daily_by_action logger = logging.getLogger(__name__) + + # 可选:线程池限制(避免大量线程同时创建,建议使用 Celery,这里简单用线程) THREAD_POOL_SIZE = 10 _semaphore = threading.Semaphore(THREAD_POOL_SIZE) @@ -149,7 +153,6 @@ def _send_to_single_partner(dingdan, club): logger.error(f"同步订单异常(尝试 {attempt}/3): {e}") if attempt < 3: - import time time.sleep(2 ** attempt) # 指数退避 else: logger.error(f"同步订单 {dingdan.dingdan_id} 到俱乐部 {club.club_id} 最终失败") @@ -158,7 +161,6 @@ def _send_to_single_partner(dingdan, club): # 获取下单方ID(根据订单类型) def get_order_user_id(order): - from dingdan.models import DingdanPingtai, DingdanShangjia if order.fadan_pingtai == 1: try: ext = DingdanPingtai.objects.get(dingdan=order) @@ -176,18 +178,6 @@ def get_order_user_id(order): -# dingdan/utils/dispatch_stat.py -import logging - -import logging -from datetime import date -from decimal import Decimal -from django.db import transaction -from django.db.models import F -from peizhi.models import DailyDispatchStat # 请根据实际模型位置修改导入路径 - -logger = logging.getLogger(__name__) - def update_daily_dispatch_stat(direction, partner_club_id, dispatch_amount=None, dispatch_count=1, @@ -255,9 +245,6 @@ def update_daily_dispatch_stat(direction, partner_club_id, -from peizhi.models import DailyIncomeStat - -logger = logging.getLogger(__name__) def update_daily_income(amount): @@ -295,9 +282,6 @@ def update_daily_income(amount): -from peizhi.models import DailyPayoutStat - -logger = logging.getLogger(__name__) def update_daily_payout(amount): """ @@ -335,10 +319,6 @@ def update_daily_payout(amount): -from decimal import Decimal - -from dingdan.models import Lilubiao - def calc_shangjia_order_fencheng(jine): """ @@ -374,8 +354,6 @@ def settle_shangjia_order_guanshi_fenhong(order, dashou_id): """ 商家订单结单时结算管事分红(幂等,失败不抛异常阻断主流程)。 """ - from shangpin.models import Gsfenhong - from yonghu.models import UserDashou, UserGuanshi if getattr(order, 'fadan_pingtai', None) != 2: return @@ -433,7 +411,6 @@ def settle_shangjia_order_guanshi_fenhong(order, dashou_id): return try: - from houtai.utils import update_guanshi_daily_by_action update_guanshi_daily_by_action( yonghuid=guanshi_id, action=3, diff --git a/dingdan/views.py b/orders/views.py similarity index 90% rename from dingdan/views.py rename to orders/views.py index 3dee938..6d5a984 100644 --- a/dingdan/views.py +++ b/orders/views.py @@ -1,98 +1,84 @@ +# ==================== 标准库 ==================== import hmac import os -from dingdan.utils import sync_order_to_partners -from django.db.models import F -from django.shortcuts import render -from rest_framework.permissions import AllowAny # 新增这行 -from rest_framework.throttling import AnonRateThrottle -from dingdan.utils import sync_order_to_partners -# dingdan/views.py -import traceback +import json import time import random +import string +import uuid +import traceback +import threading +import requests import hashlib import xml.etree.ElementTree as ET -import requests -import uuid +import xmltodict +import logging from decimal import Decimal -from django.db import transaction +from datetime import datetime + from django.conf import settings +from django.db import models, transaction +from django.db.models import F, Q, Count, OuterRef, Subquery, Prefetch +from django.core.cache import cache +from django.core.paginator import Paginator +from django.core.exceptions import ObjectDoesNotExist +from django.shortcuts import render +from django.http import HttpResponse +from django.utils import timezone + from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import permissions, status +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.parsers import JSONParser +from rest_framework.throttling import AnonRateThrottle +from rest_framework_simplejwt.authentication import JWTAuthentication + +from tencentcloud.common import credential from tencentcloud.common.profile.client_profile import ClientProfile from tencentcloud.common.profile.http_profile import HttpProfile +from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException +from tencentcloud.sts.v20180813 import sts_client, models -from shangpin.models import Shangpin, ShangpinLeixing, Huiyuangoumai -from dingdan.models import Dingdan, DingdanPingtai, Lilubiao, Pingfen, DingdanShangjia, Chufajilu, Liaotian, Chufatupian from utils.weixin_broadcast import WeixinBroadcastSender -from yonghu.models import UserMain, UserDashou, UserShangjia +from utils.goeasy_service import GoEasyService +from utils.chat_utils import _send_group_message, _subscribe_users_to_group, establish_order_chat +from utils.fadan_utils import check_fadan_qiangdan_eligible -from shangdian.utils import calculate_pingtai_and_dianpu_shouyi,validate_shangpin_and_dianpu -from shangpin.utils import update_shangpin_daily_stat -from shangdian.utils import update_dianpu_daily_stat # 注意:您之前写的店铺统计方法名是 update_dianpu_daily_stat +from orders.utils import ( + update_platform_profit, calc_shangjia_order_fencheng, sync_order_to_partners, + update_daily_dispatch_stat, update_daily_income, update_daily_payout, + settle_shangjia_order_guanshi_fenhong +) +from shop.utils import calculate_pingtai_and_dianpu_shouyi, validate_shangpin_and_dianpu, update_dianpu_daily_stat +from products.utils import update_shangpin_daily_stat +from backend.utils import update_shangjia_daily, update_dashou_daily_by_action +from rank.services import record_dashou_biaoxian +from rank.utils import check_dashou_biaoqian_required -from houtai.utils import update_dashou_daily_by_action - -# dingdan/views.py (CreateOrderView) - - -# dingdan/views.py (WechatPayNotifyView) - -import threading -import hashlib -import xml.etree.ElementTree as ET -from decimal import Decimal -from django.db import transaction, models -from django.http import HttpResponse -from rest_framework.views import APIView -from rest_framework.permissions import AllowAny -from rest_framework.throttling import AnonRateThrottle -import logging -from rest_framework import permissions - -from shangpin.models import Shangpin - -from yonghu.models import UserMain, UserBoss - - -from django.db import transaction -from django.http import HttpResponse -from rest_framework.views import APIView - -from rest_framework.throttling import AnonRateThrottle -from rest_framework.permissions import AllowAny - -from peizhi.models import Szjilu +from orders.notice_tasks import dingdan_guangbo +from .models import ( + Dingdan, DingdanShangjia, DingdanPingtai, Dashoutupian, + Chufajilu, Chufatupian, Fadan, FadanFenhong, Liaotian +) +from orders.models import ( + Lilubiao, Pingfen, PreSettlement, Tuikuanjilu, CrossPlatformOrderData +) +from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai +from users.models import UserMain, UserDashou, UserShangjia, UserBoss +from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChenghao +from config.models import ( + Szjilu, Club, ClubConfig, ShangjiaLianjie, OrderTypeMapping +) logger = logging.getLogger(__name__) -# dingdan/views.py -import threading -import hashlib -import logging -import traceback -import xml.etree.ElementTree as ET -from decimal import Decimal -from django.db import transaction, models -from django.http import HttpResponse -from django.conf import settings -from rest_framework.views import APIView -from rest_framework.permissions import AllowAny -from rest_framework.throttling import AnonRateThrottle - -# 假设这些模型已正确导入 -from dingdan.models import Dingdan, DingdanPingtai -from shangpin.models import Shangpin -from yonghu.models import UserMain, UserBoss -from peizhi.models import Szjilu - logger = logging.getLogger(__name__) -# dingdan/views.py (PaymentFailView) + class PaymentFailView(APIView): """ @@ -130,32 +116,6 @@ class PaymentFailView(APIView): return Response({'code': 99, 'msg': f'删除订单失败: {str(e)}', 'data': None}) -import string -import requests -import xmltodict -import logging -from decimal import Decimal -from django.db import transaction, models -from django.core.cache import cache -from django.conf import settings -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import permissions, status -from rest_framework.parsers import JSONParser - -from yonghu.models import UserMain, UserBoss -from shangpin.models import Shangpin -from dingdan.models import Dingdan, DingdanPingtai -from peizhi.models import Szjilu - -logger = logging.getLogger(__name__) - - -from dingdan.utils import update_daily_income -import logging -import traceback - -logger = logging.getLogger(__name__) class WechatPayNotifyView(APIView): """微信支付回调接口 - 接收微信官方支付结果通知(含详细调试日志)""" @@ -359,9 +319,6 @@ class WechatPayNotifyView(APIView): # 跨平台同步 sync_order_to_partners(dingdan.dingdan_id) - #🟢【7. 新增】支付成功后的广播通知(异步,不阻塞主流程) - from dingdan.tongzhi_tasks import dingdan_guangbo - # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用) order_info = { 'dingdan_id': dingdan.dingdan_id, @@ -430,7 +387,6 @@ class WechatPayNotifyView(APIView): try: def task(): try: - from utils.weixin_broadcast import WeixinBroadcastSender # 请根据实际路径修改 order_info = { 'dingdan_id': dingdan.dingdan_id, 'game_type': self._get_game_type_name(dingdan.leixing_id), @@ -457,7 +413,6 @@ class WechatPayNotifyView(APIView): def _get_game_type_name(self, leixing_id): try: - from shangpin.models import ShangpinLeixing gt = ShangpinLeixing.objects.filter(id=leixing_id).first() return gt.jieshao if gt else '游戏订单' except Exception as e: @@ -465,7 +420,7 @@ class WechatPayNotifyView(APIView): return '游戏订单' -from django.core.cache import cache + class PaymentVerifyView(APIView): """ 订单支付成功复查接口 - 前端支付成功后调用,确认订单状态 @@ -718,30 +673,6 @@ class PaymentVerifyView(APIView): return ''.join(xml) -import os -from django.db.models import F -from django.shortcuts import render -from rest_framework.permissions import AllowAny # 新增这行 -from rest_framework.throttling import AnonRateThrottle -# dingdan/views.py -import time -import random -import hashlib -import xml.etree.ElementTree as ET -import requests -import uuid -from decimal import Decimal -from django.db import transaction -from django.conf import settings -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import permissions, status -from tencentcloud.common.profile.client_profile import ClientProfile -from tencentcloud.common.profile.http_profile import HttpProfile - -from shangpin.models import Shangpin, ShangpinLeixing, Huiyuangoumai -from dingdan.models import Dingdan, DingdanPingtai, Lilubiao, Pingfen,Chufatupian, DingdanShangjia, Chufajilu, Liaotian -from yonghu.models import UserMain, UserDashou, UserShangjia class CreateOrderView(APIView): @@ -981,7 +912,6 @@ class CreateOrderView(APIView): raise Exception('微信支付配置不完整') # 生成随机字符串 - import string nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) # 金额(单位:分) @@ -1095,23 +1025,6 @@ class CreateOrderView(APIView): raise e -# dingdan/views.py - - -# dingdan/views.py - - -from yonghu.models import UserMain - -# dingdan/views.py - -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from django.db.models import Q - -from .models import Dingdan, DingdanPingtai - class DingdanHuoquView(APIView): """订单获取接口""" @@ -1225,15 +1138,7 @@ class DingdanHuoquView(APIView): }) -# dingdan/views.py -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from django.db.models import Q -import json - -from .models import Dingdan, DingdanPingtai, Dashoutupian class DingdanXiangqingView(APIView): @@ -1281,7 +1186,6 @@ class DingdanXiangqingView(APIView): if not pingtai_kuozhan: # 如果不是平台订单,检查商家订单 - from .models import DingdanShangjia shangjia_kuozhan = DingdanShangjia.objects.filter( shangjia_id=yonghuid, dingdan__dingdan_id=dingdan_id @@ -1593,7 +1497,6 @@ class JiedanView(APIView): # 12. 更新平台收益流水(仅在平台发单时) if dingdan.fadan_pingtai == 1: try: - from dingdan.utils import update_platform_profit # 导入函数 update_platform_profit(dingdan_id) except Exception as e: # 平台收益更新失败不影响主流程,但记录日志 @@ -1623,7 +1526,6 @@ class JiedanView(APIView): """ try: # 1. 获取我方俱乐部ID - from .models import CrossPlatformOrderData my_club_config = ClubConfig.objects.filter(is_self=1).first() if not my_club_config: logger.error("未配置我方俱乐部信息,无法通知对方") @@ -1670,8 +1572,6 @@ class JiedanView(APIView): logger.error(f"通知对方平台异常: {str(e)}") -from dengji.models import DashouBiaoxian -from dengji.services import record_dashou_biaoxian class DingdanXiangqingView2(APIView): @@ -1932,7 +1832,6 @@ class JiedanView2(APIView): # 13. 平台收益流水(仅平台发单) if order.fadan_pingtai == 1: try: - from dingdan.utils import update_platform_profit # 导入函数 update_platform_profit(dingdan_id) except Exception as e: logger.error(f"平台收益更新失败: {e}") @@ -1957,11 +1856,6 @@ class JiedanView2(APIView): return Response({'code': 9, 'msg': '服务器内部错误,结单失败', 'data': None}) -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from django.db.models import Prefetch -from dengji.models import Chenghao class ShangpinLeixingView(APIView): @@ -2022,254 +1916,8 @@ class ShangpinLeixingView(APIView): except Exception as e: logger.error(f"获取商品类型失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) - -import threading -from utils.weixin_broadcast import WeixinBroadcastSender -from dengji.models import DingdanBiaoqian,Chenghao - - -import os -import threading -import logging -from decimal import Decimal -from django.db import transaction -from django.db.models import F -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework import status - -from yonghu.models import UserMain, UserShangjia, UserDashou - -from houtai.utils import update_shangjia_daily - - -# 导入 Celery 广播任务 -from dingdan.tongzhi_tasks import dingdan_guangbo - -logger = logging.getLogger(__name__) - - -"""class ShangjiaPaifaView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request): - logger.info(f"商家派单请求: user_id={request.user.yonghuid}, data={request.data}") - - try: - data = request.data - - # ----- 1. 获取原始参数(保持原类型)----- - shangpin_type_id = data.get('shangpinTypeId') - huiyuan_id = data.get('huiyuanId') - yaoqiuleixing = data.get('yaoqiuleixing') - dingdan_jieshao_raw = data.get('dingdanJieshao', '') - dingdan_beizhu_raw = data.get('dingdanBeizhu', '') - laoban_name_raw = data.get('laobanName', '') - zhiding_uid_raw = data.get('zhidingUid', '') - jiage_str = data.get('jiage') - - label_id = data.get('labelId') # 整数或 None - commission_enabled = data.get('commissionEnabled', False) - commission_value_raw = data.get('commissionValue') - - # ----- 2. 字符串字段安全处理 ----- - dingdan_jieshao = str(dingdan_jieshao_raw).strip() - dingdan_beizhu = str(dingdan_beizhu_raw).strip() - laoban_name = str(laoban_name_raw).strip() - zhiding_uid = str(zhiding_uid_raw).strip() - commission_value_str = str(commission_value_raw).strip() if commission_value_raw is not None else '' - - # ----- 3. 必填校验 ----- - if not all([shangpin_type_id, huiyuan_id, dingdan_jieshao, laoban_name, jiage_str]): - logger.warning(f"参数缺失: {request.data}") - return Response({'code': 400, 'msg': '缺少必填参数'}) - - # ----- 4. 价格验证 ----- - try: - jiage = float(jiage_str) - if jiage < 1 or jiage > 10000: - return Response({'code': 400, 'msg': '价格需在1-10000元之间'}) - if '.' in str(jiage_str) and len(str(jiage_str).split('.')[1]) > 2: - return Response({'code': 400, 'msg': '最多支持两位小数'}) - except ValueError: - return Response({'code': 400, 'msg': '价格格式错误'}) - - # ----- 5. 商品类型 ----- - try: - shangpin_type = ShangpinLeixing.objects.get(id=shangpin_type_id) - except ShangpinLeixing.DoesNotExist: - logger.error(f"商品类型不存在: {shangpin_type_id}") - return Response({'code': 400, 'msg': '商品类型不存在'}) - - # ----- 6. 押金 / 会员模式决定(修正版)----- - # 先根据商品类型中的配置填充默认值 - final_yaoqiu_type = shangpin_type.yaoqiuleixing if shangpin_type.yaoqiuleixing else 1 - final_yongjin = shangpin_type.yongjin if shangpin_type.yongjin else None - final_huiyuan_id = shangpin_type.huiyuan_id if shangpin_type.huiyuan_id else '' - - # 如果前端明确开启了佣金且金额大于0,则覆盖为押金模式 - if commission_enabled and commission_value_str: - try: - cv = float(commission_value_str) - if cv > 0: - final_yaoqiu_type = 2 - final_yongjin = Decimal(str(cv)) - final_huiyuan_id = '' - logger.info(f"前端开启押金模式: 佣金={cv}") - else: - # 佣金金额为0,视为无效,保持商品类型默认 - logger.warning("前端传佣金为0,无效,使用商品类型默认") - except ValueError: - logger.warning("佣金格式错误,使用商品类型默认") - else: - logger.info(f"使用商品类型默认模式: 类型={final_yaoqiu_type}, huiyuan_id={final_huiyuan_id}, yongjin={final_yongjin}") - - # ----- 7. 称号验证(label_id 是整数或 None)----- - chenghao_obj = None - if label_id is not None: - try: - chenghao_obj = Chenghao.objects.get(id=int(label_id)) - logger.info(f"订单将关联称号: {chenghao_obj.mingcheng}") - except (Chenghao.DoesNotExist, ValueError, TypeError): - logger.warning(f"称号不存在或ID无效: {label_id}") - return Response({'code': 400, 'msg': '所选标签不存在'}) - - # ----- 8. 商家信息 ----- - try: - shangjia = request.user.shop_profile - except UserShangjia.DoesNotExist: - logger.warning(f"用户不是商家: {request.user.yonghuid}") - return Response({'code': 403, 'msg': '您不是商家'}) - - if shangjia.zhuangtai != 1: - logger.warning(f"商家状态异常: {shangjia.zhuangtai}") - return Response({'code': 403, 'msg': '商家状态异常'}) - - if jiage > shangjia.yue: - logger.warning(f"余额不足: {shangjia.yue} < {jiage}") - return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元'}) - - # ----- 9. 指定打手 ----- - zhiding_dashou = None - if zhiding_uid: - try: - zhiding_user = UserMain.objects.get(yonghuid=zhiding_uid) - zhiding_dashou = zhiding_user.dashou_profile - if zhiding_dashou.zhanghaozhuangtai != 1: - return Response({'code': 400, 'msg': '指定打手状态异常'}) - except UserMain.DoesNotExist: - return Response({'code': 400, 'msg': '指定用户不存在'}) - except UserDashou.DoesNotExist: - return Response({'code': 400, 'msg': '指定用户不是打手'}) - - # ----- 10. 利率 ----- - try: - lilu_obj = Lilubiao.objects.get(fadanpingtai='3') - lilu = float(lilu_obj.lilu) if lilu_obj.lilu > 0 else 1 - except Lilubiao.DoesNotExist: - lilu = 1 - dashou_fencheng = round(jiage * lilu, 2) - - # ----- 11. 生成订单ID ----- - def generate_dingdan_id(): - import time, random - ts = int(time.time() * 1000) - pid = os.getpid() % 1000 - rnd = random.randint(1000, 9999) - return f"SJ{ts}{pid:03d}{rnd}" - - # ----- 12. 事务:创建订单、更新商家统计 ----- - dingdan = None - try: - with transaction.atomic(): - dingdan = Dingdan.objects.create( - dingdan_id=generate_dingdan_id(), - zhuangtai=7 if zhiding_dashou else 1, - fadan_pingtai=2, - jine=jiage, - dashou_fencheng=dashou_fencheng, - zhiding_id=zhiding_uid if zhiding_dashou else '', - shangpin_id=0, - leixing_id=int(shangpin_type_id), - yaoqiuleixing=final_yaoqiu_type, - yongjin=final_yongjin, - huiyuan_id=final_huiyuan_id, - jieshao=dingdan_jieshao[:2000], - beizhu=dingdan_beizhu[:1000], - nicheng=laoban_name[:50], - tupian=request.user.avatar, - user1_id=f"Sj{request.user.yonghuid}" - ) - logger.info(f"订单主表创建: {dingdan.dingdan_id}") - - DingdanShangjia.objects.create( - dingdan=dingdan, - shangjia_id=request.user.yonghuid, - sjnicheng=shangjia.nicheng, - ) - - if chenghao_obj: - DingdanBiaoqian.objects.create(dingdan=dingdan, chenghao=chenghao_obj) - logger.info(f"订单关联称号: {chenghao_obj.mingcheng}") - - # 更新商家统计 - UserShangjia.objects.filter(id=shangjia.id).update( - fabu=F('fabu') + 1, - yue=F('yue') - jiage, - jinridingdan=F('jinridingdan') + 1, - jinriliushui=F('jinriliushui') + jiage, - jinyuedingdan=F('jinyuedingdan') + 1, - jinyueliushui=F('jinyueliushui') + jiage, - ) - - update_shangjia_daily( - yonghuid=request.user.yonghuid, - amount=Decimal(str(jiage)), - action=1 - ) - # 事务提交,订单已持久化 - except Exception as e: - logger.error(f"订单创建事务失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '订单创建失败,请稍后重试'}) - - # ========== 以下操作均在事务外,避免异步任务查不到订单 ========== - try: - sync_order_to_partners(dingdan.dingdan_id) - except Exception as e: - logger.error(f"跨平台同步失败: {e}", exc_info=True) - - try: - order_info = { - 'dingdan_id': dingdan.dingdan_id, - 'game_type': self._get_game_type_name(dingdan.leixing_id), - 'amount': str(dingdan.jine), - 'order_desc': (dingdan.jieshao or '')[:50], - } - dingdan_guangbo.delay(order_info) - logger.info(f"已提交广播任务: {dingdan.dingdan_id}") - except Exception as e: - logger.error(f"广播任务提交失败: {e}", exc_info=True) - - logger.info(f"商家派单成功: {dingdan.dingdan_id}, 金额={jiage}") - return Response({'code': 200, 'msg': '派单成功', 'data': {'jine': str(dingdan.jine)}}) - - except Exception as e: - logger.error(f"商家派单接口未捕获异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '系统内部错误'}) - - def _get_game_type_name(self, leixing_id): - try: - gt = ShangpinLeixing.objects.filter(id=leixing_id).first() - return gt.jieshao if gt else '游戏订单' - except Exception: - return '游戏订单'""" - - -logger = logging.getLogger(__name__) - class ShangjiaPaifaView(APIView): permission_classes = [IsAuthenticated] @@ -2390,12 +2038,10 @@ class ShangjiaPaifaView(APIView): return Response({'code': 400, 'msg': '指定用户不是打手'}) # ----- 10. 利率(打手+管事分成,打手优先) ----- - from dingdan.utils import calc_shangjia_order_fencheng dashou_fencheng, guanshi_fencheng = calc_shangjia_order_fencheng(jiage) # ----- 11. 生成订单ID ----- def generate_dingdan_id(): - import time, random ts = int(time.time() * 1000) pid = os.getpid() % 1000 rnd = random.randint(1000, 9999) @@ -2490,20 +2136,7 @@ class ShangjiaPaifaView(APIView): except Exception: return '游戏订单' -# apps/dingdan/views.py -from django.db.models import Q, Count -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from .models import Dingdan, DingdanShangjia -import logging - -logger = logging.getLogger(__name__) - - # views.py 中 ShangjiaDingdanHuoquView 完整改造 - class ShangjiaDingdanHuoquView(APIView): """ 商家订单获取接口 @@ -2563,7 +2196,7 @@ class ShangjiaDingdanHuoquView(APIView): # 5. 状态过滤 base_qs = base_qs.filter(dingdan__zhuangtai__in=zhuangtai_list) - # 🆕 6. 商品类型筛选 + # 6. 商品类型筛选 if leixing_id is not None: try: leixing_id_int = int(leixing_id) @@ -2572,9 +2205,8 @@ class ShangjiaDingdanHuoquView(APIView): # 如果leixing_id不是有效整数,忽略该筛选 pass - # 🆕 7. 模糊搜索(支持订单ID、订单介绍、游戏昵称、打手ID) + # 7. 模糊搜索(支持订单ID、订单介绍、游戏昵称、打手ID) if keyword: - from django.db.models import Q # 订单ID精确匹配(dingdan_id有唯一索引,先用精确匹配) dingdan_id_match = Q(dingdan__dingdan_id__exact=keyword) # 订单介绍模糊匹配 @@ -2642,11 +2274,6 @@ class ShangjiaDingdanHuoquView(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -from dingdan.models import Dingdan, DingdanShangjia, Dashoutupian, Fadan - -from .models import FadanFenhong # 注意:fadan_fenhong 可能在其他 app 中,根据实际路径调整 - -logger = logging.getLogger(__name__) class ShangjiaDingdanXiangqingView(APIView): @@ -2821,14 +2448,7 @@ class ShangjiaDingdanXiangqingView(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -from dingdan.models import Dingdan, DingdanShangjia, PreSettlement -from dingdan.utils import update_daily_dispatch_stat - - -logger = logging.getLogger(__name__) - -from houtai.utils import update_shangjia_daily class ShangjiaJiesuanView(APIView): """商家结单接口(支持跨平台订单)""" permission_classes = [permissions.IsAuthenticated] @@ -3016,7 +2636,7 @@ class ShangjiaJiesuanView(APIView): shangjia_pingjia=liuyan, ) - # 🔥 新增:调用打手每日统计(成交) + # 新增:调用打手每日统计(成交) try: update_dashou_daily_by_action( yonghuid=jiedan_dashou_id, @@ -3037,7 +2657,6 @@ class ShangjiaJiesuanView(APIView): # 商家订单管事分红(仅 fadan_pingtai=2) if order.fadan_pingtai == 2: - from dingdan.utils import settle_shangjia_order_guanshi_fenhong settle_shangjia_order_guanshi_fenhong(order, jiedan_dashou_id) # 返回成功 @@ -3070,7 +2689,6 @@ class ShangjiaJiesuanView(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu class ShangjiaChexiaoView(APIView): @@ -3181,8 +2799,7 @@ class ShangjiaChexiaoView(APIView): dingdan.save() - # ✅ 新方法(行为驱动,更安全) - from houtai.utils import update_shangjia_daily + # 行为驱动 update_shangjia_daily( yonghuid=request.user.yonghuid, amount=Decimal(str(dingdan.jine)), @@ -3202,8 +2819,6 @@ class ShangjiaChexiaoView(APIView): except Exception as e: # 记录异常日志 - import logging - logger = logging.getLogger(__name__) logger.error(f"商家撤销订单失败: {str(e)}", exc_info=True) return Response({ @@ -3332,9 +2947,6 @@ class ShangjiaTuikuanShenqingView(APIView): # ========== 新增:如果是跨平台订单,异步通知对方平台 ========== if dingdan.is_cross == 1: - import threading - import requests - from django.conf import settings def send_refund_notify(): url = f"{settings.PARTNER_BASE_URL}{settings.PARTNER_REFUND_NOTIFY_URL}" @@ -3535,8 +3147,6 @@ class ShangjiaChufaShenqingView(APIView): }, status=status.HTTP_200_OK) except Exception as e: - import logging - logger = logging.getLogger(__name__) logger.error(f"商家处罚申请失败: {str(e)}", exc_info=True) return Response({ @@ -3546,22 +3156,8 @@ class ShangjiaChufaShenqingView(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -# apps/dingdan/views.py -from django.db.models import Q, OuterRef, Subquery -from django.core.paginator import Paginator -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework_simplejwt.authentication import JWTAuthentication -from .models import Dingdan, DingdanShangjia -# dingdan/views.py -from django.db.models import Q, OuterRef, Subquery, F, Prefetch - -from dengji.models import DingdanBiaoqian -from dengji.models import YonghuChenghao, Chenghao - class DashouDingdanHuoquView(APIView): """ @@ -3791,8 +3387,7 @@ class DashouDingdanHuoquView(APIView): return {} -# apps/dingdan/views.py -from shangpin.models import ShangpinLeixing # 假设这是你的商品类型模型 + class ShangpinLeixingHuoquView(APIView): @@ -3834,114 +3429,6 @@ class ShangpinLeixingHuoquView(APIView): }) -# apps/dingdan/views.py -from django.db import transaction -from django.utils import timezone -from django.db.models import F -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework_simplejwt.authentication import JWTAuthentication -import logging - -from .models import Dingdan, Liaotian - -from utils.goeasy_service import GoEasyService - -logger = logging.getLogger(__name__) - - -from peizhi.models import Club - -logger = logging.getLogger(__name__) - - -# apps/dingdan/views.py -from django.db import transaction -from django.utils import timezone -from django.db.models import F -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework_simplejwt.authentication import JWTAuthentication -import logging -import requests -import json - -from .models import Dingdan, DingdanPingtai, DingdanShangjia - - -logger = logging.getLogger(__name__) - -from django.db import transaction -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from django.utils import timezone -from django.db.models import F -from django.conf import settings -import json -import requests -from datetime import datetime - - -from peizhi.models import Club - -logger = logging.getLogger(__name__) - - -# dingdan/views.py -import logging -import requests -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status, permissions -from .models import Dingdan, DingdanPingtai, DingdanShangjia -from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia -from peizhi.models import ClubConfig -from utils.chat_utils import _send_group_message, _subscribe_users_to_group - -logger = logging.getLogger('dingdan') - -# dingdan/views.py -# dingdan/views.py -import logging -import requests -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status, permissions -from .models import Dingdan, DingdanPingtai, DingdanShangjia -from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia -from peizhi.models import ClubConfig - -logger = logging.getLogger('dingdan') - - -# dingdan/views.py - -import logging -import requests -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import permissions -from .models import Dingdan, DingdanPingtai, DingdanShangjia -from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia -from peizhi.models import ClubConfig - -logger = logging.getLogger('dingdan') - -import logging -import requests -import time -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import permissions - -from .models import Dingdan, DingdanPingtai, DingdanShangjia -from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia -from peizhi.models import ClubConfig - -logger = logging.getLogger('dingdan') # ----------------- 基础配置获取 ----------------- @@ -4252,11 +3739,6 @@ class DaiLiQunLiaoXiaoXiView(APIView): #通过跨平台字段判断是否是跨平台 - - -from peizhi.models import ClubConfig, ShangjiaLianjie -logger = logging.getLogger('dingdan') - # ---------- 基础配置 ---------- def get_self_goeasy_appkey(): try: @@ -4377,8 +3859,6 @@ class ShangjiaLianjieLiuYanZhuanFaView(APIView): return Response({'code': 200, 'msg': '我方发送成功,对方发送失败'}) -from peizhi.models import ClubConfig -from utils.chat_utils import establish_order_chat # 【新增】导入聊天工具 class QiangdanView(APIView): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] @@ -4398,13 +3878,11 @@ class QiangdanView(APIView): # ==================== 【新增】罚单检查 ==================== - from utils.fadan_utils import check_fadan_qiangdan_eligible fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(request.user.yonghuid,1) if not fadan_ok: return Response({'code': 400, 'msg': fadan_msg}) # ========================================================= # ========== 标签快速校验(提前拒绝,避免锁资源) ========== - from dengji.utils import check_dashou_biaoqian_required biaoqian_ok, biaoqian_msg = check_dashou_biaoqian_required(dingdan_id, request.user.yonghuid) if not biaoqian_ok: return Response({'code': 400, 'msg': biaoqian_msg}) @@ -4479,7 +3957,6 @@ class QiangdanView(APIView): # 在 QiangdanView.post 里,_execute_qiangdan 之后添加: - from dengji.services import record_dashou_biaoxian # 记录抢单表现(不影响主流程) # 如果订单表里有 bankuai 信息,可以用 order.bankuai 代替 None record_dashou_biaoxian( @@ -4764,8 +4241,6 @@ class DashouDingdanHuoquView1(APIView): }, status=status.HTTP_200_OK) except Exception as e: - import logging - logger = logging.getLogger(__name__) logger.error(f"打手订单获取失败: {str(e)}", exc_info=True) return Response({ @@ -4775,21 +4250,6 @@ class DashouDingdanHuoquView1(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -# -# views.py - 打手COS凭证接口(完整替换) -import json -import time -from tencentcloud.common import credential -from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException -from tencentcloud.sts.v20180813 import sts_client, models -from django.conf import settings -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework import status -import logging - -logger = logging.getLogger(__name__) class DashouCOSZhengshuView(APIView): """打手获取COS上传凭证接口(调试版本)""" @@ -4811,7 +4271,6 @@ class DashouCOSZhengshuView(APIView): # 3. 业务验证逻辑(根据用途分别验证) if yongtu == 'chufa': try: - from .models import Chufajilu chufa_record = Chufajilu.objects.filter( dingdan_id=dingdan_id, dashouid=request.user.yonghuid, @@ -4835,7 +4294,6 @@ class DashouCOSZhengshuView(APIView): else: # 默认为订单相关,需要验证订单且打手必须接单 try: - from .models import Dingdan dingdan = Dingdan.objects.get( dingdan_id=dingdan_id, jiedan_dashou_id=request.user.yonghuid, @@ -4971,8 +4429,6 @@ class ShangjiaCOSZhengshuView(APIView): # 3. 🔴【修改】验证订单权限 - 通过商家订单扩展表验证 try: - from .models import Dingdan, DingdanShangjia - # 获取订单 dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) @@ -4994,7 +4450,6 @@ class ShangjiaCOSZhengshuView(APIView): # 🔴【新增】检查是否已存在处罚申请(通过Chufajilu模型) # 注意:这里假设Chufajilu模型存在,并且有shangjia_id字段 try: - from .models import Chufajilu existing_chufa = Chufajilu.objects.filter( dingdan_id=dingdan_id, qingqiuid=shangjia_id, @@ -5213,7 +4668,6 @@ class DashouTijiaoView(APIView): dashou_profile.zhuangtai = 1 # 改为数字1(根据你之前说的) dashou_profile.save() - from dengji.services import record_dashou_biaoxian # ========== 新增:记录打手提交时间 ========== record_dashou_biaoxian( dingdan_id=dingdan_id, @@ -5235,8 +4689,6 @@ class DashouTijiaoView(APIView): }, status=status.HTTP_200_OK) except Exception as e: - import logging - logger = logging.getLogger(__name__) logger.error(f"打手提交订单失败: {str(e)}", exc_info=True) return Response({ @@ -5376,8 +4828,6 @@ class DashouDingdanXiangqingView(APIView): }, status=status.HTTP_200_OK) except Exception as e: - import logging - logger = logging.getLogger(__name__) logger.error(f"获取打手订单详情失败: {str(e)}", exc_info=True) return Response({ @@ -5387,16 +4837,6 @@ class DashouDingdanXiangqingView(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -# dingdan/views.py -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser -import logging - - -logger = logging.getLogger(__name__) class AdGetDingdanXiangQing(APIView): @@ -5454,7 +4894,6 @@ class AdGetDingdanXiangQing(APIView): # 6. 查询订单主表 try: - from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia, Dashoutupian # 查询订单主表 - 使用select_related预加载关联表 dingdan_obj = Dingdan.objects.select_related( @@ -5554,8 +4993,6 @@ class AdGetDingdanXiangQing(APIView): # 🔥 新增:查询退款记录表中的商家退款理由 try: # 导入退款记录模型 - from dingdan.models import Tuikuanjilu - # 查询该订单的退款记录(一个订单可能有多次退款申请,我们取最新的) tuikuan_record = Tuikuanjilu.objects.filter( dingdan_id=dingdan_id @@ -5664,7 +5101,6 @@ class AdGengHuanDaShou(APIView): # 6. 查询订单主表 try: - from dingdan.models import Dingdan dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response( @@ -5721,7 +5157,6 @@ class AdGengHuanDaShou(APIView): # 查询会员购买记录 try: - from shangpin.models import Huiyuangoumai huiyuan_goumai = Huiyuangoumai.objects.get( huiyuan_id=huiyuan_id, yonghu_id=new_dashou_id # 注意:yonghu_id存储的是用户ID(yonghuid) @@ -5821,12 +5256,6 @@ class AdGengHuanDaShou(APIView): ) -import logging - -# 获取日志器 -logger = logging.getLogger(__name__) - - class AdQiangZhiJieDan(APIView): """ 后台强制结单接口 @@ -5891,7 +5320,6 @@ class AdQiangZhiJieDan(APIView): # 6. 查询订单主表 try: - from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response( @@ -5998,7 +5426,6 @@ class AdQiangZhiJieDan(APIView): # 12. 更新平台收益流水(仅在平台发单时) if dingdan_obj.fadan_pingtai == 1: try: - from dingdan.utils import update_platform_profit # 导入函数 update_platform_profit(dingdan_id) except Exception as e: # 平台收益更新失败不影响主流程,但记录日志 @@ -6084,7 +5511,6 @@ class AdJuJueTuiKuan(APIView): # 6. 查询订单主表 try: - from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response( @@ -6293,7 +5719,6 @@ class AdTongYiTuiKuanShangJia(APIView): # 7. 查询订单主表 try: - from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response( @@ -6431,29 +5856,6 @@ class AdTongYiTuiKuanShangJia(APIView): ) -# dingdan/views.py -import logging -import time -import random -import hashlib -import requests -import xml.etree.ElementTree as ET -from decimal import Decimal -from django.db import transaction -from django.conf import settings -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser - -from yonghu.models import UserMain, UserDashou, UserBoss -from dingdan.models import Dingdan, DingdanPingtai, Tuikuanjilu -from peizhi.models import Szjilu - -logger = logging.getLogger(__name__) - - class AdTongYiTuiKuanPingTai(APIView): """ 后台同意退款接口(平台发单) @@ -6603,8 +6005,6 @@ class AdTongYiTuiKuanPingTai(APIView): # 16. 更新收支记录表(id=1) try: - #更新支出记录 - from dingdan.utils import update_daily_payout update_daily_payout(jine) @@ -6792,9 +6192,6 @@ class DashouHuoquLeixingView(APIView): try: logger.info(f"打手获取商品类型请求 - 用户ID: {request.user.yonghuid}") - # 🔴 导入商品类型模型 - from shangpin.models import ShangpinLeixing - # 🔴 查询所有正常状态的商品类型 # 条件:shenhezhuangtai=1(正常状态) # 排序:先按 paixu 降序(数值大的在前),再按 id 升序 @@ -6840,11 +6237,7 @@ class DashouHuoquLeixingView(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -logger = logging.getLogger(__name__) -from peizhi.models import Club ,OrderTypeMapping - -logger = logging.getLogger(__name__) class PartnerSyncOrderView(APIView): @@ -6987,10 +6380,6 @@ class PartnerSyncOrderView(APIView): logger.info(f"跨平台订单同步成功: partner_order_id={partner_order_id}, local_id={new_id}") - - #🟢【7. 新增】支付成功后的广播通知(异步,不阻塞主流程) - from dingdan.tongzhi_tasks import dingdan_guangbo - # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用) order_info = { @@ -7016,7 +6405,6 @@ class PartnerSyncOrderView(APIView): def _get_game_type_name(self, leixing_id): try: - from shangpin.models import ShangpinLeixing gt = ShangpinLeixing.objects.filter(id=leixing_id).first() return gt.jieshao if gt else '游戏订单' except Exception as e: @@ -7024,9 +6412,7 @@ class PartnerSyncOrderView(APIView): return '游戏订单' -from dingdan.utils import update_daily_dispatch_stat -logger = logging.getLogger(__name__) class PartnerCheckClaimView(APIView): @@ -7598,8 +6984,7 @@ class DashouXiugaiView(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -from django.core.exceptions import ObjectDoesNotExist -from dingdan.tongzhi_tasks import dingdan_guangbo + class ZxsjghdsView(APIView): """ @@ -7708,7 +7093,6 @@ class ZxsjghdsView(APIView): # 6.4 打手每日统计(退款行为) if dashou_id: try: - from houtai.utils import update_dashou_daily_by_action update_dashou_daily_by_action( yonghuid=dashou_id, amount=dashou_fencheng, @@ -7721,7 +7105,6 @@ class ZxsjghdsView(APIView): # 6.5 跨平台退款通知 if need_notify_partner: try: - from peizhi.models import ClubConfig, Club my_club_config = ClubConfig.objects.filter(is_self=1).first() if not my_club_config: raise Exception("我方俱乐部配置不存在") @@ -7748,7 +7131,6 @@ class ZxsjghdsView(APIView): return Response({'code': 500, 'msg': f"对方平台通知失败: {resp_json.get('msg')}"}) # 更新跨平台扩展数据 - from dingdan.models import CrossPlatformOrderData CrossPlatformOrderData.objects.update_or_create( dingdan_id=dingdan_id, defaults={ diff --git a/peizhi/__init__.py b/products/__init__.py similarity index 100% rename from peizhi/__init__.py rename to products/__init__.py diff --git a/peizhi/admin.py b/products/admin.py similarity index 100% rename from peizhi/admin.py rename to products/admin.py diff --git a/shangpin/apps.py b/products/apps.py similarity index 100% rename from shangpin/apps.py rename to products/apps.py diff --git a/shangpin/migrations/0001_initial.py b/products/migrations/0001_initial.py similarity index 100% rename from shangpin/migrations/0001_initial.py rename to products/migrations/0001_initial.py diff --git a/shangpin/migrations/0002_alter_huiyuangoumai_unique_together_and_more.py b/products/migrations/0002_alter_huiyuangoumai_unique_together_and_more.py similarity index 100% rename from shangpin/migrations/0002_alter_huiyuangoumai_unique_together_and_more.py rename to products/migrations/0002_alter_huiyuangoumai_unique_together_and_more.py diff --git a/shangpin/migrations/0003_shangpin_paixu_shangpinleixing_paixu_and_more.py b/products/migrations/0003_shangpin_paixu_shangpinleixing_paixu_and_more.py similarity index 100% rename from shangpin/migrations/0003_shangpin_paixu_shangpinleixing_paixu_and_more.py rename to products/migrations/0003_shangpin_paixu_shangpinleixing_paixu_and_more.py diff --git a/shangpin/migrations/0004_shangpin_shenhezhuangtai_and_more.py b/products/migrations/0004_shangpin_shenhezhuangtai_and_more.py similarity index 100% rename from shangpin/migrations/0004_shangpin_shenhezhuangtai_and_more.py rename to products/migrations/0004_shangpin_shenhezhuangtai_and_more.py diff --git a/shangpin/migrations/0005_alter_gsfenhong_dashouid.py b/products/migrations/0005_alter_gsfenhong_dashouid.py similarity index 100% rename from shangpin/migrations/0005_alter_gsfenhong_dashouid.py rename to products/migrations/0005_alter_gsfenhong_dashouid.py diff --git a/shangpin/migrations/0006_gsfenhong_huiyuan_id_huiyuan_zuzhangfc_duocifenhong.py b/products/migrations/0006_gsfenhong_huiyuan_id_huiyuan_zuzhangfc_duocifenhong.py similarity index 100% rename from shangpin/migrations/0006_gsfenhong_huiyuan_id_huiyuan_zuzhangfc_duocifenhong.py rename to products/migrations/0006_gsfenhong_huiyuan_id_huiyuan_zuzhangfc_duocifenhong.py diff --git a/shangpin/migrations/0007_remove_duocifenhong_duoci_fenho_guanshi_b3de53_idx_and_more.py b/products/migrations/0007_remove_duocifenhong_duoci_fenho_guanshi_b3de53_idx_and_more.py similarity index 100% rename from shangpin/migrations/0007_remove_duocifenhong_duoci_fenho_guanshi_b3de53_idx_and_more.py rename to products/migrations/0007_remove_duocifenhong_duoci_fenho_guanshi_b3de53_idx_and_more.py diff --git a/shangpin/migrations/0008_gsfenhong_zuzhang_fenhong_gsfenhong_zuzhang_id.py b/products/migrations/0008_gsfenhong_zuzhang_fenhong_gsfenhong_zuzhang_id.py similarity index 100% rename from shangpin/migrations/0008_gsfenhong_zuzhang_fenhong_gsfenhong_zuzhang_id.py rename to products/migrations/0008_gsfenhong_zuzhang_fenhong_gsfenhong_zuzhang_id.py diff --git a/shangpin/migrations/0009_shangpin_ewai_dashou_fencheng_and_more.py b/products/migrations/0009_shangpin_ewai_dashou_fencheng_and_more.py similarity index 100% rename from shangpin/migrations/0009_shangpin_ewai_dashou_fencheng_and_more.py rename to products/migrations/0009_shangpin_ewai_dashou_fencheng_and_more.py diff --git a/shangpin/migrations/0010_shangpinleixing_is_cross_enabled.py b/products/migrations/0010_shangpinleixing_is_cross_enabled.py similarity index 100% rename from shangpin/migrations/0010_shangpinleixing_is_cross_enabled.py rename to products/migrations/0010_shangpinleixing_is_cross_enabled.py diff --git a/shangpin/migrations/0011_shangpin_dianpu_shangpin_dianpu_leixing_and_more.py b/products/migrations/0011_shangpin_dianpu_shangpin_dianpu_leixing_and_more.py similarity index 100% rename from shangpin/migrations/0011_shangpin_dianpu_shangpin_dianpu_leixing_and_more.py rename to products/migrations/0011_shangpin_dianpu_shangpin_dianpu_leixing_and_more.py diff --git a/shangpin/migrations/0012_shangpinmeiritongji.py b/products/migrations/0012_shangpinmeiritongji.py similarity index 100% rename from shangpin/migrations/0012_shangpinmeiritongji.py rename to products/migrations/0012_shangpinmeiritongji.py diff --git a/shangpin/migrations/0013_shangpinleixing_bankuai.py b/products/migrations/0013_shangpinleixing_bankuai.py similarity index 100% rename from shangpin/migrations/0013_shangpinleixing_bankuai.py rename to products/migrations/0013_shangpinleixing_bankuai.py diff --git a/shangpin/migrations/0014_huiyuan_bankuai.py b/products/migrations/0014_huiyuan_bankuai.py similarity index 100% rename from shangpin/migrations/0014_huiyuan_bankuai.py rename to products/migrations/0014_huiyuan_bankuai.py diff --git a/shangpin/migrations/0015_gsfenhong_fenhong_leixing.py b/products/migrations/0015_gsfenhong_fenhong_leixing.py similarity index 100% rename from shangpin/migrations/0015_gsfenhong_fenhong_leixing.py rename to products/migrations/0015_gsfenhong_fenhong_leixing.py diff --git a/peizhi/migrations/__init__.py b/products/migrations/__init__.py similarity index 100% rename from peizhi/migrations/__init__.py rename to products/migrations/__init__.py diff --git a/shangpin/models.py b/products/models.py similarity index 100% rename from shangpin/models.py rename to products/models.py diff --git a/peizhi/tests.py b/products/tests.py similarity index 100% rename from peizhi/tests.py rename to products/tests.py diff --git a/shangpin/urls.py b/products/urls.py similarity index 99% rename from shangpin/urls.py rename to products/urls.py index 95f9c61..bfd630c 100644 --- a/shangpin/urls.py +++ b/products/urls.py @@ -65,7 +65,6 @@ urlpatterns = [ path('adhyjlcxhq',HuiyuanRecordListView.as_view(), name='管理员查询会员记录'), - path('zuzhangfenhong', ZuzhangFenhongView.as_view(), name='组长分红记录获取'), path('czhqdy', CzhqdyView.as_view(), name='充值打手抵押查询'), diff --git a/shangpin/utils.py b/products/utils.py similarity index 99% rename from shangpin/utils.py rename to products/utils.py index 4bc52ab..78df05e 100644 --- a/shangpin/utils.py +++ b/products/utils.py @@ -6,7 +6,7 @@ from decimal import Decimal from django.db import transaction from django.db.models import F from .models import ShangpinMeiriTongji -from shangpin.models import Shangpin +from products.models import Shangpin logger = logging.getLogger(__name__) diff --git a/shangpin/views.py b/products/views.py similarity index 75% rename from shangpin/views.py rename to products/views.py index 60d3512..4ced5df 100644 --- a/shangpin/views.py +++ b/products/views.py @@ -1,18 +1,65 @@ -# shangpin/views.py -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.throttling import AnonRateThrottle -from django.db import models -from shangpin.models import ShangpinLeixing, ShangpinZhuanqu, Shangpin +# ==================== 标准库 ==================== +import os +import json +import time +import random +import string +import uuid +import hashlib +import requests import logging -from rest_framework.permissions import AllowAny # 新增这行 +import decimal +from datetime import timedelta +from decimal import Decimal, InvalidOperation, ROUND_CEILING +import defusedxml.ElementTree as ET -# 配置日志 +# ==================== Django 内置 ==================== +from django.conf import settings +from django.db import models, transaction, connection +from django.db.models import Q, Count, Subquery, OuterRef +from django.core.paginator import Paginator +from django.shortcuts import render, get_object_or_404 +from django.http import HttpResponse +from django.utils import timezone + +# ==================== DRF 框架 ==================== +from rest_framework.views import APIView, View +from rest_framework.response import Response +from rest_framework import permissions, status +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.parsers import MultiPartParser, FormParser, JSONParser +from rest_framework.throttling import AnonRateThrottle + +# ==================== 项目工具函数 ==================== +from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss +from orders.utils import update_daily_income +from backend.utils import ( + update_guanshi_daily_by_action, + update_guanshi_xufei_daily, + update_zuzhang_daily_by_action +) +from users.fadan_fenhong_utils import process_fadan_fenhong + +# ==================== 各App模型 ==================== +from .models import ( + ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan, + Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong +) +from users.models import ( + UserMain, AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia +) + +from orders.models import Lilubiao +from config.models import Szjilu, Gonggao, Lunbo + +from orders.models import Fadan # 用于罚款类型 + +import traceback + +# ==================== 日志实例 ==================== logger = logging.getLogger(__name__) - class ShangpinHuoquView(APIView): """ 商品数据获取接口 @@ -119,215 +166,6 @@ class ShangpinHuoquView(APIView): -'''class ShangpinHuoquView(APIView): - """ - 商品数据获取接口 - 一次性返回商品类型、商品专区、商品列表 - """ - - # 应用节流(限制频率) - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - - def post(self, request): - """ - 处理POST请求,返回所有商品相关数据 - """ - try: - logger.info("收到商品数据获取请求") - - # 🔴【修改】1. 查询商品类型(只查询正常状态的类型,并排序) - # 正常运营:只显示 shenhezhuangtai=1 的商品类型 - leixing_queryset = ShangpinLeixing.objects.filter( - shenhezhuangtai=2 # 🔴 只取正常状态 - ).order_by('-paixu', 'id') # 🔴 先按paixu降序,再按id升序 - - # 🔴【注意】审核期间使用下面这行代码(先注释掉) - """leixing_queryset = ShangpinLeixing.objects.filter( - shenhezhuangtai=2 # 🔴 审核期间:只取审核专用状态 - ).order_by('-paixu', 'id')""" - - shangpinleixing_data = [] - for leixing in leixing_queryset: - shangpinleixing_data.append({ - "id": leixing.id, - "jieshao": leixing.jieshao, - "tupian_url": leixing.tupian_url, - "paixu": leixing.paixu, # 🔴 新增字段 - "shenhezhuangtai": leixing.shenhezhuangtai # 🔴 新增字段 - }) - - # 🔴【修改】2. 查询商品专区(只查询可见类型下的专区,并排序) - # 先获取可见的类型ID列表 - visible_leixing_ids = [leixing['id'] for leixing in shangpinleixing_data] - - if visible_leixing_ids: - zhuanqu_queryset = ShangpinZhuanqu.objects.filter( - leixing_id__in=visible_leixing_ids - ).order_by('-paixu', 'id') # 🔴 先按paixu降序,再按id升序 - else: - zhuanqu_queryset = ShangpinZhuanqu.objects.none() - - shangpinzhuanqu_data = [] - for zhuanqu in zhuanqu_queryset: - shangpinzhuanqu_data.append({ - "id": zhuanqu.id, - "mingzi": zhuanqu.mingzi, - "leixing_id": zhuanqu.leixing_id, - "paixu": zhuanqu.paixu # 🔴 新增字段 - }) - - # 🔴【修改】3. 查询商品列表(只查询可见专区下的商品,并排序) - # 先获取可见的专区ID列表 - visible_zhuanqu_ids = [zhuanqu['id'] for zhuanqu in shangpinzhuanqu_data] - - if visible_zhuanqu_ids: - # 注意:这里使用values()只获取指定字段,提高查询效率 - shangpin_queryset = Shangpin.objects.filter( - zhuanqu_id__in=visible_zhuanqu_ids - ).values( - 'id', 'biaoqian', 'jiage', 'leixing_id', - 'zhuanqu_id', 'tupian_url', 'duiwai_xiaoliang', 'paixu' # 🔴 新增paixu字段 - ).order_by('-paixu', 'id') # 🔴 先按paixu降序,再按id升序 - - # 将QuerySet转换为列表 - shangpinliebiao_data = list(shangpin_queryset) - else: - shangpinliebiao_data = [] - - # 4. 构建返回数据 - response_data = { - "shangpinleixing": shangpinleixing_data, - "shangpinzhuanqu": shangpinzhuanqu_data, - "shangpinliebiao": shangpinliebiao_data - } - - logger.info(f"返回数据:类型{len(shangpinleixing_data)}个," - f"专区{len(shangpinzhuanqu_data)}个," - f"商品{len(shangpinliebiao_data)}个") - - # 5. 返回响应 - return Response(response_data, status=status.HTTP_200_OK) - - except Exception as e: - # 记录异常 - logger.error(f"获取商品数据时发生错误:{str(e)}", exc_info=True) - - # 返回错误响应 - error_data = { - "error": "服务器内部错误", - "detail": "获取商品数据失败,请稍后重试" - } - return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' - - -'''class ShangpinHuoquView(APIView): - """ - 商品数据获取接口 - 一次性返回商品类型、商品专区、商品列表 - """ - - # 应用节流(限制频率) - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - - def post(self, request): - """ - 处理POST请求,返回所有商品相关数据 - - 请求: POST /api/shangpin/shangpinhuoqu/ - 返回: { - "shangpinleixing": [ - { - "id": 1, - "jieshao": "和平精英", - "tupian_url": "leixing/peace.jpg" - } - ], - "shangpinzhuanqu": [ - { - "id": 1, - "mingzi": "地铁逃生", - "leixing_id": 1 - } - ], - "shangpinliebiao": [ - { - "id": 1, - "biaoqian": "战神段位陪玩", - "jiage": 50.00, - "tupian_url": "shangpin/1.jpg", - "leixing_id": 1, - "zhuanqu_id": 1, - "duiwai_xiaoliang": 100 - } - ] - } - """ - try: - logger.info("收到商品数据获取请求") - - # 1. 查询商品类型(所有字段) - leixing_queryset = ShangpinLeixing.objects.all() - shangpinleixing_data = [] - - for leixing in leixing_queryset: - shangpinleixing_data.append({ - "id": leixing.id, - "jieshao": leixing.jieshao, - "tupian_url": leixing.tupian_url - }) - - # 2. 查询商品专区(所有字段) - zhuanqu_queryset = ShangpinZhuanqu.objects.all() - shangpinzhuanqu_data = [] - - for zhuanqu in zhuanqu_queryset: - shangpinzhuanqu_data.append({ - "id": zhuanqu.id, - "mingzi": zhuanqu.mingzi, - "leixing_id": zhuanqu.leixing_id - }) - - # 3. 查询商品列表(只查询需要的字段,减少数据传输量) - # 注意:这里使用values()只获取指定字段,提高查询效率 - shangpin_queryset = Shangpin.objects.all().values( - 'id', 'biaoqian', 'jiage', 'leixing_id', - 'zhuanqu_id', 'tupian_url', 'duiwai_xiaoliang' - ) - - # 将QuerySet转换为列表 - shangpinliebiao_data = list(shangpin_queryset) - - # 4. 构建返回数据 - response_data = { - "shangpinleixing": shangpinleixing_data, - "shangpinzhuanqu": shangpinzhuanqu_data, - "shangpinliebiao": shangpinliebiao_data - } - - logger.info(f"返回数据:类型{len(shangpinleixing_data)}个," - f"专区{len(shangpinzhuanqu_data)}个," - f"商品{len(shangpinliebiao_data)}个") - - # 5. 返回响应 - return Response(response_data, status=status.HTTP_200_OK) - - except Exception as e: - # 记录异常 - logger.error(f"获取商品数据时发生错误:{str(e)}") - - # 返回错误响应 - error_data = { - "error": "服务器内部错误", - "detail": "获取商品数据失败,请稍后重试" - } - return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' - - - - - class ShangpinXiangqingHuoquView(APIView): """ 商品详情接口 - 极简版 @@ -386,16 +224,7 @@ class ShangpinXiangqingHuoquView(APIView): -# views.py -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework import status -from shangpin.models import Huiyuan -from yonghu.models import UserDashou -import logging -logger = logging.getLogger(__name__) class DashouHuiyuanList(APIView): """ @@ -460,23 +289,6 @@ class DashouHuiyuanList(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -# views.py -import random -import string -import time -import hashlib -import defusedxml.ElementTree as ET -import requests -from django.conf import settings -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework import status -from shangpin.models import Czjilu -from yonghu.models import UserDashou -import logging - -logger = logging.getLogger(__name__) class YajinGoumai(APIView): @@ -726,17 +538,6 @@ class YajinGoumai(APIView): return ip if ip else '127.0.0.1' -# views.py -from django.views import View -from django.http import HttpResponse -from yonghu.models import UserDashou, UserMain -from shangpin.models import Czjilu -import defusedxml.ElementTree as ET -import hashlib -from django.conf import settings -import logging - -logger = logging.getLogger(__name__) @@ -808,7 +609,6 @@ class YajinHuitiao(View): dashou = UserDashou.objects.get(user__yonghuid=order.yonghuid) # 增加押金(使用Decimal确保精度) - from decimal import Decimal dashou.yajin += Decimal(str(order_amount_yuan)) dashou.save(update_fields=['yajin']) @@ -816,11 +616,6 @@ class YajinHuitiao(View): # 8b. 押金分红(仅微信确认到账后;费率 Lilubiao fadanpingtai=12) try: - from decimal import Decimal - from django.db import transaction - from dingdan.models import Lilubiao - from shangpin.models import Gsfenhong - from yonghu.models import UserGuanshi guanshi_id = dashou.yaoqingren if not guanshi_id: @@ -860,7 +655,6 @@ class YajinHuitiao(View): f"打手{order.yonghuid}, 金额{fenhong_jine}元" ) try: - from houtai.utils import update_guanshi_daily_by_action update_guanshi_daily_by_action( yonghuid=guanshi_id, action=4, @@ -885,16 +679,10 @@ class YajinHuitiao(View): # 更新收支记录表 try: - from peizhi.models import Szjilu - from decimal import Decimal - - - # 获取订单金额(确保是Decimal类型) jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine # 查询或创建ID=1的收支记录 - from dingdan.utils import update_daily_income update_daily_income(jine_decimal) szjilu, created = Szjilu.objects.get_or_create( @@ -1407,13 +1195,8 @@ class JifenHuitiao(View): # 更新收支记录表 try: - from peizhi.models import Szjilu - from decimal import Decimal - # 获取订单金额(确保是Decimal类型) jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine - - from dingdan.utils import update_daily_income update_daily_income(jine_decimal) # 查询或创建ID=1的收支记录 @@ -1675,24 +1458,6 @@ class JifenLunxun(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -# views.py -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework import status -from shangpin.models import Huiyuan, Czjilu -from yonghu.models import UserDashou -import random -import string -import time -import hashlib -import defusedxml.ElementTree as ET -import requests -from django.conf import settings -import logging - -logger = logging.getLogger(__name__) - class HuiyuanGoumai(APIView): """ @@ -1945,27 +1710,6 @@ class HuiyuanGoumai(APIView): - - - - - - - -# views.py - -from django.views import View -from django.http import HttpResponse -from yonghu.models import UserDashou, UserMain, UserGuanshi,UserZuzhang -from shangpin.models import Czjilu, Huiyuan, Huiyuangoumai, Gsfenhong,DuociFenhong -from peizhi.models import Szjilu -from datetime import timedelta -import logging - -logger = logging.getLogger(__name__) - - - class HuiyuanHuitiao(View): """ 会员支付回调接口 @@ -2029,7 +1773,6 @@ class HuiyuanHuitiao(View): try: jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine - from dingdan.utils import update_daily_income update_daily_income(jine_decimal) szjilu, _ = Szjilu.objects.get_or_create( @@ -2243,7 +1986,6 @@ class HuiyuanHuitiao(View): # ========== 管事每日统计(新购) ========== if guanshi_fenhong > 0 and cishu == 1: try: - from houtai.utils import update_guanshi_daily_by_action update_guanshi_daily_by_action( yonghuid=guanshi_id, action=2, @@ -2256,7 +1998,6 @@ class HuiyuanHuitiao(View): # ========== 管事每日统计(续费) ========== if guanshi_fenhong > 0 and cishu > 1: try: - from houtai.utils import update_guanshi_xufei_daily update_guanshi_xufei_daily( yonghuid=guanshi_id, xufei_jine=guanshi_fenhong @@ -2268,7 +2009,6 @@ class HuiyuanHuitiao(View): # ========== 组长每日统计(分佣收入) ========== if zuzhang_fenhong > 0 and zuzhang_id: try: - from houtai.utils import update_zuzhang_daily_by_action update_zuzhang_daily_by_action( yonghuid=zuzhang_id, action=2, @@ -2378,471 +2118,6 @@ class HuiyuanHuitiao(View): - - - -# views.py - -'''from django.views import View -from django.http import HttpResponse -from yonghu.models import UserDashou, UserMain, UserGuanshi -from shangpin.models import Czjilu, Huiyuan, Huiyuangoumai, Gsfenhong -import defusedxml.ElementTree as ET -import hashlib -from django.conf import settings -from django.utils import timezone -from datetime import timedelta -import logging - -logger = logging.getLogger(__name__) - - -class HuiyuanHuitiao(View): - """ - 会员支付回调接口 - 路径:/shangpin/huiyuanhuitiao - 注意:这个接口不需要JWT认证,由微信服务器调用 - """ - - def post(self, request): - try: - # 1. 解析微信回调XML数据 - request_body = request.body - logger.info(f"收到会员支付回调,原始数据: {request_body.decode('utf-8')}") - - root = ET.fromstring(request_body) - - # 获取关键字段 - return_code = root.find('return_code').text - result_code = root.find('result_code').text - out_trade_no = root.find('out_trade_no').text # 订单ID - transaction_id = root.find('transaction_id').text # 微信交易号 - total_fee = int(root.find('total_fee').text) # 支付金额(分) - - logger.info(f"会员回调数据: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") - - # 2. 验证基本返回状态 - if return_code != 'SUCCESS' or result_code != 'SUCCESS': - logger.warning(f"会员支付回调状态异常: {return_code}/{result_code}") - return self.wechat_response('FAIL', '支付失败') - - # 3. 验证签名(完整版) - if not self.verify_signature(request_body): - logger.error(f"会员签名验证失败: {out_trade_no}") - return self.wechat_response('FAIL', '签名验证失败') - - # 4. 查询订单 - try: - order = Czjilu.objects.get(dingdan_id=out_trade_no) - logger.info(f"找到会员订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") - except Czjilu.DoesNotExist: - logger.error(f"会员订单不存在: {out_trade_no}") - return self.wechat_response('FAIL', '订单不存在') - - # 5. 检查订单类型是否为会员(1) - if order.leixing != 1: - logger.error(f"订单类型错误: {order.leixing}, 期望1(会员)") - return self.wechat_response('FAIL', '订单类型错误') - - # 6. 检查订单状态是否为未支付(9) - if order.zhuangtai != 9: - logger.warning(f"会员订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") - return self.wechat_response('SUCCESS', 'OK') - - # 7. 验证金额 - wechat_amount_yuan = total_fee / 100 # 转换为元 - order_amount_yuan = float(order.jine) # 订单金额(元) - - # 允许1分钱的误差 - if abs(wechat_amount_yuan - order_amount_yuan) > 0.01: - logger.error(f"会员金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") - return self.wechat_response('FAIL', '金额不一致') - - # 8. 查询会员信息 - try: - huiyuan = Huiyuan.objects.get(huiyuan_id=order.huiyuan_id) - except Huiyuan.DoesNotExist: - logger.error(f"会员信息不存在: {order.huiyuan_id}") - return self.wechat_response('FAIL', '会员信息不存在') - - # ===== 在这里添加收支记录更新代码 ===== - # 更新收支记录表 - try: - from peizhi.models import Szjilu - from decimal import Decimal - - # 获取订单金额(确保是Decimal类型) - jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine - - # 查询或创建ID=1的收支记录 - szjilu, created = Szjilu.objects.get_or_create( - id=1, - defaults={ - 'zongsy': Decimal('0.00'), - 'zongls': Decimal('0.00'), - 'zongzc': Decimal('0.00'), - 'jrzc': Decimal('0.00'), - 'jrls': Decimal('0.00') - } - ) - - # 更新三个字段 - szjilu.zongsy += jine_decimal # 总收益 - szjilu.zongls += jine_decimal # 总流水 - szjilu.jrls += jine_decimal # 今日流水 - - szjilu.save() - - logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") - - except Exception as e: - # 收支记录更新失败不影响主流程,只记录日志 - logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) - # ===== 添加结束 ===== - - - # 9. 更新订单状态为已支付(3) - order.zhuangtai = 3 - order.save(update_fields=['zhuangtai']) - logger.info(f"会员订单状态更新成功: {order.dingdan_id} -> 3") - - # 10. 更新会员表的购买次数 - huiyuan.goumai_cishu += 1 - huiyuan.save(update_fields=['goumai_cishu']) - - # 11. 处理会员购买记录 - #self.process_huiyuan_goumai(order.yonghuid, order.huiyuan_id, huiyuan) - - goumai_record, is_new, is_first_buy = self.process_huiyuan_goumai( - order.yonghuid, order.huiyuan_id, huiyuan - ) - - # 12. 如果是首次购买任何会员,加10积分 - if is_first_buy: - self.add_first_buy_jifen(order.yonghuid) - - # 12. 处理管事分红(只有在第一次购买时才分红) - self.process_guanshi_fenhong(order, huiyuan) - - # 13. 返回成功响应给微信 - logger.info(f"会员支付回调处理完成: {out_trade_no}") - return self.wechat_response('SUCCESS', 'OK') - - except ET.ParseError as e: - logger.error(f"会员XML解析失败: {str(e)}") - return self.wechat_response('FAIL', '数据格式错误') - except Exception as e: - logger.error(f"会员支付回调处理异常: {str(e)}", exc_info=True) - return self.wechat_response('FAIL', '处理异常') - - - def process_huiyuan_goumai(self, yonghuid, huiyuan_id, huiyuan): - """处理会员购买记录(优化版)""" - try: - # 1. 先尝试获取现有记录(如果存在就是续费) - existing_record = Huiyuangoumai.objects.filter( - yonghu_id=yonghuid, - huiyuan_id=huiyuan_id - ).first() - - is_first_buy_any = False # 标记是否首次购买任何会员 - - if existing_record: - # 已存在记录,续费 - existing_record.daoqi_time += timedelta(days=30) - existing_record.huiyuan_zhuangtai = 1 - existing_record.save() - logger.info(f"会员续费: 用户{yonghuid}, 会员{huiyuan_id}") - return existing_record, False, False # (记录, 是否新建, 是否首次买任何) - else: - # 2. 检查是否是首次购买任何会员(关键查询!) - # 查询该用户是否买过其他会员 - other_huiyuan_count = Huiyuangoumai.objects.filter( - yonghu_id=yonghuid - ).exclude(huiyuan_id=huiyuan_id).count() # 排除本次会员 - - if other_huiyuan_count == 0: - # 这是该用户第一次买任何会员! - is_first_buy_any = True - logger.info(f"用户{yonghuid}首次购买任何会员,将获得10积分") - - # 3. 创建新记录 - goumai_record = Huiyuangoumai.objects.create( - yonghu_id=yonghuid, - huiyuan_id=huiyuan_id, - jieshao=huiyuan.jieshao, - huiyuan_zhuangtai=1, - daoqi_time=timezone.now() + timedelta(days=30) - ) - logger.info(f"新购会员: 用户{yonghuid}, 会员{huiyuan_id}") - return goumai_record, True, is_first_buy_any - - except Exception as e: - logger.error(f"处理会员购买记录失败: {str(e)}", exc_info=True) - # 如果是因为唯一约束冲突(数据库已拦截) - if "unique" in str(e).lower() or "duplicate" in str(e).lower(): - logger.warning(f"检测到重复创建尝试,已由数据库拦截: 用户{yonghuid}, 会员{huiyuan_id}") - # 重新查询并续费 - return self.process_huiyuan_goumai(yonghuid, huiyuan_id, huiyuan) - raise - - - - - - - - - - def add_first_buy_jifen(self, yonghuid): - """为首次购买会员的用户增加10积分""" - try: - # 找到对应的打手记录 - dashou = UserDashou.objects.get(user__yonghuid=yonghuid) - # 增加10积分 - dashou.jifen += 10 - dashou.save(update_fields=['jifen']) - logger.info(f"用户{yonghuid}首次购买会员,积分+10,当前积分: {dashou.jifen}") - - # 可选:记录积分变动历史 - # self.create_jifen_history(yonghuid, 10, "首次购买会员奖励") - - except UserDashou.DoesNotExist: - logger.error(f"找不到打手记录,无法增加积分: {yonghuid}") - except Exception as e: - logger.error(f"增加积分失败: {str(e)}") - - - - - # - # - # - # - # - - def process_huiyuan_goumai(self, yonghuid, huiyuan_id, huiyuan): - """处理会员购买记录(完整版)""" - try: - # 查询是否已有购买记录 - goumai_record, created = Huiyuangoumai.objects.get_or_create( - yonghu_id=yonghuid, - huiyuan_id=huiyuan_id, - defaults={ - 'jieshao': huiyuan.jieshao, - 'huiyuan_zhuangtai': 1, - 'daoqi_time': timezone.now() + timedelta(days=30) - } - ) - - if not created: - # 已存在记录,续费(在原来基础上加30天) - goumai_record.daoqi_time += timedelta(days=30) - goumai_record.huiyuan_zhuangtai = 1 # 确保状态为激活 - goumai_record.save(update_fields=['daoqi_time', 'huiyuan_zhuangtai']) - logger.info(f"会员续费: 用户{yonghuid}, 会员{huiyuan_id}, 新到期时间{goumai_record.daoqi_time}") - else: - logger.info(f"新购会员: 用户{yonghuid}, 会员{huiyuan_id}, 到期时间{goumai_record.daoqi_time}") - - except Exception as e: - logger.error(f"处理会员购买记录失败: {str(e)}", exc_info=True) - # - # - # - # - # 继续执行,不中断流程 - - - def process_guanshi_fenhong(self, order, huiyuan): - """ - 处理管事分红(支持首次和二次分红,特殊用户首次可配置) - """ - try: - # 1. 获取打手信息 - dashou = UserDashou.objects.get(user__yonghuid=order.yonghuid) - yaoqingren = dashou.yaoqingren - if not yaoqingren: - logger.info(f"打手{order.yonghuid}没有邀请人,无分红") - return - - # 2. 获取管事信息 - try: - guanshi = UserGuanshi.objects.get(user__yonghuid=yaoqingren) - except UserGuanshi.DoesNotExist: - logger.warning(f"管事信息不存在: {yaoqingren}") - return - - # 3. 获取打手头像和昵称 - user_main = dashou.user - avatar = user_main.avatar - nicheng = dashou.nicheng or "未知打手" - - # 4. 判断是该打手第几次分红(通过查询分红记录表) - existing_count = Gsfenhong.objects.filter(dashouid=order.yonghuid).count() - is_first = (existing_count == 0) # 第一次分红 - - # 5. 确定分红金额 - fenhong_amount = None - if is_first: - # 首次分红 - special_ids = getattr(settings, 'SPECIAL_GUANSHI_IDS', []) - if yaoqingren in special_ids: - # 特殊管事:从管事扩展表获取首次分红金额 - fenhong_amount = guanshi.fenhong_yici - logger.info(f"管事{yaoqingren}为特殊用户,首次分红金额取配置值: {fenhong_amount}") - else: - # 普通管事:从会员配置表获取分红金额 - fenhong_amount = huiyuan.guanshifc - logger.info(f"管事{yaoqingren}为普通用户,首次分红金额取会员表: {fenhong_amount}") - else: - # 第二次及以上分红(目前只支持第二次) - if guanshi.fenghong_erci_enabled: - fenhong_amount = guanshi.fenghong_erci - logger.info(f"管事{yaoqingren}开启二次分红,分红金额: {fenhong_amount}") - else: - logger.info(f"管事{yaoqingren}未开启二次分红,本次不分红") - return - - # 6. 如果分红金额 <= 0,则不分红 - if fenhong_amount <= 0: - logger.info(f"分红金额 {fenhong_amount} <=0,不分红") - return - - # 7. 创建分红记录(注意:dashouid 唯一约束已移除) - Gsfenhong.objects.create( - dingdan_id=order.dingdan_id, - guanshi=yaoqingren, - dashouid=order.yonghuid, - shuoming=order.shuoming, - fenhong=fenhong_amount, - avatar=avatar, - nicheng=nicheng - ) - - # 8. 更新管事扩展表 - guanshi.chongzhifenrun += fenhong_amount - guanshi.yue += fenhong_amount - guanshi.jinrichongzhi += 1 - guanshi.jinyuechongzhi += 1 - guanshi.save(update_fields=['chongzhifenrun', 'yue', 'jinrichongzhi', 'jinyuechongzhi']) - - logger.info(f"创建分红记录成功: 管事{yaoqingren}, 打手{order.yonghuid}, 分红{fenhong_amount}元") - - except Exception as e: - logger.error(f"处理管事分红失败: {str(e)}", exc_info=True) - # 继续执行,不中断主流程 - - - - - #不针对特殊用户,且不含二次分红的版本 - # - # - # - #def process_guanshi_fenhong(self, order, huiyuan): - """处理管事分红(完整版,只有第一次购买才分红)""" - try: - # 查询打手信息 - dashou = UserDashou.objects.get(user__yonghuid=order.yonghuid) - - # 检查是否已有分红记录(确保只有第一次购买才分红) - if Gsfenhong.objects.filter(dashouid=order.yonghuid).exists(): - logger.info(f"打手{order.yonghuid}已有分红记录,不再分红") - return - - # 获取管事ID(邀请人ID) - yaoqingren = dashou.yaoqingren - if not yaoqingren: - logger.info(f"打手{order.yonghuid}没有邀请人,无分红") - return - - # 查询管事信息 - try: - guanshi = UserGuanshi.objects.get(user__yonghuid=yaoqingren) - except UserGuanshi.DoesNotExist: - logger.warning(f"管事信息不存在: {yaoqingren}") - return - - # 获取打手头像和昵称 - user_main = dashou.user - avatar = user_main.avatar - nicheng = dashou.nicheng or "未知打手" - - # 创建分红记录 - fenhong_record = Gsfenhong.objects.create( - dingdan_id=order.dingdan_id, - guanshi=yaoqingren, - dashouid=order.yonghuid, - shuoming=order.shuoming, - fenhong=huiyuan.guanshifc, # 管事分成金额 - avatar=avatar, - nicheng=nicheng - ) - - # 更新管事扩展表数据 - guanshi.chongzhifenrun += huiyuan.guanshifc - guanshi.yue += huiyuan.guanshifc - guanshi.jinrichongzhi += 1 - guanshi.jinyuechongzhi += 1 - guanshi.save(update_fields=['chongzhifenrun', 'yue', 'jinrichongzhi', 'jinyuechongzhi']) - - logger.info(f"创建分红记录: 管事{yaoqingren}, 打手{order.yonghuid}, 分成{huiyuan.guanshifc}元") - - except Exception as e: - logger.error(f"处理管事分红失败: {str(e)}", exc_info=True) - # - # - # - # - # 继续执行,不中断主流程 - - def verify_signature(self, xml_data): - """验证微信支付签名(完整版)""" - try: - root = ET.fromstring(xml_data) - sign = root.find('sign').text - - if not sign: - logger.error("会员签名字段不存在") - return False - - # 获取所有参数并排除sign - params = {} - for child in root: - if child.tag != 'sign' and child.text is not None: - params[child.tag] = child.text - - # 按照参数名ASCII码从小到大排序 - sorted_params = sorted(params.items(), key=lambda x: x[0]) - - # 拼接字符串 - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' - - # 计算签名 - calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - is_valid = sign == calculated_sign - if not is_valid: - logger.error(f"会员签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}") - - return is_valid - - except Exception as e: - logger.error(f"会员验证签名异常: {str(e)}") - return False - - def wechat_response(self, return_code, return_msg): - """返回微信支付回调响应(完整版)""" - xml_response = f""" - - -""" - - logger.info(f"会员返回微信响应: {return_code} - {return_msg}") - return HttpResponse(xml_response, content_type='application/xml')''' - - # views.py class HuiyuanShibai(APIView): """ @@ -3012,24 +2287,6 @@ class HuiyuanLunxun(APIView): # views.py in shangpin app -import time -import random -import string -import hashlib -import defusedxml.ElementTree as ET -import requests -from decimal import Decimal -from django.conf import settings -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from yonghu.models import UserMain, UserShangjia -from shangpin.models import Czjilu -import logging - -logger = logging.getLogger(__name__) - class ShangjiaChongzhi(APIView): """ @@ -3283,9 +2540,6 @@ class ShangjiaChongzhi(APIView): -from django.db import transaction - -logger = logging.getLogger(__name__) class ShangjiaHuitiao(View): @@ -3324,7 +2578,6 @@ class ShangjiaHuitiao(View): # 4. 查询订单 try: - from decimal import Decimal order = Czjilu.objects.get(dingdan_id=out_trade_no) logger.info(f"找到商家订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") except Czjilu.DoesNotExist: @@ -3349,13 +2602,9 @@ class ShangjiaHuitiao(View): # ===== 在这里添加收支记录更新代码 ===== # 更新收支记录表 try: - from peizhi.models import Szjilu - from decimal import Decimal - # 获取订单金额(确保是Decimal类型) jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine - from dingdan.utils import update_daily_income update_daily_income(jine_decimal) # 查询或创建ID=1的收支记录 @@ -3614,13 +2863,6 @@ class ShangjiaChenggong(APIView): # 管事分红记录查询API -from django.db.models import Q -from decimal import Decimal -import logging - -logger = logging.getLogger(__name__) - - class GuanshiFenhongListAPI(APIView): """ 管事分红记录查询接口 @@ -3652,8 +2894,6 @@ class GuanshiFenhongListAPI(APIView): query = Q(guanshi=yonghuid) # 先查询总数(使用分页查询优化) - from django.db.models import Count - from .models import Gsfenhong # 查询总记录数 zong_tiaoshu = Gsfenhong.objects.filter(query).count() @@ -3701,23 +2941,6 @@ class GuanshiFenhongListAPI(APIView): # views.py - 商品模块视图 - -from django.shortcuts import render -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework import status -from django.db.models import Q -import logging - -# 导入模型 -from yonghu.models import UserMain, AdminProfile -from shangpin.models import ShangpinLeixing, ShangpinZhuanqu, Huiyuan - -# 日志记录器 -logger = logging.getLogger(__name__) - - class AdGetAllDataView(APIView): """ 获取商品类型、商品专区、会员数据的接口 @@ -3876,21 +3099,6 @@ class AdGetAllDataView(APIView): # views.py - shangpin模块 -from django.db import transaction -from rest_framework.views import APIView -from rest_framework import permissions, status -from rest_framework.response import Response -from django.shortcuts import get_object_or_404 -from rest_framework.parsers import MultiPartParser, FormParser, JSONParser -import os -import uuid - -# 导入模型和工具 -from .models import ShangpinLeixing, Huiyuan, ShangpinZhuanqu, Shangpin -from yonghu.models import UserMain, AdminProfile -from utils.oss_utils import validate_image, upload_to_oss - - class ShangpinLeixingFabuView(APIView): """ 商品类型发布接口 @@ -4086,7 +3294,6 @@ class ShangpinLeixingFabuView(APIView): except Exception as e: # 捕获所有异常,返回500错误 - import traceback traceback.print_exc() return Response({ 'code': 500, @@ -4195,7 +3402,6 @@ class ShangpinZhuanquFabuView(APIView): }) except Exception as e: - import traceback traceback.print_exc() return Response({ 'code': 500, @@ -4500,7 +3706,6 @@ class ShangpinFabuView(APIView): except Exception as e: # 捕获所有异常,返回500错误 - import traceback traceback.print_exc() return Response({ 'code': 500, @@ -4643,7 +3848,6 @@ class GuizeTupianShangchuanView(APIView): except Exception as e: # 捕获所有异常,返回500错误 - import traceback traceback.print_exc() return Response({ 'code': 500, @@ -4751,7 +3955,6 @@ class AdHuiyuanHqView(APIView): except Exception as e: # 捕获所有异常,返回500错误 - import traceback traceback.print_exc() return Response({ 'code': 500, @@ -4923,7 +4126,6 @@ class HuiyuanXiugaiView(APIView): except Exception as e: # 捕获所有异常,返回500错误 - import traceback traceback.print_exc() return Response({ 'code': 500, @@ -5104,7 +4306,6 @@ class AdHuiyuanFbView(APIView): except Exception as e: # 捕获所有异常,返回500错误 - import traceback traceback.print_exc() return Response({ 'code': 500, @@ -5112,20 +4313,6 @@ class AdHuiyuanFbView(APIView): 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - -from django.shortcuts import get_object_or_404 -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework import status -from django.db.models import Q - -# 导入模型 -from yonghu.models import UserMain, AdminProfile -from .models import ShangpinLeixing, ShangpinZhuanqu, Huiyuan, Shangpin -from peizhi.models import Gonggao, Lunbo - - class AdShangpinHuQu(APIView): """ 后台商品管理页面数据获取接口 @@ -5242,13 +4429,6 @@ class AdShangpinHuQu(APIView): # shangpin/views.py -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework import status -from yonghu.models import UserMain, AdminProfile -from peizhi.models import Gonggao - class AdGonggaoXiuGai(APIView): """ @@ -5440,16 +4620,6 @@ class AdZhuanquXiuGai(APIView): -from rest_framework.parsers import MultiPartParser, FormParser -from django.db import transaction -from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss -from peizhi.models import Lunbo -import os -import uuid -import random -import string - - class AdLunboXiuGai(APIView): """ 后台编辑轮播图接口 @@ -5610,249 +4780,6 @@ class AdLunboXiuGai(APIView): ) -'''class AdLeixingXiuGai(APIView): - """ - 后台编辑商品类型接口 - 优化版 - 支持:只改图片、只改字段、两者都改 - 拒绝:两者都不改 - """ - - permission_classes = [IsAuthenticated] - parser_classes = (MultiPartParser, FormParser, JSONParser) # 添加JSONParser支持 - - def post(self, request): - try: - # 1. 权限验证(保持不变) - qianzhanghao = request.data.get('zhanghao', '').strip() - if not qianzhanghao: - return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - dangqianyonghu = request.user - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if dangqianyonghu.user_type != 'admin': - return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - guanliyuan = dangqianyonghu.admin_profile - except AdminProfile.DoesNotExist: - return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 获取商品类型ID - leixing_id = request.data.get('leixing_id') - if not leixing_id: - return Response({'code': 400, 'message': '商品类型ID不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 3. 查询商品类型 - try: - leixing_obj = ShangpinLeixing.objects.get(id=leixing_id) - except ShangpinLeixing.DoesNotExist: - return Response({'code': 400, 'message': '商品类型不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 4. 获取字段并初始化变更标记 - has_changed = False - jieshao = request.data.get('jieshao', '').strip() - yaoqiuleixing = request.data.get('yaoqiuleixing') - huiyuan_id = request.data.get('huiyuan_id', '').strip() - yongjin = request.data.get('yongjin', '').strip() - tupian_url = request.data.get('tupian_url', '').strip() # 原有图片URL - - # 5. 检查非图片字段是否有变更 - if jieshao and jieshao != leixing_obj.jieshao: - leixing_obj.jieshao = jieshao - has_changed = True - - if yaoqiuleixing and int(yaoqiuleixing) != leixing_obj.yaoqiuleixing: - leixing_obj.yaoqiuleixing = int(yaoqiuleixing) - has_changed = True - - # 根据类型清空另一个字段 - if leixing_obj.yaoqiuleixing == 1: - leixing_obj.yongjin = None - if huiyuan_id: - leixing_obj.huiyuan_id = huiyuan_id - # 验证会员存在 - try: - Huiyuan.objects.get(huiyuan_id=huiyuan_id) - except Huiyuan.DoesNotExist: - return Response({'code': 400, 'msg': '会员ID不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - else: # 类型2 - leixing_obj.huiyuan_id = None - if yongjin: - try: - leixing_obj.yongjin = float(yongjin) - if leixing_obj.yongjin <= 0: - return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - except ValueError: - return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # ===== 在现有代码的第5步后面添加 ===== - - # 5.5 检查抢单要求类型不变时,会员ID或佣金是否有变更 - if yaoqiuleixing and int(yaoqiuleixing) == leixing_obj.yaoqiuleixing: - # 类型没有变化,但需要检查对应字段的变化 - - # 情况1:会员抢单 (yaoqiuleixing == 1) - if leixing_obj.yaoqiuleixing == 1: - # 检查会员ID是否变更 - if huiyuan_id and huiyuan_id != leixing_obj.huiyuan_id: - # 验证会员存在 - try: - Huiyuan.objects.get(huiyuan_id=huiyuan_id) - except Huiyuan.DoesNotExist: - return Response({'code': 400, 'msg': '会员ID不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - leixing_obj.huiyuan_id = huiyuan_id - has_changed = True - #print(f"会员ID变更: {leixing_obj.huiyuan_id} -> {huiyuan_id}") - - # 情况2:佣金抢单 (yaoqiuleixing == 2) - elif leixing_obj.yaoqiuleixing == 2: - # 检查佣金是否变更 - if yongjin: - try: - new_yongjin = float(yongjin) - if new_yongjin <= 0: - return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 判断是否有变化(考虑None的情况) - current_yongjin = leixing_obj.yongjin - if (current_yongjin is None and new_yongjin != 0) or \ - (current_yongjin is not None and abs(float(current_yongjin) - new_yongjin) > 0.001): - leixing_obj.yongjin = new_yongjin - has_changed = True - #print(f"佣金变更: {current_yongjin} -> {new_yongjin}") - - except ValueError: - return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # ===== 添加结束 ===== - - # 6. 处理图片上传(如果有新图片) - tupian_file = request.FILES.get('file') - if tupian_file: - # 验证图片 - is_valid, error_msg = validate_image(tupian_file) - if not is_valid: - return Response({'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 删除旧图片(如果存在) - if leixing_obj.tupian_url: - delete_from_oss(leixing_obj.tupian_url) - - # 生成文件名和路径 - file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg' - oss_folder = 'a_long' - leixing_folder = 'leixing' - - # 使用原文件名或生成新文件名 - if leixing_obj.tupian_url: - original_filename = leixing_obj.tupian_url.split('/')[-1] - else: - original_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}" - - oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{original_filename}" - - # 上传到OSS - new_tupian_url = upload_to_oss(tupian_file, oss_file_path) - if not new_tupian_url: - return Response({'code': 500, 'message': '图片上传失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - leixing_obj.tupian_url = oss_file_path # 存相对路径 - has_changed = True - elif tupian_url and tupian_url != leixing_obj.tupian_url: - # 前端传了新URL(可能是从其他地方选择的) - leixing_obj.tupian_url = tupian_url - has_changed = True - - # 7. 验证是否至少有变更 - if not has_changed: - return Response({ - 'code': 400, - 'message': '没有检测到任何更改', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 8. 保存并返回 - leixing_obj.save() - - - - # 🔴 ===========【新增:同步更新关联商品】=========== - try: - from django.db.models import Q - - # 1. 准备商品更新字典 - shangpin_update_fields = {} - - # 始终同步抢单要求类型 - shangpin_update_fields['yaoqiuleixing'] = leixing_obj.yaoqiuleixing - - # 根据抢单类型,同步对应字段并清空另一个 - if leixing_obj.yaoqiuleixing == 1: # 会员抢单 - if leixing_obj.huiyuan_id: # 仅当会员ID非空时更新 - shangpin_update_fields['huiyuan_id'] = leixing_obj.huiyuan_id - # 清空佣金字段 - shangpin_update_fields['yongjin'] = None - elif leixing_obj.yaoqiuleixing == 2: # 佣金抢单 - if leixing_obj.yongjin: # 仅当佣金非空时更新 - shangpin_update_fields['yongjin'] = leixing_obj.yongjin - # 清空会员字段 - shangpin_update_fields['huiyuan_id'] = None - - # 2. 执行批量更新(确保只有实际有变化的字段才会执行) - if shangpin_update_fields: - # 查询所有属于该类型的商品(使用filter而非all以提高效率) - affected_count = Shangpin.objects.filter( - leixing_id=leixing_obj.id - ).update(**shangpin_update_fields) - - # 可选:记录日志以便追踪 - # logger.info(f"商品类型{leixing_obj.id}修改成功,已同步更新{affected_count}个关联商品") - - except Exception as sync_error: - # 商品同步失败不影响商品类型本身的修改,但记录错误 - # 可根据需求决定是否抛出异常回滚整个事务 - # 当前设计:记录错误但允许商品类型修改成功 - logger.error(f"修改十五") - - # 可在此添加更详细的错误日志记录 - # 🔴 ===========【新增结束】=========== - - response_data = { - 'id': leixing_obj.id, - 'jieshao': leixing_obj.jieshao, - 'tupian_url': leixing_obj.tupian_url, - 'yaoqiuleixing': leixing_obj.yaoqiuleixing, - 'huiyuan_id': leixing_obj.huiyuan_id, - 'yongjin': str(leixing_obj.yongjin) if leixing_obj.yongjin else None - } - - return Response({ - 'code': 0, - 'message': '商品类型修改成功', - 'data': response_data - }) - - except Exception as e: - print(f"编辑商品类型异常: {e}") - return Response({'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' - - - - @@ -6038,8 +4965,6 @@ class AdLeixingXiuGai(APIView): # 🔴 ===========【新增:同步更新关联商品】=========== try: - from django.db.models import Q - # 1. 准备商品更新字典 shangpin_update_fields = {} @@ -6210,437 +5135,9 @@ class AdShangpinNeiRongHuoQu(APIView): ) -from rest_framework.parsers import MultiPartParser, FormParser -from django.db import transaction -'''class AdShangpinXiuGai(APIView): - """ - 后台更改商品数据接口 - 修复版 - 修复:1. 图片替换逻辑 2. 对外销量字段处理 - """ - permission_classes = [IsAuthenticated] - parser_classes = (MultiPartParser, FormParser, JSONParser) - - def post(self, request): - try: - # 1. 权限验证 - qianzhanghao = request.data.get('zhanghao', '').strip() - shangpin_id = request.data.get('shangpin_id') - - if not qianzhanghao or not shangpin_id: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证身份 - dangqianyonghu = request.user - - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - if dangqianyonghu.user_type != 'admin': - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - try: - guanliyuan = dangqianyonghu.admin_profile - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 2. 判断操作类型:删除还是修改 - action = request.data.get('action', 'update').strip().lower() - - if action == 'delete': - return self._delete_shangpin(shangpin_id) - else: - return self._update_shangpin(request, shangpin_id) - - except Exception as e: - logger.error(f"更改商品数据异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - def _delete_shangpin(self, shangpin_id): - """删除商品逻辑""" - try: - # 查询商品 - try: - shangpin_obj = Shangpin.objects.get(id=shangpin_id) - except Shangpin.DoesNotExist: - return Response( - {'code': 404, 'message': '商品不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 获取图片URL(如果有) - tupian_url = shangpin_obj.tupian_url - guizetupian_url = shangpin_obj.guize_tupian - - # 异步或同步删除OSS图片(互不影响) - if tupian_url: - try: - delete_from_oss(tupian_url) - except Exception as e: - logger.warning(f"删除商品图片失败: {tupian_url}, 错误: {e}") - - if guizetupian_url: - try: - delete_from_oss(guizetupian_url) - except Exception as e: - logger.warning(f"删除规则图片失败: {guizetupian_url}, 错误: {e}") - - # 删除数据库记录 - shangpin_obj.delete() - - return Response({ - 'code': 0, - 'message': '删除成功', - 'data': { - 'shangpin_id': shangpin_id, - 'deleted': True - } - }) - - except Exception as e: - logger.error(f"删除商品异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '删除失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - def _update_shangpin(self, request, shangpin_id): - """修改商品逻辑 - 修复:确保图片正确替换""" - try: - # 查询商品 - try: - shangpin_obj = Shangpin.objects.get(id=shangpin_id) - except Shangpin.DoesNotExist: - return Response( - {'code': 404, 'message': '商品不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - has_changes = False - - # 3. 处理图片上传(如果有新商品图片) - tupian_file = request.FILES.get('tupian_file') - if tupian_file: - # 验证图片 - is_valid, error_msg = validate_image(tupian_file) - if not is_valid: - return Response( - {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 获取原图片URL - old_tupian_url = shangpin_obj.tupian_url - - # 确定OSS路径 - if old_tupian_url: - # 有原图:使用原路径替换 - oss_file_path = old_tupian_url - - # 删除原图片 - try: - delete_from_oss(old_tupian_url) - except Exception as e: - logger.warning(f"删除原商品图片失败: {old_tupian_url}, 错误: {e}") - # 继续上传新图片 - else: - # 没有原图:创建新路径 - oss_folder = 'a_long' - shangpin_folder = 'shangpin' - file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg' - filename = f"{shangpin_id}{file_extension}" - oss_file_path = f"{oss_folder}/{shangpin_folder}/{filename}" - - # 上传到OSS - new_tupian_url = upload_to_oss(tupian_file, oss_file_path) - - if not new_tupian_url: - return Response( - {'code': 500, 'message': '图片上传失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - # 更新数据库中的图片URL - shangpin_obj.tupian_url = oss_file_path # 存相对路径 - has_changes = True - - # 4. 处理其他字段(只更新有变化的字段) - field_mapping = { - 'biaoti': 'biaoqian', - 'jieshao': 'jieshao', - 'jiage': 'jiage', - 'kucun': 'kucun', - 'duiwaixiaoliang': 'duiwai_xiaoliang', # 关键:对外销量字段 - 'xiadanxuzhi': 'xiadan_xuzhi', - 'yajin': 'yajin', - } - - for front_field, db_field in field_mapping.items(): - if front_field in request.data: - new_value = request.data.get(front_field) - - # 空字符串处理 - if new_value == '': - new_value = None - - if new_value is not None: - # 获取当前值 - current_value = getattr(shangpin_obj, db_field) - - # 类型转换 - try: - if db_field in ['jiage', 'yajin']: - # 价格和押金 - if new_value: - new_value = Decimal(str(new_value)) - else: - new_value = None - elif db_field in ['kucun', 'duiwai_xiaoliang']: - # 库存和对外销量 - if new_value: - new_value = int(float(new_value)) - else: - new_value = 0 - except (ValueError, TypeError, InvalidOperation) as e: - logger.warning(f"字段{front_field}转换失败: {new_value}, 错误: {e}") - continue - - # 检查是否有变化(处理None值) - if current_value != new_value: - setattr(shangpin_obj, db_field, new_value) - has_changes = True - - # 5. 如果没有检测到任何更改 - if not has_changes: - return Response({ - 'code': 400, - 'message': '没有检测到任何更改', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 6. 保存更改 - shangpin_obj.save() - - # 7. 返回更新后的数据 - response_data = { - 'tupian_url': shangpin_obj.tupian_url or '', - 'biaoti': shangpin_obj.biaoqian or '', - 'jieshao': shangpin_obj.jieshao or '', - 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00', - 'kucun': shangpin_obj.kucun, - 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang, # 返回对外销量 - 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '', - 'yajin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None, - 'genggaishijian': shangpin_obj.update_time.strftime( - '%Y-%m-%d %H:%M:%S') if shangpin_obj.update_time else '', - } - - return Response({ - 'code': 0, - 'message': '更新成功', - 'data': response_data - }) - - except Exception as e: - logger.error(f"更新商品异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '更新失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - -import os -from decimal import Decimal, InvalidOperation - - -class AdShangpinGuizeXiuGai(APIView): - """ - 后台规则图片更换接口 - 修复版 - 修复:确保替换时使用正确的路径 - """ - - permission_classes = [IsAuthenticated] - parser_classes = (MultiPartParser, FormParser, JSONParser) - - def post(self, request): - try: - # 1. 权限验证 - qianzhanghao = request.data.get('zhanghao', '').strip() - shangpin_id = request.data.get('shangpin_id') - action = request.data.get('action', 'update').strip().lower() - - if not qianzhanghao or not shangpin_id: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证身份 - dangqianyonghu = request.user - - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - if dangqianyonghu.user_type != 'admin': - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - try: - guanliyuan = dangqianyonghu.admin_profile - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 2. 查询商品 - try: - shangpin_obj = Shangpin.objects.get(id=shangpin_id) - except Shangpin.DoesNotExist: - return Response( - {'code': 404, 'message': '商品不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 3. 处理不同操作 - if action == 'delete': - return self._delete_guize_image(shangpin_obj) - elif action == 'update': - return self._update_guize_image(request, shangpin_obj) - else: - return Response( - {'code': 400, 'message': '操作类型无效', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - except Exception as e: - logger.error(f"规则图片操作异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - def _delete_guize_image(self, shangpin_obj): - """删除规则图片""" - guizetupian_url = shangpin_obj.guize_tupian - - if guizetupian_url: - # 从OSS删除 - try: - delete_success = delete_from_oss(guizetupian_url) - if not delete_success: - logger.warning(f"OSS删除失败,但继续删除数据库记录: {guizetupian_url}") - except Exception as e: - logger.warning(f"删除规则图片异常: {e}") - - # 清除数据库记录 - shangpin_obj.guize_tupian = '' - shangpin_obj.save() - - return Response({ - 'code': 0, - 'message': '删除成功', - 'data': { - 'guizetupian_url': '' - } - }) - - def _update_guize_image(self, request, shangpin_obj): - """更新规则图片 - 修复:确保正确替换""" - guize_file = request.FILES.get('guize_file') - - if not guize_file: - return Response( - {'code': 400, 'message': '请上传规则图片', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证图片 - is_valid, error_msg = validate_image(guize_file) - if not is_valid: - return Response( - {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 处理路径 - oss_folder = 'a_long' - spgz_folder = 'Spgz' - file_extension = os.path.splitext(guize_file.name)[1].lower() or '.jpg' - - # 获取原规则图片URL - old_guizetupian_url = shangpin_obj.guize_tupian - - if old_guizetupian_url: - # 有原图:使用原路径替换 - oss_file_path = old_guizetupian_url - - # 删除原图片 - try: - delete_from_oss(old_guizetupian_url) - except Exception as e: - logger.warning(f"删除原规则图片失败: {old_guizetupian_url}, 错误: {e}") - # 继续上传新图片 - else: - # 没有原图:创建新路径 - filename = f"{shangpin_obj.id}{file_extension}" - oss_file_path = f"{oss_folder}/{spgz_folder}/{filename}" - - # 上传到OSS(替换或新建) - new_guizetupian_url = upload_to_oss(guize_file, oss_file_path) - - if not new_guizetupian_url: - return Response( - {'code': 500, 'message': '图片上传失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - # 更新数据库(存相对路径) - shangpin_obj.guize_tupian = oss_file_path # 存相对路径,如 "a_long/Spgz/123.jpg" - shangpin_obj.save() - - return Response({ - 'code': 0, - 'message': '规则图片更新成功', - 'data': { - 'guizetupian_url': oss_file_path # 返回相对路径 - } - })''' - -import os -import time -import random -from decimal import Decimal, InvalidOperation -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.parsers import MultiPartParser, FormParser, JSONParser -from rest_framework.permissions import IsAuthenticated -import logging - -logger = logging.getLogger(__name__) class AdShangpinXiuGai(APIView): @@ -7067,28 +5564,7 @@ class AdShangpinGuizeXiuGai(APIView): - - - - - - - - # views.py -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework import status -from django.utils import timezone -from django.db.models import Q, Count, Subquery, OuterRef -from django.db import connection -from django.core.paginator import Paginator -import json - -from .models import Huiyuan -from yonghu.models import UserMain, AdminProfile - class HuiyuanTypeListView(APIView): """ 获取会员类型列表接口 @@ -7333,7 +5809,6 @@ class HuiyuanRecordListView(APIView): except Exception as e: print(f"获取会员记录失败: {str(e)}") - import traceback traceback.print_exc() return Response({ 'code': 500, @@ -7344,14 +5819,6 @@ class HuiyuanRecordListView(APIView): - - - - - - -import decimal - class ZuzhangFenhongView(APIView): """ 组长分红记录接口 @@ -7505,520 +5972,10 @@ class ZuzhangFenhongView(APIView): - - -from dingdan.models import Lilubiao - -# 常量定义(可根据需要调整) -INTEGRAL_PRICE = decimal.Decimal('5.00') # 积分购买固定价格(元) - - -'''class CzhqdyView(APIView): - """ - 获取可用抵扣身份接口 - 路径:/shangpin/czhqdy - 方法:POST - 权限:JWT认证 - 请求参数: - leixing: int # 1=会员,2=押金,3=积分 - huiyuan_id: str # 当 leixing=1 时必传,会员ID - yajin_jine: decimal # 当 leixing=2 时必传,押金金额(元) - 返回: - { - "code": 200, - "msg": "success", - "data": [ - { - "id": 1, # 身份ID:1打手 2管事 3组长 - "jieshao": "打手佣金抵扣", - "xuyao": "22.22" # 所需抵扣金额(保留两位小数) - }, - ... - ] - } - """ - permission_classes = [permissions.IsAuthenticated] - - # 身份ID与对应扩展表模型、余额字段、费率查询条件的映射 - IDENTITY_CONFIG = [ - { - 'id': 1, - 'name': '打手佣金抵扣', - 'profile_attr': 'dashou_profile', # UserMain 的反向 related_name - 'balance_field': 'yue', - 'rate_key': '5', - 'model': UserDashou, - }, - { - 'id': 2, - 'name': '管事分红抵扣', - 'profile_attr': 'guanshi_profile', - 'balance_field': 'yue', - 'rate_key': '6', - 'model': UserGuanshi, - }, - { - 'id': 3, - 'name': '组长分红抵扣', - 'profile_attr': 'zuzhang_profile', - 'balance_field': 'ketixian_jine', - 'rate_key': '8', - 'model': UserZuzhang, - }, - ] - - def post(self, request): - # 获取参数 - leixing = request.data.get('leixing') - huiyuan_id = request.data.get('huiyuan_id') - yajin_jine = request.data.get('yajin_jine') - - # 参数校验 - if leixing not in [1, 2, 3,4]: - return Response({'code': 400, 'msg': 'leixing 必须为 '}, status=status.HTTP_400_BAD_REQUEST) - - if leixing == 1 and not huiyuan_id: - return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST) - - if leixing == 2: - if not yajin_jine: - return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST) - try: - yajin_jine = decimal.Decimal(str(yajin_jine)) - if yajin_jine <= 0: - raise ValueError - except: - return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST) - - # 获取当前用户 - user = request.user - - # 获取目标价格(需支付的原价) - target_price = None - if leixing == 1: - # 会员 - try: - huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id) - target_price = huiyuan.jiage - except Huiyuan.DoesNotExist: - return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND) - elif leixing == 2: - # 押金 - target_price = yajin_jine - - - - - elif leixing == 4: - from dingdan.models import Fadan # 罚单模型 - # 其他 UserDashou、UserGuanshi、UserZuzhang、Lilubiao、Czjilu 等已有 - # 罚款缴纳 - fadan_id = request.data.get('fadan_id') - if not fadan_id: - return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST) - try: - fadan = Fadan.objects.get(id=fadan_id, beichufa_id=request.user.yonghuid) - target_price = fadan.fakuanjine - except Fadan.DoesNotExist: - return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND) - - - - - else: # leixing == 3 - # 积分(固定价格) - target_price = INTEGRAL_PRICE - - # 获取所有费率(打手、管事、组长) - rates = {} - for key in ['5', '6', '8']: - try: - rate_obj = Lilubiao.objects.get(fadanpingtai=key) - rates[key] = rate_obj.lilu - except Lilubiao.DoesNotExist: - # 如果某个费率不存在,则跳过该身份(后续不会返回) - rates[key] = None - - # 构建可用身份列表 - available_list = [] - - for ident in self.IDENTITY_CONFIG: - # 检查用户是否有该扩展表 - profile = getattr(user, ident['profile_attr'], None) - if not profile: - continue - - # 获取余额 - balance = getattr(profile, ident['balance_field'], decimal.Decimal('0.00')) - - # 获取费率 - rate = rates.get(ident['rate_key']) - if rate is None: - continue # 费率未配置,跳过该身份 - - # 计算所需抵扣金额: target_price / (1 - rate) ,向上取整到分 - # 注意 rate 是小数,如 0.08 表示 8% - try: - # 防止除零 - if rate >= 1: - continue - required = target_price / (decimal.Decimal('1.0') - rate) - # 向上取整到分(保留两位小数,最后一位进位) - # 使用 quantize 向上舍入: ROUND_CEILING - required = required.quantize(decimal.Decimal('0.01'), rounding=decimal.ROUND_CEILING) - except Exception as e: - continue # 计算异常,跳过 - - # 余额是否足够 - if balance >= required: - available_list.append({ - 'id': ident['id'], - 'jieshao': ident['name'], - 'xuyao': str(required), - }) - - return Response({ - 'code': 200, - 'msg': 'success', - 'data': available_list - })''' - - - - - - -'''from decimal import Decimal, ROUND_CEILING -from dingdan.models import Fadan # 常量定义(可根据需要调整) INTEGRAL_PRICE = Decimal('5.00') # 积分购买固定价格(元) -INTEGRAL_INCREMENT = 5 # 每次积分补充增加5分 -MAX_INTEGRAL = 10 # 最大积分限制 - -# 身份映射配置 -IDENTITY_CONFIG = { - 1: { - 'name': '打手佣金', - 'model': UserDashou, - 'balance_field': 'yue', - 'status_field': 'zhanghaozhuangtai', # 1正常 - 'rate_key': '5', - }, - 2: { - 'name': '管事分红', - 'model': UserGuanshi, - 'balance_field': 'yue', - 'status_field': 'zhuangtai', # 1正常 - 'rate_key': '6', - }, - 3: { - 'name': '组长分红', - 'model': UserZuzhang, - 'balance_field': 'ketixian_jine', - 'status_field': 'zhuangtai', # 1正常 - 'rate_key': '8', - }, -} -from dingdan.models import Fadan -from shangpin.models import Huiyuan - - -class DsqrgmdhView(APIView): - """ - 余额抵扣支付接口 - 路径:/shangpin/dsqrgmdh - 方法:POST - 权限:JWT认证 - 请求参数: - leixing: int # 购买类型:1会员 2押金 3积分 - shenfen_id: int # 抵扣身份:1打手 2管事 3组长 - huiyuan_id: str # 当 leixing=1 时必传,会员ID - yajin_jine: decimal # 当 leixing=2 时必传,押金金额(元) - 返回: - { - "code": 200, - "msg": "success", - "data": { - # 根据购买类型返回对应字段 - "yajin": 1000, # 押金更新后(leixing=2) - "jifen": 10, # 积分更新后(leixing=3) - "huiyuan": { ... } # 会员购买记录(leixing=1) - # 以及其他可能需要的数据 - } - } - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - # 获取参数 - leixing = request.data.get('leixing') - shenfen_id = request.data.get('shenfen_id') - huiyuan_id = request.data.get('huiyuan_id') - yajin_jine = request.data.get('yajin_jine') - - # ----- 参数基本校验 ----- - if leixing not in [1, 2, 3,4]: - return Response({'code': 400, 'msg': 'leixing 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST) - if shenfen_id not in [1, 2, 3]: - return Response({'code': 400, 'msg': 'shenfen_id 必须为 1/2/3'}, status=status.HTTP_400_BAD_REQUEST) - - if leixing == 1 and not huiyuan_id: - return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST) - if leixing == 2: - if not yajin_jine: - return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST) - try: - yajin_jine = Decimal(str(yajin_jine)) - if yajin_jine <= 0: - raise ValueError - if yajin_jine > Decimal('10000'): - return Response({'code': 400, 'msg': '押金金额不能超过10000元'}, status=status.HTTP_400_BAD_REQUEST) - except: - return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST) - - # ----- 获取目标价格(需支付的原价) ----- - target_price = None - if leixing == 1: - try: - huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id) - target_price = huiyuan.jiage - except Huiyuan.DoesNotExist: - return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND) - elif leixing == 2: - target_price = yajin_jine - - - elif leixing == 4: - from dingdan.models import Fadan - fadan_id = request.data.get('fadan_id') - if not fadan_id: - return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST) - try: - fadan = Fadan.objects.get(id=fadan_id, beichufa_id=request.user.yonghuid) - target_price = fadan.fakuanjine - except Fadan.DoesNotExist: - return Response({'code': 404, 'msg': '罚单不存在'}, status=status.HTTP_404_NOT_FOUND) - - - - else: # leixing == 3 - target_price = INTEGRAL_PRICE - - # ----- 获取对应身份费率 ----- - identity = IDENTITY_CONFIG.get(shenfen_id) - try: - rate_obj = Lilubiao.objects.get(fadanpingtai=identity['rate_key']) - rate = rate_obj.lilu # Decimal - if rate >= 1: - return Response({'code': 400, 'msg': f"{identity['name']}费率异常"}, status=status.HTTP_400_BAD_REQUEST) - except Lilubiao.DoesNotExist: - return Response({'code': 400, 'msg': f"{identity['name']}费率未配置"}, status=status.HTTP_400_BAD_REQUEST) - - # ----- 计算所需抵扣金额(向上取整到分) ----- - try: - required = target_price / (Decimal('1.0') - rate) - required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING) - except Exception as e: - return Response({'code': 500, 'msg': f'金额计算错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # ----- 获取当前用户 ----- - user = request.user - yonghuid = user.yonghuid - - # ----- 获取对应身份扩展表并验证状态和余额 ----- - try: - profile = identity['model'].objects.get(user=user) - except identity['model'].DoesNotExist: - return Response({'code': 403, 'msg': f'您不是{identity["name"]}身份'}) - - # 验证账号状态 - status_value = getattr(profile, identity['status_field']) - if status_value != 1: - return Response({'code': 403, 'msg': f'{identity["name"]}账号状态异常,无法使用'}) - - # 验证余额是否足够 - balance = getattr(profile, identity['balance_field']) - if balance < required: - return Response({'code': 400, 'msg': f'{identity["name"]}余额不足,需要{required}元,当前{balance}元'}, status=status.HTTP_400_BAD_REQUEST) - - # ----- 开始事务处理 ----- - try: - with transaction.atomic(): - # 锁定扩展表记录(防止并发扣除) - locked_profile = identity['model'].objects.select_for_update().get(id=profile.id) - - # 再次验证余额(安全) - current_balance = getattr(locked_profile, identity['balance_field']) - if current_balance < required: - raise ValueError(f"{identity['name']}余额不足,需要{required}元,当前{current_balance}元") - - # 扣除余额 - setattr(locked_profile, identity['balance_field'], current_balance - required) - locked_profile.save(update_fields=[identity['balance_field']]) - - # ----- 生成订单ID ----- - timestamp = str(int(time.time() * 1000))[-12:] - rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4)) - dingdan_id = f"CZ{timestamp}{rand_str}" - - # 构建订单说明 - if leixing == 1: - action_desc = f"购买会员 {huiyuan.jieshao}" - elif leixing == 2: - action_desc = f"充值押金 {yajin_jine}元" - - elif leixing ==4: - action_desc = "罚款缴纳" - else: - action_desc = "补充积分" - - shuoming = f"{identity['name']}抵扣 {action_desc},抵扣金额{required}元" - - # 创建充值记录 - czjilu = Czjilu.objects.create( - dingdan_id=dingdan_id, - huiyuan_id=huiyuan_id if leixing == 1 else None, - zhuangtai=1, # 已完成 - yonghuid=yonghuid, - jine=required, # 实际扣除的金额 - leixing=leixing, - shuoming=shuoming, - ) - - # ----- 根据购买类型执行具体业务逻辑 ----- - response_data = {} - - if leixing == 1: # 会员 - # 处理会员购买/续费 - huiyuan_record, is_first_buy = self._process_member_purchase(yonghuid, huiyuan_id, huiyuan) - response_data['huiyuan'] = { - 'huiyuanid': huiyuan_id, - 'huiyuanming': huiyuan.jieshao, - 'daoqi': huiyuan_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(huiyuan_record, 'daoqi_time') else '', - } - - # 如果首次购买任何会员,给打手加10积分 - if is_first_buy: - try: - dashou = UserDashou.objects.get(user=user) - dashou.jifen += 10 - dashou.save(update_fields=['jifen']) - response_data['jifen'] = dashou.jifen - except UserDashou.DoesNotExist: - pass - - elif leixing == 2: # 押金 - # 更新打手押金 - try: - dashou = UserDashou.objects.select_for_update().get(user=user) - dashou.yajin += yajin_jine # 注意:增加的是原价,不是抵扣金额 - dashou.save(update_fields=['yajin']) - response_data['yajin'] = dashou.yajin - except UserDashou.DoesNotExist: - return Response({'code': 500, 'msg': '打手信息不存在,无法充值押金'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - elif leixing == 4: # 🔥 罚款:只更新罚单状态,不重复扣款 - - fadan_id = request.data.get('fadan_id') - if not fadan_id: - return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST) - try: - fadan = Fadan.objects.get(id=fadan_id, beichufa_id=request.user.yonghuid) - except Fadan.DoesNotExist: - return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND) - - if fadan.zhuangtai not in [1, 3]: - return Response({'code': 400, 'msg': '罚单状态不可缴纳'}, status=status.HTTP_400_BAD_REQUEST) - - fadan.zhuangtai = 2 # 已缴纳 - fadan.save(update_fields=['zhuangtai']) - response_data['fadan_id'] = fadan.id - response_data['fadan_status'] = fadan.zhuangtai - from utils.fadan_utils import fadan_fenhong - fadan_fenhong(fadan.id) - - - - else: # leixing == 3 积分 - # 更新打手积分 - try: - dashou = UserDashou.objects.select_for_update().get(user=user) - new_jifen = dashou.jifen + INTEGRAL_INCREMENT - dashou.jifen = min(new_jifen, MAX_INTEGRAL) # 不超过10分 - dashou.save(update_fields=['jifen']) - response_data['jifen'] = dashou.jifen - except UserDashou.DoesNotExist: - return Response({'code': 500, 'msg': '打手信息不存在,无法补充积分'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - except Exception as e: - import traceback - traceback.print_exc() - return Response({'code': 500, 'msg': f'支付处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # ----- 返回成功响应,附带更新后的数据 ----- - # 补充其他可能需要的数据(如更新后的余额等) - # 可以查询最新的余额返回 - final_profile = identity['model'].objects.get(user=user) - response_data['balance'] = str(getattr(final_profile, identity['balance_field'])) - - return Response({ - 'code': 200, - 'msg': 'success', - 'data': response_data, - }) - - def _process_member_purchase(self, yonghuid, huiyuan_id, huiyuan_obj): - """ - 处理会员购买/续费 - 返回 (Huiyuangoumai对象, is_first_buy_any) - """ - is_first_buy_any = False - try: - # 检查是否已有该会员记录 - record = Huiyuangoumai.objects.filter( - yonghu_id=yonghuid, - huiyuan_id=huiyuan_id - ).first() - - if record: - # 续费 - record.daoqi_time += timedelta(days=30) - record.huiyuan_zhuangtai = 1 - record.save(update_fields=['daoqi_time', 'huiyuan_zhuangtai']) - else: - # 新购 - # 检查用户是否购买过任何会员(用于判断是否首次购买) - other_count = Huiyuangoumai.objects.filter(yonghu_id=yonghuid).exclude(huiyuan_id=huiyuan_id).count() - if other_count == 0: - is_first_buy_any = True - - record = Huiyuangoumai.objects.create( - yonghu_id=yonghuid, - huiyuan_id=huiyuan_id, - jieshao=huiyuan_obj.jieshao, - huiyuan_zhuangtai=1, - daoqi_time=timezone.now() + timedelta(days=30) - ) - return record, is_first_buy_any - except Exception as e: - # 如果出现唯一性冲突(极低概率),重试一次 - if 'unique' in str(e).lower() or 'duplicate' in str(e).lower(): - # 直接查询已存在的记录 - record = Huiyuangoumai.objects.get(yonghu_id=yonghuid, huiyuan_id=huiyuan_id) - return record, False - raise''' - - - - -from dingdan.models import Fadan # 用于罚款类型 class CzhqdyView(APIView): """ 获取可用抵扣身份接口 @@ -8101,7 +6058,7 @@ class CzhqdyView(APIView): if not yajin_jine: return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST) try: - yajin_jine = decimal.Decimal(str(yajin_jine)) + yajin_jine = Decimal(str(yajin_jine)) if yajin_jine <= 0: raise ValueError except: @@ -8155,7 +6112,7 @@ class CzhqdyView(APIView): continue # 获取余额 - balance = getattr(profile, ident['balance_field'], decimal.Decimal('0.00')) + balance = getattr(profile, ident['balance_field'], Decimal('0.00')) # 获取费率 rate = rates.get(ident['rate_key']) @@ -8166,9 +6123,9 @@ class CzhqdyView(APIView): try: if rate >= 1: continue # 防止除零及不合理费率 - required = target_price / (decimal.Decimal('1.0') - rate) + required = target_price / (Decimal('1.0') - rate) # 向上取整到分(保留两位小数,最后一位进位) - required = required.quantize(decimal.Decimal('0.01'), rounding=decimal.ROUND_CEILING) + required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING) except Exception: continue # 计算异常,跳过 @@ -8188,13 +6145,6 @@ class CzhqdyView(APIView): - - - -from decimal import Decimal, ROUND_CEILING -# ========== 修改点1: 导入最新的分红处理方法 ========== -from yonghu.fadan_fenhong_utils import process_fadan_fenhong - INTEGRAL_INCREMENT = 5 MAX_INTEGRAL = 10 diff --git a/shangdian/__init__.py b/rank/__init__.py similarity index 100% rename from shangdian/__init__.py rename to rank/__init__.py diff --git a/shangdian/admin.py b/rank/admin.py similarity index 100% rename from shangdian/admin.py rename to rank/admin.py diff --git a/dengji/apps.py b/rank/apps.py similarity index 100% rename from dengji/apps.py rename to rank/apps.py diff --git a/dengji/migrations/0001_initial.py b/rank/migrations/0001_initial.py similarity index 100% rename from dengji/migrations/0001_initial.py rename to rank/migrations/0001_initial.py diff --git a/dengji/migrations/0002_alter_dashoubiaoxian_dingdan.py b/rank/migrations/0002_alter_dashoubiaoxian_dingdan.py similarity index 100% rename from dengji/migrations/0002_alter_dashoubiaoxian_dingdan.py rename to rank/migrations/0002_alter_dashoubiaoxian_dingdan.py diff --git a/dengji/migrations/0003_alter_dashoubiaoxian_bankuai.py b/rank/migrations/0003_alter_dashoubiaoxian_bankuai.py similarity index 100% rename from dengji/migrations/0003_alter_dashoubiaoxian_bankuai.py rename to rank/migrations/0003_alter_dashoubiaoxian_bankuai.py diff --git a/dengji/migrations/0004_dingdanbiaoqian.py b/rank/migrations/0004_dingdanbiaoqian.py similarity index 100% rename from dengji/migrations/0004_dingdanbiaoqian.py rename to rank/migrations/0004_dingdanbiaoqian.py diff --git a/dengji/migrations/0005_fix_state.py b/rank/migrations/0005_fix_state.py similarity index 100% rename from dengji/migrations/0005_fix_state.py rename to rank/migrations/0005_fix_state.py diff --git a/dengji/migrations/0006_shenhejilu.py b/rank/migrations/0006_shenhejilu.py similarity index 100% rename from dengji/migrations/0006_shenhejilu.py rename to rank/migrations/0006_shenhejilu.py diff --git a/dengji/migrations/0007_kaoheguanbankuai.py b/rank/migrations/0007_kaoheguanbankuai.py similarity index 100% rename from dengji/migrations/0007_kaoheguanbankuai.py rename to rank/migrations/0007_kaoheguanbankuai.py diff --git a/dengji/migrations/0008_chenghao_kaohe_guize_shenhejilu_dashou_beizhu_and_more.py b/rank/migrations/0008_chenghao_kaohe_guize_shenhejilu_dashou_beizhu_and_more.py similarity index 100% rename from dengji/migrations/0008_chenghao_kaohe_guize_shenhejilu_dashou_beizhu_and_more.py rename to rank/migrations/0008_chenghao_kaohe_guize_shenhejilu_dashou_beizhu_and_more.py diff --git a/dengji/migrations/0009_chenghao_kaioi_jinpai_alter_shenhejilu_jilu_id_and_more.py b/rank/migrations/0009_chenghao_kaioi_jinpai_alter_shenhejilu_jilu_id_and_more.py similarity index 100% rename from dengji/migrations/0009_chenghao_kaioi_jinpai_alter_shenhejilu_jilu_id_and_more.py rename to rank/migrations/0009_chenghao_kaioi_jinpai_alter_shenhejilu_jilu_id_and_more.py diff --git a/dengji/migrations/0010_kaohejilufeiyong.py b/rank/migrations/0010_kaohejilufeiyong.py similarity index 100% rename from dengji/migrations/0010_kaohejilufeiyong.py rename to rank/migrations/0010_kaohejilufeiyong.py diff --git a/dengji/migrations/0011_kaohepaytemp.py b/rank/migrations/0011_kaohepaytemp.py similarity index 100% rename from dengji/migrations/0011_kaohepaytemp.py rename to rank/migrations/0011_kaohepaytemp.py diff --git a/shangdian/migrations/__init__.py b/rank/migrations/__init__.py similarity index 100% rename from shangdian/migrations/__init__.py rename to rank/migrations/__init__.py diff --git a/dengji/models.py b/rank/models.py similarity index 99% rename from dengji/models.py rename to rank/models.py index de3631a..0ebb16a 100644 --- a/dengji/models.py +++ b/rank/models.py @@ -1,6 +1,6 @@ from django.db import models from django.conf import settings -from dingdan.models import Dingdan +from orders.models import Dingdan # ==================== 板块表 ==================== class Bankuai(models.Model): diff --git a/dengji/services.py b/rank/services.py similarity index 100% rename from dengji/services.py rename to rank/services.py diff --git a/shangdian/tests.py b/rank/tests.py similarity index 100% rename from shangdian/tests.py rename to rank/tests.py diff --git a/dengji/urls.py b/rank/urls.py similarity index 100% rename from dengji/urls.py rename to rank/urls.py diff --git a/dengji/utils.py b/rank/utils.py similarity index 95% rename from dengji/utils.py rename to rank/utils.py index 3e0c6b1..d115360 100644 --- a/dengji/utils.py +++ b/rank/utils.py @@ -1,24 +1,26 @@ -# dengji/utils.py -from .models import Chenghao, KaoheCishuFeiyong, ShenheJilu, KaoheJiluFeiyong, YonghuChenghao, KaoheguanBankuai -from yonghu.models import UserMain, UserShenheguan, UserDashou -from django.db import models -from decimal import Decimal -from django.db.models import Q, Max +# 标准库 import time import random import logging -from django.db.models import F, Max -logger = logging.getLogger('dengji') +from decimal import Decimal +# Django 内置 +from django.db import models +from django.db.models import Q, F, Max +# 项目模型 +## 当前dengji应用 +from .models import ( + Chenghao, KaoheCishuFeiyong, ShenheJilu, KaoheJiluFeiyong, + YonghuChenghao, KaoheguanBankuai, DingdanBiaoqian +) +## users应用 +from users.models import UserMain, UserShenheguan, UserDashou +## orders应用 +from orders.models import YonghuChenghao +# 日志对象 +logger = logging.getLogger(__name__) - - -# dingdan/utils.py -from .models import DingdanBiaoqian -from yonghu.models import UserMain -from dengji.models import YonghuChenghao - def check_dashou_biaoqian_required(order_id, dashou_yonghuid): """ 检查打手是否满足订单的标签需求。 @@ -60,8 +62,6 @@ def check_dashou_biaoqian_required(order_id, dashou_yonghuid): except Exception as e: # 标签校验失败不影响主流程,记录日志并保守放行(也可保守拒绝,按需求选择) - import logging - logger = logging.getLogger('dingdan') logger.error(f"标签校验异常: {e}", exc_info=True) # 安全起见,如果校验出错,选择拒绝,避免越权 return False, '订单标签校验异常,请稍后重试' @@ -282,10 +282,6 @@ def create_shenhe_jilu_from_temp(user, temp_data): 参数 temp_data 是 KaohePayTemp 对象 返回 (success, message, record) """ - from .models import ShenheJilu, KaoheJiluFeiyong, Chenghao, YonghuChenghao - from django.db.models import F - import logging - logger = logging.getLogger('dengji') try: logger.info(f"开始从临时表创建审核记录: 用户={user.yonghuid}, 订单={temp_data.dingdan_id}") diff --git a/dengji/views.py b/rank/views.py similarity index 96% rename from dengji/views.py rename to rank/views.py index a26b81a..cfbd185 100644 --- a/dengji/views.py +++ b/rank/views.py @@ -1,15 +1,21 @@ -# dengji/views.py -import logging import json +import logging +from decimal import Decimal +from django.db import models, transaction, IntegrityError +from django.db.models import F, Q, OuterRef, Subquery, Count, Max from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from .utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan +from .models import ( + YonghuChenghao, Chenghao, KaoheCishuFeiyong, KaoheJujueJilu, + KaoheguanBankuai, Bankuai, ShenheJilu, KaoheJiluFeiyong +) +from users.models import UserShenheguan, UserMain, UserDashou +from products.models import ShangpinLeixing -from shangpin.models import ShangpinLeixing -from .models import YonghuChenghao, Chenghao, KaoheCishuFeiyong, KaoheJujueJilu -from .utils import get_tag_fee, create_shenhe_jilu -logger = logging.getLogger('dengji') +logger = logging.getLogger(__name__) class DashouBiaoqianView(APIView): @@ -50,9 +56,6 @@ class DashouBiaoqianView(APIView): return Response({'code': 1, 'msg': '服务器错误', 'data': None}) - -from shangpin.models import ShangpinLeixing -from .models import YonghuChenghao, Chenghao class BankuaiBiaoqianView(APIView): """获取商品类型所属板块的所有标签""" permission_classes = [IsAuthenticated] @@ -91,18 +94,6 @@ class BankuaiBiaoqianView(APIView): - -# dengji/views.py -import logging -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from .models import KaoheguanBankuai, Bankuai -from django.db import transaction -from yonghu.models import UserShenheguan, UserMain, UserDashou - -logger = logging.getLogger('dengji') - # 硬编码邀请码 KAOHEGUAN_INVITE_CODE = 'KHG2026VIP' @@ -211,14 +202,6 @@ class KaoheguanRefreshView(APIView): -import json -from django.db import transaction, IntegrityError -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response - -from .models import Bankuai, ShenheJilu, KaoheguanBankuai, Chenghao - class ShenheDatingModuleView(APIView): """审核大厅获取模块列表(板块 + 待审核数量 + 是否关联)""" permission_classes = [IsAuthenticated] @@ -377,15 +360,7 @@ class ShenheQiangdanView(APIView): -import json -import logging -from decimal import Decimal -from django.db import transaction, models -from django.db.models import F, Q, OuterRef, Subquery, Count, Max -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response class ShenheXiangqingView(APIView): """获取当前考核官审核中的记录详情(新增待收考核费字段)""" @@ -684,10 +659,7 @@ class KaoheJinpaiTagsView(APIView): except Exception as e: return Response({'code': 500, 'msg': str(e)}, status=500) -# dengji/utils.py -from .models import Chenghao, KaoheCishuFeiyong, ShenheJilu, KaoheJiluFeiyong, YonghuChenghao -from yonghu.models import UserMain -from django.db import models + class KaoheJiluView(APIView): """获取打手本人的考核记录(支持筛选状态)""" permission_classes = [IsAuthenticated] @@ -762,7 +734,7 @@ class KaoheJiluView(APIView): -from .utils import validate_shenheguan + class KaoheFreeApplyView(APIView): """免费申请考核(首次/再次,费用为0时调用)""" permission_classes = [IsAuthenticated] diff --git a/rp.py b/rp.py new file mode 100644 index 0000000..d62c8b0 --- /dev/null +++ b/rp.py @@ -0,0 +1,80 @@ +import os + +# 目录定义 +ROOT_DIR = r"./" +EXTRA_FILES = [ + "./TransPyC.py", + "./StubGen.py", + "./Projectrans.py" +] + +def snake_to_upper_camel(s: str) -> str: + if not s: + return s + + if s.startswith('_'): + first = '_' + rest = s[1:] + else: + first = '' + rest = s + + parts = rest.split('_') + pascal = ''.join(part.capitalize() for part in parts) + return first + pascal + +while True: + old_str = input("请输入原蛇形字符串:").strip() + new_input = input("替换内容(留空自动转大驼峰):").strip() + + if not old_str: + print("错误:原字符串不能为空\n") + continue + + total_mod = 0 + + # ====================== 1. 处理 lib 下所有 .py ====================== + for root, _, files in os.walk(ROOT_DIR): + for file in files: + if file.endswith(".py"): + file_path = os.path.join(root, file) + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + if new_input == "": + replace_target = snake_to_upper_camel(old_str) + new_content = content.replace(old_str, replace_target) + else: + new_content = content.replace(old_str, new_input) + + if new_content != content: + with open(file_path, "w", encoding="utf-8") as f: + f.write(new_content) + print(f"已修改:{file_path}") + total_mod += 1 + except Exception as err: + print(f"异常 {file_path}:{err}") + + # ====================== 2. 处理根目录下 3 个指定文件 ====================== + for file_path in EXTRA_FILES: + if os.path.exists(file_path): + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + if new_input == "": + replace_target = snake_to_upper_camel(old_str) + new_content = content.replace(old_str, replace_target) + else: + new_content = content.replace(old_str, new_input) + + if new_content != content: + with open(file_path, "w", encoding="utf-8") as f: + f.write(new_content) + print(f"已修改:{file_path}") + total_mod += 1 + except Exception as err: + print(f"异常 {file_path}:{err}") + + print(f"\n处理完成,共计修改 {total_mod} 个文件\n") \ No newline at end of file diff --git a/shangpin/__init__.py b/shop/__init__.py similarity index 100% rename from shangpin/__init__.py rename to shop/__init__.py diff --git a/shangpin/admin.py b/shop/admin.py similarity index 100% rename from shangpin/admin.py rename to shop/admin.py diff --git a/shangdian/apps.py b/shop/apps.py similarity index 100% rename from shangdian/apps.py rename to shop/apps.py diff --git a/shangdian/migrations/0001_initial.py b/shop/migrations/0001_initial.py similarity index 100% rename from shangdian/migrations/0001_initial.py rename to shop/migrations/0001_initial.py diff --git a/shangdian/migrations/0002_alter_yonghupingzheng_yonghu.py b/shop/migrations/0002_alter_yonghupingzheng_yonghu.py similarity index 100% rename from shangdian/migrations/0002_alter_yonghupingzheng_yonghu.py rename to shop/migrations/0002_alter_yonghupingzheng_yonghu.py diff --git a/shangdian/migrations/0003_dianpumorenpeizhi_dianpu_kaiqi_meiri_xiane_and_more.py b/shop/migrations/0003_dianpumorenpeizhi_dianpu_kaiqi_meiri_xiane_and_more.py similarity index 100% rename from shangdian/migrations/0003_dianpumorenpeizhi_dianpu_kaiqi_meiri_xiane_and_more.py rename to shop/migrations/0003_dianpumorenpeizhi_dianpu_kaiqi_meiri_xiane_and_more.py diff --git a/shangdian/migrations/0004_dianpushangpinshenheshezhi.py b/shop/migrations/0004_dianpushangpinshenheshezhi.py similarity index 100% rename from shangdian/migrations/0004_dianpushangpinshenheshezhi.py rename to shop/migrations/0004_dianpushangpinshenheshezhi.py diff --git a/shangdian/migrations/0005_dianpu_fenhong_choucheng_feilv_and_more.py b/shop/migrations/0005_dianpu_fenhong_choucheng_feilv_and_more.py similarity index 100% rename from shangdian/migrations/0005_dianpu_fenhong_choucheng_feilv_and_more.py rename to shop/migrations/0005_dianpu_fenhong_choucheng_feilv_and_more.py diff --git a/shangdian/migrations/0006_dianpushouzhimeiritongji_gei_shangji_cishu_and_more.py b/shop/migrations/0006_dianpushouzhimeiritongji_gei_shangji_cishu_and_more.py similarity index 100% rename from shangdian/migrations/0006_dianpushouzhimeiritongji_gei_shangji_cishu_and_more.py rename to shop/migrations/0006_dianpushouzhimeiritongji_gei_shangji_cishu_and_more.py diff --git a/shangpin/migrations/__init__.py b/shop/migrations/__init__.py similarity index 100% rename from shangpin/migrations/__init__.py rename to shop/migrations/__init__.py diff --git a/shangdian/models.py b/shop/models.py similarity index 100% rename from shangdian/models.py rename to shop/models.py diff --git a/shangpin/tests.py b/shop/tests.py similarity index 100% rename from shangpin/tests.py rename to shop/tests.py diff --git a/shangdian/urls.py b/shop/urls.py similarity index 94% rename from shangdian/urls.py rename to shop/urls.py index 0bca675..48ff274 100644 --- a/shangdian/urls.py +++ b/shop/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from shangdian.views import BindDianpuView, ShopGoodsView, ShangdianLoginView, ShopOverviewView, ShopQrcodeView, \ +from .views import BindDianpuView, ShopGoodsView, ShangdianLoginView, ShopOverviewView, ShopQrcodeView, \ ShopProductConfigView, ShopProductModifyView,ShopProductListView, ShopProductModifyView1,\ MemberListView,DianpuShouzhiTongjiView, QrcodeDownloadView, SubShopListView, SubShopModifyView,\ ShopOrderManageView,ShopOrderStatisticsView, ShopRefundView, ShopForceCompleteView,\ diff --git a/shangdian/utils.py b/shop/utils.py similarity index 99% rename from shangdian/utils.py rename to shop/utils.py index a5a0467..a4ab3bc 100644 --- a/shangdian/utils.py +++ b/shop/utils.py @@ -4,8 +4,8 @@ from rest_framework.response import Response from .models import Dianpu, YonghuPingzheng -from shangpin.models import Shangpin -from shangdian.models import Dianpu, DianpuMorenPeizhi +from products.models import Shangpin +from .models import Dianpu, DianpuMorenPeizhi # shangdian/utils.py 或 tongji/utils.py @@ -14,7 +14,7 @@ from datetime import date from decimal import Decimal from django.db import transaction from django.db.models import F -from shangdian.models import Dianpu, DianpuShouzhiMeiriTongji +from .models import Dianpu, DianpuShouzhiMeiriTongji logger = logging.getLogger(__name__) diff --git a/shangdian/views.py b/shop/views.py similarity index 97% rename from shangdian/views.py rename to shop/views.py index f07b496..3673e96 100644 --- a/shangdian/views.py +++ b/shop/views.py @@ -1,38 +1,60 @@ - -from django.db import models - -from utils.oss_utils import upload_to_oss, delete_from_oss -from yonghu.models import UserMain, UserDashou, UserShangjia, UserGuanshi, UserZuzhang -from shangdian.models import Dianpu, YonghuDianpuBangding, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi -from shangpin.models import ShangpinLeixing, ShangpinZhuanqu, Shangpin - - -from rest_framework.permissions import AllowAny - -from django.utils import timezone -from rest_framework_simplejwt.tokens import RefreshToken - - -from yonghu.models import UserMain -from .models import YonghuPingzheng, Dianpu - - -import logging - -# shangdian/views.py 继续添加 +# ==================== 标准库 ==================== import io +import json import time +import random +import string +import hmac +import hashlib import requests +import logging +import traceback +import calendar +import threading +import xmltodict +from decimal import Decimal +from collections import defaultdict +from concurrent.futures import ThreadPoolExecutor, as_completed + from django.conf import settings +from django.db import models, transaction, IntegrityError +from django.db.models import F, Sum, Count, Q, Prefetch from django.core.cache import cache -from django.db import transaction +from django.core.paginator import Paginator, EmptyPage +from django.core.exceptions import ObjectDoesNotExist +from django.utils import timezone +from django.http import HttpResponse + from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework import status +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework_simplejwt.tokens import RefreshToken + +from utils.oss_utils import upload_to_oss, delete_from_oss, get_oss_client + from .utils import verify_shop_permission -from .models import Dianpu -from django.db.models import F +from shop.utils import verify_shop_permission, update_dianpu_daily_stat +from products.utils import update_shangpin_daily_stat +from orders.utils import update_daily_payout +from backend.utils import update_dashou_daily_by_action + +from .models import YonghuPingzheng, Dianpu, DianpuShouzhiMeiriTongji + +from users.models import UserMain, UserDashou, UserShangjia, UserGuanshi, UserZuzhang + +from shop.models import YonghuDianpuBangding, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi + +from products.models import ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan + +from orders.models import ( + Dingdan, DingdanPingtai, Dashoutupian, Tuikuanjilu, + OrderDashouHistory, CrossPlatformOrderData, Fadan +) + +from config.models import ClubConfig, Club + + logger = logging.getLogger(__name__) @@ -438,10 +460,6 @@ class ShopOverviewView(APIView): -# 新增导入 -from django.http import HttpResponse -from utils.oss_utils import get_oss_client # 使用您已有的 COS 客户端获取方法 - class QrcodeDownloadView(APIView): """ 店铺二维码下载接口(通过后端代理下载,解决跨域限制) @@ -492,10 +510,6 @@ class QrcodeDownloadView(APIView): -# shangdian/views.py 追加 -from django.db.models import Sum - -from shangdian.models import DianpuShouzhiMeiriTongji class DianpuShouzhiTongjiView(APIView): """ @@ -542,7 +556,6 @@ class DianpuShouzhiTongjiView(APIView): ).order_by('ri') # 补全缺失的日期(默认值为0) - import calendar days_in_month = calendar.monthrange(year, month)[1] result = [] for day in range(1, days_in_month + 1): @@ -758,10 +771,6 @@ class ShopQrcodeView(APIView): -from django.db.models import Count -from rest_framework.views import APIView - -from shangpin.models import ShangpinLeixing class ShopProductConfigView(APIView): @@ -875,8 +884,6 @@ class ShopProductConfigView(APIView): }) -from django.db import transaction -from utils.oss_utils import delete_from_oss # 引入文件删除工具 class ShopProductModifyView(APIView): @@ -1222,7 +1229,7 @@ class ShopProductModifyView(APIView): return Response({'code': 500, 'msg': '删除失败,请稍后重试'}) -from django.core.paginator import Paginator + class ShopProductListView(APIView): """ 商品列表接口,支持筛选、分页。 @@ -1718,7 +1725,7 @@ class ShopProductModifyView1(APIView): return Response({'code': 0, 'msg': '商品已删除'}) -from shangpin.models import Huiyuan # 会员模型,根据实际路径调整 + class MemberListView(APIView): """ 获取所有会员列表(供商品发布时选择) @@ -1763,12 +1770,6 @@ class MemberListView(APIView): - -from django.db import transaction, IntegrityError -from django.core.paginator import Paginator, EmptyPage - - - class SubShopListView(APIView): """ 获取下级店铺列表,同时返回上级分红汇总 @@ -2019,16 +2020,6 @@ class SubShopModifyView(APIView): 2. 订单列表:支持筛选、分页、返回订单详情及打手图片(含跨平台图片拉取) """ -from concurrent.futures import ThreadPoolExecutor, as_completed - -from django.db.models import Count, Q, Prefetch - -from django.core.paginator import Paginator, EmptyPage - # 已存在的验证函数 -from dingdan.models import Dingdan, DingdanPingtai, Dashoutupian -from peizhi.models import ClubConfig, Club # 跨平台相关模型,按实际路径调整 - -from collections import defaultdict # 如果有 ClubConfig 模型用于获取存储桶域名,确保导入(如没有可忽略,使用 settings 中的 OSS 域名) @@ -2194,6 +2185,7 @@ class ShopOrderStatisticsView(APIView): 'code': 500, 'msg': '统计失败,服务器内部错误' }) + # ================================================================ # 辅助函数:获取OSS域名(用于拼接本地图片绝对URL) # ================================================================ @@ -2542,28 +2534,6 @@ class ShopOrderManageView(APIView): # shangdian/views/refund_view.py # 店铺退款接口 —— 基于跨平台退款接口二改,只处理平台订单,我方派单 # ================================================================ -import json -import time -import random -import string -import hashlib -import hmac -import logging -import traceback -from decimal import Decimal -import xmltodict # 解析微信退款回执 -import requests -from django.db import transaction -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response - -from shangdian.utils import verify_shop_permission # 店铺身份验证 -from dingdan.models import Dingdan, DingdanPingtai, Dashoutupian, Tuikuanjilu -from yonghu.models import UserMain -from shangdian.models import Dianpu logger = logging.getLogger(__name__) @@ -2647,7 +2617,6 @@ class ShopRefundView(APIView): # 打手每日统计(退款动作) try: - from houtai.utils import update_dashou_daily_by_action update_dashou_daily_by_action( yonghuid=dashou_id, amount=order.dashou_fencheng or Decimal('0.00'), @@ -2658,15 +2627,12 @@ class ShopRefundView(APIView): # 更新平台支出记录 try: - from dingdan.utils import update_daily_payout update_daily_payout(order.jine) except Exception as e: logger.error(f"更新每日支出失败: {e}") # ========== 新增:商品和店铺每日统计 ========== try: - from shangdian.utils import update_dianpu_daily_stat - from shangpin.utils import update_shangpin_daily_stat # 商品每日统计(只要有商品ID就执行) if order.shangpin_id: # 从扩展表获取已计算好的收益值(下单时已存储) @@ -2808,7 +2774,6 @@ class ShopRefundView(APIView): 此方法执行不影响主流程,任何异常仅记录日志。 """ try: - from dingdan.models import ClubConfig, Club, CrossPlatformOrderData my_config = ClubConfig.objects.filter(is_self=1).first() if not my_config: logger.error("未配置我方俱乐部信息,无法通知对方") @@ -2987,7 +2952,6 @@ class ShopForceCompleteView(APIView): # ---------- 打手每日统计(成交) ---------- try: - from houtai.utils import update_dashou_daily_by_action update_dashou_daily_by_action( yonghuid=dashou_id, amount=dashou_fencheng, @@ -3001,7 +2965,6 @@ class ShopForceCompleteView(APIView): # 前提:订单创建时已存储了 dianpu_shouyi 到扩展表 if pingtai_ext.dianpu_shouyi: try: - from shangpin.utils import update_shangpin_daily_stat # 商品每日统计(只要有商品ID就执行) if order.shangpin_id: # 从扩展表获取已计算好的收益值(下单时已存储) @@ -3019,7 +2982,6 @@ class ShopForceCompleteView(APIView): dianpu_zongshouyi=dianpu_shouyi ) logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") - from shangdian.utils import update_dianpu_daily_stat update_dianpu_daily_stat( @@ -3061,7 +3023,6 @@ class ShopForceCompleteView(APIView): partner_domain = club_rel.partner_domain.rstrip('/') notify_url = f"{partner_domain}/houtai/dfddtk" - import requests payload = { 'order_id': partner_order_id, 'club_id': self_club_id, @@ -3091,17 +3052,6 @@ class ShopForceCompleteView(APIView): # shangdian/views/transfer_hall_view.py # 店铺转移大厅接口 —— 添加本地打手退款统计 # ================================================================ -import logging -import threading - -from django.core.exceptions import ObjectDoesNotExist - -from dingdan.models import Dingdan, DingdanPingtai, OrderDashouHistory - -from houtai.utils import update_dashou_daily_by_action # 打手每日统计(退款用) - - - class ShopTransferHallView(APIView): """ @@ -3215,7 +3165,6 @@ class ShopTransferHallView(APIView): # 获取我方俱乐部ID my_club_config = None try: - from peizhi.models import ClubConfig my_club_config = ClubConfig.objects.filter(is_self=1).first() except Exception as e: logger.error(f"获取俱乐部配置失败: {e}") @@ -3224,8 +3173,6 @@ class ShopTransferHallView(APIView): self_club_id = my_club_config.club_id def notify(): try: - from dingdan.models import CrossPlatformOrderData - from peizhi.models import Club # 确保导入 club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) partner_domain = club_rel.partner_domain.rstrip('/') notify_url = f"{partner_domain}/houtai/kpttzzydt" @@ -3284,10 +3231,6 @@ class ShopTransferHallView(APIView): # shangdian/views/fine_apply_view.py # 店铺罚款申请接口 —— 二改自 FineApplyView(客服罚款) # ================================================================ - -from dingdan.models import Dingdan, DingdanPingtai, Fadan # 订单、罚款记录 - - class ShopFineApplyView(APIView): """ 店铺罚款申请接口 diff --git a/yonghu/__init__.py b/users/__init__.py similarity index 100% rename from yonghu/__init__.py rename to users/__init__.py diff --git a/yonghu/admin.py b/users/admin.py similarity index 100% rename from yonghu/admin.py rename to users/admin.py diff --git a/yonghu/apps.py b/users/apps.py similarity index 100% rename from yonghu/apps.py rename to users/apps.py diff --git a/yonghu/fadan_fenhong_utils.py b/users/fadan_fenhong_utils.py similarity index 98% rename from yonghu/fadan_fenhong_utils.py rename to users/fadan_fenhong_utils.py index c816b50..ddc4352 100644 --- a/yonghu/fadan_fenhong_utils.py +++ b/users/fadan_fenhong_utils.py @@ -7,8 +7,8 @@ from decimal import Decimal from django.db import transaction from django.core.exceptions import ObjectDoesNotExist -from dingdan.models import Fadan, FadanFenhong, FadanFenhongLilv -from yonghu.models import UserShangjia +from orders.models import Fadan, FadanFenhong, FadanFenhongLilv +from users.models import UserShangjia logger = logging.getLogger(__name__) diff --git a/yonghu/migrations/0001_initial.py b/users/migrations/0001_initial.py similarity index 100% rename from yonghu/migrations/0001_initial.py rename to users/migrations/0001_initial.py diff --git a/yonghu/migrations/0002_adminprofile_yaoqingma.py b/users/migrations/0002_adminprofile_yaoqingma.py similarity index 100% rename from yonghu/migrations/0002_adminprofile_yaoqingma.py rename to users/migrations/0002_adminprofile_yaoqingma.py diff --git a/yonghu/migrations/0003_rankingrecord.py b/users/migrations/0003_rankingrecord.py similarity index 100% rename from yonghu/migrations/0003_rankingrecord.py rename to users/migrations/0003_rankingrecord.py diff --git a/yonghu/migrations/0004_alter_usermain_avatar.py b/users/migrations/0004_alter_usermain_avatar.py similarity index 100% rename from yonghu/migrations/0004_alter_usermain_avatar.py rename to users/migrations/0004_alter_usermain_avatar.py diff --git a/yonghu/migrations/0005_officialaccountuser_usermain_unionid.py b/users/migrations/0005_officialaccountuser_usermain_unionid.py similarity index 100% rename from yonghu/migrations/0005_officialaccountuser_usermain_unionid.py rename to users/migrations/0005_officialaccountuser_usermain_unionid.py diff --git a/yonghu/migrations/0006_alter_officialaccountuser_unionid.py b/users/migrations/0006_alter_officialaccountuser_unionid.py similarity index 100% rename from yonghu/migrations/0006_alter_officialaccountuser_unionid.py rename to users/migrations/0006_alter_officialaccountuser_unionid.py diff --git a/yonghu/migrations/0007_userkefu.py b/users/migrations/0007_userkefu.py similarity index 100% rename from yonghu/migrations/0007_userkefu.py rename to users/migrations/0007_userkefu.py diff --git a/yonghu/migrations/0008_userguanshi_fenghong_erci_and_more.py b/users/migrations/0008_userguanshi_fenghong_erci_and_more.py similarity index 100% rename from yonghu/migrations/0008_userguanshi_fenghong_erci_and_more.py rename to users/migrations/0008_userguanshi_fenghong_erci_and_more.py diff --git a/yonghu/migrations/0009_userdashou_ewai_xiane_userdashou_jinritixian_jine_and_more.py b/users/migrations/0009_userdashou_ewai_xiane_userdashou_jinritixian_jine_and_more.py similarity index 100% rename from yonghu/migrations/0009_userdashou_ewai_xiane_userdashou_jinritixian_jine_and_more.py rename to users/migrations/0009_userdashou_ewai_xiane_userdashou_jinritixian_jine_and_more.py diff --git a/yonghu/migrations/0010_userguanshi_invite_qrcode_url.py b/users/migrations/0010_userguanshi_invite_qrcode_url.py similarity index 100% rename from yonghu/migrations/0010_userguanshi_invite_qrcode_url.py rename to users/migrations/0010_userguanshi_invite_qrcode_url.py diff --git a/yonghu/migrations/0011_usermain_shoujihao_renzheng.py b/users/migrations/0011_usermain_shoujihao_renzheng.py similarity index 100% rename from yonghu/migrations/0011_usermain_shoujihao_renzheng.py rename to users/migrations/0011_usermain_shoujihao_renzheng.py diff --git a/yonghu/migrations/0012_usershenheguan.py b/users/migrations/0012_usershenheguan.py similarity index 100% rename from yonghu/migrations/0012_usershenheguan.py rename to users/migrations/0012_usershenheguan.py diff --git a/yonghu/migrations/0013_usershenheguan_is_renzheng.py b/users/migrations/0013_usershenheguan_is_renzheng.py similarity index 100% rename from yonghu/migrations/0013_usershenheguan_is_renzheng.py rename to users/migrations/0013_usershenheguan_is_renzheng.py diff --git a/yonghu/migrations/0014_tixianautorecord_tixianshenhejilu_and_more.py b/users/migrations/0014_tixianautorecord_tixianshenhejilu_and_more.py similarity index 100% rename from yonghu/migrations/0014_tixianautorecord_tixianshenhejilu_and_more.py rename to users/migrations/0014_tixianautorecord_tixianshenhejilu_and_more.py diff --git a/yonghu/migrations/__init__.py b/users/migrations/__init__.py similarity index 100% rename from yonghu/migrations/__init__.py rename to users/migrations/__init__.py diff --git a/yonghu/models.py b/users/models.py similarity index 100% rename from yonghu/models.py rename to users/models.py diff --git a/yonghu/paihang_views.py b/users/paihang_views.py similarity index 98% rename from yonghu/paihang_views.py rename to users/paihang_views.py index 6b686a7..d24ea42 100644 --- a/yonghu/paihang_views.py +++ b/users/paihang_views.py @@ -11,13 +11,13 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from houtai.models import ( +from backend.models import ( DashouRiTongji, GuanshiRiTongji, ZuzhangRiTongji, ShangjiaRiTongji, ) -from yonghu.models import UserMain, UserDashou, UserShangjia, UserBoss +from users.models import UserMain, UserDashou, UserShangjia, UserBoss logger = logging.getLogger(__name__) diff --git a/yonghu/ranking_tasks.py b/users/ranking_tasks.py similarity index 99% rename from yonghu/ranking_tasks.py rename to users/ranking_tasks.py index 4614f9f..86d29d0 100644 --- a/yonghu/ranking_tasks.py +++ b/users/ranking_tasks.py @@ -11,7 +11,7 @@ from datetime import datetime, timedelta import logging from decimal import Decimal -from yonghu.models import ( +from users.models import ( UserMain, UserDashou, UserShangjia, UserGuanshi, UserBoss, RankingRecord ) diff --git a/yonghu/tasks.py b/users/tasks.py similarity index 98% rename from yonghu/tasks.py rename to users/tasks.py index ce07131..b46ac27 100644 --- a/yonghu/tasks.py +++ b/users/tasks.py @@ -12,7 +12,7 @@ from datetime import datetime import logging # 导入模型 -from yonghu.models import UserDashou, UserShangjia, UserGuanshi +from users.models import UserDashou, UserShangjia, UserGuanshi from utils.celery_utils import log_task_execution, rollback_on_failure logger = logging.getLogger(__name__) diff --git a/yonghu/tests.py b/users/tests.py similarity index 100% rename from yonghu/tests.py rename to users/tests.py diff --git a/yonghu/tixian_shenhe_services.py b/users/tixian_shenhe_services.py similarity index 99% rename from yonghu/tixian_shenhe_services.py rename to users/tixian_shenhe_services.py index 1b0e5cd..9ce1d7c 100644 --- a/yonghu/tixian_shenhe_services.py +++ b/users/tixian_shenhe_services.py @@ -25,11 +25,16 @@ from django.conf import settings from django.db import transaction from django.utils import timezone -from dingdan.models import Dingdan, DingdanShangjia, Fadan, Lilubiao -from peizhi.models import Szjilu, TixianQuotaDefault +from orders.models import Dingdan, DingdanShangjia, Fadan, Lilubiao +from config.models import Szjilu, TixianQuotaDefault from utils.fadan_utils import check_fadan_qiangdan_eligible +from products.models import Huiyuangoumai +from backend.models import TixianRiTongji + +from utils.wechat_v3 import build_authorization + from .models import ( TixianAutoRecord, TixianShenheJilu, @@ -42,6 +47,10 @@ from .models import ( UserZuzhang, ) +from orders.utils import update_daily_payout + +from django.db.models import Sum + logger = logging.getLogger('yonghu.tixian_shenhe') ORDER_STATUS_COMPLETED = 3 @@ -159,7 +168,6 @@ def check_dashou_has_valid_huiyuan(yonghuid): 打手至少持有一个未过期会员(Huiyuangoumai + jiance_shifou_daoqi 同步状态) 返回 (True, '') 或 (False, 原因) """ - from shangpin.models import Huiyuangoumai records = Huiyuangoumai.objects.filter(yonghu_id=yonghuid) if not records.exists(): @@ -423,8 +431,6 @@ def _get_active_auto_record(shenhe_danhao): def _get_platform_daily_total(leixing, today=None): """平台今日已提现总额:只读 houtai.TixianRiTongji(到账金额口径)""" - from houtai.models import TixianRiTongji - if today is None: today = date.today() try: @@ -559,7 +565,6 @@ def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe 本审核单已有进行中/成功打款记录则视为已预占,不重复加计。 返回 (True, '', '') 或 (False, msg, 'personal'|'platform') """ - from houtai.models import TixianRiTongji today = date.today() shijidaozhang = shijidaozhang or decimal.Decimal('0.00') @@ -637,8 +642,6 @@ def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe def release_collect_quota_reservation(user_main, leixing, jine, shijidaozhang): """微信打款明确失败/用户取消时回滚收款预占的限额""" - from houtai.models import TixianRiTongji - today = date.today() shijidaozhang = shijidaozhang or decimal.Decimal('0.00') try: @@ -706,7 +709,6 @@ def _quota_already_reserved_for_record(auto_record): """判断本笔打款是否已在收款预占时写入日统计(避免成功时重复累加)""" if not auto_record.shenhe_danhao: return False - from django.db.models import Sum today = date.today() shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00') @@ -730,9 +732,6 @@ def apply_transfer_success_quota(auto_record): """ if not auto_record.shenhe_danhao: return - - from houtai.models import TixianRiTongji - today = date.today() leixing = auto_record.leixing jine = auto_record.jine @@ -917,7 +916,6 @@ def query_wechat_transfer_bill(out_bill_no): GET /v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no} 返回 dict;网络/系统异常返回 None;微信无此单返回 {'_not_found': True} """ - from utils.wechat_v3 import build_authorization url = ( 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/' @@ -961,7 +959,6 @@ def _parse_package_info(raw): def _apply_platform_accounting(auto_record): """平台收支统计(幂等由 mark_transfer_success 外层保证只调一次)""" - from dingdan.utils import update_daily_payout update_daily_payout(auto_record.shijidaozhang) try: diff --git a/yonghu/tixian_shenhe_views.py b/users/tixian_shenhe_views.py similarity index 98% rename from yonghu/tixian_shenhe_views.py rename to users/tixian_shenhe_views.py index 6e1b8a1..2fd8c31 100644 --- a/yonghu/tixian_shenhe_views.py +++ b/users/tixian_shenhe_views.py @@ -19,7 +19,10 @@ from rest_framework import permissions from rest_framework.response import Response from rest_framework.views import APIView -from .models import TixianAutoRecord, TixianShenheJilu +from utils.redis_lock import acquire_lock, release_lock +from utils.wechat_v3 import build_authorization + +from .models import TixianAutoRecord from .tixian_shenhe_services import ( RECONCILE_ALLOW_NEW, RECONCILE_COMPLETED, @@ -113,8 +116,6 @@ def _build_wx_transfer_body(wx_cfg, tixian_id, openid, yonghuid, leixing, shijid def _call_wechat_transfer(body, wx_cfg): - from utils.wechat_v3 import build_authorization - url = 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills' body_json = json.dumps(body, separators=(',', ':')) auth = build_authorization('POST', url, body_json) @@ -137,8 +138,6 @@ class TixianZddkshApplyView(APIView): permission_classes = [permissions.IsAuthenticated] def post(self, request): - from utils.redis_lock import acquire_lock, release_lock - user_main = request.user lock_key = f'tixian_audit_apply:{user_main.yonghuid}' identifier = acquire_lock(lock_key, timeout=10) @@ -184,8 +183,6 @@ def process_audit_collect(request): 若 TixianAutoRecord 已有 tixian_id → 只问微信,非终态绝不新建 仅微信 FAIL/CANCELLED 终态后才允许新建打款记录 """ - from utils.redis_lock import acquire_lock, release_lock - user_main = request.user yonghuid = user_main.yonghuid tixianjilu_id = request.data.get('tixianjilu_id') or request.data.get('id') diff --git a/yonghu/urls.py b/users/urls.py similarity index 100% rename from yonghu/urls.py rename to users/urls.py diff --git a/yonghu/views.py b/users/views.py similarity index 96% rename from yonghu/views.py rename to users/views.py index b626da0..4c45ee3 100644 --- a/yonghu/views.py +++ b/users/views.py @@ -1,13558 +1,12885 @@ -# yonghu/views.py -import time -import random -import hashlib -import requests -from django.db import transaction -from django.conf import settings -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework_simplejwt.tokens import RefreshToken - -from .models import UserMain, UserBoss, UserDashou, UserShangjia, UserGuanshi -from dingdan.models import Dingdan, DingdanPingtai -from peizhi.models import Qunpeizhi # 假设群配置表在这个模块 -from rest_framework.throttling import AnonRateThrottle -from rest_framework.permissions import AllowAny # 新增这行 - - - -from houtai.utils import update_dashou_daily_by_action - - - - - -from django.conf import settings -from django.utils import timezone -from django.db import transaction -from django.core.exceptions import ObjectDoesNotExist -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.throttling import AnonRateThrottle -from rest_framework.permissions import AllowAny -from rest_framework_simplejwt.tokens import RefreshToken -from .models import UserMain, AdminProfile - - - - - -# 获取用户代理信息(用于更详细的追踪) -def huoquYonghuDaili(request): - """ - 获取用户浏览器/设备信息 - """ - user_agent = request.META.get('HTTP_USER_AGENT', '') - - # 这里可以添加更复杂的用户代理解析 - # 为简化,我们只返回原始字符串的前100个字符 - return user_agent[:100] if user_agent else '' - - - - - - -import ipaddress - -def huoquKehuduanIP(request): - """ - 强化版:安全、兼容地获取客户端真实IP地址 - """ - ip = None - - # 1. 从标准头部获取IP链 - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - # 按逗号分割,取第一个(即最原始的客户端IP) - ip = x_forwarded_for.split(',')[0].strip() - - # 2. 如果上面没有,则用Nginx传递的X-Real-IP - if not ip: - ip = request.META.get('HTTP_X_REAL_IP') - - # 3. 最后降级到连接地址 - if not ip: - ip = request.META.get('REMOTE_ADDR') - - # ===== 核心:清洗和验证IP格式 ===== - if ip: - # 情况A: 处理IPv4映射的IPv6地址 (::ffff:192.168.1.1) - if ip.startswith('::ffff:'): - ip = ip[7:] - - # 情况B: 处理带端口的IPv4地址 (223.91.110.123:62417) - # 这是导致您记录“三段”的最可能原因! - # 如果IP字符串中有冒号,且最后一个冒号前的部分是IPv4,则分离端口。 - if ':' in ip and '.' in ip: - # 尝试按冒号分割,取第一部分 - possible_ip = ip.rsplit(':', 1)[0] # 从右边分割一次 - # 验证它是否是有效的IPv4地址 - try: - ipaddress.IPv4Address(possible_ip) # 如果通过验证,说明格式正确 - ip = possible_ip - except (ipaddress.AddressValueError, ValueError): - # 如果验证失败,说明可能不是IPv4带端口,保持原样或进行其他处理 - pass - - # 情况C: 验证最终结果是否为有效IPv4(可选,但推荐) - try: - # 此步骤会抛出异常,如果ip变量此时仍是“223.91.110.”这种格式 - ipaddress.IPv4Address(ip) - except (ipaddress.AddressValueError, ValueError): - # 记录一个警告日志,并将IP标记为无效或存为原始字符串 - print(f"警告:获取到无效或非标准IP地址格式: {ip}") - # 您可以选择在此处 return None 或存储原始字符串以供排查 - - return ip - - - - - - -from yonghu.models import UserShenheguan - -class WechatMiniProgramLoginView(APIView): - """ - 微信小程序登录接口 - """ - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - - def post(self, request): - try: - code = request.data.get('code', '').strip() - if not code: - return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 获取微信openid和unionid - wechat_data = self.get_wechat_openid(code) - if not wechat_data or 'openid' not in wechat_data: - error_msg = wechat_data.get('errmsg', '微信授权失败') - return Response({'code': 2, 'msg': f'微信登录失败: {error_msg}', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - openid = wechat_data['openid'] - session_key = wechat_data.get('session_key', '') - unionid = wechat_data.get('unionid', '') # 🆕【新增】获取unionid - - # 获取客户端真实IP - kehuduan_ip = self.huoquKehuduanIP(request) - - with transaction.atomic(): - # 创建或获取用户 - user_main, created = UserMain.objects.select_for_update().get_or_create( - openid=openid, - defaults={'yonghuid': self.shengchengYonghuID(), 'user_type': 'normal'} - ) - - # 🆕【新增】保存unionid - if unionid and unionid != user_main.unionid: - user_main.unionid = unionid - - cunchu_ip = kehuduan_ip - user_main.ip = cunchu_ip - user_main.last_login_time = timezone.now() - user_main.save() - - if created: - UserBoss.objects.create(user=user_main, nickname='板板大人') - - # 检查扩展表 - dashou_status = 0 - shangjia_status = 0 - guanshi_status = 0 - boss_nickname = '微信用户' - - try: - boss_profile = UserBoss.objects.get(user=user_main) - boss_nickname = boss_profile.nickname if boss_profile.nickname else '微信用户' - except UserBoss.DoesNotExist: - pass - - try: - UserDashou.objects.get(user=user_main) - dashou_status = 1 - except UserDashou.DoesNotExist: - dashou_status = None - - try: - UserShangjia.objects.get(user=user_main) - shangjia_status = 1 - except UserShangjia.DoesNotExist: - shangjia_status = None - - try: - UserGuanshi.objects.get(user=user_main) - guanshi_status = 1 - except UserGuanshi.DoesNotExist: - guanshi_status = None - - - try: - UserZuzhang.objects.get(user=user_main) - zuzhang_status = 1 - except UserZuzhang.DoesNotExist: - zuzhang_status = None - - - # 在检查 zuzhang 扩展表之后,插入以下代码 - try: - kg = UserShenheguan.objects.get(user=user_main) - kaoheguan_status = 1 if kg.zhuangtai == 1 else 0 - except UserShenheguan.DoesNotExist: - kaoheguan_status = 0 - - # 生成JWT token - refresh = RefreshToken.for_user(user_main) - token = str(refresh.access_token) - - # 计算订单数量 - order_counts = self.jisuanDingdanShuliang(user_main.yonghuid) - - # 获取群配置 - group_info = self.huoquQunPeizhi() - - # 返回数据 - response_data = { - 'token': token, - 'nicheng': boss_nickname, - 'uid': user_main.yonghuid, - 'touxiang': user_main.avatar or '', - 'shangjiastatus': shangjia_status, - 'dashoustatus': dashou_status, - 'guanshistatus': guanshi_status, - 'zuzhangstatus': zuzhang_status, - 'kaoheguanstatus': kaoheguan_status, - 'dingdantiaoshu': order_counts, - 'dashouqun': group_info.get('dashouqun', ''), - 'dashouqunid': group_info.get('dashouqunid', ''), - 'guanshiqun': group_info.get('guanshiqun', ''), - 'guanshiqunid': group_info.get('guanshiqunid', '') - } - - return Response({'code': 0, 'msg': '登录成功', 'data': response_data}) - - except Exception as e: - import logging - logger = logging.getLogger(__name__) - error_openid = locals().get('openid', 'N/A') - logger.error(f"微信登录异常 - openid: {error_openid}, 错误类型: {type(e).__name__}, 错误详情: {str(e)}", - exc_info=True) - - return Response({'code': 99, 'msg': '系统繁忙,请稍后重试', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def get_wechat_openid(self, code): - """ - 调用微信接口获取openid和unionid - """ - try: - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') - - if not appid or not secret: - raise ValueError('微信配置未设置') - - url = 'https://api.weixin.qq.com/sns/jscode2session' - params = {'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code'} - - response = requests.get(url, params=params, timeout=10) - result = response.json() - - if 'openid' in result: - return {'openid': result['openid'], 'session_key': result.get('session_key', ''), - 'unionid': result.get('unionid', '')} - else: - errcode = result.get('errcode', 'unknown') - errmsg = result.get('errmsg', '未知错误') - return {'errmsg': f'[{errcode}]{errmsg}'} - - except requests.exceptions.Timeout: - return {'errmsg': '请求微信接口超时'} - except Exception as e: - return {'errmsg': f'请求微信接口异常: {str(e)}'} - - def shengchengYonghuID(self): - """ - 生成7位数字用户ID - """ - for _ in range(10): - timestamp_part = str(int(time.time()))[-5:].zfill(5) - random_part = str(random.randint(0, 99)).zfill(2) - user_id = timestamp_part + random_part - - if len(user_id) == 7 and user_id.isdigit(): - if not UserMain.objects.filter(yonghuid=user_id).exists(): - return user_id - - raise Exception('生成用户ID失败') - - def huoquKehuduanIP(self, request): - """ - 获取客户端真实IP地址 - """ - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0].strip() - if ip: - return ip - - ip = request.META.get('REMOTE_ADDR', '') - return ip if ip else '0.0.0.0' - - def jisuanDingdanShuliang(self, yonghuid): - """ - 统计订单数量 - """ - try: - if not DingdanPingtai.objects.filter(laoban_id=yonghuid).exists(): - return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} - - dingdan_ids = DingdanPingtai.objects.filter(laoban_id=yonghuid).values_list('dingdan__id', flat=True) - - from django.db.models import Q, Count - from django.db.models import Case, When, IntegerField - - results = Dingdan.objects.filter(id__in=dingdan_ids).aggregate( - daifuwu=Count(Case(When(Q(zhuangtai=1) | Q(zhuangtai=7), then=1), output_field=IntegerField())), - fuwuzhong=Count(Case(When(zhuangtai=2, then=1), output_field=IntegerField())), - yiwancheng=Count(Case(When(zhuangtai=3, then=1), output_field=IntegerField())), - yituikuan=Count(Case(When(zhuangtai=5, then=1), output_field=IntegerField())) - ) - - return { - 'daifuwu': results['daifuwu'] or 0, - 'fuwuzhong': results['fuwuzhong'] or 0, - 'yiwancheng': results['yiwancheng'] or 0, - 'yituikuan': results['yituikuan'] or 0 - } - - except Exception: - return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} - - def huoquQunPeizhi(self): - """ - 获取群配置信息 - """ - try: - qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) - - result = { - 'dashouqun': '', - 'dashouqunid': '', - 'guanshiqun': '', - 'guanshiqunid': '' - } - - for config in qun_configs: - if config.id == 1: - result['dashouqun'] = config.neirong or '' - result['dashouqunid'] = config.qunid or '' - elif config.id == 2: - result['guanshiqun'] = config.neirong or '' - result['guanshiqunid'] = config.qunid or '' - - return result - - except Exception: - return { - 'dashouqun': '', - 'dashouqunid': '', - 'guanshiqun': '', - 'guanshiqunid': '' - } - -# views/weixin_official.py - 完整文件 -import hashlib -import time -import logging -from django.views.decorators.csrf import csrf_exempt -from django.http import HttpResponse -from django.utils import timezone -import defusedxml.ElementTree as ET -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from django.conf import settings -from .models import OfficialAccountUser - -logger = logging.getLogger(__name__) - - - -class WeixinOfficialCallbackView(APIView): - """ - 微信服务号回调接口 - 处理用户关注、取消关注等事件 - 核心:有关注事件就存,不管有没有UnionID - """ - throttle_classes = [] - permission_classes = [AllowAny] - authentication_classes = [] - - def get(self, request): - """验证服务器配置(微信官方要求)""" - try: - signature = request.GET.get('signature', '') - timestamp = request.GET.get('timestamp', '') - nonce = request.GET.get('nonce', '') - echostr = request.GET.get('echostr', '') - - token = getattr(settings, 'WEIXIN_OFFICIAL_TOKEN', '') - - if not token: - logger.error("服务号Token未配置") - return HttpResponse('Token not configured', status=500) - - # 验证签名 - tmp_list = sorted([token, timestamp, nonce]) - tmp_str = ''.join(tmp_list) - hash_str = hashlib.sha1(tmp_str.encode()).hexdigest() - - if hash_str == signature: - return HttpResponse(echostr) - else: - logger.error(f"签名验证失败") - return HttpResponse('Signature verification failed', status=403) - - except Exception as e: - logger.error(f"验证服务器异常: {str(e)}") - return HttpResponse('Internal Server Error', status=500) - - def post(self, request): - """处理事件推送(关注、取关)""" - try: - xml_str = request.body.decode('utf-8') - logger.info(f"收到服务号事件,原始数据(前200字符): {xml_str[:200]}") - - root = ET.fromstring(xml_str) - - msg_type = root.find('MsgType').text - from_user = root.find('FromUserName').text # 服务号OpenID - - if msg_type == 'event': - event = root.find('Event').text - - if event == 'subscribe': - return self.handle_subscribe_event(root, from_user) - elif event == 'unsubscribe': - return self.handle_unsubscribe_event(root, from_user) - else: - # 其他事件(如菜单点击)直接返回success - return HttpResponse('success') - - # 非事件消息也直接返回success - return HttpResponse('success') - - except Exception as e: - logger.error(f"处理服务号事件异常: {str(e)}", exc_info=True) - # 即使异常也返回success,避免微信服务器重试 - return HttpResponse('success') - - def handle_subscribe_event(self, root, from_user): - """处理用户关注事件 - 简化版:有就存,没有拉倒""" - try: - # 尝试获取UnionID(有就有,没有也没关系) - unionid_elem = root.find('UnionId') - unionid = unionid_elem.text if unionid_elem is not None else None - - logger.info(f"用户关注,OpenID: {from_user}, UnionID: {unionid}") - - # 核心逻辑:更新或创建记录 - obj, created = OfficialAccountUser.objects.update_or_create( - official_openid=from_user, - defaults={ - 'unionid': unionid, - 'is_subscribed': True, - 'subscribe_time': timezone.now() - } - ) - - action = "新建" if created else "更新" - logger.info(f"{action}用户记录成功: OpenID={from_user}") - - # 回复欢迎消息(可选) - return self.generate_text_response(root, "感谢关注星阙网络服务!平台有新订单时您会收到通知。") - - except Exception as e: - logger.error(f"处理关注事件异常: {str(e)}", exc_info=True) - # 依然返回success,不让微信重试 - return HttpResponse('success') - - def handle_unsubscribe_event(self, root, from_user): - """处理用户取消关注事件 - 简化版:标记为未订阅""" - try: - updated_count = OfficialAccountUser.objects.filter( - official_openid=from_user - ).update(is_subscribed=False) - - if updated_count > 0: - logger.info(f"用户取消关注,已标记为未订阅: {from_user}") - else: - logger.info(f"用户取消关注,但记录不存在(无需处理): {from_user}") - - return HttpResponse('success') - - except Exception as e: - logger.error(f"处理取消关注事件异常: {str(e)}", exc_info=True) - return HttpResponse('success') - - def generate_text_response(self, root, content): - """生成文本消息响应""" - try: - to_user = root.find('ToUserName').text - from_user = root.find('FromUserName').text - - response_xml = f""" - - -{int(time.time())} - - -""" - - return HttpResponse(response_xml, content_type='application/xml') - - except Exception as e: - logger.error(f"生成回复消息异常: {str(e)}") - # 即使生成回复失败,也返回一个空的success - return HttpResponse('success') - - - - - - - - - - - - - - - - - - - -# yonghu/views.py -from rest_framework.parsers import MultiPartParser, FormParser,JSONParser -from rest_framework import permissions -from django.db import transaction -from rest_framework.response import Response -from rest_framework import status -from rest_framework.views import APIView -from .models import UserMain, UserBoss, UserDashou, UserShangjia, UserGuanshi -from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss -import os -import uuid - -import hashlib -from django.core.cache import cache - - - - - - - - -class UserInfoUpdateView(APIView): - permission_classes = [permissions.IsAuthenticated] - - # 进程内缓存,防止同一code被重复调用微信(单进程安全,多进程小概率重复,但用户极少可忽略) - _phone_cache = {} - - def post(self, request): - try: - user = request.user - boss = self._get_or_create_boss(user) - result = {} - - # 头像 - av = request.FILES.get('avatar') - if av: - ok, msg = validate_image(av) - if not ok: - return Response({'code': 1, 'msg': msg, 'data': None}, status=400) - url = self._upload_avatar(user, av) - if url: - result['touxiang'] = url - user.avatar = url - else: - return Response({'code': 2, 'msg': '头像上传失败', 'data': None}, status=500) - - # 昵称 - nick = request.data.get('nicheng') - if nick: - nick = str(nick).strip() - if len(nick) > 20: - return Response({'code': 3, 'msg': '昵称过长', 'data': None}, status=400) - boss.nickname = nick - result['nicheng'] = nick - - # 手机号 —— 极简可靠版 - code = request.data.get('shoujihao_code') - if code: - phone = self._get_phone_safe(code) - if phone is None: - return Response({'code': 6, 'msg': '手机号获取失败,请重新授权', 'data': None}, status=400) - user.phone = phone - user.shoujihao_renzheng = True - result['phone'] = phone - - if not result: - return Response({'code': 5, 'msg': '无更新', 'data': None}, status=400) - - with transaction.atomic(): - user.save() - boss.save() - return Response({'code': 0, 'msg': '更新成功', 'data': result}) - - except Exception as e: - logger.exception("系统异常") - return Response({'code': 99, 'msg': str(e), 'data': None}, status=500) - - # ---------- 安全手机号(内存去重) ---------- - def _get_phone_safe(self, code): - # 如果已经处理过这个code,直接返回结果 - if code in self._phone_cache: - cached = self._phone_cache[code] - return cached if cached != '__FAIL__' else None - - # 标记处理中(简单防重入) - self._phone_cache[code] = '__PROCESSING__' - try: - phone = self._call_wechat(code) - self._phone_cache[code] = phone - return phone - except Exception as e: - logger.error(f"微信调用失败: {str(e)}") - self._phone_cache[code] = '__FAIL__' - return None - - # ---------- 微信接口调用(每次全新token) ---------- - def _call_wechat(self, code): - token = self._get_new_token() - url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}' - resp = requests.post(url, json={'code': code}, timeout=10) - data = resp.json() - errcode = data.get('errcode') - if errcode == 0: - return data['phone_info']['purePhoneNumber'] - # 即使刚拿的token也可能意外失效,重试一次 - if errcode == 40001: - token = self._get_new_token() - url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}' - resp = requests.post(url, json={'code': code}, timeout=10) - data = resp.json() - if data.get('errcode') == 0: - return data['phone_info']['purePhoneNumber'] - raise Exception(f"微信错误(errcode={errcode}): {data.get('errmsg')}") - - # ---------- 获取全新 access_token(不缓存,每次调微信) ---------- - def _get_new_token(self): - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') - if not appid or not secret: - raise Exception('缺少微信配置') - resp = requests.get('https://api.weixin.qq.com/cgi-bin/token', params={ - 'grant_type': 'client_credential', - 'appid': appid, - 'secret': secret - }) - data = resp.json() - token = data.get('access_token') - if token: - return token - raise Exception(f"获取access_token失败: {data}") - - # ---------- 工具 ---------- - def _get_or_create_boss(self, user): - try: - return UserBoss.objects.get(user=user) - except UserBoss.DoesNotExist: - return UserBoss.objects.create(user=user, nickname='微信用户') - - def _upload_avatar(self, user, file): - try: - uid = user.yonghuid - if not uid: return None - old = user.avatar - if old: delete_from_oss(old) - ext = os.path.splitext(file.name)[1] or '.jpg' - name = f"{uuid.uuid4().hex}{ext}" - path = f"avatar/{uid}/{name}" - return path if upload_to_oss(file, path) else None - except: - return None - - - - - - - - -# yonghu/views.py 或 yonghu/api_views.py - - - -# yonghu/views.py 或 yonghu/api_views.py - -from django.utils import timezone -from django.shortcuts import get_object_or_404 -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework import status -from .models import UserMain, UserDashou -from shangpin.models import Huiyuangoumai, Gsfenhong # 需要导入会员模型 -import logging - - -class DashouXinxiAPIView(APIView): - """ - 获取打手信息接口 - 请求方式:POST - 认证方式:JWT Token - 返回:打手基本信息、资产信息、会员信息,金牌打手额外返回专属字段 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取当前登录用户 - current_user = request.user - yonghuid = current_user.yonghuid - - # 2. 查询打手扩展表 - try: - dashou_profile = current_user.dashou_profile - except UserDashou.DoesNotExist: - return Response({ - 'code': 400, - 'message': '您不是打手身份', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 构建基础信息响应(原有字段完全保留) - data = { - 'dashounicheng': dashou_profile.nicheng or '', - 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, - 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, - 'dashouzhuangtai': dashou_profile.zhuangtai, - 'yongjin': str(dashou_profile.yue) if dashou_profile.yue is not None else '0.00', - 'zonge': str(dashou_profile.zonge) if dashou_profile.zonge is not None else '0.00', - 'yajin': str(dashou_profile.yajin) if dashou_profile.yajin is not None else '0.00', - 'jiedanZongliang':dashou_profile.jiedanzongliang if dashou_profile.jiedanzongliang is not None else '0', - 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00', - 'chengjiaoliang': dashou_profile.chengjiaozongliang, - 'jifen': dashou_profile.jifen, - 'chenghao': dashou_profile.chenghao or '', - } - - # 4. 查询会员信息(原有逻辑) - huiyuan_list = [] - try: - huiyuan_records = Huiyuangoumai.objects.filter( - yonghu_id=yonghuid - ).select_related() - - for record in huiyuan_records: - # 检查会员是否过期 - is_expired = record.jiance_shifou_daoqi() - - # 只返回未过期的会员(huiyuan_zhuangtai = 1) - if record.huiyuan_zhuangtai == 1: - huiyuan_list.append({ - 'huiyuanid': record.huiyuan_id, - 'huiyuanming': record.jieshao or f"会员{record.huiyuan_id}", - 'daoqi': record.daoqi_time - }) - except Exception as e: - # 会员信息查询失败不影响主要功能,返回空列表 - huiyuan_list = [] - - data['clumber'] = huiyuan_list - - # 5. 如果是金牌打手,添加专属字段 - if dashou_profile.chenghao == "金牌打手": - data.update({ - 'jiedanZongliang': dashou_profile.jiedanzongliang, # 接单总量 - 'jinriJiedan': str(dashou_profile.jinrishouyi) if dashou_profile.jinrishouyi is not None else '0.00', # 今日接单金额 - 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00', # 最近提现金额(今日已提现) - }) - - # 6. 返回成功响应 - return Response({ - 'code': 200, - 'message': '获取成功', - 'data': data - }, status=status.HTTP_200_OK) - - except Exception as e: - # 生产环境应使用 logging 记录错误,而不是 print - # logger.error(f"获取打手信息失败: {e}", exc_info=True) - return Response({ - 'code': 500, - 'message': f'服务器内部错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -class HuoQuYaoQingRenView(APIView): - """ - 获取当前打手的邀请人信息 - POST /yonghu/hqwdyqr - Headers: Authorization: Bearer - 返回邀请人的用户ID、昵称(来自老板扩展表)、头像相对URL - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - current_user = request.user # 当前登录用户 - yonghuid = current_user.yonghuid # 7位用户ID - - # 1. 校验打手身份及账号状态(只检查账号状态,不检查接单状态) - try: - dashou = UserDashou.objects.get(user=current_user) - if dashou.zhanghaozhuangtai != 1: - return Response({ - 'code': 403, - 'message': '您的账号已被封禁,无法查看邀请人', - 'data': None - }) - except UserDashou.DoesNotExist: - return Response({ - 'code': 404, - 'message': '您还不是打手,无法查看邀请人', - 'data': None - }) - - # 2. 获取邀请人ID - yaoqingren_id = dashou.yaoqingren - if not yaoqingren_id: - return Response({ - 'code': 404, - 'message': '您没有邀请人', - 'data': None - }) - - # 3. 查询邀请人主表(头像) - try: - inviter_main = UserMain.objects.get(yonghuid=yaoqingren_id) - touxiang = inviter_main.avatar or '' - except UserMain.DoesNotExist: - return Response({ - 'code': 404, - 'message': '邀请人信息不存在', - 'data': None - }) - - # 4. 查询老板扩展表(昵称),如果没有则使用空字符串 - nicheng = '' - try: - boss = UserBoss.objects.get(user=inviter_main) - nicheng = boss.nickname or '' - except UserBoss.DoesNotExist: - pass # 邀请人可能不是老板,昵称留空 - - response_data = { - 'uid': yaoqingren_id, - 'nicheng': nicheng, - 'touxiang': touxiang # 相对路径,前端自行拼接 - } - - return Response({ - 'code': 200, - 'message': '获取成功', - 'data': response_data - }) - - except Exception as e: - logger.error(f'获取邀请人失败: {str(e)}', exc_info=True) - return Response({ - 'code': 500, - 'message': '系统繁忙,请稍后重试', - 'data': None - }) - - - - -# yonghu/views.py 或 yonghu/api_views.py - -class ZaixianZhuangtaiAPIView(APIView): - """ - 更改打手在线状态接口 - 请求方式:POST - 认证方式:JWT Token - 请求参数:status(0=离线,1=在线) - 返回:更新后的状态信息 - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取请求参数 - new_status = request.data.get('status') - - if new_status is None: - return Response({ - 'code': 400, - 'message': '缺少状态参数', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 转换为整数,确保是0或1 - try: - new_status = int(new_status) - if new_status not in [0, 1]: - return Response({ - 'code': 400, - 'message': '状态参数必须是0或1', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - except ValueError: - return Response({ - 'code': 400, - 'message': '状态参数必须是整数', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 获取当前登录用户 - current_user = request.user - yonghuid = current_user.yonghuid - - - - # 3. 查询打手扩展表 - try: - dashou_profile = current_user.dashou_profile - except UserDashou.DoesNotExist: - - return Response({ - 'code': 400, - 'message': '您不是打手身份', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 检查账号状态(是否被封禁) - if dashou_profile.zhanghaozhuangtai != 1: - - return Response({ - 'code': 403, - 'message': '账号状态异常,无法更改在线状态', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 5. 更新在线状态 - old_status = dashou_profile.zaixianzhuangtai - - # 如果状态没有变化,直接返回成功 - if old_status == new_status: - - return Response({ - 'code': 200, - 'message': '状态未变化', - 'data': { - 'zaixianzhuangtai': new_status, - 'status_text': '在线' if new_status == 1 else '离线' - } - }, status=status.HTTP_200_OK) - - # 更新状态 - dashou_profile.zaixianzhuangtai = new_status - dashou_profile.save() - - - - # 6. 返回成功响应 - return Response({ - 'code': 200, - 'message': '状态更新成功', - 'data': { - 'zaixianzhuangtai': new_status, - 'status_text': '在线' if new_status == 1 else '离线' - } - }, status=status.HTTP_200_OK) - - except Exception as e: - - return Response({ - 'code': 500, - 'message': f'服务器内部错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -from shangpin.models import Gsfenhong # 需要导入会员模型 -class GuanshiXinxiView(APIView): - """ - 获取管事信息接口 - 路径:/yonghu/guanshixinxi - 方法:POST - 权限:JWT认证 - 功能:获取当前管事的详细信息,包括基本信息、分红信息以及已充值打手数量 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 获取当前用户(通过JWT认证) - current_user = request.user - - # 查询管事扩展表 - try: - guanshi_profile = UserGuanshi.objects.get(user=current_user) - - # 🔥【新增】查询已充值打手数量(从分红表中统计) - # Gsfenhong 表中 guanshi 字段存储管事ID(yonghuid) - yichongzhi_count = Gsfenhong.objects.filter(guanshi=current_user.yonghuid).count() - - # 返回管事信息 - return Response({ - 'code': 200, - 'msg': '获取成功', - 'data': { - 'gszhstatus': guanshi_profile.zhuangtai, # 管事账号状态 - 'yaoqingzongshu': guanshi_profile.yaogingshuliang, # 邀请打手总数 - 'fenyongzonge': float(guanshi_profile.chongzhifenrun), # 充值分佣总额 - 'fenyongtixian': float(guanshi_profile.yue), # 可提现余额 - 'yichongzhiDashou': yichongzhi_count, # 已充值打手数量 - } - }) - - except UserGuanshi.DoesNotExist: - # 用户不是管事,返回错误 - return Response({ - 'code': 400, - 'msg': '您还不是管事,请先注册', - 'data': None - }, status=400) - - except Exception as e: - # 捕获所有异常,返回错误信息并记录日志 - import logging - logger = logging.getLogger(__name__) - logger.error(f"获取管事信息异常: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'msg': f'获取信息失败: {str(e)}', - 'data': None - }, status=500) - - - - -# yonghu/views.py - -from rest_framework import status -from django.core.exceptions import ObjectDoesNotExist - -from django.db.models import Sum -from houtai.models import ShangjiaRiTongji # 商家日统计表 -class ShangJiaXinXiView(APIView): - """ - 获取商家信息接口 (POST /yonghu/shangjiaxinxi) - 权限:需JWT认证 - 返回:商家基本信息 + 称号列表 + 今日/本月统计 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 获取商家扩展资料 - shangjia = request.user.shop_profile - except ObjectDoesNotExist: - return Response({ - 'code': 4001, - 'msg': '您还未注册成为商家或商家资料不存在', - 'data': None - }, status=status.HTTP_200_OK) - - user = request.user - yonghuid = user.yonghuid - today = timezone.now().date() - this_month_start = today.replace(day=1) - - # ------------------- 1. 从 UserShangjia 获取基础字段 ------------------- - response_data = { - 'sjzhzhuangtai': shangjia.zhuangtai, # 商家账号状态 - 'fadanzong': shangjia.fabu, # 发单总量(累计) - 'tuikuanzong': shangjia.tuikuan, # 退款总量(累计) - 'sjyue': float(shangjia.yue), # 商家余额 - # 'riliushui' 和 'yueliushui' 将从统计表计算,不再使用原字段 - } - - # ------------------- 2. 获取称号列表(仅限商家称号) ------------------- - chenghao_list = [] - # 查询当前用户的所有称号关联 - yonghu_chenghao_qs = YonghuChenghao.objects.filter(yonghu=user).select_related('chenghao') - for yc in yonghu_chenghao_qs: - chenghao_obj = yc.chenghao - # 只保留类型为“商家”的称号 - if chenghao_obj.leixing == 'shangjia': - chenghao_list.append({ - 'mingcheng': chenghao_obj.mingcheng, - 'texiao_json': chenghao_obj.texiao_miaoshu, # 前端需要的特效描述 - # 如果还需要其他字段(如图标URL)可在此添加,打手端有 texiao_json - }) - response_data['chenghao_list'] = chenghao_list - - # ------------------- 3. 从 ShangjiaRiTongji 获取今日统计 ------------------- - try: - today_stat = ShangjiaRiTongji.objects.get(yonghuid=yonghuid, riqi=today) - jinri_paifa_dingdan = today_stat.paifa_dingdan_shu - jinri_paifa_jine = float(today_stat.paifa_jine) - jinri_tuikuan_dingdan = today_stat.tuikuan_dingdan_shu - except ShangjiaRiTongji.DoesNotExist: - jinri_paifa_dingdan = 0 - jinri_paifa_jine = 0.00 - jinri_tuikuan_dingdan = 0 - - response_data['jinripaidan'] = jinri_paifa_dingdan # 今日派单数(新增) - response_data['jinrituikuan'] = jinri_tuikuan_dingdan # 今日退款数(新增) - response_data['riliushui'] = jinri_paifa_jine # 今日流水(派发总金额) - - # ------------------- 4. 从 ShangjiaRiTongji 获取本月统计 ------------------- - month_stats = ShangjiaRiTongji.objects.filter( - yonghuid=yonghuid, - riqi__gte=this_month_start, - riqi__lte=today - ).aggregate( - total_paifa_jine=Sum('paifa_jine'), - total_paifa_dingdan=Sum('paifa_dingdan_shu') - ) - yue_paifa_jine = float(month_stats.get('total_paifa_jine') or 0.00) - # yue_paifa_dingdan = month_stats.get('total_paifa_dingdan') or 0 # 如果前端不需要可不加 - - response_data['yueliushui'] = yue_paifa_jine # 今月流水(派发总金额) - - # (可选)如果前端还需要今月派单总数,取消下面注释 - # response_data['jinyuepaidan'] = yue_paifa_dingdan - - return Response({ - 'code': 200, - 'msg': '获取商家信息成功', - 'data': response_data - }, status=status.HTTP_200_OK) - - - - - - - - - - - - -class BossBiaoshiView(APIView): - """ - 获取用户的GoEasy标识 - 极简实现 - POST /yonghu/bossbiaoshi - 需要JWT Token认证 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 直接从request.user获取用户 - user = request.user - - # 2. 获取yonghuid(确保用户已登录) - if not user or not hasattr(user, 'yonghuid'): - return Response({ - 'code': 401, - 'msg': '用户未认证或用户信息不完整' - }, status=401) - - # 3. 生成标识:Boss + yonghuid(与前端约定) - boss_biaoshi = f"Boss{user.yonghuid}" - - # 4. 返回给前端 - return Response({ - 'code': 200, - 'msg': '获取成功', - 'data': { - 'boss_biaoshi': boss_biaoshi, - 'yonghuid': user.yonghuid, - 'user_type': user.user_type - } - }) - - -""" -阿龙电竞陪玩平台 - 邀请码相关API接口 -""" - -from utils.yaoqingma_utils import chuangjianYaoqingma, yanzhengYaoqingmaWeiyixing - - -class YaoqingmaView(APIView): - """ - 获取或生成管事邀请码 - 访问路径: /api/yonghu/yaoqingma/ - 请求方法: GET - 权限要求: JWT Token认证通过的用户 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - """ - 获取当前用户的邀请码 - 逻辑: - 1. 通过JWT获取当前用户 (request.user) - 2. 反向查询UserGuanshi扩展表 - 3. 验证管事状态(zhuangtai=1) - 4. 已有邀请码直接返回,没有则生成并保存 - """ - # 获取当前认证用户 - current_user = request.user - - # 1. 使用反向查询获取管事扩展信息 (性能最优方式) - try: - guanshi_profile = current_user.guanshi_profile - except UserGuanshi.DoesNotExist: - # 用户不是管事,没有扩展表记录 - return Response({ - 'code': 403, - 'message': '当前用户不是管事身份,无法生成邀请码', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 2. 验证管事状态 (zhuangtai 必须等于 1) - if guanshi_profile.zhuangtai != 1: - return Response({ - 'code': 403, - 'message': '管事账号状态异常,无法生成邀请码', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 3. 检查是否已存在邀请码 - existing_yaoqingma = guanshi_profile.yaoqingma - - # 如果存在且非空,直接返回 - if existing_yaoqingma and existing_yaoqingma.strip() != '': - return Response({ - 'code': 200, - 'message': '成功获取现有邀请码', - 'data': { - 'yaoqingma': existing_yaoqingma, - 'is_new': False # 标记是否为新生成的 - } - }) - - # 4. 生成新的邀请码 (使用用户ID字符串) - # 注意: 这里使用yonghuid,不是主键id,如你要求的"123456"这种形式 - yonghuid_str = str(current_user.yonghuid) - new_yaoqingma = chuangjianYaoqingma(yonghuid_str) - - # 5. 验证生成的邀请码格式 - if not yanzhengYaoqingmaWeiyixing(new_yaoqingma): - return Response({ - 'code': 500, - 'message': '邀请码生成失败,格式验证错误', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 6. 保存到数据库 (使用事务确保数据一致性) - try: - with transaction.atomic(): - guanshi_profile.yaoqingma = new_yaoqingma - guanshi_profile.save() - except Exception as e: - # 这里可以记录日志 - return Response({ - 'code': 500, - 'message': f'保存邀请码到数据库失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 7. 返回新生成的邀请码 - return Response({ - 'code': 200, - 'message': '成功生成新的邀请码', - 'data': { - 'yaoqingma': new_yaoqingma, - 'is_new': True # 标记是新生成的 - } - }) - - - -""" -阿龙电竞陪玩平台 - 打手注册及相关API接口 -""" - -from shangpin.models import Huiyuangoumai # 导入会员购买模型 -from utils.yaoqingma_utils import chuangjianYaoqingma # 导入之前写的邀请码生成算法 - -class DashouZhuceView(APIView): - """ - 打手注册接口 (通过管事邀请码) - 访问路径: /api/yonghu/dashouzhuce/ - 请求方法: POST - 权限要求: JWT Token认证通过的用户 - 请求体: { "inviteCode": "AbC123..." } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 获取并验证邀请码参数 - yaoqingma = request.data.get('inviteCode') - if not yaoqingma: - return Response({ - 'code': 400, - 'message': '邀请码不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - yaoqingma = yaoqingma.strip() - if len(yaoqingma) > 100: - return Response({ - 'code': 400, - 'message': '邀请码长度超过限制', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - current_user = request.user - # 注意:这里request.user是UserMain实例 - - # 2. 检查用户是否已是打手 (使用反向查询,性能最优) - try: - dashou_profile = current_user.dashou_profile - # 用户已是打手,直接返回现有信息 - return self.fanhuiXianyouDashouXinxi(dashou_profile, current_user) - except UserDashou.DoesNotExist: - # 用户不是打手,继续注册流程 - pass - - # 3. 根据邀请码查找对应的管事 - try: - # 使用select_related一次性获取管事及其关联的主表用户信息,性能最高 - guanshi_profile = UserGuanshi.objects.select_related('user').get(yaoqingma=yaoqingma) - except UserGuanshi.DoesNotExist: - return Response({ - 'code': 404, - 'message': '邀请码无效或不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 4. 验证管事状态 - - # 5. 核心:在数据库事务中创建用户的所有身份 - try: - with transaction.atomic(): - # 获取管事的用户ID (yonghuid) - guanshi_yonghuid = guanshi_profile.user.yonghuid - - # 5.1 创建打手扩展表 - dashou_profile = UserDashou.objects.create( - user=current_user, - nicheng='大手子', # 默认昵称 - chenghao='普通大手', # 默认称号 - yaoqingren=guanshi_yonghuid, # 设置邀请人ID - # 其他字段全部使用模型定义的默认值 - ) - - # 5.2 创建商家扩展表 - try: - boss_profile = current_user.boss_profile - if boss_profile.nickname: - boss_nickname = boss_profile.nickname - except: - pass - - - # 5.3 创建管事扩展表 - - # 5.4 更新主表的用户类型 (如果需要,可以设为'dashou',或保持原逻辑) - # current_user.user_type = 'dashou' # 根据你的业务逻辑决定 - # current_user.save() - - # 5.5 更新原管事的邀请数量 (使用行锁防止并发问题) - # 重新查询并锁定该行记录 - locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk) - locked_guanshi.yaogingshuliang += 1 - locked_guanshi.save() - - # ========== 新增:管事每日统计(邀请打手) ========== - try: - import logging - - logger = logging.getLogger(__name__) - from houtai.utils import update_guanshi_daily_by_action - update_guanshi_daily_by_action( - yonghuid=guanshi_yonghuid, - action=1, # 1 = 邀请打手 - amount=Decimal('0.00') - ) - logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}") - except Exception as e: - logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}") - - except Exception as e: - - # 事务会自动回滚 - # 这里可以记录更详细的日志 - return Response({ - 'code': 500, - 'message': f'注册过程中发生系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 6. 注册成功,返回信息 (新注册用户没有会员记录,clumber返回空列表) - fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=True) - return Response({ - 'code': 200, - 'message': '注册成功!已开通打手、管事身份。', - 'data': fanhui_data - }) - - def fanhuiXianyouDashouXinxi(self, dashou_profile, current_user): - """处理已是打手的用户,返回其现有信息""" - fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=False) - return Response({ - 'code': 200, - 'message': '您已是打手,返回当前信息。', - 'data': fanhui_data - }) - - def zhuangbeiFanhuiShuju(self, dashou_profile, current_user, is_new=False): - """准备返回给前端的打手数据,字段名严格匹配前端""" - - - - # 构建基础信息 - data = { - 'dashounicheng': dashou_profile.nicheng, - 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, - 'yongjin': str(dashou_profile.yue), # Decimal转字符串 - 'zonge': str(dashou_profile.zonge), - 'yajin': str(dashou_profile.yajin), - 'chenghao': dashou_profile.chenghao, - 'jinfen': dashou_profile.jifen, - 'chengjiaoliang': dashou_profile.chengjiaozongliang, - 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, - 'dashouzhuangtai': dashou_profile.zhuangtai, - } - - # 查询并处理会员列表 (clumber) - # 使用当前用户的yonghuid进行查询 - huiyuan_goumai_list = [] - if not is_new: # 如果是新用户,直接返回空列表,无需查询 - goumai_records = Huiyuangoumai.objects.filter(yonghu_id=current_user.yonghuid) - for record in goumai_records: - # 调用方法检查是否到期,此方法内部会更新状态 - shifou_daoqi = record.jiance_shifou_daoqi() - huiyuan_goumai_list.append({ - 'huiyuan_id': record.huiyuan_id, - 'huiyuan_zhuangtai': record.huiyuan_zhuangtai, - 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None, - 'shifou_daoqi': shifou_daoqi, - 'jieshao': record.jieshao - }) - - data['clumber'] = huiyuan_goumai_list - return data - - - - -from django.core.paginator import Paginator, EmptyPage - - - - -class GuanshiYaoqingDashouListView(APIView): - """ - 管事获取已邀请打手列表(支持筛选、搜索、分页) - POST /api/yonghu/gshqyqds/ - Headers: Authorization: Bearer - Body: - { - "page": 1, // 页码,从1开始 - "pageSize": 30, // 每页条数,最大100 - "keyword": "", // 搜索关键词(用户ID或昵称) - "zhanghaozhuangtai": 1, // 账号状态:1=正常,2=封禁(可选) - "zaixianzhuangtai": 1 // 在线状态:1=在线,2=离线(可选) - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - current_user = request.user - guanshi_yonghuid = current_user.yonghuid - - # 1. 分页参数 - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('pageSize', 30)) - if page < 1 or page_size < 1: - return Response({'code': 400, 'message': '分页参数必须大于0'}, status=400) - if page_size > 100: - page_size = 100 - - # 2. 筛选参数 - keyword = request.data.get('keyword', '').strip() - zhanghaozhuangtai = request.data.get('zhanghaozhuangtai') # 可选:1=正常,2=封禁 - zaixianzhuangtai = request.data.get('zaixianzhuangtai') # 可选:1=在线,2=离线 - - # 3. 基础查询:当前管事邀请的所有打手 - base_qs = UserDashou.objects.select_related('user').filter( - yaoqingren=guanshi_yonghuid - ) - - # 4. 全局统计(不受筛选影响,直接基于 base_qs) - total_all = base_qs.count() - zaixian_all = base_qs.filter(zaixianzhuangtai=1).count() - zhengchang_all = base_qs.filter(zhanghaozhuangtai=1).count() - fengjin_all = total_all - zhengchang_all - - # 5. 应用筛选条件 - if keyword: - base_qs = base_qs.filter( - Q(nicheng__icontains=keyword) | Q(user__yonghuid__icontains=keyword) - ) - if zhanghaozhuangtai is not None: - try: - val = int(zhanghaozhuangtai) - if val == 1: - base_qs = base_qs.filter(zhanghaozhuangtai=1) - else: - base_qs = base_qs.exclude(zhanghaozhuangtai=1) - except (ValueError, TypeError): - pass - if zaixianzhuangtai is not None: - try: - val = int(zaixianzhuangtai) - if val == 1: - base_qs = base_qs.filter(zaixianzhuangtai=1) - else: - base_qs = base_qs.exclude(zaixianzhuangtai=1) - except (ValueError, TypeError): - pass - - # 6. 排序和分页 - base_qs = base_qs.order_by('-create_time') - paginator = Paginator(base_qs, page_size) - try: - page_obj = paginator.page(page) - except EmptyPage: - page_obj = [] - has_more = False - filtered_total = paginator.count if paginator.count else 0 - else: - has_more = page_obj.has_next() - filtered_total = paginator.count - - # 7. 构建打手列表 - dashou_list = [] - for dashou in page_obj: - user_main = dashou.user - dashou_list.append({ - 'uid': user_main.yonghuid, - 'nicheng': dashou.nicheng or '打手', - 'zaixianzhuangtai': dashou.zaixianzhuangtai, - 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, - 'touxiang': user_main.avatar or '', - 'jiedanzongliang': dashou.jiedanzongliang, - 'chengjiaozongliang': dashou.chengjiaozongliang, - 'tuikuanliang': dashou.tuikuanliang, - 'yue': str(dashou.zonge), - }) - - # 8. 获取管事邀请总数(来自管事扩展表) - try: - yaoqingzongshu = current_user.guanshi_profile.yaogingshuliang - except Exception: - yaoqingzongshu = total_all - - response_data = { - 'dashouList': dashou_list, - 'yaoqingzongshu': yaoqingzongshu, # 邀请总数(全局) - 'totalCount': filtered_total, # 当前筛选条件下的总数 - 'zaixianCount': zaixian_all, # 全局在线 - 'zhengchangCount': zhengchang_all, # 全局正常 - 'fengjinCount': fengjin_all, # 全局封禁 - 'currentPage': page, - 'pageSize': page_size, - 'hasMore': has_more, - } - - return Response({'code': 200, 'message': '获取成功', 'data': response_data}) - - except ValueError: - return Response({'code': 400, 'message': '分页参数格式错误'}, status=400) - except Exception as e: - logger.error(f'获取打手列表失败: {str(e)}', exc_info=True) - return Response({'code': 500, 'message': '系统错误'}, status=500) - - - -from rest_framework.views import APIView -from rest_framework import permissions, status -from rest_framework.response import Response -from django.db import transaction -from .models import UserMain, UserDashou, UserGuanshi, Tixianjilu -import os -import uuid -from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss -from dingdan.models import Dingdan, DingdanPingtai, Lilubiao - - - -class TixianXinxiHuoquView(APIView): - """ - 获取用户提现收款信息接口 - 返回用户已设置的手机号、收款码、收款账号 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - """ - 处理获取收款信息的POST请求 - 直接返回用户当前的收款设置 - """ - try: - # 从request.user获取当前用户 - user_main = request.user - - # 🔥【修改开始 - 使用Decimal,字段名是lilu】 - # 查询打手提现费率 (fadanpingtai='5') - dashou_rate_obj = Lilubiao.objects.filter(fadanpingtai='5').first() - dashou_rate = decimal.Decimal(str(dashou_rate_obj.lilu)) if dashou_rate_obj else decimal.Decimal('0.00') - - # 查询管事提现费率 (fadanpingtai='6') - guanshi_rate_obj = Lilubiao.objects.filter(fadanpingtai='6').first() - guanshi_rate = decimal.Decimal(str(guanshi_rate_obj.lilu)) if guanshi_rate_obj else decimal.Decimal('0.00') - # 🔥【修改结束】 - - # 构建返回数据 - response_data = { - 'txdianhua': user_main.phone if user_main.phone else '', - 'txtupian': user_main.zhifu if user_main.zhifu else '', - 'txzh': user_main.skzhanghao if user_main.skzhanghao else '', - # 🔥【修改开始】 - 'dashou_rate': dashou_rate, # 打手提现费率 - 'guanshi_rate': guanshi_rate, # 管事提现费率 - # 🔥【修改结束】 - } - - # 返回成功响应 - return Response({ - 'code': 0, - 'msg': '获取成功', - 'data': response_data - }) - - except Exception as e: - return Response({ - 'code': 99, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - -from rest_framework.parsers import MultiPartParser, FormParser,JSONParser - - -class ShoukuanXinxiShangchuanView(APIView): - """ - 上传用户收款信息接口 - 支持同时上传手机号、收款账号、收款码图片 - 使用multipart/form-data格式 - """ - permission_classes = [permissions.IsAuthenticated] - parser_classes = (MultiPartParser,FormParser,JSONParser) - - def post(self, request): - """ - 处理上传收款信息的POST请求 - 验证并保存用户的收款设置 - """ - try: - # 获取当前用户 - user_main = request.user - yonghuid = user_main.yonghuid - - # 准备响应数据 - response_data = {} - - # 1. 验证必填字段:手机号必须要有 - txdianhua = request.data.get('txdianhua', '').strip() - if not txdianhua: - return Response({ - 'code': 1, - 'msg': '收款人电话不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证手机号格式(11位数字) - if not txdianhua.isdigit() or len(txdianhua) != 11: - return Response({ - 'code': 2, - 'msg': '请输入11位有效的手机号码', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 获取收款账号(可能为空) - txzh = request.data.get('txzh', '').strip() - - # 3. 处理收款码图片上传(如果有) - txtupian_file = request.FILES.get('file') # 注意:前端upload.js使用'file'作为字段名 - txtupian_url = '' - - if txtupian_file: - # 验证图片 - is_valid, error_msg = validate_image(txtupian_file) - if not is_valid: - return Response({ - 'code': 3, - 'msg': f'图片验证失败: {error_msg}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 上传收款码图片 - txtupian_url = self.handle_shoukuanma_upload(user_main, txtupian_file) - if txtupian_url: - response_data['txtupian'] = txtupian_url - else: - return Response({ - 'code': 4, - 'msg': '收款码上传失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - else: - response_data['txtupian'] = '' - - # 4. 验证收款账号和收款码至少有一个 - if not txzh and not txtupian_url: - return Response({ - 'code': 5, - 'msg': '请填写收款账号或上传收款码', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 5. 保存所有信息到数据库 - with transaction.atomic(): - user_main.phone = txdianhua - user_main.skzhanghao = txzh if txzh else '' - - # 只有上传了图片才更新zhifu字段 - if txtupian_url: - user_main.zhifu = txtupian_url - - user_main.save() - - # 6. 构建完整的返回数据 - response_data['txdianhua'] = txdianhua - response_data['txzh'] = txzh if txzh else '' - - # 7. 返回成功响应 - return Response({ - 'code': 0, - 'msg': '收款信息设置成功', - 'data': response_data - }) - - except Exception as e: - import traceback - traceback.print_exc() # 打印详细错误信息到控制台 - return Response({ - 'code': 99, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def handle_shoukuanma_upload(self, user_main, image_file): - """ - 处理收款码图片上传到腾讯云COS - 存储路径: tixian/{yonghuid}/{yonghuid}.扩展名 - """ - try: - yonghuid = user_main.yonghuid - if not yonghuid: - return None - - # 获取旧收款码URL - old_shoukuanma_url = user_main.zhifu - - # 生成文件扩展名 - file_name = image_file.name - file_extension = os.path.splitext(file_name)[1].lower() - if not file_extension: - file_extension = '.jpg' - - # 构建OSS文件路径: tixian/{yonghuid}/{yonghuid}{扩展名} - # 使用用户ID作为文件名,确保同一用户多次上传会覆盖 - oss_file_name = f"{yonghuid}{file_extension}" - oss_file_path = f"tixian/{yonghuid}/{oss_file_name}" - - # 删除旧收款码(如果存在) - if old_shoukuanma_url and old_shoukuanma_url.strip(): - delete_from_oss(old_shoukuanma_url) - - # 上传新收款码到OSS - new_shoukuanma_url = upload_to_oss(image_file, oss_file_path) - - if new_shoukuanma_url: - # 返回相对路径(从tixian文件夹开始) - return oss_file_path - else: - return None - - except Exception as e: - print(f"处理收款码上传失败: {e}") - return None - - - - - - - -from rest_framework.views import APIView -from rest_framework import permissions, status -from rest_framework.response import Response -from django.db import transaction -from django.db.models import F -import decimal -import traceback -from .models import UserMain, UserDashou, UserGuanshi, Tixianjilu - - - - - - - - -from utils.fadan_utils import check_fadan_qiangdan_eligible - - -# ========== 订单状态常量(请根据您的实际业务修改)========== -# 假设状态值:已完成=9,已退款=10(示例,实际以您的项目为准) -ORDER_STATUS_COMPLETED = 3 # 订单已完成 -ORDER_STATUS_REFUNDED = 5 # 订单已退款 -ORDER_STATUS_FAIL = 6 -# 进行中状态(不可提现)例如:待接单、已接单、进行中等,您需要定义排除列表 -FORBIDDEN_STATUS_FOR_WITHDRAW = [1, 2, 3, 4, 5, 6, 7, 8] # 除已完成和已退款外的所有状态 - - -class TixianShenqingView(APIView): - """ - 用户提现申请接口(完整版) - 支持类型: - 1-打手佣金提现 - 2-管事分红提现 - 3-组长分红提现 - 4-审核官分佣提现 - 5-打手押金提现(退出平台) - 6-商家余额提现(退出平台) - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user_main = request.user - yonghuid = user_main.yonghuid - - # 1. 获取并验证参数 - leixing = request.data.get('leixing') - jine = request.data.get('jine') - fangshi = request.data.get('fangshi') - txskfs = request.data.get('txskfs', 0) - - # 罚单检查(共用) - fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2) - if not fadan_ok: - return Response({'code': 400, 'msg': fadan_msg}) - - # 参数完整性 - missing = [] - if leixing is None: missing.append('leixing') - if jine is None: missing.append('jine') - if fangshi is None: missing.append('fangshi') - if missing: - return Response({'code': 1, 'msg': f'缺少参数: {", ".join(missing)}'}, - status=status.HTTP_400_BAD_REQUEST) - - try: - leixing = int(leixing) - jine = decimal.Decimal(str(jine)) - fangshi = int(fangshi) - txskfs = int(txskfs) - except (ValueError, TypeError, decimal.InvalidOperation) as e: - return Response({'code': 2, 'msg': f'参数格式错误: {str(e)}'}, - status=status.HTTP_400_BAD_REQUEST) - - if jine <= decimal.Decimal('0'): - return Response({'code': 3, 'msg': '提现金额必须大于0'}, - status=status.HTTP_400_BAD_REQUEST) - - # 支持的提现类型(1-6) - if leixing not in [1, 2, 3, 4, 5, 6]: - return Response({'code': 4, 'msg': '无效的提现类型'}, - status=status.HTTP_400_BAD_REQUEST) - - if fangshi not in [1, 2]: - return Response({'code': 5, 'msg': '无效的收款方式,只能是1(微信)或2(支付宝)'}, - status=status.HTTP_400_BAD_REQUEST) - - # 2. 获取对应费率及手续费 - current_rate = decimal.Decimal('0.00') - rate_key = { - 1: '5', # 打手佣金 - 2: '6', # 管事分红 - 3: '8', # 组长分红 - 4: '9', # 审核官分佣 - 5: '11', # 打手押金提现 - 6: '10', # 商家余额提现 - }.get(leixing) - - if rate_key: - rate_obj = Lilubiao.objects.filter(fadanpingtai=rate_key).first() - if rate_obj: - current_rate = decimal.Decimal(str(rate_obj.lilu)) - else: - return Response({'code': 41, 'msg': f'提现费率未配置(类型{leixing})'}, - status=status.HTTP_400_BAD_REQUEST) - - shouxufei = jine * current_rate - shijidaozhang = jine - shouxufei - if shijidaozhang <= decimal.Decimal('0'): - return Response({'code': 6, 'msg': '提现金额过低,扣除手续费后无实际到账'}, - status=status.HTTP_400_BAD_REQUEST) - - # 3. 根据提现类型执行校验和扣款 - nicheng = '' - if leixing == 1: - result = self._check_dashou_commission_withdraw(user_main, jine) - if result['code'] != 0: - return Response(result, status=status.HTTP_400_BAD_REQUEST) - nicheng = result['data']['nicheng'] - # 扣款(佣金) - with transaction.atomic(): - profile = UserDashou.objects.select_for_update().get(user=user_main) - if profile.yue < jine: - return Response({'code': 13, 'msg': '余额不足'}) - profile.yue = F('yue') - jine - profile.save() - elif leixing == 2: - result = self._check_guanshi_dividend_withdraw(user_main, jine) - if result['code'] != 0: - return Response(result, status=status.HTTP_400_BAD_REQUEST) - with transaction.atomic(): - profile = UserGuanshi.objects.select_for_update().get(user=user_main) - if profile.yue < jine: - return Response({'code': 22, 'msg': '余额不足'}) - profile.yue = F('yue') - jine - profile.save() - nicheng = "管事用户" - elif leixing == 3: - result = self._check_zuzhang_withdraw(user_main, jine) - if result['code'] != 0: - return Response(result, status=status.HTTP_400_BAD_REQUEST) - with transaction.atomic(): - profile = UserZuzhang.objects.select_for_update().get(user=user_main) - if profile.ketixian_jine < jine: - return Response({'code': 32, 'msg': '可提现金额不足'}) - profile.ketixian_jine = F('ketixian_jine') - jine - profile.save() - nicheng = user_main.nicheng if hasattr(user_main, 'nicheng') else '' - elif leixing == 4: - result = self._check_shenheguan_withdraw(user_main, jine) - if result['code'] != 0: - return Response(result, status=status.HTTP_400_BAD_REQUEST) - with transaction.atomic(): - profile = UserShenheguan.objects.select_for_update().get(user=user_main) - if profile.yue < jine: - return Response({'code': 42, 'msg': '可提现金额不足'}) - profile.yue = F('yue') - jine - profile.save() - nicheng = "审核官" - elif leixing == 5: - # 打手押金提现(退出) - result = self._check_dashou_yajin_withdraw(user_main, jine) - if result['code'] != 0: - return Response(result, status=status.HTTP_400_BAD_REQUEST) - with transaction.atomic(): - profile = UserDashou.objects.select_for_update().get(user=user_main) - if profile.yajin < jine: - return Response({'code': 53, 'msg': '押金余额不足'}) - profile.yajin = F('yajin') - jine - # 押金提现后封禁打手账号 - profile.zhanghaozhuangtai = 0 # 0表示禁用 - profile.save() - nicheng = profile.nicheng or '' - elif leixing == 6: - # 商家余额提现(退出) - result = self._check_shangjia_withdraw(user_main, jine) - if result['code'] != 0: - return Response(result, status=status.HTTP_400_BAD_REQUEST) - with transaction.atomic(): - profile = UserShangjia.objects.select_for_update().get(user=user_main) - if profile.yue < jine: - return Response({'code': 63, 'msg': '商家余额不足'}) - profile.yue = F('yue') - jine - # 注意:是否封禁商家账号?根据需求,用户未明确要求封禁,暂时只扣款,可后续扩展。 - profile.save() - nicheng = profile.nicheng or '' - - # 4. 创建提现记录(先扣款后创建,确保资金安全) - try: - avatar = user_main.avatar or '' - phone = user_main.phone or '' - zhifu = user_main.zhifu or '' - skzhanghao = user_main.skzhanghao or '' - tixian_record = Tixianjilu.objects.create( - yonghuid=yonghuid, - avatar=avatar, - phone=phone, - nicheng=nicheng, - leixing=leixing, - zhifu=zhifu, - skzhanghao=skzhanghao, - jine=shijidaozhang, # 记录实际到账金额 - zhuangtai=1, # 审核中 - fangshi=fangshi, - shenheid=None, - bhliyou='', - ) - except Exception as e: - return Response({'code': 99, 'msg': f'创建提现记录失败: {str(e)}'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 5. 返回成功响应 - return Response({ - 'code': 0, - 'msg': '提现申请提交成功', - 'data': { - 'tixian_id': tixian_record.id, - 'leixing': leixing, - 'jine': str(jine.quantize(decimal.Decimal('0.01'))), - 'fangshi': fangshi, - 'zhuangtai': 1, - 'create_time': tixian_record.create_time.strftime('%Y-%m-%d %H:%M:%S'), - 'current_rate': str(current_rate.quantize(decimal.Decimal('0.0000'))), - 'shouxufei': str(shouxufei.quantize(decimal.Decimal('0.01'))), - 'shijidaozhang': str(shijidaozhang.quantize(decimal.Decimal('0.01'))), - 'tip': f'本次提现手续费率{current_rate * 100}%,手续费{shouxufei}元,实际到账{shijidaozhang}元' - } - }) - - except Exception as e: - traceback.print_exc() - return Response({'code': 99, 'msg': f'系统错误: {str(e)}'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # ==================== 校验函数 ==================== - - def _check_dashou_commission_withdraw(self, user_main, jine): - """打手佣金提现校验""" - try: - dashou = UserDashou.objects.get(user=user_main) - except ObjectDoesNotExist: - return {'code': 10, 'msg': '您不是打手身份,无法提现佣金'} - - if dashou.zhanghaozhuangtai != 1: - return {'code': 11, 'msg': '打手账号已被封禁,无法提现'} - if dashou.zhuangtai != 1: - return {'code': 12, 'msg': '您有订单进行中,请完成后提现'} - if dashou.jifen != 10: - return {'code': 15, 'msg': '积分不足10分,请补充积分后提现'} - if dashou.yue < jine: - return {'code': 13, 'msg': f'余额不足,当前余额: {dashou.yue}'} - - return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}} - - def _check_guanshi_dividend_withdraw(self, user_main, jine): - """管事分红提现校验""" - try: - guanshi = UserGuanshi.objects.get(user=user_main) - except ObjectDoesNotExist: - return {'code': 20, 'msg': '您不是管事身份,无法提现分红'} - if guanshi.zhuangtai != 1: - return {'code': 21, 'msg': '管事账号已被封禁,无法提现'} - if guanshi.yue < jine: - return {'code': 22, 'msg': f'余额不足,当前余额: {guanshi.yue}'} - return {'code': 0, 'msg': 'ok', 'data': {}} - - def _check_zuzhang_withdraw(self, user_main, jine): - """组长分红提现校验""" - try: - zuzhang = UserZuzhang.objects.get(user=user_main) - except ObjectDoesNotExist: - return {'code': 30, 'msg': '您不是组长身份,无法提现组长分红'} - if zuzhang.zhuangtai != 1: - return {'code': 31, 'msg': '组长账号已被封禁,无法提现'} - if zuzhang.ketixian_jine < jine: - return {'code': 32, 'msg': f'可提现金额不足,当前可提: {zuzhang.ketixian_jine}'} - return {'code': 0, 'msg': 'ok', 'data': {}} - - def _check_shenheguan_withdraw(self, user_main, jine): - """审核官分佣提现校验""" - try: - shenheguan = UserShenheguan.objects.get(user=user_main) - except ObjectDoesNotExist: - return {'code': 40, 'msg': '您不是审核官身份,无法提现分佣'} - if shenheguan.zhuangtai != 1: - return {'code': 41, 'msg': '审核官账号已被封禁,无法提现'} - if shenheguan.yue < jine: - return {'code': 42, 'msg': f'可提现金额不足,当前可提: {shenheguan.yue}'} - return {'code': 0, 'msg': 'ok', 'data': {}} - - from django.db.models import Q - - def _check_shangjia_withdraw(self, user_main, jine): - """ - 商家余额提现校验(退出平台) - 包含:订单、自身罚单、申请的罚单检查 - """ - try: - shangjia = UserShangjia.objects.get(user=user_main) - except ObjectDoesNotExist: - return {'code': 60, 'msg': '您不是商家身份,无法提现商家余额'} - - if shangjia.zhuangtai != 1: - return {'code': 61, 'msg': '商家账号已被禁用,无法提现'} - if shangjia.yue < jine: - return {'code': 62, 'msg': f'商家余额不足,当前余额: {shangjia.yue}'} - - # 订单检查(使用 Q 取反) - unfinished_orders = DingdanShangjia.objects.filter( - shangjia_id=user_main.yonghuid, - ).exclude( - dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED,ORDER_STATUS_FAIL] - ).exists() - if unfinished_orders: - return {'code': 63, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提现余额'} - - # 自身被处罚的罚单检查(状态不是已缴纳(2)和已驳回(4)) - self_fadan = Fadan.objects.filter( - beichufa_id=user_main.yonghuid, - ).exclude( - zhuangtai__in=[2, 4] - ).exists() - if self_fadan: - return {'code': 64, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'} - - # 申请的对他人处罚罚单检查(状态不是已缴纳(2)和已驳回(4)) - applied_fadan = Fadan.objects.filter( - shenqing_chufa=user_main.yonghuid, - ).exclude( - zhuangtai__in=[2, 4] - ).exists() - if applied_fadan: - return {'code': 65, 'msg': '您发起的对其他用户的处罚尚未处理完毕(待缴纳/申诉中),请等待完结'} - - return {'code': 0, 'msg': 'ok', 'data': {'nicheng': shangjia.nicheng or ''}} - - def _check_dashou_yajin_withdraw(self, user_main, jine): - """ - 打手押金提现校验(退出平台) - 要求:无进行中的订单,无未结算订单,自身罚单需已处理 - """ - try: - dashou = UserDashou.objects.get(user=user_main) - except ObjectDoesNotExist: - return {'code': 50, 'msg': '您不是打手身份,无法提取押金'} - - if dashou.zhanghaozhuangtai != 1: - return {'code': 51, 'msg': '打手账号已禁用,无法提取押金'} - if dashou.zhuangtai != 1: - return {'code': 52, 'msg': '您有订单进行中,请完成后提取押金'} - if dashou.yajin < jine: - return {'code': 53, 'msg': f'押金余额不足,当前押金: {dashou.yajin}'} - - # 订单检查(使用 exclude + __in) - unfinished_orders = Dingdan.objects.filter( - jiedan_dashou_id=user_main.yonghuid, - ).exclude( - zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED] - ).exists() - if unfinished_orders: - return {'code': 54, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提取押金'} - - # 自身罚单检查 - self_fadan = Fadan.objects.filter( - beichufa_id=user_main.yonghuid, - ).exclude( - zhuangtai__in=[2, 4] - ).exists() - if self_fadan: - return {'code': 55, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'} - - return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}} - - - - - - - - - -class TixianJiluHuoquViewV2(APIView): - """ - 提现记录获取接口 - mode=1:手动提现记录(默认,兼容旧版) - mode=2:自动提现审核记录(含 shenhe_jilu_id / shenhe_danhao,支持 zhuangtai 筛选) - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - from .models import TixianAutoRecord, TixianShenheJilu - - user_main = request.user - yonghuid = user_main.yonghuid - page = request.data.get('page', 1) - limit = request.data.get('limit', 50) - mode = int(request.data.get('mode', 1) or 1) - zhuangtai_filter = int(request.data.get('zhuangtai', 0) or 0) - - try: - page = int(page) - limit = int(limit) - if page < 1 or limit < 1: - return Response({'code': 1, 'msg': '参数错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - except (TypeError, ValueError): - return Response({'code': 1, 'msg': '参数格式错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - if limit > 100: - limit = 100 - offset = (page - 1) * limit - - qs = Tixianjilu.objects.filter(yonghuid=yonghuid) - if mode == 2: - # 自动提现:含新审核关联记录;zhuangtai>0 时按状态筛选 - if zhuangtai_filter > 0: - qs = qs.filter(zhuangtai=zhuangtai_filter) - qs = qs.order_by('-create_time') - - records_queryset = qs.values( - 'id', 'leixing', 'jine', 'zhuangtai', 'create_time', 'fangshi', - 'phone', 'skzhanghao', 'zhifu', 'bhliyou', - 'shenhe_jilu_id', 'shenhe_danhao', - )[offset:offset + limit + 1] - - records_list = list(records_queryset) - has_more = len(records_list) > limit - records_to_return = records_list[:limit] if has_more else records_list - - # 批量取审核表 fail_reason - audit_ids = [r['shenhe_jilu_id'] for r in records_to_return if r.get('shenhe_jilu_id')] - audit_map = {} - if audit_ids: - for a in TixianShenheJilu.objects.filter(id__in=audit_ids).values( - 'id', 'fail_reason', 'shenqing_jine', 'shenhe_danhao' - ): - audit_map[a['id']] = a - - # 批量取自动打款失败原因 - danhao_list = [r['shenhe_danhao'] for r in records_to_return if r.get('shenhe_danhao')] - auto_fail_map = {} - if danhao_list: - for ar in TixianAutoRecord.objects.filter( - shenhe_danhao__in=danhao_list, zhuangtai=2 - ).order_by('-create_time').values('shenhe_danhao', 'fail_reason'): - if ar['shenhe_danhao'] not in auto_fail_map: - auto_fail_map[ar['shenhe_danhao']] = ar['fail_reason'] - - formatted_records = [] - for record in records_to_return: - audit = audit_map.get(record.get('shenhe_jilu_id')) if record.get('shenhe_jilu_id') else None - fail_reason = '' - if audit and audit.get('fail_reason'): - fail_reason = audit['fail_reason'] - elif record.get('shenhe_danhao'): - fail_reason = auto_fail_map.get(record['shenhe_danhao'], '') or '' - - jine_val = float(record['jine']) if record['jine'] else 0.00 - if mode == 2 and audit and audit.get('shenqing_jine') is not None: - jine_val = float(audit['shenqing_jine']) - - shenhe_danhao_val = record.get('shenhe_danhao') or (audit.get('shenhe_danhao') if audit else '') - formatted_records.append({ - 'id': record['id'], - 'tixianjilu_id': record['id'], - 'tixian_id': record['id'], - 'leixing': record['leixing'], - 'jine': jine_val, - 'zhuangtai': record['zhuangtai'], - 'create': record['create_time'].strftime('%Y-%m-%d %H:%M:%S'), - 'fangshi': record['fangshi'], - 'txdianhua': record['phone'] or '', - 'txzh': record['skzhanghao'] or '', - 'txtupian': record['zhifu'] or '', - 'shibaiyuanyin': record['bhliyou'] or '', - 'bhliyou': record['bhliyou'] or '', - 'fail_reason': fail_reason, - 'shenhe_jilu_id': record.get('shenhe_jilu_id'), - 'shenhe_danhao': shenhe_danhao_val, - }) - - return Response({ - 'code': 0, - 'msg': '获取成功', - 'data': { - 'list': formatted_records, - 'has_more': has_more, - 'page': page, - 'limit': limit, - 'current_count': len(formatted_records), - }, - }) - - except Exception as e: - return Response({ - 'code': 99, - 'msg': f'系统错误: {str(e)}', - 'data': None, - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -# yonghu/views.py -import jwt -import datetime - - - -class AdminLoginView(APIView): - """ - 管理员登录接口 - 路径:/yonghu/adlog/ - 方法:POST - 字段:zhanghao(账号), mima(密码), ip(二次验证码) - """ - - # 限制匿名用户访问频率,防止暴力破解 - throttle_classes = [AnonRateThrottle] - # 允许任何人访问(登录接口不需要认证) - permission_classes = [AllowAny] - - def post(self, request): - """ - 处理管理员登录请求 - 安全策略:任何错误都返回401,不透露具体信息 - """ - try: - # 1. 获取客户端真实IP和用户代理信息 - kehuduan_ip = huoquKehuduanIP(request) - yonghu_daili = huoquYonghuDaili(request) - - - # 2. 获取前端传递的数据 - data = request.data - - # 检查必需字段是否存在 - bixu_ziduan = ['zhanghao', 'mima', 'ip'] - for ziduan in bixu_ziduan: - if ziduan not in data: - # 字段缺失,返回401 - return Response( - {'code': 401, 'msg': '登录失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 提取并清理数据 - zhanghao = data.get('zhanghao', '').strip() - mima = data.get('mima', '').strip() - erci_yanzhengma = data.get('ip', '').strip() # 二次验证码(前端叫ip) - - # 简单验证字段非空 - if not zhanghao or not mima or not erci_yanzhengma: - return Response( - {'code': 401, 'msg': '登录失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 3. 使用事务确保数据一致性 - with transaction.atomic(): - # 【优化点1】使用select_related一次性获取管理员扩展表 - # 只查询管理员类型的用户 - user = UserMain.objects.filter( - phone=zhanghao, # 手机号作为账号 - password=mima, # 密码 - user_type='admin' # 必须是管理员类型 - ).select_related('admin_profile').first() - - # 如果用户不存在或不是管理员,返回401 - if not user: - return Response( - {'code': 401, 'msg': '登录失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 【优化点2】检查管理员扩展表是否存在 - if not hasattr(user, 'admin_profile'): - return Response( - {'code': 401, 'msg': '登录失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 【优化点3】验证二次密码(管理员扩展表中的password字段) - admin_profile = user.admin_profile - if admin_profile.password != erci_yanzhengma: - return Response( - {'code': 401, 'msg': '登录失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 4. 更新用户登录信息 - # 获取当前时间 - now_time = timezone.now() - - # 处理IP地址长度问题 - # 如果原ip字段长度不够,截取前11个字符(保持兼容性) - - cunchu_ip = kehuduan_ip - #print(cunchu_ip) - - # 更新用户信息 - user.ip = cunchu_ip # 存储客户端IP - user.last_login_time = now_time # 更新最后登录时间 - user.save() - - # 【优化点4】生成JWT token(使用和微信登录相同的方式) - # 使用djangorestframework-simplejwt生成token - refresh = RefreshToken.for_user(user) - token = str(refresh.access_token) - - # 5. 准备返回数据 - # 返回用户基本信息(可根据需要调整) - response_data = { - 'token': token, - 'yonghuid': user.yonghuid, - 'phone': user.phone or '', - 'user_type': user.user_type, - 'last_login_time': now_time.strftime('%Y-%m-%d %H:%M:%S') if now_time else '', - } - - # 可以添加管理员特定信息 - if hasattr(user, 'admin_profile'): - response_data['wechat'] = user.admin_profile.wechat or '' - - return Response({ - 'code': 0, - 'msg': '登录成功', - 'data': response_data - }) - - except Exception as e: - # 【安全点】任何异常都返回401,不暴露具体错误信息 - # 记录日志用于排查问题,但前端只收到通用错误信息 - # 这里可以添加日志记录,比如: - # import logging - # logging.error(f'管理员登录异常: {str(e)}', exc_info=True) - - return Response( - {'code': 401, 'msg': '登录失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - -# yonghu/views.py -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from django.db import transaction -from django.core.exceptions import ObjectDoesNotExist -import json - -# 导入模型 -from yonghu.models import UserMain, AdminProfile -from peizhi.models import Szjilu - - -class AdminFinancialDataView(APIView): - """ - 管理员财务数据接口 - 路径:/yonghu/adhq/ - 方法:POST - 认证:JWT Token - 权限:仅管理员 - 返回:收支记录数据 - """ - - # 使用JWT认证 - permission_classes = [IsAuthenticated] - - def post(self, request): - """ - 处理管理员财务数据请求 - 安全策略:任何错误都返回401,不透露具体信息 - """ - try: - # 1. 获取前端传递的数据 - data = request.data - - # 检查必需字段 - if 'zhanghao' not in data: - return Response( - {'code': 401, 'msg': '请求参数错误', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - qianzhanghao = data.get('zhanghao', '').strip() - - # 简单验证字段非空 - if not qianzhanghao: - return Response( - {'code': 401, 'msg': '请求参数错误', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 2. 获取当前登录用户(JWT认证后,request.user就是UserMain实例) - dangqianyonghu = request.user - - # 3. 验证用户类型是否为admin - if not hasattr(dangqianyonghu, 'user_type') or dangqianyonghu.user_type != 'admin': - return Response( - {'code': 401, 'msg': '权限不足', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 4. 验证前端传递的账号与当前用户的phone字段是否一致 - if not dangqianyonghu.phone or dangqianyonghu.phone != qianzhanghao: - return Response( - {'code': 401, 'msg': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 5. 验证管理员扩展表是否存在(反向查询) - # 使用高效的ORM查询,避免额外的数据库查询 - try: - # 使用select_related一次性获取管理员扩展表 - # 注意:这里假设user_type='admin'的用户都有admin_profile - yonghu_xiangxi = UserMain.objects.select_related('admin_profile').get( - pk=dangqianyonghu.pk, - user_type='admin' - ) - - # 检查管理员扩展表是否存在 - if not hasattr(yonghu_xiangxi, 'admin_profile'): - return Response( - {'code': 401, 'msg': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - except UserMain.DoesNotExist: - return Response( - {'code': 401, 'msg': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 6. 查询收支记录表(Szjilu) - # 使用高效的ORM查询,获取最新的一条记录 - # 注意:根据你的业务需求,这里获取最新的一条记录 - # 如果需要汇总多条记录,可以使用aggregate进行聚合查询 - - # 方法1:获取最新的一条记录(假设表中只有一条汇总记录) - shouzhijilu = Szjilu.objects.order_by('-create_time').first() - - # 方法2:如果表中可能有多条记录,需要汇总,可以使用以下方式: - # from django.db.models import Sum - # zonghe = Szjilu.objects.aggregate( - # zongliushui=Sum('zongls'), - # zongshouyi=Sum('zongsy'), - # zongzhichu=Sum('zongzc'), - # jrls=Sum('jrls'), - # jrzc=Sum('jrzc') - # ) - - # 准备返回数据 - if shouzhijilu: - # 有记录的情况 - huizongshuju = { - 'zongliushui': float(shouzhijilu.zongls or 0), - 'zongshouyi': float(shouzhijilu.zongsy or 0), - 'zongzhichu': float(shouzhijilu.zongzc or 0), - 'jrls': float(shouzhijilu.jrls or 0), - 'jrzc': float(shouzhijilu.jrzc or 0) - } - else: - # 没有记录的情况,返回默认值 - huizongshuju = { - 'zongliushui': 0.00, - 'zongshouyi': 0.00, - 'zongzhichu': 0.00, - 'jrls': 0.00, - 'jrzc': 0.00 - } - - # 7. 返回成功响应 - return Response({ - 'code': 0, - 'msg': '获取成功', - 'data': huizongshuju - }) - - except Exception as e: - # 【安全策略】任何异常都返回401,不暴露具体错误信息 - # 这里可以记录日志,但不返回给前端 - # import logging - # logging.error(f'管理员财务数据接口异常: {str(e)}', exc_info=True) - - return Response( - {'code': 401, 'msg': '获取失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - - - - - -# shangpin/views.py -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser -import logging - -logger = logging.getLogger(__name__) - - -class AdGetOrderTypes(APIView): - """ - 后台获取订单类型列表接口 - 前端调用地址: /shangpin/getOrderTypes - 前端需要字段: id, biaoti, tupian_url - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - try: - # 1. 获取参数 - qianzhanghao = request.data.get('zhanghao', '').strip() - - if not qianzhanghao: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2. 身份验证(按你说的方式) - dangqianyonghu = request.user - - # 验证手机号 - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证管理员身份 - if dangqianyonghu.user_type != 'admin': - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 3. 验证管理员扩展表 - try: - guanliyuan = dangqianyonghu.admin_profile - except AttributeError: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 4. 查询订单类型(商品类型)表 - # 注意:这里需要根据你实际的模型名调整 - # 如果模型名不是ShangpinType,请告诉我正确的模型名 - - # 假设模型名是ShangpinLeixing(因为我之前看到你有商品类型表) - # 你需要告诉我确切的模型名 - try: - # 请替换为你的实际模型名 - from shangpin.models import ShangpinLeixing # 或者 ShangpinType - # 只查询前端需要的三个字段 - shangpin_types = ShangpinLeixing.objects.all().only('id', 'jieshao', 'tupian_url') - - # 构建响应数据 - type_list = [ - { - 'id': item.id, - 'biaoti': item.jieshao or '', - 'tupian_url': item.tupian_url or '' - } - for item in shangpin_types - ] - - - return Response({ - 'code': 0, - 'message': '获取成功', - 'data': type_list - }) - - except Exception as e: - logger.error(f"查询商品类型表失败: {str(e)}") - # 如果表不存在,返回空数组 - return Response({ - 'code': 0, - 'message': '获取成功', - 'data': [] - }) - - except Exception as e: - logger.error(f"获取订单类型异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - - - - -# yonghu/views.py 或 dingdan/views.py -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser -import logging -from django.db.models import Q, Count -import time - -logger = logging.getLogger(__name__) - - -class AdGetDingdanList(APIView): - """ - 后台订单列表获取接口 - 前端调用地址: /yonghu/adddhq - 前端需要字段: dingdan_id, jieshao, zhuangtai, leixing, fadanpingtai, jiage, tupian_url, fadanshijian - 统计字段: stats.platform.total, stats.platform.completed, stats.merchant.total, stats.merchant.completed - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - try: - # 1. 获取参数(严格按前端传的字段名) - qianzhanghao = request.data.get('zhanghao', '').strip() - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('pageSize', 30)) - - # 搜索相关参数 - dingdan_id = request.data.get('dingdan_id', '').strip() - - # 筛选相关参数(严格按前端字段名) - fadanpingtai = request.data.get('fadanpingtai') # 前端叫 fadanpingtai - leixing = request.data.get('leixing') # 前端叫 leixing - zhuangtai = request.data.get('zhuangtai', '') # 前端叫 zhuangtai - - if not qianzhanghao: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2. 身份验证(与你说的完全一样) - dangqianyonghu = request.user - - # 验证手机号 - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证管理员身份 - if dangqianyonghu.user_type != 'admin': - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 3. 验证管理员扩展表 - try: - guanliyuan = dangqianyonghu.admin_profile - except AttributeError: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 4. 导入订单模型 - from dingdan.models import Dingdan # 你的订单主表模型 - - # 5. 构建查询条件(严格按前端字段名映射到数据库字段) - query_conditions = Q() - - # 判断是否为搜索模式 - is_search_mode = bool(dingdan_id) - - if is_search_mode: - # 搜索模式:只按订单ID查询 - query_conditions &= Q(dingdan_id=dingdan_id) - else: - # 筛选模式 - - # 发单平台筛选(前端: fadanpingtai -> 数据库: fadan_pingtai) - if fadanpingtai is not None: - try: - fadanpingtai_int = int(fadanpingtai) - if fadanpingtai_int in [1, 2]: - query_conditions &= Q(fadan_pingtai=fadanpingtai_int) - except (ValueError, TypeError): - pass - - # 订单类型筛选(前端: leixing -> 数据库: leixing_id) - if leixing is not None: - try: - leixing_int = int(leixing) - query_conditions &= Q(leixing_id=leixing_int) - except (ValueError, TypeError): - pass - - # 订单状态筛选(前端: zhuangtai -> 数据库: zhuangtai) - if zhuangtai and zhuangtai != 'all': - try: - # 处理多个状态,如 "1,7" - status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()] - if status_list: - query_conditions &= Q(zhuangtai__in=status_list) - except (ValueError, TypeError): - pass - - # 6. 获取统计数据(平台订单和商家订单) - # 注意:这里统计所有订单,不考虑筛选条件 - try: - # 平台订单统计 (fadan_pingtai=1) - platform_total = Dingdan.objects.filter(fadan_pingtai=1).count() - platform_completed = Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=3).count() - - # 商家订单统计 (fadan_pingtai=2) - merchant_total = Dingdan.objects.filter(fadan_pingtai=2).count() - merchant_completed = Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=3).count() - - platform_stats = { - 'total': platform_total, - 'completed': platform_completed - } - merchant_stats = { - 'total': merchant_total, - 'completed': merchant_completed - } - except Exception as e: - logger.warning(f"统计数据查询失败: {str(e)}") - platform_stats = {'total': 0, 'completed': 0} - merchant_stats = {'total': 0, 'completed': 0} - - # 7. 执行分页查询(只查询主表,不查扩展表) - start_time = time.time() - - # 计算分页 - offset = (page - 1) * page_size - - # 查询符合条件的订单总数 - total_count = Dingdan.objects.filter(query_conditions).count() - - # 查询当前页的数据(只查询主表,只查询前端需要的字段) - dingdan_list = Dingdan.objects.filter(query_conditions).only( - 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', - 'fadan_pingtai', 'jine', 'tupian', 'create_time' - ).order_by('-create_time')[offset:offset + page_size] - - query_time = time.time() - start_time - logger.info(f"订单查询完成,耗时: {query_time:.3f}s,查询条数: {len(dingdan_list)}") - - # 8. 构建响应数据(严格按前端需要的字段) - result_list = [] - - for dingdan in dingdan_list: - # 注意:这里字段名必须与前端完全一致 - order_data = { - 'dingdan_id': dingdan.dingdan_id or '', - 'jieshao': dingdan.jieshao or '', - 'zhuangtai': dingdan.zhuangtai or 1, - 'leixing': dingdan.leixing_id or 0, # 前端需要 leixing - 'fadanpingtai': dingdan.fadan_pingtai or 1, # 前端需要 fadanpingtai - 'jiage': float(dingdan.jine) if dingdan.jine else 0.00, # 价格对应 jine 字段 - 'tupian_url': dingdan.tupian or '', # 图片对应 tupian 字段 - 'fadanshijian': dingdan.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan.create_time else '' - } - result_list.append(order_data) - - # 9. 判断是否有更多数据 - current_loaded_count = offset + len(result_list) - has_more = len(result_list) >= page_size and current_loaded_count < total_count - - # 10. 构建完整响应(严格按前端需要的结构) - response_data = { - 'stats': { - 'platform': platform_stats, - 'merchant': merchant_stats - }, - 'list': result_list, - 'hasMore': has_more, - 'total': total_count, - 'currentPage': page, - 'pageSize': page_size - } - - return Response({ - 'code': 0, - 'message': '获取成功', - 'data': response_data - }) - - except Exception as e: - logger.error(f"获取订单列表异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - - - - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser -from rest_framework import status -from django.db import transaction -import logging - -# 获取日志器 -logger = logging.getLogger(__name__) - - -class AdYaoQingDaShou(APIView): - """ - 后台获取邀请码接口(用于邀请打手) - 权限: JWT Token认证 + 管理员身份验证 - 前端调用: POST /yonghu/adyqds - 参数: { - "zhanghao": "管理员账号" - } - 返回: { - "code": 0, - "message": "成功", - "data": { - "yaoqingma": "邀请码" - } - } - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - try: - # 1. 获取前端参数 - qianzhanghao = request.data.get('zhanghao', '').strip() - - if not qianzhanghao: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2. JWT Token身份验证 - dangqianyonghu = request.user - - # 3. 验证手机号是否匹配 - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 4. 验证用户类型是否为管理员 - if dangqianyonghu.user_type != 'admin': - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 5. 验证管理员扩展表是否存在 - try: - guanliyuan = dangqianyonghu.admin_profile - except AttributeError: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 6. 获取用户ID(yonghuid),如果获取不到就用'123456' - yonghuid_str = getattr(dangqianyonghu, 'yonghuid', '123456') - - # 7. 尝试获取或创建管事扩展表 - try: - # 尝试获取现有的管事扩展表 - guanshi_profile = dangqianyonghu.guanshi_profile - except AttributeError: - # 管事扩展表不存在,创建一个 - try: - from yonghu.models import UserGuanshi - - # 创建默认的管事扩展表 - guanshi_profile = UserGuanshi.objects.create( - user=dangqianyonghu, - yaoqingma='', # 暂时为空,后面生成 - dianhua='', # 默认空 - wechat='', # 默认空 - yaogingshuliang=0, # 邀请数量为0 - zhuangtai=1, # 正常状态 - jinrichongzhi=0, # 今日充值0 - jinyuechongzhi=0, # 今月充值0 - chongzhifenrun=0, # 充值分佣0 - yue=0 # 余额0 - ) - #logger.info(f"为管理员{dangqianyonghu.yonghuid}创建了管事扩展表") - except Exception as e: - logger.error(f"创建管事扩展表失败: {str(e)}") - return Response( - {'code': 500, 'message': '创建用户信息失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - # 8. 检查是否已有邀请码 - existing_yaoqingma = guanshi_profile.yaoqingma - - if existing_yaoqingma and existing_yaoqingma.strip(): - # 已有邀请码,直接返回 - return Response({ - 'code': 0, - 'message': '成功获取邀请码', - 'data': { - 'yaoqingma': existing_yaoqingma - } - }) - - # 9. 生成新的邀请码 - try: - # 导入邀请码生成工具 - from utils.yaoqingma_generator import chuangjianYaoqingma - - # 生成邀请码 - new_yaoqingma = chuangjianYaoqingma(str(yonghuid_str)) - - # 验证邀请码格式 - from utils.yaoqingma_generator import yanzhengYaoqingmaWeiyixing - if not yanzhengYaoqingmaWeiyixing(new_yaoqingma): - raise Exception('邀请码格式验证失败') - - except ImportError: - # 如果导入失败,使用简单方法生成邀请码 - import time - import random - import string - - timestamp = str(int(time.time())) - random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) - new_yaoqingma = f"GL{timestamp[-6:]}{random_str}" - - logger.warning(f"使用简单方法生成邀请码: {new_yaoqingma}") - except Exception as e: - logger.error(f"生成邀请码失败: {str(e)}") - return Response( - {'code': 500, 'message': '生成邀请码失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - # 10. 保存邀请码到数据库 - try: - with transaction.atomic(): - guanshi_profile.yaoqingma = new_yaoqingma - guanshi_profile.save() - - logger.info(f"管理员{dangqianyonghu.yonghuid}生成邀请码: {new_yaoqingma}") - except Exception as e: - logger.error(f"保存邀请码失败: {str(e)}") - return Response( - {'code': 500, 'message': '保存邀请码失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - # 11. 返回成功响应 - return Response({ - 'code': 0, - 'message': '成功生成邀请码', - 'data': { - 'yaoqingma': new_yaoqingma - } - }) - - except Exception as e: - logger.error(f"获取邀请码接口异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser -from rest_framework import status -from django.db.models import Q -import logging - -logger = logging.getLogger(__name__) - - -class AdGuanLiYongHu(APIView): - """ - 后台用户管理列表接口 - 权限: JWT Token认证 + 管理员身份验证 - 前端调用: POST /yonghu/adgl - 参数: { - "zhanghao": "管理员账号", - "user_type": "用户类型(1-4)", - "keyword": "搜索关键词(可选)", - "page": "页码(从1开始)", - "page_size": "每页数量" - } - 返回: { - "code": 0, - "message": "成功", - "data": { - "list": [用户列表], - "stats": {"1": 总数, "2": 总数, "3": 总数, "4": 总数}, - "has_more": true/false - } - } - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - try: - # 1. 获取前端参数 - qianzhanghao = request.data.get('zhanghao', '').strip() - user_type = request.data.get('user_type') - keyword = request.data.get('keyword', '').strip() - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 30)) - - # 验证参数 - if not qianzhanghao or not user_type: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2. JWT Token身份验证 - dangqianyonghu = request.user - - # 3. 验证手机号是否匹配 - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 4. 验证用户类型是否为管理员 - if dangqianyonghu.user_type != 'admin': - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 5. 验证管理员扩展表是否存在 - try: - guanliyuan = dangqianyonghu.admin_profile - except AttributeError: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 6. 根据用户类型选择查询的模型 - user_type_int = int(user_type) - - if user_type_int not in [1, 2, 3, 4]: - return Response( - {'code': 400, 'message': '用户类型错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 7. 统计四种用户类型的总数 - stats = self._get_user_stats() - - # 8. 根据用户类型查询用户列表 - user_list, has_more = self._query_users_by_type( - user_type_int, keyword, page, page_size - ) - - # 9. 构建返回数据 - return Response({ - 'code': 0, - 'message': '成功', - 'data': { - 'list': user_list, - 'stats': stats, - 'has_more': has_more - } - }) - - except ValueError: - logger.error("参数类型错误") - return Response( - {'code': 400, 'message': '参数类型错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - except Exception as e: - logger.error(f"用户管理列表接口异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - def _get_user_stats(self): - """ - 获取四种用户类型的统计数量 - """ - try: - from yonghu.models import UserBoss, UserDashou, UserGuanshi, UserShangjia - - stats = { - '1': UserBoss.objects.count(), - '2': UserDashou.objects.count(), - '3': UserGuanshi.objects.count(), - '4': UserShangjia.objects.count() - } - - return stats - - except Exception as e: - logger.error(f"获取用户统计失败: {str(e)}") - return {'1': 0, '2': 0, '3': 0, '4': 0} - - def _query_users_by_type(self, user_type_int, keyword, page, page_size): - """ - 根据用户类型查询用户列表 - 返回: (用户列表, 是否还有更多数据) - """ - try: - # 计算偏移量 - offset = (page - 1) * page_size - - if user_type_int == 1: - return self._query_boss_users(keyword, offset, page_size) - elif user_type_int == 2: - return self._query_dashou_users(keyword, offset, page_size) - elif user_type_int == 3: - return self._query_guanshi_users(keyword, offset, page_size) - elif user_type_int == 4: - return self._query_shangjia_users(keyword, offset, page_size) - else: - return [], False - - except Exception as e: - logger.error(f"查询用户列表失败: {str(e)}") - return [], False - - def _query_boss_users(self, keyword, offset, limit): - """查询普通用户(老板)""" - try: - from yonghu.models import UserBoss - - queryset = UserBoss.objects.select_related('user') - - if keyword: - queryset = queryset.filter( - Q(user__yonghuid__icontains=keyword) | - Q(nickname__icontains=keyword) - ) - - # 获取总数 - total_count = queryset.count() - - # 获取当前页的数据 - users = list(queryset.order_by('-create_time')[offset:offset + limit]) - - # 计算是否有更多数据 - current_count = len(users) - has_more = (offset + current_count) < total_count - - - - # 构建返回数据 - user_list = [] - for boss in users: - user_main = boss.user - - user_data = { - 'yonghuid': user_main.yonghuid, - 'avatar': user_main.avatar or '', - 'nicheng': boss.nickname or '', - } - - user_list.append(user_data) - - return user_list, has_more - - except Exception as e: - logger.error(f"查询老板用户失败: {str(e)}") - return [], False - - def _query_dashou_users(self, keyword, offset, limit): - """查询打手用户""" - try: - from yonghu.models import UserDashou - - queryset = UserDashou.objects.select_related('user') - - if keyword: - queryset = queryset.filter( - Q(user__yonghuid__icontains=keyword) | - Q(nicheng__icontains=keyword) | - Q(chenghao__icontains=keyword) - ) - - # 获取总数 - total_count = queryset.count() - - # 获取当前页的数据 - dashou_list = list(queryset.order_by('-create_time')[offset:offset + limit]) - - # 计算是否有更多数据 - current_count = len(dashou_list) - has_more = (offset + current_count) < total_count - - - - # 构建返回数据 - user_list = [] - for dashou in dashou_list: - user_main = dashou.user - - user_data = { - 'yonghuid': user_main.yonghuid, - 'avatar': user_main.avatar or '', - 'zaixianzhuangtai': dashou.zaixianzhuangtai, - 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, - 'nicheng': dashou.nicheng or '', - 'zhuangtai': dashou.zhuangtai, - } - - user_list.append(user_data) - - return user_list, has_more - - except Exception as e: - logger.error(f"查询打手用户失败: {str(e)}") - return [], False - - def _query_guanshi_users(self, keyword, offset, limit): - """查询管事用户""" - try: - from yonghu.models import UserGuanshi - - queryset = UserGuanshi.objects.select_related('user') - - if keyword: - queryset = queryset.filter( - Q(user__yonghuid__icontains=keyword) - ) - - # 获取总数 - total_count = queryset.count() - - # 获取当前页的数据 - guanshi_list = list(queryset.order_by('-create_time')[offset:offset + limit]) - - # 计算是否有更多数据 - current_count = len(guanshi_list) - has_more = (offset + current_count) < total_count - - - - # 构建返回数据 - user_list = [] - for guanshi in guanshi_list: - user_main = guanshi.user - - user_data = { - 'yonghuid': user_main.yonghuid, - 'avatar': user_main.avatar or '', - 'zhanghaozhuangtai': guanshi.zhuangtai, - 'nicheng': f'管事{user_main.yonghuid}', - } - - user_list.append(user_data) - - return user_list, has_more - - except Exception as e: - logger.error(f"查询管事用户失败: {str(e)}") - return [], False - - def _query_shangjia_users(self, keyword, offset, limit): - """查询商家用户""" - try: - from yonghu.models import UserShangjia - - queryset = UserShangjia.objects.select_related('user') - - if keyword: - queryset = queryset.filter( - Q(user__yonghuid__icontains=keyword) | - Q(nicheng__icontains=keyword) - ) - - # 获取总数 - total_count = queryset.count() - - # 获取当前页的数据 - shangjia_list = list(queryset.order_by('-create_time')[offset:offset + limit]) - - # 计算是否有更多数据 - current_count = len(shangjia_list) - has_more = (offset + current_count) < total_count - - - - # 构建返回数据 - user_list = [] - for shangjia in shangjia_list: - user_main = shangjia.user - - user_data = { - 'yonghuid': user_main.yonghuid, - 'avatar': user_main.avatar or '', - 'zhanghaozhuangtai': shangjia.zhuangtai, - 'nicheng': shangjia.nicheng or '', - } - - user_list.append(user_data) - - return user_list, has_more - - except Exception as e: - logger.error(f"查询商家用户失败: {str(e)}") - return [], False - - - - - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser -from rest_framework import status -from django.db.models import Q -import logging - -logger = logging.getLogger(__name__) - - -class CfGuanLi(APIView): - """ - 后台处罚管理列表接口 - 权限: JWT Token认证 + 管理员身份验证 - 前端调用: POST /yonghu/cfgl - 参数: { - "zhanghao": "管理员账号", - "qingqiu_tongji": true/false, # 是否只请求统计信息 - "sqzhuangtai": 0 或 [1,2], # 处罚申请状态筛选(可选) - "page": "页码(从1开始)", # 分页参数 - "page_size": "每页数量" # 分页参数 - } - 返回: { - "code": 0, - "message": "成功", - "data": { - "list": [处罚记录列表], - "zongshu": 总数, - "daichuli": 待处理数量, - "yichuli": 已处理数量, - "haiyougengduo": true/false - } - } - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - try: - # 1. 获取前端参数 - qianzhanghao = request.data.get('zhanghao', '').strip() - qingqiu_tongji = request.data.get('qingqiu_tongji', False) - sqzhuangtai = request.data.get('sqzhuangtai') - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 30)) - - # 验证参数 - if not qianzhanghao: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2. JWT Token身份验证 - dangqianyonghu = request.user - - # 3. 验证手机号是否匹配 - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 4. 验证用户类型是否为管理员 - if dangqianyonghu.user_type != 'admin': - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 5. 验证管理员扩展表是否存在 - try: - guanliyuan = dangqianyonghu.admin_profile - except AttributeError: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 6. 导入模型 - from dingdan.models import Chufajilu - - # 7. 获取统计信息(总是需要统计) - zongshu = Chufajilu.objects.count() - daichuli = Chufajilu.objects.filter(sqzhuangtai__in=[0, 3]).count() - yichuli = Chufajilu.objects.filter(sqzhuangtai__in=[1, 2]).count() - - # 8. 如果只请求统计信息,直接返回 - if qingqiu_tongji: - return Response({ - 'code': 0, - 'message': '成功', - 'data': { - 'zongshu': zongshu, - 'daichuli': daichuli, - 'yichuli': yichuli - } - }) - - # 9. 构建查询集 - queryset = Chufajilu.objects.all() - - # 10. 根据状态筛选 - if sqzhuangtai is not None: - if isinstance(sqzhuangtai, list): - # 如果是数组,表示多个状态 - queryset = queryset.filter(sqzhuangtai__in=sqzhuangtai) - else: - # 单个状态 - queryset = queryset.filter(sqzhuangtai=sqzhuangtai) - - # 11. 获取总数(用于分页计算) - total_count = queryset.count() - - # 12. 计算分页偏移量 - offset = (page - 1) * page_size - - # 13. 分页查询 - chufa_list = list(queryset.order_by('-create_time')[offset:offset + page_size]) - - # 🔴【新增】高效获取图片信息 - if chufa_list: - # 收集所有需要查询的组合 - dingdan_ids = [] - yonghu_combinations = [] # 保存 (dingdan_id, yonghuid) 组合 - - for chufa in chufa_list: - if chufa.dingdan_id: - dingdan_ids.append(chufa.dingdan_id) - # 商家图片组合 - if chufa.qingqiuid: - yonghu_combinations.append((chufa.dingdan_id, chufa.qingqiuid)) - # 打手图片组合 - if chufa.dashouid: - yonghu_combinations.append((chufa.dingdan_id, chufa.dashouid)) - - # 去重 - dingdan_ids = list(set(dingdan_ids)) - yonghu_combinations = list(set(yonghu_combinations)) - - # 🔴【新增】批量查询图片表(高效查询,避免循环) - tupian_mapping = {} - if yonghu_combinations: - # 导入图片模型 - from dingdan.models import Chufatupian - - # 构建查询条件 - from django.db.models import Q - query_conditions = Q() - for dingdan_id, yonghuid in yonghu_combinations: - query_conditions |= Q(dingdan_id=dingdan_id, yonghuid=yonghuid) - - # 批量查询 - tupian_queryset = Chufatupian.objects.filter(query_conditions).values( - 'dingdan_id', 'yonghuid', 'tupian' - ) - - # 构建映射:key为"dingdan_id_yonghuid",value为图片URL列表 - for item in tupian_queryset: - key = f"{item['dingdan_id']}_{item['yonghuid']}" - if key not in tupian_mapping: - tupian_mapping[key] = [] - if item['tupian']: - tupian_mapping[key].append(item['tupian']) - else: - tupian_mapping = {} - - # 🔴【修改】14. 构建返回数据 - 添加图片和申诉理由字段 - chufa_data = [] - for chufa in chufa_list: - # 🔴【新增】获取商家证据图片 - zhengju_tupian = [] - if chufa.dingdan_id and chufa.qingqiuid: - key = f"{chufa.dingdan_id}_{chufa.qingqiuid}" - zhengju_tupian = tupian_mapping.get(key, []) - - # 🔴【新增】获取打手申诉图片 - shensu_tupian = [] - if chufa.dingdan_id and chufa.dashouid: - key = f"{chufa.dingdan_id}_{chufa.dashouid}" - shensu_tupian = tupian_mapping.get(key, []) - - chufa_data.append({ - 'dashouid': chufa.dashouid, - 'qingqiuid': chufa.qingqiuid, - 'chuliid': chufa.chuliid, - 'cfliyou': chufa.cfliyou, - 'sqzhuangtai': chufa.sqzhuangtai, - 'bhliyou': chufa.bhliyou, - 'jifen': chufa.jifen, - 'dingdan_id': chufa.dingdan_id, - 'create_time': chufa.create_time, - 'update_time': chufa.update_time, - # 🔴【新增】申诉理由字段 - 'ssliyou': chufa.ssliyou or '', - # 🔴【新增】商家证据图片 - 'zhengju_tupian': zhengju_tupian, - # 🔴【新增】打手申诉图片 - 'shensu_tupian': shensu_tupian, - }) - - # 15. 计算是否还有更多数据 - current_count = len(chufa_list) - haiyougengduo = (offset + current_count) < total_count - - return Response({ - 'code': 0, - 'message': '成功', - 'data': { - 'list': chufa_data, - 'zongshu': zongshu, - 'daichuli': daichuli, - 'yichuli': yichuli, - 'haiyougengduo': haiyougengduo - } - }) - - except ValueError: - return Response( - {'code': 400, 'message': '参数类型错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - except Exception as e: - logger.error(f"处罚管理列表接口异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - - - - - - - - - - - - - - -logger = logging.getLogger(__name__) - - -class AdTongYiChuFa(APIView): - """ - 后台同意/驳回处罚接口(已修复重复记录问题) - 权限: JWT Token认证 + 管理员身份验证 - 前端调用: POST /yonghu/adtycf - 参数: { - "zhanghao": "管理员账号", - "dingdan_id": "订单ID", - "caozuo": 1或2, # 1=同意处罚,2=拒绝处罚 - "bohui_liyou": "拒绝理由" # 拒绝处罚时必填 - } - 返回: { - "code": 0, - "message": "成功", - "data": null - } - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - try: - # 1. 获取前端参数 - qianzhanghao = request.data.get('zhanghao', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - caozuo = request.data.get('caozuo') # 1=同意,2=拒绝 - bohui_liyou = request.data.get('bohui_liyou', '').strip() - - # 验证参数 - if not qianzhanghao or not dingdan_id or caozuo is None: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证操作类型 - if caozuo not in [1, 2]: - return Response( - {'code': 400, 'message': '操作类型错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 如果是拒绝处罚,需要拒绝理由 - if caozuo == 2 and not bohui_liyou: - return Response( - {'code': 400, 'message': '拒绝处罚需要填写理由', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2. JWT Token身份验证 - dangqianyonghu = request.user - - # 3. 验证手机号是否匹配 - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 4. 验证用户类型是否为管理员 - if dangqianyonghu.user_type != 'admin': - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 5. 验证管理员扩展表是否存在 - try: - guanliyuan = dangqianyonghu.admin_profile - except AttributeError: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 6. 使用事务保证数据一致性 - with transaction.atomic(): - # 7. 导入模型 - from dingdan.models import Chufajilu, Dingdan, DingdanShangjia - from yonghu.models import UserDashou - - # 8. 🔴 修复:查询处罚记录(处理重复记录问题) - # 使用 filter().first() 而不是 get(),以防出现多条记录 - chufajilu = Chufajilu.objects.filter( - dingdan_id=dingdan_id, - sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中状态 - ).order_by('-create_time').first() # 取最新的一条 - - if not chufajilu: - # 如果没有待处理的处罚记录,检查是否有已处理的 - exists_processed = Chufajilu.objects.filter(dingdan_id=dingdan_id).exists() - if exists_processed: - return Response( - {'code': 400, 'message': '该处罚记录已处理', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - else: - return Response( - {'code': 404, 'message': '处罚记录不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 9. 验证处罚记录状态(必须是待处理或申诉中状态才能处理) - if chufajilu.sqzhuangtai not in [0, 3]: # 🔴【修改】包括待处理(0)和申诉中(3) - return Response( - {'code': 400, 'message': '该处罚记录已处理', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 10. 🔴 修复:检查是否有重复的待处理/申诉中记录,并记录警告 - duplicate_count = Chufajilu.objects.filter( - dingdan_id=dingdan_id, - sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中 - ).count() - - if duplicate_count > 1: - # 记录警告日志 - logger.warning(f"订单ID {dingdan_id} 存在 {duplicate_count} 条待处理/申诉中处罚记录,处理最新的一条") - - # 🔴 可选:将其他重复的待处理/申诉中记录标记为已拒绝 - # 防止用户重复提交相同订单的处罚申请 - duplicate_records = Chufajilu.objects.filter( - dingdan_id=dingdan_id, - sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中 - ).exclude(id=chufajilu.id) - - for dup in duplicate_records: - dup.sqzhuangtai = 2 # 已拒绝 - dup.chuliid = qianzhanghao - dup.bhliyou = "系统自动拒绝:存在重复处罚记录" - dup.save() - - # 11. 根据操作类型处理 - if caozuo == 1: - # 同意处罚 - 不扣分,只改状态 - # 11.1 检查打手是否存在 - if not chufajilu.dashouid: - return Response( - {'code': 400, 'message': '被处罚打手ID不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 11.2 查询打手扩展表(只是验证存在性) - try: - dashou = UserDashou.objects.get(user__yonghuid=chufajilu.dashouid) - except UserDashou.DoesNotExist: - return Response( - {'code': 404, 'message': '被处罚打手不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 🔴【修改】11.3 同意处罚时不扣分(因为申请时已经扣了),只更新状态 - chufajilu.sqzhuangtai = 1 # 已处罚 - chufajilu.chuliid = qianzhanghao # 处理人ID为管理员账号 - chufajilu.save() - - # 11.4 更新商家订单扩展表 - try: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) - if hasattr(dingdan, 'shangjia_kuozhan'): - shangjia_kuozhan = dingdan.shangjia_kuozhan - shangjia_kuozhan.sqzhuangtai = 1 # 已处罚 - shangjia_kuozhan.save() - except (Dingdan.DoesNotExist, AttributeError): - # 如果订单或扩展表不存在,继续执行,不中断 - pass - - logger.info(f"同意处罚成功:订单ID={dingdan_id},打手ID={chufajilu.dashouid}") - - elif caozuo == 2: - # 拒绝处罚 - 需要返还积分(加分) - # 🔴【新增】11.5 返还打手积分(加5分) - if chufajilu.jifen > 0: - try: - dashou = UserDashou.objects.get(user__yonghuid=chufajilu.dashouid) - - # 🔴【新增】安全验证:必须积分小于等于5分才允许加分 - if dashou.jifen <= 5: # 判断积分是否小于等于5分 - dashou.jifen += chufajilu.jifen # 加5分 - dashou.save() - logger.info( - f"拒绝处罚,给打手{dashou.user.yonghuid}返还积分{chufajilu.jifen}分,当前积分{dashou.jifen}") - else: - # 积分已超过5分,可能是异常情况,记录警告但不中断处理 - logger.warning( - f"拒绝处罚时打手积分异常:打手ID={chufajilu.dashouid},当前积分{dashou.jifen}已超过5分,不再加分") - except UserDashou.DoesNotExist: - # 打手不存在,记录错误,但继续处理状态 - logger.error(f"拒绝处罚时打手不存在:打手ID={chufajilu.dashouid}") - - # 11.6 更新处罚记录 - chufajilu.sqzhuangtai = 2 # 已拒绝 - chufajilu.chuliid = qianzhanghao # 处理人ID为管理员账号 - chufajilu.bhliyou = bohui_liyou # 拒绝理由 - chufajilu.save() - - # 11.7 更新商家订单扩展表 - try: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) - if hasattr(dingdan, 'shangjia_kuozhan'): - shangjia_kuozhan = dingdan.shangjia_kuozhan - shangjia_kuozhan.sqzhuangtai = 2 # 已拒绝 - shangjia_kuozhan.bhliyou = bohui_liyou # 拒绝理由 - shangjia_kuozhan.save() - except (Dingdan.DoesNotExist, AttributeError): - # 如果订单或扩展表不存在,继续执行,不中断 - pass - - logger.info(f"拒绝处罚成功:订单ID={dingdan_id},拒绝理由={bohui_liyou}") - - # 12. 返回成功响应 - return Response({ - 'code': 0, - 'message': '处理成功', - 'data': None - }) - - except ValueError: - return Response( - {'code': 400, 'message': '参数类型错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - except Exception as e: - logger.error(f"处理处罚接口异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - - - -# yonghu/views.py -from django.utils import timezone -from datetime import timedelta -from decimal import Decimal -from django.db import transaction -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework import status -from .models import ( - UserMain, UserBoss, UserDashou, UserShangjia, UserGuanshi, - AdminProfile, Xiugaijilu -) -from shangpin.models import Huiyuan, Huiyuangoumai - - -class AdckyhxqView(APIView): - """ - 获取用户详情接口 - 前端需要传递: zhanghao, uid, user_type - 返回用户信息和会员列表 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # ==================== 管理员身份验证 ==================== - # 1. 验证JWT Token - if not request.user.is_authenticated: - return Response({ - 'code': 401, - 'message': '未认证', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 验证用户类型为管理员 - if request.user.user_type != 'admin': - return Response({ - 'code': 403, - 'message': '权限不足', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 3. 验证管理员扩展表存在 - admin_profile = getattr(request.user, 'admin_profile', None) - if not admin_profile: - return Response({ - 'code': 403, - 'message': '管理员信息不完整', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 4. 验证管理员账号(phone)与前端传递的zhanghao一致 - zhanghao = request.data.get('zhanghao') - if not zhanghao or request.user.phone != zhanghao: - return Response({ - 'code': 401, - 'message': '账号验证失败', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - # ==================== 身份验证结束 ==================== - - # 获取业务参数 - uid = request.data.get('uid') # 用户ID (yonghuid) - user_type = request.data.get('user_type') # 用户类型 (1,2,3,4) - - if not uid or not user_type: - return Response({ - 'code': 400, - 'message': '参数缺失', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证用户类型 - try: - user_type = int(user_type) - if user_type not in [1, 2, 3, 4]: - raise ValueError - except ValueError: - return Response({ - 'code': 400, - 'message': '用户类型无效', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 查询用户主表 - 使用select_related优化查询 - user_main = UserMain.objects.filter(yonghuid=uid).first() - if not user_main: - return Response({ - 'code': 404, - 'message': '用户不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 构建响应数据 - response_data = { - 'user_info': {}, - 'member_list': [], - 'all_member_list': [] - } - - # 通用字段(所有用户都有) - user_info = { - 'yonghuid': user_main.yonghuid, - 'avatar': user_main.avatar, - 'phone': user_main.phone, - 'ip': user_main.ip, - 'create_time': user_main.create_time, - 'update_time': user_main.update_time, - 'user_type': user_type - } - - # 根据用户类型查询扩展信息 - if user_type == 1: # 老板 - # 使用select_related预取boss_profile - user_main = UserMain.objects.filter(yonghuid=uid).select_related('boss_profile').first() - if user_main.boss_profile: - boss_profile = user_main.boss_profile - user_info.update({ - 'nickname': boss_profile.nickname, - 'zonge': boss_profile.zonge, - 'alldingdan': boss_profile.alldingdan, - 'alltui': boss_profile.alltui, - 'create_time_boss': boss_profile.create_time - }) - - elif user_type == 2: # 打手 - # 使用select_related预取dashou_profile - user_main = UserMain.objects.filter(yonghuid=uid).select_related('dashou_profile').first() - if user_main.dashou_profile: - dashou_profile = user_main.dashou_profile - - # 构建打手信息 - user_info.update({ - 'nicheng': dashou_profile.nicheng, - 'chenghao': dashou_profile.chenghao, - 'zhuangtai': dashou_profile.zhuangtai, - 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, - 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai, - 'jieshao': dashou_profile.jieshao, - 'jiedanzongliang': dashou_profile.jiedanzongliang, - 'chengjiaozongliang': dashou_profile.chengjiaozongliang, - 'tuikuanliang': dashou_profile.tuikuanliang, - 'yue': dashou_profile.yue, # 打手可提现余额 - 'zonge': dashou_profile.zonge, # 打手赚取总额 - 'dianhua': dashou_profile.dianhua, - 'wechat': dashou_profile.wechat, - 'yaoqingren': dashou_profile.yaoqingren, - 'jinrijiedan': dashou_profile.jinrijiedan, - 'jinrishouyi': dashou_profile.jinrishouyi, - 'jinyuejiedan': dashou_profile.jinyuejiedan, - 'jinyueshouyi': dashou_profile.jinyueshouyi, - 'jifen': dashou_profile.jifen, - 'yajin': dashou_profile.yajin, - 'create_time_dashou': dashou_profile.create_time - }) - - # 查询打手的会员信息(只返回未过期的) - # 使用select_related预取huiyuan信息(如果有关联的话) - member_records = Huiyuangoumai.objects.filter( - yonghu_id=uid - ).order_by('-create_time') - - member_list = [] - for record in member_records: - # 调用方法检查是否过期,并更新状态 - is_expired = record.jiance_shifou_daoqi() - - if not is_expired: # 只返回未过期的 - # 查询会员详情 - huiyuan = Huiyuan.objects.filter( - huiyuan_id=record.huiyuan_id - ).first() - - if huiyuan: - member_list.append({ - 'huiyuan_id': record.huiyuan_id, - 'huiyuan_name': huiyuan.jieshao, - 'daoqi_time': record.daoqi_time, - 'is_active': record.huiyuan_zhuangtai == 1, - 'create_time': record.create_time - }) - - response_data['member_list'] = member_list - - elif user_type == 3: # 管事 - # 使用select_related预取guanshi_profile - user_main = UserMain.objects.filter(yonghuid=uid).select_related('guanshi_profile').first() - if user_main.guanshi_profile: - guanshi_profile = user_main.guanshi_profile - user_info.update({ - 'nicheng': user_main.phone, # 管事昵称用手机号 - 'yaoqingma': guanshi_profile.yaoqingma, - 'dianhua': guanshi_profile.dianhua, - 'wechat': guanshi_profile.wechat, - 'yaogingshuliang': guanshi_profile.yaogingshuliang, - 'zhuangtai': guanshi_profile.zhuangtai, - 'jinrichongzhi': guanshi_profile.jinrichongzhi, - 'jinyuechongzhi': guanshi_profile.jinyuechongzhi, - 'chongzhifenrun': guanshi_profile.chongzhifenrun, - 'yue': guanshi_profile.yue, # 管事可提现余额 - 'create_time_guanshi': guanshi_profile.create_time - }) - - elif user_type == 4: # 商家 - # 使用select_related预取shop_profile - user_main = UserMain.objects.filter(yonghuid=uid).select_related('shop_profile').first() - if user_main.shop_profile: - shop_profile = user_main.shop_profile - user_info.update({ - 'nicheng': shop_profile.nicheng, - 'zhuangtai': shop_profile.zhuangtai, - 'dianhua': shop_profile.dianhua, - 'wechat': shop_profile.wechat, - 'fabu': shop_profile.fabu, - 'tuikuan': shop_profile.tuikuan, - 'yue': shop_profile.yue, # 商家余额 - 'chengjiao': shop_profile.chengjiao, - 'jinridingdan': shop_profile.jinridingdan, - 'jinriliushui': shop_profile.jinriliushui, - 'jinyuedingdan': shop_profile.jinyuedingdan, - 'jinyueliushui': shop_profile.jinyueliushui, - 'create_time_shop': shop_profile.create_time - }) - - # 获取所有会员列表(用于前端下拉选择) - all_huiyuan = Huiyuan.objects.all().order_by('jiage') - all_member_list = [ - { - 'id': huiyuan.huiyuan_id, - 'nicheng': huiyuan.jieshao, - 'jiage': huiyuan.jiage, - 'jieshao': huiyuan.jtjieshao - } - for huiyuan in all_huiyuan - ] - - response_data['user_info'] = user_info - response_data['all_member_list'] = all_member_list - - return Response({ - 'code': 0, - 'message': '获取成功', - 'data': response_data - }, status=status.HTTP_200_OK) - - except Exception as e: - return Response({ - 'code': 500, - 'message': f'获取用户详情失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -class AdcjxgView(APIView): - """ - 修改用户信息接口 - 前端需要传递: zhanghao, uid, user_type, 以及其他修改的字段 - 支持打手会员添加功能 - 敏感字段修改会记录到修改记录表 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # ==================== 管理员身份验证 ==================== - if not request.user.is_authenticated: - return Response({ - 'code': 401, - 'message': '未认证', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - if request.user.user_type != 'admin': - return Response({ - 'code': 403, - 'message': '权限不足', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - admin_profile = getattr(request.user, 'admin_profile', None) - if not admin_profile: - return Response({ - 'code': 403, - 'message': '管理员信息不完整', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - zhanghao = request.data.get('zhanghao') - if not zhanghao or request.user.phone != zhanghao: - return Response({ - 'code': 401, - 'message': '账号验证失败', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - # ==================== 身份验证结束 ==================== - - # 获取业务参数 - uid = request.data.get('uid') - user_type = request.data.get('user_type') - - if not uid or not user_type: - return Response({ - 'code': 400, - 'message': '参数缺失', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - try: - user_type = int(user_type) - if user_type not in [1, 2, 3, 4]: - raise ValueError - except ValueError: - return Response({ - 'code': 400, - 'message': '用户类型无效', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 查询用户主表 - user_main = UserMain.objects.filter(yonghuid=uid).first() - if not user_main: - return Response({ - 'code': 404, - 'message': '用户不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 使用事务确保数据一致性 - with transaction.atomic(): - # 存储修改记录的数据 - modify_record_data = { - 'yonghuid': uid, - 'xiugaiid': zhanghao, - 'leixing': user_type, - 'qitashuoming': '', - 'create_time': timezone.now() - } - - # 标记是否有敏感字段被修改 - has_sensitive_change = False - - if user_type == 1: # 老板 - 不允许修改 - return Response({ - 'code': 403, - 'message': '老板信息不允许修改', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - elif user_type == 2: # 打手 - # 获取或创建打手扩展记录 - dashou_profile, created = UserDashou.objects.get_or_create( - user=user_main, - defaults={'nicheng': user_main.phone or f'打手{uid}'} - ) - - # 记录敏感字段的旧值 - old_yue = dashou_profile.yue # 打手可提现余额 - old_zonge = dashou_profile.zonge # 打手提现金额 - old_jifen = dashou_profile.jifen # 打手积分 - old_yajin = dashou_profile.yajin # 打手押金 - - # 需要更新的字段列表 - update_fields = [ - 'chenghao', 'dianhua', 'yue', 'zonge', 'jifen', - 'yajin', 'yaoqingren', 'jieshao', 'zhuangtai', - 'zaixianzhuangtai', 'zhanghaozhuangtai' - ] - - # 记录被修改的字段 - modified_fields = [] - - for field in update_fields: - if field in request.data: - new_value = request.data.get(field) - old_value = getattr(dashou_profile, field) - - # 检查值是否改变 - if str(new_value) != str(old_value): - # 处理特殊字段类型 - if field in ['yue', 'zonge', 'yajin']: - new_value = Decimal(str(new_value)) - setattr(dashou_profile, field, new_value) - elif field in ['jifen']: - new_value = int(new_value) - setattr(dashou_profile, field, new_value) - elif field in ['zhuangtai', 'zaixianzhuangtai', 'zhanghaozhuangtai']: - new_value = int(new_value) - setattr(dashou_profile, field, new_value) - else: - setattr(dashou_profile, field, new_value) - - modified_fields.append(field) - - # 记录敏感字段修改 - if field == 'yue': - has_sensitive_change = True - modify_record_data['xiugaitijiao'] = new_value # 使用xiugaitijiao字段记录打手余额修改 - modify_record_data['xiugaitijiaoq'] = old_yue - elif field == 'zonge': - has_sensitive_change = True - # zonge字段在修改记录表中可能没有直接对应,可以记录在备注中 - modify_record_data['qitashuoming'] += f' 打手总额从{old_zonge}修改为{new_value}' - elif field == 'jifen': - has_sensitive_change = True - modify_record_data['jifen'] = new_value - modify_record_data['yjifen'] = old_jifen - elif field == 'yajin': - has_sensitive_change = True - modify_record_data['yajin'] = new_value - modify_record_data['yyajin'] = old_yajin - - # 保存打手信息 - dashou_profile.save() - - # 记录被修改的非敏感字段 - non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'zonge', 'jifen', 'yajin']] - if non_sensitive_fields: - modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}' - - elif user_type == 3: # 管事 - # 获取或创建管事扩展记录 - guanshi_profile, created = UserGuanshi.objects.get_or_create( - user=user_main, - defaults={'yaoqingma': f'GS{uid[-4:]}'} - ) - - # 记录敏感字段的旧值 - old_yue = guanshi_profile.yue # 管事可提现余额 - - update_fields = [ - 'yaogingshuliang', 'chongzhifenrun', 'yue', - 'zhuangtai', 'dianhua', 'wechat' - ] - - modified_fields = [] - - for field in update_fields: - if field in request.data: - new_value = request.data.get(field) - old_value = getattr(guanshi_profile, field) - - if str(new_value) != str(old_value): - if field in ['chongzhifenrun', 'yue']: - new_value = Decimal(str(new_value)) - setattr(guanshi_profile, field, new_value) - elif field in ['yaogingshuliang']: - new_value = int(new_value) - setattr(guanshi_profile, field, new_value) - elif field in ['zhuangtai']: - new_value = int(new_value) - setattr(guanshi_profile, field, new_value) - else: - setattr(guanshi_profile, field, new_value) - - modified_fields.append(field) - - # 记录敏感字段修改 - if field == 'yue': - has_sensitive_change = True - modify_record_data['guanshiyue'] = new_value - modify_record_data['guanshiyueq'] = old_yue - elif field == 'chongzhifenrun': - has_sensitive_change = True - modify_record_data['qitashuoming'] += f' 管事分佣从{old_value}修改为{new_value}' - - guanshi_profile.save() - - # 记录非敏感字段 - non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'chongzhifenrun']] - if non_sensitive_fields: - modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}' - - elif user_type == 4: # 商家 - # 获取或创建商家扩展记录 - shop_profile, created = UserShangjia.objects.get_or_create( - user=user_main, - defaults={'nicheng': user_main.phone or f'商家{uid}'} - ) - - # 记录敏感字段的旧值 - old_yue = shop_profile.yue # 商家余额 - - update_fields = ['yue', 'zhuangtai', 'dianhua', 'wechat'] - - modified_fields = [] - - for field in update_fields: - if field in request.data: - new_value = request.data.get(field) - old_value = getattr(shop_profile, field) - - if str(new_value) != str(old_value): - if field == 'yue': - new_value = Decimal(str(new_value)) - setattr(shop_profile, field, new_value) - elif field == 'zhuangtai': - new_value = int(new_value) - setattr(shop_profile, field, new_value) - else: - setattr(shop_profile, field, new_value) - - modified_fields.append(field) - - # 记录敏感字段修改 - if field == 'yue': - has_sensitive_change = True - modify_record_data['shangjiayue'] = new_value - modify_record_data['shangjiayueq'] = old_yue - - shop_profile.save() - - # 记录非敏感字段 - non_sensitive_fields = [f for f in modified_fields if f != 'yue'] - if non_sensitive_fields: - modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}' - - # 处理打手会员添加(只有打手可以添加会员) - - # 🔥🔥🔥 后端修改:支持正负数会员天数 - # 修改会员处理逻辑部分 - - if user_type == 2 and 'huiyuan_id' in request.data and 'days' in request.data: - huiyuan_id = request.data.get('huiyuan_id') - days = int(request.data.get('days', 0)) - - # 🔥 修改:允许正负数,但不能为0 - if huiyuan_id and days != 0: - # 🔥 修改:检查绝对值范围 - if abs(days) < 1 or abs(days) > 10000: - return Response({ - 'code': 400, - 'message': '会员天数必须在1-10000之间(正数增加,负数减少)', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 检查会员是否存在 - huiyuan = Huiyuan.objects.filter(huiyuan_id=huiyuan_id).first() - if not huiyuan: - return Response({ - 'code': 404, - 'message': '会员不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 检查是否已购买该会员 - existing_record = Huiyuangoumai.objects.filter( - yonghu_id=uid, - huiyuan_id=huiyuan_id - ).first() - - if existing_record: - # 🔥🔥🔥 重要修改:支持减少天数 - # 计算新的到期时间 - new_daoqi_time = existing_record.daoqi_time + timedelta(days=days) - - # 🔥 安全验证:减少天数后不能小于当前时间前90天(防止过度减少) - # 这个限制可以根据业务调整 - min_allowed_time = timezone.now() - timedelta(days=90) - if new_daoqi_time < min_allowed_time: - return Response({ - 'code': 400, - 'message': f'减少天数过多,调整后到期时间不能早于 {(min_allowed_time + timedelta(days=90)).strftime("%Y-%m-%d")}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - existing_record.daoqi_time = new_daoqi_time - existing_record.save() - - # 记录会员操作 - has_sensitive_change = True - modify_record_data['huiyuants'] = days - modify_record_data['huiyuan_id'] = huiyuan_id - - if days > 0: - modify_record_data['qitashuoming'] += f' 续费会员 {huiyuan.jieshao} {days}天' - # 只有增加天数时才增加购买次数 - huiyuan.goumai_cishu += 1 - huiyuan.save() - else: - modify_record_data['qitashuoming'] += f' 减少会员 {huiyuan.jieshao} {abs(days)}天' - # 减少天数时不增加购买次数 - - else: - # 🔥 修改:新购买时只能增加天数 - if days > 0: - daoqi_time = timezone.now() + timedelta(days=days) - new_record = Huiyuangoumai.objects.create( - huiyuan_id=huiyuan_id, - yonghu_id=uid, - jieshao=huiyuan.jieshao, - daoqi_time=daoqi_time - ) - - # 记录会员购买 - has_sensitive_change = True - modify_record_data['huiyuants'] = days - modify_record_data['huiyuan_id'] = huiyuan_id - modify_record_data['qitashuoming'] += f' 购买会员 {huiyuan.jieshao} {days}天' - - # 更新会员购买次数 - huiyuan.goumai_cishu += 1 - huiyuan.save() - else: - # 🔥 不能为不存在的会员减少天数 - return Response({ - 'code': 400, - 'message': '用户未购买该会员,无法减少天数', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - - - - - - - - - - - # 如果有敏感字段被修改或会员操作,创建修改记录 - if has_sensitive_change or 'huiyuan_id' in request.data: - # 创建修改记录 - Xiugaijilu.objects.create(**modify_record_data) - elif modify_record_data.get('qitashuoming', '').strip(): - # 只有非敏感字段修改,也记录但qitashuoming不同 - Xiugaijilu.objects.create(**modify_record_data) - - return Response({ - 'code': 0, - 'message': '修改成功', - 'data': None - }, status=status.HTTP_200_OK) - - except Exception as e: - return Response({ - 'code': 500, - 'message': f'修改失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -# yonghu/views.py -from django.utils import timezone -from django.db.models import Q -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework import status -from .models import UserMain, AdminProfile, Tixianjilu - - -# 管理员认证装饰器 -def admin_required(view_func): - """管理员权限验证装饰器""" - - def wrapped_view(self, request, *args, **kwargs): - try: - # 1. 验证JWT Token - if not request.user.is_authenticated: - return Response({ - 'code': 401, - 'message': '未认证', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 验证用户类型为管理员 - if request.user.user_type != 'admin': - return Response({ - 'code': 403, - 'message': '权限不足', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 3. 验证管理员扩展表存在 - admin_profile = getattr(request.user, 'admin_profile', None) - if not admin_profile: - return Response({ - 'code': 403, - 'message': '管理员信息不完整', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 4. 验证管理员账号(phone)与前端传递的zhanghao一致 - zhanghao = request.data.get('zhanghao') - if not zhanghao or request.user.phone != zhanghao: - return Response({ - 'code': 401, - 'message': '账号验证失败', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - return view_func(self, request, *args, **kwargs) - except Exception as e: - return Response({ - 'code': 500, - 'message': f'权限验证异常: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - return wrapped_view - - -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger - -from django.utils import timezone -from django.db.models import Q -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework import status -from yonghu.models import Tixianjilu - - -class AddtxshView(APIView): - """ - 获取提现审核列表接口 - 与用户管理页面保持一致的分页逻辑 - 支持按用户ID搜索,排序改为按申请时间升序(最早申请排最前) - """ - permission_classes = [IsAuthenticated] - - @admin_required - def post(self, request): - try: - # 获取参数 - zhanghao = request.data.get('zhanghao') - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 5)) - zhuangtai = int(request.data.get('zhuangtai', 1)) - leixing = int(request.data.get('leixing', 1)) - fangshi = request.data.get('fangshi') - - # 🔴【新增】获取搜索参数(用户ID) - search_uid = request.data.get('search_uid', '').strip() - - if page < 1: - page = 1 - - # 计算偏移量(与用户管理页面完全一致) - offset = (page - 1) * page_size - - # 构建查询条件 - query = Q() - - # 状态筛选 - if zhuangtai == 1: - query &= Q(zhuangtai=1) - elif zhuangtai == 2: - query &= Q(zhuangtai__in=[2, 3]) - - # 类型筛选 - query &= Q(leixing=leixing) - - # 收款方式筛选(可选) - if fangshi: - query &= Q(fangshi=int(fangshi)) - - # 🔴【新增】如果搜索了用户ID,添加条件 - if search_uid: - query &= Q(yonghuid=search_uid) - - # 🔴【修改】排序改为按申请时间升序(最早申请排最前) - queryset = Tixianjilu.objects.filter(query).order_by('create_time') - - # 🔴【修改】分页逻辑:如果是搜索模式,不分页,返回所有匹配记录 - if search_uid: - records = list(queryset) - total_count = len(records) - has_more = False # 搜索模式没有更多数据 - else: - total_count = queryset.count() - records = list(queryset[offset:offset + page_size]) - current_count = len(records) - has_more = (offset + current_count) < total_count - - # 获取状态统计(统计与搜索无关,依然按筛选条件统计) - status_counts = { - 'awaiting': Tixianjilu.objects.filter( - zhuangtai=1, - leixing=leixing - ).count(), - 'processed': Tixianjilu.objects.filter( - zhuangtai__in=[2, 3], - leixing=leixing - ).count() - } - - # 构建响应数据 - withdrawal_list = [] - for record in records: - withdrawal_list.append({ - 'id': record.id, - 'yonghuid': record.yonghuid, - 'avatar': record.avatar, - 'phone': record.phone, - 'nicheng': record.nicheng, - 'leixing': record.leixing, - 'zhifu': record.zhifu, - 'skzhanghao': record.skzhanghao, - 'jine': str(record.jine) if record.jine else '0.00', - 'zhuangtai': record.zhuangtai, - 'fangshi': record.fangshi, - 'shenheid': record.shenheid, - 'bhliyou': record.bhliyou, - 'create_time': record.create_time, - 'update_time': record.update_time - }) - - return Response({ - 'code': 0, - 'message': '获取成功', - 'data': { - 'list': withdrawal_list, - 'total_count': total_count, - 'status_counts': status_counts, - 'has_more': has_more - } - }, status=status.HTTP_200_OK) - - except Exception as e: - return Response({ - 'code': 500, - 'message': f'获取提现列表失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from django.db import transaction -from django.utils import timezone -from peizhi.models import Szjilu # 添加收支记录表导入 - - -# 假设你的提现记录模型导入 -# from your_app.models import Tixianjilu - -class AdtixianqkView(APIView): - """ - 处理提现申请接口(已更新:添加收支记录表更新) - """ - permission_classes = [IsAuthenticated] - - @admin_required # 假设你有这个装饰器 - def post(self, request): - try: - zhanghao = request.data.get('zhanghao') - tixian_id = request.data.get('tixian_id') - result = int(request.data.get('result', 0)) - reason = request.data.get('reason', '') - - if not tixian_id: - return Response({ - 'code': 400, - 'message': '提现记录ID不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if result not in [2, 3]: - return Response({ - 'code': 400, - 'message': '操作结果无效', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 如果是拒绝,需要理由 - if result == 3 and not reason: - return Response({ - 'code': 400, - 'message': '拒绝提现需要提供理由', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 获取提现记录 - try: - withdrawal = Tixianjilu.objects.get(id=tixian_id) - except Tixianjilu.DoesNotExist: - return Response({ - 'code': 404, - 'message': '提现记录不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 检查状态,只有待审核的可以处理 - if withdrawal.zhuangtai != 1: - return Response({ - 'code': 400, - 'message': '该提现申请已被处理', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 使用事务确保数据一致性 - with transaction.atomic(): - # 更新提现记录 - withdrawal.zhuangtai = result - withdrawal.shenheid = request.user.yonghuid - - if result == 3: # 拒绝 - withdrawal.bhliyou = reason - - withdrawal.update_time = timezone.now() - withdrawal.save() - - # 🟢 新增:如果同意提现(result=2),更新收支记录表 - if result == 2: - try: - # 查询收支记录表,id=1的记录 - szjilu_record = Szjilu.objects.get(id=1) - - # 获取提现金额 - tixian_jine = withdrawal.jine # 假设提现记录模型中有jine字段 - - from dingdan.utils import update_daily_payout - update_daily_payout(tixian_jine) - - # 更新收支记录 - szjilu_record.zongsy -= tixian_jine # 总收益减去提现金额 - szjilu_record.zongzc += tixian_jine # 总支出加上提现金额 - szjilu_record.jrzc += tixian_jine # 今日支出加上提现金额 - szjilu_record.update_time = timezone.now() - szjilu_record.save() - - except Szjilu.DoesNotExist: - # 如果收支记录表不存在id=1的记录,创建一个 - szjilu_record = Szjilu.objects.create( - id=1, - zongsy=-tixian_jine if result == 2 else 0, - zongzc=tixian_jine if result == 2 else 0, - jrzc=tixian_jine if result == 2 else 0 - ) - except Exception as e: - # 如果更新收支记录失败,抛出异常,事务会回滚 - raise Exception(f"更新收支记录失败: {str(e)}") - - return Response({ - 'code': 0, - 'message': '处理成功', - 'data': { - 'id': withdrawal.id, - 'zhuangtai': withdrawal.zhuangtai, - 'shenheid': withdrawal.shenheid, - 'update_time': withdrawal.update_time - } - }, status=status.HTTP_200_OK) - - except Exception as e: - return Response({ - 'code': 500, - 'message': f'处理提现失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - -# views.py (yonghu应用) -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework import status -from .models import UserMain, UserDashou -import logging - -logger = logging.getLogger(__name__) - - -class DashouZiliaoHuoquView(APIView): - """ - 打手资料获取接口 - URL: /yonghu/dszjxxhq - 方法: POST - 认证: JWT Token - 返回: 打手扩展表信息 - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 使用select_related高效查询,避免N+1问题 - dashou = UserDashou.objects.select_related('user').get(user=request.user) - - # 构建返回数据 - data = { - 'nicheng': dashou.nicheng if dashou.nicheng else '', - 'jieshao': dashou.jieshao if dashou.jieshao else '', - 'dianhua': dashou.dianhua if dashou.dianhua else '', - 'wechat': dashou.wechat if dashou.wechat else '', - 'uid': request.user.yonghuid, # 从主表获取UID - 'avatar': request.user.avatar if request.user.avatar else '', # 头像相对URL - } - - return Response({ - 'code': 200, - 'msg': 'success', - 'data': data - }, status=status.HTTP_200_OK) - - except UserDashou.DoesNotExist: - logger.warning(f'打手扩展表不存在: user_id={request.user.id}') - return Response({ - 'code': 404, - 'msg': '打手信息不存在' - }, status=status.HTTP_404_NOT_FOUND) - - except Exception as e: - logger.error(f'获取打手资料异常: {str(e)}', exc_info=True) - return Response({ - 'code': 500, - 'msg': '服务器内部错误' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -# views.py (yonghu应用) -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework import status -from .models import UserDashou -import re -import logging - -logger = logging.getLogger(__name__) - - -class DashouGengxinView(APIView): - """ - 打手信息更新接口 - URL: /yonghu/dsgxxx - 方法: POST - 认证: JWT Token - 参数: nicheng(昵称), dianhua(电话), wechat(微信), jieshao(介绍) - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 获取请求数据 - nicheng = request.data.get('nicheng', '').strip() - dianhua = request.data.get('dianhua', '').strip() - wechat = request.data.get('wechat', '').strip() - jieshao = request.data.get('jieshao', '').strip() - - # 数据验证 - validation_errors = self.validate_data(nicheng, dianhua, wechat, jieshao) - if validation_errors: - return Response({ - 'code': 400, - 'msg': validation_errors - }, status=status.HTTP_400_BAD_REQUEST) - - # 获取打手扩展表 - try: - dashou = UserDashou.objects.get(user=request.user) - except UserDashou.DoesNotExist: - return Response({ - 'code': 404, - 'msg': '打手信息不存在' - }, status=status.HTTP_404_NOT_FOUND) - - # 更新数据 - update_fields = [] - - if nicheng is not None: - dashou.nicheng = nicheng if nicheng != '' else None - update_fields.append('nicheng') - - if dianhua is not None: - dashou.dianhua = dianhua if dianhua != '' else None - update_fields.append('dianhua') - - if wechat is not None: - dashou.wechat = wechat if wechat != '' else None - update_fields.append('wechat') - - if jieshao is not None: - dashou.jieshao = jieshao if jieshao != '' else None - update_fields.append('jieshao') - - # 保存更新(只更新修改的字段) - if update_fields: - dashou.save(update_fields=update_fields) - - return Response({ - 'code': 200, - 'msg': '更新成功', - 'data': { - 'nicheng': dashou.nicheng or '', - 'dianhua': dashou.dianhua or '', - 'wechat': dashou.wechat or '', - 'jieshao': dashou.jieshao or '' - } - }, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f'更新打手信息异常: {str(e)}', exc_info=True) - return Response({ - 'code': 500, - 'msg': '服务器内部错误' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def validate_data(self, nicheng, dianhua, wechat, jieshao): - """数据验证方法""" - errors = [] - - # 1. 昵称验证(必填) - if not nicheng: - errors.append('昵称不能为空') - elif len(nicheng) > 20: - errors.append('昵称不能超过20个字符') - - # 2. 手机号验证(非必填,但填写时必须正确) - if dianhua: - # 手机号正则:1开头,11位数字 - phone_pattern = r'^1[3-9]\d{9}$' - if not re.match(phone_pattern, dianhua): - errors.append('手机号格式不正确') - - # 3. 微信号验证(非必填) - if wechat and len(wechat) > 30: - errors.append('微信号不能超过30个字符') - - # 4. 个人介绍验证 - if jieshao and len(jieshao) > 200: - errors.append('个人介绍不能超过200个字符') - - return '; '.join(errors) if errors else None - - - - - - - - -class ShangjiaZhuceView(APIView): - """ - 商家注册接口 (通过管理员邀请码) - 访问路径: /api/yonghu/shangjiahuce/ - 请求方法: POST - 权限要求: JWT Token认证通过的用户 - 请求体: { "inviteCode": "管理员邀请码" } - - 返回字段说明: - - code: 状态码(200成功,其他失败) - - message: 提示信息 - - data: 商家信息数据,包含以下字段(与前端商家页面完全一致): - - nicheng: 商家昵称 - - sjyue: 商家余额(字符串,保留两位小数) - - fabu: 发布订单总数 - - chengjiao: 成交订单总数 - - tuikuan: 退款订单总数 - - jinridingdan: 今日订单 - - jinriliushui: 今日流水(字符串) - - jinyuedingdan: 今月订单 - - jinyueliushui: 今月流水(字符串) - - zhuangtai: 商家状态(1正常) - - dianhua: 商家电话 - - wechat: 商家微信 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 获取并验证邀请码参数 - yaoqingma = request.data.get('inviteCode') - if not yaoqingma: - return Response({ - 'code': 400, - 'message': '邀请码不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - yaoqingma = yaoqingma.strip() - if len(yaoqingma) > 100: - return Response({ - 'code': 400, - 'message': '邀请码长度超过限制', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - current_user = request.user - - # 2. 检查用户是否已是商家 - try: - shangjia_profile = current_user.shop_profile - # 用户已是商家,直接返回现有信息 - return self.fanhuiXianyouShangjiaXinxi(shangjia_profile, current_user) - except UserShangjia.DoesNotExist: - # 用户不是商家,继续注册流程 - pass - - # 3. 根据邀请码查找对应的管理员 - try: - # 从管理员表查找邀请码 - admin_profile = AdminProfile.objects.select_related('user').get(yaoqingma=yaoqingma) - except AdminProfile.DoesNotExist: - return Response({ - 'code': 404, - 'message': '管理员邀请码无效或不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 4. 核心:在数据库事务中创建商家身份 - try: - with transaction.atomic(): - # 4.1 查询老板扩展表获取老板昵称 - boss_nickname = '普通商家' - try: - boss_profile = current_user.boss_profile - if boss_profile.nickname and boss_profile.nickname.strip(): - boss_nickname = boss_profile.nickname.strip() - except UserBoss.DoesNotExist: - # 老板扩展表不存在,使用默认值 - pass - - # 4.2 创建商家扩展表 - # 使用老板昵称作为商家昵称的基础 - shangjia_nicheng = boss_nickname - - # 检查昵称是否已存在(安全考虑) - if UserShangjia.objects.filter(nicheng=shangjia_nicheng).exists(): - # 如果存在,添加随机数确保唯一 - import random - shangjia_nicheng = f"{boss_nickname}{random.randint(1000, 9999)}" - - # 4.3 创建商家记录 - shop_profile = UserShangjia.objects.create( - user=current_user, - nicheng=shangjia_nicheng, - zhuangtai=1, # 正常状态 - yue=0.00, # 商家余额 - fabu=0, # 发布订单总数 - tuikuan=0, # 退款订单总数 - chengjiao=0, # 成交订单总数 - jinridingdan=0, # 今日订单 - jinriliushui=0.00, # 今日流水 - jinyuedingdan=0, # 今月订单 - jinyueliushui=0.00, # 今月流水 - dianhua='', # 电话 - wechat='', # 微信 - ) - - except Exception as e: - # 事务会自动回滚 - import traceback - error_detail = traceback.format_exc() - #print(f"商家注册错误详情: {error_detail}") - - return Response({ - 'code': 500, - 'message': f'商家注册过程中发生系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 5. 注册成功,返回商家信息 - fanhui_data = self.zhuangbeiFanhuiShuju(shop_profile, current_user) - return Response({ - 'code': 200, - 'message': '商家注册成功!', - 'data': fanhui_data - }) - - def fanhuiXianyouShangjiaXinxi(self, shangjia_profile, current_user): - """处理已是商家的用户,返回其现有信息""" - fanhui_data = self.zhuangbeiFanhuiShuju(shangjia_profile, current_user) - return Response({ - 'code': 200, - 'message': '您已是商家,返回当前信息。', - 'data': fanhui_data - }) - - def zhuangbeiFanhuiShuju(self, shangjia_profile, current_user): - """ - 准备返回给前端的商家数据 - 字段名必须与前端商家页面完全一致 - """ - data = { - # 🟢 商家基本信息 - 'nicheng': shangjia_profile.nicheng, # 商家昵称 - 'sjyue': format(shangjia_profile.yue, '.2f'), # 商家余额,保留两位小数 - 'fabu': shangjia_profile.fabu, # 发布订单总数 - 'chengjiao': shangjia_profile.chengjiao, # 成交订单总数 - 'tuikuan': shangjia_profile.tuikuan, # 退款订单总数 - 'jinridingdan': shangjia_profile.jinridingdan, # 今日订单 - 'jinriliushui': format(shangjia_profile.jinriliushui, '.2f'), # 今日流水 - 'jinyuedingdan': shangjia_profile.jinyuedingdan, # 今月订单 - 'jinyueliushui': format(shangjia_profile.jinyueliushui, '.2f'), # 今月流水 - 'zhuangtai': shangjia_profile.zhuangtai, # 商家状态 - 'dianhua': shangjia_profile.dianhua or '', # 电话 - 'wechat': shangjia_profile.wechat or '', # 微信 - - # 🟢 可能需要的主表信息 - 'uid': current_user.yonghuid, # 用户ID - } - return data - - - - - - - -# yonghu/ranking_views.py -""" -排行榜查询接口 - 管理员专用 -安全认证:JWT Token + 管理员权限验证 -""" - -from django.db.models import Q -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from datetime import datetime -from .models import RankingRecord -import logging - - - -logger = logging.getLogger(__name__) - -# yonghu/ranking_views.py - 修改PaihangbangGuanliQueryView的post方法参数接收 -class PaihangbangGuanliQueryView(APIView): - """ - 排行榜查询接口 - 管理员专用 - 安全要求: - 1. JWT Token认证 - 2. 管理员权限验证 - 3. 参数严格验证 - 4. 返回统一格式 - """ - - # JWT Token认证 - permission_classes = [IsAuthenticated] - - @admin_required - def post(self, request): - """ - POST请求查询排行榜数据 - 参数: - - zhanghao: 管理员账号 - - shenfen: 用户身份(必须,2=打手,3=管事,4=商家) - - zhouqi: 周期类型(必须,1=日榜,2=月榜) - - date: 查询日期(必须,格式:YYYY-MM-DD) - - 返回格式: - { - "code": 0, # 0=成功,非0=失败 - "message": "成功", - "data": { - "list": [...], # 排行榜数据列表 - "query_info": {...} # 查询信息 - } - } - """ - try: - # 1. 获取请求数据 - data = request.data - zhanghao = data.get('zhanghao') - shenfen = data.get('shenfen') - zhouqi = data.get('zhouqi') - date_str = data.get('date') - - # 2. 参数验证 - if not zhanghao: - return Response({ - 'code': 400, - 'message': '管理员账号不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if not shenfen: - return Response({ - 'code': 400, - 'message': '用户身份不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if not zhouqi: - return Response({ - 'code': 400, - 'message': '周期类型不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if not date_str: - return Response({ - 'code': 400, - 'message': '查询日期不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 参数类型和范围验证 - try: - zhouqi_int = int(zhouqi) - shenfen_int = int(shenfen) - date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() - except ValueError as e: - return Response({ - 'code': 400, - 'message': f'参数格式错误: {str(e)}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证周期类型范围 - if zhouqi_int not in [1, 2]: - return Response({ - 'code': 400, - 'message': '周期类型必须为1(日榜)或2(月榜)', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证用户身份范围 - if shenfen_int not in [2, 3, 4]: - return Response({ - 'code': 400, - 'message': '用户身份必须为2(打手)、3(管事)或4(商家)', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 构建查询条件 - query = Q( - zhouqi=zhouqi_int, - tongji_date=date_obj, - shenfen=shenfen_int - ) - - # 5. 执行查询(按排名升序) - records = RankingRecord.objects.filter(query).order_by('paiming') - - # 6. 格式化返回数据 - data_list = [] - for record in records: - # 根据用户身份确定指标值单位 - zhizhi_danwei = self._get_zhizhi_danwei(shenfen_int) - - data_list.append({ - 'paiming': record.paiming, - 'yonghuid': record.yonghuid, - 'nicheng': record.nicheng, - 'avatar': record.avatar or '', - 'zhizhi': float(record.zhizhi), - 'zhizhi_danwei': zhizhi_danwei, - }) - - # 7. 返回成功响应 - response_data = { - 'code': 0, - 'message': '查询成功', - 'data': { - 'list': data_list, - 'total_count': len(data_list), - 'query_info': { - 'zhouqi': zhouqi_int, - 'zhouqi_text': '日榜' if zhouqi_int == 1 else '月榜', - 'shenfen': shenfen_int, - 'shenfen_text': self._get_shenfen_text(shenfen_int), - 'date': date_str, - } - } - } - - return Response(response_data, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f"管理员查询排行榜失败: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'message': f'查询排行榜失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def _get_shenfen_text(self, shenfen): - """获取身份文本""" - shenfen_map = {2: '打手', 3: '管事', 4: '商家'} - return shenfen_map.get(shenfen, '未知') - - def _get_zhizhi_danwei(self, shenfen): - """获取指标值单位""" - danwei_map = {2: '元', 3: '人', 4: '元'} - return danwei_map.get(shenfen, '') - - - - - - - -# yonghu/ranking_views.py - 添加以下代码 -class PaihangbangRiqiliebiaoView(APIView): - """ - 获取排行榜可查询日期列表 - 管理员专用 - 返回当前排行榜表中存在的日期(日榜和月榜分开) - """ - - # JWT Token认证 - permission_classes = [IsAuthenticated] - - @admin_required - def post(self, request): - """ - POST请求获取可查询日期列表 - 参数: - - zhanghao: 管理员账号(用于验证) - - 返回格式: - { - "code": 0, - "message": "成功", - "data": { - "ribang_dates": ["2024-01-15", "2024-01-14", ...], # 日榜日期列表 - "yuebang_dates": ["2024-01-01", "2023-12-01", ...], # 月榜日期列表 - } - } - """ - try: - # 1. 获取请求数据 - data = request.data - zhanghao = data.get('zhanghao') - - if not zhanghao: - return Response({ - 'code': 400, - 'message': '管理员账号不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 查询日榜日期(最近3个月内的) - three_months_ago = timezone.now().date() - timedelta(days=90) - - ribang_dates = RankingRecord.objects.filter( - zhouqi=1, # 日榜 - tongji_date__gte=three_months_ago # 只查询最近3个月 - ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date') - - # 3. 查询月榜日期(最近12个月内的) - twelve_months_ago = timezone.now().date() - timedelta(days=365) - - yuebang_dates = RankingRecord.objects.filter( - zhouqi=2, # 月榜 - tongji_date__gte=twelve_months_ago # 只查询最近12个月 - ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date') - - # 4. 格式化日期 - ribang_date_list = [date.strftime('%Y-%m-%d') for date in ribang_dates] - yuebang_date_list = [date.strftime('%Y-%m-%d') for date in yuebang_dates] - - # 5. 返回成功响应 - response_data = { - 'code': 0, - 'message': '获取成功', - 'data': { - 'ribang_dates': ribang_date_list, - 'yuebang_dates': yuebang_date_list, - 'ribang_count': len(ribang_date_list), - 'yuebang_count': len(yuebang_date_list), - } - } - - return Response(response_data, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f"获取排行榜日期列表失败: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'message': f'获取日期列表失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - - - - -class WechatLoginAndDashouRegisterView(APIView): - """ - 微信登录并注册打手一体化接口 - 访问路径: /api/yonghu/wdlyhdl - 请求方法: POST - 权限要求: 无需登录 - 请求体: { "code": "微信登录code", "inviteCode": "邀请码" } - - 功能: 用户扫码进入但未登录时,一次性完成微信登录和打手注册 - """ - - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - - def post(self, request): - """ - 处理微信登录并注册打手请求 - """ - try: - # 1. 获取并验证参数 - code = request.data.get('code', '').strip() - yaoqingma = request.data.get('inviteCode', '').strip() - - if not code: - return Response({ - 'code': 1, - 'msg': '微信授权码不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if not yaoqingma: - return Response({ - 'code': 2, - 'msg': '邀请码不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if len(yaoqingma) > 100: - return Response({ - 'code': 3, - 'msg': '邀请码长度超过限制', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 获取微信openid - wechat_data = self.get_wechat_openid(code) - if not wechat_data or 'openid' not in wechat_data: - error_msg = wechat_data.get('errmsg', '微信授权失败') - return Response({ - 'code': 4, - 'msg': f'微信登录失败: {error_msg}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - openid = wechat_data['openid'] - - # 3. 获取客户端真实IP - kehuduan_ip = self.huoquKehuduanIP(request) - - # 4. 开始数据库事务(确保登录和注册的原子性) - with transaction.atomic(): - # 4.1 创建或获取用户(登录逻辑) - user_main, created = UserMain.objects.select_for_update().get_or_create( - openid=openid, - defaults={ - 'yonghuid': self.shengchengYonghuID(), - 'user_type': 'normal', - } - ) - - # 更新用户IP和最后登录时间 - cunchu_ip = kehuduan_ip - user_main.ip = cunchu_ip - user_main.last_login_time = timezone.now() - user_main.save() - - if created: - # 新用户:创建老板扩展表 - UserBoss.objects.create(user=user_main, nickname='微信用户') - - # 4.2 验证邀请码对应的管事(注册逻辑) - try: - # 使用select_related获取管事及其关联的主表用户信息 - guanshi_profile = UserGuanshi.objects.select_related('user').get(yaoqingma=yaoqingma) - except UserGuanshi.DoesNotExist: - # 邀请码无效,但用户已登录,可以返回登录成功但注册失败 - # 这里我们继续执行,但只返回登录信息 - return self.fanhuiZhihouDengluXinxi(user_main, yaoqingma_wuxiao=True) - - # 验证管事状态 - - # 4.3 检查用户是否已是打手 - dashou_exists = UserDashou.objects.filter(user=user_main).exists() - if dashou_exists: - # 用户已是打手,直接返回信息 - dashou_profile = UserDashou.objects.get(user=user_main) - return self.fanhuiDengluZhuceshuju(user_main, dashou_profile) - - # 4.4 核心:为用户创建所有身份(打手、商家、管事) - guanshi_yonghuid = guanshi_profile.user.yonghuid - - # 创建打手扩展表 - dashou_profile = UserDashou.objects.create( - user=user_main, - nicheng='大神', - chenghao='普通大神', - yaoqingren=guanshi_yonghuid, - # 其他字段使用模型默认值 - ) - - # 创建商家扩展表 - - # 创建管事扩展表 - - # 更新原管事的邀请数量 - locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk) - locked_guanshi.yaogingshuliang += 1 - locked_guanshi.save() - - # ========== 新增:管事每日统计(邀请打手) ========== - try: - import logging - - logger = logging.getLogger(__name__) - from houtai.utils import update_guanshi_daily_by_action - update_guanshi_daily_by_action( - yonghuid=guanshi_yonghuid, - action=1, # 1 = 邀请打手 - amount=Decimal('0.00') - ) - logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}") - except Exception as e: - logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}") - - # 5. 生成JWT token - refresh = RefreshToken.for_user(user_main) - token = str(refresh.access_token) - - # 6. 准备返回数据 - response_data = self.zhuangbeiFanhuiShuju(user_main, dashou_profile, token) - - return Response({ - 'code': 0, - 'msg': '登录并注册成功!已开通打手、管事等身份。', - 'data': response_data - }) - - except Exception as e: - # 记录错误日志 - import logging - logger = logging.getLogger(__name__) - error_openid = locals().get('openid', 'N/A') - logger.error( - f"微信登录注册一体化接口异常 - openid: {error_openid}, " - f"错误类型: {type(e).__name__}, 错误详情: {str(e)}", - exc_info=True - ) - - # 返回友好错误信息 - return Response({ - 'code': 99, - 'msg': '系统繁忙,请稍后重试', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def get_wechat_openid(self, code): - """ - 调用微信接口获取openid - """ - try: - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') - - if not appid or not secret: - raise ValueError('微信配置未设置') - - url = 'https://api.weixin.qq.com/sns/jscode2session' - params = { - 'appid': appid, - 'secret': secret, - 'js_code': code, - 'grant_type': 'authorization_code' - } - - response = requests.get(url, params=params, timeout=10) - result = response.json() - - if 'openid' in result: - return result - else: - errcode = result.get('errcode', 'unknown') - errmsg = result.get('errmsg', '未知错误') - return {'errmsg': f'[{errcode}]{errmsg}'} - - except requests.exceptions.Timeout: - return {'errmsg': '请求微信接口超时'} - except Exception as e: - return {'errmsg': f'请求微信接口异常: {str(e)}'} - - def shengchengYonghuID(self): - """ - 生成7位数字用户ID - """ - for _ in range(10): - timestamp_part = str(int(time.time()))[-5:].zfill(5) - random_part = str(random.randint(0, 99)).zfill(2) - user_id = timestamp_part + random_part - - if len(user_id) == 7 and user_id.isdigit(): - if not UserMain.objects.filter(yonghuid=user_id).exists(): - return user_id - - raise Exception('生成用户ID失败') - - def huoquKehuduanIP(self, request): - """ - 获取客户端真实IP地址 - """ - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0].strip() - if ip: - return ip - - ip = request.META.get('REMOTE_ADDR', '') - return ip if ip else '0.0.0.0' - - def huoquQunPeizhi(self): - """ - 获取群配置信息 - """ - try: - qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) - - result = { - 'dashouqun': '', - 'dashouqunid': '', - 'guanshiqun': '', - 'guanshiqunid': '' - } - - for config in qun_configs: - if config.id == 1: - result['dashouqun'] = config.neirong or '' - result['dashouqunid'] = config.qunid or '' - elif config.id == 2: - result['guanshiqun'] = config.neirong or '' - result['guanshiqunid'] = config.qunid or '' - - return result - - except Exception: - return { - 'dashouqun': '', - 'dashouqunid': '', - 'guanshiqun': '', - 'guanshiqunid': '' - } - - def fanhuiZhihouDengluXinxi(self, user_main, yaoqingma_wuxiao=False, guanshi_jinyong=False): - """ - 返回只登录成功的信息(当邀请码无效或管事禁用时) - """ - # 获取老板昵称 - boss_nickname = '微信用户' - try: - boss_profile = UserBoss.objects.get(user=user_main) - if boss_profile.nickname: - boss_nickname = boss_profile.nickname - except UserBoss.DoesNotExist: - pass - - # 检查各身份状态 - dashou_status = 1 if UserDashou.objects.filter(user=user_main).exists() else 0 - shangjia_status = 1 if UserShangjia.objects.filter(user=user_main).exists() else 0 - guanshi_status = 1 if UserGuanshi.objects.filter(user=user_main).exists() else 0 - - # 生成token - refresh = RefreshToken.for_user(user_main) - token = str(refresh.access_token) - - # 获取群配置 - group_info = self.huoquQunPeizhi() - - # 构建返回数据 - response_data = { - 'token': token, - 'nicheng': boss_nickname, - 'uid': user_main.yonghuid, - 'touxiang': user_main.avatar or '', - 'shangjiastatus': shangjia_status, - 'dashoustatus': dashou_status, - 'guanshistatus': guanshi_status, - # 🟢 注意:这里没有返回dingdantiaoshu(订单数量),与前端需求一致 - 'dashouqun': group_info.get('dashouqun', ''), - 'dashouqunid': group_info.get('dashouqunid', ''), - 'guanshiqun': group_info.get('guanshiqun', ''), - 'guanshiqunid': group_info.get('guanshiqunid', '') - } - - # 根据情况设置返回消息 - if yaoqingma_wuxiao: - msg = '登录成功,但邀请码无效' - elif guanshi_jinyong: - msg = '登录成功,但邀请码对应的管事账号已被禁用' - else: - msg = '登录成功' - - return Response({ - 'code': 0, - 'msg': msg, - 'data': response_data - }) - - def fanhuiDengluZhuceshuju(self, user_main, dashou_profile): - """ - 返回登录并注册的数据(用户已是打手的情况) - """ - # 获取老板昵称 - boss_nickname = '微信用户' - try: - boss_profile = UserBoss.objects.get(user=user_main) - if boss_profile.nickname: - boss_nickname = boss_profile.nickname - except UserBoss.DoesNotExist: - pass - - # 检查各身份状态(这里用户已是打手,所以都是1) - dashou_status = 1 - shangjia_status = 1 if UserShangjia.objects.filter(user=user_main).exists() else 1 # 确保是1 - guanshi_status = 1 if UserGuanshi.objects.filter(user=user_main).exists() else 1 # 确保是1 - - # 生成token - refresh = RefreshToken.for_user(user_main) - token = str(refresh.access_token) - - # 获取群配置 - group_info = self.huoquQunPeizhi() - - # 准备打手信息 - dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile) - - # 合并所有数据 - response_data = { - 'token': token, - 'nicheng': boss_nickname, - 'uid': user_main.yonghuid, - 'touxiang': user_main.avatar or '', - 'shangjiastatus': shangjia_status, - 'dashoustatus': dashou_status, - 'guanshistatus': guanshi_status, - 'dashouqun': group_info.get('dashouqun', ''), - 'dashouqunid': group_info.get('dashouqunid', ''), - 'guanshiqun': group_info.get('guanshiqun', ''), - 'guanshiqunid': group_info.get('guanshiqunid', ''), - # 合并打手信息 - **dashou_data - } - - return Response({ - 'code': 0, - 'msg': '您已是打手,登录成功', - 'data': response_data - }) - - def zhuangbeiFanhuiShuju(self, user_main, dashou_profile, token): - """ - 准备完整的返回数据(登录+注册成功的情况) - """ - # 获取老板昵称 - boss_nickname = '微信用户' - try: - boss_profile = UserBoss.objects.get(user=user_main) - if boss_profile.nickname: - boss_nickname = boss_profile.nickname - except UserBoss.DoesNotExist: - pass - - # 身份状态(刚注册成功,应该都是1) - dashou_status = 1 - shangjia_status = 1 - guanshi_status = 1 - - # 获取群配置 - group_info = self.huoquQunPeizhi() - - # 准备打手信息 - dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile) - - # 合并所有数据 - response_data = { - 'token': token, - 'nicheng': boss_nickname, - 'uid': user_main.yonghuid, - 'touxiang': user_main.avatar or '', - 'shangjiastatus': shangjia_status, - 'dashoustatus': dashou_status, - 'guanshistatus': guanshi_status, - 'dashouqun': group_info.get('dashouqun', ''), - 'dashouqunid': group_info.get('dashouqunid', ''), - 'guanshiqun': group_info.get('guanshiqun', ''), - 'guanshiqunid': group_info.get('guanshiqunid', ''), - # 合并打手信息 - **dashou_data - } - - return response_data - - def zhuangbeiDashouFanhuiShuju(self, dashou_profile): - """ - 准备打手返回数据(字段名严格匹配前端) - """ - # 构建基础信息 - data = { - 'dashounicheng': dashou_profile.nicheng, - 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, - 'yongjin': str(dashou_profile.yue), # Decimal转字符串 - 'zonge': str(dashou_profile.zonge), - 'yajin': str(dashou_profile.yajin), - 'chenghao': dashou_profile.chenghao, - 'jinfen': dashou_profile.jifen, - 'chengjiaoliang': dashou_profile.chengjiaozongliang, - 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, - 'dashouzhuangtai': dashou_profile.zhuangtai, - } - - # 查询并处理会员列表 (clumber) - # 使用当前用户的yonghuid进行查询 - huiyuan_goumai_list = [] - goumai_records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid) - for record in goumai_records: - # 调用方法检查是否到期,此方法内部会更新状态 - shifou_daoqi = record.jiance_shifou_daoqi() - huiyuan_goumai_list.append({ - 'huiyuan_id': record.huiyuan_id, - 'huiyuan_zhuangtai': record.huiyuan_zhuangtai, - 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None, - 'shifou_daoqi': shifou_daoqi, - 'jieshao': record.jieshao - }) - - data['clumber'] = huiyuan_goumai_list - return data - -from datetime import datetime # 🔴【关键修改】 -class YonghuTixianShenheXiugaiView(APIView): - """ - 用户修改审核中提现申请接口 - 访问路径: /api/yonghu/yonghutxshxg - 请求方法: POST - 权限要求: JWT Token认证通过的用户 - 请求体: multipart/form-data 格式 - 支持字段: tixian_id, fangshi, txdianhua, txzh, file (图片文件) - - 功能: 允许用户修改审核中提现申请的收款方式、电话、账号、收款码等 - """ - - permission_classes = [IsAuthenticated] - parser_classes = (MultiPartParser, FormParser, JSONParser) - - def post(self, request): - """ - 处理用户修改审核中提现申请的POST请求 - """ - try: - # 1. 获取当前用户 - user_main = request.user - current_yonghuid = user_main.yonghuid - - # 2. 验证必填字段:提现记录ID - tixian_id = request.data.get('tixian_id') - if not tixian_id: - return Response({ - 'code': 1, - 'msg': '提现记录ID不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 查询提现记录(使用select_for_update确保事务安全) - with transaction.atomic(): - try: - tixian_record = Tixianjilu.objects.select_for_update().get( - id=tixian_id, - yonghuid=current_yonghuid # 确保只能修改自己的记录 - ) - except Tixianjilu.DoesNotExist: - return Response({ - 'code': 2, - 'msg': '提现记录不存在或无权修改', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 4. 验证提现记录状态(必须是审核中) - if tixian_record.zhuangtai != 1: - return Response({ - 'code': 3, - 'msg': '只有审核中的提现申请可以修改', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 5. 获取并验证提现方式 - fangshi = request.data.get('fangshi') - if fangshi: - fangshi = int(fangshi) - if fangshi not in [1, 2]: - return Response({ - 'code': 4, - 'msg': '提现方式无效,请选择1(微信)或2(支付宝)', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 检查提现类型与方式的匹配(如果前端提供了leixing) - leixing = request.data.get('leixing') - if leixing: - leixing = int(leixing) - # 这里可以根据业务逻辑添加类型与方式的验证 - # 例如:某些类型只能使用某些方式 - - # 6. 验证手机号(必填) - txdianhua = request.data.get('txdianhua', '').strip() - if not txdianhua: - return Response({ - 'code': 5, - 'msg': '收款人电话不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证手机号格式 - if not txdianhua.isdigit() or len(txdianhua) != 11: - return Response({ - 'code': 6, - 'msg': '请输入11位有效的手机号码', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 7. 获取收款账号(可选) - txzh = request.data.get('txzh', '').strip() - - # 8. 处理收款码图片上传(如果有) - txtupian_file = request.FILES.get('file') - new_zhifu_url = None - old_zhifu_url = tixian_record.zhifu - - if txtupian_file: - # 验证图片 - is_valid, error_msg = validate_image(txtupian_file) - if not is_valid: - return Response({ - 'code': 7, - 'msg': f'图片验证失败: {error_msg}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 上传收款码图片 - new_zhifu_url = self.handle_shoukuanma_upload( - user_main, - txtupian_file, - old_zhifu_url, - tixian_record.leixing - ) - - if not new_zhifu_url: - return Response({ - 'code': 8, - 'msg': '收款码上传失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 9. 验证收款信息(账号和图片至少有一个) - if not txzh and not (new_zhifu_url or old_zhifu_url): - return Response({ - 'code': 9, - 'msg': '请填写收款账号或上传收款码', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 10. 更新提现记录 - update_fields = [] - - # 更新提现方式(如果有) - if fangshi: - tixian_record.fangshi = fangshi - update_fields.append('fangshi') - - # 更新手机号(必填) - tixian_record.phone = txdianhua - update_fields.append('phone') - - # 更新收款账号(如果有) - if txzh is not None: # 注意:空字符串也要更新 - tixian_record.skzhanghao = txzh - update_fields.append('skzhanghao') - - # 更新收款码图片(如果有新图片) - if new_zhifu_url: - tixian_record.zhifu = new_zhifu_url - update_fields.append('zhifu') - - # 保存更新 - if update_fields: - tixian_record.save(update_fields=update_fields) - - # 11. 构建返回数据 - response_data = { - 'tixian_id': tixian_record.id, - 'fangshi': tixian_record.fangshi, - 'txdianhua': tixian_record.phone, - 'txzh': tixian_record.skzhanghao or '', - 'txtupian': tixian_record.zhifu or '' # 返回相对URL - } - - # 12. 返回成功响应 - return Response({ - 'code': 0, - 'msg': '提现申请修改成功', - 'data': response_data - }) - - except Exception as e: - # 记录详细错误日志 - import logging - logger = logging.getLogger(__name__) - logger.error( - f"修改提现申请异常 - 用户ID: {getattr(user_main, 'yonghuid', 'N/A')}, " - f"提现ID: {tixian_id}, " - f"错误类型: {type(e).__name__}, " - f"错误详情: {str(e)}", - exc_info=True - ) - - # 返回友好错误信息 - return Response({ - 'code': 99, - 'msg': '系统繁忙,请稍后重试', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def handle_shoukuanma_upload(self, user_main, image_file, old_zhifu_url, leixing=None): - """ - 处理收款码图片上传到腾讯云COS - 存储路径: tixian/{yonghuid}/{timestamp_uuid}.扩展名 - - 参数: - - user_main: 当前用户对象 - - image_file: 图片文件对象 - - old_zhifu_url: 旧收款码URL(用于删除) - - leixing: 提现类型(可选,用于文件夹分类) - - 返回: 新的图片相对路径 - """ - try: - yonghuid = user_main.yonghuid - if not yonghuid: - return None - - # 删除旧收款码(如果存在且不是空字符串) - if old_zhifu_url and old_zhifu_url.strip(): - delete_from_oss(old_zhifu_url) - - # 获取文件扩展名 - file_name = image_file.name - file_extension = os.path.splitext(file_name)[1].lower() - if not file_extension: - file_extension = '.jpg' - - # 生成唯一文件名(使用时间戳+UUID确保不重复) - timestamp = datetime.now().strftime('%Y%m%d%H%M%S') - unique_id = str(uuid.uuid4())[:8] # 取UUID前8位 - unique_file_name = f"{timestamp}_{unique_id}{file_extension}" - - # 根据提现类型构建文件夹路径(如果提供了leixing) - if leixing: - # leixing=1:打手佣金, leixing=2:管事分红 - type_folder = 'dashou' if leixing == 1 else 'guanshi' - oss_file_path = f"tixian/{type_folder}/{yonghuid}/{unique_file_name}" - else: - # 默认路径 - oss_file_path = f"tixian/{yonghuid}/{unique_file_name}" - - # 上传新收款码到OSS - new_shoukuanma_url = upload_to_oss(image_file, oss_file_path) - - if new_shoukuanma_url: - # 返回相对路径(从tixian文件夹开始) - return oss_file_path - else: - return None - - except Exception as e: - print(f"处理收款码上传失败: {e}") - return None - - -# views.py -from django.db.models import Q, Prefetch -from django.db import connection -from django.core.paginator import Paginator -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from dingdan.models import Chufajilu, Chufatupian -from yonghu.models import UserDashou -import logging - -logger = logging.getLogger(__name__) - - -class DashouPermission: - """ - 打手权限验证类 - 确保用户是打手且账号状态正常 - """ - - def has_permission(self, request, view): - user = request.user - - # 检查用户是否已认证 - if not user.is_authenticated: - return False - - try: - # 通过反向关系获取打手扩展信息 - dashou_profile = user.dashou_profile - - # 检查账号状态是否为1(正常) - if dashou_profile.zhanghaozhuangtai != 1: - return False - - return True - except UserDashou.DoesNotExist: - return False - except Exception as e: - logger.error(f"打手权限验证异常: {e}") - return False - - -from django.db.models import Q, Count, Case, When, IntegerField - - - -logger = logging.getLogger(__name__) - - -class ChufaJiluHuoquView(APIView): - """ - 打手获取处罚记录接口 - 路径: /yonghu/dshqcfjl - 方法: POST - 权限: JWT Token + 打手权限验证 - """ - permission_classes = [IsAuthenticated, DashouPermission] - - def post(self, request): - try: - # 获取当前用户信息 - user_main = request.user - yonghuid = user_main.yonghuid - - # 通过反向关系获取打手扩展信息 - try: - dashou_profile = user_main.dashou_profile - # 再次确认账号状态 - if dashou_profile.zhanghaozhuangtai != 1: - return Response({ - 'code': 3, - 'msg': '账号已被封禁,无法查看处罚记录', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - except UserDashou.DoesNotExist: - return Response({ - 'code': 2, - 'msg': '用户不是打手身份', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 判断请求类型:统计信息 or 记录列表 - qingqiu_tongji = request.data.get('qingqiu_tongji', False) - - if qingqiu_tongji: - # 返回统计信息 - return self.get_tongji_info(yonghuid) - else: - # 返回处罚记录列表 - return self.get_chufa_list(yonghuid, request.data) - - except Exception as e: - logger.error(f"获取处罚记录异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True) - return Response({ - 'code': 99, - 'msg': '系统繁忙,请稍后重试', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def get_tongji_info(self, yonghuid): - """ - 获取统计信息(总记录、待处理、已处理) - """ - try: - # 使用单个查询获取所有统计信息 - stats = Chufajilu.objects.filter(dashouid=yonghuid).aggregate( - # 总记录数 - zongshu=Count('id'), - - # 待处理数:状态为0(待处罚)和3(申诉中) - daichuli=Count( - Case( - When(Q(sqzhuangtai=0) | Q(sqzhuangtai=3), then=1), - output_field=IntegerField() - ) - ), - - # 已处理数:状态为1(已处罚)和2(已驳回) - yichuli=Count( - Case( - When(Q(sqzhuangtai=1) | Q(sqzhuangtai=2), then=1), - output_field=IntegerField() - ) - ) - ) - - # 如果查询结果为None,则设置为0 - tongji_data = { - 'zongshu': stats.get('zongshu', 0) or 0, - 'daichuli': stats.get('daichuli', 0) or 0, - 'yichuli': stats.get('yichuli', 0) or 0 - } - - return Response({ - 'code': 0, - 'msg': '获取统计信息成功', - 'data': tongji_data - }) - - except Exception as e: - logger.error(f"获取统计信息异常 - 用户ID: {yonghuid}, 错误: {str(e)}") - return Response({ - 'code': 1, - 'msg': '获取统计信息失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def get_chufa_list(self, yonghuid, request_data): - """ - 获取处罚记录列表(高性能批量查询) - 🔴 只修改图片查询逻辑,字段保持原样 - """ - try: - # 获取分页参数 - page = int(request_data.get('page', 1)) - page_size = int(request_data.get('page_size', 10)) - - # 限制最大每页数量,防止恶意请求 - if page_size > 50: - page_size = 50 - if page_size < 1: - page_size = 10 - - # 获取状态筛选参数 - zhuangtai = request_data.get('zhuangtai', 0) - - # 🔴 构建查询条件 - 打手端根据dashouid查询 - queryset = Chufajilu.objects.filter(dashouid=yonghuid) - - if zhuangtai == 0: - # 待处理:状态为0(待处罚)和3(申诉中) - queryset = queryset.filter(Q(sqzhuangtai=0) | Q(sqzhuangtai=3)) - elif zhuangtai == 1: - # 已处理:状态为1(已处罚)和2(已驳回) - queryset = queryset.filter(Q(sqzhuangtai=1) | Q(sqzhuangtai=2)) - else: - # 默认返回所有状态 - pass - - # 按创建时间倒序排列(最新在前) - queryset = queryset.order_by('-create_time') - - # 使用游标分页,避免传统分页的性能问题 - paginator = Paginator(queryset, page_size) - - try: - page_obj = paginator.page(page) - except: - # 页码超出范围,返回空列表 - return Response({ - 'code': 0, - 'msg': '获取成功', - 'data': { - 'list': [], - 'has_more': False, - 'page': page, - 'page_size': page_size, - 'current_count': 0 - } - }) - - # 获取当前页的记录 - chufa_records = list(page_obj) - - if not chufa_records: - # 没有数据直接返回 - return Response({ - 'code': 0, - 'msg': '获取成功', - 'data': { - 'list': [], - 'has_more': False, - 'page': page, - 'page_size': page_size, - 'current_count': 0 - } - }) - - # 🔴【核心修复】收集所有需要查询图片的组合 - dingdan_ids = [] - shangjia_ids = [] - - for record in chufa_records: - if record.dingdan_id: - dingdan_ids.append(record.dingdan_id) - if record.qingqiuid: # 商家ID - shangjia_ids.append(record.qingqiuid) - - # 去重 - dingdan_ids = list(set(dingdan_ids)) - shangjia_ids = list(set(shangjia_ids)) - - # 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误) - # 1. 查询证据图片(商家上传的) - zhengju_mapping = {} - if dingdan_ids and shangjia_ids: - # 构建Q对象:dingdan_id在列表中 AND yonghuid在商家ID列表中 - zhengju_q = Q(dingdan_id__in=dingdan_ids, yonghuid__in=shangjia_ids) - zhengju_queryset = Chufatupian.objects.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian') - - for item in zhengju_queryset: - key = f"{item['dingdan_id']}_{item['yonghuid']}" - if key not in zhengju_mapping: - zhengju_mapping[key] = [] - if item['tupian']: - zhengju_mapping[key].append(item['tupian']) - - # 2. 查询申诉图片(打手上传的) - shensu_mapping = {} - if dingdan_ids: - # 构建Q对象:dingdan_id在列表中 AND yonghuid=当前打手ID - shensu_q = Q(dingdan_id__in=dingdan_ids, yonghuid=yonghuid) - shensu_queryset = Chufatupian.objects.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian') - - for item in shensu_queryset: - key = f"{item['dingdan_id']}_{item['yonghuid']}" - if key not in shensu_mapping: - shensu_mapping[key] = [] - if item['tupian']: - shensu_mapping[key].append(item['tupian']) - - # 🔴【核心修复】组装返回数据 - 字段保持原样! - chufa_list = [] - for record in chufa_records: - dingdan_id = record.dingdan_id - shangjia_id = record.qingqiuid - - # 获取证据图片(商家上传的) - zhengju_tupian = [] - if dingdan_id and shangjia_id: - key = f"{dingdan_id}_{shangjia_id}" - zhengju_tupian = zhengju_mapping.get(key, []) - - # 获取申诉图片(打手上传的) - shensu_tupian = [] - if dingdan_id: - key = f"{dingdan_id}_{yonghuid}" - shensu_tupian = shensu_mapping.get(key, []) - - # 🔴【字段保持原样!】与前端完全对应! - chufa_data = { - 'id': record.id, - 'dingdan_id': record.dingdan_id or '', - 'qingqiuid': record.qingqiuid or '', # 商家ID - 'cfliyou': record.cfliyou or '', - 'sqzhuangtai': record.sqzhuangtai or 0, - 'ssliyou': record.ssliyou or '', - 'jifen': record.jifen or 0, - 'create_time': record.create_time.strftime('%Y-%m-%d %H:%M:%S') if record.create_time else '', - 'update_time': record.update_time.strftime('%Y-%m-%d %H:%M:%S') if record.update_time else '', - 'zhengju_tupian': zhengju_tupian, # 商家上传的证据图片 - 'shensu_tupian': shensu_tupian, # 打手上传的申诉图片 - } - chufa_list.append(chufa_data) - - # 判断是否还有更多数据 - has_more = page_obj.has_next() - - # 构建响应数据 - response_data = { - 'list': chufa_list, - 'has_more': has_more, - 'page': page, - 'page_size': page_size, - 'current_count': len(chufa_list) - } - - return Response({ - 'code': 0, - 'msg': '获取成功', - 'data': response_data - }) - - except ValueError as e: - logger.error(f"参数错误 - 用户ID: {yonghuid}, 错误: {str(e)}") - return Response({ - 'code': 4, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - logger.error(f"获取处罚记录列表异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True) - return Response({ - 'code': 5, - 'msg': '获取记录列表失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - -class DashouShensuView(APIView): - """ - 打手申诉接口 - 路径: /yonghu/dscfss - 方法: POST - 权限: JWT Token + 打手权限验证 - 功能: 打手对处罚进行申诉 - """ - permission_classes = [IsAuthenticated, DashouPermission] - - def post(self, request): - try: - # 获取当前用户信息 - user_main = request.user - yonghuid = user_main.yonghuid - - # 🔴🆕【关键】验证打手账号状态 - try: - dashou_profile = user_main.dashou_profile - if dashou_profile.zhanghaozhuangtai != 1: - return Response({ - 'code': 3, - 'msg': '账号已被封禁,无法申诉', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - except UserDashou.DoesNotExist: - return Response({ - 'code': 2, - 'msg': '用户不是打手身份', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 🔴🆕【关键】获取请求参数 - chufa_id = request.data.get('chufa_id') # 处罚记录ID - shensu_liyou = request.data.get('shensu_liyou', '').strip() # 申诉理由 - shensu_tupian_urls = request.data.get('shensu_tupian_urls', []) # 申诉图片URL数组 - - # 验证必填字段 - if not chufa_id: - return Response({ - 'code': 6, - 'msg': '处罚记录ID不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if not shensu_liyou: - return Response({ - 'code': 7, - 'msg': '申诉理由不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证申诉理由长度 - if len(shensu_liyou) > 500: - return Response({ - 'code': 8, - 'msg': '申诉理由不能超过500字', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证图片数量 - if len(shensu_tupian_urls) > 9: - return Response({ - 'code': 9, - 'msg': '最多只能上传9张图片', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 🔴🆕【关键】使用事务确保数据一致性 - from django.db import transaction - - with transaction.atomic(): - # 1. 查询处罚记录 - try: - chufa_record = Chufajilu.objects.select_for_update().get( - id=chufa_id, - dashouid=yonghuid # 确保只能申诉自己的处罚记录 - ) - except Chufajilu.DoesNotExist: - return Response({ - 'code': 10, - 'msg': '处罚记录不存在或无权申诉', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 2. 验证处罚记录状态(必须是待处理状态0) - if chufa_record.sqzhuangtai != 0: - return Response({ - 'code': 11, - 'msg': '该处罚记录已处理,无法申诉', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 验证订单是否存在(可选,根据需求决定) - # 这里可以根据dingdan_id去查询订单表,但根据需求描述,我们已经有了处罚记录 - # 处罚记录中包含了dingdan_id,如果dingdan_id不存在,说明订单可能被删除 - # 但根据业务逻辑,有处罚记录就应该有订单,所以这里可以跳过这个检查 - - # 4. 检查是否已有申诉记录(防止重复申诉) - if chufa_record.ssliyou or chufa_record.sqzhuangtai == 3: - # 检查是否已经有申诉图片 - existing_shensu_tupian = Chufatupian.objects.filter( - dingdan_id=chufa_record.dingdan_id, - yonghuid=yonghuid - ).exists() - - if existing_shensu_tupian: - return Response({ - 'code': 12, - 'msg': '您已经提交过申诉,请等待处理结果', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 5. 更新处罚记录 - chufa_record.ssliyou = shensu_liyou - chufa_record.sqzhuangtai = 3 # 状态改为申诉中 - chufa_record.save() - - # 6. 保存申诉图片(如果有) - saved_tupian_urls = [] - if shensu_tupian_urls: - for tupian_url in shensu_tupian_urls: - if tupian_url: # 确保URL不为空 - chufa_tupian = Chufatupian.objects.create( - dingdan_id=chufa_record.dingdan_id, - yonghuid=yonghuid, - tupian=tupian_url - ) - saved_tupian_urls.append(tupian_url) - - # 7. 构建返回数据 - response_data = { - 'chufa_id': chufa_record.id, - 'dingdan_id': chufa_record.dingdan_id or '', - 'shensu_liyou': shensu_liyou, - 'shensu_tupian_urls': saved_tupian_urls, - 'sqzhuangtai': chufa_record.sqzhuangtai, - 'update_time': chufa_record.update_time.strftime( - '%Y-%m-%d %H:%M:%S') if chufa_record.update_time else '' - } - - # 记录操作日志 - logger.info(f"打手申诉成功 - 用户ID: {yonghuid}, 处罚ID: {chufa_id}, 订单ID: {chufa_record.dingdan_id}") - - return Response({ - 'code': 0, - 'msg': '申诉提交成功', - 'data': response_data - }) - - except Exception as e: - logger.error(f"打手申诉异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True) - return Response({ - 'code': 99, - 'msg': '系统繁忙,请稍后重试', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - - -class ShangjiaChufaJiluHuoquView(APIView): - """ - 商家获取处罚记录接口 - 路径: /yonghu/sjcfjlhq - 方法: POST - 权限: JWT Token + 商家权限验证 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 获取当前用户信息 - user_main = request.user - yonghuid = user_main.yonghuid - - # 通过反向关系获取商家扩展信息 - try: - shangjia_profile = user_main.shop_profile - # 再次确认账号状态 - if shangjia_profile.zhuangtai != 1: - return Response({ - 'code': 3, - 'msg': '商家账号状态异常,无法查看处罚记录', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - except UserShangjia.DoesNotExist: - return Response({ - 'code': 2, - 'msg': '用户不是商家身份', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 判断请求类型 - qingqiu_tongji = request.data.get('qingqiu_tongji', False) - - if qingqiu_tongji: - return self.get_tongji_info(yonghuid) - else: - return self.get_chufa_list(yonghuid, request.data) - - except Exception as e: - logger.error(f"商家获取处罚记录异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True) - return Response({ - 'code': 99, - 'msg': '系统繁忙,请稍后重试', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def get_tongji_info(self, yonghuid): - """ - 获取统计信息 - """ - try: - # 🔴 查询条件:qingqiuid = 当前商家ID - stats = Chufajilu.objects.filter(qingqiuid=yonghuid).aggregate( - zongshu=Count('id'), - daichuli=Count( - Case( - When(Q(sqzhuangtai=0) | Q(sqzhuangtai=3), then=1), - output_field=IntegerField() - ) - ), - yichuli=Count( - Case( - When(Q(sqzhuangtai=1) | Q(sqzhuangtai=2), then=1), - output_field=IntegerField() - ) - ) - ) - - tongji_data = { - 'zongshu': stats.get('zongshu', 0) or 0, - 'daichuli': stats.get('daichuli', 0) or 0, - 'yichuli': stats.get('yichuli', 0) or 0 - } - - return Response({ - 'code': 0, - 'msg': '获取统计信息成功', - 'data': tongji_data - }) - - except Exception as e: - logger.error(f"商家获取统计信息异常 - 用户ID: {yonghuid}, 错误: {str(e)}") - return Response({ - 'code': 1, - 'msg': '获取统计信息失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def get_chufa_list(self, yonghuid, request_data): - """ - 获取处罚记录列表(商家端) - 🔴 只修改图片查询逻辑,字段保持原样 - """ - try: - # 获取分页参数 - page = int(request_data.get('page', 1)) - page_size = int(request_data.get('page_size', 10)) - - if page_size > 50: - page_size = 50 - if page_size < 1: - page_size = 10 - - # 获取状态筛选参数 - zhuangtai = request_data.get('zhuangtai', 0) - - # 🔴 查询条件:qingqiuid = 当前商家ID - queryset = Chufajilu.objects.filter(qingqiuid=yonghuid) - - if zhuangtai == 0: - queryset = queryset.filter(Q(sqzhuangtai=0) | Q(sqzhuangtai=3)) - elif zhuangtai == 1: - queryset = queryset.filter(Q(sqzhuangtai=1) | Q(sqzhuangtai=2)) - else: - pass - - # 按创建时间倒序排列 - queryset = queryset.order_by('-create_time') - - # 分页逻辑 - paginator = Paginator(queryset, page_size) - - try: - page_obj = paginator.page(page) - except: - return Response({ - 'code': 0, - 'msg': '获取成功', - 'data': { - 'list': [], - 'has_more': False, - 'page': page, - 'page_size': page_size, - 'current_count': 0 - } - }) - - chufa_records = list(page_obj) - - if not chufa_records: - return Response({ - 'code': 0, - 'msg': '获取成功', - 'data': { - 'list': [], - 'has_more': False, - 'page': page, - 'page_size': page_size, - 'current_count': 0 - } - }) - - # 🔴【核心修复】收集所有需要查询图片的组合 - dingdan_ids = [] - dashou_ids = [] - - for record in chufa_records: - if record.dingdan_id: - dingdan_ids.append(record.dingdan_id) - if record.dashouid: # 打手ID - dashou_ids.append(record.dashouid) - - # 去重 - dingdan_ids = list(set(dingdan_ids)) - dashou_ids = list(set(dashou_ids)) - - # 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误) - # 1. 查询证据图片(商家自己上传的) - zhengju_mapping = {} - if dingdan_ids: - # 构建Q对象:dingdan_id在列表中 AND yonghuid=当前商家ID - zhengju_q = Q(dingdan_id__in=dingdan_ids, yonghuid=yonghuid) - zhengju_queryset = Chufatupian.objects.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian') - - for item in zhengju_queryset: - key = f"{item['dingdan_id']}_{item['yonghuid']}" - if key not in zhengju_mapping: - zhengju_mapping[key] = [] - if item['tupian']: - zhengju_mapping[key].append(item['tupian']) - - # 2. 查询申诉图片(打手上传的) - shensu_mapping = {} - if dingdan_ids and dashou_ids: - # 构建Q对象:dingdan_id在列表中 AND yonghuid在打手ID列表中 - shensu_q = Q(dingdan_id__in=dingdan_ids, yonghuid__in=dashou_ids) - shensu_queryset = Chufatupian.objects.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian') - - for item in shensu_queryset: - key = f"{item['dingdan_id']}_{item['yonghuid']}" - if key not in shensu_mapping: - shensu_mapping[key] = [] - if item['tupian']: - shensu_mapping[key].append(item['tupian']) - - # 🔴【核心修复】组装返回数据 - 字段保持原样! - chufa_list = [] - for record in chufa_records: - dingdan_id = record.dingdan_id - dashou_id = record.dashouid - - # 获取证据图片(商家自己上传的) - zhengju_tupian = [] - if dingdan_id: - key = f"{dingdan_id}_{yonghuid}" - zhengju_tupian = zhengju_mapping.get(key, []) - - # 获取申诉图片(打手上传的) - shensu_tupian = [] - if dingdan_id and dashou_id: - key = f"{dingdan_id}_{dashou_id}" - shensu_tupian = shensu_mapping.get(key, []) - - # 🔴【字段保持原样!】与前端完全对应! - chufa_data = { - 'id': record.id, - 'dingdan_id': record.dingdan_id or '', - 'qingqiuid': record.dashouid or '', # 打手ID - 'cfliyou': record.cfliyou or '', - 'sqzhuangtai': record.sqzhuangtai or 0, - 'ssliyou': record.ssliyou or '', - 'jifen': record.jifen or 0, - 'create_time': record.create_time.strftime('%Y-%m-%d %H:%M:%S') if record.create_time else '', - 'update_time': record.update_time.strftime('%Y-%m-%d %H:%M:%S') if record.update_time else '', - 'zhengju_tupian': zhengju_tupian, # 商家自己上传的证据图片 - 'shensu_tupian': shensu_tupian, # 打手上传的申诉图片 - } - chufa_list.append(chufa_data) - - # 判断是否还有更多数据 - has_more = page_obj.has_next() - - response_data = { - 'list': chufa_list, - 'has_more': has_more, - 'page': page, - 'page_size': page_size, - 'current_count': len(chufa_list) - } - - return Response({ - 'code': 0, - 'msg': '获取成功', - 'data': response_data - }) - - except ValueError as e: - logger.error(f"商家端参数错误 - 用户ID: {yonghuid}, 错误: {str(e)}") - return Response({ - 'code': 4, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - logger.error(f"商家获取处罚记录列表异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True) - return Response({ - 'code': 5, - 'msg': '获取记录列表失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - - - - - - - - - - - - - - - - - -from .models import UserMain, UserKefu - -logger = logging.getLogger(__name__) - -class KefuLoginView(APIView): - """ - 客服登录接口(支持手机号重复) - 请求:POST /yonghu/kefujinru - 请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"} - 返回: - code=0 成功,data包含token,nicheng,yonghuid - code=4 账号被封禁(提示“账号已被封禁”) - 其他错误统一 code=1 msg="登录失败" - """ - throttle_classes = [AnonRateThrottle] # 限制匿名请求频率 - permission_classes = [AllowAny] # 任何人都可访问 - - def post(self, request): - # 1. 获取参数 - phone = request.data.get('phone', '').strip() - password = request.data.get('password', '') - erjimima = request.data.get('erjimima', '') - - if not phone or not password or not erjimima: - return Response({ - 'code': 1, - 'msg': '登录失败', # 统一提示,不暴露具体原因 - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 获取客户端真实IP - client_ip = self.get_client_ip(request) - - # 3. 查询所有手机号匹配且类型为客服的用户 - # 注意:UserMain 中需要有 user_type 字段,且值为 'kefu' 代表客服 - user_mains = UserMain.objects.filter( - phone=phone, - user_type='kefu' # 只查找客服账号 - ).select_related('kefu_profile') # 预加载客服扩展表 - - if not user_mains.exists(): - logger.warning(f"登录失败,手机号不存在或非客服: {phone}, IP: {client_ip}") - return Response({ - 'code': 1, - 'msg': '登录失败', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 遍历所有匹配的用户,验证密码和二级密码 - target_user = None - target_kefu = None - - for user in user_mains: - # 验证主密码(明文比较,按您的要求) - if user.password != password: - continue - - # 检查是否存在客服扩展表 - try: - kefu = user.kefu_profile - except UserKefu.DoesNotExist: - continue - - # 验证二级密码(明文比较) - if kefu.erjimima != erjimima: - continue - - # 如果账号被禁用,记录但继续查找(可能还有其他正常账号?) - if kefu.zhuangtai != 1: - # 如果找到被禁用的,记录下来,但继续查找是否有正常账号 - if target_user is None: - # 暂时记录这个禁用账号,但继续遍历 - target_user = user - target_kefu = kefu - continue - - # 找到完全匹配且状态正常的账号,立即使用 - target_user = user - target_kefu = kefu - break - else: - # 循环结束未找到正常账号,但可能找到了禁用账号 - if target_user and target_kefu: - # 只有被禁用的账号匹配,返回封禁提示 - logger.warning(f"登录失败,客服账号已禁用: {phone}, IP: {client_ip}") - return Response({ - 'code': 4, - 'msg': '账号已被封禁', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - else: - # 没有任何账号匹配(密码/二级密码错误) - logger.warning(f"登录失败,密码或二级密码错误: {phone}, IP: {client_ip}") - return Response({ - 'code': 1, - 'msg': '登录失败', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 5. 更新IP和最后登录时间 - target_user.ip = client_ip - target_user.last_login_time = timezone.now() - target_user.save(update_fields=['ip', 'last_login_time']) - - # 6. 生成JWT token - refresh = RefreshToken.for_user(target_user) - token = str(refresh.access_token) - - # 7. 返回成功数据 - return Response({ - 'code': 0, - 'msg': '登录成功', - 'data': { - 'token': token, - 'nicheng': target_kefu.nicheng, - 'yonghuid': target_user.yonghuid, - } - }) - - def get_client_ip(self, request): - """ - 获取客户端真实IP - """ - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0].strip() - if ip: - return ip - return request.META.get('REMOTE_ADDR', '0.0.0.0') - - - - - - -logger = logging.getLogger(__name__) - -class KefuStatsView(APIView): - """ - 客服统计数据接口 - 请求:POST /yonghu/kfjrhqzl - 请求体:{"phone": "13800138000"} - 认证:必须携带有效的 JWT token(放在 Authorization 头) - 返回: - code=0 成功,data 包含 jinrichuli, jinyuechuli, zongchuli - 验证失败统一返回 401,不透露具体原因 - """ - - permission_classes = [IsAuthenticated] # 必须登录 - - - def post(self, request): - # 1. 获取请求中的 phone - phone = request.data.get('phone', '').strip() - if not phone: - # 缺少参数,但按约定返回 401(可以改为 400,但为了统一,用401) - logger.warning(f"统计接口缺少 phone 参数,用户: {request.user.yonghuid}") - return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 获取当前登录用户(从 JWT 中解析) - current_user = request.user - - # 3. 验证前端传来的 phone 是否与当前用户的 phone 一致 - if current_user.phone != phone: - # 不一致,可能 token 被冒用或参数错误 - logger.warning(f"统计接口 phone 不匹配: 请求phone={phone}, 用户phone={current_user.phone}") - return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 4. 验证用户类型是否为客服 - if current_user.user_type != 'kefu': - logger.warning(f"统计接口用户类型非客服: {current_user.yonghuid}") - return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 5. 尝试获取客服扩展表数据 - try: - kefu_profile = current_user.kefu_profile # OneToOneField 反向关系 - except UserKefu.DoesNotExist: - logger.warning(f"统计接口客服扩展表不存在: {current_user.yonghuid}") - return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 6. 返回统计数据(字段名与前端约定一致) - return Response({ - 'code': 0, - 'data': { - 'jinrichuli': kefu_profile.jinrichuli, - 'jinyuechuli': kefu_profile.jinyuechuli, - 'zongchuli': kefu_profile.zongchuli, - } - }, status=status.HTTP_200_OK) - - - - - - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser -import logging -from django.core.exceptions import ObjectDoesNotExist - -from yonghu.models import UserMain, UserKefu -from shangpin.models import ShangpinLeixing # 假设商品类型表名,请根据实际调整 - -logger = logging.getLogger(__name__) - -class KefuGetOrderTypesView(APIView): - """ - 客服获取订单类型列表接口(平台订单用) - 请求:POST /yonghu/kfhqptddlx - 参数:{"phone": "13800138000"} - 认证:JWT - 返回:code=0 + 类型列表 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - phone = request.data.get('phone', '').strip() - if not phone: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - current_user = request.user - - # 2. 验证手机号一致性 - if getattr(current_user, 'phone', '') != phone: - logger.warning(f"手机号不匹配: 请求phone={phone}, 用户phone={current_user.phone}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 3. 验证用户类型及客服状态 - if current_user.user_type != 'kefu': - logger.warning(f"用户类型非客服: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - kefu_profile = current_user.kefu_profile - except ObjectDoesNotExist: - logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if kefu_profile.zhuangtai != 1: - logger.warning(f"客服账号已被禁用: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 4. 查询订单类型(商品类型) - try: - # 只查询需要的字段,减少数据传输 - types_qs = ShangpinLeixing.objects.all().only('id', 'jieshao', 'tupian_url') - type_list = [ - { - 'id': t.id, - 'biaoti': t.jieshao or '', # 假设商品类型的标题字段为 jieshao - 'tupian_url': t.tupian_url or '' - } - for t in types_qs - ] - return Response({'code': 0, 'data': type_list}) - except Exception as e: - logger.error(f"查询商品类型表失败: {str(e)}") - # 表不存在或查询失败,返回空列表 - return Response({'code': 0, 'data': []}) - - - - - - - -import time -import logging -from django.db.models import Q, Count -from django.core.exceptions import ObjectDoesNotExist -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser - -from yonghu.models import UserMain, UserKefu, UserBoss -from dingdan.models import Dingdan, DingdanPingtai - -logger = logging.getLogger(__name__) - - - - - -from rest_framework.views import APIView - -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser -from django.db.models import Q, Count -from django.core.paginator import Paginator - -class KefuGetOrderListView(APIView): - """ - 客服获取平台订单列表接口(发单平台=1,非跨平台) - 需要权限:002ab(平台订单管理) - 请求参数: - username: 客服账号(用于权限校验) - page: 页码 - pageSize: 每页条数 - dingdan_id: 订单ID(精确搜索) - laoban_id: 老板ID(搜索平台扩展表中的老板) - dashou_id: 打手ID(搜索主表中的接单打手) - leixing: 商品类型ID - zhuangtai: 状态(多个用逗号分隔,如 "1,7") - date: 日期(YYYY-MM-DD),筛选订单创建时间 - nicheng: 游戏昵称(订单主表的 nicheng 字段) - jieshao: 订单介绍(订单主表的 jieshao 字段) - clkf: 处理客服(订单主表的 clkf 字段) - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - start_total = time.time() - - # 1. 获取前端参数 - username = request.data.get('phone', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 3. 检查平台订单管理权限(002ab) - if '002ab' not in permissions: - return Response({'code': 403, 'msg': '您无权查看平台订单列表'}) - - # 4. 获取分页和筛选参数 - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('pageSize', 20)) - - dingdan_id = request.data.get('dingdan_id', '').strip() - laoban_id = request.data.get('laoban_id', '').strip() - dashou_id = request.data.get('dashou_id', '').strip() - leixing = request.data.get('leixing') - zhuangtai = request.data.get('zhuangtai', '') - date = request.data.get('date', '').strip() - # 新增筛选条件 - nicheng = request.data.get('nicheng', '').strip() - jieshao = request.data.get('jieshao', '').strip() - clkf = request.data.get('clkf', '').strip() - - # ---------- 查询条件 ---------- - # 基础条件:平台发单 + 非跨平台 - base_q = Q(fadan_pingtai=1) & Q(is_cross=0) - - if dingdan_id: - q_conditions = base_q & Q(dingdan_id=dingdan_id) - else: - q_conditions = base_q - if leixing is not None: - try: - leixing_int = int(leixing) - q_conditions &= Q(leixing_id=leixing_int) - except (ValueError, TypeError): - pass - if zhuangtai and zhuangtai != 'all': - try: - status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()] - if status_list: - q_conditions &= Q(zhuangtai__in=status_list) - except (ValueError, TypeError): - pass - if dashou_id: - q_conditions &= Q(jiedan_dashou_id=dashou_id) - if laoban_id: - q_conditions &= Q(pingtai_kuozhan__laoban_id=laoban_id) - if date: - q_conditions &= Q(create_time__date=date) - # 新增筛选条件 - if nicheng: - q_conditions &= Q(nicheng__icontains=nicheng) - if jieshao: - q_conditions &= Q(jieshao__icontains=jieshao) - if clkf: - q_conditions &= Q(clkf__icontains=clkf) - - # ---------- 统计(仅按发单平台,非跨平台) ---------- - stats = Dingdan.objects.filter(fadan_pingtai=1, is_cross=0).aggregate( - total_orders=Count('id'), - completed_orders=Count('id', filter=Q(zhuangtai=3)), - refund_orders=Count('id', filter=Q(zhuangtai=5)), - pending_orders=Count('id', filter=Q(zhuangtai__in=[4,8])) - ) - - # ---------- 状态计数(所有状态都返回,非跨平台) ---------- - status_counts = { - 'all': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0).count(), - '1,7': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai__in=[1,7]).count(), - '2': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=2).count(), - '8': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=8).count(), - '3': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=3).count(), - '4': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=4).count(), - '5': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=5).count(), - '6': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=6).count(), - } - - # ---------- 分页查询 ---------- - total_count = Dingdan.objects.filter(q_conditions).count() - - orders_qs = Dingdan.objects.filter(q_conditions).select_related( - 'pingtai_kuozhan' - ).only( - 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', 'fadan_pingtai', - 'jine', 'tupian', 'create_time', 'nicheng', 'jiedan_dashou_id', - 'pingtai_kuozhan__laoban_id' - ).order_by('-create_time')[(page-1)*page_size : page*page_size] - - # ---------- 获取老板昵称 ---------- - laoban_ids = [o.pingtai_kuozhan.laoban_id for o in orders_qs if hasattr(o, 'pingtai_kuozhan') and o.pingtai_kuozhan.laoban_id] - nickname_map = {} - if laoban_ids: - users = UserMain.objects.filter(yonghuid__in=laoban_ids).select_related('boss_profile').only( - 'yonghuid', 'boss_profile__nickname' - ) - for user in users: - nickname_map[user.yonghuid] = user.boss_profile.nickname or '' if hasattr(user, 'boss_profile') else '' - - result_list = [] - for order in orders_qs: - ext = getattr(order, 'pingtai_kuozhan', None) - laoban_id = ext.laoban_id if ext else '' - youxi_nicheng = nickname_map.get(laoban_id, order.nicheng or '') - result_list.append({ - 'dingdan_id': order.dingdan_id or '', - 'jieshao': order.jieshao or '', - 'zhuangtai': order.zhuangtai, - 'leixing': order.leixing_id, - 'fadanpingtai': order.fadan_pingtai, - 'jiage': float(order.jine) if order.jine else 0.00, - 'tupian_url': order.tupian or '', - 'fadanshijian': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '', - 'youxi_nicheng': youxi_nicheng - }) - - response_data = { - 'stats': { - 'totalOrders': stats['total_orders'] or 0, - 'completedOrders': stats['completed_orders'] or 0, - 'refundOrders': stats['refund_orders'] or 0, - 'pendingOrders': stats['pending_orders'] or 0 - }, - 'statusCounts': status_counts, - 'list': result_list, - 'total': total_count, - 'page': page, - 'pageSize': page_size - } - - logger.info(f"平台订单接口总耗时: {time.time()-start_total:.3f}s") - return Response({'code': 0, 'data': response_data}) - -class KefuGetShangjiaOrderListView(APIView): - """ - 客服获取商家订单列表接口(发单平台=2,非跨平台) - 需要权限:002ac(商家订单管理)—— 如不区分可使用 002ab - 请求参数: - username: 客服账号(用于权限校验) - page: 页码 - pageSize: 每页条数 - dingdan_id: 订单ID(精确搜索) - shangjia_id: 商家ID(搜索商家扩展表中的商家ID) - dashou_id: 打手ID(搜索主表中的接单打手) - leixing: 商品类型ID - zhuangtai: 状态(多个用逗号分隔,如 "1,7") - date: 日期(YYYY-MM-DD),筛选订单创建时间 - nicheng: 游戏昵称(订单主表的 nicheng 字段) - jieshao: 订单介绍(订单主表的 jieshao 字段) - clkf: 处理客服(订单主表的 clkf 字段) - shangjia_nicheng: 商家昵称(商家扩展表中的昵称) - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - start_total = time.time() - - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 3. 检查商家订单管理权限(使用 002ac,如果没有可改用 002ab) - if '002ac' not in permissions: - return Response({'code': 403, 'msg': '您无权查看商家订单列表'}) - - # 4. 获取分页和筛选参数 - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('pageSize', 20)) - - dingdan_id = request.data.get('dingdan_id', '').strip() - shangjia_id = request.data.get('shangjia_id', '').strip() - dashou_id = request.data.get('dashou_id', '').strip() - leixing = request.data.get('leixing') - zhuangtai = request.data.get('zhuangtai', '') - date = request.data.get('date', '').strip() - # 新增筛选条件 - nicheng = request.data.get('nicheng', '').strip() - jieshao = request.data.get('jieshao', '').strip() - clkf = request.data.get('clkf', '').strip() - shangjia_nicheng = request.data.get('shangjia_nicheng', '').strip() - - # ---------- 查询条件 ---------- - # 基础条件:商家发单 + 非跨平台 - base_q = Q(fadan_pingtai=2) & Q(is_cross=0) - - if dingdan_id: - q_conditions = base_q & Q(dingdan_id=dingdan_id) - else: - q_conditions = base_q - if leixing is not None: - try: - leixing_int = int(leixing) - q_conditions &= Q(leixing_id=leixing_int) - except (ValueError, TypeError): - pass - if zhuangtai and zhuangtai != 'all': - try: - status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()] - if status_list: - q_conditions &= Q(zhuangtai__in=status_list) - except (ValueError, TypeError): - pass - if dashou_id: - q_conditions &= Q(jiedan_dashou_id=dashou_id) - if shangjia_id: - q_conditions &= Q(shangjia_kuozhan__shangjia_id=shangjia_id) - if date: - q_conditions &= Q(create_time__date=date) - if nicheng: - q_conditions &= Q(nicheng__icontains=nicheng) - if jieshao: - q_conditions &= Q(jieshao__icontains=jieshao) - if clkf: - q_conditions &= Q(clkf__icontains=clkf) - if shangjia_nicheng: - # 通过商家扩展表关联 UserShangjia 的昵称 - q_conditions &= Q(shangjia_kuozhan__shangjia_id__in=UserMain.objects.filter( - shop_profile__nicheng__icontains=shangjia_nicheng - ).values_list('yonghuid', flat=True)) - - # ---------- 统计(仅按发单平台,非跨平台) ---------- - stats = Dingdan.objects.filter(fadan_pingtai=2, is_cross=0).aggregate( - total_orders=Count('id'), - completed_orders=Count('id', filter=Q(zhuangtai=3)), - refund_orders=Count('id', filter=Q(zhuangtai=5)), - pending_orders=Count('id', filter=Q(zhuangtai__in=[4,8])) - ) - - # ---------- 状态计数(所有状态都返回,非跨平台) ---------- - status_counts = { - 'all': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0).count(), - '1,7': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai__in=[1,7]).count(), - '2': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=2).count(), - '8': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=8).count(), - '3': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=3).count(), - '4': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=4).count(), - '5': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=5).count(), - '6': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=6).count(), - } - - # ---------- 分页查询 ---------- - total_count = Dingdan.objects.filter(q_conditions).count() - - orders_qs = Dingdan.objects.filter(q_conditions).select_related( - 'shangjia_kuozhan' - ).only( - 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', 'fadan_pingtai', - 'jine', 'tupian', 'create_time', 'nicheng', 'jiedan_dashou_id', - 'shangjia_kuozhan__shangjia_id' - ).order_by('-create_time')[(page-1)*page_size : page*page_size] - - # ---------- 获取商家昵称 ---------- - shangjia_ids = [o.shangjia_kuozhan.shangjia_id for o in orders_qs if hasattr(o, 'shangjia_kuozhan') and o.shangjia_kuozhan.shangjia_id] - nickname_map = {} - if shangjia_ids: - users = UserMain.objects.filter(yonghuid__in=shangjia_ids).select_related('shop_profile').only( - 'yonghuid', 'shop_profile__nicheng' - ) - for user in users: - nickname_map[user.yonghuid] = user.shop_profile.nicheng or '' if hasattr(user, 'shop_profile') else '' - - result_list = [] - for order in orders_qs: - ext = getattr(order, 'shangjia_kuozhan', None) - shangjia_id = ext.shangjia_id if ext else '' - youxi_nicheng = order.nicheng or '' # 商家订单的玩家昵称 - result_list.append({ - 'dingdan_id': order.dingdan_id or '', - 'jieshao': order.jieshao or '', - 'zhuangtai': order.zhuangtai, - 'leixing': order.leixing_id, - 'fadanpingtai': order.fadan_pingtai, - 'jiage': float(order.jine) if order.jine else 0.00, - 'tupian_url': order.tupian or '', - 'fadanshijian': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '', - 'youxi_nicheng': youxi_nicheng - }) - - response_data = { - 'stats': { - 'totalOrders': stats['total_orders'] or 0, - 'completedOrders': stats['completed_orders'] or 0, - 'refundOrders': stats['refund_orders'] or 0, - 'pendingOrders': stats['pending_orders'] or 0 - }, - 'statusCounts': status_counts, - 'list': result_list, - 'total': total_count, - 'page': page, - 'pageSize': page_size - } - - logger.info(f"商家订单接口总耗时: {time.time()-start_total:.3f}s") - return Response({'code': 0, 'data': response_data}) - - - - - - - - - - - - - - - - - - - - -import logging -from django.db import transaction -from django.core.exceptions import ObjectDoesNotExist -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser - -from yonghu.models import UserMain, UserKefu, UserDashou -from dingdan.models import Dingdan -from shangpin.models import Huiyuangoumai # 假设会员购买模型存在 - -logger = logging.getLogger(__name__) - -class KefuChangeDashouView(APIView): - """ - 客服更换打手接口 - 请求:POST /yonghu/kefughds - 参数:{ - "phone": "客服手机号", - "dingdan_id": "订单ID", - "new_dashou_id": "新打手ID" - } - 认证:JWT - 返回:code=0 成功 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - phone = request.data.get('phone', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - new_dashou_id = request.data.get('new_dashou_id', '').strip() - - if not phone or not dingdan_id or not new_dashou_id: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 客服身份验证 - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - logger.warning(f"手机号不匹配: {phone}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if current_user.user_type != 'kefu': - logger.warning(f"用户类型非客服: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - kefu = current_user.kefu_profile - except ObjectDoesNotExist: - logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if kefu.zhuangtai != 1: - logger.warning(f"客服账号已禁用: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 3. 查询订单 - try: - order = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) - - # 4. 验证订单状态是否允许更换打手(进行中2 或 结算中8) - if order.zhuangtai not in [2, 8]: - return Response({'code': 400, 'msg': '订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST) - - # 5. 查询新打手 - try: - new_dashou_user = UserMain.objects.get(yonghuid=new_dashou_id) - except UserMain.DoesNotExist: - return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND) - - # 6. 验证打手扩展表 - try: - new_dashou_profile = new_dashou_user.dashou_profile - except ObjectDoesNotExist: - return Response({'code': 404, 'msg': '打手信息不完整'}, status=status.HTTP_404_NOT_FOUND) - - # 7. 验证打手状态(打手状态和账号状态必须为1) - if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1: - return Response({'code': 400, 'msg': '打手状态不允许接单'}, status=status.HTTP_400_BAD_REQUEST) - # 8. 验证打手积分是否足够(至少5分) - if new_dashou_profile.jifen < 5: - return Response({'code': 400, 'msg': '打手积分不足,无法接单'}, status=status.HTTP_400_BAD_REQUEST) - - # 8. 根据订单的抢单要求类型进行额外验证 - yaoqiuleixing = order.yaoqiuleixing - - if yaoqiuleixing == 1: # 会员抢单 - huiyuan_id = order.huiyuan_id - if not huiyuan_id: - return Response({'code': 400, 'msg': '订单会员ID为空'}, status=status.HTTP_400_BAD_REQUEST) - - try: - # 查询打手的会员购买记录 - huiyuan_goumai = Huiyuangoumai.objects.get( - huiyuan_id=huiyuan_id, - yonghu_id=new_dashou_id # 假设 yonghu_id 字段存储用户ID - ) - except Huiyuangoumai.DoesNotExist: - return Response({'code': 400, 'msg': '打手未购买该订单要求的会员'}, status=status.HTTP_400_BAD_REQUEST) - - # 检查会员是否过期(调用模型方法) - is_expired = huiyuan_goumai.jiance_shifou_daoqi() - if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1: - return Response({'code': 400, 'msg': '打手的会员已过期'}, status=status.HTTP_400_BAD_REQUEST) - - elif yaoqiuleixing == 2: # 押金抢单 - order_yajin = order.yongjin # 假设订单的 yongjin 字段存储押金要求 - if order_yajin is None: - return Response({'code': 400, 'msg': '订单押金为空'}, status=status.HTTP_400_BAD_REQUEST) - - if new_dashou_profile.yajin < order_yajin: - return Response({'code': 400, 'msg': '打手押金不足'}, status=status.HTTP_400_BAD_REQUEST) - else: - return Response({'code': 400, 'msg': '订单抢单要求类型错误'}, status=status.HTTP_400_BAD_REQUEST) - - # 9. 所有验证通过,开始更换打手(使用事务) - with transaction.atomic(): - # 9.1 更新新打手扩展表 - new_dashou_profile.zhuangtai = 0 # 设为接单中 - new_dashou_profile.jiedanzongliang += 1 - new_dashou_profile.jinrijiedan += 1 - new_dashou_profile.jinyuejiedan += 1 - new_dashou_profile.save() - - # 9.2 处理原打手(如果存在) - old_dashou_id = order.jiedan_dashou_id - if old_dashou_id: - try: - old_dashou_user = UserMain.objects.get(yonghuid=old_dashou_id) - old_dashou_profile = old_dashou_user.dashou_profile - old_dashou_profile.zhuangtai = 1 # 恢复空闲 - old_dashou_profile.tuikuanliang += 1 # 原打手退款总量+1(业务逻辑) - old_dashou_profile.save() - logger.info(f"原打手 {old_dashou_id} 状态已更新") - except (UserMain.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"原打手 {old_dashou_id} 不存在或信息不完整,跳过更新") - except Exception as e: - logger.error(f"更新原打手时异常: {str(e)}", exc_info=True) - - # 9.3 更新订单 - order.jiedan_dashou_id = new_dashou_id - order.zhuangtai = 2 # 进行中 - order.clkf = current_user.yonghuid # 记录处理客服ID - order.save() - - return Response({'code': 0, 'msg': '更换打手成功'}) - - - - - -class KefuRecoverOrderView(APIView): - """ - 客服恢复订单接口(仅状态9可恢复) - 请求:POST /yonghu/kefuhf - 参数:{ - "phone": "客服手机号", - "dingdan_id": "订单ID" - } - 认证:JWT - 返回:code=0 成功 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - phone = request.data.get('phone', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - - if not phone or not dingdan_id: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 客服身份验证 - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if current_user.user_type != 'kefu': - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - kefu = current_user.kefu_profile - except ObjectDoesNotExist: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if kefu.zhuangtai != 1: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 3. 查询订单 - try: - order = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) - - # 4. 验证订单状态是否为9(未付款) - if order.zhuangtai != 9: - return Response({'code': 400, 'msg': '订单状态不允许恢复'}, status=status.HTTP_400_BAD_REQUEST) - - # 5. 根据是否有指定ID决定新状态 - if order.zhiding_id: - new_status = 7 # 指定中 - else: - new_status = 1 # 下单中 - - # 6. 更新订单 - order.zhuangtai = new_status - order.clkf = current_user.yonghuid - order.save() - - return Response({'code': 0, 'msg': '恢复成功', 'data': {'new_status': new_status}}) - - - - - - - - -# dingdan/views.py -import logging -import random -import hashlib -import requests -import xmltodict -import time -from decimal import Decimal -from django.db import transaction -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser - -from yonghu.models import UserMain, UserKefu, UserDashou -from dingdan.models import Dingdan, DingdanPingtai, Tuikuanjilu # 注意:Tuikuanjilu 模型未在本次提供,但假设存在 - -logger = logging.getLogger(__name__) - - -class KefuPlatformRefundView(APIView): - """ - 客服平台订单退款接口(调用微信支付) - 请求:POST /yonghu/kefupttk - 参数:{ - "phone": "客服手机号", - "dingdan_id": "订单ID", - "tuikuan_liyou": "退款理由(可选)" - } - 认证:JWT - 返回:code=0 成功,code=其他 失败 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - phone = request.data.get('phone', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() - - if not phone or not dingdan_id: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 客服身份验证 - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - logger.warning(f"手机号不匹配: {phone}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if current_user.user_type != 'kefu': - logger.warning(f"用户类型非客服: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - kefu = current_user.kefu_profile - except ObjectDoesNotExist: - logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if kefu.zhuangtai != 1: - logger.warning(f"客服账号已禁用: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 3. 查询订单并加锁 - try: - with transaction.atomic(): - order = Dingdan.objects.select_related('pingtai_kuozhan').select_for_update().get(dingdan_id=dingdan_id) - - # 4. 验证平台订单 - if order.fadan_pingtai != 1: - return Response({'code': 400, 'msg': '该订单不是平台订单'}, status=status.HTTP_400_BAD_REQUEST) - - # 5. 验证状态允许退款(根据模型,状态值:1下单中,2进行中,3已完成,4退款审核,5已退款,6退款失败,7指定中,8结算中) - allowed_statuses = [1, 2, 4, 7, 8] # 允许退款的已支付或处理中状态 - if order.zhuangtai not in allowed_statuses: - return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST) - - # 6. 获取微信交易号(如果存在) - wechat_transaction_id = getattr(order, 'wechat_transaction_id', None) - - # 7. 调用微信退款接口 - refund_result = self.call_wechat_refund( - dingdan_id=order.dingdan_id, - jine=order.jine, - transaction_id=wechat_transaction_id - ) - - if refund_result['code'] != 0: - # 微信退款失败,直接返回错误,数据库不更新 - logger.error(f"微信退款失败,订单 {dingdan_id},原因:{refund_result['msg']}") - return Response({'code': 400, 'msg': refund_result['msg']}, status=status.HTTP_400_BAD_REQUEST) - - # 8. 更新订单状态为5(已退款) - order.zhuangtai = 5 - order.clkf = current_user.phone # 记录处理客服手机号 - if tuikuan_liyou: - order.tkly = tuikuan_liyou - order.save() - - # 9. 更新打手数据 - jiedan_dashou_id = order.jiedan_dashou_id - if jiedan_dashou_id: - try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.tuikuanliang += 1 - dashou_profile.zhuangtai = 1 # 打手状态设为空闲 - dashou_profile.save() - logger.info(f"平台退款:打手 {jiedan_dashou_id} 退款订单+1") - except (UserMain.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"平台退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新") - - # ========== 新增:商品和店铺每日统计 ========== - try: - # 商品每日统计(只要有商品ID就执行) - if order.shangpin_id: - # 从扩展表获取已计算好的收益值(下单时已存储) - pingtai_shouyi = Decimal('0.00') - dianpu_shouyi = Decimal('0.00') - if hasattr(order, 'pingtai_kuozhan'): - pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') - dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - - update_shangpin_daily_stat( - shangpin_id=order.shangpin_id, - caozuo_leixing=3, # 3 = 退款 - dingdan_jiage=order.jine, - pingtai_shouyi=pingtai_shouyi, - dianpu_zongshouyi=dianpu_shouyi - ) - logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") - - # 店铺每日统计(仅当存在店铺ID时执行) - dianpu_id = None - if hasattr(order, 'pingtai_kuozhan'): - dianpu_id = order.pingtai_kuozhan.dianpu_id - if dianpu_id: - dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - update_dianpu_daily_stat( - dianpu_id=dianpu_id, - caozuo_leixing=3, # 3 = 退款 - dingdan_jiage=order.jine, - dianpu_shouyi=dianpu_shouyi - ) - logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") - except Exception as e: - logger.error(f"商品/店铺统计更新失败: {e}") - logger.error(traceback.format_exc()) - # 统计失败不影响主流程 - # ============================================= - - # 10. 更新退款记录表(假设存在 Tuikuanjilu 模型,字段需根据实际调整) - - #更新支出记录 - from dingdan.utils import update_daily_payout - update_daily_payout(order.jine) - - - - - try: - from dingdan.models import Tuikuanjilu # 确保模型已导入 - refund_record, created = Tuikuanjilu.objects.get_or_create( - dingdan_id=dingdan_id, - defaults={ - 'dashouid': jiedan_dashou_id or '', - 'qingqiuid': '', - 'chuliid': phone, - 'liyou': tuikuan_liyou or '客服退款', - 'sqzhuangtai': 1, # 成功 - 'jine': order.jine, - 'dashou_fencheng': order.dashou_fencheng or 0, - 'jieshao': order.jieshao or '', - 'beizhu': '客服平台退款', - 'nicheng': order.nicheng or '', - } - ) - if not created: - refund_record.sqzhuangtai = 1 - refund_record.chuliid = phone - if tuikuan_liyou: - refund_record.liyou = tuikuan_liyou - refund_record.save() - except Exception as e: - logger.error(f"平台退款更新退款记录异常: {str(e)}", exc_info=True) - - # 11. 更新客服统计数据 - kefu.jinrichuli += 1 - kefu.jinyuechuli += 1 - kefu.zongchuli += 1 - kefu.save() - - # 🔥 新增:调用打手每日统计(成交) - try: - update_dashou_daily_by_action( - yonghuid=order.jiedan_dashou_id, - amount=order.dashou_fencheng, - action=3 # 3 表示退款(退款) - ) - logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - # 不影响主流程 - - logger.info(f"平台退款成功,订单 {dingdan_id},退款单号 {refund_result.get('refund_id')}") - return Response({'code': 0, 'msg': '退款成功'}) - - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - logger.error(f"平台退款接口异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def call_wechat_refund(self, dingdan_id, jine, transaction_id=None): - """ - 调用微信支付退款接口 - :param dingdan_id: 商户订单号(我们的订单ID) - :param jine: 退款金额 - :param transaction_id: 微信支付订单号(优先使用) - :return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'} - """ - # 获取配置 - appid = getattr(settings, 'WEIXIN_APPID', '') - mch_id = getattr(settings, 'WEIXIN_MCHID', '') - key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') - cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '') - key_path = getattr(settings, 'WEIXIN_KEY_PATH', '') - - if not all([appid, mch_id, key, cert_path, key_path]): - missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v] - logger.error(f"微信支付配置缺失: {missing}") - return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'} - - # 生成退款单号 - out_refund_no = self.generate_refund_no(dingdan_id) - total_fee = int(float(jine) * 100) - refund_fee = total_fee - refund_desc = '客服退款' - - # 构造请求参数 - params = { - 'appid': appid, - 'mch_id': mch_id, - 'nonce_str': self.generate_nonce_str(), - 'out_refund_no': out_refund_no, - 'total_fee': total_fee, - 'refund_fee': refund_fee, - 'refund_desc': refund_desc, - } - if transaction_id: - params['transaction_id'] = transaction_id - else: - params['out_trade_no'] = dingdan_id # 使用订单ID作为商户订单号 - - # 生成签名 - sorted_keys = sorted(params.keys()) - stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys]) - stringSignTemp = f"{stringA}&key={key}" - sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() - params['sign'] = sign - - xml_data = self.dict_to_xml(params) - url = 'https://api.mch.weixin.qq.com/secapi/pay/refund' - - try: - response = requests.post( - url, - data=xml_data, - cert=(cert_path, key_path), - timeout=10 - ) - result = xmltodict.parse(response.content)['xml'] - logger.info(f"微信退款响应: {result}") - except Exception as e: - logger.error(f"微信退款请求异常: {str(e)}", exc_info=True) - return {'code': 500, 'msg': '微信退款请求异常'} - - if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': - return {'code': 0, 'msg': 'success', 'refund_id': out_refund_no} - else: - err_msg = result.get('err_code_des', result.get('return_msg', '未知错误')) - logger.warning(f"微信退款失败: {err_msg}") - return {'code': 400, 'msg': err_msg} - - def generate_nonce_str(self): - import string - return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - - def generate_refund_no(self, dingdan_id): - timestamp = str(int(time.time())) - rand = ''.join(random.choices('0123456789', k=4)) - return f"{dingdan_id}REF{timestamp}{rand}" - - def dict_to_xml(self, data): - xml = [''] - for k, v in data.items(): - xml.append(f'<{k}>{v}') - xml.append('') - return ''.join(xml) - - - - - -# dingdan/views.py -import logging -from django.db import transaction -from django.core.exceptions import ObjectDoesNotExist -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser - -from yonghu.models import UserMain, UserKefu, UserDashou, UserShangjia -from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu - -logger = logging.getLogger(__name__) - - -class KefuRejectRefundView(APIView): - """ - 客服拒绝退款接口 - 请求:POST /yonghu/kefujjtk - 参数:{ - "phone": "客服手机号", - "dingdan_id": "订单ID", - "jujue_liyou": "拒绝理由(可选)" - } - 认证:JWT - 返回:code=0 成功 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - phone = request.data.get('phone', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - jujue_liyou = request.data.get('jujue_liyou', '').strip() - - if not phone or not dingdan_id: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 客服身份验证 - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if current_user.user_type != 'kefu': - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - kefu = current_user.kefu_profile - except ObjectDoesNotExist: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if kefu.zhuangtai != 1: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 3. 查询订单并加锁 - try: - with transaction.atomic(): - order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) - - # 4. 验证订单状态为退款审核中(4) - if order.zhuangtai != 4: - return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST) - - # 获取必要字段 - jiedan_dashou_id = order.jiedan_dashou_id - dashou_fencheng = order.dashou_fencheng - fadan_pingtai = order.fadan_pingtai - - # 5. 验证打手分成金额是否存在 - if dashou_fencheng is None: - return Response({'code': 400, 'msg': '订单缺少打手分成金额'}, status=status.HTTP_400_BAD_REQUEST) - - # 6. 更新订单状态为退款失败(6) - order.zhuangtai = 6 - order.clkf = current_user.phone # 记录处理客服手机号 - if jujue_liyou: - order.tkly = jujue_liyou - order.save() - - # 7. 给打手结算(返还分成) - if jiedan_dashou_id: - try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) - dashou_profile = dashou_user.dashou_profile - # 增加打手相关统计数据 - dashou_profile.chengjiaozongliang += 1 - dashou_profile.yue += dashou_fencheng - dashou_profile.zonge += dashou_fencheng - dashou_profile.jinrishouyi += dashou_fencheng - dashou_profile.jinyueshouyi += dashou_fencheng - dashou_profile.save() - logger.info(f"拒绝退款:打手 {jiedan_dashou_id} 获得分成 {dashou_fencheng}") - except (UserMain.DoesNotExist, AttributeError): - logger.warning(f"拒绝退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过结算") - - # 8. 如果是商家发单,更新商家成交订单数量 - if fadan_pingtai == 2: - try: - # 获取商家订单扩展表 - shangjia_kuozhan = order.shangjia_kuozhan - shangjia_id = shangjia_kuozhan.shangjia_id - if shangjia_id: - shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) - shangjia_profile = shangjia_user.shop_profile - shangjia_profile.chengjiao += 1 - shangjia_profile.save() - logger.info(f"拒绝退款:商家 {shangjia_id} 成交订单+1") - - # ✅ 新方法(行为驱动,更安全) - from houtai.utils import update_shangjia_daily - update_shangjia_daily( - yonghuid=shangjia_id, - amount=Decimal(str(order.jine)), - action=2 # 2 = 结算 - ) - except (ObjectDoesNotExist, UserMain.DoesNotExist, AttributeError): - logger.warning(f"拒绝退款:商家信息不存在,跳过商家更新") - - # 9. 更新退款记录表 - try: - refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) - refund_record.sqzhuangtai = 2 # 2表示拒绝 - refund_record.chuliid = phone - refund_record.bhliyou = jujue_liyou # 记录驳回理由(假设模型有此字段) - refund_record.save() - logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}") - except Tuikuanjilu.DoesNotExist: - # 如果退款记录不存在,可以创建一条记录(可选) - Tuikuanjilu.objects.create( - dingdan_id=dingdan_id, - dashouid=jiedan_dashou_id or '', - qingqiuid='', - chuliid=phone, - liyou=jujue_liyou or '客服拒绝退款', - sqzhuangtai=2, - jine=order.jine, - dashou_fencheng=dashou_fencheng or 0, - jieshao=order.jieshao or '', - beizhu='客服拒绝退款', - nicheng=order.nicheng or '', - bhliyou=jujue_liyou or '', # 假设模型有 bhliyou 字段 - ) - except Exception as e: - logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True) - - # 🔥 新增:调用打手每日统计(成交) - try: - update_dashou_daily_by_action( - yonghuid=order.jiedan_dashou_id, - amount=dashou_fencheng, - action=2 # 2 表示成交(结算) - ) - logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - # 不影响主流程 - - # 10. 更新客服统计数据 - kefu.jinrichuli += 1 - kefu.jinyuechuli += 1 - kefu.zongchuli += 1 - kefu.save() - - return Response({'code': 0, 'msg': '拒绝退款成功'}) - - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - logger.error(f"拒绝退款接口异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - - - - - - - -class KefuMerchantRefundView(APIView): - """ - 客服商家订单退款接口(发单平台=2) - 请求:POST /yonghu/kefusjtk - 参数:{ - "phone": "客服手机号", - "dingdan_id": "订单ID", - "tuikuan_liyou": "退款理由(可选)" - } - 认证:JWT - 返回:code=0 成功 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - phone = request.data.get('phone', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() - - if not phone or not dingdan_id: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 客服身份验证(同前,省略以节省篇幅,但实际必须完整) - current_user = request.user - # ... 验证 - if getattr(current_user, 'phone', '') != phone: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if current_user.user_type != 'kefu': - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - kefu = current_user.kefu_profile - except ObjectDoesNotExist: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if kefu.zhuangtai != 1: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - - # 查询订单,预加载商家扩展表 - try: - order = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) - - # 验证商家订单 - if order.fadan_pingtai != 2: - return Response({'code': 400, 'msg': '该订单不是商家订单'}, status=status.HTTP_400_BAD_REQUEST) - - # 验证状态允许退款 - allowed_statuses = [1, 7, 2, 8, 4] - if order.zhuangtai not in allowed_statuses: - return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST) - - # 获取必要数据 - jiedan_dashou_id = order.jiedan_dashou_id - jine = order.jine - if jine is None: - return Response({'code': 400, 'msg': '订单金额缺失'}, status=status.HTTP_400_BAD_REQUEST) - - try: - shangjia_kuozhan = order.shangjia_kuozhan - shangjia_id = shangjia_kuozhan.shangjia_id - except ObjectDoesNotExist: - return Response({'code': 400, 'msg': '订单商家信息不存在'}, status=status.HTTP_400_BAD_REQUEST) - - with transaction.atomic(): - # 更新订单 - order.zhuangtai = 5 - order.clkf = current_user.phone - if tuikuan_liyou: - order.tkly = tuikuan_liyou - order.save() - - # 更新打手 - if jiedan_dashou_id: - try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.tuikuanliang += 1 - dashou_profile.zhuangtai = 1 - dashou_profile.save() - except (UserMain.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"商家退款:打手 {jiedan_dashou_id} 不存在,跳过更新") - - # 更新商家 - try: - shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) - shangjia_profile = shangjia_user.shop_profile - shangjia_profile.tuikuan += 1 - shangjia_profile.yue += jine - shangjia_profile.save() - except (UserMain.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"商家退款:商家 {shangjia_id} 不存在,跳过更新") - - # 更新退款记录表 - try: - refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) - refund_record.sqzhuangtai = 1 - refund_record.chuliid = phone - refund_record.save() - except Tuikuanjilu.DoesNotExist: - pass - - # 更新客服统计 - - # ✅ 新方法(行为驱动,更安全) - from houtai.utils import update_shangjia_daily - update_shangjia_daily( - yonghuid=shangjia_id, - amount=Decimal(str(order.jine)), - action=3 # 3 = 退款 - ) - - - # 🔥 新增:调用打手每日统计(成交) - try: - update_dashou_daily_by_action( - yonghuid=order.jiedan_dashou_id, - amount=order.dashou_fencheng, - action=3 # 3 表示退款(退款) - ) - logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - # 不影响主流程 - - - - kefu = current_user.kefu_profile - kefu.jinrichuli += 1 - kefu.jinyuechuli += 1 - kefu.zongchuli += 1 - kefu.save() - - return Response({'code': 0, 'msg': '退款成功'}) - - - - - -logger = logging.getLogger(__name__) - -class KefuRejectSettlementView(APIView): - """ - 客服拒绝结算接口(仅平台订单,状态2/8) - 请求:POST /yonghu/kefujjjs - 参数:{ - "phone": "客服手机号", - "dingdan_id": "订单ID", - "tkly": "拒绝结算理由(必填)" - } - 认证:JWT - 返回:code=0 成功,其他失败 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - phone = request.data.get('phone', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - tkly = request.data.get('tkly', '').strip() - - if not phone or not dingdan_id or not tkly: - return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 客服身份验证 - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - logger.warning(f"手机号不匹配: {phone}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if current_user.user_type != 'kefu': - logger.warning(f"用户类型非客服: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - kefu = current_user.kefu_profile - except ObjectDoesNotExist: - logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if kefu.zhuangtai != 1: - logger.warning(f"客服账号已禁用: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 3. 查询订单,预加载平台扩展表 - try: - order = Dingdan.objects.select_related('pingtai_kuozhan').get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) - - # 4. 校验订单条件:平台发单(1)且状态为2或8 - if order.fadan_pingtai != 1 or order.zhuangtai not in [2, 8]: - return Response({'code': 400, 'msg': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算'}, status=status.HTTP_400_BAD_REQUEST) - - jiedan_dashou_id = order.jiedan_dashou_id - if not jiedan_dashou_id: - return Response({'code': 400, 'msg': '订单暂无接单打手'}, status=status.HTTP_400_BAD_REQUEST) - - # 5. 使用事务更新 - with transaction.atomic(): - # 5.1 更新订单主表 - order.zhuangtai = 5 # 已退款 - order.tkly = tkly # 拒绝理由 - order.clkf = phone # 记录客服手机号 - order.save() - - # 5.2 更新平台订单扩展表:标记为拒绝结算(假设字段 jjjs_biaoshi) - # 注意:需要确保 DingdanPingtai 模型中已添加 jjjs_biaoshi 字段,IntegerField,默认0 - if hasattr(order, 'pingtai_kuozhan') and order.pingtai_kuozhan: - pingtai_ext = order.pingtai_kuozhan - pingtai_ext.jjjs_biaoshi = 1 # 1表示拒绝结算 - pingtai_ext.save() - else: - # 理论上应该存在,但若不存在则记录日志 - logger.warning(f"订单 {dingdan_id} 缺少平台扩展表,无法标记拒绝结算") - - # 5.3 更新打手扩展表 - try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.zhuangtai = 1 # 打手状态设为空闲 - dashou_profile.tuikuanliang += 1 # 退款订单总量+1 - dashou_profile.save() - logger.info(f"拒绝结算:打手 {jiedan_dashou_id} 退款订单+1,状态空闲") - except (UserMain.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"拒绝结算:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新") - except Exception as e: - logger.error(f"拒绝结算更新打手异常: {str(e)}", exc_info=True) - - # 5.4 更新客服扩展表统计数据 - - - # 🔥 新增:调用打手每日统计(成交) - try: - update_dashou_daily_by_action( - yonghuid=order.jiedan_dashou_id, - amount=order.dashou_fencheng, - action=3 # 3 表示退款(退款) - ) - logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - # 不影响主流程 - - - - kefu.jinrichuli += 1 - kefu.jinyuechuli += 1 - kefu.zongchuli += 1 - kefu.save() - - return Response({'code': 0, 'msg': '拒绝结算成功'}) - - - -class KefuTransferHallView(APIView): - """ - 客服恢复大厅(转移大厅)接口 - 请求:POST /yonghu/kefuzydt - 参数:{ - "phone": "客服手机号", - "dingdan_id": "订单ID" - } - 认证:JWT - 返回:code=0 成功 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - phone = request.data.get('phone', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - - if not phone or not dingdan_id: - return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 客服身份验证(同前,略,但必须完整实现) - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - if current_user.user_type != 'kefu': - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - try: - kefu = current_user.kefu_profile - if kefu.zhuangtai != 1: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - except ObjectDoesNotExist: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 3. 查询订单 - try: - order = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) - - # 4. 校验订单状态(允许状态:2,8,4) - if order.zhuangtai not in [2, 8, 4]: - return Response({'code': 400, 'msg': '订单状态不允许转移大厅'}, status=status.HTTP_400_BAD_REQUEST) - - - if order.jiedan_dashou_id: - UserDashou.objects.filter(user__yonghuid=order.jiedan_dashou_id).update(zhuangtai=1) - else: - pass - - # 5. 根据是否有指定ID决定新状态 - if order.zhiding_id: - new_status = 7 # 指定中 - else: - new_status = 1 # 下单中 - - # 6. 使用事务更新 - with transaction.atomic(): - # 清空接单打手ID,更新状态,记录客服 - order.jiedan_dashou_id = None - order.zhuangtai = new_status - order.clkf = phone - order.save() - - return Response({'code': 0, 'msg': '转移大厅成功'}) - - -class KefuCancelDesignationView(APIView): - """ - 客服取消指定接口(仅状态7) - 请求:POST /yonghu/kefuqxzd - 参数:{ - "phone": "客服手机号", - "dingdan_id": "订单ID" - } - 认证:JWT - 返回:code=0 成功 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - phone = request.data.get('phone', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - - if not phone or not dingdan_id: - return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 客服身份验证(同前,略,但必须完整实现) - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - if current_user.user_type != 'kefu': - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - try: - kefu = current_user.kefu_profile - if kefu.zhuangtai != 1: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - except ObjectDoesNotExist: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 3. 查询订单 - try: - order = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) - - # 4. 校验订单状态为7(指定中) - - if order.zhuangtai != 7: - print(999) - return Response({'code': 400, 'msg': '仅状态为指定中(7)的订单可取消指定'}, status=status.HTTP_400_BAD_REQUEST) - - # 5. 使用事务更新 - with transaction.atomic(): - # 状态改为1(下单中),清空指定ID,记录客服 - order.zhuangtai = 1 - order.zhiding_id = None - order.clkf = phone - order.save() - - return Response({'code': 0, 'msg': '取消指定成功'}) - - - - - - -import logging -from django.db.models import Q -from django.core.exceptions import ObjectDoesNotExist -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser - -from yonghu.models import UserMain, UserKefu, UserDashou - -logger = logging.getLogger(__name__) - -class KefuGetDashouListView(APIView): - """ - 客服获取打手列表接口(仅打手类型) - 请求:POST /yonghu/kefuhqdslb - 参数:{ - "phone": "客服手机号", - "keyword": "搜索关键词(可选,搜索用户ID或昵称)", - "page": "页码(从1开始)", - "page_size": "每页数量" - } - 认证:JWT - 返回:{ - "code": 0, - "data": { - "list": [打手对象], - "has_more": true/false - } - } - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - phone = request.data.get('zhanghao', '').strip() - keyword = request.data.get('keyword', '').strip() - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - except ValueError: - return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST) - - if not phone: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 客服身份验证 - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - logger.warning(f"手机号不匹配: {phone}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if current_user.user_type != 'kefu': - logger.warning(f"用户类型非客服: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - kefu = current_user.kefu_profile - except ObjectDoesNotExist: - logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if kefu.zhuangtai != 1: - logger.warning(f"客服账号已禁用: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 3. 构建查询条件(只查询打手) - # 通过 UserDashou 扩展表关联 UserMain - queryset = UserDashou.objects.select_related('user').all() - - if keyword: - # 搜索用户ID或打手昵称 - queryset = queryset.filter( - Q(user__yonghuid__icontains=keyword) | - Q(nicheng__icontains=keyword) - ) - - # 4. 计算总数并分页 - total = queryset.count() - offset = (page - 1) * page_size - dashou_list = queryset.order_by('-create_time')[offset:offset + page_size] - - # 5. 构建返回数据(只取需要的字段) - result = [] - for dashou in dashou_list: - user = dashou.user - result.append({ - 'yonghuid': user.yonghuid, - 'avatar': user.avatar or '', - 'zaixianzhuangtai': dashou.zaixianzhuangtai, - 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, - 'nicheng': dashou.nicheng or '', - 'zhuangtai': dashou.zhuangtai, - }) - - # 6. 判断是否有更多数据 - has_more = (offset + len(dashou_list)) < total - - return Response({ - 'code': 0, - 'data': { - 'list': result, - 'has_more': has_more - } - }) - - - - - - - -class KefuGetDashouDetailView(APIView): - """ - 客服获取打手详情接口 - 请求:POST /yonghu/kefuhqdsxq - 参数:{ - "phone": "客服手机号", - "uid": "打手ID (yonghuid)" - } - 认证:JWT - 返回:code=0 + 打手信息 + 会员列表 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - phone = request.data.get('phone', '').strip() - uid = request.data.get('uid', '').strip() - - if not phone or not uid: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 客服身份验证 - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - logger.warning(f"手机号不匹配: {phone}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if current_user.user_type != 'kefu': - logger.warning(f"用户类型非客服: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - kefu = current_user.kefu_profile - except ObjectDoesNotExist: - logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if kefu.zhuangtai != 1: - logger.warning(f"客服账号已禁用: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 3. 查询打手主表和扩展表 - try: - # 使用 select_related 预加载 dashou_profile,减少数据库查询 - user_main = UserMain.objects.select_related('dashou_profile').get( - yonghuid=uid - ) - except UserMain.DoesNotExist: - return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND) - - # 验证用户类型是否为打手(可选,但建议验证) - # 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册) - try: - dashou = user_main.dashou_profile - except ObjectDoesNotExist: - # 用户存在但未注册打手身份,返回基本信息 - user_info = { - 'yonghuid': user_main.yonghuid, - 'avatar': user_main.avatar or '', - 'zaixianzhuangtai': 0, - 'zhanghaozhuangtai': 0, - 'zhuangtai': 0, - 'nicheng': '', - 'chenghao': '', - 'dianhua': '', - 'wechat': '', - 'phone': user_main.phone or '', - 'create_time': user_main.create_time, - 'ip': user_main.ip or '', - 'jiedanzongliang': 0, - 'chengjiaozongliang': 0, - 'tuikuanliang': 0, - 'jinrijiedan': 0, - 'yue': 0.00, - 'zonge': 0.00, - 'jifen': 0, - 'yajin': 0.00, - 'jinrishouyi': 0.00, - 'jinyueshouyi': 0.00, - 'yaoqingren': '', - 'jieshao': '', - } - member_list = [] - return Response({ - 'code': 0, - 'data': { - 'user_info': user_info, - 'member_list': member_list - } - }) - - # 4. 构建打手信息 - user_info = { - 'yonghuid': user_main.yonghuid, - 'avatar': user_main.avatar or '', - 'zaixianzhuangtai': dashou.zaixianzhuangtai, - 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, - 'zhuangtai': dashou.zhuangtai, - 'nicheng': dashou.nicheng or '', - 'chenghao': dashou.chenghao or '', - 'dianhua': dashou.dianhua or '', - 'wechat': dashou.wechat or '', - 'phone': user_main.phone or '', - 'create_time': user_main.create_time, - 'ip': user_main.ip or '', - 'jiedanzongliang': dashou.jiedanzongliang, - 'chengjiaozongliang': dashou.chengjiaozongliang, - 'tuikuanliang': dashou.tuikuanliang, - 'jinrijiedan': dashou.jinrijiedan, - 'yue': float(dashou.yue) if dashou.yue else 0.00, - 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, - 'jifen': dashou.jifen, - 'yajin': float(dashou.yajin) if dashou.yajin else 0.00, - 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00, - 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00, - 'yaoqingren': dashou.yaoqingren or '', - 'jieshao': dashou.jieshao or '', - } - - # 5. 查询打手会员信息(仅未过期) - member_records = Huiyuangoumai.objects.filter( - yonghu_id=uid - ).order_by('-create_time') - - member_list = [] - for record in member_records: - # 调用模型方法检查是否过期 - if hasattr(record, 'jiance_shifou_daoqi'): - is_expired = record.jiance_shifou_daoqi() - else: - # 如果方法不存在,简单判断 - is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True - - if not is_expired: - # 获取会员详情 - try: - huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id) - huiyuan_name = huiyuan.jieshao - except Huiyuan.DoesNotExist: - huiyuan_name = '未知会员' - - member_list.append({ - 'id': record.id, - 'huiyuan_name': huiyuan_name, - 'daoqi_time': record.daoqi_time, - 'is_active': record.huiyuan_zhuangtai == 1, - }) - - return Response({ - 'code': 0, - 'data': { - 'user_info': user_info, - 'member_list': member_list - } - }) - - - - -from yonghu.models import Xiugaijilu # 确保已导入 - - - - -from houtai.utils import verify_kefu_permission - -logger = logging.getLogger(__name__) - -class KefuUpdateDashouView(APIView): - permission_classes = [IsAuthenticated] - """ - 客服修改打手信息接口 - 支持操作: - - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb) - - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb) - - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd) - - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee) - - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee) - - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee) - - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg) - """ - def post(self, request): - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - dashou_id = request.data.get('dashou_id', '').strip() - caozuo = request.data.get('caozuo', '').strip() - value = request.data.get('value') # 金额、积分、天数等 - huiyuan_id = request.data.get('huiyuan_id', '').strip() # 会员ID(加/减会员时使用) - - if not username or not dashou_id or not caozuo: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 权限校验(统一方法) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 查询打手是否存在 - try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id, user_type='dashou') - dashou_profile = dashou_user.dashou_profile - except UserMain.DoesNotExist: - return Response({'code': 404, 'msg': '打手不存在'}) - except UserDashou.DoesNotExist: - return Response({'code': 404, 'msg': '打手扩展信息缺失'}) - - # 4. 根据操作类型执行(事务内) - with transaction.atomic(): - try: - # 记录修改前的值(用于日志) - before = { - 'yue': dashou_profile.yue, - 'yajin': dashou_profile.yajin, - 'jifen': dashou_profile.jifen, - 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai, - 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, - 'zhuangtai': dashou_profile.zhuangtai, - } - - # ---------- 余额操作 ---------- - if caozuo == 'jian_yue': - if '001aa' not in permissions: - return Response({'code': 403, 'msg': '无权限减打手余额'}) - amount = Decimal(str(value)) - if dashou_profile.yue < amount: - return Response({'code': 400, 'msg': '余额不足'}) - dashou_profile.yue -= amount - dashou_profile.save() - after_yue = dashou_profile.yue - # 记录日志 - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, # 2=打手 - xiugaitijiao=after_yue, - xiugaitijiaoq=before['yue'], - ) - return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after_yue)}}) - - elif caozuo == 'jia_yue': - if '001bb' not in permissions: - return Response({'code': 403, 'msg': '无权限加打手余额'}) - amount = Decimal(str(value)) - dashou_profile.yue += amount - dashou_profile.save() - after_yue = dashou_profile.yue - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - xiugaitijiao=after_yue, - xiugaitijiaoq=before['yue'], - ) - return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after_yue)}}) - - # ---------- 押金操作 ---------- - elif caozuo == 'jian_yajin': - if '001aa' not in permissions: - return Response({'code': 403, 'msg': '无权限减打手押金'}) - amount = Decimal(str(value)) - if dashou_profile.yajin < amount: - return Response({'code': 400, 'msg': '押金不足'}) - dashou_profile.yajin -= amount - dashou_profile.save() - after_yajin = dashou_profile.yajin - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - yajin=after_yajin, - yyajin=before['yajin'], - ) - return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after_yajin)}}) - - elif caozuo == 'jia_yajin': - if '001bb' not in permissions: - return Response({'code': 403, 'msg': '无权限加打手押金'}) - amount = Decimal(str(value)) - dashou_profile.yajin += amount - dashou_profile.save() - after_yajin = dashou_profile.yajin - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - yajin=after_yajin, - yyajin=before['yajin'], - ) - return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after_yajin)}}) - - # ---------- 积分操作 ---------- - elif caozuo == 'jian_jifen': - if '001cc' not in permissions: - return Response({'code': 403, 'msg': '无权限减打手积分'}) - jifen = int(value) - if dashou_profile.jifen < jifen: - return Response({'code': 400, 'msg': '积分不足'}) - dashou_profile.jifen -= jifen - dashou_profile.save() - after_jifen = dashou_profile.jifen - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - jifen=after_jifen, - yjifen=before['jifen'], - ) - return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after_jifen}}) - - elif caozuo == 'jia_jifen': - if '001dd' not in permissions: - return Response({'code': 403, 'msg': '无权限加打手积分'}) - jifen = int(value) - dashou_profile.jifen += jifen - dashou_profile.save() - after_jifen = dashou_profile.jifen - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - jifen=after_jifen, - yjifen=before['jifen'], - ) - return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after_jifen}}) - - # ---------- 状态修改 ---------- - elif caozuo == 'gai_zhanghaozhuangtai': - if '001ee' not in permissions: - return Response({'code': 403, 'msg': '无权限修改打手账号状态'}) - new_status = int(value) - if new_status not in [0, 1]: - return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'}) - dashou_profile.zhanghaozhuangtai = new_status - dashou_profile.save() - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - qitashuoming=f'修改账号状态为{new_status}', - ) - return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}}) - - elif caozuo == 'gai_zaixianzhuangtai': - if '001ee' not in permissions: - return Response({'code': 403, 'msg': '无权限修改打手在线状态'}) - new_status = int(value) - if new_status not in [0, 1]: - return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'}) - dashou_profile.zaixianzhuangtai = new_status - dashou_profile.save() - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - qitashuoming=f'修改在线状态为{new_status}', - ) - return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}}) - - elif caozuo == 'gai_zhuangtai': - if '001ee' not in permissions: - return Response({'code': 403, 'msg': '无权限修改打手接单状态'}) - new_status = int(value) - if new_status not in [0, 1]: - return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'}) - dashou_profile.zhuangtai = new_status - dashou_profile.save() - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - qitashuoming=f'修改接单状态为{new_status}', - ) - return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}}) - - # ---------- 会员操作 ---------- - elif caozuo == 'jia_huiyuan': - if '001ff' not in permissions: - return Response({'code': 403, 'msg': '无权限给打手加会员'}) - if not huiyuan_id: - return Response({'code': 400, 'msg': '缺少会员ID'}) - days = int(value) if value else 30 # 默认加30天 - try: - huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id) - except Huiyuan.DoesNotExist: - return Response({'code': 404, 'msg': '会员类型不存在'}) - # 获取或创建购买记录 - record, created = Huiyuangoumai.objects.select_for_update().get_or_create( - yonghu_id=dashou_id, - huiyuan_id=huiyuan_id, - defaults={ - 'huiyuan_zhuangtai': 1, - 'jieshao': huiyuan.jieshao, - 'daoqi_time': timezone.now() + timedelta(days=days), - } - ) - if not created: - # 延长有效期 - new_daoqi = record.daoqi_time + timedelta(days=days) - record.daoqi_time = new_daoqi - record.huiyuan_zhuangtai = 1 - record.save() - # 记录修改日志 - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - huiyuants=days, - huiyuan_id=huiyuan_id, - qitashuoming=f'加会员 {huiyuan_id},增加{days}天', - ) - return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}}) - - elif caozuo == 'jian_huiyuan': - if '001gg' not in permissions: - return Response({'code': 403, 'msg': '无权限给打手减会员'}) - if not huiyuan_id: - return Response({'code': 400, 'msg': '缺少会员ID'}) - try: - record = Huiyuangoumai.objects.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id) - except Huiyuangoumai.DoesNotExist: - return Response({'code': 404, 'msg': '该打手未购买此会员'}) - # 删除会员记录(或设置为过期,根据业务要求) - record.delete() - Xiugaijilu.objects.create( - yonghuid=dashou_id, - xiugaiid=kefu.user.phone, - leixing=2, - huiyuan_id=huiyuan_id, - qitashuoming=f'移除会员 {huiyuan_id}', - ) - return Response({'code': 0, 'msg': '减会员成功'}) - - else: - return Response({'code': 400, 'msg': '未知操作类型'}) - - except Exception as e: - logger.error(f"修改打手信息异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - - - - -class KefuPunishmentListView(APIView): - """ - 客服获取处罚记录列表接口 - 请求:POST /yonghu/kefu_cfgl - 参数:{ - "phone": "客服手机号", - "status": [0,3] 或 [1,2], # 状态数组 - "dingdan_id": "订单ID(可选)", - "dashouid": "打手ID(可选)", - "page": 1, - "page_size": 20 - } - 认证:JWT - 返回:{ - "code": 0, - "data": { - "list": [处罚记录], - "total": 总数, - "stats": { - "zongshu": 总记录数, - "daichuli": 待处理数, - "yichuli": 已处理数 - } - } - } - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - phone = request.data.get('phone', '').strip() - status_list = request.data.get('status') - dingdan_id = request.data.get('dingdan_id', '').strip() - dashouid = request.data.get('dashouid', '').strip() - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - except ValueError: - return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST) - - if not phone: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 客服身份验证 - current_user = request.user - # 1. 获取前端参数 - username = request.data.get('phone', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 2. 公共权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 3. 检查平台订单管理权限(002ab) - if '005aa' not in permissions: - return Response({'code': 403, 'msg': '您无权查看平台订单列表'}) - - # 3. 获取统计信息(不分页) - stats = { - 'zongshu': Chufajilu.objects.count(), - 'daichuli': Chufajilu.objects.filter(sqzhuangtai__in=[0, 3]).count(), - 'yichuli': Chufajilu.objects.filter(sqzhuangtai__in=[1, 2]).count(), - } - - # 4. 构建查询条件 - queryset = Chufajilu.objects.all() - - if status_list and isinstance(status_list, list): - queryset = queryset.filter(sqzhuangtai__in=status_list) - - if dingdan_id: - queryset = queryset.filter(dingdan_id=dingdan_id) - - if dashouid: - queryset = queryset.filter(dashouid=dashouid) - - # 5. 分页 - total = queryset.count() - offset = (page - 1) * page_size - records = queryset.order_by('-create_time')[offset:offset + page_size] - - # 6. 构建返回列表 - list_data = [] - for item in records: - list_data.append({ - 'dingdan_id': item.dingdan_id, - 'dashouid': item.dashouid, - 'qingqiuid': item.qingqiuid, - 'cfliyou': item.cfliyou, - 'sqzhuangtai': item.sqzhuangtai, - 'create_time': item.create_time, - 'update_time': item.update_time, - # 其他字段按需添加 - }) - - return Response({ - 'code': 0, - 'data': { - 'list': list_data, - 'total': total, - 'stats': stats - } - }) - - - - - - -import sys -import time -import traceback -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser - -from yonghu.models import UserMain, UserKefu -from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia, Dashoutupian, Tuikuanjilu - - -class KefuGetOrderDetailView(APIView): - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 强制打印到控制台并立即刷新 - def p(msg): - #print(f"[订单详情] {msg}") - sys.stdout.flush() - - #p("========== 视图开始执行 ==========") - - # 1. 打印原始请求数据 - #p(f"request.data: {request.data}") - - phone = request.data.get('phone', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - #p(f"phone={phone}, dingdan_id={dingdan_id}") - - if not phone or not dingdan_id: - #p("参数不完整,返回401") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 获取当前用户 - current_user = request.user - - - # 验证手机号 - if getattr(current_user, 'phone', '') != phone: - #p("手机号不匹配") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 验证用户类型 - if current_user.user_type != 'kefu': - #p("用户类型不是客服") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 客服扩展表 - try: - kefu = current_user.kefu_profile - #p(f"kefu_profile: {kefu}") - if kefu.zhuangtai != 1: - #p(f"客服状态异常: {kefu.zhuangtai}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - except Exception as e: - #p(f"获取客服扩展表异常: {e}") - return Response({'code': 500, 'msg': '服务器错误'}, status=500) - - # 3. 查询订单 - try: - #p(f"开始查询订单: {dingdan_id}") - dingdan_obj = Dingdan.objects.select_related( - 'pingtai_kuozhan', 'shangjia_kuozhan' - ).get(dingdan_id=dingdan_id) - #p("订单查询成功") - except Dingdan.DoesNotExist: - #p("订单不存在") - return Response({'code': 404, 'msg': '订单不存在'}, status=404) - except Exception as e: - #p(f"订单查询异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '服务器错误'}, status=500) - - # 4. 构建基础数据 - try: - response_data = { - 'dingdan_id': dingdan_obj.dingdan_id or '', - 'zhuangtai': dingdan_obj.zhuangtai or 0, - 'fadanpingtai': dingdan_obj.fadan_pingtai or 0, - 'jiage': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00, - 'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00, - 'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '', - 'dashou_liuyan': dingdan_obj.dashou_liuyan or '', - 'zhiding_id': dingdan_obj.zhiding_id or '', - 'shangpin_id': dingdan_obj.shangpin_id or '', - 'leixing': dingdan_obj.leixing_id or 0, - 'tupian_url': dingdan_obj.tupian or '', - 'jieshao': dingdan_obj.jieshao or '', - 'beizhu': dingdan_obj.beizhu or '', - 'tkly': dingdan_obj.tkly or '', - 'nicheng': dingdan_obj.nicheng or '', - 'clkf': dingdan_obj.clkf or '', - 'chuangjianshijian': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.create_time else '', - 'genggaishijian': dingdan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.update_time else '', - } - #p("基础数据构建完成") - except Exception as e: - #p(f"构建基础数据异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '服务器错误'}, status=500) - - # 5. 扩展表数据 - try: - fadan_pingtai = dingdan_obj.fadan_pingtai - if fadan_pingtai == 1: - if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan: - pingtai = dingdan_obj.pingtai_kuozhan - response_data['pingtai_kuozhan'] = { - 'laoban_id': pingtai.laoban_id or '', - 'laoban_pingjia': pingtai.laoban_pingjia or '', - } - else: - response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''} - elif fadan_pingtai == 2: - if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan: - shangjia = dingdan_obj.shangjia_kuozhan - response_data['shangjia_kuozhan'] = { - 'shangjia_id': shangjia.shangjia_id or '', - 'sjnicheng': shangjia.sjnicheng or '', - 'shangjia_pingjia': shangjia.shangjia_pingjia or '', - 'cfliyou': shangjia.cfliyou or '', - 'sqzhuangtai': shangjia.sqzhuangtai if shangjia.sqzhuangtai is not None else 0, - 'bhliyou': shangjia.bhliyou or '', - } - else: - response_data['shangjia_kuozhan'] = { - 'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '', - 'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': '' - } - #p("扩展数据构建成功") - except Exception as e: - #p(f"扩展数据异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '服务器错误'}, status=500) - - # 6. 打手图片 - try: - dashou_images = Dashoutupian.objects.filter(dingdan_id=dingdan_id).values_list('tupian', flat=True) - response_data['dashou_images'] = [img for img in dashou_images if img] - #p(f"打手图片数量: {len(response_data['dashou_images'])}") - except Exception as e: - #p(f"打手图片异常: {traceback.format_exc()}") - response_data['dashou_images'] = [] - - # 7. 退款理由 - try: - tuikuan_record = Tuikuanjilu.objects.filter(dingdan_id=dingdan_id).order_by('-create_time').first() - response_data['tuikuan_liyou'] = tuikuan_record.liyou if tuikuan_record and tuikuan_record.liyou else '' - #p("退款理由查询完成") - except Exception as e: - #p(f"退款理由异常: {traceback.format_exc()}") - response_data['tuikuan_liyou'] = '' - - #p("视图正常结束,返回成功") - return Response({'code': 0, 'data': response_data}) - - - - - - - - - - - - - - - - -class KefuPunishmentDetailView(APIView): - """ - 客服获取处罚详情接口 - 请求:POST /yonghu/kefu_cf_detail - 参数:{ - "phone": "客服手机号", - "dingdan_id": "订单ID" - } - 认证:JWT - 返回:code=0 + 处罚记录详情(含图片) - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - phone = request.data.get('phone', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - - if not phone or not dingdan_id: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 客服身份验证 - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - logger.warning(f"手机号不匹配: {phone}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if current_user.user_type != 'kefu': - logger.warning(f"用户类型非客服: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - kefu = current_user.kefu_profile - except ObjectDoesNotExist: - logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if kefu.zhuangtai != 1: - logger.warning(f"客服账号已禁用: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 查询处罚记录(按创建时间倒序取最新一条) - try: - record = Chufajilu.objects.filter(dingdan_id=dingdan_id).order_by('-create_time').first() - if not record: - return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - logger.error(f"查询处罚记录失败: {e}") - return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 查询商家证据图片 - zhengju_images = [] - shensu_images = [] - try: - # 商家证据图片:上传者为请求人 (qingqiuid) - zhengju_qs = Chufatupian.objects.filter(dingdan_id=dingdan_id, yonghuid=record.qingqiuid) - zhengju_images = [item.tupian for item in zhengju_qs if item.tupian] - - # 打手申诉图片:上传者为打手 (dashouid) - shensu_qs = Chufatupian.objects.filter(dingdan_id=dingdan_id, yonghuid=record.dashouid) - shensu_images = [item.tupian for item in shensu_qs if item.tupian] - except Exception as e: - logger.error(f"查询图片失败: {e}") - - # 构建返回数据 - data = { - 'dingdan_id': record.dingdan_id, - 'dashouid': record.dashouid, - 'qingqiuid': record.qingqiuid, - 'chuliid': record.chuliid, - 'cfliyou': record.cfliyou, - 'sqzhuangtai': record.sqzhuangtai, - 'bhliyou': record.bhliyou, - 'jifen': record.jifen, - 'ssliyou': record.ssliyou or '', - 'zhengju_tupian': zhengju_images, - 'shensu_tupian': shensu_images, - 'create_time': record.create_time, - 'update_time': record.update_time, - } - - return Response({'code': 0, 'data': data}) - - - -from django.db import transaction -from decimal import Decimal - -class KefuPunishmentActionView(APIView): - """ - 客服同意/驳回处罚接口 - 请求:POST /yonghu/kefu_cf_action - 参数:{ - "phone": "客服手机号", - "dingdan_id": "订单ID", - "action": 1(同意) / 2(驳回), - "reject_reason": "驳回理由(驳回时必填)" - } - 认证:JWT - 返回:code=0 成功 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - phone = request.data.get('phone', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - action = request.data.get('action') - reject_reason = request.data.get('reject_reason', '').strip() - - if not phone or not dingdan_id or action not in [1, 2]: - return Response({'code': 401, 'msg': '参数错误'}, status=status.HTTP_400_BAD_REQUEST) - - if action == 2 and not reject_reason: - return Response({'code': 400, 'msg': '驳回理由不能为空'}, status=status.HTTP_400_BAD_REQUEST) - - # 客服身份验证(完整验证,此处略) - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - if current_user.user_type != 'kefu': - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - try: - kefu = current_user.kefu_profile - if kefu.zhuangtai != 1: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - except ObjectDoesNotExist: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 查询待处理的处罚记录(状态0或3) - try: - record = Chufajilu.objects.filter( - dingdan_id=dingdan_id, - sqzhuangtai__in=[0, 3] - ).order_by('-create_time').first() - if not record: - exists = Chufajilu.objects.filter(dingdan_id=dingdan_id).exists() - if exists: - return Response({'code': 400, 'msg': '该处罚记录已处理'}, status=status.HTTP_400_BAD_REQUEST) - else: - return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - logger.error(f"查询处罚记录失败: {e}") - return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - with transaction.atomic(): - # 处理重复记录:同一订单的其他待处理/申诉中记录标记为驳回 - duplicate_records = Chufajilu.objects.filter( - dingdan_id=dingdan_id, - sqzhuangtai__in=[0, 3] - ).exclude(id=record.id) - for dup in duplicate_records: - dup.sqzhuangtai = 2 - dup.chuliid = phone - dup.bhliyou = "系统自动驳回:存在重复处罚记录" - dup.save() - logger.info(f"自动驳回重复处罚记录 ID={dup.id}") - - if action == 1: # 同意 - # 可选验证打手存在 - if record.dashouid: - try: - dashou = UserDashou.objects.get(user__yonghuid=record.dashouid) - except UserDashou.DoesNotExist: - return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST) - - record.sqzhuangtai = 1 - record.chuliid = phone - record.save() - - # 更新商家订单扩展表 - try: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) - if hasattr(dingdan, 'shangjia_kuozhan'): - dingdan.shangjia_kuozhan.sqzhuangtai = 1 - dingdan.shangjia_kuozhan.save() - except Exception as e: - logger.warning(f"更新商家扩展表失败: {e}") - - elif action == 2: # 驳回 - # 返还积分 - if record.jifen > 0 and record.dashouid: - try: - dashou = UserDashou.objects.get(user__yonghuid=record.dashouid) - if dashou.jifen <= 1000: # 安全限制 - dashou.jifen += record.jifen - dashou.save() - logger.info(f"驳回处罚,打手{record.dashouid}返还积分{record.jifen}") - else: - logger.warning(f"打手积分异常({dashou.jifen}),不再返还") - except UserDashou.DoesNotExist: - logger.warning(f"打手{record.dashouid}不存在,无法返还积分") - - record.sqzhuangtai = 2 - record.chuliid = phone - record.bhliyou = reject_reason - record.save() - - try: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) - if hasattr(dingdan, 'shangjia_kuozhan'): - dingdan.shangjia_kuozhan.sqzhuangtai = 2 - dingdan.shangjia_kuozhan.bhliyou = reject_reason - dingdan.shangjia_kuozhan.save() - except Exception as e: - logger.warning(f"更新商家扩展表失败: {e}") - - return Response({'code': 0, 'msg': '处理成功'}) - - - - - - - - - - - -from dingdan.utils import update_daily_dispatch_stat -from dingdan.models import PreSettlement - -logger = logging.getLogger(__name__) -from shangpin.utils import update_shangpin_daily_stat -from shangdian.utils import update_dianpu_daily_stat # 注意:您之前写的店铺统计方法名是 update_dianpu_daily_stat - -logger = logging.getLogger(__name__) -from shangpin.utils import update_shangpin_daily_stat -from shangdian.utils import update_dianpu_daily_stat # 注意:您之前写的店铺统计方法名是 update_dianpu_daily_stat - - - -class KefuForceCompleteView(APIView): - """ - 客服强制结单接口(仅本地订单) - 请求:POST /yonghu/kefuqiangzhijiedan - 参数:{"phone": "客服手机号", "dingdan_id": "订单ID"} - 认证:JWT,仅限状态正常的客服 - 逻辑: - 1. 验证客服身份及状态 - 2. 查询订单并加锁(事务内) - 3. 校验订单状态是否为8(结算中) - 4. 校验订单必须有接单打手且打手分成金额有效 - 5. 给打手增加余额、成交统计、今日/本月收益 - 6. 如果是商家发单,更新商家成交订单数量 - 7. 更新订单状态为3(已完成) - 8. 更新客服处理统计 - 9. 返回成功 - """ - permission_classes = [permissions.IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - phone = request.data.get('phone', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - - if not phone or not dingdan_id: - return Response({'code': 401, 'msg': '参数缺失'}) - - # 1. 客服身份验证 - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - return Response({'code': 401, 'msg': '认证失败'}) - - if current_user.user_type != 'kefu': - return Response({'code': 401, 'msg': '认证失败'}) - - try: - kefu = current_user.kefu_profile - except ObjectDoesNotExist: - return Response({'code': 401, 'msg': '认证失败'}) - - if kefu.zhuangtai != 1: - return Response({'code': 401, 'msg': '客服状态异常'}) - - # 2. 订单处理(事务内) - try: - with transaction.atomic(): - order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) - - # 3. 校验订单状态 - if order.zhuangtai != 8: - return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'}) - - # 4. 校验必要字段 - dashou_id = order.jiedan_dashou_id - dashou_fencheng = order.dashou_fencheng - if not dashou_id: - return Response({'code': 400, 'msg': '订单无接单打手'}) - if not dashou_fencheng or dashou_fencheng <= 0: - return Response({'code': 400, 'msg': '打手分成金额无效'}) - - # 5. 给打手结算 - try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - # 原子更新(避免并发重复加) - dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1 - dashou_profile.yue = F('yue') + dashou_fencheng - dashou_profile.zonge = F('zonge') + dashou_fencheng - dashou_profile.jinrishouyi = F('jinrishouyi') + dashou_fencheng - dashou_profile.jinyueshouyi = F('jinyueshouyi') + dashou_fencheng - dashou_profile.save(update_fields=[ - 'chengjiaozongliang', 'yue', 'zonge', - 'jinrishouyi', 'jinyueshouyi' - ]) - except (UserMain.DoesNotExist, AttributeError): - return Response({'code': 400, 'msg': '打手不存在或扩展表缺失'}) - - # 6. 如果是商家发单,更新商家成交订单数量 - if order.fadan_pingtai == 2: - try: - # 通过 DingdanShangjia 获取商家ID - shangjia_ext = order.shangjia_kuozhan # 假设 related_name='shangjia_kuozhan' - if shangjia_ext and shangjia_ext.shangjia_id: - shangjia_user = UserMain.objects.get(yonghuid=shangjia_ext.shangjia_id) - shangjia_profile = shangjia_user.shop_profile - shangjia_profile.chengjiao = F('chengjiao') + 1 - shangjia_profile.save(update_fields=['chengjiao']) - - # ✅ 新方法(行为驱动,更安全) - from houtai.utils import update_shangjia_daily - update_shangjia_daily( - yonghuid=shangjia_ext.shangjia_id, - amount=Decimal(str(order.jine)), - action=2 # 2 = 结算 - ) - - except ObjectDoesNotExist: - # 没有商家扩展记录则忽略 - pass - - # 7. 更新订单状态 - order.zhuangtai = 3 - order.clkf = phone - order.save(update_fields=['zhuangtai', 'clkf']) - - - if order.fadan_pingtai == 1: - - # ========== 新增:商品和店铺每日统计 ========== - try: - # 商品每日统计(只要有商品ID就执行) - if order.shangpin_id: - # 从扩展表获取已计算好的收益值(下单时已存储) - pingtai_shouyi = Decimal('0.00') - dianpu_shouyi = Decimal('0.00') - if hasattr(order, 'pingtai_kuozhan'): - pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') - dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - - update_shangpin_daily_stat( - shangpin_id=order.shangpin_id, - caozuo_leixing=2, # 2 = 结单 - dingdan_jiage=order.jine, - pingtai_shouyi=pingtai_shouyi, - dianpu_zongshouyi=dianpu_shouyi - ) - logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") - - # 店铺每日统计(仅当存在店铺ID时执行) - dianpu_id = None - if hasattr(order, 'pingtai_kuozhan'): - dianpu_id = order.pingtai_kuozhan.dianpu_id - if dianpu_id: - dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - update_dianpu_daily_stat( - dianpu_id=dianpu_id, - caozuo_leixing=2, # 2 = 结单 - dingdan_jiage=order.jine, - dianpu_shouyi=dianpu_shouyi - ) - logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") - except Exception as e: - logger.error(f"商品/店铺统计更新失败: {e}") - logger.error(traceback.format_exc()) - # 统计失败不影响主流程 - # ============================================= - - # 🔥 新增:调用打手每日统计(成交) - try: - update_dashou_daily_by_action( - yonghuid=order.jiedan_dashou_id, - amount=dashou_fencheng, - action=2 # 2 表示成交(结算) - ) - logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - # 不影响主流程 - - # 商家订单管事分红 - if order.fadan_pingtai == 2: - from dingdan.utils import settle_shangjia_order_guanshi_fenhong - settle_shangjia_order_guanshi_fenhong(order, dashou_id) - - # 8. 更新客服统计 - kefu.jinrichuli = F('jinrichuli') + 1 - kefu.jinyuechuli = F('jinyuechuli') + 1 - kefu.zongchuli = F('zongchuli') + 1 - kefu.save(update_fields=['jinrichuli', 'jinyuechuli', 'zongchuli']) - - return Response({'code': 0, 'msg': '强制结单成功'}) - - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}) - except Exception as e: - logger.error(f"强制结单异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - - - - - - - - - -from django.db.models import Q, Sum - - -# yonghu/views.py -import logging -import sys -from django.db import DatabaseError -from django.db.models import Q, Sum -from yonghu.models import Tixianjilu, TixianShenheJilu - -kefu_txsh_list_logger = logging.getLogger('yonghu.kefu_txsh_list') -if not kefu_txsh_list_logger.handlers: - _console_handler = logging.StreamHandler(sys.stderr) - _console_handler.setFormatter(logging.Formatter( - '[%(asctime)s] %(levelname)s %(name)s: %(message)s' - )) - kefu_txsh_list_logger.addHandler(_console_handler) - kefu_txsh_list_logger.setLevel(logging.DEBUG) - kefu_txsh_list_logger.propagate = False - -_TIXIAN_LIST_BASE_FIELDS = ( - 'id', 'yonghuid', 'avatar', 'phone', 'nicheng', 'leixing', - 'zhifu', 'skzhanghao', 'jine', 'zhuangtai', 'fangshi', - 'shenheid', 'bhliyou', 'create_time', 'update_time', -) -_TIXIAN_LIST_AUDIT_FIELDS = ('shenhe_jilu_id', 'shenhe_danhao') - - -class KefuWithdrawListView(APIView): - """ - 客服获取提现审核列表接口 - 请求:POST /yonghu/kefu_txsh_list - 参数:{ - "phone": "客服手机号", - "page": 1, - "page_size": 20, - "status": 1(待审核)/2(已处理), - "type": 1(打手佣金)/2(管事分红)/3(组长分红)/4(审核官分佣)/5(打手押金)/6(商家余额), 可选 - "payment": 1(微信)/2(支付宝) 可选, - "search_uid": "用户ID搜索" 可选, - "date": "YYYY-MM-DD" 可选, - "min_amount": 0, # 最小提现金额(元)可选 - "max_amount": 999999, # 最大提现金额(元)可选 - "dakuan_mode": 1 # 打款方式:1手动打款 2自动打款,可选 - } - 认证:JWT - 返回:{ - "code": 0, - "data": { - "list": [提现记录], - "total": 总数, - "stats": { - "total": 总记录数, - "awaiting": 待审核数, - "processed": 已处理数, - "total_amount": "总金额(元)" - } - } - } - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def _safe_request_params(self, request): - try: - return dict(request.data) - except Exception: - return {'raw': str(request.data)} - - def _shenhe_jilu_field_available(self): - """探测 shenhe_jilu_id 字段是否已在数据库中就绪""" - try: - Tixianjilu.objects.values_list('shenhe_jilu_id', flat=True).first() - return True - except DatabaseError as e: - kefu_txsh_list_logger.error( - 'kefu_txsh_list: shenhe_jilu_id 字段不可用,将按旧表结构查询: %s', e, - exc_info=True, - ) - return False - - def _list_queryset(self, q, shenhe_field_ok): - """字段未迁移时 only 旧字段,避免 SELECT 不存在的列导致 500""" - fields = _TIXIAN_LIST_BASE_FIELDS + ( - _TIXIAN_LIST_AUDIT_FIELDS if shenhe_field_ok else () - ) - return Tixianjilu.objects.filter(q).only(*fields) - - def _resolve_row_dakuan_mode(self, item, audit_mode_map, shenhe_field_ok): - """无关联审核或审核表未标记自动 → 默认手动(1)""" - if not shenhe_field_ok: - return 1 - sid = getattr(item, 'shenhe_jilu_id', None) - if not sid: - return 1 - if audit_mode_map.get(sid) == 2: - return 2 - return 1 - - def post(self, request): - dakuan_mode_filter = request.data.get('dakuan_mode') - req_params = self._safe_request_params(request) - kefu_txsh_list_logger.info('kefu_txsh_list 请求开始 params=%s', req_params) - - try: - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST) - - if page < 1: - page = 1 - if page_size < 1 or page_size > 100: - page_size = min(max(page_size, 1), 100) - - status_filter = request.data.get('status') - type_filter = request.data.get('type') - payment_filter = request.data.get('payment') - search_uid = (request.data.get('search_uid') or '').strip() - date_filter = (request.data.get('date') or '').strip() - min_amount = request.data.get('min_amount') - max_amount = request.data.get('max_amount') - - phone = (request.data.get('phone') or '').strip() - if not phone: - return Response({'code': 401, 'msg': '缺少客服手机号'}, status=status.HTTP_401_UNAUTHORIZED) - - kefu, permissions = verify_kefu_permission(request, phone) - if kefu is None: - return permissions - if '005bb' not in permissions: - return Response({'code': 403, 'msg': '您无权查看平台订单列表'}) - - q = Q() - - try: - status_val = int(status_filter) if status_filter is not None and str(status_filter).strip() != '' else None - except (TypeError, ValueError): - status_val = None - - try: - dakuan_mode_val = int(dakuan_mode_filter) if dakuan_mode_filter is not None and str(dakuan_mode_filter).strip() != '' else None - except (TypeError, ValueError): - dakuan_mode_val = None - - # 未传打款方式参数 → 默认手动打款(1) - effective_dakuan = dakuan_mode_val if dakuan_mode_val in (1, 2) else 1 - - if status_val == 1: - q &= Q(zhuangtai=1) - elif status_val == 2: - if effective_dakuan == 2: - q &= Q(zhuangtai__in=[2, 3, 4, 5, 6]) - else: - q &= Q(zhuangtai__in=[2, 3]) - - if type_filter is not None and str(type_filter).strip() != '': - try: - type_val = int(type_filter) - if type_val in [1, 2, 3, 4, 5, 6]: - q &= Q(leixing=type_val) - except (TypeError, ValueError): - pass - - if payment_filter is not None and str(payment_filter).strip() != '': - try: - payment_val = int(payment_filter) - if payment_val in [1, 2]: - q &= Q(fangshi=payment_val) - except (TypeError, ValueError): - pass - - if search_uid: - q &= Q(yonghuid=search_uid) - - if date_filter: - q &= Q(update_time__date=date_filter) - - if min_amount is not None and str(min_amount).strip() != '': - try: - q &= Q(jine__gte=float(min_amount)) - except (TypeError, ValueError): - pass - if max_amount is not None and str(max_amount).strip() != '': - try: - q &= Q(jine__lte=float(max_amount)) - except (TypeError, ValueError): - pass - - shenhe_field_ok = self._shenhe_jilu_field_available() - if shenhe_field_ok: - if effective_dakuan == 2: - q &= Q(shenhe_jilu_id__isnull=False) - else: - q &= Q(shenhe_jilu_id__isnull=True) - elif effective_dakuan == 2: - kefu_txsh_list_logger.warning('kefu_txsh_list: 自动打款筛选不可用,返回空列表') - return Response({ - 'code': 0, - 'data': { - 'list': [], - 'total': 0, - 'stats': {'total': 0, 'awaiting': 0, 'processed': 0, 'total_amount': '0.00'}, - }, - }) - - processed_statuses = [2, 3, 4, 5, 6] if effective_dakuan == 2 else [2, 3] - - base_qs = self._list_queryset(q, shenhe_field_ok) - total_count = base_qs.count() - awaiting_count = self._list_queryset(q & Q(zhuangtai=1), shenhe_field_ok).count() - processed_count = self._list_queryset( - q & Q(zhuangtai__in=processed_statuses), shenhe_field_ok, - ).count() - agg = base_qs.aggregate(total=Sum('jine')) - total_amount = agg.get('total') or 0 - - stats = { - 'total': total_count, - 'awaiting': awaiting_count, - 'processed': processed_count, - 'total_amount': str(round(float(total_amount), 2)), - } - - queryset = base_qs.order_by('create_time') - total = queryset.count() - records = list(queryset[(page - 1) * page_size: page * page_size]) - - audit_ids = [ - getattr(item, 'shenhe_jilu_id', None) - for item in records if getattr(item, 'shenhe_jilu_id', None) - ] - audit_meta_map = {} - if audit_ids: - try: - from .tixian_shenhe_services import load_audit_meta_map - audit_meta_map = load_audit_meta_map(audit_ids) - except Exception as e: - kefu_txsh_list_logger.error( - 'kefu_txsh_list: 查询审核表信息失败: %s', e, - exc_info=True, - ) - - audit_mode_map = { - aid: meta.get('dakuan_mode', 2) for aid, meta in audit_meta_map.items() - } - list_data = [] - for item in records: - shenhe_jilu_id = getattr(item, 'shenhe_jilu_id', None) if shenhe_field_ok else None - shenhe_danhao = (getattr(item, 'shenhe_danhao', None) or '') if shenhe_field_ok else '' - if shenhe_jilu_id and not shenhe_danhao: - shenhe_danhao = audit_meta_map.get(shenhe_jilu_id, {}).get('shenhe_danhao', '') - list_data.append({ - 'id': item.id, - 'shenhe_jilu_id': shenhe_jilu_id, - 'shenhe_danhao': shenhe_danhao, - 'yonghuid': item.yonghuid, - 'avatar': item.avatar or '', - 'phone': item.phone or '', - 'nicheng': item.nicheng or '', - 'leixing': item.leixing, - 'zhifu': item.zhifu or '', - 'skzhanghao': item.skzhanghao or '', - 'jine': str(item.jine) if item.jine is not None else '0.00', - 'zhuangtai': item.zhuangtai, - 'fangshi': item.fangshi, - 'dakuan_mode': self._resolve_row_dakuan_mode(item, audit_mode_map, shenhe_field_ok), - 'shenheid': item.shenheid or '', - 'bhliyou': item.bhliyou or '', - 'create_time': item.create_time.strftime('%Y-%m-%d %H:%M:%S') if item.create_time else '', - 'update_time': item.update_time.strftime('%Y-%m-%d %H:%M:%S') if item.update_time else '', - }) - - return Response({ - 'code': 0, - 'data': { - 'list': list_data, - 'total': total, - 'stats': stats, - }, - }) - - except DatabaseError as e: - kefu_txsh_list_logger.error( - 'kefu_txsh_list 数据库错误 dakuan_mode=%s params=%s err=%s', - dakuan_mode_filter, req_params, e, - exc_info=True, - ) - return Response({ - 'code': 500, - 'msg': '数据库查询失败,请确认已执行 python manage.py migrate yonghu', - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - except Exception as e: - kefu_txsh_list_logger.error( - 'kefu_txsh_list 接口异常 dakuan_mode=%s params=%s err=%s', - dakuan_mode_filter, req_params, e, - exc_info=True, - ) - return Response({ - 'code': 500, - 'msg': '获取提现列表失败', - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - -import logging -from decimal import Decimal -from django.core.exceptions import ObjectDoesNotExist -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser - -from yonghu.models import UserMain, UserKefu, Tixianjilu, UserDashou, UserGuanshi - -logger = logging.getLogger(__name__) - -class KefuWithdrawDetailView(APIView): - """ - 客服获取提现详情接口 - 请求:POST /yonghu/kefu_txsh_detail - 参数:{ - "phone": "客服手机号", - "tixian_id": "提现记录ID" - } - 认证:JWT - 返回:code=0 + 提现记录详情 + 用户扩展信息 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - phone = request.data.get('phone', '').strip() - tixian_id = request.data.get('tixian_id') - - if not phone or not tixian_id: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 客服身份验证 - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - logger.warning(f"手机号不匹配: {phone}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if current_user.user_type != 'kefu': - logger.warning(f"用户类型非客服: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - kefu = current_user.kefu_profile - except ObjectDoesNotExist: - logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if kefu.zhuangtai != 1: - logger.warning(f"客服账号已禁用: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 查询提现记录 - try: - record = Tixianjilu.objects.get(id=tixian_id) - except Tixianjilu.DoesNotExist: - return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND) - - # 查询用户主表 - try: - user = UserMain.objects.get(yonghuid=record.yonghuid) - except UserMain.DoesNotExist: - user = None - - # 构建基础返回数据(字段必须与前端一致) - data = { - 'id': record.id, - 'yonghuid': record.yonghuid, - 'avatar': record.avatar or '', - 'phone': record.phone or '', - 'nicheng': record.nicheng or '', - 'leixing': record.leixing, # 1打手 2管事 - 'zhifu': record.zhifu or '', - 'skzhanghao': record.skzhanghao or '', - 'jine': str(record.jine) if record.jine else '0.00', - 'zhuangtai': record.zhuangtai, - 'fangshi': record.fangshi, - 'shenheid': record.shenheid or '', - 'bhliyou': record.bhliyou or '', - 'create_time': record.create_time, - 'update_time': record.update_time, - } - - # 根据提现类型添加扩展信息 - if user: - if record.leixing == 1: # 打手 - try: - dashou = UserDashou.objects.get(user=user) - # 计算成交率和退款率 - total = dashou.jiedanzongliang or 0 - completed = dashou.chengjiaozongliang or 0 - refund = dashou.tuikuanliang or 0 - completion_rate = round((completed / total * 100) if total > 0 else 0) - refund_rate = round((refund / completed * 100) if completed > 0 else 0) - - data['dashouInfo'] = { - 'zhuangtai': dashou.zhuangtai, # 工作状态 - 'zaixianzhuangtai': dashou.zaixianzhuangtai, # 在线状态 - 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, # 账号状态 - 'yue': float(dashou.yue) if dashou.yue else 0.00, # 可提现余额 - 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, # 接单总额 - 'jifen': dashou.jifen, # 积分 - 'jiedanzongliang': dashou.jiedanzongliang, # 接单总量 - 'chengjiaozongliang': dashou.chengjiaozongliang, # 成交总量 - 'tuikuanliang': dashou.tuikuanliang, # 退款总量 - 'completionRate': completion_rate, - 'refundRate': refund_rate, - } - except ObjectDoesNotExist: - data['dashouInfo'] = None - elif record.leixing == 2: # 管事 - try: - guanshi = UserGuanshi.objects.get(user=user) - data['guanshiInfo'] = { - 'zhuangtai': guanshi.zhuangtai, # 账号状态 - 'yue': float(guanshi.yue) if guanshi.yue else 0.00, - 'yaogingshuliang': guanshi.yaogingshuliang, # 邀请打手总数 - 'jinrichongzhi': guanshi.jinrichongzhi, # 今日充值 - 'jinyuechongzhi': guanshi.jinyuechongzhi, # 今月充值 - 'chongzhifenrun': float(guanshi.chongzhifenrun) if guanshi.chongzhifenrun else 0.00, - } - except ObjectDoesNotExist: - data['guanshiInfo'] = None - - return Response({'code': 0, 'data': data}) - -import logging -import json -from decimal import Decimal -from django.db import transaction -from django.utils import timezone -from django.core.exceptions import ObjectDoesNotExist -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser - -from yonghu.models import UserMain, UserKefu, Tixianjilu, UserDashou, UserGuanshi - -logger = logging.getLogger(__name__) - - -class KefuWithdrawActionView(APIView): - """ - 客服处理提现接口(同意/拒绝)—— 仅此接口处理后台审核打款 - POST /yonghu/kefu_txsh_action - - 单条:{ phone, action, tixian_id, yonghuid, leixing, reason? } - 批量:{ phone, action, batch_list: [{tixian_id, yonghuid, leixing}, ...], reason? } - - 自动打款(dakuan_mode=2):同意→6待收款;拒绝→5+驳回原因+退还可到账金额(shijidaozhang),手续费不退,不更新平台收支/每日统计 - 手动打款(dakuan_mode=1):原逻辑不变 - 状态要求:Tixianjilu.zhuangtai=1 且 TixianShenheJilu.zhuangtai=1(审核中),其他状态一律拒绝 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def _verify_kefu(self, request, phone): - if not phone: - return None, Response({'code': 401, 'msg': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST) - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - if current_user.user_type != 'kefu': - return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - try: - kefu = current_user.kefu_profile - except ObjectDoesNotExist: - return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - if kefu.zhuangtai != 1: - return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - return current_user, None - - def _parse_items(self, request): - """解析单条或 batch_list,返回 [{tixian_id, yonghuid, leixing}]""" - batch_list = request.data.get('batch_list') - if batch_list is not None: - if not isinstance(batch_list, list) or len(batch_list) == 0: - return None, Response({'code': 400, 'msg': 'batch_list 不能为空'}, status=status.HTTP_400_BAD_REQUEST) - items = [] - for idx, row in enumerate(batch_list): - if not isinstance(row, dict): - return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 格式错误'}, status=status.HTTP_400_BAD_REQUEST) - tid = row.get('tixian_id') - yid = (row.get('yonghuid') or '').strip() - lx = row.get('leixing') - if not tid or not yid or lx is None: - return None, Response( - {'code': 400, 'msg': f'batch_list[{idx}] 缺少 tixian_id / yonghuid / leixing'}, - status=status.HTTP_400_BAD_REQUEST, - ) - try: - items.append({ - 'tixian_id': int(tid), - 'yonghuid': yid, - 'leixing': int(lx), - }) - except (TypeError, ValueError): - return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 参数格式错误'}, status=status.HTTP_400_BAD_REQUEST) - return items, None - - tixian_id = request.data.get('tixian_id') - if not tixian_id: - return None, Response({'code': 400, 'msg': '提现记录ID不能为空'}, status=status.HTTP_400_BAD_REQUEST) - yonghuid = (request.data.get('yonghuid') or '').strip() - leixing = request.data.get('leixing') - try: - item = {'tixian_id': int(tixian_id), 'yonghuid': yonghuid, 'leixing': int(leixing) if leixing is not None else None} - except (TypeError, ValueError): - return None, Response({'code': 400, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST) - return [item], None - - def _process_auto_item(self, item, action, reason, kefu_user): - """自动打款单条处理(须在 transaction.atomic 内调用)""" - from .models import TixianShenheJilu - from .tixian_shenhe_services import refund_balance, sync_audit_and_jilu_status - - tixian_id = item['tixian_id'] - req_yonghuid = item['yonghuid'] - req_leixing = item['leixing'] - - if req_leixing not in [1, 2, 3, 4, 5, 6]: - raise ValueError(f'提现记录{tixian_id}:提现类型无效') - - try: - tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id) - except Tixianjilu.DoesNotExist: - raise ValueError(f'提现记录{tixian_id}不存在') - - if tixian.yonghuid != req_yonghuid: - raise ValueError(f'提现记录{tixian_id}:用户ID不匹配') - if tixian.leixing != req_leixing: - raise ValueError(f'提现记录{tixian_id}:提现类型不匹配') - if tixian.zhuangtai != 1: - raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理') - if not tixian.shenhe_jilu_id: - raise ValueError(f'提现记录{tixian_id}:非自动打款审核记录') - - try: - audit = TixianShenheJilu.objects.select_for_update().get(id=tixian.shenhe_jilu_id) - except TixianShenheJilu.DoesNotExist: - raise ValueError(f'提现记录{tixian_id}:审核记录不存在') - - if audit.yonghuid != req_yonghuid: - raise ValueError(f'提现记录{tixian_id}:审核记录用户ID不匹配') - if audit.leixing != req_leixing: - raise ValueError(f'提现记录{tixian_id}:审核记录提现类型不匹配') - if audit.zhuangtai != 1: - raise ValueError(f'提现记录{tixian_id}:审核记录非审核中状态') - - try: - user = UserMain.objects.select_for_update().get(yonghuid=req_yonghuid) - except UserMain.DoesNotExist: - raise ValueError(f'用户{req_yonghuid}不存在') - - if action == 1: - sync_audit_and_jilu_status(audit, 6, shenhe_ren_id=kefu_user.yonghuid) - tixian.shenheid = kefu_user.yonghuid - tixian.save(update_fields=['shenheid', 'update_time']) - else: - sync_audit_and_jilu_status( - audit, 5, - bhliyou=reason, - bo_hui_ren_id=kefu_user.yonghuid, - ) - # 退还可到账金额,申请时扣的 shenqing_jine 中的手续费(shouxufei)不退 - refund_balance(user, audit.leixing, audit.shijidaozhang) - tixian.shenheid = kefu_user.yonghuid - tixian.bhliyou = reason - tixian.save(update_fields=['shenheid', 'bhliyou', 'update_time']) - - def _process_manual_item(self, item, action, reason, phone): - """手动打款单条处理(原逻辑,须在 transaction.atomic 内调用)""" - tixian_id = item['tixian_id'] - try: - tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id) - except Tixianjilu.DoesNotExist: - raise ValueError(f'提现记录{tixian_id}不存在') - - req_yonghuid = item.get('yonghuid') - req_leixing = item.get('leixing') - if req_yonghuid and tixian.yonghuid != req_yonghuid: - raise ValueError(f'提现记录{tixian_id}:用户ID不匹配') - if req_leixing is not None and tixian.leixing != req_leixing: - raise ValueError(f'提现记录{tixian_id}:提现类型不匹配') - if tixian.zhuangtai != 1: - raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理') - - if getattr(tixian, 'shenhe_jilu_id', None): - raise ValueError(f'提现记录{tixian_id}:自动打款请走自动流程') - - try: - user = UserMain.objects.get(yonghuid=tixian.yonghuid) - except UserMain.DoesNotExist: - raise ValueError(f'用户{tixian.yonghuid}不存在') - - if action == 1: - tixian.zhuangtai = 2 - try: - from dingdan.utils import update_daily_payout - update_daily_payout(tixian.jine) - from peizhi.models import Szjilu - szjilu, _ = Szjilu.objects.get_or_create(id=1) - szjilu.zongsy -= tixian.jine - szjilu.zongzc += tixian.jine - szjilu.jrzc += tixian.jine - szjilu.update_time = timezone.now() - szjilu.save() - except Exception as e: - logger.error(f'更新收支记录失败: {e}') - else: - from .tixian_shenhe_services import refund_balance - try: - refund_balance(user, tixian.leixing, tixian.jine) - except ValueError as e: - raise ValueError(str(e)) - tixian.zhuangtai = 3 - tixian.bhliyou = reason - - tixian.shenheid = phone - tixian.update_time = timezone.now() - tixian.save() - - def post(self, request): - logger.info(f'接收到提现处理请求,数据: {request.data}') - - phone = request.data.get('phone', '').strip() - action_raw = request.data.get('action') - reason = request.data.get('reason', '').strip() - - try: - action = int(action_raw) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': 'action 参数必须为数字'}, status=status.HTTP_400_BAD_REQUEST) - if action not in [1, 2]: - return Response({'code': 400, 'msg': 'action 必须为1(同意)或2(拒绝)'}, status=status.HTTP_400_BAD_REQUEST) - if action == 2 and not reason: - return Response({'code': 400, 'msg': '拒绝提现时理由不能为空'}, status=status.HTTP_400_BAD_REQUEST) - - kefu_user, err_resp = self._verify_kefu(request, phone) - if err_resp: - return err_resp - - items, err_resp = self._parse_items(request) - if err_resp: - return err_resp - - is_batch = isinstance(request.data.get('batch_list'), list) and len(request.data.get('batch_list')) > 0 - - try: - with transaction.atomic(): - for item in items: - tixian_id = item['tixian_id'] - try: - tixian_probe = Tixianjilu.objects.select_for_update().get(id=tixian_id) - except Tixianjilu.DoesNotExist: - raise ValueError(f'提现记录{tixian_id}不存在') - - # 有关联审核记录(shenhe_jilu_id)即为 zddksh 自动打款,不依赖 dakuan_mode 字段 - is_auto = bool(getattr(tixian_probe, 'shenhe_jilu_id', None)) - - if is_auto: - self._process_auto_item(item, action, reason, kefu_user) - else: - if is_batch: - raise ValueError(f'提现记录{tixian_id}:批量操作仅支持自动打款记录') - self._process_manual_item(item, action, reason, phone) - - except ValueError as e: - return Response({'code': 400, 'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - logger.error(f'处理提现事务失败: {e}', exc_info=True) - return Response({'code': 500, 'msg': f'处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - msg = '批量处理成功' if is_batch and len(items) > 1 else '处理成功' - return Response({'code': 0, 'msg': msg, 'data': {'count': len(items)}}) - - - - - - - - -# dingdan/views.py -import logging -import uuid -from django.db import transaction -from django.core.exceptions import ObjectDoesNotExist -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser - -from yonghu.models import UserMain, UserKefu, UserDashou -from dingdan.models import Dingdan, Chufajilu - -logger = logging.getLogger(__name__) - - - - - -class KefuPunishView(APIView): - """ - 客服处罚打手接口 - 请求:POST /yonghu/kefuchufa - 参数:{ - "phone": "客服手机号", - "dingdan_id": "订单ID", - "reason": "处罚原因(可选)" - } - 认证:JWT - 返回:code=0 成功 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - phone = request.data.get('phone', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - reason = request.data.get('reason', '').strip() - - if not phone or not dingdan_id: - return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) - - # 2. 客服身份验证 - current_user = request.user - if getattr(current_user, 'phone', '') != phone: - logger.warning(f"手机号不匹配: {phone}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if current_user.user_type != 'kefu': - logger.warning(f"用户类型非客服: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - kefu = current_user.kefu_profile - except ObjectDoesNotExist: - logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if kefu.zhuangtai != 1: - logger.warning(f"客服账号已禁用: {current_user.yonghuid}") - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 3. 查询订单 - try: - order = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) - - # 4. 获取打手ID - dashou_id = order.jiedan_dashou_id - if not dashou_id: - return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}, status=status.HTTP_400_BAD_REQUEST) - - # 5. 查询打手 - try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou = dashou_user.dashou_profile - except UserMain.DoesNotExist: - return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND) - except ObjectDoesNotExist: - return Response({'code': 400, 'msg': '该用户不是打手'}, status=status.HTTP_400_BAD_REQUEST) - - # 6. 使用事务 - with transaction.atomic(): - # 创建处罚记录(积分扣除5分) - jifen = 5 - chufa = Chufajilu.objects.create( - dashouid=dashou_id, - qingqiuid=phone, # 客服手机号作为请求人 - cfliyou=reason or '', - sqzhuangtai=0, # 待处理 - jifen=jifen, - dingdan_id=dingdan_id, - ) - - # 扣除打手积分 - if dashou.jifen >= jifen: - dashou.jifen -= jifen - else: - dashou.jifen = 0 - dashou.save() - - # 可选:更新订单的 clkf 字段记录处理客服 - order.clkf = phone - order.save() - - return Response({'code': 0, 'msg': '处罚成功'}) - - -from django.db import transaction -from django.contrib.auth.hashers import make_password - -class AdKftjView(APIView): - """ - 添加客服接口 - 权限:管理员JWT - 请求:POST /yonghu/adkftj - 参数:{ - "zhanghao": "管理员账号", - "phone": "客服手机号(必填)", - "password": "密码(必填)", - "erjimima": "二级密码(必填)", - "nicheng": "昵称(可选)" - } - 返回:code=0 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 参数 - zhanghao = request.data.get('zhanghao', '').strip() - phone = request.data.get('phone', '').strip() - password = request.data.get('password', '').strip() - erjimima = request.data.get('erjimima', '').strip() - nicheng = request.data.get('nicheng', '').strip() - - if not all([zhanghao, phone, password, erjimima]): - return Response({'code': 400, 'message': '手机号、密码、二级密码不能为空'}) - - # 2. 管理员验证(同前) - current_user = request.user - if current_user.phone != zhanghao or current_user.user_type != 'admin': - return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) - try: - admin_profile = current_user.admin_profile - except: - return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 3. 检查手机号是否已被使用 - if UserMain.objects.filter(phone=phone).exists(): - return Response({'code': 400, 'message': '该手机号已被注册'}) - - # 4. 生成唯一 yonghuid - def generate_yonghuid(): - import random, time - for _ in range(10): - timestamp = str(int(time.time()))[-5:] - rand = str(random.randint(0, 99)).zfill(2) - uid = timestamp + rand - if len(uid) == 7 and not UserMain.objects.filter(yonghuid=uid).exists(): - return uid - raise Exception('无法生成唯一用户ID') - - with transaction.atomic(): - # 创建主表用户 - user = UserMain.objects.create( - yonghuid=generate_yonghuid(), - phone=phone, - password=password, - user_type='kefu', - openid=f'kefu_{phone}' # 临时openid - ) - # 创建客服扩展表 - kefu = UserKefu.objects.create( - user=user, - nicheng=nicheng or f'客服{user.yonghuid}', - erjimima=erjimima, # 建议实际项目中加密存储 - zhuangtai=1, - jinrichuli=0, - jinyuechuli=0, - zongchuli=0 - ) - - return Response({'code': 0, 'message': '添加成功'}) - - - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.parsers import JSONParser -from django.db.models import Q -import logging - -from yonghu.models import UserMain, UserKefu, AdminProfile - -logger = logging.getLogger(__name__) - -class AdKfglView(APIView): - """ - 客服管理列表接口 - 权限:管理员JWT - 请求:POST /yonghu/adkfgl - 参数:{ - "zhanghao": "管理员账号", - "keyword": "搜索关键词(可选,匹配客服ID或昵称)" - } - 返回:{ - "code": 0, - "data": { - "list": [客服对象...] - } - } - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 参数获取 - zhanghao = request.data.get('zhanghao', '').strip() - keyword = request.data.get('keyword', '').strip() - - if not zhanghao: - return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) - - # 2. 管理员身份验证 - current_user = request.user - if not hasattr(current_user, 'phone') or str(current_user.phone) != zhanghao: - return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if current_user.user_type != 'admin': - return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - admin_profile = current_user.admin_profile - except AttributeError: - return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 3. 查询所有客服(通过扩展表关联主表) - queryset = UserKefu.objects.select_related('user').all() - - if keyword: - queryset = queryset.filter( - #Q(user__yonghuid__icontains=keyword) | - Q(nicheng__icontains=keyword) | - Q(user__phone__icontains=keyword) - ) - - # 4. 构建返回数据 - kefu_list = [] - for kefu in queryset: - user = kefu.user - kefu_list.append({ - 'yonghuid': user.phone, - 'phone': user.phone or '', - 'nicheng': kefu.nicheng or '', - 'zhuangtai': kefu.zhuangtai, - 'jinrichuli': kefu.jinrichuli, - 'jinyuechuli': kefu.jinyuechuli, - 'zongchuli': kefu.zongchuli, - }) - - return Response({ - 'code': 0, - 'data': {'list': kefu_list} - }) - - - - - -class AdKfxgView(APIView): - """ - 修改客服信息接口 - 权限:管理员JWT - 请求:POST /yonghu/adkfxg - 参数:{ - "zhanghao": "管理员账号", - "uid": "客服ID (yonghuid)", - "nicheng": "新昵称(可选)", - "phone": "新手机号(可选)", - "password": "新密码(可选)", - "erjimima": "新二级密码(可选)", - "zhuangtai": 0/1(可选) - } - 返回:code=0 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - zhanghao = request.data.get('zhanghao', '').strip() - uid = request.data.get('uid', '').strip() - - if not zhanghao or not uid: - return Response({'code': 400, 'message': '参数不完整'}) - - # 管理员验证 - current_user = request.user - if current_user.phone != zhanghao or current_user.user_type != 'admin': - return Response({'code': 401, 'message': '身份验证失败'}) - try: - admin_profile = current_user.admin_profile - except: - return Response({'code': 401, 'message': '身份验证失败'}) - - # 查询客服 - try: - user = UserMain.objects.get(phone=uid) - kefu = user.kefu_profile - except (UserMain.DoesNotExist, UserKefu.DoesNotExist): - return Response({'code': 404, 'message': '客服不存在'}) - - # 更新数据 - with transaction.atomic(): - # 更新主表字段 - if 'phone' in request.data: - new_phone = request.data.get('phone').strip() - if new_phone and UserMain.objects.filter(phone=new_phone).exclude(phone=uid).exists(): - return Response({'code': 400, 'message': '手机号已被使用'}) - user.phone = new_phone - if 'password' in request.data: - user.password = request.data['password'] - user.save() - - # 更新扩展表 - if 'nicheng' in request.data: - kefu.nicheng = request.data['nicheng'] - if 'erjimima' in request.data: - kefu.erjimima = request.data['erjimima'] - if 'zhuangtai' in request.data: - kefu.zhuangtai = int(request.data['zhuangtai']) - kefu.save() - - return Response({'code': 0, 'message': '修改成功'}) - - - - - -from .models import ( - UserMain, UserBoss, UserDashou, UserGuanshi, - UserZuzhang -) - - - -class WechatLoginAndGuanshiRegisterView(APIView): - """ - 未登录用户微信登录+管事注册一体化接口 - 路径: /api/yonghu/zuzhangyqmzc - 方法: POST - 权限: 允许所有 - 请求体: { "code": "微信登录code", "inviteCode": "组长邀请码" } - 成功返回 code: 200,数据同 GuanshiRegisterView - """ - permission_classes = [AllowAny] - - def post(self, request): - code = request.data.get('code', '').strip() - yaoqingma = request.data.get('inviteCode', '').strip() - - if not code: - return Response({'code': 400, 'msg': '微信授权码不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - if not yaoqingma: - return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - if len(yaoqingma) > 100: - return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - wechat_data = self._get_wechat_openid(code) - if not wechat_data or 'openid' not in wechat_data: - error_msg = wechat_data.get('errmsg', '微信授权失败') - return Response({'code': 400, 'msg': f'微信登录失败: {error_msg}', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - openid = wechat_data['openid'] - kehuduan_ip = self._huoqu_kehuduan_ip(request) - - with transaction.atomic(): - user_main, created = UserMain.objects.select_for_update().get_or_create( - openid=openid, - defaults={ - 'yonghuid': self._shengcheng_yonghu_id(), - 'user_type': 'normal', - } - ) - user_main.ip = kehuduan_ip - user_main.last_login_time = timezone.now() - user_main.save() - - if created: - UserBoss.objects.create(user=user_main, nickname='微信用户') - - if hasattr(user_main, 'guanshi_profile'): - return self._fanhui_xianyou_guanshi_info(user_main) - - try: - zuzhang = UserZuzhang.objects.select_related('user').get(yaoqingma=yaoqingma) - except UserZuzhang.DoesNotExist: - return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND) - - if zuzhang.zhuangtai != 1: - return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None}, - status=status.HTTP_403_FORBIDDEN) - - zuzhang_yonghuid = zuzhang.user.yonghuid - - guanshi_yaoqingma = chuangjianYaoqingma(str(user_main.yonghuid)) - guanshi_profile = UserGuanshi.objects.create( - user=user_main, - yaoqingma=guanshi_yaoqingma, - yaoqingren=zuzhang_yonghuid, - ) - - UserZuzhang.objects.filter(pk=zuzhang.pk).update( - yaoqing_zongshu=models.F('yaoqing_zongshu') + 1 - ) - - # ========== 新增:组长每日统计(邀请管事 action=1) ========== - try: - from houtai.utils import update_zuzhang_daily_by_action - update_zuzhang_daily_by_action( - yonghuid=zuzhang_yonghuid, - action=1 # 1 = 邀请管事 - ) - logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}") - except Exception as e: - logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}") - - - if not hasattr(user_main, 'dashou_profile'): - dashou_profile = UserDashou.objects.create( - user=user_main, - nicheng='大神', - chenghao='普通大神', - yaoqingren=zuzhang_yonghuid, - ) - else: - dashou_profile = user_main.dashou_profile - - refresh = RefreshToken.for_user(user_main) - token = str(refresh.access_token) - - response_data = self._zhuangbei_fanhui_shuju( - user_main, dashou_profile, guanshi_profile, token - ) - - return Response({'code': 0, 'msg': '登录并注册成功!', 'data': response_data}) - - # ---------- 辅助方法(与接口1完全相同,完整列出以保证完整性) ---------- - - def _get_wechat_openid(self, code): - try: - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') - if not appid or not secret: - raise ValueError('微信配置未设置') - url = 'https://api.weixin.qq.com/sns/jscode2session' - params = { - 'appid': appid, - 'secret': secret, - 'js_code': code, - 'grant_type': 'authorization_code' - } - response = requests.get(url, params=params, timeout=10) - result = response.json() - if 'openid' in result: - return result - else: - errcode = result.get('errcode', 'unknown') - errmsg = result.get('errmsg', '未知错误') - return {'errmsg': f'[{errcode}]{errmsg}'} - except requests.exceptions.Timeout: - logger.error('请求微信接口超时') - return {'errmsg': '请求微信接口超时'} - except Exception as e: - logger.error(f'请求微信接口异常: {e}') - return {'errmsg': f'请求微信接口异常: {str(e)}'} - - def _shengcheng_yonghu_id(self): - for _ in range(10): - timestamp_part = str(int(time.time()))[-5:].zfill(5) - random_part = str(random.randint(0, 99)).zfill(2) - user_id = timestamp_part + random_part - if len(user_id) == 7 and user_id.isdigit(): - if not UserMain.objects.filter(yonghuid=user_id).exists(): - return user_id - raise Exception('生成用户ID失败') - - def _huoqu_kehuduan_ip(self, request): - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0].strip() - if ip: - return ip - ip = request.META.get('REMOTE_ADDR', '') - return ip if ip else '0.0.0.0' - - def _huoqu_qun_peizhi(self): - try: - qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) - result = { - 'dashouqun': '', - 'dashouqunid': '', - 'guanshiqun': '', - 'guanshiqunid': '' - } - for config in qun_configs: - if config.id == 1: - result['dashouqun'] = config.neirong or '' - result['dashouqunid'] = config.qunid or '' - elif config.id == 2: - result['guanshiqun'] = config.neirong or '' - result['guanshiqunid'] = config.qunid or '' - return result - except Exception as e: - logger.error(f"获取群配置失败: {e}") - return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''} - - def _zhuangbei_dashou_fanhui(self, dashou_profile): - data = { - 'dashounicheng': dashou_profile.nicheng, - 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, - 'yongjin': str(dashou_profile.yue), - 'zonge': str(dashou_profile.zonge), - 'yajin': str(dashou_profile.yajin), - 'chenghao': dashou_profile.chenghao, - 'jinfen': dashou_profile.jifen, - 'chengjiaoliang': dashou_profile.chengjiaozongliang, - 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, - 'dashouzhuangtai': dashou_profile.zhuangtai, - } - huiyuan_list = [] - try: - records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid) - for record in records: - shifou_daoqi = record.jiance_shifou_daoqi() - huiyuan_list.append({ - 'huiyuan_id': record.huiyuan_id, - 'huiyuan_zhuangtai': record.huiyuan_zhuangtai, - 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None, - 'shifou_daoqi': shifou_daoqi, - 'jieshao': record.jieshao - }) - except Exception as e: - logger.warning(f"获取会员列表失败: {e}") - data['clumber'] = huiyuan_list - return data - - def _fanhui_xianyou_guanshi_info(self, user_main): - guanshi_profile = user_main.guanshi_profile - dashou_profile = getattr(user_main, 'dashou_profile', None) - group_info = self._huoqu_qun_peizhi() - dashou_status = 1 if dashou_profile else 0 - shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0 - guanshi_status = 1 - try: - nicheng = user_main.boss_profile.nickname or '微信用户' - except: - nicheng = '微信用户' - refresh = RefreshToken.for_user(user_main) - token = str(refresh.access_token) - data = { - 'token': token, - 'nicheng': nicheng, - 'uid': user_main.yonghuid, - 'touxiang': user_main.avatar or '', - 'shangjiastatus': shangjia_status, - 'dashoustatus': dashou_status, - 'guanshistatus': guanshi_status, - **group_info, - } - guanshi_data = { - 'gszhstatus': guanshi_profile.zhuangtai, - 'yaoqingzongshu': guanshi_profile.yaogingshuliang, - 'fenyongzonge': str(guanshi_profile.chongzhifenrun), - 'fenyongtixian': str(guanshi_profile.yue), - 'yichongzhiDashou': guanshi_profile.jinrichongzhi, - } - data.update(guanshi_data) - if dashou_profile: - dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile) - data.update(dashou_data) - else: - data.update({ - 'dashounicheng': '', - 'zhanghaostatus': 0, - 'yongjin': '0.00', - 'zonge': '0.00', - 'yajin': '0.00', - 'chenghao': '', - 'jinfen': 0, - 'chengjiaoliang': 0, - 'zaixianzhuangtai': 0, - 'dashouzhuangtai': 0, - 'clumber': [] - }) - return Response({'code': 0, 'msg': '您已是管事', 'data': data}) - - def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token): - try: - nicheng = user_main.boss_profile.nickname or '微信用户' - except: - nicheng = '微信用户' - dashou_status = 1 - shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0 - guanshi_status = 1 - group_info = self._huoqu_qun_peizhi() - data = { - 'token': token, - 'nicheng': nicheng, - 'uid': user_main.yonghuid, - 'touxiang': user_main.avatar or '', - 'shangjiastatus': shangjia_status, - 'dashoustatus': dashou_status, - 'guanshistatus': guanshi_status, - **group_info, - } - guanshi_data = { - 'gszhstatus': guanshi_profile.zhuangtai, - 'yaoqingzongshu': guanshi_profile.yaogingshuliang, - 'fenyongzonge': str(guanshi_profile.chongzhifenrun), - 'fenyongtixian': str(guanshi_profile.yue), - 'yichongzhiDashou': guanshi_profile.jinrichongzhi, - } - data.update(guanshi_data) - dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile) - data.update(dashou_data) - return data - - - - - - - -from django.db import transaction, models - - - - - - - - -class GuanshiRegisterView(APIView): - """ - 已登录用户注册管事接口 - 路径: /api/yonghu/guanshizhuce - 方法: POST - 权限: JWT认证 - 请求体: { "inviteCode": "组长邀请码" } - 成功返回 code: 200 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 获取并验证邀请码 - yaoqingma = request.data.get('inviteCode', '').strip() - if not yaoqingma: - return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - if len(yaoqingma) > 100: - return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - current_user = request.user - - # 2. 检查用户是否已是管事 - if hasattr(current_user, 'guanshi_profile'): - # 用户已是管事,直接返回现有信息 - return self._fanhui_xianyou_guanshi_info(current_user) - - # 3. 验证邀请码对应的组长 - try: - zuzhang = UserZuzhang.objects.select_related('user').get(yaoqingma=yaoqingma) - except UserZuzhang.DoesNotExist: - return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND) - - if zuzhang.zhuangtai != 1: - return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None}, - status=status.HTTP_403_FORBIDDEN) - - zuzhang_yonghuid = zuzhang.user.yonghuid - - # 4. 事务内创建管事实体和打手(如果需要) - with transaction.atomic(): - # 创建管事扩展表 - guanshi_yaoqingma = chuangjianYaoqingma(str(current_user.yonghuid)) - guanshi_profile = UserGuanshi.objects.create( - user=current_user, - yaoqingma=guanshi_yaoqingma, - yaoqingren=zuzhang_yonghuid, - ) - - # 更新组长的邀请总数 - UserZuzhang.objects.filter(pk=zuzhang.pk).update( - yaoqing_zongshu=models.F('yaoqing_zongshu') + 1 - ) - - # ========== 新增:组长每日统计(邀请管事 action=1) ========== - try: - from houtai.utils import update_zuzhang_daily_by_action - update_zuzhang_daily_by_action( - yonghuid=zuzhang_yonghuid, - action=1 # 1 = 邀请管事 - ) - logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}") - except Exception as e: - logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}") - - # 检查用户是否已是打手,如果不是则创建 - if not hasattr(current_user, 'dashou_profile'): - dashou_profile = UserDashou.objects.create( - user=current_user, - nicheng='大手子', - chenghao='普通大手', - yaoqingren=zuzhang_yonghuid, - ) - else: - dashou_profile = current_user.dashou_profile - - # 5. 生成新token - refresh = RefreshToken.for_user(current_user) - token = str(refresh.access_token) - - # 6. 准备返回数据(包含所有字段) - response_data = self._zhuangbei_fanhui_shuju( - current_user, dashou_profile, guanshi_profile, token - ) - - return Response({'code': 200, 'msg': '注册成功!', 'data': response_data}) - - # ---------- 辅助方法 ---------- - - def _huoqu_qun_peizhi(self): - """获取群配置信息""" - try: - qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) - result = { - 'dashouqun': '', - 'dashouqunid': '', - 'guanshiqun': '', - 'guanshiqunid': '' - } - for config in qun_configs: - if config.id == 1: - result['dashouqun'] = config.neirong or '' - result['dashouqunid'] = config.qunid or '' - elif config.id == 2: - result['guanshiqun'] = config.neirong or '' - result['guanshiqunid'] = config.qunid or '' - return result - except Exception as e: - logger.error(f"获取群配置失败: {e}") - return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''} - - def _zhuangbei_dashou_fanhui(self, dashou_profile): - """组装打手信息(字段名与前端完全匹配)""" - data = { - 'dashounicheng': dashou_profile.nicheng, - 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, - 'yongjin': str(dashou_profile.yue), - 'zonge': str(dashou_profile.zonge), - 'yajin': str(dashou_profile.yajin), - 'chenghao': dashou_profile.chenghao, - 'jinfen': dashou_profile.jifen, - 'chengjiaoliang': dashou_profile.chengjiaozongliang, - 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, - 'dashouzhuangtai': dashou_profile.zhuangtai, - } - - # 会员列表 - huiyuan_list = [] - try: - records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid) - for record in records: - shifou_daoqi = record.jiance_shifou_daoqi() - huiyuan_list.append({ - 'huiyuan_id': record.huiyuan_id, - 'huiyuan_zhuangtai': record.huiyuan_zhuangtai, - 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None, - 'shifou_daoqi': shifou_daoqi, - 'jieshao': record.jieshao - }) - except Exception as e: - logger.warning(f"获取会员列表失败: {e}") - data['clumber'] = huiyuan_list - return data - - def _fanhui_xianyou_guanshi_info(self, user_main): - """用户已是管事时的返回""" - guanshi_profile = user_main.guanshi_profile - dashou_profile = getattr(user_main, 'dashou_profile', None) - - group_info = self._huoqu_qun_peizhi() - - dashou_status = 1 if dashou_profile else 0 - shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0 - guanshi_status = 1 - - try: - nicheng = user_main.boss_profile.nickname or '微信用户' - except: - nicheng = '微信用户' - - refresh = RefreshToken.for_user(user_main) - token = str(refresh.access_token) - - data = { - 'token': token, - 'nicheng': nicheng, - 'uid': user_main.yonghuid, - 'touxiang': user_main.avatar or '', - 'shangjiastatus': shangjia_status, - 'dashoustatus': dashou_status, - 'guanshistatus': guanshi_status, - **group_info, - } - - # 管事信息 - guanshi_data = { - 'gszhstatus': guanshi_profile.zhuangtai, - 'yaoqingzongshu': guanshi_profile.yaogingshuliang, - 'fenyongzonge': str(guanshi_profile.chongzhifenrun), - 'fenyongtixian': str(guanshi_profile.yue), - 'yichongzhiDashou': guanshi_profile.jinrichongzhi, - } - data.update(guanshi_data) - - if dashou_profile: - dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile) - data.update(dashou_data) - else: - data.update({ - 'dashounicheng': '', - 'zhanghaostatus': 0, - 'yongjin': '0.00', - 'zonge': '0.00', - 'yajin': '0.00', - 'chenghao': '', - 'jinfen': 0, - 'chengjiaoliang': 0, - 'zaixianzhuangtai': 0, - 'dashouzhuangtai': 0, - 'clumber': [] - }) - - return Response({'code': 200, 'msg': '您已是管事', 'data': data}) - - def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token): - """组装完整返回数据(新注册)""" - try: - nicheng = user_main.boss_profile.nickname or '微信用户' - except: - nicheng = '微信用户' - - dashou_status = 1 - shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0 - guanshi_status = 1 - - group_info = self._huoqu_qun_peizhi() - - data = { - 'token': token, - 'nicheng': nicheng, - 'uid': user_main.yonghuid, - 'touxiang': user_main.avatar or '', - 'shangjiastatus': shangjia_status, - 'dashoustatus': dashou_status, - 'guanshistatus': guanshi_status, - **group_info, - } - - # 管事信息 - guanshi_data = { - 'gszhstatus': guanshi_profile.zhuangtai, - 'yaoqingzongshu': guanshi_profile.yaogingshuliang, - 'fenyongzonge': str(guanshi_profile.chongzhifenrun), - 'fenyongtixian': str(guanshi_profile.yue), - 'yichongzhiDashou': guanshi_profile.jinrichongzhi, - } - data.update(guanshi_data) - - dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile) - data.update(dashou_data) - - return data - - - - - - - - - -from rest_framework_simplejwt.authentication import JWTAuthentication -from django.db import IntegrityError - - - -class ZuzhangZhuceView(APIView): - """ - 组长注册/激活接口 - POST /yonghu/zuzhangzhuce - 请求体: {"inviteCode": "xxx"} - 返回: - { - "code": 200, - "msg": "success", - "data": { - "ketixian": "0.00", # 可提现金额 - "guanshiCount": 0, # 邀请的管事数量 - "fenhongZonge": "0.00" # 分红总额 - } - } - 或错误码 - """ - - permission_classes = [IsAuthenticated] - - # 硬编码激活码(注意转义特殊字符) - ACTIVATION_CODE = "237huwehdw77e777eyw224fr" - - def post(self, request): - user = request.user - invite_code = request.data.get('inviteCode', '').strip() - - if not invite_code: - return Response({'code': 400, 'msg': '邀请码不能为空'}, status=400) - - # 检查是否已是组长 - try: - zuzhang = UserZuzhang.objects.select_related('user').get(user=user) - # 已注册,直接返回数据 - data = self._build_response_data(zuzhang, user) - return Response({'code':200, 'msg': '已注册', 'data': data}) - except UserZuzhang.DoesNotExist: - pass - - # 比对激活码 - if invite_code != self.ACTIVATION_CODE: - return Response({'code': 400, 'msg': '邀请码无效'}, status=400) - - # 生成唯一组长邀请码:时间戳 + 用户ID后6位 + 随机字符 - yaoqingma = self._generate_unique_yaoqingma(user.yonghuid) - - # 创建组长扩展记录 - try: - with transaction.atomic(): - zuzhang = UserZuzhang.objects.create( - user=user, - yaoqingma=yaoqingma, - fenyong_zonge=0.00, - ketixian_jine=0.00, - yaoqing_zongshu=0, - jinri_fenyong=0.00, - jinyue_fenyong=0.00, - zhuangtai=1, - jinri_tixian=0.00, - ewai_tixian_xiane=0.00, - kaioi_ewai_tixian=False, - kaioi_ewai_fenhong=False, - ewai_fenhong_jine=0.00 - ) - except IntegrityError: - # 极低概率重复,重试一次 - yaoqingma = self._generate_unique_yaoqingma(user.yonghuid, retry=True) - zuzhang = UserZuzhang.objects.create( - user=user, - yaoqingma=yaoqingma, - # 其他字段默认同上 - fenyong_zonge=0.00, - ketixian_jine=0.00, - yaoqing_zongshu=0, - jinri_fenyong=0.00, - jinyue_fenyong=0.00, - zhuangtai=1, - jinri_tixian=0.00, - ewai_tixian_xiane=0.00, - kaioi_ewai_tixian=False, - kaioi_ewai_fenhong=False, - ewai_fenhong_jine=0.00 - ) - - data = self._build_response_data(zuzhang, user) - return Response({'code':200, 'msg': '注册成功', 'data': data}) - - def _generate_unique_yaoqingma(self, yonghuid, retry=False): - """生成唯一邀请码:时间戳+用户ID后6位+随机2位字符""" - import random, string - timestamp = str(int(time.time()))[-6:] # 取后6位 - uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0') - rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2)) - base = f"Z{timestamp}{uid_part}{rand_str}" - # 确保唯一性(若已存在则追加随机数,最多尝试3次) - for _ in range(3): - if not UserZuzhang.objects.filter(yaoqingma=base).exists(): - return base - base = base + random.choice(string.digits) - # 极端情况,再附加时间戳毫秒 - millis = str(time.time()).replace('.', '')[-4:] - return base + millis - - def _build_response_data(self, zuzhang, user): - """组装前端所需数据""" - # 计算邀请的管事数量(通过UserGuanshi表的yaoqingren字段关联到user的yonghuid) - guanshi_count = UserGuanshi.objects.filter(yaoqingren=user.yonghuid).count() - return { - 'ketixian': str(zuzhang.ketixian_jine), # 可提现余额 - 'guanshiCount': guanshi_count, # 邀请管事数 - 'fenhongZonge': str(zuzhang.fenyong_zonge) # 分红总额 - } - - - -class ZuzhangXinxiView(APIView): - """ - 组长信息查询接口 - POST /yonghu/zuzhangxinxi - 请求体: 无 - 返回: 同注册接口成功返回格式 - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - user = request.user - try: - zuzhang = UserZuzhang.objects.get(user=user) - except UserZuzhang.DoesNotExist: - return Response({'code': 404, 'msg': '用户不是组长'}, status=404) - - guanshi_count = UserGuanshi.objects.filter(yaoqingren=user.yonghuid).count() - data = { - 'ketixian': str(zuzhang.ketixian_jine), - 'guanshiCount': guanshi_count, - 'fenhongZonge': str(zuzhang.fenyong_zonge) - } - return Response({'code': 200, 'msg': 'success', 'data': data}) - - - - - -# apps/yonghu/views_dashou_jianquan.py -""" -打手 IM 消息权限鉴权接口 -URL: /yonghu/dshqltqx -方法: POST -认证: JWT Token (request.user) - -鉴权逻辑(满足任意一条即通过): - 1. 押金 >= 5 元 → 通过 - 2. 拥有未过期会员 → 通过 - 3. 存在订单状态为 2 或 8 的订单(作为接单打手) → 通过 - 以上均不满足 → 拒绝 -""" - - -class DashouJianquanView(APIView): - """打手消息权限鉴权""" - permission_classes = [IsAuthenticated] - - def post(self, request): - # ========== 1. 通过 JWT 获取当前登录用户 ========== - user = request.user # UserMain 实例 - - # ========== 2. 检查是否为已注册打手 ========== - try: - dashou = user.dashou_profile # OneToOneField 反向查询 - except UserDashou.DoesNotExist: - return Response({ - 'code': 0, - 'allow': 0, - 'msg': '您尚未注册打手身份,请先注册' - }) - - # ========== 3. 条件一:押金 >= 5 元 ========== - if dashou.yajin and dashou.yajin >= 5: - return Response({ - 'code': 0, - 'allow': 1, - 'msg': '鉴权通过(押金满足条件)' - }) - - # ========== 4. 条件二:存在未过期会员 ========== - # 高效查询:利用 yonghu_id + huiyuan_zhuangtai 复合索引 - huiyuan_list = Huiyuangoumai.objects.filter( - yonghu_id=user.yonghuid, - huiyuan_zhuangtai=1 # 只查状态为"可用"的,减少内存遍历 - ).only('id', 'daoqi_time', 'huiyuan_zhuangtai') - - for hy in huiyuan_list: - if not hy.jiance_shifou_daoqi(): # 未过期返回 False - return Response({ - 'code': 0, - 'allow': 1, - 'msg': '鉴权通过(会员未过期)' - }) - - # ========== 5. 条件三:存在订单状态为 2 或 8 的订单 ========== - # 利用 jiedan_dashou_id + zhuangtai 复合索引,只查 1 条 - has_valid_order = Dingdan.objects.filter( - jiedan_dashou_id=user.yonghuid, - zhuangtai__in=[2, 8] - ).only('id').exists() # exists() 高性能判断,只发 SELECT 1 LIMIT 1 - - if has_valid_order: - return Response({ - 'code': 0, - 'allow': 1, - 'msg': '鉴权通过(存在有效订单)' - }) - - # ========== 6. 以上均不满足 → 拒绝 ========== - return Response({ - 'code': 0, - 'allow': 0, - 'msg': '消息权限不足,请充值会员或联系管理员' - }) - - - -# apps/yonghu/views_laoban_jianquan.py -""" -老板(普通用户) IM 消息权限鉴权接口 -URL: /yonghu/lbhqltqx -方法: POST -认证: JWT Token (request.user) - -鉴权逻辑: - 查询平台订单扩展表(DingdanPingtai), - 通过关联的订单主表(Dingdan)查找该老板的订单, - 只要存在任意一条订单状态在 1~8 范围内 → 通过 -""" - - - -class LaobanJianquanView(APIView): - """老板消息权限鉴权""" - permission_classes = [IsAuthenticated] - - def post(self, request): - # ========== 1. 通过 JWT 获取当前登录用户 ========== - user = request.user # UserMain 实例 - - # ========== 2. 高效查询平台订单扩展表 ========== - # 利用 DingdanPingtai.laoban_id 索引 + Dingdan.zhuangtai 索引 - # 只查 1 条,用 exists() 实现最高性能 - has_valid_order = DingdanPingtai.objects.filter( - laoban_id=user.yonghuid - ).filter( - dingdan__zhuangtai__in=[1, 2, 3, 4, 5, 6, 7, 8] - ).only('id').exists() - - if has_valid_order: - return Response({ - 'code': 0, - 'allow': 1, - 'msg': '鉴权通过' - }) - - return Response({ - 'code': 0, - 'allow': 0, - 'msg': '您暂无订单记录,消息功能暂不可用' - }) - - - - - - - -# views.py (在处罚相关app的views中) -import logging -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.views import APIView -from django.db.models import Count, Q, Case, When, IntegerField -from dingdan.models import Fadan -from dingdan.models import FadanShensuTupian - -logger = logging.getLogger('xiaochengxu') - -class FaKuanJiFenTongJiView(APIView): - """ - 获取罚款与积分处罚统计总览 - 路径: POST /yonghu/cffktjhq - """ - permission_classes = [IsAuthenticated, DashouPermission] - - def post(self, request): - try: - user_main = request.user - yonghuid = user_main.yonghuid - - # ================= 罚款统计 ================= - fadan_stats = Fadan.objects.filter(beichufa_id=yonghuid).aggregate( - total=Count('id'), - daijiaona=Count('id', filter=Q(zhuangtai=1)), - shensuzhong=Count('id', filter=Q(zhuangtai=3)), - yijiaona=Count('id', filter=Q(zhuangtai=2)), - yibohui=Count('id', filter=Q(zhuangtai=4)), - ) - - # ================= 积分处罚统计 ================= - jifen_stats = Chufajilu.objects.filter(dashouid=yonghuid).aggregate( - total=Count('id'), - daichuli=Count('id', filter=Q(sqzhuangtai=0) | Q(sqzhuangtai=3)), - yichuli=Count('id', filter=Q(sqzhuangtai=1) | Q(sqzhuangtai=2)), - # 细分 - daichuli_0=Count('id', filter=Q(sqzhuangtai=0)), - shensuzhong_3=Count('id', filter=Q(sqzhuangtai=3)), - yichufa_1=Count('id', filter=Q(sqzhuangtai=1)), - yibohui_2=Count('id', filter=Q(sqzhuangtai=2)), - ) - - data = { - "fakuan": { - "total": fadan_stats['total'], - "daijiaona": fadan_stats['daijiaona'], - "shensuzhong": fadan_stats['shensuzhong'], - "yijiaona": fadan_stats['yijiaona'], - "yibohui": fadan_stats['yibohui'], - }, - "jifen": { - "total": jifen_stats['total'], - "daichuli": jifen_stats['daichuli'], - "yichuli": jifen_stats['yichuli'], - "daichuli_0": jifen_stats['daichuli_0'], - "shensuzhong_3": jifen_stats['shensuzhong_3'], - "yichufa_1": jifen_stats['yichufa_1'], - "yibohui_2": jifen_stats['yibohui_2'], - } - } - - return Response({'code': 0, 'msg': '获取统计成功', 'data': data}) - - except Exception as e: - logger.error(f"获取统计异常 {yonghuid}: {e}", exc_info=True) - return Response({'code': 99, 'msg': '系统繁忙'}, status=500) - -class DaShouFaKuanLieBiaoView(APIView): - """ - 打手罚款列表接口 - 路径:POST /yonghu/dsfklbhq - 参数: - page: 页码(默认1) - page_size: 每页条数(默认10,最大50) - zhuangtai: 状态筛选(1待缴纳/2已缴纳/3申诉中/4申诉成功,0或不传表示全部) - sousuo_dingdan_id: 订单号模糊搜索 - 返回:{ - list: [{ - id, beichufa_id, guanliandingdan_id, chufaliyou, fakuanjine, zhuangtai, - yingxiang_qiangdan, shensuliyou, bohuiliyou, - zhengju_tupian: [...], // 证据图片(yongtu=1) - shensu_tupian: [...], // 申诉图片(yongtu=2) - create_time, update_time - }], - has_more, page, page_size - } - """ - permission_classes = [IsAuthenticated, DashouPermission] - - def post(self, request): - try: - user_main = request.user - yonghuid = user_main.yonghuid - - # 获取分页参数 - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 10)) - if page_size > 50: - page_size = 50 - if page_size < 1: - page_size = 10 - - # 状态筛选 - zhuangtai = request.data.get('zhuangtai') - # 订单号搜索 - sousuo_dingdan_id = request.data.get('sousuo_dingdan_id', '').strip() - - # 基础查询:当前用户的罚单 - qs = Fadan.objects.filter(beichufa_id=yonghuid) - - # 状态筛选 - if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]: - qs = qs.filter(zhuangtai=int(zhuangtai)) - - # 订单号模糊搜索 - if sousuo_dingdan_id: - qs = qs.filter(guanliandingdan_id__icontains=sousuo_dingdan_id) - - # 按创建时间倒序 - qs = qs.order_by('-create_time') - - # 分页 - paginator = Paginator(qs, page_size) - try: - page_obj = paginator.page(page) - except Exception: - return Response({ - 'code': 0, 'msg': '获取成功', - 'data': {'list': [], 'has_more': False, 'page': page, 'page_size': page_size} - }) - - records = list(page_obj) - fadan_ids = [r.id for r in records] - - # 批量获取图片,按用途分组 - zhengju_map = {} # key: fadan_id, value: [url, ...] - shensu_map = {} - if fadan_ids: - tupians = FadanShensuTupian.objects.filter( - fadan_id__in=fadan_ids - ).values('fadan_id', 'tupian_url', 'yongtu') - for t in tupians: - fid = t['fadan_id'] - url = t['tupian_url'] - if t['yongtu'] == 1: - zhengju_map.setdefault(fid, []).append(url) - else: - shensu_map.setdefault(fid, []).append(url) - - # 组装返回数据 - chufa_list = [] - for r in records: - rid = r.id - item = { - 'id': rid, - 'beichufa_id': r.beichufa_id, - 'guanliandingdan_id': r.guanliandingdan_id or '', - 'chufaliyou': r.chufaliyou or '', - 'fakuanjine': str(r.fakuanjine), - 'zhuangtai': r.zhuangtai, - 'yingxiang_qiangdan': r.yingxiang_qiangdan, - 'shensuliyou': r.shensuliyou or '', - 'bohuiliyou': r.bohuiliyou or '', - 'zhengju_tupian': zhengju_map.get(rid, []), # 证据图片 - 'shensu_tupian': shensu_map.get(rid, []), # 申诉图片 - 'create_time': r.create_time.strftime('%Y-%m-%d %H:%M:%S') if r.create_time else '', - 'update_time': r.update_time.strftime('%Y-%m-%d %H:%M:%S') if r.update_time else '', - } - chufa_list.append(item) - - has_more = page_obj.has_next() - return Response({ - 'code': 0, - 'msg': '获取成功', - 'data': { - 'list': chufa_list, - 'has_more': has_more, - 'page': page, - 'page_size': page_size, - } - }) - - except Exception as e: - logger.error(f"获取罚款列表异常 {yonghuid if 'yonghuid' in locals() else ''}: {e}", exc_info=True) - return Response({'code': 99, 'msg': '系统繁忙'}, status=500) - - -class FaKuanShenSuView(APIView): - """ - 罚款申诉提交/修改接口 - 路径:POST /yonghu/fkss - 参数: - fadan_id: 罚单ID - shensu_liyou: 申诉理由 - shensu_tupian_urls: 图片相对URL数组(可空数组,但至少要有理由) - 逻辑: - - 仅待缴纳状态且无驳回理由的罚单可申诉 - - 允许对已有申诉进行修改:先删除旧申诉图片(yongtu=2),再添加新图片 - """ - permission_classes = [IsAuthenticated, DashouPermission] - - def post(self, request): - try: - user_main = request.user - yonghuid = user_main.yonghuid - fadan_id = request.data.get('fadan_id') - shensu_liyou = request.data.get('shensu_liyou', '').strip() - tupian_urls = request.data.get('shensu_tupian_urls', []) - - if not fadan_id: - return Response({'code': 400, 'msg': '缺少罚款ID'}, status=400) - if not shensu_liyou: - return Response({'code': 400, 'msg': '请输入申诉理由'}, status=400) - - # 查询罚单,确保属于当前打手 - try: - fadan = Fadan.objects.get(id=fadan_id, beichufa_id=yonghuid) - except Fadan.DoesNotExist: - return Response({'code': 404, 'msg': '罚单不存在或无权操作'}, status=404) - - # 只允许待缴纳状态(状态1)且没有驳回理由的罚单申诉 - if fadan.zhuangtai != 1: - return Response({'code': 400, 'msg': '当前状态不可申诉'}, status=400) - if fadan.bohuiliyou: - return Response({'code': 400, 'msg': '该罚单申诉已被驳回,不可再次申诉'}, status=400) - - # 更新罚单状态为申诉中,并记录理由 - fadan.zhuangtai = 3 - fadan.shensuliyou = shensu_liyou - fadan.save() - - # 删除旧的申诉图片(yongtu=2),仅删除打手自己上传的申诉图 - FadanShensuTupian.objects.filter(fadan=fadan, yongtu=2).delete() - - # 插入新上传的申诉图片 - for url in tupian_urls: - FadanShensuTupian.objects.create( - fadan=fadan, - beichufa_id=yonghuid, - tupian_url=url, - yongtu=2 - ) - - return Response({'code': 0, 'msg': '申诉提交成功'}) - - except Exception as e: - logger.error(f"罚款申诉异常 {yonghuid if 'yonghuid' in locals() else ''}: {e}", exc_info=True) - return Response({'code': 99, 'msg': '系统繁忙'}, status=500) - - - - - - - -# 需要替换为你自己项目中的导入路径 -from shangpin.models import Czjilu -import string # 【致命错误修正】必须导入 string - -logger = logging.getLogger('xiaochengxu') - -# ==================== 1. 罚款支付下单接口 ==================== - -class FaKuanPayView(APIView): - """ - 罚款微信支付下单接口(完全照抄 YajinGoumai) - 路径:POST /yonghu/fkjn - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - fadan_id = request.data.get('fadan_id') - if not fadan_id: - return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) - - try: - fadan = Fadan.objects.get(id=fadan_id, beichufa_id=request.user.yonghuid) - except Fadan.DoesNotExist: - return Response({'code': 404, 'message': '罚单不存在或不属于您'}, status=status.HTTP_404_NOT_FOUND) - - if fadan.zhuangtai not in [1, 3]: - return Response({'code': 400, 'message': '罚单当前状态不可支付'}, status=status.HTTP_400_BAD_REQUEST) - - jine = float(fadan.fakuanjine) - - timestamp_ms = int(time.time() * 1000) - timestamp_ns = int(time.time_ns() // 1000) % 1000000 - random_num = random.randint(0, 99) - dingdanid = f"FK{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" - - chongzhi_jilu = Czjilu.objects.create( - dingdan_id=dingdanid, - zhuangtai=9, - yonghuid=request.user.yonghuid, - jine=jine, - leixing=4, - shuoming=settings.FAKUAN_PAY_DESCRIPTION - ) - - try: - pay_params = self.generate_wechat_pay_params( - dingdanid=dingdanid, - jine=jine, - openid=request.user.openid, - pay_type='fakuan' - ) - except Exception as e: - chongzhi_jilu.delete() - logger.error(f"生成微信支付参数失败: {str(e)}") - return Response({ - 'code': 500, - 'message': f'支付参数生成失败: {str(e)}' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - return Response({ - 'code': 200, - 'message': '支付参数生成成功', - 'payParams': pay_params, - 'dingdanid': dingdanid - }, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f"罚款支付下单接口异常: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'message': '服务器内部错误,请稍后重试' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): - APPID = settings.WEIXIN_APPID - MCHID = settings.WEIXIN_MCHID - KEY = settings.WEIXIN_SHANGHUMIYAO - - if pay_type == 'fakuan': - PAY_DESCRIPTION = settings.FAKUAN_PAY_DESCRIPTION - NOTIFY_URL = settings.FAKUAN_PAY_NOTIFY_URL # 回调地址在这里 - else: - raise Exception('未知的支付类型') - - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - total_fee = int(jine * 100) - trade_type = 'JSAPI' - user_ip = self.get_client_ip() - - # 第一次签名 - sign_data = { - 'appid': APPID, - 'mch_id': MCHID, - 'nonce_str': nonce_str, - 'body': PAY_DESCRIPTION, - 'out_trade_no': dingdanid, - 'total_fee': total_fee, - 'spbill_create_ip': user_ip, - 'notify_url': NOTIFY_URL, # 关键:把回调地址传入 - 'trade_type': trade_type, - 'openid': openid - } - - sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) - sign_string += f'&key={KEY}' - sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - # 构建 XML,这里必须包含 notify_url - xml_data = f""" -{APPID} -{PAY_DESCRIPTION} -{MCHID} -{nonce_str} -{NOTIFY_URL} -{openid} -{dingdanid} -{user_ip} -{total_fee} -{trade_type} -{sign} -""" - - response = requests.post( - 'https://api.mch.weixin.qq.com/pay/unifiedorder', - data=xml_data.encode('utf-8'), - headers={'Content-Type': 'application/xml'}, - timeout=10 - ) - - root = ET.fromstring(response.content) - return_code = root.find('return_code').text - result_code = root.find('result_code').text - - if return_code == 'SUCCESS' and result_code == 'SUCCESS': - prepay_id = root.find('prepay_id').text - timestamp = str(int(time.time())) - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - package = f'prepay_id={prepay_id}' - sign_type = 'MD5' - - pay_sign_data = { - 'appId': APPID, - 'timeStamp': timestamp, - 'nonceStr': nonce_str, - 'package': package, - 'signType': sign_type - } - - sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) - pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) - pay_sign_string += f'&key={KEY}' - pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() - - return { - 'appId': APPID, - 'timeStamp': timestamp, - 'nonceStr': nonce_str, - 'package': package, - 'signType': sign_type, - 'paySign': pay_sign - } - else: - err_code = root.find('err_code') - err_code_des = root.find('err_code_des') - error_details = f"return_code: {return_code}" - if err_code is not None: - error_details += f", err_code: {err_code.text}" - if err_code_des is not None: - error_details += f", err_code_des: {err_code_des.text}" - raise Exception(f"微信支付下单失败: {error_details}") - - def get_client_ip(self): - request = self.request - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR') - return ip if ip else '127.0.0.1' - - -# ==================== 2. 罚款支付回调接口 ==================== -from django.views import View -class FaKuanHuitiaoView(View): - - """ - 罚款支付回调接口(严格借鉴 YajinHuitiao 接口,参数一个不少) - 路径:POST /yonghu/fk_huitiao - """ - def post(self, request): - try: - # 1. 解析微信回调XML数据 - request_body = request.body - logger.info(f"收到微信支付回调,原始数据: {request_body.decode('utf-8')}") - - root = ET.fromstring(request_body) - - # 获取关键字段 - return_code = root.find('return_code').text - result_code = root.find('result_code').text - out_trade_no = root.find('out_trade_no').text # 订单ID - transaction_id = root.find('transaction_id').text # 微信交易号 - total_fee = int(root.find('total_fee').text) # 支付金额(分) - - logger.info(f"回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") - - # 2. 验证基本返回状态 - if return_code != 'SUCCESS' or result_code != 'SUCCESS': - logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}") - return self.wechat_response('FAIL', '支付失败') - - # 3. 验证签名(确保是微信发来的) - if not self.verify_signature(request_body): - logger.error(f"签名验证失败: {out_trade_no}") - return self.wechat_response('FAIL', '签名验证失败') - - # 4. 查询订单 - try: - order = Czjilu.objects.get(dingdan_id=out_trade_no) - logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") - except Czjilu.DoesNotExist: - logger.error(f"订单不存在: {out_trade_no}") - return self.wechat_response('FAIL', '订单不存在') - - # 5. 检查订单状态是否为未支付(9) - if order.zhuangtai != 9: - logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") - # 已处理过的订单直接返回成功,避免微信重复回调 - return self.wechat_response('SUCCESS', 'OK') - - # 6. 验证金额(微信返回的是分,需要转换) - wechat_amount_yuan = total_fee / 100 # 转换为元 - order_amount_yuan = float(order.jine) # 订单金额(元) - - # 允许1分钱的误差(浮点数比较) - if abs(wechat_amount_yuan - order_amount_yuan) > 0.01: - logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") - return self.wechat_response('FAIL', '金额不一致') - - # 7. 更新订单状态为已支付(3) - order.zhuangtai = 3 - order.save(update_fields=['zhuangtai']) - logger.info(f"订单状态更新成功: {order.dingdan_id} -> 3") - - # 8. 更新罚单状态为已缴纳(2) - try: - # 根据订单中的用户ID和金额匹配待缴纳的罚单 - fadan = Fadan.objects.filter( - beichufa_id=order.yonghuid, - fakuanjine=order.jine, - zhuangtai__in=[1, 3] - ).first() - if fadan: - fadan.zhuangtai = 2 # 已缴纳 - fadan.save(update_fields=['zhuangtai']) - logger.info(f"罚单 {fadan.id} 已自动标记为已缴纳") - - from yonghu.fadan_fenhong_utils import process_fadan_fenhong - success, msg = process_fadan_fenhong(fadan.id) - if not success: - logger.error(f"罚单 {fadan.id} 分红处理失败: {msg}") - else: - logger.info(f"罚单 {fadan.id} 分红处理完成: {msg}") - - except Exception as e: - logger.error(f"更新罚单状态失败: {str(e)}", exc_info=True) - - # 9. 更新收支记录表 - try: - from peizhi.models import Szjilu - from decimal import Decimal - - # 获取订单金额(确保是Decimal类型) - jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine - - from dingdan.utils import update_daily_income - update_daily_income(jine_decimal) - - # 查询或创建ID=1的收支记录 - szjilu, created = Szjilu.objects.get_or_create( - id=1, - defaults={ - 'zongsy': Decimal('0.00'), - 'zongls': Decimal('0.00'), - 'zongzc': Decimal('0.00'), - 'jrzc': Decimal('0.00'), - 'jrls': Decimal('0.00') - } - ) - - # 更新三个字段 - szjilu.zongsy += jine_decimal # 总收益 - szjilu.zongls += jine_decimal # 总流水 - szjilu.jrls += jine_decimal # 今日流水 - - szjilu.save() - - logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") - - except Exception as e: - # 收支记录更新失败不影响主流程,只记录日志 - logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) - - # 10. 返回成功响应给微信 - logger.info(f"罚款支付回调处理完成: {out_trade_no}") - return self.wechat_response('SUCCESS', 'OK') - - except ET.ParseError as e: - logger.error(f"XML解析失败: {str(e)}") - return self.wechat_response('FAIL', '数据格式错误') - except Exception as e: - logger.error(f"罚款支付回调处理异常: {str(e)}", exc_info=True) - # 返回FAIL让微信重试 - return self.wechat_response('FAIL', '处理异常') - - def verify_signature(self, xml_data): - """验证微信支付签名(完全照搬 YajinHuitiao 的 verify_signature)""" - try: - root = ET.fromstring(xml_data) - sign = root.find('sign').text - - if not sign: - logger.error("签名字段不存在") - return False - - # 获取所有参数并排除sign - params = {} - for child in root: - if child.tag != 'sign' and child.text is not None: - params[child.tag] = child.text - - # 按照参数名ASCII码从小到大排序 - sorted_params = sorted(params.items(), key=lambda x: x[0]) - - # 拼接字符串 - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' - - # 计算签名 - calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - is_valid = sign == calculated_sign - if not is_valid: - logger.error(f"签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}") - - return is_valid - - except Exception as e: - logger.error(f"验证签名异常: {str(e)}") - return False - - def wechat_response(self, return_code, return_msg): - """返回微信支付回调响应(完全照搬 YajinHuitiao 的 wechat_response)""" - xml_response = f""" - - -""" - - logger.info(f"返回微信响应: {return_code} - {return_msg}") - return HttpResponse(xml_response, content_type='application/xml') - - -# ==================== 3. 支付轮询接口 ==================== -class FaKuanPayPollView(APIView): - """罚款支付轮询接口(借鉴 YajinLunxun)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({'code': 400, 'message': '订单ID不能为空'}) - - order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid) - if order.zhuangtai == 3: - return Response({'code': 200, 'message': '支付成功'}) - else: - return Response({'code': 400, 'message': '订单尚未支付'}) - except Czjilu.DoesNotExist: - return Response({'code': 400, 'message': '订单不存在'}) - except Exception as e: - logger.error(f"轮询异常: {str(e)}") - return Response({'code': 500, 'message': '服务器内部错误'}) - - -# ==================== 4. 支付失败处理接口 ==================== -class FaKuanPayFailView(APIView): - """罚款支付失败处理接口(借鉴 YajinShibai)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({'code': 400, 'message': '订单ID不能为空'}) - - order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid) - if order.zhuangtai == 9: # 未支付 - order.delete() - logger.info(f"用户{request.user.yonghuid}删除未支付订单: {dingdanid}") - return Response({'code': 200, 'message': '订单已删除'}) - else: - return Response({'code': 400, 'message': '订单状态不是未支付,无法删除'}) - except Czjilu.DoesNotExist: - return Response({'code': 400, 'message': '订单不存在'}) - except Exception as e: - logger.error(f"支付失败处理异常: {str(e)}") - return Response({'code': 500, 'message': '服务器内部错误'}) - - - - - -from shangpin.models import Czjilu -from dengji.models import Chenghao, ShenheJilu, KaoheCishuFeiyong, YonghuChenghao -from dengji.utils import get_tag_fee # 您需要确保此函数已定义 - -from dengji.utils import create_shenhe_jilu -from dengji.utils import validate_shenheguan - -logger = logging.getLogger('xiaochengxu') - - - -class KaohePayView(APIView): - """ - 考核支付下单接口 - 路径:POST /yonghu/khzf - 参数: - tag_id: 标签ID - game_nick: 游戏昵称 - remark: 备注 - reapply: 是否再次申请 (true/false) - jilu_id: 再次申请时的原记录ID(reapply=true时必传) - shenheguan_id: 指定考核官ID(可选) - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - tag_id = request.data.get('tag_id') - game_nick = request.data.get('game_nick', '').strip() - remark = request.data.get('remark', '').strip() - reapply = request.data.get('reapply', False) - jilu_id = request.data.get('jilu_id') if reapply else None - shenheguan_id = request.data.get('shenheguan_id', '').strip() - - logger.info(f"用户 {user.yonghuid} 发起考核支付申请,标签ID: {tag_id}, 再次申请: {reapply}") - - if not tag_id or not game_nick: - logger.warning(f"用户 {user.yonghuid} 参数不完整: tag_id={tag_id}, game_nick={game_nick}") - return Response({'code': 400, 'message': '标签ID和游戏昵称不能为空'}) - - # 1. 检查是否有未支付的考核订单 - """unpaid_order = Czjilu.objects.filter( - yonghuid=user.yonghuid, - leixing=5, - zhuangtai=9 - ).exists() - if unpaid_order: - logger.warning(f"用户 {user.yonghuid} 存在未支付的考核订单") - return Response({'code': 400, 'message': '您有未支付的考核订单,请先完成支付或取消'})""" - - # 2. 验证标签 - try: - chenghao = Chenghao.objects.get(id=tag_id) - logger.info(f"标签验证通过: {chenghao.mingcheng}") - except Chenghao.DoesNotExist: - logger.error(f"标签不存在: tag_id={tag_id}") - return Response({'code': 404, 'message': '标签不存在'}) - - # 3. 检查用户是否已拥有该标签 - if YonghuChenghao.objects.filter(yonghu=user, chenghao=chenghao).exists(): - logger.warning(f"用户 {user.yonghuid} 已拥有标签 {chenghao.mingcheng}") - return Response({'code': 400, 'message': '您已拥有该标签,无需重复考核'}) - - # 4. 检查是否有未完成的申请 - if ShenheJilu.objects.filter( - shenqingren_id=user.yonghuid, - chenghao=chenghao, - zhuangtai__in=[0, 3] # 审核中、待审核 - ).exists(): - logger.warning(f"用户 {user.yonghuid} 有未完成的考核申请") - return Response({'code': 400, 'message': '您有未完成的考核申请,请等待结果'}) - - # 5. 计算考核次数和费用 - if reapply and jilu_id: - last = ShenheJilu.objects.filter( - jilu_id=jilu_id, - shenqingren_id=user.yonghuid, - zhuangtai=2 - ).first() - if not last: - logger.warning(f"用户 {user.yonghuid} 再次申请失败:原记录不存在或状态不是未通过,jilu_id={jilu_id}") - return Response({'code': 400, 'message': '原记录不存在或状态不是未通过'}) - new_cishu = last.cishu + 1 - logger.info(f"再次申请,原记录ID: {jilu_id},新次数: {new_cishu}") - else: - max_cishu = ShenheJilu.objects.filter( - shenqingren_id=user.yonghuid, - chenghao=chenghao, - zhuangtai__in=[1, 2] - ).aggregate(max=models.Max('cishu'))['max'] or 0 - new_cishu = max_cishu + 1 - logger.info(f"首次申请,计算次数: {new_cishu}") - - # 6. 获取本次费用 - fee = get_tag_fee(chenghao, new_cishu) - logger.info(f"本次考核费用: {fee}元,次数: {new_cishu}") - - if fee <= 0: - logger.warning(f"费用为0,应使用免费申请接口,tag_id={tag_id}, cishu={new_cishu}") - return Response({'code': 400, 'message': '本次费用为0,请直接使用免费申请接口'}) - - # 7. 生成订单ID - timestamp_ms = int(time.time() * 1000) - timestamp_ns = int(time.time_ns() // 1000) % 1000000 - random_num = random.randint(0, 99) - dingdanid = f"KH{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" - logger.info(f"生成订单ID: {dingdanid}") - - # 8. 创建支付订单(Czjilu) - order = Czjilu.objects.create( - dingdan_id=dingdanid, - zhuangtai=9, # 未支付 - yonghuid=user.yonghuid, - jine=float(fee), - leixing=5, # 5-考核支付 - shuoming=settings.KAOHE_PAY_DESCRIPTION - ) - logger.info(f"创建支付订单成功: {dingdanid}") - - # 9. 保存考核支付临时上下文(用于回调时取业务参数) - try: - from dengji.models import KaohePayTemp - KaohePayTemp.objects.create( - dingdan_id=dingdanid, - yonghuid=user.yonghuid, - tag_id=tag_id, - game_nick=game_nick, - remark=remark, - reapply=reapply, - jilu_id=jilu_id if reapply else None, - shenheguan_id=shenheguan_id, - cishu=new_cishu, - fee=fee, - ) - logger.info(f"保存临时上下文成功: {dingdanid}") - except Exception as e: - logger.error(f"保存临时上下文失败: {str(e)}", exc_info=True) - # 临时表保存失败应该回滚订单 - order.delete() - return Response({'code': 500, 'message': '系统异常,请稍后重试'}) - - # 10. 构造 attach 参数(只传订单ID,确保不超127字节) - attach_str = json.dumps({'oid': dingdanid}, ensure_ascii=False) - logger.info(f"attach内容: {attach_str},长度: {len(attach_str.encode('utf-8'))}字节") - - # 11. 生成微信支付参数 - try: - pay_params = self.generate_wechat_pay_params( - dingdanid=dingdanid, - jine=float(fee), - openid=user.openid, - attach=attach_str, - pay_type='kaohe' - ) - logger.info(f"微信支付参数生成成功: {dingdanid}") - except Exception as e: - order.delete() # 下单失败,删除订单记录 - # 同时删除临时表记录 - try: - from dengji.models import KaohePayTemp - KaohePayTemp.objects.filter(dingdan_id=dingdanid).delete() - except Exception as del_e: - logger.error(f"删除临时表记录失败: {str(del_e)}") - logger.error(f"生成微信支付参数失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'message': f'支付参数生成失败: {str(e)}'}) - - return Response({ - 'code': 200, - 'message': '支付参数生成成功', - 'payParams': pay_params, - 'dingdanid': dingdanid - }) - - except Exception as e: - logger.error(f"考核支付下单异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'message': '服务器内部错误'}) - - def generate_wechat_pay_params(self, dingdanid, jine, openid, attach, pay_type): - """生成微信JSAPI支付参数""" - APPID = settings.WEIXIN_APPID - MCHID = settings.WEIXIN_MCHID - KEY = settings.WEIXIN_SHANGHUMIYAO - - if pay_type == 'kaohe': - PAY_DESCRIPTION = settings.KAOHE_PAY_DESCRIPTION - NOTIFY_URL = settings.KAOHE_PAY_NOTIFY_URL - else: - raise Exception('未知支付类型') - - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - total_fee = int(jine * 100) - trade_type = 'JSAPI' - user_ip = self.get_client_ip() - - logger.info(f"微信支付下单参数: out_trade_no={dingdanid}, total_fee={total_fee}, openid={openid}, ip={user_ip}") - - # 第一次签名(统一下单) - sign_data = { - 'appid': APPID, - 'mch_id': MCHID, - 'nonce_str': nonce_str, - 'body': PAY_DESCRIPTION, - 'out_trade_no': dingdanid, - 'total_fee': total_fee, - 'spbill_create_ip': user_ip, - 'notify_url': NOTIFY_URL, - 'trade_type': trade_type, - 'openid': openid, - 'attach': attach, - } - - sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) - sign_string += f'&key={KEY}' - sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - # 构建XML请求体 - xml_data = f""" -{APPID} -{PAY_DESCRIPTION} -{MCHID} -{nonce_str} -{NOTIFY_URL} -{openid} -{dingdanid} -{user_ip} -{total_fee} -{trade_type} - -{sign} -""" - - logger.info(f"微信支付请求XML: {xml_data[:500]}...") # 只打印前500字符 - - response = requests.post( - 'https://api.mch.weixin.qq.com/pay/unifiedorder', - data=xml_data.encode('utf-8'), - headers={'Content-Type': 'application/xml'}, - timeout=10 - ) - - logger.info(f"微信支付响应: {response.text}") - - root = ET.fromstring(response.content) - - # 安全获取return_code - return_code_node = root.find('return_code') - if return_code_node is None or return_code_node.text is None: - logger.error(f"微信返回缺少return_code节点,原始内容: {response.text}") - raise Exception('微信支付接口异常,缺少return_code') - return_code = return_code_node.text - - # 安全获取result_code - result_code_node = root.find('result_code') - if result_code_node is None or result_code_node.text is None: - logger.error(f"微信返回缺少result_code节点,原始内容: {response.text}") - raise Exception('微信支付接口异常,缺少result_code') - result_code = result_code_node.text - - if return_code == 'SUCCESS' and result_code == 'SUCCESS': - prepay_id_node = root.find('prepay_id') - if prepay_id_node is None or prepay_id_node.text is None: - logger.error(f"统一下单成功但未返回prepay_id,原始内容: {response.text}") - raise Exception('微信支付接口异常,缺少prepay_id') - prepay_id = prepay_id_node.text - - timestamp = str(int(time.time())) - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - package = f'prepay_id={prepay_id}' - sign_type = 'MD5' - - # 第二次签名(前端调起支付用) - pay_sign_data = { - 'appId': APPID, - 'timeStamp': timestamp, - 'nonceStr': nonce_str, - 'package': package, - 'signType': sign_type, - } - sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) - pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) - pay_sign_string += f'&key={KEY}' - pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() - - logger.info(f"微信支付预下单成功,prepay_id={prepay_id}") - - return { - 'appId': APPID, - 'timeStamp': timestamp, - 'nonceStr': nonce_str, - 'package': package, - 'signType': sign_type, - 'paySign': pay_sign, - } - else: - # 安全获取错误信息 - err_code_node = root.find('err_code') - err_code = err_code_node.text if err_code_node is not None else '未知错误码' - err_code_des_node = root.find('err_code_des') - err_code_des = err_code_des_node.text if err_code_des_node is not None else '未知错误描述' - error_details = f"return_code: {return_code}, result_code: {result_code}, err_code: {err_code}, err_code_des: {err_code_des}" - logger.error(f"微信支付下单失败: {error_details}") - raise Exception(f"微信支付下单失败: {error_details}") - - def get_client_ip(self): - """获取客户端IP""" - request = self.request - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR', '127.0.0.1') - return ip if ip else '127.0.0.1' - - -class KaohePayCallbackView(View): - """ - 考核支付回调接口 - 路径:/yonghu/kh_huitiao - """ - - def post(self, request): - try: - request_body = request.body - logger.info(f"收到考核支付回调: {request_body.decode('utf-8')}") - - root = ET.fromstring(request_body) - - # 安全获取return_code - return_code_node = root.find('return_code') - if return_code_node is None or return_code_node.text is None: - logger.error(f"微信回调XML缺少return_code节点,原始内容: {request_body.decode('utf-8')}") - return self.wechat_response('FAIL', '数据格式错误') - return_code = return_code_node.text - - # 安全获取result_code - result_code_node = root.find('result_code') - if result_code_node is None or result_code_node.text is None: - logger.error(f"微信回调XML缺少result_code节点,原始内容: {request_body.decode('utf-8')}") - return self.wechat_response('FAIL', '数据格式错误') - result_code = result_code_node.text - - # 安全获取out_trade_no - out_trade_no_node = root.find('out_trade_no') - if out_trade_no_node is None or out_trade_no_node.text is None: - logger.error(f"微信回调XML缺少out_trade_no节点,原始内容: {request_body.decode('utf-8')}") - return self.wechat_response('FAIL', '数据格式错误') - out_trade_no = out_trade_no_node.text - - # 安全获取transaction_id - transaction_id_node = root.find('transaction_id') - if transaction_id_node is None or transaction_id_node.text is None: - logger.error(f"微信回调XML缺少transaction_id节点,原始内容: {request_body.decode('utf-8')}") - return self.wechat_response('FAIL', '数据格式错误') - transaction_id = transaction_id_node.text - - # 安全获取total_fee - total_fee_node = root.find('total_fee') - if total_fee_node is None or total_fee_node.text is None: - logger.error(f"微信回调XML缺少total_fee节点,原始内容: {request_body.decode('utf-8')}") - return self.wechat_response('FAIL', '数据格式错误') - total_fee = int(total_fee_node.text) - - logger.info( - f"回调参数解析成功: out_trade_no={out_trade_no}, transaction_id={transaction_id}, total_fee={total_fee}") - - if return_code != 'SUCCESS' or result_code != 'SUCCESS': - logger.warning(f"支付状态异常: return_code={return_code}, result_code={result_code}") - return self.wechat_response('FAIL', '支付失败') - - # 验证签名 - if not self.verify_signature(request_body): - logger.error(f"签名验证失败,订单号: {out_trade_no}") - return self.wechat_response('FAIL', '签名验证失败') - logger.info(f"签名验证成功,订单号: {out_trade_no}") - - # 查找订单 - try: - order = Czjilu.objects.get(dingdan_id=out_trade_no, leixing=5) - logger.info(f"找到订单: {out_trade_no}, 当前状态: {order.zhuangtai}") - except Czjilu.DoesNotExist: - logger.error(f"考核订单不存在: {out_trade_no}") - return self.wechat_response('FAIL', '订单不存在') - - if order.zhuangtai != 9: - logger.warning(f"订单状态非未支付: {order.zhuangtai}, 订单号: {out_trade_no}") - # 已支付的订单直接返回成功,避免微信重复回调 - if order.zhuangtai == 3: - logger.info(f"订单已支付,直接返回成功: {out_trade_no}") - return self.wechat_response('SUCCESS', 'OK') - return self.wechat_response('FAIL', '订单状态异常') - - # 验证金额 - wechat_yuan = total_fee / 100 - if abs(wechat_yuan - float(order.jine)) > 0.01: - logger.error(f"金额不一致: 订单金额={order.jine}, 回调金额={wechat_yuan}, 订单号: {out_trade_no}") - return self.wechat_response('FAIL', '金额不一致') - logger.info(f"金额验证通过: {wechat_yuan}元") - - # 更新订单状态为已支付 - order.zhuangtai = 3 - order.save() - logger.info(f"订单状态更新为已支付: {out_trade_no}") - - # 从临时表获取业务参数并创建审核记录 - try: - from dengji.models import KaohePayTemp - from dengji.utils import create_shenhe_jilu_from_temp - from yonghu.models import UserMain - - temp = KaohePayTemp.objects.get(dingdan_id=out_trade_no, yonghuid=order.yonghuid) - logger.info(f"找到临时表记录: {out_trade_no}, 用户ID: {order.yonghuid}") - - user = UserMain.objects.get(yonghuid=order.yonghuid) - logger.info(f"获取用户信息成功: {user.yonghuid}") - - success, msg, record = create_shenhe_jilu_from_temp(user, temp) - if success: - logger.info(f"创建审核记录成功: jilu_id={record.jilu_id}, 订单号: {out_trade_no}") - else: - logger.error(f"创建审核记录失败: {msg}, 订单号: {out_trade_no}") - # 订单已支付但记录创建失败,需要标记订单异常,人工介入 - order.zhuangtai = 10 # 10表示支付成功但业务处理失败 - order.save() - logger.error(f"订单状态标记为异常(10): {out_trade_no}") - - except KaohePayTemp.DoesNotExist: - logger.error(f"临时表无记录,订单号: {out_trade_no}, 用户ID: {order.yonghuid}") - # 尝试从attach解析(兼容旧数据) - attach_element = root.find('attach') - if attach_element is not None and attach_element.text: - try: - attach_data = json.loads(attach_element.text) - logger.info(f"从attach解析数据成功: {attach_data}") - # 这里可以保留旧逻辑,但建议只记录日志 - except Exception as e: - logger.error(f"解析attach失败: {str(e)}") - except Exception as e: - logger.error(f"处理临时表或创建审核记录异常: {str(e)}", exc_info=True) - # 标记订单异常 - order.zhuangtai = 10 - order.save() - logger.error(f"订单状态标记为异常(10): {out_trade_no}") - - # 更新收支记录 - try: - from peizhi.models import Szjilu - from decimal import Decimal - from dingdan.utils import update_daily_income - - jine_decimal = Decimal(str(order.jine)) - update_daily_income(jine_decimal) - logger.info(f"更新每日收入成功: {jine_decimal}元") - - szjilu, created = Szjilu.objects.get_or_create( - id=1, - defaults={ - 'zongsy': Decimal('0.00'), - 'zongls': Decimal('0.00'), - 'zongzc': Decimal('0.00'), - 'jrzc': Decimal('0.00'), - 'jrls': Decimal('0.00'), - } - ) - szjilu.zongsy += jine_decimal - szjilu.zongls += jine_decimal - szjilu.jrls += jine_decimal - szjilu.save() - logger.info(f"更新收支记录成功: zongsy={szjilu.zongsy}") - except Exception as e: - logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) - - logger.info(f"考核支付回调处理完成,订单号: {out_trade_no}") - return self.wechat_response('SUCCESS', 'OK') - - except ET.ParseError as e: - logger.error(f"XML解析失败: {str(e)}, 原始数据: {request.body.decode('utf-8')}") - return self.wechat_response('FAIL', '数据格式错误') - except Exception as e: - logger.error(f"考核支付回调异常: {str(e)}", exc_info=True) - return self.wechat_response('FAIL', '处理异常') - - def verify_signature(self, xml_data): - """验证微信支付签名""" - try: - root = ET.fromstring(xml_data) - sign_node = root.find('sign') - if sign_node is None or sign_node.text is None: - logger.error("微信回调XML缺少sign节点") - return False - sign = sign_node.text - - params = {} - for child in root: - if child.tag != 'sign' and child.text is not None: - params[child.tag] = child.text - - sorted_params = sorted(params.items(), key=lambda x: x[0]) - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' - calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - result = sign == calculated_sign - if not result: - logger.error(f"签名验证失败: 原签名={sign}, 计算签名={calculated_sign}") - return result - except Exception as e: - logger.error(f"签名验证异常: {str(e)}", exc_info=True) - return False - - def wechat_response(self, return_code, return_msg): - """构造微信回调响应XML""" - xml = f""" - - -""" - logger.info(f"返回微信响应: {xml}") - return HttpResponse(xml, content_type='application/xml') - - - - - - - -class KaohePayPollView(APIView): - """考核支付轮询""" - permission_classes = [IsAuthenticated] - - def post(self, request): - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({'code': 400, 'message': '订单ID不能为空'}) - try: - order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5) - if order.zhuangtai == 3: - return Response({'code': 200, 'message': '支付成功'}) - return Response({'code': 400, 'message': '订单尚未支付'}) - except Czjilu.DoesNotExist: - return Response({'code': 400, 'message': '订单不存在'}) - - -class KaohePayFailView(APIView): - """考核支付失败处理""" - permission_classes = [IsAuthenticated] - - def post(self, request): - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({'code': 400, 'message': '订单ID不能为空'}) - try: - order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5) - if order.zhuangtai == 9: - order.delete() - return Response({'code': 200, 'message': '订单已删除'}) - return Response({'code': 400, 'message': '订单状态无法删除'}) - except Czjilu.DoesNotExist: - return Response({'code': 400, 'message': '订单不存在'}) - - - - - - - - -class TixianAssetView(APIView): - """ - 获取用户所有可提现资产及费率 - 请求方式:POST - 认证:JWT Token - 返回: - { - "code": 200, - "msg": "success", - "data": { - "dashou_yue": "123.45", - "dashou_yajin": "500.00", - "shangjia_yue": "0.00", - "guanshi_fenyong": "67.89", - "zuzhang_fenyong": "0.00", - "kaoheguan_fenyong": "0.00", - "dashou_rate": 0.08, - "dashou_yajin_rate": 0.05, - "shangjia_rate": 0.06, - "guanshi_rate": 0.08, - "zuzhang_rate": 0.05, - "kaoheguan_rate": 0.07 - } - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - user = request.user - # 初始化返回数据,默认全部为0 - data = { - 'dashou_yue': '0.00', - 'dashou_yajin': '0.00', - 'shangjia_yue': '0.00', - 'guanshi_fenyong': '0.00', - 'zuzhang_fenyong': '0.00', - 'kaoheguan_fenyong': '0.00', - 'dashou_rate': 0.00, - 'dashou_yajin_rate': 0.00, - 'shangjia_rate': 0.00, - 'guanshi_rate': 0.00, - 'zuzhang_rate': 0.00, - 'kaoheguan_rate': 0.00, - } - - # ------------------- 打手资产 ------------------- - try: - dashou = user.dashou_profile - data['dashou_yue'] = str(dashou.yue) - data['dashou_yajin'] = str(dashou.yajin) - except ObjectDoesNotExist: - pass - - # ------------------- 商家资产 ------------------- - try: - shangjia = user.shop_profile - data['shangjia_yue'] = str(shangjia.yue) - except ObjectDoesNotExist: - pass - - # ------------------- 管事资产 ------------------- - try: - guanshi = user.guanshi_profile - data['guanshi_fenyong'] = str(guanshi.yue) - except ObjectDoesNotExist: - pass - - # ------------------- 组长资产 ------------------- - try: - zuzhang = user.zuzhang_profile - data['zuzhang_fenyong'] = str(zuzhang.ketixian_jine) - except ObjectDoesNotExist: - pass - - # ------------------- 审核官(考核官)资产 ------------------- - try: - kaoheguan = user.shenheguan_profile - data['kaoheguan_fenyong'] = str(kaoheguan.yue) - except ObjectDoesNotExist: - pass - - # ------------------- 费率查询(从 Lilubiao 获取) ------------------- - # 打手提现费率 (类型 '5') - lilu_obj = Lilubiao.objects.filter(fadanpingtai='5').first() - data['dashou_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 - - # 打手保证金提现费率 (类型 '11') - lilu_obj = Lilubiao.objects.filter(fadanpingtai='11').first() - data['dashou_yajin_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 - - # 商家提现费率 (类型 '10') - lilu_obj = Lilubiao.objects.filter(fadanpingtai='10').first() - data['shangjia_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 - - # 管事提现费率 (类型 '6') - lilu_obj = Lilubiao.objects.filter(fadanpingtai='6').first() - data['guanshi_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 - - # 组长提现费率 (类型 '8') - lilu_obj = Lilubiao.objects.filter(fadanpingtai='8').first() - data['zuzhang_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 - - # 审核官(考核官)提现费率 (类型 '9') - lilu_obj = Lilubiao.objects.filter(fadanpingtai='9').first() - data['kaoheguan_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 - - return Response({ - 'code': 200, - 'msg': '获取成功', - 'data': data - }, status=status.HTTP_200_OK) - - - - - -class GuanshiContactView(APIView): - """ - 管事获取自己的邀请人(组长)信息 - 路径: /api/yonghu/guanshi_contact - 方法: POST - 权限: JWT认证 - 请求体: 无(仅需携带合法token) - 成功返回: - code: 200, - data: { - uid: "1234567", // 组长用户ID - nicheng: "张三", // 组长昵称 - touxiang: "avatar/xxx.jpg" // 组长头像相对路径(前端拼接完整URL) - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 获取当前登录用户(必须已通过JWT认证) - current_user = request.user # 类型: UserMain - - # 2. 校验是否为管事身份(存在管事扩展表记录) - try: - guanshi_profile = current_user.guanshi_profile - except ObjectDoesNotExist: - return Response({ - 'code': 403, - 'msg': '您还不是管事,无法获取组长信息', - 'data': None - }) - - # 3. 获取邀请人ID(管事扩展表中的 yaoqingren 字段存储的是邀请人的 yonghuid) - inviter_yonghuid = guanshi_profile.yaoqingren - if not inviter_yonghuid: - return Response({ - 'code': 404, - 'msg': '未找到邀请人信息', - 'data': None - }) - - # 4. 根据邀请人用户ID查询用户主表 - try: - inviter_user = UserMain.objects.get(yonghuid=inviter_yonghuid) - except UserMain.DoesNotExist: - logger.error(f"邀请人用户不存在: yonghuid={inviter_yonghuid}") - return Response({ - 'code': 404, - 'msg': '邀请人账号异常', - 'data': None - }) - - # 5. 获取邀请人昵称(从老板扩展表取,若不存在则给默认值) - try: - inviter_nicheng = inviter_user.boss_profile.nickname - except ObjectDoesNotExist: - inviter_nicheng = '组长' # 兜底昵称 - - # 6. 构造返回数据 - response_data = { - 'uid': inviter_user.yonghuid, - 'nicheng': inviter_nicheng, - 'touxiang': inviter_user.avatar or '' # 头像相对路径 - } - - return Response({ - 'code': 200, - 'msg': '获取成功', - 'data': response_data - }) - - - - - - - - - -import logging - -import requests -from django.db import transaction -from django.conf import settings -from django.core.cache import cache -from django.utils import timezone -from rest_framework.views import APIView -from rest_framework import permissions, status -from rest_framework.response import Response - - - - - - -logger = logging.getLogger(__name__) - -def generate_tixian_id(): - """生成唯一提现订单号:TX + 时间戳(13位) + 4位随机数""" - timestamp = str(int(time.time() * 1000)) - rand = ''.join(random.choices(string.digits, k=4)) - return f'TX{timestamp}{rand}' - - -class TixianShenqingV3View(APIView): - """ - 自动提现收款接口(新流程第二步) - POST /yonghu/tixiansq - 必须传 shenhe_danhao;审核状态须为 6=待收款;申请时已扣款,本接口不再扣款 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - shenhe_danhao = (request.data.get('shenhe_danhao') or '').strip() - if shenhe_danhao: - from .tixian_shenhe_views import process_audit_collect - return process_audit_collect(request) - return Response({ - 'code': 7, - 'msg': '缺少审核单号,请先提交提现申请(/yonghu/zddksh)并等待审核通过后再收款', - }) - - - - - - - -from utils.wechat_v3 import verify_wechat_sign, decrypt_callback_ciphertext - -class TixianCallbackV3View(APIView): - """ - 微信转账结果回调接口(最终状态通知) - 处理微信异步发送的转账成功/失败结果 - """ - permission_classes = [] # 无需认证,签名验证保证安全 - - def post(self, request): - headers = request.headers - body = request.body.decode('utf-8') - - # 1. 验证签名 - if not verify_wechat_sign(headers, body): - return Response({'code': 'FAIL', 'message': '签名验证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 解密数据 - try: - data = json.loads(body) - resource = data.get('resource') - if not resource: - return Response({'code': 'FAIL', 'message': 'resource不存在'}, status=status.HTTP_400_BAD_REQUEST) - - associated_data = resource.get('associated_data', '') - nonce = resource.get('nonce') - ciphertext = resource.get('ciphertext') - if not nonce or not ciphertext: - return Response({'code': 'FAIL', 'message': '解密字段缺失'}, status=status.HTTP_400_BAD_REQUEST) - - plaintext = decrypt_callback_ciphertext(associated_data, nonce, ciphertext) - callback_data = json.loads(plaintext) - except Exception as e: - logger.error(f"回调解密失败: {str(e)}", exc_info=True) - return Response({'code': 'FAIL', 'message': f'解密失败: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST) - - # 3. 获取关键信息 - out_bill_no = callback_data.get('out_bill_no') # 商户提现单号(即 tixian_id) - transfer_status = callback_data.get('transfer_status') # SUCCESS / FAIL - wechat_transfer_no = callback_data.get('transfer_no') # 微信转账单号 - fail_reason = callback_data.get('fail_reason', '') - - if not out_bill_no: - return Response({'code': 'FAIL', 'message': 'out_bill_no缺失'}, status=status.HTTP_400_BAD_REQUEST) - - # 4. 幂等处理:只更新状态为0(处理中)的记录,已最终状态不再处理 - with transaction.atomic(): - try: - auto_record = TixianAutoRecord.objects.select_for_update().get(tixian_id=out_bill_no) - except TixianAutoRecord.DoesNotExist: - # 记录不存在,视为已处理,返回成功 - return Response({'code': 'SUCCESS', 'message': 'ok'}) - - # 如果已经是最新状态,直接返回成功(幂等) - if auto_record.zhuangtai != 0: - return Response({'code': 'SUCCESS', 'message': 'already processed'}) - - if transfer_status == 'SUCCESS': - from .tixian_shenhe_services import mark_transfer_success - mark_transfer_success( - auto_record, - wechat_transfer_no=wechat_transfer_no, - with_accounting=True, - ) - - else: # transfer_status == 'FAIL' - auto_record.zhuangtai = 2 - auto_record.fail_reason = fail_reason or '微信转账失败' - auto_record.save(update_fields=['zhuangtai', 'fail_reason']) - - if not auto_record.shenhe_danhao: - logger.error('回调失败但打款记录无审核单号 bill=%s', out_bill_no) - else: - # 申请时已扣款,回调失败不退余额,恢复待收款(6)可重试 - from .models import TixianShenheJilu - from .tixian_shenhe_services import ( - release_collect_quota_for_record, - sync_audit_and_jilu_status, - ) - try: - audit = TixianShenheJilu.objects.select_for_update().get( - shenhe_danhao=auto_record.shenhe_danhao - ) - audit.tixian_auto_id = None - audit.fail_reason = auto_record.fail_reason - sync_audit_and_jilu_status(audit, 6, fail_reason=auto_record.fail_reason) - except TixianShenheJilu.DoesNotExist: - logger.warning(f'回调失败但审核记录不存在: {auto_record.shenhe_danhao}') - release_collect_quota_for_record(auto_record) - - return Response({'code': 'SUCCESS', 'message': 'ok'}) - - - -class TixianQueRenAutoView(APIView): - """ - 用户确认提现结果 - POST /yonghu/tixianqr - 新审核流程:打款记录带 shenhe_danhao 时,确认成功同步审核表+提现记录表为 2;取消不退款(申请已扣),恢复待收款 6 可重试 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - tixian_id = request.data.get('tixian_id') - result = request.data.get('result') # 1成功 0失败/取消 - - if not tixian_id or result is None: - return Response({'code': 1, 'msg': '缺少参数'}, status=status.HTTP_400_BAD_REQUEST) - - try: - result = int(result) - except ValueError: - return Response({'code': 2, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST) - - if result not in [0, 1]: - return Response({'code': 3, 'msg': 'result值无效'}, status=status.HTTP_400_BAD_REQUEST) - - user_main = request.user - - with transaction.atomic(): - try: - auto_record = TixianAutoRecord.objects.select_for_update().get( - tixian_id=tixian_id, - yonghuid=user_main.yonghuid, - ) - except TixianAutoRecord.DoesNotExist: - return Response({'code': 4, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND) - - if result == 1: - if auto_record.zhuangtai == 1: - return Response({'code': 0, 'msg': '提现成功', 'data': None}) - - if auto_record.zhuangtai != 0: - return Response({'code': 5, 'msg': '该提现已处理'}) - - from .tixian_shenhe_services import mark_transfer_success - mark_transfer_success(auto_record, with_accounting=True) - - return Response({'code': 0, 'msg': '提现成功', 'data': None}) - - # 用户取消或失败 - auto_record.zhuangtai = 2 - auto_record.fail_reason = '用户取消收款' - auto_record.save(update_fields=['zhuangtai', 'fail_reason']) - - if not auto_record.shenhe_danhao: - logger.error('确认取消但打款记录无审核单号 bill=%s', tixian_id) - return Response({'code': 5, 'msg': '该提现已处理'}) - - # 申请时已扣款,取消不退余额,审核单恢复 6 待收款,可再次点收款 - from .models import TixianShenheJilu - from .tixian_shenhe_services import ( - release_collect_quota_for_record, - sync_audit_and_jilu_status, - ) - try: - audit = TixianShenheJilu.objects.select_for_update().get( - shenhe_danhao=auto_record.shenhe_danhao, - ) - audit.tixian_auto_id = None - sync_audit_and_jilu_status( - audit, 6, - fail_reason='用户取消收款,可重新发起', - ) - except TixianShenheJilu.DoesNotExist: - logger.warning(f'确认取消但审核记录不存在: {auto_record.shenhe_danhao}') - release_collect_quota_for_record(auto_record) - - return Response({'code': 0, 'msg': '提现已取消', 'data': None}) - - except Exception as e: - logger.error(f'提现确认异常: {str(e)}', exc_info=True) - return Response({'code': 99, 'msg': '系统错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - +import os +import sys +import json +import re +import time +import uuid +import random +import string +import requests +import hashlib +import traceback +import decimal +import jwt +import ipaddress +import xmltodict +from datetime import datetime, timedelta +from decimal import Decimal +import defusedxml.ElementTree as ET + +from django.conf import settings +from django.db import models, transaction, IntegrityError, DatabaseError, connection +from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch +from django.core.cache import cache +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.core.exceptions import ObjectDoesNotExist +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from django.http import HttpResponse +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from django.contrib.auth.hashers import make_password + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import permissions, status +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.parsers import MultiPartParser, FormParser, JSONParser +from rest_framework.throttling import AnonRateThrottle +from rest_framework_simplejwt.tokens import RefreshToken +from rest_framework_simplejwt.authentication import JWTAuthentication + +from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss +from utils.fadan_utils import check_fadan_qiangdan_eligible +from utils.wechat_v3 import verify_wechat_sign, decrypt_callback_ciphertext + +from Django.utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode +from users.fadan_fenhong_utils import process_fadan_fenhong + +from orders.utils import ( + update_daily_payout, update_daily_dispatch_stat, + update_daily_income, settle_shangjia_order_guanshi_fenhong +) +from backend.utils import ( + update_dashou_daily_by_action, update_guanshi_daily_by_action, + update_shangjia_daily, update_zuzhang_daily_by_action, + verify_kefu_permission +) +from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan + +from .models import ( + UserMain, UserBoss, UserDashou, UserShangjia, UserGuanshi, + UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser, + TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu, + RankingRecord +) +from .tixian_shenhe_services import ( + load_audit_meta_map, refund_balance, sync_audit_and_jilu_status, + mark_transfer_success, release_collect_quota_for_record +) +from .tixian_shenhe_views import process_audit_collect + +from orders.models import ( + Dingdan, DingdanPingtai, DingdanShangjia, Lilubiao, + Chufajilu, Chufatupian, Tuikuanjilu, Dashoutupian, + PreSettlement, Fadan, FadanShensuTupian +) +from products.models import ( + ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu +) +from config.models import Qunpeizhi, Szjilu +from backend.models import ShangjiaRiTongji +from rank.models import ( + KaohePayTemp, Chenghao, ShenheJilu, + KaoheCishuFeiyong, YonghuChenghao +) +from users.models import UserShenheguan +from products.utils import update_shangpin_daily_stat +from shop.utils import update_dianpu_daily_stat +from orders.utils import create_shenhe_jilu_from_temp + +import logging +logger = logging.getLogger(__name__) + + + + + + + + +# 获取用户代理信息(用于更详细的追踪) +def huoquYonghuDaili(request): + """ + 获取用户浏览器/设备信息 + """ + user_agent = request.META.get('HTTP_USER_AGENT', '') + + # 这里可以添加更复杂的用户代理解析 + # 为简化,我们只返回原始字符串的前100个字符 + return user_agent[:100] if user_agent else '' + + + + + +def huoquKehuduanIP(request): + """ + 强化版:安全、兼容地获取客户端真实IP地址 + """ + ip = None + + # 1. 从标准头部获取IP链 + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + # 按逗号分割,取第一个(即最原始的客户端IP) + ip = x_forwarded_for.split(',')[0].strip() + + # 2. 如果上面没有,则用Nginx传递的X-Real-IP + if not ip: + ip = request.META.get('HTTP_X_REAL_IP') + + # 3. 最后降级到连接地址 + if not ip: + ip = request.META.get('REMOTE_ADDR') + + # ===== 核心:清洗和验证IP格式 ===== + if ip: + # 情况A: 处理IPv4映射的IPv6地址 (::ffff:192.168.1.1) + if ip.startswith('::ffff:'): + ip = ip[7:] + + # 情况B: 处理带端口的IPv4地址 (223.91.110.123:62417) + # 这是导致您记录“三段”的最可能原因! + # 如果IP字符串中有冒号,且最后一个冒号前的部分是IPv4,则分离端口。 + if ':' in ip and '.' in ip: + # 尝试按冒号分割,取第一部分 + possible_ip = ip.rsplit(':', 1)[0] # 从右边分割一次 + # 验证它是否是有效的IPv4地址 + try: + ipaddress.IPv4Address(possible_ip) # 如果通过验证,说明格式正确 + ip = possible_ip + except (ipaddress.AddressValueError, ValueError): + # 如果验证失败,说明可能不是IPv4带端口,保持原样或进行其他处理 + pass + + # 情况C: 验证最终结果是否为有效IPv4(可选,但推荐) + try: + # 此步骤会抛出异常,如果ip变量此时仍是“223.91.110.”这种格式 + ipaddress.IPv4Address(ip) + except (ipaddress.AddressValueError, ValueError): + # 记录一个警告日志,并将IP标记为无效或存为原始字符串 + print(f"警告:获取到无效或非标准IP地址格式: {ip}") + # 您可以选择在此处 return None 或存储原始字符串以供排查 + + return ip + + + + +class WechatMiniProgramLoginView(APIView): + """ + 微信小程序登录接口 + """ + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + + def post(self, request): + try: + code = request.data.get('code', '').strip() + if not code: + return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # 获取微信openid和unionid + wechat_data = self.get_wechat_openid(code) + if not wechat_data or 'openid' not in wechat_data: + error_msg = wechat_data.get('errmsg', '微信授权失败') + return Response({'code': 2, 'msg': f'微信登录失败: {error_msg}', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + openid = wechat_data['openid'] + session_key = wechat_data.get('session_key', '') + unionid = wechat_data.get('unionid', '') # 🆕【新增】获取unionid + + # 获取客户端真实IP + kehuduan_ip = self.huoquKehuduanIP(request) + + with transaction.atomic(): + # 创建或获取用户 + user_main, created = UserMain.objects.select_for_update().get_or_create( + openid=openid, + defaults={'yonghuid': self.shengchengYonghuID(), 'user_type': 'normal'} + ) + + # 🆕【新增】保存unionid + if unionid and unionid != user_main.unionid: + user_main.unionid = unionid + + cunchu_ip = kehuduan_ip + user_main.ip = cunchu_ip + user_main.last_login_time = timezone.now() + user_main.save() + + if created: + UserBoss.objects.create(user=user_main, nickname='板板大人') + + # 检查扩展表 + dashou_status = 0 + shangjia_status = 0 + guanshi_status = 0 + boss_nickname = '微信用户' + + try: + boss_profile = UserBoss.objects.get(user=user_main) + boss_nickname = boss_profile.nickname if boss_profile.nickname else '微信用户' + except UserBoss.DoesNotExist: + pass + + try: + UserDashou.objects.get(user=user_main) + dashou_status = 1 + except UserDashou.DoesNotExist: + dashou_status = None + + try: + UserShangjia.objects.get(user=user_main) + shangjia_status = 1 + except UserShangjia.DoesNotExist: + shangjia_status = None + + try: + UserGuanshi.objects.get(user=user_main) + guanshi_status = 1 + except UserGuanshi.DoesNotExist: + guanshi_status = None + + + try: + UserZuzhang.objects.get(user=user_main) + zuzhang_status = 1 + except UserZuzhang.DoesNotExist: + zuzhang_status = None + + + # 在检查 zuzhang 扩展表之后,插入以下代码 + try: + kg = UserShenheguan.objects.get(user=user_main) + kaoheguan_status = 1 if kg.zhuangtai == 1 else 0 + except UserShenheguan.DoesNotExist: + kaoheguan_status = 0 + + # 生成JWT token + refresh = RefreshToken.for_user(user_main) + token = str(refresh.access_token) + + # 计算订单数量 + order_counts = self.jisuanDingdanShuliang(user_main.yonghuid) + + # 获取群配置 + group_info = self.huoquQunPeizhi() + + # 返回数据 + response_data = { + 'token': token, + 'nicheng': boss_nickname, + 'uid': user_main.yonghuid, + 'touxiang': user_main.avatar or '', + 'shangjiastatus': shangjia_status, + 'dashoustatus': dashou_status, + 'guanshistatus': guanshi_status, + 'zuzhangstatus': zuzhang_status, + 'kaoheguanstatus': kaoheguan_status, + 'dingdantiaoshu': order_counts, + 'dashouqun': group_info.get('dashouqun', ''), + 'dashouqunid': group_info.get('dashouqunid', ''), + 'guanshiqun': group_info.get('guanshiqun', ''), + 'guanshiqunid': group_info.get('guanshiqunid', '') + } + + return Response({'code': 0, 'msg': '登录成功', 'data': response_data}) + + except Exception as e: + error_openid = locals().get('openid', 'N/A') + logger.error(f"微信登录异常 - openid: {error_openid}, 错误类型: {type(e).__name__}, 错误详情: {str(e)}", + exc_info=True) + + return Response({'code': 99, 'msg': '系统繁忙,请稍后重试', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def get_wechat_openid(self, code): + """ + 调用微信接口获取openid和unionid + """ + try: + appid = getattr(settings, 'WEIXIN_APPID', '') + secret = getattr(settings, 'WEIXIN_SECRET', '') + + if not appid or not secret: + raise ValueError('微信配置未设置') + + url = 'https://api.weixin.qq.com/sns/jscode2session' + params = {'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code'} + + response = requests.get(url, params=params, timeout=10) + result = response.json() + + if 'openid' in result: + return {'openid': result['openid'], 'session_key': result.get('session_key', ''), + 'unionid': result.get('unionid', '')} + else: + errcode = result.get('errcode', 'unknown') + errmsg = result.get('errmsg', '未知错误') + return {'errmsg': f'[{errcode}]{errmsg}'} + + except requests.exceptions.Timeout: + return {'errmsg': '请求微信接口超时'} + except Exception as e: + return {'errmsg': f'请求微信接口异常: {str(e)}'} + + def shengchengYonghuID(self): + """ + 生成7位数字用户ID + """ + for _ in range(10): + timestamp_part = str(int(time.time()))[-5:].zfill(5) + random_part = str(random.randint(0, 99)).zfill(2) + user_id = timestamp_part + random_part + + if len(user_id) == 7 and user_id.isdigit(): + if not UserMain.objects.filter(yonghuid=user_id).exists(): + return user_id + + raise Exception('生成用户ID失败') + + def huoquKehuduanIP(self, request): + """ + 获取客户端真实IP地址 + """ + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0].strip() + if ip: + return ip + + ip = request.META.get('REMOTE_ADDR', '') + return ip if ip else '0.0.0.0' + + def jisuanDingdanShuliang(self, yonghuid): + """ + 统计订单数量 + """ + try: + if not DingdanPingtai.objects.filter(laoban_id=yonghuid).exists(): + return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} + + dingdan_ids = DingdanPingtai.objects.filter(laoban_id=yonghuid).values_list('dingdan__id', flat=True) + + results = Dingdan.objects.filter(id__in=dingdan_ids).aggregate( + daifuwu=Count(Case(When(Q(zhuangtai=1) | Q(zhuangtai=7), then=1), output_field=IntegerField())), + fuwuzhong=Count(Case(When(zhuangtai=2, then=1), output_field=IntegerField())), + yiwancheng=Count(Case(When(zhuangtai=3, then=1), output_field=IntegerField())), + yituikuan=Count(Case(When(zhuangtai=5, then=1), output_field=IntegerField())) + ) + + return { + 'daifuwu': results['daifuwu'] or 0, + 'fuwuzhong': results['fuwuzhong'] or 0, + 'yiwancheng': results['yiwancheng'] or 0, + 'yituikuan': results['yituikuan'] or 0 + } + + except Exception: + return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} + + def huoquQunPeizhi(self): + """ + 获取群配置信息 + """ + try: + qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) + + result = { + 'dashouqun': '', + 'dashouqunid': '', + 'guanshiqun': '', + 'guanshiqunid': '' + } + + for config in qun_configs: + if config.id == 1: + result['dashouqun'] = config.neirong or '' + result['dashouqunid'] = config.qunid or '' + elif config.id == 2: + result['guanshiqun'] = config.neirong or '' + result['guanshiqunid'] = config.qunid or '' + + return result + + except Exception: + return { + 'dashouqun': '', + 'dashouqunid': '', + 'guanshiqun': '', + 'guanshiqunid': '' + } + +# views/weixin_official.py - 完整文件 +class WeixinOfficialCallbackView(APIView): + """ + 微信服务号回调接口 + 处理用户关注、取消关注等事件 + 核心:有关注事件就存,不管有没有UnionID + """ + throttle_classes = [] + permission_classes = [AllowAny] + authentication_classes = [] + + def get(self, request): + """验证服务器配置(微信官方要求)""" + try: + signature = request.GET.get('signature', '') + timestamp = request.GET.get('timestamp', '') + nonce = request.GET.get('nonce', '') + echostr = request.GET.get('echostr', '') + + token = getattr(settings, 'WEIXIN_OFFICIAL_TOKEN', '') + + if not token: + logger.error("服务号Token未配置") + return HttpResponse('Token not configured', status=500) + + # 验证签名 + tmp_list = sorted([token, timestamp, nonce]) + tmp_str = ''.join(tmp_list) + hash_str = hashlib.sha1(tmp_str.encode()).hexdigest() + + if hash_str == signature: + return HttpResponse(echostr) + else: + logger.error(f"签名验证失败") + return HttpResponse('Signature verification failed', status=403) + + except Exception as e: + logger.error(f"验证服务器异常: {str(e)}") + return HttpResponse('Internal Server Error', status=500) + + def post(self, request): + """处理事件推送(关注、取关)""" + try: + xml_str = request.body.decode('utf-8') + logger.info(f"收到服务号事件,原始数据(前200字符): {xml_str[:200]}") + + root = ET.fromstring(xml_str) + + msg_type = root.find('MsgType').text + from_user = root.find('FromUserName').text # 服务号OpenID + + if msg_type == 'event': + event = root.find('Event').text + + if event == 'subscribe': + return self.handle_subscribe_event(root, from_user) + elif event == 'unsubscribe': + return self.handle_unsubscribe_event(root, from_user) + else: + # 其他事件(如菜单点击)直接返回success + return HttpResponse('success') + + # 非事件消息也直接返回success + return HttpResponse('success') + + except Exception as e: + logger.error(f"处理服务号事件异常: {str(e)}", exc_info=True) + # 即使异常也返回success,避免微信服务器重试 + return HttpResponse('success') + + def handle_subscribe_event(self, root, from_user): + """处理用户关注事件 - 简化版:有就存,没有拉倒""" + try: + # 尝试获取UnionID(有就有,没有也没关系) + unionid_elem = root.find('UnionId') + unionid = unionid_elem.text if unionid_elem is not None else None + + logger.info(f"用户关注,OpenID: {from_user}, UnionID: {unionid}") + + # 核心逻辑:更新或创建记录 + obj, created = OfficialAccountUser.objects.update_or_create( + official_openid=from_user, + defaults={ + 'unionid': unionid, + 'is_subscribed': True, + 'subscribe_time': timezone.now() + } + ) + + action = "新建" if created else "更新" + logger.info(f"{action}用户记录成功: OpenID={from_user}") + + # 回复欢迎消息(可选) + return self.generate_text_response(root, "感谢关注星阙网络服务!平台有新订单时您会收到通知。") + + except Exception as e: + logger.error(f"处理关注事件异常: {str(e)}", exc_info=True) + # 依然返回success,不让微信重试 + return HttpResponse('success') + + def handle_unsubscribe_event(self, root, from_user): + """处理用户取消关注事件 - 简化版:标记为未订阅""" + try: + updated_count = OfficialAccountUser.objects.filter( + official_openid=from_user + ).update(is_subscribed=False) + + if updated_count > 0: + logger.info(f"用户取消关注,已标记为未订阅: {from_user}") + else: + logger.info(f"用户取消关注,但记录不存在(无需处理): {from_user}") + + return HttpResponse('success') + + except Exception as e: + logger.error(f"处理取消关注事件异常: {str(e)}", exc_info=True) + return HttpResponse('success') + + def generate_text_response(self, root, content): + """生成文本消息响应""" + try: + to_user = root.find('ToUserName').text + from_user = root.find('FromUserName').text + response_xml = f""" + + +{int(time.time())} + + +""" + return HttpResponse(response_xml, content_type='application/xml') + + except Exception as e: + logger.error(f"生成回复消息异常: {str(e)}") + # 即使生成回复失败,也返回一个空的success + return HttpResponse('success') + + + + +# yonghu/views.py + +class UserInfoUpdateView(APIView): + permission_classes = [permissions.IsAuthenticated] + + # 进程内缓存,防止同一code被重复调用微信(单进程安全,多进程小概率重复,但用户极少可忽略) + _phone_cache = {} + + def post(self, request): + try: + user = request.user + boss = self._get_or_create_boss(user) + result = {} + + # 头像 + av = request.FILES.get('avatar') + if av: + ok, msg = validate_image(av) + if not ok: + return Response({'code': 1, 'msg': msg, 'data': None}, status=400) + url = self._upload_avatar(user, av) + if url: + result['touxiang'] = url + user.avatar = url + else: + return Response({'code': 2, 'msg': '头像上传失败', 'data': None}, status=500) + + # 昵称 + nick = request.data.get('nicheng') + if nick: + nick = str(nick).strip() + if len(nick) > 20: + return Response({'code': 3, 'msg': '昵称过长', 'data': None}, status=400) + boss.nickname = nick + result['nicheng'] = nick + + # 手机号 —— 极简可靠版 + code = request.data.get('shoujihao_code') + if code: + phone = self._get_phone_safe(code) + if phone is None: + return Response({'code': 6, 'msg': '手机号获取失败,请重新授权', 'data': None}, status=400) + user.phone = phone + user.shoujihao_renzheng = True + result['phone'] = phone + + if not result: + return Response({'code': 5, 'msg': '无更新', 'data': None}, status=400) + + with transaction.atomic(): + user.save() + boss.save() + return Response({'code': 0, 'msg': '更新成功', 'data': result}) + + except Exception as e: + logger.exception("系统异常") + return Response({'code': 99, 'msg': str(e), 'data': None}, status=500) + + # ---------- 安全手机号(内存去重) ---------- + def _get_phone_safe(self, code): + # 如果已经处理过这个code,直接返回结果 + if code in self._phone_cache: + cached = self._phone_cache[code] + return cached if cached != '__FAIL__' else None + + # 标记处理中(简单防重入) + self._phone_cache[code] = '__PROCESSING__' + try: + phone = self._call_wechat(code) + self._phone_cache[code] = phone + return phone + except Exception as e: + logger.error(f"微信调用失败: {str(e)}") + self._phone_cache[code] = '__FAIL__' + return None + + # ---------- 微信接口调用(每次全新token) ---------- + def _call_wechat(self, code): + token = self._get_new_token() + url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}' + resp = requests.post(url, json={'code': code}, timeout=10) + data = resp.json() + errcode = data.get('errcode') + if errcode == 0: + return data['phone_info']['purePhoneNumber'] + # 即使刚拿的token也可能意外失效,重试一次 + if errcode == 40001: + token = self._get_new_token() + url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}' + resp = requests.post(url, json={'code': code}, timeout=10) + data = resp.json() + if data.get('errcode') == 0: + return data['phone_info']['purePhoneNumber'] + raise Exception(f"微信错误(errcode={errcode}): {data.get('errmsg')}") + + # ---------- 获取全新 access_token(不缓存,每次调微信) ---------- + def _get_new_token(self): + appid = getattr(settings, 'WEIXIN_APPID', '') + secret = getattr(settings, 'WEIXIN_SECRET', '') + if not appid or not secret: + raise Exception('缺少微信配置') + resp = requests.get('https://api.weixin.qq.com/cgi-bin/token', params={ + 'grant_type': 'client_credential', + 'appid': appid, + 'secret': secret + }) + data = resp.json() + token = data.get('access_token') + if token: + return token + raise Exception(f"获取access_token失败: {data}") + + # ---------- 工具 ---------- + def _get_or_create_boss(self, user): + try: + return UserBoss.objects.get(user=user) + except UserBoss.DoesNotExist: + return UserBoss.objects.create(user=user, nickname='微信用户') + + def _upload_avatar(self, user, file): + try: + uid = user.yonghuid + if not uid: return None + old = user.avatar + if old: delete_from_oss(old) + ext = os.path.splitext(file.name)[1] or '.jpg' + name = f"{uuid.uuid4().hex}{ext}" + path = f"avatar/{uid}/{name}" + return path if upload_to_oss(file, path) else None + except: + return None + + + +# yonghu/views.py 或 yonghu/api_views.py + +class DashouXinxiAPIView(APIView): + """ + 获取打手信息接口 + 请求方式:POST + 认证方式:JWT Token + 返回:打手基本信息、资产信息、会员信息,金牌打手额外返回专属字段 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取当前登录用户 + current_user = request.user + yonghuid = current_user.yonghuid + + # 2. 查询打手扩展表 + try: + dashou_profile = current_user.dashou_profile + except UserDashou.DoesNotExist: + return Response({ + 'code': 400, + 'message': '您不是打手身份', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 构建基础信息响应(原有字段完全保留) + data = { + 'dashounicheng': dashou_profile.nicheng or '', + 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, + 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, + 'dashouzhuangtai': dashou_profile.zhuangtai, + 'yongjin': str(dashou_profile.yue) if dashou_profile.yue is not None else '0.00', + 'zonge': str(dashou_profile.zonge) if dashou_profile.zonge is not None else '0.00', + 'yajin': str(dashou_profile.yajin) if dashou_profile.yajin is not None else '0.00', + 'jiedanZongliang':dashou_profile.jiedanzongliang if dashou_profile.jiedanzongliang is not None else '0', + 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00', + 'chengjiaoliang': dashou_profile.chengjiaozongliang, + 'jifen': dashou_profile.jifen, + 'chenghao': dashou_profile.chenghao or '', + } + + # 4. 查询会员信息(原有逻辑) + huiyuan_list = [] + try: + huiyuan_records = Huiyuangoumai.objects.filter( + yonghu_id=yonghuid + ).select_related() + + for record in huiyuan_records: + # 检查会员是否过期 + is_expired = record.jiance_shifou_daoqi() + + # 只返回未过期的会员(huiyuan_zhuangtai = 1) + if record.huiyuan_zhuangtai == 1: + huiyuan_list.append({ + 'huiyuanid': record.huiyuan_id, + 'huiyuanming': record.jieshao or f"会员{record.huiyuan_id}", + 'daoqi': record.daoqi_time + }) + except Exception as e: + # 会员信息查询失败不影响主要功能,返回空列表 + huiyuan_list = [] + + data['clumber'] = huiyuan_list + + # 5. 如果是金牌打手,添加专属字段 + if dashou_profile.chenghao == "金牌打手": + data.update({ + 'jiedanZongliang': dashou_profile.jiedanzongliang, # 接单总量 + 'jinriJiedan': str(dashou_profile.jinrishouyi) if dashou_profile.jinrishouyi is not None else '0.00', # 今日接单金额 + 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00', # 最近提现金额(今日已提现) + }) + + # 6. 返回成功响应 + return Response({ + 'code': 200, + 'message': '获取成功', + 'data': data + }, status=status.HTTP_200_OK) + + except Exception as e: + # 生产环境应使用 logging 记录错误,而不是 print + # logger.error(f"获取打手信息失败: {e}", exc_info=True) + return Response({ + 'code': 500, + 'message': f'服务器内部错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class HuoQuYaoQingRenView(APIView): + """ + 获取当前打手的邀请人信息 + POST /yonghu/hqwdyqr + Headers: Authorization: Bearer + 返回邀请人的用户ID、昵称(来自老板扩展表)、头像相对URL + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + current_user = request.user # 当前登录用户 + yonghuid = current_user.yonghuid # 7位用户ID + + # 1. 校验打手身份及账号状态(只检查账号状态,不检查接单状态) + try: + dashou = UserDashou.objects.get(user=current_user) + if dashou.zhanghaozhuangtai != 1: + return Response({ + 'code': 403, + 'message': '您的账号已被封禁,无法查看邀请人', + 'data': None + }) + except UserDashou.DoesNotExist: + return Response({ + 'code': 404, + 'message': '您还不是打手,无法查看邀请人', + 'data': None + }) + + # 2. 获取邀请人ID + yaoqingren_id = dashou.yaoqingren + if not yaoqingren_id: + return Response({ + 'code': 404, + 'message': '您没有邀请人', + 'data': None + }) + + # 3. 查询邀请人主表(头像) + try: + inviter_main = UserMain.objects.get(yonghuid=yaoqingren_id) + touxiang = inviter_main.avatar or '' + except UserMain.DoesNotExist: + return Response({ + 'code': 404, + 'message': '邀请人信息不存在', + 'data': None + }) + + # 4. 查询老板扩展表(昵称),如果没有则使用空字符串 + nicheng = '' + try: + boss = UserBoss.objects.get(user=inviter_main) + nicheng = boss.nickname or '' + except UserBoss.DoesNotExist: + pass # 邀请人可能不是老板,昵称留空 + + response_data = { + 'uid': yaoqingren_id, + 'nicheng': nicheng, + 'touxiang': touxiang # 相对路径,前端自行拼接 + } + + return Response({ + 'code': 200, + 'message': '获取成功', + 'data': response_data + }) + + except Exception as e: + logger.error(f'获取邀请人失败: {str(e)}', exc_info=True) + return Response({ + 'code': 500, + 'message': '系统繁忙,请稍后重试', + 'data': None + }) + + + + +# yonghu/views.py 或 yonghu/api_views.py + +class ZaixianZhuangtaiAPIView(APIView): + """ + 更改打手在线状态接口 + 请求方式:POST + 认证方式:JWT Token + 请求参数:status(0=离线,1=在线) + 返回:更新后的状态信息 + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取请求参数 + new_status = request.data.get('status') + + if new_status is None: + return Response({ + 'code': 400, + 'message': '缺少状态参数', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 转换为整数,确保是0或1 + try: + new_status = int(new_status) + if new_status not in [0, 1]: + return Response({ + 'code': 400, + 'message': '状态参数必须是0或1', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + except ValueError: + return Response({ + 'code': 400, + 'message': '状态参数必须是整数', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 获取当前登录用户 + current_user = request.user + yonghuid = current_user.yonghuid + + + + # 3. 查询打手扩展表 + try: + dashou_profile = current_user.dashou_profile + except UserDashou.DoesNotExist: + + return Response({ + 'code': 400, + 'message': '您不是打手身份', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 检查账号状态(是否被封禁) + if dashou_profile.zhanghaozhuangtai != 1: + + return Response({ + 'code': 403, + 'message': '账号状态异常,无法更改在线状态', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 5. 更新在线状态 + old_status = dashou_profile.zaixianzhuangtai + + # 如果状态没有变化,直接返回成功 + if old_status == new_status: + + return Response({ + 'code': 200, + 'message': '状态未变化', + 'data': { + 'zaixianzhuangtai': new_status, + 'status_text': '在线' if new_status == 1 else '离线' + } + }, status=status.HTTP_200_OK) + + # 更新状态 + dashou_profile.zaixianzhuangtai = new_status + dashou_profile.save() + + + + # 6. 返回成功响应 + return Response({ + 'code': 200, + 'message': '状态更新成功', + 'data': { + 'zaixianzhuangtai': new_status, + 'status_text': '在线' if new_status == 1 else '离线' + } + }, status=status.HTTP_200_OK) + + except Exception as e: + + return Response({ + 'code': 500, + 'message': f'服务器内部错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class GuanshiXinxiView(APIView): + """ + 获取管事信息接口 + 路径:/yonghu/guanshixinxi + 方法:POST + 权限:JWT认证 + 功能:获取当前管事的详细信息,包括基本信息、分红信息以及已充值打手数量 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 获取当前用户(通过JWT认证) + current_user = request.user + + # 查询管事扩展表 + try: + guanshi_profile = UserGuanshi.objects.get(user=current_user) + + # 🔥【新增】查询已充值打手数量(从分红表中统计) + # Gsfenhong 表中 guanshi 字段存储管事ID(yonghuid) + yichongzhi_count = Gsfenhong.objects.filter(guanshi=current_user.yonghuid).count() + + # 返回管事信息 + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'gszhstatus': guanshi_profile.zhuangtai, # 管事账号状态 + 'yaoqingzongshu': guanshi_profile.yaogingshuliang, # 邀请打手总数 + 'fenyongzonge': float(guanshi_profile.chongzhifenrun), # 充值分佣总额 + 'fenyongtixian': float(guanshi_profile.yue), # 可提现余额 + 'yichongzhiDashou': yichongzhi_count, # 已充值打手数量 + } + }) + + except UserGuanshi.DoesNotExist: + # 用户不是管事,返回错误 + return Response({ + 'code': 400, + 'msg': '您还不是管事,请先注册', + 'data': None + }, status=400) + + except Exception as e: + # 捕获所有异常,返回错误信息并记录日志 + logger.error(f"获取管事信息异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'msg': f'获取信息失败: {str(e)}', + 'data': None + }, status=500) + + + + +# yonghu/views.py +class ShangJiaXinXiView(APIView): + """ + 获取商家信息接口 (POST /yonghu/shangjiaxinxi) + 权限:需JWT认证 + 返回:商家基本信息 + 称号列表 + 今日/本月统计 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 获取商家扩展资料 + shangjia = request.user.shop_profile + except ObjectDoesNotExist: + return Response({ + 'code': 4001, + 'msg': '您还未注册成为商家或商家资料不存在', + 'data': None + }, status=status.HTTP_200_OK) + + user = request.user + yonghuid = user.yonghuid + today = timezone.now().date() + this_month_start = today.replace(day=1) + + # ------------------- 1. 从 UserShangjia 获取基础字段 ------------------- + response_data = { + 'sjzhzhuangtai': shangjia.zhuangtai, # 商家账号状态 + 'fadanzong': shangjia.fabu, # 发单总量(累计) + 'tuikuanzong': shangjia.tuikuan, # 退款总量(累计) + 'sjyue': float(shangjia.yue), # 商家余额 + # 'riliushui' 和 'yueliushui' 将从统计表计算,不再使用原字段 + } + + # ------------------- 2. 获取称号列表(仅限商家称号) ------------------- + chenghao_list = [] + # 查询当前用户的所有称号关联 + yonghu_chenghao_qs = YonghuChenghao.objects.filter(yonghu=user).select_related('chenghao') + for yc in yonghu_chenghao_qs: + chenghao_obj = yc.chenghao + # 只保留类型为“商家”的称号 + if chenghao_obj.leixing == 'shangjia': + chenghao_list.append({ + 'mingcheng': chenghao_obj.mingcheng, + 'texiao_json': chenghao_obj.texiao_miaoshu, # 前端需要的特效描述 + # 如果还需要其他字段(如图标URL)可在此添加,打手端有 texiao_json + }) + response_data['chenghao_list'] = chenghao_list + + # ------------------- 3. 从 ShangjiaRiTongji 获取今日统计 ------------------- + try: + today_stat = ShangjiaRiTongji.objects.get(yonghuid=yonghuid, riqi=today) + jinri_paifa_dingdan = today_stat.paifa_dingdan_shu + jinri_paifa_jine = float(today_stat.paifa_jine) + jinri_tuikuan_dingdan = today_stat.tuikuan_dingdan_shu + except ShangjiaRiTongji.DoesNotExist: + jinri_paifa_dingdan = 0 + jinri_paifa_jine = 0.00 + jinri_tuikuan_dingdan = 0 + + response_data['jinripaidan'] = jinri_paifa_dingdan # 今日派单数(新增) + response_data['jinrituikuan'] = jinri_tuikuan_dingdan # 今日退款数(新增) + response_data['riliushui'] = jinri_paifa_jine # 今日流水(派发总金额) + + # ------------------- 4. 从 ShangjiaRiTongji 获取本月统计 ------------------- + month_stats = ShangjiaRiTongji.objects.filter( + yonghuid=yonghuid, + riqi__gte=this_month_start, + riqi__lte=today + ).aggregate( + total_paifa_jine=Sum('paifa_jine'), + total_paifa_dingdan=Sum('paifa_dingdan_shu') + ) + yue_paifa_jine = float(month_stats.get('total_paifa_jine') or 0.00) + # yue_paifa_dingdan = month_stats.get('total_paifa_dingdan') or 0 # 如果前端不需要可不加 + + response_data['yueliushui'] = yue_paifa_jine # 今月流水(派发总金额) + + # (可选)如果前端还需要今月派单总数,取消下面注释 + # response_data['jinyuepaidan'] = yue_paifa_dingdan + + return Response({ + 'code': 200, + 'msg': '获取商家信息成功', + 'data': response_data + }, status=status.HTTP_200_OK) + + + + + + + + + + + + +class BossBiaoshiView(APIView): + """ + 获取用户的GoEasy标识 - 极简实现 + POST /yonghu/bossbiaoshi + 需要JWT Token认证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 直接从request.user获取用户 + user = request.user + + # 2. 获取yonghuid(确保用户已登录) + if not user or not hasattr(user, 'yonghuid'): + return Response({ + 'code': 401, + 'msg': '用户未认证或用户信息不完整' + }, status=401) + + # 3. 生成标识:Boss + yonghuid(与前端约定) + boss_biaoshi = f"Boss{user.yonghuid}" + + # 4. 返回给前端 + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'boss_biaoshi': boss_biaoshi, + 'yonghuid': user.yonghuid, + 'user_type': user.user_type + } + }) + + +""" +阿龙电竞陪玩平台 - 邀请码相关API接口 +""" + +class YaoqingmaView(APIView): + """ + 获取或生成管事邀请码 + 访问路径: /api/yonghu/yaoqingma/ + 请求方法: GET + 权限要求: JWT Token认证通过的用户 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + """ + 获取当前用户的邀请码 + 逻辑: + 1. 通过JWT获取当前用户 (request.user) + 2. 反向查询UserGuanshi扩展表 + 3. 验证管事状态(zhuangtai=1) + 4. 已有邀请码直接返回,没有则生成并保存 + """ + # 获取当前认证用户 + current_user = request.user + + # 1. 使用反向查询获取管事扩展信息 (性能最优方式) + try: + guanshi_profile = current_user.guanshi_profile + except UserGuanshi.DoesNotExist: + # 用户不是管事,没有扩展表记录 + return Response({ + 'code': 403, + 'message': '当前用户不是管事身份,无法生成邀请码', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 2. 验证管事状态 (zhuangtai 必须等于 1) + if guanshi_profile.zhuangtai != 1: + return Response({ + 'code': 403, + 'message': '管事账号状态异常,无法生成邀请码', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 3. 检查是否已存在邀请码 + existing_yaoqingma = guanshi_profile.yaoqingma + + # 如果存在且非空,直接返回 + if existing_yaoqingma and existing_yaoqingma.strip() != '': + return Response({ + 'code': 200, + 'message': '成功获取现有邀请码', + 'data': { + 'yaoqingma': existing_yaoqingma, + 'is_new': False # 标记是否为新生成的 + } + }) + + # 4. 生成新的邀请码 (使用用户ID字符串) + # 注意: 这里使用yonghuid,不是主键id,如你要求的"123456"这种形式 + yonghuid_str = str(current_user.yonghuid) + new_yaoqingma = CreateInvitationCode(yonghuid_str) + + # 5. 验证生成的邀请码格式 + if not VerifyInvitationCode(new_yaoqingma): + return Response({ + 'code': 500, + 'message': '邀请码生成失败,格式验证错误', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 6. 保存到数据库 (使用事务确保数据一致性) + try: + with transaction.atomic(): + guanshi_profile.yaoqingma = new_yaoqingma + guanshi_profile.save() + except Exception as e: + # 这里可以记录日志 + return Response({ + 'code': 500, + 'message': f'保存邀请码到数据库失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 7. 返回新生成的邀请码 + return Response({ + 'code': 200, + 'message': '成功生成新的邀请码', + 'data': { + 'yaoqingma': new_yaoqingma, + 'is_new': True # 标记是新生成的 + } + }) + + + +""" +阿龙电竞陪玩平台 - 打手注册及相关API接口 +""" + + +class DashouZhuceView(APIView): + """ + 打手注册接口 (通过管事邀请码) + 访问路径: /api/yonghu/dashouzhuce/ + 请求方法: POST + 权限要求: JWT Token认证通过的用户 + 请求体: { "inviteCode": "AbC123..." } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取并验证邀请码参数 + yaoqingma = request.data.get('inviteCode') + if not yaoqingma: + return Response({ + 'code': 400, + 'message': '邀请码不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + yaoqingma = yaoqingma.strip() + if len(yaoqingma) > 100: + return Response({ + 'code': 400, + 'message': '邀请码长度超过限制', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + current_user = request.user + # 注意:这里request.user是UserMain实例 + + # 2. 检查用户是否已是打手 (使用反向查询,性能最优) + try: + dashou_profile = current_user.dashou_profile + # 用户已是打手,直接返回现有信息 + return self.fanhuiXianyouDashouXinxi(dashou_profile, current_user) + except UserDashou.DoesNotExist: + # 用户不是打手,继续注册流程 + pass + + # 3. 根据邀请码查找对应的管事 + try: + # 使用select_related一次性获取管事及其关联的主表用户信息,性能最高 + guanshi_profile = UserGuanshi.objects.select_related('user').get(yaoqingma=yaoqingma) + except UserGuanshi.DoesNotExist: + return Response({ + 'code': 404, + 'message': '邀请码无效或不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 4. 验证管事状态 + + # 5. 核心:在数据库事务中创建用户的所有身份 + try: + with transaction.atomic(): + # 获取管事的用户ID (yonghuid) + guanshi_yonghuid = guanshi_profile.user.yonghuid + + # 5.1 创建打手扩展表 + dashou_profile = UserDashou.objects.create( + user=current_user, + nicheng='大手子', # 默认昵称 + chenghao='普通大手', # 默认称号 + yaoqingren=guanshi_yonghuid, # 设置邀请人ID + # 其他字段全部使用模型定义的默认值 + ) + + # 5.2 创建商家扩展表 + try: + boss_profile = current_user.boss_profile + if boss_profile.nickname: + boss_nickname = boss_profile.nickname + except: + pass + + + # 5.3 创建管事扩展表 + + # 5.4 更新主表的用户类型 (如果需要,可以设为'dashou',或保持原逻辑) + # current_user.user_type = 'dashou' # 根据你的业务逻辑决定 + # current_user.save() + + # 5.5 更新原管事的邀请数量 (使用行锁防止并发问题) + # 重新查询并锁定该行记录 + locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk) + locked_guanshi.yaogingshuliang += 1 + locked_guanshi.save() + + # ========== 新增:管事每日统计(邀请打手) ========== + try: + update_guanshi_daily_by_action( + yonghuid=guanshi_yonghuid, + action=1, # 1 = 邀请打手 + amount=Decimal('0.00') + ) + logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}") + except Exception as e: + logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}") + + except Exception as e: + + # 事务会自动回滚 + # 这里可以记录更详细的日志 + return Response({ + 'code': 500, + 'message': f'注册过程中发生系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 6. 注册成功,返回信息 (新注册用户没有会员记录,clumber返回空列表) + fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=True) + return Response({ + 'code': 200, + 'message': '注册成功!已开通打手、管事身份。', + 'data': fanhui_data + }) + + def fanhuiXianyouDashouXinxi(self, dashou_profile, current_user): + """处理已是打手的用户,返回其现有信息""" + fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=False) + return Response({ + 'code': 200, + 'message': '您已是打手,返回当前信息。', + 'data': fanhui_data + }) + + def zhuangbeiFanhuiShuju(self, dashou_profile, current_user, is_new=False): + """准备返回给前端的打手数据,字段名严格匹配前端""" + + + + # 构建基础信息 + data = { + 'dashounicheng': dashou_profile.nicheng, + 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, + 'yongjin': str(dashou_profile.yue), # Decimal转字符串 + 'zonge': str(dashou_profile.zonge), + 'yajin': str(dashou_profile.yajin), + 'chenghao': dashou_profile.chenghao, + 'jinfen': dashou_profile.jifen, + 'chengjiaoliang': dashou_profile.chengjiaozongliang, + 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, + 'dashouzhuangtai': dashou_profile.zhuangtai, + } + + # 查询并处理会员列表 (clumber) + # 使用当前用户的yonghuid进行查询 + huiyuan_goumai_list = [] + if not is_new: # 如果是新用户,直接返回空列表,无需查询 + goumai_records = Huiyuangoumai.objects.filter(yonghu_id=current_user.yonghuid) + for record in goumai_records: + # 调用方法检查是否到期,此方法内部会更新状态 + shifou_daoqi = record.jiance_shifou_daoqi() + huiyuan_goumai_list.append({ + 'huiyuan_id': record.huiyuan_id, + 'huiyuan_zhuangtai': record.huiyuan_zhuangtai, + 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None, + 'shifou_daoqi': shifou_daoqi, + 'jieshao': record.jieshao + }) + + data['clumber'] = huiyuan_goumai_list + return data + + + + + + +class GuanshiYaoqingDashouListView(APIView): + """ + 管事获取已邀请打手列表(支持筛选、搜索、分页) + POST /api/yonghu/gshqyqds/ + Headers: Authorization: Bearer + Body: + { + "page": 1, // 页码,从1开始 + "pageSize": 30, // 每页条数,最大100 + "keyword": "", // 搜索关键词(用户ID或昵称) + "zhanghaozhuangtai": 1, // 账号状态:1=正常,2=封禁(可选) + "zaixianzhuangtai": 1 // 在线状态:1=在线,2=离线(可选) + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + current_user = request.user + guanshi_yonghuid = current_user.yonghuid + + # 1. 分页参数 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('pageSize', 30)) + if page < 1 or page_size < 1: + return Response({'code': 400, 'message': '分页参数必须大于0'}, status=400) + if page_size > 100: + page_size = 100 + + # 2. 筛选参数 + keyword = request.data.get('keyword', '').strip() + zhanghaozhuangtai = request.data.get('zhanghaozhuangtai') # 可选:1=正常,2=封禁 + zaixianzhuangtai = request.data.get('zaixianzhuangtai') # 可选:1=在线,2=离线 + + # 3. 基础查询:当前管事邀请的所有打手 + base_qs = UserDashou.objects.select_related('user').filter( + yaoqingren=guanshi_yonghuid + ) + + # 4. 全局统计(不受筛选影响,直接基于 base_qs) + total_all = base_qs.count() + zaixian_all = base_qs.filter(zaixianzhuangtai=1).count() + zhengchang_all = base_qs.filter(zhanghaozhuangtai=1).count() + fengjin_all = total_all - zhengchang_all + + # 5. 应用筛选条件 + if keyword: + base_qs = base_qs.filter( + Q(nicheng__icontains=keyword) | Q(user__yonghuid__icontains=keyword) + ) + if zhanghaozhuangtai is not None: + try: + val = int(zhanghaozhuangtai) + if val == 1: + base_qs = base_qs.filter(zhanghaozhuangtai=1) + else: + base_qs = base_qs.exclude(zhanghaozhuangtai=1) + except (ValueError, TypeError): + pass + if zaixianzhuangtai is not None: + try: + val = int(zaixianzhuangtai) + if val == 1: + base_qs = base_qs.filter(zaixianzhuangtai=1) + else: + base_qs = base_qs.exclude(zaixianzhuangtai=1) + except (ValueError, TypeError): + pass + + # 6. 排序和分页 + base_qs = base_qs.order_by('-create_time') + paginator = Paginator(base_qs, page_size) + try: + page_obj = paginator.page(page) + except EmptyPage: + page_obj = [] + has_more = False + filtered_total = paginator.count if paginator.count else 0 + else: + has_more = page_obj.has_next() + filtered_total = paginator.count + + # 7. 构建打手列表 + dashou_list = [] + for dashou in page_obj: + user_main = dashou.user + dashou_list.append({ + 'uid': user_main.yonghuid, + 'nicheng': dashou.nicheng or '打手', + 'zaixianzhuangtai': dashou.zaixianzhuangtai, + 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, + 'touxiang': user_main.avatar or '', + 'jiedanzongliang': dashou.jiedanzongliang, + 'chengjiaozongliang': dashou.chengjiaozongliang, + 'tuikuanliang': dashou.tuikuanliang, + 'yue': str(dashou.zonge), + }) + + # 8. 获取管事邀请总数(来自管事扩展表) + try: + yaoqingzongshu = current_user.guanshi_profile.yaogingshuliang + except Exception: + yaoqingzongshu = total_all + + response_data = { + 'dashouList': dashou_list, + 'yaoqingzongshu': yaoqingzongshu, # 邀请总数(全局) + 'totalCount': filtered_total, # 当前筛选条件下的总数 + 'zaixianCount': zaixian_all, # 全局在线 + 'zhengchangCount': zhengchang_all, # 全局正常 + 'fengjinCount': fengjin_all, # 全局封禁 + 'currentPage': page, + 'pageSize': page_size, + 'hasMore': has_more, + } + + return Response({'code': 200, 'message': '获取成功', 'data': response_data}) + + except ValueError: + return Response({'code': 400, 'message': '分页参数格式错误'}, status=400) + except Exception as e: + logger.error(f'获取打手列表失败: {str(e)}', exc_info=True) + return Response({'code': 500, 'message': '系统错误'}, status=500) + + + + + +class TixianXinxiHuoquView(APIView): + """ + 获取用户提现收款信息接口 + 返回用户已设置的手机号、收款码、收款账号 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + """ + 处理获取收款信息的POST请求 + 直接返回用户当前的收款设置 + """ + try: + # 从request.user获取当前用户 + user_main = request.user + + # 🔥【修改开始 - 使用Decimal,字段名是lilu】 + # 查询打手提现费率 (fadanpingtai='5') + dashou_rate_obj = Lilubiao.objects.filter(fadanpingtai='5').first() + dashou_rate = decimal.Decimal(str(dashou_rate_obj.lilu)) if dashou_rate_obj else decimal.Decimal('0.00') + + # 查询管事提现费率 (fadanpingtai='6') + guanshi_rate_obj = Lilubiao.objects.filter(fadanpingtai='6').first() + guanshi_rate = decimal.Decimal(str(guanshi_rate_obj.lilu)) if guanshi_rate_obj else decimal.Decimal('0.00') + # 🔥【修改结束】 + + # 构建返回数据 + response_data = { + 'txdianhua': user_main.phone if user_main.phone else '', + 'txtupian': user_main.zhifu if user_main.zhifu else '', + 'txzh': user_main.skzhanghao if user_main.skzhanghao else '', + # 🔥【修改开始】 + 'dashou_rate': dashou_rate, # 打手提现费率 + 'guanshi_rate': guanshi_rate, # 管事提现费率 + # 🔥【修改结束】 + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '获取成功', + 'data': response_data + }) + + except Exception as e: + return Response({ + 'code': 99, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + +class ShoukuanXinxiShangchuanView(APIView): + """ + 上传用户收款信息接口 + 支持同时上传手机号、收款账号、收款码图片 + 使用multipart/form-data格式 + """ + permission_classes = [permissions.IsAuthenticated] + parser_classes = (MultiPartParser,FormParser,JSONParser) + + def post(self, request): + """ + 处理上传收款信息的POST请求 + 验证并保存用户的收款设置 + """ + try: + # 获取当前用户 + user_main = request.user + yonghuid = user_main.yonghuid + + # 准备响应数据 + response_data = {} + + # 1. 验证必填字段:手机号必须要有 + txdianhua = request.data.get('txdianhua', '').strip() + if not txdianhua: + return Response({ + 'code': 1, + 'msg': '收款人电话不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证手机号格式(11位数字) + if not txdianhua.isdigit() or len(txdianhua) != 11: + return Response({ + 'code': 2, + 'msg': '请输入11位有效的手机号码', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 获取收款账号(可能为空) + txzh = request.data.get('txzh', '').strip() + + # 3. 处理收款码图片上传(如果有) + txtupian_file = request.FILES.get('file') # 注意:前端upload.js使用'file'作为字段名 + txtupian_url = '' + + if txtupian_file: + # 验证图片 + is_valid, error_msg = validate_image(txtupian_file) + if not is_valid: + return Response({ + 'code': 3, + 'msg': f'图片验证失败: {error_msg}', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 上传收款码图片 + txtupian_url = self.handle_shoukuanma_upload(user_main, txtupian_file) + if txtupian_url: + response_data['txtupian'] = txtupian_url + else: + return Response({ + 'code': 4, + 'msg': '收款码上传失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + else: + response_data['txtupian'] = '' + + # 4. 验证收款账号和收款码至少有一个 + if not txzh and not txtupian_url: + return Response({ + 'code': 5, + 'msg': '请填写收款账号或上传收款码', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 5. 保存所有信息到数据库 + with transaction.atomic(): + user_main.phone = txdianhua + user_main.skzhanghao = txzh if txzh else '' + + # 只有上传了图片才更新zhifu字段 + if txtupian_url: + user_main.zhifu = txtupian_url + + user_main.save() + + # 6. 构建完整的返回数据 + response_data['txdianhua'] = txdianhua + response_data['txzh'] = txzh if txzh else '' + + # 7. 返回成功响应 + return Response({ + 'code': 0, + 'msg': '收款信息设置成功', + 'data': response_data + }) + + except Exception as e: + traceback.print_exc() # 打印详细错误信息到控制台 + return Response({ + 'code': 99, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def handle_shoukuanma_upload(self, user_main, image_file): + """ + 处理收款码图片上传到腾讯云COS + 存储路径: tixian/{yonghuid}/{yonghuid}.扩展名 + """ + try: + yonghuid = user_main.yonghuid + if not yonghuid: + return None + + # 获取旧收款码URL + old_shoukuanma_url = user_main.zhifu + + # 生成文件扩展名 + file_name = image_file.name + file_extension = os.path.splitext(file_name)[1].lower() + if not file_extension: + file_extension = '.jpg' + + # 构建OSS文件路径: tixian/{yonghuid}/{yonghuid}{扩展名} + # 使用用户ID作为文件名,确保同一用户多次上传会覆盖 + oss_file_name = f"{yonghuid}{file_extension}" + oss_file_path = f"tixian/{yonghuid}/{oss_file_name}" + + # 删除旧收款码(如果存在) + if old_shoukuanma_url and old_shoukuanma_url.strip(): + delete_from_oss(old_shoukuanma_url) + + # 上传新收款码到OSS + new_shoukuanma_url = upload_to_oss(image_file, oss_file_path) + + if new_shoukuanma_url: + # 返回相对路径(从tixian文件夹开始) + return oss_file_path + else: + return None + + except Exception as e: + print(f"处理收款码上传失败: {e}") + return None + + + + + + +# ========== 订单状态常量(请根据您的实际业务修改)========== +# 假设状态值:已完成=9,已退款=10(示例,实际以您的项目为准) +ORDER_STATUS_COMPLETED = 3 # 订单已完成 +ORDER_STATUS_REFUNDED = 5 # 订单已退款 +ORDER_STATUS_FAIL = 6 +# 进行中状态(不可提现)例如:待接单、已接单、进行中等,您需要定义排除列表 +FORBIDDEN_STATUS_FOR_WITHDRAW = [1, 2, 3, 4, 5, 6, 7, 8] # 除已完成和已退款外的所有状态 + + +class TixianShenqingView(APIView): + """ + 用户提现申请接口(完整版) + 支持类型: + 1-打手佣金提现 + 2-管事分红提现 + 3-组长分红提现 + 4-审核官分佣提现 + 5-打手押金提现(退出平台) + 6-商家余额提现(退出平台) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user_main = request.user + yonghuid = user_main.yonghuid + + # 1. 获取并验证参数 + leixing = request.data.get('leixing') + jine = request.data.get('jine') + fangshi = request.data.get('fangshi') + txskfs = request.data.get('txskfs', 0) + + # 罚单检查(共用) + fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2) + if not fadan_ok: + return Response({'code': 400, 'msg': fadan_msg}) + + # 参数完整性 + missing = [] + if leixing is None: missing.append('leixing') + if jine is None: missing.append('jine') + if fangshi is None: missing.append('fangshi') + if missing: + return Response({'code': 1, 'msg': f'缺少参数: {", ".join(missing)}'}, + status=status.HTTP_400_BAD_REQUEST) + + try: + leixing = int(leixing) + jine = decimal.Decimal(str(jine)) + fangshi = int(fangshi) + txskfs = int(txskfs) + except (ValueError, TypeError, decimal.InvalidOperation) as e: + return Response({'code': 2, 'msg': f'参数格式错误: {str(e)}'}, + status=status.HTTP_400_BAD_REQUEST) + + if jine <= decimal.Decimal('0'): + return Response({'code': 3, 'msg': '提现金额必须大于0'}, + status=status.HTTP_400_BAD_REQUEST) + + # 支持的提现类型(1-6) + if leixing not in [1, 2, 3, 4, 5, 6]: + return Response({'code': 4, 'msg': '无效的提现类型'}, + status=status.HTTP_400_BAD_REQUEST) + + if fangshi not in [1, 2]: + return Response({'code': 5, 'msg': '无效的收款方式,只能是1(微信)或2(支付宝)'}, + status=status.HTTP_400_BAD_REQUEST) + + # 2. 获取对应费率及手续费 + current_rate = decimal.Decimal('0.00') + rate_key = { + 1: '5', # 打手佣金 + 2: '6', # 管事分红 + 3: '8', # 组长分红 + 4: '9', # 审核官分佣 + 5: '11', # 打手押金提现 + 6: '10', # 商家余额提现 + }.get(leixing) + + if rate_key: + rate_obj = Lilubiao.objects.filter(fadanpingtai=rate_key).first() + if rate_obj: + current_rate = decimal.Decimal(str(rate_obj.lilu)) + else: + return Response({'code': 41, 'msg': f'提现费率未配置(类型{leixing})'}, + status=status.HTTP_400_BAD_REQUEST) + + shouxufei = jine * current_rate + shijidaozhang = jine - shouxufei + if shijidaozhang <= decimal.Decimal('0'): + return Response({'code': 6, 'msg': '提现金额过低,扣除手续费后无实际到账'}, + status=status.HTTP_400_BAD_REQUEST) + + # 3. 根据提现类型执行校验和扣款 + nicheng = '' + if leixing == 1: + result = self._check_dashou_commission_withdraw(user_main, jine) + if result['code'] != 0: + return Response(result, status=status.HTTP_400_BAD_REQUEST) + nicheng = result['data']['nicheng'] + # 扣款(佣金) + with transaction.atomic(): + profile = UserDashou.objects.select_for_update().get(user=user_main) + if profile.yue < jine: + return Response({'code': 13, 'msg': '余额不足'}) + profile.yue = F('yue') - jine + profile.save() + elif leixing == 2: + result = self._check_guanshi_dividend_withdraw(user_main, jine) + if result['code'] != 0: + return Response(result, status=status.HTTP_400_BAD_REQUEST) + with transaction.atomic(): + profile = UserGuanshi.objects.select_for_update().get(user=user_main) + if profile.yue < jine: + return Response({'code': 22, 'msg': '余额不足'}) + profile.yue = F('yue') - jine + profile.save() + nicheng = "管事用户" + elif leixing == 3: + result = self._check_zuzhang_withdraw(user_main, jine) + if result['code'] != 0: + return Response(result, status=status.HTTP_400_BAD_REQUEST) + with transaction.atomic(): + profile = UserZuzhang.objects.select_for_update().get(user=user_main) + if profile.ketixian_jine < jine: + return Response({'code': 32, 'msg': '可提现金额不足'}) + profile.ketixian_jine = F('ketixian_jine') - jine + profile.save() + nicheng = user_main.nicheng if hasattr(user_main, 'nicheng') else '' + elif leixing == 4: + result = self._check_shenheguan_withdraw(user_main, jine) + if result['code'] != 0: + return Response(result, status=status.HTTP_400_BAD_REQUEST) + with transaction.atomic(): + profile = UserShenheguan.objects.select_for_update().get(user=user_main) + if profile.yue < jine: + return Response({'code': 42, 'msg': '可提现金额不足'}) + profile.yue = F('yue') - jine + profile.save() + nicheng = "审核官" + elif leixing == 5: + # 打手押金提现(退出) + result = self._check_dashou_yajin_withdraw(user_main, jine) + if result['code'] != 0: + return Response(result, status=status.HTTP_400_BAD_REQUEST) + with transaction.atomic(): + profile = UserDashou.objects.select_for_update().get(user=user_main) + if profile.yajin < jine: + return Response({'code': 53, 'msg': '押金余额不足'}) + profile.yajin = F('yajin') - jine + # 押金提现后封禁打手账号 + profile.zhanghaozhuangtai = 0 # 0表示禁用 + profile.save() + nicheng = profile.nicheng or '' + elif leixing == 6: + # 商家余额提现(退出) + result = self._check_shangjia_withdraw(user_main, jine) + if result['code'] != 0: + return Response(result, status=status.HTTP_400_BAD_REQUEST) + with transaction.atomic(): + profile = UserShangjia.objects.select_for_update().get(user=user_main) + if profile.yue < jine: + return Response({'code': 63, 'msg': '商家余额不足'}) + profile.yue = F('yue') - jine + # 注意:是否封禁商家账号?根据需求,用户未明确要求封禁,暂时只扣款,可后续扩展。 + profile.save() + nicheng = profile.nicheng or '' + + # 4. 创建提现记录(先扣款后创建,确保资金安全) + try: + avatar = user_main.avatar or '' + phone = user_main.phone or '' + zhifu = user_main.zhifu or '' + skzhanghao = user_main.skzhanghao or '' + tixian_record = Tixianjilu.objects.create( + yonghuid=yonghuid, + avatar=avatar, + phone=phone, + nicheng=nicheng, + leixing=leixing, + zhifu=zhifu, + skzhanghao=skzhanghao, + jine=shijidaozhang, # 记录实际到账金额 + zhuangtai=1, # 审核中 + fangshi=fangshi, + shenheid=None, + bhliyou='', + ) + except Exception as e: + return Response({'code': 99, 'msg': f'创建提现记录失败: {str(e)}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 5. 返回成功响应 + return Response({ + 'code': 0, + 'msg': '提现申请提交成功', + 'data': { + 'tixian_id': tixian_record.id, + 'leixing': leixing, + 'jine': str(jine.quantize(decimal.Decimal('0.01'))), + 'fangshi': fangshi, + 'zhuangtai': 1, + 'create_time': tixian_record.create_time.strftime('%Y-%m-%d %H:%M:%S'), + 'current_rate': str(current_rate.quantize(decimal.Decimal('0.0000'))), + 'shouxufei': str(shouxufei.quantize(decimal.Decimal('0.01'))), + 'shijidaozhang': str(shijidaozhang.quantize(decimal.Decimal('0.01'))), + 'tip': f'本次提现手续费率{current_rate * 100}%,手续费{shouxufei}元,实际到账{shijidaozhang}元' + } + }) + + except Exception as e: + traceback.print_exc() + return Response({'code': 99, 'msg': f'系统错误: {str(e)}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # ==================== 校验函数 ==================== + + def _check_dashou_commission_withdraw(self, user_main, jine): + """打手佣金提现校验""" + try: + dashou = UserDashou.objects.get(user=user_main) + except ObjectDoesNotExist: + return {'code': 10, 'msg': '您不是打手身份,无法提现佣金'} + + if dashou.zhanghaozhuangtai != 1: + return {'code': 11, 'msg': '打手账号已被封禁,无法提现'} + if dashou.zhuangtai != 1: + return {'code': 12, 'msg': '您有订单进行中,请完成后提现'} + if dashou.jifen != 10: + return {'code': 15, 'msg': '积分不足10分,请补充积分后提现'} + if dashou.yue < jine: + return {'code': 13, 'msg': f'余额不足,当前余额: {dashou.yue}'} + + return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}} + + def _check_guanshi_dividend_withdraw(self, user_main, jine): + """管事分红提现校验""" + try: + guanshi = UserGuanshi.objects.get(user=user_main) + except ObjectDoesNotExist: + return {'code': 20, 'msg': '您不是管事身份,无法提现分红'} + if guanshi.zhuangtai != 1: + return {'code': 21, 'msg': '管事账号已被封禁,无法提现'} + if guanshi.yue < jine: + return {'code': 22, 'msg': f'余额不足,当前余额: {guanshi.yue}'} + return {'code': 0, 'msg': 'ok', 'data': {}} + + def _check_zuzhang_withdraw(self, user_main, jine): + """组长分红提现校验""" + try: + zuzhang = UserZuzhang.objects.get(user=user_main) + except ObjectDoesNotExist: + return {'code': 30, 'msg': '您不是组长身份,无法提现组长分红'} + if zuzhang.zhuangtai != 1: + return {'code': 31, 'msg': '组长账号已被封禁,无法提现'} + if zuzhang.ketixian_jine < jine: + return {'code': 32, 'msg': f'可提现金额不足,当前可提: {zuzhang.ketixian_jine}'} + return {'code': 0, 'msg': 'ok', 'data': {}} + + def _check_shenheguan_withdraw(self, user_main, jine): + """审核官分佣提现校验""" + try: + shenheguan = UserShenheguan.objects.get(user=user_main) + except ObjectDoesNotExist: + return {'code': 40, 'msg': '您不是审核官身份,无法提现分佣'} + if shenheguan.zhuangtai != 1: + return {'code': 41, 'msg': '审核官账号已被封禁,无法提现'} + if shenheguan.yue < jine: + return {'code': 42, 'msg': f'可提现金额不足,当前可提: {shenheguan.yue}'} + return {'code': 0, 'msg': 'ok', 'data': {}} + + + def _check_shangjia_withdraw(self, user_main, jine): + """ + 商家余额提现校验(退出平台) + 包含:订单、自身罚单、申请的罚单检查 + """ + try: + shangjia = UserShangjia.objects.get(user=user_main) + except ObjectDoesNotExist: + return {'code': 60, 'msg': '您不是商家身份,无法提现商家余额'} + + if shangjia.zhuangtai != 1: + return {'code': 61, 'msg': '商家账号已被禁用,无法提现'} + if shangjia.yue < jine: + return {'code': 62, 'msg': f'商家余额不足,当前余额: {shangjia.yue}'} + + # 订单检查(使用 Q 取反) + unfinished_orders = DingdanShangjia.objects.filter( + shangjia_id=user_main.yonghuid, + ).exclude( + dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED,ORDER_STATUS_FAIL] + ).exists() + if unfinished_orders: + return {'code': 63, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提现余额'} + + # 自身被处罚的罚单检查(状态不是已缴纳(2)和已驳回(4)) + self_fadan = Fadan.objects.filter( + beichufa_id=user_main.yonghuid, + ).exclude( + zhuangtai__in=[2, 4] + ).exists() + if self_fadan: + return {'code': 64, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'} + + # 申请的对他人处罚罚单检查(状态不是已缴纳(2)和已驳回(4)) + applied_fadan = Fadan.objects.filter( + shenqing_chufa=user_main.yonghuid, + ).exclude( + zhuangtai__in=[2, 4] + ).exists() + if applied_fadan: + return {'code': 65, 'msg': '您发起的对其他用户的处罚尚未处理完毕(待缴纳/申诉中),请等待完结'} + + return {'code': 0, 'msg': 'ok', 'data': {'nicheng': shangjia.nicheng or ''}} + + def _check_dashou_yajin_withdraw(self, user_main, jine): + """ + 打手押金提现校验(退出平台) + 要求:无进行中的订单,无未结算订单,自身罚单需已处理 + """ + try: + dashou = UserDashou.objects.get(user=user_main) + except ObjectDoesNotExist: + return {'code': 50, 'msg': '您不是打手身份,无法提取押金'} + + if dashou.zhanghaozhuangtai != 1: + return {'code': 51, 'msg': '打手账号已禁用,无法提取押金'} + if dashou.zhuangtai != 1: + return {'code': 52, 'msg': '您有订单进行中,请完成后提取押金'} + if dashou.yajin < jine: + return {'code': 53, 'msg': f'押金余额不足,当前押金: {dashou.yajin}'} + + # 订单检查(使用 exclude + __in) + unfinished_orders = Dingdan.objects.filter( + jiedan_dashou_id=user_main.yonghuid, + ).exclude( + zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED] + ).exists() + if unfinished_orders: + return {'code': 54, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提取押金'} + + # 自身罚单检查 + self_fadan = Fadan.objects.filter( + beichufa_id=user_main.yonghuid, + ).exclude( + zhuangtai__in=[2, 4] + ).exists() + if self_fadan: + return {'code': 55, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'} + + return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}} + + + + + + + + + +class TixianJiluHuoquViewV2(APIView): + """ + 提现记录获取接口 + mode=1:手动提现记录(默认,兼容旧版) + mode=2:自动提现审核记录(含 shenhe_jilu_id / shenhe_danhao,支持 zhuangtai 筛选) + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + user_main = request.user + yonghuid = user_main.yonghuid + page = request.data.get('page', 1) + limit = request.data.get('limit', 50) + mode = int(request.data.get('mode', 1) or 1) + zhuangtai_filter = int(request.data.get('zhuangtai', 0) or 0) + + try: + page = int(page) + limit = int(limit) + if page < 1 or limit < 1: + return Response({'code': 1, 'msg': '参数错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + except (TypeError, ValueError): + return Response({'code': 1, 'msg': '参数格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + if limit > 100: + limit = 100 + offset = (page - 1) * limit + + qs = Tixianjilu.objects.filter(yonghuid=yonghuid) + if mode == 2: + # 自动提现:含新审核关联记录;zhuangtai>0 时按状态筛选 + if zhuangtai_filter > 0: + qs = qs.filter(zhuangtai=zhuangtai_filter) + qs = qs.order_by('-create_time') + + records_queryset = qs.values( + 'id', 'leixing', 'jine', 'zhuangtai', 'create_time', 'fangshi', + 'phone', 'skzhanghao', 'zhifu', 'bhliyou', + 'shenhe_jilu_id', 'shenhe_danhao', + )[offset:offset + limit + 1] + + records_list = list(records_queryset) + has_more = len(records_list) > limit + records_to_return = records_list[:limit] if has_more else records_list + + # 批量取审核表 fail_reason + audit_ids = [r['shenhe_jilu_id'] for r in records_to_return if r.get('shenhe_jilu_id')] + audit_map = {} + if audit_ids: + for a in TixianShenheJilu.objects.filter(id__in=audit_ids).values( + 'id', 'fail_reason', 'shenqing_jine', 'shenhe_danhao' + ): + audit_map[a['id']] = a + + # 批量取自动打款失败原因 + danhao_list = [r['shenhe_danhao'] for r in records_to_return if r.get('shenhe_danhao')] + auto_fail_map = {} + if danhao_list: + for ar in TixianAutoRecord.objects.filter( + shenhe_danhao__in=danhao_list, zhuangtai=2 + ).order_by('-create_time').values('shenhe_danhao', 'fail_reason'): + if ar['shenhe_danhao'] not in auto_fail_map: + auto_fail_map[ar['shenhe_danhao']] = ar['fail_reason'] + + formatted_records = [] + for record in records_to_return: + audit = audit_map.get(record.get('shenhe_jilu_id')) if record.get('shenhe_jilu_id') else None + fail_reason = '' + if audit and audit.get('fail_reason'): + fail_reason = audit['fail_reason'] + elif record.get('shenhe_danhao'): + fail_reason = auto_fail_map.get(record['shenhe_danhao'], '') or '' + + jine_val = float(record['jine']) if record['jine'] else 0.00 + if mode == 2 and audit and audit.get('shenqing_jine') is not None: + jine_val = float(audit['shenqing_jine']) + + shenhe_danhao_val = record.get('shenhe_danhao') or (audit.get('shenhe_danhao') if audit else '') + formatted_records.append({ + 'id': record['id'], + 'tixianjilu_id': record['id'], + 'tixian_id': record['id'], + 'leixing': record['leixing'], + 'jine': jine_val, + 'zhuangtai': record['zhuangtai'], + 'create': record['create_time'].strftime('%Y-%m-%d %H:%M:%S'), + 'fangshi': record['fangshi'], + 'txdianhua': record['phone'] or '', + 'txzh': record['skzhanghao'] or '', + 'txtupian': record['zhifu'] or '', + 'shibaiyuanyin': record['bhliyou'] or '', + 'bhliyou': record['bhliyou'] or '', + 'fail_reason': fail_reason, + 'shenhe_jilu_id': record.get('shenhe_jilu_id'), + 'shenhe_danhao': shenhe_danhao_val, + }) + + return Response({ + 'code': 0, + 'msg': '获取成功', + 'data': { + 'list': formatted_records, + 'has_more': has_more, + 'page': page, + 'limit': limit, + 'current_count': len(formatted_records), + }, + }) + + except Exception as e: + return Response({ + 'code': 99, + 'msg': f'系统错误: {str(e)}', + 'data': None, + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +# yonghu/views.py +class AdminLoginView(APIView): + """ + 管理员登录接口 + 路径:/yonghu/adlog/ + 方法:POST + 字段:zhanghao(账号), mima(密码), ip(二次验证码) + """ + + # 限制匿名用户访问频率,防止暴力破解 + throttle_classes = [AnonRateThrottle] + # 允许任何人访问(登录接口不需要认证) + permission_classes = [AllowAny] + + def post(self, request): + """ + 处理管理员登录请求 + 安全策略:任何错误都返回401,不透露具体信息 + """ + try: + # 1. 获取客户端真实IP和用户代理信息 + kehuduan_ip = huoquKehuduanIP(request) + yonghu_daili = huoquYonghuDaili(request) + + + # 2. 获取前端传递的数据 + data = request.data + + # 检查必需字段是否存在 + bixu_ziduan = ['zhanghao', 'mima', 'ip'] + for ziduan in bixu_ziduan: + if ziduan not in data: + # 字段缺失,返回401 + return Response( + {'code': 401, 'msg': '登录失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 提取并清理数据 + zhanghao = data.get('zhanghao', '').strip() + mima = data.get('mima', '').strip() + erci_yanzhengma = data.get('ip', '').strip() # 二次验证码(前端叫ip) + + # 简单验证字段非空 + if not zhanghao or not mima or not erci_yanzhengma: + return Response( + {'code': 401, 'msg': '登录失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 3. 使用事务确保数据一致性 + with transaction.atomic(): + # 【优化点1】使用select_related一次性获取管理员扩展表 + # 只查询管理员类型的用户 + user = UserMain.objects.filter( + phone=zhanghao, # 手机号作为账号 + password=mima, # 密码 + user_type='admin' # 必须是管理员类型 + ).select_related('admin_profile').first() + + # 如果用户不存在或不是管理员,返回401 + if not user: + return Response( + {'code': 401, 'msg': '登录失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 【优化点2】检查管理员扩展表是否存在 + if not hasattr(user, 'admin_profile'): + return Response( + {'code': 401, 'msg': '登录失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 【优化点3】验证二次密码(管理员扩展表中的password字段) + admin_profile = user.admin_profile + if admin_profile.password != erci_yanzhengma: + return Response( + {'code': 401, 'msg': '登录失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 4. 更新用户登录信息 + # 获取当前时间 + now_time = timezone.now() + + # 处理IP地址长度问题 + # 如果原ip字段长度不够,截取前11个字符(保持兼容性) + + cunchu_ip = kehuduan_ip + #print(cunchu_ip) + + # 更新用户信息 + user.ip = cunchu_ip # 存储客户端IP + user.last_login_time = now_time # 更新最后登录时间 + user.save() + + # 【优化点4】生成JWT token(使用和微信登录相同的方式) + # 使用djangorestframework-simplejwt生成token + refresh = RefreshToken.for_user(user) + token = str(refresh.access_token) + + # 5. 准备返回数据 + # 返回用户基本信息(可根据需要调整) + response_data = { + 'token': token, + 'yonghuid': user.yonghuid, + 'phone': user.phone or '', + 'user_type': user.user_type, + 'last_login_time': now_time.strftime('%Y-%m-%d %H:%M:%S') if now_time else '', + } + + # 可以添加管理员特定信息 + if hasattr(user, 'admin_profile'): + response_data['wechat'] = user.admin_profile.wechat or '' + + return Response({ + 'code': 0, + 'msg': '登录成功', + 'data': response_data + }) + + except Exception as e: + # 【安全点】任何异常都返回401,不暴露具体错误信息 + # 记录日志用于排查问题,但前端只收到通用错误信息 + # 这里可以添加日志记录,比如: + logging.error(f'管理员登录异常: {str(e)}', exc_info=True) + + return Response( + {'code': 401, 'msg': '登录失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + +# yonghu/views.py +class AdminFinancialDataView(APIView): + """ + 管理员财务数据接口 + 路径:/yonghu/adhq/ + 方法:POST + 认证:JWT Token + 权限:仅管理员 + 返回:收支记录数据 + """ + + # 使用JWT认证 + permission_classes = [IsAuthenticated] + + def post(self, request): + """ + 处理管理员财务数据请求 + 安全策略:任何错误都返回401,不透露具体信息 + """ + try: + # 1. 获取前端传递的数据 + data = request.data + + # 检查必需字段 + if 'zhanghao' not in data: + return Response( + {'code': 401, 'msg': '请求参数错误', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + qianzhanghao = data.get('zhanghao', '').strip() + + # 简单验证字段非空 + if not qianzhanghao: + return Response( + {'code': 401, 'msg': '请求参数错误', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 2. 获取当前登录用户(JWT认证后,request.user就是UserMain实例) + dangqianyonghu = request.user + + # 3. 验证用户类型是否为admin + if not hasattr(dangqianyonghu, 'user_type') or dangqianyonghu.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限不足', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 4. 验证前端传递的账号与当前用户的phone字段是否一致 + if not dangqianyonghu.phone or dangqianyonghu.phone != qianzhanghao: + return Response( + {'code': 401, 'msg': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 5. 验证管理员扩展表是否存在(反向查询) + # 使用高效的ORM查询,避免额外的数据库查询 + try: + # 使用select_related一次性获取管理员扩展表 + # 注意:这里假设user_type='admin'的用户都有admin_profile + yonghu_xiangxi = UserMain.objects.select_related('admin_profile').get( + pk=dangqianyonghu.pk, + user_type='admin' + ) + + # 检查管理员扩展表是否存在 + if not hasattr(yonghu_xiangxi, 'admin_profile'): + return Response( + {'code': 401, 'msg': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + except UserMain.DoesNotExist: + return Response( + {'code': 401, 'msg': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 6. 查询收支记录表(Szjilu) + # 使用高效的ORM查询,获取最新的一条记录 + # 注意:根据你的业务需求,这里获取最新的一条记录 + # 如果需要汇总多条记录,可以使用aggregate进行聚合查询 + + # 方法1:获取最新的一条记录(假设表中只有一条汇总记录) + shouzhijilu = Szjilu.objects.order_by('-create_time').first() + + # 方法2:如果表中可能有多条记录,需要汇总,可以使用以下方式: + # from django.db.models Sum + # zonghe = Szjilu.objects.aggregate( + # zongliushui=Sum('zongls'), + # zongshouyi=Sum('zongsy'), + # zongzhichu=Sum('zongzc'), + # jrls=Sum('jrls'), + # jrzc=Sum('jrzc') + # ) + + # 准备返回数据 + if shouzhijilu: + # 有记录的情况 + huizongshuju = { + 'zongliushui': float(shouzhijilu.zongls or 0), + 'zongshouyi': float(shouzhijilu.zongsy or 0), + 'zongzhichu': float(shouzhijilu.zongzc or 0), + 'jrls': float(shouzhijilu.jrls or 0), + 'jrzc': float(shouzhijilu.jrzc or 0) + } + else: + # 没有记录的情况,返回默认值 + huizongshuju = { + 'zongliushui': 0.00, + 'zongshouyi': 0.00, + 'zongzhichu': 0.00, + 'jrls': 0.00, + 'jrzc': 0.00 + } + + # 7. 返回成功响应 + return Response({ + 'code': 0, + 'msg': '获取成功', + 'data': huizongshuju + }) + + except Exception as e: + # 【安全策略】任何异常都返回401,不暴露具体错误信息 + # 这里可以记录日志,但不返回给前端 + # logging.error(f'管理员财务数据接口异常: {str(e)}', exc_info=True) + + return Response( + {'code': 401, 'msg': '获取失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + + + + + +# shangpin/views.py +class AdGetOrderTypes(APIView): + """ + 后台获取订单类型列表接口 + 前端调用地址: /shangpin/getOrderTypes + 前端需要字段: id, biaoti, tupian_url + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + # 1. 获取参数 + qianzhanghao = request.data.get('zhanghao', '').strip() + + if not qianzhanghao: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2. 身份验证(按你说的方式) + dangqianyonghu = request.user + + # 验证手机号 + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证管理员身份 + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 3. 验证管理员扩展表 + try: + guanliyuan = dangqianyonghu.admin_profile + except AttributeError: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 4. 查询订单类型(商品类型)表 + # 注意:这里需要根据你实际的模型名调整 + # 如果模型名不是ShangpinType,请告诉我正确的模型名 + + # 假设模型名是ShangpinLeixing(因为我之前看到你有商品类型表) + # 你需要告诉我确切的模型名 + try: + # 请替换为你的实际模型名 + # 只查询前端需要的三个字段 + shangpin_types = ShangpinLeixing.objects.all().only('id', 'jieshao', 'tupian_url') + + # 构建响应数据 + type_list = [ + { + 'id': item.id, + 'biaoti': item.jieshao or '', + 'tupian_url': item.tupian_url or '' + } + for item in shangpin_types + ] + + + return Response({ + 'code': 0, + 'message': '获取成功', + 'data': type_list + }) + + except Exception as e: + logger.error(f"查询商品类型表失败: {str(e)}") + # 如果表不存在,返回空数组 + return Response({ + 'code': 0, + 'message': '获取成功', + 'data': [] + }) + + except Exception as e: + logger.error(f"获取订单类型异常: {str(e)}", exc_info=True) + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + + + +# yonghu/views.py 或 dingdan/views.py +class AdGetDingdanList(APIView): + """ + 后台订单列表获取接口 + 前端调用地址: /yonghu/adddhq + 前端需要字段: dingdan_id, jieshao, zhuangtai, leixing, fadanpingtai, jiage, tupian_url, fadanshijian + 统计字段: stats.platform.total, stats.platform.completed, stats.merchant.total, stats.merchant.completed + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + # 1. 获取参数(严格按前端传的字段名) + qianzhanghao = request.data.get('zhanghao', '').strip() + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('pageSize', 30)) + + # 搜索相关参数 + dingdan_id = request.data.get('dingdan_id', '').strip() + + # 筛选相关参数(严格按前端字段名) + fadanpingtai = request.data.get('fadanpingtai') # 前端叫 fadanpingtai + leixing = request.data.get('leixing') # 前端叫 leixing + zhuangtai = request.data.get('zhuangtai', '') # 前端叫 zhuangtai + + if not qianzhanghao: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2. 身份验证(与你说的完全一样) + dangqianyonghu = request.user + + # 验证手机号 + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证管理员身份 + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 3. 验证管理员扩展表 + try: + guanliyuan = dangqianyonghu.admin_profile + except AttributeError: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 5. 构建查询条件(严格按前端字段名映射到数据库字段) + query_conditions = Q() + + # 判断是否为搜索模式 + is_search_mode = bool(dingdan_id) + + if is_search_mode: + # 搜索模式:只按订单ID查询 + query_conditions &= Q(dingdan_id=dingdan_id) + else: + # 筛选模式 + + # 发单平台筛选(前端: fadanpingtai -> 数据库: fadan_pingtai) + if fadanpingtai is not None: + try: + fadanpingtai_int = int(fadanpingtai) + if fadanpingtai_int in [1, 2]: + query_conditions &= Q(fadan_pingtai=fadanpingtai_int) + except (ValueError, TypeError): + pass + + # 订单类型筛选(前端: leixing -> 数据库: leixing_id) + if leixing is not None: + try: + leixing_int = int(leixing) + query_conditions &= Q(leixing_id=leixing_int) + except (ValueError, TypeError): + pass + + # 订单状态筛选(前端: zhuangtai -> 数据库: zhuangtai) + if zhuangtai and zhuangtai != 'all': + try: + # 处理多个状态,如 "1,7" + status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()] + if status_list: + query_conditions &= Q(zhuangtai__in=status_list) + except (ValueError, TypeError): + pass + + # 6. 获取统计数据(平台订单和商家订单) + # 注意:这里统计所有订单,不考虑筛选条件 + try: + # 平台订单统计 (fadan_pingtai=1) + platform_total = Dingdan.objects.filter(fadan_pingtai=1).count() + platform_completed = Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=3).count() + + # 商家订单统计 (fadan_pingtai=2) + merchant_total = Dingdan.objects.filter(fadan_pingtai=2).count() + merchant_completed = Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=3).count() + + platform_stats = { + 'total': platform_total, + 'completed': platform_completed + } + merchant_stats = { + 'total': merchant_total, + 'completed': merchant_completed + } + except Exception as e: + logger.warning(f"统计数据查询失败: {str(e)}") + platform_stats = {'total': 0, 'completed': 0} + merchant_stats = {'total': 0, 'completed': 0} + + # 7. 执行分页查询(只查询主表,不查扩展表) + start_time = time.time() + + # 计算分页 + offset = (page - 1) * page_size + + # 查询符合条件的订单总数 + total_count = Dingdan.objects.filter(query_conditions).count() + + # 查询当前页的数据(只查询主表,只查询前端需要的字段) + dingdan_list = Dingdan.objects.filter(query_conditions).only( + 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', + 'fadan_pingtai', 'jine', 'tupian', 'create_time' + ).order_by('-create_time')[offset:offset + page_size] + + query_time = time.time() - start_time + logger.info(f"订单查询完成,耗时: {query_time:.3f}s,查询条数: {len(dingdan_list)}") + + # 8. 构建响应数据(严格按前端需要的字段) + result_list = [] + + for dingdan in dingdan_list: + # 注意:这里字段名必须与前端完全一致 + order_data = { + 'dingdan_id': dingdan.dingdan_id or '', + 'jieshao': dingdan.jieshao or '', + 'zhuangtai': dingdan.zhuangtai or 1, + 'leixing': dingdan.leixing_id or 0, # 前端需要 leixing + 'fadanpingtai': dingdan.fadan_pingtai or 1, # 前端需要 fadanpingtai + 'jiage': float(dingdan.jine) if dingdan.jine else 0.00, # 价格对应 jine 字段 + 'tupian_url': dingdan.tupian or '', # 图片对应 tupian 字段 + 'fadanshijian': dingdan.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan.create_time else '' + } + result_list.append(order_data) + + # 9. 判断是否有更多数据 + current_loaded_count = offset + len(result_list) + has_more = len(result_list) >= page_size and current_loaded_count < total_count + + # 10. 构建完整响应(严格按前端需要的结构) + response_data = { + 'stats': { + 'platform': platform_stats, + 'merchant': merchant_stats + }, + 'list': result_list, + 'hasMore': has_more, + 'total': total_count, + 'currentPage': page, + 'pageSize': page_size + } + + return Response({ + 'code': 0, + 'message': '获取成功', + 'data': response_data + }) + + except Exception as e: + logger.error(f"获取订单列表异常: {str(e)}", exc_info=True) + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + + + + +class AdYaoQingDaShou(APIView): + """ + 后台获取邀请码接口(用于邀请打手) + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /yonghu/adyqds + 参数: { + "zhanghao": "管理员账号" + } + 返回: { + "code": 0, + "message": "成功", + "data": { + "yaoqingma": "邀请码" + } + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + # 1. 获取前端参数 + qianzhanghao = request.data.get('zhanghao', '').strip() + + if not qianzhanghao: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2. JWT Token身份验证 + dangqianyonghu = request.user + + # 3. 验证手机号是否匹配 + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 4. 验证用户类型是否为管理员 + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 5. 验证管理员扩展表是否存在 + try: + guanliyuan = dangqianyonghu.admin_profile + except AttributeError: + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 6. 获取用户ID(yonghuid),如果获取不到就用'123456' + yonghuid_str = getattr(dangqianyonghu, 'yonghuid', '123456') + + # 7. 尝试获取或创建管事扩展表 + try: + # 尝试获取现有的管事扩展表 + guanshi_profile = dangqianyonghu.guanshi_profile + except AttributeError: + # 管事扩展表不存在,创建一个 + try: + # 创建默认的管事扩展表 + guanshi_profile = UserGuanshi.objects.create( + user=dangqianyonghu, + yaoqingma='', # 暂时为空,后面生成 + dianhua='', # 默认空 + wechat='', # 默认空 + yaogingshuliang=0, # 邀请数量为0 + zhuangtai=1, # 正常状态 + jinrichongzhi=0, # 今日充值0 + jinyuechongzhi=0, # 今月充值0 + chongzhifenrun=0, # 充值分佣0 + yue=0 # 余额0 + ) + #logger.info(f"为管理员{dangqianyonghu.yonghuid}创建了管事扩展表") + except Exception as e: + logger.error(f"创建管事扩展表失败: {str(e)}") + return Response( + {'code': 500, 'message': '创建用户信息失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 8. 检查是否已有邀请码 + existing_yaoqingma = guanshi_profile.yaoqingma + + if existing_yaoqingma and existing_yaoqingma.strip(): + # 已有邀请码,直接返回 + return Response({ + 'code': 0, + 'message': '成功获取邀请码', + 'data': { + 'yaoqingma': existing_yaoqingma + } + }) + + # 9. 生成新的邀请码 + try: + # 生成邀请码 + new_yaoqingma = CreateInvitationCode(str(yonghuid_str)) + + # 验证邀请码格式 + if not VerifyInvitationCode(new_yaoqingma): + raise Exception('邀请码格式验证失败') + + except ImportError: + # 如果导入失败,使用简单方法生成邀请码 + timestamp = str(int(time.time())) + random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) + new_yaoqingma = f"GL{timestamp[-6:]}{random_str}" + + logger.warning(f"使用简单方法生成邀请码: {new_yaoqingma}") + except Exception as e: + logger.error(f"生成邀请码失败: {str(e)}") + return Response( + {'code': 500, 'message': '生成邀请码失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 10. 保存邀请码到数据库 + try: + with transaction.atomic(): + guanshi_profile.yaoqingma = new_yaoqingma + guanshi_profile.save() + + logger.info(f"管理员{dangqianyonghu.yonghuid}生成邀请码: {new_yaoqingma}") + except Exception as e: + logger.error(f"保存邀请码失败: {str(e)}") + return Response( + {'code': 500, 'message': '保存邀请码失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 11. 返回成功响应 + return Response({ + 'code': 0, + 'message': '成功生成邀请码', + 'data': { + 'yaoqingma': new_yaoqingma + } + }) + + except Exception as e: + logger.error(f"获取邀请码接口异常: {str(e)}", exc_info=True) + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + + +class AdGuanLiYongHu(APIView): + """ + 后台用户管理列表接口 + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /yonghu/adgl + 参数: { + "zhanghao": "管理员账号", + "user_type": "用户类型(1-4)", + "keyword": "搜索关键词(可选)", + "page": "页码(从1开始)", + "page_size": "每页数量" + } + 返回: { + "code": 0, + "message": "成功", + "data": { + "list": [用户列表], + "stats": {"1": 总数, "2": 总数, "3": 总数, "4": 总数}, + "has_more": true/false + } + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + # 1. 获取前端参数 + qianzhanghao = request.data.get('zhanghao', '').strip() + user_type = request.data.get('user_type') + keyword = request.data.get('keyword', '').strip() + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 30)) + + # 验证参数 + if not qianzhanghao or not user_type: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2. JWT Token身份验证 + dangqianyonghu = request.user + + # 3. 验证手机号是否匹配 + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 4. 验证用户类型是否为管理员 + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 5. 验证管理员扩展表是否存在 + try: + guanliyuan = dangqianyonghu.admin_profile + except AttributeError: + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 6. 根据用户类型选择查询的模型 + user_type_int = int(user_type) + + if user_type_int not in [1, 2, 3, 4]: + return Response( + {'code': 400, 'message': '用户类型错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 7. 统计四种用户类型的总数 + stats = self._get_user_stats() + + # 8. 根据用户类型查询用户列表 + user_list, has_more = self._query_users_by_type( + user_type_int, keyword, page, page_size + ) + + # 9. 构建返回数据 + return Response({ + 'code': 0, + 'message': '成功', + 'data': { + 'list': user_list, + 'stats': stats, + 'has_more': has_more + } + }) + + except ValueError: + logger.error("参数类型错误") + return Response( + {'code': 400, 'message': '参数类型错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except Exception as e: + logger.error(f"用户管理列表接口异常: {str(e)}", exc_info=True) + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + def _get_user_stats(self): + """ + 获取四种用户类型的统计数量 + """ + try: + + stats = { + '1': UserBoss.objects.count(), + '2': UserDashou.objects.count(), + '3': UserGuanshi.objects.count(), + '4': UserShangjia.objects.count() + } + + return stats + + except Exception as e: + logger.error(f"获取用户统计失败: {str(e)}") + return {'1': 0, '2': 0, '3': 0, '4': 0} + + def _query_users_by_type(self, user_type_int, keyword, page, page_size): + """ + 根据用户类型查询用户列表 + 返回: (用户列表, 是否还有更多数据) + """ + try: + # 计算偏移量 + offset = (page - 1) * page_size + + if user_type_int == 1: + return self._query_boss_users(keyword, offset, page_size) + elif user_type_int == 2: + return self._query_dashou_users(keyword, offset, page_size) + elif user_type_int == 3: + return self._query_guanshi_users(keyword, offset, page_size) + elif user_type_int == 4: + return self._query_shangjia_users(keyword, offset, page_size) + else: + return [], False + + except Exception as e: + logger.error(f"查询用户列表失败: {str(e)}") + return [], False + + def _query_boss_users(self, keyword, offset, limit): + """查询普通用户(老板)""" + try: + queryset = UserBoss.objects.select_related('user') + + if keyword: + queryset = queryset.filter( + Q(user__yonghuid__icontains=keyword) | + Q(nickname__icontains=keyword) + ) + + # 获取总数 + total_count = queryset.count() + + # 获取当前页的数据 + users = list(queryset.order_by('-create_time')[offset:offset + limit]) + + # 计算是否有更多数据 + current_count = len(users) + has_more = (offset + current_count) < total_count + + + + # 构建返回数据 + user_list = [] + for boss in users: + user_main = boss.user + + user_data = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar or '', + 'nicheng': boss.nickname or '', + } + + user_list.append(user_data) + + return user_list, has_more + + except Exception as e: + logger.error(f"查询老板用户失败: {str(e)}") + return [], False + + def _query_dashou_users(self, keyword, offset, limit): + """查询打手用户""" + try: + queryset = UserDashou.objects.select_related('user') + + if keyword: + queryset = queryset.filter( + Q(user__yonghuid__icontains=keyword) | + Q(nicheng__icontains=keyword) | + Q(chenghao__icontains=keyword) + ) + + # 获取总数 + total_count = queryset.count() + + # 获取当前页的数据 + dashou_list = list(queryset.order_by('-create_time')[offset:offset + limit]) + + # 计算是否有更多数据 + current_count = len(dashou_list) + has_more = (offset + current_count) < total_count + + + + # 构建返回数据 + user_list = [] + for dashou in dashou_list: + user_main = dashou.user + + user_data = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar or '', + 'zaixianzhuangtai': dashou.zaixianzhuangtai, + 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, + 'nicheng': dashou.nicheng or '', + 'zhuangtai': dashou.zhuangtai, + } + + user_list.append(user_data) + + return user_list, has_more + + except Exception as e: + logger.error(f"查询打手用户失败: {str(e)}") + return [], False + + def _query_guanshi_users(self, keyword, offset, limit): + """查询管事用户""" + try: + queryset = UserGuanshi.objects.select_related('user') + + if keyword: + queryset = queryset.filter( + Q(user__yonghuid__icontains=keyword) + ) + + # 获取总数 + total_count = queryset.count() + + # 获取当前页的数据 + guanshi_list = list(queryset.order_by('-create_time')[offset:offset + limit]) + + # 计算是否有更多数据 + current_count = len(guanshi_list) + has_more = (offset + current_count) < total_count + + + + # 构建返回数据 + user_list = [] + for guanshi in guanshi_list: + user_main = guanshi.user + + user_data = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar or '', + 'zhanghaozhuangtai': guanshi.zhuangtai, + 'nicheng': f'管事{user_main.yonghuid}', + } + + user_list.append(user_data) + + return user_list, has_more + + except Exception as e: + logger.error(f"查询管事用户失败: {str(e)}") + return [], False + + def _query_shangjia_users(self, keyword, offset, limit): + """查询商家用户""" + try: + queryset = UserShangjia.objects.select_related('user') + + if keyword: + queryset = queryset.filter( + Q(user__yonghuid__icontains=keyword) | + Q(nicheng__icontains=keyword) + ) + + # 获取总数 + total_count = queryset.count() + + # 获取当前页的数据 + shangjia_list = list(queryset.order_by('-create_time')[offset:offset + limit]) + + # 计算是否有更多数据 + current_count = len(shangjia_list) + has_more = (offset + current_count) < total_count + + + + # 构建返回数据 + user_list = [] + for shangjia in shangjia_list: + user_main = shangjia.user + + user_data = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar or '', + 'zhanghaozhuangtai': shangjia.zhuangtai, + 'nicheng': shangjia.nicheng or '', + } + + user_list.append(user_data) + + return user_list, has_more + + except Exception as e: + logger.error(f"查询商家用户失败: {str(e)}") + return [], False + + + + + +class CfGuanLi(APIView): + """ + 后台处罚管理列表接口 + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /yonghu/cfgl + 参数: { + "zhanghao": "管理员账号", + "qingqiu_tongji": true/false, # 是否只请求统计信息 + "sqzhuangtai": 0 或 [1,2], # 处罚申请状态筛选(可选) + "page": "页码(从1开始)", # 分页参数 + "page_size": "每页数量" # 分页参数 + } + 返回: { + "code": 0, + "message": "成功", + "data": { + "list": [处罚记录列表], + "zongshu": 总数, + "daichuli": 待处理数量, + "yichuli": 已处理数量, + "haiyougengduo": true/false + } + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + # 1. 获取前端参数 + qianzhanghao = request.data.get('zhanghao', '').strip() + qingqiu_tongji = request.data.get('qingqiu_tongji', False) + sqzhuangtai = request.data.get('sqzhuangtai') + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 30)) + + # 验证参数 + if not qianzhanghao: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2. JWT Token身份验证 + dangqianyonghu = request.user + + # 3. 验证手机号是否匹配 + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 4. 验证用户类型是否为管理员 + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 5. 验证管理员扩展表是否存在 + try: + guanliyuan = dangqianyonghu.admin_profile + except AttributeError: + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 7. 获取统计信息(总是需要统计) + zongshu = Chufajilu.objects.count() + daichuli = Chufajilu.objects.filter(sqzhuangtai__in=[0, 3]).count() + yichuli = Chufajilu.objects.filter(sqzhuangtai__in=[1, 2]).count() + + # 8. 如果只请求统计信息,直接返回 + if qingqiu_tongji: + return Response({ + 'code': 0, + 'message': '成功', + 'data': { + 'zongshu': zongshu, + 'daichuli': daichuli, + 'yichuli': yichuli + } + }) + + # 9. 构建查询集 + queryset = Chufajilu.objects.all() + + # 10. 根据状态筛选 + if sqzhuangtai is not None: + if isinstance(sqzhuangtai, list): + # 如果是数组,表示多个状态 + queryset = queryset.filter(sqzhuangtai__in=sqzhuangtai) + else: + # 单个状态 + queryset = queryset.filter(sqzhuangtai=sqzhuangtai) + + # 11. 获取总数(用于分页计算) + total_count = queryset.count() + + # 12. 计算分页偏移量 + offset = (page - 1) * page_size + + # 13. 分页查询 + chufa_list = list(queryset.order_by('-create_time')[offset:offset + page_size]) + + # 🔴【新增】高效获取图片信息 + if chufa_list: + # 收集所有需要查询的组合 + dingdan_ids = [] + yonghu_combinations = [] # 保存 (dingdan_id, yonghuid) 组合 + + for chufa in chufa_list: + if chufa.dingdan_id: + dingdan_ids.append(chufa.dingdan_id) + # 商家图片组合 + if chufa.qingqiuid: + yonghu_combinations.append((chufa.dingdan_id, chufa.qingqiuid)) + # 打手图片组合 + if chufa.dashouid: + yonghu_combinations.append((chufa.dingdan_id, chufa.dashouid)) + + # 去重 + dingdan_ids = list(set(dingdan_ids)) + yonghu_combinations = list(set(yonghu_combinations)) + + # 🔴【新增】批量查询图片表(高效查询,避免循环) + tupian_mapping = {} + if yonghu_combinations: + # 构建查询条件 + query_conditions = Q() + for dingdan_id, yonghuid in yonghu_combinations: + query_conditions |= Q(dingdan_id=dingdan_id, yonghuid=yonghuid) + + # 批量查询 + tupian_queryset = Chufatupian.objects.filter(query_conditions).values( + 'dingdan_id', 'yonghuid', 'tupian' + ) + + # 构建映射:key为"dingdan_id_yonghuid",value为图片URL列表 + for item in tupian_queryset: + key = f"{item['dingdan_id']}_{item['yonghuid']}" + if key not in tupian_mapping: + tupian_mapping[key] = [] + if item['tupian']: + tupian_mapping[key].append(item['tupian']) + else: + tupian_mapping = {} + + # 🔴【修改】14. 构建返回数据 - 添加图片和申诉理由字段 + chufa_data = [] + for chufa in chufa_list: + # 🔴【新增】获取商家证据图片 + zhengju_tupian = [] + if chufa.dingdan_id and chufa.qingqiuid: + key = f"{chufa.dingdan_id}_{chufa.qingqiuid}" + zhengju_tupian = tupian_mapping.get(key, []) + + # 🔴【新增】获取打手申诉图片 + shensu_tupian = [] + if chufa.dingdan_id and chufa.dashouid: + key = f"{chufa.dingdan_id}_{chufa.dashouid}" + shensu_tupian = tupian_mapping.get(key, []) + + chufa_data.append({ + 'dashouid': chufa.dashouid, + 'qingqiuid': chufa.qingqiuid, + 'chuliid': chufa.chuliid, + 'cfliyou': chufa.cfliyou, + 'sqzhuangtai': chufa.sqzhuangtai, + 'bhliyou': chufa.bhliyou, + 'jifen': chufa.jifen, + 'dingdan_id': chufa.dingdan_id, + 'create_time': chufa.create_time, + 'update_time': chufa.update_time, + # 🔴【新增】申诉理由字段 + 'ssliyou': chufa.ssliyou or '', + # 🔴【新增】商家证据图片 + 'zhengju_tupian': zhengju_tupian, + # 🔴【新增】打手申诉图片 + 'shensu_tupian': shensu_tupian, + }) + + # 15. 计算是否还有更多数据 + current_count = len(chufa_list) + haiyougengduo = (offset + current_count) < total_count + + return Response({ + 'code': 0, + 'message': '成功', + 'data': { + 'list': chufa_data, + 'zongshu': zongshu, + 'daichuli': daichuli, + 'yichuli': yichuli, + 'haiyougengduo': haiyougengduo + } + }) + + except ValueError: + return Response( + {'code': 400, 'message': '参数类型错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except Exception as e: + logger.error(f"处罚管理列表接口异常: {str(e)}", exc_info=True) + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + + + + + + + + + + + + + +logger = logging.getLogger(__name__) + + +class AdTongYiChuFa(APIView): + """ + 后台同意/驳回处罚接口(已修复重复记录问题) + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /yonghu/adtycf + 参数: { + "zhanghao": "管理员账号", + "dingdan_id": "订单ID", + "caozuo": 1或2, # 1=同意处罚,2=拒绝处罚 + "bohui_liyou": "拒绝理由" # 拒绝处罚时必填 + } + 返回: { + "code": 0, + "message": "成功", + "data": null + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + # 1. 获取前端参数 + qianzhanghao = request.data.get('zhanghao', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + caozuo = request.data.get('caozuo') # 1=同意,2=拒绝 + bohui_liyou = request.data.get('bohui_liyou', '').strip() + + # 验证参数 + if not qianzhanghao or not dingdan_id or caozuo is None: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证操作类型 + if caozuo not in [1, 2]: + return Response( + {'code': 400, 'message': '操作类型错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 如果是拒绝处罚,需要拒绝理由 + if caozuo == 2 and not bohui_liyou: + return Response( + {'code': 400, 'message': '拒绝处罚需要填写理由', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2. JWT Token身份验证 + dangqianyonghu = request.user + + # 3. 验证手机号是否匹配 + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 4. 验证用户类型是否为管理员 + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 5. 验证管理员扩展表是否存在 + try: + guanliyuan = dangqianyonghu.admin_profile + except AttributeError: + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 6. 使用事务保证数据一致性 + with transaction.atomic(): + # 8. 🔴 修复:查询处罚记录(处理重复记录问题) + # 使用 filter().first() 而不是 get(),以防出现多条记录 + chufajilu = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中状态 + ).order_by('-create_time').first() # 取最新的一条 + + if not chufajilu: + # 如果没有待处理的处罚记录,检查是否有已处理的 + exists_processed = Chufajilu.objects.filter(dingdan_id=dingdan_id).exists() + if exists_processed: + return Response( + {'code': 400, 'message': '该处罚记录已处理', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + else: + return Response( + {'code': 404, 'message': '处罚记录不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 9. 验证处罚记录状态(必须是待处理或申诉中状态才能处理) + if chufajilu.sqzhuangtai not in [0, 3]: # 🔴【修改】包括待处理(0)和申诉中(3) + return Response( + {'code': 400, 'message': '该处罚记录已处理', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 10. 🔴 修复:检查是否有重复的待处理/申诉中记录,并记录警告 + duplicate_count = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中 + ).count() + + if duplicate_count > 1: + # 记录警告日志 + logger.warning(f"订单ID {dingdan_id} 存在 {duplicate_count} 条待处理/申诉中处罚记录,处理最新的一条") + + # 🔴 可选:将其他重复的待处理/申诉中记录标记为已拒绝 + # 防止用户重复提交相同订单的处罚申请 + duplicate_records = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中 + ).exclude(id=chufajilu.id) + + for dup in duplicate_records: + dup.sqzhuangtai = 2 # 已拒绝 + dup.chuliid = qianzhanghao + dup.bhliyou = "系统自动拒绝:存在重复处罚记录" + dup.save() + + # 11. 根据操作类型处理 + if caozuo == 1: + # 同意处罚 - 不扣分,只改状态 + # 11.1 检查打手是否存在 + if not chufajilu.dashouid: + return Response( + {'code': 400, 'message': '被处罚打手ID不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 11.2 查询打手扩展表(只是验证存在性) + try: + dashou = UserDashou.objects.get(user__yonghuid=chufajilu.dashouid) + except UserDashou.DoesNotExist: + return Response( + {'code': 404, 'message': '被处罚打手不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 🔴【修改】11.3 同意处罚时不扣分(因为申请时已经扣了),只更新状态 + chufajilu.sqzhuangtai = 1 # 已处罚 + chufajilu.chuliid = qianzhanghao # 处理人ID为管理员账号 + chufajilu.save() + + # 11.4 更新商家订单扩展表 + try: + dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + if hasattr(dingdan, 'shangjia_kuozhan'): + shangjia_kuozhan = dingdan.shangjia_kuozhan + shangjia_kuozhan.sqzhuangtai = 1 # 已处罚 + shangjia_kuozhan.save() + except (Dingdan.DoesNotExist, AttributeError): + # 如果订单或扩展表不存在,继续执行,不中断 + pass + + logger.info(f"同意处罚成功:订单ID={dingdan_id},打手ID={chufajilu.dashouid}") + + elif caozuo == 2: + # 拒绝处罚 - 需要返还积分(加分) + # 🔴【新增】11.5 返还打手积分(加5分) + if chufajilu.jifen > 0: + try: + dashou = UserDashou.objects.get(user__yonghuid=chufajilu.dashouid) + + # 🔴【新增】安全验证:必须积分小于等于5分才允许加分 + if dashou.jifen <= 5: # 判断积分是否小于等于5分 + dashou.jifen += chufajilu.jifen # 加5分 + dashou.save() + logger.info( + f"拒绝处罚,给打手{dashou.user.yonghuid}返还积分{chufajilu.jifen}分,当前积分{dashou.jifen}") + else: + # 积分已超过5分,可能是异常情况,记录警告但不中断处理 + logger.warning( + f"拒绝处罚时打手积分异常:打手ID={chufajilu.dashouid},当前积分{dashou.jifen}已超过5分,不再加分") + except UserDashou.DoesNotExist: + # 打手不存在,记录错误,但继续处理状态 + logger.error(f"拒绝处罚时打手不存在:打手ID={chufajilu.dashouid}") + + # 11.6 更新处罚记录 + chufajilu.sqzhuangtai = 2 # 已拒绝 + chufajilu.chuliid = qianzhanghao # 处理人ID为管理员账号 + chufajilu.bhliyou = bohui_liyou # 拒绝理由 + chufajilu.save() + + # 11.7 更新商家订单扩展表 + try: + dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + if hasattr(dingdan, 'shangjia_kuozhan'): + shangjia_kuozhan = dingdan.shangjia_kuozhan + shangjia_kuozhan.sqzhuangtai = 2 # 已拒绝 + shangjia_kuozhan.bhliyou = bohui_liyou # 拒绝理由 + shangjia_kuozhan.save() + except (Dingdan.DoesNotExist, AttributeError): + # 如果订单或扩展表不存在,继续执行,不中断 + pass + + logger.info(f"拒绝处罚成功:订单ID={dingdan_id},拒绝理由={bohui_liyou}") + + # 12. 返回成功响应 + return Response({ + 'code': 0, + 'message': '处理成功', + 'data': None + }) + + except ValueError: + return Response( + {'code': 400, 'message': '参数类型错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except Exception as e: + logger.error(f"处理处罚接口异常: {str(e)}", exc_info=True) + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + + +# yonghu/views.py +class AdckyhxqView(APIView): + """ + 获取用户详情接口 + 前端需要传递: zhanghao, uid, user_type + 返回用户信息和会员列表 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # ==================== 管理员身份验证 ==================== + # 1. 验证JWT Token + if not request.user.is_authenticated: + return Response({ + 'code': 401, + 'message': '未认证', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 验证用户类型为管理员 + if request.user.user_type != 'admin': + return Response({ + 'code': 403, + 'message': '权限不足', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 3. 验证管理员扩展表存在 + admin_profile = getattr(request.user, 'admin_profile', None) + if not admin_profile: + return Response({ + 'code': 403, + 'message': '管理员信息不完整', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 4. 验证管理员账号(phone)与前端传递的zhanghao一致 + zhanghao = request.data.get('zhanghao') + if not zhanghao or request.user.phone != zhanghao: + return Response({ + 'code': 401, + 'message': '账号验证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + # ==================== 身份验证结束 ==================== + + # 获取业务参数 + uid = request.data.get('uid') # 用户ID (yonghuid) + user_type = request.data.get('user_type') # 用户类型 (1,2,3,4) + + if not uid or not user_type: + return Response({ + 'code': 400, + 'message': '参数缺失', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证用户类型 + try: + user_type = int(user_type) + if user_type not in [1, 2, 3, 4]: + raise ValueError + except ValueError: + return Response({ + 'code': 400, + 'message': '用户类型无效', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 查询用户主表 - 使用select_related优化查询 + user_main = UserMain.objects.filter(yonghuid=uid).first() + if not user_main: + return Response({ + 'code': 404, + 'message': '用户不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 构建响应数据 + response_data = { + 'user_info': {}, + 'member_list': [], + 'all_member_list': [] + } + + # 通用字段(所有用户都有) + user_info = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar, + 'phone': user_main.phone, + 'ip': user_main.ip, + 'create_time': user_main.create_time, + 'update_time': user_main.update_time, + 'user_type': user_type + } + + # 根据用户类型查询扩展信息 + if user_type == 1: # 老板 + # 使用select_related预取boss_profile + user_main = UserMain.objects.filter(yonghuid=uid).select_related('boss_profile').first() + if user_main.boss_profile: + boss_profile = user_main.boss_profile + user_info.update({ + 'nickname': boss_profile.nickname, + 'zonge': boss_profile.zonge, + 'alldingdan': boss_profile.alldingdan, + 'alltui': boss_profile.alltui, + 'create_time_boss': boss_profile.create_time + }) + + elif user_type == 2: # 打手 + # 使用select_related预取dashou_profile + user_main = UserMain.objects.filter(yonghuid=uid).select_related('dashou_profile').first() + if user_main.dashou_profile: + dashou_profile = user_main.dashou_profile + + # 构建打手信息 + user_info.update({ + 'nicheng': dashou_profile.nicheng, + 'chenghao': dashou_profile.chenghao, + 'zhuangtai': dashou_profile.zhuangtai, + 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, + 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai, + 'jieshao': dashou_profile.jieshao, + 'jiedanzongliang': dashou_profile.jiedanzongliang, + 'chengjiaozongliang': dashou_profile.chengjiaozongliang, + 'tuikuanliang': dashou_profile.tuikuanliang, + 'yue': dashou_profile.yue, # 打手可提现余额 + 'zonge': dashou_profile.zonge, # 打手赚取总额 + 'dianhua': dashou_profile.dianhua, + 'wechat': dashou_profile.wechat, + 'yaoqingren': dashou_profile.yaoqingren, + 'jinrijiedan': dashou_profile.jinrijiedan, + 'jinrishouyi': dashou_profile.jinrishouyi, + 'jinyuejiedan': dashou_profile.jinyuejiedan, + 'jinyueshouyi': dashou_profile.jinyueshouyi, + 'jifen': dashou_profile.jifen, + 'yajin': dashou_profile.yajin, + 'create_time_dashou': dashou_profile.create_time + }) + + # 查询打手的会员信息(只返回未过期的) + # 使用select_related预取huiyuan信息(如果有关联的话) + member_records = Huiyuangoumai.objects.filter( + yonghu_id=uid + ).order_by('-create_time') + + member_list = [] + for record in member_records: + # 调用方法检查是否过期,并更新状态 + is_expired = record.jiance_shifou_daoqi() + + if not is_expired: # 只返回未过期的 + # 查询会员详情 + huiyuan = Huiyuan.objects.filter( + huiyuan_id=record.huiyuan_id + ).first() + + if huiyuan: + member_list.append({ + 'huiyuan_id': record.huiyuan_id, + 'huiyuan_name': huiyuan.jieshao, + 'daoqi_time': record.daoqi_time, + 'is_active': record.huiyuan_zhuangtai == 1, + 'create_time': record.create_time + }) + + response_data['member_list'] = member_list + + elif user_type == 3: # 管事 + # 使用select_related预取guanshi_profile + user_main = UserMain.objects.filter(yonghuid=uid).select_related('guanshi_profile').first() + if user_main.guanshi_profile: + guanshi_profile = user_main.guanshi_profile + user_info.update({ + 'nicheng': user_main.phone, # 管事昵称用手机号 + 'yaoqingma': guanshi_profile.yaoqingma, + 'dianhua': guanshi_profile.dianhua, + 'wechat': guanshi_profile.wechat, + 'yaogingshuliang': guanshi_profile.yaogingshuliang, + 'zhuangtai': guanshi_profile.zhuangtai, + 'jinrichongzhi': guanshi_profile.jinrichongzhi, + 'jinyuechongzhi': guanshi_profile.jinyuechongzhi, + 'chongzhifenrun': guanshi_profile.chongzhifenrun, + 'yue': guanshi_profile.yue, # 管事可提现余额 + 'create_time_guanshi': guanshi_profile.create_time + }) + + elif user_type == 4: # 商家 + # 使用select_related预取shop_profile + user_main = UserMain.objects.filter(yonghuid=uid).select_related('shop_profile').first() + if user_main.shop_profile: + shop_profile = user_main.shop_profile + user_info.update({ + 'nicheng': shop_profile.nicheng, + 'zhuangtai': shop_profile.zhuangtai, + 'dianhua': shop_profile.dianhua, + 'wechat': shop_profile.wechat, + 'fabu': shop_profile.fabu, + 'tuikuan': shop_profile.tuikuan, + 'yue': shop_profile.yue, # 商家余额 + 'chengjiao': shop_profile.chengjiao, + 'jinridingdan': shop_profile.jinridingdan, + 'jinriliushui': shop_profile.jinriliushui, + 'jinyuedingdan': shop_profile.jinyuedingdan, + 'jinyueliushui': shop_profile.jinyueliushui, + 'create_time_shop': shop_profile.create_time + }) + + # 获取所有会员列表(用于前端下拉选择) + all_huiyuan = Huiyuan.objects.all().order_by('jiage') + all_member_list = [ + { + 'id': huiyuan.huiyuan_id, + 'nicheng': huiyuan.jieshao, + 'jiage': huiyuan.jiage, + 'jieshao': huiyuan.jtjieshao + } + for huiyuan in all_huiyuan + ] + + response_data['user_info'] = user_info + response_data['all_member_list'] = all_member_list + + return Response({ + 'code': 0, + 'message': '获取成功', + 'data': response_data + }, status=status.HTTP_200_OK) + + except Exception as e: + return Response({ + 'code': 500, + 'message': f'获取用户详情失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class AdcjxgView(APIView): + """ + 修改用户信息接口 + 前端需要传递: zhanghao, uid, user_type, 以及其他修改的字段 + 支持打手会员添加功能 + 敏感字段修改会记录到修改记录表 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # ==================== 管理员身份验证 ==================== + if not request.user.is_authenticated: + return Response({ + 'code': 401, + 'message': '未认证', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + if request.user.user_type != 'admin': + return Response({ + 'code': 403, + 'message': '权限不足', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + admin_profile = getattr(request.user, 'admin_profile', None) + if not admin_profile: + return Response({ + 'code': 403, + 'message': '管理员信息不完整', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + zhanghao = request.data.get('zhanghao') + if not zhanghao or request.user.phone != zhanghao: + return Response({ + 'code': 401, + 'message': '账号验证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + # ==================== 身份验证结束 ==================== + + # 获取业务参数 + uid = request.data.get('uid') + user_type = request.data.get('user_type') + + if not uid or not user_type: + return Response({ + 'code': 400, + 'message': '参数缺失', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + user_type = int(user_type) + if user_type not in [1, 2, 3, 4]: + raise ValueError + except ValueError: + return Response({ + 'code': 400, + 'message': '用户类型无效', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 查询用户主表 + user_main = UserMain.objects.filter(yonghuid=uid).first() + if not user_main: + return Response({ + 'code': 404, + 'message': '用户不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 使用事务确保数据一致性 + with transaction.atomic(): + # 存储修改记录的数据 + modify_record_data = { + 'yonghuid': uid, + 'xiugaiid': zhanghao, + 'leixing': user_type, + 'qitashuoming': '', + 'create_time': timezone.now() + } + + # 标记是否有敏感字段被修改 + has_sensitive_change = False + + if user_type == 1: # 老板 - 不允许修改 + return Response({ + 'code': 403, + 'message': '老板信息不允许修改', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + elif user_type == 2: # 打手 + # 获取或创建打手扩展记录 + dashou_profile, created = UserDashou.objects.get_or_create( + user=user_main, + defaults={'nicheng': user_main.phone or f'打手{uid}'} + ) + + # 记录敏感字段的旧值 + old_yue = dashou_profile.yue # 打手可提现余额 + old_zonge = dashou_profile.zonge # 打手提现金额 + old_jifen = dashou_profile.jifen # 打手积分 + old_yajin = dashou_profile.yajin # 打手押金 + + # 需要更新的字段列表 + update_fields = [ + 'chenghao', 'dianhua', 'yue', 'zonge', 'jifen', + 'yajin', 'yaoqingren', 'jieshao', 'zhuangtai', + 'zaixianzhuangtai', 'zhanghaozhuangtai' + ] + + # 记录被修改的字段 + modified_fields = [] + + for field in update_fields: + if field in request.data: + new_value = request.data.get(field) + old_value = getattr(dashou_profile, field) + + # 检查值是否改变 + if str(new_value) != str(old_value): + # 处理特殊字段类型 + if field in ['yue', 'zonge', 'yajin']: + new_value = Decimal(str(new_value)) + setattr(dashou_profile, field, new_value) + elif field in ['jifen']: + new_value = int(new_value) + setattr(dashou_profile, field, new_value) + elif field in ['zhuangtai', 'zaixianzhuangtai', 'zhanghaozhuangtai']: + new_value = int(new_value) + setattr(dashou_profile, field, new_value) + else: + setattr(dashou_profile, field, new_value) + + modified_fields.append(field) + + # 记录敏感字段修改 + if field == 'yue': + has_sensitive_change = True + modify_record_data['xiugaitijiao'] = new_value # 使用xiugaitijiao字段记录打手余额修改 + modify_record_data['xiugaitijiaoq'] = old_yue + elif field == 'zonge': + has_sensitive_change = True + # zonge字段在修改记录表中可能没有直接对应,可以记录在备注中 + modify_record_data['qitashuoming'] += f' 打手总额从{old_zonge}修改为{new_value}' + elif field == 'jifen': + has_sensitive_change = True + modify_record_data['jifen'] = new_value + modify_record_data['yjifen'] = old_jifen + elif field == 'yajin': + has_sensitive_change = True + modify_record_data['yajin'] = new_value + modify_record_data['yyajin'] = old_yajin + + # 保存打手信息 + dashou_profile.save() + + # 记录被修改的非敏感字段 + non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'zonge', 'jifen', 'yajin']] + if non_sensitive_fields: + modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}' + + elif user_type == 3: # 管事 + # 获取或创建管事扩展记录 + guanshi_profile, created = UserGuanshi.objects.get_or_create( + user=user_main, + defaults={'yaoqingma': f'GS{uid[-4:]}'} + ) + + # 记录敏感字段的旧值 + old_yue = guanshi_profile.yue # 管事可提现余额 + + update_fields = [ + 'yaogingshuliang', 'chongzhifenrun', 'yue', + 'zhuangtai', 'dianhua', 'wechat' + ] + + modified_fields = [] + + for field in update_fields: + if field in request.data: + new_value = request.data.get(field) + old_value = getattr(guanshi_profile, field) + + if str(new_value) != str(old_value): + if field in ['chongzhifenrun', 'yue']: + new_value = Decimal(str(new_value)) + setattr(guanshi_profile, field, new_value) + elif field in ['yaogingshuliang']: + new_value = int(new_value) + setattr(guanshi_profile, field, new_value) + elif field in ['zhuangtai']: + new_value = int(new_value) + setattr(guanshi_profile, field, new_value) + else: + setattr(guanshi_profile, field, new_value) + + modified_fields.append(field) + + # 记录敏感字段修改 + if field == 'yue': + has_sensitive_change = True + modify_record_data['guanshiyue'] = new_value + modify_record_data['guanshiyueq'] = old_yue + elif field == 'chongzhifenrun': + has_sensitive_change = True + modify_record_data['qitashuoming'] += f' 管事分佣从{old_value}修改为{new_value}' + + guanshi_profile.save() + + # 记录非敏感字段 + non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'chongzhifenrun']] + if non_sensitive_fields: + modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}' + + elif user_type == 4: # 商家 + # 获取或创建商家扩展记录 + shop_profile, created = UserShangjia.objects.get_or_create( + user=user_main, + defaults={'nicheng': user_main.phone or f'商家{uid}'} + ) + + # 记录敏感字段的旧值 + old_yue = shop_profile.yue # 商家余额 + + update_fields = ['yue', 'zhuangtai', 'dianhua', 'wechat'] + + modified_fields = [] + + for field in update_fields: + if field in request.data: + new_value = request.data.get(field) + old_value = getattr(shop_profile, field) + + if str(new_value) != str(old_value): + if field == 'yue': + new_value = Decimal(str(new_value)) + setattr(shop_profile, field, new_value) + elif field == 'zhuangtai': + new_value = int(new_value) + setattr(shop_profile, field, new_value) + else: + setattr(shop_profile, field, new_value) + + modified_fields.append(field) + + # 记录敏感字段修改 + if field == 'yue': + has_sensitive_change = True + modify_record_data['shangjiayue'] = new_value + modify_record_data['shangjiayueq'] = old_yue + + shop_profile.save() + + # 记录非敏感字段 + non_sensitive_fields = [f for f in modified_fields if f != 'yue'] + if non_sensitive_fields: + modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}' + + # 处理打手会员添加(只有打手可以添加会员) + + # 🔥🔥🔥 后端修改:支持正负数会员天数 + # 修改会员处理逻辑部分 + + if user_type == 2 and 'huiyuan_id' in request.data and 'days' in request.data: + huiyuan_id = request.data.get('huiyuan_id') + days = int(request.data.get('days', 0)) + + # 🔥 修改:允许正负数,但不能为0 + if huiyuan_id and days != 0: + # 🔥 修改:检查绝对值范围 + if abs(days) < 1 or abs(days) > 10000: + return Response({ + 'code': 400, + 'message': '会员天数必须在1-10000之间(正数增加,负数减少)', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 检查会员是否存在 + huiyuan = Huiyuan.objects.filter(huiyuan_id=huiyuan_id).first() + if not huiyuan: + return Response({ + 'code': 404, + 'message': '会员不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 检查是否已购买该会员 + existing_record = Huiyuangoumai.objects.filter( + yonghu_id=uid, + huiyuan_id=huiyuan_id + ).first() + + if existing_record: + # 🔥🔥🔥 重要修改:支持减少天数 + # 计算新的到期时间 + new_daoqi_time = existing_record.daoqi_time + timedelta(days=days) + + # 🔥 安全验证:减少天数后不能小于当前时间前90天(防止过度减少) + # 这个限制可以根据业务调整 + min_allowed_time = timezone.now() - timedelta(days=90) + if new_daoqi_time < min_allowed_time: + return Response({ + 'code': 400, + 'message': f'减少天数过多,调整后到期时间不能早于 {(min_allowed_time + timedelta(days=90)).strftime("%Y-%m-%d")}', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + existing_record.daoqi_time = new_daoqi_time + existing_record.save() + + # 记录会员操作 + has_sensitive_change = True + modify_record_data['huiyuants'] = days + modify_record_data['huiyuan_id'] = huiyuan_id + + if days > 0: + modify_record_data['qitashuoming'] += f' 续费会员 {huiyuan.jieshao} {days}天' + # 只有增加天数时才增加购买次数 + huiyuan.goumai_cishu += 1 + huiyuan.save() + else: + modify_record_data['qitashuoming'] += f' 减少会员 {huiyuan.jieshao} {abs(days)}天' + # 减少天数时不增加购买次数 + + else: + # 🔥 修改:新购买时只能增加天数 + if days > 0: + daoqi_time = timezone.now() + timedelta(days=days) + new_record = Huiyuangoumai.objects.create( + huiyuan_id=huiyuan_id, + yonghu_id=uid, + jieshao=huiyuan.jieshao, + daoqi_time=daoqi_time + ) + + # 记录会员购买 + has_sensitive_change = True + modify_record_data['huiyuants'] = days + modify_record_data['huiyuan_id'] = huiyuan_id + modify_record_data['qitashuoming'] += f' 购买会员 {huiyuan.jieshao} {days}天' + + # 更新会员购买次数 + huiyuan.goumai_cishu += 1 + huiyuan.save() + else: + # 🔥 不能为不存在的会员减少天数 + return Response({ + 'code': 400, + 'message': '用户未购买该会员,无法减少天数', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + + + + + + + + + + + # 如果有敏感字段被修改或会员操作,创建修改记录 + if has_sensitive_change or 'huiyuan_id' in request.data: + # 创建修改记录 + Xiugaijilu.objects.create(**modify_record_data) + elif modify_record_data.get('qitashuoming', '').strip(): + # 只有非敏感字段修改,也记录但qitashuoming不同 + Xiugaijilu.objects.create(**modify_record_data) + + return Response({ + 'code': 0, + 'message': '修改成功', + 'data': None + }, status=status.HTTP_200_OK) + + except Exception as e: + return Response({ + 'code': 500, + 'message': f'修改失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +# 管理员认证装饰器 +def admin_required(view_func): + """管理员权限验证装饰器""" + + def wrapped_view(self, request, *args, **kwargs): + try: + # 1. 验证JWT Token + if not request.user.is_authenticated: + return Response({ + 'code': 401, + 'message': '未认证', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 验证用户类型为管理员 + if request.user.user_type != 'admin': + return Response({ + 'code': 403, + 'message': '权限不足', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 3. 验证管理员扩展表存在 + admin_profile = getattr(request.user, 'admin_profile', None) + if not admin_profile: + return Response({ + 'code': 403, + 'message': '管理员信息不完整', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 4. 验证管理员账号(phone)与前端传递的zhanghao一致 + zhanghao = request.data.get('zhanghao') + if not zhanghao or request.user.phone != zhanghao: + return Response({ + 'code': 401, + 'message': '账号验证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + return view_func(self, request, *args, **kwargs) + except Exception as e: + return Response({ + 'code': 500, + 'message': f'权限验证异常: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return wrapped_view + + +class AddtxshView(APIView): + """ + 获取提现审核列表接口 + 与用户管理页面保持一致的分页逻辑 + 支持按用户ID搜索,排序改为按申请时间升序(最早申请排最前) + """ + permission_classes = [IsAuthenticated] + + @admin_required + def post(self, request): + try: + # 获取参数 + zhanghao = request.data.get('zhanghao') + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 5)) + zhuangtai = int(request.data.get('zhuangtai', 1)) + leixing = int(request.data.get('leixing', 1)) + fangshi = request.data.get('fangshi') + + # 🔴【新增】获取搜索参数(用户ID) + search_uid = request.data.get('search_uid', '').strip() + + if page < 1: + page = 1 + + # 计算偏移量(与用户管理页面完全一致) + offset = (page - 1) * page_size + + # 构建查询条件 + query = Q() + + # 状态筛选 + if zhuangtai == 1: + query &= Q(zhuangtai=1) + elif zhuangtai == 2: + query &= Q(zhuangtai__in=[2, 3]) + + # 类型筛选 + query &= Q(leixing=leixing) + + # 收款方式筛选(可选) + if fangshi: + query &= Q(fangshi=int(fangshi)) + + # 🔴【新增】如果搜索了用户ID,添加条件 + if search_uid: + query &= Q(yonghuid=search_uid) + + # 🔴【修改】排序改为按申请时间升序(最早申请排最前) + queryset = Tixianjilu.objects.filter(query).order_by('create_time') + + # 🔴【修改】分页逻辑:如果是搜索模式,不分页,返回所有匹配记录 + if search_uid: + records = list(queryset) + total_count = len(records) + has_more = False # 搜索模式没有更多数据 + else: + total_count = queryset.count() + records = list(queryset[offset:offset + page_size]) + current_count = len(records) + has_more = (offset + current_count) < total_count + + # 获取状态统计(统计与搜索无关,依然按筛选条件统计) + status_counts = { + 'awaiting': Tixianjilu.objects.filter( + zhuangtai=1, + leixing=leixing + ).count(), + 'processed': Tixianjilu.objects.filter( + zhuangtai__in=[2, 3], + leixing=leixing + ).count() + } + + # 构建响应数据 + withdrawal_list = [] + for record in records: + withdrawal_list.append({ + 'id': record.id, + 'yonghuid': record.yonghuid, + 'avatar': record.avatar, + 'phone': record.phone, + 'nicheng': record.nicheng, + 'leixing': record.leixing, + 'zhifu': record.zhifu, + 'skzhanghao': record.skzhanghao, + 'jine': str(record.jine) if record.jine else '0.00', + 'zhuangtai': record.zhuangtai, + 'fangshi': record.fangshi, + 'shenheid': record.shenheid, + 'bhliyou': record.bhliyou, + 'create_time': record.create_time, + 'update_time': record.update_time + }) + + return Response({ + 'code': 0, + 'message': '获取成功', + 'data': { + 'list': withdrawal_list, + 'total_count': total_count, + 'status_counts': status_counts, + 'has_more': has_more + } + }, status=status.HTTP_200_OK) + + except Exception as e: + return Response({ + 'code': 500, + 'message': f'获取提现列表失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + +class AdtixianqkView(APIView): + """ + 处理提现申请接口(已更新:添加收支记录表更新) + """ + permission_classes = [IsAuthenticated] + + @admin_required # 假设你有这个装饰器 + def post(self, request): + try: + zhanghao = request.data.get('zhanghao') + tixian_id = request.data.get('tixian_id') + result = int(request.data.get('result', 0)) + reason = request.data.get('reason', '') + + if not tixian_id: + return Response({ + 'code': 400, + 'message': '提现记录ID不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if result not in [2, 3]: + return Response({ + 'code': 400, + 'message': '操作结果无效', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 如果是拒绝,需要理由 + if result == 3 and not reason: + return Response({ + 'code': 400, + 'message': '拒绝提现需要提供理由', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 获取提现记录 + try: + withdrawal = Tixianjilu.objects.get(id=tixian_id) + except Tixianjilu.DoesNotExist: + return Response({ + 'code': 404, + 'message': '提现记录不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 检查状态,只有待审核的可以处理 + if withdrawal.zhuangtai != 1: + return Response({ + 'code': 400, + 'message': '该提现申请已被处理', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 使用事务确保数据一致性 + with transaction.atomic(): + # 更新提现记录 + withdrawal.zhuangtai = result + withdrawal.shenheid = request.user.yonghuid + + if result == 3: # 拒绝 + withdrawal.bhliyou = reason + + withdrawal.update_time = timezone.now() + withdrawal.save() + + # 🟢 新增:如果同意提现(result=2),更新收支记录表 + if result == 2: + try: + # 查询收支记录表,id=1的记录 + szjilu_record = Szjilu.objects.get(id=1) + + # 获取提现金额 + tixian_jine = withdrawal.jine # 假设提现记录模型中有jine字段 + + update_daily_payout(tixian_jine) + + # 更新收支记录 + szjilu_record.zongsy -= tixian_jine # 总收益减去提现金额 + szjilu_record.zongzc += tixian_jine # 总支出加上提现金额 + szjilu_record.jrzc += tixian_jine # 今日支出加上提现金额 + szjilu_record.update_time = timezone.now() + szjilu_record.save() + + except Szjilu.DoesNotExist: + # 如果收支记录表不存在id=1的记录,创建一个 + szjilu_record = Szjilu.objects.create( + id=1, + zongsy=-tixian_jine if result == 2 else 0, + zongzc=tixian_jine if result == 2 else 0, + jrzc=tixian_jine if result == 2 else 0 + ) + except Exception as e: + # 如果更新收支记录失败,抛出异常,事务会回滚 + raise Exception(f"更新收支记录失败: {str(e)}") + + return Response({ + 'code': 0, + 'message': '处理成功', + 'data': { + 'id': withdrawal.id, + 'zhuangtai': withdrawal.zhuangtai, + 'shenheid': withdrawal.shenheid, + 'update_time': withdrawal.update_time + } + }, status=status.HTTP_200_OK) + + except Exception as e: + return Response({ + 'code': 500, + 'message': f'处理提现失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + +# views.py (yonghu应用) +class DashouZiliaoHuoquView(APIView): + """ + 打手资料获取接口 + URL: /yonghu/dszjxxhq + 方法: POST + 认证: JWT Token + 返回: 打手扩展表信息 + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 使用select_related高效查询,避免N+1问题 + dashou = UserDashou.objects.select_related('user').get(user=request.user) + + # 构建返回数据 + data = { + 'nicheng': dashou.nicheng if dashou.nicheng else '', + 'jieshao': dashou.jieshao if dashou.jieshao else '', + 'dianhua': dashou.dianhua if dashou.dianhua else '', + 'wechat': dashou.wechat if dashou.wechat else '', + 'uid': request.user.yonghuid, # 从主表获取UID + 'avatar': request.user.avatar if request.user.avatar else '', # 头像相对URL + } + + return Response({ + 'code': 200, + 'msg': 'success', + 'data': data + }, status=status.HTTP_200_OK) + + except UserDashou.DoesNotExist: + logger.warning(f'打手扩展表不存在: user_id={request.user.id}') + return Response({ + 'code': 404, + 'msg': '打手信息不存在' + }, status=status.HTTP_404_NOT_FOUND) + + except Exception as e: + logger.error(f'获取打手资料异常: {str(e)}', exc_info=True) + return Response({ + 'code': 500, + 'msg': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + +# views.py (yonghu应用) + +class DashouGengxinView(APIView): + """ + 打手信息更新接口 + URL: /yonghu/dsgxxx + 方法: POST + 认证: JWT Token + 参数: nicheng(昵称), dianhua(电话), wechat(微信), jieshao(介绍) + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 获取请求数据 + nicheng = request.data.get('nicheng', '').strip() + dianhua = request.data.get('dianhua', '').strip() + wechat = request.data.get('wechat', '').strip() + jieshao = request.data.get('jieshao', '').strip() + + # 数据验证 + validation_errors = self.validate_data(nicheng, dianhua, wechat, jieshao) + if validation_errors: + return Response({ + 'code': 400, + 'msg': validation_errors + }, status=status.HTTP_400_BAD_REQUEST) + + # 获取打手扩展表 + try: + dashou = UserDashou.objects.get(user=request.user) + except UserDashou.DoesNotExist: + return Response({ + 'code': 404, + 'msg': '打手信息不存在' + }, status=status.HTTP_404_NOT_FOUND) + + # 更新数据 + update_fields = [] + + if nicheng is not None: + dashou.nicheng = nicheng if nicheng != '' else None + update_fields.append('nicheng') + + if dianhua is not None: + dashou.dianhua = dianhua if dianhua != '' else None + update_fields.append('dianhua') + + if wechat is not None: + dashou.wechat = wechat if wechat != '' else None + update_fields.append('wechat') + + if jieshao is not None: + dashou.jieshao = jieshao if jieshao != '' else None + update_fields.append('jieshao') + + # 保存更新(只更新修改的字段) + if update_fields: + dashou.save(update_fields=update_fields) + + return Response({ + 'code': 200, + 'msg': '更新成功', + 'data': { + 'nicheng': dashou.nicheng or '', + 'dianhua': dashou.dianhua or '', + 'wechat': dashou.wechat or '', + 'jieshao': dashou.jieshao or '' + } + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f'更新打手信息异常: {str(e)}', exc_info=True) + return Response({ + 'code': 500, + 'msg': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def validate_data(self, nicheng, dianhua, wechat, jieshao): + """数据验证方法""" + errors = [] + + # 1. 昵称验证(必填) + if not nicheng: + errors.append('昵称不能为空') + elif len(nicheng) > 20: + errors.append('昵称不能超过20个字符') + + # 2. 手机号验证(非必填,但填写时必须正确) + if dianhua: + # 手机号正则:1开头,11位数字 + phone_pattern = r'^1[3-9]\d{9}$' + if not re.match(phone_pattern, dianhua): + errors.append('手机号格式不正确') + + # 3. 微信号验证(非必填) + if wechat and len(wechat) > 30: + errors.append('微信号不能超过30个字符') + + # 4. 个人介绍验证 + if jieshao and len(jieshao) > 200: + errors.append('个人介绍不能超过200个字符') + + return '; '.join(errors) if errors else None + + + + + + + + +class ShangjiaZhuceView(APIView): + """ + 商家注册接口 (通过管理员邀请码) + 访问路径: /api/yonghu/shangjiahuce/ + 请求方法: POST + 权限要求: JWT Token认证通过的用户 + 请求体: { "inviteCode": "管理员邀请码" } + + 返回字段说明: + - code: 状态码(200成功,其他失败) + - message: 提示信息 + - data: 商家信息数据,包含以下字段(与前端商家页面完全一致): + - nicheng: 商家昵称 + - sjyue: 商家余额(字符串,保留两位小数) + - fabu: 发布订单总数 + - chengjiao: 成交订单总数 + - tuikuan: 退款订单总数 + - jinridingdan: 今日订单 + - jinriliushui: 今日流水(字符串) + - jinyuedingdan: 今月订单 + - jinyueliushui: 今月流水(字符串) + - zhuangtai: 商家状态(1正常) + - dianhua: 商家电话 + - wechat: 商家微信 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取并验证邀请码参数 + yaoqingma = request.data.get('inviteCode') + if not yaoqingma: + return Response({ + 'code': 400, + 'message': '邀请码不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + yaoqingma = yaoqingma.strip() + if len(yaoqingma) > 100: + return Response({ + 'code': 400, + 'message': '邀请码长度超过限制', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + current_user = request.user + + # 2. 检查用户是否已是商家 + try: + shangjia_profile = current_user.shop_profile + # 用户已是商家,直接返回现有信息 + return self.fanhuiXianyouShangjiaXinxi(shangjia_profile, current_user) + except UserShangjia.DoesNotExist: + # 用户不是商家,继续注册流程 + pass + + # 3. 根据邀请码查找对应的管理员 + try: + # 从管理员表查找邀请码 + admin_profile = AdminProfile.objects.select_related('user').get(yaoqingma=yaoqingma) + except AdminProfile.DoesNotExist: + return Response({ + 'code': 404, + 'message': '管理员邀请码无效或不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 4. 核心:在数据库事务中创建商家身份 + try: + with transaction.atomic(): + # 4.1 查询老板扩展表获取老板昵称 + boss_nickname = '普通商家' + try: + boss_profile = current_user.boss_profile + if boss_profile.nickname and boss_profile.nickname.strip(): + boss_nickname = boss_profile.nickname.strip() + except UserBoss.DoesNotExist: + # 老板扩展表不存在,使用默认值 + pass + + # 4.2 创建商家扩展表 + # 使用老板昵称作为商家昵称的基础 + shangjia_nicheng = boss_nickname + + # 检查昵称是否已存在(安全考虑) + if UserShangjia.objects.filter(nicheng=shangjia_nicheng).exists(): + # 如果存在,添加随机数确保唯一 + shangjia_nicheng = f"{boss_nickname}{random.randint(1000, 9999)}" + + # 4.3 创建商家记录 + shop_profile = UserShangjia.objects.create( + user=current_user, + nicheng=shangjia_nicheng, + zhuangtai=1, # 正常状态 + yue=0.00, # 商家余额 + fabu=0, # 发布订单总数 + tuikuan=0, # 退款订单总数 + chengjiao=0, # 成交订单总数 + jinridingdan=0, # 今日订单 + jinriliushui=0.00, # 今日流水 + jinyuedingdan=0, # 今月订单 + jinyueliushui=0.00, # 今月流水 + dianhua='', # 电话 + wechat='', # 微信 + ) + + except Exception as e: + # 事务会自动回滚 + error_detail = traceback.format_exc() + #print(f"商家注册错误详情: {error_detail}") + + return Response({ + 'code': 500, + 'message': f'商家注册过程中发生系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 5. 注册成功,返回商家信息 + fanhui_data = self.zhuangbeiFanhuiShuju(shop_profile, current_user) + return Response({ + 'code': 200, + 'message': '商家注册成功!', + 'data': fanhui_data + }) + + def fanhuiXianyouShangjiaXinxi(self, shangjia_profile, current_user): + """处理已是商家的用户,返回其现有信息""" + fanhui_data = self.zhuangbeiFanhuiShuju(shangjia_profile, current_user) + return Response({ + 'code': 200, + 'message': '您已是商家,返回当前信息。', + 'data': fanhui_data + }) + + def zhuangbeiFanhuiShuju(self, shangjia_profile, current_user): + """ + 准备返回给前端的商家数据 + 字段名必须与前端商家页面完全一致 + """ + data = { + # 🟢 商家基本信息 + 'nicheng': shangjia_profile.nicheng, # 商家昵称 + 'sjyue': format(shangjia_profile.yue, '.2f'), # 商家余额,保留两位小数 + 'fabu': shangjia_profile.fabu, # 发布订单总数 + 'chengjiao': shangjia_profile.chengjiao, # 成交订单总数 + 'tuikuan': shangjia_profile.tuikuan, # 退款订单总数 + 'jinridingdan': shangjia_profile.jinridingdan, # 今日订单 + 'jinriliushui': format(shangjia_profile.jinriliushui, '.2f'), # 今日流水 + 'jinyuedingdan': shangjia_profile.jinyuedingdan, # 今月订单 + 'jinyueliushui': format(shangjia_profile.jinyueliushui, '.2f'), # 今月流水 + 'zhuangtai': shangjia_profile.zhuangtai, # 商家状态 + 'dianhua': shangjia_profile.dianhua or '', # 电话 + 'wechat': shangjia_profile.wechat or '', # 微信 + + # 🟢 可能需要的主表信息 + 'uid': current_user.yonghuid, # 用户ID + } + return data + + + + + + + +# yonghu/ranking_views.py +""" +排行榜查询接口 - 管理员专用 +安全认证:JWT Token + 管理员权限验证 +""" + +# yonghu/ranking_views.py - 修改PaihangbangGuanliQueryView的post方法参数接收 +class PaihangbangGuanliQueryView(APIView): + """ + 排行榜查询接口 - 管理员专用 + 安全要求: + 1. JWT Token认证 + 2. 管理员权限验证 + 3. 参数严格验证 + 4. 返回统一格式 + """ + + # JWT Token认证 + permission_classes = [IsAuthenticated] + + @admin_required + def post(self, request): + """ + POST请求查询排行榜数据 + 参数: + - zhanghao: 管理员账号 + - shenfen: 用户身份(必须,2=打手,3=管事,4=商家) + - zhouqi: 周期类型(必须,1=日榜,2=月榜) + - date: 查询日期(必须,格式:YYYY-MM-DD) + + 返回格式: + { + "code": 0, # 0=成功,非0=失败 + "message": "成功", + "data": { + "list": [...], # 排行榜数据列表 + "query_info": {...} # 查询信息 + } + } + """ + try: + # 1. 获取请求数据 + data = request.data + zhanghao = data.get('zhanghao') + shenfen = data.get('shenfen') + zhouqi = data.get('zhouqi') + date_str = data.get('date') + + # 2. 参数验证 + if not zhanghao: + return Response({ + 'code': 400, + 'message': '管理员账号不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not shenfen: + return Response({ + 'code': 400, + 'message': '用户身份不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not zhouqi: + return Response({ + 'code': 400, + 'message': '周期类型不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not date_str: + return Response({ + 'code': 400, + 'message': '查询日期不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 参数类型和范围验证 + try: + zhouqi_int = int(zhouqi) + shenfen_int = int(shenfen) + date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() + except ValueError as e: + return Response({ + 'code': 400, + 'message': f'参数格式错误: {str(e)}', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证周期类型范围 + if zhouqi_int not in [1, 2]: + return Response({ + 'code': 400, + 'message': '周期类型必须为1(日榜)或2(月榜)', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证用户身份范围 + if shenfen_int not in [2, 3, 4]: + return Response({ + 'code': 400, + 'message': '用户身份必须为2(打手)、3(管事)或4(商家)', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 构建查询条件 + query = Q( + zhouqi=zhouqi_int, + tongji_date=date_obj, + shenfen=shenfen_int + ) + + # 5. 执行查询(按排名升序) + records = RankingRecord.objects.filter(query).order_by('paiming') + + # 6. 格式化返回数据 + data_list = [] + for record in records: + # 根据用户身份确定指标值单位 + zhizhi_danwei = self._get_zhizhi_danwei(shenfen_int) + + data_list.append({ + 'paiming': record.paiming, + 'yonghuid': record.yonghuid, + 'nicheng': record.nicheng, + 'avatar': record.avatar or '', + 'zhizhi': float(record.zhizhi), + 'zhizhi_danwei': zhizhi_danwei, + }) + + # 7. 返回成功响应 + response_data = { + 'code': 0, + 'message': '查询成功', + 'data': { + 'list': data_list, + 'total_count': len(data_list), + 'query_info': { + 'zhouqi': zhouqi_int, + 'zhouqi_text': '日榜' if zhouqi_int == 1 else '月榜', + 'shenfen': shenfen_int, + 'shenfen_text': self._get_shenfen_text(shenfen_int), + 'date': date_str, + } + } + } + + return Response(response_data, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"管理员查询排行榜失败: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': f'查询排行榜失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def _get_shenfen_text(self, shenfen): + """获取身份文本""" + shenfen_map = {2: '打手', 3: '管事', 4: '商家'} + return shenfen_map.get(shenfen, '未知') + + def _get_zhizhi_danwei(self, shenfen): + """获取指标值单位""" + danwei_map = {2: '元', 3: '人', 4: '元'} + return danwei_map.get(shenfen, '') + + + + + + + +# yonghu/ranking_views.py - 添加以下代码 +class PaihangbangRiqiliebiaoView(APIView): + """ + 获取排行榜可查询日期列表 - 管理员专用 + 返回当前排行榜表中存在的日期(日榜和月榜分开) + """ + + # JWT Token认证 + permission_classes = [IsAuthenticated] + + @admin_required + def post(self, request): + """ + POST请求获取可查询日期列表 + 参数: + - zhanghao: 管理员账号(用于验证) + + 返回格式: + { + "code": 0, + "message": "成功", + "data": { + "ribang_dates": ["2024-01-15", "2024-01-14", ...], # 日榜日期列表 + "yuebang_dates": ["2024-01-01", "2023-12-01", ...], # 月榜日期列表 + } + } + """ + try: + # 1. 获取请求数据 + data = request.data + zhanghao = data.get('zhanghao') + + if not zhanghao: + return Response({ + 'code': 400, + 'message': '管理员账号不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询日榜日期(最近3个月内的) + three_months_ago = timezone.now().date() - timedelta(days=90) + + ribang_dates = RankingRecord.objects.filter( + zhouqi=1, # 日榜 + tongji_date__gte=three_months_ago # 只查询最近3个月 + ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date') + + # 3. 查询月榜日期(最近12个月内的) + twelve_months_ago = timezone.now().date() - timedelta(days=365) + + yuebang_dates = RankingRecord.objects.filter( + zhouqi=2, # 月榜 + tongji_date__gte=twelve_months_ago # 只查询最近12个月 + ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date') + + # 4. 格式化日期 + ribang_date_list = [date.strftime('%Y-%m-%d') for date in ribang_dates] + yuebang_date_list = [date.strftime('%Y-%m-%d') for date in yuebang_dates] + + # 5. 返回成功响应 + response_data = { + 'code': 0, + 'message': '获取成功', + 'data': { + 'ribang_dates': ribang_date_list, + 'yuebang_dates': yuebang_date_list, + 'ribang_count': len(ribang_date_list), + 'yuebang_count': len(yuebang_date_list), + } + } + + return Response(response_data, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"获取排行榜日期列表失败: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': f'获取日期列表失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + + + +class WechatLoginAndDashouRegisterView(APIView): + """ + 微信登录并注册打手一体化接口 + 访问路径: /api/yonghu/wdlyhdl + 请求方法: POST + 权限要求: 无需登录 + 请求体: { "code": "微信登录code", "inviteCode": "邀请码" } + + 功能: 用户扫码进入但未登录时,一次性完成微信登录和打手注册 + """ + + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + + def post(self, request): + """ + 处理微信登录并注册打手请求 + """ + try: + # 1. 获取并验证参数 + code = request.data.get('code', '').strip() + yaoqingma = request.data.get('inviteCode', '').strip() + + if not code: + return Response({ + 'code': 1, + 'msg': '微信授权码不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not yaoqingma: + return Response({ + 'code': 2, + 'msg': '邀请码不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if len(yaoqingma) > 100: + return Response({ + 'code': 3, + 'msg': '邀请码长度超过限制', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 获取微信openid + wechat_data = self.get_wechat_openid(code) + if not wechat_data or 'openid' not in wechat_data: + error_msg = wechat_data.get('errmsg', '微信授权失败') + return Response({ + 'code': 4, + 'msg': f'微信登录失败: {error_msg}', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + openid = wechat_data['openid'] + + # 3. 获取客户端真实IP + kehuduan_ip = self.huoquKehuduanIP(request) + + # 4. 开始数据库事务(确保登录和注册的原子性) + with transaction.atomic(): + # 4.1 创建或获取用户(登录逻辑) + user_main, created = UserMain.objects.select_for_update().get_or_create( + openid=openid, + defaults={ + 'yonghuid': self.shengchengYonghuID(), + 'user_type': 'normal', + } + ) + + # 更新用户IP和最后登录时间 + cunchu_ip = kehuduan_ip + user_main.ip = cunchu_ip + user_main.last_login_time = timezone.now() + user_main.save() + + if created: + # 新用户:创建老板扩展表 + UserBoss.objects.create(user=user_main, nickname='微信用户') + + # 4.2 验证邀请码对应的管事(注册逻辑) + try: + # 使用select_related获取管事及其关联的主表用户信息 + guanshi_profile = UserGuanshi.objects.select_related('user').get(yaoqingma=yaoqingma) + except UserGuanshi.DoesNotExist: + # 邀请码无效,但用户已登录,可以返回登录成功但注册失败 + # 这里我们继续执行,但只返回登录信息 + return self.fanhuiZhihouDengluXinxi(user_main, yaoqingma_wuxiao=True) + + # 验证管事状态 + + # 4.3 检查用户是否已是打手 + dashou_exists = UserDashou.objects.filter(user=user_main).exists() + if dashou_exists: + # 用户已是打手,直接返回信息 + dashou_profile = UserDashou.objects.get(user=user_main) + return self.fanhuiDengluZhuceshuju(user_main, dashou_profile) + + # 4.4 核心:为用户创建所有身份(打手、商家、管事) + guanshi_yonghuid = guanshi_profile.user.yonghuid + + # 创建打手扩展表 + dashou_profile = UserDashou.objects.create( + user=user_main, + nicheng='大神', + chenghao='普通大神', + yaoqingren=guanshi_yonghuid, + # 其他字段使用模型默认值 + ) + + # 创建商家扩展表 + + # 创建管事扩展表 + + # 更新原管事的邀请数量 + locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk) + locked_guanshi.yaogingshuliang += 1 + locked_guanshi.save() + + # ========== 新增:管事每日统计(邀请打手) ========== + try: + update_guanshi_daily_by_action( + yonghuid=guanshi_yonghuid, + action=1, # 1 = 邀请打手 + amount=Decimal('0.00') + ) + logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}") + except Exception as e: + logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}") + + # 5. 生成JWT token + refresh = RefreshToken.for_user(user_main) + token = str(refresh.access_token) + + # 6. 准备返回数据 + response_data = self.zhuangbeiFanhuiShuju(user_main, dashou_profile, token) + + return Response({ + 'code': 0, + 'msg': '登录并注册成功!已开通打手、管事等身份。', + 'data': response_data + }) + + except Exception as e: + # 记录错误日志 + error_openid = locals().get('openid', 'N/A') + logger.error( + f"微信登录注册一体化接口异常 - openid: {error_openid}, " + f"错误类型: {type(e).__name__}, 错误详情: {str(e)}", + exc_info=True + ) + + # 返回友好错误信息 + return Response({ + 'code': 99, + 'msg': '系统繁忙,请稍后重试', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def get_wechat_openid(self, code): + """ + 调用微信接口获取openid + """ + try: + appid = getattr(settings, 'WEIXIN_APPID', '') + secret = getattr(settings, 'WEIXIN_SECRET', '') + + if not appid or not secret: + raise ValueError('微信配置未设置') + + url = 'https://api.weixin.qq.com/sns/jscode2session' + params = { + 'appid': appid, + 'secret': secret, + 'js_code': code, + 'grant_type': 'authorization_code' + } + + response = requests.get(url, params=params, timeout=10) + result = response.json() + + if 'openid' in result: + return result + else: + errcode = result.get('errcode', 'unknown') + errmsg = result.get('errmsg', '未知错误') + return {'errmsg': f'[{errcode}]{errmsg}'} + + except requests.exceptions.Timeout: + return {'errmsg': '请求微信接口超时'} + except Exception as e: + return {'errmsg': f'请求微信接口异常: {str(e)}'} + + def shengchengYonghuID(self): + """ + 生成7位数字用户ID + """ + for _ in range(10): + timestamp_part = str(int(time.time()))[-5:].zfill(5) + random_part = str(random.randint(0, 99)).zfill(2) + user_id = timestamp_part + random_part + + if len(user_id) == 7 and user_id.isdigit(): + if not UserMain.objects.filter(yonghuid=user_id).exists(): + return user_id + + raise Exception('生成用户ID失败') + + def huoquKehuduanIP(self, request): + """ + 获取客户端真实IP地址 + """ + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0].strip() + if ip: + return ip + + ip = request.META.get('REMOTE_ADDR', '') + return ip if ip else '0.0.0.0' + + def huoquQunPeizhi(self): + """ + 获取群配置信息 + """ + try: + qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) + + result = { + 'dashouqun': '', + 'dashouqunid': '', + 'guanshiqun': '', + 'guanshiqunid': '' + } + + for config in qun_configs: + if config.id == 1: + result['dashouqun'] = config.neirong or '' + result['dashouqunid'] = config.qunid or '' + elif config.id == 2: + result['guanshiqun'] = config.neirong or '' + result['guanshiqunid'] = config.qunid or '' + + return result + + except Exception: + return { + 'dashouqun': '', + 'dashouqunid': '', + 'guanshiqun': '', + 'guanshiqunid': '' + } + + def fanhuiZhihouDengluXinxi(self, user_main, yaoqingma_wuxiao=False, guanshi_jinyong=False): + """ + 返回只登录成功的信息(当邀请码无效或管事禁用时) + """ + # 获取老板昵称 + boss_nickname = '微信用户' + try: + boss_profile = UserBoss.objects.get(user=user_main) + if boss_profile.nickname: + boss_nickname = boss_profile.nickname + except UserBoss.DoesNotExist: + pass + + # 检查各身份状态 + dashou_status = 1 if UserDashou.objects.filter(user=user_main).exists() else 0 + shangjia_status = 1 if UserShangjia.objects.filter(user=user_main).exists() else 0 + guanshi_status = 1 if UserGuanshi.objects.filter(user=user_main).exists() else 0 + + # 生成token + refresh = RefreshToken.for_user(user_main) + token = str(refresh.access_token) + + # 获取群配置 + group_info = self.huoquQunPeizhi() + + # 构建返回数据 + response_data = { + 'token': token, + 'nicheng': boss_nickname, + 'uid': user_main.yonghuid, + 'touxiang': user_main.avatar or '', + 'shangjiastatus': shangjia_status, + 'dashoustatus': dashou_status, + 'guanshistatus': guanshi_status, + # 🟢 注意:这里没有返回dingdantiaoshu(订单数量),与前端需求一致 + 'dashouqun': group_info.get('dashouqun', ''), + 'dashouqunid': group_info.get('dashouqunid', ''), + 'guanshiqun': group_info.get('guanshiqun', ''), + 'guanshiqunid': group_info.get('guanshiqunid', '') + } + + # 根据情况设置返回消息 + if yaoqingma_wuxiao: + msg = '登录成功,但邀请码无效' + elif guanshi_jinyong: + msg = '登录成功,但邀请码对应的管事账号已被禁用' + else: + msg = '登录成功' + + return Response({ + 'code': 0, + 'msg': msg, + 'data': response_data + }) + + def fanhuiDengluZhuceshuju(self, user_main, dashou_profile): + """ + 返回登录并注册的数据(用户已是打手的情况) + """ + # 获取老板昵称 + boss_nickname = '微信用户' + try: + boss_profile = UserBoss.objects.get(user=user_main) + if boss_profile.nickname: + boss_nickname = boss_profile.nickname + except UserBoss.DoesNotExist: + pass + + # 检查各身份状态(这里用户已是打手,所以都是1) + dashou_status = 1 + shangjia_status = 1 if UserShangjia.objects.filter(user=user_main).exists() else 1 # 确保是1 + guanshi_status = 1 if UserGuanshi.objects.filter(user=user_main).exists() else 1 # 确保是1 + + # 生成token + refresh = RefreshToken.for_user(user_main) + token = str(refresh.access_token) + + # 获取群配置 + group_info = self.huoquQunPeizhi() + + # 准备打手信息 + dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile) + + # 合并所有数据 + response_data = { + 'token': token, + 'nicheng': boss_nickname, + 'uid': user_main.yonghuid, + 'touxiang': user_main.avatar or '', + 'shangjiastatus': shangjia_status, + 'dashoustatus': dashou_status, + 'guanshistatus': guanshi_status, + 'dashouqun': group_info.get('dashouqun', ''), + 'dashouqunid': group_info.get('dashouqunid', ''), + 'guanshiqun': group_info.get('guanshiqun', ''), + 'guanshiqunid': group_info.get('guanshiqunid', ''), + # 合并打手信息 + **dashou_data + } + + return Response({ + 'code': 0, + 'msg': '您已是打手,登录成功', + 'data': response_data + }) + + def zhuangbeiFanhuiShuju(self, user_main, dashou_profile, token): + """ + 准备完整的返回数据(登录+注册成功的情况) + """ + # 获取老板昵称 + boss_nickname = '微信用户' + try: + boss_profile = UserBoss.objects.get(user=user_main) + if boss_profile.nickname: + boss_nickname = boss_profile.nickname + except UserBoss.DoesNotExist: + pass + + # 身份状态(刚注册成功,应该都是1) + dashou_status = 1 + shangjia_status = 1 + guanshi_status = 1 + + # 获取群配置 + group_info = self.huoquQunPeizhi() + + # 准备打手信息 + dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile) + + # 合并所有数据 + response_data = { + 'token': token, + 'nicheng': boss_nickname, + 'uid': user_main.yonghuid, + 'touxiang': user_main.avatar or '', + 'shangjiastatus': shangjia_status, + 'dashoustatus': dashou_status, + 'guanshistatus': guanshi_status, + 'dashouqun': group_info.get('dashouqun', ''), + 'dashouqunid': group_info.get('dashouqunid', ''), + 'guanshiqun': group_info.get('guanshiqun', ''), + 'guanshiqunid': group_info.get('guanshiqunid', ''), + # 合并打手信息 + **dashou_data + } + + return response_data + + def zhuangbeiDashouFanhuiShuju(self, dashou_profile): + """ + 准备打手返回数据(字段名严格匹配前端) + """ + # 构建基础信息 + data = { + 'dashounicheng': dashou_profile.nicheng, + 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, + 'yongjin': str(dashou_profile.yue), # Decimal转字符串 + 'zonge': str(dashou_profile.zonge), + 'yajin': str(dashou_profile.yajin), + 'chenghao': dashou_profile.chenghao, + 'jinfen': dashou_profile.jifen, + 'chengjiaoliang': dashou_profile.chengjiaozongliang, + 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, + 'dashouzhuangtai': dashou_profile.zhuangtai, + } + + # 查询并处理会员列表 (clumber) + # 使用当前用户的yonghuid进行查询 + huiyuan_goumai_list = [] + goumai_records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid) + for record in goumai_records: + # 调用方法检查是否到期,此方法内部会更新状态 + shifou_daoqi = record.jiance_shifou_daoqi() + huiyuan_goumai_list.append({ + 'huiyuan_id': record.huiyuan_id, + 'huiyuan_zhuangtai': record.huiyuan_zhuangtai, + 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None, + 'shifou_daoqi': shifou_daoqi, + 'jieshao': record.jieshao + }) + + data['clumber'] = huiyuan_goumai_list + return data + +class YonghuTixianShenheXiugaiView(APIView): + """ + 用户修改审核中提现申请接口 + 访问路径: /api/yonghu/yonghutxshxg + 请求方法: POST + 权限要求: JWT Token认证通过的用户 + 请求体: multipart/form-data 格式 + 支持字段: tixian_id, fangshi, txdianhua, txzh, file (图片文件) + + 功能: 允许用户修改审核中提现申请的收款方式、电话、账号、收款码等 + """ + + permission_classes = [IsAuthenticated] + parser_classes = (MultiPartParser, FormParser, JSONParser) + + def post(self, request): + """ + 处理用户修改审核中提现申请的POST请求 + """ + try: + # 1. 获取当前用户 + user_main = request.user + current_yonghuid = user_main.yonghuid + + # 2. 验证必填字段:提现记录ID + tixian_id = request.data.get('tixian_id') + if not tixian_id: + return Response({ + 'code': 1, + 'msg': '提现记录ID不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 查询提现记录(使用select_for_update确保事务安全) + with transaction.atomic(): + try: + tixian_record = Tixianjilu.objects.select_for_update().get( + id=tixian_id, + yonghuid=current_yonghuid # 确保只能修改自己的记录 + ) + except Tixianjilu.DoesNotExist: + return Response({ + 'code': 2, + 'msg': '提现记录不存在或无权修改', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 4. 验证提现记录状态(必须是审核中) + if tixian_record.zhuangtai != 1: + return Response({ + 'code': 3, + 'msg': '只有审核中的提现申请可以修改', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 5. 获取并验证提现方式 + fangshi = request.data.get('fangshi') + if fangshi: + fangshi = int(fangshi) + if fangshi not in [1, 2]: + return Response({ + 'code': 4, + 'msg': '提现方式无效,请选择1(微信)或2(支付宝)', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 检查提现类型与方式的匹配(如果前端提供了leixing) + leixing = request.data.get('leixing') + if leixing: + leixing = int(leixing) + # 这里可以根据业务逻辑添加类型与方式的验证 + # 例如:某些类型只能使用某些方式 + + # 6. 验证手机号(必填) + txdianhua = request.data.get('txdianhua', '').strip() + if not txdianhua: + return Response({ + 'code': 5, + 'msg': '收款人电话不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证手机号格式 + if not txdianhua.isdigit() or len(txdianhua) != 11: + return Response({ + 'code': 6, + 'msg': '请输入11位有效的手机号码', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 7. 获取收款账号(可选) + txzh = request.data.get('txzh', '').strip() + + # 8. 处理收款码图片上传(如果有) + txtupian_file = request.FILES.get('file') + new_zhifu_url = None + old_zhifu_url = tixian_record.zhifu + + if txtupian_file: + # 验证图片 + is_valid, error_msg = validate_image(txtupian_file) + if not is_valid: + return Response({ + 'code': 7, + 'msg': f'图片验证失败: {error_msg}', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 上传收款码图片 + new_zhifu_url = self.handle_shoukuanma_upload( + user_main, + txtupian_file, + old_zhifu_url, + tixian_record.leixing + ) + + if not new_zhifu_url: + return Response({ + 'code': 8, + 'msg': '收款码上传失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 9. 验证收款信息(账号和图片至少有一个) + if not txzh and not (new_zhifu_url or old_zhifu_url): + return Response({ + 'code': 9, + 'msg': '请填写收款账号或上传收款码', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 10. 更新提现记录 + update_fields = [] + + # 更新提现方式(如果有) + if fangshi: + tixian_record.fangshi = fangshi + update_fields.append('fangshi') + + # 更新手机号(必填) + tixian_record.phone = txdianhua + update_fields.append('phone') + + # 更新收款账号(如果有) + if txzh is not None: # 注意:空字符串也要更新 + tixian_record.skzhanghao = txzh + update_fields.append('skzhanghao') + + # 更新收款码图片(如果有新图片) + if new_zhifu_url: + tixian_record.zhifu = new_zhifu_url + update_fields.append('zhifu') + + # 保存更新 + if update_fields: + tixian_record.save(update_fields=update_fields) + + # 11. 构建返回数据 + response_data = { + 'tixian_id': tixian_record.id, + 'fangshi': tixian_record.fangshi, + 'txdianhua': tixian_record.phone, + 'txzh': tixian_record.skzhanghao or '', + 'txtupian': tixian_record.zhifu or '' # 返回相对URL + } + + # 12. 返回成功响应 + return Response({ + 'code': 0, + 'msg': '提现申请修改成功', + 'data': response_data + }) + + except Exception as e: + # 记录详细错误日志 + logger.error( + f"修改提现申请异常 - 用户ID: {getattr(user_main, 'yonghuid', 'N/A')}, " + f"提现ID: {tixian_id}, " + f"错误类型: {type(e).__name__}, " + f"错误详情: {str(e)}", + exc_info=True + ) + + # 返回友好错误信息 + return Response({ + 'code': 99, + 'msg': '系统繁忙,请稍后重试', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def handle_shoukuanma_upload(self, user_main, image_file, old_zhifu_url, leixing=None): + """ + 处理收款码图片上传到腾讯云COS + 存储路径: tixian/{yonghuid}/{timestamp_uuid}.扩展名 + + 参数: + - user_main: 当前用户对象 + - image_file: 图片文件对象 + - old_zhifu_url: 旧收款码URL(用于删除) + - leixing: 提现类型(可选,用于文件夹分类) + + 返回: 新的图片相对路径 + """ + try: + yonghuid = user_main.yonghuid + if not yonghuid: + return None + + # 删除旧收款码(如果存在且不是空字符串) + if old_zhifu_url and old_zhifu_url.strip(): + delete_from_oss(old_zhifu_url) + + # 获取文件扩展名 + file_name = image_file.name + file_extension = os.path.splitext(file_name)[1].lower() + if not file_extension: + file_extension = '.jpg' + + # 生成唯一文件名(使用时间戳+UUID确保不重复) + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + unique_id = str(uuid.uuid4())[:8] # 取UUID前8位 + unique_file_name = f"{timestamp}_{unique_id}{file_extension}" + + # 根据提现类型构建文件夹路径(如果提供了leixing) + if leixing: + # leixing=1:打手佣金, leixing=2:管事分红 + type_folder = 'dashou' if leixing == 1 else 'guanshi' + oss_file_path = f"tixian/{type_folder}/{yonghuid}/{unique_file_name}" + else: + # 默认路径 + oss_file_path = f"tixian/{yonghuid}/{unique_file_name}" + + # 上传新收款码到OSS + new_shoukuanma_url = upload_to_oss(image_file, oss_file_path) + + if new_shoukuanma_url: + # 返回相对路径(从tixian文件夹开始) + return oss_file_path + else: + return None + + except Exception as e: + print(f"处理收款码上传失败: {e}") + return None + + +# views.py +class DashouPermission: + """ + 打手权限验证类 + 确保用户是打手且账号状态正常 + """ + + def has_permission(self, request, view): + user = request.user + + # 检查用户是否已认证 + if not user.is_authenticated: + return False + + try: + # 通过反向关系获取打手扩展信息 + dashou_profile = user.dashou_profile + + # 检查账号状态是否为1(正常) + if dashou_profile.zhanghaozhuangtai != 1: + return False + + return True + except UserDashou.DoesNotExist: + return False + except Exception as e: + logger.error(f"打手权限验证异常: {e}") + return False + + + + +class ChufaJiluHuoquView(APIView): + """ + 打手获取处罚记录接口 + 路径: /yonghu/dshqcfjl + 方法: POST + 权限: JWT Token + 打手权限验证 + """ + permission_classes = [IsAuthenticated, DashouPermission] + + def post(self, request): + try: + # 获取当前用户信息 + user_main = request.user + yonghuid = user_main.yonghuid + + # 通过反向关系获取打手扩展信息 + try: + dashou_profile = user_main.dashou_profile + # 再次确认账号状态 + if dashou_profile.zhanghaozhuangtai != 1: + return Response({ + 'code': 3, + 'msg': '账号已被封禁,无法查看处罚记录', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + except UserDashou.DoesNotExist: + return Response({ + 'code': 2, + 'msg': '用户不是打手身份', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 判断请求类型:统计信息 or 记录列表 + qingqiu_tongji = request.data.get('qingqiu_tongji', False) + + if qingqiu_tongji: + # 返回统计信息 + return self.get_tongji_info(yonghuid) + else: + # 返回处罚记录列表 + return self.get_chufa_list(yonghuid, request.data) + + except Exception as e: + logger.error(f"获取处罚记录异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True) + return Response({ + 'code': 99, + 'msg': '系统繁忙,请稍后重试', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def get_tongji_info(self, yonghuid): + """ + 获取统计信息(总记录、待处理、已处理) + """ + try: + # 使用单个查询获取所有统计信息 + stats = Chufajilu.objects.filter(dashouid=yonghuid).aggregate( + # 总记录数 + zongshu=Count('id'), + + # 待处理数:状态为0(待处罚)和3(申诉中) + daichuli=Count( + Case( + When(Q(sqzhuangtai=0) | Q(sqzhuangtai=3), then=1), + output_field=IntegerField() + ) + ), + + # 已处理数:状态为1(已处罚)和2(已驳回) + yichuli=Count( + Case( + When(Q(sqzhuangtai=1) | Q(sqzhuangtai=2), then=1), + output_field=IntegerField() + ) + ) + ) + + # 如果查询结果为None,则设置为0 + tongji_data = { + 'zongshu': stats.get('zongshu', 0) or 0, + 'daichuli': stats.get('daichuli', 0) or 0, + 'yichuli': stats.get('yichuli', 0) or 0 + } + + return Response({ + 'code': 0, + 'msg': '获取统计信息成功', + 'data': tongji_data + }) + + except Exception as e: + logger.error(f"获取统计信息异常 - 用户ID: {yonghuid}, 错误: {str(e)}") + return Response({ + 'code': 1, + 'msg': '获取统计信息失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def get_chufa_list(self, yonghuid, request_data): + """ + 获取处罚记录列表(高性能批量查询) + 🔴 只修改图片查询逻辑,字段保持原样 + """ + try: + # 获取分页参数 + page = int(request_data.get('page', 1)) + page_size = int(request_data.get('page_size', 10)) + + # 限制最大每页数量,防止恶意请求 + if page_size > 50: + page_size = 50 + if page_size < 1: + page_size = 10 + + # 获取状态筛选参数 + zhuangtai = request_data.get('zhuangtai', 0) + + # 🔴 构建查询条件 - 打手端根据dashouid查询 + queryset = Chufajilu.objects.filter(dashouid=yonghuid) + + if zhuangtai == 0: + # 待处理:状态为0(待处罚)和3(申诉中) + queryset = queryset.filter(Q(sqzhuangtai=0) | Q(sqzhuangtai=3)) + elif zhuangtai == 1: + # 已处理:状态为1(已处罚)和2(已驳回) + queryset = queryset.filter(Q(sqzhuangtai=1) | Q(sqzhuangtai=2)) + else: + # 默认返回所有状态 + pass + + # 按创建时间倒序排列(最新在前) + queryset = queryset.order_by('-create_time') + + # 使用游标分页,避免传统分页的性能问题 + paginator = Paginator(queryset, page_size) + + try: + page_obj = paginator.page(page) + except: + # 页码超出范围,返回空列表 + return Response({ + 'code': 0, + 'msg': '获取成功', + 'data': { + 'list': [], + 'has_more': False, + 'page': page, + 'page_size': page_size, + 'current_count': 0 + } + }) + + # 获取当前页的记录 + chufa_records = list(page_obj) + + if not chufa_records: + # 没有数据直接返回 + return Response({ + 'code': 0, + 'msg': '获取成功', + 'data': { + 'list': [], + 'has_more': False, + 'page': page, + 'page_size': page_size, + 'current_count': 0 + } + }) + + # 🔴【核心修复】收集所有需要查询图片的组合 + dingdan_ids = [] + shangjia_ids = [] + + for record in chufa_records: + if record.dingdan_id: + dingdan_ids.append(record.dingdan_id) + if record.qingqiuid: # 商家ID + shangjia_ids.append(record.qingqiuid) + + # 去重 + dingdan_ids = list(set(dingdan_ids)) + shangjia_ids = list(set(shangjia_ids)) + + # 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误) + # 1. 查询证据图片(商家上传的) + zhengju_mapping = {} + if dingdan_ids and shangjia_ids: + # 构建Q对象:dingdan_id在列表中 AND yonghuid在商家ID列表中 + zhengju_q = Q(dingdan_id__in=dingdan_ids, yonghuid__in=shangjia_ids) + zhengju_queryset = Chufatupian.objects.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian') + + for item in zhengju_queryset: + key = f"{item['dingdan_id']}_{item['yonghuid']}" + if key not in zhengju_mapping: + zhengju_mapping[key] = [] + if item['tupian']: + zhengju_mapping[key].append(item['tupian']) + + # 2. 查询申诉图片(打手上传的) + shensu_mapping = {} + if dingdan_ids: + # 构建Q对象:dingdan_id在列表中 AND yonghuid=当前打手ID + shensu_q = Q(dingdan_id__in=dingdan_ids, yonghuid=yonghuid) + shensu_queryset = Chufatupian.objects.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian') + + for item in shensu_queryset: + key = f"{item['dingdan_id']}_{item['yonghuid']}" + if key not in shensu_mapping: + shensu_mapping[key] = [] + if item['tupian']: + shensu_mapping[key].append(item['tupian']) + + # 🔴【核心修复】组装返回数据 - 字段保持原样! + chufa_list = [] + for record in chufa_records: + dingdan_id = record.dingdan_id + shangjia_id = record.qingqiuid + + # 获取证据图片(商家上传的) + zhengju_tupian = [] + if dingdan_id and shangjia_id: + key = f"{dingdan_id}_{shangjia_id}" + zhengju_tupian = zhengju_mapping.get(key, []) + + # 获取申诉图片(打手上传的) + shensu_tupian = [] + if dingdan_id: + key = f"{dingdan_id}_{yonghuid}" + shensu_tupian = shensu_mapping.get(key, []) + + # 🔴【字段保持原样!】与前端完全对应! + chufa_data = { + 'id': record.id, + 'dingdan_id': record.dingdan_id or '', + 'qingqiuid': record.qingqiuid or '', # 商家ID + 'cfliyou': record.cfliyou or '', + 'sqzhuangtai': record.sqzhuangtai or 0, + 'ssliyou': record.ssliyou or '', + 'jifen': record.jifen or 0, + 'create_time': record.create_time.strftime('%Y-%m-%d %H:%M:%S') if record.create_time else '', + 'update_time': record.update_time.strftime('%Y-%m-%d %H:%M:%S') if record.update_time else '', + 'zhengju_tupian': zhengju_tupian, # 商家上传的证据图片 + 'shensu_tupian': shensu_tupian, # 打手上传的申诉图片 + } + chufa_list.append(chufa_data) + + # 判断是否还有更多数据 + has_more = page_obj.has_next() + + # 构建响应数据 + response_data = { + 'list': chufa_list, + 'has_more': has_more, + 'page': page, + 'page_size': page_size, + 'current_count': len(chufa_list) + } + + return Response({ + 'code': 0, + 'msg': '获取成功', + 'data': response_data + }) + + except ValueError as e: + logger.error(f"参数错误 - 用户ID: {yonghuid}, 错误: {str(e)}") + return Response({ + 'code': 4, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + logger.error(f"获取处罚记录列表异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True) + return Response({ + 'code': 5, + 'msg': '获取记录列表失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + +class DashouShensuView(APIView): + """ + 打手申诉接口 + 路径: /yonghu/dscfss + 方法: POST + 权限: JWT Token + 打手权限验证 + 功能: 打手对处罚进行申诉 + """ + permission_classes = [IsAuthenticated, DashouPermission] + + def post(self, request): + try: + # 获取当前用户信息 + user_main = request.user + yonghuid = user_main.yonghuid + + # 🔴🆕【关键】验证打手账号状态 + try: + dashou_profile = user_main.dashou_profile + if dashou_profile.zhanghaozhuangtai != 1: + return Response({ + 'code': 3, + 'msg': '账号已被封禁,无法申诉', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + except UserDashou.DoesNotExist: + return Response({ + 'code': 2, + 'msg': '用户不是打手身份', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 🔴🆕【关键】获取请求参数 + chufa_id = request.data.get('chufa_id') # 处罚记录ID + shensu_liyou = request.data.get('shensu_liyou', '').strip() # 申诉理由 + shensu_tupian_urls = request.data.get('shensu_tupian_urls', []) # 申诉图片URL数组 + + # 验证必填字段 + if not chufa_id: + return Response({ + 'code': 6, + 'msg': '处罚记录ID不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not shensu_liyou: + return Response({ + 'code': 7, + 'msg': '申诉理由不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证申诉理由长度 + if len(shensu_liyou) > 500: + return Response({ + 'code': 8, + 'msg': '申诉理由不能超过500字', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证图片数量 + if len(shensu_tupian_urls) > 9: + return Response({ + 'code': 9, + 'msg': '最多只能上传9张图片', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 🔴🆕【关键】使用事务确保数据一致性 + + with transaction.atomic(): + # 1. 查询处罚记录 + try: + chufa_record = Chufajilu.objects.select_for_update().get( + id=chufa_id, + dashouid=yonghuid # 确保只能申诉自己的处罚记录 + ) + except Chufajilu.DoesNotExist: + return Response({ + 'code': 10, + 'msg': '处罚记录不存在或无权申诉', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 2. 验证处罚记录状态(必须是待处理状态0) + if chufa_record.sqzhuangtai != 0: + return Response({ + 'code': 11, + 'msg': '该处罚记录已处理,无法申诉', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 验证订单是否存在(可选,根据需求决定) + # 这里可以根据dingdan_id去查询订单表,但根据需求描述,我们已经有了处罚记录 + # 处罚记录中包含了dingdan_id,如果dingdan_id不存在,说明订单可能被删除 + # 但根据业务逻辑,有处罚记录就应该有订单,所以这里可以跳过这个检查 + + # 4. 检查是否已有申诉记录(防止重复申诉) + if chufa_record.ssliyou or chufa_record.sqzhuangtai == 3: + # 检查是否已经有申诉图片 + existing_shensu_tupian = Chufatupian.objects.filter( + dingdan_id=chufa_record.dingdan_id, + yonghuid=yonghuid + ).exists() + + if existing_shensu_tupian: + return Response({ + 'code': 12, + 'msg': '您已经提交过申诉,请等待处理结果', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 5. 更新处罚记录 + chufa_record.ssliyou = shensu_liyou + chufa_record.sqzhuangtai = 3 # 状态改为申诉中 + chufa_record.save() + + # 6. 保存申诉图片(如果有) + saved_tupian_urls = [] + if shensu_tupian_urls: + for tupian_url in shensu_tupian_urls: + if tupian_url: # 确保URL不为空 + chufa_tupian = Chufatupian.objects.create( + dingdan_id=chufa_record.dingdan_id, + yonghuid=yonghuid, + tupian=tupian_url + ) + saved_tupian_urls.append(tupian_url) + + # 7. 构建返回数据 + response_data = { + 'chufa_id': chufa_record.id, + 'dingdan_id': chufa_record.dingdan_id or '', + 'shensu_liyou': shensu_liyou, + 'shensu_tupian_urls': saved_tupian_urls, + 'sqzhuangtai': chufa_record.sqzhuangtai, + 'update_time': chufa_record.update_time.strftime( + '%Y-%m-%d %H:%M:%S') if chufa_record.update_time else '' + } + + # 记录操作日志 + logger.info(f"打手申诉成功 - 用户ID: {yonghuid}, 处罚ID: {chufa_id}, 订单ID: {chufa_record.dingdan_id}") + + return Response({ + 'code': 0, + 'msg': '申诉提交成功', + 'data': response_data + }) + + except Exception as e: + logger.error(f"打手申诉异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True) + return Response({ + 'code': 99, + 'msg': '系统繁忙,请稍后重试', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + +class ShangjiaChufaJiluHuoquView(APIView): + """ + 商家获取处罚记录接口 + 路径: /yonghu/sjcfjlhq + 方法: POST + 权限: JWT Token + 商家权限验证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 获取当前用户信息 + user_main = request.user + yonghuid = user_main.yonghuid + + # 通过反向关系获取商家扩展信息 + try: + shangjia_profile = user_main.shop_profile + # 再次确认账号状态 + if shangjia_profile.zhuangtai != 1: + return Response({ + 'code': 3, + 'msg': '商家账号状态异常,无法查看处罚记录', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + except UserShangjia.DoesNotExist: + return Response({ + 'code': 2, + 'msg': '用户不是商家身份', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 判断请求类型 + qingqiu_tongji = request.data.get('qingqiu_tongji', False) + + if qingqiu_tongji: + return self.get_tongji_info(yonghuid) + else: + return self.get_chufa_list(yonghuid, request.data) + + except Exception as e: + logger.error(f"商家获取处罚记录异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True) + return Response({ + 'code': 99, + 'msg': '系统繁忙,请稍后重试', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def get_tongji_info(self, yonghuid): + """ + 获取统计信息 + """ + try: + # 🔴 查询条件:qingqiuid = 当前商家ID + stats = Chufajilu.objects.filter(qingqiuid=yonghuid).aggregate( + zongshu=Count('id'), + daichuli=Count( + Case( + When(Q(sqzhuangtai=0) | Q(sqzhuangtai=3), then=1), + output_field=IntegerField() + ) + ), + yichuli=Count( + Case( + When(Q(sqzhuangtai=1) | Q(sqzhuangtai=2), then=1), + output_field=IntegerField() + ) + ) + ) + + tongji_data = { + 'zongshu': stats.get('zongshu', 0) or 0, + 'daichuli': stats.get('daichuli', 0) or 0, + 'yichuli': stats.get('yichuli', 0) or 0 + } + + return Response({ + 'code': 0, + 'msg': '获取统计信息成功', + 'data': tongji_data + }) + + except Exception as e: + logger.error(f"商家获取统计信息异常 - 用户ID: {yonghuid}, 错误: {str(e)}") + return Response({ + 'code': 1, + 'msg': '获取统计信息失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def get_chufa_list(self, yonghuid, request_data): + """ + 获取处罚记录列表(商家端) + 🔴 只修改图片查询逻辑,字段保持原样 + """ + try: + # 获取分页参数 + page = int(request_data.get('page', 1)) + page_size = int(request_data.get('page_size', 10)) + + if page_size > 50: + page_size = 50 + if page_size < 1: + page_size = 10 + + # 获取状态筛选参数 + zhuangtai = request_data.get('zhuangtai', 0) + + # 🔴 查询条件:qingqiuid = 当前商家ID + queryset = Chufajilu.objects.filter(qingqiuid=yonghuid) + + if zhuangtai == 0: + queryset = queryset.filter(Q(sqzhuangtai=0) | Q(sqzhuangtai=3)) + elif zhuangtai == 1: + queryset = queryset.filter(Q(sqzhuangtai=1) | Q(sqzhuangtai=2)) + else: + pass + + # 按创建时间倒序排列 + queryset = queryset.order_by('-create_time') + + # 分页逻辑 + paginator = Paginator(queryset, page_size) + + try: + page_obj = paginator.page(page) + except: + return Response({ + 'code': 0, + 'msg': '获取成功', + 'data': { + 'list': [], + 'has_more': False, + 'page': page, + 'page_size': page_size, + 'current_count': 0 + } + }) + + chufa_records = list(page_obj) + + if not chufa_records: + return Response({ + 'code': 0, + 'msg': '获取成功', + 'data': { + 'list': [], + 'has_more': False, + 'page': page, + 'page_size': page_size, + 'current_count': 0 + } + }) + + # 🔴【核心修复】收集所有需要查询图片的组合 + dingdan_ids = [] + dashou_ids = [] + + for record in chufa_records: + if record.dingdan_id: + dingdan_ids.append(record.dingdan_id) + if record.dashouid: # 打手ID + dashou_ids.append(record.dashouid) + + # 去重 + dingdan_ids = list(set(dingdan_ids)) + dashou_ids = list(set(dashou_ids)) + + # 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误) + # 1. 查询证据图片(商家自己上传的) + zhengju_mapping = {} + if dingdan_ids: + # 构建Q对象:dingdan_id在列表中 AND yonghuid=当前商家ID + zhengju_q = Q(dingdan_id__in=dingdan_ids, yonghuid=yonghuid) + zhengju_queryset = Chufatupian.objects.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian') + + for item in zhengju_queryset: + key = f"{item['dingdan_id']}_{item['yonghuid']}" + if key not in zhengju_mapping: + zhengju_mapping[key] = [] + if item['tupian']: + zhengju_mapping[key].append(item['tupian']) + + # 2. 查询申诉图片(打手上传的) + shensu_mapping = {} + if dingdan_ids and dashou_ids: + # 构建Q对象:dingdan_id在列表中 AND yonghuid在打手ID列表中 + shensu_q = Q(dingdan_id__in=dingdan_ids, yonghuid__in=dashou_ids) + shensu_queryset = Chufatupian.objects.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian') + + for item in shensu_queryset: + key = f"{item['dingdan_id']}_{item['yonghuid']}" + if key not in shensu_mapping: + shensu_mapping[key] = [] + if item['tupian']: + shensu_mapping[key].append(item['tupian']) + + # 🔴【核心修复】组装返回数据 - 字段保持原样! + chufa_list = [] + for record in chufa_records: + dingdan_id = record.dingdan_id + dashou_id = record.dashouid + + # 获取证据图片(商家自己上传的) + zhengju_tupian = [] + if dingdan_id: + key = f"{dingdan_id}_{yonghuid}" + zhengju_tupian = zhengju_mapping.get(key, []) + + # 获取申诉图片(打手上传的) + shensu_tupian = [] + if dingdan_id and dashou_id: + key = f"{dingdan_id}_{dashou_id}" + shensu_tupian = shensu_mapping.get(key, []) + + # 🔴【字段保持原样!】与前端完全对应! + chufa_data = { + 'id': record.id, + 'dingdan_id': record.dingdan_id or '', + 'qingqiuid': record.dashouid or '', # 打手ID + 'cfliyou': record.cfliyou or '', + 'sqzhuangtai': record.sqzhuangtai or 0, + 'ssliyou': record.ssliyou or '', + 'jifen': record.jifen or 0, + 'create_time': record.create_time.strftime('%Y-%m-%d %H:%M:%S') if record.create_time else '', + 'update_time': record.update_time.strftime('%Y-%m-%d %H:%M:%S') if record.update_time else '', + 'zhengju_tupian': zhengju_tupian, # 商家自己上传的证据图片 + 'shensu_tupian': shensu_tupian, # 打手上传的申诉图片 + } + chufa_list.append(chufa_data) + + # 判断是否还有更多数据 + has_more = page_obj.has_next() + + response_data = { + 'list': chufa_list, + 'has_more': has_more, + 'page': page, + 'page_size': page_size, + 'current_count': len(chufa_list) + } + + return Response({ + 'code': 0, + 'msg': '获取成功', + 'data': response_data + }) + + except ValueError as e: + logger.error(f"商家端参数错误 - 用户ID: {yonghuid}, 错误: {str(e)}") + return Response({ + 'code': 4, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + logger.error(f"商家获取处罚记录列表异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True) + return Response({ + 'code': 5, + 'msg': '获取记录列表失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + + + + + + + + + +class KefuLoginView(APIView): + """ + 客服登录接口(支持手机号重复) + 请求:POST /yonghu/kefujinru + 请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"} + 返回: + code=0 成功,data包含token,nicheng,yonghuid + code=4 账号被封禁(提示“账号已被封禁”) + 其他错误统一 code=1 msg="登录失败" + """ + throttle_classes = [AnonRateThrottle] # 限制匿名请求频率 + permission_classes = [AllowAny] # 任何人都可访问 + + def post(self, request): + # 1. 获取参数 + phone = request.data.get('phone', '').strip() + password = request.data.get('password', '') + erjimima = request.data.get('erjimima', '') + + if not phone or not password or not erjimima: + return Response({ + 'code': 1, + 'msg': '登录失败', # 统一提示,不暴露具体原因 + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 获取客户端真实IP + client_ip = self.get_client_ip(request) + + # 3. 查询所有手机号匹配且类型为客服的用户 + # 注意:UserMain 中需要有 user_type 字段,且值为 'kefu' 代表客服 + user_mains = UserMain.objects.filter( + phone=phone, + user_type='kefu' # 只查找客服账号 + ).select_related('kefu_profile') # 预加载客服扩展表 + + if not user_mains.exists(): + logger.warning(f"登录失败,手机号不存在或非客服: {phone}, IP: {client_ip}") + return Response({ + 'code': 1, + 'msg': '登录失败', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 遍历所有匹配的用户,验证密码和二级密码 + target_user = None + target_kefu = None + + for user in user_mains: + # 验证主密码(明文比较,按您的要求) + if user.password != password: + continue + + # 检查是否存在客服扩展表 + try: + kefu = user.kefu_profile + except UserKefu.DoesNotExist: + continue + + # 验证二级密码(明文比较) + if kefu.erjimima != erjimima: + continue + + # 如果账号被禁用,记录但继续查找(可能还有其他正常账号?) + if kefu.zhuangtai != 1: + # 如果找到被禁用的,记录下来,但继续查找是否有正常账号 + if target_user is None: + # 暂时记录这个禁用账号,但继续遍历 + target_user = user + target_kefu = kefu + continue + + # 找到完全匹配且状态正常的账号,立即使用 + target_user = user + target_kefu = kefu + break + else: + # 循环结束未找到正常账号,但可能找到了禁用账号 + if target_user and target_kefu: + # 只有被禁用的账号匹配,返回封禁提示 + logger.warning(f"登录失败,客服账号已禁用: {phone}, IP: {client_ip}") + return Response({ + 'code': 4, + 'msg': '账号已被封禁', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + else: + # 没有任何账号匹配(密码/二级密码错误) + logger.warning(f"登录失败,密码或二级密码错误: {phone}, IP: {client_ip}") + return Response({ + 'code': 1, + 'msg': '登录失败', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 5. 更新IP和最后登录时间 + target_user.ip = client_ip + target_user.last_login_time = timezone.now() + target_user.save(update_fields=['ip', 'last_login_time']) + + # 6. 生成JWT token + refresh = RefreshToken.for_user(target_user) + token = str(refresh.access_token) + + # 7. 返回成功数据 + return Response({ + 'code': 0, + 'msg': '登录成功', + 'data': { + 'token': token, + 'nicheng': target_kefu.nicheng, + 'yonghuid': target_user.yonghuid, + } + }) + + def get_client_ip(self, request): + """ + 获取客户端真实IP + """ + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0].strip() + if ip: + return ip + return request.META.get('REMOTE_ADDR', '0.0.0.0') + + + + + + +logger = logging.getLogger(__name__) + +class KefuStatsView(APIView): + """ + 客服统计数据接口 + 请求:POST /yonghu/kfjrhqzl + 请求体:{"phone": "13800138000"} + 认证:必须携带有效的 JWT token(放在 Authorization 头) + 返回: + code=0 成功,data 包含 jinrichuli, jinyuechuli, zongchuli + 验证失败统一返回 401,不透露具体原因 + """ + + permission_classes = [IsAuthenticated] # 必须登录 + + + def post(self, request): + # 1. 获取请求中的 phone + phone = request.data.get('phone', '').strip() + if not phone: + # 缺少参数,但按约定返回 401(可以改为 400,但为了统一,用401) + logger.warning(f"统计接口缺少 phone 参数,用户: {request.user.yonghuid}") + return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 获取当前登录用户(从 JWT 中解析) + current_user = request.user + + # 3. 验证前端传来的 phone 是否与当前用户的 phone 一致 + if current_user.phone != phone: + # 不一致,可能 token 被冒用或参数错误 + logger.warning(f"统计接口 phone 不匹配: 请求phone={phone}, 用户phone={current_user.phone}") + return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 4. 验证用户类型是否为客服 + if current_user.user_type != 'kefu': + logger.warning(f"统计接口用户类型非客服: {current_user.yonghuid}") + return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 5. 尝试获取客服扩展表数据 + try: + kefu_profile = current_user.kefu_profile # OneToOneField 反向关系 + except UserKefu.DoesNotExist: + logger.warning(f"统计接口客服扩展表不存在: {current_user.yonghuid}") + return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 6. 返回统计数据(字段名与前端约定一致) + return Response({ + 'code': 0, + 'data': { + 'jinrichuli': kefu_profile.jinrichuli, + 'jinyuechuli': kefu_profile.jinyuechuli, + 'zongchuli': kefu_profile.zongchuli, + } + }, status=status.HTTP_200_OK) + + + + + + +class KefuGetOrderTypesView(APIView): + """ + 客服获取订单类型列表接口(平台订单用) + 请求:POST /yonghu/kfhqptddlx + 参数:{"phone": "13800138000"} + 认证:JWT + 返回:code=0 + 类型列表 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + phone = request.data.get('phone', '').strip() + if not phone: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + current_user = request.user + + # 2. 验证手机号一致性 + if getattr(current_user, 'phone', '') != phone: + logger.warning(f"手机号不匹配: 请求phone={phone}, 用户phone={current_user.phone}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 3. 验证用户类型及客服状态 + if current_user.user_type != 'kefu': + logger.warning(f"用户类型非客服: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + kefu_profile = current_user.kefu_profile + except ObjectDoesNotExist: + logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if kefu_profile.zhuangtai != 1: + logger.warning(f"客服账号已被禁用: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 4. 查询订单类型(商品类型) + try: + # 只查询需要的字段,减少数据传输 + types_qs = ShangpinLeixing.objects.all().only('id', 'jieshao', 'tupian_url') + type_list = [ + { + 'id': t.id, + 'biaoti': t.jieshao or '', # 假设商品类型的标题字段为 jieshao + 'tupian_url': t.tupian_url or '' + } + for t in types_qs + ] + return Response({'code': 0, 'data': type_list}) + except Exception as e: + logger.error(f"查询商品类型表失败: {str(e)}") + # 表不存在或查询失败,返回空列表 + return Response({'code': 0, 'data': []}) + + + + + +class KefuGetOrderListView(APIView): + """ + 客服获取平台订单列表接口(发单平台=1,非跨平台) + 需要权限:002ab(平台订单管理) + 请求参数: + username: 客服账号(用于权限校验) + page: 页码 + pageSize: 每页条数 + dingdan_id: 订单ID(精确搜索) + laoban_id: 老板ID(搜索平台扩展表中的老板) + dashou_id: 打手ID(搜索主表中的接单打手) + leixing: 商品类型ID + zhuangtai: 状态(多个用逗号分隔,如 "1,7") + date: 日期(YYYY-MM-DD),筛选订单创建时间 + nicheng: 游戏昵称(订单主表的 nicheng 字段) + jieshao: 订单介绍(订单主表的 jieshao 字段) + clkf: 处理客服(订单主表的 clkf 字段) + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + start_total = time.time() + + # 1. 获取前端参数 + username = request.data.get('phone', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 3. 检查平台订单管理权限(002ab) + if '002ab' not in permissions: + return Response({'code': 403, 'msg': '您无权查看平台订单列表'}) + + # 4. 获取分页和筛选参数 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('pageSize', 20)) + + dingdan_id = request.data.get('dingdan_id', '').strip() + laoban_id = request.data.get('laoban_id', '').strip() + dashou_id = request.data.get('dashou_id', '').strip() + leixing = request.data.get('leixing') + zhuangtai = request.data.get('zhuangtai', '') + date = request.data.get('date', '').strip() + # 新增筛选条件 + nicheng = request.data.get('nicheng', '').strip() + jieshao = request.data.get('jieshao', '').strip() + clkf = request.data.get('clkf', '').strip() + + # ---------- 查询条件 ---------- + # 基础条件:平台发单 + 非跨平台 + base_q = Q(fadan_pingtai=1) & Q(is_cross=0) + + if dingdan_id: + q_conditions = base_q & Q(dingdan_id=dingdan_id) + else: + q_conditions = base_q + if leixing is not None: + try: + leixing_int = int(leixing) + q_conditions &= Q(leixing_id=leixing_int) + except (ValueError, TypeError): + pass + if zhuangtai and zhuangtai != 'all': + try: + status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()] + if status_list: + q_conditions &= Q(zhuangtai__in=status_list) + except (ValueError, TypeError): + pass + if dashou_id: + q_conditions &= Q(jiedan_dashou_id=dashou_id) + if laoban_id: + q_conditions &= Q(pingtai_kuozhan__laoban_id=laoban_id) + if date: + q_conditions &= Q(create_time__date=date) + # 新增筛选条件 + if nicheng: + q_conditions &= Q(nicheng__icontains=nicheng) + if jieshao: + q_conditions &= Q(jieshao__icontains=jieshao) + if clkf: + q_conditions &= Q(clkf__icontains=clkf) + + # ---------- 统计(仅按发单平台,非跨平台) ---------- + stats = Dingdan.objects.filter(fadan_pingtai=1, is_cross=0).aggregate( + total_orders=Count('id'), + completed_orders=Count('id', filter=Q(zhuangtai=3)), + refund_orders=Count('id', filter=Q(zhuangtai=5)), + pending_orders=Count('id', filter=Q(zhuangtai__in=[4,8])) + ) + + # ---------- 状态计数(所有状态都返回,非跨平台) ---------- + status_counts = { + 'all': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0).count(), + '1,7': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai__in=[1,7]).count(), + '2': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=2).count(), + '8': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=8).count(), + '3': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=3).count(), + '4': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=4).count(), + '5': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=5).count(), + '6': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=6).count(), + } + + # ---------- 分页查询 ---------- + total_count = Dingdan.objects.filter(q_conditions).count() + + orders_qs = Dingdan.objects.filter(q_conditions).select_related( + 'pingtai_kuozhan' + ).only( + 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', 'fadan_pingtai', + 'jine', 'tupian', 'create_time', 'nicheng', 'jiedan_dashou_id', + 'pingtai_kuozhan__laoban_id' + ).order_by('-create_time')[(page-1)*page_size : page*page_size] + + # ---------- 获取老板昵称 ---------- + laoban_ids = [o.pingtai_kuozhan.laoban_id for o in orders_qs if hasattr(o, 'pingtai_kuozhan') and o.pingtai_kuozhan.laoban_id] + nickname_map = {} + if laoban_ids: + users = UserMain.objects.filter(yonghuid__in=laoban_ids).select_related('boss_profile').only( + 'yonghuid', 'boss_profile__nickname' + ) + for user in users: + nickname_map[user.yonghuid] = user.boss_profile.nickname or '' if hasattr(user, 'boss_profile') else '' + + result_list = [] + for order in orders_qs: + ext = getattr(order, 'pingtai_kuozhan', None) + laoban_id = ext.laoban_id if ext else '' + youxi_nicheng = nickname_map.get(laoban_id, order.nicheng or '') + result_list.append({ + 'dingdan_id': order.dingdan_id or '', + 'jieshao': order.jieshao or '', + 'zhuangtai': order.zhuangtai, + 'leixing': order.leixing_id, + 'fadanpingtai': order.fadan_pingtai, + 'jiage': float(order.jine) if order.jine else 0.00, + 'tupian_url': order.tupian or '', + 'fadanshijian': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '', + 'youxi_nicheng': youxi_nicheng + }) + + response_data = { + 'stats': { + 'totalOrders': stats['total_orders'] or 0, + 'completedOrders': stats['completed_orders'] or 0, + 'refundOrders': stats['refund_orders'] or 0, + 'pendingOrders': stats['pending_orders'] or 0 + }, + 'statusCounts': status_counts, + 'list': result_list, + 'total': total_count, + 'page': page, + 'pageSize': page_size + } + + logger.info(f"平台订单接口总耗时: {time.time()-start_total:.3f}s") + return Response({'code': 0, 'data': response_data}) + +class KefuGetShangjiaOrderListView(APIView): + """ + 客服获取商家订单列表接口(发单平台=2,非跨平台) + 需要权限:002ac(商家订单管理)—— 如不区分可使用 002ab + 请求参数: + username: 客服账号(用于权限校验) + page: 页码 + pageSize: 每页条数 + dingdan_id: 订单ID(精确搜索) + shangjia_id: 商家ID(搜索商家扩展表中的商家ID) + dashou_id: 打手ID(搜索主表中的接单打手) + leixing: 商品类型ID + zhuangtai: 状态(多个用逗号分隔,如 "1,7") + date: 日期(YYYY-MM-DD),筛选订单创建时间 + nicheng: 游戏昵称(订单主表的 nicheng 字段) + jieshao: 订单介绍(订单主表的 jieshao 字段) + clkf: 处理客服(订单主表的 clkf 字段) + shangjia_nicheng: 商家昵称(商家扩展表中的昵称) + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + start_total = time.time() + + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 3. 检查商家订单管理权限(使用 002ac,如果没有可改用 002ab) + if '002ac' not in permissions: + return Response({'code': 403, 'msg': '您无权查看商家订单列表'}) + + # 4. 获取分页和筛选参数 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('pageSize', 20)) + + dingdan_id = request.data.get('dingdan_id', '').strip() + shangjia_id = request.data.get('shangjia_id', '').strip() + dashou_id = request.data.get('dashou_id', '').strip() + leixing = request.data.get('leixing') + zhuangtai = request.data.get('zhuangtai', '') + date = request.data.get('date', '').strip() + # 新增筛选条件 + nicheng = request.data.get('nicheng', '').strip() + jieshao = request.data.get('jieshao', '').strip() + clkf = request.data.get('clkf', '').strip() + shangjia_nicheng = request.data.get('shangjia_nicheng', '').strip() + + # ---------- 查询条件 ---------- + # 基础条件:商家发单 + 非跨平台 + base_q = Q(fadan_pingtai=2) & Q(is_cross=0) + + if dingdan_id: + q_conditions = base_q & Q(dingdan_id=dingdan_id) + else: + q_conditions = base_q + if leixing is not None: + try: + leixing_int = int(leixing) + q_conditions &= Q(leixing_id=leixing_int) + except (ValueError, TypeError): + pass + if zhuangtai and zhuangtai != 'all': + try: + status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()] + if status_list: + q_conditions &= Q(zhuangtai__in=status_list) + except (ValueError, TypeError): + pass + if dashou_id: + q_conditions &= Q(jiedan_dashou_id=dashou_id) + if shangjia_id: + q_conditions &= Q(shangjia_kuozhan__shangjia_id=shangjia_id) + if date: + q_conditions &= Q(create_time__date=date) + if nicheng: + q_conditions &= Q(nicheng__icontains=nicheng) + if jieshao: + q_conditions &= Q(jieshao__icontains=jieshao) + if clkf: + q_conditions &= Q(clkf__icontains=clkf) + if shangjia_nicheng: + # 通过商家扩展表关联 UserShangjia 的昵称 + q_conditions &= Q(shangjia_kuozhan__shangjia_id__in=UserMain.objects.filter( + shop_profile__nicheng__icontains=shangjia_nicheng + ).values_list('yonghuid', flat=True)) + + # ---------- 统计(仅按发单平台,非跨平台) ---------- + stats = Dingdan.objects.filter(fadan_pingtai=2, is_cross=0).aggregate( + total_orders=Count('id'), + completed_orders=Count('id', filter=Q(zhuangtai=3)), + refund_orders=Count('id', filter=Q(zhuangtai=5)), + pending_orders=Count('id', filter=Q(zhuangtai__in=[4,8])) + ) + + # ---------- 状态计数(所有状态都返回,非跨平台) ---------- + status_counts = { + 'all': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0).count(), + '1,7': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai__in=[1,7]).count(), + '2': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=2).count(), + '8': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=8).count(), + '3': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=3).count(), + '4': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=4).count(), + '5': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=5).count(), + '6': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=6).count(), + } + + # ---------- 分页查询 ---------- + total_count = Dingdan.objects.filter(q_conditions).count() + + orders_qs = Dingdan.objects.filter(q_conditions).select_related( + 'shangjia_kuozhan' + ).only( + 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', 'fadan_pingtai', + 'jine', 'tupian', 'create_time', 'nicheng', 'jiedan_dashou_id', + 'shangjia_kuozhan__shangjia_id' + ).order_by('-create_time')[(page-1)*page_size : page*page_size] + + # ---------- 获取商家昵称 ---------- + shangjia_ids = [o.shangjia_kuozhan.shangjia_id for o in orders_qs if hasattr(o, 'shangjia_kuozhan') and o.shangjia_kuozhan.shangjia_id] + nickname_map = {} + if shangjia_ids: + users = UserMain.objects.filter(yonghuid__in=shangjia_ids).select_related('shop_profile').only( + 'yonghuid', 'shop_profile__nicheng' + ) + for user in users: + nickname_map[user.yonghuid] = user.shop_profile.nicheng or '' if hasattr(user, 'shop_profile') else '' + + result_list = [] + for order in orders_qs: + ext = getattr(order, 'shangjia_kuozhan', None) + shangjia_id = ext.shangjia_id if ext else '' + youxi_nicheng = order.nicheng or '' # 商家订单的玩家昵称 + result_list.append({ + 'dingdan_id': order.dingdan_id or '', + 'jieshao': order.jieshao or '', + 'zhuangtai': order.zhuangtai, + 'leixing': order.leixing_id, + 'fadanpingtai': order.fadan_pingtai, + 'jiage': float(order.jine) if order.jine else 0.00, + 'tupian_url': order.tupian or '', + 'fadanshijian': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '', + 'youxi_nicheng': youxi_nicheng + }) + + response_data = { + 'stats': { + 'totalOrders': stats['total_orders'] or 0, + 'completedOrders': stats['completed_orders'] or 0, + 'refundOrders': stats['refund_orders'] or 0, + 'pendingOrders': stats['pending_orders'] or 0 + }, + 'statusCounts': status_counts, + 'list': result_list, + 'total': total_count, + 'page': page, + 'pageSize': page_size + } + + logger.info(f"商家订单接口总耗时: {time.time()-start_total:.3f}s") + return Response({'code': 0, 'data': response_data}) + + + + + + + + + + + +class KefuChangeDashouView(APIView): + """ + 客服更换打手接口 + 请求:POST /yonghu/kefughds + 参数:{ + "phone": "客服手机号", + "dingdan_id": "订单ID", + "new_dashou_id": "新打手ID" + } + 认证:JWT + 返回:code=0 成功 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + phone = request.data.get('phone', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + new_dashou_id = request.data.get('new_dashou_id', '').strip() + + if not phone or not dingdan_id or not new_dashou_id: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 客服身份验证 + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + logger.warning(f"手机号不匹配: {phone}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if current_user.user_type != 'kefu': + logger.warning(f"用户类型非客服: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + kefu = current_user.kefu_profile + except ObjectDoesNotExist: + logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if kefu.zhuangtai != 1: + logger.warning(f"客服账号已禁用: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 3. 查询订单 + try: + order = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 4. 验证订单状态是否允许更换打手(进行中2 或 结算中8) + if order.zhuangtai not in [2, 8]: + return Response({'code': 400, 'msg': '订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST) + + # 5. 查询新打手 + try: + new_dashou_user = UserMain.objects.get(yonghuid=new_dashou_id) + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 6. 验证打手扩展表 + try: + new_dashou_profile = new_dashou_user.dashou_profile + except ObjectDoesNotExist: + return Response({'code': 404, 'msg': '打手信息不完整'}, status=status.HTTP_404_NOT_FOUND) + + # 7. 验证打手状态(打手状态和账号状态必须为1) + if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1: + return Response({'code': 400, 'msg': '打手状态不允许接单'}, status=status.HTTP_400_BAD_REQUEST) + # 8. 验证打手积分是否足够(至少5分) + if new_dashou_profile.jifen < 5: + return Response({'code': 400, 'msg': '打手积分不足,无法接单'}, status=status.HTTP_400_BAD_REQUEST) + + # 8. 根据订单的抢单要求类型进行额外验证 + yaoqiuleixing = order.yaoqiuleixing + + if yaoqiuleixing == 1: # 会员抢单 + huiyuan_id = order.huiyuan_id + if not huiyuan_id: + return Response({'code': 400, 'msg': '订单会员ID为空'}, status=status.HTTP_400_BAD_REQUEST) + + try: + # 查询打手的会员购买记录 + huiyuan_goumai = Huiyuangoumai.objects.get( + huiyuan_id=huiyuan_id, + yonghu_id=new_dashou_id # 假设 yonghu_id 字段存储用户ID + ) + except Huiyuangoumai.DoesNotExist: + return Response({'code': 400, 'msg': '打手未购买该订单要求的会员'}, status=status.HTTP_400_BAD_REQUEST) + + # 检查会员是否过期(调用模型方法) + is_expired = huiyuan_goumai.jiance_shifou_daoqi() + if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1: + return Response({'code': 400, 'msg': '打手的会员已过期'}, status=status.HTTP_400_BAD_REQUEST) + + elif yaoqiuleixing == 2: # 押金抢单 + order_yajin = order.yongjin # 假设订单的 yongjin 字段存储押金要求 + if order_yajin is None: + return Response({'code': 400, 'msg': '订单押金为空'}, status=status.HTTP_400_BAD_REQUEST) + + if new_dashou_profile.yajin < order_yajin: + return Response({'code': 400, 'msg': '打手押金不足'}, status=status.HTTP_400_BAD_REQUEST) + else: + return Response({'code': 400, 'msg': '订单抢单要求类型错误'}, status=status.HTTP_400_BAD_REQUEST) + + # 9. 所有验证通过,开始更换打手(使用事务) + with transaction.atomic(): + # 9.1 更新新打手扩展表 + new_dashou_profile.zhuangtai = 0 # 设为接单中 + new_dashou_profile.jiedanzongliang += 1 + new_dashou_profile.jinrijiedan += 1 + new_dashou_profile.jinyuejiedan += 1 + new_dashou_profile.save() + + # 9.2 处理原打手(如果存在) + old_dashou_id = order.jiedan_dashou_id + if old_dashou_id: + try: + old_dashou_user = UserMain.objects.get(yonghuid=old_dashou_id) + old_dashou_profile = old_dashou_user.dashou_profile + old_dashou_profile.zhuangtai = 1 # 恢复空闲 + old_dashou_profile.tuikuanliang += 1 # 原打手退款总量+1(业务逻辑) + old_dashou_profile.save() + logger.info(f"原打手 {old_dashou_id} 状态已更新") + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"原打手 {old_dashou_id} 不存在或信息不完整,跳过更新") + except Exception as e: + logger.error(f"更新原打手时异常: {str(e)}", exc_info=True) + + # 9.3 更新订单 + order.jiedan_dashou_id = new_dashou_id + order.zhuangtai = 2 # 进行中 + order.clkf = current_user.yonghuid # 记录处理客服ID + order.save() + + return Response({'code': 0, 'msg': '更换打手成功'}) + + + + + +class KefuRecoverOrderView(APIView): + """ + 客服恢复订单接口(仅状态9可恢复) + 请求:POST /yonghu/kefuhf + 参数:{ + "phone": "客服手机号", + "dingdan_id": "订单ID" + } + 认证:JWT + 返回:code=0 成功 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + phone = request.data.get('phone', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not phone or not dingdan_id: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 客服身份验证 + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if current_user.user_type != 'kefu': + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + kefu = current_user.kefu_profile + except ObjectDoesNotExist: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if kefu.zhuangtai != 1: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 3. 查询订单 + try: + order = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 4. 验证订单状态是否为9(未付款) + if order.zhuangtai != 9: + return Response({'code': 400, 'msg': '订单状态不允许恢复'}, status=status.HTTP_400_BAD_REQUEST) + + # 5. 根据是否有指定ID决定新状态 + if order.zhiding_id: + new_status = 7 # 指定中 + else: + new_status = 1 # 下单中 + + # 6. 更新订单 + order.zhuangtai = new_status + order.clkf = current_user.yonghuid + order.save() + + return Response({'code': 0, 'msg': '恢复成功', 'data': {'new_status': new_status}}) + + + + + + + + +# dingdan/views.py +class KefuPlatformRefundView(APIView): + """ + 客服平台订单退款接口(调用微信支付) + 请求:POST /yonghu/kefupttk + 参数:{ + "phone": "客服手机号", + "dingdan_id": "订单ID", + "tuikuan_liyou": "退款理由(可选)" + } + 认证:JWT + 返回:code=0 成功,code=其他 失败 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + phone = request.data.get('phone', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() + + if not phone or not dingdan_id: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 客服身份验证 + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + logger.warning(f"手机号不匹配: {phone}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if current_user.user_type != 'kefu': + logger.warning(f"用户类型非客服: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + kefu = current_user.kefu_profile + except ObjectDoesNotExist: + logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if kefu.zhuangtai != 1: + logger.warning(f"客服账号已禁用: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 3. 查询订单并加锁 + try: + with transaction.atomic(): + order = Dingdan.objects.select_related('pingtai_kuozhan').select_for_update().get(dingdan_id=dingdan_id) + + # 4. 验证平台订单 + if order.fadan_pingtai != 1: + return Response({'code': 400, 'msg': '该订单不是平台订单'}, status=status.HTTP_400_BAD_REQUEST) + + # 5. 验证状态允许退款(根据模型,状态值:1下单中,2进行中,3已完成,4退款审核,5已退款,6退款失败,7指定中,8结算中) + allowed_statuses = [1, 2, 4, 7, 8] # 允许退款的已支付或处理中状态 + if order.zhuangtai not in allowed_statuses: + return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST) + + # 6. 获取微信交易号(如果存在) + wechat_transaction_id = getattr(order, 'wechat_transaction_id', None) + + # 7. 调用微信退款接口 + refund_result = self.call_wechat_refund( + dingdan_id=order.dingdan_id, + jine=order.jine, + transaction_id=wechat_transaction_id + ) + + if refund_result['code'] != 0: + # 微信退款失败,直接返回错误,数据库不更新 + logger.error(f"微信退款失败,订单 {dingdan_id},原因:{refund_result['msg']}") + return Response({'code': 400, 'msg': refund_result['msg']}, status=status.HTTP_400_BAD_REQUEST) + + # 8. 更新订单状态为5(已退款) + order.zhuangtai = 5 + order.clkf = current_user.phone # 记录处理客服手机号 + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + + # 9. 更新打手数据 + jiedan_dashou_id = order.jiedan_dashou_id + if jiedan_dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 # 打手状态设为空闲 + dashou_profile.save() + logger.info(f"平台退款:打手 {jiedan_dashou_id} 退款订单+1") + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"平台退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新") + + # ========== 新增:商品和店铺每日统计 ========== + try: + # 商品每日统计(只要有商品ID就执行) + if order.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(order, 'pingtai_kuozhan'): + pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=order.shangpin_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + dianpu_id = None + if hasattr(order, 'pingtai_kuozhan'): + dianpu_id = order.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + # 10. 更新退款记录表(假设存在 Tuikuanjilu 模型,字段需根据实际调整) + + #更新支出记录 + update_daily_payout(order.jine) + + + try: + refund_record, created = Tuikuanjilu.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'dashouid': jiedan_dashou_id or '', + 'qingqiuid': '', + 'chuliid': phone, + 'liyou': tuikuan_liyou or '客服退款', + 'sqzhuangtai': 1, # 成功 + 'jine': order.jine, + 'dashou_fencheng': order.dashou_fencheng or 0, + 'jieshao': order.jieshao or '', + 'beizhu': '客服平台退款', + 'nicheng': order.nicheng or '', + } + ) + if not created: + refund_record.sqzhuangtai = 1 + refund_record.chuliid = phone + if tuikuan_liyou: + refund_record.liyou = tuikuan_liyou + refund_record.save() + except Exception as e: + logger.error(f"平台退款更新退款记录异常: {str(e)}", exc_info=True) + + # 11. 更新客服统计数据 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=3 # 3 表示退款(退款) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + + logger.info(f"平台退款成功,订单 {dingdan_id},退款单号 {refund_result.get('refund_id')}") + return Response({'code': 0, 'msg': '退款成功'}) + + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"平台退款接口异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def call_wechat_refund(self, dingdan_id, jine, transaction_id=None): + """ + 调用微信支付退款接口 + :param dingdan_id: 商户订单号(我们的订单ID) + :param jine: 退款金额 + :param transaction_id: 微信支付订单号(优先使用) + :return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'} + """ + # 获取配置 + appid = getattr(settings, 'WEIXIN_APPID', '') + mch_id = getattr(settings, 'WEIXIN_MCHID', '') + key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') + cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '') + key_path = getattr(settings, 'WEIXIN_KEY_PATH', '') + + if not all([appid, mch_id, key, cert_path, key_path]): + missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v] + logger.error(f"微信支付配置缺失: {missing}") + return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'} + + # 生成退款单号 + out_refund_no = self.generate_refund_no(dingdan_id) + total_fee = int(float(jine) * 100) + refund_fee = total_fee + refund_desc = '客服退款' + + # 构造请求参数 + params = { + 'appid': appid, + 'mch_id': mch_id, + 'nonce_str': self.generate_nonce_str(), + 'out_refund_no': out_refund_no, + 'total_fee': total_fee, + 'refund_fee': refund_fee, + 'refund_desc': refund_desc, + } + if transaction_id: + params['transaction_id'] = transaction_id + else: + params['out_trade_no'] = dingdan_id # 使用订单ID作为商户订单号 + + # 生成签名 + sorted_keys = sorted(params.keys()) + stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys]) + stringSignTemp = f"{stringA}&key={key}" + sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() + params['sign'] = sign + + xml_data = self.dict_to_xml(params) + url = 'https://api.mch.weixin.qq.com/secapi/pay/refund' + + try: + response = requests.post( + url, + data=xml_data, + cert=(cert_path, key_path), + timeout=10 + ) + result = xmltodict.parse(response.content)['xml'] + logger.info(f"微信退款响应: {result}") + except Exception as e: + logger.error(f"微信退款请求异常: {str(e)}", exc_info=True) + return {'code': 500, 'msg': '微信退款请求异常'} + + if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': + return {'code': 0, 'msg': 'success', 'refund_id': out_refund_no} + else: + err_msg = result.get('err_code_des', result.get('return_msg', '未知错误')) + logger.warning(f"微信退款失败: {err_msg}") + return {'code': 400, 'msg': err_msg} + + def generate_nonce_str(self): + return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + def generate_refund_no(self, dingdan_id): + timestamp = str(int(time.time())) + rand = ''.join(random.choices('0123456789', k=4)) + return f"{dingdan_id}REF{timestamp}{rand}" + + def dict_to_xml(self, data): + xml = [''] + for k, v in data.items(): + xml.append(f'<{k}>{v}') + xml.append('') + return ''.join(xml) + + + + + +# dingdan/views.py +class KefuRejectRefundView(APIView): + """ + 客服拒绝退款接口 + 请求:POST /yonghu/kefujjtk + 参数:{ + "phone": "客服手机号", + "dingdan_id": "订单ID", + "jujue_liyou": "拒绝理由(可选)" + } + 认证:JWT + 返回:code=0 成功 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + phone = request.data.get('phone', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + jujue_liyou = request.data.get('jujue_liyou', '').strip() + + if not phone or not dingdan_id: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 客服身份验证 + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if current_user.user_type != 'kefu': + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + kefu = current_user.kefu_profile + except ObjectDoesNotExist: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if kefu.zhuangtai != 1: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 3. 查询订单并加锁 + try: + with transaction.atomic(): + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + + # 4. 验证订单状态为退款审核中(4) + if order.zhuangtai != 4: + return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST) + + # 获取必要字段 + jiedan_dashou_id = order.jiedan_dashou_id + dashou_fencheng = order.dashou_fencheng + fadan_pingtai = order.fadan_pingtai + + # 5. 验证打手分成金额是否存在 + if dashou_fencheng is None: + return Response({'code': 400, 'msg': '订单缺少打手分成金额'}, status=status.HTTP_400_BAD_REQUEST) + + # 6. 更新订单状态为退款失败(6) + order.zhuangtai = 6 + order.clkf = current_user.phone # 记录处理客服手机号 + if jujue_liyou: + order.tkly = jujue_liyou + order.save() + + # 7. 给打手结算(返还分成) + if jiedan_dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_profile = dashou_user.dashou_profile + # 增加打手相关统计数据 + dashou_profile.chengjiaozongliang += 1 + dashou_profile.yue += dashou_fencheng + dashou_profile.zonge += dashou_fencheng + dashou_profile.jinrishouyi += dashou_fencheng + dashou_profile.jinyueshouyi += dashou_fencheng + dashou_profile.save() + logger.info(f"拒绝退款:打手 {jiedan_dashou_id} 获得分成 {dashou_fencheng}") + except (UserMain.DoesNotExist, AttributeError): + logger.warning(f"拒绝退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过结算") + + # 8. 如果是商家发单,更新商家成交订单数量 + if fadan_pingtai == 2: + try: + # 获取商家订单扩展表 + shangjia_kuozhan = order.shangjia_kuozhan + shangjia_id = shangjia_kuozhan.shangjia_id + if shangjia_id: + shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) + shangjia_profile = shangjia_user.shop_profile + shangjia_profile.chengjiao += 1 + shangjia_profile.save() + logger.info(f"拒绝退款:商家 {shangjia_id} 成交订单+1") + + # ✅ 新方法(行为驱动,更安全) + update_shangjia_daily( + yonghuid=shangjia_id, + amount=Decimal(str(order.jine)), + action=2 # 2 = 结算 + ) + except (ObjectDoesNotExist, UserMain.DoesNotExist, AttributeError): + logger.warning(f"拒绝退款:商家信息不存在,跳过商家更新") + + # 9. 更新退款记录表 + try: + refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) + refund_record.sqzhuangtai = 2 # 2表示拒绝 + refund_record.chuliid = phone + refund_record.bhliyou = jujue_liyou # 记录驳回理由(假设模型有此字段) + refund_record.save() + logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}") + except Tuikuanjilu.DoesNotExist: + # 如果退款记录不存在,可以创建一条记录(可选) + Tuikuanjilu.objects.create( + dingdan_id=dingdan_id, + dashouid=jiedan_dashou_id or '', + qingqiuid='', + chuliid=phone, + liyou=jujue_liyou or '客服拒绝退款', + sqzhuangtai=2, + jine=order.jine, + dashou_fencheng=dashou_fencheng or 0, + jieshao=order.jieshao or '', + beizhu='客服拒绝退款', + nicheng=order.nicheng or '', + bhliyou=jujue_liyou or '', # 假设模型有 bhliyou 字段 + ) + except Exception as e: + logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True) + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=dashou_fencheng, + action=2 # 2 表示成交(结算) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + + # 10. 更新客服统计数据 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + return Response({'code': 0, 'msg': '拒绝退款成功'}) + + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"拒绝退款接口异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + + + + + + +class KefuMerchantRefundView(APIView): + """ + 客服商家订单退款接口(发单平台=2) + 请求:POST /yonghu/kefusjtk + 参数:{ + "phone": "客服手机号", + "dingdan_id": "订单ID", + "tuikuan_liyou": "退款理由(可选)" + } + 认证:JWT + 返回:code=0 成功 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + phone = request.data.get('phone', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() + + if not phone or not dingdan_id: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 客服身份验证(同前,省略以节省篇幅,但实际必须完整) + current_user = request.user + # ... 验证 + if getattr(current_user, 'phone', '') != phone: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if current_user.user_type != 'kefu': + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + kefu = current_user.kefu_profile + except ObjectDoesNotExist: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if kefu.zhuangtai != 1: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + + # 查询订单,预加载商家扩展表 + try: + order = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 验证商家订单 + if order.fadan_pingtai != 2: + return Response({'code': 400, 'msg': '该订单不是商家订单'}, status=status.HTTP_400_BAD_REQUEST) + + # 验证状态允许退款 + allowed_statuses = [1, 7, 2, 8, 4] + if order.zhuangtai not in allowed_statuses: + return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST) + + # 获取必要数据 + jiedan_dashou_id = order.jiedan_dashou_id + jine = order.jine + if jine is None: + return Response({'code': 400, 'msg': '订单金额缺失'}, status=status.HTTP_400_BAD_REQUEST) + + try: + shangjia_kuozhan = order.shangjia_kuozhan + shangjia_id = shangjia_kuozhan.shangjia_id + except ObjectDoesNotExist: + return Response({'code': 400, 'msg': '订单商家信息不存在'}, status=status.HTTP_400_BAD_REQUEST) + + with transaction.atomic(): + # 更新订单 + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + + # 更新打手 + if jiedan_dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"商家退款:打手 {jiedan_dashou_id} 不存在,跳过更新") + + # 更新商家 + try: + shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) + shangjia_profile = shangjia_user.shop_profile + shangjia_profile.tuikuan += 1 + shangjia_profile.yue += jine + shangjia_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"商家退款:商家 {shangjia_id} 不存在,跳过更新") + + # 更新退款记录表 + try: + refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) + refund_record.sqzhuangtai = 1 + refund_record.chuliid = phone + refund_record.save() + except Tuikuanjilu.DoesNotExist: + pass + + # 更新客服统计 + update_shangjia_daily( + yonghuid=shangjia_id, + amount=Decimal(str(order.jine)), + action=3 # 3 = 退款 + ) + + + # 调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=3 # 3 表示退款(退款) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + + + + kefu = current_user.kefu_profile + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + return Response({'code': 0, 'msg': '退款成功'}) + + + +class KefuRejectSettlementView(APIView): + """ + 客服拒绝结算接口(仅平台订单,状态2/8) + 请求:POST /yonghu/kefujjjs + 参数:{ + "phone": "客服手机号", + "dingdan_id": "订单ID", + "tkly": "拒绝结算理由(必填)" + } + 认证:JWT + 返回:code=0 成功,其他失败 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + phone = request.data.get('phone', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + tkly = request.data.get('tkly', '').strip() + + if not phone or not dingdan_id or not tkly: + return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 客服身份验证 + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + logger.warning(f"手机号不匹配: {phone}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if current_user.user_type != 'kefu': + logger.warning(f"用户类型非客服: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + kefu = current_user.kefu_profile + except ObjectDoesNotExist: + logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if kefu.zhuangtai != 1: + logger.warning(f"客服账号已禁用: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 3. 查询订单,预加载平台扩展表 + try: + order = Dingdan.objects.select_related('pingtai_kuozhan').get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 4. 校验订单条件:平台发单(1)且状态为2或8 + if order.fadan_pingtai != 1 or order.zhuangtai not in [2, 8]: + return Response({'code': 400, 'msg': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算'}, status=status.HTTP_400_BAD_REQUEST) + + jiedan_dashou_id = order.jiedan_dashou_id + if not jiedan_dashou_id: + return Response({'code': 400, 'msg': '订单暂无接单打手'}, status=status.HTTP_400_BAD_REQUEST) + + # 5. 使用事务更新 + with transaction.atomic(): + # 5.1 更新订单主表 + order.zhuangtai = 5 # 已退款 + order.tkly = tkly # 拒绝理由 + order.clkf = phone # 记录客服手机号 + order.save() + + # 5.2 更新平台订单扩展表:标记为拒绝结算(假设字段 jjjs_biaoshi) + # 注意:需要确保 DingdanPingtai 模型中已添加 jjjs_biaoshi 字段,IntegerField,默认0 + if hasattr(order, 'pingtai_kuozhan') and order.pingtai_kuozhan: + pingtai_ext = order.pingtai_kuozhan + pingtai_ext.jjjs_biaoshi = 1 # 1表示拒绝结算 + pingtai_ext.save() + else: + # 理论上应该存在,但若不存在则记录日志 + logger.warning(f"订单 {dingdan_id} 缺少平台扩展表,无法标记拒绝结算") + + # 5.3 更新打手扩展表 + try: + dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.zhuangtai = 1 # 打手状态设为空闲 + dashou_profile.tuikuanliang += 1 # 退款订单总量+1 + dashou_profile.save() + logger.info(f"拒绝结算:打手 {jiedan_dashou_id} 退款订单+1,状态空闲") + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"拒绝结算:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新") + except Exception as e: + logger.error(f"拒绝结算更新打手异常: {str(e)}", exc_info=True) + + # 5.4 更新客服扩展表统计数据 + + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=3 # 3 表示退款(退款) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + + + + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + return Response({'code': 0, 'msg': '拒绝结算成功'}) + + + +class KefuTransferHallView(APIView): + """ + 客服恢复大厅(转移大厅)接口 + 请求:POST /yonghu/kefuzydt + 参数:{ + "phone": "客服手机号", + "dingdan_id": "订单ID" + } + 认证:JWT + 返回:code=0 成功 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + phone = request.data.get('phone', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not phone or not dingdan_id: + return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 客服身份验证(同前,略,但必须完整实现) + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + if current_user.user_type != 'kefu': + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + try: + kefu = current_user.kefu_profile + if kefu.zhuangtai != 1: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + except ObjectDoesNotExist: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 3. 查询订单 + try: + order = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 4. 校验订单状态(允许状态:2,8,4) + if order.zhuangtai not in [2, 8, 4]: + return Response({'code': 400, 'msg': '订单状态不允许转移大厅'}, status=status.HTTP_400_BAD_REQUEST) + + + if order.jiedan_dashou_id: + UserDashou.objects.filter(user__yonghuid=order.jiedan_dashou_id).update(zhuangtai=1) + else: + pass + + # 5. 根据是否有指定ID决定新状态 + if order.zhiding_id: + new_status = 7 # 指定中 + else: + new_status = 1 # 下单中 + + # 6. 使用事务更新 + with transaction.atomic(): + # 清空接单打手ID,更新状态,记录客服 + order.jiedan_dashou_id = None + order.zhuangtai = new_status + order.clkf = phone + order.save() + + return Response({'code': 0, 'msg': '转移大厅成功'}) + + +class KefuCancelDesignationView(APIView): + """ + 客服取消指定接口(仅状态7) + 请求:POST /yonghu/kefuqxzd + 参数:{ + "phone": "客服手机号", + "dingdan_id": "订单ID" + } + 认证:JWT + 返回:code=0 成功 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + phone = request.data.get('phone', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not phone or not dingdan_id: + return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 客服身份验证(同前,略,但必须完整实现) + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + if current_user.user_type != 'kefu': + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + try: + kefu = current_user.kefu_profile + if kefu.zhuangtai != 1: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + except ObjectDoesNotExist: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 3. 查询订单 + try: + order = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 4. 校验订单状态为7(指定中) + + if order.zhuangtai != 7: + print(999) + return Response({'code': 400, 'msg': '仅状态为指定中(7)的订单可取消指定'}, status=status.HTTP_400_BAD_REQUEST) + + # 5. 使用事务更新 + with transaction.atomic(): + # 状态改为1(下单中),清空指定ID,记录客服 + order.zhuangtai = 1 + order.zhiding_id = None + order.clkf = phone + order.save() + + return Response({'code': 0, 'msg': '取消指定成功'}) + + + + + + + + +class KefuGetDashouListView(APIView): + """ + 客服获取打手列表接口(仅打手类型) + 请求:POST /yonghu/kefuhqdslb + 参数:{ + "phone": "客服手机号", + "keyword": "搜索关键词(可选,搜索用户ID或昵称)", + "page": "页码(从1开始)", + "page_size": "每页数量" + } + 认证:JWT + 返回:{ + "code": 0, + "data": { + "list": [打手对象], + "has_more": true/false + } + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + phone = request.data.get('zhanghao', '').strip() + keyword = request.data.get('keyword', '').strip() + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + except ValueError: + return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST) + + if not phone: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 客服身份验证 + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + logger.warning(f"手机号不匹配: {phone}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if current_user.user_type != 'kefu': + logger.warning(f"用户类型非客服: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + kefu = current_user.kefu_profile + except ObjectDoesNotExist: + logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if kefu.zhuangtai != 1: + logger.warning(f"客服账号已禁用: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 3. 构建查询条件(只查询打手) + # 通过 UserDashou 扩展表关联 UserMain + queryset = UserDashou.objects.select_related('user').all() + + if keyword: + # 搜索用户ID或打手昵称 + queryset = queryset.filter( + Q(user__yonghuid__icontains=keyword) | + Q(nicheng__icontains=keyword) + ) + + # 4. 计算总数并分页 + total = queryset.count() + offset = (page - 1) * page_size + dashou_list = queryset.order_by('-create_time')[offset:offset + page_size] + + # 5. 构建返回数据(只取需要的字段) + result = [] + for dashou in dashou_list: + user = dashou.user + result.append({ + 'yonghuid': user.yonghuid, + 'avatar': user.avatar or '', + 'zaixianzhuangtai': dashou.zaixianzhuangtai, + 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, + 'nicheng': dashou.nicheng or '', + 'zhuangtai': dashou.zhuangtai, + }) + + # 6. 判断是否有更多数据 + has_more = (offset + len(dashou_list)) < total + + return Response({ + 'code': 0, + 'data': { + 'list': result, + 'has_more': has_more + } + }) + + + + + + + +class KefuGetDashouDetailView(APIView): + """ + 客服获取打手详情接口 + 请求:POST /yonghu/kefuhqdsxq + 参数:{ + "phone": "客服手机号", + "uid": "打手ID (yonghuid)" + } + 认证:JWT + 返回:code=0 + 打手信息 + 会员列表 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + phone = request.data.get('phone', '').strip() + uid = request.data.get('uid', '').strip() + + if not phone or not uid: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 客服身份验证 + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + logger.warning(f"手机号不匹配: {phone}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if current_user.user_type != 'kefu': + logger.warning(f"用户类型非客服: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + kefu = current_user.kefu_profile + except ObjectDoesNotExist: + logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if kefu.zhuangtai != 1: + logger.warning(f"客服账号已禁用: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 3. 查询打手主表和扩展表 + try: + # 使用 select_related 预加载 dashou_profile,减少数据库查询 + user_main = UserMain.objects.select_related('dashou_profile').get( + yonghuid=uid + ) + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 验证用户类型是否为打手(可选,但建议验证) + # 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册) + try: + dashou = user_main.dashou_profile + except ObjectDoesNotExist: + # 用户存在但未注册打手身份,返回基本信息 + user_info = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar or '', + 'zaixianzhuangtai': 0, + 'zhanghaozhuangtai': 0, + 'zhuangtai': 0, + 'nicheng': '', + 'chenghao': '', + 'dianhua': '', + 'wechat': '', + 'phone': user_main.phone or '', + 'create_time': user_main.create_time, + 'ip': user_main.ip or '', + 'jiedanzongliang': 0, + 'chengjiaozongliang': 0, + 'tuikuanliang': 0, + 'jinrijiedan': 0, + 'yue': 0.00, + 'zonge': 0.00, + 'jifen': 0, + 'yajin': 0.00, + 'jinrishouyi': 0.00, + 'jinyueshouyi': 0.00, + 'yaoqingren': '', + 'jieshao': '', + } + member_list = [] + return Response({ + 'code': 0, + 'data': { + 'user_info': user_info, + 'member_list': member_list + } + }) + + # 4. 构建打手信息 + user_info = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar or '', + 'zaixianzhuangtai': dashou.zaixianzhuangtai, + 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, + 'zhuangtai': dashou.zhuangtai, + 'nicheng': dashou.nicheng or '', + 'chenghao': dashou.chenghao or '', + 'dianhua': dashou.dianhua or '', + 'wechat': dashou.wechat or '', + 'phone': user_main.phone or '', + 'create_time': user_main.create_time, + 'ip': user_main.ip or '', + 'jiedanzongliang': dashou.jiedanzongliang, + 'chengjiaozongliang': dashou.chengjiaozongliang, + 'tuikuanliang': dashou.tuikuanliang, + 'jinrijiedan': dashou.jinrijiedan, + 'yue': float(dashou.yue) if dashou.yue else 0.00, + 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, + 'jifen': dashou.jifen, + 'yajin': float(dashou.yajin) if dashou.yajin else 0.00, + 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00, + 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00, + 'yaoqingren': dashou.yaoqingren or '', + 'jieshao': dashou.jieshao or '', + } + + # 5. 查询打手会员信息(仅未过期) + member_records = Huiyuangoumai.objects.filter( + yonghu_id=uid + ).order_by('-create_time') + + member_list = [] + for record in member_records: + # 调用模型方法检查是否过期 + if hasattr(record, 'jiance_shifou_daoqi'): + is_expired = record.jiance_shifou_daoqi() + else: + # 如果方法不存在,简单判断 + is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True + + if not is_expired: + # 获取会员详情 + try: + huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id) + huiyuan_name = huiyuan.jieshao + except Huiyuan.DoesNotExist: + huiyuan_name = '未知会员' + + member_list.append({ + 'id': record.id, + 'huiyuan_name': huiyuan_name, + 'daoqi_time': record.daoqi_time, + 'is_active': record.huiyuan_zhuangtai == 1, + }) + + return Response({ + 'code': 0, + 'data': { + 'user_info': user_info, + 'member_list': member_list + } + }) + + + + +class KefuUpdateDashouView(APIView): + permission_classes = [IsAuthenticated] + """ + 客服修改打手信息接口 + 支持操作: + - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb) + - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb) + - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd) + - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee) + - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee) + - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee) + - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg) + """ + def post(self, request): + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + dashou_id = request.data.get('dashou_id', '').strip() + caozuo = request.data.get('caozuo', '').strip() + value = request.data.get('value') # 金额、积分、天数等 + huiyuan_id = request.data.get('huiyuan_id', '').strip() # 会员ID(加/减会员时使用) + + if not username or not dashou_id or not caozuo: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 权限校验(统一方法) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 查询打手是否存在 + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id, user_type='dashou') + dashou_profile = dashou_user.dashou_profile + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '打手不存在'}) + except UserDashou.DoesNotExist: + return Response({'code': 404, 'msg': '打手扩展信息缺失'}) + + # 4. 根据操作类型执行(事务内) + with transaction.atomic(): + try: + # 记录修改前的值(用于日志) + before = { + 'yue': dashou_profile.yue, + 'yajin': dashou_profile.yajin, + 'jifen': dashou_profile.jifen, + 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai, + 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, + 'zhuangtai': dashou_profile.zhuangtai, + } + + # ---------- 余额操作 ---------- + if caozuo == 'jian_yue': + if '001aa' not in permissions: + return Response({'code': 403, 'msg': '无权限减打手余额'}) + amount = Decimal(str(value)) + if dashou_profile.yue < amount: + return Response({'code': 400, 'msg': '余额不足'}) + dashou_profile.yue -= amount + dashou_profile.save() + after_yue = dashou_profile.yue + # 记录日志 + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, # 2=打手 + xiugaitijiao=after_yue, + xiugaitijiaoq=before['yue'], + ) + return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after_yue)}}) + + elif caozuo == 'jia_yue': + if '001bb' not in permissions: + return Response({'code': 403, 'msg': '无权限加打手余额'}) + amount = Decimal(str(value)) + dashou_profile.yue += amount + dashou_profile.save() + after_yue = dashou_profile.yue + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + xiugaitijiao=after_yue, + xiugaitijiaoq=before['yue'], + ) + return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after_yue)}}) + + # ---------- 押金操作 ---------- + elif caozuo == 'jian_yajin': + if '001aa' not in permissions: + return Response({'code': 403, 'msg': '无权限减打手押金'}) + amount = Decimal(str(value)) + if dashou_profile.yajin < amount: + return Response({'code': 400, 'msg': '押金不足'}) + dashou_profile.yajin -= amount + dashou_profile.save() + after_yajin = dashou_profile.yajin + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + yajin=after_yajin, + yyajin=before['yajin'], + ) + return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after_yajin)}}) + + elif caozuo == 'jia_yajin': + if '001bb' not in permissions: + return Response({'code': 403, 'msg': '无权限加打手押金'}) + amount = Decimal(str(value)) + dashou_profile.yajin += amount + dashou_profile.save() + after_yajin = dashou_profile.yajin + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + yajin=after_yajin, + yyajin=before['yajin'], + ) + return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after_yajin)}}) + + # ---------- 积分操作 ---------- + elif caozuo == 'jian_jifen': + if '001cc' not in permissions: + return Response({'code': 403, 'msg': '无权限减打手积分'}) + jifen = int(value) + if dashou_profile.jifen < jifen: + return Response({'code': 400, 'msg': '积分不足'}) + dashou_profile.jifen -= jifen + dashou_profile.save() + after_jifen = dashou_profile.jifen + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + jifen=after_jifen, + yjifen=before['jifen'], + ) + return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after_jifen}}) + + elif caozuo == 'jia_jifen': + if '001dd' not in permissions: + return Response({'code': 403, 'msg': '无权限加打手积分'}) + jifen = int(value) + dashou_profile.jifen += jifen + dashou_profile.save() + after_jifen = dashou_profile.jifen + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + jifen=after_jifen, + yjifen=before['jifen'], + ) + return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after_jifen}}) + + # ---------- 状态修改 ---------- + elif caozuo == 'gai_zhanghaozhuangtai': + if '001ee' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手账号状态'}) + new_status = int(value) + if new_status not in [0, 1]: + return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'}) + dashou_profile.zhanghaozhuangtai = new_status + dashou_profile.save() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + qitashuoming=f'修改账号状态为{new_status}', + ) + return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}}) + + elif caozuo == 'gai_zaixianzhuangtai': + if '001ee' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手在线状态'}) + new_status = int(value) + if new_status not in [0, 1]: + return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'}) + dashou_profile.zaixianzhuangtai = new_status + dashou_profile.save() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + qitashuoming=f'修改在线状态为{new_status}', + ) + return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}}) + + elif caozuo == 'gai_zhuangtai': + if '001ee' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手接单状态'}) + new_status = int(value) + if new_status not in [0, 1]: + return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'}) + dashou_profile.zhuangtai = new_status + dashou_profile.save() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + qitashuoming=f'修改接单状态为{new_status}', + ) + return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}}) + + # ---------- 会员操作 ---------- + elif caozuo == 'jia_huiyuan': + if '001ff' not in permissions: + return Response({'code': 403, 'msg': '无权限给打手加会员'}) + if not huiyuan_id: + return Response({'code': 400, 'msg': '缺少会员ID'}) + days = int(value) if value else 30 # 默认加30天 + try: + huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response({'code': 404, 'msg': '会员类型不存在'}) + # 获取或创建购买记录 + record, created = Huiyuangoumai.objects.select_for_update().get_or_create( + yonghu_id=dashou_id, + huiyuan_id=huiyuan_id, + defaults={ + 'huiyuan_zhuangtai': 1, + 'jieshao': huiyuan.jieshao, + 'daoqi_time': timezone.now() + timedelta(days=days), + } + ) + if not created: + # 延长有效期 + new_daoqi = record.daoqi_time + timedelta(days=days) + record.daoqi_time = new_daoqi + record.huiyuan_zhuangtai = 1 + record.save() + # 记录修改日志 + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + huiyuants=days, + huiyuan_id=huiyuan_id, + qitashuoming=f'加会员 {huiyuan_id},增加{days}天', + ) + return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}}) + + elif caozuo == 'jian_huiyuan': + if '001gg' not in permissions: + return Response({'code': 403, 'msg': '无权限给打手减会员'}) + if not huiyuan_id: + return Response({'code': 400, 'msg': '缺少会员ID'}) + try: + record = Huiyuangoumai.objects.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id) + except Huiyuangoumai.DoesNotExist: + return Response({'code': 404, 'msg': '该打手未购买此会员'}) + # 删除会员记录(或设置为过期,根据业务要求) + record.delete() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + huiyuan_id=huiyuan_id, + qitashuoming=f'移除会员 {huiyuan_id}', + ) + return Response({'code': 0, 'msg': '减会员成功'}) + + else: + return Response({'code': 400, 'msg': '未知操作类型'}) + + except Exception as e: + logger.error(f"修改打手信息异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + + +class KefuPunishmentListView(APIView): + """ + 客服获取处罚记录列表接口 + 请求:POST /yonghu/kefu_cfgl + 参数:{ + "phone": "客服手机号", + "status": [0,3] 或 [1,2], # 状态数组 + "dingdan_id": "订单ID(可选)", + "dashouid": "打手ID(可选)", + "page": 1, + "page_size": 20 + } + 认证:JWT + 返回:{ + "code": 0, + "data": { + "list": [处罚记录], + "total": 总数, + "stats": { + "zongshu": 总记录数, + "daichuli": 待处理数, + "yichuli": 已处理数 + } + } + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + phone = request.data.get('phone', '').strip() + status_list = request.data.get('status') + dingdan_id = request.data.get('dingdan_id', '').strip() + dashouid = request.data.get('dashouid', '').strip() + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + except ValueError: + return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST) + + if not phone: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 客服身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('phone', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 3. 检查平台订单管理权限(002ab) + if '005aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看平台订单列表'}) + + # 3. 获取统计信息(不分页) + stats = { + 'zongshu': Chufajilu.objects.count(), + 'daichuli': Chufajilu.objects.filter(sqzhuangtai__in=[0, 3]).count(), + 'yichuli': Chufajilu.objects.filter(sqzhuangtai__in=[1, 2]).count(), + } + + # 4. 构建查询条件 + queryset = Chufajilu.objects.all() + + if status_list and isinstance(status_list, list): + queryset = queryset.filter(sqzhuangtai__in=status_list) + + if dingdan_id: + queryset = queryset.filter(dingdan_id=dingdan_id) + + if dashouid: + queryset = queryset.filter(dashouid=dashouid) + + # 5. 分页 + total = queryset.count() + offset = (page - 1) * page_size + records = queryset.order_by('-create_time')[offset:offset + page_size] + + # 6. 构建返回列表 + list_data = [] + for item in records: + list_data.append({ + 'dingdan_id': item.dingdan_id, + 'dashouid': item.dashouid, + 'qingqiuid': item.qingqiuid, + 'cfliyou': item.cfliyou, + 'sqzhuangtai': item.sqzhuangtai, + 'create_time': item.create_time, + 'update_time': item.update_time, + # 其他字段按需添加 + }) + + return Response({ + 'code': 0, + 'data': { + 'list': list_data, + 'total': total, + 'stats': stats + } + }) + + + + + + +class KefuGetOrderDetailView(APIView): + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 强制打印到控制台并立即刷新 + def p(msg): + #print(f"[订单详情] {msg}") + sys.stdout.flush() + + #p("========== 视图开始执行 ==========") + + # 1. 打印原始请求数据 + #p(f"request.data: {request.data}") + + phone = request.data.get('phone', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + #p(f"phone={phone}, dingdan_id={dingdan_id}") + + if not phone or not dingdan_id: + #p("参数不完整,返回401") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 获取当前用户 + current_user = request.user + + + # 验证手机号 + if getattr(current_user, 'phone', '') != phone: + #p("手机号不匹配") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 验证用户类型 + if current_user.user_type != 'kefu': + #p("用户类型不是客服") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 客服扩展表 + try: + kefu = current_user.kefu_profile + #p(f"kefu_profile: {kefu}") + if kefu.zhuangtai != 1: + #p(f"客服状态异常: {kefu.zhuangtai}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + except Exception as e: + #p(f"获取客服扩展表异常: {e}") + return Response({'code': 500, 'msg': '服务器错误'}, status=500) + + # 3. 查询订单 + try: + #p(f"开始查询订单: {dingdan_id}") + dingdan_obj = Dingdan.objects.select_related( + 'pingtai_kuozhan', 'shangjia_kuozhan' + ).get(dingdan_id=dingdan_id) + #p("订单查询成功") + except Dingdan.DoesNotExist: + #p("订单不存在") + return Response({'code': 404, 'msg': '订单不存在'}, status=404) + except Exception as e: + #p(f"订单查询异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器错误'}, status=500) + + # 4. 构建基础数据 + try: + response_data = { + 'dingdan_id': dingdan_obj.dingdan_id or '', + 'zhuangtai': dingdan_obj.zhuangtai or 0, + 'fadanpingtai': dingdan_obj.fadan_pingtai or 0, + 'jiage': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00, + 'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00, + 'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '', + 'dashou_liuyan': dingdan_obj.dashou_liuyan or '', + 'zhiding_id': dingdan_obj.zhiding_id or '', + 'shangpin_id': dingdan_obj.shangpin_id or '', + 'leixing': dingdan_obj.leixing_id or 0, + 'tupian_url': dingdan_obj.tupian or '', + 'jieshao': dingdan_obj.jieshao or '', + 'beizhu': dingdan_obj.beizhu or '', + 'tkly': dingdan_obj.tkly or '', + 'nicheng': dingdan_obj.nicheng or '', + 'clkf': dingdan_obj.clkf or '', + 'chuangjianshijian': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.create_time else '', + 'genggaishijian': dingdan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.update_time else '', + } + #p("基础数据构建完成") + except Exception as e: + #p(f"构建基础数据异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器错误'}, status=500) + + # 5. 扩展表数据 + try: + fadan_pingtai = dingdan_obj.fadan_pingtai + if fadan_pingtai == 1: + if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan: + pingtai = dingdan_obj.pingtai_kuozhan + response_data['pingtai_kuozhan'] = { + 'laoban_id': pingtai.laoban_id or '', + 'laoban_pingjia': pingtai.laoban_pingjia or '', + } + else: + response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''} + elif fadan_pingtai == 2: + if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan: + shangjia = dingdan_obj.shangjia_kuozhan + response_data['shangjia_kuozhan'] = { + 'shangjia_id': shangjia.shangjia_id or '', + 'sjnicheng': shangjia.sjnicheng or '', + 'shangjia_pingjia': shangjia.shangjia_pingjia or '', + 'cfliyou': shangjia.cfliyou or '', + 'sqzhuangtai': shangjia.sqzhuangtai if shangjia.sqzhuangtai is not None else 0, + 'bhliyou': shangjia.bhliyou or '', + } + else: + response_data['shangjia_kuozhan'] = { + 'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '', + 'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': '' + } + #p("扩展数据构建成功") + except Exception as e: + #p(f"扩展数据异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器错误'}, status=500) + + # 6. 打手图片 + try: + dashou_images = Dashoutupian.objects.filter(dingdan_id=dingdan_id).values_list('tupian', flat=True) + response_data['dashou_images'] = [img for img in dashou_images if img] + #p(f"打手图片数量: {len(response_data['dashou_images'])}") + except Exception as e: + #p(f"打手图片异常: {traceback.format_exc()}") + response_data['dashou_images'] = [] + + # 7. 退款理由 + try: + tuikuan_record = Tuikuanjilu.objects.filter(dingdan_id=dingdan_id).order_by('-create_time').first() + response_data['tuikuan_liyou'] = tuikuan_record.liyou if tuikuan_record and tuikuan_record.liyou else '' + #p("退款理由查询完成") + except Exception as e: + #p(f"退款理由异常: {traceback.format_exc()}") + response_data['tuikuan_liyou'] = '' + + #p("视图正常结束,返回成功") + return Response({'code': 0, 'data': response_data}) + + + + + + + + + + + + + + + + +class KefuPunishmentDetailView(APIView): + """ + 客服获取处罚详情接口 + 请求:POST /yonghu/kefu_cf_detail + 参数:{ + "phone": "客服手机号", + "dingdan_id": "订单ID" + } + 认证:JWT + 返回:code=0 + 处罚记录详情(含图片) + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + phone = request.data.get('phone', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not phone or not dingdan_id: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 客服身份验证 + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + logger.warning(f"手机号不匹配: {phone}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if current_user.user_type != 'kefu': + logger.warning(f"用户类型非客服: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + kefu = current_user.kefu_profile + except ObjectDoesNotExist: + logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if kefu.zhuangtai != 1: + logger.warning(f"客服账号已禁用: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 查询处罚记录(按创建时间倒序取最新一条) + try: + record = Chufajilu.objects.filter(dingdan_id=dingdan_id).order_by('-create_time').first() + if not record: + return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"查询处罚记录失败: {e}") + return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 查询商家证据图片 + zhengju_images = [] + shensu_images = [] + try: + # 商家证据图片:上传者为请求人 (qingqiuid) + zhengju_qs = Chufatupian.objects.filter(dingdan_id=dingdan_id, yonghuid=record.qingqiuid) + zhengju_images = [item.tupian for item in zhengju_qs if item.tupian] + + # 打手申诉图片:上传者为打手 (dashouid) + shensu_qs = Chufatupian.objects.filter(dingdan_id=dingdan_id, yonghuid=record.dashouid) + shensu_images = [item.tupian for item in shensu_qs if item.tupian] + except Exception as e: + logger.error(f"查询图片失败: {e}") + + # 构建返回数据 + data = { + 'dingdan_id': record.dingdan_id, + 'dashouid': record.dashouid, + 'qingqiuid': record.qingqiuid, + 'chuliid': record.chuliid, + 'cfliyou': record.cfliyou, + 'sqzhuangtai': record.sqzhuangtai, + 'bhliyou': record.bhliyou, + 'jifen': record.jifen, + 'ssliyou': record.ssliyou or '', + 'zhengju_tupian': zhengju_images, + 'shensu_tupian': shensu_images, + 'create_time': record.create_time, + 'update_time': record.update_time, + } + + return Response({'code': 0, 'data': data}) + + + + +class KefuPunishmentActionView(APIView): + """ + 客服同意/驳回处罚接口 + 请求:POST /yonghu/kefu_cf_action + 参数:{ + "phone": "客服手机号", + "dingdan_id": "订单ID", + "action": 1(同意) / 2(驳回), + "reject_reason": "驳回理由(驳回时必填)" + } + 认证:JWT + 返回:code=0 成功 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + phone = request.data.get('phone', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + action = request.data.get('action') + reject_reason = request.data.get('reject_reason', '').strip() + + if not phone or not dingdan_id or action not in [1, 2]: + return Response({'code': 401, 'msg': '参数错误'}, status=status.HTTP_400_BAD_REQUEST) + + if action == 2 and not reject_reason: + return Response({'code': 400, 'msg': '驳回理由不能为空'}, status=status.HTTP_400_BAD_REQUEST) + + # 客服身份验证(完整验证,此处略) + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + if current_user.user_type != 'kefu': + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + try: + kefu = current_user.kefu_profile + if kefu.zhuangtai != 1: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + except ObjectDoesNotExist: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 查询待处理的处罚记录(状态0或3) + try: + record = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + sqzhuangtai__in=[0, 3] + ).order_by('-create_time').first() + if not record: + exists = Chufajilu.objects.filter(dingdan_id=dingdan_id).exists() + if exists: + return Response({'code': 400, 'msg': '该处罚记录已处理'}, status=status.HTTP_400_BAD_REQUEST) + else: + return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"查询处罚记录失败: {e}") + return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + with transaction.atomic(): + # 处理重复记录:同一订单的其他待处理/申诉中记录标记为驳回 + duplicate_records = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + sqzhuangtai__in=[0, 3] + ).exclude(id=record.id) + for dup in duplicate_records: + dup.sqzhuangtai = 2 + dup.chuliid = phone + dup.bhliyou = "系统自动驳回:存在重复处罚记录" + dup.save() + logger.info(f"自动驳回重复处罚记录 ID={dup.id}") + + if action == 1: # 同意 + # 可选验证打手存在 + if record.dashouid: + try: + dashou = UserDashou.objects.get(user__yonghuid=record.dashouid) + except UserDashou.DoesNotExist: + return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST) + + record.sqzhuangtai = 1 + record.chuliid = phone + record.save() + + # 更新商家订单扩展表 + try: + dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + if hasattr(dingdan, 'shangjia_kuozhan'): + dingdan.shangjia_kuozhan.sqzhuangtai = 1 + dingdan.shangjia_kuozhan.save() + except Exception as e: + logger.warning(f"更新商家扩展表失败: {e}") + + elif action == 2: # 驳回 + # 返还积分 + if record.jifen > 0 and record.dashouid: + try: + dashou = UserDashou.objects.get(user__yonghuid=record.dashouid) + if dashou.jifen <= 1000: # 安全限制 + dashou.jifen += record.jifen + dashou.save() + logger.info(f"驳回处罚,打手{record.dashouid}返还积分{record.jifen}") + else: + logger.warning(f"打手积分异常({dashou.jifen}),不再返还") + except UserDashou.DoesNotExist: + logger.warning(f"打手{record.dashouid}不存在,无法返还积分") + + record.sqzhuangtai = 2 + record.chuliid = phone + record.bhliyou = reject_reason + record.save() + + try: + dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + if hasattr(dingdan, 'shangjia_kuozhan'): + dingdan.shangjia_kuozhan.sqzhuangtai = 2 + dingdan.shangjia_kuozhan.bhliyou = reject_reason + dingdan.shangjia_kuozhan.save() + except Exception as e: + logger.warning(f"更新商家扩展表失败: {e}") + + return Response({'code': 0, 'msg': '处理成功'}) + + + + + + + + +class KefuForceCompleteView(APIView): + """ + 客服强制结单接口(仅本地订单) + 请求:POST /yonghu/kefuqiangzhijiedan + 参数:{"phone": "客服手机号", "dingdan_id": "订单ID"} + 认证:JWT,仅限状态正常的客服 + 逻辑: + 1. 验证客服身份及状态 + 2. 查询订单并加锁(事务内) + 3. 校验订单状态是否为8(结算中) + 4. 校验订单必须有接单打手且打手分成金额有效 + 5. 给打手增加余额、成交统计、今日/本月收益 + 6. 如果是商家发单,更新商家成交订单数量 + 7. 更新订单状态为3(已完成) + 8. 更新客服处理统计 + 9. 返回成功 + """ + permission_classes = [permissions.IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + phone = request.data.get('phone', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not phone or not dingdan_id: + return Response({'code': 401, 'msg': '参数缺失'}) + + # 1. 客服身份验证 + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + return Response({'code': 401, 'msg': '认证失败'}) + + if current_user.user_type != 'kefu': + return Response({'code': 401, 'msg': '认证失败'}) + + try: + kefu = current_user.kefu_profile + except ObjectDoesNotExist: + return Response({'code': 401, 'msg': '认证失败'}) + + if kefu.zhuangtai != 1: + return Response({'code': 401, 'msg': '客服状态异常'}) + + # 2. 订单处理(事务内) + try: + with transaction.atomic(): + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + + # 3. 校验订单状态 + if order.zhuangtai != 8: + return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'}) + + # 4. 校验必要字段 + dashou_id = order.jiedan_dashou_id + dashou_fencheng = order.dashou_fencheng + if not dashou_id: + return Response({'code': 400, 'msg': '订单无接单打手'}) + if not dashou_fencheng or dashou_fencheng <= 0: + return Response({'code': 400, 'msg': '打手分成金额无效'}) + + # 5. 给打手结算 + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + # 原子更新(避免并发重复加) + dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1 + dashou_profile.yue = F('yue') + dashou_fencheng + dashou_profile.zonge = F('zonge') + dashou_fencheng + dashou_profile.jinrishouyi = F('jinrishouyi') + dashou_fencheng + dashou_profile.jinyueshouyi = F('jinyueshouyi') + dashou_fencheng + dashou_profile.save(update_fields=[ + 'chengjiaozongliang', 'yue', 'zonge', + 'jinrishouyi', 'jinyueshouyi' + ]) + except (UserMain.DoesNotExist, AttributeError): + return Response({'code': 400, 'msg': '打手不存在或扩展表缺失'}) + + # 6. 如果是商家发单,更新商家成交订单数量 + if order.fadan_pingtai == 2: + try: + # 通过 DingdanShangjia 获取商家ID + shangjia_ext = order.shangjia_kuozhan # 假设 related_name='shangjia_kuozhan' + if shangjia_ext and shangjia_ext.shangjia_id: + shangjia_user = UserMain.objects.get(yonghuid=shangjia_ext.shangjia_id) + shangjia_profile = shangjia_user.shop_profile + shangjia_profile.chengjiao = F('chengjiao') + 1 + shangjia_profile.save(update_fields=['chengjiao']) + + # (行为驱动,更安全) + update_shangjia_daily( + yonghuid=shangjia_ext.shangjia_id, + amount=Decimal(str(order.jine)), + action=2 # 2 = 结算 + ) + + except ObjectDoesNotExist: + # 没有商家扩展记录则忽略 + pass + + # 7. 更新订单状态 + order.zhuangtai = 3 + order.clkf = phone + order.save(update_fields=['zhuangtai', 'clkf']) + + + if order.fadan_pingtai == 1: + + # ========== 新增:商品和店铺每日统计 ========== + try: + # 商品每日统计(只要有商品ID就执行) + if order.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(order, 'pingtai_kuozhan'): + pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=order.shangpin_id, + caozuo_leixing=2, # 2 = 结单 + dingdan_jiage=order.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + dianpu_id = None + if hasattr(order, 'pingtai_kuozhan'): + dianpu_id = order.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=2, # 2 = 结单 + dingdan_jiage=order.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=dashou_fencheng, + action=2 # 2 表示成交(结算) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + + # 商家订单管事分红 + if order.fadan_pingtai == 2: + settle_shangjia_order_guanshi_fenhong(order, dashou_id) + + # 8. 更新客服统计 + kefu.jinrichuli = F('jinrichuli') + 1 + kefu.jinyuechuli = F('jinyuechuli') + 1 + kefu.zongchuli = F('zongchuli') + 1 + kefu.save(update_fields=['jinrichuli', 'jinyuechuli', 'zongchuli']) + + return Response({'code': 0, 'msg': '强制结单成功'}) + + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}) + except Exception as e: + logger.error(f"强制结单异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + +kefu_txsh_list_logger = logging.getLogger('yonghu.kefu_txsh_list') +if not kefu_txsh_list_logger.handlers: + _console_handler = logging.StreamHandler(sys.stderr) + _console_handler.setFormatter(logging.Formatter( + '[%(asctime)s] %(levelname)s %(name)s: %(message)s' + )) + kefu_txsh_list_logger.addHandler(_console_handler) + kefu_txsh_list_logger.setLevel(logging.DEBUG) + kefu_txsh_list_logger.propagate = False + +_TIXIAN_LIST_BASE_FIELDS = ( + 'id', 'yonghuid', 'avatar', 'phone', 'nicheng', 'leixing', + 'zhifu', 'skzhanghao', 'jine', 'zhuangtai', 'fangshi', + 'shenheid', 'bhliyou', 'create_time', 'update_time', +) +_TIXIAN_LIST_AUDIT_FIELDS = ('shenhe_jilu_id', 'shenhe_danhao') + + +class KefuWithdrawListView(APIView): + """ + 客服获取提现审核列表接口 + 请求:POST /yonghu/kefu_txsh_list + 参数:{ + "phone": "客服手机号", + "page": 1, + "page_size": 20, + "status": 1(待审核)/2(已处理), + "type": 1(打手佣金)/2(管事分红)/3(组长分红)/4(审核官分佣)/5(打手押金)/6(商家余额), 可选 + "payment": 1(微信)/2(支付宝) 可选, + "search_uid": "用户ID搜索" 可选, + "date": "YYYY-MM-DD" 可选, + "min_amount": 0, # 最小提现金额(元)可选 + "max_amount": 999999, # 最大提现金额(元)可选 + "dakuan_mode": 1 # 打款方式:1手动打款 2自动打款,可选 + } + 认证:JWT + 返回:{ + "code": 0, + "data": { + "list": [提现记录], + "total": 总数, + "stats": { + "total": 总记录数, + "awaiting": 待审核数, + "processed": 已处理数, + "total_amount": "总金额(元)" + } + } + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def _safe_request_params(self, request): + try: + return dict(request.data) + except Exception: + return {'raw': str(request.data)} + + def _shenhe_jilu_field_available(self): + """探测 shenhe_jilu_id 字段是否已在数据库中就绪""" + try: + Tixianjilu.objects.values_list('shenhe_jilu_id', flat=True).first() + return True + except DatabaseError as e: + kefu_txsh_list_logger.error( + 'kefu_txsh_list: shenhe_jilu_id 字段不可用,将按旧表结构查询: %s', e, + exc_info=True, + ) + return False + + def _list_queryset(self, q, shenhe_field_ok): + """字段未迁移时 only 旧字段,避免 SELECT 不存在的列导致 500""" + fields = _TIXIAN_LIST_BASE_FIELDS + ( + _TIXIAN_LIST_AUDIT_FIELDS if shenhe_field_ok else () + ) + return Tixianjilu.objects.filter(q).only(*fields) + + def _resolve_row_dakuan_mode(self, item, audit_mode_map, shenhe_field_ok): + """无关联审核或审核表未标记自动 → 默认手动(1)""" + if not shenhe_field_ok: + return 1 + sid = getattr(item, 'shenhe_jilu_id', None) + if not sid: + return 1 + if audit_mode_map.get(sid) == 2: + return 2 + return 1 + + def post(self, request): + dakuan_mode_filter = request.data.get('dakuan_mode') + req_params = self._safe_request_params(request) + kefu_txsh_list_logger.info('kefu_txsh_list 请求开始 params=%s', req_params) + + try: + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST) + + if page < 1: + page = 1 + if page_size < 1 or page_size > 100: + page_size = min(max(page_size, 1), 100) + + status_filter = request.data.get('status') + type_filter = request.data.get('type') + payment_filter = request.data.get('payment') + search_uid = (request.data.get('search_uid') or '').strip() + date_filter = (request.data.get('date') or '').strip() + min_amount = request.data.get('min_amount') + max_amount = request.data.get('max_amount') + + phone = (request.data.get('phone') or '').strip() + if not phone: + return Response({'code': 401, 'msg': '缺少客服手机号'}, status=status.HTTP_401_UNAUTHORIZED) + + kefu, permissions = verify_kefu_permission(request, phone) + if kefu is None: + return permissions + if '005bb' not in permissions: + return Response({'code': 403, 'msg': '您无权查看平台订单列表'}) + + q = Q() + + try: + status_val = int(status_filter) if status_filter is not None and str(status_filter).strip() != '' else None + except (TypeError, ValueError): + status_val = None + + try: + dakuan_mode_val = int(dakuan_mode_filter) if dakuan_mode_filter is not None and str(dakuan_mode_filter).strip() != '' else None + except (TypeError, ValueError): + dakuan_mode_val = None + + # 未传打款方式参数 → 默认手动打款(1) + effective_dakuan = dakuan_mode_val if dakuan_mode_val in (1, 2) else 1 + + if status_val == 1: + q &= Q(zhuangtai=1) + elif status_val == 2: + if effective_dakuan == 2: + q &= Q(zhuangtai__in=[2, 3, 4, 5, 6]) + else: + q &= Q(zhuangtai__in=[2, 3]) + + if type_filter is not None and str(type_filter).strip() != '': + try: + type_val = int(type_filter) + if type_val in [1, 2, 3, 4, 5, 6]: + q &= Q(leixing=type_val) + except (TypeError, ValueError): + pass + + if payment_filter is not None and str(payment_filter).strip() != '': + try: + payment_val = int(payment_filter) + if payment_val in [1, 2]: + q &= Q(fangshi=payment_val) + except (TypeError, ValueError): + pass + + if search_uid: + q &= Q(yonghuid=search_uid) + + if date_filter: + q &= Q(update_time__date=date_filter) + + if min_amount is not None and str(min_amount).strip() != '': + try: + q &= Q(jine__gte=float(min_amount)) + except (TypeError, ValueError): + pass + if max_amount is not None and str(max_amount).strip() != '': + try: + q &= Q(jine__lte=float(max_amount)) + except (TypeError, ValueError): + pass + + shenhe_field_ok = self._shenhe_jilu_field_available() + if shenhe_field_ok: + if effective_dakuan == 2: + q &= Q(shenhe_jilu_id__isnull=False) + else: + q &= Q(shenhe_jilu_id__isnull=True) + elif effective_dakuan == 2: + kefu_txsh_list_logger.warning('kefu_txsh_list: 自动打款筛选不可用,返回空列表') + return Response({ + 'code': 0, + 'data': { + 'list': [], + 'total': 0, + 'stats': {'total': 0, 'awaiting': 0, 'processed': 0, 'total_amount': '0.00'}, + }, + }) + + processed_statuses = [2, 3, 4, 5, 6] if effective_dakuan == 2 else [2, 3] + + base_qs = self._list_queryset(q, shenhe_field_ok) + total_count = base_qs.count() + awaiting_count = self._list_queryset(q & Q(zhuangtai=1), shenhe_field_ok).count() + processed_count = self._list_queryset( + q & Q(zhuangtai__in=processed_statuses), shenhe_field_ok, + ).count() + agg = base_qs.aggregate(total=Sum('jine')) + total_amount = agg.get('total') or 0 + + stats = { + 'total': total_count, + 'awaiting': awaiting_count, + 'processed': processed_count, + 'total_amount': str(round(float(total_amount), 2)), + } + + queryset = base_qs.order_by('create_time') + total = queryset.count() + records = list(queryset[(page - 1) * page_size: page * page_size]) + + audit_ids = [ + getattr(item, 'shenhe_jilu_id', None) + for item in records if getattr(item, 'shenhe_jilu_id', None) + ] + audit_meta_map = {} + if audit_ids: + try: + audit_meta_map = load_audit_meta_map(audit_ids) + except Exception as e: + kefu_txsh_list_logger.error( + 'kefu_txsh_list: 查询审核表信息失败: %s', e, + exc_info=True, + ) + + audit_mode_map = { + aid: meta.get('dakuan_mode', 2) for aid, meta in audit_meta_map.items() + } + list_data = [] + for item in records: + shenhe_jilu_id = getattr(item, 'shenhe_jilu_id', None) if shenhe_field_ok else None + shenhe_danhao = (getattr(item, 'shenhe_danhao', None) or '') if shenhe_field_ok else '' + if shenhe_jilu_id and not shenhe_danhao: + shenhe_danhao = audit_meta_map.get(shenhe_jilu_id, {}).get('shenhe_danhao', '') + list_data.append({ + 'id': item.id, + 'shenhe_jilu_id': shenhe_jilu_id, + 'shenhe_danhao': shenhe_danhao, + 'yonghuid': item.yonghuid, + 'avatar': item.avatar or '', + 'phone': item.phone or '', + 'nicheng': item.nicheng or '', + 'leixing': item.leixing, + 'zhifu': item.zhifu or '', + 'skzhanghao': item.skzhanghao or '', + 'jine': str(item.jine) if item.jine is not None else '0.00', + 'zhuangtai': item.zhuangtai, + 'fangshi': item.fangshi, + 'dakuan_mode': self._resolve_row_dakuan_mode(item, audit_mode_map, shenhe_field_ok), + 'shenheid': item.shenheid or '', + 'bhliyou': item.bhliyou or '', + 'create_time': item.create_time.strftime('%Y-%m-%d %H:%M:%S') if item.create_time else '', + 'update_time': item.update_time.strftime('%Y-%m-%d %H:%M:%S') if item.update_time else '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': list_data, + 'total': total, + 'stats': stats, + }, + }) + + except DatabaseError as e: + kefu_txsh_list_logger.error( + 'kefu_txsh_list 数据库错误 dakuan_mode=%s params=%s err=%s', + dakuan_mode_filter, req_params, e, + exc_info=True, + ) + return Response({ + 'code': 500, + 'msg': '数据库查询失败,请确认已执行 python manage.py migrate yonghu', + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + except Exception as e: + kefu_txsh_list_logger.error( + 'kefu_txsh_list 接口异常 dakuan_mode=%s params=%s err=%s', + dakuan_mode_filter, req_params, e, + exc_info=True, + ) + return Response({ + 'code': 500, + 'msg': '获取提现列表失败', + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + +class KefuWithdrawDetailView(APIView): + """ + 客服获取提现详情接口 + 请求:POST /yonghu/kefu_txsh_detail + 参数:{ + "phone": "客服手机号", + "tixian_id": "提现记录ID" + } + 认证:JWT + 返回:code=0 + 提现记录详情 + 用户扩展信息 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + phone = request.data.get('phone', '').strip() + tixian_id = request.data.get('tixian_id') + + if not phone or not tixian_id: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 客服身份验证 + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + logger.warning(f"手机号不匹配: {phone}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if current_user.user_type != 'kefu': + logger.warning(f"用户类型非客服: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + kefu = current_user.kefu_profile + except ObjectDoesNotExist: + logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if kefu.zhuangtai != 1: + logger.warning(f"客服账号已禁用: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 查询提现记录 + try: + record = Tixianjilu.objects.get(id=tixian_id) + except Tixianjilu.DoesNotExist: + return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 查询用户主表 + try: + user = UserMain.objects.get(yonghuid=record.yonghuid) + except UserMain.DoesNotExist: + user = None + + # 构建基础返回数据(字段必须与前端一致) + data = { + 'id': record.id, + 'yonghuid': record.yonghuid, + 'avatar': record.avatar or '', + 'phone': record.phone or '', + 'nicheng': record.nicheng or '', + 'leixing': record.leixing, # 1打手 2管事 + 'zhifu': record.zhifu or '', + 'skzhanghao': record.skzhanghao or '', + 'jine': str(record.jine) if record.jine else '0.00', + 'zhuangtai': record.zhuangtai, + 'fangshi': record.fangshi, + 'shenheid': record.shenheid or '', + 'bhliyou': record.bhliyou or '', + 'create_time': record.create_time, + 'update_time': record.update_time, + } + + # 根据提现类型添加扩展信息 + if user: + if record.leixing == 1: # 打手 + try: + dashou = UserDashou.objects.get(user=user) + # 计算成交率和退款率 + total = dashou.jiedanzongliang or 0 + completed = dashou.chengjiaozongliang or 0 + refund = dashou.tuikuanliang or 0 + completion_rate = round((completed / total * 100) if total > 0 else 0) + refund_rate = round((refund / completed * 100) if completed > 0 else 0) + + data['dashouInfo'] = { + 'zhuangtai': dashou.zhuangtai, # 工作状态 + 'zaixianzhuangtai': dashou.zaixianzhuangtai, # 在线状态 + 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, # 账号状态 + 'yue': float(dashou.yue) if dashou.yue else 0.00, # 可提现余额 + 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, # 接单总额 + 'jifen': dashou.jifen, # 积分 + 'jiedanzongliang': dashou.jiedanzongliang, # 接单总量 + 'chengjiaozongliang': dashou.chengjiaozongliang, # 成交总量 + 'tuikuanliang': dashou.tuikuanliang, # 退款总量 + 'completionRate': completion_rate, + 'refundRate': refund_rate, + } + except ObjectDoesNotExist: + data['dashouInfo'] = None + elif record.leixing == 2: # 管事 + try: + guanshi = UserGuanshi.objects.get(user=user) + data['guanshiInfo'] = { + 'zhuangtai': guanshi.zhuangtai, # 账号状态 + 'yue': float(guanshi.yue) if guanshi.yue else 0.00, + 'yaogingshuliang': guanshi.yaogingshuliang, # 邀请打手总数 + 'jinrichongzhi': guanshi.jinrichongzhi, # 今日充值 + 'jinyuechongzhi': guanshi.jinyuechongzhi, # 今月充值 + 'chongzhifenrun': float(guanshi.chongzhifenrun) if guanshi.chongzhifenrun else 0.00, + } + except ObjectDoesNotExist: + data['guanshiInfo'] = None + + return Response({'code': 0, 'data': data}) + + +class KefuWithdrawActionView(APIView): + """ + 客服处理提现接口(同意/拒绝)—— 仅此接口处理后台审核打款 + POST /yonghu/kefu_txsh_action + + 单条:{ phone, action, tixian_id, yonghuid, leixing, reason? } + 批量:{ phone, action, batch_list: [{tixian_id, yonghuid, leixing}, ...], reason? } + + 自动打款(dakuan_mode=2):同意→6待收款;拒绝→5+驳回原因+退还可到账金额(shijidaozhang),手续费不退,不更新平台收支/每日统计 + 手动打款(dakuan_mode=1):原逻辑不变 + 状态要求:Tixianjilu.zhuangtai=1 且 TixianShenheJilu.zhuangtai=1(审核中),其他状态一律拒绝 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def _verify_kefu(self, request, phone): + if not phone: + return None, Response({'code': 401, 'msg': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST) + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + if current_user.user_type != 'kefu': + return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + try: + kefu = current_user.kefu_profile + except ObjectDoesNotExist: + return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + if kefu.zhuangtai != 1: + return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + return current_user, None + + def _parse_items(self, request): + """解析单条或 batch_list,返回 [{tixian_id, yonghuid, leixing}]""" + batch_list = request.data.get('batch_list') + if batch_list is not None: + if not isinstance(batch_list, list) or len(batch_list) == 0: + return None, Response({'code': 400, 'msg': 'batch_list 不能为空'}, status=status.HTTP_400_BAD_REQUEST) + items = [] + for idx, row in enumerate(batch_list): + if not isinstance(row, dict): + return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 格式错误'}, status=status.HTTP_400_BAD_REQUEST) + tid = row.get('tixian_id') + yid = (row.get('yonghuid') or '').strip() + lx = row.get('leixing') + if not tid or not yid or lx is None: + return None, Response( + {'code': 400, 'msg': f'batch_list[{idx}] 缺少 tixian_id / yonghuid / leixing'}, + status=status.HTTP_400_BAD_REQUEST, + ) + try: + items.append({ + 'tixian_id': int(tid), + 'yonghuid': yid, + 'leixing': int(lx), + }) + except (TypeError, ValueError): + return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 参数格式错误'}, status=status.HTTP_400_BAD_REQUEST) + return items, None + + tixian_id = request.data.get('tixian_id') + if not tixian_id: + return None, Response({'code': 400, 'msg': '提现记录ID不能为空'}, status=status.HTTP_400_BAD_REQUEST) + yonghuid = (request.data.get('yonghuid') or '').strip() + leixing = request.data.get('leixing') + try: + item = {'tixian_id': int(tixian_id), 'yonghuid': yonghuid, 'leixing': int(leixing) if leixing is not None else None} + except (TypeError, ValueError): + return None, Response({'code': 400, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST) + return [item], None + + def _process_auto_item(self, item, action, reason, kefu_user): + """自动打款单条处理(须在 transaction.atomic 内调用)""" + tixian_id = item['tixian_id'] + req_yonghuid = item['yonghuid'] + req_leixing = item['leixing'] + + if req_leixing not in [1, 2, 3, 4, 5, 6]: + raise ValueError(f'提现记录{tixian_id}:提现类型无效') + + try: + tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id) + except Tixianjilu.DoesNotExist: + raise ValueError(f'提现记录{tixian_id}不存在') + + if tixian.yonghuid != req_yonghuid: + raise ValueError(f'提现记录{tixian_id}:用户ID不匹配') + if tixian.leixing != req_leixing: + raise ValueError(f'提现记录{tixian_id}:提现类型不匹配') + if tixian.zhuangtai != 1: + raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理') + if not tixian.shenhe_jilu_id: + raise ValueError(f'提现记录{tixian_id}:非自动打款审核记录') + + try: + audit = TixianShenheJilu.objects.select_for_update().get(id=tixian.shenhe_jilu_id) + except TixianShenheJilu.DoesNotExist: + raise ValueError(f'提现记录{tixian_id}:审核记录不存在') + + if audit.yonghuid != req_yonghuid: + raise ValueError(f'提现记录{tixian_id}:审核记录用户ID不匹配') + if audit.leixing != req_leixing: + raise ValueError(f'提现记录{tixian_id}:审核记录提现类型不匹配') + if audit.zhuangtai != 1: + raise ValueError(f'提现记录{tixian_id}:审核记录非审核中状态') + + try: + user = UserMain.objects.select_for_update().get(yonghuid=req_yonghuid) + except UserMain.DoesNotExist: + raise ValueError(f'用户{req_yonghuid}不存在') + + if action == 1: + sync_audit_and_jilu_status(audit, 6, shenhe_ren_id=kefu_user.yonghuid) + tixian.shenheid = kefu_user.yonghuid + tixian.save(update_fields=['shenheid', 'update_time']) + else: + sync_audit_and_jilu_status( + audit, 5, + bhliyou=reason, + bo_hui_ren_id=kefu_user.yonghuid, + ) + # 退还可到账金额,申请时扣的 shenqing_jine 中的手续费(shouxufei)不退 + refund_balance(user, audit.leixing, audit.shijidaozhang) + tixian.shenheid = kefu_user.yonghuid + tixian.bhliyou = reason + tixian.save(update_fields=['shenheid', 'bhliyou', 'update_time']) + + def _process_manual_item(self, item, action, reason, phone): + """手动打款单条处理(原逻辑,须在 transaction.atomic 内调用)""" + tixian_id = item['tixian_id'] + try: + tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id) + except Tixianjilu.DoesNotExist: + raise ValueError(f'提现记录{tixian_id}不存在') + + req_yonghuid = item.get('yonghuid') + req_leixing = item.get('leixing') + if req_yonghuid and tixian.yonghuid != req_yonghuid: + raise ValueError(f'提现记录{tixian_id}:用户ID不匹配') + if req_leixing is not None and tixian.leixing != req_leixing: + raise ValueError(f'提现记录{tixian_id}:提现类型不匹配') + if tixian.zhuangtai != 1: + raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理') + + if getattr(tixian, 'shenhe_jilu_id', None): + raise ValueError(f'提现记录{tixian_id}:自动打款请走自动流程') + + try: + user = UserMain.objects.get(yonghuid=tixian.yonghuid) + except UserMain.DoesNotExist: + raise ValueError(f'用户{tixian.yonghuid}不存在') + + if action == 1: + tixian.zhuangtai = 2 + try: + update_daily_payout(tixian.jine) + szjilu, _ = Szjilu.objects.get_or_create(id=1) + szjilu.zongsy -= tixian.jine + szjilu.zongzc += tixian.jine + szjilu.jrzc += tixian.jine + szjilu.update_time = timezone.now() + szjilu.save() + except Exception as e: + logger.error(f'更新收支记录失败: {e}') + else: + try: + refund_balance(user, tixian.leixing, tixian.jine) + except ValueError as e: + raise ValueError(str(e)) + tixian.zhuangtai = 3 + tixian.bhliyou = reason + + tixian.shenheid = phone + tixian.update_time = timezone.now() + tixian.save() + + def post(self, request): + logger.info(f'接收到提现处理请求,数据: {request.data}') + + phone = request.data.get('phone', '').strip() + action_raw = request.data.get('action') + reason = request.data.get('reason', '').strip() + + try: + action = int(action_raw) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': 'action 参数必须为数字'}, status=status.HTTP_400_BAD_REQUEST) + if action not in [1, 2]: + return Response({'code': 400, 'msg': 'action 必须为1(同意)或2(拒绝)'}, status=status.HTTP_400_BAD_REQUEST) + if action == 2 and not reason: + return Response({'code': 400, 'msg': '拒绝提现时理由不能为空'}, status=status.HTTP_400_BAD_REQUEST) + + kefu_user, err_resp = self._verify_kefu(request, phone) + if err_resp: + return err_resp + + items, err_resp = self._parse_items(request) + if err_resp: + return err_resp + + is_batch = isinstance(request.data.get('batch_list'), list) and len(request.data.get('batch_list')) > 0 + + try: + with transaction.atomic(): + for item in items: + tixian_id = item['tixian_id'] + try: + tixian_probe = Tixianjilu.objects.select_for_update().get(id=tixian_id) + except Tixianjilu.DoesNotExist: + raise ValueError(f'提现记录{tixian_id}不存在') + + # 有关联审核记录(shenhe_jilu_id)即为 zddksh 自动打款,不依赖 dakuan_mode 字段 + is_auto = bool(getattr(tixian_probe, 'shenhe_jilu_id', None)) + + if is_auto: + self._process_auto_item(item, action, reason, kefu_user) + else: + if is_batch: + raise ValueError(f'提现记录{tixian_id}:批量操作仅支持自动打款记录') + self._process_manual_item(item, action, reason, phone) + + except ValueError as e: + return Response({'code': 400, 'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + logger.error(f'处理提现事务失败: {e}', exc_info=True) + return Response({'code': 500, 'msg': f'处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + msg = '批量处理成功' if is_batch and len(items) > 1 else '处理成功' + return Response({'code': 0, 'msg': msg, 'data': {'count': len(items)}}) + + + + + + + + +# dingdan/views.py +class KefuPunishView(APIView): + """ + 客服处罚打手接口 + 请求:POST /yonghu/kefuchufa + 参数:{ + "phone": "客服手机号", + "dingdan_id": "订单ID", + "reason": "处罚原因(可选)" + } + 认证:JWT + 返回:code=0 成功 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + phone = request.data.get('phone', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + reason = request.data.get('reason', '').strip() + + if not phone or not dingdan_id: + return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) + + # 2. 客服身份验证 + current_user = request.user + if getattr(current_user, 'phone', '') != phone: + logger.warning(f"手机号不匹配: {phone}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if current_user.user_type != 'kefu': + logger.warning(f"用户类型非客服: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + kefu = current_user.kefu_profile + except ObjectDoesNotExist: + logger.warning(f"客服扩展表不存在: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if kefu.zhuangtai != 1: + logger.warning(f"客服账号已禁用: {current_user.yonghuid}") + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 3. 查询订单 + try: + order = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 4. 获取打手ID + dashou_id = order.jiedan_dashou_id + if not dashou_id: + return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}, status=status.HTTP_400_BAD_REQUEST) + + # 5. 查询打手 + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou = dashou_user.dashou_profile + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND) + except ObjectDoesNotExist: + return Response({'code': 400, 'msg': '该用户不是打手'}, status=status.HTTP_400_BAD_REQUEST) + + # 6. 使用事务 + with transaction.atomic(): + # 创建处罚记录(积分扣除5分) + jifen = 5 + chufa = Chufajilu.objects.create( + dashouid=dashou_id, + qingqiuid=phone, # 客服手机号作为请求人 + cfliyou=reason or '', + sqzhuangtai=0, # 待处理 + jifen=jifen, + dingdan_id=dingdan_id, + ) + + # 扣除打手积分 + if dashou.jifen >= jifen: + dashou.jifen -= jifen + else: + dashou.jifen = 0 + dashou.save() + + # 可选:更新订单的 clkf 字段记录处理客服 + order.clkf = phone + order.save() + + return Response({'code': 0, 'msg': '处罚成功'}) + + + +class AdKftjView(APIView): + """ + 添加客服接口 + 权限:管理员JWT + 请求:POST /yonghu/adkftj + 参数:{ + "zhanghao": "管理员账号", + "phone": "客服手机号(必填)", + "password": "密码(必填)", + "erjimima": "二级密码(必填)", + "nicheng": "昵称(可选)" + } + 返回:code=0 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 参数 + zhanghao = request.data.get('zhanghao', '').strip() + phone = request.data.get('phone', '').strip() + password = request.data.get('password', '').strip() + erjimima = request.data.get('erjimima', '').strip() + nicheng = request.data.get('nicheng', '').strip() + + if not all([zhanghao, phone, password, erjimima]): + return Response({'code': 400, 'message': '手机号、密码、二级密码不能为空'}) + + # 2. 管理员验证(同前) + current_user = request.user + if current_user.phone != zhanghao or current_user.user_type != 'admin': + return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) + try: + admin_profile = current_user.admin_profile + except: + return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 3. 检查手机号是否已被使用 + if UserMain.objects.filter(phone=phone).exists(): + return Response({'code': 400, 'message': '该手机号已被注册'}) + + # 4. 生成唯一 yonghuid + def generate_yonghuid(): + for _ in range(10): + timestamp = str(int(time.time()))[-5:] + rand = str(random.randint(0, 99)).zfill(2) + uid = timestamp + rand + if len(uid) == 7 and not UserMain.objects.filter(yonghuid=uid).exists(): + return uid + raise Exception('无法生成唯一用户ID') + + with transaction.atomic(): + # 创建主表用户 + user = UserMain.objects.create( + yonghuid=generate_yonghuid(), + phone=phone, + password=password, + user_type='kefu', + openid=f'kefu_{phone}' # 临时openid + ) + # 创建客服扩展表 + kefu = UserKefu.objects.create( + user=user, + nicheng=nicheng or f'客服{user.yonghuid}', + erjimima=erjimima, # 建议实际项目中加密存储 + zhuangtai=1, + jinrichuli=0, + jinyuechuli=0, + zongchuli=0 + ) + + return Response({'code': 0, 'message': '添加成功'}) + + + +class AdKfglView(APIView): + """ + 客服管理列表接口 + 权限:管理员JWT + 请求:POST /yonghu/adkfgl + 参数:{ + "zhanghao": "管理员账号", + "keyword": "搜索关键词(可选,匹配客服ID或昵称)" + } + 返回:{ + "code": 0, + "data": { + "list": [客服对象...] + } + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 参数获取 + zhanghao = request.data.get('zhanghao', '').strip() + keyword = request.data.get('keyword', '').strip() + + if not zhanghao: + return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) + + # 2. 管理员身份验证 + current_user = request.user + if not hasattr(current_user, 'phone') or str(current_user.phone) != zhanghao: + return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if current_user.user_type != 'admin': + return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + admin_profile = current_user.admin_profile + except AttributeError: + return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 3. 查询所有客服(通过扩展表关联主表) + queryset = UserKefu.objects.select_related('user').all() + + if keyword: + queryset = queryset.filter( + #Q(user__yonghuid__icontains=keyword) | + Q(nicheng__icontains=keyword) | + Q(user__phone__icontains=keyword) + ) + + # 4. 构建返回数据 + kefu_list = [] + for kefu in queryset: + user = kefu.user + kefu_list.append({ + 'yonghuid': user.phone, + 'phone': user.phone or '', + 'nicheng': kefu.nicheng or '', + 'zhuangtai': kefu.zhuangtai, + 'jinrichuli': kefu.jinrichuli, + 'jinyuechuli': kefu.jinyuechuli, + 'zongchuli': kefu.zongchuli, + }) + + return Response({ + 'code': 0, + 'data': {'list': kefu_list} + }) + + + + + +class AdKfxgView(APIView): + """ + 修改客服信息接口 + 权限:管理员JWT + 请求:POST /yonghu/adkfxg + 参数:{ + "zhanghao": "管理员账号", + "uid": "客服ID (yonghuid)", + "nicheng": "新昵称(可选)", + "phone": "新手机号(可选)", + "password": "新密码(可选)", + "erjimima": "新二级密码(可选)", + "zhuangtai": 0/1(可选) + } + 返回:code=0 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + zhanghao = request.data.get('zhanghao', '').strip() + uid = request.data.get('uid', '').strip() + + if not zhanghao or not uid: + return Response({'code': 400, 'message': '参数不完整'}) + + # 管理员验证 + current_user = request.user + if current_user.phone != zhanghao or current_user.user_type != 'admin': + return Response({'code': 401, 'message': '身份验证失败'}) + try: + admin_profile = current_user.admin_profile + except: + return Response({'code': 401, 'message': '身份验证失败'}) + + # 查询客服 + try: + user = UserMain.objects.get(phone=uid) + kefu = user.kefu_profile + except (UserMain.DoesNotExist, UserKefu.DoesNotExist): + return Response({'code': 404, 'message': '客服不存在'}) + + # 更新数据 + with transaction.atomic(): + # 更新主表字段 + if 'phone' in request.data: + new_phone = request.data.get('phone').strip() + if new_phone and UserMain.objects.filter(phone=new_phone).exclude(phone=uid).exists(): + return Response({'code': 400, 'message': '手机号已被使用'}) + user.phone = new_phone + if 'password' in request.data: + user.password = request.data['password'] + user.save() + + # 更新扩展表 + if 'nicheng' in request.data: + kefu.nicheng = request.data['nicheng'] + if 'erjimima' in request.data: + kefu.erjimima = request.data['erjimima'] + if 'zhuangtai' in request.data: + kefu.zhuangtai = int(request.data['zhuangtai']) + kefu.save() + + return Response({'code': 0, 'message': '修改成功'}) + + + + + + + + +class WechatLoginAndGuanshiRegisterView(APIView): + """ + 未登录用户微信登录+管事注册一体化接口 + 路径: /api/yonghu/zuzhangyqmzc + 方法: POST + 权限: 允许所有 + 请求体: { "code": "微信登录code", "inviteCode": "组长邀请码" } + 成功返回 code: 200,数据同 GuanshiRegisterView + """ + permission_classes = [AllowAny] + + def post(self, request): + code = request.data.get('code', '').strip() + yaoqingma = request.data.get('inviteCode', '').strip() + + if not code: + return Response({'code': 400, 'msg': '微信授权码不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + if not yaoqingma: + return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + if len(yaoqingma) > 100: + return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + wechat_data = self._get_wechat_openid(code) + if not wechat_data or 'openid' not in wechat_data: + error_msg = wechat_data.get('errmsg', '微信授权失败') + return Response({'code': 400, 'msg': f'微信登录失败: {error_msg}', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + openid = wechat_data['openid'] + kehuduan_ip = self._huoqu_kehuduan_ip(request) + + with transaction.atomic(): + user_main, created = UserMain.objects.select_for_update().get_or_create( + openid=openid, + defaults={ + 'yonghuid': self._shengcheng_yonghu_id(), + 'user_type': 'normal', + } + ) + user_main.ip = kehuduan_ip + user_main.last_login_time = timezone.now() + user_main.save() + + if created: + UserBoss.objects.create(user=user_main, nickname='微信用户') + + if hasattr(user_main, 'guanshi_profile'): + return self._fanhui_xianyou_guanshi_info(user_main) + + try: + zuzhang = UserZuzhang.objects.select_related('user').get(yaoqingma=yaoqingma) + except UserZuzhang.DoesNotExist: + return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND) + + if zuzhang.zhuangtai != 1: + return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + zuzhang_yonghuid = zuzhang.user.yonghuid + + guanshi_yaoqingma = CreateInvitationCode(str(user_main.yonghuid)) + guanshi_profile = UserGuanshi.objects.create( + user=user_main, + yaoqingma=guanshi_yaoqingma, + yaoqingren=zuzhang_yonghuid, + ) + + UserZuzhang.objects.filter(pk=zuzhang.pk).update( + yaoqing_zongshu=models.F('yaoqing_zongshu') + 1 + ) + + # ========== 新增:组长每日统计(邀请管事 action=1) ========== + try: + update_zuzhang_daily_by_action( + yonghuid=zuzhang_yonghuid, + action=1 # 1 = 邀请管事 + ) + logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}") + except Exception as e: + logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}") + + + if not hasattr(user_main, 'dashou_profile'): + dashou_profile = UserDashou.objects.create( + user=user_main, + nicheng='大神', + chenghao='普通大神', + yaoqingren=zuzhang_yonghuid, + ) + else: + dashou_profile = user_main.dashou_profile + + refresh = RefreshToken.for_user(user_main) + token = str(refresh.access_token) + + response_data = self._zhuangbei_fanhui_shuju( + user_main, dashou_profile, guanshi_profile, token + ) + + return Response({'code': 0, 'msg': '登录并注册成功!', 'data': response_data}) + + # ---------- 辅助方法(与接口1完全相同,完整列出以保证完整性) ---------- + + def _get_wechat_openid(self, code): + try: + appid = getattr(settings, 'WEIXIN_APPID', '') + secret = getattr(settings, 'WEIXIN_SECRET', '') + if not appid or not secret: + raise ValueError('微信配置未设置') + url = 'https://api.weixin.qq.com/sns/jscode2session' + params = { + 'appid': appid, + 'secret': secret, + 'js_code': code, + 'grant_type': 'authorization_code' + } + response = requests.get(url, params=params, timeout=10) + result = response.json() + if 'openid' in result: + return result + else: + errcode = result.get('errcode', 'unknown') + errmsg = result.get('errmsg', '未知错误') + return {'errmsg': f'[{errcode}]{errmsg}'} + except requests.exceptions.Timeout: + logger.error('请求微信接口超时') + return {'errmsg': '请求微信接口超时'} + except Exception as e: + logger.error(f'请求微信接口异常: {e}') + return {'errmsg': f'请求微信接口异常: {str(e)}'} + + def _shengcheng_yonghu_id(self): + for _ in range(10): + timestamp_part = str(int(time.time()))[-5:].zfill(5) + random_part = str(random.randint(0, 99)).zfill(2) + user_id = timestamp_part + random_part + if len(user_id) == 7 and user_id.isdigit(): + if not UserMain.objects.filter(yonghuid=user_id).exists(): + return user_id + raise Exception('生成用户ID失败') + + def _huoqu_kehuduan_ip(self, request): + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0].strip() + if ip: + return ip + ip = request.META.get('REMOTE_ADDR', '') + return ip if ip else '0.0.0.0' + + def _huoqu_qun_peizhi(self): + try: + qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) + result = { + 'dashouqun': '', + 'dashouqunid': '', + 'guanshiqun': '', + 'guanshiqunid': '' + } + for config in qun_configs: + if config.id == 1: + result['dashouqun'] = config.neirong or '' + result['dashouqunid'] = config.qunid or '' + elif config.id == 2: + result['guanshiqun'] = config.neirong or '' + result['guanshiqunid'] = config.qunid or '' + return result + except Exception as e: + logger.error(f"获取群配置失败: {e}") + return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''} + + def _zhuangbei_dashou_fanhui(self, dashou_profile): + data = { + 'dashounicheng': dashou_profile.nicheng, + 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, + 'yongjin': str(dashou_profile.yue), + 'zonge': str(dashou_profile.zonge), + 'yajin': str(dashou_profile.yajin), + 'chenghao': dashou_profile.chenghao, + 'jinfen': dashou_profile.jifen, + 'chengjiaoliang': dashou_profile.chengjiaozongliang, + 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, + 'dashouzhuangtai': dashou_profile.zhuangtai, + } + huiyuan_list = [] + try: + records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid) + for record in records: + shifou_daoqi = record.jiance_shifou_daoqi() + huiyuan_list.append({ + 'huiyuan_id': record.huiyuan_id, + 'huiyuan_zhuangtai': record.huiyuan_zhuangtai, + 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None, + 'shifou_daoqi': shifou_daoqi, + 'jieshao': record.jieshao + }) + except Exception as e: + logger.warning(f"获取会员列表失败: {e}") + data['clumber'] = huiyuan_list + return data + + def _fanhui_xianyou_guanshi_info(self, user_main): + guanshi_profile = user_main.guanshi_profile + dashou_profile = getattr(user_main, 'dashou_profile', None) + group_info = self._huoqu_qun_peizhi() + dashou_status = 1 if dashou_profile else 0 + shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0 + guanshi_status = 1 + try: + nicheng = user_main.boss_profile.nickname or '微信用户' + except: + nicheng = '微信用户' + refresh = RefreshToken.for_user(user_main) + token = str(refresh.access_token) + data = { + 'token': token, + 'nicheng': nicheng, + 'uid': user_main.yonghuid, + 'touxiang': user_main.avatar or '', + 'shangjiastatus': shangjia_status, + 'dashoustatus': dashou_status, + 'guanshistatus': guanshi_status, + **group_info, + } + guanshi_data = { + 'gszhstatus': guanshi_profile.zhuangtai, + 'yaoqingzongshu': guanshi_profile.yaogingshuliang, + 'fenyongzonge': str(guanshi_profile.chongzhifenrun), + 'fenyongtixian': str(guanshi_profile.yue), + 'yichongzhiDashou': guanshi_profile.jinrichongzhi, + } + data.update(guanshi_data) + if dashou_profile: + dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile) + data.update(dashou_data) + else: + data.update({ + 'dashounicheng': '', + 'zhanghaostatus': 0, + 'yongjin': '0.00', + 'zonge': '0.00', + 'yajin': '0.00', + 'chenghao': '', + 'jinfen': 0, + 'chengjiaoliang': 0, + 'zaixianzhuangtai': 0, + 'dashouzhuangtai': 0, + 'clumber': [] + }) + return Response({'code': 0, 'msg': '您已是管事', 'data': data}) + + def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token): + try: + nicheng = user_main.boss_profile.nickname or '微信用户' + except: + nicheng = '微信用户' + dashou_status = 1 + shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0 + guanshi_status = 1 + group_info = self._huoqu_qun_peizhi() + data = { + 'token': token, + 'nicheng': nicheng, + 'uid': user_main.yonghuid, + 'touxiang': user_main.avatar or '', + 'shangjiastatus': shangjia_status, + 'dashoustatus': dashou_status, + 'guanshistatus': guanshi_status, + **group_info, + } + guanshi_data = { + 'gszhstatus': guanshi_profile.zhuangtai, + 'yaoqingzongshu': guanshi_profile.yaogingshuliang, + 'fenyongzonge': str(guanshi_profile.chongzhifenrun), + 'fenyongtixian': str(guanshi_profile.yue), + 'yichongzhiDashou': guanshi_profile.jinrichongzhi, + } + data.update(guanshi_data) + dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile) + data.update(dashou_data) + return data + + + + +class GuanshiRegisterView(APIView): + """ + 已登录用户注册管事接口 + 路径: /api/yonghu/guanshizhuce + 方法: POST + 权限: JWT认证 + 请求体: { "inviteCode": "组长邀请码" } + 成功返回 code: 200 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取并验证邀请码 + yaoqingma = request.data.get('inviteCode', '').strip() + if not yaoqingma: + return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + if len(yaoqingma) > 100: + return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + current_user = request.user + + # 2. 检查用户是否已是管事 + if hasattr(current_user, 'guanshi_profile'): + # 用户已是管事,直接返回现有信息 + return self._fanhui_xianyou_guanshi_info(current_user) + + # 3. 验证邀请码对应的组长 + try: + zuzhang = UserZuzhang.objects.select_related('user').get(yaoqingma=yaoqingma) + except UserZuzhang.DoesNotExist: + return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND) + + if zuzhang.zhuangtai != 1: + return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + zuzhang_yonghuid = zuzhang.user.yonghuid + + # 4. 事务内创建管事实体和打手(如果需要) + with transaction.atomic(): + # 创建管事扩展表 + guanshi_yaoqingma = CreateInvitationCode(str(current_user.yonghuid)) + guanshi_profile = UserGuanshi.objects.create( + user=current_user, + yaoqingma=guanshi_yaoqingma, + yaoqingren=zuzhang_yonghuid, + ) + + # 更新组长的邀请总数 + UserZuzhang.objects.filter(pk=zuzhang.pk).update( + yaoqing_zongshu=models.F('yaoqing_zongshu') + 1 + ) + + # ========== 新增:组长每日统计(邀请管事 action=1) ========== + try: + update_zuzhang_daily_by_action( + yonghuid=zuzhang_yonghuid, + action=1 # 1 = 邀请管事 + ) + logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}") + except Exception as e: + logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}") + + # 检查用户是否已是打手,如果不是则创建 + if not hasattr(current_user, 'dashou_profile'): + dashou_profile = UserDashou.objects.create( + user=current_user, + nicheng='大手子', + chenghao='普通大手', + yaoqingren=zuzhang_yonghuid, + ) + else: + dashou_profile = current_user.dashou_profile + + # 5. 生成新token + refresh = RefreshToken.for_user(current_user) + token = str(refresh.access_token) + + # 6. 准备返回数据(包含所有字段) + response_data = self._zhuangbei_fanhui_shuju( + current_user, dashou_profile, guanshi_profile, token + ) + + return Response({'code': 200, 'msg': '注册成功!', 'data': response_data}) + + # ---------- 辅助方法 ---------- + + def _huoqu_qun_peizhi(self): + """获取群配置信息""" + try: + qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) + result = { + 'dashouqun': '', + 'dashouqunid': '', + 'guanshiqun': '', + 'guanshiqunid': '' + } + for config in qun_configs: + if config.id == 1: + result['dashouqun'] = config.neirong or '' + result['dashouqunid'] = config.qunid or '' + elif config.id == 2: + result['guanshiqun'] = config.neirong or '' + result['guanshiqunid'] = config.qunid or '' + return result + except Exception as e: + logger.error(f"获取群配置失败: {e}") + return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''} + + def _zhuangbei_dashou_fanhui(self, dashou_profile): + """组装打手信息(字段名与前端完全匹配)""" + data = { + 'dashounicheng': dashou_profile.nicheng, + 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, + 'yongjin': str(dashou_profile.yue), + 'zonge': str(dashou_profile.zonge), + 'yajin': str(dashou_profile.yajin), + 'chenghao': dashou_profile.chenghao, + 'jinfen': dashou_profile.jifen, + 'chengjiaoliang': dashou_profile.chengjiaozongliang, + 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, + 'dashouzhuangtai': dashou_profile.zhuangtai, + } + + # 会员列表 + huiyuan_list = [] + try: + records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid) + for record in records: + shifou_daoqi = record.jiance_shifou_daoqi() + huiyuan_list.append({ + 'huiyuan_id': record.huiyuan_id, + 'huiyuan_zhuangtai': record.huiyuan_zhuangtai, + 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None, + 'shifou_daoqi': shifou_daoqi, + 'jieshao': record.jieshao + }) + except Exception as e: + logger.warning(f"获取会员列表失败: {e}") + data['clumber'] = huiyuan_list + return data + + def _fanhui_xianyou_guanshi_info(self, user_main): + """用户已是管事时的返回""" + guanshi_profile = user_main.guanshi_profile + dashou_profile = getattr(user_main, 'dashou_profile', None) + + group_info = self._huoqu_qun_peizhi() + + dashou_status = 1 if dashou_profile else 0 + shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0 + guanshi_status = 1 + + try: + nicheng = user_main.boss_profile.nickname or '微信用户' + except: + nicheng = '微信用户' + + refresh = RefreshToken.for_user(user_main) + token = str(refresh.access_token) + + data = { + 'token': token, + 'nicheng': nicheng, + 'uid': user_main.yonghuid, + 'touxiang': user_main.avatar or '', + 'shangjiastatus': shangjia_status, + 'dashoustatus': dashou_status, + 'guanshistatus': guanshi_status, + **group_info, + } + + # 管事信息 + guanshi_data = { + 'gszhstatus': guanshi_profile.zhuangtai, + 'yaoqingzongshu': guanshi_profile.yaogingshuliang, + 'fenyongzonge': str(guanshi_profile.chongzhifenrun), + 'fenyongtixian': str(guanshi_profile.yue), + 'yichongzhiDashou': guanshi_profile.jinrichongzhi, + } + data.update(guanshi_data) + + if dashou_profile: + dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile) + data.update(dashou_data) + else: + data.update({ + 'dashounicheng': '', + 'zhanghaostatus': 0, + 'yongjin': '0.00', + 'zonge': '0.00', + 'yajin': '0.00', + 'chenghao': '', + 'jinfen': 0, + 'chengjiaoliang': 0, + 'zaixianzhuangtai': 0, + 'dashouzhuangtai': 0, + 'clumber': [] + }) + + return Response({'code': 200, 'msg': '您已是管事', 'data': data}) + + def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token): + """组装完整返回数据(新注册)""" + try: + nicheng = user_main.boss_profile.nickname or '微信用户' + except: + nicheng = '微信用户' + + dashou_status = 1 + shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0 + guanshi_status = 1 + + group_info = self._huoqu_qun_peizhi() + + data = { + 'token': token, + 'nicheng': nicheng, + 'uid': user_main.yonghuid, + 'touxiang': user_main.avatar or '', + 'shangjiastatus': shangjia_status, + 'dashoustatus': dashou_status, + 'guanshistatus': guanshi_status, + **group_info, + } + + # 管事信息 + guanshi_data = { + 'gszhstatus': guanshi_profile.zhuangtai, + 'yaoqingzongshu': guanshi_profile.yaogingshuliang, + 'fenyongzonge': str(guanshi_profile.chongzhifenrun), + 'fenyongtixian': str(guanshi_profile.yue), + 'yichongzhiDashou': guanshi_profile.jinrichongzhi, + } + data.update(guanshi_data) + + dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile) + data.update(dashou_data) + + return data + + + + + + + +class ZuzhangZhuceView(APIView): + """ + 组长注册/激活接口 + POST /yonghu/zuzhangzhuce + 请求体: {"inviteCode": "xxx"} + 返回: + { + "code": 200, + "msg": "success", + "data": { + "ketixian": "0.00", # 可提现金额 + "guanshiCount": 0, # 邀请的管事数量 + "fenhongZonge": "0.00" # 分红总额 + } + } + 或错误码 + """ + + permission_classes = [IsAuthenticated] + + # 硬编码激活码(注意转义特殊字符) + ACTIVATION_CODE = "237huwehdw77e777eyw224fr" + + def post(self, request): + user = request.user + invite_code = request.data.get('inviteCode', '').strip() + + if not invite_code: + return Response({'code': 400, 'msg': '邀请码不能为空'}, status=400) + + # 检查是否已是组长 + try: + zuzhang = UserZuzhang.objects.select_related('user').get(user=user) + # 已注册,直接返回数据 + data = self._build_response_data(zuzhang, user) + return Response({'code':200, 'msg': '已注册', 'data': data}) + except UserZuzhang.DoesNotExist: + pass + + # 比对激活码 + if invite_code != self.ACTIVATION_CODE: + return Response({'code': 400, 'msg': '邀请码无效'}, status=400) + + # 生成唯一组长邀请码:时间戳 + 用户ID后6位 + 随机字符 + yaoqingma = self._generate_unique_yaoqingma(user.yonghuid) + + # 创建组长扩展记录 + try: + with transaction.atomic(): + zuzhang = UserZuzhang.objects.create( + user=user, + yaoqingma=yaoqingma, + fenyong_zonge=0.00, + ketixian_jine=0.00, + yaoqing_zongshu=0, + jinri_fenyong=0.00, + jinyue_fenyong=0.00, + zhuangtai=1, + jinri_tixian=0.00, + ewai_tixian_xiane=0.00, + kaioi_ewai_tixian=False, + kaioi_ewai_fenhong=False, + ewai_fenhong_jine=0.00 + ) + except IntegrityError: + # 极低概率重复,重试一次 + yaoqingma = self._generate_unique_yaoqingma(user.yonghuid, retry=True) + zuzhang = UserZuzhang.objects.create( + user=user, + yaoqingma=yaoqingma, + # 其他字段默认同上 + fenyong_zonge=0.00, + ketixian_jine=0.00, + yaoqing_zongshu=0, + jinri_fenyong=0.00, + jinyue_fenyong=0.00, + zhuangtai=1, + jinri_tixian=0.00, + ewai_tixian_xiane=0.00, + kaioi_ewai_tixian=False, + kaioi_ewai_fenhong=False, + ewai_fenhong_jine=0.00 + ) + + data = self._build_response_data(zuzhang, user) + return Response({'code':200, 'msg': '注册成功', 'data': data}) + + def _generate_unique_yaoqingma(self, yonghuid, retry=False): + """生成唯一邀请码:时间戳+用户ID后6位+随机2位字符""" + timestamp = str(int(time.time()))[-6:] # 取后6位 + uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0') + rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2)) + base = f"Z{timestamp}{uid_part}{rand_str}" + # 确保唯一性(若已存在则追加随机数,最多尝试3次) + for _ in range(3): + if not UserZuzhang.objects.filter(yaoqingma=base).exists(): + return base + base = base + random.choice(string.digits) + # 极端情况,再附加时间戳毫秒 + millis = str(time.time()).replace('.', '')[-4:] + return base + millis + + def _build_response_data(self, zuzhang, user): + """组装前端所需数据""" + # 计算邀请的管事数量(通过UserGuanshi表的yaoqingren字段关联到user的yonghuid) + guanshi_count = UserGuanshi.objects.filter(yaoqingren=user.yonghuid).count() + return { + 'ketixian': str(zuzhang.ketixian_jine), # 可提现余额 + 'guanshiCount': guanshi_count, # 邀请管事数 + 'fenhongZonge': str(zuzhang.fenyong_zonge) # 分红总额 + } + + + +class ZuzhangXinxiView(APIView): + """ + 组长信息查询接口 + POST /yonghu/zuzhangxinxi + 请求体: 无 + 返回: 同注册接口成功返回格式 + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + user = request.user + try: + zuzhang = UserZuzhang.objects.get(user=user) + except UserZuzhang.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是组长'}, status=404) + + guanshi_count = UserGuanshi.objects.filter(yaoqingren=user.yonghuid).count() + data = { + 'ketixian': str(zuzhang.ketixian_jine), + 'guanshiCount': guanshi_count, + 'fenhongZonge': str(zuzhang.fenyong_zonge) + } + return Response({'code': 200, 'msg': 'success', 'data': data}) + + + + + +# apps/yonghu/views_dashou_jianquan.py +""" +打手 IM 消息权限鉴权接口 +URL: /yonghu/dshqltqx +方法: POST +认证: JWT Token (request.user) + +鉴权逻辑(满足任意一条即通过): + 1. 押金 >= 5 元 → 通过 + 2. 拥有未过期会员 → 通过 + 3. 存在订单状态为 2 或 8 的订单(作为接单打手) → 通过 + 以上均不满足 → 拒绝 +""" + + +class DashouJianquanView(APIView): + """打手消息权限鉴权""" + permission_classes = [IsAuthenticated] + + def post(self, request): + # ========== 1. 通过 JWT 获取当前登录用户 ========== + user = request.user # UserMain 实例 + + # ========== 2. 检查是否为已注册打手 ========== + try: + dashou = user.dashou_profile # OneToOneField 反向查询 + except UserDashou.DoesNotExist: + return Response({ + 'code': 0, + 'allow': 0, + 'msg': '您尚未注册打手身份,请先注册' + }) + + # ========== 3. 条件一:押金 >= 5 元 ========== + if dashou.yajin and dashou.yajin >= 5: + return Response({ + 'code': 0, + 'allow': 1, + 'msg': '鉴权通过(押金满足条件)' + }) + + # ========== 4. 条件二:存在未过期会员 ========== + # 高效查询:利用 yonghu_id + huiyuan_zhuangtai 复合索引 + huiyuan_list = Huiyuangoumai.objects.filter( + yonghu_id=user.yonghuid, + huiyuan_zhuangtai=1 # 只查状态为"可用"的,减少内存遍历 + ).only('id', 'daoqi_time', 'huiyuan_zhuangtai') + + for hy in huiyuan_list: + if not hy.jiance_shifou_daoqi(): # 未过期返回 False + return Response({ + 'code': 0, + 'allow': 1, + 'msg': '鉴权通过(会员未过期)' + }) + + # ========== 5. 条件三:存在订单状态为 2 或 8 的订单 ========== + # 利用 jiedan_dashou_id + zhuangtai 复合索引,只查 1 条 + has_valid_order = Dingdan.objects.filter( + jiedan_dashou_id=user.yonghuid, + zhuangtai__in=[2, 8] + ).only('id').exists() # exists() 高性能判断,只发 SELECT 1 LIMIT 1 + + if has_valid_order: + return Response({ + 'code': 0, + 'allow': 1, + 'msg': '鉴权通过(存在有效订单)' + }) + + # ========== 6. 以上均不满足 → 拒绝 ========== + return Response({ + 'code': 0, + 'allow': 0, + 'msg': '消息权限不足,请充值会员或联系管理员' + }) + + + +# apps/yonghu/views_laoban_jianquan.py +""" +老板(普通用户) IM 消息权限鉴权接口 +URL: /yonghu/lbhqltqx +方法: POST +认证: JWT Token (request.user) + +鉴权逻辑: + 查询平台订单扩展表(DingdanPingtai), + 通过关联的订单主表(Dingdan)查找该老板的订单, + 只要存在任意一条订单状态在 1~8 范围内 → 通过 +""" + + + +class LaobanJianquanView(APIView): + """老板消息权限鉴权""" + permission_classes = [IsAuthenticated] + + def post(self, request): + # ========== 1. 通过 JWT 获取当前登录用户 ========== + user = request.user # UserMain 实例 + + # ========== 2. 高效查询平台订单扩展表 ========== + # 利用 DingdanPingtai.laoban_id 索引 + Dingdan.zhuangtai 索引 + # 只查 1 条,用 exists() 实现最高性能 + has_valid_order = DingdanPingtai.objects.filter( + laoban_id=user.yonghuid + ).filter( + dingdan__zhuangtai__in=[1, 2, 3, 4, 5, 6, 7, 8] + ).only('id').exists() + + if has_valid_order: + return Response({ + 'code': 0, + 'allow': 1, + 'msg': '鉴权通过' + }) + + return Response({ + 'code': 0, + 'allow': 0, + 'msg': '您暂无订单记录,消息功能暂不可用' + }) + + + + + + + +# views.py (在处罚相关app的views中) +class FaKuanJiFenTongJiView(APIView): + """ + 获取罚款与积分处罚统计总览 + 路径: POST /yonghu/cffktjhq + """ + permission_classes = [IsAuthenticated, DashouPermission] + + def post(self, request): + try: + user_main = request.user + yonghuid = user_main.yonghuid + + # ================= 罚款统计 ================= + fadan_stats = Fadan.objects.filter(beichufa_id=yonghuid).aggregate( + total=Count('id'), + daijiaona=Count('id', filter=Q(zhuangtai=1)), + shensuzhong=Count('id', filter=Q(zhuangtai=3)), + yijiaona=Count('id', filter=Q(zhuangtai=2)), + yibohui=Count('id', filter=Q(zhuangtai=4)), + ) + + # ================= 积分处罚统计 ================= + jifen_stats = Chufajilu.objects.filter(dashouid=yonghuid).aggregate( + total=Count('id'), + daichuli=Count('id', filter=Q(sqzhuangtai=0) | Q(sqzhuangtai=3)), + yichuli=Count('id', filter=Q(sqzhuangtai=1) | Q(sqzhuangtai=2)), + # 细分 + daichuli_0=Count('id', filter=Q(sqzhuangtai=0)), + shensuzhong_3=Count('id', filter=Q(sqzhuangtai=3)), + yichufa_1=Count('id', filter=Q(sqzhuangtai=1)), + yibohui_2=Count('id', filter=Q(sqzhuangtai=2)), + ) + + data = { + "fakuan": { + "total": fadan_stats['total'], + "daijiaona": fadan_stats['daijiaona'], + "shensuzhong": fadan_stats['shensuzhong'], + "yijiaona": fadan_stats['yijiaona'], + "yibohui": fadan_stats['yibohui'], + }, + "jifen": { + "total": jifen_stats['total'], + "daichuli": jifen_stats['daichuli'], + "yichuli": jifen_stats['yichuli'], + "daichuli_0": jifen_stats['daichuli_0'], + "shensuzhong_3": jifen_stats['shensuzhong_3'], + "yichufa_1": jifen_stats['yichufa_1'], + "yibohui_2": jifen_stats['yibohui_2'], + } + } + + return Response({'code': 0, 'msg': '获取统计成功', 'data': data}) + + except Exception as e: + logger.error(f"获取统计异常 {yonghuid}: {e}", exc_info=True) + return Response({'code': 99, 'msg': '系统繁忙'}, status=500) + +class DaShouFaKuanLieBiaoView(APIView): + """ + 打手罚款列表接口 + 路径:POST /yonghu/dsfklbhq + 参数: + page: 页码(默认1) + page_size: 每页条数(默认10,最大50) + zhuangtai: 状态筛选(1待缴纳/2已缴纳/3申诉中/4申诉成功,0或不传表示全部) + sousuo_dingdan_id: 订单号模糊搜索 + 返回:{ + list: [{ + id, beichufa_id, guanliandingdan_id, chufaliyou, fakuanjine, zhuangtai, + yingxiang_qiangdan, shensuliyou, bohuiliyou, + zhengju_tupian: [...], // 证据图片(yongtu=1) + shensu_tupian: [...], // 申诉图片(yongtu=2) + create_time, update_time + }], + has_more, page, page_size + } + """ + permission_classes = [IsAuthenticated, DashouPermission] + + def post(self, request): + try: + user_main = request.user + yonghuid = user_main.yonghuid + + # 获取分页参数 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 10)) + if page_size > 50: + page_size = 50 + if page_size < 1: + page_size = 10 + + # 状态筛选 + zhuangtai = request.data.get('zhuangtai') + # 订单号搜索 + sousuo_dingdan_id = request.data.get('sousuo_dingdan_id', '').strip() + + # 基础查询:当前用户的罚单 + qs = Fadan.objects.filter(beichufa_id=yonghuid) + + # 状态筛选 + if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]: + qs = qs.filter(zhuangtai=int(zhuangtai)) + + # 订单号模糊搜索 + if sousuo_dingdan_id: + qs = qs.filter(guanliandingdan_id__icontains=sousuo_dingdan_id) + + # 按创建时间倒序 + qs = qs.order_by('-create_time') + + # 分页 + paginator = Paginator(qs, page_size) + try: + page_obj = paginator.page(page) + except Exception: + return Response({ + 'code': 0, 'msg': '获取成功', + 'data': {'list': [], 'has_more': False, 'page': page, 'page_size': page_size} + }) + + records = list(page_obj) + fadan_ids = [r.id for r in records] + + # 批量获取图片,按用途分组 + zhengju_map = {} # key: fadan_id, value: [url, ...] + shensu_map = {} + if fadan_ids: + tupians = FadanShensuTupian.objects.filter( + fadan_id__in=fadan_ids + ).values('fadan_id', 'tupian_url', 'yongtu') + for t in tupians: + fid = t['fadan_id'] + url = t['tupian_url'] + if t['yongtu'] == 1: + zhengju_map.setdefault(fid, []).append(url) + else: + shensu_map.setdefault(fid, []).append(url) + + # 组装返回数据 + chufa_list = [] + for r in records: + rid = r.id + item = { + 'id': rid, + 'beichufa_id': r.beichufa_id, + 'guanliandingdan_id': r.guanliandingdan_id or '', + 'chufaliyou': r.chufaliyou or '', + 'fakuanjine': str(r.fakuanjine), + 'zhuangtai': r.zhuangtai, + 'yingxiang_qiangdan': r.yingxiang_qiangdan, + 'shensuliyou': r.shensuliyou or '', + 'bohuiliyou': r.bohuiliyou or '', + 'zhengju_tupian': zhengju_map.get(rid, []), # 证据图片 + 'shensu_tupian': shensu_map.get(rid, []), # 申诉图片 + 'create_time': r.create_time.strftime('%Y-%m-%d %H:%M:%S') if r.create_time else '', + 'update_time': r.update_time.strftime('%Y-%m-%d %H:%M:%S') if r.update_time else '', + } + chufa_list.append(item) + + has_more = page_obj.has_next() + return Response({ + 'code': 0, + 'msg': '获取成功', + 'data': { + 'list': chufa_list, + 'has_more': has_more, + 'page': page, + 'page_size': page_size, + } + }) + + except Exception as e: + logger.error(f"获取罚款列表异常 {yonghuid if 'yonghuid' in locals() else ''}: {e}", exc_info=True) + return Response({'code': 99, 'msg': '系统繁忙'}, status=500) + + +class FaKuanShenSuView(APIView): + """ + 罚款申诉提交/修改接口 + 路径:POST /yonghu/fkss + 参数: + fadan_id: 罚单ID + shensu_liyou: 申诉理由 + shensu_tupian_urls: 图片相对URL数组(可空数组,但至少要有理由) + 逻辑: + - 仅待缴纳状态且无驳回理由的罚单可申诉 + - 允许对已有申诉进行修改:先删除旧申诉图片(yongtu=2),再添加新图片 + """ + permission_classes = [IsAuthenticated, DashouPermission] + + def post(self, request): + try: + user_main = request.user + yonghuid = user_main.yonghuid + fadan_id = request.data.get('fadan_id') + shensu_liyou = request.data.get('shensu_liyou', '').strip() + tupian_urls = request.data.get('shensu_tupian_urls', []) + + if not fadan_id: + return Response({'code': 400, 'msg': '缺少罚款ID'}, status=400) + if not shensu_liyou: + return Response({'code': 400, 'msg': '请输入申诉理由'}, status=400) + + # 查询罚单,确保属于当前打手 + try: + fadan = Fadan.objects.get(id=fadan_id, beichufa_id=yonghuid) + except Fadan.DoesNotExist: + return Response({'code': 404, 'msg': '罚单不存在或无权操作'}, status=404) + + # 只允许待缴纳状态(状态1)且没有驳回理由的罚单申诉 + if fadan.zhuangtai != 1: + return Response({'code': 400, 'msg': '当前状态不可申诉'}, status=400) + if fadan.bohuiliyou: + return Response({'code': 400, 'msg': '该罚单申诉已被驳回,不可再次申诉'}, status=400) + + # 更新罚单状态为申诉中,并记录理由 + fadan.zhuangtai = 3 + fadan.shensuliyou = shensu_liyou + fadan.save() + + # 删除旧的申诉图片(yongtu=2),仅删除打手自己上传的申诉图 + FadanShensuTupian.objects.filter(fadan=fadan, yongtu=2).delete() + + # 插入新上传的申诉图片 + for url in tupian_urls: + FadanShensuTupian.objects.create( + fadan=fadan, + beichufa_id=yonghuid, + tupian_url=url, + yongtu=2 + ) + + return Response({'code': 0, 'msg': '申诉提交成功'}) + + except Exception as e: + logger.error(f"罚款申诉异常 {yonghuid if 'yonghuid' in locals() else ''}: {e}", exc_info=True) + return Response({'code': 99, 'msg': '系统繁忙'}, status=500) + + + + + + + +# 需要替换为你自己项目中的导入路径 + +# XIAOCHENGXU LOGGER? +# ==================== 1. 罚款支付下单接口 ==================== + +class FaKuanPayView(APIView): + """ + 罚款微信支付下单接口(完全照抄 YajinGoumai) + 路径:POST /yonghu/fkjn + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + fadan_id = request.data.get('fadan_id') + if not fadan_id: + return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) + + try: + fadan = Fadan.objects.get(id=fadan_id, beichufa_id=request.user.yonghuid) + except Fadan.DoesNotExist: + return Response({'code': 404, 'message': '罚单不存在或不属于您'}, status=status.HTTP_404_NOT_FOUND) + + if fadan.zhuangtai not in [1, 3]: + return Response({'code': 400, 'message': '罚单当前状态不可支付'}, status=status.HTTP_400_BAD_REQUEST) + + jine = float(fadan.fakuanjine) + + timestamp_ms = int(time.time() * 1000) + timestamp_ns = int(time.time_ns() // 1000) % 1000000 + random_num = random.randint(0, 99) + dingdanid = f"FK{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" + + chongzhi_jilu = Czjilu.objects.create( + dingdan_id=dingdanid, + zhuangtai=9, + yonghuid=request.user.yonghuid, + jine=jine, + leixing=4, + shuoming=settings.FAKUAN_PAY_DESCRIPTION + ) + + try: + pay_params = self.generate_wechat_pay_params( + dingdanid=dingdanid, + jine=jine, + openid=request.user.openid, + pay_type='fakuan' + ) + except Exception as e: + chongzhi_jilu.delete() + logger.error(f"生成微信支付参数失败: {str(e)}") + return Response({ + 'code': 500, + 'message': f'支付参数生成失败: {str(e)}' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response({ + 'code': 200, + 'message': '支付参数生成成功', + 'payParams': pay_params, + 'dingdanid': dingdanid + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"罚款支付下单接口异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误,请稍后重试' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): + APPID = settings.WEIXIN_APPID + MCHID = settings.WEIXIN_MCHID + KEY = settings.WEIXIN_SHANGHUMIYAO + + if pay_type == 'fakuan': + PAY_DESCRIPTION = settings.FAKUAN_PAY_DESCRIPTION + NOTIFY_URL = settings.FAKUAN_PAY_NOTIFY_URL # 回调地址在这里 + else: + raise Exception('未知的支付类型') + + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + total_fee = int(jine * 100) + trade_type = 'JSAPI' + user_ip = self.get_client_ip() + + # 第一次签名 + sign_data = { + 'appid': APPID, + 'mch_id': MCHID, + 'nonce_str': nonce_str, + 'body': PAY_DESCRIPTION, + 'out_trade_no': dingdanid, + 'total_fee': total_fee, + 'spbill_create_ip': user_ip, + 'notify_url': NOTIFY_URL, # 关键:把回调地址传入 + 'trade_type': trade_type, + 'openid': openid + } + + sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) + sign_string += f'&key={KEY}' + sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + # 构建 XML,这里必须包含 notify_url + xml_data = f""" +{APPID} +{PAY_DESCRIPTION} +{MCHID} +{nonce_str} +{NOTIFY_URL} +{openid} +{dingdanid} +{user_ip} +{total_fee} +{trade_type} +{sign} +""" + + response = requests.post( + 'https://api.mch.weixin.qq.com/pay/unifiedorder', + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + timeout=10 + ) + + root = ET.fromstring(response.content) + return_code = root.find('return_code').text + result_code = root.find('result_code').text + + if return_code == 'SUCCESS' and result_code == 'SUCCESS': + prepay_id = root.find('prepay_id').text + timestamp = str(int(time.time())) + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + package = f'prepay_id={prepay_id}' + sign_type = 'MD5' + + pay_sign_data = { + 'appId': APPID, + 'timeStamp': timestamp, + 'nonceStr': nonce_str, + 'package': package, + 'signType': sign_type + } + + sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) + pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) + pay_sign_string += f'&key={KEY}' + pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() + + return { + 'appId': APPID, + 'timeStamp': timestamp, + 'nonceStr': nonce_str, + 'package': package, + 'signType': sign_type, + 'paySign': pay_sign + } + else: + err_code = root.find('err_code') + err_code_des = root.find('err_code_des') + error_details = f"return_code: {return_code}" + if err_code is not None: + error_details += f", err_code: {err_code.text}" + if err_code_des is not None: + error_details += f", err_code_des: {err_code_des.text}" + raise Exception(f"微信支付下单失败: {error_details}") + + def get_client_ip(self): + request = self.request + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip if ip else '127.0.0.1' + + +# ==================== 2. 罚款支付回调接口 ==================== +class FaKuanHuitiaoView(View): + + """ + 罚款支付回调接口(严格借鉴 YajinHuitiao 接口,参数一个不少) + 路径:POST /yonghu/fk_huitiao + """ + def post(self, request): + try: + # 1. 解析微信回调XML数据 + request_body = request.body + logger.info(f"收到微信支付回调,原始数据: {request_body.decode('utf-8')}") + + root = ET.fromstring(request_body) + + # 获取关键字段 + return_code = root.find('return_code').text + result_code = root.find('result_code').text + out_trade_no = root.find('out_trade_no').text # 订单ID + transaction_id = root.find('transaction_id').text # 微信交易号 + total_fee = int(root.find('total_fee').text) # 支付金额(分) + + logger.info(f"回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") + + # 2. 验证基本返回状态 + if return_code != 'SUCCESS' or result_code != 'SUCCESS': + logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}") + return self.wechat_response('FAIL', '支付失败') + + # 3. 验证签名(确保是微信发来的) + if not self.verify_signature(request_body): + logger.error(f"签名验证失败: {out_trade_no}") + return self.wechat_response('FAIL', '签名验证失败') + + # 4. 查询订单 + try: + order = Czjilu.objects.get(dingdan_id=out_trade_no) + logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") + except Czjilu.DoesNotExist: + logger.error(f"订单不存在: {out_trade_no}") + return self.wechat_response('FAIL', '订单不存在') + + # 5. 检查订单状态是否为未支付(9) + if order.zhuangtai != 9: + logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") + # 已处理过的订单直接返回成功,避免微信重复回调 + return self.wechat_response('SUCCESS', 'OK') + + # 6. 验证金额(微信返回的是分,需要转换) + wechat_amount_yuan = total_fee / 100 # 转换为元 + order_amount_yuan = float(order.jine) # 订单金额(元) + + # 允许1分钱的误差(浮点数比较) + if abs(wechat_amount_yuan - order_amount_yuan) > 0.01: + logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") + return self.wechat_response('FAIL', '金额不一致') + + # 7. 更新订单状态为已支付(3) + order.zhuangtai = 3 + order.save(update_fields=['zhuangtai']) + logger.info(f"订单状态更新成功: {order.dingdan_id} -> 3") + + # 8. 更新罚单状态为已缴纳(2) + try: + # 根据订单中的用户ID和金额匹配待缴纳的罚单 + fadan = Fadan.objects.filter( + beichufa_id=order.yonghuid, + fakuanjine=order.jine, + zhuangtai__in=[1, 3] + ).first() + if fadan: + fadan.zhuangtai = 2 # 已缴纳 + fadan.save(update_fields=['zhuangtai']) + logger.info(f"罚单 {fadan.id} 已自动标记为已缴纳") + + success, msg = process_fadan_fenhong(fadan.id) + if not success: + logger.error(f"罚单 {fadan.id} 分红处理失败: {msg}") + else: + logger.info(f"罚单 {fadan.id} 分红处理完成: {msg}") + + except Exception as e: + logger.error(f"更新罚单状态失败: {str(e)}", exc_info=True) + + # 9. 更新收支记录表 + try: + + # 获取订单金额(确保是Decimal类型) + jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine + + update_daily_income(jine_decimal) + + # 查询或创建ID=1的收支记录 + szjilu, created = Szjilu.objects.get_or_create( + id=1, + defaults={ + 'zongsy': Decimal('0.00'), + 'zongls': Decimal('0.00'), + 'zongzc': Decimal('0.00'), + 'jrzc': Decimal('0.00'), + 'jrls': Decimal('0.00') + } + ) + + # 更新三个字段 + szjilu.zongsy += jine_decimal # 总收益 + szjilu.zongls += jine_decimal # 总流水 + szjilu.jrls += jine_decimal # 今日流水 + + szjilu.save() + + logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") + + except Exception as e: + # 收支记录更新失败不影响主流程,只记录日志 + logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) + + # 10. 返回成功响应给微信 + logger.info(f"罚款支付回调处理完成: {out_trade_no}") + return self.wechat_response('SUCCESS', 'OK') + + except ET.ParseError as e: + logger.error(f"XML解析失败: {str(e)}") + return self.wechat_response('FAIL', '数据格式错误') + except Exception as e: + logger.error(f"罚款支付回调处理异常: {str(e)}", exc_info=True) + # 返回FAIL让微信重试 + return self.wechat_response('FAIL', '处理异常') + + def verify_signature(self, xml_data): + """验证微信支付签名(完全照搬 YajinHuitiao 的 verify_signature)""" + try: + root = ET.fromstring(xml_data) + sign = root.find('sign').text + + if not sign: + logger.error("签名字段不存在") + return False + + # 获取所有参数并排除sign + params = {} + for child in root: + if child.tag != 'sign' and child.text is not None: + params[child.tag] = child.text + + # 按照参数名ASCII码从小到大排序 + sorted_params = sorted(params.items(), key=lambda x: x[0]) + + # 拼接字符串 + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) + sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + + # 计算签名 + calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + is_valid = sign == calculated_sign + if not is_valid: + logger.error(f"签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}") + + return is_valid + + except Exception as e: + logger.error(f"验证签名异常: {str(e)}") + return False + + def wechat_response(self, return_code, return_msg): + """返回微信支付回调响应(完全照搬 YajinHuitiao 的 wechat_response)""" + xml_response = f""" + + +""" + + logger.info(f"返回微信响应: {return_code} - {return_msg}") + return HttpResponse(xml_response, content_type='application/xml') + + +# ==================== 3. 支付轮询接口 ==================== +class FaKuanPayPollView(APIView): + """罚款支付轮询接口(借鉴 YajinLunxun)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 400, 'message': '订单ID不能为空'}) + + order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid) + if order.zhuangtai == 3: + return Response({'code': 200, 'message': '支付成功'}) + else: + return Response({'code': 400, 'message': '订单尚未支付'}) + except Czjilu.DoesNotExist: + return Response({'code': 400, 'message': '订单不存在'}) + except Exception as e: + logger.error(f"轮询异常: {str(e)}") + return Response({'code': 500, 'message': '服务器内部错误'}) + + +# ==================== 4. 支付失败处理接口 ==================== +class FaKuanPayFailView(APIView): + """罚款支付失败处理接口(借鉴 YajinShibai)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 400, 'message': '订单ID不能为空'}) + + order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid) + if order.zhuangtai == 9: # 未支付 + order.delete() + logger.info(f"用户{request.user.yonghuid}删除未支付订单: {dingdanid}") + return Response({'code': 200, 'message': '订单已删除'}) + else: + return Response({'code': 400, 'message': '订单状态不是未支付,无法删除'}) + except Czjilu.DoesNotExist: + return Response({'code': 400, 'message': '订单不存在'}) + except Exception as e: + logger.error(f"支付失败处理异常: {str(e)}") + return Response({'code': 500, 'message': '服务器内部错误'}) + + + + + + +class KaohePayView(APIView): + """ + 考核支付下单接口 + 路径:POST /yonghu/khzf + 参数: + tag_id: 标签ID + game_nick: 游戏昵称 + remark: 备注 + reapply: 是否再次申请 (true/false) + jilu_id: 再次申请时的原记录ID(reapply=true时必传) + shenheguan_id: 指定考核官ID(可选) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + tag_id = request.data.get('tag_id') + game_nick = request.data.get('game_nick', '').strip() + remark = request.data.get('remark', '').strip() + reapply = request.data.get('reapply', False) + jilu_id = request.data.get('jilu_id') if reapply else None + shenheguan_id = request.data.get('shenheguan_id', '').strip() + + logger.info(f"用户 {user.yonghuid} 发起考核支付申请,标签ID: {tag_id}, 再次申请: {reapply}") + + if not tag_id or not game_nick: + logger.warning(f"用户 {user.yonghuid} 参数不完整: tag_id={tag_id}, game_nick={game_nick}") + return Response({'code': 400, 'message': '标签ID和游戏昵称不能为空'}) + + # 1. 检查是否有未支付的考核订单 + """unpaid_order = Czjilu.objects.filter( + yonghuid=user.yonghuid, + leixing=5, + zhuangtai=9 + ).exists() + if unpaid_order: + logger.warning(f"用户 {user.yonghuid} 存在未支付的考核订单") + return Response({'code': 400, 'message': '您有未支付的考核订单,请先完成支付或取消'})""" + + # 2. 验证标签 + try: + chenghao = Chenghao.objects.get(id=tag_id) + logger.info(f"标签验证通过: {chenghao.mingcheng}") + except Chenghao.DoesNotExist: + logger.error(f"标签不存在: tag_id={tag_id}") + return Response({'code': 404, 'message': '标签不存在'}) + + # 3. 检查用户是否已拥有该标签 + if YonghuChenghao.objects.filter(yonghu=user, chenghao=chenghao).exists(): + logger.warning(f"用户 {user.yonghuid} 已拥有标签 {chenghao.mingcheng}") + return Response({'code': 400, 'message': '您已拥有该标签,无需重复考核'}) + + # 4. 检查是否有未完成的申请 + if ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + chenghao=chenghao, + zhuangtai__in=[0, 3] # 审核中、待审核 + ).exists(): + logger.warning(f"用户 {user.yonghuid} 有未完成的考核申请") + return Response({'code': 400, 'message': '您有未完成的考核申请,请等待结果'}) + + # 5. 计算考核次数和费用 + if reapply and jilu_id: + last = ShenheJilu.objects.filter( + jilu_id=jilu_id, + shenqingren_id=user.yonghuid, + zhuangtai=2 + ).first() + if not last: + logger.warning(f"用户 {user.yonghuid} 再次申请失败:原记录不存在或状态不是未通过,jilu_id={jilu_id}") + return Response({'code': 400, 'message': '原记录不存在或状态不是未通过'}) + new_cishu = last.cishu + 1 + logger.info(f"再次申请,原记录ID: {jilu_id},新次数: {new_cishu}") + else: + max_cishu = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + chenghao=chenghao, + zhuangtai__in=[1, 2] + ).aggregate(max=models.Max('cishu'))['max'] or 0 + new_cishu = max_cishu + 1 + logger.info(f"首次申请,计算次数: {new_cishu}") + + # 6. 获取本次费用 + fee = get_tag_fee(chenghao, new_cishu) + logger.info(f"本次考核费用: {fee}元,次数: {new_cishu}") + + if fee <= 0: + logger.warning(f"费用为0,应使用免费申请接口,tag_id={tag_id}, cishu={new_cishu}") + return Response({'code': 400, 'message': '本次费用为0,请直接使用免费申请接口'}) + + # 7. 生成订单ID + timestamp_ms = int(time.time() * 1000) + timestamp_ns = int(time.time_ns() // 1000) % 1000000 + random_num = random.randint(0, 99) + dingdanid = f"KH{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" + logger.info(f"生成订单ID: {dingdanid}") + + # 8. 创建支付订单(Czjilu) + order = Czjilu.objects.create( + dingdan_id=dingdanid, + zhuangtai=9, # 未支付 + yonghuid=user.yonghuid, + jine=float(fee), + leixing=5, # 5-考核支付 + shuoming=settings.KAOHE_PAY_DESCRIPTION + ) + logger.info(f"创建支付订单成功: {dingdanid}") + + # 9. 保存考核支付临时上下文(用于回调时取业务参数) + try: + KaohePayTemp.objects.create( + dingdan_id=dingdanid, + yonghuid=user.yonghuid, + tag_id=tag_id, + game_nick=game_nick, + remark=remark, + reapply=reapply, + jilu_id=jilu_id if reapply else None, + shenheguan_id=shenheguan_id, + cishu=new_cishu, + fee=fee, + ) + logger.info(f"保存临时上下文成功: {dingdanid}") + except Exception as e: + logger.error(f"保存临时上下文失败: {str(e)}", exc_info=True) + # 临时表保存失败应该回滚订单 + order.delete() + return Response({'code': 500, 'message': '系统异常,请稍后重试'}) + + # 10. 构造 attach 参数(只传订单ID,确保不超127字节) + attach_str = json.dumps({'oid': dingdanid}, ensure_ascii=False) + logger.info(f"attach内容: {attach_str},长度: {len(attach_str.encode('utf-8'))}字节") + + # 11. 生成微信支付参数 + try: + pay_params = self.generate_wechat_pay_params( + dingdanid=dingdanid, + jine=float(fee), + openid=user.openid, + attach=attach_str, + pay_type='kaohe' + ) + logger.info(f"微信支付参数生成成功: {dingdanid}") + except Exception as e: + order.delete() # 下单失败,删除订单记录 + # 同时删除临时表记录 + try: + KaohePayTemp.objects.filter(dingdan_id=dingdanid).delete() + except Exception as del_e: + logger.error(f"删除临时表记录失败: {str(del_e)}") + logger.error(f"生成微信支付参数失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'message': f'支付参数生成失败: {str(e)}'}) + + return Response({ + 'code': 200, + 'message': '支付参数生成成功', + 'payParams': pay_params, + 'dingdanid': dingdanid + }) + + except Exception as e: + logger.error(f"考核支付下单异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'message': '服务器内部错误'}) + + def generate_wechat_pay_params(self, dingdanid, jine, openid, attach, pay_type): + """生成微信JSAPI支付参数""" + APPID = settings.WEIXIN_APPID + MCHID = settings.WEIXIN_MCHID + KEY = settings.WEIXIN_SHANGHUMIYAO + + if pay_type == 'kaohe': + PAY_DESCRIPTION = settings.KAOHE_PAY_DESCRIPTION + NOTIFY_URL = settings.KAOHE_PAY_NOTIFY_URL + else: + raise Exception('未知支付类型') + + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + total_fee = int(jine * 100) + trade_type = 'JSAPI' + user_ip = self.get_client_ip() + + logger.info(f"微信支付下单参数: out_trade_no={dingdanid}, total_fee={total_fee}, openid={openid}, ip={user_ip}") + + # 第一次签名(统一下单) + sign_data = { + 'appid': APPID, + 'mch_id': MCHID, + 'nonce_str': nonce_str, + 'body': PAY_DESCRIPTION, + 'out_trade_no': dingdanid, + 'total_fee': total_fee, + 'spbill_create_ip': user_ip, + 'notify_url': NOTIFY_URL, + 'trade_type': trade_type, + 'openid': openid, + 'attach': attach, + } + + sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) + sign_string += f'&key={KEY}' + sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + # 构建XML请求体 + xml_data = f""" +{APPID} +{PAY_DESCRIPTION} +{MCHID} +{nonce_str} +{NOTIFY_URL} +{openid} +{dingdanid} +{user_ip} +{total_fee} +{trade_type} + +{sign} +""" + + logger.info(f"微信支付请求XML: {xml_data[:500]}...") # 只打印前500字符 + + response = requests.post( + 'https://api.mch.weixin.qq.com/pay/unifiedorder', + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + timeout=10 + ) + + logger.info(f"微信支付响应: {response.text}") + + root = ET.fromstring(response.content) + + # 安全获取return_code + return_code_node = root.find('return_code') + if return_code_node is None or return_code_node.text is None: + logger.error(f"微信返回缺少return_code节点,原始内容: {response.text}") + raise Exception('微信支付接口异常,缺少return_code') + return_code = return_code_node.text + + # 安全获取result_code + result_code_node = root.find('result_code') + if result_code_node is None or result_code_node.text is None: + logger.error(f"微信返回缺少result_code节点,原始内容: {response.text}") + raise Exception('微信支付接口异常,缺少result_code') + result_code = result_code_node.text + + if return_code == 'SUCCESS' and result_code == 'SUCCESS': + prepay_id_node = root.find('prepay_id') + if prepay_id_node is None or prepay_id_node.text is None: + logger.error(f"统一下单成功但未返回prepay_id,原始内容: {response.text}") + raise Exception('微信支付接口异常,缺少prepay_id') + prepay_id = prepay_id_node.text + + timestamp = str(int(time.time())) + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + package = f'prepay_id={prepay_id}' + sign_type = 'MD5' + + # 第二次签名(前端调起支付用) + pay_sign_data = { + 'appId': APPID, + 'timeStamp': timestamp, + 'nonceStr': nonce_str, + 'package': package, + 'signType': sign_type, + } + sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) + pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) + pay_sign_string += f'&key={KEY}' + pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() + + logger.info(f"微信支付预下单成功,prepay_id={prepay_id}") + + return { + 'appId': APPID, + 'timeStamp': timestamp, + 'nonceStr': nonce_str, + 'package': package, + 'signType': sign_type, + 'paySign': pay_sign, + } + else: + # 安全获取错误信息 + err_code_node = root.find('err_code') + err_code = err_code_node.text if err_code_node is not None else '未知错误码' + err_code_des_node = root.find('err_code_des') + err_code_des = err_code_des_node.text if err_code_des_node is not None else '未知错误描述' + error_details = f"return_code: {return_code}, result_code: {result_code}, err_code: {err_code}, err_code_des: {err_code_des}" + logger.error(f"微信支付下单失败: {error_details}") + raise Exception(f"微信支付下单失败: {error_details}") + + def get_client_ip(self): + """获取客户端IP""" + request = self.request + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR', '127.0.0.1') + return ip if ip else '127.0.0.1' + + +class KaohePayCallbackView(View): + """ + 考核支付回调接口 + 路径:/yonghu/kh_huitiao + """ + + def post(self, request): + try: + request_body = request.body + logger.info(f"收到考核支付回调: {request_body.decode('utf-8')}") + + root = ET.fromstring(request_body) + + # 安全获取return_code + return_code_node = root.find('return_code') + if return_code_node is None or return_code_node.text is None: + logger.error(f"微信回调XML缺少return_code节点,原始内容: {request_body.decode('utf-8')}") + return self.wechat_response('FAIL', '数据格式错误') + return_code = return_code_node.text + + # 安全获取result_code + result_code_node = root.find('result_code') + if result_code_node is None or result_code_node.text is None: + logger.error(f"微信回调XML缺少result_code节点,原始内容: {request_body.decode('utf-8')}") + return self.wechat_response('FAIL', '数据格式错误') + result_code = result_code_node.text + + # 安全获取out_trade_no + out_trade_no_node = root.find('out_trade_no') + if out_trade_no_node is None or out_trade_no_node.text is None: + logger.error(f"微信回调XML缺少out_trade_no节点,原始内容: {request_body.decode('utf-8')}") + return self.wechat_response('FAIL', '数据格式错误') + out_trade_no = out_trade_no_node.text + + # 安全获取transaction_id + transaction_id_node = root.find('transaction_id') + if transaction_id_node is None or transaction_id_node.text is None: + logger.error(f"微信回调XML缺少transaction_id节点,原始内容: {request_body.decode('utf-8')}") + return self.wechat_response('FAIL', '数据格式错误') + transaction_id = transaction_id_node.text + + # 安全获取total_fee + total_fee_node = root.find('total_fee') + if total_fee_node is None or total_fee_node.text is None: + logger.error(f"微信回调XML缺少total_fee节点,原始内容: {request_body.decode('utf-8')}") + return self.wechat_response('FAIL', '数据格式错误') + total_fee = int(total_fee_node.text) + + logger.info( + f"回调参数解析成功: out_trade_no={out_trade_no}, transaction_id={transaction_id}, total_fee={total_fee}") + + if return_code != 'SUCCESS' or result_code != 'SUCCESS': + logger.warning(f"支付状态异常: return_code={return_code}, result_code={result_code}") + return self.wechat_response('FAIL', '支付失败') + + # 验证签名 + if not self.verify_signature(request_body): + logger.error(f"签名验证失败,订单号: {out_trade_no}") + return self.wechat_response('FAIL', '签名验证失败') + logger.info(f"签名验证成功,订单号: {out_trade_no}") + + # 查找订单 + try: + order = Czjilu.objects.get(dingdan_id=out_trade_no, leixing=5) + logger.info(f"找到订单: {out_trade_no}, 当前状态: {order.zhuangtai}") + except Czjilu.DoesNotExist: + logger.error(f"考核订单不存在: {out_trade_no}") + return self.wechat_response('FAIL', '订单不存在') + + if order.zhuangtai != 9: + logger.warning(f"订单状态非未支付: {order.zhuangtai}, 订单号: {out_trade_no}") + # 已支付的订单直接返回成功,避免微信重复回调 + if order.zhuangtai == 3: + logger.info(f"订单已支付,直接返回成功: {out_trade_no}") + return self.wechat_response('SUCCESS', 'OK') + return self.wechat_response('FAIL', '订单状态异常') + + # 验证金额 + wechat_yuan = total_fee / 100 + if abs(wechat_yuan - float(order.jine)) > 0.01: + logger.error(f"金额不一致: 订单金额={order.jine}, 回调金额={wechat_yuan}, 订单号: {out_trade_no}") + return self.wechat_response('FAIL', '金额不一致') + logger.info(f"金额验证通过: {wechat_yuan}元") + + # 更新订单状态为已支付 + order.zhuangtai = 3 + order.save() + logger.info(f"订单状态更新为已支付: {out_trade_no}") + + # 从临时表获取业务参数并创建审核记录 + try: + temp = KaohePayTemp.objects.get(dingdan_id=out_trade_no, yonghuid=order.yonghuid) + logger.info(f"找到临时表记录: {out_trade_no}, 用户ID: {order.yonghuid}") + + user = UserMain.objects.get(yonghuid=order.yonghuid) + logger.info(f"获取用户信息成功: {user.yonghuid}") + + success, msg, record = create_shenhe_jilu_from_temp(user, temp) + if success: + logger.info(f"创建审核记录成功: jilu_id={record.jilu_id}, 订单号: {out_trade_no}") + else: + logger.error(f"创建审核记录失败: {msg}, 订单号: {out_trade_no}") + # 订单已支付但记录创建失败,需要标记订单异常,人工介入 + order.zhuangtai = 10 # 10表示支付成功但业务处理失败 + order.save() + logger.error(f"订单状态标记为异常(10): {out_trade_no}") + + except KaohePayTemp.DoesNotExist: + logger.error(f"临时表无记录,订单号: {out_trade_no}, 用户ID: {order.yonghuid}") + # 尝试从attach解析(兼容旧数据) + attach_element = root.find('attach') + if attach_element is not None and attach_element.text: + try: + attach_data = json.loads(attach_element.text) + logger.info(f"从attach解析数据成功: {attach_data}") + # 这里可以保留旧逻辑,但建议只记录日志 + except Exception as e: + logger.error(f"解析attach失败: {str(e)}") + except Exception as e: + logger.error(f"处理临时表或创建审核记录异常: {str(e)}", exc_info=True) + # 标记订单异常 + order.zhuangtai = 10 + order.save() + logger.error(f"订单状态标记为异常(10): {out_trade_no}") + + # 更新收支记录 + try: + jine_decimal = Decimal(str(order.jine)) + update_daily_income(jine_decimal) + logger.info(f"更新每日收入成功: {jine_decimal}元") + + szjilu, created = Szjilu.objects.get_or_create( + id=1, + defaults={ + 'zongsy': Decimal('0.00'), + 'zongls': Decimal('0.00'), + 'zongzc': Decimal('0.00'), + 'jrzc': Decimal('0.00'), + 'jrls': Decimal('0.00'), + } + ) + szjilu.zongsy += jine_decimal + szjilu.zongls += jine_decimal + szjilu.jrls += jine_decimal + szjilu.save() + logger.info(f"更新收支记录成功: zongsy={szjilu.zongsy}") + except Exception as e: + logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) + + logger.info(f"考核支付回调处理完成,订单号: {out_trade_no}") + return self.wechat_response('SUCCESS', 'OK') + + except ET.ParseError as e: + logger.error(f"XML解析失败: {str(e)}, 原始数据: {request.body.decode('utf-8')}") + return self.wechat_response('FAIL', '数据格式错误') + except Exception as e: + logger.error(f"考核支付回调异常: {str(e)}", exc_info=True) + return self.wechat_response('FAIL', '处理异常') + + def verify_signature(self, xml_data): + """验证微信支付签名""" + try: + root = ET.fromstring(xml_data) + sign_node = root.find('sign') + if sign_node is None or sign_node.text is None: + logger.error("微信回调XML缺少sign节点") + return False + sign = sign_node.text + + params = {} + for child in root: + if child.tag != 'sign' and child.text is not None: + params[child.tag] = child.text + + sorted_params = sorted(params.items(), key=lambda x: x[0]) + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) + sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + result = sign == calculated_sign + if not result: + logger.error(f"签名验证失败: 原签名={sign}, 计算签名={calculated_sign}") + return result + except Exception as e: + logger.error(f"签名验证异常: {str(e)}", exc_info=True) + return False + + def wechat_response(self, return_code, return_msg): + """构造微信回调响应XML""" + xml = f""" + + +""" + logger.info(f"返回微信响应: {xml}") + return HttpResponse(xml, content_type='application/xml') + + + + + + + +class KaohePayPollView(APIView): + """考核支付轮询""" + permission_classes = [IsAuthenticated] + + def post(self, request): + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 400, 'message': '订单ID不能为空'}) + try: + order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5) + if order.zhuangtai == 3: + return Response({'code': 200, 'message': '支付成功'}) + return Response({'code': 400, 'message': '订单尚未支付'}) + except Czjilu.DoesNotExist: + return Response({'code': 400, 'message': '订单不存在'}) + + +class KaohePayFailView(APIView): + """考核支付失败处理""" + permission_classes = [IsAuthenticated] + + def post(self, request): + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 400, 'message': '订单ID不能为空'}) + try: + order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5) + if order.zhuangtai == 9: + order.delete() + return Response({'code': 200, 'message': '订单已删除'}) + return Response({'code': 400, 'message': '订单状态无法删除'}) + except Czjilu.DoesNotExist: + return Response({'code': 400, 'message': '订单不存在'}) + + + + + + + + +class TixianAssetView(APIView): + """ + 获取用户所有可提现资产及费率 + 请求方式:POST + 认证:JWT Token + 返回: + { + "code": 200, + "msg": "success", + "data": { + "dashou_yue": "123.45", + "dashou_yajin": "500.00", + "shangjia_yue": "0.00", + "guanshi_fenyong": "67.89", + "zuzhang_fenyong": "0.00", + "kaoheguan_fenyong": "0.00", + "dashou_rate": 0.08, + "dashou_yajin_rate": 0.05, + "shangjia_rate": 0.06, + "guanshi_rate": 0.08, + "zuzhang_rate": 0.05, + "kaoheguan_rate": 0.07 + } + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + user = request.user + # 初始化返回数据,默认全部为0 + data = { + 'dashou_yue': '0.00', + 'dashou_yajin': '0.00', + 'shangjia_yue': '0.00', + 'guanshi_fenyong': '0.00', + 'zuzhang_fenyong': '0.00', + 'kaoheguan_fenyong': '0.00', + 'dashou_rate': 0.00, + 'dashou_yajin_rate': 0.00, + 'shangjia_rate': 0.00, + 'guanshi_rate': 0.00, + 'zuzhang_rate': 0.00, + 'kaoheguan_rate': 0.00, + } + + # ------------------- 打手资产 ------------------- + try: + dashou = user.dashou_profile + data['dashou_yue'] = str(dashou.yue) + data['dashou_yajin'] = str(dashou.yajin) + except ObjectDoesNotExist: + pass + + # ------------------- 商家资产 ------------------- + try: + shangjia = user.shop_profile + data['shangjia_yue'] = str(shangjia.yue) + except ObjectDoesNotExist: + pass + + # ------------------- 管事资产 ------------------- + try: + guanshi = user.guanshi_profile + data['guanshi_fenyong'] = str(guanshi.yue) + except ObjectDoesNotExist: + pass + + # ------------------- 组长资产 ------------------- + try: + zuzhang = user.zuzhang_profile + data['zuzhang_fenyong'] = str(zuzhang.ketixian_jine) + except ObjectDoesNotExist: + pass + + # ------------------- 审核官(考核官)资产 ------------------- + try: + kaoheguan = user.shenheguan_profile + data['kaoheguan_fenyong'] = str(kaoheguan.yue) + except ObjectDoesNotExist: + pass + + # ------------------- 费率查询(从 Lilubiao 获取) ------------------- + # 打手提现费率 (类型 '5') + lilu_obj = Lilubiao.objects.filter(fadanpingtai='5').first() + data['dashou_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 + + # 打手保证金提现费率 (类型 '11') + lilu_obj = Lilubiao.objects.filter(fadanpingtai='11').first() + data['dashou_yajin_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 + + # 商家提现费率 (类型 '10') + lilu_obj = Lilubiao.objects.filter(fadanpingtai='10').first() + data['shangjia_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 + + # 管事提现费率 (类型 '6') + lilu_obj = Lilubiao.objects.filter(fadanpingtai='6').first() + data['guanshi_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 + + # 组长提现费率 (类型 '8') + lilu_obj = Lilubiao.objects.filter(fadanpingtai='8').first() + data['zuzhang_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 + + # 审核官(考核官)提现费率 (类型 '9') + lilu_obj = Lilubiao.objects.filter(fadanpingtai='9').first() + data['kaoheguan_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': data + }, status=status.HTTP_200_OK) + + + + + +class GuanshiContactView(APIView): + """ + 管事获取自己的邀请人(组长)信息 + 路径: /api/yonghu/guanshi_contact + 方法: POST + 权限: JWT认证 + 请求体: 无(仅需携带合法token) + 成功返回: + code: 200, + data: { + uid: "1234567", // 组长用户ID + nicheng: "张三", // 组长昵称 + touxiang: "avatar/xxx.jpg" // 组长头像相对路径(前端拼接完整URL) + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取当前登录用户(必须已通过JWT认证) + current_user = request.user # 类型: UserMain + + # 2. 校验是否为管事身份(存在管事扩展表记录) + try: + guanshi_profile = current_user.guanshi_profile + except ObjectDoesNotExist: + return Response({ + 'code': 403, + 'msg': '您还不是管事,无法获取组长信息', + 'data': None + }) + + # 3. 获取邀请人ID(管事扩展表中的 yaoqingren 字段存储的是邀请人的 yonghuid) + inviter_yonghuid = guanshi_profile.yaoqingren + if not inviter_yonghuid: + return Response({ + 'code': 404, + 'msg': '未找到邀请人信息', + 'data': None + }) + + # 4. 根据邀请人用户ID查询用户主表 + try: + inviter_user = UserMain.objects.get(yonghuid=inviter_yonghuid) + except UserMain.DoesNotExist: + logger.error(f"邀请人用户不存在: yonghuid={inviter_yonghuid}") + return Response({ + 'code': 404, + 'msg': '邀请人账号异常', + 'data': None + }) + + # 5. 获取邀请人昵称(从老板扩展表取,若不存在则给默认值) + try: + inviter_nicheng = inviter_user.boss_profile.nickname + except ObjectDoesNotExist: + inviter_nicheng = '组长' # 兜底昵称 + + # 6. 构造返回数据 + response_data = { + 'uid': inviter_user.yonghuid, + 'nicheng': inviter_nicheng, + 'touxiang': inviter_user.avatar or '' # 头像相对路径 + } + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': response_data + }) + + + + + + + + + +logger = logging.getLogger(__name__) # 恢复区间? + +def generate_tixian_id(): + """生成唯一提现订单号:TX + 时间戳(13位) + 4位随机数""" + timestamp = str(int(time.time() * 1000)) + rand = ''.join(random.choices(string.digits, k=4)) + return f'TX{timestamp}{rand}' + + +class TixianShenqingV3View(APIView): + """ + 自动提现收款接口(新流程第二步) + POST /yonghu/tixiansq + 必须传 shenhe_danhao;审核状态须为 6=待收款;申请时已扣款,本接口不再扣款 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + shenhe_danhao = (request.data.get('shenhe_danhao') or '').strip() + if shenhe_danhao: + return process_audit_collect(request) + return Response({ + 'code': 7, + 'msg': '缺少审核单号,请先提交提现申请(/yonghu/zddksh)并等待审核通过后再收款', + }) + + + +class TixianCallbackV3View(APIView): + """ + 微信转账结果回调接口(最终状态通知) + 处理微信异步发送的转账成功/失败结果 + """ + permission_classes = [] # 无需认证,签名验证保证安全 + + def post(self, request): + headers = request.headers + body = request.body.decode('utf-8') + + # 1. 验证签名 + if not verify_wechat_sign(headers, body): + return Response({'code': 'FAIL', 'message': '签名验证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 解密数据 + try: + data = json.loads(body) + resource = data.get('resource') + if not resource: + return Response({'code': 'FAIL', 'message': 'resource不存在'}, status=status.HTTP_400_BAD_REQUEST) + + associated_data = resource.get('associated_data', '') + nonce = resource.get('nonce') + ciphertext = resource.get('ciphertext') + if not nonce or not ciphertext: + return Response({'code': 'FAIL', 'message': '解密字段缺失'}, status=status.HTTP_400_BAD_REQUEST) + + plaintext = decrypt_callback_ciphertext(associated_data, nonce, ciphertext) + callback_data = json.loads(plaintext) + except Exception as e: + logger.error(f"回调解密失败: {str(e)}", exc_info=True) + return Response({'code': 'FAIL', 'message': f'解密失败: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST) + + # 3. 获取关键信息 + out_bill_no = callback_data.get('out_bill_no') # 商户提现单号(即 tixian_id) + transfer_status = callback_data.get('transfer_status') # SUCCESS / FAIL + wechat_transfer_no = callback_data.get('transfer_no') # 微信转账单号 + fail_reason = callback_data.get('fail_reason', '') + + if not out_bill_no: + return Response({'code': 'FAIL', 'message': 'out_bill_no缺失'}, status=status.HTTP_400_BAD_REQUEST) + + # 4. 幂等处理:只更新状态为0(处理中)的记录,已最终状态不再处理 + with transaction.atomic(): + try: + auto_record = TixianAutoRecord.objects.select_for_update().get(tixian_id=out_bill_no) + except TixianAutoRecord.DoesNotExist: + # 记录不存在,视为已处理,返回成功 + return Response({'code': 'SUCCESS', 'message': 'ok'}) + + # 如果已经是最新状态,直接返回成功(幂等) + if auto_record.zhuangtai != 0: + return Response({'code': 'SUCCESS', 'message': 'already processed'}) + + if transfer_status == 'SUCCESS': + mark_transfer_success( + auto_record, + wechat_transfer_no=wechat_transfer_no, + with_accounting=True, + ) + + else: # transfer_status == 'FAIL' + auto_record.zhuangtai = 2 + auto_record.fail_reason = fail_reason or '微信转账失败' + auto_record.save(update_fields=['zhuangtai', 'fail_reason']) + + if not auto_record.shenhe_danhao: + logger.error('回调失败但打款记录无审核单号 bill=%s', out_bill_no) + else: + # 申请时已扣款,回调失败不退余额,恢复待收款(6)可重试 + try: + audit = TixianShenheJilu.objects.select_for_update().get( + shenhe_danhao=auto_record.shenhe_danhao + ) + audit.tixian_auto_id = None + audit.fail_reason = auto_record.fail_reason + sync_audit_and_jilu_status(audit, 6, fail_reason=auto_record.fail_reason) + except TixianShenheJilu.DoesNotExist: + logger.warning(f'回调失败但审核记录不存在: {auto_record.shenhe_danhao}') + release_collect_quota_for_record(auto_record) + + return Response({'code': 'SUCCESS', 'message': 'ok'}) + + + +class TixianQueRenAutoView(APIView): + """ + 用户确认提现结果 + POST /yonghu/tixianqr + 新审核流程:打款记录带 shenhe_danhao 时,确认成功同步审核表+提现记录表为 2;取消不退款(申请已扣),恢复待收款 6 可重试 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + tixian_id = request.data.get('tixian_id') + result = request.data.get('result') # 1成功 0失败/取消 + + if not tixian_id or result is None: + return Response({'code': 1, 'msg': '缺少参数'}, status=status.HTTP_400_BAD_REQUEST) + + try: + result = int(result) + except ValueError: + return Response({'code': 2, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST) + + if result not in [0, 1]: + return Response({'code': 3, 'msg': 'result值无效'}, status=status.HTTP_400_BAD_REQUEST) + + user_main = request.user + + with transaction.atomic(): + try: + auto_record = TixianAutoRecord.objects.select_for_update().get( + tixian_id=tixian_id, + yonghuid=user_main.yonghuid, + ) + except TixianAutoRecord.DoesNotExist: + return Response({'code': 4, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND) + + if result == 1: + if auto_record.zhuangtai == 1: + return Response({'code': 0, 'msg': '提现成功', 'data': None}) + + if auto_record.zhuangtai != 0: + return Response({'code': 5, 'msg': '该提现已处理'}) + + mark_transfer_success(auto_record, with_accounting=True) + + return Response({'code': 0, 'msg': '提现成功', 'data': None}) + + # 用户取消或失败 + auto_record.zhuangtai = 2 + auto_record.fail_reason = '用户取消收款' + auto_record.save(update_fields=['zhuangtai', 'fail_reason']) + + if not auto_record.shenhe_danhao: + logger.error('确认取消但打款记录无审核单号 bill=%s', tixian_id) + return Response({'code': 5, 'msg': '该提现已处理'}) + + # 申请时已扣款,取消不退余额,审核单恢复 6 待收款,可再次点收款 + try: + audit = TixianShenheJilu.objects.select_for_update().get( + shenhe_danhao=auto_record.shenhe_danhao, + ) + audit.tixian_auto_id = None + sync_audit_and_jilu_status( + audit, 6, + fail_reason='用户取消收款,可重新发起', + ) + except TixianShenheJilu.DoesNotExist: + logger.warning(f'确认取消但审核记录不存在: {auto_record.shenhe_danhao}') + release_collect_quota_for_record(auto_record) + + return Response({'code': 0, 'msg': '提现已取消', 'data': None}) + + except Exception as e: + logger.error(f'提现确认异常: {str(e)}', exc_info=True) + return Response({'code': 99, 'msg': '系统错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + diff --git a/utils/chat_utils.py b/utils/chat_utils.py index d6e6bed..b33287b 100644 --- a/utils/chat_utils.py +++ b/utils/chat_utils.py @@ -3,9 +3,9 @@ import json import logging import requests from django.conf import settings -from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia -from peizhi.models import ClubConfig -from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia +from orders.models import Dingdan, DingdanPingtai, DingdanShangjia +from config.models import ClubConfig +from users.models import UserMain, UserDashou, UserBoss, UserShangjia logger = logging.getLogger('chat_utils') diff --git a/utils/fadan_utils.py b/utils/fadan_utils.py index fdfda71..8abd86f 100644 --- a/utils/fadan_utils.py +++ b/utils/fadan_utils.py @@ -1,5 +1,5 @@ # utils/fadan_utils.py -from dingdan.models import Fadan +from orders.models import Fadan import logging logger = logging.getLogger('xiaochengxu') diff --git a/utils/invitationcode_utils.py b/utils/invitationcode_utils.py new file mode 100644 index 0000000..55a2b6c --- /dev/null +++ b/utils/invitationcode_utils.py @@ -0,0 +1,56 @@ +""" +阿龙电竞陪玩平台 - 邀请码生成工具 +生成规则:毫秒时间戳 + 用户ID + 随机数 -> Base62编码 -> 固定20位邀请码 +特点:唯一性高、生成快、无序不可读 +""" +import time +import random + +# Base62 编码字符集 (数字+小写字母+大写字母) +_BASE62_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +_BASE62_LENGTH = len(_BASE62_CHARS) + + +def base62_encode(num: int) -> str: + """ + 将整数转换为 Base62 字符串,固定长度为 20 位 + """ + if not num: + return _BASE62_CHARS[0] + res = [] + while num: + num, r = divmod(num, _BASE62_LENGTH) + res.append(_BASE62_CHARS[r]) + return ''.join(reversed(res)) + + +def CreateInvitationCode(uid_str: str) -> str: + """ + 创建唯一邀请码 (20位固定长度) + + 参数: + uid_str: 用户ID字符串,如 "123456" + + 返回: + str: 生成的唯一邀请码 + """ + ts = int(time.time()*1000) + try: + uid = int(uid_str) + except ValueError: + uid = abs(hash(uid_str)) % 10**8 + num = int(f"{ts}{uid:08d}{random.randint(0,9999):04d}") + res = base62_encode(num) + return res.rjust(20, _BASE62_CHARS[0]) if len(res)<20 else res[-20:] + + +def VerifyInvitationCode(yaoqingma: str) -> bool: + """ + 验证邀请码是否有效 (简单的格式验证) + 参数: + yaoqingma: 待验证的邀请码 + 返回: + bool: 邀请码格式是否有效 + """ + + return bool(yaoqingma and len(yaoqingma)==20 and all(c in _BASE62_CHARS for c in yaoqingma)) \ No newline at end of file diff --git a/utils/weixin_broadcast.py b/utils/weixin_broadcast.py index 47774b6..88c1b4e 100644 --- a/utils/weixin_broadcast.py +++ b/utils/weixin_broadcast.py @@ -5,7 +5,7 @@ import json # 新增,用于格式化打印 import requests from django.core.cache import cache from django.conf import settings -from yonghu.models import OfficialAccountUser +from users.models import OfficialAccountUser logger = logging.getLogger('weixin_broadcast') diff --git a/utils/yaoqingma_utils.py b/utils/yaoqingma_utils.py deleted file mode 100644 index 6ddcfbc..0000000 --- a/utils/yaoqingma_utils.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -阿龙电竞陪玩平台 - 邀请码生成工具 -生成规则:毫秒时间戳 + 用户ID + 随机数 -> Base62编码 -> 固定20位邀请码 -特点:唯一性高、生成快、无序不可读 -""" -import time -import random - -# Base62 编码字符集 (数字+小写字母+大写字母) -_BASE62_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" -_BASE62_LENGTH = 62 - - -def base62_encode(num): - """将数字转换为Base62字符串""" - if num == 0: - return _BASE62_CHARS[0] - - result = [] - while num > 0: - num, remainder = divmod(num, _BASE62_LENGTH) - result.append(_BASE62_CHARS[remainder]) - - # 反转结果字符串 - return ''.join(reversed(result)) - - -def chuangjianYaoqingma(yonghuid_str): - """ - 创建唯一邀请码 (20位固定长度) - - 参数: - yonghuid_str: 用户ID字符串,如 "123456" - - 返回: - 20位的Base62编码邀请码字符串 - """ - # 1. 获取当前毫秒时间戳 (13位) - timestamp_ms = int(time.time() * 1000) - - # 2. 将用户ID字符串转换为数字 (确保处理字符串) - try: - # 先尝试直接转换整数 - yonghuid_num = int(yonghuid_str) - except ValueError: - # 如果用户ID不是纯数字,使用哈希值 - yonghuid_num = abs(hash(yonghuid_str)) % (10 ** 8) # 取8位数字 - - # 3. 生成一个随机数 (0-9999) - random_salt = random.randint(0, 9999) - - # 4. 组合数字: 时间戳(13位) + 用户ID数字(最多8位) + 随机数(4位) - # 示例: 1647850123456 + 123456 + 7890 = 16478501234561234567890 - combined_num = int(f"{timestamp_ms}{yonghuid_num:08d}{random_salt:04d}") - - # 5. Base62编码 - encoded_str = base62_encode(combined_num) - - # 6. 确保固定20位长度: 不足补0,超过取后20位 - # 理论上组合数字非常大,编码后通常会超过20位 - if len(encoded_str) < 20: - # 左补0 (理论上不会发生,但保持安全) - encoded_str = encoded_str.rjust(20, _BASE62_CHARS[0]) - else: - # 取后20位 (更随机) - encoded_str = encoded_str[-20:] - - return encoded_str - - -def yanzhengYaoqingmaWeiyixing(yaoqingma): - """ - 验证邀请码是否有效 (简单的格式验证) - - 参数: - yaoqingma: 待验证的邀请码 - - 返回: - bool: 邀请码格式是否有效 - """ - if not yaoqingma or len(yaoqingma) != 20: - return False - - # 检查是否只包含Base62字符 - for char in yaoqingma: - if char not in _BASE62_CHARS: - return False - - return True \ No newline at end of file