
设备坏了找不到人修、报修流程靠口头传达、维修进度无法跟踪这类问题在教学楼和实验室里其实非常常见。本文基于微信小程序 Spring Boot 技术栈完整实现一个教学设备报修系统覆盖需求分析、表结构设计、后端接口开发、小程序端页面搭建、消息通知以及上线排错全流程。系统采用原生微信小程序语法开发后端使用 Spring Boot 2.x 与 MyBatis Plus整体结构清晰、代码可直接复用既能作为毕业设计参考也能作为实际项目的初始版本。1. 教学设备报修系统的需求与整体设计在开始写代码之前先弄清楚系统到底要解决什么问题。只有把业务流程理清楚后端的表结构和接口设计才会有依据。1.1 传统设备报修方式的问题在很多学校场景中设备报修仍然依赖以下几种方式电话联系管理员管理员手工登记信息。填写纸质报修单流转效率低。通过微信群消息报修消息容易被刷掉维修结果无从追踪。这些方式的共同问题是报修信息不完整、维修人员响应慢、维修进度不透明、设备历史记录无归档。设备状态和使用寿命无法形成数据资产后续做设备维保统计分析时也没有数据支撑。1.2 系统需要覆盖的角色一个完整的教学设备报修系统至少要包含三类角色角色核心诉求主要操作学生/教师快速发起报修、查看处理进度选择设备、填写故障描述、提交报修单、查看本人记录维修人员及时接收任务、更新处理结果查看待处理工单、接单、填写处理意见、标记完成系统管理员掌握全局情况、分配任务、统计分析管理设备、管理用户、分配维修人员、查看统计报表1.3 系统核心流程整个报修流程可以用一组状态来描述待处理(0) - 处理中(1) - 已完成(2) | | | -- 已驳回(3) - 已驳回(3)用户进入小程序通过微信授权登录。用户选择故障设备填写故障描述和照片提交报修单。管理员或维修人员看到待处理的报修单更新状态为“处理中”。维修完成后更新状态为“已完成”并填写处理说明。用户随时在报修记录列表查看最新状态。1.4 技术选型说明这里采用的技术方案在校园类项目和毕设项目中非常常见技术成熟、参考资料多、开发效率高小程序端原生微信小程序使用 WXML、WXSS、JavaScript。后端Spring Boot简化配置内置 Tomcat方便快速部署。ORMMyBatis Plus代码量少分页和条件构造器都很方便。数据库MySQL存储用户、设备、报修单、处理记录等数据。工具微信开发者工具、IDEA、Navicat、Postman。2. 环境准备与项目结构2.1 环境版本版本需要根据你的实际环境调整下面给出的是常见组合JDK 1.8 或 11Spring Boot 2.3.x 或 2.7.xMySQL 5.7 或 8.0MyBatis Plus 3.4.xMaven 3.6微信开发者工具稳定版即可一个已注册的小程序 AppID测试阶段可用测试号2.2 后端项目目录结构建议按下面的包结构组织代码避免所有类堆在一个包下面src/main/java/com/example/repair/ ├── RepairApplication.java ├── common/ │ ├── Result.java │ ├── PageResult.java │ └── GlobalExceptionHandler.java ├── config/ │ └── MybatisPlusConfig.java ├── controller/ │ ├── UserController.java │ ├── DeviceController.java │ ├── RepairOrderController.java │ ├── WechatController.java │ └── UploadController.java ├── entity/ │ ├── SysUser.java │ ├── Device.java │ ├── RepairOrder.java │ └── RepairLog.java ├── mapper/ │ ├── SysUserMapper.java │ ├── DeviceMapper.java │ ├── RepairOrderMapper.java │ └── RepairLogMapper.java ├── service/ │ ├── UserService.java │ ├── DeviceService.java │ ├── RepairOrderService.java │ └── WechatService.java └── service/impl/ └── RepairOrderServiceImpl.java2.3 小程序项目目录结构miniprogram/ ├── app.js ├── app.json ├── app.wxss ├── utils/ │ ├── request.js │ └── auth.js ├── pages/ │ ├── index/ // 首页展示设备分类和快捷入口 │ ├── report/ // 发起报修页 │ ├── records/ // 报修记录列表 │ ├── detail/ // 报修详情页 │ └── mine/ // 个人中心3. 数据库表结构设计数据库设计是整个系统的地基。字段和索引设计不合理后面写接口时就会不断返工。3.1 用户表 sys_user用户表主要保存微信用户的 openid、昵称、头像以及角色信息。注意 openid 是微信体系下用户的唯一标识必须加唯一索引。CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, openid VARCHAR(64) DEFAULT NULL COMMENT 微信openid, nickname VARCHAR(64) DEFAULT NULL COMMENT 昵称, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像地址, phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, role TINYINT NOT NULL DEFAULT 1 COMMENT 角色1-普通用户2-维修人员3-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;3.2 设备表 device设备表记录资产位置、名称、分类和当前状态。设备状态和设备报修状态是两个不同概念需要区分开。CREATE TABLE device ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, device_code VARCHAR(64) DEFAULT NULL COMMENT 设备编号, device_name VARCHAR(128) NOT NULL COMMENT 设备名称, category VARCHAR(64) DEFAULT NULL COMMENT 设备分类如投影仪、电脑、空调, location VARCHAR(128) DEFAULT NULL COMMENT 存放位置如A栋201, status TINYINT NOT NULL DEFAULT 0 COMMENT 设备状态0-正常1-维修中2-报废, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_category (category), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT设备表;3.3 报修单表 repair_order报修单是系统的核心业务表。为了查询方便这里冗余了设备名称、报修人等信息避免每次关联多张表。CREATE TABLE repair_order ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, repair_no VARCHAR(32) NOT NULL COMMENT 报修单号, device_id BIGINT NOT NULL COMMENT 设备ID, device_name VARCHAR(128) DEFAULT NULL COMMENT 设备名称冗余, location VARCHAR(128) DEFAULT NULL COMMENT 设备位置冗余, reporter_id BIGINT NOT NULL COMMENT 报修人ID, reporter_name VARCHAR(64) DEFAULT NULL COMMENT 报修人姓名冗余, reporter_phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, description TEXT COMMENT 故障描述, images VARCHAR(1000) DEFAULT NULL COMMENT 图片地址多个用逗号分隔, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0-待处理1-处理中2-已完成3-已驳回, priority TINYINT NOT NULL DEFAULT 2 COMMENT 优先级1-紧急2-普通, assignee_id BIGINT DEFAULT NULL COMMENT 维修人员ID, assignee_name VARCHAR(64) DEFAULT NULL COMMENT 维修人员姓名, handle_remark VARCHAR(500) DEFAULT NULL COMMENT 处理说明, handle_time DATETIME DEFAULT NULL COMMENT 处理完成时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_repair_no (repair_no), KEY idx_reporter (reporter_id), KEY idx_status (status), KEY idx_assignee (assignee_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报修单表;3.4 处理记录表 repair_log每一次状态变更都写入日志方便后续追溯“谁在什么时间做了什么操作”。CREATE TABLE repair_log ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, order_id BIGINT NOT NULL COMMENT 报修单ID, operator_id BIGINT DEFAULT NULL COMMENT 操作人ID, operator_name VARCHAR(64) DEFAULT NULL COMMENT 操作人姓名, action VARCHAR(32) DEFAULT NULL COMMENT 动作CREATE/ASSIGN/PROCESS/FINISH/REJECT, remark VARCHAR(500) DEFAULT NULL COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报修处理日志表;4. Spring Boot 后端核心接口实现后端的职责是提供小程序端需要的全部接口。下面重点讲解登录、报修单创建、状态流转、列表分页和统计接口。4.1 微信登录接口获取 openid小程序端通过wx.login获取临时 code后端拿 code 向微信接口换取 openid。真实的 appid 和 secret 要从微信公众平台获取生产环境不要硬编码在代码里建议放到配置文件或环境变量中。// 文件路径src/main/java/com/example/repair/controller/WechatController.java RestController RequestMapping(/api/wechat) public class WechatController { Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody LoginRequest request) { // 1. 使用 code 调用微信接口获取 openid String url https://api.weixin.qq.com/sns/jscode2session?appid appId secret appSecret js_code request.getCode() grant_typeauthorization_code; RestTemplate restTemplate new RestTemplate(); String response restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(response); String openid json.getString(openid); // 2. 根据 openid 查找用户不存在则自动注册 SysUser user userService.findOrCreateUser(openid); return Result.success(user); } }这里有几个注意点code 是一次性的五分钟内有效只能使用一次。换取的 session_key 在小程序端有时需要用来解密手机号等敏感信息。后端返回给前端的不建议直接传 openid更推荐的做法是返回你自己生成的 token后续请求通过 token 识别用户。4.2 创建报修单接口创建报修单是整个系统中最核心的接口。需要注意的是设备状态要同步更新为“维修中”同时要写入一条日志记录方便追踪整个流程。// 文件路径src/main/java/com/example/repair/service/impl/RepairOrderServiceImpl.java Override Transactional(rollbackFor Exception.class) public RepairOrder createOrder(RepairOrderCreateRequest request, Long reporterId) { // 1. 校验设备是否存在 Device device deviceMapper.selectById(request.getDeviceId()); if (device null) { throw new BusinessException(设备不存在); } // 2. 生成报修单号规则日期 随机数 String repairNo BX DateUtil.format(new Date(), yyyyMMddHHmmss) RandomUtil.randomNumbers(4); // 3. 组装报修单 RepairOrder order new RepairOrder(); order.setRepairNo(repairNo); order.setDeviceId(device.getId()); order.setDeviceName(device.getDeviceName()); order.setLocation(device.getLocation()); order.setReporterId(reporterId); order.setDescription(request.getDescription()); order.setImages(request.getImages()); order.setPriority(request.getPriority()); order.setStatus(0); // 待处理 repairOrderMapper.insert(order); // 4. 同步更新设备状态为维修中 device.setStatus(1); deviceMapper.updateById(device); // 5. 写入操作日志 RepairLog log new RepairLog(); log.setOrderId(order.getId()); log.setAction(CREATE); log.setRemark(用户提交报修单); repairLogMapper.insert(log); return order; }这段代码使用了Transactional事务注解保证报修单、设备状态、日志三步操作要么全部成功要么全部回滚避免出现数据不一致。4.3 报修单状态流转接口状态流转接口是系统权限控制的关键点。普通用户只能提交报修不能随意修改状态维修人员可以将待处理改成处理中或已完成管理员可以分配维修人员也可以驳回报修单。// 文件路径src/main/java/com/example/repair/service/impl/RepairOrderServiceImpl.java Override Transactional(rollbackFor Exception.class) public void updateStatus(RepairOrderStatusRequest request, Long operatorId, Integer operatorRole) { RepairOrder order repairOrderMapper.selectById(request.getOrderId()); if (order null) { throw new BusinessException(报修单不存在); } // 状态机校验只允许合法的状态流转 int currentStatus order.getStatus(); int targetStatus request.getTargetStatus(); if (!canTransit(currentStatus, targetStatus, operatorRole)) { throw new BusinessException(当前状态不允许该操作); } if (FINISH.equals(request.getAction())) { order.setStatus(2); order.setHandleRemark(request.getHandleRemark()); order.setHandleTime(new Date()); // 同步恢复设备正常状态 Device device deviceMapper.selectById(order.getDeviceId()); if (device ! null) { device.setStatus(0); deviceMapper.updateById(device); } } else if (PROCESS.equals(request.getAction())) { order.setStatus(1); order.setAssigneeId(operatorId); order.setAssigneeName(operatorName); } else if (REJECT.equals(request.getAction())) { order.setStatus(3); order.setHandleRemark(request.getHandleRemark()); } repairOrderMapper.updateById(order); // 写入日志 RepairLog log new RepairLog(); log.setOrderId(order.getId()); log.setOperatorId(operatorId); log.setAction(request.getAction()); log.setRemark(request.getHandleRemark()); repairLogMapper.insert(log); }状态机校验可以抽成一个独立方法避免散落在业务代码中。下面是一个简单的状态流转表当前状态允许操作目标状态允许角色待处理(0)PROCESS处理中(1)维修人员、管理员待处理(0)REJECT已驳回(3)管理员处理中(1)FINISH已完成(2)维修人员、管理员4.4 报修单分页列表接口列表接口需要支持按用户查询、按状态筛选和分页。MyBatis Plus 提供的分页插件非常方便。// 文件路径src/main/java/com/example/repair/controller/RepairOrderController.java GetMapping(/list) public Result list( RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) Integer status, RequestParam(required false) Long reporterId, RequestParam(required false) Long assigneeId) { PageRepairOrder pageParam new Page(page, size); LambdaQueryWrapperRepairOrder wrapper new LambdaQueryWrapper(); wrapper.eq(status ! null, RepairOrder::getStatus, status); wrapper.eq(reporterId ! null, RepairOrder::getReporterId, reporterId); wrapper.eq(assigneeId ! null, RepairOrder::getAssigneeId, assigneeId); wrapper.orderByDesc(RepairOrder::getCreateTime); PageRepairOrder result repairOrderMapper.selectPage(pageParam, wrapper); return Result.success(result); }分页接口在设计时要把查询条件作为可选参数而不是每个条件都写一个接口。这样可以减少接口数量也方便前端组合筛选。4.5 报修统计接口统计接口服务于管理员首页展示待处理数量、处理中数量、已完成数量和总报修数量帮助管理层快速掌握当前设备维修态势。GetMapping(/statistics) public Result statistics() { MapString, Object result new HashMap(); result.put(pending, repairOrderMapper.selectCount( new LambdaQueryWrapperRepairOrder().eq(RepairOrder::getStatus, 0))); result.put(processing, repairOrderMapper.selectCount( new LambdaQueryWrapperRepairOrder().eq(RepairOrder::getStatus, 1))); result.put(finished, repairOrderMapper.selectCount( new LambdaQueryWrapperRepairOrder().eq(RepairOrder::getStatus, 2))); result.put(total, repairOrderMapper.selectCount(null)); return Result.success(result); }5. 微信小程序端完整实现后端接口就绪后开始编写小程序端页面。下面会给出关键页面的核心代码完整项目可以在此基础上继续扩展。5.1 全局配置 app.json小程序的全局配置里最核心的是页面路径、窗口样式和请求域名设置。开发阶段可以在开发者工具中勾选“不校验合法域名”但真机预览和发布时必须配置合法域名。{ pages: [ pages/index/index, pages/report/report, pages/records/records, pages/detail/detail, pages/mine/mine ], window: { backgroundTextStyle: light, navigationBarBackgroundColor: #4A90D9, navigationBarTitleText: 教学设备报修, navigationBarTextStyle: white }, tabBar: { list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/records/records, text: 报修记录 }, { pagePath: pages/mine/mine, text: 我的 } ] }, style: v2, sitemapLocation: sitemap.json }5.2 封装请求工具 request.js所有请求都应该走统一的 request 封装统一处理 baseURL、token、错误提示和加载状态。// 文件路径utils/request.js const BASE_URL https://your-server-domain.com/api function request(url, method GET, data {}) { return new Promise((resolve, reject) { const token wx.getStorageSync(token) wx.request({ url: BASE_URL url, method: method, data: data, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success: (res) { if (res.statusCode 200) { resolve(res.data) } else if (res.statusCode 401) { wx.showToast({ title: 登录已过期请重新登录, icon: none }) // 跳转登录逻辑 } else { wx.showToast({ title: res.data.message || 请求失败, icon: none }) reject(res) } }, fail: (err) { wx.showToast({ title: 网络异常请稍后重试, icon: none }) reject(err) } }) }) } module.exports { request, BASE_URL }5.3 登录逻辑实现微信官方已调整了用户头像昵称的获取规则目前更推荐使用“头像昵称填写能力”由用户主动点击填写。下面代码演示了登录流程的核心逻辑。// 文件路径utils/auth.js function login() { return new Promise((resolve, reject) { wx.login({ success: async (res) { if (res.code) { try { const response await request(/wechat/login, POST, { code: res.code }) if (response.code 200) { const user response.data wx.setStorageSync(userInfo, user) resolve(user) } else { reject(new Error(登录失败)) } } catch (e) { reject(e) } } else { reject(new Error(wx.login 获取 code 失败)) } } }) }) } module.exports { login }5.4 首页与设备分类展示首页主要承担三个功能展示设备分类、展示待处理统计、提供快捷报修入口。这里以设备分类展示为例通过wx.cloud或后端接口获取分类数据后渲染到页面。!-- 文件路径pages/index/index.wxml -- view classindex-container view classstat-card view classstat-item wx:for{{statistics}} wx:keyname text classstat-num{{item.value}}/text text classstat-label{{item.name}}/text /view /view view classcategory-section view classsection-title设备分类/view view classcategory-grid view classcategory-item wx:for{{categories}} wx:keyid bindtapselectCategory >// 文件路径pages/index/index.js const { request } require(../../utils/request) const { login } require(../../utils/auth) Page({ data: { categories: [], statistics: [] }, async onShow() { const userInfo wx.getStorageSync(userInfo) if (!userInfo) { await login() } this.loadCategories() this.loadStatistics() }, async loadCategories() { const res await request(/device/categories, GET) if (res.code 200) { this.setData({ categories: res.data }) } }, async loadStatistics() { const res await request(/repair/statistics, GET) if (res.code 200) { const data res.data this.setData({ statistics: [ { name: 待处理, value: data.pending }, { name: 处理中, value: data.processing }, { name: 已完成, value: data.finished }, { name: 总数, value: data.total } ] }) } }, goReport() { wx.switchTab({ url: /pages/report/report }) } })5.5 报修表单页面报修表单是整个系统交互最复杂的页面包括设备选择、故障描述、图片上传、联系方式填写。图片上传使用wx.chooseMedia需要注意临时文件路径与正式上传路径的区别。!-- 文件路径pages/report/report.wxml -- view classform-container view classform-item text classlabel设备名称/text picker modeselector range{{deviceNames}} bindchangeonDeviceChange view classpicker-value{{selectedDeviceName || 请选择设备}}/view /picker /view view classform-item text classlabel故障描述/text textarea placeholder请详细描述设备故障现象 bindinputonDescriptionInput / /view view classform-item text classlabel故障图片/text view classupload-area view classupload-item wx:for{{images}} wx:key*this image src{{item}} modeaspectFill bindtappreviewImage>// 文件路径pages/report/report.js const { request, BASE_URL } require(../../utils/request) Page({ data: { deviceList: [], deviceNames: [], selectedDeviceId: null, selectedDeviceName: , description: , phone: , images: [] }, async onLoad() { const res await request(/device/list, GET) const deviceList res.data this.setData({ deviceList, deviceNames: deviceList.map(item item.deviceName) }) }, chooseImage() { const remain 3 - this.data.images.length wx.chooseMedia({ count: remain, mediaType: [image], sourceType: [album, camera], success: (res) { const tempFiles res.tempFiles.map(item item.tempFilePath) this.setData({ images: this.data.images.concat(tempFiles) }) } }) }, async submitOrder() { const { selectedDeviceId, description, phone, images } this.data if (!selectedDeviceId) { wx.showToast({ title: 请选择设备, icon: none }) return } if (!description.trim()) { wx.showToast({ title: 请填写故障描述, icon: none }) return } wx.showLoading({ title: 提交中... }) // 1. 先上传图片得到可访问的 URL const uploadTasks images.map((filePath) this.uploadImage(filePath)) const imageUrls await Promise.all(uploadTasks) // 2. 提交报修单 const res await request(/repair/create, POST, { deviceId: selectedDeviceId, description: description, phone: phone, images: imageUrls.join(,) }) wx.hideLoading() if (res.code 200) { wx.showToast({ title: 提交成功, icon: success }) setTimeout(() { wx.switchTab({ url: /pages/records/records }) }, 1500) } }, uploadImage(filePath) { return new Promise((resolve, reject) { wx.uploadFile({ url: BASE_URL /upload, filePath: filePath, name: file, success: (res) { const data JSON.parse(res.data) if (data.code 200) { resolve(data.data.url) } else { reject(new Error(上传失败)) } }, fail: reject }) }) } })5.6 报修记录列表与会话消息报修记录页面需要支持按状态切换查看使用wx:for渲染列表数据下拉刷新重新加载最新状态。// 文件路径pages/records/records.js const { request } require(../../utils/request) Page({ data: { activeTab: 0, tabs: [全部, 待处理, 处理中, 已完成, 已驳回], statusMap: [null, 0, 1, 2, 3], list: [], page: 1, size: 10, loading: false, hasMore: true }, onShow() { this.refreshList() }, onPullDownRefresh() { this.refreshList() }, onReachBottom() { if (this.data.hasMore) { this.loadList() } }, async refreshList() { this.setData({ page: 1, list: [], hasMore: true }) await this.loadList() wx.stopPullDownRefresh() }, async loadList() { if (this.data.loading || !this.data.hasMore) return this.setData({ loading: true }) const status this.data.statusMap[this.data.activeTab] const res await request(/repair/list, GET, { page: this.data.page, size: this.data.size, status: status }) const newList this.data.page 1 ? res.data.records : this.data.list.concat(res.data.records) this.setData({ list: newList, page: this.data.page 1, hasMore: newList.length res.data.total, loading: false }) }, switchTab(e) { const index e.currentTarget.dataset.index this.setData({ activeTab: index }) this.refreshList() } })5.7 订阅消息通知关键步骤维修进度提醒可以通过微信订阅消息实现整体流程如下在微信公众平台申请“报修进度通知”模板拿到模板 ID。小程序端在用户提交报修单成功后调用wx.requestSubscribeMessage请求用户授权订阅。后端在状态流转时调用订阅消息发送接口通知用户。小程序端订阅授权示例wx.requestSubscribeMessage({ tmplIds: [替换成你的模板ID], success(res) { // res[替换成你的模板ID] accept 表示用户同意 console.log(订阅结果, res) }, fail(err) { console.log(订阅失败, err) } })后端发送订阅消息时需要先获取 access_token再调用subscribeMessage.send接口。注意用户同意一次订阅后端只能发送一条消息无法反复通知所以订阅消息授权必须放在关键动作之后确保尽量触发一次有效发送。6. 运行验证与联调排错系统开发完成后需要在小程序开发者工具中完成全流程联调。按照下面的验证清单逐项检查可以快速定位问题。6.1 功能验证清单验证项操作步骤预期结果登录打开小程序自动触发 wx.login后端返回用户信息本地存储成功设备列表首页进入报修页能正常加载设备分类和设备列表发起报修选择设备、填写描述、上传图片、点击提交页面提示提交成功记录出现在“报修记录”列表状态流转管理员/维修人员操作报修单状态用户列表状态同步更新设备状态恢复权限控制使用普通用户账号操作驳回或分配接口提示无权限消息通知提交报修并订阅维修完成后收到服务通知6.2 常见问题表格问题现象常见原因解决思路wx.login 获取 code 失败小程序没有正确配置 AppID检查 AppID 是否填写测试号也可以使用请求接口报 401未携带 token 或 token 过期检查请求封装中的 header统一处理 401 跳转登录真机预览请求失败未配置合法域名或服务器未启用 HTTPS后端配置 HTTPS 证书小程序后台添加 request 合法域名报修单提交后设备状态未更新事务没有生效或 updateById 使用错误检查 Service 方法是否添加 Transactional检查设备 ID 是否正确上传图片失败上传接口路径错误或服务器目录无写权限检查 uploadFile 的 url确认服务器上传目录存在且有写权限订阅消息发送失败模板 ID 错误或用户未授权核对模板 ID检查 access_token 是否过期确认用户是否已取消订阅模拟器中正常但真机异常开发者工具开启了“不校验合法域名”真机没有在小程序管理后台配置合法域名官方要求必须 HTTPS6.3 后端排查建议后端联调时遇到问题先看日志再看数据库最后看接口文档。推荐在 application.yml 中开启 SQL 日志便于定位 SQL 执行问题。logging: level: com.example.repair.mapper: debug开启 SQL 日志后控制台会打印 MyBatis 执行的每条 SQL 语句能帮助快速定位查询条件写错、字段映射失败等问题。7. 工程落地中的最佳实践与扩展建议实际项目中如果只是把功能跑通离“可以上线”的标准还有距离。下面几个方向的工程化实践非常关键。7.1 状态机与业务流程固化报修单的状态变更如果不做校验很容易出现“已完成又变回待处理”之类的非法操作。建议在 Service 层建立状态流转表每次变更都校验操作角色和目标状态。可以考虑将状态机抽成独立的枚举类后续项目复用性更好。// 文件路径src/main/java/com/example/repair/common/OrderStatus.java public enum OrderStatus { PENDING(0, 待处理), PROCESSING(1, 处理中), FINISHED(2, 已完成), REJECTED(3, 已驳回); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } }状态机能够防止业务逻辑在迭代过程中被破坏越是多人协作的项目越需要把状态定义收敛到一处。7.2 接口安全与权限校验目前示例代码中只做了简单的逻辑判断。生产环境中必须有完整权限体系推荐使用 Spring Boot 拦截器或 Spring Security 做接口级权限控制。报修单操作权限用户只能操作自己的报修单。管理员权限设备管理、用户管理、状态驳回等接口只对管理员开放。登录态校验所有/api/**接口都校验 token 有效性。7.3 日志与监控报修系统虽然业务简单但涉及线下维修周期性长日志尤其重要。建议记录用户登录日志openid、登录时间、IP。操作日志谁在什么时间对哪张报修单做了什么操作。异常日志接口异常时记录完整堆栈和请求参数。7.4 数据库索引与性能优化当报修单数量增长到十万级以上时一定要关注索引设计。当前核心查询基本都围绕 reporter_id、status、assignee_id 三个字段过滤这三个字段已经在建表 SQL 中加入了索引。如果有按时间范围查询的需求建议增加(status, create_time)联合索引。7.5 后续可扩展功能消息通知优化除了订阅消息外可以增加短信或邮件通知。设备二维码为每一台设备生成独立二维码用户扫码直接进入报修页并自动携带设备 ID。备件库存管理维修人员领用备件时自动扣减库存。数据看板在管理端增加维修耗时、设备故障率、分类维修次数等图表统计。维修评价用户确认完成后可以对维修服务进行评价。7.6 上线发布注意事项小程序发布前必须配置合法域名且在“小程序后台 → 开发 → 开发设置 → 服务器域名”中添加 request 合法域名和 uploadFile 合法域名。版本发布要经过体验版测试使用不同型号手机进行真机验证重点检查图片上传和订阅消息。首次发布前可在开发者工具中开启“代码质量”扫描清理无用代码和未使用的资源文件。上线后注意观察服务器日志和数据库增长定期备份数据。教学设备报修系统的核心不只是“报修维修”两个动作而是一整套设备全生命周期管理能力。把报修单状态流转设计清楚把权限边界划分清楚把日志记录沉淀下来系统的价值会远远超过一个简单的表单提交工具。开发过程中如果遇到问题优先从前端请求参数、后端日志、数据库状态三个环节去排查大多数问题都能在这个链路里找到答案。