feat: 小程序资金冻结只读接口(汇总/明细/兼容字段)

提现资产与角色信息增量返回 dongjie;新增 fund-freeze-mine/ledger 只读 API,不改扣款逻辑。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-26 19:11:47 +08:00
parent 3194a47f52
commit a505a1ab2d
6 changed files with 277 additions and 1 deletions

View File

@@ -62,6 +62,12 @@ from .tixian import (
generate_tixian_id,
)
from .fund_freeze_mp import (
FundFreezeMineView,
FundFreezeMyLedgerDetailView,
FundFreezeMyLedgerView,
)
from .admin import (
AdGetOrderList,
AdGetOrderTypes,

View File

@@ -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)
}

View File

@@ -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,
},
})

View File

@@ -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

View File

@@ -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, # 已充值打手数量
}
})