junboV2/docs/plans/2026-02-05-sqlite-migration-design.md

614 lines
16 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.

# SQLite 数据迁移设计方案
## 概述
将 junboV2 项目的数据存储从 YAML/Excel 文件迁移到 SQLite 数据库,同时提供 Web 管理界面和双向同步能力。
## 技术选型
| 组件 | 技术 |
|------|------|
| 后端框架 | Spring Boot |
| 数据访问 | Spring Data JPA + Hibernate |
| 数据库 | SQLite |
| 前端框架 | Vue 3 |
| UI 组件库 | Element Plus |
| 使用场景 | 个人使用,无需认证 |
## 架构设计
### 整体架构
```
┌─────────────────────────────────────────────────────────┐
│ Vue 3 Web 界面 │
└─────────────────────┬───────────────────────────────────┘
│ REST API
┌─────────────────────▼───────────────────────────────────┐
│ Spring Boot 服务层 │
│ AttendanceService / OrganizationService / ... │
└───┬─────────────────┼─────────────────┬─────────────────┘
│ │ │
┌───▼───┐ ┌────▼────┐ ┌────▼────┐
│ Excel │ │ YAML │ │ SQLite │
│ DAO │ │ DAO │ │ DAO │
└───┬───┘ └────┬────┘ └────┬────┘
│ │ │
┌───▼───┐ ┌────▼────┐ ┌────▼────┐
│ .xlsx │ │ .yml │ │ .db │
└───────┘ └─────────┘ └─────────┘
```
### 设计要点
- 多 DAO 并存:保留现有 Excel/YAML DAO新增 SQLite DAO
- 业务层可按需选择数据源
- 双向同步通过导入/导出功能实现
## 数据库设计
### 组织架构模块
```sql
-- 员工表
CREATE TABLE employee (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 员工别名表
CREATE TABLE employee_alias (
id INTEGER PRIMARY KEY AUTOINCREMENT,
employee_id INTEGER NOT NULL,
alias VARCHAR(50) NOT NULL,
FOREIGN KEY (employee_id) REFERENCES employee(id)
);
-- 部门表
CREATE TABLE department (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL UNIQUE
);
-- 部门层级关系表
CREATE TABLE department_relation (
id INTEGER PRIMARY KEY AUTOINCREMENT,
parent_id INTEGER NOT NULL,
child_id INTEGER NOT NULL,
effective_date DATE NOT NULL,
expiry_date DATE,
FOREIGN KEY (parent_id) REFERENCES department(id),
FOREIGN KEY (child_id) REFERENCES department(id)
);
-- 员工部门关系表
CREATE TABLE employee_department (
id INTEGER PRIMARY KEY AUTOINCREMENT,
employee_id INTEGER NOT NULL,
department_id INTEGER NOT NULL,
effective_date DATE NOT NULL,
expiry_date DATE,
FOREIGN KEY (employee_id) REFERENCES employee(id),
FOREIGN KEY (department_id) REFERENCES department(id)
);
```
### 考勤模块
```sql
-- 工作日历表
CREATE TABLE work_calendar (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date DATE NOT NULL UNIQUE,
is_workday BOOLEAN NOT NULL,
is_weekend BOOLEAN NOT NULL,
is_holiday BOOLEAN NOT NULL,
holiday_name VARCHAR(50),
remark VARCHAR(200)
);
-- 签到记录表 (每次打卡一条记录)
CREATE TABLE sign_record (
id INTEGER PRIMARY KEY AUTOINCREMENT,
employee_id INTEGER NOT NULL,
date DATE NOT NULL,
time TIME NOT NULL,
is_outside BOOLEAN DEFAULT FALSE,
remark VARCHAR(500),
FOREIGN KEY (employee_id) REFERENCES employee(id)
);
CREATE INDEX idx_sign_employee_date ON sign_record(employee_id, date);
-- 请假记录表
CREATE TABLE leave_record (
id INTEGER PRIMARY KEY AUTOINCREMENT,
employee_id INTEGER NOT NULL,
date DATE NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
leave_type VARCHAR(20) NOT NULL,
reason VARCHAR(500),
approval_status VARCHAR(20),
approval_result VARCHAR(20),
remark VARCHAR(500),
FOREIGN KEY (employee_id) REFERENCES employee(id)
);
```
### 业务模块
```sql
-- Sprint 迭代表
CREATE TABLE sprint (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
remark VARCHAR(500)
);
-- Story 用户故事表
CREATE TABLE story (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sprint_id INTEGER NOT NULL,
title VARCHAR(200) NOT NULL,
story_point DECIMAL(5,1),
status VARCHAR(20),
remark VARCHAR(500),
FOREIGN KEY (sprint_id) REFERENCES sprint(id)
);
-- Story 角色关联表
CREATE TABLE story_member (
id INTEGER PRIMARY KEY AUTOINCREMENT,
story_id INTEGER NOT NULL,
employee_id INTEGER NOT NULL,
role VARCHAR(20) NOT NULL, -- PO/DEV/TEST/ACCEPT
FOREIGN KEY (story_id) REFERENCES story(id),
FOREIGN KEY (employee_id) REFERENCES employee(id)
);
-- KPA 记录主表
CREATE TABLE kpa_record (
id INTEGER PRIMARY KEY AUTOINCREMENT,
year INTEGER NOT NULL,
month INTEGER NOT NULL,
title VARCHAR(200),
description TEXT,
remark VARCHAR(500),
UNIQUE(year, month, title)
);
-- KPA 员工得分表
CREATE TABLE kpa_employee_score (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kpa_record_id INTEGER NOT NULL,
employee_id INTEGER NOT NULL,
score DECIMAL(5,2),
level VARCHAR(10),
remark VARCHAR(500),
FOREIGN KEY (kpa_record_id) REFERENCES kpa_record(id),
FOREIGN KEY (employee_id) REFERENCES employee(id)
);
-- 激励记录表
CREATE TABLE incentive_record (
id INTEGER PRIMARY KEY AUTOINCREMENT,
employee_id INTEGER NOT NULL,
date DATE NOT NULL,
type VARCHAR(50),
amount DECIMAL(10,2),
reason VARCHAR(500),
FOREIGN KEY (employee_id) REFERENCES employee(id)
);
```
## JPA Entity 设计
### 组织架构模块
```java
@Entity
@Table(name = "employee")
@Getter @Setter
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
@OneToMany(mappedBy = "employee", cascade = CascadeType.ALL)
private List<EmployeeAlias> aliases = new ArrayList<>();
@OneToMany(mappedBy = "employee")
private List<EmployeeDepartment> departmentRelations = new ArrayList<>();
private LocalDateTime createdAt;
}
@Entity
@Table(name = "employee_alias")
@Getter @Setter
public class EmployeeAlias {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "employee_id", nullable = false)
private Employee employee;
@Column(nullable = false)
private String alias;
}
@Entity
@Table(name = "department")
@Getter @Setter
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
}
@Entity
@Table(name = "employee_department")
@Getter @Setter
public class EmployeeDepartment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "employee_id", nullable = false)
private Employee employee;
@ManyToOne
@JoinColumn(name = "department_id", nullable = false)
private Department department;
@Column(nullable = false)
private LocalDate effectiveDate;
private LocalDate expiryDate;
}
```
### 考勤模块
```java
@Entity
@Table(name = "work_calendar")
@Getter @Setter
public class WorkCalendar {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private LocalDate date;
private boolean workday;
private boolean weekend;
private boolean holiday;
private String holidayName;
private String remark;
}
@Entity
@Table(name = "sign_record")
@Getter @Setter
public class SignRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "employee_id", nullable = false)
private Employee employee;
@Column(nullable = false)
private LocalDate date;
@Column(nullable = false)
private LocalTime time;
private boolean outside;
private String remark;
}
@Entity
@Table(name = "leave_record")
@Getter @Setter
public class LeaveRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "employee_id", nullable = false)
private Employee employee;
@Column(nullable = false)
private LocalDate date;
@Column(nullable = false)
private LocalTime startTime;
@Column(nullable = false)
private LocalTime endTime;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private LeaveType leaveType;
private String reason;
private String approvalStatus;
private String approvalResult;
private String remark;
}
public enum LeaveType {
SICK, PERSONAL, VACATION, CHANGE_REST, OUT, MATERNITY, FUNERAL, MARRIAGE
}
```
### 业务模块
```java
@Entity
@Table(name = "sprint")
@Getter @Setter
public class Sprint {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private LocalDate startDate;
@Column(nullable = false)
private LocalDate endDate;
private String remark;
@OneToMany(mappedBy = "sprint")
private List<Story> stories = new ArrayList<>();
}
@Entity
@Table(name = "story")
@Getter @Setter
public class Story {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "sprint_id", nullable = false)
private Sprint sprint;
@Column(nullable = false)
private String title;
private BigDecimal storyPoint;
private String status;
private String remark;
@OneToMany(mappedBy = "story", cascade = CascadeType.ALL, orphanRemoval = true)
private List<StoryMember> members = new ArrayList<>();
}
@Entity
@Table(name = "story_member")
@Getter @Setter
public class StoryMember {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "story_id", nullable = false)
private Story story;
@ManyToOne
@JoinColumn(name = "employee_id", nullable = false)
private Employee employee;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private StoryRole role;
}
public enum StoryRole {
PO, DEV, TEST, ACCEPT
}
@Entity
@Table(name = "kpa_record")
@Getter @Setter
public class KpaRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private Integer year;
@Column(nullable = false)
private Integer month;
private String title;
@Column(columnDefinition = "TEXT")
private String description;
private String remark;
@OneToMany(mappedBy = "kpaRecord", cascade = CascadeType.ALL, orphanRemoval = true)
private List<KpaEmployeeScore> employeeScores = new ArrayList<>();
}
@Entity
@Table(name = "kpa_employee_score")
@Getter @Setter
public class KpaEmployeeScore {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "kpa_record_id", nullable = false)
private KpaRecord kpaRecord;
@ManyToOne
@JoinColumn(name = "employee_id", nullable = false)
private Employee employee;
private BigDecimal score;
private String level;
private String remark;
}
@Entity
@Table(name = "incentive_record")
@Getter @Setter
public class IncentiveRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "employee_id", nullable = false)
private Employee employee;
@Column(nullable = false)
private LocalDate date;
private String type;
private BigDecimal amount;
private String reason;
}
```
## Service 层设计
### 多数据源架构
```java
public enum DataSourceType {
SQLITE, EXCEL, YAML
}
@Service
@RequiredArgsConstructor
public class SignServiceImpl implements SignService {
private final SignRecordRepository signRecordRepository; // SQLite
private final SignExcelDao signExcelDao; // Excel
private final EmployeeRepository employeeRepository;
@Value("${app.datasource.sign:SQLITE}")
private DataSourceType defaultSource;
public List<SignBo> getSignRecords(Employee employee, LocalDate start, LocalDate end,
DataSourceType source) {
return switch (source) {
case SQLITE -> loadFromSqlite(employee, start, end);
case EXCEL -> loadFromExcel(employee, start, end);
default -> throw new IllegalArgumentException("Unsupported source: " + source);
};
}
@Transactional
public void importFromExcel(String filePath) {
List<SignBo> excelData = signExcelDao.read2Bo();
excelData.forEach(this::saveSignRecord);
}
public void exportToExcel(String filePath, LocalDate start, LocalDate end) {
List<SignRecord> records = signRecordRepository
.findByDateBetweenOrderByEmployeeAscDateAscTimeAsc(start, end);
// 调用 Excel 写入工具
}
}
```
## REST API 设计
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/employees | 员工列表 |
| POST | /api/employees | 新增员工 |
| PUT | /api/employees/{id} | 更新员工 |
| DELETE | /api/employees/{id} | 删除员工 |
| GET | /api/departments | 部门列表 |
| GET | /api/sign | 签到记录列表 |
| POST | /api/sign | 新增签到记录 |
| GET | /api/leaves | 请假记录列表 |
| POST | /api/leaves | 新增请假记录 |
| GET | /api/calendar | 工作日历 |
| GET | /api/sprints | Sprint 列表 |
| GET | /api/stories | Story 列表 |
| GET | /api/kpa | KPA 记录列表 |
| GET | /api/incentives | 激励记录列表 |
| POST | /api/sync/import/yaml | 从 YAML 导入 |
| POST | /api/sync/import/excel | 从 Excel 导入 |
| POST | /api/sync/export/yaml | 导出到 YAML |
| POST | /api/sync/export/excel | 导出到 Excel |
## 前端设计
### 项目结构
```
junbo-web/
├── src/
│ ├── api/ # API 调用
│ │ ├── employee.js
│ │ ├── sign.js
│ │ ├── leave.js
│ │ └── sync.js
│ ├── views/ # 页面
│ │ ├── employee/
│ │ ├── attendance/
│ │ ├── scrum/
│ │ ├── kpa/
│ │ └── sync/
│ ├── components/ # 公共组件
│ ├── router/
│ └── App.vue
└── package.json
```
### 导航菜单
| 菜单 | 页面 |
|------|------|
| 组织架构 | 员工管理、部门管理 |
| 考勤管理 | 签到记录、请假记录、工作日历 |
| Scrum | Sprint、Story |
| 绩效 | KPA 记录、激励记录 |
| 数据同步 | 导入导出面板 |
## 实现阶段
| 阶段 | 内容 | 优先级 |
|------|------|--------|
| 1 | 基础设施Spring Boot 项目、SQLite 配置、JPA Entity | 高 |
| 2 | 组织架构模块:员工、部门 CRUD + YAML 导入 | 高 |
| 3 | 考勤模块:签到、请假 CRUD + Excel 导入 | 高 |
| 4 | Vue 前端:基础框架 + 组织架构页面 | 中 |
| 5 | 其他业务模块Sprint、Story、KPA、Incentive | 中 |
| 6 | 数据同步:完整的导入导出功能 | 低 |