2026秋软件工程结对作业(第二次之程序实现)
| 项目内容 | 内容 |
|---|---|
| 这个作业属于哪个课程 | H202601 软件工程与软件工程实践 |
| 这个作业要求在哪里 | 2026秋软件工程结对作业(第二次之程序实现) |
| 这个作业的目标 | 完成校园失物招领小程序的开发 |
| 学号 | 102401509-102401519 |
| GitHub 仓库 | zhewanyiershinicheng/102401509-102401519 |
校园失物招领小程序
分工
| 模块 | 主要负责人 | 协作内容 |
|---|---|---|
| 需求分析、原型整理、博客与 PSP | 本人 | 协作者复核功能流程 |
| Python API、SQLite 数据迁移、权限与安全 | 本人 | 协作者联调请求和数据 |
| 页面布局、交互、图片画廊、搜索筛选 | 协作者 | 本人检查移动端显示和异常输入 |
| 代码审查、测试用例设计、README | 两人共同 | 按功能提交并互相审查 |
PSP
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 25 |
| Estimate | 估计这个任务需要多少时间 | 20 | 20 |
| Development | 开发 | 20 | 25 |
| Analysis | 需求分析(包括学习新技术) | 60 | 75 |
| Design Spec | 生成设计文档 | 60 | 80 |
| Design Review | 设计复审 | 30 | 35 |
| Coding Standard | 代码规范 | 30 | 35 |
| Design | 具体设计 | 90 | 110 |
| Coding | 具体编码 | 280 | 360 |
| Code Review | 代码复审 | 60 | 80 |
| Test | 测试、修改代码和提交 | 130 | 170 |
| Reporting | 报告 | 120 | 150 |
| Test Report | 测试报告 | 50 | 65 |
| Size Measurement | 计算工作量 | 20 | 25 |
| Postmortem & Process Improvement Plan | 事后总结和过程改进 | 45 | 55 |
| 合计 | 1035 | 1310 |
实际耗时高于预估,主要来自多图压缩、数据库结构迁移、移动端显示回归和独立状态语义调整。过程改进项是:先冻结 JSON 数据契约,再并行开发前后端;数据库迁移先在副本上验证。
1. 问题分析与设计实现
1.1 需求拆解
核心闭环是:
发布信息 -> 浏览或搜索 -> 查看详情 -> 联系发布者 -> 更新状态
其中“状态”不能和“关闭”耦合。寻物信息完成后显示“已找到”,招领信息完成后显示“已归还”;关闭只是停止公开浏览,不应阻止发布者继续切换完成状态。
1.2 总体数据流
浏览器页面
│ fetch(JSON) + X-Owner-Token
▼
REST API(server.py)
│ 字段校验 / 权限校验 / 时间范围校验
▼
SQLite(items 表)
│ row_to_item()
▼
内存缓存(store.js)
│
├── 首页卡片列表
├── 搜索结果列表
├── 详情页图片画廊
└── 我的发布管理
1.3 数据模型
items 表保存物品类型、名称、类别、发生时间、地点、描述、联系方式、发布者昵称、images JSON 数组、found 和 closed 两个独立布尔字段,以及发布者令牌摘要。
图片在前端压缩为 JPEG data URL,服务端最多接收 6 张,并接受 PNG、JPEG、WebP 的 Base64 data URL。旧版本的单图字段会自动迁移为单元素 images 数组。
1.4 代码实现思路
- 用
server.py提供静态页面和同源 REST API,避免前端直接访问数据库。 - 创建、修改数据前统一经过
_validate_item(),把非法类型、超长文本、非法时间和非法图片挡在数据库之外。 store.js负责请求、缓存和发布者令牌,各页面只处理展示和交互。utils.js、ui.js放置跨页面复用的图片压缩、状态文案、卡片和大图组件。- 视图使用 Hash 路由切换,发布和编辑复用同一个表单,降低重复代码。
- SQLite 启动迁移保证旧单图、旧状态结构可以平滑升级到当前
images、found、closed结构。
1.5 关键代码
发布者令牌摘要
服务端不保存原始令牌,只保存 SHA-256 摘要,避免数据库泄露后直接获得管理权限:
def _owner_hash(token):
return hashlib.sha256(token.encode('utf-8')).hexdigest()
图片数组校验
MAX_IMAGES = 6
def _validate_images(value):
if not isinstance(value, list):
raise RequestError(400, 'images 必须是数组')
if len(value) > MAX_IMAGES:
raise RequestError(400, 'images 最多包含 6 张图片')
images = []
for index, image in enumerate(value, 1):
if not isinstance(image, str) or not IMAGE_RE.fullmatch(image):
raise RequestError(400, 'images 第 %d 项必须是受支持的图片 data URL' % index)
images.append(image)
return json.dumps(images, ensure_ascii=False, separators=(',', ':'))
组合搜索与时间范围
GET /api/items 同时支持关键词、类型、类别、时间范围和本人信息筛选。时间边界使用 julianday(time) 比较,避免不同 ISO 时间格式的字符串比较产生边界误差:
if time_from:
where.append('julianday(time) >= julianday(?)')
params.append(time_from.isoformat().replace('+00:00', 'Z'))
if time_to:
where.append('julianday(time) <= julianday(?)')
params.append(time_to.isoformat().replace('+00:00', 'Z'))
状态文案与图片放大
function resolutionLabel(item) {
return item && item.type === 'found' ? '已归还' : '已找到';
}
详情页将每张图片渲染成横向滚动画廊;点击图片调用 LF.openLightbox(),支持关闭按钮、背景点击、方向键切换和 Esc 关闭。
2. 附加特点设计与展示
2.1 多图、滚动和放大
发布页最多选择 6 张图片,图片会自动压缩,缩略图横向滚动,可单张删除;详情页显示完整图片画廊和当前页码,点击后进入大图模式。它解决了单一图片无法描述外观细节的问题,也适合展示校园卡正反面、钥匙串、设备细节等。
2.2 独立的“找到/归还”和“关闭”
状态拆为 found、closed 两个字段。发布者可以先关闭已经结束的信息,之后仍能切换处理状态;招领信息使用“已归还”,寻物信息使用“已找到”,减少语义歧义和重复询问。
2.3 时间范围筛选
搜索页提供开始日期、结束日期和清除按钮,日期范围可与关键词、类型、类别组合。API 也提供 from、to 参数,方便后续接入更多客户端。
2.4 联系方式快捷操作
详情页支持一键复制联系方式;识别为手机号时显示拨号按钮,减少用户手动输入和误联系。
2.5 数据与安全
.env是唯一配置来源,不依赖进程环境变量。- 静态资源使用白名单,数据库和源代码不能通过 Web 下载。
- 响应启用 CSP、禁止 iframe、禁用 MIME 嗅探。
- 创建、修改、删除都必须携带发布者令牌,服务端再次校验摘要。
2.6 附加特性代码
状态展示和大图预览都采用轻量、无依赖的原生实现:
function stateBadgesHTML(item) {
return (item.found === true
? badgeHTML('resolved', LF.resolutionLabel(item)) : '') +
(item.closed === true ? badgeHTML('closed', '已关闭') : '');
}
button.addEventListener('click', function () {
LF.openLightbox(galleryImages,
Number(button.getAttribute('data-photo-index')));
});
stateBadgesHTML() 根据信息类型显示“已找到”或“已归还”;大图组件复用在发布缩略图和详情画廊中,支持键盘操作。
2.7 实现成果展示

首页集中显示最新信息,搜索页支持关键词、类型、类别和时间范围组合筛选;发布页支持多图;详情页提供图片画廊、联系方式操作和发布者状态管理。
3. 目录说明
校园失物招领小程序/
├── server.py # Python 标准库 REST API、SQLite 和静态资源服务
├── index.html # 单页应用入口
├── css/
│ ├── base.css # 颜色、字体、布局基础
│ ├── components.css # 按钮、卡片、表单、上传等组件
│ └── pages.css # 首页、搜索、发布、详情和我的页面
├── js/
│ ├── app.js # Hash 路由和应用初始化
│ ├── store.js # REST 请求和内存缓存
│ ├── utils.js # 图标、格式化、图片压缩等工具
│ ├── ui.js # 卡片、弹窗、Toast、大图组件
│ └── views/ # home/search/publish/detail/my 页面视图
├── images/ # Logo、空状态和分类占位图
├── .env # host、port、database、max_request_bytes
└── README.md # 项目使用说明和 API 文档
根目录保留 README.md、界面预览 image.png 和本博客文档,不依赖第三方前端框架或构建工具。
4. 测试人员运行说明
-
安装 Python 3.10 或更高版本,推荐使用 Google Chrome。
-
在命令行进入项目目录:
cd 校园失物招领小程序 python server.py -
打开 Chrome 访问
http://127.0.0.1:8765/。 -
首次启动会自动创建
lostfound.db。配置文件位于.env,修改后重启服务。 -
测试基本流程:发布一条寻物或招领信息 -> 首页/搜索页查找 -> 进入详情 -> 复制或拨打联系方式 -> 在“我的发布”标记完成或关闭。
-
测试多图时选择 1 到 6 张 JPG/PNG/WebP 图片,确认缩略图可滚动、删除和点击放大。
-
测试时间筛选时分别设置开始日期、结束日期,并验证关键词、类型和类别可以叠加。
5. 单元测试
5.1 测试工具与学习过程
后端使用 Python 标准库 unittest,前端交互使用浏览器手工回归;测试思路参考 Python 官方 unittest 文档和“边界值、等价类、白盒分支覆盖”方法。每个测试准备最小的合法请求,再逐项改变一个条件,观察返回状态、错误信息和数据库结果。当前仓库按项目交付约定不保留 tests/ 文件,以下代码片段用于博客记录测试设计,测试人员可以按片段在临时目录运行。
简易学习教程:
- 用
import unittest引入 Python 自带测试框架,无需安装第三方包。 - 继承
unittest.TestCase,每个方法只验证一个明确行为。 - 合法结果用
assertEqual、assertTrue,非法输入用assertRaises。 - 在
setUp()中准备最小测试数据,在tearDown()中删除临时数据库。 - 使用
python -m unittest <模块名> -v运行,并阅读每个用例的通过或失败信息。
5.2 测试用例(12 个)
| 编号 | 用例 | 输入/操作 | 预期结果 |
|---|---|---|---|
| T01 | 创建合法寻物 | 完整字段、images: [] |
返回 201,字段入库 |
| T02 | 缺少名称 | name 为空 |
返回 400,不写入数据库 |
| T03 | 类型非法 | type: exchange |
返回 400 |
| T04 | 时间非法 | time: not-a-date |
返回 400 |
| T05 | 图片数量越界 | images 为 7 个合法 data URL |
返回 400 |
| T06 | 图片格式不支持 | data:image/gif;base64,... |
返回 400 |
| T07 | 两个状态独立 | found: true, closed: false |
两个字段分别保存 |
| T08 | 关闭后仍可切换完成 | 先关闭,再更新 found |
更新成功,公开列表不显示 |
| T09 | 公开列表过滤关闭项 | 查询 GET /api/items |
不返回 closed: true |
| T10 | 无权限修改 | 使用另一令牌 PUT |
返回 403 |
| T11 | 关键词和类别组合 | kw=校园卡&category=校园卡 |
只返回匹配项 |
| T12 | 时间范围边界 | from=2026-10-09&to=2026-10-09 |
包含当天开始和结束时间 |
5.3 测试代码示例
import unittest
class ItemValidationTests(unittest.TestCase):
def test_valid_item(self):
item = VALID_ITEM.copy()
item['images'] = []
self.assertEqual(_validate_item(item)['name'], '蓝色校园卡')
def test_reject_missing_name(self):
item = VALID_ITEM.copy()
item['name'] = ''
with self.assertRaises(RequestError):
_validate_item(item)
def test_reject_too_many_images(self):
item = VALID_ITEM.copy()
item['images'] = [VALID_IMAGE] * 7
with self.assertRaises(RequestError):
_validate_item(item)
def test_found_and_closed_are_independent(self):
item = _validate_item({**VALID_ITEM, 'found': True, 'closed': True})
self.assertTrue(item['found'])
self.assertTrue(item['closed'])
if __name__ == '__main__':
unittest.main()
前端的关键回归点通过 Chrome DevTools 或手工脚本检查:输入框必填校验、最多 6 张图片、横向滚动、大图关闭方式、时间范围清除、复制联系方式和状态按钮文案。
5.4 测试数据构造
合法数据覆盖寻物/招领两种类型和所有类别;边界数据覆盖空文本、最长文本、过去/未来时间、0/1/6/7 张图片、非 Base64 data URL;权限数据使用两个随机发布者令牌;状态数据分别组合 found、closed 的四种情况。针对测试人员的“刁难”,还考虑了重复点击按钮、连续上传失败、浏览器隐私模式无法写入 localStorage、中文和表情输入、超长请求、非法查询参数、旧单图数据库迁移、关闭后修改状态以及并发修改同一信息等情况。
6. 代码签入记录
协作时由一名同学维护主仓库,另一名同学 fork;每完成一个功能就创建功能分支并发起 Pull Request,另一人完成代码审查后再合并。
提交记录:

7. 开发中遇到的问题与解决方法
7.1 数据库状态迁移
问题:早期原型把处理状态保存在单个 status 字段,后续需要独立切换“找到/归还”和“关闭”。
尝试:直接添加字段会破坏旧数据,先用 SQLite 表重建迁移,再把 status 映射到 found、closed。
结果:旧记录、发布者摘要和创建时间均保留,迁移后 API 结构统一。
收获:迁移脚本应先在临时数据库验证,再对正式数据库执行,并在启动时保持幂等。
7.2 多图体积和展示
问题:6 张原图可能超过请求上限,单图布局也无法横向浏览。
尝试:前端使用 Canvas 压缩到 900 像素、JPEG 质量 0.72;后端限制 6 张并校验 data URL;画廊使用 CSS scroll-snap 和独立大图组件。
结果:移动端可流畅滚动,图片可删除和放大。
收获:上传控件、服务端校验和展示组件必须使用同一份数据契约。
7.3 状态语义
问题:“已找到”对招领信息不准确,且关闭状态曾影响完成状态。
尝试:把文案按 type 动态切换,并将 found、closed 作为独立字段。
结果:招领显示“已归还”,寻物显示“已找到”,两个操作互不阻塞。
收获:业务状态不应为了页面筛选而合并,数据库字段应表达真实业务含义。
8. 队友评价
值得学习的地方
- 能把原型中的视觉流程拆成可测试的 API 和页面状态。
- 对中文显示、移动端宽度和异常输入比较敏感,能及时提出修改意见。
需要改进的地方
- 部分功能可以更早拆成小提交,方便通过 commit history 回溯设计决策。
- 代码审查时关注权限、数据迁移和长期维护,不只满足于页面能打开。
9. 总结
本项目完成了“发布信息—浏览或搜索—查看详情—联系发布者—更新状态”的完整闭环,并增加了多图上传、滚动图片画廊、大图预览、时间范围筛选、复制/拨号联系方式、独立处理状态、SQLite 持久化和基本安全防护。

浙公网安备 33010602011771号