好——阿杰,接着你的「日撸 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 + 前端)