diff --git a/users/urls.py b/users/urls.py index 8b11779..e219998 100644 --- a/users/urls.py +++ b/users/urls.py @@ -17,7 +17,8 @@ from .views import WechatMiniProgramLoginView, UserInfoUpdateView, DashouXinxiAP AdKfxgView,GuanshiRegisterView, WechatLoginAndGuanshiRegisterView,ZuzhangZhuceView, ZuzhangXinxiView,KefuForceCompleteView,HuoQuYaoQingRenView, DashouJianquanView, LaobanJianquanView,\ FaKuanJiFenTongJiView, DaShouFaKuanLieBiaoView, ShangjiaCufaTongjiView, ShangjiaFakuanLieBiaoView,\ FaKuanShenSuView, FaKuanPayView,FaKuanHuitiaoView, FaKuanPayPollView, FaKuanPayFailView,KaohePayView,KaohePayCallbackView, KaohePayPollView,KaohePayFailView, TixianAssetView, GuanshiContactView,TixianQueRenAutoView, TixianShenqingV3View, TixianCallbackV3View, \ - GetUserPhoneView, BindUserPhoneView, BossSearchDashouView + GetUserPhoneView, BindUserPhoneView, BossSearchDashouView, \ + FundFreezeMineView, FundFreezeMyLedgerView, FundFreezeMyLedgerDetailView from .tixian_shenhe_views import TixianZddkshApplyView from .paihang_views import PhbHqsjView from .xzj_paihang_views import XzjPhbHqsjView @@ -164,6 +165,9 @@ urlpatterns = [ path('khzfsb', KaohePayFailView.as_view(), name='考核支付失败'), path('tixianzichan', TixianAssetView.as_view(), name='小程序提现数据获取'), + path('fund-freeze-mine', FundFreezeMineView.as_view(), name='小程序冻结资金汇总'), + path('fund-freeze-ledger', FundFreezeMyLedgerView.as_view(), name='小程序冻结资金明细'), + path('fund-freeze-ledger-detail', FundFreezeMyLedgerDetailView.as_view(), name='小程序冻结单详情'), path('guanshi_contact', GuanshiContactView.as_view(), name='获取组长'), diff --git a/users/views/__init__.py b/users/views/__init__.py index 5222b83..5ffca33 100644 --- a/users/views/__init__.py +++ b/users/views/__init__.py @@ -62,6 +62,12 @@ from .tixian import ( generate_tixian_id, ) +from .fund_freeze_mp import ( + FundFreezeMineView, + FundFreezeMyLedgerDetailView, + FundFreezeMyLedgerView, +) + from .admin import ( AdGetOrderList, AdGetOrderTypes, diff --git a/users/views/auth.py b/users/views/auth.py index 1adf3b9..4f218c6 100644 --- a/users/views/auth.py +++ b/users/views/auth.py @@ -1869,6 +1869,7 @@ class ZuzhangZhuceView(APIView): guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count() return { 'ketixian': str(zuzhang.ketixian_jine), # 可提现余额 + 'dongjie_yue': str(getattr(zuzhang, 'dongjie_yue', 0) or 0), 'guanshiCount': guanshi_count, # 邀请管事数 'fenhongZonge': str(zuzhang.fenyong_zonge) # 分红总额 } @@ -1894,6 +1895,7 @@ class ZuzhangXinxiView(APIView): guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count() data = { 'ketixian': str(zuzhang.ketixian_jine), + 'dongjie_yue': str(getattr(zuzhang, 'dongjie_yue', 0) or 0), 'guanshiCount': guanshi_count, 'fenhongZonge': str(zuzhang.fenyong_zonge) } diff --git a/users/views/fund_freeze_mp.py b/users/views/fund_freeze_mp.py new file mode 100644 index 0000000..c2545c1 --- /dev/null +++ b/users/views/fund_freeze_mp.py @@ -0,0 +1,253 @@ +"""小程序资金冻结只读接口。 + +仅返回当前登录用户自己的冻结汇总/明细,不改余额、不触发解冻。 +字段均为增量返回,旧前端忽略即可。 +""" +from __future__ import annotations + +from decimal import Decimal, ROUND_HALF_UP +from typing import Any, Dict, List + +from django.core.exceptions import ObjectDoesNotExist +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from jituan.models import FundFreezeLedger, FundUnfreezeEvent +from jituan.services.club_user import get_user_club_id +from jituan.services.fund_freeze import ( + ROLE_DASHOU, + ROLE_GUANSHI, + ROLE_SHENHEGUAN, + ROLE_ZUZHANG, +) + +ROLE_LABEL = { + ROLE_DASHOU: '接单员', + ROLE_GUANSHI: '管事', + ROLE_ZUZHANG: '组长', + ROLE_SHENHEGUAN: '考核官', +} + +STATUS_LABEL = { + FundFreezeLedger.STATUS_FROZEN: '冻结中', + FundFreezeLedger.STATUS_PARTIAL: '部分解冻', + FundFreezeLedger.STATUS_DONE: '已解冻', + FundFreezeLedger.STATUS_VOID: '已作废', +} + +SOURCE_LABEL = { + 'order_settle': '订单结算', + 'fenhong': '分红入账', + 'kaohe_fee': '考核费用', +} + + +def _money(v) -> str: + try: + return str(Decimal(str(v or 0)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)) + except Exception: + return '0.00' + + +def _profile_dongjie(user, attr: str) -> str: + try: + profile = getattr(user, attr) + return _money(getattr(profile, 'dongjie_yue', 0)) + except (ObjectDoesNotExist, AttributeError): + return '0.00' + + +def _profile_withdrawable(user, attr: str, field: str = 'yue') -> str: + try: + profile = getattr(user, attr) + return _money(getattr(profile, field, 0)) + except (ObjectDoesNotExist, AttributeError): + return '0.00' + + +class FundFreezeMineView(APIView): + """ + POST /yonghu/fund-freeze-mine + 当前用户冻结汇总(可提现 + 冻结中,按角色)。 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + user = request.user + club_id = get_user_club_id(user) or '' + roles: List[Dict[str, Any]] = [ + { + 'role': ROLE_DASHOU, + 'role_name': ROLE_LABEL[ROLE_DASHOU], + 'withdrawable': _profile_withdrawable(user, 'DashouProfile', 'yue'), + 'dongjie_yue': _profile_dongjie(user, 'DashouProfile'), + }, + { + 'role': ROLE_GUANSHI, + 'role_name': ROLE_LABEL[ROLE_GUANSHI], + 'withdrawable': _profile_withdrawable(user, 'GuanshiProfile', 'yue'), + 'dongjie_yue': _profile_dongjie(user, 'GuanshiProfile'), + }, + { + 'role': ROLE_ZUZHANG, + 'role_name': ROLE_LABEL[ROLE_ZUZHANG], + 'withdrawable': _profile_withdrawable(user, 'ZuzhangProfile', 'ketixian_jine'), + 'dongjie_yue': _profile_dongjie(user, 'ZuzhangProfile'), + }, + { + 'role': ROLE_SHENHEGUAN, + 'role_name': ROLE_LABEL[ROLE_SHENHEGUAN], + 'withdrawable': _profile_withdrawable(user, 'ShenheguanProfile', 'yue'), + 'dongjie_yue': _profile_dongjie(user, 'ShenheguanProfile'), + }, + ] + # 只展示有身份余额或冻结的角色,避免空壳干扰 + visible = [ + r for r in roles + if Decimal(r['withdrawable']) > 0 or Decimal(r['dongjie_yue']) > 0 + ] + total_dongjie = sum((Decimal(r['dongjie_yue']) for r in roles), Decimal('0.00')) + total_withdrawable = sum((Decimal(r['withdrawable']) for r in roles), Decimal('0.00')) + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'club_id': club_id, + 'roles': visible or roles[:1], + 'all_roles': roles, + 'total_dongjie': _money(total_dongjie), + 'total_withdrawable': _money(total_withdrawable), + 'has_freeze': total_dongjie > 0, + }, + }) + + +class FundFreezeMyLedgerView(APIView): + """ + POST /yonghu/fund-freeze-ledger + 当前用户冻结明细(只读分页)。 + body: page, page_size, role?, status? + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + user = request.user + uid = user.UserUID + club_id = get_user_club_id(user) or '' + data = request.data if isinstance(request.data, dict) else {} + page = max(1, int(data.get('page') or 1)) + page_size = min(50, max(1, int(data.get('page_size') or data.get('limit') or 20))) + role = (data.get('role') or '').strip().lower() + status = data.get('status') + + qs = FundFreezeLedger.query.filter(user_id=uid) + if club_id: + qs = qs.filter(club_id=club_id) + if role: + qs = qs.filter(role=role) + if status not in (None, ''): + try: + qs = qs.filter(status=int(status)) + except (TypeError, ValueError): + pass + + total = qs.count() + rows = qs.order_by('-id')[(page - 1) * page_size: page * page_size] + items = [] + for r in rows: + remain = Decimal(str(r.amount_total or 0)) - Decimal(str(r.amount_unfrozen or 0)) + items.append({ + 'id': r.id, + 'role': r.role, + 'role_name': ROLE_LABEL.get(r.role, r.role), + 'amount_total': _money(r.amount_total), + 'amount_unfrozen': _money(r.amount_unfrozen), + 'amount_remain': _money(remain), + 'status': r.status, + 'status_label': STATUS_LABEL.get(r.status, str(r.status)), + 'frozen_at': r.frozen_at.strftime('%Y-%m-%d %H:%M') if r.frozen_at else '', + 'next_eligible_at': ( + r.next_eligible_at.strftime('%Y-%m-%d %H:%M') if r.next_eligible_at else '' + ), + 'source_type': r.source_type, + 'source_label': SOURCE_LABEL.get(r.source_type, r.source_type or '入账'), + 'order_id': r.order_id or '', + 'freeze_reason': r.freeze_reason or '', + 'current_step': r.current_step, + }) + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'club_id': club_id, + 'list': items, + 'total': total, + 'page': page, + 'page_size': page_size, + 'has_more': page * page_size < total, + }, + }) + + +class FundFreezeMyLedgerDetailView(APIView): + """ + POST /yonghu/fund-freeze-ledger-detail + 单笔冻结单 + 解冻事件(仅本人)。 + body: ledger_id + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + user = request.user + data = request.data if isinstance(request.data, dict) else {} + try: + ledger_id = int(data.get('ledger_id') or data.get('id') or 0) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': 'ledger_id 无效', 'data': None}, status=400) + if ledger_id <= 0: + return Response({'code': 400, 'msg': '缺少 ledger_id', 'data': None}, status=400) + + ledger = FundFreezeLedger.query.filter(id=ledger_id, user_id=user.UserUID).first() + if not ledger: + return Response({'code': 404, 'msg': '记录不存在', 'data': None}, status=404) + + remain = Decimal(str(ledger.amount_total or 0)) - Decimal(str(ledger.amount_unfrozen or 0)) + events = FundUnfreezeEvent.query.filter( + ledger_id=ledger.id, + ).order_by('step_seq', 'id')[:50] + event_list = [{ + 'id': e.id, + 'step_seq': e.step_seq, + 'amount': _money(e.amount), + 'unfrozen_at': e.unfrozen_at.strftime('%Y-%m-%d %H:%M') if e.unfrozen_at else '', + 'trigger': e.trigger, + 'reason': e.reason or '', + } for e in events] + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'id': ledger.id, + 'role': ledger.role, + 'role_name': ROLE_LABEL.get(ledger.role, ledger.role), + 'amount_total': _money(ledger.amount_total), + 'amount_unfrozen': _money(ledger.amount_unfrozen), + 'amount_remain': _money(remain), + 'status': ledger.status, + 'status_label': STATUS_LABEL.get(ledger.status, str(ledger.status)), + 'frozen_at': ledger.frozen_at.strftime('%Y-%m-%d %H:%M') if ledger.frozen_at else '', + 'next_eligible_at': ( + ledger.next_eligible_at.strftime('%Y-%m-%d %H:%M') + if ledger.next_eligible_at else '' + ), + 'source_type': ledger.source_type, + 'source_label': SOURCE_LABEL.get(ledger.source_type, ledger.source_type or '入账'), + 'order_id': ledger.order_id or '', + 'freeze_reason': ledger.freeze_reason or '', + 'current_step': ledger.current_step, + 'events': event_list, + }, + }) diff --git a/users/views/tixian.py b/users/views/tixian.py index 25fb498..13ec472 100644 --- a/users/views/tixian.py +++ b/users/views/tixian.py @@ -1050,6 +1050,7 @@ class TixianAssetView(APIView): 'DashouProfile', 'ShopProfile', 'GuanshiProfile', 'ZuzhangProfile', 'ShenheguanProfile' ).get(UserUID=request.user.UserUID) # 初始化返回数据,默认全部为0 + # 冻结字段为增量兼容字段:旧前端忽略;提现仍只读 *_yue / *_fenyong data = { 'dashou_yue': '0.00', 'dashou_yajin': '0.00', @@ -1057,6 +1058,10 @@ class TixianAssetView(APIView): 'guanshi_fenyong': '0.00', 'zuzhang_fenyong': '0.00', 'kaoheguan_fenyong': '0.00', + 'dashou_dongjie_yue': '0.00', + 'guanshi_dongjie_yue': '0.00', + 'zuzhang_dongjie_yue': '0.00', + 'kaoheguan_dongjie_yue': '0.00', 'dashou_rate': 0.00, 'dashou_yajin_rate': 0.00, 'shangjia_rate': 0.00, @@ -1070,6 +1075,7 @@ class TixianAssetView(APIView): dashou = user.DashouProfile data['dashou_yue'] = str(dashou.yue) data['dashou_yajin'] = str(dashou.yajin) + data['dashou_dongjie_yue'] = str(getattr(dashou, 'dongjie_yue', 0) or 0) except ObjectDoesNotExist: pass @@ -1084,6 +1090,7 @@ class TixianAssetView(APIView): try: guanshi = user.GuanshiProfile data['guanshi_fenyong'] = str(guanshi.yue) + data['guanshi_dongjie_yue'] = str(getattr(guanshi, 'dongjie_yue', 0) or 0) except ObjectDoesNotExist: pass @@ -1091,6 +1098,7 @@ class TixianAssetView(APIView): try: zuzhang = user.ZuzhangProfile data['zuzhang_fenyong'] = str(zuzhang.ketixian_jine) + data['zuzhang_dongjie_yue'] = str(getattr(zuzhang, 'dongjie_yue', 0) or 0) except ObjectDoesNotExist: pass @@ -1098,6 +1106,7 @@ class TixianAssetView(APIView): try: kaoheguan = user.ShenheguanProfile data['kaoheguan_fenyong'] = str(kaoheguan.yue) + data['kaoheguan_dongjie_yue'] = str(getattr(kaoheguan, 'dongjie_yue', 0) or 0) except ObjectDoesNotExist: pass diff --git a/users/views/user_info.py b/users/views/user_info.py index 734bd33..098e7a0 100644 --- a/users/views/user_info.py +++ b/users/views/user_info.py @@ -406,6 +406,7 @@ class DashouXinxiAPIView(APIView): 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, 'dashouzhuangtai': dashou_profile.zhuangtai, 'yongjin': str(dashou_profile.yue) if dashou_profile.yue is not None else '0.00', + 'dongjie_yue': str(getattr(dashou_profile, 'dongjie_yue', 0) or 0), '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', @@ -667,6 +668,7 @@ class GuanshiXinxiView(APIView): 'yaoqingzongshu': guanshi_profile.yaogingshuliang, # 邀请打手总数 'fenyongzonge': float(guanshi_profile.chongzhifenrun), # 充值分佣总额 'fenyongtixian': float(guanshi_profile.yue), # 可提现余额 + 'dongjie_yue': float(getattr(guanshi_profile, 'dongjie_yue', 0) or 0), 'yichongzhiDashou': yichongzhi_count, # 已充值打手数量 } })