ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

微信小程序访客预约系统:从表单到后端审核的完整实现

2026/9/11 18:39:35 拓冰建站 浏览量
微信小程序访客预约系统:从表单到后端审核的完整实现 简介面向需要构建访客预约系统的企业、开发团队及学习者这款基于微信小程序与后端管理的完整设计源码将小程序端预约登记、信息查询与后台管理端的审核、统计功能打通可直接用于企事业单位的访客管理场景或教学实战。压缩包共770个文件、11.54MB其中Java文件289个负责后端业务逻辑与接口152个HTML与104个JavaScript文件支撑管理端页面与交互40个CSS文件用于界面样式33个XML文件以及wxml、wxss等承担配置和小程序页面结构另有SQL、YML等覆盖数据库与部署环境整体模块化程度较高便于按需查阅。目前已有631人学习下载项目中附带后端管理系统既能作为一套可运行的完整方案也能通过源码理解小程序与后端协作、权限控制及数据流转的实际写法对前后端分离项目的工程组织也有参考价值。1. 访客预约系统在微信小程序端的形态与取舍访客预约系统的存在是把访客登记从纸质表单移到微信小程序里让后端管理从“事后补录”变成“事前审核”。拿到这套基于微信小程序与后端管理的访客预约系统源码时我第一件事不是急着跑起来而是先看文件构成768个文件里Java文件占289个HTML文件152个JavaScript文件104个CSS和XML分别40和33个。这个比例说明后端逻辑密度远高于前端预约审核、状态流转和消息通知都压在服务端。它解决的是“访客在小程序端预约、后台人员在管理端审核、门岗按结果放行”的完整闭环适合有固定安保流程的园区、写字楼和企事业单位。微信小程序端的价值在于访客不用装App点开微信就能填后端管理的价值则在于预约记录可追溯、数据可统计。下文按请求链路拆系统从预约表单、接口封装到Java后端审核逻辑和订阅消息推送各层能直接复用的代码和容易踩的坑都会讲到。2. 微信小程序端预约表单设计与请求层封装2.1 小程序页面结构与预约入口小程序端的入口一般放在首页的“访客预约”按钮上点击后跳转到预约表单页。这套源码里HTML文件152个但那主要是后台管理页面小程序原生页面以wxml、wxss、js、json四个文件为一组放在pages目录下。目录结构常见是这样miniprogram/ ├─ pages/ │ ├─ index/ // 首页访客预约入口按钮 │ ├─ appointment/ // 预约表单页提交预约 │ └─ record/ // 预约记录页查询审核状态 ├─ utils/ │ └─ request.js // 统一请求封装 └─ app.js // 小程序全局配置与基础URL预约表单的字段建议精简到最核心的信息访客姓名、手机号、身份证尾号、来访事由、被访人、预计到访时间、随行人数。字段不是越多越好后端要做入库校验前端字段过多反而拉低首次填写完成率。表单校验拆成两层前端保证格式正确后端保证数据可信后面3.2节会给出对应的DTO校验。2.2 统一request封装与登录态处理每个页面直接调wx.request会产生大量重复代码token过期和错误提示也难以统一处理。我在小程序端通常会先封装utils/request.js用Promise承接后端统一返回体const request (url, method GET, data {}) { return new Promise((resolve, reject) { const token wx.getStorageSync(token); wx.request({ url: getApp().globalData.baseUrl url, method, data, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success(res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else if (res.statusCode 401) { wx.redirectTo({ url: /pages/login/login }); reject(res.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络异常请稍后重试, icon: none }); reject(err); } }); }); }; module.exports { request };这段逻辑的关键是统一处理三个状态200且业务码为0表示成功直接解包返回data401表示登录态过期重定向到登录页其他错误统一弹Toast。header里的Authorization带的是带Bearer前缀的token后端过滤器摘掉前缀后解析JWT。注意这里baseUrl在小程序真机上必须是HTTPS不能用开发者工具里的localhost否则手机端请求会被微信拦截。2.3 预约表单校验与提交表单数据在提交前要先做一次轻量校验手机号、到访时间这两项最容易被漏掉。使用form的bindsubmit收集字段再调用requestsubmitAppointment(e) { const form e.detail.value; if (!/^1[3-9]\d{9}$/.test(form.phone)) { wx.showToast({ title: 请填写正确手机号, icon: none }); return; } if (!form.visitTime) { wx.showToast({ title: 请选择预计到访时间, icon: none }); return; } request(/api/appointment/submit, POST, { visitorName: form.name, phone: form.phone, idCardTail: form.idCardTail, reason: form.reason, interviewee: form.interviewee, visitTime: form.visitTime, accompanyNum: Number(form.accompanyNum) || 0 }).then(() { wx.showToast({ title: 预约成功等待审核, icon: success }); setTimeout(() wx.navigateBack(), 1500); }).catch(() {}); }visitTime建议后端按“2025-02-18 14:30:00”的格式接收前端不要直接传Date对象。小程序在iOS上解析“2025-02-18 14:30”会返回Invalid Date这个坑在真机联调时很容易踩到。后来我统一改成后端接口接收标准字符串存储层转成timestamp前端展示时再格式化成页面文案。预约表单字段对应关系可以整理成下面这张表字段名类型必填校验/说明visitorNamestring是访客姓名1-30字符phonestring是手机号正则校验idCardTailstring否身份证后4位门岗核对用reasonstring是来访事由200字以内intervieweestring是被访人姓名后端关联员工表visitTimestring是预计到访时间格式yyyy-MM-dd HH:mm:ssaccompanyNumint否随行人数默认02.4 提交按钮防重复点击表单提交接口是写操作用户连续点两次就会生成两条预约单。前端需要用一个submitting状态拦住重复点击if (this.data.submitting) return; this.setData({ submitting: true }); wx.showLoading({ title: 提交中... }); // 调用request // finally里 wx.hideLoading() 并 setData({ submitting: false })后端也要做配合最简单的是在appointment表上建一个手机号加visit_time的唯一索引或者在Service层用分布式锁。防重不是单端的事前后端任何一端漏掉生产环境都会出现重复预约。3. Java后端管理系统接口与访客审核实现3.1 从289个Java文件反推工程结构289个Java文件不是单表CRUD能堆出来的量。Spring Boot加MyBatis的项目按Controller、Service、Mapper、Entity、DTO、Config、Util划分一个访客预约模块至少几十个类。这个数量说明源码做了模块化拆分不是把所有接口塞进一个Controller。常见工程布局src/main/java/com/company/visitor/ ├─ controller/ // 小程序端与管理端接口 ├─ service/ // 业务逻辑审核状态流转 ├─ mapper/ // MyBatis数据访问层 ├─ entity/ // 数据库实体 ├─ dto/ // 请求响应对象 ├─ config/ // 拦截器、跨域、微信配置 └─ common/ // 统一返回体、异常处理管理端的152个HTML文件对应后台页面模板配合CSS和JavaScript完成预约列表、审核操作、数据看板。小程序端和管理端共用同一套Java接口区别在于登录角色小程序端用token识别访客身份管理端在token基础上增加管理员角色校验Controller层通过自定义注解区分接口权限。3.2 预约提交与分页查询接口预约提交接口需要处理新增和防重通过Valid注解完成入参校验。Controller层代码RestController RequestMapping(/api/appointment) public class AppointmentController { Autowired private AppointmentService appointmentService; PostMapping(/submit) public ResultVoid submit(RequestBody Valid AppointmentSubmitDTO dto) { appointmentService.submit(dto); return Result.success(); } GetMapping(/list) public ResultPageResultAppointmentVO list(RequestParam Integer page, RequestParam Integer size, RequestParam(required false) String status) { PageResultAppointmentVO result appointmentService.queryPage(page, size, status); return Result.success(result); } }这里的Result是统一返回体包含code、msg、data三个字段与小程序端request封装里处理的res.data.code对应。DTO上的校验注解可以控制非法参数在进入Service前就被拦下public class AppointmentSubmitDTO { NotBlank(message 访客姓名不能为空) private String visitorName; NotBlank(message 手机号不能为空) Pattern(regexp ^1[3-9]\\d{9}$, message 手机号格式不正确) private String phone; private String idCardTail; private String reason; private String interviewee; NotNull(message 到访时间不能为空) JsonFormat(pattern yyyy-MM-dd HH:mm:ss) private LocalDateTime visitTime; private Integer accompanyNum; }JsonFormat注解是接收前端时间字符串的关键很多联调问题出在忘记加这个注解Spring解析ISO格式字符串时会直接抛400。如果前端传的是时间戳这里改成Long类型再用LocalDateTime转换即可。3.3 审核操作与状态驱动管理后台的审核动作本质是把预约单从PENDING置为APPROVED或REJECTED。有两个细节容易被忽略审核必须带操作人ID便于事后追溯状态更新必须带“当前状态PENDING”的查询条件否则重复审核会互相覆盖。MyBatis注解SQL实现条件更新Mapper public interface AppointmentMapper { Update(UPDATE appointment SET status #{newStatus}, auditor_id #{auditorId}, audit_remark #{auditRemark}, audit_time NOW() WHERE id #{id} AND status PENDING) int updateStatusWithPending(AppointmentAuditDTO dto); }更新行数返回0时Service层直接抛业务异常提示“该预约已被处理请刷新页面”。这种数据库条件更新相当于乐观锁避免并发审核造成状态错乱。审核之后还要写一条audit_log记录变更前状态、变更后状态、操作人和操作时间方便后期排查。预约状态不是随意流转按照状态机约束来设计状态编码含义允许流向PENDING待审核APPROVED, REJECTED, CANCELLEDAPPROVED已通过CHECKED_IN, CANCELLEDREJECTED已拒绝无CHECKED_IN已签入无CANCELLED已取消无3.4 管理看板的统计口径后台首页通常要展示今日预约数、待审核数、到访率三个指标可以聚合在一条SQL里避免多次查询数据库Select(SELECT date(visit_time) AS day, COUNT(*) AS total, SUM(CASE WHEN status APPROVED OR status CHECKED_IN THEN 1 ELSE 0 END) AS approved FROM appointment WHERE visit_time BETWEEN #{startDate} AND #{endDate} GROUP BY date(visit_time) ORDER BY day) ListDailyStatVO statDaily(Param(startDate) LocalDateTime startDate, Param(endDate) LocalDateTime endDate);到访率要用CHECKED_IN除以总预约数而不是APPROVED除以总数。因为审核通过不代表人一定来只有门岗确认签入才是真正的到访统计口径不一致会直接影响管理人员对预约转化率的判断。4. 预约状态流转与微信订阅消息通知的落地4.1 状态流转为什么需要独立服务审核通过不只是改一条状态还要给访客发微信订阅消息、生成门岗核验记录。这三件事要么全成功要么全不成功所以Service层要加Transactional。注意事务只覆盖数据库操作调用微信接口属于外部IO不能放在事务中间否则微信用时过长会长期占用数据库连接。常见做法是先提交事务再发订阅消息发失败时通过定时任务补偿。状态流转的Java代码示例Service public class AppointmentServiceImpl implements AppointmentService { Transactional Override public void auditAppointment(AppointmentAuditDTO dto) { String newStatus dto.getApproved() ? APPROVED : REJECTED; int rows appointmentMapper.updateStatusWithPending( dto.getId(), newStatus, dto.getAuditorId(), dto.getAuditRemark()); if (rows 0) { throw new BizException(预约状态已变化请刷新后重试); } auditLogMapper.insert(new AuditLog(dto.getId(), PENDING, newStatus, dto.getAuditorId())); // 数据库事务提交后再执行微信通知 } }updateStatusWithPending方法里的where条件包含statusPENDING这是乐观锁的简化实现。如果后续预约量变大可以把状态机抽成策略模式但当前单表单场景下条件更新已经足够。4.2 后端调用微信订阅消息接口预约审核通过后小程序端需要收到结果通知。微信小程序的订阅消息是一次性的用户授权一次后台才能发送一条。所以预约提交时前端要引导用户点击“允许”订阅后端保存模板授权记录。发送接口使用Spring的RestTemplatepublic void sendSubscribeMessage(String openid, String appointmentId, String status) { String url https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token getAccessToken(); MapString, Object body new HashMap(); body.put(touser, openid); body.put(template_id, 审核结果通知模板ID); body.put(page, pages/record/record?appointmentId appointmentId); MapString, String event new HashMap(); event.put(value, status); MapString, Object data new HashMap(); data.put(thing1, event); body.put(data, data); restTemplate.postForObject(url, body, String.class); }模板字段要参考微信公众平台申请模板里定义的字段比如“审核状态{{thing1.DATA}}”这里data的key就是模板字段的key不能自定义命名。page字段是用户点击订阅消息后跳转的小程序页面该页面路径必须在主包或分包内配置过。提示template_id必须与page跳转页面属于同一小程序主体否则发送接口会返回47003错误。getAccessToken必须做缓存微信接口有单日调用上限每次请求都去拉token很容易触发频率限制。常见做法是把access_token存到Redis设置7000秒过期获取前先查缓存缓存没有再去微信接口拉取。4.3 小程序端订阅消息授权操作订阅消息授权必须由用户点击行为触发不能进页面就调用。预约表单底部加一个“预约成功时通知我”的开关用户打开开关后再请求授权wx.requestSubscribeMessage({ tmplIds: [审核结果通知模板ID], success(res) { if (res[审核结果通知模板ID] accept) { this.setData({ subscribeAccepted: true }); } else { console.warn(用户拒绝订阅消息); } }, fail() { // 用户未授权时依然允许提交预约只是收不到消息通知 } });用户拒绝或授权失败都不应该阻塞预约主流程。把subscribeAccepted存进data里提交预约时一起发给后端后端只在授权成功时调用订阅消息接口。订阅消息模板字段对应关系如下模板字段类型示例值说明thing1短文本审核通过状态描述20字以内thing2短文本张工访客姓名date3日期时间2025-02-18 15:30预计到访时间phrase4短语请准时到访固定话术如果用户拒绝接收消息小程序端的预约记录页还需要提供手动刷新按钮避免访客因为收不到通知而重复提交。5. 真机联调、访问令牌与后端排查清单5.1 小程序端token校验的最小实现后端接口统一通过过滤器解析Authorization请求头把openid写入request属性业务层按需读取Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String auth request.getHeader(Authorization); if (StringUtils.hasText(auth) auth.startsWith(Bearer )) { Claims claims JwtUtil.parse(auth.substring(7)); request.setAttribute(openid, claims.get(openid)); } chain.doFilter(request, response); } }如果是管理端接口过滤器解出openid之后还需要继续校验管理员角色。这个角色校验不放在过滤器里而是放在Service层便于复用审计日志记录操作人。5.2 真机联调常见问题对照现象原因处理办法真机请求一直pending小程序后台未配置合法域名开发阶段可勾选“不校验合法域名”开发者工具正常手机预览失败手机端强制校验域名与HTTPS使用已备案HTTPS域名或将IP场景改走内网通道日期显示NaNiOS不识别“-”分隔字符串前后端统一切换为timestamp订阅消息收不到template_id与data字段不匹配按4.3节的字段对应关系核对模板5.3 上线前的最后一份检查清单确认小程序后台的request合法域名已加入并校验通过。检查后端JWT密钥是否从配置文件外部注入不硬编码在代码里。验证access_token缓存是否生效避免线上日志出现频率超限。预约时间字段统一使用timestamp入库前端展示再做格式化。为预约单添加手机号与visit_time联合唯一索引兜底防重。管理端审核按钮加二次确认弹窗减少误操作对审核记录的影响。这份清单的最后一项也就是审核按钮的二次确认通常在开发阶段容易被忽略但它直接影响后台审核记录的可信度。无论预约量大小我都建议在任何访客管理类项目里保留。本文还有配套的精品资源点击获取