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.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<VisitApplication> getList(String openid) {
log.info("查询用户预约列表, openid: {}", openid);
List<VisitApplication> 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;
return visitApplicationMapper.updateStatusToCancelled(id, openid) > 0;
}
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) {
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;
}
}
@@ -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<String, Object> 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<String, Object> 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<String, Object> applyData = buildApplyData(visitorName, visitorPhone, visitorCompany,
visitPurpose, visitTime, visiteeName, visitArea);
// 构建摘要信息
List<Map<String, Object>> summaryList = 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);
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> resp = objectMapper.readValue(HttpUtil.postJson(url, json), Map.class);
if ("0".equals(String.valueOf(resp.get("errcode")))) {
@SuppressWarnings("unchecked")
Map<String, Object> info = (Map<String, Object>) 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<String, Object> buildApplyData(String visitorName, String visitorPhone,
String visitorCompany, String visitPurpose,
String visitTime, String visiteeName,
String visitArea) {
String visitTime, String visiteeName, String visitArea) {
List<Map<String, Object>> 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<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> 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<String, Object> buildDateControl(String id, String dateTime) {
private Map<String, Object> 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<Map<String, Object>> 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<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) {
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;
@@ -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:
port: 8080
spring:
application:
name: mini_program
@@ -13,19 +13,14 @@ spring:
# 微信小程序配置
wx:
miniapp:
# 小程序AppID
appid: wx50fe0c5c28dd3060
# 小程序AppSecret
secret: e82fa407fad13a9df35503f2d176e5a4
corp: # 企业ID
subscribe-template-id: Csf_dJU7DhvVFt_03sphPPBCGlnmcWQSPhgqfxHZ5RQ
corp:
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
# MyBatis配置
@@ -34,5 +29,3 @@ mybatis:
type-aliases-package: com.example.mini_program.entity
configuration:
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`;