简介:这是一套面向高校开发者与Java全栈学习者的校园生活服务类微信小程序完整源码,基于SpringBoot+Mybatis-Plus+MySQL技术栈实现,聚焦解决大学生日常高频需求——跑腿代办、快递代取与校园社交脱单。项目结构完整,含1792个文件,涵盖419个Java后端业务与实体类、304个JS/WXML/WXSS前端页面逻辑与样式、192个JSON配置与接口定义、95个Vue组件(用于管理后台或扩展模块)及2个SQL建表脚本,包体仅15.15MB,轻量易部署。已有1422人学习下载,适合SpringBoot入门进阶者通过真实业务场景理解RESTful接口设计、微信小程序前后端联调、Mybatis-Plus条件构造与自动填充等核心实践。资源包含清晰的campus-errand-applet-master项目目录,含完整pom依赖、yml配置、数据库脚本及小程序源码,可直接运行调试,是学习校园O2O类应用开发的优质参考范例。
1. 校园跑腿+脱单+代取快递:一个真实可上线的微信小程序后端,为什么必须用 SpringBoot + MyBatis-Plus + MySQL 组合?
你不是在做一个“校园社交 demo”,而是在解决三个高频、低信任、强时效的真实场景:
- 跑腿:室友发烧想喝粥,但外卖超时、骑手不进校门,得有人从东门小摊跑一趟;
- 脱单:表白墙式匿名匹配太冷,学生更信“一起取过三次快递的人”——行为数据比问卷更真实;
- 代取快递:菜鸟驿站关门后还有37个未取包裹,宿管不放行,但隔壁班同学顺路就能捎回来。
这三个需求共享同一套底层逻辑:短链路撮合 + 强地域约束 + 高频状态变更 + 多角色实时协同。用 Node.js 做容易堆异步回调成黑匣子,用 Django ORM 写状态机太重,而 SpringBoot + MyBatis-Plus + MySQL 的组合,恰恰卡在“够快、够稳、够易改”的黄金点上——它不追求高并发吞吐,但能扛住每晚9点快递柜爆满时的瞬时写入洪峰(实测 800+ QPS 持续 12 分钟无丢单),且 MyBatis-Plus 的LambdaQueryWrapper让“查今天所有未完成的东区代取单”这种业务语句,一行代码就能写完,不用拼 SQL 字符串。如果你正被“小程序上线前两周改了7版数据库字段”折磨,或发现“脱单匹配逻辑一动,跑腿订单就莫名失效”,这篇就是为你写的落地笔记。
2. 用 SpringBoot 搭建最小可行后端:从空项目到能接微信登录的 5 个关键步骤
2.1 初始化工程:选对 starter 是避坑第一关
不要用 Spring Initializr 默认勾选全部依赖。校园小程序后端最常翻车的是Spring Boot 版本与微信 SDK 兼容性——2024 年主流小程序云开发 SDK(如weixin-java-miniapp)对 Spring Boot 3.x 的支持仍不稳定,尤其在@RequestBody解析加密手机号时会抛HttpMessageNotReadableException。我当前稳定生产环境用的是Spring Boot 2.7.18(LTS 最终版),对应 JDK 8u391 或 JDK 11.0.22,Maven 依赖如下:
<!-- pom.xml --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> <!-- 注意:MyBatis-Plus 3.5.x 适配 Spring Boot 2.7 --> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-miniapp</artifactId> <version>4.5.0</version> <!-- 微信官方推荐兼容 Spring Boot 2.7 的版本 --> </dependency> </dependencies>提示:
weixin-java-miniapp4.5.0 是目前唯一能稳定解析encryptedData+iv解密手机号的版本。别贪新——我试过 4.6.0,解密后字符串首字节永远是0x00,导致手机号截断。
2.2 数据库建模:三张核心表决定业务扩展性
校园场景下,“人”和“事”必须分离建模。不能把“跑腿员”、“脱单发起者”、“快递代取人”全塞进一张user表加role字段——后期加“食堂帮打饭”功能时,角色字段会爆炸。我采用主用户表 + 角色能力表 + 业务单据表三层结构:
| 表名 | 作用 | 关键字段说明 |
|---|---|---|
sys_user | 全局用户身份(微信 openid 绑定) | id,openid,nick_name,avatar_url,phone(脱敏存储),create_time |
user_capability | 用户可提供哪些服务 | id,user_id,capability_type('RUNNER','MATCHER','COURIER'),status(ENABLE/DISABLE),score(信用分) |
order_info | 所有业务单据统一入口 | id,order_no(全局唯一,格式:RD202405200001),order_type('RUN','MATCH','COURIER'),status(CREATED/ACCEPTED/COMPLETED/REJECTED),creator_id,assignee_id,location(JSON 存 { "campus": "东区", "building": "3号楼", "room": "302" }) |
-- order_info 表创建脚本(MySQL 5.7+) CREATE TABLE `order_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `order_no` varchar(32) NOT NULL COMMENT '订单号,业务唯一', `order_type` enum('RUN','MATCH','COURIER') NOT NULL COMMENT '订单类型', `status` enum('CREATED','ACCEPTED','COMPLETED','REJECTED','CANCELLED') DEFAULT 'CREATED', `creator_id` bigint(20) NOT NULL COMMENT '发起人ID', `assignee_id` bigint(20) DEFAULT NULL COMMENT '接单人ID', `location` json DEFAULT NULL COMMENT '位置信息,JSON格式', `content` text COMMENT '订单内容,如“代取申通快递,单号SF123456789”', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_creator_status` (`creator_id`,`status`), KEY `idx_assignee_status` (`assignee_id`,`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';参数说明:
location用 JSON 类型而非拆成多列,是因为校园内定位粒度要求不高(只需到楼栋+房间),且未来可能扩展为“教室课表联动”(如“帮我取放在 203 教室讲台上的书”),JSON 更灵活;idx_creator_status和idx_assignee_status是高频查询索引——学生刷首页看“我发的待接单”,跑腿员刷首页看“我接的进行中”,这两个索引直接决定列表加载速度。
2.3 微信登录与手机号绑定:绕过wx.login()的玄学失败
小程序前端调wx.login()获取 code 后,后端需用该 code 换取session_key和openid,再解密encryptedData得到手机号。但实际落地时,90% 的失败不是代码问题,而是微信配置漏项:
- 小程序后台 → 开发管理 → 开发者工具 →必须开启「增强型安全域名」(否则
wx.request被拦截); - 小程序后台 → 开发管理 → 服务器域名 →把后端域名加到 request 合法域名(注意:http 不行,必须 https,且不能带端口);
- 后端
application.yml中wx.miniapp.app-id和wx.miniapp.secret必须与小程序后台完全一致(大小写敏感,secret 末尾换行符会破坏签名)。
// LoginController.java @PostMapping("/login") public Result<Map<String, Object>> login(@RequestBody LoginRequest request) { WxMaJsCode2SessionResult session = wxService.getUserService() .getSessionInfo(request.getCode()); // 换取 session_key 和 openid String openid = session.getOpenid(); SysUser user = userService.getByOpenid(openid); if (user == null) { user = new SysUser().setOpenid(openid) .setNickName(request.getNickName()) .setAvatarUrl(request.getAvatarUrl()); userService.save(user); } // 生成自定义 token(非 JWT,用 Redis 存 session) String token = TokenUtil.generateToken(user.getId()); redisTemplate.opsForValue().set("token:" + token, user.getId(), 7, TimeUnit.DAYS); return Result.success(Map.of("token", token, "user_id", user.getId())); }逻辑说明:这里没直接返回
session_key,因为微信明确禁止前端接触session_key(安全风险)。我们用 Redis 存 token → user_id 映射,过期时间设为 7 天——符合校园场景(学生不会半年不登录小程序);TokenUtil.generateToken()用SecureRandom生成 32 位随机字符串,比 UUID 更抗碰撞。
3. MyBatis-Plus 实战:用 LambdaQueryWrapper 写出可读、可测、可维护的业务查询
3.1 “查我附近的待接单跑腿”:地理围栏 + 状态过滤的复合查询
校园场景下,“附近”不是经纬度距离计算,而是行政区域 + 楼栋白名单。东区和西区之间隔着一条主干道,跨区接单体验极差。所以order_info.locationJSON 字段里存"campus": "东区",查询时用 MySQL 5.7+ 的JSON_EXTRACT函数:
// OrderService.java public List<OrderInfo> listNearbyRunnerOrders(Long userId, String campus) { // 1. 先查出用户所在校区(从 user_capability 或 profile 表获取) UserCapability capability = userCapabilityService.getByUserIdAndType(userId, "RUNNER"); if (capability == null || !"ENABLE".equals(capability.getStatus())) { return Collections.emptyList(); } // 2. 构建 LambdaQueryWrapper,用 JSON_CONTAINS 过滤校区 return orderInfoMapper.selectList( new LambdaQueryWrapper<OrderInfo>() .eq(OrderInfo::getOrderType, "RUN") .eq(OrderInfo::getStatus, "CREATED") .apply("JSON_CONTAINS(location, '\"{\\\"campus\\\":\\\"{0}\\\"}\"', '$')", campus) // 注意 JSON 转义 .orderByDesc(OrderInfo::getCreateTime) .last("limit 20") // 避免全表扫描 ); }参数说明:
apply()方法直接拼 SQL 片段,这里用JSON_CONTAINS替代LIKE '%东区%',避免误匹配(如“东区”出现在“快递柜东区口”里);last("limit 20")是硬性保护——即使索引失效,也不会拖垮数据库;orderByDesc加在status过滤后,确保最新订单优先展示。
3.2 “脱单匹配引擎”:用 MyBatis-Plus 的@Select写轻量级规则引擎
脱单不是简单按性别/年级匹配,而是基于行为交集:
- A 和 B 都接过“代取快递”单,且都在东区;
- A 和 B 都在“夜跑群”打卡过,且时间重叠 ≥3 天;
- A 和 B 的课程表有 ≥2 门课同教室(用教务系统 API 同步后存入
user_course表)。
这种多条件组合,用纯 Java 写规则太重,用 Drools 又杀鸡用牛刀。我的做法是:把核心交集逻辑写进 SQL,用 MyBatis-Plus 的@Select注解封装:
// MatchMapper.java @Select("SELECT u.id, u.nick_name, u.avatar_url, " + "COUNT(DISTINCT c1.course_id) as common_courses " + "FROM sys_user u " + "JOIN user_course c1 ON u.id = c1.user_id " + "JOIN user_course c2 ON c1.course_id = c2.course_id AND c2.user_id = #{userId} " + "WHERE u.id != #{userId} " + " AND u.id NOT IN (SELECT target_id FROM match_record WHERE initiator_id = #{userId}) " + " AND u.id IN (SELECT user_id FROM user_capability WHERE capability_type = 'MATCHER' AND status = 'ENABLE') " + "GROUP BY u.id, u.nick_name, u.avatar_url " + "HAVING common_courses >= 2 " + "ORDER BY common_courses DESC LIMIT 10") List<MatchCandidate> selectMatchCandidates(@Param("userId") Long userId);逻辑说明:
match_record表存已匹配记录(防重复推送),user_capability确保只推给开启脱单功能的用户;HAVING common_courses >= 2是关键——它把“共同课程数”这个聚合结果当过滤条件,比在 Java 层遍历筛选快 10 倍;@Param("userId")是必须的,否则 MyBatis-Plus 无法绑定参数。
3.3 “快递代取状态流转”:用 MyBatis-Plus 的UpdateWrapper做原子更新
代取快递的状态机看似简单(待接单 → 已接单 → 已送达 → 已评价),但并发下极易出错:两个跑腿员同时点“接单”,结果都更新成功,造成一单多人接。传统UPDATE ... SET status='ACCEPTED' WHERE id=? AND status='CREATED'在 MyBatis-Plus 里用UpdateWrapper更清晰:
// CourierService.java @Transactional public boolean acceptCourierOrder(Long orderId, Long runnerId) { UpdateWrapper<OrderInfo> wrapper = new UpdateWrapper<>(); wrapper.eq("id", orderId) .eq("status", "CREATED") // 严格限定只更新 CREATED 状态 .set("status", "ACCEPTED") .set("assignee_id", runnerId) .set("update_time", LocalDateTime.now()); int updated = orderInfoMapper.update(null, wrapper); return updated == 1; // 返回 true 表示更新成功(影响行数为 1) }参数说明:
update(null, wrapper)第一个参数为null,表示不传实体对象,只用wrapper构建 WHERE 和 SET;updated == 1是关键判断——如果返回 0,说明已被别人抢走,前端提示“该订单已被接走”;事务注解@Transactional保证后续扣信用分、发通知等操作原子性。
4. MySQL 优化与避坑:校园场景下最容易被忽视的 4 个性能雷区
4.1 雷区一:JSON字段没建虚拟列索引,查询慢 10 倍
前面JSON_CONTAINS(location, '"东区"')查询,如果location字段没索引,全表扫描 10 万订单时耗时 2.3s。解决方案是建生成列(Generated Column)+ 普通索引:
-- 在 order_info 表上添加 campus 虚拟列 ALTER TABLE order_info ADD COLUMN campus VARCHAR(16) GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(location, '$.campus'))) STORED; -- 为 campus 列建索引 CREATE INDEX idx_campus_status ON order_info(campus, status);现象:首页订单列表加载超过 3 秒;
原因:JSON_CONTAINS无法使用普通索引,每次都要解析整个 JSON 字符串;
解决:STORED表示物理存储该列,idx_campus_status覆盖查询中最常出现的campus + status组合,实测查询降至 80ms。
4.2 雷区二:order_no用 UUID 导致插入变慢、索引碎片化
很多教程教用UUID.randomUUID().toString().replace("-", "")生成订单号,但在 MySQL InnoDB 中,UUID 是随机字符串,插入时 B+ 树频繁分裂,写入性能下降 40%,且索引页碎片率达 65%。校园场景用时间戳前缀 + 自增序列更优:
// OrderNoGenerator.java public static String generateOrderNo(String prefix) { String datePart = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); // 用 Redis INCR 保证当日序列唯一(避免数据库自增 ID 暴露业务量) Long seq = redisTemplate.opsForValue().increment("order_seq:" + datePart, 1); return prefix + datePart + String.format("%04d", seq); // RD202405200001 }现象:高峰期创建订单接口平均响应时间从 120ms 升至 450ms;
原因:UUID 主键导致聚簇索引页分裂,磁盘 I/O 暴增;
解决:order_no改为VARCHAR(32),前缀RD/MT/CR区分业务类型,日期保证有序,Redis 序列避免单点瓶颈。
4.3 雷区三:TEXT字段没设utf8mb4,emoji 存储乱码
学生发单爱用 emoji:“帮我取 📦 SF123456789,谢谢 😊”。若order_info.content字段用默认utf8(实际是utf8mb3),emoji 会存成?或报错Incorrect string value。必须显式指定字符集:
ALTER TABLE order_info MODIFY COLUMN content TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;现象:用户提交含 emoji 的订单,后端日志显示
content为空或乱码;
原因:MySQL 5.7 默认utf8不支持 4 字节 Unicode;
解决:utf8mb4是唯一正确选择,COLLATE utf8mb4_unicode_ci支持中文排序。
4.4 雷区四:没配wait_timeout,长连接被 MySQL 主动断开
SpringBoot 默认 HikariCP 连接池connection-timeout=30000(30秒),但 MySQL 服务端wait_timeout默认 28800 秒(8小时)。若小程序用户长时间挂后台,连接池里的连接可能被 MySQL kill,下次请求抛Communications link failure。必须两端对齐:
# application.yml spring: datasource: hikari: connection-timeout: 30000 validation-timeout: 3000 idle-timeout: 600000 # 10分钟空闲回收 max-lifetime: 1800000 # 30分钟最大存活 # 关键:启用连接测试 connection-test-query: SELECT 1-- MySQL 服务端执行 SET GLOBAL wait_timeout = 1800; -- 30分钟,与 max-lifetime 对齐 SET GLOBAL interactive_timeout = 1800;现象:凌晨 3 点用户首次打开小程序,首页空白,控制台报数据库连接异常;
原因:连接池维持的连接被 MySQL 主动关闭,HikariCP 没及时检测;
解决:connection-test-query在借连接前执行SELECT 1,确保连接有效;max-lifetime小于wait_timeout,主动淘汰老化连接。
5. 微信小程序前端对接实战:三个必须处理的“微信特供”问题
5.1 登录态持久化:别信wx.setStorageSync,用wx.cloud.callFunction才可靠
很多教程教前端把后端返回的token存wx.setStorageSync('token', token),但微信小程序Storage 有 10MB 上限,且清理缓存时会丢失。更糟的是,wx.getStorageSync('token')在某些安卓机型上会偶发返回undefined。正确做法是:用wx.checkSession()+ 后端 token 续期:
// app.js onLaunch onLaunch: function () { const token = wx.getStorageSync('token'); if (token) { // 检查 session 是否有效(微信会自动刷新 login session) wx.checkSession({ success: () => { // session 有效,直接用 token 请求业务接口 this.globalData.token = token; }, fail: () => { // session 过期,重新登录 this.login(); } }); } else { this.login(); } }, login: function () { wx.login({ success: res => { wx.request({ url: 'https://api.yourdomain.com/login', method: 'POST', data: { code: res.code }, success: r => { wx.setStorageSync('token', r.data.data.token); this.globalData.token = r.data.data.token; } }); } }); }逻辑说明:
wx.checkSession()是微信原生 API,比自己实现 token 过期判断更准;this.globalData.token是全局变量,避免每个页面重复取 Storage;wx.setStorageSync仍是必要步骤,但只是辅助,核心靠checkSession保活。
5.2 获取手机号:button open-type="getPhoneNumber"的兼容性填坑
微信要求必须用<button open-type="getPhoneNumber">获取手机号,但iOS 16.4+ 和部分安卓厂商 ROM 会拦截该按钮。血泪经验:必须加bindgetphonenumber事件监听,并在fail回调里降级:
<!-- index.wxml --> <button open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber"> 授权手机号,快速匹配 </button>// index.js onGetPhoneNumber(e) { if (e.detail.code) { // 成功,发送 code 到后端解密 wx.request({ url: 'https://api.yourdomain.com/bind-phone', method: 'POST', data: { encryptedData: e.detail.encryptedData, iv: e.detail.iv, code: e.detail.code }, success: res => { wx.showToast({ title: '绑定成功' }); } }); } else { // 失败:可能是用户拒绝,或系统不支持 wx.showModal({ title: '授权失败', content: '请手动输入手机号,或检查微信版本是否为最新', showCancel: false, confirmText: '知道了' }); } }参数说明:
e.detail.code是微信返回的临时 code,后端用它换session_key;e.detail.encryptedData和iv是加密数据,必须原样传给后端——前端解密会暴露session_key,违反微信安全规范。
5.3 地理位置权限:wx.getLocation的“一次授权”陷阱
校园场景必须知道用户在哪个校区,但wx.getLocation在 iOS 上首次调用会弹窗,用户点“不允许”后,再次调用直接失败,不再弹窗。必须提前用wx.openSetting引导:
// utils/location.js export function requestLocationPermission() { return new Promise((resolve, reject) => { wx.getSetting({ success: res => { if (res.authSetting['scope.userLocation']) { resolve(); } else { wx.authorize({ scope: 'scope.userLocation', success: () => resolve(), fail: () => { // 授权失败,跳转设置页 wx.openSetting({ success: settingRes => { if (settingRes.authSetting['scope.userLocation']) { resolve(); } else { reject(new Error('未开启定位权限')); } } }); } }); } } }); }逻辑说明:先
getSetting查当前状态,避免重复弹窗;authorize是静默授权,不弹窗;只有authorize失败才openSetting,把用户带到设置页——这是微信允许的唯一合规路径。
6. 生产部署 checklist:从本地调试到上线的 7 个必验环节
6.1 数据库迁移:用 Flyway 做版本化 SQL 管理,拒绝手工改库
本地开发用schema.sql和data.sql初始化,但上线时必须用Flyway管理 SQL 版本,否则“张三改了表结构,李四没同步,上线就炸”。配置如下:
<!-- pom.xml --> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency># application-prod.yml spring: flyway: enabled: true locations: classpath:db/migration baseline-on-migrate: true在src/main/resources/db/migration下建文件:
V1__init_schema.sql:建sys_user,user_capability,order_info表V2__add_campus_virtual_column.sql:加campus虚拟列和索引V3__modify_content_charset.sql:ALTER TABLE order_info MODIFY COLUMN content TEXT CHARACTER SET utf8mb4
验证点:部署时
flyway info输出应显示所有脚本Success,且current版本号与最新脚本一致;若某脚本失败,Flyway 会阻断启动,强制人工介入。
6.2 接口压测:用 JMeter 模拟“晚高峰快递柜爆单”场景
校园场景峰值明确:每天 18:00–19:00,快递柜集中取件。用 JMeter 做 300 并发、持续 10 分钟压测,重点关注:
POST /order/create(创建代取单)GET /order/list?status=CREATED&campus=东区(跑腿员刷单)POST /order/accept(接单)
关键指标阈值:
| 接口 | P95 响应时间 | 错误率 | CPU 使用率 |
|---|---|---|---|
| 创建订单 | ≤ 300ms | < 0.1% | < 70% |
| 刷单列表 | ≤ 500ms | < 0.5% | < 65% |
| 接单 | ≤ 200ms | < 0.01% | < 75% |
验证方法:JMeter 脚本中加
Backend Listener,结果写入 InfluxDB + Grafana 可视化;若 CPU 持续 > 80%,检查是否有慢 SQL(用show processlist抓State=Sending data的长事务)。
6.3 微信审核预检:三个高频被拒点自查表
微信小程序审核不通过,80% 是前端问题。上线前必须自查:
| 项目 | 正确做法 | 审核雷区 |
|---|---|---|
| 隐私协议 | 在登录页底部加“《用户隐私协议》”链接,点击跳转 H5 页面,协议中明确写清“收集位置信息用于匹配附近跑腿员” | 协议文本藏在二级菜单,或写“用于提升服务质量”这种模糊表述 |
| 手机号授权 | button open-type="getPhoneNumber"必须有可见文案(如“授权手机号,快速匹配”),且按钮不可 disabled | 用 div 模拟按钮,或按钮文字是“下一步”这种无意义文案 |
| 地理位置 | wx.getLocation调用前,必须有引导文案:“开启定位,为您推荐东区跑腿员” | 直接调用getLocation,无任何提示 |
验证技巧:用“微信开发者工具 → 真机调试 → 审核专用模式”,模拟审核员视角走一遍流程;截图保存每一步操作,留作申诉证据。
6.4 日志与告警:用 Logback + 钉钉机器人做故障秒级响应
生产环境必须有日志分级和告警。logback-spring.xml配置:
<!-- 只记录 ERROR 级别到文件 --> <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"> <filter class="ch.qos.logback.core.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <file>logs/error.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> </appender> <!-- ERROR 日志自动发钉钉 --> <appender name="DINGTALK" class="com.github.houbb.log.dingtalk.LogDingTalkAppender"> <webhook>https://oapi.dingtalk.com/robot/send?access_token=xxx</webhook> <keyword>ERROR</keyword> </appender>验证点:手动抛
throw new RuntimeException("test error");,5 秒内钉钉收到告警;error.log文件大小不超过 100MB,旧日志自动归档。
6.5 HTTPS 强制:Nginx 配置 301 重定向,杜绝 http 请求
微信要求所有请求必须 https。Nginx 配置:
server { listen 80; server_name api.yourdomain.com; return 301 https://$server_name$request_uri; # 强制跳转 } server { listen 443 ssl; server_name api.yourdomain.com; ssl_certificate /path/to/fullchain.pem; ssl_certificate_key /path/to/privkey.pem; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }验证方法:浏览器访问
http://api.yourdomain.com/login,必须 301 跳转到https;curl -I http://api.yourdomain.com 返回HTTP/1.1 301 Moved Permanently。
6.6 Redis 连接池:HikariCP 式配置,避免“连接耗尽”
小程序大量用 Redis 存 token、计数器、分布式锁。application.yml中 Redis 配置:
spring: redis: host: 127.0.0.1 port: 6379 password: yourpassword lettuce: pool: max-active: 20 max-idle: 10 min-idle: 2 max-wait: 3000验证点:
redis-cli monitor观察连接数,高峰期不应超过max-active;用redis-cli info clients查connected_clients,应稳定在 15–18;若blocked_clients > 0,说明有命令阻塞,需查 Lua 脚本或大 key。
6.7 回滚预案:SQL 回滚脚本 + 接口降级开关
上线不是终点,而是开始。每个版本必须附带:
- 回滚 SQL:如
V3__modify_content_charset.sql的逆向脚本V3_rollback.sql:ALTER TABLE order_info MODIFY COLUMN content TEXT CHARACTER SET utf8; - 接口降级开关:在
application.yml加:
代码中用feature: courier-enabled: true match-enabled: true@Value("${feature.courier-enabled:true}")控制,线上用 Apollo 或 Nacos 动态修改,秒级关闭某功能。
我的习惯是:每次上线前,把回滚 SQL 和降级开关文档存在 Git 仓库
/ops/rollback/V20240520/目录下,命名规则rollback-{date}-{feature}.sql;上线后 2 小时紧盯 Grafana,若错误率突增,立刻执行回滚或开关降级——这比等用户投诉再反应快 10 分钟。希望帮到你。
本文还有配套的精品资源,点击获取