Skip to content
掌握度

库表设计 ​

共 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 是用户的主组织,上传文件未显式指定归属时缺省落到这里。

字段名类型描述
idBIGINT AUTO_INCREMENT主键
usernameVARCHAR(255) NOT NULL,UNIQUE用户名,唯一索引支撑登录查询
passwordVARCHAR(255) NOT NULLBCrypt 哈希后的密码
roleENUM('ADMIN','USER') NOT NULL角色,URL 级权限判定依据
org_tagsVARCHAR(255)逗号分隔的组织标签集合(层级展开后),签发 JWT 时写入
primary_orgVARCHAR(255)主组织标签,上传未指定归属时的缺省值
created_atDATETIME(6)创建时间
updated_atDATETIME(6)更新时间

二、组织标签表(organization_tags) ​

organization_tags 表定义多租户的组织结构。tag_id 是字符串主键,值即标签本身,文件表的 org_tag 字段直接存它,避免联表;parent_tag 以自引用方式表达组织层级(无外键约束,层级展开在应用层完成并缓存进 Redis);upload_max_size_bytes 实现组织级上传配额,分片上传时逐片预检;created_by 外键关联创建该标签的管理员。

字段名类型描述
tag_idVARCHAR(255) NOT NULL主键,标签值本身(文件表 org_tag 直接引用该值)
nameVARCHAR(255) NOT NULL组织名称
parent_tagVARCHAR(255)父标签,自引用表达层级;应用层展开为有效标签集合并缓存
upload_max_size_bytesBIGINT组织级单文件上传上限(字节),分片上传逐片预检
descriptionTEXT组织描述
created_byBIGINT NOT NULL,FK→users.id创建该标签的管理员
created_atDATETIME(6)创建时间
updated_atDATETIME(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 文档落库,支撑检索与列表的权限过滤。

字段名类型描述
idBIGINT AUTO_INCREMENT主键
file_md5VARCHAR(32) NOT NULL,UK(file_md5, user_id)文件指纹(前端 SparkMD5 分块计算),身份标识
file_nameVARCHAR(255)文件名
total_sizeBIGINT NOT NULL文件大小(字节),服务端据此重算期望分片数
statusINT NOT NULL上传状态机:0=上传中,1=合并中,2=已完成;合并乐观锁的条件字段
user_idVARCHAR(64) NOT NULL,UK(file_md5, user_id)属主,权限三元组之一
org_tagVARCHAR(255)组织归属,权限三元组之二
is_publicBIT(1) NOT NULL公开标记,权限三元组之三
merged_atDATETIME(6)合并完成时间
vectorization_statusVARCHAR(32)异步处理状态:PENDING/PROCESSING/COMPLETED/FAILED
vectorization_error_messageVARCHAR(1000)失败根因入库,全链路可观测的核心字段
estimated_embedding_tokensBIGINT合并时预估的 embedding 消耗
estimated_chunk_countINT合并时预估的切片数
actual_embedding_tokensBIGINT向量化完成后的实际消耗,配额结算依据
actual_chunk_countINT实际切片数
created_atDATETIME(6)登记时间

四、分片表(chunk_info) ​

chunk_info 表存储每个分片的元数据,是分片去重的数据库兜底层。uk_file_md5_chunk_index 唯一约束保证同一文件的同一分片最多一行——并发重复上传时插入冲突,服务捕获异常按幂等成功处理,这也是合并前数量校验正确性的前提;chunk_md5 存服务端重算的分片哈希(不信任前端声明);storage_path 记录 MinIO 中的确定性分片路径,合并时按 chunk_index 升序取出构建 composeObject 源列表,与 Redis 位图(进度缓存)和 MinIO 对象(内容)构成三方校验的事实层。

字段名类型描述
idBIGINT AUTO_INCREMENT主键
file_md5VARCHAR(32) NOT NULL,UK(file_md5, chunk_index)所属文件指纹
chunk_indexINT NOT NULL,UK(file_md5, chunk_index)分片序号(从 0 起),合并排序与去重依据
chunk_md5VARCHAR(32) NOT NULL服务端重算的分片哈希,内容校验用
storage_pathVARCHAR(255) NOT NULLMinIO 分片路径(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_idBIGINT AUTO_INCREMENT主键
file_md5VARCHAR(32) NOT NULL所属文件指纹
chunk_idINT NOT NULL文件内块序号,ES 确定性文档 ID 的组成部分
text_contentLONGTEXT文本块内容,向量化输入与重处理数据源
page_numberINTPDF 页码,引用定位"第 X 页"
anchor_textVARCHAR(512)页内锚点,精确引用定位
user_idVARCHAR(64) NOT NULL权限三元组之一
org_tagVARCHAR(50)权限三元组之二
is_publicBIT(1) NOT NULL权限三元组之三
model_versionVARCHAR(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 组合索引支撑"按用户按日查询消耗"的运营报表路径。

字段名类型描述
idBIGINT AUTO_INCREMENT主键
user_idVARCHAR(255) NOT NULL,KEY idx_user_date(user_id, record_date)用户标识
token_typeENUM('EMBEDDING','LLM') NOT NULL额度账本类型,两本账独立记账
change_typeENUM('CONSUME','INCREASE') NOT NULL变动方向
amountBIGINT NOT NULL变动数量
balance_beforeBIGINT流水前余额快照
balance_afterBIGINT流水后余额快照,用于对账
request_countBIGINT NOT NULL关联的请求次数
reasonVARCHAR(500)变动原因(注册赠送/充值/调用结算等)
remarkVARCHAR(500)备注
record_dateDATE NOT NULL,KEY idx_user_date(user_id, record_date)流水日期,按日统计维度
created_atDATETIME(6)记录时间

七、充值套餐表(recharge_packages) ​

recharge_packages 表定义可售卖的额度套餐,是配额商城的 SKU 表。每个套餐同时携带 LLM 与 EMBEDDING 两种额度;package_price 以 BIGINT 存储(分为单位)避免浮点精度问题;deleted 逻辑删除标记支持下架而不破坏历史订单引用;sort_order 控制商城展示顺序。

字段名类型描述
idINT AUTO_INCREMENT主键
package_nameVARCHAR(128) NOT NULL套餐名
package_priceBIGINT NOT NULL价格(分为单位,BIGINT 避免浮点误差)
llm_tokenBIGINT NOT NULL套餐包含的 LLM 额度
embedding_tokenBIGINT NOT NULL套餐包含的 Embedding 额度
package_descTEXT套餐描述
package_benefitTEXT权益说明
sort_orderINT NOT NULL商城展示排序
enabledBIT(1) NOT NULL上架开关
deletedBIT(1) NOT NULL逻辑删除,保护历史订单引用
created_at / updated_atDATETIME(6)创建/更新时间

八、充值订单表(recharge_orders) ​

recharge_orders 表记录每笔额度充值订单。trade_no 唯一约束是业务订单号的防重关键——支付回调可能重放,靠它保证一笔订单只被结算一次;wx_transaction_id 保存微信支付侧交易号用于对账;status 枚举覆盖支付全生命周期;user_id 与 package_id 关联买家与购买的套餐。

字段名类型描述
idBIGINT AUTO_INCREMENT主键
trade_noVARCHAR(255) NOT NULL,UNIQUE业务订单号,唯一约束防支付回调重放
user_idVARCHAR(64) NOT NULL买家
package_idINT NOT NULL购买的套餐
amountBIGINT NOT NULL支付金额(分)
llm_tokenBIGINT NOT NULL到账 LLM 额度
embedding_tokenBIGINT NOT NULL到账 Embedding 额度
statusENUM('NOT_PAY','PAYING','SUCCEED','FAIL','CANCELLED') NOT NULL支付状态机
wx_transaction_idVARCHAR(255)微信支付交易号,对账用
pay_timeDATETIME(6)支付完成时间
descriptionVARCHAR(255)订单描述
created_at / updated_atDATETIME(6)创建/更新时间

九、限流配置表(rate_limit_configs) ​

rate_limit_configs 表存储限流策略的可调参数,与 token 余额配额是两套并行的闸门:余额管"总量",这张表管"速率"。config_key 主键对应一个限流维度(如 LLM 调用、embedding 调用),每个维度可独立配置分钟级、天级、单次请求三级窗口上限;参数由管理端维护、服务热加载,调整限流不需要重启。

字段名类型描述
config_keyVARCHAR(64) NOT NULL主键,限流维度标识
minute_maxBIGINT分钟窗口内最大请求数
minute_window_secondsBIGINT分钟窗口长度(秒)
day_maxBIGINT天窗口内最大请求数
day_window_secondsBIGINT天窗口长度(秒)
single_maxINT单次请求上限
single_window_secondsBIGINT单次窗口长度(秒)
updated_byVARCHAR(255) NOT NULL最近修改人
created_at / updated_atDATETIME(6)创建/更新时间

十、模型供应商配置表(model_provider_configs) ​

model_provider_configs 表实现大模型供应商的运行时可配置与热切换。uk(config_scope, provider_code) 唯一约束保证每个作用域(LLM / embedding)下每个供应商一行配置;active 标记当前生效的供应商,切换只改这一行并刷新内存缓存,不重启服务;api_key_ciphertext 将供应商密钥加密后落库,避免明文;embedding_dimension 记录向量维度,维度变更需要重嵌入,当前版本禁止直接切换。

字段名类型描述
idBIGINT AUTO_INCREMENT主键
config_scopeVARCHAR(32) NOT NULL,UK(config_scope, provider_code)配置作用域:LLM / embedding
provider_codeVARCHAR(64) NOT NULL,UK(config_scope, provider_code)供应商标识(deepseek/aliyun 等)
display_nameVARCHAR(128) NOT NULL展示名
api_styleVARCHAR(64) NOT NULLAPI 风格(OpenAI 兼容等)
api_base_urlVARCHAR(512) NOT NULL接口地址
api_key_ciphertextVARCHAR(2048)加密存储的密钥,运行时解密使用
model_nameVARCHAR(255) NOT NULL模型名
embedding_dimensionINTembedding 向量维度(仅 embedding 作用域)
enabledBIT(1) NOT NULL启用开关
activeBIT(1) NOT NULL当前生效供应商标记,热切换只改此字段
updated_byVARCHAR(255) NOT NULL最近修改人
created_at / updated_atDATETIME(6)创建/更新时间

十一、会话表(conversation_sessions) ​

conversation_sessions 表定义用户的会话(一次多轮对话的容器)。conversation_id 唯一索引是对外会话标识(UUID),客户端凭它拉取历史;status 支持会话归档;user_id 外键关联用户,idx_cs_user_id 支撑"我的会话列表"查询。

字段名类型描述
idBIGINT AUTO_INCREMENT主键
conversation_idVARCHAR(64) NOT NULL,UNIQUE对外会话标识(UUID)
user_idBIGINT NOT NULL,FK→users.id,KEY idx_cs_user_id会话属主
titleVARCHAR(255)会话标题
statusENUM('ACTIVE','ARCHIVED') NOT NULL会话状态,支持归档
created_at / updated_atDATETIME(6)创建/更新时间

十二、对话记录表(conversations) ​

conversations 表持久化每一轮问答。question/answer 成对存储;reference_mappings_json 以 JSON 保存本轮回答的引用映射(编号 → 文件名/页码/块 ID),历史会话渲染时引用预览不丢失——这是聊天历史模块不回归引用功能的关键字段;idx_user_id 与 idx_conversation_id 分别支撑"按用户拉全部历史"和"按会话拉消息"两种读路径。

字段名类型描述
idBIGINT AUTO_INCREMENT主键
user_idBIGINT NOT NULL,FK→users.id,KEY idx_user_id所属用户
conversation_idVARCHAR(64),KEY idx_conversation_id所属会话
questionTEXT NOT NULL用户提问
answerTEXT NOT NULL模型回答
reference_mappings_jsonLONGTEXT引用映射 JSON(编号→文件/页码/块ID),历史渲染引用预览的数据源
timestampDATETIME(6),KEY idx_timestamp问答时间

十三、用户每日聊天计数表(user_daily_chat_count) ​

user_daily_chat_count 表是每日聊天次数限流的持久层。uk_user_date 唯一约束保证一用户一天一行,计数递增;每日聊天上限的判定读这张表(或其缓存),跨天自然重置——与 Redis 余额限流(总量维度)、rate_limit_configs(速率维度)构成三套互补的管控。

字段名类型描述
idBIGINT AUTO_INCREMENT主键
user_idVARCHAR(50) NOT NULL,UK(user_id, record_date)用户标识
record_dateDATE NOT NULL,UK(user_id, record_date)统计日期,跨天自然重置
chat_request_countBIGINT NOT NULL当日聊天请求计数
created_at / updated_atDATETIME(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 语义——选型看场景,不是信仰。

本站仅供个人学习使用,请勿外传