|
|
@@ -0,0 +1,457 @@
|
|
|
+# 实施执行计划
|
|
|
+
|
|
|
+> 基于文档一致性修正(已完成)后的编码实现计划
|
|
|
+> 状态:v1.0 Draft,待确认后执行
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 前置条件
|
|
|
+
|
|
|
+- ✅ 文档一致性修正(创始人码/三重条件清理)已完成
|
|
|
+- ✅ `@EnableScheduling` 已在 `NumApplication.java` 启用
|
|
|
+- ✅ `ChatService.sendMessage()` 已调用 `quotaService.checkAndConsume()`
|
|
|
+- ✅ `QuotaService`(含 `resetDailyQuota`)已编码完成
|
|
|
+- ❌ 新增 `sys_config` 种子数据尚未注册到 `ConfigService.java`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 总体计划(4 个 Sprint)
|
|
|
+
|
|
|
+| Sprint | 内容 | 预估工作量 | 风险 |
|
|
|
+|--------|------|-----------|------|
|
|
|
+| 1 | 定时任务基础设施 + 配额重置 | 1 文件 + 5 行 seed 数据 | 低 |
|
|
|
+| 2 | 提现系统重构(通道/税务/批处理) | 5 文件(Entity+Service+SQL+Config) | 中 |
|
|
|
+| 3 | 分红结算定时任务 | 2 文件(SQL+Service) | 高(递归算法+资金池精度) |
|
|
|
+| 4 | 参数入库 + 整体对接 | 1 文件(ConfigService种子更新) | 低 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Sprint 1:定时任务基础设施 + 配额重置
|
|
|
+
|
|
|
+### 1.1 新增 `ScheduledTasks.java`
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.num.service;
|
|
|
+
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.scheduling.annotation.Scheduled;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 系统定时任务容器。
|
|
|
+ * 集中管理所有 @Scheduled 任务,避免分散在多个 Service 中难以维护。
|
|
|
+ */
|
|
|
+@Component
|
|
|
+public class ScheduledTasks {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
|
|
|
+
|
|
|
+ private final QuotaService quotaService;
|
|
|
+
|
|
|
+ public ScheduledTasks(QuotaService quotaService) { this.quotaService = quotaService; }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 每日 00:00 重置所有用户咨询配额(chat_count_today = 0)。
|
|
|
+ */
|
|
|
+ @Scheduled(cron = "0 0 0 * * ?")
|
|
|
+ public void resetDailyQuota() {
|
|
|
+ int affected = quotaService.resetDailyQuota();
|
|
|
+ log.info("[Scheduled] 每日配额重置完成,影响 {} 条记录", affected);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**文件路径:** `num-server/src/main/java/com/etotem/num/service/ScheduledTasks.java`
|
|
|
+
|
|
|
+### 1.2 ConfigService 新增种子数据
|
|
|
+
|
|
|
+在 `ConfigService.java` 的 `initSeedData()` 和 `getAllSeeds()` 中追加:
|
|
|
+
|
|
|
+```java
|
|
|
+// Quota limits (per-role)
|
|
|
+seeds.put("quota.person_limit.normal", new SeedEntry("3", "int", "普通用户可咨询人数上限"));
|
|
|
+seeds.put("quota.person_limit.annual", new SeedEntry("9", "int", "C端会员可咨询人数上限"));
|
|
|
+seeds.put("quota.person_limit.family", new SeedEntry("9", "int", "家庭套餐可咨询人数上限"));
|
|
|
+seeds.put("quota.person_limit.practitioner", new SeedEntry("0", "int", "能量师可咨询人数上限(0=不限制)"));
|
|
|
+seeds.put("quota.chat_limit.normal", new SeedEntry("3", "int", "普通用户每日每人聊天次数上限"));
|
|
|
+seeds.put("quota.chat_limit.annual", new SeedEntry("0", "int", "C端会员每日每人聊天次数上限(0=不限制)"));
|
|
|
+seeds.put("quota.chat_limit.family", new SeedEntry("0", "int", "家庭套餐每日每人聊天次数上限(0=不限制)"));
|
|
|
+seeds.put("quota.chat_limit.practitioner", new SeedEntry("0", "int", "能量师每日每人聊天次数上限(0=不限制)"));
|
|
|
+```
|
|
|
+
|
|
|
+### 1.3 验证点
|
|
|
+
|
|
|
+- [ ] `resetDailyQuota()` cron 定时执行且日志正确
|
|
|
+- [ ] 跨天后 `chat_count_today` 归零
|
|
|
+- [ ] `quota.person_limit.normal=3` 限制正确生效
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Sprint 2:提现系统重构
|
|
|
+
|
|
|
+### 2.1 提现 Entity 重构 — `Withdraw.java`
|
|
|
+
|
|
|
+**现状:** 只有 `id, userId, amount, status, createdAt, updatedAt`
|
|
|
+**目标:** 按 `docs/withdrawal-design.md` 补充以下字段
|
|
|
+
|
|
|
+```java
|
|
|
+// === 新增字段(加在现有字段后)===
|
|
|
+
|
|
|
+// 提现通道:REFUND(退费) / WECHAT_TRANSFER(企业付款) / THIRD_PARTY(第三方)
|
|
|
+@Column(length = 30)
|
|
|
+private String channel;
|
|
|
+
|
|
|
+// 状态机:pending → queued → processing → success / failed / frozen
|
|
|
+// pending(待审批) → approved(已审批) → queued(已入列) → processing(处理中)
|
|
|
+// → success(成功) / failed(失败) / frozen(冻结)
|
|
|
+@Column(length = 30)
|
|
|
+private String channelStatus; // 通道侧状态码
|
|
|
+
|
|
|
+// 税务字段
|
|
|
+@Column(name = "tax_fen")
|
|
|
+private Integer taxFen = 0; // 预扣税额(分)
|
|
|
+@Column(name = "net_amount_fen")
|
|
|
+private Integer netAmountFen; // 实际到账金额(分)
|
|
|
+@Column(name = "tax_rate")
|
|
|
+private Integer taxRate = 0; // 适用税率(万分比)
|
|
|
+
|
|
|
+// 批处理字段
|
|
|
+@Column(name = "batch_id", length = 50)
|
|
|
+private String batchId; // 批次号(年月日+序号,如 20260608-001)
|
|
|
+@Column(name = "processed_at")
|
|
|
+private LocalDateTime processedAt; // 实际处理时间
|
|
|
+
|
|
|
+// 微信/通道侧字段
|
|
|
+@Column(name = "channel_order_no", length = 64)
|
|
|
+private String channelOrderNo; // 微信付款单号
|
|
|
+@Column(name = "channel_error", length = 200)
|
|
|
+private String channelError; // 通道错误信息
|
|
|
+
|
|
|
+// 审批字段
|
|
|
+@Column(name = "approved_by")
|
|
|
+private Long approvedBy; // 审批人用户ID
|
|
|
+@Column(name = "approved_at")
|
|
|
+private LocalDateTime approvedAt; // 审批时间
|
|
|
+@Column(name = "remark", length = 200)
|
|
|
+private String remark;
|
|
|
+```
|
|
|
+
|
|
|
+### 2.2 提现 Repository — `WithdrawRepository.java`
|
|
|
+
|
|
|
+**现状:** 标准 JPA(findByUserIdOrderByCreatedAtDesc, findAll)
|
|
|
+**新增方法:**
|
|
|
+
|
|
|
+```java
|
|
|
+// 查询待批处理提现记录
|
|
|
+List<Withdraw> findByStatusIn(List<String> statuses);
|
|
|
+
|
|
|
+// 按批次查询
|
|
|
+List<Withdraw> findByBatchId(String batchId);
|
|
|
+
|
|
|
+// 统计指定用户已提现总额
|
|
|
+@Query("SELECT COALESCE(SUM(w.amount), 0) FROM Withdraw w WHERE w.userId = :userId AND w.status = 'success'")
|
|
|
+long sumSuccessAmountByUserId(@Param("userId") Long userId);
|
|
|
+
|
|
|
+// 查询用户首次提现是否在30天内
|
|
|
+@Query("SELECT w.createdAt FROM Withdraw w WHERE w.userId = :userId ORDER BY w.createdAt ASC")
|
|
|
+List<LocalDateTime> findFirstWithdrawTime(@Param("userId") Long userId, Pageable pageable);
|
|
|
+```
|
|
|
+
|
|
|
+### 2.3 提现 Service 重构 — `WithdrawService.java`
|
|
|
+
|
|
|
+**核心方法清单:**
|
|
|
+
|
|
|
+```java
|
|
|
+// ==================== 用户侧 ====================
|
|
|
+
|
|
|
+// 1. 申请提现(可选channel参数)
|
|
|
+public Withdraw apply(Long userId, int amount, String channel)
|
|
|
+// 逻辑:
|
|
|
+// - 金额 ≥ ¥100
|
|
|
+// - 查询可用余额(commission表)
|
|
|
+// - 自动判断通道:
|
|
|
+// a) 首提且订单 <30天 → REFUND(微信退费通道)
|
|
|
+// b) 金额 > ¥800 → THIRD_PARTY
|
|
|
+// c) 否则 → WECHAT_TRANSFER
|
|
|
+// - 计算预扣税额(≤¥800免征,>¥800按劳务报酬预扣)
|
|
|
+// - 创建 status='pending' 记录
|
|
|
+
|
|
|
+// 2. 查询可用余额
|
|
|
+public long getAvailableBalance(Long userId)
|
|
|
+
|
|
|
+// 3. 查询提现记录(带分页)
|
|
|
+public Page<Withdraw> getMyWithdraws(Long userId, int page, int size)
|
|
|
+
|
|
|
+// ==================== 管理后台侧 ====================
|
|
|
+
|
|
|
+// 4. 审批通过(入列等待夜间批量处理)
|
|
|
+public void approve(Long id, Long adminId)
|
|
|
+
|
|
|
+// 5. 驳回
|
|
|
+public void reject(Long id, String reason)
|
|
|
+
|
|
|
+// 6. 冻结/解冻
|
|
|
+public void freeze(Long id)
|
|
|
+public void unfreeze(Long id)
|
|
|
+
|
|
|
+// ==================== 定时任务(夜间批处理) ====================
|
|
|
+
|
|
|
+// 7. 夜间 23:30 批量处理
|
|
|
+@Scheduled(cron = "0 30 23 * * ?")
|
|
|
+public void batchProcessQueuedWithdraws()
|
|
|
+// 逻辑:
|
|
|
+// a) 查询所有 status='queued' 的记录
|
|
|
+// b) 生成批次号
|
|
|
+// c) 标记 BATCH_PROCESSING
|
|
|
+// d) 检查微信商户平台余额(merchant_balance)
|
|
|
+// e) 若余额不足 → 记录错误,次日重试
|
|
|
+// f) 逐条调用对应通道 API
|
|
|
+// g) 更新最终状态
|
|
|
+```
|
|
|
+
|
|
|
+### 2.4 提现 Controller 增强 — `WithdrawController.java`
|
|
|
+
|
|
|
+**现状:** `apply`, `list`, `balance`
|
|
|
+**新增端点:**
|
|
|
+
|
|
|
+```java
|
|
|
+// 管理后台:提现列表(全部)
|
|
|
+@PostMapping("/admin/list") → Page<Withdraw>
|
|
|
+
|
|
|
+// 管理后台:审批通过
|
|
|
+@PostMapping("/admin/approve")
|
|
|
+
|
|
|
+// 管理后台:驳回
|
|
|
+@PostMapping("/admin/reject")
|
|
|
+
|
|
|
+// 管理后台:冻结
|
|
|
+@PostMapping("/admin/freeze")
|
|
|
+
|
|
|
+// 用户侧:提现详情
|
|
|
+@PostMapping("/detail")
|
|
|
+```
|
|
|
+
|
|
|
+### 2.5 sys_config 种子数据
|
|
|
+
|
|
|
+```java
|
|
|
+// 提现配置
|
|
|
+seeds.put("withdraw.min_amount", new SeedEntry("10000", "amount", "最低提现金额(分)"));
|
|
|
+seeds.put("withdraw.max_amount", new SeedEntry("5000000","amount", "单次最高提现金额(分)"));
|
|
|
+seeds.put("withdraw.fee_free_threshold", new SeedEntry("80000", "amount", "免税额(分),≤¥800免征劳务报酬税"));
|
|
|
+seeds.put("withdraw.tax_rate", new SeedEntry("2000", "percent", "劳务报酬预扣税率(万分比)"));
|
|
|
+seeds.put("withdraw.refund_days", new SeedEntry("30", "int", "首笔可走退费通道的天数"));
|
|
|
+seeds.put("withdraw.merchant_min_balance",new SeedEntry("5000000","amount","微信商户最低保留余额(分)"));
|
|
|
+seeds.put("withdraw.batch_time", new SeedEntry("2330", "string", "批处理时间(HHmm)"));
|
|
|
+seeds.put("withdraw.frozen_on_error", new SeedEntry("true", "bool", "通道失败时是否自动冻结"));
|
|
|
+```
|
|
|
+
|
|
|
+### 2.6 提现状态机
|
|
|
+
|
|
|
+```
|
|
|
+apply() → pending ──→ approve() ──→ queued ──→ batchProcess() ──→ processing ──→ success
|
|
|
+ ↕ ↕
|
|
|
+ reject() failed ──→ frozen(自动)
|
|
|
+ ↕ ↕
|
|
|
+ frozen ←──→ unfreeze() manual_review
|
|
|
+```
|
|
|
+
|
|
|
+### 2.7 验证点
|
|
|
+
|
|
|
+- [ ] 用户提现申请时通道自动选择正确(首笔30天退费/≤¥800微信/>¥800第三方)
|
|
|
+- [ ] 税务计算正确(¥800免征,超出部分按20%预扣)
|
|
|
+- [ ] 夜间批处理 23:30 准时触发
|
|
|
+- [ ] 商户余额不足时正确跳过并记录日志
|
|
|
+- [ ] 多次失败后自动冻结
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Sprint 3:分红结算定时任务
|
|
|
+
|
|
|
+### 3.1 monthly_bonus_records DDL
|
|
|
+
|
|
|
+将以下 SQL 放入 `num-server/src/main/resources/schema.sql` 或 Flyway migration:
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE IF NOT EXISTS `monthly_bonus_records` (
|
|
|
+ `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ `energy_master_id` BIGINT NOT NULL COMMENT '能量师用户ID',
|
|
|
+ `year_month` VARCHAR(7) NOT NULL COMMENT '结算月份(如 2026-06)',
|
|
|
+ `platform_revenue_fen` BIGINT NOT NULL DEFAULT 0,
|
|
|
+ `personal_pool_fen` BIGINT NOT NULL DEFAULT 0,
|
|
|
+ `team_pool_fen` BIGINT NOT NULL DEFAULT 0,
|
|
|
+ `personal_sales_fen` BIGINT NOT NULL DEFAULT 0,
|
|
|
+ `total_personal_sales_fen` BIGINT NOT NULL DEFAULT 0,
|
|
|
+ `personal_share_pct` INT NOT NULL DEFAULT 0,
|
|
|
+ `team_sales_fen` BIGINT NOT NULL DEFAULT 0,
|
|
|
+ `total_team_sales_fen` BIGINT NOT NULL DEFAULT 0,
|
|
|
+ `team_share_pct` INT NOT NULL DEFAULT 0,
|
|
|
+ `personal_bonus_fen` BIGINT NOT NULL DEFAULT 0,
|
|
|
+ `team_bonus_fen` BIGINT NOT NULL DEFAULT 0,
|
|
|
+ `total_bonus_fen` BIGINT NOT NULL DEFAULT 0,
|
|
|
+ `qualified` TINYINT(1) NOT NULL DEFAULT 0,
|
|
|
+ `status` VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
|
+ `settled_at` DATETIME,
|
|
|
+ `paid_at` DATETIME,
|
|
|
+ `remark` VARCHAR(200),
|
|
|
+ UNIQUE KEY `uk_master_month` (`energy_master_id`, `year_month`),
|
|
|
+ INDEX `idx_year_month` (`year_month`),
|
|
|
+ INDEX `idx_status` (`status`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='能量师月度分红结算表';
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 新增 `BonusSettlementService.java`
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.num.service;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 能量师月度分红结算服务(资金池竞争制)。
|
|
|
+ *
|
|
|
+ * 每月 1 日 00:00 执行:
|
|
|
+ * 1. 计算平台月总收入(实付金额-退款)
|
|
|
+ * 2. 划出个人分红池(5%)和团队分红池(5%)
|
|
|
+ * 3. 筛选当月有直接销售的活跃能量师
|
|
|
+ * 4. 计算各能量师的个人直接销售额和团队销售额(递归,遇无销售截断)
|
|
|
+ * 5. 按占比分配两个资金池
|
|
|
+ * 6. 写入 monthly_bonus_records
|
|
|
+ */
|
|
|
+@Service
|
|
|
+public class BonusSettlementService {
|
|
|
+
|
|
|
+ // Dependencies: OrderRepository, UserRepository, ConfigService, (for write: BonusRecordRepository)
|
|
|
+
|
|
|
+ @Scheduled(cron = "0 0 1 * * ?") // 每月1日 00:00
|
|
|
+ @Transactional
|
|
|
+ public void settleMonthlyBonus() {
|
|
|
+ // 1. 计算平台当月总收入
|
|
|
+ // SELECT COALESCE(SUM(amount), 0) FROM orders
|
|
|
+ // WHERE status = 'paid' AND paid_at BETWEEN startOfMonth AND endOfMonth
|
|
|
+
|
|
|
+ // 2. 查询所有能量师
|
|
|
+ // SELECT * FROM users WHERE vipType = 'practitioner'
|
|
|
+
|
|
|
+ // 3. 统计每个能量师当月直接销售额
|
|
|
+ // Map<userId, sales>
|
|
|
+
|
|
|
+ // 4. 筛选活跃能量师(当月有直接销售额 > 0)
|
|
|
+
|
|
|
+ // 5. 计算活跃能量师的团队销售额(递归)
|
|
|
+ // calcTeamSalesRecursive(userId, yearMonth)
|
|
|
+ // - 自身直接销售 + ∑(下级有销售者递归)
|
|
|
+
|
|
|
+ // 6. 计算总分母(总个人销售额、总团队销售额)
|
|
|
+
|
|
|
+ // 7. 对每个活跃能量师计算占比和分红
|
|
|
+ // 个人分红 = (个人销售额 / 总个人销售额) × 个人分红池
|
|
|
+ // 团队分红 = (团队销售额 / 总团队销售额) × 团队分红池
|
|
|
+
|
|
|
+ // 8. 写入 monthly_bonus_records
|
|
|
+ }
|
|
|
+
|
|
|
+ // 递归方法
|
|
|
+ private long calcTeamSalesRecursive(Long userId, YearMonth yearMonth) {
|
|
|
+ // 自身直接销售 + 下级的团队销售(仅当月有销售的下级才递归)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 辅助方法
|
|
|
+ private long getMonthlySales(Long userId, YearMonth yearMonth) {
|
|
|
+ // 从 orders 表查询该用户当月直接下级订单
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**注意:** `monthly_bonus_records` 缺少对应的 JPA Entity + Repository。需创建:
|
|
|
+
|
|
|
+- `MonthlyBonusRecord.java`(JPA Entity)
|
|
|
+- `MonthlyBonusRecordRepository.java`(JPA Repository,含 `findByYearMonth`)
|
|
|
+
|
|
|
+### 3.3 sys_config 种子数据
|
|
|
+
|
|
|
+```java
|
|
|
+// 分红配置
|
|
|
+seeds.put("bonus.personal.pool_rate", new SeedEntry("500", "percent", "个人分红池占总收入万分比(500=5%)"));
|
|
|
+seeds.put("bonus.team.pool_rate", new SeedEntry("500", "percent", "团队分红池占总收入万分比(500=5%)"));
|
|
|
+seeds.put("bonus.settlement_day", new SeedEntry("1", "int", "月度结算日"));
|
|
|
+seeds.put("bonus.min_payout", new SeedEntry("10000","amount", "最低发放金额(分,¥100),不足滚入下月"));
|
|
|
+```
|
|
|
+
|
|
|
+### 3.4 验证点
|
|
|
+
|
|
|
+- [ ] 每月 1 日 00:00 准时触发结算
|
|
|
+- [ ] 个人/团队各 5% 资金池正确划出
|
|
|
+- [ ] 递归团队计算在无销售成员处截断
|
|
|
+- [ ] 资金池总额 = 两个 5% 池之和
|
|
|
+- [ ] 每个能量师的占比之和 = 100%(验证算术精度)
|
|
|
+- [ ] 不合格能量师(当月无销售)不参与分配
|
|
|
+- [ ] 分红 < ¥100 的记录标记 `rollover` 不下发
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Sprint 4:参数入库 + 整体对接
|
|
|
+
|
|
|
+### 4.1 ConfigService 种子数据全部追加
|
|
|
+
|
|
|
+一次性在 `initSeedData()` 和 `getAllSeeds()` 追加所有新配置:
|
|
|
+
|
|
|
+| 分组 | Key | 默认值 | 说明 |
|
|
|
+|------|-----|--------|------|
|
|
|
+| 配额 | `quota.person_limit.*` | normal=3, annual=9, family=9, practitioner=0 | 角色咨询人数上限 |
|
|
|
+| 配额 | `quota.chat_limit.*` | normal=3, annual=0, family=0, practitioner=0 | 角色每日聊天次数上限 |
|
|
|
+| 提现 | `withdraw.min_amount` | 10000 | 最低提现金额 ¥100 |
|
|
|
+| 提现 | `withdraw.max_amount` | 5000000 | 最高提现金额 ¥50,000 |
|
|
|
+| 提现 | `withdraw.fee_free_threshold` | 80000 | 免税额 ¥800 |
|
|
|
+| 提现 | `withdraw.tax_rate` | 2000 | 劳务报酬预扣税率 20% |
|
|
|
+| 提现 | `withdraw.refund_days` | 30 | 首笔退费通道天数 |
|
|
|
+| 提现 | `withdraw.merchant_min_balance` | 5000000 | 商户最低保留余额 |
|
|
|
+| 提现 | `withdraw.batch_time` | 2330 | 批处理时间 |
|
|
|
+| 提现 | `withdraw.frozen_on_error` | true | 失败自动冻结 |
|
|
|
+| 分红 | `bonus.personal.pool_rate` | 500 | 个人分红池 5% |
|
|
|
+| 分红 | `bonus.team.pool_rate` | 500 | 团队分红池 5% |
|
|
|
+| 分红 | `bonus.settlement_day` | 1 | 结算日 |
|
|
|
+| 分红 | `bonus.min_payout` | 10000 | 最低发放 ¥100 |
|
|
|
+
|
|
|
+### 4.2 交叉验证清单
|
|
|
+
|
|
|
+- [ ] `ChatService` 配额拦截:免费用户咨询第 4 人时被阻止(`quota.person_limit.normal=3`)
|
|
|
+- [ ] `ChatService` 配额拦截:免费用户对同一人第 4 次聊天被阻止(`quota.chat_limit.normal=3`)
|
|
|
+- [ ] C端会员可咨询 9 人、无限聊天
|
|
|
+- [ ] 能量师不限人数、不限聊天
|
|
|
+- [ ] 次日 00:00 配额自动重置
|
|
|
+- [ ] `WithdrawService.batchProcessQueuedWithdraws` 只在 23:30 执行
|
|
|
+- [ ] `BonusSettlementService.settleMonthlyBonus` 只在每月 1 日执行
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 实施顺序(推荐)
|
|
|
+
|
|
|
+```
|
|
|
+Sprint 1: ScheduledTasks.java + ConfigService quota seeds
|
|
|
+ ↓
|
|
|
+Sprint 2: Withdraw Entity重构 → Repository新增方法 → Service重写 → Controller增强
|
|
|
+ ↓ (Sprint 2&3 无依赖,可并行)
|
|
|
+Sprint 3: monthly_bonus_records DDL + MonthlyBonusRecord Entity + Repository + BonusSettlementService
|
|
|
+ ↓
|
|
|
+Sprint 4: ConfigService 全部种子数据入库 + 整体验证
|
|
|
+```
|
|
|
+
|
|
|
+**并行策略:** Sprint 2(提现)和 Sprint 3(分红)无依赖,可委托两个子代理并行执行。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件变更汇总
|
|
|
+
|
|
|
+| Sprint | 文件 | 操作 |
|
|
|
+|--------|------|------|
|
|
|
+| 1 | `service/ScheduledTasks.java` | **新建** |
|
|
|
+| 1 | `service/ConfigService.java` | 编辑(追加 quota seeds) |
|
|
|
+| 2 | `entity/Withdraw.java` | **重写**(扩字段) |
|
|
|
+| 2 | `repository/WithdrawRepository.java` | 编辑(新增查询方法) |
|
|
|
+| 2 | `service/WithdrawService.java` | **重写**(批处理+税务+通道选择) |
|
|
|
+| 2 | `controller/WithdrawController.java` | 编辑(新增管理端点) |
|
|
|
+| 2 | `service/WeChatService.java` | **可能需编辑**(企业付款API) |
|
|
|
+| 3 | `resources/schema.sql` 或 Flyway migration | **新建**(monthly_bonus_records DDL) |
|
|
|
+| 3 | `entity/MonthlyBonusRecord.java` | **新建** |
|
|
|
+| 3 | `repository/MonthlyBonusRecordRepository.java` | **新建** |
|
|
|
+| 3 | `service/BonusSettlementService.java` | **新建** |
|
|
|
+| 4 | `service/ConfigService.java` | 编辑(追加提现+分红 seeds) |
|