福利发消息整改

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;
import com.budwk.app.sys.models.Sys_user;
import org.nutz.lang.util.NutMap;
import java.util.List;
@@ -47,6 +48,15 @@ public interface SmsService {
* @param content 消息正文
* @param pcUrl PC端跳转链接
* @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.util.NutMap;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -66,12 +67,18 @@ public class SmsYpiServiceImpl implements SmsService {
* 兼容统一消息V4新增的批量发送接口;旧版实现不再注册为发送服务,仅保留历史调用能力。
*/
@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()
.map(Sys_user::getLoginname)
.filter(StrUtil::isNotBlank)
.toList();
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.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import java.util.List;
@@ -63,9 +64,9 @@ public class UnifiedMessageV4ServiceImpl implements SmsService {
}
@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()) {
return;
return List.of();
}
List<Receiver> receivers = users.stream()
.filter(user -> StrUtil.isNotBlank(user.getLoginname()) && StrUtil.isNotBlank(user.getUsername()))
@@ -74,13 +75,14 @@ public class UnifiedMessageV4ServiceImpl implements SmsService {
if (receivers.isEmpty()) {
throw new BaseException("学校统一消息发送失败:接收人工号或姓名不能为空");
}
sendMessage(title, content, createReceivers(receivers), pcUrl, mobileUrl);
return sendMessage(title, content, createReceivers(receivers), pcUrl, mobileUrl);
}
/**
* 调用统一消息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();
boolean hasLink = StrUtil.isNotBlank(pcUrl) || StrUtil.isNotBlank(mobileUrl);
JSONObject body = JSONUtil.createObj();
@@ -108,7 +110,20 @@ public class UnifiedMessageV4ServiceImpl implements SmsService {
if (response.getInt("state", 0) != 200) {
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) {
throw 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秒避免临界过期。
*/
@@ -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;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.param.SchoolOaTodoBatchCreateParam;
/**
* 学校 OA 待办同步服务。
@@ -43,30 +44,17 @@ public interface SchoolOaTodoService {
void updateFlowFinishedStepName(Long processInstanceId);
/**
* 创建福利选择提醒待办。
* 创建学校OA批量待办。
*
* @param projectId 福利项目ID,用于构造学校OA业务标识
* @param title 待办标题,学校OA接口没有独立正文时用于展示提醒内容
* @param pcUrl PC端福利选择地址
* @param appUrl 手机端福利选择地址
* @param creatorId 待办创建人ID
* @param receiverId 待办接收人ID
* @return 学校OA待办唯一值
* @param todoParam 批量待办公共字段及userList接收人数组
*/
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.ProcessTaskActor;
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.sys.models.Sys_user;
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 java.util.Date;
import java.util.ArrayList;
import java.util.List;
/**
@@ -201,48 +204,63 @@ public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
}
@Override
public String createWelfareReminderTodo(String projectId, String title, String pcUrl, String appUrl, String creatorId, String receiverId) {
if (!isSchoolOaEnabled("创建福利选择提醒待办", null, null)) {
return null;
@Aop(TransAop.READ_COMMITTED)
public void createBatchTodo(SchoolOaTodoBatchCreateParam todoParam) {
if (!isSchoolOaEnabled("创建批量待办", null, null)) {
throw new BaseException("学校OA待办开关状态已变化,本次待办未发送");
}
if (StrUtil.hasBlank(projectId, title, pcUrl, appUrl, creatorId, receiverId)) {
throw new BaseException("学校OA福利选择提醒待办参数不完整");
if (todoParam == null || StrUtil.hasBlank(todoParam.getFlowTypeName(), todoParam.getFlowId(), todoParam.getTitle(),
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);
if (creator == null || StrUtil.hasBlank(creator.getLoginname(), creator.getUsername())) {
throw new BaseException("学校OA福利选择提醒待办创建人工号或姓名为空,userId={}", creatorId);
List<JSONObject> userList = new ArrayList<>();
for (SchoolOaTodoReceiverParam receiverParam : todoParam.getUserList()) {
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);
if (receiver == null || StrUtil.hasBlank(receiver.getLoginname(), receiver.getUsername())) {
throw new BaseException("学校OA福利选择提醒待办接收人工号或姓名为空,userId={}", receiverId);
}
return sendWelfareReminderTodo(projectId, title, pcUrl, appUrl, creator, receiver,
formatTime(System.currentTimeMillis()));
// 学校OA批量接口的公共流程字段在根节点,接收人差异字段仅放在userList中。
JSONObject body = new JSONObject();
body.set("flowTypeName", todoParam.getFlowTypeName());
body.set("flowId", todoParam.getFlowId());
body.set("title", todoParam.getTitle());
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
public void completeWelfareReminderTodo(String uniqueId) {
if (!isSchoolOaEnabled("完成福利选择提醒待办", null, null) || StrUtil.isBlank(uniqueId)) {
@Aop(TransAop.READ_COMMITTED)
public void completeTodoByUniqueId(String uniqueId) {
if (!isSchoolOaEnabled("完成待办", null, null) || StrUtil.isBlank(uniqueId)) {
return;
}
String updateUrl = requireConfig("school-oa.todo.update-url").replace("{uniqueId}", uniqueId);
String responseBody = sendTodoRequest(updateUrl,
"学校OA福利选择提醒待办完成失败", "学校OA福利选择提醒待办完成异常");
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);
String responseBody = sendTodoRequest(updateUrl, "学校OA待办完成失败", "学校OA待办完成异常");
log.info("学校OA待办完成成功,uniqueId={}response={}", uniqueId, responseBody);
}
/**
@@ -273,29 +291,8 @@ public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
body.set("receiveTime", receiveTime);
String insertUrl = requireConfig("school-oa.todo.insert-url");
String requestBody = JSONUtil.toJsonStr(body);
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);
} 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());
}
String responseBody = sendTodoRequest(insertUrl, body, "学校OA待办创建失败", "学校OA待办创建异常");
log.info("学校OA待办创建成功taskId={}receiveCode={}response={}", task.getId(), actor.getActorAccount(), responseBody);
}
/**
@@ -309,9 +306,8 @@ public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
}
String uniqueId = task.getId() + "_" + actor.getActorId();
String updateUrl = requireConfig("school-oa.todo.update-url").replace("{uniqueId}", uniqueId);
String responseBody = sendTodoRequest(updateUrl, "学校OA待办完成失败", "学校OA待办完成异常");
log.info("学校OA待办完成成功,taskId={}receiveCode={}uniqueId={}response={}", task.getId(), actor.getActorAccount(), uniqueId, responseBody);
completeTodoByUniqueId(uniqueId);
log.info("学校OA待办完成成功,taskId={}receiveCode={}uniqueId={}", task.getId(), actor.getActorAccount(), uniqueId);
}
/**
@@ -399,79 +395,6 @@ public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
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) {
// 部分流程没有配置 H5 审核页,学校 OA 的移动端地址为空时按要求传固定域名兜底。
if (StrUtil.isBlank(h5FormKey)) {
@@ -107,6 +107,7 @@ public class WelfareSelectionSituationController {
}
@At
@Aop(TransAop.READ_COMMITTED)
@SLog(type = "welfare", tag = "选择情况", msg = "发送福利选择提醒")
@SaCheckPermission("welfare.selection.situation")
@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.params.ExcelExportEntity;
import cn.afterturn.easypoi.excel.export.ExcelExportService;
import cn.hutool.core.date.DateUtil;
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.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl;
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.sys.models.Sys_file;
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.sys.models.Sys_user;
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.WelfareProject;
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.WelfareUserSelection;
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.Sqls;
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.IocBean;
import org.nutz.lang.random.R;
import org.nutz.lang.util.NutMap;
import javax.imageio.ImageIO;
@@ -65,6 +76,8 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
private SmsService smsService;
@Inject
private SchoolOaTodoService schoolOaTodoService;
@Inject
private ManyAddOrRenewUtil manyAddOrRenewUtil;
@Override
public String getPreferredMobile(String userId) {
@@ -168,11 +181,13 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
}
@Override
@Aop(TransAop.READ_COMMITTED)
public NutMap sendReminder(WelfareSelectionSituationPageForm pageForm, String[] userIds, String content) {
List<String> recipientIds = getReminderRecipientIds(pageForm, userIds);
if (recipientIds.isEmpty()) {
return buildReminderResult(0, 0, 0, 0, 0,
Boolean.TRUE.equals(Globals.MyConfig.getBoolean("AppSchoolOa")));
return buildReminderResult(0, 0, 0, 0, 0, 0,
Boolean.TRUE.equals(Globals.MyConfig.getBoolean("AppSms")),
Boolean.TRUE.equals(Globals.MyConfig.getBoolean("AppSchoolOa")), 0, 0);
}
// 各消息渠道统一使用福利业务标题,学校OA再拼接自定义正文作为待办标题。
String title = "智慧工会职工福利:";
@@ -183,20 +198,41 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
localMessageSendStrategy.send(title, content, 2, recipientIds, null);
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 控制学校消息平台发送,消息链接跳转到手机端福利选择列表。
if (Boolean.TRUE.equals(Globals.MyConfig.getBoolean("AppSms")) && !receiverUsers.isEmpty()) {
smsService.massSendByUsers(receiverUsers, title, content, pcUrl, appUrl);
if (messageEnabled) {
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 existingTodoCount = 0;
int todoFailedCount = 0;
if (todoEnabled) {
Map<String, Sys_user> receiverUserMap = receiverUsers.stream()
.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) {
WelfareReminderTodo reminderTodo = getReminderTodo(pageForm.getProjectId(), recipientId);
WelfareReminderTodo reminderTodo = reminderTodoMap.get(recipientId);
if (isExistingReminderTodo(reminderTodo)) {
existingTodoCount++;
continue;
@@ -204,21 +240,15 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
Sys_user receiver = receiverUserMap.get(recipientId);
try {
// 发送前先落库为“发送中”,确保每一次实际发起的OA待办都可追溯
reminderTodo = saveSendingReminderTodo(reminderTodo, pageForm.getProjectId(), receiver,
SecurityUtil.getUserId(), title + content, content);
String uniqueId = schoolOaTodoService.createWelfareReminderTodo(pageForm.getProjectId(), title + content,
pcUrl, appUrl, SecurityUtil.getUserId(), recipientId);
if (StrUtil.isBlank(uniqueId)) {
throw new IllegalStateException("学校OA待办开关状态已变化,本次待办未发送");
// 仅在内存中组装记录,随后按“新增”和“重试”分别执行批量数据库写入
reminderTodo = prepareSendingReminderTodo(reminderTodo, pageForm.getProjectId(), receiver,
sender, title + content, content);
sendingReceiverIds.add(recipientId);
if (reminderTodoMap.containsKey(recipientId)) {
retrySendingReminderTodos.add(reminderTodo);
} 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) {
todoFailedCount++;
saveReminderTodoFailure(reminderTodo, pageForm.getProjectId(), recipientId, receiver,
@@ -227,12 +257,42 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
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);
}
int updatedCount = updateReminderTodoBatchStatus(pageForm.getProjectId(), sendingReceiverIds,
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(), todoSuccessCount,
existingTodoCount, todoFailedCount, todoEnabled);
return buildReminderResult(recipientIds.size(), recipientIds.size(), messageSuccessCount,
messageFailedCount, messageResultQueryFailedCount, todoSuccessCount,
messageEnabled, todoEnabled, existingTodoCount, todoFailedCount);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void completeReminderTodo(String projectId, String userId) {
WelfareReminderTodo reminderTodo = getReminderTodo(projectId, userId);
if (reminderTodo == null || !Objects.equals(reminderTodo.getTodoStatus(), WelfareReminderTodo.STATUS_PENDING)
@@ -240,7 +300,7 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
return;
}
try {
schoolOaTodoService.completeWelfareReminderTodo(reminderTodo.getOaUniqueId());
schoolOaTodoService.completeTodoByUniqueId(reminderTodo.getOaUniqueId());
reminderTodo.setTodoStatus(WelfareReminderTodo.STATUS_COMPLETED);
reminderTodo.setFinishTime(new Date());
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));
}
/**
* 批量查询当前项目的提醒待办,避免发送前按接收人逐条访问数据库。
*
* @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待办,重复提醒时不再创建第二条。
* 发送失败记录允许沿用原唯一值重试。
@@ -288,24 +569,31 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
}
/**
* 新建或重置待办发送记录。固定的流程ID和唯一值保证同一项目、同一用户最多一条OA待办
* 组装发送中的待办记录。新增记录补齐批量插入所需主键和审计字段,失败重试记录保留原主键
*/
private WelfareReminderTodo saveSendingReminderTodo(WelfareReminderTodo reminderTodo, String projectId,
Sys_user receiver, String senderId, String title, String content) {
private WelfareReminderTodo prepareSendingReminderTodo(WelfareReminderTodo reminderTodo, String projectId,
Sys_user receiver, Sys_user sender, String title, String content) {
if (receiver == null || StrUtil.hasBlank(receiver.getId(), receiver.getLoginname(), receiver.getUsername())) {
throw new IllegalArgumentException("学校OA福利选择提醒接收人信息不完整");
}
Sys_user sender = dao().fetch(Sys_user.class, senderId);
if (sender == null || StrUtil.hasBlank(sender.getLoginname(), sender.getUsername())) {
if (sender == null || StrUtil.hasBlank(sender.getId(), sender.getLoginname(), sender.getUsername())) {
throw new IllegalArgumentException("学校OA福利选择提醒创建人信息不完整");
}
boolean newRecord = reminderTodo == null;
WelfareReminderTodo record = newRecord ? new WelfareReminderTodo() : reminderTodo;
long now = System.currentTimeMillis();
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.setUserId(receiver.getId());
record.setOaFlowId(uniqueId);
record.setOaFlowId(buildWelfareReminderBatchFlowId(projectId));
record.setOaUniqueId(uniqueId);
record.setReceiverCode(receiver.getLoginname());
record.setReceiverName(receiver.getUsername());
@@ -318,14 +606,48 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
record.setSendTime(null);
record.setFinishTime(null);
record.setFailMessage(null);
if (newRecord) {
dao().insert(record);
} else {
dao().update(record);
}
record.setUpdatedBy(sender.getId());
record.setUpdatedAt(now);
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待办发送失败原因;若发送前校验失败导致记录尚未创建,则补建一条失败记录。
*/
@@ -336,7 +658,7 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
record = new WelfareReminderTodo();
record.setProjectId(projectId);
record.setUserId(userId);
record.setOaFlowId(buildWelfareReminderUniqueId(projectId, userId));
record.setOaFlowId(buildWelfareReminderBatchFlowId(projectId));
record.setOaUniqueId(buildWelfareReminderUniqueId(projectId, userId));
record.setSenderId(SecurityUtil.getUserId());
if (receiver != null) {
@@ -363,9 +685,21 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
/**
* 组装发送统计和前端提示,消息与OA待办分别计数,避免把重复跳过误报为发送成功。
*/
private NutMap buildReminderResult(int recipientCount, int messageSuccessCount, int todoSuccessCount,
int existingTodoCount, int todoFailedCount, boolean todoEnabled) {
StringBuilder summary = new StringBuilder("消息发送成功").append(messageSuccessCount).append("");
private NutMap buildReminderResult(int recipientCount, int localMessageSuccessCount, int messageSuccessCount,
int messageFailedCount, int messageResultQueryFailedCount, int todoSuccessCount,
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) {
summary.append("OA待办发送成功").append(todoSuccessCount).append("");
if (existingTodoCount > 0) {
@@ -379,7 +713,11 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
}
return NutMap.NEW()
.addv("recipientCount", recipientCount)
.addv("localMessageSuccessCount", localMessageSuccessCount)
.addv("messageSuccessCount", messageSuccessCount)
.addv("messageFailedCount", messageFailedCount)
.addv("messageResultQueryFailedCount", messageResultQueryFailedCount)
.addv("messageEnabled", messageEnabled)
.addv("todoSuccessCount", todoSuccessCount)
.addv("existingTodoCount", existingTodoCount)
.addv("todoFailedCount", todoFailedCount)
@@ -401,6 +739,65 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
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待办唯一值。 */
private String buildWelfareReminderUniqueId(String projectId, String userId) {
return "welfare_reminder_" + projectId + "_" + userId;
@@ -167,7 +167,7 @@ layout("/layouts/platform.html"){
<!-- </el-form-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-form-item label="入院时间" prop="hospitalizationTime">
<el-date-picker
@@ -23,7 +23,7 @@ const condolenceInfo = {
<!-- <el-descriptions-item label="收款账户">{{ viewData.bankCardNumber }}</el-descriptions-item>-->
<!-- <el-descriptions-item label="户名">{{ viewData.bankUserName }}</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.leaveHospitalTime }}</el-descriptions-item>
<el-descriptions-item label="当年第几次住院">{{ viewData.thisYearHospitalizationNum }}
@@ -279,7 +279,7 @@ layout("/layouts/platform_h5.html"){
<!-- name="bankOfDeposit"-->
<!-- ></van-field>-->
<template v-if="['1', '2'].includes(formData.typeCode)">
<template v-if="(chooseType.name || formData.typeName || '').includes('住院')">
<van-field
v-model="formData.hospitalizationTime"
name="hospitalizationTime"
@@ -23,7 +23,7 @@ const condolenceInfo = {
<!-- <van-cell class="direction-column-cell" title="开户行">-->
<!-- {{ viewData.bankOfDeposit || '暂无' }}-->
<!-- </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.leaveHospitalTime }}</van-cell>
<van-cell title="当年第几次住院(次)">{{ viewData.thisYearHospitalizationNum }}</van-cell>