福利发消息整改

This commit is contained in:
2026-09-02 15:16:29 +08:00
parent 4ce900ee6a
commit 32b288053a
14 changed files with 742 additions and 198 deletions
@@ -1,6 +1,7 @@
package com.budwk.app.base.sms; package com.budwk.app.base.sms;
import com.budwk.app.sys.models.Sys_user; import com.budwk.app.sys.models.Sys_user;
import org.nutz.lang.util.NutMap;
import java.util.List; import java.util.List;
@@ -47,6 +48,15 @@ public interface SmsService {
* @param content 消息正文 * @param content 消息正文
* @param pcUrl PC端跳转链接 * @param pcUrl PC端跳转链接
* @param mobileUrl 手机端跳转链接 * @param mobileUrl 手机端跳转链接
* @return 统一消息平台返回的消息标识集合;每个标识可用于查询该批消息的最终发送结果
*/ */
void massSendByUsers(List<Sys_user> users, String title, String content, String pcUrl, String mobileUrl); List<String> massSendByUsers(List<Sys_user> users, String title, String content, String pcUrl, String mobileUrl);
/**
* 查询学校统一消息平台的批量发送结果。
*
* @param messageId 批量发送接口返回的消息标识;同一批次的接收人使用该标识查询最终送达结果
* @return 查询结果,包含消息总数、成功数、失败数及失败接收人数组;调用失败时抛出业务异常
*/
NutMap getMessageResult(String messageId);
} }
@@ -14,6 +14,7 @@ import org.nutz.json.Json;
import org.nutz.lang.Lang; import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -66,12 +67,18 @@ public class SmsYpiServiceImpl implements SmsService {
* 兼容统一消息V4新增的批量发送接口;旧版实现不再注册为发送服务,仅保留历史调用能力。 * 兼容统一消息V4新增的批量发送接口;旧版实现不再注册为发送服务,仅保留历史调用能力。
*/ */
@Override @Override
public void massSendByUsers(List<Sys_user> users, String title, String content, String pcUrl, String mobileUrl) { public List<String> massSendByUsers(List<Sys_user> users, String title, String content, String pcUrl, String mobileUrl) {
List<String> loginNames = users.stream() List<String> loginNames = users.stream()
.map(Sys_user::getLoginname) .map(Sys_user::getLoginname)
.filter(StrUtil::isNotBlank) .filter(StrUtil::isNotBlank)
.toList(); .toList();
massSend(loginNames, title, content, StrUtil.blankToDefault(mobileUrl, pcUrl)); massSend(loginNames, title, content, StrUtil.blankToDefault(mobileUrl, pcUrl));
return Collections.emptyList();
}
@Override
public NutMap getMessageResult(String messageId) {
throw new UnsupportedOperationException("旧版学校消息网关不支持统一消息V4发送结果查询");
} }
/** /**
@@ -13,6 +13,7 @@ import org.nutz.integration.jedis.RedisService;
import org.nutz.ioc.impl.PropertiesProxy; import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import java.util.List; import java.util.List;
@@ -63,9 +64,9 @@ public class UnifiedMessageV4ServiceImpl implements SmsService {
} }
@Override @Override
public void massSendByUsers(List<Sys_user> users, String title, String content, String pcUrl, String mobileUrl) { public List<String> massSendByUsers(List<Sys_user> users, String title, String content, String pcUrl, String mobileUrl) {
if (users == null || users.isEmpty()) { if (users == null || users.isEmpty()) {
return; return List.of();
} }
List<Receiver> receivers = users.stream() List<Receiver> receivers = users.stream()
.filter(user -> StrUtil.isNotBlank(user.getLoginname()) && StrUtil.isNotBlank(user.getUsername())) .filter(user -> StrUtil.isNotBlank(user.getLoginname()) && StrUtil.isNotBlank(user.getUsername()))
@@ -74,13 +75,14 @@ public class UnifiedMessageV4ServiceImpl implements SmsService {
if (receivers.isEmpty()) { if (receivers.isEmpty()) {
throw new BaseException("学校统一消息发送失败:接收人工号或姓名不能为空"); throw new BaseException("学校统一消息发送失败:接收人工号或姓名不能为空");
} }
sendMessage(title, content, createReceivers(receivers), pcUrl, mobileUrl); return sendMessage(title, content, createReceivers(receivers), pcUrl, mobileUrl);
} }
/** /**
* 调用统一消息V4普通消息接口;有链接时按外链模式同时传递PC和手机端地址。 * 调用统一消息V4普通消息接口;有链接时按外链模式同时传递PC和手机端地址。
* 返回值为平台入队后生成的消息标识集合,供业务侧随后查询最终逐人发送结果。
*/ */
private void sendMessage(String title, String content, JSONArray receivers, String pcUrl, String mobileUrl) { private List<String> sendMessage(String title, String content, JSONArray receivers, String pcUrl, String mobileUrl) {
String token = getAccessToken(); String token = getAccessToken();
boolean hasLink = StrUtil.isNotBlank(pcUrl) || StrUtil.isNotBlank(mobileUrl); boolean hasLink = StrUtil.isNotBlank(pcUrl) || StrUtil.isNotBlank(mobileUrl);
JSONObject body = JSONUtil.createObj(); JSONObject body = JSONUtil.createObj();
@@ -108,7 +110,20 @@ public class UnifiedMessageV4ServiceImpl implements SmsService {
if (response.getInt("state", 0) != 200) { if (response.getInt("state", 0) != 200) {
throw new BaseException("学校统一消息发送失败:{}", response.getStr("message", responseBody)); throw new BaseException("学校统一消息发送失败:{}", response.getStr("message", responseBody));
} }
log.info("学校统一消息发送成功,接收人数量={},response={}", receivers.size(), responseBody); JSONArray messageIdArray = response.getJSONArray("data");
if (messageIdArray == null || messageIdArray.isEmpty()) {
throw new BaseException("学校统一消息发送失败:平台未返回消息标识");
}
List<String> messageIds = messageIdArray.stream()
.map(String::valueOf)
.filter(StrUtil::isNotBlank)
.toList();
if (messageIds.isEmpty()) {
throw new BaseException("学校统一消息发送失败:平台返回的消息标识为空");
}
log.info("学校统一消息发送已入队,接收人数量={}messageIds={}response={}",
receivers.size(), messageIds, responseBody);
return messageIds;
} catch (BaseException e) { } catch (BaseException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
@@ -117,6 +132,47 @@ public class UnifiedMessageV4ServiceImpl implements SmsService {
} }
} }
/**
* 调用统一消息V4结果查询接口。
* 参数为批量发送返回的messageId;返回NutMap中的messageTotal、successCount、failCount、resultState和failedUsers
* 分别表示平台统计的总人数、成功人数、失败人数、结果状态和失败用户明细。
*/
@Override
public NutMap getMessageResult(String messageId) {
if (StrUtil.isBlank(messageId)) {
throw new BaseException("学校统一消息结果查询失败:消息标识不能为空");
}
String resultUrl = getRequiredConfig("school-message.result-url").replace("{messageId}", messageId);
try {
HttpRequest request = HttpRequest.post(resultUrl)
.header("domain-name", getRequiredConfig("school-message.domain-name"))
.header("token", getAccessToken());
addInterceptDomainHeader(request);
String responseBody = request.execute().body();
JSONObject response = JSONUtil.parseObj(responseBody);
if (response.getInt("state", 0) != 200 || response.getJSONObject("data") == null) {
throw new BaseException("学校统一消息结果查询失败:{}", response.getStr("message", responseBody));
}
JSONObject data = response.getJSONObject("data");
NutMap result = NutMap.NEW()
.addv("messageId", messageId)
.addv("messageTotal", data.getInt("msgTotal", 0))
.addv("resultState", data.getStr("state"))
.addv("successCount", data.getInt("successCount", 0))
.addv("failCount", data.getInt("failCount", 0))
.addv("failedUsers", data.getJSONArray("user"));
log.info("学校统一消息结果查询成功,messageId={}messageTotal={}successCount={}failCount={}resultState={}",
messageId, result.getInt("messageTotal", 0), result.getInt("successCount", 0),
result.getInt("failCount", 0), result.getString("resultState"));
return result;
} catch (BaseException e) {
throw e;
} catch (Exception e) {
log.error("学校统一消息结果查询异常,url={}messageId={}", resultUrl, messageId, e);
throw new BaseException("学校统一消息结果查询异常:{}", e.getMessage());
}
}
/** /**
* 获取学校统一消息V4令牌;文档有效期为300秒,缓存270秒避免临界过期。 * 获取学校统一消息V4令牌;文档有效期为300秒,缓存270秒避免临界过期。
*/ */
@@ -0,0 +1,24 @@
package com.budwk.app.flow.param;
import lombok.Data;
import java.util.List;
/**
* 学校OA批量待办创建参数。
* 公共流程字段在本对象中传递,接收人差异字段放在userList中,对应学校OA的/handle/batch/insert接口。
*/
@Data
public class SchoolOaTodoBatchCreateParam {
private String flowTypeName;
private String flowId;
private String title;
private String stepName;
private String creatorCode;
private String creatorName;
private String creatorTime;
private String startPcPreviewUrl;
private String startAppPreviewUrl;
private List<SchoolOaTodoReceiverParam> userList;
}
@@ -0,0 +1,17 @@
package com.budwk.app.flow.param;
import lombok.Data;
/**
* 学校OA批量待办中的单个接收人参数,对应批量接口userList数组的一个元素。
*/
@Data
public class SchoolOaTodoReceiverParam {
private String pcUrl;
private String appUrl;
private String uniqueId;
private String receiveCode;
private String receiveName;
private String receiveTime;
}
@@ -1,6 +1,7 @@
package com.budwk.app.flow.service; package com.budwk.app.flow.service;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.param.SchoolOaTodoBatchCreateParam;
/** /**
* 学校 OA 待办同步服务。 * 学校 OA 待办同步服务。
@@ -43,30 +44,17 @@ public interface SchoolOaTodoService {
void updateFlowFinishedStepName(Long processInstanceId); void updateFlowFinishedStepName(Long processInstanceId);
/** /**
* 创建福利选择提醒待办。 * 创建学校OA批量待办。
* *
* @param projectId 福利项目ID,用于构造学校OA业务标识 * @param todoParam 批量待办公共字段及userList接收人数组
* @param title 待办标题,学校OA接口没有独立正文时用于展示提醒内容
* @param pcUrl PC端福利选择地址
* @param appUrl 手机端福利选择地址
* @param creatorId 待办创建人ID
* @param receiverId 待办接收人ID
* @return 学校OA待办唯一值
*/ */
String createWelfareReminderTodo(String projectId, String title, String pcUrl, String appUrl, String creatorId, String receiverId); void createBatchTodo(SchoolOaTodoBatchCreateParam todoParam);
/** /**
* 将指定的学校OA福利提醒待办改为已办。 * 根据待办唯一值将学校OA待办改为已办。
* *
* @param uniqueId 创建待办时记录的学校OA唯一值 * @param uniqueId 创建待办时传入的唯一标识
*/ */
void completeWelfareReminderTodo(String uniqueId); void completeTodoByUniqueId(String uniqueId);
/**
* 删除用户已完成福利选择对应的学校OA提醒待办。
*
* @param projectId 福利项目ID
* @param userId 完成选择的用户ID
*/
void deleteWelfareReminderTodo(String projectId, String userId);
} }
@@ -14,6 +14,8 @@ import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor; import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.ProcessTaskStateEnum; import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.param.SchoolOaTodoBatchCreateParam;
import com.budwk.app.flow.param.SchoolOaTodoReceiverParam;
import com.budwk.app.flow.service.SchoolOaTodoService; import com.budwk.app.flow.service.SchoolOaTodoService;
import com.budwk.app.sys.models.Sys_user; import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.web.commons.base.Globals; import com.budwk.app.web.commons.base.Globals;
@@ -27,6 +29,7 @@ import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import java.util.Date; import java.util.Date;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@@ -201,48 +204,63 @@ public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
} }
@Override @Override
public String createWelfareReminderTodo(String projectId, String title, String pcUrl, String appUrl, String creatorId, String receiverId) { @Aop(TransAop.READ_COMMITTED)
if (!isSchoolOaEnabled("创建福利选择提醒待办", null, null)) { public void createBatchTodo(SchoolOaTodoBatchCreateParam todoParam) {
return null; if (!isSchoolOaEnabled("创建批量待办", null, null)) {
throw new BaseException("学校OA待办开关状态已变化,本次待办未发送");
} }
if (StrUtil.hasBlank(projectId, title, pcUrl, appUrl, creatorId, receiverId)) { if (todoParam == null || StrUtil.hasBlank(todoParam.getFlowTypeName(), todoParam.getFlowId(), todoParam.getTitle(),
throw new BaseException("学校OA福利选择提醒待办参数不完整"); todoParam.getStepName(), todoParam.getCreatorCode(), todoParam.getCreatorName(), todoParam.getCreatorTime(),
todoParam.getStartPcPreviewUrl(), todoParam.getStartAppPreviewUrl())
|| todoParam.getUserList() == null || todoParam.getUserList().isEmpty()) {
throw new BaseException("学校OA批量待办创建参数不完整");
} }
Sys_user creator = dao.fetch(Sys_user.class, creatorId); List<JSONObject> userList = new ArrayList<>();
if (creator == null || StrUtil.hasBlank(creator.getLoginname(), creator.getUsername())) { for (SchoolOaTodoReceiverParam receiverParam : todoParam.getUserList()) {
throw new BaseException("学校OA福利选择提醒待办创建人工号或姓名为空,userId={}", creatorId); if (receiverParam == null || StrUtil.hasBlank(receiverParam.getPcUrl(), receiverParam.getAppUrl(),
receiverParam.getUniqueId(), receiverParam.getReceiveCode(), receiverParam.getReceiveName(),
receiverParam.getReceiveTime())) {
throw new BaseException("学校OA批量待办接收人参数不完整");
}
JSONObject receiverBody = new JSONObject();
receiverBody.set("pcUrl", receiverParam.getPcUrl());
receiverBody.set("appUrl", receiverParam.getAppUrl());
receiverBody.set("uniqueId", receiverParam.getUniqueId());
receiverBody.set("receiveCode", receiverParam.getReceiveCode());
receiverBody.set("receiveName", receiverParam.getReceiveName());
receiverBody.set("receiveTime", receiverParam.getReceiveTime());
userList.add(receiverBody);
} }
Sys_user receiver = dao.fetch(Sys_user.class, receiverId); // 学校OA批量接口的公共流程字段在根节点,接收人差异字段仅放在userList中。
if (receiver == null || StrUtil.hasBlank(receiver.getLoginname(), receiver.getUsername())) { JSONObject body = new JSONObject();
throw new BaseException("学校OA福利选择提醒待办接收人工号或姓名为空,userId={}", receiverId); body.set("flowTypeName", todoParam.getFlowTypeName());
} body.set("flowId", todoParam.getFlowId());
return sendWelfareReminderTodo(projectId, title, pcUrl, appUrl, creator, receiver, body.set("title", todoParam.getTitle());
formatTime(System.currentTimeMillis())); body.set("stepName", todoParam.getStepName());
body.set("creatorCode", todoParam.getCreatorCode());
body.set("creatorName", todoParam.getCreatorName());
body.set("creatorTime", todoParam.getCreatorTime());
body.set("startPcPreviewUrl", todoParam.getStartPcPreviewUrl());
body.set("startAppPreviewUrl", todoParam.getStartAppPreviewUrl());
body.set("userList", userList);
String batchInsertUrl = requireConfig("school-oa.todo.batch-insert-url");
String responseBody = sendTodoRequest(batchInsertUrl, body, "学校OA批量待办创建失败", "学校OA批量待办创建异常");
log.info("学校OA批量待办创建成功,flowId={}receiverCount={}response={}",
todoParam.getFlowId(), userList.size(), responseBody);
} }
@Override @Override
public void completeWelfareReminderTodo(String uniqueId) { @Aop(TransAop.READ_COMMITTED)
if (!isSchoolOaEnabled("完成福利选择提醒待办", null, null) || StrUtil.isBlank(uniqueId)) { public void completeTodoByUniqueId(String uniqueId) {
if (!isSchoolOaEnabled("完成待办", null, null) || StrUtil.isBlank(uniqueId)) {
return; return;
} }
String updateUrl = requireConfig("school-oa.todo.update-url").replace("{uniqueId}", uniqueId); String updateUrl = requireConfig("school-oa.todo.update-url").replace("{uniqueId}", uniqueId);
String responseBody = sendTodoRequest(updateUrl, String responseBody = sendTodoRequest(updateUrl, "学校OA待办完成失败", "学校OA待办完成异常");
"学校OA福利选择提醒待办完成失败", "学校OA福利选择提醒待办完成异常"); log.info("学校OA待办完成成功,uniqueId={}response={}", uniqueId, responseBody);
log.info("学校OA福利选择提醒待办完成成功,uniqueId={}response={}", uniqueId, responseBody);
}
@Override
public void deleteWelfareReminderTodo(String projectId, String userId) {
if (!isSchoolOaEnabled("删除福利选择提醒待办", null, null) || StrUtil.hasBlank(projectId, userId)) {
return;
}
// 兼容删除改造前按项目和用户生成的固定唯一值,新的重复提醒不再调用删除逻辑。
String uniqueId = buildWelfareReminderFlowId(projectId, userId);
String deleteUrl = requireConfig("school-oa.todo.delete-url").replace("{uniqueId}", uniqueId);
sendTodoRequest(deleteUrl, "学校OA福利选择提醒待办删除失败", "学校OA福利选择提醒待办删除异常");
log.info("学校OA福利选择提醒待办删除成功,projectId={}userId={}uniqueId={}", projectId, userId, uniqueId);
} }
/** /**
@@ -273,29 +291,8 @@ public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
body.set("receiveTime", receiveTime); body.set("receiveTime", receiveTime);
String insertUrl = requireConfig("school-oa.todo.insert-url"); String insertUrl = requireConfig("school-oa.todo.insert-url");
String requestBody = JSONUtil.toJsonStr(body); String responseBody = sendTodoRequest(insertUrl, body, "学校OA待办创建失败", "学校OA待办创建异常");
log.info("学校OA待办创建请求,url={}taskId={}processInstanceId={}receiveCode={}body={}",
insertUrl, task.getId(), instance.getId(), actor.getActorAccount(), requestBody);
try {
HttpRequest request = HttpUtil.createPost(insertUrl);
setTodoRequestHeaders(request);
request.body(requestBody);
String responseBody = request.execute().body();
log.info("学校OA待办创建响应,taskId={}receiveCode={}response={}", task.getId(), actor.getActorAccount(), responseBody);
JSONObject response = JSONUtil.parseObj(responseBody);
if (response.getInt("state", 0) != 200) {
log.warn("学校OA待办创建失败响应,taskId={}receiveCode={}response={}", task.getId(), actor.getActorAccount(), responseBody);
throw new BaseException("学校OA待办创建失败:{}", response.getStr("message", responseBody));
}
log.info("学校OA待办创建成功,taskId={}receiveCode={}response={}", task.getId(), actor.getActorAccount(), responseBody); log.info("学校OA待办创建成功,taskId={}receiveCode={}response={}", task.getId(), actor.getActorAccount(), responseBody);
} catch (BaseException e) {
throw e;
} catch (Exception e) {
log.error("学校OA待办创建异常,url={}taskId={}processInstanceId={}receiveCode={}body={}",
insertUrl, task.getId(), instance.getId(), actor.getActorAccount(), requestBody, e);
throw new BaseException("学校OA待办创建异常:{}", e.getMessage());
}
} }
/** /**
@@ -309,9 +306,8 @@ public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
} }
String uniqueId = task.getId() + "_" + actor.getActorId(); String uniqueId = task.getId() + "_" + actor.getActorId();
String updateUrl = requireConfig("school-oa.todo.update-url").replace("{uniqueId}", uniqueId); completeTodoByUniqueId(uniqueId);
String responseBody = sendTodoRequest(updateUrl, "学校OA待办完成失败", "学校OA待办完成异常"); log.info("学校OA待办完成成功,taskId={}receiveCode={}uniqueId={}", task.getId(), actor.getActorAccount(), uniqueId);
log.info("学校OA待办完成成功,taskId={}receiveCode={}uniqueId={}response={}", task.getId(), actor.getActorAccount(), uniqueId, responseBody);
} }
/** /**
@@ -399,79 +395,6 @@ public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
return baseUrl + separator + "taskId=" + task.getId() + "&instanceId=" + task.getProcessInstanceId(); return baseUrl + separator + "taskId=" + task.getId() + "&instanceId=" + task.getProcessInstanceId();
} }
/**
* 按学校OA固定待办格式创建福利选择提醒,标题承载自定义提醒内容。
*/
private String sendWelfareReminderTodo(String projectId, String title, String pcUrl, String appUrl,
Sys_user creator, Sys_user receiver, String creatorTime) {
String uniqueId = buildWelfareReminderUniqueId(projectId, receiver.getId());
JSONObject body = new JSONObject();
body.set("flowTypeName", "福利选择");
body.set("flowId", buildWelfareReminderFlowId(projectId, receiver.getId()));
body.set("title", title);
body.set("stepName", "福利选择提醒");
body.set("creatorCode", creator.getLoginname());
body.set("creatorName", creator.getUsername());
body.set("creatorTime", creatorTime);
body.set("pcUrl", pcUrl);
body.set("appUrl", appUrl);
body.set("pcPreviewUrl", pcUrl);
body.set("appPreviewUrl", appUrl);
// 同一项目和用户固定使用一个唯一值,确保学校OA最多存在一条福利提醒待办。
body.set("uniqueId", uniqueId);
body.set("receiveCode", receiver.getLoginname());
body.set("receiveName", receiver.getUsername());
body.set("receiveTime", creatorTime);
String insertUrl = requireConfig("school-oa.todo.insert-url");
String requestBody = JSONUtil.toJsonStr(body);
log.info("学校OA福利选择提醒待办创建请求,url={}projectId={}receiveCode={}body={}",
insertUrl, projectId, receiver.getLoginname(), requestBody);
JSONObject response = executeWelfareReminderTodoRequest(insertUrl, requestBody, projectId, receiver.getLoginname());
String responseMessage = response.getStr("message", "");
if (response.getInt("state", 0) != 200) {
// 历史待办已存在时视为创建成功,由本系统补记待办记录并阻止后续重复创建。
if (StrUtil.contains(responseMessage, "已存在")) {
log.info("学校OA福利选择提醒待办已存在,按发送成功处理,projectId={}receiveCode={}uniqueId={}",
projectId, receiver.getLoginname(), uniqueId);
return uniqueId;
}
throw new BaseException("学校OA福利选择提醒待办创建失败:{}", StrUtil.blankToDefault(responseMessage, JSONUtil.toJsonStr(response)));
}
log.info("学校OA福利选择提醒待办创建成功,projectId={}receiveCode={}response={}",
projectId, receiver.getLoginname(), JSONUtil.toJsonStr(response));
return uniqueId;
}
/**
* 调用学校OA插入待办接口并返回原始响应;网络和解析异常统一转换为业务异常。
*/
private JSONObject executeWelfareReminderTodoRequest(String insertUrl, String requestBody, String projectId, String receiverLoginName) {
try {
HttpRequest request = HttpUtil.createPost(insertUrl);
setTodoRequestHeaders(request);
request.body(requestBody);
String responseBody = request.execute().body();
log.info("学校OA福利选择提醒待办创建响应,projectId={}receiveCode={}response={}",
projectId, receiverLoginName, responseBody);
return JSONUtil.parseObj(responseBody);
} catch (Exception e) {
log.error("学校OA福利选择提醒待办创建异常,url={}projectId={}receiveCode={}body={}",
insertUrl, projectId, receiverLoginName, requestBody, e);
throw new BaseException("学校OA福利选择提醒待办创建异常:{}", e.getMessage());
}
}
/** 同一项目和用户共用固定流程标识。 */
private String buildWelfareReminderFlowId(String projectId, String userId) {
return "welfare_reminder_" + projectId + "_" + userId;
}
/** 同一项目和用户固定使用一个待办唯一值。 */
private String buildWelfareReminderUniqueId(String projectId, String userId) {
return buildWelfareReminderFlowId(projectId, userId);
}
private String buildAppTaskUrl(String h5FormKey, ProcessTask task) { private String buildAppTaskUrl(String h5FormKey, ProcessTask task) {
// 部分流程没有配置 H5 审核页,学校 OA 的移动端地址为空时按要求传固定域名兜底。 // 部分流程没有配置 H5 审核页,学校 OA 的移动端地址为空时按要求传固定域名兜底。
if (StrUtil.isBlank(h5FormKey)) { if (StrUtil.isBlank(h5FormKey)) {
@@ -107,6 +107,7 @@ public class WelfareSelectionSituationController {
} }
@At @At
@Aop(TransAop.READ_COMMITTED)
@SLog(type = "welfare", tag = "选择情况", msg = "发送福利选择提醒") @SLog(type = "welfare", tag = "选择情况", msg = "发送福利选择提醒")
@SaCheckPermission("welfare.selection.situation") @SaCheckPermission("welfare.selection.situation")
@ApiOperation("发送福利选择提醒") @ApiOperation("发送福利选择提醒")
@@ -0,0 +1,121 @@
package com.budwk.app.zhgh.welfare.model;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Index;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableIndexes;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.Date;
/**
* 福利选择学校统一消息发送记录。
* 每次提醒批次中的每位接收人各保存一条记录,用于回写统一消息平台的最终成功或失败结果。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("welfare_reminder_message")
@Comment("福利选择学校统一消息发送记录")
@TableIndexes({
@Index(name = "UK_WELFARE_REMINDER_MESSAGE_BATCH_USER", fields = {"messageBatchId", "userId"}, unique = true),
@Index(name = "IDX_WELFARE_REMINDER_MESSAGE_PROJECT_STATUS", fields = {"projectId", "messageStatus"}, unique = false)
})
public class WelfareReminderMessage extends BaseModel {
/** 已提交统一消息平台,等待查询最终结果。 */
public static final int STATUS_WAITING_RESULT = 0;
/** 统一消息平台返回该接收人发送成功。 */
public static final int STATUS_SUCCESS = 1;
/** 统一消息平台返回该接收人发送失败,或发送前参数校验失败。 */
public static final int STATUS_FAILED = 2;
/** 统一消息结果查询异常,尚未能确认接收人的最终发送状态。 */
public static final int STATUS_RESULT_QUERY_FAILED = 3;
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("消息发送批次ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String messageBatchId;
@Column
@Comment("福利项目ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String projectId;
@Column
@Comment("接收用户ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String userId;
@Column
@Comment("统一消息平台消息标识,多个标识以英文逗号分隔")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String messageId;
@Column
@Comment("接收人工号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String receiverCode;
@Column
@Comment("接收人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String receiverName;
@Column
@Comment("发送人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String senderId;
@Column
@Comment("发送人工号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String senderCode;
@Column
@Comment("发送人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String senderName;
@Column
@Comment("消息标题")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String title;
@Column
@Comment("消息内容")
@ColDefine(type = ColType.VARCHAR, width = 1000)
private String content;
@Column
@Comment("状态:0等待结果、1成功、2失败、3结果查询异常")
@ColDefine(type = ColType.INT)
private Integer messageStatus;
@Column
@Comment("提交到统一消息平台时间")
@ColDefine(type = ColType.DATETIME)
private Date sendTime;
@Column
@Comment("查询到最终结果时间")
@ColDefine(type = ColType.DATETIME)
private Date resultTime;
@Column
@Comment("发送或结果查询失败原因")
@ColDefine(type = ColType.VARCHAR, width = 1000)
private String failMessage;
}
@@ -6,14 +6,21 @@ import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.afterturn.easypoi.excel.export.ExcelExportService; import cn.afterturn.easypoi.excel.export.ExcelExportService;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil; import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.ManyAddOrRenewUtil;
import com.budwk.app.base.utils.PageUtil; import com.budwk.app.base.utils.PageUtil;
//import com.budwk.app.sys.models.Sys_file; //import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.base.sms.SmsService; import com.budwk.app.base.sms.SmsService;
import com.budwk.app.flow.param.SchoolOaTodoBatchCreateParam;
import com.budwk.app.flow.param.SchoolOaTodoReceiverParam;
import com.budwk.app.flow.service.SchoolOaTodoService; import com.budwk.app.flow.service.SchoolOaTodoService;
import com.budwk.app.sys.models.Sys_user; import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.sys.services.SysFileService; import com.budwk.app.sys.services.SysFileService;
@@ -25,6 +32,7 @@ import com.budwk.app.zhgh.dayofficework.message.strategy.impl.LocalMessageSendSt
import com.budwk.app.zhgh.welfare.model.WelfareList; import com.budwk.app.zhgh.welfare.model.WelfareList;
import com.budwk.app.zhgh.welfare.model.WelfareProject; import com.budwk.app.zhgh.welfare.model.WelfareProject;
import com.budwk.app.zhgh.welfare.model.WelfareProjectSubjectOption; import com.budwk.app.zhgh.welfare.model.WelfareProjectSubjectOption;
import com.budwk.app.zhgh.welfare.model.WelfareReminderMessage;
import com.budwk.app.zhgh.welfare.model.WelfareReminderTodo; import com.budwk.app.zhgh.welfare.model.WelfareReminderTodo;
import com.budwk.app.zhgh.welfare.model.WelfareUserSelection; import com.budwk.app.zhgh.welfare.model.WelfareUserSelection;
import com.budwk.app.zhgh.welfare.param.WelfareSelectionSituationPageForm; import com.budwk.app.zhgh.welfare.param.WelfareSelectionSituationPageForm;
@@ -37,8 +45,11 @@ import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.random.R;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@@ -65,6 +76,8 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
private SmsService smsService; private SmsService smsService;
@Inject @Inject
private SchoolOaTodoService schoolOaTodoService; private SchoolOaTodoService schoolOaTodoService;
@Inject
private ManyAddOrRenewUtil manyAddOrRenewUtil;
@Override @Override
public String getPreferredMobile(String userId) { public String getPreferredMobile(String userId) {
@@ -168,11 +181,13 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public NutMap sendReminder(WelfareSelectionSituationPageForm pageForm, String[] userIds, String content) { public NutMap sendReminder(WelfareSelectionSituationPageForm pageForm, String[] userIds, String content) {
List<String> recipientIds = getReminderRecipientIds(pageForm, userIds); List<String> recipientIds = getReminderRecipientIds(pageForm, userIds);
if (recipientIds.isEmpty()) { if (recipientIds.isEmpty()) {
return buildReminderResult(0, 0, 0, 0, 0, return buildReminderResult(0, 0, 0, 0, 0, 0,
Boolean.TRUE.equals(Globals.MyConfig.getBoolean("AppSchoolOa"))); Boolean.TRUE.equals(Globals.MyConfig.getBoolean("AppSms")),
Boolean.TRUE.equals(Globals.MyConfig.getBoolean("AppSchoolOa")), 0, 0);
} }
// 各消息渠道统一使用福利业务标题,学校OA再拼接自定义正文作为待办标题。 // 各消息渠道统一使用福利业务标题,学校OA再拼接自定义正文作为待办标题。
String title = "智慧工会职工福利:"; String title = "智慧工会职工福利:";
@@ -183,20 +198,41 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
localMessageSendStrategy.send(title, content, 2, recipientIds, null); localMessageSendStrategy.send(title, content, 2, recipientIds, null);
List<Sys_user> receiverUsers = dao().query(Sys_user.class, Cnd.where(Sys_user::getId, "in", recipientIds)); List<Sys_user> receiverUsers = dao().query(Sys_user.class, Cnd.where(Sys_user::getId, "in", recipientIds));
boolean messageEnabled = Boolean.TRUE.equals(Globals.MyConfig.getBoolean("AppSms"));
boolean todoEnabled = Boolean.TRUE.equals(Globals.MyConfig.getBoolean("AppSchoolOa"));
String senderId = SecurityUtil.getUserId();
Sys_user sender = null;
if (messageEnabled || todoEnabled) {
sender = dao().fetch(Sys_user.class, senderId);
if (sender == null || StrUtil.hasBlank(sender.getId(), sender.getLoginname(), sender.getUsername())) {
throw new IllegalStateException("福利选择提醒创建人信息不完整");
}
}
int messageSuccessCount = 0;
int messageFailedCount = 0;
int messageResultQueryFailedCount = 0;
// AppSms 控制学校消息平台发送,消息链接跳转到手机端福利选择列表。 // AppSms 控制学校消息平台发送,消息链接跳转到手机端福利选择列表。
if (Boolean.TRUE.equals(Globals.MyConfig.getBoolean("AppSms")) && !receiverUsers.isEmpty()) { if (messageEnabled) {
smsService.massSendByUsers(receiverUsers, title, content, pcUrl, appUrl); NutMap messageResult = sendReminderMessageBatch(pageForm.getProjectId(), recipientIds, receiverUsers,
sender, title, content, pcUrl, appUrl);
messageSuccessCount = messageResult.getInt("successCount", 0);
messageFailedCount = messageResult.getInt("failedCount", 0);
messageResultQueryFailedCount = messageResult.getInt("resultQueryFailedCount", 0);
} }
boolean todoEnabled = Boolean.TRUE.equals(Globals.MyConfig.getBoolean("AppSchoolOa"));
int todoSuccessCount = 0; int todoSuccessCount = 0;
int existingTodoCount = 0; int existingTodoCount = 0;
int todoFailedCount = 0; int todoFailedCount = 0;
if (todoEnabled) { if (todoEnabled) {
Map<String, Sys_user> receiverUserMap = receiverUsers.stream() Map<String, Sys_user> receiverUserMap = receiverUsers.stream()
.collect(Collectors.toMap(Sys_user::getId, user -> user, (left, right) -> left)); .collect(Collectors.toMap(Sys_user::getId, user -> user, (left, right) -> left));
Map<String, WelfareReminderTodo> reminderTodoMap = getReminderTodoMap(pageForm.getProjectId(), recipientIds);
List<WelfareReminderTodo> newSendingReminderTodos = new ArrayList<>();
List<WelfareReminderTodo> retrySendingReminderTodos = new ArrayList<>();
List<String> sendingReceiverIds = new ArrayList<>();
for (String recipientId : recipientIds) { for (String recipientId : recipientIds) {
WelfareReminderTodo reminderTodo = getReminderTodo(pageForm.getProjectId(), recipientId); WelfareReminderTodo reminderTodo = reminderTodoMap.get(recipientId);
if (isExistingReminderTodo(reminderTodo)) { if (isExistingReminderTodo(reminderTodo)) {
existingTodoCount++; existingTodoCount++;
continue; continue;
@@ -204,21 +240,15 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
Sys_user receiver = receiverUserMap.get(recipientId); Sys_user receiver = receiverUserMap.get(recipientId);
try { try {
// 发送前先落库为“发送中”,确保每一次实际发起的OA待办都可追溯 // 仅在内存中组装记录,随后按“新增”和“重试”分别执行批量数据库写入
reminderTodo = saveSendingReminderTodo(reminderTodo, pageForm.getProjectId(), receiver, reminderTodo = prepareSendingReminderTodo(reminderTodo, pageForm.getProjectId(), receiver,
SecurityUtil.getUserId(), title + content, content); sender, title + content, content);
String uniqueId = schoolOaTodoService.createWelfareReminderTodo(pageForm.getProjectId(), title + content, sendingReceiverIds.add(recipientId);
pcUrl, appUrl, SecurityUtil.getUserId(), recipientId); if (reminderTodoMap.containsKey(recipientId)) {
if (StrUtil.isBlank(uniqueId)) { retrySendingReminderTodos.add(reminderTodo);
throw new IllegalStateException("学校OA待办开关状态已变化,本次待办未发送"); } else {
newSendingReminderTodos.add(reminderTodo);
} }
reminderTodo.setOaUniqueId(uniqueId);
reminderTodo.setTodoStatus(WelfareReminderTodo.STATUS_PENDING);
reminderTodo.setSendTime(new Date());
reminderTodo.setFinishTime(null);
reminderTodo.setFailMessage(null);
dao().update(reminderTodo);
todoSuccessCount++;
} catch (Exception e) { } catch (Exception e) {
todoFailedCount++; todoFailedCount++;
saveReminderTodoFailure(reminderTodo, pageForm.getProjectId(), recipientId, receiver, saveReminderTodoFailure(reminderTodo, pageForm.getProjectId(), recipientId, receiver,
@@ -227,12 +257,42 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
pageForm.getProjectId(), recipientId, e); pageForm.getProjectId(), recipientId, e);
} }
} }
if (!sendingReceiverIds.isEmpty()) {
persistSendingReminderTodos(pageForm.getProjectId(), newSendingReminderTodos,
retrySendingReminderTodos, senderId);
log.info("福利选择提醒待办批量写入发送中完成,projectId={},接收人总数={},已有待办跳过={},新增记录={},失败重试记录={}",
pageForm.getProjectId(), recipientIds.size(), existingTodoCount,
newSendingReminderTodos.size(), retrySendingReminderTodos.size());
try {
SchoolOaTodoBatchCreateParam todoParam = buildWelfareReminderBatchTodoParam(
pageForm.getProjectId(), title + content, pcUrl, appUrl, sender,
sendingReceiverIds, receiverUserMap);
schoolOaTodoService.createBatchTodo(todoParam);
} catch (Exception e) {
int updatedCount = updateReminderTodoBatchStatus(pageForm.getProjectId(), sendingReceiverIds,
senderId, WelfareReminderTodo.STATUS_FAILED, null, limitFailureMessage(e.getMessage()));
todoFailedCount += sendingReceiverIds.size();
log.error("福利选择提醒待办批量发送失败并批量更新状态,projectId={},待办人数={},数据库更新行数={},状态={},失败原因={}",
pageForm.getProjectId(), sendingReceiverIds.size(), updatedCount,
WelfareReminderTodo.STATUS_FAILED, e.getMessage(), e);
return buildReminderResult(recipientIds.size(), recipientIds.size(), messageSuccessCount,
messageFailedCount, messageResultQueryFailedCount, todoSuccessCount,
messageEnabled, todoEnabled, existingTodoCount, todoFailedCount);
} }
return buildReminderResult(recipientIds.size(), recipientIds.size(), todoSuccessCount, int updatedCount = updateReminderTodoBatchStatus(pageForm.getProjectId(), sendingReceiverIds,
existingTodoCount, todoFailedCount, todoEnabled); senderId, WelfareReminderTodo.STATUS_PENDING, new Date(), null);
todoSuccessCount = sendingReceiverIds.size();
log.info("福利选择提醒待办批量发送成功并批量更新状态,projectId={},待办人数={},数据库更新行数={},状态={}",
pageForm.getProjectId(), todoSuccessCount, updatedCount, WelfareReminderTodo.STATUS_PENDING);
}
}
return buildReminderResult(recipientIds.size(), recipientIds.size(), messageSuccessCount,
messageFailedCount, messageResultQueryFailedCount, todoSuccessCount,
messageEnabled, todoEnabled, existingTodoCount, todoFailedCount);
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void completeReminderTodo(String projectId, String userId) { public void completeReminderTodo(String projectId, String userId) {
WelfareReminderTodo reminderTodo = getReminderTodo(projectId, userId); WelfareReminderTodo reminderTodo = getReminderTodo(projectId, userId);
if (reminderTodo == null || !Objects.equals(reminderTodo.getTodoStatus(), WelfareReminderTodo.STATUS_PENDING) if (reminderTodo == null || !Objects.equals(reminderTodo.getTodoStatus(), WelfareReminderTodo.STATUS_PENDING)
@@ -240,7 +300,7 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
return; return;
} }
try { try {
schoolOaTodoService.completeWelfareReminderTodo(reminderTodo.getOaUniqueId()); schoolOaTodoService.completeTodoByUniqueId(reminderTodo.getOaUniqueId());
reminderTodo.setTodoStatus(WelfareReminderTodo.STATUS_COMPLETED); reminderTodo.setTodoStatus(WelfareReminderTodo.STATUS_COMPLETED);
reminderTodo.setFinishTime(new Date()); reminderTodo.setFinishTime(new Date());
reminderTodo.setFailMessage(null); reminderTodo.setFailMessage(null);
@@ -253,6 +313,209 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
} }
} }
/**
* 批量发送福利选择统一消息并回写逐人结果。
* 每名接收人先落一条等待结果记录;统一消息平台接收整批请求后,使用返回的消息标识查询结果,
* 再依据失败用户工号批量更新本地成功、失败或查询异常状态。
*
* @param projectId 福利项目ID
* @param recipientIds 本次提醒的全部接收用户ID
* @param receiverUsers 已查询到的接收人用户信息
* @param sender 发起提醒的系统用户,用于记录发送人信息
* @param title 统一消息标题,长度不得超过学校接口要求的100个字符
* @param content 统一消息正文
* @param pcUrl PC端福利选择链接
* @param mobileUrl 移动端福利选择链接
* @return 包含成功数、失败数和结果查询异常数的统计结果
*/
private NutMap sendReminderMessageBatch(String projectId, List<String> recipientIds, List<Sys_user> receiverUsers,
Sys_user sender, String title, String content, String pcUrl, String mobileUrl) {
if (StrUtil.hasBlank(projectId, title, content) || recipientIds.isEmpty()
|| sender == null || StrUtil.hasBlank(sender.getId(), sender.getLoginname(), sender.getUsername())) {
throw new IllegalArgumentException("福利选择统一消息批量发送参数不完整");
}
if (title.length() > 100) {
throw new IllegalArgumentException("福利选择统一消息标题长度不能超过100个字符");
}
String senderId = sender.getId();
String messageBatchId = R.UU32();
long now = System.currentTimeMillis();
Map<String, Sys_user> receiverUserMap = receiverUsers.stream()
.collect(Collectors.toMap(Sys_user::getId, user -> user, (left, right) -> left));
List<WelfareReminderMessage> records = new ArrayList<>();
List<WelfareReminderMessage> validRecords = new ArrayList<>();
List<Sys_user> validUsers = new ArrayList<>();
for (String recipientId : recipientIds) {
Sys_user receiver = receiverUserMap.get(recipientId);
WelfareReminderMessage record = createReminderMessageRecord(messageBatchId, projectId, recipientId,
receiver, sender, title, content, now);
records.add(record);
if (receiver == null || StrUtil.hasBlank(receiver.getLoginname(), receiver.getUsername())) {
record.setMessageStatus(WelfareReminderMessage.STATUS_FAILED);
record.setFailMessage("学校统一消息发送失败:接收人工号或姓名为空");
} else {
validRecords.add(record);
validUsers.add(receiver);
}
}
log.info("福利选择统一消息记录已在内存组装完成,projectId={}messageBatchId={},接收人总数={},可发送人数={},接收人信息失败人数={}",
projectId, messageBatchId, records.size(), validUsers.size(), records.size() - validUsers.size());
if (validUsers.isEmpty()) {
persistReminderMessageRecordsAsync(records, projectId, messageBatchId);
return NutMap.NEW().addv("successCount", 0).addv("failedCount", records.size()).addv("resultQueryFailedCount", 0);
}
List<String> messageIds;
String messageIdText;
try {
messageIds = smsService.massSendByUsers(validUsers, title, content, pcUrl, mobileUrl);
if (messageIds == null || messageIds.isEmpty()) {
throw new IllegalStateException("学校统一消息平台未返回消息标识");
}
messageIdText = String.join(",", messageIds);
Date sendTime = new Date();
validRecords.forEach(record -> {
record.setMessageId(messageIdText);
record.setMessageStatus(WelfareReminderMessage.STATUS_WAITING_RESULT);
record.setSendTime(sendTime);
record.setFailMessage(null);
record.setUpdatedBy(senderId);
record.setUpdatedAt(System.currentTimeMillis());
});
log.info("福利选择统一消息批量发送已入队,等待结果查询后异步写入记录,projectId={}messageBatchId={}messageIds={},接收人数={}",
projectId, messageBatchId, messageIds, validRecords.size());
} catch (Exception e) {
markReminderMessageRecords(validRecords, senderId, WelfareReminderMessage.STATUS_FAILED,
null, null, limitFailureMessage(e.getMessage()));
persistReminderMessageRecordsAsync(records, projectId, messageBatchId);
log.error("福利选择统一消息批量发送失败,已提交异步入库记录,projectId={}messageBatchId={},接收人数={},失败原因={}",
projectId, messageBatchId, validRecords.size(), e.getMessage(), e);
return NutMap.NEW().addv("successCount", 0).addv("failedCount", records.size()).addv("resultQueryFailedCount", 0);
}
try {
Map<String, String> failedMessageByCode = queryFailedMessageByReceiverCode(messageIds);
Date resultTime = new Date();
for (WelfareReminderMessage record : validRecords) {
String failMessage = failedMessageByCode.get(record.getReceiverCode());
record.setMessageStatus(StrUtil.isBlank(failMessage)
? WelfareReminderMessage.STATUS_SUCCESS : WelfareReminderMessage.STATUS_FAILED);
record.setResultTime(resultTime);
record.setFailMessage(failMessage);
record.setUpdatedBy(senderId);
record.setUpdatedAt(System.currentTimeMillis());
}
int externalFailedCount = (int) validRecords.stream()
.filter(record -> failedMessageByCode.containsKey(record.getReceiverCode()))
.count();
int failedCount = records.size() - validRecords.size() + externalFailedCount;
int successCount = validRecords.size() - externalFailedCount;
persistReminderMessageRecordsAsync(records, projectId, messageBatchId);
log.info("福利选择统一消息结果查询完成,已提交异步入库记录,projectId={}messageBatchId={},成功人数={},失败人数={}messageIds={}",
projectId, messageBatchId, successCount, failedCount, messageIds);
return NutMap.NEW().addv("successCount", successCount).addv("failedCount", failedCount).addv("resultQueryFailedCount", 0);
} catch (Exception e) {
markReminderMessageRecords(validRecords, senderId, WelfareReminderMessage.STATUS_RESULT_QUERY_FAILED,
messageIdText, new Date(), limitFailureMessage(e.getMessage()));
persistReminderMessageRecordsAsync(records, projectId, messageBatchId);
log.error("福利选择统一消息结果查询异常,已提交异步入库记录,projectId={}messageBatchId={}messageIds={},接收人数={},失败原因={}",
projectId, messageBatchId, messageIds, validRecords.size(), e.getMessage(), e);
return NutMap.NEW().addv("successCount", 0).addv("failedCount", records.size() - validRecords.size())
.addv("resultQueryFailedCount", validRecords.size());
}
}
/**
* 创建统一消息接收人记录,并显式补齐fastInsert不会执行的主键和审计字段。
*/
private WelfareReminderMessage createReminderMessageRecord(String messageBatchId, String projectId, String userId,
Sys_user receiver, Sys_user sender, String title,
String content, long now) {
WelfareReminderMessage record = new WelfareReminderMessage();
record.setId(R.UU32());
record.setMessageBatchId(messageBatchId);
record.setProjectId(projectId);
record.setUserId(userId);
if (receiver != null) {
record.setReceiverCode(receiver.getLoginname());
record.setReceiverName(receiver.getUsername());
}
record.setSenderId(sender.getId());
record.setSenderCode(sender.getLoginname());
record.setSenderName(sender.getUsername());
record.setTitle(limitText(title, 100));
record.setContent(limitText(content, 1000));
record.setMessageStatus(WelfareReminderMessage.STATUS_WAITING_RESULT);
record.setCreatedBy(sender.getId());
record.setCreatedAt(now);
record.setUpdatedBy(sender.getId());
record.setUpdatedAt(now);
record.setDelFlag(false);
return record;
}
/**
* 查询所有消息标识的结果并汇总失败用户工号与失败原因。
* 当前配置仅使用一个发送渠道,通常只返回一个消息标识;多标识场景仍逐个查询并按工号合并失败结果。
*/
private Map<String, String> queryFailedMessageByReceiverCode(List<String> messageIds) {
Map<String, String> failedMessageByCode = new HashMap<>();
for (String messageId : messageIds) {
NutMap messageResult = smsService.getMessageResult(messageId);
Object failedUsers = messageResult.get("failedUsers");
if (failedUsers instanceof JSONArray failedUserArray) {
for (Object failedUserValue : failedUserArray) {
JSONObject failedUser = JSONUtil.parseObj(failedUserValue);
String userCode = failedUser.getStr("userCode");
if (StrUtil.isNotBlank(userCode)) {
failedMessageByCode.put(userCode, limitFailureMessage(failedUser.getStr("errorInfo")));
}
}
}
if ("0".equals(messageResult.getString("resultState")) && failedMessageByCode.isEmpty()) {
throw new IllegalStateException("学校统一消息平台返回发送失败,但未返回失败人员明细,messageId=" + messageId);
}
}
return failedMessageByCode;
}
/**
* 在内存中更新一批统一消息记录状态;所有结果明确后再统一异步插入,避免异步插入和同步更新发生竞争。
*/
private void markReminderMessageRecords(List<WelfareReminderMessage> records, String senderId, int messageStatus,
String messageId, Date resultTime, String failMessage) {
if (records.isEmpty()) {
return;
}
Date sendTime = messageId == null ? null : new Date();
records.forEach(record -> {
if (messageId != null) {
record.setMessageId(messageId);
record.setSendTime(sendTime);
}
record.setMessageStatus(messageStatus);
record.setResultTime(resultTime);
record.setFailMessage(failMessage);
record.setUpdatedBy(senderId);
record.setUpdatedAt(System.currentTimeMillis());
});
}
/**
* 使用项目线程池异步分批插入最终消息记录。记录已显式设置主键和审计字段,
* 因此可安全使用fastInsert;本方法只提交任务,不等待数据库写入完成。
*/
private void persistReminderMessageRecordsAsync(List<WelfareReminderMessage> records, String projectId, String messageBatchId) {
if (records.isEmpty()) {
return;
}
manyAddOrRenewUtil.asyncExecuteFastInsert(records, 200);
log.info("福利选择统一消息记录已提交异步批量入库,projectId={}messageBatchId={},记录数={}batchSize={}",
projectId, messageBatchId, records.size(), 200);
}
/** /**
* 查询项目和用户对应的唯一提醒待办记录。 * 查询项目和用户对应的唯一提醒待办记录。
*/ */
@@ -264,6 +527,24 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
.and(WelfareReminderTodo::getUserId, "=", userId)); .and(WelfareReminderTodo::getUserId, "=", userId));
} }
/**
* 批量查询当前项目的提醒待办,避免发送前按接收人逐条访问数据库。
*
* @param projectId 福利项目ID
* @param userIds 本次实际接收人ID集合
* @return 以用户ID为键的待办记录;未创建过待办的用户不包含在结果中
*/
private Map<String, WelfareReminderTodo> getReminderTodoMap(String projectId, List<String> userIds) {
if (StrUtil.isBlank(projectId) || userIds.isEmpty()) {
return Collections.emptyMap();
}
return dao().query(WelfareReminderTodo.class, Cnd.where(WelfareReminderTodo::getProjectId, "=", projectId)
.and(WelfareReminderTodo::getUserId, "in", userIds))
.stream()
.collect(Collectors.toMap(WelfareReminderTodo::getUserId, reminderTodo -> reminderTodo,
(left, right) -> left));
}
/** /**
* 发送中、待办和已办记录均表示该项目已创建过OA待办,重复提醒时不再创建第二条。 * 发送中、待办和已办记录均表示该项目已创建过OA待办,重复提醒时不再创建第二条。
* 发送失败记录允许沿用原唯一值重试。 * 发送失败记录允许沿用原唯一值重试。
@@ -288,24 +569,31 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
} }
/** /**
* 新建或重置待办发送记录。固定的流程ID和唯一值保证同一项目、同一用户最多一条OA待办 * 组装发送中的待办记录。新增记录补齐批量插入所需主键和审计字段,失败重试记录保留原主键
*/ */
private WelfareReminderTodo saveSendingReminderTodo(WelfareReminderTodo reminderTodo, String projectId, private WelfareReminderTodo prepareSendingReminderTodo(WelfareReminderTodo reminderTodo, String projectId,
Sys_user receiver, String senderId, String title, String content) { Sys_user receiver, Sys_user sender, String title, String content) {
if (receiver == null || StrUtil.hasBlank(receiver.getId(), receiver.getLoginname(), receiver.getUsername())) { if (receiver == null || StrUtil.hasBlank(receiver.getId(), receiver.getLoginname(), receiver.getUsername())) {
throw new IllegalArgumentException("学校OA福利选择提醒接收人信息不完整"); throw new IllegalArgumentException("学校OA福利选择提醒接收人信息不完整");
} }
Sys_user sender = dao().fetch(Sys_user.class, senderId); if (sender == null || StrUtil.hasBlank(sender.getId(), sender.getLoginname(), sender.getUsername())) {
if (sender == null || StrUtil.hasBlank(sender.getLoginname(), sender.getUsername())) {
throw new IllegalArgumentException("学校OA福利选择提醒创建人信息不完整"); throw new IllegalArgumentException("学校OA福利选择提醒创建人信息不完整");
} }
boolean newRecord = reminderTodo == null; boolean newRecord = reminderTodo == null;
WelfareReminderTodo record = newRecord ? new WelfareReminderTodo() : reminderTodo; WelfareReminderTodo record = newRecord ? new WelfareReminderTodo() : reminderTodo;
long now = System.currentTimeMillis();
String uniqueId = buildWelfareReminderUniqueId(projectId, receiver.getId()); String uniqueId = buildWelfareReminderUniqueId(projectId, receiver.getId());
if (newRecord) {
// fastInsert不会触发实体的PrevInsert,主键和审计字段需在批量入库前显式赋值。
record.setId(R.UU32());
record.setCreatedBy(sender.getId());
record.setCreatedAt(now);
record.setDelFlag(false);
}
record.setProjectId(projectId); record.setProjectId(projectId);
record.setUserId(receiver.getId()); record.setUserId(receiver.getId());
record.setOaFlowId(uniqueId); record.setOaFlowId(buildWelfareReminderBatchFlowId(projectId));
record.setOaUniqueId(uniqueId); record.setOaUniqueId(uniqueId);
record.setReceiverCode(receiver.getLoginname()); record.setReceiverCode(receiver.getLoginname());
record.setReceiverName(receiver.getUsername()); record.setReceiverName(receiver.getUsername());
@@ -318,14 +606,48 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
record.setSendTime(null); record.setSendTime(null);
record.setFinishTime(null); record.setFinishTime(null);
record.setFailMessage(null); record.setFailMessage(null);
if (newRecord) { record.setUpdatedBy(sender.getId());
dao().insert(record); record.setUpdatedAt(now);
} else {
dao().update(record);
}
return record; return record;
} }
/**
* 批量持久化发送中状态:新记录使用fastInsert,历史失败记录使用DAO批量更新。
*/
private void persistSendingReminderTodos(String projectId, List<WelfareReminderTodo> newRecords,
List<WelfareReminderTodo> retryRecords, String senderId) {
if (!newRecords.isEmpty()) {
dao().fastInsert(newRecords);
log.info("福利选择提醒待办批量新增记录完成,projectId={},新增数量={},状态={}",
projectId, newRecords.size(), WelfareReminderTodo.STATUS_SENDING);
}
if (!retryRecords.isEmpty()) {
dao().update(retryRecords,
"oaFlowId|oaUniqueId|receiverCode|receiverName|senderId|senderCode|senderName|title|content|todoStatus|sendTime|finishTime|failMessage|updatedBy|updatedAt");
log.info("福利选择提醒待办批量重试记录更新完成,projectId={},重试数量={},状态={},操作人={}",
projectId, retryRecords.size(), WelfareReminderTodo.STATUS_SENDING, senderId);
}
}
/**
* 按本批用户统一更新待办状态。成功时写入发送时间,失败时写入统一失败原因。
*/
private int updateReminderTodoBatchStatus(String projectId, List<String> userIds, String senderId,
int todoStatus, Date sendTime, String failMessage) {
if (StrUtil.isBlank(projectId) || userIds.isEmpty()) {
return 0;
}
Chain chain = Chain.make("todoStatus", todoStatus)
.add("sendTime", sendTime)
.add("finishTime", null)
.add("failMessage", failMessage)
.add("updatedBy", senderId)
.add("updatedAt", System.currentTimeMillis());
return dao().update(WelfareReminderTodo.class, chain,
Cnd.where(WelfareReminderTodo::getProjectId, "=", projectId)
.and(WelfareReminderTodo::getUserId, "in", userIds));
}
/** /**
* 保存OA待办发送失败原因;若发送前校验失败导致记录尚未创建,则补建一条失败记录。 * 保存OA待办发送失败原因;若发送前校验失败导致记录尚未创建,则补建一条失败记录。
*/ */
@@ -336,7 +658,7 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
record = new WelfareReminderTodo(); record = new WelfareReminderTodo();
record.setProjectId(projectId); record.setProjectId(projectId);
record.setUserId(userId); record.setUserId(userId);
record.setOaFlowId(buildWelfareReminderUniqueId(projectId, userId)); record.setOaFlowId(buildWelfareReminderBatchFlowId(projectId));
record.setOaUniqueId(buildWelfareReminderUniqueId(projectId, userId)); record.setOaUniqueId(buildWelfareReminderUniqueId(projectId, userId));
record.setSenderId(SecurityUtil.getUserId()); record.setSenderId(SecurityUtil.getUserId());
if (receiver != null) { if (receiver != null) {
@@ -363,9 +685,21 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
/** /**
* 组装发送统计和前端提示,消息与OA待办分别计数,避免把重复跳过误报为发送成功。 * 组装发送统计和前端提示,消息与OA待办分别计数,避免把重复跳过误报为发送成功。
*/ */
private NutMap buildReminderResult(int recipientCount, int messageSuccessCount, int todoSuccessCount, private NutMap buildReminderResult(int recipientCount, int localMessageSuccessCount, int messageSuccessCount,
int existingTodoCount, int todoFailedCount, boolean todoEnabled) { int messageFailedCount, int messageResultQueryFailedCount, int todoSuccessCount,
StringBuilder summary = new StringBuilder("消息发送成功").append(messageSuccessCount).append(""); boolean messageEnabled, boolean todoEnabled, int existingTodoCount, int todoFailedCount) {
StringBuilder summary = new StringBuilder("本地消息发送成功").append(localMessageSuccessCount).append("");
if (messageEnabled) {
summary.append(",学校统一消息发送成功").append(messageSuccessCount).append("");
if (messageFailedCount > 0) {
summary.append(",学校统一消息发送失败").append(messageFailedCount).append("");
}
if (messageResultQueryFailedCount > 0) {
summary.append(",学校统一消息结果查询异常").append(messageResultQueryFailedCount).append("");
}
} else {
summary.append(",学校统一消息开关未开启");
}
if (todoEnabled) { if (todoEnabled) {
summary.append("OA待办发送成功").append(todoSuccessCount).append(""); summary.append("OA待办发送成功").append(todoSuccessCount).append("");
if (existingTodoCount > 0) { if (existingTodoCount > 0) {
@@ -379,7 +713,11 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
} }
return NutMap.NEW() return NutMap.NEW()
.addv("recipientCount", recipientCount) .addv("recipientCount", recipientCount)
.addv("localMessageSuccessCount", localMessageSuccessCount)
.addv("messageSuccessCount", messageSuccessCount) .addv("messageSuccessCount", messageSuccessCount)
.addv("messageFailedCount", messageFailedCount)
.addv("messageResultQueryFailedCount", messageResultQueryFailedCount)
.addv("messageEnabled", messageEnabled)
.addv("todoSuccessCount", todoSuccessCount) .addv("todoSuccessCount", todoSuccessCount)
.addv("existingTodoCount", existingTodoCount) .addv("existingTodoCount", existingTodoCount)
.addv("todoFailedCount", todoFailedCount) .addv("todoFailedCount", todoFailedCount)
@@ -401,6 +739,65 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
return safeText.length() > maxLength ? safeText.substring(0, maxLength) : safeText; return safeText.length() > maxLength ? safeText.substring(0, maxLength) : safeText;
} }
/**
* 组装福利选择的学校OA批量待办参数。
* 公共OA服务只负责校验参数和调用接口;福利项目标识、标题、链接及接收人信息均在此处按福利业务规则生成。
*
* @param projectId 福利项目ID,用于生成学校OA批次流程标识和每个接收人的唯一值
* @param title 学校OA待办标题,包含福利提醒正文
* @param pcUrl 福利选择PC端跳转地址
* @param appUrl 福利选择移动端跳转地址
* @param sender 发起提醒的系统用户,提供学校OA创建人工号和姓名
* @param receiverIds 本次实际发送的接收人ID集合
* @param receiverUserMap 接收人ID与系统用户信息的映射
* @return 可直接传给学校OA批量待办接口的参数对象
*/
private SchoolOaTodoBatchCreateParam buildWelfareReminderBatchTodoParam(String projectId, String title,
String pcUrl, String appUrl, Sys_user sender,
List<String> receiverIds,
Map<String, Sys_user> receiverUserMap) {
if (StrUtil.hasBlank(projectId, title, pcUrl, appUrl)
|| sender == null || StrUtil.hasBlank(sender.getLoginname(), sender.getUsername())
|| receiverIds == null || receiverIds.isEmpty()) {
throw new IllegalArgumentException("学校OA福利选择提醒批量待办参数不完整");
}
String sendTime = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss");
List<SchoolOaTodoReceiverParam> userList = new ArrayList<>();
for (String receiverId : receiverIds) {
Sys_user receiver = receiverUserMap.get(receiverId);
if (receiver == null || StrUtil.hasBlank(receiver.getLoginname(), receiver.getUsername())) {
throw new IllegalArgumentException("学校OA福利选择提醒接收人信息不完整,userId=" + receiverId);
}
SchoolOaTodoReceiverParam receiverParam = new SchoolOaTodoReceiverParam();
receiverParam.setPcUrl(pcUrl);
receiverParam.setAppUrl(appUrl);
receiverParam.setUniqueId(buildWelfareReminderUniqueId(projectId, receiverId));
receiverParam.setReceiveCode(receiver.getLoginname());
receiverParam.setReceiveName(receiver.getUsername());
receiverParam.setReceiveTime(sendTime);
userList.add(receiverParam);
}
SchoolOaTodoBatchCreateParam todoParam = new SchoolOaTodoBatchCreateParam();
todoParam.setFlowTypeName("福利选择");
todoParam.setFlowId(buildWelfareReminderBatchFlowId(projectId));
todoParam.setTitle(title);
todoParam.setStepName("福利选择提醒");
todoParam.setCreatorCode(sender.getLoginname());
todoParam.setCreatorName(sender.getUsername());
todoParam.setCreatorTime(sendTime);
todoParam.setStartPcPreviewUrl(pcUrl);
todoParam.setStartAppPreviewUrl(appUrl);
todoParam.setUserList(userList);
return todoParam;
}
/** 同一项目的一批福利提醒共用学校OA流程标识,接收人通过uniqueId区分。 */
private String buildWelfareReminderBatchFlowId(String projectId) {
return "welfare_reminder_" + projectId;
}
/** 同一项目和用户固定使用一个OA待办唯一值。 */ /** 同一项目和用户固定使用一个OA待办唯一值。 */
private String buildWelfareReminderUniqueId(String projectId, String userId) { private String buildWelfareReminderUniqueId(String projectId, String userId) {
return "welfare_reminder_" + projectId + "_" + userId; return "welfare_reminder_" + projectId + "_" + userId;
@@ -167,7 +167,7 @@ layout("/layouts/platform.html"){
<!-- </el-form-item>--> <!-- </el-form-item>-->
<!-- </el-descriptions-item>--> <!-- </el-descriptions-item>-->
<template v-if="['1', '2'].includes(formData.typeCode)"> <template v-if="(chooseType.name || formData.typeName || '').includes('住院')">
<el-descriptions-item label="入院时间"> <el-descriptions-item label="入院时间">
<el-form-item label="入院时间" prop="hospitalizationTime"> <el-form-item label="入院时间" prop="hospitalizationTime">
<el-date-picker <el-date-picker
@@ -23,7 +23,7 @@ const condolenceInfo = {
<!-- <el-descriptions-item label="收款账户">{{ viewData.bankCardNumber }}</el-descriptions-item>--> <!-- <el-descriptions-item label="收款账户">{{ viewData.bankCardNumber }}</el-descriptions-item>-->
<!-- <el-descriptions-item label="户名">{{ viewData.bankUserName }}</el-descriptions-item>--> <!-- <el-descriptions-item label="户名">{{ viewData.bankUserName }}</el-descriptions-item>-->
<!-- <el-descriptions-item label="开户行">{{ viewData.bankOfDeposit }}</el-descriptions-item>--> <!-- <el-descriptions-item label="开户行">{{ viewData.bankOfDeposit }}</el-descriptions-item>-->
<template v-if="['1', '2'].includes(viewData.typeCode)"> <template v-if="(viewData.typeName || '').includes('住院')">
<el-descriptions-item label="入院时间">{{ viewData.hospitalizationTime }}</el-descriptions-item> <el-descriptions-item label="入院时间">{{ viewData.hospitalizationTime }}</el-descriptions-item>
<el-descriptions-item label="出院时间">{{ viewData.leaveHospitalTime }}</el-descriptions-item> <el-descriptions-item label="出院时间">{{ viewData.leaveHospitalTime }}</el-descriptions-item>
<el-descriptions-item label="当年第几次住院">{{ viewData.thisYearHospitalizationNum }} <el-descriptions-item label="当年第几次住院">{{ viewData.thisYearHospitalizationNum }}
@@ -279,7 +279,7 @@ layout("/layouts/platform_h5.html"){
<!-- name="bankOfDeposit"--> <!-- name="bankOfDeposit"-->
<!-- ></van-field>--> <!-- ></van-field>-->
<template v-if="['1', '2'].includes(formData.typeCode)"> <template v-if="(chooseType.name || formData.typeName || '').includes('住院')">
<van-field <van-field
v-model="formData.hospitalizationTime" v-model="formData.hospitalizationTime"
name="hospitalizationTime" name="hospitalizationTime"
@@ -23,7 +23,7 @@ const condolenceInfo = {
<!-- <van-cell class="direction-column-cell" title="开户行">--> <!-- <van-cell class="direction-column-cell" title="开户行">-->
<!-- {{ viewData.bankOfDeposit || '暂无' }}--> <!-- {{ viewData.bankOfDeposit || '暂无' }}-->
<!-- </van-cell>--> <!-- </van-cell>-->
<template v-if="['1', '2'].includes(viewData.typeCode)"> <template v-if="(viewData.typeName || '').includes('住院')">
<van-cell title="入院时间">{{ viewData.hospitalizationTime }}</van-cell> <van-cell title="入院时间">{{ viewData.hospitalizationTime }}</van-cell>
<van-cell title="出院时间">{{ viewData.leaveHospitalTime }}</van-cell> <van-cell title="出院时间">{{ viewData.leaveHospitalTime }}</van-cell>
<van-cell title="当年第几次住院(次)">{{ viewData.thisYearHospitalizationNum }}</van-cell> <van-cell title="当年第几次住院(次)">{{ viewData.thisYearHospitalizationNum }}</van-cell>