好——阿杰,接着你的「日撸 Java 三百行」系列,给你 DAY10 综合任务1:一个偏实战、能把多项进阶点串在一起的小项目(约 200–300 行),适合练 OOP、集合、文件 I/O、异常处理、日期时间、和一点并发。
我设计的是:员工考勤管理(Console)
功能要点(综合考察):
- 员工增删改查(
Employee
) - 打卡记录(
AttendanceRecord
),支持上班/下班两种打卡 - 支持按日导出/保存到 CSV 文件(持久化)
- 支持查询某员工某日出勤、统计小时数
- 简单的命令行交互(菜单)
- 使用
ScheduledExecutorService
做自动定时保存(演示并发/任务调度)
下面直接给你完整可运行代码,粘到 AttendanceApp.java
编译运行即可(JDK 8+)。
import java.io.*;
import java.nio.file.*;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.*;
/**
* 日撸 Java 三百行 — DAY10 综合任务1
* 简单员工考勤管理(控制台)
*
* 用法: javac AttendanceApp.java && java AttendanceApp
*/
public class AttendanceApp {
// ------------------ Model ------------------
static class Employee {
final String id;
String name;
Employee(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return id + " - " + name;
}
}
static class AttendanceRecord {
final String empId;
final LocalDate date;
LocalTime checkIn; // 可以为 null
LocalTime checkOut; // 可以为 null
AttendanceRecord(String empId, LocalDate date) {
this.empId = empId;
this.date = date;
}
void setCheckIn(LocalTime t) { this.checkIn = t; }
void setCheckOut(LocalTime t) { this.checkOut = t; }
double workedHours() {
if (checkIn != null && checkOut != null) {
Duration d = Duration.between(checkIn, checkOut);
if (d.isNegative()) return 0;
return d.toMinutes() / 60.0;
}
return 0;
}
String toCsvLine() {
DateTimeFormatter tf = DateTimeFormatter.ofPattern("HH:mm:ss");
return String.join(",",
empId,
date.toString(),
checkIn == null ? "" : checkIn.format(tf),
checkOut == null ? "" : checkOut.format(tf));
}
static AttendanceRecord fromCsvLine(String line) {
// empId,date,checkIn,checkOut
String[] parts = line.split(",", -1);
if (parts.length < 4) return null;
AttendanceRecord r = new AttendanceRecord(parts[0], LocalDate.parse(parts[1]));
if (!parts[2].isEmpty()) r.checkIn = LocalTime.parse(parts[2]);
if (!parts[3].isEmpty()) r.checkOut = LocalTime.parse(parts[3]);
return r;
}
@Override
public String toString() {
return String.format("%s %s in:%s out:%s (%.2fh)",
empId, date,
checkIn == null ? "-" : checkIn,
checkOut == null ? "-" : checkOut,
workedHours());
}
}
// ------------------ Manager ------------------
static class AttendanceManager {
private final Map<String, Employee> employees = new HashMap<>();
// key: empId + "|" + date
private final Map<String, AttendanceRecord> records = new HashMap<>();
private final Path persistFile;
AttendanceManager(Path persistFile) {
this.persistFile = persistFile;
}
// Employee methods
boolean addEmployee(String id, String name) {
if (employees.containsKey(id)) return false;
employees.put(id, new Employee(id, name));
return true;
}
boolean removeEmployee(String id) {
if (!employees.containsKey(id)) return false;
employees.remove(id);
// also remove attendance records
records.keySet().removeIf(k -> k.startsWith(id + "|"));
return true;
}
Employee getEmployee(String id) { return employees.get(id); }
Collection<Employee> listEmployees() { return employees.values(); }
// Attendance methods
AttendanceRecord getRecord(String empId, LocalDate date) {
String k = key(empId, date);
return records.computeIfAbsent(k, s -> new AttendanceRecord(empId, date));
}
void checkIn(String empId, LocalDateTime now) {
AttendanceRecord r = getRecord(empId, now.toLocalDate());
r.setCheckIn(now.toLocalTime());
}
void checkOut(String empId, LocalDateTime now) {
AttendanceRecord r = getRecord(empId, now.toLocalDate());
r.setCheckOut(now.toLocalTime());
}
List<AttendanceRecord> listRecordsFor(String empId) {
List<AttendanceRecord> out = new ArrayList<>();
for (AttendanceRecord r : records.values()) {
if (r.empId.equals(empId)) out.add(r);
}
out.sort(Comparator.comparing(ar -> ar.date));
return out;
}
double totalHoursFor(String empId, LocalDate from, LocalDate to) {
double sum = 0;
for (AttendanceRecord r : records.values()) {
if (!r.empId.equals(empId)) continue;
if ((r.date.isBefore(from)) || (r.date.isAfter(to))) continue;
sum += r.workedHours();
}
return sum;
}
private String key(String empId, LocalDate date) {
return empId + "|" + date.toString();
}
// ---------------- Persistence (CSV) ----------------
synchronized void saveToFile() throws IOException {
try (BufferedWriter bw = Files.newBufferedWriter(persistFile)) {
// employees
bw.write("#EMPLOYEES");
bw.newLine();
for (Employee e : employees.values()) {
bw.write(e.id + "," + escape(e.name));
bw.newLine();
}
bw.write("#RECORDS");
bw.newLine();
for (AttendanceRecord r : records.values()) {
bw.write(r.toCsvLine());
bw.newLine();
}
}
}
synchronized void loadFromFile() throws IOException {
if (!Files.exists(persistFile)) return;
List<String> lines = Files.readAllLines(persistFile);
boolean inEmp = false, inRec = false;
for (String ln : lines) {
if (ln.trim().isEmpty()) continue;
if (ln.startsWith("#EMPLOYEES")) { inEmp = true; inRec = false; continue; }
if (ln.startsWith("#RECORDS")) { inEmp = false; inRec = true; continue; }
if (inEmp) {
String[] p = ln.split(",", 2);
if (p.length == 2) employees.put(p[0], new Employee(p[0], unescape(p[1])));
} else if (inRec) {
AttendanceRecord r = AttendanceRecord.fromCsvLine(ln);
if (r != null) records.put(key(r.empId, r.date), r);
}
}
}
private String escape(String s) {
return s.replace("\n", "\\n").replace(",", "\\,");
}
private String unescape(String s) {
return s.replace("\\n", "\n").replace("\\,", ",");
}
// export daily CSV (for a date)
synchronized void exportDay(LocalDate date, Path outFile) throws IOException {
try (BufferedWriter bw = Files.newBufferedWriter(outFile)) {
bw.write("empId,date,checkIn,checkOut,hours");
bw.newLine();
for (AttendanceRecord r : records.values()) {
if (r.date.equals(date)) {
bw.write(String.join(",",
r.empId, r.date.toString(),
r.checkIn == null ? "" : r.checkIn.toString(),
r.checkOut == null ? "" : r.checkOut.toString(),
String.format("%.2f", r.workedHours())
));
bw.newLine();
}
}
}
}
}
// ------------------ Simple Console UI ------------------
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Path dataFile = Paths.get("attendance_data.csv");
AttendanceManager mgr = new AttendanceManager(dataFile);
// load existing data
try {
mgr.loadFromFile();
System.out.println("已载入数据:" + dataFile.toAbsolutePath());
} catch (IOException e) {
System.out.println("载入数据失败:" + e.getMessage());
}
// scheduled autosave every 60s
ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
ses.scheduleAtFixedRate(() -> {
try {
mgr.saveToFile();
System.out.println("[自动保存] " + LocalDateTime.now());
} catch (IOException e) {
System.err.println("[自动保存失败] " + e.getMessage());
}
}, 60, 60, TimeUnit.SECONDS);
boolean running = true;
while (running) {
printMenu();
System.out.print("选择> ");
String cmd = sc.nextLine().trim();
try {
switch (cmd) {
case "1": // add employee
System.out.print("员工ID: ");
String id = sc.nextLine().trim();
System.out.print("员工姓名: ");
String name = sc.nextLine().trim();
if (mgr.addEmployee(id, name)) System.out.println("添加成功.");
else System.out.println("ID 已存在.");
break;
case "2": // remove
System.out.print("员工ID: ");
String rid = sc.nextLine().trim();
if (mgr.removeEmployee(rid)) System.out.println("已删除.");
else System.out.println("未找到员工.");
break;
case "3": // list employees
System.out.println("员工列表:");
for (Employee e : mgr.listEmployees()) System.out.println(" " + e);
break;
case "4": // check in
System.out.print("员工ID: ");
String cin = sc.nextLine().trim();
if (mgr.getEmployee(cin) == null) { System.out.println("未找到员工"); break; }
mgr.checkIn(cin, LocalDateTime.now());
System.out.println("打卡上班记录完成:" + LocalDateTime.now());
break;
case "5": // check out
System.out.print("员工ID: ");
String cout = sc.nextLine().trim();
if (mgr.getEmployee(cout) == null) { System.out.println("未找到员工"); break; }
mgr.checkOut(cout, LocalDateTime.now());
System.out.println("打卡下班记录完成:" + LocalDateTime.now());
break;
case "6": // view records
System.out.print("员工ID: ");
String vid = sc.nextLine().trim();
if (mgr.getEmployee(vid) == null) { System.out.println("未找到员工"); break; }
for (AttendanceRecord r : mgr.listRecordsFor(vid)) System.out.println(" " + r);
break;
case "7": // total hours
System.out.print("员工ID: ");
String tid = sc.nextLine().trim();
System.out.print("起始日期(YYYY-MM-DD): ");
LocalDate from = LocalDate.parse(sc.nextLine().trim());
System.out.print("结束日期(YYYY-MM-DD): ");
LocalDate to = LocalDate.parse(sc.nextLine().trim());
double hours = mgr.totalHoursFor(tid, from, to);
System.out.printf("总工时: %.2f 小时%n", hours);
break;
case "8": // export day
System.out.print("日期(YYYY-MM-DD): ");
LocalDate d = LocalDate.parse(sc.nextLine().trim());
Path out = Paths.get("export_" + d.toString() + ".csv");
mgr.exportDay(d, out);
System.out.println("导出到: " + out.toAbsolutePath());
break;
case "9": // manual save
mgr.saveToFile();
System.out.println("保存成功.");
break;
case "0": // exit
running = false;
break;
default:
System.out.println("未知选项.");
}
} catch (Exception ex) {
System.err.println("操作异常: " + ex.getMessage());
}
}
// shutdown
ses.shutdownNow();
try {
mgr.saveToFile();
} catch (IOException e) {
System.err.println("退出保存失败: " + e.getMessage());
}
System.out.println("已退出。");
}
private static void printMenu() {
System.out.println("\n=== 员工考勤管理(日撸 Java DAY10) ===");
System.out.println("1. 添加员工");
System.out.println("2. 删除员工");
System.out.println("3. 列出员工");
System.out.println("4. 打卡上班");
System.out.println("5. 打卡下班");
System.out.println("6. 查看员工记录");
System.out.println("7. 统计总工时");
System.out.println("8. 导出某天考勤 CSV");
System.out.println("9. 手动保存");
System.out.println("0. 退出");
}
}
说明与拓展建议
- 持久化格式:我用简单的自定义 CSV(员工区块 + 记录区块),便于查看和修改;你可替换为 JSON、SQLite、或直接用 JDBC 连接数据库。
- 并发演示:用
ScheduledExecutorService
每 60 秒自动保存一次(演示线程与主线程并发访问持久化方法)。实际生产请加锁/事务(这里saveToFile
已用synchronized
)。 - 可拓展项(练手):
- 增加用户登录与权限(管理员/普通员工)
- 支持手动修改某条记录(修正打卡时间)
- 支持导入已有 CSV、或导出为 Excel(Apache POI)
- 添加单元测试(JUnit),并用 mock 时间做时间相关测试
- 使用 GUI(Swing/JavaFX)或改成 Web(Spring Boot + 前端)
发表回复