简介基于 SpringBoot 与 Uniapp 构建的前后端分离校园社区项目覆盖校园集市、表白墙、论坛、失物招领、校园墙与跳蚤市场等多元场景适合用于课程设计、毕业设计或前后端分离开发实践。项目后端集成 MyBatisPlus、SpringSecurity、Quartz、MinIO、Redis 与 MongoDB 等技术前端使用 Uniapp 搭配 uView 完成跨端页面搭建从用户认证、帖子发布、文件上传到定时任务调度展示了较完整的业务闭环前后端分离、接口联调与权限控制的设计思路也适合开发者逐层拆解学习。资源共 564 个文件以 Java、Vue、JavaScript 为主同时包含 SQL 数据库脚本和少量配置文件压缩包整体约 1.85MB目录按照业务模块划分检索与部署都比较方便。整体代码结构清晰注释与配置相对完整便于理解后端分层、路由与数据交互方式。目前已有 75 人学习下载获取后可同时得到完整项目源代码与数据库文件既能直接部署运行也可作为校园社区类系统的二次开发底座。1. 基于 SpringBoot Uniapp 的前后端分离校园圈先别急着解压源代码把交付物拆清楚做校园圈这类项目最容易踩的坑是拿到“基于 SpringBoot Uniapp 的前后端分离校园圈(校园集市/校园表白墙/校园论坛/失物招领/校园墙/跳骚市场)项目源代码数据库”后先打开前端再启动后端结果一登录就报错。六个模块看起来多剥掉壳说白了就是一套内容发布系统加一套用户体系难点不在功能多而在于前后端分离后的跨域、登录态、文件上传和多端打包。这篇按我二次开发同类项目的习惯从数据库设计、后端接口、Uniapp 前端到部署验证走一遍顺手把每一步的坑标出来。适合要快速交付完整可用版本的开发者也适合评估这个校园方向到底值不值得投入。2. 数据库与接口设计先行六个业务模块收敛成五张核心表加一个 type2.1 为什么这个项目必须前后端分离一句话后端只出接口前端只管展示。校园集市、表白墙、论坛、失物招领、校园墙、跳蚤市场标题里的“跳骚”是常见同音误写这六个入口要同时出现在微信小程序、H5、安卓 App 里。如果不做前后端分离每一端都要重新写一套后端逻辑等于把同一个查询翻译三遍。用 SpringBoot 做 REST APIUniapp 做多端壳一套接口服务多端这才是这个选型的价值。很多人会想到若依框架前后端分离那套现成脚手架。若依的代码规范适合中后台但把它的用户、角色、菜单模型套到校园圈这种前台内容产品上要先拆掉一堆东西。我一般不用完整若依但会借它的思路Controller 只做参数和响应包装Service 管业务Mapper 管 SQL。这样以后把 PostMapper 换成别的数据源实现前端不需要知道后端换了什么。前后端分离最容易被忽略的是“契约”。前端关心的不是 SQL而是接口返回什么 JSON。所以设计数据表之前先把接口入参、出参定下来。响应类要么只有 code、msg、data 三个字段要么把分页也包进 data。别在 Controller 里直接返回 HashMap否则前端同学每天都在猜字段名。2.2 数据库核心表设计t_post 一张主表吃下五个业务我的做法是只用五张核心表用户表、内容主表、评论表、点赞表、举报表。集市、表白墙、论坛、失物招领、校园墙全部落到 t_post 里靠一个 biz_type 字段区分。下面是可直接导入 MySQL 的建表脚本DROP TABLE IF EXISTS t_report; DROP TABLE IF EXISTS t_like; DROP TABLE IF EXISTS t_comment; DROP TABLE IF EXISTS t_post; DROP TABLE IF EXISTS t_user; CREATE TABLE t_user ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(64) NOT NULL, password VARCHAR(128) NOT NULL DEFAULT , nickname VARCHAR(64) NOT NULL DEFAULT , avatar VARCHAR(255) NOT NULL DEFAULT , phone VARCHAR(20) NOT NULL DEFAULT , wx_openid VARCHAR(64) NOT NULL DEFAULT , status TINYINT NOT NULL DEFAULT 1, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username), UNIQUE KEY uk_wx_openid (wx_openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE t_post ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, biz_type TINYINT NOT NULL COMMENT 1集市 2表白墙 3论坛 4失物招领 5校园墙, title VARCHAR(128) NOT NULL DEFAULT , content TEXT, images VARCHAR(2000) NOT NULL DEFAULT COMMENT 多张图片用英文逗号分隔, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, contact VARCHAR(128) NOT NULL DEFAULT , status TINYINT NOT NULL DEFAULT 1 COMMENT 1展示 0下架 2已成交/已认领, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_biz_status_time (biz_type, status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT内容主表; CREATE TABLE t_comment ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, post_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL, parent_id BIGINT UNSIGNED NOT NULL DEFAULT 0, content VARCHAR(500) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_post_id (post_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT评论表; CREATE TABLE t_like ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, post_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_post_user (post_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT点赞表; CREATE TABLE t_report ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, post_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL, reason VARCHAR(255) NOT NULL DEFAULT , status TINYINT NOT NULL DEFAULT 1, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT举报表;biz_type 是这个设计的灵魂。别为集市商品建 t_goods为表白墙建 t_confession为论坛建 t_topic。否则“查我发布过的所有内容”要 join 三张表统计校园圈活跃度也没法一次 SQL 算完。images 字段建议直接存 JSON 数组字符串而不是逗号分隔。因为商品图要做多图删除Jackson 序列化后可以直接对应前端数组。price、contact 只对集市和失物招领有意义但空字符串和 0 也不占多少空间换来的是列表查询不用 UNION 多张表。联合索引 idx_biz_status_time 是列表页的命脉。前端点“集市”时SQL 条件就是 biz_type1 and status1 order by create_time desc这个联合索引恰好覆盖。别只给 biz_type 单独建索引然后 order by 走内存排序数据到几万条就能明显感觉到慢。这个建表脚本里没有外键是故意的。外键在数据量上来后会变成写锁的定时炸弹而且校园圈业务删除基本是逻辑删除或软下架外键根本用不上。表之间的关联靠应用层保证t_comment 里的 post_id 如果不存在Service 层先查出 post 再写评论就行。2.3 接入 MySQL连接池和时区配置必须在启动前改好下载源代码包后启动后端常见的第一步错误是数据库连不上报Access denied for user或Communications link failure。大半不是依赖没装而是 application.yml 里的 springboot配置没改对。这里给一份适合校园圈初始化阶段的最小配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_circle?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这段配置有四个常见坑。serverTimezoneAsia/Shanghai必须加。MySQL 8 默认时区是 UTC不加的话 Java 读到的时间会差 8 小时表白墙发布时间的排序会乱。allowPublicKeyRetrievaltrue是给 MySQL 8 的很多开发机只配了 url、username、password结果连接池校验时报错原因就是Public Key Retrieval is not allowed。连接池我用 HikariMySQL 的数据库连接池不用再引 dbcp 或 c3p0Hikari 是 SpringBoot 2.x 默认实现。maximum-pool-size 设成 20校园圈日活几百到几千这种量级够用。别照抄老项目里的 maxActive100那只会让一个炸掉的接口连带打垮 MySQL。logic-delete-field需要每张表有 deleted 字段如果数据库脚本没这个字段启动时实体映射会报错。项目起步时可以先注释掉这一项弄清楚逻辑删除和唯一索引的冲突后再打开这点后面避坑章节会展开。检查数据库能不能连最快的方法不是直接启动整个 SpringBoot 应用。先看端口有没有被占用再用数据库客户端直接连一次。用 Navicat 或 dbx 数据库管理工具执行建表脚本确认 campus_circle 库存在再启动后端。把这一步固定进流程能省掉大量“启动十分钟报错看半天”的时间。3. SpringBoot 后端落地登录鉴权、发布帖子和分页列表三个接口串起来3.1 项目目录与依赖不要所有代码塞一个 Controller二次开发别人源码第一件事先看目录。常见分层是这样com.campus ├── CampusApplication.java ├── common/ │ ├── R.java │ ├── TokenUtil.java │ └── GlobalExceptionHandler.java ├── config/ │ ├── LoginInterceptor.java │ ├── WebConfig.java │ └── MybatisPlusConfig.java ├── controller/ │ ├── UserController.java │ ├── PostController.java │ └── UploadController.java ├── service/ │ ├── PostService.java │ └── impl/PostServiceImpl.java ├── mapper/ │ ├── PostMapper.java │ └── UserMapper.java ├── entity/ │ ├── User.java │ └── Post.java └── dto/ ├── LoginDTO.java ├── PostQueryDTO.java └── PostCreateDTO.java依赖最小集合版本交给 SpringBoot 的 parent 统一管理dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId /dependency我一般不引 Spring Security校园圈没有复杂的角色模型。登录用 JWT拦截器统一校验代码量比若依框架里那套 Security 配置少一半。如果后续要上后台管理再引入 Security 不迟前台内容服务引它纯粹是给自己找麻烦。3.2 JWT 登录与拦截器写三个接口前先锁住所有接口登录接口只做一件事校验用户名密码签发 token。RestController RequestMapping(/api/user) public class UserController { PostMapping(/login) public R login(RequestBody LoginDTO dto) { User user userService.login(dto); String token TokenUtil.createToken(user.getId(), user.getNickname()); return R.ok(token); } }TokenUtil 里的密钥放在 application.yml通过 Value 注入。登录成功后前端每次请求在 header 带Authorization: token。拦截器统一校验Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } if (!(handler instanceof HandlerMethod)) { return true; } String token request.getHeader(Authorization); if (token ! null TokenUtil.validate(token)) { return true; } response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\登录已过期\,\data\:null}); return false; } }放行 OPTIONS 请求是必须的。跨域预检请求不带 Authorization如果拦截器先执行预检会被挡成 401前端看到的就是一堆 CORS 报错。鉴权收敛到拦截器是前后端分离项目实战里的通用做法Controller 里再写 token 校验就是多余。如果你拿到的是现成 jar 包而不是完整源代码可以先反编译看拦截器部分再回源码核对。反编译能帮你理解实现但真正交付还是把 JWT 逻辑自己写一遍。密钥和过期时间在 jar 包里有默认值不反编译根本不知道默认密钥是什么。3.3 发布帖子、图片上传与文件存储切换发布接口RestController RequestMapping(/api/post) public class PostController { PostMapping(/create) public R create(RequestBody PostCreateDTO dto, HttpServletRequest request) { Long userId TokenUtil.getUserId(request.getHeader(Authorization)); postService.create(dto, userId); return R.ok(); } }create 方法里要校验 biz_type 是否在合法枚举范围内再把 images 数组存进 t_post.images。失物招领初始状态应该是“寻找中”status 先给 1不要默认成已认领。图片上传接口开发阶段最简单的方式是存本地磁盘RestController RequestMapping(/api/upload) public class UploadController { Value(${file.upload-dir:./upload}) private String uploadDir; PostMapping(/image) public R uploadImage(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return R.error(请选择文件); } String ext StringUtils.getFilenameExtension(file.getOriginalFilename()); String filename UUID.randomUUID() . ext; File dest new File(uploadDir, filename); file.transferTo(dest); return R.ok(/upload/ filename); } }“上传到本地磁盘”在开发阶段没问题部署到公网后一般会把文件切到 MinIO。我习惯先定义一个 FileStorageService 接口本地磁盘和 MinIO 各写一个实现用配置切换而不是在 Controller 里到处写 FileOutputStream。MinIO 加入 SpringBoot 的常见写法是引入 minio-java SDK注册 MinioClient Bean上传时 bucket 不存在就自动创建。这样以后换对象存储前端接口不变。3.4 MyBatis-Plus 分页插件的用法列表页后端主干校园圈五个模块都有列表页分页逻辑必须统一。先加分页配置Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(100L); interceptor.addInnerInterceptor(pagination); return interceptor; } }列表查询public PagePost pageByType(PostQueryDTO query) { LambdaQueryWrapperPost wrapper new LambdaQueryWrapper(); wrapper.eq(Post::getBizType, query.getBizType()) .eq(Post::getStatus, query.getStatus()) .orderByDesc(Post::getCreateTime); return this.page(new Page(query.getCurrent(), query.getSize()), wrapper); }PaginationInnerInterceptor是分页插件的核心没有它Page 对象只是个空壳SQL 不会真的拼 LIMIT。setMaxLimit(100L) 防止有人传 size10000 把整张表拉出来前端一次给 10 到 20 条就够了。列表页要留意 SQL 有没有走联合索引。MyBatis-Plus 控制台会打印 SQL如果看到 typeALL 或 Using filesort说明索引设计有问题回到 2.2 的建表脚本核对。这里补一个安全和数据质量提醒表白墙和论坛帖子经常有人贴富文本后端必须加全局过滤器把script、onerror这类内容转义否则轻则页面样式被冲烂重则存储型 XSS。上传 PDF 时也一样要在过滤器里限制 MIME 和内容嗅探否则“上传 PDF 触发 XSS”不是危言耸听是安全测试里常挂的一条。别以为校园项目没人攻击公开上线的内容平台第一波扫描脚本很快就会来。4. Uniapp 前端与多端打包从目录设置到 manifest 配置一次讲完4.1 pages 目录与 tabBar五个入口为什么只要四个 tabUniapp 项目源码拿到后第一步不要跑先看 pages.json。常见目录src/ ├── pages/ │ ├── index/index.vue │ ├── market/market.vue │ ├── wall/wall.vue │ ├── forum/forum.vue │ ├── lost/lost.vue │ ├── editor/editor.vue │ ├── login/login.vue │ └── user/user.vue ├── api/ │ ├── request.js │ ├── post.js │ └── user.js ├── store/ └── static/pages.json 的 tabBar 配置{ pages: [ {path: pages/index/index, style: {navigationBarTitleText: 校园圈}}, {path: pages/market/market, style: {navigationBarTitleText: 集市}}, {path: pages/forum/forum, style: {navigationBarTitleText: 论坛}}, {path: pages/wall/wall, style: {navigationBarTitleText: 表白墙}}, {path: pages/lost/lost, style: {navigationBarTitleText: 失物招领}}, {path: pages/editor/editor, style: {navigationBarTitleText: 发布}}, {path: pages/login/login, style: {navigationBarTitleText: 登录}}, {path: pages/user/user, style: {navigationBarTitleText: 我的}} ], tabBar: { color: #999999, selectedColor: #1296db, list: [ {pagePath: pages/index/index, text: 首页}, {pagePath: pages/market/market, text: 集市}, {pagePath: pages/forum/forum, text: 论坛}, {pagePath: pages/user/user, text: 我的} ] } }微信小程序 tabBar 最多五项而且只能用原生 tabBar。校园圈如果把表白墙、失物招领、校园墙全塞进去超过限制不说交互也挤成一排。主 tab 放首页、集市、论坛、我的表白墙和失物招领从首页宫格进这个方案比硬塞 tabBar 合理。“发布”按钮不要放 tabBar单独做成首页右上角加号图标跳转到 editor 页面。editor 页面承载发布类型选择比每个模块单独写一个发布页好维护。4.2 request 封装H5 要指向两个域名时BASE_URL 别写死在页面最怕的是每个页面各自 copy 一段uni.request接口域名一改要全局替换。我在 api 目录下只留一个 request.js// api/request.js const BASE_URL http://192.168.1.100:8080/api export function request({ url, method GET, data {} }) { const token uni.getStorageSync(token) return new Promise((resolve, reject) { uni.request({ url: BASE_URL url, method, data, header: { Content-Type: application/json, Authorization: token || }, success: (res) { const data res.data if (data data.code 200) { resolve(data.data) } else if (res.statusCode 401) { uni.removeStorageSync(token) uni.navigateTo({ url: /pages/login/login }) reject(data) } else { uni.showToast({ title: data?.msg || 请求失败, icon: none }) reject(data) } }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }) reject(err) } }) }) }401 处理统一放在 request 里业务页面不用每个都判断。打开 App 发现 token 过期自动跳登录页这是前后端分离项目的基本体验。“uniapp 封装 H5 如何指向 2 个域名”这个问题的本质就是让 BASE_URL 变成运行时配置。我见过两种可靠做法一是打包前根据编译模式写环境变量H5 与小程序各读一个域名二是 App 启动时请求一个全局配置接口拿到配置写入本地缓存request 方法每次从这里读。校园圈这种规模用第一种就够了别为两个域名把请求层做成全家桶。4.3 多端适配微信小程序、H5、安卓/iOS/鸿蒙差异集中在这三点存储H5 能用 localStorage但小程序不支持 window 对象App 端 localStorage 在部分设备上和浏览器不一致。统一用uni.setStorageSync三端通吃。项目里如果不小心写了 windowH5 没问题一到小程序就报 undefined。请求域名微信开发者工具里勾了“不校验合法域名”能用真机预览就必须在小程序后台配置 request 合法域名且必须是 HTTPS。H5 部署要后端支持 CORS否则浏览器拦截跨域请求。安卓模拟器访问宿主机要用 10.0.2.2真机要用局域网 IP。“开发工具正常、手机不行”这个几乎每天都能见到的问题绝大多数是 IP 和 HTTPS 配置引起的。登录态微信小程序是 wx.login 拿 code 再换 openidH5 是账号密码或手机验证码App 可能是第三方登录。Uniapp 项目里把登录逻辑收敛到 api/user.js用条件编译#ifdef MP-WEIXIN区分而不是在主页面写一堆 if 分支。条件编译在编译期就干掉了无关代码比运行时判断省流量也省心。现在说鸿蒙Uniapp 对鸿蒙的交付已经不是远期话题但校园圈这种内容型应用建议先把微信小程序跑通业务再评估鸿蒙版。信息流页面复现成本低但地图定位、相机权限、支付回调这类原生能力在微信小程序、iOS、安卓、鸿蒙上各有各的实现差异。别被“一套代码全端上线”的广告话术带偏那是业务层原生化改造是另一份工作量。4.4 manifest.json 配置清单上架安卓应用市场前查四项Uniapp 项目的 manifest.json 是全局配置HBuilderX 界面改的东西最终会同步到这里。重点检查四项{ name: 校园圈, appid: __UNI__你的appid, versionName: 1.0.0, versionCode: 100, mp-weixin: { appid: 你的微信小程序appid, setting: { urlCheck: false }, usingComponents: true }, app-plus: { distribute: { android: { minSdkVersion: 21, targetSdkVersion: 30, permissions: [ android.permission.INTERNET ], abiFilters: [armeabi-v7a, arm64-v8a] } } }, h5: { router: { mode: hash } } }mp-weixin.setting.urlCheck: false只用于开发阶段正式提交微信审核前必须改成 true否则体验版被拦。app-plus.distribute.android的权限列表尽量只保留用到的权限。上架安卓应用市场时很多市场会扫敏感权限把摄像头、定位、通讯录全写上审核就要多问几轮。校园圈如果不上传视频、不做附近的人INTERNET 和相册读取权限就够。h5.router.mode建议用 hash。history 模式需要后端把所有非接口路由回退到 index.html否则部署后刷新详情页直接 404。这个坑在第 5 章展开。App 图标、启动图、签名证书不在 JSON 里写但上架安卓应用市场前必须在 HBuilderX 的“发行-原生App-云打包”里配置好。没有签名证书可以先打测试包上市场必须用正式证书这坑我踩过一次审核被驳回才回头补证书工期白搭三天。5. 常见问题排查前后端分离项目翻车最多的 5 个位置5.1 跨域接口明明通了浏览器还是报 blocked by CORS现象用 Postman 调http://localhost:8080/api/post/list返回正常Uniapp H5 一调就报 CORS errorNetwork 面板里 response header 没有Access-Control-Allow-Origin。原因SpringBoot 后端没有暴露 CORS 响应头或者只在某个 Controller 加了CrossOrigin注解H5 部署后的页面域名不在这个注解允许列表里。解决在后端加一个全局 CORS 过滤器而不是每个 Controller 加注解Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }这里有个细节addAllowedOriginPattern不要用addAllowedOrigin(*)。后一种写法在 allowCredentials(true) 时会被浏览器直接当作不合法配置返回 500。上线后把 allowedOriginPattern 收紧到 H5 实际域名。5.2 时间字段数据库时间没问题JSON 返回却少了 8 小时现象t_post 表里 create_time 是 22:00前端页面显示 14:00iOS 上甚至显示 NaN。原因后端返回 Date 对象时Jackson 默认按 UTC 序列化数据库连接串里没指定 serverTimezoneMySQL 驱动用的又是 UTC。解决连接串加serverTimezoneAsia/Shanghai实体字段上再用 JsonFormat 统一格式化JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;顺手检查如果前端拿到的是时间戳格式小程序端很多模板不会自动格式化。别在每个页面重复 new Date()把格式化函数放进公共工具类。否则后面加一个“XXX 分钟前”的需求要改十几个文件。5.3 图片 404文件已经传到服务器前端就是显示不出来现象上传接口返回/upload/xxx.jpg数据库也存了但image srchttp://ip:8080/upload/xxx.jpg就是裂图。原因SpringBoot 默认不会把磁盘上的./upload目录当作静态资源对外映射。放在src/main/resources/static下的文件开发期能访问上传到./upload的不行。解决在 WebConfig 里加资源映射Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-dir:./upload}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir /); } }注意file:后面要跟绝对路径写成file:./upload/在某些 Tomcat 部署场景会失效。另一种更稳的做法是不暴露静态目录上传成功后文件转存 MinIO再走一个/api/file/{id}接口读出来权限也更好控制。校园圈这种项目我能用 MinIO 就不用本地磁盘省得后面迁移数据。5.4 Uniapp 真机不打印日志、请求也不通现象H5 端正常微信开发者工具正常一用手机预览就白屏、loading 不停。console 面板什么都看不到。原因开发时的 baseURL 是http://localhost:8080手机访问 localhost 指向手机自己当然连不上。微信真机预览要求合法域名开发者工具里勾的“不校验合法域名”只在工具内生效。Uniapp 真机调试时 console 默认打不到电脑端vConsole 没开的话看起来就像代码没执行。解决先确认手机和电脑同一局域网baseURL 改成电脑的局域网 IP。微信小程序真机预览要在小程序后台把 request 合法域名配成 HTTPS 域名。想看日志在 manifest.json 调试开关里打开 vConsole或直接看开发者工具 Network 面板。Uniapp 不打印日志信息多数时候不是代码没跑是日志通道没开。这个“不是 bug 是配置”的坑最容易消耗新人半天时间。5.5 刷新页面就 404H5 打包上线后的路由问题现象站点首页打开正常点进商品详情也正常一按浏览器刷新就 404。原因uni-app 默认路由模式是 hashURL 里是#/pages/detail/detail刷新无感。如果 manifest.json 里h5.router.mode被改成 history后端没做 fallback 重写刷新真实路径就会 404。解决没有明确分享带参路径需求时路由模式用 hash 最省事。如果非用 historynginx 配置要加一行location / { try_files $uri $uri/ /index.html; }Tomcat 部署前后端分离项目时同理后端 SpringBoot 只管/api前端 H5 部署在同一个域名静态目录下刷新问题全靠 fallback 解决。我排查线上问题时的第一条线索就是看 URL 里有没有#有#还是 404多半是静态资源路径配错了没有#的 404多半是后端 fallback 没配。6. 部署与验证从源代码到交付前用一次接口体检替代盲改前端6.1 后端 jar 包的最小验证路径后端打包启动的流程我在新环境里永远是这三条命令mvn clean package -DskipTests java -jar target/campus-1.0.0.jar 如果项目要求部署到外部 Tomcat就打成 war 包放到 tomcat/webapps但 SpringBoot 的 jar 自带嵌入式 Tomcat部署成本更低。启动后按从内到外的顺序验证四个接口curl -X POST http://localhost:8080/api/user/login \ -H Content-Type: application/json \ -d {username:admin,password:123456} curl http://localhost:8080/api/post/list?bizType2current1size10 \ -H Authorization: Bearer token curl -F filetest.jpg http://localhost:8080/api/upload/image \ -H Authorization: Bearer token只看三件事状态码、耗时、返回 JSON 的 data 结构。跨过登录直接调列表接口正确结果应该被拦截器挡成 401。能返回 401 我反而放心至少说明过滤器和拦截器都在工作。如果 list 直接返回了数据那才要回头查拦截器注册是不是漏了路径。6.2 二次开发前我先看这三个文件拿到任何 SpringBoot Uniapp 的源代码包我的检查顺序是application.yml 里端口、数据源、上传目录、token 密钥manifest.json 里小程序 appid、H5 路由模式数据库脚本里的字符集和索引。三个文件确认完才开始跑前端。项目交付前先准备一组接口自动化脚本按“登录→发帖→评论→列表”走一遍很多前端白屏问题回头查接口就能定位不用把时间耗在黑匣子里。我现在做校园圈这类项目不再逐个模块手动点 UI 验证了。先用命令把后端验完再启动前端套页面。这套流程也适合验别人交付的成品省掉大量读代码时间。希望这组落地细节帮到你。本文还有配套的精品资源点击获取