"""小程序资金冻结只读接口。 仅返回当前登录用户自己的冻结汇总/明细,不改余额、不触发解冻。 字段均为增量返回,旧前端忽略即可。 """ 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 ClubFundFreezeConfig, 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' def _default_freeze_reason() -> str: return ( '部分佣金暂存为冻结保障金,用于订单售后与纠纷保障;' '按俱乐部规则到期或达标后自动转入可提现余额,冻结中金额不可提现。' ) def _freeze_display_reasons(club_id: str) -> Dict[str, str]: """按角色返回冻结说明;无配置时用统一默认文案。""" default = _default_freeze_reason() out = { ROLE_DASHOU: default, ROLE_GUANSHI: default, ROLE_ZUZHANG: default, ROLE_SHENHEGUAN: default, } if not club_id: return out try: for row in ClubFundFreezeConfig.query.filter(club_id=club_id): if row.role in out and (row.remark or '').strip(): out[row.role] = row.remark.strip() except Exception: pass return out 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')) reasons = _freeze_display_reasons(club_id) display_reason = reasons.get(ROLE_DASHOU) or _default_freeze_reason() 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, 'display_reason': display_reason, 'freeze_display_reasons': reasons, }, }) 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 ( (r.config_snapshot or {}).get('display_reason') or (r.config_snapshot or {}).get('remark') 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 ( (ledger.config_snapshot or {}).get('display_reason') or (ledger.config_snapshot or {}).get('remark') or '' ), 'current_step': ledger.current_step, 'events': event_list, }, })