This commit is contained in:
chenglijuan
2026-04-22 00:43:58 +08:00
parent c0e75c0ee7
commit 254a317ff9
9 changed files with 752 additions and 0 deletions
@@ -0,0 +1,18 @@
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);
}
}
@@ -0,0 +1,31 @@
package com.example.mini_program.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "wx.corp")
public class WxCorpConfig {
/**
* 企业ID
*/
private String corpid;
/**
* 应用Secret
*/
private String corpsecret;
/**
* 审批模板ID
*/
private String approvalTemplateId;
/**
* 审批回调URL(可选)
*/
private String callbackUrl;
}
@@ -0,0 +1,290 @@
package com.example.mini_program.service;
import com.example.mini_program.config.WxCorpConfig;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
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 =
"https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent?access_token=%s";
private static final String GET_APPROVAL_DETAIL_URL =
"https://qyapi.weixin.qq.com/cgi-bin/oa/getapprovaldetail?access_token=%s";
private final WxCorpConfig wxCorpConfig;
private final RestTemplate restTemplate;
/**
* 获取企业微信AccessToken
*/
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);
}
} catch (Exception e) {
log.error("调用企业微信接口失败", e);
throw new RuntimeException("获取access_token异常: " + e.getMessage());
}
}
/**
* 提交审批申请
*
* @param approverUserId 审批人用户ID
* @param visitorName 访客姓名
* @param visitorPhone 访客电话
* @param visitorCompany 访客公司(可选)
* @param visitPurpose 来访事由
* @param visitTime 日期+时间
* @param visiteeName 被访人(可选)
* @param visitArea 拜访区域(可选)
* @param applicantUserId 申请人用户ID(可选,用于追踪)
* @return 审批单号
*/
public String submitApproval(String approverUserId, String visitorName, String visitorPhone,
String visitorCompany, String visitPurpose, String visitTime,
String visiteeName, String visitArea,
String applicantUserId) {
String accessToken = getAccessToken();
String url = String.format(SUBMIT_APPROVAL_URL, accessToken);
// 构建审批表单数据(apply_data)
Map<String, Object> applyData = buildApplyData(visitorName, visitorPhone, visitorCompany,
visitPurpose, visitTime, visiteeName, visitArea);
// 构建审批流程(use_template_approver=0 时必填)
Map<String, Object> process = buildProcess(approverUserId);
// 构建摘要信息
List<Map<String, Object>> summaryList = buildSummaryList(visitorName, visitPurpose);
// 构建完整的审批请求体
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("creator_userid", applicantUserId != null ? applicantUserId : approverUserId);
requestBody.put("template_id", wxCorpConfig.getApprovalTemplateId());
requestBody.put("use_template_approver", 0);
requestBody.put("apply_data", applyData);
requestBody.put("process", process);
requestBody.put("summary_list", summaryList);
log.info("提交审批申请, visitorName: {}, visitorPhone: {}", visitorName, visitorPhone);
log.debug("审批请求体: {}", requestBody);
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);
return spNo;
} else {
log.error("审批提交失败: {}", response);
throw new RuntimeException("审批提交失败: " + response);
}
} catch (Exception e) {
log.error("提交审批申请异常", e);
throw new RuntimeException("提交审批申请异常: " + e.getMessage());
}
}
/**
* 构建审批表单数据(apply_data
* 控件ID来源于"获取审批模板详情"接口,需与审批模板中的控件ID一致
*
* 模板控件对应关系:
* Text-1776786661954 → 姓名
* Text-1776786666351 → 手机号
* Text-1776786668098 → 公司
* Text-1776786672408 → 来访事由
* Date-1776786680089 → 日期+时间
* Text-1776786690968 → 被访人
* Text-1776786692400 → 拜访区域
*/
private Map<String, Object> buildApplyData(String visitorName, String visitorPhone,
String visitorCompany, String visitPurpose,
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));
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 : "")
);
}
/**
* 构建日期/日期+时间控件数据
* 对应 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;
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();
} catch (Exception e) {
log.warn("日期时间解析失败: {}", dateTime, e);
}
}
return Map.of(
"control", "Date",
"id", id,
"value", Map.of(
"date", Map.of(
"type", "hour",
"s_timestamp", String.valueOf(timestamp)
)
)
);
}
/**
* 构建审批流程
* use_template_approver=0 时必填
* node_list: 流程节点列表
* type: 1-审批人 2-抄送人 3-办理人
* apv_rel: 多人审批方式 1-会签 2-或签 3-依次审批
* userid: 用户ID列表
*
* @param approverUserId 审批人用户ID
*/
private Map<String, Object> buildProcess(String approverUserId) {
List<Map<String, Object>> nodeList = List.of(
Map.of(
"type", 1,
"apv_rel", 1,
"userid", List.of(approverUserId)
)
);
return Map.of("node_list", nodeList);
}
/**
* 构建摘要信息,显示在审批通知卡片和审批列表中,最多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("summary_info", List.of(
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) {
if (spStatus == null) return "未知";
return switch (spStatus) {
case 1 -> "审批中";
case 2 -> "已通过";
case 3 -> "已拒绝";
case 4 -> "已撤销";
default -> "未知状态";
};
}
/**
* 审批状态结果类
*/
@Data
public static class ApprovalStatus {
private String spNo;
private Integer spStatus;
private String spStatusText;
}
}