福州大学 202601 软件工程第二次结对作业

这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering
这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/homework/16743
这个作业的目标 结对实现“校园失物招领小程序”
项目 GitHub 仓库 https://github.com/rt265/102401215-102401119
结对队伍 102401215 李义东;102401119 林祺皓
结对队伍成员博客 watermelonabc;LLLqh

需求背景与功能概览

校园里丢失物品后,信息通常散落在班群、年级群和社交平台中,搜索困难、有效期短,也不方便更新“已经找回”的状态。因此我们实现了一个本地闭环的校园失物招领应用,让用户可以完成:

发布失物/招领信息 → 浏览或搜索 → 查看详情 → 联系发布者 → 更新为已找到/已归还

主要功能如下:

  • 首页浏览:浏览失物和招领卡片,按类型、分类筛选,并按时间排序。
  • 关键词搜索:按物品名称、地点、描述和分类搜索;搜索条件按空白拆分,多个词需要全部命中。
  • 详情查看:查看地点、发生时间、描述、联系方式和图片,图片支持轮播。
  • 发布信息:校验必填项,支持从相册选图或拍照,最多保存 9 张图片。
  • 我的:管理当前本机账户发布的信息,编辑、删除、标记“已找到”或“已归还”。
  • 设置:切换跟随系统、浅色或深色模式,自定义 Material 3 主题色,管理账户并查看关于页面。
  • 本地持久化:信息、账户、设置、搜索记录保存在本机 SQLite;图片保存在应用私有目录。

本项目暂不实现远程服务器、实名认证、即时聊天和地图定位。它首先解决的是单机环境下的完整交互闭环。

结对分工

成员 主要工作
李义东 Flutter 项目基础搭建;首页、发布页、详情页;ItemPost、PostQuery 和 PostStore;表单校验;部分 widget 测试
林祺皓 “我的”和设置页面;SQLite 数据层与数据库迁移;图片文件存储与选图抽象;搜索历史、多用户隔离;对应测试和构建配置
共同完成 需求讨论、UI 走查、问题定位、代码审查、测试修复、README 与开发文档整理

涉及数据模型、页面接口和测试脚手架的改动会先沟通,再由一人实现、另一人验证。

PSP 表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 60 75
Estimate 估计任务规模 45 60
Development 开发 — —
Analysis 需求分析 300 360
Design Spec 生成设计文档 120 150
Design Review 设计复审 60 90
Coding Standard 代码规范 45 45
Design 具体设计 180 240
Coding 具体编码 1500 1800
Code Review 代码复审 180 210
Test 测试 450 540
Reporting 报告 — —
Test Report 测试报告 120 150
Size Measurement 计算工作量 30 30
Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 150 180
合计 具体子阶段合计 3240 3930

解题思路与设计实现

从用户流程拆分页面

我们没有先从数据库表开始,而是先把用户任务拆成页面:

首页
├── 搜索页
└── 详情页

发布页
└── 发布成功提示

我的
├── 我的发布 → 详情页
├── 编辑页
└── 设置
    ├── 外观
    ├── 账户
    └── 关于

三大主界面由 MainShell 统一管理。窄屏使用 NavigationBar,宽屏使用 NavigationRail;三个页面放在 IndexedStack 中,切换标签时保留输入内容和滚动位置。

分层架构

项目采用“页面/组件—Store—Repository—SQLite/文件系统”的结构:

flowchart TD UI[pages 与 widgets] --> Store[ChangeNotifier Store] Store --> Repo[Repository 接口] Repo --> SQLite[(SQLite)] Store --> Photo[PhotoStore] Photo --> Files[(应用私有图片目录)] UI --> Query[PostQuery] Query --> Store
  • models/ 保存 ItemPost、UserAccount、PostQuery 等领域对象。
  • pages/ 负责页面状态和导航。
  • widgets/ 放置发布表单、卡片、筛选栏和图片显示等可复用控件。
  • data/*Store 保存页面需要的内存快照并通过 ChangeNotifier 通知 UI。
  • data/*Repository 隔离 SQLite 细节,使页面不直接拼 SQL。
  • services/photo_picker.dart 抽象平台选图能力,测试时可以替换成假的 picker。

信息读写流程

应用启动时先打开数据库,再把数据库仓储注入 Store 并加载数据。页面始终同步读取 Store,这样筛选和排序不必每次等待异步查询。

sequenceDiagram participant App as main() participant DB as AppDatabase participant R as Repository participant S as PostStore participant P as Page App->>DB: open() DB->>DB: 建表/执行迁移/首次写入示例数据 App->>R: 创建 SQLite Repository App->>S: 注入 Repository S->>R: load() R-->>S: ItemPost 列表 S-->>P: notifyListeners() P->>S: 同步读取并筛选

写操作采用“先更新内存、再持久化”的写穿策略:

  1. PostStore.addPost/updatePost/removePost 先更新内存并通知界面。
  2. 页面立即显示新状态,用户不必等待磁盘 I/O。
  3. Store 调用 Repository 写入 SQLite。
  4. 写库失败不会被静默吞掉,而是从返回的 Future 抛出,便于后续接入统一错误提示。

关键代码片段(PostStore):

Future<void> addPost(ItemPost post) async {
  _posts.insert(0, post);
  notifyListeners();
  await _persist(
    (ItemRepository repository) => repository.insertPost(post),
  );
}

PostScope 使用 Flutter 自带的 InheritedNotifier 下发 Store,避免引入额外状态管理框架。详情页只保存 postId,每次从 Store 查找信息;因此同一条信息被编辑或标记后,已经打开的详情页也能看到最新状态。

SQLite 数据设计与迁移

核心数据表包括:

  • posts:类型、标题、分类、地点、发生时间、联系方式、描述、状态、作者 ID 和图片文件名数组。
  • user_account:本机账户的稳定 ID、显示名称、联系方式和创建时间。
  • app_settings:主题模式和主题种子色。
  • search_history:关键词主键和最近搜索时间。

枚举值存储为 name,时间存储为毫秒时间戳,图片列存储 JSON 数组。数据库通过版本号逐级迁移:旧库从 v1 升级时依次补设置表、搜索历史表、作者列和多用户账户结构,而不是删除重建用户数据。

首次建库才写入示例数据。用户删除示例信息后再次启动,不会被重新播种。

搜索有两套实现:内存列表使用 PostQuery.apply(),SQLite 使用 search_text LIKE。两套实现保持相同规则:关键词全部命中、忽略大小写,并对 %、_ 和反斜杠进行 LIKE 转义。测试中把同一组查询同时交给 Dart 和 SQLite,比较返回的 ID 列表,避免两边结果不一致。

图片存储

数据库不保存绝对路径,只保存图片文件名,例如 时间戳-序号.jpg。应用启动时把文件名解析为当前设备上的绝对路径,从而避免重新安装后应用目录变化导致路径失效。

graph TD a[相册/相机临时路径] --> b[FormImageSession.savePicked] b --> c[ImageFileStore 保存到应用私有 photos/ 目录] c --> d[posts.image_paths 保存文件名 JSON] d --> e[PostStore 读取并解析为绝对路径] e --> f[卡片缩略图 / 详情轮播]

表单会话会追踪“已选择但尚未提交”的文件。点击清空、退出编辑或删除信息时,会删除无归属图片;应用启动时还会根据数据库引用关系清理孤儿文件。图片读取失败时统一回退到物品分类图标,避免单张坏图破坏整个列表。

附加特点设计与展示

响应式导航与 Material 3 主题

项目不仅面向手机竖屏,也考虑了平板、折叠屏和横屏:

  • 可用宽度小于断点时使用底部导航栏。
  • 可用宽度达到断点时使用左侧导航栏。
  • 页面和标签定义只保留一份,避免两套导航出现顺序或选中态不一致。
  • 使用 Material 3 ColorScheme,设置页支持自动、浅色、深色和自定义主题色。

这个设计的意义是:同一套功能不需要为平板重新做一套页面,宽屏只是重新安排导航空间,正文仍保持一致。

关键实现:

return LayoutBuilder(
  builder: (BuildContext context, BoxConstraints constraints) {
    final bool wide =
        constraints.maxWidth >= AppLayout.navigationRailBreakpoint;
    return Scaffold(
      body: wide
          ? Row(children: <Widget>[
              _buildNavigationRail(),
              Expanded(child: content),
            ])
          : content,
      bottomNavigationBar: wide ? null : _buildNavigationBar(),
    );
  },
);

搜索历史

搜索历史只在用户明确提交搜索、点击历史词或点击示例词时记录,不在每次键盘输入时记录。相同关键词忽略大小写,最多保存 10 条,重搜会移动到最前;清空全部记录需要二次确认,单条删除不打断用户。

目录结构与使用说明

目录组织

lib/
├── main.dart                 # 初始化数据库、Store 和应用
├── models/                   # 领域模型与查询对象
├── pages/                    # 页面级 UI
├── widgets/                  # 可复用控件
├── data/                     # SQLite、Repository、Store、图片数据层
├── services/                 # 平台能力抽象,例如选图
├── theme/                    # Material 3 主题与布局断点
└── utils/                    # 通用工具
test/                         # widget、数据层和集成式测试
docs/developer/               # 开发者文档
docs/agents/                  # 开发过程记录

测试人员运行方式

环境建议:Flutter 3.47.5 stable、Dart 3.13.4。先确认设备或模拟器可用:

flutter doctor -v
flutter pub get
flutter devices

运行开发版:

flutter run

执行静态分析和全部测试:

flutter analyze
flutter test

构建 Android release 包:

flutter build apk --release

首次启动会在应用私有目录创建 SQLite 数据库,并写入示例信息。测试过程中可以使用“我的”登记本机账户,发布一条信息,然后在首页搜索、进入详情、编辑、删除或标记状态,验证完整闭环。

单元测试

工具与学习方式

项目使用 Flutter 官方的 flutter_test:

  • test() 测试纯 Dart 的模型、仓储和文件逻辑。
  • testWidgets() 构造 widget 树,模拟点击、输入、滚动和导航。
  • WidgetTester 的 pumpWidget、tap、enterText、pumpAndSettle 分别用于挂载、点击、输入和等待界面稳定。
  • sqflite_common_ffi 为测试提供可控的 SQLite 数据库工厂,测试结束后关闭数据库。

一个最小 widget 测试示例:

testWidgets('发布页校验必填字段', (WidgetTester tester) async {
  await tester.pumpWidget(buildPublishHost());

  await tester.tap(find.byKey(const Key('publish-submit-button')));
  await tester.pump();

  expect(find.text('请输入物品名称'), findsOneWidget);
  expect(find.byKey(const Key('publish-success-dialog')), findsNothing);
});

学习单元测试时,我们先从一个页面的可见行为入手,再逐步把数据库、时间、文件和平台选图替换为可注入的实现。这样测试不是依赖私有方法,而是验证用户能观察到的结果。

测试范围

测试文件 测试内容
sqlite_storage_test.dart 建库、迁移、信息 CRUD、查询对拍、账户隔离
photo_storage_test.dart 文件保存、文件名解析、删除、迁移和孤儿清理
post_form_photo_test.dart 相册/拍照选图、删除图片、9 张上限和取消操作
publish_page_test.dart 表单校验、清空、发布成功提示
search_page_test.dart 关键词搜索、筛选、空结果和返回
search_history_test.dart 搜索历史的去重、排序、上限和持久化
post_detail_page_test.dart 详情展示、图片轮播和状态操作
post_edit_page_test.dart 编辑保存、返回拦截和图片会话
settings_page_test.dart 设置目录、外观、账户和关于页面
appearance_page_test.dart 主题模式和主题色切换

测试数据设计

测试数据同时覆盖正常、空值、边界和异常情况:

  • 正常信息:包含失物、招领、不同分类、时间和联系方式。
  • 空状态:无搜索结果、未登记账户、没有历史记录、没有图片。
  • 边界值:关键词前后空格、大小写重复、包含 % 和 _、9 张图片上限。
  • 状态变化:待处理 → 已找到/已归还,编辑后详情同步更新,删除后详情页能正确返回。
  • 数据隔离:账户 A 不能修改或删除账户 B 发布的信息,但公开浏览仍能看到全部信息。
  • 异常文件:数据库仍有图片文件名但文件已经不存在时,界面显示分类图标,不让异常扩散到整个页面。

“刁难测试人员”可能会快速连续点击、输入空白、反复搜索同一个词、删除后返回旧详情页、在编辑页选图后直接退出。针对这些情况,测试重点放在状态生命周期和资源清理,而不只检查某个按钮是否存在。

GitHub 签入记录

图片

问题、困难与解决方法

问题一:UI 状态和数据库状态可能不同步

  • 问题:如果每次筛选都直接等待数据库查询,界面会出现延迟;如果只改内存,重启应用数据又会丢失。
  • 尝试:最初考虑让页面全部使用 FutureBuilder;后来发现筛选和排序是高频即时操作,不适合每次等待异步任务。
  • 解决:Store 保留内存快照,写操作先通知界面再写库;启动时统一 load()。Repository 负责持久化,页面不直接接触 SQL。
  • 收获:异步持久化和同步 UI 并不冲突,关键是明确数据的权威来源和失败传播方式。

问题二:图片绝对路径在重装后失效

  • 问题:直接把临时图片路径写入数据库,应用目录改变后图片无法读取。
  • 尝试:尝试保存完整路径,后来发现路径与平台、安装实例相关,不能作为稳定数据。
  • 解决:数据库只保存文件名,路径解析集中在 ImageFileStore;启动时迁移旧路径、清理孤儿文件,读图失败时统一降级。
  • 收获:数据库中的业务标识应与运行环境路径解耦。

问题三:内存查询与 SQL 查询口径不一致

  • 问题:Dart 列表查询和 SQL LIKE 处理空格、大小写和通配符的方式不同。
  • 尝试:只测试页面显示结果,无法覆盖所有关键词边界。
  • 解决:抽出统一的 PostQuery 规则,并在 SQLite 测试中把同一批查询分别执行两次,比较 ID 顺序;额外覆盖 %、_ 和反斜杠转义。
  • 收获:当一个规则存在两份实现时,应使用对拍测试,而不是只测试其中一份。

结对协作困难

最大的问题是双方的沟通问题。我在仓库内留下的文档仍不能让一个人从零开始构建起整个项目,而我似乎也预估错误了其他人的工具使用和协作水平。这需要更加详细的文档,和更频繁的沟通交流。

队友评价

我的结对伙伴是林祺皓。参与问题比较积极,但是工具使用经验不足,导致不能很好地和我协作。我留下的文档也没有完全阅读,导致实际提交质量和上游仓库的要求不一致。

posted @ 2026-10-10 20:28  Watermelonabc  阅读(5)  评论(0)    收藏  举报