From 25d6ec70f31f7f6213010ff95ea29d705786e85c Mon Sep 17 00:00:00 2001 From: XingQue Date: Fri, 12 Jun 2026 03:43:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=80=E6=96=B0=E7=89=88=E7=94=9F=E4=BA=A7?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 11 + _init_.py | 2 + a_long_dianjing/__init__.py | 8 + a_long_dianjing/asgi.py | 16 + a_long_dianjing/celery.py | 200 + a_long_dianjing/celery_app.py | 10 + a_long_dianjing/settings.py | 541 + a_long_dianjing/urls.py | 58 + a_long_dianjing/wsgi.py | 16 + celerybeat-schedule | Bin 0 -> 61440 bytes dengji/__init__.py | 0 dengji/admin.py | 3 + dengji/apps.py | 6 + dengji/migrations/0001_initial.py | 111 + .../0002_alter_dashoubiaoxian_dingdan.py | 20 + .../0003_alter_dashoubiaoxian_bankuai.py | 19 + dengji/migrations/0004_dingdanbiaoqian.py | 26 + dengji/migrations/0005_fix_state.py | 36 + dengji/migrations/0006_shenhejilu.py | 35 + dengji/migrations/0007_kaoheguanbankuai.py | 29 + ...guize_shenhejilu_dashou_beizhu_and_more.py | 48 + ...inpai_alter_shenhejilu_jilu_id_and_more.py | 58 + dengji/migrations/0010_kaohejilufeiyong.py | 29 + dengji/migrations/0011_kaohepaytemp.py | 35 + dengji/migrations/__init__.py | 0 dengji/models.py | 224 + dengji/services.py | 87 + dengji/tests.py | 3 + dengji/urls.py | 22 + dengji/utils.py | 414 + dengji/views.py | 975 + dingdan/__init__.py | 0 dingdan/admin.py | 3 + dingdan/apps.py | 10 + dingdan/cross_platform_tasks.py | 38 + dingdan/migrations/0001_initial.py | 205 + ...dan_auto_expire_at_dingdan_auto_task_id.py | 23 + ..._pending_dispatch_dingdan_status_8_time.py | 23 + .../0004_alter_tuikuanjilu_chuliid.py | 18 + .../0005_alter_chufajilu_chuliid.py | 18 + dingdan/migrations/0006_chufatupian.py | 28 + dingdan/migrations/0007_chufajilu_ssliyou.py | 18 + ...ngdan_is_cross_dingdan_partner_order_id.py | 23 + ...ingtai_clkf_dingdanpingtai_jjjs_biaoshi.py | 23 + ...remove_dingdanpingtai_clkf_dingdan_clkf.py | 22 + dingdan/migrations/0011_dingdan_tkly.py | 18 + ..._dingdan_wechat_transaction_id_and_more.py | 22 + .../0013_alter_chufajilu_qingqiuid.py | 18 + ...n_dispatch_type_dingdan_partner_club_id.py | 23 + ...esettlement_orderdashouhistory_and_more.py | 73 + ...alter_presettlement_dingdan_id_and_more.py | 22 + .../0017_dingdanpingtai_dianpu_id_and_more.py | 32 + .../0018_dingdanshangjia_partner_yonghu_id.py | 18 + ...ngdanpingtai_shangji_dianpu_id_and_more.py | 23 + dingdan/migrations/0020_fadanfenhong_fadan.py | 58 + dingdan/migrations/0021_fadanshensutupian.py | 30 + ...rtner_club_id_fadan_shenqingren_shenfen.py | 23 + .../0023_fadanshensutupian_yongtu.py | 18 + ...4_fadan_chulizhe_fadan_chulizhe_shenfen.py | 23 + .../0025_alter_fadan_shenqingren_shenfen.py | 18 + ...lter_fadan_shenqingren_shenfen_and_more.py | 37 + dingdan/migrations/0027_dingdanpeihu.py | 42 + ...28_dingdanshangjia_shangyige_dingdan_id.py | 18 + .../0029_fadanfenhong_is_individual_rate.py | 18 + .../0030_fadan_shenqingren_fenhong_jine.py | 18 + .../0031_dingdan_guanshi_fencheng.py | 18 + dingdan/migrations/__init__.py | 0 dingdan/models.py | 588 + dingdan/signals.py | 614 + dingdan/tasks.py | 216 + dingdan/tests.py | 3 + dingdan/tongzhi_tasks.py | 71 + dingdan/urls.py | 91 + dingdan/utils.py | 443 + dingdan/views.py | 11494 +++++++++++ gunicorn.conf.py | 52 + houtai/_init_.py | 0 houtai/apps.py | 5 + houtai/migrations/0001_initial.py | 296 + ...zhangritongji_shangjiaritongji_and_more.py | 339 + houtai/migrations/0003_tixianritongji.py | 54 + houtai/migrations/__init__.py | 0 houtai/migrations/_init_.py | 0 houtai/models.py | 269 + houtai/serializers.py | 39 + houtai/urls.py | 137 + houtai/utils.py | 424 + houtai/view.py | 12489 ++++++++++++ manage.py | 22 + peizhi/__init__.py | 0 peizhi/admin.py | 3 + peizhi/apps.py | 5 + peizhi/migrations/0001_initial.py | 92 + .../0002_shangjiamoban_shangjialianjie.py | 56 + .../0003_tupianpeizhi_fangwen_cishu.py | 18 + peizhi/migrations/0004_tixianquotadefault.py | 28 + ...ordertypemapping_dailydispatchstat_club.py | 76 + .../0006_clubconfig_accountpermission.py | 52 + ...ilydispatchstat_claimed_amount_and_more.py | 28 + .../0008_clubconfig_storage_bucket_name.py | 18 + .../0009_dailypayoutstat_dailyincomestat.py | 53 + ...ge_popupimage_popupconfig_page_and_more.py | 84 + peizhi/migrations/0011_withdrawconfig.py | 27 + ...0012_shangjiamoban_chenghao_id_and_more.py | 28 + ...jialianjie_previous_dingdan_id_and_more.py | 23 + peizhi/migrations/__init__.py | 0 peizhi/models.py | 598 + peizhi/serializers.py | 32 + peizhi/tasks.py | 86 + peizhi/tests.py | 3 + peizhi/urls.py | 40 + peizhi/views.py | 5563 ++++++ requirements.txt | 43 + shangdian/__init__.py | 0 shangdian/admin.py | 3 + shangdian/apps.py | 6 + shangdian/migrations/0001_initial.py | 92 + .../0002_alter_yonghupingzheng_yonghu.py | 18 + ...eizhi_dianpu_kaiqi_meiri_xiane_and_more.py | 85 + .../0004_dianpushangpinshenheshezhi.py | 27 + ...dianpu_fenhong_choucheng_feilv_and_more.py | 33 + ...imeiritongji_gei_shangji_cishu_and_more.py | 38 + shangdian/migrations/__init__.py | 0 shangdian/models.py | 227 + shangdian/tests.py | 3 + shangdian/urls.py | 41 + shangdian/utils.py | 640 + shangdian/views.py | 3430 ++++ shangpin/__init__.py | 0 shangpin/admin.py | 3 + shangpin/apps.py | 5 + shangpin/migrations/0001_initial.py | 149 + ..._huiyuangoumai_unique_together_and_more.py | 29 + ...in_paixu_shangpinleixing_paixu_and_more.py | 33 + .../0004_shangpin_shenhezhuangtai_and_more.py | 23 + .../0005_alter_gsfenhong_dashouid.py | 18 + ...iyuan_id_huiyuan_zuzhangfc_duocifenhong.py | 45 + ...duoci_fenho_guanshi_b3de53_idx_and_more.py | 47 + ...ng_zuzhang_fenhong_gsfenhong_zuzhang_id.py | 23 + ..._shangpin_ewai_dashou_fencheng_and_more.py | 23 + .../0010_shangpinleixing_is_cross_enabled.py | 18 + ...dianpu_shangpin_dianpu_leixing_and_more.py | 60 + .../migrations/0012_shangpinmeiritongji.py | 42 + .../0013_shangpinleixing_bankuai.py | 20 + shangpin/migrations/0014_huiyuan_bankuai.py | 20 + .../0015_gsfenhong_fenhong_leixing.py | 18 + shangpin/migrations/__init__.py | 0 shangpin/models.py | 364 + shangpin/tests.py | 3 + shangpin/urls.py | 74 + shangpin/utils.py | 113 + shangpin/views.py | 8453 ++++++++ utils/__init__.py | 0 utils/admin.py | 3 + utils/apps.py | 5 + utils/celery_utils.py | 114 + utils/chat_utils.py | 374 + utils/exception_handler.py | 58 + utils/fadan_utils.py | 142 + utils/goeasy_service.py | 103 + utils/migrations/__init__.py | 0 utils/models.py | 3 + utils/oss_utils.py | 511 + utils/redis_lock.py | 18 + utils/tests.py | 3 + utils/views.py | 3 + utils/wechat_v3.py | 207 + utils/weixin_broadcast.py | 197 + utils/yaoqingma_utils.py | 89 + yonghu/__init__.py | 0 yonghu/admin.py | 3 + yonghu/apps.py | 5 + yonghu/fadan_fenhong_utils.py | 168 + yonghu/migrations/0001_initial.py | 213 + .../migrations/0002_adminprofile_yaoqingma.py | 18 + yonghu/migrations/0003_rankingrecord.py | 35 + .../migrations/0004_alter_usermain_avatar.py | 18 + ...05_officialaccountuser_usermain_unionid.py | 35 + .../0006_alter_officialaccountuser_unionid.py | 18 + yonghu/migrations/0007_userkefu.py | 36 + ...0008_userguanshi_fenghong_erci_and_more.py | 28 + ...ne_userdashou_jinritixian_jine_and_more.py | 89 + .../0010_userguanshi_invite_qrcode_url.py | 18 + .../0011_usermain_shoujihao_renzheng.py | 18 + yonghu/migrations/0012_usershenheguan.py | 36 + .../0013_usershenheguan_is_renzheng.py | 18 + ...ianautorecord_tixianshenhejilu_and_more.py | 113 + yonghu/migrations/__init__.py | 0 yonghu/models.py | 600 + yonghu/paihang_views.py | 217 + yonghu/ranking_tasks.py | 280 + yonghu/tasks.py | 176 + yonghu/tests.py | 3 + yonghu/tixian_shenhe_services.py | 1358 ++ yonghu/tixian_shenhe_views.py | 383 + yonghu/urls.py | 166 + yonghu/views.py | 16051 ++++++++++++++++ 197 files changed, 74825 insertions(+) create mode 100644 .gitignore create mode 100644 _init_.py create mode 100644 a_long_dianjing/__init__.py create mode 100644 a_long_dianjing/asgi.py create mode 100644 a_long_dianjing/celery.py create mode 100644 a_long_dianjing/celery_app.py create mode 100644 a_long_dianjing/settings.py create mode 100644 a_long_dianjing/urls.py create mode 100644 a_long_dianjing/wsgi.py create mode 100644 celerybeat-schedule create mode 100644 dengji/__init__.py create mode 100644 dengji/admin.py create mode 100644 dengji/apps.py create mode 100644 dengji/migrations/0001_initial.py create mode 100644 dengji/migrations/0002_alter_dashoubiaoxian_dingdan.py create mode 100644 dengji/migrations/0003_alter_dashoubiaoxian_bankuai.py create mode 100644 dengji/migrations/0004_dingdanbiaoqian.py create mode 100644 dengji/migrations/0005_fix_state.py create mode 100644 dengji/migrations/0006_shenhejilu.py create mode 100644 dengji/migrations/0007_kaoheguanbankuai.py create mode 100644 dengji/migrations/0008_chenghao_kaohe_guize_shenhejilu_dashou_beizhu_and_more.py create mode 100644 dengji/migrations/0009_chenghao_kaioi_jinpai_alter_shenhejilu_jilu_id_and_more.py create mode 100644 dengji/migrations/0010_kaohejilufeiyong.py create mode 100644 dengji/migrations/0011_kaohepaytemp.py create mode 100644 dengji/migrations/__init__.py create mode 100644 dengji/models.py create mode 100644 dengji/services.py create mode 100644 dengji/tests.py create mode 100644 dengji/urls.py create mode 100644 dengji/utils.py create mode 100644 dengji/views.py create mode 100644 dingdan/__init__.py create mode 100644 dingdan/admin.py create mode 100644 dingdan/apps.py create mode 100644 dingdan/cross_platform_tasks.py create mode 100644 dingdan/migrations/0001_initial.py create mode 100644 dingdan/migrations/0002_dingdan_auto_expire_at_dingdan_auto_task_id.py create mode 100644 dingdan/migrations/0003_dingdan_pending_dispatch_dingdan_status_8_time.py create mode 100644 dingdan/migrations/0004_alter_tuikuanjilu_chuliid.py create mode 100644 dingdan/migrations/0005_alter_chufajilu_chuliid.py create mode 100644 dingdan/migrations/0006_chufatupian.py create mode 100644 dingdan/migrations/0007_chufajilu_ssliyou.py create mode 100644 dingdan/migrations/0008_dingdan_is_cross_dingdan_partner_order_id.py create mode 100644 dingdan/migrations/0009_dingdanpingtai_clkf_dingdanpingtai_jjjs_biaoshi.py create mode 100644 dingdan/migrations/0010_remove_dingdanpingtai_clkf_dingdan_clkf.py create mode 100644 dingdan/migrations/0011_dingdan_tkly.py create mode 100644 dingdan/migrations/0012_dingdan_wechat_transaction_id_and_more.py create mode 100644 dingdan/migrations/0013_alter_chufajilu_qingqiuid.py create mode 100644 dingdan/migrations/0014_dingdan_dispatch_type_dingdan_partner_club_id.py create mode 100644 dingdan/migrations/0015_presettlement_orderdashouhistory_and_more.py create mode 100644 dingdan/migrations/0016_alter_presettlement_dingdan_id_and_more.py create mode 100644 dingdan/migrations/0017_dingdanpingtai_dianpu_id_and_more.py create mode 100644 dingdan/migrations/0018_dingdanshangjia_partner_yonghu_id.py create mode 100644 dingdan/migrations/0019_dingdanpingtai_shangji_dianpu_id_and_more.py create mode 100644 dingdan/migrations/0020_fadanfenhong_fadan.py create mode 100644 dingdan/migrations/0021_fadanshensutupian.py create mode 100644 dingdan/migrations/0022_fadan_partner_club_id_fadan_shenqingren_shenfen.py create mode 100644 dingdan/migrations/0023_fadanshensutupian_yongtu.py create mode 100644 dingdan/migrations/0024_fadan_chulizhe_fadan_chulizhe_shenfen.py create mode 100644 dingdan/migrations/0025_alter_fadan_shenqingren_shenfen.py create mode 100644 dingdan/migrations/0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more.py create mode 100644 dingdan/migrations/0027_dingdanpeihu.py create mode 100644 dingdan/migrations/0028_dingdanshangjia_shangyige_dingdan_id.py create mode 100644 dingdan/migrations/0029_fadanfenhong_is_individual_rate.py create mode 100644 dingdan/migrations/0030_fadan_shenqingren_fenhong_jine.py create mode 100644 dingdan/migrations/0031_dingdan_guanshi_fencheng.py create mode 100644 dingdan/migrations/__init__.py create mode 100644 dingdan/models.py create mode 100644 dingdan/signals.py create mode 100644 dingdan/tasks.py create mode 100644 dingdan/tests.py create mode 100644 dingdan/tongzhi_tasks.py create mode 100644 dingdan/urls.py create mode 100644 dingdan/utils.py create mode 100644 dingdan/views.py create mode 100644 gunicorn.conf.py create mode 100644 houtai/_init_.py create mode 100644 houtai/apps.py create mode 100644 houtai/migrations/0001_initial.py create mode 100644 houtai/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py create mode 100644 houtai/migrations/0003_tixianritongji.py create mode 100644 houtai/migrations/__init__.py create mode 100644 houtai/migrations/_init_.py create mode 100644 houtai/models.py create mode 100644 houtai/serializers.py create mode 100644 houtai/urls.py create mode 100644 houtai/utils.py create mode 100644 houtai/view.py create mode 100644 manage.py create mode 100644 peizhi/__init__.py create mode 100644 peizhi/admin.py create mode 100644 peizhi/apps.py create mode 100644 peizhi/migrations/0001_initial.py create mode 100644 peizhi/migrations/0002_shangjiamoban_shangjialianjie.py create mode 100644 peizhi/migrations/0003_tupianpeizhi_fangwen_cishu.py create mode 100644 peizhi/migrations/0004_tixianquotadefault.py create mode 100644 peizhi/migrations/0005_ordertypemapping_dailydispatchstat_club.py create mode 100644 peizhi/migrations/0006_clubconfig_accountpermission.py create mode 100644 peizhi/migrations/0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more.py create mode 100644 peizhi/migrations/0008_clubconfig_storage_bucket_name.py create mode 100644 peizhi/migrations/0009_dailypayoutstat_dailyincomestat.py create mode 100644 peizhi/migrations/0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more.py create mode 100644 peizhi/migrations/0011_withdrawconfig.py create mode 100644 peizhi/migrations/0012_shangjiamoban_chenghao_id_and_more.py create mode 100644 peizhi/migrations/0013_shangjialianjie_previous_dingdan_id_and_more.py create mode 100644 peizhi/migrations/__init__.py create mode 100644 peizhi/models.py create mode 100644 peizhi/serializers.py create mode 100644 peizhi/tasks.py create mode 100644 peizhi/tests.py create mode 100644 peizhi/urls.py create mode 100644 peizhi/views.py create mode 100644 requirements.txt create mode 100644 shangdian/__init__.py create mode 100644 shangdian/admin.py create mode 100644 shangdian/apps.py create mode 100644 shangdian/migrations/0001_initial.py create mode 100644 shangdian/migrations/0002_alter_yonghupingzheng_yonghu.py create mode 100644 shangdian/migrations/0003_dianpumorenpeizhi_dianpu_kaiqi_meiri_xiane_and_more.py create mode 100644 shangdian/migrations/0004_dianpushangpinshenheshezhi.py create mode 100644 shangdian/migrations/0005_dianpu_fenhong_choucheng_feilv_and_more.py create mode 100644 shangdian/migrations/0006_dianpushouzhimeiritongji_gei_shangji_cishu_and_more.py create mode 100644 shangdian/migrations/__init__.py create mode 100644 shangdian/models.py create mode 100644 shangdian/tests.py create mode 100644 shangdian/urls.py create mode 100644 shangdian/utils.py create mode 100644 shangdian/views.py create mode 100644 shangpin/__init__.py create mode 100644 shangpin/admin.py create mode 100644 shangpin/apps.py create mode 100644 shangpin/migrations/0001_initial.py create mode 100644 shangpin/migrations/0002_alter_huiyuangoumai_unique_together_and_more.py create mode 100644 shangpin/migrations/0003_shangpin_paixu_shangpinleixing_paixu_and_more.py create mode 100644 shangpin/migrations/0004_shangpin_shenhezhuangtai_and_more.py create mode 100644 shangpin/migrations/0005_alter_gsfenhong_dashouid.py create mode 100644 shangpin/migrations/0006_gsfenhong_huiyuan_id_huiyuan_zuzhangfc_duocifenhong.py create mode 100644 shangpin/migrations/0007_remove_duocifenhong_duoci_fenho_guanshi_b3de53_idx_and_more.py create mode 100644 shangpin/migrations/0008_gsfenhong_zuzhang_fenhong_gsfenhong_zuzhang_id.py create mode 100644 shangpin/migrations/0009_shangpin_ewai_dashou_fencheng_and_more.py create mode 100644 shangpin/migrations/0010_shangpinleixing_is_cross_enabled.py create mode 100644 shangpin/migrations/0011_shangpin_dianpu_shangpin_dianpu_leixing_and_more.py create mode 100644 shangpin/migrations/0012_shangpinmeiritongji.py create mode 100644 shangpin/migrations/0013_shangpinleixing_bankuai.py create mode 100644 shangpin/migrations/0014_huiyuan_bankuai.py create mode 100644 shangpin/migrations/0015_gsfenhong_fenhong_leixing.py create mode 100644 shangpin/migrations/__init__.py create mode 100644 shangpin/models.py create mode 100644 shangpin/tests.py create mode 100644 shangpin/urls.py create mode 100644 shangpin/utils.py create mode 100644 shangpin/views.py create mode 100644 utils/__init__.py create mode 100644 utils/admin.py create mode 100644 utils/apps.py create mode 100644 utils/celery_utils.py create mode 100644 utils/chat_utils.py create mode 100644 utils/exception_handler.py create mode 100644 utils/fadan_utils.py create mode 100644 utils/goeasy_service.py create mode 100644 utils/migrations/__init__.py create mode 100644 utils/models.py create mode 100644 utils/oss_utils.py create mode 100644 utils/redis_lock.py create mode 100644 utils/tests.py create mode 100644 utils/views.py create mode 100644 utils/wechat_v3.py create mode 100644 utils/weixin_broadcast.py create mode 100644 utils/yaoqingma_utils.py create mode 100644 yonghu/__init__.py create mode 100644 yonghu/admin.py create mode 100644 yonghu/apps.py create mode 100644 yonghu/fadan_fenhong_utils.py create mode 100644 yonghu/migrations/0001_initial.py create mode 100644 yonghu/migrations/0002_adminprofile_yaoqingma.py create mode 100644 yonghu/migrations/0003_rankingrecord.py create mode 100644 yonghu/migrations/0004_alter_usermain_avatar.py create mode 100644 yonghu/migrations/0005_officialaccountuser_usermain_unionid.py create mode 100644 yonghu/migrations/0006_alter_officialaccountuser_unionid.py create mode 100644 yonghu/migrations/0007_userkefu.py create mode 100644 yonghu/migrations/0008_userguanshi_fenghong_erci_and_more.py create mode 100644 yonghu/migrations/0009_userdashou_ewai_xiane_userdashou_jinritixian_jine_and_more.py create mode 100644 yonghu/migrations/0010_userguanshi_invite_qrcode_url.py create mode 100644 yonghu/migrations/0011_usermain_shoujihao_renzheng.py create mode 100644 yonghu/migrations/0012_usershenheguan.py create mode 100644 yonghu/migrations/0013_usershenheguan_is_renzheng.py create mode 100644 yonghu/migrations/0014_tixianautorecord_tixianshenhejilu_and_more.py create mode 100644 yonghu/migrations/__init__.py create mode 100644 yonghu/models.py create mode 100644 yonghu/paihang_views.py create mode 100644 yonghu/ranking_tasks.py create mode 100644 yonghu/tasks.py create mode 100644 yonghu/tests.py create mode 100644 yonghu/tixian_shenhe_services.py create mode 100644 yonghu/tixian_shenhe_views.py create mode 100644 yonghu/urls.py create mode 100644 yonghu/views.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30eb466 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +__pycache__/ +*.pyc +db.sqlite3 +/venv/ +/env/ +.env +staticfiles/ +media/ +*.log +.DS_Store +migrations/00*.py diff --git a/_init_.py b/_init_.py new file mode 100644 index 0000000..a0b11ba --- /dev/null +++ b/_init_.py @@ -0,0 +1,2 @@ +# 3. 在项目的 `__init__.py` 文件(与manage.py同级)中,添加两行代码 +# 文件路径:a_long_dianjing/__init__.py diff --git a/a_long_dianjing/__init__.py b/a_long_dianjing/__init__.py new file mode 100644 index 0000000..4743afc --- /dev/null +++ b/a_long_dianjing/__init__.py @@ -0,0 +1,8 @@ +""" +阿龙电竞 - Django项目初始化 +确保Celery在Django启动时自动加载 +""" + +from .celery import app as celery_app + +__all__ = ('celery_app',) \ No newline at end of file diff --git a/a_long_dianjing/asgi.py b/a_long_dianjing/asgi.py new file mode 100644 index 0000000..58db31d --- /dev/null +++ b/a_long_dianjing/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for a_long_dianjing project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'a_long_dianjing.settings') + +application = get_asgi_application() diff --git a/a_long_dianjing/celery.py b/a_long_dianjing/celery.py new file mode 100644 index 0000000..7927a29 --- /dev/null +++ b/a_long_dianjing/celery.py @@ -0,0 +1,200 @@ +""" +阿龙电竞 - Celery定时任务主配置 +生产环境就绪版本,支持精确延时任务和周期性任务 +""" + + + + + +import os +from celery import Celery +from celery.schedules import crontab +# 在 Django Shell 中执行 + + +# 设置Django默认设置模块 +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'a_long_dianjing.settings') + +# 创建Celery应用实例 +app = Celery('a_long_dianjing') + +# 从Django settings中加载Celery配置(CELERY_前缀) +app.config_from_object('django.conf:settings', namespace='CELERY') + +# 自动发现所有已注册app中的tasks.py文件 +#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', + 'schedule': crontab(hour=23, minute=50, day_of_month='28-31'), # ✅ 修改这里 + 'options': {'queue': 'periodic_tasks', 'priority': 7}, + }, + + + + # 日榜数据转移(每天23:55执行,在清零前) + 'ribang_zhuanyi': { + 'task': 'yonghu.ranking_tasks.zhuanyi_ribang', + #'schedule': crontab(minute='*/1'), # 每分钟执行一次,测试用 + 'schedule': crontab(hour=23, minute=55), # 每天23:55 + 'options': {'queue': 'periodic_tasks', 'priority': 7}, # 优先级高于清零任务 + }, + + + + # 清理旧历史数据(每月1号凌晨1点执行) + 'qingli_jiulishuju': { + 'task': 'yonghu.ranking_tasks.qingli_jiulishuju', + 'schedule': crontab(hour=1, minute=0, day_of_month=1), # 每月1号凌晨1点 + 'options': {'queue': 'periodic_tasks', 'priority': 4}, + }, + + + + + + + + + # 🔥 补偿检查任务(每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', + 'schedule': crontab(hour=0, minute=0), # 每天0点 + 'options': { + 'queue': 'periodic_tasks', + 'priority': 5 + }, + 'args': (), + 'kwargs': {} + }, + + # 2. 每月1日凌晨0点执行 - 月度清零任务 + 'monthly_reset_task': { + 'task': 'yonghu.tasks.monthly_reset_task', + 'schedule': crontab(hour=0, minute=0, day_of_month=1), # 每月1日0点 + 'options': { + 'queue': 'periodic_tasks', + 'priority': 5 + }, + 'args': (), + 'kwargs': {} + }, + + + + + + # 4. 每天凌晨0点5分清理收支记录 + 'daily_sz_reset_task': { + 'task': 'peizhi.tasks.daily_sz_reset_task', + 'schedule': crontab(hour=0, minute=5), # 每天0点5分 + 'options': { + 'queue': 'periodic_tasks', + 'priority': 4 + }, + 'args': (), + 'kwargs': {} + } +} + + +# 配置周期性任务(Celery Beat Schedule) +'''app.conf.beat_schedule = { + # 1. 每日凌晨0点执行 - 清零任务 + 'daily_reset_task': { + 'task': 'yonghu.tasks.daily_reset_task', + 'schedule': crontab(hour=0, minute=0), # 每天0点 + 'options': { + 'queue': 'periodic_tasks', + 'priority': 5 + }, + 'args': (), + 'kwargs': {} + }, + + # 2. 每月1日凌晨0点执行 - 月度清零任务 + 'monthly_reset_task': { + 'task': 'yonghu.tasks.monthly_reset_task', + 'schedule': crontab(hour=0, minute=0, day_of_month=1), # 每月1日0点 + 'options': { + 'queue': 'periodic_tasks', + 'priority': 5 + }, + 'args': (), + 'kwargs': {} + }, + + # 3. 每5分钟检查一次待处理订单 - 作为补偿机制 + 'check_order_expire_task': { + 'task': 'dingdan.tasks.check_order_expire_task', + 'schedule': crontab(minute='*/5'), # 每5分钟 + 'options': { + 'queue': 'order_tasks', + 'priority': 3 + }, + 'args': (), + 'kwargs': {} + }, + + # 4. 每天凌晨0点5分清理收支记录 + 'daily_sz_reset_task': { + 'task': 'peizhi.tasks.daily_sz_reset_task', + 'schedule': crontab(hour=0, minute=5), # 每天0点5分 + 'options': { + 'queue': 'periodic_tasks', + 'priority': 4 + }, + 'args': (), + 'kwargs': {} + } +}''' + +# 时区设置 +app.conf.timezone = 'Asia/Shanghai' +app.conf.enable_utc = True + +# 队列路由配置 +app.conf.task_routes = { + 'dingdan.tasks.*': {'queue': 'order_tasks'}, + 'yonghu.tasks.*': {'queue': 'periodic_tasks'}, + 'peizhi.tasks.*': {'queue': 'periodic_tasks'}, +} + +# 任务序列化 +app.conf.accept_content = ['json'] +app.conf.task_serializer = 'json' +app.conf.result_serializer = 'json' + +# 任务超时设置 +app.conf.task_time_limit = 300 # 任务最大执行时间300秒 +app.conf.task_soft_time_limit = 240 # 软超时240秒 + +# Worker并发设置 +app.conf.worker_concurrency = 4 +app.conf.worker_prefetch_multiplier = 1 + +# 任务确认设置 +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 diff --git a/a_long_dianjing/celery_app.py b/a_long_dianjing/celery_app.py new file mode 100644 index 0000000..10629c2 --- /dev/null +++ b/a_long_dianjing/celery_app.py @@ -0,0 +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 diff --git a/a_long_dianjing/settings.py b/a_long_dianjing/settings.py new file mode 100644 index 0000000..149e788 --- /dev/null +++ b/a_long_dianjing/settings.py @@ -0,0 +1,541 @@ +""" +Django settings for a_long_dianjing project. +开发环境配置 - 参照旧项目结构精简 +""" +from pathlib import Path +from datetime import timedelta +import os + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +SECRET_KEY = 'django-insecure-9+xu)m%#!#qblgby5sq!sjyg5nj9lir78+&*f$28$459#yktej' # 开发环境可暂用,生产必须改 +DEBUG = False # 开发环境为 True +ALLOWED_HOSTS = ['localhost', '127.0.0.1','www.abas.asia'] # 根据开发前端地址可添加 + +# Application definition +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # 第三方应用 (根据 requirements.txt 已安装的) + 'rest_framework', + 'rest_framework_simplejwt', + 'corsheaders', + 'django_celery_results', + # 你的自定义应用 (按已创建的拼音命名) + 'celery', + 'yonghu', + 'dingdan', + 'shangpin', + 'peizhi', + 'houtai', + # 'games', # 【待确认】你提到 games 还没创建,暂不添加 + 'utils', + 'shangdian', + 'dengji', +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', # 跨域中间件放最前 + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', # 默认开启,如API无需可注释 + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + # 'guanapi.middleware.OpLogMiddleware', # 【待确认】这是旧项目的自定义中间件,新项目需要吗? +] + +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' + +# Database +# 根据你提供的信息配置 (本地开发,端口3307) +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'xaio_cheng_xu', # 你创建的数据库名 + 'USER': 'root', # 你的本地MySQL用户名 + #'PASSWORD': 'Dujieduze6363....', # 你的本地MySQL密码 + #'PASSWORD': 'ahgs673g2h63hgawwdqw', + #'HOST': '172.19.0.2', + 'HOST': '172.17.0.15', + 'PASSWORD':'sajksh.sdfGH3YUge.wjkd+', + #'HOST': 'sh-cdb-hmyxpv1m.sql.tencentcdb.com', + 'PORT': '3306', # 注意端口是3307 + #'PORT': '20950', + 'OPTIONS': { + 'charset': 'utf8mb4', + 'init_command': "SET time_zone='+08:00'", # 保持与旧项目一致 + } + } +} + +# Password validation +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', + }, +] + +# Internationalization +LANGUAGE_CODE = 'zh-Hans' # 使用中文 +TIME_ZONE = 'Asia/Shanghai' +USE_I18N = True +USE_TZ = False # 保持与旧项目一致,不使用UTC时区 + +# Static files (CSS, JavaScript, Images) +STATIC_URL = 'static/' +# 开发环境静态文件目录,可参照旧项目添加 MEDIA 配置 +# MEDIA_URL = 'media/' +# MEDIA_ROOT = BASE_DIR / 'media' + +# Default primary key field type +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# ==================== 以下为第三方库及自定义配置 ==================== +# settings.py +# 修改SIMPLE_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', + # 修改这里:使用yonghuid而不是id + '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', +] + +# 必须指定用户模型(即使它不继承AbstractBaseUser) +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', # 匿名用户每分钟100次 + 'user': '1000/minute', # 登录用户每分钟1000次 + }, + # 🔥 关键:使用自定义异常处理器 + 'EXCEPTION_HANDLER': 'utils.exception_handler.custom_exception_handler', + + # 🔥 生产环境只返回JSON,不返回HTML(更安全) + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + ], +} + + + + + + + + +# settings.py +# H5静态页面域名配置 +H5_DOMAIN = 'https://www.abas.asia:666' # 生产环境 +# H5_DOMAIN = 'https://test-h5.yourdomain.com' # 测试环境 + + + + + + + + + +# CORS settings +CORS_ALLOWED_ORIGINS = [ + "http://localhost:8080", # 假设前端开发服务器地址 + "http://127.0.0.1:8080", + "https://www.abas.asia", + #"http://192.168.240.1:5173/", + "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", + + + #"www.abas.asia", +] +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", +] + +# 【待确认】自定义用户模型,你需要在 yonghu/models.py 中创建 class Yonghu +#AUTH_USER_MODEL = 'yonghu.Yonghu' + + + + + + +# OSS配置 +COS_SECRET_ID ='AKIDU6WikZBcz96xesxpVM3XoslS3TjogLDx' +COS_SECRET_KEY ='qcJzVqLdkeKyXwKckjV5SCmncFfrqdu2' +COS_REGION = 'ap-shanghai' +COS_BUCKET = 'xingque999qygwuyq-1404472910' +COS_SCHEME = 'https' # 使用HTTPS +COS_DOMAIN = f'https://{COS_BUCKET}.cos.{COS_REGION}.myqcloud.com' +# settings.py - 在COS配置部分添加 +COS_APPID = '1404472910' # 你的腾讯云APPID(julebu-1361527063中的数字部分) +COS_STS_DURATION = 7200 # STS临时凭证有效期(秒) + +# ==================== 业务相关配置 (值先空着,结构可保留) ==================== + +# 微信小程序配置 (开发环境可先留空或使用测试号) +WEIXIN_APPID = 'wx0e4be86faac4a8d1' # 你的小程序APPID +WEIXIN_SECRET = '3c3673cf052571dea9e3bea88e198434' # 你的小程序SECRET +WEIXIN_MCHID = '1746545364' # 商户号,支付功能需要 +WEIXIN_SHANGHUMIYAO = 'Dujieduze53631383771411017633490' # 商户密钥,支付功能需要 +# 🆕【新增】服务号配置 +WEIXIN_OFFICIAL_APPID = 'wxd400adf1911c534f' # 服务号APPID +WEIXIN_OFFICIAL_SECRET = '19c946cf02ac6ef13f40333fc8029b92' # 服务号SECRET +WEIXIN_OFFICIAL_TOKEN = '78324hjwewehrui3yferhf34' # 服务号Token +WEIXIN_OFFICIAL_ENCODING_AES_KEY = 'NimrG1Fmq2ydplHJrXeg34NqbCJerltAGVx9xJ5WBx2' # 可选 +WEIXIN_TEMPLATE_ID = 'jZ73G_0KbmFK864WVUr0QFNnqdH-ILBp_hUPMiwx4YY' # 模板ID + +# 🆕【新增】模板消息配置 +WEIXIN_TEMPLATE_MAX_PER_MINUTE = 950 # 每分钟最大发送量(留50缓冲) +WEIXIN_TEMPLATE_BATCH_SIZE = 100 # 每批发送数量 + +WEIXIN_CERT_PATH = '/opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.1/tuikuanzhengshu/apiclient_cert.pem' # 证书文件路径 +WEIXIN_KEY_PATH = '/opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.1/tuikuanzhengshu/apiclient_key.pem' # 密钥文件路径 + +WEIXIN_NOTIFY_URL = 'https://www.abas.asia/hqhd/dingdan/pay-notify/' # 支付回调地址,如:http://你的域名/api/pay/callback/ +# settings.py 或相应配置文件中 +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 # 未配置费用时的默认金额 + + +# settings.py +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', +} + +# HashID (用于加密ID,旧项目有,新项目需要吗?) +# HASHID_FIELD_SALT = 'a_long_dianjing_2025_salt' # 【待确认】请设置你的盐值 + + + # settings.py 尾部添加 +# settings.py +# settings.py +# settings.py 中添加配置 +GOEASY_APPKEY = 'BC-32e2991c69bc47e1b538e73aaed37fc1' # 你的AppKey +GOEASY_SECRET = 'A469ABAA3cc34D64' # 从GoEasy控制台获取 +GOEASY_REST_URL = 'https://rest-hz.goeasy.io/v2/im/message' # 正式环境 + + + + + + + + + + + +""" +阿龙电竞 - Django设置文件中的Celery配置部分 +请在您的settings.py中添加以下配置 +""" + +# ==================== Celery配置 ==================== +# Redis作为消息代理和结果后端 +# 生产环境请修改这里的Redis连接信息 +REDIS_HOST = '172.19.0.2' # Redis服务器IP +REDIS_PORT = 6379 +REDIS_PASSWORD = 'Dujieduze5.3' # 如果有密码,替换为你的密码 +REDIS_DB_CELERY = 0 +REDIS_DB_RESULT = 1 + + + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/2', + 'TIMEOUT': 300, + 'OPTIONS': { + # 不需要 CLIENT_CLASS + } + } +} + + + + + +# 构建Redis连接字符串 +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 + + + +# settings.py 末尾添加(在其他 Celery 配置之后) +CELERY_IMPORTS = ('dingdan.tongzhi_tasks',) + + + +# 任务超时设置 +CELERY_TASK_TIME_LIMIT = 300 # 任务最大执行时间300秒 +CELERY_TASK_SOFT_TIME_LIMIT = 240 # 软超时240秒 + +# Worker并发设置 +CELERY_WORKER_CONCURRENCY = 4 # 根据服务器CPU核心数调整 +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', + }, +} + +# ==================== 定时任务时间配置 ==================== +# 所有时间配置都在这里,方便修改 + +# 订单超时时间(秒) - 48小时 +ORDER_EXPIRE_SECONDS = 48000 * 60 * 60 # 172800秒 + +# 每日任务执行时间(24小时制) +DAILY_TASK_HOUR = 0 # 0点 +DAILY_TASK_MINUTE = 30 # 0分 + +# 每月任务执行时间 +MONTHLY_TASK_DAY = 1 # 每月1日 +MONTHLY_TASK_HOUR = 0 # 0点 +MONTHLY_TASK_MINUTE = 0 # 0分 + +# 订单检查频率(分钟) +ORDER_CHECK_INTERVAL = 5 # 每5分钟检查一次 + + + + + + + + + + + + +# ==================== 跨平台订单互通配置 ==================== +# 接口认证令牌(双方约定,用于简单验证) +CROSS_PLATFORM_TOKEN = 'jdjdjshwjqk' + +# 对方平台接口基础URL(需根据实际部署修改) +PARTNER_BASE_URL = 'https://ios808.top/hqhd' + +# 对方平台各接口路径(可根据实际情况调整,但双方需保持一致) +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' # 退款通知 + +# 我方代表对方发单的商家ID(需在数据库中存在,且具有商家扩展表) +AGENT_SHANGJIA_ID = '9106027' + +# 用于验证的会员ID(根据业务约定) +CROSS_PLATFORM_MEMBER_ID = 'V00QQI' + +# 默认游戏类型ID(当对方未传递时使用) +CROSS_PLATFORM_GAME_TYPE_ID = 1 + +# 允许跨平台的商家ID列表(白名单) +CROSS_PLATFORM_WHITELIST = ['5438898'] + + + + + +# 特殊管事ID列表(yonghuid),这些管事的首次分红金额从 UserGuanshi.fenhong_yici 字段获取 +SPECIAL_GUANSHI_IDS = [ + '2295516', + '9106027', + '2244062', + # ... 按需添加 +] + + + + + + + +WECHAT_PAY_V3_CONFIG = { + 'MCHID': '1746545364', # 商户号 + 'APPID': 'wx0e4be86faac4a8d1', # 小程序APPID + 'API_V3_KEY': 'dujie4637rufjd98ijuyrgcvbvbn4789', # APIv3密钥(32字节) + 'PRIVATE_KEY_PATH': '/opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.443/huizhifuzhengshu/apiclient_key.pem', # 商户私钥文件路径 + 'CERT_SERIAL_NO': '13D0CECB42C48DE78FF35A8A72EF4341FE769FE9', # 商户证书序列号 + 'PLATFORM_CERT_DIR': '/opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.443/huizhifuzhengshu/pub_key.pem', # 存放微信平台证书公钥的目录(文件名:序列号.pem) + 'NOTIFY_URL': 'https://www.abas.asia/hqhd/yonghu/tixiancallback', # 回调地址 + 'TRANSFER_SCENE_ID': '1005', # 替换为你的实际场景ID +} + + +CROSS_PLATFORM_SYNC_TOKEN = 'xK9mP2vL8qW5rT7y' \ No newline at end of file diff --git a/a_long_dianjing/urls.py b/a_long_dianjing/urls.py new file mode 100644 index 0000000..c076e01 --- /dev/null +++ b/a_long_dianjing/urls.py @@ -0,0 +1,58 @@ +""" +URL configuration for a_long_dianjing project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/6.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +'''from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +]''' + +# a_long_dianjing/urls.py +""" +项目根路由配置 - 按照生产环境格式,连接所有APP路由 +注意:本项目使用MinIO模拟OSS,文件上传不经过Django MEDIA_URL,所以不需要static(settings.MEDIA_URL)配置 +""" + +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + # Django管理后台 + path('admin/', admin.site.urls), + + # 用户相关路由 + path('yonghu/', include('yonghu.urls')), + + # 订单相关路由 + path('dingdan/', include('dingdan.urls')), + + # 商品/陪玩服务相关路由 + path('shangpin/', include('shangpin.urls')), + + + # 配置相关路由(系统配置) + path('peizhi/', include('peizhi.urls')), + path('houtai/', include('houtai.urls')), + path('shangdian/', include('shangdian.urls')), + path('dengji/', include('dengji.urls')), +] + +# 重要说明: +# 1. 本项目使用MinIO(模拟OSS)作为对象存储,文件上传通过MinIO的S3接口直接处理 +# 2. 文件存储不经过Django的MEDIA_URL,所以不需要配置 static(settings.MEDIA_URL, ...) +# 3. 未来迁移到腾讯云COS/阿里云OSS时,也通过django-storages直接处理,不经过Django静态文件服务 +# 4. 所有文件访问将通过MinIO/COS的CDN域名直接访问,不需要Django路由转发 diff --git a/a_long_dianjing/wsgi.py b/a_long_dianjing/wsgi.py new file mode 100644 index 0000000..8d00672 --- /dev/null +++ b/a_long_dianjing/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for a_long_dianjing project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'a_long_dianjing.settings') + +application = get_wsgi_application() diff --git a/celerybeat-schedule b/celerybeat-schedule new file mode 100644 index 0000000000000000000000000000000000000000..10b1ca64756779bceef2f98aed5f34d8dad3cb6a GIT binary patch literal 61440 zcmeHQ4Qw3672drc?AS@19})w090&X-IXYuWK>GoavB1s>Szm04U zUV?o8u{&U=iOc_b40dL7S?YqF7A|i+XqNwQpILsBe_ovb#!>U}_m7$77xRuMUoan! z9Wl%Oht2Zpv{~NK3vwH5q*n~s>EQCA?Xa_y%dc_$S8@4!55ta-f5E?xkiVaI{yUGr z`CC2Q zuO0yTWgl#>EZdy$G4x7BDJ~aaK57D)a~TY zYk1Oot|`fVfxSkE`oD>!HXe^e_5}{ISpY<1J$QboJ9M5AN==b>=K~y)KIhV;7G1MV zjmC%7sAhyHIV zpA|{RC$yo_F~#_zv6ZN1{>ovQzDxhYl0J`ri6qyaUScRgzHM}neuuC`&XHFcVOecU zkx%S323PJSubq1E=CqO~TF;$O654o78)B%Ck2g*bIiaN{jc_ncKcg>Fi zf>aDZ_!ztro9s9p4b<&%`b-V9ofxQm)!o8Cn~IO!9kr%v8YokJ%Qu99_B7kOepNG2 z8j_P^f;hqqQtKFiBS?jTn&LLOz2G+N1wGyio=khe`~N14%vVE3b}}7b?J|Us_51CO zEYqT=6=Rrwr%1~xyP{_1s10sh$d&QD}v<;tgR&8-*F2SCJgh^gAc#cEdsAxw-i_lLXk9 z78%jfV_H&8kE9Kni~5v)7SBZ`qVZ933S{>}7H30Js$C+u_#PCUqeR^giDnH8GKc1y zHaDRSl2mMDN=--OlN!7DZv9Nli}Q0L@DW4Jz4E_vudH5JBq7H#gAI^`Yne!~g0r?GXXOpTa|gE-8CH9TvkG0aGC8c3w4JNQ zg-6JWJZwkZ8w*Q4E)C-#yp}@;*^cN{?1-=dMs!|1V`zKgHCAzQZjy z2$02KNx;-d z8=^ILmO)LfQ0Wp%r5B$jG-WfPDn_MQ?p!fb?zCQ2YDP;&`RLP-rv;E3eeKLkbGY z#7yLk-xRHY%u;~K3hiXFg1HsY2?OeKv6xtX9qSBgTS1XFI=r~vn}lz`mNTgGkcbggPLr;Tr>Trc);ooN6(FP0) z)E=F-QFnCcZQ(w2ddeMTQ~T>};J>MG;C~qo{4K^mnclH zm!u+{v)xaYaDR*bD-teUH>3hf0)Y3^Hov8-`I^^-|N4W{kydqCl|le*#3tm}5>rQo zApjQIKaGX}HjoTDL@1Zna0npxK^u8|3Pl1AgRo`v9qCXW|7Ho+i;1ovOLXnoDvLA62yL@z?cyI&TgN(QNQXdu&B*kyFw z4jlMAQv>UxaoZco)5;!m*d!E}F`! z+TmEgOx#&%{|~w9eXn{p0t=0BbfpR86UwoEHn3lMx6sH(Z@c#@0>~{U!~QO^06Yx) z&-N_k`r0YwZX(b91Wkif(~41U)}zHeL;w*$1P}oy2xQ0qUi3`^%s0Jh*hSyeU(3EJ z!H0|}Zq4|nn`_564N%`y-u-p%4)p!Hi|Fpj@jo=eo4+j_7Y6bP^-TlJH{G&XXym82 z-Fv<%xy8gc?faxq^Gd3wCzoaMvt?P9-77SmMK!IMZ(38~01Ua-ambZYZj>w_4h2~c zV#rlZhe|$}n@8H|T36nmAV4WsI&!5TvY2Nnx2h4?JO*~+7~?sJ03v`0)Hwp#@qg5V zxhZr;OE{x!{i+-OD7t%-jk;l{a7G{Ut~1Is9m*70Fp!Ik z|4+y|tYwq~l+y|-T^$fa-;^!in&*rn1J`l{HprfAwK66c_|6()VD|8(XR_8gfQmUFph_mhnwQ01-e05CKF05kLeG0YtzR1pWsJ;ruiJ literal 0 HcmV?d00001 diff --git a/dengji/__init__.py b/dengji/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dengji/admin.py b/dengji/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/dengji/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/dengji/apps.py b/dengji/apps.py new file mode 100644 index 0000000..e330120 --- /dev/null +++ b/dengji/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DengjiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'dengji' diff --git a/dengji/migrations/0001_initial.py b/dengji/migrations/0001_initial.py new file mode 100644 index 0000000..d9615ff --- /dev/null +++ b/dengji/migrations/0001_initial.py @@ -0,0 +1,111 @@ +# Generated by Django 4.2.27 on 2026-05-12 02:17 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('dingdan', '0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Bankuai', + fields=[ + ('bankuai_id', models.AutoField(primary_key=True, serialize=False, verbose_name='板块ID')), + ('mingcheng', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='板块名称')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ], + options={ + 'verbose_name': '板块', + 'verbose_name_plural': '板块', + 'db_table': 'bankuai', + }, + ), + migrations.CreateModel( + name='Chenghao', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('mingcheng', models.CharField(max_length=50, unique=True, verbose_name='称号名称')), + ('leixing', models.CharField(choices=[('dashou', '打手'), ('boss', '老板'), ('shangjia', '商家'), ('guanshi', '管事'), ('zuzhang', '组长')], max_length=10, verbose_name='称号类型')), + ('tubiao_url', models.CharField(blank=True, max_length=200, null=True, verbose_name='称号图标相对URL')), + ('texiao_miaoshu', models.TextField(blank=True, null=True, verbose_name='特效描述(JSON)')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('bankuai', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dengji.bankuai', verbose_name='所属板块(空=全板块通用)')), + ], + options={ + 'verbose_name': '称号', + 'verbose_name_plural': '称号', + 'db_table': 'chenghao', + }, + ), + migrations.CreateModel( + name='YonghuChenghao', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('huode_time', models.DateTimeField(auto_now_add=True, verbose_name='获得时间')), + ('chenghao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dengji.chenghao', verbose_name='称号')), + ('yonghu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chenghao_list', to=settings.AUTH_USER_MODEL, verbose_name='用户')), + ], + options={ + 'verbose_name': '用户称号关联', + 'verbose_name_plural': '用户称号关联', + 'db_table': 'yonghu_chenghao', + 'indexes': [models.Index(fields=['yonghu', 'chenghao'], name='yonghu_chen_yonghu__cac0a1_idx')], + 'unique_together': {('yonghu', 'chenghao')}, + }, + ), + migrations.CreateModel( + name='JingyanJilu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')), + ('yonghu_leixing', models.CharField(choices=[('dashou', '打手'), ('boss', '老板'), ('shangjia', '商家'), ('guanshi', '管事'), ('zuzhang', '组长')], max_length=10, verbose_name='用户类型')), + ('sudu_jingyan', models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='上线速度经验')), + ('shili_jingyan', models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='打手实力经验')), + ('taidu_jingyan', models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='服务态度经验')), + ('zonghe_jingyan', models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='综合经验')), + ('dingdan_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='关联订单ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('bankuai', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dengji.bankuai', verbose_name='所属板块')), + ], + options={ + 'verbose_name': '经验变动记录', + 'verbose_name_plural': '经验变动记录', + 'db_table': 'jingyan_jilu', + 'indexes': [models.Index(fields=['yonghu_id', 'bankuai', 'create_time'], name='jingyan_jil_yonghu__786992_idx')], + }, + ), + migrations.CreateModel( + name='DashouBiaoxian', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dashou_id', models.CharField(db_index=True, max_length=32, verbose_name='打手ID')), + ('shangxian_sudu', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='上线速度评价(1-5)')), + ('fuwu_taidu', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='服务态度评价(1-5)')), + ('fanche_cishu', models.PositiveIntegerField(blank=True, null=True, verbose_name='翻车次数')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('jiedan_time', models.DateTimeField(blank=True, null=True, verbose_name='接单时间')), + ('tijiao_time', models.DateTimeField(blank=True, null=True, verbose_name='提交时间')), + ('bankuai', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dengji.bankuai', verbose_name='所属板块')), + ('dingdan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='biaoxian', to='dingdan.dingdan', verbose_name='关联订单')), + ], + options={ + 'verbose_name': '打手订单表现', + 'verbose_name_plural': '打手订单表现', + 'db_table': 'dashou_biaoxian', + 'indexes': [models.Index(fields=['dashou_id', 'bankuai', 'create_time'], name='dashou_biao_dashou__36e1c3_idx')], + }, + ), + migrations.AddIndex( + model_name='chenghao', + index=models.Index(fields=['leixing', 'bankuai'], name='chenghao_leixing_8f4371_idx'), + ), + ] diff --git a/dengji/migrations/0002_alter_dashoubiaoxian_dingdan.py b/dengji/migrations/0002_alter_dashoubiaoxian_dingdan.py new file mode 100644 index 0000000..5b42a74 --- /dev/null +++ b/dengji/migrations/0002_alter_dashoubiaoxian_dingdan.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.27 on 2026-05-12 02:25 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more'), + ('dengji', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='dashoubiaoxian', + name='dingdan', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='biaoxian', to='dingdan.dingdan', to_field='dingdan_id', verbose_name='关联订单'), + ), + ] diff --git a/dengji/migrations/0003_alter_dashoubiaoxian_bankuai.py b/dengji/migrations/0003_alter_dashoubiaoxian_bankuai.py new file mode 100644 index 0000000..ca07ec2 --- /dev/null +++ b/dengji/migrations/0003_alter_dashoubiaoxian_bankuai.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.27 on 2026-05-12 02:32 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dengji', '0002_alter_dashoubiaoxian_dingdan'), + ] + + operations = [ + migrations.AlterField( + model_name='dashoubiaoxian', + name='bankuai', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='dengji.bankuai', verbose_name='所属板块'), + ), + ] diff --git a/dengji/migrations/0004_dingdanbiaoqian.py b/dengji/migrations/0004_dingdanbiaoqian.py new file mode 100644 index 0000000..bddbc36 --- /dev/null +++ b/dengji/migrations/0004_dingdanbiaoqian.py @@ -0,0 +1,26 @@ +# dengji/migrations/0004_dingdanbiaoqian.py +from django.db import migrations + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more'), # 保持你之前的依赖 + ('dengji', '0003_alter_dashoubiaoxian_bankuai'), + ] + + operations = [ + migrations.RunSQL( + sql=""" + CREATE TABLE IF NOT EXISTS dingdan_biaoqian ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + create_time DATETIME(6) NOT NULL, + chenghao_id BIGINT NOT NULL, + dingdan_id VARCHAR(32) NOT NULL, + CONSTRAINT fk_dingdanbiaoqian_chenghao FOREIGN KEY (chenghao_id) REFERENCES chenghao (id), + CONSTRAINT fk_dingdanbiaoqian_dingdan FOREIGN KEY (dingdan_id) REFERENCES dingdan (dingdan_id), + UNIQUE KEY uniq_dingdan_chenghao (dingdan_id, chenghao_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + """, + reverse_sql="DROP TABLE IF EXISTS dingdan_biaoqian;" + ), + ] \ No newline at end of file diff --git a/dengji/migrations/0005_fix_state.py b/dengji/migrations/0005_fix_state.py new file mode 100644 index 0000000..0128710 --- /dev/null +++ b/dengji/migrations/0005_fix_state.py @@ -0,0 +1,36 @@ +# dengji/migrations/0005_fix_state.py +from django.db import migrations, models +import django.db.models.deletion + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more'), # 保持你之前的依赖 + ('dengji', '0004_dingdanbiaoqian'), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=[], # 表已存在,不做任何数据库操作 + state_operations=[ + migrations.CreateModel( + name='DingdanBiaoqian', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('chenghao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dengji.chenghao', verbose_name='标签')), + ('dingdan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dingdan.dingdan', to_field='dingdan_id', verbose_name='订单')), + ], + options={ + 'verbose_name': '订单需求标签', + 'verbose_name_plural': '订单需求标签', + 'db_table': 'dingdan_biaoqian', + 'unique_together': {('dingdan', 'chenghao')}, + 'indexes': [ + models.Index(fields=['dingdan'], name='dingdan_bia_dingdan_4d046b_idx'), + ], + }, + ), + ], + ), + ] \ No newline at end of file diff --git a/dengji/migrations/0006_shenhejilu.py b/dengji/migrations/0006_shenhejilu.py new file mode 100644 index 0000000..a41aa13 --- /dev/null +++ b/dengji/migrations/0006_shenhejilu.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.27 on 2026-05-14 18:42 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dengji', '0005_fix_state'), + ] + + operations = [ + migrations.CreateModel( + name='ShenheJilu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('shenqingren_id', models.CharField(max_length=7, verbose_name='申请人用户ID')), + ('shenheguan_id', models.CharField(max_length=7, null=True, verbose_name='审核官用户ID')), + ('zhuangtai', models.IntegerField(choices=[(0, '审核中'), (1, '通过'), (2, '未通过')], default=0, verbose_name='审核状态')), + ('cishu', models.IntegerField(default=1, verbose_name='第几次审核')), + ('jiaofei_jine', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='缴纳费用')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('bankuai', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dengji.bankuai', verbose_name='审核板块')), + ('chenghao', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dengji.chenghao', verbose_name='审核标签')), + ], + options={ + 'verbose_name': '审核记录', + 'verbose_name_plural': '审核记录', + 'db_table': 'shenhe_jilu', + 'indexes': [models.Index(fields=['shenqingren_id', 'zhuangtai'], name='shenhe_jilu_shenqin_502e9b_idx'), models.Index(fields=['shenheguan_id'], name='shenhe_jilu_shenheg_5c9959_idx')], + }, + ), + ] diff --git a/dengji/migrations/0007_kaoheguanbankuai.py b/dengji/migrations/0007_kaoheguanbankuai.py new file mode 100644 index 0000000..cd43b15 --- /dev/null +++ b/dengji/migrations/0007_kaoheguanbankuai.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.27 on 2026-05-15 00:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dengji', '0006_shenhejilu'), + ] + + operations = [ + migrations.CreateModel( + name='KaoheguanBankuai', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('kaoheguan_id', models.CharField(db_index=True, max_length=7, verbose_name='考核官用户ID')), + ('bankuai', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dengji.bankuai', verbose_name='所属板块')), + ], + options={ + 'verbose_name': '考核官板块关联', + 'verbose_name_plural': '考核官板块关联', + 'db_table': 'kaoheguan_bankuai', + 'indexes': [models.Index(fields=['kaoheguan_id'], name='kaoheguan_b_kaohegu_00f7c5_idx')], + 'unique_together': {('kaoheguan_id', 'bankuai')}, + }, + ), + ] diff --git a/dengji/migrations/0008_chenghao_kaohe_guize_shenhejilu_dashou_beizhu_and_more.py b/dengji/migrations/0008_chenghao_kaohe_guize_shenhejilu_dashou_beizhu_and_more.py new file mode 100644 index 0000000..f69f5fc --- /dev/null +++ b/dengji/migrations/0008_chenghao_kaohe_guize_shenhejilu_dashou_beizhu_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 4.2.27 on 2026-05-15 04:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dengji', '0007_kaoheguanbankuai'), + ] + + operations = [ + migrations.AddField( + model_name='chenghao', + name='kaohe_guize', + field=models.TextField(blank=True, null=True, verbose_name='考核规则'), + ), + migrations.AddField( + model_name='shenhejilu', + name='dashou_beizhu', + field=models.TextField(blank=True, null=True, verbose_name='打手备注'), + ), + migrations.AddField( + model_name='shenhejilu', + name='dashou_youxi_id', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='打手游戏ID'), + ), + migrations.AddField( + model_name='shenhejilu', + name='guize_neirong', + field=models.TextField(blank=True, null=True, verbose_name='规则内容'), + ), + migrations.AddField( + model_name='shenhejilu', + name='jilu_id', + field=models.CharField(db_index=True, max_length=32, null=True, verbose_name='记录ID'), + ), + migrations.AddField( + model_name='shenhejilu', + name='shenheguan_youxi_id', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='审核官游戏ID'), + ), + migrations.AlterField( + model_name='shenhejilu', + name='zhuangtai', + field=models.IntegerField(choices=[(0, '审核中'), (1, '通过'), (2, '未通过'), (3, '待审核')], default=0, verbose_name='审核状态'), + ), + ] diff --git a/dengji/migrations/0009_chenghao_kaioi_jinpai_alter_shenhejilu_jilu_id_and_more.py b/dengji/migrations/0009_chenghao_kaioi_jinpai_alter_shenhejilu_jilu_id_and_more.py new file mode 100644 index 0000000..2c45282 --- /dev/null +++ b/dengji/migrations/0009_chenghao_kaioi_jinpai_alter_shenhejilu_jilu_id_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 4.2.27 on 2026-05-15 13:20 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dengji', '0008_chenghao_kaohe_guize_shenhejilu_dashou_beizhu_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='chenghao', + name='kaioi_jinpai', + field=models.BooleanField(default=False, verbose_name='是否同时开启金牌'), + ), + migrations.AlterField( + model_name='shenhejilu', + name='jilu_id', + field=models.CharField(db_index=True, default='Sh111', max_length=32, unique=True, verbose_name='记录ID'), + preserve_default=False, + ), + migrations.CreateModel( + name='KaoheJujueJilu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cishu', models.IntegerField(verbose_name='对应次数')), + ('shenheguan_id', models.CharField(max_length=7, verbose_name='考核官用户ID')), + ('yuanyin', models.TextField(verbose_name='拒绝原因')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('jilu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dengji.shenhejilu', to_field='jilu_id', verbose_name='审核记录')), + ], + options={ + 'verbose_name': '拒绝原因记录', + 'verbose_name_plural': '拒绝原因记录', + 'db_table': 'kaohe_jujue_jilu', + 'indexes': [models.Index(fields=['jilu_id', 'cishu'], name='kaohe_jujue_jilu_id_1f2b29_idx')], + }, + ), + migrations.CreateModel( + name='KaoheCishuFeiyong', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cishu', models.IntegerField(verbose_name='第几次考核')), + ('feiyong', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='需缴纳金额')), + ('chenghao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dengji.chenghao', verbose_name='标签')), + ], + options={ + 'verbose_name': '考核次数费用', + 'verbose_name_plural': '考核次数费用', + 'db_table': 'kaohe_cishu_feiyong', + 'indexes': [models.Index(fields=['chenghao', 'cishu'], name='kaohe_cishu_chengha_edc272_idx')], + 'unique_together': {('chenghao', 'cishu')}, + }, + ), + ] diff --git a/dengji/migrations/0010_kaohejilufeiyong.py b/dengji/migrations/0010_kaohejilufeiyong.py new file mode 100644 index 0000000..acad590 --- /dev/null +++ b/dengji/migrations/0010_kaohejilufeiyong.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.27 on 2026-05-15 16:10 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dengji', '0009_chenghao_kaioi_jinpai_alter_shenhejilu_jilu_id_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='KaoheJiluFeiyong', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cishu', models.IntegerField(verbose_name='第几次考核')), + ('jine', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='缴纳金额')), + ('jilu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dengji.shenhejilu', to_field='jilu_id', verbose_name='考核记录')), + ], + options={ + 'verbose_name': '考核记录费用', + 'verbose_name_plural': '考核记录费用', + 'db_table': 'kaohe_jilu_feiyong', + 'unique_together': {('jilu', 'cishu')}, + }, + ), + ] diff --git a/dengji/migrations/0011_kaohepaytemp.py b/dengji/migrations/0011_kaohepaytemp.py new file mode 100644 index 0000000..6a24eac --- /dev/null +++ b/dengji/migrations/0011_kaohepaytemp.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.27 on 2026-05-19 13:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dengji', '0010_kaohejilufeiyong'), + ] + + operations = [ + migrations.CreateModel( + name='KaohePayTemp', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dingdan_id', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='订单ID')), + ('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')), + ('tag_id', models.IntegerField(verbose_name='标签ID')), + ('game_nick', models.CharField(max_length=100, verbose_name='游戏昵称')), + ('remark', models.TextField(blank=True, verbose_name='备注')), + ('reapply', models.BooleanField(default=False, verbose_name='是否再次申请')), + ('jilu_id', models.CharField(blank=True, max_length=32, null=True, verbose_name='原记录ID(再次申请时)')), + ('shenheguan_id', models.CharField(blank=True, max_length=7, verbose_name='指定考核官ID')), + ('cishu', models.IntegerField(verbose_name='本次考核次数(预计算)')), + ('fee', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='本次费用(分账单位)')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ], + options={ + 'verbose_name': '考核支付临时表', + 'verbose_name_plural': '考核支付临时表', + 'db_table': 'kaohe_pay_temp', + }, + ), + ] diff --git a/dengji/migrations/__init__.py b/dengji/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dengji/models.py b/dengji/models.py new file mode 100644 index 0000000..de3631a --- /dev/null +++ b/dengji/models.py @@ -0,0 +1,224 @@ +from django.db import models +from django.conf import settings +from dingdan.models import Dingdan + +# ==================== 板块表 ==================== +class Bankuai(models.Model): + bankuai_id = models.AutoField(primary_key=True, verbose_name='板块ID') + mingcheng = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='板块名称') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'bankuai' + verbose_name = '板块' + verbose_name_plural = verbose_name + + def __str__(self): + return self.mingcheng + + +# ==================== 打手订单表现记录 ==================== +class DashouBiaoxian(models.Model): + dingdan = models.ForeignKey('dingdan.Dingdan', to_field='dingdan_id', on_delete=models.CASCADE, related_name='biaoxian', verbose_name='关联订单', db_index=True) + + dashou_id = models.CharField(max_length=32, db_index=True, verbose_name='打手ID') + bankuai = models.ForeignKey(Bankuai, on_delete=models.PROTECT, to_field='bankuai_id', verbose_name='所属板块', db_index=True,null=True) + shangxian_sudu = models.PositiveSmallIntegerField(null=True, blank=True, verbose_name='上线速度评价(1-5)') + fuwu_taidu = models.PositiveSmallIntegerField(null=True, blank=True, verbose_name='服务态度评价(1-5)') + fanche_cishu = models.PositiveIntegerField(null=True, blank=True, verbose_name='翻车次数') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + jiedan_time = models.DateTimeField(null=True, blank=True, verbose_name='接单时间') + tijiao_time = models.DateTimeField(null=True, blank=True, verbose_name='提交时间') + + class Meta: + db_table = 'dashou_biaoxian' + verbose_name = '打手订单表现' + verbose_name_plural = verbose_name + indexes = [models.Index(fields=['dashou_id', 'bankuai', 'create_time'])] + + +# ==================== 经验变动记录 ==================== +class JingyanJilu(models.Model): + yonghu_id = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') + yonghu_leixing = models.CharField(max_length=10, choices=[('dashou','打手'),('boss','老板'),('shangjia','商家'),('guanshi','管事'),('zuzhang','组长')], verbose_name='用户类型') + bankuai = models.ForeignKey(Bankuai, on_delete=models.PROTECT, to_field='bankuai_id', verbose_name='所属板块', db_index=True) + sudu_jingyan = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name='上线速度经验') + shili_jingyan = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name='打手实力经验') + taidu_jingyan = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name='服务态度经验') + zonghe_jingyan = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name='综合经验') + dingdan_id = models.CharField(max_length=32, null=True, blank=True, db_index=True, verbose_name='关联订单ID') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + + class Meta: + db_table = 'jingyan_jilu' + verbose_name = '经验变动记录' + verbose_name_plural = verbose_name + indexes = [models.Index(fields=['yonghu_id', 'bankuai', 'create_time'])] + + +# ==================== 称号主表 ==================== +class Chenghao(models.Model): + mingcheng = models.CharField(max_length=50, unique=True, verbose_name='称号名称') + leixing = models.CharField(max_length=10, choices=[('dashou','打手'),('boss','老板'),('shangjia','商家'),('guanshi','管事'),('zuzhang','组长')], verbose_name='称号类型') + bankuai = models.ForeignKey(Bankuai, on_delete=models.PROTECT, to_field='bankuai_id', null=True, blank=True, verbose_name='所属板块(空=全板块通用)') + tubiao_url = models.CharField(max_length=200, null=True, blank=True, verbose_name='称号图标相对URL') + texiao_miaoshu = models.TextField(null=True, blank=True, verbose_name='特效描述(JSON)') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + kaohe_guize = models.TextField(null=True, blank=True, verbose_name='考核规则') # 新增 + kaioi_jinpai = models.BooleanField(default=False, verbose_name='是否同时开启金牌') + + class Meta: + db_table = 'chenghao' + verbose_name = '称号' + verbose_name_plural = verbose_name + indexes = [models.Index(fields=['leixing', 'bankuai'])] + + +# ==================== 用户 - 称号关联 ==================== +class YonghuChenghao(models.Model): + yonghu = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='chenghao_list', verbose_name='用户', db_index=True) + chenghao = models.ForeignKey(Chenghao, on_delete=models.CASCADE, verbose_name='称号') + huode_time = models.DateTimeField(auto_now_add=True, verbose_name='获得时间') + + class Meta: + db_table = 'yonghu_chenghao' + verbose_name = '用户称号关联' + verbose_name_plural = verbose_name + unique_together = ('yonghu', 'chenghao') + # 修复:只索引本表字段 + indexes = [models.Index(fields=['yonghu', 'chenghao'])] + + + + + + +class DingdanBiaoqian(models.Model): + """订单需求标签关联表""" + dingdan = models.ForeignKey( + Dingdan, + to_field='dingdan_id', # 关联到字符串字段 dingdan_id + on_delete=models.CASCADE, + verbose_name='订单', + db_index=True + ) + chenghao = models.ForeignKey('dengji.Chenghao', on_delete=models.CASCADE, verbose_name='标签') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'dingdan_biaoqian' + verbose_name = '订单需求标签' + verbose_name_plural = verbose_name + unique_together = ('dingdan', 'chenghao') + indexes = [models.Index(fields=['dingdan'])] + + + + +class ShenheJilu(models.Model): + """审核记录表""" + shenqingren_id = models.CharField(max_length=7, verbose_name='申请人用户ID') + shenheguan_id = models.CharField(max_length=7, null = True,verbose_name='审核官用户ID') + zhuangtai = models.IntegerField(choices=[(0, '审核中'), (1, '通过'), (2, '未通过'), (3, '待审核')], default=0, verbose_name='审核状态') + bankuai = models.ForeignKey(Bankuai, on_delete=models.PROTECT, to_field='bankuai_id', null=True, blank=True, verbose_name='审核板块') + chenghao = models.ForeignKey(Chenghao, on_delete=models.PROTECT, null=True, blank=True, verbose_name='审核标签') + cishu = models.IntegerField(default=1, verbose_name='第几次审核') + jiaofei_jine = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='缴纳费用') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + jilu_id = models.CharField(max_length=32, unique=True, db_index=True, verbose_name='记录ID') # 新增 + guize_neirong = models.TextField(null=True, blank=True, verbose_name='规则内容') + shenheguan_youxi_id = models.CharField(max_length=50, null=True, blank=True, verbose_name='审核官游戏ID') + dashou_youxi_id = models.CharField(max_length=50, null=True, blank=True, verbose_name='打手游戏ID') + dashou_beizhu = models.TextField(null=True, blank=True, verbose_name='打手备注') + + class Meta: + db_table = 'shenhe_jilu' + verbose_name = '审核记录' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['shenqingren_id', 'zhuangtai']), + models.Index(fields=['shenheguan_id']), + ] + + + +class KaohePayTemp(models.Model): + """考核支付临时上下文(仅用于支付成功后回调取参,不删除数据)""" + dingdan_id = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='订单ID') + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') + tag_id = models.IntegerField(verbose_name='标签ID') + game_nick = models.CharField(max_length=100, verbose_name='游戏昵称') + remark = models.TextField(blank=True, verbose_name='备注') + reapply = models.BooleanField(default=False, verbose_name='是否再次申请') + jilu_id = models.CharField(max_length=32, null=True, blank=True, verbose_name='原记录ID(再次申请时)') + shenheguan_id = models.CharField(max_length=7, blank=True, verbose_name='指定考核官ID') + cishu = models.IntegerField(verbose_name='本次考核次数(预计算)') + fee = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='本次费用(分账单位)') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'kaohe_pay_temp' + verbose_name = '考核支付临时表' + verbose_name_plural = verbose_name + + + + + +class KaoheCishuFeiyong(models.Model): + """各标签各考核次数所需缴纳金额""" + chenghao = models.ForeignKey(Chenghao, on_delete=models.CASCADE, verbose_name='标签') + cishu = models.IntegerField(verbose_name='第几次考核') + feiyong = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='需缴纳金额') + + class Meta: + db_table = 'kaohe_cishu_feiyong' + verbose_name = '考核次数费用' + verbose_name_plural = verbose_name + unique_together = ('chenghao', 'cishu') + indexes = [models.Index(fields=['chenghao', 'cishu'])] + + +class KaoheJujueJilu(models.Model): + """考核拒绝原因记录""" + jilu = models.ForeignKey(ShenheJilu, on_delete=models.CASCADE, to_field='jilu_id', verbose_name='审核记录') + cishu = models.IntegerField(verbose_name='对应次数') + shenheguan_id = models.CharField(max_length=7, verbose_name='考核官用户ID') + yuanyin = models.TextField(verbose_name='拒绝原因') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'kaohe_jujue_jilu' + verbose_name = '拒绝原因记录' + verbose_name_plural = verbose_name + indexes = [models.Index(fields=['jilu_id', 'cishu'])] + + +class KaoheJiluFeiyong(models.Model): + """每次考核记录实际缴纳的费用明细""" + jilu = models.ForeignKey(ShenheJilu, on_delete=models.CASCADE, to_field='jilu_id', verbose_name='考核记录') + cishu = models.IntegerField(verbose_name='第几次考核') + jine = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='缴纳金额') + + class Meta: + db_table = 'kaohe_jilu_feiyong' + verbose_name = '考核记录费用' + verbose_name_plural = verbose_name + unique_together = ('jilu', 'cishu') + + + +class KaoheguanBankuai(models.Model): + """考核官与板块多对多关联表""" + kaoheguan_id = models.CharField(max_length=7, db_index=True, verbose_name='考核官用户ID') + bankuai = models.ForeignKey(Bankuai, on_delete=models.CASCADE, to_field='bankuai_id', verbose_name='所属板块') + + class Meta: + db_table = 'kaoheguan_bankuai' + verbose_name = '考核官板块关联' + verbose_name_plural = verbose_name + unique_together = ('kaoheguan_id', 'bankuai') + indexes = [models.Index(fields=['kaoheguan_id'])] \ No newline at end of file diff --git a/dengji/services.py b/dengji/services.py new file mode 100644 index 0000000..30ffcca --- /dev/null +++ b/dengji/services.py @@ -0,0 +1,87 @@ +# dengji/services.py +import logging +from django.db import transaction, IntegrityError +from django.utils import timezone +from .models import DashouBiaoxian + +logger = logging.getLogger('dengji') + +def record_dashou_biaoxian(dingdan_id, dashou_id, xingwei, **kwargs): + """ + 统一记录打手订单表现 + :param dingdan_id: 订单ID + :param dashou_id: 打手ID + :param xingwei: 行为类型 1-抢单 2-提交订单 3-结算 + :param kwargs: 可选字段(与模型字段同名): + jiedan_time, tijiao_time, + bankuai, shangxian_sudu, fuwu_taidu, fanche_cishu + 注意:不管是哪种行为,dingdan_id 和 dashou_id 都是必传的, + 如果没传时间且业务需要时间,则自动取当前时间。 + """ + try: + # ----- 抢单:创建记录 ----- + if xingwei == 1: + jiedan_time = kwargs.get('jiedan_time') or timezone.now() + bankuai = kwargs.get('bankuai') # 可为 None + + # 避免重复创建(同一订单只会有一条表现记录) + obj, created = DashouBiaoxian.objects.get_or_create( + dingdan_id=dingdan_id, + dashou_id=dashou_id, + defaults={ + 'bankuai': bankuai, + 'jiedan_time': jiedan_time, + } + ) + if not created: + logger.info(f"表现记录已存在,跳过创建: dingdan={dingdan_id}, dashou={dashou_id}") + else: + logger.info(f"创建抢单表现记录: dingdan={dingdan_id}, dashou={dashou_id}") + + # ----- 提交订单:更新提交时间 ----- + elif xingwei == 2: + tijiao_time = kwargs.get('tijiao_time') or timezone.now() + # 只更新已存在的记录,找不到就直接跳过 + updated = DashouBiaoxian.objects.filter( + dingdan_id=dingdan_id, + dashou_id=dashou_id + ).update(tijiao_time=tijiao_time) + if updated: + logger.info(f"更新提交时间: dingdan={dingdan_id}, dashou={dashou_id}") + else: + logger.info(f"无对应记录,跳过提交时间更新: dingdan={dingdan_id}, dashou={dashou_id}") + + # ----- 结算:更新评价信息 ----- + elif xingwei == 3: + # 构建要更新的字段,只更新 kwargs 中传入的非 None 字段 + update_fields = {} + for field in ['shangxian_sudu', 'fuwu_taidu', 'fanche_cishu']: + val = kwargs.get(field) + if val is not None: + update_fields[field] = val + # 如果还传了板块也可以更新 + bankuai = kwargs.get('bankuai') + if bankuai is not None: + update_fields['bankuai'] = bankuai + + if update_fields: + updated = DashouBiaoxian.objects.filter( + dingdan_id=dingdan_id, + dashou_id=dashou_id + ).update(**update_fields) + if updated: + logger.info(f"更新评价信息: dingdan={dingdan_id}, dashou={dashou_id}, fields={update_fields}") + else: + logger.info(f"无对应记录,跳过评价更新: dingdan={dingdan_id}, dashou={dashou_id}") + else: + logger.info(f"结算时未传入任何评价字段,跳过更新") + + else: + logger.warning(f"未知的行为类型: {xingwei}") + + except IntegrityError as e: + # 唯一约束冲突(get_or_create 理论上已处理,但并发时仍可能撞上) + logger.warning(f"表现记录写入冲突,可能并发创建: {e}") + except Exception as e: + # 无论发生什么错误,都不影响主流程 + logger.error(f"记录打手表现异常: dingdan={dingdan_id}, dashou={dashou_id}, xingwei={xingwei}, error={e}", exc_info=True) \ No newline at end of file diff --git a/dengji/tests.py b/dengji/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/dengji/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/dengji/urls.py b/dengji/urls.py new file mode 100644 index 0000000..aa54ee2 --- /dev/null +++ b/dengji/urls.py @@ -0,0 +1,22 @@ +from django.urls import path +from .views import DashouBiaoqianView , BankuaiBiaoqianView, KaoheguanRegisterView, \ + KaoheguanRefreshView , ShenheDatingModuleView, ShenheDatingDataView, ShenheQiangdanView, ShenheCaozuoView,ShenheXiangqingView, KaoheJinpaiTagsView, KaoheJiluView, KaoheFreeApplyView,\ + KaoheJiluListView + +urlpatterns = [ + path('dsbqhq', DashouBiaoqianView.as_view(), name='打手标签获取'), + # 其他已有路由保持不变 + path('bkqhbq', BankuaiBiaoqianView.as_view(), name='抢单页面获取标签'), + path('khgzc', KaoheguanRegisterView.as_view(), name='kaoheguan_register'), + path('khgsx', KaoheguanRefreshView.as_view(), name='kaoheguan_refresh'), + path('shdthqmk', ShenheDatingModuleView.as_view(), name='审核官获取板块'), + path('hqshsj', ShenheDatingDataView.as_view(), name='审核官获取待审核列表'), + path('shqd', ShenheQiangdanView.as_view(), name='审核官接待审核'), + path('hqkhxq', ShenheXiangqingView.as_view(), name='获取审核内容'), + path('khgxgkh', ShenheCaozuoView.as_view(), name='修改审核状态'), + + path('dskhhq', KaoheJinpaiTagsView.as_view(), name='考核金牌页面标签'), + path('dskhjl', KaoheJiluView.as_view(), name='打手考核记录'), + path('dssqkh', KaoheFreeApplyView.as_view(), name='免费考核申请接口'), + path('khghqkhjl', KaoheJiluListView.as_view(), name='考核官获取考核记录'), +] \ No newline at end of file diff --git a/dengji/utils.py b/dengji/utils.py new file mode 100644 index 0000000..3e0c6b1 --- /dev/null +++ b/dengji/utils.py @@ -0,0 +1,414 @@ +# 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') + + + + + +# 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): + """ + 检查打手是否满足订单的标签需求。 + - 如果订单没有需求标签,直接放行。 + - 如果有,则要求打手至少拥有其中一个标签。 + 返回 (allowed: bool, message: str) + """ + try: + # 1. 获取订单的需求标签ID集合 + required_ids = set( + DingdanBiaoqian.objects.filter(dingdan_id=order_id) + .values_list('chenghao_id', flat=True) + ) + if not required_ids: + return True, '' + + # 2. 找到打手对应的用户记录 + try: + user = UserMain.objects.get(yonghuid=dashou_yonghuid) + except UserMain.DoesNotExist: + return False, '用户信息异常' + + # 3. 获取打手已拥有的标签ID集合(只取打手类型) + owned_ids = set( + YonghuChenghao.objects.filter( + yonghu=user, + chenghao__leixing='dashou' + ).values_list('chenghao_id', flat=True) + ) + + if not owned_ids: + return False, '您未拥有任何打手标签,无法抢此订单' + + # 4. 交集判断 + if required_ids.intersection(owned_ids): + return True, '' + else: + return False, '您的标签不符合此订单要求,无法抢单' + + except Exception as e: + # 标签校验失败不影响主流程,记录日志并保守放行(也可保守拒绝,按需求选择) + import logging + logger = logging.getLogger('dingdan') + logger.error(f"标签校验异常: {e}", exc_info=True) + # 安全起见,如果校验出错,选择拒绝,避免越权 + return False, '订单标签校验异常,请稍后重试' + + + + + + + + +def get_tag_fee(chenghao, cishu): + """ + 获取标签在指定考核次数下的费用。 + - 若有对应次数的配置,直接返回。 + - 若没有该次数,取该标签下已有最大次数的费用(作为后续次数的价格)。 + - 若该标签完全没有费用记录,返回 0.00(免费)。 + """ + fee_obj = KaoheCishuFeiyong.objects.filter(chenghao=chenghao, cishu=cishu).first() + if fee_obj: + return fee_obj.feiyong + + max_obj = KaoheCishuFeiyong.objects.filter(chenghao=chenghao).order_by('-cishu').first() + if max_obj: + return max_obj.feiyong + + return Decimal('0.00') + + +def generate_unique_jilu_id(): + """ + 生成唯一的审核记录 ID,格式:SH + 时间戳(10位) + 随机数(4位) + 循环检查数据库避免重复,最多尝试 10 次。 + """ + for _ in range(10): + timestamp = str(int(time.time() * 1000))[-10:] + random_part = str(random.randint(1000, 9999)) + jilu_id = f'SH{timestamp}{random_part}' + if not ShenheJilu.objects.filter(jilu_id=jilu_id).exists(): + return jilu_id + raise Exception('无法生成唯一记录ID,请稍后重试') + + +def validate_shenheguan(shenheguan_id, bankuai, fee, applicant_user): + """ + 校验指定考核官是否合法 + - 考核官必须存在且未被禁用 + - 必须属于对应板块 + - 必须处于空闲状态 + - 若费用>0,则要求已认证 + 返回 (valid, error_msg) + """ + if not shenheguan_id: + return True, '' + try: + kg = UserShenheguan.objects.select_related('user').get( + user__yonghuid=shenheguan_id, + zhuangtai=1 + ) + except UserShenheguan.DoesNotExist: + return False, '考核官不存在或已被禁用' + + # 板块权限 + if not KaoheguanBankuai.objects.filter(kaoheguan_id=shenheguan_id, bankuai=bankuai).exists(): + return False, '该考核官不属于此板块' + + # 空闲状态 + if kg.shenhe_zhuangtai != 0: + return False, '该考核官当前忙碌,无法指定' + + # 费用>0需要认证 + if float(fee) > 0 and not kg.is_renzheng: + return False, '费用大于0时只能指定已认证的考核官' + + return True, '' + + +def create_shenhe_jilu(user, tag_id, game_nick, remark, reapply, jilu_id=None, shenheguan_id=''): + """ + 创建或更新审核记录。 + - 首次申请:创建新记录,生成唯一 jilu_id。 + - 再次申请:基于原记录(必须状态为未通过)更新次数、累加总费用、更新备注和游戏昵称, + 保留审核官信息,状态重置为待审核。不创建新记录。 + - 每次申请均创建一条费用明细记录(KaoheJiluFeiyong),记录本次缴费。 + - 若原记录有考核官,则将其状态改为审核中。 + - 新增 shenheguan_id 参数:指定考核官ID,仅在原记录无考核官时可传入。 + 返回 (success, message, record) + """ + try: + # 1. 校验标签存在 + try: + chenghao = Chenghao.objects.get(id=tag_id) + except Chenghao.DoesNotExist: + return False, '标签不存在', None + + # 2. 判断用户是否已拥有该标签 + if YonghuChenghao.objects.filter(yonghu=user, chenghao=chenghao).exists(): + return False, '您已拥有该标签,无需重复考核', None + + # 3. 检查是否有进行中的审核记录(待审核、审核中) + has_pending = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + chenghao=chenghao, + zhuangtai__in=[0, 3] + ).exists() + if has_pending: + return False, '您有未完成的考核申请,请等待结果', None + + last_record = None + current_fee = Decimal('0.00') + + # 4. 处理再次申请 + if reapply and jilu_id: + # 查询原记录(未通过状态) + last_record = ShenheJilu.objects.filter( + jilu_id=jilu_id, + shenqingren_id=user.yonghuid, + zhuangtai=2 + ).first() + if not last_record: + return False, '原记录不存在或状态不是未通过', None + + new_cishu = last_record.cishu + 1 + current_fee = get_tag_fee(chenghao, new_cishu) + + # 确定最终状态:有考核官 = 审核中(0),否则待审核(3) + final_kg_id = last_record.shenheguan_id # 保留原来的 + if shenheguan_id and not last_record.shenheguan_id: + final_kg_id = shenheguan_id # 新指定 + + final_zhuangtai = 0 if final_kg_id else 3 # 有考核官就是审核中 + if final_zhuangtai == 3: + final_kg_id = None + + # 原子化更新 + ShenheJilu.objects.filter(pk=last_record.pk).update( + cishu=new_cishu, + jiaofei_jine=F('jiaofei_jine') + current_fee, + dashou_youxi_id=game_nick, + dashou_beizhu=remark, + guize_neirong=chenghao.kaohe_guize or '', + zhuangtai=final_zhuangtai, + shenheguan_id=final_kg_id, + ) + last_record.refresh_from_db() + record = last_record + + # 如果原考核官被替换或清除,释放旧考核官 + if last_record.shenheguan_id != final_kg_id: + if last_record.shenheguan_id: + UserShenheguan.objects.filter( + user__yonghuid=last_record.shenheguan_id + ).update(shenhe_zhuangtai=0) + else: + # 首次申请 + max_cishu = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + chenghao=chenghao, + zhuangtai__in=[1, 2] + ).aggregate(max=Max('cishu'))['max'] or 0 + new_cishu = max_cishu + 1 + current_fee = get_tag_fee(chenghao, new_cishu) + + bankuai = chenghao.bankuai + rule_text = chenghao.kaohe_guize or '' + new_jilu_id = generate_unique_jilu_id() + + initial_zhuangtai = 0 if shenheguan_id else 3 # 指定→审核中,否则待审核 + record = ShenheJilu.objects.create( + jilu_id=new_jilu_id, + shenqingren_id=user.yonghuid, + zhuangtai=initial_zhuangtai, + bankuai=bankuai, + chenghao=chenghao, + cishu=new_cishu, + jiaofei_jine=current_fee, + dashou_youxi_id=game_nick, + dashou_beizhu=remark, + guize_neirong=rule_text, + shenheguan_id=shenheguan_id if shenheguan_id else None, + ) + + # 5. 创建费用明细(防重复) + KaoheJiluFeiyong.objects.get_or_create( + jilu=record, + cishu=record.cishu, + defaults={'jine': current_fee} + ) + + # 6. 更新考核官统计(仅当状态为审核中且有考核官) + if record.zhuangtai == 0 and record.shenheguan_id: + UserShenheguan.objects.filter( + user__yonghuid=record.shenheguan_id + ).update( + shenhe_zongshu=F('shenhe_zongshu') + 1, + shenhe_zhuangtai=1 + ) + + return True, '申请成功', record + + except Exception as e: + logger.error(f"创建审核记录异常: {e}", exc_info=True) + return False, '系统异常', None + + + + + + + + + + + +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}") + + chenghao = Chenghao.objects.get(id=temp_data.tag_id) + logger.info(f"获取标签成功: {chenghao.mingcheng}") + + # 检查是否已拥有标签 + if YonghuChenghao.objects.filter(yonghu=user, chenghao=chenghao).exists(): + logger.warning(f"用户 {user.yonghuid} 已拥有标签 {chenghao.mingcheng}") + return False, '您已拥有该标签,无需重复考核', None + + # 检查是否有进行中的审核 + if ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + chenghao=chenghao, + zhuangtai__in=[0, 3] + ).exists(): + logger.warning(f"用户 {user.yonghuid} 有未完成的考核申请") + return False, '您有未完成的考核申请,请等待结果', None + + # 使用临时表中预计算好的次数和费用 + new_cishu = temp_data.cishu + current_fee = temp_data.fee + reapply = temp_data.reapply + jilu_id = temp_data.jilu_id + shenheguan_id = temp_data.shenheguan_id + + logger.info(f"临时表数据: cishu={new_cishu}, fee={current_fee}, reapply={reapply}, jilu_id={jilu_id}, shenheguan_id={shenheguan_id}") + + # 再次申请校验 + if reapply: + if not jilu_id: + logger.error("再次申请但未提供原记录ID") + return False, '再次申请必须提供原记录ID', None + last = ShenheJilu.objects.filter( + jilu_id=jilu_id, + shenqingren_id=user.yonghuid, + zhuangtai=2 + ).first() + if not last: + logger.error(f"原记录不存在或状态不是未通过: jilu_id={jilu_id}") + return False, '原记录不存在或状态不是未通过', None + if new_cishu != last.cishu + 1: + logger.error(f"考核次数不一致: 临时表={new_cishu}, 原记录次数={last.cishu}") + return False, '考核次数不一致', None + logger.info(f"再次申请校验通过,原记录ID: {jilu_id}") + + # 创建或更新审核记录 + if reapply and jilu_id: + # 更新原记录 + final_kg_id = last.shenheguan_id + if shenheguan_id and not last.shenheguan_id: + final_kg_id = shenheguan_id + final_zhuangtai = 0 if final_kg_id else 3 + if final_zhuangtai == 3: + final_kg_id = None + + ShenheJilu.objects.filter(pk=last.pk).update( + cishu=new_cishu, + jiaofei_jine=F('jiaofei_jine') + current_fee, + dashou_youxi_id=temp_data.game_nick, + dashou_beizhu=temp_data.remark, + guize_neirong=chenghao.kaohe_guize or '', + zhuangtai=final_zhuangtai, + shenheguan_id=final_kg_id, + ) + last.refresh_from_db() + record = last + + # 释放旧考核官 + if last.shenheguan_id != final_kg_id and last.shenheguan_id: + UserShenheguan.objects.filter(user__yonghuid=last.shenheguan_id).update(shenhe_zhuangtai=0) + logger.info(f"释放旧考核官: {last.shenheguan_id}") + + logger.info(f"更新审核记录成功: jilu_id={record.jilu_id}, 新次数={new_cishu}") + else: + # 首次申请 + bankuai = chenghao.bankuai + rule_text = chenghao.kaohe_guize or '' + new_jilu_id = generate_unique_jilu_id() + initial_zhuangtai = 0 if shenheguan_id else 3 + + record = ShenheJilu.objects.create( + jilu_id=new_jilu_id, + shenqingren_id=user.yonghuid, + zhuangtai=initial_zhuangtai, + bankuai=bankuai, + chenghao=chenghao, + cishu=new_cishu, + jiaofei_jine=current_fee, + dashou_youxi_id=temp_data.game_nick, + dashou_beizhu=temp_data.remark, + guize_neirong=rule_text, + shenheguan_id=shenheguan_id if shenheguan_id else None, + ) + logger.info(f"创建审核记录成功: jilu_id={new_jilu_id}, 状态={initial_zhuangtai}") + + # 记录费用明细 + fee_detail, created = KaoheJiluFeiyong.objects.get_or_create( + jilu=record, + cishu=record.cishu, + defaults={'jine': current_fee} + ) + if created: + logger.info(f"创建费用明细成功: 记录ID={record.jilu_id}, 金额={current_fee}") + else: + logger.warning(f"费用明细已存在: 记录ID={record.jilu_id}, 次数={record.cishu}") + + # 更新考核官状态 + if record.zhuangtai == 0 and record.shenheguan_id: + UserShenheguan.objects.filter(user__yonghuid=record.shenheguan_id).update( + shenhe_zongshu=F('shenhe_zongshu') + 1, + shenhe_zhuangtai=1 + ) + logger.info(f"更新考核官状态: {record.shenheguan_id}, 审核总数+1, 状态=忙碌") + + logger.info(f"从临时表创建审核记录完成: jilu_id={record.jilu_id}") + return True, '申请成功', record + + except Chenghao.DoesNotExist: + logger.error(f"标签不存在: tag_id={temp_data.tag_id}") + return False, '标签不存在', None + except Exception as e: + logger.error(f"从临时表创建审核记录异常: {str(e)}", exc_info=True) + return False, '系统异常', None \ No newline at end of file diff --git a/dengji/views.py b/dengji/views.py new file mode 100644 index 0000000..a26b81a --- /dev/null +++ b/dengji/views.py @@ -0,0 +1,975 @@ +# dengji/views.py +import logging +import json +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +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') + + +class DashouBiaoqianView(APIView): + """获取当前打手所有称号标签""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + # 查询用户的所有称号关联,一次连表取出 + relations = YonghuChenghao.objects.filter( + yonghu=yonghu + ).select_related('chenghao') + + chenghao_list = [] + for rel in relations: + ch = rel.chenghao + # 解析特效描述 JSON(前端需要对象) + texiao_json = {} + if ch.texiao_miaoshu: + try: + texiao_json = json.loads(ch.texiao_miaoshu) + except json.JSONDecodeError: + texiao_json = {} + chenghao_list.append({ + 'mingcheng': ch.mingcheng, + 'tubiao_url': ch.tubiao_url or '', + 'texiao_json': texiao_json, + }) + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': {'chenghao_list': chenghao_list} + }) + except Exception as e: + logger.error(f"获取打手标签失败: {e}", exc_info=True) + return Response({'code': 1, 'msg': '服务器错误', 'data': None}) + + + +from shangpin.models import ShangpinLeixing +from .models import YonghuChenghao, Chenghao +class BankuaiBiaoqianView(APIView): + """获取商品类型所属板块的所有标签""" + permission_classes = [IsAuthenticated] + + def post(self, request): + leixing_id = request.data.get('leixing_id') + if not leixing_id: + return Response({'code': 1, 'msg': '缺少leixing_id'}) + + try: + leixing = ShangpinLeixing.objects.select_related('bankuai').get(id=leixing_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 2, 'msg': '商品类型不存在'}) + + bankuai = leixing.bankuai + if not bankuai: + return Response({'code': 0, 'data': {'biaoqian_list': []}}) + + # 获取该板块下所有类型的标签(leixing可以为dashou/shangjia等,这里先返回所有) + chenghao_list = Chenghao.objects.filter(bankuai=bankuai) + data = [] + for ch in chenghao_list: + texiao_json = {} + if ch.texiao_miaoshu: + try: + texiao_json = json.loads(ch.texiao_miaoshu) + except: + pass + data.append({ + 'id': ch.id, + 'mingcheng': ch.mingcheng, + 'texiao_json': texiao_json + }) + return Response({'code': 0, 'data': {'biaoqian_list': data}}) + + + + + +# 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' + +class KaoheguanRegisterView(APIView): + """ + 考核官注册接口 + POST /dengji/khgzc + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + invite_code = request.data.get('inviteCode', '').strip() + + if not invite_code: + return Response({'code': 400, 'msg': '邀请码不能为空'}) + if invite_code != KAOHEGUAN_INVITE_CODE: + return Response({'code': 400, 'msg': '邀请码错误'}) + + kg = UserShenheguan.objects.filter(user=yonghu).first() + if kg: + if kg.zhuangtai == 1: + return Response({'code': 200, 'msg': '您已是考核官', 'data': self._build_data(kg)}) + else: + return Response({'code': 400, 'msg': '账号已被封禁'}) + + with transaction.atomic(): + kg = UserShenheguan.objects.create( + user=yonghu, + zhuangtai=1, + shenhe_zhuangtai=0, + shenhe_zongshu=0, + tongguo_zongshu=0, + yue=0.00, + zonge=0.00 + ) + return Response({'code': 200, 'msg': '注册成功', 'data': self._build_data(kg)}) + + except Exception as e: + logger.error(f"考核官注册失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + def _build_data(self, kg): + bankuai_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=kg.user.yonghuid).select_related('bankuai') + bankuai_list = [{'bankuai_id': b.bankuai.bankuai_id, 'mingcheng': b.bankuai.mingcheng} for b in bankuai_qs] + return { + 'zhuangtai': kg.zhuangtai, + 'shenhe_zongshu': kg.shenhe_zongshu, + 'tongguo_zongshu': kg.tongguo_zongshu, + 'yue': str(kg.yue), + 'zonge': str(kg.zonge), + 'shenhe_zhuangtai': kg.shenhe_zhuangtai, + 'bankuai_list': bankuai_list, + 'kaoheguanstatus': 1 + } + + + + + +class KaoheguanRefreshView(APIView): + """ + 考核官数据刷新接口 + POST /dengji/khgsx + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + kg = UserShenheguan.objects.filter(user=yonghu).first() + if not kg: + return Response({'code': 400, 'msg': '您还未注册考核官'}) + + if kg.zhuangtai != 1: + return Response({'code': 200, 'data': {'zhuangtai': kg.zhuangtai, 'kaoheguanstatus': 0}}) + + # 查询所属板块 + bankuai_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=kg.user.yonghuid).select_related('bankuai') + bankuai_list = [{'bankuai_id': b.bankuai.bankuai_id, 'mingcheng': b.bankuai.mingcheng} for b in bankuai_qs] + + # 统计待审核 / 审核中的记录数量 + pending_count = ShenheJilu.objects.filter( + shenheguan_id=yonghu.yonghuid, + zhuangtai=0 # 审核中 + ).count() + + data = { + 'zhuangtai': kg.zhuangtai, + 'shenhe_zongshu': kg.shenhe_zongshu, + 'tongguo_zongshu': kg.tongguo_zongshu, + 'yue': str(kg.yue), + 'zonge': str(kg.zonge), + 'shenhe_zhuangtai': kg.shenhe_zhuangtai, + 'bankuai_list': bankuai_list, + 'kaoheguanstatus': 1, + 'is_renzheng': kg.is_renzheng, # 🆕 是否认证 + 'pending_shenhe_count': pending_count # 🆕 审核中数量 + } + return Response({'code': 200, 'data': data}) + + except Exception as e: + logger.error(f"考核官刷新失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + +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] + + def post(self, request): + try: + yonghu = request.user + related_ids = set(KaoheguanBankuai.objects.filter(kaoheguan_id=yonghu.yonghuid) + .values_list('bankuai_id', flat=True)) + + bankuai_list = [] + for bk in Bankuai.objects.all(): + count = ShenheJilu.objects.filter(bankuai=bk, zhuangtai=3, shenheguan_id__isnull=True).count() + bankuai_list.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'count': count, + 'is_related': bk.bankuai_id in related_ids + }) + + return Response({'code': 0, 'data': {'modules': bankuai_list}}) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + +class ShenheDatingDataView(APIView): + """获取审核大厅待审核记录列表""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + data = request.data + bankuai_id = data.get('bankuai_id') + page = int(data.get('page', 1)) + page_size = int(data.get('page_size', 5)) + + query = ShenheJilu.objects.filter(zhuangtai=3, shenheguan_id__isnull=True) + if bankuai_id: + query = query.filter(bankuai_id=bankuai_id) + + total = query.count() + offset = (page - 1) * page_size + jilu_list = query.select_related('bankuai', 'chenghao').order_by('-create_time')[ + offset:offset + page_size] + + records = [] + for jl in jilu_list: + user = UserMain.objects.filter(yonghuid=jl.shenqingren_id).first() + avatar = user.avatar if user else '' + texiao_json = {} + if jl.chenghao and jl.chenghao.texiao_miaoshu: + try: + texiao_json = json.loads(jl.chenghao.texiao_miaoshu) + except: + pass + + # 新增字段:本次考核费用 + kaohe_feiyong = float(jl.jiaofei_jine) if jl.jiaofei_jine is not None else 0.0 + + records.append({ + 'jilu_id': jl.jilu_id, + 'shenqingren_id': jl.shenqingren_id, + 'shenqingren_avatar': avatar, + 'dashou_youxi_id': jl.dashou_youxi_id or '', + 'dashou_beizhu': jl.dashou_beizhu or '', + 'bankuai_id': jl.bankuai.bankuai_id if jl.bankuai else None, + 'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '', + 'chenghao_id': jl.chenghao_id, + 'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '', + 'chenghao_texiao_json': texiao_json, + 'guize_neirong': jl.guize_neirong or '', + 'kaohe_feiyong': kaohe_feiyong, # 新增 + 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S') if jl.create_time else '', + }) + + has_more = (page * page_size) < total + return Response({'code': 0, 'data': {'list': records, 'has_more': has_more, 'total': total}}) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + +class ShenheQiangdanView(APIView): + """ + 审核官抢单(接审核任务) + 校验:身份、账号封禁、已有进行中审核、记录状态、不能审核自己、板块权限 + 成功后更新考核官扩展表:审核总数+1,审核状态设为“审核中” + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + jilu_id = request.data.get('jilu_id') + if not jilu_id: + return Response({'code': 400, 'msg': '记录ID不能为空'}) + + # 1. 检查考核官身份及账号是否被禁用 + kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() + if not kg: + return Response({'code': 403, 'msg': '您不是考核官或账号已被禁用'}) + + # 2. 检查是否有正在审核中的记录(zhuangtai=0),防止同时审核多个 + ongoing = ShenheJilu.objects.filter( + shenheguan_id=yonghu.yonghuid, zhuangtai=0 + ).exists() + if ongoing: + return Response({'code': 400, 'msg': '您有正在审核中的任务,请完成后再接新任务'}) + + with transaction.atomic(): + # 3. 行锁抢单:仅当状态为待审核(3)且无审核官时才能抢 + jilu = ShenheJilu.objects.select_for_update().filter( + jilu_id=jilu_id, + zhuangtai=3, + shenheguan_id__isnull=True + ).first() + if not jilu: + return Response({'code': 400, 'msg': '该审核任务已被接走、状态变更或不存在'}) + + # 4. 不能审核自己 + if jilu.shenqingren_id == yonghu.yonghuid: + return Response({'code': 400, 'msg': '不能审核自己提交的申请'}) + + # 5. 板块权限:考核官必须属于该板块 + is_related = KaoheguanBankuai.objects.filter( + kaoheguan_id=yonghu.yonghuid, + bankuai=jilu.bankuai + ).exists() + if not is_related: + return Response({'code': 400, 'msg': '您不属于该板块,无法审核'}) + + # 新增:6. 费用>0时,要求考核官已认证 + if jilu.jiaofei_jine and float(jilu.jiaofei_jine) > 0: + if not kg.is_renzheng: + return Response({'code': 400, 'msg': '本考核需要认证考核官接单,您尚未认证'}) + + # 7. 更新审核记录:分配给当前考核官,状态变为审核中 + jilu.zhuangtai = 0 # 审核中 + jilu.shenheguan_id = yonghu.yonghuid + jilu.save() + + # 8. 更新考核官扩展表:审核总数 +1,审核状态置为审核中(1) + UserShenheguan.objects.filter(user=yonghu).update( + shenhe_zongshu=F('shenhe_zongshu') + 1, + shenhe_zhuangtai=1 + ) + + return Response({ + 'code': 0, + 'msg': '抢单成功,请前往游戏内审核,审核完成后请到考核打分页面进行评定。', + 'data': {'bankuai_id': jilu.bankuai.bankuai_id} + }) + + except Exception as e: + logger.error(f"审核官抢单异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}, status=500) + + + + + +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): + """获取当前考核官审核中的记录详情(新增待收考核费字段)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() + if not kg: + return Response({'code': 403, 'msg': '您不是考核官或账号异常'}) + + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 10)) + + # 查询当前考核官名下状态为“审核中”的记录 + base_query = ShenheJilu.objects.filter( + shenheguan_id=yonghu.yonghuid, + zhuangtai=0 + ).select_related('bankuai', 'chenghao') + total = base_query.count() + offset = (page - 1) * page_size + jilu_list = base_query[offset:offset + page_size] + + records = [] + for jl in jilu_list: + user = UserMain.objects.filter(yonghuid=jl.shenqingren_id).first() + avatar = user.avatar if user else '' + texiao_json = {} + if jl.chenghao and jl.chenghao.texiao_miaoshu: + try: + texiao_json = json.loads(jl.chenghao.texiao_miaoshu) + except: + pass + + # 是否开启金牌 + is_jinpai = jl.chenghao.kaioi_jinpai if jl.chenghao else False + + # 本次待收考核费(基于记录中的当前次数,从费用明细表查询) + daishou_fei = 0.00 + fee_detail = KaoheJiluFeiyong.objects.filter( + jilu=jl, + cishu=jl.cishu + ).first() + if fee_detail: + daishou_fei = float(fee_detail.jine) + else: + # 兜底:若明细表无记录,尝试用通用费用表计算 + daishou_fei = 0.00 + # 上次失败原因 + last_fail_reason = '' + last_fail = KaoheJujueJilu.objects.filter( + jilu=jl, + cishu=jl.cishu - 1 + ).order_by('-create_time').first() + if last_fail: + last_fail_reason = last_fail.yuanyin + + records.append({ + 'jilu_id': jl.jilu_id, + 'shenqingren_id': jl.shenqingren_id, + 'shenqingren_avatar': avatar, + 'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '', + 'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '', + 'chenghao_texiao_json': texiao_json, + 'guize_neirong': jl.guize_neirong or '', + 'dashou_beizhu': jl.dashou_beizhu or '', + 'dashou_youxi_id': jl.dashou_youxi_id or '', + 'shenheguan_youxi_id': jl.shenheguan_youxi_id or '', + 'cishu': jl.cishu, + 'daishou_fei': daishou_fei, # 新增:待收考核费 + 'last_fail_reason': last_fail_reason, + 'is_jinpai': is_jinpai, + 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S') if jl.create_time else '', + }) + + has_more = (page * page_size) < total + return Response({'code': 0, 'data': {'list': records, 'has_more': has_more, 'total': total}}) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + +class ShenheCaozuoView(APIView): + """ + 考核官执行操作(同意/拒绝/转移/修改昵称),并更新考核官收益 + - 只有“同意”或“拒绝”才增加余额和总额,金额严格从 KaoheJiluFeiyong 表获取 + - 查不到费用则加 0 元,绝不影响流程 + - 转移、修改昵称等操作不涉及收益 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + # 1. 验证考核官身份及账号状态 + kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() + if not kg: + return Response({'code': 403, 'msg': '账号异常,无法操作'}) + + jilu_id = request.data.get('jilu_id') + caozuo = request.data.get('caozuo') # agree / reject / transfer / modify_nick + if not jilu_id or not caozuo: + return Response({'code': 400, 'msg': '缺少必要参数'}) + + with transaction.atomic(): + # 2. 锁定审核记录,确保属于当前考核官且状态为审核中(0) + jilu = ShenheJilu.objects.select_for_update().filter( + jilu_id=jilu_id, + shenheguan_id=yonghu.yonghuid, + zhuangtai=0 + ).first() + if not jilu: + return Response({'code': 400, 'msg': '该记录不存在或不属于您'}) + + # 3. 本次操作产生的收入(仅通过/拒绝时有意义) + daishou_fei = 0.00 + + # 4. 根据操作类型执行对应业务逻辑 + if caozuo == 'agree': + # --- 通过考核 --- + # 查询本次考核应得的费用(基于记录ID和当前次数) + fee_detail = KaoheJiluFeiyong.objects.filter( + jilu=jilu, + cishu=jilu.cishu + ).first() + if fee_detail: + daishou_fei = float(fee_detail.jine) + # 找不到就保持 0,绝不使用任何默认值 + + jilu.zhuangtai = 1 + jilu.save() + + # 授予标签 + if jilu.chenghao: + yh = UserMain.objects.get(yonghuid=jilu.shenqingren_id) + if not YonghuChenghao.objects.filter(yonghu=yh, chenghao=jilu.chenghao).exists(): + YonghuChenghao.objects.create(yonghu=yh, chenghao=jilu.chenghao) + # 如果标签开启了金牌,同步更新打手称号 + if jilu.chenghao.kaioi_jinpai: + dashou_prof = UserDashou.objects.filter(user=yh).first() + if dashou_prof: + dashou_prof.chenghao = '金牌选手' + dashou_prof.save() + + elif caozuo == 'reject': + # --- 拒绝考核 --- + yuanyin = request.data.get('yuanyin', '') + if not yuanyin.strip(): + return Response({'code': 400, 'msg': '请填写拒绝原因'}) + + # 查询本次考核应得的费用(拒绝时同样获得收入) + fee_detail = KaoheJiluFeiyong.objects.filter( + jilu=jilu, + cishu=jilu.cishu + ).first() + if fee_detail: + daishou_fei = float(fee_detail.jine) + # 找不到就保持 0 + + jilu.zhuangtai = 2 + jilu.save() + # 记录拒绝原因 + KaoheJujueJilu.objects.create( + jilu=jilu, + cishu=jilu.cishu, + shenheguan_id=yonghu.yonghuid, + yuanyin=yuanyin + ) + + # 新增:不合格时,如果考核官未认证,则清空考核官ID和游戏昵称 + if not kg.is_renzheng: + jilu.shenheguan_id = None + jilu.shenheguan_youxi_id = '' + jilu.save(update_fields=['shenheguan_id', 'shenheguan_youxi_id']) + + elif caozuo == 'transfer': + # 转移考核官:清空考核官信息,状态回退为待审核 + jilu.shenheguan_id = None + jilu.shenheguan_youxi_id = None + jilu.zhuangtai = 3 + jilu.save() + # 转移不更新收益,直接返回 + + + elif caozuo == 'modify_nick': + # 修改考核官游戏昵称 + youxi_id = request.data.get('youxi_id', '').strip() + if not youxi_id: + return Response({'code': 400, 'msg': '请输入游戏昵称'}) + jilu.shenheguan_youxi_id = youxi_id + jilu.save() + return Response({'code': 0, 'msg': '修改成功'}) + + else: + return Response({'code': 400, 'msg': '无效的操作类型'}) + + # 5. 更新考核官收益(仅“通过”或“拒绝”时执行) + if caozuo in ('transfer'): + update_fields = { + + 'shenhe_zhuangtai': 0, # 设为空闲 + } + UserShenheguan.objects.filter(user=yonghu).update(**update_fields) + if caozuo in ('agree', 'reject'): + update_fields = { + 'yue': F('yue') + Decimal(str(daishou_fei)), + 'zonge': F('zonge') + Decimal(str(daishou_fei)), + 'shenhe_zhuangtai': 0, # 设为空闲 + } + # 只有同意时才增加“通过总数” + if caozuo == 'agree': + update_fields['tongguo_zongshu'] = F('tongguo_zongshu') + 1 + + # 审核总数不再增加 + UserShenheguan.objects.filter(user=yonghu).update(**update_fields) + + return Response({'code': 0, 'msg': '操作成功'}) + + except Exception as e: + logger.error(f"考核操作异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}, status=500) + + + + +# dengji/views.py +class KaoheJinpaiTagsView(APIView): + """获取考核金牌页面的标签和板块信息""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + # 获取用户已拥有的标签名称集合 + owned_names = set( + YonghuChenghao.objects.filter(yonghu=user, chenghao__leixing='dashou') + .values_list('chenghao__mingcheng', flat=True) + ) + + # 板块列表 + plate_list = [] + for bk in Bankuai.objects.all(): + tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou') + tag_list = [] + for tag in tags: + # 费用列表 + fee_objs = KaoheCishuFeiyong.objects.filter(chenghao=tag).order_by('cishu') + fee_list = [{'cishu': f.cishu, 'feiyong': float(f.feiyong)} for f in fee_objs] + + # 若没有记录,补充默认费用 + if not fee_list: + fee_list = [{'cishu': 1, 'feiyong': 5.00}] + + tag_list.append({ + 'id': tag.id, + 'name': tag.mingcheng, + 'texiao_json': json.loads(tag.texiao_miaoshu) if tag.texiao_miaoshu else {}, + 'jinpai': tag.kaioi_jinpai, + 'rule': tag.kaohe_guize or '', + 'fee_list': fee_list, + 'owned': tag.mingcheng in owned_names, + }) + + if tag_list: + plate_list.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'tags': tag_list + }) + + # 新增:返回所有在职考核官信息(用于指定考核官) + kaoheguan_list = [] + for kg in UserShenheguan.objects.filter(zhuangtai=1).select_related('user'): + uid = kg.user.yonghuid + avatar = kg.user.avatar or '' + # 获取考核官的打手昵称(从打手扩展表) + dashou_nick = '' + dashou_prof = UserDashou.objects.filter(user=kg.user).first() + if dashou_prof: + dashou_nick = dashou_prof.nicheng or '' + kaoheguan_list.append({ + 'yonghuid': uid, + 'avatar': avatar, + 'dashou_nick': dashou_nick, + 'is_renzheng': kg.is_renzheng, + 'shenhe_zhuangtai': kg.shenhe_zhuangtai, + }) + + return Response({ + 'code': 0, + 'data': { + 'plate_list': plate_list, + 'my_tags': list(owned_names), + 'kaoheguan_list': kaoheguan_list, # 新增 + } + }) + 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] + + def post(self, request): + try: + user = request.user + status = request.data.get('status', 'pending') # pending / passed + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 5)) + + if status == 'pending': + # 进行中/未通过:审核中(0)、待审核(3)、未通过(2) + query = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + zhuangtai__in=[0, 2, 3] + ) + else: + query = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + zhuangtai=1 # 已通过 + ) + + total = query.count() + records = query.order_by('-create_time')[(page-1)*page_size : page*page_size] + + data = [] + for jl in records: + # 汇总缴纳总额 + total_fee = KaoheJiluFeiyong.objects.filter(jilu=jl).aggregate( + total=models.Sum('jine') + )['total'] or 0.00 + + # 上次拒绝原因(最近一条) + last_fail = KaoheJujueJilu.objects.filter(jilu=jl).order_by('-create_time').first() + fail_reason = last_fail.yuanyin if last_fail else '' + + # 考核官头像 + kg_user = UserMain.objects.filter(yonghuid=jl.shenheguan_id).first() + kg_avatar = kg_user.avatar if kg_user else '' + + data.append({ + 'jilu_id': jl.jilu_id, + 'chenghao_name': jl.chenghao.mingcheng if jl.chenghao else '', + 'bankuai_name': jl.bankuai.mingcheng if jl.bankuai else '', + 'zhuangtai': jl.zhuangtai, + 'statusText': dict(ShenheJilu._meta.get_field('zhuangtai').choices).get(jl.zhuangtai, '未知'), + 'statusColor': {0: '#FFA500', 1: '#27ae60', 2: '#e74c3c', 3: '#3498db'}.get(jl.zhuangtai, '#888'), + 'shenheguan_id': jl.shenheguan_id, + 'shenheguan_avatar': kg_avatar, + 'dashou_youxi_id': jl.dashou_youxi_id, + 'dashou_beizhu': jl.dashou_beizhu, + 'cishu': jl.cishu, + 'total_fee': float(total_fee), + 'last_fail_reason': fail_reason, + 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S'), + # 为再次申请预留标签对象数据(前端可能用到) + 'tag_obj': { + 'id': jl.chenghao_id, + 'name': jl.chenghao.mingcheng if jl.chenghao else '', + # 其他字段按需,但申请时会重新拉取 + } if jl.chenghao else None, + }) + + has_more = (page * page_size) < total + return Response({ + 'code': 0, + 'data': {'list': data, 'has_more': has_more, 'total': total} + }) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + + + +from .utils import validate_shenheguan +class KaoheFreeApplyView(APIView): + """免费申请考核(首次/再次,费用为0时调用)""" + 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() # 新增 + + if not tag_id or not game_nick: + return Response({'code': 400, 'msg': '标签ID和游戏昵称必填'}) + + # 校验费用是否为0 + try: + chenghao = Chenghao.objects.get(id=tag_id) + except Chenghao.DoesNotExist: + return Response({'code': 400, 'msg': '标签不存在'}) + + # 计算本次次数 + if reapply and jilu_id: + last_record = ShenheJilu.objects.filter( + jilu_id=jilu_id, shenqingren_id=user.yonghuid, zhuangtai=2 + ).first() + if not last_record: + return Response({'code': 400, 'msg': '原记录不存在或状态不是未通过'}) + new_cishu = last_record.cishu + 1 + 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 + + fee = get_tag_fee(chenghao, new_cishu) + if fee > 0: + return Response({'code': 400, 'msg': '您有未通过的记录请查看并重考'}) + + # 新增:如果指定了考核官,进行合法性校验 + if shenheguan_id: + valid, err = validate_shenheguan(shenheguan_id, chenghao.bankuai, fee, user) + if not valid: + return Response({'code': 400, 'msg': err}) + + # 调用公共创建逻辑 + success, msg, record = create_shenhe_jilu( + user, tag_id, game_nick, remark, reapply, jilu_id, + shenheguan_id=shenheguan_id # 新增参数 + ) + if success: + return Response({'code': 0, 'msg': msg, 'data': {'jilu_id': record.jilu_id}}) + else: + return Response({'code': 400, 'msg': msg}) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + + + + + + +class KaoheJiluListView(APIView): + """ + 获取当前考核官的历史审核记录 + 路径:POST /dengji/khghqkhjl + 权限:JWT Token (考核官) + 参数: + bankuai_id : int (可选,0或空表示全部) + status : int (可选,0-全部 1-审核中 2-已通过 3-未通过) + page : int (默认1) + page_size : int (默认5) + 返回: + list, has_more, total + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + # 1. 验证考核官身份及账号状态 + kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() + if not kg: + return Response({'code': 403, 'msg': '您不是考核官或账号异常'}) + + # 2. 获取参数 + bankuai_id = request.data.get('bankuai_id') + status = int(request.data.get('status', 0)) # 0全部 1审核中 2已通过 3未通过 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 5)) + + # 3. 构建查询条件:只查当前考核官审核的记录 + base_query = ShenheJilu.objects.filter(shenheguan_id=yonghu.yonghuid) + + # 板块筛选 + if bankuai_id and int(bankuai_id) > 0: + base_query = base_query.filter(bankuai_id=int(bankuai_id)) + + # 状态筛选(前端传的状态映射到后端zhuangtai字段) + status_map = { + 0: None, # 全部不筛选 + 1: 0, # 审核中 + 2: 1, # 已通过 + 3: 2, # 未通过 + } + if status_map.get(status) is not None: + base_query = base_query.filter(zhuangtai=status_map[status]) + + # 4. 计数与分页 + total = base_query.count() + offset = (page - 1) * page_size + jilu_list = base_query.order_by('-create_time')[offset:offset + page_size] + + # 5. 预取关联数据,减少查询次数 + jilu_list = jilu_list.select_related('bankuai', 'chenghao') + # 为每条记录预取费用明细和拒绝记录(通过 related_name 或手动过滤) + # 由于 KaoheJiluFeiyong 和 KaoheJujueJilu 通过外键指向 ShenheJilu (to_field='jilu_id'), + # 可以使用 prefetch_related + jilu_ids = [j.jilu_id for j in jilu_list] + fee_dict = {} + reject_dict = {} + if jilu_ids: + # 批量获取所有费用明细 + fee_qs = KaoheJiluFeiyong.objects.filter(jilu_id__in=jilu_ids).order_by('cishu') + for fee in fee_qs: + if fee.jilu_id not in fee_dict: + fee_dict[fee.jilu_id] = [] + fee_dict[fee.jilu_id].append({'cishu': fee.cishu, 'jine': float(fee.jine)}) + + # 批量获取所有拒绝记录 + reject_qs = KaoheJujueJilu.objects.filter(jilu_id__in=jilu_ids).order_by('cishu') + for rej in reject_qs: + if rej.jilu_id not in reject_dict: + reject_dict[rej.jilu_id] = [] + reject_dict[rej.jilu_id].append({ + 'cishu': rej.cishu, + 'yuanyin': rej.yuanyin, + 'create_time': rej.create_time.strftime('%Y-%m-%d %H:%M:%S') if rej.create_time else '' + }) + + # 6. 批量获取申请人信息(昵称和头像) + applicant_ids = {j.shenqingren_id for j in jilu_list} + user_map = {} + if applicant_ids: + users = UserMain.objects.filter(yonghuid__in=applicant_ids).select_related('dashou_profile') + for u in users: + avatar = u.avatar or '' + nicheng = '' + try: + # 打手扩展表可能有 nicheng + nicheng = u.dashou_profile.nicheng if u.dashou_profile else '' + except UserDashou.DoesNotExist: + pass + user_map[u.yonghuid] = { + 'avatar': avatar, + 'nicheng': nicheng or f'用户{u.yonghuid}' + } + + # 7. 组装数据 + records = [] + for jl in jilu_list: + user_info = user_map.get(jl.shenqingren_id, {'avatar': '', 'nicheng': '未知'}) + # 标签特效JSON + texiao_json = {} + if jl.chenghao and jl.chenghao.texiao_miaoshu: + try: + texiao_json = json.loads(jl.chenghao.texiao_miaoshu) + except: + pass + + # 汇总总缴纳费用 + fee_items = fee_dict.get(jl.jilu_id, []) + total_fee = sum(item['jine'] for item in fee_items) + + records.append({ + 'jilu_id': jl.jilu_id, + 'shenqingren_id': jl.shenqingren_id, + 'shenqingren_avatar': user_info['avatar'], + 'shenqingren_nicheng': user_info['nicheng'], + 'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '', + 'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '', + 'chenghao_texiao_json': texiao_json, + 'zhuangtai': jl.zhuangtai, # 0审核中 1通过 2未通过 3待审核 + 'cishu': jl.cishu, + 'total_fee': total_fee, + 'dashou_youxi_id': jl.dashou_youxi_id or '', + 'dashou_beizhu': jl.dashou_beizhu or '', + 'shenheguan_youxi_id': jl.shenheguan_youxi_id or '', + 'guize_neirong': jl.guize_neirong or '', + 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S') if jl.create_time else '', + 'update_time': jl.update_time.strftime('%Y-%m-%d %H:%M:%S') if jl.update_time else '', + 'fee_list': fee_items, + 'reject_list': reject_dict.get(jl.jilu_id, []), + }) + + has_more = (page * page_size) < total + return Response({ + 'code': 0, + 'data': { + 'list': records, + 'has_more': has_more, + 'total': total + } + }) + + except Exception as e: + logger.error(f"获取考核记录异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}, status=500) \ No newline at end of file diff --git a/dingdan/__init__.py b/dingdan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dingdan/admin.py b/dingdan/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/dingdan/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/dingdan/apps.py b/dingdan/apps.py new file mode 100644 index 0000000..0a14712 --- /dev/null +++ b/dingdan/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig + + +class DingdanConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'dingdan' + + def ready(self): + # 导入信号处理,确保信号被注册 + import dingdan.signals \ No newline at end of file diff --git a/dingdan/cross_platform_tasks.py b/dingdan/cross_platform_tasks.py new file mode 100644 index 0000000..afcf707 --- /dev/null +++ b/dingdan/cross_platform_tasks.py @@ -0,0 +1,38 @@ +# dingdan/cross_platform_tasks.py +import logging +import requests +from django.conf import settings + +logger = logging.getLogger(__name__) + +def send_partner_sync_order(partner_order_id, jine, dashou_fencheng, jieshao, beizhu, create_time): + """ + 异步发送派单通知给对方平台(使用简单线程,非Celery) + 注意:此函数应在线程中调用,避免阻塞主请求。 + 生产环境建议使用Celery,此处提供线程兼容版本。 + """ + url = f"{settings.PARTNER_BASE_URL}/dingdan/partner_sync_order" + payload = { + 'token': settings.CROSS_PLATFORM_TOKEN, + 'partner_order_id': partner_order_id, + 'jine': jine, + 'dashou_fencheng': dashou_fencheng, + 'jieshao': jieshao, + 'beizhu': beizhu, + 'create_time': create_time, + } + try: + response = requests.post(url, json=payload, timeout=5) + if response.status_code == 200: + data = response.json() + if data.get('code') == 0: + logger.info(f"跨平台派单同步成功: {partner_order_id}") + # 可在此处保存对方返回的 local_order_id 到本地订单(如果需要) + # local_order_id = data['data'].get('local_order_id') + # Dingdan.objects.filter(dingdan_id=partner_order_id).update(partner_order_id=local_order_id) + else: + logger.error(f"跨平台派单同步业务失败: {data}") + else: + logger.error(f"跨平台派单同步HTTP失败: {response.status_code}") + except Exception as e: + logger.error(f"跨平台派单同步异常: {str(e)}", exc_info=True) \ No newline at end of file diff --git a/dingdan/migrations/0001_initial.py b/dingdan/migrations/0001_initial.py new file mode 100644 index 0000000..964ac46 --- /dev/null +++ b/dingdan/migrations/0001_initial.py @@ -0,0 +1,205 @@ +# Generated by Django 5.2.10 on 2026-01-14 19:59 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Lilubiao', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('lilu', models.DecimalField(decimal_places=4, max_digits=5, null=True, verbose_name='平台分成利率,1是平台,2是商家')), + ('fadanpingtai', models.CharField(max_length=10, null=True, verbose_name='订单所属平台,(商家或平台)')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'db_table': 'lilubiao', + }, + ), + migrations.CreateModel( + name='Ptliushui', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('zonge', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='收取总金额')), + ('zhichuzonge', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='支出总金额')), + ('zongshouyi', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='总收益')), + ('rizonge', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='日收取')), + ('rizhichu', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='日支出')), + ('rishouyi', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='日收益')), + ('zongshoucishu', models.IntegerField(default=0, verbose_name='总收取次数')), + ('rishoucishu', models.IntegerField(default=0, verbose_name='日收取次数')), + ('zongzhichucishu', models.IntegerField(default=0, verbose_name='总支出次数')), + ('rizhichucishu', models.IntegerField(default=0, verbose_name='日支出次数')), + ], + options={ + 'db_table': 'ptliushui', + }, + ), + migrations.CreateModel( + name='Chufajilu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dashouid', models.CharField(db_index=True, max_length=7, null=True, verbose_name='被罚打手ID')), + ('qingqiuid', models.CharField(db_index=True, max_length=7, null=True, verbose_name='请求处罚ID')), + ('chuliid', models.CharField(db_index=True, max_length=7, null=True, verbose_name='处理处罚ID')), + ('cfliyou', models.TextField(blank=True, null=True, verbose_name='处罚理由')), + ('sqzhuangtai', models.IntegerField(blank=True, default=0, null=True, verbose_name='处罚申请状态:0待处理,1成功,2驳回')), + ('bhliyou', models.TextField(blank=True, null=True, verbose_name='驳回处罚理由')), + ('jifen', models.IntegerField(default=0, verbose_name='扣除打手积分')), + ('dingdan_id', models.CharField(db_index=True, max_length=32, null=True, verbose_name='订单ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'db_table': 'chufajilu', + 'indexes': [models.Index(fields=['dashouid'], name='chufajilu_dashoui_07aac0_idx'), models.Index(fields=['qingqiuid'], name='chufajilu_qingqiu_ac160e_idx'), models.Index(fields=['chuliid'], name='chufajilu_chuliid_cd63ac_idx'), models.Index(fields=['dingdan_id'], name='chufajilu_dingdan_b8f0c2_idx')], + }, + ), + migrations.CreateModel( + name='Dashoutupian', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dingdan_id', models.CharField(db_index=True, max_length=32, null=True, verbose_name='订单ID')), + ('dashou', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='接单打手ID')), + ('tupian', models.CharField(blank=True, max_length=500, null=True, verbose_name='打手提交图片URL')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'db_table': 'dashoutupian', + 'indexes': [models.Index(fields=['dashou'], name='dashoutupia_dashou_7de8bd_idx'), models.Index(fields=['dingdan_id'], name='dashoutupia_dingdan_97c685_idx')], + }, + ), + migrations.CreateModel( + name='Dingdan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dingdan_id', models.CharField(db_index=True, max_length=32, unique=True, verbose_name='订单ID')), + ('zhuangtai', models.IntegerField(blank=True, db_index=True, default=1, null=True, verbose_name='订单状态')), + ('fadan_pingtai', models.IntegerField(blank=True, null=True, verbose_name='发单平台')), + ('jine', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='订单金额')), + ('dashou_fencheng', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='打手分成')), + ('jiedan_dashou_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='接单打手ID')), + ('dashou_liuyan', models.TextField(blank=True, null=True, verbose_name='打手留言')), + ('zhiding_id', models.CharField(blank=True, max_length=32, null=True, verbose_name='指定ID')), + ('shangpin_id', models.IntegerField(blank=True, null=True, verbose_name='商品ID')), + ('leixing_id', models.PositiveIntegerField(blank=True, null=True, verbose_name='商品类型ID')), + ('tupian', models.CharField(blank=True, max_length=500, null=True, verbose_name='商品图片URL')), + ('yaoqiuleixing', models.PositiveIntegerField(blank=True, null=True, verbose_name='抢单要求类型')), + ('huiyuan_id', models.CharField(blank=True, max_length=6, null=True, verbose_name='会员ID')), + ('yongjin', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='佣金要求')), + ('jieshao', models.TextField(blank=True, null=True, verbose_name='订单介绍')), + ('beizhu', models.TextField(blank=True, null=True, verbose_name='订单备注')), + ('nicheng', models.CharField(blank=True, max_length=50, null=True, verbose_name='游戏昵称')), + ('user1_id', models.CharField(db_index=True, max_length=30, null=True, verbose_name='用户1标识')), + ('user2_id', models.CharField(db_index=True, max_length=30, null=True, verbose_name='用户2打手标识')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '订单', + 'verbose_name_plural': '订单', + 'db_table': 'dingdan', + 'indexes': [models.Index(fields=['zhuangtai'], name='dingdan_zhuangt_2febe8_idx'), models.Index(fields=['dingdan_id'], name='dingdan_dingdan_83dcb6_idx'), models.Index(fields=['jiedan_dashou_id'], name='dingdan_jiedan__921bfa_idx')], + }, + ), + migrations.CreateModel( + name='Liaotian', + fields=[ + ('id', models.BigAutoField(primary_key=True, serialize=False, verbose_name='主键ID')), + ('user1_id', models.CharField(db_index=True, max_length=30, verbose_name='用户1标识')), + ('user2_id', models.CharField(db_index=True, max_length=30, verbose_name='用户2标识')), + ('dingdan_id', models.CharField(max_length=32, unique=True, verbose_name='订单ID')), + ('zhuangtai', models.IntegerField(choices=[(1, '开启中'), (0, '已关闭')], db_index=True, default=1, verbose_name='聊天状态')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'db_table': 'liaotian', + 'indexes': [models.Index(fields=['user1_id'], name='liaotian_user1_i_6be6cd_idx'), models.Index(fields=['user2_id'], name='liaotian_user2_i_9f61a4_idx'), models.Index(fields=['zhuangtai'], name='liaotian_zhuangt_5e8be5_idx')], + }, + ), + migrations.CreateModel( + name='Pingfen', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dashou', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='接单打手ID')), + ('zongfen', models.PositiveIntegerField(default=0, verbose_name='总分')), + ('cishu', models.PositiveIntegerField(default=0, verbose_name='评分次数')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '评分表', + 'db_table': 'pingfenbiao', + 'indexes': [models.Index(fields=['dashou'], name='pingfenbiao_dashou_f62f10_idx')], + }, + ), + migrations.CreateModel( + name='Tuikuanjilu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dingdan_id', models.CharField(max_length=32, unique=True, verbose_name='订单ID')), + ('dashouid', models.CharField(max_length=7, null=True, verbose_name='接单打手ID')), + ('qingqiuid', models.CharField(max_length=7, null=True, verbose_name='请求用户ID')), + ('chuliid', models.CharField(db_index=True, max_length=7, null=True, verbose_name='处理人ID')), + ('liyou', models.TextField(blank=True, null=True, verbose_name='理由')), + ('sqzhuangtai', models.IntegerField(blank=True, db_index=True, default=0, null=True, verbose_name='申请状态:0待处理,1成功,2驳回')), + ('bhliyou', models.TextField(blank=True, null=True, verbose_name='驳回理由')), + ('jine', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='订单金额')), + ('dashou_fencheng', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='打手分成')), + ('jieshao', models.TextField(blank=True, null=True, verbose_name='订单介绍')), + ('beizhu', models.TextField(blank=True, null=True, verbose_name='订单备注')), + ('nicheng', models.CharField(blank=True, max_length=50, null=True, verbose_name='游戏昵称')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'db_table': 'tuikuanjilu', + 'indexes': [models.Index(fields=['chuliid'], name='tuikuanjilu_chuliid_0b0f16_idx'), models.Index(fields=['sqzhuangtai'], name='tuikuanjilu_sqzhuan_d89dc9_idx')], + }, + ), + migrations.CreateModel( + name='DingdanPingtai', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('laoban_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='老板ID')), + ('laoban_pingjia', models.TextField(blank=True, null=True, verbose_name='老板评价')), + ('dingdan', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='pingtai_kuozhan', to='dingdan.dingdan', verbose_name='订单')), + ], + options={ + 'verbose_name': '平台订单扩展', + 'verbose_name_plural': '平台订单扩展', + 'db_table': 'dingdan_pingtai', + 'indexes': [models.Index(fields=['laoban_id'], name='dingdan_pin_laoban__d55e6f_idx')], + }, + ), + migrations.CreateModel( + name='DingdanShangjia', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('shangjia_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='商家ID')), + ('shangjia_pingjia', models.TextField(blank=True, null=True, verbose_name='商家评价')), + ('cfliyou', models.TextField(blank=True, null=True, verbose_name='商家处罚理由')), + ('sqzhuangtai', models.IntegerField(blank=True, default=0, null=True, verbose_name='处罚申请状态:0待处理,1成功,2驳回')), + ('bhliyou', models.TextField(blank=True, null=True, verbose_name='驳回商家处罚理由')), + ('sjnicheng', models.CharField(max_length=50, null=True, verbose_name='商家昵称')), + ('dingdan', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='shangjia_kuozhan', to='dingdan.dingdan', verbose_name='订单')), + ], + options={ + 'verbose_name': '商家订单扩展', + 'verbose_name_plural': '商家订单扩展', + 'db_table': 'dingdan_shangjia', + 'indexes': [models.Index(fields=['shangjia_id'], name='dingdan_sha_shangji_64d28b_idx')], + }, + ), + ] diff --git a/dingdan/migrations/0002_dingdan_auto_expire_at_dingdan_auto_task_id.py b/dingdan/migrations/0002_dingdan_auto_expire_at_dingdan_auto_task_id.py new file mode 100644 index 0000000..70c4d19 --- /dev/null +++ b/dingdan/migrations/0002_dingdan_auto_expire_at_dingdan_auto_task_id.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-01-17 15:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='dingdan', + name='auto_expire_at', + field=models.DateTimeField(blank=True, null=True, verbose_name='计划过期时间'), + ), + migrations.AddField( + model_name='dingdan', + name='auto_task_id', + field=models.CharField(blank=True, default='', max_length=255, verbose_name='延时任务ID'), + ), + ] diff --git a/dingdan/migrations/0003_dingdan_pending_dispatch_dingdan_status_8_time.py b/dingdan/migrations/0003_dingdan_pending_dispatch_dingdan_status_8_time.py new file mode 100644 index 0000000..0991dfd --- /dev/null +++ b/dingdan/migrations/0003_dingdan_pending_dispatch_dingdan_status_8_time.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-01-17 16:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0002_dingdan_auto_expire_at_dingdan_auto_task_id'), + ] + + operations = [ + migrations.AddField( + model_name='dingdan', + name='pending_dispatch', + field=models.BooleanField(db_index=True, default=False, verbose_name='待调度标记'), + ), + migrations.AddField( + model_name='dingdan', + name='status_8_time', + field=models.DateTimeField(blank=True, null=True, verbose_name='状态变为8的时间'), + ), + ] diff --git a/dingdan/migrations/0004_alter_tuikuanjilu_chuliid.py b/dingdan/migrations/0004_alter_tuikuanjilu_chuliid.py new file mode 100644 index 0000000..b188bc8 --- /dev/null +++ b/dingdan/migrations/0004_alter_tuikuanjilu_chuliid.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-01-17 23:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0003_dingdan_pending_dispatch_dingdan_status_8_time'), + ] + + operations = [ + migrations.AlterField( + model_name='tuikuanjilu', + name='chuliid', + field=models.CharField(db_index=True, max_length=11, null=True, verbose_name='处理人ID'), + ), + ] diff --git a/dingdan/migrations/0005_alter_chufajilu_chuliid.py b/dingdan/migrations/0005_alter_chufajilu_chuliid.py new file mode 100644 index 0000000..b2fa0f3 --- /dev/null +++ b/dingdan/migrations/0005_alter_chufajilu_chuliid.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-01-20 19:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0004_alter_tuikuanjilu_chuliid'), + ] + + operations = [ + migrations.AlterField( + model_name='chufajilu', + name='chuliid', + field=models.CharField(db_index=True, max_length=11, null=True, verbose_name='处理处罚ID'), + ), + ] diff --git a/dingdan/migrations/0006_chufatupian.py b/dingdan/migrations/0006_chufatupian.py new file mode 100644 index 0000000..9c33af9 --- /dev/null +++ b/dingdan/migrations/0006_chufatupian.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.27 on 2026-02-07 19:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0005_alter_chufajilu_chuliid'), + ] + + operations = [ + migrations.CreateModel( + name='Chufatupian', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dingdan_id', models.CharField(db_index=True, max_length=32, null=True, verbose_name='订单ID')), + ('yonghuid', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='用户ID')), + ('tupian', models.CharField(blank=True, max_length=500, null=True, verbose_name='证据图片URL')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'db_table': 'chufatupian', + 'indexes': [models.Index(fields=['yonghuid'], name='chufatupian_yonghui_71e585_idx'), models.Index(fields=['dingdan_id'], name='chufatupian_dingdan_ce0dd4_idx')], + }, + ), + ] diff --git a/dingdan/migrations/0007_chufajilu_ssliyou.py b/dingdan/migrations/0007_chufajilu_ssliyou.py new file mode 100644 index 0000000..542bd6b --- /dev/null +++ b/dingdan/migrations/0007_chufajilu_ssliyou.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-02-08 00:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0006_chufatupian'), + ] + + operations = [ + migrations.AddField( + model_name='chufajilu', + name='ssliyou', + field=models.TextField(blank=True, null=True, verbose_name='打手申诉理由'), + ), + ] diff --git a/dingdan/migrations/0008_dingdan_is_cross_dingdan_partner_order_id.py b/dingdan/migrations/0008_dingdan_is_cross_dingdan_partner_order_id.py new file mode 100644 index 0000000..710df57 --- /dev/null +++ b/dingdan/migrations/0008_dingdan_is_cross_dingdan_partner_order_id.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-02-14 18:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0007_chufajilu_ssliyou'), + ] + + operations = [ + migrations.AddField( + model_name='dingdan', + name='is_cross', + field=models.IntegerField(default=0, verbose_name='是否跨平台订单(0否,1是)'), + ), + migrations.AddField( + model_name='dingdan', + name='partner_order_id', + field=models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='对方平台订单ID'), + ), + ] diff --git a/dingdan/migrations/0009_dingdanpingtai_clkf_dingdanpingtai_jjjs_biaoshi.py b/dingdan/migrations/0009_dingdanpingtai_clkf_dingdanpingtai_jjjs_biaoshi.py new file mode 100644 index 0000000..b7fd291 --- /dev/null +++ b/dingdan/migrations/0009_dingdanpingtai_clkf_dingdanpingtai_jjjs_biaoshi.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-02-21 14:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0008_dingdan_is_cross_dingdan_partner_order_id'), + ] + + operations = [ + migrations.AddField( + model_name='dingdanpingtai', + name='clkf', + field=models.CharField(db_index=True, max_length=20, null=True, verbose_name='处理客服'), + ), + migrations.AddField( + model_name='dingdanpingtai', + name='jjjs_biaoshi', + field=models.IntegerField(default=0, verbose_name='拒绝结算标记(1表示拒绝结算)'), + ), + ] diff --git a/dingdan/migrations/0010_remove_dingdanpingtai_clkf_dingdan_clkf.py b/dingdan/migrations/0010_remove_dingdanpingtai_clkf_dingdan_clkf.py new file mode 100644 index 0000000..25befe3 --- /dev/null +++ b/dingdan/migrations/0010_remove_dingdanpingtai_clkf_dingdan_clkf.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.27 on 2026-02-21 16:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0009_dingdanpingtai_clkf_dingdanpingtai_jjjs_biaoshi'), + ] + + operations = [ + migrations.RemoveField( + model_name='dingdanpingtai', + name='clkf', + ), + migrations.AddField( + model_name='dingdan', + name='clkf', + field=models.CharField(db_index=True, max_length=20, null=True, verbose_name='处理客服'), + ), + ] diff --git a/dingdan/migrations/0011_dingdan_tkly.py b/dingdan/migrations/0011_dingdan_tkly.py new file mode 100644 index 0000000..e535e27 --- /dev/null +++ b/dingdan/migrations/0011_dingdan_tkly.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-02-21 17:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0010_remove_dingdanpingtai_clkf_dingdan_clkf'), + ] + + operations = [ + migrations.AddField( + model_name='dingdan', + name='tkly', + field=models.CharField(blank=True, max_length=500, null=True, verbose_name='退款/拒绝理由'), + ), + ] diff --git a/dingdan/migrations/0012_dingdan_wechat_transaction_id_and_more.py b/dingdan/migrations/0012_dingdan_wechat_transaction_id_and_more.py new file mode 100644 index 0000000..f85a37f --- /dev/null +++ b/dingdan/migrations/0012_dingdan_wechat_transaction_id_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.27 on 2026-02-22 12:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0011_dingdan_tkly'), + ] + + operations = [ + migrations.AddField( + model_name='dingdan', + name='wechat_transaction_id', + field=models.CharField(blank=True, db_index=True, max_length=64, null=True, verbose_name='微信支付订单号'), + ), + migrations.AddIndex( + model_name='dingdan', + index=models.Index(fields=['wechat_transaction_id'], name='dingdan_wechat__278b79_idx'), + ), + ] diff --git a/dingdan/migrations/0013_alter_chufajilu_qingqiuid.py b/dingdan/migrations/0013_alter_chufajilu_qingqiuid.py new file mode 100644 index 0000000..7f1cde8 --- /dev/null +++ b/dingdan/migrations/0013_alter_chufajilu_qingqiuid.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-02-22 19:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0012_dingdan_wechat_transaction_id_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='chufajilu', + name='qingqiuid', + field=models.CharField(db_index=True, max_length=11, null=True, verbose_name='请求处罚ID'), + ), + ] diff --git a/dingdan/migrations/0014_dingdan_dispatch_type_dingdan_partner_club_id.py b/dingdan/migrations/0014_dingdan_dispatch_type_dingdan_partner_club_id.py new file mode 100644 index 0000000..609b314 --- /dev/null +++ b/dingdan/migrations/0014_dingdan_dispatch_type_dingdan_partner_club_id.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-03-24 17:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0013_alter_chufajilu_qingqiuid'), + ] + + operations = [ + migrations.AddField( + model_name='dingdan', + name='dispatch_type', + field=models.IntegerField(choices=[(1, '我方派单'), (2, '对方派单')], default=0, verbose_name='派发情况'), + ), + migrations.AddField( + model_name='dingdan', + name='partner_club_id', + field=models.CharField(blank=True, db_index=True, max_length=6, null=True, verbose_name='对方俱乐部ID'), + ), + ] diff --git a/dingdan/migrations/0015_presettlement_orderdashouhistory_and_more.py b/dingdan/migrations/0015_presettlement_orderdashouhistory_and_more.py new file mode 100644 index 0000000..e545049 --- /dev/null +++ b/dingdan/migrations/0015_presettlement_orderdashouhistory_and_more.py @@ -0,0 +1,73 @@ +# Generated by Django 4.2.27 on 2026-04-01 09:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0014_dingdan_dispatch_type_dingdan_partner_club_id'), + ] + + operations = [ + migrations.CreateModel( + name='PreSettlement', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dingdan_id', models.CharField(db_index=True, max_length=32, unique=True, verbose_name='我方订单ID')), + ('partner_order_id', models.CharField(db_index=True, max_length=32, verbose_name='对方订单ID')), + ('partner_club_id', models.CharField(db_index=True, max_length=6, verbose_name='对方俱乐部ID')), + ('dashou_id', models.CharField(db_index=True, max_length=7, verbose_name='接单打手ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='打手应分金额')), + ('order_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='订单原金额')), + ('status', models.SmallIntegerField(choices=[(0, '待结算'), (1, '已结算'), (2, '已拒绝')], default=0, verbose_name='结算状态')), + ('process_time', models.DateTimeField(blank=True, null=True, verbose_name='处理时间')), + ('operator', models.CharField(blank=True, default='', max_length=32, verbose_name='操作人')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '预结算表', + 'verbose_name_plural': '预结算表', + 'db_table': 'pre_settlement', + 'indexes': [models.Index(fields=['dingdan_id'], name='pre_settlem_dingdan_4f3541_idx'), models.Index(fields=['partner_order_id'], name='pre_settlem_partner_45b29e_idx'), models.Index(fields=['partner_club_id'], name='pre_settlem_partner_d2c44c_idx'), models.Index(fields=['dashou_id'], name='pre_settlem_dashou__d5fe7c_idx'), models.Index(fields=['status'], name='pre_settlem_status_0aa711_idx')], + }, + ), + migrations.CreateModel( + name='OrderDashouHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dingdan_id', models.CharField(db_index=True, max_length=32, verbose_name='订单ID')), + ('dashou_id', models.CharField(db_index=True, max_length=7, verbose_name='打手ID')), + ('times', models.PositiveSmallIntegerField(verbose_name='第几次接单(从1开始)')), + ('operator', models.CharField(max_length=11, verbose_name='操作客服ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ], + options={ + 'verbose_name': '订单打手变更历史', + 'verbose_name_plural': '订单打手变更历史', + 'db_table': 'order_dashou_history', + 'indexes': [models.Index(fields=['dingdan_id'], name='order_dasho_dingdan_1e79ab_idx'), models.Index(fields=['dashou_id'], name='order_dasho_dashou__0a637c_idx')], + }, + ), + migrations.CreateModel( + name='CrossPlatformOrderData', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dingdan_id', models.CharField(db_index=True, max_length=32, unique=True, verbose_name='我方订单ID')), + ('partner_order_id', models.CharField(db_index=True, max_length=32, verbose_name='对方平台订单ID')), + ('partner_club_id', models.CharField(db_index=True, max_length=6, verbose_name='对方俱乐部ID')), + ('partner_order_status', models.IntegerField(default=0, verbose_name='对方平台订单状态')), + ('partner_dashou_id', models.CharField(blank=True, default='', max_length=32, verbose_name='对方平台接单打手ID')), + ('partner_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='对方平台订单金额')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '跨平台订单扩展', + 'verbose_name_plural': '跨平台订单扩展', + 'db_table': 'cross_platform_order_data', + 'indexes': [models.Index(fields=['dingdan_id'], name='cross_platf_dingdan_498d08_idx'), models.Index(fields=['partner_order_id'], name='cross_platf_partner_5d5615_idx'), models.Index(fields=['partner_club_id'], name='cross_platf_partner_1054e1_idx')], + }, + ), + ] diff --git a/dingdan/migrations/0016_alter_presettlement_dingdan_id_and_more.py b/dingdan/migrations/0016_alter_presettlement_dingdan_id_and_more.py new file mode 100644 index 0000000..fc7e742 --- /dev/null +++ b/dingdan/migrations/0016_alter_presettlement_dingdan_id_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.27 on 2026-04-04 15:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0015_presettlement_orderdashouhistory_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='presettlement', + name='dingdan_id', + field=models.CharField(db_index=True, max_length=32, verbose_name='我方订单ID'), + ), + migrations.AlterUniqueTogether( + name='presettlement', + unique_together={('dingdan_id', 'dashou_id')}, + ), + ] diff --git a/dingdan/migrations/0017_dingdanpingtai_dianpu_id_and_more.py b/dingdan/migrations/0017_dingdanpingtai_dianpu_id_and_more.py new file mode 100644 index 0000000..8df76b7 --- /dev/null +++ b/dingdan/migrations/0017_dingdanpingtai_dianpu_id_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.27 on 2026-04-23 14:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0016_alter_presettlement_dingdan_id_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='dingdanpingtai', + name='dianpu_id', + field=models.PositiveIntegerField(blank=True, db_index=True, null=True, verbose_name='所属店铺ID'), + ), + migrations.AddField( + model_name='dingdanpingtai', + name='dianpu_shouyi', + field=models.DecimalField(blank=True, decimal_places=2, default=0.0, max_digits=10, null=True, verbose_name='店铺收益'), + ), + migrations.AddField( + model_name='dingdanpingtai', + name='pingtai_shouyi', + field=models.DecimalField(blank=True, decimal_places=2, default=0.0, max_digits=10, null=True, verbose_name='平台收益'), + ), + migrations.AddIndex( + model_name='dingdanpingtai', + index=models.Index(fields=['dianpu_id'], name='dingdan_pin_dianpu__9fbe27_idx'), + ), + ] diff --git a/dingdan/migrations/0018_dingdanshangjia_partner_yonghu_id.py b/dingdan/migrations/0018_dingdanshangjia_partner_yonghu_id.py new file mode 100644 index 0000000..476cab1 --- /dev/null +++ b/dingdan/migrations/0018_dingdanshangjia_partner_yonghu_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-04-30 02:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0017_dingdanpingtai_dianpu_id_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='dingdanshangjia', + name='partner_yonghu_id', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='对方平台用户ID'), + ), + ] diff --git a/dingdan/migrations/0019_dingdanpingtai_shangji_dianpu_id_and_more.py b/dingdan/migrations/0019_dingdanpingtai_shangji_dianpu_id_and_more.py new file mode 100644 index 0000000..92e3eef --- /dev/null +++ b/dingdan/migrations/0019_dingdanpingtai_shangji_dianpu_id_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-05-05 10:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0018_dingdanshangjia_partner_yonghu_id'), + ] + + operations = [ + migrations.AddField( + model_name='dingdanpingtai', + name='shangji_dianpu_id', + field=models.PositiveIntegerField(blank=True, db_index=True, null=True, verbose_name='该订单的上级店铺ID'), + ), + migrations.AddField( + model_name='dingdanpingtai', + name='shangji_fenhong', + field=models.DecimalField(blank=True, decimal_places=2, default=0.0, max_digits=10, null=True, verbose_name='上级分红金额'), + ), + ] diff --git a/dingdan/migrations/0020_fadanfenhong_fadan.py b/dingdan/migrations/0020_fadanfenhong_fadan.py new file mode 100644 index 0000000..526cbdf --- /dev/null +++ b/dingdan/migrations/0020_fadanfenhong_fadan.py @@ -0,0 +1,58 @@ +# Generated by Django 4.2.27 on 2026-05-06 11:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0019_dingdanpingtai_shangji_dianpu_id_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='FadanFenhong', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='自增主键')), + ('fenhongzhe_id', models.CharField(db_index=True, max_length=20, verbose_name='分红者ID')), + ('zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='分红总额')), + ('ketixian_fenhong', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='可提现分红')), + ('shenfen', models.IntegerField(choices=[(1, '商家'), (2, '客服'), (3, '平台管理者')], default=1, verbose_name='分红者身份')), + ('fenhong_cishu', models.IntegerField(default=0, verbose_name='分红次数')), + ('yingde_lilv', models.DecimalField(decimal_places=4, default=0.0, max_digits=5, verbose_name='分红应得利率')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '罚款分红', + 'verbose_name_plural': '罚款分红', + 'db_table': 'fadan_fenhong', + 'indexes': [models.Index(fields=['fenhongzhe_id'], name='fadan_fenhong_zhe_idx'), models.Index(fields=['shenfen'], name='fadan_fenhong_sf_idx')], + }, + ), + migrations.CreateModel( + name='Fadan', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='自增主键')), + ('beichufa_id', models.CharField(db_index=True, max_length=20, verbose_name='被处罚用户ID')), + ('shenqing_chufa', models.CharField(blank=True, max_length=20, null=True, verbose_name='申请处罚者ID')), + ('shenfen', models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '商家'), (4, '组长')], default=1, verbose_name='被处罚者身份')), + ('chufaliyou', models.TextField(blank=True, null=True, verbose_name='处罚理由')), + ('fakuanjine', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='罚款金额')), + ('guanliandingdan_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='关联订单ID')), + ('zhuangtai', models.IntegerField(choices=[(1, '待缴纳'), (2, '已缴纳'), (3, '申诉中'), (4, '已驳回')], db_index=True, default=1, verbose_name='罚单状态')), + ('yingxiang_qiangdan', models.IntegerField(choices=[(1, '影响抢单'), (0, '不影响抢单')], db_index=True, default=1, verbose_name='是否影响抢单')), + ('shensuliyou', models.TextField(blank=True, null=True, verbose_name='申诉理由')), + ('tongyi_chufaliyou', models.TextField(blank=True, null=True, verbose_name='同意处罚理由')), + ('bohuiliyou', models.TextField(blank=True, null=True, verbose_name='驳回理由')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '罚单', + 'verbose_name_plural': '罚单', + 'db_table': 'fadan', + 'indexes': [models.Index(fields=['beichufa_id', 'yingxiang_qiangdan', 'zhuangtai'], name='fadan_qiangdan_check'), models.Index(fields=['guanliandingdan_id'], name='fadan_dingdan_idx')], + }, + ), + ] diff --git a/dingdan/migrations/0021_fadanshensutupian.py b/dingdan/migrations/0021_fadanshensutupian.py new file mode 100644 index 0000000..2270083 --- /dev/null +++ b/dingdan/migrations/0021_fadanshensutupian.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.27 on 2026-05-07 11:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0020_fadanfenhong_fadan'), + ] + + operations = [ + migrations.CreateModel( + name='FadanShensuTupian', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('beichufa_id', models.CharField(db_index=True, max_length=7, verbose_name='被处罚用户ID')), + ('tupian_url', models.CharField(max_length=500, verbose_name='图片相对URL')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('fadan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shensu_tupians', to='dingdan.fadan', verbose_name='关联罚单')), + ], + options={ + 'verbose_name': '罚款申诉图片', + 'verbose_name_plural': '罚款申诉图片', + 'db_table': 'fadan_shensu_tupian', + 'indexes': [models.Index(fields=['fadan_id'], name='fadan_shens_fadan_i_78fab9_idx'), models.Index(fields=['beichufa_id'], name='fadan_shens_beichuf_59748e_idx')], + }, + ), + ] diff --git a/dingdan/migrations/0022_fadan_partner_club_id_fadan_shenqingren_shenfen.py b/dingdan/migrations/0022_fadan_partner_club_id_fadan_shenqingren_shenfen.py new file mode 100644 index 0000000..e880b33 --- /dev/null +++ b/dingdan/migrations/0022_fadan_partner_club_id_fadan_shenqingren_shenfen.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-05-07 16:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0021_fadanshensutupian'), + ] + + operations = [ + migrations.AddField( + model_name='fadan', + name='partner_club_id', + field=models.CharField(blank=True, db_index=True, max_length=6, null=True, verbose_name='对方俱乐部ID'), + ), + migrations.AddField( + model_name='fadan', + name='shenqingren_shenfen', + field=models.IntegerField(choices=[(1, '客服'), (2, '售后'), (3, '管理员')], default=1, verbose_name='申请处罚人身份'), + ), + ] diff --git a/dingdan/migrations/0023_fadanshensutupian_yongtu.py b/dingdan/migrations/0023_fadanshensutupian_yongtu.py new file mode 100644 index 0000000..1540e2f --- /dev/null +++ b/dingdan/migrations/0023_fadanshensutupian_yongtu.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-05-07 17:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0022_fadan_partner_club_id_fadan_shenqingren_shenfen'), + ] + + operations = [ + migrations.AddField( + model_name='fadanshensutupian', + name='yongtu', + field=models.IntegerField(choices=[(1, '处罚证据图片'), (2, '申诉图片')], default=2, verbose_name='图片用途'), + ), + ] diff --git a/dingdan/migrations/0024_fadan_chulizhe_fadan_chulizhe_shenfen.py b/dingdan/migrations/0024_fadan_chulizhe_fadan_chulizhe_shenfen.py new file mode 100644 index 0000000..0d6fb37 --- /dev/null +++ b/dingdan/migrations/0024_fadan_chulizhe_fadan_chulizhe_shenfen.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-05-07 19:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0023_fadanshensutupian_yongtu'), + ] + + operations = [ + migrations.AddField( + model_name='fadan', + name='chulizhe', + field=models.CharField(blank=True, max_length=20, null=True, verbose_name='处理者'), + ), + migrations.AddField( + model_name='fadan', + name='chulizhe_shenfen', + field=models.IntegerField(choices=[(1, '客服'), (2, '售后'), (3, '管理员')], default=2, verbose_name='处理者身份'), + ), + ] diff --git a/dingdan/migrations/0025_alter_fadan_shenqingren_shenfen.py b/dingdan/migrations/0025_alter_fadan_shenqingren_shenfen.py new file mode 100644 index 0000000..0a408f9 --- /dev/null +++ b/dingdan/migrations/0025_alter_fadan_shenqingren_shenfen.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-05-10 00:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0024_fadan_chulizhe_fadan_chulizhe_shenfen'), + ] + + operations = [ + migrations.AlterField( + model_name='fadan', + name='shenqingren_shenfen', + field=models.IntegerField(choices=[(1, '客服'), (2, '售后'), (3, '管理员'), (4, '店铺')], default=1, verbose_name='申请处罚人身份'), + ), + ] diff --git a/dingdan/migrations/0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more.py b/dingdan/migrations/0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more.py new file mode 100644 index 0000000..3989e0a --- /dev/null +++ b/dingdan/migrations/0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.27 on 2026-05-10 17:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0025_alter_fadan_shenqingren_shenfen'), + ] + + operations = [ + migrations.CreateModel( + name='FadanFenhongLilv', + fields=[ + ('shenfen', models.IntegerField(choices=[(1, '客服'), (2, '售后'), (3, '管理员'), (4, '店铺'), (5, '商家')], primary_key=True, serialize=False, verbose_name='申请处罚人身份')), + ('lilv', models.DecimalField(decimal_places=4, default=0.0, max_digits=5, verbose_name='分红费率')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '罚款分红费率配置', + 'verbose_name_plural': '罚款分红费率配置', + 'db_table': 'fadan_fenhong_lilv', + }, + ), + migrations.AlterField( + model_name='fadan', + name='shenqingren_shenfen', + field=models.IntegerField(choices=[(1, '客服'), (2, '售后'), (3, '管理员'), (4, '店铺'), (5, '商家')], default=1, verbose_name='申请处罚人身份'), + ), + migrations.AlterField( + model_name='fadanfenhong', + name='shenfen', + field=models.IntegerField(choices=[(1, '商家'), (2, '客服'), (3, '平台管理者'), (4, '店铺'), (5, '售后')], default=1, verbose_name='分红者身份'), + ), + ] diff --git a/dingdan/migrations/0027_dingdanpeihu.py b/dingdan/migrations/0027_dingdanpeihu.py new file mode 100644 index 0000000..c929d3b --- /dev/null +++ b/dingdan/migrations/0027_dingdanpeihu.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.27 on 2026-05-22 15:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='DingdanPeihu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dingdan_id', models.CharField(db_index=True, max_length=32, unique=True, verbose_name='订单ID')), + ('zhudashou_id', models.CharField(db_index=True, max_length=32, verbose_name='主打手ID')), + ('peihu1_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='陪护1打手ID')), + ('peihu2_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='陪护2打手ID')), + ('zhuangtai', models.IntegerField(default=1, verbose_name='陪护订单状态')), + ('peihu_xuqiu_shuliang', models.IntegerField(default=1, verbose_name='陪护需求数量')), + ('yongjin_yaoqiu', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='抢单佣金要求')), + ('peihu1_dengji_xuqiu', models.PositiveIntegerField(blank=True, null=True, verbose_name='陪护1等级需求(称号ID)')), + ('peihu2_dengji_xuqiu', models.PositiveIntegerField(blank=True, null=True, verbose_name='陪护2等级需求(称号ID)')), + ('peihu1_zhadan_danze', models.CharField(blank=True, max_length=10, null=True, verbose_name='陪护1炸单担责比例')), + ('peihu2_zhadan_danze', models.CharField(blank=True, max_length=10, null=True, verbose_name='陪护2炸单担责比例')), + ('peihu1_fenhong', models.CharField(blank=True, max_length=10, null=True, verbose_name='陪护1分红比例')), + ('peihu2_fenhong', models.CharField(blank=True, max_length=10, null=True, verbose_name='陪护2分红比例')), + ('zhiding_peihu1', models.CharField(blank=True, max_length=32, null=True, verbose_name='指定陪护1 ID')), + ('zhiding_peihu2', models.CharField(blank=True, max_length=32, null=True, verbose_name='指定陪护2 ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '陪护订单', + 'verbose_name_plural': '陪护订单', + 'db_table': 'dingdan_peihu', + 'indexes': [models.Index(fields=['zhudashou_id'], name='dingdan_pei_zhudash_583f4e_idx'), models.Index(fields=['peihu1_id'], name='dingdan_pei_peihu1__728279_idx'), models.Index(fields=['peihu2_id'], name='dingdan_pei_peihu2__77e45e_idx'), models.Index(fields=['zhuangtai'], name='dingdan_pei_zhuangt_22eaf6_idx')], + }, + ), + ] diff --git a/dingdan/migrations/0028_dingdanshangjia_shangyige_dingdan_id.py b/dingdan/migrations/0028_dingdanshangjia_shangyige_dingdan_id.py new file mode 100644 index 0000000..ee6851d --- /dev/null +++ b/dingdan/migrations/0028_dingdanshangjia_shangyige_dingdan_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-05-29 17:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0027_dingdanpeihu'), + ] + + operations = [ + migrations.AddField( + model_name='dingdanshangjia', + name='shangyige_dingdan_id', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='上一个订单ID'), + ), + ] diff --git a/dingdan/migrations/0029_fadanfenhong_is_individual_rate.py b/dingdan/migrations/0029_fadanfenhong_is_individual_rate.py new file mode 100644 index 0000000..99076b9 --- /dev/null +++ b/dingdan/migrations/0029_fadanfenhong_is_individual_rate.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-05-31 22:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0028_dingdanshangjia_shangyige_dingdan_id'), + ] + + operations = [ + migrations.AddField( + model_name='fadanfenhong', + name='is_individual_rate', + field=models.BooleanField(default=False, verbose_name='是否单独开启分红利率'), + ), + ] diff --git a/dingdan/migrations/0030_fadan_shenqingren_fenhong_jine.py b/dingdan/migrations/0030_fadan_shenqingren_fenhong_jine.py new file mode 100644 index 0000000..7ddb093 --- /dev/null +++ b/dingdan/migrations/0030_fadan_shenqingren_fenhong_jine.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-06-01 00:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0029_fadanfenhong_is_individual_rate'), + ] + + operations = [ + migrations.AddField( + model_name='fadan', + name='shenqingren_fenhong_jine', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='申请人分红金额'), + ), + ] diff --git a/dingdan/migrations/0031_dingdan_guanshi_fencheng.py b/dingdan/migrations/0031_dingdan_guanshi_fencheng.py new file mode 100644 index 0000000..3831bb7 --- /dev/null +++ b/dingdan/migrations/0031_dingdan_guanshi_fencheng.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-06-12 01:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0030_fadan_shenqingren_fenhong_jine'), + ] + + operations = [ + migrations.AddField( + model_name='dingdan', + name='guanshi_fencheng', + field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=10, null=True, verbose_name='管事分成'), + ), + ] diff --git a/dingdan/migrations/__init__.py b/dingdan/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dingdan/models.py b/dingdan/models.py new file mode 100644 index 0000000..f9c89da --- /dev/null +++ b/dingdan/models.py @@ -0,0 +1,588 @@ +from django.db import models + +class Dingdan(models.Model): + """订单主表""" + # 核心编号:唯一且不可为空[citation:3][citation:9] + dingdan_id = models.CharField(max_length=32, unique=True,db_index=True, verbose_name='订单ID') + # 状态与平台:用整数表示,具体含义由业务逻辑定义 + zhuangtai = models.IntegerField(default=1, null=True, blank=True,db_index=True, verbose_name='订单状态') + fadan_pingtai = models.IntegerField(null=True, blank=True, verbose_name='发单平台') + # 金额相关 + jine = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='订单金额') + dashou_fencheng = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='打手分成') + guanshi_fencheng = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, default=0, verbose_name='管事分成') + # 用户与商品关联ID + jiedan_dashou_id = models.CharField(max_length=32, null=True, db_index=True,blank=True, verbose_name='接单打手ID') + dashou_liuyan = models.TextField(null=True, blank=True, verbose_name='打手留言') + zhiding_id = models.CharField(max_length=32, null=True, blank=True, verbose_name='指定ID') + shangpin_id = models.IntegerField(null=True, blank=True, verbose_name='商品ID') + leixing_id = models.PositiveIntegerField(null=True, blank=True, verbose_name='商品类型ID') # 直接存数字 + tupian = models.CharField(max_length=500, null=True, blank=True, verbose_name='商品图片URL') + + yaoqiuleixing = models.PositiveIntegerField(null=True, blank=True, verbose_name='抢单要求类型') + huiyuan_id = models.CharField(max_length=6, null=True, blank=True, verbose_name='会员ID') + yongjin = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='佣金要求') + # 订单描述信息 + jieshao = models.TextField(null=True, blank=True, verbose_name='订单介绍') + beizhu = models.TextField(null=True, blank=True, verbose_name='订单备注') + partner_order_id = models.CharField(max_length=32, null=True, blank=True, db_index=True,verbose_name='对方平台订单ID') + is_cross = models.IntegerField(default=0, verbose_name='是否跨平台订单(0否,1是)') + + dispatch_type = models.IntegerField(default=0, choices=[(1, '我方派单'), (2, '对方派单')], verbose_name='派发情况') + # 🔥 新增:对方俱乐部ID + partner_club_id = models.CharField(max_length=6, null=True, blank=True, db_index=True, verbose_name='对方俱乐部ID') + # 新增字段:游戏昵称(前端提交的游戏昵称) + nicheng = models.CharField(max_length=50, null=True, blank=True, verbose_name='游戏昵称') + user1_id = models.CharField(max_length=30,null=True, db_index=True, verbose_name='用户1标识') + user2_id = models.CharField(max_length=30, null=True,db_index=True, verbose_name='用户2打手标识') + #liaotian_pindao = models.CharField(max_length=255, blank=True, null=True, verbose_name='聊天频道名') + auto_task_id = models.CharField('延时任务ID', max_length=255, blank=True, default='') + auto_expire_at = models.DateTimeField('计划过期时间', null=True, blank=True) + pending_dispatch = models.BooleanField('待调度标记', default=False, db_index=True) + status_8_time = models.DateTimeField('状态变为8的时间', null=True, blank=True) + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + clkf = models.CharField(max_length=20, null=True, db_index=True, verbose_name='处理客服') + tkly = models.CharField(max_length=500, blank=True, null=True, verbose_name='退款/拒绝理由') + wechat_transaction_id = models.CharField( + max_length=64, + blank=True, + null=True, + db_index=True, + verbose_name='微信支付订单号' + ) + + + class Meta: + db_table = 'dingdan' + verbose_name = '订单' + verbose_name_plural = '订单' + # 为高频查询字段添加索引 + indexes = [ + models.Index(fields=['zhuangtai']), + models.Index(fields=['dingdan_id']), + models.Index(fields=['jiedan_dashou_id']), + models.Index(fields=['wechat_transaction_id']), + + ] + + def __str__(self): + return self.dingdan_id + +class DingdanPingtai(models.Model): + """平台订单扩展表 (当 fadan_pingtai=1 时使用)""" + dingdan = models.OneToOneField(Dingdan, on_delete=models.CASCADE, related_name='pingtai_kuozhan', verbose_name='订单') + laoban_id = models.CharField(max_length=32, null=True, blank=True, db_index=True,verbose_name='老板ID') + laoban_pingjia = models.TextField(null=True, blank=True, verbose_name='老板评价') + jjjs_biaoshi = models.IntegerField(default=0, verbose_name='拒绝结算标记(1表示拒绝结算)') + + # ========== 新增字段 ========== + pingtai_shouyi = models.DecimalField(max_digits=10,decimal_places=2,null=True,blank=True,default=0.00,verbose_name='平台收益') + dianpu_shouyi = models.DecimalField(max_digits=10,decimal_places=2, null=True,blank=True,default=0.00,verbose_name='店铺收益') + dianpu_id = models.PositiveIntegerField(null=True,blank=True,db_index=True,verbose_name='所属店铺ID') + + # ---------- 上级分红快照 ---------- + shangji_dianpu_id = models.PositiveIntegerField(null=True, blank=True, db_index=True, + verbose_name='该订单的上级店铺ID') + shangji_fenhong = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, default=0.00, + verbose_name='上级分红金额') + + # ============================= + + + class Meta: + db_table = 'dingdan_pingtai' + verbose_name = '平台订单扩展' + verbose_name_plural = '平台订单扩展' + indexes = [ + models.Index(fields=['laoban_id']), + models.Index(fields=['dianpu_id']), # 为新字段加索引 + + + ] + + def __str__(self): + return f"平台扩展-{self.dingdan.dingdan_id}" + +class DingdanShangjia(models.Model): + """商家发单扩展表 (当 fadan_pingtai=2 时使用)""" + dingdan = models.OneToOneField(Dingdan, on_delete=models.CASCADE, related_name='shangjia_kuozhan', verbose_name='订单') + shangjia_id = models.CharField(max_length=32, null=True, blank=True,db_index=True, verbose_name='商家ID') + shangjia_pingjia = models.TextField(null=True, blank=True, verbose_name='商家评价') + cfliyou = models.TextField(null=True, blank=True, verbose_name='商家处罚理由') + sqzhuangtai =models.IntegerField(default=0, null=True, blank=True, verbose_name='处罚申请状态:0待处理,1成功,2驳回') + bhliyou = models.TextField(null=True, blank=True, verbose_name='驳回商家处罚理由') + sjnicheng =models.CharField(max_length=50,null=True, verbose_name='商家昵称') + partner_yonghu_id = models.CharField(max_length=32, blank=True, null=True, verbose_name='对方平台用户ID') + # 在 dingdan/models.py 的 DingdanShangjia 类中添加 + shangyige_dingdan_id = models.CharField( + max_length=32, + null=True, + blank=True, + verbose_name='上一个订单ID' + ) + + class Meta: + db_table = 'dingdan_shangjia' + verbose_name = '商家订单扩展' + verbose_name_plural = '商家订单扩展' + indexes = [ + models.Index(fields=['shangjia_id']), + + ] + + def __str__(self): + return f"商家扩展-{self.dingdan.dingdan_id}" + + + + + + + + + +class DingdanPeihu(models.Model): + """陪护订单表(一个订单最多2个陪护)""" + # 关联订单(一对一,陪护订单依附于原订单) + dingdan_id = models.CharField(max_length=32, unique=True, db_index=True, verbose_name='订单ID') + # 主打手ID(冗余,方便查询,必须与订单的 jiedan_dashou_id 一致) + zhudashou_id = models.CharField(max_length=32, db_index=True, verbose_name='主打手ID') + # 实际接单陪护 + peihu1_id = models.CharField(max_length=32, null=True, blank=True, db_index=True, verbose_name='陪护1打手ID') + peihu2_id = models.CharField(max_length=32, null=True, blank=True, db_index=True, verbose_name='陪护2打手ID') + # 订单状态(1=待抢单,2=已抢待确认,3=进行中,4=已完成,5=已取消,7=指定状态...) + zhuangtai = models.IntegerField(default=1, verbose_name='陪护订单状态') + # 需求配置 + peihu_xuqiu_shuliang = models.IntegerField(default=1, verbose_name='陪护需求数量') + yongjin_yaoqiu = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='抢单佣金要求') + # 等级需求(关联称号ID,可空表示无限制) + peihu1_dengji_xuqiu = models.PositiveIntegerField(null=True, blank=True, verbose_name='陪护1等级需求(称号ID)') + peihu2_dengji_xuqiu = models.PositiveIntegerField(null=True, blank=True, verbose_name='陪护2等级需求(称号ID)') + # 处罚与分红比例(格式:"1/5" 或 "2/5") + peihu1_zhadan_danze = models.CharField(max_length=10, null=True, blank=True, verbose_name='陪护1炸单担责比例') + peihu2_zhadan_danze = models.CharField(max_length=10, null=True, blank=True, verbose_name='陪护2炸单担责比例') + peihu1_fenhong = models.CharField(max_length=10, null=True, blank=True, verbose_name='陪护1分红比例') + peihu2_fenhong = models.CharField(max_length=10, null=True, blank=True, verbose_name='陪护2分红比例') + # 指定陪护(可预设打手ID,可为空) + zhiding_peihu1 = models.CharField(max_length=32, null=True, blank=True, verbose_name='指定陪护1 ID') + zhiding_peihu2 = models.CharField(max_length=32, null=True, blank=True, verbose_name='指定陪护2 ID') + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'dingdan_peihu' + verbose_name = '陪护订单' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['zhudashou_id']), + models.Index(fields=['peihu1_id']), + models.Index(fields=['peihu2_id']), + models.Index(fields=['zhuangtai']), + ] + + + + + + +# 建议路径:dingdan/models.py 或 独立 fadan/models.py +from django.db import models + +class Fadan(models.Model): + """罚单表——平台各类处罚信息""" + id = models.AutoField(primary_key=True, verbose_name='自增主键') + beichufa_id = models.CharField(max_length=20, db_index=True, verbose_name='被处罚用户ID') # 直接用yonghuid,后续可改为外键 + shenqing_chufa = models.CharField(max_length=20, blank=True, null=True, verbose_name='申请处罚者ID') + shenfen = models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '商家'), (4, '组长')], default=1, verbose_name='被处罚者身份') + chufaliyou = models.TextField(blank=True, null=True, verbose_name='处罚理由') + fakuanjine = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='罚款金额') + guanliandingdan_id = models.CharField(max_length=32, blank=True, null=True, db_index=True, verbose_name='关联订单ID') + zhuangtai = models.IntegerField(choices=[(1, '待缴纳'), (2, '已缴纳'), (3, '申诉中'), (4, '已驳回')], default=1, db_index=True, verbose_name='罚单状态') + yingxiang_qiangdan = models.IntegerField(choices=[(1, '影响抢单'), (0, '不影响抢单')], default=1, db_index=True, verbose_name='是否影响抢单') + shensuliyou = models.TextField(blank=True, null=True, verbose_name='申诉理由') + tongyi_chufaliyou = models.TextField(blank=True, null=True, verbose_name='同意处罚理由') + bohuiliyou = models.TextField(blank=True, null=True, verbose_name='驳回理由') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + # 在 Fadan 模型中添加 + partner_club_id = models.CharField(max_length=6, blank=True, null=True, db_index=True, verbose_name='对方俱乐部ID') + # 在 class Fadan 中添加 + shenqingren_shenfen = models.IntegerField( + choices=[(1, '客服'), (2, '售后'), (3, '管理员'),(4,'店铺'),(5,'商家')], # 按实际扩展 + default=1, + verbose_name='申请处罚人身份' + ) + chulizhe_shenfen = models.IntegerField( + choices=[(1, '客服'), (2, '售后'), (3, '管理员')], + default=2, + verbose_name='处理者身份' + ) + chulizhe = models.CharField(max_length=20, blank=True, null=True, verbose_name='处理者') + # ========== 新增字段 ========== + shenqingren_fenhong_jine = models.DecimalField( + max_digits=12, decimal_places=2, default=0.00, + verbose_name='申请人分红金额' + ) + + class Meta: + db_table = 'fadan' + verbose_name = '罚单' + verbose_name_plural = '罚单' + indexes = [ + models.Index(fields=['beichufa_id', 'yingxiang_qiangdan', 'zhuangtai'], name='fadan_qiangdan_check'), + models.Index(fields=['guanliandingdan_id'], name='fadan_dingdan_idx'), + ] + + def __str__(self): + return f"{self.beichufa_id} - 罚款{self.fakuanjine}元 - 状态{self.zhuangtai}" + + +# ==================== 罚款申诉图片表 ==================== +class FadanShensuTupian(models.Model): + """罚款申诉图片,关联罚单""" + fadan = models.ForeignKey(Fadan, on_delete=models.CASCADE, related_name='shensu_tupians', verbose_name='关联罚单') + beichufa_id = models.CharField(max_length=7, db_index=True, verbose_name='被处罚用户ID') # 冗余,便于直接按用户查 + tupian_url = models.CharField(max_length=500, verbose_name='图片相对URL') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + # 在 FadanShensuTupian 模型中添加 + yongtu = models.IntegerField( + choices=[(1, '处罚证据图片'), (2, '申诉图片')], + default=2, + verbose_name='图片用途' + ) + + class Meta: + db_table = 'fadan_shensu_tupian' + verbose_name = '罚款申诉图片' + verbose_name_plural = '罚款申诉图片' + indexes = [ + models.Index(fields=['fadan_id']), + models.Index(fields=['beichufa_id']), + ] + + + +# 与 Fadan 放在同一个文件或独立的 fadan/models.py +class FadanFenhong(models.Model): + """罚款分红表——记录罚款产生的分红信息""" + id = models.AutoField(primary_key=True, verbose_name='自增主键') + fenhongzhe_id = models.CharField(max_length=20, db_index=True, verbose_name='分红者ID') + zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='分红总额') + ketixian_fenhong = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='可提现分红') + shenfen = models.IntegerField(choices=[(1, '商家'), (2, '客服'), (3, '平台管理者'),(4,'店铺'),(5, '售后')], default=1, verbose_name='分红者身份') + fenhong_cishu = models.IntegerField(default=0, verbose_name='分红次数') + # ✅ 新增字段:是否单独开启分红利率(若开启,则使用本记录的 yingde_lilv;否则使用全局配置) + is_individual_rate = models.BooleanField(default=False, verbose_name='是否单独开启分红利率') + yingde_lilv = models.DecimalField(max_digits=5, decimal_places=4, default=0.0000, verbose_name='分红应得利率') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'fadan_fenhong' + verbose_name = '罚款分红' + verbose_name_plural = '罚款分红' + indexes = [ + models.Index(fields=['fenhongzhe_id'], name='fadan_fenhong_zhe_idx'), + models.Index(fields=['shenfen'], name='fadan_fenhong_sf_idx'), + ] + + def __str__(self): + return f"{self.fenhongzhe_id} - 分红{self.ketixian_fenhong}元" + + + + +class FadanFenhongLilv(models.Model): + shenfen = models.IntegerField(primary_key=True, choices=[(1, '客服'), (2, '售后'), (3, '管理员'), (4, '店铺'), (5, '商家')], verbose_name='申请处罚人身份') + lilv = models.DecimalField(max_digits=5, decimal_places=4, default=0.0000, verbose_name='分红费率') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'fadan_fenhong_lilv' + verbose_name = '罚款分红费率配置' + verbose_name_plural = verbose_name + + def __str__(self): + return f"身份{self.shenfen} - 费率{self.lilv}" + + + + +class CrossPlatformOrderData(models.Model): + """ + 跨平台订单扩展数据表 + 存储我方订单与对方平台订单的关联及对方订单状态 + """ + dingdan_id = models.CharField(max_length=32, unique=True, db_index=True, verbose_name='我方订单ID') + partner_order_id = models.CharField(max_length=32, db_index=True, verbose_name='对方平台订单ID') + partner_club_id = models.CharField(max_length=6, db_index=True, verbose_name='对方俱乐部ID') + partner_order_status = models.IntegerField(default=0, verbose_name='对方平台订单状态') # 对方订单当前状态码 + partner_dashou_id = models.CharField(max_length=32, blank=True, default='', verbose_name='对方平台接单打手ID') + partner_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='对方平台订单金额') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'cross_platform_order_data' + verbose_name = '跨平台订单扩展' + verbose_name_plural = '跨平台订单扩展' + indexes = [ + models.Index(fields=['dingdan_id']), + models.Index(fields=['partner_order_id']), + models.Index(fields=['partner_club_id']), + ] + + def __str__(self): + return f"{self.dingdan_id} -> {self.partner_order_id}" + + + + +# apps/dingdan/models.py 中添加 + +class PreSettlement(models.Model): + """ + 预结算表 + 用于记录对方平台派单订单的待结算款项,等待对方确认后才能真正入账 + """ + STATUS_CHOICES = ( + (0, '待结算'), + (1, '已结算'), + (2, '已拒绝'), + ) + # 我方订单ID + dingdan_id = models.CharField(max_length=32, db_index=True, verbose_name='我方订单ID') + # 对方订单ID + partner_order_id = models.CharField(max_length=32, db_index=True, verbose_name='对方订单ID') + # 对方俱乐部ID + partner_club_id = models.CharField(max_length=6, db_index=True, verbose_name='对方俱乐部ID') + # 接单打手ID + dashou_id = models.CharField(max_length=7, db_index=True, verbose_name='接单打手ID') + # 打手应分金额 + amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='打手应分金额') + # 订单金额(原金额,可选) + order_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='订单原金额') + # 结算状态 + status = models.SmallIntegerField(choices=STATUS_CHOICES, default=0, verbose_name='结算状态') + # 处理时间(结算或拒绝的时间) + process_time = models.DateTimeField(null=True, blank=True, verbose_name='处理时间') + # 处理客服/对方操作人(可选) + operator = models.CharField(max_length=32, blank=True, default='', verbose_name='操作人') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'pre_settlement' + verbose_name = '预结算表' + verbose_name_plural = verbose_name + unique_together = [['dingdan_id', 'dashou_id']] + indexes = [ + models.Index(fields=['dingdan_id']), + models.Index(fields=['partner_order_id']), + models.Index(fields=['partner_club_id']), + models.Index(fields=['dashou_id']), + models.Index(fields=['status']), + ] + + def __str__(self): + return f"预结算-{self.dingdan_id} -> {self.partner_order_id}" + + +# apps/dingdan/models.py 中添加 + +class OrderDashouHistory(models.Model): + """ + 订单打手变更历史表 + 记录订单每次更换打手的操作 + """ + dingdan_id = models.CharField(max_length=32, db_index=True, verbose_name='订单ID') + dashou_id = models.CharField(max_length=7, db_index=True, verbose_name='打手ID') + times = models.PositiveSmallIntegerField(verbose_name='第几次接单(从1开始)') + operator = models.CharField(max_length=11, verbose_name='操作客服ID') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'order_dashou_history' + verbose_name = '订单打手变更历史' + verbose_name_plural = '订单打手变更历史' + indexes = [ + models.Index(fields=['dingdan_id']), + models.Index(fields=['dashou_id']), + ] + + def __str__(self): + return f"{self.dingdan_id} -> {self.dashou_id} 第{self.times}次" + + + + + + +class Dashoutupian(models.Model): + dingdan_id = models.CharField(max_length=32,db_index=True, null=True, verbose_name='订单ID') + dashou = models.CharField(max_length=32, null=True, blank=True,db_index=True, verbose_name='接单打手ID') + tupian = models.CharField(max_length=500, null=True, blank=True, verbose_name='打手提交图片URL') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + class Meta: + db_table = 'dashoutupian' + + indexes = [ + models.Index(fields=['dashou']), + models.Index(fields=['dingdan_id']), + + ] + + + +class Pingfen(models.Model): + dashou = models.CharField(max_length=32, null=True, blank=True, db_index=True,verbose_name='接单打手ID') + zongfen = models.PositiveIntegerField(default=0, verbose_name='总分') + cishu = models.PositiveIntegerField(default=0, verbose_name='评分次数') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'pingfenbiao' + verbose_name = '评分表' + indexes = [ + models.Index(fields=['dashou']), + ] + def __str__(self): + return f"{self.dashou} - 平均分: {self.pingjun_fen():.2f}" + + def pingjun_fen(self): + """计算平均分""" + if self.cishu > 0: + return float(self.zongfen) / self.cishu + return 0.0 + + + +class Lilubiao(models.Model): + lilu = models.DecimalField(max_digits=5, decimal_places=4,null=True, verbose_name='平台分成利率,1是平台,2是商家') + #sahngjialilu = models.DecimalField(max_digits=5, decimal_places=4,null=True, verbose_name='商家利率') + fadanpingtai = models.CharField(max_length=10,null=True,verbose_name='订单所属平台,(商家或平台)') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + class Meta: + db_table = 'lilubiao' + + + + +class Chufajilu(models.Model): + dashouid = models.CharField(max_length=7, null=True, db_index=True, verbose_name='被罚打手ID') + qingqiuid = models.CharField(max_length=11, null=True, db_index=True, verbose_name='请求处罚ID') + chuliid = models.CharField(max_length=11, null=True, db_index=True, verbose_name='处理处罚ID') + cfliyou = models.TextField(null=True, blank=True, verbose_name='处罚理由') + ssliyou = models.TextField(null=True, blank=True, verbose_name='打手申诉理由') + sqzhuangtai = models.IntegerField(default=0, null=True, blank=True, verbose_name='处罚申请状态:0待处理,1成功,2驳回') + bhliyou = models.TextField(null=True, blank=True, verbose_name='驳回处罚理由') + jifen = models.IntegerField(default=0, verbose_name='扣除打手积分') + dingdan_id = models.CharField(max_length=32, null=True, db_index=True, verbose_name='订单ID') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + class Meta: + db_table = 'chufajilu' + indexes = [ + models.Index(fields=['dashouid']), + models.Index(fields=['qingqiuid']), + models.Index(fields=['chuliid']), + models.Index(fields=['dingdan_id']), + ] + + +class Chufatupian(models.Model): + dingdan_id = models.CharField(max_length=32,db_index=True, null=True, verbose_name='订单ID') + yonghuid = models.CharField(max_length=32, null=True, blank=True,db_index=True, verbose_name='用户ID') + tupian = models.CharField(max_length=500, null=True, blank=True, verbose_name='证据图片URL') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + class Meta: + db_table = 'chufatupian' + + indexes = [ + models.Index(fields=['yonghuid']), + models.Index(fields=['dingdan_id']), + + ] + + +class Tuikuanjilu(models.Model): + dingdan_id = models.CharField(max_length=32, unique=True, verbose_name='订单ID') + dashouid = models.CharField(max_length=7, null=True, verbose_name='接单打手ID') + qingqiuid = models.CharField(max_length=7, null=True, verbose_name='请求用户ID') + chuliid = models.CharField(max_length=11, null=True, db_index=True, verbose_name='处理人ID') + liyou = models.TextField(null=True, blank=True, verbose_name='理由') + sqzhuangtai = models.IntegerField(default=0, null=True, blank=True,db_index=True, verbose_name='申请状态:0待处理,1成功,2驳回') + bhliyou = models.TextField(null=True, blank=True, verbose_name='驳回理由') + jine = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='订单金额') + dashou_fencheng = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='打手分成') + jieshao = models.TextField(null=True, blank=True, verbose_name='订单介绍') + beizhu = models.TextField(null=True, blank=True, verbose_name='订单备注') + # 新增字段:游戏昵称(前端提交的游戏昵称) + nicheng = models.CharField(max_length=50, null=True, blank=True, verbose_name='游戏昵称') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + class Meta: + db_table = 'tuikuanjilu' + indexes = [ + models.Index(fields=['chuliid']), + models.Index(fields=['sqzhuangtai']), + ] + + +class Ptliushui(models.Model): + zonge = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='收取总金额') + zhichuzonge = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='支出总金额') + zongshouyi = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='总收益') + rizonge = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='日收取') + rizhichu = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='日支出') + rishouyi = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='日收益') + zongshoucishu = models.IntegerField(default=0, verbose_name='总收取次数') + rishoucishu = models.IntegerField(default=0, verbose_name='日收取次数') + zongzhichucishu = models.IntegerField(default=0, verbose_name='总支出次数') + rizhichucishu = models.IntegerField(default=0, verbose_name='日支出次数') + class Meta: + db_table = 'ptliushui' + + +# models.py - ChatRelation 模型 + + + +class Liaotian(models.Model): + """聊天关系表 - 极简设计,不设置唯一约束""" + # 主键 + id = models.BigAutoField(primary_key=True, verbose_name='主键ID') + + # 聊天双方的用户标识(GoEasy的user_id) + user1_id = models.CharField(max_length=30, db_index=True, verbose_name='用户1标识') + user2_id = models.CharField(max_length=30, db_index=True, verbose_name='用户2标识') + + # 关联的订单(可以为空,但实际业务中应该关联) + dingdan_id = models.CharField(max_length=32, unique=True, verbose_name='订单ID') + # 状态与平台:用整数表示,具体含义由业务逻辑定义 + zhuangtai = models.IntegerField(default=1,db_index=True,choices=[(1, '开启中'), (0, '已关闭')],verbose_name='聊天状态') + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'liaotian' + indexes = [ + models.Index(fields=['user1_id']), + models.Index(fields=['user2_id']), + models.Index(fields=['zhuangtai']), + ] + + def __str__(self): + return f'{self.user1_id} ↔ {self.user2_id} ({self.zhuangtai})' + + + diff --git a/dingdan/signals.py b/dingdan/signals.py new file mode 100644 index 0000000..b34f571 --- /dev/null +++ b/dingdan/signals.py @@ -0,0 +1,614 @@ +""" +阿龙电竞 - 信号处理器(修复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/dingdan/tasks.py b/dingdan/tasks.py new file mode 100644 index 0000000..c079059 --- /dev/null +++ b/dingdan/tasks.py @@ -0,0 +1,216 @@ +""" +阿龙电竞 - 订单定时任务 +处理订单超时自动确认等逻辑 +完整版本,包含所有必要的异常处理 +""" + +from celery import shared_task +from django.db import transaction +from django.db.models import F, Q +from django.conf import settings +from django.utils import timezone +from datetime import timedelta +from decimal import Decimal +import logging + +# 导入模型 - 确保路径正确 +from dingdan.models import Dingdan, DingdanShangjia +from yonghu.models import UserMain, UserDashou, UserShangjia +from utils.celery_utils import safe_decimal_operation, log_task_execution, rollback_on_failure + +logger = logging.getLogger(__name__) + + + + + + + + + +@shared_task(bind=True, max_retries=3, default_retry_delay=60) +def process_expired_order(self, dingdan_id): + """ + 处理单个超时订单(订单状态=8,超过48小时未处理) + """ + + + try: + # 使用select_for_update锁定记录,防止并发修改 + with transaction.atomic(): + + # 获取订单,同时锁定记录 + order = Dingdan.objects.select_for_update().get( + dingdan_id=dingdan_id + ) + + + + # 其余代码不变... + # ... 保持原有代码 + + + + # 再次检查订单状态是否为8 + if order.zhuangtai != 8: + logger.info(f"订单{dingdan_id}状态已不是8(当前:{order.zhuangtai}),跳过处理") + return f"订单{dingdan_id}状态已变更,跳过处理" + + # 获取发单平台类型 + fadan_pingtai = order.fadan_pingtai + + # 获取打手ID和分成金额 + dashou_id = order.jiedan_dashou_id + dashou_fencheng = safe_decimal_operation(order.dashou_fencheng) + + # 1. 更新订单状态为3(已完成) + order.zhuangtai = 3 + order.save(update_fields=['zhuangtai']) + + # 2. 更新打手信息(如果存在) + updated_dashou = False + if dashou_id: + try: + # 查询打手用户主表 + dashou_user = UserMain.objects.filter(yonghuid=dashou_id).first() + if dashou_user: + # 获取打手扩展表 + dashou_profile = getattr(dashou_user, 'dashou_profile', None) + if dashou_profile: + # 使用F表达式原子更新 + UserDashou.objects.filter(pk=dashou_profile.pk).update( + chengjiaozongliang=F('chengjiaozongliang') + 1, + yue=F('yue') + dashou_fencheng, + zonge=F('zonge') + dashou_fencheng, + jinrishouyi=F('jinrishouyi') + dashou_fencheng, + jinyueshouyi=F('jinyueshouyi') + dashou_fencheng + ) + updated_dashou = True + logger.info(f"更新打手{dashou_id}信息成功") + except Exception as e: + logger.error(f"更新打手信息失败,打手ID: {dashou_id}, 错误: {str(e)}") + # 继续执行,不中断流程 + + # 3. 如果是商家发单,更新商家信息 + updated_shangjia = False + if fadan_pingtai == 2: # 商家发单 + try: + # 获取商家扩展信息 + shangjia_kuozhan = getattr(order, 'shangjia_kuozhan', None) + if shangjia_kuozhan: + shangjia_id = shangjia_kuozhan.shangjia_id + if shangjia_id: + # 查询商家用户 + shangjia_user = UserMain.objects.filter(yonghuid=shangjia_id).first() + if shangjia_user: + shangjia_profile = getattr(shangjia_user, 'shop_profile', None) + if shangjia_profile: + # 更新商家成交订单数量 + UserShangjia.objects.filter(pk=shangjia_profile.pk).update( + chengjiao=F('chengjiao') + 1 + ) + updated_shangjia = True + logger.info(f"更新商家{shangjia_id}信息成功") + except Exception as e: + logger.error(f"更新商家信息失败,订单ID: {dingdan_id}, 错误: {str(e)}") + # 继续执行,不中断流程 + + # 记录任务执行结果 + result_msg = f"订单{dingdan_id}自动结算完成" + if updated_dashou: + result_msg += ",打手信息已更新" + if updated_shangjia: + result_msg += ",商家信息已更新" + + if fadan_pingtai == 1: + try: + from dingdan.utils import update_platform_profit # 导入函数 + update_platform_profit(dingdan_id) + except Exception as e: + # 平台收益更新失败不影响主流程,但记录日志 + logger.error(f"强制结单后更新平台收益失败: {str(e)}") + + log_task_execution(f"自动结算订单 {dingdan_id}", True, result_msg) + return result_msg + + except Dingdan.DoesNotExist: + error_msg = f"订单{dingdan_id}不存在" + logger.warning(error_msg) + return error_msg + except Exception as e: + error_msg = f"处理订单{dingdan_id}时发生错误: {str(e)}" + logger.error(error_msg) + + # 重试机制 + if self.request.retries < self.max_retries: + logger.info(f"订单{dingdan_id}处理失败,准备第{self.request.retries + 1}次重试") + raise self.retry(exc=e) + + log_task_execution(f"自动结算订单 {dingdan_id}", False, error_msg) + return error_msg + +@shared_task +@rollback_on_failure("批量检查超时订单") +def check_order_expire_task(): + """ + 批量检查超时订单(补偿机制) + 每5分钟执行一次,作为信号处理的备用方案 + 查找状态为8且创建时间超过72小时的订单(宽松时间,防止误处理) + """ + try: + # 计算更宽松的时间点:当前时间减去72小时(48+24小时) + # 这样避免误处理刚变为状态8的订单 + check_time = timezone.now() - timedelta(seconds=settings.ORDER_EXPIRE_SECONDS + 86400) # 48+24=72小时 + + # 查询状态为8且创建时间早于检查时间的订单 + expired_orders = Dingdan.objects.filter( + zhuangtai=8, + create_time__lt=check_time + ).values_list('dingdan_id', flat=True) + + expired_count = expired_orders.count() + + if expired_count == 0: + logger.info("补偿检查:无超时订单") + return {"success": True, "message": "无超时订单", "count": 0} + + # 分批处理超时订单(避免一次处理太多) + batch_size = 10 + processed_count = 0 + + for i in range(0, expired_count, batch_size): + batch = expired_orders[i:i+batch_size] + + # 异步处理每个超时订单 + for dingdan_id in batch: + process_expired_order.apply_async( + args=[dingdan_id], + queue='order_tasks', + priority=8 # 补偿任务的优先级低一些 + ) + processed_count += 1 + + result_msg = f"补偿检查:发现{expired_count}个可能超时的订单,已提交处理{processed_count}个" + log_task_execution("批量检查超时订单", True, result_msg) + + return { + "success": True, + "message": result_msg, + "total_count": expired_count, + "processed_count": processed_count + } + + except Exception as e: + error_msg = f"批量检查超时订单失败: {str(e)}" + logger.error(error_msg) + log_task_execution("批量检查超时订单", False, error_msg) + + return { + "success": False, + "message": error_msg, + "error": str(e) + } + + + + diff --git a/dingdan/tests.py b/dingdan/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/dingdan/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/dingdan/tongzhi_tasks.py b/dingdan/tongzhi_tasks.py new file mode 100644 index 0000000..874c4b7 --- /dev/null +++ b/dingdan/tongzhi_tasks.py @@ -0,0 +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 + } \ No newline at end of file diff --git a/dingdan/urls.py b/dingdan/urls.py new file mode 100644 index 0000000..ef2c3d9 --- /dev/null +++ b/dingdan/urls.py @@ -0,0 +1,91 @@ +from django.urls import path +from django.views.decorators.csrf import csrf_exempt +from .views import CreateOrderView, WechatPayNotifyView, \ + PaymentFailView, PaymentVerifyView, DingdanHuoquView, DingdanXiangqingView, JiedanView, \ + ShangpinLeixingView, ShangjiaPaifaView, ShangjiaDingdanHuoquView, ShangjiaJiesuanView, \ + ShangjiaDingdanXiangqingView, ShangjiaChufaShenqingView, ShangjiaTuikuanShenqingView, \ + ShangjiaChexiaoView, DashouDingdanHuoquView, ShangpinLeixingHuoquView, QiangdanView, \ + DashouDingdanHuoquView1, DashouCOSZhengshuView, DashouTijiaoView, DashouDingdanXiangqingView, AdGetDingdanXiangQing, \ + AdTongYiTuiKuanPingTai, AdTongYiTuiKuanShangJia, AdQiangZhiJieDan, AdJuJueTuiKuan,\ + AdGengHuanDaShou,ShangjiaCOSZhengshuView, DashouHuoquLeixingView,PartnerNotifyClaimView, PartnerSyncOrderView, PartnerCheckClaimView, PartnerRefundNotifyView,AdJuJueJieSuan, AdZhuanYiDaTing, AdQuXiaoZhiDing,DashouXiugaiView,DaiLiQunLiaoXiaoXiView,\ + ShangjiaLianjieLiuYanZhuanFaView, DingdanXiangqingView2, JiedanView2, ZxsjghdsView, \ + ShangjiaFakuanApplyView, ShangjiaFakuaiXiugaiView + + +urlpatterns = [ + path('xiadan', CreateOrderView.as_view(), name='下单订单生成接口'), + #path('pay-notify/', WechatPayNotifyView.as_view(), name='老板下单微信支付回调'), + path('pay-notify/', csrf_exempt(WechatPayNotifyView.as_view()), name='老板下单微信支付回调'), + path('shibai', PaymentFailView.as_view(), name='老板支付失败接口'), + path('fucha', PaymentVerifyView.as_view(), name='老板支付成功复查接口'), + path('dingdanhuoqu', DingdanHuoquView.as_view(), name='老板订单获取'), + path('dingdanxiangqing', DingdanXiangqingView.as_view(), name='老板订单详情获取'), + path('jiedan', JiedanView.as_view(), name='老板结单'), + path('sjspleixing', ShangpinLeixingView.as_view(), name='派单获取商品类型'), + path('sjpaifa', ShangjiaPaifaView.as_view(), name='商家发单'), + path('sjdingdanhq', ShangjiaDingdanHuoquView.as_view(), name='商家订单获取'), + path('sjddxq', ShangjiaDingdanXiangqingView.as_view(), name='商家订单详情获取'), + path('sjjiesuan', ShangjiaJiesuanView.as_view(), name='商家结单'), + path('sjchexiao', ShangjiaChexiaoView.as_view(), name='商家撤销订单'), + path('sjtkshenqing', ShangjiaTuikuanShenqingView.as_view(), name='商家退款申请'), + path('sjchufa', ShangjiaChufaShenqingView.as_view(), name='商家处罚打手申请'), + path('ddlxhq', ShangpinLeixingHuoquView.as_view(), name='打手抢单页商品类型获取'), + path('dshqdingdan', DashouDingdanHuoquView1.as_view(), name='打手订单列表页面获取'), + + # 获取订单列表 + path('ddhq', DashouDingdanHuoquView.as_view(), name='打手抢单页获取订单'), + path('qiangdan', QiangdanView.as_view(), name='打手抢单'), + path('dsscpz', DashouCOSZhengshuView.as_view(), name='打手上传图片OSS参数获取接口'), + # 打手提交订单完成接口 + path('dstijiao', DashouTijiaoView.as_view(), name='打手订单提交'), + # 打手订单详情获取接口 + path('dsddxq', DashouDingdanXiangqingView.as_view(), name='打手获取订单详情'), + path('adddxq', AdGetDingdanXiangQing.as_view(), name='管理员获取订单详情页面具体信息'), + path('adddpttk', AdTongYiTuiKuanPingTai.as_view(), name='同意平台订单退款'), + + # 商家订单退款接口(之前写的) + path('adddsjtk', AdTongYiTuiKuanShangJia.as_view(), name='同意商家退款'), + + # 强制结单接口 + #path('adddqzjd', AdQiangZhiJieDan.as_view(), name='强制结单'), + + # 拒绝退款接口 + path('adddjj', AdJuJueTuiKuan.as_view(), name='拒绝退款'), + + # 更换打手接口 + path('adddgh', AdGengHuanDaShou.as_view(), name='管理更换打手'), + path('sjscpz', ShangjiaCOSZhengshuView.as_view(), name='商家上传证据图片获取密钥'), + path('dsqdhqddlx', DashouHuoquLeixingView.as_view(), name='打手抢单页面获取订单类型'), + + + + path('adddjjjs', AdJuJueJieSuan.as_view(), name='管理员拒绝给打手钱'), + #path('adddzydt', AdZhuanYiDaTing.as_view(), name='订单转移大厅'), + path('adddqxzd', AdQuXiaoZhiDing.as_view(), name='取消指定'), + + + + + + path('partner_sync_order', csrf_exempt(PartnerSyncOrderView.as_view()), name='东道主派单接口'), + path('partner_check_claim', csrf_exempt(PartnerCheckClaimView.as_view()), name='对方抢单询问接口'), + + + path('partner_notify_claim', csrf_exempt(PartnerNotifyClaimView.as_view()), name='东道主订单被抢通知接口'), + #path('partner_refund_notify', csrf_exempt(PartnerRefundNotifyView.as_view()), name='接受东道主退款通知'), + path('dsxiugaidd', DashouXiugaiView.as_view(), name='打手修改订单详情'), + + path('kptxxfs', DaiLiQunLiaoXiaoXiView.as_view(), name='前端聊天转发'), # 群聊消息代理发送 + path('sjljlbxxzf', ShangjiaLianjieLiuYanZhuanFaView.as_view(), name='商家链接老板聊天转发'), + + path('dingdanxiangqing2', DingdanXiangqingView2.as_view(), name='老板获取订单详情新街口'), + path('jiedan2', JiedanView2.as_view(), name='老板结单新街口'), + path('zxsjghds', ZxsjghdsView.as_view(), name='商家更换打手'), # 商家更换打手 + path('sjfksq', ShangjiaFakuanApplyView.as_view(), name='商家罚款'), + path('sjfkxgycx', ShangjiaFakuaiXiugaiView.as_view(), name='商家罚款和退款修改'), + + + + + +] \ No newline at end of file diff --git a/dingdan/utils.py b/dingdan/utils.py new file mode 100644 index 0000000..0c64ed8 --- /dev/null +++ b/dingdan/utils.py @@ -0,0 +1,443 @@ + +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 logging +from datetime import date + +from django.db import transaction +from django.db.models import F +from peizhi.models import DailyDispatchStat # 假设模型在 peizhi 应用中 + +logger = logging.getLogger(__name__) + +# 可选:线程池限制(避免大量线程同时创建,建议使用 Celery,这里简单用线程) +THREAD_POOL_SIZE = 10 +_semaphore = threading.Semaphore(THREAD_POOL_SIZE) + +def sync_order_to_partners(dingdan_id): + """ + 异步向所有开启互通的合作平台同步订单。 + 使用信号量限制并发线程数。 + """ + def _task(): + _semaphore.acquire() + try: + _sync_order_task(dingdan_id) + finally: + _semaphore.release() + + thread = threading.Thread(target=_task) + thread.daemon = True + thread.start() + +def _sync_order_task(dingdan_id): + """实际同步任务(在线程中执行)""" + try: + # 获取订单对象(只取必要字段,提升性能) + dingdan = Dingdan.objects.only( + 'dingdan_id', 'zhuangtai', 'jine', 'dashou_fencheng', 'jieshao', + 'beizhu', 'nicheng', 'leixing_id', 'yaoqiuleixing', 'huiyuan_id', + 'yongjin', 'zhiding_id', 'create_time' + ).get(dingdan_id=dingdan_id) + + # 基本条件判断 + if dingdan.zhuangtai != 1: + logger.info(f"订单 {dingdan_id} 状态 {dingdan.zhuangtai} 不是1,不同步") + return + if dingdan.zhiding_id: + logger.info(f"订单 {dingdan_id} 是指定单,不同步") + return + if dingdan.yaoqiuleixing != 1: + logger.info(f"订单 {dingdan_id} 抢单要求类型不是会员抢单,不同步") + return + if not dingdan.leixing_id: + logger.info(f"订单 {dingdan_id} 无商品类型,不同步") + return + try: + leixing = ShangpinLeixing.objects.get(id=dingdan.leixing_id) + if not leixing.is_cross_enabled: + logger.info(f"订单 {dingdan_id} 的商品类型未开启互通,不同步") + return + except ShangpinLeixing.DoesNotExist: + logger.info(f"订单 {dingdan_id} 的商品类型不存在,不同步") + return + + # 查询所有开启互通的俱乐部 + clubs = Club.objects.filter(is_interop_enabled=True).select_related() + if not clubs.exists(): + return + + # 为每个俱乐部发送订单 + for club in clubs: + _send_to_single_partner(dingdan, club) + + except Exception as e: + logger.error(f"同步订单任务异常: {e}", exc_info=True) + +def _send_to_single_partner(dingdan, club): + """向单个俱乐部发送订单(带重试,幂等处理)""" + # 计算实际金额:订单打手分成 × 对方费率 + actual_amount = dingdan.dashou_fencheng * club.dashou_rate + + # 构建请求数据 + payload = { + 'token': settings.CROSS_PLATFORM_TOKEN, + 'our_club_id': club.club_id, # 我方在对方系统中的标识(存储于对方表的 partner_club_id) + 'partner_club_id': club.partner_club_id, # 对方自己的俱乐部ID + 'partner_order_id': dingdan.dingdan_id, # 我方订单ID作为对方的外部订单号 + 'jine': str(actual_amount), # 对方平台应展示的订单金额 + 'dashou_fencheng': str(actual_amount), # 对方平台打手分成(与金额一致) + 'jieshao': dingdan.jieshao, + 'beizhu': dingdan.beizhu, + 'create_time': dingdan.create_time.isoformat(), + 'nicheng': dingdan.nicheng, + 'leixing_id': dingdan.leixing_id, # 我方商品类型ID,对方需要映射 + 'user_id': get_order_user_id(dingdan), # 对方平台下单用户ID(不带前缀) + } + + # 重试机制(最多3次,指数退避) + for attempt in range(1, 4): + try: + response = requests.post( + club.partner_domain + '/dingdan/partner_sync_order', + json=payload, + timeout=5 + ) + if response.status_code == 200: + data = response.json() + # 对方返回 code=0 表示成功(包括新建或已存在) + if data.get('code') == 0: + # 判断本次响应是新创建还是已存在 + status = data.get('data', {}).get('status', '') + if status == 'existed': + # 订单已经存在于对方系统,不重复统计 + logger.info(f"订单 {dingdan.dingdan_id} 在俱乐部 {club.club_id} 已存在,跳过统计") + else: + # 新创建成功,更新统计 + try: + update_daily_dispatch_stat( + direction=1, # 我方派单 + partner_club_id=club.partner_club_id, + dispatch_amount=actual_amount, + + ) + logger.info(f"成功同步订单 {dingdan.dingdan_id} 到俱乐部 {club.club_id}") + except Exception as e: + logger.error(f"更新派单统计失败: {e}", exc_info=True) + + # 标记订单为跨平台(仅当第一次成功时标记,避免重复标记) + if not dingdan.is_cross: + Dingdan.objects.filter(dingdan_id=dingdan.dingdan_id).update( + is_cross=1, + dispatch_type=1 # 我方派单 + ) + logger.info(f"订单 {dingdan.dingdan_id} 已标记为跨平台订单") + break + else: + # 对方返回业务错误(如映射不存在等),不重试 + logger.error(f"同步订单失败(业务): {data}") + break + else: + logger.error(f"同步订单HTTP失败: {response.status_code}") + except Exception as e: + 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} 最终失败") + + + +# 获取下单方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) + return ext.laoban_id # 老板ID + except DingdanPingtai.DoesNotExist: + return None + elif order.fadan_pingtai == 2: + try: + ext = DingdanShangjia.objects.get(dingdan=order) + return ext.shangjia_id # 商家ID + except DingdanShangjia.DoesNotExist: + return None + return None + + + + +# 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, + claimed_amount=None, claimed_count=1, + success_amount=None, success_count=1): + """ + 原子更新每日派单统计(支持派单、抢单、成交三种类型)。 + 只更新传递了非None参数的字段,未传递的字段保持不变。 + + 参数: + direction: 1=我方派单,2=对方派单 + partner_club_id: 对方俱乐部ID + dispatch_amount: 派单金额(Decimal),如果提供则累加到 dispatch_amount 和 dispatch_count + dispatch_count: 派单数量,默认为1,与 dispatch_amount 同时提供时生效 + claimed_amount: 抢单金额(Decimal),如果提供则累加到 claimed_amount 和 claimed_count + claimed_count: 抢单数量,默认为1,与 claimed_amount 同时提供时生效 + success_amount: 成交金额(Decimal),如果提供则累加到 success_amount 和 success_count + success_count: 成交数量,默认为1,与 success_amount 同时提供时生效 + """ + today = date.today() + year, month, day = today.year, today.month, today.day + + with transaction.atomic(): + # 使用 select_for_update 锁定行,避免并发累加冲突 + stat, created = DailyDispatchStat.objects.select_for_update().get_or_create( + date=today, + year=year, + month=month, + day=day, + direction=direction, + partner_club_id=partner_club_id, + defaults={ + 'dispatch_count': dispatch_count if dispatch_amount is not None else 0, + 'dispatch_amount': dispatch_amount or Decimal('0.00'), + 'claimed_count': claimed_count if claimed_amount is not None else 0, + 'claimed_amount': claimed_amount or Decimal('0.00'), + 'success_count': success_count if success_amount is not None else 0, + 'success_amount': success_amount or Decimal('0.00'), + } + ) + + if not created: + update_fields = [] + if dispatch_amount is not None: + stat.dispatch_count = F('dispatch_count') + dispatch_count + stat.dispatch_amount = F('dispatch_amount') + dispatch_amount + update_fields.extend(['dispatch_count', 'dispatch_amount']) + if claimed_amount is not None: + stat.claimed_count = F('claimed_count') + claimed_count + stat.claimed_amount = F('claimed_amount') + claimed_amount + update_fields.extend(['claimed_count', 'claimed_amount']) + if success_amount is not None: + stat.success_count = F('success_count') + success_count + stat.success_amount = F('success_amount') + success_amount + update_fields.extend(['success_count', 'success_amount']) + + if update_fields: + stat.save(update_fields=update_fields) + + logger.info(f"更新派单统计: 日期={today}, 方向={direction}, 俱乐部={partner_club_id}, " + f"派单+{dispatch_count if dispatch_amount else 0}单/{dispatch_amount or 0}, " + f"抢单+{claimed_count if claimed_amount else 0}单/{claimed_amount or 0}, " + f"成交+{success_count if success_amount else 0}单/{success_amount or 0}") + + + + +from peizhi.models import DailyIncomeStat + +logger = logging.getLogger(__name__) + + +def update_daily_income(amount): + """ + 原子更新当日收入统计(金额累加,笔数加1)。 + 用于微信支付成功回调。 + + 参数: + amount: Decimal 本次收入金额 + """ + today = date.today() + year, month, day = today.year, today.month, today.day + + with transaction.atomic(): + stat, created = DailyIncomeStat.objects.select_for_update().get_or_create( + date=today, + defaults={ + 'year': year, + 'month': month, + 'day': day, + 'total_amount': amount, + 'total_count': 1 + } + ) + if not created: + # 原子累加 + stat.total_amount = F('total_amount') + amount + stat.total_count = F('total_count') + 1 + stat.save(update_fields=['total_amount', 'total_count']) + + logger.info( + f"每日收入更新: {today}, +{amount}元, 总金额={stat.total_amount if created else stat.total_amount + amount}, 总笔数={stat.total_count if created else stat.total_count + 1}") + + + + + +from peizhi.models import DailyPayoutStat + +logger = logging.getLogger(__name__) + +def update_daily_payout(amount): + """ + 原子更新当日出款统计(金额累加,笔数加1)。 + 用于提现成功、结算打款等场景。 + + 参数: + amount: Decimal 本次出款金额 + """ + today = date.today() + year, month, day = today.year, today.month, today.day + + with transaction.atomic(): + stat, created = DailyPayoutStat.objects.select_for_update().get_or_create( + date=today, + defaults={ + 'year': year, + 'month': month, + 'day': day, + 'total_amount': amount, + 'total_count': 1 + } + ) + if not created: + # 原子累加 + stat.total_amount = F('total_amount') + amount + stat.total_count = F('total_count') + 1 + stat.save(update_fields=['total_amount', 'total_count']) + + logger.info(f"每日出款更新: {today}, +{amount}元, 总金额={stat.total_amount if created else stat.total_amount + amount}, 总笔数={stat.total_count if created else stat.total_count + 1}") + + + + + + + +from decimal import Decimal + +from dingdan.models import Lilubiao + + +def calc_shangjia_order_fencheng(jine): + """ + 商家发单时计算打手/管事分成。 + 打手优先:打手+管事 > 订单金额时,管事分成为 0。 + """ + jine = Decimal(str(jine)) + try: + lilu_obj = Lilubiao.objects.get(fadanpingtai='3') + rate_dashou = Decimal(str(lilu_obj.lilu)) if lilu_obj.lilu and lilu_obj.lilu > 0 else Decimal('1') + except Lilubiao.DoesNotExist: + rate_dashou = Decimal('1') + + lilu_guanshi_obj = Lilubiao.objects.filter(fadanpingtai='13').first() + rate_guanshi = ( + Decimal(str(lilu_guanshi_obj.lilu)) + if lilu_guanshi_obj and lilu_guanshi_obj.lilu is not None + else Decimal('0') + ) + + dashou_fencheng = (jine * rate_dashou).quantize(Decimal('0.01')) + guanshi_raw = (jine * rate_guanshi).quantize(Decimal('0.01')) + + if dashou_fencheng + guanshi_raw > jine: + guanshi_fencheng = Decimal('0.00') + else: + guanshi_fencheng = guanshi_raw + + return dashou_fencheng, guanshi_fencheng + + +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 + + guanshi_fencheng = order.guanshi_fencheng or Decimal('0') + if guanshi_fencheng <= 0: + return + + if Gsfenhong.objects.filter(dingdan_id=order.dingdan_id).exists(): + logger.info(f"商家订单管事分红已处理: {order.dingdan_id}") + return + + if not dashou_id: + return + + try: + dashou = UserDashou.objects.select_related('user').get(user__yonghuid=dashou_id) + except UserDashou.DoesNotExist: + logger.info(f"商家订单管事分红跳过: 打手{dashou_id}不存在") + return + + guanshi_id = dashou.yaoqingren + if not guanshi_id: + logger.info(f"商家订单管事分红跳过: 打手{dashou_id}无邀请管事") + return + + try: + with transaction.atomic(): + guanshi = UserGuanshi.objects.select_for_update().get(user__yonghuid=guanshi_id) + UserGuanshi.objects.filter(id=guanshi.id).update( + yue=F('yue') + guanshi_fencheng, + chongzhifenrun=F('chongzhifenrun') + guanshi_fencheng, + ) + + user_main = dashou.user + Gsfenhong.objects.create( + dingdan_id=order.dingdan_id, + guanshi=guanshi_id, + dashouid=dashou_id, + shuoming='商家订单分红', + fenhong=guanshi_fencheng, + avatar=user_main.avatar if user_main else None, + nicheng=dashou.nicheng or '未知打手', + fenhong_leixing=3, + ) + logger.info( + f"商家订单管事分红成功: 订单{order.dingdan_id}, 管事{guanshi_id}, " + f"打手{dashou_id}, 金额{guanshi_fencheng}元" + ) + except UserGuanshi.DoesNotExist: + logger.warning(f"商家订单管事分红跳过: 管事{guanshi_id}不存在") + return + except Exception as e: + logger.error(f"商家订单管事分红失败: {e}", exc_info=True) + return + + try: + from houtai.utils import update_guanshi_daily_by_action + update_guanshi_daily_by_action( + yonghuid=guanshi_id, + action=3, + amount=guanshi_fencheng, + ) + except Exception as e: + logger.error(f"商家订单管事日统计更新失败: {e}") diff --git a/dingdan/views.py b/dingdan/views.py new file mode 100644 index 0000000..d67d0fc --- /dev/null +++ b/dingdan/views.py @@ -0,0 +1,11494 @@ +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 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, DingdanShangjia, Chufajilu, Liaotian, Chufatupian +from utils.weixin_broadcast import WeixinBroadcastSender +from yonghu.models import UserMain, UserDashou, UserShangjia + +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 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 + +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): + """ + 前端支付失败接口 - 处理用户主动取消或支付失败 + 只允许删除状态为9(未支付)的订单 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}) + + with transaction.atomic(): + # 锁定订单防止并发 + dingdan = Dingdan.objects.select_for_update().filter(dingdan_id=dingdanid).first() + if not dingdan: + return Response({'code': 2, 'msg': '订单不存在', 'data': None}) + + # 只允许删除未支付订单(状态9) + if dingdan.zhuangtai != 9: + return Response({'code': 3, 'msg': '只能删除未支付的订单', 'data': None}) + + # 删除扩展表和主表 + DingdanPingtai.objects.filter(dingdan=dingdan).delete() + dingdan.delete() + + return Response({'code': 0, 'msg': '订单已删除', 'data': None}) + + except Exception as e: + 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): + """微信支付回调接口 - 接收微信官方支付结果通知(含详细调试日志)""" + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + + def post(self, request): + # 记录原始请求数据(调试用) + xml_data = request.body.decode('utf-8') + + # 最外层异常捕获,确保任何错误都能被记录并返回适当响应 + try: + # 1. 解析 XML + try: + root = ET.fromstring(xml_data) + except ET.ParseError as e: + logger.error(f"XML解析失败: {e}") + return self._gen_response_xml('FAIL', 'XML解析失败') + + # 2. 获取关键参数 + return_code = root.find('return_code').text if root.find('return_code') is not None else '' + result_code = root.find('result_code').text if root.find('result_code') is not None else '' + out_trade_no = root.find('out_trade_no').text if root.find('out_trade_no') is not None else '' + transaction_id = root.find('transaction_id').text if root.find('transaction_id') is not None else '' + + # 3. 签名验证 + if not self._verify_signature(xml_data): + logger.warning("签名验证失败") + return self._gen_response_xml('FAIL', '签名验证失败') + + # 4. 根据支付结果处理 + if return_code == 'SUCCESS' and result_code == 'SUCCESS': + self._handle_success(out_trade_no, transaction_id, root) + else: + logger.info(f"支付失败或异常,return_code={return_code}, result_code={result_code}") + self._handle_failure(out_trade_no) + + # 返回微信要求的成功响应(必须返回SUCCESS,即使内部处理失败也要返回,否则微信会重复通知) + return self._gen_response_xml('SUCCESS', 'OK') + + except Exception as e: + # 捕获任何未处理的异常,记录完整堆栈 + logger.error(f"回调处理发生未捕获异常: {str(e)}") + logger.error(traceback.format_exc()) + return self._gen_response_xml('FAIL', f'服务器内部错误: {str(e)}') + + def _verify_signature(self, xml_data): + """微信支付签名验证(MD5)""" + try: + root = ET.fromstring(xml_data) + sign = root.find('sign').text + # 构建待签名字符串(排除sign字段) + data = {child.tag: child.text for child in root if child.tag != 'sign'} + # 按key排序 + sorted_keys = sorted(data.keys()) + stringA = '&'.join([f"{k}={data[k]}" for k in sorted_keys]) + stringSignTemp = f"{stringA}&key={settings.WEIXIN_SHANGHUMIYAO}" + calc_sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() + logger.debug(f"计算签名: {calc_sign}, 微信签名: {sign}") + return calc_sign == sign + except Exception as e: + logger.error(f"签名验证过程异常: {e}") + logger.error(traceback.format_exc()) + return False + + def _gen_response_xml(self, return_code, return_msg): + """生成返回给微信的XML响应""" + xml = f""" + + +""" + logger.info(f"返回给微信的响应: {xml}") + return HttpResponse(xml, content_type='application/xml') + + @transaction.atomic + def _handle_success(self, out_trade_no, transaction_id, root): + """处理支付成功""" + logger.info(f"进入 _handle_success, out_trade_no={out_trade_no}, transaction_id={transaction_id}") + + # 锁定订单防止并发 + try: + dingdan = Dingdan.objects.select_for_update().filter(dingdan_id=out_trade_no).first() + except Exception as e: + logger.error(f"查询订单异常: {e}") + raise + + if not dingdan: + logger.error(f"订单不存在: {out_trade_no}") + return + + logger.info(f"订单当前状态: {dingdan.zhuangtai}") + + # 幂等处理:只有状态为9(未支付)才处理,已处理则跳过 + if dingdan.zhuangtai != 9: + logger.info(f"订单 {out_trade_no} 已处理过,状态={dingdan.zhuangtai},跳过") + return + + # 保存微信交易号 + if transaction_id: + dingdan.wechat_transaction_id = transaction_id + logger.info(f"保存微信交易号: {transaction_id}") + else: + logger.warning("微信未返回transaction_id") + + # 更新订单状态 + if dingdan.zhiding_id and dingdan.zhiding_id.strip(): + dingdan.zhuangtai = 7 + logger.info("指定单,状态设为7") + else: + dingdan.zhuangtai = 1 + logger.info("普通单,状态设为1") + + dingdan.save() + + logger.info(f"订单状态已更新为 {dingdan.zhuangtai}") + + # 删除同一用户同一商品的其他未支付订单(改为逻辑删除,状态25) + try: + abandoned_count = Dingdan.objects.filter( + user1_id=dingdan.user1_id, + shangpin_id=dingdan.shangpin_id, + zhuangtai=9 + ).exclude(dingdan_id=out_trade_no).update(zhuangtai=25) + logger.info(f"将 {abandoned_count} 个未支付订单标记为已废弃(25)") + except Exception as e: + logger.error(f"标记其他未支付订单异常: {e}") + + # 更新老板扩展表 + try: + laoban_id = None + if hasattr(dingdan, 'pingtai_kuozhan'): + laoban_id = dingdan.pingtai_kuozhan.laoban_id + if laoban_id: + user_main = UserMain.objects.filter(yonghuid=laoban_id).first() + if user_main and hasattr(user_main, 'boss_profile'): + boss = user_main.boss_profile + boss.zonge = (boss.zonge or 0) + (dingdan.jine or 0) + boss.alldingdan = (boss.alldingdan or 0) + 1 + boss.save() + logger.info(f"更新老板 {laoban_id} 数据成功") + except Exception as e: + logger.error(f"更新老板数据异常: {e}") + + # 更新商品库存 + try: + if dingdan.shangpin_id: + Shangpin.objects.filter(id=dingdan.shangpin_id).update( + kucun=F('kucun') - 1 + ) + logger.info(f"商品 {dingdan.shangpin_id} 库存减1") + except Exception as e: + logger.error(f"更新商品库存异常: {e}") + + + + + # ========== 新增:商品和店铺每日统计 ========== + try: + # 商品每日统计(只要有商品ID就执行) + if dingdan.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(dingdan, 'pingtai_kuozhan'): + pingtai_shouyi = dingdan.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=dingdan.shangpin_id, + caozuo_leixing=1, # 1 = 下单 + dingdan_jiage=dingdan.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {dingdan.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + dianpu_id = None + if hasattr(dingdan, 'pingtai_kuozhan'): + dianpu_id = dingdan.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=1, # 1 = 下单 + dingdan_jiage=dingdan.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + + + + + + + + # 更新收支记录(总累计) + self._update_szjilu(dingdan.jine) + + # 更新每日收入统计 + update_daily_income(dingdan.jine) + + # 跨平台同步 + 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, + '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) # 瞬间返回,不阻塞请求 + + # 异步广播通知 + #self._broadcast_async(dingdan) + + logger.info(f"订单 {out_trade_no} 支付成功处理完成") + + @transaction.atomic + def _handle_failure(self, out_trade_no): + """处理支付失败""" + logger.info(f"进入 _handle_failure, out_trade_no={out_trade_no}") + + try: + dingdan = Dingdan.objects.select_for_update().filter(dingdan_id=out_trade_no).first() + if not dingdan: + logger.warning(f"订单不存在: {out_trade_no}") + return + + logger.info(f"订单当前状态: {dingdan.zhuangtai}") + + # 仅当订单状态为9(未支付)时才标记失败,不直接删除 + if dingdan.zhuangtai == 9: + dingdan.zhuangtai = 14 # 自定义支付失败状态 + dingdan.save() + logger.info(f"订单 {out_trade_no} 支付失败,标记为14") + else: + logger.info(f"订单 {out_trade_no} 状态非9,不处理") + except Exception as e: + logger.error(f"处理支付失败异常: {e}") + logger.error(traceback.format_exc()) + + def _update_szjilu(self, jine): + """更新收支记录表(ID=1的记录)""" + try: + jine_decimal = Decimal(str(jine)) + with transaction.atomic(): + szjilu, created = Szjilu.objects.select_for_update().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') + } + ) + if created: + logger.info("创建ID=1的收支记录") + szjilu.zongsy += jine_decimal + szjilu.zongls += jine_decimal + szjilu.jrls += jine_decimal + szjilu.save() + logger.info(f"收支记录更新成功,增加金额: {jine}") + except Exception as e: + logger.error(f"更新收支记录失败: {e}") + + def _broadcast_async(self, dingdan): + """异步广播通知""" + 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), + 'amount': str(dingdan.jine), + 'order_desc': (dingdan.jieshao or '')[:50], + } + logger.info(f"准备广播订单: {order_info}") + sender = WeixinBroadcastSender() + result = sender.broadcast_order(order_info) + if result.get('success'): + logger.info(f"订单广播完成: {result.get('msg')}") + else: + logger.warning(f"订单广播异常: {result.get('msg')}") + except Exception as e: + logger.error(f"广播任务异常: {e}") + logger.error(traceback.format_exc()) + + thread = threading.Thread(target=task) + thread.daemon = True + thread.start() + logger.info("广播线程已启动") + except Exception as e: + logger.error(f"启动广播线程异常: {e}") + + 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 '游戏订单' + + + + +from django.core.cache import cache +class PaymentVerifyView(APIView): + """ + 订单支付成功复查接口 - 前端支付成功后调用,确认订单状态 + 当订单状态为9时,主动查询微信支付状态,若已支付则更新本地状态 + 需要token认证 + """ + permission_classes = [permissions.IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + # ---------- 1. 获取参数 ---------- + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 1, 'msg': '订单ID不能为空'}) + + # ---------- 2. 查询订单 ---------- + dingdan = Dingdan.objects.filter(dingdan_id=dingdanid).first() + if not dingdan: + return Response({'code': 2, 'msg': '订单不存在'}) + + # ---------- 3. 验证订单归属(安全) ---------- + current_user = request.user + user_identifier = f"Boss{current_user.yonghuid}" + if dingdan.user1_id != user_identifier: + logger.warning(f"用户 {current_user.yonghuid} 尝试复查他人订单 {dingdanid}") + return Response({'code': 403, 'msg': '无权访问'}) + + # ---------- 4. 如果订单状态已是1/7,直接返回成功 ---------- + if dingdan.zhuangtai in [1, 7]: + return Response({ + 'code': 0, + 'msg': '订单状态正常', + 'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.zhuangtai} + }) + + # ---------- 5. 如果状态为9,则主动查询微信支付状态 ---------- + if dingdan.zhuangtai == 9: + # 使用分布式锁防止并发处理同一订单 + lock_key = f"payment_verify_lock_{dingdanid}" + if not cache.add(lock_key, "locked", timeout=10): + # 已有其他进程在处理,让前端稍后重试 + return Response({'code': 1, 'msg': '订单正在处理中,请稍后'}) + + try: + # 调用微信订单查询接口 + trade_state, transaction_id = self._query_wechat_payment(dingdanid) + + if trade_state == 'SUCCESS': + # 支付成功,执行本地更新 + self._process_payment_success(dingdan, transaction_id, current_user.yonghuid) + # 重新获取订单以获取最新状态 + dingdan.refresh_from_db() + return Response({ + 'code': 0, + 'msg': '支付成功', + 'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.zhuangtai} + }) + elif trade_state in ['NOTPAY', 'USERPAYING']: + # 用户尚未支付或支付中 + return Response({'code': 1, 'msg': '支付中', 'data': {'zhuangtai': 9}}) + else: + # 其他状态:CLOSED, REVOKED, PAYERROR 等,视为支付失败 + return Response({'code': 2, 'msg': '支付失败'}) + + except Exception as e: + logger.error(f"查询微信支付异常: {e}", exc_info=True) + return Response({'code': 1, 'msg': '查询失败,请稍后重试'}) + finally: + cache.delete(lock_key) + else: + # ---------- 6. 其他异常状态 ---------- + status_msg = { + 2: '订单已取消', + 3: '订单已完成', + 4: '退款审核中', + 5: '已退款', + 6: '退款失败', + 8: '结算中', + 0: '订单已作废', + }.get(dingdan.zhuangtai, '订单状态异常') + return Response({'code': 3, 'msg': status_msg, 'data': {'zhuangtai': dingdan.zhuangtai}}) + + except Exception as e: + logger.error(f"订单复查接口异常: {e}", exc_info=True) + return Response({'code': 99, 'msg': f'系统错误: {str(e)}'}) + + def _process_payment_success(self, dingdan, transaction_id, operator_id): + """ + 支付成功后的原子操作(与微信回调逻辑完全一致) + 在事务内完成所有状态更新和数据一致性操作 + """ + with transaction.atomic(): + # 重新锁定订单,防止并发更新 + dingdan = Dingdan.objects.select_for_update().get(pk=dingdan.pk) + if dingdan.zhuangtai != 9: + # 订单状态已被其他进程更新,直接返回(幂等) + return + + # 更新订单状态 + dingdan.zhuangtai = 7 if dingdan.zhiding_id and dingdan.zhiding_id.strip() else 1 + dingdan.wechat_transaction_id = transaction_id + dingdan.clkf = operator_id # 可选:记录操作人ID + dingdan.save() + + # 删除同一用户同一商品的其他未支付订单(逻辑删除,状态25) + Dingdan.objects.filter( + user1_id=dingdan.user1_id, + shangpin_id=dingdan.shangpin_id, + zhuangtai=9 + ).exclude(dingdan_id=dingdan.dingdan_id).update(zhuangtai=25) + + # 更新老板扩展表 + laoban_id = dingdan.pingtai_kuozhan.laoban_id if hasattr(dingdan, 'pingtai_kuozhan') else None + if laoban_id: + user_main = UserMain.objects.filter(yonghuid=laoban_id).first() + if user_main and hasattr(user_main, 'boss_profile'): + boss = user_main.boss_profile + boss.zonge = (boss.zonge or 0) + (dingdan.jine or 0) + boss.alldingdan = (boss.alldingdan or 0) + 1 + boss.save() + + # 更新商品库存 + if dingdan.shangpin_id: + Shangpin.objects.filter(id=dingdan.shangpin_id).update( + kucun=F('kucun') - 1 + ) + + + + # ========== 新增:商品和店铺每日统计 ========== + try: + # 商品每日统计(只要有商品ID就执行) + if dingdan.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(dingdan, 'pingtai_kuozhan'): + pingtai_shouyi = dingdan.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=dingdan.shangpin_id, + caozuo_leixing=1, # 1 = 下单 + dingdan_jiage=dingdan.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {dingdan.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + dianpu_id = None + if hasattr(dingdan, 'pingtai_kuozhan'): + dianpu_id = dingdan.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=1, # 1 = 下单 + dingdan_jiage=dingdan.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + + + + + + + + # 更新收支记录 + self._update_szjilu(dingdan.jine) + + # 更新每日收入统计 + update_daily_income(dingdan.jine) + + # 跨平台同步(复查成功也需要通知) + sync_order_to_partners(dingdan.dingdan_id) + + def _query_wechat_payment(self, out_trade_no): + """ + 调用微信支付订单查询接口,返回 (trade_state, transaction_id) + 若查询失败,抛出异常 + """ + appid = settings.WEIXIN_APPID + mch_id = settings.WEIXIN_MCHID + key = settings.WEIXIN_SHANGHUMIYAO + nonce_str = self._generate_nonce_str() + + params = { + 'appid': appid, + 'mch_id': mch_id, + 'out_trade_no': out_trade_no, + 'nonce_str': nonce_str, + } + + # 生成签名 + 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/pay/orderquery' + response = requests.post(url, data=xml_data, timeout=10) + result = xmltodict.parse(response.content)['xml'] + + if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': + return result.get('trade_state'), result.get('transaction_id') + else: + err_msg = result.get('return_msg', '未知错误') + raise Exception(f"微信订单查询失败: {err_msg}") + + def _update_szjilu(self, jine): + """更新收支记录表(ID=1的记录)""" + try: + jine_decimal = Decimal(str(jine)) + with transaction.atomic(): + szjilu, created = Szjilu.objects.select_for_update().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') + } + ) + if created: + logger.info("创建ID=1的收支记录") + szjilu.zongsy += jine_decimal + szjilu.zongls += jine_decimal + szjilu.jrls += jine_decimal + szjilu.save() + logger.info(f"收支记录更新成功,增加金额: {jine}") + except Exception as e: + logger.error(f"更新收支记录失败: {e}") + # 不抛出异常,因为收支记录不是核心流程,但记录错误供排查 + + def _generate_nonce_str(self): + """生成随机字符串,长度32""" + return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + def _dict_to_xml(self, data): + """将字典转换为XML字符串""" + xml = [''] + for k, v in data.items(): + xml.append(f'<{k}>{v}') + xml.append('') + 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): + """ + 创建订单并调用微信支付接口 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + # 1. 获取前端传递的字段 + shangpin_id = request.data.get('shangpin_id') + nicheng = request.data.get('nicheng', '').strip() + beizhu = request.data.get('beizhu', '').strip() + zhiding = request.data.get('zhiding', '').strip() + jine = request.data.get('jine', 0) + + # 2. 验证必填字段 + if not shangpin_id: + return Response({'code': 1, 'msg': '商品ID不能为空', 'data': None}) + + if not nicheng: + return Response({'code': 2, 'msg': '游戏昵称不能为空', 'data': None}) + + if len(nicheng) > 50: + return Response({'code': 3, 'msg': '游戏昵称过长', 'data': None}) + + # 3. 查询商品信息 + shangpin = Shangpin.objects.filter(id=shangpin_id).first() + if not shangpin: + return Response({'code': 4, 'msg': '商品不存在', 'data': None}) + + + is_valid, err_msg, _ = validate_shangpin_and_dianpu(shangpin_id) + if not is_valid: + return Response({'code': 5, 'msg': err_msg, 'data': None}) + + # 检查库存 + if hasattr(shangpin, 'kucun') and shangpin.kucun <= 0: + return Response({'code': 5, 'msg': '商品库存不足', 'data': None}) + + # 获取商品价格 + shangpin_jiage = getattr(shangpin, 'jiage', 0) + if not shangpin_jiage: + return Response({'code': 6, 'msg': '商品价格异常', 'data': None}) + #获取商品图片 + shangpin_tupian = getattr(shangpin, 'tupian_url', None) + + # 4. 验证指定打手(如果存在) + zhiding_id_value = None + if zhiding: + # 查询主表中是否存在该打手 + zhiding_user = UserMain.objects.filter( + yonghuid=zhiding, + #user_type='dashou' + ).first() + + if not zhiding_user: + return Response({'code': 7, 'msg': '指定打手不存在', 'data': None}) + + # 查询打手扩展表 + dashou_profile = UserDashou.objects.filter(user=zhiding_user).first() + + if not dashou_profile: + return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None}) + + # 检查在线状态 + if getattr(dashou_profile, 'zhanghaozhuangtai', 0) == 0: + return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None}) + + zhiding_id_value = zhiding + + # 5. 生成订单ID + timestamp = int(time.time()) + timestamp_ns = int(time.time_ns() // 1000) % 1000000 + random_num = random.randint(0, 99) + dingdanid = f"PT{timestamp:011d}{timestamp_ns:06d}{random_num:02d}" + + # 6. 查询利率并计算分成 + # 查询打手利率(字符'1') + dashou_lilu = Lilubiao.objects.filter(fadanpingtai='1').first() + + + # 🔥 新增:判断是否使用商品配置的额外分成 + if shangpin.kaioi_ewai_dashou_fencheng and shangpin.ewai_dashou_fencheng is not None: + # 使用商品指定的额外分成金额 + dashou_fencheng = shangpin.ewai_dashou_fencheng + else: + # 按原有逻辑计算打手分成 + dashou_fencheng = Decimal(str(shangpin_jiage)) * Decimal(str(dashou_lilu.lilu)) + + + + # 查询管事利率(字符'2') + #guanshi_lilu = Lilubiao.objects.filter(fadanpingtai='2').first() + + if not dashou_lilu: #or not guanshi_lilu: + return Response({'code': 9, 'msg': '系统利率配置不完整', 'data': None}) + + # 计算打手分成 + #dashou_fencheng = Decimal(str(shangpin_jiage)) * Decimal(str(dashou_lilu.lilu)) + + + + # 开始数据库事务 + with transaction.atomic(): + # 获取当前用户信息 + current_user = request.user + laoban_yonghuid = getattr(current_user, 'yonghuid', '') + user_openid = getattr(current_user, 'openid', '') + + if not user_openid: + return Response({'code': 10, 'msg': '用户openid不存在', 'data': None}) + + # 创建订单主表数据 + order_data = { + 'dingdan_id': dingdanid, + 'zhuangtai': 9, # 未付款状态 + 'jine': shangpin_jiage, + 'fadan_pingtai':1, + 'dashou_fencheng': dashou_fencheng, + 'zhiding_id': zhiding_id_value, + 'shangpin_id': shangpin_id, + 'yaoqiuleixing': getattr(shangpin, 'yaoqiuleixing', None), + 'huiyuan_id': getattr(shangpin, 'huiyuan_id', ''), + 'yongjin': getattr(shangpin, 'yongjin', 0), + 'jieshao': getattr(shangpin, 'jieshao', ''), + 'beizhu': beizhu, + 'nicheng': nicheng, + 'tupian':shangpin_tupian, + 'user1_id':f"Boss{request.user.yonghuid}", + 'leixing_id':shangpin.leixing_id + } + + # 创建订单记录 + + dingdan = Dingdan.objects.create(**order_data) + + + + + # CreateOrderView.post 事务内部,替换原有的扩展表创建代码 + + dianpu_id, pingtai_shouyi, dianpu_shouyi, shangji_dianpu_id, shangji_fenhong = calculate_pingtai_and_dianpu_shouyi( + shangpin_id=shangpin_id, + dingdan_jiage=shangpin_jiage, + dashou_fencheng=dashou_fencheng + ) + + pingtaikuozhan_data = { + 'dingdan': dingdan, + 'laoban_id': laoban_yonghuid, + 'pingtai_shouyi': pingtai_shouyi, + # ---------- 新增上级分红快照 ---------- + 'shangji_dianpu_id': shangji_dianpu_id, + 'shangji_fenhong': shangji_fenhong if shangji_fenhong else Decimal('0.00'), + } + if dianpu_id: + pingtaikuozhan_data['dianpu_id'] = dianpu_id + pingtaikuozhan_data['dianpu_shouyi'] = dianpu_shouyi + else: + pingtaikuozhan_data['dianpu_shouyi'] = Decimal('0.00') + + DingdanPingtai.objects.create(**pingtaikuozhan_data) + + + + + # 计算收益 + '''dianpu_id, pingtai_shouyi, dianpu_shouyi = calculate_pingtai_and_dianpu_shouyi( + shangpin_id=shangpin_id, + dingdan_jiage=shangpin_jiage, + dashou_fencheng=dashou_fencheng + ) + + # 构建扩展表数据,平台收益必定保存,店铺收益和店铺ID仅当dianpu_id存在时才赋值 + pingtaikuozhan_data = { + 'dingdan': dingdan, + 'laoban_id': laoban_yonghuid, + 'pingtai_shouyi': pingtai_shouyi, + } + if dianpu_id: + pingtaikuozhan_data['dianpu_id'] = dianpu_id + pingtaikuozhan_data['dianpu_shouyi'] = dianpu_shouyi + else: + # 无店铺时,店铺收益字段保持默认(0.00),也可显式设为0 + pingtaikuozhan_data['dianpu_shouyi'] = Decimal('0.00') + + DingdanPingtai.objects.create(**pingtaikuozhan_data)''' + + + + + # 7. 生成微信支付参数(向微信官方发送请求) + pay_params = self.generate_wechat_pay_params( + dingdanid=dingdanid, + jine=shangpin_jiage, + beizhu=getattr(settings, 'WEIXIN_PAY_DESCRIPTION'), + user_ip=self.get_client_ip(request), + openid=user_openid + ) + + if not pay_params: + # 手动抛出异常触发事务回滚 + raise Exception('支付参数生成失败') + + # 8. 返回成功响应 + response_data = { + 'code': 0, + 'msg': '订单创建成功', + 'data': { + 'dingdanid': dingdanid, + 'payParams': pay_params + } + } + + return Response(response_data) + + except Exception as e: + # 这里会自动回滚事务 + return Response({'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None}) + + 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] + else: + ip = request.META.get('REMOTE_ADDR') + return ip + + def generate_wechat_pay_params(self, dingdanid, jine, beizhu, user_ip, openid): + """ + 调用微信支付统一下单接口,生成支付参数 + 完全按照您提供的正确示例代码编写 + """ + try: + # 微信支付配置 + APPID = getattr(settings, 'WEIXIN_APPID', '') + MCHID = getattr(settings, 'WEIXIN_MCHID', '') + KEY = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') + NOTIFY_URL = getattr(settings, 'WEIXIN_NOTIFY_URL', '') + + if not all([APPID, MCHID, KEY, NOTIFY_URL]): + raise Exception('微信支付配置不完整') + + # 生成随机字符串 + import string + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + # 金额(单位:分) + total_fee = int(float(jine) * 100) + + # 交易类型 + trade_type = 'JSAPI' + + # 生成签名 + sign_data = { + 'appid': APPID, + 'mch_id': MCHID, + 'nonce_str': nonce_str, + 'body': beizhu, + 'out_trade_no': dingdanid, + 'total_fee': total_fee, + 'spbill_create_ip': user_ip, + 'notify_url': NOTIFY_URL, + 'trade_type': trade_type, + 'openid': openid + } + + # 按照参数名ASCII码从小到大排序 + 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}' + + # MD5签名 + sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + # 构造XML数据 + xml_data = f""" +{APPID} +{beizhu} +{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 + ) + + # 解析返回的XML + 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}' + + # 生成支付签名 + pay_sign_data = { + 'appId': APPID, + 'timeStamp': timestamp, + 'nonceStr': nonce_str, + 'package': package, + 'signType': 'MD5' + } + + 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() + + # 返回支付参数(格式与前端完全匹配) + result = { + 'timeStamp': timestamp, + 'nonceStr': nonce_str, + 'package': package, + 'signType': 'MD5', + 'paySign': pay_sign + } + + return result + else: + # 获取详细的错误信息 + err_code = root.find('err_code') + err_code_des = root.find('err_code_des') + return_msg = root.find('return_msg') + + 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}, " + if return_msg is not None: + error_details += f"return_msg: {return_msg.text}" + + raise Exception(f"微信支付下单失败: {error_details}") + + except Exception as e: + raise e + + + + + +'''# dingdan/views.py +import xml.etree.ElementTree as ET +from django.db import transaction +from django.http import HttpResponse +from rest_framework.views import APIView +from rest_framework import permissions + +from shangpin.models import Shangpin + +from yonghu.models import UserMain, UserBoss + +# views/pay_views.py - WechatPayNotifyView完整代码 +import xml.etree.ElementTree as ET +from django.db import transaction +from django.http import HttpResponse +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 peizhi.models import Szjilu +import logging + +logger = logging.getLogger(__name__) + + +import threading +from utils.weixin_broadcast import WeixinBroadcastSender + +class WechatPayNotifyView(APIView): + """微信支付回调接口 - 接收微信官方支付结果通知""" + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + + def post(self, request): + try: + # 1. 解析微信回调的XML数据 + xml_data = request.body.decode('utf-8') + root = ET.fromstring(xml_data) + + # 2. 获取回调参数 + return_code = root.find('return_code').text + result_code = root.find('result_code').text + out_trade_no = root.find('out_trade_no').text # 订单ID + + # 3. 验证签名(重要安全步骤) + if not self.verify_signature(request.body): + response_xml = self.generate_response_xml('FAIL', '签名验证失败') + return HttpResponse(response_xml, content_type='application/xml') + + # 4. 检查支付结果 + if return_code == 'SUCCESS' and result_code == 'SUCCESS': + # 支付成功,处理订单 + self.handle_payment_success(out_trade_no) + response_xml = self.generate_response_xml('SUCCESS', 'OK') + return HttpResponse(response_xml, content_type='application/xml') + else: + # 支付失败,删除订单 + self.handle_payment_failure(out_trade_no) + response_xml = self.generate_response_xml('SUCCESS', 'OK') + return HttpResponse(response_xml, content_type='application/xml') + + except Exception as e: + logger.error(f"支付回调处理异常: {str(e)}", exc_info=True) + response_xml = self.generate_response_xml('FAIL', f'处理失败: {str(e)}') + return HttpResponse(response_xml, content_type='application/xml') + + def verify_signature(self, xml_data): + """验证微信回调签名""" + try: + # 这里需要实现微信支付签名验证逻辑 + # 根据你的微信支付配置实现具体验证 + return True # 暂时返回True,你需要根据实际情况实现 + except: + return False + + def generate_response_xml(self, return_code, return_msg): + """生成返回给微信的XML响应""" + return f""" + + +""" + + @transaction.atomic + def handle_payment_success(self, dingdan_id): + """处理支付成功的订单""" + try: + # 1. 查询订单信息 + dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + if not dingdan: + raise Exception('订单不存在') + + # 2. 检查订单状态是否为9(未支付) + if dingdan.zhuangtai != 9: + return + + # 3. 查询订单扩展表获取老板ID + pingtaikuozhan = DingdanPingtai.objects.filter(dingdan=dingdan).first() + if not pingtaikuozhan: + raise Exception('订单扩展表不存在') + + laoban_id = pingtaikuozhan.laoban_id + + # 4. 根据是否指定打手更新订单状态 + if dingdan.zhiding_id and dingdan.zhiding_id.strip(): + dingdan.zhuangtai = 7 + else: + dingdan.zhuangtai = 1 + + dingdan.save() + + # 5. 更新老板扩展表 + user_main = UserMain.objects.filter(yonghuid=laoban_id).first() + if user_main: + boss_profile = UserBoss.objects.filter(user=user_main).first() + if boss_profile: + boss_profile.zonge = (boss_profile.zonge or 0) + (dingdan.jine or 0) + boss_profile.alldingdan = (boss_profile.alldingdan or 0) + 1 + boss_profile.save() + + # 6. 更新商品库存和销量 + shangpin_id = dingdan.shangpin_id + if shangpin_id: + shangpin = Shangpin.objects.filter(id=shangpin_id).first() + if shangpin: + current_kucun = getattr(shangpin, 'kucun', 0) + if current_kucun > 0: + shangpin.kucun = current_kucun - 1 + shangpin.save() + + jine = dingdan.jine + self.update_szjilu(jine) + + + + 🟢【7. 新增】支付成功后的广播通知(异步,不阻塞主流程) + from dingdan.tongzhi_tasks import dingdan_guangbo + + # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用) + """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)""" # 瞬间返回,不阻塞请求 + + # 🟢【7. 新增】支付成功后的广播通知(异步,不阻塞主流程) + #self.broadcast_order_notification_async(dingdan) + + except Exception as e: + raise Exception(f'处理支付成功订单失败: {str(e)}') + + def broadcast_order_notification_async(self, dingdan): + """异步广播订单通知给所有关注用户""" + try: + def broadcast_task(): + 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[:50] if dingdan.jieshao else '新订单' + } + + # 调用广播器 + 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=broadcast_task) + thread.daemon = True + thread.start() + + except Exception as e: + logger.error(f"启动广播线程异常: {str(e)}") + + # 🗑️【重要】原 send_notification_to_dashou 和 send_notification_to_all_dashous 方法不再需要 + # 可以注释掉或删除,因为现在统一用 broadcast_order_notification_async + + def get_game_type_name(self, leixing_id): + """获取游戏类型名称(保持不变)""" + try: + game_type = ShangpinLeixing.objects.filter(id=leixing_id).first() + return game_type.jieshao if game_type else '游戏订单' + except Exception: + return '游戏订单' + + def update_szjilu(self, jine): + """更新收支记录表(保持不变)""" + try: + from decimal import Decimal + jine_decimal = Decimal(str(jine)) if not isinstance(jine, Decimal) else jine + + with transaction.atomic(): + try: + szjilu = Szjilu.objects.select_for_update().get(id=1) + except Szjilu.DoesNotExist: + szjilu = Szjilu.objects.create( + id=1, + zongsy=Decimal('0.00'), + zongls=Decimal('0.00'), + zongzc=Decimal('0.00'), + jrzc=Decimal('0.00'), + jrls=Decimal('0.00') + ) + logger.info(f"创建ID=1的收支记录") + + szjilu.zongsy = szjilu.zongsy + jine_decimal + szjilu.zongls = szjilu.zongls + jine_decimal + szjilu.jrls = szjilu.jrls + jine_decimal + szjilu.save() + + logger.info(f"收支记录更新成功: 订单金额={jine}") + + except Exception as e: + logger.error(f"更新收支记录失败: {str(e)}") + + @transaction.atomic + def handle_payment_failure(self, dingdan_id): + """处理支付失败的订单(删除订单)(保持不变)""" + try: + dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + if not dingdan: + return + + DingdanPingtai.objects.filter(dingdan=dingdan).delete() + dingdan.delete() + + except Exception as e: + raise Exception(f'删除支付失败订单失败: {str(e)}')''' + + + + + +# dingdan/views.py +'''class PaymentFailView(APIView): + """ + 前端支付失败接口 - 处理用户主动取消或支付失败 + 需要token认证 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + # 1. 获取前端传递的字段 + dingdanid = request.data.get('dingdanid') + + + # 2. 验证必填字段 + if not dingdanid: + return Response({ + 'code': 1, + 'msg': '订单ID不能为空', + 'data': None + }) + + # 3. 查询订单 + dingdan = Dingdan.objects.filter(dingdan_id=dingdanid).first() + if not dingdan: + return Response({ + 'code': 2, + 'msg': '订单不存在', + 'data': None + }) + + # 4. 删除订单数据 + with transaction.atomic(): + # 删除订单扩展表 + DingdanPingtai.objects.filter(dingdan=dingdan).delete() + + # 删除订单主表 + dingdan.delete() + + # 5. 返回成功响应(与前端格式匹配) + return Response({ + 'code': 0, + 'msg': '订单已删除', + 'data': None + }) + + except Exception as e: + return Response({ + 'code': 99, + 'msg': f'删除订单失败: {str(e)}', + 'data': None + })''' + + +# dingdan/views.py +'''class PaymentVerifyView(APIView): + """ + 订单支付成功复查接口 - 前端支付成功后调用,确认订单状态 + 需要token认证 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + # 1. 获取前端传递的订单ID + + dingdanid = request.data.get('dingdanid') + + + # 2. 验证必填字段 + if not dingdanid: + return Response({ + 'code': 1, + 'msg': '订单ID不能为空', + 'data': None + }) + + # 3. 查询订单 + dingdan = Dingdan.objects.filter(dingdan_id=dingdanid).first() + if not dingdan: + return Response({ + 'code': 2, + 'msg': '订单不存在', + 'data': None + }) + + # 4. 检查订单状态 + zhuangtai = dingdan.zhuangtai + + if zhuangtai in [1, 7]: + # 订单状态正常(1:待接单, 7:已指定打手待接单) + return Response({ + 'code': 0, + 'msg': '订单状态正常', + 'data': { + 'dingdanid': dingdanid, + 'zhuangtai': zhuangtai + } + }) + else: + # 订单状态异常 + error_msg = '订单状态异常,请联系管理员或客服' + + # 可以根据具体状态给出更详细的提示 + if zhuangtai == 9: + error_msg = '订单未支付,请重新支付' + elif zhuangtai == 2: + error_msg = '订单已取消' + elif zhuangtai == 3: + error_msg = '订单已完成' + # 其他状态... + + return Response({ + 'code': 3, + 'msg': error_msg, + 'data': { + 'dingdanid': dingdanid, + 'zhuangtai': zhuangtai + } + }) + + except Exception as e: + return Response({ + 'code': 99, + 'msg': f'订单复查失败: {str(e)}', + 'data': None + })''' + + + +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): + """订单获取接口""" + permission_classes = [IsAuthenticated] + + def post(self, request): + # 获取当前登录用户 + try: + current_user = request.user + yonghuid = current_user.yonghuid + except Exception as e: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }) + + # 获取前端参数 + try: + data = request.data + zhuangtai_list = data.get('zhuangtai_list', []) + page = int(data.get('page', 1)) + page_size = int(data.get('page_size', 10)) + except Exception as e: + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }) + + # 参数验证 + if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0: + return Response({ + 'code': 3, + 'msg': '状态列表不能为空', + 'data': None + }) + + if page < 1: + page = 1 + if page_size <= 0: + page_size = 10 + + try: + # 查询扩展表,获取用户的所有订单ID + pingtai_orders = DingdanPingtai.objects.filter(laoban_id=yonghuid) + pingtai_order_ids = list(pingtai_orders.values_list('dingdan__dingdan_id', flat=True)) + + if not pingtai_order_ids: + return Response({ + 'code': 0, + 'msg': '成功', + 'data': { + 'list': [], + 'has_more': False, + 'total': 0 + } + }) + + # 查询主表 + status_query = Q() + if zhuangtai_list != [1, 2, 3, 4, 5, 6, 7, 8]: + status_query = Q(zhuangtai__in=zhuangtai_list) + + query = Q(dingdan_id__in=pingtai_order_ids) & status_query + orders = Dingdan.objects.filter(query).order_by('-create_time') + #total_orders = orders.count() + + # 手动分页逻辑 + start_index = (page - 1) * page_size + end_index = start_index + page_size + + # 获取当前页的数据 + current_page_orders = orders[start_index:end_index] + + # 构建返回数据 + order_list = [] + for order in current_page_orders: + order_data = { + 'dingdanId': order.dingdan_id, + 'zhuangtai': order.zhuangtai, + 'jine': float(order.jine) if order.jine else 0.0, + 'tupian': order.tupian or '', + 'jieshao': order.jieshao or '', + 'beizhu': order.beizhu or '', + 'nicheng': order.nicheng or '', + 'chuangjianshijian': order.create_time.strftime('%Y-%m-%d %H:%M:%S') + } + order_list.append(order_data) + + + # 判断是否还有更多数据 + current_page_count = len(current_page_orders) + has_more = current_page_count == page_size + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': { + 'list': order_list, + 'has_more': has_more, + #'total': total_orders + } + }) + + except Exception as e: + return Response({ + 'code': 4, + 'msg': '服务器内部错误', + 'data': None + }) + + +# 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): + """订单详情接口""" + permission_classes = [IsAuthenticated] + + def post(self, request): + # 获取当前登录用户 + try: + current_user = request.user + yonghuid = current_user.yonghuid + except Exception as e: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }) + + # 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdanId', '') + except Exception as e: + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }) + + # 参数验证 + if not dingdan_id: + return Response({ + 'code': 3, + 'msg': '订单ID不能为空', + 'data': None + }) + + try: + # 1. 验证订单是否属于当前用户 + # 查询平台订单扩展表,检查laoban_id是否等于当前用户 + pingtai_kuozhan = DingdanPingtai.objects.filter( + laoban_id=yonghuid, + dingdan__dingdan_id=dingdan_id + ).first() + + if not pingtai_kuozhan: + # 如果不是平台订单,检查商家订单 + from .models import DingdanShangjia + shangjia_kuozhan = DingdanShangjia.objects.filter( + shangjia_id=yonghuid, + dingdan__dingdan_id=dingdan_id + ).first() + + if not shangjia_kuozhan: + return Response({ + 'code': 4, + 'msg': '订单不存在或无权限查看', + 'data': None + }) + + # 2. 查询订单主表 + dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + + if not dingdan: + return Response({ + 'code': 5, + 'msg': '订单不存在', + 'data': None + }) + + # 3. 获取订单信息 + zhuangtai = dingdan.zhuangtai or 0 + zhiding_id = dingdan.zhiding_id or '' + jiedan_dashou_id = dingdan.jiedan_dashou_id or '' + dashou_liuyan = dingdan.dashou_liuyan or '' + + # 4. 查询打手交付图片 + dashoutupian_records = Dashoutupian.objects.filter( + dingdan_id=dingdan_id + ).order_by('create_time')[:9] # 最多取9条 + + dashoujiaofu_list = [] + for record in dashoutupian_records: + if record.tupian: + dashoujiaofu_list.append(record.tupian) + + # 5. 构建返回数据(字段名与前端对应) + response_data = { + 'zhuangtai': zhuangtai, # 订单状态 + 'zhiding': zhiding_id, # 指定打手ID + 'dashou': jiedan_dashou_id, # 服务打手ID + 'dashouliuyan': dashou_liuyan, # 打手留言 + 'dashoujiaofu': dashoujiaofu_list # 打手交付图片列表 + } + + # 6. 返回成功响应 + return Response({ + 'code': 0, + 'msg': '成功', + 'data': response_data + }) + + except Exception as e: + print(f"订单详情查询失败: {str(e)}") + return Response({ + 'code': 6, + 'msg': '服务器内部错误', + 'data': None + }) + + + + + + +class JiedanView(APIView): + """结单接口""" + permission_classes = [IsAuthenticated] + + def post(self, request): + # 获取当前登录用户 + try: + current_user = request.user + yonghuid = current_user.yonghuid + except Exception as e: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }) + + # 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdanId', '') + pingfen = int(data.get('pingfen', 0)) # 评分,默认0 + liuyan = data.get('liuyan', '') # 留言,默认空 + except Exception as e: + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }) + + # 参数验证 + if not dingdan_id: + return Response({ + 'code': 3, + 'msg': '订单ID不能为空', + 'data': None + }) + + # 验证评分范围 + if pingfen < 0 or pingfen > 5: + return Response({ + 'code': 4, + 'msg': '评分必须在0-5分之间', + 'data': None + }) + + # 使用事务确保数据一致性 + try: + with transaction.atomic(): + # 1. 验证订单是否存在且属于当前用户 + pingtai_kuozhan = DingdanPingtai.objects.filter( + laoban_id=yonghuid, + dingdan__dingdan_id=dingdan_id + ).select_related('dingdan').first() + + if not pingtai_kuozhan: + return Response({ + 'code': 5, + 'msg': '订单不存在或无权限操作', + 'data': None + }) + + dingdan = pingtai_kuozhan.dingdan + + # 2. 验证订单状态(必须是结算中 - 状态8) + if dingdan.zhuangtai != 8: + return Response({ + 'code': 6, + 'msg': '订单不在结算状态,无法结单', + 'data': None + }) + + # 3. 获取订单相关数据 + dashou_fencheng = dingdan.dashou_fencheng or Decimal('0.00') # 打手分成 + jiedan_dashou_id = dingdan.jiedan_dashou_id or '' # 接单打手ID + shangpin_id = dingdan.shangpin_id # 商品ID + + # 4. 更新平台扩展表的老板评价(如果有留言) + if liuyan: + pingtai_kuozhan.laoban_pingjia = liuyan + pingtai_kuozhan.save() + + # 5. 如果有评分,更新评分表 + if pingfen > 0 and jiedan_dashou_id: + # 使用filter查询,更健壮 + pingfen_records = Pingfen.objects.filter(dashou=jiedan_dashou_id) + + if pingfen_records.exists(): + # 已有记录,更新 + pingfen_record = pingfen_records.first() + pingfen_record.zongfen += Decimal(str(pingfen)) + pingfen_record.cishu += 1 + pingfen_record.save() + else: + # 新记录,创建 + Pingfen.objects.create( + dashou=jiedan_dashou_id, + zongfen=Decimal(str(pingfen)), + cishu=1 + ) + + # 6. 更新商品真实销量(如果有商品ID) + if shangpin_id: + shangpin_records = Shangpin.objects.filter(id=shangpin_id) + + if shangpin_records.exists(): + shangpin = shangpin_records.first() + # 检查字段是否存在 + if hasattr(shangpin, 'zhenshi_xiaoliang'): + shangpin.zhenshi_xiaoliang = (shangpin.zhenshi_xiaoliang or 0) + 1 + shangpin.save() + + # ========== 新增:跨平台我方派单且有对方信息,直接通知对方并跳过本地结算 ========== + if (dingdan.is_cross == 1 + and dingdan.partner_order_id + and dingdan.partner_club_id): + # 更新订单状态为已完成 + dingdan.zhuangtai = 3 + dingdan.clkf = current_user.phone if hasattr(current_user, 'phone') else yonghuid + dingdan.save() + + # 事务提交后异步通知对方平台 + transaction.on_commit( + lambda: self._notify_partner_jiedan( + my_dingdan_id=dingdan.dingdan_id, + partner_club_id=dingdan.partner_club_id, + partner_order_id=dingdan.partner_order_id, + new_status=3, + current_user=current_user + ) + ) + + # 直接返回,跳过后续所有本地打手结算、评价、统计等 + return Response({ + 'code': 0, + 'msg': '结单成功(已通知对方平台)', + 'data': None + }) + # ==================================================================== + + # 7. 更新打手扩展表(如果有打手ID和分成) + if jiedan_dashou_id and dashou_fencheng > 0: + # 查询打手用户 + dashou_user = UserMain.objects.filter(yonghuid=jiedan_dashou_id).first() + + if dashou_user: + # 查询打手扩展表 + dashou_kuozhan = UserDashou.objects.filter(user=dashou_user).first() + + if dashou_kuozhan: + # 更新四个金额字段 + dashou_kuozhan.zonge = (dashou_kuozhan.zonge or Decimal('0.00')) + dashou_fencheng + dashou_kuozhan.yue = (dashou_kuozhan.yue or Decimal('0.00')) + dashou_fencheng + dashou_kuozhan.jinrishouyi = (dashou_kuozhan.jinrishouyi or Decimal( + '0.00')) + dashou_fencheng + dashou_kuozhan.jinyueshouyi = (dashou_kuozhan.jinyueshouyi or Decimal( + '0.00')) + dashou_fencheng + + # 成交总量加1 + dashou_kuozhan.chengjiaozongliang = (dashou_kuozhan.chengjiaozongliang or 0) + 1 + + # 状态改为数字1 + dashou_kuozhan.zhuangtai = 1 + + dashou_kuozhan.save() + + # 8. 更新订单状态为已完成(状态3) + dingdan.zhuangtai = 3 + dingdan.save() + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=jiedan_dashou_id, + amount=dashou_fencheng, + action=2 # 2 表示成交(结算) + ) + logger.info(f"打手 {jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + + + # ========== 新增:商品和店铺每日统计 ========== + try: + # 商品每日统计(只要有商品ID就执行) + if dingdan.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(dingdan, 'pingtai_kuozhan'): + pingtai_shouyi = dingdan.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=dingdan.shangpin_id, + caozuo_leixing=2, # 2 = 结单 + dingdan_jiage=dingdan.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {dingdan.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + + + # ========== 修改为 ========== + # 店铺每日统计(包含上级分红) + dianpu_id = None + shangji_id = None + shangji_fen = Decimal('0.00') + if hasattr(dingdan, 'pingtai_kuozhan'): + dianpu_id = dingdan.pingtai_kuozhan.dianpu_id + shangji_id = dingdan.pingtai_kuozhan.shangji_dianpu_id + shangji_fen = dingdan.pingtai_kuozhan.shangji_fenhong or Decimal('0.00') + if dianpu_id: + dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=2, # 结算 + dingdan_jiage=dingdan.jine, + dianpu_shouyi=dianpu_shouyi, + shangji_dianpu_id=shangji_id, # 新增:上级店铺ID + shangji_fenhong=shangji_fen # 新增:该单给上级的分红 + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}, 上级ID: {shangji_id}, 上级分红: {shangji_fen}") + + + + + '''dianpu_id = None + if hasattr(dingdan, 'pingtai_kuozhan'): + dianpu_id = dingdan.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=2, # 2 = 结单 + dingdan_jiage=dingdan.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")''' + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + # 12. 更新平台收益流水(仅在平台发单时) + if dingdan.fadan_pingtai == 1: + try: + from dingdan.utils import update_platform_profit # 导入函数 + update_platform_profit(dingdan_id) + except Exception as e: + # 平台收益更新失败不影响主流程,但记录日志 + logger.error(f"强制结单后更新平台收益失败: {str(e)}") + + # 9. 返回成功响应 + return Response({ + 'code': 0, + 'msg': '结单成功', + 'data': None + }) + + except Exception as e: + + # 事务会自动回滚 + return Response({ + 'code': 7, + 'msg': '结单失败,服务器内部错误', + 'data': None + }) + + + def _notify_partner_jiedan(self, my_dingdan_id, partner_club_id, partner_order_id, new_status, current_user): + """ + 通知对方俱乐部我方订单已完成 + 参照 CrossPlatformForceCompleteView._notify_partner_order_status 实现 + """ + try: + # 1. 获取我方俱乐部ID + from .models import CrossPlatformOrderData + 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 + + # 2. 查询对方服务器域名 + 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 + + # 3. 构造通知请求 + url = partner_domain.rstrip('/') + '/houtai/dfddtk' + payload = { + 'order_id': partner_order_id, # 在对方看来这是他们的订单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: + # 4. 更新跨平台订单扩展表(记录对方状态) + CrossPlatformOrderData.objects.update_or_create( + dingdan_id=my_dingdan_id, # 我方订单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') + } + ) + 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)}") + + + + + + + +from dengji.models import DashouBiaoxian +from dengji.services import record_dashou_biaoxian + + + + + + +class DingdanXiangqingView2(APIView): + """订单详情接口(老板端)- 返回完整评价信息""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghuid = request.user.yonghuid + except Exception: + return Response({'code': 1, 'msg': '用户认证失败', 'data': None}) + + dingdan_id = request.data.get('dingdanId', '') + if not dingdan_id: + return Response({'code': 2, 'msg': '订单ID不能为空', 'data': None}) + + try: + # 权限验证 + pingtai_ext = DingdanPingtai.objects.filter( + laoban_id=yonghuid, + dingdan__dingdan_id=dingdan_id + ).select_related('dingdan').first() + + if not pingtai_ext: + return Response({'code': 3, 'msg': '订单不存在或无权限查看', 'data': None}) + + order = pingtai_ext.dingdan + zhuangtai = order.zhuangtai or 0 + jiedan_dashou_id = order.jiedan_dashou_id or '' + dashou_liuyan = order.dashou_liuyan or '' + update_time = order.update_time.strftime('%Y-%m-%d %H:%M:%S') if order.update_time else '' + + # 交付图片 + dashoujiaofu_list = list( + Dashoutupian.objects.filter(dingdan_id=dingdan_id) + .order_by('create_time')[:9] + .values_list('tupian', flat=True) + ) + + # 打手信息 + dashou_nicheng = '' + dashou_touxiang = '' + jiedan_time = '' + tijiao_time = '' + if jiedan_dashou_id: + dashou_user = UserMain.objects.filter(yonghuid=jiedan_dashou_id).first() + if dashou_user: + dashou_touxiang = dashou_user.avatar or '' + try: + dashou_nicheng = dashou_user.dashou_profile.nicheng or '' + except UserDashou.DoesNotExist: + pass + biaoxian = DashouBiaoxian.objects.filter( + dingdan_id=dingdan_id, + dashou_id=jiedan_dashou_id + ).first() + if biaoxian: + if biaoxian.jiedan_time: + jiedan_time = biaoxian.jiedan_time.strftime('%Y-%m-%d %H:%M:%S') + if biaoxian.tijiao_time: + tijiao_time = biaoxian.tijiao_time.strftime('%Y-%m-%d %H:%M:%S') + + # 老板自己的评价(如果已经结算过,可能已有评价) + laoban_pingjia = pingtai_ext.laoban_pingjia or '' + shangxian_sudu = None + fuwu_taidu = None + fanche_cishu = None + if jiedan_dashou_id and zhuangtai in [3, 4, 5, 6]: # 已完成或退款相关状态才可能有评价 + biaoxian = DashouBiaoxian.objects.filter( + dingdan_id=dingdan_id, + dashou_id=jiedan_dashou_id + ).first() + if biaoxian: + shangxian_sudu = biaoxian.shangxian_sudu + fuwu_taidu = biaoxian.fuwu_taidu + fanche_cishu = biaoxian.fanche_cishu + + data = { + 'zhuangtai': zhuangtai, + 'dashou': jiedan_dashou_id, + 'dashou_nicheng': dashou_nicheng, + 'dashou_touxiang': dashou_touxiang, + 'dashouliuyan': dashou_liuyan, + 'dashoujiaofu': dashoujiaofu_list, + 'update_time': update_time, + 'jiedan_time': jiedan_time, + 'tijiao_time': tijiao_time, + 'laoban_pingjia': laoban_pingjia, + 'shangxian_sudu': shangxian_sudu, # 1-5 或 null + 'fuwu_taidu': fuwu_taidu, # 1-5 或 null + 'fanche_cishu': fanche_cishu # int 或 null + } + + return Response({'code': 0, 'msg': '成功', 'data': data}) + + except Exception as e: + logger.error(f"订单详情查询异常: {e}", exc_info=True) + return Response({'code': 4, 'msg': '服务器内部错误', 'data': None}) + + + +class JiedanView2(APIView): + """结单接口(老板端)- 接收全新评价字段并写入表现表""" + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 用户认证 + try: + current_user = request.user + yonghuid = current_user.yonghuid + except Exception: + return Response({'code': 1, 'msg': '用户认证失败', 'data': None}) + + # 2. 获取参数(字段名与前端完全一致) + try: + data = request.data + dingdan_id = data.get('dingdanId', '') + fanche_cishu = data.get('fanche_cishu') # 前端传的字符串或数字,后端转为整数 + shangxian_sudu = data.get('shangxian_sudu') + fuwu_taidu = data.get('fuwu_taidu') + liuyan = data.get('liuyan', '') + except Exception: + return Response({'code': 2, 'msg': '参数格式错误', 'data': None}) + + # 参数校验 + if not dingdan_id: + return Response({'code': 3, 'msg': '订单ID不能为空', 'data': None}) + + # 转换并校验必填评价字段 + try: + fanche_cishu = int(fanche_cishu) if fanche_cishu is not None else None + shangxian_sudu = int(shangxian_sudu) if shangxian_sudu is not None else None + fuwu_taidu = int(fuwu_taidu) if fuwu_taidu is not None else None + except (ValueError, TypeError): + return Response({'code': 4, 'msg': '评价参数必须为整数', 'data': None}) + + if fanche_cishu is None: + return Response({'code': 5, 'msg': '翻车次数不能为空', 'data': None}) + if not (1 <= shangxian_sudu <= 5 and 1 <= fuwu_taidu <= 5): + return Response({'code': 6, 'msg': '评价分数必须在1-5之间', 'data': None}) + + # 使用事务保证数据一致性 + try: + with transaction.atomic(): + # 3. 权限验证:订单属于当前老板 + pingtai_ext = DingdanPingtai.objects.filter( + laoban_id=yonghuid, + dingdan__dingdan_id=dingdan_id + ).select_related('dingdan').first() + + if not pingtai_ext: + return Response({'code': 7, 'msg': '订单不存在或无权限操作', 'data': None}) + + order = pingtai_ext.dingdan + + # 4. 状态校验:必须是结算中(状态8) + if order.zhuangtai != 8: + return Response({'code': 8, 'msg': '订单不在结算状态', 'data': None}) + + # 5. 提取订单关键数据 + dashou_fencheng = order.dashou_fencheng or Decimal('0.00') + jiedan_dashou_id = order.jiedan_dashou_id or '' + shangpin_id = order.shangpin_id + + # 6. 保存老板评价(兼容旧逻辑) + if liuyan: + pingtai_ext.laoban_pingjia = liuyan + pingtai_ext.save() + + # 7. 更新评分表(用服务态度分作为综合评分,保留原有统计) + if jiedan_dashou_id and fuwu_taidu: + pingfen_records = Pingfen.objects.filter(dashou=jiedan_dashou_id) + if pingfen_records.exists(): + pf = pingfen_records.first() + pf.zongfen += Decimal(str(fuwu_taidu)) + pf.cishu += 1 + pf.save() + else: + Pingfen.objects.create( + dashou=jiedan_dashou_id, + zongfen=Decimal(str(fuwu_taidu)), + cishu=1 + ) + + # 8. 更新商品真实销量 + if shangpin_id: + shangpin_qs = Shangpin.objects.filter(id=shangpin_id) + if shangpin_qs.exists(): + sp = shangpin_qs.first() + if hasattr(sp, 'zhenshi_xiaoliang'): + sp.zhenshi_xiaoliang = (sp.zhenshi_xiaoliang or 0) + 1 + sp.save() + + # 9. 更新打手收益与状态 + if jiedan_dashou_id and dashou_fencheng > 0: + dashou_user = UserMain.objects.filter(yonghuid=jiedan_dashou_id).first() + if dashou_user: + try: + dashou_ext = dashou_user.dashou_profile + dashou_ext.zonge = (dashou_ext.zonge or Decimal('0.00')) + dashou_fencheng + dashou_ext.yue = (dashou_ext.yue or Decimal('0.00')) + dashou_fencheng + dashou_ext.jinrishouyi = (dashou_ext.jinrishouyi or Decimal('0.00')) + dashou_fencheng + dashou_ext.jinyueshouyi = (dashou_ext.jinyueshouyi or Decimal('0.00')) + dashou_fencheng + dashou_ext.chengjiaozongliang = (dashou_ext.chengjiaozongliang or 0) + 1 + dashou_ext.zhuangtai = 1 + dashou_ext.save() + except UserDashou.DoesNotExist: + pass + + # 10. 更新订单状态为已完成 + order.zhuangtai = 3 + order.save() + + # 11. 打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=jiedan_dashou_id, + amount=dashou_fencheng, + action=2 # 2=结算/成交 + ) + except Exception as e: + logger.error(f"打手每日统计失败: {e}") + + # 12. 商品/店铺每日统计 + try: + if 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=shangpin_id, + caozuo_leixing=2, + dingdan_jiage=order.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + + # 店铺统计(含上级分红) + if hasattr(order, 'pingtai_kuozhan'): + dianpu_id = order.pingtai_kuozhan.dianpu_id + shangji_id = order.pingtai_kuozhan.shangji_dianpu_id + shangji_fen = order.pingtai_kuozhan.shangji_fenhong or Decimal('0.00') + if dianpu_id: + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=2, + dingdan_jiage=order.jine, + dianpu_shouyi=dianpu_shouyi, + shangji_dianpu_id=shangji_id, + shangji_fenhong=shangji_fen + ) + except Exception as e: + logger.error(f"商品/店铺统计失败: {e}") + + # 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}") + + # 14. 🆕 写入打手表现表(结算评价) + if jiedan_dashou_id: + record_dashou_biaoxian( + dingdan_id=dingdan_id, + dashou_id=jiedan_dashou_id, + xingwei=3, # 3=结算 + fanche_cishu=fanche_cishu, + shangxian_sudu=shangxian_sudu, + fuwu_taidu=fuwu_taidu + # bankuai 暂不传入,后续可从订单商品关联 + ) + + # 15. 返回成功 + return Response({'code': 0, 'msg': '结单成功', 'data': None}) + + except Exception as e: + logger.error(f"结单事务异常: {e}", exc_info=True) + 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): + """ + 获取商家可用的商品类型列表(含关联板块的称号) + POST /dingdan/sjspleixing + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + + try: + # 1. 查询审核通过且抢单要求类型为1的商品类型,按排序权重升序 + shangpin_queryset = ShangpinLeixing.objects.filter( + yaoqiuleixing=1, + shenhezhuangtai=1 + ).select_related('bankuai').order_by('paixu') + + # 2. 收集所有涉及到的板块ID + bankuai_ids = shangpin_queryset.values_list('bankuai_id', flat=True).distinct() + + # 3. 查询这些板块下的所有称号(每个称号绑定到一个板块) + chenghao_list = Chenghao.objects.filter( + bankuai_id__in=bankuai_ids + ).values('id', 'mingcheng', 'texiao_miaoshu', 'bankuai_id') + + # 4. 构建 板块ID -> 称号列表 的映射(确保准确关联) + bankuai_chenghao_map = {} + for ch in chenghao_list: + bid = ch['bankuai_id'] + if bid not in bankuai_chenghao_map: + bankuai_chenghao_map[bid] = [] + bankuai_chenghao_map[bid].append({ + 'id': ch['id'], + 'mingcheng': ch['mingcheng'], + 'texiao_json': ch['texiao_miaoshu'] # 前端 chenghao-tag 组件需要 + }) + + # 5. 组装最终数据 + data_list = [] + for sp in shangpin_queryset: + item = { + 'id': sp.id, + 'yaoqiuleixing': sp.yaoqiuleixing, + 'huiyuan_id': sp.huiyuan_id, + 'jieshao': sp.jieshao, + # 当前类型所属板块的称号列表(无板块则为空数组) + 'chenghaoList': bankuai_chenghao_map.get(sp.bankuai_id, []) + } + data_list.append(item) + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': data_list + }) + + 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 + + + + + +'''class ShangjiaPaifaView(APIView): + """商家派单接口""" + permission_classes = [IsAuthenticated] + + def post(self, request): + # 记录请求日志 + logger.info(f"商家派单请求: user_id={request.user.yonghuid}, data={request.data}") + + # 1. 获取前端参数 + data = request.data + shangpin_type_id = data.get('shangpinTypeId') + huiyuan_id = data.get('huiyuanId') + yaoqiuleixing = data.get('yaoqiuleixing') + dingdan_jieshao = data.get('dingdanJieshao', '').strip() + dingdan_beizhu = data.get('dingdanBeizhu', '').strip() + laoban_name = data.get('laobanName', '').strip() + zhiding_uid = data.get('zhidingUid', '').strip() + jiage_str = data.get('jiage') + + # 2. 基本参数验证 + if not all([shangpin_type_id, huiyuan_id, dingdan_jieshao, laoban_name, jiage_str]): + logger.warning(f"参数缺失: {request.data}") + return Response({'code': 400, 'msg': '缺少必填参数', 'data': None}, status=status.HTTP_400_BAD_REQUEST) + + try: + jiage = float(jiage_str) + if jiage < 1 or jiage > 10000: + return Response({'code': 400, 'msg': '价格需在1-10000元之间', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + if '.' in jiage_str and len(jiage_str.split('.')[1]) > 2: + return Response({'code': 400, 'msg': '最多支持两位小数', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + except ValueError: + return Response({'code': 400, 'msg': '价格格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST) + + # 3. 获取商家扩展信息 + try: + shangjia = request.user.shop_profile + except UserShangjia.DoesNotExist: + logger.warning(f"用户不是商家: user_id={request.user.yonghuid}") + return Response({'code': 403, 'msg': '您不是商家或商家信息未完善', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + # 4. 验证商家状态 + if shangjia.zhuangtai != 1: + logger.warning(f"商家状态异常: user_id={request.user.yonghuid}, status={shangjia.zhuangtai}") + return Response({'code': 403, 'msg': '商家账号状态异常,请联系管理员', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + # 5. 验证商家余额 + if jiage > shangjia.yue: + logger.warning(f"余额不足: user_id={request.user.yonghuid}, 余额={shangjia.yue}, 需要={jiage}") + return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # 6. 验证指定打手 + zhiding_dashou = None + dashou_user_main = None + if zhiding_uid: + try: + zhiding_user = UserMain.objects.get(yonghuid=zhiding_uid) + dashou_user_main = zhiding_user + + try: + zhiding_dashou = zhiding_user.dashou_profile + except UserDashou.DoesNotExist: + logger.warning(f"指定用户不是打手: zhiding_uid={zhiding_uid}") + return Response({'code': 400, 'msg': '指定用户不是打手', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + if zhiding_dashou.zhanghaozhuangtai != 1: + logger.warning( + f"指定打手状态异常: zhiding_uid={zhiding_uid}, status={zhiding_dashou.zhanghaozhuangtai}") + return Response({'code': 400, 'msg': '指定打手账号状态异常', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + except UserMain.DoesNotExist: + logger.warning(f"指定用户不存在: zhiding_uid={zhiding_uid}") + return Response({'code': 400, 'msg': '指定用户不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # 7. 查询利率 + try: + lilu_obj = Lilubiao.objects.get(fadanpingtai='3') + lilu = float(lilu_obj.lilu) + if lilu <= 0: + lilu = 1 + except Lilubiao.DoesNotExist: + logger.warning("利率表不存在,使用默认值1") + lilu = 1 + + dashou_fencheng = round(jiage * lilu, 2) + + # 8. 生成订单ID + def generate_dingdan_id(): + import time + import random + 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}" + + # 9. 使用事务 + 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=int(yaoqiuleixing) if yaoqiuleixing else 1, + huiyuan_id=str(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}" + ) + + # 创建商家订单扩展表 + DingdanShangjia.objects.create( + dingdan=dingdan, + shangjia_id=request.user.yonghuid, + sjnicheng=request.user.boss_profile.nickname, + ) + + # 更新商家统计信息 + from django.db.models import 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, + ) + + # 🟢【10. 修改】发送模板消息 - 现在改为广播给所有用户 + from houtai.utils import update_shangjia_daily + + # ✅ 新方法(行为驱动,更安全) + update_shangjia_daily( + yonghuid=request.user.yonghuid, + amount=Decimal(str(jiage)), + action=1 # 1 = 派发 + ) + + + + + + + + + + #🟢【7. 新增】支付成功后的广播通知(异步,不阻塞主流程) + from dingdan.tongzhi_tasks import dingdan_guangbo + + # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用) + 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) # 瞬间返回,不阻塞请求 + + sync_order_to_partners(dingdan.dingdan_id) + + + + + + + + + #self.broadcast_order_notification_async(dingdan) + + # 记录成功日志 + logger.info(f"商家派单成功: order_id={dingdan.dingdan_id}, amount={jiage}, zhiding={zhiding_uid}") + + return Response({'code': 200, 'msg': '派单成功', 'data': {'jine': str(dingdan.jine)}}, + status=status.HTTP_200_OK) + + 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 broadcast_order_notification_async(self, dingdan): + """异步广播订单通知给所有关注用户""" + try: + def broadcast_task(): + 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[:50] if dingdan.jieshao else '新订单' + } + + # 调用广播器 + 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=broadcast_task) + thread.daemon = True + thread.start() + + except Exception as e: + logger.error(f"启动广播线程异常: {str(e)}") + + # 🗑️【重要】原 send_template_notification_async 方法不再需要 + # 可以注释掉或删除,因为现在统一用 broadcast_order_notification_async + + 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 '游戏订单''''' + + + +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] + + 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_yongjin = None + 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: + final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1 + final_huiyuan_id = str(huiyuan_id) if huiyuan_id else '' + logger.warning("佣金为0,降级为会员模式") + except ValueError: + final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1 + final_huiyuan_id = str(huiyuan_id) if huiyuan_id else '' + logger.warning("佣金格式错误,降级为会员模式") + else: + final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1 + final_huiyuan_id = str(huiyuan_id) if huiyuan_id else '' + logger.info(f"会员模式: 类型={final_yaoqiu_type}") + + # ----- 7. 称号(可选:不传/空值/无效/不存在均视为无标签)----- + chenghao_obj = None + label_id_str = str(label_id).strip() if label_id is not None else '' + if label_id_str and label_id_str not in ('0', 'null', 'undefined'): + try: + lid = int(label_id_str) + if lid > 0: + chenghao_obj = Chenghao.objects.filter(id=lid).first() + if chenghao_obj: + logger.info(f"订单将关联称号: {chenghao_obj.mingcheng}") + else: + logger.info(f"标签不存在,按无标签派单: {label_id}") + except (ValueError, TypeError): + logger.info(f"标签ID无效,按无标签派单: {label_id}") + + # ----- 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. 利率(打手+管事分成,打手优先) ----- + 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) + 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, + guanshi_fencheng=guanshi_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) + + # Celery 广播任务(异步) + 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 '游戏订单' + +# 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__) + + + +'''class ShangjiaDingdanHuoquView(APIView): + """ + 商家订单获取接口 + POST /dingdan/shangjia/dingdan + 需要JWT认证 + 支持分页、状态筛选、精确搜索(订单ID或游戏昵称) + 返回待处理订单数量(pending_count) + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取当前登录用户的yonghuid + try: + current_user = request.user + yonghuid = current_user.yonghuid + except AttributeError: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 解析参数 + try: + data = request.data + zhuangtai_list = data.get('zhuangtai_list', []) + page = max(1, int(data.get('page', 1))) + page_size = min(100, max(1, int(data.get('page_size', 10)))) + + # 搜索参数(精确匹配) + search_type = data.get('search_type') # 可能的值:'order_id' 或 'nicheng' + search_value = data.get('search_value', '').strip() + except (ValueError, TypeError): + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 验证状态列表 + if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0: + return Response({ + 'code': 3, + 'msg': '状态列表不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 4. 构建基础查询(该商家的所有订单) + base_qs = DingdanShangjia.objects.filter( + shangjia_id=yonghuid + ).select_related('dingdan') + + # 5. 状态过滤(始终使用传入的zhuangtai_list) + if zhuangtai_list: + base_qs = base_qs.filter(dingdan__zhuangtai__in=zhuangtai_list) + + # 6. 搜索过滤(精确匹配) + if search_type and search_value: + if search_type == 'order_id': + # 订单ID精确匹配(dingdan_id有唯一索引) + base_qs = base_qs.filter(dingdan__dingdan_id=search_value) + elif search_type == 'nicheng': + # 游戏昵称精确匹配(建议为nicheng字段添加索引) + base_qs = base_qs.filter(dingdan__nicheng=search_value) + # 其他类型忽略 + + # 7. 计算待处理订单数量(可抢订单:状态为1或7) + pending_count = DingdanShangjia.objects.filter( + shangjia_id=yonghuid, + dingdan__zhuangtai__in=[1, 7] + ).count() + + # 8. 排序、分页 + total_count = base_qs.count() + start_index = (page - 1) * page_size + end_index = start_index + page_size + # 注意:这里使用切片会触发数据库LIMIT,性能良好 + current_page_orders = base_qs.order_by('-dingdan__create_time')[start_index:end_index] + + # 9. 构建返回数据 + order_list = [] + for shangjia_order in current_page_orders: + order = shangjia_order.dingdan + order_data = { + 'dingdan_id': order.dingdan_id, + 'zhuangtai': order.zhuangtai, + 'jine': float(order.jine) if order.jine else 0.0, + 'jieshao': order.jieshao or '', + 'beizhu': order.beizhu or '', + 'nicheng': order.nicheng or '', + 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '' + } + order_list.append(order_data) + + # 10. 判断是否还有更多 + current_page_count = len(current_page_orders) + has_more = current_page_count == page_size and end_index < total_count + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': { + 'list': order_list, + 'has_more': has_more, + 'total': total_count, + 'pending_count': pending_count # 新增待处理数量 + } + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"商家订单获取失败: {str(e)}", exc_info=True) + return Response({ + 'code': 4, + 'msg': '服务器内部错误', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' + + +# views.py 中 ShangjiaDingdanHuoquView 完整改造 + +class ShangjiaDingdanHuoquView(APIView): + """ + 商家订单获取接口 + POST /dingdan/sjdingdanhq + 需要JWT认证 + 支持分页、状态筛选、商品类型筛选、模糊搜索(订单ID/订单介绍/游戏昵称/打手ID) + 返回待结算订单数量(pending_count) + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取当前登录用户的yonghuid + try: + current_user = request.user + yonghuid = current_user.yonghuid + except AttributeError: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 解析参数 + try: + data = request.data + zhuangtai_list = data.get('zhuangtai_list', []) + page = max(1, int(data.get('page', 1))) + page_size = min(100, max(1, int(data.get('page_size', 10)))) + + # 🆕 商品类型筛选 + leixing_id = data.get('leixing_id', None) + + # 🆕 模糊搜索关键字(支持订单ID、订单介绍、游戏昵称、打手ID) + keyword = data.get('keyword', '').strip() + except (ValueError, TypeError): + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 验证状态列表 + if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0: + return Response({ + 'code': 3, + 'msg': '状态列表不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 4. 构建基础查询(该商家的所有订单) + base_qs = DingdanShangjia.objects.filter( + shangjia_id=yonghuid + ).select_related('dingdan') + + # 5. 状态过滤 + base_qs = base_qs.filter(dingdan__zhuangtai__in=zhuangtai_list) + + # 🆕 6. 商品类型筛选 + if leixing_id is not None: + try: + leixing_id_int = int(leixing_id) + base_qs = base_qs.filter(dingdan__leixing_id=leixing_id_int) + except (ValueError, TypeError): + # 如果leixing_id不是有效整数,忽略该筛选 + pass + + # 🆕 7. 模糊搜索(支持订单ID、订单介绍、游戏昵称、打手ID) + if keyword: + from django.db.models import Q + # 订单ID精确匹配(dingdan_id有唯一索引,先用精确匹配) + dingdan_id_match = Q(dingdan__dingdan_id__exact=keyword) + # 订单介绍模糊匹配 + jieshao_match = Q(dingdan__jieshao__icontains=keyword) + # 游戏昵称模糊匹配 + nicheng_match = Q(dingdan__nicheng__icontains=keyword) + # 打手ID精确匹配 + dashou_match = Q(dingdan__jiedan_dashou_id__exact=keyword) + + base_qs = base_qs.filter( + dingdan_id_match | jieshao_match | nicheng_match | dashou_match + ) + + # 8. 计算待结算订单数量(状态为8) + pending_count = DingdanShangjia.objects.filter( + shangjia_id=yonghuid, + dingdan__zhuangtai=8 + ).count() + + # 9. 排序、分页 + total_count = base_qs.count() + start_index = (page - 1) * page_size + end_index = start_index + page_size + current_page_orders = base_qs.order_by('-dingdan__create_time')[start_index:end_index] + + # 10. 构建返回数据 + order_list = [] + for shangjia_order in current_page_orders: + order = shangjia_order.dingdan + order_data = { + 'dingdan_id': order.dingdan_id, + 'zhuangtai': order.zhuangtai, + 'jine': float(order.jine) if order.jine else 0.0, + 'jieshao': order.jieshao or '', + 'beizhu': order.beizhu or '', + 'nicheng': order.nicheng or '', + 'leixing_id': order.leixing_id, # 🆕 返回商品类型ID + 'tupian': order.tupian or '', # 🆕 返回商品图片 + 'jiedan_dashou_id': order.jiedan_dashou_id or '', # 🆕 返回打手ID + 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '' + } + order_list.append(order_data) + + # 11. 判断是否还有更多 + current_page_count = len(current_page_orders) + has_more = current_page_count == page_size and end_index < total_count + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': { + 'list': order_list, + 'has_more': has_more, + 'total': total_count, + 'pending_count': pending_count + } + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"商家订单获取失败: {str(e)}", exc_info=True) + return Response({ + 'code': 4, + 'msg': '服务器内部错误', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + + + + + +'''class ShangjiaDingdanXiangqingView(APIView): + """商家订单详情接口 - 高性能实现""" + permission_classes = [IsAuthenticated] # JWT认证 + + def post(self, request): + # 1. 获取当前登录用户的yonghuid + try: + current_user = request.user + yonghuid = current_user.yonghuid + except AttributeError: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdan_id', '').strip() + except Exception: + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 参数验证 + if not dingdan_id: + return Response({ + 'code': 3, + 'msg': '订单ID不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 4. 高性能查询:一次性获取所有关联数据 + # 使用select_related和prefetch_related避免N+1查询 + + # 先查询商家订单扩展表,验证订单权限 + shangjia_kuozhan = DingdanShangjia.objects.filter( + shangjia_id=yonghuid, + dingdan__dingdan_id=dingdan_id + ).select_related('dingdan').first() + + if not shangjia_kuozhan: + return Response({ + 'code': 4, + 'msg': '订单不存在或您无权限查看', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 5. 查询打手交付图片(使用prefetch_related可以一次性获取所有相关图片) + # 这里我们单独查询,因为是多对一关系 + dashou_tupian_list = Dashoutupian.objects.filter( + dingdan_id=dingdan_id + ).order_by('create_time').values_list('tupian', flat=True) + + # 转换为列表,确保即使为空也返回列表 + dashou_jiaofu = list(dashou_tupian_list) + + # 6. 构建返回数据 - 字段名必须与前端完全匹配 + # 注意:所有字段即使为空也要返回,前端会处理空值 + order = shangjia_kuozhan.dingdan + + response_data = { + # 基础字段 + 'dingdan_id': order.dingdan_id, + 'zhuangtai': order.zhuangtai or 0, + + # 服务相关信息 + 'fuwudashou_id': order.jiedan_dashou_id or '', # 服务打手ID + 'dashou_liuyan': order.dashou_liuyan or '', # 打手留言 + 'dashou_jiaofu': dashou_jiaofu, # 打手交付图片列表 + + # 商家订单扩展表字段 + 'shangjia_liuyan': shangjia_kuozhan.shangjia_pingjia or '', # 商家留言 + 'tkliyou': order.tkly or '', # 退款理由(暂时没有字段) + 'cfliyou': shangjia_kuozhan.cfliyou or '', # 处罚申请原因 + 'sqzhuangtai': shangjia_kuozhan.sqzhuangtai or 0, # 申请状态 + 'bhliyou': shangjia_kuozhan.bhliyou or '', # 驳回理由 + 'chufa_jieguo': '', # 处罚结果(暂时没有字段) + + # 其他可能需要的基础信息 + 'jine': float(order.jine) if order.jine else 0.0, + 'jieshao': order.jieshao or '', + 'beizhu': order.beizhu or '', + 'nicheng': order.nicheng or '', + 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S'), + + + } + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': response_data + }, 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({ + 'code': 5, + 'msg': '服务器内部错误', + 'data': None + }, 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): + """ + 商家订单详情接口 + 返回订单基础信息、打手信息、交付图片、罚款数据、分红利率、更换打手产生的新订单ID等 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取当前登录用户的 yonghuid + try: + current_user = request.user + yonghuid = current_user.yonghuid + except AttributeError: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdan_id', '').strip() + except Exception: + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not dingdan_id: + return Response({ + 'code': 3, + 'msg': '订单ID不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 3. 查询商家订单扩展表,验证权限并获取基础订单数据 + shangjia_kuozhan = DingdanShangjia.objects.filter( + shangjia_id=yonghuid, + dingdan__dingdan_id=dingdan_id + ).select_related('dingdan').first() + + if not shangjia_kuozhan: + return Response({ + 'code': 4, + 'msg': '订单不存在或您无权限查看', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + order = shangjia_kuozhan.dingdan + + # 4. 交付图片列表 + dashou_tupian_list = Dashoutupian.objects.filter( + dingdan_id=dingdan_id + ).order_by('create_time').values_list('tupian', flat=True) + dashou_jiaofu = list(dashou_tupian_list) + + # 5. 打手详细信息(头像、昵称、用户ID) + dashou_yonghuid = order.jiedan_dashou_id or '' + dashou_nicheng = '' + dashou_avatar = '' + if dashou_yonghuid: + dashou_user = UserMain.objects.filter( + yonghuid=dashou_yonghuid + ).select_related('dashou_profile').first() + if dashou_user: + dashou_avatar = dashou_user.avatar or '' + if hasattr(dashou_user, 'dashou_profile') and dashou_user.dashou_profile: + dashou_nicheng = dashou_user.dashou_profile.nicheng or '' + + # 6. 罚款数据(如果存在) + fadan_obj = Fadan.objects.filter( + guanliandingdan_id=dingdan_id, + beichufa_id=dashou_yonghuid + ).first() + fadan_dict = None + if fadan_obj: + fadan_dict = { + 'id': fadan_obj.id, + 'chufaliyou': fadan_obj.chufaliyou or '', + 'fakuanjine': float(fadan_obj.fakuanjine) if fadan_obj.fakuanjine else 0.0, + 'yingxiang_qiangdan': fadan_obj.yingxiang_qiangdan, + 'zhuangtai': fadan_obj.zhuangtai, + 'shensuliyou': fadan_obj.shensuliyou or '', + 'tongyi_chufaliyou': fadan_obj.tongyi_chufaliyou or '', + 'bohuiliyou': fadan_obj.bohuiliyou or '', + 'create_time': fadan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if fadan_obj.create_time else '', + 'update_time': fadan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if fadan_obj.update_time else '', + } + + # 7. 商家自己的分红利率(用于罚款分红计算) + fenhong_lilv = 0.0 + try: + # 查找商家作为分红者的最新记录,取应得利率 + last_fenhong = FadanFenhong.objects.filter( + fenhongzhe_id=yonghuid, + shenfen=1 # 1=商家 + ).order_by('-create_time').first() + if last_fenhong: + fenhong_lilv = float(last_fenhong.yingde_lilv or 0) + except Exception as e: + logger.warning(f'获取分红利率失败: {e}') + + # 8. 更换打手后产生的新订单ID(如果当前订单被更换过) + xin_dingdan_id = '' + try: + # 查找以当前订单作为“上一个订单ID”的新订单扩展记录 + new_shangjia_order = DingdanShangjia.objects.filter( + shangyige_dingdan_id=dingdan_id, + shangjia_id=yonghuid + ).select_related('dingdan').first() + if new_shangjia_order: + xin_dingdan_id = new_shangjia_order.dingdan.dingdan_id + except Exception as e: + logger.warning(f'查找新订单ID失败: {e}') + + # 9. 构建返回数据 + response_data = { + # 基础订单字段 + 'dingdan_id': order.dingdan_id, + 'zhuangtai': order.zhuangtai or 0, + 'jine': float(order.jine) if order.jine else 0.0, + 'jieshao': order.jieshao or '', + 'beizhu': order.beizhu or '', + 'nicheng': order.nicheng or '', + 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '', + + # 打手信息 + 'dashou_yonghuid': dashou_yonghuid, + 'dashou_nicheng': dashou_nicheng, + 'dashou_avatar': dashou_avatar, + + # 服务详情 + 'fuwudashou_id': dashou_yonghuid, # 保留旧字段兼容 + 'dashou_liuyan': order.dashou_liuyan or '', + 'dashou_jiaofu': dashou_jiaofu, + + # 商家扩展字段(旧) + 'shangjia_liuyan': shangjia_kuozhan.shangjia_pingjia or '', + 'tkliyou': order.tkly or '', + 'cfliyou': shangjia_kuozhan.cfliyou or '', # 保留,但不影响新罚款 + 'sqzhuangtai': shangjia_kuozhan.sqzhuangtai or 0, + 'bhliyou': shangjia_kuozhan.bhliyou or '', + 'chufa_jieguo': '', # 已弃用 + + # 新增:罚款数据 + 'fadan': fadan_dict, + + # 新增:分红利率 + 'fenhong_lilv': fenhong_lilv, + + # 新增:更换打手后的新订单ID + 'xin_dingdan_id': xin_dingdan_id, + } + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': response_data + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"商家订单详情查询失败: {str(e)}", exc_info=True) + return Response({ + 'code': 5, + 'msg': '服务器内部错误', + 'data': None + }, 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] + + def post(self, request): + # 1. 获取当前登录用户 + try: + current_user = request.user + yonghuid = current_user.yonghuid + + try: + shangjia = current_user.shop_profile + except UserShangjia.DoesNotExist: + return Response({ + 'code': 1, + 'msg': '您不是商家或商家信息未完善', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + except AttributeError: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdan_id', '').strip() + pingfen = int(data.get('pingfen', 0)) + liuyan = data.get('liuyan', '').strip() + except (ValueError, TypeError): + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 参数验证 + if not dingdan_id: + return Response({ + 'code': 3, + 'msg': '订单ID不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if pingfen < 0 or pingfen > 5: + return Response({ + 'code': 4, + 'msg': '评分必须在0-5分之间', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + with transaction.atomic(): + # 4. 查询订单并加锁(修复并发问题) + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + + # 验证订单是否属于当前商家 + shangjia_kuozhan = DingdanShangjia.objects.filter( + dingdan=order, + shangjia_id=yonghuid + ).first() + + if not shangjia_kuozhan: + return Response({ + 'code': 5, + 'msg': '订单不存在或您无权限操作', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 5. 验证订单状态 + if order.zhuangtai != 8: + return Response({ + 'code': 6, + 'msg': '订单不在结算状态,无法结单', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 获取订单字段 + jine = order.jine or Decimal('0') + dashou_fencheng = order.dashou_fencheng or Decimal('0') + jiedan_dashou_id = order.jiedan_dashou_id + dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单 + is_cross = order.is_cross + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + + if not jiedan_dashou_id: + return Response({ + 'code': 7, + 'msg': '订单没有接单打手,无法结单', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # ========== 跨平台判断 ========== + is_cross_claimed_by_partner = (is_cross == 1 and (partner_club_id or partner_order_id)) + + # ========== 根据订单类型处理 ========== + # 情况1:对方派单(我方打手接单)-> 预结算,不直接给打手加钱 + if dispatch_type == 2: + # 创建预结算记录 + PreSettlement.objects.update_or_create( + dingdan_id=dingdan_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id or '000001', + 'dashou_id': jiedan_dashou_id, + 'amount': dashou_fencheng, + 'order_amount': jine, + 'status': 0, + 'operator': request.user.phone, + } + ) + # 更新订单状态 + order.zhuangtai = 3 + order.save() + + # 更新统计表 + update_daily_dispatch_stat( + direction=2, + partner_club_id=partner_club_id or '000001', + success_amount=jine, + success_count=1 + ) + + # 情况2:我方派单且被对方打手接单 -> 只改状态,不给本地打手加钱 + elif dispatch_type == 1 and is_cross_claimed_by_partner: + order.zhuangtai = 3 + order.save() + + update_daily_dispatch_stat( + direction=1, + partner_club_id=partner_club_id or '000001', + success_amount=dashou_fencheng, + success_count=1 + ) + + # 情况3:本地订单 或 跨平台但自家打手接单 -> 正常结算给打手 + else: + # 查询打手信息 + try: + dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou = dashou_user.dashou_profile + except (UserMain.DoesNotExist, UserDashou.DoesNotExist): + return Response({ + 'code': 8, + 'msg': '接单打手不存在或信息不完整', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 原子更新打手数据(使用 F 表达式) + UserDashou.objects.filter(id=dashou.id).update( + chengjiaozongliang=F('chengjiaozongliang') + 1, + yue=F('yue') + dashou_fencheng, + zonge=F('zonge') + dashou_fencheng, + jinrishouyi=F('jinrishouyi') + dashou_fencheng, + jinyueshouyi=F('jinyueshouyi') + dashou_fencheng, + ) + + # 更新商家成交单量 + UserShangjia.objects.filter(id=shangjia.id).update( + chengjiao=F('chengjiao') + 1, + ) + + # 更新订单状态 + order.zhuangtai = 3 + order.save() + + # 处理评分(所有情况都处理) + if pingfen > 0: + pingfen_obj, created = Pingfen.objects.get_or_create( + dashou=jiedan_dashou_id, + defaults={'zongfen': pingfen, 'cishu': 1} + ) + if not created: + Pingfen.objects.filter(id=pingfen_obj.id).update( + zongfen=F('zongfen') + pingfen, + cishu=F('cishu') + 1, + ) + + # 更新商家订单扩展表的评价 + DingdanShangjia.objects.filter(id=shangjia_kuozhan.id).update( + shangjia_pingjia=liuyan, + ) + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=jiedan_dashou_id, + amount=dashou_fencheng, + action=2 # 2 表示成交(结算) + ) + logger.info(f"打手 {jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + + # ✅ 新方法(行为驱动,更安全) + update_shangjia_daily( + yonghuid=request.user.yonghuid, + amount=Decimal(str(jine)), + action=2 # 2 = 结算 + ) + + # 商家订单管事分红(仅 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) + + # 返回成功 + return Response({ + 'code': 0, + 'msg': '结单成功', + 'data': { + 'dingdan_id': dingdan_id, + 'new_zhuangtai': 3, + 'jine': float(jine), + 'dashou_fencheng': float(dashou_fencheng), + 'jiedan_dashou_id': jiedan_dashou_id, + 'pingfen': pingfen, + 'liuyan': liuyan + } + }, status=status.HTTP_200_OK) + + except Dingdan.DoesNotExist: + return Response({ + 'code': 5, + 'msg': '订单不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"商家结单失败: {str(e)}", exc_info=True) + return Response({ + 'code': 9, + 'msg': '服务器内部错误,结单失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + +from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu + + +class ShangjiaChexiaoView(APIView): + """商家撤销订单申请接口""" + permission_classes = [IsAuthenticated] # JWT认证 + + def post(self, request): + # 1. 获取当前登录用户并验证商家身份 + try: + current_user = request.user + # 使用反向查询获取商家扩展信息(性能最佳) + shangjia = current_user.shop_profile + except UserShangjia.DoesNotExist: + return Response({ + 'code': 1, + 'msg': '您不是商家或商家信息未完善', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 2. 检查商家账号状态 + if shangjia.zhuangtai != 1: + return Response({ + 'code': 2, + 'msg': '商家账号状态异常,无法操作', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 3. 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdan_id', '').strip() + chexiao_liyou = data.get('chexiao_liyou', '').strip() + except Exception: + return Response({ + 'code': 3, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 参数验证 + if not dingdan_id or not chexiao_liyou: + return Response({ + 'code': 4, + 'msg': '订单ID和撤销原因不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 5. 使用事务确保数据一致性 + with transaction.atomic(): + # 查询订单主表 + dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + if not dingdan: + return Response({ + 'code': 5, + 'msg': '订单不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 6. 验证订单状态 + if dingdan.zhuangtai not in [1, 7]: + return Response({ + 'code': 6, + 'msg': '当前订单状态不可撤销', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 7. 验证订单所属商家(使用反向查询) + shangjia_kuozhan = DingdanShangjia.objects.filter( + dingdan__dingdan_id=dingdan_id, + shangjia_id=current_user.yonghuid + ).first() + + if not shangjia_kuozhan: + return Response({ + 'code': 7, + 'msg': '您无权撤销此订单', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 8. 创建退款记录(注意:申请状态直接设为1-成功) + Tuikuanjilu.objects.create( + dingdan_id=dingdan_id, + qingqiuid=current_user.yonghuid, + liyou=chexiao_liyou, + sqzhuangtai=1, # 直接成功 + jine=dingdan.jine, + dashou_fencheng=dingdan.dashou_fencheng, + jieshao=dingdan.jieshao, + beizhu=dingdan.beizhu, + nicheng=dingdan.nicheng, + + ) + + # 9. 更新商家扩展表数据(使用F表达式原子操作) + UserShangjia.objects.filter(id=shangjia.id).update( + yue=F('yue') + dingdan.jine, + tuikuan=F('tuikuan') + 1, + jinridingdan=F('jinridingdan') - 1, + jinyuedingdan=F('jinyuedingdan') - 1, + jinriliushui=F('jinriliushui') - dingdan.jine, + jinyueliushui=F('jinyueliushui') - dingdan.jine, + + ) + + # 10. 更新订单状态为5(已退款) + dingdan.zhuangtai = 5 + + dingdan.save() + + # ✅ 新方法(行为驱动,更安全) + from houtai.utils import update_shangjia_daily + update_shangjia_daily( + yonghuid=request.user.yonghuid, + amount=Decimal(str(dingdan.jine)), + action=3 # 3 = 退款 + ) + + return Response({ + 'code': 0, + 'msg': '撤销成功', + 'data': { + 'dingdan_id': dingdan_id, + 'new_zhuangtai': 5, + 'tuikuan_liyou': chexiao_liyou, + 'jine': float(dingdan.jine) if dingdan.jine else 0.0 + } + }, 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({ + 'code': 8, + 'msg': '服务器内部错误,撤销失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + + + +class ShangjiaTuikuanShenqingView(APIView): + """商家退款申请接口""" + permission_classes = [IsAuthenticated] # JWT认证 + + def post(self, request): + # 1. 获取当前登录用户并验证商家身份 + try: + current_user = request.user + shangjia = current_user.shop_profile + except UserShangjia.DoesNotExist: + return Response({ + 'code': 1, + 'msg': '您不是商家或商家信息未完善', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 2. 检查商家账号状态 + if shangjia.zhuangtai != 1: + return Response({ + 'code': 2, + 'msg': '商家账号状态异常,无法操作', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 3. 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdan_id', '').strip() + tuikuan_liyou = data.get('tuikuan_liyou', '').strip() + except Exception: + return Response({ + 'code': 3, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 参数验证 + if not dingdan_id or not tuikuan_liyou: + return Response({ + 'code': 4, + 'msg': '订单ID和退款原因不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 5. 使用事务确保数据一致性 + with transaction.atomic(): + # 查询订单主表 + dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + if not dingdan: + return Response({ + 'code': 5, + 'msg': '订单不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 6. 验证订单状态 + if dingdan.zhuangtai not in [2, 8]: + return Response({ + 'code': 6, + 'msg': '当前订单状态不可申请退款', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 7. 验证订单所属商家 + shangjia_kuozhan = DingdanShangjia.objects.filter( + dingdan__dingdan_id=dingdan_id, + shangjia_id=current_user.yonghuid + ).first() + + if not shangjia_kuozhan: + return Response({ + 'code': 7, + 'msg': '您无权为此订单申请退款', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # ========== 8. 处理退款记录(修改点:先查询是否存在) ========== + refund_record, created = Tuikuanjilu.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'dashouid': dingdan.jiedan_dashou_id, + 'qingqiuid': current_user.yonghuid, + 'liyou': tuikuan_liyou, + 'sqzhuangtai': 0, # 待处理 + 'jine': dingdan.jine, + 'dashou_fencheng': dingdan.dashou_fencheng, + 'jieshao': dingdan.jieshao, + 'beizhu': dingdan.beizhu, + 'nicheng': dingdan.nicheng, + } + ) + if not created: + # 记录已存在,更新退款理由和申请时间 + refund_record.liyou = tuikuan_liyou + refund_record.save(update_fields=['liyou', 'update_time']) + logger.info(f"订单 {dingdan_id} 已有退款记录,已更新退款理由") + + # 9. 更新订单状态为4(退款中) + dingdan.zhuangtai = 4 + dingdan.tkly = tuikuan_liyou + dingdan.save() + + # ====== 原有功能:更新打手状态 ====== + if dingdan.jiedan_dashou_id: + try: + dashou_main = UserMain.objects.filter( + yonghuid=dingdan.jiedan_dashou_id + ).first() + if dashou_main and hasattr(dashou_main, 'dashou_profile'): + dashou_profile = dashou_main.dashou_profile + dashou_profile.zhuangtai = 1 + dashou_profile.save() + logger.info(f"退款申请成功,打手状态已更新:订单{dingdan_id},打手{dingdan.jiedan_dashou_id}状态变更为1") + except Exception as e: + logger.error(f"更新打手状态失败:{str(e)},订单{dingdan_id},打手ID{dingdan.jiedan_dashou_id}") + + # ========== 新增:如果是跨平台订单,异步通知对方平台 ========== + 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}" + payload = { + 'token': settings.CROSS_PLATFORM_TOKEN, + 'order_id': dingdan.dingdan_id + } + try: + resp = requests.post(url, json=payload, timeout=5) + if resp.status_code == 200: + data = resp.json() + if data.get('code') == 0: + logger.info(f"跨平台退款通知发送成功: {dingdan.dingdan_id}") + else: + logger.error(f"跨平台退款通知业务失败: {data}") + else: + logger.error(f"跨平台退款通知HTTP失败: {resp.status_code}") + except Exception as e: + logger.error(f"跨平台退款通知异常: {str(e)}", exc_info=True) + + thread = threading.Thread(target=send_refund_notify) + thread.daemon = True + thread.start() + logger.info(f"跨平台退款通知线程已启动: {dingdan.dingdan_id}") + # ========== 新增结束 ========== + + return Response({ + 'code': 0, + 'msg': '退款申请提交成功', + 'data': { + 'dingdan_id': dingdan_id, + 'new_zhuangtai': 4, + 'tuikuan_liyou': tuikuan_liyou, + 'jine': float(dingdan.jine) if dingdan.jine else 0.0, + 'dashou_id': dingdan.jiedan_dashou_id + } + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"商家退款申请失败: {str(e)}", exc_info=True) + return Response({ + 'code': 8, + 'msg': '服务器内部错误,申请失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + +'''class ShangjiaTuikuanShenqingView(APIView): + """商家退款申请接口""" + permission_classes = [IsAuthenticated] # JWT认证 + + def post(self, request): + # 1. 获取当前登录用户并验证商家身份 + try: + current_user = request.user + shangjia = current_user.shop_profile + except UserShangjia.DoesNotExist: + return Response({ + 'code': 1, + 'msg': '您不是商家或商家信息未完善', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 2. 检查商家账号状态 + if shangjia.zhuangtai != 1: + return Response({ + 'code': 2, + 'msg': '商家账号状态异常,无法操作', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 3. 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdan_id', '').strip() + tuikuan_liyou = data.get('tuikuan_liyou', '').strip() + except Exception: + return Response({ + 'code': 3, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 参数验证 + if not dingdan_id or not tuikuan_liyou: + return Response({ + 'code': 4, + 'msg': '订单ID和退款原因不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 5. 使用事务确保数据一致性 + with transaction.atomic(): + # 查询订单主表 + dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + if not dingdan: + return Response({ + 'code': 5, + 'msg': '订单不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 6. 验证订单状态 + if dingdan.zhuangtai not in [2, 8]: + return Response({ + 'code': 6, + 'msg': '当前订单状态不可申请退款', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 7. 验证订单所属商家 + shangjia_kuozhan = DingdanShangjia.objects.filter( + dingdan__dingdan_id=dingdan_id, + shangjia_id=current_user.yonghuid + ).first() + + if not shangjia_kuozhan: + return Response({ + 'code': 7, + 'msg': '您无权为此订单申请退款', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 8. 创建退款记录(申请状态为0-待处理) + Tuikuanjilu.objects.create( + dingdan_id=dingdan_id, + dashouid=dingdan.jiedan_dashou_id, + qingqiuid=current_user.yonghuid, + liyou=tuikuan_liyou, + sqzhuangtai=0, # 待处理 + jine=dingdan.jine, + dashou_fencheng=dingdan.dashou_fencheng, + jieshao=dingdan.jieshao, + beizhu=dingdan.beizhu, + nicheng=dingdan.nicheng, + + ) + + # 9. 更新订单状态为4(退款中) + dingdan.zhuangtai = 4 + dingdan.tkly = tuikuan_liyou + + dingdan.save() + + + # ====== 新增功能:更新打手状态 ====== + if dingdan.jiedan_dashou_id: + try: + # 高效查询:通过yonghuid查询用户主表,然后通过反向关系获取打手扩展表 + dashou_main = UserMain.objects.filter( + yonghuid=dingdan.jiedan_dashou_id + ).first() + + if dashou_main and hasattr(dashou_main, 'dashou_profile'): + # 通过OneToOneField反向关系直接获取打手扩展表 + dashou_profile = dashou_main.dashou_profile + # 将打手状态更新为1 + dashou_profile.zhuangtai = 1 + dashou_profile.save() + + # 记录日志 + import logging + logger = logging.getLogger(__name__) + logger.info( + f"退款申请成功,打手状态已更新:订单{dingdan_id},打手{dingdan.jiedan_dashou_id}状态变更为1") + except Exception as e: + # 打手状态更新失败不影响退款申请主流程,但记录错误日志 + import logging + logger = logging.getLogger(__name__) + logger.error(f"更新打手状态失败:{str(e)},订单{dingdan_id},打手ID{dingdan.jiedan_dashou_id}") + + return Response({ + 'code': 0, + 'msg': '退款申请提交成功', + 'data': { + 'dingdan_id': dingdan_id, + 'new_zhuangtai': 4, + 'tuikuan_liyou': tuikuan_liyou, + 'jine': float(dingdan.jine) if dingdan.jine else 0.0, + 'dashou_id': dingdan.jiedan_dashou_id + } + }, 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({ + 'code': 8, + 'msg': '服务器内部错误,申请失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' + + + +class ShangjiaChufaShenqingView(APIView): + """商家处罚申请接口(已添加图片上传功能)""" + permission_classes = [IsAuthenticated] # JWT认证 + + def post(self, request): + # 1. 获取当前登录用户并验证商家身份 + try: + current_user = request.user + shangjia = current_user.shop_profile + except UserShangjia.DoesNotExist: + return Response({ + 'code': 1, + 'msg': '您不是商家或商家信息未完善', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 2. 检查商家账号状态 + if shangjia.zhuangtai != 1: + return Response({ + 'code': 2, + 'msg': '商家账号状态异常,无法操作', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 3. 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdan_id', '').strip() + dashou_id = data.get('dashou_id', '').strip() + chufa_liyou = data.get('chufa_liyou', '').strip() + chufa_tupian_urls = data.get('chufa_tupian_urls', []) # 🔴 新增:图片URL数组 + except Exception: + return Response({ + 'code': 3, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 参数验证 + if not dingdan_id or not dashou_id or not chufa_liyou: + return Response({ + 'code': 4, + 'msg': '订单ID、打手ID和处罚原因不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 5. 使用事务确保数据一致性 + with transaction.atomic(): + # 验证打手身份 + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou = dashou_user.dashou_profile + except UserMain.DoesNotExist: + return Response({ + 'code': 5, + 'msg': '打手用户不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + except UserDashou.DoesNotExist: + return Response({ + 'code': 5, + 'msg': '该用户不是打手', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 查询订单主表 + dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + if not dingdan: + return Response({ + 'code': 6, + 'msg': '订单不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 验证订单状态 + if dingdan.zhuangtai not in [2, 8, 4, 5, 6, 3]: + return Response({ + 'code': 7, + 'msg': '当前订单状态不可申请处罚', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证订单所属商家 + shangjia_kuozhan = DingdanShangjia.objects.filter( + dingdan__dingdan_id=dingdan_id, + shangjia_id=current_user.yonghuid + ).first() + + if not shangjia_kuozhan: + return Response({ + 'code': 8, + 'msg': '您无权为此订单申请处罚', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 验证订单的打手是否与提供的打手ID一致 + if dingdan.jiedan_dashou_id != dashou_id: + return Response({ + 'code': 9, + 'msg': '该打手并非此订单的服务打手', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 创建处罚记录 + chufajilu = Chufajilu.objects.create( + dashouid=dashou_id, + qingqiuid=current_user.yonghuid, + cfliyou=chufa_liyou, + sqzhuangtai=0, # 待处理 + jifen=5, # 扣除5积分 + dingdan_id=dingdan_id, + ) + # 🔴【新增】在创建处罚记录后立即扣除打手积分 + # 注意:这里使用前面已经获取的dashou对象(打手扩展表) + jifen_kouchu = chufajilu.jifen # 扣除的积分,这里是5 + if jifen_kouchu > 0: + # 检查打手积分是否足够 + if dashou.jifen < jifen_kouchu: + dashou.jifen = 0 + else: + dashou.jifen -= jifen_kouchu + dashou.save() + + # 🔴【新增】保存处罚图片到数据库 + if chufa_tupian_urls and isinstance(chufa_tupian_urls, list): + for tupian_url in chufa_tupian_urls: + if tupian_url and tupian_url.strip(): + Chufatupian.objects.create( + dingdan_id=dingdan_id, + yonghuid=current_user.yonghuid, + tupian=tupian_url + ) + + # 更新商家订单扩展表的处罚相关字段 + shangjia_kuozhan.cfliyou = chufa_liyou + shangjia_kuozhan.sqzhuangtai = 0 + shangjia_kuozhan.save() + + return Response({ + 'code': 0, + 'msg': '处罚申请提交成功', + 'data': { + 'dingdan_id': dingdan_id, + 'dashou_id': dashou_id, + 'chufa_liyou': chufa_liyou, + 'sqzhuangtai': 0, + 'jifen': 5, + 'tupian_count': len(chufa_tupian_urls) if chufa_tupian_urls else 0 # 🔴 返回图片数量 + } + }, 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({ + 'code': 10, + 'msg': '服务器内部错误,申请失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + +'''class ShangjiaChufaShenqingView(APIView): + """商家处罚申请接口""" + permission_classes = [IsAuthenticated] # JWT认证 + + def post(self, request): + # 1. 获取当前登录用户并验证商家身份 + try: + current_user = request.user + shangjia = current_user.shop_profile + except UserShangjia.DoesNotExist: + return Response({ + 'code': 1, + 'msg': '您不是商家或商家信息未完善', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 2. 检查商家账号状态 + if shangjia.zhuangtai != 1: + return Response({ + 'code': 2, + 'msg': '商家账号状态异常,无法操作', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 3. 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdan_id', '').strip() + dashou_id = data.get('dashou_id', '').strip() + chufa_liyou = data.get('chufa_liyou', '').strip() + except Exception: + return Response({ + 'code': 3, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 参数验证 + if not dingdan_id or not dashou_id or not chufa_liyou: + return Response({ + 'code': 4, + 'msg': '订单ID、打手ID和处罚原因不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 5. 使用事务确保数据一致性 + with transaction.atomic(): + # 验证打手身份 + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou = dashou_user.dashou_profile + except UserMain.DoesNotExist: + return Response({ + 'code': 5, + 'msg': '打手用户不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + except UserDashou.DoesNotExist: + return Response({ + 'code': 5, + 'msg': '该用户不是打手', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 查询订单主表 + dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + if not dingdan: + return Response({ + 'code': 6, + 'msg': '订单不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 验证订单状态 + if dingdan.zhuangtai not in [2, 8,3,4,5,6]: + return Response({ + 'code': 7, + 'msg': '当前订单状态不可申请处罚', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证订单所属商家 + shangjia_kuozhan = DingdanShangjia.objects.filter( + dingdan__dingdan_id=dingdan_id, + shangjia_id=current_user.yonghuid + ).first() + + if not shangjia_kuozhan: + return Response({ + 'code': 8, + 'msg': '您无权为此订单申请处罚', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 验证订单的打手是否与提供的打手ID一致 + if dingdan.jiedan_dashou_id != dashou_id: + return Response({ + 'code': 9, + 'msg': '该打手并非此订单的服务打手', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 创建处罚记录 + Chufajilu.objects.create( + dashouid=dashou_id, + qingqiuid=current_user.yonghuid, + cfliyou=chufa_liyou, + sqzhuangtai=0, # 待处理 + jifen=5, # 扣除5积分 + dingdan_id=dingdan_id, + + ) + + # 更新商家订单扩展表的处罚相关字段 + shangjia_kuozhan.cfliyou = chufa_liyou + shangjia_kuozhan.sqzhuangtai = 0 + + shangjia_kuozhan.save() + + return Response({ + 'code': 0, + 'msg': '处罚申请提交成功', + 'data': { + 'dingdan_id': dingdan_id, + 'dashou_id': dashou_id, + 'chufa_liyou': chufa_liyou, + 'sqzhuangtai': 0, + 'jifen': 5 + } + }, 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({ + 'code': 10, + 'msg': '服务器内部错误,申请失败', + 'data': None + }, 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 + + + + + + + +'''class DashouDingdanHuoquView(APIView): + """ + 获取可抢订单列表 + POST /dingdan/ddhq + 需要JWT Token认证 + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取请求参数 + data = request.data + leixing_id = data.get('leixing_id') + page = int(data.get('page', 1)) + page_size = int(data.get('page_size', 10)) + + # 2. 构建基础查询条件 + base_query = Q(zhuangtai__in=[1, 7]) + if leixing_id: + base_query &= Q(leixing_id=leixing_id) + + # 3. 构建基础查询集(仅过滤,不执行) + base_qs = Dingdan.objects.filter(base_query).order_by('-create_time') + + # 4. 获取总记录数 + total = base_qs.count() + + # 5. 计算分页偏移量 + offset = (page - 1) * page_size + limit = page_size + + # 6. 获取当前页数据(数据库层切片,只返回当前页) + page_qs = base_qs[offset:offset + limit] + + # 7. 使用子查询获取商家昵称(性能优化,只针对当前页) + shangjia_sub = DingdanShangjia.objects.filter( + dingdan_id=OuterRef('id') + ).values('sjnicheng')[:1] + + # 8. 获取当前页所有需要的字段(包含商家昵称) + dingdan_values = page_qs.annotate( + sjnicheng=Subquery(shangjia_sub) + ).values( + 'dingdan_id', 'zhuangtai', 'fadan_pingtai', 'dashou_fencheng', + 'zhiding_id', 'leixing_id', 'tupian', 'yaoqiuleixing', + 'huiyuan_id', 'yongjin', 'jieshao', 'beizhu', 'create_time', + 'sjnicheng' + ) + + # 9. 转换为列表以便后续修改 + dingdan_list = list(dingdan_values) + + # 10. 批量获取指定打手的头像和昵称 + zhiding_ids = {item['zhiding_id'] for item in dingdan_list if item.get('zhiding_id')} + zhiding_info = {} + if zhiding_ids: + # 一次性查询所有打手信息(通过 UserMain 关联 UserDashou) + dashou_query = UserMain.objects.filter( + yonghuid__in=zhiding_ids + ).select_related('dashou_profile').annotate( + nicheng=F('dashou_profile__nicheng') + ).values('yonghuid', 'avatar', 'nicheng') + + zhiding_info = { + item['yonghuid']: { + 'avatar': item['avatar'] or '', + 'nicheng': item['nicheng'] or '' + } + for item in dashou_query + } + + # 11. 格式化返回数据,并添加打手头像和昵称 + formatted_list = [] + for order in dingdan_list: + zid = order.get('zhiding_id') + info = zhiding_info.get(zid, {}) + formatted_order = { + 'dingdan_id': order['dingdan_id'], + 'zhuangtai': order['zhuangtai'], + 'pingtai': order['fadan_pingtai'], + 'fadan_pingtai': order['fadan_pingtai'], + 'dashou_fencheng': float(order['dashou_fencheng']) if order['dashou_fencheng'] else 0.0, + 'zhiding_id': order['zhiding_id'] or '', + 'leixing_id': order['leixing_id'] or 0, + 'tupian': order['tupian'] or '', + 'yaoqiuleixing': order['yaoqiuleixing'] or 0, + 'huiyuan_id': order['huiyuan_id'] or '', + 'yajin': float(order['yongjin']) if order['yongjin'] else 0.0, + 'yongjin': float(order['yongjin']) if order['yongjin'] else 0.0, + 'jieshao': order['jieshao'] or '', + 'beizhu': order['beizhu'] or '', + 'creat_time': order['create_time'].strftime('%Y-%m-%d %H:%M:%S') if order['create_time'] else '', + 'sjnicheng': order['sjnicheng'] or '', + 'zhiding_uid': order['zhiding_id'] or '', + # 新增字段 + 'zhiding_avatar': info.get('avatar', ''), + 'zhiding_nicheng': info.get('nicheng', ''), + } + formatted_list.append(formatted_order) + + # 12. 判断是否有更多数据 + has_more = (page * page_size) < total + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'list': formatted_list, + 'has_more': has_more, + 'total': total, + 'current_page': page, + 'page_size': page_size, + 'total_pages': (total + page_size - 1) // page_size, + } + })''' + + + + + + + + + +# 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): + """ + 获取可抢订单列表(支持标签筛选) + POST /dingdan/ddhq + 需要JWT Token认证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取请求参数 + data = request.data + leixing_id = data.get('leixing_id') + page = int(data.get('page', 1)) + page_size = int(data.get('page_size', 10)) + biaoqian_id = data.get('biaoqian_id') + + # 2. 构建基础查询条件 + base_query = Q(zhuangtai__in=[1, 7]) + if leixing_id: + base_query &= Q(leixing_id=leixing_id) + + # 3. 标签筛选:先查出符合条件的订单ID,避免跨表 JOIN 排序冲突 + if biaoqian_id and int(biaoqian_id) > 0: + biaoqian_order_ids = DingdanBiaoqian.objects.filter( + chenghao_id=int(biaoqian_id) + ).values_list('dingdan_id', flat=True) + base_query &= Q(dingdan_id__in=list(biaoqian_order_ids)) + + # 4. 构建基础查询集 + base_qs = Dingdan.objects.filter(base_query).order_by('-create_time').distinct() + + # 5. 获取总记录数 + total = base_qs.count() + + # 6. 计算分页偏移量 + offset = (page - 1) * page_size + limit = page_size + + # 7. 获取当前页数据 + page_qs = base_qs[offset:offset + limit] + + # 8. 使用子查询获取商家昵称 + shangjia_sub = DingdanShangjia.objects.filter( + dingdan__dingdan_id=OuterRef('dingdan_id') + ).values('sjnicheng')[:1] + + # 9. 获取当前页所有需要的字段 + dingdan_values = page_qs.annotate( + sjnicheng=Subquery(shangjia_sub) + ).values( + 'dingdan_id', 'zhuangtai', 'fadan_pingtai', 'dashou_fencheng', + 'zhiding_id', 'leixing_id', 'tupian', 'yaoqiuleixing', + 'huiyuan_id', 'yongjin', 'jieshao', 'beizhu', 'create_time', + 'sjnicheng' + ) + + # 10. 转换为列表 + dingdan_list = list(dingdan_values) + + # 11. 批量获取指定打手的头像和昵称 + zhiding_ids = {item['zhiding_id'] for item in dingdan_list if item.get('zhiding_id')} + zhiding_info = {} + if zhiding_ids: + dashou_query = UserMain.objects.filter( + yonghuid__in=zhiding_ids + ).select_related('dashou_profile').annotate( + nicheng=F('dashou_profile__nicheng') + ).values('yonghuid', 'avatar', 'nicheng') + zhiding_info = { + item['yonghuid']: { + 'avatar': item['avatar'] or '', + 'nicheng': item['nicheng'] or '' + } + for item in dashou_query + } + + # ============ 批量获取三层标签 ============ + order_ids = [item['dingdan_id'] for item in dingdan_list] + + # --- 订单需求标签 --- + xuqiu_tags_map = {} + if order_ids: + try: + dbq_qs = DingdanBiaoqian.objects.filter( + dingdan_id__in=order_ids + ).select_related('chenghao') + for dbq in dbq_qs: + oid = dbq.dingdan_id + if oid not in xuqiu_tags_map: + xuqiu_tags_map[oid] = [] + ch = dbq.chenghao + xuqiu_tags_map[oid].append({ + 'id': ch.id, + 'mingcheng': ch.mingcheng, + 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu) + }) + except Exception as e: + logger.error(f"查询订单需求标签失败: {e}", exc_info=True) + + # --- 所有需要查询标签的用户ID集合(指定打手 + 商家) --- + yonghuid_set = set(zhiding_ids) + shangjia_order_ids = [item['dingdan_id'] for item in dingdan_list if item['fadan_pingtai'] == 2] + shangjia_id_map = {} + if shangjia_order_ids: + # 使用跨表查询 dingdan__dingdan_id__in 匹配字符串订单ID + sj_exts = DingdanShangjia.objects.filter( + dingdan__dingdan_id__in=shangjia_order_ids + ).values('dingdan_id', 'shangjia_id') + for sj in sj_exts: + shangjia_id_map[sj['dingdan_id']] = sj['shangjia_id'] + if sj['shangjia_id']: + yonghuid_set.add(sj['shangjia_id']) + + # 将 yonghuid 字符串转换为 UserMain 的主键 id + user_id_map = {} + if yonghuid_set: + users = UserMain.objects.filter(yonghuid__in=yonghuid_set).values('id', 'yonghuid') + for u in users: + user_id_map[u['yonghuid']] = u['id'] + + user_main_ids = list(user_id_map.values()) + user_tags_map = {} + if user_main_ids: + uc_qs = YonghuChenghao.objects.filter( + yonghu_id__in=user_main_ids + ).select_related('chenghao') + for uc in uc_qs: + uid = uc.yonghu_id + if uid not in user_tags_map: + user_tags_map[uid] = [] + user_tags_map[uid].append(uc.chenghao) + + dashou_tags_map = {} + shangjia_tags_map = {} + for item in dingdan_list: + oid = item['dingdan_id'] + # 指定打手标签 + zhid = item['zhiding_id'] + dashou_tags = [] + if zhid and zhid in user_id_map: + uid = user_id_map[zhid] + if uid in user_tags_map: + for ch in user_tags_map[uid]: + if ch.leixing == 'dashou': + dashou_tags.append({ + 'id': ch.id, + 'mingcheng': ch.mingcheng, + 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu) + }) + dashou_tags_map[oid] = dashou_tags + + # 商家标签 + sj_tags = [] + if item['fadan_pingtai'] == 2 and oid in shangjia_id_map: + sj_yonghuid = shangjia_id_map[oid] + if sj_yonghuid and sj_yonghuid in user_id_map: + uid = user_id_map[sj_yonghuid] + if uid in user_tags_map: + for ch in user_tags_map[uid]: + if ch.leixing == 'shangjia': + sj_tags.append({ + 'id': ch.id, + 'mingcheng': ch.mingcheng, + 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu) + }) + shangjia_tags_map[oid] = sj_tags + + # 12. 格式化返回数据 + formatted_list = [] + for order in dingdan_list: + oid = order['dingdan_id'] + zid = order.get('zhiding_id') + info = zhiding_info.get(zid, {}) + formatted_list.append({ + 'dingdan_id': order['dingdan_id'], + 'zhuangtai': order['zhuangtai'], + 'pingtai': order['fadan_pingtai'], + 'fadan_pingtai': order['fadan_pingtai'], + 'dashou_fencheng': float(order['dashou_fencheng']) if order['dashou_fencheng'] else 0.0, + 'zhiding_id': order['zhiding_id'] or '', + 'leixing_id': order['leixing_id'] or 0, + 'tupian': order['tupian'] or '', + 'yaoqiuleixing': order['yaoqiuleixing'] or 0, + 'huiyuan_id': order['huiyuan_id'] or '', + 'yajin': float(order['yongjin']) if order['yongjin'] else 0.0, + 'yongjin': float(order['yongjin']) if order['yongjin'] else 0.0, + 'jieshao': order['jieshao'] or '', + 'beizhu': order['beizhu'] or '', + 'creat_time': order['create_time'].strftime('%Y-%m-%d %H:%M:%S') if order['create_time'] else '', + 'sjnicheng': order['sjnicheng'] or '', + 'zhiding_uid': order['zhiding_id'] or '', + 'zhiding_avatar': info.get('avatar', ''), + 'zhiding_nicheng': info.get('nicheng', ''), + # 三层标签 + 'xuqiu_biaoqian': xuqiu_tags_map.get(oid, []), + 'dashou_biaoqian': dashou_tags_map.get(oid, []), + 'shangjia_biaoqian': shangjia_tags_map.get(oid, []), + }) + + has_more = (page * page_size) < total + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'list': formatted_list, + 'has_more': has_more, + 'total': total, + 'current_page': page, + 'page_size': page_size, + 'total_pages': (total + page_size - 1) // page_size, + } + }) + + except Exception as e: + logger.error(f"订单列表查询失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + def _parse_texiao_json(self, texiao_miaoshu): + if not texiao_miaoshu: + return {} + try: + return json.loads(texiao_miaoshu) + except: + return {} + + +# apps/dingdan/views.py +from shangpin.models import ShangpinLeixing # 假设这是你的商品类型模型 + + +class ShangpinLeixingHuoquView(APIView): + """ + 获取商品类型列表 + POST /dingdan/ddlxhq + 需要JWT Token认证 + """ + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 查询商品类型表 + # 假设你的商品类型模型字段为:id, jieshao, tupian_url + leixing_list = ShangpinLeixing.objects.all().values( + 'id', # 商品类型ID + 'jieshao', # 类型介绍 + 'tupian_url' # 类型图片URL(相对URL) + ).order_by('id') # 按ID排序,可根据需要调整 + + # 2. 格式化返回数据 + formatted_list = [] + for leixing in leixing_list: + formatted_leixing = { + 'id': leixing['id'], + 'jieshao': leixing['jieshao'] or '', # 处理空值 + 'tupian_url': leixing['tupian_url'] or '' # 处理空值 + } + formatted_list.append(formatted_leixing) + + # 3. 返回响应 + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'list': formatted_list, + 'total': len(formatted_list) + } + }) + + +# 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__) + + + + + + +'''class QiangdanView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request): + data = request.data + dingdan_id = data.get('dingdan_id') + if not dingdan_id: + return Response({'code': 400, 'msg': '订单ID不能为空'}, status=400) + + try: + with transaction.atomic(): + # 1. 验证打手身份和状态 + dashou_profile = self._validate_dashou_status(request.user) + if not dashou_profile: + return Response({'code': 403, 'msg': '您不是打手或账号状态异常'}, status=403) + + # 2. 锁定并验证订单(行锁) + order_result = self._validate_and_lock_order(dingdan_id, request.user.yonghuid) + if isinstance(order_result, str): + return Response({'code': 400, 'msg': order_result}) + order = order_result + + is_cross = order.is_cross == 1 + dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单 + + # ========== 跨平台订单处理 ========== + if is_cross and dispatch_type == 2: + # 对方派单(跨平台且对方派给我方) + validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) + if not validation_result['success']: + return Response({'code': 400, 'msg': validation_result['message']}) + + # 向对方平台发送抢单询问 + claim_allowed = self._request_partner_check_claim(order, request.user.yonghuid) + if claim_allowed is None: + return Response({'code': 500, 'msg': '跨平台通信失败,请重试'}, status=500) + elif claim_allowed is False: + if order.zhuangtai == 1: + order.zhuangtai = 21 + order.save() + logger.info(f"订单 {dingdan_id} 被对方拒绝抢单,状态改为21") + else: + order.zhuangtai = 20 + order.save() + logger.info(f"订单 {dingdan_id} 已被其他平台抢走,状态改为20") + return Response({'code': 400, 'msg': '订单已被其他平台打手抢走或不可抢'}) + + # 允许抢单,执行本地抢单 + self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) + + # 更新抢单统计(对方派单,我方抢到) + try: + update_daily_dispatch_stat( + direction=2, # 对方派单 + partner_club_id=order.partner_club_id or '000001', + claimed_amount=order.jine, # 订单金额 + claimed_count=1 + ) + except Exception as e: + logger.error(f"更新抢单统计失败: {e}", exc_info=True) + + elif is_cross and dispatch_type == 1: + # 我方派单 + validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) + if not validation_result['success']: + return Response({'code': 400, 'msg': validation_result['message']}) + + self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) + + # 异步通知所有合作俱乐部:此订单已被抢 + self._async_notify_partners_order_claimed(order) + + else: + # 普通订单 + validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) + if not validation_result['success']: + return Response({'code': 400, 'msg': validation_result['message']}) + self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) + + # 抢单成功后建立聊天关系(事务外) + chat_success = self._establish_chat_relationship(order, request.user.yonghuid, dashou_profile, request.user.avatar) + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=1 # 1 表示接单(接单) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + + return Response({ + 'code': 200, + 'msg': '抢单成功', + 'data': { + 'dingdan_id': dingdan_id, + 'chat_established': chat_success + } + }) + + except Exception as e: + logger.error(f"抢单异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '系统异常,请稍后重试'}, status=500) + + # ---------- 辅助方法 ---------- + def _validate_dashou_status(self, user): + try: + dashou_profile = user.dashou_profile + if (dashou_profile.zhuangtai != 1 or + dashou_profile.zhanghaozhuangtai != 1 or + dashou_profile.jifen < 5): + return None + return dashou_profile + except UserDashou.DoesNotExist: + return None + + def _validate_and_lock_order(self, dingdan_id, dashou_yonghuid): + try: + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + if order.zhuangtai not in [1, 7]: + return "订单状态不可抢" + if order.zhuangtai == 7: + if not order.zhiding_id or order.zhiding_id != dashou_yonghuid: + return "此订单为指定单,您不是指定打手" + if order.jiedan_dashou_id: + return "订单已被其他打手抢走" + return order + except Dingdan.DoesNotExist: + return "订单不存在" + + def _validate_order_requirements(self, order, dashou_profile, dashou_yonghuid): + if order.yaoqiuleixing == 1: + return self._validate_huiyuan_requirement(order, dashou_yonghuid) + elif order.yaoqiuleixing == 2: + return self._validate_yajin_requirement(order, dashou_profile) + return {'success': True, 'message': '验证通过'} + + def _validate_huiyuan_requirement(self, order, dashou_yonghuid): + try: + huiyuan_record = Huiyuangoumai.objects.get( + huiyuan_id=order.huiyuan_id, + yonghu_id=dashou_yonghuid + ) + if huiyuan_record.jiance_shifou_daoqi() or huiyuan_record.huiyuan_zhuangtai != 1: + return {'success': False, 'message': '会员已过期或未开通对应会员'} + return {'success': True, 'message': '会员验证通过'} + except Huiyuangoumai.DoesNotExist: + return {'success': False, 'message': '未开通对应会员,无法抢此订单'} + + def _validate_yajin_requirement(self, order, dashou_profile): + yajin_required = order.yongjin or 0 + dashou_yajin = dashou_profile.yajin or 0 + if dashou_yajin < yajin_required: + return {'success': False, 'message': f'押金不足,需要{yajin_required}元,当前有{dashou_yajin}元'} + return {'success': True, 'message': '押金验证通过'} + + def _execute_qiangdan(self, order, dashou_profile, dashou_yonghuid): + order.zhuangtai = 2 + order.jiedan_dashou_id = dashou_yonghuid + order.user2_id = f"Ds{dashou_yonghuid}" + order.save() + + dashou_profile.zhuangtai = 0 + dashou_profile.jiedanzongliang = F('jiedanzongliang') + 1 + dashou_profile.jinrijiedan = F('jinrijiedan') + 1 + dashou_profile.jinyuejiedan = F('jinyuejiedan') + 1 + dashou_profile.save() + + # ---------- 跨平台询问对方 ---------- + def _request_partner_check_claim(self, order, dashou_id): + partner_club_id = order.partner_club_id or '000001' + try: + club = Club.objects.filter(partner_club_id=partner_club_id).first() + if not club or not club.partner_domain: + logger.error(f"未找到对方俱乐部配置: partner_club_id={partner_club_id}") + return None + + my_club = Club.objects.filter(partner_club_id=club.partner_club_id).first() + if not my_club: + logger.error(f"未找到我方俱乐部配置: partner_club_id={club.partner_club_id}") + return None + + payload = { + 'token': settings.CROSS_PLATFORM_TOKEN, + 'wofangdingdan_id': order.partner_order_id, + 'partner_order_id': order.dingdan_id, + 'our_club_id': my_club.club_id, + 'partner_club_id': club.partner_club_id, + 'dashou_id': dashou_id, + 'sj_id': f"Sj{settings.AGENT_SHANGJIA_ID}" + } + + url = club.partner_domain.rstrip('/') + '/dingdan/partner_check_claim' + resp = requests.post(url, json=payload, timeout=5) + + if resp.status_code != 200: + logger.error(f"跨平台抢单询问HTTP失败: {resp.status_code}") + return None + + data = resp.json() + if data.get('code') == 0: + return data.get('data', {}).get('can_claim') is True + else: + logger.error(f"跨平台抢单询问业务失败: {data}") + return False + except Exception as e: + logger.error(f"跨平台抢单询问异常: {e}", exc_info=True) + return None + + # ---------- 异步通知其他俱乐部 ---------- + def _async_notify_partners_order_claimed(self, order): + def notify_task(): + clubs = Club.objects.filter(is_interop_enabled=True).select_related() + for club in clubs: + for attempt in range(1, 4): + success = self._send_claim_notification_to_partner(order, club) + if success: + break + if attempt < 3: + import time + time.sleep(2 ** attempt) + else: + logger.error(f"通知俱乐部 {club.club_id} 订单已抢失败,重试3次后仍失败") + + thread = threading.Thread(target=notify_task) + thread.daemon = True + thread.start() + + def _send_claim_notification_to_partner(self, order, club): + try: + my_club = Club.objects.filter(partner_club_id=club.partner_club_id).first() + if not my_club: + logger.warning(f"未找到我方俱乐部配置: partner_club_id={club.partner_club_id}") + return False + + payload = { + 'token': settings.CROSS_PLATFORM_TOKEN, + 'our_club_id': my_club.club_id, + 'partner_club_id': club.partner_club_id, + 'partner_order_id': order.dingdan_id, + 'claimed': True + } + url = club.partner_domain.rstrip('/') + '/dingdan/partner_notify_claim' + resp = requests.post(url, json=payload, timeout=3) + return resp.status_code == 200 + except Exception as e: + logger.error(f"通知俱乐部 {club.club_id} 异常: {e}") + return False + + # ---------- 聊天关系建立(完整版) ---------- + def _establish_chat_relationship(self, order, dashou_yonghuid, dashou_profile, dashou_avatar): + try: + dashou_info = { + 'goeasy_id': f"Ds{dashou_yonghuid}", + 'avatar': dashou_avatar or '', + 'nickname': dashou_profile.nicheng or f'打手{dashou_yonghuid}' + } + + laoban_info = self._get_laoban_info(order) + if not laoban_info: + return False + + # 打手给老板/商家发消息 + success1 = self._send_goeasy_message( + sender_id=dashou_info['goeasy_id'], + sender_data={ + "avatar": dashou_info['avatar'], + "name": dashou_info['nickname'], + "nickname": dashou_info['nickname'] + }, + receiver_id=laoban_info['goeasy_id'], + receiver_data={ + "avatar": laoban_info['avatar'], + "name": laoban_info['nickname'], + "nickname": laoban_info['nickname'] + }, + message_type="text", + message_payload={ + "action": "new_chat", + "partner_id": laoban_info['goeasy_id'], + "partner_info": { + "userId": laoban_info['goeasy_id'], + "avatar": laoban_info['avatar'], + "name": laoban_info['nickname'], + "nickname": laoban_info['nickname'] + }, + "order_id": order.dingdan_id, + "message": f"您好,我已接单:{order.jieshao},您的备注是:{order.beizhu},游戏ID是:{order.nicheng} 请确认订单信息" + } + ) + + # 老板/商家给打手发消息 + success2 = self._send_goeasy_message( + sender_id=laoban_info['goeasy_id'], + sender_data={ + "avatar": laoban_info['avatar'], + "name": laoban_info['nickname'], + "nickname": laoban_info['nickname'] + }, + receiver_id=dashou_info['goeasy_id'], + receiver_data={ + "avatar": dashou_info['avatar'], + "name": dashou_info['nickname'], + "nickname": dashou_info['nickname'] + }, + message_type="text", + message_payload={ + "action": "new_chat", + "partner_id": dashou_info['goeasy_id'], + "partner_info": { + "userId": dashou_info['goeasy_id'], + "avatar": dashou_info['avatar'], + "name": dashou_info['nickname'], + "nickname": dashou_info['nickname'] + }, + "order_id": order.dingdan_id, + "message": f"好的,订单已确认,请开始服务" + } + ) + + return success1 and success2 + + except Exception as e: + logger.error(f"建立聊天关系异常: {e}", exc_info=True) + return False + + def _get_laoban_info(self, order): + try: + laoban_info = { + 'goeasy_id': '', + 'avatar': '', + 'nickname': '' + } + + if order.fadan_pingtai == 1: + pingtai_kuozhan = DingdanPingtai.objects.filter(dingdan=order).first() + if not pingtai_kuozhan or not pingtai_kuozhan.laoban_id: + return None + yonghuid = pingtai_kuozhan.laoban_id + laoban_info['goeasy_id'] = f"Boss{yonghuid}" + laoban_user = UserMain.objects.filter(yonghuid=yonghuid).first() + if not laoban_user: + return None + laoban_info['avatar'] = laoban_user.avatar or '' + boss_profile = UserBoss.objects.filter(user=laoban_user).first() + laoban_info['nickname'] = boss_profile.nickname if boss_profile and boss_profile.nickname else f'老板{yonghuid}' + + elif order.fadan_pingtai == 2: + shangjia_kuozhan = DingdanShangjia.objects.filter(dingdan=order).first() + if not shangjia_kuozhan or not shangjia_kuozhan.shangjia_id: + return None + yonghuid = shangjia_kuozhan.shangjia_id + laoban_info['goeasy_id'] = f"Sj{yonghuid}" + shangjia_user = UserMain.objects.filter(yonghuid=yonghuid).first() + if not shangjia_user: + return None + laoban_info['avatar'] = shangjia_user.avatar or '' + if shangjia_kuozhan.sjnicheng: + laoban_info['nickname'] = shangjia_kuozhan.sjnicheng + else: + shop_profile = UserShangjia.objects.filter(user=shangjia_user).first() + laoban_info['nickname'] = shop_profile.nicheng if shop_profile else f'商家{yonghuid}' + else: + if order.user1_id: + laoban_info['goeasy_id'] = order.user1_id + if laoban_info['goeasy_id'].startswith('Boss'): + yonghuid = laoban_info['goeasy_id'][4:] + laoban_info['nickname'] = f'老板{yonghuid}' + elif laoban_info['goeasy_id'].startswith('Sj'): + yonghuid = laoban_info['goeasy_id'][2:] + laoban_info['nickname'] = f'商家{yonghuid}' + else: + laoban_info['nickname'] = '用户' + else: + return None + + if not laoban_info['goeasy_id']: + return None + return laoban_info + + except Exception as e: + logger.error(f"获取老板信息异常: {e}", exc_info=True) + return None + + def _send_goeasy_message(self, sender_id, sender_data, receiver_id, receiver_data, message_type, message_payload): + try: + if not hasattr(settings, 'GOEASY_APPKEY') or not settings.GOEASY_APPKEY: + return False + if not hasattr(settings, 'GOEASY_SECRET') or not settings.GOEASY_SECRET: + return False + if not hasattr(settings, 'GOEASY_REST_URL') or not settings.GOEASY_REST_URL: + return False + + appkey = settings.GOEASY_APPKEY + secret = settings.GOEASY_SECRET + url = settings.GOEASY_REST_URL + + if isinstance(message_payload, dict) and 'message' in message_payload: + payload_text = str(message_payload['message']) + else: + payload_text = str(message_payload) + + payload_data = {"text": payload_text} + payload_str = json.dumps(payload_data, ensure_ascii=False) + + request_data = { + "appkey": appkey, + "senderId": str(sender_id), + "senderData": { + "avatar": str(sender_data.get('avatar', '')), + "name": str(sender_data.get('name', '系统通知')) + }, + "to": { + "type": "private", + "id": str(receiver_id), + "data": { + "avatar": str(receiver_data.get('avatar', '')), + "name": str(receiver_data.get('name', '用户')) + } + }, + "type": "text", + "payload": payload_str + } + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {secret}" + } + + response = requests.post(url, headers=headers, json=request_data, timeout=10) + return response.status_code == 200 + + except Exception as e: + logger.error(f"发送GoEasy消息异常: {e}", exc_info=True) + return False''' + + + + + +# 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') + + +# ----------------- 基础配置获取 ----------------- +def get_self_goeasy_appkey(): + try: + cfg = ClubConfig.objects.filter(is_self=1).first() + return cfg.chat_api_key if cfg else None + except Exception: + return None + + +def get_self_goeasy_secret(): + try: + cfg = ClubConfig.objects.filter(is_self=1).first() + return cfg.chat_api_id if cfg else None + except Exception: + return None + + +def get_self_storage_domain(): + try: + cfg = ClubConfig.objects.filter(is_self=1).first() + return cfg.storage_bucket_domain if cfg else '' + except Exception: + return '' + + +def full_avatar(relative_url, bucket_domain=None): + if not relative_url: + return '' + if relative_url.startswith('http'): + return relative_url + domain = bucket_domain or get_self_storage_domain() + if domain: + domain = domain.rstrip('/') + '/' + return domain + relative_url.lstrip('/') + return relative_url + + +# ----------------- GoEasy API 封装 ----------------- +def subscribe_users_to_group(user_ids, group_ids, appkey=None, secret=None): + """订阅用户到群组,确保能收发该群消息""" + if not appkey: + appkey = get_self_goeasy_appkey() + if not secret: + secret = get_self_goeasy_secret() + if not appkey: + logger.error("订阅失败:GoEasy AppKey 未配置") + return False + url = 'https://rest-hangzhou.goeasy.io/v2/im/subscribe-groups' + body = {"appkey": appkey, "userIds": user_ids, "groupIds": group_ids} + headers = {"Content-Type": "application/json"} + if secret: + headers["Authorization"] = f"Bearer {secret}" + try: + r = requests.post(url, headers=headers, json=body, timeout=10) + if r.status_code == 200: + logger.info(f"订阅成功:用户 {user_ids} -> 群 {group_ids}") + return True + logger.error(f"订阅失败:{r.status_code} {r.text}") + return False + except Exception as e: + logger.exception("订阅异常") + return False + + +def send_group_message( + appkey, secret, group_id, sender_id, sender_name, sender_avatar, + message_text='', custom_payload=None, group_name='', order_id='', is_cross=0 +): + """ + 通过 GoEasy REST API 发送群消息。 + message_text: 普通文本内容 + custom_payload: 自定义消息体(如订单卡片),此时 type 设为 'order' + """ + url = 'https://rest-hangzhou.goeasy.io/v2/im/message' + to_data = { + "name": group_name or group_id, + "avatar": sender_avatar or '', + "orderId": order_id, + "isCross": is_cross + } + msg_type = 'order' if custom_payload else 'text' + payload = custom_payload if custom_payload else message_text + + body = { + "appkey": appkey, + "senderId": sender_id, + "senderData": {"avatar": sender_avatar, "name": sender_name}, + "to": {"type": "group", "id": group_id, "data": to_data}, + "type": msg_type, + "payload": payload + } + headers = {"Content-Type": "application/json"} + if secret: + headers["Authorization"] = f"Bearer {secret}" + + for attempt in range(3): # 最多重试3次 + try: + r = requests.post(url, headers=headers, json=body, timeout=10) + if r.status_code == 200: + logger.info(f"消息发送成功:sender={sender_id}, group={group_id}, type={msg_type}") + return True + logger.warning(f"消息发送失败 (尝试 {attempt+1}/3):状态码 {r.status_code}, 响应 {r.text}") + except Exception as e: + logger.warning(f"消息发送异常 (尝试 {attempt+1}/3):{e}") + if attempt < 2: + time.sleep(1) + logger.error(f"消息最终发送失败:sender={sender_id}, group={group_id}") + return False + + +# ----------------- 权限校验 ----------------- +def check_user_permission(identity_type, uid, order): + """验证用户身份、账号状态、以及是否属于该订单""" + try: + user = UserMain.objects.get(yonghuid=uid) + except UserMain.DoesNotExist: + return False, '用户不存在' + + if identity_type == 'dashou': + try: + dashou = UserDashou.objects.get(user=user) + if dashou.zhanghaozhuangtai != 1 : + return False, '打手账号已被封禁或状态异常' + except UserDashou.DoesNotExist: + return False, '该用户不是打手' + if order.jiedan_dashou_id != uid: + return False, '你不是该订单的接单打手' + + elif identity_type == 'shangjia': + try: + shop = UserShangjia.objects.get(user=user) + if shop.zhuangtai != 1: + return False, '商家账号已被禁用' + except UserShangjia.DoesNotExist: + return False, '该用户不是商家' + try: + if order.shangjia_kuozhan.shangjia_id != uid: + return False, '你不是该订单的发布商家' + except DingdanShangjia.DoesNotExist: + return False, '订单缺少商家信息' + + elif identity_type == 'boss': + try: + UserBoss.objects.get(user=user) # 老板暂无封禁字段,仅需存在 + except UserBoss.DoesNotExist: + return False, '该用户不是老板' + try: + if order.pingtai_kuozhan.laoban_id != uid: + return False, '你不是该订单的老板' + except DingdanPingtai.DoesNotExist: + return False, '订单缺少平台信息' + + else: + return False, '不支持的身份类型' + return True, '' + + +# ----------------- 跨平台对方配置获取 ----------------- +def get_partner_club_config(partner_club_id): + try: + return ClubConfig.objects.get(club_id=partner_club_id, is_self=0) + except ClubConfig.DoesNotExist: + return None + + +# ----------------- 主视图 ----------------- + + + +class DaiLiQunLiaoXiaoXiView(APIView): + """ + 代理发送群聊消息 + 支持: + - 非跨平台订单:直接用我方 GoEasy Key 以真实身份发送 + - 跨平台订单(双方任意一方派单):除了我方群外,还需用对方 GoEasy Key 以代理身份向对方群发送 + 跨平台判断条件:订单同时存在 partner_order_id 和 partner_club_id 即视为跨平台 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + # ===== 1. 参数解析 ===== + order_id = request.data.get('orderId') + identity_type = request.data.get('identityType') # dashou / shangjia / boss + sender_name = request.data.get('senderName', '') + sender_avatar_raw = request.data.get('senderAvatar', '') + message_type = request.data.get('messageType', 'text') + message_text = request.data.get('text', '') + order_payload = request.data.get('orderPayload', None) + + logger.info(f"[群聊代理] 收到请求: orderId={order_id}, identityType={identity_type}, " + f"messageType={message_type}, text={message_text[:20]}...") + + if not order_id or not identity_type: + return Response({'code': 400, 'msg': '缺少 orderId 或 identityType'}) + if identity_type not in ('dashou', 'shangjia', 'boss'): + return Response({'code': 400, 'msg': 'identityType 无效'}) + + # ===== 2. 构造 senderId ===== + uid = request.user.yonghuid + prefix_map = {'dashou': 'Ds', 'shangjia': 'Sj', 'boss': 'Boss'} + sender_id = prefix_map[identity_type] + uid + logger.info(f"[群聊代理] 登录用户 yonghuid={uid}, 构造 senderId={sender_id}") + + # ===== 3. 查询订单 ===== + try: + order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=order_id) + except Dingdan.DoesNotExist: + logger.warning(f"[群聊代理] 订单 {order_id} 不存在") + return Response({'code': 404, 'msg': '订单不存在'}) + + # ===== 4. 权限校验 ===== + valid, err_msg = check_user_permission(identity_type, uid, order) + if not valid: + logger.warning(f"[群聊代理] 权限校验失败: {err_msg}, uid={uid}, identity={identity_type}") + return Response({'code': 403, 'msg': err_msg}) + + # ===== 5. 我方 GoEasy 配置 ===== + self_appkey = get_self_goeasy_appkey() + self_secret = get_self_goeasy_secret() + if not self_appkey: + logger.error("[群聊代理] 我方 GoEasy AppKey 未配置") + return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) + + # ===== 6. 公共群信息 ===== + real_group_id = f"group_{order.dingdan_id}" + group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else f"订单{order.dingdan_id}" + sender_avatar = full_avatar(sender_avatar_raw) if sender_avatar_raw else full_avatar('') + + # ===== 7. 订阅发送者到群 ===== + if not subscribe_users_to_group([sender_id], [real_group_id], self_appkey, self_secret): + logger.error("[群聊代理] 订阅群组失败") + return Response({'code': 500, 'msg': '订阅群组失败'}) + + # ===== 8. 判断跨平台(只看对方订单ID和俱乐部ID) ===== + partner_order_id = order.partner_order_id + partner_club_id = order.partner_club_id + is_cross = 1 if (partner_order_id and partner_club_id) else 0 + logger.info(f"[群聊代理] 订单 {order_id} partner_order_id={partner_order_id}, partner_club_id={partner_club_id}, 判定跨平台={is_cross}") + + custom_payload = order_payload if message_type == 'order' else None + text_content = message_text + + # ---------- 先向我方群发送消息(永远执行) ---------- + local_ok = send_group_message( + appkey=self_appkey, + secret=self_secret, + group_id=real_group_id, + sender_id=sender_id, + sender_name=sender_name, + sender_avatar=sender_avatar, + message_text=text_content, + custom_payload=custom_payload, + group_name=group_name, + order_id=order.dingdan_id, + is_cross=is_cross + ) + if not local_ok: + return Response({'code': 500, 'msg': '消息发送失败'}) + + # ---------- 跨平台转发(如果有对方订单ID和俱乐部ID) ---------- + if is_cross: + logger.info("[群聊代理] 满足跨平台条件,准备双平台发送") + partner_cfg = get_partner_club_config(partner_club_id) + if not partner_cfg or not partner_cfg.chat_api_key: + logger.warning("[群聊代理] 对方俱乐部配置不可用,仅发送我方群") + return Response({'code': 200, 'msg': '发送成功(对方GoEasy未配置)'}) + + partner_appkey = partner_cfg.chat_api_key + partner_secret = partner_cfg.chat_api_id or '' + partner_group_id = f"group_{partner_order_id}" + + # 构造代理身份 + agent_sender_id = f"{order.dingdan_id}_{sender_id}" + if identity_type == 'dashou': + agent_sender_name = f"[我方]{sender_name}" + elif identity_type == 'shangjia': + agent_sender_name = f"[我方商家]{sender_name}" + else: + agent_sender_name = f"[我方老板]{sender_name}" + agent_avatar = full_avatar('') + + logger.info(f"[群聊代理] 对方群组={partner_group_id}, 代理身份={agent_sender_id}/{agent_sender_name}") + + # 向对方群发送代理消息 + partner_ok = send_group_message( + appkey=partner_appkey, + secret=partner_secret, + group_id=partner_group_id, + sender_id=agent_sender_id, + sender_name=agent_sender_name, + sender_avatar=agent_avatar, + message_text=text_content, + custom_payload=custom_payload, + group_name='跨平台订单交流', + order_id=partner_order_id, + is_cross=1 + ) + logger.info(f"[群聊代理] 对方群发送结果: {partner_ok}") + + if partner_ok: + return Response({'code': 200, 'msg': '发送成功(双平台)'}) + else: + return Response({'code': 200, 'msg': '我方发送成功,对方发送失败'}) + else: + logger.info("[群聊代理] 非跨平台,仅发送我方群") + return Response({'code': 200, 'msg': '发送成功'}) + + +#通过跨平台字段判断是否是跨平台 +'''class DaiLiQunLiaoXiaoXiView(APIView): + """ + 代理发送群聊消息 + - 永远向我方群 group_{订单ID} 发送真实身份消息 + - 如果是跨平台订单且存在 partner_order_id + partner_club_id,且对方配置有效, + 则同时向对方群 group_{partner_order_id} 发送代理消息(不管派单方向) + - 前端传递 identityType: 'dashou' / 'shangjia' / 'boss' + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + # ===== 1. 参数解析 ===== + order_id = request.data.get('orderId') + identity_type = request.data.get('identityType') # 原有字段:dashou / shangjia / boss + sender_name = request.data.get('senderName', '') + sender_avatar_raw = request.data.get('senderAvatar', '') + message_type = request.data.get('messageType', 'text') + message_text = request.data.get('text', '') + order_payload = request.data.get('orderPayload', None) + + logger.info(f"[群聊代理] orderId={order_id}, identityType={identity_type}, text={message_text[:20]}...") + + if not order_id or not identity_type: + return Response({'code': 400, 'msg': '缺少 orderId 或 identityType'}) + if identity_type not in ('dashou', 'shangjia', 'boss'): + return Response({'code': 400, 'msg': 'identityType 无效 (dashou/shangjia/boss)'}) + + # ===== 2. 构造 senderId ===== + uid = request.user.yonghuid + prefix_map = {'dashou': 'Ds', 'shangjia': 'Sj', 'boss': 'Boss'} + sender_id = prefix_map[identity_type] + uid + # 记录真实身份名称(用于代理命名) + identity_name_map = {'dashou': '打手', 'shangjia': '商家', 'boss': '老板'} + identity_name = identity_name_map[identity_type] + logger.info(f"[群聊代理] 用户 yonghuid={uid}, senderId={sender_id}, 身份={identity_name}") + + # ===== 3. 查询订单 ===== + try: + order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=order_id) + except Dingdan.DoesNotExist: + logger.warning(f"[群聊代理] 订单 {order_id} 不存在") + return Response({'code': 404, 'msg': '订单不存在'}) + + # ===== 4. 权限校验 ===== + valid, err_msg = check_user_permission(identity_type, uid, order) + if not valid: + logger.warning(f"[群聊代理] 权限校验失败: {err_msg}, uid={uid}, role={identity_type}") + return Response({'code': 403, 'msg': err_msg}) + + # ===== 5. 我方 GoEasy 配置 ===== + self_appkey = get_self_goeasy_appkey() + self_secret = get_self_goeasy_secret() + if not self_appkey: + logger.error("[群聊代理] 我方 GoEasy AppKey 未配置") + return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) + + # ===== 6. 公共群信息(我方) ===== + real_group_id = f"group_{order.dingdan_id}" + group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else f"订单{order.dingdan_id}" + sender_avatar = full_avatar(sender_avatar_raw) if sender_avatar_raw else full_avatar('') + is_cross = order.is_cross + + # ===== 7. 先发送到我方群(永远执行) ===== + local_ok = send_group_message( + appkey=self_appkey, + secret=self_secret, + group_id=real_group_id, + sender_id=sender_id, + sender_name=sender_name, + sender_avatar=sender_avatar, + message_text=message_text, + custom_payload=order_payload if message_type == 'order' else None, + group_name=group_name, + order_id=order.dingdan_id, + is_cross=1 if is_cross == 1 else 0 + ) + logger.info(f"[群聊代理] 我方群发送结果: {local_ok}") + if not local_ok: + return Response({'code': 500, 'msg': '消息发送失败'}) + + # ===== 8. 判断是否需要向对方群发代理消息 ===== + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + + # 非跨平台,或者缺少对方订单/俱乐部ID,直接结束 + if is_cross != 1 or not partner_club_id or not partner_order_id: + logger.info("[群聊代理] 未满足跨平台双发条件,仅发送我方群") + return Response({'code': 200, 'msg': '发送成功'}) + + # 获取对方俱乐部配置 + partner_cfg = get_partner_club_config(partner_club_id) + if not partner_cfg or not partner_cfg.chat_api_key: + logger.warning(f"[群聊代理] 对方俱乐部配置不可用,仅发送我方群") + return Response({'code': 200, 'msg': '发送成功(对方GoEasy未配置)'}) + + # ===== 9. 构造代理身份并发送到对方群 ===== + agent_sender_id = f"{order.dingdan_id}_{sender_id}" # 代理ID + agent_sender_name = f"[我方{identity_name}]{uid}" # 代理名称:如 [我方打手]1234567 + agent_avatar = full_avatar('') + + partner_group_id = f"group_{partner_order_id}" + logger.info(f"[群聊代理] 对方群组={partner_group_id}, 代理身份={agent_sender_id}/{agent_sender_name}") + + partner_ok = send_group_message( + appkey=partner_cfg.chat_api_key, + secret=partner_cfg.chat_api_id or '', + group_id=partner_group_id, + sender_id=agent_sender_id, + sender_name=agent_sender_name, + sender_avatar=agent_avatar, + message_text=message_text, + custom_payload=order_payload if message_type == 'order' else None, + group_name='跨平台订单交流', + order_id=partner_order_id, + is_cross=1 + ) + logger.info(f"[群聊代理] 对方群发送结果: {partner_ok}") + + if partner_ok: + return Response({'code': 200, 'msg': '发送成功(双平台)'}) + else: + return Response({'code': 200, 'msg': '我方发送成功,对方发送失败'})''' + + + + + + + + + + + + +from peizhi.models import ClubConfig, ShangjiaLianjie +logger = logging.getLogger('dingdan') + +# ---------- 基础配置 ---------- +def get_self_goeasy_appkey(): + try: + cfg = ClubConfig.objects.filter(is_self=1).first() + return cfg.chat_api_key if cfg else None + except Exception: + return None + +def get_self_goeasy_secret(): + try: + cfg = ClubConfig.objects.filter(is_self=1).first() + return cfg.chat_api_id if cfg else None + except Exception: + return None + +def get_partner_club_config(partner_club_id): + try: + return ClubConfig.objects.get(club_id=partner_club_id, is_self=0) + except ClubConfig.DoesNotExist: + return None + + +class ShangjiaLianjieLiuYanZhuanFaView(APIView): + """ + 商家链接留言转发接口 + 路径:POST /dingdan/sjljlbxxzf + 固定以 B{商家ID} 身份发送群消息。通过 partner_order_id/partner_club_id 判断是否跨平台。 + """ + permission_classes = [] + + def post(self, request): + token = request.data.get('token') + text = request.data.get('text', '') + + if not token or not text: + return Response({'code': 400, 'msg': '缺少参数'}) + + try: + lianjie = ShangjiaLianjie.objects.get(lianjie_token=token) + except ShangjiaLianjie.DoesNotExist: + return Response({'code': 404, 'msg': '链接不存在或已失效'}) + + if not lianjie.dingdan_id: + return Response({'code': 400, 'msg': '链接缺少订单ID'}) + + try: + order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=lianjie.dingdan_id) + except Dingdan.DoesNotExist: + return Response({'code': 500, 'msg': '订单数据异常'}) + + shangjia_uid = lianjie.yonghu_id + sender_id = f"B{shangjia_uid}" + sender_name = "老板" + sender_avatar = '' + + appkey = get_self_goeasy_appkey() + secret = get_self_goeasy_secret() + if not appkey: + return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) + + group_id = f"group_{order.dingdan_id}" + group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else f"订单{order.dingdan_id}" + + local_cross_flag = 1 if (order.partner_order_id and order.partner_club_id) else 0 + + if not _subscribe_users_to_group([sender_id], [group_id], appkey, secret): + return Response({'code': 500, 'msg': '订阅群组失败'}) + + local_ok = _send_group_message( + appkey=appkey, + secret=secret, + group_id=group_id, + sender_id=sender_id, + sender_name=sender_name, + sender_avatar=sender_avatar, + message_text=text, + custom_payload=None, + group_name=group_name, + order_id=order.dingdan_id, + is_cross=local_cross_flag + ) + if not local_ok: + return Response({'code': 500, 'msg': '消息发送失败'}) + + # 跨平台转发 + partner_order_id = order.partner_order_id + partner_club_id = order.partner_club_id + + if not partner_order_id or not partner_club_id: + return Response({'code': 200, 'msg': '发送成功'}) + + partner_cfg = get_partner_club_config(partner_club_id) + if not partner_cfg or not partner_cfg.chat_api_key: + return Response({'code': 200, 'msg': '发送成功(对方未配置)'}) + + agent_sender_id = f"{order.dingdan_id}_{sender_id}" + agent_sender_name = f"[我方老板]{shangjia_uid}" + + partner_group_id = f"group_{partner_order_id}" + + partner_ok = _send_group_message( + appkey=partner_cfg.chat_api_key, + secret=partner_cfg.chat_api_id or '', + group_id=partner_group_id, + sender_id=agent_sender_id, + sender_name=agent_sender_name, + sender_avatar='', + message_text=text, + custom_payload=None, + group_name='跨平台订单交流', + order_id=partner_order_id, + is_cross=1 + ) + + if partner_ok: + return Response({'code': 200, 'msg': '发送成功(双平台)'}) + else: + return Response({'code': 200, 'msg': '我方发送成功,对方发送失败'}) + + + + + + + + + + + + + +'''class DaiLiQunLiaoXiaoXiView(APIView): + """ + 代理发送群聊消息 + 支持: + - 非跨平台订单:直接用我方 GoEasy Key 以真实身份发送 + - 跨平台订单(我方派单):同上 + - 跨平台订单(对方派单):除了我方群外,还需用对方 GoEasy Key 以代理身份向对方群发送 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + # ===== 1. 参数解析 ===== + order_id = request.data.get('orderId') + identity_type = request.data.get('identityType') # 前端必传:dashou / shangjia / boss + sender_name = request.data.get('senderName', '') + sender_avatar_raw = request.data.get('senderAvatar', '') + message_type = request.data.get('messageType', 'text') + message_text = request.data.get('text', '') + order_payload = request.data.get('orderPayload', None) + + logger.info(f"[群聊代理] 收到请求: orderId={order_id}, identityType={identity_type}, " + f"messageType={message_type}, text={message_text[:20]}...") + + if not order_id or not identity_type: + return Response({'code': 400, 'msg': '缺少 orderId 或 identityType'}) + if identity_type not in ('dashou', 'shangjia', 'boss'): + return Response({'code': 400, 'msg': 'identityType 无效'}) + + # ===== 2. 构造 senderId ===== + uid = request.user.yonghuid + prefix_map = {'dashou': 'Ds', 'shangjia': 'Sj', 'boss': 'Boss'} + sender_id = prefix_map[identity_type] + uid + logger.info(f"[群聊代理] 登录用户 yonghuid={uid}, 构造 senderId={sender_id}") + + # ===== 3. 查询订单 ===== + try: + order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=order_id) + except Dingdan.DoesNotExist: + logger.warning(f"[群聊代理] 订单 {order_id} 不存在") + return Response({'code': 404, 'msg': '订单不存在'}) + + # ===== 4. 权限校验 ===== + valid, err_msg = check_user_permission(identity_type, uid, order) + if not valid: + logger.warning(f"[群聊代理] 权限校验失败: {err_msg}, uid={uid}, identity={identity_type}") + return Response({'code': 403, 'msg': err_msg}) + + # ===== 5. 我方 GoEasy 配置 ===== + self_appkey = get_self_goeasy_appkey() + self_secret = get_self_goeasy_secret() + if not self_appkey: + logger.error("[群聊代理] 我方 GoEasy AppKey 未配置") + return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) + + # ===== 6. 公共群信息 ===== + real_group_id = f"group_{order.dingdan_id}" + group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else f"订单{order.dingdan_id}" + sender_avatar = full_avatar(sender_avatar_raw) if sender_avatar_raw else full_avatar('') + + # ===== 7. 订阅发送者到群(确保在群内) ===== + if not subscribe_users_to_group([sender_id], [real_group_id], self_appkey, self_secret): + logger.error("[群聊代理] 订阅群组失败") + return Response({'code': 500, 'msg': '订阅群组失败'}) + + # ===== 8. 判断跨平台状态 ===== + is_cross = order.is_cross + dispatch_type = order.dispatch_type # 1=我方派单, 2=对方派单 + logger.info(f"[群聊代理] 订单 {order_id} is_cross={is_cross}, dispatch_type={dispatch_type}") + + custom_payload = order_payload if message_type == 'order' else None + text_content = message_text + + # ---------- 非跨平台 或 我方派单 ---------- + if is_cross != 1 or dispatch_type == 1: + logger.info("[群聊代理] 走非跨平台/我方派单发送通道") + success = send_group_message( + appkey=self_appkey, + secret=self_secret, + group_id=real_group_id, + sender_id=sender_id, + sender_name=sender_name, + sender_avatar=sender_avatar, + message_text=text_content, + custom_payload=custom_payload, + group_name=group_name, + order_id=order.dingdan_id, + is_cross=1 if is_cross == 1 else 0 + ) + if success: + return Response({'code': 200, 'msg': '发送成功'}) + else: + return Response({'code': 500, 'msg': '消息发送失败'}) + + # ---------- 跨平台且对方派单 ---------- + logger.info("[群聊代理] 检测到跨平台对方派单,准备双平台发送") + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + + if not partner_club_id or not partner_order_id: + logger.warning("[群聊代理] 缺少 partner_club_id 或 partner_order_id,降级为本地发送") + success = send_group_message( + self_appkey, self_secret, real_group_id, sender_id, sender_name, + sender_avatar, text_content, custom_payload, group_name, order.dingdan_id, is_cross=1 + ) + return Response({'code': 200, 'msg': '发送成功(对方信息缺失)'} if success else ('发送失败', 500)) + + partner_cfg = get_partner_club_config(partner_club_id) + if not partner_cfg or not partner_cfg.chat_api_key: + logger.warning("[群聊代理] 对方俱乐部配置不可用,仅发送我方群") + success = send_group_message( + self_appkey, self_secret, real_group_id, sender_id, sender_name, + sender_avatar, text_content, custom_payload, group_name, order.dingdan_id, is_cross=1 + ) + return Response({'code': 200, 'msg': '发送成功(对方GoEasy未配置)'} if success else ('发送失败', 500)) + + partner_appkey = partner_cfg.chat_api_key + partner_secret = partner_cfg.chat_api_id or '' + partner_group_id = f"group_{partner_order_id}" + + # 构造代理身份(参考 chat_utils.py) + agent_sender_id = f"{order.dingdan_id}_{sender_id}" + if identity_type == 'dashou': + agent_sender_name = f"[我方]{sender_name}" + elif identity_type == 'shangjia': + agent_sender_name = f"[我方商家]{sender_name}" + else: + agent_sender_name = f"[我方老板]{sender_name}" + agent_avatar = full_avatar('') # 可定制 + + logger.info(f"[群聊代理] 对方群组={partner_group_id}, 代理身份={agent_sender_id}/{agent_sender_name}") + + # 1) 在我方群发送真实身份消息 + local_ok = send_group_message( + self_appkey, self_secret, real_group_id, sender_id, sender_name, + sender_avatar, text_content, custom_payload, group_name, order.dingdan_id, is_cross=1 + ) + logger.info(f"[群聊代理] 我方群发送结果: {local_ok}") + + # 2) 在对方群发送代理身份消息 + partner_ok = send_group_message( + partner_appkey, partner_secret, partner_group_id, + agent_sender_id, agent_sender_name, agent_avatar, + text_content, custom_payload, + '跨平台订单交流', partner_order_id, is_cross=1 + ) + logger.info(f"[群聊代理] 对方群发送结果: {partner_ok}") + + if local_ok and partner_ok: + return Response({'code': 200, 'msg': '发送成功(双平台)'}) + elif local_ok: + return Response({'code': 200, 'msg': '我方发送成功,对方发送失败'}) + else: + return Response({'code': 500, 'msg': '消息发送失败'})''' + + + +from peizhi.models import ClubConfig +from utils.chat_utils import establish_order_chat # 【新增】导入聊天工具 +class QiangdanView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request): + data = request.data + dingdan_id = data.get('dingdan_id') + if not dingdan_id: + return Response({'code': 400, 'msg': '订单ID不能为空'}, status=400) + + try: + with transaction.atomic(): + # 1. 验证打手身份和状态 + dashou_profile = self._validate_dashou_status(request.user) + if not dashou_profile: + return Response({'code': 403, 'msg': '您不是打手或账号状态异常'}, status=403) + + + # ==================== 【新增】罚单检查 ==================== + 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}) + # =========================================================== + + # 2. 锁定并验证订单(行锁) + order_result = self._validate_and_lock_order(dingdan_id, request.user.yonghuid) + if isinstance(order_result, str): + return Response({'code': 400, 'msg': order_result}) + order = order_result + + is_cross = order.is_cross == 1 + dispatch_type = order.dispatch_type + + if is_cross and dispatch_type == 2: + validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) + if not validation_result['success']: + return Response({'code': 400, 'msg': validation_result['message']}) + + claim_allowed = self._request_partner_check_claim(order, request.user.yonghuid) + if claim_allowed is None: + return Response({'code': 500, 'msg': '跨平台通信失败,请重试'}, status=500) + elif claim_allowed is False: + if order.zhuangtai == 1: + order.zhuangtai = 21 + order.save() + logger.info(f"订单 {dingdan_id} 被对方拒绝抢单,状态改为21") + else: + order.zhuangtai = 20 + order.save() + logger.info(f"订单 {dingdan_id} 已被其他平台抢走,状态改为20") + return Response({'code': 400, 'msg': '订单已被其他平台打手抢走或不可抢'}) + + self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) + + try: + update_daily_dispatch_stat( + direction=2, + partner_club_id=order.partner_club_id or '000001', + claimed_amount=order.jine, + claimed_count=1 + ) + except Exception as e: + logger.error(f"更新抢单统计失败: {e}", exc_info=True) + + elif is_cross and dispatch_type == 1: + validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) + if not validation_result['success']: + return Response({'code': 400, 'msg': validation_result['message']}) + + self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) + self._async_notify_partners_order_claimed(order) + + else: + validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) + if not validation_result['success']: + return Response({'code': 400, 'msg': validation_result['message']}) + self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) + + # ========== 【唯一改动】聊天建立方式 ========== + chat_success = establish_order_chat(dingdan_id) + + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=1 + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + + + # 在 QiangdanView.post 里,_execute_qiangdan 之后添加: + from dengji.services import record_dashou_biaoxian + # 记录抢单表现(不影响主流程) + # 如果订单表里有 bankuai 信息,可以用 order.bankuai 代替 None + record_dashou_biaoxian( + dingdan_id=dingdan_id, + dashou_id=order.jiedan_dashou_id, + xingwei=1, + jiedan_time=timezone.now(), # 直接取当前时间 + bankuai=None # 暂不填,后续提交时补上 + ) + + return Response({ + 'code': 200, + 'msg': '抢单成功', + 'data': { + 'dingdan_id': dingdan_id, + 'chat_established': chat_success + } + }) + + except Exception as e: + logger.error(f"抢单异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '系统异常,请稍后重试'}, status=500) + + # ---------- 以下所有方法原封不动照抄旧版 ---------- + def _validate_dashou_status(self, user): + try: + dashou_profile = user.dashou_profile + if (dashou_profile.zhuangtai != 1 or + dashou_profile.zhanghaozhuangtai != 1 or + dashou_profile.jifen < 5): + return None + return dashou_profile + except UserDashou.DoesNotExist: + return None + + def _validate_and_lock_order(self, dingdan_id, dashou_yonghuid): + try: + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + if order.zhuangtai not in [1, 7]: + return "订单状态不可抢" + if order.zhuangtai == 7: + if not order.zhiding_id or order.zhiding_id != dashou_yonghuid: + return "此订单为指定单,您不是指定打手" + if order.jiedan_dashou_id: + return "订单已被其他打手抢走" + return order + except Dingdan.DoesNotExist: + return "订单不存在" + + def _validate_order_requirements(self, order, dashou_profile, dashou_yonghuid): + if order.yaoqiuleixing == 1: + return self._validate_huiyuan_requirement(order, dashou_yonghuid) + elif order.yaoqiuleixing == 2: + return self._validate_yajin_requirement(order, dashou_profile) + return {'success': True, 'message': '验证通过'} + + def _validate_huiyuan_requirement(self, order, dashou_yonghuid): + try: + huiyuan_record = Huiyuangoumai.objects.get( + huiyuan_id=order.huiyuan_id, + yonghu_id=dashou_yonghuid + ) + if huiyuan_record.jiance_shifou_daoqi() or huiyuan_record.huiyuan_zhuangtai != 1: + return {'success': False, 'message': '会员已过期或未开通对应会员'} + return {'success': True, 'message': '会员验证通过'} + except Huiyuangoumai.DoesNotExist: + return {'success': False, 'message': '未开通对应会员,无法抢此订单'} + + def _validate_yajin_requirement(self, order, dashou_profile): + yajin_required = order.yongjin or 0 + dashou_yajin = dashou_profile.yajin or 0 + if dashou_yajin < yajin_required: + return {'success': False, 'message': f'押金不足,需要{yajin_required}元,当前有{dashou_yajin}元'} + return {'success': True, 'message': '押金验证通过'} + + def _execute_qiangdan(self, order, dashou_profile, dashou_yonghuid): + order.zhuangtai = 2 + order.jiedan_dashou_id = dashou_yonghuid + order.user2_id = f"Ds{dashou_yonghuid}" + order.save() + + dashou_profile.zhuangtai = 0 + dashou_profile.jiedanzongliang = F('jiedanzongliang') + 1 + dashou_profile.jinrijiedan = F('jinrijiedan') + 1 + dashou_profile.jinyuejiedan = F('jinyuejiedan') + 1 + dashou_profile.save() + + # ---------- 跨平台询问对方(照抄) ---------- + def _request_partner_check_claim(self, order, dashou_id): + partner_club_id = order.partner_club_id or '000001' + try: + club = Club.objects.filter(partner_club_id=partner_club_id).first() + if not club or not club.partner_domain: + logger.error(f"未找到对方俱乐部配置: partner_club_id={partner_club_id}") + return None + + my_club = Club.objects.filter(partner_club_id=club.partner_club_id).first() + if not my_club: + logger.error(f"未找到我方俱乐部配置: partner_club_id={club.partner_club_id}") + return None + + payload = { + 'token': settings.CROSS_PLATFORM_TOKEN, + 'wofangdingdan_id': order.partner_order_id, + 'partner_order_id': order.dingdan_id, + 'our_club_id': my_club.club_id, + 'partner_club_id': club.partner_club_id, + 'dashou_id': dashou_id, + 'sj_id': f"Sj{settings.AGENT_SHANGJIA_ID}" + } + + url = club.partner_domain.rstrip('/') + '/dingdan/partner_check_claim' + resp = requests.post(url, json=payload, timeout=5) + + if resp.status_code != 200: + logger.error(f"跨平台抢单询问HTTP失败: {resp.status_code}") + return None + + data = resp.json() + if data.get('code') == 0: + return data.get('data', {}).get('can_claim') is True + else: + logger.error(f"跨平台抢单询问业务失败: {data}") + return False + except Exception as e: + logger.error(f"跨平台抢单询问异常: {e}", exc_info=True) + return None + + # ---------- 异步通知其他俱乐部 ---------- + def _async_notify_partners_order_claimed(self, order): + def notify_task(): + clubs = Club.objects.filter(is_interop_enabled=True).select_related() + for club in clubs: + for attempt in range(1, 4): + success = self._send_claim_notification_to_partner(order, club) + if success: + break + if attempt < 3: + time.sleep(2 ** attempt) + else: + logger.error(f"通知俱乐部 {club.club_id} 订单已抢失败,重试3次后仍失败") + + thread = threading.Thread(target=notify_task) + thread.daemon = True + thread.start() + + def _send_claim_notification_to_partner(self, order, club): + try: + my_club = Club.objects.filter(partner_club_id=club.partner_club_id).first() + if not my_club: + logger.warning(f"未找到我方俱乐部配置: partner_club_id={club.partner_club_id}") + return False + + payload = { + 'token': settings.CROSS_PLATFORM_TOKEN, + 'our_club_id': my_club.club_id, + 'partner_club_id': club.partner_club_id, + 'partner_order_id': order.dingdan_id, + 'claimed': True + } + url = club.partner_domain.rstrip('/') + '/dingdan/partner_notify_claim' + resp = requests.post(url, json=payload, timeout=3) + return resp.status_code == 200 + except Exception as e: + logger.error(f"通知俱乐部 {club.club_id} 异常: {e}") + return False + + + + + + + +class DashouDingdanHuoquView1(APIView): + """ + 打手订单获取接口 + 支持筛选条件: + - zhuangtai_list: 订单状态列表(可选,空数组或不传表示全部状态) + - leixing_id: 商品类型ID(可选) + - keyword: 搜索关键词,模糊匹配订单ID和订单介绍(可选) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取当前登录用户的 yonghuid + try: + current_user = request.user + yonghuid = current_user.yonghuid + except AttributeError: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 获取并校验前端参数 + try: + data = request.data + zhuangtai_list = data.get('zhuangtai_list', []) # 可选,默认空列表(全部) + page = max(1, int(data.get('page', 1))) + page_size = min(100, max(1, int(data.get('page_size', 10)))) + leixing_id = data.get('leixing_id', None) # 可选,商品类型ID + keyword = data.get('keyword', '').strip() # 可选,搜索关键词 + except (ValueError, TypeError): + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 参数默认值与类型修正 + if not isinstance(zhuangtai_list, list): + zhuangtai_list = [] # 非列表则视为全部 + if leixing_id is not None: + try: + leixing_id = int(leixing_id) + except (ValueError, TypeError): + leixing_id = None + + # 4. 构建查询条件 + try: + # 基础条件:当前打手接单 + query = Q(jiedan_dashou_id=yonghuid) + + # 状态筛选:如果 zhuangtai_list 不为空列表,则加入筛选;否则查询全部状态 + if zhuangtai_list: + query &= Q(zhuangtai__in=zhuangtai_list) + + # 商品类型筛选 + if leixing_id is not None: + query &= Q(leixing_id=leixing_id) + + # 关键词搜索:模糊匹配订单ID和订单介绍 + if keyword: + keyword_query = Q(dingdan_id__icontains=keyword) | Q(jieshao__icontains=keyword) + query &= keyword_query + + # 5. 执行查询,按创建时间倒序 + dingdan_query = Dingdan.objects.filter(query).order_by('-create_time') + + # 只选择需要的字段,提高性能 + dingdan_query = dingdan_query.only( + 'dingdan_id', + 'zhuangtai', + 'dashou_fencheng', # 打手分成金额 + 'jieshao', + 'tupian', + 'nicheng', + 'create_time' + ) + + total_count = dingdan_query.count() + + # 6. 分页处理 + start_index = (page - 1) * page_size + end_index = start_index + page_size + current_page_orders = dingdan_query[start_index:end_index] + current_page_count = len(current_page_orders) + + # 7. 构建返回数据列表 + order_list = [] + for order in current_page_orders: + order_data = { + 'dingdan_id': order.dingdan_id, + 'zhuangtai': order.zhuangtai, + 'jine': float(order.dashou_fencheng) if order.dashou_fencheng else 0.0, + 'jieshao': order.jieshao or '', + 'tupian': order.tupian or '', + 'nicheng': order.nicheng or '', + 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '' + } + order_list.append(order_data) + + # 8. 判断是否还有更多数据 + has_more = (current_page_count == page_size) and (end_index < total_count) + if total_count == 0: + has_more = False + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': { + 'list': order_list, + 'has_more': has_more, + 'total': total_count + } + }, 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({ + 'code': 4, + 'msg': '服务器内部错误', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +'''class DashouDingdanHuoquView1(APIView): + """打手订单获取接口""" + permission_classes = [IsAuthenticated] # JWT认证 + + def post(self, request): + # 1. 获取当前登录用户的yonghuid + try: + current_user = request.user + yonghuid = current_user.yonghuid + + except AttributeError: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 获取和验证前端参数 + try: + data = request.data + zhuangtai_list = data.get('zhuangtai_list', []) + page = max(1, int(data.get('page', 1))) + page_size = min(100, max(1, int(data.get('page_size', 10)))) + + + + except (ValueError, TypeError): + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 验证状态列表参数 + if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0: + return Response({ + 'code': 3, + 'msg': '状态列表不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 4. 构建查询条件 - 直接查询订单主表 + # 条件1: 当前打手是接单打手 + # 条件2: 订单状态在指定列表中 + + # 注意:打手看不到状态1和7的订单(已下单、指定中) + # 但这里由前端控制传递的zhuangtai_list决定 + dashou_query = Q(jiedan_dashou_id=yonghuid) + zhuangtai_query = Q(zhuangtai__in=zhuangtai_list) + + # 5. 执行查询 - 只查询需要的字段,按创建时间倒序 + dingdan_query = Dingdan.objects.filter( + dashou_query & zhuangtai_query + ).order_by('-create_time') + + # 只选择需要的字段,提高性能 + dingdan_query = dingdan_query.only( + 'dingdan_id', + 'zhuangtai', + 'dashou_fencheng', # ✅ 关键:打手分成,不是jine + 'jieshao', + 'tupian', + 'nicheng', + 'create_time' + ) + + total_count = dingdan_query.count() + + + # 6. 计算分页 + start_index = (page - 1) * page_size + end_index = start_index + page_size + + # 获取当前页的订单数据 + current_page_orders = dingdan_query[start_index:end_index] + current_page_count = len(current_page_orders) + + # 7. 构建返回数据 - 只返回前端需要的字段 + order_list = [] + for order in current_page_orders: + order_data = { + # 核心字段 - 必须与前端完全对应 + 'dingdan_id': order.dingdan_id, # 订单ID + 'zhuangtai': order.zhuangtai, # 订单状态码 + # ✅ 关键:这里返回的是打手分成,不是订单金额 + 'jine': float(order.dashou_fencheng) if order.dashou_fencheng else 0.0, + 'jieshao': order.jieshao or '', # 订单介绍 + 'tupian': order.tupian or '', # 商品图片URL + 'nicheng': order.nicheng or '', # 游戏昵称 + 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '' + } + order_list.append(order_data) + + + + # 8. 判断是否还有更多数据 + # 条件1: 当前页获取了满page_size条数据 + # 条件2: 还有更多数据没取完 + has_more = current_page_count == page_size and end_index < total_count + + # 如果总数为0,直接没有更多 + if total_count == 0: + has_more = False + + + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': { + 'list': order_list, # 订单列表 + 'has_more': has_more, # 是否还有更多数据 + 'total': total_count # 总订单数(可选) + } + }, 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({ + 'code': 4, + 'msg': '服务器内部错误', + 'data': None + }, 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上传凭证接口(使用腾讯云STS临时密钥)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + + + # 1. 接收参数 + dingdan_id = request.data.get('dingdan_id', '').strip() + if not dingdan_id: + return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST) + + # 2. 业务验证逻辑(保持你的原有逻辑) + try: + from .models import Dingdan + dingdan = Dingdan.objects.get( + dingdan_id=dingdan_id, + jiedan_dashou_id=request.user.yonghuid, + zhuangtai=2 + ) + + except Dingdan.DoesNotExist: + return Response({'code': 5, 'msg': '订单无权限或状态不对', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + except Exception as e: + + return Response({'code': 500, 'msg': '服务器验证异常', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 3. 获取腾讯云STS临时密钥 + try: + # 初始化固定凭证 + cred = credential.Credential( + settings.COS_SECRET_ID, + settings.COS_SECRET_KEY + ) + + # 创建STS客户端 + http_profile = HttpProfile() + http_profile.endpoint = "sts.tencentcloudapi.com" + client_profile = ClientProfile() + client_profile.httpProfile = http_profile + + client = sts_client.StsClient(cred, "ap-shanghai", client_profile) + + # 构建策略(允许上传到订单目录) + policy = { + "version": "2.0", + "statement": [ + { + "effect": "allow", + "action": [ + "cos:PutObject", + "cos:PostObject", + "cos:InitiateMultipartUpload", + "cos:UploadPart", + "cos:CompleteMultipartUpload" + ], + "resource": [ + f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*" + ] + } + ] + } + + # 请求STS临时凭证 + req = models.GetFederationTokenRequest() + req.Name = f"dashou_{dingdan_id}_{int(time.time())}" + req.Policy = json.dumps(policy) + req.DurationSeconds = 7200 # 2小时有效期 + + resp = client.GetFederationToken(req) + + # 提取临时密钥 + credentials = resp.Credentials + + + except TencentCloudSDKException as e: + logger.error(f"获取STS临时密钥失败: {str(e)}") + return Response({ + 'code': 500, + 'msg': f'获取上传凭证失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 4. 构建响应数据(严格遵循小程序COS SDK要求) + current_time = int(time.time()) + response_data = { + 'credentials': { + 'tmpSecretId': credentials.TmpSecretId, # 临时SecretId + 'tmpSecretKey': credentials.TmpSecretKey, # 临时SecretKey + 'sessionToken': credentials.Token, # 必须的Token + }, + 'startTime': current_time, + 'expiredTime': current_time + 7200, + 'bucket': settings.COS_BUCKET, + 'region': settings.COS_REGION, + 'upload_dir': f'order/{dingdan_id}/' + } + + + + # 5. 返回成功 + return Response({ + 'code': 0, + 'msg': '成功', + 'data': response_data + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f'COS凭证接口未知错误: {str(e)}', exc_info=True) + return Response({ + 'code': 500, + 'msg': f'服务器内部错误: {str(e)}', + 'data': None + }, 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上传凭证接口(调试版本)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 接收参数(修复 int 无 strip 问题) + dingdan_id = str(request.data.get('dingdan_id', '')).strip() + yongtu = request.data.get('yongtu', 'dingdan') + + # 2. 条件判断修正:除了 fakuan 之外,其他用途均需提供 dingdan_id + if yongtu != 'fakuan' and not dingdan_id: + return Response( + {'code': 400, 'msg': '订单ID不能为空', 'error': 'dingdan_id is required when yongtu is not fakuan'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 3. 业务验证逻辑(根据用途分别验证) + if yongtu == 'chufa': + try: + from .models import Chufajilu + chufa_record = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + dashouid=request.user.yonghuid, + sqzhuangtai__in=[0, 3] + ).first() + if not chufa_record: + return Response( + {'code': 403, 'msg': '处罚记录无权限或状态不对', + 'error': f'No punishment record found for user {request.user.yonghuid} on order {dingdan_id}'}, + status=status.HTTP_403_FORBIDDEN + ) + except Exception as e: + logger.error(f"查处罚记录异常: {str(e)}") + return Response( + {'code': 500, 'msg': '服务器内部错误', 'error': str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + elif yongtu == 'fakuan': + # 罚款申诉不需要订单验证 + pass + else: + # 默认为订单相关,需要验证订单且打手必须接单 + try: + from .models import Dingdan + dingdan = Dingdan.objects.get( + dingdan_id=dingdan_id, + jiedan_dashou_id=request.user.yonghuid, + zhuangtai__in=[2, 8] + ) + except Dingdan.DoesNotExist: + return Response( + {'code': 403, 'msg': '此功能暂未开放', 'error': f'Order {dingdan_id} not found or not assigned to user {request.user.yonghuid}'}, + status=status.HTTP_403_FORBIDDEN + ) + + # 4. 获取腾讯云STS临时密钥 + try: + # 初始化固定凭证 + cred = credential.Credential( + settings.COS_SECRET_ID, + settings.COS_SECRET_KEY + ) + + # 创建STS客户端 + http_profile = HttpProfile() + http_profile.endpoint = "sts.tencentcloudapi.com" + client_profile = ClientProfile() + client_profile.httpProfile = http_profile + client = sts_client.StsClient(cred, "ap-shanghai", client_profile) + + # 构建策略资源 + resources = [] + if yongtu == 'chufa': + resources.append( + f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/a_long/chfajltp/dssstp/*" + ) + elif yongtu == 'fakuan': + resources.append( + f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/dashoufakuan/dashoufakuanshensu/*" + ) + else: + resources.append( + f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*" + ) + + policy = { + "version": "2.0", + "statement": [{ + "effect": "allow", + "action": [ + "cos:PutObject", + "cos:PostObject", + "cos:InitiateMultipartUpload", + "cos:UploadPart", + "cos:CompleteMultipartUpload" + ], + "resource": resources + }] + } + + req = models.GetFederationTokenRequest() + req.Name = f"dashou_{dingdan_id}_{int(time.time())}" + req.Policy = json.dumps(policy) + req.DurationSeconds = 7200 + + resp = client.GetFederationToken(req) + credentials = resp.Credentials + + except TencentCloudSDKException as e: + logger.error(f"获取STS临时密钥失败: {str(e)}") + return Response({ + 'code': 500, + 'msg': f'获取上传凭证失败: {str(e)}', + 'error_detail': str(e) + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 5. 构建响应 + current_time = int(time.time()) + upload_dir = 'fakuan/dashoufakuan/dashoufakuanshensu/' if yongtu == 'fakuan' else ( + 'a_long/chfajltp/dssstp/' if yongtu == 'chufa' else f'order/{dingdan_id}/' + ) + response_data = { + 'credentials': { + 'tmpSecretId': credentials.TmpSecretId, + 'tmpSecretKey': credentials.TmpSecretKey, + 'sessionToken': credentials.Token, + }, + 'startTime': current_time, + 'expiredTime': current_time + 7200, + 'bucket': settings.COS_BUCKET, + 'region': settings.COS_REGION, + 'upload_dir': upload_dir, + } + return Response({'code': 0, 'msg': '成功', 'data': response_data}) + + except Exception as e: + # 捕捉所有未预期的异常,返回完整 traceback + logger.error(f'COS凭证接口未知错误: {traceback.format_exc()}') + return Response({ + 'code': 500, + 'msg': f'服务器内部错误: {str(e)}', + 'error_detail': traceback.format_exc() + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +'''class DashouCOSZhengshuView(APIView): + """打手获取COS上传凭证接口(使用腾讯云STS临时密钥)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 接收参数 + dingdan_id = str(request.data.get('dingdan_id', '')).strip() + yongtu = request.data.get('yongtu', 'dingdan') # 🔴【新增】用途参数:dingdan=订单,chufa=处罚 + + if not dingdan_id and yongtu is not 'fakuan': + return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST) + + + + # 2. 业务验证逻辑 + try: + from .models import Dingdan + # 如果是处罚相关上传,验证处罚权限 + if yongtu == 'chufa': + # 验证处罚记录是否存在且属于当前打手 + from .models import Chufajilu + chufa_record = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + dashouid=request.user.yonghuid, + sqzhuangtai__in=[0, 3] # 待处罚或申诉中状态 + ).first() + + if not chufa_record: + return Response({'code': 5, 'msg': '处罚记录无权限或状态不对', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + elif yongtu == 'fakuan': + pass + else: + # 原有订单验证逻辑 + dingdan = Dingdan.objects.get( + dingdan_id=dingdan_id, + jiedan_dashou_id=request.user.yonghuid, + zhuangtai__in=[2,8] + #zhuangtai=2 + ) + + except Dingdan.DoesNotExist: + return Response({'code': 5, 'msg': '此功能暂未开放', 'data': None}) + except Exception as e: + logger.error(f"订单验证异常: {str(e)}") + return Response({'code': 500, 'msg': '服务器验证异常', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 3. 获取腾讯云STS临时密钥 + try: + # 初始化固定凭证 + cred = credential.Credential( + settings.COS_SECRET_ID, + settings.COS_SECRET_KEY + ) + + # 创建STS客户端 + http_profile = HttpProfile() + http_profile.endpoint = "sts.tencentcloudapi.com" + client_profile = ClientProfile() + client_profile.httpProfile = http_profile + + client = sts_client.StsClient(cred, "ap-shanghai", client_profile) + + # 🔴【修复】构建策略(允许上传到多个路径) + resources = [] + + if yongtu == 'chufa': + # 处罚申诉图片路径 + resources.append( + f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/a_long/chfajltp/dssstp/*" + ) + + elif yongtu == 'fakuan': + # 🔥 罚款申诉图片专用路径 + resources.append(f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/dashoufakuan/dashoufakuanshensu/*") + else: + # 订单相关图片路径 + resources.append( + f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*" + ) + + policy = { + "version": "2.0", + "statement": [ + { + "effect": "allow", + "action": [ + "cos:PutObject", + "cos:PostObject", + "cos:InitiateMultipartUpload", + "cos:UploadPart", + "cos:CompleteMultipartUpload" + ], + "resource": resources + } + ] + } + + # 请求STS临时凭证 + req = models.GetFederationTokenRequest() + req.Name = f"dashou_{dingdan_id}_{int(time.time())}" + req.Policy = json.dumps(policy) + req.DurationSeconds = 7200 # 2小时有效期 + + resp = client.GetFederationToken(req) + + # 提取临时密钥 + credentials = resp.Credentials + + except TencentCloudSDKException as e: + logger.error(f"获取STS临时密钥失败: {str(e)}") + return Response({ + 'code': 500, + 'msg': f'获取上传凭证失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 4. 构建响应数据 + current_time = int(time.time()) + response_data = { + 'credentials': { + 'tmpSecretId': credentials.TmpSecretId, + 'tmpSecretKey': credentials.TmpSecretKey, + 'sessionToken': credentials.Token, + }, + 'startTime': current_time, + 'expiredTime': current_time + 7200, + 'bucket': settings.COS_BUCKET, + 'region': settings.COS_REGION, + 'upload_dir': 'fakuan/dashoufakuan/dashoufakuanshensu/' if yongtu == 'fakuan' else ( + 'a_long/chfajltp/dssstp/' if yongtu == 'chufa' else f'order/{dingdan_id}/') + } + + # 5. 返回成功 + return Response({ + 'code': 0, + 'msg': '成功', + 'data': response_data + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f'COS凭证接口未知错误: {str(e)}', exc_info=True) + return Response({ + 'code': 500, + 'msg': f'服务器内部错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' + + +class ShangjiaCOSZhengshuView(APIView): + """商家获取COS上传凭证接口(用于处罚申诉图片上传)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 接收参数 + dingdan_id = request.data.get('dingdan_id', '').strip() + yongtu = request.data.get('yongtu', 'chufa') # 用途:chufa=处罚申诉 + + if not dingdan_id: + return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # 2. 🔴【修改】验证商家身份 - 通过反向查询shop_profile判断是否为商家 + try: + # 检查是否有商家扩展表 + shangjia_profile = request.user.shop_profile + + # 验证商家状态是否为1(正常) + if shangjia_profile.zhuangtai != 1: + return Response({'code': 3, 'msg': '商家账号状态异常,无法操作', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + # 🔴【重要】获取商家ID(从商家扩展表中获取) + shangjia_id = request.user.yonghuid # 假设商家扩展表中有yonghuid字段 + + except AttributeError: + # 没有shop_profile属性,说明不是商家 + return Response({'code': 3, 'msg': '非商家用户无权限', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + # 3. 🔴【修改】验证订单权限 - 通过商家订单扩展表验证 + try: + from .models import Dingdan, DingdanShangjia + + # 获取订单 + dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + + # 🔴【重要】通过反向查询商家扩展表验证订单是否属于当前商家 + try: + shangjia_kuozhan = dingdan.shangjia_kuozhan # 使用反向关系名 + + # 验证商家ID是否匹配 + if str(shangjia_kuozhan.shangjia_id) != str(shangjia_id): + return Response({'code': 2, 'msg': '订单不属于当前商家', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + # 验证订单状态是否允许申请处罚(根据前端逻辑) + allowed_zhuangtai = [2, 4, 5, 6, 8] + if dingdan.zhuangtai not in allowed_zhuangtai: + return Response({'code': 4, 'msg': '当前订单状态不允许申请处罚', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # 🔴【新增】检查是否已存在处罚申请(通过Chufajilu模型) + # 注意:这里假设Chufajilu模型存在,并且有shangjia_id字段 + try: + from .models import Chufajilu + existing_chufa = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + qingqiuid=shangjia_id, + sqzhuangtai__in=[0, 1] # 审核中或已处罚状态 + ).exists() + + if existing_chufa: + return Response({'code': 5, 'msg': '该订单已有处罚申请,请勿重复上传', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + except ImportError: + # 如果没有Chufajilu模型,跳过这个检查 + pass + + except DingdanShangjia.DoesNotExist: + # 没有商家扩展记录,说明这不是商家订单 + return Response({'code': 2, 'msg': '非商家订单无法操作', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + except Dingdan.DoesNotExist: + return Response({'code': 2, 'msg': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"订单验证异常: {str(e)}") + return Response({'code': 500, 'msg': '服务器验证异常', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 4. 获取腾讯云STS临时密钥 + try: + # 初始化固定凭证 + cred = credential.Credential( + settings.COS_SECRET_ID, + settings.COS_SECRET_KEY + ) + + # 创建STS客户端 + http_profile = HttpProfile() + http_profile.endpoint = "sts.tencentcloudapi.com" + client_profile = ClientProfile() + client_profile.httpProfile = http_profile + + client = sts_client.StsClient(cred, "ap-shanghai", client_profile) + + # 🔴【修改】构建策略 - 只允许上传到商家处罚证据图片目录 + resources = [ + f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/a_long/chfajltp/sjzjtp/*" + ] + + policy = { + "version": "2.0", + "statement": [ + { + "effect": "allow", + "action": [ + "cos:PutObject", + "cos:PostObject", + "cos:InitiateMultipartUpload", + "cos:UploadPart", + "cos:CompleteMultipartUpload" + ], + "resource": resources + } + ] + } + + # 请求STS临时凭证 + req = models.GetFederationTokenRequest() + # 🔴【修改】命名包含商家ID + req.Name = f"shangjia_{shangjia_id}_{dingdan_id}_{int(time.time())}" + req.Policy = json.dumps(policy) + req.DurationSeconds = 7200 # 2小时有效期 + + resp = client.GetFederationToken(req) + + # 提取临时密钥 + credentials = resp.Credentials + + except TencentCloudSDKException as e: + logger.error(f"商家获取STS临时密钥失败: {str(e)}") + return Response({ + 'code': 500, + 'msg': f'获取上传凭证失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 5. 构建响应数据 + current_time = int(time.time()) + response_data = { + 'credentials': { + 'tmpSecretId': credentials.TmpSecretId, + 'tmpSecretKey': credentials.TmpSecretKey, + 'sessionToken': credentials.Token, + }, + 'startTime': current_time, + 'expiredTime': current_time + 7200, + 'bucket': settings.COS_BUCKET, + 'region': settings.COS_REGION, + # 🔴【修改】商家专用上传目录 + 'upload_dir': 'a_long/chfajltp/sjzjtp/', + # 🔴【新增】返回商家ID和订单ID,便于前端生成文件名 + 'dingdan_id': dingdan_id, + 'shangjia_id': shangjia_id, + # 🔴【新增】可选:返回商家昵称,便于前端显示 + 'shangjia_nicheng': shangjia_profile.nicheng if hasattr(shangjia_profile, 'nicheng') else '' + } + + # 6. 返回成功 + return Response({ + 'code': 0, + 'msg': '成功', + 'data': response_data + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f'商家COS凭证接口未知错误: {str(e)}', exc_info=True) + return Response({ + 'code': 500, + 'msg': f'服务器内部错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class DashouTijiaoView(APIView): + """打手提交订单完成接口""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取参数 + data = request.data + dingdan_id = data.get('dingdan_id', '') + liuyan = data.get('liuyan', '') + jiaofu_tupian_urls = data.get('jiaofu_tupian_urls', []) + + # 2. 参数验证 + if not dingdan_id: + return Response({ + 'code': 1, + 'msg': '订单ID不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not isinstance(jiaofu_tupian_urls, list): + return Response({ + 'code': 2, + 'msg': '图片URL列表格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 限制图片数量(最多9张,最少1张) + if len(jiaofu_tupian_urls) == 0: + return Response({ + 'code': 3, + 'msg': '至少需要上传一张图片', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if len(jiaofu_tupian_urls) > 9: + return Response({ + 'code': 4, + 'msg': '最多只能上传9张图片', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 留言长度限制(50字) + if liuyan and len(liuyan) > 50: + return Response({ + 'code': 5, + 'msg': '留言最多50个字', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 获取当前用户并验证打手身份 + current_user = request.user + yonghuid = current_user.yonghuid + + try: + dashou_profile = current_user.dashou_profile + except UserDashou.DoesNotExist: + return Response({ + 'code': 6, + 'msg': '用户不是打手身份', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 4. 验证订单状态和权限 + try: + dingdan = Dingdan.objects.get( + dingdan_id=dingdan_id, + jiedan_dashou_id=yonghuid, + zhuangtai=2 # 必须是进行中的订单 + ) + except Dingdan.DoesNotExist: + return Response({ + 'code': 7, + 'msg': '订单不存在或没有提交权限', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 5. 存储图片到Dashoutupian表 + for tupian_url in jiaofu_tupian_urls: + # 创建打手提交图片记录 + Dashoutupian.objects.create( + dingdan_id=dingdan_id, + dashou=yonghuid, + tupian=tupian_url, + create_time=timezone.now() + ) + + # 6. 更新订单主表 + dingdan.dashou_liuyan = liuyan + dingdan.zhuangtai = 8 # 状态改为结算中 + dingdan.update_time = timezone.now() + dingdan.save() + + # 7. 更新打手扩展表状态 + dashou_profile.zhuangtai = 1 # 改为数字1(根据你之前说的) + dashou_profile.save() + + from dengji.services import record_dashou_biaoxian + # ========== 新增:记录打手提交时间 ========== + record_dashou_biaoxian( + dingdan_id=dingdan_id, + dashou_id=yonghuid, + xingwei=2, # 提交 + tijiao_time=timezone.now() + ) + # =========================================== + + # 8. 返回成功响应 + return Response({ + 'code': 0, + 'msg': '订单提交成功', + 'data': { + 'dingdan_id': dingdan_id, + 'new_zhuangtai': 8, # 新的订单状态 + 'dashou_zhuangtai': 1 # 打手新的状态 + } + }, 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({ + 'code': 8, + 'msg': '服务器内部错误', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + + + + + +class DashouDingdanXiangqingView(APIView): + """打手订单详情获取接口""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdan_id = request.data.get('dingdan_id', '') + if not dingdan_id: + return Response({ + 'code': 1, + 'msg': '订单ID不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 获取当前用户并验证权限 + current_user = request.user + yonghuid = current_user.yonghuid + + try: + dashou_profile = current_user.dashou_profile + except UserDashou.DoesNotExist: + return Response({ + 'code': 2, + 'msg': '用户不是打手身份', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 3. 查询订单主表 + try: + dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response({ + 'code': 3, + 'msg': '订单不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 4. 验证订单是否属于当前打手 + if dingdan.jiedan_dashou_id != yonghuid: + return Response({ + 'code': 4, + 'msg': '没有权限查看此订单', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 5. 构建基础订单数据 + dingdan_data = { + 'dingdan_id': dingdan.dingdan_id, + 'zhuangtai': dingdan.zhuangtai, + 'jine': float(dingdan.dashou_fencheng) if dingdan.dashou_fencheng else 0.0, + 'jieshao': dingdan.jieshao or '', + 'tupian': dingdan.tupian or '', + 'nicheng': dingdan.nicheng or '', + 'beizhu': dingdan.beizhu or '', + 'create_time': dingdan.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan.create_time else '', + 'tuikuan_liyou':dingdan.tkly or '', + 'dashou_liuyan': dingdan.dashou_liuyan or '', + 'fuwudashou_id': dingdan.jiedan_dashou_id or '', + 'fadanpingtai': dingdan.fadan_pingtai or '' + } + + # 6. 查询打手提交的图片 + dashou_tupian_list = Dashoutupian.objects.filter( + dingdan_id=dingdan_id, + dashou=yonghuid + ).order_by('create_time') + + jiaofu_tupian_urls = [] + for tupian_record in dashou_tupian_list: + if tupian_record.tupian: + jiaofu_tupian_urls.append(tupian_record.tupian) + + dingdan_data['dashou_jiaofu'] = jiaofu_tupian_urls + + # 7. 查询商家扩展表信息 + shangjia_info = {} + try: + shangjia_kuozhan = dingdan.shangjia_kuozhan + shangjia_info = { + 'shangjia_id': shangjia_kuozhan.shangjia_id or '', + 'shangjia_nicheng': shangjia_kuozhan.sjnicheng or '', + 'shangjia_liuyan': shangjia_kuozhan.shangjia_pingjia or '', + #'tuikuan_liyou': '', + 'chufa_liyou': shangjia_kuozhan.cfliyou or '', + 'chufa_zhuangtai': shangjia_kuozhan.sqzhuangtai or 0, + #'chufa_jieguo': shangjia_kuozhan.chufa_jieguo or '', + 'bohui_liyou': shangjia_kuozhan.bhliyou or '' + } + except DingdanShangjia.DoesNotExist: + # 没有商家扩展信息,返回空值 + shangjia_info = { + 'shangjia_id': '', + 'shangjia_nicheng': '', + 'shangjia_liuyan': '', + 'tuikuan_liyou': '', + 'chufa_liyou': '', + 'chufa_zhuangtai': 0, + 'chufa_jieguo': '', + 'bohui_liyou': '' + } + + # 8. 查询平台扩展表信息(如果有的话) + pingtai_info = {} + try: + pingtai_kuozhan = dingdan.pingtai_kuozhan + pingtai_info = { + 'laoban_id': pingtai_kuozhan.laoban_id or '', + 'laoban_pingjia': pingtai_kuozhan.laoban_pingjia or '' + } + except DingdanPingtai.DoesNotExist: + pingtai_info = { + 'laoban_id': '', + 'laoban_pingjia': '' + } + + # 9. 合并所有数据 + response_data = { + **dingdan_data, + **shangjia_info, + **pingtai_info + } + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': response_data + }, 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({ + 'code': 5, + 'msg': '服务器内部错误', + 'data': None + }, 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): + """ + 后台订单详情获取接口 + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /dingdan/adddxq + 参数: {"zhanghao": "管理员手机号", "dingdan_id": "订单ID"} + """ + + 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() + + if not qianzhanghao or not dingdan_id: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2. JWT Token身份验证 (IsAuthenticated已处理) + 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. 查询订单主表 + try: + from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia, Dashoutupian + + # 查询订单主表 - 使用select_related预加载关联表 + dingdan_obj = Dingdan.objects.select_related( + 'pingtai_kuozhan', 'shangjia_kuozhan' + ).get(dingdan_id=dingdan_id) + + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 7. 构建返回数据 - 主表字段 + response_data = { + # 基础信息字段 + 'dingdan_id': dingdan_obj.dingdan_id or '', + 'jieshao': dingdan_obj.jieshao or '', + 'zhuangtai': dingdan_obj.zhuangtai or 0, + 'leixing': dingdan_obj.leixing_id or 0, # 前端需要 leixing,对应 leixing_id + 'fadanpingtai': dingdan_obj.fadan_pingtai or 0, # 前端需要 fadanpingtai,对应 fadan_pingtai + 'jiage': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00, # 前端需要 jiage,对应 jine + 'tupian_url': dingdan_obj.tupian or '', # 前端需要 tupian_url,对应 tupian + 'beizhu': dingdan_obj.beizhu or '', + + # 服务信息字段 + 'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '', + 'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00, + 'zhiding_id': dingdan_obj.zhiding_id or '', + 'dashou_liuyan': dingdan_obj.dashou_liuyan 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 '', + } + + # 8. 根据发单平台查询扩展表数据 + fadan_pingtai = dingdan_obj.fadan_pingtai + + if fadan_pingtai == 1: + # 平台发单 - 查询平台订单扩展表 + try: + # 使用反向关联 pingtai_kuozhan + pingtai_kuozhan = dingdan_obj.pingtai_kuozhan + + response_data.update({ + 'laoban_id': pingtai_kuozhan.laoban_id or '', + 'nicheng': dingdan_obj.nicheng or '', # 这个字段在订单主表 + 'laoban_pingjia': pingtai_kuozhan.laoban_pingjia or '', + }) + except DingdanPingtai.DoesNotExist: + # 扩展表不存在,返回空值 + response_data.update({ + 'laoban_id': '', + 'nicheng': dingdan_obj.nicheng or '', + 'laoban_pingjia': '', + }) + + elif fadan_pingtai == 2: + # 商家发单 - 查询商家订单扩展表 + try: + # 使用反向关联 shangjia_kuozhan + shangjia_kuozhan = dingdan_obj.shangjia_kuozhan + + response_data.update({ + 'shangjia_id': shangjia_kuozhan.shangjia_id or '', + 'sjnicheng': shangjia_kuozhan.sjnicheng or '', + 'shangjia_pingjia': shangjia_kuozhan.shangjia_pingjia or '', + 'cfliyou': shangjia_kuozhan.cfliyou or '', # 商家处罚理由 + 'bhliyou': shangjia_kuozhan.bhliyou or '', # 驳回理由 + }) + except DingdanShangjia.DoesNotExist: + # 扩展表不存在,返回空值 + response_data.update({ + 'shangjia_id': '', + 'sjnicheng': '', + 'shangjia_pingjia': '', + 'cfliyou': '', + 'bhliyou': '', + }) + + # 9. 查询打手提交的图片 + try: + # 查询该订单的所有打手提交图片 + dashou_tupian_list = Dashoutupian.objects.filter( + dingdan_id=dingdan_id + ).values_list('tupian', flat=True) + + # 转换为列表,过滤空值 + dashou_images = [img for img in dashou_tupian_list if img] + response_data['dashou_images'] = dashou_images + except Exception as e: + logger.warning(f"查询打手图片失败: {str(e)}") + response_data['dashou_images'] = [] + + # 🔥 新增:查询退款记录表中的商家退款理由 + try: + # 导入退款记录模型 + from dingdan.models import Tuikuanjilu + + # 查询该订单的退款记录(一个订单可能有多次退款申请,我们取最新的) + tuikuan_record = Tuikuanjilu.objects.filter( + dingdan_id=dingdan_id + ).order_by('-create_time').first() # 按创建时间倒序,取最新的 + + if tuikuan_record and tuikuan_record.liyou: + # 如果有退款记录且理由不为空,添加到返回数据 + response_data['tuikuan_liyou'] = tuikuan_record.liyou + else: + # 没有找到退款记录或理由为空 + response_data['tuikuan_liyou'] = '' + except Tuikuanjilu.DoesNotExist: + # 退款记录表不存在该订单的记录 + response_data['tuikuan_liyou'] = '' + except Exception as e: + # 查询过程中出现其他异常 + logger.warning(f"查询退款记录失败: {str(e)}") + response_data['tuikuan_liyou'] = '' + + # 10. 返回成功响应 + 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 + ) + + + + +# 获取日志器 +logger = logging.getLogger(__name__) + + +class AdGengHuanDaShou(APIView): + """ + 后台更换打手接口 + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /dingdan/adddgh + 参数: { + "zhanghao": "管理员账号", + "dingdan_id": "订单ID", + "new_dashou_id": "新打手ID" + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + """ + 更换订单的打手 + 核心逻辑: + 1. 验证管理员身份 + 2. 验证订单存在且状态为进行中(2)或结算中(8) + 3. 验证新打手ID存在且是打手身份 + 4. 验证新打手状态正常(zhuangtai=1且zhanghaozhuangtai=1) + 5. 根据订单的抢单要求类型进行验证: + - 会员抢单(1):验证打手有对应会员且未过期 + - 押金抢单(2):验证打手押金≥订单押金 + 6. 更新新打手状态和统计数据 + 7. 更新原打手状态和统计数据 + 8. 更新订单信息 + """ + try: + # 1. 获取前端参数 + qianzhanghao = request.data.get('zhanghao', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + new_dashou_id = request.data.get('new_dashou_id', '').strip() + + if not qianzhanghao or not dingdan_id or not new_dashou_id: + 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. 查询订单主表 + try: + from dingdan.models import Dingdan + dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 7. 验证订单状态是否为进行中(2)或结算中(8) + if dingdan_obj.zhuangtai not in [2, 8]: + return Response( + {'code': 400, 'message': '订单状态不允许更换打手', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 8. 验证新打手ID是否存在,并且是打手身份 + try: + new_dashou_user = UserMain.objects.get( + yonghuid=new_dashou_id, + #user_type='dashou' + ) + except UserMain.DoesNotExist: + return Response( + {'code': 404, 'message': '打手不存在或不是打手身份', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 9. 获取新打手扩展表 + try: + new_dashou_profile = new_dashou_user.dashou_profile + except AttributeError: + return Response( + {'code': 404, 'message': '打手信息不完整', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 10. 验证新打手状态:zhuangtai=1且zhanghaozhuangtai=1 + if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1: + return Response( + {'code': 400, 'message': '打手状态不允许接单', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 11. 根据订单的抢单要求类型进行验证 + yaoqiuleixing = dingdan_obj.yaoqiuleixing + + if yaoqiuleixing == 1: # 会员抢单 + # 获取订单的会员ID + huiyuan_id = dingdan_obj.huiyuan_id + if not huiyuan_id: + return Response( + {'code': 400, 'message': '订单会员ID为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 查询会员购买记录 + try: + from shangpin.models import Huiyuangoumai + huiyuan_goumai = Huiyuangoumai.objects.get( + huiyuan_id=huiyuan_id, + yonghu_id=new_dashou_id # 注意:yonghu_id存储的是用户ID(yonghuid) + ) + except Huiyuangoumai.DoesNotExist: + return Response( + {'code': 400, 'message': '打手未购买该订单要求的会员', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 检查会员是否过期 + # ✅ 可以调用!因为jiance_shifou_daoqi()是Huiyuangoumai模型的实例方法 + is_expired = huiyuan_goumai.jiance_shifou_daoqi() + if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1: + return Response( + {'code': 400, 'message': '打手的会员已过期', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + elif yaoqiuleixing == 2: # 押金抢单 + # 获取订单的押金(yongjin字段) + order_yajin = dingdan_obj.yongjin + if order_yajin is None: + return Response( + {'code': 400, 'message': '订单押金为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证打手押金是否大于等于订单押金 + if new_dashou_profile.yajin < order_yajin: + return Response( + {'code': 400, 'message': '打手押金不足', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + else: + # 未知的抢单要求类型 + return Response( + {'code': 400, 'message': '订单抢单要求类型错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 12. 所有验证通过,开始更换打手(使用事务保证一致性) + with transaction.atomic(): + # 12.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() + + # 12.2 如果原订单有接单打手,则更新原打手状态 + old_dashou_id = dingdan_obj.jiedan_dashou_id + if old_dashou_id: + try: + # 查询原打手主表 + old_dashou_user = UserMain.objects.get( + yonghuid=old_dashou_id, + #user_type='dashou' + ) + + # 获取原打手扩展表 + 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: + # 原打手不存在,继续执行不影响更换 + logger.warning(f"原打手{old_dashou_id}不存在,跳过更新") + except AttributeError: + # 原打手扩展表不存在,继续执行不影响更换 + logger.warning(f"原打手{old_dashou_id}扩展信息不存在,跳过更新") + except Exception as e: + # 其他异常,记录日志但继续执行 + logger.error(f"更新原打手状态异常: {str(e)}", exc_info=True) + + # 12.3 更新订单表 + dingdan_obj.jiedan_dashou_id = new_dashou_id + dingdan_obj.zhuangtai = 2 # 进行中状态 + dingdan_obj.save() + + # 13. 返回成功响应 + return Response({ + 'code': 0, + 'message': '更换打手成功', + 'data': None + }) + + 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 logging + +# 获取日志器 +logger = logging.getLogger(__name__) + + +class AdQiangZhiJieDan(APIView): + """ + 后台强制结单接口 + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /dingdan/adddqzjd + 参数: { + "zhanghao": "管理员账号", + "dingdan_id": "订单ID" + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + """ + 强制结单接口 + 核心逻辑: + 1. 验证管理员身份 + 2. 验证订单状态为2(进行中)、8(结算中)或4(退款审核中)才能强制结单 + 3. 根据发单平台(1=平台发单, 2=商家发单)分别处理 + 4. 更新订单状态为3(已完成) + 5. 给打手结算分成金额 + 6. 如果是商家发单,还需要更新商家成交订单数量 + """ + try: + # 1. 获取前端参数 + qianzhanghao = request.data.get('zhanghao', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not qianzhanghao or not dingdan_id: + 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. 查询订单主表 + try: + from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia + dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 7. 验证订单状态是否为2、8或4 + if dingdan_obj.zhuangtai not in [2, 8, 4]: + return Response( + {'code': 400, 'message': '订单状态不允许强制结单', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 8. 获取订单的重要字段 + jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID + dashou_fencheng = dingdan_obj.dashou_fencheng # 打手分成金额 + fadan_pingtai = dingdan_obj.fadan_pingtai # 发单平台 + + # 9. 验证打手分成金额是否存在 + if dashou_fencheng is None: + return Response( + {'code': 400, 'message': '订单缺少打手分成金额', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 10. 使用事务保证数据一致性 + with transaction.atomic(): + # 10.1 更新订单状态为3(已完成) + dingdan_obj.zhuangtai = 3 + dingdan_obj.save() + + # 10.2 处理打手结算 + if jiedan_dashou_id: + try: + # 查询打手用户主表 + dashou_user = UserMain.objects.get( + yonghuid=jiedan_dashou_id, + #user_type='dashou' + ) + + # 获取打手扩展表 + dashou_profile = dashou_user.dashou_profile + + # 更新打手扩展表数据 + # ✅ 确认字段:chengjiaozongliang 成交单总量 + dashou_profile.chengjiaozongliang += 1 + + # ✅ 确认字段:yue 可提现余额 + dashou_profile.yue += dashou_fencheng + + # ✅ 确认字段:zonge 打单赚取总额 + dashou_profile.zonge += dashou_fencheng + + # ✅ 确认字段:jinrishouyi 今日收益金额 + dashou_profile.jinrishouyi += dashou_fencheng + + # ✅ 确认字段:jinyueshouyi 今月收益金额 + dashou_profile.jinyueshouyi += dashou_fencheng + + dashou_profile.save() + logger.info(f"强制结单成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}") + + except UserMain.DoesNotExist: + # 打手用户不存在,记录日志但不影响强制结单 + logger.warning(f"强制结单:打手{jiedan_dashou_id}不存在,跳过打手结算") + except AttributeError: + # 打手扩展表不存在,记录日志但不影响强制结单 + logger.warning(f"强制结单:打手{jiedan_dashou_id}扩展信息不存在,跳过打手结算") + + # 10.3 如果是商家发单,还需要更新商家扩展表 + if fadan_pingtai == 2: + try: + # 获取商家订单扩展表 + shangjia_kuozhan = dingdan_obj.shangjia_kuozhan + shangjia_id = shangjia_kuozhan.shangjia_id + + if shangjia_id: + # 查询商家用户主表 + shangjia_user = UserMain.objects.get( + yonghuid=shangjia_id, + #user_type='shop' + ) + + # 获取商家扩展表 + shangjia_profile = shangjia_user.shop_profile + + # ✅ 确认字段:chengjiao 商家成交订单总量 + shangjia_profile.chengjiao += 1 + shangjia_profile.save() + + logger.info(f"强制结单成功,商家{shangjia_id}成交订单+1") + + except DingdanShangjia.DoesNotExist: + # 商家订单扩展表不存在,跳过商家更新 + logger.warning(f"强制结单:订单{dingdan_id}商家扩展表不存在,跳过商家更新") + except UserMain.DoesNotExist: + # 商家用户不存在,跳过商家更新 + logger.warning(f"强制结单:商家{shangjia_id}不存在,跳过商家更新") + except AttributeError: + # 商家扩展表不存在,跳过商家更新 + logger.warning(f"强制结单:商家{shangjia_id}扩展信息不存在,跳过商家更新") + + # 12. 更新平台收益流水(仅在平台发单时) + if dingdan_obj.fadan_pingtai == 1: + try: + from dingdan.utils import update_platform_profit # 导入函数 + update_platform_profit(dingdan_id) + except Exception as e: + # 平台收益更新失败不影响主流程,但记录日志 + logger.error(f"强制结单后更新平台收益失败: {str(e)}") + + # 11. 返回成功响应 + return Response({ + 'code': 0, + 'message': '强制结单成功', + 'data': None + }) + + 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 AdJuJueTuiKuan(APIView): + """ + 后台拒绝退款接口 + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /dingdan/adddjj + 参数: { + "zhanghao": "管理员账号", + "dingdan_id": "订单ID", + "jujue_liyou": "拒绝退款理由(可选)" + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + """ + 拒绝退款接口 + 核心逻辑: + 1. 验证管理员身份 + 2. 拒绝退款后订单状态改为6(退款失败) + 3. 给打手结算分成金额 + 4. 如果是商家发单,更新商家成交订单数量 + 5. 更新退款记录表,状态改为2(驳回) + """ + try: + # 1. 获取前端参数 + qianzhanghao = request.data.get('zhanghao', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + jujue_liyou = request.data.get('jujue_liyou', '').strip() + + if not qianzhanghao or not dingdan_id: + 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. 查询订单主表 + try: + from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu + dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 7. 获取订单的重要字段 + jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID + dashou_fencheng = dingdan_obj.dashou_fencheng # 打手分成金额 + fadan_pingtai = dingdan_obj.fadan_pingtai # 发单平台 + + # 8. 验证打手分成金额是否存在 + if dashou_fencheng is None: + return Response( + {'code': 400, 'message': '订单缺少打手分成金额', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 9. 使用事务保证数据一致性 + with transaction.atomic(): + # 9.1 更新订单状态为6(退款失败) + dingdan_obj.zhuangtai = 6 + dingdan_obj.save() + + # 9.2 处理打手结算 + if jiedan_dashou_id: + try: + # 查询打手用户主表 + dashou_user = UserMain.objects.get( + yonghuid=jiedan_dashou_id, + #user_type='dashou' + ) + + # 获取打手扩展表 + dashou_profile = dashou_user.dashou_profile + + # 更新打手扩展表数据 + # ✅ 确认字段:chengjiaozongliang 成交单总量 + dashou_profile.chengjiaozongliang += 1 + + # ✅ 确认字段:yue 可提现余额 + dashou_profile.yue += dashou_fencheng + + # ✅ 确认字段:zonge 打单赚取总额 + dashou_profile.zonge += dashou_fencheng + + # ✅ 确认字段:jinrishouyi 今日收益金额 + dashou_profile.jinrishouyi += dashou_fencheng + + # ✅ 确认字段:jinyueshouyi 今月收益金额 + dashou_profile.jinyueshouyi += dashou_fencheng + + dashou_profile.save() + logger.info(f"拒绝退款成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}") + + except UserMain.DoesNotExist: + # 打手用户不存在,记录日志但不影响操作 + logger.warning(f"拒绝退款:打手{jiedan_dashou_id}不存在,跳过打手结算") + except AttributeError: + # 打手扩展表不存在,记录日志但不影响操作 + logger.warning(f"拒绝退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手结算") + + # 9.3 如果是商家发单,还需要更新商家扩展表 + if fadan_pingtai == 2: + try: + # 获取商家订单扩展表 + shangjia_kuozhan = dingdan_obj.shangjia_kuozhan + shangjia_id = shangjia_kuozhan.shangjia_id + + if shangjia_id: + # 查询商家用户主表 + shangjia_user = UserMain.objects.get( + yonghuid=shangjia_id, + #user_type='shop' + ) + + # 获取商家扩展表 + shangjia_profile = shangjia_user.shop_profile + + # ✅ 确认字段:chengjiao 商家成交订单总量 + shangjia_profile.chengjiao += 1 + shangjia_profile.save() + + logger.info(f"拒绝退款成功,商家{shangjia_id}成交订单+1") + + except DingdanShangjia.DoesNotExist: + # 商家订单扩展表不存在,跳过商家更新 + logger.warning(f"拒绝退款:订单{dingdan_id}商家扩展表不存在,跳过商家更新") + except UserMain.DoesNotExist: + # 商家用户不存在,跳过商家更新 + logger.warning(f"拒绝退款:商家{shangjia_id}不存在,跳过商家更新") + except AttributeError: + # 商家扩展表不存在,跳过商家更新 + logger.warning(f"拒绝退款:商家{shangjia_id}扩展信息不存在,跳过商家更新") + + # 9.4 更新退款记录表 + try: + # 查询退款记录表 + tuikuan_jilu = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) + + # ✅ 确认字段:sqzhuangtai 申请状态,改为2(驳回) + tuikuan_jilu.sqzhuangtai = 2 + + # ✅ 确认字段:bhliyou 驳回理由 + tuikuan_jilu.bhliyou = jujue_liyou + + # ✅ 确认字段:chuliid 处理人ID(使用管理员账号,即手机号) + tuikuan_jilu.chuliid = qianzhanghao + + tuikuan_jilu.save() + logger.info(f"拒绝退款成功,更新退款记录表,订单:{dingdan_id}") + + except Tuikuanjilu.DoesNotExist: + # 退款记录表不存在,记录日志但不影响主流程 + logger.warning(f"拒绝退款:订单{dingdan_id}退款记录不存在,跳过更新") + + # 10. 返回成功响应 + return Response({ + 'code': 0, + 'message': '拒绝退款成功', + 'data': None + }) + + 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 AdTongYiTuiKuanShangJia(APIView): + """ + 后台同意退款接口(商家发单) + 权限: JWT Token认证 + 管理员身份验证 + 密码验证 + 前端调用: POST /dingdan/adddsjtk + 参数: { + "zhanghao": "管理员账号", + "dingdan_id": "订单ID", + "tuikuan_liyou": "退款理由(可选)", + "pay_password": "支付密码(openid)" + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + """ + 同意退款接口(商家发单) + 核心逻辑: + 1. 验证管理员身份 + 2. 验证支付密码(openid) + 3. 检查订单状态为4(退款审核中) + 4. 更新订单状态为5(已退款) + 5. 更新打手退款订单数量 + 6. 更新商家退款订单数量和余额(加上订单金额) + 7. 更新退款记录表,状态改为1(成功) + """ + try: + # 1. 获取前端参数 + qianzhanghao = request.data.get('zhanghao', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() + pay_password = request.data.get('pay_password', '').strip() + + if not qianzhanghao or not dingdan_id or not pay_password: + 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. 验证支付密码(对比openid) + if pay_password != dangqianyonghu.openid: + return Response( + {'code': 401, 'message': '支付密码错误', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 7. 查询订单主表 + try: + from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu + dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 8. 验证订单状态为4(退款审核中) + if dingdan_obj.zhuangtai != 4: + return Response( + {'code': 400, 'message': '订单状态不是退款审核中', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 9. 验证订单为商家发单 + if dingdan_obj.fadan_pingtai != 2: + return Response( + {'code': 400, 'message': '此接口只处理商家发单的退款', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 10. 获取订单的重要字段 + jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID + jine = dingdan_obj.jine # 订单金额(不是打手分成) + + # 11. 验证订单金额是否存在 + if jine is None: + return Response( + {'code': 400, 'message': '订单缺少订单金额', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 12. 获取商家ID + try: + shangjia_kuozhan = dingdan_obj.shangjia_kuozhan + shangjia_id = shangjia_kuozhan.shangjia_id + except DingdanShangjia.DoesNotExist: + return Response( + {'code': 400, 'message': '订单商家信息不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 13. 使用事务保证数据一致性 + with transaction.atomic(): + # 13.1 更新订单状态为5(已退款) + dingdan_obj.zhuangtai = 5 + dingdan_obj.save() + + # 13.2 更新打手扩展表(退款订单总量+1) + if jiedan_dashou_id: + try: + # 查询打手用户主表 + dashou_user = UserMain.objects.get( + yonghuid=jiedan_dashou_id, + #user_type='dashou' + ) + + # 获取打手扩展表 + dashou_profile = dashou_user.dashou_profile + + # ✅ 确认字段:tuikuanliang 退款订单总量 + dashou_profile.tuikuanliang += 1 + + dashou_profile.save() + logger.info(f"同意退款成功,打手{jiedan_dashou_id}退款订单+1") + + except UserMain.DoesNotExist: + # 打手用户不存在,记录日志但不影响操作 + logger.warning(f"同意退款:打手{jiedan_dashou_id}不存在,跳过打手更新") + except AttributeError: + # 打手扩展表不存在,记录日志但不影响操作 + logger.warning(f"同意退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手更新") + + # 13.3 更新商家扩展表 + if shangjia_id: + try: + # 查询商家用户主表 + shangjia_user = UserMain.objects.get( + yonghuid=shangjia_id, + #user_type='shop' + ) + + # 获取商家扩展表 + shangjia_profile = shangjia_user.shop_profile + + # ✅ 确认字段:tuikuan 退款订单总量 + shangjia_profile.tuikuan += 1 + + # ✅ 确认字段:yue 商家余额(加上订单金额) + shangjia_profile.yue += jine + + shangjia_profile.save() + logger.info(f"同意退款成功,商家{shangjia_id}退款订单+1,余额增加:{jine}") + + except UserMain.DoesNotExist: + # 商家用户不存在,记录日志但不影响操作 + logger.warning(f"同意退款:商家{shangjia_id}不存在,跳过商家更新") + except AttributeError: + # 商家扩展表不存在,记录日志但不影响操作 + logger.warning(f"同意退款:商家{shangjia_id}扩展信息不存在,跳过商家更新") + + # 13.4 更新退款记录表 + try: + # 查询退款记录表 + tuikuan_jilu = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) + + # ✅ 确认字段:sqzhuangtai 申请状态,改为1(成功) + tuikuan_jilu.sqzhuangtai = 1 + + # 可以添加同意理由字段,如:tongyi_liyou + # tuikuan_jilu.tongyi_liyou = tuikuan_liyou + + # ✅ 确认字段:chuliid 处理人ID(使用管理员账号,即手机号) + tuikuan_jilu.chuliid = qianzhanghao + + tuikuan_jilu.save() + logger.info(f"同意退款成功,更新退款记录表,订单:{dingdan_id}") + + except Tuikuanjilu.DoesNotExist: + # 退款记录表不存在,记录日志但不影响主流程 + logger.warning(f"同意退款:订单{dingdan_id}退款记录不存在,跳过更新") + + # 14. 返回成功响应 + return Response({ + 'code': 0, + 'message': '同意退款成功', + 'data': None + }) + + 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 + ) + + + + + + + + + +# 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): + """ + 后台同意退款接口(平台发单) + 权限: JWT Token认证 + 管理员身份验证 + 密码验证 + 前端调用: POST /dingdan/adddpttk + 参数: { + "zhanghao": "管理员账号", + "dingdan_id": "订单ID", + "tuikuan_liyou": "退款理由(可选)", + "pay_password": "支付密码(openid)" + } + """ + 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() + tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() + pay_password = request.data.get('pay_password', '').strip() + + if not qianzhanghao or not dingdan_id or not pay_password: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2. JWT Token身份验证 + dangqianyonghu = request.user + + # 3. 验证手机号是否匹配 + if str(getattr(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. 验证支付密码(对比openid) + if pay_password != dangqianyonghu.openid: + return Response( + {'code': 401, 'message': '支付密码错误', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 7. 查询订单主表(加锁) + with transaction.atomic(): + try: + dingdan_obj = Dingdan.objects.select_related('pingtai_kuozhan').select_for_update().get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 8. 验证订单状态为1、7、2、8、4(允许退款的状态) + if dingdan_obj.zhuangtai not in [1, 7, 2, 8, 4]: + return Response( + {'code': 400, 'message': '订单状态不允许退款', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 9. 验证订单为平台发单 + if dingdan_obj.fadan_pingtai != 1: + return Response( + {'code': 400, 'message': '此接口只处理平台发单的退款', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 10. 获取订单的重要字段 + jiedan_dashou_id = dingdan_obj.jiedan_dashou_id + jine = dingdan_obj.jine + if jine is None: + return Response( + {'code': 400, 'message': '订单缺少订单金额', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 11. 获取老板ID(从平台订单扩展表) + if not hasattr(dingdan_obj, 'pingtai_kuozhan') or not dingdan_obj.pingtai_kuozhan: + return Response( + {'code': 400, 'message': '订单老板信息不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + laoban_id = dingdan_obj.pingtai_kuozhan.laoban_id + + # 12. 调用微信退款接口(优先使用 wechat_transaction_id) + transaction_id = getattr(dingdan_obj, 'wechat_transaction_id', None) + weixin_refund_result = self.call_weixin_refund( + out_trade_no=dingdan_id, + jine=jine, + transaction_id=transaction_id + ) + + if not weixin_refund_result.get('success'): + logger.error(f"微信退款失败,订单:{dingdan_id},错误:{weixin_refund_result.get('msg')}") + return Response( + {'code': 400, 'message': weixin_refund_result.get('msg', '退款失败')}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 13. 更新订单状态为5(已退款) + dingdan_obj.zhuangtai = 5 + dingdan_obj.save() + + # 14. 更新打手扩展表 + if jiedan_dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.zhuangtai = 1 # 打手状态设为空闲 + dashou_profile.tuikuanliang += 1 + dashou_profile.save() + except (UserMain.DoesNotExist, AttributeError): + logger.warning(f"平台退款:打手{jiedan_dashou_id}不存在或扩展信息缺失,跳过") + + # 15. 更新老板扩展表 + if laoban_id: + try: + laoban_user = UserMain.objects.get(yonghuid=laoban_id) + laoban_profile = laoban_user.boss_profile + laoban_profile.alltui += 1 + laoban_profile.zonge -= jine + if laoban_profile.zonge < 0: + laoban_profile.zonge = 0 + laoban_profile.save() + except (UserMain.DoesNotExist, AttributeError): + logger.warning(f"平台退款:老板{laoban_id}不存在或扩展信息缺失,跳过") + + # 16. 更新收支记录表(id=1) + try: + + #更新支出记录 + from dingdan.utils import update_daily_payout + update_daily_payout(jine) + + + szjilu_obj = Szjilu.objects.get(id=1) + szjilu_obj.zongsy -= jine + szjilu_obj.zongzc += jine + szjilu_obj.jrzc += jine + szjilu_obj.save() + except Szjilu.DoesNotExist: + logger.error(f"平台退款:收支记录表(id=1)不存在,跳过") + except Exception as e: + logger.error(f"更新收支记录表异常: {str(e)}") + + # 17. 更新退款记录表 + try: + tuikuan_jilu, created = Tuikuanjilu.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'dashouid': jiedan_dashou_id if jiedan_dashou_id else '', + 'qingqiuid': laoban_id if laoban_id else '', + 'chuliid': qianzhanghao, + 'liyou': tuikuan_liyou if tuikuan_liyou else '给老板退款', + 'sqzhuangtai': 1, + 'jine': jine, + 'dashou_fencheng': dingdan_obj.dashou_fencheng or 0, + 'jieshao': dingdan_obj.jieshao or '', + 'beizhu': '给老板退款', + 'nicheng': dingdan_obj.nicheng or '', + } + ) + if not created: + tuikuan_jilu.sqzhuangtai = 1 + if tuikuan_liyou: + tuikuan_jilu.liyou = tuikuan_liyou + tuikuan_jilu.chuliid = qianzhanghao + tuikuan_jilu.save() + except Exception as e: + logger.error(f"更新退款记录表异常: {str(e)}") + + logger.info(f"平台退款成功,订单:{dingdan_id},退款单号:{weixin_refund_result.get('refund_id')}") + + return Response({'code': 0, 'message': '退款处理成功', 'data': None}) + + 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 call_weixin_refund(self, out_trade_no, jine, transaction_id=None): + """ + 调用微信退款接口 + :param out_trade_no: 商户订单号 + :param jine: 退款金额 + :param transaction_id: 微信支付订单号(优先使用) + :return: {'success': True/False, 'msg': '消息', 'refund_id': '退款单号'} + """ + try: + # 检查配置 + required_settings = ['WEIXIN_APPID', 'WEIXIN_MCHID', 'WEIXIN_SHANGHUMIYAO', 'WEIXIN_CERT_PATH', 'WEIXIN_KEY_PATH'] + for setting in required_settings: + if not hasattr(settings, setting) or not getattr(settings, setting): + return {'success': False, 'msg': f'缺少微信支付配置: {setting}'} + + # 生成退款单号 + refund_id = f"TK{int(time.time())}{random.randint(1000, 9999)}" + total_fee_in_fen = int(float(jine) * 100) + + # 构造请求参数 + refund_data = { + 'appid': settings.WEIXIN_APPID, + 'mch_id': settings.WEIXIN_MCHID, + 'nonce_str': hashlib.md5(str(time.time()).encode()).hexdigest(), + 'out_refund_no': refund_id, + 'total_fee': total_fee_in_fen, + 'refund_fee': total_fee_in_fen, + 'op_user_id': settings.WEIXIN_MCHID, + 'refund_account': 'REFUND_SOURCE_UNSETTLED_FUNDS' + } + # 优先使用 transaction_id,否则用 out_trade_no + if transaction_id: + refund_data['transaction_id'] = transaction_id + else: + refund_data['out_trade_no'] = out_trade_no + + # 生成签名 + sorted_params = sorted(refund_data.items()) + sign_string = '&'.join([f"{k}={v}" for k, v in sorted_params if v]) + sign_string += f"&key={settings.WEIXIN_SHANGHUMIYAO}" + sign = hashlib.md5(sign_string.encode()).hexdigest().upper() + refund_data['sign'] = sign + + xml_data = self.dict_to_xml(refund_data) + url = "https://api.mch.weixin.qq.com/secapi/pay/refund" + response = requests.post( + url, + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + cert=(settings.WEIXIN_CERT_PATH, settings.WEIXIN_KEY_PATH), + timeout=30 + ) + + if response.status_code == 200: + result = self.xml_to_dict(response.content) + if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': + logger.info(f"微信退款成功,订单:{out_trade_no},退款单号:{refund_id}") + return {'success': True, 'msg': '退款申请提交成功', 'refund_id': refund_id} + else: + error_msg = result.get('err_code_des', result.get('return_msg', '退款申请失败')) + logger.error(f"微信退款失败,订单:{out_trade_no},错误:{error_msg}") + return {'success': False, 'msg': f'微信退款失败: {error_msg}'} + else: + logger.error(f"微信退款接口调用失败,状态码:{response.status_code}") + return {'success': False, 'msg': '微信退款接口调用失败'} + + except requests.exceptions.Timeout: + logger.error(f"微信退款接口请求超时,订单:{out_trade_no}") + return {'success': False, 'msg': '微信退款接口请求超时'} + except Exception as e: + logger.error(f"调用微信退款接口异常: {str(e)}") + return {'success': False, 'msg': f'退款处理异常: {str(e)}'} + + def dict_to_xml(self, dict_data): + xml = [""] + for k, v in dict_data.items(): + xml.append(f"<{k}>{v}") + xml.append("") + return "".join(xml) + + def xml_to_dict(self, xml_data): + try: + root = ET.fromstring(xml_data) + return {child.tag: child.text for child in root} + except ET.ParseError as e: + logger.error(f"XML解析失败: {str(e)}") + return {'return_code': 'FAIL', 'return_msg': 'XML解析失败'} + + + + +'''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 +from django.conf import settings +import logging +import time +import random +import hashlib +import requests +import xml.etree.ElementTree as ET + +# 获取日志器 +logger = logging.getLogger(__name__) + + + +class AdTongYiTuiKuanPingTai(APIView): + """ + 后台同意退款接口(平台发单) + 权限: JWT Token认证 + 管理员身份验证 + 密码验证 + 前端调用: POST /dingdan/adddpttk + 参数: { + "zhanghao": "管理员账号", + "dingdan_id": "订单ID", + "tuikuan_liyou": "退款理由(可选)", + "pay_password": "支付密码(openid)" + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + """ + 同意退款接口(平台发单) + 核心逻辑: + 1. 验证管理员身份和支付密码 + 2. 验证订单状态为2、8或4(进行中、结算中、退款审核中) + 3. 调用微信退款接口 + 4. 如果微信受理成功,更新数据库(订单、打手、老板、收支、退款记录) + 5. 如果微信受理失败,返回错误,数据库不变 + """ + try: + # 1. 获取前端参数 + qianzhanghao = request.data.get('zhanghao', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() + pay_password = request.data.get('pay_password', '').strip() + + if not qianzhanghao or not dingdan_id or not pay_password: + 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. 验证支付密码(对比openid) + if pay_password != dangqianyonghu.openid: + return Response( + {'code': 401, 'message': '支付密码错误', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 7. 查询订单主表 + try: + from dingdan.models import Dingdan, DingdanPingtai, Tuikuanjilu + from peizhi.models import Szjilu + + # 使用select_related预加载平台订单扩展表 + dingdan_obj = Dingdan.objects.select_related('pingtai_kuozhan').get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 8. 验证订单状态为2、8或4 + if dingdan_obj.zhuangtai not in [1,7,2, 8, 4]: + return Response( + {'code': 400, 'message': '订单状态不允许退款', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 9. 验证订单为平台发单 + if dingdan_obj.fadan_pingtai != 1: + return Response( + {'code': 400, 'message': '此接口只处理平台发单的退款', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 10. 获取订单的重要字段 + jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID + jine = dingdan_obj.jine # 订单金额(全额退款) + + # 11. 验证订单金额是否存在 + if jine is None: + return Response( + {'code': 400, 'message': '订单缺少订单金额', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 12. 获取老板ID(从平台订单扩展表) + try: + pingtai_kuozhan = dingdan_obj.pingtai_kuozhan + laoban_id = pingtai_kuozhan.laoban_id + except DingdanPingtai.DoesNotExist: + return Response( + {'code': 400, 'message': '订单老板信息不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # ========== 修改点开始 ========== + # 13. 先调用微信退款接口,成功后才更新数据库 + weixin_refund_result = self.call_weixin_refund(dingdan_id, jine) + + if not weixin_refund_result.get('success'): + # 微信退款失败,直接返回错误,数据库不更新 + logger.error(f"微信退款失败,订单:{dingdan_id},错误:{weixin_refund_result.get('msg')}") + return Response( + {'code': 400, 'message': weixin_refund_result.get('msg', '退款失败')}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 14. 微信退款受理成功,开始事务更新数据库 + with transaction.atomic(): + # 14.1 更新订单状态为5(已退款) + dingdan_obj.zhuangtai = 5 + dingdan_obj.save() + + # 14.2 更新打手扩展表(退款订单总量+1) + if jiedan_dashou_id: + try: + # 查询打手用户主表 + dashou_user = UserMain.objects.get( + yonghuid=jiedan_dashou_id, + #user_type='dashou' + ) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.save() + except UserMain.DoesNotExist: + logger.warning(f"平台退款:打手{jiedan_dashou_id}不存在,跳过打手更新") + except AttributeError: + logger.warning(f"平台退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手更新") + + # 14.3 更新老板扩展表 + if laoban_id: + try: + laoban_user = UserMain.objects.get( + yonghuid=laoban_id, + #user_type='normal' + ) + laoban_profile = laoban_user.boss_profile + laoban_profile.alltui += 1 + laoban_profile.zonge -= jine + if laoban_profile.zonge < 0: + laoban_profile.zonge = 0 + laoban_profile.save() + except UserMain.DoesNotExist: + logger.warning(f"平台退款:老板{laoban_id}不存在,跳过老板更新") + except AttributeError: + logger.warning(f"平台退款:老板{laoban_id}扩展信息不存在,跳过老板更新") + + # 14.4 更新收支记录表(id=1的记录) + try: + szjilu_obj = Szjilu.objects.get(id=1) + szjilu_obj.zongsy -= jine + szjilu_obj.zongzc += jine + szjilu_obj.jrzc += jine + szjilu_obj.save() + except Szjilu.DoesNotExist: + logger.error(f"平台退款:收支记录表(id=1)不存在,跳过更新") + except Exception as e: + logger.error(f"更新收支记录表异常: {str(e)}") + + # 14.5 更新退款记录表 + try: + # 查询退款记录表,是否已有该订单的记录 + try: + tuikuan_jilu = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) + tuikuan_jilu.sqzhuangtai = 1 + if tuikuan_liyou: + tuikuan_jilu.liyou = tuikuan_liyou + tuikuan_jilu.chuliid = qianzhanghao + tuikuan_jilu.save() + except Tuikuanjilu.DoesNotExist: + # 退款记录表不存在,创建新的记录 + Tuikuanjilu.objects.create( + dingdan_id=dingdan_id, + dashouid=jiedan_dashou_id if jiedan_dashou_id else '', + qingqiuid=laoban_id if laoban_id else '', + chuliid=qianzhanghao, + liyou=tuikuan_liyou if tuikuan_liyou else '给老板退款', + sqzhuangtai=1, + jine=jine, + dashou_fencheng=dingdan_obj.dashou_fencheng if dingdan_obj.dashou_fencheng else 0, + jieshao=dingdan_obj.jieshao if dingdan_obj.jieshao else '', + beizhu='给老板退款', + nicheng=dingdan_obj.nicheng if dingdan_obj.nicheng else '', + ) + except Exception as e: + logger.error(f"更新退款记录表异常: {str(e)}") + + logger.info(f"平台退款成功,订单:{dingdan_id},退款单号:{weixin_refund_result.get('refund_id')}") + # ========== 修改点结束 ========== + + # 15. 返回成功响应 + return Response({ + 'code': 0, + 'message': '退款处理成功', + 'data': None + }) + + 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 call_weixin_refund(self, dingdan_id, jine): + """ + 调用微信退款接口 + 参考你提供的接口,但根据现有模型和配置调整 + + 需要在settings.py中配置以下参数: + WEIXIN_APPID = '你的小程序appid' + WEIXIN_MCHID = '你的商户号' + WEIXIN_SHANGHUMIYAO = '你的商户密钥' + WEIXIN_CERT_PATH = '证书路径/cert/apiclient_cert.pem' + WEIXIN_KEY_PATH = '证书路径/cert/apiclient_key.pem' + + 返回:{'success': True/False, 'msg': '消息'} + """ + try: + # 检查必要的配置 + required_settings = ['WEIXIN_APPID', 'WEIXIN_MCHID', 'WEIXIN_SHANGHUMIYAO', 'WEIXIN_CERT_PATH', + 'WEIXIN_KEY_PATH'] + for setting in required_settings: + if not hasattr(settings, setting) or not getattr(settings, setting): + return {'success': False, 'msg': f'缺少微信支付配置: {setting}'} + + # 生成退款单号 + refund_id = f"TK{int(time.time())}{random.randint(1000, 9999)}" + + # 金额转换为分(必须是整数) + total_fee_in_fen = int(float(jine) * 100) + + # 准备微信退款参数 + refund_data = { + 'appid': settings.WEIXIN_APPID, + 'mch_id': settings.WEIXIN_MCHID, + 'nonce_str': hashlib.md5(str(time.time()).encode()).hexdigest(), + 'out_trade_no': dingdan_id, # 商户订单号(我们的订单ID) + 'out_refund_no': refund_id, # 商户退款单号 + 'total_fee': total_fee_in_fen, # 订单总金额(分) + 'refund_fee': total_fee_in_fen, # 退款金额(分),全额退款 + 'op_user_id': settings.WEIXIN_MCHID, # 操作员ID,默认为商户号 + 'refund_account': 'REFUND_SOURCE_UNSETTLED_FUNDS' # 退款资金来源 + } + + # 生成签名 + sorted_params = sorted(refund_data.items()) + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) + sign_string += f"&key={settings.WEIXIN_SHANGHUMIYAO}" + sign = hashlib.md5(sign_string.encode()).hexdigest().upper() + refund_data['sign'] = sign + + # 将字典转换为XML + xml_data = self.dict_to_xml(refund_data) + + # 微信退款API地址 + weixin_refund_url = "https://api.mch.weixin.qq.com/secapi/pay/refund" + + # 设置超时时间 + timeout = 30 + + # 调用微信退款API(需要证书) + headers = {'Content-Type': 'application/xml'} + response = requests.post( + weixin_refund_url, + data=xml_data.encode('utf-8'), + headers=headers, + cert=(settings.WEIXIN_CERT_PATH, settings.WEIXIN_KEY_PATH), + timeout=timeout + ) + + # 解析返回结果 + if response.status_code == 200: + result = self.xml_to_dict(response.content) + + if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': + logger.info(f"微信退款成功,订单:{dingdan_id},退款单号:{refund_id}") + return {'success': True, 'msg': '退款申请提交成功', 'refund_id': refund_id} + else: + error_msg = result.get('err_code_des', result.get('return_msg', '退款申请失败')) + logger.error(f"微信退款失败,订单:{dingdan_id},错误:{error_msg}") + return {'success': False, 'msg': f'微信退款失败: {error_msg}'} + else: + logger.error(f"微信退款接口调用失败,状态码:{response.status_code}") + return {'success': False, 'msg': '微信退款接口调用失败'} + + except requests.exceptions.Timeout: + logger.error(f"微信退款接口请求超时,订单:{dingdan_id}") + return {'success': False, 'msg': '微信退款接口请求超时'} + except Exception as e: + logger.error(f"调用微信退款接口异常: {str(e)}") + return {'success': False, 'msg': f'退款处理异常: {str(e)}'} + + + + def dict_to_xml(self, dict_data): + """将字典转换为XML字符串""" + xml = [""] + for k, v in dict_data.items(): + xml.append(f"<{k}>{v}") + xml.append("") + return "".join(xml) + + def xml_to_dict(self, xml_data): + """将XML转换为字典""" + try: + root = ET.fromstring(xml_data) + result = {} + for child in root: + result[child.tag] = child.text + return result + except ET.ParseError as e: + logger.error(f"XML解析失败: {str(e)}") + return {'return_code': 'FAIL', 'return_msg': 'XML解析失败'}''' + + + + + + + + + + + + + + + + +logger = logging.getLogger(__name__) + + +class DashouHuoquLeixingView(APIView): + """ + 打手获取商品类型接口 - 抢单页面使用 + 接口地址:POST /dingdan/dsqdhqddlx + 功能:返回所有正常的商品类型,已按排序权重排序 + + 返回格式(与前端原数据格式完全一致): + { + "code": 200, + "msg": "成功", + "data": { + "list": [ + { + "id": 1, + "jieshao": "王者荣耀", + "tupian_url": "leixing/wzry.jpg", + "yaoqiuleixing": null, + "huiyuan_id": null, + "yongjin": null, + "paixu": 100, + "shenhezhuangtai": 1 + }, + // ... 更多类型 + ] + } + } + """ + + # 🔴 使用JWT Token认证 + permission_classes = [IsAuthenticated] + + def post(self, request): + """ + 处理POST请求,返回商品类型列表 + + 请求参数:无特殊参数,只验证用户身份 + + 安全验证: + 1. JWT Token验证用户身份 + 2. 只返回 shenhezhuangtai=1(正常状态)的类型 + 3. 数据已排序:paixu降序 → id升序 + """ + try: + logger.info(f"打手获取商品类型请求 - 用户ID: {request.user.yonghuid}") + + # 🔴 导入商品类型模型 + from shangpin.models import ShangpinLeixing + + # 🔴 查询所有正常状态的商品类型 + # 条件:shenhezhuangtai=1(正常状态) + # 排序:先按 paixu 降序(数值大的在前),再按 id 升序 + leixing_queryset = ShangpinLeixing.objects.filter( + shenhezhuangtai=1 # 🔴 只取正常状态,排除审核专用 + ).order_by('-paixu', 'id') + + # 🔴 构建返回数据列表 + leixing_list = [] + for leixing in leixing_queryset: + leixing_list.append({ + "id": leixing.id, + "jieshao": leixing.jieshao or "", + "tupian_url": leixing.tupian_url or "", + "yaoqiuleixing": leixing.yaoqiuleixing, + "huiyuan_id": leixing.huiyuan_id, + "yongjin": float(leixing.yongjin) if leixing.yongjin else None, + "paixu": leixing.paixu or 0, + "shenhezhuangtai": leixing.shenhezhuangtai or 1 + }) + + # 🔴 记录日志 + logger.info(f"返回商品类型数据:{len(leixing_list)}条,用户: {request.user.yonghuid}") + + # 🔴 返回成功响应 + # 注意:保持与前端原数据格式完全一致 + return Response({ + "code": 200, + "msg": "成功", + "data": { + "list": leixing_list # 🔴 保持原字段名 "list" + } + }, 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": f"服务器内部错误: {str(e)}", + "data": None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + + + + + + + + +logger = logging.getLogger(__name__) + +from peizhi.models import Club ,OrderTypeMapping + +logger = logging.getLogger(__name__) + + + + +class PartnerSyncOrderView(APIView): + """ + 接收对方平台派单通知接口 + 对方平台商家派单成功后,异步调用此接口,将订单信息同步过来。 + """ + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 验证 token + token = request.data.get('token') + if token != settings.CROSS_PLATFORM_TOKEN: + logger.warning(f"跨平台派单 token 无效: {token}") + return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) + + # 2. 获取必要参数 + our_club_id = request.data.get('our_club_id') # 对方平台发送的我方俱乐部ID(即我方在对方表中的 partner_club_id) + partner_club_id = request.data.get('partner_club_id') # 对方平台的俱乐部ID(对方自己的ID) + partner_order_id = request.data.get('partner_order_id') + jine = request.data.get('jine') + dashou_fencheng = request.data.get('dashou_fencheng') + jieshao = request.data.get('jieshao') + beizhu = request.data.get('beizhu', '') + create_time_str = request.data.get('create_time') + nicheng = request.data.get('nicheng') + leixing_id = request.data.get('leixing_id') # 对方平台的商品类型ID + partner_yonghu_id = request.data.get('user_id') # 对方下单用户ID + + # 必需字段校验 + required_fields = [our_club_id, partner_club_id, partner_order_id, jine, dashou_fencheng, + jieshao, create_time_str, nicheng, leixing_id] + if not all(required_fields): + logger.error(f"跨平台派单参数缺失: {request.data}") + return Response({'code': 400, 'message': 'Missing required parameters'}, status=status.HTTP_400_BAD_REQUEST) + + # 3. 验证我方俱乐部是否存在且同意接收订单 + try: + my_club = Club.objects.get(partner_club_id=our_club_id) + except Club.DoesNotExist: + logger.warning(f"我方俱乐部不存在,partner_club_id={our_club_id}") + return Response({'code': 404, 'message': 'Club not found'}, status=status.HTTP_404_NOT_FOUND) + + if not my_club.is_receive_enabled: + logger.warning(f"我方俱乐部 {my_club.club_id} 未同意接收订单") + return Response({'code': 403, 'message': 'Receiving not allowed'}, status=status.HTTP_403_FORBIDDEN) + + # 4. 查找订单类型映射 + try: + mapping = OrderTypeMapping.objects.get( + partner_club_id=our_club_id, # 对方在我方的标识 + partner_type_id=leixing_id + ) + our_type_id = mapping.our_type_id + except OrderTypeMapping.DoesNotExist: + logger.error(f"未找到订单类型映射: 对方俱乐部={our_club_id}, 对方类型ID={leixing_id}") + return Response({'code': 400, 'message': 'Order type mapping not found'}, status=status.HTTP_400_BAD_REQUEST) + + # 5. 获取我方商品类型信息(用于获取会员ID、佣金等) + try: + our_leixing = ShangpinLeixing.objects.get(id=our_type_id) + except ShangpinLeixing.DoesNotExist: + logger.error(f"我方商品类型不存在: {our_type_id}") + return Response({'code': 400, 'message': 'Local product type not found'}, status=status.HTTP_400_BAD_REQUEST) + + # 6. 幂等检查:如果已存在相同 partner_order_id 的订单,直接返回(附带 status='existed') + existing = Dingdan.objects.filter(partner_order_id=partner_order_id).first() + if existing: + logger.info(f"跨平台派单幂等返回: partner_order_id={partner_order_id} 已存在") + return Response({ + 'code': 0, + 'data': { + 'local_order_id': existing.dingdan_id, + 'status': 'existed' # 明确告知已存在 + } + }) + + # 7. 生成我方订单ID(确保唯一性) + 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}" + + # 8. 获取代理商家头像(用于订单图片) + agent_user = UserMain.objects.filter(yonghuid=settings.AGENT_SHANGJIA_ID).first() + agent_avatar = agent_user.avatar if agent_user else '' + + # 9. 计算我方实际打手分成金额:对方传来的打手分成 × 我方接收费率 + try: + actual_dashou_fencheng = Decimal(dashou_fencheng) * my_club.receive_rate + except Exception: + actual_dashou_fencheng = Decimal(dashou_fencheng) + + # 10. 事务创建订单 + try: + with transaction.atomic(): + new_id = generate_dingdan_id() + # 创建主订单 + dingdan = Dingdan.objects.create( + dingdan_id=new_id, + zhuangtai=1, # 普通订单(待接单) + fadan_pingtai=2, # 商家发单 + jine=jine, # 原始订单金额 + dashou_fencheng=actual_dashou_fencheng, # 调整后的打手分成 + jieshao=jieshao, + beizhu=beizhu, + create_time=create_time_str, + yaoqiuleixing=our_leixing.yaoqiuleixing, + nicheng=nicheng, + huiyuan_id=our_leixing.huiyuan_id, + yongjin=our_leixing.yongjin, + leixing_id=our_type_id, + partner_order_id=partner_order_id, # 存储对方订单ID + is_cross=1, + dispatch_type=2, # 对方派单 + partner_club_id=our_club_id, # 对方俱乐部ID + tupian=agent_avatar, # 使用代理商家头像作为订单图片 + ) + # 创建商家扩展表(使用代理商家) + DingdanShangjia.objects.create( + dingdan=dingdan, + shangjia_id=settings.AGENT_SHANGJIA_ID, + sjnicheng='跨平台代理', + partner_yonghu_id=partner_yonghu_id or '', + ) + + # 成功创建后更新每日派单统计(方向=对方派单) + try: + update_daily_dispatch_stat( + direction=2, # 对方派单 + partner_club_id=our_club_id, + dispatch_amount=Decimal(jine), # 统计使用原始订单金额 + + ) + except Exception as e: + logger.error(f"更新派单统计失败: {e}", exc_info=True) + + 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 = { + '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) # 瞬间返回,不阻塞请求 + + + + + return Response({ + 'code': 0, + 'data': { + 'local_order_id': new_id, + 'status': 'created' + } + }) + + except Exception as e: + logger.error(f"跨平台订单同步失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'message': 'Internal error'}, 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 '游戏订单' + + + + +from dingdan.utils import update_daily_dispatch_stat + +logger = logging.getLogger(__name__) + + + + + + +class PartnerCheckClaimView(APIView): + """ + 接收对方平台抢单询问(对方打手抢我方派出的订单) + 如果允许抢单: + 1. 订单状态改为2(已接单) + 2. 记录对方打手ID、对方俱乐部ID、对方订单ID + 3. 更新派单成交统计(success_count/success_amount) + """ + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + token = request.data.get('token') + if token != settings.CROSS_PLATFORM_TOKEN: + logger.warning(f"token无效: {token}") + return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) + + wofangdingdan_id = request.data.get('wofangdingdan_id') + partner_order_id = request.data.get('partner_order_id') + our_club_id = request.data.get('our_club_id') + dashou_id = request.data.get('dashou_id') + sj_id = request.data.get('sj_id') + + if not all([wofangdingdan_id, partner_order_id, our_club_id, dashou_id, sj_id]): + logger.error(f"参数缺失: {request.data}") + return Response({'code': 400, 'message': 'Missing parameters'}, status=status.HTTP_400_BAD_REQUEST) + + try: + with transaction.atomic(): + # 查询我方俱乐部配置 + try: + club = Club.objects.select_for_update().get(partner_club_id=our_club_id) + except Club.DoesNotExist: + logger.warning(f"俱乐部配置不存在 partner_club_id={our_club_id}") + return Response({'code': 0, 'data': {'can_claim': False}}) + + if not club.is_interop_enabled: + logger.warning(f"俱乐部 {club.club_id} 未开启互通") + return Response({'code': 0, 'data': {'can_claim': False}}) + + # 锁定订单 + try: + order = Dingdan.objects.select_for_update().get(dingdan_id=wofangdingdan_id) + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {wofangdingdan_id}") + return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND) + + # 只有状态1可抢 + if order.zhuangtai != 1: + logger.info(f"订单 {order.dingdan_id} 状态 {order.zhuangtai} 不可抢") + return Response({'code': 0, 'data': {'can_claim': False}}) + + # 商品类型检查 + try: + leixing = ShangpinLeixing.objects.get(id=order.leixing_id) + except ShangpinLeixing.DoesNotExist: + logger.warning(f"商品类型不存在: {order.leixing_id}") + return Response({'code': 0, 'data': {'can_claim': False}}) + + if not leixing.is_cross_enabled: + logger.warning(f"商品类型 {leixing.jieshao} 未开启跨平台") + return Response({'code': 0, 'data': {'can_claim': False}}) + + # 执行抢单:状态改为2(已接单) + order.zhuangtai = 2 + order.jiedan_dashou_id = dashou_id + order.partner_club_id = our_club_id + order.partner_order_id = partner_order_id + if order.is_cross != 1: + order.is_cross = 1 + # dispatch_type 保持原值(1-我方派单),不变 + order.save() + + # 更新派单成交统计(我方派单被成功接单) + update_daily_dispatch_stat( + direction=1, + partner_club_id=our_club_id, + claimed_amount=order.jine, + + ) + + logger.info(f"对方抢单成功: 订单 {order.dingdan_id}, 对方打手 {dashou_id}") + return Response({'code': 0, 'data': {'can_claim': True}}) + + except Exception as e: + logger.error(f"异常: {e}", exc_info=True) + return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + +class PartnerNotifyClaimView(APIView): + """ + 接收对方平台抢单成功通知(对方派单被他们自己的打手抢了) + 我方根据 partner_order_id 找到订单,如果状态为1则改为20,不更新任何统计。 + """ + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + token = request.data.get('token') + if token != settings.CROSS_PLATFORM_TOKEN: + logger.warning(f"token无效: {token}") + return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) + + partner_order_id = request.data.get('partner_order_id') + if not partner_order_id: + logger.error("缺少 partner_order_id") + return Response({'code': 400, 'message': 'Missing partner_order_id'}, status=status.HTTP_400_BAD_REQUEST) + + try: + with transaction.atomic(): + order = Dingdan.objects.select_for_update().get(partner_order_id=partner_order_id) + # 只有状态为1(未被抢)时才改为20 + if order.zhuangtai == 1: + order.zhuangtai = 20 + order.save() + logger.info(f"订单 {order.dingdan_id} 状态改为20(被其他俱乐部抢走)") + else: + logger.info(f"订单 {order.dingdan_id} 当前状态 {order.zhuangtai},无需修改") + return Response({'code': 0, 'message': 'OK'}) + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在 partner_order_id={partner_order_id}") + return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"异常: {e}", exc_info=True) + return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + +'''class PartnerSyncOrderView(APIView): + """ + 接收对方平台派单通知接口 + 对方平台商家派单成功后,异步调用此接口,将订单信息同步过来。 + 请求方式: POST + 接口地址: /dingdan/partner_sync_order + """ + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 验证 token + token = request.data.get('token') + if token != settings.CROSS_PLATFORM_TOKEN: + logger.warning(f"跨平台派单 token 无效: {token}") + return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) + + # 2. 获取必要参数 + our_club_id = request.data.get('our_club_id') # 对方平台发送的我方俱乐部ID(即对方在 Club 表中 partner_club_id) + partner_club_id = request.data.get('partner_club_id') # 对方平台的俱乐部ID(对方自己的ID) + partner_order_id = request.data.get('partner_order_id') + jine = request.data.get('jine') + dashou_fencheng = request.data.get('dashou_fencheng') + jieshao = request.data.get('jieshao') + beizhu = request.data.get('beizhu', '') + create_time_str = request.data.get('create_time') + nicheng = request.data.get('nicheng') + leixing_id = request.data.get('leixing_id') # 对方平台的商品类型ID + + # 必需字段校验 + required_fields = [our_club_id, partner_club_id, partner_order_id, jine, dashou_fencheng, + jieshao, create_time_str, nicheng, leixing_id] + if not all(required_fields): + logger.error(f"跨平台派单参数缺失: {request.data}") + return Response({'code': 400, 'message': 'Missing required parameters'}, status=status.HTTP_400_BAD_REQUEST) + + # 3. 验证我方俱乐部是否存在且同意接收订单 + # 注意:our_club_id 是对方发送过来的,代表我方在对方系统中的ID,所以用 partner_club_id 匹配 + try: + my_club = Club.objects.get(partner_club_id=our_club_id) + except Club.DoesNotExist: + logger.warning(f"我方俱乐部不存在,partner_club_id={our_club_id}") + return Response({'code': 404, 'message': 'Club not found'}, status=status.HTTP_404_NOT_FOUND) + + if not my_club.is_receive_enabled: + logger.warning(f"我方俱乐部 {my_club.club_id} 未同意接收订单") + return Response({'code': 403, 'message': 'Receiving not allowed'}, status=status.HTTP_403_FORBIDDEN) + + # 4. 查找订单类型映射 + try: + mapping = OrderTypeMapping.objects.get( + partner_club_id=our_club_id, + partner_type_id=leixing_id + ) + our_type_id = mapping.our_type_id + except OrderTypeMapping.DoesNotExist: + logger.error(f"未找到订单类型映射: 对方俱乐部={our_club_id}, 对方类型ID={leixing_id}") + return Response({'code': 400, 'message': 'Order type mapping not found'}, status=status.HTTP_400_BAD_REQUEST) + + # 5. 获取我方商品类型信息(用于获取会员ID、佣金等) + try: + our_leixing = ShangpinLeixing.objects.get(id=our_type_id) + except ShangpinLeixing.DoesNotExist: + logger.error(f"我方商品类型不存在: {our_type_id}") + return Response({'code': 400, 'message': 'Local product type not found'}, status=status.HTTP_400_BAD_REQUEST) + + # 6. 幂等检查:如果已存在相同 partner_order_id 的订单,直接返回 + existing = Dingdan.objects.filter(partner_order_id=partner_order_id).first() + if existing: + logger.info(f"跨平台派单幂等返回: partner_order_id={partner_order_id} 已存在") + return Response({ + 'code': 0, + 'data': {'local_order_id': existing.dingdan_id} + }) + + # 7. 生成我方订单ID + def generate_dingdan_id(): + import time, random, os + 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}" + + # 8. 获取代理商家头像(用于订单图片) + agent_user = UserMain.objects.filter(yonghuid=settings.AGENT_SHANGJIA_ID).first() + agent_avatar = agent_user.avatar if agent_user else '' + + # 9. 事务创建订单 + try: + with transaction.atomic(): + new_id = generate_dingdan_id() + # 创建主订单 + dingdan = Dingdan.objects.create( + dingdan_id=new_id, + zhuangtai=1, # 普通订单(默认) + fadan_pingtai=2, # 商家发单 + jine=jine, + dashou_fencheng=dashou_fencheng, + jieshao=jieshao, + beizhu=beizhu, + create_time=create_time_str, + yaoqiuleixing=our_leixing.yaoqiuleixing, + nicheng=nicheng, + huiyuan_id=our_leixing.huiyuan_id, + yongjin=our_leixing.yongjin, + leixing_id=our_type_id, + partner_order_id=partner_order_id, + is_cross=1, + dispatch_type=2, # 对方派给我方 + partner_club_id=our_club_id, # 对方的俱乐部ID + tupian=agent_avatar, # 使用代理商家头像作为订单图片 + ) + # 创建商家扩展表(使用代理商家) + DingdanShangjia.objects.create( + dingdan=dingdan, + shangjia_id=settings.AGENT_SHANGJIA_ID, + sjnicheng='跨平台代理' + ) + logger.info(f"跨平台订单同步成功: partner_order_id={partner_order_id}, local_id={new_id}") + return Response({ + 'code': 0, + 'data': {'local_order_id': new_id} + },status = 200) + except Exception as e: + logger.error(f"跨平台订单同步失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +from dingdan.utils import update_daily_dispatch_stat + +logger = logging.getLogger(__name__) + + + + + + +class PartnerCheckClaimView(APIView): + """ + 接收对方平台抢单询问接口 + 对方平台打手抢单前,调用此接口询问订单是否可抢。 + 请求方式: POST + 接口地址: /dingdan/partner_check_claim + 请求参数: + token: 认证令牌 + partner_order_id: 对方平台订单ID(即我方记录的 partner_order_id) + partner_club_id: 对方俱乐部ID(对方在我方系统中的标识) + dashou_id: 对方平台打手ID + sj_id: 对方平台商家ID(带前缀,如 SjAGENT001) + 返回: + code: 0成功 + data: { + can_claim: true/false + } + 注意:此接口会原子地检查并更新订单状态,必须使用行锁。 + """ + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 验证 token + token = request.data.get('token') + if token != settings.CROSS_PLATFORM_TOKEN: + logger.warning(f"跨平台抢单询问 token 无效: {token}") + return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) + + # 2. 获取必要参数 + duifangdingdan_id = request.data.get('wofangdingdan_id') + partner_order_id = request.data.get('partner_order_id') # 我方订单ID + partner_club_id = request.data.get('our_club_id') # 对方俱乐部ID + dashou_id = request.data.get('dashou_id') # 对方打手ID + sj_id = request.data.get('sj_id') # 对方商家ID(带前缀,如 SjAGENT001) + + if not all([partner_order_id, partner_club_id, dashou_id, sj_id]): + logger.error(f"跨平台抢单询问参数缺失: {request.data}") + return Response({'code': 400, 'message': 'Missing parameters'}, status=status.HTTP_400_BAD_REQUEST) + + try: + with transaction.atomic(): + # 3. 查询我方俱乐部配置(通过对方的 partner_club_id) + try: + club = Club.objects.select_for_update().get(partner_club_id=partner_club_id) + except Club.DoesNotExist: + logger.warning(f"跨平台抢单询问: 我方俱乐部配置不存在 partner_club_id={partner_club_id}") + return Response({'code': 404, 'message': 'Club not found'}, status=status.HTTP_404_NOT_FOUND) + + # 检查俱乐部是否已开启平台互通(我方同意接收对方订单) + if not club.is_interop_enabled: + logger.warning(f"俱乐部 {club.club_id} 未开启接收跨平台订单,拒绝抢单") + return Response({'code': 0, 'data': {'can_claim': False}}) + + # 4. 查询订单并锁定 + try: + order = Dingdan.objects.select_for_update().get(dingdan_id=partner_order_id) + except Dingdan.DoesNotExist: + logger.warning(f"跨平台抢单询问: 订单不存在 dingdan_id={partner_order_id}") + return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND) + + # 5. 检查订单状态(必须为1) + if order.zhuangtai != 1: + logger.info(f"跨平台抢单失败: 订单状态 {order.zhuangtai},不可抢") + return Response({'code': 0, 'data': {'can_claim': False}}) + + # 6. 检查订单对应的商品类型是否支持跨平台抢单 + try: + leixing = ShangpinLeixing.objects.get(id=order.leixing_id) + except ShangpinLeixing.DoesNotExist: + logger.warning(f"订单 {order.dingdan_id} 的商品类型 {order.leixing_id} 不存在") + return Response({'code': 0, 'data': {'can_claim': False}}) + + if not leixing.is_cross_enabled: + logger.warning(f"商品类型 {leixing.jieshao} 未开启跨平台抢单") + return Response({'code': 0, 'data': {'can_claim': False}}) + + # 7. 所有检查通过,执行抢单操作 + # 更新订单主表 + order.zhuangtai = 2 # 已接单 + order.jiedan_dashou_id = dashou_id # 对方打手ID + order.is_cross = 1 # 标记为跨平台订单 + order.dispatch_type = 1 # 我方派单,对方抢单 + order.partner_club_id = partner_club_id # 记录对方俱乐部ID + order.partner_order_id = duifangdingdan_id#记录对方订单id + # 存储对方商家ID(带前缀)到商家扩展表的 shangjia_id(如果原来有则更新) + # 注意:商家扩展表已存在,需要更新或创建?原订单可能已有 shangjia_kuozhan,我们直接更新 + order.save() + + + + # 8. 记录每日派单统计(使用新统计函数,不再更新 Club 表) + fencheng = order.dashou_fencheng or Decimal('0.00') + amount = fencheng * club.dashou_rate + update_daily_dispatch_stat( + dispatch_date=timezone.now().date(), + direction=2, # 对方派单 + partner_club_id=partner_club_id, + amount=amount, + count=1 + ) + # 9. 返回成功,告知对方可以抢单 + logger.info(f"跨平台抢单成功: 订单 {order.dingdan_id}, 对方打手 {dashou_id}, 俱乐部 {club.club_id}") + return Response({'code': 0, 'data': {'can_claim': True}}) + + except Exception as e: + logger.error(f"跨平台抢单询问异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + + + + +class PartnerNotifyClaimView(APIView): + """ + 接收对方平台抢单成功通知接口 + 对方平台打手抢单成功后,调用此接口,我方处理本地订单。 + 请求方式: POST + 接口地址: /dingdan/partner_notify_claim + 请求参数: + token: 认证令牌,需与settings.CROSS_PLATFORM_TOKEN一致 + order_id: 对方平台的订单ID(即我方记录的 partner_order_id) + 返回: + code: 0成功,其他失败 + message: 描述信息 + """ + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 验证token + token = request.data.get('token') + if token != settings.CROSS_PLATFORM_TOKEN: + logger.warning(f"跨平台抢单通知 token 无效: {token}") + return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) + + order_id = request.data.get('local_order_id') # 对方平台的订单ID(即我方的 partner_order_id) + if not order_id: + logger.error("跨平台抢单通知参数缺失: order_id") + return Response({'code': 400, 'message': 'Missing order_id'}, status=status.HTTP_400_BAD_REQUEST) + + try: + with transaction.atomic(): + # 2. 根据 partner_order_id 查找订单 + order = Dingdan.objects.select_for_update().get(partner_order_id=order_id) + + # 3. 根据订单状态处理 + if order.zhuangtai == 1: + # 订单未被抢,直接删除(包括关联的商家扩展表) + # 先删除扩展表 + DingdanShangjia.objects.filter(dingdan=order).delete() + # 再删除主订单 + order.delete() + logger.info(f"跨平台抢单通知:订单状态为1,已删除,partner_order_id={order_id}") + return Response({'code': 0, 'message': 'Order deleted'}) + else: + # 订单状态不是1(已被抢或异常),改为状态15(异常订单) + order.zhuangtai = 15 + order.save() + logger.info(f"跨平台抢单通知:订单状态异常,已改为15,partner_order_id={order_id}") + return Response({'code': 0, 'message': 'Order marked as abnormal'}) + + except Dingdan.DoesNotExist: + logger.warning(f"跨平台抢单通知:订单不存在 partner_order_id={order_id}") + return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"跨平台抢单通知异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' + + + + + + + + + +class PartnerRefundNotifyView(APIView): + """ + 接收对方平台退款通知接口 + 对方平台商家申请退款时,调用此接口通知我方,我方将本地对应订单改为退款状态。 + 请求方式: POST + 接口地址: /dingdan/partner_refund_notify + 请求参数: + token: 认证令牌,需与settings.CROSS_PLATFORM_TOKEN一致 + order_id: 对方平台的订单ID(即我方记录的 partner_order_id) + 返回: + code: 0成功,其他失败 + message: 描述信息 + """ + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 验证token + token = request.data.get('token') + if token != settings.CROSS_PLATFORM_TOKEN: + logger.warning(f"跨平台退款通知 token 无效: {token}") + return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) + + order_id = request.data.get('order_id') # 对方平台的订单ID(即我方的 partner_order_id) + if not order_id: + logger.error("跨平台退款通知参数缺失: order_id") + return Response({'code': 400, 'message': 'Missing order_id'}, status=status.HTTP_400_BAD_REQUEST) + + try: + with transaction.atomic(): + # 2. 根据 partner_order_id 查找订单(必须存在且为跨平台订单) + order = Dingdan.objects.select_for_update().get(partner_order_id=order_id, is_cross=1) + # 如果订单已完成,不允许退款 + if order.zhuangtai == 3: + logger.warning(f"跨平台退款通知:订单 {order_id} 已完成,不可退款") + return Response({'code': 400, 'message': 'Order already completed, cannot refund'}, status=status.HTTP_400_BAD_REQUEST) + + # 3. 如果订单状态不是5,则改为5(已退款),并更新打手信息 + if order.zhuangtai != 5: + # 记录原打手ID + old_dashou_id = order.jiedan_dashou_id + if old_dashou_id: + try: + # 查询打手用户主表 + dashou_user = UserMain.objects.get(yonghuid=old_dashou_id) + # 通过反向关系获取打手扩展表 + if hasattr(dashou_user, 'dashou_profile'): + dashou_profile = dashou_user.dashou_profile + dashou_profile.zhuangtai = 1 # 恢复打手状态 + dashou_profile.tuikuanliang += 1 # 退款订单量+1 + dashou_profile.save() + logger.info(f"退款通知:打手 {old_dashou_id} 状态已恢复,退款订单+1") + except UserMain.DoesNotExist: + logger.warning(f"退款通知:打手用户不存在 {old_dashou_id}") + except Exception as e: + logger.error(f"退款通知:更新打手信息失败 {str(e)}") + + # 更新订单状态为5 + order.zhuangtai = 5 + # 可选:清空接单打手ID + #order.jiedan_dashou_id = None + order.save() + logger.info(f"跨平台退款通知成功:订单 {order_id} 已改为退款状态") + else: + logger.info(f"跨平台退款通知:订单 {order_id} 已是退款状态,无需重复处理") + + return Response({'code': 0, 'message': 'Refund processed successfully'}) + + except Dingdan.DoesNotExist: + logger.warning(f"跨平台退款通知:订单不存在或不是跨平台订单,partner_order_id={order_id}") + return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"跨平台退款通知异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + + +logger = logging.getLogger(__name__) + + +class AdJuJueJieSuan(APIView): + """ + 后台拒绝结算接口(平台发单) + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /dingdan/adddjjjs + 参数: { + "zhanghao": "管理员账号(手机号)", + "dingdan_id": "订单ID", + "tkly": "拒绝结算理由(必填)" + } + """ + 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() + tkly = request.data.get('tkly', '').strip() + + if not all([qianzhanghao, dingdan_id, tkly]): + return Response( + {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id, tkly)', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2. JWT用户验证 + dangqianyonghu = request.user + + # 3. 验证手机号匹配(管理员账号通常为手机号) + if 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: + dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 6. 校验订单条件:平台发单(1)且状态为2或8 + if dingdan.fadan_pingtai != 1 or dingdan.zhuangtai not in [2, 8]: + return Response( + {'code': 400, 'message': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + jiedan_dashou_id = dingdan.jiedan_dashou_id + if not jiedan_dashou_id: + return Response( + {'code': 400, 'message': '订单暂无接单打手', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + with transaction.atomic(): + # 7. 更新订单状态为5(已退款),并保存拒绝理由 + dingdan.zhuangtai = 5 + dingdan.tkly = tkly + dingdan.save() + + # 8. 更新打手扩展表 + 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() + except (UserMain.DoesNotExist, AttributeError) as e: + logger.warning(f"拒绝结算:打手{jiedan_dashou_id}信息更新失败 - {str(e)}") + + return Response({'code': 0, 'message': '拒绝结算成功', 'data': None}) + + 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 AdZhuanYiDaTing(APIView): + """ + 后台转移大厅接口 + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /dingdan/adddzydt + 参数: { + "zhanghao": "管理员账号(手机号)", + "dingdan_id": "订单ID" + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + qianzhanghao = request.data.get('zhanghao', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not all([qianzhanghao, dingdan_id]): + return Response( + {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id)', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + dangqianyonghu = request.user + + if 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: + dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 校验订单状态:允许2、8、4 + if dingdan.zhuangtai not in [2, 8, 4]: + return Response( + {'code': 400, 'message': '仅状态为进行中(2)、结算中(8)或退款审核(4)的订单可转移大厅', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + jiedan_dashou_id = dingdan.jiedan_dashou_id + + with transaction.atomic(): + # 更新订单:状态改为1(下单中),清空接单打手ID + dingdan.zhuangtai = 1 + dingdan.jiedan_dashou_id = None + dingdan.save() + + # 如果有接单打手,更新其扩展表 + if jiedan_dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.zhuangtai = 1 + dashou_profile.tuikuanliang += 1 + dashou_profile.save() + except (UserMain.DoesNotExist, AttributeError) as e: + logger.warning(f"转移大厅:打手{jiedan_dashou_id}信息更新失败 - {str(e)}") + + return Response({'code': 0, 'message': '转移大厅成功', 'data': None}) + + 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 AdQuXiaoZhiDing(APIView): + """ + 后台取消指定接口 + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /dingdan/adddqxzd + 参数: { + "zhanghao": "管理员账号(手机号)", + "dingdan_id": "订单ID" + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + qianzhanghao = request.data.get('zhanghao', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not all([qianzhanghao, dingdan_id]): + return Response( + {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id)', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + dangqianyonghu = request.user + + if 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: + dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 校验订单状态:必须为7(指定中) + if dingdan.zhuangtai != 7: + return Response( + {'code': 400, 'message': '仅状态为指定中(7)的订单可取消指定', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + with transaction.atomic(): + # 更新订单:状态改为1(下单中),清空指定ID + dingdan.zhuangtai = 1 + dingdan.zhiding_id = None + dingdan.save() + + return Response({'code': 0, 'message': '取消指定成功', 'data': None}) + + 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 DashouXiugaiView(APIView): + """ + 打手修改订单交付内容接口 + 请求方式:POST + 请求路径:/dingdan/dsxiugaidd + 权限:需携带有效JWT Token,且用户为打手身份 + 请求参数: + dingdan_id : 订单ID(必填) + liuyan : 新的留言内容(可选,最长50字) + deleted_images : 要删除的图片URL列表(数组,每个元素为完整COS URL) + new_images : 新增的图片URL列表(数组,每个元素为完整COS URL) + 返回: + 成功:{"code":0,"msg":"修改成功","data":null} + 失败:返回相应错误码和提示 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 获取请求数据 + data = request.data + dingdan_id = data.get('dingdan_id', '') + liuyan = data.get('liuyan', '') + deleted_images = data.get('deleted_images', []) + new_images = data.get('new_images', []) + + # ---------- 参数校验 ---------- + if not dingdan_id: + return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + if not isinstance(deleted_images, list): + return Response({'code': 2, 'msg': 'deleted_images 必须为数组', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + if not isinstance(new_images, list): + return Response({'code': 3, 'msg': 'new_images 必须为数组', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + if liuyan and len(liuyan) > 50: + return Response({'code': 4, 'msg': '留言最多50个字', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # ---------- 用户身份验证 ---------- + current_user = request.user + yonghuid = current_user.yonghuid + + # 检查是否为打手 + try: + dashou_profile = current_user.dashou_profile # 假设User模型有related_name='dashou_profile' + except UserDashou.DoesNotExist: + return Response({'code': 5, 'msg': '用户不是打手身份', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + # ---------- 订单查询及权限验证 ---------- + try: + # 订单必须属于该打手,且状态为4(退款中)或8(结算中) + dingdan = Dingdan.objects.get( + dingdan_id=dingdan_id, + jiedan_dashou_id=yonghuid, + zhuangtai__in=[4, 8] # 只允许修改退款中或结算中的订单 + ) + except Dingdan.DoesNotExist: + return Response({'code': 6, 'msg': '订单不存在或没有修改权限', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + # ---------- 执行修改(使用事务保证原子性) ---------- + try: + with transaction.atomic(): + # 1. 处理删除的图片:从 Dashoutupian 表中删除记录 + if deleted_images: + # 根据订单ID、打手ID和图片URL删除记录 + delete_count, _ = Dashoutupian.objects.filter( + dingdan_id=dingdan_id, + dashou=yonghuid, + tupian__in=deleted_images + ).delete() + # 可选:记录删除数量 + logger.info(f"订单 {dingdan_id} 删除图片 {delete_count} 张") + + # 2. 处理新增的图片:插入新记录 + if new_images: + # 批量创建 + new_records = [] + for url in new_images: + new_records.append(Dashoutupian( + dingdan_id=dingdan_id, + dashou=yonghuid, + tupian=url, + create_time=timezone.now() + )) + Dashoutupian.objects.bulk_create(new_records) + logger.info(f"订单 {dingdan_id} 新增图片 {len(new_records)} 张") + + # 3. 更新订单留言(如果发生变化) + # 注意:即使留言为空字符串,也允许更新,表示清空留言 + if 'liuyan' in data: # 判断是否传了liuyan字段(可能为空字符串) + dingdan.dashou_liuyan = liuyan + dingdan.update_time = timezone.now() + dingdan.save(update_fields=['dashou_liuyan', 'update_time']) + + # 全部成功,返回成功响应 + return Response({ + 'code': 0, + 'msg': '修改成功', + 'data': None + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"修改订单失败: {str(e)}", exc_info=True) + return Response({ + 'code': 7, + 'msg': '服务器内部错误', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + + + + + +from django.core.exceptions import ObjectDoesNotExist +from dingdan.tongzhi_tasks import dingdan_guangbo + +class ZxsjghdsView(APIView): + """ + 商家更换打手接口 + 路径:POST /dingdan/zxsjghds + 权限:仅限已认证商家(通过JWT) + 功能: + 1. 将原订单状态改为已退款(5),释放接单打手(若有) + 2. 更新打手统计(退款量、状态、每日统计) + 3. 写入退款记录 + 4. 若为跨平台订单,通知对方平台退款(必须成功) + 5. 基于原订单生成新订单(状态直接为1),复制核心数据 + 6. 不操作商家余额、退款次数等(替换订单) + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # ========== 1. 获取当前商家用户 ========== + current_user = request.user + try: + yonghuid = current_user.yonghuid + except AttributeError: + return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # ========== 2. 提取订单ID ========== + dingdan_id = request.data.get('dingdan_id', '').strip() + if not dingdan_id: + return Response({'code': 2, 'msg': '订单ID不能为空'}, status=status.HTTP_400_BAD_REQUEST) + + # ========== 3. 查询订单并校验权限 ========== + try: + # 关联查询商家扩展表,确保当前商家是该订单的发布者 + shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').get( + dingdan__dingdan_id=dingdan_id, + shangjia_id=yonghuid + ) + except DingdanShangjia.DoesNotExist: + return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN) + + old_order = shangjia_kuozhan.dingdan + + # ========== 4. 状态校验(仅允许进行中或结算中) ========== + if old_order.zhuangtai not in [2, 8]: + return Response({'code': 4, 'msg': '当前订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST) + + # ========== 5. 收集订单关键数据(用于新订单复制) ========== + # 这些字段将在退款处理后用于创建新订单 + 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 + fadan_pingtai = old_order.fadan_pingtai + + # 跨平台相关 + 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 + + dashou_id = old_order.jiedan_dashou_id # 可能为空 + + # ========== 6. 退款处理(事务块1:退款原订单 + 打手统计 + 跨平台通知) ========== + try: + with transaction.atomic(): + # 6.1 订单状态更新为已退款 + old_order.zhuangtai = 5 + old_order.save(update_fields=['zhuangtai']) + + # 6.2 释放打手 + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang = (dashou_profile.tuikuanliang or 0) + 1 + dashou_profile.zhuangtai = 1 # 空闲 + dashou_profile.save(update_fields=['tuikuanliang', 'zhuangtai']) + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"打手 {dashou_id} 不存在或扩展表缺失,跳过更新") + + # 6.3 更新退款记录 + Tuikuanjilu.objects.update_or_create( + dingdan_id=dingdan_id, + defaults={ + 'dashouid': dashou_id or '', + 'qingqiuid': yonghuid, # 申请人为当前商家 + 'chuliid': yonghuid, # 处理人也是商家自己 + 'liyou': '商家更换打手', + 'sqzhuangtai': 1, # 状态 1=成功 + 'jine': jine or 0, + 'dashou_fencheng': dashou_fencheng or 0, + 'jieshao': jieshao or '', + 'beizhu': '更换打手系统自动退款', + 'nicheng': nicheng or '', + } + ) + + # 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, + action=3 # 3=退款 + ) + except Exception as e: + logger.error(f"更新打手每日统计失败: {e}") + # 统计失败不应阻断主流程,但可记录 + + # 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("我方俱乐部配置不存在") + 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.rstrip('/') + + notify_url = f"{partner_domain}/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: + transaction.set_rollback(True) + logger.error(f"对方平台接口HTTP错误: {resp.status_code}") + return Response({'code': 500, 'msg': '通知对方平台失败'}) + 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')}"}) + + # 更新跨平台扩展数据 + 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)}'}) + + except Exception as e: + logger.error(f"退款处理事务失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '退款处理失败'}) + + # ========== 7. 生成新订单(事务块2) ========== + try: + with transaction.atomic(): + # 7.1 生成新的订单ID + 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}" + + new_dingdan_id = generate_dingdan_id() + # 确保唯一 + while Dingdan.objects.filter(dingdan_id=new_dingdan_id).exists(): + new_dingdan_id = generate_dingdan_id() + + # 7.2 创建新订单(复制数据,状态为 1) + new_order = Dingdan.objects.create( + dingdan_id=new_dingdan_id, + zhuangtai=1, # 已下单 + fadan_pingtai=fadan_pingtai, + jine=jine, + dashou_fencheng=dashou_fencheng, + leixing_id=leixing_id, + yaoqiuleixing=yaoqiuleixing, + huiyuan_id=huiyuan_id, + yongjin=yongjin, + jieshao=jieshao, + nicheng=nicheng, + beizhu=beizhu, + user1_id=user1_id, # 商家标识 + tupian=tupian, + # 不继承:zhiding_id, jiedan_dashou_id, dashou_liuyan 等 + ) + + # 7.3 创建商家扩展表(关联新订单) + DingdanShangjia.objects.create( + dingdan=new_order, + shangjia_id=yonghuid, + sjnicheng=shangjia_kuozhan.sjnicheng or '', + ) + + # 7.4 异步通知打手(广播新订单) + try: + order_info = { + 'dingdan_id': new_dingdan_id, + 'game_type': self._get_game_type_name(leixing_id), + 'amount': str(jine), + 'order_desc': (jieshao or '')[:50], + } + dingdan_guangbo.delay(order_info) + except Exception as e: + logger.error(f"广播通知失败: {e}") + + # 7.5 跨平台同步(如果适用) + try: + sync_order_to_partners(new_dingdan_id) + except Exception as e: + logger.error(f"跨平台同步失败: {e}") + + # 注意:此处不更新商家余额、发布量等统计,因为是替换订单 + + logger.info(f"商家{yonghuid}更换打手成功,旧订单{dingdan_id}已退款,新订单{new_dingdan_id}") + + return Response({ + 'code': 0, + 'msg': '更换成功', + 'data': { + 'xin_dingdan_id': new_dingdan_id, + } + }) + + except Exception as e: + logger.error(f"生成新订单事务失败: {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 '游戏订单' + + + +# ===================== 视图:商家罚款申请 ===================== +# 文件路径:dingdan/views.py 或单独文件,根据项目结构放置 + + + + +class ShangjiaFakuanApplyView(APIView): + """ + 商家罚款申请接口 + 路径:POST /dingdan/sjfksq + 权限:JWT认证的商家用户 + 参数: + dingdan_id - 订单ID (必填) + chufa_liyou - 罚款原因 (必填) + fakuanjine - 罚款金额,元 (必填) + yingxiang_qiangdan - 是否影响抢单 1是 0否 (必填) + 说明: + 1. 自动从订单中获取被打手ID,防止商家篡改目标。 + 2. 同一订单同一打手不允许重复罚款。 + 3. 跨平台订单若为我方派单,则通知对方平台处罚;否则本地生成罚单。 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # ========== 1. 身份验证 ========== + current_user = request.user + try: + shangjia_id = current_user.yonghuid # 商家的用户ID + except AttributeError: + return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # ========== 2. 参数提取与校验 ========== + dingdan_id = request.data.get('dingdan_id', '').strip() + chufa_liyou = request.data.get('chufa_liyou', '').strip() + fakuanjine = request.data.get('fakuanjine', 0) + yingxiang_qiangdan = int(request.data.get('yingxiang_qiangdan', 1)) + + if not dingdan_id or not chufa_liyou or not fakuanjine: + return Response({'code': 2, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) + + try: + fakuanjine = float(fakuanjine) + if fakuanjine <= 0: + raise ValueError + except (ValueError, TypeError): + return Response({'code': 2, 'msg': '罚款金额格式错误'}, status=status.HTTP_400_BAD_REQUEST) + + # ========== 3. 获取订单,校验归属商家 ========== + try: + # 关联查询商家扩展表,确保订单属于当前商家 + shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').get( + dingdan__dingdan_id=dingdan_id, + shangjia_id=shangjia_id + ) + order = shangjia_kuozhan.dingdan + except DingdanShangjia.DoesNotExist: + return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN) + + # ========== 4. 获取被打手ID ========== + dashou_id = order.jiedan_dashou_id + if not dashou_id: + return Response({'code': 4, 'msg': '该订单暂无接单打手,无法罚款'}, status=status.HTTP_400_BAD_REQUEST) + + # ========== 5. 重复罚款检查 ========== + if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists(): + return Response({'code': 5, 'msg': '该打手在此订单已被罚款,不能重复申请'}, status=status.HTTP_400_BAD_REQUEST) + + # ========== 6. 跨平台信息 ========== + is_cross = order.is_cross + partner_order_id = order.partner_order_id or '' + partner_club_id = order.partner_club_id or '' + dispatch_type = order.dispatch_type or 0 # 1=我方派单,2=对方派单 + + # 是否需要通知对方平台:我方派单且存在对方订单ID + need_notify_partner = is_cross and partner_order_id and dispatch_type == 1 + + # ========== 7. 本地创建罚单(可重用) ========== + def create_local_fadan(): + Fadan.objects.create( + beichufa_id=dashou_id, + shenqing_chufa=shangjia_id, # 申请人为当前商家 + shenfen=1, # 被打者身份:1=打手 + chufaliyou=chufa_liyou, + fakuanjine=fakuanjine, + guanliandingdan_id=dingdan_id, + zhuangtai=1, # 待缴纳 + yingxiang_qiangdan=yingxiang_qiangdan, + shenqingren_shenfen=5, # 申请人是商家 + partner_club_id='', + ) + + # ========== 8. 本地处罚分支 ========== + if not need_notify_partner: + with transaction.atomic(): + create_local_fadan() + logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}罚款成功,金额{fakuanjine}") + return Response({'code': 0, 'msg': '罚款申请已提交'}) + + # ========== 9. 跨平台通知分支 ========== + try: + # 9.1 获取我方俱乐部ID + my_club = ClubConfig.objects.filter(is_self=1).first() + if not my_club: + return Response({'code': 500, 'msg': '系统配置错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + self_club_id = my_club.club_id + + # 9.2 获取对方平台域名 + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain.rstrip('/') + except ObjectDoesNotExist: + return Response({'code': 500, 'msg': '对方平台信息未配置'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 9.3 构造通知参数 + notify_url = f"{partner_domain}/houtai/kffkdssq_notify" # 与后台接口保持一致 + payload = { + 'order_id': partner_order_id, + 'club_id': self_club_id, + 'chufaliyou': chufa_liyou, + 'fakuanjine': fakuanjine, + 'yingxiang_qiangdan': yingxiang_qiangdan, + 'shenqingren_shenfen': 5, # 商家身份 + 'operator': shangjia_id, # 操作人ID + } + + # 9.4 带重试的通知 + timeout = 3 + retries = 2 + success = False + for attempt in range(retries): + try: + resp = requests.post(notify_url, json=payload, timeout=timeout) + if resp.status_code == 200: + resp_data = resp.json() + if resp_data.get('code') == 0: + success = True + break + else: + logger.warning(f"对方平台罚款通知失败,响应: {resp_data}") + else: + logger.warning(f"对方平台HTTP错误: {resp.status_code}") + except Exception as e: + logger.warning(f"通知对方平台异常,第{attempt+1}次尝试: {e}") + continue + + if not success: + return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 9.5 通知成功后,本地也创建罚单(标记跨平台) + with transaction.atomic(): + Fadan.objects.create( + beichufa_id=dashou_id, + shenqing_chufa=shangjia_id, + shenfen=1, + chufaliyou=chufa_liyou, + fakuanjine=fakuanjine, + guanliandingdan_id=dingdan_id, + zhuangtai=1, + yingxiang_qiangdan=yingxiang_qiangdan, + shenqingren_shenfen=5, + partner_club_id=partner_club_id, # 记录对方俱乐部ID + ) + + logger.info(f"商家{shangjia_id}跨平台罚款成功: {dingdan_id}") + return Response({'code': 0, 'msg': '罚款已提交至对方平台'}) + + + + + + + + + + +class ShangjiaFakuaiXiugaiView(APIView): + """ + 商家端统一修改接口:罚款修改/撤销/再次申请,订单退款撤销/修改理由 + URL: /dingdan/sjfkxgycx + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + user = request.user + # 验证商家身份 + try: + shangjia = user.shop_profile + except UserShangjia.DoesNotExist: + return Response({'code': 403, 'msg': '您不是商家'}, status=status.HTTP_403_FORBIDDEN) + if shangjia.zhuangtai != 1: + return Response({'code': 403, 'msg': '商家状态异常'}, status=status.HTTP_403_FORBIDDEN) + + operation = request.data.get('operation') + dingdan_id = request.data.get('dingdan_id') + if not dingdan_id: + return Response({'code': 400, 'msg': '缺少订单ID'}, status=status.HTTP_400_BAD_REQUEST) + + # 验证订单属于该商家 + try: + dingdan = 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 not hasattr(dingdan, 'shangjia_kuozhan') or dingdan.shangjia_kuozhan.shangjia_id != user.yonghuid: + return Response({'code': 403, 'msg': '无权操作此订单'}, status=status.HTTP_403_FORBIDDEN) + + # 处理订单相关操作 + if operation == 'cancel_refund': + return self._cancel_refund(dingdan, request.data.get('liyou', '')) + elif operation == 'modify_refund_reason': + return self._modify_refund_reason(dingdan, request.data.get('liyou', '')) + + # 处理罚款相关操作 + 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) + except Fadan.DoesNotExist: + return Response({'code': 404, 'msg': '罚单不存在'}, status=status.HTTP_404_NOT_FOUND) + if fadan.guanliandingdan_id != dingdan_id: + return Response({'code': 403, 'msg': '罚单与订单不匹配'}, status=status.HTTP_403_FORBIDDEN) + + if operation == 'modify_penalty': + return self._modify_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine')) + elif operation == 'cancel_penalty': + return self._cancel_penalty(fadan, request.data.get('liyou', '')) + elif operation == 'resubmit_penalty': + return self._resubmit_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine')) + else: + return Response({'code': 400, 'msg': '无效的操作类型'}, status=status.HTTP_400_BAD_REQUEST) + + def _cancel_refund(self, dingdan, liyou): + """撤销退款申请(状态4 → 状态8,清空退款理由)""" + if dingdan.zhuangtai != 4: + return Response({'code': 400, 'msg': '只有退款中的订单可以撤销退款申请'}) + with transaction.atomic(): + dingdan.zhuangtai = 8 + dingdan.tkly = '' # 清空退款理由 + dingdan.save() + # 记录撤销理由可存入日志(如需要可另建日志表,此处简单处理) + logger.info(f"商家撤销退款订单 {dingdan.dingdan_id},理由: {liyou}") + return Response({'code': 0, 'msg': '已撤销退款申请'}) + + def _modify_refund_reason(self, dingdan, liyou): + """修改退款理由(仅状态4时)""" + if dingdan.zhuangtai != 4: + return Response({'code': 400, 'msg': '只有退款中的订单可以修改退款理由'}) + if not liyou.strip(): + return Response({'code': 400, 'msg': '退款理由不能为空'}) + dingdan.tkly = liyou[:500] + dingdan.save() + return Response({'code': 0, 'msg': '退款理由修改成功'}) + + def _modify_penalty(self, fadan, liyou, jine_str): + """修改罚款(仅状态1待缴纳 或 3申诉中)""" + if fadan.zhuangtai not in (1, 3): + return Response({'code': 400, 'msg': '当前罚单状态不可修改'}) + if not liyou.strip(): + return Response({'code': 400, 'msg': '罚款理由不能为空'}) + try: + jine = Decimal(jine_str) + if jine <= 0: + raise ValueError + except: + return Response({'code': 400, 'msg': '罚款金额必须为正数'}) + with transaction.atomic(): + fadan.chufaliyou = liyou[:500] + fadan.fakuanjine = jine + # 如果之前是驳回状态修改,需要重置状态为待缴纳?不,修改不应改变状态 + fadan.save() + return Response({'code': 0, 'msg': '罚款修改成功'}) + + def _cancel_penalty(self, fadan, liyou): + """撤销罚款(状态1或3 → 状态4 已驳回)""" + if fadan.zhuangtai not in (1, 3): + return Response({'code': 400, 'msg': '当前罚单状态不可撤销'}) + if not liyou.strip(): + return Response({'code': 400, 'msg': '驳回理由不能为空'}) + with transaction.atomic(): + fadan.zhuangtai = 4 # 已驳回 + fadan.bohuiliyou = liyou[:500] + fadan.save() + return Response({'code': 0, 'msg': '罚款已撤销(驳回)'}) + + def _resubmit_penalty(self, fadan, liyou, jine_str): + """再次申请罚款(状态4 → 状态1,修改理由和金额)""" + if fadan.zhuangtai != 4: + return Response({'code': 400, 'msg': '只有已驳回的罚单可再次申请'}) + if not liyou.strip(): + return Response({'code': 400, 'msg': '罚款理由不能为空'}) + try: + jine = Decimal(jine_str) + if jine <= 0: + raise ValueError + except: + return Response({'code': 400, 'msg': '罚款金额必须为正数'}) + with transaction.atomic(): + fadan.zhuangtai = 1 # 待缴纳 + fadan.chufaliyou = liyou[:500] + fadan.fakuanjine = jine + fadan.bohuiliyou = '' # 清空驳回理由 + fadan.save() + return Response({'code': 0, 'msg': '已重新申请罚款'}) \ No newline at end of file diff --git a/gunicorn.conf.py b/gunicorn.conf.py new file mode 100644 index 0000000..1e0e690 --- /dev/null +++ b/gunicorn.conf.py @@ -0,0 +1,52 @@ +# /opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.443/django/gunicorn.conf.py + +# 🔥 绑定的IP和端口(根据你的Nginx配置,后端在8001端口) +bind = "127.0.0.1:8001" + +# 🔥 工作进程数(根据CPU核心数调整,建议:CPU核心数*2+1) +# 查看CPU核心数:cat /proc/cpuinfo | grep "processor" | wc -l +workers = 4 + +# 🔥 工作模式(使用gevent支持异步,更高效) +#worker_class = "gevent" +worker_class = "sync" # 改成这个 + +# 🔥 每个worker的最大请求数(防止内存泄漏) +max_requests = 1000 +max_requests_jitter = 50 # 随机抖动,避免所有worker同时重启 + +# 🔥 超时时间(秒) +timeout = 30 +keepalive = 2 + +# 🔥 预加载应用(减少内存占用,加快请求处理) +preload_app = True + +# 🔥 日志配置 +# 访问日志:输出到标准输出,便于OnePanel查看 +accesslog = "-" +# 错误日志:输出到标准错误 +errorlog = "-" +# 日志级别 +loglevel = "info" + +# 🔥 进程名称(便于识别) +proc_name = "a_long_dianjing_gunicorn" + +# 🔥 工作进程的用户/组(安全考虑,用非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) +daemon = False \ No newline at end of file diff --git a/houtai/_init_.py b/houtai/_init_.py new file mode 100644 index 0000000..e69de29 diff --git a/houtai/apps.py b/houtai/apps.py new file mode 100644 index 0000000..bc21450 --- /dev/null +++ b/houtai/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class HoutaiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'houtai' \ No newline at end of file diff --git a/houtai/migrations/0001_initial.py b/houtai/migrations/0001_initial.py new file mode 100644 index 0000000..e71b4e2 --- /dev/null +++ b/houtai/migrations/0001_initial.py @@ -0,0 +1,296 @@ +# Generated by Django 4.2.27 on 2026-04-06 23:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="AbnormalUserLog", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "account_id", + models.CharField( + db_index=True, max_length=20, verbose_name="账号ID(phone)" + ), + ), + ( + "ip_address", + models.GenericIPAddressField( + blank=True, null=True, verbose_name="IP地址" + ), + ), + ( + "real_ip", + models.GenericIPAddressField( + blank=True, null=True, verbose_name="真实IP(穿透代理后)" + ), + ), + ( + "x_forwarded_for", + models.CharField( + blank=True, + default="", + max_length=255, + verbose_name="X-Forwarded-For", + ), + ), + ( + "user_agent", + models.CharField( + blank=True, + default="", + max_length=500, + verbose_name="User-Agent", + ), + ), + ( + "request_method", + models.CharField( + blank=True, default="", max_length=10, verbose_name="请求方法" + ), + ), + ( + "request_path", + models.CharField( + blank=True, default="", max_length=500, verbose_name="请求路径" + ), + ), + ( + "request_params", + models.TextField( + blank=True, default="", verbose_name="请求参数(GET/POST)" + ), + ), + ( + "request_body", + models.TextField( + blank=True, default="", verbose_name="请求体(原始)" + ), + ), + ( + "request_headers", + models.TextField( + blank=True, default="", verbose_name="请求头(JSON格式)" + ), + ), + ( + "attempt_type", + models.CharField( + blank=True, default="", max_length=50, verbose_name="攻击类型" + ), + ), + ( + "detail", + models.TextField(blank=True, default="", verbose_name="详细描述"), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ], + options={ + "verbose_name": "异常用户日志", + "verbose_name_plural": "异常用户日志", + "db_table": "abnormal_user_log", + }, + ), + migrations.CreateModel( + name="Permission", + fields=[ + ( + "perm_id", + models.AutoField( + primary_key=True, serialize=False, verbose_name="权限ID" + ), + ), + ( + "perm_code", + models.CharField( + db_index=True, + max_length=100, + unique=True, + verbose_name="权限编码", + ), + ), + ( + "perm_name", + models.CharField(max_length=100, verbose_name="权限名称"), + ), + ( + "description", + models.CharField( + blank=True, default="", max_length=200, verbose_name="权限描述" + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "verbose_name": "权限", + "verbose_name_plural": "权限", + "db_table": "permission", + }, + ), + migrations.CreateModel( + name="Role", + fields=[ + ( + "role_id", + models.AutoField( + primary_key=True, serialize=False, verbose_name="角色ID" + ), + ), + ( + "role_code", + models.CharField( + db_index=True, + max_length=50, + unique=True, + verbose_name="角色编码", + ), + ), + ("role_name", models.CharField(max_length=50, verbose_name="角色名称")), + ( + "description", + models.CharField( + blank=True, default="", max_length=200, verbose_name="角色描述" + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "verbose_name": "角色", + "verbose_name_plural": "角色", + "db_table": "role", + }, + ), + migrations.CreateModel( + name="UserRole", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ( + "account_id", + models.CharField( + db_index=True, max_length=11, verbose_name="账号ID" + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ( + "role", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="houtai.role", + verbose_name="角色", + ), + ), + ], + options={ + "verbose_name": "用户角色关联", + "verbose_name_plural": "用户角色关联", + "db_table": "user_role", + }, + ), + migrations.CreateModel( + name="RolePermission", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "permission", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="houtai.permission", + verbose_name="权限", + ), + ), + ( + "role", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="houtai.role", + verbose_name="角色", + ), + ), + ], + options={ + "verbose_name": "角色权限关联", + "verbose_name_plural": "角色权限关联", + "db_table": "role_permission", + }, + ), + migrations.AddIndex( + model_name="role", + index=models.Index(fields=["role_code"], name="role_role_co_b9f497_idx"), + ), + migrations.AddIndex( + model_name="permission", + index=models.Index( + fields=["perm_code"], name="permission_perm_co_56cbe3_idx" + ), + ), + migrations.AddIndex( + model_name="abnormaluserlog", + index=models.Index( + fields=["account_id"], name="abnormal_us_account_425899_idx" + ), + ), + migrations.AddIndex( + model_name="abnormaluserlog", + index=models.Index( + fields=["ip_address"], name="abnormal_us_ip_addr_3d612b_idx" + ), + ), + migrations.AddIndex( + model_name="abnormaluserlog", + index=models.Index( + fields=["create_time"], name="abnormal_us_create__ee16da_idx" + ), + ), + migrations.AlterUniqueTogether( + name="userrole", + unique_together={("account_id", "role")}, + ), + migrations.AlterUniqueTogether( + name="rolepermission", + unique_together={("role", "permission")}, + ), + ] diff --git a/houtai/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py b/houtai/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py new file mode 100644 index 0000000..6284dd8 --- /dev/null +++ b/houtai/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py @@ -0,0 +1,339 @@ +# Generated by Django 4.2.27 on 2026-04-09 14:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("houtai", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="ZuzhangRiTongji", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "yonghuid", + models.CharField( + db_index=True, max_length=7, verbose_name="组长用户ID" + ), + ), + ("riqi", models.DateField(db_index=True, verbose_name="日期")), + ( + "yaoqing_guanshi_shu", + models.IntegerField(default=0, verbose_name="邀请管事总数"), + ), + ( + "shouru_zonge", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="收入总额(分佣)", + ), + ), + ( + "fenyong_jine", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="分佣金额", + ), + ), + ], + options={ + "verbose_name": "组长每日统计", + "db_table": "zuzhang_ri_tongji", + "indexes": [ + models.Index( + fields=["riqi", "-yaoqing_guanshi_shu"], + name="zuzhang_ri__riqi_87169e_idx", + ), + models.Index( + fields=["riqi", "-shouru_zonge"], + name="zuzhang_ri__riqi_1d7e51_idx", + ), + ], + "unique_together": {("yonghuid", "riqi")}, + }, + ), + migrations.CreateModel( + name="ShangjiaRiTongji", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "yonghuid", + models.CharField( + db_index=True, max_length=7, verbose_name="商家用户ID" + ), + ), + ("riqi", models.DateField(db_index=True, verbose_name="日期")), + ( + "paifa_dingdan_shu", + models.IntegerField(default=0, verbose_name="派发订单总数"), + ), + ( + "paifa_jine", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="派发订单总金额", + ), + ), + ( + "jiesuan_dingdan_shu", + models.IntegerField(default=0, verbose_name="结算订单总数"), + ), + ( + "jiesuan_jine", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="结算总金额", + ), + ), + ( + "tuikuan_dingdan_shu", + models.IntegerField(default=0, verbose_name="退款订单总数"), + ), + ( + "tuikuan_jine", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="退款总金额", + ), + ), + ], + options={ + "verbose_name": "商家每日统计", + "db_table": "shangjia_ri_tongji", + "indexes": [ + models.Index( + fields=["riqi", "-paifa_jine"], + name="shangjia_ri_riqi_d00f7f_idx", + ), + models.Index( + fields=["riqi", "-jiesuan_jine"], + name="shangjia_ri_riqi_25772a_idx", + ), + models.Index( + fields=["riqi", "-paifa_dingdan_shu"], + name="shangjia_ri_riqi_e05c54_idx", + ), + ], + "unique_together": {("yonghuid", "riqi")}, + }, + ), + migrations.CreateModel( + name="GuanshiXufeiRiTongji", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "yonghuid", + models.CharField( + db_index=True, max_length=7, verbose_name="管事用户ID" + ), + ), + ("riqi", models.DateField(db_index=True, verbose_name="日期")), + ( + "xufei_zongshu", + models.IntegerField(default=0, verbose_name="今日续费总数"), + ), + ( + "xufei_shouyi", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="续费收益金额", + ), + ), + ], + options={ + "verbose_name": "管事续费每日统计", + "db_table": "guanshi_xufei_ri_tongji", + "indexes": [ + models.Index( + fields=["riqi", "-xufei_zongshu"], + name="guanshi_xuf_riqi_7c128f_idx", + ), + models.Index( + fields=["riqi", "-xufei_shouyi"], + name="guanshi_xuf_riqi_8f09b5_idx", + ), + ], + "unique_together": {("yonghuid", "riqi")}, + }, + ), + migrations.CreateModel( + name="GuanshiRiTongji", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "yonghuid", + models.CharField( + db_index=True, max_length=7, verbose_name="管事用户ID" + ), + ), + ("riqi", models.DateField(db_index=True, verbose_name="日期")), + ( + "yaoqing_dashou_shu", + models.IntegerField(default=0, verbose_name="邀请打手总数"), + ), + ( + "chongzhi_dashou_shu", + models.IntegerField(default=0, verbose_name="充值打手数量"), + ), + ( + "shouru_zonge", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="收入总额", + ), + ), + ], + options={ + "verbose_name": "管事每日统计", + "db_table": "guanshi_ri_tongji", + "indexes": [ + models.Index( + fields=["riqi", "-yaoqing_dashou_shu"], + name="guanshi_ri__riqi_075e1a_idx", + ), + models.Index( + fields=["riqi", "-chongzhi_dashou_shu"], + name="guanshi_ri__riqi_78a79d_idx", + ), + models.Index( + fields=["riqi", "-shouru_zonge"], + name="guanshi_ri__riqi_334f52_idx", + ), + ], + "unique_together": {("yonghuid", "riqi")}, + }, + ), + migrations.CreateModel( + name="DashouRiTongji", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "yonghuid", + models.CharField( + db_index=True, max_length=7, verbose_name="打手用户ID" + ), + ), + ("riqi", models.DateField(db_index=True, verbose_name="日期")), + ( + "jiedan_zongliang", + models.IntegerField(default=0, verbose_name="接单总量"), + ), + ( + "chengjiao_zongliang", + models.IntegerField(default=0, verbose_name="成交单总量"), + ), + ( + "jiedan_zonge", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="接单总金额", + ), + ), + ( + "chengjiao_zonge", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="成交总金额", + ), + ), + ( + "tuikuan_liang", + models.IntegerField(default=0, verbose_name="退款订单量"), + ), + ( + "tuikuan_jine", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="退款总金额", + ), + ), + ], + options={ + "verbose_name": "打手每日统计", + "db_table": "dashou_ri_tongji", + "indexes": [ + models.Index( + fields=["riqi", "-jiedan_zongliang"], + name="dashou_ri_t_riqi_95a785_idx", + ), + models.Index( + fields=["riqi", "-chengjiao_zongliang"], + name="dashou_ri_t_riqi_ad9c1a_idx", + ), + models.Index( + fields=["riqi", "-jiedan_zonge"], + name="dashou_ri_t_riqi_5da703_idx", + ), + models.Index( + fields=["riqi", "-chengjiao_zonge"], + name="dashou_ri_t_riqi_0a3156_idx", + ), + ], + "unique_together": {("yonghuid", "riqi")}, + }, + ), + ] diff --git a/houtai/migrations/0003_tixianritongji.py b/houtai/migrations/0003_tixianritongji.py new file mode 100644 index 0000000..8149a08 --- /dev/null +++ b/houtai/migrations/0003_tixianritongji.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.27 on 2026-04-10 18:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("houtai", "0002_zuzhangritongji_shangjiaritongji_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="TixianRiTongji", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("riqi", models.DateField(db_index=True, verbose_name="日期")), + ( + "leixing", + models.IntegerField( + choices=[(1, "打手"), (2, "管事"), (3, "组长")], + verbose_name="用户类型", + ), + ), + ( + "total_amount", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="当天提现总额(元)", + ), + ), + ], + options={ + "verbose_name": "每日提现统计", + "db_table": "tixian_ri_tongji", + "indexes": [ + models.Index( + fields=["riqi", "leixing"], name="tixian_ri_t_riqi_c18bb2_idx" + ) + ], + "unique_together": {("riqi", "leixing")}, + }, + ), + ] diff --git a/houtai/migrations/__init__.py b/houtai/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/houtai/migrations/_init_.py b/houtai/migrations/_init_.py new file mode 100644 index 0000000..e69de29 diff --git a/houtai/models.py b/houtai/models.py new file mode 100644 index 0000000..b252fb3 --- /dev/null +++ b/houtai/models.py @@ -0,0 +1,269 @@ +from django.db import models + +# ==================== 1. 角色表 ==================== +class Role(models.Model): + """角色表""" + role_id = models.AutoField(primary_key=True, verbose_name='角色ID') + role_code = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='角色编码') + role_name = models.CharField(max_length=50, verbose_name='角色名称') + description = models.CharField(max_length=200, blank=True, default='', verbose_name='角色描述') + + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'role' + verbose_name = '角色' + verbose_name_plural = '角色' + indexes = [ + models.Index(fields=['role_code']), + + ] + + def __str__(self): + return self.role_name + + +# ==================== 2. 权限表 ==================== +class Permission(models.Model): + """权限表""" + perm_id = models.AutoField(primary_key=True, verbose_name='权限ID') + perm_code = models.CharField(max_length=100, unique=True, db_index=True, verbose_name='权限编码') + perm_name = models.CharField(max_length=100, verbose_name='权限名称') + description = models.CharField(max_length=200, blank=True, default='', verbose_name='权限描述') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'permission' + verbose_name = '权限' + verbose_name_plural = '权限' + indexes = [ + models.Index(fields=['perm_code']), + ] + + def __str__(self): + return f"{self.perm_name} ({self.perm_code})" + + +# ==================== 3. 角色-权限关联表 ==================== +class RolePermission(models.Model): + """角色权限关联表""" + id = models.AutoField(primary_key=True) + role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色') + permission = models.ForeignKey(Permission, on_delete=models.CASCADE, db_index=True, verbose_name='权限') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'role_permission' + verbose_name = '角色权限关联' + verbose_name_plural = '角色权限关联' + unique_together = [['role', 'permission']] # 防止重复授权 + + def __str__(self): + return f"{self.role.role_name} -> {self.permission.perm_name}" + + +# ==================== 4. 用户-角色关联表 ==================== +class UserRole(models.Model): + """用户角色关联表""" + id = models.AutoField(primary_key=True) + account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID') # 对应 UserMain 的 phone 字段 + role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'user_role' + verbose_name = '用户角色关联' + verbose_name_plural = '用户角色关联' + unique_together = [['account_id', 'role']] # 同一用户同一角色只能有一条 + + def __str__(self): + return f"{self.account_id} -> {self.role.role_name}" + + +class AbnormalUserLog(models.Model): + """记录恶意尝试越权操作的用户信息""" + # 账号信息 + account_id = models.CharField(max_length=20, db_index=True, verbose_name='账号ID(phone)') + + # 网络信息 + ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name='IP地址') + real_ip = models.GenericIPAddressField(null=True, blank=True, verbose_name='真实IP(穿透代理后)') + x_forwarded_for = models.CharField(max_length=255, blank=True, default='', verbose_name='X-Forwarded-For') + user_agent = models.CharField(max_length=500, blank=True, default='', verbose_name='User-Agent') + + # 地理位置信息(需借助第三方API,这里只存储请求时的IP,后续可离线解析) + # 但我们可以存储更多请求细节 + request_method = models.CharField(max_length=10, blank=True, default='', verbose_name='请求方法') + request_path = models.CharField(max_length=500, blank=True, default='', verbose_name='请求路径') + request_params = models.TextField(blank=True, default='', verbose_name='请求参数(GET/POST)') + request_body = models.TextField(blank=True, default='', verbose_name='请求体(原始)') + request_headers = models.TextField(blank=True, default='', verbose_name='请求头(JSON格式)') + + # 攻击特征 + attempt_type = models.CharField(max_length=50, blank=True, default='', verbose_name='攻击类型') + detail = models.TextField(blank=True, default='', verbose_name='详细描述') + + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'abnormal_user_log' + verbose_name = '异常用户日志' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['account_id']), + models.Index(fields=['ip_address']), + models.Index(fields=['create_time']), + ] + + def __str__(self): + return f"{self.account_id} @ {self.ip_address} - {self.create_time}" + + + + + +class ShangjiaRiTongji(models.Model): + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='商家用户ID') + riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 + + # 派发统计 + paifa_dingdan_shu = models.IntegerField(default=0, verbose_name='派发订单总数') + paifa_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='派发订单总金额') + + # 结算统计(成交) + jiesuan_dingdan_shu = models.IntegerField(default=0, verbose_name='结算订单总数') + jiesuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='结算总金额') + + # 退款统计(可选) + tuikuan_dingdan_shu = models.IntegerField(default=0, verbose_name='退款订单总数') + tuikuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总金额') + + class Meta: + db_table = 'shangjia_ri_tongji' + verbose_name = '商家每日统计' + unique_together = [['yonghuid', 'riqi']] # 每人每天一条记录 + indexes = [ + models.Index(fields=['riqi', '-paifa_jine']), + models.Index(fields=['riqi', '-jiesuan_jine']), + models.Index(fields=['riqi', '-paifa_dingdan_shu']), + ] + + + + + +class DashouRiTongji(models.Model): + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='打手用户ID') + riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 + + # 接单统计 + jiedan_zongliang = models.IntegerField(default=0, verbose_name='接单总量') + chengjiao_zongliang = models.IntegerField(default=0, verbose_name='成交单总量') + + # 金额统计 + jiedan_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='接单总金额') + chengjiao_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='成交总金额') + + # 退款统计 + tuikuan_liang = models.IntegerField(default=0, verbose_name='退款订单量') + tuikuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总金额') + + class Meta: + db_table = 'dashou_ri_tongji' + verbose_name = '打手每日统计' + unique_together = [['yonghuid', 'riqi']] + indexes = [ + models.Index(fields=['riqi', '-jiedan_zongliang']), + models.Index(fields=['riqi', '-chengjiao_zongliang']), + models.Index(fields=['riqi', '-jiedan_zonge']), + models.Index(fields=['riqi', '-chengjiao_zonge']), + ] + + + +class ZuzhangRiTongji(models.Model): + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='组长用户ID') + riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 + + yaoqing_guanshi_shu = models.IntegerField(default=0, verbose_name='邀请管事总数') + shouru_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='收入总额(分佣)') + + # 可选明细 + fenyong_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='分佣金额') + + class Meta: + db_table = 'zuzhang_ri_tongji' + verbose_name = '组长每日统计' + unique_together = [['yonghuid', 'riqi']] + indexes = [ + models.Index(fields=['riqi', '-yaoqing_guanshi_shu']), + models.Index(fields=['riqi', '-shouru_zonge']), + ] + + + + +class GuanshiRiTongji(models.Model): + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID') + riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 + + yaoqing_dashou_shu = models.IntegerField(default=0, verbose_name='邀请打手总数') + chongzhi_dashou_shu = models.IntegerField(default=0, verbose_name='充值打手数量') + shouru_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='收入总额') + + class Meta: + db_table = 'guanshi_ri_tongji' + verbose_name = '管事每日统计' + unique_together = [['yonghuid', 'riqi']] + indexes = [ + models.Index(fields=['riqi', '-yaoqing_dashou_shu']), + models.Index(fields=['riqi', '-chongzhi_dashou_shu']), + models.Index(fields=['riqi', '-shouru_zonge']), + ] + + + + +class GuanshiXufeiRiTongji(models.Model): + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID') + riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 + + xufei_zongshu = models.IntegerField(default=0, verbose_name='今日续费总数') # 续费订单数(或续费打手数) + xufei_shouyi = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='续费收益金额') + + class Meta: + db_table = 'guanshi_xufei_ri_tongji' + verbose_name = '管事续费每日统计' + unique_together = [['yonghuid', 'riqi']] + indexes = [ + models.Index(fields=['riqi', '-xufei_zongshu']), + models.Index(fields=['riqi', '-xufei_shouyi']), + ] + + + +# models.py 中添加 + +class TixianRiTongji(models.Model): + """ + 每日提现统计表(按身份统计当天提现总额) + """ + riqi = models.DateField(db_index=True, verbose_name='日期') + leixing = models.IntegerField( + choices=[(1, '打手'), (2, '管事'), (3, '组长')], + verbose_name='用户类型' + ) + total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='当天提现总额(元)') + + class Meta: + db_table = 'tixian_ri_tongji' + verbose_name = '每日提现统计' + unique_together = [['riqi', 'leixing']] # 每天每个身份一条记录 + indexes = [models.Index(fields=['riqi', 'leixing'])] + + def __str__(self): + return f"{self.riqi} - {self.get_leixing_display()} 提现总额: {self.total_amount}" \ No newline at end of file diff --git a/houtai/serializers.py b/houtai/serializers.py new file mode 100644 index 0000000..68c7b37 --- /dev/null +++ b/houtai/serializers.py @@ -0,0 +1,39 @@ +# serializers.py + +#小程序弹窗序列化器 +from rest_framework import serializers +from peizhi.models import PopupPage, PopupConfig, PopupImage + +class PopupImageSerializer(serializers.ModelSerializer): + """图片序列化器(输出用)""" + class Meta: + model = PopupImage + fields = ['id', 'image_url', 'text', 'sort_order'] + + +class PopupConfigSerializer(serializers.ModelSerializer): + """弹窗配置序列化器(输出用)""" + images = PopupImageSerializer(many=True, read_only=True) + + class Meta: + model = PopupConfig + fields = [ + 'id', 'popup_id', 'title', 'description', + 'strategy_type', 'max_count', 'reset_interval', + 'duration', 'force_even_muted', 'sort_order', + 'is_active', 'start_time', 'end_time', + 'created_at', 'updated_at', 'images' + ] + + +class PopupPageSerializer(serializers.ModelSerializer): + """页面序列化器(输出用)""" + popups = PopupConfigSerializer(many=True, read_only=True) + + class Meta: + model = PopupPage + fields = [ + 'id', 'page_key', 'name', 'description', + 'is_active', 'ignore_user_mute', + 'created_at', 'updated_at', 'popups' + ] \ No newline at end of file diff --git a/houtai/urls.py b/houtai/urls.py new file mode 100644 index 0000000..5b45078 --- /dev/null +++ b/houtai/urls.py @@ -0,0 +1,137 @@ +from django.urls import path +from django.views.decorators.csrf import csrf_exempt + +from .view import GetClubConfigView, GetCrossOrderListView, PartnerGetOrderDataView,PartnerRefundNotifyView,\ + CrossPlatformRefundView, GetCrossOrderDetailView, CrossPlatformForceCompleteView, CrossPlatformTransferHallView,\ + PartnerTransferHallNotifyView, GetSettingsView, UpdateSettingsView, GetMappingsView, UpdateMappingsView,\ + PartnerUpdateSelfConfigView, GetPartnerClubsView, GetStatsView, PunishDashouView, PartnerPunishNotifyView,\ + GetClubConfigForPreSettlementView, PreSettlementActionView, GetPreSettlementDataView, GetRolePermissionView,\ + ModifyRolePermissionView, AddRoleView, GetAdminRolesView, GetAdminUserListView, AddAdminUserView,\ + ModifyAdminUserView, KefuGetDashouListView, KefuGetDashouDetailView, KefuUpdateDashouView, KefuGetShangjiaListView,\ + KefuGetShangjiaDetailView, KefuUpdateShangjiaView, GetProductBaseDataView, GetProductListView, SaveProductOrderView,\ + AddProductView, DeleteProductView, GetProductDetailView, UpdateProductView, UpdateMemberView, GetMemberListView,\ + AddMemberView, GetGuanliListView, GetGuanliDetailView, UpdateGuanliView, GetWithdrawSettingsView,\ + UpdateWithdrawSettingsView, GetZuzhangListView, GetZuzhangDetailView, UpdateZuzhangView, GetProductTypeZoneView,\ + ModifyProductTypeZoneView, GetRateView, ModifyRateView, PopupNoticeListAPIView, PopupNoticeModifyAPIView,ShopListView, ShopModifyView, ShopPublicTypeAndAuditView, ShopListForProductView, ShopProductModifyView,\ + ShopProductTypeMappingView, ProductListView, ProductModifyView, KefuChatPermissionsView,\ + PartnerFineNotifyView,FineApplyView, FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanChuangJianView,\ + HqbkxxView,BkxgView,CaiwuView,CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView,\ + CwqtczhqView, KhpzhqView, ChzsgcView,KhgglView, ShgxgsjView, ZxkfghdsView, KptxwztjbView + +urlpatterns = [ + + path('kpthq', GetClubConfigView.as_view(), name='跨平台订单页面获取俱乐部数据'), + path('hqddlb', GetCrossOrderListView.as_view(), name='获取跨平台订单'), + path('kpthqtp',csrf_exempt(PartnerGetOrderDataView.as_view()), name='跨平台接受对方服务器获取订单数据'), + # 对方平台退款通知接口 + path('dfddtk', csrf_exempt(PartnerRefundNotifyView.as_view()), name='跨平台接受对方服务器更新订单状态'), + path('kptk', CrossPlatformRefundView.as_view(), name='跨平台订单退款'), + path('hqkptddxq', GetCrossOrderDetailView.as_view(), name='获取跨平台订单数据详情'), + path('kpqzjd', CrossPlatformForceCompleteView.as_view(), name='跨平台订单结算'), + path('kpzydt', CrossPlatformTransferHallView.as_view(), name='跨平台转移大厅'), + path('kpttzzydt', csrf_exempt(PartnerTransferHallNotifyView.as_view()), name='跨平台接受对方转移大厅通知'), + path('hqkptsz', GetSettingsView.as_view(), name='获取俱乐部数据信息'), + path('xgjlbsj', UpdateSettingsView.as_view(), name='更新俱乐部数据信息'), + path('hqqtjlbsj', GetMappingsView.as_view(), name='获取俱乐部订单类型映射'), + path('xgdfddys', UpdateMappingsView.as_view(), name='工薪俱乐部订单类型映射'), + path('xgzjsjcs', csrf_exempt(PartnerUpdateSelfConfigView.as_view()), name='partner_update_self_config'), + path('kpthqpt', GetPartnerClubsView.as_view(), name='前端获取俱乐部数据展示派发数据'), + path('hqpfsj', GetStatsView.as_view(), name='前端派发统计表获取'), + path('kpcf', PunishDashouView.as_view(), name='跨平台处罚打手'), + path('kptcfds', PartnerPunishNotifyView.as_view(), name='接受跨平台对方平台处罚申请'), + path('hqkttjsyjs', GetClubConfigForPreSettlementView.as_view(), name='预处理获取俱乐部数据'), + path('hqyjssj', GetPreSettlementDataView.as_view(), name='获取预处理数据'), + path('yjsjjhdk', PreSettlementActionView.as_view(), name='操作预处理'), + + path('gljshq', GetRolePermissionView.as_view(), name='获取就角色信息'), + path('sctjjsnr', ModifyRolePermissionView.as_view(), name='修改角色数据'), + path('tjjs', AddRoleView.as_view(), name='添加角色'), + + path('hqyhjsgl', GetAdminRolesView.as_view(), name='管理用户页面获取用户角色'), + path('hqjsyhsj', GetAdminUserListView.as_view(), name='管理用户页面获取用户数据'), + path('xghtyhsj', ModifyAdminUserView.as_view(), name='管理用户页面修改用户数据'), + path('tjyhjs', AddAdminUserView.as_view(), name='管理用户页面添加用户'), + path('kefuhqdslb', KefuGetDashouListView.as_view(), name='客服获取打手数据'), + path('kefuhqdsxq', KefuGetDashouDetailView.as_view(), name='客服获取打手具体信息'), + path('kefuxgds', KefuUpdateDashouView.as_view(), name='客服修改打手数据'), + path('hqsjgllb', KefuGetShangjiaListView.as_view(), name='后台获取商家列表'), + path('hthqsjxq', KefuGetShangjiaDetailView.as_view(), name='后台获取商家详情'), + path('xgsjxx', KefuUpdateShangjiaView.as_view(), name='后台修改商家数据'), + + path('htsphqjcsx', GetProductBaseDataView.as_view(), name='后台获取商品类型专区'), + path('hthqsplb', GetProductListView.as_view(), name='后台获取商品数据'), + path('htsppaixu', SaveProductOrderView.as_view(), name='后台修改商品类型排序'), + path('htfbsp', AddProductView.as_view(), name='后台添加商品'), + path('htscsp', DeleteProductView.as_view(), name='后台删除商品'), + path('htsphqxq', GetProductDetailView.as_view(), name='后台获取商品详情'), + path('bcspxg', UpdateProductView.as_view(), name='后台更新商品数据'), + + path('hthqhylb', GetMemberListView.as_view(), name='后台获取会员列表'), + path('htxghyxx', UpdateMemberView.as_view(), name='后台更新会员信息'), + path('httjhy', AddMemberView.as_view(), name='后台添加会员'), + path('hthqgslb', GetGuanliListView.as_view(), name='后台获取管事列表'), + + path('hthqgsxq', GetGuanliDetailView.as_view(), name='后台获取管事详情信息'), + path('htxggsxx', UpdateGuanliView.as_view(), name='后台更新管事数据'), + path('hthqtxpz', GetWithdrawSettingsView.as_view(), name='后台获取提现配置'), + path('htxgtxsz', UpdateWithdrawSettingsView.as_view(), name='后台修改提现配置'), + path('hthqzzlb', GetZuzhangListView.as_view(), name='后台获取组长列表'), + path('hthqzzxq', GetZuzhangDetailView.as_view(), name='后台获取组长详情页面'), + path('htxgzzxx', UpdateZuzhangView.as_view(), name='后台修改组长信息'), + + + path('hthqsplxzq', GetProductTypeZoneView.as_view(), name='后台获取商品类型专区'), + path('htxgsplx', ModifyProductTypeZoneView.as_view(), name='后台修改添加商品类型专区'), + path('hthqfhpz', GetRateView.as_view(), name='后台获取费率'), + path('htxgfl', ModifyRateView.as_view(), name='后台修改费率'), + path('hthqtcxx', PopupNoticeListAPIView.as_view(), name='后台获取弹窗配置'), + path('htxgtcxx', PopupNoticeModifyAPIView.as_view(), name='后台更新创建弹窗配置'), + + path('hqdplbsj', ShopListView.as_view(), name='获取店铺列表数据'), + path('xgdpxx', ShopModifyView.as_view(), name='修改或添加店铺数据'), + + + path('htdphqsplx', ShopPublicTypeAndAuditView.as_view(), name='后台获取店铺公共商品类型,默认审核配置'), + path('htdphqdpys', ShopListForProductView.as_view(), name='后台获取商品店铺'), + path('htdphqyssplx', ShopProductTypeMappingView.as_view(), name='后台获取商品店铺类型'), + path('htxgdpsplx', ShopProductModifyView.as_view(), name='后台修改店铺商品类型和默认审核模式'), + + path('hqhtdpsplx', ProductListView.as_view(), name='后台获取店铺商品数据'), + path('htxgdpspsj', ProductModifyView.as_view(), name='后台修改店铺商品数据'), + + path('kfhqltqx', KefuChatPermissionsView.as_view(), name='后台获取客服聊天配置'), + + path('kffkdssq', FineApplyView.as_view(), name='跨平台处罚罚款申请接口'), + path('kffkdssq_notify', PartnerFineNotifyView.as_view(), name='跨屏要对方平台处罚通知接口'), + + + path('htglyhqcfsltj', FaKuanTongJiView.as_view(),name='后台罚单统计图片'), # 罚款统计 + path('hthqfklb', FaKuanLieBiaoView.as_view(),name='后台罚单列表'), # 罚款列表 + path('glyclfk', FaKuanChuLiView.as_view(),name='后台罚单操作'), # 处理罚款 + path('htfksc', FaKuanChuangJianView.as_view(),name='后台生成罚单'), # 创建罚款 + + path('hqbkxx', HqbkxxView.as_view(), name='获取板块数据'), # 获取板块数据 + path('bkxg', BkxgView.as_view(), name='板块增删改查'), # 板块增删改 + path('caiwu', CaiwuView.as_view(), name='财务页面基础数据'), + + path('cwhybkhq', CwhybkhqView.as_view(), name='财务会员模块'), + path('hybkjtsj', HybkjtsjView.as_view(), name='财务会员统计'), + path('szxx', SzxxView.as_view(), name='详细收支记录'), + + path('cwddhqlx', CwddhqlxView.as_view(), name='后台财务获取订单类型'), + path('cwhqjtddsj', CwhqjtddsjView.as_view(), name='后台财务获取订单数据'), + path('cwqtczhq', CwqtczhqView.as_view(), name='财务获取其他充值统计'), + + path('khpzhq', KhpzhqView.as_view(), name='后台获取称号配置'), + path('chzsgc', ChzsgcView.as_view(), name='后台对称号增删改查'), + + path('khggl', KhgglView.as_view(), name='获取考核管信息'), + path('shgxgsj', ShgxgsjView.as_view(), name='修改考核管数据'), + + path('zxkfghds', ZxkfghdsView.as_view(), name='链接换打手或重新下单'), + + path('kptxwztjb', csrf_exempt(KptxwztjbView.as_view()), name='跨平台脚本询问订单状态'), + + + +] \ No newline at end of file diff --git a/houtai/utils.py b/houtai/utils.py new file mode 100644 index 0000000..95fc5bb --- /dev/null +++ b/houtai/utils.py @@ -0,0 +1,424 @@ +# houtai/utils.py +import logging +import json +from django.core.exceptions import ObjectDoesNotExist +from rest_framework.response import Response +from .models import AbnormalUserLog, UserRole, RolePermission, Permission, DashouRiTongji, ShangjiaRiTongji, \ + GuanshiXufeiRiTongji, GuanshiRiTongji, ZuzhangRiTongji, TixianRiTongji + +from django.db import transaction +from django.db.models import F +from datetime import date +from decimal import Decimal + +logger = logging.getLogger(__name__) + + +def _get_real_ip(request): + """获取真实IP(考虑代理)""" + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0].strip() + return ip, x_forwarded_for + else: + ip = request.META.get('REMOTE_ADDR') + return ip, '' + + +def _record_abnormal(account_id, request, attempt_type, detail=''): + """记录异常操作(仅用于越权等攻击行为)""" + try: + real_ip, xff = _get_real_ip(request) + headers = {k: v for k, v in request.META.items() if k.startswith('HTTP_')} + headers_json = json.dumps(headers, ensure_ascii=False)[:2000] + + request_params = json.dumps(request.query_params.dict(), ensure_ascii=False) if request.query_params else '' + request_body = request.body.decode('utf-8', errors='ignore')[:5000] + + AbnormalUserLog.objects.create( + account_id=account_id, + ip_address=real_ip, + real_ip=real_ip, + x_forwarded_for=xff, + user_agent=request.META.get('HTTP_USER_AGENT', ''), + request_method=request.method, + request_path=request.path, + request_params=request_params, + request_body=request_body, + request_headers=headers_json, + attempt_type=attempt_type, + detail=detail, + ) + logger.warning(f"记录异常用户: {account_id} IP:{real_ip} 类型:{attempt_type}") + except Exception as e: + logger.error(f"记录异常日志失败: {e}") + + +def verify_kefu_permission(request, username_from_frontend=None): + """ + 验证客服身份,并返回该用户的所有权限列表(基于新RBAC表) + 只有前端传递的username与当前登录用户不匹配时,才记录异常并警告前端。 + + 参数: + request: DRF request 对象 + username_from_frontend: 前端传递的账号(用于防越权) + + 返回: + (kefu_obj, permissions_list) # 验证通过 + (None, Response) # 验证失败,直接返回Response + """ + current_user = request.user + + # ---------- 1. 必须是客服 ---------- + if current_user.user_type != 'kefu': + return None, Response({'code': 403, 'msg': '身份错误,非客服账号'}, status=403) + + # ---------- 2. 客服扩展表存在且状态正常 ---------- + try: + kefu = current_user.kefu_profile + if kefu.zhuangtai != 1: + return None, Response({'code': 403, 'msg': '客服账号已被禁用'}, status=403) + except ObjectDoesNotExist: + return None, Response({'code': 403, 'msg': '客服账号不存在'}, status=403) + + # ---------- 3. 越权检测(只记录前端篡改username的情况) ---------- + if username_from_frontend and current_user.phone != username_from_frontend: + detail = f'前端username: {username_from_frontend},实际登录账号: {current_user.phone}' + _record_abnormal(current_user.phone, request, '越权尝试篡改username', detail=detail) + return None, Response({ + 'code': 403, + 'msg': '身份错误,无权操作其他账号,相关操作已记录' + }, status=403) + + # ---------- 4. 获取用户的所有权限(通过角色) ---------- + # 步骤1:获取用户的所有角色ID + role_ids = UserRole.objects.filter(account_id=current_user.phone).values_list('role_id', flat=True) + # 步骤2:通过角色ID获取权限编码 + permissions = list(Permission.objects.filter( + rolepermission__role_id__in=role_ids + ).values_list('perm_code', flat=True).distinct()) + + if not permissions: + logger.warning(f"用户 {current_user.phone} 没有任何权限,请检查角色分配") + + return kefu, permissions + + + + + +def update_dashou_daily_by_action(yonghuid, amount, action): + """ + 根据行为类型更新打手每日统计(接单、成交、退款) + + 参数: + yonghuid: 打手用户ID + amount: 本单涉及的金额(Decimal) + action: 1 = 接单, 2 = 成交(结算), 3 = 退款 + """ + amount = Decimal(str(amount)) if amount else Decimal('0.00') + if amount <= 0: + return # 金额非正不统计 + + today = date.today() + + with transaction.atomic(): + stat, created = DashouRiTongji.objects.select_for_update().get_or_create( + yonghuid=yonghuid, + riqi=today, + defaults={ + 'yonghuid': yonghuid, + 'riqi': today, + 'jiedan_zongliang': 0, + 'jiedan_zonge': Decimal('0.00'), + 'chengjiao_zongliang': 0, + 'chengjiao_zonge': Decimal('0.00'), + 'tuikuan_liang': 0, + 'tuikuan_jine': Decimal('0.00'), + } + ) + + update_fields = {} + if action == 1: # 接单 + update_fields['jiedan_zongliang'] = F('jiedan_zongliang') + 1 + update_fields['jiedan_zonge'] = F('jiedan_zonge') + amount + elif action == 2: # 成交 + update_fields['chengjiao_zongliang'] = F('chengjiao_zongliang') + 1 + update_fields['chengjiao_zonge'] = F('chengjiao_zonge') + amount + elif action == 3: # 退款 + update_fields['tuikuan_liang'] = F('tuikuan_liang') + 1 + update_fields['tuikuan_jine'] = F('tuikuan_jine') + amount + else: + return # 无效行为 + + if update_fields: + DashouRiTongji.objects.filter(id=stat.id).update(**update_fields) + + +def update_shangjia_daily(yonghuid, amount, action): + """ + 根据操作行为更新商家每日统计(派发、结算、退款) + + 参数: + yonghuid: 商家用户ID + amount: 当前订单涉及的金额(Decimal) + action: 操作类型,1 = 派发,2 = 结算(成交),3 = 退款 + + 说明: + - 金额必须大于0,否则不执行任何操作 + - 使用 select_for_update + F() 表达式保证原子性和并发安全 + - 每天每个商家只会有一条统计记录,不存在则自动创建 + """ + amount = Decimal(str(amount)) if amount else Decimal('0.00') + if amount <= 0: + return # 金额非正,不统计 + + today = date.today() + + with transaction.atomic(): + # 获取或创建当天统计记录,同时加锁防止并发 + stat, created = ShangjiaRiTongji.objects.select_for_update().get_or_create( + yonghuid=yonghuid, + riqi=today, + defaults={ + 'yonghuid': yonghuid, + 'riqi': today, + 'paifa_dingdan_shu': 0, + 'paifa_jine': Decimal('0.00'), + 'jiesuan_dingdan_shu': 0, + 'jiesuan_jine': Decimal('0.00'), + 'tuikuan_dingdan_shu': 0, + 'tuikuan_jine': Decimal('0.00'), + } + ) + + # 构建本次更新的字段,使用 F 表达式进行原子累加 + update_fields = {} + if action == 1: # 派发 + update_fields['paifa_dingdan_shu'] = F('paifa_dingdan_shu') + 1 + update_fields['paifa_jine'] = F('paifa_jine') + amount + elif action == 2: # 结算(成交) + update_fields['jiesuan_dingdan_shu'] = F('jiesuan_dingdan_shu') + 1 + update_fields['jiesuan_jine'] = F('jiesuan_jine') + amount + elif action == 3: # 退款 + update_fields['tuikuan_dingdan_shu'] = F('tuikuan_dingdan_shu') + 1 + update_fields['tuikuan_jine'] = F('tuikuan_jine') + amount + else: + # 无效的行为类型,直接返回 + return + + # 执行原子更新 + ShangjiaRiTongji.objects.filter(id=stat.id).update(**update_fields) + + + +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(): + stat, created = GuanshiRiTongji.objects.select_for_update().get_or_create( + yonghuid=yonghuid, + riqi=today, + defaults={ + 'yonghuid': yonghuid, + 'riqi': today, + 'yaoqing_dashou_shu': 0, + 'chongzhi_dashou_shu': 0, + 'shouru_zonge': Decimal('0.00'), + } + ) + + if action == 1: + # 邀请打手:无论新创建还是已存在,直接 +1 + if created: + stat.yaoqing_dashou_shu = 1 + stat.save() + else: + GuanshiRiTongji.objects.filter(id=stat.id).update( + yaoqing_dashou_shu=F('yaoqing_dashou_shu') + 1 + ) + + elif action == 2: + amount = Decimal(str(amount)) if amount else Decimal('0.00') + if amount <= 0: + return + if created: + stat.chongzhi_dashou_shu = 1 + stat.shouru_zonge = amount + stat.save() + else: + GuanshiRiTongji.objects.filter(id=stat.id).update( + chongzhi_dashou_shu=F('chongzhi_dashou_shu') + 1, + shouru_zonge=F('shouru_zonge') + amount + ) + + elif action in (3, 4): + amount = Decimal(str(amount)) if amount else Decimal('0.00') + if amount <= 0: + return + if created: + stat.shouru_zonge = amount + stat.save() + else: + GuanshiRiTongji.objects.filter(id=stat.id).update( + shouru_zonge=F('shouru_zonge') + amount + ) + +'''def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')): + """ + 根据操作行为更新管事每日统计(邀请打手、充值会员) + + 参数: + yonghuid: 管事用户ID + action: 操作类型 + 1 = 邀请打手(次数自动+1,无需传其他参数) + 2 = 充值会员(需要传 amount 参数) + amount: 充值金额(Decimal),仅在 action=2 时有效 + + 说明: + - 每次调用只代表一次操作,不会批量处理。 + - 使用 select_for_update + F() 保证原子性和并发安全。 + """ + today = date.today() + + # 邀请打手:次数+1,金额不变 + if action == 1: + count_field = 'yaoqing_dashou_shu' + amount = Decimal('0.00') + + # 充值会员:次数+1,金额累加 + elif action == 2: + amount = Decimal(str(amount)) if amount else Decimal('0.00') + if amount <= 0: + return + count_field = 'chongzhi_dashou_shu' + + else: + return # 无效行为,直接返回 + + with transaction.atomic(): + stat, created = GuanshiRiTongji.objects.select_for_update().get_or_create( + yonghuid=yonghuid, + riqi=today, + defaults={ + 'yonghuid': yonghuid, + 'riqi': today, + 'yaoqing_dashou_shu': 0, + 'chongzhi_dashou_shu': 0, + 'shouru_zonge': Decimal('0.00'), + } + ) + + if created: + # 新记录直接赋初值 + if action == 1: + stat.yaoqing_dashou_shu = 1 + else: + # 已存在记录,使用 F 表达式原子累加次数 + update_fields = {count_field: F(count_field) + 1} + + if action == 2: + update_fields['shouru_zonge'] = F('shouru_zonge') + amount + + GuanshiRiTongji.objects.filter(id=stat.id).update(**update_fields)''' + +def update_guanshi_xufei_daily(yonghuid, xufei_jine=Decimal('0.00')): + """ + 管事续费统计(单独表) + 用法: update_guanshi_xufei_daily('300001', Decimal('30')) + """ + today = date.today() + with transaction.atomic(): + stat, created = GuanshiXufeiRiTongji.objects.select_for_update().get_or_create( + yonghuid=yonghuid, + riqi=today, + defaults={ + 'yonghuid': yonghuid, + 'riqi': today, + 'xufei_zongshu': 1, + 'xufei_shouyi': xufei_jine, + } + ) + if not created: + GuanshiXufeiRiTongji.objects.filter(id=stat.id).update( + xufei_zongshu=F('xufei_zongshu') + 1, + xufei_shouyi=F('xufei_shouyi') + xufei_jine + ) + + +def update_zuzhang_daily_by_action(yonghuid, action, amount=Decimal('0.00')): + """ + 根据操作行为更新组长每日统计(邀请管事、分佣收入) + + 参数: + yonghuid: 组长用户ID + action: 操作类型 + 1 = 邀请管事(次数+1,无金额) + 2 = 分佣收入(累加金额) + amount: 分佣金额(Decimal),仅在 action=2 时有效 + """ + today = date.today() + + with transaction.atomic(): + stat, created = ZuzhangRiTongji.objects.select_for_update().get_or_create( + yonghuid=yonghuid, + riqi=today, + defaults={ + 'yonghuid': yonghuid, + 'riqi': today, + 'yaoqing_guanshi_shu': 0, + 'shouru_zonge': Decimal('0.00'), + 'fenyong_jine': Decimal('0.00'), + } + ) + + if created: + # 新记录直接设置初始值 + if action == 1: + stat.yaoqing_guanshi_shu = 1 + elif action == 2: + amount = Decimal(str(amount)) if amount else Decimal('0.00') + stat.shouru_zonge = amount + stat.fenyong_jine = amount + stat.save() + else: + # 已存在记录,使用 F 表达式原子累加 + if action == 1: + ZuzhangRiTongji.objects.filter(id=stat.id).update( + yaoqing_guanshi_shu=F('yaoqing_guanshi_shu') + 1 + ) + elif action == 2: + amount = Decimal(str(amount)) if amount else Decimal('0.00') + if amount > 0: + ZuzhangRiTongji.objects.filter(id=stat.id).update( + shouru_zonge=F('shouru_zonge') + amount, + fenyong_jine=F('fenyong_jine') + amount + ) + + + +def update_tixian_daily_stat(leixing, amount): + """ + 更新每日提现统计(平台维度) + leixing: 1=打手, 2=管事, 3=组长 + amount: 本次提现金额(申请金额,含手续费) + """ + today = date.today() + with transaction.atomic(): + stat, created = TixianRiTongji.objects.select_for_update().get_or_create( + riqi=today, + leixing=leixing, + defaults={'total_amount': amount} + ) + if not created: + TixianRiTongji.objects.filter(id=stat.id).update( + total_amount=F('total_amount') + amount + ) \ No newline at end of file diff --git a/houtai/view.py b/houtai/view.py new file mode 100644 index 0000000..8f7211b --- /dev/null +++ b/houtai/view.py @@ -0,0 +1,12489 @@ +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): + 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): + # 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): + 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): + # 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): + 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. 每日总限额 (leixing=4,5,6) + total_limit_map = {} + for leixing, total_code in [(1, 4), (2, 5), (3, 6)]: + obj = TixianQuotaDefault.objects.filter(leixing=total_code).first() + total_limit_map[leixing] = float(obj.default_quota) if obj else 0.0 + + # 4. 当日已提现总额 + today = date.today() + today_totals = {1: 0.0, 2: 0.0, 3: 0.0} + records = TixianRiTongji.objects.filter(riqi=today) + for rec in records: + 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'} + # 总限额对应的 leixing 值(4,5,6) + total_code_map = {1: 4, 2: 5, 3: 6} + + 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 \ + (str(role) in total_limits and total_limits[str(role)] is not None): + return Response({'code': 403, 'msg': f'无权限修改{["","打手","管事","组长"][role]}相关设置(需要{perm_needed})'}) + + # 其他提现手续费(审核官/打手押金/商家余额):需 5500a/b/c 任一 + for role in [4, 5, 6]: + if _rate_key_present(rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')): + role_names = {4: '审核官', 5: '打手押金', 6: '商家余额'} + return Response({'code': 403, 'msg': f'无权限修改{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. 修改每日总限额 + for role in [1, 2, 3]: + if str(role) in total_limits: + val = total_limits[str(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): + 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', 'xK9mP2vL8qW5rT7y') + if 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/manage.py b/manage.py new file mode 100644 index 0000000..df91779 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'a_long_dianjing.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/peizhi/__init__.py b/peizhi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/peizhi/admin.py b/peizhi/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/peizhi/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/peizhi/apps.py b/peizhi/apps.py new file mode 100644 index 0000000..f012e26 --- /dev/null +++ b/peizhi/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PeizhiConfig(AppConfig): + name = "peizhi" diff --git a/peizhi/migrations/0001_initial.py b/peizhi/migrations/0001_initial.py new file mode 100644 index 0000000..f5be730 --- /dev/null +++ b/peizhi/migrations/0001_initial.py @@ -0,0 +1,92 @@ +# Generated by Django 5.2.10 on 2026-01-14 19:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Gonggao', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('leixing', models.IntegerField(blank=True, null=True, verbose_name='公告类型')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')), + ('jieshao', models.TextField(blank=True, null=True, verbose_name='公告介绍')), + ], + options={ + 'verbose_name': '公告', + 'verbose_name_plural': '公告', + 'db_table': 'gonggao', + }, + ), + migrations.CreateModel( + name='Lunbo', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tupian_url', models.CharField(blank=True, max_length=500, null=True, verbose_name='轮播图片URL')), + ('leixing', models.IntegerField(blank=True, null=True, verbose_name='图片类型:1:轮播2:个人中心背景')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')), + ], + options={ + 'verbose_name': '轮播', + 'verbose_name_plural': '轮播', + 'db_table': 'lunbo', + }, + ), + migrations.CreateModel( + name='Qunpeizhi', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('peizhiid', models.IntegerField(blank=True, null=True, verbose_name='群类型:1.打手 2.管事')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')), + ('neirong', models.CharField(blank=True, max_length=200, null=True, verbose_name='群介绍')), + ('qunid', models.CharField(blank=True, max_length=100, null=True, verbose_name='群id')), + ('jieshao', models.CharField(blank=True, max_length=200, null=True, verbose_name='介绍')), + ], + options={ + 'db_table': 'qunpeizhi', + }, + ), + migrations.CreateModel( + name='Szjilu', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='记录ID')), + ('zongsy', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总收益')), + ('zongls', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总流水')), + ('zongzc', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总支出')), + ('jrzc', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日支出')), + ('jrls', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日流水')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')), + ], + options={ + 'verbose_name': '收支记录', + 'verbose_name_plural': '收支记录', + 'db_table': 'szjilu', + }, + ), + migrations.CreateModel( + name='Tupianpeizhi', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tupian', models.CharField(blank=True, max_length=500, null=True, verbose_name='配置图片URL')), + ('leixing', models.PositiveIntegerField(blank=True, null=True, verbose_name='提现类型1:打手规则图片2:默认头像图片')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')), + ], + options={ + 'verbose_name': '配置图片', + 'verbose_name_plural': '配置图片', + 'db_table': 'tupianpeizhi', + }, + ), + ] diff --git a/peizhi/migrations/0002_shangjiamoban_shangjialianjie.py b/peizhi/migrations/0002_shangjiamoban_shangjialianjie.py new file mode 100644 index 0000000..b1f0403 --- /dev/null +++ b/peizhi/migrations/0002_shangjiamoban_shangjialianjie.py @@ -0,0 +1,56 @@ +# Generated by Django 4.2.27 on 2026-01-27 12:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='ShangjiaMoban', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='模板ID')), + ('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')), + ('leixing_id', models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')), + ('moban_jieshao', models.CharField(max_length=500, verbose_name='模板介绍')), + ('jiage', models.DecimalField(decimal_places=2, help_text='单位:元', max_digits=10, verbose_name='模板价格')), + ('fabu_shuliang', models.PositiveIntegerField(default=0, verbose_name='发布数量')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '商家订单模板', + 'verbose_name_plural': '商家订单模板', + 'db_table': 'shangjia_moban', + 'indexes': [models.Index(fields=['yonghu_id', 'leixing_id'], name='shangjia_mo_yonghu__bb287d_idx'), models.Index(fields=['yonghu_id', 'leixing_id', 'id'], name='shangjia_mo_yonghu__4240d0_idx')], + 'unique_together': {('yonghu_id', 'leixing_id', 'moban_jieshao')}, + }, + ), + migrations.CreateModel( + name='ShangjiaLianjie', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='链接ID')), + ('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')), + ('leixing_id', models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')), + ('moban_id', models.PositiveIntegerField(db_index=True, verbose_name='模板ID')), + ('lianjie', models.CharField(max_length=500, unique=True, verbose_name='链接地址')), + ('dingdan_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='订单ID')), + ('is_used', models.BooleanField(db_index=True, default=False, verbose_name='是否已使用')), + ('shiyongzhe_ip', models.CharField(blank=True, max_length=50, null=True, verbose_name='使用者IP')), + ('youxiao_shijian', models.DateTimeField(blank=True, null=True, verbose_name='有效截止时间')), + ('lianjie_token', models.CharField(blank=True, db_index=True, max_length=32, null=True, unique=True, verbose_name='链接Token')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '商家链接', + 'verbose_name_plural': '商家链接', + 'db_table': 'shangjia_lianjie', + 'indexes': [models.Index(fields=['yonghu_id', 'moban_id', 'leixing_id'], name='shangjia_li_yonghu__9f2b25_idx'), models.Index(fields=['lianjie_token'], name='shangjia_li_lianjie_99014c_idx'), models.Index(fields=['dingdan_id'], name='shangjia_li_dingdan_95b682_idx')], + }, + ), + ] diff --git a/peizhi/migrations/0003_tupianpeizhi_fangwen_cishu.py b/peizhi/migrations/0003_tupianpeizhi_fangwen_cishu.py new file mode 100644 index 0000000..3538643 --- /dev/null +++ b/peizhi/migrations/0003_tupianpeizhi_fangwen_cishu.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-03-16 17:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0002_shangjiamoban_shangjialianjie'), + ] + + operations = [ + migrations.AddField( + model_name='tupianpeizhi', + name='fangwen_cishu', + field=models.PositiveIntegerField(default=0, verbose_name='访问次数'), + ), + ] diff --git a/peizhi/migrations/0004_tixianquotadefault.py b/peizhi/migrations/0004_tixianquotadefault.py new file mode 100644 index 0000000..4b2a1bd --- /dev/null +++ b/peizhi/migrations/0004_tixianquotadefault.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.27 on 2026-03-20 15:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0003_tupianpeizhi_fangwen_cishu'), + ] + + operations = [ + migrations.CreateModel( + name='TixianQuotaDefault', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('leixing', models.IntegerField(choices=[(1, '打手'), (2, '管事')], unique=True, verbose_name='用户类型')), + ('default_quota', models.DecimalField(decimal_places=2, default=20.0, max_digits=10, verbose_name='默认每日提现限额(元)')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '默认提现限额', + 'verbose_name_plural': '默认提现限额', + 'db_table': 'tixian_quota_default', + }, + ), + ] diff --git a/peizhi/migrations/0005_ordertypemapping_dailydispatchstat_club.py b/peizhi/migrations/0005_ordertypemapping_dailydispatchstat_club.py new file mode 100644 index 0000000..161236c --- /dev/null +++ b/peizhi/migrations/0005_ordertypemapping_dailydispatchstat_club.py @@ -0,0 +1,76 @@ +# Generated by Django 4.2.27 on 2026-03-24 17:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0004_tixianquotadefault'), + ] + + operations = [ + migrations.CreateModel( + name='OrderTypeMapping', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('partner_club_id', models.CharField(db_index=True, max_length=6, verbose_name='对方俱乐部ID')), + ('partner_type_id', models.PositiveIntegerField(verbose_name='对方订单类型ID')), + ('partner_type_name', models.CharField(max_length=100, verbose_name='对方订单类型名称')), + ('our_type_id', models.PositiveIntegerField(verbose_name='我方订单类型ID')), + ('our_type_name', models.CharField(max_length=100, verbose_name='我方订单类型名称')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '订单类型映射', + 'verbose_name_plural': '订单类型映射', + 'db_table': 'order_type_mapping', + 'unique_together': {('partner_club_id', 'partner_type_id')}, + }, + ), + migrations.CreateModel( + name='DailyDispatchStat', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(db_index=True, verbose_name='统计日期')), + ('year', models.PositiveSmallIntegerField(verbose_name='年份')), + ('month', models.PositiveSmallIntegerField(verbose_name='月份')), + ('day', models.PositiveSmallIntegerField(verbose_name='日期')), + ('direction', models.PositiveSmallIntegerField(choices=[(1, '我方派单'), (2, '对方派单')], verbose_name='派单方向')), + ('partner_club_id', models.CharField(db_index=True, max_length=6, verbose_name='对方俱乐部ID')), + ('dispatch_count', models.PositiveIntegerField(default=0, verbose_name='派单数')), + ('dispatch_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='派单总额')), + ('success_count', models.PositiveIntegerField(default=0, verbose_name='成交订单数')), + ('success_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='成交总额')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '每日派单统计', + 'verbose_name_plural': '每日派单统计', + 'db_table': 'daily_dispatch_stat', + 'indexes': [models.Index(fields=['year', 'month', 'day', 'direction', 'partner_club_id'], name='daily_dispa_year_f98f91_idx'), models.Index(fields=['date'], name='daily_dispa_date_9ac7fe_idx')], + }, + ), + migrations.CreateModel( + name='Club', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('club_id', models.CharField(db_index=True, max_length=6, verbose_name='本俱乐部ID')), + ('partner_club_id', models.CharField(max_length=6, unique=True, verbose_name='对方俱乐部ID')), + ('is_interop_enabled', models.BooleanField(default=False, verbose_name='是否开启平台互通')), + ('is_receive_enabled', models.BooleanField(default=False, verbose_name='是否同意接收对方订单')), + ('dashou_rate', models.DecimalField(decimal_places=4, default=1.0, max_digits=5, verbose_name='打手分红费率')), + ('partner_domain', models.URLField(blank=True, verbose_name='对方服务器域名')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '俱乐部', + 'verbose_name_plural': '俱乐部', + 'db_table': 'club', + 'indexes': [models.Index(fields=['club_id'], name='club_club_id_e75636_idx'), models.Index(fields=['partner_club_id'], name='club_partner_b76039_idx')], + }, + ), + ] diff --git a/peizhi/migrations/0006_clubconfig_accountpermission.py b/peizhi/migrations/0006_clubconfig_accountpermission.py new file mode 100644 index 0000000..6a5fd32 --- /dev/null +++ b/peizhi/migrations/0006_clubconfig_accountpermission.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.27 on 2026-04-01 09:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0005_ordertypemapping_dailydispatchstat_club'), + ] + + operations = [ + migrations.CreateModel( + name='ClubConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('club_id', models.CharField(db_index=True, max_length=6, verbose_name='俱乐部ID')), + ('club_nickname', models.CharField(blank=True, default='', max_length=50, verbose_name='俱乐部昵称')), + ('club_avatar', models.CharField(blank=True, default='', max_length=500, verbose_name='俱乐部头像地址')), + ('backend_domain', models.CharField(blank=True, default='', max_length=200, verbose_name='后台域名')), + ('chat_api_key', models.CharField(blank=True, default='', max_length=200, verbose_name='聊天API密钥')), + ('chat_api_id', models.CharField(blank=True, default='', max_length=200, verbose_name='聊天API ID')), + ('customer_service_api_url', models.CharField(blank=True, default='', max_length=200, verbose_name='客服API URL')), + ('customer_service_corp_id', models.CharField(blank=True, default='', max_length=100, verbose_name='客服企业ID')), + ('storage_bucket_domain', models.CharField(blank=True, default='', max_length=500, verbose_name='存储桶域名')), + ('is_self', models.SmallIntegerField(choices=[(0, '对方'), (1, '我方')], db_index=True, default=0, verbose_name='是否我方俱乐部')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '俱乐部配置', + 'verbose_name_plural': '俱乐部配置', + 'db_table': 'club_config', + 'unique_together': {('club_id',)}, + }, + ), + migrations.CreateModel( + name='AccountPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('account_id', models.CharField(db_index=True, max_length=11, verbose_name='账号ID')), + ('permission_code', models.IntegerField(verbose_name='权限代码')), + ('is_enabled', models.BooleanField(default=False, verbose_name='是否开启')), + ], + options={ + 'verbose_name': '账号权限', + 'verbose_name_plural': '账号权限', + 'db_table': 'account_permission', + 'unique_together': {('account_id', 'permission_code')}, + }, + ), + ] diff --git a/peizhi/migrations/0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more.py b/peizhi/migrations/0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more.py new file mode 100644 index 0000000..2290868 --- /dev/null +++ b/peizhi/migrations/0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.27 on 2026-04-02 13:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0006_clubconfig_accountpermission'), + ] + + operations = [ + migrations.AddField( + model_name='club', + name='receive_rate', + field=models.DecimalField(decimal_places=4, default=1.0, max_digits=5, verbose_name='接收打手分红费率'), + ), + migrations.AddField( + model_name='dailydispatchstat', + name='claimed_amount', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='抢单总额'), + ), + migrations.AddField( + model_name='dailydispatchstat', + name='claimed_count', + field=models.PositiveIntegerField(default=0, verbose_name='抢单数'), + ), + ] diff --git a/peizhi/migrations/0008_clubconfig_storage_bucket_name.py b/peizhi/migrations/0008_clubconfig_storage_bucket_name.py new file mode 100644 index 0000000..c7a0fbf --- /dev/null +++ b/peizhi/migrations/0008_clubconfig_storage_bucket_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-04-04 18:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='clubconfig', + name='storage_bucket_name', + field=models.CharField(blank=True, default='', max_length=100, verbose_name='存储桶名称'), + ), + ] diff --git a/peizhi/migrations/0009_dailypayoutstat_dailyincomestat.py b/peizhi/migrations/0009_dailypayoutstat_dailyincomestat.py new file mode 100644 index 0000000..47a2925 --- /dev/null +++ b/peizhi/migrations/0009_dailypayoutstat_dailyincomestat.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.27 on 2026-04-05 13:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0008_clubconfig_storage_bucket_name'), + ] + + operations = [ + migrations.CreateModel( + name='DailyPayoutStat', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(db_index=True, unique=True, verbose_name='统计日期')), + ('year', models.PositiveSmallIntegerField(verbose_name='年份')), + ('month', models.PositiveSmallIntegerField(verbose_name='月份')), + ('day', models.PositiveSmallIntegerField(verbose_name='日期')), + ('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='出款总额')), + ('total_count', models.PositiveIntegerField(default=0, verbose_name='出款总笔数')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '每日出款统计', + 'verbose_name_plural': '每日出款统计', + 'db_table': 'daily_payout_stat', + 'indexes': [models.Index(fields=['date'], name='daily_payou_date_b534ec_idx'), models.Index(fields=['year', 'month', 'day'], name='daily_payou_year_6376ca_idx')], + }, + ), + migrations.CreateModel( + name='DailyIncomeStat', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(db_index=True, unique=True, verbose_name='统计日期')), + ('year', models.PositiveSmallIntegerField(verbose_name='年份')), + ('month', models.PositiveSmallIntegerField(verbose_name='月份')), + ('day', models.PositiveSmallIntegerField(verbose_name='日期')), + ('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='总收入金额')), + ('total_count', models.PositiveIntegerField(default=0, verbose_name='总收入笔数')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '每日收入统计', + 'verbose_name_plural': '每日收入统计', + 'db_table': 'daily_income_stat', + 'indexes': [models.Index(fields=['date'], name='daily_incom_date_75e6df_idx'), models.Index(fields=['year', 'month', 'day'], name='daily_incom_year_d85d10_idx')], + }, + ), + ] diff --git a/peizhi/migrations/0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more.py b/peizhi/migrations/0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more.py new file mode 100644 index 0000000..ed347a7 --- /dev/null +++ b/peizhi/migrations/0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more.py @@ -0,0 +1,84 @@ +# Generated by Django 4.2.27 on 2026-04-22 11:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0009_dailypayoutstat_dailyincomestat'), + ] + + operations = [ + migrations.CreateModel( + name='PopupConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('popup_id', models.CharField(max_length=50, verbose_name='弹窗唯一ID')), + ('title', models.CharField(blank=True, max_length=200, verbose_name='标题(后台用)')), + ('description', models.TextField(blank=True, verbose_name='描述(后台用)')), + ('strategy_type', models.CharField(choices=[('once', '仅一次'), ('daily', '每日'), ('weekly', '每周'), ('monthly', '每月'), ('yearly', '每年'), ('always', '始终'), ('count', '周期内次数限制')], default='once', max_length=20)), + ('max_count', models.PositiveIntegerField(default=1, verbose_name='周期内最大展示次数')), + ('reset_interval', models.CharField(blank=True, choices=[('day', '天'), ('week', '周'), ('month', '月'), ('year', '年')], max_length=20, verbose_name='重置周期(count类型时使用)')), + ('duration', models.PositiveIntegerField(default=0, verbose_name='强制阅读秒数(0=无强制)')), + ('force_even_muted', models.BooleanField(default=False, verbose_name='强制弹出(忽略用户今日屏蔽)')), + ('sort_order', models.PositiveIntegerField(default=0, verbose_name='展示顺序')), + ('is_active', models.BooleanField(default=True, verbose_name='是否启用')), + ('start_time', models.DateTimeField(blank=True, null=True, verbose_name='生效开始时间')), + ('end_time', models.DateTimeField(blank=True, null=True, verbose_name='生效结束时间')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': '弹窗配置', + 'verbose_name_plural': '弹窗配置', + 'db_table': 'popup_config', + 'ordering': ['sort_order', 'id'], + }, + ), + migrations.CreateModel( + name='PopupPage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('page_key', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='页面标识')), + ('name', models.CharField(max_length=100, verbose_name='页面名称')), + ('description', models.TextField(blank=True, verbose_name='描述')), + ('is_active', models.BooleanField(default=True, verbose_name='是否启用')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('ignore_user_mute', models.BooleanField(default=False, verbose_name='忽略用户今日屏蔽')), + ], + options={ + 'verbose_name': '弹窗页面', + 'verbose_name_plural': '弹窗页面', + 'db_table': 'popup_page', + }, + ), + migrations.CreateModel( + name='PopupImage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image_url', models.CharField(max_length=500, verbose_name='图片相对路径')), + ('text', models.TextField(blank=True, verbose_name='图片上方文字')), + ('sort_order', models.PositiveIntegerField(default=0, verbose_name='展示顺序')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('popup_config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='peizhi.popupconfig')), + ], + options={ + 'verbose_name': '弹窗图片', + 'verbose_name_plural': '弹窗图片', + 'db_table': 'popup_image', + 'ordering': ['sort_order', 'id'], + }, + ), + migrations.AddField( + model_name='popupconfig', + name='page', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='popups', to='peizhi.popuppage'), + ), + migrations.AlterUniqueTogether( + name='popupconfig', + unique_together={('page', 'popup_id')}, + ), + ] diff --git a/peizhi/migrations/0011_withdrawconfig.py b/peizhi/migrations/0011_withdrawconfig.py new file mode 100644 index 0000000..1ef207e --- /dev/null +++ b/peizhi/migrations/0011_withdrawconfig.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.27 on 2026-04-25 12:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='WithdrawConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('mode', models.IntegerField(choices=[(1, '自动提现'), (2, '手动提现')], default=2, verbose_name='提现模式')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '提现模式配置', + 'verbose_name_plural': '提现模式配置', + 'db_table': 'withdraw_config', + }, + ), + ] diff --git a/peizhi/migrations/0012_shangjiamoban_chenghao_id_and_more.py b/peizhi/migrations/0012_shangjiamoban_chenghao_id_and_more.py new file mode 100644 index 0000000..6e6970d --- /dev/null +++ b/peizhi/migrations/0012_shangjiamoban_chenghao_id_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.27 on 2026-05-17 18:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0011_withdrawconfig'), + ] + + operations = [ + migrations.AddField( + model_name='shangjiamoban', + name='chenghao_id', + field=models.IntegerField(blank=True, null=True, verbose_name='关联称号ID'), + ), + migrations.AddField( + model_name='shangjiamoban', + name='yongjin_jiage', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='佣金价格'), + ), + migrations.AddField( + model_name='shangjiamoban', + name='yongjin_qiangdan', + field=models.BooleanField(default=False, verbose_name='是否开启佣金抢单'), + ), + ] diff --git a/peizhi/migrations/0013_shangjialianjie_previous_dingdan_id_and_more.py b/peizhi/migrations/0013_shangjialianjie_previous_dingdan_id_and_more.py new file mode 100644 index 0000000..3fedcbf --- /dev/null +++ b/peizhi/migrations/0013_shangjialianjie_previous_dingdan_id_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-05-26 03:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0012_shangjiamoban_chenghao_id_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='shangjialianjie', + name='previous_dingdan_id', + field=models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='上一个订单ID'), + ), + migrations.AddField( + model_name='shangjialianjie', + name='previous_lianjie_id', + field=models.PositiveIntegerField(blank=True, db_index=True, null=True, verbose_name='上一个链接ID'), + ), + ] diff --git a/peizhi/migrations/__init__.py b/peizhi/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/peizhi/models.py b/peizhi/models.py new file mode 100644 index 0000000..103b69e --- /dev/null +++ b/peizhi/models.py @@ -0,0 +1,598 @@ +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:个人中心背景') + + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间') + class Meta: + db_table = 'lunbo' + verbose_name = '轮播' + verbose_name_plural = '轮播' + # 不要索引!数据量太小 + + +class Gonggao(models.Model): + leixing = models.IntegerField(null=True, blank=True, verbose_name='公告类型') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间') + jieshao = models.TextField(null=True, blank=True, verbose_name='公告介绍') + class Meta: + db_table = 'gonggao' + verbose_name = '公告' + verbose_name_plural = '公告' + # 不要索引!数据量太小 + +class Qunpeizhi(models.Model): + peizhiid = models.IntegerField(null=True, blank=True, verbose_name='群类型:1.打手 2.管事') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间') + neirong = models.CharField(max_length=200, null=True, blank=True, verbose_name='群介绍') + qunid = models.CharField(max_length=100, null=True, blank=True, verbose_name='群id') + jieshao = models.CharField(max_length=200, null=True, blank=True, verbose_name='介绍') + class Meta: + db_table = 'qunpeizhi' + +class Tupianpeizhi(models.Model): + tupian = models.CharField(max_length=500, null=True, blank=True, verbose_name='配置图片URL') + leixing = models.PositiveIntegerField(null=True, blank=True, verbose_name='提现类型1:打手规则图片2:默认头像图片') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间') + fangwen_cishu = models.PositiveIntegerField(default=0, verbose_name='访问次数') # 新增字段 + class Meta: + db_table = 'tupianpeizhi' + verbose_name = '配置图片' + verbose_name_plural = '配置图片' + + +class Szjilu(models.Model): + id = models.AutoField(primary_key=True, verbose_name='记录ID') + zongsy = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总收益') + zongls = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总流水') + zongzc = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总支出') + jrzc = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日支出') + jrls = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日流水') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间') + class Meta: + db_table = 'szjilu' + verbose_name = '收支记录' + verbose_name_plural = '收支记录' + + + + + + + +# models/shangjia_moban.py +from django.db import models + + +class ShangjiaMoban(models.Model): + """商家订单模板表 - 存储商家创建的订单模板""" + + # 主键ID(自动递增) + id = models.AutoField(primary_key=True, verbose_name='模板ID') + + # 关联字段 + yonghu_id = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') + leixing_id = models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID') + + # 模板内容 + + moban_jieshao = models.CharField(max_length=500, verbose_name='模板介绍') + jiage = models.DecimalField( + max_digits=10, + decimal_places=2, + verbose_name='模板价格', + help_text='单位:元' + ) + + # 统计字段 + fabu_shuliang = models.PositiveIntegerField(default=0, verbose_name='发布数量') + + yongjin_qiangdan = models.BooleanField(default=False, verbose_name='是否开启佣金抢单') + yongjin_jiage = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='佣金价格') + chenghao_id = models.IntegerField(null=True, blank=True, verbose_name='关联称号ID') + + + # 时间字段 + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'shangjia_moban' + verbose_name = '商家订单模板' + verbose_name_plural = verbose_name + indexes = [ + # 高频查询组合索引 + models.Index(fields=['yonghu_id', 'leixing_id']), + models.Index(fields=['yonghu_id', 'leixing_id','id']), + + ] + # 同一个商家同一类型下模板介绍不能重复(允许修改) + unique_together = ['yonghu_id', 'leixing_id', 'moban_jieshao'] + + def __str__(self): + return f"模板{self.id}-{self.yonghu_id}" + + +# models/shangjia_lianjie.py +from django.db import models + + +class ShangjiaLianjie(models.Model): + """商家链接存储表 - 存储生成的派单链接""" + + # 主键ID(自动递增) + id = models.AutoField(primary_key=True, verbose_name='链接ID') + + # 关联字段 + yonghu_id = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') + leixing_id = models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID') + moban_id = models.PositiveIntegerField(db_index=True, verbose_name='模板ID') + + # 链接核心数据 + lianjie = models.CharField(max_length=500, unique=True, verbose_name='链接地址') + #lianjie_token = models.CharField(max_length=32, unique=True, db_index=True, verbose_name='链接Token') + + # 订单关联(生成订单后更新) + dingdan_id = models.CharField(max_length=32, null=True, blank=True, db_index=True, verbose_name='订单ID') + + # 使用状态 + is_used = models.BooleanField(default=False, verbose_name='是否已使用', db_index=True) + #shiyong_time = models.DateTimeField(null=True, blank=True, verbose_name='使用时间') + + # 使用者信息 + shiyongzhe_ip = models.CharField(max_length=50, null=True, blank=True, verbose_name='使用者IP') + #shiyongzhe_info = models.TextField(null=True, blank=True, verbose_name='使用者信息') + + # 链接控制 + #shiyong_cishu = models.PositiveIntegerField(default=1, verbose_name='可使用次数') + #yixiang_cishu = models.PositiveIntegerField(default=0, verbose_name='已使用次数') + youxiao_shijian = models.DateTimeField(null=True, blank=True, verbose_name='有效截止时间') + + # 安全字段 + #anquan_ma = models.CharField(max_length=100, null=True, blank=True, verbose_name='安全码') + # 新增 Token 字段 + lianjie_token = models.CharField( + max_length=32, + unique=True, + db_index=True, + verbose_name='链接Token', + null=True, + blank=True # 先设置为可选,后面再设为必填 + ) + + # 时间字段 + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + # 在 ShangjiaLianjie 类中添加 + + # 上一个链接的ID(可选,记录订单被更换前的链接) + previous_lianjie_id = models.PositiveIntegerField( + null=True, + blank=True, + db_index=True, + verbose_name='上一个链接ID' + ) + + # 上一个订单的ID(可选,记录被更换的旧订单ID) + previous_dingdan_id = models.CharField( + max_length=32, + null=True, + blank=True, + db_index=True, + verbose_name='上一个订单ID' + ) + + class Meta: + db_table = 'shangjia_lianjie' + verbose_name = '商家链接' + verbose_name_plural = verbose_name + indexes = [ + # 高频查询组合索引 + models.Index(fields=['yonghu_id', 'moban_id','leixing_id']), + models.Index(fields=['lianjie_token']), + #models.Index(fields=['lianjie_token']), + models.Index(fields=['dingdan_id']), + #models.Index(fields=['moban_id']), + #models.Index(fields=['create_time']), + #models.Index(fields=['youxiao_shijian', 'is_used']), + ] + #ordering = ['-create_time'] + + def __str__(self): + return f"链接{self.id}-{self.yonghu_id}" + + + + + +class TixianQuotaDefault(models.Model): + """ + 默认每日提现限额表 + """ + id = models.AutoField(primary_key=True) + leixing = models.IntegerField( + choices=[(1, '打手'), (2, '管事')], + unique=True, + verbose_name='用户类型' + ) + default_quota = models.DecimalField( + max_digits=10, + decimal_places=2, + default=20.00, + verbose_name='默认每日提现限额(元)' + ) + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'tixian_quota_default' + verbose_name = '默认提现限额' + verbose_name_plural = verbose_name + + def __str__(self): + return f"{self.get_leixing_display()} 默认限额: {self.default_quota}" + + + + +class WithdrawConfig(models.Model): + """ + 提现模式配置表 + - mode = 1: 自动提现(前端展示收款码模式) + - mode = 2: 手动提现(前端展示微信零钱模式) + 默认值为 2(手动打款),没有记录时前端也按默认 2 处理 + """ + MODE_CHOICES = ( + (1, '自动提现'), # 对应前端模式一 + (2, '手动提现'), # 对应前端模式二 + ) + + mode = models.IntegerField( + choices=MODE_CHOICES, + default=2, + verbose_name='提现模式' + ) + created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + updated_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'withdraw_config' + verbose_name = '提现模式配置' + verbose_name_plural = verbose_name + + def __str__(self): + return f'提现模式: {"自动" if self.mode == 1 else "手动"}' + + + + + +# apps/club/models.py +from django.db import models + +class Club(models.Model): + """合作俱乐部/平台信息表""" + # 自己的俱乐部ID(6位字符) + club_id = models.CharField(max_length=6, db_index=True, verbose_name='本俱乐部ID') + # 对方的俱乐部ID(6位字符) + partner_club_id = models.CharField(max_length=6, unique= True,verbose_name='对方俱乐部ID') + # 是否开启平台互通(双方互相同意才可传递订单) + is_interop_enabled = models.BooleanField(default=False, verbose_name='是否开启平台互通') + # 是否同意接收对方订单(对方同意我方发单,但接收订单需我方同意) + is_receive_enabled = models.BooleanField(default=False, verbose_name='是否同意接收对方订单') + # 打手分红费率(默认100%,即原金额,对方平台打手分成 = 我方打手分成 × 费率) + dashou_rate = models.DecimalField(max_digits=5, decimal_places=4, default=1.0000, verbose_name='打手分红费率') + # 新增字段:接收对方订单时打手分红费率(默认100%) + receive_rate = models.DecimalField(max_digits=5, decimal_places=4, default=1.0000, verbose_name='接收打手分红费率') + + + # 对方服务器域名(完整 URL,如 https://api.partner.com) + partner_domain = models.URLField(max_length=200, blank=True, verbose_name='对方服务器域名') + + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'club' + verbose_name = '俱乐部' + verbose_name_plural = '俱乐部' + indexes = [ + models.Index(fields=['club_id']), + models.Index(fields=['partner_club_id']), + ] + + +class OrderTypeMapping(models.Model): + """对方订单类型 → 我方订单类型 映射表""" + partner_club_id = models.CharField(max_length=6, db_index=True, verbose_name='对方俱乐部ID') + partner_type_id = models.PositiveIntegerField(verbose_name='对方订单类型ID') + partner_type_name = models.CharField(max_length=100, verbose_name='对方订单类型名称') + our_type_id = models.PositiveIntegerField(verbose_name='我方订单类型ID') + our_type_name = models.CharField(max_length=100, verbose_name='我方订单类型名称') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'order_type_mapping' + verbose_name = '订单类型映射' + verbose_name_plural = '订单类型映射' + unique_together = (('partner_club_id', 'partner_type_id'),) + + +class ClubConfig(models.Model): + """ + 俱乐部配置信息表 + 存储俱乐部(我方或对方)的详细配置,如昵称、头像、API密钥等 + """ + # 俱乐部ID(6位字符,与 Club 表中的 club_id 或 partner_club_id 对应) + club_id = models.CharField(max_length=6, db_index=True, verbose_name='俱乐部ID') + + # 俱乐部昵称 + club_nickname = models.CharField(max_length=50, blank=True, default='', verbose_name='俱乐部昵称') + + # 俱乐部头像地址(URL) + club_avatar = models.CharField(max_length=500, blank=True, default='', verbose_name='俱乐部头像地址') + + # 俱乐部后台域名(用于API请求) + backend_domain = models.CharField(max_length=200, blank=True, default='', verbose_name='后台域名') + + # 聊天API密钥 + chat_api_key = models.CharField(max_length=200, blank=True, default='', verbose_name='聊天API密钥') + + # 聊天API ID + chat_api_id = models.CharField(max_length=200, blank=True, default='', verbose_name='聊天API ID') + + # 客服路径 + customer_service_api_url = models.CharField(max_length=200, blank=True, default='', verbose_name='客服API URL') + customer_service_corp_id = models.CharField(max_length=100, blank=True, default='', verbose_name='客服企业ID') + + # 存储桶域名(如腾讯云COS、阿里云OSS的访问域名) + storage_bucket_domain = models.CharField(max_length=500, blank=True, default='', verbose_name='存储桶域名') + storage_bucket_name = models.CharField(max_length=100, blank=True, default='', verbose_name='存储桶名称') + + # 标识是我方还是对方俱乐部:1=我方,0=对方(默认0) + is_self = models.SmallIntegerField( + choices=((0, '对方'), (1, '我方')), + default=0, + db_index=True, + verbose_name='是否我方俱乐部' + ) + + # 创建时间 + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + # 更新时间 + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'club_config' + verbose_name = '俱乐部配置' + verbose_name_plural = '俱乐部配置' + # 联合唯一约束:同一个俱乐部ID只能有一条记录(无论我方对方),若需要分开则可不加 + # 但通常一个俱乐部只会存在一种身份,所以加联合唯一约束更安全 + unique_together = [['club_id']] # 确保每个club_id只有一条配置记录 + + def __str__(self): + 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') + permission_code = models.IntegerField(verbose_name='权限代码') + is_enabled = models.BooleanField(default=False, verbose_name='是否开启') + + class Meta: + db_table = 'account_permission' + verbose_name = '账号权限' + verbose_name_plural = '账号权限' + unique_together = [['account_id', 'permission_code']] # 一个账号的同一权限只能有一条记录 + + def __str__(self): + return f"{self.account_id} 权限{self.permission_code}: {'开启' if self.is_enabled else '关闭'}" + + + + + + +# apps/dingdan/models.py (添加新模型) +class DailyDispatchStat(models.Model): + """ + 每日派单统计表 + 记录每天向各合作俱乐部派单的数据,用于替代俱乐部表中的累计字段。 + """ + # 日期相关字段(便于快速筛选) + date = models.DateField(verbose_name='统计日期', db_index=True) + year = models.PositiveSmallIntegerField(verbose_name='年份') + month = models.PositiveSmallIntegerField(verbose_name='月份') + day = models.PositiveSmallIntegerField(verbose_name='日期') + # 派单方向:1=我方派单(我方订单被对方抢单),2=对方派单(对方订单被我方抢单) + direction = models.PositiveSmallIntegerField(choices=[(1, '我方派单'), (2, '对方派单')], verbose_name='派单方向') + # 对方俱乐部ID + partner_club_id = models.CharField(max_length=6, db_index=True, verbose_name='对方俱乐部ID') + # 派单数 + dispatch_count = models.PositiveIntegerField(default=0, verbose_name='派单数') + # 派单总额(打手分成 * 费率) + dispatch_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='派单总额') + # 抢单统计(我方实际抢到的对方派单数/金额) + claimed_count = models.PositiveIntegerField(default=0, verbose_name='抢单数') + claimed_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='抢单总额') + # 成交订单数(预留,当前暂不更新) + success_count = models.PositiveIntegerField(default=0, verbose_name='成交订单数') + # 成交总额(预留) + success_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='成交总额') + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'daily_dispatch_stat' + verbose_name = '每日派单统计' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['year', 'month', 'day', 'direction', 'partner_club_id']), + models.Index(fields=['date']), + ] + + def __str__(self): + return f"{self.date} - 方向{self.direction} - 俱乐部{self.partner_club_id} - 派单{self.dispatch_count}次" + + + + + +# apps/dingdan/models.py (新增模型) + +class DailyIncomeStat(models.Model): + """ + 每日收入统计表 + 记录平台每天通过微信支付获得的总收入和总笔数 + """ + # 日期相关字段(便于快速筛选和分组) + date = models.DateField(verbose_name='统计日期', db_index=True, unique=True) + year = models.PositiveSmallIntegerField(verbose_name='年份') + month = models.PositiveSmallIntegerField(verbose_name='月份') + day = models.PositiveSmallIntegerField(verbose_name='日期') + + # 收入统计 + total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='总收入金额') + total_count = models.PositiveIntegerField(default=0, verbose_name='总收入笔数') + + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'daily_income_stat' + verbose_name = '每日收入统计' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['date']), + models.Index(fields=['year', 'month', 'day']), + ] + + def __str__(self): + return f"{self.date} - 收入{self.total_amount}元 - {self.total_count}笔" + + +# apps/dingdan/models.py + +class DailyPayoutStat(models.Model): + """ + 每日出款统计表 + 记录平台每天通过提现/结算等方式的出款总额和总笔数 + """ + date = models.DateField(verbose_name='统计日期', db_index=True, unique=True) + year = models.PositiveSmallIntegerField(verbose_name='年份') + month = models.PositiveSmallIntegerField(verbose_name='月份') + day = models.PositiveSmallIntegerField(verbose_name='日期') + + total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='出款总额') + total_count = models.PositiveIntegerField(default=0, verbose_name='出款总笔数') + + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'daily_payout_stat' + verbose_name = '每日出款统计' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['date']), + models.Index(fields=['year', 'month', 'day']), + ] + + def __str__(self): + return f"{self.date} - 出款{self.total_amount}元 - {self.total_count}笔" + + + + + + + + +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='页面名称') + description = models.TextField(blank=True, verbose_name='描述') + is_active = models.BooleanField(default=True, verbose_name='是否启用') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + ignore_user_mute = models.BooleanField(default=False, verbose_name='忽略用户今日屏蔽') + + class Meta: + db_table = 'popup_page' + verbose_name = '弹窗页面' + verbose_name_plural = '弹窗页面' + + def __str__(self): + return f'{self.name} ({self.page_key})' + + +class PopupConfig(models.Model): + STRATEGY_CHOICES = ( + ('once', '仅一次'), + ('daily', '每日'), + ('weekly', '每周'), + ('monthly', '每月'), + ('yearly', '每年'), + ('always', '始终'), + ('count', '周期内次数限制'), + ) + page = models.ForeignKey(PopupPage, on_delete=models.CASCADE, related_name='popups') + popup_id = models.CharField(max_length=50, verbose_name='弹窗唯一ID') + title = models.CharField(max_length=200, blank=True, verbose_name='标题(后台用)') + description = models.TextField(blank=True, verbose_name='描述(后台用)') + strategy_type = models.CharField(max_length=20, choices=STRATEGY_CHOICES, default='once') + max_count = models.PositiveIntegerField(default=1, verbose_name='周期内最大展示次数') + reset_interval = models.CharField(max_length=20, blank=True, + choices=(('day','天'),('week','周'),('month','月'),('year','年')), + verbose_name='重置周期(count类型时使用)') + duration = models.PositiveIntegerField(default=0, verbose_name='强制阅读秒数(0=无强制)') + force_even_muted = models.BooleanField(default=False, verbose_name='强制弹出(忽略用户今日屏蔽)') + sort_order = models.PositiveIntegerField(default=0, verbose_name='展示顺序') + is_active = models.BooleanField(default=True, verbose_name='是否启用') + start_time = models.DateTimeField(null=True, blank=True, verbose_name='生效开始时间') + end_time = models.DateTimeField(null=True, blank=True, verbose_name='生效结束时间') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'popup_config' + verbose_name = '弹窗配置' + verbose_name_plural = '弹窗配置' + unique_together = ('page', 'popup_id') + ordering = ['sort_order', 'id'] + + def __str__(self): + return f'{self.page.name} - {self.popup_id}' + + +class PopupImage(models.Model): + popup_config = models.ForeignKey(PopupConfig, on_delete=models.CASCADE, related_name='images') + image_url = models.CharField(max_length=500, verbose_name='图片相对路径') + text = models.TextField(blank=True, verbose_name='图片上方文字') + sort_order = models.PositiveIntegerField(default=0, verbose_name='展示顺序') + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = 'popup_image' + verbose_name = '弹窗图片' + verbose_name_plural = '弹窗图片' + ordering = ['sort_order', 'id'] + + def __str__(self): + return f'{self.popup_config.popup_id} - 图片{self.sort_order}' \ No newline at end of file diff --git a/peizhi/serializers.py b/peizhi/serializers.py new file mode 100644 index 0000000..b53a33a --- /dev/null +++ b/peizhi/serializers.py @@ -0,0 +1,32 @@ +#微信小程序弹窗组件 +from rest_framework import serializers +from .models import PopupImage, PopupConfig + + +class PopupImageSerializer(serializers.ModelSerializer): + class Meta: + model = PopupImage + fields = ['url', 'text'] # 注意:前端需要 url 字段,我们序列化时映射 image_url -> url + # 手动处理字段名映射 + + url = serializers.CharField(source='image_url') + + +class PopupConfigSerializer(serializers.ModelSerializer): + images = PopupImageSerializer(many=True, read_only=True) + strategy = serializers.SerializerMethodField() + + class Meta: + model = PopupConfig + fields = ['popup_id', 'title', 'duration', 'force_even_muted', 'images', 'strategy'] + + + def get_strategy(self, obj): + # 组装前端需要的 strategy 对象 + strategy = { + 'type': obj.strategy_type, + 'maxCount': obj.max_count, + } + if obj.reset_interval: + strategy['resetInterval'] = obj.reset_interval + return strategy \ No newline at end of file diff --git a/peizhi/tasks.py b/peizhi/tasks.py new file mode 100644 index 0000000..f49f474 --- /dev/null +++ b/peizhi/tasks.py @@ -0,0 +1,86 @@ +""" +阿龙电竞 - 收支记录定时任务 +处理收支记录表的每日清零 +固定使用ID=1的记录 +""" + +from celery import shared_task +from django.db import transaction +from django.utils import timezone +from datetime import datetime +from decimal import Decimal +import logging + +# 导入模型 +from peizhi.models import Szjilu +from utils.celery_utils import log_task_execution, rollback_on_failure + +logger = logging.getLogger(__name__) + +@shared_task +@rollback_on_failure("收支记录每日清零") +def daily_sz_reset_task(): + """ + 收支记录每日清零 - 每天凌晨0点5分执行 + 清零收支记录表的今日相关字段,固定使用ID=1的记录 + """ + try: + with transaction.atomic(): + # 获取今天的日期 + today = datetime.now().strftime('%Y-%m-%d') + + logger.info(f"开始执行收支记录清零任务,日期: {today}") + + # 查询ID=1的收支记录,如果不存在则创建 + try: + szjilu = Szjilu.objects.get(id=1) + logger.info(f"找到ID=1的收支记录") + except Szjilu.DoesNotExist: + # 创建ID=1的初始记录 + szjilu = Szjilu.objects.create( + id=1, + zongsy=Decimal('0.00'), + zongls=Decimal('0.00'), + zongzc=Decimal('0.00'), + jrzc=Decimal('0.00'), + jrls=Decimal('0.00') + ) + logger.info(f"创建ID=1的收支记录") + + # 记录清零前的值(用于日志) + old_jrzc = szjilu.jrzc + old_jrls = szjilu.jrls + + # 清零今日字段 + szjilu.jrzc = Decimal('0.00') + szjilu.jrls = Decimal('0.00') + szjilu.save(update_fields=['jrzc', 'jrls', 'update_time']) + + logger.info(f"收支记录清零完成,今日支出原值: {old_jrzc},今日流水原值: {old_jrls}") + + result_msg = f"收支记录清零完成 - 日期: {today}, 记录ID: {szjilu.id}" + + log_task_execution("收支记录每日清零", True, result_msg) + + return { + "success": True, + "date": today, + "record_id": szjilu.id, + "jrzc_cleared": True, + "jrls_cleared": True, + "old_jrzc": str(old_jrzc), + "old_jrls": str(old_jrls), + "message": result_msg + } + + except Exception as e: + error_msg = f"收支记录每日清零失败: {str(e)}" + logger.error(error_msg) + log_task_execution("收支记录每日清零", False, error_msg) + + return { + "success": False, + "error": str(e), + "date": datetime.now().strftime('%Y-%m-%d'), + "message": error_msg + } \ No newline at end of file diff --git a/peizhi/tests.py b/peizhi/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/peizhi/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/peizhi/urls.py b/peizhi/urls.py new file mode 100644 index 0000000..3dc61e6 --- /dev/null +++ b/peizhi/urls.py @@ -0,0 +1,40 @@ +# peizhi/urls.py +from django.urls import path +from .views import ShangpinGonggaoView, AdminUpdateConfigView, AdminUploadImageView, AdminConfigQueryView, \ + DashouPaihangView,GetAdminZhanghaoView, GetXiugaiJiluView,ShangjiaTianjiaMobanView, ShangjiaMobanListView, \ + ShangjiaGengxinMobanView, ShangjiaShanchuMobanView, ShangjiaGenerateLinkView,KehuGetDingdanLianjieView, \ + KehuTianxieDingdanView,GuanZhuAListView, ZuzhangHaibaoView,GuanshiQRCodeView,\ + GetDynamicConfigView, PopupConfigView, GetWithdrawModeView,CheckPhoneAuthView,\ + ShangjiaLianjieListView + +urlpatterns = [ + path('shangpingonggao/', ShangpinGonggaoView.as_view(), name='商品公告轮播获取'), + path('adpzhq', AdminConfigQueryView.as_view(), name='管理员获取配置'), + path('adxgtp', AdminUploadImageView.as_view(), name='管理员更新配置图片'), + path('adupdate', AdminUpdateConfigView.as_view(), name='管理员更新群和利禄'), + path('dsph', DashouPaihangView.as_view(), name='排行数据获取'), + path('adzhhq', GetAdminZhanghaoView.as_view(), name='管理员查询操作记录 账号'), + path('adhqxg', GetXiugaiJiluView.as_view(), name='管理员铲鲟操作记录数据'), + + path('sjtjddmb', ShangjiaTianjiaMobanView.as_view(), name='商家添加订单模板'), + path('sjddmblb', ShangjiaMobanListView.as_view(), name='商家获取订单模板'), + path('sjddmbgx', ShangjiaGengxinMobanView.as_view(), name='商家修改订单模板'), + path('sjddmbsc', ShangjiaShanchuMobanView.as_view(), name='商家删除订单模板'), + # 商家生成链接接口 + path('sjljhq', ShangjiaGenerateLinkView.as_view(), name='商家生成链接'), + # 客户获取订单链接信息 + path('khhqddlj', KehuGetDingdanLianjieView.as_view(), name='客户获取订单链接数据'), + + # 客户填写订单信息 + path('kehutx', KehuTianxieDingdanView.as_view(), name='客户修改订单链接'), + path('gzal', GuanZhuAListView.as_view(), name='关注阿龙'), + path('guanshiewm', GuanshiQRCodeView.as_view(), name='管事生成二维码'), + path('zuzhanghb', ZuzhangHaibaoView.as_view(), name='组长二维码'), + path('qdpzhq', GetDynamicConfigView.as_view(), name='小程序获取配置'), + + path('tanchuang', PopupConfigView.as_view(), name='微信小程序获取弹窗数据'), + path('hqtxzsym', GetWithdrawModeView.as_view(), name='小程序获取提现方式'), + + 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/peizhi/views.py new file mode 100644 index 0000000..f213cf4 --- /dev/null +++ b/peizhi/views.py @@ -0,0 +1,5563 @@ +# 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 + return boss.zonge >= 20 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0563dae --- /dev/null +++ b/requirements.txt @@ -0,0 +1,43 @@ +amqp==5.3.1 +asgiref==3.11.0 +billiard==4.2.4 +celery==5.3.1 +certifi==2025.11.12 +charset-normalizer==3.4.4 +click==8.3.1 +click-didyoumean==0.3.1 +click-plugins==1.1.1.2 +click-repl==0.3.0 +cos_python_sdk_v5==1.9.40 +crcmod==1.7 +cron_descriptor==2.0.6 +Django==4.2.27 +django-celery-beat==2.5.0 +django-celery-results==2.5.0 +django-cors-headers==4.4.0 +django-extensions==3.2.3 +django-timezone-field==7.2.1 +djangorestframework==3.15.2 +djangorestframework-simplejwt==5.3.1 +hashids==1.3.1 +idna==3.11 +kombu==5.6.1 +mysqlclient==2.2.7 +packaging==25.0 +pillow==12.0.0 +pycryptodome==3.20.0 +PyJWT==2.10.1 +python-crontab==3.3.0 +python-dateutil==2.9.0.post0 +python-decouple==3.8 +redis==4.5.4 +requests==2.32.5 +six==1.17.0 +sqlparse==0.5.4 +sts==0.0.1 +tencentcloud-sdk-python==3.1.24 +typing_extensions==4.15.0 +tzdata==2025.3 +urllib3==2.6.2 +vine==5.1.0 +xmltodict==1.0.2 \ No newline at end of file diff --git a/shangdian/__init__.py b/shangdian/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shangdian/admin.py b/shangdian/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/shangdian/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/shangdian/apps.py b/shangdian/apps.py new file mode 100644 index 0000000..5be5e60 --- /dev/null +++ b/shangdian/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ShangdianConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'shangdian' diff --git a/shangdian/migrations/0001_initial.py b/shangdian/migrations/0001_initial.py new file mode 100644 index 0000000..6077a4d --- /dev/null +++ b/shangdian/migrations/0001_initial.py @@ -0,0 +1,92 @@ +# Generated by Django 4.2.27 on 2026-04-21 12:36 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('shangpin', '0010_shangpinleixing_is_cross_enabled'), + ] + + operations = [ + migrations.CreateModel( + name='YonghuPingzheng', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='自增主键ID')), + ('zhanghao', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='登录账号')), + ('mima', models.CharField(max_length=255, verbose_name='密码')), + ('is_active', models.BooleanField(default=True, verbose_name='是否启用')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('yonghu', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, to_field='yonghuid', verbose_name='关联用户ID')), + ], + options={ + 'verbose_name': '用户认证凭证', + 'verbose_name_plural': '用户认证凭证', + 'db_table': 'yonghu_pingzheng', + }, + ), + migrations.CreateModel( + name='Dianpu', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='店铺自增ID')), + ('dianpu_mingcheng', models.CharField(max_length=50, verbose_name='店铺名称')), + ('dianpu_touxiang', models.CharField(blank=True, max_length=500, null=True, verbose_name='店铺头像URL')), + ('lianxi_dianhua', models.CharField(blank=True, max_length=20, null=True, verbose_name='联系电话')), + ('weixinhao', models.CharField(blank=True, max_length=50, null=True, verbose_name='微信号')), + ('zhuangtai', models.IntegerField(choices=[(0, '封禁'), (1, '正常')], default=1, verbose_name='店铺状态')), + ('bangding_yonghushu', models.PositiveIntegerField(default=0, verbose_name='绑定用户总数')), + ('erweima_url', models.CharField(blank=True, max_length=500, null=True, verbose_name='店铺二维码相对URL')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('pingzheng', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='shangdian.yonghupingzheng', verbose_name='关联用户凭证')), + ], + options={ + 'verbose_name': '平台商家店铺', + 'verbose_name_plural': '平台商家店铺', + 'db_table': 'dianpu', + }, + ), + migrations.CreateModel( + name='YonghuDianpuBangding', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='绑定自增ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('dianpu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shangdian.dianpu', verbose_name='关联店铺ID')), + ('yonghu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, to_field='yonghuid', verbose_name='用户ID')), + ], + options={ + 'verbose_name': '用户店铺绑定关系', + 'verbose_name_plural': '用户店铺绑定关系', + 'db_table': 'yonghu_dianpu_bangding', + 'unique_together': {('yonghu', 'dianpu')}, + }, + ), + migrations.CreateModel( + name='ShangpinLeixingDianpu', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='类型自增ID')), + ('jieshao', models.CharField(max_length=45, verbose_name='类型介绍')), + ('tupian_url', models.CharField(blank=True, max_length=500, null=True, verbose_name='类型图片URL')), + ('paixu', models.IntegerField(default=0, verbose_name='排序权重')), + ('shangjia_zhuangtai', models.BooleanField(default=True, verbose_name='是否上架')), + ('fengjin_zhuangtai', models.BooleanField(default=False, verbose_name='是否封禁')), + ('shenhe_zhuangtai', models.BooleanField(default=False, help_text='True=审核通过,False=未审核', verbose_name='平台审核状态')), + ('dianpu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shangdian.dianpu', verbose_name='所属店铺')), + ('gonggong_leixing', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shangpin.shangpinleixing', verbose_name='映射的公共商品类型')), + ], + options={ + 'verbose_name': '店铺商品类型映射', + 'verbose_name_plural': '店铺商品类型映射', + 'db_table': 'shangdian_shangpin_leixing', + 'indexes': [models.Index(fields=['dianpu'], name='shangdian_s_dianpu__100f93_idx'), models.Index(fields=['gonggong_leixing'], name='shangdian_s_gonggon_88a9a0_idx')], + }, + ), + ] diff --git a/shangdian/migrations/0002_alter_yonghupingzheng_yonghu.py b/shangdian/migrations/0002_alter_yonghupingzheng_yonghu.py new file mode 100644 index 0000000..9dc84eb --- /dev/null +++ b/shangdian/migrations/0002_alter_yonghupingzheng_yonghu.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-04-21 13:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangdian', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='yonghupingzheng', + name='yonghu', + field=models.CharField(db_index=True, max_length=7, unique=True, verbose_name='关联用户ID'), + ), + ] diff --git a/shangdian/migrations/0003_dianpumorenpeizhi_dianpu_kaiqi_meiri_xiane_and_more.py b/shangdian/migrations/0003_dianpumorenpeizhi_dianpu_kaiqi_meiri_xiane_and_more.py new file mode 100644 index 0000000..8f09233 --- /dev/null +++ b/shangdian/migrations/0003_dianpumorenpeizhi_dianpu_kaiqi_meiri_xiane_and_more.py @@ -0,0 +1,85 @@ +# Generated by Django 4.2.27 on 2026-04-23 14:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangdian', '0002_alter_yonghupingzheng_yonghu'), + ] + + operations = [ + migrations.CreateModel( + name='DianpuMorenPeizhi', + fields=[ + ('id', models.PositiveSmallIntegerField(default=1, editable=False, primary_key=True, serialize=False)), + ('moren_choucheng_feilv', models.DecimalField(decimal_places=4, default=0.1, max_digits=5, verbose_name='默认订单抽成费率(小数,0.1000=10%)')), + ('moren_tixian_xiane', models.DecimalField(decimal_places=2, default=1000.0, max_digits=10, verbose_name='默认可提现限额')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '店铺默认配置', + 'verbose_name_plural': '店铺默认配置', + 'db_table': 'dianpu_moren_peizhi', + }, + ), + migrations.AddField( + model_name='dianpu', + name='kaiqi_meiri_xiane', + field=models.BooleanField(default=False, verbose_name='是否开启每日提现限额'), + ), + migrations.AddField( + model_name='dianpu', + name='kaiqi_shouyi_choucheng', + field=models.BooleanField(default=False, verbose_name='是否开启店铺订单收益抽成'), + ), + migrations.AddField( + model_name='dianpu', + name='ketixian_yue', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='店铺可提现余额'), + ), + migrations.AddField( + model_name='dianpu', + name='meiri_tixian_xiane', + field=models.DecimalField(blank=True, decimal_places=2, default=0.0, max_digits=10, null=True, verbose_name='店铺每日提现限额'), + ), + migrations.AddField( + model_name='dianpu', + name='shouyi_choucheng_feilv', + field=models.DecimalField(blank=True, decimal_places=4, default=0.0, max_digits=5, null=True, verbose_name='店铺订单收益抽成费率(小数,如0.1000=10%)'), + ), + migrations.AddField( + model_name='dianpu', + name='zhengqu_zonge', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='店铺赚取总额'), + ), + migrations.CreateModel( + name='DianpuShouzhiMeiriTongji', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dianpu_id', models.PositiveIntegerField(db_index=True, verbose_name='店铺ID')), + ('riqi', models.DateField(db_index=True, verbose_name='统计日期')), + ('nian', models.PositiveSmallIntegerField(verbose_name='年份')), + ('yue', models.PositiveSmallIntegerField(verbose_name='月份')), + ('ri', models.PositiveSmallIntegerField(verbose_name='日')), + ('xiadan_zongliang', models.PositiveIntegerField(default=0, verbose_name='下单总量')), + ('xiadan_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='下单总额')), + ('chengjiao_zongliang', models.PositiveIntegerField(default=0, verbose_name='成交总量')), + ('chengjiao_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='成交总额')), + ('tuikuan_zongliang', models.PositiveIntegerField(default=0, verbose_name='退款总量')), + ('tuikuan_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='退款总额')), + ('shouyi_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='收益总额')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '店铺每日收支统计', + 'verbose_name_plural': '店铺每日收支统计', + 'db_table': 'dianpu_shouzhi_meiri_tongji', + 'indexes': [models.Index(fields=['dianpu_id', 'riqi'], name='dianpu_shou_dianpu__bffdb2_idx'), models.Index(fields=['nian', 'yue', 'ri'], name='dianpu_shou_nian_a0f415_idx')], + 'unique_together': {('dianpu_id', 'riqi')}, + }, + ), + ] diff --git a/shangdian/migrations/0004_dianpushangpinshenheshezhi.py b/shangdian/migrations/0004_dianpushangpinshenheshezhi.py new file mode 100644 index 0000000..d1fda6c --- /dev/null +++ b/shangdian/migrations/0004_dianpushangpinshenheshezhi.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.27 on 2026-04-25 12:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangdian', '0003_dianpumorenpeizhi_dianpu_kaiqi_meiri_xiane_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='DianpuShangpinShenheShezhi', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('kaiqi_shenhe', models.BooleanField(default=False, verbose_name='是否开启商品审核模式')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '商品审核模式配置', + 'verbose_name_plural': '商品审核模式配置', + 'db_table': 'dianpu_shangpin_shenhe_shezhi', + }, + ), + ] diff --git a/shangdian/migrations/0005_dianpu_fenhong_choucheng_feilv_and_more.py b/shangdian/migrations/0005_dianpu_fenhong_choucheng_feilv_and_more.py new file mode 100644 index 0000000..aabbb9d --- /dev/null +++ b/shangdian/migrations/0005_dianpu_fenhong_choucheng_feilv_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.27 on 2026-05-05 10:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangdian', '0004_dianpushangpinshenheshezhi'), + ] + + operations = [ + migrations.AddField( + model_name='dianpu', + name='fenhong_choucheng_feilv', + field=models.DecimalField(blank=True, decimal_places=4, default=0.05, max_digits=5, null=True, verbose_name='上级分红抽成费率(默认5%)'), + ), + migrations.AddField( + model_name='dianpu', + name='shangji_fenhong_leiji', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='作为上级累计获得分红'), + ), + migrations.AddField( + model_name='dianpu', + name='shangji_ketixian_fenhong', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='作为上级可提现分红余额'), + ), + migrations.AddField( + model_name='dianpu', + name='yaoqing_dianpu_id', + field=models.PositiveIntegerField(blank=True, db_index=True, null=True, verbose_name='邀请人店铺ID(上级)'), + ), + ] diff --git a/shangdian/migrations/0006_dianpushouzhimeiritongji_gei_shangji_cishu_and_more.py b/shangdian/migrations/0006_dianpushouzhimeiritongji_gei_shangji_cishu_and_more.py new file mode 100644 index 0000000..2426c1e --- /dev/null +++ b/shangdian/migrations/0006_dianpushouzhimeiritongji_gei_shangji_cishu_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.27 on 2026-05-10 18:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangdian', '0005_dianpu_fenhong_choucheng_feilv_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='dianpushouzhimeiritongji', + name='gei_shangji_cishu', + field=models.PositiveIntegerField(default=0, verbose_name='给上级分红次数'), + ), + migrations.AddField( + model_name='dianpushouzhimeiritongji', + name='gei_shangji_zonge', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='给上级分红总额'), + ), + migrations.AddField( + model_name='dianpushouzhimeiritongji', + name='shangji_fenhong_cishu', + field=models.PositiveIntegerField(default=0, verbose_name='作为上级获得分红次数'), + ), + migrations.AddField( + model_name='dianpushouzhimeiritongji', + name='shangji_fenhong_zonge', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='作为上级获得分红总额'), + ), + migrations.AddField( + model_name='dianpushouzhimeiritongji', + name='shangji_id', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='当天对应的上级店铺ID(若无则为空)'), + ), + ] diff --git a/shangdian/migrations/__init__.py b/shangdian/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shangdian/models.py b/shangdian/models.py new file mode 100644 index 0000000..45f1cdf --- /dev/null +++ b/shangdian/models.py @@ -0,0 +1,227 @@ +from django.db import models + +# Create your models here. + + +class YonghuPingzheng(models.Model): + id = models.AutoField(primary_key=True, verbose_name='自增主键ID') + yonghu = models.CharField( + max_length=7, + unique=True, # 确保一个用户只有一条凭证记录 + db_index=True, + verbose_name='关联用户ID' + ) + zhanghao = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='登录账号') + mima = models.CharField(max_length=255, verbose_name='密码') + is_active = models.BooleanField(default=True, verbose_name='是否启用') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'yonghu_pingzheng' + verbose_name = '用户认证凭证' + verbose_name_plural = verbose_name + + +class Dianpu(models.Model): + id = models.AutoField(primary_key=True, verbose_name='店铺自增ID') + pingzheng = models.OneToOneField( + 'shangdian.YonghuPingzheng', # 同应用内可简写,但完整路径更清晰 + on_delete=models.CASCADE, + db_index=True, + verbose_name='关联用户凭证' + ) + dianpu_mingcheng = models.CharField(max_length=50, verbose_name='店铺名称') + dianpu_touxiang = models.CharField(max_length=500, null=True, blank=True, verbose_name='店铺头像URL') + + lianxi_dianhua = models.CharField(max_length=20, null=True, blank=True, verbose_name='联系电话') + weixinhao = models.CharField(max_length=50, null=True, blank=True, verbose_name='微信号') + zhuangtai = models.IntegerField(choices=[(0, '封禁'), (1, '正常')], default=1, verbose_name='店铺状态') + bangding_yonghushu = models.PositiveIntegerField(default=0, verbose_name='绑定用户总数') + erweima_url = models.CharField(max_length=500, null=True, blank=True, verbose_name='店铺二维码相对URL') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + kaiqi_shouyi_choucheng = models.BooleanField(default=False, verbose_name='是否开启店铺订单收益抽成') + shouyi_choucheng_feilv = models.DecimalField(max_digits=5, decimal_places=4, null=True, blank=True, default=0.0000, + verbose_name='店铺订单收益抽成费率(小数,如0.1000=10%)') + ketixian_yue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='店铺可提现余额') + zhengqu_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='店铺赚取总额') + meiri_tixian_xiane = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, default=0.00, + verbose_name='店铺每日提现限额') + kaiqi_meiri_xiane = models.BooleanField(default=False, verbose_name='是否开启每日提现限额') + + + yaoqing_dianpu_id = models.PositiveIntegerField(null=True, blank=True, db_index=True, + verbose_name='邀请人店铺ID(上级)') + fenhong_choucheng_feilv = models.DecimalField(max_digits=5, decimal_places=4, null=True, blank=True, default=0.0500, + verbose_name='上级分红抽成费率(默认5%)') + shangji_fenhong_leiji = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, + verbose_name='作为上级累计获得分红') + shangji_ketixian_fenhong = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, + verbose_name='作为上级可提现分红余额') + + class Meta: + db_table = 'dianpu' + verbose_name = '平台商家店铺' + verbose_name_plural = verbose_name + + + + + +class DianpuMorenPeizhi(models.Model): + id = models.PositiveSmallIntegerField(primary_key=True, default=1, editable=False) + moren_choucheng_feilv = models.DecimalField(max_digits=5, decimal_places=4, default=0.1000, verbose_name='默认订单抽成费率(小数,0.1000=10%)') + moren_tixian_xiane = models.DecimalField(max_digits=10, decimal_places=2, default=1000.00, verbose_name='默认可提现限额') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'dianpu_moren_peizhi' + verbose_name = '店铺默认配置' + verbose_name_plural = verbose_name + + def __str__(self): + return f"默认配置 - 费率:{self.moren_choucheng_feilv} 限额:{self.moren_tixian_xiane}" + + def save(self, *args, **kwargs): + self.id = 1 + super().save(*args, **kwargs) + + + + + +class DianpuShouzhiMeiriTongji(models.Model): + dianpu_id = models.PositiveIntegerField(db_index=True, verbose_name='店铺ID') + riqi = models.DateField(db_index=True, verbose_name='统计日期') + nian = models.PositiveSmallIntegerField(verbose_name='年份') + yue = models.PositiveSmallIntegerField(verbose_name='月份') + ri = models.PositiveSmallIntegerField(verbose_name='日') + xiadan_zongliang = models.PositiveIntegerField(default=0, verbose_name='下单总量') + xiadan_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='下单总额') + chengjiao_zongliang = models.PositiveIntegerField(default=0, verbose_name='成交总量') + chengjiao_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='成交总额') + tuikuan_zongliang = models.PositiveIntegerField(default=0, verbose_name='退款总量') + tuikuan_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总额') + shouyi_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='收益总额') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + shangji_fenhong_cishu = models.PositiveIntegerField(default=0, verbose_name='作为上级获得分红次数') + shangji_fenhong_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, + verbose_name='作为上级获得分红总额') + + # ---------- 作为下级,给上级的分红记录 ---------- + shangji_id = models.PositiveIntegerField(null=True, blank=True, verbose_name='当天对应的上级店铺ID(若无则为空)') + gei_shangji_cishu = models.PositiveIntegerField(default=0, verbose_name='给上级分红次数') + gei_shangji_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, + verbose_name='给上级分红总额') + + + class Meta: + db_table = 'dianpu_shouzhi_meiri_tongji' + verbose_name = '店铺每日收支统计' + verbose_name_plural = verbose_name + unique_together = [['dianpu_id', 'riqi']] + indexes = [ + models.Index(fields=['dianpu_id', 'riqi']), + models.Index(fields=['nian', 'yue', 'ri']), + ] + + def __str__(self): + return f"店铺ID:{self.dianpu_id} - {self.riqi}" + + def clean(self): + if self.riqi: + self.nian = self.riqi.year + self.yue = self.riqi.month + self.ri = self.riqi.day + super().clean() + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + + + +class YonghuDianpuBangding(models.Model): + id = models.AutoField(primary_key=True, verbose_name='绑定自增ID') + yonghu = models.ForeignKey( + 'yonghu.UserMain', # 🔥 完整路径 + to_field='yonghuid', + on_delete=models.CASCADE, + db_index=True, + verbose_name='用户ID' + ) + dianpu = models.ForeignKey('Dianpu', on_delete=models.CASCADE, db_index=True, verbose_name='关联店铺ID') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'yonghu_dianpu_bangding' + verbose_name = '用户店铺绑定关系' + verbose_name_plural = verbose_name + unique_together = [['yonghu', 'dianpu']] + + +# shangdian/models.py + +class ShangpinLeixingDianpu(models.Model): + """ + 店铺级商品类型映射表 + 与公共商品类型表 ShangpinLeixing 强制关联,用于店铺自定义类型 + """ + id = models.AutoField(primary_key=True, verbose_name='类型自增ID') + dianpu = models.ForeignKey( + 'shangdian.Dianpu', + on_delete=models.CASCADE, + db_index=True, + verbose_name='所属店铺' + ) + # 🔥 强制映射到公共商品类型,不可为空 + gonggong_leixing = models.ForeignKey( + 'shangpin.ShangpinLeixing', + on_delete=models.CASCADE, + db_index=True, + verbose_name='映射的公共商品类型' + ) + jieshao = models.CharField(max_length=45, verbose_name='类型介绍') + tupian_url = models.CharField(max_length=500, null=True, blank=True, verbose_name='类型图片URL') + paixu = models.IntegerField(default=0, verbose_name='排序权重') + shangjia_zhuangtai = models.BooleanField(default=True, verbose_name='是否上架') + fengjin_zhuangtai = models.BooleanField(default=False, verbose_name='是否封禁') + shenhe_zhuangtai = models.BooleanField(default=False, verbose_name='平台审核状态',help_text='True=审核通过,False=未审核') + + class Meta: + db_table = 'shangdian_shangpin_leixing' + verbose_name = '店铺商品类型映射' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['dianpu']), + models.Index(fields=['gonggong_leixing']), + ] + + def __str__(self): + return f"{self.dianpu.dianpu_mingcheng} - {self.jieshao}" + + + + + +# shangdian/models.py 或独立应用 models.py +class DianpuShangpinShenheShezhi(models.Model): + """店铺商品审核模式全局配置(仅一条记录)""" + kaiqi_shenhe = models.BooleanField(default=False, verbose_name='是否开启商品审核模式') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'dianpu_shangpin_shenhe_shezhi' + verbose_name = '商品审核模式配置' + verbose_name_plural = verbose_name + + def save(self, *args, **kwargs): + self.pk = 1 # 强制使用 ID=1 保证唯一 + super().save(*args, **kwargs) diff --git a/shangdian/tests.py b/shangdian/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/shangdian/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/shangdian/urls.py b/shangdian/urls.py new file mode 100644 index 0000000..0bca675 --- /dev/null +++ b/shangdian/urls.py @@ -0,0 +1,41 @@ +from django.urls import path + +from shangdian.views import BindDianpuView, ShopGoodsView, ShangdianLoginView, ShopOverviewView, ShopQrcodeView, \ + ShopProductConfigView, ShopProductModifyView,ShopProductListView, ShopProductModifyView1,\ + MemberListView,DianpuShouzhiTongjiView, QrcodeDownloadView, SubShopListView, SubShopModifyView,\ + ShopOrderManageView,ShopOrderStatisticsView, ShopRefundView, ShopForceCompleteView,\ + ShopTransferHallView, ShopFineApplyView + +urlpatterns = [ + path('bdsd', BindDianpuView.as_view(), name='绑定或更新商家店铺'), + path('hqsp', ShopGoodsView.as_view(), name='获取商品数据'), + + path('login', ShangdianLoginView.as_view(), name='商家登录'), + path('sjdpgl', ShopOverviewView.as_view(), name='商家获取店铺信息'), + path('sdscdpm', ShopQrcodeView.as_view(), name='生成或更新店铺二维码'), + + path('dphqspxx', ShopProductConfigView.as_view(), name='商店获取商品类型专区'), + path('xgdpsplx', ShopProductModifyView.as_view(), name='商店修改添加修改商品类型专区'), + + path('sdhqspsj', ShopProductListView.as_view(), name='商店获取商品数据'), + path('xgdpsplx1', ShopProductModifyView1.as_view(), name='商店增删改商品数据'), + + path('hqhy', MemberListView.as_view(), name='商店获取会员'), + path('dpsztj', DianpuShouzhiTongjiView.as_view(), name='获取店铺数据统计'), + + path('xzewm', QrcodeDownloadView.as_view(), name='下载店铺二维码'), + + path('wdxjdp', SubShopListView.as_view(), name='获取下级店铺列表'), # 获取下级店铺列表 + path('xjdpbj', SubShopModifyView.as_view(), name='添加/修改下级店铺'), # 添加/修改下级店铺 + # 稍后添加具体路由 + # 稍后添加具体路由 + path('ddlbhq', ShopOrderManageView.as_view(), name='店铺订单数据统计与获取'), + path('ddltj', ShopOrderStatisticsView.as_view(), name='店铺订单数量统计接口'), + path('tk', ShopRefundView.as_view(), name='店铺退款'), + + path('qzjd', ShopForceCompleteView.as_view(), name='店铺强制结算'), + + path('zydt', ShopTransferHallView.as_view(), name='店铺转移大厅'), + + path('fksq', ShopFineApplyView.as_view(), name='店铺开罚单'), +] \ No newline at end of file diff --git a/shangdian/utils.py b/shangdian/utils.py new file mode 100644 index 0000000..a5a0467 --- /dev/null +++ b/shangdian/utils.py @@ -0,0 +1,640 @@ + +from rest_framework.response import Response + +from .models import Dianpu, YonghuPingzheng + + +from shangpin.models import Shangpin +from shangdian.models import Dianpu, DianpuMorenPeizhi + +# shangdian/utils.py 或 tongji/utils.py + +import logging +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 + + +logger = logging.getLogger(__name__) + + +def verify_shop_permission(request, frontend_user_id=None): + """ + 验证商家后台用户身份及店铺状态 + + 参数: + request: DRF request 对象 + frontend_user_id: 前端传递的用户ID(用于防越权校验,可选) + + 返回: + (dianpu_obj, None) # 验证成功,返回店铺对象 + (None, Response) # 验证失败,返回错误响应 + """ + current_user = request.user + + # 1. 从 request.user 获取当前认证用户的 yonghuid + # 注意:您的用户模型可能通过 JWT 的 sub 或其他字段标识,此处假设 request.user.yonghuid 存在 + if not hasattr(current_user, 'yonghuid'): + logger.error(f"用户对象缺少 yonghuid 属性: {current_user}") + return None, Response( + {'code': 500, 'msg': '用户身份信息异常'} + ) + + auth_user_id = current_user.yonghuid + + # 2. 越权检测:如果前端传了用户ID,必须与当前登录用户一致 + if frontend_user_id is not None and str(frontend_user_id) != str(auth_user_id): + logger.warning( + f"越权尝试: 登录用户 {auth_user_id} 尝试操作 {frontend_user_id} 的数据" + ) + return None, Response( + {'code': 403, 'msg': '无权操作其他用户数据'} + ) + + # 3. 查询用户凭证表,验证账号是否存在且状态正常 + try: + pingzheng = YonghuPingzheng.objects.get(yonghu=auth_user_id) + if not pingzheng.is_active: + return None, Response( + {'code': 403, 'msg': '用户账号已被禁用'} + ) + except YonghuPingzheng.DoesNotExist: + return None, Response( + {'code': 404, 'msg': '用户凭证不存在'} + ) + + # 4. 查询关联的店铺信息,验证店铺状态 + try: + dianpu = Dianpu.objects.get(pingzheng=pingzheng) + if dianpu.zhuangtai != 1: # 1 表示正常 + return None, Response( + {'code': 403, 'msg': '店铺已被封禁'} + ) + except Dianpu.DoesNotExist: + return None, Response( + {'code': 404, 'msg': '店铺信息不存在'} + ) + + # 验证通过,返回店铺对象 + return dianpu, None + + + + + + + + + + + + + +'''def update_dianpu_daily_stat( + dianpu_id, + caozuo_leixing, + dingdan_jiage=Decimal('0.00'), + dianpu_shouyi=Decimal('0.00') +): + """ + 原子更新店铺每日统计数据,并根据结算操作更新店铺余额与总额(高并发安全) + + 参数: + dianpu_id: 店铺ID (int) + caozuo_leixing: 操作类型 + 1 = 下单 + 2 = 结算(成交) + 3 = 退款 + dingdan_jiage: 订单金额(商品价格),默认0.00 + dianpu_shouyi: 店铺本次收益金额,默认0.00 + + 注意: + - 金额若小于0则自动设为0 + - 日期自动取当天 + - 使用 select_for_update 配合事务,保证并发时数据绝对准确 + - 若店铺不存在,仅记录日志不中断主流程 + - 结算操作会同步更新 Dianpu 表的 ketixian_yue 和 zhengqu_zonge + """ + # 参数归一化与合法性处理 + dingdan_jiage = Decimal(str(dingdan_jiage)) if dingdan_jiage else Decimal('0.00') + dianpu_shouyi = Decimal(str(dianpu_shouyi)) if dianpu_shouyi else Decimal('0.00') + + if dingdan_jiage < 0: + dingdan_jiage = Decimal('0.00') + if dianpu_shouyi < 0: + dianpu_shouyi = Decimal('0.00') + + today = date.today() + + # 校验店铺是否存在(不加锁,仅校验) + if not Dianpu.objects.filter(id=dianpu_id).exists(): + logger.error(f"店铺ID {dianpu_id} 不存在,统计更新失败") + return + + # 开启事务,使用行锁确保并发安全 + with transaction.atomic(): + # 1. 获取或创建统计记录,并锁定该行 + tongji, created = DianpuShouzhiMeiriTongji.objects.select_for_update().get_or_create( + dianpu_id=dianpu_id, + riqi=today, + defaults={ + 'nian': today.year, + 'yue': today.month, + 'ri': today.day, + 'xiadan_zongliang': 0, + 'xiadan_zonge': Decimal('0.00'), + 'chengjiao_zongliang': 0, + 'chengjiao_zonge': Decimal('0.00'), + 'tuikuan_zongliang': 0, + 'tuikuan_zonge': Decimal('0.00'), + 'shouyi_zonge': Decimal('0.00'), + } + ) + + # 根据操作类型构建统计更新字段 + update_fields = {} + + if caozuo_leixing == 1: # 下单 + update_fields['xiadan_zongliang'] = F('xiadan_zongliang') + 1 + update_fields['xiadan_zonge'] = F('xiadan_zonge') + dingdan_jiage + + elif caozuo_leixing == 2: # 结算(成交) + update_fields['chengjiao_zongliang'] = F('chengjiao_zongliang') + 1 + update_fields['chengjiao_zonge'] = F('chengjiao_zonge') + dingdan_jiage + update_fields['shouyi_zonge'] = F('shouyi_zonge') + dianpu_shouyi + + # 结算时同步更新店铺表的可提现余额和赚取总额(需加锁店铺行) + try: + dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id) + dianpu.ketixian_yue = F('ketixian_yue') + dianpu_shouyi + dianpu.zhengqu_zonge = F('zhengqu_zonge') + dianpu_shouyi + dianpu.save(update_fields=['ketixian_yue', 'zhengqu_zonge', 'update_time']) + except Dianpu.DoesNotExist: + logger.error(f"结算时店铺ID {dianpu_id} 不存在,但已校验过,此处不应发生") + raise + + elif caozuo_leixing == 3: # 退款 + update_fields['tuikuan_zongliang'] = F('tuikuan_zongliang') + 1 + update_fields['tuikuan_zonge'] = F('tuikuan_zonge') + dingdan_jiage + # 退款是否扣减收益需根据业务决定,此处只累加退款统计 + + else: + logger.warning(f"未知的操作类型: {caozuo_leixing}") + return + + # 原子更新统计字段 + if update_fields: + for field, value in update_fields.items(): + setattr(tongji, field, value) + tongji.save(update_fields=list(update_fields.keys())) + + logger.info( + f"店铺统计更新: 店铺ID={dianpu_id}, 操作类型={caozuo_leixing}, " + f"订单金额={dingdan_jiage}, 店铺收益={dianpu_shouyi}, 日期={today}" + ) + + + + + +def calculate_pingtai_and_dianpu_shouyi(shangpin_id, dingdan_jiage, dashou_fencheng): + """ + 根据商品价格和打手分成计算平台收益和店铺收益 + + 逻辑: + 总收益 = 订单金额 - 打手分成 + 1. 从商品表获取所属店铺ID + 2. 若商品无店铺ID:平台收益 = 总收益,店铺收益 = 0 + 3. 若有店铺ID: + - 查询店铺配置: + * 若店铺开启抽成,则费率 = 店铺表.shouyi_choucheng_feilv + * 否则从店铺默认配置表(ID=1)获取默认费率,若记录不存在则自动创建 + - 平台收益 = 总收益 * 费率 + - 店铺收益 = 总收益 - 平台收益 + + 参数: + shangpin_id: 商品ID (int) + dingdan_jiage: 订单金额(商品价格)(Decimal 或 数字) + dashou_fencheng: 打手分成金额 (Decimal 或 数字) + + 返回: + (dianpu_id, pingtai_shouyi, dianpu_shouyi) 三元组 + dianpu_id 可能为 None + pingtai_shouyi 必定有值 + dianpu_shouyi 必定有值 + """ + # 类型转换与安全处理 + dingdan_jiage = Decimal(str(dingdan_jiage)) if dingdan_jiage else Decimal('0.00') + dashou_fencheng = Decimal(str(dashou_fencheng)) if dashou_fencheng else Decimal('0.00') + if dingdan_jiage < 0: + dingdan_jiage = Decimal('0.00') + if dashou_fencheng < 0: + dashou_fencheng = Decimal('0.00') + + # 计算总收益(可用于抽成的基础) + zong_shouyi = dingdan_jiage - dashou_fencheng + if zong_shouyi < 0: + zong_shouyi = Decimal('0.00') + + # 获取商品信息及所属店铺 + try: + shangpin = Shangpin.objects.select_related('dianpu').only('id', 'dianpu').get(id=shangpin_id) + except Shangpin.DoesNotExist: + logger.error(f"计算收益失败:商品ID {shangpin_id} 不存在") + return None, zong_shouyi, Decimal('0.00') # 无商品时平台收益为总收益,店铺0 + + dianpu_id = shangpin.dianpu_id + + # 无店铺ID:平台拿走全部总收益,店铺收益为0 + if dianpu_id is None: + logger.info(f"商品 {shangpin_id} 未关联店铺,平台收益 = 总收益 {zong_shouyi}") + return None, zong_shouyi, Decimal('0.00') + + # 有店铺ID:查询店铺抽成配置 + try: + dianpu = Dianpu.objects.only( + 'id', 'kaiqi_shouyi_choucheng', 'shouyi_choucheng_feilv' + ).get(id=dianpu_id) + except Dianpu.DoesNotExist: + logger.error(f"计算收益失败:店铺ID {dianpu_id} 不存在") + return dianpu_id, zong_shouyi, Decimal('0.00') # 店铺不存在时平台收益也为总收益 + + if dianpu.kaiqi_shouyi_choucheng and dianpu.shouyi_choucheng_feilv is not None: + feilv = dianpu.shouyi_choucheng_feilv + else: + # 从默认配置表获取或创建 + moren_peizhi, created = DianpuMorenPeizhi.objects.get_or_create( + id=1, + defaults={ + 'moren_choucheng_feilv': Decimal('0.1000'), # 默认10% + 'moren_tixian_xiane': Decimal('1000.00') + } + ) + feilv = moren_peizhi.moren_choucheng_feilv + + # 确保费率在合法范围内 + if feilv is None or feilv < 0: + feilv = Decimal('0.0000') + if feilv > 1: + feilv = Decimal('1.0000') + + # 计算平台收益(费率 * 总收益) + pingtai_shouyi = zong_shouyi * feilv + # 店铺收益 = 总收益 - 平台收益 + dianpu_shouyi = zong_shouyi - pingtai_shouyi + + logger.info( + f"收益计算: 商品ID={shangpin_id}, 订单金额={dingdan_jiage}, " + f"打手分成={dashou_fencheng}, 总收益={zong_shouyi}, 费率={feilv}, " + f"平台收益={pingtai_shouyi}, 店铺收益={dianpu_shouyi}" + ) + + return dianpu_id, pingtai_shouyi, dianpu_shouyi''' + + + + + + + +def update_dianpu_daily_stat( + dianpu_id, + caozuo_leixing, + dingdan_jiage=Decimal('0.00'), + dianpu_shouyi=Decimal('0.00'), + shangji_dianpu_id=None, # 新增:上级店铺ID + shangji_fenhong=Decimal('0.00') # 新增:该笔订单给上级的分红 +): + """ + 原子更新店铺每日统计数据,结算时同步更新店铺余额,并安全处理上级分红统计。 + + 参数: + dianpu_id: 本店铺ID + caozuo_leixing: 1=下单, 2=结算, 3=退款 + dingdan_jiage: 订单金额 + dianpu_shouyi: 店铺自身收益 + shangji_dianpu_id: 上级店铺ID(可选) + shangji_fenhong: 给上级的分红金额(可选) + + 注意: + - 金额自动归一化,负数取0 + - 使用 select_for_update 行锁保证并发安全 + - 结算时,本店余额和统计先落定,上级分红统计放在 try 块内,失败不影响本店 + - 新增字段均包含在统计表中:给上级次数/总额/上级ID,以及作为上级获得次数/总额 + """ + # 金额安全处理 + dingdan_jiage = Decimal(str(dingdan_jiage)) if dingdan_jiage else Decimal('0.00') + dianpu_shouyi = Decimal(str(dianpu_shouyi)) if dianpu_shouyi else Decimal('0.00') + shangji_fenhong = Decimal(str(shangji_fenhong)) if shangji_fenhong else Decimal('0.00') + if dingdan_jiage < 0: dingdan_jiage = Decimal('0.00') + if dianpu_shouyi < 0: dianpu_shouyi = Decimal('0.00') + if shangji_fenhong < 0: shangji_fenhong = Decimal('0.00') + + today = date.today() + + # 轻量校验店铺存在性 + if not Dianpu.objects.filter(id=dianpu_id).exists(): + logger.error(f"店铺ID {dianpu_id} 不存在,统计更新失败") + return + + # 开启事务,全部使用行锁 + with transaction.atomic(): + # 1. 获取或创建本店今日统计记录并锁定 + tongji, created = DianpuShouzhiMeiriTongji.objects.select_for_update().get_or_create( + dianpu_id=dianpu_id, + riqi=today, + defaults={ + 'nian': today.year, + 'yue': today.month, + 'ri': today.day, + 'xiadan_zongliang': 0, + 'xiadan_zonge': Decimal('0.00'), + 'chengjiao_zongliang': 0, + 'chengjiao_zonge': Decimal('0.00'), + 'tuikuan_zongliang': 0, + 'tuikuan_zonge': Decimal('0.00'), + 'shouyi_zonge': Decimal('0.00'), + # 新增字段默认值 + 'shangji_fenhong_cishu': 0, + 'shangji_fenhong_zonge': Decimal('0.00'), + 'gei_shangji_cishu': 0, + 'gei_shangji_zonge': Decimal('0.00'), + 'shangji_id': None, + } + ) + + # 构建更新字段字典 + update_fields_dict = {} + + if caozuo_leixing == 1: # 下单 + update_fields_dict['xiadan_zongliang'] = F('xiadan_zongliang') + 1 + update_fields_dict['xiadan_zonge'] = F('xiadan_zonge') + dingdan_jiage + + elif caozuo_leixing == 2: # 结算 + # --- 本店成交统计 --- + update_fields_dict['chengjiao_zongliang'] = F('chengjiao_zongliang') + 1 + update_fields_dict['chengjiao_zonge'] = F('chengjiao_zonge') + dingdan_jiage + update_fields_dict['shouyi_zonge'] = F('shouyi_zonge') + dianpu_shouyi + + # --- 更新本店主表余额(加锁) --- + try: + dianpu_locked = Dianpu.objects.select_for_update().get(id=dianpu_id) + dianpu_locked.ketixian_yue = F('ketixian_yue') + dianpu_shouyi + dianpu_locked.zhengqu_zonge = F('zhengqu_zonge') + dianpu_shouyi + dianpu_locked.save(update_fields=['ketixian_yue', 'zhengqu_zonge', 'update_time']) + except Dianpu.DoesNotExist: + logger.error(f"结算时店铺 {dianpu_id} 不存在") + raise + + # --- 处理给上级的分红(本店作为下级)--- + if shangji_dianpu_id and shangji_fenhong > 0: + # 更新本店统计的“给上级”字段 + update_fields_dict['gei_shangji_cishu'] = F('gei_shangji_cishu') + 1 + update_fields_dict['gei_shangji_zonge'] = F('gei_shangji_zonge') + shangji_fenhong + + # 设置当天上级店铺ID(仅当天第一次结算时写入,后续保持不变) + current_shangji_id = tongji.shangji_id + if current_shangji_id is None: + update_fields_dict['shangji_id'] = shangji_dianpu_id + elif current_shangji_id != shangji_dianpu_id: + logger.warning( + f"店铺 {dianpu_id} 当天上级ID不一致,已有 {current_shangji_id}," + f"新上级 {shangji_dianpu_id},保持原上级ID" + ) + + # --- 更新上级店铺的“作为上级获得”统计(完全隔离保护) --- + try: + _update_shangji_gain_stat_within_tx(shangji_dianpu_id, shangji_fenhong) + except Exception as e: + logger.error( + f"更新上级店铺 {shangji_dianpu_id} 统计失败,不影响本店: {e}" + ) + + elif caozuo_leixing == 3: # 退款 + update_fields_dict['tuikuan_zongliang'] = F('tuikuan_zongliang') + 1 + update_fields_dict['tuikuan_zonge'] = F('tuikuan_zonge') + dingdan_jiage + else: + logger.warning(f"未知操作类型: {caozuo_leixing}") + return + + # 原子更新本店统计字段 + if update_fields_dict: + for field, value in update_fields_dict.items(): + setattr(tongji, field, value) + tongji.save(update_fields=list(update_fields_dict.keys())) + + logger.info( + f"店铺统计更新完成: dianpu={dianpu_id}, 操作={caozuo_leixing}, " + f"金额={dingdan_jiage}, 店铺收益={dianpu_shouyi}, " + f"上级ID={shangji_dianpu_id}, 上级分红={shangji_fenhong}" + ) + + +def _update_shangji_gain_stat_within_tx(shangji_dianpu_id, fenhong_jine): + """ + 在事务内更新上级店铺的“作为上级获得”统计及主表余额。 + 注意:本函数必须由处于同一事务中的 update_dianpu_daily_stat 调用, + 并且调用方已获取必要的行锁,这里再次加锁不会死锁(按相同顺序加锁)。 + """ + today = date.today() + + # 获取或创建上级店铺的今日统计记录并加锁 + tongji_shangji, _ = DianpuShouzhiMeiriTongji.objects.select_for_update().get_or_create( + dianpu_id=shangji_dianpu_id, + riqi=today, + defaults={ + 'nian': today.year, + 'yue': today.month, + 'ri': today.day, + 'xiadan_zongliang': 0, + 'xiadan_zonge': Decimal('0.00'), + 'chengjiao_zongliang': 0, + 'chengjiao_zonge': Decimal('0.00'), + 'tuikuan_zongliang': 0, + 'tuikuan_zonge': Decimal('0.00'), + 'shouyi_zonge': Decimal('0.00'), + 'shangji_fenhong_cishu': 0, + 'shangji_fenhong_zonge': Decimal('0.00'), + 'gei_shangji_cishu': 0, + 'gei_shangji_zonge': Decimal('0.00'), + 'shangji_id': None, + } + ) + + # 更新上级店铺的“作为上级获得”字段 + tongji_shangji.shangji_fenhong_cishu = F('shangji_fenhong_cishu') + 1 + tongji_shangji.shangji_fenhong_zonge = F('shangji_fenhong_zonge') + fenhong_jine + tongji_shangji.save(update_fields=['shangji_fenhong_cishu', 'shangji_fenhong_zonge', 'update_time']) + + # 更新上级店铺主表的分红余额和累计分红 + shangji = Dianpu.objects.select_for_update().get(id=shangji_dianpu_id) + shangji.shangji_fenhong_leiji = F('shangji_fenhong_leiji') + fenhong_jine + shangji.shangji_ketixian_fenhong = F('shangji_ketixian_fenhong') + fenhong_jine + shangji.save(update_fields=['shangji_fenhong_leiji', 'shangji_ketixian_fenhong', 'update_time']) + + logger.info(f"上级店铺 {shangji_dianpu_id} 获得分红 +{fenhong_jine}") + + + +def calculate_pingtai_and_dianpu_shouyi(shangpin_id, dingdan_jiage, dashou_fencheng): + """ + 计算平台收益、店铺收益、上级分红 + + 分配链路(上级分红完全隔离,不影响平台及店铺核心计算): + 总收益 = 订单金额 - 打手分成 + 平台抽成 = 总收益 * (店铺开启则用店铺费率,否则用平台默认费率) + 剩余收益 = 总收益 - 平台抽成 + 上级分红 = 剩余收益 * 本店设置的上级分红费率(若有上级且满足条件) + 店铺收益 = 剩余收益 - 上级分红 + + 参数: + shangpin_id: 商品ID + dingdan_jiage: 订单金额 + dashou_fencheng: 打手分成金额 + + 返回: + 五元组 (dianpu_id, pingtai_shouyi, dianpu_shouyi, shangji_dianpu_id, shangji_fenhong) + - dianpu_id: 店铺ID,可能为None + - pingtai_shouyi: 平台收益 + - dianpu_shouyi: 店铺最终收益 + - shangji_dianpu_id: 上级店铺ID,可能为None + - shangji_fenhong: 上级分红金额,默认0.00 + """ + # ==================== 金额安全处理 ==================== + dingdan_jiage = Decimal(str(dingdan_jiage)) if dingdan_jiage else Decimal('0.00') + dashou_fencheng = Decimal(str(dashou_fencheng)) if dashou_fencheng else Decimal('0.00') + if dingdan_jiage < 0: + dingdan_jiage = Decimal('0.00') + if dashou_fencheng < 0: + dashou_fencheng = Decimal('0.00') + + zong_shouyi = dingdan_jiage - dashou_fencheng + if zong_shouyi < 0: + zong_shouyi = Decimal('0.00') + + # ==================== 获取商品及店铺 ==================== + try: + shangpin = Shangpin.objects.select_related('dianpu').only('id', 'dianpu').get(id=shangpin_id) + except Shangpin.DoesNotExist: + logger.error(f"收益计算失败:商品ID {shangpin_id} 不存在") + return None, zong_shouyi, Decimal('0.00'), None, Decimal('0.00') + + dianpu_id = shangpin.dianpu_id + if dianpu_id is None: + logger.info(f"商品 {shangpin_id} 未关联店铺,平台收益={zong_shouyi}") + return None, zong_shouyi, Decimal('0.00'), None, Decimal('0.00') + + # ==================== 获取店铺配置 ==================== + try: + dianpu = Dianpu.objects.only( + 'id', 'kaiqi_shouyi_choucheng', 'shouyi_choucheng_feilv', + 'yaoqing_dianpu_id', 'fenhong_choucheng_feilv' + ).get(id=dianpu_id) + except Dianpu.DoesNotExist: + logger.error(f"收益计算失败:店铺ID {dianpu_id} 不存在") + return dianpu_id, zong_shouyi, Decimal('0.00'), None, Decimal('0.00') + + # ==================== 平台抽成 ==================== + if dianpu.kaiqi_shouyi_choucheng and dianpu.shouyi_choucheng_feilv is not None: + feilv = dianpu.shouyi_choucheng_feilv + else: + moren_peizhi, created = DianpuMorenPeizhi.objects.get_or_create( + id=1, + defaults={ + 'moren_choucheng_feilv': Decimal('0.1000'), + 'moren_tixian_xiane': Decimal('1000.00') + } + ) + feilv = moren_peizhi.moren_choucheng_feilv + if feilv is None or feilv < 0: + feilv = Decimal('0.0000') + if feilv > 1: + feilv = Decimal('1.0000') + + pingtai_shouyi = zong_shouyi * feilv + shengyu = zong_shouyi - pingtai_shouyi + + # ==================== 上级分红(完全隔离,不影响店铺收益) ==================== + shangji_dianpu_id = dianpu.yaoqing_dianpu_id + shangji_fenhong = Decimal('0.00') + + if shangji_dianpu_id and dianpu.fenhong_choucheng_feilv: + try: + # 检查上级店铺状态 + shangji = Dianpu.objects.filter(id=shangji_dianpu_id).only('id', 'zhuangtai').first() + if shangji and shangji.zhuangtai == 1: + fz_rate = dianpu.fenhong_choucheng_feilv + if fz_rate and fz_rate > 0: + shangji_fenhong = shengyu * fz_rate + if shangji_fenhong < 0: + shangji_fenhong = Decimal('0.00') + if shangji_fenhong > shengyu: + shangji_fenhong = shengyu + shengyu -= shangji_fenhong + else: + if shangji: + logger.info(f"上级店铺 {shangji_dianpu_id} 状态非正常,不参与分红") + else: + logger.warning(f"上级店铺ID {shangji_dianpu_id} 不存在,不参与分红") + except Exception as e: + logger.exception(f"上级分红计算异常,忽略: {e}") + shangji_fenhong = Decimal('0.00') + # 注意:shengyu 未被修改,店铺收益不变 + + dianpu_shouyi = shengyu + + logger.info( + f"收益计算完成: 商品={shangpin_id}, 总收益={zong_shouyi}, 平台={pingtai_shouyi}, " + f"上级ID={shangji_dianpu_id}, 上级分红={shangji_fenhong}, 店铺={dianpu_shouyi}" + ) + + return dianpu_id, pingtai_shouyi, dianpu_shouyi, shangji_dianpu_id, shangji_fenhong + + + + + + + + + + +def validate_shangpin_and_dianpu(shangpin_id): + """ + 校验商品及关联店铺(区分有无店铺) + + 无店铺:仅校验商品是否上架 + 有店铺:校验商品审核、封禁、上架,以及店铺是否封禁 + + 返回: + (True, None, shangpin_obj) # 通过 + (False, "错误信息", None) # 失败 + """ + try: + shangpin = Shangpin.objects.select_related('dianpu').get(id=shangpin_id) + except Shangpin.DoesNotExist: + return False, "商品不存在", None + + # 无店铺:只检查上架 + if not shangpin.dianpu_id: + if not shangpin.shangjia_zhuangtai: + return False, "商品已下架", None + return True, None, shangpin + + # 有店铺:完整校验 + if not shangpin.shenhe_zhuangtai: + return False, "商品未通过平台审核", None + if shangpin.fengjin_zhuangtai: + return False, "商品已被封禁", None + if not shangpin.shangjia_zhuangtai: + return False, "商品已下架", None + + try: + if shangpin.dianpu.zhuangtai != 1: + return False, "店铺已被封禁", None + except Exception: + return False, "关联店铺异常", None + + return True, None, shangpin \ No newline at end of file diff --git a/shangdian/views.py b/shangdian/views.py new file mode 100644 index 0000000..f07b496 --- /dev/null +++ b/shangdian/views.py @@ -0,0 +1,3430 @@ + +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 time +import requests +from django.conf import settings +from django.core.cache import cache +from django.db import transaction +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework import status +from .utils import verify_shop_permission +from .models import Dianpu +from django.db.models import F +logger = logging.getLogger(__name__) + + +class BindDianpuView(APIView): + """ + 店铺绑定接口 + 接收前端传入的店铺ID,更新用户与店铺的绑定关系 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + dianpu_id = request.data.get('dianpu_id') + if not dianpu_id: + return Response({'code': 400, 'msg': '店铺ID不能为空'}) + + user = request.user + if not hasattr(user, 'yonghuid'): + return Response({'code': 500, 'msg': '用户身份信息异常'}) + + # 先获取 UserMain 对象(不加锁,仅用于校验存在性) + try: + user_main = UserMain.objects.get(yonghuid=user.yonghuid) + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + + # 校验店铺是否存在且状态正常(不加锁) + try: + dianpu = Dianpu.objects.get(id=dianpu_id) + except Dianpu.DoesNotExist: + return Response({'code': 404, 'msg': '店铺不存在'}) + + if dianpu.zhuangtai != 1: + return Response({'code': 200, 'msg': '店铺已被封禁,无法绑定'}) + + # 在事务内部进行加锁操作 + try: + with transaction.atomic(): + # 重新获取加锁的用户对象(确保并发安全) + user_main_locked = UserMain.objects.select_for_update().get(yonghuid=user.yonghuid) + dianpu_locked = Dianpu.objects.select_for_update().get(id=dianpu_id) + + # 再次校验店铺状态(防止事务期间被修改) + if dianpu_locked.zhuangtai != 1: + return Response({'code': 200, 'msg': '店铺已被封禁,无法绑定'}) + + # 处理绑定关系 + try: + existing_binding = YonghuDianpuBangding.objects.select_for_update().get(yonghu=user_main_locked) + old_dianpu = existing_binding.dianpu + if old_dianpu.id == dianpu_locked.id: + return Response({'code': 200, 'msg': '已绑定该店铺'}) + + # 旧店铺绑定人数减1 + old_dianpu.bangding_yonghushu = F('bangding_yonghushu') - 1 + old_dianpu.save(update_fields=['bangding_yonghushu']) + + # 更新绑定关系 + existing_binding.dianpu = dianpu_locked + existing_binding.save() + + except YonghuDianpuBangding.DoesNotExist: + YonghuDianpuBangding.objects.create(yonghu=user_main_locked, dianpu=dianpu_locked) + + # 新店铺绑定人数加1 + dianpu_locked.bangding_yonghushu = F('bangding_yonghushu') + 1 + dianpu_locked.save(update_fields=['bangding_yonghushu']) + + logger.info(f"用户 {user.yonghuid} 绑定店铺 {dianpu_id} 成功") + return Response({'code': 200, 'msg': '绑定成功'}) + + except Exception as e: + logger.exception(f"绑定店铺失败: {e}") + return Response({'code': 500, 'msg': '绑定失败,服务器内部错误'}) + + + + + + + +class ShopGoodsView(APIView): + """ + 获取店铺商品数据(类型、专区、商品列表) + 根据用户绑定店铺状态及身份,返回店铺专属或公共商品 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + user = request.user + try: + user_main = UserMain.objects.get(yonghuid=user.yonghuid) + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + + # 1. 查询用户店铺绑定关系 + binding = YonghuDianpuBangding.objects.filter( + yonghu=user_main + ).select_related('dianpu').first() + + # 2. 判断用户是否拥有特殊身份 + has_special = self._has_special_identity(user_main) + + # 3. 根据绑定关系及店铺状态决定数据获取策略 + if binding: + dianpu = binding.dianpu + if dianpu.zhuangtai == 1: + # 店铺正常 -> 返回店铺专属商品(需通过平台审核) + data = self._get_shop_goods(dianpu) + else: + # 店铺被封 -> 返回公共商品,有特殊身份则返回审核状态为1的,否则返回2的 + data = self._get_public_goods(True) + else: + # 无绑定店铺,根据身份决定返回公共商品的审核状态 + data = self._get_public_goods(has_special) + + return Response({'code': 200, 'data': data}) + + def _has_special_identity(self, user_main): + """判断用户是否拥有打手/商家/管事/组长任一身份""" + return (UserDashou.objects.filter(user=user_main).exists() or + UserShangjia.objects.filter(user=user_main).exists() or + UserGuanshi.objects.filter(user=user_main).exists() or + UserZuzhang.objects.filter(user=user_main).exists()) + + def _get_shop_goods(self, dianpu): + """ + 获取店铺专属商品 + 类型、专区、商品均从店铺相关表中获取,且需通过平台审核(shenhe_zhuangtai=True) + """ + # 1. 店铺商品类型:未封禁、已上架、平台审核通过 + leixing_queryset = ShangpinLeixingDianpu.objects.filter( + dianpu=dianpu, + fengjin_zhuangtai=False, + shangjia_zhuangtai=True, + shenhe_zhuangtai=True # 🔥 必须通过平台审核 + ).select_related('gonggong_leixing').order_by('-paixu', 'id') + + leixing_data = [] + for lx in leixing_queryset: + leixing_data.append({ + 'id': lx.id, # 店铺类型ID,前端归类用 + 'jieshao': lx.jieshao, + 'tupian_url': lx.tupian_url, + 'paixu': lx.paixu + }) + + leixing_ids = [lx['id'] for lx in leixing_data] + + # 2. 专区:根据店铺和店铺类型ID,未封禁、已上架 + if leixing_ids: + zhuanqu_queryset = ShangpinZhuanqu.objects.filter( + dianpu=dianpu, + dianpu_leixing_id__in=leixing_ids, + fengjin_zhuangtai=False, + shangjia_zhuangtai=True + ).order_by('-paixu', 'id') + else: + zhuanqu_queryset = ShangpinZhuanqu.objects.none() + + zhuanqu_data = [] + for zq in zhuanqu_queryset: + zhuanqu_data.append({ + 'id': zq.id, + 'mingzi': zq.mingzi, + 'leixing_id': zq.dianpu_leixing_id, # 对应店铺类型ID + 'paixu': zq.paixu + }) + + # 3. 商品:根据店铺、店铺类型、专区,未封禁、已上架、平台审核通过、原有审核状态为正常 + zhuanqu_ids = [zq['id'] for zq in zhuanqu_data] + if zhuanqu_ids: + shangpin_queryset = Shangpin.objects.filter( + dianpu=dianpu, + dianpu_leixing_id__in=leixing_ids, + zhuanqu_id__in=zhuanqu_ids, + fengjin_zhuangtai=False, + shangjia_zhuangtai=True, + shenhe_zhuangtai=True, # 🔥 平台审核通过 + shenhezhuangtai=1 # 原有审核状态:正常 + ).values( + 'id', 'biaoqian', 'jiage', 'tupian_url', 'duiwai_xiaoliang', 'paixu', + 'dianpu_leixing_id', 'zhuanqu_id' + ).order_by('-paixu', 'id') + + shangpin_data = list(shangpin_queryset) + # 字段映射:前端统一使用 leixing_id + for sp in shangpin_data: + sp['leixing_id'] = sp.pop('dianpu_leixing_id') + else: + shangpin_data = [] + + return { + 'shangpinleixing': leixing_data, + 'shangpinzhuanqu': zhuanqu_data, + 'shangpinliebiao': shangpin_data + } + + def _get_public_goods(self, use_audit_status_one=True): + """ + 获取公共商品 + 类型、专区、商品均从公共表获取。 + - 必须满足 dianpu 为空且 dianpu_leixing 为空(即纯公共数据)。 + - 审核状态:若 use_audit_status_one=True,则返回 shenhezhuangtai=1 的数据; + 否则返回 shenhezhuangtai=2 的数据(审核专用)。 + """ + target_shenhe = 1 if use_audit_status_one else 2 + + # 1. 公共商品类型:原有审核状态为 target_shenhe + leixing_queryset = ShangpinLeixing.objects.filter( + shenhezhuangtai=target_shenhe + ).order_by('-paixu', 'id') + + leixing_data = [] + for lx in leixing_queryset: + leixing_data.append({ + 'id': lx.id, + 'jieshao': lx.jieshao, + 'tupian_url': lx.tupian_url, + 'paixu': lx.paixu + }) + + visible_leixing_ids = [lx['id'] for lx in leixing_data] + + # 2. 专区:dianpu 为空,且 dianpu_leixing 为空,leixing_id 在可见范围内,原有审核状态为 target_shenhe + if visible_leixing_ids: + zhuanqu_queryset = ShangpinZhuanqu.objects.filter( + dianpu__isnull=True, + dianpu_leixing__isnull=True, # 🔥 必须为空 + leixing_id__in=visible_leixing_ids, + shenhezhuangtai=target_shenhe + ).order_by('-paixu', 'id') + else: + zhuanqu_queryset = ShangpinZhuanqu.objects.none() + + zhuanqu_data = [] + for zq in zhuanqu_queryset: + zhuanqu_data.append({ + 'id': zq.id, + 'mingzi': zq.mingzi, + 'leixing_id': zq.leixing_id, + 'paixu': zq.paixu + }) + + # 3. 商品:dianpu 为空,dianpu_leixing 为空,专区在可见范围内,原有审核状态为 target_shenhe + zhuanqu_ids = [zq['id'] for zq in zhuanqu_data] + if zhuanqu_ids: + shangpin_queryset = Shangpin.objects.filter( + dianpu__isnull=True, + dianpu_leixing__isnull=True, # 🔥 必须为空 + leixing_id__in=visible_leixing_ids, + zhuanqu_id__in=zhuanqu_ids, + shenhezhuangtai=target_shenhe + ).values( + 'id', 'biaoqian', 'jiage', 'tupian_url', 'duiwai_xiaoliang', 'paixu', + 'leixing_id', 'zhuanqu_id' + ).order_by('-paixu', 'id') + shangpin_data = list(shangpin_queryset) + else: + shangpin_data = [] + + return { + 'shangpinleixing': leixing_data, + 'shangpinzhuanqu': zhuanqu_data, + 'shangpinliebiao': shangpin_data + } + + +class ShangdianLoginView(APIView): + """ + 商家后台登录接口(通过凭证直接获取店铺) + POST /shangdian/login + 请求参数: + yonghuid : 用户ID(7位) + zhanghao : 登录账号 + mima : 密码 + 返回数据: + code: 0, data: { + token: "jwt_token", + yonghuid: "1234567", + dianpu_mingcheng: "店铺名称", + dianpu_touxiang: "头像相对URL", + erweima_url: "二维码相对URL" + } + """ + permission_classes = [AllowAny] + + def post(self, request): + # 1. 获取请求参数 + yonghuid = request.data.get('yonghuid', '').strip() + zhanghao = request.data.get('zhanghao', '').strip() + mima = request.data.get('mima', '').strip() + + # 2. 参数完整性校验 + if not yonghuid or not zhanghao or not mima: + return Response({'code': 400, 'msg': '用户ID、账号、密码均不能为空'}) + + # 3. 验证用户主表是否存在 + try: + user_main = UserMain.objects.get(yonghuid=yonghuid) + except UserMain.DoesNotExist: + logger.warning(f"登录失败:用户ID不存在 - {yonghuid}") + return Response({'code': 404, 'msg': '账号或密码错误'}) + + # 4. 通过用户ID + 账号查询凭证表 + try: + pingzheng = YonghuPingzheng.objects.get( + yonghu=yonghuid, + zhanghao=zhanghao, + is_active=True + ) + except YonghuPingzheng.DoesNotExist: + logger.warning(f"登录失败:账号或密码错误 - 用户ID:{yonghuid}, 账号:{zhanghao}") + return Response({'code': 401, 'msg': '账号或密码错误'}) + + # 5. 校验密码 + if pingzheng.mima != mima: + logger.warning(f"登录失败:密码错误 - 用户ID:{yonghuid}, 账号:{zhanghao}") + return Response({'code': 401, 'msg': '账号或密码错误'}) + + # 6. 【修改点】通过凭证直接获取关联的店铺(利用 OneToOneField 反向关系) + try: + dianpu = pingzheng.dianpu + except Dianpu.DoesNotExist: + logger.error(f"登录异常:凭证 {zhanghao} 未关联任何店铺") + return Response({'code': 403, 'msg': '您尚未关联店铺,请联系管理员'}) + + # 7. 检查店铺状态 + if dianpu.zhuangtai != 1: + logger.warning(f"登录失败:店铺已被封禁 - 店铺ID:{dianpu.id}, 用户ID:{yonghuid}") + return Response({'code': 403, 'msg': '店铺已被封禁,无法登录'}) + + # 8. 生成 JWT Token + refresh = RefreshToken.for_user(user_main) + token = str(refresh.access_token) + + # 9. 更新最后登录时间 + user_main.last_login_time = timezone.now() + user_main.save(update_fields=['last_login_time']) + + # 10. 构造返回数据 + data = { + 'token': token, + 'yonghuid': user_main.yonghuid, + 'dianpu_mingcheng': dianpu.dianpu_mingcheng, + 'dianpu_touxiang': dianpu.dianpu_touxiang or '', + 'erweima_url': dianpu.erweima_url or '' + } + + logger.info(f"商家登录成功:用户ID {yonghuid} - 店铺 {dianpu.dianpu_mingcheng}") + return Response({'code': 0, 'msg': '登录成功', 'data': data}) + + + + +class ShopOverviewView(APIView): + """ + 店铺概览数据接口 + URL: /shangdian/sjdpgl + Method: POST + 权限: JWT认证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 从请求体中获取前端传递的 userId(用于防越权) + frontend_user_id = request.data.get('userId') + if not frontend_user_id: + return Response( + {'code': 400, 'msg': '缺少必要参数 userId'} + ) + + # 调用公共验证方法 + dianpu, error_response = verify_shop_permission(request, frontend_user_id) + if error_response: + return error_response + + # 验证通过,构造返回数据(新增字段,其余保持不变) + data = { + '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 '', + 'create_time': dianpu.create_time.isoformat() if dianpu.create_time else '', + 'update_time': dianpu.update_time.isoformat() if dianpu.update_time else '', + # 新增字段 + 'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng, + 'shouyi_choucheng_feilv': float(dianpu.shouyi_choucheng_feilv) if dianpu.shouyi_choucheng_feilv else 0.0, + 'ketixian_yue': float(dianpu.ketixian_yue), + 'zhengqu_zonge': float(dianpu.zhengqu_zonge), + 'meiri_tixian_xiane': float(dianpu.meiri_tixian_xiane) if dianpu.meiri_tixian_xiane else 0.0, + 'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane, + } + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': data + }) + + + +# 新增导入 +from django.http import HttpResponse +from utils.oss_utils import get_oss_client # 使用您已有的 COS 客户端获取方法 + +class QrcodeDownloadView(APIView): + """ + 店铺二维码下载接口(通过后端代理下载,解决跨域限制) + URL: /shangdian/xzewm?dianpu_id=xx + 权限: JWT认证 + 店铺归属验证 + """ + permission_classes = [IsAuthenticated] + + def get(self, request): + # 1. 身份验证与店铺校验 + frontend_user_id = request.GET.get('userId') # 前端通过查询参数传递 userId + if not frontend_user_id: + return Response({'code': 400, 'msg': '缺少必要参数 userId'}, status=400) + + dianpu, error_response = verify_shop_permission(request, frontend_user_id) + if error_response: + return error_response + + # 2. 获取店铺的二维码相对路径 + erweima_relative_url = dianpu.erweima_url + if not erweima_relative_url: + return HttpResponse('店铺暂无二维码', status=404) + + # 3. 拼接 OSS 完整访问地址(使用 settings 中的域名) + oss_domain = getattr(settings, 'COS_DOMAIN', '') + if not oss_domain: + logger.error("COS_DOMAIN 未配置") + return HttpResponse('OSS域名未配置', status=500) + + image_url = oss_domain.rstrip('/') + '/' + erweima_relative_url.lstrip('/') + + # 4. 从腾讯云 COS 获取图片内容 + try: + client = get_oss_client() + bucket = getattr(settings, 'COS_BUCKET', '') + response = client.get_object(Bucket=bucket, Key=erweima_relative_url) + image_data = response['Body'].get_raw_stream().read() + except Exception as e: + logger.exception("从COS获取二维码失败") + return HttpResponse('下载失败,请稍后重试', status=502) + + # 5. 构造下载响应,强制浏览器保存为文件 + filename = f"店铺二维码_{dianpu.dianpu_mingcheng}.png" + response = HttpResponse(image_data, content_type='image/png') + response['Content-Disposition'] = f'attachment; filename="{filename}"' + response['Content-Length'] = str(len(image_data)) + return response + + + +# shangdian/views.py 追加 +from django.db.models import Sum + +from shangdian.models import DianpuShouzhiMeiriTongji + +class DianpuShouzhiTongjiView(APIView): + """ + 店铺每日收支统计接口(支持按日、月、年聚合) + URL: /dianpu/dpsztj + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 身份验证 + frontend_user_id = request.data.get('userId') + if not frontend_user_id: + return Response({'code': 400, 'msg': '缺少 userId'}) + + dianpu, error = verify_shop_permission(request, frontend_user_id) + if error: + return error + + granularity = request.data.get('granularity', 'day') # day / month / year + year = request.data.get('year') + month = request.data.get('month') + + # 查询当前店铺的统计记录 + qs = DianpuShouzhiMeiriTongji.objects.filter(dianpu_id=dianpu.id) + + if granularity == 'day': + if not year or not month: + return Response({'code': 400, 'msg': '日颗粒度需要提供年份和月份'}) + try: + year = int(year) + month = int(month) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '年份或月份格式错误'}) + + # 取出该月每一天的数据,并按日排序 + daily_data = qs.filter(nian=year, yue=month).values('ri').annotate( + xiadan_zongliang=Sum('xiadan_zongliang'), + xiadan_zonge=Sum('xiadan_zonge'), + chengjiao_zongliang=Sum('chengjiao_zongliang'), + chengjiao_zonge=Sum('chengjiao_zonge'), + tuikuan_zongliang=Sum('tuikuan_zongliang'), + tuikuan_zonge=Sum('tuikuan_zonge'), + shouyi_zonge=Sum('shouyi_zonge'), + ).order_by('ri') + + # 补全缺失的日期(默认值为0) + import calendar + days_in_month = calendar.monthrange(year, month)[1] + result = [] + for day in range(1, days_in_month + 1): + found = next((item for item in daily_data if item['ri'] == day), None) + if found: + result.append(found) + else: + result.append({ + 'ri': day, + 'xiadan_zongliang': 0, 'xiadan_zonge': 0, + 'chengjiao_zongliang': 0, 'chengjiao_zonge': 0, + 'tuikuan_zongliang': 0, 'tuikuan_zonge': 0, + 'shouyi_zonge': 0, + }) + return Response({'code': 0, 'data': result, 'type': 'day'}) + + elif granularity == 'month': + if not year: + return Response({'code': 400, 'msg': '月颗粒度需要提供年份'}) + try: + year = int(year) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '年份格式错误'}) + + # 按月份聚合 + monthly_data = qs.filter(nian=year).values('yue').annotate( + xiadan_zongliang=Sum('xiadan_zongliang'), + xiadan_zonge=Sum('xiadan_zonge'), + chengjiao_zongliang=Sum('chengjiao_zongliang'), + chengjiao_zonge=Sum('chengjiao_zonge'), + tuikuan_zongliang=Sum('tuikuan_zongliang'), + tuikuan_zonge=Sum('tuikuan_zonge'), + shouyi_zonge=Sum('shouyi_zonge'), + ).order_by('yue') + + # 补全 1~12 月 + result = [] + for m in range(1, 13): + found = next((item for item in monthly_data if item['yue'] == m), None) + if found: + result.append(found) + else: + result.append({ + 'yue': m, + 'xiadan_zongliang': 0, 'xiadan_zonge': 0, + 'chengjiao_zongliang': 0, 'chengjiao_zonge': 0, + 'tuikuan_zongliang': 0, 'tuikuan_zonge': 0, + 'shouyi_zonge': 0, + }) + return Response({'code': 0, 'data': result, 'type': 'month'}) + + elif granularity == 'year': + # 按年份聚合 + yearly_data = qs.values('nian').annotate( + xiadan_zongliang=Sum('xiadan_zongliang'), + xiadan_zonge=Sum('xiadan_zonge'), + chengjiao_zongliang=Sum('chengjiao_zongliang'), + chengjiao_zonge=Sum('chengjiao_zonge'), + tuikuan_zongliang=Sum('tuikuan_zongliang'), + tuikuan_zonge=Sum('tuikuan_zonge'), + shouyi_zonge=Sum('shouyi_zonge'), + ).order_by('nian') + return Response({'code': 0, 'data': list(yearly_data), 'type': 'year'}) + + else: + return Response({'code': 400, 'msg': '无效的颗粒度'}) + + + +class ShopQrcodeView(APIView): + """ + 店铺专属二维码生成/更新接口 + URL: /shangdian/sdscdpm + Method: POST + 权限: JWT认证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 获取前端传递的 userId + frontend_user_id = request.data.get('userId') + if not frontend_user_id: + return Response( + {'code': 400, 'msg': '缺少必要参数 userId'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 公共验证 + dianpu, error_response = verify_shop_permission(request, frontend_user_id) + if error_response: + return error_response + + # 获取缓存的微信 access_token + access_token = self._get_wx_access_token() + if not access_token: + return Response( + {'code': 500, 'msg': '获取微信 access_token 失败'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 构造小程序码请求参数 + # 注意:scene 最大32个字符,我们使用店铺ID + scene = str(dianpu.id) + page_path = "pages/index/index" # 前端小程序首页路径 + 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, 'msg': '调用微信服务失败'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 判断返回是否为图片 + if wx_resp.status_code != 200 or 'image' not in wx_resp.headers.get('content-type', ''): + error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {} + errmsg = error_info.get('errmsg', '未知错误') + logger.error(f"微信生成二维码失败: {errmsg}") + return Response( + {'code': 500, 'msg': f'生成二维码失败: {errmsg}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 构造文件名和 COS 路径 + timestamp = int(time.time()) + # 三层文件夹结构: dianpu/shangjiadianpu/dianpuerweima/ + filename = f"dianpu_{dianpu.id}_{timestamp}.png" + oss_path = f"dianpu/shangjiadianpu/dianpuerweima/{filename}" + file_obj = io.BytesIO(wx_resp.content) + + # 上传到 COS + try: + full_url = upload_to_oss(file_obj, oss_path) + except Exception as e: + logger.error(f"上传二维码到 COS 失败: {e}") + return Response( + {'code': 500, 'msg': '图片上传失败'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + if not full_url: + return Response( + {'code': 500, 'msg': '图片上传失败'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 提取相对路径(根据您的 upload_to_oss 返回值格式调整) + 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 + + # 删除旧二维码文件(如果存在) + old_relative_path = dianpu.erweima_url + if old_relative_path: + try: + delete_from_oss(old_relative_path) + except Exception as e: + logger.warning(f"删除旧二维码文件失败: {old_relative_path}, 错误: {e}") + + # 更新数据库 + with transaction.atomic(): + dianpu.erweima_url = relative_path + dianpu.save(update_fields=['erweima_url', 'update_time']) + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': {'erweima_url': relative_path} + }) + + def _get_wx_access_token(self): + """ + 获取微信小程序 access_token,带缓存 + 缓存键: wx_mini_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("微信小程序 AppID 或 Secret 未配置") + 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: + # 提前200秒过期,防止边界情况 + cache.set(cache_key, token, expires_in - 200) + return token + except Exception as e: + logger.error(f"获取微信 access_token 异常: {e}") + return None + + + + +from django.db.models import Count +from rest_framework.views import APIView + +from shangpin.models import ShangpinLeixing + + +class ShopProductConfigView(APIView): + """ + 获取商品发布所需配置:公共类型、店铺商品类型、专区及各自商品数量 + URL: /shangdian/dphqspxx + 权限: JWT认证 + 店铺归属验证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 身份验证,获取店铺对象 + frontend_user_id = request.data.get('userId') + if not frontend_user_id: + return Response({'code': 400, 'msg': '缺少必要参数 userId'}) + + dianpu, error = verify_shop_permission(request, frontend_user_id) + if error: + return error + + # ---------- 2. 公共商品类型(需审核正常) ---------- + try: + public_types_qs = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values( + 'id', 'jieshao', 'tupian_url' + ) + public_types = [{ + 'id': pt['id'], + 'jieshao': pt['jieshao'] or '', + 'tupian_url': pt['tupian_url'] or '', + } for pt in public_types_qs] + except Exception as e: + logger.exception("查询公共商品类型失败") + return Response({'code': 500, 'msg': '服务器错误'}) + + # ---------- 3. 店铺商品类型 ---------- + try: + shop_types = ShangpinLeixingDianpu.objects.filter( + dianpu_id=dianpu.id + ).select_related('gonggong_leixing').order_by('-paixu') + except Exception as e: + logger.exception("查询店铺商品类型失败") + return Response({'code': 500, 'msg': '服务器错误'}) + + type_ids = [] + shop_product_types = [] + for t in shop_types: + shop_product_types.append({ + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'paixu': t.paixu, + 'shangjia_zhuangtai': t.shangjia_zhuangtai, + 'fengjin_zhuangtai': t.fengjin_zhuangtai, + 'shenhe_zhuangtai': t.shenhe_zhuangtai, + 'gonggong_leixing_id': t.gonggong_leixing_id, + }) + type_ids.append(t.id) + + # ---------- 4. 各类型商品数量统计 ---------- + type_product_counts = {} + if type_ids: + counts = Shangpin.objects.filter( + dianpu_id=dianpu.id, + dianpu_leixing_id__in=type_ids + ).values('dianpu_leixing_id').annotate(cnt=Count('id')) + type_product_counts = {item['dianpu_leixing_id']: item['cnt'] for item in counts} + + # ---------- 5. 专区列表 ---------- + try: + zhuanqu_qs = ShangpinZhuanqu.objects.filter( + dianpu_id=dianpu.id + ).order_by('-paixu').values( + 'id', 'mingzi', 'paixu', 'shangjia_zhuangtai', + 'fengjin_zhuangtai', 'dianpu_leixing_id' + ) + zhuanqu_list = [] + zhuanqu_ids = [] + for zq in zhuanqu_qs: + zhuanqu_list.append({ + 'id': zq['id'], + 'mingzi': zq['mingzi'] or '', + 'paixu': zq['paixu'], + 'shangjia_zhuangtai': zq['shangjia_zhuangtai'], + 'fengjin_zhuangtai': zq['fengjin_zhuangtai'], + 'dianpu_leixing_id': zq['dianpu_leixing_id'], + }) + zhuanqu_ids.append(zq['id']) + except Exception as e: + logger.exception("查询专区失败") + return Response({'code': 500, 'msg': '服务器错误'}) + + # ---------- 6. 各专区商品数量统计 ---------- + zhuanqu_product_counts = {} + if zhuanqu_ids: + counts = Shangpin.objects.filter( + dianpu_id=dianpu.id, + zhuanqu_id__in=zhuanqu_ids + ).values('zhuanqu_id').annotate(cnt=Count('id')) + zhuanqu_product_counts = {item['zhuanqu_id']: item['cnt'] for item in counts} + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'public_types': public_types, + 'shop_product_types': shop_product_types, + 'zhuanqu_list': zhuanqu_list, + 'type_product_counts': type_product_counts, + 'zhuanqu_product_counts': zhuanqu_product_counts, + } + }) + + +from django.db import transaction +from utils.oss_utils import delete_from_oss # 引入文件删除工具 + + +class ShopProductModifyView(APIView): + """店铺商品类型与专区管理(增、改、删)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + frontend_user_id = request.data.get('userId') or request.POST.get('userId') + if not frontend_user_id: + return Response({'code': 400, 'msg': '缺少必要参数 userId'}) + + dianpu, error = verify_shop_permission(request, frontend_user_id) + if error: + return error + + action = request.data.get('action') or request.POST.get('action') + if not action: + return Response({'code': 400, 'msg': '缺少操作类型 action'}) + + handlers = { + 'add_type': self._add_type, + 'update_type': self._update_type, + 'delete_type': self._delete_type, + 'add_zhuanqu': self._add_zhuanqu, + 'update_zhuanqu': self._update_zhuanqu, + 'delete_zhuanqu': self._delete_zhuanqu, + } + handler = handlers.get(action) + if not handler: + return Response({'code': 400, 'msg': '不支持的操作类型'}) + + return handler(dianpu, request) + + except Exception as e: + logger.exception("ShopProductModifyView 全局异常") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + # ---------- 商品类型操作 ---------- + @transaction.atomic + def _add_type(self, dianpu, request): + """添加店铺商品类型""" + try: + jieshao = request.data.get('jieshao') or request.POST.get('jieshao') + gonggong_leixing_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id') + paixu = request.data.get('paixu', 0) or request.POST.get('paixu', 0) + + if not jieshao or not gonggong_leixing_id: + return Response({'code': 400, 'msg': '缺少介绍或公共类型ID'}) + + # 验证公共类型存在且正常 + if not ShangpinLeixing.objects.filter(id=gonggong_leixing_id, shenhezhuangtai=1).exists(): + return Response({'code': 400, 'msg': '所选公共商品类型无效'}) + + image_file = request.FILES.get('image') + if not image_file: + return Response({'code': 400, 'msg': '必须上传类型图片'}) + + # 上传图片 + file_path = f"dianpu/shangpin/shangpinleixing/{dianpu.id}_{int(time.time())}.png" + full_url = upload_to_oss(image_file, file_path) + if not full_url: + return Response({'code': 500, 'msg': '图片上传失败'}) + + # 提取相对路径 + tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) + + # 审核状态 + audit_config = DianpuShangpinShenheShezhi.objects.filter(id=1).first() + shenhe_status = not (audit_config and audit_config.kaiqi_shenhe) + + ShangpinLeixingDianpu.objects.create( + dianpu_id=dianpu.id, + gonggong_leixing_id=gonggong_leixing_id, + jieshao=jieshao, + tupian_url=tupian_url, + paixu=paixu, + shangjia_zhuangtai=True, + fengjin_zhuangtai=False, + shenhe_zhuangtai=shenhe_status, + ) + return Response({'code': 0, 'msg': '商品类型添加成功'}) + except Exception as e: + logger.exception("添加商品类型失败") + return Response({'code': 500, 'msg': '添加失败,请稍后重试'}) + + @transaction.atomic + def _update_type(self, dianpu, request): + """修改店铺商品类型,支持更换图片(先删旧图)""" + type_id = request.data.get('type_id') or request.POST.get('type_id') + if not type_id: + return Response({'code': 400, 'msg': '缺少 type_id'}) + + try: + obj = ShangpinLeixingDianpu.objects.select_for_update().get( + id=type_id, dianpu_id=dianpu.id + ) + except ShangpinLeixingDianpu.DoesNotExist: + return Response({'code': 404, 'msg': '商品类型不存在'}) + except Exception as e: + logger.exception("查询商品类型异常") + return Response({'code': 500, 'msg': '服务器错误'}) + + try: + update_fields = [] + content_changed = False + + # 简介 + if 'jieshao' in request.data or 'jieshao' in request.POST: + obj.jieshao = request.data.get('jieshao') or request.POST.get('jieshao') + update_fields.append('jieshao') + content_changed = True + + # 映射公共类型 + if 'gonggong_leixing_id' in request.data or 'gonggong_leixing_id' in request.POST: + new_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id') + if not ShangpinLeixing.objects.filter(id=new_id, shenhezhuangtai=1).exists(): + return Response({'code': 400, 'msg': '公共商品类型无效'}) + obj.gonggong_leixing_id = new_id + update_fields.append('gonggong_leixing_id') + content_changed = True + + # 排序 + if 'paixu' in request.data or 'paixu' in request.POST: + obj.paixu = request.data.get('paixu') or request.POST.get('paixu') + update_fields.append('paixu') + + # 上架状态 + if 'shangjia_zhuangtai' in request.data or 'shangjia_zhuangtai' in request.POST: + val = request.data.get('shangjia_zhuangtai') or request.POST.get('shangjia_zhuangtai') + if isinstance(val, str): + val = val.lower() in ('true', '1', 'yes') + obj.shangjia_zhuangtai = bool(val) + update_fields.append('shangjia_zhuangtai') + + # 图片更换:先删除旧图,再上传新图 + image_file = request.FILES.get('image') + if image_file: + if obj.tupian_url: + delete_from_oss(obj.tupian_url) # 删除旧图片 + file_path = f"dianpu/shangpin/shangpinleixing/{dianpu.id}_{int(time.time())}.png" + full_url = upload_to_oss(image_file, file_path) + if not full_url: + return Response({'code': 500, 'msg': '图片上传失败'}) + obj.tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) + update_fields.append('tupian_url') + content_changed = True + + # 审核重置 + if content_changed: + audit_config = DianpuShangpinShenheShezhi.objects.filter(id=1).first() + if audit_config and audit_config.kaiqi_shenhe: + obj.shenhe_zhuangtai = False + update_fields.append('shenhe_zhuangtai') + + if update_fields: + obj.save(update_fields=update_fields) + return Response({'code': 0, 'msg': '商品类型更新成功'}) + except Exception as e: + logger.exception("修改商品类型失败") + return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) + + @transaction.atomic + def _delete_type(self, dianpu, request): + """删除店铺商品类型,可选是否同时删除关联商品""" + type_id = request.data.get('id') or request.POST.get('id') + if not type_id: + return Response({'code': 400, 'msg': '缺少类型ID'}) + + try: + obj = ShangpinLeixingDianpu.objects.select_for_update().get( + id=type_id, dianpu_id=dianpu.id + ) + except ShangpinLeixingDianpu.DoesNotExist: + return Response({'code': 404, 'msg': '商品类型不存在'}) + except Exception as e: + logger.exception("查询商品类型异常") + return Response({'code': 500, 'msg': '服务器错误'}) + + try: + delete_products = request.data.get('delete_products') or request.POST.get('delete_products') + delete_products = str(delete_products).lower() in ('true', '1', 'yes') + + if delete_products: + # 删除该类型下所有关联商品(同时清理图片) + products = Shangpin.objects.filter( + dianpu_leixing_id=obj.id, dianpu_id=dianpu.id + ) + for p in products: + if p.tupian_url: + delete_from_oss(p.tupian_url) + if p.guize_tupian: + delete_from_oss(p.guize_tupian) + p.delete() + else: + # 仅解除关联,不删商品 + Shangpin.objects.filter( + dianpu_leixing_id=obj.id, dianpu_id=dianpu.id + ).update(dianpu_leixing=None) + + # 删除类型自身图片 + if obj.tupian_url: + delete_from_oss(obj.tupian_url) + obj.delete() + return Response({'code': 0, 'msg': '类型删除成功'}) + except Exception as e: + logger.exception("删除商品类型失败") + return Response({'code': 500, 'msg': '删除失败,请稍后重试'}) + + # ---------- 专区操作 ---------- + @transaction.atomic + def _add_zhuanqu(self, dianpu, request): + """添加专区,自动从店铺商品类型获取 leixing_id(公共商品类型ID)""" + try: + mingzi = request.data.get('mingzi') + dianpu_leixing_id = request.data.get('dianpu_leixing_id') + paixu = request.data.get('paixu', 0) + + if not mingzi or not dianpu_leixing_id: + return Response({'code': 400, 'msg': '缺少名称或所属商品类型'}) + + # 获取店铺商品类型,并拿到映射的公共类型ID + try: + shop_type = ShangpinLeixingDianpu.objects.get( + id=dianpu_leixing_id, dianpu_id=dianpu.id + ) + leixing_id = shop_type.gonggong_leixing_id # 这就是专区需要的 leixing_id + except ShangpinLeixingDianpu.DoesNotExist: + return Response({'code': 400, 'msg': '所选商品类型无效'}) + + # 验证公共类型有效 + if not ShangpinLeixing.objects.filter(id=leixing_id, shenhezhuangtai=1).exists(): + return Response({'code': 400, 'msg': '关联的公共商品类型无效'}) + + ShangpinZhuanqu.objects.create( + dianpu_id=dianpu.id, + mingzi=mingzi, + dianpu_leixing_id=dianpu_leixing_id, + leixing_id=leixing_id, # 自动填充 + paixu=paixu, + shangjia_zhuangtai=True, + fengjin_zhuangtai=False, + ) + return Response({'code': 0, 'msg': '专区添加成功'}) + except Exception as e: + logger.exception("添加专区失败") + return Response({'code': 500, 'msg': '添加失败,请稍后重试'}) + + @transaction.atomic + def _update_zhuanqu(self, dianpu, request): + """修改专区,若更换所属商品类型则同步更新 leixing_id""" + zhuanqu_id = request.data.get('zhuanqu_id') + if not zhuanqu_id: + return Response({'code': 400, 'msg': '缺少 zhuanqu_id'}) + + try: + obj = ShangpinZhuanqu.objects.select_for_update().get( + id=zhuanqu_id, dianpu_id=dianpu.id + ) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 404, 'msg': '专区不存在'}) + except Exception as e: + logger.exception("查询专区异常") + return Response({'code': 500, 'msg': '服务器错误'}) + + try: + update_fields = [] + + if 'mingzi' in request.data: + obj.mingzi = request.data.get('mingzi') + update_fields.append('mingzi') + + if 'dianpu_leixing_id' in request.data: + new_id = request.data.get('dianpu_leixing_id') + try: + shop_type = ShangpinLeixingDianpu.objects.get( + id=new_id, dianpu_id=dianpu.id + ) + obj.dianpu_leixing_id = new_id + obj.leixing_id = shop_type.gonggong_leixing_id # 同步更新公共类型ID + update_fields.extend(['dianpu_leixing_id', 'leixing_id']) + except ShangpinLeixingDianpu.DoesNotExist: + return Response({'code': 400, 'msg': '所选商品类型无效'}) + + if 'paixu' in request.data: + obj.paixu = request.data.get('paixu') + update_fields.append('paixu') + + if 'shangjia_zhuangtai' in request.data: + val = request.data.get('shangjia_zhuangtai') + if isinstance(val, str): + val = val.lower() in ('true', '1', 'yes') + obj.shangjia_zhuangtai = bool(val) + update_fields.append('shangjia_zhuangtai') + + if update_fields: + obj.save(update_fields=update_fields) + return Response({'code': 0, 'msg': '专区更新成功'}) + except Exception as e: + logger.exception("修改专区失败") + return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) + + @transaction.atomic + def _delete_zhuanqu(self, dianpu, request): + """删除专区,可选是否同时删除关联商品""" + zhuanqu_id = request.data.get('id') + if not zhuanqu_id: + return Response({'code': 400, 'msg': '缺少专区ID'}) + + try: + obj = ShangpinZhuanqu.objects.select_for_update().get( + id=zhuanqu_id, dianpu_id=dianpu.id + ) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 404, 'msg': '专区不存在'}) + except Exception as e: + logger.exception("查询专区异常") + return Response({'code': 500, 'msg': '服务器错误'}) + + try: + delete_products = request.data.get('delete_products') + delete_products = str(delete_products).lower() in ('true', '1', 'yes') + + if delete_products: + products = Shangpin.objects.filter( + zhuanqu_id=obj.id, dianpu_id=dianpu.id + ) + for p in products: + if p.tupian_url: + delete_from_oss(p.tupian_url) + if p.guize_tupian: + delete_from_oss(p.guize_tupian) + p.delete() + else: + Shangpin.objects.filter( + zhuanqu_id=obj.id, dianpu_id=dianpu.id + ).update(zhuanqu_id=None) + + obj.delete() + return Response({'code': 0, 'msg': '专区删除成功'}) + except Exception as e: + logger.exception("删除专区失败") + return Response({'code': 500, 'msg': '删除失败,请稍后重试'}) + + +from django.core.paginator import Paginator +class ShopProductListView(APIView): + """ + 商品列表接口,支持筛选、分页。 + URL: /shangdian/sdhqspsj + 权限: JWT认证 + 店铺验证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 身份验证,获取店铺对象 + frontend_user_id = request.data.get('userId') + if not frontend_user_id: + return Response({'code': 400, 'msg': '缺少必要参数 userId'}) + + dianpu, error = verify_shop_permission(request, frontend_user_id) + if error: + return error + + # 2. 安全解析分页与筛选参数 + 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': '分页参数格式错误'}) + + keyword = str(request.data.get('keyword', '')).strip() + shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型ID + zhuanqu_id = request.data.get('zhuanqu_id') # 专区ID + + # 布尔型筛选字段 + def parse_bool(val): + if val is None or val == '': + return None + if isinstance(val, bool): + return val + if isinstance(val, str): + return val.lower() in ('true', '1', 'yes') + 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')) + + # 3. 构建查询(只查当前店铺的商品) + products = Shangpin.objects.filter(dianpu_id=dianpu.id).select_related( + 'dianpu_leixing' + ).only( + 'id', 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'zhuanqu_id', + 'zhenshi_xiaoliang', 'duiwai_xiaoliang', 'jieshao', 'xiadan_xuzhi', + 'guize_tupian', 'yaoqiuleixing', 'huiyuan_id', 'yongjin', + 'tupian_url', 'ewai_dashou_fencheng', 'paixu', + 'dianpu_leixing_id', 'shangjia_zhuangtai', 'fengjin_zhuangtai', + 'shenhe_zhuangtai', 'create_time', 'update_time' + ) + + # 筛选 + 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 格式错误'}) + + if zhuanqu_id is not None: + try: + products = products.filter(zhuanqu_id=int(zhuanqu_id)) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': 'zhuanqu_id 格式错误'}) + + 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、标题、介绍) + if keyword: + products = products.filter( + models.Q(id__icontains=keyword) | + models.Q(biaoqian__icontains=keyword) | + models.Q(jieshao__icontains=keyword) + ) + + # 4. 分页 + total_count = products.count() + paginator = Paginator(products, page_size) + page_obj = paginator.get_page(page) + + # 5. 构造返回数据 + 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, + 'zhuanqu_id': p.zhuanqu_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, + 'tupian_url': p.tupian_url or '', + 'ewai_dashou_fencheng': p.ewai_dashou_fencheng, + 'paixu': p.paixu, + 'dianpu_leixing_id': p.dianpu_leixing_id, + 'shangjia_zhuangtai': p.shangjia_zhuangtai, + 'fengjin_zhuangtai': p.fengjin_zhuangtai, + 'shenhe_zhuangtai': p.shenhe_zhuangtai, + 'create_time': p.create_time.isoformat() if p.create_time else '', + 'update_time': p.update_time.isoformat() if p.update_time else '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': product_list, + 'total': total_count, + } + }) + + + +class ShopProductModifyView1(APIView): + """ + 商品统一操作:修改、添加、删除 + URL: /shangdian/xgdpsplx1 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + frontend_user_id = request.data.get('userId') + if not frontend_user_id: + return Response({'code': 400, 'msg': '缺少必要参数 userId'}) + + dianpu, error = verify_shop_permission(request, frontend_user_id) + if error: + return error + + action = request.data.get('action') + if not action: + return Response({'code': 400, 'msg': '缺少操作类型 action'}) + + if action == 'update_product': + return self._update_product(dianpu, request.data, request.FILES) + elif action == 'add_product': + return self._add_product(dianpu, request.data, request.FILES) + elif action == 'delete_product': + return self._delete_product(dianpu, request.data) + else: + return Response({'code': 400, 'msg': '不支持的操作类型'}) + + # ---------- 修改商品 ---------- + @transaction.atomic + def _update_product(self, dianpu, data, files): + product_id = data.get('product_id') + if not product_id: + return Response({'code': 400, 'msg': '缺少 product_id'}) + + try: + product = Shangpin.objects.select_for_update().get(id=product_id, dianpu_id=dianpu.id) + except Shangpin.DoesNotExist: + return Response({'code': 404, 'msg': '商品不存在'}) + + update_fields = [] + content_changed = False + + # 辅助函数:安全转换为整数,失败则返回原值(让其报错) + def to_int(val): + try: + return int(val) + except (TypeError, ValueError): + return val + + def to_float(val): + try: + return float(val) + except (TypeError, ValueError): + return val + + # 标题 + if 'biaoqian' in data: + product.biaoqian = data['biaoqian'] + update_fields.append('biaoqian') + content_changed = True + + # 价格 + if 'jiage' in data: + product.jiage = to_float(data['jiage']) + update_fields.append('jiage') + content_changed = True + + # 库存 + if 'kucun' in data: + product.kucun = to_int(data['kucun']) + update_fields.append('kucun') + content_changed = True + + # 店铺商品类型 + if 'dianpu_leixing_id' in data: + new_type_id = to_int(data['dianpu_leixing_id']) + if not ShangpinLeixingDianpu.objects.filter(id=new_type_id, dianpu_id=dianpu.id).exists(): + return Response({'code': 400, 'msg': '无效的店铺商品类型'}) + product.dianpu_leixing_id = new_type_id + update_fields.append('dianpu_leixing_id') + content_changed = True + + # 专区 + if 'zhuanqu_id' in data: + new_zq_id = data['zhuanqu_id'] + if new_zq_id is not None and new_zq_id != '': + new_zq_id = to_int(new_zq_id) + if not ShangpinZhuanqu.objects.filter(id=new_zq_id, dianpu_id=dianpu.id).exists(): + return Response({'code': 400, 'msg': '无效的专区'}) + product.zhuanqu_id = new_zq_id + else: + product.zhuanqu_id = None + update_fields.append('zhuanqu_id') + content_changed = True + + # 介绍 + if 'jieshao' in data: + product.jieshao = data['jieshao'] + update_fields.append('jieshao') + content_changed = True + + # 下单须知 + if 'xiadan_xuzhi' in data: + product.xiadan_xuzhi = data['xiadan_xuzhi'] + update_fields.append('xiadan_xuzhi') + + # ★ 抢单要求类型及会员/佣金自动处理 + if 'yaoqiuleixing' in data: + yqlx = to_int(data['yaoqiuleixing']) + if yqlx not in [1, 2]: + return Response({'code': 400, 'msg': '抢单要求类型无效'}) + product.yaoqiuleixing = yqlx + update_fields.append('yaoqiuleixing') + content_changed = True + + if yqlx == 1: + # 会员抢单:优先使用前端显式传递的非空会员ID + if 'huiyuan_id' in data and data.get('huiyuan_id'): + product.huiyuan_id = data['huiyuan_id'] + update_fields.append('huiyuan_id') + product.yongjin = None + if 'yongjin' not in update_fields: + update_fields.append('yongjin') + else: + # 自动从公共类型获取会员ID,或转为佣金 + try: + shop_type = ShangpinLeixingDianpu.objects.get( + id=product.dianpu_leixing_id, dianpu_id=dianpu.id + ) + public_type = ShangpinLeixing.objects.get(id=shop_type.gonggong_leixing_id) + auto_huiyuan_id = public_type.huiyuan_id + if auto_huiyuan_id: + product.huiyuan_id = auto_huiyuan_id + update_fields.append('huiyuan_id') + product.yongjin = None + if 'yongjin' not in update_fields: + update_fields.append('yongjin') + elif public_type.yaoqiuleixing == 2 and public_type.yongjin: + # 无会员,强制转为佣金 + product.yaoqiuleixing = 2 + product.yongjin = public_type.yongjin + product.huiyuan_id = None + # yaoqiuleixing 已在 update_fields 中,需要添加 huiyuan_id 和 yongjin + update_fields.append('huiyuan_id') + update_fields.append('yongjin') + else: + return Response({'code': 400, 'msg': '会员抢单缺少会员ID,公共类型也无会员ID或佣金'}) + except (ShangpinLeixingDianpu.DoesNotExist, ShangpinLeixing.DoesNotExist): + return Response({'code': 400, 'msg': '店铺商品类型无效'}) + elif yqlx == 2: + # 佣金抢单,清除会员ID,设置佣金 + product.huiyuan_id = None + update_fields.append('huiyuan_id') + if 'yongjin' in data: + product.yongjin = to_float(data['yongjin']) or 0 + else: + product.yongjin = 0 + update_fields.append('yongjin') + else: + # 未修改抢单类型,保留原有独立修改逻辑 + if 'huiyuan_id' in data: + product.huiyuan_id = data['huiyuan_id'] or None + update_fields.append('huiyuan_id') + content_changed = True + if 'yongjin' in data: + product.yongjin = to_float(data['yongjin']) or 0 + update_fields.append('yongjin') + content_changed = True + + # 打手额外分成金额 + if 'ewai_dashou_fencheng' in data: + fencheng = to_float(data['ewai_dashou_fencheng']) or 0 + + + if fencheng > (product.jiage or 0): + return Response({'code': 400, 'msg': '分成金额不能超过商品价格'}) + product.ewai_dashou_fencheng = fencheng + update_fields.append('ewai_dashou_fencheng') + content_changed = True + + # 排序 + if 'paixu' in data: + product.paixu = to_int(data['paixu']) + update_fields.append('paixu') + + # 上架状态(保持原逻辑,兼容字符串) + if 'shangjia_zhuangtai' in data: + val = data['shangjia_zhuangtai'] + if isinstance(val, str): + val = val.lower() in ('true', '1', 'yes') + product.shangjia_zhuangtai = bool(val) + update_fields.append('shangjia_zhuangtai') + + # ★ 商品图片真上传 + image_file = files.get('image') + if image_file: + if product.tupian_url: + delete_from_oss(product.tupian_url) + file_path = f"dianpu/shangpin/shangpin/{dianpu.id}_{int(time.time())}.png" + full_url = upload_to_oss(image_file, file_path) + if not full_url: + return Response({'code': 500, 'msg': '商品图片上传失败'}) + product.tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) + update_fields.append('tupian_url') + content_changed = True + + # ★ 规则图片真上传 + rule_file = files.get('rule_image') + remove_rule = data.get('remove_rule_image', '0') + if rule_file: + if product.guize_tupian: + delete_from_oss(product.guize_tupian) + file_path = f"dianpu/shangpin/shangpinguize/{dianpu.id}_{int(time.time())}.png" + full_url = upload_to_oss(rule_file, file_path) + if full_url: + product.guize_tupian = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) + update_fields.append('guize_tupian') + content_changed = True + elif remove_rule == '1' and product.guize_tupian: + delete_from_oss(product.guize_tupian) + product.guize_tupian = None + update_fields.append('guize_tupian') + content_changed = True + + if content_changed: + audit_config = DianpuShangpinShenheShezhi.objects.filter(id=1).first() + if audit_config and audit_config.kaiqi_shenhe: + product.shenhe_zhuangtai = False + update_fields.append('shenhe_zhuangtai') + + if update_fields: + product.save(update_fields=update_fields + ['update_time']) + return Response({'code': 0, 'msg': '商品修改成功'}) + + # ---------- 添加商品(增强会员/佣金默认逻辑) ---------- + @transaction.atomic + def _add_product(self, dianpu, data, files): + biaoqian = data.get('biaoqian') + jiage = data.get('jiage') + kucun = data.get('kucun') + dianpu_leixing_id = data.get('dianpu_leixing_id') + zhuanqu_id = data.get('zhuanqu_id') + jieshao = data.get('jieshao') + + if not all([biaoqian, jiage is not None, kucun is not None, dianpu_leixing_id, zhuanqu_id, jieshao]): + return Response({'code': 400, 'msg': '必填项缺失'}) + + try: + shop_type = ShangpinLeixingDianpu.objects.get(id=dianpu_leixing_id, dianpu_id=dianpu.id) + except ShangpinLeixingDianpu.DoesNotExist: + return Response({'code': 400, 'msg': '无效的店铺商品类型'}) + + try: + public_type = ShangpinLeixing.objects.get(id=shop_type.gonggong_leixing_id, shenhezhuangtai=1) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 400, 'msg': '关联的公共商品类型无效'}) + + # 商品图片上传 + image_file = files.get('image') + if not image_file: + return Response({'code': 400, 'msg': '必须上传商品图片'}) + file_path = f"dianpu/shangpin/shangpin/{dianpu.id}_{int(time.time())}.png" + full_url = upload_to_oss(image_file, file_path) + if not full_url: + return Response({'code': 500, 'msg': '商品图片上传失败'}) + tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) + + # 规则图片 + guize_tupian = None + rule_file = files.get('rule_image') + if rule_file: + file_path = f"dianpu/shangpin/shangpinguize/{dianpu.id}_{int(time.time())}.png" + full_url = upload_to_oss(rule_file, file_path) + if full_url: + guize_tupian = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) + + yaoqiuleixing = int(data.get('yaoqiuleixing', 1)) + if yaoqiuleixing == 1: + huiyuan_id = data.get('huiyuan_id') + # 如果前端没传有效的会员ID,尝试从公共类型获取 + if not huiyuan_id: + huiyuan_id = public_type.huiyuan_id + # 仍然没有会员ID,检查公共类型是否可转为佣金 + if not huiyuan_id: + if public_type.yaoqiuleixing == 2 and public_type.yongjin: + # 自动转为佣金抢单 + yaoqiuleixing = 2 + yongjin = public_type.yongjin + huiyuan_id = None + else: + return Response({'code': 400, 'msg': '会员抢单缺少会员ID,且无法自动获取'}) + else: + yongjin = None + else: + huiyuan_id = None + yongjin = data.get('yongjin') or public_type.yongjin or 0 + + ewai_fencheng = data.get('ewai_dashou_fencheng') or 0 + + + try: + ewai_val = float(ewai_fencheng) + jiage_val = float(jiage) + if ewai_val > jiage_val: + return Response({'code': 400, 'msg': '分成金额不能超过商品价格'}) + except ValueError: + # 处理转换失败的情况,比如记录日志或设为默认值 + print("无法转换为数字") + + + + + '''if ewai_fencheng > jiage: + return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})''' + + shenhe_status = True + audit_config = DianpuShangpinShenheShezhi.objects.filter(id=1).first() + if audit_config and audit_config.kaiqi_shenhe: + shenhe_status = False + + product = Shangpin.objects.create( + biaoqian=biaoqian, + jiage=jiage, + kucun=kucun, + dianpu_id=dianpu.id, + dianpu_leixing_id=dianpu_leixing_id, + zhuanqu_id=zhuanqu_id, + leixing_id=public_type.id, + jieshao=jieshao, + xiadan_xuzhi=data.get('xiadan_xuzhi', ''), + guize_tupian=guize_tupian, + tupian_url=tupian_url, + yaoqiuleixing=yaoqiuleixing, + huiyuan_id=huiyuan_id, + yongjin=yongjin, + ewai_dashou_fencheng=ewai_fencheng, + kaioi_ewai_dashou_fencheng=True, + paixu=data.get('paixu', 0), + shangjia_zhuangtai=data.get('shangjia_zhuangtai', True), + fengjin_zhuangtai=False, + shenhe_zhuangtai=shenhe_status, + shenhezhuangtai=1, + ) + return Response({'code': 0, 'msg': '商品添加成功', 'product_id': product.id}) + + # ---------- 删除商品(完全不变) ---------- + @transaction.atomic + def _delete_product(self, dianpu, data): + product_id = data.get('product_id') + if not product_id: + return Response({'code': 400, 'msg': '缺少 product_id'}) + + try: + product = Shangpin.objects.select_for_update().get(id=product_id, dianpu_id=dianpu.id) + except Shangpin.DoesNotExist: + return Response({'code': 404, 'msg': '商品不存在'}) + + if product.tupian_url: + delete_from_oss(product.tupian_url) + if product.guize_tupian: + delete_from_oss(product.guize_tupian) + + product.delete() + return Response({'code': 0, 'msg': '商品已删除'}) + + +from shangpin.models import Huiyuan # 会员模型,根据实际路径调整 +class MemberListView(APIView): + """ + 获取所有会员列表(供商品发布时选择) + URL: /shangdian/hqhy + 权限:JWT认证 + 店铺归属验证 + 说明:前端需传递 userId,接口会进行身份及店铺状态校验 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ---------- 1. 获取前端传递的用户ID(必须) ---------- + frontend_user_id = request.data.get('userId') + if not frontend_user_id: + return Response({'code': 400, 'msg': '缺少必要参数 userId'}) + + # ---------- 2. 调用公共验证方法(会校验用户身份、店铺状态) ---------- + dianpu, error_response = verify_shop_permission(request, frontend_user_id) + if error_response: + return error_response + + # ---------- 3. 查询所有会员(只返回前端需要的字段) ---------- + try: + members = Huiyuan.objects.all().values('huiyuan_id', 'jieshao') + member_list = [ + { + 'huiyuan_id': m['huiyuan_id'], + 'jieshao': m['jieshao'] or '' + } + for m in members + ] + except Exception as e: + logger.exception("获取会员列表失败") + return Response({'code': 500, 'msg': '服务器错误'}) + + # ---------- 4. 返回成功响应 ---------- + return Response({ + 'code': 0, + 'msg': 'success', + 'data': member_list + }) + + + + + +from django.db import transaction, IntegrityError +from django.core.paginator import Paginator, EmptyPage + + + +class SubShopListView(APIView): + """ + 获取下级店铺列表,同时返回上级分红汇总 + URL: /shangdian/wdxjdp + 方法: POST + 权限: JWT认证 + 上级店铺身份验证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 身份验证 - 获取上级店铺对象 + user_id = request.data.get('userId') + if not user_id: + return Response({'code': 400, 'msg': '缺少 userId'}) + + parent_dianpu, error = verify_shop_permission(request, user_id) + if error: + return error + # 此时 parent_dianpu 即为当前上级店铺 + + # 2. 读取筛选参数 + dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip() + zhuangtai = request.data.get('zhuangtai') + bind_min = request.data.get('bind_min') + bind_max = request.data.get('bind_max') + page = request.data.get('page', 1) + page_size = request.data.get('page_size', 10) + + # 3. 查询下级店铺:yaoqing_dianpu_id 等于当前上级店铺ID + queryset = Dianpu.objects.filter(yaoqing_dianpu_id=parent_dianpu.id) + + # 4. 筛选条件 + if dianpu_mingcheng: + queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng) + if zhuangtai is not None: + try: + zhuangtai = int(zhuangtai) + queryset = queryset.filter(zhuangtai=zhuangtai) + except (ValueError, TypeError): + pass + if bind_min is not None: + try: + bind_min = int(bind_min) + queryset = queryset.filter(bangding_yonghushu__gte=bind_min) + except (ValueError, TypeError): + pass + if bind_max is not None: + try: + bind_max = int(bind_max) + queryset = queryset.filter(bangding_yonghushu__lte=bind_max) + except (ValueError, TypeError): + pass + + # 5. 排序:按创建时间倒序 + queryset = queryset.order_by('-create_time') + + # 6. 分页 + try: + paginator = Paginator(queryset, page_size) + page_obj = paginator.page(page) + except EmptyPage: + page_obj = paginator.page(1) + + # 7. 构建列表数据 + shop_list = [] + for shop in page_obj: + shop_list.append({ + 'id': shop.id, + 'dianpu_mingcheng': shop.dianpu_mingcheng, + 'dianpu_touxiang': shop.dianpu_touxiang or '', + 'lianxi_dianhua': shop.lianxi_dianhua or '', + 'zhuangtai': shop.zhuangtai, + 'bangding_yonghushu': shop.bangding_yonghushu, + 'zhengqu_zonge': str(shop.zhengqu_zonge), + 'ketixian_yue': str(shop.ketixian_yue), + 'create_time': shop.create_time.strftime('%Y-%m-%d %H:%M:%S') if shop.create_time else '', + 'update_time': shop.update_time.strftime('%Y-%m-%d %H:%M:%S') if shop.update_time else '', + 'erweima_url': shop.erweima_url or '', + 'fenhong_choucheng_feilv': str(shop.fenhong_choucheng_feilv) if shop.fenhong_choucheng_feilv else '0.0000', + }) + + # 8. 上级分红汇总(从当前店铺主表读取) + shangji_leiji = str(parent_dianpu.shangji_fenhong_leiji) if parent_dianpu.shangji_fenhong_leiji else '0.00' + shangji_ketixian = str(parent_dianpu.shangji_ketixian_fenhong) if parent_dianpu.shangji_ketixian_fenhong else '0.00' + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'list': shop_list, + 'total': paginator.count, + 'page': page_obj.number, + 'page_size': page_size, + 'shangji_fenhong_leiji': shangji_leiji, + 'shangji_ketixian_fenhong': shangji_ketixian, + } + }) + + +class SubShopModifyView(APIView): + """ + 添加/修改下级店铺 + URL: /shangdian/xjdpbj + 方法: POST + 权限: JWT认证 + 上级店铺身份验证 + action字段: + - 'add' : 添加新的子店铺 + - 'update': 修改已有子店铺的状态/分红费率 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 身份验证 + user_id = request.data.get('userId') + if not user_id: + return Response({'code': 400, 'msg': '缺少 userId'}) + + parent_dianpu, error = verify_shop_permission(request, user_id) + if error: + return error + + action = request.data.get('action', '').strip().lower() + if action == 'add': + return self._add_subshop(request, parent_dianpu) + elif action == 'update': + return self._update_subshop(request, parent_dianpu) + else: + return Response({'code': 400, 'msg': '未知操作类型,action 应为 add 或 update'}) + + def _add_subshop(self, request, parent_dianpu): + """添加子店铺,需提供用户UID、账号、密码、店铺名称等""" + user_uid = request.data.get('user_uid', '').strip() + zhanghao = request.data.get('zhanghao', '').strip() + mima = request.data.get('mima', '') + dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip() + lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip() or None + fenhong_feilv = request.data.get('fenhong_choucheng_feilv', None) + + # 必填校验 + if not all([user_uid, zhanghao, mima, dianpu_mingcheng]): + return Response({'code': 400, 'msg': '必填字段缺失(user_uid, zhanghao, mima, dianpu_mingcheng)'}) + + # 检查用户是否存在 + if not UserMain.objects.filter(yonghuid=user_uid).exists(): + return Response({'code': 404, 'msg': '对应的小程序用户不存在'}) + + # 检查账号是否已占用 + if YonghuPingzheng.objects.filter(zhanghao=zhanghao).exists(): + return Response({'code': 400, 'msg': '登录账号已存在'}) + if YonghuPingzheng.objects.filter(yonghu=user_uid).exists(): + return Response({'code': 400, 'msg': '该用户 ID 已有店铺凭证'}) + + # 费率处理:前端可选,后端默认 0.0500 + if fenhong_feilv is not None: + try: + fenhong_feilv = float(fenhong_feilv) + if fenhong_feilv < 0 or fenhong_feilv > 1: + return Response({'code': 400, 'msg': '分红费率需在 0~1 之间'}) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': '分红费率格式错误'}) + else: + fenhong_feilv = 0.0500 + + try: + with transaction.atomic(): + # 创建凭证 + pingzheng = YonghuPingzheng.objects.create( + yonghu=user_uid, + zhanghao=zhanghao, + mima=mima, + is_active=True + ) + # 创建店铺,关联上级 + dianpu = Dianpu.objects.create( + pingzheng=pingzheng, + dianpu_mingcheng=dianpu_mingcheng, + lianxi_dianhua=lianxi_dianhua or '', + yaoqing_dianpu_id=parent_dianpu.id, # 关键:绑定上级 + fenhong_choucheng_feilv=fenhong_feilv, + # 其他字段使用默认值 + ) + return Response({ + 'code': 200, + 'msg': '子店铺添加成功', + 'dianpu_id': dianpu.id + }) + except IntegrityError as e: + logger.exception("创建子店铺违反唯一约束") + return Response({'code': 400, 'msg': '数据冲突,请检查账号或用户ID'}) + except Exception as e: + logger.exception("创建子店铺失败") + return Response({'code': 500, 'msg': '创建失败,请稍后重试'}) + + def _update_subshop(self, request, parent_dianpu): + """修改子店铺状态和上级分红费率""" + dianpu_id = request.data.get('dianpu_id') + if not dianpu_id: + return Response({'code': 400, 'msg': '缺少 dianpu_id'}) + + try: + dianpu_id = int(dianpu_id) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': 'dianpu_id 需为数字'}) + + try: + with transaction.atomic(): + sub_dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id) + # 权限校验:确保该店铺的上级就是当前店铺 + if sub_dianpu.yaoqing_dianpu_id != parent_dianpu.id: + return Response({'code': 403, 'msg': '无权修改该店铺,非您的下级'}) + + # 更新状态(如果有传) + if 'zhuangtai' in request.data: + zhuangtai = request.data['zhuangtai'] + if zhuangtai not in (0, 1, '0', '1'): + return Response({'code': 400, 'msg': '店铺状态只能为 0(封禁) 或 1(正常)'}) + sub_dianpu.zhuangtai = int(zhuangtai) + + # 更新上级分红费率(如果有传) + if 'fenhong_choucheng_feilv' in request.data: + feilv = request.data['fenhong_choucheng_feilv'] + try: + feilv = float(feilv) + if feilv < 0 or feilv > 1: + return Response({'code': 400, 'msg': '分红费率需在 0~1 之间'}) + sub_dianpu.fenhong_choucheng_feilv = feilv + except (ValueError, TypeError): + return Response({'code': 400, 'msg': '分红费率格式错误'}) + + sub_dianpu.save(update_fields=['zhuangtai', 'fenhong_choucheng_feilv', 'update_time']) + return Response({'code': 200, 'msg': '修改成功'}) + + except Dianpu.DoesNotExist: + return Response({'code': 404, 'msg': '店铺不存在'}) + except Exception as e: + logger.exception("修改下级店铺失败") + return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) + + + + + +# shangdian/views/order_manage_views.py +""" +店铺订单管理接口 +包含: +1. 统计数据:商品类型及订单总数、各状态订单数 +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 域名) + +# ---------- 图片域名 ---------- +""" +店铺订单管理接口 +================= +功能1:获取统计数据(action='statistics') + - 每个商品类型的订单总数(order_count) + - 每个商品类型下各状态的订单数量(status_counts) + - 全局各状态的订单总数(status_counts) + - 店铺订单总量(total_orders) + +功能2:获取订单列表(默认,不带action参数时执行) + - 支持商品类型、订单状态、订单ID、打手ID、游戏昵称、老板ID、订单介绍筛选 + - 支持分页、自定义每页大小 + - 返回打手提交图片(绝对URL)、老板信息、店铺收益等 + +接口地址:POST /shangdian/ddlbhq +权限:JWT认证 + verify_shop_permission 验证店铺身份 +""" + +class ShopOrderStatisticsView(APIView): + """ + 店铺订单统计接口 + URL: POST /shangdian/ddltj + 权限: JWT认证 + 店铺身份验证 + + 请求参数: + userId (必填): 店铺所属用户的ID + + 返回结构: + { + "code": 200, + "msg": "统计成功", + "data": { + "types": [ # 所有有效商品类型 + { + "id": 类型ID, + "jieshao": "类型介绍", + "tupian_url": "类型图片相对路径", + "order_count": 该类型订单总数, + "status_counts": { # 该类型下各状态订单数量 + "1": 数量, + "7": 数量, + "2": 数量, + "8": 数量, + "3": 数量, + "4": 数量, + "5": 数量, + "6": 数量 + } + }, + ... + ], + "status_counts": { # 全局各状态订单总数 + "1": 总数, + "7": 总数, + "2": 总数, + "8": 总数, + "3": 总数, + "4": 总数, + "5": 总数, + "6": 总数 + }, + "total_orders": 订单总量 + } + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 身份验证 + user_id = request.data.get('userId', '').strip() + if not user_id: + return Response({'code': 400, 'msg': '缺少 userId'}) + + dianpu, error = verify_shop_permission(request, user_id) + if error: + return error + + shop_id = dianpu.id + + try: + # 2. 获取所有正常状态(审核通过)的商品类型 + types_qs = ShangpinLeixing.objects.filter( + shenhezhuangtai=1 + ).order_by('-paixu', 'id') + + type_list = [] + type_id_map = {} + for idx, t in enumerate(types_qs): + item = { + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'order_count': 0, + 'status_counts': { + '1': 0, '7': 0, '2': 0, '8': 0, + '3': 0, '4': 0, '5': 0, '6': 0 + } + } + type_list.append(item) + type_id_map[t.id] = idx + + # 3. 获取本店铺所有订单的主键ID列表(通过平台扩展表) + order_pks = list( + DingdanPingtai.objects.filter(dianpu_id=shop_id) + .values_list('dingdan_id', flat=True) # 这里取的是订单主表的主键id + ) + + total_orders = 0 + global_counts = defaultdict(int) + + if order_pks: + # 4. 用主键列表进行聚合统计,按商品类型和订单状态分组 + aggregation = ( + Dingdan.objects + .filter(id__in=order_pks) # 修正点:使用id__in,而不是dingdan_id__in + .values('leixing_id', 'zhuangtai') + .annotate(cnt=Count('id')) + ) + + for row in aggregation: + lid = row['leixing_id'] # 商品类型ID + st = row['zhuangtai'] # 订单状态码 + cnt = row['cnt'] + + # 转为字符串键,方便前端展示 + st_key = str(st) + + # 累加总量 + total_orders += cnt + global_counts[st_key] += cnt + + # 填充到对应商品类型 + if lid in type_id_map: + idx = type_id_map[lid] + type_list[idx]['order_count'] += cnt + if st_key in type_list[idx]['status_counts']: + type_list[idx]['status_counts'][st_key] += cnt + + # 5. 确保全局状态返回所有8个键,即使值为0 + all_status_keys = ['1', '7', '2', '8', '3', '4', '5', '6'] + final_global_counts = { + k: global_counts.get(k, 0) for k in all_status_keys + } + + # 6. 返回统计结果 + return Response({ + 'code': 200, + 'msg': '统计成功', + 'data': { + 'types': type_list, + 'status_counts': final_global_counts, + 'total_orders': total_orders + } + }) + + except Exception as e: + logger.exception("订单统计接口异常") + return Response({ + 'code': 500, + 'msg': '统计失败,服务器内部错误' + }) +# ================================================================ +# 辅助函数:获取OSS域名(用于拼接本地图片绝对URL) +# ================================================================ +def get_oss_domain(): + """ + 获取对象存储的域名,末尾包含 '/'。 + 优先从跨平台配置表 ClubConfig 中获取本俱乐部的 storage_bucket_domain, + 如果获取失败或表不存在,则使用预设的 OSS 域名作为降级方案。 + 返回:字符串,如 'https://example.com/' + """ + try: + # 尝试从跨平台配置中读取 + from kuaping.models import ClubConfig + config = ClubConfig.objects.filter(is_self=1).first() + if config and config.storage_bucket_domain: + # 确保域名末尾有 '/' + return config.storage_bucket_domain.rstrip('/') + '/' + except Exception: + # 表不存在或其它异常,忽略,使用下方默认域名 + pass + # 默认 OSS 域名,请替换为你的实际域名 + return 'https://xingque999qygwuyq-1404472910.cos.ap-shanghai.myqcloud.com/' + + +# ================================================================ +# 辅助函数:获取单个订单的打手提交图片(绝对URL) +# ================================================================ +def _fetch_images(order, oss_domain): + """ + 查询指定订单的打手提交图片记录(Dashoutupian 表), + 将相对路径拼接为完整的绝对 URL 并返回。 + + 参数: + order: Dingdan 订单模型实例 + oss_domain: OSS 域名,末尾含 '/' + + 返回: + (images_list, is_punished, punishment_info) 三元组 + - images_list: 图片绝对 URL 列表 + - is_punished: 暂未使用,默认 False + - punishment_info: 暂未使用,默认空字典 + """ + images = [] + is_punished = False + punishment_info = {} + + # 仅当订单有接单打手ID时才查询打手图片 + if order.jiedan_dashou_id: + # 从打手图片表中查询该订单、该打手的所有图片记录 + image_urls = Dashoutupian.objects.filter( + dingdan_id=order.dingdan_id, # 订单ID匹配 + dashou=order.jiedan_dashou_id # 接单打手ID匹配 + ).values_list('tupian', flat=True) # 只取图片URL字段 + + # 遍历所有图片路径 + for img_path in image_urls: + if img_path: + if img_path.startswith('http'): + # 已经是完整URL,直接添加 + images.append(img_path) + else: + # 相对路径,拼接 OSS 域名 + images.append(oss_domain + img_path.lstrip('/')) + + # 跨平台图片拉取逻辑(根据实际业务需求补充,此处保留结构) + # 如果有跨平台订单,需要请求对方平台接口获取打手图片 + return images, is_punished, punishment_info + + +# ================================================================ +# 辅助函数:并发获取多个订单的打手图片 +# ================================================================ +def _batch_images(orders, oss_domain): + """ + 并发调用 _fetch_images,提升批量获取图片的速度。 + + 参数: + orders: Dingdan 订单对象列表 + oss_domain: OSS 域名 + + 返回: + 字典,键为订单ID (dingdan_id),值为 (images_list, is_punished, punishment_info) + """ + result = {} # 存储结果 + # 使用线程池并发执行,max_workers 可根据服务器性能调整 + with ThreadPoolExecutor(max_workers=10) as executor: + # 建立 future 到订单ID的映射 + future_to_oid = { + executor.submit(_fetch_images, order, oss_domain): order.dingdan_id + for order in orders + } + # 等待所有任务完成 + for future in as_completed(future_to_oid): + oid = future_to_oid[future] # 对应的订单ID + try: + # 获取任务返回值 + imgs, pun, pinfo = future.result() + result[oid] = (imgs, pun, pinfo) + except Exception as e: + # 某个订单获取失败,记录日志并赋予空值,保证整体流程不中断 + logger.error(f"获取订单 {oid} 图片失败: {e}") + result[oid] = ([], False, {}) + return result + + +# ================================================================ +# 核心视图类 +# ================================================================ +class ShopOrderManageView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + """ + 统一入口,根据 action 参数分发到统计或列表模块。 + """ + # 1. 从请求中提取店铺用户ID + user_id = request.data.get('userId', '').strip() + if not user_id: + return Response({'code': 400, 'msg': '缺少 userId'}) + + # 2. 验证店铺身份,获取店铺对象 + dianpu, error_response = verify_shop_permission(request, user_id) + if error_response: + return error_response + + # 3. 根据 action 决定调用哪个方法 + action = request.data.get('action', '').strip().lower() + if action == 'statistics': + return self._statistics(dianpu) # 统计 + else: + return self._order_list(request, dianpu) # 列表 + + # ----------------------------------------------------------------- + # 统计实现 (这就是你要求的100个统计字段的详细实现) + # ----------------------------------------------------------------- + def _statistics(self, dianpu): + """ + 统计本店铺的订单数据,输出以下信息: + - types: 商品类型数组,每个元素包含: + id : 类型ID + jieshao : 类型介绍 + tupian_url : 类型图片 + order_count : 【该类型的订单总数】 + status_counts: 该类型下各个状态的订单数量 + - status_counts: 全局各状态订单总数(键为状态码字符串) + - total_orders : 店铺订单总量 + + 所有字段名均与前端保持一致。 + """ + # 获取店铺ID + shop_id = dianpu.id + + # ------------------------------------------------------------ + # 步骤1:获取所有正常的商品类型(审核状态为1) + # ------------------------------------------------------------ + types_query = ShangpinLeixing.objects.filter( + shenhezhuangtai=1 # 审核状态为1(正常) + ).order_by('-paixu', 'id') # 按排序权重和ID排序 + + # 构建返回的类型列表,并预初始化各状态计数 + type_list = [] # 最终要返回的类型数组 + type_ids = [] # 所有类型的ID列表,用于后续快速索引 + + # 定义可能出现的所有状态码(1/7已付款,2进行中,8待结算,3已完成,4退款审核,5已退款,6退款失败) + status_keys = ['1', '7', '2', '8', '3', '4', '5', '6'] + + for t in types_query: + # 构造每个类型的基础数据 + type_item = { + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'order_count': 0, # 该类型订单总数,稍后填充 + 'status_counts': {k: 0 for k in status_keys} # 各状态计数初始为0 + } + type_list.append(type_item) + type_ids.append(t.id) # 记录ID + + # ------------------------------------------------------------ + # 步骤2:获取本店铺的所有订单ID(通过平台扩展表) + # ------------------------------------------------------------ + # 从 DingdanPingtai 中过滤出属于当前店铺的订单扩展记录,并提取订单主表ID + order_ids = list( + DingdanPingtai.objects.filter( + dianpu_id=shop_id # 店铺ID匹配 + ).values_list('dingdan_id', flat=True) # 只取订单ID字段 + ) + + # 统计变量 + total_orders = 0 # 订单总量 + global_status_counts = defaultdict(int) # 全局各状态计数,键为状态码字符串 + + # ------------------------------------------------------------ + # 步骤3:如果店铺有订单,进行聚合统计 + # ------------------------------------------------------------ + if order_ids: + # 使用 Django 聚合查询:按 leixing_id(商品类型)和 zhuangtai(订单状态)分组计数 + aggregation = Dingdan.objects.filter( + dingdan_id__in=order_ids # 从该店铺的订单中查询 + ).values( + 'leixing_id', # 按类型ID分组 + 'zhuangtai' # 按状态分组 + ).annotate( + cnt=Count('id') # 统计每个分组的记录数 + ) + + # 遍历每个聚合结果,累加到对应的类型和全局计数中 + for row in aggregation: + # 提取分组字段 + lid = row['leixing_id'] # 商品类型ID + st = row['zhuangtai'] # 订单状态码 + cnt = row['cnt'] # 该分组下的订单数量 + + # 累加到全局总数 + total_orders += cnt + global_status_counts[str(st)] += cnt + + # 累加到对应商品类型的状态计数中 + if lid in type_ids: + # 在 type_list 中找到对应的类型条目 + for tp in type_list: + if tp['id'] == lid: + # 更新该类型的订单总数 + tp['order_count'] += cnt + # 更新该类型下对应状态的计数(键为字符串) + tp['status_counts'][str(st)] += cnt + break # 找到后跳出循环,因为ID唯一 + + # 将全局状态计数转换为按固定顺序的字典,确保前端展示顺序稳定 + final_global_counts = {k: global_status_counts.get(k, 0) for k in status_keys} + + # ------------------------------------------------------------ + # 步骤4:返回统计结果 + # ------------------------------------------------------------ + return Response({ + 'code': 200, + 'msg': '统计成功', + 'data': { + 'types': type_list, # 商品类型列表(含各类型统计) + 'status_counts': final_global_counts, # 全局各状态总数 + 'total_orders': total_orders # 订单总量 + } + }) + + # ----------------------------------------------------------------- + # 订单列表实现 + # ----------------------------------------------------------------- + def _order_list(self, request, dianpu): + """处理订单列表请求,支持筛选、分页、并发获取图片。""" + shop_id = dianpu.id + + # 解析筛选参数 + leixing_id = request.data.get('leixing_id') + zhuangtai = request.data.get('zhuangtai') + dingdan_id_key = request.data.get('dingdan_id', '').strip() + jiedan_dashou_id_key = request.data.get('jiedan_dashou_id', '').strip() + nicheng_key = request.data.get('nicheng', '').strip() + laoban_id_key = request.data.get('laoban_id', '').strip() + jieshao_key = request.data.get('jieshao', '').strip() + + page = request.data.get('page', 1) + page_size = request.data.get('page_size', 20) + + # 基础查询:从平台扩展表关联订单主表,并按创建时间倒序 + qs = DingdanPingtai.objects.filter( + dianpu_id=shop_id + ).select_related('dingdan').order_by('-dingdan__create_time') + + # 应用筛选条件 + if leixing_id is not None: + qs = qs.filter(dingdan__leixing_id=leixing_id) + if zhuangtai is not None: + qs = qs.filter(dingdan__zhuangtai=zhuangtai) + if dingdan_id_key: + qs = qs.filter(dingdan__dingdan_id__icontains=dingdan_id_key) + if jiedan_dashou_id_key: + qs = qs.filter(dingdan__jiedan_dashou_id=jiedan_dashou_id_key) + if nicheng_key: + qs = qs.filter(dingdan__nicheng__icontains=nicheng_key) + if laoban_id_key: + qs = qs.filter(laoban_id=laoban_id_key) + if jieshao_key: + qs = qs.filter(dingdan__jieshao__icontains=jieshao_key) + + # 分页 + paginator = Paginator(qs, page_size) + try: + page_obj = paginator.page(page) + except EmptyPage: + page_obj = paginator.page(1) + + # 获取当前页的所有订单和扩展对象 + orders_with_ext = [(item.dingdan, item) for item in page_obj] + # 提取订单对象列表,用于并发获取图片 + order_objs = [order for order, _ in orders_with_ext] + oss_domain = get_oss_domain() + img_map = _batch_images(order_objs, oss_domain) + + # 构建返回数据 + list_data = [] + for order, ext in orders_with_ext: + imgs, is_pun, _ = img_map.get(order.dingdan_id, ([], False, {})) + list_data.append({ + 'dingdan_id': order.dingdan_id, + 'zhuangtai': order.zhuangtai, + 'jine': float(order.jine) if order.jine else 0.00, + 'dashou_fencheng': float(order.dashou_fencheng) if order.dashou_fencheng else 0.00, + 'jiedan_dashou_id': order.jiedan_dashou_id or '', + 'dashou_liuyan': order.dashou_liuyan or '', + 'zhiding_id': order.zhiding_id or '', + 'shangpin_id': order.shangpin_id or '', + 'tupian': order.tupian or '', + 'jieshao': order.jieshao or '', + 'beizhu': order.beizhu or '', + 'nicheng': order.nicheng or '', + 'leixing_id': order.leixing_id, + 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '', + 'update_time': order.update_time.strftime('%Y-%m-%d %H:%M:%S') if order.update_time else '', + 'pingtai_kuozhan': { + 'laoban_id': ext.laoban_id or '', + 'laoban_pingjia': ext.laoban_pingjia or '', + 'dianpu_shouyi': float(ext.dianpu_shouyi) if ext.dianpu_shouyi else 0.00, + }, + 'dashou_images': imgs, + 'is_punished': is_pun, + 'is_fadaned': False, + }) + + return Response({ + 'code': 200, + 'msg': '列表成功', + 'data': { + 'list': list_data, + 'total': paginator.count, + 'page': page_obj.number, + 'page_size': page_size + } + }) + + + + + + +# ================================================================ +# 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__) + + +class ShopRefundView(APIView): + """ + 店铺订单退款接口 + URL: POST /shangdian/tk + 权限: JWT认证 + 店铺身份验证(只处理平台订单,我方派单) + + 请求参数: + userId (必填) : 店铺所属用户的ID + dingdan_id (必填): 订单ID + reason (可选) : 退款理由 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取参数 + user_id = request.data.get('userId', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + reason = request.data.get('reason', '').strip() # 退款理由 + + if not user_id or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 店铺身份验证 + dianpu, error = verify_shop_permission(request, user_id) + if error: + return error + + shop_id = dianpu.id + + # 3. 查询订单,必须属于本店铺(通过扩展表关联) + try: + # 通过扩展表确认归属,并 select_related 获取订单主表 + pingtai_ext = DingdanPingtai.objects.select_related('dingdan').get( + dianpu_id=shop_id, + dingdan__dingdan_id=dingdan_id + ) + order = pingtai_ext.dingdan + except DingdanPingtai.DoesNotExist: + logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在") + return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) + + # 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': '订单状态不允许退款'}) + + # 5. 全部视为平台订单(我方派单),调用微信退款 + refund_result = self._call_wechat_refund(order) + if refund_result['code'] != 0: + return Response({'code': 400, 'msg': refund_result['msg']}) + + # 6. 事务内更新订单、打手、记录、统计 + dashou_id = order.jiedan_dashou_id + try: + with transaction.atomic(): + # 更新订单状态为已退款 + order.zhuangtai = 5 + order.clkf = str(shop_id) # 处理人记录为店铺ID + if reason: + order.tkly = reason + order.save() + + # 更新打手统计(退款量+1,状态置为空闲) + 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, str(shop_id), reason) + + # 打手每日统计(退款动作) + 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'), + action=3 # 3 表示退款 + ) + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + + # 更新平台支出记录 + 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: + # 从扩展表获取已计算好的收益值(下单时已存储) + 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()) + # 统计失败不影响主流程 + # ============================================= + + except Exception as e: + logger.exception("退款事务执行失败") + return Response({'code': 500, 'msg': '退款处理失败,请稍后重试'}) + + # 7. 跨平台通知(如果存在对方平台订单ID,异步通知,不影响主流程) + partner_order_id = order.partner_order_id + partner_club_id = order.partner_club_id + if partner_order_id and partner_club_id: + try: + self._notify_partner_refund(dingdan_id, partner_club_id, partner_order_id, reason, order.jine) + except Exception as e: + logger.error(f"跨平台退款通知异常: {e}") + + return Response({'code': 200, 'msg': '退款成功'}) + + # ========== 微信退款 ========== + def _call_wechat_refund(self, order): + """调用微信支付退款接口""" + 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]): + return {'code': 500, 'msg': '微信支付配置不完整'} + + out_refund_no = self._generate_refund_no(order.dingdan_id) + total_fee = int(float(order.jine or 0) * 100) + refund_fee = total_fee + + 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': '店铺退款', + } + 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"微信退款请求异常: {e}", exc_info=True) + return {'code': 500, 'msg': '微信退款请求异常'} + + if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': + return {'code': 0, 'msg': '退款成功'} + else: + err_msg = result.get('err_code_des', result.get('return_msg', '未知错误')) + return {'code': 400, 'msg': err_msg} + + # ========== 退款记录 ========== + def _update_refund_record(self, order, dashou_id, chuli_id, reason): + """更新退款记录表""" + try: + refund_record, created = Tuikuanjilu.objects.get_or_create( + dingdan_id=order.dingdan_id, + defaults={ + 'dashouid': dashou_id or '', + 'qingqiuid': '', + 'chuliid': chuli_id, + 'liyou': reason 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_id + if reason: + refund_record.liyou = reason + refund_record.save() + except Exception as e: + logger.error(f"更新退款记录异常: {e}") + + # ========== 跨平台通知 ========== + def _notify_partner_refund(self, dingdan_id, partner_club_id, partner_order_id, reason, amount): + """ + 通知对方平台我方已退款。 + 此方法执行不影响主流程,任何异常仅记录日志。 + """ + try: + from dingdan.models import ClubConfig, Club, CrossPlatformOrderData + my_config = ClubConfig.objects.filter(is_self=1).first() + if not my_config: + logger.error("未配置我方俱乐部信息,无法通知对方") + return + self_club_id = my_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.rstrip('/') + notify_url = f"{partner_domain}/houtai/dfddtk" + + payload = { + 'order_id': partner_order_id, + 'club_id': self_club_id, + 'status': 5, + 'reason': reason or '店铺退款' + } + 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': amount 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 Exception as e: + logger.error(f"通知对方平台异常: {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 ShopForceCompleteView(APIView): + """ + 店铺强制结单接口 + URL: POST /shangdian/qzjd + 权限: JWT认证 + 店铺身份验证 + + 请求参数: + userId (必填) : 店铺所属用户的ID + dingdan_id(必填) : 订单ID + + 说明: + - 只处理平台订单(fadan_pingtai == 1) + - 订单必须属于当前店铺 + - 订单状态必须为 8(结算中)才能强制结单 + - 默认视为我方派单,根据是否有对方订单ID判断是否跨平台 + - 本地订单:给打手结算、更新打手余额和统计、更新店铺收益统计 + - 跨平台订单:仅通知对方平台,不处理打手 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ========== 1. 获取参数 ========== + user_id = request.data.get('userId', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not user_id or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # ========== 2. 店铺身份验证 ========== + dianpu, error = verify_shop_permission(request, user_id) + if error: + return error + + shop_id = dianpu.id + + # ========== 3. 查询订单并验证归属 ========== + try: + # 通过平台扩展表确认订单属于本店铺 + pingtai_ext = DingdanPingtai.objects.select_related('dingdan').get( + dianpu_id=shop_id, + dingdan__dingdan_id=dingdan_id + ) + order = pingtai_ext.dingdan + except DingdanPingtai.DoesNotExist: + logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在") + return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) + + # ========== 4. 校验订单状态必须为 8(结算中) ========== + if order.zhuangtai != 8: + logger.warning(f"订单 {dingdan_id} 当前状态为 {order.zhuangtai},不是结算中") + return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'}) + + # ========== 5. 判断是否为跨平台订单(有对方订单ID就认为是跨平台) ========== + is_cross = order.is_cross and (order.partner_order_id or order.partner_club_id) + dashou_id = order.jiedan_dashou_id + dashou_fencheng = order.dashou_fencheng or Decimal('0.00') + jine = order.jine or Decimal('0.00') + + try: + with transaction.atomic(): + # 锁定订单行,防止并发 + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + + # ============================ + # 情况1:跨平台订单(我方派单给对方平台打手) + # ============================ + if is_cross: + # 只需更新我方订单状态,打手在对方平台,不处理我方打手余额 + order.zhuangtai = 3 # 已完成 + order.clkf = str(shop_id) # 处理人记录为店铺ID + order.save() + + # 店铺结算时,若涉及跨平台收益,这里是否要更新店铺统计? + # 根据业务需求,可在此处调用 update_dianpu_daily_stat 等 + # 但通常跨平台订单的资金已在对方平台流转,店铺仅作为发单方, + # 收益可能已在其他环节计算,此处可根据需要添加统计。 + + # 通知对方平台(异步,不影响主流程) + if order.partner_club_id and order.partner_order_id: + transaction.on_commit( + lambda: self._notify_partner_order_complete( + order.partner_club_id, + order.partner_order_id, + shop_id + ) + ) + + # ============================ + # 情况2:本地订单(我方打手接单) + # ============================ + else: + if not dashou_id: + return Response({'code': 400, 'msg': '订单无接单打手,无法结算'}) + if dashou_fencheng < 0: + return Response({'code': 400, 'msg': '打手分成金额无效'}) + + # ---------- 更新打手余额和统计 ---------- + 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.zhuangtai = 1 # 空闲 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"本地订单结算:打手 {dashou_id} 不存在,跳过打手更新") + + # ---------- 更新订单状态 ---------- + order.zhuangtai = 3 + order.clkf = str(shop_id) + order.save() + + # ---------- 打手每日统计(成交) ---------- + try: + from houtai.utils import update_dashou_daily_by_action + update_dashou_daily_by_action( + yonghuid=dashou_id, + amount=dashou_fencheng, + action=2 # 2 表示成交(结算) + ) + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + + # ---------- 店铺收益统计(结算) ---------- + # 根据订单上的店铺收益 (从扩展表取) 更新店铺每日统计及余额 + # 前提:订单创建时已存储了 dianpu_shouyi 到扩展表 + if pingtai_ext.dianpu_shouyi: + try: + 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=2, # 3 = 退款 + dingdan_jiage=order.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") + from shangdian.utils import update_dianpu_daily_stat + + + update_dianpu_daily_stat( + dianpu_id=shop_id, + caozuo_leixing=2, # 结算操作 + dingdan_jiage=jine, + dianpu_shouyi=pingtai_ext.dianpu_shouyi, + # 如果涉及上级分红,可从扩展表取相应字段传入,此处略 + ) + except Exception as e: + logger.error(f"店铺每日统计更新失败: {e}") + + # ========== 6. 记录操作日志 ========== + logger.info(f"强制结单成功,订单 {dingdan_id},店铺 {shop_id}") + + return Response({'code': 200, '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_complete(self, partner_club_id, partner_order_id, shop_id): + """通知对方平台我方订单已完成(状态3)""" + try: + # 尝试获取对方域名等配置(与退款接口类似) + + + my_config = ClubConfig.objects.filter(is_self=1).first() + if not my_config: + logger.error("未配置我方俱乐部信息,无法通知对方") + return + self_club_id = my_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.rstrip('/') + notify_url = f"{partner_domain}/houtai/dfddtk" + + import requests + payload = { + 'order_id': partner_order_id, + 'club_id': self_club_id, + 'status': 3 + } + resp = requests.post(notify_url, json=payload, timeout=3) + if resp.status_code == 200: + resp_data = resp.json() + if resp_data.get('code') == 0: + logger.info(f"通知对方订单已完成: {partner_order_id}") + else: + logger.warning(f"对方平台返回错误: {resp_data.get('msg')}") + else: + logger.warning(f"通知对方HTTP错误: {resp.status_code}") + except Exception as e: + logger.error(f"通知对方平台异常: {e}") + + + + + +# ================================================================ +# shangdian/views/transfer_hall_view.py +# 店铺转移大厅接口 —— 严格二改,不添加任何原接口没有的逻辑 +# ================================================================ +# ================================================================ +# 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): + """ + 店铺转移大厅接口 + URL: POST /shangdian/zydt + 权限: JWT认证 + 店铺身份验证 + + 请求参数: + userId (必填) : 店铺所属用户的ID + dingdan_id(必填) : 订单ID + + 说明: + - 只处理平台订单,我方派单 + - 允许转移的状态:2,8,4 + - 决定是否处理本地打手:有对方订单ID或俱乐部ID(跨平台)则不处理本地打手,只通知对方; + 没有对方信息则处理本地打手(释放+退款统计) + - 转移后清空对方平台信息,订单重回可接单状态 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ---------- 1. 获取参数 ---------- + user_id = request.data.get('userId', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not user_id or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # ---------- 2. 店铺身份验证 ---------- + dianpu, error = verify_shop_permission(request, user_id) + if error: + return error + + shop_id = dianpu.id + + # ---------- 3. 验证订单归属 ---------- + try: + pingtai_ext = DingdanPingtai.objects.select_related('dingdan').get( + dianpu_id=shop_id, + dingdan__dingdan_id=dingdan_id + ) + order = pingtai_ext.dingdan + except DingdanPingtai.DoesNotExist: + logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在") + return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) + + # ---------- 4. 状态校验 ---------- + if order.zhuangtai not in [2, 8, 4]: + return Response({'code': 400, 'msg': f'订单状态不允许转移大厅,当前状态: {order.zhuangtai}'}) + + # ---------- 5. 提取关键字段 ---------- + current_dashou_id = order.jiedan_dashou_id + zhiding_id = order.zhiding_id + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + dashou_fencheng = order.dashou_fencheng or 0 # 用于退款统计 + + # 是否跨平台:只要有对方信息即认为是跨平台订单 + is_cross = bool(partner_club_id or partner_order_id) + + try: + with transaction.atomic(): + # 锁定订单行 + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + + # ---------- 6. 记录打手历史(原接口保留逻辑) ---------- + 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=str(shop_id) + ) + + # ---------- 7. 释放本地打手 + 退款统计(仅当非跨平台时处理) ---------- + if not is_cross and current_dashou_id: + try: + dashou_profile = UserDashou.objects.get(user__yonghuid=current_dashou_id) + dashou_profile.zhuangtai = 1 # 设为空闲 + dashou_profile.save() + except ObjectDoesNotExist: + logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新") + else: + # 打手释放成功后,记录每日退款统计(转移视为退款) + try: + update_dashou_daily_by_action( + yonghuid=current_dashou_id, + amount=dashou_fencheng, + action=3 # 3 表示退款 + ) + logger.info(f"打手 {current_dashou_id} 转移大厅退款统计成功") + except Exception as e: + logger.error(f"打手每日统计失败: {e}") + + # ---------- 8. 清空对方平台信息并重置订单 ---------- + order.partner_club_id = None + order.partner_order_id = None + order.jiedan_dashou_id = None + order.clkf = str(shop_id) + + # 根据是否有指定打手决定状态 + order.zhuangtai = 7 if zhiding_id else 1 + order.save() + + # ---------- 9. 异步通知对方平台(如果有对方信息) ---------- + if is_cross and partner_club_id and partner_order_id: + # 获取我方俱乐部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}") + + if my_club_config: + 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" + payload = { + 'order_id': partner_order_id, + 'club_id': self_club_id + } + resp = requests.post(notify_url, json=payload, timeout=3) + if resp.status_code == 200: + resp_json = resp.json() + if resp_json.get('code') == 0: + # 更新跨平台订单数据(原接口逻辑) + try: + 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': '', + 'partner_amount': order.jine or 0, + } + ) + if not _: + 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}") + except Exception as e: + logger.error(f"更新跨平台订单数据失败: {e}") + else: + logger.warning(f"对方平台返回错误: {resp_json.get('msg')}") + else: + logger.warning(f"对方接口HTTP错误: {resp.status_code}") + except Club.DoesNotExist: + logger.error(f"未找到对方俱乐部配置: partner_club_id={partner_club_id}") + except Exception as e: + logger.error(f"转移大厅通知对方异常: {e}") + + threading.Thread(target=notify).start() + + logger.info(f"转移大厅成功,订单 {dingdan_id}") + return Response({'code': 200, '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': '服务器内部错误'}) + + + + + + +# ================================================================ +# shangdian/views/fine_apply_view.py +# 店铺罚款申请接口 —— 二改自 FineApplyView(客服罚款) +# ================================================================ + +from dingdan.models import Dingdan, DingdanPingtai, Fadan # 订单、罚款记录 + + +class ShopFineApplyView(APIView): + """ + 店铺罚款申请接口 + URL: POST /shangdian/fksq + 权限: JWT认证 + 店铺身份验证 + + 请求参数: + userId (必填) : 店铺所属用户的ID(用于身份验证) + dingdan_id (必填) : 订单ID + reason (必填) : 罚款原因 + amount (必填) : 罚款金额(元) + yingxiang_qiangdan (可选) : 是否影响抢单,1=是,0=否,默认1 + + 说明: + - 仅处理平台订单,需属于当前店铺 + - 本地/对方派单处理逻辑与客服接口一致: + 无对方订单ID 或 对方派单 → 本地生成罚单 + 有对方订单ID且我方派单 → 跨平台通知对方,本地也记录(标记对方俱乐部ID) + - 申请人:店铺ID,身份码固定为4(店铺管理员) + - 被处罚人:接单打手,身份码固定为1 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ---------- 1. 提取参数 ---------- + user_id = request.data.get('userId', '').strip() # 店铺用户ID + dingdan_id = request.data.get('dingdan_id', '').strip() + chufaliyou = request.data.get('reason', '').strip() # 罚款原因(对应前端的reason) + fakuanjine = request.data.get('amount', 0) # 罚款金额(对应前端的amount) + yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) # 影响抢单 + + if not all([user_id, dingdan_id, chufaliyou, fakuanjine]): + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + # ---------- 2. 店铺身份验证 ---------- + dianpu, error = verify_shop_permission(request, user_id) + if error: + return error + shop_id = dianpu.id # 店铺ID,作为罚款申请人 + + # ---------- 3. 验证订单归属 ---------- + try: + pingtai_ext = DingdanPingtai.objects.select_related('dingdan').get( + dianpu_id=shop_id, + dingdan__dingdan_id=dingdan_id + ) + order = pingtai_ext.dingdan + except DingdanPingtai.DoesNotExist: + logger.warning(f"罚款申请:订单 {dingdan_id} 不属于店铺 {shop_id}") + return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) + + # ---------- 4. 获取打手ID ---------- + dashou_id = order.jiedan_dashou_id + if not dashou_id: + return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'}) + + # ---------- 5. 重复罚款检查 ---------- + if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists(): + return Response({'code': 400, 'msg': '该打手已被罚款过'}) + + # ---------- 6. 跨平台相关字段 ---------- + partner_order_id = order.partner_order_id or '' + partner_club_id = order.partner_club_id or '' + dispatch_type = order.dispatch_type or 0 # 1=我方派单,2=对方派单 + + # 本地创建罚单的通用函数 + def create_local_fadan(partner_club=''): + Fadan.objects.create( + beichufa_id=dashou_id, + shenqing_chufa=str(shop_id), # 申请人记为店铺ID + shenfen=1, # 被处罚人身份:1 打手 + chufaliyou=chufaliyou, + fakuanjine=fakuanjine, + guanliandingdan_id=dingdan_id, + zhuangtai=1, # 待缴纳 + yingxiang_qiangdan=yingxiang_qiangdan, + shenqingren_shenfen=4, # 申请人身份:4 店铺管理员 + partner_club_id=partner_club, + ) + + # ---------- 7. 判断处罚流向 ---------- + # 情况A:没有对方订单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': 200, 'msg': '罚款已生成'}) + + # ---------- 8. 情况B:有对方订单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 Club.DoesNotExist: + return Response({'code': 500, 'msg': '未找到对方平台配置,无法通知'}) + except Exception as e: + logger.error(f"获取对方平台信息失败: {e}") + return Response({'code': 500, 'msg': '获取对方平台信息失败'}) + + # 构造通知 payload + url = partner_domain.rstrip('/') + '/houtai/kffkdssq_notify' + payload = { + 'order_id': partner_order_id, # 对方平台的订单ID + 'club_id': self_club_id, # 我方俱乐部ID + 'chufaliyou': chufaliyou, + 'fakuanjine': fakuanjine, + 'yingxiang_qiangdan': yingxiang_qiangdan, + 'shenqingren_shenfen': 4, # 店铺管理员身份 + 'operator': str(shop_id), + } + + # 带重试的通知 + success = False + for _ in range(2): + try: + resp = requests.post(url, json=payload, timeout=3) + 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(): + create_local_fadan(partner_club=partner_club_id) + + logger.info(f"跨平台罚款通知成功: 订单 {dingdan_id}") + return Response({'code': 200, 'msg': '罚款已提交至对方平台'}) + + except Exception as e: + logger.error(f"店铺罚款申请异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) \ No newline at end of file diff --git a/shangpin/__init__.py b/shangpin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shangpin/admin.py b/shangpin/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/shangpin/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/shangpin/apps.py b/shangpin/apps.py new file mode 100644 index 0000000..9666a1b --- /dev/null +++ b/shangpin/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ShangpinConfig(AppConfig): + name = "shangpin" diff --git a/shangpin/migrations/0001_initial.py b/shangpin/migrations/0001_initial.py new file mode 100644 index 0000000..dbc04a5 --- /dev/null +++ b/shangpin/migrations/0001_initial.py @@ -0,0 +1,149 @@ +# Generated by Django 5.2.10 on 2026-01-14 19:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Huiyuan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('huiyuan_id', models.CharField(max_length=6, unique=True, verbose_name='会员ID')), + ('jieshao', models.CharField(max_length=30, verbose_name='会员介绍')), + ('jtjieshao', models.TextField(blank=True, null=True, verbose_name='会员规则介绍')), + ('jiage', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='会员价格')), + ('guanshifc', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='管事分成')), + ('goumai_cishu', models.PositiveIntegerField(default=0, verbose_name='购买次数')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '会员', + 'verbose_name_plural': '会员', + 'db_table': 'huiyuan', + }, + ), + migrations.CreateModel( + name='Huiyuangoumai', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='购买记录ID')), + ('huiyuan_id', models.CharField(db_index=True, max_length=6, verbose_name='会员ID')), + ('yonghu_id', models.CharField(db_index=True, max_length=32, verbose_name='用户ID')), + ('huiyuan_zhuangtai', models.PositiveIntegerField(default=1, verbose_name='会员状态')), + ('jieshao', models.CharField(max_length=30, null=True, verbose_name='会员介绍')), + ('daoqi_time', models.DateTimeField(verbose_name='会员到期时间')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '用户购买会员', + 'verbose_name_plural': '用户购买会员', + 'db_table': 'huiyuangoumai', + }, + ), + migrations.CreateModel( + name='Shangpin', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='商品ID')), + ('biaoqian', models.CharField(blank=True, max_length=45, null=True, verbose_name='商品标题')), + ('jiage', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='商品价格')), + ('kucun', models.PositiveIntegerField(default=0, verbose_name='商品库存')), + ('leixing_id', models.PositiveIntegerField(blank=True, null=True, verbose_name='商品类型ID')), + ('zhuanqu_id', models.PositiveIntegerField(blank=True, null=True, verbose_name='商品专区ID')), + ('zhenshi_xiaoliang', models.PositiveIntegerField(default=0, verbose_name='商品真实销量')), + ('duiwai_xiaoliang', models.PositiveIntegerField(default=0, verbose_name='商品对外销量')), + ('jieshao', models.TextField(blank=True, null=True, verbose_name='商品介绍')), + ('xiadan_xuzhi', models.TextField(blank=True, null=True, verbose_name='下单须知')), + ('guize_tupian', models.CharField(blank=True, max_length=500, null=True, verbose_name='规则图片URL')), + ('yaoqiuleixing', models.PositiveIntegerField(blank=True, null=True, verbose_name='抢单要求类型')), + ('huiyuan_id', models.CharField(blank=True, max_length=6, null=True, verbose_name='会员ID')), + ('yongjin', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='佣金要求')), + ('tupian_url', models.CharField(blank=True, max_length=500, null=True, verbose_name='商品图片URL')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')), + ], + options={ + 'verbose_name': '商品', + 'verbose_name_plural': '商品', + 'db_table': 'shangpin', + }, + ), + migrations.CreateModel( + name='ShangpinLeixing', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='类型ID')), + ('yaoqiuleixing', models.PositiveIntegerField(blank=True, null=True, verbose_name='抢单要求类型')), + ('huiyuan_id', models.CharField(blank=True, max_length=6, null=True, verbose_name='会员ID')), + ('yongjin', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='佣金要求')), + ('jieshao', models.CharField(blank=True, max_length=45, null=True, verbose_name='类型介绍')), + ('tupian_url', models.CharField(blank=True, max_length=500, null=True, verbose_name='类型图片URL')), + ], + options={ + 'verbose_name': '商品类型', + 'verbose_name_plural': '商品类型', + 'db_table': 'shangpin_leixing', + }, + ), + migrations.CreateModel( + name='ShangpinZhuanqu', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='专区ID')), + ('mingzi', models.CharField(blank=True, max_length=45, null=True, verbose_name='专区名称')), + ('leixing_id', models.PositiveIntegerField(verbose_name='类型ID')), + ], + options={ + 'verbose_name': '商品专区', + 'verbose_name_plural': '商品专区', + 'db_table': 'shangpin_zhuanqu', + }, + ), + migrations.CreateModel( + name='Czjilu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dingdan_id', models.CharField(db_index=True, max_length=32, unique=True, verbose_name='订单ID')), + ('huiyuan_id', models.CharField(blank=True, max_length=6, null=True, verbose_name='会员ID')), + ('zhuangtai', models.IntegerField(blank=True, null=True, verbose_name='订单状态')), + ('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')), + ('jine', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='订单金额')), + ('leixing', models.IntegerField(blank=True, null=True, verbose_name='充值类型,1:会员,2:押金,3:积分,4:商家')), + ('shuoming', models.CharField(blank=True, max_length=100, null=True, verbose_name='订单说明')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '充值记录订单', + 'verbose_name_plural': '充值记录订单', + 'db_table': 'czjilu', + 'indexes': [models.Index(fields=['dingdan_id'], name='czjilu_dingdan_39bbd0_idx'), models.Index(fields=['yonghuid'], name='czjilu_yonghui_a0555f_idx')], + }, + ), + migrations.CreateModel( + name='Gsfenhong', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dingdan_id', models.CharField(max_length=32, unique=True, verbose_name='订单ID')), + ('guanshi', models.CharField(db_index=True, max_length=7, verbose_name='管事ID')), + ('dashouid', models.CharField(db_index=True, max_length=7, unique=True, verbose_name='打手ID')), + ('shuoming', models.CharField(blank=True, max_length=100, null=True, verbose_name='订单说明')), + ('fenhong', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='订单分红金额')), + ('avatar', models.CharField(blank=True, max_length=500, null=True, verbose_name='打手头像')), + ('nicheng', models.CharField(max_length=50, null=True, verbose_name='打手昵称')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '管事分红记录', + 'verbose_name_plural': '管事分红记录', + 'db_table': 'gsfenhong', + 'indexes': [models.Index(fields=['guanshi'], name='gsfenhong_guanshi_dc440f_idx'), models.Index(fields=['dashouid'], name='gsfenhong_dashoui_5473ca_idx')], + }, + ), + ] diff --git a/shangpin/migrations/0002_alter_huiyuangoumai_unique_together_and_more.py b/shangpin/migrations/0002_alter_huiyuangoumai_unique_together_and_more.py new file mode 100644 index 0000000..4807b38 --- /dev/null +++ b/shangpin/migrations/0002_alter_huiyuangoumai_unique_together_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.27 on 2026-01-18 14:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangpin', '0001_initial'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='huiyuangoumai', + unique_together={('yonghu_id', 'huiyuan_id')}, + ), + migrations.AddIndex( + model_name='huiyuangoumai', + index=models.Index(fields=['yonghu_id'], name='huiyuangoum_yonghu__ba2138_idx'), + ), + migrations.AddIndex( + model_name='huiyuangoumai', + index=models.Index(fields=['huiyuan_id'], name='huiyuangoum_huiyuan_35fe1d_idx'), + ), + migrations.AddIndex( + model_name='huiyuangoumai', + index=models.Index(fields=['yonghu_id', 'huiyuan_id'], name='huiyuangoum_yonghu__21e6e1_idx'), + ), + ] diff --git a/shangpin/migrations/0003_shangpin_paixu_shangpinleixing_paixu_and_more.py b/shangpin/migrations/0003_shangpin_paixu_shangpinleixing_paixu_and_more.py new file mode 100644 index 0000000..d3ceedd --- /dev/null +++ b/shangpin/migrations/0003_shangpin_paixu_shangpinleixing_paixu_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.27 on 2026-02-07 19:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangpin', '0002_alter_huiyuangoumai_unique_together_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='shangpin', + name='paixu', + field=models.IntegerField(default=0, verbose_name='排序权重'), + ), + migrations.AddField( + model_name='shangpinleixing', + name='paixu', + field=models.IntegerField(default=0, verbose_name='排序权重'), + ), + migrations.AddField( + model_name='shangpinleixing', + name='shenhezhuangtai', + field=models.IntegerField(default=1, help_text='1=正常, 2=审核专用', verbose_name='审核状态'), + ), + migrations.AddField( + model_name='shangpinzhuanqu', + name='paixu', + field=models.IntegerField(default=0, verbose_name='排序权重'), + ), + ] diff --git a/shangpin/migrations/0004_shangpin_shenhezhuangtai_and_more.py b/shangpin/migrations/0004_shangpin_shenhezhuangtai_and_more.py new file mode 100644 index 0000000..5c71efd --- /dev/null +++ b/shangpin/migrations/0004_shangpin_shenhezhuangtai_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-02-09 15:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangpin', '0003_shangpin_paixu_shangpinleixing_paixu_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='shangpin', + name='shenhezhuangtai', + field=models.IntegerField(default=1, help_text='1=正常, 2=审核专用', verbose_name='审核状态'), + ), + migrations.AddField( + model_name='shangpinzhuanqu', + name='shenhezhuangtai', + field=models.IntegerField(default=1, help_text='1=正常, 2=审核专用', verbose_name='审核状态'), + ), + ] diff --git a/shangpin/migrations/0005_alter_gsfenhong_dashouid.py b/shangpin/migrations/0005_alter_gsfenhong_dashouid.py new file mode 100644 index 0000000..c339de3 --- /dev/null +++ b/shangpin/migrations/0005_alter_gsfenhong_dashouid.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-02-27 15:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangpin', '0004_shangpin_shenhezhuangtai_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='gsfenhong', + name='dashouid', + field=models.CharField(db_index=True, max_length=7, verbose_name='打手ID'), + ), + ] diff --git a/shangpin/migrations/0006_gsfenhong_huiyuan_id_huiyuan_zuzhangfc_duocifenhong.py b/shangpin/migrations/0006_gsfenhong_huiyuan_id_huiyuan_zuzhangfc_duocifenhong.py new file mode 100644 index 0000000..6f00224 --- /dev/null +++ b/shangpin/migrations/0006_gsfenhong_huiyuan_id_huiyuan_zuzhangfc_duocifenhong.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.27 on 2026-03-18 12:09 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangpin', '0005_alter_gsfenhong_dashouid'), + ] + + operations = [ + migrations.AddField( + model_name='gsfenhong', + name='huiyuan_id', + field=models.CharField(blank=True, max_length=6, null=True, verbose_name='会员ID'), + ), + migrations.AddField( + model_name='huiyuan', + name='zuzhangfc', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='组长分成'), + ), + migrations.CreateModel( + name='DuociFenhong', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('guanshi', models.CharField(db_index=True, max_length=7, verbose_name='管事ID')), + ('zuzhang', models.CharField(blank=True, db_index=True, max_length=7, null=True, verbose_name='组长ID')), + ('cishu', models.PositiveIntegerField(verbose_name='分红次数')), + ('guanshi_fenhong', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='管事分红金额')), + ('zuzhang_fenhong', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='组长分红金额')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('huiyuan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shangpin.huiyuan', verbose_name='会员')), + ], + options={ + 'verbose_name': '多次分红配置', + 'verbose_name_plural': '多次分红配置', + 'db_table': 'duoci_fenhong', + 'indexes': [models.Index(fields=['guanshi'], name='duoci_fenho_guanshi_b3de53_idx'), models.Index(fields=['zuzhang'], name='duoci_fenho_zuzhang_69997d_idx')], + 'unique_together': {('huiyuan', 'guanshi', 'cishu')}, + }, + ), + ] diff --git a/shangpin/migrations/0007_remove_duocifenhong_duoci_fenho_guanshi_b3de53_idx_and_more.py b/shangpin/migrations/0007_remove_duocifenhong_duoci_fenho_guanshi_b3de53_idx_and_more.py new file mode 100644 index 0000000..ef78070 --- /dev/null +++ b/shangpin/migrations/0007_remove_duocifenhong_duoci_fenho_guanshi_b3de53_idx_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.27 on 2026-03-19 13:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangpin', '0006_gsfenhong_huiyuan_id_huiyuan_zuzhangfc_duocifenhong'), + ] + + operations = [ + migrations.RemoveIndex( + model_name='duocifenhong', + name='duoci_fenho_guanshi_b3de53_idx', + ), + migrations.RemoveIndex( + model_name='duocifenhong', + name='duoci_fenho_zuzhang_69997d_idx', + ), + migrations.RenameField( + model_name='duocifenhong', + old_name='guanshi', + new_name='yonghuid', + ), + migrations.AlterUniqueTogether( + name='duocifenhong', + unique_together=set(), + ), + migrations.AlterField( + model_name='duocifenhong', + name='huiyuan', + field=models.CharField(max_length=10, verbose_name='会员ID'), + ), + migrations.AlterUniqueTogether( + name='duocifenhong', + unique_together={('huiyuan', 'yonghuid', 'cishu')}, + ), + migrations.AddIndex( + model_name='duocifenhong', + index=models.Index(fields=['yonghuid'], name='duoci_fenho_yonghui_db6a2f_idx'), + ), + migrations.RemoveField( + model_name='duocifenhong', + name='zuzhang', + ), + ] diff --git a/shangpin/migrations/0008_gsfenhong_zuzhang_fenhong_gsfenhong_zuzhang_id.py b/shangpin/migrations/0008_gsfenhong_zuzhang_fenhong_gsfenhong_zuzhang_id.py new file mode 100644 index 0000000..c3493dd --- /dev/null +++ b/shangpin/migrations/0008_gsfenhong_zuzhang_fenhong_gsfenhong_zuzhang_id.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-03-19 13:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangpin', '0007_remove_duocifenhong_duoci_fenho_guanshi_b3de53_idx_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='gsfenhong', + name='zuzhang_fenhong', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='组长分红金额'), + ), + migrations.AddField( + model_name='gsfenhong', + name='zuzhang_id', + field=models.CharField(blank=True, db_index=True, max_length=7, null=True, verbose_name='组长ID'), + ), + ] diff --git a/shangpin/migrations/0009_shangpin_ewai_dashou_fencheng_and_more.py b/shangpin/migrations/0009_shangpin_ewai_dashou_fencheng_and_more.py new file mode 100644 index 0000000..3f1401d --- /dev/null +++ b/shangpin/migrations/0009_shangpin_ewai_dashou_fencheng_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-03-22 15:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangpin', '0008_gsfenhong_zuzhang_fenhong_gsfenhong_zuzhang_id'), + ] + + operations = [ + migrations.AddField( + model_name='shangpin', + name='ewai_dashou_fencheng', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='打手额外分成金额'), + ), + migrations.AddField( + model_name='shangpin', + name='kaioi_ewai_dashou_fencheng', + field=models.BooleanField(default=False, verbose_name='是否开启打手额外分成'), + ), + ] diff --git a/shangpin/migrations/0010_shangpinleixing_is_cross_enabled.py b/shangpin/migrations/0010_shangpinleixing_is_cross_enabled.py new file mode 100644 index 0000000..9b7dedc --- /dev/null +++ b/shangpin/migrations/0010_shangpinleixing_is_cross_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-03-24 17:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangpin', '0009_shangpin_ewai_dashou_fencheng_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='shangpinleixing', + name='is_cross_enabled', + field=models.BooleanField(default=False, verbose_name='是否开启平台互通'), + ), + ] diff --git a/shangpin/migrations/0011_shangpin_dianpu_shangpin_dianpu_leixing_and_more.py b/shangpin/migrations/0011_shangpin_dianpu_shangpin_dianpu_leixing_and_more.py new file mode 100644 index 0000000..213c5dd --- /dev/null +++ b/shangpin/migrations/0011_shangpin_dianpu_shangpin_dianpu_leixing_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 4.2.27 on 2026-04-21 12:36 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangdian', '0001_initial'), + ('shangpin', '0010_shangpinleixing_is_cross_enabled'), + ] + + operations = [ + migrations.AddField( + model_name='shangpin', + name='dianpu', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='shangdian.dianpu', verbose_name='所属店铺'), + ), + migrations.AddField( + model_name='shangpin', + name='dianpu_leixing', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shangdian.shangpinleixingdianpu', verbose_name='店铺商品类型'), + ), + migrations.AddField( + model_name='shangpin', + name='fengjin_zhuangtai', + field=models.BooleanField(default=True, verbose_name='是否封禁'), + ), + migrations.AddField( + model_name='shangpin', + name='shangjia_zhuangtai', + field=models.BooleanField(default=True, verbose_name='是否上架'), + ), + migrations.AddField( + model_name='shangpin', + name='shenhe_zhuangtai', + field=models.BooleanField(default=False, help_text='True=审核通过,False=未审核', verbose_name='平台审核状态'), + ), + migrations.AddField( + model_name='shangpinzhuanqu', + name='dianpu', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='shangdian.dianpu', verbose_name='所属店铺'), + ), + migrations.AddField( + model_name='shangpinzhuanqu', + name='dianpu_leixing', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shangdian.shangpinleixingdianpu', verbose_name='店铺商品类型'), + ), + migrations.AddField( + model_name='shangpinzhuanqu', + name='fengjin_zhuangtai', + field=models.BooleanField(default=True, verbose_name='是否封禁'), + ), + migrations.AddField( + model_name='shangpinzhuanqu', + name='shangjia_zhuangtai', + field=models.BooleanField(default=True, verbose_name='是否上架'), + ), + ] diff --git a/shangpin/migrations/0012_shangpinmeiritongji.py b/shangpin/migrations/0012_shangpinmeiritongji.py new file mode 100644 index 0000000..10bc77c --- /dev/null +++ b/shangpin/migrations/0012_shangpinmeiritongji.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.27 on 2026-04-23 14:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangpin', '0011_shangpin_dianpu_shangpin_dianpu_leixing_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ShangpinMeiriTongji', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('riqi', models.DateField(db_index=True, verbose_name='统计日期')), + ('nian', models.PositiveSmallIntegerField(verbose_name='年份')), + ('yue', models.PositiveSmallIntegerField(verbose_name='月份')), + ('ri', models.PositiveSmallIntegerField(verbose_name='日')), + ('diandan_zongliang', models.PositiveIntegerField(default=0, verbose_name='点单总量')), + ('diandan_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='点单总额')), + ('chengjiao_zongliang', models.PositiveIntegerField(default=0, verbose_name='成交总量')), + ('chengjiao_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='成交总额')), + ('tuikuan_zongliang', models.PositiveIntegerField(default=0, verbose_name='退款总量')), + ('tuikuan_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='退款总额')), + ('pingtai_shouyi', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='平台收益')), + ('dianpu_zongshouyi', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='店铺总收益')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('shangpin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shangpin.shangpin', verbose_name='商品ID')), + ], + options={ + 'verbose_name': '商品每日统计', + 'verbose_name_plural': '商品每日统计', + 'db_table': 'shangpin_meiri_tongji', + 'indexes': [models.Index(fields=['shangpin', 'riqi'], name='shangpin_me_shangpi_d27e5c_idx'), models.Index(fields=['nian', 'yue', 'ri'], name='shangpin_me_nian_e3b409_idx')], + 'unique_together': {('shangpin', 'riqi')}, + }, + ), + ] diff --git a/shangpin/migrations/0013_shangpinleixing_bankuai.py b/shangpin/migrations/0013_shangpinleixing_bankuai.py new file mode 100644 index 0000000..e0802dc --- /dev/null +++ b/shangpin/migrations/0013_shangpinleixing_bankuai.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.27 on 2026-05-14 04:25 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dengji', '0003_alter_dashoubiaoxian_bankuai'), + ('shangpin', '0012_shangpinmeiritongji'), + ] + + operations = [ + migrations.AddField( + model_name='shangpinleixing', + name='bankuai', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dengji.bankuai', verbose_name='所属板块'), + ), + ] diff --git a/shangpin/migrations/0014_huiyuan_bankuai.py b/shangpin/migrations/0014_huiyuan_bankuai.py new file mode 100644 index 0000000..61dbc6d --- /dev/null +++ b/shangpin/migrations/0014_huiyuan_bankuai.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.27 on 2026-05-16 16:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dengji', '0010_kaohejilufeiyong'), + ('shangpin', '0013_shangpinleixing_bankuai'), + ] + + operations = [ + migrations.AddField( + model_name='huiyuan', + name='bankuai', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dengji.bankuai', verbose_name='所属板块'), + ), + ] diff --git a/shangpin/migrations/0015_gsfenhong_fenhong_leixing.py b/shangpin/migrations/0015_gsfenhong_fenhong_leixing.py new file mode 100644 index 0000000..777c6d4 --- /dev/null +++ b/shangpin/migrations/0015_gsfenhong_fenhong_leixing.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-06-12 01:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangpin', '0014_huiyuan_bankuai'), + ] + + operations = [ + migrations.AddField( + model_name='gsfenhong', + name='fenhong_leixing', + field=models.PositiveSmallIntegerField(choices=[(1, '会员分红'), (2, '押金分红'), (3, '商家订单分红')], default=1, verbose_name='分红类型'), + ), + ] diff --git a/shangpin/migrations/__init__.py b/shangpin/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shangpin/models.py b/shangpin/models.py new file mode 100644 index 0000000..fe5da00 --- /dev/null +++ b/shangpin/models.py @@ -0,0 +1,364 @@ +from django.db import models +from django.utils import timezone + + +class Shangpin(models.Model): + """商品表 - 100-1000条数据""" + id = models.AutoField(primary_key=True, verbose_name='商品ID') + biaoqian = models.CharField(max_length=45,null=True, blank=True, verbose_name='商品标题') # 15汉字 + jiage = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='商品价格') + kucun = models.PositiveIntegerField(default=0, verbose_name='商品库存') + leixing_id = models.PositiveIntegerField(null=True, blank=True,verbose_name='商品类型ID') # 直接存数字 + zhuanqu_id = models.PositiveIntegerField(null=True, blank=True,verbose_name='商品专区ID') # 直接存数字 + zhenshi_xiaoliang = models.PositiveIntegerField(default=0, verbose_name='商品真实销量') + duiwai_xiaoliang = models.PositiveIntegerField(default=0, verbose_name='商品对外销量') + jieshao = models.TextField(null=True, blank=True, verbose_name='商品介绍') + + # 新增字段 + xiadan_xuzhi = models.TextField(null=True, blank=True, verbose_name='下单须知') # 支持500字,可换行 + guize_tupian = models.CharField(max_length=500, null=True, blank=True, verbose_name='规则图片URL') + + yaoqiuleixing = models.PositiveIntegerField(null=True, blank=True, verbose_name='抢单要求类型') + huiyuan_id = models.CharField(max_length=6, null=True, blank=True, verbose_name='会员ID') + yongjin = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True,verbose_name='佣金要求') + + # 🔥 新增:是否开启打手额外分成 + kaioi_ewai_dashou_fencheng = models.BooleanField(default=False, verbose_name='是否开启打手额外分成') + # 🔥 新增:打手额外分成金额(开启时使用此值,否则按利率计算) + ewai_dashou_fencheng = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True,verbose_name='打手额外分成金额') + + # 原有图片URL + tupian_url = models.CharField(max_length=500, null=True, blank=True, verbose_name='商品图片URL') + + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间') + paixu = models.IntegerField(default=0, verbose_name='排序权重') + shenhezhuangtai = models.IntegerField(default=1, verbose_name='审核状态', help_text='1=正常, 2=审核专用') + + dianpu = models.ForeignKey( + 'shangdian.Dianpu', # 🔥 跨应用完整路径 + on_delete=models.CASCADE, + null=True, + blank=True, + db_index=True, + verbose_name='所属店铺' + ) + dianpu_leixing = models.ForeignKey( + 'shangdian.ShangpinLeixingDianpu', + on_delete=models.SET_NULL, + null=True, + blank=True, + db_index=True, + verbose_name='店铺商品类型' + ) + shangjia_zhuangtai = models.BooleanField(default=True, verbose_name='是否上架') + fengjin_zhuangtai = models.BooleanField(default=True, verbose_name='是否封禁') + shenhe_zhuangtai = models.BooleanField(default=False, verbose_name='平台审核状态',help_text='True=审核通过,False=未审核') + + class Meta: + db_table = 'shangpin' + verbose_name = '商品' + verbose_name_plural = '商品' + # 不要索引!数据量太小 + + def __str__(self): + return f"商品{self.id}:{self.biaoqian}" + + + + +class ShangpinLeixing(models.Model): + """商品类型表 - <10条数据""" + id = models.AutoField(primary_key=True, verbose_name='类型ID') + yaoqiuleixing = models.PositiveIntegerField(null=True, blank=True, verbose_name='抢单要求类型') + huiyuan_id = models.CharField(max_length=6, null=True, blank=True, verbose_name='会员ID') + yongjin = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='佣金要求') + jieshao = models.CharField(max_length=45, null=True, blank=True, verbose_name='类型介绍') # 15汉字 + tupian_url = models.CharField(max_length=500, null=True, blank=True, verbose_name='类型图片URL') # 用CharField + is_cross_enabled = models.BooleanField(default=False, verbose_name='是否开启平台互通') + paixu = models.IntegerField(default=0, verbose_name='排序权重') + shenhezhuangtai = models.IntegerField(default=1, verbose_name='审核状态', help_text='1=正常, 2=审核专用') + bankuai = models.ForeignKey( + 'dengji.Bankuai', + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name='所属板块' + ) + + class Meta: + db_table = 'shangpin_leixing' + verbose_name = '商品类型' + verbose_name_plural = '商品类型' + + def __str__(self): + return f"{self.jieshao}" + + +class ShangpinZhuanqu(models.Model): + """商品专区表 - <50条数据""" + id = models.AutoField(primary_key=True, verbose_name='专区ID') + mingzi = models.CharField(max_length=45, null=True, blank=True, verbose_name='专区名称') # 15汉字 + leixing_id = models.PositiveIntegerField(verbose_name='类型ID') # 直接存数字,不关联 + paixu = models.IntegerField(default=0, verbose_name='排序权重') + shenhezhuangtai = models.IntegerField(default=1, verbose_name='审核状态', help_text='1=正常, 2=审核专用') + + dianpu = models.ForeignKey( + 'shangdian.Dianpu', # 🔥 跨应用完整路径 + on_delete=models.CASCADE, + null=True, + blank=True, + db_index=True, + verbose_name='所属店铺' + ) + dianpu_leixing = models.ForeignKey( + 'shangdian.ShangpinLeixingDianpu', + on_delete=models.SET_NULL, + null=True, + blank=True, + db_index=True, + verbose_name='店铺商品类型' + ) + shangjia_zhuangtai = models.BooleanField(default=True, verbose_name='是否上架') + fengjin_zhuangtai = models.BooleanField(default=True, verbose_name='是否封禁') + + + class Meta: + db_table = 'shangpin_zhuanqu' + verbose_name = '商品专区' + verbose_name_plural = '商品专区' + + def __str__(self): + return f"{self.mingzi}" + + +class Huiyuan(models.Model): + huiyuan_id = models.CharField(max_length=6, unique=True, verbose_name='会员ID') + jieshao = models.CharField(max_length=30, verbose_name='会员介绍') + jtjieshao = models.TextField(null=True, blank=True, verbose_name='会员规则介绍') + jiage = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='会员价格') + guanshifc = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='管事分成') + + zuzhangfc = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='组长分成') + goumai_cishu = models.PositiveIntegerField(default=0, verbose_name='购买次数') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + bankuai = models.ForeignKey( + 'dengji.Bankuai', + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name='所属板块' + ) + + class Meta: + db_table = 'huiyuan' + verbose_name = '会员' + verbose_name_plural = '会员' + + def __str__(self): + return f"会员{self.huiyuan_id}:{self.jieshao}" + + +class Huiyuangoumai(models.Model): + id = models.AutoField(primary_key=True, verbose_name='购买记录ID') + huiyuan_id = models.CharField(max_length=6, db_index=True, verbose_name='会员ID') + yonghu_id = models.CharField(max_length=32, db_index=True, verbose_name='用户ID') + huiyuan_zhuangtai = models.PositiveIntegerField(default=1, verbose_name='会员状态') + jieshao = models.CharField(max_length=30,null=True,verbose_name='会员介绍') + daoqi_time = models.DateTimeField(verbose_name='会员到期时间') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'huiyuangoumai' + verbose_name = '用户购买会员' + verbose_name_plural = '用户购买会员' + # 添加这个联合唯一约束 + unique_together = [['yonghu_id', 'huiyuan_id']] + + # 顺便优化索引(可选的,建议加) + indexes = [ + models.Index(fields=['yonghu_id']), + models.Index(fields=['huiyuan_id']), + models.Index(fields=['yonghu_id', 'huiyuan_id']), # 复合索引加速查询 + ] + + def __str__(self): + return f"用户{self.yonghu_id}-会员{self.huiyuan_id}" + + def jiance_shifou_daoqi(self): + # 使用Django的timezone,它会自动处理时区 + now = timezone.now() + if now > self.daoqi_time: + # 过期了 + if self.huiyuan_zhuangtai != 0: + self.huiyuan_zhuangtai = 0 + self.save() + return True + else: + # 没过期 + if self.huiyuan_zhuangtai != 1: + self.huiyuan_zhuangtai = 1 + self.save() + return False + + +class Czjilu(models.Model): + dingdan_id = models.CharField(max_length=32, unique=True, db_index=True, verbose_name='订单ID') + huiyuan_id = models.CharField(max_length=6, null=True, blank=True, verbose_name='会员ID') + zhuangtai = models.IntegerField(null=True, blank=True, verbose_name='订单状态') + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') + jine = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='订单金额') + leixing = models.IntegerField(null=True, blank=True, verbose_name='充值类型,1:会员,2:押金,3:积分,4:商家') + shuoming = models.CharField(max_length=100, null=True, blank=True, verbose_name='订单说明') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + class Meta: + db_table = 'czjilu' + verbose_name = '充值记录订单' + verbose_name_plural = '充值记录订单' + # 为高频查询字段添加索引 + indexes = [ + models.Index(fields=['dingdan_id']), + models.Index(fields=['yonghuid']), + + + ] + + + + +class Gsfenhong(models.Model): + dingdan_id = models.CharField(max_length=32, unique=True, verbose_name='订单ID') + guanshi = models.CharField(max_length=7, db_index=True, verbose_name='管事ID') + dashouid = models.CharField(max_length=7, db_index=True,verbose_name='打手ID') + shuoming = models.CharField(max_length=100, null=True, blank=True, verbose_name='订单说明') + fenhong = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='订单分红金额') + avatar = models.CharField(max_length=500, null=True, blank=True, verbose_name='打手头像') + nicheng = models.CharField(max_length=50, null=True, verbose_name='打手昵称') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + zuzhang_id = models.CharField(max_length=7, null=True, blank=True, db_index=True, verbose_name='组长ID') + zuzhang_fenhong = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True,verbose_name='组长分红金额') + fenhong_leixing = models.PositiveSmallIntegerField( + default=1, + choices=[(1, '会员分红'), (2, '押金分红'), (3, '商家订单分红')], + verbose_name='分红类型' + ) + + huiyuan_id = models.CharField(max_length=6, null=True, blank=True, verbose_name='会员ID') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'gsfenhong' + verbose_name = '管事分红记录' + verbose_name_plural = '管事分红记录' + # 为高频查询字段添加索引 + indexes = [ + models.Index(fields=['guanshi']), + models.Index(fields=['dashouid']), + + ] + + + + + + +class DuociFenhong(models.Model): + """ + 多次分红配置表 + 用于配置不同次数下管事和组长的分红金额 + """ + huiyuan = models.CharField(max_length=10, verbose_name='会员ID') + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事ID') + + cishu = models.PositiveIntegerField(verbose_name='分红次数') + guanshi_fenhong = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='管事分红金额') + zuzhang_fenhong = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='组长分红金额') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'duoci_fenhong' + verbose_name = '多次分红配置' + verbose_name_plural = '多次分红配置' + unique_together = ('huiyuan', 'yonghuid', 'cishu') # 同一管事、同一会员、同一次数唯一 + indexes = [ + models.Index(fields=['yonghuid']), + + ] + + def __str__(self): + return f"会员{self.huiyuan_id} 用户{self.yonghuid} 次数{self.cishu}" + + + + + + + +class ShangpinMeiriTongji(models.Model): + """ + 商品每日统计表 + 按商品 + 日期维度记录点单、成交、退款及收益数据 + 日期精确到天,冗余年月日字段便于分组查询 + """ + shangpin = models.ForeignKey( + 'shangpin.Shangpin', + on_delete=models.CASCADE, + db_index=True, + verbose_name='商品ID' + ) + riqi = models.DateField(db_index=True, verbose_name='统计日期') + nian = models.PositiveSmallIntegerField(verbose_name='年份') + yue = models.PositiveSmallIntegerField(verbose_name='月份') + ri = models.PositiveSmallIntegerField(verbose_name='日') + + # 点单统计(下单即累加) + diandan_zongliang = models.PositiveIntegerField(default=0, verbose_name='点单总量') + diandan_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='点单总额') + + # 成交统计(订单完成时累加) + chengjiao_zongliang = models.PositiveIntegerField(default=0, verbose_name='成交总量') + chengjiao_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='成交总额') + + # 退款统计 + tuikuan_zongliang = models.PositiveIntegerField(default=0, verbose_name='退款总量') + tuikuan_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总额') + + # 收益统计(新增) + pingtai_shouyi = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='平台收益') + dianpu_zongshouyi = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='店铺总收益') + + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'shangpin_meiri_tongji' + verbose_name = '商品每日统计' + verbose_name_plural = verbose_name + # 每天每个商品唯一记录 + unique_together = [['shangpin', 'riqi']] + indexes = [ + models.Index(fields=['shangpin', 'riqi']), + models.Index(fields=['nian', 'yue', 'ri']), + ] + + def __str__(self): + return f"{self.shangpin.biaoqian} - {self.riqi}" + + def clean(self): + """保存前自动填充年月日冗余字段""" + if self.riqi: + self.nian = self.riqi.year + self.yue = self.riqi.month + self.ri = self.riqi.day + super().clean() + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) \ No newline at end of file diff --git a/shangpin/tests.py b/shangpin/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/shangpin/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/shangpin/urls.py b/shangpin/urls.py new file mode 100644 index 0000000..95f9c61 --- /dev/null +++ b/shangpin/urls.py @@ -0,0 +1,74 @@ +# shangpin/urls.py +from django.urls import path +from django.views.decorators.csrf import csrf_exempt +from .views import ShangpinHuoquView, ShangpinXiangqingHuoquView, DashouHuiyuanList, \ + YajinGoumai, YajinHuitiao, YajinShibai, YajinLunxun, JifenBuchong, JifenHuitiao, JifenShibai, \ + JifenLunxun, HuiyuanGoumai, HuiyuanHuitiao, HuiyuanShibai, HuiyuanLunxun, ShangjiaChenggong, \ + ShangjiaChongzhi, ShangjiaHuitiao, ShangjiaShibai, GuanshiFenhongListAPI, AdGetAllDataView, ShangpinLeixingFabuView, \ + ShangpinZhuanquFabuView, ShangpinFabuView, GuizeTupianShangchuanView, AdHuiyuanHqView, HuiyuanXiugaiView, \ + AdHuiyuanFbView, AdShangpinHuQu, AdLeixingXiuGai, AdLunboXiuGai, AdZhuanquXiuGai, AdGonggaoXiuGai, \ + AdShangpinGuizeXiuGai, AdShangpinXiuGai, AdShangpinNeiRongHuoQu,\ + HuiyuanTypeListView, HuiyuanRecordListView, ZuzhangFenhongView, CzhqdyView, DsqrgmdhView + +urlpatterns = [ + #path('shangpinhuoqu/', ShangpinHuoquView.as_view(), name='获取商品数据商品类型和专区'), + path('xiangqinghuoqu/', ShangpinXiangqingHuoquView.as_view(), name='获取商品详情数据'), + path('dshyhq', DashouHuiyuanList.as_view(), name='打手充值获取会员信息'), + path('yajingoumai', YajinGoumai.as_view(), name='打手购买押金'), + #path('yajinhuitiao', YajinHuitiao.as_view(), name='押金支付回调'), + path('yajinhuitiao', csrf_exempt(YajinHuitiao.as_view()), name='押金支付回调'), + path('yajinshibai', YajinShibai.as_view(), name='押金支付失败'), + path('yajinlunxun', YajinLunxun.as_view(), name='押金支付轮询'), + # 积分相关接口 + path('jifenbc', JifenBuchong.as_view(), name='积分补充参数生成'), + #path('jifenhuitiao', JifenHuitiao.as_view(), name='积分充值回调'), + path('jifenhuitiao', csrf_exempt(JifenHuitiao.as_view()), name='积分充值回调'), + path('jifenshibai', JifenShibai.as_view(), name='积分支付失败'), + path('jifenlunxun', JifenLunxun.as_view(), name='积分支付轮询'), + + # 会员相关接口 + path('huiyuangm', HuiyuanGoumai.as_view(), name='会员购买参数生成'), + #path('huiyuanhuitiao', HuiyuanHuitiao.as_view(), name='会员支付回调'), + path('huiyuanhuitiao', csrf_exempt(HuiyuanHuitiao.as_view()), name='会员支付回调'), + path('huiyuanshibai', HuiyuanShibai.as_view(), name='会员支付失败'), + path('huiyuanlx', HuiyuanLunxun.as_view(), name='会员支付轮询'), + path('sjcz', ShangjiaChongzhi.as_view(), name='商家充值支付参数获取'), # 商家充值支付参数 + path('sjhuitiao', csrf_exempt(ShangjiaHuitiao.as_view()), name='商家支付官方回调'), + #path('sjhuitiao', ShangjiaHuitiao.as_view(), name='商家支付官方回调'), # 商家支付回调 + path('sjshibai', ShangjiaShibai.as_view(), name='商家支付失败'), # 商家支付失败处理 + path('sjcg', ShangjiaChenggong.as_view(), name='商家支付成功'), # 商家支付成功轮询 + path('fenhonghq', GuanshiFenhongListAPI.as_view(), name='管事获取打手充值记录'), + path('adhqlx', AdGetAllDataView.as_view(), name='管理员获取商品类型会员类型专区类型'), + path('leixingfabu', ShangpinLeixingFabuView.as_view(), name='添加商品类型'), + # 商品专区相关接口 + path('zhuanqufabu', ShangpinZhuanquFabuView.as_view(), name='添加商品专区'), + # 商品发布相关接口 + path('shangpinsc', ShangpinFabuView.as_view(), name='商品发布'), + # 规则图片上传接口 + path('guizesc', GuizeTupianShangchuanView.as_view(), name='发布商品规则'), + path('adhuiyuanhq', AdHuiyuanHqView.as_view(), name='管理员获取会员信息'), + path('huiyuanxiugai', HuiyuanXiugaiView.as_view(), name='修改会员信息'), # 修改会员 + path('adhuiyuanfb', AdHuiyuanFbView.as_view(), name='发布会员'), # 发布会员 + path('adsphq', AdShangpinHuQu.as_view(), name='管理员商品管理页面数据获取'), + path('adggxg', AdGonggaoXiuGai.as_view(), name='管理员公告修改'), + path('adzqxg', AdZhuanquXiuGai.as_view(), name='管理员专区修改'), + path('adlunboxg', AdLunboXiuGai.as_view(), name='管理员轮播图修改'), + path('adleixingxg', AdLeixingXiuGai.as_view(), name='管理员商品类型修改'), + path('adspnrhq', AdShangpinNeiRongHuoQu.as_view(), name='管理员商品详情获取'), + + # 2. 更改商品数据接口(修改/删除) + path('adspxqxg',AdShangpinXiuGai.as_view(), name='管理员商品详情修改'), + + # 3. 规则图片更换接口 + path('adspgzxg',AdShangpinGuizeXiuGai.as_view(), name='管理员商品规则修改'), + path('adhyjllxcx',HuiyuanTypeListView.as_view(), name='管理员查询会员记录获取会员类型'), + path('adhyjlcxhq',HuiyuanRecordListView.as_view(), name='管理员查询会员记录'), + + + + + path('zuzhangfenhong', ZuzhangFenhongView.as_view(), name='组长分红记录获取'), + path('czhqdy', CzhqdyView.as_view(), name='充值打手抵押查询'), + path('dsqrgmdh', DsqrgmdhView.as_view(), name='充值抵扣'), + +] \ No newline at end of file diff --git a/shangpin/utils.py b/shangpin/utils.py new file mode 100644 index 0000000..4bc52ab --- /dev/null +++ b/shangpin/utils.py @@ -0,0 +1,113 @@ +# tongji/utils.py 或 shangpin/utils.py + +import logging +from datetime import date +from decimal import Decimal +from django.db import transaction +from django.db.models import F +from .models import ShangpinMeiriTongji +from shangpin.models import Shangpin + +logger = logging.getLogger(__name__) + + +def update_shangpin_daily_stat( + shangpin_id, + caozuo_leixing, + dingdan_jiage=Decimal('0.00'), + pingtai_shouyi=Decimal('0.00'), + dianpu_zongshouyi=Decimal('0.00') +): + """ + 原子更新商品每日统计数据(高并发安全,无店铺关联) + + 参数: + shangpin_id: 商品ID (int) + caozuo_leixing: 操作类型 + 1 = 下单(点单) + 2 = 结算(成交) + 3 = 退款 + dingdan_jiage: 订单金额(商品价格),默认0.00 + pingtai_shouyi: 平台收益金额,默认0.00 + dianpu_zongshouyi: 店铺总收益金额,默认0.00 + + 注意: + - 金额若小于0则自动设为0 + - 日期自动取当天 + - 使用 select_for_update 配合事务,保证并发时数据绝对准确 + - 若商品不存在,仅记录日志不中断主流程 + """ + # 参数归一化与合法性处理 + dingdan_jiage = Decimal(str(dingdan_jiage)) if dingdan_jiage else Decimal('0.00') + pingtai_shouyi = Decimal(str(pingtai_shouyi)) if pingtai_shouyi else Decimal('0.00') + dianpu_zongshouyi = Decimal(str(dianpu_zongshouyi)) if dianpu_zongshouyi else Decimal('0.00') + + if dingdan_jiage < 0: + dingdan_jiage = Decimal('0.00') + if pingtai_shouyi < 0: + pingtai_shouyi = Decimal('0.00') + if dianpu_zongshouyi < 0: + dianpu_zongshouyi = Decimal('0.00') + + today = date.today() + + # 校验商品是否存在(不加锁,仅校验) + if not Shangpin.objects.filter(id=shangpin_id).exists(): + logger.error(f"商品ID {shangpin_id} 不存在,统计更新失败") + return + + # 开启事务,使用行锁确保并发安全 + with transaction.atomic(): + # 获取或创建统计记录,并锁定该行 + tongji, created = ShangpinMeiriTongji.objects.select_for_update().get_or_create( + shangpin_id=shangpin_id, + riqi=today, + defaults={ + 'nian': today.year, + 'yue': today.month, + 'ri': today.day, + 'diandan_zongliang': 0, + 'diandan_zonge': Decimal('0.00'), + 'chengjiao_zongliang': 0, + 'chengjiao_zonge': Decimal('0.00'), + 'tuikuan_zongliang': 0, + 'tuikuan_zonge': Decimal('0.00'), + 'pingtai_shouyi': Decimal('0.00'), + 'dianpu_zongshouyi': Decimal('0.00'), + } + ) + + # 根据操作类型构建更新字段(使用 F 表达式累加) + update_fields = {} + + if caozuo_leixing == 1: # 下单 + update_fields['diandan_zongliang'] = F('diandan_zongliang') + 1 + update_fields['diandan_zonge'] = F('diandan_zonge') + dingdan_jiage + + elif caozuo_leixing == 2: # 结算(成交) + update_fields['chengjiao_zongliang'] = F('chengjiao_zongliang') + 1 + update_fields['chengjiao_zonge'] = F('chengjiao_zonge') + dingdan_jiage + # 收益直接累加传入值 + update_fields['pingtai_shouyi'] = F('pingtai_shouyi') + pingtai_shouyi + update_fields['dianpu_zongshouyi'] = F('dianpu_zongshouyi') + dianpu_zongshouyi + + elif caozuo_leixing == 3: # 退款 + update_fields['tuikuan_zongliang'] = F('tuikuan_zongliang') + 1 + update_fields['tuikuan_zonge'] = F('tuikuan_zonge') + dingdan_jiage + # 退款是否应扣减收益?根据业务需求决定,此处暂不处理收益字段 + + else: + logger.warning(f"未知的操作类型: {caozuo_leixing}") + return + + # 原子更新字段 + if update_fields: + for field, value in update_fields.items(): + setattr(tongji, field, value) + tongji.save(update_fields=list(update_fields.keys())) + + logger.info( + f"商品统计更新: 商品ID={shangpin_id}, 操作类型={caozuo_leixing}, " + f"订单金额={dingdan_jiage}, 平台收益={pingtai_shouyi}, " + f"店铺收益={dianpu_zongshouyi}, 日期={today}" + ) \ No newline at end of file diff --git a/shangpin/views.py b/shangpin/views.py new file mode 100644 index 0000000..fa8f3f9 --- /dev/null +++ b/shangpin/views.py @@ -0,0 +1,8453 @@ +# 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 logging +from rest_framework.permissions import AllowAny # 新增这行 + +# 配置日志 +logger = logging.getLogger(__name__) + + + +class ShangpinHuoquView(APIView): + """ + 商品数据获取接口 + 一次性返回商品类型、商品专区、商品列表 + """ + + # 应用节流(限制频率) + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + + def post(self, request): + """ + 处理POST请求,返回所有商品相关数据 + """ + try: + logger.info("收到商品数据获取请求") + + # 1. 查询商品类型(只查询正常状态的类型,并排序) + # 正常运营:只显示 shenhezhuangtai=2 的商品类型 + leixing_queryset = ShangpinLeixing.objects.filter( + shenhezhuangtai=1 # 只取正常状态 + ).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 + }) + + # 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, + shenhezhuangtai=1 # 🔴【添加】只筛选审核状态为2的专区 + ).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, + shenhezhuangtai=1 # 🔴【添加】只筛选审核状态为2的商品 + ).values( + 'id', 'biaoqian', 'jiage', 'leixing_id', + 'zhuanqu_id', 'tupian_url', 'duiwai_xiaoliang', '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请求,返回所有商品相关数据 + """ + 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): + """ + 商品详情接口 - 极简版 + 假设所有字段都存在,直接返回 + """ + + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + + def post(self, request): + try: + shangpin_id = request.data.get('shangpin_id') + + if not shangpin_id: + return Response({ + "code": 1, + "msg": "商品ID不能为空", + "data": None + }, status=400) + + # 直接查询,数据量小,性能没问题 + shangpin = Shangpin.objects.filter(id=shangpin_id).first() + + if not shangpin: + return Response({ + "code": 2, + "msg": "商品不存在", + "data": None + }, status=404) + + # 直接构建数据,不处理空值 + data = { + "tupian": shangpin.tupian_url, + "biaoti": shangpin.biaoqian, + "jiage": shangpin.jiage, + "xiangqing": shangpin.jieshao, + "xiadanxuzhi": shangpin.xiadan_xuzhi, + "guize": shangpin.guize_tupian, + "xiaoliang": shangpin.duiwai_xiaoliang, + "kucun": shangpin.kucun, + } + + return Response({ + "code": 0, + "msg": "success", + "data": data + }) + + except Exception as e: + logger.error(f"接口异常: {e}") + return Response({ + "code": 500, + "msg": "服务器错误", + "data": None + }, status=500) + + + +# 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): + """ + 获取打手会员商品列表 + 路径:/shangpin/dshyhq + 请求方式:POST + 权限:JWT认证 + 返回:会员商品列表,字段对应前端需求 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 检查用户是否是打手(通过反向查询) + try: + dashou = request.user.dashou_profile + except UserDashou.DoesNotExist: + logger.warning(f"用户{request.user.yonghuid}不是打手,尝试访问打手接口") + return Response({ + 'code': 400, + 'message': '用户不是打手,无法访问此接口' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 检查打手账号状态 + if dashou.zhanghaozhuangtai != 1: + logger.warning(f"打手{request.user.yonghuid}账号状态异常: {dashou.zhanghaozhuangtai}") + return Response({ + 'code': 400, + 'message': '账号状态异常,无法购买会员' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 查询所有会员数据(ORM查询,确保性能) + huiyuan_queryset = Huiyuan.objects.all().only( + 'huiyuan_id', # 会员ID + 'jieshao', # 会员名字(注意:jieshao字段实际存储的是会员名字) + 'jtjieshao', # 会员具体介绍 + 'jiage' # 会员价格 + ).order_by('jiage') # 按价格排序 + + # 4. 构建返回数据列表 + huiyuan_list = [] + for huiyuan in huiyuan_queryset: + huiyuan_list.append({ + 'id': huiyuan.huiyuan_id, # 会员ID(对应前端id) + 'mingzi': huiyuan.jieshao, # 会员名字(对应前端mingzi) + 'jiage': float(huiyuan.jiage), # 会员价格(转为浮点数) + 'jieshao': huiyuan.jtjieshao # 会员介绍(对应前端jieshao) + }) + + # 5. 返回数据 + return Response({ + 'code': 200, + 'message': '获取会员列表成功', + 'data': huiyuan_list # 对应前端接收的列表 + }, 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) + + +# views.py +import random +import string +import time +import hashlib +import xml.etree.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): + """ + 押金充值支付参数接口 + 路径:/shangpin/yajingoumai + 请求方式:POST + 权限:JWT认证 + 参数:jine(金额,1-10000元) + 返回:支付参数(payParams)和订单ID(dingdanid) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 验证用户身份和状态 + try: + dashou = request.user.dashou_profile + except UserDashou.DoesNotExist: + return Response({ + 'code': 400, + 'message': '用户不是打手,无法充值押金' + }, status=status.HTTP_400_BAD_REQUEST) + + if dashou.zhanghaozhuangtai != 1: + return Response({ + 'code': 400, + 'message': '账号状态异常,无法进行充值' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 获取并验证金额参数 + jine = request.data.get('jine') + if not jine: + return Response({ + 'code': 400, + 'message': '请填写充值金额' + }, status=status.HTTP_400_BAD_REQUEST) + + try: + jine = int(jine) + except (ValueError, TypeError): + return Response({ + 'code': 400, + 'message': '金额格式错误,请填写1-10000的整数' + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证金额范围 + if jine < 1 or jine > 10000: + return Response({ + 'code': 400, + 'message': '充值金额必须在1-10000元之间' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 生成订单ID(使用你提供的算法,前缀改为CzYJ) + timestamp_ms = int(time.time() * 1000) + timestamp_ns = int(time.time_ns() // 1000) % 1000000 + random_num = random.randint(0, 99) + + dingdanid = f"CzYJ{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" + + # 4. 创建充值记录订单 + chongzhi_jilu = Czjilu.objects.create( + dingdan_id=dingdanid, + zhuangtai=9, # 未支付状态 + yonghuid=request.user.yonghuid, + jine=jine, + leixing=2, # 充值类型:2=押金 + shuoming=settings.YAJIN_PAY_DESCRIPTION + ) + + logger.info(f"创建押金充值订单: 用户{request.user.yonghuid}, 订单{dingdanid}, 金额{jine}元") + + # 5. 生成微信支付参数 + try: + pay_params = self.generate_wechat_pay_params( + dingdanid=dingdanid, + jine=jine, + openid=request.user.openid, # 直接从request.user获取openid + pay_type='yajin' + ) + 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) + + # 6. 返回支付参数和订单ID + return Response({ + 'code': 200, + 'message': '支付参数生成成功', + 'payParams': pay_params, # 支付参数(前端接收为payParams) + 'dingdanid': dingdanid # 订单ID(前端接收为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 == 'yajin': + PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION + NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL + elif pay_type == 'jifen': + PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION + NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL + elif pay_type == 'huiyuan': + PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION + NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL + else: + raise Exception('未知的支付类型') + + # 生成随机字符串 + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + # 金额转换(元转分) + total_fee = int(jine * 100) + + # 交易类型(小程序支付使用JSAPI) + trade_type = 'JSAPI' + + # 获取客户端IP + user_ip = self.get_client_ip() + + # 1. 第一次签名(统一下单接口) + 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 # 小程序支付必须传openid + } + + # 按照参数名ASCII码从小到大排序 + 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}' + + # MD5签名 + 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} +""" + + # 调用微信统一下单接口(官方地址) + response = requests.post( + 'https://api.mch.weixin.qq.com/pay/unifiedorder', + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + timeout=10 # 设置超时 + ) + + # 解析返回的XML + 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 + prepay_id = root.find('prepay_id').text + + # 2. 第二次签名(小程序支付参数) + 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' + + # 生成支付签名(注意:这里key的大小写和字段名与第一次不同) + pay_sign_data = { + 'appId': APPID, # 注意:这里是appId,不是appid + 'timeStamp': timestamp, # 注意:这里是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, # 对应前端:appId + 'timeStamp': timestamp, # 对应前端:timeStamp + 'nonceStr': nonce_str, # 对应前端:nonceStr + 'package': package, # 对应前端:package + 'signType': sign_type, # 对应前端:signType + 'paySign': pay_sign # 对应前端:paySign + } + else: + # 获取错误信息 + err_code = root.find('err_code') + err_code_des = root.find('err_code_des') + return_msg = root.find('return_msg') + + 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}" + if return_msg is not None: + error_details += f", return_msg: {return_msg.text}" + + raise Exception(f"微信支付下单失败: {error_details}") + + def get_client_ip(self): + """获取客户端IP(从DRF的request对象中获取)""" + 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' + + +# views.py +from django.views import View +from django.http import HttpResponse +from yonghu.models import UserDashou, UserMain +from shangpin.models import Czjilu +import xml.etree.ElementTree as ET +import hashlib +from django.conf import settings +import logging + +logger = logging.getLogger(__name__) + + + +class YajinHuitiao(View): + """ + 押金支付回调接口 + 路径:/shangpin/yajinhuitiao + 注意:这个接口不需要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. 检查订单状态是否为未支付(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. 更新打手押金 + try: + # 通过用户ID查找打手(高效查询) + 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']) + + logger.info(f"打手押金更新成功: 用户{order.yonghuid}, 增加{order_amount_yuan}元, 新押金{dashou.yajin}") + + # 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: + logger.info(f"押金分红跳过: 打手{order.yonghuid}无邀请管事") + elif Gsfenhong.objects.filter(dingdan_id=order.dingdan_id).exists(): + logger.info(f"押金分红已处理: 订单{order.dingdan_id}") + else: + lilu_obj = Lilubiao.objects.filter(fadanpingtai='12').first() + rate = lilu_obj.lilu if lilu_obj and lilu_obj.lilu is not None else Decimal('0') + if rate <= 0: + logger.info(f"押金分红跳过: 费率未配置或为0") + else: + recharge_amount = Decimal(str(order_amount_yuan)) + fenhong_jine = (recharge_amount * rate).quantize(Decimal('0.01')) + if fenhong_jine > 0: + with transaction.atomic(): + guanshi = UserGuanshi.objects.select_for_update().get( + user__yonghuid=guanshi_id + ) + guanshi.yue += fenhong_jine + guanshi.chongzhifenrun += fenhong_jine + guanshi.save(update_fields=['yue', 'chongzhifenrun']) + + user_main = dashou.user + Gsfenhong.objects.create( + dingdan_id=order.dingdan_id, + guanshi=guanshi_id, + dashouid=order.yonghuid, + shuoming='押金分红', + fenhong=fenhong_jine, + avatar=user_main.avatar, + nicheng=dashou.nicheng or '未知打手', + fenhong_leixing=2, + ) + logger.info( + f"押金分红成功: 订单{order.dingdan_id}, 管事{guanshi_id}, " + 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, + amount=fenhong_jine, + ) + except Exception as e: + logger.error(f"押金分红管事日统计更新失败: {e}") + except UserGuanshi.DoesNotExist: + logger.warning(f"押金分红跳过: 管事{guanshi_id}不存在") + except Exception as e: + logger.error(f"押金分红处理失败: {str(e)}", exc_info=True) + + except UserDashou.DoesNotExist: + # 如果找不到打手,记录错误,但依然返回成功(避免微信重复回调) + logger.error(f"打手信息不存在: 用户{order.yonghuid}") + return self.wechat_response('SUCCESS', 'OK') + except Exception as e: + logger.error(f"更新打手押金失败: {str(e)}", exc_info=True) + 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的收支记录 + from dingdan.utils import update_daily_income + update_daily_income(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"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") + + except Exception as e: + # 收支记录更新失败不影响主流程,只记录日志 + logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) + + # ==== 添加结束 ==== + + # 9. 返回成功响应给微信 + 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): + """验证微信支付签名""" + 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 YajinShibai(APIView): + """ + 押金支付失败处理接口 + 路径:/shangpin/yajinshibai + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:删除结果 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的订单) + try: + order = Czjilu.objects.get( + dingdan_id=dingdanid, + yonghuid=request.user.yonghuid + ) + except Czjilu.DoesNotExist: + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单状态是否为未支付(9) + if order.zhuangtai == 9: + # 删除订单 + order.delete() + logger.info(f"用户{request.user.yonghuid}删除未支付订单: {dingdanid}") + + return Response({ + 'code': 200, + 'message': '订单已删除' + }, status=status.HTTP_200_OK) + else: + return Response({ + 'code': 400, + 'message': '订单状态不是未支付,无法删除' + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"押金支付失败处理异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +# views.py +class YajinLunxun(APIView): + """ + 押金支付成功轮询接口 + 路径:/shangpin/yajinlunxun + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:订单状态和当前押金 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的订单) + try: + order = Czjilu.objects.get( + dingdan_id=dingdanid, + yonghuid=request.user.yonghuid + ) + except Czjilu.DoesNotExist: + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单状态是否为已支付(3) + if order.zhuangtai != 3: + logger.info(f"订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") + return Response({ + 'code': 400, + 'message': '订单尚未支付完成' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 获取打手押金(通过反向查询,最快捷的方式) + try: + dashou = request.user.dashou_profile + yajin = float(dashou.yajin) # 转为浮点数方便前端处理 + + logger.info(f"轮询成功: 订单{dingdanid}已支付,用户{request.user.yonghuid}押金为{yajin}元") + + return Response({ + 'code': 200, + 'message': '支付流程全部完成', + 'yajin': yajin # 返回当前押金(前端需要更新全局变量) + }, status=status.HTTP_200_OK) + + except UserDashou.DoesNotExist: + logger.error(f"打手信息不存在: 用户{request.user.yonghuid}") + return Response({ + 'code': 400, + 'message': '打手信息不存在' + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"押金轮询异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +# views.py +class JifenBuchong(APIView): + """ + 积分补充支付参数接口 + 路径:/shangpin/jifenbc + 请求方式:POST + 权限:JWT认证 + 参数:无(固定5元) + 返回:支付参数(payParams)和订单ID(dingdanid) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 验证用户身份和状态 + try: + dashou = request.user.dashou_profile + except UserDashou.DoesNotExist: + return Response({ + 'code': 400, + 'message': '用户不是打手,无法补充积分' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 检查打手账号状态 + if dashou.zhanghaozhuangtai != 1: + return Response({ + 'code': 400, + 'message': '账号状态异常,无法进行操作' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查积分是否已满(满分10分) + if dashou.jifen >= 10: + return Response({ + 'code': 400, + 'message': '积分已满,无需补充' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 生成订单ID(前缀CzJF表示积分充值) + timestamp_ms = int(time.time() * 1000) + timestamp_ns = int(time.time_ns() // 1000) % 1000000 + random_num = random.randint(0, 99) + + dingdanid = f"CzJF{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" + + # 5. 创建积分充值记录订单(固定5元) + jine = 5.00 # 固定5元 + chongzhi_jilu = Czjilu.objects.create( + dingdan_id=dingdanid, + zhuangtai=9, # 未支付状态 + yonghuid=request.user.yonghuid, + jine=jine, + leixing=3, # 充值类型:3=积分 + shuoming=settings.JIFEN_PAY_DESCRIPTION + ) + + logger.info(f"创建积分充值订单: 用户{request.user.yonghuid}, 订单{dingdanid}, 金额{jine}元") + + # 6. 生成微信支付参数(完整版) + try: + pay_params = self.generate_wechat_pay_params( + dingdanid=dingdanid, + jine=jine, + openid=request.user.openid, + pay_type='jifen' + ) + 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) + + # 7. 返回支付参数和订单ID + return Response({ + 'code': 200, + 'message': '支付参数生成成功', + 'payParams': pay_params, # 对应前端payParams + 'dingdanid': 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): + """生成微信支付参数(完整版,无省略)""" + # 微信支付配置 - 从settings中获取 + APPID = settings.WEIXIN_APPID + MCHID = settings.WEIXIN_MCHID + KEY = settings.WEIXIN_SHANGHUMIYAO + + # 根据支付类型获取对应的配置 + if pay_type == 'yajin': + PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION + NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL + elif pay_type == 'jifen': + PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION + NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL + elif pay_type == 'huiyuan': + PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION + NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL + else: + raise Exception('未知的支付类型') + + # 生成随机字符串 + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + # 金额转换(元转分) + total_fee = int(jine * 100) + + # 交易类型(小程序支付使用JSAPI) + trade_type = 'JSAPI' + + # 获取客户端IP + user_ip = self.get_client_ip() + + # 1. 第一次签名(统一下单接口) + 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 # 小程序支付必须传openid + } + + # 按照参数名ASCII码从小到大排序 + 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}' + + # MD5签名 + 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} +""" + + # 调用微信统一下单接口(官方地址) + response = requests.post( + 'https://api.mch.weixin.qq.com/pay/unifiedorder', + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + timeout=10 # 设置超时 + ) + + # 解析返回的XML + 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 + prepay_id = root.find('prepay_id').text + + # 2. 第二次签名(小程序支付参数) + 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' + + # 生成支付签名(注意:这里key的大小写和字段名与第一次不同) + pay_sign_data = { + 'appId': APPID, # 注意:这里是appId,不是appid + 'timeStamp': timestamp, # 注意:这里是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, # 对应前端:appId + 'timeStamp': timestamp, # 对应前端:timeStamp + 'nonceStr': nonce_str, # 对应前端:nonceStr + 'package': package, # 对应前端:package + 'signType': sign_type, # 对应前端:signType + 'paySign': pay_sign # 对应前端:paySign + } + else: + # 获取错误信息 + err_code = root.find('err_code') + err_code_des = root.find('err_code_des') + return_msg = root.find('return_msg') + + 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}" + if return_msg is not None: + error_details += f", return_msg: {return_msg.text}" + + 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') + return ip if ip else '127.0.0.1' + + +# views.py +class JifenHuitiao(View): + """ + 积分支付回调接口 + 路径:/shangpin/jifenhuitiao + 注意:这个接口不需要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. 检查订单状态是否为未支付(9) + if order.zhuangtai != 9: + logger.warning(f"积分订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") + return self.wechat_response('SUCCESS', 'OK') + + # 6. 验证金额(固定5元) + wechat_amount_yuan = total_fee / 100 # 转换为元 + order_amount_yuan = float(order.jine) # 订单金额(元) + + # 验证是否是5元(允许1分钱误差) + if abs(wechat_amount_yuan - 5.00) > 0.01: + logger.error(f"积分金额错误: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") + 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 + + 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) + + # ==== 添加结束 ==== + + # 7. 更新订单状态为已支付(3) + order.zhuangtai = 3 + order.save(update_fields=['zhuangtai']) + logger.info(f"积分订单状态更新成功: {order.dingdan_id} -> 3") + + # 8. 更新打手积分 + try: + # 通过用户ID查找打手 + dashou = UserDashou.objects.get(user__yonghuid=order.yonghuid) + + # 检查积分是否小于10 + if dashou.jifen < 10: + # 增加5积分,但不超过10 + new_jifen = dashou.jifen + 5 + dashou.jifen = min(new_jifen, 10) # 确保不超过10 + dashou.save(update_fields=['jifen']) + + logger.info(f"打手积分更新成功: 用户{order.yonghuid}, 增加5积分, 新积分{dashou.jifen}") + + + + + else: + logger.warning(f"打手积分已满: 用户{order.yonghuid}, 当前积分{dashou.jifen}") + + except UserDashou.DoesNotExist: + logger.error(f"打手信息不存在: 用户{order.yonghuid}") + return self.wechat_response('SUCCESS', 'OK') + except Exception as e: + logger.error(f"更新打手积分失败: {str(e)}", exc_info=True) + return self.wechat_response('FAIL', '更新积分失败') + + # 9. 返回成功响应给微信 + 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 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 JifenShibai(APIView): + """ + 积分支付失败处理接口 + 路径:/shangpin/jifenshibai + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:删除结果 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的订单) + try: + order = Czjilu.objects.get( + dingdan_id=dingdanid, + yonghuid=request.user.yonghuid + ) + except Czjilu.DoesNotExist: + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单类型是否为积分(3) + if order.leixing != 3: + return Response({ + 'code': 400, + 'message': '订单类型错误,不是积分订单' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 检查订单状态是否为未支付(9) + if order.zhuangtai == 9: + # 删除订单 + order.delete() + logger.info(f"用户{request.user.yonghuid}删除未支付积分订单: {dingdanid}") + + return Response({ + 'code': 200, + 'message': '积分订单已删除' + }, status=status.HTTP_200_OK) + else: + return Response({ + 'code': 400, + 'message': '积分订单状态不是未支付,无法删除' + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"积分支付失败处理异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +# views.py +class JifenLunxun(APIView): + """ + 积分支付成功轮询接口 + 路径:/shangpin/jifenlunxun + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:订单状态和当前积分 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的订单) + try: + order = Czjilu.objects.get( + dingdan_id=dingdanid, + yonghuid=request.user.yonghuid + ) + except Czjilu.DoesNotExist: + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单类型是否为积分(3) + if order.leixing != 3: + return Response({ + 'code': 400, + 'message': '订单类型错误,不是积分订单' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 检查订单状态是否为已支付(3) + if order.zhuangtai != 3: + logger.info(f"积分订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") + return Response({ + 'code': 400, + 'message': '积分订单尚未支付完成' + }, status=status.HTTP_400_BAD_REQUEST) + + # 5. 获取打手积分(通过反向查询) + try: + dashou = request.user.dashou_profile + jifen = dashou.jifen # 直接使用整数 + + logger.info(f"积分轮询成功: 订单{dingdanid}已支付,用户{request.user.yonghuid}积分为{jifen}") + + return Response({ + 'code': 200, + 'message': '支付流程全部完成', + 'jifen': jifen # 返回当前积分(前端需要更新全局变量) + }, status=status.HTTP_200_OK) + + except UserDashou.DoesNotExist: + logger.error(f"打手信息不存在: 用户{request.user.yonghuid}") + return Response({ + 'code': 400, + 'message': '打手信息不存在' + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"积分轮询异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, 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 xml.etree.ElementTree as ET +import requests +from django.conf import settings +import logging + +logger = logging.getLogger(__name__) + + +class HuiyuanGoumai(APIView): + """ + 会员充值支付参数接口 + 路径:/shangpin/huiyuangm + 请求方式:POST + 权限:JWT认证 + 参数:huiyuanid(会员ID) + 返回:支付参数(payParams)和订单ID(dingdanid) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取会员ID参数 + huiyuanid = request.data.get('huiyuanid') + if not huiyuanid: + return Response({ + 'code': 400, + 'message': '会员ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 验证用户身份和状态 + try: + dashou = request.user.dashou_profile + except UserDashou.DoesNotExist: + return Response({ + 'code': 400, + 'message': '用户不是打手,无法购买会员' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查打手账号状态 + if dashou.zhanghaozhuangtai != 1: + return Response({ + 'code': 400, + 'message': '账号状态异常,无法进行购买' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 查询会员信息 + try: + huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuanid) + except Huiyuan.DoesNotExist: + return Response({ + 'code': 400, + 'message': '会员信息不存在' + }, status=status.HTTP_400_BAD_REQUEST) + + # 5. 生成订单ID(前缀CzHY表示会员充值) + timestamp_ms = int(time.time() * 1000) + timestamp_ns = int(time.time_ns() // 1000) % 1000000 + random_num = random.randint(0, 99) + + dingdanid = f"CzHY{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" + + # 6. 创建充值记录订单 + jine = float(huiyuan.jiage) # 会员价格 + # 组合订单说明:会员介绍 + 配置描述 + shuoming = f"{huiyuan.jieshao} - {settings.HUIYUAN_PAY_DESCRIPTION}" + + chongzhi_jilu = Czjilu.objects.create( + dingdan_id=dingdanid, + zhuangtai=9, # 未支付状态 + yonghuid=request.user.yonghuid, + jine=jine, + leixing=1, # 充值类型:1=会员 + shuoming=shuoming, + huiyuan_id=huiyuanid # 存储会员ID + ) + + logger.info( + f"创建会员充值订单: 用户{request.user.yonghuid}, 订单{dingdanid}, 会员{huiyuanid}, 金额{jine}元") + + # 7. 生成微信支付参数 + try: + pay_params = self.generate_wechat_pay_params( + dingdanid=dingdanid, + jine=jine, + openid=request.user.openid, + pay_type='huiyuan' + ) + 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) + + # 8. 返回支付参数和订单ID + return Response({ + 'code': 200, + 'message': '支付参数生成成功', + 'payParams': pay_params, # 对应前端payParams + 'dingdanid': 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): + """生成微信支付参数(完整版,无省略)""" + # 微信支付配置 - 从settings中获取 + APPID = settings.WEIXIN_APPID + MCHID = settings.WEIXIN_MCHID + KEY = settings.WEIXIN_SHANGHUMIYAO + + # 根据支付类型获取对应的配置 + if pay_type == 'yajin': + PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION + NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL + elif pay_type == 'jifen': + PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION + NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL + elif pay_type == 'huiyuan': + PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION + NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL + else: + raise Exception('未知的支付类型') + + # 生成随机字符串 + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + # 金额转换(元转分) + total_fee = int(jine * 100) + + # 交易类型(小程序支付使用JSAPI) + trade_type = 'JSAPI' + + # 获取客户端IP + user_ip = self.get_client_ip() + + # 1. 第一次签名(统一下单接口) + 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 # 小程序支付必须传openid + } + + # 按照参数名ASCII码从小到大排序 + 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}' + + # MD5签名 + 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} +""" + + # 调用微信统一下单接口(官方地址) + response = requests.post( + 'https://api.mch.weixin.qq.com/pay/unifiedorder', + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + timeout=10 # 设置超时 + ) + + # 解析返回的XML + 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 + prepay_id = root.find('prepay_id').text + + # 2. 第二次签名(小程序支付参数) + 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' + + # 生成支付签名(注意:这里key的大小写和字段名与第一次不同) + pay_sign_data = { + 'appId': APPID, # 注意:这里是appId,不是appid + 'timeStamp': timestamp, # 注意:这里是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, # 对应前端:appId + 'timeStamp': timestamp, # 对应前端:timeStamp + 'nonceStr': nonce_str, # 对应前端:nonceStr + 'package': package, # 对应前端:package + 'signType': sign_type, # 对应前端:signType + 'paySign': pay_sign # 对应前端:paySign + } + else: + # 获取错误信息 + err_code = root.find('err_code') + err_code_des = root.find('err_code_des') + return_msg = root.find('return_msg') + + 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}" + if return_msg is not None: + error_details += f", return_msg: {return_msg.text}" + + 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') + 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, 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): + """ + 会员支付回调接口 + 路径:/shangpin/huiyuanhuitiao + """ + + 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 + 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}分") + + if return_code != 'SUCCESS' or result_code != 'SUCCESS': + logger.warning(f"会员支付回调状态异常: {return_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', '签名验证失败') + + # 查询订单 + try: + order = Czjilu.objects.get(dingdan_id=out_trade_no) + except Czjilu.DoesNotExist: + logger.error(f"会员订单不存在: {out_trade_no}") + return self.wechat_response('FAIL', '订单不存在') + + if order.leixing != 1: + logger.error(f"订单类型错误: {order.leixing}, 期望1(会员)") + return self.wechat_response('FAIL', '订单类型错误') + + if order.zhuangtai != 9: + logger.warning(f"会员订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") + return self.wechat_response('SUCCESS', 'OK') + + # 验证金额 + wechat_amount_yuan = total_fee / 100 + order_amount_yuan = float(order.jine) + if abs(wechat_amount_yuan - order_amount_yuan) > 0.01: + logger.error(f"会员金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") + return self.wechat_response('FAIL', '金额不一致') + + # 查询会员信息 + 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: + 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( + 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) + + # 更新订单状态 + order.zhuangtai = 3 + order.save(update_fields=['zhuangtai']) + logger.info(f"会员订单状态更新成功: {order.dingdan_id} -> 3") + + # 更新会员购买次数 + huiyuan.goumai_cishu += 1 + huiyuan.save(update_fields=['goumai_cishu']) + + # 处理会员购买记录 + goumai_record, is_new, is_first_buy = self.process_huiyuan_goumai( + order.yonghuid, order.huiyuan_id, huiyuan + ) + + if is_first_buy: + self.add_first_buy_jifen(order.yonghuid) + + # 🆕 新增:处理分红(包含管事和组长) + with transaction.atomic(): + self.process_fenhong(order, huiyuan) + + 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: + 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: + 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积分") + + 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): + """首次购买积分奖励(与原代码一致)""" + try: + dashou = UserDashou.objects.get(user__yonghuid=yonghuid) + dashou.jifen += 10 + dashou.save(update_fields=['jifen']) + logger.info(f"用户{yonghuid}首次购买会员,积分+10,当前积分: {dashou.jifen}") + except UserDashou.DoesNotExist: + logger.error(f"找不到打手记录,无法增加积分: {yonghuid}") + except Exception as e: + logger.error(f"增加积分失败: {str(e)}") + + # 🆕 新增:统一分红处理函数 + def process_fenhong(self, order, huiyuan): + """ + 处理管事和组长分红(支持多次分红配置) + """ + try: + # 获取打手信息 + dashou = UserDashou.objects.get(user__yonghuid=order.yonghuid) + guanshi_id = dashou.yaoqingren + if not guanshi_id: + logger.info(f"打手{order.yonghuid}没有邀请人,无分红") + return + + # 获取管事信息 + try: + guanshi = UserGuanshi.objects.get(user__yonghuid=guanshi_id) + except UserGuanshi.DoesNotExist: + logger.warning(f"管事信息不存在: {guanshi_id}") + return + + # 获取组长ID(从管事扩展表的邀请人) + zuzhang_id = guanshi.yaoqingren + + # 获取打手头像和昵称 + user_main = dashou.user + avatar = user_main.avatar + nicheng = dashou.nicheng or "未知打手" + + # 查询该打手针对该管事的已分红次数 + existing_count = Gsfenhong.objects.filter( + dashouid=order.yonghuid, + guanshi=guanshi_id + ).count() + cishu = existing_count + 1 # 本次是第几次 + + # 获取订单金额(用于限制分红总额) + order_amount = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine + + # 计算管事分红金额 + guanshi_fenhong = self._calc_guanshi_fenhong(guanshi, huiyuan, guanshi_id, cishu) + if guanshi_fenhong is None: + logger.info(f"管事{guanshi_id}本次无分红") + guanshi_fenhong = Decimal('0.00') + + # 计算组长分红金额(如果存在组长) + zuzhang_fenhong = Decimal('0.00') + if zuzhang_id: + # 获取组长信息 + try: + zuzhang = UserZuzhang.objects.get(user__yonghuid=zuzhang_id) + except UserZuzhang.DoesNotExist: + logger.warning(f"组长信息不存在: {zuzhang_id}") + zuzhang = None + + zuzhang_fenhong = self._calc_zuzhang_fenhong(zuzhang, huiyuan, zuzhang_id, cishu) + if zuzhang_fenhong is None: + logger.info(f"组长{zuzhang_id}本次无分红") + zuzhang_fenhong = Decimal('0.00') + else: + logger.info(f"管事{guanshi_id}没有组长,无组长分红") + + # 检查分红总额是否超过订单金额(优先保证管事分红) + total_fenhong = guanshi_fenhong + zuzhang_fenhong + if total_fenhong > order_amount: + logger.warning(f"分红总额{total_fenhong}超过订单金额{order_amount},组长分红设为0") + zuzhang_fenhong = Decimal('0.00') + total_fenhong = guanshi_fenhong + if total_fenhong > order_amount: + logger.warning(f"管事分红{guanshi_fenhong}也超过订单金额,强制设为0") + guanshi_fenhong = Decimal('0.00') + total_fenhong = Decimal('0.00') + + # 创建分红记录(先创建主记录,即使金额为0也记录?但为0无意义,所以只有大于0才创建) + if guanshi_fenhong > 0 or zuzhang_fenhong > 0: + # 更新管事扩展表 + if guanshi_fenhong > 0: + guanshi.chongzhifenrun += guanshi_fenhong + guanshi.yue += guanshi_fenhong + guanshi.jinrichongzhi += 1 + guanshi.jinyuechongzhi += 1 + guanshi.save(update_fields=['chongzhifenrun', 'yue', 'jinrichongzhi', 'jinyuechongzhi']) + logger.info(f"管事分红成功: 管事{guanshi_id}, 金额{guanshi_fenhong}") + + # 更新组长扩展表 + if zuzhang_fenhong > 0 and zuzhang: + zuzhang.fenyong_zonge += zuzhang_fenhong + zuzhang.ketixian_jine += zuzhang_fenhong + zuzhang.jinri_fenyong += zuzhang_fenhong + zuzhang.jinyue_fenyong += zuzhang_fenhong + zuzhang.save(update_fields=['fenyong_zonge', 'ketixian_jine', 'jinri_fenyong', 'jinyue_fenyong']) + logger.info(f"组长分红成功: 组长{zuzhang_id}, 金额{zuzhang_fenhong}") + + # 创建分红记录 + Gsfenhong.objects.create( + dingdan_id=order.dingdan_id, + guanshi=guanshi_id, + dashouid=order.yonghuid, + shuoming=order.shuoming, + fenhong=guanshi_fenhong, + avatar=avatar, + nicheng=nicheng, + # 新增字段 + zuzhang_id=zuzhang_id if zuzhang_fenhong > 0 else None, + zuzhang_fenhong=zuzhang_fenhong if zuzhang_fenhong > 0 else None, + huiyuan_id=huiyuan.huiyuan_id, + fenhong_leixing=1, + ) + + + + # ========== 管事每日统计(新购) ========== + 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, + amount=guanshi_fenhong + ) + logger.info(f"管事每日统计更新成功(新购):管事{guanshi_id},分红{guanshi_fenhong}") + except Exception as e: + logger.error(f"管事每日统计更新失败(新购):{str(e)}") + + # ========== 管事每日统计(续费) ========== + 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 + ) + logger.info(f"管事续费统计更新成功:管事{guanshi_id},续费分红{guanshi_fenhong}") + except Exception as e: + logger.error(f"管事续费统计更新失败:{str(e)}") + + # ========== 组长每日统计(分佣收入) ========== + 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, + amount=zuzhang_fenhong + ) + logger.info(f"组长每日统计更新成功:组长{zuzhang_id},分佣{zuzhang_fenhong}") + except Exception as e: + logger.error(f"组长每日统计更新失败:{str(e)}") + + + logger.info(f"分红记录创建成功: 订单{order.dingdan_id}") + else: + logger.info(f"分红金额均为0,不创建记录") + + except Exception as e: + logger.error(f"处理分红失败: {str(e)}", exc_info=True) + # 不抛出异常,避免影响主流程 + + def _calc_guanshi_fenhong(self, guanshi, huiyuan, guanshi_id, cishu): + """计算管事分红金额""" + # 先查多次分红配置表 + try: + config = DuociFenhong.objects.get( + huiyuan=huiyuan, + yonghuid=guanshi_id, + cishu=cishu + ) + return Decimal(str(config.guanshi_fenhong)) + except DuociFenhong.DoesNotExist: + pass + + if cishu == 1: + # 第一次分红,取会员表默认金额 + return Decimal(str(huiyuan.guanshifc)) + else: + # 第二次及以上,检查是否开启二次分红 + if guanshi.fenghong_erci_enabled: + return Decimal(str(guanshi.fenghong_erci)) + else: + return None # 无分红 + + def _calc_zuzhang_fenhong(self, zuzhang, huiyuan, zuzhang_id, cishu): + """计算组长分红金额""" + if zuzhang is None: + return None + # 查多次分红配置表 + try: + config = DuociFenhong.objects.get( + huiyuan=huiyuan, + yonghuid=zuzhang_id, + cishu=cishu + ) + return Decimal(str(config.zuzhang_fenhong)) + except DuociFenhong.DoesNotExist: + pass + + if cishu == 1: + # 第一次分红,取会员表默认组长分成 + return Decimal(str(huiyuan.zuzhangfc)) + else: + # 第二次及以上,检查组长是否开启额外分红 + if zuzhang.kaioi_ewai_fenhong: + return Decimal(str(zuzhang.ewai_fenhong_jine)) + else: + return None + + # 以下为原辅助方法,保持原样 + def verify_signature(self, xml_data): + """验证微信支付签名(与原代码一致)""" + try: + root = ET.fromstring(xml_data) + sign = root.find('sign').text + if not sign: + logger.error("会员签名字段不存在") + return False + + 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() + return sign == calculated_sign + 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 + +'''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 xml.etree.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): + """ + 会员支付失败处理接口 + 路径:/shangpin/huiyuanshibai + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:删除结果 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的订单) + try: + order = Czjilu.objects.get( + dingdan_id=dingdanid, + yonghuid=request.user.yonghuid + ) + except Czjilu.DoesNotExist: + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单类型是否为会员(1) + if order.leixing != 1: + return Response({ + 'code': 400, + 'message': '订单类型错误,不是会员订单' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 检查订单状态是否为未支付(9) + if order.zhuangtai == 9: + # 删除订单 + order.delete() + logger.info(f"用户{request.user.yonghuid}删除未支付会员订单: {dingdanid}") + + return Response({ + 'code': 200, + 'message': '会员订单已删除' + }, status=status.HTTP_200_OK) + else: + return Response({ + 'code': 400, + 'message': '会员订单状态不是未支付,无法删除' + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"会员支付失败处理异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +# views.py +class HuiyuanLunxun(APIView): + """ + 会员支付成功轮询接口 + 路径:/shangpin/huiyuanlx + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:订单状态和会员信息 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的订单) + try: + order = Czjilu.objects.get( + dingdan_id=dingdanid, + yonghuid=request.user.yonghuid + ) + except Czjilu.DoesNotExist: + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单类型是否为会员(1) + if order.leixing != 1: + return Response({ + 'code': 400, + 'message': '订单类型错误,不是会员订单' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 检查订单状态是否为已支付(3) + if order.zhuangtai != 3: + logger.info(f"会员订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") + return Response({ + 'code': 400, + 'message': '会员订单尚未支付完成' + }, status=status.HTTP_400_BAD_REQUEST) + + # 5. 查询会员信息 + try: + huiyuan = Huiyuan.objects.get(huiyuan_id=order.huiyuan_id) + + # 6. 查询会员购买记录 + goumai_record = Huiyuangoumai.objects.filter( + yonghu_id=request.user.yonghuid, + huiyuan_id=order.huiyuan_id + ).first() + + if not goumai_record: + return Response({ + 'code': 400, + 'message': '会员购买记录不存在' + }, status=status.HTTP_400_BAD_REQUEST) + + # 7. 格式化到期时间 + #from django.utils.timezone import localtime + #daoqi_time = localtime(goumai_record.daoqi_time).strftime('%Y-%m-%d %H:%M:%S') + # 直接格式化,不使用时区转换 + daoqi_time = goumai_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') + current_user = request.user + dashou = current_user.dashou_profile + + logger.info(f"会员轮询成功: 订单{dingdanid}已支付,用户{request.user.yonghuid}, 会员{order.huiyuan_id}") + + # 8. 返回会员信息(用于更新前端全局变量) + return Response({ + 'code': 200, + 'message': '支付流程全部完成', + 'jifen': dashou.jifen, + 'huiyuan': { + 'huiyuanid': order.huiyuan_id, + 'huiyuanming': huiyuan.jieshao, + 'daoqi': daoqi_time + } + }, status=status.HTTP_200_OK) + + except Huiyuan.DoesNotExist: + logger.error(f"会员信息不存在: {order.huiyuan_id}") + return Response({ + 'code': 400, + 'message': '会员信息不存在' + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"会员轮询异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + +# views.py in shangpin app +import time +import random +import string +import hashlib +import xml.etree.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): + """ + 商家余额充值支付参数接口 + 路径:/shangpin/sjcz + 请求方式:POST + 权限:JWT认证 + 参数:jine(金额,1-10000元) + 返回:支付参数(payParams)和订单ID(dingdanid) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 验证用户身份和商家状态 + try: + shangjia = request.user.shop_profile + except UserShangjia.DoesNotExist: + logger.warning(f"用户{request.user.yonghuid}尝试充值但不是商家") + return Response({ + 'code': 400, + 'message': '用户不是商家,无法充值' + }, status=status.HTTP_400_BAD_REQUEST) + + # 检查商家状态 + if shangjia.zhuangtai != 1: + logger.warning(f"商家{shangjia.nicheng}状态异常: {shangjia.zhuangtai}") + return Response({ + 'code': 400, + 'message': '商家状态异常,无法进行充值' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 获取并验证金额参数 + jine = request.data.get('jine') + if not jine: + return Response({ + 'code': 400, + 'message': '请填写充值金额' + }, status=status.HTTP_400_BAD_REQUEST) + + try: + jine = float(jine) + except (ValueError, TypeError): + return Response({ + 'code': 400, + 'message': '金额格式错误,请填写有效的数字' + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证金额范围 + if jine < settings.SHANGJIA_CZ_MIN_AMOUNT or jine > settings.SHANGJIA_CZ_MAX_AMOUNT: + return Response({ + 'code': 400, + 'message': f'充值金额必须在{settings.SHANGJIA_CZ_MIN_AMOUNT}-{settings.SHANGJIA_CZ_MAX_AMOUNT}元之间' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 生成订单ID(使用配置的前缀) + timestamp_ms = int(time.time() * 1000) + timestamp_ns = int(time.time_ns() // 1000) % 1000000 + random_num = random.randint(0, 99) + + dingdanid = f"{settings.SHANGJIA_CZ_PREFIX}{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" + + # 4. 创建充值记录订单 + try: + chongzhi_jilu = Czjilu.objects.create( + dingdan_id=dingdanid, + zhuangtai=9, # 未支付状态 + yonghuid=request.user.yonghuid, + jine=jine, + leixing=4, # 充值类型:4=商家充值 + shuoming=settings.SHANGJIA_CZ_PAY_DESCRIPTION + ) + logger.info(f"创建商家充值订单: 商家{shangjia.nicheng}, 订单{dingdanid}, 金额{jine}元") + except Exception as e: + logger.error(f"创建订单失败: {str(e)}") + return Response({ + 'code': 500, + 'message': '订单创建失败,请重试' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 5. 生成微信支付参数 + try: + pay_params = self.generate_wechat_pay_params( + dingdanid=dingdanid, + jine=jine, + openid=request.user.openid, + pay_type='shangjia' + ) + 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) + + # 6. 返回支付参数和订单ID + logger.info(f"商家{shangjia.nicheng}支付参数生成成功: {dingdanid}") + 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 == 'shangjia': + PAY_DESCRIPTION = settings.SHANGJIA_CZ_PAY_DESCRIPTION + NOTIFY_URL = settings.SHANGJIA_CZ_PAY_NOTIFY_URL + else: + raise Exception('未知的支付类型') + + # 生成随机字符串 + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + # 金额转换(元转分) + total_fee = int(Decimal(str(jine)) * 100) + + # 交易类型(小程序支付使用JSAPI) + trade_type = 'JSAPI' + + # 获取客户端IP + user_ip = self.get_client_ip() + + # 1. 第一次签名(统一下单接口) + 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 # 小程序支付必须传openid + } + + # 按照参数名ASCII码从小到大排序 + 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}' + + # MD5签名 + 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} +""" + + # 调用微信统一下单接口 + response = requests.post( + 'https://api.mch.weixin.qq.com/pay/unifiedorder', + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + timeout=10 + ) + + # 解析返回的XML + 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 + prepay_id = root.find('prepay_id').text + + # 2. 第二次签名(小程序支付参数) + 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') + return_msg = root.find('return_msg') + + 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}" + if return_msg is not None: + error_details += f", return_msg: {return_msg.text}" + + 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') + return ip if ip else '127.0.0.1' + + + +from django.db import transaction + +logger = logging.getLogger(__name__) + + +class ShangjiaHuitiao(View): + """ + 商家支付回调接口 + 路径:/shangpin/sjhuitiao + 注意:这个接口不需要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: + 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: + 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 = Decimal(str(total_fee)) / Decimal('100') # 转换为元 + order_amount_yuan = Decimal(str(order.jine)) # 订单金额(元) + + # 允许1分钱的误差 + if abs(wechat_amount_yuan - order_amount_yuan) > Decimal('0.01'): + logger.error(f"商家订单金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") + 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 + + 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) + # ===== 添加结束 ===== + + + + # 7. 使用事务更新订单状态和商家余额 + with transaction.atomic(): + # 更新订单状态为已支付(3) + order.zhuangtai = 3 + order.save(update_fields=['zhuangtai']) + logger.info(f"商家订单状态更新成功: {order.dingdan_id} -> 3") + + # 更新商家余额 + try: + # 通过用户ID查找商家(高效查询) + shangjia = UserShangjia.objects.get(user__yonghuid=order.yonghuid) + + # 增加余额 + shangjia.yue += order_amount_yuan + shangjia.save(update_fields=['yue']) + + logger.info(f"商家余额更新成功: 商家{shangjia.nicheng}, 增加{order_amount_yuan}元, 新余额{shangjia.yue}") + + except UserShangjia.DoesNotExist: + # 如果找不到商家,记录错误,但依然返回成功(避免微信重复回调) + logger.error(f"商家信息不存在: 用户{order.yonghuid}") + return self.wechat_response('SUCCESS', 'OK') + except Exception as e: + logger.error(f"更新商家余额失败: {str(e)}", exc_info=True) + # 回滚事务 + raise + + # 8. 返回成功响应给微信 + 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): + """验证微信支付签名""" + 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 ShangjiaShibai(APIView): + """ + 商家支付失败处理接口 + 路径:/shangpin/sjshibai + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:删除结果 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的订单) + try: + order = Czjilu.objects.get( + dingdan_id=dingdanid, + yonghuid=request.user.yonghuid, + leixing=4 # 确保是商家充值订单 + ) + except Czjilu.DoesNotExist: + logger.warning(f"商家订单不存在或不属于当前用户: {dingdanid}, 用户{request.user.yonghuid}") + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单状态是否为未支付(9) + if order.zhuangtai == 9: + # 删除订单 + order.delete() + logger.info(f"商家{request.user.shop_profile.nicheng}删除未支付订单: {dingdanid}") + + return Response({ + 'code': 200, + 'message': '订单已删除' + }, status=status.HTTP_200_OK) + else: + return Response({ + 'code': 400, + 'message': '订单状态不是未支付,无法删除' + }, status=status.HTTP_400_BAD_REQUEST) + + except UserShangjia.DoesNotExist: + return Response({ + 'code': 400, + 'message': '用户不是商家' + }, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + logger.error(f"商家支付失败处理异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + +# views.py +class ShangjiaChenggong(APIView): + """ + 商家支付成功轮询接口 + 路径:/shangpin/sjcg + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:订单状态和当前商家余额 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的商家订单) + try: + order = Czjilu.objects.get( + dingdan_id=dingdanid, + yonghuid=request.user.yonghuid, + leixing=4 # 商家充值类型 + ) + except Czjilu.DoesNotExist: + logger.warning(f"商家订单不存在: {dingdanid}, 用户{request.user.yonghuid}") + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单状态是否为已支付(3) + if order.zhuangtai != 3: + logger.info(f"商家订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") + return Response({ + 'code': 400, + 'message': '订单尚未支付完成' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 获取商家余额(通过反向查询) + try: + shangjia = request.user.shop_profile + yue = float(shangjia.yue) # 转为浮点数方便前端处理 + + logger.info(f"商家轮询成功: 订单{dingdanid}已支付,商家{shangjia.nicheng}余额为{yue}元") + + return Response({ + 'code': 200, + 'message': '支付流程全部完成', + 'yue': yue # 返回当前商家余额(前端需要更新全局变量) + }, status=status.HTTP_200_OK) + + except UserShangjia.DoesNotExist: + logger.error(f"商家信息不存在: 用户{request.user.yonghuid}") + return Response({ + 'code': 400, + 'message': '商家信息不存在' + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"商家轮询异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +# 管事分红记录查询API + +from django.db.models import Q +from decimal import Decimal +import logging + +logger = logging.getLogger(__name__) + + +class GuanshiFenhongListAPI(APIView): + """ + 管事分红记录查询接口 + 权限:需要JWT认证 + 请求方式:POST + 分页参数:page, page_size + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 获取当前管事用户ID + yonghuid = request.user.yonghuid + + # 获取分页参数 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 50)) + + if page < 1: + page = 1 + if page_size < 1 or page_size > 100: # 限制最大100条 + page_size = 50 + + # 计算偏移量 + offset = (page - 1) * page_size + + # 查询条件:管事ID匹配 + query = Q(guanshi=yonghuid) + + # 先查询总数(使用分页查询优化) + from django.db.models import Count + from .models import Gsfenhong + + # 查询总记录数 + zong_tiaoshu = Gsfenhong.objects.filter(query).count() + + # 查询去重后的打手数量 + chongzhi_dashou_shuliang = Gsfenhong.objects.filter(query).values('dashouid').distinct().count() + + # 查询当前页数据,按创建时间倒序排列 + fenhong_list = Gsfenhong.objects.filter(query) \ + .order_by('-create_time') \ + .values('dashouid', 'avatar', 'nicheng', 'fenhong', 'create_time')[offset:offset + page_size] + + # 格式化数据 + formatted_list = [] + for item in fenhong_list: + formatted_list.append({ + 'yonghuid': item['dashouid'], # 打手ID + 'avatar': item['avatar'] or '', # 打手头像 + 'nicheng': item['nicheng'] or '未知用户', # 打手昵称 + 'fenhong': str(item['fenhong']), # 分红金额 + 'create_time': item['create_time'].strftime('%Y-%m-%d %H:%M:%S') if item['create_time'] else '' + }) + + # 返回数据 + return Response({ + 'code': 200, + 'msg': '成功', + 'data': { + 'list': formatted_list, + 'total': zong_tiaoshu, # 总记录数 + 'chongzhi_dashou_shuliang': chongzhi_dashou_shuliang, # 充值打手数量(去重) + 'page': page, + 'page_size': page_size, + 'has_more': len(formatted_list) == page_size # 是否还有更多数据 + } + }) + + except Exception as e: + logger.error(f"查询管事分红记录失败: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'msg': f'服务器错误: {str(e)}', + 'data': {} + }) + + +# 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): + """ + 获取商品类型、商品专区、会员数据的接口 + 接口地址:/shangpin/adhqlx + 请求方法:POST + 权限要求:JWT Token认证 + 管理员权限 + """ + + # 需要JWT Token认证 + permission_classes = [IsAuthenticated] + + def post(self, request): + """ + 处理POST请求,返回商品相关数据 + """ + try: + # 1. 获取请求数据 + zhanghao = request.data.get('zhanghao') + + if not zhanghao: + logger.warning(f"管理员接口:未接收到账号参数,请求用户:{request.user.yonghuid}") + return Response({ + 'code': 400, + 'message': '缺少账号参数', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 获取当前登录用户 + current_user = request.user + + # 3. 验证用户类型是否为管理员 + if current_user.user_type != 'admin': + logger.warning( + f"权限验证失败:用户 {current_user.yonghuid} 的用户类型为 {current_user.user_type},非管理员") + return Response({ + 'code': 401, + 'message': '无管理员权限', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 4. 验证手机号与传递的账号是否一致 + if current_user.phone != zhanghao: + logger.warning( + f"账号验证失败:用户 {current_user.yonghuid} 的phone={current_user.phone},传递的zhanghao={zhanghao}") + return Response({ + 'code': 401, + 'message': '账号验证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 5. 验证管理员扩展表是否存在 + try: + admin_profile = current_user.admin_profile + logger.info(f"管理员验证通过:用户 {current_user.yonghuid},手机号 {current_user.phone}") + except AdminProfile.DoesNotExist: + logger.error( + f"管理员扩展表不存在:用户 {current_user.yonghuid} 虽然user_type=admin,但没有AdminProfile记录") + return Response({ + 'code': 401, + 'message': '管理员信息不完整', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 6. 查询商品类型数据 + try: + # 使用select_related或prefetch_related优化查询(如果需要关联查询) + # 这里直接查询所有字段,然后提取需要的字段 + shangpinleixing_queryset = ShangpinLeixing.objects.all() + + # 转换为前端需要的格式 + shangpinleixing_list = [] + for leixing in shangpinleixing_queryset: + leixing_data = { + 'id': leixing.id, + 'yaoqiuleixing': leixing.yaoqiuleixing if leixing.yaoqiuleixing is not None else None, + 'huiyuan_id': leixing.huiyuan_id if leixing.huiyuan_id else None, + 'yongjin': str(leixing.yongjin) if leixing.yongjin is not None else None, + 'jieshao': leixing.jieshao if leixing.jieshao else '', + 'tupian_url': leixing.tupian_url if leixing.tupian_url else '' + } + shangpinleixing_list.append(leixing_data) + + #logger.info(f"成功查询到 {len(shangpinleixing_list)} 条商品类型数据") + + except Exception as e: + logger.error(f"查询商品类型数据时出错:{str(e)}") + shangpinleixing_list = [] + + # 7. 查询商品专区数据 + try: + # 查询所有商品专区 + zhuanqu_queryset = ShangpinZhuanqu.objects.all() + + # 转换为前端需要的格式 + zhuanqu_list = [] + for zhuanqu in zhuanqu_queryset: + zhuanqu_data = { + 'id': zhuanqu.id, + 'mingzi': zhuanqu.mingzi if zhuanqu.mingzi else '', + 'leixing_id': zhuanqu.leixing_id + } + zhuanqu_list.append(zhuanqu_data) + + # logger.info(f"成功查询到 {len(zhuanqu_list)} 条商品专区数据") + + except Exception as e: + logger.error(f"查询商品专区数据时出错:{str(e)}") + zhuanqu_list = [] + + # 8. 查询会员数据 + try: + # 查询所有会员 + huiyuan_queryset = Huiyuan.objects.all() + + # 转换为前端需要的格式 + huiyuan_list = [] + for huiyuan in huiyuan_queryset: + huiyuan_data = { + 'huiyuan_id': huiyuan.huiyuan_id, + 'jieshao': huiyuan.jieshao, + 'jiage': str(huiyuan.jiage) # Decimal转为字符串 + } + huiyuan_list.append(huiyuan_data) + + # logger.info(f"成功查询到 {len(huiyuan_list)} 条会员数据") + + except Exception as e: + logger.error(f"查询会员数据时出错:{str(e)}") + huiyuan_list = [] + + # 9. 检查会员列表是否为空 + if not huiyuan_list: + logger.warning("会员列表为空,可能会影响商品发布功能") + + # 10. 组织返回数据 + response_data = { + 'code': 0, + 'message': '获取数据成功', + 'data': { + 'shangpinleixingList': shangpinleixing_list, + 'zhuanquList': zhuanqu_list, + 'huiyuanList': huiyuan_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) + + +# 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): + """ + 商品类型发布接口 + 接收图片、类型介绍、抢单要求类型等字段 + 需要管理员权限 + """ + permission_classes = [permissions.IsAuthenticated] + parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 + + def post(self, request): + try: + # === 1. 权限验证 === + # 获取当前用户 + user_main = request.user + + # 1.1 验证前端传递的账号是否匹配(使用phone字段) + zhanghao = request.data.get('zhanghao', '').strip() + if not zhanghao: + return Response( + {'code': 401, 'msg': '未提供账号信息', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 验证账号是否匹配(使用phone字段比较) + if user_main.phone != zhanghao: + return Response( + {'code': 401, 'msg': '账号验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.2 验证是否为管理员(检查user_type字段) + if user_main.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限不足', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.3 验证管理员扩展表(使用AdminProfile模型) + try: + admin_profile = AdminProfile.objects.get(user=user_main) + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'msg': '管理员信息不存在', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # === 2. 接收并验证字段 === + # 2.1 必填字段验证 + jieshao = request.data.get('jieshao', '').strip() + yaoqiuleixing = request.data.get('yaoqiuleixing') + + + if not jieshao: + return Response( + {'code': 400, 'msg': '类型介绍不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if yaoqiuleixing not in ['1', '2']: + return Response( + {'code': 400, 'msg': '抢单要求类型无效', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.2 验证图片 + tupian_file = request.FILES.get('file') + if not tupian_file: + + return Response( + {'code': 400, 'msg': '请上传类型图片', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证图片格式和大小 + is_valid, error_msg = validate_image(tupian_file) + if not is_valid: + return Response( + {'code': 400, 'msg': f'图片验证失败: {error_msg}', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.3 根据抢单要求类型验证对应字段 + huiyuan_id = request.data.get('huiyuan_id', '').strip() + yongjin = request.data.get('yongjin', '').strip() + + + # 转换yaoqiuleixing为整数便于处理 + yaoqiuleixing_int = int(yaoqiuleixing) + + if yaoqiuleixing_int == 1: # 会员抢单 + if not huiyuan_id: + return Response( + {'code': 400, 'msg': '会员抢单必须选择会员', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + # 验证会员ID是否存在 + try: + huiyuan_obj = Huiyuan.objects.get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response( + {'code': 400, 'msg': '会员ID不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + # 会员ID为字符串,佣金为空 + yongjin_value = None + huiyuan_id_value = huiyuan_id + else: # yaoqiuleixing_int == 2 佣金抢单 + if not yongjin: + return Response( + {'code': 400, 'msg': '佣金抢单必须填写佣金金额', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + try: + # 验证佣金为有效数字 + yongjin_value = float(yongjin) + if yongjin_value <= 0: + return Response( + {'code': 400, 'msg': '佣金金额必须大于0', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except ValueError: + return Response( + {'code': 400, 'msg': '佣金金额格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + huiyuan_id_value = None + + # === 3. 创建商品类型记录 === + with transaction.atomic(): + # 3.1 创建商品类型对象 + leixing_obj = ShangpinLeixing( + jieshao=jieshao, + yaoqiuleixing=yaoqiuleixing_int, + huiyuan_id=huiyuan_id_value, + yongjin=yongjin_value, + # tupian_url 先留空,上传成功后再更新 + ) + + # 保存到数据库获取ID + leixing_obj.save() + leixing_id = leixing_obj.id + + # === 4. 处理图片上传 === + try: + # 4.1 生成文件名和路径 + file_extension = os.path.splitext(tupian_file.name)[1].lower() + if not file_extension: + file_extension = '.jpg' + + # 生成唯一文件名:商品类型ID + 随机UUID + 后缀 + unique_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}" + + # 构建OSS文件路径:a_long/leixing/{leixing_id}/{filename} + oss_folder = 'a_long' # 根文件夹 + leixing_folder = 'leixing' # 类型文件夹 + + # 构建完整路径 + oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{unique_filename}" + + # 4.2 上传到OSS + tupian_url = upload_to_oss(tupian_file, oss_file_path) + + if not tupian_url: + # 上传失败,抛出异常触发回滚 + raise Exception("图片上传到OSS失败") + + # 4.3 更新商品类型的图片URL(存储相对路径) + leixing_obj.tupian_url = oss_file_path # 存相对路径,方便拼接 + leixing_obj.save() + + except Exception as oss_error: + # 图片上传失败,事务会回滚,商品类型记录也会被删除 + print(f"OSS上传失败: {oss_error}") + # 手动抛出异常触发事务回滚 + raise Exception(f"图片上传失败: {str(oss_error)}") + + # === 5. 构建返回数据 === + response_data = { + 'id': leixing_id, + 'jieshao': jieshao, + 'yaoqiuleixing': yaoqiuleixing_int, + 'tupian_url': leixing_obj.tupian_url, # 相对路径 + 'huiyuan_id': huiyuan_id_value, + 'yongjin': str(yongjin_value) if yongjin_value else None + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '商品类型添加成功', + 'data': response_data + }) + + except Exception as e: + # 捕获所有异常,返回500错误 + import traceback + traceback.print_exc() + return Response({ + 'code': 500, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class ShangpinZhuanquFabuView(APIView): + """ + 商品专区发布接口 + 接收专区名称、类型ID + 需要管理员权限 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + # === 1. 权限验证 === + user_main = request.user + + # 1.1 验证账号(使用phone字段) + zhanghao = request.data.get('zhanghao', '').strip() + if not zhanghao: + return Response( + {'code': 401, 'msg': '未提供账号信息', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if user_main.phone != zhanghao: + return Response( + {'code': 401, 'msg': '账号验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.2 验证是否为管理员(检查user_type字段) + if user_main.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限不足', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.3 验证管理员扩展表(使用AdminProfile模型) + try: + admin_profile = AdminProfile.objects.get(user=user_main) + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'msg': '管理员信息不存在', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # === 2. 接收并验证字段 === + mingzi = request.data.get('mingzi', '').strip() + leixing_id = request.data.get('leixing_id') + + # 2.1 验证必填字段 + if not mingzi: + return Response( + {'code': 400, 'msg': '专区名称不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not leixing_id: + return Response( + {'code': 400, 'msg': '类型ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.2 验证类型ID是否存在 + try: + # 验证商品类型是否存在 + leixing_obj = ShangpinLeixing.objects.get(id=leixing_id) + except ShangpinLeixing.DoesNotExist: + return Response( + {'code': 400, 'msg': '商品类型不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.3 验证名称长度(15汉字以内) + if len(mingzi) > 45: # 数据库字段最大长度 + return Response( + {'code': 400, 'msg': '专区名称过长', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 3. 创建商品专区记录 === + zhuanqu_obj = ShangpinZhuanqu( + mingzi=mingzi, + leixing_id=leixing_id + ) + + zhuanqu_obj.save() + + # === 4. 构建返回数据 === + response_data = { + 'id': zhuanqu_obj.id, + 'mingzi': mingzi, + 'leixing_id': leixing_id + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '商品专区添加成功', + 'data': response_data + }) + + except Exception as e: + import traceback + traceback.print_exc() + return Response({ + 'code': 500, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class ShangpinFabuView(APIView): + """ + 商品发布接口 + 接收商品信息、图片等字段 + 需要管理员权限 + """ + permission_classes = [permissions.IsAuthenticated] + parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 + + def post(self, request): + try: + # === 1. 权限验证 === + user_main = request.user + + # 1.1 验证账号(使用phone字段) + zhanghao = request.data.get('zhanghao', '').strip() + if not zhanghao: + return Response( + {'code': 401, 'msg': '未提供账号信息', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if user_main.phone != zhanghao: + return Response( + {'code': 401, 'msg': '账号验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.2 验证是否为管理员(检查user_type字段) + if user_main.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限不足', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.3 验证管理员扩展表(使用AdminProfile模型) + try: + admin_profile = AdminProfile.objects.get(user=user_main) + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'msg': '管理员信息不存在', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # === 2. 接收并验证字段 === + # 2.1 验证必填字段 + biaoti = request.data.get('biaoti', '').strip() + jiage = request.data.get('jiage', '').strip() + kucun = request.data.get('kucun', '').strip() + leixing_id = request.data.get('leixing_id') + zhuanqu_id = request.data.get('zhuanqu_id') + yaoqiuleixing = request.data.get('yaoqiuleixing') + jieshao = request.data.get('jieshao', '').strip() + + # 验证图片 + shangpin_tupian = request.FILES.get('shangpin_tupian') + if not shangpin_tupian: + + return Response( + {'code': 400, 'msg': '请上传商品图片', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证图片格式和大小 + is_valid, error_msg = validate_image(shangpin_tupian) + if not is_valid: + return Response( + {'code': 400, 'msg': f'商品图片验证失败: {error_msg}', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.2 验证必填字段是否为空 + required_fields = { + 'biaoti': biaoti, + 'jiage': jiage, + 'kucun': kucun, + 'leixing_id': leixing_id, + 'zhuanqu_id': zhuanqu_id, + 'yaoqiuleixing': yaoqiuleixing, + 'jieshao': jieshao + } + + for field_name, field_value in required_fields.items(): + if not field_value: + return Response( + {'code': 400, 'msg': f'{field_name}不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.3 验证数值字段 + try: + jiage_value = float(jiage) + if jiage_value <= 0: + return Response( + {'code': 400, 'msg': '商品价格必须大于0', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except ValueError: + return Response( + {'code': 400, 'msg': '商品价格格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + kucun_value = int(kucun) + if kucun_value < 0: + return Response( + {'code': 400, 'msg': '库存不能为负数', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except ValueError: + return Response( + {'code': 400, 'msg': '库存必须为整数', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证抢单要求类型 + if yaoqiuleixing not in ['1', '2']: + return Response( + {'code': 400, 'msg': '抢单要求类型无效', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 3. 验证关联数据 === + # 3.1 验证商品类型 + try: + leixing_obj = ShangpinLeixing.objects.get(id=leixing_id) + except ShangpinLeixing.DoesNotExist: + return Response( + {'code': 400, 'msg': '商品类型不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 3.2 根据抢单要求类型验证商品类型 + yaoqiuleixing_int = int(yaoqiuleixing) + + if yaoqiuleixing_int == 1: # 会员抢单 + # 需要验证商品类型的抢单要求类型是否为1 + if leixing_obj.yaoqiuleixing != 1: + return Response( + {'code': 400, 'msg': '商品类型与抢单要求类型不匹配', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 获取会员ID(从前端或商品类型) + huiyuan_id = request.data.get('huiyuan_id', leixing_obj.huiyuan_id) + if not huiyuan_id: + return Response( + {'code': 400, 'msg': '会员ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证会员是否存在 + try: + huiyuan_obj = Huiyuan.objects.get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response( + {'code': 400, 'msg': '会员不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 设置会员相关字段 + huiyuan_id_value = huiyuan_id + yongjin_value = None + else: # 佣金抢单 + # 需要验证商品类型的抢单要求类型是否为2 + if leixing_obj.yaoqiuleixing != 2: + return Response( + {'code': 400, 'msg': '商品类型与抢单要求类型不匹配', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 获取佣金(从前端或商品类型) + yongjin = request.data.get('yongjin', leixing_obj.yongjin) + if not yongjin: + return Response( + {'code': 400, 'msg': '佣金金额不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + yongjin_value = float(yongjin) + if yongjin_value <= 0: + return Response( + {'code': 400, 'msg': '佣金金额必须大于0', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except ValueError: + return Response( + {'code': 400, 'msg': '佣金金额格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + huiyuan_id_value = None + + # 3.3 验证商品专区 + try: + zhuanqu_obj = ShangpinZhuanqu.objects.get(id=zhuanqu_id, leixing_id=leixing_id) + except ShangpinZhuanqu.DoesNotExist: + return Response( + {'code': 400, 'msg': '商品专区不存在或与商品类型不匹配', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 4. 创建商品记录 === + with transaction.atomic(): + # 4.1 获取其他字段 + xuzhi = request.data.get('xuzhi', '').strip() + xiaoliang = request.data.get('xiaoliang', '0').strip() + + # 转换销量为整数 + try: + duiwai_xiaoliang_value = int(xiaoliang) if xiaoliang else 0 + except ValueError: + duiwai_xiaoliang_value = 0 + + # 4.2 创建商品对象 + shangpin_obj = Shangpin( + biaoqian=biaoti, + jiage=jiage_value, + kucun=kucun_value, + leixing_id=leixing_id, + zhuanqu_id=zhuanqu_id, + duiwai_xiaoliang=duiwai_xiaoliang_value, + jieshao=jieshao, + xiadan_xuzhi=xuzhi, + yaoqiuleixing=yaoqiuleixing_int, + huiyuan_id=huiyuan_id_value, + yongjin=yongjin_value, + # tupian_url 先留空,上传成功后再更新 + ) + + # 保存到数据库获取ID + shangpin_obj.save() + shangpin_id = shangpin_obj.id + + # === 5. 处理商品图片上传 === + try: + # 5.1 生成文件名和路径 + file_extension = os.path.splitext(shangpin_tupian.name)[1].lower() + if not file_extension: + file_extension = '.jpg' + + # 构建OSS文件路径:a_long/Spcc{shangpin_id}/{filename} + oss_folder = 'a_long' # 根文件夹 + spcc_folder = f"Spcc{shangpin_id}" # 商品图片文件夹 + + # 生成文件名:Spcc{shangpin_id}{file_extension} + unique_filename = f"Spcc{shangpin_id}{file_extension}" + + # 构建完整路径 + oss_file_path = f"{oss_folder}/{spcc_folder}/{unique_filename}" + + # 5.2 上传到OSS + tupian_url = upload_to_oss(shangpin_tupian, oss_file_path) + + if not tupian_url: + # 上传失败,抛出异常触发回滚 + raise Exception("商品图片上传到OSS失败") + + # 5.3 更新商品的图片URL(存储相对路径) + shangpin_obj.tupian_url = oss_file_path # 存相对路径 + shangpin_obj.save() + + except Exception as oss_error: + # 图片上传失败,事务会回滚,商品记录也会被删除 + #print(f"商品图片OSS上传失败: {oss_error}") + # 手动抛出异常触发事务回滚 + raise Exception(f"商品图片上传失败: {str(oss_error)}") + + # === 6. 构建返回数据 === + response_data = { + 'shangpin_id': shangpin_id, + 'biaoti': biaoti, + 'jiage': str(jiage_value), + 'kucun': kucun_value, + 'leixing_id': leixing_id, + 'zhuanqu_id': zhuanqu_id, + 'xiaoliang': duiwai_xiaoliang_value, + 'jieshao': jieshao, + 'xuzhi': xuzhi, + 'yaoqiuleixing': yaoqiuleixing_int, + 'huiyuan_id': huiyuan_id_value, + 'yongjin': str(yongjin_value) if yongjin_value else None, + 'tupian_url': shangpin_obj.tupian_url # 相对路径 + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '商品发布成功', + 'data': response_data + }) + + except Exception as e: + # 捕获所有异常,返回500错误 + import traceback + traceback.print_exc() + return Response({ + 'code': 500, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class GuizeTupianShangchuanView(APIView): + """ + 规则图片上传接口 + 接收商品ID和规则图片 + 需要管理员权限 + """ + permission_classes = [permissions.IsAuthenticated] + parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 + + def post(self, request): + try: + # === 1. 权限验证 === + user_main = request.user + + # 1.1 验证账号(使用phone字段) + zhanghao = request.data.get('zhanghao', '').strip() + if not zhanghao: + return Response( + {'code': 401, 'msg': '未提供账号信息', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if user_main.phone != zhanghao: + return Response( + {'code': 401, 'msg': '账号验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.2 验证是否为管理员(检查user_type字段) + if user_main.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限不足', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.3 验证管理员扩展表(使用AdminProfile模型) + try: + admin_profile = AdminProfile.objects.get(user=user_main) + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'msg': '管理员信息不存在', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # === 2. 接收并验证字段 === + # 2.1 验证商品ID + shangpin_id = request.data.get('shangpin_id') + if not shangpin_id: + return Response( + {'code': 400, 'msg': '商品ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.2 验证规则图片 + guize_tupian = request.FILES.get('guize_tupian') + if not guize_tupian: + return Response( + {'code': 400, 'msg': '请上传规则图片', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证图片格式和大小 + is_valid, error_msg = validate_image(guize_tupian) + if not is_valid: + return Response( + {'code': 400, 'msg': f'规则图片验证失败: {error_msg}', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 3. 验证商品是否存在 === + try: + shangpin_obj = Shangpin.objects.get(id=shangpin_id) + except Shangpin.DoesNotExist: + return Response( + {'code': 400, 'msg': '商品不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 4. 处理规则图片上传 === + try: + # 4.1 生成文件名和路径 + file_extension = os.path.splitext(guize_tupian.name)[1].lower() + if not file_extension: + file_extension = '.jpg' + + # 构建OSS文件路径:a_long/Spgz{shangpin_id}/{filename} + oss_folder = 'a_long' # 根文件夹 + spgz_folder = f"Spgz{shangpin_id}" # 规则图片文件夹 + + # 生成文件名:Spgz{shangpin_id}{file_extension} + unique_filename = f"Spgz{shangpin_id}{file_extension}" + + # 构建完整路径 + oss_file_path = f"{oss_folder}/{spgz_folder}/{unique_filename}" + + # 4.2 上传到OSS + tupian_url = upload_to_oss(guize_tupian, oss_file_path) + + if not tupian_url: + # 上传失败 + return Response({ + 'code': 500, + 'msg': '规则图片上传失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 4.3 更新商品的规则图片URL(存储相对路径) + shangpin_obj.guize_tupian = oss_file_path # 存相对路径 + shangpin_obj.save() + + except Exception as oss_error: + # OSS上传失败 + print(f"规则图片OSS上传失败: {oss_error}") + return Response({ + 'code': 500, + 'msg': f'规则图片上传失败: {str(oss_error)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # === 5. 构建返回数据 === + response_data = { + 'shangpin_id': shangpin_id, + 'guize_tupian_url': oss_file_path # 相对路径 + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '规则图片上传成功', + 'data': response_data + }) + + except Exception as e: + # 捕获所有异常,返回500错误 + import traceback + traceback.print_exc() + return Response({ + 'code': 500, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + +class AdHuiyuanHqView(APIView): + """ + 管理员获取会员信息和商品类型接口 + 需要管理员权限 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + # === 1. 权限验证 === + # 获取当前用户 + user_main = request.user + + # 1.1 验证前端传递的账号是否匹配(使用phone字段) + zhanghao = request.data.get('zhanghao', '').strip() + if not zhanghao: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 验证账号是否匹配(使用phone字段比较) + if user_main.phone != zhanghao: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.2 验证是否为管理员(检查user_type字段) + if user_main.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.3 验证管理员扩展表(使用AdminProfile模型) + try: + admin_profile = AdminProfile.objects.get(user=user_main) + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # === 2. 查询会员表数据 === + # 查询所有会员数据,按创建时间倒序排列 + huiyuan_queryset = Huiyuan.objects.all().order_by('-create_time') + + # 构建会员数据列表 + huiyuan_list = [] + for huiyuan in huiyuan_queryset: + huiyuan_data = { + 'huiyuan_id': huiyuan.huiyuan_id, # 会员ID + 'jieshao': huiyuan.jieshao, # 会员介绍(会员标题) + 'jtjieshao': huiyuan.jtjieshao if huiyuan.jtjieshao else '', # 会员规则介绍(具体介绍) + 'jiage': str(huiyuan.jiage), # 会员价格(转换为字符串,避免Decimal序列化问题) + 'guanshifc': str(huiyuan.guanshifc), # 管事分成(转换为字符串) + 'goumai_cishu': huiyuan.goumai_cishu, # 购买次数(销量) + 'create_time': huiyuan.create_time.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.create_time else None, + # 创建时间 + 'update_time': huiyuan.update_time.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.update_time else None + # 更新时间 + } + huiyuan_list.append(huiyuan_data) + + # === 3. 查询商品类型表数据 === + # 筛选抢单要求类型等于数字1的所有数据(yaoqiuleixing=1) + shangpinleixing_queryset = ShangpinLeixing.objects.filter( + yaoqiuleixing=1 # 抢单要求类型为1(会员抢单) + ).order_by('id') # 按ID排序 + + # 构建商品类型数据列表 + shangpinleixing_list = [] + for leixing in shangpinleixing_queryset: + # 注意:这里只需要特定的字段 + leixing_data = { + 'id': leixing.id, # 商品类型ID + 'huiyuan_id': leixing.huiyuan_id, # 会员ID(关联字段) + 'jieshao': leixing.jieshao, # 类型介绍(商品类型名称) + 'tupian_url': leixing.tupian_url if leixing.tupian_url else '' # 商品类型图片相对URL + } + shangpinleixing_list.append(leixing_data) + + # === 4. 构建返回数据 === + response_data = { + 'huiyuanList': huiyuan_list, + 'shangpinleixingList': shangpinleixing_list + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '获取数据成功', + 'data': response_data + }) + + except Exception as e: + # 捕获所有异常,返回500错误 + import traceback + traceback.print_exc() + return Response({ + 'code': 500, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + +class HuiyuanXiugaiView(APIView): + """ + 会员修改接口 + 需要管理员权限 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + # === 1. 权限验证 === + user_main = request.user + + # 1.1 验证前端传递的账号是否匹配(使用phone字段) + zhanghao = request.data.get('zhanghao', '').strip() + if not zhanghao: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if user_main.phone != zhanghao: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.2 验证是否为管理员(检查user_type字段) + if user_main.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.3 验证管理员扩展表 + try: + admin_profile = AdminProfile.objects.get(user=user_main) + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # === 2. 接收并验证必填字段 === + huiyuan_id = request.data.get('huiyuan_id', '').strip() + if not huiyuan_id: + return Response( + {'code': 400, 'msg': '会员ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 3. 查询会员数据 === + try: + huiyuan_obj = Huiyuan.objects.get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response( + {'code': 400, 'msg': '会员不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 4. 更新会员信息 === + # 4.1 获取前端传递的字段(只更新有值的字段) + jieshao = request.data.get('jieshao', '').strip() + jtjieshao = request.data.get('jtjieshao', '').strip() + jiage = request.data.get('jiage', '').strip() + guanshifc = request.data.get('guanshifc', '').strip() + + # 4.2 更新字段 + if jieshao: + # 验证名称长度(30个字符以内) + if len(jieshao) > 30: + return Response( + {'code': 400, 'msg': '会员名称不能超过30个字符', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + huiyuan_obj.jieshao = jieshao + + if jtjieshao: + # 验证介绍长度(没有长度限制,但做一下基本检查) + if len(jtjieshao) > 2000: + return Response( + {'code': 400, 'msg': '会员介绍过长', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + huiyuan_obj.jtjieshao = jtjieshao + + if jiage: + try: + jiage_value = float(jiage) + if jiage_value < 0: + return Response( + {'code': 400, 'msg': '会员价格不能为负数', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + if jiage_value > 100000: + return Response( + {'code': 400, 'msg': '会员价格不能超过10万元', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + huiyuan_obj.jiage = jiage_value + except ValueError: + return Response( + {'code': 400, 'msg': '会员价格格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if guanshifc: + try: + guanshifc_value = float(guanshifc) + if guanshifc_value < 0: + return Response( + {'code': 400, 'msg': '管事分成不能为负数', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + # 检查管事分成是否超过会员价格 + if hasattr(huiyuan_obj, 'jiage') and huiyuan_obj.jiage: + if guanshifc_value > float(huiyuan_obj.jiage): + return Response( + {'code': 400, 'msg': '管事分成不能超过会员价格', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + huiyuan_obj.guanshifc = guanshifc_value + except ValueError: + return Response( + {'code': 400, 'msg': '管事分成格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 4.3 检查是否有任何字段需要更新 + if not (jieshao or jtjieshao or jiage or guanshifc): + return Response( + {'code': 400, 'msg': '没有需要更新的字段', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 4.4 保存更新 + huiyuan_obj.save() + + # === 5. 构建返回数据 === + response_data = { + 'huiyuan_id': huiyuan_obj.huiyuan_id, + 'jieshao': huiyuan_obj.jieshao, + 'jtjieshao': huiyuan_obj.jtjieshao if huiyuan_obj.jtjieshao else '', + 'jiage': str(huiyuan_obj.jiage), + 'guanshifc': str(huiyuan_obj.guanshifc), + 'goumai_cishu': huiyuan_obj.goumai_cishu, + 'create_time': huiyuan_obj.create_time.strftime( + '%Y-%m-%d %H:%M:%S') if huiyuan_obj.create_time else None, + 'update_time': huiyuan_obj.update_time.strftime( + '%Y-%m-%d %H:%M:%S') if huiyuan_obj.update_time else None + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '会员信息修改成功', + 'data': response_data + }) + + except Exception as e: + # 捕获所有异常,返回500错误 + import traceback + traceback.print_exc() + return Response({ + 'code': 500, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + + +class AdHuiyuanFbView(APIView): + """ + 会员发布接口 + 需要管理员权限 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + # === 1. 权限验证 === + user_main = request.user + + # 1.1 验证前端传递的账号是否匹配 + zhanghao = request.data.get('zhanghao', '').strip() + if not zhanghao: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if user_main.phone != zhanghao: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.2 验证是否为管理员 + if user_main.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.3 验证管理员扩展表 + try: + admin_profile = AdminProfile.objects.get(user=user_main) + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # === 2. 接收并验证字段 === + jieshao = request.data.get('jieshao', '').strip() + jtjieshao = request.data.get('jtjieshao', '').strip() + jiage = request.data.get('jiage', '').strip() + guanshifc = request.data.get('guanshifc', '').strip() + + # 2.1 验证必填字段 + if not jieshao: + return Response( + {'code': 400, 'msg': '会员名称不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not jiage: + return Response( + {'code': 400, 'msg': '会员价格不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not guanshifc: + return Response( + {'code': 400, 'msg': '管事分成不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.2 验证字段长度和格式 + if len(jieshao) > 30: + return Response( + {'code': 400, 'msg': '会员名称不能超过30个字符', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if jtjieshao and len(jtjieshao) > 2000: + return Response( + {'code': 400, 'msg': '会员介绍过长', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.3 验证价格和分成格式 + try: + jiage_value = float(jiage) + if jiage_value <= 0: + return Response( + {'code': 400, 'msg': '会员价格必须大于0', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + if jiage_value > 100000: + return Response( + {'code': 400, 'msg': '会员价格不能超过10万元', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except ValueError: + return Response( + {'code': 400, 'msg': '会员价格格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + guanshifc_value = float(guanshifc) + if guanshifc_value < 0: + return Response( + {'code': 400, 'msg': '管事分成不能为负数', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + if guanshifc_value > jiage_value: + return Response( + {'code': 400, 'msg': '管事分成不能超过会员价格', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except ValueError: + return Response( + {'code': 400, 'msg': '管事分成格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 3. 生成会员ID === + # 生成6位唯一会员ID,格式:V + 5位随机数字字母 + def generate_huiyuan_id(): + # 使用V开头,后跟5位随机字符(数字和大写字母) + chars = string.digits + string.ascii_uppercase + while True: + random_part = ''.join(random.choices(chars, k=5)) + huiyuan_id = f"V{random_part}" + # 检查是否已存在 + if not Huiyuan.objects.filter(huiyuan_id=huiyuan_id).exists(): + return huiyuan_id + + huiyuan_id = generate_huiyuan_id() + + # === 4. 创建会员记录 === + with transaction.atomic(): + huiyuan_obj = Huiyuan( + huiyuan_id=huiyuan_id, + jieshao=jieshao, + jtjieshao=jtjieshao if jtjieshao else '', + jiage=jiage_value, + guanshifc=guanshifc_value, + goumai_cishu=0, # 初始购买次数为0 + ) + + huiyuan_obj.save() + + # === 5. 构建返回数据 === + response_data = { + 'huiyuan_id': huiyuan_obj.huiyuan_id, + 'jieshao': huiyuan_obj.jieshao, + 'jtjieshao': huiyuan_obj.jtjieshao, + 'jiage': str(huiyuan_obj.jiage), + 'guanshifc': str(huiyuan_obj.guanshifc), + 'goumai_cishu': huiyuan_obj.goumai_cishu, + 'create_time': huiyuan_obj.create_time.strftime( + '%Y-%m-%d %H:%M:%S') if huiyuan_obj.create_time else None, + 'update_time': huiyuan_obj.update_time.strftime( + '%Y-%m-%d %H:%M:%S') if huiyuan_obj.update_time else None + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '会员发布成功', + 'data': response_data + }) + + except Exception as e: + # 捕获所有异常,返回500错误 + import traceback + traceback.print_exc() + return Response({ + 'code': 500, + 'msg': f'系统错误: {str(e)}', + '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): + """ + 后台商品管理页面数据获取接口 + 路径: /shangpin/adsphq/ + 方法: POST + 权限: JWT认证 + 管理员权限 + 功能: 获取商品管理页面需要的所有数据 + """ + + permission_classes = [IsAuthenticated] + + 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 + ) + + # 2. 从JWT Token获取当前用户 + dangqianyonghu = request.user + + # 3. 验证账号匹配 + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 4. 验证用户类型为管理员 + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 5. 验证管理员扩展表数据 + try: + guanliyuan = dangqianyonghu.admin_profile + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 6. 查询公告数据(类型=1的公告) + gonggao_obj = Gonggao.objects.filter(leixing=1).first() + shangpingonggao = gonggao_obj.jieshao if gonggao_obj else '' + + # 7. 查询轮播图数据(类型=1的轮播图) + lunbo_objs = Lunbo.objects.filter(leixing=1).values('tupian_url') + shangpinlunbo = [item['tupian_url'] for item in lunbo_objs] + + # 8. 查询商品类型数据 + shangpinleixing_data = ShangpinLeixing.objects.all().values( + 'id', # 类型ID + 'jieshao', # 类型介绍(类型名称) + 'tupian_url', # 类型图片URL + 'yaoqiuleixing', # 抢单要求类型(1=会员抢单,2=佣金抢单) + 'huiyuan_id', # 会员ID(如果yaoqiuleixing=1) + 'yongjin' # 佣金要求(如果yaoqiuleixing=2) + ) + + # 9. 查询商品专区数据 + shangpinzhuanqu_data = ShangpinZhuanqu.objects.all().values( + 'id', # 专区ID + 'mingzi', # 专区名称 + 'leixing_id' # 所属类型ID + ) + + # 10. 查询会员数据 + huiyuan_data = Huiyuan.objects.all().values( + 'huiyuan_id', # 会员ID + 'jieshao', # 会员介绍(会员名称) + 'jiage' # 会员价格 + ) + + # 11. 查询商品数据 + shangpin_data = Shangpin.objects.all().values( + 'id', # 商品ID + 'biaoqian', # 商品标题/标签 + 'jiage', # 商品价格 + 'leixing_id', # 商品类型ID + 'zhuanqu_id', # 商品专区ID + 'tupian_url', # 商品图片URL + 'duiwai_xiaoliang' # 商品对外销量 + ) + + # 12. 构造返回数据 + response_data = { + 'code': 0, + 'message': '获取成功', + 'data': { + 'shangpingonggao': shangpingonggao, # 商品公告 + 'shangpinlunbo': shangpinlunbo, # 轮播图相对URL数组 + 'shangpinleixing': list(shangpinleixing_data), # 商品类型列表 + 'shangpinzhuanqu': list(shangpinzhuanqu_data), # 商品专区列表 + 'huiyuanliebiao': list(huiyuan_data), # 会员列表 + 'shangpinliebiao': list(shangpin_data), # 商品列表 + } + } + + return Response(response_data, status=status.HTTP_200_OK) + + except Exception as e: + print(f"获取商品管理数据异常: {e}") + return Response( + {'code': 500, 'message': '服务器内部错误'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + +# 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): + """ + 后台编辑公告接口 + 路径: /shangpin/adggxg/ + 方法: POST + 权限: JWT认证 + 管理员权限 + 功能: 修改公告内容 + """ + + permission_classes = [IsAuthenticated] + + 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 + ) + + # 2. 从JWT Token获取当前用户 + dangqianyonghu = request.user + + # 3. 验证账号匹配 + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 4. 验证用户类型为管理员 + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 5. 验证管理员扩展表数据 + try: + guanliyuan = dangqianyonghu.admin_profile + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 6. 获取公告内容 + gonggao_neirong = request.data.get('gonggao', '').strip() + + # 7. 验证公告内容 + if not gonggao_neirong: + return Response( + {'code': 400, 'message': '公告内容不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if len(gonggao_neirong) > 300: + return Response( + {'code': 400, 'message': '公告内容不能超过300字', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 8. 查询公告表,类型为1的公告 + gonggao_obj = Gonggao.objects.filter(leixing=1).first() + + # 如果不存在,则创建 + if not gonggao_obj: + gonggao_obj = Gonggao(leixing=1) + + # 9. 更新公告内容 + gonggao_obj.jieshao = gonggao_neirong + gonggao_obj.save() + + # 10. 返回成功响应 + return Response({ + 'code': 0, + 'message': '公告更新成功', + 'data': { + 'gonggao': gonggao_neirong + } + }) + + except Exception as e: + print(f"编辑公告异常: {e}") + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +class AdZhuanquXiuGai(APIView): + """ + 后台修改专区名称接口 + 路径: /shangpin/adzqxg/ + 方法: POST + 权限: JWT认证 + 管理员权限 + 功能: 修改专区名称 + """ + + permission_classes = [IsAuthenticated] + + 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和新的专区名称 + zhuanqu_id = request.data.get('zhuanqu_id') + mingzi = request.data.get('mingzi', '').strip() + + if not zhuanqu_id: + return Response( + {'code': 400, 'message': '专区ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not mingzi: + return Response( + {'code': 400, 'message': '专区名称不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if len(mingzi) > 10: + return Response( + {'code': 400, 'message': '专区名称不能大于10个字', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 3. 查询专区 + try: + zhuanqu_obj = ShangpinZhuanqu.objects.get(id=zhuanqu_id) + except ShangpinZhuanqu.DoesNotExist: + return Response( + {'code': 400, 'message': '专区不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 4. 更新专区名称 + zhuanqu_obj.mingzi = mingzi + zhuanqu_obj.save() + + # 5. 返回成功响应 + return Response({ + 'code': 0, + 'message': '专区名称修改成功', + 'data': { + 'id': zhuanqu_obj.id, + 'mingzi': zhuanqu_obj.mingzi, + 'leixing_id': zhuanqu_obj.leixing_id + } + }) + + except Exception as e: + print(f"修改专区名称异常: {e}") + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + + +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): + """ + 后台编辑轮播图接口 + 路径: /shangpin/adlunboxg/ + 方法: POST + 权限: JWT认证 + 管理员权限 + 功能: 添加或删除轮播图 + """ + + permission_classes = [IsAuthenticated] + parser_classes = (MultiPartParser, FormParser,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. 获取操作类型 + action = request.data.get('action', '').strip() # 'add' 或 'delete' + + if action not in ['add', 'delete']: + return Response( + {'code': 400, 'message': '操作类型无效', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 3. 处理删除操作 + if action == 'delete': + tupian_url = request.data.get('tupian_url', '').strip() + + if not tupian_url: + return Response( + {'code': 400, 'message': '图片URL不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 查询轮播图记录 + lunbo_obj = Lunbo.objects.filter(tupian_url=tupian_url, leixing=1).first() + + if not lunbo_obj: + return Response( + {'code': 400, 'message': '轮播图不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 从OSS删除图片 + delete_success = delete_from_oss(tupian_url) + + if not delete_success: + return Response( + {'code': 500, 'message': 'OSS删除失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 从数据库删除记录 + lunbo_obj.delete() + + return Response({ + 'code': 0, + 'message': '删除成功', + 'data': None + }) + + # 4. 处理添加操作 + else: # action == 'add' + tupian_file = request.FILES.get('file') + + if not tupian_file: + + + return Response( + {'code': 400, 'message': '请上传图片', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证图片 + is_valid, error_msg = validate_image(tupian_file) + if not is_valid: + #print(f"[轮播图上传] 图片验证失败详情: {error_msg}") + # 还可以记录文件大小、名称等信息 + #print(f"[轮播图上传] 问题文件: {tupian_file.name}, Size: {tupian_file.size}") + #print(is_valid) + return Response( + {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 生成文件名和路径 + file_extension = os.path.splitext(tupian_file.name)[1].lower() + if not file_extension: + file_extension = '.jpg' + + # 生成6位随机字符串作为文件名 + random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) + unique_filename = f"{random_str}{file_extension}" + + # 构建OSS文件路径:a_long/lunbo/{filename} + oss_folder = 'a_long' + lunbo_folder = 'lunbo' + + oss_file_path = f"{oss_folder}/{lunbo_folder}/{unique_filename}" + + # 上传到OSS + tupian_url = upload_to_oss(tupian_file, oss_file_path) + + if not tupian_url: + return Response( + {'code': 500, 'message': '图片上传失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 创建轮播图记录 + lunbo_obj = Lunbo.objects.create( + tupian_url=oss_file_path, # 存相对路径 + leixing=1 + ) + + return Response({ + 'code': 0, + 'message': '添加成功', + 'data': { + 'tupian_url': lunbo_obj.tupian_url + } + }) + + except Exception as e: + #print(f"编辑轮播图异常: {e}") + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +'''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)''' + + + + + + + +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() + # 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) + + + +logger = logging.getLogger(__name__) + + +class AdShangpinNeiRongHuoQu(APIView): + """ + 后台获取商品详情接口 + 路径: /shangpin/adspnrhq/ + 方法: POST + 权限: JWT认证 + 管理员权限 + 功能: 获取指定商品的详细信息 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [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 + ) + + # 2. 权限验证 + dangqianyonghu = request.user + + # 验证phone字段匹配 + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + logger.warning( + f"账号不匹配: 当前用户phone={getattr(dangqianyonghu, 'phone', '无')}, 前端账号={qianzhanghao}") + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 验证用户类型 + if dangqianyonghu.user_type != 'admin': + logger.warning(f"非管理员访问: user_type={dangqianyonghu.user_type}") + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 3. 验证管理员扩展表 + try: + guanliyuan = dangqianyonghu.admin_profile + except AdminProfile.DoesNotExist: + logger.warning(f"管理员扩展表不存在: user_id={dangqianyonghu.id}") + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 4. 查询商品数据 + 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 + ) + + # 5. 构建返回数据(与前端字段对应) + response_data = { + 'tupian_url': shangpin_obj.tupian_url or '', # 商品图片相对URL + 'biaoti': shangpin_obj.biaoqian or '', # 商品标题(前端用biaoti,对应biaoqian) + 'jieshao': shangpin_obj.jieshao or '', # 商品介绍 + 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00', # 商品价格 + 'kucun': shangpin_obj.kucun, # 商品库存 + 'zhenshixiaoliang': shangpin_obj.zhenshi_xiaoliang, # 商品真实销量 + 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang, # 商品对外销量 + 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '', # 下单须知 + 'guizetupian_url': shangpin_obj.guize_tupian or '', # 规则图片相对URL + 'yajin': str(shangpin_obj.yajin) if hasattr(shangpin_obj, 'yajin') and shangpin_obj.yajin else None, + # 押金(可能不存在) + 'chuangjianshijian': shangpin_obj.create_time.strftime( + '%Y-%m-%d %H:%M:%S') if shangpin_obj.create_time else '', + 'genggaishijian': shangpin_obj.update_time.strftime( + '%Y-%m-%d %H:%M:%S') if shangpin_obj.update_time else '', + # 额外字段,前端可能不需要但可以保留 + 'leixing_id': shangpin_obj.leixing_id, + 'zhuanqu_id': shangpin_obj.zhuanqu_id, + 'yaoqiuleixing': shangpin_obj.yaoqiuleixing, + 'huiyuan_id': shangpin_obj.huiyuan_id, + 'yongjin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None, + } + + 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.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): + """ + 后台更改商品数据接口 - 修复版 + 修复: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 + + # 获取原图片URL(如果有) + old_tupian_url = shangpin_obj.tupian_url + + # 3. 处理商品图片上传(如果有新商品图片) + tupian_file = request.FILES.get('tupian_file') + new_tupian_url = None + + 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 + ) + + # 生成新文件名:时间戳 + 随机数 + 原扩展名 + timestamp = int(time.time() * 1000) % 100000000 # 8位时间戳 + random_num = random.randint(1000, 9999) + file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg' + new_filename = f"sp_{timestamp}_{random_num}{file_extension}" + + # 确定OSS路径 + oss_folder = 'a_long' + shangpin_folder = 'shangpin' + oss_file_path = f"{oss_folder}/{shangpin_folder}/{new_filename}" + + # 删除旧图片(如果存在) + if old_tupian_url: + try: + delete_from_oss(old_tupian_url) + logger.info(f"已删除旧商品图片: {old_tupian_url}") + except Exception as e: + logger.warning(f"删除旧商品图片失败: {old_tupian_url}, 错误: {e}") + # 继续上传新图片,不中断流程 + + # 上传到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 + logger.info(f"商品图片更新成功: {oss_file_path}") + + # 4. 处理其他字段(只更新有变化的字段) + # 注意:这里同时处理 request.data(文本字段)和可能有文件的情况 + 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(): + # 注意:request.data 可能包含文件和其他字段,但DRF会处理好 + 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 + ) + + +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 delete_success: + logger.info(f"已删除规则图片: {guizetupian_url}") + else: + 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 + ) + + # 获取原规则图片URL + old_guizetupian_url = shangpin_obj.guize_tupian + + # 生成新文件名:时间戳 + 随机数 + 原扩展名 + timestamp = int(time.time() * 1000) % 100000000 # 8位时间戳 + random_num = random.randint(1000, 9999) + file_extension = os.path.splitext(guize_file.name)[1].lower() or '.jpg' + new_filename = f"gz_{timestamp}_{random_num}{file_extension}" + + # 确定OSS路径 + oss_folder = 'a_long' + guize_folder = 'guize' # 修改为你的要求:a_long/guize/ + oss_file_path = f"{oss_folder}/{guize_folder}/{new_filename}" + + # 删除旧图片(如果存在) + if old_guizetupian_url: + try: + delete_from_oss(old_guizetupian_url) + #logger.info(f"已删除旧规则图片: {old_guizetupian_url}") + except Exception as e: + logger.warning(f"删除旧规则图片失败: {old_guizetupian_url}, 错误: {e}") + # 继续上传新图片,不中断流程 + + # 上传到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 + shangpin_obj.save() + + #logger.info(f"规则图片更新成功: {oss_file_path}") + + return Response({ + 'code': 0, + 'message': '规则图片更新成功', + 'data': { + 'guizetupian_url': oss_file_path # 返回相对路径 + } + }) + + + + + + + + + + + + +# 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): + """ + 获取会员类型列表接口 + 路径:/shangpin/adhyjllxcx + 方法:POST + 权限:JWT Token + 管理员验证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 🔥 1. 身份验证 + user = request.user + + # 检查是否是管理员 + if not hasattr(user, 'admin_profile'): + return Response({ + 'code': 401, + 'message': '无权限访问', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 🔥 2. 获取所有会员类型 + huiyuan_list = Huiyuan.objects.all().order_by('create_time') + + # 🔥 3. 构建返回数据 + huiyuan_data = [] + for huiyuan in huiyuan_list: + huiyuan_data.append({ + 'id': huiyuan.huiyuan_id, # 会员ID + 'jieshao': huiyuan.jieshao, # 会员介绍 + 'jiage': str(huiyuan.jiage), # 价格 + 'goumai_cishu': huiyuan.goumai_cishu, # 购买次数 + 'create_time': huiyuan.create_time.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.create_time else None + }) + + return Response({ + 'code': 0, + 'message': '获取成功', + 'data': huiyuan_data + }, status=status.HTTP_200_OK) + + except Exception as e: + print(f"获取会员类型列表失败: {str(e)}") + return Response({ + 'code': 500, + 'message': '系统错误', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + +class HuiyuanRecordListView(APIView): + """ + 获取会员记录列表接口 + 路径:/shangpin/adhyjlcxhq + 方法:POST + 权限:JWT Token + 管理员验证 + 参数: + - zhanghao: 管理员账号 + - huiyuan_type: 会员类型ID + - jiluzhuangtai: 记录状态(1=未过期, 2=已过期) + - page: 页码 + - pagesize: 每页条数 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 🔥 1. 身份验证 + user = request.user + + # 检查是否是管理员 + if not hasattr(user, 'admin_profile'): + return Response({ + 'code': 401, + 'message': '无权限访问', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 🔥 2. 获取参数 + zhanghao = request.data.get('zhanghao', '').strip() + huiyuan_type = request.data.get('huiyuan_type', '').strip() + jiluzhuangtai = request.data.get('jiluzhuangtai', 1) # 默认未过期 + page = int(request.data.get('page', 1)) + pagesize = int(request.data.get('pagesize', 5)) + + # 🔥 3. 参数验证 + if not zhanghao: + return Response({ + 'code': 400, + 'message': '管理员账号不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not huiyuan_type: + return Response({ + 'code': 400, + 'message': '会员类型不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 🔥 4. 检查会员类型是否存在 + huiyuan_obj = Huiyuan.objects.filter(huiyuan_id=huiyuan_type).first() + if not huiyuan_obj: + return Response({ + 'code': 404, + 'message': '会员类型不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 🔥 5. 构建基础查询(一次性获取所有符合条件的记录ID) + now = timezone.now() + + # 🔥 使用原生SQL优化查询(避免N+1问题) + with connection.cursor() as cursor: + # 先查询符合条件的总记录数 + count_sql = """ + SELECT COUNT(*) + FROM huiyuangoumai hg + WHERE hg.huiyuan_id = %s + """ + params = [huiyuan_type] + + if jiluzhuangtai == 1: # 未过期 + count_sql += " AND hg.daoqi_time > %s" + params.append(now) + else: # 已过期 + count_sql += " AND hg.daoqi_time <= %s" + params.append(now) + + cursor.execute(count_sql, params) + total_count = cursor.fetchone()[0] + + # 🔥 6. 分页查询(使用窗口函数提高性能) + offset = (page - 1) * pagesize + + record_sql = """ + SELECT + hg.id, + hg.yonghu_id, + hg.huiyuan_id, + hg.jieshao, + hg.daoqi_time, + hg.create_time, + um.avatar, + ud.nicheng + FROM huiyuangoumai hg + LEFT JOIN user_main um ON hg.yonghu_id = um.yonghuid + LEFT JOIN user_dashou ud ON um.id = ud.user_id + WHERE hg.huiyuan_id = %s + """ + + record_params = [huiyuan_type] + + if jiluzhuangtai == 1: # 未过期 + record_sql += " AND hg.daoqi_time > %s" + record_params.append(now) + else: # 已过期 + record_sql += " AND hg.daoqi_time <= %s" + record_params.append(now) + + record_sql += " ORDER BY hg.create_time DESC LIMIT %s OFFSET %s" + record_params.extend([pagesize, offset]) + + cursor.execute(record_sql, record_params) + records = cursor.fetchall() + + # 🔥 7. 处理查询结果 + record_list = [] + for record in records: + record_list.append({ + 'id': record[0], + 'yonghuid': record[1], # 用户ID + 'huiyuan_id': record[2], + 'jieshao': record[3], + 'daoqi_time': record[4].strftime('%Y-%m-%d %H:%M:%S') if record[4] else None, + 'create_time': record[5].strftime('%Y-%m-%d %H:%M:%S') if record[5] else None, + 'touxiang': record[6], # 头像相对URL + 'nicheng': record[7] or record[1], # 昵称或用户ID + 'yonghu_type': 2 # 🔥 默认打手类型(实际应该根据user_type判断) + }) + + # 🔥 8. 判断是否还有更多数据 + has_more = (page * pagesize) < total_count + + # 🔥 9. 获取会员类型统计信息(所有会员类型的当前状态总数) + huiyuan_types = Huiyuan.objects.all() + huiyuan_stats = [] + + for huiyuan in huiyuan_types: + # 使用COUNT查询统计 + stats_sql = """ + SELECT COUNT(*) + FROM huiyuangoumai + WHERE huiyuan_id = %s + """ + stats_params = [huiyuan.huiyuan_id] + + if jiluzhuangtai == 1: # 未过期 + stats_sql += " AND daoqi_time > %s" + stats_params.append(now) + else: # 已过期 + stats_sql += " AND daoqi_time <= %s" + stats_params.append(now) + + with connection.cursor() as cursor: + cursor.execute(stats_sql, stats_params) + count = cursor.fetchone()[0] + + huiyuan_stats.append({ + 'id': huiyuan.huiyuan_id, + 'jieshao': huiyuan.jieshao, + 'count': count + }) + + return Response({ + 'code': 0, + 'message': '获取成功', + 'data': { + 'list': record_list, + 'total': total_count, + 'current_page': page, + 'page_size': pagesize, + 'has_more': has_more, + 'huiyuan_stats': huiyuan_stats, # 🔥 所有会员类型的统计 + 'current_huiyuan': { + 'id': huiyuan_obj.huiyuan_id, + 'jieshao': huiyuan_obj.jieshao, + 'count': total_count + } + } + }, 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) + + + + + + + + + + +import decimal + +class ZuzhangFenhongView(APIView): + """ + 组长分红记录接口 + 路径:/shangpin/zuzhangfenhong + 方法:POST + 权限:JWT认证,仅限组长身份 + 请求参数: + { + "page": 1, # 页码,默认1 + "page_size": 5 # 每页条数,默认5 + } + 返回: + { + "code": 200, + "msg": "success", + "data": { + "fenhong_zonge": "1234.56", # 分红总额 + "yaoqing_guanshi": 10, # 邀请管事数 + "fenhong_cishu": 50, # 分红次数 + "total": 50, # 总记录数 + "has_more": false, # 是否有下一页 + "list": [...] # 记录列表 + } + } + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + current_user = request.user + + # 1. 验证组长身份 + try: + zuzhang = UserZuzhang.objects.get(user=current_user) + except UserZuzhang.DoesNotExist: + return Response({ + 'code': 403, + 'msg': '当前用户不是组长身份', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 2. 请求参数 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 5)) + page = max(page, 1) + page_size = max(page_size, 1) + + # 3. 统计数据 + # 3.1 分红总额(从组长扩展表) + fenhong_zonge = zuzhang.fenyong_zonge + + # 3.2 邀请管事数(统计邀请人等于当前用户ID的管事记录) + yaoqing_guanshi = UserGuanshi.objects.filter(yaoqingren=current_user.yonghuid).count() + + # 3.3 分红次数(总记录数) + base_queryset = Gsfenhong.objects.filter(zuzhang_id=current_user.yonghuid).order_by('-create_time') + fenhong_cishu = base_queryset.count() + + # 4. 分页查询 + start = (page - 1) * page_size + end = start + page_size + records = base_queryset[start:end] + + # 5. 批量获取管事用户信息(只查 boss_profile 获取昵称) + guanshi_ids = list(set(rec.guanshi for rec in records if rec.guanshi)) + guanshi_info_map = self._get_guanshi_info(guanshi_ids) + + # 6. 组装列表数据 + list_data = [] + for rec in records: + # 打手信息(直接从记录中取快照) + dashou_avatar = rec.avatar or '' + dashou_nicheng = rec.nicheng or '' + dashou_yonghuid = rec.dashouid or '' + + # 管事信息 + guanshi_id = rec.guanshi + guanshi_info = guanshi_info_map.get(guanshi_id, {'avatar': '', 'nicheng': ''}) + guanshi_avatar = guanshi_info['avatar'] + guanshi_nicheng = guanshi_info['nicheng'] + + # 组长分红金额 + fenhong = rec.zuzhang_fenhong + if fenhong is None: + fenhong = decimal.Decimal('0.00') + fenhong_str = str(fenhong.quantize(decimal.Decimal('0.00'))) + + # 时间格式化 + create_time = rec.create_time.strftime('%Y-%m-%d %H:%M:%S') if rec.create_time else '' + + list_data.append({ + 'dashou_avatar': dashou_avatar, + 'dashou_nicheng': dashou_nicheng, + 'dashou_yonghuid': dashou_yonghuid, + 'guanshi_avatar': guanshi_avatar, + 'guanshi_nicheng': guanshi_nicheng, + 'guanshi_yonghuid': guanshi_id, + 'fenhong': fenhong_str, + 'create_time': create_time, + }) + + # 7. 是否有更多数据 + has_more = (end < fenhong_cishu) + + return Response({ + 'code': 200, + 'msg': 'success', + 'data': { + 'fenhong_zonge': str(fenhong_zonge.quantize(decimal.Decimal('0.00'))), + 'yaoqing_guanshi': yaoqing_guanshi, + 'fenhong_cishu': fenhong_cishu, + 'total': fenhong_cishu, + 'has_more': has_more, + 'list': list_data, + } + }) + + def _get_guanshi_info(self, guanshi_ids): + """ + 批量获取管事用户的头像和昵称 + 昵称从老板扩展表(boss_profile)获取,头像从主表获取 + 返回字典:{用户ID: {'avatar': 相对路径, 'nicheng': 昵称}} + """ + if not guanshi_ids: + return {} + + # 查询用户主表,并预加载 boss_profile + users = UserMain.objects.filter(yonghuid__in=guanshi_ids).select_related( + 'boss_profile' + ).only( + 'yonghuid', 'avatar', + 'boss_profile__nickname' + ) + + info = {} + for user in users: + avatar = user.avatar or '' + # 从 boss_profile 获取昵称 + if hasattr(user, 'boss_profile') and user.boss_profile: + nicheng = user.boss_profile.nickname or '' + else: + # 如果没有 boss_profile,使用默认昵称 + nicheng = f"用户{user.yonghuid[-4:]}" + info[user.yonghuid] = {'avatar': avatar, 'nicheng': nicheng} + + # 对于未查询到的用户,提供默认值 + for gid in guanshi_ids: + if gid not in info: + info[gid] = {'avatar': '', 'nicheng': f"用户{gid[-4:]}"} + + return info + + + + + +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): + """ + 获取可用抵扣身份接口 + 路径:/shangpin/czhqdy + 方法:POST + 权限:JWT认证 + 请求参数: + leixing: int # 1=会员,2=押金,3=积分,4=罚款 + huiyuan_id: str # 当 leixing=1 时必传,会员ID + yajin_jine: decimal # 当 leixing=2 时必传,押金金额(元) + fadan_id: int # 当 leixing=4 时必传,罚单ID + 返回: + { + "code": 200, + "msg": "success", + "data": [ + { + "id": 1, # 身份ID:1打手佣金 2管事分红 3组长分红 4打手押金 + "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, + }, + # 新增打手押金抵扣 + { + 'id': 4, + 'name': '打手押金抵扣', + 'profile_attr': 'dashou_profile', # 与打手佣金共用扩展表 + 'balance_field': 'yajin', # 押金字段 + 'rate_key': '11', # 押金提现费率对应的 fadanpingtai + 'model': UserDashou, + }, + ] + + def post(self, request): + # 获取参数 + leixing = request.data.get('leixing') + huiyuan_id = request.data.get('huiyuan_id') + yajin_jine = request.data.get('yajin_jine') + fadan_id = request.data.get('fadan_id') + + # 参数校验 + 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 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) + + if leixing == 4 and not fadan_id: + return Response({'code': 400, 'msg': '缴纳罚款时必须提供 fadan_id'}, 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 == 3: + # 积分(固定价格) + target_price = INTEGRAL_PRICE + elif leixing == 4: + # 罚款缴纳 + try: + fadan = Fadan.objects.get(id=fadan_id, beichufa_id=user.yonghuid) + target_price = fadan.fakuanjine + except Fadan.DoesNotExist: + return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND) + + # 获取所有费率(打手佣金、管事分红、组长分红、打手押金) + rates = {} + for key in ['5', '6', '8', '11']: + 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) ,向上取整到分 + try: + if rate >= 1: + continue # 防止除零及不合理费率 + required = target_price / (decimal.Decimal('1.0') - rate) + # 向上取整到分(保留两位小数,最后一位进位) + required = required.quantize(decimal.Decimal('0.01'), rounding=decimal.ROUND_CEILING) + except Exception: + 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 +# ========== 修改点1: 导入最新的分红处理方法 ========== +from yonghu.fadan_fenhong_utils import process_fadan_fenhong + +INTEGRAL_INCREMENT = 5 +MAX_INTEGRAL = 10 + +IDENTITY_CONFIG = { + 1: { + 'name': '打手佣金', + 'model': UserDashou, + 'balance_field': 'yue', + 'status_field': 'zhanghaozhuangtai', + 'rate_key': '5', + }, + 2: { + 'name': '管事分红', + 'model': UserGuanshi, + 'balance_field': 'yue', + 'status_field': 'zhuangtai', + 'rate_key': '6', + }, + 3: { + 'name': '组长分红', + 'model': UserZuzhang, + 'balance_field': 'ketixian_jine', + 'status_field': 'zhuangtai', + 'rate_key': '8', + }, + 4: { + 'name': '打手押金', + 'model': UserDashou, + 'balance_field': 'yajin', + 'status_field': 'zhanghaozhuangtai', + 'rate_key': '11', + }, +} + + +class DsqrgmdhView(APIView): + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + 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') + fadan_id = request.data.get('fadan_id') + + logger.info(f"抵扣支付请求: leixing={leixing}, shenfen_id={shenfen_id}, huiyuan_id={huiyuan_id}, yajin_jine={yajin_jine}, fadan_id={fadan_id}") + + 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, 4]: + return Response({'code': 400, 'msg': 'shenfen_id 必须为 1/2/3/4'}, 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 == 3: + target_price = INTEGRAL_PRICE + elif leixing == 4: + if not fadan_id: + return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST) + try: + fadan_obj = Fadan.objects.get(id=fadan_id, beichufa_id=request.user.yonghuid) + target_price = fadan_obj.fakuanjine + except Fadan.DoesNotExist: + return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND) + + identity = IDENTITY_CONFIG.get(shenfen_id) + try: + rate_obj = Lilubiao.objects.get(fadanpingtai=identity['rate_key']) + rate = rate_obj.lilu + 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) + + logger.info(f"目标价格={target_price}, 费率={rate}") + + try: + required = target_price / (Decimal('1.0') - rate) + required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING) + logger.info(f"所需抵扣金额={required}") + except Exception as e: + logger.error(f"金额计算错误: {str(e)}", exc_info=True) + 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']]) + logger.info(f"扣款成功: {identity['name']} 扣除 {required} 元,剩余 {current_balance - required} 元") + + 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 == 3: + action_desc = "补充积分" + elif leixing == 4: + action_desc = f"缴纳罚款 {target_price}元" + + 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 '', + } + 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: + if fadan_obj.zhuangtai not in [1, 3]: + return Response({'code': 400, 'msg': '罚单状态不可缴纳'}, status=status.HTTP_400_BAD_REQUEST) + fadan_obj.zhuangtai = 2 + fadan_obj.save(update_fields=['zhuangtai']) + response_data['fadan_id'] = fadan_obj.id + response_data['fadan_status'] = fadan_obj.zhuangtai + + # ========== 修改点2: 使用最新的分红处理方法 ========== + success, msg = process_fadan_fenhong(fadan_obj.id) + if not success: + logger.error(f"罚单 {fadan_obj.id} 分红处理失败: {msg}") + else: + logger.info(f"罚单 {fadan_obj.id} 分红处理成功: {msg}") + + 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) + 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: + logger.error(f"事务处理异常: {str(e)}", exc_info=True) + 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, + }) + + except Exception as e: + logger.error(f"接口顶层异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': f'系统错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def _process_member_purchase(self, yonghuid, huiyuan_id, huiyuan_obj): + 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 \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/admin.py b/utils/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/utils/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/utils/apps.py b/utils/apps.py new file mode 100644 index 0000000..3e82e49 --- /dev/null +++ b/utils/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class UtilsConfig(AppConfig): + name = "utils" diff --git a/utils/celery_utils.py b/utils/celery_utils.py new file mode 100644 index 0000000..b9b380f --- /dev/null +++ b/utils/celery_utils.py @@ -0,0 +1,114 @@ +""" +阿龙电竞 - Celery工具函数 +提供通用的Celery辅助函数,确保数据安全和操作稳定 +【修复版】已解决装饰器导致Celery任务无法识别的问题 +""" + +import logging +from django.db import transaction +from datetime import datetime +from decimal import Decimal, InvalidOperation +import functools # 【核心修复】必须导入此模块 + +# 配置日志 +logger = logging.getLogger(__name__) + +def safe_decimal_operation(value, default=Decimal('0.00')): + """ + 安全处理Decimal操作,防止类型转换错误 + :param value: 输入值 + :param default: 默认值 + :return: Decimal对象 + """ + try: + if value is None: + return default + + if isinstance(value, Decimal): + return value + + if isinstance(value, (int, float)): + return Decimal(str(value)) + + if isinstance(value, str): + # 移除可能的空白字符和特殊字符 + value = value.strip() + if not value: + return default + return Decimal(value) + + # 其他类型尝试转换 + return Decimal(str(value)) + except (InvalidOperation, TypeError, ValueError) as e: + logger.warning(f"Decimal转换失败: {e}, 输入值: {value}, 类型: {type(value)}, 使用默认值: {default}") + return default + except Exception as e: + logger.error(f"Decimal转换发生未知错误: {e}, 输入值: {value}, 使用默认值: {default}") + return default + +def log_task_execution(task_name, success=True, details=""): + """ + 记录任务执行日志 + :param task_name: 任务名称 + :param success: 是否成功 + :param details: 详细信息 + """ + try: + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + status = "成功" if success else "失败" + + log_message = f"[{timestamp}] {task_name} - {status}" + if details: + log_message += f" | {details}" + + if success: + logger.info(log_message) + else: + logger.error(log_message) + + return log_message + except Exception as e: + # 即使日志记录失败也不影响主流程 + print(f"日志记录失败: {e}") + return f"日志记录失败: {e}" + +def rollback_on_failure(operation_name): + """ + 事务回滚装饰器(修复版) + :param operation_name: 操作名称 + """ + def decorator(func): + @functools.wraps(func) # 【核心修复】保留原函数元数据,使Celery能正确识别任务 + def wrapper(*args, **kwargs): + try: + with transaction.atomic(): + return func(*args, **kwargs) + except Exception as e: + logger.error(f"{operation_name}事务执行失败: {str(e)}") + raise + return wrapper + return decorator + +def retry_on_failure(max_retries=3, delay=60): + """ + 重试装饰器(修复版,可用于Celery任务) + :param max_retries: 最大重试次数 + :param delay: 重试延迟(秒) + """ + def decorator(func): + @functools.wraps(func) # 【核心修复】保留原函数元数据 + def wrapper(*args, **kwargs): + retries = 0 + while retries < max_retries: + try: + return func(*args, **kwargs) + except Exception as e: + retries += 1 + if retries == max_retries: + logger.error(f"任务{func.__name__}重试{max_retries}次后失败: {str(e)}") + raise + logger.warning(f"任务{func.__name__}第{retries}次失败,{delay}秒后重试: {str(e)}") + import time + time.sleep(delay) + return wrapper + return decorator \ No newline at end of file diff --git a/utils/chat_utils.py b/utils/chat_utils.py new file mode 100644 index 0000000..d6e6bed --- /dev/null +++ b/utils/chat_utils.py @@ -0,0 +1,374 @@ +# utils/chat_utils.py +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 + +logger = logging.getLogger('chat_utils') + +# ========== 辅助配置获取(从 ClubConfig) ========== +def _get_self_club_config(): + if not hasattr(_get_self_club_config, '_config'): + try: + _get_self_club_config._config = ClubConfig.objects.filter(is_self=1).first() + except Exception as e: + logger.warning(f"获取我方俱乐部配置失败: {e}") + _get_self_club_config._config = None + return _get_self_club_config._config + +def _get_self_goeasy_appkey(): + cfg = _get_self_club_config() + if cfg and cfg.chat_api_key: + return cfg.chat_api_key + return getattr(settings, 'GOEASY_APPKEY', '') + +def _get_self_goeasy_secret(): + cfg = _get_self_club_config() + if cfg and cfg.chat_api_id: + return cfg.chat_api_id + return getattr(settings, 'GOEASY_SECRET', '') + +def _get_self_storage_domain(): + cfg = _get_self_club_config() + if cfg and cfg.storage_bucket_domain: + return cfg.storage_bucket_domain + return '' + +def _get_self_default_avatar(): + cfg = _get_self_club_config() + if cfg and cfg.club_avatar: + return cfg.club_avatar + return '' + +# ========== 头像拼接 ========== +def _full_local_avatar(relative_url): + storage_domain = _get_self_storage_domain() + default_avatar = _get_self_default_avatar() + if not relative_url: + return default_avatar or '' + if relative_url.startswith('http'): + return relative_url + if storage_domain.endswith('/'): + return storage_domain + relative_url.lstrip('/') + return storage_domain + '/' + relative_url if storage_domain else relative_url + +def _full_remote_avatar(storage_bucket_domain, relative_url): + if not relative_url: + return '' + if relative_url.startswith('http'): + return relative_url + if storage_bucket_domain.endswith('/'): + return storage_bucket_domain + relative_url.lstrip('/') + return storage_bucket_domain + '/' + relative_url if storage_bucket_domain else relative_url + +# ========== GoEasy 订阅 ========== +def _subscribe_users_to_group(user_ids, group_ids, appkey=None, secret=None): + if not appkey: + appkey = _get_self_goeasy_appkey() + if not secret: + secret = _get_self_goeasy_secret() + if not appkey: + logger.error("GoEasy AppKey 未配置") + return False + url = 'https://rest-hangzhou.goeasy.io/v2/im/subscribe-groups' + body = {"appkey": appkey, "userIds": user_ids, "groupIds": group_ids} + headers = {"Content-Type": "application/json"} + if secret: + headers["Authorization"] = f"Bearer {secret}" + try: + resp = requests.post(url, headers=headers, json=body, timeout=10) + if resp.status_code == 200: + return True + logger.error(f"订阅失败: {resp.status_code} {resp.text}") + return False + except Exception as e: + logger.error(f"订阅异常: {e}", exc_info=True) + return False + +# ========== 发送群消息(完整参数) ========== +def _send_group_message(appkey, secret, group_id, sender_id, sender_name, sender_avatar, + message_text, custom_payload=None, group_name=None, order_id=None, is_cross=0): + if not appkey: + appkey = _get_self_goeasy_appkey() + if not secret: + secret = _get_self_goeasy_secret() + if not appkey: + logger.error("GoEasy AppKey 未配置") + return False + + url = 'https://rest-hangzhou.goeasy.io/v2/im/message' + + # to.data 包含群聊展示信息及业务字段 + to_data = { + "name": group_name or group_id, + "avatar": sender_avatar or "", + } + if order_id: + to_data["orderId"] = order_id + to_data["isCross"] = is_cross + + payload_str = custom_payload.get("text", message_text) if custom_payload else message_text + + request_body = { + "appkey": appkey, + "senderId": sender_id, + "senderData": {"avatar": sender_avatar, "name": sender_name}, + "to": { + "type": "group", + "id": group_id, + "data": to_data + }, + "type": "text", + "payload": payload_str + } + headers = {"Content-Type": "application/json"} + if secret: + headers["Authorization"] = f"Bearer {secret}" + try: + resp = requests.post(url, headers=headers, json=request_body, timeout=10) + if resp.status_code == 200: + return True + logger.error(f"群聊消息发送失败,状态码:{resp.status_code},响应:{resp.text}") + return False + except Exception as e: + logger.error(f"发送群聊消息异常: {e}", exc_info=True) + return False + +# ========== 核心入口 ========== +def establish_order_chat(dingdan_id): + try: + order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + logger.error(f"订单 {dingdan_id} 不存在") + return False + + dashou_uid = order.jiedan_dashou_id + dashou_goeasy_id = f"Ds{dashou_uid}" + dashou_nickname = f'打手{dashou_uid[:6]}' + dashou_avatar_relative = '' + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_uid) + dashou_avatar_relative = dashou_user.avatar or '' + dashou_profile = UserDashou.objects.get(user=dashou_user) + if dashou_profile.nicheng: + dashou_nickname = dashou_profile.nicheng + except Exception as e: + logger.warning(f"获取打手信息失败: {e}") + dashou_avatar = _full_local_avatar(dashou_avatar_relative) + + appkey = _get_self_goeasy_appkey() + secret = _get_self_goeasy_secret() + if not appkey: + logger.error("GoEasy AppKey 未配置") + return False + + # 判断是否跨平台 + if order.is_cross != 1: + return _handle_local_order(order, dashou_goeasy_id, dashou_nickname, dashou_avatar, appkey, secret) + else: + if order.dispatch_type == 1: + # 我方派单,与普通订单处理一致 + return _handle_local_order(order, dashou_goeasy_id, dashou_nickname, dashou_avatar, appkey, secret) + elif order.dispatch_type == 2: + return _handle_cross_partner_order(order, dashou_goeasy_id, dashou_nickname, dashou_avatar, appkey, secret) + else: + return False + +# ========== 普通订单 / 我方派单 ========== +def _handle_local_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret): + group_id = f"group_{order.dingdan_id}" + group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else (order.jieshao or f"订单{order.dingdan_id}") + group_avatar = _full_local_avatar(order.tupian) if order.tupian else _full_local_avatar('') + + partner_goeasy_id, partner_name, partner_avatar = _get_local_partner_info(order) + if not partner_goeasy_id: + return False + + # 订阅双方 + if not _subscribe_users_to_group([dashou_goeasy_id, partner_goeasy_id], [group_id], appkey, secret): + return False + + # 1. 打手发送初始化消息 + init_msg_text = f"订单已接单,内容:{order.jieshao},备注:{order.beizhu},游戏ID:{order.nicheng}" + success1 = _send_group_message( + appkey, secret, group_id, dashou_goeasy_id, dashou_name, group_avatar, + init_msg_text, None, group_name=group_name, order_id=order.dingdan_id, + is_cross=order.is_cross + ) + + # 2. 【新增】派单方也发一条消息,确保他也能看到群聊 + partner_msg = f"订单已确认,请开始服务。" + success2 = _send_group_message( + appkey, secret, group_id, partner_goeasy_id, partner_name, partner_avatar, + partner_msg, None, group_name=group_name, order_id=order.dingdan_id, + is_cross=order.is_cross + ) + + return success1 and success2 + +def _get_local_partner_info(order): + """本地订单下单方:老板或商家""" + if order.fadan_pingtai == 1: # 老板 + try: + ext = order.pingtai_kuozhan + laoban_id = ext.laoban_id + avatar_full = _full_local_avatar('') + nickname = f'老板{laoban_id[:6]}' + try: + boss_user = UserMain.objects.get(yonghuid=laoban_id) + avatar_full = _full_local_avatar(boss_user.avatar or '') + boss_profile = UserBoss.objects.get(user=boss_user) + if boss_profile.nickname: + nickname = boss_profile.nickname + except Exception: + pass + return f"Boss{laoban_id}", nickname, avatar_full + except Exception as e: + logger.error(f"获取老板信息失败: {e}") + return None, None, None + elif order.fadan_pingtai == 2: # 商家 + try: + ext = order.shangjia_kuozhan + shangjia_id = ext.shangjia_id + nickname = ext.sjnicheng or f'商家{shangjia_id[:6]}' + avatar_full = _full_local_avatar('') + try: + sj_user = UserMain.objects.get(yonghuid=shangjia_id) + avatar_full = _full_local_avatar(sj_user.avatar or '') + except Exception: + pass + return f"Sj{shangjia_id}", nickname, avatar_full + except Exception as e: + logger.error(f"获取商家信息失败: {e}") + return None, None, None + else: + if order.user1_id: + return order.user1_id, "用户", _full_local_avatar('') + return None, None, None + +# ========== 跨平台 + 对方派单 ========== +def _handle_cross_partner_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret): + partner_club_id = order.partner_club_id + if not partner_club_id: + logger.warning("缺少 partner_club_id") + return _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret) + + try: + partner_config = ClubConfig.objects.get(club_id=partner_club_id, is_self=0) + except ClubConfig.DoesNotExist: + logger.error(f"未找到对方俱乐部配置: {partner_club_id}") + return _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret) + + partner_order_id = order.partner_order_id + if not partner_order_id: + logger.error("partner_order_id 为空") + return _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret) + + # 推断对方下单方标识 + target_partner_goeasy_id = _get_cross_partner_identity(order, partner_order_id) + if not target_partner_goeasy_id: + logger.error("无法推断对方下单方标识") + return _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret) + + # 对方默认头像 + partner_default_avatar = _full_remote_avatar(partner_config.storage_bucket_domain or '', partner_config.club_avatar or '') or _full_local_avatar('') + + # ---- 1. 在我方建群 ---- + local_group_id = f"group_{order.dingdan_id}" + local_group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else f"跨平台订单{order.dingdan_id}" + local_group_avatar = _full_local_avatar(order.tupian) if order.tupian else _full_local_avatar('') + proxy_partner_id = f"{partner_order_id}_{target_partner_goeasy_id}" + proxy_partner_name = f"对方用户({partner_club_id})" + # 对方代理头像使用对方默认头像 + proxy_partner_avatar = partner_default_avatar + + if not _subscribe_users_to_group([dashou_goeasy_id, proxy_partner_id], [local_group_id], appkey, secret): + return False + + # 1. 打手发送初始化消息 + init_msg_text = f"跨平台订单已接单,内容:{order.jieshao},备注:{order.beizhu}" + _send_group_message( + appkey, secret, local_group_id, dashou_goeasy_id, dashou_name, local_group_avatar, + init_msg_text, None, group_name=local_group_name, order_id=order.dingdan_id, + is_cross=1 + ) + + # 2. 【新增】对方代理也发一条消息,确保对方能看到群聊 + partner_msg = f"订单已确认,请等待服务。" + _send_group_message( + appkey, secret, local_group_id, proxy_partner_id, proxy_partner_name, proxy_partner_avatar, + partner_msg, None, group_name=local_group_name, order_id=order.dingdan_id, + is_cross=1 + ) + + # ---- 2. 在对方建群 ---- + partner_appkey = partner_config.chat_api_key + partner_secret = partner_config.chat_api_id or '' + if not partner_appkey: + return True # 无对方 key,仅完成我方建群 + + partner_group_id = f"group_{partner_order_id}" + partner_group_name = f"跨平台订单-我方打手已接单" + # 代理我方打手 ID + our_agent_id = f"{order.dingdan_id}_{dashou_goeasy_id}" + our_agent_name = f"[我方]{dashou_name}" + # 对方频道头像用对方俱乐部默认头像 + partner_group_avatar = partner_default_avatar + + if not _subscribe_users_to_group([our_agent_id, target_partner_goeasy_id], [partner_group_id], partner_appkey, partner_secret): + logger.error("向对方频道订阅失败") + return True + + # 我方打手在对方频道发消息 + cross_msg_text = f"我方打手已接单,订单:{order.jieshao},请确认" + _send_group_message( + partner_appkey, partner_secret, partner_group_id, our_agent_id, our_agent_name, partner_group_avatar, + cross_msg_text, None, group_name=partner_group_name, order_id=partner_order_id, + is_cross=1 + ) + + # 【新增】对方用户在对方频道也发一条消息(以对方原始身份) + target_partner_name = f"用户{target_partner_goeasy_id}" # 简单命名,可按需优化 + _send_group_message( + partner_appkey, partner_secret, partner_group_id, target_partner_goeasy_id, target_partner_name, partner_default_avatar, + "订单信息已收到。", None, group_name=partner_group_name, order_id=partner_order_id, + is_cross=1 + ) + + return True + +def _get_cross_partner_identity(order, partner_order_id): + """ + 获取对方下单方的 GoEasy 标识。 + 优先从商家扩展表取 partner_yonghu_id,否则按 partner_order_id 前缀推断。 + """ + # 尝试从商家扩展表取(仅当发单平台为商家且存在 partner_yonghu_id) + if order.fadan_pingtai == 2 and hasattr(order, 'shangjia_kuozhan'): + ext = order.shangjia_kuozhan + if ext and ext.partner_yonghu_id: + # 直接根据订单前缀构造标识 + prefix = partner_order_id[:2].upper() + if prefix == 'SJ': + return f"Sj{ext.partner_yonghu_id}" + elif prefix == 'PT': + return f"Boss{ext.partner_yonghu_id}" + else: + return f"User{ext.partner_yonghu_id}" + + # 后备:根据 partner_order_id 前缀推断 + prefix = partner_order_id[:2].upper() + uid = partner_order_id[2:] if len(partner_order_id) > 2 else '' + if prefix == 'SJ': + return f"Sj{uid}" + elif prefix == 'PT': + return f"Boss{uid}" + else: + return f"User{uid}" + +def _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret): + group_id = f"group_{order.dingdan_id}" + _subscribe_users_to_group([dashou_goeasy_id], [group_id], appkey, secret) + return False \ No newline at end of file diff --git a/utils/exception_handler.py b/utils/exception_handler.py new file mode 100644 index 0000000..55f4576 --- /dev/null +++ b/utils/exception_handler.py @@ -0,0 +1,58 @@ +# /opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.443/django/utils/exception_handler.py + +from rest_framework.views import exception_handler +from rest_framework.response import Response +from rest_framework import status +import logging +import os + +logger = logging.getLogger('django') + +def custom_exception_handler(exc, context): + """ + 生产环境安全异常处理器 - 不泄露任何敏感信息 + 基于你之前项目的代码,做了小优化 + """ + # 先调用默认异常处理器 + response = exception_handler(exc, context) + + if response is not None: + # 🔥 记录原始错误信息到日志(便于排查) + request = context['request'] + user_info = f"用户: {request.user.id if request.user.is_authenticated else '未登录'}" + logger.error( + f"API异常 - 路径: {request.path}, " + f"方法: {request.method}, {user_info}, " + f"状态码: {response.status_code}, 错误: {str(exc)}" + ) + + # 🔥 生产环境返回标准化错误,不暴露任何细节 + error_messages = { + status.HTTP_400_BAD_REQUEST: {'code': 400, 'message': '请求参数错误'}, + status.HTTP_401_UNAUTHORIZED: {'code': 401, 'message': '请先登录'}, + status.HTTP_403_FORBIDDEN: {'code': 403, 'message': '权限不足'}, + status.HTTP_404_NOT_FOUND: {'code': 404, 'message': '资源不存在'}, + status.HTTP_405_METHOD_NOT_ALLOWED: {'code': 405, 'message': '请求方法不允许'}, + status.HTTP_408_REQUEST_TIMEOUT: {'code': 408, 'message': '请求超时'}, + status.HTTP_429_TOO_MANY_REQUESTS: {'code': 429, 'message': '请求过于频繁'}, + status.HTTP_500_INTERNAL_SERVER_ERROR: {'code': 500, 'message': '服务器内部错误'}, + status.HTTP_502_BAD_GATEWAY: {'code': 502, 'message': '网关错误'}, + status.HTTP_503_SERVICE_UNAVAILABLE: {'code': 503, 'message': '服务暂不可用'}, + status.HTTP_504_GATEWAY_TIMEOUT: {'code': 504, 'message': '网关超时'}, + } + + # 🔥 如果是调试模式,返回详细错误(便于开发) + from django.conf import settings + if settings.DEBUG: + return response + + # 🔥 生产环境返回标准错误 + standard_response = error_messages.get( + response.status_code, + {'code': response.status_code, 'message': '请求错误'} + ) + + return Response(standard_response, status=response.status_code) + + # 对于非DRF异常,这里可以根据需要处理 + return response \ No newline at end of file diff --git a/utils/fadan_utils.py b/utils/fadan_utils.py new file mode 100644 index 0000000..fdfda71 --- /dev/null +++ b/utils/fadan_utils.py @@ -0,0 +1,142 @@ +# utils/fadan_utils.py +from dingdan.models import Fadan +import logging + +logger = logging.getLogger('xiaochengxu') + + +def check_fadan_qiangdan_eligible(yonghuid: str, action_type: int) -> tuple: + """ + 通用罚单资格检查。 + + 参数: + yonghuid : 被检查的用户 yonghuid + action_type : 1 = 抢单, 2 = 提现 + + 返回: + (is_eligible: bool, message: str) + True 允许操作,False 返回拦截原因。 + """ + try: + if action_type == 1: + # 抢单:仅当存在“影响抢单”且“未解决”的罚单时拦截(待缴纳或申诉中) + blocked = Fadan.objects.filter( + beichufa_id=yonghuid, + yingxiang_qiangdan=1, + zhuangtai__in=[1, 3] # 1=待缴纳, 3=申诉中 + ).exists() + if blocked: + logger.info(f"用户 {yonghuid} 抢单被拦截:存在未解决的影响抢单罚单") + return (False, "您有未缴纳或申诉中的罚单,请先处理后再抢单") + return (True, "无罚单限制") + + elif action_type == 2: + # 提现:只要存在任意未解决的罚单(待缴纳/申诉中),不论是否影响抢单,一律禁止提现 + blocked = Fadan.objects.filter( + beichufa_id=yonghuid, + zhuangtai__in=[1, 3] # 1=待缴纳, 3=申诉中 + ).exists() + if blocked: + logger.info(f"用户 {yonghuid} 提现被拦截:存在未解决的罚单") + return (False, "您有未处理完的罚单,需全部完成或驳回后方可提现") + return (True, "罚单状态正常,可提现") + + else: + logger.warning(f"未知的检查类型: {action_type}") + return (False, "罚单查询系统异常(无效的检查类型)") + + except Exception as e: + logger.error(f"检查用户 {yonghuid} 罚单状态异常: {e}", exc_info=True) + # 安全起见,异常时禁止操作 + return (False, "罚单查询系统繁忙,请稍后重试") + + + + + +import logging +from django.db import transaction +from decimal import Decimal +from dingdan.models import Fadan, FadanFenhong, FadanFenhongLilv + +logger = logging.getLogger('xiaochengxu') + +def fadan_fenhong(fadan_id): + """ + 罚款缴纳后,给申请处罚人累加分红。 + 自动映射身份,异常不抛出,不影响主流程。 + """ + try: + # 1. 查询罚单 + try: + fadan = Fadan.objects.get(id=fadan_id) + except Fadan.DoesNotExist: + logger.warning(f"罚单 {fadan_id} 不存在,分红跳过") + return + + if fadan.zhuangtai != 2: + logger.info(f"罚单 {fadan_id} 状态非已缴纳,不分红") + return + + fenhongzhe_id = fadan.shenqing_chufa or '' + shenfen_in_fadan = fadan.shenqingren_shenfen # 罚单中的申请人身份 + jine = fadan.fakuanjine + + if not fenhongzhe_id or jine <= 0: + logger.info(f"罚单 {fadan_id} 缺少申请处罚人或金额为0,不分红") + return + + # 2. 直接用罚单身份的原始值查询费率(费率表 shenfen 与之含义一致) + rate_record = FadanFenhongLilv.objects.filter(shenfen=shenfen_in_fadan).first() + if not rate_record or rate_record.lilv <= 0: + logger.info(f"身份 {shenfen_in_fadan} 无有效分红费率,不分红") + return + + rate = rate_record.lilv + + # 3. 计算分红金额 + amount = (jine * rate).quantize(Decimal('0.01')) + if amount <= 0: + logger.info(f"罚单 {fadan_id} 分红金额为0,不分红") + return + + # 4. 硬编码映射:罚单身份 → 分红记录表身份 + SHENFEN_MAP = { + 1: 2, # 客服 → 客服 + 2: 5, # 售后 → 售后 + 3: 3, # 管理员 → 平台管理者 + 4: 4, # 店铺 → 店铺 + 5: 1, # 商家 → 商家 + } + shenfen_in_fenhong = SHENFEN_MAP.get(shenfen_in_fadan) + if shenfen_in_fenhong is None: + logger.info(f"罚单 {fadan_id} 身份 {shenfen_in_fadan} 无对应的分红记录映射,不分红") + return + + # 5. 事务累加分红记录(同一用户+身份只保留一条记录) + with transaction.atomic(): + record = FadanFenhong.objects.select_for_update().filter( + fenhongzhe_id=fenhongzhe_id, + shenfen=shenfen_in_fenhong # 使用映射后的身份值 + ).first() + + if record: + record.zonge += amount + record.ketixian_fenhong += amount + record.fenhong_cishu += 1 + record.yingde_lilv = rate + record.save() + else: + FadanFenhong.objects.create( + fenhongzhe_id=fenhongzhe_id, + shenfen=shenfen_in_fenhong, + zonge=amount, + ketixian_fenhong=amount, + fenhong_cishu=1, + yingde_lilv=rate, + ) + + logger.info(f"罚单 {fadan_id} 分红完成:用户 {fenhongzhe_id} 身份 {shenfen_in_fenhong} 金额 {amount}") + + except Exception as e: + logger.error(f"罚单 {fadan_id} 分红异常:{e}", exc_info=True) \ No newline at end of file diff --git a/utils/goeasy_service.py b/utils/goeasy_service.py new file mode 100644 index 0000000..932ddd4 --- /dev/null +++ b/utils/goeasy_service.py @@ -0,0 +1,103 @@ +# apps/dingdan/utils/goeasy_service.py +import requests +import json +import logging +from django.conf import settings + +logger = logging.getLogger(__name__) + + +class GoEasyService: + """ + GoEasy聊天服务工具类 + 用于建立用户间的私聊关系 + """ + + def __init__(self): + self.appkey = settings.GOEASY_APPKEY # 需要在settings.py中配置 + self.secret = settings.GOEASY_SECRET # GoEasy控制台获取的Secret + self.rest_host = "https://rest-hz.goeasy.io" # 杭州区域 + + def create_private_chat(self, user1_id, user2_id, user1_info=None, user2_info=None): + """ + 建立两个用户间的私聊关系 + + Args: + user1_id: 用户1在GoEasy的标识 + user2_id: 用户2在GoEasy的标识 + user1_info: 用户1的附加信息 (avatar, nickname等) + user2_info: 用户2的附加信息 + Returns: + bool: 是否成功 + """ + try: + # 构造消息数据 + message_data = { + "appkey": self.appkey, + "senderId": "system_admin", + "senderData": { + "avatar": "/static/system_avatar.png", + "name": "系统通知" + }, + "to": { + "type": "private", + "id": user1_id, + "data": user2_info or { + "avatar": "", + "name": f"用户{user2_id}" + } + }, + "type": "system", + "payload": { + "action": "create_chat", + "partner_id": user2_id, + "message": "已为您建立聊天连接" + }, + "notification": { + "title": "新聊天", + "body": "您有新的聊天连接" + } + } + + # 发送请求 + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.secret}" + } + + response = requests.post( + f"{self.rest_host}/v2/im/message", + headers=headers, + json=message_data, + timeout=5 # 5秒超时 + ) + + if response.status_code == 200: + logger.info(f"成功建立聊天关系: {user1_id} ↔ {user2_id}") + return True + else: + logger.error(f"建立聊天关系失败: {response.status_code}, {response.text}") + return False + + except Exception as e: + logger.error(f"调用GoEasy API异常: {str(e)}") + return False + + def establish_chat_for_order(self, order, dashou_yonghuid, dashou_info=None): + """ + 为订单建立聊天关系 + """ + # user1_id 是老板/商家标识 (从订单中获取) + # user2_id 是打手标识 + user1_id = order.user1_id + user2_id = f"Ds{dashou_yonghuid}" + + if not user1_id or not user2_id: + logger.warning(f"缺少用户标识,无法建立聊天: user1_id={user1_id}, user2_id={user2_id}") + return False + + # 建立双向聊天关系 + success1 = self.create_private_chat(user1_id, user2_id, user2_info=dashou_info) + success2 = self.create_private_chat(user2_id, user1_id) + + return success1 or success2 # 只要有一个方向成功就认为成功 \ No newline at end of file diff --git a/utils/migrations/__init__.py b/utils/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/models.py b/utils/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/utils/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/utils/oss_utils.py b/utils/oss_utils.py new file mode 100644 index 0000000..662dc5d --- /dev/null +++ b/utils/oss_utils.py @@ -0,0 +1,511 @@ +# utils/oss_utils.py +import os +import uuid +from django.conf import settings +from PIL import Image, UnidentifiedImageError +import io + +'''try: + from qcloud_cos import CosConfig, CosS3Client + + TENCENT_COS_AVAILABLE = True +except ImportError: + TENCENT_COS_AVAILABLE = False + +try: + import oss2 + + ALIYUN_OSS_AVAILABLE = True +except ImportError: + ALIYUN_OSS_AVAILABLE = False + + +def validate_image(file): + """ + 验证图片文件 + 使用Pillow检查图片是否有效 + """ + try: + # 打开图片但不保存到磁盘 + img = Image.open(file) + img.verify() # 验证文件完整性 + + # 重置文件指针(因为PIL读取后会移动指针) + file.seek(0) + + # 检查图片格式 + allowed_formats = ['JPEG', 'PNG', 'GIF', 'WEBP', 'BMP'] + if img.format not in allowed_formats: + return False, f"不支持{img.format}格式,请使用JPEG、PNG、GIF、WEBP或BMP格式" + + # 检查图片大小(最大5MB) + file_size = len(file.read()) + file.seek(0) + if file_size > 5 * 1024 * 1024: + return False, "图片大小不能超过5MB" + + # 检查图片尺寸(最大2000x2000) + img = Image.open(file) + file.seek(0) + if img.width > 3000 or img.height > 3000: + return False, "图片尺寸不能超过2000x2000像素" + + return True, "图片验证通过" + + except UnidentifiedImageError: + return False, "文件不是有效的图片" + except Exception as e: + return False, f"图片验证失败: {str(e)}" + + +def get_oss_client(): + """ + 获取OSS客户端 + 根据配置自动选择云厂商 + """ + oss_type = getattr(settings, 'OSS_TYPE', 'tencent') # 默认为腾讯云 + + if oss_type == 'tencent' and TENCENT_COS_AVAILABLE: + # 腾讯云COS + config = CosConfig( + Region=getattr(settings, 'COS_REGION', 'ap-shanghai'), + SecretId=getattr(settings, 'COS_SECRET_ID', ''), + SecretKey=getattr(settings, 'COS_SECRET_KEY', ''), + Scheme=getattr(settings, 'COS_SCHEME', 'https') + ) + return CosS3Client(config) + + elif oss_type == 'aliyun' and ALIYUN_OSS_AVAILABLE: + # 阿里云OSS + auth = oss2.Auth( + getattr(settings, 'ALIYUN_ACCESS_KEY_ID', ''), + getattr(settings, 'ALIYUN_ACCESS_KEY_SECRET', '') + ) + endpoint = getattr(settings, 'ALIYUN_OSS_ENDPOINT', '') + bucket_name = getattr(settings, 'ALIYUN_OSS_BUCKET', '') + return oss2.Bucket(auth, endpoint, bucket_name) + + else: + raise ImportError(f"不支持{oss_type}云存储或相关SDK未安装") + + +def upload_to_oss(file_obj, file_path): + """ + 通用上传文件到OSS + file_obj: Django的UploadedFile对象或类文件对象 + file_path: 在OSS中的相对路径,如 'avatar/123/abc.jpg' + 返回: 文件访问URL + """ + try: + oss_type = getattr(settings, 'OSS_TYPE', 'tencent') + + if oss_type == 'tencent': + return _upload_to_tencent_cos(file_obj, file_path) + elif oss_type == 'aliyun': + return _upload_to_aliyun_oss(file_obj, file_path) + else: + raise ValueError(f"不支持的OSS类型: {oss_type}") + + except Exception as e: + print(f"OSS上传失败: {e}") + return None + + +def _upload_to_tencent_cos(file_obj, file_path): + """ + 上传到腾讯云COS + """ + client = get_oss_client() + + # 获取文件扩展名 + ext = file_path.split('.')[-1].lower() if '.' in file_path else '' + content_type_map = { + 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', + 'png': 'image/png', 'gif': 'image/gif', + 'webp': 'image/webp', 'bmp': 'image/bmp' + } + content_type = content_type_map.get(ext, 'image/jpeg') + + # 如果是Django的UploadedFile,需要读取内容 + if hasattr(file_obj, 'read'): + file_content = file_obj.read() + else: + file_content = file_obj + + # 上传文件 + response = client.put_object( + Bucket=getattr(settings, 'COS_BUCKET', ''), + Body=file_content, + Key=file_path, + ContentType=content_type, + ContentDisposition='inline' + ) + + # 返回完整URL + domain = getattr(settings, 'COS_DOMAIN', '') + return f"{domain}/{file_path}" + + +def _upload_to_aliyun_oss(file_obj, file_path): + """ + 上传到阿里云OSS + """ + bucket = get_oss_client() + + # 如果是Django的UploadedFile,需要读取内容 + if hasattr(file_obj, 'read'): + file_content = file_obj.read() + else: + file_content = file_obj + + # 上传文件 + result = bucket.put_object(file_path, file_content) + + if result.status == 200: + # 阿里云OSS返回的URL + domain = getattr(settings, 'ALIYUN_OSS_DOMAIN', '') + return f"{domain}/{file_path}" + else: + return None + + +def delete_from_oss(file_url): + """ + 从OSS删除文件 + file_url: 可以是完整URL或相对路径 + """ + try: + if not file_url: + return False + + oss_type = getattr(settings, 'OSS_TYPE', 'tencent') + + # 从URL中提取文件路径 + if oss_type == 'tencent': + domain = getattr(settings, 'COS_DOMAIN', '') + + # 判断是完整URL还是相对路径 + if file_url.startswith('http'): + # 完整URL + if domain not in file_url: + return False # URL不属于我们的域名 + file_key = file_url.replace(f"{domain}/", "") + else: + # 相对路径,直接使用 + file_key = file_url + + return _delete_from_tencent_cos(file_key) + + elif oss_type == 'aliyun': + domain = getattr(settings, 'ALIYUN_OSS_DOMAIN', '') + + if file_url.startswith('http'): + if domain not in file_url: + return False + file_key = file_url.replace(f"{domain}/", "") + else: + file_key = file_url + + return _delete_from_aliyun_oss(file_key) + + else: + return False + + except Exception as e: + print(f"OSS删除失败: {e}") + return False + + +def _delete_from_tencent_cos(file_key): + """ + 从腾讯云COS删除文件 + """ + client = get_oss_client() + + try: + client.delete_object( + Bucket=getattr(settings, 'COS_BUCKET', ''), + Key=file_key + ) + return True + except Exception as e: + print(f"腾讯云COS删除失败: {e}") + return False + + +def _delete_from_aliyun_oss(file_key): + """ + 从阿里云OSS删除文件 + """ + bucket = get_oss_client() + + try: + bucket.delete_object(file_key) + return True + except Exception as e: + print(f"阿里云OSS删除失败: {e}") + return False''' + + + +# utils/oss_utils.py +import os +import uuid +from django.conf import settings +from PIL import Image, UnidentifiedImageError +import io + +try: + from qcloud_cos import CosConfig, CosS3Client + + TENCENT_COS_AVAILABLE = True +except ImportError: + TENCENT_COS_AVAILABLE = False + +try: + import oss2 + + ALIYUN_OSS_AVAILABLE = True +except ImportError: + ALIYUN_OSS_AVAILABLE = False + +# ========== 通用 MIME 类型映射(扩展支持非图片文件) ========== +MIME_TYPES = { + # 图片 + 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png', + 'gif': 'image/gif', 'webp': 'image/webp', 'bmp': 'image/bmp', + # JSON / 文本 + 'json': 'application/json', 'txt': 'text/plain', + # 音频(小程序录音常见格式) + 'mp3': 'audio/mpeg', 'aac': 'audio/aac', 'wav': 'audio/wav', + 'm4a': 'audio/mp4', 'ogg': 'audio/ogg', + # 视频 + 'mp4': 'video/mp4', 'webm': 'video/webm', + # 二进制 + 'bin': 'application/octet-stream', +} + +# ==================================================================== + +def validate_image(file): + """ + 验证图片文件 + 使用Pillow检查图片是否有效 + """ + try: + # 打开图片但不保存到磁盘 + img = Image.open(file) + img.verify() # 验证文件完整性 + + # 重置文件指针(因为PIL读取后会移动指针) + file.seek(0) + + # 检查图片格式 + allowed_formats = ['JPEG', 'PNG', 'GIF', 'WEBP', 'BMP'] + if img.format not in allowed_formats: + return False, f"不支持{img.format}格式,请使用JPEG、PNG、GIF、WEBP或BMP格式" + + # 检查图片大小(最大5MB) + file_size = len(file.read()) + file.seek(0) + if file_size > 5 * 1024 * 1024: + return False, "图片大小不能超过5MB" + + # 检查图片尺寸(最大2000x2000) + img = Image.open(file) + file.seek(0) + if img.width > 3000 or img.height > 3000: + return False, "图片尺寸不能超过2000x2000像素" + + return True, "图片验证通过" + + except UnidentifiedImageError: + return False, "文件不是有效的图片" + except Exception as e: + return False, f"图片验证失败: {str(e)}" + + +def get_oss_client(): + """ + 获取OSS客户端 + 根据配置自动选择云厂商 + """ + oss_type = getattr(settings, 'OSS_TYPE', 'tencent') # 默认为腾讯云 + + if oss_type == 'tencent' and TENCENT_COS_AVAILABLE: + # 腾讯云COS + config = CosConfig( + Region=getattr(settings, 'COS_REGION', 'ap-shanghai'), + SecretId=getattr(settings, 'COS_SECRET_ID', ''), + SecretKey=getattr(settings, 'COS_SECRET_KEY', ''), + Scheme=getattr(settings, 'COS_SCHEME', 'https') + ) + return CosS3Client(config) + + elif oss_type == 'aliyun' and ALIYUN_OSS_AVAILABLE: + # 阿里云OSS + auth = oss2.Auth( + getattr(settings, 'ALIYUN_ACCESS_KEY_ID', ''), + getattr(settings, 'ALIYUN_ACCESS_KEY_SECRET', '') + ) + endpoint = getattr(settings, 'ALIYUN_OSS_ENDPOINT', '') + bucket_name = getattr(settings, 'ALIYUN_OSS_BUCKET', '') + return oss2.Bucket(auth, endpoint, bucket_name) + + else: + raise ImportError(f"不支持{oss_type}云存储或相关SDK未安装") + + +def upload_to_oss(file_obj, file_path, content_type=None): + """ + 通用上传文件到OSS + file_obj: Django的UploadedFile对象或类文件对象 + file_path: 在OSS中的相对路径,如 'avatar/123/abc.jpg' + content_type: 可选,MIME类型;若不传则根据文件扩展名自动推断 + 返回: 文件访问URL + """ + try: + oss_type = getattr(settings, 'OSS_TYPE', 'tencent') + + if oss_type == 'tencent': + return _upload_to_tencent_cos(file_obj, file_path, content_type) + elif oss_type == 'aliyun': + return _upload_to_aliyun_oss(file_obj, file_path) + else: + raise ValueError(f"不支持的OSS类型: {oss_type}") + + except Exception as e: + print(f"OSS上传失败: {e}") + return None + + +def _upload_to_tencent_cos(file_obj, file_path, content_type=None): + """ + 上传到腾讯云COS(扩展支持任意文件类型) + """ + client = get_oss_client() + + # 若调用方未指定 Content-Type,则根据扩展名自动推断 + if not content_type: + ext = file_path.rsplit('.', 1)[-1].lower() if '.' in file_path else '' + content_type = MIME_TYPES.get(ext, 'application/octet-stream') + + # 读取文件内容(兼容 Django UploadedFile 和普通 bytes) + if hasattr(file_obj, 'read'): + file_content = file_obj.read() + else: + file_content = file_obj + + # 上传文件 + response = client.put_object( + Bucket=getattr(settings, 'COS_BUCKET', ''), + Body=file_content, + Key=file_path, + ContentType=content_type, + ContentDisposition='inline' + ) + + # 返回完整URL + domain = getattr(settings, 'COS_DOMAIN', '') + return f"{domain.rstrip('/')}/{file_path.lstrip('/')}" + + +def _upload_to_aliyun_oss(file_obj, file_path): + """ + 上传到阿里云OSS(保持原样,未修改) + """ + bucket = get_oss_client() + + # 如果是Django的UploadedFile,需要读取内容 + if hasattr(file_obj, 'read'): + file_content = file_obj.read() + else: + file_content = file_obj + + # 上传文件 + result = bucket.put_object(file_path, file_content) + + if result.status == 200: + # 阿里云OSS返回的URL + domain = getattr(settings, 'ALIYUN_OSS_DOMAIN', '') + return f"{domain}/{file_path}" + else: + return None + + +def delete_from_oss(file_url): + """ + 从OSS删除文件 + file_url: 可以是完整URL或相对路径 + """ + try: + if not file_url: + return False + + oss_type = getattr(settings, 'OSS_TYPE', 'tencent') + + # 从URL中提取文件路径 + if oss_type == 'tencent': + domain = getattr(settings, 'COS_DOMAIN', '') + + # 判断是完整URL还是相对路径 + if file_url.startswith('http'): + # 完整URL + if domain not in file_url: + return False # URL不属于我们的域名 + file_key = file_url.replace(f"{domain}/", "") + else: + # 相对路径,直接使用 + file_key = file_url + + return _delete_from_tencent_cos(file_key) + + elif oss_type == 'aliyun': + domain = getattr(settings, 'ALIYUN_OSS_DOMAIN', '') + + if file_url.startswith('http'): + if domain not in file_url: + return False + file_key = file_url.replace(f"{domain}/", "") + else: + file_key = file_url + + return _delete_from_aliyun_oss(file_key) + + else: + return False + + except Exception as e: + print(f"OSS删除失败: {e}") + return False + + +def _delete_from_tencent_cos(file_key): + """ + 从腾讯云COS删除文件 + """ + client = get_oss_client() + + try: + client.delete_object( + Bucket=getattr(settings, 'COS_BUCKET', ''), + Key=file_key + ) + return True + except Exception as e: + print(f"腾讯云COS删除失败: {e}") + return False + + +def _delete_from_aliyun_oss(file_key): + """ + 从阿里云OSS删除文件 + """ + bucket = get_oss_client() + + try: + bucket.delete_object(file_key) + return True + except Exception as e: + print(f"阿里云OSS删除失败: {e}") + return False \ No newline at end of file diff --git a/utils/redis_lock.py b/utils/redis_lock.py new file mode 100644 index 0000000..c46d078 --- /dev/null +++ b/utils/redis_lock.py @@ -0,0 +1,18 @@ +import redis +from django.conf import settings + +redis_client = redis.Redis( + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + password=settings.REDIS_PASSWORD, + db=2, + decode_responses=True +) + +def acquire_lock(lock_key, timeout=10): + """获取锁,返回是否成功""" + return redis_client.set(lock_key, '1', nx=True, ex=timeout) + +def release_lock(lock_key): + """释放锁""" + redis_client.delete(lock_key) \ No newline at end of file diff --git a/utils/tests.py b/utils/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/utils/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/utils/views.py b/utils/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/utils/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/utils/wechat_v3.py b/utils/wechat_v3.py new file mode 100644 index 0000000..4fa097c --- /dev/null +++ b/utils/wechat_v3.py @@ -0,0 +1,207 @@ +# utils/wechat_v3.py +'''import time +import hashlib +import base64 +import json +from urllib.parse import urlparse + +import rsa +from Crypto.Cipher import AES + +from django.conf import settings + + +def load_private_key(): + """加载商户私钥(PEM格式)""" + with open(settings.WECHAT_PAY_V3_CONFIG['PRIVATE_KEY_PATH'], 'rb') as f: + key_data = f.read() + return rsa.PrivateKey.load_pkcs1(key_data) + + +def build_authorization(method, url, body): + """ + 生成V3接口的Authorization头 + :param method: GET/POST + :param url: 完整URL(含域名和路径) + :param body: 请求体JSON字符串 + :return: Authorization字符串 + """ + private_key = load_private_key() + mchid = settings.WECHAT_PAY_V3_CONFIG['MCHID'] + serial_no = settings.WECHAT_PAY_V3_CONFIG['CERT_SERIAL_NO'] + + # 提取URL路径(不含query) + parsed = urlparse(url) + path = parsed.path + if parsed.query: + path += '?' + parsed.query + + timestamp = str(int(time.time())) + nonce = hashlib.md5((timestamp + 'wechat').encode()).hexdigest()[:32] + + # 构造签名串 + message = '\n'.join([method, path, timestamp, nonce, body]) + '\n' + + # 使用私钥签名(SHA256) + signature = rsa.sign(message.encode('utf-8'), private_key, 'SHA-256') + signature_b64 = base64.b64encode(signature).decode('utf-8') + + # 组装Authorization + auth = f'WECHATPAY2-SHA256-RSA2048 mchid="{mchid}",nonce_str="{nonce}",timestamp="{timestamp}",serial_no="{serial_no}",signature="{signature_b64}"' + return auth + + +def verify_wechat_sign(headers, body): + """ + 验证微信回调的签名 + 需要提前从微信平台下载证书公钥,存放在 PLATFORM_CERT_DIR 下,文件名 = 序列号.pem + :param headers: 请求头 + :param body: 原始请求体字符串 + :return: bool + """ + serial = headers.get('Wechatpay-Serial') + signature = headers.get('Wechatpay-Signature') + timestamp = headers.get('Wechatpay-Timestamp') + nonce = headers.get('Wechatpay-Nonce') + + if not all([serial, signature, timestamp, nonce]): + return False + + # 构造验签名串 + message = '\n'.join([timestamp, nonce, body]) + '\n' + + # 读取对应序列号的平台证书公钥 + cert_path = f"{settings.WECHAT_PAY_V3_CONFIG['PLATFORM_CERT_DIR']}/{serial}.pem" + try: + with open(cert_path, 'rb') as f: + pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(f.read()) + except FileNotFoundError: + # 如果证书不存在,可尝试下载(生产环境建议定时下载) + return False + + signature_bytes = base64.b64decode(signature) + try: + rsa.verify(message.encode('utf-8'), signature_bytes, pub_key) + return True + except rsa.VerificationError: + return False + + +def decrypt_callback_ciphertext(associated_data, nonce, ciphertext): + """ + 使用APIv3密钥解密回调中的加密数据 + :param associated_data: 附加数据 + :param nonce: 随机串 + :param ciphertext: 密文(Base64) + :return: 解密后的JSON字符串 + """ + api_v3_key = settings.WECHAT_PAY_V3_CONFIG['API_V3_KEY'].encode('utf-8') + nonce_bytes = nonce.encode('utf-8') + ciphertext_bytes = base64.b64decode(ciphertext) + + # AES-GCM解密 + cipher = AES.new(api_v3_key, AES.MODE_GCM, nonce=nonce_bytes) + if associated_data: + cipher.update(associated_data.encode('utf-8')) + plaintext = cipher.decrypt_and_verify(ciphertext_bytes[:-16], ciphertext_bytes[-16:]) + return plaintext.decode('utf-8')''' + +# utils/wechat_v3.py +import time +import hashlib +import base64 +import json +from urllib.parse import urlparse + +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.backends import default_backend +from Crypto.Cipher import AES + +from django.conf import settings +import os + + +def load_private_key(): + """加载私钥(支持 PKCS#1 和 PKCS#8)""" + key_path = settings.WECHAT_PAY_V3_CONFIG['PRIVATE_KEY_PATH'] + with open(key_path, 'rb') as f: + key_data = f.read() + + # cryptography 自动识别格式 + private_key = serialization.load_pem_private_key( + key_data, + password=None, + backend=default_backend() + ) + return private_key + + +def build_authorization(method, url, body): + """生成V3接口的Authorization头""" + private_key = load_private_key() + mchid = settings.WECHAT_PAY_V3_CONFIG['MCHID'] + serial_no = settings.WECHAT_PAY_V3_CONFIG['CERT_SERIAL_NO'] + + parsed = urlparse(url) + path = parsed.path + if parsed.query: + path += '?' + parsed.query + + timestamp = str(int(time.time())) + nonce = hashlib.md5((timestamp + 'wechat').encode()).hexdigest()[:32] + + # 构造签名串 + message = '\n'.join([method, path, timestamp, nonce, body]) + '\n' + + # 使用 cryptography 签名 + signature = private_key.sign( + message.encode('utf-8'), + padding.PKCS1v15(), + hashes.SHA256() + ) + signature_b64 = base64.b64encode(signature).decode('utf-8') + + auth = f'WECHATPAY2-SHA256-RSA2048 mchid="{mchid}",nonce_str="{nonce}",timestamp="{timestamp}",serial_no="{serial_no}",signature="{signature_b64}"' + return auth + + +def verify_wechat_sign(headers, body): + """验证微信回调签名(需提前下载平台证书)""" + # 这部分使用 rsa 库验签,也可以改用 cryptography,但 rsa 已足够 + import rsa + serial = headers.get('Wechatpay-Serial') + signature = headers.get('Wechatpay-Signature') + timestamp = headers.get('Wechatpay-Timestamp') + nonce = headers.get('Wechatpay-Nonce') + + if not all([serial, signature, timestamp, nonce]): + return False + + message = '\n'.join([timestamp, nonce, body]) + '\n' + cert_path = f"{settings.WECHAT_PAY_V3_CONFIG['PLATFORM_CERT_DIR']}/{serial}.pem" + if not os.path.exists(cert_path): + return False + + with open(cert_path, 'rb') as f: + pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(f.read()) + + signature_bytes = base64.b64decode(signature) + try: + rsa.verify(message.encode('utf-8'), signature_bytes, pub_key) + return True + except rsa.VerificationError: + return False + + +def decrypt_callback_ciphertext(associated_data, nonce, ciphertext): + """解密回调数据(使用 AES-GCM)""" + api_v3_key = settings.WECHAT_PAY_V3_CONFIG['API_V3_KEY'].encode('utf-8') + nonce_bytes = nonce.encode('utf-8') + ciphertext_bytes = base64.b64decode(ciphertext) + + cipher = AES.new(api_v3_key, AES.MODE_GCM, nonce=nonce_bytes) + if associated_data: + cipher.update(associated_data.encode('utf-8')) + plaintext = cipher.decrypt_and_verify(ciphertext_bytes[:-16], ciphertext_bytes[-16:]) + return plaintext.decode('utf-8') \ No newline at end of file diff --git a/utils/weixin_broadcast.py b/utils/weixin_broadcast.py new file mode 100644 index 0000000..47774b6 --- /dev/null +++ b/utils/weixin_broadcast.py @@ -0,0 +1,197 @@ +# utils/weixin_broadcast.py - 强调试版本 +import time +import logging +import json # 新增,用于格式化打印 +import requests +from django.core.cache import cache +from django.conf import settings +from yonghu.models import OfficialAccountUser + +logger = logging.getLogger('weixin_broadcast') + +class WeixinBroadcastSender: + """ + 微信模板消息广播发送器 - 调试版 + """ + + def __init__(self): + self.appid = getattr(settings, 'WEIXIN_OFFICIAL_APPID', '') + self.secret = getattr(settings, 'WEIXIN_OFFICIAL_SECRET', '') + self.template_id = getattr(settings, 'WEIXIN_TEMPLATE_ID', '') + + # 发送控制参数 + self.batch_size = 100 + self.delay_between_batches = 1.5 + + def get_access_token(self): + """获取服务号access_token""" + cache_key = f'weixin_official_token_{self.appid}' + token = cache.get(cache_key) + if token: + return token + + url = 'https://api.weixin.qq.com/cgi-bin/token' + params = {'grant_type': 'client_credential', 'appid': self.appid, 'secret': self.secret} + try: + response = requests.get(url, params=params, timeout=10) + result = response.json() + if 'access_token' in result: + token = result['access_token'] + expires_in = result.get('expires_in', 7200) - 300 + cache.set(cache_key, token, expires_in) + logger.info("✅ 获取access_token成功") + return token + else: + logger.error(f"❌ 获取access_token失败: {result}") + return None + except Exception as e: + logger.error(f"❌ 获取access_token异常: {str(e)}") + return None + + def build_order_message_data(self, order_info): + """ + 构建订单消息数据 - 【调试关键点1】 + 打印传入的 order_info 和构建出的最终数据 + """ + # 🟡 调试点1:打印传入的参数 + logger.info(f"🟡【调试-输入】build_order_message_data 收到的 order_info: {json.dumps(order_info, ensure_ascii=False, indent=2)}") + + # 这是你现在的构建逻辑,我们原样保留但加上调试 + # 注意:这里保持你目前的字段名(short_thing5, short_thing4, thing15, amount8) + data = { + "short_thing5": { + "value": order_info.get('order_type', '平台发单'), + "color": "#173177" + }, + "short_thing4": { + "value": order_info.get('game_type', '游戏订单'), + "color": "#173177" + }, + "thing15": { + "value": str(order_info.get('order_desc', '新订单'))[:20], + "color": "#173177" + }, + "amount8": { + "value": f"{order_info.get('amount', '0')}元", + "color": "#173177" + } + } + + # 🟡 调试点2:打印构建出的最终数据结构 + logger.info(f"🟡【调试-输出】构建完成的模板数据 data 字段:") + for key, content in data.items(): + logger.info(f" 字段名: '{key}' -> 值: '{content.get('value', '')}'") + logger.info(f"🟡【调试-输出】完整 data 结构: {json.dumps(data, ensure_ascii=False)}") + + # 🔴 关键检查:确认 short_thing5 是否存在,值是否为空 + if 'short_thing5' not in data: + logger.error("❌【致命错误】构建的数据中根本没有 'short_thing5' 这个字段!") + else: + st5_value = data['short_thing5'].get('value', '') + if not st5_value: + logger.error(f"❌【致命错误】'short_thing5' 字段的值为空!") + else: + logger.info(f"✅ 'short_thing5' 字段存在,值为: '{st5_value}'") + + return data + + def send_to_user(self, official_openid, template_data, order_id=None): + """发送模板消息给单个用户 - 【调试关键点2】""" + # 🟡 调试点3:打印每次发送前的数据 + logger.info(f"🟡【调试-发送前】准备发送给 {official_openid[:10]}... 的数据:") + logger.info(f" 模板ID: {self.template_id}") + logger.info(f" pagepath: pages/dingdan-xiangqing/dingdan-xiangqing?id={order_id}" if order_id else " 无pagepath") + for key, content in template_data.items(): + logger.info(f" 字段 '{key}': '{content.get('value', '')}'") + + access_token = self.get_access_token() + if not access_token: + return {'success': False, 'msg': '获取access_token失败', 'retry': True} + + request_data = { + "touser": official_openid, + "template_id": self.template_id, + "data": template_data + } + + if order_id and hasattr(settings, 'WEIXIN_APPID'): + request_data["miniprogram"] = { + "appid": settings.WEIXIN_APPID, + "pagepath": f"pages/jiedan/jiedan?id={order_id}" + } + + send_url = f'https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={access_token}' + try: + response = requests.post(send_url, json=request_data, timeout=10) + result = response.json() + logger.info(f"🟡【调试-微信响应】{official_openid[:10]}... 的发送结果: {result}") + + if result.get('errcode') == 0: + return {'success': True, 'msg': '发送成功', 'msgid': result.get('msgid')} + else: + # 如果是未关注或拒收,更新本地状态 + if result.get('errcode') in [43004, 43101]: + try: + OfficialAccountUser.objects.filter(official_openid=official_openid).update(is_subscribed=False) + except: + pass + return {'success': False, 'msg': f"[{result.get('errcode')}]{result.get('errmsg')}", 'retry': False} + + except Exception as e: + logger.error(f"❌ 发送给用户异常: {official_openid[:10]}..., 错误: {str(e)}") + return {'success': False, 'msg': f'请求异常: {str(e)}', 'retry': True} + + def broadcast_order(self, order_info): + """广播订单消息给所有关注用户 - 【调试入口】""" + logger.info(f"🚀【开始广播】订单: {order_info.get('dingdan_id')}") + + # 1. 获取所有关注用户 + try: + subscribed_users = OfficialAccountUser.objects.filter(is_subscribed=True).values_list('official_openid', flat=True) + total_users = subscribed_users.count() + logger.info(f"📊【广播统计】将从数据库查询到 {total_users} 个订阅用户") + if total_users == 0: + return {'success': True, 'msg': '无订阅用户', 'total': 0} + except Exception as e: + logger.error(f"❌ 获取订阅用户列表失败: {str(e)}") + return {'success': False, 'msg': f'获取用户失败: {str(e)}'} + + # 2. 构建模板数据(这里会触发上面的调试打印) + template_data = self.build_order_message_data(order_info) + order_id = order_info.get('dingdan_id') + + # 3. 分批发送 + success_count = 0 + fail_count = 0 + current_batch = 0 + openid_list = list(subscribed_users) + + logger.info(f"🟡【调试-广播循环】开始遍历 {len(openid_list)} 个用户进行发送...") + for i, openid in enumerate(openid_list): + try: + result = self.send_to_user(openid, template_data, order_id) + if result['success']: + success_count += 1 + else: + fail_count += 1 + if fail_count <= 5: # 只打印前几个失败原因 + logger.warning(f"⚠️ 发送失败 {openid[:10]}...: {result.get('msg')}") + + # 分批控制 + current_batch += 1 + if current_batch >= self.batch_size: + time.sleep(self.delay_between_batches) + current_batch = 0 + + except Exception as e: + fail_count += 1 + logger.error(f"❌ 处理用户 {openid[:10]}... 异常: {str(e)}") + + logger.info(f"🏁【广播完成】订单 {order_id},总计: {total_users},成功: {success_count},失败: {fail_count}") + return { + 'success': True, + 'msg': f'广播完成: 成功{success_count}, 失败{fail_count}', + 'total': total_users, + 'success_count': success_count, + 'fail_count': fail_count + } \ No newline at end of file diff --git a/utils/yaoqingma_utils.py b/utils/yaoqingma_utils.py new file mode 100644 index 0000000..6ddcfbc --- /dev/null +++ b/utils/yaoqingma_utils.py @@ -0,0 +1,89 @@ +""" +阿龙电竞陪玩平台 - 邀请码生成工具 +生成规则:毫秒时间戳 + 用户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 diff --git a/yonghu/__init__.py b/yonghu/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yonghu/admin.py b/yonghu/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/yonghu/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/yonghu/apps.py b/yonghu/apps.py new file mode 100644 index 0000000..36f20dc --- /dev/null +++ b/yonghu/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class YonghuConfig(AppConfig): + name = "yonghu" diff --git a/yonghu/fadan_fenhong_utils.py b/yonghu/fadan_fenhong_utils.py new file mode 100644 index 0000000..c816b50 --- /dev/null +++ b/yonghu/fadan_fenhong_utils.py @@ -0,0 +1,168 @@ +# yonghu/fadan_fenhong_utils.py +# 罚款缴纳分红处理公共方法(支持更新罚单中的申请人分红金额) + +import decimal +import logging +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 + +logger = logging.getLogger(__name__) + + +# 身份映射字典 +IDENTITY_MAP = { + 1: {'fenhong': 2, 'lilv': 1}, # 客服 + 2: {'fenhong': 5, 'lilv': 2}, # 售后 + 3: {'fenhong': 3, 'lilv': 3}, # 管理员 + 4: {'fenhong': 4, 'lilv': 4}, # 店铺 + 5: {'fenhong': 1, 'lilv': 5}, # 商家 +} + + +def process_fadan_fenhong(fadan_id): + """ + 罚款缴纳后的分红处理(在回调中调用) + 参数: + fadan_id: 罚单 ID + 返回: + (success, message) + """ + try: + # 1. 获取罚单对象(必须是已缴纳状态) + try: + fadan = Fadan.objects.get(id=fadan_id, zhuangtai=2) + except Fadan.DoesNotExist: + logger.error(f"罚单 {fadan_id} 不存在或状态不是已缴纳") + return False, "罚单不存在或状态异常" + + # 2. 获取申请处罚者ID和身份(源身份数字) + shenqing_chufa = fadan.shenqing_chufa + src_shenfen = fadan.shenqingren_shenfen + + if not shenqing_chufa: + logger.info(f"罚单 {fadan_id} 无申请处罚者,不进行分红") + # 即使无申请者,也可将分红金额字段设为0(默认已是0) + return True, "无申请处罚者,无需分红" + + # 3. 映射身份数字 + if src_shenfen not in IDENTITY_MAP: + logger.error(f"未知的申请处罚者身份: {src_shenfen},罚单 {fadan_id}") + return False, f"未知身份 {src_shenfen}" + + mapped = IDENTITY_MAP[src_shenfen] + fenhong_shenfen = mapped['fenhong'] # 用于 FadanFenhong.shenfen + lilv_shenfen = mapped['lilv'] # 用于 FadanFenhongLilv.shenfen + + # 4. 获取罚款金额 + fakuan_jine = fadan.fakuanjine + if fakuan_jine <= 0: + logger.info(f"罚单 {fadan_id} 罚款金额为0,不产生分红") + fadan.shenqingren_fenhong_jine = Decimal('0.00') + fadan.save(update_fields=['shenqingren_fenhong_jine']) + return True, "罚款金额为0,无分红" + + # 5. 确定分红利率 + rate = _get_fenhong_rate(shenqing_chufa, fenhong_shenfen, lilv_shenfen) + if rate is None: + logger.warning(f"未找到身份 {src_shenfen} (映射后 fenhong_shenfen={fenhong_shenfen}, lilv_shenfen={lilv_shenfen}) 的分红利率,罚单 {fadan_id} 不进行分红") + fadan.shenqingren_fenhong_jine = Decimal('0.00') + fadan.save(update_fields=['shenqingren_fenhong_jine']) + return True, "未配置分红利率,无分红" + + # 6. 计算分红金额 + fenhong_jine = (Decimal(str(fakuan_jine)) * Decimal(str(rate))).quantize(Decimal('0.01')) + if fenhong_jine <= 0: + logger.info(f"罚单 {fadan_id} 计算分红金额为0,无需处理") + fadan.shenqingren_fenhong_jine = Decimal('0.00') + fadan.save(update_fields=['shenqingren_fenhong_jine']) + return True, "分红金额为0" + + # 7. 根据申请处罚者身份执行不同的分红逻辑 + # src_shenfen == 5 代表商家(源身份) + if src_shenfen == 5: + # 商家特殊处理:只记录总额,可提现分红永远为0,直接加到商家余额 + with transaction.atomic(): + fenhong_record, created = FadanFenhong.objects.get_or_create( + fenhongzhe_id=shenqing_chufa, + shenfen=fenhong_shenfen, + defaults={ + 'zonge': Decimal('0.00'), + 'ketixian_fenhong': Decimal('0.00'), + 'fenhong_cishu': 0, + 'yingde_lilv': rate, + 'is_individual_rate': False + } + ) + fenhong_record.zonge += fenhong_jine + # 可提现分红不累加,保持0 + fenhong_record.fenhong_cishu += 1 + fenhong_record.save() + + # 直接增加商家余额(UserShangjia.yue) + try: + shangjia = UserShangjia.objects.get(user__yonghuid=shenqing_chufa) + shangjia.yue += fenhong_jine + shangjia.save(update_fields=['yue']) + logger.info(f"商家 {shenqing_chufa} 余额增加 {fenhong_jine} 元") + except UserShangjia.DoesNotExist: + logger.error(f"商家 {shenqing_chufa} 扩展表不存在,无法增加余额") + raise Exception(f"商家 {shenqing_chufa} 扩展表不存在,无法增加余额") + + # 更新罚单中的申请人分红金额 + fadan.shenqingren_fenhong_jine = fenhong_jine + fadan.save(update_fields=['shenqingren_fenhong_jine']) + + else: + # 其他身份(客服、售后、管理员、店铺):同时增加总额和可提现分红 + with transaction.atomic(): + fenhong_record, created = FadanFenhong.objects.get_or_create( + fenhongzhe_id=shenqing_chufa, + shenfen=fenhong_shenfen, + defaults={ + 'zonge': Decimal('0.00'), + 'ketixian_fenhong': Decimal('0.00'), + 'fenhong_cishu': 0, + 'yingde_lilv': rate, + 'is_individual_rate': False + } + ) + fenhong_record.zonge += fenhong_jine + fenhong_record.ketixian_fenhong += fenhong_jine + fenhong_record.fenhong_cishu += 1 + fenhong_record.save() + + # 更新罚单中的申请人分红金额 + fadan.shenqingren_fenhong_jine = fenhong_jine + fadan.save(update_fields=['shenqingren_fenhong_jine']) + + logger.info(f"罚单 {fadan_id} 分红处理成功: 申请者 {shenqing_chufa}, 分红金额 {fenhong_jine} 元, 源身份 {src_shenfen}, 映射后分红身份 {fenhong_shenfen}") + return True, "分红处理成功" + + except Exception as e: + logger.error(f"罚单 {fadan_id} 分红处理异常: {str(e)}", exc_info=True) + return False, f"分红处理异常: {str(e)}" + + +def _get_fenhong_rate(fenhongzhe_id, fenhong_shenfen, lilv_shenfen): + """ + 获取分红利率(优先使用个人单独配置,否则使用全局身份配置) + """ + try: + individual = FadanFenhong.objects.get( + fenhongzhe_id=fenhongzhe_id, + shenfen=fenhong_shenfen, + is_individual_rate=True + ) + return individual.yingde_lilv + except FadanFenhong.DoesNotExist: + pass + + try: + global_rate = FadanFenhongLilv.objects.get(shenfen=lilv_shenfen) + return global_rate.lilv + except FadanFenhongLilv.DoesNotExist: + return None \ No newline at end of file diff --git a/yonghu/migrations/0001_initial.py b/yonghu/migrations/0001_initial.py new file mode 100644 index 0000000..dbd1243 --- /dev/null +++ b/yonghu/migrations/0001_initial.py @@ -0,0 +1,213 @@ +# Generated by Django 5.2.10 on 2026-01-14 19:59 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Xiugaijilu', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='记录ID')), + ('yonghuid', models.CharField(max_length=7, verbose_name='被修改用户ID')), + ('xiugaiid', models.CharField(max_length=30, verbose_name='修改人账号ID')), + ('leixing', models.IntegerField(verbose_name='被修改用户类型')), + ('xiugaitijiao', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='修改后的打手提现金额')), + ('xiugaitijiaoq', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='修改前的打手提现金额')), + ('jifen', models.IntegerField(default=0, verbose_name='修改后的打手积分')), + ('yjifen', models.IntegerField(default=0, verbose_name='修改前的打手积分')), + ('yajin', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='修改后的打手押金')), + ('yyajin', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='修改前的打手押金')), + ('shangjiayue', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='修改后的商家余额')), + ('shangjiayueq', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='修改前的商家余额')), + ('guanshiyue', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='修改后的管事余额')), + ('guanshiyueq', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='修改前的管事余额')), + ('huiyuants', models.IntegerField(default=0, verbose_name='添加会员天数')), + ('huiyuan_id', models.CharField(blank=True, max_length=6, null=True, verbose_name='会员ID')), + ('qitashuoming', models.TextField(blank=True, null=True, verbose_name='其他说明')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '修改记录表', + 'verbose_name_plural': '修改记录表', + 'db_table': 'xiugaijilu', + }, + ), + migrations.CreateModel( + name='UserMain', + fields=[ + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='主键ID')), + ('yonghuid', models.CharField(db_index=True, max_length=7, unique=True, verbose_name='用户ID')), + ('openid', models.CharField(db_index=True, max_length=64, unique=True, verbose_name='微信OpenID')), + ('avatar', models.CharField(blank=True, max_length=500, null=True, verbose_name='头像')), + ('phone', models.CharField(blank=True, max_length=11, null=True, verbose_name='手机号')), + ('password', models.CharField(blank=True, max_length=200, null=True, verbose_name='密码')), + ('zhifu', models.CharField(blank=True, max_length=500, null=True, verbose_name='收款码图片')), + ('skzhanghao', models.CharField(blank=True, max_length=11, null=True, verbose_name='收款账号')), + ('ip', models.CharField(blank=True, max_length=50, null=True, verbose_name='来访用户ip')), + ('last_login_time', models.DateTimeField(blank=True, null=True, verbose_name='最后登录时间')), + ('user_type', models.CharField(choices=[('normal', '普通用户'), ('dashou', '打手'), ('shop', '商家'), ('guanshi', '管事'), ('admin', '管理员')], default='normal', max_length=10, verbose_name='用户类型')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '用户主表', + 'verbose_name_plural': '用户主表', + 'db_table': 'user_main', + 'indexes': [models.Index(fields=['yonghuid'], name='user_main_yonghui_b8b2fe_idx'), models.Index(fields=['openid'], name='user_main_openid_c9edbe_idx'), models.Index(fields=['user_type'], name='user_main_user_ty_8515fd_idx')], + }, + ), + migrations.CreateModel( + name='Tixianjilu', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='提现记录ID')), + ('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='提现用户ID')), + ('avatar', models.CharField(blank=True, max_length=500, null=True, verbose_name='头像')), + ('phone', models.CharField(blank=True, max_length=11, null=True, verbose_name='手机号')), + ('nicheng', models.CharField(max_length=50, null=True, verbose_name='打手昵称')), + ('leixing', models.PositiveIntegerField(blank=True, null=True, verbose_name='提现类型1:打手佣金2:管事分红')), + ('zhifu', models.CharField(blank=True, max_length=500, null=True, verbose_name='收款码图片')), + ('skzhanghao', models.CharField(blank=True, max_length=11, null=True, verbose_name='收款账号')), + ('jine', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='提现金额')), + ('zhuangtai', models.IntegerField(blank=True, db_index=True, default=1, null=True, verbose_name='提现状态1:审核2:成功3:失败')), + ('fangshi', models.IntegerField(blank=True, db_index=True, null=True, verbose_name='收款方式1:微信2:支付宝')), + ('shenheid', models.CharField(db_index=True, max_length=7, null=True, verbose_name='审核用户ID')), + ('bhliyou', models.TextField(blank=True, null=True, verbose_name='驳回理由')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '提现记录表', + 'verbose_name_plural': '提现记录表', + 'db_table': 'tixianjilu', + 'indexes': [models.Index(fields=['yonghuid'], name='tixianjilu_yonghui_8083bb_idx'), models.Index(fields=['zhuangtai'], name='tixianjilu_zhuangt_839d8e_idx'), models.Index(fields=['shenheid'], name='tixianjilu_shenhei_470e0c_idx'), models.Index(fields=['fangshi'], name='tixianjilu_fangshi_a1e0bc_idx')], + }, + ), + migrations.CreateModel( + name='AdminProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=255, verbose_name='密码')), + ('wechat', models.CharField(blank=True, max_length=50, verbose_name='管理员微信')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='admin_profile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户')), + ], + options={ + 'verbose_name': '管理员扩展表', + 'verbose_name_plural': '管理员扩展表', + 'db_table': 'admin_profile', + 'indexes': [models.Index(fields=['user'], name='admin_profi_user_id_7d067d_idx')], + }, + ), + migrations.CreateModel( + name='UserBoss', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nickname', models.CharField(max_length=50, verbose_name='老板昵称')), + ('zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='消费总额')), + ('alldingdan', models.IntegerField(default=0, verbose_name='订单总数')), + ('alltui', models.IntegerField(default=0, verbose_name='退款总数')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='boss_profile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户')), + ], + options={ + 'verbose_name': '老板扩展表', + 'verbose_name_plural': '老板扩展表', + 'db_table': 'user_boss', + 'indexes': [models.Index(fields=['user'], name='user_boss_user_id_4e81a4_idx')], + }, + ), + migrations.CreateModel( + name='UserDashou', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nicheng', models.CharField(max_length=50, null=True, verbose_name='打手昵称')), + ('chenghao', models.CharField(max_length=50, null=True, verbose_name='打手称号')), + ('zhuangtai', models.IntegerField(default=1, verbose_name='打手状态')), + ('zaixianzhuangtai', models.IntegerField(default=1, verbose_name='打手在线状态')), + ('zhanghaozhuangtai', models.IntegerField(default=1, verbose_name='打手账号状态')), + ('jieshao', models.TextField(null=True, verbose_name='打手介绍')), + ('jiedanzongliang', models.IntegerField(default=0, verbose_name='接单总量')), + ('chengjiaozongliang', models.IntegerField(default=0, verbose_name='成交单总量')), + ('tuikuanliang', models.IntegerField(default=0, verbose_name='退款订单总量')), + ('yue', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='可提现余额')), + ('zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='打单赚取总额')), + ('dianhua', models.CharField(max_length=20, null=True, verbose_name='打手电话')), + ('wechat', models.CharField(max_length=50, null=True, verbose_name='打手微信号')), + ('yaoqingren', models.CharField(db_index=True, max_length=7, null=True, verbose_name='邀请人ID')), + ('jinrijiedan', models.IntegerField(default=0, verbose_name='今日接单数量')), + ('jinrishouyi', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日收益金额')), + ('jinyuejiedan', models.IntegerField(default=0, verbose_name='今月接单数量')), + ('jinyueshouyi', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今月收益金额')), + ('jifen', models.IntegerField(default=0, verbose_name='打手积分')), + ('yajin', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='打手押金')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='dashou_profile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户')), + ], + options={ + 'verbose_name': '打手扩展表', + 'verbose_name_plural': '打手扩展表', + 'db_table': 'user_dashou', + 'indexes': [models.Index(fields=['user'], name='user_dashou_user_id_cf0410_idx'), models.Index(fields=['yaoqingren'], name='user_dashou_yaoqing_894658_idx')], + }, + ), + migrations.CreateModel( + name='UserGuanshi', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('yaoqingma', models.CharField(db_index=True, max_length=20, null=True, verbose_name='邀请码')), + ('dianhua', models.CharField(max_length=20, null=True, verbose_name='管事电话')), + ('wechat', models.CharField(max_length=50, null=True, verbose_name='管事微信号')), + ('yaogingshuliang', models.IntegerField(default=0, verbose_name='邀请打手总数')), + ('zhuangtai', models.IntegerField(choices=[(0, '禁用'), (1, '正常')], default=1, verbose_name='管事状态')), + ('jinrichongzhi', models.IntegerField(default=0, verbose_name='今日充值打手数量')), + ('jinyuechongzhi', models.IntegerField(default=0, verbose_name='今月充值打手数量')), + ('chongzhifenrun', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='充值分佣总额')), + ('yue', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='可提现余额')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='guanshi_profile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户')), + ], + options={ + 'verbose_name': '管事扩展表', + 'verbose_name_plural': '管事扩展表', + 'db_table': 'user_guanshi', + 'indexes': [models.Index(fields=['user'], name='user_guansh_user_id_cbadac_idx'), models.Index(fields=['yaoqingma'], name='user_guansh_yaoqing_21d1d4_idx')], + }, + ), + migrations.CreateModel( + name='UserShangjia', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nicheng', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='商家昵称')), + ('zhuangtai', models.IntegerField(choices=[(0, '待审核'), (1, '正常'), (2, '禁用')], default=1, verbose_name='商家状态')), + ('dianhua', models.CharField(max_length=20, null=True, verbose_name='商家电话')), + ('wechat', models.CharField(max_length=50, null=True, verbose_name='商家微信号')), + ('fabu', models.IntegerField(default=0, verbose_name='发布订单总数量')), + ('tuikuan', models.IntegerField(default=0, verbose_name='退款订单总量')), + ('yue', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='商家余额')), + ('chengjiao', models.IntegerField(default=0, verbose_name='商家成交订单总量')), + ('jinridingdan', models.IntegerField(default=0, verbose_name='商家今日订单总量')), + ('jinriliushui', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='商家今日流水')), + ('jinyuedingdan', models.IntegerField(default=0, verbose_name='商家今月订单总量')), + ('jinyueliushui', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='商家今月流水')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='shop_profile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户')), + ], + options={ + 'verbose_name': '商家扩展表', + 'verbose_name_plural': '商家扩展表', + 'db_table': 'user_shangjia', + 'indexes': [models.Index(fields=['user'], name='user_shangj_user_id_823c5c_idx'), models.Index(fields=['nicheng'], name='user_shangj_nicheng_6967bc_idx')], + }, + ), + ] diff --git a/yonghu/migrations/0002_adminprofile_yaoqingma.py b/yonghu/migrations/0002_adminprofile_yaoqingma.py new file mode 100644 index 0000000..638ed87 --- /dev/null +++ b/yonghu/migrations/0002_adminprofile_yaoqingma.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-01-21 12:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('yonghu', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='adminprofile', + name='yaoqingma', + field=models.CharField(db_index=True, max_length=20, null=True, verbose_name='管理员邀请码'), + ), + ] diff --git a/yonghu/migrations/0003_rankingrecord.py b/yonghu/migrations/0003_rankingrecord.py new file mode 100644 index 0000000..0110fff --- /dev/null +++ b/yonghu/migrations/0003_rankingrecord.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.27 on 2026-01-24 23:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('yonghu', '0002_adminprofile_yaoqingma'), + ] + + operations = [ + migrations.CreateModel( + name='RankingRecord', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID')), + ('zhouqi', models.IntegerField(choices=[(1, '日榜'), (2, '月榜')], verbose_name='周期类型')), + ('tongji_date', models.DateField(db_index=True, verbose_name='统计日期')), + ('shenfen', models.IntegerField(choices=[(2, '打手'), (3, '管事'), (4, '商家')], verbose_name='用户身份')), + ('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')), + ('paiming', models.IntegerField(verbose_name='排名')), + ('zhizhi', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='指标值')), + ('nicheng', models.CharField(max_length=50, null=True, verbose_name='用户昵称')), + ('avatar', models.CharField(blank=True, max_length=500, null=True, verbose_name='头像')), + ('chuangjian_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ], + options={ + 'verbose_name': '排行榜记录', + 'verbose_name_plural': '排行榜记录', + 'db_table': 'ranking_record', + 'indexes': [models.Index(fields=['zhouqi', 'tongji_date', 'shenfen', 'paiming'], name='ranking_rec_zhouqi_fc9f4d_idx'), models.Index(fields=['yonghuid', 'zhouqi'], name='ranking_rec_yonghui_e5be77_idx'), models.Index(fields=['tongji_date'], name='ranking_rec_tongji__820158_idx')], + 'unique_together': {('zhouqi', 'tongji_date', 'shenfen', 'paiming')}, + }, + ), + ] diff --git a/yonghu/migrations/0004_alter_usermain_avatar.py b/yonghu/migrations/0004_alter_usermain_avatar.py new file mode 100644 index 0000000..b006029 --- /dev/null +++ b/yonghu/migrations/0004_alter_usermain_avatar.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-01-24 23:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('yonghu', '0003_rankingrecord'), + ] + + operations = [ + migrations.AlterField( + model_name='usermain', + name='avatar', + field=models.CharField(blank=True, default='a_long/morentouxiang.jpg', max_length=500, null=True, verbose_name='头像'), + ), + ] diff --git a/yonghu/migrations/0005_officialaccountuser_usermain_unionid.py b/yonghu/migrations/0005_officialaccountuser_usermain_unionid.py new file mode 100644 index 0000000..7b368ae --- /dev/null +++ b/yonghu/migrations/0005_officialaccountuser_usermain_unionid.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.27 on 2026-02-07 19:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('yonghu', '0004_alter_usermain_avatar'), + ] + + operations = [ + migrations.CreateModel( + name='OfficialAccountUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('unionid', models.CharField(db_index=True, max_length=64, verbose_name='UnionID')), + ('official_openid', models.CharField(db_index=True, max_length=64, unique=True, verbose_name='服务号OpenID')), + ('subscribe_time', models.DateTimeField(auto_now_add=True, verbose_name='关注时间')), + ('is_subscribed', models.BooleanField(default=True, verbose_name='是否关注')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '服务号用户', + 'verbose_name_plural': '服务号用户', + 'db_table': 'official_account_user', + }, + ), + migrations.AddField( + model_name='usermain', + name='unionid', + field=models.CharField(blank=True, db_index=True, max_length=64, null=True, verbose_name='微信UnionID'), + ), + ] diff --git a/yonghu/migrations/0006_alter_officialaccountuser_unionid.py b/yonghu/migrations/0006_alter_officialaccountuser_unionid.py new file mode 100644 index 0000000..e6d9df2 --- /dev/null +++ b/yonghu/migrations/0006_alter_officialaccountuser_unionid.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-02-07 22:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('yonghu', '0005_officialaccountuser_usermain_unionid'), + ] + + operations = [ + migrations.AlterField( + model_name='officialaccountuser', + name='unionid', + field=models.CharField(db_index=True, max_length=64, null=True, verbose_name='UnionID'), + ), + ] diff --git a/yonghu/migrations/0007_userkefu.py b/yonghu/migrations/0007_userkefu.py new file mode 100644 index 0000000..cf8e244 --- /dev/null +++ b/yonghu/migrations/0007_userkefu.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.27 on 2026-02-21 14:37 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('yonghu', '0006_alter_officialaccountuser_unionid'), + ] + + operations = [ + migrations.CreateModel( + name='UserKefu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nicheng', models.CharField(max_length=50, verbose_name='客服昵称')), + ('erjimima', models.CharField(max_length=128, verbose_name='二级密码')), + ('zhuangtai', models.IntegerField(choices=[(0, '禁用'), (1, '正常')], default=1, verbose_name='客服状态')), + ('jinrichuli', models.IntegerField(default=0, verbose_name='今日处理订单数')), + ('jinyuechuli', models.IntegerField(default=0, verbose_name='本月处理订单数')), + ('zongchuli', models.IntegerField(default=0, verbose_name='累计处理订单数')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='kefu_profile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户')), + ], + options={ + 'verbose_name': '客服扩展表', + 'verbose_name_plural': '客服扩展表', + 'db_table': 'user_kefu', + 'indexes': [models.Index(fields=['user'], name='user_kefu_user_id_5fe3a1_idx'), models.Index(fields=['zhuangtai'], name='user_kefu_zhuangt_317491_idx')], + }, + ), + ] diff --git a/yonghu/migrations/0008_userguanshi_fenghong_erci_and_more.py b/yonghu/migrations/0008_userguanshi_fenghong_erci_and_more.py new file mode 100644 index 0000000..84e4b47 --- /dev/null +++ b/yonghu/migrations/0008_userguanshi_fenghong_erci_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.27 on 2026-02-27 15:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('yonghu', '0007_userkefu'), + ] + + operations = [ + migrations.AddField( + model_name='userguanshi', + name='fenghong_erci', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='二次分红金额'), + ), + migrations.AddField( + model_name='userguanshi', + name='fenghong_erci_enabled', + field=models.BooleanField(default=False, verbose_name='开启二次分红'), + ), + migrations.AddField( + model_name='userguanshi', + name='fenhong_yici', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='首次分红金额'), + ), + ] diff --git a/yonghu/migrations/0009_userdashou_ewai_xiane_userdashou_jinritixian_jine_and_more.py b/yonghu/migrations/0009_userdashou_ewai_xiane_userdashou_jinritixian_jine_and_more.py new file mode 100644 index 0000000..f0290f3 --- /dev/null +++ b/yonghu/migrations/0009_userdashou_ewai_xiane_userdashou_jinritixian_jine_and_more.py @@ -0,0 +1,89 @@ +# Generated by Django 4.2.27 on 2026-03-16 17:04 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('yonghu', '0008_userguanshi_fenghong_erci_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='userdashou', + name='ewai_xiane', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='额外提现限额(元)'), + ), + migrations.AddField( + model_name='userdashou', + name='jinritixian_jine', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日已提现金额'), + ), + migrations.AddField( + model_name='userdashou', + name='kaioi_ewai_xiane', + field=models.BooleanField(default=False, verbose_name='是否开启额外提现限额'), + ), + migrations.AddField( + model_name='userdashou', + name='last_tixian_date', + field=models.DateField(blank=True, null=True, verbose_name='最近提现日期'), + ), + migrations.AddField( + model_name='userguanshi', + name='ewai_xiane', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='额外提现限额(元)'), + ), + migrations.AddField( + model_name='userguanshi', + name='jinritixian_jine', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日已提现金额'), + ), + migrations.AddField( + model_name='userguanshi', + name='kaioi_ewai_xiane', + field=models.BooleanField(default=False, verbose_name='是否开启额外提现限额'), + ), + migrations.AddField( + model_name='userguanshi', + name='last_tixian_date', + field=models.DateField(blank=True, null=True, verbose_name='最近提现日期'), + ), + migrations.AddField( + model_name='userguanshi', + name='yaoqingren', + field=models.CharField(db_index=True, max_length=7, null=True, verbose_name='邀请人ID'), + ), + migrations.CreateModel( + name='UserZuzhang', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('yaoqingma', models.CharField(db_index=True, max_length=20, null=True, verbose_name='组长邀请码')), + ('fenyong_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='分佣总额(元)')), + ('ketixian_jine', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='可提现金额(元)')), + ('yaoqing_zongshu', models.IntegerField(default=0, verbose_name='邀请总数')), + ('jinri_fenyong', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日分佣(元)')), + ('jinyue_fenyong', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今月分佣(元)')), + ('zhuangtai', models.IntegerField(choices=[(0, '禁用'), (1, '正常')], default=1, verbose_name='账号状态')), + ('haibao_url', models.CharField(blank=True, max_length=500, null=True, verbose_name='推广海报相对URL')), + ('jinri_tixian', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日提现金额(元)')), + ('last_tixian_time', models.DateTimeField(blank=True, null=True, verbose_name='最后提现时间')), + ('ewai_tixian_xiane', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='额外提现限额(元)')), + ('kaioi_ewai_tixian', models.BooleanField(default=False, verbose_name='开启额外提现')), + ('kaioi_ewai_fenhong', models.BooleanField(default=False, verbose_name='开启额外分红')), + ('ewai_fenhong_jine', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='额外分红金额(元)')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='zuzhang_profile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户')), + ], + options={ + 'verbose_name': '组长扩展表', + 'verbose_name_plural': '组长扩展表', + 'db_table': 'user_zuzhang', + 'indexes': [models.Index(fields=['user'], name='user_zuzhan_user_id_8064f2_idx'), models.Index(fields=['yaoqingma'], name='user_zuzhan_yaoqing_cba91d_idx')], + }, + ), + ] diff --git a/yonghu/migrations/0010_userguanshi_invite_qrcode_url.py b/yonghu/migrations/0010_userguanshi_invite_qrcode_url.py new file mode 100644 index 0000000..818f7d2 --- /dev/null +++ b/yonghu/migrations/0010_userguanshi_invite_qrcode_url.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-03-19 12:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('yonghu', '0009_userdashou_ewai_xiane_userdashou_jinritixian_jine_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='userguanshi', + name='invite_qrcode_url', + field=models.CharField(blank=True, max_length=500, null=True, verbose_name='邀请码二维码图片URL'), + ), + ] diff --git a/yonghu/migrations/0011_usermain_shoujihao_renzheng.py b/yonghu/migrations/0011_usermain_shoujihao_renzheng.py new file mode 100644 index 0000000..7bf606a --- /dev/null +++ b/yonghu/migrations/0011_usermain_shoujihao_renzheng.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-05-10 02:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('yonghu', '0010_userguanshi_invite_qrcode_url'), + ] + + operations = [ + migrations.AddField( + model_name='usermain', + name='shoujihao_renzheng', + field=models.BooleanField(default=False, verbose_name='手机号是否已认证'), + ), + ] diff --git a/yonghu/migrations/0012_usershenheguan.py b/yonghu/migrations/0012_usershenheguan.py new file mode 100644 index 0000000..85dae1c --- /dev/null +++ b/yonghu/migrations/0012_usershenheguan.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.27 on 2026-05-14 18:42 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('yonghu', '0011_usermain_shoujihao_renzheng'), + ] + + operations = [ + migrations.CreateModel( + name='UserShenheguan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('zhuangtai', models.IntegerField(choices=[(0, '禁用'), (1, '正常')], default=1, verbose_name='账号状态')), + ('shenhe_zhuangtai', models.IntegerField(choices=[(0, '空闲'), (1, '审核中')], default=0, verbose_name='审核官状态')), + ('shenhe_zongshu', models.IntegerField(default=0, verbose_name='审核总数')), + ('tongguo_zongshu', models.IntegerField(default=0, verbose_name='通过总数')), + ('yue', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='可提现余额')), + ('zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='赚取总额')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='shenheguan_profile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户')), + ], + options={ + 'verbose_name': '审核官扩展表', + 'verbose_name_plural': '审核官扩展表', + 'db_table': 'user_shenheguan', + 'indexes': [models.Index(fields=['user'], name='user_shenhe_user_id_624f19_idx')], + }, + ), + ] diff --git a/yonghu/migrations/0013_usershenheguan_is_renzheng.py b/yonghu/migrations/0013_usershenheguan_is_renzheng.py new file mode 100644 index 0000000..41f4a85 --- /dev/null +++ b/yonghu/migrations/0013_usershenheguan_is_renzheng.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.27 on 2026-05-18 02:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('yonghu', '0012_usershenheguan'), + ] + + operations = [ + migrations.AddField( + model_name='usershenheguan', + name='is_renzheng', + field=models.BooleanField(default=False, verbose_name='是否认证'), + ), + ] diff --git a/yonghu/migrations/0014_tixianautorecord_tixianshenhejilu_and_more.py b/yonghu/migrations/0014_tixianautorecord_tixianshenhejilu_and_more.py new file mode 100644 index 0000000..bf27299 --- /dev/null +++ b/yonghu/migrations/0014_tixianautorecord_tixianshenhejilu_and_more.py @@ -0,0 +1,113 @@ +# Generated by Django 4.2.27 on 2026-06-08 23:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('yonghu', '0013_usershenheguan_is_renzheng'), + ] + + operations = [ + migrations.CreateModel( + name='TixianAutoRecord', + fields=[ + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('tixian_id', models.CharField(db_index=True, max_length=32, unique=True, verbose_name='提现订单号(自定义)')), + ('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')), + ('leixing', models.IntegerField(verbose_name='提现类型 1打手 2管事')), + ('jine', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='申请提现金额')), + ('shouxufei', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='手续费')), + ('shijidaozhang', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='实际到账金额')), + ('feilv', models.DecimalField(decimal_places=4, max_digits=5, verbose_name='费率')), + ('zhuangtai', models.IntegerField(choices=[(0, '处理中'), (1, '成功'), (2, '失败')], default=0, verbose_name='状态')), + ('wechat_transfer_no', models.CharField(blank=True, max_length=64, null=True, verbose_name='微信转账单号')), + ('wechat_package', models.TextField(blank=True, null=True, verbose_name='微信返回的package_info(JSON)')), + ('fail_reason', models.TextField(blank=True, null=True, verbose_name='失败原因')), + ('shenhe_danhao', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='关联审核单号')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '自动提现记录', + 'verbose_name_plural': '自动提现记录', + 'db_table': 'tixian_auto_record', + }, + ), + migrations.CreateModel( + name='TixianShenheJilu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('shenhe_danhao', models.CharField(db_index=True, max_length=32, unique=True, verbose_name='审核单号')), + ('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')), + ('leixing', models.PositiveSmallIntegerField(db_index=True, verbose_name='提现类型')), + ('zhuangtai', models.IntegerField(db_index=True, default=1, verbose_name='审核状态(1审核中2提现成功3提现失败4审核成功5审核失败6待收款)')), + ('shenqing_jine', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='申请提现金额(扣款额)')), + ('shouxufei', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='手续费')), + ('shijidaozhang', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='实际到账金额')), + ('feilv', models.DecimalField(decimal_places=4, default=0, max_digits=5, verbose_name='手续费率')), + ('bhliyou', models.TextField(blank=True, null=True, verbose_name='驳回/失败原因')), + ('bo_hui_ren_id', models.CharField(blank=True, db_index=True, max_length=11, null=True, verbose_name='驳回人用户ID')), + ('shenhe_ren_id', models.CharField(blank=True, db_index=True, max_length=11, null=True, verbose_name='审核通过操作人ID')), + ('tixianjilu_id', models.IntegerField(blank=True, db_index=True, null=True, verbose_name='关联Tixianjilu.id')), + ('tixian_auto_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='最近一次打款单号(非唯一,重试收款会更新)')), + ('fail_reason', models.TextField(blank=True, null=True, verbose_name='微信打款失败原因')), + ('dakuan_mode', models.PositiveSmallIntegerField(db_index=True, default=1, verbose_name='打款方式(1手动2自动)')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '提现审核记录', + 'verbose_name_plural': '提现审核记录', + 'db_table': 'tixian_shenhe_jilu', + }, + ), + migrations.AddField( + model_name='tixianjilu', + name='shenhe_danhao', + field=models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='关联提现审核单号'), + ), + migrations.AddField( + model_name='tixianjilu', + name='shenhe_jilu_id', + field=models.IntegerField(blank=True, db_index=True, null=True, verbose_name='关联提现审核记录ID(TixianShenheJilu.id)'), + ), + migrations.AlterField( + model_name='tixianjilu', + name='shenheid', + field=models.CharField(db_index=True, max_length=11, null=True, verbose_name='审核用户ID'), + ), + migrations.AddIndex( + model_name='tixianjilu', + index=models.Index(fields=['shenhe_danhao'], name='tixianjilu_shenhe__7b3e7f_idx'), + ), + migrations.AddIndex( + model_name='tixianjilu', + index=models.Index(fields=['shenhe_jilu_id'], name='tixianjilu_shenhe__d9355e_idx'), + ), + migrations.AddIndex( + model_name='tixianshenhejilu', + index=models.Index(fields=['yonghuid', 'zhuangtai'], name='tixian_shen_yonghui_a782e9_idx'), + ), + migrations.AddIndex( + model_name='tixianshenhejilu', + index=models.Index(fields=['yonghuid', 'leixing'], name='tixian_shen_yonghui_bb44eb_idx'), + ), + migrations.AddIndex( + model_name='tixianshenhejilu', + index=models.Index(fields=['shenhe_danhao'], name='tixian_shen_shenhe__4a1ca7_idx'), + ), + migrations.AddIndex( + model_name='tixianautorecord', + index=models.Index(fields=['tixian_id'], name='tixian_auto_tixian__273dac_idx'), + ), + migrations.AddIndex( + model_name='tixianautorecord', + index=models.Index(fields=['yonghuid'], name='tixian_auto_yonghui_61c5b6_idx'), + ), + migrations.AddIndex( + model_name='tixianautorecord', + index=models.Index(fields=['zhuangtai'], name='tixian_auto_zhuangt_24d9fa_idx'), + ), + ] diff --git a/yonghu/migrations/__init__.py b/yonghu/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yonghu/models.py b/yonghu/models.py new file mode 100644 index 0000000..8245b45 --- /dev/null +++ b/yonghu/models.py @@ -0,0 +1,600 @@ +# models.py +from django.db import models +import random +import string +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager + +# ==================== 用户主表 ==================== +class UserMain(AbstractBaseUser): + # ID字段 + id = models.AutoField(primary_key=True, verbose_name='主键ID') + yonghuid = models.CharField(max_length=7, unique=True, db_index=True, verbose_name='用户ID') + + # 微信相关字段 + openid = models.CharField(max_length=64, unique=True, db_index=True, verbose_name='微信OpenID') + unionid = models.CharField(max_length=64, null=True, blank=True, db_index=True, verbose_name='微信UnionID') + + # 基础信息字段 + #tupian_url = models.CharField(max_length=500, null=True, blank=True, verbose_name='商品图片URL') # 用CharField + avatar = models.CharField(max_length=500, null=True, blank=True,default='a_long/morentouxiang.jpg', verbose_name='头像') # 用CharField + phone = models.CharField(max_length=11, blank=True, null=True, verbose_name='手机号') + shoujihao_renzheng = models.BooleanField(default=False, verbose_name='手机号是否已认证') + password = models.CharField(max_length=200, null=True, blank=True, verbose_name='密码') + zhifu = models.CharField(max_length=500, null=True, blank=True, verbose_name='收款码图片') # 用CharField + skzhanghao = models.CharField(max_length=11, blank=True, null=True, verbose_name='收款账号') + ip = models.CharField(max_length=50, blank=True, null=True, verbose_name='来访用户ip') + last_login_time = models.DateTimeField(null=True, blank=True, verbose_name='最后登录时间') + + + # 用户类型字段 + user_type = models.CharField( + max_length=10, + choices=[ + ('normal', '普通用户'), + ('dashou', '打手'), + ('shop', '商家'), + ('guanshi', '管事'), + ('admin', '管理员') + ], + default='normal', + verbose_name='用户类型' + ) + + # 状态和时间字段 + #is_active = models.BooleanField(default=True, verbose_name='是否激活') + #last_login = models.DateTimeField(blank=True, null=True, verbose_name='最后登录时间') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + # 邀请码字段 + #yaoqingma = models.CharField(max_length=20, blank=True, null=True, verbose_name='邀请码') + + # 只需要添加这3行 + USERNAME_FIELD = 'openid' # 用openid作为用户名 + REQUIRED_FIELDS = ['yonghuid'] # 必填字段 + + class Meta: + db_table = 'user_main' + verbose_name = '用户主表' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['yonghuid']), + models.Index(fields=['openid']), + #models.Index(fields=['phone']), + models.Index(fields=['user_type']), + #models.Index(fields=['create_time']), + ] + + def __str__(self): + return f'{self.yonghuid}' + + + +# 新增服务号用户表 +class OfficialAccountUser(models.Model): + unionid = models.CharField(max_length=64, null =True,db_index=True, verbose_name='UnionID') + official_openid = models.CharField(max_length=64, db_index=True, unique=True, verbose_name='服务号OpenID') + subscribe_time = models.DateTimeField(auto_now_add=True, verbose_name='关注时间') + is_subscribed = models.BooleanField(default=True, verbose_name='是否关注') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'official_account_user' + verbose_name = '服务号用户' + verbose_name_plural = verbose_name + + def __str__(self): + return f"{self.unionid[:8]}... (关注: {self.is_subscribed})" + + +# ==================== 老板扩展表 ==================== +class UserBoss(models.Model): + user = models.OneToOneField(UserMain, on_delete=models.CASCADE, related_name='boss_profile', + verbose_name='关联用户') + nickname = models.CharField(max_length=50, verbose_name='老板昵称') + zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='消费总额') + alldingdan = models.IntegerField(default=0, verbose_name='订单总数') + alltui = models.IntegerField(default=0, verbose_name='退款总数') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'user_boss' + verbose_name = '老板扩展表' + verbose_name_plural = verbose_name + indexes = [models.Index(fields=['user']), ] + + +# ==================== 打手扩展表 ==================== +class UserDashou(models.Model): + user = models.OneToOneField(UserMain, on_delete=models.CASCADE, related_name='dashou_profile', + verbose_name='关联用户') + nicheng = models.CharField(max_length=50, null=True, verbose_name='打手昵称') + chenghao = models.CharField(max_length=50, null=True,verbose_name='打手称号') + zhuangtai = models.IntegerField(default=1, verbose_name='打手状态') + zaixianzhuangtai = models.IntegerField(default=1, verbose_name='打手在线状态') + zhanghaozhuangtai = models.IntegerField(default=1, verbose_name='打手账号状态') + jieshao = models.TextField(null=True, verbose_name='打手介绍') + jiedanzongliang = models.IntegerField(default=0, verbose_name='接单总量') + chengjiaozongliang = models.IntegerField(default=0, verbose_name='成交单总量') + tuikuanliang = models.IntegerField(default=0, verbose_name='退款订单总量') + yue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='可提现余额') + zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='打单赚取总额') + dianhua = models.CharField(max_length=20, null=True, verbose_name='打手电话') + wechat = models.CharField(max_length=50,null=True, verbose_name='打手微信号') + yaoqingren = models.CharField(max_length=7,null=True, db_index=True, verbose_name='邀请人ID') + jinrijiedan = models.IntegerField(default=0, verbose_name='今日接单数量') + jinrishouyi = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日收益金额') + jinyuejiedan = models.IntegerField(default=0, verbose_name='今月接单数量') + jinyueshouyi = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今月收益金额') + jifen = models.IntegerField(default=0, verbose_name='打手积分') + yajin= models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='打手押金') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + last_tixian_date = models.DateField(null=True, blank=True, verbose_name='最近提现日期') # 用于重置今日提现金额 + jinritixian_jine = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日已提现金额') + kaioi_ewai_xiane = models.BooleanField(default=False, verbose_name='是否开启额外提现限额') # True=开启额外限额 + ewai_xiane = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='额外提现限额(元)') + + class Meta: + db_table = 'user_dashou' + verbose_name = '打手扩展表' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['user']), + #models.Index(fields=['zhuangtai']), + models.Index(fields=['yaoqingren']), + #models.Index(fields=['nicheng']), + + ] + + +# ==================== 商家扩展表 ==================== +class UserShangjia(models.Model): + user = models.OneToOneField(UserMain, on_delete=models.CASCADE, related_name='shop_profile', + verbose_name='关联用户') + nicheng = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='商家昵称') + zhuangtai = models.IntegerField(choices=[(0, '待审核'), (1, '正常'), (2, '禁用')], default=1, + verbose_name='商家状态') + dianhua = models.CharField(max_length=20,null=True,verbose_name='商家电话') + wechat = models.CharField(max_length=50, null=True, verbose_name='商家微信号') + fabu = models.IntegerField(default=0, verbose_name='发布订单总数量') + tuikuan = models.IntegerField(default=0, verbose_name='退款订单总量') + yue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='商家余额') + chengjiao = models.IntegerField(default=0, verbose_name='商家成交订单总量') + jinridingdan = models.IntegerField(default=0, verbose_name='商家今日订单总量') + jinriliushui = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='商家今日流水') + jinyuedingdan = models.IntegerField(default=0, verbose_name='商家今月订单总量') + jinyueliushui = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='商家今月流水') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'user_shangjia' + verbose_name = '商家扩展表' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['user']), + models.Index(fields=['nicheng']), + ] + + +# ==================== 管事扩展表 ==================== +class UserGuanshi(models.Model): + user = models.OneToOneField(UserMain, on_delete=models.CASCADE, related_name='guanshi_profile', + verbose_name='关联用户') + yaoqingren = models.CharField(max_length=7,null=True, db_index=True, verbose_name='邀请人ID') + yaoqingma = models.CharField(max_length=20,null=True,db_index=True, verbose_name='邀请码') + invite_qrcode_url = models.CharField(max_length=500, null=True, blank=True, verbose_name='邀请码二维码图片URL') + dianhua = models.CharField(max_length=20,null=True, verbose_name='管事电话') + wechat = models.CharField(max_length=50,null=True, verbose_name='管事微信号') + yaogingshuliang = models.IntegerField(default=0, verbose_name='邀请打手总数') + zhuangtai = models.IntegerField(choices=[(0, '禁用'), (1, '正常')], default=1, verbose_name='管事状态') + jinrichongzhi = models.IntegerField(default=0, verbose_name='今日充值打手数量') + jinyuechongzhi = models.IntegerField(default=0, verbose_name='今月充值打手数量') + chongzhifenrun = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='充值分佣总额') + yue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='可提现余额') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + # 新增:首次分红金额(针对特殊用户) + fenhong_yici = models.DecimalField(max_digits=10,decimal_places=2,default=0.00,verbose_name='首次分红金额') + + # 新增:二次分红开关 + fenghong_erci_enabled = models.BooleanField(default=False,verbose_name='开启二次分红') + + # 新增:二次分红金额 + fenghong_erci = models.DecimalField(max_digits=10,decimal_places=2,default=0.00,verbose_name='二次分红金额') + + + last_tixian_date = models.DateField(null=True, blank=True, verbose_name='最近提现日期') + jinritixian_jine = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日已提现金额') + kaioi_ewai_xiane = models.BooleanField(default=False, verbose_name='是否开启额外提现限额') + ewai_xiane = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='额外提现限额(元)') + + class Meta: + db_table = 'user_guanshi' + verbose_name = '管事扩展表' + verbose_name_plural = verbose_name + indexes = [models.Index(fields=['user']), + models.Index(fields=['yaoqingma']), + ] + + + + +class UserZuzhang(models.Model): + """组长扩展表(与UserMain一对一关联)""" + user = models.OneToOneField( + UserMain, + on_delete=models.CASCADE, + related_name='zuzhang_profile', + verbose_name='关联用户', + db_index=True + ) + # 组长邀请码 + yaoqingma = models.CharField(max_length=20, null=True, db_index=True, verbose_name='组长邀请码') + # 分佣总额 + fenyong_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='分佣总额(元)') + # 可提现金额 + ketixian_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='可提现金额(元)') + # 邀请总数 + yaoqing_zongshu = models.IntegerField(default=0, verbose_name='邀请总数') + # 今日分佣 + jinri_fenyong = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日分佣(元)') + # 今月分佣 + jinyue_fenyong = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今月分佣(元)') + # 账号状态 + zhuangtai = models.IntegerField(choices=[(0, '禁用'), (1, '正常')], default=1, verbose_name='账号状态') + # 推广海报相对URL + haibao_url = models.CharField(max_length=500, null=True, blank=True, verbose_name='推广海报相对URL') + # 今日提现 + jinri_tixian = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日提现金额(元)') + # 最后提现时间 + last_tixian_time = models.DateTimeField(null=True, blank=True, verbose_name='最后提现时间') + # 额外提现限额 + ewai_tixian_xiane = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='额外提现限额(元)') + # 是否开启额外提现 + kaioi_ewai_tixian = models.BooleanField(default=False, verbose_name='开启额外提现') + # 是否开启额外分红 + kaioi_ewai_fenhong = models.BooleanField(default=False, verbose_name='开启额外分红') + # 额外分红金额 + ewai_fenhong_jine = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='额外分红金额(元)') + # 创建时间 + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + # 更新时间 + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'user_zuzhang' + verbose_name = '组长扩展表' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['user']), + models.Index(fields=['yaoqingma']), + ] + + def __str__(self): + return f"组长 {self.user.yonghuid}" + +class UserShenheguan(models.Model): + """审核官扩展表(与UserMain一对一关联)""" + user = models.OneToOneField(UserMain, on_delete=models.CASCADE, related_name='shenheguan_profile', verbose_name='关联用户', db_index=True) + zhuangtai = models.IntegerField(choices=[(0, '禁用'), (1, '正常')], default=1, verbose_name='账号状态') + shenhe_zhuangtai = models.IntegerField(choices=[(0, '空闲'), (1, '审核中')], default=0, verbose_name='审核官状态') + is_renzheng = models.BooleanField(default=False, verbose_name='是否认证') + shenhe_zongshu = models.IntegerField(default=0, verbose_name='审核总数') + tongguo_zongshu = models.IntegerField(default=0, verbose_name='通过总数') + yue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='可提现余额') + zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='赚取总额') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'user_shenheguan' + verbose_name = '审核官扩展表' + verbose_name_plural = verbose_name + indexes = [models.Index(fields=['user'])] + + +# ==================== 管理员扩展表 ==================== +class AdminProfile(models.Model): + user = models.OneToOneField(UserMain, on_delete=models.CASCADE, related_name='admin_profile', + verbose_name='关联用户') + password = models.CharField(max_length=255, verbose_name='密码') + wechat = models.CharField(max_length=50, blank=True, verbose_name='管理员微信') + yaoqingma = models.CharField(max_length=20, null=True, db_index=True,verbose_name='管理员邀请码') # 🟢 新增字段 + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'admin_profile' + verbose_name = '管理员扩展表' + verbose_name_plural = verbose_name + indexes = [models.Index(fields=['user']), ] + + +class UserKefu(models.Model): + user = models.OneToOneField(UserMain, on_delete=models.CASCADE, related_name='kefu_profile', verbose_name='关联用户') + nicheng = models.CharField(max_length=50, verbose_name='客服昵称') + erjimima = models.CharField(max_length=128, verbose_name='二级密码') + zhuangtai = models.IntegerField(choices=[(0, '禁用'), (1, '正常')], default=1, verbose_name='客服状态') + jinrichuli = models.IntegerField(default=0, verbose_name='今日处理订单数') + jinyuechuli = models.IntegerField(default=0, verbose_name='本月处理订单数') + zongchuli = models.IntegerField(default=0, verbose_name='累计处理订单数') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'user_kefu' + verbose_name = '客服扩展表' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['user']), + models.Index(fields=['zhuangtai']), + ] + + def __str__(self): + return f"{self.nicheng} ({self.user.yonghuid})" + + + + +class Tixianjilu(models.Model): + id = models.AutoField(primary_key=True, verbose_name='提现记录ID') + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='提现用户ID') + avatar = models.CharField(max_length=500, null=True, blank=True, verbose_name='头像') # 用CharField + phone = models.CharField(max_length=11, blank=True, null=True, verbose_name='手机号') + nicheng = models.CharField(max_length=50, null=True, verbose_name='打手昵称') + leixing = models.PositiveIntegerField(null=True, blank=True, verbose_name='提现类型1:打手佣金2:管事分红') + zhifu = models.CharField(max_length=500, null=True, blank=True, verbose_name='收款码图片') # 用CharField + skzhanghao = models.CharField(max_length=11, blank=True, null=True, verbose_name='收款账号') + jine = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='提现金额') + # 状态与平台:用整数表示,具体含义由业务逻辑定义 + zhuangtai = models.IntegerField(default=1, null=True, blank=True, db_index=True, verbose_name='提现状态1:审核2:成功3:失败') + fangshi = models.IntegerField(null=True, blank=True,db_index=True, verbose_name='收款方式1:微信2:支付宝') + shenheid = models.CharField(max_length=11, null=True, db_index=True, verbose_name='审核用户ID') + bhliyou = models.TextField(null=True, blank=True, verbose_name='驳回理由') + # ---------- 自动提现审核新流程关联字段(旧数据为空,兼容历史记录) ---------- + shenhe_jilu_id = models.IntegerField( + null=True, blank=True, db_index=True, + verbose_name='关联提现审核记录ID(TixianShenheJilu.id)' + ) + shenhe_danhao = models.CharField( + max_length=32, null=True, blank=True, db_index=True, + verbose_name='关联提现审核单号' + ) + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'tixianjilu' + verbose_name = '提现记录表' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['yonghuid']), + models.Index(fields=['zhuangtai']), + models.Index(fields=['shenheid']), + models.Index(fields=['fangshi']), + models.Index(fields=['shenhe_danhao']), + models.Index(fields=['shenhe_jilu_id']), + ] + + + + + + +class TixianShenheJilu(models.Model): + """ + 提现审核记录表(自动打款新流程) + 状态码与前端 TX_STATUS / Tixianjilu.zhuangtai 完全对标: + 1=审核中 2=提现成功(已收款) 3=提现失败 4=审核成功 5=审核失败 6=待收款 + """ + # 审核单号(唯一,前端字段 shenhe_danhao) + shenhe_danhao = models.CharField( + max_length=32, unique=True, db_index=True, verbose_name='审核单号' + ) + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') + # 提现类型:1打手佣金 2管事分红 3组长分红 4考核官分佣 5打手押金 6商家余额 + leixing = models.PositiveSmallIntegerField(db_index=True, verbose_name='提现类型') + zhuangtai = models.IntegerField( + default=1, db_index=True, + verbose_name='审核状态(1审核中2提现成功3提现失败4审核成功5审核失败6待收款)' + ) + # 金额:shenqing_jine=申请扣款金额;shijidaozhang=实际打款到账金额 + shenqing_jine = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='申请提现金额(扣款额)') + shouxufei = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='手续费') + shijidaozhang = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='实际到账金额') + feilv = models.DecimalField(max_digits=5, decimal_places=4, default=0, verbose_name='手续费率') + # 审核/驳回 + bhliyou = models.TextField(null=True, blank=True, verbose_name='驳回/失败原因') + bo_hui_ren_id = models.CharField( + max_length=11, null=True, blank=True, db_index=True, verbose_name='驳回人用户ID' + ) + shenhe_ren_id = models.CharField( + max_length=11, null=True, blank=True, db_index=True, verbose_name='审核通过操作人ID' + ) + # 关联提现记录表主键 + tixianjilu_id = models.IntegerField(null=True, blank=True, db_index=True, verbose_name='关联Tixianjilu.id') + # 最近一次打款单号(仅作记录,不唯一;同一审核单可多次收款产生多条打款记录) + tixian_auto_id = models.CharField( + max_length=32, null=True, blank=True, db_index=True, + verbose_name='最近一次打款单号(非唯一,重试收款会更新)' + ) + fail_reason = models.TextField(null=True, blank=True, verbose_name='微信打款失败原因') + # 打款方式:1=手动打款 2=自动打款(微信商家转账) + dakuan_mode = models.PositiveSmallIntegerField( + default=1, db_index=True, verbose_name='打款方式(1手动2自动)' + ) + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'tixian_shenhe_jilu' + verbose_name = '提现审核记录' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['yonghuid', 'zhuangtai']), + models.Index(fields=['yonghuid', 'leixing']), + models.Index(fields=['shenhe_danhao']), + ] + + def __str__(self): + return f'{self.shenhe_danhao}({self.yonghuid})' + + + +class TixianAutoRecord(models.Model): + """ + 自动提现记录表(微信自动打款) + """ + id = models.BigAutoField(primary_key=True) + tixian_id = models.CharField( + max_length=32, + unique=True, + db_index=True, + verbose_name='提现订单号(自定义)' + ) + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') + leixing = models.IntegerField(verbose_name='提现类型 1打手 2管事') + jine = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='申请提现金额') + shouxufei = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='手续费') + shijidaozhang = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='实际到账金额') + feilv = models.DecimalField(max_digits=5, decimal_places=4, verbose_name='费率') + zhuangtai = models.IntegerField( + default=0, + choices=[(0, '处理中'), (1, '成功'), (2, '失败')], + verbose_name='状态' + ) + wechat_transfer_no = models.CharField( + max_length=64, + null=True, + blank=True, + verbose_name='微信转账单号' + ) + wechat_package = models.TextField( + null=True, + blank=True, + verbose_name='微信返回的package_info(JSON)' + ) + fail_reason = models.TextField(null=True, blank=True, verbose_name='失败原因') + # 关联提现审核单号(新流程必填,旧数据为空) + shenhe_danhao = models.CharField( + max_length=32, null=True, blank=True, db_index=True, verbose_name='关联审核单号' + ) + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'tixian_auto_record' + verbose_name = '自动提现记录' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['tixian_id']), + models.Index(fields=['yonghuid']), + models.Index(fields=['zhuangtai']), + ] + + def __str__(self): + return f"{self.tixian_id} - {self.get_zhuangtai_display()}" + + + + + +# yonghu/models.py +class Xiugaijilu(models.Model): + id = models.AutoField(primary_key=True, verbose_name='记录ID') + yonghuid = models.CharField(max_length=7, verbose_name='被修改用户ID') + xiugaiid = models.CharField(max_length=30, verbose_name='修改人账号ID') + leixing = models.IntegerField(verbose_name='被修改用户类型') + + # 打手相关金钱字段 + xiugaitijiao = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, + verbose_name='修改后的打手提现金额') + xiugaitijiaoq = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, + verbose_name='修改前的打手提现金额') + + # 打手积分 + jifen = models.IntegerField(default=0, verbose_name='修改后的打手积分') + yjifen = models.IntegerField(default=0, verbose_name='修改前的打手积分') + + # 打手押金 + yajin = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='修改后的打手押金') + yyajin = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='修改前的打手押金') + + # 商家余额 + shangjiayue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='修改后的商家余额') + shangjiayueq = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='修改前的商家余额') + + # 管事余额 + guanshiyue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='修改后的管事余额') + guanshiyueq = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='修改前的管事余额') + + # 会员相关 + huiyuants = models.IntegerField(default=0, verbose_name='添加会员天数') + huiyuan_id = models.CharField(max_length=6, null=True, blank=True, verbose_name='会员ID') + + # 其他字段 + qitashuoming = models.TextField(null=True, blank=True, verbose_name='其他说明') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'xiugaijilu' + verbose_name = '修改记录表' + verbose_name_plural = verbose_name + + def __str__(self): + return f"修改记录-用户{self.yonghuid}" + + + + + +# yonghu/models.py - 新增排行榜模型 +class RankingRecord(models.Model): + # id: 主键ID + id = models.AutoField(primary_key=True, verbose_name='ID') + + # zhouqi: 周期类型,1=日榜,2=月榜 + zhouqi = models.IntegerField(choices=[(1, '日榜'), (2, '月榜')], verbose_name='周期类型') + + # tongji_date: 统计日期(日榜存具体日期,月榜存月份第一天) + tongji_date = models.DateField(db_index=True, verbose_name='统计日期') + + # shenfen: 用户身份,2=打手,3=管事,4=商家(1=普通用户不统计) + shenfen = models.IntegerField(choices=[(2, '打手'), (3, '管事'), (4, '商家')], verbose_name='用户身份') + + # yonghuid: 用户ID(冗余存储,避免连表查询) + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') + + # paiming: 排名(从1开始) + paiming = models.IntegerField(verbose_name='排名') + + # zhizhi: 指标数值(打手:收益金额,管事:充值人数,商家:流水金额) + zhizhi = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='指标值') + + # nicheng: 用户昵称 + nicheng = models.CharField(max_length=50, null=True,verbose_name='用户昵称') + + # avatar: 用户头像URL + avatar = models.CharField(max_length=500, blank=True, null=True, verbose_name='头像') + + # chuangjian_time: 记录创建时间 + chuangjian_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'ranking_record' + verbose_name = '排行榜记录' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['zhouqi', 'tongji_date', 'shenfen', 'paiming']), + models.Index(fields=['yonghuid', 'zhouqi']), + models.Index(fields=['tongji_date']), + ] + unique_together = [['zhouqi', 'tongji_date', 'shenfen', 'paiming']] + + def __str__(self): + zhouqi_str = '日' if self.zhouqi == 1 else '月' + shenfen_map = {2: '打手', 3: '管事', 4: '商家'} + return f"{self.tongji_date} {zhouqi_str}榜 {shenfen_map.get(self.shenfen)} 第{self.paiming}名" + + diff --git a/yonghu/paihang_views.py b/yonghu/paihang_views.py new file mode 100644 index 0000000..6b686a7 --- /dev/null +++ b/yonghu/paihang_views.py @@ -0,0 +1,217 @@ +""" +排行榜接口 — 基于 houtai 日统计表聚合查询 +POST /yonghu/phbhqsj +""" +import logging +from datetime import date, timedelta +from decimal import Decimal + +from django.db.models import Sum +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from houtai.models import ( + DashouRiTongji, + GuanshiRiTongji, + ZuzhangRiTongji, + ShangjiaRiTongji, +) +from yonghu.models import UserMain, UserDashou, UserShangjia, UserBoss + +logger = logging.getLogger(__name__) + +MAX_RANK = 50 + +RIQI_OPTIONS = frozenset({'今日', '本周', '本月', '总榜', '昨日', '上周', '上月'}) +SHENFEN_OPTIONS = frozenset({'dashou', 'guanshi', 'zuzhang', 'shangjia'}) + +ROLE_CONFIG = { + 'dashou': { + 'model': DashouRiTongji, + 'sort_field': 'chengjiao_zonge', + 'fields': ('jiedan_zongliang', 'chengjiao_zongliang', 'jiedan_zonge', 'chengjiao_zonge'), + 'int_fields': frozenset({'jiedan_zongliang', 'chengjiao_zongliang'}), + }, + 'guanshi': { + 'model': GuanshiRiTongji, + 'sort_field': 'shouru_zonge', + 'fields': ('yaoqing_dashou_shu', 'chongzhi_dashou_shu', 'shouru_zonge'), + 'int_fields': frozenset({'yaoqing_dashou_shu', 'chongzhi_dashou_shu'}), + }, + 'zuzhang': { + 'model': ZuzhangRiTongji, + 'sort_field': 'shouru_zonge', + 'fields': ('yaoqing_guanshi_shu', 'fenyong_jine', 'shouru_zonge'), + 'int_fields': frozenset({'yaoqing_guanshi_shu'}), + }, + 'shangjia': { + 'model': ShangjiaRiTongji, + 'sort_field': 'jiesuan_jine', + 'fields': ('paifa_dingdan_shu', 'paifa_jine', 'jiesuan_dingdan_shu', 'jiesuan_jine'), + 'int_fields': frozenset({'paifa_dingdan_shu', 'jiesuan_dingdan_shu'}), + }, +} + + +def _resolve_date_range(riqi: str): + """根据前端 riqi 参数返回 (start_date, end_date) 闭区间。""" + today = date.today() + if riqi == '今日': + return today, today + if riqi == '昨日': + d = today - timedelta(days=1) + return d, d + if riqi == '本周': + monday = today - timedelta(days=today.weekday()) + return monday, today + if riqi == '上周': + this_monday = today - timedelta(days=today.weekday()) + last_sunday = this_monday - timedelta(days=1) + last_monday = last_sunday - timedelta(days=6) + return last_monday, last_sunday + if riqi == '本月': + return today.replace(day=1), today + if riqi == '上月': + first_this_month = today.replace(day=1) + last_day_prev = first_this_month - timedelta(days=1) + return last_day_prev.replace(day=1), last_day_prev + return None + + +def _fmt_value(val, is_int: bool): + if val is None: + return 0 if is_int else 0.0 + if is_int: + return int(val) + if isinstance(val, Decimal): + return float(val.quantize(Decimal('0.01'))) + return float(val) + + +class PhbHqsjView(APIView): + """ + 排行榜数据接口 + POST /yonghu/phbhqsj + + 请求参数: + shenfen: dashou | guanshi | zuzhang | shangjia + riqi: 今日 | 本周 | 本月 | 总榜 | 昨日 | 上周 | 上月 + + 响应 data.list[](最多50条,已按各身份排序字段降序): + mingci, yonghuid, nicheng, touxiang + 各身份统计字段 + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + shenfen = (request.data.get('shenfen') or '').strip() + riqi = (request.data.get('riqi') or '').strip() + + if shenfen not in SHENFEN_OPTIONS: + return Response({ + 'code': 400, + 'msg': '参数错误:shenfen 必须为 dashou/guanshi/zuzhang/shangjia', + 'data': {'list': []}, + }) + if riqi not in RIQI_OPTIONS: + return Response({ + 'code': 400, + 'msg': '参数错误:riqi 必须为 今日/本周/本月/总榜/昨日/上周/上月', + 'data': {'list': []}, + }) + + try: + if riqi == '总榜': + rows = self._query_rank_rows(shenfen, all_time=True) + else: + date_range = _resolve_date_range(riqi) + if date_range is None: + return Response({ + 'code': 400, + 'msg': '参数错误:无效的日期范围', + 'data': {'list': []}, + }) + rows = self._query_rank_rows(shenfen, date_range[0], date_range[1]) + nick_map, avatar_map = self._load_user_display(rows, shenfen) + cfg = ROLE_CONFIG[shenfen] + int_fields = cfg['int_fields'] + + result_list = [] + for idx, row in enumerate(rows): + yonghuid = row['yonghuid'] + item = { + 'mingci': idx + 1, + 'yonghuid': yonghuid, + 'nicheng': nick_map.get(yonghuid, '用户'), + 'touxiang': avatar_map.get(yonghuid, '') or '', + } + for field in cfg['fields']: + item[field] = _fmt_value(row.get(field), field in int_fields) + result_list.append(item) + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': {'list': result_list}, + }) + except Exception as e: + logger.error('排行榜查询失败 shenfen=%s riqi=%s: %s', shenfen, riqi, e, exc_info=True) + return Response({ + 'code': 500, + 'msg': '服务器内部错误', + 'data': {'list': []}, + }) + + def _query_rank_rows(self, shenfen, start_date=None, end_date=None, all_time=False): + cfg = ROLE_CONFIG[shenfen] + model = cfg['model'] + sort_field = cfg['sort_field'] + fields = cfg['fields'] + annotate_kwargs = {f: Sum(f) for f in fields} + + if all_time: + rows = ( + model.objects + .values('yonghuid') + .annotate(**annotate_kwargs) + .order_by(f'-{sort_field}')[:MAX_RANK] + ) + return list(rows) + + if start_date == end_date: + qs = model.objects.filter(riqi=start_date).order_by(f'-{sort_field}')[:MAX_RANK] + return [ + {'yonghuid': r.yonghuid, **{f: getattr(r, f) for f in fields}} + for r in qs + ] + + rows = ( + model.objects + .filter(riqi__gte=start_date, riqi__lte=end_date) + .values('yonghuid') + .annotate(**annotate_kwargs) + .order_by(f'-{sort_field}')[:MAX_RANK] + ) + return list(rows) + + def _load_user_display(self, rows, shenfen): + yonghuids = [r['yonghuid'] for r in rows if r.get('yonghuid')] + if not yonghuids: + return {}, {} + + users = UserMain.objects.filter(yonghuid__in=yonghuids) + avatar_map = {u.yonghuid: u.avatar or '' for u in users} + + nick_map = {} + if shenfen == 'dashou': + for p in UserDashou.objects.filter(user__yonghuid__in=yonghuids).select_related('user'): + nick_map[p.user.yonghuid] = p.nicheng or '用户' + elif shenfen == 'shangjia': + for p in UserShangjia.objects.filter(user__yonghuid__in=yonghuids).select_related('user'): + nick_map[p.user.yonghuid] = p.nicheng or '用户' + else: + for b in UserBoss.objects.filter(user__yonghuid__in=yonghuids).select_related('user'): + nick_map[b.user.yonghuid] = b.nickname or '用户' + + return nick_map, avatar_map diff --git a/yonghu/ranking_tasks.py b/yonghu/ranking_tasks.py new file mode 100644 index 0000000..4614f9f --- /dev/null +++ b/yonghu/ranking_tasks.py @@ -0,0 +1,280 @@ +# yonghu/ranking_tasks.py +""" +排行榜数据转移任务 - 生产环境专用 +在清空前将数据转移到排行榜表 +""" + +from celery import shared_task +from django.db import transaction +from django.utils import timezone +from datetime import datetime, timedelta +import logging +from decimal import Decimal + +from yonghu.models import ( + UserMain, UserDashou, UserShangjia, UserGuanshi, UserBoss, + RankingRecord +) + +logger = logging.getLogger(__name__) + +def get_user_nicheng(user, shenfen): + """ + 获取用户昵称 + 规则:打手用打手nicheng,其他用老板nickname + """ + try: + if shenfen == 2: # 打手 + dashou_profile = getattr(user, 'dashou_profile', None) + if dashou_profile and dashou_profile.nicheng: + return dashou_profile.nicheng + else: # 管事或商家 + boss_profile = getattr(user, 'boss_profile', None) + if boss_profile and boss_profile.nickname: + return boss_profile.nickname + + # 默认返回用户ID + return f"用户{user.yonghuid}" + except Exception: + return f"用户{user.yonghuid}" + +@shared_task +def zhuanyi_ribang(): + """ + 转移日榜数据 - 每天23:55执行 + 在清零前将今日数据转移到排行榜表 + """ + try: + # 统计日期(今天) + today = timezone.now().date() + logger.info(f"开始转移日榜数据,日期: {today}") + + records_to_create = [] + + # 1. 转移打手日榜(今日收益 > 0) + dashou_list = UserDashou.objects.filter( + jinrishouyi__gt=0 # 只转移收益大于0的 + ).select_related('user').order_by('-jinrishouyi') + + for i, dashou in enumerate(dashou_list, start=1): + if i > 100: # 最多100条 + break + + user = dashou.user + record = RankingRecord( + zhouqi=1, # 日榜 + tongji_date=today, + shenfen=2, # 打手 + yonghuid=user.yonghuid, + paiming=i, + zhizhi=dashou.jinrishouyi, + nicheng=get_user_nicheng(user, 2), + avatar=user.avatar or '' + ) + records_to_create.append(record) + + # 2. 转移商家日榜(今日流水 > 0) + shangjia_list = UserShangjia.objects.filter( + jinriliushui__gt=0 # 只转移流水大于0的 + ).select_related('user').order_by('-jinriliushui') + + for i, shangjia in enumerate(shangjia_list, start=1): + if i > 100: # 最多100条 + break + + user = shangjia.user + record = RankingRecord( + zhouqi=1, + tongji_date=today, + shenfen=4, # 商家 + yonghuid=user.yonghuid, + paiming=i, + zhizhi=shangjia.jinriliushui, + nicheng=get_user_nicheng(user, 4), + avatar=user.avatar or '' + ) + records_to_create.append(record) + + # 3. 转移管事日榜(今日充值人数 > 0) + guanshi_list = UserGuanshi.objects.filter( + jinrichongzhi__gt=0 # 只转移充值人数大于0的 + ).select_related('user').order_by('-jinrichongzhi') + + for i, guanshi in enumerate(guanshi_list, start=1): + if i > 100: # 最多100条 + break + + user = guanshi.user + record = RankingRecord( + zhouqi=1, + tongji_date=today, + shenfen=3, # 管事 + yonghuid=user.yonghuid, + paiming=i, + zhizhi=Decimal(guanshi.jinrichongzhi), + nicheng=get_user_nicheng(user, 3), + avatar=user.avatar or '' + ) + records_to_create.append(record) + + # 批量保存 + if records_to_create: + with transaction.atomic(): + RankingRecord.objects.bulk_create(records_to_create) + logger.info(f"日榜数据转移完成,保存{len(records_to_create)}条记录") + + result = { + 'success': True, + 'date': today.strftime('%Y-%m-%d'), + 'total_count': len(records_to_create), + } + + return result + + except Exception as e: + logger.error(f"日榜数据转移失败: {str(e)}", exc_info=True) + raise + +@shared_task +def zhuanyi_yuebang(): + """ + 转移月榜数据 - 每月最后一天23:50执行 + 在清零前将本月数据转移到排行榜表 + """ + try: + # 🔥 获取当前时间 + now = timezone.now() + today = now.date() + + # 🔥 判断今天是否是当月的最后一天 + # 方法:计算明天的日期,如果明天的月份和今天不同,那么今天就是最后一天 + tomorrow = today + timedelta(days=1) + if tomorrow.month == today.month: + # 今天不是最后一天,直接返回 + logger.info(f"今天不是当月最后一天({today}),跳过月榜转移") + return { + 'success': True, + 'message': f'今天不是当月最后一天,跳过执行', + 'is_last_day': False, + 'today': today.strftime('%Y-%m-%d'), + } + + logger.info(f"今天是当月最后一天({today}),开始转移月榜数据") + + # 🔥 统计日期为当月第一天(如2024-01-01) + tongji_date = datetime(today.year, today.month, 1).date() + + records_to_create = [] + + # 1. 转移打手月榜(今月收益 > 0) + dashou_list = UserDashou.objects.filter( + jinyueshouyi__gt=0 # 只转移收益大于0的 + ).select_related('user').order_by('-jinyueshouyi') + + for i, dashou in enumerate(dashou_list, start=1): + if i > 100: # 最多100条 + break + + user = dashou.user + record = RankingRecord( + zhouqi=2, # 月榜 + tongji_date=tongji_date, + shenfen=2, # 打手 + yonghuid=user.yonghuid, + paiming=i, + zhizhi=dashou.jinyueshouyi, + nicheng=get_user_nicheng(user, 2), + avatar=user.avatar or '' + ) + records_to_create.append(record) + + # 2. 转移商家月榜(今月流水 > 0) + shangjia_list = UserShangjia.objects.filter( + jinyueliushui__gt=0 # 只转移流水大于0的 + ).select_related('user').order_by('-jinyueliushui') + + for i, shangjia in enumerate(shangjia_list, start=1): + if i > 100: # 最多100条 + break + + user = shangjia.user + record = RankingRecord( + zhouqi=2, + tongji_date=tongji_date, + shenfen=4, # 商家 + yonghuid=user.yonghuid, + paiming=i, + zhizhi=shangjia.jinyueliushui, + nicheng=get_user_nicheng(user, 4), + avatar=user.avatar or '' + ) + records_to_create.append(record) + + # 3. 转移管事月榜(今月充值人数 > 0) + guanshi_list = UserGuanshi.objects.filter( + jinyuechongzhi__gt=0 # 只转移充值人数大于0的 + ).select_related('user').order_by('-jinyuechongzhi') + + for i, guanshi in enumerate(guanshi_list, start=1): + if i > 100: # 最多100条 + break + + user = guanshi.user + record = RankingRecord( + zhouqi=2, + tongji_date=tongji_date, + shenfen=3, # 管事 + yonghuid=user.yonghuid, + paiming=i, + zhizhi=Decimal(guanshi.jinyuechongzhi), + nicheng=get_user_nicheng(user, 3), + avatar=user.avatar or '' + ) + records_to_create.append(record) + + # 批量保存 + if records_to_create: + with transaction.atomic(): + RankingRecord.objects.bulk_create(records_to_create) + logger.info(f"月榜数据转移完成,保存{len(records_to_create)}条记录") + + result = { + 'success': True, + 'month': tongji_date.strftime('%Y-%m'), + 'total_count': len(records_to_create), + 'is_last_day': True, + 'today': today.strftime('%Y-%m-%d'), + } + + return result + + except Exception as e: + logger.error(f"月榜数据转移失败: {str(e)}", exc_info=True) + raise + +@shared_task +def qingli_jiulishuju(): + """ + 清理旧历史数据 - 每月1号凌晨1点执行 + 清理3个月前的排行榜数据,保留最近3个月的数据 + """ + try: + # 计算3个月前的日期 + three_months_ago = timezone.now().date() - timedelta(days=90) + + # 删除旧数据 + deleted_count, _ = RankingRecord.objects.filter( + tongji_date__lt=three_months_ago + ).delete() + + logger.info(f"清理旧历史数据完成,删除{deleted_count}条记录") + + return { + 'success': True, + 'deleted_count': deleted_count, + 'before_date': three_months_ago.strftime('%Y-%m-%d') + } + + except Exception as e: + logger.error(f"清理旧历史数据失败: {str(e)}") + raise \ No newline at end of file diff --git a/yonghu/tasks.py b/yonghu/tasks.py new file mode 100644 index 0000000..ce07131 --- /dev/null +++ b/yonghu/tasks.py @@ -0,0 +1,176 @@ +""" +阿龙电竞 - 用户定时任务 +处理每日/每月清零等用户相关定时任务 +完整版本,包含所有异常处理 +""" + +from celery import shared_task +from django.db import transaction +from django.db.models import F, Q +from django.utils import timezone +from datetime import datetime +import logging + +# 导入模型 +from yonghu.models import UserDashou, UserShangjia, UserGuanshi +from utils.celery_utils import log_task_execution, rollback_on_failure + +logger = logging.getLogger(__name__) + +@shared_task +@rollback_on_failure("每日清零任务") +def daily_reset_task(): + """ + 每日清零任务 - 每天凌晨0点执行 + 清零打手、商家、管事的今日相关字段 + """ + try: + with transaction.atomic(): + start_time = timezone.now() + today = datetime.now().strftime('%Y-%m-%d') + + logger.info(f"开始执行每日清零任务,日期: {today}") + + # 1. 清零打手今日数据 + dashou_updated = UserDashou.objects.filter( + Q(jinrijiedan__gt=0) | Q(jinrishouyi__gt=0) + ).update( + jinrijiedan=0, + jinrishouyi=0 + ) + + logger.info(f"打手今日数据清零完成,影响记录: {dashou_updated}条") + + # 2. 清零商家今日数据 + shangjia_updated = UserShangjia.objects.filter( + Q(jinridingdan__gt=0) | Q(jinriliushui__gt=0) + ).update( + jinridingdan=0, + jinriliushui=0 + ) + + logger.info(f"商家今日数据清零完成,影响记录: {shangjia_updated}条") + + # 3. 清零管事今日数据 + guanshi_updated = UserGuanshi.objects.filter( + jinrichongzhi__gt=0 + ).update( + jinrichongzhi=0 + ) + + logger.info(f"管事今日数据清零完成,影响记录: {guanshi_updated}条") + + end_time = timezone.now() + duration = (end_time - start_time).total_seconds() + + # 记录执行结果 + result_msg = ( + f"每日清零完成 - 日期: {today}, " + f"打手: {dashou_updated}条, " + f"商家: {shangjia_updated}条, " + f"管事: {guanshi_updated}条, " + f"耗时: {duration:.2f}秒" + ) + + log_task_execution("每日清零任务", True, result_msg) + + return { + "success": True, + "date": today, + "updated_dashou": dashou_updated, + "updated_shangjia": shangjia_updated, + "updated_guanshi": guanshi_updated, + "duration_seconds": duration, + "message": result_msg + } + + except Exception as e: + error_msg = f"每日清零任务失败: {str(e)}" + logger.error(error_msg) + log_task_execution("每日清零任务", False, error_msg) + + return { + "success": False, + "error": str(e), + "date": datetime.now().strftime('%Y-%m-%d'), + "message": error_msg + } + +@shared_task +@rollback_on_failure("每月清零任务") +def monthly_reset_task(): + """ + 每月清零任务 - 每月1日凌晨0点执行 + 清零打手、商家、管事的本月相关字段 + """ + try: + with transaction.atomic(): + start_time = timezone.now() + current_month = datetime.now().strftime('%Y-%m') + + logger.info(f"开始执行每月清零任务,月份: {current_month}") + + # 1. 清零打手本月数据 + dashou_updated = UserDashou.objects.filter( + Q(jinyuejiedan__gt=0) | Q(jinyueshouyi__gt=0) + ).update( + jinyuejiedan=0, + jinyueshouyi=0 + ) + + logger.info(f"打手本月数据清零完成,影响记录: {dashou_updated}条") + + # 2. 清零商家本月数据 + shangjia_updated = UserShangjia.objects.filter( + Q(jinyuedingdan__gt=0) | Q(jinyueliushui__gt=0) + ).update( + jinyuedingdan=0, + jinyueliushui=0 + ) + + logger.info(f"商家本月数据清零完成,影响记录: {shangjia_updated}条") + + # 3. 清零管事本月数据 + guanshi_updated = UserGuanshi.objects.filter( + jinyuechongzhi__gt=0 + ).update( + jinyuechongzhi=0 + ) + + logger.info(f"管事本月数据清零完成,影响记录: {guanshi_updated}条") + + end_time = timezone.now() + duration = (end_time - start_time).total_seconds() + + # 记录执行结果 + result_msg = ( + f"每月清零完成 - 月份: {current_month}, " + f"打手: {dashou_updated}条, " + f"商家: {shangjia_updated}条, " + f"管事: {guanshi_updated}条, " + f"耗时: {duration:.2f}秒" + ) + + log_task_execution("每月清零任务", True, result_msg) + + return { + "success": True, + "month": current_month, + "updated_dashou": dashou_updated, + "updated_shangjia": shangjia_updated, + "updated_guanshi": guanshi_updated, + "duration_seconds": duration, + "message": result_msg + } + + except Exception as e: + error_msg = f"每月清零任务失败: {str(e)}" + logger.error(error_msg) + log_task_execution("每月清零任务", False, error_msg) + + return { + "success": False, + "error": str(e), + "month": datetime.now().strftime('%Y-%m'), + "message": error_msg + } \ No newline at end of file diff --git a/yonghu/tests.py b/yonghu/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/yonghu/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/yonghu/tixian_shenhe_services.py b/yonghu/tixian_shenhe_services.py new file mode 100644 index 0000000..1b0e5cd --- /dev/null +++ b/yonghu/tixian_shenhe_services.py @@ -0,0 +1,1358 @@ +# yonghu/tixian_shenhe_services.py +""" +自动提现审核流程业务服务层(仅服务于 zddksh 申请接口 与 tixiansq 收款接口) + +流程: + ① zddksh 提交审核:资格校验 → 扣款(shenqing_jine) → 写审核表+提现记录表(状态=1) + (不在申请阶段校验/占用每日限额) + ② 后台审核通过:状态变为 6(待收款)—— 由原有后台接口处理,此处不改动 + ③ tixiansq 收款:资格校验 → 只读预估 → 事务内原子预占限额 → 新建打款记录 → 调微信 + 微信明确失败/取消则回滚预占;待查单期间保留预占防并发超额 + ④ 打款成功(tixianqr/回调/查单):若收款前已预占则不再累加;未预占的旧单/查单直成则补记 + +状态码(与前端 TX_STATUS 一致): + 1=审核中 2=提现成功 3=提现失败 4=审核成功 5=审核失败 6=待收款 +""" +import decimal +import json +import logging +import random +import time +from datetime import date, timedelta + +import requests +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 utils.fadan_utils import check_fadan_qiangdan_eligible + +from .models import ( + TixianAutoRecord, + TixianShenheJilu, + Tixianjilu, + UserDashou, + UserGuanshi, + UserMain, + UserShenheguan, + UserShangjia, + UserZuzhang, +) + +logger = logging.getLogger('yonghu.tixian_shenhe') + +ORDER_STATUS_COMPLETED = 3 +ORDER_STATUS_REFUNDED = 5 + +LEIXING_RATE_KEY = { + 1: '5', + 2: '6', + 3: '8', + 4: '9', + 5: '11', + 6: '10', +} + +# 进行中的审核状态(不允许重复提交申请) +PENDING_AUDIT_STATUSES = [1, 4, 6] + +DAKUAN_MODE_MANUAL = 1 +DAKUAN_MODE_AUTO = 2 + +# 微信转账单状态(查单接口返回 state 字段) +WX_STATE_SUCCESS = ('SUCCESS',) +WX_STATE_WAIT = ('WAIT_USER_CONFIRM', 'TRANSFERING', 'ACCEPTED', 'PROCESSING') +WX_STATE_FAIL = ('FAIL', 'CLOSED', 'CANCELLED') +WX_STATE_CANCELING = ('CANCELING',) # 撤销中,非终态,禁止换新单号 + +RECONCILE_COMPLETED = 'completed' +RECONCILE_WAIT_CONFIRM = 'wait_confirm' +RECONCILE_ALLOW_NEW = 'allow_new' +RECONCILE_PENDING = 'pending_query' + +# 查单失败且本地 zhuangtai=0 超过该分钟数,才允许新建打款记录 +PENDING_QUERY_GRACE_MINUTES = 10 + + +def has_dakuan_mode_field(): + """生产库未跑 0032 迁移时模型无此字段,需兼容""" + try: + TixianShenheJilu._meta.get_field('dakuan_mode') + return True + except Exception: + return False + + +def is_auto_dakuan_audit(audit): + """判断是否自动打款审核单;无 dakuan_mode 属性时,有审核记录即视为 zddksh 自动流程""" + if audit is None: + return False + mode = getattr(audit, 'dakuan_mode', None) + if mode is None: + return True + try: + return int(mode) == DAKUAN_MODE_AUTO + except (TypeError, ValueError): + return True + + +def load_audit_dakuan_mode_map(audit_ids): + """批量取审核单打款方式;字段不存在时有关联审核即视为自动(2)""" + return { + aid: meta.get('dakuan_mode', DAKUAN_MODE_AUTO) + for aid, meta in load_audit_meta_map(audit_ids).items() + } + + +def load_audit_meta_map(audit_ids): + """ + 批量取审核表元数据(供后台列表等) + 返回 {审核记录id: {'dakuan_mode': int, 'shenhe_danhao': str}} + """ + if not audit_ids: + return {} + value_fields = ['id', 'shenhe_danhao'] + if has_dakuan_mode_field(): + value_fields.append('dakuan_mode') + result = {} + for row in TixianShenheJilu.objects.filter(id__in=audit_ids).values(*value_fields): + result[row['id']] = { + 'shenhe_danhao': row.get('shenhe_danhao') or '', + 'dakuan_mode': row.get('dakuan_mode', DAKUAN_MODE_AUTO), + } + return result + + +def generate_shenhe_danhao(): + """生成唯一审核单号:TXSH + 时间戳后10位 + 4位随机数""" + for _ in range(10): + timestamp = str(int(time.time() * 1000))[-10:] + random_part = str(random.randint(1000, 9999)) + danhao = f'TXSH{timestamp}{random_part}' + if not TixianShenheJilu.objects.filter(shenhe_danhao=danhao).exists(): + return danhao + raise RuntimeError('生成审核单号失败,请重试') + + +def get_tixian_feilv(leixing): + rate_key = LEIXING_RATE_KEY.get(leixing) + if not rate_key: + raise ValueError('无效的提现类型') + rate_obj = Lilubiao.objects.filter(fadanpingtai=rate_key).first() + if not rate_obj: + raise ValueError(f'提现费率未配置(类型{leixing})') + return decimal.Decimal(str(rate_obj.lilu)) + + +def calc_fee_amounts(jine, feilv): + """申请金额 jine;实际到账 = jine - 手续费""" + shouxufei = (jine * feilv).quantize(decimal.Decimal('0.01')) + shijidaozhang = (jine - shouxufei).quantize(decimal.Decimal('0.01')) + return shouxufei, shijidaozhang + + +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(): + return False, '您暂无会员记录,无法提现' + + for record in records: + is_expired = record.jiance_shifou_daoqi() + if not is_expired and record.huiyuan_zhuangtai == 1: + return True, '' + return False, '您的会员已全部过期,请先续费后再操作' + + +def validate_withdraw_eligibility(user_main, leixing, jine): + """ + 提现资格校验(对齐手动提现 TixianShenqingView) + """ + yonghuid = user_main.yonghuid + + fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2) + if not fadan_ok: + return False, fadan_msg, {} + + if leixing == 1: + try: + dashou = UserDashou.objects.get(user=user_main) + except UserDashou.DoesNotExist: + return False, '您不是打手身份,无法提现佣金', {} + if dashou.zhanghaozhuangtai != 1: + return False, '打手账号已被封禁,无法提现', {} + if dashou.zhuangtai != 1: + return False, '有订单进行中,请完成后提现', {} + # 与手动提现一致:积分必须正好 10 分 + if dashou.jifen != 10: + return False, '积分不足10分,请补充积分后提现', {} + huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid) + if not huiyuan_ok: + return False, huiyuan_msg, {} + if dashou.yue < jine: + return False, f'余额不足,当前余额: {dashou.yue}', {} + return True, '', {'nicheng': dashou.nicheng or ''} + + if leixing == 2: + try: + guanshi = UserGuanshi.objects.get(user=user_main) + except UserGuanshi.DoesNotExist: + return False, '您不是管事身份,无法提现分红', {} + if guanshi.zhuangtai != 1: + return False, '管事账号已被封禁,无法提现', {} + if guanshi.yue < jine: + return False, f'余额不足,当前余额: {guanshi.yue}', {} + return True, '', {'nicheng': '管事用户'} + + if leixing == 3: + try: + zuzhang = UserZuzhang.objects.get(user=user_main) + except UserZuzhang.DoesNotExist: + return False, '您不是组长身份,无法提现组长分红', {} + if zuzhang.zhuangtai != 1: + return False, '组长账号已被封禁,无法提现', {} + if zuzhang.ketixian_jine < jine: + return False, f'可提现金额不足,当前可提: {zuzhang.ketixian_jine}', {} + return True, '', {'nicheng': ''} + + if leixing == 4: + try: + kaoheguan = UserShenheguan.objects.get(user=user_main) + except UserShenheguan.DoesNotExist: + return False, '您不是考核官身份,无法提现分佣', {} + if kaoheguan.zhuangtai != 1: + return False, '考核官账号已被封禁,无法提现', {} + if kaoheguan.yue < jine: + return False, f'考核官可提现金额不足,当前余额: {kaoheguan.yue}', {} + return True, '', {'nicheng': '考核官'} + + if leixing == 5: + try: + dashou = UserDashou.objects.get(user=user_main) + except UserDashou.DoesNotExist: + return False, '您不是打手身份,无法提取押金', {} + if dashou.zhanghaozhuangtai != 1: + return False, '打手账号已禁用,无法提取押金', {} + if dashou.zhuangtai != 1: + return False, '您有订单进行中,请完成后提取押金', {} + if dashou.jifen != 10: + return False, '积分不足10分,请补充积分后提取押金', {} + huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid) + if not huiyuan_ok: + return False, huiyuan_msg, {} + if dashou.yajin < jine: + return False, f'押金余额不足,当前押金: {dashou.yajin}', {} + unfinished = Dingdan.objects.filter( + jiedan_dashou_id=yonghuid, + ).exclude(zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() + if unfinished: + return False, '您还有未完成或未退款的订单,请先处理完毕再提取押金', {} + self_fadan = Fadan.objects.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + if self_fadan: + return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {} + return True, '', {'nicheng': dashou.nicheng or ''} + + if leixing == 6: + try: + shangjia = UserShangjia.objects.get(user=user_main) + except UserShangjia.DoesNotExist: + return False, '您不是商家身份,无法提现商家余额', {} + if shangjia.zhuangtai != 1: + return False, '商家账号已被禁用,无法提现', {} + if shangjia.yue < jine: + return False, f'商家余额不足,当前余额: {shangjia.yue}', {} + unfinished_orders = DingdanShangjia.objects.filter( + shangjia_id=yonghuid, + ).exclude(dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() + if unfinished_orders: + return False, '您还有未完成或未退款的订单,请先处理完毕再提现余额', {} + self_fadan = Fadan.objects.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + if self_fadan: + return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {} + applied_fadan = Fadan.objects.filter(shenqing_chufa=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + if applied_fadan: + return False, '您发起的对其他用户的处罚尚未处理完毕,请等待完结', {} + return True, '', {'nicheng': shangjia.nicheng or ''} + + return False, '无效的提现类型', {} + + +def validate_collect_eligibility(user_main, leixing): + """ + 收款前二次校验(申请时已扣款,不再校余额/金额)。 + 打手佣金/押金:罚单、封禁、接单中、积分、会员等; + 其他身份:账号状态、罚单/订单等(不含金额)。 + """ + yonghuid = user_main.yonghuid + + fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2) + if not fadan_ok: + return False, fadan_msg + + if leixing == 1: + try: + dashou = UserDashou.objects.get(user=user_main) + except UserDashou.DoesNotExist: + return False, '您不是打手身份,无法收款' + if dashou.zhanghaozhuangtai != 1: + return False, '打手账号已被封禁,无法收款' + if dashou.zhuangtai != 1: + return False, '您有订单进行中,请完成后再收款' + if dashou.jifen != 10: + return False, '积分不足10分,无法收款' + huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid) + if not huiyuan_ok: + return False, huiyuan_msg + return True, '' + + if leixing == 5: + try: + dashou = UserDashou.objects.get(user=user_main) + except UserDashou.DoesNotExist: + return False, '您不是打手身份,无法收款' + if dashou.zhanghaozhuangtai != 1: + return False, '打手账号已禁用,无法收款' + if dashou.zhuangtai != 1: + return False, '您有订单进行中,请完成后再收款' + if dashou.jifen != 10: + return False, '积分不足10分,无法收款' + huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid) + if not huiyuan_ok: + return False, huiyuan_msg + unfinished = Dingdan.objects.filter( + jiedan_dashou_id=yonghuid, + ).exclude(zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() + if unfinished: + return False, '您还有未完成或未退款的订单,无法收款' + self_fadan = Fadan.objects.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + if self_fadan: + return False, '您有未处理完毕的自身罚单,无法收款' + return True, '' + + if leixing == 2: + try: + guanshi = UserGuanshi.objects.get(user=user_main) + except UserGuanshi.DoesNotExist: + return False, '您不是管事身份,无法收款' + if guanshi.zhuangtai != 1: + return False, '管事账号已被封禁,无法收款' + return True, '' + + if leixing == 3: + try: + zuzhang = UserZuzhang.objects.get(user=user_main) + except UserZuzhang.DoesNotExist: + return False, '您不是组长身份,无法收款' + if zuzhang.zhuangtai != 1: + return False, '组长账号已被封禁,无法收款' + return True, '' + + if leixing == 4: + try: + kaoheguan = UserShenheguan.objects.get(user=user_main) + except UserShenheguan.DoesNotExist: + return False, '您不是考核官身份,无法收款' + if kaoheguan.zhuangtai != 1: + return False, '考核官账号已被封禁,无法收款' + return True, '' + + if leixing == 6: + try: + shangjia = UserShangjia.objects.get(user=user_main) + except UserShangjia.DoesNotExist: + return False, '您不是商家身份,无法收款' + if shangjia.zhuangtai != 1: + return False, '商家账号已被禁用,无法收款' + unfinished_orders = DingdanShangjia.objects.filter( + shangjia_id=yonghuid, + ).exclude(dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() + if unfinished_orders: + return False, '您还有未完成或未退款的订单,无法收款' + self_fadan = Fadan.objects.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + if self_fadan: + return False, '您有未处理完毕的自身罚单,无法收款' + applied_fadan = Fadan.objects.filter(shenqing_chufa=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + if applied_fadan: + return False, '您发起的处罚尚未处理完毕,无法收款' + return True, '' + + return False, '无效的提现类型' + + +def _get_personal_daily_quota(leixing, profile): + """打手/管事/组长个人每日限额:额外开启优先,否则默认配置""" + if leixing == 1: + if profile.kaioi_ewai_xiane and profile.ewai_xiane > 0: + return profile.ewai_xiane + return TixianQuotaDefault.objects.get(leixing=1).default_quota + if leixing == 2: + if profile.kaioi_ewai_xiane and profile.ewai_xiane > 0: + return profile.ewai_xiane + return TixianQuotaDefault.objects.get(leixing=2).default_quota + if leixing == 3: + try: + default_quota = TixianQuotaDefault.objects.get(leixing=3).default_quota + except TixianQuotaDefault.DoesNotExist: + default_quota = decimal.Decimal('0.00') + if profile.kaioi_ewai_tixian and profile.ewai_tixian_xiane > 0: + return profile.ewai_tixian_xiane + return default_quota + raise ValueError('无效的个人限额身份') + + +def _get_active_auto_record(shenhe_danhao): + if not shenhe_danhao: + return None + return ( + TixianAutoRecord.objects.filter( + shenhe_danhao=shenhe_danhao, + zhuangtai__in=[0, 1], + ) + .order_by('-create_time') + .first() + ) + + +def _get_platform_daily_total(leixing, today=None): + """平台今日已提现总额:只读 houtai.TixianRiTongji(到账金额口径)""" + from houtai.models import TixianRiTongji + + if today is None: + today = date.today() + try: + return TixianRiTongji.objects.get(riqi=today, leixing=leixing).total_amount + except TixianRiTongji.DoesNotExist: + return decimal.Decimal('0.00') + + +def _calc_platform_projected(leixing, shijidaozhang, shenhe_danhao=''): + """ + 平台今日预占后总额(收款前只读预估)。 + 本审核单若已有进行中打款记录,其金额尚未写入统计表,但不应重复加计。 + 返回 (daily_used, projected_total, additional_amount) + """ + daily_used = _get_platform_daily_total(leixing) + active_rec = _get_active_auto_record(shenhe_danhao) + if active_rec: + return daily_used, daily_used, decimal.Decimal('0.00') + additional = shijidaozhang or decimal.Decimal('0.00') + return daily_used, daily_used + additional, additional + + +def _projected_personal_total(personal_today, jine, shenhe_danhao=''): + """个人今日占用预估(申请金额口径)""" + rec = _get_active_auto_record(shenhe_danhao) + already_counted = rec.jine if rec else decimal.Decimal('0.00') + return personal_today - already_counted + jine + + +def _get_platform_daily_limit(leixing): + """ + 平台每日总限额(peizhi.TixianQuotaDefault): + 提现身份1打手→配置4,2管事→5,3组长→6,4考核官→7,5押金→8,6商家→9 + """ + quota_leixing = leixing + 3 + try: + return TixianQuotaDefault.objects.get(leixing=quota_leixing).default_quota + except TixianQuotaDefault.DoesNotExist: + logger.warning( + '平台每日总限额未配置 leixing=%s(读取quota_leixing=%s),按0处理禁止新增收款', + leixing, quota_leixing, + ) + return decimal.Decimal('0.00') + + +def _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao=''): + """个人每日限额只读校验(打手/管事/组长)""" + if leixing not in (1, 2, 3): + return True, '', '' + + today = date.today() + if leixing == 1: + try: + profile = UserDashou.objects.get(user=user_main) + except UserDashou.DoesNotExist: + return False, '打手信息不存在', 'personal' + if profile.last_tixian_date != today: + personal_today = decimal.Decimal('0.00') + else: + personal_today = profile.jinritixian_jine + elif leixing == 2: + try: + profile = UserGuanshi.objects.get(user=user_main) + except UserGuanshi.DoesNotExist: + return False, '管事信息不存在', 'personal' + if profile.last_tixian_date != today: + personal_today = decimal.Decimal('0.00') + else: + personal_today = profile.jinritixian_jine + else: + try: + profile = UserZuzhang.objects.get(user=user_main) + except UserZuzhang.DoesNotExist: + return False, '组长信息不存在', 'personal' + if profile.last_tixian_time and profile.last_tixian_time.date() == today: + personal_today = profile.jinri_tixian + else: + personal_today = decimal.Decimal('0.00') + + personal_quota = _get_personal_daily_quota(leixing, profile) + projected_personal = _projected_personal_total(personal_today, jine, shenhe_danhao) + if projected_personal > personal_quota: + return ( + False, + f'今日提现已达个人限额,今日已提{personal_today}元,本次申请{jine}元,限额{personal_quota}元', + 'personal', + ) + return True, '', '' + + +def _check_platform_quota_readonly(leixing, shijidaozhang, shenhe_danhao=''): + """平台每日总限额只读校验:今日已用 + 本笔到账 > 总限额 则拒绝""" + platform_limit = _get_platform_daily_limit(leixing) + shijidaozhang = shijidaozhang or decimal.Decimal('0.00') + + if platform_limit <= 0 and shijidaozhang > 0: + return False, '今日该角色提现已关闭(总限额为0),暂无法收款', 'platform' + + daily_used, projected, additional = _calc_platform_projected( + leixing, shijidaozhang, shenhe_danhao, + ) + if projected > platform_limit: + logger.warning( + '平台总限额拦截 leixing=%s shenhe_danhao=%s daily_used=%s additional=%s ' + 'projected=%s limit=%s', + leixing, shenhe_danhao, daily_used, additional, projected, platform_limit, + ) + return ( + False, + f'今日该角色提现总额已达上限,今日已提{daily_used}元,本次到账{additional}元,限额{platform_limit}元', + 'platform', + ) + + return True, '', '' + + +def check_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''): + """ + 收款限额只读校验(复用待确认打款单等场景,不写入)。 + 返回 (True, '', '') 或 (False, msg, 'personal'|'platform') + """ + ok, msg, kind = _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao) + if not ok: + return False, msg, kind + return _check_platform_quota_readonly(leixing, shijidaozhang, shenhe_danhao) + + +def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''): + """ + 收款前原子预占每日限额(必须在 transaction.atomic 内调用)。 + 锁定 TixianRiTongji + 用户行,校验通过后立即写入统计,防止高并发超额。 + 本审核单已有进行中/成功打款记录则视为已预占,不重复加计。 + 返回 (True, '', '') 或 (False, msg, 'personal'|'platform') + """ + from houtai.models import TixianRiTongji + + today = date.today() + shijidaozhang = shijidaozhang or decimal.Decimal('0.00') + + if _get_active_auto_record(shenhe_danhao): + return True, '', '' + + personal_profile = None + if leixing in (1, 2, 3): + if leixing == 1: + personal_profile = UserDashou.objects.select_for_update().get(user=user_main) + if personal_profile.last_tixian_date != today: + personal_profile.jinritixian_jine = decimal.Decimal('0.00') + personal_profile.last_tixian_date = today + personal_today = personal_profile.jinritixian_jine + elif leixing == 2: + personal_profile = UserGuanshi.objects.select_for_update().get(user=user_main) + if personal_profile.last_tixian_date != today: + personal_profile.jinritixian_jine = decimal.Decimal('0.00') + personal_profile.last_tixian_date = today + personal_today = personal_profile.jinritixian_jine + else: + personal_profile = UserZuzhang.objects.select_for_update().get(user=user_main) + if personal_profile.last_tixian_time and personal_profile.last_tixian_time.date() != today: + personal_profile.jinri_tixian = decimal.Decimal('0.00') + elif not personal_profile.last_tixian_time: + personal_profile.jinri_tixian = decimal.Decimal('0.00') + personal_today = personal_profile.jinri_tixian + + personal_quota = _get_personal_daily_quota(leixing, personal_profile) + if personal_today + jine > personal_quota: + return ( + False, + f'今日提现已达个人限额,今日已提{personal_today}元,本次申请{jine}元,限额{personal_quota}元', + 'personal', + ) + + platform_stat, _ = TixianRiTongji.objects.select_for_update().get_or_create( + riqi=today, + leixing=leixing, + defaults={'total_amount': decimal.Decimal('0.00')}, + ) + platform_limit = _get_platform_daily_limit(leixing) + if platform_limit <= 0 and shijidaozhang > 0: + return False, '今日该角色提现已关闭(总限额为0),暂无法收款', 'platform' + if platform_stat.total_amount + shijidaozhang > platform_limit: + return ( + False, + f'今日该角色提现总额已达上限,今日已提{platform_stat.total_amount}元,' + f'本次到账{shijidaozhang}元,限额{platform_limit}元', + 'platform', + ) + + if personal_profile is not None: + if leixing == 1: + personal_profile.jinritixian_jine += jine + personal_profile.save(update_fields=['jinritixian_jine', 'last_tixian_date']) + elif leixing == 2: + personal_profile.jinritixian_jine += jine + personal_profile.save(update_fields=['jinritixian_jine', 'last_tixian_date']) + else: + personal_profile.jinri_tixian += jine + personal_profile.last_tixian_time = timezone.now() + personal_profile.save(update_fields=['jinri_tixian', 'last_tixian_time']) + + platform_stat.total_amount += shijidaozhang + platform_stat.save(update_fields=['total_amount']) + + logger.info( + '收款限额预占成功 yonghuid=%s leixing=%s shenhe_danhao=%s jine=%s shijidaozhang=%s platform_total=%s', + user_main.yonghuid, leixing, shenhe_danhao, jine, shijidaozhang, platform_stat.total_amount, + ) + return True, '', '' + + +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: + with transaction.atomic(): + if leixing == 1: + dashou = UserDashou.objects.select_for_update().get(user=user_main) + if dashou.last_tixian_date == today and dashou.jinritixian_jine >= jine: + dashou.jinritixian_jine -= jine + dashou.save(update_fields=['jinritixian_jine']) + elif leixing == 2: + guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) + if guanshi.last_tixian_date == today and guanshi.jinritixian_jine >= jine: + guanshi.jinritixian_jine -= jine + guanshi.save(update_fields=['jinritixian_jine']) + elif leixing == 3: + zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) + if zuzhang.jinri_tixian >= jine: + zuzhang.jinri_tixian -= jine + zuzhang.save(update_fields=['jinri_tixian']) + + try: + stat = TixianRiTongji.objects.select_for_update().get( + riqi=today, leixing=leixing, + ) + stat.total_amount = max( + stat.total_amount - shijidaozhang, + decimal.Decimal('0.00'), + ) + stat.save(update_fields=['total_amount']) + except TixianRiTongji.DoesNotExist: + pass + except Exception as e: + logger.error( + '回滚收款限额预占失败 yonghuid=%s leixing=%s err=%s', + user_main.yonghuid, leixing, e, exc_info=True, + ) + return + + logger.info( + '收款限额预占已回滚 yonghuid=%s leixing=%s jine=%s shijidaozhang=%s', + user_main.yonghuid, leixing, jine, shijidaozhang, + ) + + +def release_collect_quota_for_record(auto_record): + """幂等回滚单笔打款对应的收款预占(查单/回调/取消等多路径共用,防重复扣减)""" + if not auto_record.shenhe_danhao: + return + if not _quota_already_reserved_for_record(auto_record): + return + try: + user_main = UserMain.objects.get(yonghuid=auto_record.yonghuid) + except UserMain.DoesNotExist: + logger.error('回滚限额预占失败:用户不存在 %s', auto_record.yonghuid) + return + release_collect_quota_reservation( + user_main, + auto_record.leixing, + auto_record.jine, + auto_record.shijidaozhang, + ) + + +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') + stat_total = _get_platform_daily_total(auto_record.leixing, today) + other_sum = TixianAutoRecord.objects.filter( + leixing=auto_record.leixing, + zhuangtai__in=[0, 1], + create_time__date=today, + ).exclude(tixian_id=auto_record.tixian_id).aggregate( + total=Sum('shijidaozhang'), + )['total'] or decimal.Decimal('0.00') + return stat_total >= other_sum + shijidaozhang + + +def apply_transfer_success_quota(auto_record): + """ + 打款成功时累加每日限额统计(仅自动审核流程 shenhe_danhao 非空)。 + - houtai.TixianRiTongji.total_amount += 实际到账 + - 用户扩展表个人今日已提 += 申请金额(打手/管事/组长) + 须在 transaction.atomic 内、且打款记录 zhuangtai 由 0→1 时调用一次。 + """ + if not auto_record.shenhe_danhao: + return + + from houtai.models import TixianRiTongji + + today = date.today() + leixing = auto_record.leixing + jine = auto_record.jine + shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00') + + try: + user_main = UserMain.objects.get(yonghuid=auto_record.yonghuid) + except UserMain.DoesNotExist: + logger.error('打款成功累加限额失败:用户不存在 %s', auto_record.yonghuid) + return + + platform_stat, _ = TixianRiTongji.objects.select_for_update().get_or_create( + riqi=today, + leixing=leixing, + defaults={'total_amount': decimal.Decimal('0.00')}, + ) + platform_stat.total_amount += shijidaozhang + platform_stat.save(update_fields=['total_amount']) + + if leixing == 1: + dashou = UserDashou.objects.select_for_update().get(user=user_main) + if dashou.last_tixian_date != today: + dashou.jinritixian_jine = decimal.Decimal('0.00') + dashou.last_tixian_date = today + dashou.jinritixian_jine += jine + dashou.save(update_fields=['jinritixian_jine', 'last_tixian_date']) + elif leixing == 2: + guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) + if guanshi.last_tixian_date != today: + guanshi.jinritixian_jine = decimal.Decimal('0.00') + guanshi.last_tixian_date = today + guanshi.jinritixian_jine += jine + guanshi.save(update_fields=['jinritixian_jine', 'last_tixian_date']) + elif leixing == 3: + zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) + if zuzhang.last_tixian_time and zuzhang.last_tixian_time.date() != today: + zuzhang.jinri_tixian = decimal.Decimal('0.00') + elif not zuzhang.last_tixian_time: + zuzhang.jinri_tixian = decimal.Decimal('0.00') + zuzhang.jinri_tixian += jine + zuzhang.last_tixian_time = timezone.now() + zuzhang.save(update_fields=['jinri_tixian', 'last_tixian_time']) + + logger.info( + '打款成功已累加限额 bill=%s yonghuid=%s leixing=%s jine=%s shijidaozhang=%s ' + 'platform_total=%s', + auto_record.tixian_id, auto_record.yonghuid, leixing, jine, shijidaozhang, + platform_stat.total_amount, + ) + + +def refund_balance(user_main, leixing, jine): + """审核拒绝时退还可到账金额 shijidaozhang(须在 transaction.atomic 内;手续费已在申请扣款时扣除不退还)""" + if leixing == 1: + dashou = UserDashou.objects.select_for_update().get(user=user_main) + dashou.yue += jine + dashou.save() + elif leixing == 2: + guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) + guanshi.yue += jine + guanshi.save() + elif leixing == 3: + zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) + zuzhang.ketixian_jine += jine + zuzhang.save() + elif leixing == 4: + kaoheguan = UserShenheguan.objects.select_for_update().get(user=user_main) + kaoheguan.yue += jine + kaoheguan.save() + elif leixing == 5: + dashou = UserDashou.objects.select_for_update().get(user=user_main) + dashou.yajin += jine + dashou.zhanghaozhuangtai = 1 + dashou.save() + elif leixing == 6: + shangjia = UserShangjia.objects.select_for_update().get(user=user_main) + shangjia.yue += jine + shangjia.save() + else: + raise ValueError('无效的提现类型') + + +def deduct_balance(user_main, leixing, jine): + """扣减申请金额 jine(必须在 transaction.atomic 内调用)""" + if leixing == 1: + dashou = UserDashou.objects.select_for_update().get(user=user_main) + if dashou.yue < jine: + raise ValueError('余额不足') + dashou.yue -= jine + dashou.save() + return dashou.nicheng or '' + if leixing == 2: + guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) + if guanshi.yue < jine: + raise ValueError('余额不足') + guanshi.yue -= jine + guanshi.save() + return '管事用户' + if leixing == 3: + zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) + if zuzhang.ketixian_jine < jine: + raise ValueError('可提现金额不足') + zuzhang.ketixian_jine -= jine + zuzhang.save() + return '' + if leixing == 4: + kaoheguan = UserShenheguan.objects.select_for_update().get(user=user_main) + if kaoheguan.yue < jine: + raise ValueError('可提现金额不足') + kaoheguan.yue -= jine + kaoheguan.save() + return '考核官' + if leixing == 5: + dashou = UserDashou.objects.select_for_update().get(user=user_main) + if dashou.yajin < jine: + raise ValueError('押金余额不足') + dashou.yajin -= jine + dashou.zhanghaozhuangtai = 0 + dashou.save() + return dashou.nicheng or '' + if leixing == 6: + shangjia = UserShangjia.objects.select_for_update().get(user=user_main) + if shangjia.yue < jine: + raise ValueError('商家余额不足') + shangjia.yue -= jine + shangjia.save() + return shangjia.nicheng or '' + raise ValueError('无效的提现类型') + + +def get_balance_response_data(user_main, leixing): + data = {} + if leixing == 1: + try: + dashou = UserDashou.objects.get(user=user_main) + data['new_yongjin'] = str(dashou.yue.quantize(decimal.Decimal('0.01'))) + except UserDashou.DoesNotExist: + pass + elif leixing == 2: + try: + guanshi = UserGuanshi.objects.get(user=user_main) + data['new_fenyong'] = str(guanshi.yue.quantize(decimal.Decimal('0.01'))) + except UserGuanshi.DoesNotExist: + pass + elif leixing == 3: + try: + zuzhang = UserZuzhang.objects.get(user=user_main) + data['new_zuzhang_ketixian'] = str(zuzhang.ketixian_jine.quantize(decimal.Decimal('0.01'))) + except UserZuzhang.DoesNotExist: + pass + elif leixing == 4: + try: + kg = UserShenheguan.objects.get(user=user_main) + data['new_kaoheguan_yue'] = str(kg.yue.quantize(decimal.Decimal('0.01'))) + except UserShenheguan.DoesNotExist: + pass + return data + + +def has_pending_audit(yonghuid): + return TixianShenheJilu.objects.filter( + yonghuid=yonghuid, + zhuangtai__in=PENDING_AUDIT_STATUSES, + ).exists() + + +def sync_audit_and_jilu_status(audit, new_status, **extra_fields): + audit.zhuangtai = new_status + for k, v in extra_fields.items(): + setattr(audit, k, v) + audit.save() + if audit.tixianjilu_id: + update_fields = {'zhuangtai': new_status, 'update_time': timezone.now()} + if 'bhliyou' in extra_fields: + update_fields['bhliyou'] = extra_fields['bhliyou'] + Tixianjilu.objects.filter(id=audit.tixianjilu_id).update(**update_fields) + + +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/' + f'transfer-bills/out-bill-no/{out_bill_no}' + ) + try: + auth = build_authorization('GET', url, '') + headers = { + 'Authorization': auth, + 'Accept': 'application/json', + 'User-Agent': 'Mozilla/5.0', + } + resp = requests.get(url, headers=headers, timeout=10) + if resp.status_code == 200: + return resp.json() + if resp.status_code == 404: + return {'_not_found': True} + logger.error( + '微信查单失败 bill=%s HTTP%s body=%s', + out_bill_no, resp.status_code, resp.text[:300], + ) + return None + except (requests.RequestException, OSError, ValueError) as e: + logger.error('微信查单异常 bill=%s err=%s', out_bill_no, e, exc_info=True) + return None + except Exception as e: + logger.error('微信查单未知异常 bill=%s err=%s', out_bill_no, e, exc_info=True) + return None + + +def _parse_package_info(raw): + if not raw: + return None + if isinstance(raw, dict): + return raw + try: + return json.loads(raw) + except (TypeError, ValueError): + return None + + +def _apply_platform_accounting(auto_record): + """平台收支统计(幂等由 mark_transfer_success 外层保证只调一次)""" + from dingdan.utils import update_daily_payout + + update_daily_payout(auto_record.shijidaozhang) + try: + szjilu = Szjilu.objects.select_for_update().get(id=1) + szjilu.zongsy -= auto_record.shijidaozhang + szjilu.zongzc += auto_record.shijidaozhang + szjilu.jrzc += auto_record.shijidaozhang + szjilu.save() + except Szjilu.DoesNotExist: + Szjilu.objects.create( + id=1, + zongsy=-auto_record.shijidaozhang, + zongzc=auto_record.shijidaozhang, + jrzc=auto_record.shijidaozhang, + ) + + +def mark_transfer_success(auto_record, *, wechat_transfer_no=None, with_accounting=True): + """ + 打款成功落库(tixianqr / callback / 查单对账 共用) + select_for_update + zhuangtai==0 才更新并记账,防双写 + 返回 True=本次新标记成功;False=已处理过 + """ + with transaction.atomic(): + ar = TixianAutoRecord.objects.select_for_update().get(pk=auto_record.pk) + if ar.zhuangtai != 0: + return False + + ar.zhuangtai = 1 + update_fields = ['zhuangtai', 'update_time'] + if wechat_transfer_no: + ar.wechat_transfer_no = wechat_transfer_no + update_fields.append('wechat_transfer_no') + ar.save(update_fields=update_fields) + + if ar.shenhe_danhao: + try: + audit = TixianShenheJilu.objects.select_for_update().get( + shenhe_danhao=ar.shenhe_danhao, + ) + if audit.zhuangtai != 2: + sync_audit_and_jilu_status(audit, 2) + except TixianShenheJilu.DoesNotExist: + logger.warning('打款成功但审核记录不存在: %s', ar.shenhe_danhao) + + if ar.shenhe_danhao and not _quota_already_reserved_for_record(ar): + apply_transfer_success_quota(ar) + + if with_accounting: + _apply_platform_accounting(ar) + + return True + + +def _sync_record_from_wx_query(auto_record, wx_data): + """根据微信查单结果修正本地打款记录,返回 (action, payload)""" + if wx_data.get('_not_found'): + age = timezone.now() - auto_record.create_time + if auto_record.zhuangtai == 0 and age < timedelta(minutes=PENDING_QUERY_GRACE_MINUTES): + return RECONCILE_PENDING, { + 'msg': '有收款处理中,请稍后再试', + 'tixian_id': auto_record.tixian_id, + } + if auto_record.zhuangtai == 0: + auto_record.zhuangtai = 2 + auto_record.fail_reason = '微信未找到该打款单,请重新发起收款' + auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'update_time']) + release_collect_quota_for_record(auto_record) + return RECONCILE_ALLOW_NEW, {} + + state = (wx_data.get('state') or '').upper() + + if state in WX_STATE_SUCCESS: + mark_transfer_success( + auto_record, + wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'), + ) + return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'} + + if state in WX_STATE_WAIT: + package = wx_data.get('package_info') or _parse_package_info(auto_record.wechat_package) + if not package: + return RECONCILE_PENDING, { + 'msg': '有收款处理中,请稍后再试', + 'tixian_id': auto_record.tixian_id, + } + if auto_record.zhuangtai == 0: + auto_record.wechat_package = json.dumps(package, ensure_ascii=False) + auto_record.save(update_fields=['wechat_package', 'update_time']) + wx_cfg = settings.WECHAT_PAY_V3_CONFIG + return RECONCILE_WAIT_CONFIRM, { + 'tixian_id': auto_record.tixian_id, + 'package_info': package, + 'shouxufei': str(auto_record.shouxufei), + 'shijidaozhang': str(auto_record.shijidaozhang), + 'current_rate': str(auto_record.feilv), + 'mch_id': wx_cfg.get('MCHID', ''), + 'shenhe_danhao': auto_record.shenhe_danhao or '', + } + + if state in WX_STATE_CANCELING: + return RECONCILE_PENDING, { + 'msg': '收款撤销处理中,请稍后再试', + 'tixian_id': auto_record.tixian_id, + } + + if state in WX_STATE_FAIL: + if auto_record.zhuangtai != 1: + auto_record.zhuangtai = 2 + auto_record.fail_reason = ( + wx_data.get('fail_reason') or wx_data.get('close_reason') or state + )[:500] + auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'update_time']) + release_collect_quota_for_record(auto_record) + return RECONCILE_ALLOW_NEW, {} + + if auto_record.zhuangtai == 0: + return RECONCILE_PENDING, { + 'msg': f'收款处理中(微信状态:{state or "未知"}),请稍后再试', + 'tixian_id': auto_record.tixian_id, + } + return RECONCILE_ALLOW_NEW, {} + + +def get_auto_record_for_collect(shenhe_danhao): + """ + 取本笔提现申请应对接微信的那条打款记录: + 优先 zhuangtai=0(进行中),否则取最新一条(可能本地误标失败但微信未终态) + """ + pending = ( + TixianAutoRecord.objects.filter(shenhe_danhao=shenhe_danhao, zhuangtai=0) + .order_by('-create_time') + .first() + ) + if pending: + return pending + return ( + TixianAutoRecord.objects.filter(shenhe_danhao=shenhe_danhao) + .order_by('-create_time') + .first() + ) + + +def reconcile_shenhe_wechat_bills(shenhe_danhao): + """ + P0 防双笔:同一 shenhe_danhao 下所有打款记录逐条问微信 + 任一 SUCCESS / 进行中 → 禁止新建;全部终态失败才 ALLOW_NEW + 返回 (action, payload) + """ + records = list( + TixianAutoRecord.objects.filter(shenhe_danhao=shenhe_danhao).order_by('-create_time') + ) + + if any(r.zhuangtai == 1 for r in records): + try: + audit = TixianShenheJilu.objects.get(shenhe_danhao=shenhe_danhao) + if audit.zhuangtai != 2: + sync_audit_and_jilu_status(audit, 2) + except TixianShenheJilu.DoesNotExist: + pass + return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'} + + if not records: + return RECONCILE_ALLOW_NEW, {} + + pending_hit = None + for rec in records: + if rec.zhuangtai == 1: + return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'} + + wx_data = query_wechat_transfer_bill(rec.tixian_id) + if wx_data is None: + logger.error( + '对账查单不可用 shenhe_danhao=%s bill=%s', + shenhe_danhao, rec.tixian_id, + ) + return RECONCILE_PENDING, { + 'msg': '有收款处理中,系统正在核对微信状态,请稍后再试', + 'tixian_id': rec.tixian_id, + } + + action, payload = _sync_record_from_wx_query(rec, wx_data) + if action == RECONCILE_COMPLETED: + return action, payload + if action == RECONCILE_WAIT_CONFIRM: + return action, payload + if action == RECONCILE_PENDING: + if pending_hit is None: + pending_hit = (action, payload) + continue + + if pending_hit: + return pending_hit + return RECONCILE_ALLOW_NEW, {} + + +def resolve_collect_context(yonghuid, tixianjilu_id=None, shenhe_danhao='', shenhe_jilu_id=None): + """ + 从提现记录 ID 进门(P0 收款主路径): + Tixianjilu → TixianShenheJilu,校验归属、状态=6 待收款 + 返回 ({jilu, audit}, err_msg) + """ + if tixianjilu_id is None and not shenhe_danhao: + return None, '缺少提现记录ID' + + jilu = None + if tixianjilu_id is not None: + try: + jilu = Tixianjilu.objects.get(id=int(tixianjilu_id), yonghuid=yonghuid) + except (Tixianjilu.DoesNotExist, TypeError, ValueError): + return None, '提现记录不存在或不属于当前用户' + + if jilu.zhuangtai != 6: + return None, f'当前状态不可收款(状态{jilu.zhuangtai}),请等待审核通过' + if not jilu.shenhe_jilu_id or not jilu.shenhe_danhao: + return None, '提现记录未关联审核单,无法收款' + + if shenhe_jilu_id is not None: + try: + if int(shenhe_jilu_id) != jilu.shenhe_jilu_id: + return None, '审核记录ID与提现记录不匹配' + except (TypeError, ValueError): + return None, '审核记录ID格式错误' + + if shenhe_danhao and shenhe_danhao != jilu.shenhe_danhao: + return None, '审核单号与提现记录不匹配' + + shenhe_danhao = jilu.shenhe_danhao + shenhe_jilu_id = jilu.shenhe_jilu_id + + try: + if shenhe_jilu_id: + audit = TixianShenheJilu.objects.get( + id=int(shenhe_jilu_id), + shenhe_danhao=shenhe_danhao, + yonghuid=yonghuid, + ) + else: + audit = TixianShenheJilu.objects.get( + shenhe_danhao=shenhe_danhao, + yonghuid=yonghuid, + ) + except (TixianShenheJilu.DoesNotExist, TypeError, ValueError): + return None, '审核记录不存在' + + if audit.zhuangtai == 2: + return None, '该提现已完成,请勿重复操作' + if audit.zhuangtai != 6: + return None, f'当前状态不可收款(状态{audit.zhuangtai}),请等待审核通过' + if not is_auto_dakuan_audit(audit): + return None, '该提现单为手动打款,无法使用自动收款' + + if jilu is None: + try: + jilu = Tixianjilu.objects.get( + id=audit.tixianjilu_id, + yonghuid=yonghuid, + ) if audit.tixianjilu_id else None + except Tixianjilu.DoesNotExist: + jilu = None + + return {'jilu': jilu, 'audit': audit, 'shenhe_danhao': shenhe_danhao}, '' + + +def handle_post_transfer_failure(tixian_id, shenhe_danhao, err_code='', err_msg=''): + """ + 发起转账 HTTP 非成功时:先查微信再决定是否标失败(P0,禁止盲目标失败导致双笔) + 返回 (action, payload_or_user_msg) + """ + try: + ar = TixianAutoRecord.objects.get(tixian_id=tixian_id) + except TixianAutoRecord.DoesNotExist: + return RECONCILE_PENDING, '收款处理中,请稍后再试' + + wx_data = query_wechat_transfer_bill(tixian_id) + if wx_data is None: + logger.error( + '发起转账失败且查单不可用 bill=%s code=%s msg=%s,保持待查单', + tixian_id, err_code, err_msg, + ) + return RECONCILE_PENDING, '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击' + + action, payload = _sync_record_from_wx_query(ar, wx_data) + if action == RECONCILE_ALLOW_NEW: + return RECONCILE_ALLOW_NEW, payload + if action == RECONCILE_WAIT_CONFIRM: + return RECONCILE_WAIT_CONFIRM, payload + if action == RECONCILE_COMPLETED: + return RECONCILE_COMPLETED, payload + return RECONCILE_PENDING, payload.get('msg', '收款处理中,请稍后再试') + + +def get_audit_for_collect(shenhe_danhao, yonghuid, shenhe_jilu_id=None): + """ + 收款前数据库校验(须在 transaction.atomic 内调用;微信查单在事务外单独做) + """ + try: + audit = TixianShenheJilu.objects.select_for_update().get( + shenhe_danhao=shenhe_danhao, + yonghuid=yonghuid, + ) + except TixianShenheJilu.DoesNotExist: + return None, '审核记录不存在' + + if shenhe_jilu_id is not None: + try: + if int(shenhe_jilu_id) != audit.id: + return None, '审核记录ID不匹配' + except (TypeError, ValueError): + return None, '审核记录ID格式错误' + + if audit.zhuangtai == 2: + return None, '该提现已完成,请勿重复操作' + + if audit.zhuangtai != 6: + return None, f'当前状态不可收款(状态{audit.zhuangtai}),请等待审核通过' + + if not is_auto_dakuan_audit(audit): + return None, '该提现单为手动打款,无法使用自动收款' + + return audit, '' + + +def create_audit_application(user_main, leixing, jine): + """ + 提交审核申请:校验 → 扣款 → 写审核表 + 提现记录表 + 不创建打款记录表(TixianAutoRecord 仅在收款时创建) + """ + yonghuid = user_main.yonghuid + + if has_pending_audit(yonghuid): + raise ValueError('账户余额不足或您有进行中的提现申请,请等待处理完成或10分钟后后再提交') + + ok, msg, extra = validate_withdraw_eligibility(user_main, leixing, jine) + if not ok: + raise ValueError(msg) + + feilv = get_tixian_feilv(leixing) + shouxufei, shijidaozhang = calc_fee_amounts(jine, feilv) + if shijidaozhang <= 0: + raise ValueError('提现金额过低,扣除手续费后无实际到账') + + shenhe_danhao = generate_shenhe_danhao() + nicheng = extra.get('nicheng', '') + + with transaction.atomic(): + nicheng = deduct_balance(user_main, leixing, jine) or nicheng + + audit_kwargs = { + 'shenhe_danhao': shenhe_danhao, + 'yonghuid': yonghuid, + 'leixing': leixing, + 'zhuangtai': 1, + 'shenqing_jine': jine, + 'shouxufei': shouxufei, + 'shijidaozhang': shijidaozhang, + 'feilv': feilv, + } + if has_dakuan_mode_field(): + audit_kwargs['dakuan_mode'] = DAKUAN_MODE_AUTO + audit = TixianShenheJilu.objects.create(**audit_kwargs) + + tixian_record = Tixianjilu.objects.create( + yonghuid=yonghuid, + avatar=user_main.avatar or '', + phone=user_main.phone or '', + nicheng=nicheng, + leixing=leixing, + zhifu=user_main.zhifu or '', + skzhanghao=user_main.skzhanghao or '', + jine=shijidaozhang, + zhuangtai=1, + fangshi=1, + shenheid=None, + bhliyou='', + shenhe_jilu_id=audit.id, + shenhe_danhao=shenhe_danhao, + ) + + audit.tixianjilu_id = tixian_record.id + audit.save(update_fields=['tixianjilu_id']) + + response_data = get_balance_response_data(user_main, leixing) + response_data.update({ + 'shenhe_danhao': shenhe_danhao, + 'shenhe_jilu_id': audit.id, + 'tixianjilu_id': tixian_record.id, + 'shouxufei': str(shouxufei), + 'shijidaozhang': str(shijidaozhang), + 'current_rate': str(feilv.quantize(decimal.Decimal('0.0001'))), + 'leixing': leixing, + 'zhuangtai': 1, + }) + return response_data diff --git a/yonghu/tixian_shenhe_views.py b/yonghu/tixian_shenhe_views.py new file mode 100644 index 0000000..0886021 --- /dev/null +++ b/yonghu/tixian_shenhe_views.py @@ -0,0 +1,383 @@ +# yonghu/tixian_shenhe_views.py +""" +自动提现审核 — 用户侧接口: + ① TixianZddkshApplyView → POST /yonghu/zddksh 提交审核(扣款) + ② process_audit_collect → POST /yonghu/tixiansq 收款(P0:一单到底,先查微信再决定是否新建) +""" +import decimal +import json +import logging +import random +import string +import time + +import requests +from django.conf import settings +from django.db import transaction +from django.utils import timezone +from rest_framework import permissions +from rest_framework.response import Response +from rest_framework.views import APIView + +from .models import TixianAutoRecord, TixianShenheJilu +from .tixian_shenhe_services import ( + RECONCILE_ALLOW_NEW, + RECONCILE_COMPLETED, + RECONCILE_PENDING, + RECONCILE_WAIT_CONFIRM, + check_collect_quota_limits, + create_audit_application, + get_audit_for_collect, + handle_post_transfer_failure, + reconcile_shenhe_wechat_bills, + release_collect_quota_for_record, + reserve_collect_quota_limits, + resolve_collect_context, + validate_collect_eligibility, +) + +logger = logging.getLogger('yonghu.tixian_shenhe') + + +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}' + + +def _build_collect_success_response(payload): + return Response({ + 'code': 0, + 'msg': '请确认收款', + 'data': payload, + }) + + +def _is_wx_balance_insufficient(err_code='', err_msg=''): + blob = f'{err_code} {err_msg}'.upper() + return any(k in blob for k in ('NOT_ENOUGH', 'INSUFFICIENT', '余额不足', '资金不足')) + + +def _verify_collect_quota_or_response(user_main, audit, shenhe_danhao): + """收款限额校验;不通过时直接返回 Response""" + ok, msg, limit_kind = check_collect_quota_limits( + user_main, + audit.leixing, + audit.shenqing_jine, + audit.shijidaozhang, + shenhe_danhao, + ) + if not ok: + limit_code = 33 if limit_kind == 'platform' else 400 + logger.warning( + '收款限额校验未通过 yonghuid=%s shenhe_danhao=%s leixing=%s kind=%s msg=%s', + user_main.yonghuid, shenhe_danhao, audit.leixing, limit_kind, msg, + ) + return Response({'code': limit_code, 'msg': msg}) + return None + + +def _mark_auto_record_failed(tixian_id, fail_reason): + """微信明确失败时标记打款记录终态,避免后续一直被当成「处理中」""" + TixianAutoRecord.objects.filter(tixian_id=tixian_id, zhuangtai=0).update( + zhuangtai=2, + fail_reason=(fail_reason or '打款失败')[:500], + update_time=timezone.now(), + ) + + +def _build_wx_transfer_body(wx_cfg, tixian_id, openid, yonghuid, leixing, shijidaozhang): + transfer_scene_id = wx_cfg['TRANSFER_SCENE_ID'] + role_map = {1: '打手', 2: '管事', 3: '组长', 4: '考核官', 5: '打手', 6: '商家'} + desc_map = {1: '佣金提现', 2: '分红提现', 3: '分红提现', 4: '分佣提现', 5: '押金提现', 6: '余额提现'} + transfer_scene_report_infos = [] + if transfer_scene_id == '1005': + transfer_scene_report_infos = [ + {'info_type': '岗位类型', 'info_content': role_map.get(leixing, '用户')}, + {'info_type': '报酬说明', 'info_content': desc_map.get(leixing, '提现')}, + ] + elif transfer_scene_id == '1009': + transfer_scene_report_infos = [ + {'info_type': '采购商品名称', 'info_content': '平台服务'}, + ] + return { + 'appid': wx_cfg['APPID'], + 'out_bill_no': tixian_id, + 'transfer_scene_id': transfer_scene_id, + 'openid': openid, + 'transfer_amount': int(shijidaozhang * 100), + 'transfer_remark': f'平台提现-{yonghuid}', + 'transfer_scene_report_infos': transfer_scene_report_infos, + } + + +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) + headers = { + 'Authorization': auth, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'Mozilla/5.0', + } + resp = requests.post(url, data=body_json, headers=headers, timeout=15) + try: + wx_result = resp.json() + except ValueError: + wx_result = {} + return resp, wx_result + + +class TixianZddkshApplyView(APIView): + """POST /yonghu/zddksh 提交审核申请(扣款,不创建 TixianAutoRecord)""" + 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}' + if not acquire_lock(lock_key, timeout=10): + return Response({'code': 429, 'msg': '操作频繁,请稍后重试'}) + + try: + leixing = request.data.get('leixing') + jine_str = request.data.get('jine') + if leixing is None or jine_str is None: + return Response({'code': 1, 'msg': '缺少参数: leixing 或 jine'}) + + try: + leixing = int(leixing) + jine = decimal.Decimal(str(jine_str)) + except (ValueError, decimal.InvalidOperation): + return Response({'code': 2, 'msg': '参数格式错误'}) + + if leixing not in [1, 2, 3, 4, 5, 6]: + return Response({'code': 3, 'msg': '提现类型无效'}) + if jine <= decimal.Decimal('0.1'): + return Response({'code': 4, 'msg': '提现金额必须大于0.1'}) + + try: + data = create_audit_application(user_main, leixing, jine) + except ValueError as e: + return Response({'code': 400, 'msg': str(e)}) + except Exception as e: + logger.error(f'提现审核申请异常: {e}', exc_info=True) + return Response({'code': 99, 'msg': '提现申请失败,请稍后重试'}) + + return Response({'code': 0, 'msg': '提现申请已提交,请等待审核', 'data': data}) + + finally: + release_lock(lock_key) + + +def process_audit_collect(request): + """ + POST /yonghu/tixiansq 用户收款(P0 防多提) + + 主路径:tixianjilu_id → Tixianjilu → TixianShenheJilu(状态6) + 若 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') + shenhe_danhao = (request.data.get('shenhe_danhao') or '').strip() + shenhe_jilu_id = request.data.get('shenhe_jilu_id') + + logger.info( + '收款请求 yonghuid=%s tixianjilu_id=%s shenhe_jilu_id=%s shenhe_danhao=%s', + yonghuid, tixianjilu_id, shenhe_jilu_id, shenhe_danhao, + ) + + try: + if not user_main.openid: + return Response({'code': 10, 'msg': '用户未绑定微信,无法收款'}) + + wx_cfg = settings.WECHAT_PAY_V3_CONFIG + if not wx_cfg.get('APPID') or not wx_cfg.get('MCHID') or not wx_cfg.get('TRANSFER_SCENE_ID'): + logger.error('微信打款配置不完整 APPID=%s MCHID=%s', wx_cfg.get('APPID'), wx_cfg.get('MCHID')) + return Response({'code': 11, 'msg': '系统配置异常,请联系客服'}) + + ctx, err = resolve_collect_context( + yonghuid, + tixianjilu_id=tixianjilu_id, + shenhe_danhao=shenhe_danhao, + shenhe_jilu_id=shenhe_jilu_id, + ) + if not ctx: + return Response({'code': 400, 'msg': err}) + + audit = ctx['audit'] + shenhe_danhao = ctx['shenhe_danhao'] + tixianjilu_id_val = ctx['jilu'].id if ctx.get('jilu') else audit.tixianjilu_id + leixing = audit.leixing + + # 二次资格校验:不通过仅返回错误,审核单保持待收款(6),不退款,用户补齐资质后可再点收款 + collect_ok, collect_msg = validate_collect_eligibility(user_main, leixing) + if not collect_ok: + logger.warning( + '收款校验未通过 yonghuid=%s shenhe_danhao=%s leixing=%s msg=%s', + yonghuid, shenhe_danhao, leixing, collect_msg, + ) + return Response({'code': 400, 'msg': collect_msg}) + + quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao) + if quota_resp: + return quota_resp + + lock_key = f'tixian_collect:{shenhe_danhao}' + if not acquire_lock(lock_key, timeout=15): + return Response({'code': 429, 'msg': '操作频繁,请稍后重试'}) + + tixian_id = None + quota_reserved = False + try: + action, payload = reconcile_shenhe_wechat_bills(shenhe_danhao) + if action == RECONCILE_COMPLETED: + return Response({'code': 8, 'msg': payload.get('msg', '该提现已完成,请勿重复操作')}) + if action == RECONCILE_WAIT_CONFIRM: + quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao) + if quota_resp: + return quota_resp + return _build_collect_success_response(payload) + if action == RECONCILE_PENDING: + msg = payload.get('msg') if isinstance(payload, dict) else payload + return Response({'code': 12, 'msg': msg or '有收款处理中,请稍后再试'}) + + if action != RECONCILE_ALLOW_NEW: + return Response({'code': 12, 'msg': '收款处理中,请稍后再试'}) + + with transaction.atomic(): + audit, err = get_audit_for_collect( + shenhe_danhao, yonghuid, audit.id, + ) + if not audit: + return Response({'code': 400, 'msg': err}) + + # 金额一律用审核表已落库数据(申请时扣款+算费),收款不再重算 + jine = audit.shenqing_jine + shouxufei = audit.shouxufei + shijidaozhang = audit.shijidaozhang + feilv = audit.feilv + + ok, msg, limit_kind = reserve_collect_quota_limits( + user_main, leixing, jine, shijidaozhang, shenhe_danhao, + ) + if not ok: + limit_code = 33 if limit_kind == 'platform' else 400 + return Response({'code': limit_code, 'msg': msg}) + + quota_reserved = True + + tixian_id = generate_tixian_id() + TixianAutoRecord.objects.create( + tixian_id=tixian_id, + yonghuid=yonghuid, + leixing=leixing, + jine=jine, + shouxufei=shouxufei, + shijidaozhang=shijidaozhang, + feilv=feilv, + zhuangtai=0, + shenhe_danhao=shenhe_danhao, + ) + audit.tixian_auto_id = tixian_id + audit.save(update_fields=['tixian_auto_id', 'update_time']) + + body = _build_wx_transfer_body( + wx_cfg, tixian_id, user_main.openid, yonghuid, leixing, shijidaozhang, + ) + + try: + resp, wx_result = _call_wechat_transfer(body, wx_cfg) + + if resp.status_code == 200 and wx_result.get('state') == 'WAIT_USER_CONFIRM': + package_info = wx_result.get('package_info') + auto_record = TixianAutoRecord.objects.get(tixian_id=tixian_id) + auto_record.wechat_package = json.dumps(package_info, ensure_ascii=False) + auto_record.save(update_fields=['wechat_package']) + + return _build_collect_success_response({ + 'tixian_id': tixian_id, + 'package_info': package_info, + 'shouxufei': str(shouxufei), + 'shijidaozhang': str(shijidaozhang), + 'current_rate': str(feilv), + 'mch_id': wx_cfg['MCHID'], + 'shenhe_danhao': shenhe_danhao, + 'tixianjilu_id': tixianjilu_id_val, + }) + + err_code = wx_result.get('code', '') + err_msg = wx_result.get('message', wx_result.get('detail', '微信接口调用失败')) + logger.error( + '微信发起转账未进入待确认: bill=%s code=%s msg=%s HTTP%s', + tixian_id, err_code, err_msg, resp.status_code, + ) + + is_balance_issue = _is_wx_balance_insufficient(err_code, err_msg) + if is_balance_issue: + _mark_auto_record_failed(tixian_id, err_msg) + if quota_reserved: + release_collect_quota_for_record( + TixianAutoRecord.objects.get(tixian_id=tixian_id), + ) + quota_reserved = False + return Response({ + 'code': 99, + 'msg': '运营账户资金不足,需等管理员充值后才能提现', + }) + + post_action, post_payload = handle_post_transfer_failure( + tixian_id, shenhe_danhao, err_code, err_msg, + ) + if post_action == RECONCILE_WAIT_CONFIRM: + quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao) + if quota_resp: + return quota_resp + return _build_collect_success_response(post_payload) + if post_action == RECONCILE_COMPLETED: + return Response({'code': 8, 'msg': '该提现已完成,请勿重复操作'}) + if post_action == RECONCILE_PENDING: + pending_msg = ( + post_payload.get('msg', post_payload) + if isinstance(post_payload, dict) else post_payload + ) + return Response({'code': 12, 'msg': pending_msg or '有收款处理中,请稍后再试'}) + + user_msg = err_msg or '微信收款发起失败,请稍后重试' + _mark_auto_record_failed(tixian_id, user_msg) + if quota_reserved: + release_collect_quota_for_record( + TixianAutoRecord.objects.get(tixian_id=tixian_id), + ) + quota_reserved = False + return Response({'code': 99, 'msg': user_msg}) + + except requests.RequestException as e: + logger.error( + '微信打款网络异常(保持待查单): bill=%s err=%s', + tixian_id, e, exc_info=True, + ) + return Response({ + 'code': 12, + 'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击', + }) + + finally: + release_lock(lock_key) + + except Exception as e: + logger.error( + '收款接口未捕获异常 yonghuid=%s tixianjilu_id=%s shenhe_danhao=%s err=%s', + yonghuid, tixianjilu_id, shenhe_danhao, e, + exc_info=True, + ) + return Response({'code': 99, 'msg': '系统繁忙,请稍后重试'}) diff --git a/yonghu/urls.py b/yonghu/urls.py new file mode 100644 index 0000000..da3f849 --- /dev/null +++ b/yonghu/urls.py @@ -0,0 +1,166 @@ +from django.urls import path +from django.views.decorators.csrf import csrf_exempt +from .views import WechatMiniProgramLoginView, UserInfoUpdateView, DashouXinxiAPIView, \ + ZaixianZhuangtaiAPIView, GuanshiXinxiView, ShangJiaXinXiView, BossBiaoshiView, YaoqingmaView, \ + DashouZhuceView, GuanshiYaoqingDashouListView, TixianXinxiHuoquView, ShoukuanXinxiShangchuanView, \ + TixianShenqingView, TixianJiluHuoquViewV2, AdminLoginView, AdminFinancialDataView, AdGetDingdanList, \ + AdGetOrderTypes, AdGuanLiYongHu, AdYaoQingDaShou, CfGuanLi, AdTongYiChuFa, AdckyhxqView, AdcjxgView, AddtxshView, \ + AdtixianqkView, DashouZiliaoHuoquView, DashouGengxinView,ShangjiaZhuceView,\ + PaihangbangGuanliQueryView,PaihangbangRiqiliebiaoView, WechatLoginAndDashouRegisterView, \ + YonghuTixianShenheXiugaiView, ChufaJiluHuoquView, DashouShensuView, ShangjiaChufaJiluHuoquView, \ + WeixinOfficialCallbackView,KefuLoginView, KefuStatsView, KefuGetOrderTypesView, KefuGetOrderListView, \ + KefuGetShangjiaOrderListView, KefuGetOrderDetailView, KefuChangeDashouView, KefuRecoverOrderView, \ + KefuPlatformRefundView, KefuRejectRefundView, KefuMerchantRefundView, KefuRejectSettlementView, \ + KefuTransferHallView, KefuCancelDesignationView, KefuGetDashouListView, KefuGetDashouDetailView, \ + KefuUpdateDashouView, KefuPunishmentListView, KefuPunishmentActionView, KefuPunishmentDetailView, \ + KefuWithdrawListView, KefuWithdrawDetailView, KefuWithdrawActionView, KefuPunishView, AdKfglView, AdKftjView, \ + AdKfxgView,GuanshiRegisterView, WechatLoginAndGuanshiRegisterView,ZuzhangZhuceView, ZuzhangXinxiView,KefuForceCompleteView,HuoQuYaoQingRenView, DashouJianquanView, LaobanJianquanView,\ + FaKuanJiFenTongJiView, DaShouFaKuanLieBiaoView,\ + FaKuanShenSuView, FaKuanPayView,FaKuanHuitiaoView, FaKuanPayPollView, FaKuanPayFailView,KaohePayView,KaohePayCallbackView, KaohePayPollView,KaohePayFailView, TixianAssetView, GuanshiContactView,TixianQueRenAutoView, TixianShenqingV3View, TixianCallbackV3View +from .tixian_shenhe_views import TixianZddkshApplyView +from .paihang_views import PhbHqsjView + + +urlpatterns = [ + path('wechatlogin', WechatMiniProgramLoginView.as_view(), name='用户一键微信登录'), + path('xiugai', UserInfoUpdateView.as_view(), name='用户信息修改'), + # 打手信息接口 + path('dashouxinxi', DashouXinxiAPIView.as_view(), name='打手信息查询'), + # 更改在线状态接口 + path('zaixianzhuangtai', ZaixianZhuangtaiAPIView.as_view(), name='打手在线状态更改'), + path('guanshixinxi', GuanshiXinxiView.as_view(), name='获取管事信息'), + path('shangjiaxinxi', ShangJiaXinXiView.as_view(), name='获取商家信息'), + path('bossbiaoshi', BossBiaoshiView.as_view(), name='获取老板标识'), + path('yaoqingma', YaoqingmaView.as_view(), name='管事生成邀请码'), + path('dashouzhuce',DashouZhuceView.as_view(), name='打手邀请码注册'), + path('gshqyqds', GuanshiYaoqingDashouListView.as_view(), name='管事获取邀请打手'), + path('tixianhq', TixianXinxiHuoquView.as_view(), name='获取体现缓存信息'), # 获取提现信息 + path('sksc', ShoukuanXinxiShangchuanView.as_view(), name='上传提现相关数据或收款码'), # 上传收款信息 + path('tixian', TixianShenqingView.as_view(), name='提现申请'), # 提交提现申请 + path('tixianjiluhq', TixianJiluHuoquViewV2.as_view(), name='打手提现记录查询'), + path('adlog', AdminLoginView.as_view(), name='管理元登录'), + path('adhq', AdminFinancialDataView.as_view(), name='管理元获取收支记录'), + path('adgetordertypes', AdGetOrderTypes.as_view(), name='管理员获取订单类型'), + + # 订单列表获取接口 + path('adddhq', AdGetDingdanList.as_view(), name='管理员获取订单列表'), + path('adyqds', AdYaoQingDaShou.as_view(), name='管理员获取邀请码'), + + # 用户管理列表接口 + path('adgl', AdGuanLiYongHu.as_view(), name='管理员获取用户'), + path('cfgl', CfGuanLi.as_view(), name='处罚记录获取'), + path('adtycf', AdTongYiChuFa.as_view(), name='同意或拒绝处罚'), + path('adckyhxq', AdckyhxqView.as_view(), name='管理员获取用户具体数据'), + + # 修改用户信息接口 + path('adcjxg',AdcjxgView.as_view(), name='管理员修改用户数据'), + path('addtxsh', AddtxshView.as_view(), name='管理员获取提现审核'), + + # 处理提现申请接口 + path('adtixianqk', AdtixianqkView.as_view(), name='管理员提现修改'), + # 打手资料获取 + path('dszjxxhq', DashouZiliaoHuoquView.as_view(), name='打手获取自己信息'), + + # 打手信息更新 + path('dsgxxx', DashouGengxinView.as_view(), name='打手修改自己信息'), + path('shangjiahuce', ShangjiaZhuceView.as_view(), name='商家注册'), + path('paihangbang', PaihangbangGuanliQueryView.as_view(), name='管理查询排行榜'), + path('adhqphrq', PaihangbangRiqiliebiaoView.as_view(), name='管理原获取排行榜日期'), + + + path('wdlyhdl', WechatLoginAndDashouRegisterView.as_view(), name='打手扫码注册'), + path('yonghutxshxg', YonghuTixianShenheXiugaiView.as_view(), name='用户自己体现修改'), + path('dshqcfjl', ChufaJiluHuoquView.as_view(), name='打手处罚记录获取'), + # 打手申诉接口 + path('dscfss', DashouShensuView.as_view(), name='打手处罚上诉申请'), + path('sjcfjlhq', ShangjiaChufaJiluHuoquView.as_view(), name='商家处罚记录获取'), + path('officialcallback', csrf_exempt(WeixinOfficialCallbackView.as_view()), name='用户关注取关服务号回调'), + + + + path('kefujinru', KefuLoginView.as_view(), name='客服登录'), + path('kfjrhqzl', KefuStatsView.as_view(), name='客服获取数据'), + path('kfhqptddlx', KefuGetOrderTypesView.as_view(), name='客服获取订单类型'), + path('kfhqddlb', KefuGetOrderListView.as_view(), name='客服获取订单列表'), + + path('kfhqsjddlb', KefuGetShangjiaOrderListView.as_view(),name='客服获取商家订单列表'), + path('kefuhqddxq', KefuGetOrderDetailView.as_view(), name='获取订单详情页面'), + path('kefughds', KefuChangeDashouView.as_view(), name='客服更换打手'), + path('kefuhf', KefuRecoverOrderView.as_view(), name='客服恢复订单'), + path('kefupttk', KefuPlatformRefundView.as_view(), name='客服给平台订单退款'), + path('kefujjtk', KefuRejectRefundView.as_view(), name='客服拒绝商家退款'), + path('kefusjtk', KefuMerchantRefundView.as_view(), name='客服同意商家退款'), + path('kefujjjs', KefuRejectSettlementView.as_view(), name='客服拒绝结算'), + path('kefuzydt', KefuTransferHallView.as_view(), name='客服订单返回大厅'), + path('kefuqxzd', KefuCancelDesignationView.as_view(), name='客服取消指定'), + path('kefuhqdslb', KefuGetDashouListView.as_view(), name='客服获取打手数据'), + path('kefuhqdsxq', KefuGetDashouDetailView.as_view(), name='客服获取打手具体信息'), + path('kefuxgds', KefuUpdateDashouView.as_view(), name='客服修改打手数据'), + path('kefu_cfgl', KefuPunishmentListView.as_view(), name='客服获取处罚记录'), + path('kefu_cf_detail', KefuPunishmentDetailView.as_view(), name='kefu_cf_detail'), + path('kefu_cf_action', KefuPunishmentActionView.as_view(), name='客服处理处罚'), + path('kefuqiangzhijiedan', KefuForceCompleteView.as_view(), name='客服强制结单'), + + + path('kefu_txsh_list', KefuWithdrawListView.as_view(), name='客服获取体现数据'), + path('kefu_txsh_detail', KefuWithdrawDetailView.as_view(), name='客服获取体现详情数据'), + + path('kefu_txsh_action', KefuWithdrawActionView.as_view(), name='客服处理体现'), + path('kefuchufa', KefuPunishView.as_view(), name='客服申请出发打手'), + path('adkfgl', AdKfglView.as_view(), name='管理员获取客服数据'), + path('adkftj', AdKftjView.as_view(), name='管理添加客服'), + path('adkfxg', AdKfxgView.as_view(), name='管理修改客服'), + + # 已登录用户注册管事 + #path('guanshizhuce', GuanshiRegisterView.as_view(), name='管事注册,已经登陆'), + path('guanshizhuce', GuanshiRegisterView.as_view(), name='管事注册,已经登陆'), + + # 未登录用户微信登录+管事注册 + path('zuzhangyqmzc', WechatLoginAndGuanshiRegisterView.as_view(), name='管事注册,没有登录'), + + path('zuzhangzhuce', ZuzhangZhuceView.as_view(), name='组长注册'), + path('zuzhangxinxi', ZuzhangXinxiView.as_view(), name='组长刷新获取信息'), + + path('hqwdyqr', HuoQuYaoQingRenView.as_view(), name='打手联系获取管事'), + + path('dshqltqx', DashouJianquanView.as_view(), name='前端鉴权打手是否有使用消息功能'), # 打手鉴权 + path('lbhqltqx', LaobanJianquanView.as_view(), name='鉴权老板是否有使用消息功能'), # 老板鉴权 + + + # 获取罚款 + 积分处罚综合统计 + path('cffktjhq', FaKuanJiFenTongJiView.as_view(), name='获取罚款 + 积分处罚综合统计'), + + # 获取打手自己的罚款列表 + path('dsfklbhq', DaShouFaKuanLieBiaoView.as_view(), name='获取打手自己的罚款列表'), + + # 提交罚款申诉 + path('fkss', FaKuanShenSuView.as_view(), name='提交罚款申诉'), + + + path('fkjn', FaKuanPayView.as_view(), name='提交罚款微信支付申请'), # 下单 + path('fk_huitiao', csrf_exempt(FaKuanHuitiaoView.as_view()), name='提交罚款微信支付回调'), # 回调 + path('fkzffc', FaKuanPayPollView.as_view(), name='提交罚款申微信支付成功轮询'), # 轮询 + path('fkzfsb', FaKuanPayFailView.as_view(), name='提交罚款微信支付失败'), # 失败 + + path('khzf', KaohePayView.as_view(), name='考核支付下单'), + path('kh_huitiao', csrf_exempt(KaohePayCallbackView.as_view()), name='考核支付回调'), + path('khzffc', KaohePayPollView.as_view(), name='考核支付复查轮询'), + path('khzfsb', KaohePayFailView.as_view(), name='考核支付失败'), + + path('tixianzichan', TixianAssetView.as_view(), name='小程序提现数据获取'), + path('guanshi_contact', GuanshiContactView.as_view(), name='获取组长'), + + + + path('zddksh', TixianZddkshApplyView.as_view(), name='自动提现审核申请'), + path('tixiansq', TixianShenqingV3View.as_view(), name='自动提现收款'), + path('tixianqr', TixianQueRenAutoView.as_view(), name='自动提现确认'), + path('tixiancallback', csrf_exempt(TixianCallbackV3View.as_view()), name='提现回调'), + path('phbhqsj', PhbHqsjView.as_view(), name='排行榜数据'), + + + + + + +] \ No newline at end of file diff --git a/yonghu/views.py b/yonghu/views.py new file mode 100644 index 0000000..d7e09b3 --- /dev/null +++ b/yonghu/views.py @@ -0,0 +1,16051 @@ +# 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 +from xml.etree import 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') + + + +'''class WeixinOfficialCallbackView(APIView): + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + """ + 微信服务号回调接口 + 处理用户关注、取消关注等事件 + """ + + 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"收到服务号事件: {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) + + return HttpResponse('success') + + except Exception as e: + logger.error(f"处理服务号事件异常: {str(e)}") + 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 + + if unionid: + # 保存或更新关联 + OfficialAccountUser.objects.update_or_create( + unionid=unionid, + defaults={ + 'official_openid': from_user, + 'is_subscribed': True + } + ) + logger.info(f"用户关注成功: unionid={unionid}") + else: + logger.warning(f"用户关注但未获取到UnionID") + + # 返回欢迎消息 + return self.generate_text_response(root, "感谢关注!您将收到订单通知。") + + except Exception as e: + logger.error(f"处理关注事件异常: {str(e)}") + return HttpResponse('success') + + def handle_unsubscribe_event(self, root, from_user): + """处理用户取消关注事件""" + try: + # 获取UnionID + unionid_elem = root.find('UnionId') + unionid = unionid_elem.text if unionid_elem is not None else None + + if unionid: + OfficialAccountUser.objects.filter(unionid=unionid).update(is_subscribed=False) + logger.info(f"用户取消关注: unionid={unionid}") + else: + OfficialAccountUser.objects.filter(official_openid=from_user).update(is_subscribed=False) + + return HttpResponse('success') + + except Exception as e: + logger.error(f"处理取消关注事件异常: {str(e)}") + return HttpResponse('success') + + def generate_text_response(self, root, content): + """生成文本消息响应""" + 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')''' + + + + + + + + + + + + + + + + +# 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 + + + + + +'''class UserInfoUpdateView(APIView): + """ + 用户个人信息修改接口 + + 功能: + - 上传 / 更新头像 + - 修改昵称 + - 更新手机号(支持明文 phone 或微信 getPhoneNumber 组件返回的 shoujihao_code) + 当传 shoujihao_code 时,会通过微信接口解密得到真实手机号并自动设置 shoujihao_renzheng=True + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + """ + 处理 POST 请求,支持 multipart/form-data 格式 + """ + try: + # ---------- 获取当前用户 ---------- + user_main = request.user + + # ---------- 获取或创建老板扩展表记录 ---------- + try: + user_boss = UserBoss.objects.get(user=user_main) + except UserBoss.DoesNotExist: + user_boss = UserBoss.objects.create(user=user_main, nickname='微信用户') + + response_data = {} + + # ==================== 1. 处理头像上传 ==================== + avatar_file = request.FILES.get('avatar') + if avatar_file: + # 验证图片格式 / 大小 + is_valid, error_msg = validate_image(avatar_file) + if not is_valid: + return Response({ + 'code': 1, + 'msg': f'图片验证失败: {error_msg}', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 上传头像到 OSS / COS + avatar_url = self.handle_avatar_upload(user_main, avatar_file) + if avatar_url: + response_data['touxiang'] = avatar_url + user_main.avatar = avatar_url + else: + return Response({ + 'code': 2, + 'msg': '头像上传失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # ==================== 2. 处理昵称更新 ==================== + nicheng = request.data.get('nicheng') + if nicheng is not None: + nicheng = str(nicheng).strip() + if nicheng: + if len(nicheng) > 20: + return Response({ + 'code': 3, + 'msg': '昵称不能超过20个字符', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + user_boss.nickname = nicheng + response_data['nicheng'] = nicheng + + # ==================== 3. 处理手机号更新 ==================== + # 明文手机号(传统方式) + phone =None + shoujihao_code = request.data.get('shoujihao_code') # 微信手机号快速验证组件返回的临时 code + + if shoujihao_code: + # 如果前端传了 shoujihao_code,则通过微信接口解密得到真实手机号 + try: + phone = self._get_phone_by_code(shoujihao_code) + logger.info(f"手机号解密成功: {phone}") + except Exception as e: + logger.error(f"手机号解密失败: {str(e)}") + return Response({ + 'code': 6, + 'msg': f'获取手机号失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + if phone is not None: + phone = str(phone).strip() + if phone: + # 验证手机号格式(11 位数字) + if not phone.isdigit() or len(phone) != 11: + return Response({ + 'code': 4, + 'msg': '请输入11位有效的手机号码', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 更新主表手机号,并标记手机号已认证 + user_main.phone = phone + user_main.shoujihao_renzheng = True + response_data['phone'] = phone + + # ==================== 4. 检查是否有任何更新 ==================== + if not response_data: + return Response({ + 'code': 5, + 'msg': '没有需要更新的数据', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # ==================== 5. 事务保存 ==================== + with transaction.atomic(): + user_main.save() + user_boss.save() + + # ==================== 6. 返回成功 ==================== + return Response({ + 'code': 0, + 'msg': '更新成功', + 'data': response_data + }) + + except Exception as e: + logger.exception("用户信息更新异常") + return Response({ + 'code': 99, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # ==================== 原有方法:头像上传 ==================== + def handle_avatar_upload(self, user_main, avatar_file): + """ + 上传头像到 OSS / COS,并返回存储的相对路径 + """ + try: + yonghuid = user_main.yonghuid + if not yonghuid: + logger.warning("用户 ID 为空,无法上传头像") + return None + + # 获取旧头像,用于后续删除 + old_avatar_url = user_main.avatar + + # 生成新文件名 + file_extension = os.path.splitext(avatar_file.name)[1].lower() + if not file_extension: + file_extension = '.jpg' + unique_filename = f"{uuid.uuid4().hex}{file_extension}" + + # OSS 文件路径:avatar/{yonghuid}/{filename} + avatar_folder = 'avatar' + oss_file_path = f"{avatar_folder}/{yonghuid}/{unique_filename}" + + # 删除旧头像(如果存在) + if old_avatar_url and old_avatar_url.strip(): + delete_from_oss(old_avatar_url) + + # 上传新头像 + new_avatar_url = upload_to_oss(avatar_file, oss_file_path) + if new_avatar_url: + logger.info(f"头像上传成功: {oss_file_path}") + return oss_file_path + else: + logger.error("头像上传失败,upload_to_oss 返回 None") + return None + + except Exception as e: + logger.exception("处理头像上传失败") + return None + + # ==================== 新增方法:微信接口相关 ==================== + + def _get_access_token(self): + """ + 获取微信小程序全局 access_token,带缓存 + access_token 有效期 7200 秒,提前 300 秒过期 + """ + import requests + from django.core.cache import cache + + # 尝试从缓存中读取 + token = cache.get('wechat_access_token') + if token: + return token + + # 从 settings 读取 AppID 和 AppSecret + appid = getattr(settings, 'WEIXIN_APPID', None) + secret = getattr(settings, 'WEIXIN_SECRET', None) + if not appid or not secret: + raise ValueError( + 'settings.py 中缺少 WECHAT_APPID 或 WECHAT_APPSECRET,' + '请添加:\n' + "WECHAT_APPID = 'wx0e4be86faac4a8d1'\n" + "WECHAT_APPSECRET = '你的小程序密钥'" + ) + + url = 'https://api.weixin.qq.com/cgi-bin/token' + params = { + 'grant_type': 'client_credential', + 'appid': appid, + 'secret': secret + } + try: + resp = requests.get(url, params=params) + data = resp.json() + except Exception as e: + raise Exception(f'请求微信 access_token 接口异常: {str(e)}') + + if 'access_token' in data: + token = data['access_token'] + # 缓存时间设 7100 秒,提前 5 分钟过期 + cache.set('wechat_access_token', token, timeout=7100) + logger.info("成功获取微信 access_token") + return token + else: + errcode = data.get('errcode') + errmsg = data.get('errmsg') + logger.error(f"获取 access_token 失败: errcode={errcode}, errmsg={errmsg}") + raise Exception(f"获取 access_token 失败: {errmsg}") + + def _get_phone_by_code(self, code): + """ + 通过微信手机号快速验证组件返回的 code 换取真实手机号 + 接口文档:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html + """ + import requests + + # 先获取有效的 access_token + access_token = self._get_access_token() + + # 构建请求 URL + url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={access_token}' + + # 请求体:code 必须放在 JSON 中 + payload = {'code': code} + + try: + resp = requests.post(url, json=payload, timeout=10) + data = resp.json() + except Exception as e: + raise Exception(f'请求微信手机号解密接口异常: {str(e)}') + + errcode = data.get('errcode') + if errcode == 0: + phone_info = data.get('phone_info', {}) + pure_phone = phone_info.get('purePhoneNumber') + if not pure_phone: + raise Exception('手机号为空,可能用户未授权真实手机号') + return pure_phone + else: + errmsg = data.get('errmsg') + logger.error(f"手机号解密失败: errcode={errcode}, errmsg={errmsg}") + raise Exception(f'微信接口错误 (errcode={errcode}): {errmsg}')''' + + + +# 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 ShangJiaXinXiView(APIView): + """ + 获取商家信息接口 (POST /yonghu/shangjiaxinxi) + 权限:需JWT认证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 关键:使用 get() 方法。如果反向关联不存在,会抛出 UserShangjia.DoesNotExist 异常。 + shangjia = request.user.shop_profile + # 如果反向关系存在,但你想更显式地查询,也可以这样写(效果相同): + # shangjia = UserShangjia.objects.get(user=request.user) + + except ObjectDoesNotExist: + # 捕获“不存在”异常,说明当前用户还没有商家扩展资料 + return Response({ + 'code': 4001, # 你可以定义特定的业务错误码 + 'msg': '您还未注册成为商家或商家资料不存在', + 'data': None + }, status=status.HTTP_200_OK) # 业务错误,HTTP状态码通常仍用200 + + # 构建返回给前端的数据 + response_data = { + 'sjzhzhuangtai': shangjia.zhuangtai, # 商家账号状态 + 'fadanzong': shangjia.fabu, # 发单总量 + 'tuikuanzong': shangjia.tuikuan, # 退款总量 + 'sjyue': float(shangjia.yue), # 商家余额 + 'riliushui': float(shangjia.jinriliushui), # 今日流水 + 'yueliushui': float(shangjia.jinyueliushui), # 今月流水 + } + + return Response({ + 'code': 200, + 'msg': '获取商家信息成功', + 'data': response_data + })''' + + + + +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. 验证管事状态 + '''if guanshi_profile.zhuangtai != 1: + return Response({ + 'code': 403, + 'message': '该邀请码对应的管事账号已被禁用', + 'data': None + }, status=status.HTTP_403_FORBIDDEN)''' + + # 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 创建商家扩展表 + # 尝试从老板表获取昵称,没有则用默认 + '''boss_nickname = '普通商家' + try: + boss_profile = current_user.boss_profile + if boss_profile.nickname: + boss_nickname = boss_profile.nickname + except: + pass + + # 商家昵称需要唯一,这里用 用户ID + “的店铺” 来确保唯一性 + shangjia_nicheng = f"{current_user.yonghuid}的店铺" + shop_profile = UserShangjia.objects.create( + user=current_user, + nicheng=shangjia_nicheng, + # 其他字段使用默认值 + )''' + + # 5.3 创建管事扩展表 + # 生成管事的邀请码 + '''guanshi_yaoqingma = chuangjianYaoqingma(str(current_user.yonghuid)) + # 如果生成失败,guanshi_yaoqingma可能为None或空,根据你的算法调整 + new_guanshi_profile = UserGuanshi.objects.create( + user=current_user, + yaoqingma=guanshi_yaoqingma, + # 其他字段使用默认值 + )''' + + # 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) +'''class TixianXinxiHuoquView(APIView): + """ + 获取用户提现收款信息接口 + 返回用户已设置的手机号、收款码、收款账号 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + """ + 处理获取收款信息的POST请求 + 直接返回用户当前的收款设置 + """ + try: + # 从request.user获取当前用户 + user_main = request.user + + # 构建返回数据 + 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 '' + } + + # 返回成功响应 + 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 + + +'''class TixianShenqingView(APIView): + """ + 用户提现申请接口(修复版) + 支持打手佣金提现、管事分红提现、组长分红提现 + 严格校验所有业务条件,修复400错误问题 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + """ + 处理提现申请的POST请求 + 加强参数验证和错误处理 + """ + try: + # 获取当前用户 + user_main = request.user + yonghuid = user_main.yonghuid + + # 1. 获取并严格验证前端参数 + leixing = request.data.get('leixing') # 提现类型:1-佣金 2-分红 3-组长分红 + jine = request.data.get('jine') + fangshi = request.data.get('fangshi') + txskfs = request.data.get('txskfs', 0) + + # ==================== 【新增】罚单检查 ==================== + from utils.fadan_utils import check_fadan_qiangdan_eligible + fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(request.user.yonghuid, 2) + if not fadan_ok: + return Response({'code': 400, 'msg': fadan_msg}) + # ========================================================= + + # 验证必需参数是否存在 + missing_params = [] + if leixing is None: + missing_params.append('leixing') + if jine is None: + missing_params.append('jine') + if fangshi is None: + missing_params.append('fangshi') + + if missing_params: + return Response({ + 'code': 1, + 'msg': f'缺少必需参数: {", ".join(missing_params)}', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证参数类型和格式 + try: + leixing = int(leixing) + if isinstance(jine, str): + jine = decimal.Decimal(jine) + else: + 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)}', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证提现金额必须大于0 + if jine <= decimal.Decimal('0'): + return Response({ + 'code': 3, + 'msg': '提现金额必须大于0', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证提现类型有效性(新增类型3) + if leixing not in [1, 2, 3,4]: + return Response({ + 'code': 4, + 'msg': '无效的提现类型,只能是1(佣金)、2(分红)或3(组长分红)', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证收款方式有效性 + if fangshi not in [1, 2]: + return Response({ + 'code': 5, + 'msg': '无效的收款方式,只能是1(微信)或2(支付宝)', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 🔥【组长支持】获取对应费率(增加组长) + current_rate = decimal.Decimal('0.00') + if leixing == 1: + dashou_rate_obj = Lilubiao.objects.filter(fadanpingtai='5').first() + if dashou_rate_obj: + current_rate = decimal.Decimal(str(dashou_rate_obj.lilu)) + elif leixing == 2: + guanshi_rate_obj = Lilubiao.objects.filter(fadanpingtai='6').first() + if guanshi_rate_obj: + current_rate = decimal.Decimal(str(guanshi_rate_obj.lilu)) + elif leixing == 3: + # 组长提现费率 (fadanpingtai='8') + zuzhang_rate_obj = Lilubiao.objects.filter(fadanpingtai='8').first() + if zuzhang_rate_obj: + current_rate = decimal.Decimal(str(zuzhang_rate_obj.lilu)) + else: + return Response({ + 'code': 31, + 'msg': '组长费率未配置,无法提现', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + elif leixing == 4: + # 审核官提现费率 (fadanpingtai='9',数字可自定义) + kaoheguan_rate_obj = Lilubiao.objects.filter(fadanpingtai='9').first() + if kaoheguan_rate_obj: + current_rate = decimal.Decimal(str(kaoheguan_rate_obj.lilu)) + else: + return Response({ + 'code': 41, + 'msg': '审核官费率未配置,无法提现', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 计算手续费和实际到账金额 + shouxufei = jine * current_rate + shijidaozhang = jine - shouxufei + + # 验证手续费和实际到账金额的有效性 + if shijidaozhang <= decimal.Decimal('0'): + return Response({ + 'code': 6, + 'msg': '提现金额过低,扣除手续费后无实际到账', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 根据提现类型进行不同的校验 + if leixing == 1: + # 佣金提现 - 打手 + result = self.yanzheng_dashou_tixian(user_main, jine) + if result['code'] != 0: + return Response(result, status=status.HTTP_400_BAD_REQUEST) + + dashou_info = result['data'] + nicheng = dashou_info.get('nicheng', '') + + # 使用事务更新打手余额 + try: + with transaction.atomic(): + dashou_profile = UserDashou.objects.select_for_update().get(user=user_main) + if dashou_profile.yue < jine: + return Response({ + 'code': 13, + 'msg': '余额不足,无法提现', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + dashou_profile.yue = F('yue') - jine + dashou_profile.save() + dashou_profile.refresh_from_db() + except Exception as e: + return Response({ + 'code': 99, + 'msg': f'更新余额失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + elif leixing == 2: + # 分红提现 - 管事 + result = self.yanzheng_guanshi_tixian(user_main, jine) + if result['code'] != 0: + return Response(result, status=status.HTTP_400_BAD_REQUEST) + + nicheng = "管事用户" + + try: + with transaction.atomic(): + guanshi_profile = UserGuanshi.objects.select_for_update().get(user=user_main) + if guanshi_profile.yue < jine: + return Response({ + 'code': 22, + 'msg': '余额不足,无法提现', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + guanshi_profile.yue = F('yue') - jine + guanshi_profile.save() + guanshi_profile.refresh_from_db() + except Exception as e: + return Response({ + 'code': 99, + 'msg': f'更新余额失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 🔥【组长支持】新增组长提现分支 + elif leixing == 3: + # 组长分红提现 + result = self.yanzheng_zuzhang_tixian(user_main, jine) + if result['code'] != 0: + return Response(result, status=status.HTTP_400_BAD_REQUEST) + + zuzhang_info = result['data'] + nicheng = zuzhang_info.get('nicheng', '') + + try: + with transaction.atomic(): + zuzhang_profile = UserZuzhang.objects.select_for_update().get(user=user_main) + if zuzhang_profile.ketixian_jine < jine: + return Response({ + 'code': 32, + 'msg': '组长可提现金额不足', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + # 扣除申请金额 + zuzhang_profile.ketixian_jine = F('ketixian_jine') - jine + zuzhang_profile.save() + zuzhang_profile.refresh_from_db() + except Exception as e: + return Response({ + 'code': 99, + 'msg': f'更新组长余额失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + # 🔥【审核官支持】新增审核官提现分支 + elif leixing == 4: + # 审核官分佣提现 + result = self.yanzheng_kaoheguan_tixian(user_main, jine) + if result['code'] != 0: + return Response(result, status=status.HTTP_400_BAD_REQUEST) + + kaoheguan_info = result['data'] + nicheng = kaoheguan_info.get('nicheng', '') + + try: + with transaction.atomic(): + kaoheguan_profile = UserShenheguan.objects.select_for_update().get(user=user_main) + if kaoheguan_profile.yue < jine: + return Response({ + 'code': 42, + 'msg': '审核官可提现金额不足', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + # 扣除申请金额 + kaoheguan_profile.yue = F('yue') - jine + kaoheguan_profile.save() + kaoheguan_profile.refresh_from_db() + except Exception as e: + return Response({ + 'code': 99, + 'msg': f'更新审核官余额失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + # 3. 创建提现记录 + try: + avatar = user_main.avatar if user_main.avatar else '' + phone = user_main.phone if user_main.phone else '' + zhifu = user_main.zhifu if user_main.zhifu else '' + skzhanghao = user_main.skzhanghao if user_main.skzhanghao else '' + + # 对于打手或组长,从扩展表获取昵称 + if leixing == 1: + try: + dashou_profile = UserDashou.objects.get(user=user_main) + nicheng = dashou_profile.nicheng if dashou_profile.nicheng else '' + except UserDashou.DoesNotExist: + nicheng = '' + elif leixing == 3: + # 组长昵称从组长扩展表获取(如果需要) + try: + zuzhang_profile = UserZuzhang.objects.get(user=user_main) + # 组长扩展表可能没有昵称字段,可以从UserMain获取 + nicheng = user_main.nicheng if hasattr(user_main, 'nicheng') else '' + except UserZuzhang.DoesNotExist: + nicheng = '' + + tixian_jilu = 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='', + ) + + # 4. 返回成功响应 + return Response({ + 'code': 0, + 'msg': '提现申请提交成功', + 'data': { + 'tixian_id': tixian_jilu.id, + 'leixing': leixing, + 'jine': str(jine.quantize(decimal.Decimal('0.01'))), + 'fangshi': fangshi, + 'zhuangtai': 1, + 'create_time': tixian_jilu.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)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + 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 yanzheng_kaoheguan_tixian(self, user_main, jine): + """ + 校验审核官分佣提现条件 + """ + try: + # 查询审核官扩展表 + try: + kaoheguan_profile = UserShenheguan.objects.get(user=user_main) + except UserShenheguan.DoesNotExist: + return { + 'code': 40, + 'msg': '您不是审核官身份,无法提现分佣', + 'data': None + } + + # 校验1: 审核官账号状态 (zhuangtai != 1) + if kaoheguan_profile.zhuangtai != 1: + return { + 'code': 41, + 'msg': '审核官账号已被封禁,无法提现', + 'data': None + } + + # 校验2: 可提现金额是否足够 (yue < jine) + if kaoheguan_profile.yue < jine: + return { + 'code': 42, + 'msg': f'审核官可提现金额不足,当前可提: {kaoheguan_profile.yue},申请金额: {jine}', + 'data': None + } + + # 所有校验通过 + return { + 'code': 0, + 'msg': '校验通过', + 'data': { + 'nicheng': '审核官', # 可根据需要调整 + 'yue': kaoheguan_profile.yue + } + } + + except Exception as e: + return { + 'code': 99, + 'msg': f'审核官提现校验过程出错: {str(e)}', + 'data': None + } + + def yanzheng_dashou_tixian(self, user_main, jine): + """ + 校验打手佣金提现条件(修复版) + 修正了状态判断逻辑 + """ + try: + # print(f"开始校验打手提现 - 用户: {user_main.yonghuid}, 金额: {jine}") + + # 查询打手扩展表 + try: + dashou_profile = UserDashou.objects.get(user=user_main) + # print( + # f"打手信息 - 状态: {dashou_profile.zhuangtai}, 账号状态: {dashou_profile.zhanghaozhuangtai}, 积分: {dashou_profile.jifen}, 余额: {dashou_profile.yue}") + except UserDashou.DoesNotExist: + # print("打手扩展表记录不存在") + return { + 'code': 10, + 'msg': '您不是打手身份,无法提现佣金', + 'data': None + } + + # 校验1: 打手账号状态 (zhanghaozhuangtai != 1 则封禁) + if dashou_profile.zhanghaozhuangtai != 1: + return { + 'code': 11, + 'msg': '打手账号已被封禁,无法提现', + 'data': None + } + + # 校验2: 打手接单状态 (zhuangtai != 1 则有订单进行中) - 修复这里 + if dashou_profile.zhuangtai != 1: + return { + 'code': 12, + 'msg': '您有订单进行中,请完成后提现', + 'data': None + } + + # 校验3: 积分检查 (jifen != 10 则不足) - 新增 + if dashou_profile.jifen != 10: + return { + 'code': 15, # 使用新的错误码 + 'msg': '积分不足10分,请补充积分后提现', + 'data': None + } + + # 校验4: 余额是否足够 (yue < jine) + if dashou_profile.yue < jine: + return { + 'code': 13, + 'msg': f'余额不足,当前余额: {dashou_profile.yue},提现金额: {jine}', + 'data': None + } + + # print("打手提现校验通过") + + # 所有校验通过 + return { + 'code': 0, + 'msg': '校验通过', + 'data': { + 'nicheng': dashou_profile.nicheng if dashou_profile.nicheng else '', + 'yue': dashou_profile.yue, + 'jifen': dashou_profile.jifen + } + } + + except Exception as e: + # print(f"打手提现校验异常: {e}") + return { + 'code': 99, + 'msg': f'校验过程出错: {str(e)}', + 'data': None + } + + def yanzheng_guanshi_tixian(self, user_main, jine): + """ + 校验管事分红提现条件 + """ + try: + # print(f"开始校验管事提现 - 用户: {user_main.yonghuid}, 金额: {jine}") + + # 查询管事扩展表 + try: + guanshi_profile = UserGuanshi.objects.get(user=user_main) + # print(f"管事信息 - 状态: {guanshi_profile.zhuangtai}, 余额: {guanshi_profile.yue}") + except UserGuanshi.DoesNotExist: + # print("管事扩展表记录不存在") + return { + 'code': 20, + 'msg': '您不是管事身份,无法提现分红', + 'data': None + } + + # 校验1: 管事账号状态 (zhuangtai != 1) + if guanshi_profile.zhuangtai != 1: + return { + 'code': 21, + 'msg': '管事账号已被封禁,无法提现', + 'data': None + } + + # 校验2: 余额是否足够 (yue < jine) + if guanshi_profile.yue < jine: + return { + 'code': 22, + 'msg': f'余额不足,当前余额: {guanshi_profile.yue},提现金额: {jine}', + 'data': None + } + + # print("管事提现校验通过") + + # 所有校验通过 + return { + 'code': 0, + 'msg': '校验通过', + 'data': { + 'yue': guanshi_profile.yue + } + } + + except Exception as e: + # print(f"管事提现校验异常: {e}") + return { + 'code': 99, + 'msg': f'校验过程出错: {str(e)}', + 'data': None + } + + + # 🔥【组长支持】新增组长校验函数 + def yanzheng_zuzhang_tixian(self, user_main, jine): + """ + 校验组长分红提现条件 + """ + try: + # 查询组长扩展表 + try: + zuzhang_profile = UserZuzhang.objects.get(user=user_main) + except UserZuzhang.DoesNotExist: + return { + 'code': 30, + 'msg': '您不是组长身份,无法提现组长分红', + 'data': None + } + + # 校验1: 组长账号状态 (zhuangtai != 1) + if zuzhang_profile.zhuangtai != 1: + return { + 'code': 31, + 'msg': '组长账号已被封禁,无法提现', + 'data': None + } + + # 校验2: 可提现金额是否足够 + if zuzhang_profile.ketixian_jine < jine: + return { + 'code': 32, + 'msg': f'组长可提现金额不足,当前可提: {zuzhang_profile.ketixian_jine},申请金额: {jine}', + 'data': None + } + + # 所有校验通过 + return { + 'code': 0, + 'msg': '校验通过', + 'data': { + 'nicheng': user_main.nicheng if hasattr(user_main, 'nicheng') else '', + 'ketixian_jine': zuzhang_profile.ketixian_jine + } + } + + except Exception as e: + return { + 'code': 99, + 'msg': f'组长提现校验过程出错: {str(e)}', + 'data': None + }''' + + + + + +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): + """ + 提现记录获取接口(优化版) + 使用更高效的查询方式,减少数据库压力 + """ + 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) + + # 参数验证 + 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: + return Response({ + 'code': 1, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 限制最大查询数量 + if limit > 100: + limit = 100 + + # 计算偏移量 + offset = (page - 1) * limit + + # 🔴【修改开始】添加必要字段:id, phone, skzhanghao, zhifu + records_queryset = Tixianjilu.objects.filter( + yonghuid=yonghuid + ).order_by('-create_time').values( + 'id', # 🔴【关键】添加ID字段 + 'leixing', + 'jine', + 'zhuangtai', + 'create_time', + 'fangshi', + 'phone', # 🔴【新增】手机号 + 'skzhanghao', # 🔴【新增】收款账号 + 'zhifu' , # 🔴【新增】收款码图片 + 'bhliyou' + )[offset:offset + limit + 1] # 🔴【修复】使用limit + 1 + + # 转换为列表 + records_list = list(records_queryset) + records_count = len(records_list) + + # 判断是否有更多数据 + has_more = records_count > limit + + # 如果查询到的数量大于limit,只返回limit条 + if has_more: + records_to_return = records_list[:limit] + else: + records_to_return = records_list + + # 格式化数据 + formatted_records = [] + for record in records_to_return: + formatted_records.append({ + 'tixian_id': record['id'], # 🔴【关键】使用tixian_id作为字段名 + 'leixing': record['leixing'], + 'jine': float(record['jine']) if record['jine'] else 0.00, + '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 '', + }) + + # 构建响应 + response_data = { + 'list': formatted_records, + 'has_more': has_more, + 'page': page, + 'limit': limit, + 'current_count': len(formatted_records) + } + + 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 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个字符(保持兼容性) + '''if len(kehuduan_ip) > 11: + # 如果ip字段长度限制是11,我们截取前11个字符 + # 建议在数据库中修改ip字段为50长度 + cunchu_ip = kehuduan_ip[:11] + else: + cunchu_ip = kehuduan_ip''' + 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 + ) + + + + + +'''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=0).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]) + + # 14. 构建返回数据 + chufa_data = [] + for chufa in chufa_list: + 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 + }) + + # 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=0 # 只查询待处理状态的记录 + ).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 != 0: + return Response( + {'code': 400, 'message': '该处罚记录已处理', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 10. 🔴 修复:检查是否有重复的待处理记录,并记录警告 + duplicate_count = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + sqzhuangtai=0 + ).count() + + if duplicate_count > 1: + # 记录警告日志 + logger.warning(f"订单ID {dingdan_id} 存在 {duplicate_count} 条待处理处罚记录,处理最新的一条") + + # 🔴 可选:将其他重复的待处理记录标记为已驳回 + # 防止用户重复提交相同订单的处罚申请 + duplicate_records = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + sqzhuangtai=0 + ).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 扣除打手积分(确保积分不为负数) + jifen_kouchu = chufajilu.jifen + if jifen_kouchu > 0: + # 检查积分是否足够 + if dashou.jifen < jifen_kouchu: + # 如果积分不足,设置为0 + dashou.jifen = 0 + else: + dashou.jifen -= jifen_kouchu + dashou.save() + + # 11.4 更新处罚记录 + chufajilu.sqzhuangtai = 1 # 已处罚 + chufajilu.chuliid = qianzhanghao # 处理人ID为管理员账号 + chufajilu.save() + + # 11.5 更新商家订单扩展表 + 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},扣除积分={jifen_kouchu}") + + elif caozuo == 2: + # 驳回处罚 + # 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 + )''' + + + + +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)) + + if huiyuan_id and days > 0: + if days < 1 or 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: + # 续费:延长到期时间 + if existing_record.daoqi_time < timezone.now(): + # 已过期,从当前时间开始算 + new_daoqi_time = timezone.now() + timedelta(days=days) + else: + # 未过期,从原到期时间开始算 + new_daoqi_time = existing_record.daoqi_time + timedelta(days=days) + + 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 + modify_record_data['qitashuoming'] += f' 续费会员 {huiyuan.jieshao} {days}天' + else: + # 新购买 + 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()''' + + # 🔥🔥🔥 后端修改:支持正负数会员天数 + # 修改会员处理逻辑部分 + + 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) + +'''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) + + # 更新提现记录 + withdrawal.zhuangtai = result + withdrawal.shenheid = request.user.yonghuid + + if result == 3: # 拒绝 + withdrawal.bhliyou = reason + + withdrawal.update_time = timezone.now() + withdrawal.save() + + 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) + + # 验证管事状态 + '''if guanshi_profile.zhuangtai != 1: + # 管事账号已被禁用 + return self.fanhuiZhihouDengluXinxi(user_main, guanshi_jinyong=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, + # 其他字段使用模型默认值 + ) + + # 创建商家扩展表 + # 商家昵称需要唯一,这里用 用户ID + "的店铺" 来确保唯一性 + '''shangjia_nicheng = f"{user_main.yonghuid}的店铺" + UserShangjia.objects.create( + user=user_main, + nicheng=shangjia_nicheng, + # 其他字段使用默认值 + )''' + + # 创建管事扩展表 + '''guanshi_yaoqingma = chuangjianYaoqingma(str(user_main.yonghuid)) + new_guanshi_profile = UserGuanshi.objects.create( + user=user_main, + yaoqingma=guanshi_yaoqingma, + # 其他字段使用默认值 + )''' + + # 更新原管事的邀请数量 + 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__) + +'''class KefuGetOrderListView(APIView): + """ + 客服获取平台订单列表接口(发单平台=1) + 请求参数: + phone: 客服账号 + page: 页码 + pageSize: 每页条数 + dingdan_id: 订单ID(精确搜索) + laoban_id: 老板ID(搜索平台扩展表中的老板) + dashou_id: 打手ID(搜索主表中的接单打手) + leixing: 商品类型ID + zhuangtai: 状态(多个用逗号分隔,如 "1,7") + date: 日期(YYYY-MM-DD),筛选订单创建时间 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + start_total = time.time() + + phone = request.data.get('phone', '').strip() + 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() # 日期筛选 + + if not phone: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + 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 '002ab' not in permissions: + return Response({'code': 403, 'msg': '您无权查看平台订单列表'}) + + # ---------- 查询条件 ---------- + base_q = Q(fadan_pingtai=1) # 平台订单 + + 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) # 按日期筛选 + + # ---------- 统计(仅按发单平台) ---------- + stats = Dingdan.objects.filter(fadan_pingtai=1).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).count(), + '1,7': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai__in=[1,7]).count(), + '2': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=2).count(), + '8': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=8).count(), + '3': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=3).count(), + '4': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=4).count(), + '5': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=5).count(), + '6': Dingdan.objects.filter(fadan_pingtai=1, 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}) + + + + + + + +logger = logging.getLogger(__name__) + +class KefuGetShangjiaOrderListView(APIView): + """ + 客服获取商家订单列表接口(发单平台=2) + 请求参数: + phone: 客服账号 + page: 页码 + pageSize: 每页条数 + dingdan_id: 订单ID(精确搜索) + shangjia_id: 商家ID(搜索商家扩展表中的商家ID) + dashou_id: 打手ID(搜索主表中的接单打手) + leixing: 商品类型ID + zhuangtai: 状态(多个用逗号分隔,如 "1,7") + date: 日期(YYYY-MM-DD),筛选订单创建时间 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + start_total = time.time() + + phone = request.data.get('phone', '').strip() + 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() # 格式 YYYY-MM-DD + + if not phone: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + 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. 检查商家订单管理权限(使用 002ac,如果没有可改用 002ab) + if '002ac' not in permissions: + return Response({'code': 403, 'msg': '您无权查看商家订单列表'}) + + # ---------- 查询条件 ---------- + base_q = Q(fadan_pingtai=2) # 商家订单 + + 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) # 按日期筛选 + + # ---------- 统计(仅按发单平台) ---------- + stats = Dingdan.objects.filter(fadan_pingtai=2).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).count(), + '1,7': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai__in=[1,7]).count(), + '2': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=2).count(), + '8': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=8).count(), + '3': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=3).count(), + '4': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=4).count(), + '5': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=5).count(), + '6': Dingdan.objects.filter(fadan_pingtai=2, 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' # 商家ID + ).order_by('-create_time')[(page-1)*page_size : page*page_size] + + # ---------- 获取商家昵称(如果有) ---------- + # 假设商家昵称在 UserShangjia 扩展表中,通过 shangjia_id 关联 UserMain,再关联 UserShangjia + # 此处简化,实际可根据需要查询 + 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 '' + # 游戏昵称从订单主表的 nicheng 取,或从老板昵称取(商家订单可能没有老板昵称,保持简单) + 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})''' + + + + + +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 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: + order = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 4. 验证订单状态为退款审核中(4) + if order.zhuangtai != 4: + return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST) + + # 5. 使用事务更新 + with transaction.atomic(): + # 5.1 更新订单状态为退款失败(6) + order.zhuangtai = 6 + order.clkf = current_user.yonghuid + if jujue_liyou: + order.tkly = jujue_liyou # 存储拒绝理由 + order.save() + + # 5.2 更新退款记录表(如果存在) + try: + refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) + refund_record.sqzhuangtai = 2 # 2表示拒绝 + refund_record.chuliid = phone + refund_record.save() + logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}") + except Tuikuanjilu.DoesNotExist: + pass + except Exception as e: + logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True) + + # 5.3 更新客服统计数据 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + return Response({'code': 0, 'msg': '拒绝退款成功'})''' + + + + + + + +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): + """ + 客服修改打手信息接口 + 支持操作: + - 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 KefuUpdateDashouView(APIView): + """ + 客服修改打手信息接口 + 请求:POST /yonghu/kefuxgds + 参数:{ + "phone": "客服手机号", + "uid": "打手ID", + // 以下字段可选,只传需要修改的字段 + "yue": 100.00, + "jifen": 200, + "yajin": 50.00, + "zaixianzhuangtai": 0/1, + "zhanghaozhuangtai": 0/1, + "zhuangtai": 1/2 + } + 认证: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: + 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: + return Response({'code': 400, 'msg': '该用户未注册打手身份'}, status=status.HTTP_400_BAD_REQUEST) + + # 4. 收集要修改的字段 + update_fields = {} + editable_fields = [ + 'yue', 'jifen', 'yajin', + 'zaixianzhuangtai', 'zhanghaozhuangtai', 'zhuangtai' + ] + + for field in editable_fields: + if field in request.data: + value = request.data.get(field) + # 类型转换 + if field in ['yue', 'yajin']: + try: + value = Decimal(str(value)) + except: + return Response({'code': 400, 'msg': f'{field} 格式错误'}, status=status.HTTP_400_BAD_REQUEST) + elif field in ['jifen']: + try: + value = int(value) + except: + return Response({'code': 400, 'msg': f'{field} 必须为整数'}, status=status.HTTP_400_BAD_REQUEST) + elif field in ['zaixianzhuangtai', 'zhanghaozhuangtai', 'zhuangtai']: + try: + value = int(value) + if field == 'zhuangtai' and value not in [1, 2]: + return Response({'code': 400, 'msg': f'{field} 值无效'}, status=status.HTTP_400_BAD_REQUEST) + if field != 'zhuangtai' and value not in [0, 1]: + return Response({'code': 400, 'msg': f'{field} 值无效'}, status=status.HTTP_400_BAD_REQUEST) + except: + return Response({'code': 400, 'msg': f'{field} 必须为数字'}, status=status.HTTP_400_BAD_REQUEST) + update_fields[field] = value + + if not update_fields: + return Response({'code': 400, 'msg': '没有要修改的字段'}, status=status.HTTP_400_BAD_REQUEST) + + # 5. 使用事务更新并记录修改日志 + with transaction.atomic(): + # 保存旧值用于记录(可选) + old_values = {field: getattr(dashou, field) for field in update_fields} + + for field, value in update_fields.items(): + setattr(dashou, field, value) + dashou.save() + + # 记录修改日志 + try: + # 构建修改描述 + desc_parts = [] + for field in update_fields: + old = old_values.get(field) + new = update_fields[field] + desc_parts.append(f"{field}: {old} -> {new}") + desc = "; ".join(desc_parts) + + Xiugaijilu.objects.create( + yonghuid=uid, + xiugaiid=phone, # 客服手机号 + leixing=2, # 打手类型 + qitashuoming=desc, + create_time=timezone.now() + ) + except Exception as e: + logger.error(f"记录修改日志失败: {e}") + # 不影响主流程,但记录错误 + + return Response({'code': 0, '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}) + + + + + +'''import time +import logging +from django.db import models +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 +from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia, Dashoutupian, Tuikuanjilu + +logger = logging.getLogger(__name__) + +class KefuGetOrderDetailView(APIView): + """ + 客服获取订单详情接口 + 请求:POST /yonghu/kefuhqddxq + 参数:{"phone": "客服手机号", "dingdan_id": "订单ID"} + 认证:JWT + 返回:code=0 + 订单详情数据 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + start_time = time.time() + + # 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: + 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 预加载扩展表,避免后续 N+1 查询 + dingdan_obj = Dingdan.objects.select_related( + 'pingtai_kuozhan', + 'shangjia_kuozhan' + ).only( + 'dingdan_id', 'zhuangtai', 'fadan_pingtai', 'jine', 'dashou_fencheng', + 'jiedan_dashou_id', 'dashou_liuyan', 'zhiding_id', 'shangpin_id', + 'leixing_id', 'tupian', 'jieshao', 'beizhu', 'tkly', 'nicheng', + 'clkf', 'create_time', 'update_time' + ).get(dingdan_id=dingdan_id) + + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {dingdan_id}") + return Response( + {'code': 404, 'msg': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 4. 构建基础响应数据(主表字段) + 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 '', + } + + # 5. 根据发单平台添加扩展表数据 + 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': '', + } + + # 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] + except Exception as e: + logger.warning(f"查询打手图片失败: {str(e)}") + response_data['dashou_images'] = [] + + # 7. 查询退款理由(从退款记录表取最新一条) + try: + tuikuan_record = Tuikuanjilu.objects.filter( + dingdan_id=dingdan_id + ).order_by('-create_time').first() + if tuikuan_record and tuikuan_record.liyou: + response_data['tuikuan_liyou'] = tuikuan_record.liyou + else: + response_data['tuikuan_liyou'] = '' + except Exception as e: + logger.warning(f"查询退款记录失败: {str(e)}") + response_data['tuikuan_liyou'] = '' + + logger.info(f"订单详情接口耗时: {time.time()-start_time:.3f}s") + 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': '服务器内部错误'}) + + + + + + + + + + +'''class KefuWithdrawListView(APIView): + """ + 客服获取提现审核列表接口 + 请求:POST /yonghu/kefu_txsh_list + 参数:{ + "phone": "客服手机号", + "page": 1, + "page_size": 20, + "status": 1(待审核)/2(已处理), + "type": 1(打手)/2(管事) 可选, + "payment": 1(微信)/2(支付宝) 可选, + "search_uid": "用户ID搜索" 可选, + "date": "YYYY-MM-DD" 可选 按创建日期筛选 + } + 认证:JWT + 返回:{ + "code": 0, + "data": { + "list": [提现记录], + "total": 总数, + "stats": { + "total": 总记录, + "awaiting": 待审核数, + "processed": 已处理数 + } + } + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + phone = request.data.get('phone', '').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) + + status_filter = request.data.get('status') # 1待审核 2已处理 + type_filter = request.data.get('type') + payment_filter = request.data.get('payment') + search_uid = request.data.get('search_uid', '').strip() + date_filter = request.data.get('date', '').strip() + + if not phone: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 客服身份验证 + 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 '005bb' not in permissions: + return Response({'code': 403, 'msg': '您无权查看平台订单列表'}) + + # 构建查询条件 + q = Q() + + if status_filter == 1: + q &= Q(zhuangtai=1) + elif status_filter == 2: + q &= Q(zhuangtai__in=[2, 3]) + + if type_filter: + try: + q &= Q(leixing=int(type_filter)) + except ValueError: + pass + + if payment_filter: + try: + q &= Q(fangshi=int(payment_filter)) + except ValueError: + pass + + if search_uid: + q &= Q(yonghuid=search_uid) + + if date_filter: + q &= Q(create_time__date=date_filter) + + # 统计信息(整体,不考虑分页) + stats = { + 'total': Tixianjilu.objects.filter(q).count(), + 'awaiting': Tixianjilu.objects.filter(q & Q(zhuangtai=1)).count(), + 'processed': Tixianjilu.objects.filter(q & Q(zhuangtai__in=[2, 3])).count(), + } + + # 分页查询 + total = Tixianjilu.objects.filter(q).count() + queryset = Tixianjilu.objects.filter(q).order_by('create_time') # 按申请时间升序 + records = queryset[(page-1)*page_size : page*page_size] + + # 构建返回列表 + list_data = [] + for item in records: + list_data.append({ + 'id': item.id, + '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 else '0.00', + 'zhuangtai': item.zhuangtai, + 'fangshi': item.fangshi, + 'shenheid': item.shenheid or '', + 'bhliyou': item.bhliyou or '', + 'create_time': item.create_time, + 'update_time': item.update_time, + }) + + return Response({ + 'code': 0, + 'data': { + 'list': list_data, + 'total': total, + 'stats': stats + } + })''' + + +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) +