主题
库表设计
共 14 张业务表。前五张(users、organization_tags、file_upload、chunk_info、document_vectors)为核心五表,支撑文件上传、权限模型与 RAG 数据流,编号与设计文档一致;其余九张为支撑表(配额计费、模型配置、会话对话)。
关系库之外的存储对应关系:Elasticsearch 的
knowledge_base索引对应 document_vectors 的检索视图(向量 + 权限三元组);Redis 的user:token:*对应 user_token_record 的实时余额(库表只存流水);Redis 的upload:{userId}:{fileMd5}位图对应 chunk_info 的进度缓存;MinIO 的chunks/、merged/对象路径记录在 chunk_info.storage_path 与 file_upload.file_md5 中。
第一部分:核心五表
一、用户表(users)
users 表存储注册用户。username 唯一索引支撑登录时的账号查询;password 存 BCrypt 哈希;role 用枚举限定两级角色(USER/ADMIN),是 URL 级权限判定的依据;org_tags 以逗号分隔存储层级展开后的组织标签集合,登录签发 JWT 时原样写入 token,供资源级授权与组织匹配使用;primary_org 是用户的主组织,上传文件未显式指定归属时缺省落到这里。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | BIGINT AUTO_INCREMENT | 主键 |
| username | VARCHAR(255) NOT NULL,UNIQUE | 用户名,唯一索引支撑登录查询 |
| password | VARCHAR(255) NOT NULL | BCrypt 哈希后的密码 |
| role | ENUM('ADMIN','USER') NOT NULL | 角色,URL 级权限判定依据 |
| org_tags | VARCHAR(255) | 逗号分隔的组织标签集合(层级展开后),签发 JWT 时写入 |
| primary_org | VARCHAR(255) | 主组织标签,上传未指定归属时的缺省值 |
| created_at | DATETIME(6) | 创建时间 |
| updated_at | DATETIME(6) | 更新时间 |
二、组织标签表(organization_tags)
organization_tags 表定义多租户的组织结构。tag_id 是字符串主键,值即标签本身,文件表的 org_tag 字段直接存它,避免联表;parent_tag 以自引用方式表达组织层级(无外键约束,层级展开在应用层完成并缓存进 Redis);upload_max_size_bytes 实现组织级上传配额,分片上传时逐片预检;created_by 外键关联创建该标签的管理员。
| 字段名 | 类型 | 描述 |
|---|---|---|
| tag_id | VARCHAR(255) NOT NULL | 主键,标签值本身(文件表 org_tag 直接引用该值) |
| name | VARCHAR(255) NOT NULL | 组织名称 |
| parent_tag | VARCHAR(255) | 父标签,自引用表达层级;应用层展开为有效标签集合并缓存 |
| upload_max_size_bytes | BIGINT | 组织级单文件上传上限(字节),分片上传逐片预检 |
| description | TEXT | 组织描述 |
| created_by | BIGINT NOT NULL,FK→users.id | 创建该标签的管理员 |
| created_at | DATETIME(6) | 创建时间 |
| updated_at | DATETIME(6) | 更新时间 |
三、文件主表(file_upload)
file_upload 表记录每个文件的完整生命周期。file_md5 与 user_id 组成唯一约束 uk_file_upload_md5_user,确保同一用户不会重复登记同一文件,也是合并接口幂等返回的基础;status 是文件上传状态机(上传中/合并中/已完成),合并并发控制依赖对该行的带状态条件 UPDATE(乐观锁);vectorization_status 与 vectorization_error_message 构成异步处理的可观测层(处理中/完成/失败 + 根因入库);estimated 与 actual 两组字段分别记录合并时的预估消耗和向量化后的实际消耗,支撑配额对账;user_id、org_tag、is_public 三元组是贯穿全系统的权限模型,向量化时随 ES 文档落库,支撑检索与列表的权限过滤。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | BIGINT AUTO_INCREMENT | 主键 |
| file_md5 | VARCHAR(32) NOT NULL,UK(file_md5, user_id) | 文件指纹(前端 SparkMD5 分块计算),身份标识 |
| file_name | VARCHAR(255) | 文件名 |
| total_size | BIGINT NOT NULL | 文件大小(字节),服务端据此重算期望分片数 |
| status | INT NOT NULL | 上传状态机:0=上传中,1=合并中,2=已完成;合并乐观锁的条件字段 |
| user_id | VARCHAR(64) NOT NULL,UK(file_md5, user_id) | 属主,权限三元组之一 |
| org_tag | VARCHAR(255) | 组织归属,权限三元组之二 |
| is_public | BIT(1) NOT NULL | 公开标记,权限三元组之三 |
| merged_at | DATETIME(6) | 合并完成时间 |
| vectorization_status | VARCHAR(32) | 异步处理状态:PENDING/PROCESSING/COMPLETED/FAILED |
| vectorization_error_message | VARCHAR(1000) | 失败根因入库,全链路可观测的核心字段 |
| estimated_embedding_tokens | BIGINT | 合并时预估的 embedding 消耗 |
| estimated_chunk_count | INT | 合并时预估的切片数 |
| actual_embedding_tokens | BIGINT | 向量化完成后的实际消耗,配额结算依据 |
| actual_chunk_count | INT | 实际切片数 |
| created_at | DATETIME(6) | 登记时间 |
四、分片表(chunk_info)
chunk_info 表存储每个分片的元数据,是分片去重的数据库兜底层。uk_file_md5_chunk_index 唯一约束保证同一文件的同一分片最多一行——并发重复上传时插入冲突,服务捕获异常按幂等成功处理,这也是合并前数量校验正确性的前提;chunk_md5 存服务端重算的分片哈希(不信任前端声明);storage_path 记录 MinIO 中的确定性分片路径,合并时按 chunk_index 升序取出构建 composeObject 源列表,与 Redis 位图(进度缓存)和 MinIO 对象(内容)构成三方校验的事实层。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | BIGINT AUTO_INCREMENT | 主键 |
| file_md5 | VARCHAR(32) NOT NULL,UK(file_md5, chunk_index) | 所属文件指纹 |
| chunk_index | INT NOT NULL,UK(file_md5, chunk_index) | 分片序号(从 0 起),合并排序与去重依据 |
| chunk_md5 | VARCHAR(32) NOT NULL | 服务端重算的分片哈希,内容校验用 |
| storage_path | VARCHAR(255) NOT NULL | MinIO 分片路径(chunks/{md5}/{index}),合并时按序取出 |
五、解析结果表(document_vectors)
document_vectors 表存储文档解析的产物——文本块,一行为一个切片。它是解析与向量化两段流水线之间的衔接点:解析切块落这张表,向量化再从它读取,向量化失败重试时无需重新解析文件。chunk_id 是文件内递增序号,与 file_md5 组合成 Elasticsearch 文档的确定性 ID({file_md5}_{chunk_id}),保证重复写入是覆盖而非追加;page_number 与 anchor_text 支撑聊天引用的"第 X 页 + 锚点"定位;user_id、org_tag、is_public 权限三元组在此落库并随块写入 ES;model_version 记录向量所属的模型版本,为未来模型升级的重嵌入提供追溯依据。表上没有 (file_md5, chunk_id) 唯一约束,重跑清理由应用层在解析前显式完成。
| 字段名 | 类型 | 描述 |
|---|---|---|
| vector_id | BIGINT AUTO_INCREMENT | 主键 |
| file_md5 | VARCHAR(32) NOT NULL | 所属文件指纹 |
| chunk_id | INT NOT NULL | 文件内块序号,ES 确定性文档 ID 的组成部分 |
| text_content | LONGTEXT | 文本块内容,向量化输入与重处理数据源 |
| page_number | INT | PDF 页码,引用定位"第 X 页" |
| anchor_text | VARCHAR(512) | 页内锚点,精确引用定位 |
| user_id | VARCHAR(64) NOT NULL | 权限三元组之一 |
| org_tag | VARCHAR(50) | 权限三元组之二 |
| is_public | BIT(1) NOT NULL | 权限三元组之三 |
| model_version | VARCHAR(32) | 生成向量时的 embedding 模型版本 |
第二部分:支撑表
六、用户 Token 流水表(user_token_record)
user_token_record 表是配额计费系统的流水账。系统对每个用户维护 LLM 与 EMBEDDING 两本独立的额度账:实时余额存 Redis(user:token:embedding:{userId} 等键),这张表只记每一笔增减——token_type 区分账本,change_type 区分充值与消费,balance_before/after 记录每笔流水前后的余额快照用于对账;idx_user_date 组合索引支撑"按用户按日查询消耗"的运营报表路径。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | BIGINT AUTO_INCREMENT | 主键 |
| user_id | VARCHAR(255) NOT NULL,KEY idx_user_date(user_id, record_date) | 用户标识 |
| token_type | ENUM('EMBEDDING','LLM') NOT NULL | 额度账本类型,两本账独立记账 |
| change_type | ENUM('CONSUME','INCREASE') NOT NULL | 变动方向 |
| amount | BIGINT NOT NULL | 变动数量 |
| balance_before | BIGINT | 流水前余额快照 |
| balance_after | BIGINT | 流水后余额快照,用于对账 |
| request_count | BIGINT NOT NULL | 关联的请求次数 |
| reason | VARCHAR(500) | 变动原因(注册赠送/充值/调用结算等) |
| remark | VARCHAR(500) | 备注 |
| record_date | DATE NOT NULL,KEY idx_user_date(user_id, record_date) | 流水日期,按日统计维度 |
| created_at | DATETIME(6) | 记录时间 |
七、充值套餐表(recharge_packages)
recharge_packages 表定义可售卖的额度套餐,是配额商城的 SKU 表。每个套餐同时携带 LLM 与 EMBEDDING 两种额度;package_price 以 BIGINT 存储(分为单位)避免浮点精度问题;deleted 逻辑删除标记支持下架而不破坏历史订单引用;sort_order 控制商城展示顺序。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | INT AUTO_INCREMENT | 主键 |
| package_name | VARCHAR(128) NOT NULL | 套餐名 |
| package_price | BIGINT NOT NULL | 价格(分为单位,BIGINT 避免浮点误差) |
| llm_token | BIGINT NOT NULL | 套餐包含的 LLM 额度 |
| embedding_token | BIGINT NOT NULL | 套餐包含的 Embedding 额度 |
| package_desc | TEXT | 套餐描述 |
| package_benefit | TEXT | 权益说明 |
| sort_order | INT NOT NULL | 商城展示排序 |
| enabled | BIT(1) NOT NULL | 上架开关 |
| deleted | BIT(1) NOT NULL | 逻辑删除,保护历史订单引用 |
| created_at / updated_at | DATETIME(6) | 创建/更新时间 |
八、充值订单表(recharge_orders)
recharge_orders 表记录每笔额度充值订单。trade_no 唯一约束是业务订单号的防重关键——支付回调可能重放,靠它保证一笔订单只被结算一次;wx_transaction_id 保存微信支付侧交易号用于对账;status 枚举覆盖支付全生命周期;user_id 与 package_id 关联买家与购买的套餐。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | BIGINT AUTO_INCREMENT | 主键 |
| trade_no | VARCHAR(255) NOT NULL,UNIQUE | 业务订单号,唯一约束防支付回调重放 |
| user_id | VARCHAR(64) NOT NULL | 买家 |
| package_id | INT NOT NULL | 购买的套餐 |
| amount | BIGINT NOT NULL | 支付金额(分) |
| llm_token | BIGINT NOT NULL | 到账 LLM 额度 |
| embedding_token | BIGINT NOT NULL | 到账 Embedding 额度 |
| status | ENUM('NOT_PAY','PAYING','SUCCEED','FAIL','CANCELLED') NOT NULL | 支付状态机 |
| wx_transaction_id | VARCHAR(255) | 微信支付交易号,对账用 |
| pay_time | DATETIME(6) | 支付完成时间 |
| description | VARCHAR(255) | 订单描述 |
| created_at / updated_at | DATETIME(6) | 创建/更新时间 |
九、限流配置表(rate_limit_configs)
rate_limit_configs 表存储限流策略的可调参数,与 token 余额配额是两套并行的闸门:余额管"总量",这张表管"速率"。config_key 主键对应一个限流维度(如 LLM 调用、embedding 调用),每个维度可独立配置分钟级、天级、单次请求三级窗口上限;参数由管理端维护、服务热加载,调整限流不需要重启。
| 字段名 | 类型 | 描述 |
|---|---|---|
| config_key | VARCHAR(64) NOT NULL | 主键,限流维度标识 |
| minute_max | BIGINT | 分钟窗口内最大请求数 |
| minute_window_seconds | BIGINT | 分钟窗口长度(秒) |
| day_max | BIGINT | 天窗口内最大请求数 |
| day_window_seconds | BIGINT | 天窗口长度(秒) |
| single_max | INT | 单次请求上限 |
| single_window_seconds | BIGINT | 单次窗口长度(秒) |
| updated_by | VARCHAR(255) NOT NULL | 最近修改人 |
| created_at / updated_at | DATETIME(6) | 创建/更新时间 |
十、模型供应商配置表(model_provider_configs)
model_provider_configs 表实现大模型供应商的运行时可配置与热切换。uk(config_scope, provider_code) 唯一约束保证每个作用域(LLM / embedding)下每个供应商一行配置;active 标记当前生效的供应商,切换只改这一行并刷新内存缓存,不重启服务;api_key_ciphertext 将供应商密钥加密后落库,避免明文;embedding_dimension 记录向量维度,维度变更需要重嵌入,当前版本禁止直接切换。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | BIGINT AUTO_INCREMENT | 主键 |
| config_scope | VARCHAR(32) NOT NULL,UK(config_scope, provider_code) | 配置作用域:LLM / embedding |
| provider_code | VARCHAR(64) NOT NULL,UK(config_scope, provider_code) | 供应商标识(deepseek/aliyun 等) |
| display_name | VARCHAR(128) NOT NULL | 展示名 |
| api_style | VARCHAR(64) NOT NULL | API 风格(OpenAI 兼容等) |
| api_base_url | VARCHAR(512) NOT NULL | 接口地址 |
| api_key_ciphertext | VARCHAR(2048) | 加密存储的密钥,运行时解密使用 |
| model_name | VARCHAR(255) NOT NULL | 模型名 |
| embedding_dimension | INT | embedding 向量维度(仅 embedding 作用域) |
| enabled | BIT(1) NOT NULL | 启用开关 |
| active | BIT(1) NOT NULL | 当前生效供应商标记,热切换只改此字段 |
| updated_by | VARCHAR(255) NOT NULL | 最近修改人 |
| created_at / updated_at | DATETIME(6) | 创建/更新时间 |
十一、会话表(conversation_sessions)
conversation_sessions 表定义用户的会话(一次多轮对话的容器)。conversation_id 唯一索引是对外会话标识(UUID),客户端凭它拉取历史;status 支持会话归档;user_id 外键关联用户,idx_cs_user_id 支撑"我的会话列表"查询。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | BIGINT AUTO_INCREMENT | 主键 |
| conversation_id | VARCHAR(64) NOT NULL,UNIQUE | 对外会话标识(UUID) |
| user_id | BIGINT NOT NULL,FK→users.id,KEY idx_cs_user_id | 会话属主 |
| title | VARCHAR(255) | 会话标题 |
| status | ENUM('ACTIVE','ARCHIVED') NOT NULL | 会话状态,支持归档 |
| created_at / updated_at | DATETIME(6) | 创建/更新时间 |
十二、对话记录表(conversations)
conversations 表持久化每一轮问答。question/answer 成对存储;reference_mappings_json 以 JSON 保存本轮回答的引用映射(编号 → 文件名/页码/块 ID),历史会话渲染时引用预览不丢失——这是聊天历史模块不回归引用功能的关键字段;idx_user_id 与 idx_conversation_id 分别支撑"按用户拉全部历史"和"按会话拉消息"两种读路径。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | BIGINT AUTO_INCREMENT | 主键 |
| user_id | BIGINT NOT NULL,FK→users.id,KEY idx_user_id | 所属用户 |
| conversation_id | VARCHAR(64),KEY idx_conversation_id | 所属会话 |
| question | TEXT NOT NULL | 用户提问 |
| answer | TEXT NOT NULL | 模型回答 |
| reference_mappings_json | LONGTEXT | 引用映射 JSON(编号→文件/页码/块ID),历史渲染引用预览的数据源 |
| timestamp | DATETIME(6),KEY idx_timestamp | 问答时间 |
十三、用户每日聊天计数表(user_daily_chat_count)
user_daily_chat_count 表是每日聊天次数限流的持久层。uk_user_date 唯一约束保证一用户一天一行,计数递增;每日聊天上限的判定读这张表(或其缓存),跨天自然重置——与 Redis 余额限流(总量维度)、rate_limit_configs(速率维度)构成三套互补的管控。
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | BIGINT AUTO_INCREMENT | 主键 |
| user_id | VARCHAR(50) NOT NULL,UK(user_id, record_date) | 用户标识 |
| record_date | DATE NOT NULL,UK(user_id, record_date) | 统计日期,跨天自然重置 |
| chat_request_count | BIGINT NOT NULL | 当日聊天请求计数 |
| created_at / updated_at | DATETIME(6) NOT NULL | 创建/更新时间 |
持久层:Spring Data JPA(技术栈口径)
三层关系:JPA 是 Java 的 ORM 规范,Hibernate 是默认实现,Spring Data JPA 是 Spring 在其上的再封装——项目用 spring-boot-starter-data-jpa,三层全在用。项目里四类典型用法:实体映射(@Entity + @Table 把 User/FileUpload 等类钉在表上,含唯一约束声明);方法名推导查询(existsByFileMd5AndChunkIndex 三方校验、findByFileMd5OrderByChunkIndexAsc 合并取分片、findByConversationIdOrderByTimestampAsc 历史排序——方法名即 SQL);JPQL + 注解增强(合并乐观锁 @Modifying + @Query 的带状态条件 UPDATE、邀请码悲观锁 @Lock(PESSIMISTIC_WRITE));事务集成(注册/删除文档的 @Transactional)。
和 MyBatis 的区别?为什么选 JPA?
哲学不同:MyBatis 是 SQL 驱动——SQL 自己写,框架做参数与结果映射,可控性强;JPA 是对象驱动——定义好实体,简单 SQL 按方法名生成,开发效率高、持久层零模板代码。本项目 CRUD 为主、复杂查询有限(最复杂的就是权限过滤的 IN 条件和两个带锁查询),JPA 完全覆盖;代价是超复杂 SQL 和深度调优时可控性弱于 MyBatis,真遇到会用 nativeQuery 或引入 MyBatis 做补充。两个带锁查询说明 JPA 也能精确控制 SQL 语义——选型看场景,不是信仰。