|
|
@@ -0,0 +1,220 @@
|
|
|
+# user_person_quota 表结构设计
|
|
|
+
|
|
|
+> 配额体系:按不同角色控制"可咨询人数上限" + "每人每日聊天次数上限"
|
|
|
+> 状态:v1.0 Draft,方案 B 推荐
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 一、业务规则回顾
|
|
|
+
|
|
|
+| 角色 | 可咨询人数 | 每人聊天次数 |
|
|
|
+|------|-----------|-------------|
|
|
|
+| 普通用户 | 3 人 | 3 次/天 |
|
|
|
+| C端会员 | 9 人 | 不限 |
|
|
|
+| 家庭套餐 | 9 人 | 不限 |
|
|
|
+| 超级会员 | 9 人 | 不限(暂缓) |
|
|
|
+| 能量师 | 不限 | 不限 |
|
|
|
+
|
|
|
+> "次数"以 API 调用 `/api/chat/send` 为准,能量盘创建本身不限次数。
|
|
|
+> 数在每天 00:00 按用户本地时区(或北京时间)自动重置。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 二、两种方案对比
|
|
|
+
|
|
|
+### 方案 A:JSON 字段(存 users 表)
|
|
|
+
|
|
|
+```sql
|
|
|
+ALTER TABLE users ADD COLUMN consultation_quota JSON COMMENT '咨询配额数据';
|
|
|
+```
|
|
|
+
|
|
|
+**JSON 结构示例:**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "persons": ["张三", "李四", "王五"],
|
|
|
+ "dailyCounts": {
|
|
|
+ "2026-06-05": {"张三": 3, "李四": 1},
|
|
|
+ "2026-06-04": {"张三": 2}
|
|
|
+ },
|
|
|
+ "chatLimitPerDay": 3,
|
|
|
+ "lastResetDate": "2026-06-05"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+| 维度 | 方案 A — JSON | 方案 B — 独立表 |
|
|
|
+|-----|--------------|----------------|
|
|
|
+| 查询「已用人数」 | `JSON_LENGTH(persons)` — 快 | `SELECT COUNT(DISTINCT person_name)` — 快 |
|
|
|
+| 查询「今日次数」 | 需解析 JSON — 慢 | `WHERE last_chat_date = CURDATE()` — 快 |
|
|
|
+| 并发更新 | 整字段读写,锁冲突风险 | 单行更新,无冲突 |
|
|
|
+| 每日重置 | 需 JSON 重构 | `UPDATE ... SET chat_count_today = 0 WHERE last_chat_date < TODAY` |
|
|
|
+| 数据一致性 | 弱(应用层保证) | 强(DB 约束) |
|
|
|
+| 扩展性 | 差(新增字段需改 JSON schema) | 好(加字段即可) |
|
|
|
+| 历史分析 | 难(每日计数仅保留最近 N 天) | 易(可保留全量记录) |
|
|
|
+| MySQL 版本要求 | 5.7.8+(JSON 函数) | 无要求 |
|
|
|
+
|
|
|
+**结论:** 方案 A 适合快速原型,数据量小、一致性要求低的场景。
|
|
|
+本项目有付费交易,配额数据直接影响用户体验,推荐方案 B。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 方案 B:独立表 + 每日重置(推荐)
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE `user_consultation_quota` (
|
|
|
+ `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ `user_id` BIGINT NOT NULL COMMENT '用户ID',
|
|
|
+ `person_name` VARCHAR(50) NOT NULL COMMENT '被咨询者姓名',
|
|
|
+ `chat_count_today` INT NOT NULL DEFAULT 0 COMMENT '今日已用聊天次数',
|
|
|
+ `last_chat_date` DATE COMMENT '最后聊天日期(用于判断是否跨天重置)',
|
|
|
+ `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
+ UNIQUE KEY `uk_user_person` (`user_id`, `person_name`),
|
|
|
+ INDEX `idx_user_date` (`user_id`, `last_chat_date`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
|
+```
|
|
|
+
|
|
|
+**核心查询:**
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 1. 查询已咨询人数(检查人数上限)
|
|
|
+SELECT COUNT(DISTINCT person_name)
|
|
|
+FROM user_consultation_quota
|
|
|
+WHERE user_id = ?
|
|
|
+
|
|
|
+-- 2. 查询某人在今日已聊天次数(检查次数上限)
|
|
|
+SELECT chat_count_today, last_chat_date
|
|
|
+FROM user_consultation_quota
|
|
|
+WHERE user_id = ? AND person_name = ?
|
|
|
+
|
|
|
+-- 3. 新增被咨询人(首次咨询某人)
|
|
|
+INSERT IGNORE INTO user_consultation_quota (user_id, person_name, last_chat_date)
|
|
|
+VALUES (?, ?, CURDATE())
|
|
|
+
|
|
|
+-- 4. 聊天次数 +1(原子操作)
|
|
|
+UPDATE user_consultation_quota
|
|
|
+SET chat_count_today = chat_count_today + 1,
|
|
|
+ last_chat_date = CURDATE()
|
|
|
+WHERE user_id = ? AND person_name = ?
|
|
|
+ AND (last_chat_date != CURDATE() OR chat_count_today = 0)
|
|
|
+-- 注意:跨天时自动重置为 0 再 +1
|
|
|
+
|
|
|
+-- 5. 每日重置(定时任务 00:00)
|
|
|
+UPDATE user_consultation_quota
|
|
|
+SET chat_count_today = 0,
|
|
|
+ last_chat_date = CURDATE()
|
|
|
+WHERE last_chat_date < CURDATE()
|
|
|
+```
|
|
|
+
|
|
|
+**数据生命周期:**
|
|
|
+- 正常情况:每日 chat_count_today 归零,数据保留(历史可用于分析)
|
|
|
+- 建议:每月清理 30 天未访问过的 persons 记录,或保留全量用于数据挖掘
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 三、配额检查流程(后端 API 层)
|
|
|
+
|
|
|
+```
|
|
|
+用户调用 POST /api/chat/send
|
|
|
+│
|
|
|
+├─ 1. 查用户 vipType → 获取角色配额规则
|
|
|
+│ (从 sys_config 或内存缓存读取)
|
|
|
+│
|
|
|
+├─ 2. 检查人数配额
|
|
|
+│ ├─ SELECT COUNT(DISTINCT person_name) FROM user_consultation_quota WHERE user_id = ?
|
|
|
+│ ├─ IF 已用人数 >= 角色上限 → 返回 403「已达咨询人数上限」
|
|
|
+│ └─ IF 角色上限 = 0(不限)→ 跳过
|
|
|
+│
|
|
|
+├─ 3. 检查次数配额
|
|
|
+│ ├─ SELECT chat_count_today FROM user_consultation_quota
|
|
|
+│ │ WHERE user_id = ? AND person_name = ?
|
|
|
+│ ├─ IF last_chat_date != TODAY → 视为 0 次(跨天自动重置)
|
|
|
+│ ├─ IF 次数 >= 角色上限 → 返回 403「今日咨询次数已用完」
|
|
|
+│ └─ IF 角色上限 = 0(不限)→ 跳过
|
|
|
+│
|
|
|
+├─ 4. 记录消耗
|
|
|
+│ ├─ INSERT IGNORE INTO user_consultation_quota (user_id, person_name, last_chat_date)
|
|
|
+│ └─ UPDATE user_consultation_quota SET chat_count_today = chat_count_today + 1
|
|
|
+│ WHERE user_id = ? AND person_name = ?
|
|
|
+│
|
|
|
+└─ 5. 继续执行业务逻辑(调用 Dify / 返回缓存结果)
|
|
|
+```
|
|
|
+
|
|
|
+**并发安全:** 用 `UPDATE ... WHERE` 带条件的原子更新,避免超发。
|
|
|
+**幂等性:** 同一聊天消息不应重复扣配额,由消息幂等键(message_id)保证。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 四、角色配额规则(sys_config)
|
|
|
+
|
|
|
+```sql
|
|
|
+INSERT INTO sys_config (`key`, `value`, `desc`, `value_type`) VALUES
|
|
|
+-- 可咨询人数上限(0=不限制)
|
|
|
+('quota.person_limit.normal', '3', '普通用户可咨询人数上限', 'number'),
|
|
|
+('quota.person_limit.annual', '9', 'C端会员可咨询人数上限', 'number'),
|
|
|
+('quota.person_limit.family', '9', '家庭套餐可咨询人数上限', 'number'),
|
|
|
+('quota.person_limit.super', '9', '超级会员可咨询人数上限(暂缓)', 'number'),
|
|
|
+('quota.person_limit.practitioner', '0', '能量师可咨询人数上限(0=不限)', 'number'),
|
|
|
+-- 每人每日聊天次数上限(0=不限制)
|
|
|
+('quota.chat_limit.normal', '3', '普通用户每人每日聊天次数上限', 'number'),
|
|
|
+('quota.chat_limit.annual', '0', 'C端会员每人每日聊天次数上限(0=不限)', 'number'),
|
|
|
+('quota.chat_limit.family', '0', '家庭套餐每人每日聊天次数上限', 'number'),
|
|
|
+('quota.chat_limit.super', '0', '超级会员每人每日聊天次数上限(暂缓)', 'number'),
|
|
|
+('quota.chat_limit.practitioner', '0', '能量师每人每日聊天次数上限', 'number'),
|
|
|
+-- 每日重置时间(小时,北京时间)
|
|
|
+('quota.reset_hour', '0', '配额每日重置小时(0=00:00)', 'number');
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 五、每日重置定时任务
|
|
|
+
|
|
|
+```java
|
|
|
+@Scheduled(cron = "0 0 ${quota.reset_hour} * * *") // 每天 N 点执行
|
|
|
+public void resetDailyQuota() {
|
|
|
+ int resetHour = configService.getInt("quota.reset_hour", 0);
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
+
|
|
|
+ // 方式1:UPDATE 批量重置(高效,适合数据量 < 100 万)
|
|
|
+ jdbcTemplate.update(
|
|
|
+ "UPDATE user_consultation_quota SET chat_count_today = 0, last_chat_date = ? " +
|
|
|
+ "WHERE last_chat_date < ?",
|
|
|
+ today, today
|
|
|
+ );
|
|
|
+
|
|
|
+ // 方式2(备选):清理超过 90 天未使用的人员记录
|
|
|
+ jdbcTemplate.update(
|
|
|
+ "DELETE FROM user_consultation_quota WHERE last_chat_date < DATE_SUB(?, INTERVAL 90 DAY)",
|
|
|
+ today
|
|
|
+ );
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 六、与 chart_record 的关系
|
|
|
+
|
|
|
+`user_consultation_quota` 追踪"聊天次数限制",`chart_record` 存储"能量盘记录"。
|
|
|
+
|
|
|
+| 场景 | chart_record | user_consultation_quota |
|
|
|
+|------|-------------|------------------------|
|
|
|
+| 用户输入生日 → 生成能量盘 | ✅ 插入 | 不扣(能量盘创建不占配额) |
|
|
|
+| 用户在能量盘页发消息聊天 | ❌ 不插入 | ✅ 计数 +1 |
|
|
|
+| 用户首次咨询某个人 | ✅ 插入 | ✅ person_name 记录 |
|
|
|
+| 同一人对同一人重复咨询 | ✅ 返回历史 | ✅ chat_count_today +1 |
|
|
|
+
|
|
|
+> 人数上限以 `user_consultation_quota.person_name` 去重计数,**不依赖** chart_record,
|
|
|
+> 因为用户可能在 chart_record 中注册了多人,但从未聊天。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 七、方案选择建议
|
|
|
+
|
|
|
+| 维度 | 推荐 |
|
|
|
+|------|------|
|
|
|
+| 数据结构 | **方案 B(独立表)** |
|
|
|
+| 重置策略 | 每日定时 UPDATE 归零 |
|
|
|
+| 并发控制 | UPDATE WHERE 原子操作 |
|
|
|
+| 数据保留 | 保留全量,每月清理 90 天未活跃记录 |
|
|
|
+| sys_config | 全部参数化,支持后台动态调整 |
|
|
|
+| 与 chart_record | 解耦,各自承担不同职责 |
|