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
- 业务层可按需选择数据源
- 双向同步通过导入/导出功能实现
数据库设计
组织架构模块
-- 员工表
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)
);
考勤模块
-- 工作日历表
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)
);
业务模块
-- 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 设计
组织架构模块
@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;
}
考勤模块
@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
}
业务模块
@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 层设计
多数据源架构
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 |
数据同步:完整的导入导出功能 |
低 |