From 4ac8fa20cb3585bf42053263dc729b6cbf4b5748 Mon Sep 17 00:00:00 2001 From: ws Date: Wed, 22 Apr 2026 17:49:44 +0800 Subject: [PATCH] feat: add subscribe message push and refactor HTTP layer --- .../config/RestTemplateConfig.java | 18 -- .../service/AppointmentService.java | 121 ++++---- .../service/WxApprovalService.java | 258 ++++++------------ .../service/WxSubscribeMessageService.java | 106 +++++++ .../example/mini_program/util/HttpUtil.java | 72 +++++ src/main/resources/application.yml | 23 +- src/main/resources/data/data.sql | 26 ++ src/main/resources/data/db.sql | 5 + 8 files changed, 346 insertions(+), 283 deletions(-) delete mode 100644 src/main/java/com/example/mini_program/config/RestTemplateConfig.java create mode 100644 src/main/java/com/example/mini_program/util/HttpUtil.java diff --git a/src/main/java/com/example/mini_program/config/RestTemplateConfig.java b/src/main/java/com/example/mini_program/config/RestTemplateConfig.java deleted file mode 100644 index 12193bd..0000000 --- a/src/main/java/com/example/mini_program/config/RestTemplateConfig.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.mini_program.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.SimpleClientHttpRequestFactory; -import org.springframework.web.client.RestTemplate; - -@Configuration -public class RestTemplateConfig { - - @Bean - public RestTemplate restTemplate() { - SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); - factory.setConnectTimeout(10000); - factory.setReadTimeout(30000); - return new RestTemplate(factory); - } -} diff --git a/src/main/java/com/example/mini_program/service/AppointmentService.java b/src/main/java/com/example/mini_program/service/AppointmentService.java index d362d61..6e276a6 100644 --- a/src/main/java/com/example/mini_program/service/AppointmentService.java +++ b/src/main/java/com/example/mini_program/service/AppointmentService.java @@ -10,6 +10,10 @@ import org.springframework.stereotype.Service; import java.util.List; import java.util.UUID; +/** + * 访客预约服务 + * 创建预约 → 提交企微审批 → 推送订阅消息通知 + */ @Slf4j @Service @RequiredArgsConstructor @@ -17,118 +21,91 @@ public class AppointmentService { private final VisitApplicationMapper visitApplicationMapper; private final WxApprovalService wxApprovalService; + private final WxSubscribeMessageService wxSubscribeMessageService; @Value("${wx.corp.creator-userid:}") private String creatorUserId; - /** - * 根据openid获取最新的一条预约记录 - */ + /** 根据openid获取最新一条预约 */ public VisitApplication getLatest(String openid) { - log.info("查询用户最新预约记录, openid: {}", openid); - VisitApplication result = visitApplicationMapper.selectLatestByOpenid(openid); - if (result != null) { - log.info("找到预约记录, id: {}", result.getId()); - } else { - log.info("未找到预约记录"); - } - return result; + return visitApplicationMapper.selectLatestByOpenid(openid); } - /** - * 获取用户所有预约记录(按创建时间倒序) - */ + /** 获取用户所有预约记录(按创建时间倒序) */ public List getList(String openid) { - log.info("查询用户预约列表, openid: {}", openid); - List list = visitApplicationMapper.selectListByOpenid(openid); - log.info("查询到 {} 条预约记录", list.size()); - return list; + return visitApplicationMapper.selectListByOpenid(openid); } - /** - * 创建预约记录 - */ + /** 创建预约记录 */ public VisitApplication create(VisitApplication record) { record.setId(UUID.randomUUID().toString().replace("-", "")); record.setStatus("pending"); record.setStatusText("待审核"); - // 发起企业微信审批 + // 提交企业微信审批 try { - String visitTime = record.getVisitDate(); - if (record.getVisitTime() != null && !record.getVisitTime().isEmpty()) { - visitTime = record.getVisitDate() + " " + record.getVisitTime(); - } String spNo = wxApprovalService.submitApproval( - creatorUserId, - record.getName(), - record.getPhone(), - record.getCompany(), - record.getReason(), - visitTime, - record.getHostName(), - record.getArea() - ); + creatorUserId, record.getName(), record.getPhone(), record.getCompany(), + record.getReason(), formatVisitTime(record), record.getHostName(), record.getArea()); record.setSpNo(spNo); - log.info("企业微信审批提交成功, spNo: {}", spNo); } catch (Exception e) { - log.error("企业微信审批提交失败,预约记录仍会保存", e); + log.error("企微审批提交失败,预约仍会保存", e); } visitApplicationMapper.insert(record); - log.info("创建预约记录成功, id: {}, openid: {}", record.getId(), record.getOpenid()); + log.info("创建预约成功, id={}", record.getId()); + + // 推送订阅消息 + try { + wxSubscribeMessageService.sendSubscribeMessage( + record.getOpenid(), record.getName(), record.getReason(), + formatVisitTime(record), record.getArea(), "待审核"); + } catch (Exception e) { + log.error("订阅消息推送失败", e); + } + return record; } - /** - * 取消预约(仅pending状态可取消,需校验openid) - */ + /** 取消预约(仅 pending 状态可取消) */ public boolean cancel(String id, String openid) { - log.info("取消预约, id: {}, openid: {}", id, openid); - VisitApplication existing = visitApplicationMapper.selectByIdAndOpenid(id, openid); - if (existing == null) { - log.warn("预约记录不存在或不属于该用户, id: {}, openid: {}", id, openid); + if (existing == null || !"pending".equals(existing.getStatus())) { return false; } - if (!"pending".equals(existing.getStatus())) { - log.warn("预约状态不允许取消, id: {}, status: {}", id, existing.getStatus()); - return false; - } - - int rows = visitApplicationMapper.updateStatusToCancelled(id, openid); - if (rows > 0) { - log.info("取消预约成功, id: {}", id); - return true; - } - log.warn("取消预约失败, id: {}", id); - return false; + return visitApplicationMapper.updateStatusToCancelled(id, openid) > 0; } - /** - * 审批预约(通过/拒绝) - */ + /** 审批预约(通过/拒绝) */ public boolean approve(String id, String status) { - log.info("审批预约, id: {}, status: {}", id, status); - VisitApplication existing = visitApplicationMapper.selectById(id); - if (existing == null) { - log.warn("预约记录不存在, id: {}", id); - return false; - } - if (!"pending".equals(existing.getStatus())) { - log.warn("预约状态不允许审批, id: {}, currentStatus: {}", id, existing.getStatus()); + if (existing == null || !"pending".equals(existing.getStatus())) { return false; } String statusText = "approved".equals(status) ? "已通过" : "已拒绝"; - int rows = visitApplicationMapper.updateStatus(id, status, statusText); - if (rows <= 0) { - log.warn("审批更新失败, id: {}", id); + if (visitApplicationMapper.updateStatus(id, status, statusText) <= 0) { return false; } - log.info("审批成功, id: {}, status: {}", id, statusText); + // 推送审批结果订阅消息 + try { + wxSubscribeMessageService.sendSubscribeMessage( + existing.getOpenid(), existing.getName(), existing.getReason(), + formatVisitTime(existing), existing.getArea(), statusText); + } catch (Exception e) { + log.error("审批结果订阅消息推送失败", e); + } + return true; } + + /** 拼接访问日期+时间 */ + private String formatVisitTime(VisitApplication record) { + String time = record.getVisitDate(); + if (record.getVisitTime() != null && !record.getVisitTime().isEmpty()) { + time = record.getVisitDate() + " " + record.getVisitTime(); + } + return time; + } } diff --git a/src/main/java/com/example/mini_program/service/WxApprovalService.java b/src/main/java/com/example/mini_program/service/WxApprovalService.java index 97f534d..b8ae38b 100644 --- a/src/main/java/com/example/mini_program/service/WxApprovalService.java +++ b/src/main/java/com/example/mini_program/service/WxApprovalService.java @@ -1,17 +1,25 @@ package com.example.mini_program.service; import com.example.mini_program.config.WxCorpConfig; +import com.example.mini_program.util.HttpUtil; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +/** + * 企业微信审批服务 + * 调用企微 /cgi-bin/oa/applyevent 提交审批,/cgi-bin/oa/getapprovaldetail 查询审批状态 + */ @Slf4j @Service @RequiredArgsConstructor @@ -19,228 +27,125 @@ public class WxApprovalService { private static final String GET_TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"; - private static final String SUBMIT_APPROVAL_URL = + private static final String SUBMIT_URL = "https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent?access_token=%s"; - private static final String GET_APPROVAL_DETAIL_URL = + private static final String DETAIL_URL = "https://qyapi.weixin.qq.com/cgi-bin/oa/getapprovaldetail?access_token=%s"; private final WxCorpConfig wxCorpConfig; - private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; - /** - * 获取企业微信AccessToken - */ + /** 获取企业微信 access_token */ public String getAccessToken() { String url = String.format(GET_TOKEN_URL, wxCorpConfig.getCorpid(), wxCorpConfig.getCorpsecret()); - log.info("获取企业微信access_token, corpid: {}", wxCorpConfig.getCorpid()); - try { - Map response = restTemplate.getForObject(url, Map.class); - if (response != null && "0".equals(String.valueOf(response.get("errcode")))) { - String accessToken = (String) response.get("access_token"); - log.info("获取access_token成功: {}", accessToken); - return accessToken; - } else { - log.error("获取access_token失败: {}", response); - throw new RuntimeException("获取access_token失败: " + response); + Map resp = objectMapper.readValue(HttpUtil.get(url), Map.class); + if ("0".equals(String.valueOf(resp.get("errcode")))) { + return (String) resp.get("access_token"); } + throw new RuntimeException("获取access_token失败: " + resp); } catch (Exception e) { - log.error("调用企业微信接口失败", e); - throw new RuntimeException("获取access_token异常: " + e.getMessage()); + throw new RuntimeException("获取access_token异常: " + e.getMessage(), e); } } - /** - * 提交审批申请(使用模板中配置的审批流程) - * - * @param creatorUserId 申请人用户ID - * @param visitorName 访客姓名 - * @param visitorPhone 访客电话 - * @param visitorCompany 访客公司(可选) - * @param visitPurpose 来访事由 - * @param visitTime 日期+时间 - * @param visiteeName 被访人(可选) - * @param visitArea 拜访区域(可选) - * @return 审批单号 - */ + /** 提交审批申请 */ public String submitApproval(String creatorUserId, String visitorName, String visitorPhone, String visitorCompany, String visitPurpose, String visitTime, String visiteeName, String visitArea) { - String accessToken = getAccessToken(); - String url = String.format(SUBMIT_APPROVAL_URL, accessToken); + String url = String.format(SUBMIT_URL, getAccessToken()); - // 构建审批表单数据(apply_data) - Map applyData = buildApplyData(visitorName, visitorPhone, visitorCompany, - visitPurpose, visitTime, visiteeName, visitArea); - - // 构建摘要信息 - List> summaryList = buildSummaryList(visitorName, visitPurpose); - - // 构建完整的审批请求体(use_template_approver=1 使用模板中配置的审批流程) - Map requestBody = new HashMap<>(); - requestBody.put("creator_userid", creatorUserId); - requestBody.put("template_id", wxCorpConfig.getApprovalTemplateId()); - requestBody.put("use_template_approver", 1); - requestBody.put("apply_data", applyData); - requestBody.put("summary_list", summaryList); - - log.info("提交审批申请, visitorName: {}, visitorPhone: {}", visitorName, visitorPhone); - log.debug("审批请求体: {}", requestBody); + Map body = new HashMap<>(); + body.put("creator_userid", creatorUserId); + body.put("template_id", wxCorpConfig.getApprovalTemplateId()); + body.put("use_template_approver", 1); + body.put("apply_data", buildApplyData(visitorName, visitorPhone, visitorCompany, + visitPurpose, visitTime, visiteeName, visitArea)); + body.put("summary_list", buildSummaryList(visitorName, visitPurpose)); try { - Map response = restTemplate.postForObject(url, requestBody, Map.class); - if (response != null && "0".equals(String.valueOf(response.get("errcode")))) { - String spNo = (String) response.get("sp_no"); - log.info("审批提交成功, spNo: {}", spNo); + String json = objectMapper.writeValueAsString(body); + Map resp = objectMapper.readValue(HttpUtil.postJson(url, json), Map.class); + if ("0".equals(String.valueOf(resp.get("errcode")))) { + String spNo = (String) resp.get("sp_no"); + log.info("审批提交成功, spNo={}", spNo); return spNo; - } else { - log.error("审批提交失败: {}", response); - throw new RuntimeException("审批提交失败: " + response); } + throw new RuntimeException("审批提交失败: " + resp); } catch (Exception e) { - log.error("提交审批申请异常", e); - throw new RuntimeException("提交审批申请异常: " + e.getMessage()); + throw new RuntimeException("提交审批申请异常: " + e.getMessage(), e); } } - /** - * 构建审批表单数据(apply_data) - * 控件ID来源于"获取审批模板详情"接口,需与审批模板中的控件ID一致 - * - * 模板控件对应关系: - * Text-1776786661954 → 姓名 - * Text-1776786666351 → 手机号 - * Text-1776786668098 → 公司 - * Text-1776786672408 → 来访事由 - * Date-1776786680089 → 日期+时间 - * Text-1776786690968 → 被访人 - * Text-1776786692400 → 拜访区域 - */ + /** 查询审批状态 */ + public ApprovalStatus getApprovalStatus(String spNo) { + String url = String.format(DETAIL_URL, getAccessToken()); + try { + String json = objectMapper.writeValueAsString(Map.of("sp_no", spNo)); + Map resp = objectMapper.readValue(HttpUtil.postJson(url, json), Map.class); + if ("0".equals(String.valueOf(resp.get("errcode")))) { + @SuppressWarnings("unchecked") + Map info = (Map) resp.get("info"); + ApprovalStatus status = new ApprovalStatus(); + if (info != null) { + status.setSpNo((String) info.get("sp_no")); + status.setSpStatus((Integer) info.get("sp_status")); + status.setSpStatusText(toStatusText((Integer) info.get("sp_status"))); + } + return status; + } + throw new RuntimeException("审批状态查询失败: " + resp); + } catch (Exception e) { + throw new RuntimeException("查询审批状态异常: " + e.getMessage(), e); + } + } + + // ---- 内部方法 ---- + private Map buildApplyData(String visitorName, String visitorPhone, String visitorCompany, String visitPurpose, - String visitTime, String visiteeName, - String visitArea) { + String visitTime, String visiteeName, String visitArea) { List> contents = new ArrayList<>(); - contents.add(buildTextControl("Text-1776786661954", visitorName)); - contents.add(buildTextControl("Text-1776786666351", visitorPhone)); - contents.add(buildTextControl("Text-1776786668098", visitorCompany)); - contents.add(buildTextControl("Text-1776786672408", visitPurpose)); - contents.add(buildDateControl("Date-1776786680089", visitTime)); - contents.add(buildTextControl("Text-1776786690968", visiteeName)); - contents.add(buildTextControl("Text-1776786692400", visitArea)); - + contents.add(textControl("Text-1776786661954", visitorName)); + contents.add(textControl("Text-1776786666351", visitorPhone)); + contents.add(textControl("Text-1776786668098", visitorCompany)); + contents.add(textControl("Text-1776786672408", visitPurpose)); + contents.add(dateControl("Date-1776786680089", visitTime)); + contents.add(textControl("Text-1776786690968", visiteeName)); + contents.add(textControl("Text-1776786692400", visitArea)); return Map.of("contents", contents); } - /** - * 构建文本控件数据 - * 对应 control 参数为 Text 或 Textarea - * - * @param id 控件ID(需与审批模板中的控件ID一致) - * @param value 文本内容 - */ - private Map buildTextControl(String id, String value) { - return Map.of( - "control", "Text", - "id", id, - "value", Map.of("text", value != null ? value : "") - ); + private Map textControl(String id, String value) { + return Map.of("control", "Text", "id", id, + "value", Map.of("text", value != null ? value : "")); } - /** - * 构建日期/日期+时间控件数据 - * 对应 control 参数为 Date - * - * @param id 控件ID(需与审批模板中的控件ID一致) - * @param dateTime 日期时间字符串,格式:yyyy-MM-dd HH:mm - */ - private Map buildDateControl(String id, String dateTime) { + private Map dateControl(String id, String dateTime) { long timestamp = 0; if (dateTime != null && !dateTime.isEmpty()) { try { - java.time.LocalDateTime ldt = java.time.LocalDateTime.parse(dateTime, - java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); - timestamp = ldt.atZone(java.time.ZoneId.of("Asia/Shanghai")) - .toInstant().getEpochSecond(); + timestamp = LocalDateTime.parse(dateTime, + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) + .atZone(ZoneId.of("Asia/Shanghai")).toInstant().getEpochSecond(); } catch (Exception e) { - log.warn("日期时间解析失败: {}", dateTime, e); + log.warn("日期解析失败: {}", dateTime); } } - return Map.of( - "control", "Date", - "id", id, - "value", Map.of( - "date", Map.of( - "type", "hour", - "s_timestamp", String.valueOf(timestamp) - ) - ) - ); + return Map.of("control", "Date", "id", id, + "value", Map.of("date", Map.of("type", "hour", "s_timestamp", String.valueOf(timestamp)))); } - /** - * 构建摘要信息,显示在审批通知卡片和审批列表中,最多3行 - */ private List> buildSummaryList(String visitorName, String visitPurpose) { return List.of( Map.of("summary_info", List.of( - Map.of("text", "访客: " + (visitorName != null ? visitorName : ""), "lang", "zh_CN") - )), + Map.of("text", "访客: " + (visitorName != null ? visitorName : ""), "lang", "zh_CN"))), Map.of("summary_info", List.of( - Map.of("text", "目的: " + (visitPurpose != null ? visitPurpose : ""), "lang", "zh_CN") - )) + Map.of("text", "目的: " + (visitPurpose != null ? visitPurpose : ""), "lang", "zh_CN"))) ); } - /** - * 获取审批状态 - * - * @param spNo 审批单号 - * @return 审批状态信息 - */ - public ApprovalStatus getApprovalStatus(String spNo) { - String accessToken = getAccessToken(); - String url = String.format(GET_APPROVAL_DETAIL_URL, accessToken); - - Map requestBody = Map.of("sp_no", spNo); - - log.info("查询审批状态, spNo: {}", spNo); - - try { - Map response = restTemplate.postForObject(url, requestBody, Map.class); - if (response != null && "0".equals(String.valueOf(response.get("errcode")))) { - @SuppressWarnings("unchecked") - Map info = (Map) response.get("info"); - ApprovalStatus status = parseApprovalStatus(info); - log.info("审批状态查询成功, spNo: {}, status: {}", spNo, status); - return status; - } else { - log.error("审批状态查询失败: {}", response); - throw new RuntimeException("审批状态查询失败: " + response); - } - } catch (Exception e) { - log.error("查询审批状态异常", e); - throw new RuntimeException("查询审批状态异常: " + e.getMessage()); - } - } - - /** - * 解析审批状态 - */ - private ApprovalStatus parseApprovalStatus(Map spInfo) { - ApprovalStatus status = new ApprovalStatus(); - if (spInfo != null) { - status.setSpNo((String) spInfo.get("sp_no")); - status.setSpStatus((Integer) spInfo.get("sp_status")); - status.setSpStatusText(getStatusText((Integer) spInfo.get("sp_status"))); - } - return status; - } - - /** - * 状态码转文本 - */ - private String getStatusText(Integer spStatus) { + private static String toStatusText(Integer spStatus) { if (spStatus == null) return "未知"; return switch (spStatus) { case 1 -> "审批中"; @@ -251,9 +156,6 @@ public class WxApprovalService { }; } - /** - * 审批状态结果类 - */ @Data public static class ApprovalStatus { private String spNo; diff --git a/src/main/java/com/example/mini_program/service/WxSubscribeMessageService.java b/src/main/java/com/example/mini_program/service/WxSubscribeMessageService.java index e69de29..1e3aab0 100644 --- a/src/main/java/com/example/mini_program/service/WxSubscribeMessageService.java +++ b/src/main/java/com/example/mini_program/service/WxSubscribeMessageService.java @@ -0,0 +1,106 @@ +package com.example.mini_program.service; + +import com.example.mini_program.config.WxMiniAppConfig; +import com.example.mini_program.util.HttpUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +/** + * 微信小程序订阅消息服务 + * 调用微信 /cgi-bin/message/subscribe/send 接口推送订阅消息 + * + * 模板字段映射: + * name1 (姓名) → 访客姓名 + * thing3 (事物) → 来访事由 + * date8 (日期) → 预约时间 + * thing10 (事物) → 到访区域 + * phrase18 (短语) → 审批状态 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WxSubscribeMessageService { + + private static final String GET_TOKEN_URL = + "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s"; + private static final String SEND_MSG_URL = + "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s"; + + private final WxMiniAppConfig wxMiniAppConfig; + private final ObjectMapper objectMapper; + + @Value("${wx.miniapp.subscribe-template-id:}") + private String templateId; + + /** 获取小程序 access_token */ + public String getAccessToken() { + String url = String.format(GET_TOKEN_URL, wxMiniAppConfig.getAppid(), wxMiniAppConfig.getSecret()); + try { + Map resp = objectMapper.readValue(HttpUtil.get(url), Map.class); + if (resp.containsKey("access_token")) { + return (String) resp.get("access_token"); + } + throw new RuntimeException("获取access_token失败: " + resp); + } catch (Exception e) { + throw new RuntimeException("获取access_token异常: " + e.getMessage(), e); + } + } + + /** + * 发送订阅消息 + * 所有字段不允许为空字符串(微信返回47003),name类型不接受纯数字 + */ + public void sendSubscribeMessage(String openid, String visitorName, String reason, + String visitTime, String area, String status) { + if (templateId == null || templateId.isEmpty()) { + log.warn("未配置 subscribe-template-id,跳过订阅消息推送"); + return; + } + + String accessToken = getAccessToken(); + String url = String.format(SEND_MSG_URL, accessToken); + + // name1 是 name 类型,纯数字会被微信拒绝,需加前缀兜底 + String safeName = blankToDefault(visitorName, "访客"); + if (safeName.matches("^\\d+$")) { + safeName = "访客" + safeName; + } + + Map data = new HashMap<>(); + data.put("name1", Map.of("value", safeName)); + data.put("thing3", Map.of("value", blankToDefault(reason, "来访"))); + data.put("date8", Map.of("value", blankToDefault(visitTime, "待定"))); + data.put("thing10", Map.of("value", blankToDefault(area, "未指定"))); + data.put("phrase18", Map.of("value", blankToDefault(status, "待审核"))); + + Map body = new HashMap<>(); + body.put("touser", openid); + body.put("template_id", templateId); + body.put("page", "pages/records/records"); + body.put("data", data); + body.put("miniprogram_state", "formal"); + body.put("lang", "zh_CN"); + + try { + String json = objectMapper.writeValueAsString(body); + Map resp = objectMapper.readValue(HttpUtil.postJson(url, json), Map.class); + if ("0".equals(String.valueOf(resp.get("errcode")))) { + log.info("订阅消息推送成功, openid={}", openid); + } else { + log.error("订阅消息推送失败: {}", resp); + } + } catch (Exception e) { + log.error("订阅消息推送异常", e); + } + } + + private static String blankToDefault(String val, String def) { + return (val != null && !val.isEmpty()) ? val : def; + } +} diff --git a/src/main/java/com/example/mini_program/util/HttpUtil.java b/src/main/java/com/example/mini_program/util/HttpUtil.java new file mode 100644 index 0000000..2562fd0 --- /dev/null +++ b/src/main/java/com/example/mini_program/util/HttpUtil.java @@ -0,0 +1,72 @@ +package com.example.mini_program.util; + +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +/** + * 基于 HttpURLConnection 的轻量 HTTP 工具类 + * 替代 RestTemplate,避免 JDK 默认 Expect:100-continue 导致微信 API 返回 412 + */ +@Slf4j +public class HttpUtil { + + private static final int CONNECT_TIMEOUT = 10_000; + private static final int READ_TIMEOUT = 30_000; + + public static String get(String urlStr) throws Exception { + HttpURLConnection conn = null; + try { + URL url = new URL(urlStr); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(CONNECT_TIMEOUT); + conn.setReadTimeout(READ_TIMEOUT); + conn.setRequestProperty("Accept", "application/json"); + return readResponse(conn); + } finally { + if (conn != null) conn.disconnect(); + } + } + + public static String postJson(String urlStr, String jsonBody) throws Exception { + HttpURLConnection conn = null; + try { + URL url = new URL(urlStr); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setConnectTimeout(CONNECT_TIMEOUT); + conn.setReadTimeout(READ_TIMEOUT); + conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); + conn.setRequestProperty("Expect", ""); // 禁用 100-continue + try (OutputStream os = conn.getOutputStream()) { + os.write(jsonBody.getBytes(StandardCharsets.UTF_8)); + os.flush(); + } + return readResponse(conn); + } finally { + if (conn != null) conn.disconnect(); + } + } + + private static String readResponse(HttpURLConnection conn) throws Exception { + int code = conn.getResponseCode(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + code < 300 ? conn.getInputStream() : conn.getErrorStream(), StandardCharsets.UTF_8))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + String body = sb.toString(); + log.debug("HTTP {} code={}", conn.getURL().getPath(), code); + return body; + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a4c56dd..c6ae634 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,6 @@ - server: port: 8080 + spring: application: name: mini_program @@ -13,20 +13,15 @@ spring: # 微信小程序配置 wx: miniapp: - # 小程序AppID appid: wx50fe0c5c28dd3060 - # 小程序AppSecret secret: e82fa407fad13a9df35503f2d176e5a4 - corp: # 企业ID - corpid: ww257614cff8a1b61b - # 应用Secret - corpsecret: 2B0TAefYVewqjVHprLdGJQ8fNHz1drJq6235xN-mqNI - # 审批模板ID - approval-template-id: C4ej9uEntM19iNJbrtJsUqZakPFfjBNTPNLSKPno2 - # 审批回调URL(可选) - callback-url: http://your-domain.com/api/wx-corp/approval-callback - # 审批申请人用户ID(提交审批的企微用户) - creator-userid: i + subscribe-template-id: Csf_dJU7DhvVFt_03sphPPBCGlnmcWQSPhgqfxHZ5RQ + corp: + corpid: ww257614cff8a1b61b + corpsecret: 2B0TAefYVewqjVHprLdGJQ8fNHz1drJq6235xN-mqNI + approval-template-id: C4ej9uEntM19iNJbrtJsUqZakPFfjBNTPNLSKPno2 + callback-url: http://your-domain.com/api/wx-corp/approval-callback + creator-userid: i # MyBatis配置 mybatis: @@ -34,5 +29,3 @@ mybatis: type-aliases-package: com.example.mini_program.entity configuration: map-underscore-to-camel-case: true - -# 企业微信配置 \ No newline at end of file diff --git a/src/main/resources/data/data.sql b/src/main/resources/data/data.sql index e69de29..56fc984 100644 --- a/src/main/resources/data/data.sql +++ b/src/main/resources/data/data.sql @@ -0,0 +1,26 @@ +-- -------------------------------------------------------- +-- 访客预约系统数据库结构 + +CREATE DATABASE IF NOT EXISTS `mini` DEFAULT CHARACTER SET utf8; +USE `mini`; + +CREATE TABLE IF NOT EXISTS `visit_application` ( + `id` varchar(32) NOT NULL COMMENT '主键ID', + `name` varchar(50) NOT NULL COMMENT '访客姓名', + `phone` varchar(20) NOT NULL COMMENT '联系电话', + `company` varchar(100) DEFAULT NULL COMMENT '公司名称', + `reason` varchar(500) DEFAULT NULL COMMENT '来访原因', + `visit_date` date NOT NULL COMMENT '来访日期', + `visit_time` time DEFAULT '00:00:00' COMMENT '来访时间', + `host_name` varchar(50) DEFAULT NULL COMMENT '接待人姓名', + `area` varchar(50) DEFAULT NULL COMMENT '访问区域', + `status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT '状态', + `status_text` varchar(50) DEFAULT NULL COMMENT '状态文本描述', + `openid` varchar(64) DEFAULT NULL COMMENT '微信用户openid', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `sp_no` varchar(64) DEFAULT NULL COMMENT '企业微信审批单号', + PRIMARY KEY (`id`), + KEY `idx_status` (`status`), + KEY `idx_visit_date` (`visit_date`), + KEY `idx_openid` (`openid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='访问申请记录表'; diff --git a/src/main/resources/data/db.sql b/src/main/resources/data/db.sql index e69de29..d5e833c 100644 --- a/src/main/resources/data/db.sql +++ b/src/main/resources/data/db.sql @@ -0,0 +1,5 @@ +ALTER TABLE `visit_application` ADD COLUMN `notify_code` varchar(128) DEFAULT NULL COMMENT '订阅消息动态更新令牌' AFTER `sp_no`; + + + +