ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

微信小程序与Spring Boot实战:警务辅助人员管理系统开发详解

微信小程序与Spring Boot实战:警务辅助人员管理系统开发详解 简介面向计算机专业毕业设计或课程设计场景这套基于微信小程序的警务辅助人员管理系统完整覆盖微信小程序前端、Java后端与MySQL数据库的设计与实现。项目以微信为入口借助小程序轻量免安装的优势解决警务辅助人员日常管理中的信息录入、查询与维护问题同时配有系统分析、设计、数据库设计及测试等文档便于从需求到代码全面理解开发流程。资源包共1023个文件约18.18MB包含png/svg等界面素材、wxml/wxss小程序页面代码、js逻辑文件、vue后台管理页面、java服务端源码、json配置文件以及sql数据库脚本其中3个bat脚本可辅助完成安装、运行与构建降低本地配置门槛。已有153人学习资源内项目源码、数据库脚本与部署脚本齐全适合具备Java和微信小程序基础的学生作为课程设计或毕业设计参考也可用于二次开发与答辩演示。1. 警务辅助人员管理系统小程序端到底管哪几件事值班排班靠打印表贴公告栏、考勤靠手写签到本、临时任务靠微信群口头指派月底统计要人工翻三份台账——这是大多数警务辅助人员管理场景的现状。标题里的「基于微信小程序的警务辅助人员管理系统」拆开看就是一套带角色的轻量 OA管理员维护人员档案、中队长排班派活、队员打卡报备。它解决的不是什么高深算法问题而是把「谁在什么时间干什么、干完没有」这件事变成可查询的数据。适合打算做毕业设计、或者想接街道办/分局内部管理系统外包的开发者。这类系统的开发量不在页面而在登录态和权限模型——小程序端每次请求都要带身份凭证后端要分得清「普通队员只能看自己的考勤中队长能看整个中队」。2. 先想明白再写代码数据模型、角色权限与登录态设计动手写页面之前先把架构选型、表结构和会话方案定下来。这三件事没想清楚后面每写一个接口都会返工一次。2.1 为什么是微信小程序配 Spring Boot而不是别的组合常见做法是把前端放在微信小程序、后端用 Spring Boot 单体应用原因有三。第一是分发成本。警辅人员的手机未必装了企业办公 App日常打开最多的是微信。小程序扫码即用不用上应用商店、不用处理版本更新单位内部分发一个二维码就行。相比之下原生 Android App 要维护渠道H5 容易被当作普通链接随手关掉。第二是开发效率。小程序原生框架在组件、调试工具上对开发者足够友好单人两周能把核心页面跑通。后端选 Spring Boot是因为它的生态足够成熟MyBatis-Plus 省掉大量 SQL 样板代码Spring 官方文档可以当字典查毕业论文里写技术选型理由也站得住。第三是可行性边界。这类内部管理系统是给几十上百人用的工具不涉及公网高并发。别一上来就上微服务、消息队列单体应用加一个 MySQL 完全够用还能把答辩精力留给业务逻辑本身。如果你考虑以后把小程序迁到支付宝小程序或者 App可以改用 uni-app 做跨端开发但如果只是毕设周期原生小程序的事件生命周期更直观调试时少一层转换干扰。候选端分发成本设备兼容适合场景微信小程序扫码即用无需安装依赖微信基础库版本对内工具、低频但必须触达原生 Android App需上下架、版本管理碎片化严重机型适配成本高有大量离线功能、硬件调用H5链接直达依赖浏览器体验一般临时页面、对外展示2.2 六张核心表把警辅业务拆成可落地的 ER 结构整个系统的业务可以收敛成六张表警辅人员表、部门表、排班表、考勤表、任务工单表、培训记录表。再多就是附件表或操作日志没想清楚前不要急着加字段。设计原则有一条要记住微信账号信息不要散落在业务表里。用户表只存业务字段openid 单独放一张微信绑定表以后换设备或换绑账号时不用动业务数据。-- 警辅人员表 CREATE TABLE t_aux_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_no VARCHAR(32) NOT NULL COMMENT 警号/工号, real_name VARCHAR(64) NOT NULL COMMENT 姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, dept_id BIGINT NOT NULL COMMENT 所属部门/中队, role VARCHAR(16) NOT NULL DEFAULT OFFICER COMMENT 角色: ADMIN/CAPTAIN/OFFICER, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在职 0离职, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_no (user_no) ) COMMENT 警辅人员表; -- 排班表 CREATE TABLE t_schedule ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, work_date DATE NOT NULL COMMENT 班次日期, shift_type TINYINT NOT NULL COMMENT 1早班 2中班 3晚班 4备勤, create_by BIGINT NOT NULL COMMENT 排班人, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_date (user_id, work_date) ) COMMENT 排班表; -- 考勤表 CREATE TABLE t_attendance ( id BIGINT AUTO_INCREMENT PRIMARY KEY, schedule_id BIGINT NOT NULL COMMENT 关联排班, user_id BIGINT NOT NULL, check_in DATETIME DEFAULT NULL COMMENT 签到时间, check_out DATETIME DEFAULT NULL COMMENT 签退时间, location VARCHAR(255) DEFAULT NULL COMMENT 打卡地点描述, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_schedule_user (schedule_id, user_id) ) COMMENT 考勤表;排班表里用work_date shift_type而不是直接存一个开始时间的 datetime目的是方便按周检索查询某一天谁在什么班直接WHERE work_date ?走索引统计一个月各班组工时也只要一条带 GROUP BY 的语句。UNIQUE KEY uk_user_date保证一个人一天只能有一个班次这个约束必须在数据库层做不能只靠后端代码判断。任务工单表记录临时任务和处置反馈核心字段是task_type、content、assignee_id、status状态从 PENDING 流转到 ACCEPTED 再到 FINISHED。培训记录表和请假申请表属于低频业务字段简单不在一开始占太多设计时间。2.3 登录态与权限模型openid 不落库token 管会话小程序登录的标准流程是前端wx.login()拿到一次性 code传给后端后端拿着 code 调微信的code2session接口换回 openid 和 session_key再通过 openid 去微信绑定表反查业务用户查到就签发自己的 token。不要把 openid 当业务主键。openid 是针对「当前这个小程序」的 ID同一个微信用户在不同小程序里的 openid 不一样而且它是一串 28 位字符串做外键又长又可读性差。正确做法是业务表始终用自增user_idopenid 只放在t_wechat_bind表里做映射。会话凭证用 JWTpayload 里放uid和role过期时间设 2 小时。后端统一拦截器解析 token再把角色信息塞进请求上下文。权限模型按三种角色划分角色能看什么能做什么ADMIN全部数据、全部分析页维护人员档案、创建部门、管理角色CAPTAIN本中队人员、本中队排班考勤排班、审批请假、分配任务OFFICER本人排班、本人考勤、本人任务打卡、请假申请、任务反馈3. 用 Spring Boot 把接口落成可调用代码从配置到业务后端工程建议按 controller / service / mapper / config / interceptor / annotation / entity 分包。包结构清楚答辩画架构图的时候也省事。下面按「先定接口长相再写业务最后调参数」的顺序来。3.1 项目骨架与统一返回体先定好接口的「长相」前后端联调最容易吵架的就是返回格式不统一。先定一个 Result 类所有接口返回这个结构前端拦截器才好统一处理。Data public class ResultT { private Integer code; // 业务码2000 成功4001 参数错误4002 未登录4003 无权限 private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 2000; r.message success; r.data data; return r; } public static T ResultT fail(Integer code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }这段代码的关键是把「业务状态」和「HTTP 状态」分开。HTTP 状态始终返回 200前端只看 body 里的 code 判断成功还是失败。好处是网络层错误和业务层错误不会混在一起——HTTP 500 表示服务端炸了HTTP 200 code 4002 表示会话过期前端拦截器可以根据 code 精确处理。配合RestControllerAdvice做全局异常捕获业务层直接抛BizException(4003, 无权访问)框架统一转成 Result 返回不用每个 controller 写 try-catch。3.2 排班列表与打卡上报两个带权限的核心接口先看打卡接口。普通队员打开小程序点签到后端要校验三件事这个人有没有排班、今天是不是已经打过卡、角色是否允许执行打卡。RestController RequestMapping(/api/attendance) public class AttendanceController { Autowired private AttendanceService attendanceService; PostMapping(/check-in) RequireRole({OFFICER, CAPTAIN, ADMIN}) public ResultAttendanceVO checkIn(RequestBody CheckInDTO dto, RequestAttribute(uid) Long uid) { AttendanceVO vo attendanceService.checkIn(uid, dto); return Result.ok(vo); } } Service public class AttendanceServiceImpl implements AttendanceService { Override public AttendanceVO checkIn(Long uid, CheckInDTO dto) { LocalDate today LocalDate.now(); Schedule schedule scheduleMapper.selectOne(new LambdaQueryWrapperSchedule() .eq(Schedule::getUserId, uid) .eq(Schedule::getWorkDate, today)); if (schedule null) { throw new BizException(4004, 今日无排班无需打卡); } Long count attendanceMapper.selectCount(new LambdaQueryWrapperAttendance() .eq(Attendance::getScheduleId, schedule.getId())); if (count 0) { throw new BizException(4005, 今日已打卡请勿重复提交); } Attendance att new Attendance(); att.setScheduleId(schedule.getId()); att.setUserId(uid); att.setCheckIn(LocalDateTime.now()); att.setLocation(dto.getLocation()); attendanceMapper.insert(att); return new AttendanceVO(att.getId(), att.getCheckIn()); } }RequireRole是自定义注解由拦截器解析 token 里的角色后做比对比对不通过直接抛 4003。RequestAttribute(uid)里的 uid 是拦截器从 token 解析出来塞回请求上下文的值业务方法不用再重复解析 token。排班列表接口按周返回前端传start参数给出本周起始日期后端算出七天数据GetMapping(/week) RequireRole({OFFICER, CAPTAIN, ADMIN}) public ResultListScheduleVO weekSchedules(RequestParam String start, RequestAttribute(uid) Long uid, RequestAttribute(role) String role) { return Result.ok(scheduleService.listWeek(uid, role, LocalDate.parse(start))); }service 里需要区分角色OFFICER 只能查自己的排班CAPTAIN 可以查本中队所有人的排班。这个判断放 service 层不要放 controller避免 controller 里堆业务判断显得臃肿。3.3 参数调优Token 有效期、Redis 缓存与分页默认值后端有些参数是在配置文件里定义的改一次所有接口生效。我给这类项目常用的配置是这样server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/aux_police?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root redis: host: localhost port: 6379 database: 0 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 jwt: secret: your-256-bit-secret-here-change-in-prod expire-seconds: 7200 mybatis-plus: configuration: map-underscore-to-camel-case: trueexpire-seconds设为 7200 是综合考量的结果。内部管理系统会话设两小时足够队员白天连续使用中间会自动续签但就算 token 被截获攻击者能利用的时间窗口也短。登录过期后前端调/api/auth/refresh换新 token不需要用户重新输用户名密码。JWT 本身无法主动失效——签发出去之后在过期前只要 secret 不泄露就始终有效。所以登出功能不能只靠前端删 token后端需要用 Redis 维护一个黑名单把登出的 token 加进去拦截器解析 token 后先查黑名单。这个是答辩时的加分点很多项目只做了前端清 token后端没有真正让会话失效。Redis 另一个用途是缓存部门和班次字典这类几乎不变的数据key 设dict:dept过期设 30 分钟即可不用追求实时性。分页参数同理。所有列表接口不传 page 和 size 时默认page1, size10防止有人不传参把全表拉走。MyBatis-Plus 的 PaginationInnerInterceptor 配置好插件后Page对象直接作为 Mapper 方法参数即可。4. 微信小程序端落地登录、请求封装与业务页面后端接口就绪后小程序端的核心工作是三件事全局配置、请求封装、业务页面。配置决定小程序在哪些机型上不会翻车封装决定代码里不会有几十处重复的 token 判断。4.1 app.json 全局配置与顶部导航栏高度适配小程序原生的默认导航栏在每个机型上高度不一致iPhone 的刘海屏状态栏在 44 到 59 像素之间浮动Android 各厂商也不统一。如果用了navigationStyle: custom自定义导航栏写死 64px 会在部分机型上顶到刘海或溢出去这个坑俗称「导航栏高度玄学」。适配方案是按胶囊按钮的位置反推// utils/navbar.js function getNavBarInfo() { const windowInfo wx.getWindowInfo(); const menuRect wx.getMenuButtonBoundingClientRect(); const navBarHeight (menuRect.top - windowInfo.statusBarHeight) * 2 menuRect.height; return { statusBarHeight: windowInfo.statusBarHeight, navBarHeight: navBarHeight, navBarTop: menuRect.top, }; } module.exports { getNavBarInfo };wx.getWindowInfo()在基础库 2.20.1 以后替代了废弃的wx.getSystemInfoSync()。计算逻辑是胶囊按钮上沿到状态栏底部的距离乘以 2 再加胶囊自身高度得到的就是自定义导航栏的总高度。拿到后在onLoad里 setData 到页面的导航栏容器 style 上即可。4.2 request.js 请求封装把 401 处理收敛到一处毕设项目最常见的坏味道是每个页面里wx.request写一遍token 过期了每个页面各弹各的提示。正确的做法是把请求封装成带 Promise 的一个函数所有业务页面只调这个方法。// utils/request.js const BASE_URL https://api.example.com; // 生产环境域名本地调试可改为局域网 IP function request(path, method, data) { return new Promise((resolve, reject) { const token wx.getStorageSync(token); wx.request({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json, Authorization: token ? Bearer token : , }, success(res) { const body res.data; if (body.code 2000) { resolve(body.data); } else if (body.code 4002) { wx.removeStorageSync(token); wx.reLaunch({ url: /pages/login/index }); reject(new Error(登录已过期)); } else { wx.showToast({ title: body.message, icon: none }); reject(new Error(body.message)); } }, fail(err) { wx.showToast({ title: 网络异常请稍后重试, icon: none }); reject(err); }, }); }); } module.exports { get: (path, data) request(path, GET, data), post: (path, data) request(path, POST, data), };这段封装把「token 注入 header」和「401 统一跳登录页」两件事收敛到一处。以后新增页面不用关心 token 怎么带、过期了去哪只管调用request.post(/api/attendance/check-in, {...})拿结果。401 处理在并发请求时很容易翻车三个接口同时返回 4002就会触发三次reLaunch。更稳妥的做法是加一个标志位检测到已在跳转中就直接忽略后续的 4002避免页面跳转抖动。4.3 从 wx.login 到业务页面首页看板与请假表单登录页的流程是wx.login()取 code传给后端换 token成功后把 token 和过期时间一起写入 storage。过期时间建议设置与后端 jwt 过期一致也就是 2 小时并且每次请求成功后都用当前时间刷新一次 expiry——这就是「小程序设置缓存时间」的落地方式避免用户在临界点反复被踢下线。// pages/login/index.js wx.login({ success(res) { wx.request({ url: BASE_URL /api/auth/login, method: POST, data: { code: res.code }, success(resp) { const body resp.data; if (body.code 2000) { wx.setStorageSync(token, body.data.token); wx.setStorageSync(expireAt, Date.now() 7200 * 1000); wx.reLaunch({ url: /pages/dashboard/index }); } else { wx.showToast({ title: 登录失败, icon: none }); } }, }); }, });首页看板要展示「今日班次、本周出勤次数、待办任务」三块数据配合onPullDownRefresh做下拉刷新。这些数据后端用一个聚合接口/api/dashboard/officer一次返回前端不用串行调三个接口省一次网络往返。下拉刷新事件里要调用wx.stopPullDownRefresh()否则 loading 动画不会消失。请假表单页用picker选日期、textarea填理由提交时把日期字符串和理由一起 POST 到/api/leave/apply。这里有一个容易忽略的点picker 返回的日期格式是YYYY-MM-DD后端LocalDate直接解析没问题但如果用户手填日期一定要注意前端做一次格式校验否则后端报 400 错误用户看到的是「网络异常」而不是「日期格式错误」。5. 避坑真机请求失败、code 复用与时间格式这几个坑我踩了不止一次以下五条都是实际项目里反复出现的坑每一条都按「现象 → 原因 → 解决」的排查思路写。答辩前用这个清单自查一遍能挡住大部分低级问题。5.1 真机上请求全部失败开发者工具里却一切正常现象在微信开发者工具里所有接口正常点「预览」用真机扫码后所有请求全部报request:fail。原因开发者工具默认勾选了「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」工具里请求随便写都能通。真机上这个开关不生效小程序要求所有请求域名必须是 HTTPS 且在后台配置过 request 合法域名。解决开发阶段用真机调试时在微信公众平台「开发设置」里把开发环境的域名加进 request 合法域名列表或者干脆在真机上打开调试模式右上角菜单 → 开发调试临时绕过域名校验。正式上线前一定把域名配全只开调试模式会被微信审核打回。另外注意本地调试别写http://localhost手机访问的那个 localhost 是手机自己要写成后端电脑的局域网 IP。5.2 同一个 code 二次使用登录接口报 session 失效现象登录后快速退出再进有时会一直登录失败后端日志显示errcode 40029 code invalid。原因wx.login()返回的 code 是五分钟内有效的一次性凭证同一个 code 只能换一次 openid。如果你在页面onShow里每次都调wx.login()上一次请求还没回来就触发了第二次两次请求用的是同一个 code后一次必然失败。解决给登录逻辑包一层「进行中去重」。用一个全局变量记录当前是否正在登录已经在登录直接复用同一个 Promise如果间隙很短甚至可以在一定时间内不去调wx.login直接用本地缓存的 token 判断是否还有效。5.3 后端返回的LocalDateTime前端new Date()解析出 NaN现象考勤列表页的时间显示NaN-NaN-NaN或者出现2025-03-01T10:30:00这种带 T 的奇怪格式。原因Spring Boot 默认用 Jackson 序列化LocalDateTime默认输出 ISO 格式2025-03-01T10:30:00这个字符串直接丢给前端new Date()部分 iOS 机型解析不出正确时间。解决在application.yml里配spring.jackson.date-format: yyyy-MM-dd HH:mm:ss和spring.jackson.time-zone: GMT8让后端统一输出格式化好的人类可读时间。更省心的方案是返回Long时间戳前端new Date(timestamp)完全不受时区和格式影响只是排错时看到一长串数字不直观。我一般选格式化字符串方案日志和接口调试看着舒服。5.4 上传图片在低版本基础库上返回结构不一致现象用wx.chooseMedia在部分老手机上拿不到返回的文件路径报tempFiles为 undefined。原因wx.chooseMedia是基础库 2.21.0 之后才支持的接口低版本微信没有这个 API或者走了退化逻辑返回tempFilePaths。而wx.chooseImage返回的是tempFilePaths数组两者字段结构不一样。解决判断基础库版本后分别调用兼容层做成公共函数function chooseImage() { const version wx.getSystemInfoSync().SDKVersion; if (compareVersion(version, 2.21.0) 0) { return wx.chooseMedia({ count: 1, mediaType: [image] }); } return wx.chooseImage({ count: 1 }); }compareVersion自己写一个字符串比较函数按数字逐段比较。这类兼容代码不用多集中在 utils 里遇到类似的 API 差异加一个分支即可。5.5 前端隐藏按钮不等于后端限制现象小程序里普通队员看不到「排班管理」入口但用开发者工具或抓包工具直接调用排班接口依然能拿到数据。原因前端只是把按钮藏了「设计模式 java 实现」里常讲的原则——安全要靠服务端不能信任客户端。小程序代码在用户手机上请求是可以被完整复现的前端的任何隐藏都只是交互层面的处理。解决后端所有接口都要过权限校验。RequireRole注解只声明了对角色要求还不够拦截器必须校验角色的同时校验数据归属OFFICER 查排班时SQL 强制加user_id 当前登录uidCAPTAIN 查排班时先查这个中队长管理的中队编号再按中队过滤。这样即使有人绕过前端直接调接口也拿不到别人数据。6. 再往前一步给警辅系统加上数据脱敏与操作留痕核心功能跑通之后真正拉开差距的是两个不起眼的细节列表接口的敏感字段脱敏以及关键操作的操作日志。6.1 敏感字段脱敏警辅人员的手机号、身份证号在列表页不应该明文展示。常见做法是写一个脱敏工具类身份证和手机号分别按规则打码Component public class MaskUtil { public static String maskPhone(String phone) { if (phone null || phone.length() ! 11) return phone; return phone.substring(0, 3) **** phone.substring(7); } public static String maskIdCard(String id) { if (id null || id.length() 8) return id; return id.substring(0, 4) ********** id.substring(14); } }更优雅的做法是用 Jackson 自定义序列化器给 VO 字段加注解一处定义到处生效。但这个对毕设来说不是必须工具类在 service 层转换 VO 时调用即可。重点是让评审老师看到你知道「敏感数据不能明文露出」这个安全意识。6.2 用登录日志验证整条链路每次上线前我会在本地跑一遍最核心的链路而不是只点一遍界面拿一个 OFFICER 账号登录查看接口日志里是否有完整的「登录 → 打卡 → 查看排班」三条记录确认 token 每次都带上了、返回的时间格式没有带 T、数据库里确实插入了记录。操作日志用 AOP 统一记录OpLog(打卡)注解加在 Controller 方法上切面里解析当前用户、操作内容、耗时写入t_operation_log表。这个表能看出「谁在几点做了什么」又能在答辩时演示审计能力。我自己的习惯是每天收尾前把后端日志拉出来看一眼重点排查报错堆栈里是不是有 SQL 异常和空指针再用开发者工具把关键流程走一遍切两个不同角色的账号各测一次别只用一个管理员账号测到底。这类系统用户不多但责任边界清晰是硬要求权限侧宁严勿松。希望帮到你。本文还有配套的精品资源点击获取
返回列表