简介:一套基于SpringBoot、Mybatis-plus与MySQL开发的校园跑腿微信小程序源码,面向在校大学生与Java全栈学习者,覆盖跑腿代购、校园脱单、代理任务、快递代取等校园生活服务场景,可直接用于课程设计、毕业设计或项目二次开发。压缩包共1792个文件、约15.15MB,核心内容以Java后端接口、WXML/WXSS/JS小程序前端页面和Vue/TypeScript管理端为主,辅以JSON/XML/YML配置、SQL数据库脚本以及说明文档,目录结构与分层设计清晰,便于按模块查阅。目前已有1422人浏览学习。通过仔细阅读代码,可以掌握SpringBoot整合Mybatis-plus操作MySQL的完整实现,理解小程序端页面渲染与后端接口调用的协作逻辑;还能复用用户下单、任务分配、订单管理、代理审核等核心模块,快速搭建一个可运行的校园互助平台,是学习前后端分离开发与微信小程序落地的实用参考。
1. 校园跑腿微信小程序:SpringBoot+Mybatis-plus+mysql这套组合要怎么落地
基于SpringBoot、Mybatis-plus、MySQL做一个校园跑腿微信小程序,听上去是个标准增删改查,但真动手拆开会发现它是四类业务叠在一起:跑腿代购、代拿快递、校园代理代办,以及完全和订单无关的校园脱单模块。脱单模块要做资料脱敏、双向匹配、联系方式解锁,订单模块要做状态流转、抢单并发、超时关单,两者凑到一起,复杂度不会低于一个小型电商后台。这篇笔记写给两类人:拿这个方向做毕设或课设的在校生,以及准备接校园外包项目的一线工程师。我会按“数据建模→MySQL落库→SpringBoot接口→小程序对接→踩坑→批量与脱敏”的顺序,把可以直接复用进项目的代码和参数写清楚。
2. 后端的选型与四类业务建模:为什么这套组合适合校园项目
2.1 选型理由:SpringBoot当骨架,Mybatis-plus当ORM,MySQL当存储
先解决一个被问最多的问题:为什么不直接用Spring Data JPA?我在两个外包项目里对比过。校园项目的接口绝大多数是“按条件查列表、按状态更新字段、写一张扩展表”,Mybatis-plus的LambdaQueryWrapper写起来就是三五行Java,而JPA在条件组合查询时要么拼Specification要么写JPQL,新接手的人理解成本更高。JPA的优势在复杂实体关系聚合上更舒服,但这里四类业务核心关系就两三种,并不值得为“多表关联自动管理”引入额外的抽象成本。另外你多半会在Debug时直接用Navicat或MySQL Workbench看数据,Mybatis-plus的SQL是直观的单表update/select,出问题定位比JPA二级缓存好查很多。MySQL选型上不要去用MongoDB替代,订单、用户、脱单资料都有明确的关系约束,事务也要靠MySQL的InnoDB行锁来保证抢单时数据一致。性价比最高的组合就是SpringBoot + Mybatis-plus + MySQL,外包可以交付,实验室也压得住。
版本选择上,我一般用SpringBoot 2.7.x配JDK 8或11,Mybatis-plus用3.5之后版本。这套组合兼容性最稳,网上教程、代码生成器、各类starter都是按这个组合验证过的。如果你刚接触这个方向,不要一上来就挑战SpringBoot 3,后面第5章我会专门说版本太高引发的javax和jakarta包名迁移坑。前端程序用原生微信小程序而不是uniapp,也是同一个逻辑:标题已经锁定了微信小程序,原生开发调试更直接,不需要额外处理多端兼容;如果你以后确定要同时出Android、iOS、鸿蒙版本,再考虑用uniapp改造前端,但后端SpringBoot接口可以原样保留,前端框架切换不影响业务模型。
2.2 四类业务怎么落到三张核心表
校园跑腿、代拿快递、校园代理代办,本质是同一类“任务订单”。不同点只在于服务类型、计费方式和额外的属性字段。我用一张run_order承载这类业务,用service_type区分:1=校园跑腿(代买饭、代排队),2=代拿快递(快递点取件、寄件),3=校园代理代办(帮交材料、代报名、代购)。注意“代理”在这里指的是校园代办业务,不是网络代理。实现时千万别为每个类型单独建表,那种做法会把“查我的订单列表”从一个SQL变成三个SQL再Union,后面加第四类业务时还要改代码。扩展字段放run_order_extra,key-value结构,比如代拿快递需要填快递单号、跑腿需要填物品重量,这些不入主表,规范点在接口层做成List 传入。
脱单模块是独立体系:dating_profile存脱单资料,dating_like存心动记录。为什么要独立成模块而不塞进订单?因为脱单的业务生命周期完全不一样:用户发帖是长期状态,需要可见性控制、资料脱敏;而订单发布后几小时就结束了。混在一起会让订单分页多出很多无意义的维度。下表是我在这类项目里的默认映射方式,你可以直接抄进设计文档:
| 业务 | 表 | 核心字段 | 生命周期 |
|---|---|---|---|
| 发布/接单/完成 | run_order | order_no、user_id、runner_id、status | 几小时到几天 |
| 跑腿/快递/代理扩展信息 | run_order_extra | order_id、extra_key、extra_value | 与订单同周期 |
| 脱单资料 | dating_profile | user_id、gender、photo_url、visible | 长期存在 |
| 心动/匹配 | dating_like | from_user_id、to_user_id、status | 事件型 |
2.3 用户表怎么支撑三种角色
用户表sys_user我习惯不建“普通用户表+骑手表+管理员表”三张,而是单表加role字段:0普通用户、1接单跑腿者、2管理员。原因很简单,校园场景里“发单的人”和“接单的人”是同一批学生,今天发的单没人接自己就去接别人的单。拆表会让user和runner的数据重复维护。额外需要的是openid、student_no、campus。openid用来做微信登录态,student_no做校园认证,认证后能看到脱单模块;campus用来按校区过滤订单列表。token我用自定义登录态,在user_extra表存token和有效期。线上就别图省事直接拿openid当前端登录凭证,openid泄漏容易被伪造接口调用。
2.4 包结构与分层:entity、dto、vo别混在一层
后端代码我建议按业务模块分包,而不是按技术分层分包。常见做法是campus-order和campus-dating两个顶层包,各自内部再放entity、mapper、service、controller、dto、vo。这样做的好处是,脱单模块和订单模块将来如果要做性能拆分,直接把dtaing包拎出去变成独立服务,controller和mapper之间的依赖关系不用大改。像下面这样:
com.campus ├── dating │ ├── controller │ ├── service │ ├── mapper │ ├── entity │ ├── dto │ └── vo ├── order │ ├── controller │ ├── service │ ├── mapper │ ├── entity │ ├── dto │ └── vo └── common ├── config ├── exception └── interceptor实体类只做表映射,不要直接返回给前端;接收前端参数用dto,输出用vo。很多翻车现场就是把entity直接当vo用,密码、openid、手机号顺着JSON泄露出去了。尤其在脱单模块,entity里存的是完整手机号,vo里输出的是脱敏后的号码,这层隔离一定要有。
3. MySQL建表与Mybatis-plus落库:能复用的DDL和持久层写法
3.1 先建库建表:DDL脚本与字段设计
我建表前会把三个原则定下来:所有业务表统一逻辑删除deleted、统一自动填充create_time/update_time、订单号不允许自增id裸奔。下面是精简过的可运行DDL,你在本地按这个顺序执行就行。
-- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY, openid VARCHAR(64) NOT NULL, nickname VARCHAR(32) DEFAULT '', avatar VARCHAR(255) DEFAULT '', student_no VARCHAR(20) DEFAULT '', campus VARCHAR(20) DEFAULT '', role TINYINT DEFAULT 0 COMMENT '0普通 1跑腿 2管理员', status TINYINT DEFAULT 1 COMMENT '1正常 0禁用', deleted TINYINT DEFAULT 0, create_time DATETIME, update_time DATETIME, UNIQUE KEY uk_openid (openid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 服务订单表 CREATE TABLE run_order ( id BIGINT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL COMMENT '发布人', runner_id BIGINT DEFAULT 0 COMMENT '接单人,0未接', service_type TINYINT DEFAULT 0 COMMENT '1跑腿 2快递 3代理', pickup_address VARCHAR(255) DEFAULT '', delivery_address VARCHAR(255) DEFAULT '', contact_name VARCHAR(32) DEFAULT '', contact_phone VARCHAR(20) DEFAULT '', note VARCHAR(500) DEFAULT '', amount DECIMAL(8,2) DEFAULT 0, status TINYINT DEFAULT 0 COMMENT '0待接 1进行中 2完成 3取消', deleted TINYINT DEFAULT 0, create_time DATETIME, update_time DATETIME, KEY idx_user_status (user_id, status), KEY idx_runner_status (runner_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单扩展字段表 CREATE TABLE run_order_extra ( id BIGINT PRIMARY KEY, order_id BIGINT NOT NULL, extra_key VARCHAR(50) NOT NULL, extra_value VARCHAR(500) DEFAULT '', deleted TINYINT DEFAULT 0, create_time DATETIME ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段SQL里我刻意用BIGINT做主键而不是自增,配合Mybatis-plus的雪花ID生成,避免脱单和订单两张表在将来迁移数据时撞主键。订单表加了idx_user_status和idx_runner_status两个联合索引,因为小程序首页“我发布的/我接的”这两个列表是按user_id+status或runner_id+status过滤的。有一个常见问题:只给status建单列索引,数据量到几万后列表页明显变慢。contact_phone和amount这类查询频率低的字段不需要进索引。字段命名全程下划线,是为了让Mybatis-plus的驼峰映射在实体层自动转换。
3.2 实体类与BaseMapper:少写一半样板代码
选Mybatis-plus的核心价值就在这一步。实体类用注解把Java字段和表字段对应上,之后增删改查基本不用写XML。
@Data @TableName("run_order") public class RunOrder { @TableId(type = IdType.ASSIGN_ID) private Long id; private String orderNo; private Long userId; private Long runnerId; private Integer serviceType; private String pickupAddress; private String deliveryAddress; private String contactName; private String contactPhone; private String note; private BigDecimal amount; private Integer status; @TableLogic private Integer deleted; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }@TableId(type = IdType.ASSIGN_ID)对应DDL里的雪花ID,实体保存时不用手工set id。@TableLogic是逻辑删除注解,查询时Mybatis-plus自动追加deleted=0,手动调用removeById实际执行UPDATE deleted=1。订单、脱单心动记录这类需要保留轨迹的表,不要做物理删除。createTime和updateTime由MetaObjectHandler自动填充,字段类型用LocalDateTime而不是Date,避免后面Jackson序列化时格式到处改。@Data来自Lombok,setter/getter不用手写,但要注意给实体增加带业务含义的方法时别覆盖Lombok生成的setStatus,状态变更逻辑我建议收敛到service层。
3.3 条件构造器和分页:订单列表的三个查询套路
订单后台最常见的三个查询:按发布人查、按接单人查、管理端按状态查所有。用LambdaQueryWrapper一行一条:
LambdaQueryWrapper<RunOrder> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(RunOrder::getUserId, currentUserId) .eq(RunOrder::getStatus, 0) .orderByDesc(RunOrder::getCreateTime); Page<RunOrder> page = runOrderService.page(new Page<>(pageNum, pageSize), wrapper);逻辑说明:eq里第一个参数是方法引用,Mybatis-plus会把getUserId转成user_id,不用手写列名,重构字段时不容易断。Page对象第一个参数是页码,从1开始;pageSize别传超过100,移动端场景下拉加载一般20条一页就够。分页要生效必须在配置类里注册分页插件,这是Mybatis-plus 3.5以后最容易漏的一步,漏了page方法会变成先把全表查出来再内存分页,数据量一大直接卡死。
@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; } }分页插件maxLimit设置100是为了防止有人把pageSize传成10000来拖垮数据库。项目里如果遇到“只有第一页有数据,第二页查出来是空的”,先检查是不是在wrapper里用了group by,PaginationInnerInterceptor对带group by的count sql在某些版本会生成错误,常见做法是手动写count查询覆盖。
3.4 数据库连接配置:SSL、时区和连接池
application.yml里的DataSource配置直接关系到能不能连上、时间对不对、池够不够用。先贴我常用配置:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/campus?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password hikari: maximum-pool-size: 10 minimum-idle: 3 connection-timeout: 30000参数说明:useSSL=false在本地开发必须加,否则mysql8驱动默认尝试SSL握手,日志里会出现一串ssl handshake相关的warn甚至直接报错。serverTimezone=Asia/Shanghai解决“查出来时间比本地少8小时”的经典问题,原因是驱动默认按服务器时区转换。Hikari是SpringBoot 2.x默认连接池,maximum-pool-size对于校园项目10就够,别抄生产环境的50、100,小项目并发没到那个量,反而会把数据库连接占满。connection-timeout设30秒是给数据库瞬间重启留缓冲,别设成1秒,启动时有点抖动就误报连接超时。
4. SpringBoot接口与小程序端联调:登录态、状态机和请求封装
4.1 登录接口:wx.login换openid,再签发自定义token
微信小程序登录标准流程是:前端wx.login拿到code,后端拿code去微信接口换openid和session_key。openid相当于这个学校用户的唯一身份。但我不建议直接把openid在前后端间透传,后端要签发一个自定义token,前端每次请求放header里,后端校验通过才算登录。接口实现如下:
@PostMapping("/login") public R<String> login(@RequestBody WxLoginDTO dto) { String openid = wxApi.code2Session(dto.getCode()); User user = userService.getOne( new LambdaQueryWrapper<User>().eq(User::getOpenid, openid), false); if (user == null) { user = new User(); user.setOpenid(openid); user.setRole(0); userService.save(user); } return R.ok(tokenService.createToken(user.getId())); }逻辑说明:getOne的第二个参数传false,表示查不到多条时不抛异常,防止openid脏数据把登录接口打成500。新用户首次登录自动注册,role默认0普通用户。实际项目里campus、nickname这些放到“完善资料”接口里单独更新,而不是登录时一次收齐。tokenService.createToken内部生成一个32位随机串,存redis或user_extra表,设置7天有效期。
token校验我用HandlerInterceptor,在preHandle里从Authorization头取token,查不到或过期直接返回401,小程序端收到401统一跳回登录页。注意拦截器注册时要用addPathPatterns排除掉/login这类白名单,否则小程序端code还没换到token,请求登录接口就被拦住了。
4.2 订单接口:抢单、取消、完成的状态机
订单状态我定义为:0待接单、1进行中、2已完成、3已取消。这里最容易写坏的是“任意状态都能改到任意状态”,比如待接单订单被取消的同时另一台手机完成了它,就会产生一笔脏数据。建议至少维护一张状态流转表:
private static final Map<Integer, Set<Integer>> STATE_TRANSITIONS = new HashMap<>(); static { STATE_TRANSITIONS.put(0, new HashSet<>(Arrays.asList(1, 3))); // 待接 -> 进行中/取消 STATE_TRANSITIONS.put(1, new HashSet<>(Arrays.asList(2, 3))); // 进行中 -> 完成/取消 STATE_TRANSITIONS.put(2, Collections.emptySet()); // 完成终态 STATE_TRANSITIONS.put(3, Collections.emptySet()); // 取消终态 }接单操作是并发最敏感的地方。两个学生同时点“接单”,必须保证只有一个人能拿到这个订单。我用条件更新代替“先查再改”:
boolean grabbed = runOrderService.update( new LambdaUpdateWrapper<RunOrder>() .eq(RunOrder::getId, orderId) .eq(RunOrder::getStatus, OrderStatusEnum.PENDING.getCode()) .set(RunOrder::getRunnerId, currentUserId) .set(RunOrder::getStatus, OrderStatusEnum.RUNNING.getCode()) ); if (!grabbed) { return R.fail("手慢了,订单已被接走"); }逻辑说明:eq(RunOrder::getStatus, 0)是乐观锁的替代方案,update影响行数为0,说明状态已经被别人改掉,数据库行锁保证同一行只会有一个update成功。这比先select再update少一次网络往返,也不用给实体加@Version字段。
取消接口要注意权限:发布者取消自己的单,状态必须还是待接单;接单者想取消,我建议设计成“只能发起取消申请”,不要直接让接单者改状态。至少在接口里把runnerId和userId两个条件一起带上去校验,防止学生A传别人的orderId去取消。
4.3 小程序请求封装:token注入、401处理和缓存过期
小程序端封装一个request方法,所有页面统一走它。这段代码是常见做法的精简版,关键是把登录态和错误码收敛在同一个地方:
const request = (url, data = {}, method = 'GET') => { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method, data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' }, success(res) { if (res.statusCode === 401) { wx.removeStorageSync('token'); wx.reLaunch({ url: '/pages/login/login' }); return; } if (res.data && res.data.code !== 0) { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); return; } resolve(res.data.data); }, fail: reject }); }); };参数说明:Authorization每次从storage取,是因为token可能被登录页更新,模块加载时用闭包缓存变量容易拿到旧token。401统一reLaunch而不是navigateBack,token过期后页面栈里的旧页面可能继续发请求。res.data.code是后端统一返回体的业务码,0代表成功;业务失败时弹toast后再reject,页面里通过catch拿到错误做特殊处理。
校园项目有校区列表、用户标签这类不常变的数据,前端设置缓存并带过期时间:
const getCache = (key) => { const saved = wx.getStorageSync(key); if (saved && saved.expire > Date.now()) return saved.value; return null; }; const setCache = (key, value, expireSeconds = 300) => { wx.setStorageSync(key, { value, expire: Date.now() + expireSeconds * 1000 }); };这里没用wx.setStorage原生expire参数,是因为它在部分基础库版本上不保证生效,自己拼一个到期时间戳最可控。
4.4 订单表单:单选框、图片上传和顶部导航栏适配
下单页选择服务类型时用radio-group最省事,三个选项对应serviceType。需要注意:微信原生的radio组件样式在不同机型上宽度不一致,建议不用默认圆圈,改用自定义样式。用label包裹radio内容,点击整行都触发切换,避免用户只能点中那个小圆圈。请求参数里传的是value而不是文本,文本到类型映射交给后端枚举。
顶部导航栏的适配也别直接用系统状态栏高度硬算,不同机型胶囊按钮位置不一样。常见做法是调用wx.getMenuButtonBoundingClientRect拿到胶囊位置来计算自定义导航栏高度:
const menu = wx.getMenuButtonBoundingClientRect(); const statusBarHeight = wx.getSystemInfoSync().statusBarHeight; const navBarHeight = (menu.top - statusBarHeight) * 2 + menu.height; this.setData({ statusBarHeight, navBarHeight });参数说明:胶囊top减去状态栏高度等于状态栏到胶囊的间距,乘2是让导航栏上下留白对称,再加胶囊高度就是自定义导航栏的总高。页面里的占位视图高度要等于statusBarHeight+navBarHeight,不然列表页首行内容会被顶到胶囊下面。这块不调好,模拟器正常真机偏移,几乎是校园跑腿小程序最常见的“看起来没什么但就是丑”的问题。
5. 五个高频踩坑记录:从MySQL时区到小程序请求头
5.1 手写SQL查出来字段全是null
现象:Mybatis-plus内置方法查数据正常,自己写了一条select在控制台打印出来值正常,单测里读出对象却全是null。
原因:内置方法会把数据库下划线列名自动映射成驼峰属性,但XML里手写SQL如果写的是select id, order_no, user_id,Mybatis-plus不会自动把order_no转成orderNo。本质是手写SQL返回的列名还是下划线风格,没走驼峰自动映射。
解决:写成select id, order_no as orderNo, user_id as userId,或者干脆在Mapper方法上钉死@Results注解。我一般会让项目约定“手写SQL全部显式起别名”,比谁去背映射规则靠谱。排查时先看SQL日志里查出来的列名,再对照实体属性名,十有八九是别名问题。
5.2 MySQL连接报错或时间差8小时
现象:本地连mysql8出现SSL相关错误,或者数据库存的是14:00,查出来变成06:00。
原因:mysql8驱动默认尝试SSL握手,且JDBC URL没声明useSSL和serverTimezone,时间被驱动按默认时区转换。
解决:URL里固定带useSSL=false&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8。改配置后记得重启,连接池会把老连接保持一段时间,刚改完报错依旧很容易误导排查方向。如果还报错,在服务器上跑一下mysql的时区变量,确保数据库和驱动的时区设置一致。
5.3 wx.request在iOS上后端读不到请求参数
现象:同一套wx.request代码在Android和开发者工具上都正常,iPhone上一提交订单后端就报“请求体为空”。
原因:微信在部分iOS基础库版本把content-type改写成了application/json;charset=utf-8,带charset的请求头在某些SpringBoot组合下,@RequestBody按ISO-8859-1解,导致中文乱码或body解析失败。
解决:后端显式配置UTF-8过滤器,或在小程序端header里每次写死'Content-Type': 'application/json'。header对象不要用变量拼接,写死最稳。还有个别情况是域名在小程序后台没配request合法域名,iOS对不明域名处理更严格,表现为偶发请求失败,这种要去微信公众平台把域名加到白名单再试。
5.4 SpringBoot版本太高:javax被jakarta替换
现象:照着老教程写的import javax.annotation.Resource死活编译不过,或启动时一堆ClassNotFoundException。
原因:SpringBoot 3.x基于Jakarta EE 9,原生包名从javax.迁移到jakarta.,老教程代码自然失效。加上部分旧版Mybatis-plus starter也未适配SpringBoot 3.
解决:做校园项目我建议直接用SpringBoot 2.7.x配JDK 8/11,所有教程和starter兼容性最好。如果非要SpringBoot 3,选配套的mybatis-plus-spring-boot3-starter,把javax.servlet改成jakarta.servlet。版本这条是“能跑就少动”,没有明确收益不要升级框架大版本。
5.5 全局XSS过滤器把PDF上传和订单内容一起误杀
现象:后端加了一个全局XSS过滤器之后,上传PDF文件打不开,订单备注里写“<5元”被存成了“<5元”。
原因:过滤器用正则把请求体里的尖括号、斜杠直接替换或转义,没区分Content-Type。PDF文件本质是二进制,被按文本过滤后字节流直接损坏;订单备注是正常文本,又被误当成HTML转义。
解决:过滤器加白名单,只过滤Content-Type为application/json的接口,对multipart/form-data和application/pdf直接放行。订单备注这类“用户输入但会原样展示”的字段,存储时不做转义,出参展示时再按前端组件要求做富文本安全过滤。如果你接的是老系统,记得排查历史数据是否已经被转义污染过,污染了的备注要写脚本回滚。
6. 批量插入、脱敏输出和上线前验证:让项目从“能跑”到“能用”
6.1 Mybatis-plus批量插入要打开rewriteBatchedStatements
给脱单模块做“批量保存用户标签”时,别在for循环里一条条save,数据量不大但会产生几十次网络往返。用saveBatch即可,但还有一层容易被忽略:MySQL JDBC默认不开批量rewrite,即使saveBatch底层走的也是每条独立insert。在数据库连接URL上加参数rewriteBatchedStatements=true,才能真正把多条insert合并成一条多值insert,实测这种场景耗时能下降一半以上。
url: jdbc:mysql://127.0.0.1:3306/campus?useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true6.2 脱单模块的资料脱敏输出
脱单资料挂到个人页时,手机号和学号不能原样给所有用户看,后端出参时统一脱敏。我习惯在VO转换层做,而不是改动实体再存回去:
public String maskPhone(String phone) { return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); }规则说明:$1和$2是正则分组引用,隐含逻辑是手机号前3位和后4位保留,中间4位打码。校园场景里这层脱敏够用,更严格的方案是数据库层面只存脱敏后字段,但会牺牲运营方客服核单能力。
6.3 上线前验证清单:状态机跑一遍,真机测一轮
我自己的习惯是把订单状态机所有非法跳转列成表格,一条条在本地接口测试里跑过再提审。用日志把每次状态变更的orderId、oldStatus、newStatus、操作人userId打印出来,跑完之后grep日志,确认没有任何“非终态订单被重复完成”的记录。接着用微信开发者工具的Network面板核对每个接口的耗时和状态码,再借一台iPhone和一台Android做真机回归,重点看着导航栏高度、单选框点击区域和登录失效跳转。提交微信小程序年审前,在公众平台的隐私保护指引里声明会收集手机号、位置信息,否则审核会被隐私接口驳回。
从选型到落库、从登录态到状态机,这套方案真正难的不是某个框架的API,而是把订单和脱单两类业务边界想清楚。希望这篇笔记能让你少走几段弯路。
本文还有配套的精品资源,点击获取