微信小程序访客预约审批系统:状态机与门岗凭证实战
2026/9/17 1:31:35 网站建设 项目流程

简介:基于微信小程序的访客预约审批管理系统,是一份面向高校计算机、通信、人工智能、自动化等专业学生及初学者的毕业设计级源码包。系统完整覆盖访客预约、审批管理、状态查询等核心流程,代码经过调试测试,答辩评审达98分,适合用于课程设计、大作业或毕设参考。压缩包共480个文件,以js、wxml、wxss、json等小程序前后端文件为主,辅以png/jpg图片资源和docx安装使用手册、md说明文档,整体约2.19MB,目录结构清晰,便于按模块快速定位。目前已有163人学习下载。借助这份资源,读者可学习微信小程序项目从页面搭建、交互逻辑到数据存储的完整实现思路,还可参考文档快速部署运行,并在此基础上进行功能扩展与界面美化,对提升小程序开发与项目整合能力具有较高的借鉴价值。

1. 访客预约审批管理系统从微信小程序端切入

访客预约审批这个场景,大多数团队第一反应是先做管理后台,再做访客端小程序。实际落地过程中会发现,审批流才是整个系统的真正骨架,小程序端反而相对简单。访客端要处理的核心问题集中在三块:预约单状态流转、身份凭证校验、审批结果实时通知。而审批端要面对的则是多级审批、加签转签、黑名单拦截、时段容量控制这些数据库设计和状态机层面的问题。标题里这包源码文档如果只当成小程序 Demo 看,容易把重心放错位置。

这篇文章按从业者视角把整个系统拆成可落地的方案来讲,从预约单的状态机建模开始,到小程序端如何校验登录态与预约凭证,再到后端准时放行与数据统计,最后落在调试线上预约凭证、处理微信基础库兼容这类高频坑上。适合正在做毕业设计、接外包、或者公司内部要做访客系统的工程师参考,源码包里的东西可以当作起点,但权限边界和数据校验必须按自己的业务重写。

2. 预约单状态机:审批系统最核心的数据建模

2.1 状态字段为什么不能只用一个 status

很多初版设计里,预约单就一个 status 字段,0 待审批、1 通过、2 拒绝。实际管理场景里,访客提交预约后可能要经过部门负责人、行政复核、安保终审三层审批,每一层都可能驳回,驳回后访客修改了来访时间又要重新提交。如果只用一个字段,审批历史没法追溯,访客改期后原审批结果也会被覆盖。

常见做法是拆成两张表再加一张流转记录表。

CREATE TABLE visitor_appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, visitor_name VARCHAR(64) NOT NULL, visitor_phone VARCHAR(20) NOT NULL, id_card_no VARCHAR(18) NOT NULL, visit_date DATE NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, host_name VARCHAR(64) NOT NULL, host_dept VARCHAR(128) NOT NULL, visit_reason VARCHAR(512) NOT NULL, car_plate VARCHAR(16) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待提交 1初审中 2终审中 3已通过 4已拒绝 5已取消 6已过期 7已签到', current_approver VARCHAR(64) DEFAULT NULL, version INT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_status_date (status, visit_date), KEY idx_visitor_phone (visitor_phone) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

状态字段拆成细粒度之后,每个状态变更都必须落一条审批记录。

CREATE TABLE approval_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, appointment_id BIGINT NOT NULL, approver_id VARCHAR(64) NOT NULL, action TINYINT NOT NULL COMMENT '1通过 2驳回 3转交 4加签', comment VARCHAR(512) DEFAULT NULL, from_status TINYINT NOT NULL, to_status TINYINT NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_appointment_id (appointment_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这套拆法解决两个问题:version 字段做乐观锁防止两个审批人同时操作同一条预约单导致状态覆盖;approval_record 保证每个节点操作可追溯,访客端显示「审批进度」时直接按 appointment_id 查这张表渲染时间线。

2.2 后端状态流转的校验逻辑不能省

状态机的校验要点在接口层做,不在数据库层做。后端每个更新状态的接口都必须校验「当前状态是否允许跳转到目标状态」。

private static final Map<Integer, Set<Integer>> STATUS_TRANSITION = new HashMap<>(); static { STATUS_TRANSITION.put(0, Set.of(1, 5)); STATUS_TRANSITION.put(1, Set.of(2, 4, 5)); STATUS_TRANSITION.put(2, Set.of(3, 4, 5)); STATUS_TRANSITION.put(3, Set.of(7)); STATUS_TRANSITION.put(4, Set.of()); STATUS_TRANSITION.put(5, Set.of()); STATUS_TRANSITION.put(6, Set.of()); }

这段代码定义的是「状态可达矩阵」。比如 status=3(已通过)不允许直接改成 1(初审中),要改必须走「取消——重新提交」流程。注意 key 为 4、5、6 时对应的 Set 为空,这意味着拒绝、取消、过期都是终态,没有特殊管理权限不能回退。

提示:业务上确有「审批通过后访客改期」的需求。不要开放状态回退,应引导用户取消原预约单、生成新单。这样审批留痕最干净,也便于后期统计履约率。

2.3 过期状态用定时任务补扫,不依赖小程序端上报

小程序端用户退出后,onHide 和 onUnload 都不一定可靠,所以预约单过期不能等前端上报。后端定时任务每小时扫一次。

@Scheduled(cron = "0 0 * * * ?") public void expireAppointments() { LocalDateTime now = LocalDateTime.now(); appointmentMapper.expirePastAppointments(now); }

对应 SQL:

UPDATE visitor_appointment SET status = 6, update_time = NOW() WHERE status = 3 AND visit_date < CURDATE();

注意这里只把「已通过」和「已过期」的过滤条件限制为 visit_date 小于今天,正确。但如果有当天已经过了 end_time 的预约,也应该标记为过期,不能只按日期判断。更准确的写法:

WHERE status = 3 AND CONCAT(visit_date, ' ', end_time) < NOW();

MySQL 对 CONCAT 出来的时间字符串做隐式转换会有索引失效风险,量小无所谓,量大了建议拆一个 visit_end_time DATETIME 冗余字段。

3. 微信小程序端:登录态、预约表单与凭证生成

3.1 wx.login 拿 code 换 openid 的正确姿势

微信小程序访客端最基础的一步是登录。wx.login 拿到的是临时 code,五分钟后过期,且只能用一次。拿到 code 后必须交给后端,由后端调用 code2Session 接口换取 openid 和 session_key。

wx.login({ success: async (res) => { if (res.code) { const loginRes = await request({ url: '/api/auth/login', method: 'POST', data: { code: res.code } }); if (loginRes.data.token) { wx.setStorageSync('token', loginRes.data.token); } } } });

核心逻辑在后端:

String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + appid + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code";

换回 openid 后,业务系统里的用户表要以 openid 为唯一标识,不要用前端传来的昵称头像做标识。2021 年后微信调整了头像昵称填写能力,wx.getUserProfile 返回的头像昵称不再作为登录依据,只当展示信息。做访客系统尤其要注意这一点,访客身份最终要靠手机号或者身份证号核验,不能用微信昵称当通行凭证。

3.2 预约表单需要处理的字段与校验

小程序端预约表单建议拆成两个步骤:第一步填访客信息,第二步选时间和事由。一次展示十个字段,移动端转化率很差。

第一步字段:

  • 访客姓名(必填,去首尾空格)
  • 手机号(必填,正则校验 1[3-9]\d{9})
  • 身份证号(选填,部分园区进楼需要)
  • 车牌号(选填,有车必填)

第二步字段:

  • 被访人姓名/部门(必填,支持搜索选择)
  • 来访日期(必填,日期选择器限制只能选未来 7 天)
  • 开始时间/结束时间(必填,结束需晚于开始)
  • 来访事由(必填,textarea 最长 200 字)
  • 随行人数(选填,默认 0)

前端校验通过后提交:

const formData = { visitorName: this.data.visitorName.trim(), visitorPhone: this.data.visitorPhone.trim(), idCardNo: this.data.idCardNo.trim(), visitDate: this.data.visitDate, startTime: this.data.startTime, endTime: this.data.endTime, hostName: this.data.hostName, hostDept: this.data.hostDept, visitReason: this.data.visitReason.trim() }; const valid = validateForm(formData); if (!valid) return; const res = await request({ url: '/api/appointment/create', method: 'POST', data: formData });

后端 create 接口的校验不能只相信前端,必须再做一次字段级校验:时间范围、手机号格式、被访人是否存在、该时段预约人数是否已达上限。所有校验不过的情况统一返回 code + message,前端按 code 提示对应文案。

3.3 审批结果通知:模板消息与订阅消息的差异

微信小程序早期用模板消息,现在全部切换到订阅消息。一次订阅只能推送一次,需要用户主动点击「同意订阅」按钮。访客提交预约后,弹窗请求订阅审批结果通知:

wx.requestSubscribeMessage({ tmplIds: ['审批通过通知模板ID', '审批拒绝通知模板ID'], success: (res) => { if (res['审批通过通知模板ID'] === 'accept') { app.globalData.subscribed = true; } } });

需要说明的是,订阅消息要求用户在点击按钮时被动触发 wx.requestSubscribeMessage,不能放在 onLoad 里自动弹。一个预约单从提交到审批完成,实际推送只有一次,所以前端要在提交成功后再请求订阅。如果用户拒绝了订阅,小程序端只能通过「我的预约」列表轮询状态变化来提醒,这是订阅消息的限制,没有绕过方案。

4. 审批管理端:多级审批流与 H5 页面兼容

4.1 审批列表的分类与筛选

审批端如果做成独立后台,开发成本高;常见做法是嵌在微信小程序里做成 H5 页面,或者在管理后台 Web 端完成。无论哪种形式,审批列表的关键是三种视图:

  • 待我审批:current_approver = 当前用户
  • 我发起的:visitor_phone 关联当前用户
  • 已办结:approval_record 里 approver_id 包含当前用户

后端列表接口要带上分页和筛选:

@GetMapping("/api/approval/list") public Result<PageResult<ApprovalVO>> list(ApprovalQuery query) { Page<ApprovalDO> page = new Page<>(query.getPage(), query.getSize()); LambdaQueryWrapper<ApprovalDO> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ApprovalDO::getCurrentApprover, query.getApproverId()); wrapper.orderByDesc(ApprovalDO::getUpdateTime); return Result.ok(approvalMapper.selectPage(page, wrapper)); }

4.2 审批动作接口与多级审批的实现

一次标准的审批动作包含「通过 / 驳回 / 转交 / 加签」四个操作。

@Transactional public void approve(Long appointmentId, String approverId, Integer action, String comment, String nextApproverId) { AppointmentDO appointment = appointmentMapper.selectById(appointmentId); if (appointment == null) throw new BizException("预约单不存在"); Set<Integer> allowed = STATUS_TRANSITION.get(appointment.getStatus()); if (allowed == null || !allowed.contains(actionToStatus(action))) { throw new BizException("当前状态不允许执行该操作"); } if (StringUtils.isNotBlank(comment) && comment.length() > 200) { throw new BizException("批注长度不能超过200字符"); } }

多级审批通常用「审批层级配置表」实现,而不是硬编码三层。配置表记录每个部门或来访事由对应的审批节点顺序:

CREATE TABLE approval_flow_config ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_id VARCHAR(64) NOT NULL, node_order INT NOT NULL, approver_role VARCHAR(64) NOT NULL, node_name VARCHAR(64) NOT NULL );

后端在预约单创建时根据 dept_id 查出该部门审批链,写入 appointment_approval_chain 表,当前节点状态指向第一个节点。

4.3 H5 审批页兼容微信小程序内嵌浏览器

审批人如果使用微信消息卡片进入 H5 审批页,需要考虑微信内置浏览器的兼容问题。常见坑有三个:

第一个是 Flex 布局偏移。老版本 Android 微信内核支持不完整,用 flex- gap 会出现间距失效。替代方案是给子元素加 margin。

.approval-card__item { margin-right: 12rpx; }

第二个是 input 失焦后页面跳动。H5 审批页里的驳回意见输入框建议用 textarea 替代 input,且不要用 position: fixed 定位,避免唤起键盘后页面整体上移。

第三个是微信 H5 里 wx.config 需要通过后端获取签名,否则无法调用 wx.scanQRCode 扫访客凭证。签名接口如下:

async function getJsSdkSignature(url) { const res = await request({ url: '/api/wechat/js-sdk-signature', method: 'POST', data: { url: window.location.href.split('#')[0] } }); return res.data; }

提示:签名 URL 必须是当前页面完整地址且不带 hash,否则 wx.config 会报 invalid signature。

5. 访客凭证与门岗验证:二维码生成的细节

5.1 凭证数据结构设计与参数取舍

审批通过后,系统给访客生成一个二维码凭证。这个二维码不建议直接携带 openid 或 appointment_id 明文,原因是二维码可被拍照转发,一旦泄露等于给了陌生人进门凭证。常见做法是生成一个短期有效的 token,后端门岗小程序扫描后换取预约详情。

const crypto = require('crypto'); function generateCredential(appointmentId, secret) { const payload = `${appointmentId}.${Date.now()}`; const signature = crypto.createHmac('sha256', secret) .update(payload) .digest('hex') .slice(0, 16); return Buffer.from(`${appointmentId}.${signature}`).toString('base64url'); }

这个 token 设计上要包含有效期判断,门岗端扫码后校验三个点:token 签名是否正确、预约单状态是否为已通过、当前时间是否落在预约时间内。

5.2 门岗核销接口与防重复扫描

核销接口必须做幂等:

@PostMapping("/api/checkin/verify") public Result<CheckinVO> verify(@RequestBody VerifyRequest req) { String credential = req.getCredential(); CheckinVO vo = checkinService.verify(credential); return Result.ok(vo); }

verify 方法内部逻辑:

  1. 解析 credential,校验签名
  2. 查 appointment 表,校验 status = 3
  3. 锁行 UPDATE appointment SET status = 7 WHERE id = ? AND status = 3
  4. 如果影响行数为 0,说明已被核销或状态异常
int rows = appointmentMapper.checkin(appointmentId); if (rows == 0) { throw new BizException("二维码已被使用或状态异常"); }

这一步是防重复签到的关键。如果不带 status = 3 条件直接 UPDATE,并发请求下可能多次通过校验。

5.3 二维码的过期刷新机制

二维码不能长时间不变。访客进入门岗前 10 分钟刷新一次,小程序端可以通过定时器在前台 keep alive。

setInterval(() => { const newCredential = generateCredential( this.data.appointmentId, this.data.secret ); this.setData({ credential: newCredential }); }, 5 * 60 * 1000);

注意 setInterval 在前台运行,切后台后微信会挂起定时器,所以生成二维码的接口要在 onShow 里重新拉取一次凭证。

6. 线上 Debug:抓包排查真机预览的 3 个高频问题

6.1 真机预览里 wx.request 请求失败的排查路径

小程序开发者工具里接口正常,真机上却请求失败,最常见原因是「开发环境不校验合法域名」这个开关。工具里勾选了「不校验合法域名」,真机预览时该选项不生效,必须在小程序后台配置 request 合法域名,且域名必须备案、必须 HTTPS。

排查顺序:

  1. 后台「开发管理 - 开发设置 - 服务器域名」检查 request 合法域名
  2. 手机端打开调试模式(右上角胶囊 - 开发调试),看 console 的具体报错
  3. 用 Charles 或 Burp Suite 抓 HTTPS 包确认请求是否到达服务器

如果抓包看不到微信小程序的 HTTPS 请求,需要在电脑上安装并信任 Charles 的 CA 证书,手机代理指向电脑 IP,端口默认 8888。

6.2 体验版二维码打开空白页或加载失败

如果小程序项目里使用了未发布的功能插件,或者基础库版本过低,体验版会白屏。小程序后台「设置 - 基础库最低版本」可以调整。建议设为 2.30.0 以上,但要注意部分老旧 Android 机型最高只能跑到 2.20 左右,所以代码里不要使用太新的 API。

检查方式:

const version = wx.getSystemInfoSync().SDKVersion; console.log('SDKVersion: ', version);

在 app.js onLaunch 里输出基础库版本,可以快速判断是不是版本不兼容导致的空白。如果线上发现白屏,另一个排查点是 app.json 里 pages 数组第一个页面路径是否正确。

6.3 审批消息推送收不到时,先看订阅关系是否有效

用户订阅过一次、推送过一次之后,订阅关系就消耗掉了。访客提交第二单时如果没有重新订阅,推送必然失败。常见做法是在「我的预约」列表页加一个「接收审批通知」按钮,引导用户主动点击,调用 wx.requestSubscribeMessage 重新订阅。这个按钮的触发必须是用户点击行为,不能写在 onLoad 里。

验证推送链路是否断掉:

wx.requestSubscribeMessage({ tmplIds: ['模板ID'], success: (res) => { console.log('订阅结果: ', res['模板ID']); }, fail: (err) => { console.error('订阅失败: ', err); } });

订阅返回 reject 通常是用户拒绝了授权,需要引导用户到设置页打开「订阅消息」权限。小程序设置页可以通过 wx.openSetting 打开,注意 wx.openSetting 必须在用户点击事件里调用,不能自动触发。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询