新增协议表/签署留痕、后台与小程序 API;去掉手写依赖;支持抢单/注册/提现等卡点配置。冻结问号文案按角色字典下发。 Co-authored-by: Cursor <cursoragent@cursor.com>
295 lines
11 KiB
Python
295 lines
11 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'
|
|
|
|
|
|
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,
|
|
},
|
|
})
|