feat: add subscribe message push and refactor HTTP layer

This commit is contained in:
ws
2026-04-22 17:49:44 +08:00
parent 25d7bc9b55
commit 4ac8fa20cb
8 changed files with 346 additions and 283 deletions
@@ -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);
}
}
@@ -10,6 +10,10 @@ import org.springframework.stereotype.Service;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
/**
* 访客预约服务
* 创建预约 → 提交企微审批 → 推送订阅消息通知
*/
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -17,118 +21,91 @@ public class AppointmentService {
private final VisitApplicationMapper visitApplicationMapper; private final VisitApplicationMapper visitApplicationMapper;
private final WxApprovalService wxApprovalService; private final WxApprovalService wxApprovalService;
private final WxSubscribeMessageService wxSubscribeMessageService;
@Value("${wx.corp.creator-userid:}") @Value("${wx.corp.creator-userid:}")
private String creatorUserId; private String creatorUserId;
/** /** 根据openid获取最新一条预约 */
* 根据openid获取最新的一条预约记录
*/
public VisitApplication getLatest(String openid) { public VisitApplication getLatest(String openid) {
log.info("查询用户最新预约记录, openid: {}", openid); return visitApplicationMapper.selectLatestByOpenid(openid);
VisitApplication result = visitApplicationMapper.selectLatestByOpenid(openid);
if (result != null) {
log.info("找到预约记录, id: {}", result.getId());
} else {
log.info("未找到预约记录");
}
return result;
} }
/** /** 获取用户所有预约记录(按创建时间倒序) */
* 获取用户所有预约记录(按创建时间倒序)
*/
public List<VisitApplication> getList(String openid) { public List<VisitApplication> getList(String openid) {
log.info("查询用户预约列表, openid: {}", openid); return visitApplicationMapper.selectListByOpenid(openid);
List<VisitApplication> list = visitApplicationMapper.selectListByOpenid(openid);
log.info("查询到 {} 条预约记录", list.size());
return list;
} }
/** /** 创建预约记录 */
* 创建预约记录
*/
public VisitApplication create(VisitApplication record) { public VisitApplication create(VisitApplication record) {
record.setId(UUID.randomUUID().toString().replace("-", "")); record.setId(UUID.randomUUID().toString().replace("-", ""));
record.setStatus("pending"); record.setStatus("pending");
record.setStatusText("待审核"); record.setStatusText("待审核");
// 发起企业微信审批 // 提交企业微信审批
try { try {
String visitTime = record.getVisitDate();
if (record.getVisitTime() != null && !record.getVisitTime().isEmpty()) {
visitTime = record.getVisitDate() + " " + record.getVisitTime();
}
String spNo = wxApprovalService.submitApproval( String spNo = wxApprovalService.submitApproval(
creatorUserId, creatorUserId, record.getName(), record.getPhone(), record.getCompany(),
record.getName(), record.getReason(), formatVisitTime(record), record.getHostName(), record.getArea());
record.getPhone(),
record.getCompany(),
record.getReason(),
visitTime,
record.getHostName(),
record.getArea()
);
record.setSpNo(spNo); record.setSpNo(spNo);
log.info("企业微信审批提交成功, spNo: {}", spNo);
} catch (Exception e) { } catch (Exception e) {
log.error("业微信审批提交失败,预约记录仍会保存", e); log.error("审批提交失败,预约仍会保存", e);
} }
visitApplicationMapper.insert(record); 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; return record;
} }
/** /** 取消预约(仅 pending 状态可取消) */
* 取消预约(仅pending状态可取消,需校验openid
*/
public boolean cancel(String id, String openid) { public boolean cancel(String id, String openid) {
log.info("取消预约, id: {}, openid: {}", id, openid);
VisitApplication existing = visitApplicationMapper.selectByIdAndOpenid(id, openid); VisitApplication existing = visitApplicationMapper.selectByIdAndOpenid(id, openid);
if (existing == null) { if (existing == null || !"pending".equals(existing.getStatus())) {
log.warn("预约记录不存在或不属于该用户, id: {}, openid: {}", id, openid);
return false; return false;
} }
if (!"pending".equals(existing.getStatus())) { return visitApplicationMapper.updateStatusToCancelled(id, openid) > 0;
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;
}
/**
* 审批预约(通过/拒绝)
*/
public boolean approve(String id, String status) { public boolean approve(String id, String status) {
log.info("审批预约, id: {}, status: {}", id, status);
VisitApplication existing = visitApplicationMapper.selectById(id); VisitApplication existing = visitApplicationMapper.selectById(id);
if (existing == null) { if (existing == null || !"pending".equals(existing.getStatus())) {
log.warn("预约记录不存在, id: {}", id);
return false;
}
if (!"pending".equals(existing.getStatus())) {
log.warn("预约状态不允许审批, id: {}, currentStatus: {}", id, existing.getStatus());
return false; return false;
} }
String statusText = "approved".equals(status) ? "已通过" : "已拒绝"; String statusText = "approved".equals(status) ? "已通过" : "已拒绝";
int rows = visitApplicationMapper.updateStatus(id, status, statusText); if (visitApplicationMapper.updateStatus(id, status, statusText) <= 0) {
if (rows <= 0) {
log.warn("审批更新失败, id: {}", id);
return false; 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; 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;
}
} }
@@ -1,17 +1,25 @@
package com.example.mini_program.service; package com.example.mini_program.service;
import com.example.mini_program.config.WxCorpConfig; 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.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; 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.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* 企业微信审批服务
* 调用企微 /cgi-bin/oa/applyevent 提交审批,/cgi-bin/oa/getapprovaldetail 查询审批状态
*/
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -19,228 +27,125 @@ public class WxApprovalService {
private static final String GET_TOKEN_URL = private static final String GET_TOKEN_URL =
"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"; "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"; "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"; "https://qyapi.weixin.qq.com/cgi-bin/oa/getapprovaldetail?access_token=%s";
private final WxCorpConfig wxCorpConfig; private final WxCorpConfig wxCorpConfig;
private final RestTemplate restTemplate; private final ObjectMapper objectMapper;
/** /** 获取企业微信 access_token */
* 获取企业微信AccessToken
*/
public String getAccessToken() { public String getAccessToken() {
String url = String.format(GET_TOKEN_URL, wxCorpConfig.getCorpid(), wxCorpConfig.getCorpsecret()); String url = String.format(GET_TOKEN_URL, wxCorpConfig.getCorpid(), wxCorpConfig.getCorpsecret());
log.info("获取企业微信access_token, corpid: {}", wxCorpConfig.getCorpid());
try { try {
Map<String, Object> response = restTemplate.getForObject(url, Map.class); Map<String, Object> resp = objectMapper.readValue(HttpUtil.get(url), Map.class);
if (response != null && "0".equals(String.valueOf(response.get("errcode")))) { if ("0".equals(String.valueOf(resp.get("errcode")))) {
String accessToken = (String) response.get("access_token"); return (String) resp.get("access_token");
log.info("获取access_token成功: {}", accessToken);
return accessToken;
} else {
log.error("获取access_token失败: {}", response);
throw new RuntimeException("获取access_token失败: " + response);
} }
throw new RuntimeException("获取access_token失败: " + resp);
} catch (Exception e) { } catch (Exception e) {
log.error("调用企业微信接口失败", e); throw new RuntimeException("获取access_token异常: " + e.getMessage(), e);
throw new RuntimeException("获取access_token异常: " + e.getMessage());
} }
} }
/** /** 提交审批申请 */
* 提交审批申请(使用模板中配置的审批流程)
*
* @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, public String submitApproval(String creatorUserId, String visitorName, String visitorPhone,
String visitorCompany, String visitPurpose, String visitTime, String visitorCompany, String visitPurpose, String visitTime,
String visiteeName, String visitArea) { String visiteeName, String visitArea) {
String accessToken = getAccessToken(); String url = String.format(SUBMIT_URL, getAccessToken());
String url = String.format(SUBMIT_APPROVAL_URL, accessToken);
// 构建审批表单数据(apply_data) Map<String, Object> body = new HashMap<>();
Map<String, Object> applyData = buildApplyData(visitorName, visitorPhone, visitorCompany, body.put("creator_userid", creatorUserId);
visitPurpose, visitTime, visiteeName, visitArea); body.put("template_id", wxCorpConfig.getApprovalTemplateId());
body.put("use_template_approver", 1);
// 构建摘要信息 body.put("apply_data", buildApplyData(visitorName, visitorPhone, visitorCompany,
List<Map<String, Object>> summaryList = buildSummaryList(visitorName, visitPurpose); visitPurpose, visitTime, visiteeName, visitArea));
body.put("summary_list", buildSummaryList(visitorName, visitPurpose));
// 构建完整的审批请求体(use_template_approver=1 使用模板中配置的审批流程)
Map<String, Object> 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);
try { try {
Map<String, Object> response = restTemplate.postForObject(url, requestBody, Map.class); String json = objectMapper.writeValueAsString(body);
if (response != null && "0".equals(String.valueOf(response.get("errcode")))) { Map<String, Object> resp = objectMapper.readValue(HttpUtil.postJson(url, json), Map.class);
String spNo = (String) response.get("sp_no"); if ("0".equals(String.valueOf(resp.get("errcode")))) {
log.info("审批提交成功, spNo: {}", spNo); String spNo = (String) resp.get("sp_no");
log.info("审批提交成功, spNo={}", spNo);
return spNo; return spNo;
} else {
log.error("审批提交失败: {}", response);
throw new RuntimeException("审批提交失败: " + response);
} }
throw new RuntimeException("审批提交失败: " + resp);
} catch (Exception e) { } catch (Exception e) {
log.error("提交审批申请异常", e); throw new RuntimeException("提交审批申请异常: " + e.getMessage(), e);
throw new RuntimeException("提交审批申请异常: " + e.getMessage());
} }
} }
/** /** 查询审批状态 */
* 构建审批表单数据(apply_data public ApprovalStatus getApprovalStatus(String spNo) {
* 控件ID来源于"获取审批模板详情"接口,需与审批模板中的控件ID一致 String url = String.format(DETAIL_URL, getAccessToken());
* try {
* 模板控件对应关系: String json = objectMapper.writeValueAsString(Map.of("sp_no", spNo));
* Text-1776786661954 → 姓名 Map<String, Object> resp = objectMapper.readValue(HttpUtil.postJson(url, json), Map.class);
* Text-1776786666351 → 手机号 if ("0".equals(String.valueOf(resp.get("errcode")))) {
* Text-1776786668098 → 公司 @SuppressWarnings("unchecked")
* Text-1776786672408 → 来访事由 Map<String, Object> info = (Map<String, Object>) resp.get("info");
* Date-1776786680089 → 日期+时间 ApprovalStatus status = new ApprovalStatus();
* Text-1776786690968 → 被访人 if (info != null) {
* Text-1776786692400 → 拜访区域 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<String, Object> buildApplyData(String visitorName, String visitorPhone, private Map<String, Object> buildApplyData(String visitorName, String visitorPhone,
String visitorCompany, String visitPurpose, String visitorCompany, String visitPurpose,
String visitTime, String visiteeName, String visitTime, String visiteeName, String visitArea) {
String visitArea) {
List<Map<String, Object>> contents = new ArrayList<>(); List<Map<String, Object>> contents = new ArrayList<>();
contents.add(buildTextControl("Text-1776786661954", visitorName)); contents.add(textControl("Text-1776786661954", visitorName));
contents.add(buildTextControl("Text-1776786666351", visitorPhone)); contents.add(textControl("Text-1776786666351", visitorPhone));
contents.add(buildTextControl("Text-1776786668098", visitorCompany)); contents.add(textControl("Text-1776786668098", visitorCompany));
contents.add(buildTextControl("Text-1776786672408", visitPurpose)); contents.add(textControl("Text-1776786672408", visitPurpose));
contents.add(buildDateControl("Date-1776786680089", visitTime)); contents.add(dateControl("Date-1776786680089", visitTime));
contents.add(buildTextControl("Text-1776786690968", visiteeName)); contents.add(textControl("Text-1776786690968", visiteeName));
contents.add(buildTextControl("Text-1776786692400", visitArea)); contents.add(textControl("Text-1776786692400", visitArea));
return Map.of("contents", contents); return Map.of("contents", contents);
} }
/** private Map<String, Object> textControl(String id, String value) {
* 构建文本控件数据 return Map.of("control", "Text", "id", id,
* 对应 control 参数为 Text 或 Textarea "value", Map.of("text", value != null ? value : ""));
*
* @param id 控件ID(需与审批模板中的控件ID一致)
* @param value 文本内容
*/
private Map<String, Object> buildTextControl(String id, String value) {
return Map.of(
"control", "Text",
"id", id,
"value", Map.of("text", value != null ? value : "")
);
} }
/** private Map<String, Object> dateControl(String id, String dateTime) {
* 构建日期/日期+时间控件数据
* 对应 control 参数为 Date
*
* @param id 控件ID(需与审批模板中的控件ID一致)
* @param dateTime 日期时间字符串,格式:yyyy-MM-dd HH:mm
*/
private Map<String, Object> buildDateControl(String id, String dateTime) {
long timestamp = 0; long timestamp = 0;
if (dateTime != null && !dateTime.isEmpty()) { if (dateTime != null && !dateTime.isEmpty()) {
try { try {
java.time.LocalDateTime ldt = java.time.LocalDateTime.parse(dateTime, timestamp = LocalDateTime.parse(dateTime,
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
timestamp = ldt.atZone(java.time.ZoneId.of("Asia/Shanghai")) .atZone(ZoneId.of("Asia/Shanghai")).toInstant().getEpochSecond();
.toInstant().getEpochSecond();
} catch (Exception e) { } catch (Exception e) {
log.warn("日期时间解析失败: {}", dateTime, e); log.warn("日期解析失败: {}", dateTime);
} }
} }
return Map.of( return Map.of("control", "Date", "id", id,
"control", "Date", "value", Map.of("date", Map.of("type", "hour", "s_timestamp", String.valueOf(timestamp))));
"id", id,
"value", Map.of(
"date", Map.of(
"type", "hour",
"s_timestamp", String.valueOf(timestamp)
)
)
);
} }
/**
* 构建摘要信息,显示在审批通知卡片和审批列表中,最多3行
*/
private List<Map<String, Object>> buildSummaryList(String visitorName, String visitPurpose) { private List<Map<String, Object>> buildSummaryList(String visitorName, String visitPurpose) {
return List.of( return List.of(
Map.of("summary_info", 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("summary_info", List.of(
Map.of("text", "目的: " + (visitPurpose != null ? visitPurpose : ""), "lang", "zh_CN") Map.of("text", "目的: " + (visitPurpose != null ? visitPurpose : ""), "lang", "zh_CN")))
))
); );
} }
/** private static String toStatusText(Integer spStatus) {
* 获取审批状态
*
* @param spNo 审批单号
* @return 审批状态信息
*/
public ApprovalStatus getApprovalStatus(String spNo) {
String accessToken = getAccessToken();
String url = String.format(GET_APPROVAL_DETAIL_URL, accessToken);
Map<String, Object> requestBody = Map.of("sp_no", spNo);
log.info("查询审批状态, spNo: {}", spNo);
try {
Map<String, Object> response = restTemplate.postForObject(url, requestBody, Map.class);
if (response != null && "0".equals(String.valueOf(response.get("errcode")))) {
@SuppressWarnings("unchecked")
Map<String, Object> info = (Map<String, Object>) 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<String, Object> 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) {
if (spStatus == null) return "未知"; if (spStatus == null) return "未知";
return switch (spStatus) { return switch (spStatus) {
case 1 -> "审批中"; case 1 -> "审批中";
@@ -251,9 +156,6 @@ public class WxApprovalService {
}; };
} }
/**
* 审批状态结果类
*/
@Data @Data
public static class ApprovalStatus { public static class ApprovalStatus {
private String spNo; private String spNo;
@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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;
}
}
@@ -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;
}
}
}
+3 -10
View File
@@ -1,6 +1,6 @@
server: server:
port: 8080 port: 8080
spring: spring:
application: application:
name: mini_program name: mini_program
@@ -13,19 +13,14 @@ spring:
# 微信小程序配置 # 微信小程序配置
wx: wx:
miniapp: miniapp:
# 小程序AppID
appid: wx50fe0c5c28dd3060 appid: wx50fe0c5c28dd3060
# 小程序AppSecret
secret: e82fa407fad13a9df35503f2d176e5a4 secret: e82fa407fad13a9df35503f2d176e5a4
corp: # 企业ID subscribe-template-id: Csf_dJU7DhvVFt_03sphPPBCGlnmcWQSPhgqfxHZ5RQ
corp:
corpid: ww257614cff8a1b61b corpid: ww257614cff8a1b61b
# 应用Secret
corpsecret: 2B0TAefYVewqjVHprLdGJQ8fNHz1drJq6235xN-mqNI corpsecret: 2B0TAefYVewqjVHprLdGJQ8fNHz1drJq6235xN-mqNI
# 审批模板ID
approval-template-id: C4ej9uEntM19iNJbrtJsUqZakPFfjBNTPNLSKPno2 approval-template-id: C4ej9uEntM19iNJbrtJsUqZakPFfjBNTPNLSKPno2
# 审批回调URL(可选)
callback-url: http://your-domain.com/api/wx-corp/approval-callback callback-url: http://your-domain.com/api/wx-corp/approval-callback
# 审批申请人用户ID(提交审批的企微用户)
creator-userid: i creator-userid: i
# MyBatis配置 # MyBatis配置
@@ -34,5 +29,3 @@ mybatis:
type-aliases-package: com.example.mini_program.entity type-aliases-package: com.example.mini_program.entity
configuration: configuration:
map-underscore-to-camel-case: true map-underscore-to-camel-case: true
# 企业微信配置
+26
View File
@@ -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='访问申请记录表';
+5
View File
@@ -0,0 +1,5 @@
ALTER TABLE `visit_application` ADD COLUMN `notify_code` varchar(128) DEFAULT NULL COMMENT '订阅消息动态更新令牌' AFTER `sp_no`;