feat: 小程序资金冻结只读接口(汇总/明细/兼容字段)
提现资产与角色信息增量返回 dongjie;新增 fund-freeze-mine/ledger 只读 API,不改扣款逻辑。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -62,6 +62,12 @@ from .tixian import (
|
||||
generate_tixian_id,
|
||||
)
|
||||
|
||||
from .fund_freeze_mp import (
|
||||
FundFreezeMineView,
|
||||
FundFreezeMyLedgerDetailView,
|
||||
FundFreezeMyLedgerView,
|
||||
)
|
||||
|
||||
from .admin import (
|
||||
AdGetOrderList,
|
||||
AdGetOrderTypes,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
253
users/views/fund_freeze_mp.py
Normal file
253
users/views/fund_freeze_mp.py
Normal 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,
|
||||
},
|
||||
})
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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, # 已充值打手数量
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user