实现提现资金冻结:手动/自动打款共用冻结拆分,驳回仅退 shenqing_jine。
新增公共/用户冻结配置、流水表与后台解冻接口;修复冻结与扣款重复扣余额问题。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
146a64b710
commit
8678f54459
13 files changed
+1447
-20
No files matched your search
@@ -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 个确认点,回复「对/不对」或补充后,我再按此文档写代码。**
|
||||
@@ -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'})
|
||||
+2
-1
@@ -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='后台修改组长信息'),
|
||||
|
||||
@@ -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': '设置修改成功'})
|
||||
|
||||
|
||||
@@ -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='更新时间')
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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),
|
||||
]
|
||||
+82
-3
@@ -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')
|
||||
|
||||
Whitespace-only changes.
@@ -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'))
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
+115
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user