最近整理了手上一个Spring Boot + 微信小程序方向的毕设项目,是宠物医院微信小程序。这个题目在毕业设计里属于比较典型的管理系统类项目:前端用一个微信小程序,后台用Spring Boot搭一套接口服务,再配一个Web管理端,做完基本就能覆盖“需求分析、数据库设计、接口开发、联调测试、部署上线”整条链路。
这个项目解决的核心问题是宠物医院传统就诊流程里的几个痛点:排队挂号靠现场、宠物病历散落在纸质本子上、主人换一家医院就得重新填一堆信息、医生开完处方后缴费也要来回跑。把这些流程挪到小程序里,让用户在手机上完成预约、充值、查报告,医生在后台维护接诊信息和处方,管理员管理医生排班和药品库存,整个闭环就通了。
这篇文章不打算写成“项目说明书”,而是复盘我在做这个项目的过程中实际遇到的技术选型问题、数据库设计细节、小程序端适配和上线的坑,适合正在选毕设题目或者想做同类型项目的同学参考,也适合想了解Spring Boot和小程序怎么对接的开发者。
1. 内容整体设计与思路拆解
1.1 为什么选“宠物医院+微信小程序”这个题目
选这个题目有现实需求和技术布景两个原因。现实方面,养宠人群这些年增长很快,宠物医院普遍存在预约靠电话、病历靠纸质、就诊高峰排队时间长的现象,数字化改造的需求是真实存在的。技术方面,微信小程序不需要用户安装,用完即走,对线下服务场景特别友好,而Spring Boot作为后端框架生态成熟、资料多、上手门槛适中,恰好适合毕业生在有限时间内做出一套完整系统。
项目的核心价值不是“造一个很酷的东西”,而是把业务闭环做清楚:用户端能预约、能支付、能看报告,医生端能接诊开方,管理端能排班管库存。三端数据打通,业务流才完整。
1.2 技术选型背后的考量
这个项目选型有几个关键决策,每个决策背后都有实际理由:
-
后端用Spring Boot 2.7.x而不是3.x。Spring Boot 3要求JDK 17,虽然性能更好,但很多教材、博客、依赖库的版本适配都还停留在2.x,校园场景下出问题不好查资料,所以我选择了2.7.x,JDK用1.8。这里没有“越新越好”的必要性,毕设追求的是稳定、可控、资料丰富。
-
持久层框架选MyBatis-Plus而不是原生MyBatis。MyBatis-Plus内置了通用CRUD、分页插件、代码生成器,能省掉大量重复的Mapper XML编写时间。实际开发中我用它做基础单表操作,复杂多表联查仍然手写SQL,两者结合效率最高。
-
数据库选MySQL 8.0。8.0支持窗口函数、JSON类型,且默认字符集调整为utf8mb4,对emoji(宠物昵称里经常出现)也能正常存储。存储方面用Docker部署MySQL,开发环境与生产环境保持一致。
-
文件存储用MinIO。项目里有宠物头像、处方单图片、检查报告等文件上传需求,提前引入MinIO而不是把文件直接丢在项目目录里,是为了后面部署到云服务器时不至于因为重装导致数据丢失。MinIO部署轻量,提供了与S3兼容的API,接入成本不高。
-
缓存用Redis,主要用于两个场景:一个是微信access_token的缓存,另一个是预约时段库存的预扣。如果只做静态展示系统,Redis可以不加,但加上之后能明显体现对并发场景的思考。
为什么不选uniapp?如果目标只是微信小程序,原生小程序语法虽然写起来繁琐一点,但没有跨端编译带来的样式兼容问题,预览和真机调试也更直接。uniapp的优势是多端复用,这个项目只需要小程序和Web管理端,管理端用Vue 3 + Element Plus单独写,两边天然分离,不需要共享代码。
1.3 系统角色与功能模块划分
系统设计了三类角色,分别在小程序端和Web管理端使用不同功能。
| 角色 | 使用端 | 核心功能 |
|---|---|---|
| 普通用户(宠物主人) | 微信小程序 | 注册登录、添加/维护宠物档案、在线预约挂号、余额充值、支付订单、查看挂号记录与检查报告、评价反馈 |
| 医生 | Web管理端 | 查看当日接诊列表、填写诊断结果、电子开处方、管理常用模板、查看个人排班 |
| 管理员 | Web管理端 | 管理科室与医生信息、排班管理、宠物主人账号管理、药品库存管理与入库、营收报表、系统公告 |
这个功能清单里的每一块都要能“跑通”。比如用户预约后医生端必须能看到预约记录,医生开处方后用户端必须能查到处方详情并且能发起缴费。任何一条链路断了,演示的时候就很容易被导师或答辩老师抓到漏洞。
我用一个实际场景串联整个系统:用户打开小程序,注册并添加宠物档案,选择“皮肤科”和对应医生,查看未来7天可预约的时间段,选定后支付挂号费。医生在管理端看到新预约,接诊后在系统里记录症状、诊断和用药建议,并开出电子处方。用户在小程序端收到处方通知,查看详情并完成缴费。管理员在后台统计这笔收入,同时处方中的药品自动扣减库存。这个场景贯穿了至少6张核心表,后面数据库设计时会逐一说明。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与核心表结构解析
2.1 核心表怎么拆才合理
数据库设计是所有管理系统的地基,表拆得不好,后面的接口开发会到处碰壁。这个项目我拆了12张表,核心表包括:
- sys_user(用户表):存放用户微信openid、unionid、昵称、头像、手机号、余额、会员等级。
- pet_info(宠物档案表):关联sys_user,存储宠物昵称、品种、性别、生日、体重、疫苗记录、绝育状态。
- vet_doctor(医生表):关联科室表,包含医生姓名、职称、简介、擅长方向、头像、排班状态。
- vet_department(科室表):科室名称、位置、简介、图标。
- appointment_order(预约挂号表):核心表,记录用户、宠物、医生、科室、预约时间段、状态、支付金额。
- medical_record(就诊记录表):医生每次接诊后填写的主诉、诊断、处理意见、注意事项。
- prescription(处方表)与prescription_item(处方明细表):一主多子表的经典结构,明细表记录药品、用量、次数、天数。
- drug_info(药品表)与drug_stock(库存表):药品基本信息与库存分开,方便后期做批次管理。
- recharge_record(充值记录表):用户通过小程序充值余额的记录。
- comment_info(评价表):用户就诊后的评分与评论。
关键是在“数据归属”上想清楚:宠物档案属于用户,就诊记录属于宠物,订单既要关联用户也要关联宠物和医生。有些同学在表设计时把订单只关联到用户,结果医生想查“今天有哪些宠物来看病”就写不出SQL,这就是关系建模没做好。
2.2 预约表字段设计与索引优化
预约挂号表是整个系统里最繁忙的表,我把它的字段设计单独拿出来说明:
sql复制CREATE TABLE appointment_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL COMMENT '订单编号',
user_id BIGINT NOT NULL COMMENT '用户ID',
pet_id BIGINT NOT NULL COMMENT '宠物ID',
doctor_id BIGINT NOT NULL COMMENT '医生ID',
dept_id BIGINT NOT NULL COMMENT '科室ID',
appointment_date DATE NOT NULL COMMENT '就诊日期',
time_slot VARCHAR(20) NOT NULL COMMENT '时间段 如 09:00-09:30',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态 0待支付 1已支付 2已接诊 3已取消 4已完成',
pay_amount DECIMAL(10,2) NOT NULL COMMENT '挂号金额',
pay_type TINYINT COMMENT '支付方式 1余额 2微信支付 3线下',
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
KEY idx_user_id (user_id),
KEY idx_doctor_date_status (doctor_id, appointment_date, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约挂号表';
这个表有两个设计细节值得注意。第一个是组合索引idx_doctor_date_status,因为最频繁的查询是“某医生某天有哪些时间段可以预约”,这个索引能直接覆盖查询条件,避免文件排序。第二个是status字段用TINYINT而不是字符串,好处是查询快、状态流转清晰,Java后端用枚举类对应,不至于到处出现魔法数字。
时间段的处理也要说一下。我没有用标准的datetime字段来存储“预约时间段”,而是拆成日期字段加字符串时间段,因为小程序的日期选择器返回的是日期,时间段的展示也是固定几个档位(比如09:00-11:00每半小时一个档)。如果合并成一个datetime,前端反而不容易做slot判断。实际开发中字段设计有时候需要向前端展示方式妥协,这是正常的。
2.3 为什么用MyBatis-Plus做CRUD和分页
数据库操作层面用MyBatis-Plus省了大量时间。这里举一个分页查询的例子,搜索热词里很多人问“mybatis的分页插件的用法”,其实就是三步:
第一步,在配置类里注册分页插件:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
第二步,在Service层直接调用Page对象:
java复制Page<AppointmentOrder> page = new Page<>(current, size);
LambdaQueryWrapper<AppointmentOrder> wrapper = Wrappers.lambdaQuery();
wrapper.eq(AppointmentOrder::getUserId, userId)
.eq(AppointmentOrder::getStatus, 1)
.orderByDesc(AppointmentOrder::getCreateTime);
appointmentOrderMapper.selectPage(page, wrapper);
第三步,把page.getRecords()返回给前端,同时带上page.getTotal()作为总条数。
这里有个容易被忽略的坑:分页插件的 count 查询需要在写复杂 join 时留意性能,如果联的表多且数据量大,count语句可能很慢。我的经验是列表页只查主表加必要的关联字段,明细数据单独再查一次,不要在列表页把所有关联表都join进来。
3. 微信小程序端核心功能实现
3.1 小程序基础架构与请求封装
小程序端我采用的是“原生框架 + 分包模式”。没有引入太重的UI框架,因为宠物医院项目页面数量中等,原生写反而更可控。但基础架构一定要搭好,否则后期加页面会乱。
请求封装是其中一个重点。小程序里所有网络请求都通过wx.request,但如果每次请求都直接写,代码会非常冗余。我封装了一个request工具类,统一处理token、错误提示、加载状态:
javascript复制const request = (options) => {
return new Promise((resolve, reject) => {
wx.showLoading({ title: '加载中' })
wx.request({
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
'Authorization': getToken(),
'content-type': 'application/json'
},
success: (res) => {
wx.hideLoading()
if (res.data.code === 200) {
resolve(res.data)
} else if (res.data.code === 401) {
wx.removeStorageSync('token')
wx.navigateTo({ url: '/pages/login/login' })
} else {
wx.showToast({ title: res.data.msg, icon: 'none' })
reject(res.data)
}
},
fail: (err) => {
wx.hideLoading()
wx.showToast({ title: '网络错误', icon: 'none' })
reject(err)
}
})
})
}
统一的错误码处理特别重要。后端返回401说明token过期,小程序端直接清理登录状态并跳转登录页;返回500则提示服务端异常。不要把所有错误都丢给用户去理解,用户只需要看到友好的提示。
3.2 顶部导航栏高度适配问题
搜索热词里老是出现“微信小程序顶部导航栏高度”,因为不同型号的手机胶囊按钮位置不一样。如果页面用了自定义导航栏,直接硬编码高度是绝对会出问题的。
我的处理方式是写一个系统信息工具:
javascript复制const SYSTEM_INFO = wx.getSystemInfoSync()
const MENU_BUTTON = wx.getMenuButtonBoundingClientRect()
module.exports = {
statusBarHeight: SYSTEM_INFO.statusBarHeight,
menuButtonTop: MENU_BUTTON.top,
menuButtonHeight: MENU_BUTTON.height,
navBarHeight: MENU_BUTTON.height + (MENU_BUTTON.top - SYSTEM_INFO.statusBarHeight) * 2
}
导航栏总高度 = 状态栏高度 + 胶囊按钮高度 + 胶囊上下间距。不要自己猜数值,wx.getMenuButtonBoundingClientRect()是官方提供的能力,拿到胶囊的实际坐标后再动态计算,不同机型渲染就一致了。
3.3 挂号预约与平衡支付的设计
预约模块是小程序端的核心,它的交互流程是:选择科室 -> 选择医生 -> 选择日期 -> 选择时间段 -> 确认订单 -> 支付。这里有两个容易踩坑的地方。
第一个是时间段的展示。后端返回的可预约时间段应该是“已经扣掉已预约数量之后的剩余slot”,而不是让前端自己去算还能不能约。前端只负责把数据展示出来,判断逻辑放在后端,避免用户端数据不一致。
第二个是支付方式。项目里我设计了两种支付方式:余额支付和微信支付。
为什么选择余额支付作为默认方式?因为微信小程序对虚拟支付有严格的类目限制,而挂号、在线问诊这类服务如果直接拉起微信支付,在审核上有一定风险。余额充值时先用微信支付充值,再使用余额支付挂号费,这是很多同类型项目的通用做法,确保流程不触碰平台红线。
余额支付时后端需要扣减余额并创建订单,这两步要放在一个事务里。我先查用户余额,判断余额是否充足,再执行扣减并创建订单。这里要加乐观锁或者UPDATE ... WHERE balance >= amount,防止并发下超扣。
3.4 宠物档案与图片上传
宠物档案页面里,用户要上传宠物头像,这里涉及图片上传问题。小程序端通过wx.uploadFile上传到后端接口,后端再将文件转存到MinIO。上传时要限制文件格式和大小,我的后端接口只接受jpg、png、webp格式,大小限制在5MB内。
图片上传的坑经常出现在“临时文件路径”上。wx.chooseImage拿到的临时路径只在本次启动内有效,一定要在拿到临时路径后立即上传,不能存下来下次再用。
4. 后端Spring Boot核心实现
4.1 项目分层与统一返回结构
后端项目我严格按Controller-Service-Mapper三层结构划分,Controller层只做参数接收和返回,Service层写业务逻辑,Mapper层处理数据库操作。这种分层的好处是职责清晰,出问题时定位快。
统一返回结构是必做的,否则前端处理每个响应都要判断不同字段。我定义了一个Result<T>类:
java复制@Data
public class Result<T> {
private Integer code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("操作成功");
result.setData(data);
return result;
}
public static <T> Result<T> error(Integer code, String msg) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMsg(msg);
return result;
}
}
同时配了一个全局异常处理器,统一捕获业务异常和未知异常,避免把异常堆栈直接抛给前端。
4.2 全局过滤器处理XSS攻击
搜索热词里有一条“springboot项目全局过滤器处理上传pdf文件时xss攻击”,说明这是一个大家都会碰到的安全问题。XSS攻击的本质是恶意脚本注入,最有效的防御手段是“过滤输入+转义输出”。
我实现了一个XSS过滤器,继承OncePerRequestFilter,对所有非文件上传的请求体做清洗:
java复制public class XssFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (isFileUploadRequest(request)) {
filterChain.doFilter(request, response);
return;
}
XssHttpServletRequestWrapper wrappedRequest = new XssHttpServletRequestWrapper(request);
filterChain.doFilter(wrappedRequest, response);
}
}
核心逻辑在XssHttpServletRequestWrapper里,重写getParameter和getInputStream,对HTML特殊字符做转义,比如<script>变成<script>,这样脚本就无法在页面上执行。
文件上传请求要跳过过滤,因为pdf、图片里的内容不是文本清洗能搞定的,而且清洗文件内容会破坏文件完整性。文件名可以做安全处理,但文件内容不能动。
4.3 预约时间段防重复的并发控制
预约模块最大的并发风险是“同一个时间段被两个人同时抢到”。这个问题的解法我选择了数据库唯一约束加Redis预扣:
数据库层面,给appointment_order表添加唯一约束uk_doctor_slot (doctor_id, appointment_date, time_slot, status),那是否意味着一个医生一天同一个时间段只能有一个预约?不完全是。这里我把status放进唯一约束中,只对“待支付”和“已支付”状态加锁,保证一个医生的同一个时间段不会被两个人同时占住。
Redis预扣的做法是:预约时间段库存先加载到Redis,用户发起预约时先用DECR操作预扣,如果返回负数说明没号了,直接返回“该时间段已被约满”。用户最终取消预约时,执行INCR释放库存。
只有Redis其实不够,因为Redis和MySQL需要一致性。我的策略是:Redis只做前置拦截,最终数据一致性还是靠数据库事务保证。极端情况下Redis扣了但数据库插入失败,要利用定时任务做补偿,每5分钟对账一次。
4.4 文件上传接入MinIO
MinIO接入Spring Boot非常简单,只需要引入minio依赖,配置endpoint、accessKey、secretKey,然后写一个存储服务类:
java复制@Service
public class MinioService {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.bucket-name}")
private String bucketName;
public String upload(MultipartFile file, String objectName) throws Exception {
MinioClient client = MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
client.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build());
return endpoint + "/" + bucketName + "/" + objectName;
}
}
实际部署时的经验是:MinIO的endpoint内部地址和外部访问地址不能混淆。服务器上用Docker容器时,Java容器内访问MinIO要用http://minio:9000这种容器网络地址,但返还给用户图片的URL必须替换为公网地址。一个具体的做法是存储URL时存“相对路径”,返回时根据请求所在的域名动态拼接,这样在开发和部署不同环境切换时不用改动数据库。
上传对象名我用UUID拼接原始文件名,避免中文文件名和重复文件名导致的问题:
java复制String objectName = UUID.randomUUID().toString().replace("-", "") +
file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
4.5 全局跨域配置与JWT登录校验
小程序端请求后端时,跨域问题虽然微信开发者工具里不强制要求(和H5不同),但配置好跨域能避免很多联调时的疑惑。
我提供了一个全局CORS配置:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(Registry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
登录校验使用JWT,用户首次登录时通过wx.login拿code换取openid,后端生成token返回,小程序端之后每次请求头携带token。拦截器里校验token有效性,拦截所有非白名单路径。
这种模式下有一个实际经验:token中不要存放用户敏感信息,只存userId和过期时间。换取用户信息时再查数据库,保证数据实时性,也避免token被截获后的信息泄露。
5. 部署上线与常见问题排查
5.1 Docker部署Spring Boot项目
项目开发完成后的部署,我用的是Docker Compose方式。先对Spring Boot项目执行Maven打包:
bash复制mvn clean package -DskipTests
然后写Dockerfile:
dockerfile复制FROM openjdk:8-jdk-alpine
COPY target/pet-hospital.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar", "--spring.profiles.active=prod"]
docker-compose里编排MySQL、Redis、MinIO和应用服务,依赖关系用depends_on控制。
这里提一个实际遇到过的问题:Spring Boot项目在Docker中的时间比宿主机快8小时。原因是容器默认使用UTC时区,而项目数据库连接用了北京时间。解决方法是Dockerfile里加上时区设置:
dockerfile复制RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' > /etc/timezone
时区问题在预约系统里特别致命,如果服务器时间不一致,用户看到的可预约时段会整体偏移,所以部署后第一件事就是检查时间。
5.2 微信小程序注册、年审与发布
小程序上线和Web不一样,步骤相对繁琐。第一步要注册小程序账号,选择企业主体还是个人主体。宠物医院项目如果作为毕设展示,一般用个人主体即可,但个人主体无法开通微信支付。如果项目需要微信支付生产环境,需要企业主体,并且认证服务号,这个过程在毕设答辩前要提前安排,不要临时抱佛脚。
这里也提一下“年审”:微信小程序每年需要做一次年审,年审时如果小程序没有违规记录,过程比较快。但要注意,年审前后有一段时间,如果涉及类目和功能变更,可能会被抽检。项目上线后不是一劳永逸,要保持对平台规则的关注。
发布版本时分为开发版、体验版、正式版。日常真机调试用开发版,给导师演示用体验版会更稳定,因为体验版不依赖本地调试工具。正式版提审前要仔细检查类目是否匹配、隐私协议是否完整、用户授权是否合规。特别是今年微信平台对隐私保护协议审核严格了,小程序里如果用了相册、摄像头、联系方式,都要在隐私协议里明确说明。
5.3 常见问题速查表
开发过程中攒了很多问题,整理成速查表,按我实际遇到的频率排序:
| 问题 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| 真机预览请求不到后端 | 开发者工具正常,手机白屏 | 手机和服务器不在同一网络,或未配置合法域名 | 本地调试改用局域网IP,发布版必须配HTTPS域名 |
| 微信登录获取不到头像昵称 | 返回灰色头像和“微信用户” | 头像昵称填写能力调整,不再自动返回 | 引导用户手动设置头像昵称,或使用头像昵称填写能力组件 |
| 加减库存不一致 | 高峰期超卖 | 数据库层面没有锁 | Redis预扣前置,数据库唯一约束兜底 |
| 上传图片无法预览 | 图片上传成功但打不开 | MinIO上传了内网地址 | 上传时保存相对路径,返回时拼当前域名 |
| 订单时间偏差8小时 | 预约日期显示错位 | Docker容器UTC时区 | Dockerfile统一设为Asia/Shanghai |
| 医生端查不到新预约 | 刷新后数据不出现 | 关闭了Redis缓存但页面有缓存 | 管理端请求加上时间戳参数,禁用浏览器缓存 |
| 小程序审核被拒 | 理由为“功能涉及虚拟支付” | 挂号费通过微信支付直接支付 | 改为余额充值+余额支付模式,规避限制 |
这些问题的共性是:开发阶段看不出来,真机测试或上线阶段才暴露。所以最稳妥的做法是提前准备一台真机,从第一版功能开始就做真机联调,不要等全部开发完再集中测。
5.4 答辩时的演示路线建议
毕设答辩时会被问得最多的是这几点:系统的创新点、数据库设计的合理性、自己负责的技术难点。演示时不要从头到尾把每个页面走一遍,太消耗时间,而是按业务场景走完一个闭环最有效。
我的建议路线是:演示首页展示预约入口,选择医生和时间段并支付,切到医生端管理界面看到新预约,点击开始接诊填写病历和处方,切回小程序端查看处方明细,最后在管理后台看到订单完成和库存扣减记录。整条链路3到5分钟,却能完整展示全部核心模块,也体现出“前后端贯通”的整体意识。
技术难点答辩可以重点讲预约并发控制、XSS过滤、图片存储选型这几个点。这些话题都有明确的思考过程和技术方案,比泛泛而谈“熟悉Spring Boot”有说服力得多。
最后说一点实际的体会。做这类毕设项目,最大的坑不是技术不会,而是前期需求没想清楚。我没做项目管理就直接开写,中途因为“用户能不能同时挂两个宠物”这个需求反复改了三版表结构。如果重来一次,我会先花两天把完整的业务流程图画出来,把每个角色的动作和对应的数据流向理清楚,再开始建工程。第二位的是不要盲目追求新技术,把基础框架跑稳,把闭环打通,比塞一堆花哨的功能有用。这个项目后续要扩展的话,建议方向是增加疫苗提醒、驱虫周期管理、慢病回访,这些在真实场景里都是高频需求,技术上属于现有数据的二次利用,不会改动核心表结构。
