614 lines
16 KiB
Markdown
614 lines
16 KiB
Markdown
# 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 | 数据同步:完整的导入导出功能 | 低 |
|