Files
home4j/doc/数据库设计.md
T
2026-04-29 14:59:24 +08:00

528 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Home4j 数据库设计文档
> **版本**v1.0
> **更新日期**2026年2月3日
> **数据库**H2(默认)/ MySQL(可选)
---
## 一、设计原则
1. **简洁优先**:MVP 阶段保持表结构简单,避免过度设计
2. **扩展预留**:关键字段预留扩展空间(如 JSON 类型的 extra 字段)
3. **软删除**:重要数据支持软删除,便于恢复
4. **审计字段**:所有表包含创建时间、更新时间
---
## 二、实体关系图(ER Diagram
```
┌─────────────────┐ ┌─────────────────┐
│ t_user │ │ t_config │
├─────────────────┤ ├─────────────────┤
│ id (PK) │ │ id (PK) │
│ username │ │ config_key │
│ password_hash │ │ config_value │
│ role │ │ config_type │
│ ... │ │ ... │
└─────────────────┘ └─────────────────┘
┌─────────────────┐ ┌─────────────────┐
│ t_group │ │ t_bookmark │
├─────────────────┤ ├─────────────────┤
│ id (PK) │◄──────│ id (PK) │
│ name │ 1:N │ group_id (FK) │
│ icon │ │ name │
│ sort_order │ │ url │
│ ... │ │ icon │
└─────────────────┘ │ sort_order │
│ ... │
└─────────────────┘
```
---
## 三、数据表详细设计
### 3.1 用户表(t_user
> 存储管理员账号信息,MVP 阶段仅支持单管理员
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|-------|------|------|--------|------|
| `id` | BIGINT | PK, AUTO_INCREMENT | - | 主键 |
| `username` | VARCHAR(50) | NOT NULL, UNIQUE | - | 用户名 |
| `password_hash` | VARCHAR(255) | NOT NULL | - | 密码哈希(BCrypt |
| `nickname` | VARCHAR(100) | - | NULL | 显示昵称 |
| `email` | VARCHAR(100) | - | NULL | 邮箱(预留) |
| `role` | VARCHAR(20) | NOT NULL | 'ADMIN' | 角色:ADMIN / USER |
| `status` | TINYINT | NOT NULL | 1 | 状态:1-正常 0-禁用 |
| `last_login_time` | TIMESTAMP | - | NULL | 最后登录时间 |
| `last_login_ip` | VARCHAR(50) | - | NULL | 最后登录IP |
| `created_at` | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | 创建时间 |
| `updated_at` | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | 更新时间 |
**索引**
- `uk_username` UNIQUE (`username`)
**SQL**
```sql
CREATE TABLE t_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
nickname VARCHAR(100),
email VARCHAR(100),
role VARCHAR(20) NOT NULL DEFAULT 'ADMIN',
status TINYINT NOT NULL DEFAULT 1,
last_login_time TIMESTAMP,
last_login_ip VARCHAR(50),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uk_username UNIQUE (username)
);
```
---
### 3.2 分组表(t_group
> 书签的分类容器,支持排序和折叠
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|-------|------|------|--------|------|
| `id` | BIGINT | PK, AUTO_INCREMENT | - | 主键 |
| `name` | VARCHAR(100) | NOT NULL | - | 分组名称 |
| `icon` | VARCHAR(255) | - | NULL | 图标(图标名/URL/base64 |
| `description` | VARCHAR(500) | - | NULL | 分组描述 |
| `sort_order` | INT | NOT NULL | 0 | 排序序号(升序) |
| `collapsed` | BOOLEAN | NOT NULL | FALSE | 是否默认折叠 |
| `visible` | BOOLEAN | NOT NULL | TRUE | 是否可见 |
| `color` | VARCHAR(20) | - | NULL | 主题色(预留 v1.0 |
| `created_at` | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | 创建时间 |
| `updated_at` | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | 更新时间 |
**索引**
- `idx_sort_order` (`sort_order`)
**SQL**
```sql
CREATE TABLE t_group (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
icon VARCHAR(255),
description VARCHAR(500),
sort_order INT NOT NULL DEFAULT 0,
collapsed BOOLEAN NOT NULL DEFAULT FALSE,
visible BOOLEAN NOT NULL DEFAULT TRUE,
color VARCHAR(20),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE INDEX idx_group_sort_order ON t_group (sort_order);
```
---
### 3.3 书签表(t_bookmark
> 核心实体,存储书签链接信息
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|-------|------|------|--------|------|
| `id` | BIGINT | PK, AUTO_INCREMENT | - | 主键 |
| `group_id` | BIGINT | FK, NOT NULL | - | 所属分组ID |
| `name` | VARCHAR(100) | NOT NULL | - | 书签名称 |
| `url` | VARCHAR(2000) | NOT NULL | - | 链接地址 |
| `icon` | VARCHAR(500) | - | NULL | 图标(图标名/URL/base64 |
| `icon_type` | VARCHAR(20) | NOT NULL | 'TEXT' | 图标类型:BUILTIN/URL/UPLOAD/TEXT/EMOJI |
| `description` | VARCHAR(500) | - | NULL | 书签描述 |
| `sort_order` | INT | NOT NULL | 0 | 排序序号(升序) |
| `open_in_new_tab` | BOOLEAN | NOT NULL | TRUE | 是否新标签页打开 |
| `pinned` | BOOLEAN | NOT NULL | FALSE | 是否置顶 |
| `visible` | BOOLEAN | NOT NULL | TRUE | 是否可见 |
| `click_count` | INT | NOT NULL | 0 | 点击次数(预留 v1.0 |
| `last_click_time` | TIMESTAMP | - | NULL | 最后点击时间(预留 v1.0) |
| `created_at` | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | 创建时间 |
| `updated_at` | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | 更新时间 |
**索引**
- `idx_group_id` (`group_id`)
- `idx_group_sort` (`group_id`, `sort_order`)
**外键**
- `fk_bookmark_group` FOREIGN KEY (`group_id`) REFERENCES `t_group`(`id`)
**SQL**
```sql
CREATE TABLE t_bookmark (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
group_id BIGINT NOT NULL,
name VARCHAR(100) NOT NULL,
url VARCHAR(2000) NOT NULL,
icon VARCHAR(500),
icon_type VARCHAR(20) NOT NULL DEFAULT 'TEXT',
description VARCHAR(500),
sort_order INT NOT NULL DEFAULT 0,
open_in_new_tab BOOLEAN NOT NULL DEFAULT TRUE,
pinned BOOLEAN NOT NULL DEFAULT FALSE,
visible BOOLEAN NOT NULL DEFAULT TRUE,
click_count INT NOT NULL DEFAULT 0,
last_click_time TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_bookmark_group FOREIGN KEY (group_id) REFERENCES t_group(id) ON DELETE CASCADE
);
CREATE INDEX idx_bookmark_group_id ON t_bookmark (group_id);
CREATE INDEX idx_bookmark_group_sort ON t_bookmark (group_id, sort_order);
```
---
### 3.4 配置表(t_config
> 存储应用级配置,Key-Value 结构
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|-------|------|------|--------|------|
| `id` | BIGINT | PK, AUTO_INCREMENT | - | 主键 |
| `config_key` | VARCHAR(100) | NOT NULL, UNIQUE | - | 配置键 |
| `config_value` | TEXT | - | NULL | 配置值 |
| `config_type` | VARCHAR(20) | NOT NULL | 'STRING' | 值类型:STRING/NUMBER/BOOLEAN/JSON |
| `category` | VARCHAR(50) | NOT NULL | 'GENERAL' | 分类:GENERAL/APPEARANCE/LAYOUT |
| `description` | VARCHAR(255) | - | NULL | 配置说明 |
| `created_at` | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | 创建时间 |
| `updated_at` | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | 更新时间 |
**索引**
- `uk_config_key` UNIQUE (`config_key`)
- `idx_category` (`category`)
**SQL**
```sql
CREATE TABLE t_config (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
config_key VARCHAR(100) NOT NULL,
config_value TEXT,
config_type VARCHAR(20) NOT NULL DEFAULT 'STRING',
category VARCHAR(50) NOT NULL DEFAULT 'GENERAL',
description VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uk_config_key UNIQUE (config_key)
);
CREATE INDEX idx_config_category ON t_config (category);
```
**预置配置项**
| config_key | config_value | config_type | category | 说明 |
|-----------|--------------|-------------|----------|------|
| `app.title` | Home4j | STRING | GENERAL | 网站标题 |
| `app.subtitle` | Welcome Home | STRING | GENERAL | 副标题 |
| `app.logo` | NULL | STRING | GENERAL | Logo URL |
| `app.favicon` | NULL | STRING | GENERAL | Favicon URL |
| `app.theme` | light | STRING | APPEARANCE | 当前主题 |
| `app.language` | zh-CN | STRING | GENERAL | 界面语言 |
| `layout.columns` | 4 | NUMBER | LAYOUT | 每行列数 |
| `layout.card_size` | normal | STRING | LAYOUT | 卡片尺寸 |
| `layout.density` | normal | STRING | LAYOUT | 显示密度 |
| `search.enabled` | true | BOOLEAN | GENERAL | 启用搜索 |
| `search.placeholder` | 搜索书签... | STRING | GENERAL | 搜索框占位符 |
| `init.completed` | false | BOOLEAN | GENERAL | 初始化是否完成 |
---
### 3.5 书签标签表(t_bookmark_tag- 预留 v0.5
> 书签的标签,支持多对多关系(预留)
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|-------|------|------|--------|------|
| `id` | BIGINT | PK, AUTO_INCREMENT | - | 主键 |
| `name` | VARCHAR(50) | NOT NULL, UNIQUE | - | 标签名称 |
| `color` | VARCHAR(20) | - | NULL | 标签颜色 |
| `created_at` | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | 创建时间 |
**SQL**(预留):
```sql
CREATE TABLE t_bookmark_tag (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
color VARCHAR(20),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uk_tag_name UNIQUE (name)
);
-- 书签-标签关联表
CREATE TABLE t_bookmark_tag_rel (
bookmark_id BIGINT NOT NULL,
tag_id BIGINT NOT NULL,
PRIMARY KEY (bookmark_id, tag_id),
CONSTRAINT fk_rel_bookmark FOREIGN KEY (bookmark_id) REFERENCES t_bookmark(id) ON DELETE CASCADE,
CONSTRAINT fk_rel_tag FOREIGN KEY (tag_id) REFERENCES t_bookmark_tag(id) ON DELETE CASCADE
);
```
---
## 四、枚举值定义
### 4.1 用户角色(UserRole
```java
public enum UserRole {
ADMIN, // 管理员,可编辑
USER // 普通用户(预留 v1.0
}
```
### 4.2 图标类型(IconType
```java
public enum IconType {
BUILTIN, // 内置图标库
URL, // 外部 URL
UPLOAD, // 用户上传
TEXT, // 文字首字母
EMOJI // Emoji 表情
}
```
### 4.3 配置类型(ConfigType
```java
public enum ConfigType {
STRING, // 字符串
NUMBER, // 数字
BOOLEAN, // 布尔
JSON // JSON 对象
}
```
### 4.4 配置分类(ConfigCategory
```java
public enum ConfigCategory {
GENERAL, // 常规设置
APPEARANCE, // 外观设置
LAYOUT // 布局设置
}
```
---
## 五、Quarkus Panache 实体示例
### 5.1 Bookmark 实体
```java
package com.home4j.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "t_bookmark")
public class Bookmark extends PanacheEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "group_id", nullable = false)
public BookmarkGroup group;
@Column(nullable = false, length = 100)
public String name;
@Column(nullable = false, length = 2000)
public String url;
@Column(length = 500)
public String icon;
@Enumerated(EnumType.STRING)
@Column(name = "icon_type", nullable = false, length = 20)
public IconType iconType = IconType.TEXT;
@Column(length = 500)
public String description;
@Column(name = "sort_order", nullable = false)
public Integer sortOrder = 0;
@Column(name = "open_in_new_tab", nullable = false)
public Boolean openInNewTab = true;
@Column(nullable = false)
public Boolean pinned = false;
@Column(nullable = false)
public Boolean visible = true;
@Column(name = "click_count", nullable = false)
public Integer clickCount = 0;
@Column(name = "last_click_time")
public LocalDateTime lastClickTime;
@Column(name = "created_at", nullable = false, updatable = false)
public LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
public LocalDateTime updatedAt;
@PrePersist
public void prePersist() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
public void preUpdate() {
updatedAt = LocalDateTime.now();
}
// 常用查询方法
public static List<Bookmark> findByGroupId(Long groupId) {
return find("group.id = ?1 ORDER BY pinned DESC, sortOrder ASC", groupId).list();
}
public static List<Bookmark> findVisible() {
return find("visible = true ORDER BY pinned DESC, sortOrder ASC").list();
}
}
```
### 5.2 BookmarkGroup 实体
```java
package com.home4j.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "t_group")
public class BookmarkGroup extends PanacheEntity {
@Column(nullable = false, length = 100)
public String name;
@Column(length = 255)
public String icon;
@Column(length = 500)
public String description;
@Column(name = "sort_order", nullable = false)
public Integer sortOrder = 0;
@Column(nullable = false)
public Boolean collapsed = false;
@Column(nullable = false)
public Boolean visible = true;
@Column(length = 20)
public String color;
@OneToMany(mappedBy = "group", cascade = CascadeType.ALL, orphanRemoval = true)
public List<Bookmark> bookmarks;
@Column(name = "created_at", nullable = false, updatable = false)
public LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
public LocalDateTime updatedAt;
@PrePersist
public void prePersist() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
public void preUpdate() {
updatedAt = LocalDateTime.now();
}
// 常用查询方法
public static List<BookmarkGroup> findAllOrdered() {
return find("ORDER BY sortOrder ASC").list();
}
public static List<BookmarkGroup> findVisible() {
return find("visible = true ORDER BY sortOrder ASC").list();
}
}
```
---
## 六、数据初始化
### 6.1 首次启动初始化
```sql
-- 插入默认分组
INSERT INTO t_group (name, icon, sort_order, collapsed, visible)
VALUES ('默认分组', 'folder', 0, FALSE, TRUE);
-- 插入默认配置
INSERT INTO t_config (config_key, config_value, config_type, category, description) VALUES
('app.title', 'Home4j', 'STRING', 'GENERAL', '网站标题'),
('app.subtitle', 'Welcome Home', 'STRING', 'GENERAL', '副标题'),
('app.theme', 'light', 'STRING', 'APPEARANCE', '当前主题'),
('app.language', 'zh-CN', 'STRING', 'GENERAL', '界面语言'),
('layout.columns', '4', 'NUMBER', 'LAYOUT', '每行列数'),
('layout.card_size', 'normal', 'STRING', 'LAYOUT', '卡片尺寸'),
('search.enabled', 'true', 'BOOLEAN', 'GENERAL', '启用搜索'),
('init.completed', 'false', 'BOOLEAN', 'GENERAL', '初始化是否完成');
```
---
## 七、数据库配置
### 7.1 H2 配置(开发/默认)
```properties
# application.properties
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:file:./data/home4j;AUTO_SERVER=TRUE
quarkus.datasource.username=sa
quarkus.datasource.password=
quarkus.hibernate-orm.database.generation=update
```
### 7.2 MySQL 配置(生产可选)
```properties
# application-mysql.properties
quarkus.datasource.db-kind=mysql
quarkus.datasource.jdbc.url=jdbc:mysql://localhost:3306/home4j?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
quarkus.datasource.username=home4j
quarkus.datasource.password=your_password
quarkus.hibernate-orm.database.generation=update
```
---
## 八、表结构总览
| 表名 | 说明 | MVP | v0.5 | v1.0 |
|-----|------|-----|------|------|
| `t_user` | 用户表 | ✅ | ✅ | ✅ |
| `t_group` | 分组表 | ✅ | ✅ | ✅ |
| `t_bookmark` | 书签表 | ✅ | ✅ | ✅ |
| `t_config` | 配置表 | ✅ | ✅ | ✅ |
| `t_bookmark_tag` | 标签表 | - | ✅ | ✅ |
| `t_bookmark_tag_rel` | 书签标签关联 | - | ✅ | ✅ |
---
> **文档结束**
> 本设计将随项目进展持续更新