# 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 | 解耦,各自承担不同职责 |