简介:本资源是一套基于Spring Boot后端与微信小程序前端的闲置品交易平台完整源码,面向Java初学者、全栈开发学习者及毕业设计需求者,解决二手商品线上发布、浏览、沟通、下单与信用评价等全流程实践问题。压缩包共1273个文件,涵盖123个Java后端业务与配置类、143个Vue组件与225个JS逻辑脚本、279个PNG与82个JPG图片资源、161个SVG图标,以及WXML/WXSS等小程序核心文件,整体21.39MB,结构清晰,前后端分离明确,便于分模块学习与调试。目前已有153人学习下载,适合用于课程设计、毕设参考或小程序+Spring Boot技术栈整合实战。读者可直接运行调试,获得含用户登录、物品发布、搜索筛选、私信沟通、微信支付对接、订单状态跟踪及双向评价等完整功能链路,同时包含3个bat启动脚本与多个.bak备份文件,有助于理解开发迭代过程与关键配置回溯。
1. 为什么用 Spring Boot 搭建微信小程序闲置品交易平台,不是“选型”,而是“必选”
你正在开发一个面向高校学生或社区居民的二手书、旧手机、闲置家具流转平台,用户通过微信小程序拍照发布、在线议价、线下自提——这不是一个“能跑就行”的 Demo,而是要支撑日均 500+ 商品上架、3000+ 用户浏览、并发下单峰值达 200+ 的轻量级交易系统。此时若用传统 SSM(Spring + SpringMVC + MyBatis)手动装配事务、配置数据源、写拦截器鉴权、处理文件上传路径,光是解决跨域、JWT 登录态校验、图片缩略图生成、MySQL 乐观锁防超卖,就可能耗掉两周调试时间。而 Spring Boot 的自动配置能力,让@SpringBootApplication启动类默认加载DataSourceAutoConfiguration、JpaRepositoriesAutoConfiguration、WebMvcAutoConfiguration,配合spring-boot-starter-web、spring-boot-starter-data-jpa、spring-boot-starter-validation三个 starter,5 分钟内就能跑通「用户登录 → 发布商品 → 列表分页查询」最小闭环。它不是为“教学演示”设计的框架,而是为“快速交付可运维、可扩展、可审计的生产级小程序后端”而生——尤其当你的前端是 uni-app 编写的微信小程序,后端必须提供 RESTful 接口、统一异常响应体、标准 HTTP 状态码、支持微信 OpenID 绑定与 Session 复用时,Spring Boot 的四层架构(Controller–Service–Repository–Entity)天然匹配小程序“页面–API–数据库”的调用链路,且@Transactional注解直接保障“发布商品+扣减库存+生成快照”原子性,避免出现“商品已上架但库存未扣减”的脏数据。这正是当前 73% 的微信小程序毕业设计与中小团队商用项目选择 Spring Boot 的底层逻辑:它把“让接口稳定可用”从一项需要反复压测和人工巡检的运维任务,变成了一个可通过application.yml参数控制、通过@Test单元测试覆盖、通过 Actuator 端点实时观测的工程实践。
2. 用 Spring Boot 四层架构搭建闲置品交易核心模块:从 Entity 定义到 Controller 响应
2.1 闲置品交易的核心实体建模与 JPA 映射策略
闲置品交易场景中,关键业务对象不是泛泛的“商品”,而是具备“用户归属、状态流转、图片多张、议价痕迹”的领域实体。以Item(闲置物品)为例,需明确区分User(发布者)、Category(分类)、Image(多图关联)三类主从关系,并规避常见 ORM 坑点:
@Entity @Table(name = "t_item") public class Item { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "title", nullable = false, length = 100) private String title; // 物品标题 @Column(name = "price", nullable = false, precision = 10, scale = 2) private BigDecimal price; // 标价(单位:元) @Column(name = "status", nullable = false, columnDefinition = "TINYINT DEFAULT 1") @Enumerated(EnumType.ORDINAL) private ItemStatus status; // 枚举:1-待售 2-已售出 3-下架 @ManyToOne(fetch = FetchType.LAZY) // 关键:LAZY 防 N+1 查询 @JoinColumn(name = "user_id", nullable = false) private User owner; // 所有者,非级联删除 @ManyToOne(fetch = FetchType.EAGER) // 分类需立即加载,避免额外 SQL @JoinColumn(name = "category_id", nullable = false) private Category category; @OneToMany(mappedBy = "item", cascade = CascadeType.ALL, orphanRemoval = true) @OrderBy("sort_order ASC") // 按序号排序,保障小程序端图片展示顺序 private List<ItemImage> images = new ArrayList<>(); // getter/setter 省略 }提示:
@Enumerated(EnumType.ORDINAL)用于存储枚举序号(如ItemStatus.ON_SALE.ordinal() == 1),比STRING更节省空间且不易受枚举名变更影响;@OrderBy("sort_order ASC")是保障小程序端图片按上传顺序渲染的关键,避免依赖前端排序逻辑。
对应ItemImage实体需独立建表并记录排序字段:
@Entity @Table(name = "t_item_image") public class ItemImage { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "url", nullable = false) private String url; // 微信云存储返回的 HTTPS 地址 @Column(name = "sort_order", nullable = false, columnDefinition = "TINYINT DEFAULT 0") private Integer sortOrder; // 0 表示首图 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "item_id", nullable = false) private Item item; // getter/setter 省略 }2.2 Service 层实现发布与查询逻辑:事务边界与分页优化
发布闲置品需保证“创建 Item + 关联多张图片 + 更新用户发布计数”三步原子性,且图片 URL 来自微信小程序端上传后的cloud://路径(由小程序 SDK 上传至微信云存储后返回)。Service 方法必须包裹完整事务:
@Service @Transactional public class ItemService { @Autowired private ItemRepository itemRepository; @Autowired private UserRepository userRepository; public Item createItem(ItemCreateDTO dto, Long userId) { // 1. 校验分类是否存在 Category category = categoryRepository.findById(dto.getCategoryId()) .orElseThrow(() -> new IllegalArgumentException("分类不存在")); // 2. 创建主实体 Item item = new Item(); item.setTitle(dto.getTitle()); item.setPrice(dto.getPrice()); item.setStatus(ItemStatus.ON_SALE); item.setCategory(category); // 3. 关联用户(注意:不保存 User 实体,仅设置外键) User owner = new User(); owner.setId(userId); item.setOwner(owner); // 4. 保存主实体,获取生成的 ID Item savedItem = itemRepository.save(item); // 5. 批量保存图片(使用 saveAll 提升性能) List<ItemImage> itemImages = dto.getImageUrls().stream() .map(url -> { ItemImage img = new ItemImage(); img.setUrl(url); img.setItem(savedItem); return img; }) .collect(Collectors.toList()); itemImageRepository.saveAll(itemImages); // 6. 更新用户发布总数(使用原生 SQL 避免先查再更新的并发问题) userRepository.incrementPublishedCount(userId); return savedItem; } // 分页查询待售物品,按发布时间倒序,排除已下架项 public Page<Item> findOnSaleItems(Pageable pageable) { return itemRepository.findByStatus(ItemStatus.ON_SALE, pageable); } }参数说明:
Pageable由 Controller 层传入,例如PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdAt"));incrementPublishedCount是自定义 JPQL 更新方法,在UserRepository中声明:@Modifying @Query("UPDATE t_user SET published_count = published_count + 1 WHERE id = :userId") void incrementPublishedCount(@Param("userId") Long userId);—— 此写法绕过 JPA 一级缓存,确保高并发下计数准确。
2.3 Controller 层统一响应与微信 OpenID 绑定验证
小程序前端调用/api/items时,需携带Authorization: Bearer <token>,该 token 由小程序wx.login()获取 code 后,后端调用微信auth.code2Session接口换取openid并签发 JWT。Controller 必须校验 token 有效性,并将openid与userId关联:
@RestController @RequestMapping("/api/items") public class ItemController { @Autowired private ItemService itemService; @PostMapping public ResponseEntity<ApiResponse<Item>> createItem( @Valid @RequestBody ItemCreateDTO dto, @AuthenticationPrincipal JwtUserDetails userDetails) { // 由 SecurityFilterChain 解析 JWT Item created = itemService.createItem(dto, userDetails.getUserId()); return ResponseEntity.ok(ApiResponse.success(created)); } @GetMapping public ResponseEntity<ApiResponse<Page<Item>>> listItems( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); Page<Item> items = itemService.findOnSaleItems(pageable); return ResponseEntity.ok(ApiResponse.success(items)); } }其中ApiResponse<T>是统一响应体,强制包含code、message、data字段,避免小程序端反复解析不同结构:
public class ApiResponse<T> { private int code; private String message; private T data; public static <T> ApiResponse<T> success(T data) { ApiResponse<T> response = new ApiResponse<>(); response.code = 200; response.message = "success"; response.data = data; return response; } // getter/setter 省略 }3. 微信小程序端对接关键细节:从登录态管理到图片上传路径处理
3.1 小程序登录态与 Spring Boot JWT 的双向绑定流程
微信小程序无法直接使用 Cookie,必须依赖AuthorizationHeader 传递 token。完整链路如下:
- 小程序调用
wx.login()获取临时登录凭证code - 小程序将
code发送给 Spring Boot 后端/api/auth/login接口 - 后端用
code请求微信https://api.weixin.qq.com/sns/jscode2session,获得openid和unionid(若绑定公众号) - 后端查询数据库:若
openid已存在,取出对应userId;若不存在,则插入新User记录并生成userId - 使用
io.jsonwebtoken:jjwt-api签发 JWT,payload 包含userId、openid、exp(建议 7 天),密钥存于application.yml
jwt: secret: your-32-byte-secret-key-here-12345678901234567890123456789012 expiration: 604800 # 7 days in seconds对应 Java 配置:
@Component public class JwtTokenProvider { @Value("${jwt.secret}") private String jwtSecret; @Value("${jwt.expiration}") private int jwtExpiration; public String generateToken(Long userId, String openid) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + jwtExpiration * 1000); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("openid", openid) // 存入 openid 便于后续校验 .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, jwtSecret) .compact(); } }注意:
openid必须存入 JWT payload,而非仅存于数据库。因为小程序每次请求只带 token,后端需从中解析openid用于校验用户身份(如禁止删除他人发布的物品),避免每次请求都查库。
3.2 小程序图片上传至微信云存储后,后端如何安全接收并入库
小程序端不能直接将图片二进制上传到 Spring Boot(易触发 OOM),正确做法是:
- 小程序调用
wx.cloud.uploadFile上传至微信云开发环境,获得fileID(如cloud://xxx.png) - 小程序将
fileID作为字符串数组提交给后端/api/items接口 - 后端不操作文件,仅校验
fileID格式(正则^cloud://[a-zA-Z0-9._/-]+$),并存入t_item_image.url字段
关键校验代码:
public class ItemCreateDTO { @NotBlank(message = "标题不能为空") private String title; @NotNull(message = "价格不能为空") @DecimalMin(value = "0.01", message = "价格不能小于0.01") private BigDecimal price; @NotNull(message = "分类ID不能为空") private Long categoryId; @NotEmpty(message = "至少需上传一张图片") @Size(max = 9, message = "最多上传9张图片") private List<String> imageUrls; // 接收 cloud:// 开头的 fileID // getter/setter 省略 }Controller 层添加@Valid注解触发校验,imageUrls中每个 URL 必须匹配微信云存储格式:
@PostMapping public ResponseEntity<ApiResponse<Item>> createItem( @Valid @RequestBody ItemCreateDTO dto, @AuthenticationPrincipal JwtUserDetails userDetails) { // 校验每张图片 URL 是否为合法 cloud:// 路径 for (String url : dto.getImageUrls()) { if (!url.startsWith("cloud://")) { throw new IllegalArgumentException("图片URL必须为微信云存储路径"); } } Item created = itemService.createItem(dto, userDetails.getUserId()); return ResponseEntity.ok(ApiResponse.success(created)); }提示:微信云存储的
fileID可直接在小程序<image>组件中使用,无需后端代理;但若需做防盗链或水印,可在后端调用wx.cloud.downloadFile下载后再处理——本方案默认信任云存储安全性,聚焦业务主干。
3.3 小程序端分页加载与 Spring Boot Pageable 的精准对齐
小程序onReachBottom触发分页时,常因page参数起始值(0 或 1)与后端理解不一致导致漏数据。Spring Boot 默认PageRequest.of(0, 10)表示第 0 页(即第 1 页),共 10 条。小程序需严格按此约定传参:
// 小程序 Page.js data: { items: [], page: 0, // 当前页码,从 0 开始 size: 10, // 每页条数 hasMore: true // 是否还有更多 }, onReachBottom() { if (!this.data.hasMore) return; wx.request({ url: 'https://your-api.com/api/items?page=' + this.data.page + '&size=' + this.data.size, method: 'GET', success: (res) => { const newData = res.data.data.content; this.setData({ items: this.data.items.concat(newData), page: this.data.page + 1, hasMore: newData.length === this.data.size }); } }); }后端ItemController中@RequestParam(defaultValue = "0") int page直接映射,无需额外转换。若小程序坚持用page=1表示第一页,则后端需page - 1,但易引发混淆,强烈建议小程序端统一使用 0-based 分页索引。
4. 生产环境避坑指南:Actuator 安全加固、MyBatis 与 JPA 混用边界、微信支付 v3 对接预备
4.1 Spring Boot Actuator 未授权访问漏洞的强制防护措施
/actuator/env、/actuator/beans等端点若暴露在公网,攻击者可获取数据库密码、密钥等敏感信息。必须禁用高危端点并启用认证:
# application-prod.yml management: endpoints: web: exposure: include: health,info,metrics,prometheus # 仅开放必要端点 endpoint: env: show-values: NEVER # 禁止显示配置值 endpoints: jmx: exposure: include: health security: roles: ACTUATOR # 仅允许 ACTUATOR 角色访问同时,在SecurityConfig中限制/actuator/**路径:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz .requestMatchers("/actuator/**").hasRole("ACTUATOR") // 强制角色校验 .requestMatchers("/api/**").authenticated() .anyRequest().permitAll() ); return http.build(); } }注意:
ACTUATOR角色需在用户登录时注入GrantedAuthority,例如new SimpleGrantedAuthority("ROLE_ACTUATOR"),不可硬编码 admin 密码。
4.2 MyBatis 与 Spring Boot JPA 混用时的事务与缓存冲突处理
项目初期用 JPA 快速迭代,后期因复杂报表查询引入 MyBatis,此时极易出现事务失效或二级缓存错乱。根本解决方案是物理隔离:
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 核心交易(增删改) | 全部使用 JPA Repository | 保证@Transactional在 Service 层生效,避免混合 DAO |
| 复杂统计报表(如“本月各分类成交额”) | 单独 MyBatis Mapper +@MapperScan("com.xxx.mapper.report") | Mapper 接口不参与 JPA 事务,用@Transactional(propagation = Propagation.NOT_SUPPORTED)明确隔离 |
| 全局缓存 | 统一使用@Cacheable+ Redis | 避免 JPA 二级缓存(Hibernate)与 MyBatis 一级缓存共存 |
示例报表 Mapper:
@Mapper @MapperScan("com.example.platform.mapper.report") public interface ReportMapper { @Select("SELECT c.name as categoryName, COUNT(*) as count FROM t_item i " + "JOIN t_category c ON i.category_id = c.id " + "WHERE i.status = 2 AND i.updated_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) " + "GROUP BY c.name") List<CategorySales> findCategorySalesLast30Days(); }调用时显式声明不参与事务:
@Service public class ReportService { @Autowired private ReportMapper reportMapper; @Transactional(propagation = Propagation.NOT_SUPPORTED) public List<CategorySales> getCategorySales() { return reportMapper.findCategorySalesLast30Days(); } }4.3 微信支付 v3 对接的前置准备与沙箱环境验证要点
虽然标题中注明“支付功能暂时无法使用”,但架构设计必须预留支付扩展位。微信支付 v3 要求:
- 证书体系:商户平台下载
apiclient_key.pem(私钥)、apiclient_cert.pem(公钥+证书链),严禁硬编码或放入 Git - 签名机制:所有请求需用私钥生成
AuthorizationHeader,含mchid、nonce_str、timestamp、signature - 回调验签:收到微信服务器
POST /notify时,必须用公钥验证Wechatpay-SignatureHeader
Spring Boot 中推荐使用官方wechatpay-apache-httpclientSDK:
<dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-apache-httpclient</artifactId> <version>0.4.0</version> </dependency>初始化客户端(证书路径从application.yml读取):
@Configuration public class WechatPayConfig { @Value("${wechatpay.cert.path}") private String certPath; @Value("${wechatpay.mchid}") private String mchId; @Bean public ScheduledUpdateCertificates scheduledUpdateCertificates() { return new ScheduledUpdateCertificates( mchId, PemUtil.loadPrivateKey(new FileInputStream(certPath + "/apiclient_key.pem")), PemUtil.loadCertificate(new FileInputStream(certPath + "/apiclient_cert.pem")) ); } }关键提醒:沙箱环境(
https://api.mch.weixin.qq.com/v3/sandbox/...)必须用沙箱mchid和沙箱证书,且notify_url必须是公网可访问地址(如内网穿透),否则回调失败。正式上线前务必完成沙箱全流程测试(下单→通知→查询→退款)。
5. 微信小程序顶部导航栏高度适配与加载页定制:从app.json到uni-app的真实落地
5.1 微信小程序顶部导航栏高度的动态计算与安全区适配
微信小程序真机运行时,iPhone X 及以上机型存在“刘海屏”,顶部导航栏实际高度 ≠px像素值。直接写死height: 44px会导致内容被遮挡。正确做法是:
- 在
app.json中设置"navigationStyle": "custom",隐藏默认导航栏 - 自行实现导航组件,通过
wx.getSystemInfoSync()获取statusBarHeight(状态栏高度)与navigationBarHeight(导航栏高度)之和
// app.json { "window": { "navigationStyle": "custom" } }<!-- components/custom-nav.vue --> <template> <view class="nav-bar" :style="{ 'padding-top': statusBarHeight + 'px' }"> <view class="nav-content"> <text class="nav-title">{{ title }}</text> </view> </view> </template> <script> export default { props: ['title'], data() { return { statusBarHeight: 0 } }, mounted() { const systemInfo = wx.getSystemInfoSync(); this.statusBarHeight = systemInfo.statusBarHeight; } } </script> <style scoped> .nav-bar { width: 100%; height: 88rpx; /* 44px * 2(rpx 基准) */ background-color: #fff; position: fixed; top: 0; z-index: 999; } .nav-content { display: flex; align-items: center; justify-content: center; height: 100%; } .nav-title { font-size: 32rpx; font-weight: bold; } </style>提示:
statusBarHeight在 iOS 上通常为 20px,Android 为 24px;navigationBarHeight固定为 44px,故总高度为statusBarHeight + 44,但rpx单位已自动适配,此处只需动态设置padding-top。
5.2 修改刚进入的加载页面:pages/index/index的骨架屏与预加载策略
小程序冷启动时白屏时间过长,用户流失率陡增。需在index页面实现骨架屏(Skeleton)+ 数据预加载:
<!-- pages/index/index.vue --> <template> <view class="container"> <!-- 骨架屏:仅在 loading 状态显示 --> <view v-if="loading" class="skeleton"> <view class="skeleton-item" v-for="i in 3" :key="i"></view> </view> <!-- 实际内容 --> <scroll-view v-else scroll-y> <view class="item-list"> <block v-for="item in items" :key="item.id"> <navigator :url="'/pages/item/detail?id=' + item.id"> <view class="item-card"> <image :src="item.images[0]?.url" class="item-image" mode="aspectFill"/> <view class="item-info"> <text class="item-title">{{ item.title }}</text> <text class="item-price">¥{{ item.price }}</text> </view> </view> </navigator> </block> </view> </scroll-view> </view> </template> <script> export default { data() { return { items: [], loading: true } }, onShow() { this.fetchItems(); }, methods: { async fetchItems() { this.loading = true; try { const res = await wx.request({ url: 'https://your-api.com/api/items?page=0&size=10', method: 'GET', header: { 'Authorization': 'Bearer ' + wx.getStorageSync('token') || '' } }); if (res.statusCode === 200) { this.items = res.data.data.content; } } catch (e) { console.error('加载失败', e); } finally { this.loading = false; } } } } </script>技巧:
onShow中调用fetchItems,而非onLoad,确保用户从其他页面返回时也能刷新数据;骨架屏使用v-if而非v-show,避免 DOM 冗余;wx.request的header动态读取本地存储的 token,与 Spring Boot JWT 校验无缝衔接。
5.3 uni-app 微信小程序环境下weixin://dl/business跳转链接的合规触发条件
weixin://dl/business是微信内部协议,用于跳转至微信服务号或小程序业务页面,但仅限已备案的主体且需用户主动触发。在 uni-app 中必须满足:
- 调用
uni.openURL('weixin://dl/business?appid=xxx&path=pages/index/index')前,页面必须存在用户手势(如button的@click) button组件需设置open-type="contact"或open-type="navigate"等微信原生类型,uni.openURL本身无权限- 更可靠方式是使用
uni.navigateToMiniProgram跳转至已关联的其他小程序:
uni.navigateToMiniProgram({ appId: 'wx1234567890abcdef', // 目标小程序 AppID path: 'pages/index/index?from=platform', // 传递参数 success: (res) => { console.log('跳转成功'); } });注意:
weixin://dl/business已被微信逐步限制,新项目应优先采用navigateToMiniProgram或openEmbeddedApp(需开通微信支付服务商资质),避免因协议变更导致功能失效。
本文还有配套的精品资源,点击获取