From 8678f54459c23cb2c9c7608a6821c1e453e16cfb Mon Sep 17 00:00:00 2001 From: XingQue Date: Fri, 10 Jul 2026 04:06:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=8F=90=E7=8E=B0=E8=B5=84?= =?UTF-8?q?=E9=87=91=E5=86=BB=E7=BB=93=EF=BC=9A=E6=89=8B=E5=8A=A8/?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=89=93=E6=AC=BE=E5=85=B1=E7=94=A8=E5=86=BB?= =?UTF-8?q?=E7=BB=93=E6=8B=86=E5=88=86=EF=BC=8C=E9=A9=B3=E5=9B=9E=E4=BB=85?= =?UTF-8?q?=E9=80=80=20shenqing=5Fjine=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增公共/用户冻结配置、流水表与后台解冻接口;修复冻结与扣款重复扣余额问题。 Co-authored-by: Cursor --- docs/资金冻结功能设计方案.md | 381 +++++++++++++++ houtai/dongjie_views.py | 90 ++++ houtai/urls.py | 3 +- houtai/view.py | 13 + peizhi/models.py | 1 + yonghu/freeze_service.py | 434 ++++++++++++++++++ .../migrations/0023_freeze_balance_feature.py | 182 ++++++++ yonghu/models.py | 85 +++- yonghu/tests/__init__.py | 0 yonghu/tests/test_freeze_service.py | 70 +++ yonghu/tixian_shenhe_services.py | 86 +++- yonghu/tixian_shenhe_views.py | 7 + yonghu/views.py | 115 +++++ 13 files changed, 1447 insertions(+), 20 deletions(-) create mode 100644 docs/资金冻结功能设计方案.md create mode 100644 houtai/dongjie_views.py create mode 100644 yonghu/freeze_service.py create mode 100644 yonghu/migrations/0023_freeze_balance_feature.py create mode 100644 yonghu/tests/__init__.py create mode 100644 yonghu/tests/test_freeze_service.py diff --git a/docs/资金冻结功能设计方案.md b/docs/资金冻结功能设计方案.md new file mode 100644 index 0000000..97f88eb --- /dev/null +++ b/docs/资金冻结功能设计方案.md @@ -0,0 +1,381 @@ +# 资金冻结功能 — 设计方案(待确认) + +> 本文档仅描述思路与边界,**确认后再写代码**。 +> 目标:打手 / 管事 / 商家三类角色的可提现余额,在**自动打款申请**时按配置冻结一部分资金,进入「冻结资金池」,防止重复提现;后台可配置、可单独覆盖、可解冻。 + +--- + +## 一、需求理解(我的归纳) + +| 要点 | 理解 | +|------|------| +| 冻结对象 | 打手佣金余额(`user_dashou.yue`)、管事余额(`user_guanshi.yue`)、商家余额(`user_shangjia.yue`) | +| 冻结资金去向 | 从「可提现余额」扣出 → 进入「冻结资金池」字段,**永远不能通过提现提出** | +| 配置层级 | 公共配置(提现设置页) + 用户单独配置(各角色详情页,开启后覆盖公共) | +| 冻结方式 | **比例**:按当前余额冻结 X%;**固定金额**:冻结池累计须达到 Y 元 | +| 生效范围(一期) | **仅自动打款申请** `POST /yonghu/zddksh`;收款 `tixiansq` 不再二次冻结 | +| 驳回退款 | 仅退回**本次实际进入提现流程的金额**;本次划入冻结池的金额一并回滚 | +| 后台解冻 | 管理员在角色详情页,可将冻结池部分/全部划回可提现余额 | + +**一期不做(除非你确认要加):** + +- 组长(`leixing=3`)、审核官(`leixing=4`)、打手押金(`leixing=5`) 的冻结 +- 手动打款 `POST /yonghu/tixian` 的冻结(可二期复用同一套 `freeze_service`) + +--- + +## 二、现有提现流程(自动打款) + +``` +用户 POST /yonghu/zddksh { leixing, jine } + ↓ +create_audit_application() + ├─ validate_withdraw_eligibility() # 校验余额 >= jine + ├─ calc_fee_amounts(jine) # 手续费、实际到账 + ├─ deduct_balance(leixing, jine) # 从余额扣 jine(申请扣款额) + └─ 写 TixianShenheJilu + Tixianjilu + shenqing_jine = jine + jine(记录) = shijidaozhang(到账额) + +审核通过 → 状态 6 待收款 +用户 POST /yonghu/tixiansq 收款 → 微信打款(不再动余额) +驳回 → refund_balance(shenqing_jine) 退回余额 +``` + +**改造点:在 `deduct_balance` 之前插入「冻结拆分」逻辑。** + +--- + +## 三、表模型设计 + +### 3.1 公共冻结配置表 `tixian_dongjie_gonggong_peizhi` + +每个角色一行(`leixing` 唯一): + +| 字段 | 类型 | 说明 | +|------|------|------| +| `leixing` | SmallInt | `1`打手佣金 `2`管事 `6`商家(唯一) | +| `role_enabled` | Boolean | 该角色是否启用公共冻结(默认 `False`) | +| `dongjie_mode` | SmallInt | `1`=按比例 `2`=固定金额 | +| `dongjie_bili` | Decimal(5,4) | 比例,如 `0.1000` = 10% | +| `dongjie_guding_jine` | Decimal(12,2) | 固定冻结目标金额 | +| `update_time` | DateTime | | + +### 3.2 全局总开关(挂在现有 `withdraw_config` 表) + +| 字段 | 说明 | +|------|------| +| `dongjie_quanju_enabled` | 全员冻结总开关,默认 `False`;**仅当为 True 时,公共配置才可能生效** | + +> 生效条件(公共):`dongjie_quanju_enabled == True` **且** 该角色 `role_enabled == True` + +### 3.3 三个扩展表新增字段(结构相同) + +**`user_dashou` / `user_guanshi` / `user_shangjia`:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `dongjie_chi` | Decimal(12,2) | **冻结资金池**(累计,默认 0) | +| `dandu_dongjie_enabled` | Boolean | 是否启用单独冻结配置(默认 `False`) | +| `dongjie_mode` | SmallInt | `1`比例 / `2`固定(仅单独配置用) | +| `dongjie_bili` | Decimal(5,4) | 单独比例 | +| `dongjie_guding_jine` | Decimal(12,2) | 单独固定金额 | + +> 配置优先级: +> `dandu_dongjie_enabled == True` → 用扩展表上的 mode/bili/guding +> 否则 → 用公共表(且需全局+角色开关均开) +> 否则 → **不冻结** + +### 3.4 审核记录补充字段(驳回回滚用) + +**`tixian_shenhe_jilu` 新增:** + +| 字段 | 说明 | +|------|------| +| `benchi_dongjie_jine` | 本次申请划入冻结池的金额(驳回时须回滚) | + +**`tixianjilu` 同步写入**(与审核单一致,便于对账)。 + +### 3.5 解冻流水表 `tixian_dongjie_jilu`(建议) + +| 字段 | 说明 | +|------|------| +| `yonghuid`, `leixing`, `jine`, `dongjie_chi_before`, `dongjie_chi_after` | | +| `caozuo` | `1`申请冻结 `2`驳回回滚 `3`管理员解冻 | +| `operator_id`, `beizhu`, `create_time` | | + +便于审计,避免资金纠纷。 + +--- + +## 四、配置解析规则 + +```python +def resolve_freeze_config(profile, leixing) -> FreezeConfig | None: + """ + 返回生效的冻结配置;None 表示本笔不冻结。 + """ + if profile.dandu_dongjie_enabled: + return FreezeConfig(mode=..., bili=..., guding=..., source='user') + + global_on = WithdrawConfig.dongjie_quanju_enabled + if not global_on: + return None + + pub = TixianDongjieGonggongPeizhi.objects.get(leixing=leixing) + if not pub.role_enabled: + return None + + return FreezeConfig(mode=pub.dongjie_mode, ..., source='public') +``` + +**前端展示规则(详情页):** + +- 选「按比例」→ 只展示比例输入框 +- 选「固定金额」→ 只展示固定金额输入框 +- 始终展示:`冻结资金池当前余额`、`是否启用单独配置` 开关 + +--- + +## 五、核心算法(自动打款申请时) + +统一入口:`compute_freeze_split(balance, pool, config, apply_jine) -> FreezeResult` + +所有金额 `Decimal`,保留 2 位,`ROUND_HALF_UP`。 + +### 5.1 无配置 / 未开启 + +``` +freeze_jine = 0 +withdraw_jine = apply_jine # 走原逻辑 +``` + +### 5.2 按比例冻结(mode=1) + +**语义:按申请时刻的「可提现余额」先切出冻结部分,剩余部分才能进入提现扣款。** + +设: + +- `B` = 当前可提现余额(申请前) +- `R` = 冻结比例(如 0.10) +- `A` = 用户申请提现金额 `jine` + +**步骤:** + +1. `freeze_from_balance = quantize(B * R)` +2. `max_withdraw = B - freeze_from_balance` +3. 若 `A > max_withdraw` → **拒绝**:`最多可提现 {max_withdraw} 元({R*100}% 已冻结)` +4. 若 `freeze_from_balance > 0`: + - `balance -= freeze_from_balance` + - `dongjie_chi += freeze_from_balance` +5. 对金额 `A` 走原流程:`deduct_balance(A)` → 手续费基于 `A` 计算 + +**示例(10%):** + +| 余额 B | 申请 A | 划入冻结池 | 实际提现扣款 | 申请后余额 | 冻结池 | +|--------|--------|------------|--------------|------------|--------| +| 100 | 90 | 10 | 90 | 0 | 10 | +| 100 | 50 | 10 | 50 | 40 | 10 | +| 100 | 100 | — | 拒绝(最多 90) | — | — | + +> 关键:冻结部分**一次性从余额切走**,避免「余额还剩 10% 又能再提」的漏洞。 + +### 5.3 按固定金额冻结(mode=2) + +**语义:冻结池累计须达到目标 `T`;未满则优先用本次申请金额「填池」。** + +设: + +- `P` = 当前 `dongjie_chi` +- `T` = 配置的固定冻结目标 +- `A` = 用户申请金额 +- `B` = 当前余额(须 `A <= B`) + +**步骤:** + +1. 若 `P >= T` → **不冻结**,`withdraw_jine = A`,走原逻辑 +2. 若 `P < T`: + - `gap = T - P` + - **情况 A**:`A + P < T`(本次申请填池后仍未达标) + - 整笔 `A` 划入冻结池,**不创建提现审核单**(或创建「仅冻结」记录,状态特殊) + - `balance -= A`,`dongjie_chi += A` + - 返回提示:`已冻结 {A} 元,冻结池 {P+A}/{T},满额后可正常提现` + - **情况 B**:`A + P >= T`(本次可填满池并有余款提现) + - `fill = gap`(填池金额) + - `withdraw_jine = A - fill` + - `balance -= A`(等价:先扣 A,其中 fill 入池、withdraw_jine 进入提现) + - `dongjie_chi += fill` + - 对 `withdraw_jine` 计算手续费、写审核单 + +**示例(T=100):** + +| 池 P | 余额 B | 申请 A | 结果 | +|------|--------|--------|------| +| 30 | 50 | 50 | 50+30<100 → 全部入池,池=80,不提现 | +| 30 | 80 | 80 | 80+30≥100 → 填池70,提现10 | +| 100 | 50 | 50 | 池已满 → 正常提现50 | + +### 5.4 手续费与记录字段(不弄乱) + +仅对 **`withdraw_jine`(实际进入提现流程的金额)** 计算: + +``` +shouxufei = withdraw_jine * feilv +shijidaozhang = withdraw_jine - shouxufei + +TixianShenheJilu.shenqing_jine = withdraw_jine # 申请扣款额 +TixianShenheJilu.shijidaozhang = shijidaozhang # 到账额 +TixianShenheJilu.benchi_dongjie_jine = freeze_jine # 本次入池 +``` + +**收款阶段 `tixiansq`:** 只打 `shijidaozhang`,不再动余额/冻结池。 + +--- + +## 六、驳回 / 解冻 + +### 6.1 审核驳回(自动 + 手动均适用) + +在现有 `refund_balance(shenqing_jine)` 基础上增加: + +``` +balance += shenqing_jine # 退回提现扣款 +dongjie_chi -= benchi_dongjie_jine # 回滚本次冻结(若有) +balance += benchi_dongjie_jine # 冻结部分也回到可提现余额 +``` + +须在同一 `transaction.atomic` + `select_for_update` 内完成。 + +### 6.2 管理员解冻 + +**新接口** `POST /houtai/.../dongjie_jiedong`(三个详情页共用): + +```json +{ + "yonghuid": "0000001", + "leixing": 1, + "jine": "50.00", + "beizhu": "人工解冻" +} +``` + +校验: + +- `0 < jine <= dongjie_chi` +- `balance += jine`,`dongjie_chi -= jine` +- 写解冻流水 + +--- + +## 七、接口设计 + +### 7.1 公共配置(提现设置页) + +| 方法 | 路径 | 权限 | +|------|------|------| +| GET | `/houtai/hthqtxpz` 扩展返回冻结配置 | 现有提现设置权限 | +| POST | `/houtai/htxgtxsz` 扩展保存冻结配置 | 同上 | + +返回/保存结构示例: + +```json +{ + "dongjie_quanju_enabled": false, + "dongjie_roles": [ + { + "leixing": 1, + "role_name": "打手佣金", + "role_enabled": false, + "dongjie_mode": 1, + "dongjie_bili": "0.1000", + "dongjie_guding_jine": "0.00" + }, + { "leixing": 2, "...": "管事" }, + { "leixing": 6, "...": "商家" } + ] +} +``` + +### 7.2 用户单独配置(三详情页共用) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/houtai/hqdjdongjie?yonghuid=&leixing=` | 查询 | +| POST | `/houtai/xgdjdongjie` | 保存单独配置 / 解冻 | + +请求体: + +```json +{ + "yonghuid": "0000001", + "leixing": 1, + "dandu_dongjie_enabled": true, + "dongjie_mode": 2, + "dongjie_guding_jine": "100.00", + "jiedong_jine": "30.00", + "beizhu": "可选,有值则执行解冻" +} +``` + +`dongjie_mode=1` 时只读/写 `dongjie_bili`;`mode=2` 时只读/写 `dongjie_guding_jine`。 + +### 7.3 用户端(小程序) + +申请前可增加接口 `GET /yonghu/tixian_dongjie_info?leixing=` 返回: + +- 是否启用冻结、模式、比例/固定额 +- 当前余额、冻结池、**最大可申请金额**(前端展示用,后端申请时仍强校验) + +--- + +## 八、代码改动范围(确认后) + +| 模块 | 改动 | +|------|------| +| `peizhi/models.py` | `WithdrawConfig` 增加总开关 | +| `yonghu/models.py` | 公共配置表 + 三扩展表字段 + 审核表 `benchi_dongjie_jine` + 流水表 | +| `yonghu/freeze_service.py` | **新建** 配置解析 + 冻结拆分算法(纯函数,便于单测) | +| `yonghu/tixian_shenhe_services.py` | `create_audit_application` 接入冻结;驳回增加回滚 | +| `houtai/view.py` | 提现设置读写、单独配置/解冻接口 | +| `kefu/.../Settings.vue` | 公共冻结配置 UI | +| 打手/管事/商家详情页 | 单独冻结 + 解冻 UI | + +**不动:** `tixiansq` 收款逻辑(仅打款,不二次冻结)。 + +--- + +## 九、边界与防错 + +| 场景 | 处理 | +|------|------| +| 余额 0 | 拒绝申请 | +| 比例 100% | `max_withdraw=0`,拒绝任何提现申请 | +| 固定池已满 | 与无冻结相同,正常提现 | +| 仅冻结不入提现(固定模式情况A) | 不扣手续费、不写微信打款相关记录 | +| 并发申请 | `select_for_update` 锁扩展表余额行 | +| 驳回 | 同时回滚 `shenqing_jine` + `benchi_dongjie_jine` | +| 金额精度 | 全程 `Decimal`,禁止 `float` | + +--- + +## 十、待你确认的问题 + +1. **一期范围**:是否只做 **打手/管事/商家 + 自动打款(zddksh)**?组长、审核官、押金、手动打款是否二期? +2. **固定模式「全入池」**:申请金额不足以填满冻结池时,是否**不创建审核单**、仅入池(我按此设计)? +3. **比例模式**:是否按上文「先按余额切冻结比例,再对剩余部分提现」?(避免余额残留可再提) +4. **驳回**:是否同意「提现扣款 + 本次冻结」**全部回滚到可提现余额**? +5. **公共配置**:全局一个总开关 + 每角色独立开关,是否符合你的预期? + +--- + +## 十一、实施顺序(确认后) + +1. 迁移 + `freeze_service` 单元测试(用例覆盖第五节所有表格) +2. 接入 `zddksh` / 驳回回滚 +3. 后台 API + 提现设置页 UI +4. 三角色详情页 UI + 解冻 +5. 小程序申请前提示(可选) + +--- + +**请先看第十节 5 个确认点,回复「对/不对」或补充后,我再按此文档写代码。** diff --git a/houtai/dongjie_views.py b/houtai/dongjie_views.py new file mode 100644 index 0000000..3ea5890 --- /dev/null +++ b/houtai/dongjie_views.py @@ -0,0 +1,90 @@ +"""后台 — 提现冻结配置/解冻接口""" +import logging + +from django.db import transaction +from rest_framework.parsers import JSONParser +from rest_framework.response import Response +from rest_framework.views import APIView + +from yonghu.freeze_service import ( + FREEZE_LEIXING_SET, + admin_unfreeze_balance, + get_public_freeze_settings, + get_user_freeze_info, + save_public_freeze_settings, + save_user_freeze_settings, +) +from yonghu.models import UserMain + +from .view import verify_kefu_permission + +logger = logging.getLogger(__name__) + +_WITHDRAW_PERMS = ('5500a', '5500b', '5500c') + + +def _check_withdraw_perm(permissions): + return any(p in permissions for p in _WITHDRAW_PERMS) + + +class DongjieUserConfigView(APIView): + """ + 用户单独冻结配置 / 解冻(打手佣金1、管事2、押金5、商家6 共用) + GET POST /houtai/hqdjdongjie + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = (request.data.get('username') or '').strip() + action = (request.data.get('action') or 'get').strip() + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if not _check_withdraw_perm(permissions): + return Response({'code': 403, 'msg': '无权限操作冻结配置'}) + + yonghuid = (request.data.get('yonghuid') or '').strip() + leixing_raw = request.data.get('leixing') + try: + leixing = int(leixing_raw) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': 'leixing 无效'}) + if leixing not in FREEZE_LEIXING_SET: + return Response({'code': 400, 'msg': '该提现类型不支持冻结配置'}) + + try: + user_main = UserMain.objects.get(yonghuid=yonghuid) + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + + if action == 'get': + return Response({'code': 0, 'data': get_user_freeze_info(user_main, leixing)}) + + if action == 'save': + try: + with transaction.atomic(): + data = save_user_freeze_settings(user_main, leixing, request.data) + return Response({'code': 0, 'msg': '保存成功', 'data': data}) + except ValueError as e: + return Response({'code': 400, 'msg': str(e)}) + + if action == 'unfreeze': + jine = request.data.get('jiedong_jine') or request.data.get('jine') + beizhu = (request.data.get('beizhu') or '').strip() + if jine is None: + return Response({'code': 400, 'msg': '请填写解冻金额'}) + try: + with transaction.atomic(): + new_balance = admin_unfreeze_balance( + user_main, leixing, jine, + operator_id=username, beizhu=beizhu, + ) + info = get_user_freeze_info(user_main, leixing) + info['balance'] = str(new_balance) + return Response({'code': 0, 'msg': '解冻成功', 'data': info}) + except ValueError as e: + return Response({'code': 400, 'msg': str(e)}) + + return Response({'code': 400, 'msg': 'action 无效,支持 get/save/unfreeze'}) diff --git a/houtai/urls.py b/houtai/urls.py index 36c0f95..5a639cd 100644 --- a/houtai/urls.py +++ b/houtai/urls.py @@ -17,7 +17,7 @@ from .view import GetClubConfigView, GetCrossOrderListView, PartnerGetOrderDataV FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanChuangJianView,HqbkxxView, BkxgView, \ CaiwuView, CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView, CwqtczhqView, KhpzhqView, ChzsgcView, \ KhgglView, ShgxgsjView, ZxkfghdsView, KptxwztjbView -from .huashu_views import HuashuListAPIView, HuashuModifyAPIView +from .dongjie_views import DongjieUserConfigView from .xieyi_views import ( XieyiListAPIView, XieyiModifyAPIView, XieyiImageUploadAPIView, XieyiImageDeleteAPIView, XieyiSignRecordAPIView, @@ -80,6 +80,7 @@ urlpatterns = [ path('htxggsxx', UpdateGuanliView.as_view(), name='后台更新管事数据'), path('hthqtxpz', GetWithdrawSettingsView.as_view(), name='后台获取提现配置'), path('htxgtxsz', UpdateWithdrawSettingsView.as_view(), name='后台修改提现配置'), + path('hqdjdongjie', DongjieUserConfigView.as_view(), name='后台用户冻结配置解冻'), path('hthqzzlb', GetZuzhangListView.as_view(), name='后台获取组长列表'), path('hthqzzxq', GetZuzhangDetailView.as_view(), name='后台获取组长详情页面'), path('htxgzzxx', UpdateZuzhangView.as_view(), name='后台修改组长信息'), diff --git a/houtai/view.py b/houtai/view.py index 453a781..f3c063a 100644 --- a/houtai/view.py +++ b/houtai/view.py @@ -6728,8 +6728,10 @@ class GetWithdrawSettingsView(APIView): # 🆕 获取提现模式(自动1 / 手动2),默认2 from peizhi.models import WithdrawConfig # 确保导入模型 + from yonghu.freeze_service import get_public_freeze_settings config = WithdrawConfig.objects.filter(id=1).first() withdraw_mode = config.mode if config else 2 + freeze_settings = get_public_freeze_settings() return Response({ 'code': 0, @@ -6740,6 +6742,8 @@ class GetWithdrawSettingsView(APIView): 'quotas': quota_map, 'total_limits': total_limit_map, 'today_totals': today_totals, + 'dongjie_quanju_enabled': freeze_settings['dongjie_quanju_enabled'], + 'dongjie_roles': freeze_settings['dongjie_roles'], } }) @@ -6878,6 +6882,7 @@ class UpdateWithdrawSettingsView(APIView): # 🆕 修改提现模式(有5500a/b/c任一即可) from peizhi.models import WithdrawConfig + from yonghu.freeze_service import save_public_freeze_settings withdraw_mode = request.data.get('withdraw_mode') if withdraw_mode is not None: if not any(p in permissions for p in ('5500a', '5500b', '5500c')): @@ -6888,6 +6893,14 @@ class UpdateWithdrawSettingsView(APIView): defaults={'mode': int(withdraw_mode)} ) + if 'dongjie_quanju_enabled' in request.data or 'dongjie_roles' in request.data: + if not any(p in permissions for p in ('5500a', '5500b', '5500c')): + return Response({'code': 403, 'msg': '无权限修改冻结配置'}) + save_public_freeze_settings( + bool(request.data.get('dongjie_quanju_enabled', False)), + request.data.get('dongjie_roles', []), + ) + # 注意:TixianRiTongji 表由提现流程自动更新,此处不直接修改 return Response({'code': 0, 'msg': '设置修改成功'}) diff --git a/peizhi/models.py b/peizhi/models.py index cfc56c6..ccc91bf 100644 --- a/peizhi/models.py +++ b/peizhi/models.py @@ -275,6 +275,7 @@ class WithdrawConfig(models.Model): default=2, verbose_name='提现模式' ) + dongjie_quanju_enabled = models.BooleanField(default=False, verbose_name='全员冻结总开关') created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') updated_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') diff --git a/yonghu/freeze_service.py b/yonghu/freeze_service.py new file mode 100644 index 0000000..1ecc95c --- /dev/null +++ b/yonghu/freeze_service.py @@ -0,0 +1,434 @@ +""" +提现余额冻结服务 — 手动/自动打款申请共用。 + +规则摘要: +- 比例模式:按当前余额切出 freeze_jine 入池,申请金额 apply_jine 为实际提现扣款部分(另扣手续费)。 +- 固定模式:冻结池未满则优先填池;申请+池仍不足目标则整笔入池(freeze_only)。 +- 驳回:仅退 shenqing_jine 到余额,冻结池不回滚。 +""" +from __future__ import annotations + +import decimal +import logging +from dataclasses import dataclass +from typing import Optional, Tuple + +from django.db import transaction + +from peizhi.models import WithdrawConfig + +from .models import ( + TixianDongjieGonggongPeizhi, + TixianDongjieJilu, + UserDashou, + UserGuanshi, + UserMain, + UserShangjia, +) + +logger = logging.getLogger('yonghu.freeze') + +DONGJIE_MODE_RATIO = 1 +DONGJIE_MODE_FIXED = 2 + +# 支持冻结的提现类型 +FREEZE_LEIXING_SET = frozenset({1, 2, 5, 6}) + +LEIXING_META = { + 1: { + 'model': UserDashou, + 'balance': 'yue', + 'pool': 'dongjie_chi', + 'solo_enabled': 'dandu_dongjie_enabled', + 'mode': 'dongjie_mode', + 'bili': 'dongjie_bili', + 'guding': 'dongjie_guding_jine', + 'role_name': '打手佣金', + }, + 5: { + 'model': UserDashou, + 'balance': 'yajin', + 'pool': 'dongjie_chi_yajin', + 'solo_enabled': 'dandu_dongjie_yajin_enabled', + 'mode': 'dongjie_yajin_mode', + 'bili': 'dongjie_yajin_bili', + 'guding': 'dongjie_yajin_guding_jine', + 'role_name': '打手押金', + }, + 2: { + 'model': UserGuanshi, + 'balance': 'yue', + 'pool': 'dongjie_chi', + 'solo_enabled': 'dandu_dongjie_enabled', + 'mode': 'dongjie_mode', + 'bili': 'dongjie_bili', + 'guding': 'dongjie_guding_jine', + 'role_name': '管事分红', + }, + 6: { + 'model': UserShangjia, + 'balance': 'yue', + 'pool': 'dongjie_chi', + 'solo_enabled': 'dandu_dongjie_enabled', + 'mode': 'dongjie_mode', + 'bili': 'dongjie_bili', + 'guding': 'dongjie_guding_jine', + 'role_name': '商家余额', + }, +} + + +@dataclass +class FreezeConfig: + mode: int + bili: decimal.Decimal + guding_jine: decimal.Decimal + source: str # 'user' | 'public' + + +@dataclass +class FreezeSplitResult: + withdraw_jine: decimal.Decimal + freeze_jine: decimal.Decimal + freeze_only: bool + message: str = '' + + +@dataclass +class ApplyFreezeOutcome: + freeze_only: bool + withdraw_jine: decimal.Decimal + benchi_dongjie_jine: decimal.Decimal + message: str = '' + pool_after: decimal.Decimal = decimal.Decimal('0') + balance_after: decimal.Decimal = decimal.Decimal('0') + + +def _q(value) -> decimal.Decimal: + return decimal.Decimal(str(value or 0)).quantize( + decimal.Decimal('0.01'), rounding=decimal.ROUND_HALF_UP, + ) + + +def is_freeze_leixing(leixing: int) -> bool: + return int(leixing) in FREEZE_LEIXING_SET + + +def get_leixing_meta(leixing: int) -> dict: + meta = LEIXING_META.get(int(leixing)) + if not meta: + raise ValueError(f'不支持的冻结提现类型: {leixing}') + return meta + + +def _build_config(mode, bili, guding, source) -> FreezeConfig: + return FreezeConfig( + mode=int(mode or DONGJIE_MODE_RATIO), + bili=_q(bili), + guding_jine=_q(guding), + source=source, + ) + + +def resolve_freeze_config(profile, leixing: int) -> Optional[FreezeConfig]: + """返回生效冻结配置;None 表示本笔不冻结。""" + leixing = int(leixing) + if leixing not in FREEZE_LEIXING_SET: + return None + meta = get_leixing_meta(leixing) + + if getattr(profile, meta['solo_enabled'], False): + return _build_config( + getattr(profile, meta['mode']), + getattr(profile, meta['bili']), + getattr(profile, meta['guding']), + 'user', + ) + + wc = WithdrawConfig.objects.filter(id=1).only('dongjie_quanju_enabled').first() + if not wc or not wc.dongjie_quanju_enabled: + return None + + pub = TixianDongjieGonggongPeizhi.objects.filter(leixing=leixing).first() + if not pub or not pub.role_enabled: + return None + + return _build_config(pub.dongjie_mode, pub.dongjie_bili, pub.dongjie_guding_jine, 'public') + + +def compute_freeze_split( + balance: decimal.Decimal, + pool: decimal.Decimal, + config: Optional[FreezeConfig], + apply_jine: decimal.Decimal, +) -> FreezeSplitResult: + """纯计算:不修改数据库。""" + balance = _q(balance) + pool = _q(pool) + apply_jine = _q(apply_jine) + + if apply_jine <= 0: + raise ValueError('提现金额必须大于0') + if apply_jine > balance: + raise ValueError(f'余额不足,当前余额: {balance}') + + if config is None: + return FreezeSplitResult(apply_jine, decimal.Decimal('0'), False) + + if config.mode == DONGJIE_MODE_RATIO: + if config.bili <= 0: + return FreezeSplitResult(apply_jine, decimal.Decimal('0'), False) + if config.bili >= 1: + return FreezeSplitResult( + decimal.Decimal('0'), apply_jine, True, + message=f'已冻结 {apply_jine} 元进入资金池(比例100%)', + ) + freeze_jine = _q(balance * config.bili) + max_withdraw = balance - freeze_jine + if apply_jine > max_withdraw: + raise ValueError( + f'最多可提现 {max_withdraw} 元(需冻结 {freeze_jine} 元,占余额 {config.bili * 100:.2f}%)' + ) + return FreezeSplitResult(apply_jine, freeze_jine, False) + + if config.mode == DONGJIE_MODE_FIXED: + target = _q(config.guding_jine) + if target <= 0: + return FreezeSplitResult(apply_jine, decimal.Decimal('0'), False) + if pool >= target: + return FreezeSplitResult(apply_jine, decimal.Decimal('0'), False) + if apply_jine + pool < target: + return FreezeSplitResult( + decimal.Decimal('0'), apply_jine, True, + message=f'已冻结 {apply_jine} 元,冻结池 {pool + apply_jine}/{target} 元,满额后可正常提现', + ) + gap = target - pool + withdraw_jine = apply_jine - gap + if withdraw_jine <= 0: + return FreezeSplitResult( + decimal.Decimal('0'), apply_jine, True, + message=f'已冻结 {apply_jine} 元,冻结池 {pool + apply_jine}/{target} 元', + ) + return FreezeSplitResult(withdraw_jine, gap, False) + + return FreezeSplitResult(apply_jine, decimal.Decimal('0'), False) + + +def get_locked_profile(user_main: UserMain, leixing: int): + meta = get_leixing_meta(leixing) + Model = meta['model'] + try: + return Model.objects.select_for_update().get(user=user_main), meta + except Model.DoesNotExist: + raise ValueError(f'用户{user_main.yonghuid}缺少{meta["role_name"]}身份账户') + + +def _write_freeze_log(yonghuid, leixing, jine, pool_before, pool_after, caozuo, operator_id='', beizhu=''): + TixianDongjieJilu.objects.create( + yonghuid=yonghuid, + leixing=leixing, + jine=_q(jine), + dongjie_chi_before=_q(pool_before), + dongjie_chi_after=_q(pool_after), + caozuo=caozuo, + operator_id=operator_id or '', + beizhu=beizhu or '', + ) + + +def apply_balance_freeze(user_main: UserMain, leixing: int, apply_jine) -> ApplyFreezeOutcome: + """ + 提现申请前执行冻结拆分(须在 transaction.atomic 内调用)。 + - freeze_only=True:仅入池,不进入提现扣款流程 + - 否则返回 withdraw_jine(实际提现扣款额)与 benchi_dongjie_jine + """ + leixing = int(leixing) + apply_jine = _q(apply_jine) + + if not is_freeze_leixing(leixing): + return ApplyFreezeOutcome(False, apply_jine, decimal.Decimal('0')) + + profile, meta = get_locked_profile(user_main, leixing) + balance = _q(getattr(profile, meta['balance'])) + pool = _q(getattr(profile, meta['pool'])) + config = resolve_freeze_config(profile, leixing) + split = compute_freeze_split(balance, pool, config, apply_jine) + + # 仅将冻结部分划入冻结池;实际提现扣款由 deduct_balance 完成,避免重复扣款 + freeze_deduct = split.freeze_jine + if freeze_deduct <= 0: + if split.freeze_only: + freeze_deduct = apply_jine + else: + return ApplyFreezeOutcome( + freeze_only=False, + withdraw_jine=split.withdraw_jine, + benchi_dongjie_jine=decimal.Decimal('0'), + pool_after=pool, + balance_after=balance, + ) + + if freeze_deduct > balance: + raise ValueError('余额不足,无法完成冻结拆分') + if not split.freeze_only and freeze_deduct + split.withdraw_jine > balance: + raise ValueError('余额不足,无法完成冻结与提现拆分') + + new_balance = balance - freeze_deduct + new_pool = pool + freeze_deduct + + setattr(profile, meta['balance'], new_balance) + setattr(profile, meta['pool'], new_pool) + update_fields = [meta['balance'], meta['pool']] + if hasattr(profile, 'update_time'): + update_fields.append('update_time') + profile.save(update_fields=update_fields) + + if split.freeze_jine > 0: + _write_freeze_log( + user_main.yonghuid, leixing, freeze_deduct, pool, new_pool, + TixianDongjieJilu.CAOZUO_APPLY, + ) + logger.info( + '提现申请冻结 yonghuid=%s leixing=%s apply=%s freeze=%s withdraw=%s pool=%s balance=%s', + user_main.yonghuid, leixing, apply_jine, freeze_deduct, + split.withdraw_jine, new_pool, new_balance, + ) + + if split.freeze_only: + return ApplyFreezeOutcome( + freeze_only=True, + withdraw_jine=decimal.Decimal('0'), + benchi_dongjie_jine=freeze_deduct, + message=split.message or f'已冻结 {freeze_deduct} 元进入资金池', + pool_after=new_pool, + balance_after=new_balance, + ) + + return ApplyFreezeOutcome( + freeze_only=False, + withdraw_jine=split.withdraw_jine, + benchi_dongjie_jine=freeze_deduct, + pool_after=new_pool, + balance_after=new_balance, + ) + + +def admin_unfreeze_balance(user_main: UserMain, leixing: int, jine, operator_id='', beizhu='') -> decimal.Decimal: + """管理员解冻:冻结池 → 可提现余额。""" + leixing = int(leixing) + jine = _q(jine) + if jine <= 0: + raise ValueError('解冻金额必须大于0') + + profile, meta = get_locked_profile(user_main, leixing) + pool = _q(getattr(profile, meta['pool'])) + if jine > pool: + raise ValueError(f'解冻金额不能超过冻结池余额 {pool}') + + balance = _q(getattr(profile, meta['balance'])) + new_pool = pool - jine + new_balance = balance + jine + setattr(profile, meta['pool'], new_pool) + setattr(profile, meta['balance'], new_balance) + update_fields = [meta['balance'], meta['pool']] + if hasattr(profile, 'update_time'): + update_fields.append('update_time') + profile.save(update_fields=update_fields) + + _write_freeze_log( + user_main.yonghuid, leixing, jine, pool, new_pool, + TixianDongjieJilu.CAOZUO_UNFREEZE, operator_id=operator_id, beizhu=beizhu, + ) + return new_balance + + +def get_user_freeze_info(user_main: UserMain, leixing: int) -> dict: + """后台/详情页展示用。""" + leixing = int(leixing) + meta = get_leixing_meta(leixing) + Model = meta['model'] + try: + profile = Model.objects.get(user=user_main) + except Model.DoesNotExist: + return {'exists': False} + + balance = _q(getattr(profile, meta['balance'])) + pool = _q(getattr(profile, meta['pool'])) + config = resolve_freeze_config(profile, leixing) + max_apply = balance + if config and config.mode == DONGJIE_MODE_RATIO and config.bili > 0 and config.bili < 1: + freeze_part = _q(balance * config.bili) + max_apply = balance - freeze_part + + return { + 'exists': True, + 'leixing': leixing, + 'role_name': meta['role_name'], + 'balance': str(balance), + 'dongjie_chi': str(pool), + 'dandu_dongjie_enabled': bool(getattr(profile, meta['solo_enabled'], False)), + 'dongjie_mode': int(getattr(profile, meta['mode'], DONGJIE_MODE_RATIO)), + 'dongjie_bili': str(_q(getattr(profile, meta['bili'], 0))), + 'dongjie_guding_jine': str(_q(getattr(profile, meta['guding'], 0))), + 'max_apply_jine': str(max_apply), + 'freeze_active': config is not None, + 'freeze_source': config.source if config else '', + } + + +def get_public_freeze_settings() -> dict: + wc = WithdrawConfig.objects.filter(id=1).first() + quanju = bool(wc.dongjie_quanju_enabled) if wc else False + roles = [] + for leixing in (1, 2, 5, 6): + pub = TixianDongjieGonggongPeizhi.objects.filter(leixing=leixing).first() + meta = get_leixing_meta(leixing) + roles.append({ + 'leixing': leixing, + 'role_name': meta['role_name'], + 'role_enabled': bool(pub.role_enabled) if pub else False, + 'dongjie_mode': int(pub.dongjie_mode) if pub else DONGJIE_MODE_RATIO, + 'dongjie_bili': str(_q(pub.dongjie_bili)) if pub else '0.00', + 'dongjie_guding_jine': str(_q(pub.dongjie_guding_jine)) if pub else '0.00', + }) + return {'dongjie_quanju_enabled': quanju, 'dongjie_roles': roles} + + +def save_public_freeze_settings(quanju_enabled: bool, roles: list) -> None: + WithdrawConfig.objects.update_or_create(id=1, defaults={'dongjie_quanju_enabled': bool(quanju_enabled)}) + for row in roles: + leixing = int(row['leixing']) + if leixing not in FREEZE_LEIXING_SET: + continue + TixianDongjieGonggongPeizhi.objects.update_or_create( + leixing=leixing, + defaults={ + 'role_enabled': bool(row.get('role_enabled', False)), + 'dongjie_mode': int(row.get('dongjie_mode', DONGJIE_MODE_RATIO)), + 'dongjie_bili': _q(row.get('dongjie_bili', 0)), + 'dongjie_guding_jine': _q(row.get('dongjie_guding_jine', 0)), + }, + ) + + +def save_user_freeze_settings(user_main: UserMain, leixing: int, data: dict) -> dict: + leixing = int(leixing) + profile, meta = get_locked_profile(user_main, leixing) + + if 'dandu_dongjie_enabled' in data: + setattr(profile, meta['solo_enabled'], bool(data['dandu_dongjie_enabled'])) + if 'dongjie_mode' in data: + setattr(profile, meta['mode'], int(data['dongjie_mode'])) + if 'dongjie_bili' in data: + setattr(profile, meta['bili'], _q(data['dongjie_bili'])) + if 'dongjie_guding_jine' in data: + setattr(profile, meta['guding'], _q(data['dongjie_guding_jine'])) + + update_fields = [] + for key in (meta['solo_enabled'], meta['mode'], meta['bili'], meta['guding']): + if key in [f.name for f in profile._meta.fields]: + update_fields.append(key) + if hasattr(profile, 'update_time'): + update_fields.append('update_time') + profile.save(update_fields=update_fields) + return get_user_freeze_info(user_main, leixing) diff --git a/yonghu/migrations/0023_freeze_balance_feature.py b/yonghu/migrations/0023_freeze_balance_feature.py new file mode 100644 index 0000000..0b4efa1 --- /dev/null +++ b/yonghu/migrations/0023_freeze_balance_feature.py @@ -0,0 +1,182 @@ +# Generated manually for freeze feature +from decimal import Decimal + +from django.db import migrations, models + + +def seed_public_freeze(apps, schema_editor): + TixianDongjieGonggongPeizhi = apps.get_model('yonghu', 'TixianDongjieGonggongPeizhi') + for leixing in (1, 2, 5, 6): + TixianDongjieGonggongPeizhi.objects.get_or_create( + leixing=leixing, + defaults={ + 'role_enabled': False, + 'dongjie_mode': 1, + 'dongjie_bili': Decimal('0.0000'), + 'dongjie_guding_jine': Decimal('0.00'), + }, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0020_xcx_sys_peizhi_shangjia_paifa_limit'), + ('yonghu', '0022_backfill_tixianjilu_shenqing_jine'), + ] + + operations = [ + migrations.AddField( + model_name='withdrawconfig', + name='dongjie_quanju_enabled', + field=models.BooleanField(default=False, verbose_name='全员冻结总开关'), + ), + migrations.AddField( + model_name='userdashou', + name='dongjie_chi', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='佣金冻结资金池'), + ), + migrations.AddField( + model_name='userdashou', + name='dandu_dongjie_enabled', + field=models.BooleanField(default=False, verbose_name='佣金单独冻结配置'), + ), + migrations.AddField( + model_name='userdashou', + name='dongjie_mode', + field=models.PositiveSmallIntegerField(default=1, verbose_name='佣金冻结方式1比例2固定'), + ), + migrations.AddField( + model_name='userdashou', + name='dongjie_bili', + field=models.DecimalField(decimal_places=4, default=0.0, max_digits=5, verbose_name='佣金冻结比例'), + ), + migrations.AddField( + model_name='userdashou', + name='dongjie_guding_jine', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='佣金固定冻结额'), + ), + migrations.AddField( + model_name='userdashou', + name='dongjie_chi_yajin', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='押金冻结资金池'), + ), + migrations.AddField( + model_name='userdashou', + name='dandu_dongjie_yajin_enabled', + field=models.BooleanField(default=False, verbose_name='押金单独冻结配置'), + ), + migrations.AddField( + model_name='userdashou', + name='dongjie_yajin_mode', + field=models.PositiveSmallIntegerField(default=1, verbose_name='押金冻结方式1比例2固定'), + ), + migrations.AddField( + model_name='userdashou', + name='dongjie_yajin_bili', + field=models.DecimalField(decimal_places=4, default=0.0, max_digits=5, verbose_name='押金冻结比例'), + ), + migrations.AddField( + model_name='userdashou', + name='dongjie_yajin_guding_jine', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='押金固定冻结额'), + ), + migrations.AddField( + model_name='userguanshi', + name='dongjie_chi', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='冻结资金池'), + ), + migrations.AddField( + model_name='userguanshi', + name='dandu_dongjie_enabled', + field=models.BooleanField(default=False, verbose_name='单独冻结配置'), + ), + migrations.AddField( + model_name='userguanshi', + name='dongjie_mode', + field=models.PositiveSmallIntegerField(default=1, verbose_name='冻结方式1比例2固定'), + ), + migrations.AddField( + model_name='userguanshi', + name='dongjie_bili', + field=models.DecimalField(decimal_places=4, default=0.0, max_digits=5, verbose_name='冻结比例'), + ), + migrations.AddField( + model_name='userguanshi', + name='dongjie_guding_jine', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='固定冻结额'), + ), + migrations.AddField( + model_name='usershangjia', + name='dongjie_chi', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='冻结资金池'), + ), + migrations.AddField( + model_name='usershangjia', + name='dandu_dongjie_enabled', + field=models.BooleanField(default=False, verbose_name='单独冻结配置'), + ), + migrations.AddField( + model_name='usershangjia', + name='dongjie_mode', + field=models.PositiveSmallIntegerField(default=1, verbose_name='冻结方式1比例2固定'), + ), + migrations.AddField( + model_name='usershangjia', + name='dongjie_bili', + field=models.DecimalField(decimal_places=4, default=0.0, max_digits=5, verbose_name='冻结比例'), + ), + migrations.AddField( + model_name='usershangjia', + name='dongjie_guding_jine', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='固定冻结额'), + ), + migrations.AddField( + model_name='tixianjilu', + name='benchi_dongjie_jine', + field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=10, null=True, verbose_name='本次划入冻结池金额'), + ), + migrations.AddField( + model_name='tixianshenhejilu', + name='benchi_dongjie_jine', + field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='本次划入冻结池金额'), + ), + migrations.CreateModel( + name='TixianDongjieGonggongPeizhi', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('leixing', models.PositiveSmallIntegerField(db_index=True, unique=True, verbose_name='提现类型')), + ('role_enabled', models.BooleanField(default=False, verbose_name='该角色启用公共冻结')), + ('dongjie_mode', models.PositiveSmallIntegerField(choices=[(1, '按比例'), (2, '固定金额')], default=1, verbose_name='冻结方式')), + ('dongjie_bili', models.DecimalField(decimal_places=4, default=0.0, max_digits=5, verbose_name='冻结比例')), + ('dongjie_guding_jine', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='固定冻结目标额')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '提现公共冻结配置', + 'verbose_name_plural': '提现公共冻结配置', + 'db_table': 'tixian_dongjie_gonggong_peizhi', + }, + ), + migrations.CreateModel( + name='TixianDongjieJilu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')), + ('leixing', models.PositiveSmallIntegerField(db_index=True, verbose_name='提现类型')), + ('jine', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='变动金额')), + ('dongjie_chi_before', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='变动前冻结池')), + ('dongjie_chi_after', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='变动后冻结池')), + ('caozuo', models.PositiveSmallIntegerField(choices=[(1, '申请冻结'), (3, '管理员解冻')], verbose_name='操作类型')), + ('operator_id', models.CharField(blank=True, default='', max_length=30, verbose_name='操作人')), + ('beizhu', models.CharField(blank=True, default='', max_length=200, verbose_name='备注')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ], + options={ + 'verbose_name': '冻结流水', + 'verbose_name_plural': '冻结流水', + 'db_table': 'tixian_dongjie_jilu', + }, + ), + migrations.RunPython(seed_public_freeze, migrations.RunPython.noop), + ] diff --git a/yonghu/models.py b/yonghu/models.py index a23e463..091afef 100644 --- a/yonghu/models.py +++ b/yonghu/models.py @@ -140,6 +140,19 @@ class UserDashou(models.Model): kaioi_ewai_xiane = models.BooleanField(default=False, verbose_name='是否开启额外提现限额') # True=开启额外限额 ewai_xiane = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='额外提现限额(元)') + # ---------- 佣金提现冻结(leixing=1)---------- + dongjie_chi = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='佣金冻结资金池') + dandu_dongjie_enabled = models.BooleanField(default=False, verbose_name='佣金单独冻结配置') + dongjie_mode = models.PositiveSmallIntegerField(default=1, verbose_name='佣金冻结方式1比例2固定') + dongjie_bili = models.DecimalField(max_digits=5, decimal_places=4, default=0.0000, verbose_name='佣金冻结比例') + dongjie_guding_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='佣金固定冻结额') + # ---------- 押金提现冻结(leixing=5)---------- + dongjie_chi_yajin = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='押金冻结资金池') + dandu_dongjie_yajin_enabled = models.BooleanField(default=False, verbose_name='押金单独冻结配置') + dongjie_yajin_mode = models.PositiveSmallIntegerField(default=1, verbose_name='押金冻结方式1比例2固定') + dongjie_yajin_bili = models.DecimalField(max_digits=5, decimal_places=4, default=0.0000, verbose_name='押金冻结比例') + dongjie_yajin_guding_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='押金固定冻结额') + class Meta: db_table = 'user_dashou' verbose_name = '打手扩展表' @@ -172,9 +185,11 @@ class UserShangjia(models.Model): jinyueliushui = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='商家今月流水') create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') - - - + dongjie_chi = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='冻结资金池') + dandu_dongjie_enabled = models.BooleanField(default=False, verbose_name='单独冻结配置') + dongjie_mode = models.PositiveSmallIntegerField(default=1, verbose_name='冻结方式1比例2固定') + dongjie_bili = models.DecimalField(max_digits=5, decimal_places=4, default=0.0000, verbose_name='冻结比例') + dongjie_guding_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='固定冻结额') class Meta: db_table = 'user_shangjia' @@ -216,6 +231,12 @@ class UserGuanshi(models.Model): kaioi_ewai_xiane = models.BooleanField(default=False, verbose_name='是否开启额外提现限额') ewai_xiane = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='额外提现限额(元)') + dongjie_chi = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='冻结资金池') + dandu_dongjie_enabled = models.BooleanField(default=False, verbose_name='单独冻结配置') + dongjie_mode = models.PositiveSmallIntegerField(default=1, verbose_name='冻结方式1比例2固定') + dongjie_bili = models.DecimalField(max_digits=5, decimal_places=4, default=0.0000, verbose_name='冻结比例') + dongjie_guding_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='固定冻结额') + class Meta: db_table = 'user_guanshi' verbose_name = '管事扩展表' @@ -361,6 +382,10 @@ class Tixianjilu(models.Model): max_digits=10, decimal_places=2, null=True, blank=True, default=0, verbose_name='手续费', ) + benchi_dongjie_jine = models.DecimalField( + max_digits=10, decimal_places=2, null=True, blank=True, default=0, + verbose_name='本次划入冻结池金额', + ) # 状态与平台:用整数表示,具体含义由业务逻辑定义 zhuangtai = models.IntegerField(default=1, null=True, blank=True, db_index=True, verbose_name='提现状态1:审核2:成功3:失败') fangshi = models.IntegerField(null=True, blank=True,db_index=True, verbose_name='收款方式1:微信2:支付宝') @@ -413,6 +438,9 @@ class TixianShenheJilu(models.Model): shenqing_jine = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='申请提现金额(扣款额)') shouxufei = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='手续费') shijidaozhang = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='实际到账金额') + benchi_dongjie_jine = models.DecimalField( + max_digits=10, decimal_places=2, default=0, verbose_name='本次划入冻结池金额', + ) feilv = models.DecimalField(max_digits=5, decimal_places=4, default=0, verbose_name='手续费率') # 审核/驳回 bhliyou = models.TextField(null=True, blank=True, verbose_name='驳回/失败原因') @@ -503,6 +531,57 @@ class TixianAutoRecord(models.Model): return f"{self.tixian_id} - {self.get_zhuangtai_display()}" +class TixianDongjieGonggongPeizhi(models.Model): + """公共冻结配置(按提现类型一行)""" + DONGJIE_MODE_RATIO = 1 + DONGJIE_MODE_FIXED = 2 + DONGJIE_MODE_CHOICES = ( + (DONGJIE_MODE_RATIO, '按比例'), + (DONGJIE_MODE_FIXED, '固定金额'), + ) + + leixing = models.PositiveSmallIntegerField(unique=True, db_index=True, verbose_name='提现类型') + role_enabled = models.BooleanField(default=False, verbose_name='该角色启用公共冻结') + dongjie_mode = models.PositiveSmallIntegerField( + choices=DONGJIE_MODE_CHOICES, default=DONGJIE_MODE_RATIO, verbose_name='冻结方式', + ) + dongjie_bili = models.DecimalField(max_digits=5, decimal_places=4, default=0.0000, verbose_name='冻结比例') + dongjie_guding_jine = models.DecimalField( + max_digits=12, decimal_places=2, default=0.00, verbose_name='固定冻结目标额', + ) + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'tixian_dongjie_gonggong_peizhi' + verbose_name = '提现公共冻结配置' + verbose_name_plural = verbose_name + + +class TixianDongjieJilu(models.Model): + """冻结/解冻流水""" + CAOZUO_APPLY = 1 + CAOZUO_UNFREEZE = 3 + CAOZUO_CHOICES = ( + (CAOZUO_APPLY, '申请冻结'), + (CAOZUO_UNFREEZE, '管理员解冻'), + ) + + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') + leixing = models.PositiveSmallIntegerField(db_index=True, verbose_name='提现类型') + jine = models.DecimalField(max_digits=12, decimal_places=2, verbose_name='变动金额') + dongjie_chi_before = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='变动前冻结池') + dongjie_chi_after = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='变动后冻结池') + caozuo = models.PositiveSmallIntegerField(choices=CAOZUO_CHOICES, verbose_name='操作类型') + operator_id = models.CharField(max_length=30, blank=True, default='', verbose_name='操作人') + beizhu = models.CharField(max_length=200, blank=True, default='', verbose_name='备注') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'tixian_dongjie_jilu' + verbose_name = '冻结流水' + verbose_name_plural = verbose_name + + # yonghu/models.py class Xiugaijilu(models.Model): id = models.AutoField(primary_key=True, verbose_name='记录ID') diff --git a/yonghu/tests/__init__.py b/yonghu/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yonghu/tests/test_freeze_service.py b/yonghu/tests/test_freeze_service.py new file mode 100644 index 0000000..971f385 --- /dev/null +++ b/yonghu/tests/test_freeze_service.py @@ -0,0 +1,70 @@ +"""资金冻结拆分算法单元测试""" +import decimal +from unittest import TestCase + +from yonghu.freeze_service import ( + DONGJIE_MODE_FIXED, + DONGJIE_MODE_RATIO, + FreezeConfig, + compute_freeze_split, +) + + +class ComputeFreezeSplitTests(TestCase): + def test_no_config_returns_full_withdraw(self): + r = compute_freeze_split( + decimal.Decimal('100'), decimal.Decimal('0'), None, decimal.Decimal('50'), + ) + self.assertEqual(r.withdraw_jine, decimal.Decimal('50.00')) + self.assertEqual(r.freeze_jine, decimal.Decimal('0.00')) + self.assertFalse(r.freeze_only) + + def test_ratio_partial_freeze(self): + cfg = FreezeConfig(DONGJIE_MODE_RATIO, decimal.Decimal('0.30'), decimal.Decimal('0'), 'public') + r = compute_freeze_split( + decimal.Decimal('100'), decimal.Decimal('0'), cfg, decimal.Decimal('50'), + ) + self.assertEqual(r.freeze_jine, decimal.Decimal('30.00')) + self.assertEqual(r.withdraw_jine, decimal.Decimal('50.00')) + self.assertFalse(r.freeze_only) + + def test_ratio_exceed_max_raises(self): + cfg = FreezeConfig(DONGJIE_MODE_RATIO, decimal.Decimal('0.30'), decimal.Decimal('0'), 'public') + with self.assertRaises(ValueError): + compute_freeze_split( + decimal.Decimal('100'), decimal.Decimal('0'), cfg, decimal.Decimal('80'), + ) + + def test_ratio_100_percent_freeze_only(self): + cfg = FreezeConfig(DONGJIE_MODE_RATIO, decimal.Decimal('1'), decimal.Decimal('0'), 'public') + r = compute_freeze_split( + decimal.Decimal('100'), decimal.Decimal('0'), cfg, decimal.Decimal('40'), + ) + self.assertTrue(r.freeze_only) + self.assertEqual(r.freeze_jine, decimal.Decimal('40.00')) + self.assertEqual(r.withdraw_jine, decimal.Decimal('0.00')) + + def test_fixed_pool_not_full_freeze_only(self): + cfg = FreezeConfig(DONGJIE_MODE_FIXED, decimal.Decimal('0'), decimal.Decimal('500'), 'public') + r = compute_freeze_split( + decimal.Decimal('100'), decimal.Decimal('200'), cfg, decimal.Decimal('80'), + ) + self.assertTrue(r.freeze_only) + self.assertEqual(r.freeze_jine, decimal.Decimal('80.00')) + + def test_fixed_pool_partial_then_withdraw(self): + cfg = FreezeConfig(DONGJIE_MODE_FIXED, decimal.Decimal('0'), decimal.Decimal('500'), 'public') + r = compute_freeze_split( + decimal.Decimal('200'), decimal.Decimal('400'), cfg, decimal.Decimal('150'), + ) + self.assertFalse(r.freeze_only) + self.assertEqual(r.freeze_jine, decimal.Decimal('100.00')) + self.assertEqual(r.withdraw_jine, decimal.Decimal('50.00')) + + def test_fixed_pool_already_full(self): + cfg = FreezeConfig(DONGJIE_MODE_FIXED, decimal.Decimal('0'), decimal.Decimal('500'), 'public') + r = compute_freeze_split( + decimal.Decimal('100'), decimal.Decimal('500'), cfg, decimal.Decimal('60'), + ) + self.assertEqual(r.freeze_jine, decimal.Decimal('0.00')) + self.assertEqual(r.withdraw_jine, decimal.Decimal('60.00')) diff --git a/yonghu/tixian_shenhe_services.py b/yonghu/tixian_shenhe_services.py index a3f57c3..839efe8 100644 --- a/yonghu/tixian_shenhe_services.py +++ b/yonghu/tixian_shenhe_services.py @@ -208,11 +208,13 @@ def check_dashou_has_valid_huiyuan(yonghuid): return False, '您的会员已全部过期,请先续费后再操作' -def validate_withdraw_eligibility(user_main, leixing, jine): +def validate_withdraw_eligibility(user_main, leixing, jine, balance_check_amount=None): """ 提现资格校验(对齐手动提现 TixianShenqingView) + balance_check_amount: 余额门槛(含冻结额外占用),默认等于 jine """ yonghuid = user_main.yonghuid + check_amt = balance_check_amount if balance_check_amount is not None else jine if leixing != 6: fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2) @@ -228,7 +230,7 @@ def validate_withdraw_eligibility(user_main, leixing, jine): return False, '打手账号已被封禁,无法提现', {} if dashou.zhuangtai != 1: return False, '有订单进行中,请完成后提现', {} - if dashou.yue < jine: + if dashou.yue < check_amt: return False, f'余额不足,当前余额: {dashou.yue}', {} return True, '', {'nicheng': dashou.nicheng or ''} @@ -239,7 +241,7 @@ def validate_withdraw_eligibility(user_main, leixing, jine): return False, '您不是管事身份,无法提现分红', {} if guanshi.zhuangtai != 1: return False, '管事账号已被封禁,无法提现', {} - if guanshi.yue < jine: + if guanshi.yue < check_amt: return False, f'余额不足,当前余额: {guanshi.yue}', {} return True, '', {'nicheng': '管事用户'} @@ -274,7 +276,7 @@ def validate_withdraw_eligibility(user_main, leixing, jine): return False, '打手账号已禁用,无法提取押金', {} if dashou.zhuangtai != 1: return False, '您有订单进行中,请完成后提取押金', {} - if dashou.yajin < jine: + if dashou.yajin < check_amt: return False, f'押金余额不足,当前押金: {dashou.yajin}', {} unfinished = Dingdan.objects.filter( jiedan_dashou_id=yonghuid, @@ -293,7 +295,7 @@ def validate_withdraw_eligibility(user_main, leixing, jine): return False, '您不是商家身份,无法提现商家余额', {} if shangjia.zhuangtai != 1: return False, '商家账号已被禁用,无法提现', {} - if shangjia.yue < jine: + if shangjia.yue < check_amt: return False, f'商家余额不足,当前余额: {shangjia.yue}', {} return True, '', {'nicheng': shangjia.nicheng or ''} @@ -1520,35 +1522,80 @@ def get_audit_for_collect(shenhe_danhao, yonghuid, shenhe_jilu_id=None): def create_audit_application(user_main, leixing, jine): """ - 提交审核申请:校验 → 扣款 → 写审核表 + 提现记录表 + 提交审核申请:校验 → 冻结拆分 → 扣款 → 写审核表 + 提现记录表 不创建打款记录表(TixianAutoRecord 仅在收款时创建) """ - yonghuid = user_main.yonghuid + from yonghu.freeze_service import ( + apply_balance_freeze, + compute_freeze_split, + get_leixing_meta, + is_freeze_leixing, + resolve_freeze_config, + ) - ok, msg, extra = validate_withdraw_eligibility(user_main, leixing, jine) + yonghuid = user_main.yonghuid + apply_jine = decimal.Decimal(str(jine)) + + balance_check = apply_jine + withdraw_jine_preview = apply_jine + if is_freeze_leixing(leixing): + meta = get_leixing_meta(leixing) + Model = meta['model'] + profile = Model.objects.get(user=user_main) + config = resolve_freeze_config(profile, leixing) + balance = decimal.Decimal(str(getattr(profile, meta['balance']))) + pool = decimal.Decimal(str(getattr(profile, meta['pool']))) + split = compute_freeze_split(balance, pool, config, apply_jine) + if split.freeze_only: + balance_check = apply_jine + withdraw_jine_preview = decimal.Decimal('0') + else: + balance_check = split.freeze_jine + split.withdraw_jine + withdraw_jine_preview = split.withdraw_jine + + ok, msg, extra = validate_withdraw_eligibility( + user_main, leixing, withdraw_jine_preview or apply_jine, + balance_check_amount=balance_check, + ) if not ok: raise ValueError(msg) - feilv = get_tixian_feilv(leixing) - shouxufei, shijidaozhang = calc_fee_amounts(jine, feilv) - if shijidaozhang <= 0: - raise ValueError('提现金额过低,扣除手续费后无实际到账金额,请增加申请金额') - assert_shijidaozhang_below_wechat_limit(shijidaozhang) - shenhe_danhao = generate_shenhe_danhao() nicheng = extra.get('nicheng', '') with transaction.atomic(): - nicheng = deduct_balance(user_main, leixing, jine) or nicheng + freeze_out = apply_balance_freeze(user_main, leixing, apply_jine) + if freeze_out.freeze_only: + response_data = get_balance_response_data(user_main, leixing) + response_data.update({ + 'freeze_only': True, + 'msg': freeze_out.message, + 'dongjie_chi': str(freeze_out.pool_after), + 'benchi_dongjie_jine': str(freeze_out.benchi_dongjie_jine), + 'leixing': leixing, + }) + return response_data + + withdraw_jine = freeze_out.withdraw_jine + benchi_dongjie = freeze_out.benchi_dongjie_jine + + feilv = get_tixian_feilv(leixing) + shouxufei, shijidaozhang = calc_fee_amounts(withdraw_jine, feilv) + if shijidaozhang <= 0: + raise ValueError('提现金额过低,扣除手续费后无实际到账金额,请增加申请金额') + assert_shijidaozhang_below_wechat_limit(shijidaozhang) + + nicheng = deduct_balance(user_main, leixing, withdraw_jine) or nicheng audit_kwargs = { 'shenhe_danhao': shenhe_danhao, 'yonghuid': yonghuid, 'leixing': leixing, 'zhuangtai': 1, - 'shenqing_jine': jine, + 'shenqing_jine': withdraw_jine, 'shouxufei': shouxufei, 'shijidaozhang': shijidaozhang, + 'benchi_dongjie_jine': benchi_dongjie, 'feilv': feilv, } if has_dakuan_mode_field(): @@ -1564,8 +1611,12 @@ def create_audit_application(user_main, leixing, jine): zhifu=user_main.zhifu or '', skzhanghao=user_main.skzhanghao or '', jine=shijidaozhang, + shenqing_jine=withdraw_jine, + shouxufei=shouxufei, + benchi_dongjie_jine=benchi_dongjie, zhuangtai=1, fangshi=1, + dakuan_mode=2, shenheid=None, bhliyou='', shenhe_jilu_id=audit.id, @@ -1582,8 +1633,11 @@ def create_audit_application(user_main, leixing, jine): 'tixianjilu_id': tixian_record.id, 'shouxufei': str(shouxufei), 'shijidaozhang': str(shijidaozhang), + 'shenqing_jine': str(withdraw_jine), + 'benchi_dongjie_jine': str(benchi_dongjie), 'current_rate': str(feilv.quantize(decimal.Decimal('0.0001'))), 'leixing': leixing, 'zhuangtai': 1, + 'freeze_only': False, }) return response_data diff --git a/yonghu/tixian_shenhe_views.py b/yonghu/tixian_shenhe_views.py index 6f9e309..e22ca9e 100644 --- a/yonghu/tixian_shenhe_views.py +++ b/yonghu/tixian_shenhe_views.py @@ -173,6 +173,13 @@ class TixianZddkshApplyView(APIView): logger.error(f'提现审核申请异常: {e}', exc_info=True) return Response({'code': 99, 'msg': '提现申请失败,请稍后重试'}) + if data.get('freeze_only'): + return Response({ + 'code': 0, + 'msg': data.get('msg') or '资金已划入冻结池', + 'data': data, + }) + return Response({'code': 0, 'msg': '提现申请已提交,请等待审核', 'data': data}) finally: diff --git a/yonghu/views.py b/yonghu/views.py index 7e0b0b5..90352a6 100644 --- a/yonghu/views.py +++ b/yonghu/views.py @@ -1909,6 +1909,12 @@ class TixianShenqingView(APIView): return Response({'code': 5, 'msg': '无效的收款方式,只能是1(微信)或2(支付宝)'}, status=status.HTTP_400_BAD_REQUEST) + # 打手佣金/管事/押金/商家:统一走冻结+扣款(手动打款) + if leixing in (1, 2, 5, 6): + return self._submit_manual_withdraw_with_freeze( + user_main, yonghuid, leixing, jine, fangshi, + ) + # 2. 获取对应费率及手续费 current_rate = decimal.Decimal('0.00') rate_key = { @@ -2057,6 +2063,115 @@ class TixianShenqingView(APIView): return Response({'code': 99, 'msg': f'系统错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + def _submit_manual_withdraw_with_freeze(self, user_main, yonghuid, leixing, apply_jine, fangshi): + """手动打款:冻结拆分 + 扣款 + 创建提现记录(与自动打款冻结规则一致)""" + from yonghu.freeze_service import ( + apply_balance_freeze, + compute_freeze_split, + get_leixing_meta, + is_freeze_leixing, + resolve_freeze_config, + ) + from yonghu.tixian_shenhe_services import deduct_balance, validate_withdraw_eligibility + + rate_key = {1: '5', 2: '6', 5: '11', 6: '10'}.get(leixing) + rate_obj = Lilubiao.objects.filter(fadanpingtai=rate_key).first() + if not rate_obj: + return Response({'code': 41, 'msg': f'提现费率未配置(类型{leixing})'}, + status=status.HTTP_400_BAD_REQUEST) + current_rate = decimal.Decimal(str(rate_obj.lilu)) + + balance_check = apply_jine + withdraw_preview = apply_jine + if is_freeze_leixing(leixing): + meta = get_leixing_meta(leixing) + Model = meta['model'] + profile = Model.objects.get(user=user_main) + config = resolve_freeze_config(profile, leixing) + balance = decimal.Decimal(str(getattr(profile, meta['balance']))) + pool = decimal.Decimal(str(getattr(profile, meta['pool']))) + split = compute_freeze_split(balance, pool, config, apply_jine) + if split.freeze_only: + balance_check = apply_jine + withdraw_preview = decimal.Decimal('0') + else: + balance_check = split.freeze_jine + split.withdraw_jine + withdraw_preview = split.withdraw_jine + + ok, msg, extra = validate_withdraw_eligibility( + user_main, leixing, withdraw_preview or apply_jine, + balance_check_amount=balance_check, + ) + if not ok: + return Response({'code': 400, 'msg': msg}, status=status.HTTP_400_BAD_REQUEST) + + try: + with transaction.atomic(): + freeze_out = apply_balance_freeze(user_main, leixing, apply_jine) + if freeze_out.freeze_only: + return Response({ + 'code': 0, + 'msg': freeze_out.message or '资金已划入冻结池', + 'data': { + 'freeze_only': True, + 'dongjie_chi': str(freeze_out.pool_after), + 'benchi_dongjie_jine': str(freeze_out.benchi_dongjie_jine), + 'leixing': leixing, + }, + }) + + withdraw_jine = freeze_out.withdraw_jine + benchi_dongjie = freeze_out.benchi_dongjie_jine + shouxufei = withdraw_jine * current_rate + shijidaozhang = withdraw_jine - shouxufei + if shijidaozhang <= decimal.Decimal('0'): + raise ValueError('提现金额过低,扣除手续费后无实际到账') + + nicheng = deduct_balance(user_main, leixing, withdraw_jine) or extra.get('nicheng', '') + + tixian_record = Tixianjilu.objects.create( + yonghuid=yonghuid, + avatar=user_main.avatar or '', + phone=user_main.phone or '', + nicheng=nicheng, + leixing=leixing, + zhifu=user_main.zhifu or '', + skzhanghao=user_main.skzhanghao or '', + jine=shijidaozhang, + shenqing_jine=withdraw_jine, + shouxufei=shouxufei, + benchi_dongjie_jine=benchi_dongjie, + zhuangtai=1, + fangshi=fangshi, + dakuan_mode=1, + shenheid=None, + bhliyou='', + ) + except ValueError as e: + return Response({'code': 400, 'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + return Response({'code': 99, 'msg': f'创建提现记录失败: {str(e)}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response({ + 'code': 0, + 'msg': '提现申请提交成功', + 'data': { + 'tixian_id': tixian_record.id, + 'leixing': leixing, + 'jine': str(withdraw_jine.quantize(decimal.Decimal('0.01'))), + 'shenqing_jine': str(withdraw_jine.quantize(decimal.Decimal('0.01'))), + 'benchi_dongjie_jine': str(benchi_dongjie.quantize(decimal.Decimal('0.01'))), + 'fangshi': fangshi, + 'zhuangtai': 1, + 'create_time': tixian_record.create_time.strftime('%Y-%m-%d %H:%M:%S'), + 'current_rate': str(current_rate.quantize(decimal.Decimal('0.0000'))), + 'shouxufei': str(shouxufei.quantize(decimal.Decimal('0.01'))), + 'shijidaozhang': str(shijidaozhang.quantize(decimal.Decimal('0.01'))), + 'tip': f'本次提现手续费率{current_rate * 100}%,手续费{shouxufei}元,实际到账{shijidaozhang}元', + }, + }) + # ==================== 校验函数 ==================== def _check_dashou_commission_withdraw(self, user_main, jine):