微信小程序校园二手交易平台搭建:SSM框架完整开发实战

微信小程序校园二手交易平台搭建:SSM框架完整开发实战 又是一年毕业季宿舍楼下、公告栏里、QQ群里到处是学长学姐甩卖教材、台灯、自行车的信息。但这种方式太零散了信息发出来几分钟就沉底有价值的东西根本传不到需要的人手里。我当时在做的这个课题就是针对这个真实痛点搭建一个基于微信小程序的校园二手交易平台后端用的SSM框架也就是Spring、SpringMVC、MyBatis这一套经典组合。整个项目带完整文档和源码适合正在做课程设计、毕业设计或者想练手完整前后端项目的同学参考。这个平台解决的核心问题很明确让校园里的闲置物品能在一个封闭、可信的小范围里快速流转。学生用微信扫一扫就能进入小程序不用下载App在宿舍群、班级群里转发一下就能传播这比传统Web网站方便太多。而后端选SSM而不是Spring Boot一方面是因为这是很多高校Java课程的主流教学内容另一方面这套框架足够轻量对于这种规模的项目来说结构清晰、易于理解把Spring的IoC、AOP思想SpringMVC的请求处理流程MyBatis的数据映射机制全部串起来了做完这个项目对Java后端的理解会上一个台阶。这篇文章我会从选型思路、数据库设计、后端接口实现、小程序端页面逻辑、常见问题排查五个维度来完整拆解这个项目全程都是实际开发中会踩到的坑和验证过的方案。1. 项目整体设计与选型思路1.1 为什么用微信小程序而不是App或H5我做这个项目之前最早考虑过两个替代方案一个是纯H5手机网页另一个是Android原生App。H5的方案开发成本确实低但问题在于没有一个天然的流量入口。你做完一个网页用户怎么找到它要么记网址要么扫码进去但下次再用又要重新找入口。而且H5在微信内置浏览器里的体验明显打折底部导航栏、下拉刷新这些交互都需要额外适配更别说调用微信的登录能力了流程会绕一大圈。原生App的体验倒是好但开发周期长、还要上架应用商店对校园场景来说太重了学生不可能为了买个二手台灯专门装一个App。微信小程序完美避开了这两个问题。它在微信生态里天然存在用户用完即走下次想用从聊天记录或者最近使用的小程序里一点就开。对于校园二手交易这个高频但轻量的场景这个体验是最合适的。另外小程序提供了完整的前端开发框架和组件库页面跳转、下拉刷新、图片上传、用户登录这些能力都是现成的开发效率比原生App高一个量级。1.2 SSM框架的选型逻辑后端选择SSM我承认有一点“教学导向”的成分但这不意味着这个选型不合理。Spring负责对象管理和依赖注入Service层和Mapper层的Bean都由Spring容器统一管理这样各层之间的耦合度降到最低。SpringMVC负责HTTP请求的接收和响应Controller层的注解式开发非常直观一个RequestMapping就能完成URL映射。MyBatis负责数据库操作对于这个项目里大量涉及多表关联、动态条件查询的场景比如按分类筛选商品、模糊搜索商品名称MyBatis的动态SQL写起来非常灵活比JPA那种全自动ORM更可控。相较于Spring BootSSM确实需要手动写更多配置文件Web.xml、Spring配置、SpringMVC配置、MyBatis配置每一处都要自己配。但正因为这样整个请求处理链路才会清晰可见对理解Java Web项目的运行机制帮助很大。如果你已经掌握了SSM再去写Spring Boot就是降维打击。1.3 角色权限与核心模块拆解整个系统的用户角色分两种普通用户和管理员权限边界很清晰。普通用户的核心操作路径是注册/登录、浏览首页商品列表、按分类筛选、搜索商品、查看商品详情、收藏商品、留言咨询、发布商品、管理自己发布的商品编辑、下架、标记为已售、查看收藏列表。管理员的职责是维护平台秩序管理用户账号查看详情、禁用、管理商品分类新增、编辑、删除、审核商品对违规商品做下架处理、查看平台的基本运营数据用户总数、商品总数等。从模块化角度看整个项目可以拆成五个核心模块用户模块、商品模块、分类模块、收藏模块、留言模块。用户和商品是核心实体收藏和留言是围绕商品展开的互动行为分类是商品的维度标签。这种模块划分方式无论是数据库设计还是后端接口设计都遵循“高内聚低耦合”的原则每个模块的开发可以独立推进、单独测试。2. 数据库设计与核心表结构2.1 整体的ER关系梳理数据库设计是这类项目的地基我之前见过有同学直接在上面堆字段结果后面改接口时改到怀疑人生。正确的做法是先把实体关系理顺。这个项目的实体关系其实很清晰用户User和商品Goods是一对多关系一个用户能发布多件商品。分类Category和商品是多对一关系一件商品只属于一个分类一个分类下有多件商品。用户User和商品Goods通过收藏表Favorite形成多对多关系一个用户可以收藏多件商品一件商品可以被多个用户收藏。商品Goods和留言Message是一对多关系一件商品下可以有多条留言。顺着这个ER关系去建表逻辑上就不会乱。实际开发中我还加了两张辅助表一张是订单相关的记录这个项目以线下交易为主所以订单表做了简化只记录状态另一张是管理员的操作日志表可能有点冗余但上线有审计需求时你就知道这个表有多香。2.2 关键核心表结构与设计说明用户表user字段名类型说明idint主键自增openidvarchar(64)微信用户的唯一标识后端登录时拉取nicknamevarchar(50)昵称默认为“微信用户”avatarvarchar(255)头像URLphonevarchar(20)联系电话发布商品时需要roletinyint角色0-普通用户1-管理员statustinyint状态0-正常1-禁用create_timedatetime注册时间openid是这张表的灵魂。微信后台会为每个小程序、每个微信用户生成一个唯一的openid这就是我们的天然用户ID不需要额外做账号密码体系。商品表goods字段名类型说明idint主键自增user_idint发布者ID关联user表category_idint分类ID关联category表titlevarchar(100)商品标题descriptiontext商品描述pricedecimal(10,2)价格original_pricedecimal(10,2)原价用于显示折扣imagesvarchar(1000)商品图片多张用逗号分隔statustinyint状态0-在售1-已售2-下架view_countint浏览量create_timedatetime发布时间这里有个很关键的细节多张图片在数据表里怎么存如果你非要拆一张子表去存那查询商品列表时就要多一次联表查询性能反而差。实际项目里我用逗号分隔的字符串存储查询时直接取出来按逗号split前端循环渲染就行。这种一定程度的反规范化在数据量不大的场景下是最实用的。分类表category字段名类型说明idint主键namevarchar(50)分类名称sortint排序权重create_timedatetime创建时间分类表很简单实际可以预置一些常见分类教材书籍、数码电器、生活用品、运动器材、服饰美妆、其他。管理员可以在后台管理这些分类前台首页的分类栏直接按sort排序循环输出。收藏表favorite字段名类型说明idint主键user_idint用户IDgoods_idint商品IDcreate_timedatetime收藏时间这里有个容易忽略的点user_id和goods_id必须加联合唯一索引。不然用户手快连续点两次收藏就会插入两条重复记录列表页就会出现两行一模一样的收藏项。加了唯一索引后我直接在Service层做insert ignore重复点击也不会报错这个设计非常省心。2.3 索引设计与查询优化细节数据量小的时候索引看不出差别但一旦商品数据超过几千条没有索引的SQL就会明显变慢。我们在商品表上建三个核心索引idx_goods_categorycategory_id支撑首页按分类筛选。idx_goods_statusstatus支撑“在售/已售/下架”的状态过滤。idx_goods_create_timecreate_time支撑最新商品排序。用户表上针对openid建唯一索引这是登录查询的高频SQL每次用户进入小程序都要根据openid查一次用户信息这个索引必须加。收藏表上user_id和goods_id的联合唯一索引既是约束也是查询加速器。另外浏览量的更新策略我实际用了“延迟更新”的思路。用户在详情页每次刷新都把view_count加1的话会带来频繁的写操作。我的做法是前端离开详情页时上报一次浏览记录后端接口统一做update这样既保证了数据基本准确又限制了一次进详情页只写一次数据库。3. 后端SSM核心实现3.1 SSM三大配置文件的搭建细节我在做SSM配置时发现很多同学的困难不是代码看不懂而是配置文件的加载顺序和职责划分搞不清楚导致项目一启动就报各种Bean找不到、URL映射404的错。正确的方式是梳理好Web.xml这个总入口!-- web.xml -- web-app !-- 1. 加载Spring容器 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- 2. 配置SpringMVC前端控制器 -- servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping !-- 3. 字符编码过滤器解决中文乱码 -- filter filter-nameencoding/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter /web-app这里要注意一个非常坑的点Spring容器和SpringMVC容器是父子容器关系。SpringMVC容器只扫描ControllerSpring容器只扫描Service和Mapper。如果你把Service扫描也写进了SpringMVC的配置大概率会出现事务失效的情况因为Service层的Bean被两个容器各实例化了一次。我的配置里严格区分了扫描范围spring-mvc.xml里只配context:component-scan base-packagecom.campus.controller/applicationContext.xml里配Service、Mapper的扫描这个约定一定要记住。spring-mvc.xml里还有两个核心配置一个是注解驱动mvc:annotation-driven/这是RequestMappingHandlerMapping和RequestMappingHandlerAdapter的开关另一个是静态资源映射如果你把商品图片存在了项目根目录的upload文件夹必须配置mvc:resources mapping/upload/** location/upload//不然浏览器访问图片会返回404。MyBatis的配置相对简单核心是驼峰映射和日志configuration settings setting namemapUnderscoreToCamelCase valuetrue/ setting namelogImpl valueSTDOUT_LOGGING/ /settings /configuration开启驼峰映射后数据库的create_time字段就能自动映射到JavaBean的createTime属性否则你要在ResultMap里写一大堆字段映射完全没必要。3.2 微信登录接口的原理与实现这是全项目最关键的一个接口前端所有需要用户身份的请求都依赖它。微信小程序的登录机制是这样的小程序端调用wx.login()拿到一个临时code把code传给后端后端拿着code加上小程序的AppID和AppSecret去请求微信的接口获取openid和session_key。code的有效期只有5分钟且只能使用一次。后端核心代码如下// WxLoginController.java RestController RequestMapping(/api/user) public class WxLoginController { Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody MapString, String params) { String code params.get(code); // 两个关键参数从配置文件读取 String appId config.getAppId(); String appSecret config.getAppSecret(); // 请求微信接口获取openid String url https://api.weixin.qq.com/sns/jscode2session?appid appId secret appSecret js_code code grant_typeauthorization_code; String result HttpUtil.get(url); JSONObject json JSON.parseObject(result); String openid json.getString(openid); if (StringUtils.isEmpty(openid)) { return Result.error(登录失败); } // 查询用户是否存在不存在则自动注册 User user userService.findByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(微信用户 openid.substring(openid.length() - 6)); user.setRole(0); user.setStatus(0); userService.register(user); } // 生成自定义token这里我用UUID实际可存Redis String token UUID.randomUUID().toString().replace(-, ); // 返回给前端 MapString, Object data new HashMap(); data.put(token, token); data.put(userInfo, user); return Result.success(data); } }关于openid的获取我一直用HttpUtil直接调用微信API没有引入微信官方SDK因为这个项目只用到了登录这一个接口引入SDK反而增加了依赖复杂度。Token生成后前端每次请求都会在Header里带上token后端用拦截器统一校验。这个项目没有上Redistoken就存在内存Map里项目重启后token会失效用户需要重新登录。这在开发演示阶段完全够用但如果要上线建议换成Redis并设置过期时间。3.3 商品发布与图片上传实现商品发布是整个项目的核心业务流涉及图片上传和商品信息入库两个动作。小程序的图片上传机制和普通form表单不一样必须用wx.uploadFile而后端对应的是MultipartFile接收。这里有一个重要的经验小程序端要先把图片传到服务器拿到图片URL后再带着URL去提交商品表单。不要试图在一次请求里同时传图片和JSON参数那样会在HTTP协议层面绕弯子服务端解析也会变得非常麻烦。正确的流程是前端调用wx.chooseMedia选图。对选中的图片循环调用wx.uploadFile上传接口返回图片访问路径。把本地临时路径替换为服务器路径多个路径用逗号拼接。再调用商品保存接口把商品信息带着图片路径列表一起传给后端。后端上传接口// FileUploadController.java PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件不能为空); } // 获取原始文件名后缀 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); // 重新生成文件名避免中文、特殊字符导致的乱码问题 String newFileName System.currentTimeMillis() _ UUID.randomUUID().toString().substring(0, 8) ext; // 存储路径项目根目录/upload/日期/ String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); String realPath upload/ datePath /; File dir new File(realPath); if (!dir.exists()) { dir.mkdirs(); } try { file.transferTo(new File(realPath newFileName)); String visitPath /upload/ datePath / newFileName; return Result.success(visitPath); } catch (IOException e) { e.printStackTrace(); return Result.error(上传失败); } }我在存储路径上做了日期目录的划分这样商品图片在服务器文件系统里就不会全部堆在一起管理起来方便很多排查某一天上传的违规图片时也能快速定位到目录。商品保存接口的Service层我加了事务控制Transactional(rollbackFor Exception.class) public int addGoods(Goods goods) { goods.setStatus(0); goods.setViewCount(0); goods.setCreateTime(new Date()); return goodsMapper.insert(goods); }多张图片路径已经在本方法之前上传并拼接好了入库时就是一个带逗号的字符串。这里有个细节上传图片成功后如果用户放弃了发布那服务器上会残留一张无主的图片。我的处理是上传接口先不管等商品保存失败时再把已经上传的图片文件删除保持数据一致性。3.4 MyBatis动态SQL的灵活应用商品列表接口是查询逻辑最复杂的部分因为它要支持按分类筛选、按关键字搜索、按状态过滤、分页、排序而且这些条件还是组合出现的。如果不用MyBatis的动态SQL你会写出一堆穷举组合的SQL语句维护起来真的是噩梦。我用where标签配合if标签轻松搞定select idsearchGoods resultTypecom.campus.entity.Goods SELECT g.*, u.nickname AS publisherName, u.avatar AS publisherAvatar, c.name AS categoryName FROM goods g LEFT JOIN user u ON g.user_id u.id LEFT JOIN category c ON g.category_id c.id where if testkeyword ! null and keyword ! AND g.title LIKE CONCAT(%, #{keyword}, %) /if if testcategoryId ! null AND g.category_id #{categoryId} /if if teststatus ! null AND g.status #{status} /if /where ORDER BY choose when testsort price_asc g.price ASC /when when testsort price_desc g.price DESC /when otherwise g.create_time DESC /otherwise /choose LIMIT #{offset}, #{pageSize} /select这段SQL同时关联了用户表和分类表一次性把商品、发布者昵称头像、分类名称查出来前端就不需要再发额外的请求去查用户信息和分类名称了这是一个很实用的性能优化技巧。另外注意排序字段用了choose而不是直接把前端参数拼进ORDER BY这是为了防止SQL注入——排序字段没法用预编译参数所以必须做白名单校验。4. 小程序端核心实现4.1 项目目录结构与全局配置小程序端的目录结构遵循了微信官方的约定miniprogram/ ├── app.js # 全局逻辑初始化登录态 ├── app.json # 全局配置页面路由、窗口样式、tabBar ├── app.wxss # 全局样式 ├── utils/ │ └── request.js # 封装的请求工具 ├── pages/ │ ├── index/ # 首页 │ ├── category/ # 分类页 │ ├── goods-detail/ # 商品详情 │ ├── publish/ # 发布商品 │ ├── message/ # 留言咨询 │ ├── favorite/ # 我的收藏 │ ├── user/ # 个人中心 │ └── mine/ # 我发布的商品 ├── components/ │ ├── goods-card/ # 商品卡片组件 │ └── empty/ # 空状态组件 └── static/ └── images/ # 静态图片资源app.json里配置了底部tabBar四个入口分别对应首页、分类页、发布入口按钮这个是个人中心的快捷入口真实交互上tabBar的按钮指向个人中心、消息和个人中心。tabBar的icon我用的线上图片地址但实际操作中建议大家把icon放到本地static目录因为线上地址一旦失效tabBar上就会显示一个难看的裂图。全局请求封装是这个小程序项目的基础设施我把所有的公共逻辑都放在request.js里// utils/request.js const BASE_URL http://localhost:8080; // 开发环境地址 function request(url, method GET, data {}) { return new Promise((resolve, reject) { const token wx.getStorageSync(token); wx.request({ url: BASE_URL url, method: method, data: data, header: { Content-Type: application/json, token: token || }, success: (res) { // 业务状态码统一处理 if (res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { // token失效重新登录 wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(res.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };封装好之后每个页面只需调用request(/api/goods/list, GET, {categoryId: 1})就能拿到数据错误处理和登录态失效拦截都在统一层做了页面代码非常干净。4.2 小程序登录流程与全局用户态管理小程序的登录逻辑在app.js的onLaunch里启动// app.js App({ onLaunch: function () { this.login(); }, login: function () { const that this; return new Promise((resolve, reject) { wx.login({ success: (res) { if (res.code) { request(/api/user/login, POST, { code: res.code }).then((data) { wx.setStorageSync(token, data.token); wx.setStorageSync(userInfo, data.userInfo); that.globalData.userInfo data.userInfo; resolve(data); }).catch(reject); } else { console.error(登录失败, res); reject(res); } } }); }); } });这里有一个体验优化的细节登录成功后不强制跳转到任何页面用户直接停留在当前页面登录态在后台默默完成。因为很多用户只是随便逛逛二手市场并不想一进来就被强制登录。只有当用户点击“发布商品”或者“收藏”时才检查是否存在token如果不存在就引导去登录。实际使用中微信用户可能多次点击登录按钮所以登录必须做幂等处理。后端每次根据code换取openid时如果用户在数据库里已存在就不重复插入而是直接返回已有用户信息这也是为什么我在登录接口里做了“先查后插”的判断。4.3 首页商品列表、分类筛选与分页加载首页是小程序的流量入口我把它做成了四块顶部的搜索框、分类导航栏、商品瀑布流列表、加载状态提示。商品列表的动态更新有两个关键事件// pages/index/index.js Page({ data: { categories: [], goodsList: [], currentCategory: 0, // 0表示全部 keyword: , pageNum: 1, pageSize: 10, hasMore: true, loading: false }, // 下拉刷新 onPullDownRefresh: function () { this.setData({ pageNum: 1, goodsList: [], hasMore: true }); this.loadGoods().then(() { wx.stopPullDownRefresh(); }); }, // 触底加载更多 onReachBottom: function () { if (this.data.hasMore !this.data.loading) { this.setData({ pageNum: this.data.pageNum 1 }); this.loadGoods(); } }, loadGoods: function () { const that this; that.setData({ loading: true }); const params { pageNum: that.data.pageNum, pageSize: that.data.pageSize, categoryId: that.data.currentCategory 0 ? null : that.data.currentCategory, keyword: that.data.keyword || null }; return request(/api/goods/list, GET, params).then((res) { const list res.list || []; that.setData({ goodsList: that.data.goodsList.concat(list), hasMore: that.data.goodsList.length list.length res.total, loading: false }); }).catch(() { that.setData({ loading: false }); }); } });分页这里有一个新手常犯的错误后端返回的在售商品总数是固定的但前端在拼接列表时如果不去重会在某些边界情况下出现重复数据。我的处理是后端在做分页查询时用create_time DESC, id DESC双重排序保证每次分页顺序完全稳定这样就不会因为新增商品导致分页数据错位。4.4 发布页面与个人中心的功能联动发布商品页面的表单字段有商品标题、分类选择、价格、原价选填、描述、图片。在模拟器上用户体验还行但真机上有一个问题在模拟器上wx.chooseMedia会自动从相册选择图片但在真机上系统会弹窗询问是否允许使用相机和相册权限这是微信的统一规范无法跳过。图片选择环节我做了最多6张的限制实际开发中可以设置count: 6wx.chooseMedia({ count: 6 - this.data.images.length, mediaType: [image], success: (res) { const tempFiles res.tempFiles; // 每张图片循环调用上传接口 const uploadTasks tempFiles.map((file) this.uploadImage(file.tempFilePath)); Promise.all(uploadTasks).then((urls) { this.setData({ images: this.data.images.concat(urls) }); }); } });个人中心页面的模块设计则要突出“交易闭环”。我把它设计成了三块用户信息卡片头像、昵称、联系方式、交易入口列表我发布的、我卖出的、我买到的、我的收藏、系统功能列表意见反馈、关于平台、退出登录。其中“我发布的”页面支持对商品进行编辑和下架操作下架后商品从首页消失但数据保留在数据库中状态变为2管理员和用户本人能看到。5. 常见问题与排查技巧实录5.1 高频问题速查表问题现象可能原因解决方案真机预览白屏域名不合法或未配置开发者工具勾选“不校验合法域名”上线前配置request合法域名手机显示网络异常后端服务没跑或IP不对确保后端监听0.0.0.0前端BASE_URL改为局域网IP发布商品图片裂图图片URL是本地路径无法访问检查静态资源配置mvc:resources确认图片存在登录成功但获取用户信息失败openid不一致检查AppID是否同一个同一小程序openid才一致商品列表加载很慢全表扫描无索引按2.3节为商品表、用户表加索引页面提交后中文乱码编码过滤器没配或配置位置不对Web.xml里加CharacterEncodingFilter且放在过滤器链最前面下拉刷新无效页面没有开启enablePullDownRefreshapp.json或页面json中设置enablePullDownRefresh: true5.2 真机预览白屏和网络请求失败这是小程序开发中遇到最多的一个坑我在做这个项目时也一度被搞到怀疑人生。模拟器上一切正常一扫码真机就白屏控制台报错net::ERR_CONNECTION_REFUSED或者not found。原因有两个层面。第一是域名问题。微信小程序要求所有网络请求必须走HTTPS且域名必须在小程序后台配置为request合法域名。开发阶段为了省事开发者工具里可以勾选“不校验合法域名”选项但真机上没有这个选项。解决方法是开发阶段使用局域网IP访问本机后端但记得在手机和电脑连同一个Wi-Fi的前提下把BASE_URL从localhost改成电脑的局域网IP比如http://192.168.1.100:8080。真机上就能正常请求了。第二个看不见摸不着但常见的坑是SpringMVC默认不允许跨域。虽然小程序端的请求不算浏览器跨域请求但在真机调试时如果走了WebView某些特殊逻辑还是可能触发跨域问题。稳妥起见在后端加一个全局的CORS配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }5.3 Tomcat图片上传大小限制我实际测试时上传一张手机拍的照片大约3到5MB到后端直接报了MaxUploadSizeExceededException。排查后发现是SpringMVC默认的上传大小限制是1MB。解决办法是在spring-mvc.xml里配置multipart解析器bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namedefaultEncoding valueUTF-8/ property namemaxUploadSize value10485760/ !-- 10MB -- property namemaxUploadSizePerFile value5242880/ !-- 单个文件5MB -- /bean但这里要注意微信小程序端的上传限制是单张图片最大10MB我们服务端如果限制为5MB高频发生的真实场景是用户选了一张高清原图上传失败但前端没有提示用户误以为发布成功了结果商品图片是空的。我推荐的做法是前端在wx.chooseMedia选完图后先压缩图片再上传wx.compressImage({ src: file.tempFilePath, quality: 80, // 压缩质量 success: (res) { // 用压缩后的路径上传 } });这样既能减小流量消耗又能规避服务端大小限制问题。压缩后的图片质量对二手交易展示完全没有影响。5.4 时间字段的时区与格式化问题数据库datetime类型存储的是本地时间但Java后端返回给前端的JSON默认序列化格式是yyyy-MM-dd HH:mm:ss还是时间戳取决于Jackson的配置。我在实际项目中统一配置成中国时区的标准格式Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder - { builder.simpleDateFormat(yyyy-MM-dd HH:mm:ss); builder.timeZone(TimeZone.getTimeZone(GMT8)); }; } }不配这个的话前端拿到的可能是2024-06-01T08:00:00.00000:00这种格式直接显示在页面上非常奇怪。前端再配合一个相对时间的转换比如“3分钟前发布”体验会好很多。5.5 商品状态流转与缓存一致性商品从发布到交易的完整状态流是在售0→ 已售1或下架2。这里的核心边界情况是当卖家把商品标记为“已售”时其他用户如果刚好在浏览详情页并点击“收藏”应该给一个友好的提示而不是直接报错。我的做法是在收藏接口里先查询商品的status如果不是在售状态直接返回“该商品已下架或已出售”。因为商品列表没有做缓存每次刷新都是从数据库实时查询所以状态更新后用户重新进入首页就能看到最新状态不存在缓存一致性问题。如果以后数据量大了要给首页列表加Redis缓存到时候要注意商品状态变更后主动删除缓存不然用户会看到已售商品还挂在首页上。6. 这个项目还能怎么演化如果你打算在这个项目基础上继续做深我个人觉得有两个很有价值的方向。第一个方向是增加“订单交易”流程。目前的线下交易模式比较原始加一个订单表之后用户可以在线约定面交时间、地点、生成交易码实际见面后由卖家扫码确认交易完成这样双方的交易安全性会提高很多平台也可以做一些信用评价体系。第二个方向是引入消息推送。微信小程序提供了订阅消息能力用户收藏的商品有降价、被其他用户留言时可以给收藏者推送一条模板消息。这个功能开发量不大但对用户体验的提升是巨大的因为二手交易很大程度上靠“抢”及时的通知能显著提高成交率。我把这个项目的源码、数据库脚本、部署文档都整理在项目包里了如果按着这篇文章的顺序一步步做一个完整的前后端分离项目基本能搭起来。最想强调的还是那句话做一个项目数据表设计不要图省事配置文件的加载顺序一定要搞清楚联调阶段把日志打详细一点遇到报错先看控制台而不是凭感觉乱猜。把这些基本素养练好了你后面写什么项目都会顺手很多。