拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python Flask校园失物招领系统:关键词匹配算法实战

Python Flask校园失物招领系统:关键词匹配算法实战 校园里丢东西这事几乎每天都在发生。图书馆落下一张校园卡操场看台丢一副耳机食堂吃完饭后伞还在门口挂着人已经回宿舍了——而另一边保洁阿姨捡到一堆东西拍在群里问有没有人认识失主。消息刷得太快等看到的时候东西可能已经开始积灰了。这个项目要解决的就是这种信息不对称的问题用Python写一个基于Flask的校园失物招领系统让失物和招领信息能通过关键词相似度匹配算法自动对上再按相关度推荐给双方。这篇文章我会把这个项目的完整开发过程拆开讲从需求分析、数据模型设计到中文关键词匹配算法的实现、Flask网页端的搭建再到本地部署和踩坑记录全程偏实操。适合正在做毕设、课程设计或者单纯想练Flask和文本匹配算法的同学参考。1. 项目需求拆解校园场景下的失物招领为什么需要一套系统1.1 痛点微信群里滚动太快关键信息留不下来做过校园生活服务的都会有个直观感受失物招领这件事本质上是个“时效性信息匹配”问题。丢东西的人希望最快速度找到物品捡到东西的人希望最快速度找到失主信息本身是双向的、短促的、高频的。但你用微信群、QQ群、表白墙来跑这个流程问题很明显。消息是按时间线排布的最新一条永远在最上面三天前的“寻物启事”早就沉底了。更麻烦的是群里的文本是半结构化的——有人发“捡到一张卡请来西门保安亭”有人发“丢了个u盘金色的急”信息密度低、关键词不统一、无法检索更谈不上自动匹配。所以这个系统要做的第一件事是把零散的闲聊变成结构化的数据。用户发布失物或招领信息时系统引导他们填标题、描述、物品分类、丢失地点、联系方式和图片而不是让他们在输入框里自由发挥。结构化的好处有两个一是后续的相似度匹配有据可算二是数据沉淀下来之后能做展示、筛选和统计。1.2 功能边界发布、匹配、推荐三件事说清楚功能设计上我刻意做了减法。这个场景不适合一上来就做用户注册、权限、后台管理那一整套。我只需要确保三件事跑通第一信息发布。用户进来选择是“我要找失物”还是“我捡到了东西”填完表单提交数据进入数据库。第二智能匹配。每次有新的失物或招领信息入库后系统主动去另一张表里捞候选记录用关键词相似度算法算分把分数超过阈值的结果排列出来。第三推荐展示。用户查看一条信息时页面上直接显示“可能匹配的招领/失物”列表按相关度分数排序省去自己翻看大量无关条目的时间。这套逻辑其实和电商推荐很接近——我把它叫做“物品版的信息撮合”。实现上并不需要多高深的算法核心是文本相似度计算和一条合理的数据流转链路。后续如果有人要扩展可以加消息通知、管理员审核、过期信息自动下架但这些都不是最初版本该做的事。1.3 技术选型为什么是Flask而不是Django为什么SQLite就够技术选型这块我在立项前犹豫过最终还是定了Flask加SQLite的组合。Flask胜在轻量。这个系统总共就几张页面、十来个路由用Django的话项目脚手架、ORM、Admin后台、中间件这些东西虽然都现成但不是每一个都用得上。Django的Admin确实能省事不过也会让你失去对路由和业务逻辑的直接掌控感。Flask没有那么多约定路由自己写模板自己调数据库用一个Flask-SQLAlchemy就能集成学习曲线平滑得多。数据存储方面SQLite是被低估的。很多人一听SQLite就摇头觉得“这玩意儿能上生产吗”但你需要分清场景。这是一个本地部署、教学用途、并发量极低的系统SQLite不需要独立服务进程数据库就是一个文件备份就是复制文件整个项目可以压缩到极小的体积分发。等将来真需要上MySQL、PostgreSQLFlask-SQLAlchemy的模型代码基本不用改只改连接字符串就行。对于这种从小到大的演进路径SQLite是最好的起点。2. 轻量化数据模型设计失物、招领和推荐匹配的地基2.1 表结构两条核心表加一个状态机“轻量化”不等于数据结构可以草率。恰恰相反表设计对了后面写匹配算法会省非常多事。我把核心数据设计成两张表失物信息表和招领信息表。先看失物表的字段class LostItem(db.Model): id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(100), nullableFalse) # 标题黑色U盘 金士顿 32G description db.Column(db.Text) # 详细描述 category db.Column(db.String(50)) # 分类数码/证件/饰品/书籍/其他 place db.Column(db.String(100)) # 丢失地点图书馆三楼 contact db.Column(db.String(100)) # 联系方式 image_path db.Column(db.String(200)) # 图片路径 status db.Column(db.Integer, default0) # 0待匹配 1匹配成功 2已找回 created_at db.Column(db.DateTime, defaultdatetime.now) def to_dict(self): return { id: self.id, title: self.title, description: self.description, category: self.category, place: self.place, contact: self.contact, image_path: self.image_path, status: self.status, created_at: self.created_at.strftime(%Y-%m-%d %H:%M) }招领表的字段几乎一模一样只是在语义上表达“我捡到了什么”。为什么不直接合成一张表原因很简单失物和招领的方向不同虽然字段相似但业务流程上有差异——失物信息会主动去匹配招领信息招领信息也会反向去匹配失物信息如果混在一张表里匹配的时候还要时刻用type字段分辨方向逻辑会变绕。两张表结构对称写入和查询都直观这是用一点存储冗余换代码可读性的典型做法。status这个字段是整个系统的隐形主干。0表示还在等待匹配1表示有疑似匹配正在确认2表示已完成闭环。这个状态机的存在让系统后期可以很自然地从“匹配成功”推进到“确认归还”的流程闭环不至于做了推荐之后双方在线下对接完系统里还挂着一堆无效数据。2.2 附件和联系方式的存储路径、编码、还有那个经常出错的坑这里有个细节值得单独说。联系人信息和附件路径的设计关系到后面部署时会不会踩坑。联系方式一开始我差点直接用字符串存后来发现没有校验的话用户可能填任何东西。考虑到这个系统不是对外公开的服务我不做强校验留成普通字段但模板里会提示“手机/微信/QQ均可”。联系方式在详情页公开可见不需要登录系统因为校园场景下双方可能要在线下见面核实物品细节降低联系门槛比所谓的信息安全更实际。图片上传的路径问题是所有Flask项目新手绕不过去的坎。我在早期版本里直接存了用户上传文件的原始文件名比如“C:/Users/xxx/Desktop/photo.jpg”然后存进数据库。结果页面显示的时候浏览器开始找本机绝对路径导致图片全挂。正确的做法是上传文件时用uuid重命名保存到项目的static/uploads目录数据库里只存相对路径“uploads/xxxx.jpg”模板里用url_for(static, filenameitem.image_path)来拼接完整地址。这样无论项目搬到Windows还是Linux路径都能正确解析不会因为环境切换出现附件路径错误。2.3 为什么“轻量化”不等于不建索引表的问题解决了还有一个容易犯的毛病就是忘记索引。数据量小的时候查起来确实都快但一旦信息积累到几百上千条每次匹配推荐都要对全表做一次字符串扫描速度就会肉眼可见地变慢。在标题和分类字段上加索引成本极低收益却很明显。Flask-SQLAlchemy里加索引只需要在字段定义时传个参数title db.Column(db.String(100), nullableFalse, indexTrue) category db.Column(db.String(50), indexTrue)分类加索引尤其值得因为匹配算法第一步往往是根据category粗筛候选集把“数码”的失物和“书籍”的招领放在一起算相似度毫无意义。用分类字段先把候选范围缩小再做细粒度的文本相似度计算性能和准确率都能得到保证。3. 中文关键词相似度匹配算法从分词到加权打分3.1 为什么直接比文本行不通这是整个项目里最有意思的部分。一开始我的想法非常直接用Python内置的difflib.SequenceMatcher逐条比对文本相似度分数高的就是匹配的。但实测下来效果很惨。典型场景是这样的有人发布失物“校园卡一张李某某尾号1234”有人发布招领“捡到一卡通一张请失主联系我”。两句话在字面上几乎没有重合但语义明明是同一类东西。难点主要出在两个地方一是中文没有天然的分词边界。英文按空格切割就能得到完整的词中文必须考虑“校园卡”是一个整体被切成“校园”“卡”就丢了语义二是同义词表达问题。“校园卡”和“一卡通”指同一个东西但字面上没有任何交集。所以纯基于字面的比对方案直接放弃换成“中文分词 同义词扩展 加权相似度计算”的组合思路。3.2 基于jieba的轻量分词方案分词方案上我选了jieba。这个库虽然体积不算小但部署时直接pip安装即可分词质量稳对项目体量来说完全匹配。我的做法是先分词再过滤停用词。停用词包括“我”、“的”、“了”、“一个”、“捡到”、“丢失”、“在”、“有”这类高频但无实际辨识度的词。这一步做完信息就变成了关键词集合这是后续相似度计算的最小单位。import jieba STOP_WORDS {我, 的, 了, 一个, 在, 有, 捡到, 丢失, 寻找, 这个, 那个} def tokenize(text: str) - set: if not text: return set() words jieba.lcut(text.lower()) return {w.strip() for w in words if w.strip() and w not in STOP_WORDS}这里注意jieba.lcut返回的是列表我直接转成了set因为后续要走的集合运算要求元素唯一。还有个细节是text.lower()英文字母数字先统一成小写避免“U盘”和“u盘”在后期处理时被当成两个不同token。停用词表是这个词法方案里最值得打磨的部分。我一开始没过滤“一个”、“这种”结果分词结果全是废话词相似度被严重稀释。后来换个思路特征是宁可少不可滥每个留下的词都应该有辨识能力。这个原则在算法调优时帮了大忙。3.3 同义词映射和分类粗筛解决“校园卡”与“一卡通”分词之后下一步是同义词扩展。我维护了一个极小的同义词映射表涵盖校园场景下出现频繁的几类物品SYNONYMS { 校园卡: {校园卡, 一卡通, 饭卡, 门禁卡, 学生卡}, 身份证: {身份证, 证件}, 学生证: {学生证, 证件}, u盘: {u盘, 优盘, usb}, 充电宝: {充电宝, 移动电源}, 雨伞: {雨伞, 伞}, }注意这里的值是set因为一个词可能对应多个同义词直接用set的update方法合并进关键词集合即可。同义词映射并不是完整的知识图谱只是覆盖高频场景的偏置表——对一个课程设计或校内项目来说这个粒度已经足够。在实际匹配流程中第一步永远是分类粗筛。我会先从失物表或招领表里按category字段取候选集比如失物是“数码”类就去招领表的“数码”分类里捞候选再在候选内做关键词集合运算。这一步的价值在于它能把“U盘”和“雨伞”这种截然不同类目的文本挡在相似度计算外面避免出现高相似度的假阳性。3.4 加权相似度Jaccard系数和标题权重的组合候选集确定后进入打分环节。我用的是Jaccard系数加权重修正的混合方案。Jaccard系数的思想是“交集大小除以并集大小”语义非常直观两个关键词集合共同拥有的词越多问题越相似。纯交集的绝对数量不适合做分数因为长文本的关键词数量天然占优归一化成比例才能公平比较。def jaccard_similarity(set1: set, set2: set) - float: if not set1 or not set2: return 0.0 inter len(set1 set2) union len(set1 | set2) return inter / union if union 0 else 0.0仅靠Jaccard系数有个盲区它完全不看词序和词频。比如“图书馆东门”和“东门图书馆”在集合运算下是完全相同的但真实场景里这两个说法大概率指同一个地方问题不大。更值得注意的是失物标题和描述的信息密度差异很大标题往往是物品的浓缩概括描述则可能有大量无用的场景描写。所以我把标题和描述拆开算分再加权合并def match_score(lost_item, found_item) - float: title_score jaccard_similarity( tokenize(lost_item.title) | SYNONYMS.get(lost_item.title, set()), tokenize(found_item.title) | SYNONYMS.get(found_item.title, set()) ) desc_score jaccard_similarity( tokenize(lost_item.description), tokenize(found_item.description) ) place_score jaccard_similarity( tokenize(lost_item.place), tokenize(found_item.place) ) return round(title_score * 0.5 desc_score * 0.3 place_score * 0.2, 4)权重分配的思路是标题是物品的最强标识给50%权重描述补充细节给30%地点信息虽然重要但描述措辞千差万别而且“图书馆”和“图书馆门口”这种表达集合运算后重合度不稳定给20%。实际调参时可以按数据表现微调但比例关系不要变动太大标题永远是第一信号。同义词扩展在这里起了决定性作用。没有同义词映射时“校园卡”和“一卡通”的标题相似度是0加了映射之后两者共享同一个关键词集合相似度直接从0跳到1。这个改进让整个系统的感受质量有了质的飞跃。3.5 无效信息过滤与阈值截断匹配算法光有相似度函数还不够还要考虑什么时候该“不推荐”。默认把所有候选都显示出来等于没做推荐。我给系统设了两层过滤第一层是信息粒度过滤。如果某条记录分词后的关键词数量少于2个就跳过不参与匹配。比如标题“求帮忙”、“急急急”这种纯情绪表达没有清晰的关键词无法参与匹配。这个规则简单粗暴但有效。第二层是相似度阈值过滤。分数低于0.35的候选直接不展示。这个阈值不是拍脑袋定的而是用测试数据跑出来的经验值。实测中0.35以下的匹配结果正确率基本都在50%以下展示出来只会干扰用户判断。def recommend_lost_for_found(found_item, limit10): candidates LostItem.query.filter_by( categoryfound_item.category, status0 ).all() scored [] for lost in candidates: score match_score(lost, found_item) if score 0.35: scored.append((lost, score)) scored.sort(keylambda x: x[1], reverseTrue) return scored[:limit]另外提一句纯文本匹配算法天然无法理解“表面不同但有关联”的表述比如“黑色书包”和“黑书包”算了但“黑色双肩包”可能就匹配不上。这个世界的模糊性问题不是单靠文本就能解决的但通过不断扩充同义词表、观察失败案例、调整权重可以把准确率从六成拉到八成以上。对一个小型校内系统来说这个水平已经具备实用的价值。4. Flask网页端核心功能实现与本地部署4.1 项目结构一次把目录理清楚后端算法再好看最后也要落到网页上。项目目录结构从一开始就规划好避免后面改来改去campus-lost-found/ ├── app.py # 主入口Flask应用、路由、匹配逻辑 ├── models.py # 数据模型 ├── requirements.txt # 依赖清单 ├── templates/ │ ├── index.html # 首页列表页 │ ├── publish.html # 发布信息页面 │ ├── detail.html # 信息详情推荐结果页 └── static/ ├── css/ └── uploads/ # 上传图片存储目录这个结构几乎不需要一个独立的“config.py”配置模块因为项目的配置项太少。app.py里直接声明SQLALCHEMY_DATABASE_URI和上传目录就够用。依赖清单用requirements.txt锁定版本我实测在Python 3.8到3.12环境下都能正常跑Flask3.0.2 Flask-SQLAlchemy3.1.1 jieba0.42.1Flask 3.x版本里有些API和2.x有差异比如app.run的方式没变但底层Werkzeug升级到2.3以上之后某些依赖的兼容性需要留意。锁版本可以避免“在我机器上明明能跑”的尴尬局面。4.2 发布路由和数据入库发布页面是一个表单用户选择“发布失物”还是“发布招领”提交到不同的路由。核心代码不复杂但是有值得注意的细节。app.route(/publish/lost, methods[POST]) def publish_lost(): title request.form.get(title, ).strip() description request.form.get(description, ).strip() category request.form.get(category, 其他) place request.form.get(place, ).strip() contact request.form.get(contact, ).strip() if not title or not contact: flash(标题和联系方式是必填项) return redirect(url_for(publish_page)) item LostItem( titletitle, descriptiondescription, categorycategory, placeplace, contactcontact, status0 ) # 处理图片上传 file request.files.get(image) if file and file.filename: ext os.path.splitext(file.filename)[1].lower() if ext not in {.jpg, .jpeg, .png, .gif}: flash(图片格式不支持请上传jpg或png) return redirect(url_for(publish_page)) filename str(uuid.uuid4()) ext file.save(os.path.join(app.config[UPLOAD_FOLDER], filename)) item.image_path uploads/ filename db.session.add(item) db.session.commit() return redirect(url_for(detail, item_typelost, item_iditem.id))这里值得记住的点有三处。第一request.form.get拿到的值全部是字符串如果需要转int做比较一定记得用int()转换。我在调试时好几次遇到校验不过最后发现是字符串和整数在做比较。第二文件上传必须做白名单校验。只允许常见图片后缀的扩展名同时用uuid重命名千万不要直接用用户上传的原始文件名去保存。这里面有两个安全问题原始文件名可能包含路径穿越符号另外有同名覆盖的风险。uuid重命名一次解决两个隐患。第三图片保存路径要用os.path.join拼接不要手工拼字符串。Windows和Linux的路径分隔符不同手工用“/”后期转移到Linux就会出问题。4.3 详情页和推荐展示信息入库之后最关键的是详情页里把推荐结果展示出来。这段逻辑是“算法网页”的结合点。app.route(/detail/item_type/int:item_id) def detail(item_type, item_id): if item_type lost: item LostItem.query.get_or_404(item_id) results recommend_lost_for_found(item) results [(r[0].to_dict(), r[1]) for r in results] else: item FoundItem.query.get_or_404(item_id) results recommend_found_for_lost(item) results [(r[0].to_dict(), r[1]) for r in results] return render_template(detail.html, itemitem, resultsresults)模板里的展示就非常直观了。每条推荐结果通过Jinja2循环渲染div classrecommend-list {% for found, score in results %} div classcard div classcard-title{{ found.title }}/div div classcard-meta 匹配度{{ %.0f | format(score * 100) }}% /div div classcard-place{{ found.place }}/div div classcard-contact联系方式{{ found.contact }}/div a href{{ url_for(detail, item_typefound, item_idfound.id) }}查看详情/a /div {% empty %} p classempty暂无可匹配的招领信息建议过两天再来看/p {% endfor %} /div这个“匹配度百分比”是画龙点睛的设计。用户不一定理解算法原理但一眼就能知道这条信息的值不值得点开。我按分数区间还加了颜色标识90%以上用绿色60%-90%用橙色60%以下用灰色视觉上形成天然的注意力梯度。4.4 本地部署和运行零配置启动网页端写完本地跑起来非常容易这是Flask天生的优势。开发环境下直接启动pip install -r requirements.txt python app.py默认绑定localhost:5000浏览器打开就能访问。如果要在局域网内被其他电脑测试需要把host参数改成0.0.0.0if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)注意debugTrue是为了开发方便但生产怀疑的时候绝不能开着这个开关。debug模式除了会暴露交互式调试器给访问者还有个副作用是reloader会双进程运行如果代码里有初始化数据的逻辑可能会重复写入。要想让其他宿舍的同学也能访问还有个更要紧的坑——操作系统防火墙。Windows下第一次用局域网IP访问时会弹防火墙授权窗口一定要勾选“专用网络”允许访问否则别人怎么都连不上。真正部署到服务器更推荐用gunicornpip install gunicorn gunicorn -w 2 -b 0.0.0.0:5000 app:app这里的app:app表示从app.py文件里导入名为app的Flask实例。gunicorn是Linux下最常用的Python WSGI服务两个worker进程对这个量级的并发完全够用。5. 踩坑实录常见问题与排查方法5.1 附件路径错误是最高频的问题这个项目里我收到最多的问题就是“图片上传成功后页面不显示”。排查下来十有八九是路径拼错了。错误示范“C:/Users/abc/Desktop/project/static/uploads/xxx.jpg”存进了数据库。正确做法数据库只存相对路径模板里用url_for生成完整URL。img src{{ url_for(static, filenameitem.image_path) }} alt物品图片url_for会把static文件夹映射成/static/所以image_path存“uploads/xxx.jpg”就可以了。这里有个容易忽视的地方static目录的访问是由Flask默认的static路由提供的如果项目用了蓝图且设置了url_prefixurl_for里记得加蓝图的名称前缀否则报“Building endpoint failed”。当时我为了排查这个问题特意在浏览器里直接输出了完整URL对照才意识到绝对路径和相对路径在这个场景下的差异。后来我给所有上传图片统一走这个逻辑问题绝迹。5.2 中文乱码SQLite不背这个锅中文乱码的问题一出现很多人第一反应是数据库编码不对。SQLite本身是UTF-8存储乱码通常另有罪魁祸首。排查顺序是这样的先看HTML模板的里有没有声明charset再看浏览器收到响应头里的Content-Type。meta charsetutf-8然后确认Flask的响应头app.config[JSON_AS_ASCII] False这个配置主要影响API接口返回中文时Flask默认会把JSON中的中文转成Unicode转义字符。如果系统里做了Ajax接口记得加上。还有一个隐蔽的点是从request.form里拿到中文后如果打印日志或做字符串比较时出现乱码一般不是函数的锅而是你用来查看终端日志的终端编码不对。Windows下cmd默认是GBK不兼容UTF-8输出用VS Code自带终端或者IDE控制台查看就能规避。5.3 推荐结果为空先查阈值再查分词系统上线测试时我遇到一个尴尬状况——无论怎么发信息推荐列表都是空的。数据明明入库了分类也相同但匹配不出来。排查时我先打印了中间结果发现两个问题依次暴露。第一是推荐阈值定得过高当时随手写的0.5对标题简洁的条目来说关键词集合太小交集比例很难过0.5。降到0.35之后空结果问题缓解了不少。第二是jieba分词结果里混入了大量的停用词。比如“我的黑色U盘找不到了”分词出来是“我/的/黑色/U盘/找不到/了”如果不把“我”、“的”、“了”过滤掉它们会作为分母占用并集的体积把相似度稀释得惨不忍睹。完善STOP_WORDS集合之后分数明显回升。建议遇到“推荐为空”时先打印出分词后的集合视觉检查一遍会比反复调参高效得多。5.4 部署到服务器后静态文件加载失败本地跑得好好的放到Linux服务器上用gunicorn跑页面CSS全乱、图片全挂。排查后发现是nginx配置里没有处理/static路径的请求。gunicorn确实能服务静态文件但性能和效率都不如专用的静态文件服务来处理。如果你用nginx做反向代理加这段配置location /static/ { alias /var/www/campus-lost-found/static/; }如果不用nginx直接用gunicorn跑完整服务需要在app.py里显式注册静态文件路由from flask import send_from_directory app.route(/static/path:filename) def static_files(filename): return send_from_directory(app.config[STATIC_FOLDER], filename)不过这种做法只建议在玩具项目里用生产环境还是应该丢给nginx。另外注意Linux环境下文件路径区分大小写Windows下不区分如果本地用的是“Uploads”目录部署到Linux时路径对不上也可能导致404。5.5 模板改了不生效不是缓存是找不到文件位置开发时改模板经常看到旧页面有人怀疑是浏览器缓存。第一反应清缓存没毛病但更常见的原因是Jinja2模板文件放错了目录。Flask默认找templates目录下的模板如果你把模板放在了项目根目录或者路径拼写错误Flask不会报错而是继续渲染默认的404页面。排查方式很简单在路由里打印一下模板路径print(app.root_path) print(os.listdir(os.path.join(app.root_path, templates)))这样就能确认模板到底在哪个文件夹、文件名有没有写错。还有个小技巧模板文件名建议不要和Flask内置规则重名比如你不应该把自己的模板也命名为“index.html”放在static目录里很容易引起URL解析的混乱。写在最后前后改了三个版本这个项目的开发经历给到我最深的感受是作为一个校园场景的小型工具关键的竞争力不在于功能有多全、界面有多炫而在于两点——数据是否结构化、匹配是否精准。数据结构化解决的是“信息可检索”匹配精准解决的是“结果能落地”。这两件事做好哪怕整站只有六个页面它也能真正解决学生丢东西找不到的痛点。给想复刻这个项目的同学留几句个人经验。第一匹配算法的价值远高于界面美化先把算法调到令人满意的精度再回头打磨样式。第二Python版本尽量用3.10或更高Flask 3.x的新特性在老版本上兼容性时不时有坑。第三一定记得先用假数据测试匹配逻辑我建议至少准备20条失物和20条招领的样例数据只有数据量上来了你才会发现分词、阈值、同义词映射各自的真实问题。如果你把项目扩展后再跟其他系统联动比如接入学校已有的消息推送渠道或者在信息发布的24小时后做一个“自动提醒匹配”的功能这个项目的实际价值还能再上一个台阶。校园场景的数据量虽然不大但每一笔数据背后都是某个学生当下的困境技术工具能做到早一分钟推送就多一分帮助。最后再分享一个小技巧发布信息时如果有多张图片要上传要注意Flask的request.files.getlist(images)而不是get因为get只能拿到第一张。失物招领场景里物品照片往往比文字描述更能让人一眼确认这个环节值得多花点心思设计。
返回列表