Compare commits
10
Commits
c1a22401d5
...
1c3c3024dd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c3c3024dd | ||
|
|
cfe8b7daf4 | ||
|
|
5c8fb99ff2 | ||
|
|
b694b69549 | ||
|
|
67bbe336e7 | ||
|
|
641c942d8d | ||
|
|
b49a532442 | ||
|
|
ed58618f0e | ||
|
|
5fc4faca79 | ||
|
|
13fc53e849 |
Generated
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="cn.fjdmy.uniapp.UniappProjectDataService">
|
||||
<option name="generalBasePath" value="$PROJECT_DIR$" />
|
||||
<option name="manifestPath" value="$PROJECT_DIR$/manifest.json" />
|
||||
<option name="pagesPath" value="$PROJECT_DIR$/pages.json" />
|
||||
<option name="scanNum" value="1" />
|
||||
<option name="type" value="store" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+1
@@ -2,6 +2,7 @@
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="true" />
|
||||
<profile name="Maven default annotation processors profile" enabled="true">
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxProjectSettings">
|
||||
<option name="commitMessageIssueKeyValidationOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
<option name="commitMessageValidationEnabledOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="AliAccessStaticViaInstance" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AliArrayNamingShouldHaveBracket" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AliControlFlowStatementWithoutBraces" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AliDeprecation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AliEqualsAvoidNull" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AliLongLiteralsEndingWithLowercaseL" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AliMissingOverrideAnnotation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AliWrapperTypeEquality" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="MapOrSetKeyShouldOverrideHashCodeEquals" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
Generated
+15
@@ -1,6 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Central Repository" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="nutz" />
|
||||
<option name="name" value="nutz" />
|
||||
<option name="url" value="https://jfrog.nutz.cn/artifactory/libs-release" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="nutz-snapshots" />
|
||||
<option name="name" value="nutz-snapshots" />
|
||||
<option name="url" value="https://jfrog.nutz.cn/artifactory/snapshots" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.budwk</groupId>
|
||||
<artifactId>zhgh_jsahvc</artifactId>
|
||||
<artifactId>zhgh_jsahvc_new</artifactId>
|
||||
<version>5.6.0-plus</version>
|
||||
<build>
|
||||
<resources>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<packaging>jar</packaging>
|
||||
<groupId>com.budwk</groupId>
|
||||
<artifactId>zhgh_cug_v4</artifactId>
|
||||
<artifactId>zhgh_jsahvc_new</artifactId>
|
||||
<version>5.6.0-plus</version>
|
||||
<properties>
|
||||
<nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version>
|
||||
|
||||
@@ -2,17 +2,14 @@ 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 request 短信请求参数,需传接收人、发送人、模板ID或消息内容等字段
|
||||
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
|
||||
* @param request 短信请求参数,需传接收人、模板 Code、模板变量或消息内容等字段
|
||||
* @return 阿里云短信发送结果,返回类型为 {@link MsgPlatformResponse}
|
||||
*/
|
||||
MsgPlatformResponse sendSms(MsgPlatformSmsRequest request);
|
||||
|
||||
@@ -22,7 +19,7 @@ public interface SmsService {
|
||||
*
|
||||
* @param account 接收人工号
|
||||
* @param content 短信内容
|
||||
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
|
||||
* @return 阿里云短信发送结果,返回类型为 {@link MsgPlatformResponse}
|
||||
*/
|
||||
MsgPlatformResponse sendSmsByAccount(String account, String content);
|
||||
|
||||
@@ -32,25 +29,7 @@ public interface SmsService {
|
||||
*
|
||||
* @param mobile 接收人手机号
|
||||
* @param content 短信内容
|
||||
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
|
||||
* @return 阿里云短信发送结果,返回类型为 {@link MsgPlatformResponse}
|
||||
*/
|
||||
MsgPlatformResponse sendSmsByMobile(String mobile, String content);
|
||||
|
||||
/**
|
||||
* 发送微信消息。
|
||||
*
|
||||
* @param request 微信请求参数,需传接收人、发送人、消息类型、模板ID或消息内容等字段
|
||||
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
|
||||
*/
|
||||
MsgPlatformResponse sendWechat(MsgPlatformWechatRequest request);
|
||||
|
||||
/**
|
||||
* 发送微信文本消息的便捷方法。
|
||||
* controller 只需要传接收人工号或手机号,以及微信文本内容即可。
|
||||
*
|
||||
* @param accountOrMobile 接收人工号或手机号
|
||||
* @param content 微信文本内容
|
||||
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
|
||||
*/
|
||||
MsgPlatformResponse sendWechatTextByAccountOrMobile(String accountOrMobile, String content);
|
||||
}
|
||||
|
||||
@@ -1,544 +1,477 @@
|
||||
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.base.sms.model.MsgPlatformRecipient;
|
||||
import com.budwk.app.base.sms.model.MsgPlatformResponse;
|
||||
import com.budwk.app.base.sms.model.MsgPlatformSmsRequest;
|
||||
import com.budwk.app.sys.models.Sys_user;
|
||||
import com.budwk.app.web.commons.base.Globals;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.integration.jedis.RedisService;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
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 javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @ClassName SmsJshvcServiceImpl
|
||||
* @Author JyuHsin
|
||||
* @Date 2025/11/26 19:32
|
||||
* @Version 1.0
|
||||
* @Description TODO
|
||||
* 阿里云短信发送服务。
|
||||
* 统一封装阿里云 SendSms 接口,业务代码只需要传接收人工号/手机号和短信内容;
|
||||
* 签名、模板、模板变量名等固定参数从 sms.aliyun.* 配置中读取。
|
||||
*/
|
||||
@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;
|
||||
private static final String SMS_ACTION = "SendSms";
|
||||
private static final String SMS_VERSION = "2017-05-25";
|
||||
private static final String SIGN_ALGORITHM = "ACS3-HMAC-SHA256";
|
||||
private static final String EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
||||
|
||||
/**
|
||||
* 发送短信消息。
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
@Inject
|
||||
private PropertiesProxy conf;
|
||||
|
||||
/**
|
||||
* 通过工号发送短信文本消息的便捷方法。
|
||||
* 该方法适合 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);
|
||||
}
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
/**
|
||||
* 通过手机号发送短信文本消息的便捷方法。
|
||||
* 该方法适合 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);
|
||||
}
|
||||
/**
|
||||
* 发送短信消息。
|
||||
* 参数说明:
|
||||
* 1. recipients:接收人列表,mobile 为手机号;如果 mobile 为空但 account 不为空,会按工号查询 sys_user.mobile。
|
||||
* 2. templateId:可选,阿里云短信模板 Code;为空时读取 sms.aliyun.template-code。
|
||||
* 3. sendUserSign:可选,阿里云短信签名;为空时读取 sms.aliyun.sign-name。
|
||||
* 4. customVar:可选,阿里云模板变量 Map;为空时使用 sms.aliyun.template-param-name 作为变量名,info 作为变量值。
|
||||
* 返回值为 MsgPlatformResponse,code=0/result=true 表示阿里云返回 Code=OK,msgId 保存阿里云 BizId。
|
||||
*
|
||||
* @param request 短信发送请求参数
|
||||
* @return 阿里云短信发送结果
|
||||
*/
|
||||
@Override
|
||||
public MsgPlatformResponse sendSms(MsgPlatformSmsRequest request) {
|
||||
MsgPlatformResponse skipResponse = checkSendEnabled();
|
||||
if (skipResponse != null) {
|
||||
return skipResponse;
|
||||
}
|
||||
validateSmsRequest(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);
|
||||
}
|
||||
String phoneNumbers = resolvePhoneNumbers(request);
|
||||
String signName = Strings.isBlank(request.getSendUserSign()) ? getRequiredConfig("sms.aliyun.sign-name", "请先配置 sms.aliyun.sign-name") : request.getSendUserSign();
|
||||
String templateCode = Strings.isBlank(request.getTemplateId()) ? getRequiredConfig("sms.aliyun.template-code", "请先配置 sms.aliyun.template-code") : request.getTemplateId();
|
||||
String templateParam = buildTemplateParam(request);
|
||||
|
||||
/**
|
||||
* 发送微信文本消息的便捷方法。
|
||||
* 该方法适合 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);
|
||||
}
|
||||
return sendToAliyun(phoneNumbers, signName, templateCode, templateParam);
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装平台公共请求体。
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
/**
|
||||
* 通过工号发送阿里云短信。
|
||||
* 参数说明:
|
||||
* account 传系统用户工号,对应 sys_user.loginname;方法内部查询 sys_user.mobile。
|
||||
* content 传模板变量内容,默认写入 sms.aliyun.template-param-name 对应变量。
|
||||
* 返回值为 MsgPlatformResponse,code=0/result=true 表示发送请求提交成功。
|
||||
*
|
||||
* @param account 接收人工号
|
||||
* @param content 短信模板变量内容
|
||||
* @return 阿里云短信发送结果
|
||||
*/
|
||||
@Override
|
||||
public MsgPlatformResponse sendSmsByAccount(String account, String content) {
|
||||
if (Strings.isBlank(account)) {
|
||||
throw new BaseException("接收人工号不能为空");
|
||||
}
|
||||
Sys_user user = dao.fetch(Sys_user.class, Cnd.where(Sys_user::getLoginname, "=", account));
|
||||
if (user == null) {
|
||||
throw new BaseException("未找到工号对应用户:" + account);
|
||||
}
|
||||
return sendSmsByMobile(user.getMobile(), content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验公共请求参数,避免 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());
|
||||
}
|
||||
/**
|
||||
* 通过手机号发送阿里云短信。
|
||||
* 参数说明:
|
||||
* mobile 传 11 位手机号。
|
||||
* content 传模板变量内容,默认写入 sms.aliyun.template-param-name 对应变量。
|
||||
* 返回值为 MsgPlatformResponse,code=0/result=true 表示发送请求提交成功。
|
||||
*
|
||||
* @param mobile 接收人手机号
|
||||
* @param content 短信模板变量内容
|
||||
* @return 阿里云短信发送结果
|
||||
*/
|
||||
@Override
|
||||
public MsgPlatformResponse sendSmsByMobile(String mobile, String content) {
|
||||
MsgPlatformRecipient recipient = new MsgPlatformRecipient();
|
||||
recipient.setMobile(mobile);
|
||||
|
||||
/**
|
||||
* 解析 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("^@^"));
|
||||
}
|
||||
MsgPlatformSmsRequest request = new MsgPlatformSmsRequest();
|
||||
request.setInfo(content);
|
||||
request.setRecipients(List.of(recipient));
|
||||
return sendSms(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将单个接收人转成平台要求的分隔符格式。
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
/**
|
||||
* 检查当前环境是否允许真实发送短信。
|
||||
* 线下项目以 Globals.sso=false 识别,直接跳过阿里云调用,避免本地或离线环境误发短信;
|
||||
* 阿里云短信开关未启用时同样返回跳过结果,不影响待办主流程继续执行。
|
||||
*
|
||||
* @return 不允许发送时返回跳过响应;允许发送时返回 null
|
||||
*/
|
||||
private MsgPlatformResponse checkSendEnabled() {
|
||||
if (!Globals.sso) {
|
||||
log.warn("线下项目不允许发送短信,已跳过阿里云短信调用");
|
||||
return buildSkippedResponse("线下项目不允许发送短信,已跳过实际发送");
|
||||
}
|
||||
if (!Globals.MyConfig.getBoolean("AppSms", false)) {
|
||||
log.warn("系统参数 AppSms 未开启,已跳过阿里云短信调用");
|
||||
return buildSkippedResponse("系统参数 AppSms 未开启,已跳过实际发送");
|
||||
}
|
||||
if (!conf.getBoolean("sms.aliyun.enabled", false)) {
|
||||
log.warn("阿里云短信未启用,已跳过阿里云短信调用");
|
||||
return buildSkippedResponse("阿里云短信未启用,已跳过实际发送");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析短信接收人手机号。
|
||||
* 如果 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 message 跳过原因
|
||||
* @return 统一短信响应对象
|
||||
*/
|
||||
private MsgPlatformResponse buildSkippedResponse(String message) {
|
||||
MsgPlatformResponse response = new MsgPlatformResponse();
|
||||
response.setHttpStatus(200);
|
||||
response.setCode(0);
|
||||
response.setResult(true);
|
||||
response.setMsg(message);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验接收人数量,避免超过第三方平台单次上限。
|
||||
*
|
||||
* @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 + "人");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 校验阿里云短信请求参数。
|
||||
* 必须至少提供接收人和模板变量内容;模板变量可通过 customVar 显式传入。
|
||||
*
|
||||
* @param request 短信请求参数
|
||||
*/
|
||||
private void validateSmsRequest(MsgPlatformSmsRequest request) {
|
||||
if (request == null) {
|
||||
throw new BaseException("短信请求参数不能为空");
|
||||
}
|
||||
if (request.getRecipients() == null || request.getRecipients().isEmpty()) {
|
||||
throw new BaseException("短信接收人不能为空");
|
||||
}
|
||||
if ((request.getCustomVar() == null || request.getCustomVar().isEmpty()) && Strings.isBlank(request.getInfo())) {
|
||||
throw new BaseException("短信模板变量内容不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建工号发送专用接收人列表。
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
/**
|
||||
* 解析接收人手机号。
|
||||
* recipients.mobile 优先;mobile 为空时,使用 recipients.account 按工号查询 sys_user.mobile。
|
||||
*
|
||||
* @param request 短信请求参数
|
||||
* @return 逗号分隔的手机号列表
|
||||
*/
|
||||
private String resolvePhoneNumbers(MsgPlatformSmsRequest request) {
|
||||
List<String> mobiles = new ArrayList<>();
|
||||
for (MsgPlatformRecipient recipient : request.getRecipients()) {
|
||||
if (recipient == null) {
|
||||
continue;
|
||||
}
|
||||
String mobile = recipient.getMobile();
|
||||
if (Strings.isBlank(mobile) && Strings.isNotBlank(recipient.getAccount())) {
|
||||
Sys_user user = dao.fetch(Sys_user.class, Cnd.where(Sys_user::getLoginname, "=", recipient.getAccount()));
|
||||
mobile = user == null ? "" : user.getMobile();
|
||||
}
|
||||
if (Strings.isBlank(mobile)) {
|
||||
log.warn("短信接收人缺少手机号,account={}", recipient.getAccount());
|
||||
continue;
|
||||
}
|
||||
if (!mobile.matches("^1\\d{10}$")) {
|
||||
log.warn("短信接收人手机号格式不正确,mobile={}", mobile);
|
||||
continue;
|
||||
}
|
||||
mobiles.add(mobile);
|
||||
}
|
||||
if (mobiles.isEmpty()) {
|
||||
throw new BaseException("没有可发送短信的有效手机号");
|
||||
}
|
||||
return mobiles.stream().distinct().collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建手机号发送专用接收人列表。
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
/**
|
||||
* 构建阿里云模板变量。
|
||||
* 如果业务传了 customVar,则直接作为模板变量;否则用 sms.aliyun.template-param-name 配置项包裹 info。
|
||||
*
|
||||
* @param request 短信请求参数
|
||||
* @return JSON 字符串格式的模板变量
|
||||
*/
|
||||
private String buildTemplateParam(MsgPlatformSmsRequest request) {
|
||||
Map<String, String> customVar = request.getCustomVar();
|
||||
if (customVar != null && !customVar.isEmpty()) {
|
||||
return JSONUtil.toJsonStr(customVar);
|
||||
}
|
||||
String paramName = conf.get("sms.aliyun.template-param-name", "content");
|
||||
JSONObject params = new JSONObject();
|
||||
params.set(paramName, request.getInfo());
|
||||
return JSONUtil.toJsonStr(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建微信便捷发送专用接收人列表。
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
/**
|
||||
* 调用阿里云 SendSms 接口。
|
||||
* 使用阿里云 V3 签名机制,HTTP 请求参数放在 query 中,body 为空。
|
||||
*
|
||||
* @param phoneNumbers 手机号,多个手机号用英文逗号分隔
|
||||
* @param signName 阿里云短信签名
|
||||
* @param templateCode 阿里云短信模板 Code
|
||||
* @param templateParam 模板变量 JSON 字符串
|
||||
* @return 阿里云短信发送结果
|
||||
*/
|
||||
private MsgPlatformResponse sendToAliyun(String phoneNumbers, String signName, String templateCode, String templateParam) {
|
||||
String endpoint = conf.get("sms.aliyun.endpoint", "dysmsapi.aliyuncs.com");
|
||||
String accessKeyId = getRequiredConfig("sms.aliyun.access-key-id", "请先配置 sms.aliyun.access-key-id");
|
||||
String accessKeySecret = getRequiredConfig("sms.aliyun.access-key-secret", "请先配置 sms.aliyun.access-key-secret");
|
||||
|
||||
/**
|
||||
* 为便捷发送方法补默认发送人信息。
|
||||
* 这些字段从配置中读取,避免 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", ""));
|
||||
}
|
||||
Map<String, String> queryParams = Map.of(
|
||||
"PhoneNumbers", phoneNumbers,
|
||||
"SignName", signName,
|
||||
"TemplateCode", templateCode,
|
||||
"TemplateParam", templateParam
|
||||
);
|
||||
Map<String, String> headers = buildSignedHeaders(endpoint, accessKeyId, accessKeySecret, queryParams);
|
||||
String requestUrl = "https://" + endpoint + "/?" + canonicalQueryString(queryParams);
|
||||
|
||||
/**
|
||||
* 调用地大通讯平台 REST 接口。
|
||||
*
|
||||
* @param apiPath 具体接口路径
|
||||
* @param payload 发送报文
|
||||
* @return 平台响应结果
|
||||
*/
|
||||
private MsgPlatformResponse doPost(String apiPath, JSONObject payload) {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
connection = (HttpURLConnection) new URL(requestUrl).openConnection();
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setDoOutput(false);
|
||||
connection.setDoInput(true);
|
||||
connection.setUseCaches(false);
|
||||
connection.setConnectTimeout(conf.getInt("sms.aliyun.connect-timeout", 5000));
|
||||
connection.setReadTimeout(conf.getInt("sms.aliyun.read-timeout", 10000));
|
||||
for (Map.Entry<String, String> header : headers.entrySet()) {
|
||||
connection.setRequestProperty(header.getKey(), header.getValue());
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
int httpStatus = connection.getResponseCode();
|
||||
String responseBody = readResponseBody(connection, httpStatus);
|
||||
return parseAliyunResponse(httpStatus, responseBody);
|
||||
} 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;
|
||||
}
|
||||
/**
|
||||
* 构建阿里云 V3 签名请求头。
|
||||
* 参与签名的头包括 host、x-acs-action、x-acs-content-sha256、x-acs-date、x-acs-signature-nonce、x-acs-version。
|
||||
*
|
||||
* @param endpoint 阿里云短信服务域名
|
||||
* @param accessKeyId AccessKey ID
|
||||
* @param accessKeySecret AccessKey Secret
|
||||
* @param queryParams SendSms 请求参数
|
||||
* @return 已包含 Authorization 的请求头
|
||||
*/
|
||||
private Map<String, String> buildSignedHeaders(String endpoint, String accessKeyId, String accessKeySecret, Map<String, String> queryParams) {
|
||||
String date = Instant.now().truncatedTo(ChronoUnit.SECONDS).toString();
|
||||
String nonce = UUID.randomUUID().toString();
|
||||
String signedHeaders = "host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version";
|
||||
String canonicalHeaders = "host:" + endpoint + "\n"
|
||||
+ "x-acs-action:" + SMS_ACTION + "\n"
|
||||
+ "x-acs-content-sha256:" + EMPTY_BODY_SHA256 + "\n"
|
||||
+ "x-acs-date:" + date + "\n"
|
||||
+ "x-acs-signature-nonce:" + nonce + "\n"
|
||||
+ "x-acs-version:" + SMS_VERSION + "\n";
|
||||
String canonicalRequest = "POST\n"
|
||||
+ "/\n"
|
||||
+ canonicalQueryString(queryParams) + "\n"
|
||||
+ canonicalHeaders + "\n"
|
||||
+ signedHeaders + "\n"
|
||||
+ EMPTY_BODY_SHA256;
|
||||
String stringToSign = SIGN_ALGORITHM + "\n" + sha256Hex(canonicalRequest);
|
||||
String signature = hmacSha256Hex(stringToSign, accessKeySecret);
|
||||
String authorization = SIGN_ALGORITHM
|
||||
+ " Credential=" + accessKeyId
|
||||
+ ",SignedHeaders=" + signedHeaders
|
||||
+ ",Signature=" + signature;
|
||||
|
||||
/**
|
||||
* 读取第三方接口响应报文。
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
return Map.of(
|
||||
"host", endpoint,
|
||||
"x-acs-action", SMS_ACTION,
|
||||
"x-acs-version", SMS_VERSION,
|
||||
"x-acs-date", date,
|
||||
"x-acs-signature-nonce", nonce,
|
||||
"x-acs-content-sha256", EMPTY_BODY_SHA256,
|
||||
"Authorization", authorization
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析平台响应体。
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
/**
|
||||
* 生成规范化 QueryString,按参数名排序后进行 RFC3986 编码。
|
||||
*
|
||||
* @param params 请求参数
|
||||
* @return 规范化 QueryString
|
||||
*/
|
||||
private String canonicalQueryString(Map<String, String> params) {
|
||||
return params.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(entry -> percentEncode(entry.getKey()) + "=" + percentEncode(entry.getValue()))
|
||||
.collect(Collectors.joining("&"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台密钥。
|
||||
* 优先读取已加密密钥;未配置时再根据原始密钥按“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);
|
||||
}
|
||||
/**
|
||||
* RFC3986 URL 编码,匹配阿里云 OpenAPI 签名要求。
|
||||
*
|
||||
* @param value 原始字符串
|
||||
* @return 编码后的字符串
|
||||
*/
|
||||
private String percentEncode(String value) {
|
||||
return URLEncoder.encode(Strings.sNull(value), StandardCharsets.UTF_8)
|
||||
.replace("+", "%20")
|
||||
.replace("*", "%2A")
|
||||
.replace("%7E", "~");
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取必填配置项。
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
/**
|
||||
* 解析阿里云短信响应。
|
||||
*
|
||||
* @param httpStatus HTTP 状态码
|
||||
* @param responseBody 原始响应体
|
||||
* @return 统一响应对象
|
||||
*/
|
||||
private MsgPlatformResponse parseAliyunResponse(int httpStatus, String responseBody) {
|
||||
MsgPlatformResponse response = new MsgPlatformResponse();
|
||||
response.setHttpStatus(httpStatus);
|
||||
response.setRawBody(responseBody);
|
||||
if (Strings.isBlank(responseBody) || !JSONUtil.isTypeJSON(responseBody)) {
|
||||
response.setCode(httpStatus);
|
||||
response.setResult(false);
|
||||
response.setMsg("阿里云短信返回内容为空或不是合法JSON");
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 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());
|
||||
}
|
||||
}
|
||||
JSONObject jsonObject = JSONUtil.parseObj(responseBody);
|
||||
String code = jsonObject.getStr("Code");
|
||||
response.setCode("OK".equalsIgnoreCase(code) ? 0 : httpStatus);
|
||||
response.setResult("OK".equalsIgnoreCase(code));
|
||||
response.setMsg(jsonObject.getStr("Message"));
|
||||
response.setMsgId(jsonObject.getStr("BizId"));
|
||||
response.setDescription(code);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 HTTP 响应内容。
|
||||
*
|
||||
* @param connection HTTP 连接
|
||||
* @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 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 SHA256 十六进制摘要。
|
||||
*
|
||||
* @param content 原始内容
|
||||
* @return 十六进制摘要
|
||||
*/
|
||||
private String sha256Hex(String content) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
return bytesToHex(digest.digest(content.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (Exception e) {
|
||||
throw new BaseException("生成阿里云签名摘要失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 HMAC-SHA256 十六进制签名。
|
||||
*
|
||||
* @param content 签名原文
|
||||
* @param secret AccessKey Secret
|
||||
* @return 十六进制签名
|
||||
*/
|
||||
private String hmacSha256Hex(String content, String secret) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
||||
return bytesToHex(mac.doFinal(content.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (Exception e) {
|
||||
throw new BaseException("生成阿里云短信签名失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字节数组转十六进制字符串。
|
||||
*
|
||||
* @param bytes 字节数组
|
||||
* @return 十六进制字符串
|
||||
*/
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder builder = new StringBuilder(bytes.length * 2);
|
||||
for (byte item : bytes) {
|
||||
String hex = Integer.toHexString(item & 0xff);
|
||||
if (hex.length() == 1) {
|
||||
builder.append('0');
|
||||
}
|
||||
builder.append(hex);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
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";
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -3,34 +3,34 @@ package com.budwk.app.base.sms.model;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 通讯平台接收人信息。
|
||||
* 短信接收人信息。
|
||||
*/
|
||||
@Data
|
||||
public class MsgPlatformRecipient {
|
||||
/**
|
||||
* 接收人姓名,对应 person_info 中的第 1 段。
|
||||
* 接收人姓名,便于日志排查。
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 接收人账号,对应 person_info 中的第 2 段。
|
||||
* 短信场景一般传学工号;微信场景可传企业微信成员ID、学工号或手机号。
|
||||
* 接收人工号。
|
||||
* mobile 为空时,可用该字段按 sys_user.loginname 查询手机号。
|
||||
*/
|
||||
private String account;
|
||||
|
||||
/**
|
||||
* 接收人部门ID,对应 person_info 中的第 3 段。
|
||||
* 接收人部门ID,当前阿里云短信发送不使用,预留给调用方传递上下文。
|
||||
*/
|
||||
private String unitId;
|
||||
|
||||
/**
|
||||
* 接收人部门名称,对应 person_info 中的第 4 段。
|
||||
* 接收人部门名称,当前阿里云短信发送不使用,预留给调用方传递上下文。
|
||||
*/
|
||||
private String unitName;
|
||||
|
||||
/**
|
||||
* 接收人手机号,对应短信 person_info 中的第 5 段。
|
||||
* 微信接口不会使用该字段,微信接口会自动补空串以保留分隔符格式。
|
||||
* 接收人手机号。
|
||||
* 阿里云短信发送时优先使用该字段,必须为 11 位大陆手机号。
|
||||
*/
|
||||
private String mobile;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.budwk.app.base.sms.model;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 通讯平台响应结果。
|
||||
* 短信发送响应结果。
|
||||
*/
|
||||
@Data
|
||||
public class MsgPlatformResponse {
|
||||
@@ -13,37 +13,37 @@ public class MsgPlatformResponse {
|
||||
private Integer httpStatus;
|
||||
|
||||
/**
|
||||
* 平台返回业务状态码,0 表示成功。
|
||||
* 业务状态码,0 表示阿里云返回 OK。
|
||||
*/
|
||||
private Integer code;
|
||||
|
||||
/**
|
||||
* 平台返回消息提示。
|
||||
* 阿里云返回消息提示。
|
||||
*/
|
||||
private String msg;
|
||||
|
||||
/**
|
||||
* 平台返回业务是否成功。
|
||||
* 是否发送成功。
|
||||
*/
|
||||
private Boolean result;
|
||||
|
||||
/**
|
||||
* 发送成功后的消息ID。
|
||||
* 发送成功后的阿里云 BizId。
|
||||
*/
|
||||
private String msgId;
|
||||
|
||||
/**
|
||||
* 短信剩余额度,仅短信接口返回。
|
||||
* 短信剩余额度,当前阿里云接口不返回,预留兼容字段。
|
||||
*/
|
||||
private Integer remainingQuota;
|
||||
|
||||
/**
|
||||
* 平台失败描述。
|
||||
* 阿里云返回 Code,失败时可用于定位错误类型。
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 平台原始响应报文,排查问题时可直接查看。
|
||||
* 阿里云原始响应报文,排查问题时可直接查看。
|
||||
*/
|
||||
private String rawBody;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,42 @@
|
||||
package com.budwk.app.base.sms.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 短信发送请求参数。
|
||||
* 阿里云短信发送请求参数。
|
||||
*/
|
||||
public class MsgPlatformSmsRequest extends MsgPlatformBaseRequest {
|
||||
@Data
|
||||
public class MsgPlatformSmsRequest {
|
||||
/**
|
||||
* 接收人列表。
|
||||
* mobile 为接收手机号;mobile 为空但 account 不为空时,会按工号查询 sys_user.mobile。
|
||||
*/
|
||||
private List<MsgPlatformRecipient> recipients;
|
||||
|
||||
/**
|
||||
* 阿里云短信模板 Code。
|
||||
* 为空时使用 sms.aliyun.template-code 配置。
|
||||
*/
|
||||
private String templateId;
|
||||
|
||||
/**
|
||||
* 短信模板变量内容。
|
||||
* customVar 为空时,会使用 sms.aliyun.template-param-name 作为变量名包装该内容。
|
||||
*/
|
||||
private String info;
|
||||
|
||||
/**
|
||||
* 阿里云模板变量。
|
||||
* key 为模板变量名,value 为变量内容,例如模板中有 ${content} 时传 content -> 具体通知内容。
|
||||
*/
|
||||
private Map<String, String> customVar;
|
||||
|
||||
/**
|
||||
* 阿里云短信签名。
|
||||
* 为空时使用 sms.aliyun.sign-name 配置。
|
||||
*/
|
||||
private String sendUserSign;
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -58,8 +58,8 @@ public class ProcessTaskStartEventListener implements ProcessEventListener {
|
||||
List<ProcessTaskActor> taskActors = dao.query(ProcessTaskActor.class, Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId()));
|
||||
|
||||
for (ProcessTaskActor taskActor : taskActors) {
|
||||
// 模拟发送消息
|
||||
String message = StrUtil.format("您有一条待办任务,实例:{},处理环节:{}", taskActor.getActorName(), instanceName, taskDisplayName);
|
||||
// 待办通知内容会同步进入本地消息和短信模板变量,需明确包含接收人、实例和处理环节。
|
||||
String message = StrUtil.format("{},您有一条待办任务,实例:{},处理环节:{}", taskActor.getActorName(), instanceName, taskDisplayName);
|
||||
log.info(message);
|
||||
globalMessageSendService.sendMessage(taskDisplayName, message, 2, List.of(taskActor.getActorId()), null);
|
||||
}
|
||||
|
||||
@@ -148,6 +148,7 @@ public class SysHomeController {
|
||||
List<Sys_menu> menus = sysUserService.getMenus(SecurityUtil.getUserId());
|
||||
List<String> allMenuIds = menus.stream().map(Sys_menu::getId).toList();
|
||||
List<Sys_menu> sysMenus = list.stream()
|
||||
.filter(m -> allMenuIds.contains(m.getId()))
|
||||
.sorted(Comparator.comparing(Sys_menu::getLocation, Comparator.nullsLast(Integer::compareTo))
|
||||
.thenComparing(Sys_menu::getId))
|
||||
.toList();
|
||||
@@ -166,8 +167,11 @@ public class SysHomeController {
|
||||
);
|
||||
List<Sys_menu> menus = sysUserService.getMenus(SecurityUtil.getUserId());
|
||||
List<String> allMenuIds = menus.stream().map(Sys_menu::getId).toList();
|
||||
//List<Sys_menu> sysMenus = list.stream().filter(m -> allMenuIds.contains(m.getId())).sorted(Comparator.comparingInt(Sys_menu::getLocation)).toList();
|
||||
List<Sys_menu> sysMenus = list.stream().sorted(Comparator.comparingInt(Sys_menu::getLocation)).toList();
|
||||
List<Sys_menu> sysMenus = list.stream()
|
||||
.filter(m -> allMenuIds.contains(m.getId()))
|
||||
.sorted(Comparator.comparing(Sys_menu::getLocation, Comparator.nullsLast(Integer::compareTo))
|
||||
.thenComparing(Sys_menu::getId))
|
||||
.toList();
|
||||
return Result.success(sysMenus);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.budwk.app.sys.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.sys.services.SysMobileSsoService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.log.Log;
|
||||
import org.nutz.log.Logs;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Map;
|
||||
/**
|
||||
* 移动端单点登录控制器,提供授权跳转、授权地址获取和移动门户回调入口。
|
||||
*/
|
||||
@IocBean
|
||||
@At("/platform/h5/sso")
|
||||
@Api(tags = "移动端单点登录")
|
||||
public class SysMobileSsoController {
|
||||
private static final Log log = Logs.get();
|
||||
|
||||
@Inject
|
||||
private SysMobileSsoService sysMobileSsoService;
|
||||
|
||||
/**
|
||||
* 直接跳转到移动门户授权页面,适合浏览器访问发起单点登录。
|
||||
*
|
||||
* @param state 透传参数,可传本系统登录成功后的目标路径
|
||||
* @return 移动门户授权页重定向地址
|
||||
*/
|
||||
@At("/authorize")
|
||||
@Ok("re")
|
||||
@ApiOperation("移动端单点登录授权跳转")
|
||||
public String authorize(@Param("state") String state) {
|
||||
return ">>:" + sysMobileSsoService.buildAuthorizeUrl(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回移动门户授权地址,适合前端自行控制跳转时调用。
|
||||
*
|
||||
* @param state 透传参数,可传本系统登录成功后的目标路径
|
||||
* @return 包含授权地址的 JSON 结果
|
||||
*/
|
||||
@At("/authUrl")
|
||||
@Ok("json")
|
||||
@ApiOperation("获取移动端单点登录授权地址")
|
||||
public Object authUrl(@Param("state") String state) {
|
||||
return Result.success(Map.of("url", sysMobileSsoService.buildAuthorizeUrl(state)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动门户授权回调入口,接收 code 后换取用户学工号并登录本系统。
|
||||
*
|
||||
* @param code 移动门户返回的 authorization code
|
||||
* @param state 移动门户原样带回的透传参数
|
||||
* @param request 当前 HTTP 请求
|
||||
* @return 登录成功或失败后的重定向地址
|
||||
*/
|
||||
@At("/callback")
|
||||
@Ok("re")
|
||||
@ApiOperation("移动端单点登录回调")
|
||||
public String callback(@Param("code") String code, @Param("state") String state, HttpServletRequest request) {
|
||||
try {
|
||||
if (StrUtil.isBlank(code)) {
|
||||
return ">>:/platform/home/500";
|
||||
}
|
||||
return ">>:" + sysMobileSsoService.loginByAuthorizationCode(code, state, request);
|
||||
} catch (BaseException e) {
|
||||
log.error("移动端单点登录失败:" + e.getMessage(), e);
|
||||
return ">>:/platform/home/500";
|
||||
} catch (Exception e) {
|
||||
log.error("移动端单点登录异常", e);
|
||||
return ">>:/platform/home/500";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.budwk.app.sys.services;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 移动端单点登录服务,负责对接移动门户 Authorization Code 授权码方案。
|
||||
*/
|
||||
public interface SysMobileSsoService {
|
||||
|
||||
/**
|
||||
* 生成移动门户授权地址,供前端或浏览器跳转获取授权码。
|
||||
*
|
||||
* @param state 授权回调透传参数,可用于保存登录成功后的本系统跳转地址
|
||||
* @return 移动门户 OAuth 授权地址
|
||||
*/
|
||||
String buildAuthorizeUrl(String state);
|
||||
|
||||
/**
|
||||
* 使用移动门户回调的授权码完成本系统登录。
|
||||
*
|
||||
* @param code 移动门户回调返回的 authorization code
|
||||
* @param state 移动门户原样透传的 state 参数
|
||||
* @param request 当前登录请求,用于写入 Sa-Token 登录态和记录登录日志
|
||||
* @return 登录成功后的本系统跳转地址
|
||||
*/
|
||||
String loginByAuthorizationCode(String code, String state, HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 根据授权码换取 access_token,并从返回结果中读取当前登录用户学工号。
|
||||
*
|
||||
* @param code 移动门户回调返回的 authorization code
|
||||
* @return 移动门户 token 接口返回的 JSON 数据
|
||||
*/
|
||||
JSONObject exchangeAccessToken(String code);
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package com.budwk.app.sys.services.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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.enums.LoginType;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.sys.models.Sys_user;
|
||||
import com.budwk.app.sys.services.SysMobileSsoService;
|
||||
import com.budwk.app.sys.services.SysUserService;
|
||||
import com.budwk.app.web.commons.base.Globals;
|
||||
import org.nutz.ioc.impl.PropertiesProxy;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.log.Log;
|
||||
import org.nutz.log.Logs;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 移动端单点登录服务实现,按移动门户文档方案一完成授权码换取用户身份并登录本系统。
|
||||
*/
|
||||
@IocBean
|
||||
public class SysMobileSsoServiceImpl implements SysMobileSsoService {
|
||||
private static final Log log = Logs.get();
|
||||
|
||||
private static final String DEFAULT_AUTHORIZE_URL = "http://app.jsahvc.edu.cn/interface/oauth/authorize.htm";
|
||||
private static final String DEFAULT_TOKEN_URL = "http://app.jsahvc.edu.cn/interface/oauth/token.htm";
|
||||
private static final String GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";
|
||||
private static final String RESPONSE_TYPE_CODE = "code";
|
||||
|
||||
@Inject
|
||||
private SysUserService sysUserService;
|
||||
@Inject
|
||||
private PropertiesProxy conf;
|
||||
|
||||
/**
|
||||
* 生成移动门户 OAuth 授权地址,授权成功后移动门户会携带 code 回调 redirect_uri。
|
||||
*
|
||||
* @param state 透传字段,通常用于保存本系统登录后的目标页面
|
||||
* @return 拼接完成的移动门户授权地址
|
||||
*/
|
||||
@Override
|
||||
public String buildAuthorizeUrl(String state) {
|
||||
String clientId = getRequiredConfig("mobile.sso.client-id", "移动端单点登录 client_id 未配置");
|
||||
String redirectUri = getRedirectUri();
|
||||
String authorizeUrl = conf.get("mobile.sso.authorize-url", DEFAULT_AUTHORIZE_URL);
|
||||
|
||||
Map<String, String> params = new LinkedHashMap<>();
|
||||
params.put("response_type", RESPONSE_TYPE_CODE);
|
||||
params.put("client_id", clientId);
|
||||
params.put("redirect_uri", redirectUri);
|
||||
if (StrUtil.isNotBlank(state)) {
|
||||
params.put("state", state);
|
||||
}
|
||||
return authorizeUrl + "?" + buildQuery(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据授权码换取移动门户 access_token,并使用返回的 username 登录本系统。
|
||||
*
|
||||
* @param code 移动门户回调返回的 authorization code
|
||||
* @param state 移动门户原样透传的 state 参数
|
||||
* @param request 当前 HTTP 请求
|
||||
* @return 登录完成后的跳转地址
|
||||
*/
|
||||
@Override
|
||||
public String loginByAuthorizationCode(String code, String state, HttpServletRequest request) {
|
||||
if (StrUtil.isBlank(code)) {
|
||||
throw new BaseException("移动端单点登录授权码不能为空");
|
||||
}
|
||||
JSONObject tokenJson = exchangeAccessToken(code);
|
||||
log.debug("移动端单点登录: "+code+","+state);
|
||||
log.debug("移动端单点登录tokenJson返回内容:" + tokenJson);
|
||||
String loginName = tokenJson.getStr("username");
|
||||
if (StrUtil.isBlank(loginName)) {
|
||||
log.error("移动端单点登录未返回 username,返回内容:" + tokenJson);
|
||||
throw new BaseException("移动端单点登录未返回用户学工号");
|
||||
}
|
||||
|
||||
// 校验第三方返回的学工号必须存在于本系统,避免创建未知登录态。
|
||||
sysUserService.checkThirdPlatformLoginName(loginName);
|
||||
Sys_user sysUser = sysUserService.loginByLoginName(loginName);
|
||||
|
||||
// 复用系统统一登录增强逻辑,写入 Sa-Token 会话、用户扩展信息和登录日志。
|
||||
sysUserService.loginPlus(sysUser, LoginType.CAS, request);
|
||||
return resolveRedirectUrl(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求移动门户 token 接口,将 authorization code 换成 access_token 和 username。
|
||||
*
|
||||
* @param code 移动门户授权码
|
||||
* @return 移动门户 token 接口 JSON 响应
|
||||
*/
|
||||
@Override
|
||||
public JSONObject exchangeAccessToken(String code) {
|
||||
String clientId = getRequiredConfig("mobile.sso.client-id", "移动端单点登录 client_id 未配置");
|
||||
String clientSecret = getRequiredConfig("mobile.sso.client-secret", "移动端单点登录 client_secret 未配置");
|
||||
String tokenUrl = conf.get("mobile.sso.token-url", DEFAULT_TOKEN_URL);
|
||||
|
||||
Map<String, Object> form = new LinkedHashMap<>();
|
||||
form.put("grant_type", GRANT_TYPE_AUTHORIZATION_CODE);
|
||||
form.put("client_id", clientId);
|
||||
form.put("client_secret", clientSecret);
|
||||
form.put("code", code);
|
||||
form.put("redirect_uri", getRedirectUri());
|
||||
|
||||
try (HttpResponse response = HttpRequest.post(tokenUrl)
|
||||
.basicAuth(clientId, clientSecret)
|
||||
.form(form)
|
||||
.timeout(conf.getInt("mobile.sso.timeout", 10000))
|
||||
.execute()) {
|
||||
String body = response.body();
|
||||
log.debug("移动端单点登录返回内容:" + body);
|
||||
if (!response.isOk()) {
|
||||
log.error("移动端单点登录 token 接口请求失败,状态码:" + response.getStatus() + ",返回内容:" + body);
|
||||
throw new BaseException("移动端单点登录 token 接口请求失败");
|
||||
}
|
||||
if (!JSONUtil.isTypeJSON(body)) {
|
||||
log.error("移动端单点登录 token 接口返回非 JSON 内容:" + body);
|
||||
throw new BaseException("移动端单点登录 token 接口返回格式错误");
|
||||
}
|
||||
JSONObject tokenJson = JSONUtil.parseObj(body);
|
||||
if (StrUtil.isBlank(tokenJson.getStr("access_token"))) {
|
||||
log.error("移动端单点登录 token 接口未返回 access_token,返回内容:" + tokenJson);
|
||||
throw new BaseException("移动端单点登录 token 获取失败");
|
||||
}
|
||||
return tokenJson;
|
||||
} catch (BaseException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("移动端单点登录 token 接口调用异常", e);
|
||||
throw new BaseException("移动端单点登录 token 接口调用异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取移动门户回调地址,默认使用当前系统域名拼接本控制器 callback 地址。
|
||||
*
|
||||
* @return 与授权请求、token 请求保持一致的 redirect_uri
|
||||
*/
|
||||
private String getRedirectUri() {
|
||||
String redirectUri = conf.get("mobile.sso.redirect-uri", "");
|
||||
if (StrUtil.isNotBlank(redirectUri)) {
|
||||
return redirectUri;
|
||||
}
|
||||
return Globals.AppDomain + "/platform/h5/sso/callback";
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取必填配置,缺失时给出明确业务异常,避免使用空密钥请求移动门户。
|
||||
*
|
||||
* @param key 配置键
|
||||
* @param errorMessage 配置缺失提示
|
||||
* @return 配置值
|
||||
*/
|
||||
private String getRequiredConfig(String key, String errorMessage) {
|
||||
String value = conf.get(key, "");
|
||||
if (StrUtil.isBlank(value)) {
|
||||
throw new BaseException(errorMessage);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接 URL 查询参数,统一做 UTF-8 编码防止 redirect_uri、state 中特殊字符破坏授权地址。
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @return 编码后的查询字符串
|
||||
*/
|
||||
private String buildQuery(Map<String, String> params) {
|
||||
return params.entrySet().stream()
|
||||
.map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue()))
|
||||
.reduce((left, right) -> left + "&" + right)
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 对 URL 参数进行 UTF-8 编码。
|
||||
*
|
||||
* @param value 参数原始值
|
||||
* @return 编码后的参数值
|
||||
*/
|
||||
private String encode(String value) {
|
||||
return URLEncoder.encode(StrUtil.nullToEmpty(value), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 state 解析登录后的跳转地址,只允许本系统内路径,避免开放重定向风险。
|
||||
*
|
||||
* @param state 移动门户透传参数
|
||||
* @return 登录成功后的跳转地址
|
||||
*/
|
||||
private String resolveRedirectUrl(String state) {
|
||||
if (StrUtil.isNotBlank(state) && state.startsWith("/") && !state.startsWith("//")) {
|
||||
return Globals.AppDomain + state;
|
||||
}
|
||||
return Globals.AppDomain + "/platform/h5";
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ 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;
|
||||
@@ -106,7 +105,6 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
|
||||
ThreadUtil.execute(() -> {
|
||||
for (String loginName : loginNames) {
|
||||
smsService.sendSmsByAccount(loginName,sysMsg.getNote());
|
||||
smsService.sendWechatTextByAccountOrMobile(loginName,sysMsg.getNote());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -125,7 +125,7 @@ public class CommonController {
|
||||
@At
|
||||
@SaCheckLogin
|
||||
@ApiOperation("单位列表")
|
||||
public Result unitOptions(String unionId) {
|
||||
public Result unitOptions(String unionId, Integer unitLevel) {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
unit.*,
|
||||
@@ -138,6 +138,8 @@ public class CommonController {
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.andEX("unit.unionId", "=", unionId);
|
||||
cnd.and("unit.unitTypeCode", "=", "1");
|
||||
// unitLevel 为可选筛选条件,不传时保持原有单位列表查询逻辑。
|
||||
cnd.andEX("unit.unitLevel", "=", unitLevel);
|
||||
cnd.asc("unit.unitcode");
|
||||
sql.setCondition(cnd);
|
||||
List<NutMap> unitList = sysUserService.listMap(sql);
|
||||
|
||||
+1
-1
@@ -329,7 +329,7 @@ public class TrainSignUpManageController {
|
||||
public Result selectUnitAndClub() {
|
||||
List<NutMap> result = new ArrayList<>();
|
||||
java.util.Set<String> optionKeys = new java.util.HashSet<>();
|
||||
List<Sys_unit> unitList = dao.query(Sys_unit.class, Cnd.where(Sys_unit::getUnitLevel, "=", 2).asc(Sys_unit::getUnitcode));
|
||||
List<Sys_unit> unitList = dao.query(Sys_unit.class, Cnd.where(Sys_unit::getUnitLevel, "=", 3).asc(Sys_unit::getUnitcode));
|
||||
for (Sys_unit unit : unitList) {
|
||||
addSelectOption(result, optionKeys, unit.getId(), unit.getName(), "unit");
|
||||
}
|
||||
|
||||
+8
-3
@@ -241,10 +241,15 @@ public class ActivityWorksCollectionManageController {
|
||||
@SaCheckPermission(value = {"activity.workscollection.manage", "activity.workscollection.new"}, mode = SaMode.OR)
|
||||
public Result findOne(@Valid String id) {
|
||||
Activity_works_collection worksCollection = dao.fetch(Activity_works_collection.class, id);
|
||||
if (worksCollection == null) {
|
||||
return Result.error("活动不存在");
|
||||
}
|
||||
dao.fetchLinks(worksCollection, "subjectTypes");
|
||||
worksCollection.getSubjectTypes().forEach(item -> {
|
||||
dao.fetchLinks(item, "worksTypes");
|
||||
});
|
||||
if (worksCollection.getSubjectTypes() != null) {
|
||||
worksCollection.getSubjectTypes().forEach(item -> {
|
||||
dao.fetchLinks(item, "worksTypes");
|
||||
});
|
||||
}
|
||||
return Result.success(worksCollection);
|
||||
}
|
||||
|
||||
|
||||
+9
-11
@@ -74,23 +74,23 @@ public class SmsMessageSendStrategy implements GlobalMessageSendStrategy {
|
||||
|
||||
/**
|
||||
* 发送短信消息。
|
||||
* 接收人由全局消息服务传入的用户ID或登录名查询得到,发送时使用用户工号对接通讯平台;
|
||||
* 返回结果由 SmsService 内部解析并记录,当前策略只负责逐个用户触发发送并记录异常。
|
||||
* 接收人由全局消息服务传入的用户ID或登录名查询得到,发送时使用用户手机号对接阿里云短信;
|
||||
* 单个用户手机号缺失或发送失败只记录日志,不影响其他待审核人接收短信。
|
||||
*/
|
||||
private void sendSmsMessage(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());
|
||||
String mobile = user.getMobile();
|
||||
if (mobile == null || mobile.trim().isEmpty()) {
|
||||
log.warn("用户{}没有手机号,跳过短信发送", user.getUsername());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 构建短信内容
|
||||
String smsContent = buildSmsContent(title, content, type);
|
||||
|
||||
smsService.sendSmsByAccount(loginName, smsContent);
|
||||
log.info("短信发送完成:用户{},工号:{}", user.getUsername(), loginName);
|
||||
smsService.sendSmsByMobile(mobile, smsContent);
|
||||
log.info("短信发送完成:用户{},手机号:{}", user.getUsername(), mobile);
|
||||
} catch (Exception e) {
|
||||
log.error("发送短信给用户{}时出错:{}", user.getUsername(), e.getMessage(), e);
|
||||
}
|
||||
@@ -103,10 +103,8 @@ public class SmsMessageSendStrategy implements GlobalMessageSendStrategy {
|
||||
private String buildSmsContent(String title, String content, Integer type) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
// 根据消息类型添加前缀
|
||||
if (type != null && type == 2) {
|
||||
sb.append("【待办通知】");
|
||||
} else {
|
||||
// 待办短信由阿里云模板统一展示“待办提醒”,这里不再重复拼接【待办通知】前缀。
|
||||
if (type == null || type != 2) {
|
||||
sb.append("【系统通知】");
|
||||
}
|
||||
|
||||
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
nutz:
|
||||
application:
|
||||
name: zhgh_cug_v4
|
||||
name: zhgh_jsahvc
|
||||
mvc:
|
||||
ignore: ^(.+[.])(jsp|png|gif|jpg|js|css|jspx|jpeg|html|mp3|mp4|ico|svg|vue)$
|
||||
exclusions: /favicon/*,/assets/*,/druid/*,/upload/*,/components/*
|
||||
@@ -21,7 +21,7 @@ jetty:
|
||||
#结合ftp使用,或用nginx代理ftp路径
|
||||
#staticPath: /Users/wizzer/temp/files
|
||||
#开发模式静态资源
|
||||
staticPathLocal: E:/projects/zhgh_cug_v4/src/main/resources/static
|
||||
staticPathLocal: E:/projects/zhgh_jsahvc_new/src/main/resources/static
|
||||
|
||||
security:
|
||||
tokenName: saToken
|
||||
@@ -49,7 +49,6 @@ cas:
|
||||
enable: false
|
||||
server-url-prefix: https://i.njupt.edu.cn/cas
|
||||
server-login-url: https://i.njupt.edu.cn/cas/login
|
||||
qi-wx-server-login-url: httos:/cas.whcp.eou.cn/lyuapServer/wechatauz/wxAuthRedirect
|
||||
client-host-url: https://zhgh.njupt.edu.cn
|
||||
client-call-back-url: /platform/sso/login
|
||||
validation-type: cas
|
||||
@@ -71,8 +70,9 @@ redis:
|
||||
#nodes=192.168.6.31:6377,192.168.6.31:6378,192.168.6.28:6377,192.168.6.28:6378,192.168.6.34:6377,192.168.6.34:6378
|
||||
|
||||
jdbc:
|
||||
url: jdbc:mysql://192.168.21.215:3306/zhgh_cug_v4?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
url: jdbc:mysql://192.168.21.215:3306/zhgh_jsahvc_new?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
username: root
|
||||
# password: wqnmd2331
|
||||
password: root
|
||||
validationQuery: select 1
|
||||
maxActive: 1000
|
||||
@@ -87,7 +87,7 @@ xsssql:
|
||||
beetl:
|
||||
RESOURCE:
|
||||
#本地路径,开发时设置本地路径,便于调试
|
||||
rootLocal: E:/projects/zhgh_cug_v4/src/main/resources/views/
|
||||
rootLocal: E:/projects/zhgh_jsahvc_new/src/main/resources/views/
|
||||
root: views/
|
||||
DELIMITER_STATEMENT_START: "<!--#"
|
||||
DELIMITER_STATEMENT_END: "#-->"
|
||||
@@ -165,40 +165,19 @@ minio:
|
||||
endPoint: http://192.168.21.214:9000
|
||||
bucket: njupt
|
||||
|
||||
data-center:
|
||||
grant_type: password
|
||||
scope: read
|
||||
username: 3848A3C738EFBB2C2E76E0D59B419DACA16A80F3AB0354CD
|
||||
password: 3848A3C738EFBB2C2E76E0D59B419DACA16A80F3AB0354CD
|
||||
token-url: https://bd.whcp.edu.cn/sjjkfwpt/api/bd-api/oauth/token
|
||||
urls:
|
||||
teacher: https://bd.whcp.edu.cn/sjjkfwpt/api/bd-api/get/JZGJCSJZLB
|
||||
unit: https://bd.whcp.edu.cn/sjjkfwpt/api/bd-api/get/YXSDWJBSJZLB
|
||||
msg:
|
||||
platform:
|
||||
sms:
|
||||
aliyun:
|
||||
# 是否启用阿里云短信。未启用时不会调用阿里云,避免开发环境误发短信。
|
||||
enabled: false
|
||||
# 地大通讯平台基础地址,默认对接正式地址
|
||||
base-url: https://msg.cug.edu.cn
|
||||
# 授权系统名称,对应文档中的 tp_name
|
||||
tp-name : nwszl.cug.edu.cn
|
||||
# 便捷发送方法默认发送人工号,controller 只传接收人和内容时会读取这里
|
||||
default-send-user-id: 790036
|
||||
# 便捷发送方法默认发送人姓名,controller 只传接收人和内容时会读取这里
|
||||
default-send-user-name: 周斐霏
|
||||
# 便捷发送方法默认发送机构ID,可不填
|
||||
default-send-unit-id: 421
|
||||
# 便捷发送方法默认发送机构名称,可不填
|
||||
default-send-unit-name: 工会
|
||||
# 便捷发送方法默认签名,可不填
|
||||
default-send-user-sign:
|
||||
#短信中如果不包含链接和电话,可以使用公共模板ID:
|
||||
default-template-id: 1978744168869937153
|
||||
# 原始密钥,未提前加密时可直接填这里,程序会按 Base64 后 SHA 自动计算 secret_key
|
||||
raw-secret-key: Z21kbDlwYWJ4c2Vtbnk4NjlxeQ==
|
||||
# 如果对方已经提供了可直接使用的 secret_key,也可以直接配置该值,优先级高于 raw-secret-key
|
||||
encrypted-secret-key:
|
||||
# HTTP连接超时时间,单位毫秒
|
||||
endpoint: dysmsapi.aliyuncs.com
|
||||
access-key-id: LTAI5t6CeHgF4fnsXca8Xwee
|
||||
access-key-secret: 09hqxEplUSAgwn02Pzv4UuAoUxZoD2
|
||||
# 阿里云短信签名名称,必须使用审核通过的签名。
|
||||
sign-name: 江苏农牧科技职业学院
|
||||
# 阿里云短信模板 Code,必须使用审核通过的模板。
|
||||
template-code: SMS_508220213
|
||||
# 模板变量名。例如模板内容为“您有一条待办:${title}”,这里填 title。
|
||||
template-param-name: title
|
||||
connect-timeout: 5000
|
||||
# HTTP读取超时时间,单位毫秒
|
||||
read-timeout: 10000
|
||||
|
||||
|
||||
@@ -1,17 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<configuration scan="false" scanPeriod="60000" debug="false">
|
||||
|
||||
<!-- 定义日志文件的存储路径 -->
|
||||
<!-- 日志存储路径 -->
|
||||
<property name="LOG_HOME" value="./logs"/>
|
||||
|
||||
<!-- 控制台输出配置 -->
|
||||
<!-- 控制台输出 -->
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- INFO级别日志文件配置 -->
|
||||
|
||||
<!--
|
||||
#################################################################################
|
||||
# #
|
||||
# 全文输出日志(一个文件存储)start #
|
||||
# #
|
||||
#################################################################################
|
||||
-->
|
||||
|
||||
<!-- 所有级别日志写入同一个文件 -->
|
||||
<appender name="ALL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/app.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/app-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>300</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- 框架日志级别(按需调整) -->
|
||||
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||
<logger name="org.quartz" level="INFO"/>
|
||||
<logger name="org.nutz" level="DEBUG"/>
|
||||
|
||||
<!-- root logger:DEBUG 级别,输出到控制台和全量文件 -->
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
<appender-ref ref="ALL_FILE"/>
|
||||
</root>
|
||||
|
||||
<!--
|
||||
#################################################################################
|
||||
# #
|
||||
# 全文输出日志(一个文件存储)end #
|
||||
# #
|
||||
#################################################################################
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
#################################################################################
|
||||
# #
|
||||
# debug、info、error分文件输出 start #
|
||||
# #
|
||||
#################################################################################
|
||||
-->
|
||||
|
||||
<!-- DEBUG级别日志文件配置 -->
|
||||
<!--<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/debug.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<level>DEBUG</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/debug-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>300</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!– INFO级别日志文件配置 –>
|
||||
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/info.log</file>
|
||||
<encoder>
|
||||
@@ -24,11 +92,11 @@
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/info-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
<maxHistory>300</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- ERROR级别日志文件配置 -->
|
||||
<!– ERROR级别日志文件配置 –>
|
||||
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/error.log</file>
|
||||
<encoder>
|
||||
@@ -41,19 +109,31 @@
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/error-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
<maxHistory>300</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<logger name="java" additivity="false" />
|
||||
<!– 框架日志级别 –>
|
||||
<!– <logger name="java" additivity="false" />–>
|
||||
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||
<logger name="org.quartz" level="INFO"/>
|
||||
<logger name="org.nutz" level="DEBUG"/>
|
||||
|
||||
<!-- 日志级别和appender的关联 -->
|
||||
<!– 日志级别和appender的关联 –>
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
<appender-ref ref="DEBUG_FILE"/>
|
||||
<appender-ref ref="INFO_FILE"/>
|
||||
<appender-ref ref="ERROR_FILE"/>
|
||||
</root>
|
||||
</root>-->
|
||||
|
||||
<!--
|
||||
#################################################################################
|
||||
# #
|
||||
# debug、info、error分文件输出 end #
|
||||
# #
|
||||
#################################################################################
|
||||
-->
|
||||
|
||||
</configuration>
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ const businessTool = {
|
||||
}
|
||||
})
|
||||
},
|
||||
listUnit(unionId = null) {
|
||||
listUnit(unionId = null, unitLevel = null) {
|
||||
return commonUtil
|
||||
.axiosService()
|
||||
.post("/open/common/unitOptions", {unionId})
|
||||
.post("/open/common/unitOptions", {unionId, unitLevel})
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
return resp.data
|
||||
|
||||
@@ -1034,7 +1034,7 @@ const ACTIVITY_CULTURE_APPLY_ACTIVITY = {
|
||||
this.clubOptions.map((v) => {
|
||||
v.name = v.clubName
|
||||
})
|
||||
const units = await this.$businessTool.listUnit()
|
||||
const units = await this.$businessTool.listUnit(null, 3)
|
||||
this.unitOptions = this.clubOptions.concat(units)
|
||||
} finally {
|
||||
this.formLoading = false
|
||||
|
||||
@@ -1285,7 +1285,7 @@
|
||||
this.clubOptions.map((v) => {
|
||||
v.name = v.clubName
|
||||
})
|
||||
const units = await this.$businessTool.listUnit()
|
||||
const units = await this.$businessTool.listUnit(null, 3)
|
||||
this.unitOptions = this.clubOptions.concat(units)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ layout("/layouts/platform.html"){
|
||||
<style></style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<guava ref="guava" v-model="guavaIndex">
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<search @search="doSearch">
|
||||
@@ -386,6 +386,7 @@ layout("/layouts/platform.html"){
|
||||
formOptionsPromise: null,
|
||||
unitOptions: [],
|
||||
clubOptions: [],
|
||||
guavaIndex: window.location.pathname.toLowerCase() === "/platform/activity/workscollection/new" ? "edit" : "index",
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@@ -409,6 +410,7 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
afterSaveSuccess() {
|
||||
this.formVisible = false
|
||||
this.guavaIndex = "index"
|
||||
if (this.isNewPage()) {
|
||||
this.backToManage()
|
||||
} else {
|
||||
@@ -416,14 +418,14 @@ layout("/layouts/platform.html"){
|
||||
this.$refs.guava.index()
|
||||
}
|
||||
},
|
||||
initNewPage() {
|
||||
async initNewPage() {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const mode = params.get("mode")
|
||||
const id = params.get("id")
|
||||
if (mode === "edit" && id) {
|
||||
this.openEdit(id, true)
|
||||
await this.openEdit(id, true)
|
||||
} else {
|
||||
this.openAdd()
|
||||
await this.openAdd()
|
||||
}
|
||||
},
|
||||
async getActivityGroup() {
|
||||
@@ -454,7 +456,7 @@ layout("/layouts/platform.html"){
|
||||
this.formOptionsPromise = Promise.all([
|
||||
this.getActivityGroup(),
|
||||
this.getClubsByRole(),
|
||||
this.$businessTool.listUnit()
|
||||
this.$businessTool.listUnit(null, 3)
|
||||
]).then(([activityGroupList, clubOptions, units]) => {
|
||||
this.activityGroupList = activityGroupList
|
||||
this.clubOptions = clubOptions
|
||||
@@ -501,7 +503,7 @@ layout("/layouts/platform.html"){
|
||||
this.copyTemplateName = ""
|
||||
await this.loadFormOptions()
|
||||
this.formVisible = true
|
||||
this.$refs.guava.edit()
|
||||
this.guavaIndex = "edit"
|
||||
this.$nextTick(() => {
|
||||
this.formData = {
|
||||
nbCount: 0,
|
||||
@@ -574,10 +576,14 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
async openEdit(id, copyTemplateMode = false) {
|
||||
this.copyTemplateMode = !!copyTemplateMode
|
||||
const [resp] = await Promise.all([
|
||||
this.$axios.post("/platform/activity/worksCollection/manage/findOne", {id}),
|
||||
this.loadFormOptions()
|
||||
])
|
||||
this.loadFormOptions().catch(() => {
|
||||
this.$message.warning("下拉数据加载失败,请刷新后重试")
|
||||
})
|
||||
const resp = await this.$axios.post("/platform/activity/worksCollection/manage/findOne", {id})
|
||||
if (!resp || resp.code !== 0 || !resp.data) {
|
||||
this.$message.warning(resp && resp.msg ? resp.msg : "活动数据不存在")
|
||||
return
|
||||
}
|
||||
this.formVisible = true
|
||||
this.formData = resp.data
|
||||
if (this.copyTemplateMode) {
|
||||
@@ -592,12 +598,11 @@ layout("/layouts/platform.html"){
|
||||
// this.formData.nbTime = [this.formData.nbStartDateTime, this.formData.nbEndDateTime]
|
||||
this.$set(this.formData, "nbTime", [this.formData.nbStartDateTime, this.formData.nbEndDateTime])
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.$refs.guava.edit()
|
||||
})
|
||||
this.guavaIndex = "edit"
|
||||
},
|
||||
close() {
|
||||
this.formVisible = false
|
||||
this.guavaIndex = "index"
|
||||
if (this.isNewPage()) {
|
||||
this.backToManage()
|
||||
} else {
|
||||
@@ -734,7 +739,6 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
async created() {
|
||||
if (this.isNewPage()) {
|
||||
await this.loadFormOptions()
|
||||
this.$nextTick(() => {
|
||||
this.initNewPage()
|
||||
})
|
||||
|
||||
@@ -193,9 +193,9 @@ const apply = {
|
||||
<el-col :span="24">
|
||||
<el-form-item label="批量预约">
|
||||
<div style="display: flex; align-items: flex-start; column-gap: 12px; flex-wrap: wrap; line-height: 1.7;">
|
||||
<el-checkbox v-model="yearlyReserve">预约本年后续每周同一时段</el-checkbox>
|
||||
<el-checkbox v-model="yearlyReserve">预约本月后续每周同一时段</el-checkbox>
|
||||
<span style="color: #909399;">
|
||||
例如先选某个周一 09:00-11:00,勾选后会自动预约本年剩余所有周一的这个时段。
|
||||
例如先选某个周一 09:00-11:00,勾选后会自动预约本月剩余所有周一的这个时段。
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="yearlyReserve" style="margin-top: 10px; max-width: 320px;">
|
||||
|
||||
@@ -35,14 +35,6 @@
|
||||
<!-- <dict-select v-model="pageForm.preparedBys" placeholder="请选择编制类别" @change="doSearch"-->
|
||||
<!-- code="USER_PREPARED_BY_TYPE" multiple collapse-tags></dict-select>-->
|
||||
<!-- </search-item>-->
|
||||
<search-item label="人员分类">
|
||||
<dict-select v-model="pageForm.aidFundMemberUserType" code="AIDFUND_MEMBER_USER_TYPE"
|
||||
style="width: 100%"></dict-select>
|
||||
</search-item>
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-model="pageForm.userAttribute" code="USER_ATTRIBUTE"
|
||||
style="width: 100%"></dict-select>
|
||||
</search-item>
|
||||
</template>
|
||||
</search>
|
||||
`,
|
||||
|
||||
-10
@@ -30,14 +30,6 @@
|
||||
<!-- <dict-select v-model="pageForm.preparedBys" placeholder="请选择编制类别" @change="doSearch"-->
|
||||
<!-- code="USER_PREPARED_BY_TYPE" multiple collapse-tags></dict-select>-->
|
||||
<!-- </search-item>-->
|
||||
<search-item label="人员分类">
|
||||
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
|
||||
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
|
||||
</search-item>
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-model="pageForm.userAttribute" placeholder="请选择人员属性" @change="doSearch"
|
||||
code="USER_ATTRIBUTE"></dict-select>
|
||||
</search-item>
|
||||
</search>
|
||||
`,
|
||||
store,
|
||||
@@ -53,8 +45,6 @@
|
||||
userStates: [],
|
||||
personTypes: [],
|
||||
preparedBys: [],
|
||||
aidFundMemberUserType: '',
|
||||
userAttribute: '',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
-16
@@ -145,18 +145,6 @@ const MEMBER_CHANGE = {
|
||||
:disabled="allowFields('personType')" style="width: 100%"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="人员属性">
|
||||
<el-form-item prop="userAttribute">
|
||||
<dict-select v-model="formData.userAttribute" code="USER_ATTRIBUTE"
|
||||
style="width: 100%"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="人员分类">
|
||||
<el-form-item prop="aidFundMemberUserType">
|
||||
<dict-select v-model="formData.aidFundMemberUserType" code="AIDFUND_MEMBER_USER_TYPE"
|
||||
style="width: 100%"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
<template v-if="formData.userState == '退休'">
|
||||
<el-descriptions-item label="退休日期">
|
||||
@@ -455,8 +443,6 @@ const MEMBER_CHANGE = {
|
||||
userState,
|
||||
personType,
|
||||
preparedBy,
|
||||
userAttribute,
|
||||
aidFundMemberUserType,
|
||||
idCard,
|
||||
mobile,
|
||||
email,
|
||||
@@ -482,8 +468,6 @@ const MEMBER_CHANGE = {
|
||||
this.$set(this.formData, "userState", userState)
|
||||
this.$set(this.formData, "personType", personType)
|
||||
this.$set(this.formData, "preparedBy", preparedBy)
|
||||
this.$set(this.formData, "userAttribute", userAttribute)
|
||||
this.$set(this.formData, "aidFundMemberUserType", aidFundMemberUserType)
|
||||
this.$set(this.formData, "unitName", unit ? unit.name : null)
|
||||
this.$set(this.formData, "unitId", unit ? unit.id : null)
|
||||
this.$set(this.formData, "unionName", union ? union.name : null)
|
||||
|
||||
@@ -38,14 +38,6 @@ layout("/layouts/platform.html"){
|
||||
<!-- <dict-select v-model="pageForm.preparedBy" placeholder="请选择编制类别" @change="doSearch"-->
|
||||
<!-- code="USER_PREPARED_BY_TYPE"></dict-select>-->
|
||||
<!-- </search-item>-->
|
||||
<search-item label="人员分类">
|
||||
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
|
||||
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
|
||||
</search-item>
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-model="pageForm.userAttribute" placeholder="请选择人员属性" @change="doSearch"
|
||||
code="USER_ATTRIBUTE"></dict-select>
|
||||
</search-item>
|
||||
<search-item label="更新日期">
|
||||
<el-date-picker :picker-options="pickerOptions" @change="changeDateRangeChange"
|
||||
align="right" end-placeholder="结束日期" format="yyyy-MM-dd"
|
||||
@@ -63,16 +55,6 @@ layout("/layouts/platform.html"){
|
||||
设置会员
|
||||
</el-button>
|
||||
|
||||
<el-button :disabled="!checkUsers.length" @click="openBatchEditAidFundType" class="ml5" icon="el-icon-edit"
|
||||
size="small" type="primary">
|
||||
设置人员分类
|
||||
</el-button>
|
||||
|
||||
<el-button :disabled="!checkUsers.length" @click="openBatchEditUserAttribute" class="ml5" icon="el-icon-edit"
|
||||
size="small" type="primary">
|
||||
设置人员属性
|
||||
</el-button>
|
||||
|
||||
<el-button :disabled="!checkUsers.length" @click="openBatchEditUserState" class="ml5" icon="el-icon-edit"
|
||||
size="small" type="primary">
|
||||
设置在职状态
|
||||
@@ -230,49 +212,6 @@ layout("/layouts/platform.html"){
|
||||
<member-all-change ref="memberAllChangeRef"></member-all-change>
|
||||
</template>
|
||||
|
||||
<el-drawer
|
||||
:before-close="handleClose"
|
||||
:visible.sync="batchEditUserAttributeVisible"
|
||||
size="1000px" title="批量设置人员属性">
|
||||
<el-timeline style="margin-right: 40px">
|
||||
<el-timeline-item placement="top" timestamp="">
|
||||
<el-form :model="batchFormDataUserAttribute" label-width="100px" ref="batchFormUserAttribute">
|
||||
<el-row :gutter="60">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="人员属性">
|
||||
<dict-select
|
||||
@change="checkUsers.map(v => $set(v, 'userAttribute', batchFormDataUserAttribute.userAttribute))"
|
||||
clearable code="USER_ATTRIBUTE"
|
||||
placeholder="请选择人员属性"
|
||||
style="width: 300px"
|
||||
v-model="batchFormDataUserAttribute.userAttribute"></dict-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item placement="top" timestamp="">
|
||||
<el-table :data="checkUsers" class="vi-table" height="70vh" row-key="id" style="width: 100%">
|
||||
<el-table-column align="center" header-align="center" label="工号" prop="loginname" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="姓名" prop="username" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="所属单位" prop="unitName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="所属工会" prop="unionName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="人员属性" prop="userAttribute" show-overflow-tooltip width="300px">
|
||||
<template scope="{row}">
|
||||
<dict-select clearable code="USER_ATTRIBUTE" placeholder="请选择人员属性" style="width: 200px"
|
||||
v-model="row.userAttribute"></dict-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
|
||||
<span style="position: absolute;bottom: 20px;right: 20px">
|
||||
<el-button @click="batchEditUserAttributeVisible = false;checkUsers=[];$refs.table.clearSelection()">取消</el-button>
|
||||
<el-button :loading="batchEditLoading" @click="doBatchEditUserAttribute" type="primary">确定</el-button>
|
||||
</span>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
:before-close="handleClose"
|
||||
:visible.sync="batchEditMemberVisible"
|
||||
@@ -330,49 +269,6 @@ layout("/layouts/platform.html"){
|
||||
</span>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
:before-close="handleClose"
|
||||
:visible.sync="batchEditAidFundTypeVisible"
|
||||
size="1000px" title="批量设置人员分类">
|
||||
<el-timeline style="margin-right: 40px">
|
||||
<el-timeline-item placement="top" timestamp="">
|
||||
<el-form :model="batchFormDataAidFundType" label-width="100px" ref="batchFormAidFundType">
|
||||
<el-row :gutter="60">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="人员分类">
|
||||
<dict-select
|
||||
@change="checkUsers.map(v => $set(v, 'aidFundMemberUserType', batchFormDataAidFundType.aidFundMemberUserType))"
|
||||
clearable code="AIDFUND_MEMBER_USER_TYPE"
|
||||
placeholder="请选择人员分类"
|
||||
style="width: 300px"
|
||||
v-model="batchFormDataAidFundType.aidFundMemberUserType"></dict-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item placement="top" timestamp="">
|
||||
<el-table :data="checkUsers" class="vi-table" height="70vh" row-key="id" style="width: 100%">
|
||||
<el-table-column align="center" header-align="center" label="工号" prop="loginname" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="姓名" prop="username" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="所属单位" prop="unitName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="所属工会" prop="unionName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="人员分类" prop="aidFundMemberUserType" show-overflow-tooltip width="300px">
|
||||
<template scope="{row}">
|
||||
<dict-select clearable code="AIDFUND_MEMBER_USER_TYPE" placeholder="请选择人员分类" style="width: 200px"
|
||||
v-model="row.aidFundMemberUserType"></dict-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
|
||||
<span style="position: absolute;bottom: 20px;right: 20px">
|
||||
<el-button @click="batchEditAidFundTypeVisible = false;checkUsers=[];$refs.table.clearSelection()">取消</el-button>
|
||||
<el-button :loading="batchEditLoading" @click="doBatchEditAidFundType" type="primary">确定</el-button>
|
||||
</span>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
:before-close="handleClose"
|
||||
:visible.sync="batchEditUserStateVisible"
|
||||
@@ -513,22 +409,16 @@ layout("/layouts/platform.html"){
|
||||
changed: 2,
|
||||
isMember: 1,
|
||||
changeTypes: [],
|
||||
aidFundMemberUserType: "",
|
||||
userAttribute: "",
|
||||
},
|
||||
unions: [],
|
||||
units: [],
|
||||
id: "",
|
||||
checkUsers: [],
|
||||
batchEditLoading: false,
|
||||
batchEditUserAttributeVisible: false,
|
||||
batchEditMemberVisible: false,
|
||||
batchEditAidFundTypeVisible: false,
|
||||
batchEditUserStateVisible: false,
|
||||
unitMoveVisible: false,
|
||||
batchFormDataUserAttribute: {},
|
||||
batchFormDataMember: {member: false, welfareMember: false},
|
||||
batchFormDataAidFundType: {},
|
||||
batchFormDataUserState: {},
|
||||
unitMoveFormData: {
|
||||
unitId: "",
|
||||
@@ -553,8 +443,6 @@ layout("/layouts/platform.html"){
|
||||
{ prop: "postDoctoralJoinDate", label: "进站时间", sortable: true, checked: 0 },
|
||||
{ prop: "unionName", label: "所属工会", width: 120, sortable: true },
|
||||
{ prop: "unitName", label: "所属单位", width: 120, sortable: true },
|
||||
{ prop: "aidFundMemberUserType", label: "人员分类", width: 120, sortable: true },
|
||||
{ prop: "userAttribute", label: "人员属性", width: 120, sortable: true },
|
||||
{ prop: "idCardType", label: "证件类型", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "idCard", label: "证件号码", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "marriage", label: "婚姻状况", width: 120, sortable: true, checked: 0 },
|
||||
@@ -685,18 +573,10 @@ layout("/layouts/platform.html"){
|
||||
this.$refs.table.clearSelection()
|
||||
}).catch(() => {})
|
||||
},
|
||||
openBatchEditUserAttribute() {
|
||||
this.batchFormDataUserAttribute = {}
|
||||
this.batchEditUserAttributeVisible = true
|
||||
},
|
||||
openBatchEditMember() {
|
||||
this.batchFormDataMember = {member: false, welfareMember: false}
|
||||
this.batchEditMemberVisible = true
|
||||
},
|
||||
openBatchEditAidFundType() {
|
||||
this.batchFormDataAidFundType = {}
|
||||
this.batchEditAidFundTypeVisible = true
|
||||
},
|
||||
openBatchEditUserState() {
|
||||
this.batchFormDataUserState = {}
|
||||
this.batchEditUserStateVisible = true
|
||||
@@ -739,23 +619,6 @@ layout("/layouts/platform.html"){
|
||||
this.$set(this.unitMoveFormData, "unitName", "")
|
||||
}
|
||||
},
|
||||
async doBatchEditUserAttribute() {
|
||||
const batch = this.checkUsers.map(v => {
|
||||
return {id: v.id, userAttribute: v.userAttribute}
|
||||
})
|
||||
this.batchEditLoading = true
|
||||
const resp = await this.$axios.post("/platform/member/change/manage/batchUpdateUserAttribute", {users: JSON.stringify(batch)})
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
this.$refs.table.clearSelection()
|
||||
this.batchEditUserAttributeVisible = false
|
||||
this.checkUsers = []
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
}
|
||||
this.batchEditLoading = false
|
||||
},
|
||||
async doBatchEditMember() {
|
||||
const batch = this.checkUsers.map(v => {
|
||||
return {id: v.id, member: v.member, welfareMember: v.welfareMember}
|
||||
@@ -773,23 +636,6 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
this.batchEditLoading = false
|
||||
},
|
||||
async doBatchEditAidFundType() {
|
||||
const batch = this.checkUsers.map(v => {
|
||||
return {id: v.id, aidFundMemberUserType: v.aidFundMemberUserType}
|
||||
})
|
||||
this.batchEditLoading = true
|
||||
const resp = await this.$axios.post("/platform/member/change/manage/batchUpdateAidFundType", {users: JSON.stringify(batch)})
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
this.$refs.table.clearSelection()
|
||||
this.batchEditAidFundTypeVisible = false
|
||||
this.checkUsers = []
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
}
|
||||
this.batchEditLoading = false
|
||||
},
|
||||
async doBatchEditUserState() {
|
||||
const batch = this.checkUsers.map(v => {
|
||||
return {id: v.id, userState: v.userState}
|
||||
|
||||
@@ -38,14 +38,6 @@ layout("/layouts/platform.html"){
|
||||
v-for="item in changeTypeData"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="人员分类">
|
||||
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
|
||||
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
|
||||
</search-item>
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-model="pageForm.userAttribute" placeholder="请选择人员属性" @change="doSearch"
|
||||
code="USER_ATTRIBUTE"></dict-select>
|
||||
</search-item>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
@@ -149,9 +141,7 @@ layout("/layouts/platform.html"){
|
||||
unitId: "",
|
||||
userState: "",
|
||||
personType: "",
|
||||
changeType: "",
|
||||
aidFundMemberUserType: "",
|
||||
userAttribute: ""
|
||||
changeType: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -82,8 +82,6 @@ const MEMBER_INFO = {
|
||||
<el-descriptions-item label="岗级">{{ viewData.positionLevel }}</el-descriptions-item>
|
||||
<el-descriptions-item label="行政岗级">{{ viewData.administrativePositionLevel }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="人员分类">{{ viewData.aidFundMemberUserType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="人员属性">{{ viewData.userAttribute }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -45,14 +45,6 @@
|
||||
<!-- <dict-select v-model="pageForm.preparedBys" placeholder="请选择编制类别" @change="doSearch"-->
|
||||
<!-- code="USER_PREPARED_BY_TYPE" multiple collapse-tags></dict-select>-->
|
||||
<!-- </search-item>-->
|
||||
<search-item label="人员分类">
|
||||
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
|
||||
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
|
||||
</search-item>
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-model="pageForm.userAttribute" placeholder="请选择人员属性" @change="doSearch"
|
||||
code="USER_ATTRIBUTE"></dict-select>
|
||||
</search-item>
|
||||
</search>
|
||||
`,
|
||||
store,
|
||||
@@ -76,8 +68,6 @@
|
||||
userStates: [],
|
||||
personTypes: [],
|
||||
preparedBys: [],
|
||||
aidFundMemberUserType: '',
|
||||
userAttribute: '',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+1
-13
@@ -102,18 +102,6 @@
|
||||
style="width: 100%"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="人员属性">
|
||||
<el-form-item prop="userAttribute">
|
||||
<dict-select v-model="formData.userAttribute" code="USER_ATTRIBUTE"
|
||||
style="width: 100%"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="人员分类">
|
||||
<el-form-item prop="aidFundMemberUserType">
|
||||
<dict-select v-model="formData.aidFundMemberUserType" code="AIDFUND_MEMBER_USER_TYPE"
|
||||
style="width: 100%"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<!-- <el-descriptions-item label="编制类别">-->
|
||||
<!-- <el-form-item prop="preparedBy">-->
|
||||
<!-- <dict-select v-model="formData.preparedBy" code="USER_PREPARED_BY_TYPE"-->
|
||||
@@ -252,7 +240,7 @@
|
||||
`,
|
||||
store,
|
||||
dicts: ["USER_NATION", "USER_POLITICAL", "USER_EDUCATION", "USER_ACADEMIC_DEGREE", "USER_CAMPUS",
|
||||
"USER_STATE", "USER_PREPARED_BY_TYPE", "USER_PERSON_TYPE", "USER_MARRIAGE","AIDFUND_MEMBER_USER_TYPE","USER_ATTRIBUTE"],
|
||||
"USER_STATE", "USER_PREPARED_BY_TYPE", "USER_PERSON_TYPE", "USER_MARRIAGE"],
|
||||
data() {
|
||||
return {
|
||||
formData: {},
|
||||
|
||||
+1
-1
@@ -291,7 +291,7 @@ layout("/layouts/platform.html"){
|
||||
search(pageForm) {
|
||||
if (pageForm) {
|
||||
// 当前页的在职状态、人员类型使用标签数组筛选,公共查询组件回传的是字符串,
|
||||
// 这里保留当前页数组,避免选择人员属性后把标签筛选状态覆盖掉。
|
||||
// 这里保留当前页数组,避免覆盖标签筛选状态。
|
||||
const currentUserStates = this.pageForm.userStates
|
||||
const currentPersonTypes = this.pageForm.personTypes
|
||||
this.pageForm = {...this.pageForm, ...pageForm}
|
||||
|
||||
@@ -238,9 +238,7 @@ layout("/layouts/platform.html"){
|
||||
// { prop: "preparedBy", label: "编制类别", width: 120, sortable: true },
|
||||
{ prop: "postDoctoralJoinDate", label: "进站时间", sortable: true, checked: 0 },
|
||||
{ prop: "unionName", label: "所属工会", width: 120, sortable: true },
|
||||
{ prop: "unitName", label: "所属单位", width: 120, sortable: true },
|
||||
// { prop: "aidFundMemberUserType", label: "人员分类", width: 120, sortable: true },
|
||||
// { prop: "userAttribute", label: "人员属性", width: 120, sortable: true },
|
||||
{ prop: "unitName", label: "所属单位", sortable: true },
|
||||
{ prop: "idCardType", label: "证件类型", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "idCard", label: "证件号码", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "marriage", label: "婚姻状况", width: 120, sortable: true, checked: 0 },
|
||||
|
||||
@@ -46,12 +46,6 @@ layout("/layouts/platform.html"){
|
||||
<search-item label="人员类型">
|
||||
<dict-select v-model="pageForm.personType" placeholder="请选择人员类型" @change="doSearch" code="USER_PERSON_TYPE"></dict-select>
|
||||
</search-item>
|
||||
<search-item label="人员分类">
|
||||
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch" code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
|
||||
</search-item>
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-model="pageForm.userAttribute" placeholder="请选择人员属性" @change="doSearch" code="USER_ATTRIBUTE"></dict-select>
|
||||
</search-item>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
@@ -101,15 +95,13 @@ layout("/layouts/platform.html"){
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
dicts: ["USER_STATE", "USER_PERSON_TYPE", "MEMBER_CHANGE_TYPE", "AIDFUND_MEMBER_USER_TYPE", "USER_ATTRIBUTE"],
|
||||
dicts: ["USER_STATE", "USER_PERSON_TYPE", "MEMBER_CHANGE_TYPE"],
|
||||
data() {
|
||||
return {
|
||||
pageForm: {
|
||||
year: new Date().getFullYear() - 1 + "",
|
||||
memberSearchName: "",
|
||||
memberSearchKeyWord: "",
|
||||
aidFundMemberUserType: "",
|
||||
userAttribute: ""
|
||||
memberSearchKeyWord: ""
|
||||
},
|
||||
userId: "",
|
||||
unions: [],
|
||||
|
||||
@@ -46,12 +46,6 @@ layout("/layouts/platform.html"){
|
||||
<search-item label="人员类型">
|
||||
<dict-select v-model="pageForm.personType" placeholder="请选择人员类型" @change="doSearch" code="USER_PERSON_TYPE"></dict-select>
|
||||
</search-item>
|
||||
<search-item label="人员分类">
|
||||
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch" code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
|
||||
</search-item>
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-model="pageForm.userAttribute" placeholder="请选择人员属性" @change="doSearch" code="USER_ATTRIBUTE"></dict-select>
|
||||
</search-item>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
@@ -108,7 +102,7 @@ layout("/layouts/platform.html"){
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
dicts: ["USER_STATE", "USER_PERSON_TYPE", "MEMBER_CHANGE_TYPE","AIDFUND_MEMBER_USER_TYPE","USER_ATTRIBUTE"],
|
||||
dicts: ["USER_STATE", "USER_PERSON_TYPE", "MEMBER_CHANGE_TYPE"],
|
||||
data() {
|
||||
return {
|
||||
pageForm: {
|
||||
|
||||
+1
-81
@@ -80,56 +80,6 @@ layout("/layouts/platform.html"){
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<el-row type="flex" align="middle" class="query-row query-row-tag">
|
||||
<el-col class="query-row-title">人员分类:</el-col>
|
||||
<el-col class="query-row-content query-row-content-tag">
|
||||
<el-tag
|
||||
class="query-tag-item"
|
||||
v-for="item in aidFundMemberUserTypeOptions"
|
||||
:key="item.code"
|
||||
:effect="pageForm.aidFundMemberUserTypes.includes(item.code)?'dark':'plain'"
|
||||
@click="tagClick('aidFundMemberUserTypes',item.code)">
|
||||
{{ item.name }}
|
||||
</el-tag>
|
||||
<span class="query-tag-actions">
|
||||
<el-link type="danger"
|
||||
v-if="aidFundMemberUserTypeOptions.length&&pageForm.aidFundMemberUserTypes.length"
|
||||
:underline="false"
|
||||
@click="pageForm.aidFundMemberUserTypes=[];doSearch()">清空
|
||||
</el-link>
|
||||
<el-link :underline="false"
|
||||
@click="pageForm.aidFundMemberUserTypes=aidFundMemberUserTypeOptions.map(p=>p.code);doSearch()"
|
||||
type="success">全部
|
||||
</el-link>
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row type="flex" align="middle" class="query-row query-row-tag">
|
||||
<el-col class="query-row-title">人员属性:</el-col>
|
||||
<el-col class="query-row-content query-row-content-tag">
|
||||
<el-tag class="query-tag-item"
|
||||
v-for="item in userAttributeOptions"
|
||||
:key="item.code"
|
||||
:effect="pageForm.userAttributes.includes(item.code)?'dark':'plain'"
|
||||
@click="tagClick('userAttributes',item.code)">
|
||||
{{ item.name }}
|
||||
</el-tag>
|
||||
<span class="query-tag-actions">
|
||||
<el-link type="danger"
|
||||
v-if="userAttributeOptions.length&&pageForm.userAttributes.length"
|
||||
:underline="false"
|
||||
@click="pageForm.userAttributes=[];doSearch()">清空
|
||||
</el-link>
|
||||
<el-link :underline="false"
|
||||
@click="pageForm.userAttributes=userAttributeOptions.map(p=>p.code);doSearch()"
|
||||
type="success">全部
|
||||
</el-link>
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mt10">
|
||||
<table-tool label="会员分析"></table-tool>
|
||||
<el-table :data="tableData" show-summary>
|
||||
@@ -211,13 +161,9 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
unions: [],
|
||||
units: [],
|
||||
aidFundMemberUserTypeOptions: [],
|
||||
userAttributeOptions: [],
|
||||
pageForm: {
|
||||
currentYear: moment().format("YYYY"),
|
||||
queryType: "fgh",
|
||||
userAttributes: [],
|
||||
aidFundMemberUserTypes: []
|
||||
queryType: "fgh"
|
||||
},
|
||||
fullTableData: []
|
||||
},
|
||||
@@ -239,18 +185,9 @@ layout("/layouts/platform.html"){
|
||||
this.units = data
|
||||
})
|
||||
}
|
||||
this.$businessTool.getDictOptions("AIDFUND_MEMBER_USER_TYPE").then((data) => {
|
||||
this.aidFundMemberUserTypeOptions = data
|
||||
})
|
||||
this.$businessTool.getDictOptions("USER_ATTRIBUTE").then((data) => {
|
||||
this.userAttributeOptions = data
|
||||
})
|
||||
},
|
||||
pageData() {
|
||||
// 统计接口按 JSON 字符串接收多选条件,这里统一在提交前做转换,保证人员分类和人员属性都能正确过滤。
|
||||
const pageForm = clone(this.pageForm)
|
||||
pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes)
|
||||
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
|
||||
this.$axios.post(loc() + "/pageData", pageForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.tableData = res.data
|
||||
@@ -258,23 +195,6 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
})
|
||||
},
|
||||
tagClick(key,code) {
|
||||
if (key === "userAttributes") {
|
||||
if (this.pageForm.userAttributes.includes(code)) {
|
||||
this.pageForm.userAttributes.splice(this.pageForm.userAttributes.indexOf(code), 1)
|
||||
} else {
|
||||
this.pageForm.userAttributes.push(code)
|
||||
}
|
||||
}
|
||||
if (key === "aidFundMemberUserTypes") {
|
||||
if (this.pageForm.aidFundMemberUserTypes.includes(code)) {
|
||||
this.pageForm.aidFundMemberUserTypes.splice(this.pageForm.aidFundMemberUserTypes.indexOf(code), 1)
|
||||
} else {
|
||||
this.pageForm.aidFundMemberUserTypes.push(code)
|
||||
}
|
||||
}
|
||||
this.doSearch()
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.initData()
|
||||
|
||||
+1
-7
@@ -162,9 +162,7 @@ layout("/layouts/platform.html"){
|
||||
// { prop: "preparedBy", label: "编制类别", width: 120, sortable: true },
|
||||
{ prop: "postDoctoralJoinDate", label: "进站时间", sortable: true, checked: 0 },
|
||||
{ prop: "unionName", label: "所属工会", width: 120, sortable: true },
|
||||
{ prop: "unitName", label: "所属单位", width: 120, sortable: true },
|
||||
{ prop: "aidFundMemberUserType", label: "人员分类", width: 120, sortable: true },
|
||||
{ prop: "userAttribute", label: "人员属性", width: 120, sortable: true },
|
||||
{ prop: "unitName", label: "所属单位", sortable: true },
|
||||
{ prop: "idCardType", label: "证件类型", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "idCard", label: "证件号码", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "marriage", label: "婚姻状况", width: 120, sortable: true, checked: 0 },
|
||||
@@ -203,8 +201,6 @@ layout("/layouts/platform.html"){
|
||||
pageForm.sexTypes = JSON.stringify(this.pageForm.sexTypes)
|
||||
pageForm.memberTypes = JSON.stringify(this.pageForm.memberTypes)
|
||||
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
|
||||
pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes)
|
||||
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
|
||||
pageForm.userStates = JSON.stringify(this.pageForm.userStates)
|
||||
const data = this.tableColumns.filter(item => this.checkedFields.includes(item.prop)).map(item => {
|
||||
return {prop: item.prop, label: item.label}
|
||||
@@ -228,8 +224,6 @@ layout("/layouts/platform.html"){
|
||||
pageForm.sexTypes = JSON.stringify(this.pageForm.sexTypes)
|
||||
pageForm.memberTypes = JSON.stringify(this.pageForm.memberTypes)
|
||||
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
|
||||
pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes)
|
||||
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
|
||||
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
|
||||
pageForm.memberStatus = JSON.stringify(this.pageForm.memberStatus)
|
||||
pageForm.userStates = JSON.stringify(this.pageForm.userStates)
|
||||
|
||||
-62
@@ -88,56 +88,6 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
|
||||
</el-row>
|
||||
|
||||
|
||||
<el-row type="flex" align="middle" class="query-row query-row-tag">
|
||||
<el-col class="query-row-title">人员分类:</el-col>
|
||||
<el-col class="query-row-content query-row-content-tag">
|
||||
<el-tag
|
||||
style="margin-right: 10px;cursor: pointer"
|
||||
v-for="item in aidFundMemberUserTypeOptions"
|
||||
:key="item.code"
|
||||
:type="item.name"
|
||||
:effect="pageForm.aidFundMemberUserTypes.includes(item.code)?'dark':'plain'"
|
||||
@click="tagClick('aidFundMemberUserTypes',item.code)">
|
||||
{{ item.name }}
|
||||
</el-tag>
|
||||
|
||||
<el-link type="danger"
|
||||
v-if="aidFundMemberUserTypeOptions.length&&pageForm.aidFundMemberUserTypes.length"
|
||||
:underline="false"
|
||||
@click="pageForm.aidFundMemberUserTypes=[];doSearch()">清空
|
||||
</el-link>
|
||||
<el-link :underline="false"
|
||||
@click="pageForm.aidFundMemberUserTypes=aidFundMemberUserTypeOptions.map(p=>p.code);doSearch()"
|
||||
type="success">全部
|
||||
</el-link>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row type="flex" align="middle" class="query-row query-row-tag">
|
||||
<el-col class="query-row-title">人员属性:</el-col>
|
||||
<el-col class="query-row-content query-row-content-tag">
|
||||
<el-tag
|
||||
style="margin-right: 10px;cursor: pointer"
|
||||
v-for="item in userAttributeOptions"
|
||||
:key="item.code"
|
||||
:type="item.name"
|
||||
:effect="pageForm.userAttributes.includes(item.code)?'dark':'plain'"
|
||||
@click="tagClick('userAttributes',item.code)">
|
||||
{{ item.name }}
|
||||
</el-tag>
|
||||
|
||||
<el-link type="danger"
|
||||
v-if="userAttributeOptions.length&&pageForm.userAttributes.length"
|
||||
:underline="false"
|
||||
@click="pageForm.userAttributes=[];doSearch()">清空
|
||||
</el-link>
|
||||
<el-link :underline="false"
|
||||
@click="pageForm.userAttributes=userAttributeOptions.map(p=>p.code);doSearch()"
|
||||
type="success">全部
|
||||
</el-link>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row type="flex" align="middle" class="query-row">
|
||||
<el-col class="query-row-content">
|
||||
<el-row>
|
||||
@@ -234,8 +184,6 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
|
||||
searchName: "username",
|
||||
personTypes: [],
|
||||
preparedBys: [],
|
||||
aidFundMemberUserTypes: [],
|
||||
userAttributes: [],
|
||||
userStates: [],
|
||||
memberStatus: [],
|
||||
year: moment().format("YYYY"),
|
||||
@@ -252,8 +200,6 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
|
||||
},
|
||||
personTypeOptions: [],
|
||||
preparedByOptions: [],
|
||||
aidFundMemberUserTypeOptions: [],
|
||||
userAttributeOptions: [],
|
||||
userStateOptions: [],
|
||||
memberStatusOptions: [],
|
||||
unions: [],
|
||||
@@ -345,8 +291,6 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
|
||||
this.pageForm.sexTypes = []
|
||||
this.pageForm.personTypes = []
|
||||
this.pageForm.preparedBys = []
|
||||
this.pageForm.aidFundMemberUserTypes = []
|
||||
this.pageForm.userAttributes = []
|
||||
this.pageForm.memberStatus = []
|
||||
this.pageForm.userStates = []
|
||||
this.pageForm.unionId = []
|
||||
@@ -384,12 +328,6 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
|
||||
this.$businessTool.getDictOptions("USER_PREPARED_BY_TYPE").then((data) => {
|
||||
this.preparedByOptions = data
|
||||
})
|
||||
this.$businessTool.getDictOptions("AIDFUND_MEMBER_USER_TYPE").then((data) => {
|
||||
this.aidFundMemberUserTypeOptions = data
|
||||
})
|
||||
this.$businessTool.getDictOptions("USER_ATTRIBUTE").then((data) => {
|
||||
this.userAttributeOptions = data
|
||||
})
|
||||
this.$businessTool.getDictOptions("USER_STATE").then((data) => {
|
||||
this.userStateOptions = data
|
||||
})
|
||||
|
||||
+1
-81
@@ -80,56 +80,6 @@ layout("/layouts/platform.html"){
|
||||
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mt10">
|
||||
<el-row type="flex" align="middle" class="query-row query-row-tag">
|
||||
<el-col class="query-row-title">人员分类:</el-col>
|
||||
<el-col class="query-row-content query-row-content-tag">
|
||||
<el-tag
|
||||
class="query-tag-item"
|
||||
v-for="item in aidFundMemberUserTypeOptions"
|
||||
:key="item.code"
|
||||
:effect="pageForm.aidFundMemberUserTypes.includes(item.code)?'dark':'plain'"
|
||||
@click="tagClick('aidFundMemberUserTypes',item.code)">
|
||||
{{ item.name }}
|
||||
</el-tag>
|
||||
<span class="query-tag-actions">
|
||||
<el-link type="danger"
|
||||
v-if="aidFundMemberUserTypeOptions.length&&pageForm.aidFundMemberUserTypes.length"
|
||||
:underline="false"
|
||||
@click="pageForm.aidFundMemberUserTypes=[];doSearch()">清空
|
||||
</el-link>
|
||||
<el-link :underline="false"
|
||||
@click="pageForm.aidFundMemberUserTypes=aidFundMemberUserTypeOptions.map(p=>p.code);doSearch()"
|
||||
type="success">全部
|
||||
</el-link>
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row type="flex" align="middle" class="query-row query-row-tag">
|
||||
<el-col class="query-row-title">人员属性:</el-col>
|
||||
<el-col class="query-row-content query-row-content-tag">
|
||||
<el-tag class="query-tag-item"
|
||||
v-for="item in userAttributeOptions"
|
||||
:key="item.code"
|
||||
:effect="pageForm.userAttributes.includes(item.code)?'dark':'plain'"
|
||||
@click="tagClick('userAttributes',item.code)">
|
||||
{{ item.name }}
|
||||
</el-tag>
|
||||
<span class="query-tag-actions">
|
||||
<el-link type="danger"
|
||||
v-if="userAttributeOptions.length&&pageForm.userAttributes.length"
|
||||
:underline="false"
|
||||
@click="pageForm.userAttributes=[];doSearch()">清空
|
||||
</el-link>
|
||||
<el-link :underline="false"
|
||||
@click="pageForm.userAttributes=userAttributeOptions.map(p=>p.code);doSearch()"
|
||||
type="success">全部
|
||||
</el-link>
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mt10">
|
||||
<table-tool label="会员统计"></table-tool>
|
||||
<el-table :data="tableData" show-summary>
|
||||
@@ -173,14 +123,10 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
unions: [],
|
||||
units: [],
|
||||
userAttributeOptions: [],
|
||||
pageForm: {
|
||||
currentYear: moment().format("YYYY"),
|
||||
queryType: "fgh",
|
||||
userAttributes: [],
|
||||
aidFundMemberUserTypes: []
|
||||
queryType: "fgh"
|
||||
},
|
||||
aidFundMemberUserTypeOptions: [],
|
||||
},
|
||||
methods: {
|
||||
doExport() {},
|
||||
@@ -200,35 +146,9 @@ layout("/layouts/platform.html"){
|
||||
this.units = data
|
||||
})
|
||||
}
|
||||
this.$businessTool.getDictOptions("AIDFUND_MEMBER_USER_TYPE").then((data) => {
|
||||
this.aidFundMemberUserTypeOptions = data
|
||||
})
|
||||
this.$businessTool.getDictOptions("USER_ATTRIBUTE").then((data) => {
|
||||
this.userAttributeOptions = data
|
||||
})
|
||||
},
|
||||
tagClick(key,code) {
|
||||
if (key === "userAttributes") {
|
||||
if (this.pageForm.userAttributes.includes(code)) {
|
||||
this.pageForm.userAttributes.splice(this.pageForm.userAttributes.indexOf(code), 1)
|
||||
} else {
|
||||
this.pageForm.userAttributes.push(code)
|
||||
}
|
||||
}
|
||||
if (key === "aidFundMemberUserTypes") {
|
||||
if (this.pageForm.aidFundMemberUserTypes.includes(code)) {
|
||||
this.pageForm.aidFundMemberUserTypes.splice(this.pageForm.aidFundMemberUserTypes.indexOf(code), 1)
|
||||
} else {
|
||||
this.pageForm.aidFundMemberUserTypes.push(code)
|
||||
}
|
||||
}
|
||||
this.doSearch()
|
||||
},
|
||||
pageData() {
|
||||
const pageForm = clone(this.pageForm)
|
||||
// 统计接口按 JSON 字符串接收多选条件,这里把人员分类和人员属性统一成同一提交口径。
|
||||
pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes)
|
||||
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
|
||||
this.$axios.post(loc() + "/pageData", pageForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.tableData = res.data
|
||||
|
||||
Reference in New Issue
Block a user