¥
立即购买

编程教学专家助手

467 浏览
45 试用
9 购买
Nov 27, 2025更新

本提示词帮助用户通过专家视角学习编程,清晰讲解各语言核心概念、最佳实践及工具使用,指导项目构建与问题解决,并跟进行业趋势,提升学习效率与实战能力。

下面给出一个完整、可直接运行和测试的项目示例,满足你对教学与实践的全部要求。包含:

  • data_pipeline.py:数据清洗、统计、命令行参数、日志与性能基准
  • requirements.txt:依赖
  • test_pipeline.py:pytest 单元测试(3 个用例)
  • 关键设计与调试要点说明

======================== data_pipeline.py

#!/usr/bin/env python3

-- coding: utf-8 --

""" 电商订单数据清洗与统计脚本:data_pipeline.py

功能概述:

  • 使用 pandas 读取 UTF-8 CSV(order_id, item_name, category, price, quantity, status, created_at)
  • 数据清洗:
    • 去除关键字段缺失(order_id, category, price, quantity, created_at)
    • 解析 created_at 为 datetime(先按 UTC 解析,再统一到本地时区)
    • 过滤退款订单(status != 'refunded')
    • 新增字段 amount = price * quantity(保留两位小数)
  • 统计输出:
    • 按 category 汇总 GMV(sum(amount))、订单数(去重 order_id)、客单价(GMV/订单数),并按 GMV 降序取前 N(--top-n,默认 10)
    • 按日(created_at 的日期)汇总 GMV 与订单数
  • 命令行参数(argparse):
    • --input 输入 CSV 路径
    • --out-dir 输出目录
    • --start-date/--end-date 筛选时间范围(含边界)
    • --top-n 默认为 10
    • --log-level 默认为 INFO
  • 工程规范:
    • 清晰的函数划分、类型注解与 docstring
    • logging 输出关键步骤与耗时;对文件缺失、日期解析异常做优雅错误处理
  • 单元测试(pytest)见 test_pipeline.py
  • 性能基准:对比向量化与 for 循环的 amount 计算耗时(time.perf_counter)

示例 CSV(UTF-8,7 列): order_id,item_name,category,price,quantity,status,created_at 1001,水杯,家居,19.9,2,paid,2024-06-01T10:11:00Z 1002,数据线,数码,12.5,1,paid,2024-06-01T12:40:00Z 1003,运动袜,服饰,15.0,3,refunded,2024-06-02T09:20:00Z 1004,鼠标,数码,79.0,1,paid,2024-06-02T11:05:00Z 1005,抱枕,家居,35.0,1,paid,2024-06-03T08:00:00Z

示例运行: python data_pipeline.py --input ./orders.csv --out-dir ./out --start-date 2024-06-01 --end-date 2024-06-03 --top-n 5 --log-level INFO

预期输出(控制台日志片段示意): [INFO] Loaded 5 rows in 0.01s [INFO] Cleaned data: 4 rows remain (dropped=1, refunded removed) [INFO] Date range filter applied: 2024-06-01 00:00:00+08:00 ~ 2024-06-03 23:59:59.999999+08:00 -> 4 rows [INFO] Category summary generated (top 5), Daily summary generated [INFO] Saved category summary to out/category_summary_top5.csv [INFO] Saved daily summary to out/daily_summary.csv [INFO] Benchmark amount calc: vectorized=0.0003s, loop=0.0121s

输出文件:

  • out/category_summary_top5.csv(列:category, gmv, order_count, aov)
  • out/daily_summary.csv(列:date, gmv, order_count) """

from future import annotations

import argparse import logging import os import sys import time from typing import Optional, Tuple

import pandas as pd from dateutil import parser as du_parser from dateutil.tz import tzlocal

关键字段

REQUIRED_COLUMNS = ["order_id", "item_name", "category", "price", "quantity", "status", "created_at"]

logger = logging.getLogger(name)

def setup_logging(level: str) -> None: """ 配置全局日志格式与级别。 """ log_level = getattr(logging, level.upper(), logging.INFO) logging.basicConfig( level=log_level, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", )

def read_data(csv_path: str) -> pd.DataFrame: """ 从 CSV 读取订单数据。

Parameters:
    csv_path: 输入 CSV 文件路径

Returns:
    DataFrame 包含原始数据

Raises:
    FileNotFoundError: 当文件不存在时
    ValueError: 当缺少必要列时
"""
start = time.perf_counter()
if not os.path.exists(csv_path):
    logger.error(f"Input file does not exist: {csv_path}")
    raise FileNotFoundError(csv_path)

try:
    df = pd.read_csv(csv_path, encoding="utf-8")
except Exception as e:
    logger.error(f"Failed to read CSV: {e}")
    raise

missing_cols = [c for c in REQUIRED_COLUMNS if c not in df.columns]
if missing_cols:
    raise ValueError(f"Missing required columns: {missing_cols}")

logger.info(f"Loaded {len(df)} rows in {time.perf_counter() - start:.4f}s")
return df

def clean_data(df: pd.DataFrame) -> pd.DataFrame: """ 清洗数据: - 保留必要列;去除关键字段缺失的行 - price/quantity 转数值,created_at 解析为本地时区的 tz-aware datetime - 过滤退款订单(status != 'refunded') - 新增 amount = price * quantity(保留两位小数)

Parameters:
    df: 原始 DataFrame

Returns:
    清洗后的 DataFrame(包含 amount 列,created_at 为本地时区 tz-aware)
"""
t0 = time.perf_counter()

# 只保留必要列(保持顺序)
df = df[[c for c in REQUIRED_COLUMNS if c in df.columns]].copy()

# 丢弃关键字段为空的行(字符串空值在 to_numeric 时转 NaN)
before = len(df)

# 转换价格与数量为数值
df["price"] = pd.to_numeric(df["price"], errors="coerce")
df["quantity"] = pd.to_numeric(df["quantity"], errors="coerce")

# 解析时间:先转为 UTC,再转本地时区
# errors='coerce' 将异常解析设为 NaT,后续丢弃
created_at_utc = pd.to_datetime(df["created_at"], utc=True, errors="coerce")
local_tz = tzlocal()
df["created_at"] = created_at_utc.dt.tz_convert(local_tz)

# 丢弃关键字段为空或 NaT
df = df.dropna(subset=["order_id", "category", "price", "quantity", "created_at"])

# 过滤退款
df = df[df["status"].astype(str).str.lower() != "refunded"]

# 计算金额(向量化)
df["amount"] = (df["price"] * df["quantity"]).round(2)

after = len(df)
logger.info(
    f"Cleaned data: {after} rows remain (dropped={before - after}) in {time.perf_counter() - t0:.4f}s"
)
return df

def parse_date_arg(s: Optional[str]) -> Optional[pd.Timestamp]: """ 将命令行的日期字符串解析为本地时区的 tz-aware Timestamp。 - 若为空返回 None - 若无时区信息,按本地时区解释 - 若仅提供日期(YYYY-MM-DD),调用方负责设定为当天起止时间

Parameters:
    s: 输入字符串

Returns:
    tz-aware Timestamp 或 None

Raises:
    ValueError: 解析失败
"""
if not s:
    return None
try:
    dt = du_parser.parse(s)
except Exception as e:
    raise ValueError(f"Invalid date value: {s}, error: {e}") from e

local_tz = tzlocal()
if dt.tzinfo is None:
    # 视为本地时间
    dt = dt.replace(tzinfo=local_tz)
else:
    # 转换到本地时区
    dt = pd.Timestamp(dt).tz_convert(local_tz).to_pydatetime()
return pd.Timestamp(dt)

def normalize_date_range( start: Optional[pd.Timestamp], end: Optional[pd.Timestamp] ) -> Tuple[Optional[pd.Timestamp], Optional[pd.Timestamp]]: """ 规范化起止时间(含边界): - 若只给出日期(无时分秒),start 设为当天 00:00:00,本地时区;end 设为当天 23:59:59.999999,本地时区 - 确保 start <= end

Returns:
    (start, end) 规范化后的时间
"""
local_tz = tzlocal()

def is_pure_date(ts: pd.Timestamp) -> bool:
    return ts.hour == 0 and ts.minute == 0 and ts.second == 0 and ts.microsecond == 0

if start is not None:
    if is_pure_date(start):
        start = start.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=start.tzinfo or local_tz)
if end is not None:
    if is_pure_date(end):
        end = end.replace(hour=23, minute=59, second=59, microsecond=999999, tzinfo=end.tzinfo or local_tz)

if start and end and start > end:
    raise ValueError(f"start-date {start} must be <= end-date {end}")

return start, end

def filter_by_date_range( df: pd.DataFrame, start: Optional[pd.Timestamp], end: Optional[pd.Timestamp] ) -> pd.DataFrame: """ 按日期时间范围筛选(含边界)。

Parameters:
    df: 清洗后的 DataFrame(created_at 为本地时区 tz-aware)
    start: 起始时间(含),或 None
    end: 结束时间(含),或 None

Returns:
    过滤后的 DataFrame
"""
t0 = time.perf_counter()
if start is None and end is None:
    return df

mask = pd.Series([True] * len(df), index=df.index)
if start is not None:
    mask &= df["created_at"] >= start
if end is not None:
    mask &= df["created_at"] <= end

out = df[mask].copy()
logger.info(
    f"Date range filter applied: {start} ~ {end} -> {len(out)} rows (from {len(df)}) in "
    f"{time.perf_counter() - t0:.4f}s"
)
return out

def compute_category_summary(df: pd.DataFrame, top_n: int = 10) -> pd.DataFrame: """ 计算按品类的汇总(GMV、订单数、客单价),并按 GMV 降序取前 top_n。

Parameters:
    df: 清洗后的 DataFrame(包含 amount, created_at)
    top_n: 取前 N

Returns:
    DataFrame 列:category, gmv, order_count, aov
"""
t0 = time.perf_counter()
grp = df.groupby("category").agg(
    gmv=("amount", "sum"),
    order_count=("order_id", pd.Series.nunique),
)
grp["aov"] = (grp["gmv"] / grp["order_count"]).round(2)
grp = grp.sort_values("gmv", ascending=False)
res = grp.head(top_n).reset_index()
# 数值保留两位
res["gmv"] = res["gmv"].round(2)
logger.info(f"Category summary generated (top {top_n}) in {time.perf_counter() - t0:.4f}s")
return res

def compute_daily_summary(df: pd.DataFrame) -> pd.DataFrame: """ 按日汇总 GMV 与订单数。

Parameters:
    df: 清洗后的 DataFrame

Returns:
    DataFrame 列:date, gmv, order_count
"""
t0 = time.perf_counter()
tmp = df.copy()
tmp["date"] = tmp["created_at"].dt.date
grp = tmp.groupby("date").agg(
    gmv=("amount", "sum"),
    order_count=("order_id", pd.Series.nunique),
)
res = grp.reset_index()
res["gmv"] = res["gmv"].round(2)
logger.info(f"Daily summary generated in {time.perf_counter() - t0:.4f}s")
return res

def save_reports( category_df: pd.DataFrame, daily_df: pd.DataFrame, out_dir: str, top_n: int, ) -> Tuple[str, str]: """ 保存报表到 CSV。

Returns:
    (category_path, daily_path)
"""
os.makedirs(out_dir, exist_ok=True)
category_path = os.path.join(out_dir, f"category_summary_top{top_n}.csv")
daily_path = os.path.join(out_dir, "daily_summary.csv")
category_df.to_csv(category_path, index=False, encoding="utf-8")
daily_df.to_csv(daily_path, index=False, encoding="utf-8")
logger.info(f"Saved category summary to {category_path}")
logger.info(f"Saved daily summary to {daily_path}")
return category_path, daily_path

def benchmark_vectorized_vs_loop(df: pd.DataFrame) -> Tuple[float, float]: """ 简单性能基准:对比向量化与 for 循环的 amount 计算。

Parameters:
    df: 清洗前或清洗后的 DataFrame(需包含 price, quantity)

Returns:
    (t_vec, t_loop) 两种方式的耗时(秒)
"""
# 仅使用 price 与 quantity 计算,避免影响原列
tmp = df[["price", "quantity"]].copy()

# 向量化实现
t0 = time.perf_counter()
_ = (tmp["price"] * tmp["quantity"]).round(2)
t_vec = time.perf_counter() - t0

# for 循环实现
t0 = time.perf_counter()
res = []
for row in tmp.itertuples(index=False):
    res.append(round(float(row.price) * float(row.quantity), 2))
# 防止优化器忽略计算
_ = res
t_loop = time.perf_counter() - t0

logger.info(f"Benchmark amount calc: vectorized={t_vec:.6f}s, loop={t_loop:.6f}s")
return t_vec, t_loop

def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="E-commerce order data cleaning and reporting") parser.add_argument("--input", required=True, help="Input CSV path (UTF-8)") parser.add_argument("--out-dir", required=True, help="Output directory") parser.add_argument("--start-date", default=None, help="Start date/time (inclusive), e.g. 2024-06-01 or 2024-06-01T10:00") parser.add_argument("--end-date", default=None, help="End date/time (inclusive)") parser.add_argument("--top-n", type=int, default=10, help="Top N categories by GMV (default: 10)") parser.add_argument("--log-level", default="INFO", help="Logging level (DEBUG, INFO, WARNING, ...)") return parser.parse_args(argv)

def main(argv: Optional[list[str]] = None) -> int: args = parse_args(argv) setup_logging(args.log_level)

try:
    raw_df = read_data(args.input)
    cleaned = clean_data(raw_df)

    # 解析时间范围
    start = parse_date_arg(args.start_date)
    end = parse_date_arg(args.end_date)
    start, end = normalize_date_range(start, end)

    if start is not None or end is not None:
        cleaned = filter_by_date_range(cleaned, start, end)

    if cleaned.empty:
        logger.warning("No data after cleaning/filtering. No reports generated.")
        return 0

    cat_df = compute_category_summary(cleaned, top_n=args.top_n)
    daily_df = compute_daily_summary(cleaned)
    save_reports(cat_df, daily_df, args.out_dir, top_n=args.top_n)

    # 简单基准(对清洗后的数据)
    benchmark_vectorized_vs_loop(cleaned)

    return 0
except FileNotFoundError:
    # 已在 read_data 中记录
    return 2
except ValueError as e:
    logger.error(str(e))
    return 3
except Exception as e:
    logger.exception(f"Unexpected error: {e}")
    return 1

if name == "main": sys.exit(main())

======================== requirements.txt

pandas>=2.0 python-dateutil>=2.8.2 pytest>=7.0

======================== test_pipeline.py

-- coding: utf-8 --

import pandas as pd import pytest

from data_pipeline import ( clean_data, filter_by_date_range, compute_category_summary, parse_date_arg, normalize_date_range, )

@pytest.fixture def raw_df_fixture(): """ 构造内存 DataFrame,覆盖: - 正常行 - 退款行 - price/quantity/order_id 缺失 - 多个品类与订单 """ data = [ # valid { "order_id": "1001", "item_name": "A", "category": "Cat1", "price": 10.0, "quantity": 2, "status": "paid", "created_at": "2024-06-01T12:00:00Z", }, # refunded -> should be filtered { "order_id": "1002", "item_name": "B", "category": "Cat1", "price": 5.0, "quantity": 1, "status": "refunded", "created_at": "2024-06-01T13:00:00Z", }, # missing price -> drop { "order_id": "1003", "item_name": "C", "category": "Cat2", "price": None, "quantity": 1, "status": "paid", "created_at": "2024-06-02T10:00:00Z", }, # missing order_id -> drop { "order_id": None, "item_name": "D", "category": "Cat2", "price": 20.0, "quantity": 1, "status": "paid", "created_at": "2024-06-02T11:00:00Z", }, # another valid { "order_id": "1004", "item_name": "E", "category": "Cat2", "price": 30.0, "quantity": 1, "status": "paid", "created_at": "2024-06-02T15:00:00Z", }, ] return pd.DataFrame(data)

def test_cleaning_logic(raw_df_fixture): df_clean = clean_data(raw_df_fixture) # refunded + missing price + missing order_id -> 3 rows removed, leaving 2 assert len(df_clean) == 2 # amount calculation accuracy # rows: 1001 amount=20.0, 1004 amount=30.0 amounts = sorted(df_clean["amount"].tolist()) assert amounts == [20.0, 30.0] # created_at should be tz-aware (local timezone) assert str(df_clean["created_at"].dt.tz) != "None"

def test_date_range_filter_inclusive(raw_df_fixture): df_clean = clean_data(raw_df_fixture) # Use inclusive boundaries that cover both valid rows (2024-06-01 and 2024-06-02, local time) start = parse_date_arg("2024-06-01") end = parse_date_arg("2024-06-02") start, end = normalize_date_range(start, end) df_filtered = filter_by_date_range(df_clean, start, end) # Both valid rows fall within these two days (local time), inclusive assert len(df_filtered) == 2

def test_category_summary_topn(raw_df_fixture): df_clean = clean_data(raw_df_fixture) # Cat1 total amount: 20.0 (order 1001) # Cat2 total amount: 30.0 (order 1004) res = compute_category_summary(df_clean, top_n=1) assert len(res) == 1 assert res.iloc[0]["category"] == "Cat2" assert float(res.iloc[0]["gmv"]) == 30.0 assert int(res.iloc[0]["order_count"]) == 1

======================== 关键设计与调试要点(教学向导)

  • 基础概念理解

    • 向量化计算:pandas 对列操作比 Python for 循环快几个数量级。示例中 amount 使用向量化实现,并提供基准对比。
    • 时间序列与时区:先统一解析为 UTC,再转换到本地时区,保证对“按日”汇总与时间筛选的本地一致性。
    • 去重计数:订单数采用 nunique(order_id)。按品类统计时,若一个订单包含多个品类,会在各品类中分别计数——与大多数品类报表的口径一致。
  • 函数与方法

    • 单一职责:read_data、clean_data、filter_by_date_range、compute_category_summary、compute_daily_summary、save_reports、benchmark_vectorized_vs_loop、main 分工清晰且可测试。
    • 类型注解与 docstring:便于 IDE 补全、类型检查与团队协作。
  • 数据结构与算法

    • groupby/agg:用于聚合统计;nunique 用于去重计数。
    • 高效筛选:布尔掩码而非逐行判断。
  • 项目实践

    • 可作为定时任务(本地或 CI)每日运行;输入来自对象存储的增量 CSV,输出到 out 目录供周报汇总。
    • 稳定输出文件名:category_summary_topN.csv 与 daily_summary.csv,便于下游消费。
  • 调试技巧

    • logging:在加载、清洗、过滤、汇总与保存各步骤输出耗时与行数,快速定位瓶颈或数据异常。
    • 异常处理:文件缺失、日期解析失败会记录日志并以非零状态退出;清洗后无数据给予 WARNING。
    • 单元测试:内存构造 DataFrame,避免 IO;覆盖清洗逻辑、日期范围(边界包含)、TopN 排序正确性。
  • 代码优化

    • 避免 Python 层循环;优先使用 pandas 内置向量化。
    • 只保留必要列减少内存与计算。
    • to_numeric(errors="coerce") 和 to_datetime(errors="coerce") 统一处理异常数据,再一次性丢弃。
  • 库与框架使用

    • pandas:数据清洗与聚合。
    • python-dateutil:稳健解析日期字符串、处理时区。
    • pytest:结构化地验证逻辑正确性。
  • 工具使用与运行

    • 安装依赖:pip install -r requirements.txt
    • 运行脚本:
      • python data_pipeline.py --input ./orders.csv --out-dir ./out --start-date 2024-06-01 --end-date 2024-06-03 --top-n 5 --log-level INFO
    • 运行测试:
      • pytest -q
  • 跟新趋势与技术发展

    • 尝试使用 Polars 等新一代列式 DataFrame 库可进一步提升性能;但 pandas 生态最成熟,作为入门与生产仍是优选。
    • 在批量处理时可结合 Apache Arrow/Parquet 提升IO与计算效率;CI 中可缓存依赖与数据样例加速执行。

这套代码与说明,覆盖从基础概念到项目化落地、从调试到优化的完整学习路径,适合入门者按“案例驱动 + 项目导向”的方式快速上手并形成实践能力。

下面给出完整可用的 React 18 实时搜索组件实现(SearchBox.jsx、ResultsList.jsx)、自定义 Hook(useSearch.js)、样式(Search.module.css)、测试(Jest + @testing-library/react),以及使用示例(index.jsx)与简短 README。并在最后总结关键可访问性与性能优化点。

文件:src/hooks/useSearch.js

/**
 * useSearch - 即时搜索逻辑 Hook
 * - 300ms 防抖
 * - AbortController 取消前一个请求
 * - 指数退避重试(最多3次,初始300ms)
 * - 最近10条查询的 LRU 缓存(Map)
 * - 网络失败与空结果分别提示,正确处理取消
 *
 * @module useSearch
 */

import { useCallback, useEffect, useRef, useState } from 'react';

/**
 * @typedef {Object} SearchItem
 * @property {string} id
 * @property {string} title
 */

/**
 * @typedef {Object} UseSearchOptions
 * @property {string} [endpoint="/api/books?query="] 基础查询接口
 * @property {number} [debounce=300] 防抖毫秒
 * @property {number} [maxRetries=3] 最大重试次数
 * @property {number} [retryBaseDelay=300] 初始重试等待毫秒(指数退避基数)
 * @property {number} [cacheSize=10] LRU 缓存容量
 */

/**
 * @returns {{
 *  query: string,
 *  setQuery: (q:string)=>void,
 *  results: SearchItem[],
 *  loading: boolean,
 *  error: string | null,
 *  status: 'idle' | 'loading' | 'success' | 'empty' | 'error' | 'cancelled',
 *  selectedIndex: number,
 *  setSelectedIndex: (n:number)=>void,
 *  onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>)=>void,
 *  clear: ()=>void,
 *  hasResults: boolean,
 * }}
 */
export function useSearch({
  endpoint = '/api/books?query=',
  debounce = 300,
  maxRetries = 3,
  retryBaseDelay = 300,
  cacheSize = 10,
} = {}) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState(/** @type {SearchItem[]} */ ([]));
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(/** @type {string|null} */ (null));
  const [status, setStatus] = useState('idle');
  const [selectedIndex, setSelectedIndex] = useState(-1);

  const controllerRef = useRef(/** @type {AbortController|null} */ (null));
  const debounceTimerRef = useRef(/** @type {ReturnType<typeof setTimeout> | null} */ (null));
  const currentQueryRef = useRef('');
  const cacheRef = useRef(new Map()); // LRU: recent -> end

  // 清理与取消当前请求
  const abortCurrent = useCallback(() => {
    if (controllerRef.current) {
      controllerRef.current.abort();
      controllerRef.current = null;
    }
  }, []);

  // LRU 操作
  const touchCacheKey = useCallback((key) => {
    const cache = cacheRef.current;
    if (!cache.has(key)) return;
    const val = cache.get(key);
    cache.delete(key);
    cache.set(key, val);
  }, []);

  const setCache = useCallback((key, value) => {
    const cache = cacheRef.current;
    if (cache.has(key)) cache.delete(key);
    cache.set(key, value);
    if (cache.size > cacheSize) {
      const oldestKey = cache.keys().next().value;
      cache.delete(oldestKey);
    }
  }, [cacheSize]);

  const clear = useCallback(() => {
    abortCurrent();
    if (debounceTimerRef.current) {
      clearTimeout(debounceTimerRef.current);
      debounceTimerRef.current = null;
    }
    setQuery('');
    setResults([]);
    setSelectedIndex(-1);
    setError(null);
    setStatus('idle');
  }, [abortCurrent]);

  // 退避等待,支持在等待中被 abort
  const waitWithAbort = (ms, signal) =>
    new Promise((resolve, reject) => {
      const t = setTimeout(resolve, ms);
      const onAbort = () => {
        clearTimeout(t);
        reject(new DOMException('Aborted', 'AbortError'));
      };
      if (signal) {
        if (signal.aborted) onAbort();
        else signal.addEventListener('abort', onAbort, { once: true });
      }
    });

  const isRetryableStatus = (status) =>
    status === 429 || (status >= 500 && status < 600);

  const fetchWithRetry = useCallback(async (q, signal) => {
    let attempt = 0;
    let delay = retryBaseDelay;
    const url = `${endpoint}${encodeURIComponent(q)}`;

    // eslint-disable-next-line no-constant-condition
    while (true) {
      try {
        const res = await fetch(url, { signal });
        if (!res.ok) {
          if (isRetryableStatus(res.status) && attempt < maxRetries - 1) {
            attempt += 1;
            await waitWithAbort(delay, signal);
            delay *= 2;
            continue;
          }
          const text = await res.text().catch(() => '');
          throw new Error(`HTTP ${res.status}${text ? `: ${text}` : ''}`);
        }
        const json = await res.json();
        const items = Array.isArray(json?.items) ? json.items : [];
        return items;
      } catch (err) {
        if (err?.name === 'AbortError') throw err;
        // 网络异常重试
        if (attempt < maxRetries - 1) {
          attempt += 1;
          await waitWithAbort(delay, signal);
          delay *= 2;
          continue;
        }
        throw err;
      }
    }
  }, [endpoint, maxRetries, retryBaseDelay]);

  // 处理键盘导航(上下选择+回车选择由组件层决定)
  const onKeyDown = useCallback((e) => {
    if (!results || results.length === 0) return;
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      setSelectedIndex((prev) => (prev + 1) % results.length);
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      setSelectedIndex((prev) => (prev - 1 + results.length) % results.length);
    }
  }, [results]);

  // 主 effect:响应 query 变化
  useEffect(() => {
    const q = query.trim();
    // 每次输入变化,先取消上一个请求
    abortCurrent();

    if (debounceTimerRef.current) {
      clearTimeout(debounceTimerRef.current);
      debounceTimerRef.current = null;
    }

    if (!q) {
      setResults([]);
      setLoading(false);
      setError(null);
      setStatus('idle');
      setSelectedIndex(-1);
      return;
    }

    // 缓存命中:不发请求,立即返回
    if (cacheRef.current.has(q)) {
      touchCacheKey(q);
      const cached = cacheRef.current.get(q);
      setResults(cached);
      setLoading(false);
      setError(null);
      setStatus(cached.length ? 'success' : 'empty');
      setSelectedIndex(cached.length ? 0 : -1);
      return;
    }

    setLoading(true);
    setError(null);
    setStatus('loading');
    setSelectedIndex(-1);

    // 防抖后发起请求
    debounceTimerRef.current = setTimeout(async () => {
      currentQueryRef.current = q;
      const controller = new AbortController();
      controllerRef.current = controller;

      try {
        const items = await fetchWithRetry(q, controller.signal);
        // 仅处理最新查询结果
        if (currentQueryRef.current !== q) return;
        setCache(q, items);
        setResults(items);
        setLoading(false);
        setError(null);
        setStatus(items.length ? 'success' : 'empty');
        setSelectedIndex(items.length ? 0 : -1);
      } catch (err) {
        if (err?.name === 'AbortError') {
          setLoading(false);
          setStatus('cancelled');
          return;
        }
        setLoading(false);
        setError(err?.message || '网络错误');
        setStatus('error');
      } finally {
        controllerRef.current = null;
      }
    }, debounce);

    return () => {
      if (debounceTimerRef.current) {
        clearTimeout(debounceTimerRef.current);
        debounceTimerRef.current = null;
      }
    };
  }, [query, abortCurrent, debounce, fetchWithRetry, setCache, touchCacheKey]);

  useEffect(() => {
    // 卸载清理
    return () => {
      abortCurrent();
      if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
    };
  }, [abortCurrent]);

  const hasResults = results && results.length > 0;

  return {
    query,
    setQuery,
    results,
    loading,
    error,
    status,
    selectedIndex,
    setSelectedIndex,
    onKeyDown,
    clear,
    hasResults,
  };
}

文件:src/components/ResultsList.jsx

/**
 * ResultsList - 结果列表(无状态展示组件)
 * 无障碍:
 * - role="listbox"
 * - item role="option" + aria-selected
 *
 * @module ResultsList
 */
import React from 'react';
import styles from './Search.module.css';

/**
 * @param {{
 *  id: string,
 *  results: {id:string, title:string}[],
 *  selectedIndex: number,
 *  onItemClick: (item:any)=>void,
 *  onItemHover: (index:number)=>void,
 *  visible: boolean
 * }} props
 */
export default function ResultsList({
  id,
  results,
  selectedIndex,
  onItemClick,
  onItemHover,
  visible,
}) {
  if (!visible) return null;

  return (
    <ul
      id={id}
      className={styles.listbox}
      role="listbox"
      aria-label="搜索结果"
    >
      {results.map((item, idx) => {
        const optionId = `${id}-opt-${item.id ?? idx}`;
        const selected = idx === selectedIndex;
        return (
          <li
            key={item.id ?? idx}
            id={optionId}
            role="option"
            aria-selected={selected}
            className={`${styles.option} ${selected ? styles.optionActive : ''}`}
            onMouseEnter={() => onItemHover(idx)}
            onMouseDown={(e) => e.preventDefault()} // 防止失焦
            onClick={() => onItemClick(item, idx)}
            title={item.title}
          >
            {item.title}
          </li>
        );
      })}
    </ul>
  );
}

文件:src/components/SearchBox.jsx

/**
 * SearchBox - 可直接使用的即时搜索组件
 * - 集成 useSearch 数据逻辑
 * - 无障碍与键盘导航
 */
import React, { useId, useMemo } from 'react';
import { useSearch } from '../hooks/useSearch';
import ResultsList from './ResultsList';
import styles from './Search.module.css';

/**
 * @param {{
 *  endpoint?: string,
 *  ariaLabel?: string,
 *  onSelect?: (item: {id:string,title:string})=>void
 * }} props
 */
export default function SearchBox({
  endpoint = '/api/books?query=',
  ariaLabel = '搜索图书',
  onSelect,
}) {
  const listId = useId();
  const {
    query, setQuery,
    results, loading, error, status,
    selectedIndex, setSelectedIndex,
    onKeyDown, clear, hasResults,
  } = useSearch({ endpoint });

  const showList = useMemo(() => {
    const hasQuery = query.trim().length > 0;
    return hasQuery && (loading || hasResults || !!error || status === 'empty');
  }, [query, loading, hasResults, error, status]);

  const activeDescendant = showList && selectedIndex >= 0 && results[selectedIndex]
    ? `${listId}-opt-${results[selectedIndex].id ?? selectedIndex}`
    : undefined;

  const handleEnter = () => {
    if (selectedIndex >= 0 && results[selectedIndex]) {
      const item = results[selectedIndex];
      onSelect?.(item);
      setQuery(item.title); // 将选中项回填输入框
    }
  };

  return (
    <div className={styles.wrapper}>
      <div className={styles.inputRow}>
        <input
          type="search"
          className={styles.input}
          value={query}
          placeholder="搜索书籍(即时搜索)"
          onChange={(e) => setQuery(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === 'Enter') {
              e.preventDefault();
              handleEnter();
              return;
            }
            if (e.key === 'Escape') {
              e.preventDefault();
              clear();
              return;
            }
            onKeyDown(e);
          }}
          aria-label={ariaLabel}
          aria-controls={listId}
          aria-expanded={showList}
          aria-activedescendant={activeDescendant}
          aria-autocomplete="list"
          autoComplete="off"
        />
        {query && (
          <button
            type="button"
            className={styles.clearBtn}
            onClick={clear}
            aria-label="清除搜索"
            title="清除搜索"
          >
            ×
          </button>
        )}
      </div>

      <div className={styles.statusRow} aria-live="polite" role="status">
        {loading && <span className={styles.loading}>正在加载…</span>}
        {!loading && status === 'empty' && (
          <span className={styles.hint}>未找到匹配结果</span>
        )}
        {!loading && error && (
          <span className={styles.error}>请求失败:{error}</span>
        )}
      </div>

      <ResultsList
        id={listId}
        results={results}
        selectedIndex={selectedIndex}
        onItemHover={(idx) => setSelectedIndex(idx)}
        onItemClick={(item, idx) => {
          onSelect?.(item);
          setSelectedIndex(idx);
          setQuery(item.title);
        }}
        visible={showList && hasResults}
      />
    </div>
  );
}

文件:src/components/Search.module.css

.wrapper {
  position: relative;
  max-width: 420px;
  margin: 1rem auto;
  font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
}

.inputRow {
  position: relative;
  display: flex;
  align-items: center;
}

.input {
  flex: 1;
  padding: 0.5rem 2rem 0.5rem 0.75rem;
  font-size: 16px;
  border: 1px solid #d0d7de;
  border-radius: 6px;
  outline: none;
}

.input:focus {
  border-color: #0969da;
  box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.2);
}

.clearBtn {
  position: absolute;
  right: 6px;
  background: #f3f4f6;
  border: 1px solid #d0d7de;
  border-radius: 4px;
  width: 26px;
  height: 26px;
  cursor: pointer;
  font-size: 18px;
  line-height: 1;
}

.clearBtn:hover {
  background: #e5e7eb;
}

.statusRow {
  min-height: 1.25rem;
  margin-top: 0.25rem;
  font-size: 12px;
  color: #6b7280;
}

.loading {
  color: #2563eb;
}

.error {
  color: #b91c1c;
}

.hint {
  color: #6b7280;
}

.listbox {
  position: absolute;
  z-index: 10;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  max-height: 288px;
  overflow: auto;
  background: #fff;
  border: 1px solid #d0d7de;
  border-radius: 6px;
  padding: 4px 0;
  box-shadow: 0 6px 18px rgba(0,0,0,0.08);
}

.option {
  padding: 8px 10px;
  font-size: 14px;
  cursor: pointer;
}

.option:hover,
.optionActive {
  background: #eff6ff;
}

@media (prefers-reduced-motion: reduce) {
  * {
    scroll-behavior: auto !important;
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

文件:src/index.jsx(演示用法)

import React from 'react';
import { createRoot } from 'react-dom/client';
import SearchBox from './components/SearchBox.jsx';

function App() {
  return (
    <div style={{ padding: 24 }}>
      <h1>资料库搜索</h1>
      <p>试试输入:js、react、node ...</p>
      <SearchBox
        endpoint="/api/books?query="
        onSelect={(item) => {
          // 选中后自定义行为
          alert(`选中:${item.title} (id: ${item.id})`);
        }}
      />
    </div>
  );
}

const root = createRoot(document.getElementById('root'));
root.render(<App />);

测试:src/tests/Search.test.jsx

/**
 * 测试重点:
 * a) 防抖与请求取消
 * b) 键盘导航与选中
 * c) 缓存命中不发请求
 * d) 错误重试逻辑(mock fetch)
 */
import React from 'react';
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SearchBox from '../components/SearchBox.jsx';

// CSS Modules stub
jest.mock('../components/Search.module.css', () => new Proxy({}, {
  get: (target, prop) => prop,
}));

beforeEach(() => {
  jest.useFakeTimers();
  global.fetch = jest.fn();
});

afterEach(() => {
  jest.runOnlyPendingTimers();
  jest.useRealTimers();
  jest.resetAllMocks();
});

const flushMicrotasks = () => Promise.resolve();

test('a) 防抖:连续输入只触发一次请求;取消:新查询应中止上一次', async () => {
  const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
  const signals = [];
  // 第一次请求保持悬而未决,便于检测 abort
  let resolve1;
  const p1 = new Promise((r) => { resolve1 = r; });
  // 第二次请求快速返回
  const p2 = Promise.resolve({
    ok: true,
    json: async () => ({ items: [{ id: '2', title: 'React 指南' }] }),
  });

  global.fetch.mockImplementation((url, opts = {}) => {
    signals.push(opts.signal);
    if (signals.length === 1) return p1;
    return p2;
  });

  render(<SearchBox endpoint="/api/books?query=" />);

  const input = screen.getByRole('textbox', { name: /搜索图书/i });

  // 连续输入 - 只触发一次请求(防抖 300ms)
  await user.type(input, 're');
  jest.advanceTimersByTime(200);
  await user.type(input, 'a');
  // 还未到300ms,不应触发
  expect(global.fetch).toHaveBeenCalledTimes(0);

  // 超过防抖时间,触发第1次请求
  jest.advanceTimersByTime(300);
  await flushMicrotasks();
  expect(global.fetch).toHaveBeenCalledTimes(1);
  expect(global.fetch).toHaveBeenLastCalledWith('/api/books?query=rea', expect.any(Object));

  // 输入新查询,需取消上一次请求
  await user.type(input, 'c');
  jest.advanceTimersByTime(300);
  await flushMicrotasks();

  // 触发第2次请求,并检查第1个 signal 已被中止
  expect(global.fetch).toHaveBeenCalledTimes(2);
  expect(signals[0].aborted).toBe(true);

  // 返回第二次结果
  await flushMicrotasks();
  const listbox = await screen.findByRole('listbox', { name: /搜索结果/ });
  expect(within(listbox).getByText('React 指南')).toBeInTheDocument();

  // 释放第一个挂起请求以避免泄漏
  resolve1({
    ok: true,
    json: async () => ({ items: [] }),
  });
});

test('b) 键盘导航与选中(ArrowDown + Enter 回填输入框)', async () => {
  const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
  global.fetch.mockResolvedValue({
    ok: true,
    json: async () => ({ items: [
      { id: '1', title: 'JavaScript 基础' },
      { id: '2', title: 'React 指南' },
    ]}),
  });

  render(<SearchBox endpoint="/api/books?query=" />);
  const input = screen.getByRole('textbox', { name: /搜索图书/i });

  await user.type(input, 'js');
  jest.advanceTimersByTime(300);

  const listbox = await screen.findByRole('listbox');
  expect(within(listbox).getByText('JavaScript 基础')).toBeInTheDocument();

  await user.keyboard('{ArrowDown}{ArrowDown}{Enter}');
  expect(input).toHaveValue('React 指南');
});

test('c) 缓存命中不发请求', async () => {
  const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });

  global.fetch.mockResolvedValue({
    ok: true,
    json: async () => ({ items: [{ id: '2', title: 'React 指南' }] }),
  });

  render(<SearchBox endpoint="/api/books?query=" />);
  const input = screen.getByRole('textbox', { name: /搜索图书/i });

  // 第一次查询 - 触发网络请求
  await user.type(input, 'react');
  jest.advanceTimersByTime(300);
  await Promise.resolve();

  expect(global.fetch).toHaveBeenCalledTimes(1);

  // 清除再输入同样的查询 - 应命中缓存,不再发请求
  await user.clear(input);
  await user.type(input, 'react');
  // 由于缓存命中,防抖计时结束后也不应再 fetch
  jest.advanceTimersByTime(350);
  await Promise.resolve();
  expect(global.fetch).toHaveBeenCalledTimes(1);
});

test('d) 错误重试逻辑(指数退避:300ms, 600ms;第3次成功)', async () => {
  const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });

  // 第1/2次失败,第三次成功
  const seq = [
    Promise.resolve({ ok: false, status: 500, text: async () => 'Server err' }),
    Promise.resolve({ ok: false, status: 500, text: async () => 'Server err' }),
    Promise.resolve({ ok: true, json: async () => ({ items: [{ id: '9', title: '重试成功' }] }) })
  ];
  let call = 0;
  global.fetch.mockImplementation(() => seq[call++]);

  render(<SearchBox endpoint="/api/books?query=" />);

  const input = screen.getByRole('textbox', { name: /搜索图书/i });
  await user.type(input, 'retry');
  jest.advanceTimersByTime(300); // 触发初次请求

  // 第一次返回失败 -> 等待300ms
  await Promise.resolve();
  jest.advanceTimersByTime(300);
  await Promise.resolve();

  // 第二次返回失败 -> 等待600ms
  jest.advanceTimersByTime(600);
  await Promise.resolve();

  // 第三次成功
  const listbox = await screen.findByRole('listbox');
  expect(within(listbox).getByText('重试成功')).toBeInTheDocument();
  expect(global.fetch).toHaveBeenCalledTimes(3);
});

API 返回 JSON 示例

{
  "items": [
    { "id": "1", "title": "JavaScript 基础" },
    { "id": "2", "title": "React 指南" }
  ]
}

简短 README(README.md)

# React 18 即时搜索组件(可访问 & 高性能)

功能
- 300ms 防抖、AbortController 取消上一个请求
- 指数退避重试(最多3次,初始300ms)
- 最近 10 条查询 LRU 缓存(Map)
- 键盘导航(↑/↓/Enter),无障碍 role=listbox/option、aria-* 属性
- 空查询隐藏结果区、清除按钮、加载/错误/空结果提示

快速开始(Vite)
1) 创建项目
   npm create vite@latest my-app -- --template react
   cd my-app
   npm i
   npm i -D @testing-library/react @testing-library/jest-dom @testing-library/user-event jest jest-environment-jsdom

2) 复制以下文件到 src:
   - hooks/useSearch.js
   - components/SearchBox.jsx
   - components/ResultsList.jsx
   - components/Search.module.css
   - index.jsx
   - __tests__/Search.test.jsx

3) 替换 main.jsx 为 index.jsx 或在 index.html 中挂载 root,即可运行:
   npm run dev

4) 测试配置(示例 package.json)
   {
     "type": "module",
     "scripts": {
       "test": "jest"
     },
     "jest": {
       "testEnvironment": "jsdom",
       "transform": {},
       "moduleNameMapper": {
         "\\.(css|less|scss|sass)$": "<rootDir>/styleMock.js"
       },
       "setupFilesAfterEnv": ["@testing-library/jest-dom"]
     }
   }
   新建 styleMock.js:
     module.exports = new Proxy({}, { get: (t, p) => p });

5) 运行测试
   npm test

API 说明
- 组件通过 GET /api/books?query=<q> 请求数据,返回 { items: [{id, title}] }
- 注意区分空数组(空结果)与网络/HTTP 错误(会显示错误信息并进行退避重试)

关键可访问性与性能优化点

  • 可访问性
    • 输入框:aria-label、aria-controls 关联结果列表,aria-activedescendant 指向当前高亮项,aria-expanded 表示面板展开。
    • 列表/项:列表 role="listbox",项 role="option" 且 aria-selected 标注高亮项。
    • 状态信息:role="status" + aria-live="polite" 公告“加载中/错误/空结果”,不打断读屏。
    • 全键盘操作:ArrowUp/Down 循环选择,Enter 选中,Escape 清空。
  • 性能
    • 300ms 防抖避免过于频繁的网络请求。
    • AbortController 在连续输入时取消过期请求,减少资源浪费与竞态。
    • Map 实现 LRU(容量10),缓存命中即时返回并跳过网络请求。
    • 指数退避重试(300ms、600ms、1200ms)平衡及时性与服务压力。
    • 只在必要时渲染列表(查询为空隐藏、无重排样式简洁)。
    • 避免多余状态更新:缓存命中时不进入 loading;选择项时仅更新必要状态。

教学要点(简短)

  • 基础概念:副作用管理(useEffect)与取消(AbortController)、可访问性语义(ARIA)、请求幂等与重试策略。
  • 函数与方法:防抖(setTimeout/clearTimeout)与可取消等待(waitWithAbort)、LRU 操作(Map 的插入顺序)。
  • 库与框架:React 18 函数组件 + 自定义 Hook 解耦 UI/数据逻辑,Testing Library 编写以用户行为为中心的测试。
  • 项目实践:将异步流(输入->防抖->取消->重试->缓存)拆解为小能力并通过 Hook 组合。
  • 调试技巧:使用 Fake Timers 精准控制防抖/退避;mock fetch 捕捉 signal.aborted 判定是否正确取消。
  • 代码优化:减少不必要的 setState;缓存优先;正确区分“取消/空结果/错误”三种状态以避免误报。
  • 工具使用:Vite 开发、Jest/RTL 测试、CSS Modules 隔离样式。

如需扩展:可加入结果高亮关键词、虚拟化长列表、SSR 预取、以及 ARIA combobox 模式等。

下面给出一个基于 Gradle 的 Java 17 控制台小项目(包名:app.taskmgr),围绕“任务管理器”场景,完整展示面向对象设计(不可变实体+构建器、策略模式、泛型+Comparator、分层架构)、线程安全的内存仓储、Javadoc、日志、受检异常、以及 JUnit 5 单元测试。示例 Main 会创建项目与任务,演示三种排序与状态变更。

一、项目结构树

  • build.gradle
  • settings.gradle
  • src
    • main
      • java
        • app
          • taskmgr
            • AppMain.java
            • exception
              • InvalidTaskOperationException.java
            • model
              • Priority.java
              • Status.java
              • Project.java
              • User.java
              • Task.java
              • TaskBuilder.java
            • repository
              • Repository.java
              • InMemoryTaskRepository.java
              • InMemoryProjectRepository.java
              • InMemoryUserRepository.java
            • service
              • TaskService.java
              • sort
                • Sorter.java
                • TaskSortStrategy.java
                • ByPriorityStrategy.java
                • ByDueDateStrategy.java
                • ByTitleStrategy.java
    • test
      • java
        • app
          • taskmgr
            • TaskBuilderTest.java
            • TaskServiceTest.java
            • SortingStrategyTest.java
            • RepositoryConcurrencyTest.java

二、Gradle 配置 build.gradle

plugins {
    id 'java'
    id 'application'
}

group = 'app.taskmgr'
version = '1.0.0'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}

test {
    useJUnitPlatform()
}

application {
    mainClass = 'app.taskmgr.AppMain'
}

settings.gradle

rootProject.name = 'taskmgr'

三、源代码

src/main/java/app/taskmgr/model/Priority.java

package app.taskmgr.model;

/**
 * Task priority. Natural order: HIGH < MEDIUM < LOW (so ascending = high-first).
 */
public enum Priority {
    HIGH, MEDIUM, LOW
}

src/main/java/app/taskmgr/model/Status.java

package app.taskmgr.model;

/** Task status. */
public enum Status {
    OPEN, CLOSED
}

src/main/java/app/taskmgr/model/Project.java

package app.taskmgr.model;

import java.util.Objects;

/**
 * Project entity (immutable).
 */
public final class Project {
    private final long id;
    private final String name;

    public Project(long id, String name) {
        this.id = id;
        this.name = Objects.requireNonNull(name, "name");
    }

    public long getId() { return id; }
    public String getName() { return name; }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Project)) return false;
        Project that = (Project) o;
        return id == that.id;
    }

    @Override public int hashCode() { return Long.hashCode(id); }

    @Override public String toString() {
        return "Project{id=" + id + ", name='" + name + "'}";
    }
}

src/main/java/app/taskmgr/model/User.java

package app.taskmgr.model;

import java.util.Objects;

/**
 * User entity (immutable).
 */
public final class User {
    private final long id;
    private final String name;

    public User(long id, String name) {
        this.id = id;
        this.name = Objects.requireNonNull(name, "name");
    }

    public long getId() { return id; }
    public String getName() { return name; }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User that = (User) o;
        return id == that.id;
    }

    @Override public int hashCode() { return Long.hashCode(id); }

    @Override public String toString() {
        return "User{id=" + id + ", name='" + name + "'}";
    }
}

src/main/java/app/taskmgr/model/Task.java

package app.taskmgr.model;

import java.time.LocalDate;
import java.util.Objects;

/**
 * Task entity: immutable; modifications produce a new instance via builder.
 * Key immutable field: id.
 */
public final class Task {
    private final long id; // 0 means unsaved/new
    private final String title;
    private final String description;
    private final Priority priority;
    private final LocalDate dueDate;
    private final Status status;

    Task(long id, String title, String description, Priority priority, LocalDate dueDate, Status status) {
        this.id = id;
        this.title = Objects.requireNonNull(title, "title");
        this.description = description;
        this.priority = Objects.requireNonNull(priority, "priority");
        this.dueDate = dueDate;
        this.status = Objects.requireNonNull(status, "status");
    }

    public long getId() { return id; }
    public String getTitle() { return title; }
    public String getDescription() { return description; }
    public Priority getPriority() { return priority; }
    public LocalDate getDueDate() { return dueDate; }
    public Status getStatus() { return status; }

    /** Create a builder pre-filled from this task. */
    public TaskBuilder toBuilder() {
        return new TaskBuilder()
                .id(this.id)
                .title(this.title)
                .description(this.description)
                .priority(this.priority)
                .dueDate(this.dueDate)
                .status(this.status);
    }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Task)) return false;
        Task that = (Task) o;
        return id > 0 && id == that.id; // Persisted identity
    }

    @Override public int hashCode() {
        return id > 0 ? Long.hashCode(id) : Objects.hash(title, priority, dueDate, status);
    }

    @Override public String toString() {
        return "Task{id=" + id +
                ", title='" + title + '\'' +
                ", priority=" + priority +
                ", dueDate=" + dueDate +
                ", status=" + status +
                '}';
    }
}

src/main/java/app/taskmgr/model/TaskBuilder.java

package app.taskmgr.model;

import java.time.LocalDate;

/**
 * Builder for Task. Provides sensible defaults and validation in build().
 */
public final class TaskBuilder {
    private long id = 0L;
    private String title;
    private String description = "";
    private Priority priority = Priority.MEDIUM;
    private LocalDate dueDate;
    private Status status = Status.OPEN;

    public TaskBuilder id(long id) { this.id = id; return this; }
    public TaskBuilder title(String title) { this.title = title; return this; }
    public TaskBuilder description(String description) { this.description = description; return this; }
    public TaskBuilder priority(Priority priority) { this.priority = priority; return this; }
    public TaskBuilder dueDate(LocalDate dueDate) { this.dueDate = dueDate; return this; }
    public TaskBuilder status(Status status) { this.status = status; return this; }

    /**
     * Build immutable Task. Validates required fields.
     */
    public Task build() {
        if (title == null || title.isBlank()) throw new IllegalArgumentException("title is required");
        if (priority == null) throw new IllegalArgumentException("priority is required");
        if (status == null) status = Status.OPEN;
        if (description == null) description = "";
        return new Task(id, title, description, priority, dueDate, status);
    }
}

src/main/java/app/taskmgr/repository/Repository.java

package app.taskmgr.repository;

import java.util.List;
import java.util.Optional;

/**
 * Generic repository interface for simple CRUD-like operations.
 */
public interface Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    T save(T entity);            // insert or update
    boolean deleteById(ID id);
    long count();
}

src/main/java/app/taskmgr/repository/InMemoryTaskRepository.java

package app.taskmgr.repository;

import app.taskmgr.model.Task;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Thread-safe in-memory Task repository using ConcurrentHashMap.
 * Assigns IDs for new tasks (id <= 0).
 */
public class InMemoryTaskRepository implements Repository<Task, Long> {
    private final ConcurrentMap<Long, Task> store = new ConcurrentHashMap<>();
    private final AtomicLong seq = new AtomicLong(0L);

    @Override
    public Optional<Task> findById(Long id) {
        if (id == null) return Optional.empty();
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public List<Task> findAll() {
        return new ArrayList<>(store.values());
    }

    @Override
    public Task save(Task entity) {
        Task toSave = entity;
        if (entity.getId() <= 0) {
            long newId = seq.incrementAndGet();
            toSave = entity.toBuilder().id(newId).build();
        }
        store.put(toSave.getId(), toSave);
        return toSave;
    }

    @Override
    public boolean deleteById(Long id) {
        return store.remove(id) != null;
    }

    @Override
    public long count() {
        return store.size();
    }
}

src/main/java/app/taskmgr/repository/InMemoryProjectRepository.java

package app.taskmgr.repository;

import app.taskmgr.model.Project;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/** Simple thread-safe in-memory repository for Project. */
public class InMemoryProjectRepository implements Repository<Project, Long> {
    private final ConcurrentHashMap<Long, Project> store = new ConcurrentHashMap<>();
    private final AtomicLong seq = new AtomicLong(0L);

    @Override
    public Optional<Project> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public List<Project> findAll() {
        return new ArrayList<>(store.values());
    }

    @Override
    public Project save(Project entity) {
        Project proj = entity;
        if (entity.getId() <= 0) {
            long id = seq.incrementAndGet();
            proj = new Project(id, entity.getName());
        }
        store.put(proj.getId(), proj);
        return proj;
    }

    @Override
    public boolean deleteById(Long id) {
        return store.remove(id) != null;
    }

    @Override
    public long count() {
        return store.size();
    }
}

src/main/java/app/taskmgr/repository/InMemoryUserRepository.java

package app.taskmgr.repository;

import app.taskmgr.model.User;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/** Simple thread-safe in-memory repository for User. */
public class InMemoryUserRepository implements Repository<User, Long> {
    private final ConcurrentHashMap<Long, User> store = new ConcurrentHashMap<>();
    private final AtomicLong seq = new AtomicLong(0L);

    @Override
    public Optional<User> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public List<User> findAll() {
        return new ArrayList<>(store.values());
    }

    @Override
    public User save(User entity) {
        User u = entity;
        if (entity.getId() <= 0) {
            long id = seq.incrementAndGet();
            u = new User(id, entity.getName());
        }
        store.put(u.getId(), u);
        return u;
    }

    @Override
    public boolean deleteById(Long id) {
        return store.remove(id) != null;
    }

    @Override
    public long count() {
        return store.size();
    }
}

src/main/java/app/taskmgr/exception/InvalidTaskOperationException.java

package app.taskmgr.exception;

/**
 * Checked exception for invalid task operations (precondition violations, illegal state transitions).
 */
public class InvalidTaskOperationException extends Exception {
    public InvalidTaskOperationException(String message) {
        super(message);
    }
}

src/main/java/app/taskmgr/service/sort/TaskSortStrategy.java

package app.taskmgr.service.sort;

import app.taskmgr.model.Task;

import java.util.Comparator;

/**
 * Strategy for sorting tasks.
 */
public interface TaskSortStrategy {
    Comparator<Task> comparator();
    default String name() {
        return this.getClass().getSimpleName();
    }
}

src/main/java/app/taskmgr/service/sort/ByPriorityStrategy.java

package app.taskmgr.service.sort;

import app.taskmgr.model.Task;

import java.util.Comparator;

/**
 * Sort tasks by priority (HIGH first), then dueDate asc, then title asc.
 */
public class ByPriorityStrategy implements TaskSortStrategy {
    private static final Comparator<Task> CMP =
            Comparator.comparing(Task::getPriority) // enum: HIGH < MEDIUM < LOW
                      .thenComparing(Task::getDueDate, Comparator.nullsLast(Comparator.naturalOrder()))
                      .thenComparing(Task::getTitle, String.CASE_INSENSITIVE_ORDER);

    @Override
    public Comparator<Task> comparator() { return CMP; }
}

src/main/java/app/taskmgr/service/sort/ByDueDateStrategy.java

package app.taskmgr.service.sort;

import app.taskmgr.model.Task;

import java.util.Comparator;

/**
 * Sort by due date asc (nulls last), then priority, then title.
 */
public class ByDueDateStrategy implements TaskSortStrategy {
    private static final Comparator<Task> CMP =
            Comparator.comparing(Task::getDueDate, Comparator.nullsLast(Comparator.naturalOrder()))
                      .thenComparing(Task::getPriority)
                      .thenComparing(Task::getTitle, String.CASE_INSENSITIVE_ORDER);

    @Override
    public Comparator<Task> comparator() { return CMP; }
}

src/main/java/app/taskmgr/service/sort/ByTitleStrategy.java

package app.taskmgr.service.sort;

import app.taskmgr.model.Task;

import java.util.Comparator;

/**
 * Sort by title (case-insensitive), then due date.
 */
public class ByTitleStrategy implements TaskSortStrategy {
    private static final Comparator<Task> CMP =
            Comparator.comparing(Task::getTitle, String.CASE_INSENSITIVE_ORDER)
                      .thenComparing(Task::getDueDate, Comparator.nullsLast(Comparator.naturalOrder()));

    @Override
    public Comparator<Task> comparator() { return CMP; }
}

src/main/java/app/taskmgr/service/sort/Sorter.java

package app.taskmgr.service.sort;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/**
 * Generic reusable sorter using Comparator and generics.
 */
public final class Sorter<T> {
    public List<T> sort(List<T> list, Comparator<? super T> comparator) {
        var copy = new ArrayList<>(list);
        copy.sort(comparator);
        return copy;
        // original list remains unchanged (functional style)
    }
}

src/main/java/app/taskmgr/service/TaskService.java

package app.taskmgr.service;

import app.taskmgr.exception.InvalidTaskOperationException;
import app.taskmgr.model.*;
import app.taskmgr.repository.Repository;
import app.taskmgr.service.sort.Sorter;
import app.taskmgr.service.sort.TaskSortStrategy;

import java.time.LocalDate;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Application service for Tasks: creation, update, close, sorting, assign to project.
 * Uses repositories and enforces preconditions and state transitions.
 */
public class TaskService {
    private static final Logger log = Logger.getLogger(TaskService.class.getName());

    private final Repository<Task, Long> taskRepo;
    private final Repository<Project, Long> projectRepo;
    private final Repository<User, Long> userRepo;

    // Project to task IDs mapping (thread-safe)
    private final ConcurrentHashMap<Long, Set<Long>> projectTasks = new ConcurrentHashMap<>();
    private final Sorter<Task> sorter = new Sorter<>();

    public TaskService(Repository<Task, Long> taskRepo,
                       Repository<Project, Long> projectRepo,
                       Repository<User, Long> userRepo) {
        this.taskRepo = taskRepo;
        this.projectRepo = projectRepo;
        this.userRepo = userRepo;
    }

    /** Create a project. */
    public Project createProject(String name) throws InvalidTaskOperationException {
        if (name == null || name.isBlank()) throw new InvalidTaskOperationException("Project name required");
        Project p = new Project(0L, name);
        p = projectRepo.save(p);
        projectTasks.putIfAbsent(p.getId(), ConcurrentHashMap.newKeySet());
        log.info(() -> "Created project: " + p);
        return p;
    }

    /** Create a user. */
    public User createUser(String name) throws InvalidTaskOperationException {
        if (name == null || name.isBlank()) throw new InvalidTaskOperationException("User name required");
        User u = new User(0L, name);
        u = userRepo.save(u);
        log.info(() -> "Created user: " + u);
        return u;
    }

    /**
     * Create a task and assign to a project.
     * Preconditions: title not blank; status must be OPEN at creation; due date not in the past (if provided).
     */
    public Task createTask(TaskBuilder builder, long projectId, long userId) throws InvalidTaskOperationException {
        Objects.requireNonNull(builder, "builder");
        Project p = projectRepo.findById(projectId)
                .orElseThrow(() -> new InvalidTaskOperationException("Project not found: " + projectId));
        userRepo.findById(userId)
                .orElseThrow(() -> new InvalidTaskOperationException("User not found: " + userId));

        Task task = builder.build();
        if (task.getStatus() == Status.CLOSED) {
            throw new InvalidTaskOperationException("Cannot create task in CLOSED state");
        }
        if (task.getDueDate() != null && task.getDueDate().isBefore(LocalDate.now())) {
            throw new InvalidTaskOperationException("Due date cannot be in the past");
        }
        Task saved = taskRepo.save(task);

        projectTasks.computeIfAbsent(p.getId(), k -> ConcurrentHashMap.newKeySet())
                    .add(saved.getId());

        log.info(() -> "Created task " + saved + " in project " + p.getName());
        return saved;
    }

    /**
     * Update editable fields of a task (title, description, priority, due date).
     * Closed tasks cannot be updated.
     */
    public Task updateTask(long taskId, String newTitle, String newDescription,
                           Priority newPriority, LocalDate newDueDate) throws InvalidTaskOperationException {
        Task existing = taskRepo.findById(taskId)
                .orElseThrow(() -> new InvalidTaskOperationException("Task not found: " + taskId));

        if (existing.getStatus() == Status.CLOSED) {
            throw new InvalidTaskOperationException("Cannot update a CLOSED task");
        }
        if (newTitle == null || newTitle.isBlank()) {
            throw new InvalidTaskOperationException("Title cannot be blank");
        }
        if (newDueDate != null && newDueDate.isBefore(LocalDate.now())) {
            throw new InvalidTaskOperationException("Due date cannot be in the past");
        }
        if (newPriority == null) {
            throw new InvalidTaskOperationException("Priority required");
        }

        Task updated = existing.toBuilder()
                .title(newTitle)
                .description(newDescription == null ? "" : newDescription)
                .priority(newPriority)
                .dueDate(newDueDate)
                .build();

        updated = taskRepo.save(updated);
        log.info(() -> "Updated task " + updated);
        return updated;
    }

    /** Close a task (state transition OPEN -> CLOSED only). */
    public Task closeTask(long taskId) throws InvalidTaskOperationException {
        Task existing = taskRepo.findById(taskId)
                .orElseThrow(() -> new InvalidTaskOperationException("Task not found: " + taskId));

        if (existing.getStatus() == Status.CLOSED) {
            throw new InvalidTaskOperationException("Task already CLOSED");
        }
        Task closed = existing.toBuilder().status(Status.CLOSED).build();
        closed = taskRepo.save(closed);
        log.info(() -> "Closed task " + closed);
        return closed;
    }

    /** Get tasks of a project. */
    public List<Task> findTasksByProject(long projectId) throws InvalidTaskOperationException {
        if (projectRepo.findById(projectId).isEmpty())
            throw new InvalidTaskOperationException("Project not found: " + projectId);
        var ids = projectTasks.getOrDefault(projectId, Set.of());
        List<Task> result = new ArrayList<>();
        for (Long id : ids) {
            taskRepo.findById(id).ifPresent(result::add);
        }
        return result;
    }

    /** Sort tasks using a strategy. */
    public List<Task> sortTasks(List<Task> tasks, TaskSortStrategy strategy) {
        Objects.requireNonNull(strategy, "strategy");
        try {
            return sorter.sort(tasks, strategy.comparator());
        } catch (Exception e) {
            log.log(Level.WARNING, "Sorting failed", e);
            return List.copyOf(tasks);
        }
    }

    /** Get all tasks (for convenience). */
    public List<Task> findAllTasks() {
        return taskRepo.findAll();
    }
}

src/main/java/app/taskmgr/AppMain.java

package app.taskmgr;

import app.taskmgr.exception.InvalidTaskOperationException;
import app.taskmgr.model.*;
import app.taskmgr.repository.InMemoryProjectRepository;
import app.taskmgr.repository.InMemoryTaskRepository;
import app.taskmgr.repository.InMemoryUserRepository;
import app.taskmgr.service.TaskService;
import app.taskmgr.service.sort.ByDueDateStrategy;
import app.taskmgr.service.sort.ByPriorityStrategy;
import app.taskmgr.service.sort.ByTitleStrategy;

import java.time.LocalDate;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Console demo: create tasks, assign to project, sort by different strategies, change status.
 */
public class AppMain {
    private static final Logger log = Logger.getLogger(AppMain.class.getName());

    public static void main(String[] args) {
        // Basic logger config
        Logger.getLogger("").getHandlers()[0].setLevel(Level.INFO);

        var taskRepo = new InMemoryTaskRepository();
        var projectRepo = new InMemoryProjectRepository();
        var userRepo = new InMemoryUserRepository();

        var service = new TaskService(taskRepo, projectRepo, userRepo);

        try {
            Project demo = service.createProject("Team Tools");
            User alice = service.createUser("Alice");

            // Create at least 5 tasks with various priority and dates
            Task t1 = service.createTask(new TaskBuilder()
                    .title("Design CLI layout")
                    .description("Define command structure and help message")
                    .priority(Priority.HIGH)
                    .dueDate(LocalDate.now().plusDays(3)), demo.getId(), alice.getId());

            Task t2 = service.createTask(new TaskBuilder()
                    .title("Implement sorting")
                    .description("Priority, due date, title")
                    .priority(Priority.MEDIUM)
                    .dueDate(LocalDate.now().plusDays(5)), demo.getId(), alice.getId());

            Task t3 = service.createTask(new TaskBuilder()
                    .title("Write unit tests")
                    .description("Cover builder, service, strategies, concurrency")
                    .priority(Priority.HIGH)
                    .dueDate(LocalDate.now().plusDays(2)), demo.getId(), alice.getId());

            Task t4 = service.createTask(new TaskBuilder()
                    .title("Add logging")
                    .description("Use java.util.logging")
                    .priority(Priority.LOW)
                    .dueDate(LocalDate.now().plusDays(7)), demo.getId(), alice.getId());

            Task t5 = service.createTask(new TaskBuilder()
                    .title("Polish docs")
                    .description("README and Javadoc")
                    .priority(Priority.MEDIUM)
                    .dueDate(LocalDate.now().plusDays(4)), demo.getId(), alice.getId());

            log.info("All tasks (unsorted):");
            print(service.findTasksByProject(demo.getId()));

            log.info("Sorted by priority:");
            print(service.sortTasks(service.findTasksByProject(demo.getId()), new ByPriorityStrategy()));

            log.info("Sorted by due date:");
            print(service.sortTasks(service.findTasksByProject(demo.getId()), new ByDueDateStrategy()));

            log.info("Sorted by title:");
            print(service.sortTasks(service.findTasksByProject(demo.getId()), new ByTitleStrategy()));

            // Update and close
            Task updated = service.updateTask(t2.getId(), "Implement strategies", "Refactor into strategy classes",
                    Priority.HIGH, LocalDate.now().plusDays(1));
            log.info("Updated: " + updated);

            Task closed = service.closeTask(t1.getId());
            log.info("Closed: " + closed);

            log.info("After update/close, by priority:");
            print(service.sortTasks(service.findTasksByProject(demo.getId()), new ByPriorityStrategy()));

        } catch (InvalidTaskOperationException e) {
            log.log(Level.SEVERE, "Operation failed: " + e.getMessage(), e);
        }
    }

    private static void print(List<Task> tasks) {
        tasks.forEach(t -> System.out.println(" - " + t));
    }
}

四、测试代码

src/test/java/app/taskmgr/TaskBuilderTest.java

package app.taskmgr;

import app.taskmgr.model.*;
import org.junit.jupiter.api.Test;

import java.time.LocalDate;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Test builder and entity immutability behavior.
 */
public class TaskBuilderTest {

    @Test
    void buildTaskWithDefaults() {
        Task t = new TaskBuilder()
                .title("Sample")
                .priority(Priority.MEDIUM)
                .build();
        assertEquals("Sample", t.getTitle());
        assertEquals(Priority.MEDIUM, t.getPriority());
        assertEquals(Status.OPEN, t.getStatus());
        assertEquals(0L, t.getId());
    }

    @Test
    void immutabilityCopyOnChange() {
        Task t1 = new TaskBuilder()
                .title("A")
                .description("d")
                .priority(Priority.HIGH)
                .dueDate(LocalDate.now().plusDays(3))
                .build();

        Task t2 = t1.toBuilder().title("B").build();

        assertNotSame(t1, t2);
        assertEquals("A", t1.getTitle());
        assertEquals("B", t2.getTitle());
    }

    @Test
    void equalsHashCodeForPersistedId() {
        Task t1 = new TaskBuilder().id(1).title("X").priority(Priority.HIGH).build();
        Task t2 = new TaskBuilder().id(1).title("Y").priority(Priority.LOW).build();
        assertEquals(t1, t2);
        assertEquals(t1.hashCode(), t2.hashCode());
    }
}

src/test/java/app/taskmgr/TaskServiceTest.java

package app.taskmgr;

import app.taskmgr.exception.InvalidTaskOperationException;
import app.taskmgr.model.*;
import app.taskmgr.repository.InMemoryProjectRepository;
import app.taskmgr.repository.InMemoryTaskRepository;
import app.taskmgr.repository.InMemoryUserRepository;
import app.taskmgr.service.TaskService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.time.LocalDate;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Test service flows for create/update/close.
 */
public class TaskServiceTest {
    TaskService service;
    Project project;
    User user;

    @BeforeEach
    void setup() throws InvalidTaskOperationException {
        service = new TaskService(new InMemoryTaskRepository(),
                                  new InMemoryProjectRepository(),
                                  new InMemoryUserRepository());
        project = service.createProject("Demo");
        user = service.createUser("Tester");
    }

    @Test
    void createUpdateCloseFlow() throws Exception {
        Task created = service.createTask(new TaskBuilder()
                .title("T1")
                .priority(Priority.MEDIUM)
                .dueDate(LocalDate.now().plusDays(2)), project.getId(), user.getId());
        assertTrue(created.getId() > 0);
        assertEquals(Status.OPEN, created.getStatus());

        Task updated = service.updateTask(created.getId(), "T1-upd", "desc", Priority.HIGH, LocalDate.now().plusDays(3));
        assertEquals("T1-upd", updated.getTitle());
        assertEquals(Priority.HIGH, updated.getPriority());

        Task closed = service.closeTask(updated.getId());
        assertEquals(Status.CLOSED, closed.getStatus());

        assertThrows(InvalidTaskOperationException.class, () -> service.updateTask(closed.getId(),
                "again", "x", Priority.LOW, LocalDate.now().plusDays(1)));
        assertThrows(InvalidTaskOperationException.class, () -> service.closeTask(closed.getId()));
    }
}

src/test/java/app/taskmgr/SortingStrategyTest.java

package app.taskmgr;

import app.taskmgr.model.*;
import app.taskmgr.service.sort.ByDueDateStrategy;
import app.taskmgr.service.sort.ByPriorityStrategy;
import app.taskmgr.service.sort.ByTitleStrategy;
import app.taskmgr.service.sort.Sorter;
import org.junit.jupiter.api.Test;

import java.time.LocalDate;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Test that strategies produce expected ordering.
 */
public class SortingStrategyTest {

    private Task t(String title, Priority p, int duePlusDays) {
        return new TaskBuilder()
                .title(title)
                .priority(p)
                .dueDate(LocalDate.now().plusDays(duePlusDays))
                .build();
    }

    @Test
    void byPriority() {
        List<Task> src = List.of(
                t("b", Priority.MEDIUM, 3),
                t("a", Priority.HIGH, 5),
                t("c", Priority.LOW, 1)
        );
        var sorted = new Sorter<Task>().sort(src, new ByPriorityStrategy().comparator());
        assertEquals("a", sorted.get(0).getTitle()); // HIGH first
    }

    @Test
    void byDueDate() {
        List<Task> src = List.of(
                t("x", Priority.MEDIUM, 5),
                t("y", Priority.HIGH, 1),
                t("z", Priority.LOW, 3)
        );
        var sorted = new Sorter<Task>().sort(src, new ByDueDateStrategy().comparator());
        assertEquals("y", sorted.get(0).getTitle()); // earliest due date first
    }

    @Test
    void byTitle() {
        List<Task> src = List.of(
                t("Zoo", Priority.LOW, 1),
                t("alpha", Priority.HIGH, 2),
                t("Beta", Priority.MEDIUM, 3)
        );
        var sorted = new Sorter<Task>().sort(src, new ByTitleStrategy().comparator());
        assertEquals("alpha", sorted.get(0).getTitle());
    }
}

src/test/java/app/taskmgr/RepositoryConcurrencyTest.java

package app.taskmgr;

import app.taskmgr.model.Priority;
import app.taskmgr.model.Task;
import app.taskmgr.model.TaskBuilder;
import app.taskmgr.repository.InMemoryTaskRepository;
import org.junit.jupiter.api.Test;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Basic concurrent safety test: concurrent inserts produce correct count.
 */
public class RepositoryConcurrencyTest {

    @Test
    void concurrentInserts() throws InterruptedException {
        var repo = new InMemoryTaskRepository();
        int threads = 10;
        int perThread = 100;
        CountDownLatch latch = new CountDownLatch(threads);
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < threads; i++) {
            Thread t = new Thread(() -> {
                for (int j = 0; j < perThread; j++) {
                    Task task = new TaskBuilder()
                            .title("T-" + Thread.currentThread().getId() + "-" + j)
                            .priority(Priority.MEDIUM)
                            .dueDate(LocalDate.now().plusDays(1))
                            .build();
                    repo.save(task);
                }
                latch.countDown();
            });
            ts.add(t);
            t.start();
        }
        latch.await();
        assertEquals(threads * perThread, repo.count());
    }
}

五、运行与测试

  • 编译与运行
    • 构建:./gradlew clean build
    • 运行示例:./gradlew run
  • 运行测试:./gradlew test

六、关键设计取舍与教学要点

  • 面向对象与不可变性
    • Task/Project/User 设计为不可变(final 字段、无 setter),通过 TaskBuilder 与 toBuilder 进行受控变更,保证线程安全、易推理。
    • equals/hashCode 以持久化后的 id 为主(id>0 时),避免因字段变更导致集合行为异常。
  • 构建器模式
    • TaskBuilder 封装缺省值与必填校验,build() 负责参数合法性;服务层再做业务前置校验(如不能创建 CLOSED、截止日不得早于今天)。
  • 分层与职责
    • Repository 层:线程安全的 ConcurrentHashMap 实现,InMemoryTaskRepository 负责分配 ID。
    • Service 层:封装业务流程与状态机规则(OPEN->CLOSED),记录项目与任务的关系映射。
  • 策略模式 + Comparator + 泛型
    • 三个策略类分别实现 TaskSortStrategy,返回 Comparator;Sorter 使用泛型与 Comparator 实现复用、返回新列表不修改原数据。
  • Optional 使用
    • Repository 的 findById 返回 Optional,服务层统一处理缺失情况并抛出受检异常,减少空指针风险。
  • 异常与日志
    • 对非法状态抛出受检异常 InvalidTaskOperationException;使用 java.util.logging 记录关键操作。
  • 并发安全
    • 仓储使用 ConcurrentHashMap + AtomicLong;单元测试验证并发插入计数,体现线程安全基础。
  • 测试覆盖
    • 构建器与不可变性测试;服务流程(新增/更新/关闭);三种排序策略;仓储并发。
  • Java 语言特性与最佳实践
    • 使用 Java 17(records 可作为进一步演示选择,这里为教学清晰性采用显式类)。
    • Comparator 组合、Optional、函数式风格的不可变列表返回。
    • 日志与异常分离:日志用于运行信息,异常用于业务约束反馈。

七、学习建议(面向入门者)

  • 概念梳理
    • 从实体不可变性、构建器模式、策略模式入手,理解“封装变化点”的思路。
  • 案例驱动
    • 跟随 AppMain 通过增/改/关与排序快速理解业务流;尝试新增新的排序策略(如按状态+优先级)。
  • 项目导向
    • 添加命令行参数解析(如 picocli)扩展 CLI;引入持久化(H2/SQLite)替代内存仓储。
  • 问题解决导向
    • 尝试引入更多校验(截止日提醒、重复标题检测);模拟并发场景与更严格的单元/集成测试。
  • 工具使用
    • 熟练使用 Gradle 任务(build/test/run)、Javadoc 生成、IDE 调试断点;结合日志定位问题。
  • 跟进行业趋势
    • 关注 Java 版本演进(模式匹配、record、sealed classes)、JUnit 5 特性、现代静态分析(SpotBugs/Checkstyle)、CI/CD 集成(GitHub Actions)。

至此,项目可直接构建运行与测试,展示了面向对象设计、策略模式、线程安全仓储、受检异常、日志与单元测试的综合实践。

示例详情

解决的问题

提供一个高效、清晰的编程教学解决方案,帮助用户快速掌握编程概念、技术要点及最佳实践,同时提升学习效率和实践能力。

适用用户

编程初学者

帮助零基础学员快速入门,系统学习编程概念和基础技能,为职业发展或兴趣学习打下扎实基础。

在职程序员

为工作中的开发者提供即时技术支持,无论是掌握新语言还是解决具体问题,都能助力效率提升。

计算机科学教师

为教育工作者提供教学辅助工具,优化教案编写与讲解细节,提升学生学习参与度和效果。

特征总结

轻松讲解编程核心概念,从零基础到进阶清晰剖析,让任何学员快速上手编程。
提供专业化编程学习指导,帮助用户掌握不同语言的特性与常见实践,提升编程效率。
支持多语言知识覆盖,涵盖Python、JavaScript、Java、C++等主流编程语言,满足多样化学习需求。
输出最佳实践与工具推荐,从开发流程到资源使用,助力用户更高效地完成项目。
帮助用户培养编程逻辑与问题解决能力,通过实用案例讲解和应用展示,提升实战水平。
紧跟技术发展趋势,实时分享最新行业动态,为用户提供前沿知识储备。
适配个性化学习需求,任意领域问题都能得到简明、系统的解决方案。
一步到位优化教学内容,自动生成通俗易懂、逻辑清晰的学习资料或指导文档。
支持项目实战指导,帮助用户规划开发思路、调试技巧及优化策略,快速完成任务。
通过细化概念与模块化拆解,降低技术门槛,让复杂内容简单化,为学习者增强自信。

如何使用购买的提示词模板

1. 直接在外部 Chat 应用中使用

将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。

2. 发布为 API 接口调用

把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。

3. 在 MCP Client 中配置使用

在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。

AI 提示词价格
¥20.00元
先用后买,用好了再付款,超安全!

您购买后可以获得什么

获得完整提示词模板
- 共 161 tokens
- 6 个可调节参数
{ 编程语言 } { 教学目标 } { 示例请求内容 } { 实践场景说明 } { 学习者水平 } { 教学风格 }
获得社区贡献内容的使用权
- 精选社区优质案例,助您快速上手提示词
使用提示词兑换券,低至 ¥ 9.9
了解兑换券 →
限时半价

不要错过!

半价获取高级提示词-优惠即将到期

17
:
23
小时
:
59
分钟
:
59