Merge remote-tracking branch 'origin/main'

This commit is contained in:
zhouhefeng
2026-05-20 09:04:22 +08:00
96 changed files with 4040 additions and 678 deletions
@@ -1,39 +1,56 @@
package com.budwk.app.base.sms;
import com.budwk.app.base.sms.model.MsgPlatformResponse;
import com.budwk.app.base.sms.model.MsgPlatformSmsRequest;
import com.budwk.app.base.sms.model.MsgPlatformWechatRequest;
import java.util.List;
public interface SmsService {
/**
*
* @param loginName 工号
* @param content 内容
* 发送短信消息。
*
* @param request 短信请求参数,需传接收人、发送人、模板ID或消息内容等字段
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
void send(String loginName, String content);
MsgPlatformResponse sendSms(MsgPlatformSmsRequest request);
/**
* 单发
* @param loginName 工号
* @param title 标题
* @param content 内容
* 通过工号发送短信文本消息的便捷方法。
* controller 只需要传接收人工号,以及短信内容即可。
*
* @param account 接收人工号
* @param content 短信内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
void send(String loginName, String title, String content);
MsgPlatformResponse sendSmsByAccount(String account, String content);
/**
* 单发
* @param loginName 工号
* @param title 标题
* @param content 内容
* @param link 链接
* 通过手机号发送短信文本消息的便捷方法。
* controller 只需要传接收人手机号,以及短信内容即可。
*
* @param mobile 接收人手机号
* @param content 短信内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
void send(String loginName, String title, String content, String link);
MsgPlatformResponse sendSmsByMobile(String mobile, String content);
/**
* 群发 多人接收内容相同时使用该方法
* @param loginNames 工号
* @param title 标题
* @param content 内容
* @param link 链接
* 发送微信消息。
*
* @param request 微信请求参数,需传接收人、发送人、消息类型、模板ID或消息内容等字段
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
void massSend(List<String> loginNames, String title, String content, String link);
MsgPlatformResponse sendWechat(MsgPlatformWechatRequest request);
/**
* 发送微信文本消息的便捷方法。
* controller 只需要传接收人工号或手机号,以及微信文本内容即可。
*
* @param accountOrMobile 接收人工号或手机号
* @param content 微信文本内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
MsgPlatformResponse sendWechatTextByAccountOrMobile(String accountOrMobile, String content);
}
@@ -0,0 +1,544 @@
package com.budwk.app.base.sms.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.sms.SmsService;
import com.budwk.app.base.sms.model.*;
import com.budwk.app.web.commons.base.Globals;
import lombok.extern.slf4j.Slf4j;
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.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @ClassName SmsJshvcServiceImpl
* @Author JyuHsin
* @Date 2025/11/26 19:32
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean
public class SmsServiceImpl implements SmsService {
private static final Log log = Logs.get();
private static final String SMS_API_PATH = "/tp_mp/api/SmsService/saveSmsInfo";
private static final String WECHAT_API_PATH = "/tp_mp/api/WechatService/saveWechatInfo";
private static final int MAX_RECEIVER_COUNT = 500;
@Inject
private PropertiesProxy conf;
/**
* 发送短信消息。
*
* @param request 短信请求参数,需传接收人、发送人、模板ID或消息内容等字段
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
@Override
public MsgPlatformResponse sendSms(MsgPlatformSmsRequest request) {
validateCommonRequest(request);
JSONObject payload = buildCommonPayload(request, true);
return doPost(SMS_API_PATH, payload);
}
/**
* 通过工号发送短信文本消息的便捷方法。
* 该方法适合 controller 里只传“工号 + 内容”的简单场景,发送人信息从配置中读取。
*
* @param account 接收人工号
* @param content 短信内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
@Override
public MsgPlatformResponse sendSmsByAccount(String account, String content) {
MsgPlatformSmsRequest request = new MsgPlatformSmsRequest();
request.setInfo(content);
request.setRecipients(buildAccountRecipients(account));
fillDefaultSenderInfo(request);
return sendSms(request);
}
/**
* 通过手机号发送短信文本消息的便捷方法。
* 该方法适合 controller 里只传“手机号 + 内容”的简单场景,发送人信息从配置中读取。
*
* @param mobile 接收人手机号
* @param content 短信内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
@Override
public MsgPlatformResponse sendSmsByMobile(String mobile, String content) {
MsgPlatformSmsRequest request = new MsgPlatformSmsRequest();
request.setInfo(content);
request.setRecipients(buildMobileRecipients(mobile));
request.setCustomVar(Map.of("1","","2","","3",content));
fillDefaultSenderInfo(request);
return sendSms(request);
}
/**
* 发送微信消息。
*
* @param request 微信请求参数,需传接收人、发送人、消息类型、模板ID或消息内容等字段
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
@Override
public MsgPlatformResponse sendWechat(MsgPlatformWechatRequest request) {
validateCommonRequest(request);
if (Strings.isBlank(request.getWechatType())) {
request.setWechatType("text");
}
if ("news".equalsIgnoreCase(request.getWechatType()) && Strings.isBlank(request.getInfo())) {
if (request.getNewsItems() == null || request.getNewsItems().isEmpty()) {
throw new BaseException("微信图文消息必须传 info 或 newsItems");
}
request.setInfo(JSONUtil.toJsonStr(request.getNewsItems()));
}
JSONObject payload = buildCommonPayload(request, false);
payload.set("wechat_type", request.getWechatType());
return doPost(WECHAT_API_PATH, payload);
}
/**
* 发送微信文本消息的便捷方法。
* 该方法适合 controller 里只传“工号/手机号 + 内容”的简单场景,发送人信息从配置中读取。
*
* @param accountOrMobile 接收人工号或手机号
* @param content 微信文本内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
@Override
public MsgPlatformResponse sendWechatTextByAccountOrMobile(String accountOrMobile, String content) {
MsgPlatformWechatRequest request = new MsgPlatformWechatRequest();
request.setInfo(content);
request.setWechatType("text");
request.setRecipients(buildWechatRecipients(accountOrMobile));
fillDefaultSenderInfo(request);
return sendWechat(request);
}
/**
* 组装平台公共请求体。
*
* @param request 平台请求参数
* @param includeMobile 是否在 person_info 第 5 段写入手机号
* @return 返回发送给第三方平台的 JSON 对象
*/
private JSONObject buildCommonPayload(MsgPlatformBaseRequest request, boolean includeMobile) {
JSONObject payload = new JSONObject();
payload.set("tp_name", getRequiredConfig("msg.platform.tp-name", "请先配置 msg.platform.tp-name"));
payload.set("secret_key", getSecretKey());
payload.set("person_info", resolvePersonInfo(request, includeMobile));
payload.set("template_id", Strings.sNull(request.getTemplateId()));
payload.set("info", Strings.sNull(request.getInfo()));
payload.set("send_priority", Strings.isBlank(request.getSendPriority()) ? "3" : request.getSendPriority());
payload.set("send_user_id", Strings.sNull(request.getSendUserId()));
payload.set("send_user_name", Strings.sNull(request.getSendUserName()));
payload.set("send_time", Strings.sNull(request.getSendTime()));
payload.set("send_unit_id", Strings.sNull(request.getSendUnitId()));
payload.set("send_unit_name", Strings.sNull(request.getSendUnitName()));
payload.set("send_user_sign", Strings.sNull(request.getSendUserSign()));
payload.set("receipt_id", Strings.isBlank(request.getReceiptId()) ? "0" : request.getReceiptId());
if (request.getCustomVar() != null && !request.getCustomVar().isEmpty()) {
payload.set("custom_var", JSONUtil.toJsonStr(request.getCustomVar()));
}
return payload;
}
/**
* 校验公共请求参数,避免 controller 传错参数后才到第三方接口报错。
*
* @param request 平台请求参数
*/
private void validateCommonRequest(MsgPlatformBaseRequest request) {
if (request == null) {
throw new BaseException("请求参数不能为空");
}
if (Strings.isBlank(request.getPersonInfo()) && (request.getRecipients() == null || request.getRecipients().isEmpty())) {
throw new BaseException("personInfo 和 recipients 不能同时为空");
}
if (Strings.isBlank(request.getTemplateId()) && Strings.isBlank(request.getInfo())) {
throw new BaseException("templateId 和 info 至少需要传一个");
}
if (Strings.isBlank(request.getSendUserId())) {
throw new BaseException("sendUserId 不能为空");
}
if (Strings.isBlank(request.getSendUserName())) {
throw new BaseException("sendUserName 不能为空");
}
if (Strings.isBlank(request.getSendPriority())) {
request.setSendPriority("3");
}
if ("4".equals(request.getSendPriority()) && Strings.isBlank(request.getSendTime())) {
throw new BaseException("sendPriority=4 时必须传 sendTime");
}
validateReceiverCount(request.getPersonInfo(), request.getRecipients());
}
/**
* 解析 person_info。
* 已传原始 personInfo 时直接使用;否则根据 recipients 自动拼接平台要求的格式。
*
* @param request 平台请求参数
* @param includeMobile 是否在第 5 段写入手机号
* @return 平台要求的 person_info 字符串
*/
private String resolvePersonInfo(MsgPlatformBaseRequest request, boolean includeMobile) {
if (Strings.isNotBlank(request.getPersonInfo())) {
return request.getPersonInfo();
}
List<MsgPlatformRecipient> recipients = request.getRecipients();
if (recipients == null || recipients.isEmpty()) {
throw new BaseException("接收人列表不能为空");
}
return recipients.stream().map(recipient -> buildReceiverLine(recipient, includeMobile)).collect(Collectors.joining("^@^"));
}
/**
* 将单个接收人转成平台要求的分隔符格式。
*
* @param recipient 接收人对象
* @param includeMobile 是否在最后一段补手机号
* @return 单个接收人的 person_info 片段
*/
private String buildReceiverLine(MsgPlatformRecipient recipient, boolean includeMobile) {
if (recipient == null) {
throw new BaseException("接收人信息中存在空对象");
}
List<String> segments = new ArrayList<>(5);
segments.add(Strings.sNull(recipient.getName()));
segments.add(Strings.sNull(recipient.getAccount()));
segments.add(Strings.sNull(recipient.getUnitId()));
segments.add(Strings.sNull(recipient.getUnitName()));
if (includeMobile) {
segments.add(resolveSmsMobile(recipient));
} else {
segments.add("");
}
return String.join("|", segments);
}
/**
* 解析短信接收人手机号。
* 如果 controller 传的是手机号,则校验手机号格式;
* 如果传的是工号,则允许手机号为空,按“姓名|工号|部门ID|部门名称|”格式发送。
*
* @param recipient 接收人信息
* @return 短信 person_info 第 5 段内容
*/
private String resolveSmsMobile(MsgPlatformRecipient recipient) {
String mobile = recipient.getMobile();
if (Strings.isBlank(mobile)) {
if (Strings.isBlank(recipient.getAccount())) {
throw new BaseException("短信接收人手机号和工号不能同时为空");
}
return "";
}
if (!mobile.matches("^\\d{11}$")) {
throw new BaseException("短信接收人手机号必须为11位数字");
}
return mobile;
}
/**
* 校验接收人数量,避免超过第三方平台单次上限。
*
* @param personInfo 原始 person_info
* @param recipients 接收人列表
*/
private void validateReceiverCount(String personInfo, List<MsgPlatformRecipient> recipients) {
int receiverCount;
if (Strings.isNotBlank(personInfo)) {
receiverCount = personInfo.split("\\^@\\^", -1).length;
} else {
receiverCount = recipients == null ? 0 : recipients.size();
}
if (receiverCount > MAX_RECEIVER_COUNT) {
throw new BaseException("单次发送人数不能超过" + MAX_RECEIVER_COUNT + "");
}
}
/**
* 构建工号发送专用接收人列表。
*
* @param account controller 传入的工号
* @return 仅包含一个接收人的列表
*/
private List<MsgPlatformRecipient> buildAccountRecipients(String account) {
if (Strings.isBlank(account)) {
throw new BaseException("接收人工号不能为空");
}
MsgPlatformRecipient recipient = new MsgPlatformRecipient();
recipient.setAccount(account);
List<MsgPlatformRecipient> recipients = new ArrayList<>(1);
recipients.add(recipient);
return recipients;
}
/**
* 构建手机号发送专用接收人列表。
*
* @param mobile controller 传入的手机号
* @return 仅包含一个接收人的列表
*/
private List<MsgPlatformRecipient> buildMobileRecipients(String mobile) {
if (Strings.isBlank(mobile)) {
throw new BaseException("接收人手机号不能为空");
}
if (!mobile.matches("^\\d{11}$")) {
throw new BaseException("接收人手机号必须为11位数字");
}
MsgPlatformRecipient recipient = new MsgPlatformRecipient();
recipient.setMobile(mobile);
List<MsgPlatformRecipient> recipients = new ArrayList<>(1);
recipients.add(recipient);
return recipients;
}
/**
* 构建微信便捷发送专用接收人列表。
*
* @param accountOrMobile controller 传入的企业微信成员ID、工号或手机号
* @return 仅包含一个接收人的列表
*/
private List<MsgPlatformRecipient> buildWechatRecipients(String accountOrMobile) {
if (Strings.isBlank(accountOrMobile)) {
throw new BaseException("微信接收人工号或手机号不能为空");
}
MsgPlatformRecipient recipient = new MsgPlatformRecipient();
recipient.setAccount(accountOrMobile);
List<MsgPlatformRecipient> recipients = new ArrayList<>(1);
recipients.add(recipient);
return recipients;
}
/**
* 为便捷发送方法补默认发送人信息。
* 这些字段从配置中读取,避免 controller 每次重复传固定参数。
*
* @param request 平台请求参数
*/
private void fillDefaultSenderInfo(MsgPlatformBaseRequest request) {
request.setSendUserId(getRequiredConfig("msg.platform.default-send-user-id", "请先配置 msg.platform.default-send-user-id"));
request.setSendUserName(getRequiredConfig("msg.platform.default-send-user-name", "请先配置 msg.platform.default-send-user-name"));
request.setSendUnitId(conf.get("msg.platform.default-send-unit-id", ""));
request.setSendUnitName(conf.get("msg.platform.default-send-unit-name", ""));
request.setSendUserSign(conf.get("msg.platform.default-send-user-sign", ""));
request.setTemplateId(conf.get("msg.platform.default-template-id", ""));
}
/**
* 调用地大通讯平台 REST 接口。
*
* @param apiPath 具体接口路径
* @param payload 发送报文
* @return 平台响应结果
*/
private MsgPlatformResponse doPost(String apiPath, JSONObject payload) {
if (!Globals.sso){
log.error("【log】开发模式不允许发送消息通知");
MsgPlatformResponse response = new MsgPlatformResponse();
response.setHttpStatus(200);
response.setCode(0);
response.setResult(true);
response.setMsg("开发模式不允许发送消息通知");
response.setRawBody(JSONUtil.toJsonStr(payload));
return response;
}
if (!Globals.MyConfig.getBoolean("AppSms", false)) {
log.error("【log】短信配置未开启");
MsgPlatformResponse response = new MsgPlatformResponse();
response.setHttpStatus(200);
response.setCode(0);
response.setResult(true);
response.setMsg("短信配置未开启");
response.setRawBody(JSONUtil.toJsonStr(payload));
return response;
}
if (!conf.getBoolean("msg.platform.enabled", false)) {
MsgPlatformResponse response = new MsgPlatformResponse();
response.setHttpStatus(200);
response.setCode(0);
response.setResult(true);
response.setMsg("通讯平台未启用,已跳过实际发送");
response.setRawBody(JSONUtil.toJsonStr(payload));
return response;
}
HttpURLConnection connection = null;
try {
String requestBody = JSONUtil.toJsonStr(payload);
log.error("发送内容:=================================================="+requestBody);
String requestUrl = buildRequestUrl(apiPath);
log.warnf("发送地址:=================================================="+requestUrl);
connection = (HttpURLConnection) new URL(requestUrl).openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setUseCaches(false);
connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
connection.setConnectTimeout(conf.getInt("msg.platform.connect-timeout", 5000));
connection.setReadTimeout(conf.getInt("msg.platform.read-timeout", 10000));
try (OutputStream outputStream = connection.getOutputStream()) {
outputStream.write(requestBody.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}
int httpStatus = connection.getResponseCode();
String responseBody = readResponseBody(connection, httpStatus);
MsgPlatformResponse response = parseResponse(httpStatus, responseBody);
log.warnf("接口调用返回结果=============================="+responseBody);
if (response.getCode() != null && response.getCode() != 0) {
log.warnf("通讯平台调用失败, httpStatus=%s, body=%s", httpStatus, responseBody);
}
return response;
} catch (IOException e) {
log.error("调用通讯平台接口异常", e);
throw new BaseException("调用通讯平台接口失败: " + e.getMessage());
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
/**
* 构建完整请求地址,支持配置带或不带结尾斜杠的 base-url。
*
* @param apiPath 接口路径
* @return 完整请求地址
*/
private String buildRequestUrl(String apiPath) {
String baseUrl = getRequiredConfig("msg.platform.base-url", "请先配置 msg.platform.base-url");
if (baseUrl.endsWith("/")) {
return baseUrl.substring(0, baseUrl.length() - 1) + apiPath;
}
return baseUrl + apiPath;
}
/**
* 读取第三方接口响应报文。
*
* @param connection HttpURLConnection
* @param httpStatus HTTP 状态码
* @return 原始响应字符串
* @throws IOException 读取流失败时抛出
*/
private String readResponseBody(HttpURLConnection connection, int httpStatus) throws IOException {
InputStream inputStream = httpStatus >= 200 && httpStatus < 400 ? connection.getInputStream() : connection.getErrorStream();
if (inputStream == null) {
return "";
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
StringBuilder builder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
return builder.toString();
}
}
/**
* 解析平台响应体。
*
* @param httpStatus HTTP 状态码
* @param responseBody 原始响应字符串
* @return 解析后的平台响应对象
*/
private MsgPlatformResponse parseResponse(int httpStatus, String responseBody) {
MsgPlatformResponse response = new MsgPlatformResponse();
response.setHttpStatus(httpStatus);
response.setRawBody(responseBody);
if (Strings.isBlank(responseBody)) {
response.setCode(httpStatus);
response.setResult(false);
response.setMsg("通讯平台未返回响应内容");
return response;
}
if (!JSONUtil.isTypeJSON(responseBody)) {
response.setCode(httpStatus);
response.setResult(false);
response.setMsg("通讯平台返回内容不是合法JSON");
return response;
}
JSONObject jsonObject = JSONUtil.parseObj(responseBody);
response.setCode(jsonObject.getInt("code"));
response.setMsg(jsonObject.getStr("msg"));
response.setResult(jsonObject.getBool("result"));
response.setMsgId(jsonObject.getStr("msg_id"));
response.setRemainingQuota(jsonObject.getInt("remaining_quota"));
response.setDescription(jsonObject.getStr("description"));
return response;
}
/**
* 获取平台密钥。
* 优先读取已加密密钥;未配置时再根据原始密钥按“Base64 后 SHA”规则自动计算。
*
* @return 平台要求的 secret_key
*/
private String getSecretKey() {
String encryptedSecretKey = conf.get("msg.platform.encrypted-secret-key", "");
if (Strings.isNotBlank(encryptedSecretKey)) {
return encryptedSecretKey;
}
String rawSecret = getRequiredConfig("msg.platform.raw-secret-key", "请先配置 msg.platform.raw-secret-key 或 msg.platform.encrypted-secret-key");
return shaHex(rawSecret);
}
/**
* 读取必填配置项。
*
* @param key 配置 key
* @param errorMessage 配置缺失时抛出的异常信息
* @return 配置值
*/
private String getRequiredConfig(String key, String errorMessage) {
String value = conf.get(key, "");
if (Strings.isBlank(value)) {
throw new BaseException(errorMessage);
}
return value;
}
/**
* 生成 SHA 十六进制摘要。
*
* @param content 待加密内容
* @return SHA 十六进制字符串
*/
private String shaHex(String content) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA");
messageDigest.update(content.getBytes());
byte[] hash = messageDigest.digest();
return Base64.getEncoder().encodeToString(hash);
} catch (Exception e) {
throw new BaseException("生成通讯平台 secret_key 失败: " + e.getMessage());
}
}
}
@@ -1,150 +0,0 @@
package com.budwk.app.base.sms.impl.jshvc;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.budwk.app.base.sms.SmsService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.integration.jedis.RedisService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import java.util.List;
import java.util.Map;
/**
* @ClassName SmsJshvcServiceImpl
* @Author JyuHsin
* @Date 2025/11/26 19:32
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean
public class SmsJshvcServiceImpl implements SmsService {
private static final String APPID = "";
private static final String APP_SECRET = "";
private static final String TOKEN_URL = "";
private static final String MSG_URL = "";
private static final String REDIS_KEY_MSG_ACCESS_TOKEN = "msg:token:";
@Inject
private RedisService redisService;
@Override
public void send(String loginName, String content) {
// send(loginName, "智慧工会", content);
}
@Override
public void send(String loginName, String title, String content) {
send(loginName, "智慧工会", content, null);
}
@Override
public void send(String loginName, String title, String content, String link) {
// Map<String, String> paramMap = Map.of("userId", loginName);
// doSend(title, content, List.of(paramMap), link);
}
@Override
public void massSend(List<String> loginNames, String title, String content, String link) {
// List<Map<String, String>> receivers = loginNames.stream()
// .map(name -> Map.of("userId", name))
// .toList();
// doSend(title, content, receivers, link);
}
/**
* sign: 请求签名:accessToken + 第一个receivers 的userID )的32位小写的MD5加密值,其中如果相应部分没有则忽略
* msgType: 0: 普通消息(默认) 1: 必读消息 2: 验证码(为验证码时消息一定不入收件箱)
* expiredTime: 当msgType 为1必读消息 ,该字段为必填字段 格式:yyyy-MM-dd HH:mm:ss
* sendType: 1.只发送PC门户 2.只发送移动校园 3.邮件 4.短信 5.微信企业号 6.钉钉企业内部应用工作通知 7.微信服务号 8.welink
* wxSendType: 当发送类型为5时此字段才会生效:text(文本消息)、textcard(文本卡片)、nes(图文卡片,如果图文,则qyWeChatImgUrl必填)、button(按钮卡片详见示例),不传默认为text
* receiverType: 1:用户 2:用户组 3:部门,默认为1
*
* @param title
* @param content
* @param receivers
*/
private void doSend(String title, String content, List<Map<String, String>> receivers, String link) {
if (Lang.isEmpty(receivers)) {
log.error("send fail by receivers is null");
return;
}
// 获取token
String accessToken = buildAccessToken();
// 获取第一个接收人的userID
String firstUserId = receivers.get(0).get("userId");
// md5小写加密生成签名
String sign = DigestUtil.md5Hex(accessToken + firstUserId);
NutMap paramsMap = new NutMap();
paramsMap.put("sign", sign);
paramsMap.put("msgType", "0");
paramsMap.put("subject", title);
paramsMap.put("content", content);
paramsMap.put("sendType", "5");
paramsMap.put("receivers", receivers);
if (StrUtil.isBlank(link)) {
paramsMap.put("wxSendType", "text");
} else {
paramsMap.put("wxSendType", "textcard");
paramsMap.put("mobileUrl", link);
// 这是图文卡片,后面用到再对接吧
// paramsMap.put("qyWeChatImgUrl", "");
}
log.debug("send params: {}", Json.toJson(paramsMap));
HttpRequest request = HttpRequest.post(MSG_URL)
.header("appId", APPID)
.header("accessToken", accessToken)
.body(Json.toJson(paramsMap));
try {
HttpResponse response = request.execute();
log.info("send response body: {}", response.body());
NutMap bodyMap = Json.fromJson(NutMap.class, response.body());
if (bodyMap.getInt("status") == 200) {
log.info("send success code: {}, msg: {}", bodyMap.getInt("status"), bodyMap.getString("msg"));
} else {
log.error("send error code: {}, msg: {}", bodyMap.getInt("status"), bodyMap.getString("msg"));
}
} catch (Exception e) {
log.error("Failed to send: {}", e.getMessage(), e);
}
}
private String buildAccessToken() {
String token = redisService.get(REDIS_KEY_MSG_ACCESS_TOKEN);
if (StrUtil.isNotBlank(token)) {
return token;
}
HttpRequest request = HttpRequest.get(TOKEN_URL);
request.header("appId", APPID);
request.header("appSecret", APP_SECRET);
HttpResponse response = request.execute();
log.info("accessToken response: {}", Json.toJson(response.body()));
NutMap bodyMap = Json.fromJson(NutMap.class, response.body());
if (bodyMap.getInt("errcode") == 0) {
log.info("accessToken status: {}", bodyMap.getInt("errorcode"));
redisService.setex(REDIS_KEY_MSG_ACCESS_TOKEN, 60 * 120, bodyMap.getString("data"));
return bodyMap.getString("data");
} else {
log.error("accessToken status: {}", bodyMap.getInt("errorcode"));
}
return "";
}
}
@@ -0,0 +1,87 @@
package com.budwk.app.base.sms.model;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* 通讯平台公共请求参数。
*/
@Data
public class MsgPlatformBaseRequest {
/**
* 接收人信息原始字符串。
* 如果 controller 已经按平台要求拼好了 person_info,可以直接传该字段;
* 否则可只传 recipients,由 service 自动组装。
*/
private String personInfo;
/**
* 接收人列表。
* 当 personInfo 为空时,service 会根据该列表自动拼接平台要求的 person_info 字符串。
*/
private List<MsgPlatformRecipient> recipients;
/**
* 模板ID。
* templateId 和 info 至少需要传一个,使用模板发送时优先传该字段。
*/
private String templateId;
/**
* 消息内容。
* templateId 和 info 至少需要传一个;不使用模板时,需要传完整消息内容。
*/
private String info;
/**
* 发送优先级。
* 3 表示立即发送,4 表示定时发送。
*/
private String sendPriority = "3";
/**
* 发送人账号。
*/
private String sendUserId;
/**
* 发送人姓名。
*/
private String sendUserName;
/**
* 模板变量。
* 使用模板发送时可传该字段,service 会自动转成平台要求的 JSON 字符串。
*/
private Map<String, String> customVar;
/**
* 定时发送时间,格式必须为 yyyy-MM-dd HH:mm:ss。
* 当 sendPriority=4 时该字段必填。
*/
private String sendTime;
/**
* 发送机构ID。
*/
private String sendUnitId;
/**
* 发送机构名称。
*/
private String sendUnitName;
/**
* 发送签名。
* 旧通用模板或特定签名场景可传该字段。
*/
private String sendUserSign;
/**
* 回执ID。
* 不需要回执时可保持默认值 0。
*/
private String receiptId = "0";
}
@@ -0,0 +1,29 @@
package com.budwk.app.base.sms.model;
import lombok.Data;
/**
* 微信图文消息单条数据。
*/
@Data
public class MsgPlatformNewsItem {
/**
* 图文标题。
*/
private String title;
/**
* 图文描述。
*/
private String description;
/**
* 点击跳转链接。
*/
private String url;
/**
* 图文封面图片链接。
*/
private String picurl;
}
@@ -0,0 +1,36 @@
package com.budwk.app.base.sms.model;
import lombok.Data;
/**
* 通讯平台接收人信息。
*/
@Data
public class MsgPlatformRecipient {
/**
* 接收人姓名,对应 person_info 中的第 1 段。
*/
private String name;
/**
* 接收人账号,对应 person_info 中的第 2 段。
* 短信场景一般传学工号;微信场景可传企业微信成员ID、学工号或手机号。
*/
private String account;
/**
* 接收人部门ID,对应 person_info 中的第 3 段。
*/
private String unitId;
/**
* 接收人部门名称,对应 person_info 中的第 4 段。
*/
private String unitName;
/**
* 接收人手机号,对应短信 person_info 中的第 5 段。
* 微信接口不会使用该字段,微信接口会自动补空串以保留分隔符格式。
*/
private String mobile;
}
@@ -0,0 +1,49 @@
package com.budwk.app.base.sms.model;
import lombok.Data;
/**
* 通讯平台响应结果。
*/
@Data
public class MsgPlatformResponse {
/**
* HTTP 状态码,便于 controller 定位网络层问题。
*/
private Integer httpStatus;
/**
* 平台返回业务状态码,0 表示成功。
*/
private Integer code;
/**
* 平台返回消息提示。
*/
private String msg;
/**
* 平台返回业务是否成功。
*/
private Boolean result;
/**
* 发送成功后的消息ID。
*/
private String msgId;
/**
* 短信剩余额度,仅短信接口返回。
*/
private Integer remainingQuota;
/**
* 平台失败描述。
*/
private String description;
/**
* 平台原始响应报文,排查问题时可直接查看。
*/
private String rawBody;
}
@@ -0,0 +1,7 @@
package com.budwk.app.base.sms.model;
/**
* 短信发送请求参数。
*/
public class MsgPlatformSmsRequest extends MsgPlatformBaseRequest {
}
@@ -0,0 +1,25 @@
package com.budwk.app.base.sms.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 微信发送请求参数。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MsgPlatformWechatRequest extends MsgPlatformBaseRequest {
/**
* 微信消息类型。
* 可传 text、news、image、file、video、voice,默认 text。
*/
private String wechatType = "text";
/**
* 图文消息列表。
* 当 wechatType=news 且未直接传 info 时,service 会将该列表转成平台要求的 JSON 数组字符串。
*/
private List<MsgPlatformNewsItem> newsItems;
}
@@ -26,44 +26,45 @@ import java.util.List;
*/
public class FlowUnitPartySecretaryHandler implements AssignmentHandler {
@Override
public List<String> assign(TaskModel model, Execution execution) {
String unitId = SecurityUtil.getUnitId();
@Override
public List<String> assign(TaskModel model, Execution execution) {
String unitId = SecurityUtil.getUnitId();
SysRoleService sysRoleService = ServiceContext.find(SysRoleService.class);
Sys_role role = sysRoleService.getByCode(RoleConstant.UNIT_PARTY_SECRETARY);
SysRoleService sysRoleService = ServiceContext.find(SysRoleService.class);
Sys_role role = sysRoleService.getByCode(RoleConstant.UNIT_PARTY_SECRETARY);
View_user user = ServiceContext.find(Dao.class).fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId()));
List<Sys_user_role> roles;
if(user.getUnionName().contains("行政")){
List<Sys_unit> unitList = ServiceContext.find(Dao.class).query(Sys_unit.class, Cnd.where(Sys_unit::getUnionId, "=", user.getUnionId()));
List<String> unitIds = unitList.stream().map(Sys_unit::getId).toList();
roles = ServiceContext.find(Dao.class).query(
Sys_user_role.class,
Cnd.where(Sys_user_role::getRoleId, "=", role.getId())
.and(Sys_user_role::getUnitId, "in", unitIds)
);
}else{
roles = ServiceContext.find(Dao.class).query(
Sys_user_role.class,
Cnd.where(Sys_user_role::getRoleId, "=", role.getId())
.and(Sys_user_role::getUnitId, "=", unitId)
);
}
View_user user = ServiceContext.find(Dao.class).fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId()));
List<Sys_user_role> roles;
if (Lang.isEmpty(roles)) {
throw new RuntimeException("当前登录用户所在单位未设置单位党委书记,请联系校工会进行设置。");
}
return roles.stream().map(Sys_user_role::getUserId).toList();
}
roles = ServiceContext.find(Dao.class).query(
Sys_user_role.class,
Cnd.where(Sys_user_role::getRoleId, "=", role.getId())
.and(Sys_user_role::getUnitId, "=", unitId)
);
@Override
public String getMessage() {
return "获取当前登录用户所在单位党委书记,若是行政就找出行政分下的党委书记。因为行政只有一个党委书记";
}
if (Lang.isEmpty(roles)) {
List<Sys_unit> unitList = ServiceContext.find(Dao.class).query(Sys_unit.class, Cnd.where(Sys_unit::getUnionId, "=", user.getUnionId()));
List<String> unitIds = unitList.stream().map(Sys_unit::getId).toList();
roles = ServiceContext.find(Dao.class).query(
Sys_user_role.class,
Cnd.where(Sys_user_role::getRoleId, "=", role.getId())
.and(Sys_user_role::getUnitId, "in", unitIds)
);
}
@Override
public int getOrder() {
return AssignmentHandler.super.getOrder();
}
if (Lang.isEmpty(roles)) {
throw new RuntimeException("当前登录用户所在单位未设置单位党委书记,请联系校工会进行设置。");
}
return roles.stream().map(Sys_user_role::getUserId).toList();
}
@Override
public String getMessage() {
return "获取当前登录用户所在单位党委书记";
}
@Override
public int getOrder() {
return AssignmentHandler.super.getOrder();
}
}
@@ -25,12 +25,15 @@ import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import io.swagger.annotations.ApiOperation;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
@@ -146,10 +149,20 @@ public class SysUnitController {
@At
@ApiOperation("按关键字查询单位下的人员")
@Ok("json:{ignoreNull:true}")
public Result listUserSelect(@Valid String unitId, @Valid String keyWord) {
public Result listUserSelect(@Valid String unitId, @Valid String keyWord, @Valid String roleCode) {
Sql sql = Sqls.create("select id,loginname,username,unitname from vw_user $condition");
Cnd cnd = Cnd.NEW();
cnd.and(View_user::getUnitId, "=", unitId);
// 选择校领导角色时,人员范围来自提案配置中的校领导单位;其他角色仍限定当前单位。
if ("UNIT_SCHOOL_LEADER".equals(roleCode)) {
ProposalConfig proposalConfig = sysUnitService.dao().fetch(ProposalConfig.class, Cnd.NEW());
if (proposalConfig == null || Lang.isEmpty(proposalConfig.getSchoolLeaderUnitIds())) {
return Result.error("请在提案基础设置里配置校领导单位!");
}
cnd.and(View_user::getUnitId, "in", proposalConfig.getSchoolLeaderUnitIds());
} else {
cnd.and(View_user::getUnitId, "=", unitId);
}
cnd.and(View_user::getMember, "=", 1);
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.orLike(View_user::getLoginname, keyWord, true);
seg.orLike(View_user::getUsername, keyWord, true);
@@ -165,13 +178,14 @@ public class SysUnitController {
@At
@Ok("json")
@Aop(TransAop.READ_COMMITTED)
public Result insertUnitUserRole(String userId, String roleCode, String unitId) {
Sys_role role = sysRoleService.fetch(Cnd.where("code", "=", roleCode));
if (Lang.isEmpty(role)) {
throw new BaseException("无法找到{}对应编码的角色", roleCode);
}
sysRoleService.clear("sys_user_role", Cnd.where("roleId", "=", role.getId()).and("userId", "=", userId).and("unitId", "=", unitId));
sysRoleService.insert("sys_user_role", Chain.make("roleId", role.getId()).add("userId", userId).add("unitId", unitId));
sysRoleService.clear("sys_user_role", Cnd.where("roleId", "=", role.getId()).and("userId", "=", userId).and("unitId", "=", unitId).and("underTakeId", "=", unitId));
sysRoleService.insert("sys_user_role", Chain.make("roleId", role.getId()).add("userId", userId).add("unitId", unitId).add("underTakeId", unitId));
sysRoleService.clearCache();
sysUserService.clearCache();
return Result.success();
@@ -179,12 +193,13 @@ public class SysUnitController {
@At
@Ok("json")
@Aop(TransAop.READ_COMMITTED)
public Result deleteUnitUserRole(String userId, String roleCode, String unitId) {
Sys_role role = sysRoleService.fetch(Cnd.where("code", "=", roleCode));
if (Lang.isEmpty(role)) {
throw new BaseException("无法找到{}对应编码的角色", roleCode);
}
sysRoleService.clear("sys_user_role", Cnd.where("roleId", "=", role.getId()).and("userId", "=", userId).and("unitId", "=", unitId));
sysRoleService.clear("sys_user_role", Cnd.where("roleId", "=", role.getId()).and("userId", "=", userId).and("unitId", "=", unitId).and("underTakeId", "=", unitId));
sysRoleService.clearCache();
sysUserService.clearCache();
return Result.success();
@@ -6,6 +6,7 @@ import cn.hutool.http.HtmlUtil;
import com.budwk.app.base.constant.RedisConstant;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.sms.SmsService;
import com.budwk.app.base.sms.model.MsgPlatformWechatRequest;
import com.budwk.app.base.utils.ManyAddOrRenewUtil;
import com.budwk.app.sys.models.Sys_msg;
import com.budwk.app.sys.models.Sys_msg_user;
@@ -102,11 +103,12 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
});
//发送学校平台消息
// for (String loginName : loginNames) {
// smsService.send(loginName, sysMsg.getTitle(), sysMsg.getNote());
// }
ThreadUtil.execute(() -> {
//smsService.massSend(loginNames, sysMsg.getTitle(), HtmlUtil.cleanHtmlTag(StrUtil.blankToDefault(sysMsg.getNote(),"")), sysMsg.getUrl());
for (String loginName : loginNames) {
smsService.sendSmsByAccount(loginName,sysMsg.getNote());
smsService.sendWechatTextByAccountOrMobile(loginName,sysMsg.getNote());
}
});
return sysMsg;
@@ -24,6 +24,8 @@ import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope;
import com.budwk.app.zhgh.activity.basic.param.ActivityUserScopePageParam;
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
import com.budwk.app.zhgh.activity.basic.template.UserTemp;
import com.budwk.app.zhgh.club.model.SysClub;
import com.budwk.app.zhgh.club.service.SysClubService;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
@@ -84,6 +86,9 @@ public class ActivityBasicScopeController {
@Inject
private RedisService redisService;
@Inject
private SysClubService sysClubService;
@At("")
@Ok("beetl:platform/zhgh/activity/basic/userScope/index.html")
@SaCheckPermission("activity.basic.scope")
@@ -97,7 +102,6 @@ public class ActivityBasicScopeController {
* @return {@link Result}
*/
@At
@SaCheckPermission("activity.basic")
public Result pageData(@Param("data") ActivityUserScopePageParam pageForm) {
Sql sql = Sqls.create("""
SELECT
@@ -216,6 +220,8 @@ public class ActivityBasicScopeController {
return Result.success(new NutMap() {{
addv("is_A06", StpUtil.hasRole(RoleConstant.SCHOOL_UNION_ADMIN.name()));
addv("is_H04", StpUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name()));
addv("is_H02", StpUtil.hasRole(RoleConstant.SCHOOL_UNION_CLUB_ADMIN.name()));
addv("is_CLUB_PRESIDENT", StpUtil.hasRole(RoleConstant.CLUB_PRESIDENT.name()));
addv("is_sysadmin", StpUtil.hasRole(RoleConstant.SYSADMIN.name()));
addv("unionid", SecurityUtil.getUnionId());
}});
@@ -344,9 +350,30 @@ public class ActivityBasicScopeController {
}
try {
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())){
cnd.andEX("u.unionid", "=", SecurityUtil.getUnionId());
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
boolean isBranchUnionChairman = AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name());
boolean isClubPresident = AuthUtil.hasRole(RoleConstant.CLUB_PRESIDENT.name());
if (isBranchUnionChairman && isClubPresident) {
// 同时具备分工会主席和社团会长身份时,未额外筛选前默认查看两类权限范围的并集。
List<String> manageClubIdList = sysClubService.getMyManageClub().stream().map(SysClub::getId).toList();
SqlExpressionGroup scopeGroup = new SqlExpressionGroup();
scopeGroup.or("u.unionid", "=", SecurityUtil.getUnionId());
if (!manageClubIdList.isEmpty()) {
scopeGroup.or("clubuser.clubid", "in", manageClubIdList);
}
cnd.and(scopeGroup);
} else if (isClubPresident) {
// 社团会长设置活动人员范围时,只允许查看自己可管理社团下的成员。
List<String> manageClubIdList = sysClubService.getMyManageClub().stream().map(SysClub::getId).toList();
if (manageClubIdList.isEmpty()) {
cnd.and("clubuser.clubid", "in", List.of("__EMPTY_CLUB_ID__"));
} else {
cnd.and("clubuser.clubid", "in", manageClubIdList);
}
} else {
// 分工会主席等分工会侧角色,继续只允许查看本分工会人员。
cnd.andEX("u.unionid", "=", SecurityUtil.getUnionId());
}
}
String[] unionIds = getUnionIds(activityUserScopePageParam);
if (Lang.isNotEmpty(unionIds)) {
@@ -21,6 +21,7 @@ import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
@@ -125,8 +126,17 @@ public class ActivityCultureApplyUserController {
public Result queryTeammate(@Valid String keyword, Integer signUpMethod, String tissueId) {
Sql sql = Sqls.create("select id userId,username userName,loginname loginName,sex,mobile,unitName from vw_user $condition limit 0,10");
Cnd cnd = Cnd.NEW();
cnd.or(Cnd.likeEX("username", keyword));
cnd.or(Cnd.likeEX("loginname", keyword));
SqlExpressionGroup keywordGroup = new SqlExpressionGroup();
keywordGroup.andLike("username", keyword);
keywordGroup.orLike("loginname", keyword);
cnd.and(keywordGroup);
if (StrUtil.isNotBlank(tissueId)) {
ActivityTissue tissue = dao.fetch(ActivityTissue.class, tissueId);
// 组队/分工会报名选人时,候选人必须落在活动新建时配置的参加人员范围内。
if (ObjectUtil.isNotNull(tissue) && ObjectUtil.isNotNull(tissue.getGroupId())) {
cnd.and("id", "in", activityBasicScopeService.buildGroupUserIdSubSql(tissue.getGroupId()));
}
}
if (signUpMethod == 3) {
cnd.and("unionid", "=", SecurityUtil.getUnionId());
}
@@ -112,7 +112,7 @@ public class ActivityCultureAuditActivityController {
cnd.andEX("tissue.unionId", "=", unionId);
cnd.andEX("tissue.activity_type", "=", activity_type);
if (approval) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
@@ -161,6 +161,11 @@ public class ActivityCultureUserStatisticsController {
cnd.and("tissue.signUpMethod", "in", List.of(1, 2, 3));
if (activity_type == 40002) {
cnd.and("tissue.unionId", "=", SecurityUtil.getUnionId());
} else if (activity_type == 40003) {
// 协会会长进入社团报名统计时,只允许查看自己管理社团下的活动名称。
List<SysClub> myManageClub = sysClubService.getMyManageClub();
List<String> clubIdList = myManageClub.stream().map(SysClub::getId).collect(Collectors.toList());
cnd.and("tissue.clubId", "in", clubIdList);
}
}
// 只查询流程实例状态为20的数据(已完成状态)
@@ -169,34 +169,35 @@ public class ActivityPrizeListController {
Map fourMap = NutMap.NEW();
Map fiveMap = NutMap.NEW();
// 部分历史数据可能未配置竞赛类别,分组汇总时需要做空值保护,避免筛选时空指针
//甲组
List<NutMap> oneList = allList.stream().filter(a -> a.getString("bszbName").equals("甲组")).collect(toList());
List<NutMap> oneList = allList.stream().filter(a -> Objects.equals("甲组", a.getString("bszbName"))).collect(toList());
if (Lang.isNotEmpty(oneList)) {
oneMap = oneList.stream().collect(Collectors.groupingBy(v -> v.getString("allName")));
}
//乙组
List<NutMap> twoList = allList.stream().filter(a -> a.getString("bszbName").equals("乙组")).collect(toList());
List<NutMap> twoList = allList.stream().filter(a -> Objects.equals("乙组", a.getString("bszbName"))).collect(toList());
if (Lang.isNotEmpty(twoList)) {
twoMap = twoList.stream().collect(Collectors.groupingBy(v -> v.getString("allName")));
}
//丙组
List<NutMap> threeList = allList.stream().filter(a -> a.getString("bszbName").equals("丙组")).collect(toList());
List<NutMap> threeList = allList.stream().filter(a -> Objects.equals("丙组", a.getString("bszbName"))).collect(toList());
if (Lang.isNotEmpty(threeList)) {
threeMap = threeList.stream().collect(Collectors.groupingBy(v -> v.getString("allName")));
}
//丁组
List<NutMap> fourList = allList.stream().filter(a -> a.getString("bszbName").equals("丁组")).collect(toList());
List<NutMap> fourList = allList.stream().filter(a -> Objects.equals("丁组", a.getString("bszbName"))).collect(toList());
if (Lang.isNotEmpty(fourList)) {
fourMap = fourList.stream().collect(Collectors.groupingBy(v -> v.getString("allName")));
}
//团体
List<NutMap> fiveList = allList.stream().filter(a -> a.getString("bszbName").equals("团体")).collect(toList());
List<NutMap> fiveList = allList.stream().filter(a -> Objects.equals("团体", a.getString("bszbName"))).collect(toList());
if (Lang.isNotEmpty(fiveList)) {
fiveMap = fiveList.stream().collect(Collectors.groupingBy(v -> v.getString("allName")));
}
@@ -2,6 +2,7 @@ package com.budwk.app.zhgh.activity.sports.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
@@ -9,6 +10,8 @@ import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnion;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnit;
import com.budwk.app.zhgh.activity.sports.models.ActivitySchool;
import com.budwk.app.zhgh.activity.sports.service.ActivitySportsApplyUserService;
import com.budwk.app.zhgh.activity.sports.service.ActivitySportsService;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
@@ -19,7 +22,12 @@ import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import org.nutz.mvc.annotation.AdaptBy;
import org.nutz.mvc.upload.TempFile;
import org.nutz.mvc.upload.UploadAdaptor;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@@ -45,6 +53,10 @@ public class ActivitySportsReadingController {
public Dao dao;
@Inject
public BaseService baseService;
@Inject
private ActivitySportsService activitySportsService;
@Inject
private ActivitySportsApplyUserService activitySportsApplyUserService;
@At
@SaCheckPermission("activity.reading")
@@ -150,5 +162,38 @@ public class ActivitySportsReadingController {
return Result.success(apply);
}
@At
@Ok("void")
@SaCheckPermission("activity.reading")
public void downloadImportTemplate(HttpServletResponse response) {
checkManageRole();
activitySportsApplyUserService.downloadImportTemplate(response);
}
@At
@SaCheckPermission("activity.reading")
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
public Result importUserActivitys(@Param("file") TempFile file, @Param("businessId") String activityId) {
checkManageRole();
return Result.success(activitySportsApplyUserService.importUserActivitys(file, activityId));
}
@At
@Ok("void")
@SaCheckPermission("activity.reading")
public void doExportByEnroll(String id, String unionId, HttpServletResponse response) {
checkManageRole();
activitySportsService.exportXlsx(id, unionId, response);
}
/**
* 参考原项目,导入和整表导出只开放给校工会管理员和系统管理员,避免普通阅览权限误操作。
*/
private void checkManageRole() {
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
throw new BaseException("您没有操作权限");
}
}
}
@@ -6,7 +6,9 @@ import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.activity.sports.models.ActivitySchoolApply;
import com.budwk.app.zhgh.activity.sports.param.ActivitySportsApplyUserPageParam;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.upload.TempFile;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
public interface ActivitySportsApplyUserService extends BaseService<ActivitySchoolApply> {
@@ -31,4 +33,20 @@ public interface ActivitySportsApplyUserService extends BaseService<ActivityScho
List<NutMap> getUserUnion(String query);
/**
* 下载报名人员导入模板。
*
* @param response 响应流
*/
void downloadImportTemplate(HttpServletResponse response);
/**
* 导入报名人员并返回错误明细,全部成功时返回 null。
*
* @param file 导入文件
* @param activityId 活动ID
* @return 导入错误统计
*/
NutMap importUserActivitys(TempFile file, String activityId);
}
@@ -1,22 +1,33 @@
package com.budwk.app.zhgh.activity.sports.service.impl;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.activity.basic.models.ActivityEvent;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnion;
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnit;
import com.budwk.app.zhgh.activity.sports.models.ActivitySchool;
import com.budwk.app.zhgh.activity.sports.models.ActivitySchoolApply;
import com.budwk.app.zhgh.activity.sports.models.ActivitySchoolEvent;
import com.budwk.app.zhgh.activity.sports.models.ActivitySchoolTeam;
import com.budwk.app.zhgh.activity.sports.param.ActivitySportsApplyUserPageParam;
import com.budwk.app.zhgh.activity.sports.service.ActivitySportsApplyUserService;
import com.budwk.app.zhgh.activity.sports.template.ActivitySportsApplyImportTemp;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
@@ -31,7 +42,9 @@ import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.upload.TempFile;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -487,4 +500,224 @@ public class ActivitySportsApplyUserServiceImpl extends BaseServiceImpl<Activity
sql.setCondition(cnd);
return listMap(sql);
}
@Override
public void downloadImportTemplate(HttpServletResponse response) {
try {
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, ActivitySportsApplyImportTemp.class, new ArrayList<>());
CommonDownloadUtil.download("报名人员导入模板.xlsx", workbook, response);
} catch (Exception e) {
throw new BaseException("下载导入模板失败");
}
}
@Override
@Aop(TransAop.READ_COMMITTED)
public NutMap importUserActivitys(TempFile file, String activityId) {
if (file == null) {
throw new BaseException("请选择导入文件");
}
if (StrUtil.isBlank(activityId)) {
throw new BaseException("请先选择活动名称");
}
ActivitySchool activitySchool = dao().fetch(ActivitySchool.class, activityId);
if (activitySchool == null) {
throw new BaseException("活动不存在");
}
try {
List<ActivitySportsApplyImportTemp> importList = ExcelImportUtil.importExcel(file.getFile(), ActivitySportsApplyImportTemp.class, new ImportParams());
if (Lang.isEmpty(importList)) {
return null;
}
List<ActivitySportsApplyImportTemp> errorList = new ArrayList<>();
int totalCount = 0;
int successCount = 0;
String currentUnionId = getUnionId();
for (ActivitySportsApplyImportTemp importTemp : importList) {
totalCount++;
String loginName = normalizeLoginName(importTemp.getLoginName());
if (StrUtil.isBlank(loginName)) {
importTemp.setNotes("工号不能为空");
errorList.add(importTemp);
continue;
}
Sys_user sysUser = dao().fetch(Sys_user.class, Cnd.where("loginname", "=", loginName));
if (sysUser == null) {
importTemp.setNotes("此用户不存在,请检查工号");
errorList.add(importTemp);
continue;
}
ActivityBasicUnit basicUnit = dao().fetch(ActivityBasicUnit.class, Cnd.where("id", "=", sysUser.getUnitId()));
if (basicUnit == null) {
importTemp.setNotes("请检查该人员活动单位是否配置正确");
errorList.add(importTemp);
continue;
}
ActivityBasicUnion basicUnion = dao().fetch(ActivityBasicUnion.class, Cnd.where("id", "=", basicUnit.getUnionId()));
if (basicUnion == null) {
importTemp.setNotes("请检查该人员所属活动工会是否配置正确");
errorList.add(importTemp);
continue;
}
if (!hasImportAuth() && !basicUnion.getId().equals(currentUnionId)) {
importTemp.setNotes("此用户不是当前工会人员");
errorList.add(importTemp);
continue;
}
String identityName = normalizeIdentityName(importTemp.getIdentity());
String projectCode = normalizeText(importTemp.getProjectCode());
if (StrUtil.isBlank(projectCode) && StrUtil.isBlank(identityName)) {
importTemp.setNotes("项目编号和身份不能同时为空");
errorList.add(importTemp);
continue;
}
ActivitySchoolApply activitySchoolApply = buildBaseApply(activityId, sysUser, basicUnit, basicUnion);
if (StrUtil.isNotBlank(identityName)) {
fillIdentityInfo(activitySchoolApply, identityName);
}
if (StrUtil.isBlank(projectCode) && StrUtil.isNotBlank(identityName)) {
replaceUnionRoleUser(activitySchoolApply, identityName);
insert(activitySchoolApply);
successCount++;
continue;
}
ActivityEvent activityEvent = dao().fetch(ActivityEvent.class, Cnd.where("projectCode", "=", projectCode));
if (activityEvent == null) {
importTemp.setNotes("项目编号不存在,请检查后重试");
errorList.add(importTemp);
continue;
}
ActivitySchoolEvent schoolEvent = dao().fetch(ActivitySchoolEvent.class, Cnd.where("activityId", "=", activityId).and("eventId", "=", activityEvent.getId()));
if (schoolEvent == null) {
importTemp.setNotes("该项目不属于当前活动,请检查项目编号");
errorList.add(importTemp);
continue;
}
activitySchoolApply.setEventId(schoolEvent.getEventId());
activitySchoolApply.setAwardsMode(activityEvent.getProjectType());
if ("2".equals(activityEvent.getProjectType())) {
ActivitySchoolTeam schoolTeam = dao().fetch(ActivitySchoolTeam.class, Cnd.where("activityId", "=", activityId).and("eventId", "=", schoolEvent.getEventId()).asc("location"));
if (schoolTeam == null) {
importTemp.setNotes("团体项目未配置队伍信息,请先维护小队");
errorList.add(importTemp);
continue;
}
activitySchoolApply.setTeamId(schoolTeam.getId());
activitySchoolApply.setTeam(schoolTeam.getName());
}
// 兼容参考项目导入逻辑:带项目编号的记录按运动员处理。
activitySchoolApply.setIdentity(List.of("1"));
clear(Cnd.where("activityId", "=", activityId).and("userId", "=", sysUser.getId()).and("eventId", "=", schoolEvent.getEventId()));
insert(activitySchoolApply);
successCount++;
}
if (Lang.isNotEmpty(errorList)) {
NutMap result = NutMap.NEW();
result.setv("totalCount", totalCount);
result.setv("successCount", successCount);
result.setv("errorCount", errorList.size());
result.setv("errorList", errorList.stream().map(v -> NutMap.NEW()
.addv("工会", normalizeText(v.getUnionName()))
.addv("工号", normalizeText(v.getLoginName()))
.addv("姓名", normalizeText(v.getUserName()))
.addv("性别", normalizeText(v.getSex()))
.addv("项目编号", normalizeText(v.getProjectCode()))
.addv("身份", normalizeText(v.getIdentity()))
.addv("错误信息", normalizeText(v.getNotes()))).collect(Collectors.toList()));
return result;
}
return null;
} catch (Exception e) {
throw new BaseException("导入报名人员失败:" + e.getMessage());
}
}
/**
* 导入记录统一补齐报名基础字段,避免多入口写入时字段不一致。
*/
private ActivitySchoolApply buildBaseApply(String activityId, Sys_user sysUser, ActivityBasicUnit basicUnit, ActivityBasicUnion basicUnion) {
ActivitySchoolApply activitySchoolApply = new ActivitySchoolApply();
activitySchoolApply.setActivityId(activityId);
activitySchoolApply.setUnitId(basicUnit.getId());
activitySchoolApply.setUnionId(basicUnion.getId());
activitySchoolApply.setApplyUser(SecurityUtil.getUserId());
activitySchoolApply.setUserId(sysUser.getId());
activitySchoolApply.setLoginname(sysUser.getLoginname());
activitySchoolApply.setUsername(sysUser.getUsername());
activitySchoolApply.setSex(sysUser.getSex());
activitySchoolApply.setMobile(sysUser.getMobile());
activitySchoolApply.setProfessionalLevel(sysUser.getProfessionalLevel());
activitySchoolApply.setBirthday(sysUser.getBirthday() == null ? null : DateUtil.formatDate(sysUser.getBirthday()));
activitySchoolApply.setApplyDate(DateUtil.now());
activitySchoolApply.setStatus(2);
activitySchoolApply.setUnitname(basicUnit.getName());
activitySchoolApply.setActivityUnionId(basicUnion.getId());
activitySchoolApply.setActivityUnionName(basicUnion.getName());
return activitySchoolApply;
}
/**
* 角色类导入只保留当前活动当前工会当前人员的一条对应身份记录,防止重复导入产生脏数据。
*/
private void replaceUnionRoleUser(ActivitySchoolApply activitySchoolApply, String identityName) {
Cnd cnd = Cnd.where("activityId", "=", activitySchoolApply.getActivityId())
.and("activityUnionId", "=", activitySchoolApply.getActivityUnionId())
.and("userId", "=", activitySchoolApply.getUserId());
if ("教练".equals(identityName)) {
cnd.and("unionCoach", "=", true);
} else if ("领队".equals(identityName)) {
cnd.and("unionLeader", "=", true);
} else if ("团长".equals(identityName)) {
cnd.and("unionHead", "=", true);
} else if ("工作人员".equals(identityName)) {
cnd.and("unionStaff", "=", true);
}
clear(cnd);
}
/**
* 统一处理导入身份映射,保证页面展示、导出与后续查询读取到一致的身份字段。
*/
private void fillIdentityInfo(ActivitySchoolApply activitySchoolApply, String identityName) {
if ("教练".equals(identityName)) {
activitySchoolApply.setUnionCoach(true);
activitySchoolApply.setIdentity(List.of("2"));
} else if ("领队".equals(identityName)) {
activitySchoolApply.setUnionLeader(true);
activitySchoolApply.setIdentity(List.of("3"));
} else if ("团长".equals(identityName)) {
activitySchoolApply.setUnionHead(true);
activitySchoolApply.setIdentity(List.of("6"));
} else if ("工作人员".equals(identityName)) {
activitySchoolApply.setUnionStaff(true);
activitySchoolApply.setIdentity(List.of("7"));
}
}
private boolean hasImportAuth() {
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name());
}
private String normalizeLoginName(String loginName) {
return StrUtil.isBlank(loginName) ? "" : loginName.replaceAll("\\s+", "");
}
private String normalizeText(String text) {
return StrUtil.trimToEmpty(text);
}
private String normalizeIdentityName(String identity) {
String identityName = normalizeText(identity);
if ("运动员".equals(identityName)) {
return "";
}
return identityName;
}
}
@@ -0,0 +1,32 @@
package com.budwk.app.zhgh.activity.sports.template;
import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
/**
* 体育活动报名导入模板
*/
@Data
public class ActivitySportsApplyImportTemp {
@Excel(name = "工会", width = 20)
private String unionName;
@Excel(name = "工号", width = 20)
private String loginName;
@Excel(name = "姓名", width = 20)
private String userName;
@Excel(name = "性别", width = 12)
private String sex;
@Excel(name = "项目编号", width = 20)
private String projectCode;
@Excel(name = "身份", width = 20)
private String identity;
@Excel(name = "备注", width = 30)
private String notes;
}
@@ -9,6 +9,7 @@ import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.sys.services.SysMsgService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
@@ -81,17 +82,20 @@ public class ActivityWorksCollectionManageController {
@At
@SaCheckPermission("activity.workscollection.manage")
@Aop(TransAop.READ_COMMITTED)
public Result insert(@Param("data") @Valid Activity_works_collection worksCollection) {
worksCollection.setIsSubmit(true);
dao.insertWith(worksCollection, "subjectTypes");
worksCollection.getSubjectTypes().forEach(item -> {
dao.insertLinks(item, "worksTypes");
});
syncHomeActivity(worksCollection);
return Result.success();
}
@At
@SaCheckPermission("activity.workscollection.manage")
@Aop(TransAop.READ_COMMITTED)
public Result save(@Param("data") @Valid Activity_works_collection worksCollection) {
worksCollection.setIsSubmit(false);
dao.insertWith(worksCollection, "subjectTypes");
@@ -122,6 +126,7 @@ public class ActivityWorksCollectionManageController {
Cnd.where(Activity_works_worksType::getId, "not in ", workIds)
.and(Activity_works_worksType::getSubjectId,"=",item.getId()));
});
syncHomeActivity(worksCollection);
return Result.success();
}
@@ -132,6 +137,7 @@ public class ActivityWorksCollectionManageController {
dao.delete(Activity_works_collection.class, id);
dao.clear(Activity_works_collection_upload.class, Cnd.where(Activity_works_collection_upload::getActivityId, "=", id));
dao.clear(Activity_works_subjectType.class, Cnd.where(Activity_works_subjectType::getActivityId, "=", id));
dao.delete(Sys_home_activity.class, id);
return Result.success();
}
@@ -148,11 +154,26 @@ public class ActivityWorksCollectionManageController {
@At
@SaCheckPermission("activity.workscollection.manage")
@Aop(TransAop.READ_COMMITTED)
public Result enableChange(@Valid String id, @Valid Boolean enable) {
dao.update(Activity_works_collection.class, Chain.make("enable", enable), Cnd.where("id", "=", id));
Activity_works_collection worksCollection = dao.fetch(Activity_works_collection.class, id);
worksCollection.setEnable(enable);
syncHomeActivity(worksCollection);
return Result.success();
}
/**
* 同步作品征集到首页活动表:开启的活动写入首页,关闭的活动移除首页入口。
*/
private void syncHomeActivity(Activity_works_collection worksCollection) {
if (Boolean.TRUE.equals(worksCollection.getEnable())) {
dao.insertOrUpdate(worksCollection.covertToSysHomeActivity());
} else {
dao.delete(Sys_home_activity.class, worksCollection.getId());
}
}
@At
@SaCheckPermission("activity.workscollection.manage")
public Result sendNotice(@Valid String id) {
@@ -11,6 +11,7 @@ import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
import com.budwk.app.zhgh.activity.workscollection.models.Activity_works_collection;
import com.budwk.app.zhgh.activity.workscollection.models.Activity_works_collection_upload;
import io.swagger.annotations.ApiOperation;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.FieldFilter;
@@ -18,6 +19,7 @@ import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.Daos;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
@@ -94,11 +96,17 @@ public class ActivityWorksCollectionUploadController {
*/
@At
@SaCheckPermission("activity.workscollection.upload")
@Aop(TransAop.READ_COMMITTED)
public Result insert(@Param("data") @Valid Activity_works_collection_upload upload) {
String activityId = upload.getActivityId();
Activity_works_collection activity = dao.fetch(Activity_works_collection.class, activityId);
if (activity.getEndDateTime().getTime() < System.currentTimeMillis()){
long now = System.currentTimeMillis();
// 作品上传只允许在活动开始时间和结束时间之间提交,防止活动未开始或已结束后继续上传。
if (activity.getStartDateTime().getTime() > now) {
return Result.error("活动未开始,不能上传!");
}
if (activity.getEndDateTime().getTime() < now){
return Result.error("活动已结束!");
}
if (activity.getActivityGroupId() != null) {
@@ -124,9 +132,15 @@ public class ActivityWorksCollectionUploadController {
@At
@SaCheckPermission("activity.workscollection.upload")
@Aop(TransAop.READ_COMMITTED)
public Result update(@Param("data") @Valid Activity_works_collection_upload upload) {
Activity_works_collection activity = dao.fetch(Activity_works_collection.class, upload.getActivityId());
if (activity.getEndDateTime().getTime() < System.currentTimeMillis()){
long now = System.currentTimeMillis();
// 编辑作品同样按活动时间窗口控制,避免过期或未开始活动被继续提交作品信息。
if (activity.getStartDateTime().getTime() > now) {
return Result.error("活动未开始,不能上传!");
}
if (activity.getEndDateTime().getTime() < now){
return Result.error("活动已结束!");
}
dao.updateIgnoreNull(upload);
@@ -155,12 +169,19 @@ public class ActivityWorksCollectionUploadController {
@At
@SaCheckPermission("activity.workscollection.upload")
/**
* 查询当前登录人有权限上传作品的活动主题。
*
* @param year 活动年度,前端传年度选择框的 yyyy 值,按活动开始时间所在年份过滤。
* @return Resultdata 为活动列表,仅返回 id、name,且保留已开启和面向对象权限过滤。
*/
@ApiOperation("获取有权限的活动列表")
@Ok("json:{ignoreNull:true}")
public Result listPerMissionActivity() {
public Result listPerMissionActivity(Long year) {
Dao extDao = Daos.ext(dao, FieldFilter.create(Activity_works_collection.class, "id|name|"));
Cnd cnd = Cnd.NEW();
cnd.and(Activity_works_collection::getEnable,"=",1);
cnd.andEX("YEAR(startDateTime)", "=", year);
cnd.desc(Activity_works_collection::getStartDateTime);
List<Activity_works_collection> list = extDao.query(Activity_works_collection.class, cnd);
list = list.stream().filter(item -> {
@@ -1,6 +1,8 @@
package com.budwk.app.zhgh.activity.workscollection.models;
import com.budwk.app.base.model.BaseModel;
import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.sys.services.SysHomeConvert;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
@@ -17,7 +19,7 @@ import java.util.List;
@Table("activity_works_collection")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("作品征集活动")
public class Activity_works_collection extends BaseModel {
public class Activity_works_collection extends BaseModel implements SysHomeConvert {
@Name
@Comment("ID")
@@ -123,4 +125,21 @@ public class Activity_works_collection extends BaseModel {
@Many(field = "activityId")
private List<Activity_works_collection_upload> uploads;
@Override
public Sys_home_activity covertToSysHomeActivity() {
Sys_home_activity sysHomeActivity = new Sys_home_activity();
sysHomeActivity.setId(this.getId());
sysHomeActivity.setName(this.getName());
sysHomeActivity.setCover(this.getCover());
sysHomeActivity.setContent(this.getContent());
sysHomeActivity.setUrl("/platform/activity/worksCollection/upload");
sysHomeActivity.setH5Url("/platform/activity/worksCollection/upload/h5");
sysHomeActivity.setStartDate(this.getStartDateTime());
sysHomeActivity.setEndDate(this.getEndDateTime());
sysHomeActivity.setAllowUserGroupId(this.getActivityGroupId());
sysHomeActivity.setEnable(Boolean.TRUE.equals(this.getEnable()));
sysHomeActivity.setClassPath(this.getClass().getPackageName() + this.getClass().getName());
return sysHomeActivity;
}
}
@@ -68,7 +68,7 @@ public class ClubUserJoinApplyController {
@Inject
private ClubUserJoinService clubUserJoinService;
@At("/")
@At("")
@SaCheckPermission("club.join.apply")
@Ok("beetl:/platform/zhgh/club/join/apply/index.html")
public void index() {
@@ -128,6 +128,12 @@ public class ClubUserJoinApplyController {
@SLog(tag = "社团管理系统-申请入/退会", msg = "申请社团入/退会")
public Result submit(@Param("data") ClubUserApply clubUserApply,
@Param("mode") Boolean mode) {
if (Boolean.TRUE.equals(mode)) {
String qualificationMsg = clubUserJoinService.checkJoinQualification(SecurityUtil.getUserId());
if (StrUtil.isNotBlank(qualificationMsg)) {
return Result.error(99, qualificationMsg);
}
}
if(mode == false) {
clubUserApply = clubUserJoinService.buildExitApply(clubUserApply.getClubId(), SecurityUtil.getUserId());
if (ObjectUtil.isEmpty(clubUserApply)) {
@@ -192,6 +198,10 @@ public class ClubUserJoinApplyController {
@ApiOperation("校验是否申请过社团")
@SaCheckPermission(value = {"club.join.apply", "h5.club.join.apply"}, mode = SaMode.OR)
public Result checkApplyClub(@Valid String clubId) {
String qualificationMsg = clubUserJoinService.checkJoinQualification(SecurityUtil.getUserId());
if (StrUtil.isNotBlank(qualificationMsg)) {
return Result.error(99, qualificationMsg);
}
View_user user = dao.fetch(View_user.class, Cnd.where("id", "=", SecurityUtil.getUserId()));
if (user.getMember() != 1) {
return Result.error(99, "抱歉,您不满足入会的条件!");
@@ -48,7 +48,7 @@ public class ClubUserJoinApprovalController {
@Inject
private ClubUserJoinService clubUserJoinService;
@At("/")
@At("")
@SaCheckPermission("club.join.clubApproval")
@Ok("beetl:/platform/zhgh/club/join/clubApproval/index.html")
public void index() {}
@@ -45,7 +45,7 @@ public class ClubUserJoinMineController {
@Inject
private ClubUserJoinService clubUserJoinService;
@At("/")
@At("")
@SaCheckPermission("club.join.mine")
@Ok("beetl:/platform/zhgh/club/join/mine/index.html")
public void index() {}
@@ -57,7 +57,7 @@ public class ClubCommonController {
@SaCheckLogin
public Result listClubByRole() {
Cnd cnd = Cnd.NEW();
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name(), RoleConstant.SCHOOL_UNION_CLUB_ADMIN.name())) {
// 报销申请只允许选择当前用户已加入的协会,避免看到未加入的协会数据。
List<ClubUser> clubUsers = sysClubService.dao().query(ClubUser.class, Cnd.where("userId", "=", SecurityUtil.getUserId()));
List<String> myClubId = clubUsers.stream().map(ClubUser::getClubId).distinct().toList();
@@ -78,4 +78,12 @@ public class ClubCommonController {
return Result.success(list);
}
@At
@SaCheckLogin
public Result listManageClubByRole() {
// 活动人员范围设置只允许按“可管理的社团”选择,避免把仅作为成员加入的社团也展示出来。
List<SysClub> clubList = sysClubService.getMyManageClub();
return Result.success(clubList);
}
}
@@ -25,4 +25,12 @@ public interface ClubUserJoinService extends BaseService<ClubUserApply> {
* @return 入会时间
*/
java.util.Date resolveJoinTime(String clubId, String userId);
/**
* 校验当前用户是否满足申请加入社团的资格。
*
* @param userId 用户ID
* @return 校验不通过时返回提示语,通过时返回null
*/
String checkJoinQualification(String userId);
}
@@ -1,6 +1,9 @@
package com.budwk.app.zhgh.club.service.impl;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.zhgh.club.model.ClubUser;
import com.budwk.app.zhgh.club.model.ClubUserApply;
@@ -15,10 +18,37 @@ import java.util.List;
@IocBean(args = {"refer:dao"})
public class ClubUserJoinServiceImpl extends BaseServiceImpl<ClubUserApply> implements ClubUserJoinService {
private static final List<String> ALLOW_JOIN_PERSON_TYPES = List.of(
"教职工",
"未起薪人员",
"教职工新入会",
"教职工其他",
"劳动服务公司",
"后勤聘用人员",
"退休人员[一]",
"退休人员[二]",
"校聘劳务派遣全额"
);
public ClubUserJoinServiceImpl(Dao dao) {
super(dao);
}
@Override
public String checkJoinQualification(String userId) {
View_user user = dao().fetch(View_user.class, Cnd.where("id", "=", userId));
if (user == null) {
return "抱歉,您不满足入会的条件!";
}
if (user.getMember() == null || user.getMember() != 1) {
return "抱歉,您不满足入会的条件!";
}
if (StrUtil.isBlank(user.getPersonType()) || !ALLOW_JOIN_PERSON_TYPES.contains(user.getPersonType())) {
return "抱歉,您不满足入会的条件!";
}
return null;
}
@Override
public ClubUserApply buildExitApply(String clubId, String userId) {
// 优先复用最近一次申请记录中的补充信息,避免用户重复填写基础资料。
@@ -275,7 +275,14 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
}
cnd.andEX("year(info.createTime)", "=", pageForm.getYear());
cnd.and("info.userId", "=", SecurityUtil.getUserId());
cnd.desc("createTime");
// 我的注册列表默认按社团编码升序展示,点击表头时按用户选择的方向排序。
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.asc("info.clubCode");
} else if ("clubCode".equals(pageForm.getPageOrderName())) {
cnd.orderBy("info.clubCode", PageUtil.getOrder(pageForm.getPageOrderBy()));
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
cnd.groupBy("info.id");
sql.setCondition(cnd);
return listPageVO(pageForm, sql, ClubRegisterPageVo.class);
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.dayofficework.message.strategy.impl;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.sms.SmsService;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.dayofficework.message.enums.GlobalMessageChannel;
import com.budwk.app.zhgh.dayofficework.message.strategy.GlobalMessageSendStrategy;
@@ -21,6 +22,8 @@ public class SmsMessageSendStrategy implements GlobalMessageSendStrategy {
@Inject
private Dao dao;
@Inject
private SmsService smsService;
@Override
public GlobalMessageChannel getChannel() {
@@ -70,29 +73,24 @@ public class SmsMessageSendStrategy implements GlobalMessageSendStrategy {
}
/**
* 发送短信消息
* 发送短信消息
* 接收人由全局消息服务传入的用户ID或登录名查询得到,发送时使用用户工号对接通讯平台;
* 返回结果由 SmsService 内部解析并记录,当前策略只负责逐个用户触发发送并记录异常。
*/
private void sendSmsMessage(String title, String content, Integer type, List<Sys_user> users, JSONObject config) {
for (Sys_user user : users) {
try {
String mobile = user.getMobile();
if (mobile == null || mobile.trim().isEmpty()) {
log.warn("用户{}没有手机号,跳过短信发送", user.getUsername());
String loginName = user.getLoginname();
if (loginName == null || loginName.trim().isEmpty()) {
log.warn("用户{}没有号,跳过短信发送", user.getUsername());
continue;
}
// 构建短信内容
String smsContent = buildSmsContent(title, content, type);
// 执行发送
boolean success = true;
if (success) {
log.info("短信发送成功:用户{},手机号:{}", user.getUsername(), mobile);
} else {
log.warn("短信发送失败:用户{},手机号:{}", user.getUsername(), mobile);
}
smsService.sendSmsByAccount(loginName, smsContent);
log.info("短信发送完成:用户{},工号:{}", user.getUsername(), loginName);
} catch (Exception e) {
log.error("发送短信给用户{}时出错:{}", user.getUsername(), e.getMessage(), e);
}
@@ -0,0 +1,113 @@
package com.budwk.app.zhgh.dayofficework.message.strategy.impl;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.sms.SmsService;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.dayofficework.message.enums.GlobalMessageChannel;
import com.budwk.app.zhgh.dayofficework.message.strategy.GlobalMessageSendStrategy;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.List;
/**
* 微信发送策略
*/
@IocBean
@Slf4j
public class WechatMessageSendStrategy implements GlobalMessageSendStrategy {
@Inject
private Dao dao;
@Inject
private SmsService smsService;
@Override
public GlobalMessageChannel getChannel() {
return GlobalMessageChannel.WECHAT;
}
@Override
public boolean supportAsync() {
return true;
}
@Override
public void send(String title, String content, Integer type, List<String> receiverIds, JSONObject config) {
try {
log.info("开始发送微信消息,标题:{},接收人数量:{}", title, receiverIds.size());
// receiverIds 为 sys_user.id,按用户ID查询后使用工号对接通讯平台微信文本消息接口。
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where(Sys_user::getId, "in", receiverIds));
sendWechatMessage(title, content, type, users, config);
log.info("微信消息发送完成,标题:{},接收人数量:{}", title, users.size());
} catch (Exception e) {
log.error("微信消息发送失败,标题:{},错误:{}", title, e.getMessage(), e);
throw new RuntimeException("微信消息发送失败:" + e.getMessage(), e);
}
}
@Override
public void sendByLoginName(String title, String content, Integer type, List<String> receiverLoginNames, JSONObject config) {
try {
log.info("开始发送微信消息(按登录名),标题:{},接收人数量:{}", title, receiverLoginNames.size());
// receiverLoginNames 为 sys_user.loginname,查询用户后仍使用工号作为通讯平台接收人标识。
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where(Sys_user::getLoginname, "in", receiverLoginNames));
sendWechatMessage(title, content, type, users, config);
log.info("微信消息发送完成(按登录名),标题:{},接收人数量:{}", title, users.size());
} catch (Exception e) {
log.error("微信消息发送失败(按登录名),标题:{},错误:{}", title, e.getMessage(), e);
throw new RuntimeException("微信消息发送失败:" + e.getMessage(), e);
}
}
/**
* 发送微信文本消息。
* 接收人来自全局消息服务传入的用户ID或登录名,方法内部转换为 Sys_user 后取工号发送;
* SmsService 返回 MsgPlatformResponse,当前策略按单人发送记录日志,单个用户失败不影响其他接收人。
*/
private void sendWechatMessage(String title, String content, Integer type, List<Sys_user> users, JSONObject config) {
for (Sys_user user : users) {
try {
String loginName = user.getLoginname();
if (loginName == null || loginName.trim().isEmpty()) {
log.warn("用户{}没有工号,跳过微信发送", user.getUsername());
continue;
}
String wechatContent = buildWechatContent(title, content, type);
smsService.sendWechatTextByAccountOrMobile(loginName, wechatContent);
log.info("微信发送完成:用户{},工号:{}", user.getUsername(), loginName);
} catch (Exception e) {
log.error("发送微信给用户{}时出错:{}", user.getUsername(), e.getMessage(), e);
}
}
}
/**
* 构建微信文本内容。
* type=2 表示待办消息,其余类型按系统通知处理,内容格式保持与短信策略一致。
*/
private String buildWechatContent(String title, String content, Integer type) {
StringBuilder sb = new StringBuilder();
if (type != null && type == 2) {
sb.append("【待办通知】");
} else {
sb.append("【系统通知】");
}
sb.append(title);
if (content != null && !content.trim().isEmpty()) {
sb.append("").append(content);
}
return sb.toString();
}
}
@@ -457,9 +457,13 @@ public class SiteCugApplyController {
return result;
}
/**
* 当预约日期就是今天时,已经过了开始时间的时间块直接标记为不可预约,
* 这样全天候和分段预约在前端展示时都会统一变成禁用状态。
*/
private NutMap buildAvailabilityBlock(String day, int startMinute, int endMinute, List<int[]> reservedRanges, List<int[]> disabledRanges, boolean siteClosed, int groupIndex, String groupLabel) {
String status = "available";
if (siteClosed || intersectsAny(startMinute, endMinute, disabledRanges)) {
if (siteClosed || isExpiredAvailabilityBlock(day, startMinute) || intersectsAny(startMinute, endMinute, disabledRanges)) {
status = "closed";
} else if (intersectsAny(startMinute, endMinute, reservedRanges)) {
status = "reserved";
@@ -478,6 +482,19 @@ public class SiteCugApplyController {
.addv("endDateTime", day + " " + formatMinutes(endMinute) + ":00");
}
/**
* 仅在当天按“时间块开始时间是否已经过去”判断是否失效,
* 避免用户继续选中已经开始或已经结束的预约时段。
*/
private boolean isExpiredAvailabilityBlock(String day, int startMinute) {
String today = DateUtil.today();
if (!StrUtil.equals(today, day)) {
return false;
}
DateTime blockStart = DateUtil.parseDateTime(day + " " + formatMinutes(startMinute) + ":00");
return !blockStart.isAfter(DateUtil.date());
}
private boolean isSiteClosed(SiteCugInfo siteInfo, String day, Set<String> holidaySet) {
if (isWeekend(day)) {
return true;
@@ -113,22 +113,23 @@ public class UnionReimburseApplyController {
return Result.success();
}
// @At
// @ApiOperation("重新提交申请")
// @Aop(TransAop.READ_COMMITTED)
// @SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
// public Result submitAgain(@Param("data") UnionReimburse unionReimburse, @Param("taskId") Long taskId) {
// if (StrUtil.isBlank(unionReimburse.getId())) unionReimburse.setCreateTime(new Date());
//
// dao.insertOrUpdate(unionReimburse);
//
// Dict dict = Dict.create();
// dict.set(FlowConst.PROCESS_TASK_ID_KEY, taskId);
// dict.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.RE_APPLY.getCode());
// dict.set("userId", unionReimburse.getCondolenceUserId());
// flowCommonService.executeTask(dict);
// return Result.success();
// }
@At
@ApiOperation("重新提交申请")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
public Result submitAgain(@Param("data") UnionReimburse unionReimburse, @Param("taskId") Long taskId) {
// 退回重提也必须复用当前提交保存逻辑,避免绕过发票明细落库和提交校验。
Result result = unionReimburseService.saveApply(unionReimburse, 2);
if (result.getCode() != 0) {
return result;
}
Dict dict = Dict.create();
dict.set(FlowConst.PROCESS_TASK_ID_KEY, taskId);
dict.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.RE_APPLY.getCode());
flowCommonService.executeTask(dict);
return Result.success();
}
@At
@SaCheckLogin
@@ -179,6 +180,14 @@ public class UnionReimburseApplyController {
return Result.success(pagination.getList());
}
@At
@ApiOperation("查询付款人")
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
@SLog(type = "unionReimburse", tag = "查询付款人", msg = "按经费来源查询付款人")
public Result listPayerUser(String keyword, String reimburseFundSource, String clubId) {
return Result.success(unionReimburseService.listPayerUser(keyword, reimburseFundSource, clubId));
}
@At
@ApiOperation("查询职工慰问类型")
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
@@ -267,6 +276,6 @@ public class UnionReimburseApplyController {
@ApiOperation("统一查询经费余额")
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
public Result getBudgetBalance(String reimburseFundSource, String clubId) {
return unionReimburseService.getBudgetBalance(reimburseFundSource, clubId);
return unionReimburseService.getBudgetBalance(reimburseFundSource, clubId, null);
}
}
@@ -7,6 +7,7 @@ import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
@@ -26,12 +27,14 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
@@ -117,12 +120,10 @@ public class UnionReimburseCollectController {
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"unionReimburse.collect", "h5.unionReimburse.collect"}, mode = SaMode.OR)
public Result updateActuallyAmount(String id,Double realMoney) {
UnionReimburse unionReimburse = unionReimburseService.fetch(id);
unionReimburse.setRealMoney(realMoney);
unionReimburseService.updateIgnoreNull(unionReimburse);
return Result.success();
return unionReimburseService.updateActuallyAmount(id, realMoney);
}
@At
@@ -237,6 +238,9 @@ public class UnionReimburseCollectController {
item.setReimburseFundSource(typeDict.getName());
}
}
if (StrUtil.isNotBlank(item.getCreateTime())) {
item.setCreateTime(DateUtil.format(DateUtil.parse(item.getCreateTime()), "yyyy-MM-dd HH:mm:ss"));
}
// 拼接备注字段(仿照系统代码逻辑,使用activityName和condolenceName字段)
if (cn.hutool.core.util.StrUtil.isNotBlank(item.getActivityName())) {
@@ -189,7 +189,7 @@ public class UnionReimburseMineController {
@SaCheckPermission(value = {"unionReimburse.mine", "h5.unionReimburse.mine"}, mode = SaMode.OR)
@SLog( tag = "删除工会报销", msg = "删除工会报销")
public Result delete(@Param("id") String id) {
unionReimburseService.delete(id);
unionReimburseService.deleteApply(id);
return Result.success();
}
@@ -227,7 +227,7 @@ public class UnionReimburseMineController {
tempFile.deleteOnExit();
docData.put("createTime", DateUtil.format(unionReimburse.getCreateTime(), "yyyy-MM-dd"));
docData.put("createTime", DateUtil.format(unionReimburse.getCreateTime(), "yyyy-MM-dd HH:mm:ss"));
docData.put("unitName", unionReimburse.getUnitName());
docData.put("userName", unionReimburse.getUserName());
docData.put("loginName", unionReimburse.getLoginName());
@@ -309,7 +309,7 @@ public class UnionReimburseMineController {
.create();
docData.put("qrcode", pictureRenderData);
docData.put("createTime", DateUtil.format(unionReimburse.getCreateTime(), "yyyy-MM-dd"));
docData.put("createTime", DateUtil.format(unionReimburse.getCreateTime(), "yyyy-MM-dd HH:mm:ss"));
docData.put("unitName", unionReimburse.getUnitName());
docData.put("userName", unionReimburse.getUserName());
docData.put("loginName", unionReimburse.getLoginName());
@@ -12,7 +12,6 @@ import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
@@ -32,7 +31,6 @@ import java.util.List;
@At("/platform/unionReimburse/review")
@Ok("json:full")
@Api("工会报销审核")
@Slf4j
public class UnionReimburseReviewController {
@Inject
@@ -110,19 +108,7 @@ public class UnionReimburseReviewController {
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"unionReimburse.review", "h5.unionReimburse.review"}, mode = SaMode.OR)
@SLog(tag = "工会报销审核", msg = "一键审核工会报销")
public Result allReview() {
try {
List<UnionReimburse> reimbursements = unionReimburseService.query(Cnd.where("stateId", "=", 2));
for (UnionReimburse reimbursement : reimbursements) {
Result result = unionReimburseService.reviewApply(reimbursement.getId(), "通过", 3);
if (result.getCode() != 0) {
return result;
}
}
return Result.success("一键审核完成,共处理" + reimbursements.size() + "条记录");
} catch (Exception e) {
log.error("一键审核失败", e);
return Result.error("一键审核失败: " + e.getMessage());
}
public Result allReview(@Param("ids[]") String[] ids) {
return unionReimburseService.batchReviewApply(ids);
}
}
@@ -6,6 +6,7 @@ import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
import com.budwk.app.zhgh.dayofficework.unionReimburse.param.UnionReimbursePageForm;
import com.budwk.app.zhgh.dayofficework.unionReimburse.vo.UnionReimburseBankHistoryVO;
import org.nutz.dao.sql.Sql;
import org.nutz.lang.util.NutMap;
import java.util.List;
@@ -23,11 +24,26 @@ public interface UnionReimburseService extends BaseService<UnionReimburse> {
*/
UnionReimburse getApplyForm(String id);
/**
* 删除报销申请,并同步删除关联发票明细。
*/
void deleteApply(String id);
/**
* 查询付款人的历史收款信息。
*/
List<UnionReimburseBankHistoryVO> getUserBankHistory(String payer);
/**
* 查询付款人候选人;协会经费按所选协会过滤,只返回该协会成员。
*
* @param keyword 姓名或工号关键字
* @param reimburseFundSource 当前选择的经费来源
* @param clubId 协会经费对应的协会ID
* @return 可选付款人列表
*/
List<NutMap> listPayerUser(String keyword, String reimburseFundSource, String clubId);
/**
* 校验发票号码是否与历史报销记录重复。
*/
@@ -41,10 +57,23 @@ public interface UnionReimburseService extends BaseService<UnionReimburse> {
/**
* 查询报销可用经费余额,同时校验是否已分配以及协会归属权限。
*/
Result getBudgetBalance(String reimburseFundSource, String clubId);
Result getBudgetBalance(String reimburseFundSource, String clubId, String unionId);
/**
* 审核报销,审核通过时扣减对应预算并新增预算使用详情。
*/
Result reviewApply(String reimburseId, String reviewOpinion, int submitType);
/**
* 管理员修改实际报销金额,已报销成功的数据需要同步调整经费预算管理台账。
*/
Result updateActuallyAmount(String id, Double realMoney);
/**
* 批量审核选中的待审核报销申请。
*
* @param reimburseIds 前端表格勾选的报销申请ID数组,只处理状态为待审核确认的数据
* @return Result,成功时返回本次实际审核通过的记录数量,失败时返回具体错误原因
*/
Result batchReviewApply(String[] reimburseIds);
}
@@ -4,6 +4,7 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_file;
@@ -122,7 +123,10 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
normalizeInvoiceDetails(unionReimburse);
if (needInvoiceDetails(unionReimburse)) {
fillInvoiceSummary(unionReimburse);
} else {
fillCondolenceInvoiceSummary(unionReimburse);
}
fillDefaultRealMoney(unionReimburse);
if (stateId == 2) {
Result validateResult = validateBeforeSubmit(unionReimburse);
if (validateResult != null) {
@@ -139,23 +143,29 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
}
if (oldRecord == null) {
unionReimburse.setCreateTime(DateUtil.date());
unionReimburse.setCreateTime(toSecondPrecision(DateUtil.date()));
unionReimburse.setDocumentNo(generateDocumentNo());
} else {
unionReimburse.setCreateTime(oldRecord.getCreateTime());
unionReimburse.setCreateTime(toSecondPrecision(oldRecord.getCreateTime()));
unionReimburse.setDocumentNo(oldRecord.getDocumentNo());
}
unionReimburse.setStateId(stateId);
if ("UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject())) {
unionReimburse.setRealMoney(unionReimburse.getCondolenceMoney());
} else {
unionReimburse.setRealMoney(unionReimburse.getMoney());
}
fillDefaultRealMoney(unionReimburse);
this.dao().insertOrUpdate(unionReimburse);
saveInvoiceDetails(unionReimburse.getId(), unionReimburse.getInvoiceDetails());
return Result.success(stateId == 2 ? "提交成功" : "保存成功");
}
/**
* 申请时间统一保存到秒,避免数据库和页面出现毫秒级时间。
*/
private Date toSecondPrecision(Date date) {
if (date == null) {
return null;
}
return new Date((date.getTime() / 1000) * 1000);
}
@Override
public UnionReimburse getApplyForm(String id) {
UnionReimburse unionReimburse = this.fetch(id);
@@ -170,6 +180,16 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
return unionReimburse;
}
@Override
public void deleteApply(String id) {
if (StrUtil.isBlank(id)) {
return;
}
// 删除申请主记录时同步清理发票明细,避免遗留无归属的发票关联数据。
this.dao().clear(UnionReimburseInvoiceDetail.class, Cnd.where("reimburseId", "=", id));
this.delete(id);
}
@Override
public List<UnionReimburseBankHistoryVO> getUserBankHistory(String payer) {
if (StrUtil.isBlank(payer)) {
@@ -199,6 +219,55 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
return this.listVO(sql, UnionReimburseBankHistoryVO.class);
}
/**
* 付款人查询要跟随经费来源;选择协会经费时,只允许从当前协会会员中选择付款人。
*/
@Override
public List<NutMap> listPayerUser(String keyword, String reimburseFundSource, String clubId) {
boolean clubFundSource = "UNION_REIMBURSE_FUND_SOURCE_3".equals(reimburseFundSource);
if (clubFundSource && StrUtil.isBlank(clubId)) {
return new ArrayList<>();
}
String clubJoinSql = clubFundSource ? "INNER JOIN club_user cu ON cu.userId = u.id" : "";
Sql sql = Sqls.create("""
SELECT
u.id,
u.username AS userName,
u.loginname AS loginName,
u.sex,
u.mobile,
u.technicalTitle,
IFNULL(u.unitname, '暂无') AS unitName,
u.unitid AS unitId,
u.unionid AS unionId,
u.unionname AS unionName,
DATE(u.birthday) AS birthday,
u.idCard,
u.unionCode
FROM
vw_user u
%s
$condition
""".formatted(clubJoinSql));
Cnd cnd = Cnd.NEW();
if (StrUtil.isNotBlank(keyword)) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or("u.loginname", "like", "%" + keyword + "%");
seg.or("u.username", "like", "%" + keyword + "%");
cnd.and(seg);
}
cnd.and("u.id", "!=", SecurityUtil.getUserId());
if (clubFundSource) {
cnd.and("cu.clubId", "=", clubId);
} else if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
cnd.and("u.unionid", "=", SecurityUtil.getUnionId());
}
cnd.groupBy("u.id");
sql.setCondition(cnd);
Pagination pagination = this.listPageMap(1, 50, sql);
return pagination.getList();
}
@Override
public Result checkInvoiceDuplicate(String invoiceNo, String reimburseId) {
String currentInvoiceNo = StrUtil.blankToDefault(StrUtil.trim(invoiceNo), "");
@@ -244,7 +313,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
* 按经费来源统一查询当前可用余额,未分配或权限不匹配时直接拦截。
*/
@Override
public Result getBudgetBalance(String reimburseFundSource, String clubId) {
public Result getBudgetBalance(String reimburseFundSource, String clubId, String unionId) {
if (StrUtil.isBlank(reimburseFundSource)) {
return Result.error("请选择经费来源");
}
@@ -257,8 +326,9 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
return Result.success(formatMoney(outlayManageSchool.getTotalQuota().subtract(outlayManageSchool.getUsedQuota())));
}
if ("UNION_REIMBURSE_FUND_SOURCE_2".equals(reimburseFundSource)) {
String queryUnionId = StrUtil.blankToDefault(unionId, SecurityUtil.getUnionId());
OutlayManageUnion outlayManageUnion = dao().fetch(OutlayManageUnion.class,
Cnd.where("unionId", "=", SecurityUtil.getUnionId()).and("year", "=", DateUtil.thisYear()));
Cnd.where("unionId", "=", queryUnionId).and("year", "=", DateUtil.thisYear()));
if (outlayManageUnion == null) {
return Result.error("分工会经费未分配");
}
@@ -315,6 +385,58 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
return Result.success();
}
/**
* 批量审核前只查询前端勾选且仍处于待审核确认状态的数据,避免误处理未勾选或状态已变化的申请。
*/
@Override
public Result batchReviewApply(String[] reimburseIds) {
if (reimburseIds == null || reimburseIds.length == 0) {
return Result.error("请选择需要审核的报销申请");
}
List<UnionReimburse> reimbursements = this.query(Cnd.where("id", "in", reimburseIds).and("stateId", "=", 2));
if (reimbursements.isEmpty()) {
return Result.error("所选记录中没有待审核的数据");
}
for (UnionReimburse reimbursement : reimbursements) {
Result result = this.reviewApply(reimbursement.getId(), "通过", 3);
if (result.getCode() != 0) {
return result;
}
}
return Result.success("一键审核完成,共处理" + reimbursements.size() + "条记录");
}
/**
* 管理员修改实际金额时,报销成功的数据已经写入预算台账,需要按差额同步已使用额度和使用明细。
*/
@Override
public Result updateActuallyAmount(String id, Double realMoney) {
if (StrUtil.isBlank(id)) {
return Result.error("报销记录ID不能为空");
}
BigDecimal newMoney = normalizeActualMoney(realMoney);
if (newMoney == null || newMoney.compareTo(BigDecimal.ZERO) <= 0) {
return Result.error("实际报销金额必须大于0");
}
UnionReimburse dbRecord = this.fetch(id);
if (dbRecord == null) {
return Result.error("未找到对应的报销记录");
}
if (Integer.valueOf(3).equals(dbRecord.getStateId())) {
OutlayUseDetail oldDetail = dao().fetch(OutlayUseDetail.class, Cnd.where("outlayReimburseId", "=", dbRecord.getId()));
BigDecimal oldMoney = oldDetail != null && oldDetail.getAdjustMoney() != null
? oldDetail.getAdjustMoney()
: getApplyMoney(dbRecord);
Result syncResult = syncApprovedActualAmount(dbRecord, oldDetail, oldMoney, newMoney);
if (syncResult != null && syncResult.getCode() != 0) {
return syncResult;
}
}
dbRecord.setRealMoney(newMoney.doubleValue());
this.updateIgnoreNull(dbRecord);
return Result.success();
}
private void normalizeInvoiceDetails(UnionReimburse unionReimburse) {
if (unionReimburse.getInvoiceDetails() == null) {
unionReimburse.setInvoiceDetails(new ArrayList<>());
@@ -360,6 +482,36 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
unionReimburse.setInvoiceNumber(String.valueOf(invoiceFileCount));
}
/**
* 慰问类报销允许不填发票;有发票时按发票汇总金额申请,没有发票时按慰问金额申请。
*/
private void fillCondolenceInvoiceSummary(UnionReimburse unionReimburse) {
double totalMoney = 0D;
int invoiceFileCount = 0;
for (UnionReimburseInvoiceDetail detail : unionReimburse.getInvoiceDetails()) {
if (detail.getInvoiceAmount() != null) {
totalMoney = BigDecimal.valueOf(totalMoney)
.add(BigDecimal.valueOf(detail.getInvoiceAmount()))
.doubleValue();
}
if (Lang.isNotEmpty(detail.getInvoiceFiles())) {
invoiceFileCount++;
}
}
Double applyMoney = invoiceFileCount > 0
? BigDecimal.valueOf(totalMoney).setScale(2, RoundingMode.HALF_UP).doubleValue()
: unionReimburse.getCondolenceMoney();
unionReimburse.setMoney(applyMoney);
unionReimburse.setInvoiceNumber(String.valueOf(invoiceFileCount));
}
/**
* 默认实际金额跟随申请金额,后续管理员可在汇总页按最终报销金额调整。
*/
private void fillDefaultRealMoney(UnionReimburse unionReimburse) {
unionReimburse.setRealMoney(getDeclaredApplyMoney(unionReimburse).doubleValue());
}
/**
* 提交前补一层后端校验,防止前端绕过校验直接提交。
*/
@@ -379,11 +531,8 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
// if (StrUtil.isBlank(unionReimburse.getBankOfDeposit())) {
// return Result.error("请填写开户行");
// }
if (StrUtil.isBlank(unionReimburse.getPaymentNotes())) {
return Result.error("请填写支付内容");
}
if (!needInvoiceDetails(unionReimburse)) {
return null;
return validateInvoiceDetails(unionReimburse, false);
}
if (StrUtil.isBlank(unionReimburse.getActivityName())) {
return Result.error("请填写活动名称");
@@ -397,7 +546,17 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
if (Lang.isEmpty(unionReimburse.getFiles())) {
return Result.error("请上传附件");
}
return validateInvoiceDetails(unionReimburse, true);
}
/**
* 发票明细提交校验:活动类必须至少一条,慰问类允许为空但填写后必须完整。
*/
private Result validateInvoiceDetails(UnionReimburse unionReimburse, boolean required) {
if (Lang.isEmpty(unionReimburse.getInvoiceDetails())) {
if (!required) {
return null;
}
return Result.error("请至少维护一条发票明细");
}
for (int i = 0; i < unionReimburse.getInvoiceDetails().size(); i++) {
@@ -426,7 +585,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
* 提交和审核前统一校验预算是否已分配、余额是否充足。
*/
private Result validateFundSourceBeforeSubmit(UnionReimburse unionReimburse) {
Result balanceResult = getBudgetBalance(unionReimburse.getReimburseFundSource(), unionReimburse.getClubId());
Result balanceResult = getBudgetBalance(unionReimburse.getReimburseFundSource(), unionReimburse.getClubId(), unionReimburse.getUnionId());
if (balanceResult == null || balanceResult.getCode() != 0) {
return balanceResult == null ? Result.error("经费余额校验失败") : balanceResult;
}
@@ -493,6 +652,117 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
return Result.error("不支持的经费来源");
}
/**
* 已通过报销单修改实际金额时,用差额同步预算主表和使用明细,避免重复全量扣减。
*/
private Result syncApprovedActualAmount(UnionReimburse unionReimburse, OutlayUseDetail oldDetail,
BigDecimal oldMoney, BigDecimal newMoney) {
BigDecimal diffMoney = newMoney.subtract(safeMoney(oldMoney)).setScale(2, RoundingMode.HALF_UP);
String reimburseFundSource = unionReimburse.getReimburseFundSource();
if ("UNION_REIMBURSE_FUND_SOURCE_1".equals(reimburseFundSource)) {
OutlayManageSchool outlayManageSchool = dao().fetch(OutlayManageSchool.class,
Cnd.where("year", "=", DateUtil.thisYear()));
if (outlayManageSchool == null) {
return Result.error("校工会经费未分配");
}
Result quotaResult = updateSchoolUsedQuota(outlayManageSchool, diffMoney);
if (quotaResult.getCode() != 0) {
return quotaResult;
}
saveOrUpdateUseDetail(unionReimburse, oldDetail, outlayManageSchool.getId(), newMoney);
return Result.success();
}
if ("UNION_REIMBURSE_FUND_SOURCE_2".equals(reimburseFundSource)) {
OutlayManageUnion outlayManageUnion = dao().fetch(OutlayManageUnion.class,
Cnd.where("unionId", "=", unionReimburse.getUnionId()).and("year", "=", DateUtil.thisYear()));
if (outlayManageUnion == null) {
return Result.error("分工会经费未分配");
}
Result quotaResult = updateUnionUsedQuota(outlayManageUnion, diffMoney);
if (quotaResult.getCode() != 0) {
return quotaResult;
}
saveOrUpdateUseDetail(unionReimburse, oldDetail, outlayManageUnion.getId(), newMoney);
return Result.success();
}
if ("UNION_REIMBURSE_FUND_SOURCE_3".equals(reimburseFundSource)) {
OutlayManageClub outlayManageClub = dao().fetch(OutlayManageClub.class,
Cnd.where("clubId", "=", unionReimburse.getClubId()).and("year", "=", DateUtil.thisYear()));
if (outlayManageClub == null) {
return Result.error("该协会本年度未生成预算记录,请先在协会预算分配中补分配");
}
Result quotaResult = updateClubUsedQuota(outlayManageClub, diffMoney);
if (quotaResult.getCode() != 0) {
return quotaResult;
}
saveOrUpdateUseDetail(unionReimburse, oldDetail, outlayManageClub.getId(), newMoney);
return Result.success();
}
return Result.error("不支持的经费来源");
}
private Result updateSchoolUsedQuota(OutlayManageSchool outlayManageSchool, BigDecimal diffMoney) {
BigDecimal newUsedQuota = buildNewUsedQuota(outlayManageSchool.getTotalQuota(), outlayManageSchool.getUsedQuota(), diffMoney, "校工会经费");
if (newUsedQuota == null) {
return Result.error("校工会经费余额不足");
}
outlayManageSchool.setUsedQuota(newUsedQuota);
dao().updateIgnoreNull(outlayManageSchool);
return Result.success();
}
private Result updateUnionUsedQuota(OutlayManageUnion outlayManageUnion, BigDecimal diffMoney) {
BigDecimal newUsedQuota = buildNewUsedQuota(outlayManageUnion.getTotalQuota(), outlayManageUnion.getUsedQuota(), diffMoney, "分工会经费");
if (newUsedQuota == null) {
return Result.error("分工会经费余额不足");
}
outlayManageUnion.setUsedQuota(newUsedQuota);
dao().updateIgnoreNull(outlayManageUnion);
return Result.success();
}
private Result updateClubUsedQuota(OutlayManageClub outlayManageClub, BigDecimal diffMoney) {
BigDecimal newUsedQuota = buildNewUsedQuota(outlayManageClub.getTotalQuota(), outlayManageClub.getUsedQuota(), diffMoney, "协会经费");
if (newUsedQuota == null) {
return Result.error("协会经费余额不足");
}
outlayManageClub.setUsedQuota(newUsedQuota);
dao().updateIgnoreNull(outlayManageClub);
return Result.success();
}
/**
* 计算调整后的已使用额度,调增时不能超过总额度,调减时不能小于0。
*/
private BigDecimal buildNewUsedQuota(BigDecimal totalQuota, BigDecimal usedQuota, BigDecimal diffMoney, String outlayName) {
BigDecimal safeTotalQuota = safeMoney(totalQuota);
BigDecimal safeUsedQuota = safeMoney(usedQuota);
BigDecimal safeDiffMoney = safeMoney(diffMoney);
BigDecimal newUsedQuota = safeUsedQuota.add(safeDiffMoney).setScale(2, RoundingMode.HALF_UP);
if (safeDiffMoney.compareTo(BigDecimal.ZERO) == 0) {
return newUsedQuota;
}
if (newUsedQuota.compareTo(BigDecimal.ZERO) < 0) {
return null;
}
if (newUsedQuota.compareTo(safeTotalQuota) > 0) {
log.warn("{}实际金额调增后超出预算,总额度:{}, 已使用:{}, 调整差额:{}", outlayName, safeTotalQuota, safeUsedQuota, diffMoney);
return null;
}
return newUsedQuota;
}
private void saveOrUpdateUseDetail(UnionReimburse unionReimburse, OutlayUseDetail oldDetail,
String outlayManageId, BigDecimal newMoney) {
if (oldDetail == null) {
insertUseDetail(unionReimburse, outlayManageId, newMoney);
return;
}
oldDetail.setOutlayManageId(outlayManageId);
oldDetail.setAdjustMoney(newMoney);
dao().updateIgnoreNull(oldDetail);
}
/**
* 一张报销单只写入一条预算使用详情,避免重复通过或重复点击造成重复明细。
*/
@@ -516,11 +786,31 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
}
private BigDecimal getApplyMoney(UnionReimburse unionReimburse) {
Double money = "UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject())
? unionReimburse.getCondolenceMoney() : unionReimburse.getMoney();
if (unionReimburse.getRealMoney() != null) {
return BigDecimal.valueOf(unionReimburse.getRealMoney()).setScale(2, RoundingMode.HALF_UP);
}
return getDeclaredApplyMoney(unionReimburse);
}
private BigDecimal getDeclaredApplyMoney(UnionReimburse unionReimburse) {
Double money = unionReimburse.getMoney();
if (money == null && "UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject())) {
money = unionReimburse.getCondolenceMoney();
}
return BigDecimal.valueOf(money == null ? 0D : money).setScale(2, RoundingMode.HALF_UP);
}
private BigDecimal normalizeActualMoney(Double realMoney) {
if (realMoney == null || realMoney.isNaN() || realMoney.isInfinite()) {
return null;
}
return realMoney == null ? null : BigDecimal.valueOf(realMoney).setScale(2, RoundingMode.HALF_UP);
}
private BigDecimal safeMoney(BigDecimal money) {
return money == null ? BigDecimal.ZERO : money.setScale(2, RoundingMode.HALF_UP);
}
private String buildUseDetailProjectName(UnionReimburse unionReimburse) {
if (StrUtil.isNotBlank(unionReimburse.getActivityName())) {
return unionReimburse.getActivityName();
@@ -851,7 +1141,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
}
/**
* 当前仅活动类报销维护发票明细,慰问类仍按原有逻辑处理金额
* 非慰问类报销必须维护发票明细,慰问类发票明细为可选
*/
private boolean needInvoiceDetails(UnionReimburse unionReimburse) {
return unionReimburse != null && !"UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject());
@@ -59,7 +59,7 @@ public class ProposalDashboardController {
nodes.add(NutMap.NEW().addv("name", "提案总数").addv("id", "total").addv("type", "total").addv("count", 0));
// 节点
ProcessDefine define = processDefineService.getLastByName("JDHTA");
ProcessDefine define = processDefineService.getLastByName("JDHTA_NC");
ProcessModel processModel = processDefineService.processDefineToModel(define);
List<TaskModel> taskModels = processModel.getTasks();
@@ -141,7 +141,7 @@ public class ProposalQueryChartController {
FROM
wf_process_design
WHERE
NAME = 'JDHTA'
NAME = 'JDHTA_NC'
""");
sql.setCallback(Sqls.callback.map());
dao.execute(sql);
@@ -120,6 +120,7 @@ public class ProposalSecondedController {
Cnd cnd = Cnd.NEW();
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
cnd.and("info.createUserId", "!=", SecurityUtil.getUserId());
if (approval) {
// 已附议页签只展示当前登录附议人已经处理过的记录。
@@ -179,7 +179,7 @@ public class TeacherCongressDelegatePushController {
""");
sql.setParam("sessionId", sessionId);
Cnd cnd = Cnd.NEW();
// cnd.and("u.member", "=", 1);
cnd.and("u.member", "=", 1);
cnd.and("u.username", "is not", null);
cnd.andEX("u.id", "not in", userIds);
cnd.and("tcd.userId", "is", null);
@@ -12,6 +12,8 @@ import com.budwk.app.bpm.service.BpmService;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
import com.budwk.app.zhgh.staffmanage.member.models.MemberChangeRecord;
import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberChangePageForm;
import com.budwk.app.zhgh.staffmanage.member.service.MemberCommonService;
@@ -22,6 +24,7 @@ import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -111,6 +114,12 @@ public class MemberChangeMineController {
cnd.and("info.userId", "=", SecurityUtil.getUserId());
}
}
// Filter manual input records created by member info input
cnd.and(new Static(
"(info.changeOrigin <> '" + MemberChangeOrigin.HAND_MOVEMENT.name()
+ "' OR info.changeOrigin IS NULL OR info.changeType <> '" + MemberChangeType.NEW.name()
+ "' OR info.changeType IS NULL)"
));
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.desc("info.applyDateTime");
} else {
@@ -46,6 +46,8 @@ import org.nutz.lang.Lang;
import org.nutz.lang.random.R;
import org.nutz.lang.util.NutMap;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
@@ -298,6 +300,7 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
@Override
public void compareChangeInfoAndUpdateMember(MemberChangeRecord record) {
normalizeArrivalAtSchoolDate(record);
List<NutMap> changeList = getChangeInfos(record);
// 如果有工会关系人员,这个user用来存储数据
@@ -503,6 +506,7 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
@Override
public void validateChangeAndSetBasicData(MemberChangeRecord record, MemberChangeOrigin origin) {
normalizeArrivalAtSchoolDate(record);
// 校验有没有发生变更
List<NutMap> changeInfos = getChangeInfos(record);
if (Lang.isEmpty(changeInfos)) {
@@ -521,6 +525,39 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
}
/**
* 统一规范变更记录中的到校日期,避免 Date 对象复制后以英文字符串形式参与比较或直接落库。
*/
private void normalizeArrivalAtSchoolDate(MemberChangeRecord record) {
if (record == null || StrUtil.isBlank(record.getArrivalAtSchoolDate())) {
return;
}
String arrivalAtSchoolDate = StrUtil.trim(record.getArrivalAtSchoolDate());
try {
Date parsedDate = parseArrivalAtSchoolDate(arrivalAtSchoolDate);
record.setArrivalAtSchoolDate(DateUtil.format(parsedDate, "yyyy-MM-dd"));
} catch (Exception e) {
throw new BaseException("到校日期格式不正确,请使用yyyy-MM-dd格式");
}
}
/**
* 兼容页面手工变更时从视图对象复制出的英文日期字符串。
*/
private Date parseArrivalAtSchoolDate(String arrivalAtSchoolDate) throws ParseException {
if (arrivalAtSchoolDate.matches("\\d{4}-\\d{2}-\\d{2}")) {
return DateUtil.parseDate(arrivalAtSchoolDate);
}
try {
return DateUtil.parse(arrivalAtSchoolDate);
} catch (Exception ignore) {
SimpleDateFormat format = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy", Locale.ENGLISH);
format.setLenient(false);
return format.parse(arrivalAtSchoolDate);
}
}
/**
* 验证申请或变更是否正在进行
* @param userId 用户id
@@ -214,7 +214,7 @@ public class WelfareProject extends BaseModel implements SysHomeConvert {
sysHomeActivity.setH5Url("/platform/h5/welfare/userSelect/index?id=" + this.getId());
sysHomeActivity.setStartDate(this.getChoiceTimeStart());
sysHomeActivity.setEndDate(this.getChoiceTimeEnd());
sysHomeActivity.setAllowUserSql("select userId from welfare_list where projectId = '" + this.getId() + "' and userId = @userId");
sysHomeActivity.setAllowUserSql("select userId from welfare_list where projectId = (select id from welfare_project where id='" + this.getId() + "' and provideMode!=1) and userId = @userId ");
sysHomeActivity.setEnable(true);
sysHomeActivity.setClassPath(this.getClass().getPackageName() + this.getClass().getName());
return sysHomeActivity;