feat: add subscribe message push and refactor HTTP layer
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
# 企业微信配置
|
||||
@@ -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='访问申请记录表';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE `visit_application` ADD COLUMN `notify_code` varchar(128) DEFAULT NULL COMMENT '订阅消息动态更新令牌' AFTER `sp_no`;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user