Files
Django/users/views/fund_freeze_mp.py
XingQue 6c96eb3bcb fix: 冻结条件可评估+分期规范化,定时扫描更及时
后台条件真正按罚款率/单量等评估;选项式配置所需字段规范化;Celery 改为每5分钟;小程序说明文案接口补齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 19:36:10 +08:00

279 lines
10 KiB
Python

"""小程序资金冻结只读接口。
仅返回当前登录用户自己的冻结汇总/明细,不改余额、不触发解冻。
字段均为增量返回,旧前端忽略即可。
"""
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'
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'))
# 用户可见说明:优先打手规则,否则取任意已配置角色
display_reason = ''
if club_id:
cfgs = list(ClubFundFreezeConfig.query.filter(club_id=club_id, enabled=True))
prefer = next((c for c in cfgs if c.role == ROLE_DASHOU), None) or (cfgs[0] if cfgs else None)
if prefer and prefer.remark:
display_reason = prefer.remark
if not display_reason:
any_cfg = ClubFundFreezeConfig.query.filter(club_id=club_id).exclude(remark='').first()
if any_cfg:
display_reason = any_cfg.remark or ''
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 or (
'部分佣金暂存为冻结保障金,用于订单售后与纠纷保障;'
'按俱乐部规则到期或达标后自动转入可提现余额,冻结中金额不可提现。'
),
},
})
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,
},
})