配额体系:按不同角色控制"可咨询人数上限" + "每人每日聊天次数上限"
状态:v1.0 Draft,方案 B 推荐
| 角色 | 可咨询人数 | 每人聊天次数 |
|---|---|---|
| 普通用户 | 3 人 | 3 次/天 |
| C端会员 | 9 人 | 不限 |
| 家庭套餐 | 9 人 | 不限 |
| 超级会员 | 9 人 | 不限(暂缓) |
| 能量师 | 不限 | 不限 |
"次数"以 API 调用
/api/chat/send为准,能量盘创建本身不限次数。
数在每天 00:00 按用户本地时区(或北京时间)自动重置。
ALTER TABLE users ADD COLUMN consultation_quota JSON COMMENT '咨询配额数据';
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。
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;
核心查询:
-- 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()
数据生命周期:
用户调用 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)保证。
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');
@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
);
}
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 | 解耦,各自承担不同职责 |