|
|
@@ -0,0 +1,649 @@
|
|
|
+# 小程序提现机制设计(含税务优化)
|
|
|
+
|
|
|
+> **状态:** Draft v1
|
|
|
+> **适用范围:** 超级会员、能量师(所有有分销佣金收益的用户角色)
|
|
|
+> **提现方式:** 微信「企业付款到零钱」(原「企业红包」API)
|
|
|
+> **税务处理:** 预扣代缴 + 年度汇总
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 整体链路
|
|
|
+
|
|
|
+```text
|
|
|
+用户申请提现
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+后端校验(余额、身份、频率、首笔退费资格)
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+创建提现记录 → 状态:QUEUED(排队中)
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+前端提示:「申请已提交,预计今晚到账」
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+┌─ 每日定时任务(23:30,微信日结前)────────────────────┐
|
|
|
+│ │
|
|
|
+│ Step 1: 拉取当日所有 QUEUED 记录 │
|
|
|
+│ Step 2: 检查微信商户 Master 余额 │
|
|
|
+│ Step 3: 按优先级排序: │
|
|
|
+│ ① 首笔退费(退款通道,扣退款余额) │
|
|
|
+│ ② 常规微信直付(≤¥800,免个税) │
|
|
|
+│ ③ 第三方通道(>¥800,代扣个税) │
|
|
|
+│ Step 4: 逐笔执行 │
|
|
|
+│ ├─ 足够余额 → 调用对应通道 API → SUCCESS │
|
|
|
+│ └─ 余额不足 → 跳过,保持 QUEUED,次日重试 │
|
|
|
+│ │
|
|
|
+│ Step 5: 更新微信 Master 余额缓存 │
|
|
|
+│ Step 6: 推送通知(成功/失败) │
|
|
|
+└─────────────────────────────────────────────────────────┘
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+超过 3 天未处理 → 告警运维 → 人工介入
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 佣金科目体系
|
|
|
+
|
|
|
+当前 4 条产品线 → 全部提取至**统一可提现余额**:
|
|
|
+
|
|
|
+| 佣金类型 | 税率级次 | 是否可提现 |
|
|
|
+|---------|---------|-----------|
|
|
|
+| B端佣金(能量师推荐,固定金额) | 劳务报酬 | ✅ |
|
|
|
+| C端佣金(40%+5%,百分比) | 劳务报酬 | ✅ |
|
|
|
+| 超级会员佣金(30%+5%,百分比) | 劳务报酬 | ✅ |
|
|
|
+| 人工方案佣金(平台留存后返还) | 劳务报酬 | ✅ |
|
|
|
+| 提现退还(失败回滚) | — | 退回可提现余额 |
|
|
|
+
|
|
|
+> **结论:** 所有分销佣金合并为一个「可提现余额」,不做科目隔离。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 提现规则(后端可配置)
|
|
|
+
|
|
|
+### 3.1 规则表(sys_config + withdraw_rules 表)
|
|
|
+
|
|
|
+| 参数 | 默认值 | 说明 |
|
|
|
+|------|--------|------|
|
|
|
+| `withdraw.min_amount` | **¥100** | 单次最低提现金额(分) |
|
|
|
+| `withdraw.max_amount_per_time` | ¥10,000 | 单次最高提现金额 |
|
|
|
+| `withdraw.max_per_week` | **1次** | 每人每周限提次数 |
|
|
|
+| `withdraw.service_fee_rate` | **0.6%** | 微信企业付款手续费(平台承担) |
|
|
|
+| `withdraw.week_reset_day` | **周一 00:00** | 周频次重置时间 |
|
|
|
+| `withdraw.min_balance_keep` | ¥0 | 提现后保留的最低余额 |
|
|
|
+| `withdraw.auto_approve` | true | 是否自动审核(≤¥500 免审) |
|
|
|
+| `withdraw.fail_limit_per_day` | **3次** | 日失败上限,超限冻结提现 24h |
|
|
|
+| `withdraw.commission_expiry_days` | 0(永不过期) | 佣金过期天数,0=永久有效 |
|
|
|
+| `withdraw.refund_period_days` | **30** | 首笔提现退费窗口期(付费后30天内) |
|
|
|
+| `withdraw.third_party_threshold` | **¥800** | 超过此金额使用第三方结算通道(分) |
|
|
|
+| `withdraw.third_party_channel` | `wechat_settle` | 第三方通道类型:wechat_settle / lianlian / huifu |
|
|
|
+| `withdraw.refund_reason_tpl` | `推广佣金退款-{user_id}-{order_no}` | 退费备注模板 |
|
|
|
+
|
|
|
+### 3.2 提现状态机
|
|
|
+
|
|
|
+```
|
|
|
+用户提交申请
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+INIT → QUEUED(等待夜间批处理)
|
|
|
+ │
|
|
|
+ ▼ [定时任务 23:30 触发]
|
|
|
+BATCH_PROCESSING(正在处理)
|
|
|
+ │
|
|
|
+ ├─ 余额足够 → 通道调用
|
|
|
+ │ ├─ 退费(首笔)→ 微信退款 API → SUCCESS
|
|
|
+ │ ├─ 微信直付 → 企业付款 API → SUCCESS
|
|
|
+ │ └─ 第三方 → 渠道 API → SUCCESS
|
|
|
+ │
|
|
|
+ ├─ 余额不足 → 跳过,保持 QUEUED → 次日重试
|
|
|
+ │
|
|
|
+ └─ 通道报错 → FAILED(可重新发起)
|
|
|
+ │
|
|
|
+ └─ 超过 3 天未成功 → FROZEN(告警运维)
|
|
|
+```
|
|
|
+
|
|
|
+**状态说明:**
|
|
|
+
|
|
|
+| 状态 | 说明 | 可见用户 |
|
|
|
+|------|------|---------|
|
|
|
+| `QUEUED` | 提交成功,等待夜间批处理 | ✅ 显示「预计今晚到账」 |
|
|
|
+| `BATCH_PROCESSING` | 定时任务正在处理(通常 23:30-00:30) | ✅ 显示「处理中」 |
|
|
|
+| `SUCCESS` | 已到账 | ✅ 显示「已到账」+ 到账金额 |
|
|
|
+| `FAILED` | 通道失败(余额不足/网络错误) | ✅ 显示「失败」+ 原因 +「重新申请」按钮 |
|
|
|
+| `REJECTED` | 风控不通过 | ✅ 显示「未通过审核」+ 原因 |
|
|
|
+| `FROZEN` | 超过 3 天未处理,需人工介入 | ✅ 显示「请联系客服」 |
|
|
|
+
|
|
|
+### 3.3 夜间定时批处理任务
|
|
|
+
|
|
|
+所有提现(含退费、直付、第三方)**不实时调用微信 API**,而是由定时任务统一处理。
|
|
|
+
|
|
|
+**触发时机:** 每日 23:30(微信商户平台通常在 00:00 前完成日结,确保当日余额可 Master)
|
|
|
+
|
|
|
+**处理流程:**
|
|
|
+
|
|
|
+```java
|
|
|
+@Scheduled(cron = "0 30 23 * * ?") // 每日 23:30
|
|
|
+public void nightlyWithdrawBatch() {
|
|
|
+ // 1. 拉取当日 QUEUED 记录
|
|
|
+ List<WithdrawRecord> queue = recordRepo.findByStatusOrderByCreatedAt("QUEUED");
|
|
|
+
|
|
|
+ // 2. 查询实时 Master 余额(调用微信「查询商户余额」API 或本地缓存)
|
|
|
+ long masterBalance = queryWechatMasterBalance();
|
|
|
+ long refundBalance = queryWechatRefundBalance();
|
|
|
+
|
|
|
+ // 3. 按优先级排序
|
|
|
+ queue.sort(Comparator
|
|
|
+ .comparing((r) -> !r.isEligibleRefund()) // 退费优先
|
|
|
+ .thenComparing(r -> r.getAmountFen())); // 小额优先(更容易成功)
|
|
|
+
|
|
|
+ for (WithdrawRecord record : queue) {
|
|
|
+ // 4. 判断通道
|
|
|
+ WithdrawChannel channel = resolveChannel(record, masterBalance, refundBalance);
|
|
|
+
|
|
|
+ if (channel == null) {
|
|
|
+ // 余额不足,跳过,等待下一批
|
|
|
+ record.setRetryCount(record.getRetryCount() + 1);
|
|
|
+ recordRepo.save(record);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 预扣税务(非退费通道)
|
|
|
+ TaxResult tax = channel.isRefund()
|
|
|
+ ? TaxResult.zero() // 退费不免税
|
|
|
+ : TaxCalculator.calc(record.getAmountFen());
|
|
|
+
|
|
|
+ // 6. 扣减冻结余额
|
|
|
+ walletService.debitFrozen(record.getUserId(), record.getAmountFen());
|
|
|
+
|
|
|
+ // 7. 调用对应通道
|
|
|
+ record.setStatus("BATCH_PROCESSING");
|
|
|
+ recordRepo.save(record);
|
|
|
+
|
|
|
+ try {
|
|
|
+ switch (channel) {
|
|
|
+ case WECHAT_REFUND -> executeRefund(record, refundBalance);
|
|
|
+ case WECHAT_TRANSFER -> executeWechatTransfer(record, masterBalance, tax);
|
|
|
+ case THIRD_PARTY -> executeThirdParty(record, tax);
|
|
|
+ }
|
|
|
+ record.setStatus("SUCCESS");
|
|
|
+ record.setProcessedAt(LocalDateTime.now());
|
|
|
+ } catch (InsufficientBalanceException e) {
|
|
|
+ // 余额幻觉(并发场景下余额被其他批次扣走)
|
|
|
+ walletService.releaseFrozen(record.getUserId(), record.getAmountFen());
|
|
|
+ record.setStatus("QUEUED");
|
|
|
+ record.setRetryCount(record.getRetryCount() + 1);
|
|
|
+ } catch (Exception e) {
|
|
|
+ record.setStatus("FAILED");
|
|
|
+ record.setStatusRemark(e.getMessage());
|
|
|
+ walletService.releaseFrozen(record.getUserId(), record.getAmountFen());
|
|
|
+ }
|
|
|
+ recordRepo.save(record);
|
|
|
+
|
|
|
+ // 8. 更新本地余额缓存
|
|
|
+ masterBalance = queryWechatMasterBalance();
|
|
|
+ refundBalance = queryWechatRefundBalance();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 9. 推送通知(微信模板消息)
|
|
|
+ notifyBatchResults(queue);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.4 微信商户账户余额约束(关键前提)
|
|
|
+
|
|
|
+```
|
|
|
+用户支付 ¥1,314 → 微信商户账户 +¥1,314
|
|
|
+ │
|
|
|
+ ├─ 微信先收手续费 0.6%(约 ¥7.88)
|
|
|
+ │
|
|
|
+ └─ 微信商户账户实际可 Master 余额 ≈ ¥1,306.12
|
|
|
+ │
|
|
|
+ ├─ 企业付款提现给用户 → 从此余额扣款
|
|
|
+ │
|
|
|
+ └─ 退费(退款)→ 扣「退款余额」(独立于 Master 余额)
|
|
|
+```
|
|
|
+
|
|
|
+**核心影响:**
|
|
|
+
|
|
|
+| 场景 | 约束 | 系统处理 |
|
|
|
+|------|------|---------|
|
|
|
+| 商户 Master 余额充足 | 按正常流程处理 | 无需特殊处理 |
|
|
|
+| 商户 Master 余额不足 | 微信接口报错 `NOT_ENOUGH` | 前端提示「余额不足,暂无法提现」,并提供「预约提现」(余额到账后自动处理) |
|
|
|
+| 首笔退费(退款通道) | 走微信「退款」API,扣「退款余额」;退款余额为 0 时不可用 | 系统降级为:标准微信直付 + 预扣个税,用户不再享受首笔退费资格 |
|
|
|
+| 退款余额为 0 | 退费通道不可用 | 降级为标准提现,首笔退费资格作废 |
|
|
|
+
|
|
|
+**运维要求(平台侧):**
|
|
|
+
|
|
|
+1. **每日对账** — 与微信商户平台核对「可 Master 余额」,低于阈值(建议 ¥5,000)时告警运维
|
|
|
+2. **定期充值** — 平台运营从企业银行账户向微信商户账户充值(在微信商户平台操作)
|
|
|
+3. **退款余额独立监控** — 退款余额与 Master 余额分开追踪,各有各的余额上限
|
|
|
+4. **提现排队机制** — 余额不足时,用户申请进入排队队列,后台定时任务每 15 分钟重试,最长等待 24 小时
|
|
|
+
|
|
|
+**新增 sys_config 参数:**
|
|
|
+
|
|
|
+| 参数 | 默认值 | 说明 |
|
|
|
+|------|--------|------|
|
|
|
+| `withdraw.wechat_master_balance_alert` | `500000` | Master 余额告警阈值(分,¥5,000) |
|
|
|
+| `withdraw.queue_retry_minutes` | `15` | 排队重试间隔(分钟) |
|
|
|
+| `withdraw.queue_max_wait_minutes` | `1440` | 最大排队等待(分钟,24h) |
|
|
|
+| `withdraw.refund_balance_fallback` | `true` | 退款余额不足时是否降级为标准提现 |
|
|
|
+| `withdraw.auto_topup_threshold` | `100000` | Master 余额低于此值时触发告警通知运营充值(分,¥1,000) |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 税务方案
|
|
|
+
|
|
|
+### 4.1 中国个税框架(劳务报酬所得)
|
|
|
+
|
|
|
+根据《个人所得税法》,个人推广佣金属于 **「劳务报酬所得」**,适用比例税率,由支付方(平台)**预扣代缴**。
|
|
|
+
|
|
|
+**劳务报酬预扣率表:**
|
|
|
+
|
|
|
+| 每次收入(税后估计) | taxable income 计算 | 税率 | 速算扣除 |
|
|
|
+|-------------------|-------------------|-----|---------|
|
|
|
+| ≤ ¥800 | 免征 | 0% | — |
|
|
|
+| ¥800 – ¥4,000 | (收入 – ¥800) × 20% | 20% | 0 |
|
|
|
+| ¥4,000 – ¥25,000 | 收入 × 80% × 20% – ¥1,050 | 20% | ¥1,050 |
|
|
|
+| ¥25,000 – ¥62,500 | 收入 × 80% × 30% – ¥4,050 | 30% | ¥4,050 |
|
|
|
+| > ¥62,500 | 收入 × 80% × 40% – ¥7,050 | 40% | ¥7,050 |
|
|
|
+
|
|
|
+### 4.2 预扣模式选择
|
|
|
+
|
|
|
+| 模式 | 描述 | 优劣 |
|
|
|
+|------|------|------|
|
|
|
+| **A:每次提现预扣(推荐)** | 每次发钱时直接按劳务报酬税率计算个税 | 合规,平台无连带责任;用户到手金额透明 |
|
|
|
+| **B:年度汇算** | 全年预扣,次年3-6月用户自行汇算 | 复杂,平台需提供年度明细 |
|
|
|
+
|
|
|
+**系统采用模式 A**(预扣代缴),并提供年度明细(模式 B 的简化版)。
|
|
|
+
|
|
|
+### 4.3 税务公式
|
|
|
+
|
|
|
+```python
|
|
|
+def calc_withholding_tax(amount_fen: int) -> dict:
|
|
|
+ amount_yuan = amount_fen / 100
|
|
|
+
|
|
|
+ if amount_yuan <= 800:
|
|
|
+ tax_yuan = 0
|
|
|
+ taxable_yuan = 0
|
|
|
+ rate = 0
|
|
|
+
|
|
|
+ if amount_yuan <= 4000:
|
|
|
+ taxable_yuan = amount_yuan - 800
|
|
|
+ tax_yuan = taxable_yuan * 0.20
|
|
|
+ rate = 0.20
|
|
|
+
|
|
|
+ else:
|
|
|
+ taxable_yuan = amount_yuan * 0.80
|
|
|
+ if taxable_yuan <= 20000:
|
|
|
+ tax_yuan = taxable_yuan * 0.20 - 1050
|
|
|
+ rate = 0.20
|
|
|
+ elif taxable_yuan <= 50000:
|
|
|
+ tax_yuan = taxable_yuan * 0.30 - 4050
|
|
|
+ rate = 0.30
|
|
|
+ else:
|
|
|
+ tax_yuan = taxable_yuan * 0.40 - 7050
|
|
|
+ rate = 0.40
|
|
|
+
|
|
|
+ return {
|
|
|
+ "gross_amount_fen": amount_fen,
|
|
|
+ "taxable_amount_fen": int(taxable_yuan * 100),
|
|
|
+ "tax_rate": rate,
|
|
|
+ "withholding_tax_fen": max(0, int(tax_yuan * 100)),
|
|
|
+ "net_amount_fen": amount_fen - max(0, int(tax_yuan * 100)),
|
|
|
+ "service_fee_fen": int(amount_fen * 0.006), # 微信手续费
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+### 4.4 节税策略(系统内置 + 用户指导)
|
|
|
+
|
|
|
+**系统侧(自动):**
|
|
|
+1. **周限提策略** – 鼓励用户合并小额提现,减少单次税率浪费
|
|
|
+ - 逻辑:周免提额 = max(周已提, min_amount),剩余额度跨周不清零(30天 VIP)
|
|
|
+
|
|
|
+2. **年累计预扣优化**(年度报表)
|
|
|
+ - 年底自动计算用户累计应税收入
|
|
|
+ - 若已预扣税率「过高」(累计税款 > 汇算后应交),退还差额至余额
|
|
|
+ - 若「过低」(累计预扣 < 汇算后应交),在次年首次提现补扣
|
|
|
+
|
|
|
+3. **800元以下小额免收割策略**
|
|
|
+ - 若用户账户余额 ≤ ¥1500,建议用户先不提现,累计到¥800+:
|
|
|
+ ```
|
|
|
+ 建议小程序前端提示:
|
|
|
+ "当前提现 ¥550,扣除个税后仅到账 ¥440(低于免征额 ¥800)。
|
|
|
+ 建议再积累 ¥300 以上佣金,可享受个税免征。"
|
|
|
+ ```
|
|
|
+
|
|
|
+**用户侧(引导):**
|
|
|
+1. **发票代开**:年度提现超过 ¥30,000 的用户,引导至税务局代开发票,税率约 2.5%-3%(远低于劳务报酬最高 40%)
|
|
|
+2. **个体工商户注册**:高频提现用户(月均 > ¥5,000),建议注册个体户,经营所得税率 5%-10%
|
|
|
+3. **合规申报辅助**:前端提供「年度汇总下载」按钮(PDF 明细),用户可自行 H5 汇算
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 数据库设计
|
|
|
+
|
|
|
+### 5.1 withdraw_records 表
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE `withdraw_records` (
|
|
|
+ `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ `user_id` BIGINT NOT NULL,
|
|
|
+ `amount_fen` INT NOT NULL COMMENT '申请提现金额(分)',
|
|
|
+
|
|
|
+ -- 税务
|
|
|
+ `withholding_tax_fen` INT NOT NULL DEFAULT 0 COMMENT '预扣个税(分)',
|
|
|
+ `tax_rate` DECIMAL(4,2) NOT NULL DEFAULT 0 COMMENT '适用税率',
|
|
|
+ `taxable_amount_fen` INT NOT NULL DEFAULT 0 COMMENT '应纳税所得额(分)',
|
|
|
+
|
|
|
+ -- 微信
|
|
|
+ `service_fee_fen` INT NOT NULL DEFAULT 0 COMMENT '微信手续费(分)',
|
|
|
+ `wechat_transfer_id` VARCHAR(64) COMMENT '微信企业付款单号',
|
|
|
+ `wechat_resp` JSON COMMENT '微信返回原始响应',
|
|
|
+
|
|
|
+ -- 风控
|
|
|
+ `status` VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/SUCCESS/FAILED',
|
|
|
+ `status_remark` VARCHAR(256) COMMENT '状态说明(失败原因等)',
|
|
|
+ `retry_count` INT NOT NULL DEFAULT 0,
|
|
|
+ `risk_pass` TINYINT NOT NULL DEFAULT 0 COMMENT '风控是否通过',
|
|
|
+ `risk_reason` VARCHAR(256) COMMENT '风控不通过原因',
|
|
|
+
|
|
|
+ -- 审计
|
|
|
+ `year` INT NOT NULL COMMENT '所属年度(汇算用)',
|
|
|
+ `tax_year_amount_fen` INT NOT NULL DEFAULT 0 COMMENT '该年度已提现总额(分)',
|
|
|
+ `tax_year_withheld_fen` INT NOT NULL DEFAULT 0 COMMENT '该年度已扣个税额(分)',
|
|
|
+
|
|
|
+ `applied_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ `processed_at` DATETIME COMMENT '处理完成时间',
|
|
|
+ INDEX `idx_user_status` (`user_id`, `status`),
|
|
|
+ INDEX `idx_user_year` (`user_id`, `year`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 user_wallet 表(余额快照)
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE `user_wallet` (
|
|
|
+ `user_id` BIGINT PRIMARY KEY,
|
|
|
+ `available_balance_fen` INT NOT NULL DEFAULT 0 COMMENT '可提现余额',
|
|
|
+ `frozen_balance_fen` INT NOT NULL DEFAULT 0 COMMENT '冻结中余额(提现申请后)',
|
|
|
+ `total_withdrawn_fen` INT NOT NULL DEFAULT 0 COMMENT '累计已提现',
|
|
|
+ `total_withholding_fen` INT NOT NULL DEFAULT 0 COMMENT '累计已扣个税',
|
|
|
+ `annual_withdrawn_fen` INT NOT NULL DEFAULT 0 COMMENT '本年度已提',
|
|
|
+ `annual_withholding_fen` INT NOT NULL DEFAULT 0 COMMENT '本年度已扣税',
|
|
|
+ `last_withdraw_at` DATETIME COMMENT '上次提现时间',
|
|
|
+ `week_withdraw_count` INT NOT NULL DEFAULT 0 COMMENT '本周次数',
|
|
|
+ `week_reset_at` DATE COMMENT '周重置日',
|
|
|
+ `day_fail_count` INT NOT NULL DEFAULT 0 COMMENT '今日失败次数',
|
|
|
+ `day_fail_date` DATE COMMENT '失败计数日期',
|
|
|
+ `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
|
+```
|
|
|
+
|
|
|
+### 5.3 commission 表新增字段(可选)
|
|
|
+
|
|
|
+```sql
|
|
|
+ALTER TABLE `commissions` ADD COLUMN `wallet_locked` TINYINT NOT NULL DEFAULT 0 COMMENT '是否已入钱包锁';
|
|
|
+ALTER TABLE `commissions` ADD COLUMN `withdraw_record_id` BIGINT COMMENT '关联提现ID';
|
|
|
+```
|
|
|
+
|
|
|
+### 5.4 sys_config 税务参数
|
|
|
+
|
|
|
+```sql
|
|
|
+INSERT INTO sys_config (`key`, `value`, `desc`, `value_type`) VALUES
|
|
|
+('withdraw.min_amount', '10000', '最低提现金额(分)', 'amount'),
|
|
|
+('withdraw.max_amount_time', '1000000', '单次最高提现(分)', 'amount'),
|
|
|
+('withdraw.max_per_week', '1', '每周限提次数', 'number'),
|
|
|
+('withdraw.auto_approve_limit', '50000', '免审阈值(分)', 'amount'),
|
|
|
+('withdraw.fail_limit_day', '3', '日失败上限', 'number'),
|
|
|
+('withdraw.fail_lock_hours', '24', '超限冻结小时', 'number'),
|
|
|
+('withdraw.service_fee_rate', '6', '微信手续费万分比(6=0.06%)', 'percent'),
|
|
|
+('withdraw.week_reset_day', '1', '周重置日(0=周日)', 'number'),
|
|
|
+('withdraw.tax.labor_rate_1', '800', '劳务报酬免征额(元)', 'amount'),
|
|
|
+('withdraw.tax.deduction_2', '2000', '$4,000档速算扣除(分,对应¥20+1050)', 'amount'),
|
|
|
+('withdraw.tax.deduction_3', '40500', '$25,000档速算扣除(分,对应¥20,000*3%-4050)', 'amount'),
|
|
|
+('withdraw.tax.deduction_4', '70500', '>$62,500档速算扣除(分)', 'amount'),
|
|
|
+('withdraw.annual_reconcile', '1', '是否开启年度汇算', 'boolean');
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 后端 API 设计
|
|
|
+
|
|
|
+### 6.1 提现流程 API
|
|
|
+
|
|
|
+| 端点 | 方法 | 说明 | 权限 |
|
|
|
+|------|------|------|------|
|
|
|
+| `POST /api/withdraw/apply` | 申请提现 | 输入 amount_fen(可选,默认全部可提) | 已登录 |
|
|
|
+| `POST /api/withdraw/records` | 提现记录列表 | 分页,含状态+金额+到账时间 | 已登录 |
|
|
|
+| `GET /api/withdraw/detail/:id` | 提现详情 | 单条明细(含税务明细) | 本人 |
|
|
|
+| `GET /api/withdraw/summary` | 提现汇总 | 累计提现/累计个税/本年度汇总 | 已登录 |
|
|
|
+| `GET /api/withdraw/tax-preview` | 税务预览 | 输入金额 → 返回预计扣税+到手 | 已登录 |
|
|
|
+| `POST /api/withdraw/cancel/:id` | 撤销提现 | PENDING 状态可撤销,解冻金额 | 本人 |
|
|
|
+| `GET /api/withdraw/annual-report` | 年度明细 PDF | 下载含扣税明细的年度 PDF | 已登录 |
|
|
|
+
|
|
|
+### 6.2 提现申请 DTO
|
|
|
+
|
|
|
+```java
|
|
|
+// WithdrawApplyRequest.java
|
|
|
+@Data
|
|
|
+public class WithdrawApplyRequest {
|
|
|
+ @NotNull(message = "提现金额不能为空")
|
|
|
+ @Min(value = 10000, message = "最低提现 ¥100") // amount in fen
|
|
|
+ @Max(value = 1000000, message = "单次最高 ¥10,000")
|
|
|
+ private Integer amountFen;
|
|
|
+
|
|
|
+ // Optional: override full withdrawal
|
|
|
+ private Boolean all = false;
|
|
|
+}
|
|
|
+
|
|
|
+// WithdrawApplyResponse.java
|
|
|
+@Data
|
|
|
+public class WithdrawApplyResponse {
|
|
|
+ private Long recordId;
|
|
|
+ private Integer grossAmountFen;
|
|
|
+ private Integer withholdingTaxFen;
|
|
|
+ private Integer serviceFeeFen;
|
|
|
+ private Integer netAmountFen; // 实际到账
|
|
|
+ private BigDecimal taxRate;
|
|
|
+ private String status; // PENDING
|
|
|
+ private String estimatedArrival; // 1-3工作日
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.3 关键业务流程
|
|
|
+
|
|
|
+```java
|
|
|
+// WithdrawService.java - 核心流程
|
|
|
+
|
|
|
+@Transactional
|
|
|
+public WithdrawApplyResponse apply(Long userId, WithdrawApplyRequest req) {
|
|
|
+ // 1. 校验余额
|
|
|
+ Wallet wallet = walletRepo.findByUserId(userId)
|
|
|
+ .orElseThrow(() -> new BizException(404, "钱包不存在"));
|
|
|
+
|
|
|
+ // 2. 校验周限次
|
|
|
+ if (wallet.getWeekWithdrawCount() >= getMaxPerWeek()) {
|
|
|
+ throw new BizException(400, "本周提现次数已用完");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 风控校验
|
|
|
+ RiskResult risk = riskCheck(userId, req.getAmountFen());
|
|
|
+ if (!risk.passed()) {
|
|
|
+ return reject(risk);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 冻结金额
|
|
|
+ int amount = req.getAll() ? wallet.getAvailableBalance() : req.getAmountFen();
|
|
|
+ if (wallet.getAvailableBalance() < amount) {
|
|
|
+ throw new BizException(400, "可提现余额不足");
|
|
|
+ }
|
|
|
+ wallet.freeze(amount);
|
|
|
+ walletRepo.save(wallet);
|
|
|
+
|
|
|
+ // 5. 税务计算
|
|
|
+ TaxResult tax = TaxCalculator.calc(amount);
|
|
|
+
|
|
|
+ // 6. 创建提现记录
|
|
|
+ WithdrawRecord record = new WithdrawRecord();
|
|
|
+ record.setUserId(userId);
|
|
|
+ record.setAmountFen(amount);
|
|
|
+ record.setWithholdingTaxFen(tax.getWithholding());
|
|
|
+ record.setTaxRate(tax.getRate());
|
|
|
+ record.setStatus("PENDING");
|
|
|
+ record.setYear(Year.now().getValue());
|
|
|
+ walletRepo.save(wallet);
|
|
|
+
|
|
|
+ // 7. 已通过自动审核(免审):直接调用微信付款
|
|
|
+ if (shouldAutoApprove(amount)) {
|
|
|
+ processWechatPayment(userId, record, tax);
|
|
|
+ }
|
|
|
+
|
|
|
+ return toResponse(record, tax);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 微信企业付款到零钱集成
|
|
|
+
|
|
|
+### 7.1 API 端点
|
|
|
+
|
|
|
+```
|
|
|
+POST https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers
|
|
|
+```
|
|
|
+
|
|
|
+**必要参数:**
|
|
|
+- `mch_appid` — 小程序 AppID
|
|
|
+- `mchid` — 商户号
|
|
|
+- `partner_trade_no` — 商户订单号(唯一)
|
|
|
+- `openid` — 用户 openid
|
|
|
+- `check_name` — `NO_CHECK`(不校验真实姓名,按公众号绑定)
|
|
|
+- `amount` — 金额(分,**实际打款金额 = amount - 服务费**)
|
|
|
+- `desc` — 企业付款说明
|
|
|
+- `spbill_create_ip` — 服务器 IP
|
|
|
+
|
|
|
+**签名校验:** 使用商户 API 证书(apiclient_key.pem)双向证书签名
|
|
|
+
|
|
|
+### 7.2 成本结构
|
|
|
+
|
|
|
+| 费用项 | 承担方 | 费率 |
|
|
|
+|--------|--------|------|
|
|
|
+| 微信企业付款手续费 | **平台** | 0.6%(最低 ¥0.1) |
|
|
|
+| 个税(劳务报酬) | **从用户提现金额中扣除** | 累进 0%-40% |
|
|
|
+| 代开发票(可选) | 用户 | 约 2.5%-3% |
|
|
|
+
|
|
|
+### 7.3 安全设计
|
|
|
+
|
|
|
+```java
|
|
|
+// WithdrawSecurity.kt (概念)
|
|
|
+@Scheduled(fixedRate = 60000)
|
|
|
+fun watchPending() {
|
|
|
+ // 检查超过 5 分钟未处理的 PENDING 记录
|
|
|
+ // 超时 → 解冻 + 记录告警
|
|
|
+}
|
|
|
+
|
|
|
+@Scheduled(fixedRate = 86400000)
|
|
|
+func dailyReconcile() {
|
|
|
+ // 与微信账单对账,确保平台记录与微信到账一致
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. 提现流程前端
|
|
|
+
|
|
|
+### 8.1 提现页(client/pages/withdraw/index.vue)
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────┐
|
|
|
+│ ← 提现 │
|
|
|
+│ │
|
|
|
+│ 可提现余额 │ 大字 + 闪烁动画
|
|
|
+│ ¥ 1,380.00 │
|
|
|
+│ ───────────────────────── │
|
|
|
+│ 输入提现金额 │
|
|
|
+│ ┌─────────────────────┐ │
|
|
|
+│ │ ¥ 500.00 │ │ 数字键盘
|
|
|
+│ └─────────────────────┘ │
|
|
|
+│ 快捷金额: ¥100 ¥500 ¥全部 │
|
|
|
+│ │
|
|
|
+│ ┌─────────────────────┐ │ 浅灰蓝卡片 F7F8FC
|
|
|
+│ │ 税务预览 │ │
|
|
|
+│ │ 提现金额 ¥ 500.00 │ │
|
|
|
+│ │ 预扣个税 ¥ 30.00 │ │ 20% 税率
|
|
|
+│ │ 微信手续费 ¥ 3.00 │ │ 0.6% 平台承担
|
|
|
+│ │ 实际到账 ¥ 467.00 │ │ 大号强调
|
|
|
+│ └─────────────────────┘ │
|
|
|
+│ │
|
|
|
+│ ⚠️ 提示:本次提现低于 ¥800 │
|
|
|
+│ 免征额,建议累计至 ¥800+ │
|
|
|
+│ │
|
|
|
+│ [ 确认提现 ] │ 深色品牌按钮
|
|
|
+│ │
|
|
|
+│ 本周已提:0/1次 | 预计1-3日到账 │ 小字灰色
|
|
|
+└─────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 8.2 提现记录页
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────┐
|
|
|
+│ 提现记录 │
|
|
|
+├─────────────────────────────┤
|
|
|
+│ ┌─────────────────────┐ │
|
|
|
+│ │ 11月20日 15:30 │ │
|
|
|
+│ │ 申请 ¥500.00 │ │
|
|
|
+│ │ 个税 ¥30 + 到账 ¥467 │ │
|
|
|
+│ │ 状态:✅ 已到账 │ │
|
|
|
+│ └─────────────────────┘ │
|
|
|
+│ ┌─────────────────────┐ │
|
|
|
+│ │ 11月15日 09:00 │ │
|
|
|
+│ │ 申请 ¥200.00 │ │
|
|
|
+│ │ 个税 ¥0(免征) │ │
|
|
|
+│ │ 状态:⏳ 处理中 │ │
|
|
|
+│ └─────────────────────┘ │
|
|
|
+│ │
|
|
|
+│ [下载年度税务明细 PDF] │
|
|
|
+└─────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. 关键字段参照表
|
|
|
+
|
|
|
+| 表名 | 关键字段 | 说明 |
|
|
|
+|------|---------|------|
|
|
|
+| `user_wallet` | available_balance_fen | 可提现余额 |
|
|
|
+| `user_wallet` | frozen_balance_fen | 申请中冻结余额 |
|
|
|
+| `withdraw_records` | status | INIT / PENDING / SUCCESS / FAILED |
|
|
|
+| `withdraw_records` | withholding_tax_fen | 预扣个税金额 |
|
|
|
+| `withdraw_records` | tax_rate | 适用税率(0.20 等)|
|
|
|
+| `withdraw_records` | wechat_transfer_id | 微信付款单号 |
|
|
|
+| `sys_config` | withdraw.* | 所有可配置参数 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 10. 税务合规备忘
|
|
|
+
|
|
|
+1. **企业必须代扣代缴** — 平台作为支付方,有法定义务预扣个税
|
|
|
+2. **年度结算报告** — 次年 6 月前,平台需向税局报送上年度支付给个人的劳务报酬总额
|
|
|
+3. **发票管理** — 个税由平台代扣时,用户可凭「扣税记录」凭证申请退税
|
|
|
+4. **联合稽查风险** — 若提现流水大(>¥100万/年),需考虑>
|
|
|
+ 在小程序有关税提示:
|
|
|
+ "年度汇总可凭此明细至税务局"
|
|
|
+ (不提供代开发票,只提供明细)
|
|
|
+5. **税率更新** — `sys_config` 支持快速更新税率,年审时直接在后台调整
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 11. 节税策略实施优先级
|
|
|
+
|
|
|
+| 优先级 | 策略 | 用户价值 | 系统改动 |
|
|
|
+|--------|------|---------|---------|
|
|
|
+| P0 | 预扣代缴 + 税务预览弹窗 | 透明化 | ✅ 已覆盖 |
|
|
|
+| P0 | ¥800免征额智能提示 | 直接省钱 | ✅ 前端提示 |
|
|
|
+| P1 | 年度汇算退补机制 | 合规+体验 | 需年审触发器 |
|
|
|
+| P2 | 小额合并提现建议 | 减少税率浪费 | 前端算法 |
|
|
|
+| P3 | 代开发票引导(>¥30,000/年) | 合法降税 | 静态链接 |
|
|
|
+| P3 | 个体户注册指引 | 长期节税 | 外部跳转 |
|