项目简介 ​

校园二手交易平台面向高校学生的闲置物品流转场景:用户可以发布商品或求购信息,围绕商品在线沟通,下单后完成交易并互相评价。

项目最初是 6 月的小组课程设计,由 Flask 后端 API 和一个移动端前端组成,我负责前端的请求层和后端的订单模块。课程设计结束后,我用 React Native + TypeScript 独立重写了整个移动端,基于 Expo,共 27 个页面。重写过程中也系统地梳理并修复了前后端接口对不上的一批问题。

🔗 在线演示:这是 React Native 应用导出的网页版,建议用手机打开;用电脑打开时会显示在手机框里,并附有扫码二维码。

功能详解 ​

页面一览 ​

底部 5 个 Tab:首页、求购、发布、消息、我的,其余页面从这几个入口进入。

模块页面做什么
浏览首页、搜索、商品详情双列商品列表,下拉刷新、上滑加载;多条件搜索;详情页看图、收藏、联系卖家、下单
求购求购列表、求购详情浏览别人的求购,收藏或直接私聊;自己的求购可以编辑、结束
发布发布、发布表单、编辑商品发布出售商品或求购信息,最多 9 张图;编辑和上下架自己的商品
交易确认购买、我的订单、订单详情、评价下单扣款,取消或确认完成,交易完成后互评
消息消息、聊天、通知会话列表和未读数;文字、图片、商品卡片消息;订单变动通知
个人我的、编辑资料、我的发布、心愿单、钱包、信用、设置个人资料、收藏、余额和交易明细、信用概览、清缓存和退出登录
他人用户主页、TA 的在售商品、TA 的评价查看卖家的商品和收到的评价

1. 账号与登录态 ​

  • 后端用 JWT 做身份认证(有效期 7 天),密码用 PBKDF2-HMAC-SHA256 加盐哈希,迭代 10 万次。
  • App 端的 Token 存在 expo-secure-store(系统级加密存储),用户信息体积较大,存在 AsyncStorage。启动时先从本地恢复登录状态:有用户信息但没有 Token,视为无效并清掉。
  • 所有请求统一带上 Authorization 头;任意请求返回 401,都会清掉登录态、提示「登录已过期」并跳到登录页。

未登录时的「我的」页:提示登录后才能查看交易和收藏信息未登录时的「我的」页:提示登录后才能查看交易和收藏信息

2. 商品浏览与搜索 ​

  • 首页和搜索结果都是无限滚动的分页列表,滑到底自动加载下一页。
  • 搜索支持关键词、分类、校区、成色、价格区间和价格排序。筛选条件都放在请求的缓存 key 里,改任何一项都会自动从第一页重新搜索。
  • 商品详情页可以左右滑动看图、点开大图预览、收藏、联系卖家,或者直接下单。

3. 发布与管理 ​

  • 同一个发布表单支持两种模式:出售商品(标题、描述、价格、分类、成色、校区)和发布求购(预算范围等)。
  • 图片用九宫格选择,最多 9 张,可以预览和删除。提交时只上传新选的本地图片,已经上传过的直接沿用原地址。
  • 发布成功后,首页、搜索、我的发布等所有商品列表自动刷新,不需要在页面之间传「要刷新」的标记。

发布入口:先选择「出售闲置」还是「求购物品」,再进入对应的表单发布入口:先选择「出售闲置」还是「求购物品」,再进入对应的表单

求购列表:每条求购显示预算范围、成色要求、校区、发布者和发布时间求购列表:每条求购显示预算范围、成色要求、校区、发布者和发布时间

4. 交易流程 ​

  • 下单:确认购买页展示商品和卖家信息,提交后在后端一个事务里完成扣款、进账、商品标为已售和创建订单(见下文难点 1)。
  • 订单状态:待交易 → 已完成 / 已取消。买卖双方都可以取消订单,只有买家可以确认完成(提示当面验货后再确认);取消后退款,商品恢复在售。
  • 评价:交易完成后可以给对方打 1~5 分并写评价,已经评价过的订单不再显示「去评价」。
  • 钱包:显示余额,交易明细由买入和卖出订单汇总而成(不含已取消的订单)。

5. 聊天与消息 ​

  • 可以从商品或求购发起私聊,支持文字、图片(相册或拍照)和商品卡片消息。相邻两条消息间隔超过 5 分钟时显示时间分隔。
  • 聊天页在前台时每 3 秒拉一次新消息,离开页面或者切到后台就自动停止。
  • 「消息」Tab 的角标 = 聊天未读数 + 通知未读数,每 5 秒刷新一次。通知根据订单变动生成(比如有人买了你的商品、订单被取消),进入通知页就算已读。

6. 后端 ​

后端是 Flask,按 api(路由)→ services(业务逻辑)→ models(数据模型)分层,共 8 个模块、47 个接口、12 张数据表(用户、商品及图片、分类、求购及图片、收藏、订单及订单日志、评价、聊天会话及消息),所有接口返回统一的 JSON 结构。

举报、信用分这两个功能目前只有前端页面,后端还没有对应的接口;信用页上的成交数、在售数和评价数,是用现有接口算出来的。

技术栈 ​

分层技术用在哪里
框架React Native 0.88、React 19、Expo SDK 58移动端 App
语言TypeScript所有接口的请求和返回都定义了类型,作为前后端的接口约定
路由Expo Router(文件路由)src/app/ 下一个文件就是一个页面,底部 Tab 是自定义的 TabBar
服务端状态TanStack Query v5接口数据的缓存、分页、轮询、失效刷新
客户端状态Zustand登录态、全局提示
存储expo-secure-store、AsyncStorageToken 加密存储;用户信息和通知已读时间
图片expo-image、expo-image-picker图片显示和缓存;相册选图、拍照
后端Python、Flask(Blueprint)、SQLModel、SQLite47 个 REST 接口
安全PyJWT、PBKDF2-HMAC-SHA256身份认证、密码哈希

系统设计 ​

移动端分四层,页面里不直接发请求:

text
src/api/       请求层:基于 fetch 封装,统一超时、Token、401、错误提示和文件上传
src/queries/   数据层:TanStack Query 的查询 hooks,所有缓存 key 集中定义在 keys.ts
src/stores/    本地状态:Zustand 管理登录态和全局提示
src/app/       页面:只负责界面和交互,数据都从 queries 拿

src/types/api.ts 按后端的真实返回定义了所有接口的数据类型,字段名一旦写错,在编译阶段就会报错。

我负责的部分 ​

  • 课程设计阶段(小组)
    • 前端请求层:加上登录过期(401)的统一处理;新增统一的文件上传函数,把各页面里手写的上传代码都换成它。
    • 订单后端:把下单、取消、完成三个操作改成原子性事务,加上买家扣款、卖家进账的资金划转和商品状态的自动变更。
    • 前后端联调:修复求购详情页图片和头像不显示、聊天时间显示 NaN 和时区差 8 小时等问题。
    • 项目文档:编写项目 README 和前端架构说明。
  • React Native 重写(独立):搭建 Expo + TypeScript 工程和分层结构,完成全部 27 个页面的迁移,并在重写过程中修复了一批接口对接问题。

难点与解决 ​

1. 订单和资金的一致性 ​

问题:原来的订单逻辑只是插入一条订单记录:先提交订单,再单独提交一次操作日志;下单时不扣款,商品状态也不变。这样一来,同一件商品可以被重复下单;而且一旦中间某一步出错,就可能出现订单建了但日志没写上的情况。加上钱包余额之后,问题更明显:扣款、进账、改商品状态这几步只要有一步失败,账就对不上。

解决:在 order_service.py 里把下单、取消、完成三个操作都放进 with session.begin(): 事务里执行,任意一步抛异常就全部回滚:

  • 下单:校验商品是否在售、不能买自己的商品 → 校验买家余额 → 买家扣款、卖家进账 → 商品状态改为 sold → 创建订单 → 写操作日志,全部在同一个事务里。
  • 取消:卖家扣回、买家退款,商品恢复为 on_sale,订单改为已取消并记录原因,同样在一个事务里完成。
  • 完成:订单状态变更和操作日志一起提交。

2. 重写时把前后端接口「对齐」 ​

问题:课程设计阶段的前端有不少地方读的字段和后端实际返回的对不上。因为 JavaScript 读到不存在的字段只会得到 undefined,不会报错,这些问题平时很难发现。重写时逐个对照后端代码,找出了一批:

  • 后端的订单和商品状态都是小写(pending、on_sale),旧页面按大写判断,导致订单的「取消 / 完成」按钮和商品的「重新上架」按钮从来不会出现;
  • 后端分页字段叫 total_pages,旧页面读的是 pages,首页永远只有一页;
  • 收藏列表的结构是 {id, item: {...}},旧页面按 itemId 比较,商品的收藏状态永远显示「未收藏」;
  • 后端的金额是字符串(如 "0.00"),旧页面直接调用 .toFixed(),钱包页一渲染就报错;
  • 后端从不返回 hasReviewed,评价过的订单还一直显示「去评价」;
  • 还有 createTime、imageUrl 这类字段名不一致,导致时间和图片显示不出来。

解决:先按后端代码的真实返回写出 types/api.ts,把它作为前后端的接口约定,页面只能读这里定义过的字段,写错在编译阶段就会报错。缺失的信息用已有接口补齐,比如用 /api/reviews/check 判断是否评价过。后端本身的缺口(没有举报接口、编辑商品时不保存图片、修改密码不校验旧密码)在代码里注明,前端不去假装成功。另外还发现发布表单和搜索筛选用的是两套不同的校区、成色选项,会导致按校区搜不到新发布的商品。这一点先原样保留并做了标注,等确认后再统一。

3. 用 TanStack Query 管理服务端数据 ​

问题:旧前端的数据都存在各个页面自己的变量里,页面之间靠本地存储传值、靠「需要刷新」的标记来通知:比如发布商品后,要设一个标记让首页回来时重新加载;编辑求购前,要先把数据存进本地存储再跳转。页面一多,就很容易漏刷新、读到旧数据。轮询也全靠手写定时器,要自己记得在离开页面时清掉。

解决:接口数据全部交给 TanStack Query 管理:

  • 缓存 key 集中定义,并且按前缀分组:所有商品列表都以 items 开头,所有订单都以 orders 开头。发布商品后执行一次 invalidate(['items']),首页、搜索、我的发布就会一起刷新;下单后同时让订单、商品、钱包的缓存失效。
  • 页面之间直接共享缓存:编辑求购时直接读详情页已经加载过的缓存,订单详情从「我的订单」列表里按 id 取,不再用本地存储传值。
  • 轮询只在需要的时候跑:聊天页只有在前台时才设置 refetchInterval;同时把 React Native 的 AppState 接到 TanStack Query 的 focusManager 上,App 切到后台时所有轮询自动暂停,回到前台时自动刷新。
  • 不自动重试:错误提示已经在请求层统一弹出,查询失败时不再自动重试,避免同一个错误弹好几次。

4. 时间显示差 8 小时 ​

问题:后端返回的时间都没有时区后缀(类似 2026-06-10T02:00:00),JavaScript 的 new Date() 会把它当成设备本地时间来解析。更麻烦的是,聊天模块存的是北京时间,而其他表存的是 UTC,两种时间混在一起。课程设计阶段,聊天页就出现过时间差 8 小时的问题。

解决:在 lib/time.ts 里提供两个解析函数:parseUtc 给没带时区的时间补上 Z,parseBeijing 补上 +08:00,再统一转成时间戳交给各种格式化函数(今天 / 昨天、几分钟前、几周前……)。所有页面都通过这两个函数来解析时间,不再直接调用 new Date(字符串)。

5. 登录过期与文件上传的统一处理 ​

问题:

  • 一个页面往往同时发好几个请求,Token 一过期,就会有好几个请求同时返回 401,每个都去跳一次登录页,结果登录页叠了好几层;
  • 旧版退出登录时只删了用户信息,Token 还留在本地;
  • 上传图片走的是另一条代码路径,容易漏掉 Token 和 401 处理。

解决:

  • 请求层用一个标记位保证多个 401 同时到达时只跳一次登录页;
  • 退出登录时同时清除 SecureStore 里的 Token 和 AsyncStorage 里的用户信息,并清空所有接口缓存,避免下一个账号看到上一个账号的数据;
  • 上传也走同一个 send() 函数,共享鉴权头、超时、401 和错误提示;React Native 的 FormData 要用 {uri, name, type} 的形式附加文件,这一点也封装在 uploadFile 里,页面只需要传入选好的图片。

6. 订单列表缺少商品信息 ​

问题:后端的订单数据只有商品 id,没有商品标题、价格和图片,旧页面读的 itemTitle、price 等字段都不存在,订单列表上显示不出商品信息。

解决:封装了 useOrderGoods(itemId),按商品 id 查商品详情来补齐标题、价格和封面图。因为有缓存,同一件商品只会请求一次。这样做本质上是 N+1 查询,订单多了会多发请求,更好的做法是让后端在订单数据里直接带上商品和用户信息,这一点已经记录下来,作为后端的改进项。

评论
我要评论