微信小程序访客预约系统:从表单到后端审核的完整实现
2026/9/11 18:39:27 网站建设 项目流程

简介:面向需要构建访客预约系统的企业、开发团队及学习者,这款基于微信小程序与后端管理的完整设计源码,将小程序端预约登记、信息查询与后台管理端的审核、统计功能打通,可直接用于企事业单位的访客管理场景或教学实战。压缩包共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表示成功,直接解包返回data;401表示登录态过期,重定向到登录页;其他错误统一弹Toast。header里的Authorization带的是带Bearer前缀的token,后端过滤器摘掉前缀后解析JWT。注意这里baseUrl在小程序真机上必须是HTTPS,不能用开发者工具里的localhost,否则手机端请求会被微信拦截。

2.3 预约表单校验与提交

表单数据在提交前要先做一次轻量校验,手机号、到访时间这两项最容易被漏掉。使用form的bindsubmit收集字段,再调用request:

submitAppointment(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:ss
accompanyNumint随行人数,默认0

2.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 Result<Void> submit(@RequestBody @Valid AppointmentSubmitDTO dto) { appointmentService.submit(dto); return Result.success(); } @GetMapping("/list") public Result<PageResult<AppointmentVO>> list(@RequestParam Integer page, @RequestParam Integer size, @RequestParam(required = false) String status) { PageResult<AppointmentVO> 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, CANCELLED
APPROVED已通过CHECKED_IN, CANCELLED
REJECTED已拒绝
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") List<DailyStatVO> 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条件包含status='PENDING',这是乐观锁的简化实现。如果后续预约量变大,可以把状态机抽成策略模式,但当前单表单场景下条件更新已经足够。

4.2 后端调用微信订阅消息接口

预约审核通过后,小程序端需要收到结果通知。微信小程序的订阅消息是一次性的,用户授权一次,后台才能发送一条。所以预约提交时,前端要引导用户点击“允许”订阅,后端保存模板授权记录。发送接口使用Spring的RestTemplate:

public void sendSubscribeMessage(String openid, String appointmentId, String status) { String url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + getAccessToken(); Map<String, Object> body = new HashMap<>(); body.put("touser", openid); body.put("template_id", "审核结果通知模板ID"); body.put("page", "pages/record/record?appointmentId=" + appointmentId); Map<String, String> event = new HashMap<>(); event.put("value", status); Map<String, 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联合唯一索引,兜底防重。
  • 管理端审核按钮加二次确认弹窗,减少误操作对审核记录的影响。

这份清单的最后一项,也就是审核按钮的二次确认,通常在开发阶段容易被忽略,但它直接影响后台审核记录的可信度。无论预约量大小,我都建议在任何访客管理类项目里保留。

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

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

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

立即咨询