Merge branch 'test' into test-dsc

* test:
  把-server项目改为jar包,新增 server-app项目作为启动器 http://172.16.46.63:31560/index.php?m=task&f=view&taskID=699
  [+]增加国密SM4加解密工具包
  fix(user-dept): 修改用户来源筛选条件
  fix(databus): 修复部门数据查询中缺少数据源过滤条件
  fix(databus): 修改用户同步的数据源过滤条件
  fix(databus): 修改用户同步的数据源过滤条件
  [+]增加国密SM4接口加解密
  [#]修改部门推送消息逻辑

# Conflicts:
#	zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/util/security/CryptoSignatureUtils.java
#	zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java
#	zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/databus/DatabusDeptProviderApiImpl.java
#	zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/SmsCallbackController.java
#	zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/enums/SmsChannelEnum.java
This commit is contained in:
ranke
2026-01-12 18:49:36 +08:00
130 changed files with 7899 additions and 147 deletions

View File

@@ -1,19 +0,0 @@
## AdoptOpenJDK 停止发布 OpenJDK 二进制,而 Eclipse Temurin 是它的延伸,提供更好的稳定性
FROM 172.16.46.66:10043/base-service/eclipse-temurin:21-jre
## 创建目录,并使用它作为工作目录
RUN mkdir -p /zt-module-system-server
WORKDIR /zt-module-system-server
## 将后端项目的 Jar 文件,复制到镜像中
COPY ./target/zt-module-system-server.jar app.jar
## 设置 TZ 时区
## 设置 JAVA_OPTS 环境变量,可通过 docker run -e "JAVA_OPTS=" 进行覆盖
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx1024m"
## 暴露后端项目的 48080 端口
EXPOSE 48081
## 启动后端项目
CMD java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar app.jar

View File

@@ -200,26 +200,14 @@
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.13.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<!-- 设置构建的 jar 包名 -->
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- 打包 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,30 +0,0 @@
package com.zt.plat.module.system;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 项目的启动类
*
* 如果你碰到启动的问题,请认真阅读 http://172.16.46.63:30888/quick-start/ 文章
* 如果你碰到启动的问题,请认真阅读 http://172.16.46.63:30888/quick-start/ 文章
* 如果你碰到启动的问题,请认真阅读 http://172.16.46.63:30888/quick-start/ 文章
*
* @author ZT
*/
@SpringBootApplication
public class SystemServerApplication {
public static void main(String[] args) {
// 如果你碰到启动的问题,请认真阅读 http://172.16.46.63:30888/quick-start/ 文章
// 如果你碰到 启动的问题,请认真阅读 http://172.16.46.63:30888/quick-start/ 文章
// 如果你碰到启动的问题,请认真阅读 http://172.16.46.63:30888/quick-start/ 文章
SpringApplication.run(SystemServerApplication.class, args);
// 如果你碰到启动的问题,请认真阅读 http://172.16.46.63:30888/quick-start/ 文章
// 如果你碰到启动的问题,请认真阅读 http://172.16.46.63:30888/quick-start/ 文章
// 如果你碰到启动的问题,请认真阅读 http://172.16.46.63:30888/quick-start/ 文章
}
}

View File

@@ -0,0 +1,224 @@
package com.zt.plat.module.system.api.databus;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.databus.api.data.DatabusDeptData;
import com.zt.plat.module.databus.api.dto.CursorPageReqDTO;
import com.zt.plat.module.databus.api.dto.CursorPageResult;
import com.zt.plat.module.databus.api.provider.DatabusDeptProviderApi;
import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO;
import com.zt.plat.module.system.dal.mysql.dept.DeptMapper;
import com.zt.plat.module.system.dal.mysql.user.AdminUserMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
import java.util.stream.Collectors;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
/**
* Databus 部门数据提供者 API 实现
*
* @author ZT
*/
@Slf4j
@RestController
@Validated
public class DatabusDeptProviderApiImpl implements DatabusDeptProviderApi {
@Resource
private DeptMapper deptMapper;
@Resource
private AdminUserMapper userMapper;
@Override
public CommonResult<CursorPageResult<DatabusDeptData>> getPageByCursor(CursorPageReqDTO reqDTO) {
// 构建游标查询条件
LambdaQueryWrapper<DeptDO> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DeptDO::getDeptSource, 3);
// 游标条件create_time > cursorTime OR (create_time = cursorTime AND id > cursorId)
if (!reqDTO.isFirstPage()) {
queryWrapper.and(w -> w
.gt(DeptDO::getCreateTime, reqDTO.getCursorTime())
.or(o -> o
.eq(DeptDO::getCreateTime, reqDTO.getCursorTime())
.gt(DeptDO::getId, reqDTO.getCursorId())
)
);
}
// 租户过滤(如果指定)
if (reqDTO.getTenantId() != null) {
queryWrapper.eq(DeptDO::getTenantId, reqDTO.getTenantId());
}
// 按 create_time, id 升序排列,确保顺序稳定
queryWrapper.orderByAsc(DeptDO::getCreateTime)
.orderByAsc(DeptDO::getId);
// 多查一条判断是否有更多数据
int limit = reqDTO.getBatchSize() != null ? reqDTO.getBatchSize() : 100;
queryWrapper.last("LIMIT " + (limit + 1));
List<DeptDO> deptList = deptMapper.selectList(queryWrapper);
// 判断是否有更多
boolean hasMore = deptList.size() > limit;
if (hasMore) {
deptList = deptList.subList(0, limit);
}
if (CollUtil.isEmpty(deptList)) {
return success(CursorPageResult.empty());
}
// 收集负责人用户ID
Set<Long> leaderUserIds = deptList.stream()
.map(DeptDO::getLeaderUserId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 批量查询负责人用户名
Map<Long, String> userNameMap = new HashMap<>();
if (CollUtil.isNotEmpty(leaderUserIds)) {
List<AdminUserDO> users = userMapper.selectBatchIds(leaderUserIds);
userNameMap = users.stream()
.collect(Collectors.toMap(AdminUserDO::getId, AdminUserDO::getNickname, (v1, v2) -> v1));
}
// 转换为同步数据
Map<Long, String> finalUserNameMap = userNameMap;
List<DatabusDeptData> dataList = deptList.stream()
.map(dept -> convertToDatabusDeptData(dept, finalUserNameMap))
.collect(Collectors.toList());
// 获取最后一条数据的游标
DeptDO lastDept = deptList.get(deptList.size() - 1);
// 首次查询时返回总数
Long total = null;
if (reqDTO.isFirstPage()) {
LambdaQueryWrapper<DeptDO> countWrapper = new LambdaQueryWrapper<>();
countWrapper.eq(DeptDO::getDeptSource, 3);
if (reqDTO.getTenantId() != null) {
countWrapper.eq(DeptDO::getTenantId, reqDTO.getTenantId());
}
total = deptMapper.selectCount(countWrapper);
}
return success(CursorPageResult.of(
dataList,
lastDept.getCreateTime(),
lastDept.getId(),
hasMore,
total
));
}
@Override
public CommonResult<DatabusDeptData> getById(Long id) {
DeptDO dept = deptMapper.selectById(id);
if (dept == null) {
return success(null);
}
// 查询负责人用户名
Map<Long, String> userNameMap = new HashMap<>();
if (dept.getLeaderUserId() != null) {
AdminUserDO user = userMapper.selectById(dept.getLeaderUserId());
if (user != null) {
userNameMap.put(user.getId(), user.getNickname());
}
}
return success(convertToDatabusDeptData(dept, userNameMap));
}
@Override
public CommonResult<List<DatabusDeptData>> getListByIds(List<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return success(Collections.emptyList());
}
List<DeptDO> deptList = deptMapper.selectBatchIds(ids);
if (CollUtil.isEmpty(deptList)) {
return success(Collections.emptyList());
}
// 收集负责人用户ID
Set<Long> leaderUserIds = deptList.stream()
.map(DeptDO::getLeaderUserId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 批量查询负责人用户名
Map<Long, String> userNameMap = new HashMap<>();
if (CollUtil.isNotEmpty(leaderUserIds)) {
List<AdminUserDO> users = userMapper.selectBatchIds(leaderUserIds);
userNameMap = users.stream()
.collect(Collectors.toMap(AdminUserDO::getId, AdminUserDO::getNickname, (v1, v2) -> v1));
}
Map<Long, String> finalUserNameMap = userNameMap;
List<DatabusDeptData> dataList = deptList.stream()
.map(dept -> convertToDatabusDeptData(dept, finalUserNameMap))
.collect(Collectors.toList());
return success(dataList);
}
@Override
public CommonResult<Long> count(Long tenantId) {
LambdaQueryWrapper<DeptDO> queryWrapper = new LambdaQueryWrapper<>();
<<<<<<< HEAD
// ⚠️ 只统计 userSource = 3 的用户
=======
>>>>>>> test
queryWrapper.eq(DeptDO::getDeptSource, 3);
if (tenantId != null) {
queryWrapper.eq(DeptDO::getTenantId, tenantId);
}
return success(deptMapper.selectCount(queryWrapper));
}
/**
* 将 DeptDO 转换为 DatabusDeptData
*/
private DatabusDeptData convertToDatabusDeptData(DeptDO dept, Map<Long, String> userNameMap) {
// 根据 isCompany 反推 deptType
Integer deptType = null;
if (Boolean.TRUE.equals(dept.getIsCompany())) {
deptType = 28; // 公司
} else if (Boolean.FALSE.equals(dept.getIsCompany())) {
deptType = 26; // 部门
}
return DatabusDeptData.builder()
.id(dept.getId())
.code(dept.getCode())
.name(dept.getName())
.shortName(dept.getShortName())
.parentId(dept.getParentId())
.sort(dept.getSort())
.status(dept.getStatus())
.deptType(deptType)
.isGroup(dept.getIsGroup())
.isCompany(dept.getIsCompany())
.deptSource(dept.getDeptSource())
.leaderUserId(dept.getLeaderUserId())
.leaderUserName(dept.getLeaderUserId() != null ? userNameMap.get(dept.getLeaderUserId()) : null)
.phone(dept.getPhone())
.email(dept.getEmail())
.tenantId(dept.getTenantId())
.createTime(dept.getCreateTime())
.updateTime(dept.getUpdateTime())
.build();
}
}

View File

@@ -0,0 +1,218 @@
package com.zt.plat.module.system.api.databus;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.databus.api.data.DatabusDeptData;
import com.zt.plat.module.databus.api.dto.CursorPageReqDTO;
import com.zt.plat.module.databus.api.dto.CursorPageResult;
import com.zt.plat.module.databus.api.provider.DatabusDeptProviderApi;
import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO;
import com.zt.plat.module.system.dal.mysql.dept.DeptMapper;
import com.zt.plat.module.system.dal.mysql.user.AdminUserMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
import java.util.stream.Collectors;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
/**
* Databus 部门数据提供者 API 实现
*
* @author ZT
*/
@Slf4j
@RestController
@Validated
public class DatabusDeptProviderApiImpl implements DatabusDeptProviderApi {
@Resource
private DeptMapper deptMapper;
@Resource
private AdminUserMapper userMapper;
@Override
public CommonResult<CursorPageResult<DatabusDeptData>> getPageByCursor(CursorPageReqDTO reqDTO) {
// 构建游标查询条件
LambdaQueryWrapper<DeptDO> queryWrapper = new LambdaQueryWrapper<>();
// 游标条件create_time > cursorTime OR (create_time = cursorTime AND id > cursorId)
if (!reqDTO.isFirstPage()) {
queryWrapper.and(w -> w
.gt(DeptDO::getCreateTime, reqDTO.getCursorTime())
.or(o -> o
.eq(DeptDO::getCreateTime, reqDTO.getCursorTime())
.gt(DeptDO::getId, reqDTO.getCursorId())
)
);
}
// 租户过滤(如果指定)
if (reqDTO.getTenantId() != null) {
queryWrapper.eq(DeptDO::getTenantId, reqDTO.getTenantId());
}
// 按 create_time, id 升序排列,确保顺序稳定
queryWrapper.orderByAsc(DeptDO::getCreateTime)
.orderByAsc(DeptDO::getId);
// 多查一条判断是否有更多数据
int limit = reqDTO.getBatchSize() != null ? reqDTO.getBatchSize() : 100;
queryWrapper.last("LIMIT " + (limit + 1));
List<DeptDO> deptList = deptMapper.selectList(queryWrapper);
// 判断是否有更多
boolean hasMore = deptList.size() > limit;
if (hasMore) {
deptList = deptList.subList(0, limit);
}
if (CollUtil.isEmpty(deptList)) {
return success(CursorPageResult.empty());
}
// 收集负责人用户ID
Set<Long> leaderUserIds = deptList.stream()
.map(DeptDO::getLeaderUserId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 批量查询负责人用户名
Map<Long, String> userNameMap = new HashMap<>();
if (CollUtil.isNotEmpty(leaderUserIds)) {
List<AdminUserDO> users = userMapper.selectBatchIds(leaderUserIds);
userNameMap = users.stream()
.collect(Collectors.toMap(AdminUserDO::getId, AdminUserDO::getNickname, (v1, v2) -> v1));
}
// 转换为同步数据
Map<Long, String> finalUserNameMap = userNameMap;
List<DatabusDeptData> dataList = deptList.stream()
.map(dept -> convertToDatabusDeptData(dept, finalUserNameMap))
.collect(Collectors.toList());
// 获取最后一条数据的游标
DeptDO lastDept = deptList.get(deptList.size() - 1);
// 首次查询时返回总数
Long total = null;
if (reqDTO.isFirstPage()) {
LambdaQueryWrapper<DeptDO> countWrapper = new LambdaQueryWrapper<>();
if (reqDTO.getTenantId() != null) {
countWrapper.eq(DeptDO::getTenantId, reqDTO.getTenantId());
}
total = deptMapper.selectCount(countWrapper);
}
return success(CursorPageResult.of(
dataList,
lastDept.getCreateTime(),
lastDept.getId(),
hasMore,
total
));
}
@Override
public CommonResult<DatabusDeptData> getById(Long id) {
DeptDO dept = deptMapper.selectById(id);
if (dept == null) {
return success(null);
}
// 查询负责人用户名
Map<Long, String> userNameMap = new HashMap<>();
if (dept.getLeaderUserId() != null) {
AdminUserDO user = userMapper.selectById(dept.getLeaderUserId());
if (user != null) {
userNameMap.put(user.getId(), user.getNickname());
}
}
return success(convertToDatabusDeptData(dept, userNameMap));
}
@Override
public CommonResult<List<DatabusDeptData>> getListByIds(List<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return success(Collections.emptyList());
}
List<DeptDO> deptList = deptMapper.selectBatchIds(ids);
if (CollUtil.isEmpty(deptList)) {
return success(Collections.emptyList());
}
// 收集负责人用户ID
Set<Long> leaderUserIds = deptList.stream()
.map(DeptDO::getLeaderUserId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 批量查询负责人用户名
Map<Long, String> userNameMap = new HashMap<>();
if (CollUtil.isNotEmpty(leaderUserIds)) {
List<AdminUserDO> users = userMapper.selectBatchIds(leaderUserIds);
userNameMap = users.stream()
.collect(Collectors.toMap(AdminUserDO::getId, AdminUserDO::getNickname, (v1, v2) -> v1));
}
Map<Long, String> finalUserNameMap = userNameMap;
List<DatabusDeptData> dataList = deptList.stream()
.map(dept -> convertToDatabusDeptData(dept, finalUserNameMap))
.collect(Collectors.toList());
return success(dataList);
}
@Override
public CommonResult<Long> count(Long tenantId) {
LambdaQueryWrapper<DeptDO> queryWrapper = new LambdaQueryWrapper<>();
if (tenantId != null) {
queryWrapper.eq(DeptDO::getTenantId, tenantId);
}
return success(deptMapper.selectCount(queryWrapper));
}
/**
* 将 DeptDO 转换为 DatabusDeptData
*/
private DatabusDeptData convertToDatabusDeptData(DeptDO dept, Map<Long, String> userNameMap) {
// 根据 isCompany 反推 deptType
Integer deptType = null;
if (Boolean.TRUE.equals(dept.getIsCompany())) {
deptType = 28; // 公司
} else if (Boolean.FALSE.equals(dept.getIsCompany())) {
deptType = 26; // 部门
}
return DatabusDeptData.builder()
.id(dept.getId())
.code(dept.getCode())
.name(dept.getName())
.shortName(dept.getShortName())
.parentId(dept.getParentId())
.sort(dept.getSort())
.status(dept.getStatus())
.deptType(deptType)
.isGroup(dept.getIsGroup())
.isCompany(dept.getIsCompany())
.deptSource(dept.getDeptSource())
.leaderUserId(dept.getLeaderUserId())
.leaderUserName(dept.getLeaderUserId() != null ? userNameMap.get(dept.getLeaderUserId()) : null)
.phone(dept.getPhone())
.email(dept.getEmail())
.tenantId(dept.getTenantId())
.createTime(dept.getCreateTime())
.updateTime(dept.getUpdateTime())
.build();
}
}

View File

@@ -0,0 +1,221 @@
package com.zt.plat.module.system.api.databus;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.databus.api.data.DatabusDeptData;
import com.zt.plat.module.databus.api.dto.CursorPageReqDTO;
import com.zt.plat.module.databus.api.dto.CursorPageResult;
import com.zt.plat.module.databus.api.provider.DatabusDeptProviderApi;
import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO;
import com.zt.plat.module.system.dal.mysql.dept.DeptMapper;
import com.zt.plat.module.system.dal.mysql.user.AdminUserMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
import java.util.stream.Collectors;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
/**
* Databus 部门数据提供者 API 实现
*
* @author ZT
*/
@Slf4j
@RestController
@Validated
public class DatabusDeptProviderApiImpl implements DatabusDeptProviderApi {
@Resource
private DeptMapper deptMapper;
@Resource
private AdminUserMapper userMapper;
@Override
public CommonResult<CursorPageResult<DatabusDeptData>> getPageByCursor(CursorPageReqDTO reqDTO) {
// 构建游标查询条件
LambdaQueryWrapper<DeptDO> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DeptDO::getDeptSource, 3);
// 游标条件create_time > cursorTime OR (create_time = cursorTime AND id > cursorId)
if (!reqDTO.isFirstPage()) {
queryWrapper.and(w -> w
.gt(DeptDO::getCreateTime, reqDTO.getCursorTime())
.or(o -> o
.eq(DeptDO::getCreateTime, reqDTO.getCursorTime())
.gt(DeptDO::getId, reqDTO.getCursorId())
)
);
}
// 租户过滤(如果指定)
if (reqDTO.getTenantId() != null) {
queryWrapper.eq(DeptDO::getTenantId, reqDTO.getTenantId());
}
// 按 create_time, id 升序排列,确保顺序稳定
queryWrapper.orderByAsc(DeptDO::getCreateTime)
.orderByAsc(DeptDO::getId);
// 多查一条判断是否有更多数据
int limit = reqDTO.getBatchSize() != null ? reqDTO.getBatchSize() : 100;
queryWrapper.last("LIMIT " + (limit + 1));
List<DeptDO> deptList = deptMapper.selectList(queryWrapper);
// 判断是否有更多
boolean hasMore = deptList.size() > limit;
if (hasMore) {
deptList = deptList.subList(0, limit);
}
if (CollUtil.isEmpty(deptList)) {
return success(CursorPageResult.empty());
}
// 收集负责人用户ID
Set<Long> leaderUserIds = deptList.stream()
.map(DeptDO::getLeaderUserId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 批量查询负责人用户名
Map<Long, String> userNameMap = new HashMap<>();
if (CollUtil.isNotEmpty(leaderUserIds)) {
List<AdminUserDO> users = userMapper.selectBatchIds(leaderUserIds);
userNameMap = users.stream()
.collect(Collectors.toMap(AdminUserDO::getId, AdminUserDO::getNickname, (v1, v2) -> v1));
}
// 转换为同步数据
Map<Long, String> finalUserNameMap = userNameMap;
List<DatabusDeptData> dataList = deptList.stream()
.map(dept -> convertToDatabusDeptData(dept, finalUserNameMap))
.collect(Collectors.toList());
// 获取最后一条数据的游标
DeptDO lastDept = deptList.get(deptList.size() - 1);
// 首次查询时返回总数
Long total = null;
if (reqDTO.isFirstPage()) {
LambdaQueryWrapper<DeptDO> countWrapper = new LambdaQueryWrapper<>();
countWrapper.eq(DeptDO::getDeptSource, 3);
if (reqDTO.getTenantId() != null) {
countWrapper.eq(DeptDO::getTenantId, reqDTO.getTenantId());
}
total = deptMapper.selectCount(countWrapper);
}
return success(CursorPageResult.of(
dataList,
lastDept.getCreateTime(),
lastDept.getId(),
hasMore,
total
));
}
@Override
public CommonResult<DatabusDeptData> getById(Long id) {
DeptDO dept = deptMapper.selectById(id);
if (dept == null) {
return success(null);
}
// 查询负责人用户名
Map<Long, String> userNameMap = new HashMap<>();
if (dept.getLeaderUserId() != null) {
AdminUserDO user = userMapper.selectById(dept.getLeaderUserId());
if (user != null) {
userNameMap.put(user.getId(), user.getNickname());
}
}
return success(convertToDatabusDeptData(dept, userNameMap));
}
@Override
public CommonResult<List<DatabusDeptData>> getListByIds(List<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return success(Collections.emptyList());
}
List<DeptDO> deptList = deptMapper.selectBatchIds(ids);
if (CollUtil.isEmpty(deptList)) {
return success(Collections.emptyList());
}
// 收集负责人用户ID
Set<Long> leaderUserIds = deptList.stream()
.map(DeptDO::getLeaderUserId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 批量查询负责人用户名
Map<Long, String> userNameMap = new HashMap<>();
if (CollUtil.isNotEmpty(leaderUserIds)) {
List<AdminUserDO> users = userMapper.selectBatchIds(leaderUserIds);
userNameMap = users.stream()
.collect(Collectors.toMap(AdminUserDO::getId, AdminUserDO::getNickname, (v1, v2) -> v1));
}
Map<Long, String> finalUserNameMap = userNameMap;
List<DatabusDeptData> dataList = deptList.stream()
.map(dept -> convertToDatabusDeptData(dept, finalUserNameMap))
.collect(Collectors.toList());
return success(dataList);
}
@Override
public CommonResult<Long> count(Long tenantId) {
LambdaQueryWrapper<DeptDO> queryWrapper = new LambdaQueryWrapper<>();
// ⚠️ 只统计 userSource = 3 的用户
queryWrapper.eq(DeptDO::getDeptSource, 3);
if (tenantId != null) {
queryWrapper.eq(DeptDO::getTenantId, tenantId);
}
return success(deptMapper.selectCount(queryWrapper));
}
/**
* 将 DeptDO 转换为 DatabusDeptData
*/
private DatabusDeptData convertToDatabusDeptData(DeptDO dept, Map<Long, String> userNameMap) {
// 根据 isCompany 反推 deptType
Integer deptType = null;
if (Boolean.TRUE.equals(dept.getIsCompany())) {
deptType = 28; // 公司
} else if (Boolean.FALSE.equals(dept.getIsCompany())) {
deptType = 26; // 部门
}
return DatabusDeptData.builder()
.id(dept.getId())
.code(dept.getCode())
.name(dept.getName())
.shortName(dept.getShortName())
.parentId(dept.getParentId())
.sort(dept.getSort())
.status(dept.getStatus())
.deptType(deptType)
.isGroup(dept.getIsGroup())
.isCompany(dept.getIsCompany())
.deptSource(dept.getDeptSource())
.leaderUserId(dept.getLeaderUserId())
.leaderUserName(dept.getLeaderUserId() != null ? userNameMap.get(dept.getLeaderUserId()) : null)
.phone(dept.getPhone())
.email(dept.getEmail())
.tenantId(dept.getTenantId())
.createTime(dept.getCreateTime())
.updateTime(dept.getUpdateTime())
.build();
}
}

View File

@@ -0,0 +1,220 @@
package com.zt.plat.module.system.api.databus;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.databus.api.data.DatabusDeptData;
import com.zt.plat.module.databus.api.dto.CursorPageReqDTO;
import com.zt.plat.module.databus.api.dto.CursorPageResult;
import com.zt.plat.module.databus.api.provider.DatabusDeptProviderApi;
import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO;
import com.zt.plat.module.system.dal.mysql.dept.DeptMapper;
import com.zt.plat.module.system.dal.mysql.user.AdminUserMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
import java.util.stream.Collectors;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
/**
* Databus 部门数据提供者 API 实现
*
* @author ZT
*/
@Slf4j
@RestController
@Validated
public class DatabusDeptProviderApiImpl implements DatabusDeptProviderApi {
@Resource
private DeptMapper deptMapper;
@Resource
private AdminUserMapper userMapper;
@Override
public CommonResult<CursorPageResult<DatabusDeptData>> getPageByCursor(CursorPageReqDTO reqDTO) {
// 构建游标查询条件
LambdaQueryWrapper<DeptDO> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DeptDO::getDeptSource, 3);
// 游标条件create_time > cursorTime OR (create_time = cursorTime AND id > cursorId)
if (!reqDTO.isFirstPage()) {
queryWrapper.and(w -> w
.gt(DeptDO::getCreateTime, reqDTO.getCursorTime())
.or(o -> o
.eq(DeptDO::getCreateTime, reqDTO.getCursorTime())
.gt(DeptDO::getId, reqDTO.getCursorId())
)
);
}
// 租户过滤(如果指定)
if (reqDTO.getTenantId() != null) {
queryWrapper.eq(DeptDO::getTenantId, reqDTO.getTenantId());
}
// 按 create_time, id 升序排列,确保顺序稳定
queryWrapper.orderByAsc(DeptDO::getCreateTime)
.orderByAsc(DeptDO::getId);
// 多查一条判断是否有更多数据
int limit = reqDTO.getBatchSize() != null ? reqDTO.getBatchSize() : 100;
queryWrapper.last("LIMIT " + (limit + 1));
List<DeptDO> deptList = deptMapper.selectList(queryWrapper);
// 判断是否有更多
boolean hasMore = deptList.size() > limit;
if (hasMore) {
deptList = deptList.subList(0, limit);
}
if (CollUtil.isEmpty(deptList)) {
return success(CursorPageResult.empty());
}
// 收集负责人用户ID
Set<Long> leaderUserIds = deptList.stream()
.map(DeptDO::getLeaderUserId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 批量查询负责人用户名
Map<Long, String> userNameMap = new HashMap<>();
if (CollUtil.isNotEmpty(leaderUserIds)) {
List<AdminUserDO> users = userMapper.selectBatchIds(leaderUserIds);
userNameMap = users.stream()
.collect(Collectors.toMap(AdminUserDO::getId, AdminUserDO::getNickname, (v1, v2) -> v1));
}
// 转换为同步数据
Map<Long, String> finalUserNameMap = userNameMap;
List<DatabusDeptData> dataList = deptList.stream()
.map(dept -> convertToDatabusDeptData(dept, finalUserNameMap))
.collect(Collectors.toList());
// 获取最后一条数据的游标
DeptDO lastDept = deptList.get(deptList.size() - 1);
// 首次查询时返回总数
Long total = null;
if (reqDTO.isFirstPage()) {
LambdaQueryWrapper<DeptDO> countWrapper = new LambdaQueryWrapper<>();
countWrapper.eq(DeptDO::getDeptSource, 3);
if (reqDTO.getTenantId() != null) {
countWrapper.eq(DeptDO::getTenantId, reqDTO.getTenantId());
}
total = deptMapper.selectCount(countWrapper);
}
return success(CursorPageResult.of(
dataList,
lastDept.getCreateTime(),
lastDept.getId(),
hasMore,
total
));
}
@Override
public CommonResult<DatabusDeptData> getById(Long id) {
DeptDO dept = deptMapper.selectById(id);
if (dept == null) {
return success(null);
}
// 查询负责人用户名
Map<Long, String> userNameMap = new HashMap<>();
if (dept.getLeaderUserId() != null) {
AdminUserDO user = userMapper.selectById(dept.getLeaderUserId());
if (user != null) {
userNameMap.put(user.getId(), user.getNickname());
}
}
return success(convertToDatabusDeptData(dept, userNameMap));
}
@Override
public CommonResult<List<DatabusDeptData>> getListByIds(List<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return success(Collections.emptyList());
}
List<DeptDO> deptList = deptMapper.selectBatchIds(ids);
if (CollUtil.isEmpty(deptList)) {
return success(Collections.emptyList());
}
// 收集负责人用户ID
Set<Long> leaderUserIds = deptList.stream()
.map(DeptDO::getLeaderUserId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 批量查询负责人用户名
Map<Long, String> userNameMap = new HashMap<>();
if (CollUtil.isNotEmpty(leaderUserIds)) {
List<AdminUserDO> users = userMapper.selectBatchIds(leaderUserIds);
userNameMap = users.stream()
.collect(Collectors.toMap(AdminUserDO::getId, AdminUserDO::getNickname, (v1, v2) -> v1));
}
Map<Long, String> finalUserNameMap = userNameMap;
List<DatabusDeptData> dataList = deptList.stream()
.map(dept -> convertToDatabusDeptData(dept, finalUserNameMap))
.collect(Collectors.toList());
return success(dataList);
}
@Override
public CommonResult<Long> count(Long tenantId) {
LambdaQueryWrapper<DeptDO> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DeptDO::getDeptSource, 3);
if (tenantId != null) {
queryWrapper.eq(DeptDO::getTenantId, tenantId);
}
return success(deptMapper.selectCount(queryWrapper));
}
/**
* 将 DeptDO 转换为 DatabusDeptData
*/
private DatabusDeptData convertToDatabusDeptData(DeptDO dept, Map<Long, String> userNameMap) {
// 根据 isCompany 反推 deptType
Integer deptType = null;
if (Boolean.TRUE.equals(dept.getIsCompany())) {
deptType = 28; // 公司
} else if (Boolean.FALSE.equals(dept.getIsCompany())) {
deptType = 26; // 部门
}
return DatabusDeptData.builder()
.id(dept.getId())
.code(dept.getCode())
.name(dept.getName())
.shortName(dept.getShortName())
.parentId(dept.getParentId())
.sort(dept.getSort())
.status(dept.getStatus())
.deptType(deptType)
.isGroup(dept.getIsGroup())
.isCompany(dept.getIsCompany())
.deptSource(dept.getDeptSource())
.leaderUserId(dept.getLeaderUserId())
.leaderUserName(dept.getLeaderUserId() != null ? userNameMap.get(dept.getLeaderUserId()) : null)
.phone(dept.getPhone())
.email(dept.getEmail())
.tenantId(dept.getTenantId())
.createTime(dept.getCreateTime())
.updateTime(dept.getUpdateTime())
.build();
}
}

View File

@@ -2,15 +2,17 @@ package com.zt.plat.module.system.api.esp;
import com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.util.object.ObjectUtils;
import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.module.system.api.dept.dto.DeptMsgRespDTO;
import com.zt.plat.module.system.api.dept.dto.DeptSaveReqDTO;
import com.zt.plat.module.system.api.esp.dto.EspDto;
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO;
import com.zt.plat.module.system.service.dept.IEspService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Objects;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
@RestController
@Validated
@@ -18,16 +20,26 @@ public class EspApiImpl implements EspApi {
@Resource
private IEspService deptService;
private IEspService espService;
@Override
public CommonResult<List<EspDto>> pushMsg(DeptSaveReqDTO syncReqDTO)
public CommonResult<Long> createDept(DeptSaveReqDTO createReqVO) {
DeptSaveReqVO reqVO = BeanUtils.toBean(createReqVO, DeptSaveReqVO.class);
Long deptId = espService.createDept(reqVO);
return success(deptId);
}
@Override
public CommonResult<List<DeptMsgRespDTO>> selectDepMsg(DeptSaveReqDTO syncReqDTO)
{
if(Objects.isNull(syncReqDTO) || null == syncReqDTO.getId())
{
return CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(),
"ID不能为空");
}
return CommonResult.success(deptService.pushMsg(syncReqDTO));
return espService.selectDepMsg(syncReqDTO);
}
}

View File

@@ -0,0 +1,84 @@
package com.zt.plat.module.system.api.msg;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.module.system.api.sms.dto.log.SmsLogRespDTO;
import com.zt.plat.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO;
import com.zt.plat.module.system.service.sms.SmsLogService;
import com.zt.plat.module.system.service.sms.SmsSendService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
@RestController
@Validated
public class MsgSendApiImpl implements MsgSendApi {
@Resource
private SmsSendService smsSendService;
@Resource
private SmsLogService smsLogService;
@Override
public CommonResult<Long> sendTextMsg(SmsSendSingleToUserReqDTO reqDTO) {
return null;
}
@Override
public CommonResult<Long> sendImageMsg(SmsSendSingleToUserReqDTO reqDTO) {
return null;
}
@Override
public CommonResult<SmsLogRespDTO> getSmsLog(Long id) {
return success(BeanUtils.toBean(smsLogService.getSmsLog(id), SmsLogRespDTO.class));
}
@Override
public CommonResult<Long> sendVideoMsg(SmsSendSingleToUserReqDTO reqDTO) {
return null;
}
@Override
public CommonResult<Long> sendFileMsg(SmsSendSingleToUserReqDTO reqDTO) {
return null;
}
@Override
public CommonResult<SmsLogRespDTO> sendTextCardMsg(Long id) {
return null;
}
@Override
public CommonResult<Long> sendTextCardMsgPich01(SmsSendSingleToUserReqDTO reqDTO) {
return null;
}
@Override
public CommonResult<Long> sendNewsMsg(SmsSendSingleToUserReqDTO reqDTO) {
return null;
}
@Override
public CommonResult<SmsLogRespDTO> sendMpNewsMsg(Long id) {
return null;
}
@Override
public CommonResult<Long> sendMarkdownMsg(SmsSendSingleToUserReqDTO reqDTO) {
return null;
}
@Override
public CommonResult<Long> sendMiniProgramNoticeMsg(SmsSendSingleToUserReqDTO reqDTO) {
return null;
}
@Override
public CommonResult<SmsLogRespDTO> sendInteractiveTaskCardMsg(Long id) {
return null;
}
}

View File

@@ -0,0 +1,18 @@
package com.zt.plat.module.system.controller.admin.sms;
import com.zt.plat.module.system.service.sms.SmsSendService;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "管理后台 - 消息回调")
@RestController
@RequestMapping("/system/sms/callback")
public class MsgCallBackController {
@Resource
private SmsSendService smsSendService;
}

View File

@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 短信回调")
@@ -73,4 +74,13 @@ public class SmsCallbackController {
return success(true);
}
@PostMapping("/zle")
@PermitAll
@TenantIgnore
@Operation(summary = "中铝e办短信的回调")
public CommonResult<Boolean> receiveZleSmsStatus(@RequestBody String requestBody) throws Throwable {
smsSendService.receiveSmsStatus(SmsChannelEnum.ZLE.getCode(), requestBody);
return success(true);
}
}

View File

@@ -6,11 +6,10 @@ import com.zt.plat.framework.common.pojo.PageParam;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.framework.excel.core.util.ExcelUtils;
import com.zt.plat.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO;
import com.zt.plat.module.system.controller.admin.sms.vo.template.SmsTemplateRespVO;
import com.zt.plat.module.system.controller.admin.sms.vo.template.SmsTemplateSaveReqVO;
import com.zt.plat.module.system.controller.admin.sms.vo.template.SmsTemplateSendReqVO;
import com.zt.plat.module.system.controller.admin.sms.vo.msg.TextMessage;
import com.zt.plat.module.system.controller.admin.sms.vo.template.*;
import com.zt.plat.module.system.dal.dataobject.sms.SmsTemplateDO;
import com.zt.plat.module.system.service.msg.ISendMsgService;
import com.zt.plat.module.system.service.sms.SmsSendService;
import com.zt.plat.module.system.service.sms.SmsTemplateService;
import io.swagger.v3.oas.annotations.Operation;
@@ -19,13 +18,13 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import static com.zt.plat.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 短信模板")
@@ -37,6 +36,8 @@ public class SmsTemplateController {
private SmsTemplateService smsTemplateService;
@Resource
private SmsSendService smsSendService;
@Resource
private ISendMsgService sendMsgService;
@PostMapping("/create")
@Operation(summary = "创建短信模板")
@@ -100,4 +101,18 @@ public class SmsTemplateController {
sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams()));
}
@PostMapping("/send-msg")
@Operation(summary = "发送消息")
@PreAuthorize("@ss.hasPermission('system:sms-template:send-msg')")
public CommonResult<Object> sendMsg(@Valid @RequestBody MsgTemplateSendReqVO sendReqVO,TextMessage textMessage) throws Exception{
String msgtype = textMessage.getMsgtype();
if (StringUtils.isBlank(msgtype)){
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s",msgtype));
}
//发送消息到MQ
CommonResult<Object> objectCommonResult = sendMsgService.sendTextMsg(sendReqVO,textMessage);
return success(objectCommonResult);
}
}

View File

@@ -0,0 +1,48 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 企业微信消息发送对象父类
*
* @author dongqiang.hao
*/
@Data
@Accessors(chain = true)
public class BaseMessage implements Serializable {
/**
* 指定接收消息的成员成员ID列表多个接收者用|分隔最多支持1000个。特殊情况指定为”@all”则向该企业应用的全部成员发送
*/
private String touser;
/**
* 指定接收消息的部门部门ID列表多个接收者用|分隔最多支持100个。当touser为”@all”时忽略本参数
*/
private String toparty;
/**
* 指定接收消息的标签标签ID列表多个接收者用|分隔最多支持100个。当touser为”@all”时忽略本参数
*/
private String totag;
/**
* 企业应用的id整型。企业内部开发可在应用的设置页面查看第三方服务商可通过接口 获取企业授权信息 获取该参数值
*/
private String agentid;
/**
* 消息类型
*/
private String msgtype;
/**
* 发送消息的自建应用类型
*/
private String appType;
/**
* 表示是否开启重复消息检查0表示否1表示是默认0
*/
private Integer enable_duplicate_check = 0;
/**
* 表示是否重复消息检查的时间间隔默认1800s最大不超过4小时
*/
private Integer duplicate_check_interval;
}

View File

@@ -0,0 +1,34 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import lombok.Data;
import java.io.Serializable;
/**
* 任务卡片消息按钮对象
*
* @author luzemin
*/
@Data
public class Btn implements Serializable {
/**
* 按钮key值用户点击后会产生任务卡片回调事件回调事件会带上该key值只能由数字、字母和“_-@”组成最长支持128字节
*/
private String key;
/**
* 按钮名称最长支持18个字节超过则截断
*/
private String name;
/**
* 按钮字体颜色可选“red”或者“blue”,默认为“blue”
*/
private String color;
/**
* 按钮字体是否加粗默认false
*/
private Boolean is_bold;
}

View File

@@ -0,0 +1,22 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import lombok.Data;
import java.io.Serializable;
/**
* 小程序通知消息内容元素对象
*
* @author luzemin
*/
@Data
public class ContentItem implements Serializable {
/**
* 元素对象键值,长度10个汉字以内
*/
private String key;
/**
* 元素对象值,长度30个汉字以内支持id转译
*/
private String value;
}

View File

@@ -0,0 +1,30 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import com.zt.plat.module.system.framework.sms.core.enums.WxMsgTypeConstant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 企业微信文件消息发送对象
*
* @author luzemin
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class FileMessage extends BaseMessage {
/**
* 消息类型此时固定为file
*/
private final String msgtype = WxMsgTypeConstant.FILE.getCode();
/**
* 企业微信文件消息体对象
*/
private Media file = new Media();
/**
* 表示是否是保密消息0表示可对外分享1表示不能分享且内容显示水印默认为0
*/
private Integer safe = 0;
}

View File

@@ -0,0 +1,31 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import com.zt.plat.module.system.framework.sms.core.enums.WxMsgTypeConstant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 企业微信图片消息发送对象
*
* @author luzemin
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class ImageMessage extends BaseMessage {
/**
* 消息类型此时固定为image
*/
private final String msgtype = WxMsgTypeConstant.IMAGE.getCode();
/**
* 企业微信图片消息体对象
*/
private Media image = new Media();
/**
* 表示是否是保密消息0表示可对外分享1表示不能分享且内容显示水印默认为0
*/
private Integer safe = 0;
}

View File

@@ -0,0 +1,40 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 企业微信任务卡片消息体对象
*
* @author luzemin
*/
@Data
public class InteractiveTaskCard implements Serializable {
/**
* 标题不超过128个字节超过会自动截断支持id转译
*/
private String title;
/**
* 描述不超过512个字节超过会自动截断支持id转译
*/
private String description;
/**
* 点击后跳转的链接。最长2048字节请确保包含了协议头(http/https)
*/
private String url;
/**
* 任务id同一个应用发送的任务卡片消息的任务id不能重复只能由数字、字母和“_-@”组成最长支持128字节
*/
private String task_id;
/**
* 按钮列表按钮个数为1~2个。
*/
private List<Btn> btn = new ArrayList<>();
}

View File

@@ -0,0 +1,29 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import com.zt.plat.module.system.framework.sms.core.enums.WxMsgTypeConstant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 企业微信任务卡片消息发送对象
* 仅企业微信3.1.6及以上版本支持
*
* @author luzemin
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class InteractiveTaskCardMessage extends BaseMessage {
/**
* 消息类型此时固定为interactive_taskcard
*/
private final String msgtype = WxMsgTypeConstant.INTERACTIVE_TASKCARD.getCode();
/**
* 任务卡片消息体对象,
*/
private InteractiveTaskCard interactive_taskcard = new InteractiveTaskCard();
/**
* 表示是否开启id转译0表示否1表示是默认0。仅第三方应用需要用到企业自建应用可以忽略。
*/
private Integer enable_id_trans = 0;
}

View File

@@ -0,0 +1,17 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import lombok.Data;
import java.io.Serializable;
/**
* 企业微信markdown内容消息体对象
*
* @author luzemin
*/
@Data
public class Markdown implements Serializable {
/**
* markdown内容最长不超过2048个字节必须是utf8编码
*/
private String content;
}

View File

@@ -0,0 +1,25 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import com.zt.plat.module.system.framework.sms.core.enums.WxMsgTypeConstant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 企业微信markdown消息发送对象
*
* @author luzemin
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class MarkdownMessage extends BaseMessage {
/**
* 消息类型此时固定为markdown
*/
private final String msgtype = WxMsgTypeConstant.MARKDOWN.getCode();
/**
* markdown消息内容对象
*/
private Markdown markdown = new Markdown();
}

View File

@@ -0,0 +1,28 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import lombok.Data;
import java.io.Serializable;
/**
* 企业微信媒体消息体对象
*
* @author luzemin
*/
@Data
public class Media implements Serializable {
/**
* 文件上传时企业微信服务器读取文件流的name
*/
public static final String MEDIA_NAME = "media";
/**
* 图片、语音、视频媒体文件id可以调用上传临时素材接口获取
*/
private String media_id;
/**
* 视频消息的标题不超过128个字节超过会自动截断视频消息
*/
private String title;
/**
* 视频消息的描述不超过512个字节超过会自动截断视频消息
*/
private String description;
}

View File

@@ -0,0 +1,44 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 企业微信图文消息体对象
*
* @author luzemin
*/
@Data
public class MiniProgramNotice implements Serializable {
/**
* 小程序appid必须是与当前应用关联的小程序
*/
private String appid;
/**
* 点击消息卡片后的小程序页面,仅限本小程序内的页面。该字段不填则消息点击后不跳转。
*/
private String page;
/**
* 消息标题长度限制4-12个汉字支持id转译
*/
private String title;
/**
* 消息描述长度限制4-12个汉字支持id转译
*/
private String description;
/**
* 是否放大第一个content_item
*/
private String emphasis_first_item;
/**
* 消息内容键值对最多允许10个item
*/
private List<ContentItem> content_item = new ArrayList<>();
}

View File

@@ -0,0 +1,28 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import com.zt.plat.module.system.framework.sms.core.enums.WxMsgTypeConstant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 企业微信小程序通知消息发送对象
*
* @author luzemin
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class MiniProgramNoticeMessage extends BaseMessage {
/**
* 消息类型此时固定为miniprogram_notice
*/
private final String msgtype = WxMsgTypeConstant.MINIPROGRAM_NOTICE.getCode();
/**
* 小程序通知消息内容对象,
*/
private MiniProgramNotice miniprogram_notice = new MiniProgramNotice();
/**
* 表示是否开启id转译0表示否1表示是默认0。仅第三方应用需要用到企业自建应用可以忽略。
*/
private Integer enable_id_trans = 0;
}

View File

@@ -0,0 +1,36 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import com.zt.plat.module.system.framework.sms.core.client.dto.msg.MpNews;
import com.zt.plat.module.system.framework.sms.core.enums.WxMsgTypeConstant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 企业微信图文消息发送对象(文件存储在企业微信)
*
* @author luzemin
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class MpNewsMessage extends BaseMessage {
/**
* 消息类型此时固定为mpnews
*/
private final String msgtype = WxMsgTypeConstant.MPNEWS.getCode();
/**
* 企业微信图文消息体对象
*/
private MpNews mpnews = new MpNews();
/**
* 表示是否是保密消息0表示可对外分享1表示不能分享且内容显示水印默认为0
*/
private Integer safe = 0;
/**
* 表示是否开启id转译0表示否1表示是默认0。仅第三方应用需要用到企业自建应用可以忽略。
*/
private Integer enable_id_trans = 0;
}

View File

@@ -0,0 +1,31 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import com.zt.plat.module.system.framework.sms.core.client.dto.msg.News;
import com.zt.plat.module.system.framework.sms.core.enums.WxMsgTypeConstant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 企业微信图文消息发送对象
*
* @author luzemin
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class NewsMessage extends BaseMessage {
/**
* 消息类型此时固定为news
*/
private final String msgtype = WxMsgTypeConstant.NEWS.getCode();
/**
* 企业微信图文消息体对象
*/
private News news = new News();
/**
* 表示是否开启id转译0表示否1表示是默认0。仅第三方应用需要用到企业自建应用可以忽略。
*/
private Integer enable_id_trans = 0;
}

View File

@@ -0,0 +1,43 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import com.zt.plat.module.system.util.StringSolveUtils;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* 企业微信文本消息体对象
*
* @author luzemin
*/
@Data
public class Text implements Serializable {
/**
* 点击事件消息模板
*/
public static final String URL_CONTENT_TEMPLATE = "<a href=\"${url}\">${content}</a>";
/**
* 消息内容最长不超过2048个字节超过将截断支持id转译
*/
private String content;
/**
* 消息点击url地址(自定义属性)
*/
private String url;
/**
* 构建点击事件消息模板
*/
public void buildUrlContent() {
if (StringUtils.isNotBlank(this.getUrl())) {
Map<String, Object> substituteMap = new HashMap<>(2);
substituteMap.put("url", this.getUrl());
substituteMap.put("content", this.getContent());
this.setContent(StringSolveUtils.placeholderReplace(URL_CONTENT_TEMPLATE, substituteMap));
}
}
}

View File

@@ -0,0 +1,29 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import com.zt.plat.module.system.framework.sms.core.client.dto.msg.TextCard;
import com.zt.plat.module.system.framework.sms.core.enums.WxMsgTypeConstant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 企业微信文本卡片消息发送对象
*
* @author luzemin
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class TextCardMessage extends BaseMessage {
/**
* 消息类型此时固定为textcard
*/
private final String msgtype = WxMsgTypeConstant.TEXTCARD.getCode();
/**
* 消息内容对象
*/
private TextCard textcard = new TextCard();
/**
* 表示是否开启id转译0表示否1表示是默认0。仅第三方应用需要用到企业自建应用可以忽略。
*/
private Integer enable_id_trans = 0;
}

View File

@@ -0,0 +1,33 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import com.zt.plat.module.system.framework.sms.core.enums.WxMsgTypeConstant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 企业微信文本消息发送对象
*
* @author luzemin
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class TextMessage extends BaseMessage {
/**
* 消息类型此时固定为text
*/
private final String msgtype = WxMsgTypeConstant.TEXT.getCode();
/**
* 消息内容对象其中text参数的content字段可以支持换行、以及A标签即可打开自定义的网页可参考以上示例代码(注意:换行符请用转义过的\n)
*/
private Text text = new Text();
/**
* 表示是否是保密消息0表示可对外分享1表示不能分享且内容显示水印默认为0
*/
private Integer safe = 0;
/**
* 表示是否开启id转译0表示否1表示是默认0。仅第三方应用需要用到企业自建应用可以忽略。
*/
private Integer enable_id_trans = 0;
}

View File

@@ -0,0 +1,30 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import com.zt.plat.module.system.framework.sms.core.enums.WxMsgTypeConstant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 企业微信视频消息发送对象
*
* @author luzemin
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class VideoMessage extends BaseMessage {
/**
* 消息类型此时固定为video
*/
private final String msgtype = WxMsgTypeConstant.VIDEO.getCode();
/**
* 企业微信视频消息体对象
*/
private Media video = new Media();
/**
* 表示是否是保密消息0表示可对外分享1表示不能分享且内容显示水印默认为0
*/
private Integer safe = 0;
}

View File

@@ -0,0 +1,25 @@
package com.zt.plat.module.system.controller.admin.sms.vo.msg;
import com.zt.plat.module.system.framework.sms.core.enums.WxMsgTypeConstant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 企业微信语音消息发送对象
*
* @author luzemin
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class VoiceMessage extends BaseMessage {
/**
* 消息类型此时固定为voice
*/
private final String msgtype = WxMsgTypeConstant.VOICE.getCode();
/**
* 企业微信语音消息体对象
*/
private Media voice = new Media();
}

View File

@@ -0,0 +1,31 @@
package com.zt.plat.module.system.controller.admin.sms.vo.template;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotNull;
import java.util.Map;
@Schema(description = "管理后台 - 短信模板的发送 Request VO")
@Data
public class MsgTemplateSendReqVO {
/* @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300")
@NotNull(message = "手机号不能为空")
private String mobile;*/
@Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01")
@NotNull(message = "模板编码不能为空")
private String templateCode;
@Schema(description = "模板参数")
private Map<String, Object> templateParams;
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01")
@NotNull(message = "用户ID不能为空")
private Long userId;
@Schema(description = "用户属性", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01")
@NotNull(message = "用户属性不能为空")
private Integer userType;
}

View File

@@ -74,14 +74,15 @@ public class DeptDO extends TenantBaseDO {
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 是否公司
*/
private Boolean isCompany;
/**
* 是否集团
*/
private Boolean isGroup;
/**
* 是否公司
*/
private Boolean isCompany;
/**
* 部门来源类型

View File

@@ -0,0 +1,69 @@
package com.zt.plat.module.system.dal.dataobject.msg;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.zt.plat.framework.common.enums.CommonStatusEnum;
import com.zt.plat.framework.mybatis.core.dataobject.BaseDO;
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
import com.zt.plat.module.system.framework.sms.core.enums.SmsChannelEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 消息渠道 DO
*
* @author zzf
* @since 2021-01-25
*/
@TableName(value = "system_sms_channel", autoResultMap = true)
@KeySequence("system_sms_channel_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@TenantIgnore
public class MsgChannelDO extends BaseDO {
/**
* 渠道编号
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 短信签名
*/
private String signature;
/**
* 企业编号epid
*/
private String epid;
/**
* 渠道编码
* 枚举 {@link SmsChannelEnum}
*/
private String code;
/**
* 启用状态
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 短信 API 的账号
*/
private String apiKey;
/**
* 短信 API 的密钥
*/
private String apiSecret;
/**
* 短信发送回调 URL
*/
private String callbackUrl;
}

View File

@@ -0,0 +1,87 @@
package com.zt.plat.module.system.dal.dataobject.msg;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.zt.plat.framework.common.enums.CommonStatusEnum;
import com.zt.plat.framework.mybatis.core.dataobject.BaseDO;
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
import com.zt.plat.module.system.dal.dataobject.sms.SmsChannelDO;
import com.zt.plat.module.system.enums.sms.SmsTemplateTypeEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.List;
/**
* 消息模板 DO
* @author zzf
* @since 2021-01-25
*/
@TableName(value = "system_sms_template", autoResultMap = true)
@KeySequence("system_sms_template_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@TenantIgnore
public class MsgTemplateDO extends BaseDO {
/**
* 自增编号
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
// ========= 模板相关字段 =========
/**
* 短信类型
* 枚举 {@link SmsTemplateTypeEnum}
*/
private Integer type;
/**
* 启用状态
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 模板编码,保证唯一
*/
private String code;
/**
* 模板名称
*/
private String name;
/**
* 模板内容
* 内容的参数,使用 {} 包括,例如说 {name}
*/
private String content;
/**
* 参数数组(自动根据内容生成)
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> params;
/**
* 备注
*/
private String remark;
/**
* 短信 API 的模板编号
*/
private String apiTemplateId;
// ========= 渠道相关字段 =========
/**
* 短信渠道编号
* 关联 {@link SmsChannelDO#getId()}
*/
private Long channelId;
/**
* 短信渠道编码
* 冗余 {@link SmsChannelDO#getCode()}
*/
private String channelCode;
}

View File

@@ -0,0 +1,56 @@
package com.zt.plat.module.system.dal.dataobject.msg;
import com.baomidou.mybatisplus.annotation.*;
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
/**
* Activemq消息队列
*
* @author Dy
* @since 2021-07-22
*/
@TableName(value = "sys_active_mq", autoResultMap = true)
@KeySequence("sys_active_mq_seq")
@Data
@ToString(callSuper = true)
@TenantIgnore
public class SysActiveMq implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "ID", type = IdType.ASSIGN_ID)
private String id;
@TableField("TITLE")
private String title;
@TableField("JMS_TYPE")
private String jmsType;
@TableField("CONTENT_TYPE")
private String contentType;
@TableField("DATETIME")
private String datetime;
@TableField("CONSUME_DATETIME")
private String consumeDatetime;
@TableField("PUBLISHER")
private String publisher;
@TableField("LISTENER_CLASS_NAME")
private String listenerClassName;
@TableField("CONSUME_FLAG")
private String consumeFlag;
@TableField("CONSUME_MESSAGE")
private String consumeMessage;
@TableField("TEXT_CONTENT")
private String textContent;
}

View File

@@ -0,0 +1,56 @@
package com.zt.plat.module.system.dal.dataobject.msg;
import com.baomidou.mybatisplus.annotation.*;
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
/**
* Activemq消息队列
*
* @author Dy
* @since 2021-07-22
*/
@TableName(value = "sys_active_mq_log", autoResultMap = true)
@KeySequence("sys_active_mq_log_seq")
@Data
@ToString(callSuper = true)
@TenantIgnore
public class SysActiveMqLog implements Serializable {
@TableId(value = "ID", type = IdType.ASSIGN_ID)
private String id;
@TableField("TITLE")
private String title;
@TableField("JMS_TYPE")
private String jmsType;
@TableField("CONTENT_TYPE")
private String contentType;
@TableField("DATETIME")
private String datetime;
@TableField("CONSUME_DATETIME")
private String consumeDatetime;
@TableField("PUBLISHER")
private String publisher;
@TableField("LISTENER_CLASS_NAME")
private String listenerClassName;
@TableField("CONSUME_FLAG")
private String consumeFlag;
@TableField("CONSUME_MESSAGE")
private String consumeMessage;
@TableField("TEXT_CONTENT")
private String textContent;
}

View File

@@ -0,0 +1,89 @@
package com.zt.plat.module.system.dal.dataobject.msg;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
/**
* 企业微信审核配置表
* @author Dy
* @since 2021-08-26
*/
@TableName(value = "sys_wx_audit_config", autoResultMap = true)
@KeySequence("sys_wx_audit_config_seq")
@Data
@ToString(callSuper = true)
@TenantIgnore
public class SysWxAuditConfig implements Serializable {
/**
* 状态-生效
*/
public static final String STATE_ACTIVE = "active";
/**
* 状态-未生效
*/
public static final String STATE_INACTIVE = "inactive";
private static final long serialVersionUID = 1L;
@TableField("APP_TYPE")
private String appType;
@TableField("CORP_ID")
private String corpId;
@TableField("CORP_SECRET")
private String corpSecret;
@TableField("AGENT_ID")
private String agentId;
@TableField("STATE")
private String state;
@TableField("SORT_INDEX")
private Integer sortIndex;
@TableField("EXT1")
private String ext1;
@TableField("EXT2")
private String ext2;
@TableField("EXT3")
private String ext3;
@TableField("CREATE_USER")
private String createUser;
@TableField("CREATE_USER_NAME")
private String createUserName;
@TableField("ID")
private Integer id;
@TableField("WX_CONTEXT_PATH")
private String wxContextPath;
@TableField("HTTP_CONTEXT_PATH")
private String httpContextPath;
@TableField("UPDATE_TIME")
private String updateTime;
@TableField("CREATE_TIME")
private String createTime;
@TableField("UPDATE_USER")
private String updateUser;
@TableField("UPDATE_USER_NAME")
private String updateUserName;
}

View File

@@ -0,0 +1,12 @@
package com.zt.plat.module.system.dal.mysql.msg;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.module.system.dal.dataobject.msg.SysActiveMqLog;
import com.zt.plat.module.system.dal.dataobject.sms.SmsLogDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysActiveMqDao extends BaseMapperX<SysActiveMqLog> {
}

View File

@@ -0,0 +1,28 @@
package com.zt.plat.module.system.framework.sms.core.client.dto.msg;
import lombok.Data;
import java.io.Serializable;
/**
* 企业微信图文消息体对象
*
* @author ZT
*/
@Data
public class Article implements Serializable {
/**
* 标题不超过128个字节超过会自动截断支持id转译
*/
private String title;
/**
* 描述不超过512个字节超过会自动截断支持id转译
*/
private String description;
/**
* 图文消息的图片链接支持JPG、PNG格式较好的效果为大图 1068*455小图150*150。
*/
private String url;
}

View File

@@ -0,0 +1,44 @@
package com.zt.plat.module.system.framework.sms.core.client.dto.msg;
import lombok.Data;
import java.io.Serializable;
/**
* 企业微信图文消息体对象(文件存储在企业微信)
*
* @author luzemin
*/
@Data
public class MpArticle implements Serializable {
/**
* 标题不超过128个字节超过会自动截断支持id转译
*/
private String title;
/**
* 图文消息缩略图的media_id, 可以通过素材管理接口获得。此处thumb_media_id即上传接口返回的media_id
*/
private String thumb_media_id;
/**
* 图文消息的作者不超过64个字节
*/
private String author;
/**
* 图文消息点击“阅读原文”之后的页面链接
*/
private String content_source_url;
/**
* 图文消息的内容支持html标签不超过666 K个字节支持id转译
*/
private String content;
/**
* 图文消息的描述不超过512个字节超过会自动截断支持id转译
*/
private String digest;
}

View File

@@ -0,0 +1,19 @@
package com.zt.plat.module.system.framework.sms.core.client.dto.msg;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 企业微信图文消息体对象(文件存储在企业微信)
*
* @author luzemin
*/
@Data
public class MpNews implements Serializable {
/**
* 图文消息一个图文消息支持1到8条图文
*/
private List<MpArticle> articles = new ArrayList<>();
}

View File

@@ -0,0 +1,19 @@
package com.zt.plat.module.system.framework.sms.core.client.dto.msg;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 企业微信图文消息体对象
*
* @author luzemin
*/
@Data
public class News implements Serializable {
/**
* 图文消息一个图文消息支持1到8条图文
*/
private List<Article> articles = new ArrayList<>();
}

View File

@@ -0,0 +1,37 @@
package com.zt.plat.module.system.framework.sms.core.client.dto.msg;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 企业微信文本卡片消息体对象
*
* @author luzemin
*/
@Data
@Accessors(chain = true)
public class TextCard implements Serializable {
/**
* 标题不超过128个字节超过会自动截断支持id转译
*/
private String title;
/**
* 描述不超过512个字节超过会自动截断支持id转译
*/
private String description;
/**
* 消息点击点击后跳转的链接。最长2048字节请确保包含了协议头(http/https)
*/
private String url;
/**
* 按钮文字。 默认为“详情”, 不超过4个文字超过自动截断。
*/
private String btntxt;
}

View File

@@ -7,7 +7,6 @@ import com.zt.plat.module.system.framework.sms.core.property.SmsChannelPropertie
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -83,6 +82,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
case QINIU: return new QiniuSmsClient(properties);
// case CMCC_MAS: return new CmccMasSmsClient(properties);
case HL95: return new Hl95SmsClient(properties);
case ZLE: return new ZleSmsClient(properties);
}
// 创建失败,错误日志 + 抛出异常
log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);

View File

@@ -0,0 +1,171 @@
package com.zt.plat.module.system.framework.sms.core.client.impl;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
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.zt.plat.framework.common.core.KeyValue;
import com.zt.plat.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.zt.plat.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import com.zt.plat.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.zt.plat.module.system.framework.sms.core.client.impl.extra.SmsBalanceClient;
import com.zt.plat.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import com.zt.plat.module.system.framework.sms.core.property.SmsChannelProperties;
import lombok.extern.slf4j.Slf4j;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
public class ZleSmsClient extends AbstractSmsClient implements SmsBalanceClient {
private static final String SEND_URL = "https://api.sms.95ytx.com:9091/mxt/send";
private static final String BALANCE_URL = "https://api.sms.95ytx.com:9091/mxt/getfee";
private static final DateTimeFormatter STATUS_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
public ZleSmsClient(SmsChannelProperties properties) {
super(properties);
Assert.notEmpty(properties.getApiKey(), "用户名(apiKey) 不能为空");
Assert.notEmpty(properties.getApiSecret(), "密码(apiSecret) 不能为空");
}
@Override
public SmsSendRespDTO sendSms(Long logId, String mobile, String content, String apiTemplateId,
List<KeyValue<String, Object>> templateParams) {
Assert.notEmpty(properties.getEpid(), "中铝e办需要配置 epid");
Assert.notEmpty(properties.getSignature(), "短信签名不能为空");
String finalContent = appendSignatureIfMissing(content, properties.getSignature());
String linkId = buildLinkId(logId);
Map<String, Object> form = new HashMap<>();
form.put("username", properties.getApiKey());
form.put("password", properties.getApiSecret());
form.put("epid", properties.getEpid());
form.put("phone", mobile);
form.put("message", finalContent);
form.put("linkid", linkId);
// subcode 可为空
String resp;
try (HttpResponse response = HttpRequest.post(SEND_URL)
.form(form)
.charset(StandardCharsets.UTF_8)
.execute()) {
resp = StrUtil.trim(response.body());
}
boolean success = StrUtil.equals(resp, "00");
return new SmsSendRespDTO()
.setSuccess(success)
.setApiCode(resp)
.setApiMsg(resp)
.setApiRequestId(linkId)
.setSerialNo(linkId);
}
/**
* 解析短信状态
* @param text 结果
* @return List<SmsReceiveRespDTO>
*/
@Override
public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
JSONObject obj = JSONUtil.parseObj(text, false);
String reportCode = obj.getStr("FReportCode");
String linkId = obj.getStr("FLinkID");
LocalDateTime deliverTime = parseDeliverTime(obj.getStr("FDeliverTime"));
String mobile = obj.getStr("FDestAddr");
boolean success = StrUtil.equalsIgnoreCase(reportCode, "DELIVRD") || StrUtil.equals(reportCode, "0");
Long logId = parseLongSafely(linkId);
SmsReceiveRespDTO dto = new SmsReceiveRespDTO()
.setSuccess(success)
.setErrorCode(reportCode)
.setErrorMsg(reportCode)
.setMobile(mobile)
.setReceiveTime(deliverTime)
.setSerialNo(linkId)
.setLogId(logId);
return Collections.singletonList(dto);
}
@Override
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) {
// 中铝e办无模板审核接口直接返回可用
return new SmsTemplateRespDTO()
.setId(apiTemplateId)
.setContent(apiTemplateId)
.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus());
}
/**
* 查询余额
* @return Integer
*/
@Override
public Integer queryBalance() {
Assert.notEmpty(properties.getEpid(), "中铝e办需要配置 epid");
Map<String, Object> form = MapUtil.<String, Object>builder()
.put("username", properties.getApiKey())
.put("password", properties.getApiSecret())
.put("epid", properties.getEpid())
.build();
String resp;
try (HttpResponse response = HttpRequest.get(BALANCE_URL)
.form(form)
.charset(StandardCharsets.UTF_8)
.execute()) {
if (response.getStatus() != HttpURLConnection.HTTP_OK) {
throw new IllegalStateException("余额查询失败HTTP 状态码:" + response.getStatus());
}
resp = StrUtil.trim(response.body());
}
if (!StrUtil.isNumeric(resp)) {
throw new IllegalStateException("余额查询失败,返回值:" + resp);
}
return Integer.valueOf(resp);
}
private static String appendSignatureIfMissing(String content, String signature) {
if (StrUtil.isBlank(signature)) {
return content;
}
String wrapped = StrUtil.startWithAny(signature, "", "[") ? signature : "" + signature + "";
return StrUtil.startWith(content, wrapped) ? content : wrapped + content;
}
private static String buildLinkId(Long logId) {
String raw = String.valueOf(logId);
return raw.length() > 20 ? raw.substring(raw.length() - 20) : raw;
}
private static LocalDateTime parseDeliverTime(String timeText) {
if (StrUtil.isBlank(timeText)) {
return null;
}
try {
return LocalDateTime.parse(timeText, STATUS_TIME_FORMATTER);
} catch (Exception ex) {
log.warn("[parseDeliverTime][无法解析时间:{}]", timeText, ex);
return null;
}
}
private static Long parseLongSafely(String text) {
try {
return Long.parseLong(text);
} catch (Exception ignore) {
return null;
}
}
}

View File

@@ -0,0 +1,155 @@
package com.zt.plat.module.system.framework.sms.core.enums;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 审核类型静态枚举类
*
* @author luzemin
*/
@Component
public class AuditConstants {
/**
* 中铝E办驰宏数字化应用企业微信
*/
public static final String CHINALCO_ZLEB_CHSZH = "chinalco_zleb_chszh";
/**
* 中铜阳光采购应用(企业微信)
*/
public static final String CHINALCO_SUNEPS = "chinalco_suneps";
/**
* token身份认证令牌
*/
public static final String TOKEN = "0123456789abcdefghijklmnopqrstuvwxyz";
/**
* 企业微信审核页面加载配置代码集ID
*/
public static final String AUDIT_CODE_SET = "qywx.audit_dispatcher";
/**
* 阳光采购企业微信审核回调地址集合映射
*/
public static final Map<String, String> REDIRECT_URL_MAPPING = new HashMap<>(1);
/**
* 企业微信审核回调地址-需要微信认证,使用时需要在前面添加微信请求uri
*/
public static final String REDIRECT_URL_AUTH = "/qywx/${appType}/index?appType=${appType}&approveType=${approveType}&processInstanceId=${processInstanceId}&taskId=${taskId}&companyCode=${companyCode}&businessId=${businessId}";
/**
* 企业微信审核回调地址-不需要微信认证,使用时需要在前面添加微信请求uri
*/
public static final String REDIRECT_URL = "/${appType}/index?appType=${appType}&approveType=${approveType}&processInstanceId=${processInstanceId}&taskId=${taskId}&companyCode=${companyCode}&businessId=${businessId}";
static {
REDIRECT_URL_MAPPING.put("noAuth", REDIRECT_URL);
REDIRECT_URL_MAPPING.put("auth", REDIRECT_URL_AUTH);
}
/**
* 审批类型
*/
public enum SunepsAuditType {
/**
* 询价单审批
*/
REQUEST_AUDIT("requestAudit", "询价单审批"),
/**
* 会审会签审批
*/
RESULT_AUDIT("resultAudit", "会审会签审批"),
/**
* 流标审批
*/
FLOW_AUDIT("flowAudit", "流标审批"),
/**
* 谈判方案审批
*/
NEGO_PLAN("negoPlan", "谈判方案审批"),
/**
* 终止审批
*/
TERM_AUDIT("termAudit", "终止审批"),
/**
* 评标报告审批
*/
REQUEST_EVA_REPORT("requestEvaReport", "评标报告审批"),
/**
* 供应商注册
*/
SUPPLIER_REGISTER("supplierRegister", "供应商注册"),
/**
* 供应商自荐
*/
SUPPLIER_SELF("supplierSelf", "供应商自荐"),
/**
* 供应商基本信息变更
*/
SUPPLIER_BASE_INFO("supplierBaseInfo", "供应商基本信息变更"),
/**
* 供应商启用
*/
SUPPLIER_ENABLE("supplierEnable", "供应商启用"),
/**
* 供应商可供大类变更
*/
SUPPLIER_MATE_TYPE("supplierMateType", "供应商可供大类变更"),
/**
* 合格供应商变更
*/
SUPPLIER_CHANGE("supplierChange", "合格供应商变更"),
/**
* 特种供应商认证审批
*/
T_SUPP_AUDIT("TSuppAudit", "特种供应商认证审批"),
/**
* 审核消息卡片描述模型
*/
AUDIT_DESCRIPTION_FORMAT("<div class=\"gray\">${subTitle}</div> <div class=\"normal\">${description}</div> <div class=\"normal\">当前节点:${taskName}</div> <div class=\"highlight\">点击查看详细情况</div>", "审核消息卡片描述模型"),
/**
* 审核回调url,使用时需要在前面添加微信请求uri
*/
REDIRECT_URL("/qywx/${appType}/index?appType=${appType}&approveType=${approveType}&processInstanceId=${processInstanceId}&taskId=${taskId}&companyCode=${companyCode}&businessId=${businessId}", "审核回调url");
/**
* 消息全局键key-key
*/
public static final String CODE_KEY = "code";
/**
* 消息全局值value-key
*/
public static final String MSG_KEY = "msg";
/**
* 审核类型代码
*/
private final String code;
/**
* 审核类型说明
*/
private final String msg;
SunepsAuditType(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
}

View File

@@ -0,0 +1,55 @@
package com.zt.plat.module.system.framework.sms.core.enums;
/**
* 消息ActiveMQ静态变量枚举
*
* @author luzemin
*/
public enum JmsConstant {
/**
* 测试点对点消息队列,使用时需要添加环境前缀JmsProperties jmsProperties===>jmsProperties.getQueuePrefix()
*/
QUEUE_TEST("queue.test", "测试点对点消息队列"),
/**
* 测试主题消息队列,使用时需要添加环境前缀JmsProperties jmsProperties===>jmsProperties.getTopicPrefix()
*/
TOPIC_TEST("topic.test", "测试主题消息队列"),
/**
* 消息类型-队列
*/
JMS_TYPE_QUEUE("queue", "消息类型-队列"),
/**
* 消息类型-主题
*/
JMS_TYPE_TOPIC("topic", "消息类型-主题"),
/**
* 消息消费结果-成功
*/
CONSUME_SUCCESS("success", "消息消费结果-成功"),
/**
* 消息消费结果-失败
*/
CONSUME_FAILURE("failure", "消息消费结果-失败");
/**
* 值
*/
private final String code;
/**
* 说明
*/
private final String msg;
JmsConstant(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
public String getMsg() {
return msg;
}
}

View File

@@ -21,6 +21,7 @@ public enum SmsChannelEnum {
QINIU("QINIU", "七牛云"),
HL95("HL95", "鸿联九五"),
// CMCC_MAS("CMCC_MAS", "中国移动云MAS"),
ZLE("ZLE", "中铝e办"),
;
/**

View File

@@ -0,0 +1,94 @@
package com.zt.plat.module.system.framework.sms.core.enums;
/**
* 企业微信消息类型枚举类
*
* @author luzemin
*/
public enum WxMsgTypeConstant {
/**
* 文本消息:其中text参数的content字段可以支持换行、以及A标签即可打开自定义的网页可参考以上示例代码(注意:换行符请用转义过的\n)
*/
TEXT("text", "文本消息"),
/**
* 图片消息
*/
IMAGE("image", "图片消息"),
/**
* 语音消息
*/
VOICE("voice", "语音消息"),
/**
* 视频消息
*/
VIDEO("video", "视频消息"),
/**
* 文件消息
*/
FILE("file", "文件消息"),
/**
* 卡片消息的展现形式非常灵活支持使用br标签或者空格来进行换行处理也支持使用div标签来使用不同的字体颜色目前内置了3种文字颜色灰色(gray)、高亮(highlight)、默认黑色(normal)将其作为div标签的class属性即可具体用法请参考上面的示例。
*/
TEXTCARD("textcard", "文本卡片消息"),
/**
* 图文消息
*/
NEWS("news", "图文消息"),
/**
* mpnews类型的图文消息跟普通的图文消息一致唯一的差异是图文内容存储在企业微信。多次发送mpnews会被认为是不同的图文阅读、点赞的统计会被分开计算。
*/
MPNEWS("mpnews", "图文消息mpnews"),
/**
* 目前仅支持markdown语法的子集,微工作台原企业号不支持展示markdown消息
*/
MARKDOWN("markdown", "markdown消息"),
/**
* 小程序通知消息
* 小程序通知消息只允许绑定了小程序的应用发送,之前,消息会通过统一的会话【小程序通知】发送给用户。
* 从2019年6月28日起用户收到的小程序通知会出现在各个独立的应用中。
* 不支持@all全员发送
*/
MINIPROGRAM_NOTICE("miniprogram_notice", "小程序通知消息"),
/**
* 任务卡片消息
* 仅企业微信3.1.6及以上版本支持
* 任务卡片消息的展现支持简单的markdown语法详情请见附录支持的markdown语法 。
* 要发送该类型的消息应用必须配置好回调URL详见配置应用回调用户点击任务卡片的按钮后企业微信会回调任务卡片事件到该URL配置的URL按任务卡片更新消息协议返回数据即可。
* 开发者可以通过更新任务卡片消息状态接口更新卡片状态。
*/
INTERACTIVE_TASKCARD("interactive_taskcard", "任务卡片消息");
/**
* 类型key
*/
private final String code;
/**
* 类型说明
*/
private final String msg;
/**
* 构造器
*
* @param code 类型
* @param msg 说明
*/
WxMsgTypeConstant(String code, String msg) {
this.code = code;
this.msg = msg;
}
/**
* 获取类型
*/
public String getCode() {
return code;
}
/**
* 获取说明
*/
public String getMsg() {
return msg;
}
}

View File

@@ -0,0 +1,176 @@
package com.zt.plat.module.system.mq.consumer.msg;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.zt.plat.module.system.controller.admin.sms.vo.msg.TextCardMessage;
import com.zt.plat.module.system.framework.sms.core.client.dto.msg.TextCard;
import com.zt.plat.module.system.framework.sms.core.enums.AuditConstants;
import com.zt.plat.module.system.framework.sms.core.enums.JmsConstant;
import com.zt.plat.module.system.framework.sms.core.enums.WxMsgTypeConstant;
import com.zt.plat.module.system.mq.message.sms.SmsSendMessage;
import com.zt.plat.module.system.service.msg.ISendWxMsgService;
import com.zt.plat.module.system.service.msg.ISysRocketMqService;
import com.zt.plat.module.system.util.StringSolveUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
/**
* 中铝 e 办的消费者
*
* @author zzf
*/
@Component
@RocketMQMessageListener(
topic = SmsSendMessage.TOPIC,
consumerGroup = SmsSendMessage.TOPIC + "_CONSUMER"
)
@Slf4j
public class MsgSendConsumer implements RocketMQListener<MessageExt> {
@Resource
private ISysRocketMqService sysActiveMqService;
@Resource
private ISendWxMsgService sendWxMsgService;
@Override
public void onMessage(MessageExt msg) {
log.info("中铝e办[onMessage][消息内容({})]", msg.toString());
String msgId = msg.getMsgId();
int reconsumeTimes = msg.getReconsumeTimes();
String consumeMsg;
try {
if (msg.getBody() == null || msg.getBody().length == 0){
log.error("[onMessage][消息体为空 msgId={}]", msgId);
return;
}
String json = new String(msg.getBody(), StandardCharsets.UTF_8);
JSONObject jsonObject = JSON.parseObject(json);
String toUser = jsonObject.getString("touser"); // 企业微信接收者
String userName = jsonObject.getString("userName"); // 用户名
String userEname = jsonObject.getString("userEname"); // 英文名
String userCname = jsonObject.getString("userCname"); // 中文名
String desc = jsonObject.getString("description");
// 校验接受用户
if (StringUtils.isBlank(toUser)) {
consumeMsg = "企业微信消息发送失败,用户【" + userEname + "-" + userCname + "】未绑定企业微信账号!";
//TODO 添加文本消息
sysActiveMqService.saveByTextMessage(this.getClass().getName(), msg, JmsConstant.JMS_TYPE_QUEUE.getCode(), JmsConstant.CONSUME_FAILURE.getCode(), consumeMsg);
} else {
String msgtype = jsonObject.getString("msgtype");
// 文本卡片消息
if (Objects.equals(msgtype, WxMsgTypeConstant.TEXTCARD.getCode())) {
/* 构建审批回调url */
String appType = jsonObject.getString("appType");
String approveType = jsonObject.getString("approveType");
String companyCode = jsonObject.getString("userCompanyCode");
String taskId = jsonObject.getString("taskId");
String processInstanceId = jsonObject.getString("processInstanceId");
String businessId = jsonObject.getString("businessId");
Map<String, Object> substituteMap = new HashMap<>(5);
substituteMap.put("appType", appType.toLowerCase(Locale.ROOT));
substituteMap.put("approveType", approveType);
substituteMap.put("companyCode", companyCode);
substituteMap.put("taskId", taskId);
substituteMap.put("processInstanceId", processInstanceId);
substituteMap.put("businessId", businessId);
String redirectUrl = StringSolveUtils.placeholderReplace(AuditConstants.SunepsAuditType.REDIRECT_URL.getCode(), substituteMap);
/* 构建文本卡片消息 */
String title = jsonObject.getString("title");
String taskName = jsonObject.getString("taskName");
String datetime = jsonObject.getString("datetime");
String btnTxt = jsonObject.getString("btntxt");
substituteMap.put("subTitle", datetime + "——" + userName);
substituteMap.put("description", desc);
substituteMap.put("taskName", taskName);
String description = StringSolveUtils.placeholderReplace(AuditConstants.SunepsAuditType.AUDIT_DESCRIPTION_FORMAT.getCode(), substituteMap);
TextCardMessage textCardMessage = new TextCardMessage();
TextCard textCard = new TextCard();
textCard.setTitle(title)
.setDescription(description)
.setUrl(redirectUrl);
if (StringUtils.isNotBlank(btnTxt)) {
textCard.setBtntxt(btnTxt);
}
textCardMessage.setTextcard(textCard).setAppType(appType).setTouser(toUser);
//TODO 发送企业微信文本卡片消息
sendWxMsgService.sendTextCardMsg(textCardMessage);
consumeMsg = "企业微信消息发送成功,接收用户【" + userEname + "-" + msgtype + "】!";
//TODO 保存文本卡片消息队列消费的消息信息
sysActiveMqService.saveByTextMessage(this.getClass().getName(), msg, JmsConstant.JMS_TYPE_QUEUE.getCode(), JmsConstant.CONSUME_SUCCESS.getCode(), consumeMsg);
}
// 文本消息
if (Objects.equals(msgtype, WxMsgTypeConstant.FILE.getCode())) {
/* 构建审批回调url */
String appType = jsonObject.getString("appType");
String approveType = jsonObject.getString("approveType");
String companyCode = jsonObject.getString("userCompanyCode");
String taskId = jsonObject.getString("taskId");
String processInstanceId = jsonObject.getString("processInstanceId");
String businessId = jsonObject.getString("businessId");
Map<String, Object> substituteMap = new HashMap<>(5);
substituteMap.put("appType", appType.toLowerCase(Locale.ROOT));
substituteMap.put("approveType", approveType);
substituteMap.put("companyCode", companyCode);
substituteMap.put("taskId", taskId);
substituteMap.put("processInstanceId", processInstanceId);
substituteMap.put("businessId", businessId);
String redirectUrl = StringSolveUtils.placeholderReplace(AuditConstants.SunepsAuditType.REDIRECT_URL.getCode(), substituteMap);
/* 构建文本消息 */
String title = jsonObject.getString("title");
String taskName = jsonObject.getString("taskName");
String datetime = jsonObject.getString("datetime");
String btnTxt = jsonObject.getString("btntxt");
substituteMap.put("subTitle", datetime + "——" + userName);
substituteMap.put("description", desc);
substituteMap.put("taskName", taskName);
String description = StringSolveUtils.placeholderReplace(AuditConstants.SunepsAuditType.AUDIT_DESCRIPTION_FORMAT.getCode(), substituteMap);
TextCardMessage textCardMessage = new TextCardMessage();
TextCard textCard = new TextCard();
textCard.setTitle(title)
.setDescription(description)
.setUrl(redirectUrl);
if (StringUtils.isNotBlank(btnTxt)) {
textCard.setBtntxt(btnTxt);
}
textCardMessage.setTextcard(textCard).setAppType(appType).setTouser(toUser);
//TODO 送企业微信文本卡片消息
sendWxMsgService.sendTextCardMsg(textCardMessage);
consumeMsg = "企业微信消息发送成功,接收用户【" + userEname + "-" + msgtype + "】!";
//TODO 存文本卡片消息队列消费的消息信息
sysActiveMqService.saveByTextMessage(this.getClass().getName(), msg, JmsConstant.JMS_TYPE_QUEUE.getCode(), JmsConstant.CONSUME_SUCCESS.getCode(), consumeMsg);
}
}
} catch (Exception e) {
log.error("消息发送失败!{}", e.getMessage(), e);
try {
// 重试时不重复保存队列消息
if (reconsumeTimes>0) {
sysActiveMqService.saveByTextMessage(this.getClass().getName(), msg, JmsConstant.JMS_TYPE_QUEUE.getCode(), JmsConstant.CONSUME_FAILURE.getCode(), e.getMessage());
}
} catch (Exception jmsException) {
log.error("消息保存失败!{}", jmsException.getMessage(), jmsException);
}
}
}
}

View File

@@ -0,0 +1,43 @@
package com.zt.plat.module.system.mq.message.msg;
import com.zt.plat.framework.common.core.KeyValue;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* 消息发送消息
*
* @author ZT
*/
@Data
public class MsgSendMessage {
public static final String TOPIC = "SMS_SEND_TOPIC";
/**
* 消息日志编号
*/
@NotNull(message = "短信日志编号不能为空")
private Long logId;
/**
* 消息内容(已按模板格式化后的文本)
*/
private String content;
/**
* 消息渠道编号
*/
@NotNull(message = "短信渠道编号不能为空")
private Long channelId;
/**
* 消息 API 的模板编号
*/
@NotNull(message = "消息 API 的模板编号不能为空")
private String apiTemplateId;
/**
* 短信模板参数
*/
private List<KeyValue<String, Object>> templateParams;
}

View File

@@ -0,0 +1,38 @@
package com.zt.plat.module.system.mq.producer.msg;
import com.zt.plat.framework.common.core.KeyValue;
import com.zt.plat.module.system.mq.message.sms.SmsSendMessage;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import java.util.List;
/**
* Sms 短信相关消息的 Producer
*
* @author zzf
* @since 2021/3/9 16:35
*/
@Slf4j
@Component
public class MsgProducer {
@Resource
private RocketMQTemplate rocketMQTemplate;
/**
* 发送 {@link SmsSendMessage} 消息
* @param logId 短信日志编号
* @param channelId 渠道编号
* @param apiTemplateId 短信模板编号
* @param templateParams 短信模板参数
*/
public void sendMsg(Long logId, String content,
Long channelId, String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
SmsSendMessage message = new SmsSendMessage().setLogId(logId).setContent(content);
message.setChannelId(channelId).setApiTemplateId(apiTemplateId).setTemplateParams(templateParams);
rocketMQTemplate.syncSend(SmsSendMessage.TOPIC, message);
}
}

View File

@@ -1,26 +1,37 @@
package com.zt.plat.module.system.service.dept;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.annotations.VisibleForTesting;
import com.zt.plat.framework.common.enums.CommonStatusEnum;
import com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.module.system.api.dept.dto.DeptMsgRespDTO;
import com.zt.plat.module.system.api.dept.dto.DeptSaveReqDTO;
import com.zt.plat.module.system.api.esp.dto.EspDto;
import com.zt.plat.module.system.controller.admin.dept.vo.depexternalcode.EspPageReqVO;
import com.zt.plat.module.system.controller.admin.dept.vo.depexternalcode.EspSaveRespVo;
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptPushMsgDO;
import com.zt.plat.module.system.dal.mysql.dept.DeptMapper;
import com.zt.plat.module.system.dal.mysql.dept.EspMapper;
import com.zt.plat.module.system.dal.redis.RedisKeyConstants;
import com.zt.plat.module.system.enums.dept.DeptSourceEnum;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.seata.common.result.Result;
import org.apache.seata.spring.annotation.GlobalTransactional;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -33,13 +44,31 @@ import static com.zt.plat.module.system.enums.ErrorCodeConstants.*;
@Validated
public class EspServiceImpl implements IEspService {
@Resource
private DeptExternalCodeService deptExternalCodeService;
@Resource
private EspMapper espMapper;
@Resource
private DeptMapper deptMapper;
@Resource
private CacheManager cacheManager;
@Resource
private com.zt.plat.module.system.mq.producer.databus.DatabusChangeProducer databusChangeProducer;
private static final String ROOT_CODE_PREFIX = "ZT";
private static final String EXTERNAL_CODE_PREFIX = "CU";
private static final int CODE_SEGMENT_LENGTH = 3;
private static final int MAX_SEQUENCE = 999;
private static final int BATCH_SIZE = 1000;
private static final Comparator<DeptDO> DEPT_COMPARATOR = Comparator
.comparing(DeptDO::getSort, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(DeptDO::getId, Comparator.nullsLast(Comparator.naturalOrder()));
@Override
@CacheEvict(cacheNames = RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST, key = "#createReqVO.deptId", beforeInvocation = false)
public Long createDeptPushMsg(EspSaveRespVo createReqVO) {
@@ -103,12 +132,243 @@ public class EspServiceImpl implements IEspService {
return espMapper.selectListByDeptId(deptId);
}
@Override
public List<EspDto> pushMsg(DeptSaveReqDTO syncReqDTO) {
return BeanUtils.toBean(espMapper.selectpushMsg(syncReqDTO), EspDto.class);
@Override
@GlobalTransactional(rollbackFor = Exception.class)
@Transactional(rollbackFor = Exception.class)
@CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST,
allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存
public Long createDept(DeptSaveReqVO createReqVO) {
// 允许上级组织为空,视为顶级组织
createReqVO.setParentId(normalizeParentId(createReqVO.getParentId()));
// 创建时默认有效
createReqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
// 默认部门来源:未指定时视为外部部门
if (createReqVO.getDeptSource() == null) {
createReqVO.setDeptSource(DeptSourceEnum.EXTERNAL.getSource());
}
// 校验父部门的有效性
validateParentDept(null, createReqVO.getParentId());
// 校验部门名的唯一性
validateDeptNameUnique(null, createReqVO.getParentId(), createReqVO.getName());
// 生成并校验部门编码所有来源统一走生成逻辑iWork 不再豁免)
if (Boolean.TRUE.equals(createReqVO.getDelayCodeGeneration())) {
createReqVO.setCode(null);
} else {
String resolvedCode = generateDeptCode(createReqVO.getParentId(), createReqVO.getDeptSource());
validateDeptCodeUnique(null, resolvedCode);
createReqVO.setCode(resolvedCode);
}
// 插入部门
DeptDO dept = BeanUtils.toBean(createReqVO, DeptDO.class);
// 设置部门来源(前置已默认化,此处兜底)
if (dept.getDeptSource() == null) {
dept.setDeptSource(DeptSourceEnum.EXTERNAL.getSource());
}
int insert = deptMapper.insert(dept);
if (insert == 0) {
throw exception(USER_DEPT_SAVE_EXISTS);
}
// 外部编码映射
upsertExternalMappingIfPresent(dept.getId(), createReqVO);
// 发布部门创建事件
databusChangeProducer.sendDeptCreatedMessage(dept);
//推送消息
DeptSaveReqDTO deptSaveReqDTO = new DeptSaveReqDTO();
BeanUtils.copyProperties(dept,deptSaveReqDTO);
return dept.getId();
}
@Override
public CommonResult<List<DeptMsgRespDTO>> selectDepMsg(DeptSaveReqDTO syncReqDTO) {
if (syncReqDTO == null) {
return CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST);
}
LambdaQueryWrapper<DeptPushMsgDO> wrapper = new LambdaQueryWrapper<>();
if(syncReqDTO.getId() != null){
wrapper.eq(DeptPushMsgDO::getId, syncReqDTO.getId());
}
List<DeptPushMsgDO> list = espMapper.selectList(wrapper);
if (CollectionUtils.isEmpty(list)){
return CommonResult.error(GlobalErrorCodeConstants.NOT_FOUND);
}
ArrayList<DeptMsgRespDTO> lists = new ArrayList<>(list.size());
for (DeptPushMsgDO deptPushMsgDO : list){
DeptMsgRespDTO deptMsgRespDTO = new DeptMsgRespDTO();
BeanUtils.copyProperties(deptPushMsgDO, deptMsgRespDTO);
lists.add(deptMsgRespDTO);
}
return CommonResult.success(lists);
}
private Long normalizeParentId(Long parentId) {
return parentId == null ? DeptDO.PARENT_ID_ROOT : parentId;
}
@VisibleForTesting
void validateParentDept(Long id, Long parentId) {
if (parentId == null || DeptDO.PARENT_ID_ROOT.equals(parentId)) {
return;
}
// 1. 不能设置自己为父部门
if (Objects.equals(id, parentId)) {
throw exception(DEPT_PARENT_ERROR);
}
// 2. 父部门不存在
DeptDO parentDept = deptMapper.selectById(parentId);
if (parentDept == null) {
return;
}
// 3. 递归校验父部门,如果父部门是自己的子部门,则报错,避免形成环路
if (id == null) { // id 为空,说明新增,不需要考虑环路
return;
}
for (int i = 0; i < Short.MAX_VALUE; i++) {
// 3.1 校验环路
parentId = parentDept.getParentId();
if (Objects.equals(id, parentId)) {
throw exception(DEPT_PARENT_IS_CHILD);
}
// 3.2 继续递归下一级父部门
if (parentId == null || DeptDO.PARENT_ID_ROOT.equals(parentId)) {
break;
}
parentDept = deptMapper.selectById(parentId);
if (parentDept == null) {
break;
}
}
}
private String generateDeptCode(Long parentId, Integer deptSource) {
Long effectiveParentId = normalizeParentId(parentId);
String prefix = resolveCodePrefix(effectiveParentId, deptSource);
int nextSequence = determineNextSequence(effectiveParentId, prefix);
assertSequenceRange(nextSequence);
return prefix + formatSequence(nextSequence);
}
@VisibleForTesting
void validateDeptNameUnique(Long id, Long parentId, String name) {
Long effectiveParentId = normalizeParentId(parentId);
if (Objects.equals(effectiveParentId, DeptDO.PARENT_ID_ROOT)) {
return;
}
DeptDO dept = deptMapper.selectByParentIdAndName(effectiveParentId, name);
if (dept == null) {
return;
}
// 如果 id 为空,说明不用比较是否为相同 id 的部门
if (id == null) {
throw exception(DEPT_NAME_DUPLICATE);
}
if (ObjectUtil.notEqual(dept.getId(), id)) {
throw exception(DEPT_NAME_DUPLICATE);
}
}
@VisibleForTesting
void validateDeptCodeUnique(Long id, String code) {
if (StrUtil.isBlank(code)) {
return;
}
DeptDO dept = deptMapper.selectByCode(code);
if (dept == null) {
return;
}
// 如果 id 为空,说明不用比较是否为相同 id 的部门
if (id == null) {
throw exception(DEPT_CODE_DUPLICATE);
}
if (ObjectUtil.notEqual(dept.getId(), id)) {
throw exception(DEPT_CODE_DUPLICATE);
}
}
private void upsertExternalMappingIfPresent(Long deptId, DeptSaveReqVO reqVO) {
String systemCode = StrUtil.trimToNull(reqVO.getExternalSystemCode());
String externalCode = StrUtil.trimToNull(reqVO.getExternalDeptCode());
if (StrUtil.hasEmpty(systemCode, externalCode) || deptId == null) {
return;
}
String externalName = StrUtil.trimToNull(reqVO.getExternalDeptName());
deptExternalCodeService.saveOrUpdateDeptExternalCode(deptId, systemCode, externalCode, externalName, reqVO.getStatus());
}
private String resolveCodePrefix(Long parentId, Integer deptSource) {
boolean isExternal = Objects.equals(deptSource, DeptSourceEnum.EXTERNAL.getSource());
if (DeptDO.PARENT_ID_ROOT.equals(parentId)) {
return isExternal ? EXTERNAL_CODE_PREFIX : ROOT_CODE_PREFIX;
}
DeptDO parentDept = deptMapper.selectById(parentId);
if (parentDept == null || StrUtil.isBlank(parentDept.getCode())) {
return isExternal ? EXTERNAL_CODE_PREFIX : ROOT_CODE_PREFIX;
}
String parentCode = parentDept.getCode();
if (isExternal) {
if (parentCode.startsWith(EXTERNAL_CODE_PREFIX)) {
return parentCode;
}
if (parentCode.startsWith(ROOT_CODE_PREFIX)) {
return EXTERNAL_CODE_PREFIX + parentCode.substring(ROOT_CODE_PREFIX.length());
}
return EXTERNAL_CODE_PREFIX;
}
return parentCode;
}
private int determineNextSequence(Long parentId, String prefix) {
DeptDO lastChild = deptMapper.selectLastChildByCode(parentId, prefix);
Integer sequence = parseSequence(lastChild != null ? lastChild.getCode() : null, prefix);
if (sequence != null) {
return sequence + 1;
}
return deptMapper.selectListByParentId(parentId, null).stream()
.map(DeptDO::getCode)
.map(code -> parseSequence(code, prefix))
.filter(Objects::nonNull)
.max(Integer::compareTo)
.map(val -> val + 1)
.orElse(1);
}
private void assertSequenceRange(int sequence) {
if (sequence > MAX_SEQUENCE) {
throw exception(DEPT_CODE_OUT_OF_RANGE);
}
}
private String formatSequence(int sequence) {
return StrUtil.padPre(String.valueOf(sequence), CODE_SEGMENT_LENGTH, '0');
}
private Integer parseSequence(String code, String prefix) {
if (StrUtil.isBlank(code) || StrUtil.isBlank(prefix) || !code.startsWith(prefix)) {
return null;
}
String suffix = code.substring(prefix.length());
if (suffix.length() != CODE_SEGMENT_LENGTH || !StrUtil.isNumeric(suffix)) {
return null;
}
return Integer.parseInt(suffix);
}
private DeptPushMsgDO validateExists(Long id) {
if (id == null) {
throw exception(DEPT_EXTERNAL_RELATION_NOT_EXISTS);

View File

@@ -1,13 +1,14 @@
package com.zt.plat.module.system.service.dept;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.system.api.dept.dto.DeptMsgRespDTO;
import com.zt.plat.module.system.api.dept.dto.DeptSaveReqDTO;
import com.zt.plat.module.system.api.esp.dto.EspDto;
import com.zt.plat.module.system.controller.admin.dept.vo.depexternalcode.EspPageReqVO;
import com.zt.plat.module.system.controller.admin.dept.vo.depexternalcode.EspSaveRespVo;
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptPushMsgDO;
import java.util.List;
/**
* 部门推送消息 Service 接口
*/
@@ -47,11 +48,18 @@ public interface IEspService {
*/
List<DeptPushMsgDO> getPushMsgByDeptId(Long deptId);
/**
* 创建部门
* @param reqVO 部门信息
* @return 部门编号
*/
Long createDept(DeptSaveReqVO reqVO);
/**
* 推送部门数据到外部系统
* @param syncReqDTO 同步请求
*/
List<EspDto> pushMsg(DeptSaveReqDTO syncReqDTO);
CommonResult<List<DeptMsgRespDTO>> selectDepMsg(DeptSaveReqDTO syncReqDTO);
}

View File

@@ -0,0 +1,68 @@
package com.zt.plat.module.system.service.msg;
import com.zt.plat.module.system.mq.message.sms.SmsSendMessage;
import java.util.List;
import java.util.Map;
/**
* 监听消息 Service 接口
* @author ZT
*/
public interface IMonitorMsgService {
/**
* 发送单条短信给管理后台的用户
* 在 mobile 为空时,使用 userId 加载对应管理员的手机号
* @param mobile 手机号
* @param userId 用户编号
* @param templateCode 短信模板编号
* @param templateParams 短信模板参数
* @return 发送日志编号
*/
Long sendSingleSmsToAdmin(String mobile, Long userId,
String templateCode, Map<String, Object> templateParams);
/**
* 发送单条短信给用户 APP 的用户
* 在 mobile 为空时,使用 userId 加载对应会员的手机号
* @param mobile 手机号
* @param userId 用户编号
* @param templateCode 短信模板编号
* @param templateParams 短信模板参数
* @return 发送日志编号
*/
Long sendSingleSmsToMember(String mobile, Long userId,
String templateCode, Map<String, Object> templateParams);
/**
* 发送单条短信给用户
* @param mobile 手机号
* @param userId 用户编号
* @param userType 用户类型
* @param templateCode 短信模板编号
* @param templateParams 短信模板参数
* @return 发送日志编号
*/
Long sendSingleSms(String mobile, Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams);
default void sendBatchSms(List<String> mobiles, List<Long> userIds, Integer userType,
String templateCode, Map<String, Object> templateParams) {
throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!");
}
/**
* 执行真正的短信发送
* 注意,该方法仅仅提供给 MQ Consumer 使用
* @param message 短信
*/
void doSendSms(SmsSendMessage message);
/**
* 接收短信的接收结果
* @param channelCode 渠道编码
* @param text 结果内容
* @throws Throwable 处理失败时,抛出异常
*/
void receiveSmsStatus(String channelCode, String text) throws Throwable;
}

View File

@@ -0,0 +1,88 @@
package com.zt.plat.module.system.service.msg;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO;
import com.zt.plat.module.system.controller.admin.sms.vo.msg.TextMessage;
import com.zt.plat.module.system.dal.dataobject.sms.SmsLogDO;
import com.zt.plat.module.system.dal.dataobject.sms.SmsTemplateDO;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 消息日志 Service 接口
*
* @author zzf
*/
public interface IMsgLogService {
/**
* 创建短信日志
*
* @param userId 用户编号
* @param userType 用户类型
* @param isSend 是否发送
* @param template 短信模板
* @param templateContent 短信内容
* @param templateParams 短信参数
* @return 发送日志编号
*/
Long createSmsLog(Long userId, Integer userType, Boolean isSend,
SmsTemplateDO template, String templateContent, Map<String, Object> templateParams);
/**
* 更新日志的发送结果
*
* @param id 日志编号
* @param success 发送是否成功
* @param apiSendCode 短信 API 发送结果的编码
* @param apiSendMsg 短信 API 发送失败的提示
* @param apiRequestId 短信 API 发送返回的唯一请求 ID
* @param apiSerialNo 短信 API 发送返回的序号
*/
void updateSmsSendResult(Long id, Boolean success,
String apiSendCode, String apiSendMsg,
String apiRequestId, String apiSerialNo);
/**
* 更新日志的接收结果
*
* @param id 日志编号
* @param success 是否接收成功
* @param receiveTime 用户接收时间
* @param apiReceiveCode API 接收结果的编码
* @param apiReceiveMsg API 接收结果的说明
*/
void updateSmsReceiveResult(Long id, Boolean success,
LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg);
/**
* 获得短信日志分页
*
* @param pageReqVO 分页查询
* @return 短信日志分页
*/
PageResult<SmsLogDO> getSmsLogPage(SmsLogPageReqVO pageReqVO);
/**
* 根据日志编号查询短信日志
*
* @param id 日志编号
* @return 短信日志
*/
SmsLogDO getSmsLog(Long id);
/**
* 创建消息日志
*
* @param userId 用户编号
* @param userType 用户类型
* @param isSend 是否发送
* @param template 短信模板
* @param textMessage 短信内容
* @param templateParams 短信参数
* @return 创建消息日志编号
*/
Long createMsgLog(Long userId, Integer userType, Boolean isSend, SmsTemplateDO template, TextMessage textMessage, Map<String, Object> templateParams);
}

View File

@@ -0,0 +1,125 @@
package com.zt.plat.module.system.service.msg;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.system.controller.admin.sms.vo.msg.*;
import com.zt.plat.module.system.controller.admin.sms.vo.template.MsgTemplateSendReqVO;
import java.util.Map;
/**
* 生产者发送消息接口类
*
* @author dongqiang.hao
*/
public interface ISendMsgService {
/**
* 发送文本消息
*
* @param textMessage 文本消息包装对象
* @return r 响应对象
*/
CommonResult<Object> sendTextMsg(MsgTemplateSendReqVO sendReqVO, TextMessage textMessage) throws Exception;
/**
* 发送企业微信图片消息
*
* @param imageMessage 图片消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendImageMsg(Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams,ImageMessage imageMessage)throws Exception;
/**
* 发送企业微信语音消息
*
* @param voiceMessage 语音消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendVoiceMsg(Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams,VoiceMessage voiceMessage)throws Exception;
/**
* 发送企业微信视频消息
*
* @param videoMessage 视频消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendVideoMsg(Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams,VideoMessage videoMessage)throws Exception;
/**
* 发送企业微信文件消息
*
* @param fileMessage 文件消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendFileMsg(Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams,FileMessage fileMessage)throws Exception;
/**
* 发送企业微信文本卡片消息
*
* @param textCardMessage 文本卡片消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendTextCardMsgPich01(Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams,TextCardMessage textCardMessage) throws Exception;
/**
* 发送企业微信文本卡片消息
*
* @param textCardMessage 文本卡片消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendTextCardMsg(Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams,TextCardMessage textCardMessage)throws Exception;
/**
* 发送企业微信图文消息
*
* @param newsMessage 图文消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendNewsMsg(Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams,NewsMessage newsMessage)throws Exception;
/**
* 发送企业微信图文消息mpnews
*
* @param mpNewsMessage 图文消息mpnews对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendMpNewsMsg(Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams,MpNewsMessage mpNewsMessage)throws Exception;
/**
* 发送企业微信markdown消息
*
* @param markdownMessage markdown消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendMarkdownMsg(Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams,MarkdownMessage markdownMessage) throws Exception;
/**
* 发送企业微信小程序通知消息
*
* @param miniProgramNoticeMessage 小程序通知消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendMiniProgramNoticeMsg(Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams,MiniProgramNoticeMessage miniProgramNoticeMessage)throws Exception;
/**
* 发送企业微信任务卡片消息
*
* @param interactiveTaskCardMessage 任务卡片消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendInteractiveTaskCardMsg(Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams,InteractiveTaskCardMessage interactiveTaskCardMessage)throws Exception;
}

View File

@@ -0,0 +1,108 @@
package com.zt.plat.module.system.service.msg;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.system.controller.admin.sms.vo.msg.*;
/**
* TODO ============================== 企微消息发送接口类 =========================================
*
* @author luzemin
*/
public interface ISendWxMsgService {
/**
* 发送企业微信文本消息
*
* @param textMessage 文本消息包装对象
* @return r 响应对象
*/
CommonResult<Object> sendTextMsg(TextMessage textMessage)throws Exception;
/**
* 发送企业微信图片消息
*
* @param imageMessage 图片消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendImageMsg(ImageMessage imageMessage)throws Exception;
/**
* 发送企业微信语音消息
*
* @param voiceMessage 语音消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendVoiceMsg(VoiceMessage voiceMessage)throws Exception;
/**
* 发送企业微信视频消息
*
* @param videoMessage 视频消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendVideoMsg(VideoMessage videoMessage)throws Exception;
/**
* 发送企业微信文件消息
*
* @param fileMessage 文件消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendFileMsg(FileMessage fileMessage)throws Exception;
/**
* 发送企业微信文本卡片消息
*
* @param textCardMessage 文本卡片消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendTextCardMsg(TextCardMessage textCardMessage)throws Exception;
/**
* 发送企业微信文本卡片消息 -物资存货智能管理 预警信息
*
* @param textCardMessage 文本卡片消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendTextCardMsgPich01(TextCardMessage textCardMessage)throws Exception;
/**
* 发送企业微信图文消息
*
* @param newsMessage 图文消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendNewsMsg(NewsMessage newsMessage)throws Exception;
/**
* 发送企业微信图文消息mpnews
*
* @param mpNewsMessage 图文消息mpnews对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendMpNewsMsg(MpNewsMessage mpNewsMessage)throws Exception;
/**
* 发送企业微信markdown消息
*
* @param markdownMessage markdown消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendMarkdownMsg(MarkdownMessage markdownMessage)throws Exception;
/**
* 发送企业微信小程序通知消息
*
* @param miniProgramNoticeMessage 小程序通知消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendMiniProgramNoticeMsg(MiniProgramNoticeMessage miniProgramNoticeMessage) throws Exception;
/**
* 发送企业微信任务卡片消息
*
* @param interactiveTaskCardMessage 任务卡片消息对象
* @return R 消息发送响应对象
*/
CommonResult<Object> sendInteractiveTaskCardMsg(InteractiveTaskCardMessage interactiveTaskCardMessage) throws Exception;
}

View File

@@ -0,0 +1,48 @@
package com.zt.plat.module.system.service.msg;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zt.plat.module.system.dal.dataobject.msg.SysActiveMqLog;
import org.apache.rocketmq.common.message.MessageExt;
/**
* RocketMq消息队列 服务类
* @author dongqiang.hao
* @since 2025-01-07
*/
public interface ISysRocketMqService extends IService<SysActiveMqLog> {
/**
* 保存消息队列消费的消息信息
*
* @param listenerClassName 队列监听类名
* @param tm 消息对象
* @param jmsType 消息类型(QUEUE-队列/TOPIC-主题)
* @throws Exception 消息消费异常
*/
void saveByTextMessage(String listenerClassName, MessageExt tm, String jmsType) throws Exception;
/**
* 保存文本卡片消息队列消费的消息信息
*
* @param listenerClassName 队列监听类名
* @param tm 消息对象
* @param jmsType 消息类型(QUEUE-队列/TOPIC-主题)
* @param consumeFlag 消费结果(success-成功/failure-失败)
* @param consumeMessage 消费失败消息
* @throws Exception 消息消费异常
*/
void saveByTextMessage(String listenerClassName, MessageExt tm, String jmsType, String consumeFlag, String consumeMessage) throws Exception;
/**
* 保存阳光采购消息队列消费的消息信息
*
* @param listenerClassName 队列监听类名
* @param tm 消息对象
* @param jmsType 消息类型(QUEUE-队列/TOPIC-主题)
* @param consumeFlag 消费结果(success-成功/failure-失败)
* @param consumeMessage 消费失败消息
* @throws Exception 消息消费异常
*/
void saveSunepsByTextMessage(String listenerClassName, MessageExt tm, String jmsType, String consumeFlag, String consumeMessage) throws Exception;
}

View File

@@ -0,0 +1,14 @@
package com.zt.plat.module.system.service.msg;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zt.plat.module.system.dal.dataobject.msg.SysWxAuditConfig;
/**
* 企业微信审核配置表 服务类
*
* @author Dy
* @since 2021-08-26
*/
public interface SysWxAuditConfigService extends IService<SysWxAuditConfig> {
}

View File

@@ -0,0 +1,296 @@
package com.zt.plat.module.system.service.msg.config;
import jakarta.annotation.PostConstruct;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 系统参数配置组件
* TODO
* @author luzemin
*/
@Component
@PropertySource("classpath:config.properties")
public class HttpUrlConfig {
/**
* 企业微信审核涉及的app
*/
public static final String[] APP_TYPES = new String[]{"suneps", "pscs", "market", "bssim", "dzxs", "boli", "lmps"};
/**
* 企业微信审核http请求url——提交审核
*/
public static final String COMMIT_URL = "commitUrl";
/**
* 企业微信审核http请求url——获取审核历史信息
*/
public static final String COMMENT_URL = "commentUrl";
/**
* 企业微信审核http请求url——获取用户待办
*/
public static final String TODO_URL = "todoUrl";
/**
* 企业微信审核http请求url——获取用户已办
*/
public static final String DONE_URL = "doneUrl";
/**
* 企业微信审核http请求url——获取流程是否结束信息
*/
public static final String FINISH_URL = "finishUrl";
/**
* 企业微信审核http请求url——文件存储
*/
public static final String STORAGE_URL = "storageUrl";
/**
* 获取合同文本内容请求url
*/
public static final String RENDERBPOCONTENT_URL = "renderBpoContentUrl";
/**
* 获字化采购报表地址
*/
private static final String FINE_REPORT_URL = "fineReportUrl";
/**
* 获数字化附件管理地址
*/
private static final String FILE_STORAGE_URL = "fileStorageUrl";
/**
* 获数字化附件预览
*/
private static final String FILE_VIEW_URL = "fileViewUrl";
/**
* 应用http调用地址
*/
private static final String HTTP_CONTEXT_PATH = "httpContextPath";
/**
* 微信审核应用地址
*/
private static final String WX_CONTEXT_PATH = "wxContextPath";
/**
* 微信审核应用地址
*/
private static final String WX_SUBMIT_DATA = "wxSubmitData";
/**
* 企业微信审核http请求url——提交审核应用和url映射集合
*/
private static final Map<String, String> COMMIT_URL_MAPPING = new HashMap<>();
/**
* 企业微信审核http请求url——获取审核历史信息应用和url映射集合
*/
private static final Map<String, String> COMMENT_URL_MAPPING = new HashMap<>();
/**
* 企业微信审核http请求url——获取用户待办应用和url映射集合
*/
private static final Map<String, String> TODO_URL_MAPPING = new HashMap<>();
/**
* 企业微信审核http请求url——获取用户已办应用和url映射集合
*/
private static final Map<String, String> DONE_URL_MAPPING = new HashMap<>();
/**
* 企业微信审核http请求url——文件存储应用和url映射集合
*/
private static final Map<String, String> STORAGE_URL_MAPPING = new HashMap<>();
/**
* 企业微信审核http请求url——文件存储应用和url映射集合
*/
private static final Map<String, String> FINISH_URL_MAPPING = new HashMap<>();
/**
* 获取合同文本内容请求url
*/
private static final Map<String, String> RENDERBPOCONTENT_URL_MAPPING = new HashMap<>();
/**
* 获取合同文本内容请求url
*/
private static final Map<String, String> FINE_REPORT_URL_MAPPING = new HashMap<>();
/**
* 获取合同文本内容请求url
*/
private static final Map<String, String> FILE_VIEW_URL_MAPPING = new HashMap<>();
/**
* 获取合同文本内容请求url
*/
private static final Map<String, String> FILE_STORAGE_URL_MAPPING = new HashMap<>();
/**
* 获取应用http调用地址
*/
private static final Map<String, String> HTTP_CONTEXT_PATH_MAPPING = new HashMap<>();
/**
* 微信审核应用地址
*/
private static final Map<String, String> WX_CONTEXT_PATH_MAPPING = new HashMap<>();
/**
* 微信审核应用地址
*/
private static final Map<String, String> WX_SUBMIT_DATA_MAPPING = new HashMap<>();
@PostConstruct
public void initConfig() throws IOException {
Properties properties = new Properties();
InputStream configInputStream = this.getClass().getClassLoader().getResourceAsStream("config.properties");
properties.load(configInputStream);
Arrays.stream(APP_TYPES).forEach(appType -> {
COMMIT_URL_MAPPING.put(appType.concat(".").concat(COMMIT_URL), properties.getProperty(appType.concat(".").concat(COMMIT_URL)));
COMMENT_URL_MAPPING.put(appType.concat(".").concat(COMMENT_URL), properties.getProperty(appType.concat(".").concat(COMMENT_URL)));
TODO_URL_MAPPING.put(appType.concat(".").concat(TODO_URL), properties.getProperty(appType.concat(".").concat(TODO_URL)));
DONE_URL_MAPPING.put(appType.concat(".").concat(DONE_URL), properties.getProperty(appType.concat(".").concat(DONE_URL)));
STORAGE_URL_MAPPING.put(appType.concat(".").concat(STORAGE_URL), properties.getProperty(appType.concat(".").concat(STORAGE_URL)));
FINISH_URL_MAPPING.put(appType.concat(".").concat(FINISH_URL), properties.getProperty(appType.concat(".").concat(FINISH_URL)));
RENDERBPOCONTENT_URL_MAPPING.put(appType.concat(".").concat(RENDERBPOCONTENT_URL), properties.getProperty(appType.concat(".").concat(RENDERBPOCONTENT_URL)));
FINE_REPORT_URL_MAPPING.put(appType.concat(".").concat(FINE_REPORT_URL), properties.getProperty(appType.concat(".").concat(FINE_REPORT_URL)));
FILE_STORAGE_URL_MAPPING.put(appType.concat(".").concat(FILE_STORAGE_URL), properties.getProperty(appType.concat(".").concat(FILE_STORAGE_URL)));
FILE_VIEW_URL_MAPPING.put(appType.concat(".").concat(FILE_VIEW_URL), properties.getProperty(appType.concat(".").concat(FILE_VIEW_URL)));
HTTP_CONTEXT_PATH_MAPPING.put(appType.concat(".").concat(HTTP_CONTEXT_PATH), properties.getProperty(appType.concat(".").concat(HTTP_CONTEXT_PATH)));
WX_CONTEXT_PATH_MAPPING.put(appType.concat(".").concat(WX_CONTEXT_PATH), properties.getProperty(appType.concat(".").concat(WX_CONTEXT_PATH)));
WX_SUBMIT_DATA_MAPPING.put(appType.concat(".").concat(WX_SUBMIT_DATA), properties.getProperty(appType.concat(".").concat(WX_SUBMIT_DATA)));
});
}
/**
* 企业微信审核http请求url——提交审核
*
* @param appType 应用类型
* @return 对应应用提交审核请求url
*/
public String getCommitUrl(String appType) {
return COMMIT_URL_MAPPING.get(appType.concat(".").concat(COMMIT_URL));
}
/**
* 企业微信审核http请求url——获取审核历史信息
*
* @param appType 应用类型
* @return 对应应用获取审核历史信息请求url
*/
public String getCommentUrl(String appType) {
return COMMENT_URL_MAPPING.get(appType.concat(".").concat(COMMENT_URL));
}
/**
* 企业微信审核http请求url——获取用户待办
*
* @param appType 应用类型
* @return 对应应用获取用户待办请求url
*/
public String getTodoUrl(String appType) {
return TODO_URL_MAPPING.get(appType.concat(".").concat(TODO_URL));
}
/**
* 企业微信审核http请求url——获取用户已办
*
* @param appType 应用类型
* @return 对应应用获取用户已办请求url
*/
public String getDoneUrl(String appType) {
return DONE_URL_MAPPING.get(appType.concat(".").concat(DONE_URL));
}
/**
* 企业微信审核http请求url——文件存储
*
* @param appType 应用类型
* @return 对应应用文件存储请求url
*/
public String getStorageUrl(String appType) {
return STORAGE_URL_MAPPING.get(appType.concat(".").concat(STORAGE_URL));
}
/**
* 企业微信审核http请求url——获取流程是否结束信息
*
* @param appType 应用类型
* @return 对应应用获取审核历史信息请求url
*/
public String getFinishUrl(String appType) {
return FINISH_URL_MAPPING.get(appType.concat(".").concat(FINISH_URL));
}
/**
* 获取合同文本内容
*
* @param appType 应用类型
* @return 获取合同文本内容请求url
*/
public String getRenderBpoContentUrl(String appType) {
return RENDERBPOCONTENT_URL_MAPPING.get(appType.concat(".").concat(RENDERBPOCONTENT_URL));
}
/**
* 数字化采购报表地址
*
* @param appType 应用类型
* @return 获取请求url
*/
public String getFineReportUrl(String appType) {
return FINE_REPORT_URL_MAPPING.get(appType.concat(".").concat(FINE_REPORT_URL));
}
/**
* 字化附件管理地址
*
* @param appType 应用类型
* @return 获取合同文本内容请求url
*/
public String getFileStorageUrl(String appType) {
return FILE_STORAGE_URL_MAPPING.get(appType.concat(".").concat(FILE_STORAGE_URL));
}
/**
* 字化附件预览
*
* @param appType 应用类型
* @return 获取合同文本内容请求url
*/
public String getFileViewUrl(String appType) {
return FILE_VIEW_URL_MAPPING.get(appType.concat(".").concat(FILE_VIEW_URL));
}
/**
* 应用http调用地址
*
* @param appType 应用类型
* @return 获取合同文本内容请求url
*/
public String getHttpContextPath(String appType) {
return HTTP_CONTEXT_PATH_MAPPING.get(appType.concat(".").concat(HTTP_CONTEXT_PATH));
}
/**
* 应用http调用地址
*
* @param appType 应用类型
* @return 获取合同文本内容请求url
*/
public String getWxContextPath(String appType) {
return WX_CONTEXT_PATH_MAPPING.get(appType.concat(".").concat(WX_CONTEXT_PATH));
}
/**
* 应用http调用地址
*
* @param appType 应用类型
* @return 获取合同文本内容请求url
*/
public String getWxSubmitData(String appType) {
return WX_SUBMIT_DATA_MAPPING.get(appType.concat(".").concat(WX_SUBMIT_DATA));
}
}

View File

@@ -0,0 +1,376 @@
package com.zt.plat.module.system.service.msg.config;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.zt.plat.framework.common.util.http.HttpClientUtils;
import com.zt.plat.module.system.api.sms.dto.code.User;
import com.zt.plat.module.system.dal.dataobject.msg.SysWxAuditConfig;
import com.zt.plat.module.system.framework.sms.core.enums.AuditConstants;
import com.zt.plat.module.system.service.msg.SysWxAuditConfigService;
import jakarta.annotation.Resource;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Component
@PropertySource("classpath:qywx.properties")
public class WeiXinProperties {
@Resource
private RedisTemplate<String, String> redisTemplate;
@Resource
private SysWxAuditConfigService sysWxAuditConfigService;
/**
* 企业微信——企业access_token获取url
*/
@Getter
@Value("${qyAccessTokenUrl}")
private String qyAccessTokenUrl;
/**
* 企业微信——消息发送url
*/
@Getter
@Value("${qySendMsgUrl}")
private String qySendMsgUrl;
/**
* 企业微信——用户Oauth2认证授权url
*/
@Getter
@Value("${qyAuthorizeUrl}")
private String qyAuthorizeUrl;
/**
* 企业微信-企业自建应用用户信息获取url
*/
@Getter
@Value("${qyUserInfoUrl}")
private String qyUserInfoUrl;
/**
* 企业微信-企业自建应用用户信息获取url
*/
@Getter
@Value("${qyUserUrl}")
private String qyUserUrl;
/**
* 企业微信-上传临时素材url
*/
@Getter
@Value("${qyUploadTempFileUrl}")
private String qyUploadTempFileUrl;
/**
* 企业微信-部门创建url
*/
@Getter
@Value("${qyDeptCreateUrl}")
private String qyDeptCreateUrl;
/**
* 企业微信-部门更新url
*/
@Getter
@Value("${qyDeptUpdateUrl}")
private String qyDeptUpdateUrl;
/**
* 企业微信-部门删除url
*/
@Getter
@Value("${qyDeptDeleteUrl}")
private String qyDeptDeleteUrl;
/**
* 企业微信-部门列表拉取url
*/
@Getter
@Value("${qyDeptQueryListUrl}")
private String qyDeptQueryListUrl;
/**
* 企业微信-部门列表拉取url
*/
@Getter
@Value("${qyMenuCreateUrl}")
private String qyMenuCreateUrl;
/**
* 企业微信-部门列表拉取url
*/
@Getter
@Value("${qyMenuDeleteUrl}")
private String qyMenuDeleteUrl;
/**
* 企业微信-部门列表拉取url
*/
@Getter
@Value("${qyMenuQueryUrl}")
private String qyMenuQueryUrl;
/**
* 企业微信-ticket获取url
*/
@Getter
@Value("${ticketUrl}")
private String ticketUrl;
/**
* 获取企业微信-企业access_token先从redis持久化缓存中获取若不存在则从腾讯获取并加入缓存key为企业微信corpId+corpSecret
*
* @param corpId 企业微信企业ID
* @param corpSecret 企业微信企业应用密钥
* @return String 企业微信-企业access_token
*/
public String getQyAccessToken(String corpId, String corpSecret) {
Object accessToken = redisTemplate.opsForValue().get(corpId.concat(corpSecret));
if (Objects.nonNull(accessToken)) {
return accessToken.toString();
} else {
JSONObject jsonObject = HttpClientUtils.httpGet(this.buildQyAccessTokenUrl(corpId, corpSecret));
String qyAccessToken = jsonObject.getString("access_token");
String expiresIn = jsonObject.getString("expires_in");
redisTemplate.opsForValue().set(corpId.concat(corpSecret), qyAccessToken, Long.parseLong(expiresIn), TimeUnit.SECONDS);
return qyAccessToken;
}
}
/**
* 获取JsapiTicket,先从redis持久化缓存中获取若不存在则从腾讯获取并加入缓存key为ticket
*/
public String getJsapiTicket(String accessToken) {
Object ticket = redisTemplate.opsForValue().get("ticket");
if (Objects.nonNull(ticket)) {
return ticket.toString();
} else {
JSONObject jsonObject = HttpClientUtils.httpGet(this.buildTicketUrl(accessToken));
String qyTicket = jsonObject.getString("ticket");
String expiresIn = jsonObject.getString("expires_in");
redisTemplate.opsForValue().set("ticket", qyTicket, Long.parseLong(expiresIn), TimeUnit.SECONDS);
return qyTicket;
}
}
/**
* 获取企业微信内建应用简单用户信息对象
*
* @param code 企业微信Oauth2授权成功后获取的授权code,每次成员授权带上的code将不一样code只能使用一次5分钟未被使用自动过期。
* @param corpId 企业微信企业ID
* @param corpSecret 企业微信企业应用密钥
* @return QywxUser企业颞部开发获取的用户信息对象
*/
public User getUser(String code, String corpId, String corpSecret) {
JSONObject jsonObject = HttpClientUtils.httpGet(this.buildQyUserInfoUrl(code, corpId, corpSecret));
return jsonObject.toJavaObject(User.class);
}
/**
* 构建企业微信access_token获取url
*
* @param corpId 企业微信企业ID
* @param corpSecret 企业微信企业应用密钥
*/
public String buildQyAccessTokenUrl(String corpId, String corpSecret) {
return this.getQyAccessTokenUrl().replace("CORP_ID", corpId).replace("CORP_SECRET", corpSecret);
}
/**
* 构建获取企业微信内建应用简单用户信息对象获取url
*
* @param code 企业微信Oauth2授权成功后获取的授权code
* @param corpId 企业微信企业ID
* @param corpSecret 企业微信企业应用密钥
*/
public String buildQyUserInfoUrl(String code, String corpId, String corpSecret) {
return this.getQyUserInfoUrl().replaceAll("ACCESS_TOKEN", this.getQyAccessToken(corpId, corpSecret)).replaceAll("CODE", code);
}
/**
* 构建读取成员url
*
* @param userId 企业微信用户id
* @param corpId 企业微信企业ID
* @param corpSecret 企业微信企业应用密钥
*/
public String buildQyUserUrl(String userId, String corpId, String corpSecret) {
return this.getQyUserUrl().replaceAll("ACCESS_TOKEN", this.getQyAccessToken(corpId, corpSecret)).replaceAll("USER_ID", userId);
}
/**
* 构建消息发送url
*
* @param corpId 企业微信企业ID
* @param corpSecret 企业微信企业应用密钥
*/
public String buildQySendMsgUrl(String corpId, String corpSecret) {
return this.getQySendMsgUrl().replace("ACCESS_TOKEN", this.getQyAccessToken(corpId, corpSecret));
}
/**
* 构建企业微信授权url
*
* @param redirectUrl 授权成功重定向url
* @param corpId 企业微信企业ID
* @param state 授权成功重定向url携带的自定义参数
* @return 企业微信授权url
*/
public String buildQyAuthorizeUrl(String corpId, String agentId, String redirectUrl, String state) {
return this.getQyAuthorizeUrl()
.replaceAll("CORPID", corpId)
.replaceAll("REDIRECT_URI", redirectUrl)
.replaceAll("AGENTID", agentId)
.replaceAll("STATE", state);
}
/**
* 获取上传临时素材url
*
* @param type 媒体文件类型分别有图片image、语音voice、视频video普通文件file
* @param corpId 企业微信企业ID
* @param corpSecret 企业微信企业应用密钥
* @return String 上传临时素材url
*/
public String buildQyUploadTempFileUrl(String type, String corpId, String corpSecret) {
return this.getQyUploadTempFileUrl().replace("ACCESS_TOKEN", this.getQyAccessToken(corpId, corpSecret)).replaceAll("TYPE", type);
}
/**
* 构建企业微信-部门创建url
*
* @param corpId 企业微信企业ID
* @param corpSecret 企业微信企业应用密钥
* @return String 部门创建url
*/
public String buildQyDeptCreateUrl(String corpId, String corpSecret) {
return this.getQyDeptCreateUrl().replace("ACCESS_TOKEN", this.getQyAccessToken(corpId, corpSecret));
}
/**
* 构建企业微信-部门更新url
*
* @param corpId 企业微信企业ID
* @param corpSecret 企业微信企业应用密钥
* @return String 部门更新url
*/
public String buildQyDeptUpdateUrl(String corpId, String corpSecret) {
return this.getQyDeptUpdateUrl().replace("ACCESS_TOKEN", this.getQyAccessToken(corpId, corpSecret));
}
/**
* 构建企业微信-部门删除url
*
* @param corpId 企业微信企业ID
* @param corpSecret 企业微信企业应用密钥
* @param id 部门id
* @return String 部门删除url
*/
public String buildQyDeptDeleteUrl(String id, String corpId, String corpSecret) {
return this.getQyDeptDeleteUrl().replace("ACCESS_TOKEN", this.getQyAccessToken(corpId, corpSecret)).replaceAll("ID", id);
}
/**
* 构建企业微信-部门列表拉取url
*
* @param id 部门id
* @param corpId 企业微信企业ID
* @param corpSecret 企业微信企业应用密钥
* @return String 部门列表拉取url
*/
public String buildQyDeptQueryListUrl(String id, String corpId, String corpSecret) {
return this.getQyDeptQueryListUrl().replace("ACCESS_TOKEN", this.getQyAccessToken(corpId, corpSecret)).replaceAll("DEPARTMENT_ID", id);
}
/**
* 构建企业微信应用菜单创建url
*
* @param corpId 企业微信企业ID
* @param corpSecret 企业微信企业应用密钥
* @param agentId 企业微信企业应用ID
* @return String 应用菜单创建url
*/
public String buildQyMenuCreateUrl(String corpId, String corpSecret, String agentId) {
return this.getQyMenuCreateUrl().replace("ACCESS_TOKEN", this.getQyAccessToken(corpId, corpSecret)).replace("AGENT_ID", agentId);
}
/**
* 构建企业微信应用菜单删除url
*
* @param corpId 企业微信企业ID
* @param corpSecret 企业微信企业应用密钥
* @param agentId 企业微信企业应用ID
* @return string 应用菜单删除url
*/
public String buildQyMenuDeleteUrl(String corpId, String corpSecret, String agentId) {
return this.getQyMenuDeleteUrl().replace("ACCESS_TOKEN", this.getQyAccessToken(corpId, corpSecret)).replace("AGENT_ID", agentId);
}
/**
* 构建企业微信应用菜单拉取url
*
* @param corpId 企业微信企业ID
* @param corpSecret 企业微信企业应用密钥
* @param agentId 企业微信企业应用ID
* @return string 应用菜单拉取url
*/
public String buildQyMenuQueryUrl(String corpId, String corpSecret, String agentId) {
return this.getQyMenuQueryUrl().replace("ACCESS_TOKEN", this.getQyAccessToken(corpId, corpSecret)).replace("AGENT_ID", agentId);
}
/**
* 构建企业微信应用ticket获取url
*/
public String buildTicketUrl(String accessToken) {
return this.getTicketUrl().replace("ACCESS_TOKEN", accessToken);
}
/**
* 获取企业微信消息发送应用配置
*
* @param appType 服务应用类型
* @return SysWxAuditConfig 企业微信消息发送应用配置
*/
/**
* 获取企业微信消息发送应用配置
*
* @param appType 服务应用类型
* @return SysWxAuditConfig 企业微信消息发送应用配置
*/
public SysWxAuditConfig buildSysWxAuditConfig(String appType) throws Exception{
if (StringUtils.isBlank(appType)) {
throw new Exception("请求缺少服务应用类型!");
}
List<SysWxAuditConfig> sysWxAuditConfigs = sysWxAuditConfigService.list(new LambdaQueryWrapper<SysWxAuditConfig>()
.eq(SysWxAuditConfig::getAppType, appType)
.eq(SysWxAuditConfig::getState, SysWxAuditConfig.STATE_ACTIVE)
.eq(SysWxAuditConfig::getExt1, AuditConstants.CHINALCO_ZLEB_CHSZH));
SysWxAuditConfig sysWxAuditConfig = sysWxAuditConfigs.get(0);
if (StringUtils.isBlank(sysWxAuditConfig.getWxContextPath())) {
throw new Exception("微信审核应用上下文根未配置,请配置后重试!");
}
if (StringUtils.isBlank(sysWxAuditConfig.getHttpContextPath())) {
throw new Exception("微信审核http远程请求上下文根未配置请配置后重试");
}
if (StringUtils.isBlank(sysWxAuditConfig.getCorpId())) {
throw new Exception("微信审核企业ID未配置请配置后重试");
}
if (StringUtils.isBlank(sysWxAuditConfig.getCorpSecret())) {
throw new Exception("微信审核企业应用密钥未配置,请配置后重试!");
}
if (StringUtils.isBlank(sysWxAuditConfig.getAgentId())) {
throw new Exception("微信审核企业应用ID未配置请配置后重试");
}
return sysWxAuditConfig;
}
}

View File

@@ -0,0 +1,78 @@
package com.zt.plat.module.system.service.msg.impl;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO;
import com.zt.plat.module.system.controller.admin.sms.vo.msg.TextMessage;
import com.zt.plat.module.system.dal.dataobject.sms.SmsLogDO;
import com.zt.plat.module.system.dal.dataobject.sms.SmsTemplateDO;
import com.zt.plat.module.system.dal.mysql.sms.SmsLogMapper;
import com.zt.plat.module.system.enums.sms.SmsReceiveStatusEnum;
import com.zt.plat.module.system.enums.sms.SmsSendStatusEnum;
import com.zt.plat.module.system.service.msg.IMsgLogService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Objects;
@Service
public class MsgLogServiceImpl implements IMsgLogService {
@Resource
private SmsLogMapper smsLogMapper;
@Override
public Long createSmsLog(Long userId, Integer userType, Boolean isSend, SmsTemplateDO template, String templateContent, Map<String, Object> templateParams) {
return 0L;
}
@Override
public void updateSmsSendResult(Long id, Boolean success, String apiSendCode, String apiSendMsg, String apiRequestId, String apiSerialNo) {
}
@Override
public void updateSmsReceiveResult(Long id, Boolean success, LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg) {
}
@Override
public PageResult<SmsLogDO> getSmsLogPage(SmsLogPageReqVO pageReqVO) {
return null;
}
@Override
public SmsLogDO getSmsLog(Long id) {
return null;
}
/**
* 创建消息日志
*/
@Override
public Long createMsgLog(Long userId, Integer userType, Boolean isSend, SmsTemplateDO template,
TextMessage textMessage, Map<String, Object> templateParams) {
SmsLogDO.SmsLogDOBuilder logBuilder = SmsLogDO.builder();
// 根据是否要发送,设置状态
logBuilder.sendStatus(Objects.equals(isSend, true) ? SmsSendStatusEnum.INIT.getStatus()
: SmsSendStatusEnum.IGNORE.getStatus());
// 设置手机相关字段
//logBuilder.mobile(mobile).userId(userId).userType(userType);
// 设置模板相关字段
logBuilder.templateId(template.getId()).templateCode(template.getCode()).templateType(template.getType());
logBuilder.templateContent(String.valueOf(textMessage)).templateParams(templateParams)
.apiTemplateId(template.getApiTemplateId());
// 设置渠道相关字段
logBuilder.channelId(template.getChannelId()).channelCode(template.getChannelCode());
// 设置接收相关字段
logBuilder.receiveStatus(SmsReceiveStatusEnum.INIT.getStatus());
// 插入数据库
SmsLogDO logDO = logBuilder.build();
smsLogMapper.insert(logDO);
return logDO.getId();
}
}

View File

@@ -0,0 +1,106 @@
package com.zt.plat.module.system.service.msg.impl;
/**
* 流程审核企业微信待办消息队列监听
*/
/*
@Slf4j
@Component
@RocketMQMessageListener(topic = "springboot-mq",consumerGroup = "consumer-service")
@Transactional(rollbackFor = Exception.class)
public class RocketmqMsgQueueReceiver implements RocketMQListener<String> {
@Resource
private ISysRocketMqService sysActiveMqService;
@Resource
private ISendMsgService sendMsgService;
*/
/**
* 接受消息
*//*
@Override
public void onMessage(String jsonStr) {
TextMessage tm = JSON.parseObject(jsonStr, TextMessage.class);
log.info("接收到消息: {}", tm);
String consumeMsg;
try {
Text text = tm.getText();
String toUser = tm.getTouser();// 企业微信接收者
String userName = tm.getToparty(); // 部门
String userEname = tm.getText().getContent();//标签
String agentid = tm.getAgentid();//企微ID
String msgtype = tm.getMsgtype();//消息类型
String appType = tm.getAppType();// 消息自建类型
Integer enableDuplicateCheck = tm.getEnable_duplicate_check();//是否重启重复消息检查
Integer duplicateCheckInterval = tm.getDuplicate_check_interval();// 是否重复消息检查的时间间隔
String userCname = tm.getText().getContent();
// 校验接受用户
if (StringUtils.isBlank(toUser)) {
consumeMsg = "企业微信消息发送失败,用户【" + userEname + "-" + userCname + "】未绑定企业微信账号!";
sysActiveMqService.saveByTextMessage(this.getClass().getName(), tm, JmsConstant.JMS_TYPE_QUEUE.getCode(), JmsConstant.CONSUME_FAILURE.getCode(), consumeMsg);
} else {
String msgType = tm.getMsgtype();
if (Objects.equals(msgType, WxMsgTypeConstant.TEXTCARD.getCode())) {
*/
/* 构建审批回调url *//*
String appType = tm.getStringProperty("appType");
String approveType = tm.getStringProperty("approveType");
String companyCode = tm.getStringProperty("userCompanyCode");
String taskId = tm.getStringProperty("taskId");
String processInstanceId = tm.getStringProperty("processInstanceId");
String businessId = tm.getStringProperty("businessId");
Map<String, Object> substituteMap = new HashMap<>(5);
substituteMap.put("appType", appType.toLowerCase(Locale.ROOT));
substituteMap.put("approveType", approveType);
substituteMap.put("companyCode", companyCode);
substituteMap.put("taskId", taskId);
substituteMap.put("processInstanceId", processInstanceId);
substituteMap.put("businessId", businessId);
String redirectUrl = StringSolveUtils.placeholderReplace(AuditConstants.SunepsAuditType.REDIRECT_URL.getCode(), substituteMap);
*/
/* 构建文本卡片消息 *//*
String title = tm.getStringProperty("title");
String taskName = tm.getStringProperty("taskName");
String datetime = tm.getStringProperty("datetime");
String btnTxt = tm.getStringProperty("btntxt");
substituteMap.put("subTitle", datetime + "——" + userName);
substituteMap.put("description", descriptionText);
substituteMap.put("taskName", taskName);
String description = StringSolveUtils.placeholderReplace(AuditConstants.SunepsAuditType.AUDIT_DESCRIPTION_FORMAT.getCode(), substituteMap);
TextCardMessage textCardMessage = new TextCardMessage();
TextCard textCard = new TextCard();
textCard.setTitle(title)
.setDescription(description)
.setUrl(redirectUrl);
if (StringUtils.isNotBlank(btnTxt)) {
textCard.setBtntxt(btnTxt);
}
textCardMessage.setTextcard(textCard).setAppType(appType).setTouser(toUser);
sendMsgService.sendTextCardMsg(textCardMessage);
consumeMsg = "企业微信消息发送成功,接收用户【" + userEname + "-" + userCname + "】!";
sysActiveMqService.saveByTextMessage(this.getClass().getName(), tm, JmsConstant.JMS_TYPE_QUEUE.getCode(), JmsConstant.CONSUME_SUCCESS.getCode(), consumeMsg);
}
}
} catch (Exception e) {
log.error("中铝e办发送消息异常:{}",e.getMessage(),e);
try {
// 重试时不重复保存队列消息
if (!message.getJMSRedelivered()) {
sysActiveMqService.saveByTextMessage(this.getClass().getName(), tm, JmsConstant.JMS_TYPE_QUEUE.getCode(), JmsConstant.CONSUME_FAILURE.getCode(), e.getMessage());
}
} catch (Exception exception) {
log.error("中铝e办发送消息异常:{}", exception.getMessage(),e);
}
}
}
}
*/

View File

@@ -0,0 +1,732 @@
package com.zt.plat.module.system.service.msg.impl;
import com.google.common.annotations.VisibleForTesting;
import com.zt.plat.framework.common.core.KeyValue;
import com.zt.plat.framework.common.enums.CommonStatusEnum;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.util.validation.MatcherSolveUtils;
import com.zt.plat.module.system.controller.admin.sms.vo.msg.*;
import com.zt.plat.module.system.controller.admin.sms.vo.template.MsgTemplateSendReqVO;
import com.zt.plat.module.system.dal.dataobject.sms.SmsChannelDO;
import com.zt.plat.module.system.dal.dataobject.sms.SmsTemplateDO;
import com.zt.plat.module.system.framework.sms.core.enums.WxMsgTypeConstant;
import com.zt.plat.module.system.mq.producer.msg.MsgProducer;
import com.zt.plat.module.system.service.msg.IMsgLogService;
import com.zt.plat.module.system.service.msg.ISendMsgService;
import com.zt.plat.module.system.service.sms.SmsChannelService;
import com.zt.plat.module.system.service.sms.SmsTemplateService;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.zt.plat.module.system.enums.ErrorCodeConstants.*;
/**
* 消息发送接口实现
* TODO =========================================== 参考这个发送消息类文件===========================================================
* @author luzemin
*/
@Service
@Transactional(rollbackFor = Exception.class)
public class SendMsgServiceImpl implements ISendMsgService {
/**
* 文本消息内容限制
*/
public static final int INT_2048 = 2048;
@Resource
private SmsChannelService smsChannelService;
@Resource
private SmsTemplateService smsTemplateService;
@Resource
private IMsgLogService msgLogService;
@Resource
private MsgProducer msgProducer;
/**
* 发送MQ文本消息
* 用户ID,模版内容,消息类型、模版编码
* @param textMessage 文本消息包装对象
* @return r 响应对象
*/
@Override
public CommonResult<Object> sendTextMsg(MsgTemplateSendReqVO sendReqVO, TextMessage textMessage) throws Exception {
Long userId = sendReqVO.getUserId();
Integer userType = sendReqVO.getUserType();
String templateCode = sendReqVO.getTemplateCode();
Map<String, Object> templateParams = sendReqVO.getTemplateParams();
// 校验模板是否合法
SmsTemplateDO template = validateSmsTemplate(templateCode);
// 校验渠道是否合法
SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
// 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
/* 基础信息校验 */
msgSendBaseCheck(textMessage);
/* 消息类型校验 */
if (!Objects.equals(textMessage.getMsgtype(), WxMsgTypeConstant.TEXT.getCode())) {
throw new Exception("文本消息类型必须为" + WxMsgTypeConstant.TEXT.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(textMessage.getText())) {
throw new Exception("消息内容不能为空!");
}
/* 消息内容校验 */
if (StringUtils.isBlank(textMessage.getText().getContent())) {
throw new Exception("消息发送消息体不能为空!");
}
/* 消息内容长度检验 */
if (textMessage.getText().getContent().length() > INT_2048) {
throw new Exception("消息发送消息内容最长不超过2048个字节超过将截断");
}
// 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
//格式化短信内容
//String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
//创建发送日志
Long sendLogId = msgLogService.createMsgLog(userId, userType, isSend, template, textMessage, templateParams);
//发送 MQ 消息,异步执行发送消息(发送日志、内容、渠道ID、模版ID)
if (isSend) {
msgProducer.sendMsg(sendLogId, textMessage.getText().getContent(), template.getChannelId(), template.getApiTemplateId(), newTemplateParams);
}
return CommonResult.success(sendLogId);
}
@Override
public CommonResult<Object> sendImageMsg(Long userId, Integer userType, String templateCode, Map<String, Object> templateParams, ImageMessage imageMessage) throws Exception {
// 校验短信模板是否合法
SmsTemplateDO template = validateSmsTemplate(templateCode);
// 校验短信渠道是否合法
SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
// 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
/* 基础信息校验 */
msgSendBaseCheck(imageMessage);
/* 消息类型校验 */
if (!Objects.equals(imageMessage.getMsgtype(), WxMsgTypeConstant.IMAGE.getCode())) {
throw new Exception("图片消息类型必须为" + WxMsgTypeConstant.IMAGE.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(imageMessage.getImage())) {
throw new Exception("消息内容不能为空!");
}
/* 图片媒体ID校验 */
if (StringUtils.isBlank(imageMessage.getImage().getMedia_id())) {
throw new Exception("图片媒体ID不能为空!");
}
// 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = msgLogService.createSmsLog(userId, userType, isSend, template, content, templateParams);
/* if (isSend) {
msgProducer.sendSendMsg(sendLogId, content, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
}*/
return CommonResult.success(sendLogId);
}
@Override
public CommonResult<Object> sendVoiceMsg(Long userId, Integer userType, String templateCode, Map<String, Object> templateParams, VoiceMessage voiceMessage) throws Exception {
// 校验短信模板是否合法
SmsTemplateDO template = validateSmsTemplate(templateCode);
// 校验短信渠道是否合法
SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
// 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
/* 基础信息校验 */
msgSendBaseCheck(voiceMessage);
/* 消息类型校验 */
if (!Objects.equals(voiceMessage.getMsgtype(), WxMsgTypeConstant.VOICE.getCode())) {
throw new Exception("语音消息类型必须为" + WxMsgTypeConstant.VOICE.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(voiceMessage.getVoice())) {
throw new Exception("消息内容不能为空!");
}
/* 语音媒体ID校验 */
if (StringUtils.isBlank(voiceMessage.getVoice().getMedia_id())) {
throw new Exception("语音媒体ID不能为空!");
}
// 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = msgLogService.createSmsLog(userId, userType, isSend, template, content, templateParams);
/* if (isSend) {
msgProducer.sendSendMsg(sendLogId, content, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
}*/
return CommonResult.success(sendLogId);
}
@Override
public CommonResult<Object> sendVideoMsg(Long userId, Integer userType, String templateCode, Map<String, Object> templateParams, VideoMessage videoMessage) throws Exception {
// 校验短信模板是否合法
SmsTemplateDO template = validateSmsTemplate(templateCode);
// 校验短信渠道是否合法
SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
// 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
/* 基础信息校验 */
msgSendBaseCheck(videoMessage);
/* 消息类型校验 */
if (!Objects.equals(videoMessage.getMsgtype(), WxMsgTypeConstant.VIDEO.getCode())) {
throw new Exception("视频消息类型必须为" + WxMsgTypeConstant.VIDEO.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(videoMessage.getVideo())) {
throw new Exception("消息内容不能为空!");
}
/* 视频媒体ID校验 */
if (StringUtils.isBlank(videoMessage.getVideo().getMedia_id())) {
throw new Exception("视频媒体ID不能为空!");
}
// 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = msgLogService.createSmsLog(userId, userType, isSend, template, content, templateParams);
/* if (isSend) {
msgProducer.sendSendMsg(sendLogId, content, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
}*/
return CommonResult.success(sendLogId);
}
@Override
public CommonResult<Object> sendFileMsg(Long userId, Integer userType, String templateCode, Map<String, Object> templateParams, FileMessage fileMessage) throws Exception {
// 校验短信模板是否合法
SmsTemplateDO template = validateSmsTemplate(templateCode);
// 校验短信渠道是否合法
SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
// 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
/* 基础信息校验 */
msgSendBaseCheck(fileMessage);
/* 消息类型校验 */
if (!Objects.equals(fileMessage.getMsgtype(), WxMsgTypeConstant.FILE.getCode())) {
throw new Exception("文件消息类型必须为" + WxMsgTypeConstant.FILE.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(fileMessage.getFile())) {
throw new Exception("消息内容不能为空!");
}
/* 文件媒体ID校验 */
if (StringUtils.isBlank(fileMessage.getFile().getMedia_id())) {
throw new Exception("文件媒体ID不能为空!");
}
// 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = msgLogService.createSmsLog(userId, userType, isSend, template, content, templateParams);
/* if (isSend) {
msgProducer.sendSendMsg(sendLogId, content, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
}*/
return CommonResult.success(sendLogId);
}
@Override
public CommonResult<Object> sendTextCardMsgPich01(Long userId, Integer userType, String templateCode, Map<String, Object> templateParams, TextCardMessage textCardMessage) throws Exception {
// 校验短信模板是否合法
SmsTemplateDO template = validateSmsTemplate(templateCode);
// 校验短信渠道是否合法
SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
// 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
/* 基础信息校验 */
msgSendBaseCheck(textCardMessage);
/* 消息类型校验 */
if (!Objects.equals(textCardMessage.getMsgtype(), WxMsgTypeConstant.TEXTCARD.getCode())) {
throw new Exception("文本卡片消息类型必须为" + WxMsgTypeConstant.TEXTCARD.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(textCardMessage.getTextcard())) {
throw new Exception("消息内容不能为空!");
}
/* 文本卡片消息标题校验 */
if (StringUtils.isBlank(textCardMessage.getTextcard().getTitle())) {
throw new Exception("文本卡片消息比标题不能为空!");
}
/* 文本卡片消息描述校验 */
if (StringUtils.isBlank(textCardMessage.getTextcard().getDescription())) {
throw new Exception("文本卡片消息描述校验不能为空!");
}
/* 文本卡片消息点击url校验 */
if (StringUtils.isBlank(textCardMessage.getTextcard().getUrl())) {
throw new Exception("文本卡片消息点击url校验不能为空!");
}
/* 文本卡片消息点击url正确性校验 */
textCardMessage.getTextcard().setUrl(textCardMessage.getTextcard().getUrl());
if (!MatcherSolveUtils.checkInternetURL(textCardMessage.getTextcard().getUrl())) {
throw new Exception("消息发送消息内容点击跳转url不是正确的网络url链接");
}
// 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = msgLogService.createSmsLog(userId, userType, isSend, template, content, templateParams);
/* if (isSend) {
msgProducer.sendSendMsg(sendLogId, content, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
}*/
return CommonResult.success(sendLogId);
}
@Override
public CommonResult<Object> sendTextCardMsg(Long userId, Integer userType, String templateCode, Map<String, Object> templateParams, TextCardMessage textCardMessage) throws Exception {
// 校验短信模板是否合法
SmsTemplateDO template = validateSmsTemplate(templateCode);
// 校验短信渠道是否合法
SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
// 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
/* 基础信息校验 */
msgSendBaseCheck(textCardMessage);
/* 消息类型校验 */
if (!Objects.equals(textCardMessage.getMsgtype(), WxMsgTypeConstant.TEXTCARD.getCode())) {
throw new Exception("文本卡片消息类型必须为" + WxMsgTypeConstant.TEXTCARD.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(textCardMessage.getTextcard())) {
throw new Exception("消息内容不能为空!");
}
/* 文本卡片消息标题校验 */
if (StringUtils.isBlank(textCardMessage.getTextcard().getTitle())) {
throw new Exception("文本卡片消息比标题不能为空!");
}
/* 文本卡片消息描述校验 */
if (StringUtils.isBlank(textCardMessage.getTextcard().getDescription())) {
throw new Exception("文本卡片消息描述校验不能为空!");
}
/* 文本卡片消息点击url校验 */
if (StringUtils.isBlank(textCardMessage.getTextcard().getUrl())) {
throw new Exception("文本卡片消息点击url校验不能为空!");
}
/* 文本卡片消息点击url正确性校验 */
if (!MatcherSolveUtils.checkInternetURL(textCardMessage.getTextcard().getUrl())) {
throw new Exception("消息发送消息内容点击跳转url不是正确的网络url链接");
}
// 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = msgLogService.createSmsLog(userId, userType, isSend, template, content, templateParams);
/* if (isSend) {
msgProducer.sendSendMsg(sendLogId, content, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
}*/
return CommonResult.success(sendLogId);
}
@Override
public CommonResult<Object> sendNewsMsg(Long userId, Integer userType, String templateCode, Map<String, Object> templateParams, NewsMessage newsMessage) throws Exception {
// 校验短信模板是否合法
SmsTemplateDO template = validateSmsTemplate(templateCode);
// 校验短信渠道是否合法
SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
// 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
/* 基础信息校验 */
msgSendBaseCheck(newsMessage);
/* 消息类型校验 */
if (!Objects.equals(newsMessage.getMsgtype(), WxMsgTypeConstant.NEWS.getCode())) {
throw new Exception("图文消息类型必须为" + WxMsgTypeConstant.NEWS.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(newsMessage.getNews())) {
throw new Exception("消息内容不能为空!");
}
/* 图文消息校验 */
if (newsMessage.getNews().getArticles().isEmpty()) {
throw new Exception("图文消息内容不能为空!");
} else {
//newsMessage.setAgentid(sysWxAuditConfig.getAgentId());
newsMessage.getNews().getArticles().forEach(article -> {
/* 消息标题校验 */
if (StringUtils.isBlank(article.getTitle())) {
try {
throw new Exception("图文消息内容标题不能为空!");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* 消息url校验 */
if (StringUtils.isBlank(article.getUrl())) {
try {
throw new Exception("图文消息内容点击url不能为空");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* 消息url正确性校验 */
// article.setUrl(sysWxAuditConfig.getWxContextPath() + article.getUrl());
if (!MatcherSolveUtils.checkInternetURL(article.getUrl())) {
try {
throw new Exception("消息发送消息内容点击跳转url不是正确的网络url链接");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
// 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = msgLogService.createSmsLog(userId, userType, isSend, template, content, templateParams);
/* if (isSend) {
msgProducer.sendSendMsg(sendLogId, content, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
}*/
return CommonResult.success(sendLogId);
}
@Override
public CommonResult<Object> sendMpNewsMsg(Long userId, Integer userType, String templateCode, Map<String, Object> templateParams, MpNewsMessage mpNewsMessage) throws Exception {
// 校验短信模板是否合法
SmsTemplateDO template = validateSmsTemplate(templateCode);
// 校验短信渠道是否合法
SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
// 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
/* 基础信息校验 */
msgSendBaseCheck(mpNewsMessage);
/* 消息类型校验 */
if (!Objects.equals(mpNewsMessage.getMsgtype(), WxMsgTypeConstant.MPNEWS.getCode())) {
throw new Exception("图文(mpnews)消息类型必须为" + WxMsgTypeConstant.MPNEWS.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(mpNewsMessage.getMpnews())) {
throw new Exception("消息内容不能为空!");
}
/* 图文消息校验 */
if (mpNewsMessage.getMpnews().getArticles().isEmpty()) {
throw new Exception("图文消息内容不能为空!");
} else {
mpNewsMessage.getMpnews().getArticles().forEach(article -> {
/* 消息标题校验 */
if (StringUtils.isBlank(article.getTitle())) {
try {
throw new Exception("图文消息内容标题不能为空!");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* 消息图片媒体ID校验 */
if (StringUtils.isBlank(article.getThumb_media_id())) {
try {
throw new Exception("图文消息内容图片媒体ID不能为空");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* 消息内容校验 */
if (StringUtils.isBlank(article.getContent())) {
try {
throw new Exception("图文消息内容能为空!");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
// 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = msgLogService.createSmsLog(userId, userType, isSend, template, content, templateParams);
/* if (isSend) {
msgProducer.sendSendMsg(sendLogId, content, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
}*/
return CommonResult.success(sendLogId);
}
@Override
public CommonResult<Object> sendMarkdownMsg(Long userId, Integer userType, String templateCode, Map<String, Object> templateParams, MarkdownMessage markdownMessage) throws Exception {
// 校验短信模板是否合法
SmsTemplateDO template = validateSmsTemplate(templateCode);
// 校验短信渠道是否合法
SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
// 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
/* 基础信息校验 */
msgSendBaseCheck(markdownMessage);
/* 消息类型校验 */
if (!Objects.equals(markdownMessage.getMsgtype(), WxMsgTypeConstant.MARKDOWN.getCode())) {
throw new Exception("markdown消息消息类型必须为" + WxMsgTypeConstant.MARKDOWN.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(markdownMessage.getMarkdown())) {
throw new Exception("markdown消息内容能为空");
}
/* 消息内容校验 */
if (StringUtils.isBlank(markdownMessage.getMarkdown().getContent())) {
throw new Exception("markdown消息内容能为空");
}
// 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = msgLogService.createSmsLog(userId, userType, isSend, template, content, templateParams);
/* if (isSend) {
msgProducer.sendSendMsg(sendLogId, content, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
}*/
return CommonResult.success(sendLogId);
}
@Override
public CommonResult<Object> sendMiniProgramNoticeMsg(Long userId, Integer userType, String templateCode, Map<String, Object> templateParams, MiniProgramNoticeMessage miniProgramNoticeMessage) throws Exception {
// 校验短信模板是否合法
SmsTemplateDO template = validateSmsTemplate(templateCode);
// 校验短信渠道是否合法
SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
// 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
/* 基础信息校验 */
if (StringUtils.isAllBlank(miniProgramNoticeMessage.getTouser(), miniProgramNoticeMessage.getToparty(), miniProgramNoticeMessage.getTotag())) {
throw new Exception("消息发送对象不能为空!");
}
/* 消息类型校验 */
if (!Objects.equals(miniProgramNoticeMessage.getMsgtype(), WxMsgTypeConstant.MINIPROGRAM_NOTICE.getCode())) {
throw new Exception("小程序通知消息消息类型必须为" + WxMsgTypeConstant.MINIPROGRAM_NOTICE.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(miniProgramNoticeMessage.getMiniprogram_notice())) {
throw new Exception("消息内容不能为空!");
}
/* 小程序appid校验 */
if (StringUtils.isBlank(miniProgramNoticeMessage.getMiniprogram_notice().getAppid())) {
throw new Exception("小程序appid必须是与当前应用关联的小程序");
}
/* 小程序标题校验 */
if (StringUtils.isBlank(miniProgramNoticeMessage.getMiniprogram_notice().getTitle())) {
throw new Exception("消息标题不能为空长度限制4-12个汉字");
}
/* 小程序消息内容校验 */
if (Objects.isNull(miniProgramNoticeMessage.getMiniprogram_notice().getContent_item())) {
throw new Exception("消息内容不能为空!");
}
/* 小程序消息内容校验 */
if (miniProgramNoticeMessage.getMiniprogram_notice().getContent_item().isEmpty()) {
throw new Exception("消息内容不能为空!");
}
/* 小程序消息内容校验key-value校验 */
miniProgramNoticeMessage.getMiniprogram_notice().getContent_item().forEach(contentItem -> {
/* 小程序消息内容key校验 */
if (StringUtils.isBlank(contentItem.getKey())) {
try {
throw new Exception("小程序消息内容key不能为空");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* 小程序消息内容value校验 */
if (StringUtils.isBlank(contentItem.getValue())) {
try {
throw new Exception("小程序消息内容value不能为空");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
// 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = msgLogService.createSmsLog(userId, userType, isSend, template, content, templateParams);
/* if (isSend) {
msgProducer.sendSendMsg(sendLogId, content, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
}*/
return CommonResult.success(sendLogId);
}
@Override
public CommonResult<Object> sendInteractiveTaskCardMsg(Long userId, Integer userType, String templateCode, Map<String, Object> templateParams, InteractiveTaskCardMessage interactiveTaskCardMessage) throws Exception {
// 校验短信模板是否合法
SmsTemplateDO template = validateSmsTemplate(templateCode);
// 校验短信渠道是否合法
SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
// 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
/* 基础信息校验 */
msgSendBaseCheck(interactiveTaskCardMessage);
/* 消息类型校验 */
if (!Objects.equals(interactiveTaskCardMessage.getMsgtype(), WxMsgTypeConstant.INTERACTIVE_TASKCARD.getCode())) {
throw new Exception("任务卡片消息消息类型必须为" + WxMsgTypeConstant.INTERACTIVE_TASKCARD.getCode() + "");
}
/* 任务卡片消息内容校验 */
if (Objects.isNull(interactiveTaskCardMessage.getInteractive_taskcard())) {
throw new Exception("消息内容不能为空!");
}
/* 任务卡片消息内容标题校验 */
if (StringUtils.isBlank(interactiveTaskCardMessage.getInteractive_taskcard().getTitle())) {
throw new Exception("任务卡片消息内容标题不能为空!");
}
/* 任务卡片消息url校验 */
if (!MatcherSolveUtils.checkInternetURL(interactiveTaskCardMessage.getInteractive_taskcard().getUrl())) {
throw new Exception("任务卡片消息内容标题不能为空点击跳转url不是正确的网络url链接");
}
/* 任务卡片消息内容任务id校验 */
if (StringUtils.isBlank(interactiveTaskCardMessage.getInteractive_taskcard().getTask_id())) {
throw new Exception("任务卡片消息内容任务id不能为空,任务id同一个应用发送的任务卡片消息的任务id不能重复只能由数字、字母和“_-@”组成最长支持128字节");
}
/* 任务卡片消息内容按钮组校验 */
if (Objects.isNull(interactiveTaskCardMessage.getInteractive_taskcard().getBtn())) {
throw new Exception("任务卡片消息内容按钮组不能为空!");
}
/* 任务卡片消息内容按钮组校验 */
interactiveTaskCardMessage.getInteractive_taskcard().getBtn().forEach(btn -> {
/* 任务卡片消息内容按钮组按钮key校验 */
if (StringUtils.isBlank(interactiveTaskCardMessage.getInteractive_taskcard().getTitle())) {
try {
throw new Exception("任务卡片消息内容按钮组按钮key不能为空按钮key值用户点击后会产生任务卡片回调事件回调事件会带上该key值只能由数字、字母和“_-@”组成最长支持128字节");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* 任务卡片消息内容按钮组阿牛name校验 */
if (StringUtils.isBlank(interactiveTaskCardMessage.getInteractive_taskcard().getTitle())) {
try {
throw new Exception("任务卡片消息内容按钮组阿牛name不能为空按钮名称最长支持18个字节超过则截断");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
// 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = msgLogService.createSmsLog(userId, userType, isSend, template, content, templateParams);
/* if (isSend) {
msgProducer.sendSendMsg(sendLogId, content, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
}*/
return CommonResult.success(sendLogId);
}
/**
* 企业微信消息发送基础信息校验
*
* @param baseMessage 企业微信消息体
*/
private void msgSendBaseCheck(BaseMessage baseMessage) throws Exception{
if (StringUtils.isAllBlank(baseMessage.getTouser(), baseMessage.getToparty(), baseMessage.getTotag())) {
throw new Exception("消息发送对象不能为空!");
}
}
@VisibleForTesting
SmsTemplateDO validateSmsTemplate(String templateCode) {
// 获得短信模板。考虑到效率,从缓存中获取
SmsTemplateDO template = smsTemplateService.getSmsTemplateByCodeFromCache(templateCode);
// 短信模板不存在
if (template == null) {
throw exception(MSG_CALLBACK_SIGN_INVALID);
}
return template;
}
@VisibleForTesting
SmsChannelDO validateSmsChannel(Long channelId) {
// 获得短信模板。考虑到效率,从缓存中获取
SmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId);
// 短信模板不存在
if (channelDO == null) {
throw exception(MSG_CHANNEL_NOT_EXISTS);
}
return channelDO;
}
@VisibleForTesting
List<KeyValue<String, Object>> buildTemplateParams(SmsTemplateDO template, Map<String, Object> templateParams) {
return template.getParams().stream().map(key -> {
Object value = templateParams.get(key);
if (value == null) {
throw exception(MSG_CALLBACK_SIGN_INVALID, key);
}
return new KeyValue<>(key, value);
}).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,587 @@
package com.zt.plat.module.system.service.msg.impl;
import com.alibaba.fastjson.JSONObject;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.util.http.HttpClientUtils;
import com.zt.plat.framework.common.util.validation.MatcherSolveUtils;
import com.zt.plat.module.system.framework.sms.core.enums.WxMsgTypeConstant;
import com.zt.plat.module.system.service.msg.config.WeiXinProperties;
import com.zt.plat.module.system.dal.dataobject.msg.SysWxAuditConfig;
import com.zt.plat.module.system.service.msg.ISendWxMsgService;
import org.apache.commons.lang3.StringUtils;
import com.zt.plat.module.system.controller.admin.sms.vo.msg.*;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Objects;
/**
* TODO ================================ 中铝e办MQ生产者 ======================================
*/
@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
public class SendWxMsgServiceImpl implements ISendWxMsgService {
/**
* 文本消息内容限制
*/
public static final int INT_2048 = 2048;
/**
* 企业微信消息发送响应结果代码key
*/
public static final String WX_MSG_RESPONSE_CODE = "errcode";
/**
* 企业微信消息发送成功响应代码值
*/
public static final String WX_MSG_RESPONSE_SUCCESS_CODE = "0";
@Resource
private WeiXinProperties weiXinProperties;
/**
* 发送企业微信文本消息
*
* @param textMessage 文本消息对象
* @return r 响应对象
*/
@Override
public CommonResult<Object> sendTextMsg(TextMessage textMessage) throws Exception{
/* 基础信息校验 */
msgSendBaseCheck(textMessage);
/* 消息类型校验 */
if (!Objects.equals(textMessage.getMsgtype(), WxMsgTypeConstant.TEXT.getCode())) {
throw new Exception("文本消息类型必须为" + WxMsgTypeConstant.TEXT.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(textMessage.getText())) {
throw new Exception("消息内容不能为空!");
}
/* 消息内容校验 */
if (StringUtils.isBlank(textMessage.getText().getContent())) {
throw new Exception("消息发送消息体不能为空!");
}
/* 消息内容长度检验 */
if (textMessage.getText().getContent().length() > INT_2048) {
throw new Exception("消息发送消息内容最长不超过2048个字节超过将截断");
}
/* 消息发送 */
SysWxAuditConfig sysWxAuditConfig = this.buildSysWxAuditConfig(textMessage);
textMessage.setAgentid(sysWxAuditConfig.getAgentId());
/* 如果消息包含点击链接,重新包装处理消息内容 */
if (StringUtils.isNotBlank(textMessage.getText().getUrl())) {
textMessage.getText().setUrl(sysWxAuditConfig.getWxContextPath() + textMessage.getText().getUrl());
/* url校验 */
if (!MatcherSolveUtils.checkInternetURL(textMessage.getText().getUrl())) {
throw new Exception("消息发送消息内容点击跳转url不是正确的网络url链接");
}
textMessage.getText().buildUrlContent();
}
JSONObject jsonObject = HttpClientUtils.httpPost(weiXinProperties.buildQySendMsgUrl(sysWxAuditConfig.getCorpId(), sysWxAuditConfig.getCorpSecret()), (JSONObject) JSONObject.toJSON(textMessage));
return buildQywxMsgSendResult(jsonObject);
}
/**
* 发送企业微信图片消息
*
* @param imageMessage 图片消息对象
* @return R 消息发送响应对象
*/
@Override
public CommonResult<Object> sendImageMsg(ImageMessage imageMessage) throws Exception{
/* 基础信息校验 */
msgSendBaseCheck(imageMessage);
/* 消息类型校验 */
if (!Objects.equals(imageMessage.getMsgtype(), WxMsgTypeConstant.IMAGE.getCode())) {
throw new Exception("图片消息类型必须为" + WxMsgTypeConstant.IMAGE.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(imageMessage.getImage())) {
throw new Exception("消息内容不能为空!");
}
/* 图片媒体ID校验 */
if (StringUtils.isBlank(imageMessage.getImage().getMedia_id())) {
throw new Exception("图片媒体ID不能为空!");
}
/* 消息发送 */
SysWxAuditConfig sysWxAuditConfig = this.buildSysWxAuditConfig(imageMessage);
imageMessage.setAgentid(sysWxAuditConfig.getAgentId());
JSONObject jsonObject = HttpClientUtils.httpPost(weiXinProperties.buildQySendMsgUrl(sysWxAuditConfig.getCorpId(), sysWxAuditConfig.getCorpSecret()), (JSONObject) JSONObject.toJSON(imageMessage));
System.out.println("图片消息发送返回:" + jsonObject.toJSONString());
return buildQywxMsgSendResult(jsonObject);
}
/**
* 发送企业微信语音消息
*
* @param voiceMessage 语音消息对象
* @return R 消息发送响应对象
*/
@Override
public CommonResult<Object> sendVoiceMsg(VoiceMessage voiceMessage) throws Exception{
/* 基础信息校验 */
msgSendBaseCheck(voiceMessage);
/* 消息类型校验 */
if (!Objects.equals(voiceMessage.getMsgtype(), WxMsgTypeConstant.VOICE.getCode())) {
throw new Exception("语音消息类型必须为" + WxMsgTypeConstant.VOICE.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(voiceMessage.getVoice())) {
throw new Exception("消息内容不能为空!");
}
/* 语音媒体ID校验 */
if (StringUtils.isBlank(voiceMessage.getVoice().getMedia_id())) {
throw new Exception("语音媒体ID不能为空!");
}
/* 消息发送 */
SysWxAuditConfig sysWxAuditConfig = this.buildSysWxAuditConfig(voiceMessage);
voiceMessage.setAgentid(sysWxAuditConfig.getAgentId());
JSONObject jsonObject = HttpClientUtils.httpPost(weiXinProperties.buildQySendMsgUrl(sysWxAuditConfig.getCorpId(), sysWxAuditConfig.getCorpSecret()), (JSONObject) JSONObject.toJSON(voiceMessage));
return buildQywxMsgSendResult(jsonObject);
}
/**
* 发送企业微信视频消息
*
* @param videoMessage 视频消息对象
* @return R 消息发送响应对象
*/
@Override
public CommonResult<Object> sendVideoMsg(VideoMessage videoMessage) throws Exception{
/* 基础信息校验 */
msgSendBaseCheck(videoMessage);
/* 消息类型校验 */
if (!Objects.equals(videoMessage.getMsgtype(), WxMsgTypeConstant.VIDEO.getCode())) {
throw new Exception("视频消息类型必须为" + WxMsgTypeConstant.VIDEO.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(videoMessage.getVideo())) {
throw new Exception("消息内容不能为空!");
}
/* 视频媒体ID校验 */
if (StringUtils.isBlank(videoMessage.getVideo().getMedia_id())) {
throw new Exception("视频媒体ID不能为空!");
}
/* 消息发送 */
SysWxAuditConfig sysWxAuditConfig = this.buildSysWxAuditConfig(videoMessage);
videoMessage.setAgentid(sysWxAuditConfig.getAgentId());
JSONObject jsonObject = HttpClientUtils.httpPost(weiXinProperties.buildQySendMsgUrl(sysWxAuditConfig.getCorpId(), sysWxAuditConfig.getCorpSecret()), (JSONObject) JSONObject.toJSON(videoMessage));
return buildQywxMsgSendResult(jsonObject);
}
/**
* 发送企业微信文件消息
*
* @param fileMessage 文件消息对象
* @return R 消息发送响应对象
*/
@Override
public CommonResult<Object> sendFileMsg(FileMessage fileMessage) throws Exception{
/* 基础信息校验 */
msgSendBaseCheck(fileMessage);
/* 消息类型校验 */
if (!Objects.equals(fileMessage.getMsgtype(), WxMsgTypeConstant.FILE.getCode())) {
throw new Exception("文件消息类型必须为" + WxMsgTypeConstant.FILE.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(fileMessage.getFile())) {
throw new Exception("消息内容不能为空!");
}
/* 文件媒体ID校验 */
if (StringUtils.isBlank(fileMessage.getFile().getMedia_id())) {
throw new Exception("文件媒体ID不能为空!");
}
/* 消息发送 */
SysWxAuditConfig sysWxAuditConfig = this.buildSysWxAuditConfig(fileMessage);
fileMessage.setAgentid(sysWxAuditConfig.getAgentId());
JSONObject jsonObject = HttpClientUtils.httpPost(weiXinProperties.buildQySendMsgUrl(sysWxAuditConfig.getCorpId(), sysWxAuditConfig.getCorpSecret()), (JSONObject) JSONObject.toJSON(fileMessage));
return buildQywxMsgSendResult(jsonObject);
}
/**
* 发送企业微信文本卡片消息
*
* @param textCardMessage 文本卡片消息对象
* @return R 消息发送响应对象
*/
@Override
public CommonResult<Object> sendTextCardMsg(TextCardMessage textCardMessage) throws Exception{
SysWxAuditConfig sysWxAuditConfig = this.buildSysWxAuditConfig(textCardMessage);
/* 基础信息校验 */
msgSendBaseCheck(textCardMessage);
/* 消息类型校验 */
if (!Objects.equals(textCardMessage.getMsgtype(), WxMsgTypeConstant.TEXTCARD.getCode())) {
throw new Exception("文本卡片消息类型必须为" + WxMsgTypeConstant.TEXTCARD.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(textCardMessage.getTextcard())) {
throw new Exception("消息内容不能为空!");
}
/* 文本卡片消息标题校验 */
if (StringUtils.isBlank(textCardMessage.getTextcard().getTitle())) {
throw new Exception("文本卡片消息比标题不能为空!");
}
/* 文本卡片消息描述校验 */
if (StringUtils.isBlank(textCardMessage.getTextcard().getDescription())) {
throw new Exception("文本卡片消息描述校验不能为空!");
}
/* 文本卡片消息点击url校验 */
if (StringUtils.isBlank(textCardMessage.getTextcard().getUrl())) {
throw new Exception("文本卡片消息点击url校验不能为空!");
}
/* 文本卡片消息点击url正确性校验 */
textCardMessage.getTextcard().setUrl(sysWxAuditConfig.getWxContextPath() + textCardMessage.getTextcard().getUrl());
if (!MatcherSolveUtils.checkInternetURL(textCardMessage.getTextcard().getUrl())) {
throw new Exception("消息发送消息内容点击跳转url不是正确的网络url链接");
}
/* 消息发送 */
textCardMessage.setAgentid(sysWxAuditConfig.getAgentId());
//TODO 此出发中铝 e 办
JSONObject jsonObject = HttpClientUtils.httpPost(weiXinProperties.buildQySendMsgUrl(sysWxAuditConfig.getCorpId(), sysWxAuditConfig.getCorpSecret()), (JSONObject) JSONObject.toJSON(textCardMessage));
return buildQywxMsgSendResult(jsonObject);
}
/**
* 发送企业微信文本卡片消息
*
* @param textCardMessage 文本卡片消息对象
* @return R 消息发送响应对象
*/
@Override
public CommonResult<Object> sendTextCardMsgPich01(TextCardMessage textCardMessage) throws Exception{
SysWxAuditConfig sysWxAuditConfig = this.buildSysWxAuditConfig(textCardMessage);
/* 基础信息校验 */
msgSendBaseCheck(textCardMessage);
/* 消息类型校验 */
if (!Objects.equals(textCardMessage.getMsgtype(), WxMsgTypeConstant.TEXTCARD.getCode())) {
throw new Exception("文本卡片消息类型必须为" + WxMsgTypeConstant.TEXTCARD.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(textCardMessage.getTextcard())) {
throw new Exception("消息内容不能为空!");
}
/* 文本卡片消息标题校验 */
if (StringUtils.isBlank(textCardMessage.getTextcard().getTitle())) {
throw new Exception("文本卡片消息比标题不能为空!");
}
/* 文本卡片消息描述校验 */
if (StringUtils.isBlank(textCardMessage.getTextcard().getDescription())) {
throw new Exception("文本卡片消息描述校验不能为空!");
}
/* 文本卡片消息点击url校验 */
if (StringUtils.isBlank(textCardMessage.getTextcard().getUrl())) {
throw new Exception("文本卡片消息点击url校验不能为空!");
}
/* 文本卡片消息点击url正确性校验 */
textCardMessage.getTextcard().setUrl(textCardMessage.getTextcard().getUrl());
if (!MatcherSolveUtils.checkInternetURL(textCardMessage.getTextcard().getUrl())) {
throw new Exception("消息发送消息内容点击跳转url不是正确的网络url链接");
}
/* 消息发送 */
textCardMessage.setAgentid(sysWxAuditConfig.getAgentId());
JSONObject jsonObject = HttpClientUtils.httpPost(weiXinProperties.buildQySendMsgUrl(sysWxAuditConfig.getCorpId(), sysWxAuditConfig.getCorpSecret()), (JSONObject) JSONObject.toJSON(textCardMessage));
return buildQywxMsgSendResult(jsonObject);
}
/**
* 发送企业微信图文消息
*
* @param newsMessage 图文消息对象
* @return R 消息发送响应对象
*/
@Override
public CommonResult<Object> sendNewsMsg(NewsMessage newsMessage) throws Exception{
SysWxAuditConfig sysWxAuditConfig = this.buildSysWxAuditConfig(newsMessage);
/* 基础信息校验 */
msgSendBaseCheck(newsMessage);
/* 消息类型校验 */
if (!Objects.equals(newsMessage.getMsgtype(), WxMsgTypeConstant.NEWS.getCode())) {
throw new Exception("图文消息类型必须为" + WxMsgTypeConstant.NEWS.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(newsMessage.getNews())) {
throw new Exception("消息内容不能为空!");
}
/* 图文消息校验 */
if (newsMessage.getNews().getArticles().isEmpty()) {
throw new Exception("图文消息内容不能为空!");
} else {
newsMessage.setAgentid(sysWxAuditConfig.getAgentId());
newsMessage.getNews().getArticles().forEach(article -> {
/* 消息标题校验 */
if (StringUtils.isBlank(article.getTitle())) {
try {
throw new Exception("图文消息内容标题不能为空!");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* 消息url校验 */
if (StringUtils.isBlank(article.getUrl())) {
try {
throw new Exception("图文消息内容点击url不能为空");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* 消息url正确性校验 */
article.setUrl(sysWxAuditConfig.getWxContextPath() + article.getUrl());
if (!MatcherSolveUtils.checkInternetURL(article.getUrl())) {
try {
throw new Exception("消息发送消息内容点击跳转url不是正确的网络url链接");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
/* 消息发送 */
JSONObject jsonObject = HttpClientUtils.httpPost(weiXinProperties.buildQySendMsgUrl(sysWxAuditConfig.getCorpId(), sysWxAuditConfig.getCorpSecret()), (JSONObject) JSONObject.toJSON(newsMessage));
return buildQywxMsgSendResult(jsonObject);
}
/**
* 发送企业微信图文消息mpnews
*
* @param mpNewsMessage 图文消息mpnews对象
* @return R 消息发送响应对象
*/
@Override
public CommonResult<Object> sendMpNewsMsg(MpNewsMessage mpNewsMessage)throws Exception {
/* 基础信息校验 */
msgSendBaseCheck(mpNewsMessage);
/* 消息类型校验 */
if (!Objects.equals(mpNewsMessage.getMsgtype(), WxMsgTypeConstant.MPNEWS.getCode())) {
throw new Exception("图文(mpnews)消息类型必须为" + WxMsgTypeConstant.MPNEWS.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(mpNewsMessage.getMpnews())) {
throw new Exception("消息内容不能为空!");
}
/* 图文消息校验 */
if (mpNewsMessage.getMpnews().getArticles().isEmpty()) {
throw new Exception("图文消息内容不能为空!");
} else {
mpNewsMessage.getMpnews().getArticles().forEach(article -> {
/* 消息标题校验 */
if (StringUtils.isBlank(article.getTitle())) {
try {
throw new Exception("图文消息内容标题不能为空!");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* 消息图片媒体ID校验 */
if (StringUtils.isBlank(article.getThumb_media_id())) {
try {
throw new Exception("图文消息内容图片媒体ID不能为空");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* 消息内容校验 */
if (StringUtils.isBlank(article.getContent())) {
try {
throw new Exception("图文消息内容能为空!");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
/* 消息发送 */
SysWxAuditConfig sysWxAuditConfig = this.buildSysWxAuditConfig(mpNewsMessage);
mpNewsMessage.setAgentid(sysWxAuditConfig.getAgentId());
JSONObject jsonObject = HttpClientUtils.httpPost(weiXinProperties.buildQySendMsgUrl(sysWxAuditConfig.getCorpId(), sysWxAuditConfig.getCorpSecret()), (JSONObject) JSONObject.toJSON(mpNewsMessage));
return buildQywxMsgSendResult(jsonObject);
}
/**
* 发送企业微信markdown消息
*
* @param markdownMessage markdown消息对象
* @return R 消息发送响应对象
*/
@Override
public CommonResult<Object> sendMarkdownMsg(MarkdownMessage markdownMessage) throws Exception{
/* 基础信息校验 */
msgSendBaseCheck(markdownMessage);
/* 消息类型校验 */
if (!Objects.equals(markdownMessage.getMsgtype(), WxMsgTypeConstant.MARKDOWN.getCode())) {
throw new Exception("markdown消息消息类型必须为" + WxMsgTypeConstant.MARKDOWN.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(markdownMessage.getMarkdown())) {
throw new Exception("markdown消息内容能为空");
}
/* 消息内容校验 */
if (StringUtils.isBlank(markdownMessage.getMarkdown().getContent())) {
throw new Exception("markdown消息内容能为空");
}
/* 消息发送 */
SysWxAuditConfig sysWxAuditConfig = this.buildSysWxAuditConfig(markdownMessage);
markdownMessage.setAgentid(sysWxAuditConfig.getAgentId());
JSONObject jsonObject = HttpClientUtils.httpPost(weiXinProperties.buildQySendMsgUrl(sysWxAuditConfig.getCorpId(), sysWxAuditConfig.getCorpSecret()), (JSONObject) JSONObject.toJSON(markdownMessage));
return buildQywxMsgSendResult(jsonObject);
}
/**
* 发送企业微信小程序通知消息
*
* @param miniProgramNoticeMessage 小程序通知消息对象
* @return R 消息发送响应对象
*/
@Override
public CommonResult<Object> sendMiniProgramNoticeMsg(MiniProgramNoticeMessage miniProgramNoticeMessage) throws Exception {
/* 基础信息校验 */
if (StringUtils.isAllBlank(miniProgramNoticeMessage.getTouser(), miniProgramNoticeMessage.getToparty(), miniProgramNoticeMessage.getTotag())) {
throw new Exception("消息发送对象不能为空!");
}
/* 消息类型校验 */
if (!Objects.equals(miniProgramNoticeMessage.getMsgtype(), WxMsgTypeConstant.MINIPROGRAM_NOTICE.getCode())) {
throw new Exception("小程序通知消息消息类型必须为" + WxMsgTypeConstant.MINIPROGRAM_NOTICE.getCode() + "");
}
/* 消息内容校验 */
if (Objects.isNull(miniProgramNoticeMessage.getMiniprogram_notice())) {
throw new Exception("消息内容不能为空!");
}
/* 小程序appid校验 */
if (StringUtils.isBlank(miniProgramNoticeMessage.getMiniprogram_notice().getAppid())) {
throw new Exception("小程序appid必须是与当前应用关联的小程序");
}
/* 小程序标题校验 */
if (StringUtils.isBlank(miniProgramNoticeMessage.getMiniprogram_notice().getTitle())) {
throw new Exception("消息标题不能为空长度限制4-12个汉字");
}
/* 小程序消息内容校验 */
if (Objects.isNull(miniProgramNoticeMessage.getMiniprogram_notice().getContent_item())) {
throw new Exception("消息内容不能为空!");
}
/* 小程序消息内容校验 */
if (miniProgramNoticeMessage.getMiniprogram_notice().getContent_item().isEmpty()) {
throw new Exception("消息内容不能为空!");
}
/* 小程序消息内容校验key-value校验 */
miniProgramNoticeMessage.getMiniprogram_notice().getContent_item().forEach(contentItem -> {
/* 小程序消息内容key校验 */
if (StringUtils.isBlank(contentItem.getKey())) {
try {
throw new Exception("小程序消息内容key不能为空");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* 小程序消息内容value校验 */
if (StringUtils.isBlank(contentItem.getValue())) {
try {
throw new Exception("小程序消息内容value不能为空");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
/* 消息发送 */
SysWxAuditConfig sysWxAuditConfig = this.buildSysWxAuditConfig(miniProgramNoticeMessage);
miniProgramNoticeMessage.setAgentid(sysWxAuditConfig.getAgentId());
JSONObject jsonObject = HttpClientUtils.httpPost(weiXinProperties.buildQySendMsgUrl(sysWxAuditConfig.getCorpId(), sysWxAuditConfig.getCorpSecret()), (JSONObject) JSONObject.toJSON(miniProgramNoticeMessage));
return buildQywxMsgSendResult(jsonObject);
}
/**
* 发送企业微信任务卡片消息
*
* @param interactiveTaskCardMessage 任务卡片消息对象
* @return R 消息发送响应对象
*/
@Override
public CommonResult<Object> sendInteractiveTaskCardMsg(InteractiveTaskCardMessage interactiveTaskCardMessage) throws Exception {
SysWxAuditConfig sysWxAuditConfig = this.buildSysWxAuditConfig(interactiveTaskCardMessage);
/* 基础信息校验 */
msgSendBaseCheck(interactiveTaskCardMessage);
/* 消息类型校验 */
if (!Objects.equals(interactiveTaskCardMessage.getMsgtype(), WxMsgTypeConstant.INTERACTIVE_TASKCARD.getCode())) {
throw new Exception("任务卡片消息消息类型必须为" + WxMsgTypeConstant.INTERACTIVE_TASKCARD.getCode() + "");
}
/* 任务卡片消息内容校验 */
if (Objects.isNull(interactiveTaskCardMessage.getInteractive_taskcard())) {
throw new Exception("消息内容不能为空!");
}
/* 任务卡片消息内容标题校验 */
if (StringUtils.isBlank(interactiveTaskCardMessage.getInteractive_taskcard().getTitle())) {
throw new Exception("任务卡片消息内容标题不能为空!");
}
/* 任务卡片消息url校验 */
interactiveTaskCardMessage.getInteractive_taskcard().setUrl(sysWxAuditConfig.getWxContextPath() + interactiveTaskCardMessage.getInteractive_taskcard().getUrl());
if (!MatcherSolveUtils.checkInternetURL(interactiveTaskCardMessage.getInteractive_taskcard().getUrl())) {
throw new Exception("任务卡片消息内容标题不能为空点击跳转url不是正确的网络url链接");
}
/* 任务卡片消息内容任务id校验 */
if (StringUtils.isBlank(interactiveTaskCardMessage.getInteractive_taskcard().getTask_id())) {
throw new Exception("任务卡片消息内容任务id不能为空,任务id同一个应用发送的任务卡片消息的任务id不能重复只能由数字、字母和“_-@”组成最长支持128字节");
}
/* 任务卡片消息内容按钮组校验 */
if (Objects.isNull(interactiveTaskCardMessage.getInteractive_taskcard().getBtn())) {
throw new Exception("任务卡片消息内容按钮组不能为空!");
}
/* 任务卡片消息内容按钮组校验 */
interactiveTaskCardMessage.getInteractive_taskcard().getBtn().forEach(btn -> {
/* 任务卡片消息内容按钮组按钮key校验 */
if (StringUtils.isBlank(interactiveTaskCardMessage.getInteractive_taskcard().getTitle())) {
try {
throw new Exception("任务卡片消息内容按钮组按钮key不能为空按钮key值用户点击后会产生任务卡片回调事件回调事件会带上该key值只能由数字、字母和“_-@”组成最长支持128字节");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* 任务卡片消息内容按钮组阿牛name校验 */
if (StringUtils.isBlank(interactiveTaskCardMessage.getInteractive_taskcard().getTitle())) {
try {
throw new Exception("任务卡片消息内容按钮组阿牛name不能为空按钮名称最长支持18个字节超过则截断");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
/* 消息发送 */
interactiveTaskCardMessage.setAgentid(sysWxAuditConfig.getAgentId());
JSONObject jsonObject = HttpClientUtils.httpPost(weiXinProperties.buildQySendMsgUrl(sysWxAuditConfig.getCorpId(), sysWxAuditConfig.getCorpSecret()), (JSONObject) JSONObject.toJSON(interactiveTaskCardMessage));
return buildQywxMsgSendResult(jsonObject);
}
/**
* 企业微信消息发送基础信息校验
*
* @param baseMessage 企业微信消息体
*/
private void msgSendBaseCheck(BaseMessage baseMessage) throws Exception{
if (StringUtils.isAllBlank(baseMessage.getTouser(), baseMessage.getToparty(), baseMessage.getTotag())) {
throw new Exception("消息发送对象不能为空!");
}
}
/**
* 获取企业微信消息发送应用配置
*
* @param baseMessage 企业微信消息体
* @return SysWxAuditConfig 企业微信消息发送应用配置
*/
private SysWxAuditConfig buildSysWxAuditConfig(BaseMessage baseMessage) throws Exception{
return weiXinProperties.buildSysWxAuditConfig(baseMessage.getAppType());
}
/**
* 处理企业微信消息发送结果响应数据
* @param jsonObject 企业微信消息发送结果对象
* @return R 消息发送响应对象
*/
private CommonResult<Object> buildQywxMsgSendResult(JSONObject jsonObject) {
if (Objects.equals(jsonObject.getString(WX_MSG_RESPONSE_CODE), WX_MSG_RESPONSE_SUCCESS_CODE)) {
return CommonResult.success(jsonObject);
} else {
return CommonResult.error(100029,"发送中铝e办失败{}").setData(jsonObject);
}
}
}

View File

@@ -0,0 +1,100 @@
package com.zt.plat.module.system.service.msg.impl;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zt.plat.framework.common.util.http.HttpClientUtils;
import com.zt.plat.framework.common.util.security.DESUtil;
import com.zt.plat.module.system.dal.dataobject.msg.SysActiveMq;
import com.zt.plat.module.system.dal.dataobject.msg.SysActiveMqLog;
import com.zt.plat.module.system.dal.mysql.msg.SysActiveMqDao;
import com.zt.plat.module.system.framework.sms.core.enums.AuditConstants;
import com.zt.plat.module.system.framework.sms.core.enums.JmsConstant;
import com.zt.plat.module.system.service.msg.ISysRocketMqService;
import com.zt.plat.module.system.service.msg.config.HttpUrlConfig;
import jakarta.annotation.Resource;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
/**
* Activemq 消息队列 服务实现类
*
* @author Dy
* @since 2021-07-22
*/
@Service
@Transactional(rollbackFor = {Exception.class, Error.class}, propagation = Propagation.REQUIRES_NEW)
public class SysActiveMqServiceImpl extends ServiceImpl<SysActiveMqDao, SysActiveMqLog> implements ISysRocketMqService {
@Resource
private HttpUrlConfig httpUrlConfig;
/**
* 保存消息队列消费的消息信息
*
* @param listenerClassName 队列监听类名
* @param tm 消息对象
* @param jmsType 消息类型(QUEUE-队列/TOPIC-主题)
*/
@Override
public void saveByTextMessage(String listenerClassName, MessageExt tm, String jmsType) throws Exception {
this.saveByTextMessage(listenerClassName, tm, jmsType, JmsConstant.CONSUME_SUCCESS.getCode(), "消费成功");
}
/**
* 保存消息队列消费的消息信息
*
* @param listenerClassName 队列监听类名
* @param tm 消息对象
* @param jmsType 消息类型(QUEUE-队列/TOPIC-主题)
* @param consumeFlag 消费结果(success-成功/failure-失败)
* @param consumeMessage 消费失败消息
*/
@Override
public void saveByTextMessage(String listenerClassName, MessageExt tm, String jmsType, String consumeFlag, String consumeMessage) throws Exception {
SysActiveMqLog sysActiveMqLog = new SysActiveMqLog();
String json = new String(tm.getBody(), StandardCharsets.UTF_8);
com.alibaba.fastjson2.JSONObject jsonObject = JSON.parseObject(json);
sysActiveMqLog.setTitle(jsonObject.getString("title"))
.setDatetime(jsonObject.getString("datetime"))
.setPublisher(jsonObject.getString("publisher"))
.setContentType(jsonObject.getString("type"))
.setConsumeDatetime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")))
.setTextContent(jsonObject.getString("content"))
.setListenerClassName(listenerClassName)
.setJmsType(jmsType)
.setConsumeFlag(consumeFlag)
.setConsumeMessage(consumeMessage);
this.baseMapper.insert(sysActiveMqLog);
}
@Override
public void saveSunepsByTextMessage(String listenerClassName, MessageExt tm, String jmsType, String consumeFlag, String consumeMessage) throws Exception {
SysActiveMq sysActiveMq = new SysActiveMq();
String json = new String(tm.getBody(), StandardCharsets.UTF_8);
com.alibaba.fastjson2.JSONObject jsonObject = JSON.parseObject(json);
sysActiveMq.setTitle(jsonObject.getString("title"))
.setDatetime(jsonObject.getString("datetime"))
.setPublisher(jsonObject.getString("publisher"))
.setContentType(jsonObject.getString("type"))
.setConsumeDatetime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")))
.setTextContent(jsonObject.getString("content"))
.setListenerClassName(listenerClassName)
.setJmsType(jmsType)
.setConsumeFlag(consumeFlag)
.setConsumeMessage(consumeMessage);
String token = Arrays.toString(new DESUtil().encrypt(AuditConstants.TOKEN.getBytes()));
HttpClientUtils.httpPostByToken(httpUrlConfig.getHttpContextPath("suneps") + "/ieps/mobile/saveMqLog", JSONObject.toJSONString(sysActiveMq), token);
}
}

View File

@@ -0,0 +1,66 @@
package com.zt.plat.module.system.service.msg.impl;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zt.plat.module.system.dal.dataobject.msg.SysWxAuditConfig;
import com.zt.plat.module.system.service.msg.SysWxAuditConfigService;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
@Service
public class SysWxAuditConfigImpl implements SysWxAuditConfigService {
@Override
public boolean saveBatch(Collection<SysWxAuditConfig> entityList, int batchSize) {
return false;
}
@Override
public boolean saveOrUpdateBatch(Collection<SysWxAuditConfig> entityList, int batchSize) {
return false;
}
@Override
public boolean updateBatchById(Collection<SysWxAuditConfig> entityList, int batchSize) {
return false;
}
@Override
public boolean saveOrUpdate(SysWxAuditConfig entity) {
return false;
}
@Override
public SysWxAuditConfig getOne(Wrapper<SysWxAuditConfig> queryWrapper, boolean throwEx) {
return null;
}
@Override
public Optional<SysWxAuditConfig> getOneOpt(Wrapper<SysWxAuditConfig> queryWrapper, boolean throwEx) {
return Optional.empty();
}
@Override
public Map<String, Object> getMap(Wrapper<SysWxAuditConfig> queryWrapper) {
return Map.of();
}
@Override
public <V> V getObj(Wrapper<SysWxAuditConfig> queryWrapper, Function<? super Object, V> mapper) {
return null;
}
@Override
public BaseMapper<SysWxAuditConfig> getBaseMapper() {
return null;
}
@Override
public Class<SysWxAuditConfig> getEntityClass() {
return null;
}
}

View File

@@ -97,7 +97,7 @@ public class SmsSendServiceImpl implements SmsSendService {
Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams);
// 发送 MQ 消息,异步执行发送短信
//TODO 发送 MQ 消息,异步执行发送短信
if (isSend) {
smsProducer.sendSmsSendMessage(sendLogId, mobile, content, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);

View File

@@ -0,0 +1,24 @@
package com.zt.plat.module.system.util;
import org.apache.commons.text.StringSubstitutor;
import java.util.Map;
/**
* 字符串处理工具类
*
* @author luzemin
*/
public class StringSolveUtils {
/**
* 使用map集合替换字符串中${key}
*
* @param str 待替换字符串
* @param substituteMap 替代品属性集合
* @return 替换后字符串
*/
public static String placeholderReplace(String str, Map<String, Object> substituteMap) {
StringSubstitutor stringSubstitutor = new StringSubstitutor(substituteMap);
return stringSubstitutor.replace(str);
}
}

View File

@@ -1,202 +0,0 @@
--- #################### 数据库相关配置 ####################
spring:
# 数据源配置项
autoconfigure:
exclude:
datasource:
druid: # Druid 【监控】相关的全局配置
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
allow: # 设置白名单,不填则允许所有访问
url-pattern: /druid/*
login-username: # 控制台管理用户名和密码
login-password:
filter:
stat:
enabled: true
log-slow-sql: true # 慢 SQL 记录
slow-sql-millis: 100
merge-sql: true
wall:
config:
multi-statement-allow: true
dynamic: # 多数据源配置
druid: # Druid 【连接池】相关的全局配置
initial-size: 5 # 初始连接数
min-idle: 10 # 最小连接池数量
max-active: 20 # 最大连接池数量
max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
test-while-idle: true
test-on-borrow: false
test-on-return: false
primary: master
datasource:
master:
#url: jdbc:dm://172.16.46.247:1050?schema=RUOYI-VUE-PRO
#username: SYSDBA
#password: pgbsci6ddJ6Sqj@e
url: jdbc:dm://172.17.11.98:20870?schema=JYGK_TEST
username: SYSDBA
password: P@ssword25
slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
lazy: true # 开启懒加载,保证启动速度
#url: jdbc:dm://172.16.46.247:1050?schema=RUOYI-VUE-PRO
#username: SYSDBA
#password: pgbsci6ddJ6Sqj@e
url: jdbc:dm://172.17.11.98:20870?schema=JYGK_TEST
username: SYSDBA
password: P@ssword25
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
data:
redis:
host: 172.16.46.63 # 地址
port: 30379 # 端口
database: 1 # 数据库索引
# password: 123456 # 密码,建议生产环境开启
--- #################### MQ 消息队列相关配置 ####################
# rocketmq 配置项,对应 RocketMQProperties 配置类
rocketmq:
name-server: ${ROCKETMQ_NAME_SERVER:172.16.46.63:30876} # RocketMQ Namesrv使用环境变量
spring:
# RabbitMQ 配置项,对应 RabbitProperties 配置类
rabbitmq:
host: 127.0.0.1 # RabbitMQ 服务的地址
port: 5672 # RabbitMQ 服务的端口
username: guest # RabbitMQ 服务的账号
password: guest # RabbitMQ 服务的密码
# Kafka 配置项,对应 KafkaProperties 配置类
kafka:
bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
--- #################### 定时任务相关配置 ####################
xxl:
job:
admin:
addresses: http://172.16.46.63:30082/xxl-job-admin # 调度中心部署跟地址
--- #################### 服务保障相关配置 ####################
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################
# Actuator 监控端点的配置项
management:
endpoints:
web:
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
# Spring Boot Admin 配置项
spring:
boot:
admin:
# Spring Boot Admin Client 客户端的相关配置
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
# Spring Boot Admin Server 服务端的相关配置
context-path: /admin # 配置 Spring
# 日志文件配置
logging:
file:
name: ${LOG_PATH:./logs}/${spring.application.name}.log # 日志文件名,使用环境变量或相对路径
--- #################### 微信公众号、小程序相关配置 ####################
wx:
mp: # 公众号配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档
# app-id: wx041349c6f39b268b
# secret: 5abee519483bc9f8cb37ce280e814bd0
app-id: wx5b23ba7a5589ecbb # 测试号
secret: 2a7b3b20c537e52e74afd395eb85f61f
# 存储配置,解决 AccessToken 的跨节点的共享
config-storage:
type: RedisTemplate # 采用 RedisTemplate 操作 Redis会自动从 Spring 中获取
key-prefix: wx # Redis Key 的前缀
http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台
miniapp: # 小程序配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md 文档
# appid: wx62056c0d5e8db250
# secret: 333ae72f41552af1e998fe1f54e1584a
appid: wx63c280fe3248a3e7 # wenhualian的接口测试号
secret: 6f270509224a7ae1296bbf1c8cb97aed
config-storage:
type: RedisTemplate # 采用 RedisTemplate 操作 Redis会自动从 Spring 中获取
key-prefix: wa # Redis Key 的前缀
http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台
--- #################### ZT相关配置 ####################
justauth:
enabled: true
type:
DINGTALK: # 钉钉
client-id: dingvrnreaje3yqvzhxg
client-secret: i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI
ignore-check-redirect-uri: true
WECHAT_ENTERPRISE: # 企业微信
client-id: wwd411c69a39ad2e54
client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw
agent-id: 1000004
ignore-check-redirect-uri: true
# noinspection SpringBootApplicationYaml
WECHAT_MINI_PROGRAM: # 微信小程序
client-id: ${wx.miniapp.appid}
client-secret: ${wx.miniapp.secret}
ignore-check-redirect-uri: true
ignore-check-state: true # 微信小程序,不会使用到 state所以不进行校验
WECHAT_MP: # 微信公众号
client-id: ${wx.mp.app-id}
client-secret: ${wx.mp.secret}
ignore-check-redirect-uri: true
cache:
type: REDIS
prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::
timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟
seata:
enabled: true
application-id: system-server
tx-service-group: dev_tx_group
enable-auto-data-source-proxy: true
data-source-proxy-mode: AT
registry:
type: file
config:
type: file
service:
vgroupMapping:
default_tx_group: default
dev_tx_group: default
test_tx_group: default
prod_tx_group: default
default:
grouplist: 172.16.46.63:30088
client:
tm:
defaultGlobalTransactionTimeout: 60000
undo:
logTable: undo_log
dataValidation: true
logSerialization: jackson
zt:
databus:
# 变更消息生产者配置(集团侧数据源必须启用)
change:
producer:
enabled: true # 集团侧启用变更消息发送

View File

@@ -1,184 +0,0 @@
--- #################### 数据库相关配置 ####################
spring:
# 数据源配置项
autoconfigure:
exclude:
- de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置
datasource:
druid: # Druid 【监控】相关的全局配置
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
allow: # 设置白名单,不填则允许所有访问
url-pattern: /druid/*
login-username: # 控制台管理用户名和密码
login-password:
filter:
stat:
enabled: true
log-slow-sql: true # 慢 SQL 记录
slow-sql-millis: 100
merge-sql: true
wall:
config:
multi-statement-allow: true
dynamic: # 多数据源配置
druid: # Druid 【连接池】相关的全局配置
initial-size: 1 # 初始连接数
min-idle: 1 # 最小连接池数量
max-active: 20 # 最大连接池数量
max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
test-while-idle: true
test-on-borrow: false
test-on-return: false
primary: master
datasource:
master:
#url: jdbc:dm://172.16.46.247:1050?schema=RUOYI-VUE-PRO
#username: SYSDBA
#password: pgbsci6ddJ6Sqj@e
url: jdbc:dm://172.17.11.98:20870?schema=JYGK_TEST
username: SYSDBA
password: P@ssword25
slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
lazy: true # 开启懒加载,保证启动速度
#url: jdbc:dm://172.16.46.247:1050?schema=RUOYI-VUE-PRO
#username: SYSDBA
#password: pgbsci6ddJ6Sqj@e
url: jdbc:dm://172.17.11.98:20870?schema=JYGK_TEST
username: SYSDBA
password: P@ssword25
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
data:
redis:
host: 172.16.46.63 # 地址
port: 30379 # 端口
database: 0 # 数据库索引
# password: 123456 # 密码,建议生产环境开启
--- #################### MQ 消息队列相关配置 ####################
# rocketmq 配置项,对应 RocketMQProperties 配置类
rocketmq:
name-server: ${ROCKETMQ_NAME_SERVER:172.16.46.63:30876} # RocketMQ Namesrv使用环境变量
--- #################### 定时任务相关配置 ####################
xxl:
job:
enabled: true # 是否开启调度中心,默认为 true 开启
admin:
addresses: http://172.16.46.63:30082/xxl-job-admin # 调度中心部署跟地址
--- #################### 服务保障相关配置 ####################
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################
# Actuator 监控端点的配置项
management:
endpoints:
web:
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
# Spring Boot Admin 配置项
spring:
boot:
admin:
# Spring Boot Admin Client 客户端的相关配置
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
# 日志文件配置
logging:
level:
# 配置自己写的 MyBatis Mapper 打印日志
com.zt.plat.module.system.dal.mysql: debug
com.zt.plat.module.system.dal.mysql.sms.SmsChannelMapper: INFO # 配置 SmsChannelMapper 的日志级别为 info
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO ZT先禁用Spring Boot 3.X 存在部分错误的 WARN 提示
--- #################### 微信公众号、小程序相关配置 ####################
wx:
mp: # 公众号配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档
# app-id: wx041349c6f39b268b # 测试号(牛希尧提供的)
# secret: 5abee519483bc9f8cb37ce280e814bd0
app-id: wx5b23ba7a5589ecbb # 测试号(自己的)
secret: 2a7b3b20c537e52e74afd395eb85f61f
# app-id: wxa69ab825b163be19 # 测试号Kongdy 提供的)
# secret: bd4f9fab889591b62aeac0d7b8d8b4a0
# 存储配置,解决 AccessToken 的跨节点的共享
config-storage:
type: RedisTemplate # 采用 RedisTemplate 操作 Redis会自动从 Spring 中获取
key-prefix: wx # Redis Key 的前缀
http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台
miniapp: # 小程序配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md 文档
# appid: wx62056c0d5e8db250 # 测试号(牛希尧提供的)
# secret: 333ae72f41552af1e998fe1f54e1584a
# appid: wx63c280fe3248a3e7 # wenhualian的接口测试号
# secret: 6f270509224a7ae1296bbf1c8cb97aed
appid: wxc4598c446f8a9cb3 # 测试号Kongdy 提供的)
secret: 4a1a04e07f6a4a0751b39c3064a92c8b
# appid: wx66186af0759f47c9 # 测试号puhui 提供的)
# secret: 3218bcbd112cbc614c7264ceb20144ac
config-storage:
type: RedisTemplate # 采用 RedisTemplate 操作 Redis会自动从 Spring 中获取
key-prefix: wa # Redis Key 的前缀
http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台
--- #################### ZT相关配置 ####################
# ZT配置项设置当前项目所有自定义的配置
zt:
env: # 多环境的配置项
tag: ${HOSTNAME}
captcha:
enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试
security:
mock-enable: true
access-log: # 访问日志的配置项
enable: true
wxa-code:
env-version: develop # 小程序版本: 正式版为 "release";体验版为 "trial";开发版为 "develop"
wxa-subscribe-message:
miniprogram-state: developer # 跳转小程序类型:开发版为 “developer”体验版为 “trial”为正式版为 “formal”
justauth:
enabled: true
type:
DINGTALK: # 钉钉
client-id: dingvrnreaje3yqvzhxg
client-secret: i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI
ignore-check-redirect-uri: true
WECHAT_ENTERPRISE: # 企业微信
client-id: wwd411c69a39ad2e54
client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw
agent-id: 1000004
ignore-check-redirect-uri: true
# noinspection SpringBootApplicationYaml
WECHAT_MINI_PROGRAM: # 微信小程序
client-id: ${wx.miniapp.appid}
client-secret: ${wx.miniapp.secret}
ignore-check-redirect-uri: true
ignore-check-state: true # 微信小程序,不会使用到 state所以不进行校验
WECHAT_MP: # 微信公众号
client-id: ${wx.mp.app-id}
client-secret: ${wx.mp.secret}
ignore-check-redirect-uri: true
cache:
type: REDIS
prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::
timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟

View File

@@ -1,288 +0,0 @@
spring:
application:
name: system-server
profiles:
active: ${env.name}
#统一nacos配置使用 profile 管理
cloud:
nacos:
server-addr: ${config.server-addr} # Nacos 服务器地址
username: ${config.username} # Nacos 账号
password: ${config.password} # Nacos 密码
discovery: # 【配置中心】配置项
namespace: ${config.namespace} # 命名空间。这里使用 maven Profile 资源过滤进行动态替换
group: ${config.group} # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: ${config.namespace} # 命名空间。这里使用 maven Profile 资源过滤进行动态替换
group: ${config.group} # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
main:
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务
config:
import:
- optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置
# Servlet 配置
servlet:
# 文件上传相关配置项
multipart:
max-file-size: 16MB # 单个文件大小
max-request-size: 32MB # 设置总上传的文件大小
# Jackson 配置项
jackson:
serialization:
write-dates-as-timestamps: true # 设置 LocalDateTime 的格式,使用时间戳
write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401
write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳
fail-on-empty-beans: false # 允许序列化无属性的 Bean
# Cache 配置项
cache:
type: REDIS
redis:
time-to-live: 1h # 设置过期时间为 1 小时
server:
port: 48081
logging:
file:
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
--- #################### 接口文档配置 ####################
springdoc:
api-docs:
enabled: true # 1. 是否开启 Swagger 接文档的元数据
path: /v3/api-docs
swagger-ui:
enabled: true # 2.1 是否开启 Swagger 文档的官方 UI 界面
path: /swagger-ui
default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档
knife4j:
enable: false # TODO ZT需要关闭增强具体原因见https://github.com/xiaoymin/knife4j/issues/874
setting:
language: zh_cn
# MyBatis Plus 的配置项
mybatis-plus:
configuration:
map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
global-config:
db-config:
id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。
# id-type: AUTO # 自增 ID适合 MySQL 等直接自增的数据库
# id-type: INPUT # 用户输入 ID适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库
# id-type: ASSIGN_ID # 分配 ID默认使用雪花算法。注意Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
banner: false # 关闭控制台的 Banner 打印
type-aliases-package: ${zt.info.base-package}.dal.dataobject
encryptor:
password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成
mybatis-plus-join:
banner: false # 关闭控制台的 Banner 打印
# Spring Data Redis 配置
spring:
data:
redis:
repositories:
enabled: false # 项目未使用到 Spring Data Redis 的 Repository所以直接禁用保证启动速度
# VO 转换(数据翻译)相关
easy-trans:
is-enable-global: true # 启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口
--- #################### iWork 集成配置 ####################
iwork:
base-url: http://172.16.36.233:8080
# app-id: f47ac10b-58cc-4372-a567-0e02b2c3d479
app-id: f47ac10b-58cc-4372-a567-0e02b2c3d479
user-id: 9869
paths:
register: /api/ec/dev/auth/regist
apply-token: /api/ec/dev/auth/applytoken
user-info: /api/workflow/paService/getUserInfo
create-workflow: /api/workflow/paService/doCreateRequest
void-workflow: /api/workflow/paService/doCancelRequest
token:
ttl-seconds: 3600
client:
connect-timeout: 60s
response-timeout: 60s
org:
token-seed: 456465
paths:
subcompany-page: /api/hrm/resful/getHrmsubcompanyWithPage
department-page: /api/hrm/resful/getHrmdepartmentWithPage
job-title-page: /api/hrm/resful/getJobtitleInfoWithPage
user-page: /api/hrm/resful/getHrmUserInfoWithPage
sync-subcompany: /api/hrm/resful/synSubcompany
sync-department: /api/hrm/resful/synDepartment
sync-job-title: /api/hrm/resful/synJobtitle
sync-user: /api/hrm/resful/synHrmresource
workflow:
seal-workflow-id: "1753"
oa:
base-url: http://172.16.36.233:8080
app-id: a17ca6ca-88b0-463e-bffa-7995086bf225
paths:
get-token: /ssologin/getToken
check-token: /ssologin/checkToken
--- #################### RPC 远程调用相关配置 ####################
--- #################### 消息队列相关 ####################
# rocketmq 配置项,对应 RocketMQProperties 配置类
rocketmq:
# Producer 配置项
producer:
group: ${spring.application.name}_PRODUCER # 生产者分组
spring:
# Kafka 配置项,对应 KafkaProperties 配置类
kafka:
# Kafka Producer 配置项
producer:
acks: 1 # 0-不应答。1-leader 应答。all-所有 leader 和 follower 应答。
retries: 3 # 发送失败时,重试发送的次数
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer # 消息的 value 的序列化
# Kafka Consumer 配置项
consumer:
auto-offset-reset: earliest # 设置消费者分组最初的消费进度为 earliest 。可参考博客 https://blog.csdn.net/lishuangzhe7047/article/details/74530417 理解
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: '*'
# Kafka Consumer Listener 监听器配置
listener:
missing-topics-fatal: false # 消费监听接口监听的主题不存在时,默认会报错。所以通过设置为 false ,解决报错
--- #################### 定时任务相关配置 ####################
xxl:
job:
executor:
appname: ${spring.application.name} # 执行器 AppName
port: 0
ip: 172.16.234.132
logpath: ${user.home}/logs/xxl-job/${spring.application.name} # 执行器运行日志文件存储磁盘路径
accessToken: default_token # 执行器通讯TOKEN
--- #################### 验证码相关配置 ####################
aj:
captcha:
jigsaw: classpath:images/jigsaw # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径
pic-click: classpath:images/pic-click # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径
cache-type: redis # 缓存 local/redis...
cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存
timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行
type: blockPuzzle # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选
water-mark: 中国铜业 # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 UnicodeLinux 可能需要转 unicode
interference-options: 0 # 滑动干扰项(0/1/2)
req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false
req-get-lock-limit: 5 # 验证失败5次get接口锁定
req-get-lock-seconds: 10 # 验证失败后,锁定时间间隔
req-get-minute-limit: 30 # get 接口一分钟内请求数限制
req-check-minute-limit: 60 # check 接口一分钟内请求数限制
req-verify-minute-limit: 60 # verify 接口一分钟内请求数限制
--- #################### ZT相关配置 ####################
zt:
info:
version: 1.0.0
base-package: com.zt.plat.module.system
web:
admin-ui:
url: http://dashboard.zt.iocoder.cn # Admin 管理后台 UI 的地址
xss:
enable: false
exclude-urls: # 如下 url仅仅是为了演示去掉配置也没关系
- ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
swagger:
title: 管理后台
description: 提供管理员管理的所有功能
version: ${zt.info.version}
tenant: # 多租户相关配置项
enable: true
ignore-urls: # 登录时不校验租户,登录后强制用户选择后进入系统
# - /admin-api/system/auth/login
ignore-visit-urls:
- /admin-api/system/captcha/**
- /admin-api/system/user/profile/**
- /admin-api/system/auth/**
ignore-tables:
- system_seq
- system_seq_dtl
- system_seq_rcd
- system_sync_log
ignore-caches:
- user_role_ids
- permission_menu_ids
- oauth_client
- notify_template
- mail_account
- mail_template
- sms_template
sms-code: # 短信验证码相关的配置项
expire-times: 10m
send-frequency: 1m
send-maximum-quantity-per-day: 20
begin-code: 100000
end-code: 999999
# E办OAuth2配置文件
eban:
oauth2:
authorize-url: ${eban.oauth2.auth-server.base-url}/authorize
client-id: tyszhjyglxt
client-secret: fa821b567e59448e9acea3937529d1b4 # 需要从 e 办系统获取
redirect-uri: http://172.16.46.63:30080/system/oauth2/callback
response-type: code
auth-server:
base-url: http://10.2.137.42/idp/oauth2
client-id: ${eban.oauth2.client-id}
client-secret: ${eban.oauth2.client-secret}
token:
url: ${eban.oauth2.auth-server.base-url}/getToken
refresh-url: ${eban.oauth2.auth-server.base-url}/refreshToken
check-url: ${eban.oauth2.auth-server.base-url}/checkTokenValid
logout:
url: http://10.2.137.42/idp/profile/AllChannel/Redirect/GLO
entity-id: tyszhjyglxt
protocol: oauth
token-type: token
user-info:
url: ${eban.oauth2.auth-server.base-url}/getUserInfo
debug: false
sync:
encrypt-key: 25@jygk # 中铝 加密 key
eplat:
share:
url-prefix: http://10.1.7.110
client-id: ztjgj5gsJ2uU20900h9j
client-secret: DC82AD38EA764719B6DC7D71AAB4856C
scope: read
token-cache-key: eplat:cache:shareToken
refresh-token-cache-key: eplat:cache:shareRefreshToken
token-header-name: Xplat-Token
token-endpoint-path: /eplat/oauth/token
token-ttl: 5000s
refresh-token-ttl: 10000s

View File

@@ -0,0 +1,155 @@
# ================= ????? (PSCS) =================
# ?????????http url
pscs.commitUrl=/pscs/mobile/doSubmit
# ?????????http url
pscs.wxSubmitData=/pscs/mobile/doSubmitData
# ?????????????http url
pscs.commentUrl=/pscs/mobile/getApproveCommentById/
# ?????????????http url
pscs.todoUrl=/pscs/mobile/getApproveToDoByUserName/
# ?????????????http url
pscs.doneUrl=/pscs/mobile/getApproveDoneByUserName/
# ?????????????http url
pscs.finishUrl=/pscs/mobile/getApproveVariablesById/
# ???????????http url
pscs.storageUrl=http://cg.chncopper.com/storage/
# ???????????????
pscs.renderBpoContentUrl=/pscs/mobile/renderBpoContent
# ?????????
pscs.fineReportUrl=https://xs.chxz.com/FineReport/ReportServer
# ?????????
pscs.fileStorageUrl=https://xs.chxz.com/storage/
# ???????
pscs.fileViewUrl=https://xs.chxz.com/fv/onlinePreview
# ?????pscs????
pscs.httpContextPath=https://szh.chxz.com/omc
# ?????pscs????????
pscs.wxContextPath=http://xs.chxz.com/qywx
# ================= ????? (BSSIM) =================
# ?????????http url
bssim.commitUrl=/bssim/mobile/doSubmit
# ?????????????http url
bssim.commentUrl=/bssim/mobile/getApproveCommentById/
# ?????????????http url
bssim.todoUrl=/bssim/mobile/getApproveToDoByUserName/
# ?????????????http url
bssim.doneUrl=/bssim/mobile/getApproveDoneByUserName/
# ?????????????http url
bssim.finishUrl=/bssim/mobile/getApproveVariablesById/
# ???????????http url
bssim.storageUrl=http://cg.chncopper.com/storage/
# ???????????????
bssim.renderBpoContentUrl=/bssim/mobile/renderBpoContent
# ?????????
bssim.fineReportUrl=https://xs.chxz.com/FineReport/ReportServer
# ?????????
bssim.fileStorageUrl=https://xs.chxz.com/storage/
# ???????
bssim.fileViewUrl=https://xs.chxz.com/fv/onlinePreview
# ?????bssim????
bssim.httpContextPath=https://szh.chxz.com/bssim
# ?????bssim????????
bssim.wxContextPath=http://xs.chxz.com/qywx
# ================= ???? (SUNEPS) =================
# ????????http url
suneps.commitUrl=/ieps/mobile/doSubmit
# ????????????http url
suneps.commentUrl=/ieps/mobile/getApproveCommentById/
# ????????????http url
suneps.todoUrl=/ieps/mobile/getApproveToDoByUserName/
# ????????????http url
suneps.doneUrl=/ieps/mobile/getApproveDoneByUserName/
# ??????????http url
suneps.storageUrl=https://cg.chncopper.com/storage/
# ????suneps????
suneps.httpContextPath=http://172.16.34.30/ieps
# ????suneps????????
suneps.wxContextPath=http://xs.chxz.com/qywx
# ================= ???? (MARKET) =================
# ??????http url
market.commitUrl=/ieps/mobile/doSubmit
# ??????????http url
market.commentUrl=/ieps/mobile/getApproveCommentById/
# ?????????http url
market.todoUrl=/ieps/mobile/getApproveToDoByUserName/
# ??????????http url
market.doneUrl=/ieps/mobile/getApproveDoneByUserName/
# ????market????
market.httpContextPath=http://172.16.34.30/market
# ????market????????
market.wxContextPath=http://xs.chxz.com/qywx
# ================= ??????? (DZXS) =================
# ???????????http url
dzxs.commitUrl=/dzxs/mobile/doSubmit
# ???????????????http url
dzxs.commentUrl=/dzxs/mobile/getApproveCommentById/
# ???????????????http url
dzxs.todoUrl=/dzxs/mobile/getApproveToDoByUserName/
# ???????????????http url
dzxs.doneUrl=/dzxs/mobile/getApproveDoneByUserName/
# ???????????????http url
dzxs.finishUrl=/dzxs/mobile/getApproveVariablesById/
# ?????????????http url
dzxs.storageUrl=http://cg.chncopper.com/storage/
# ???????????
dzxs.fineReportUrl=https://xs.chxz.com/FineReport/ReportServer
# ?????????
dzxs.fileStorageUrl=https://xs.chxz.com/storage/
# ???????
dzxs.fileViewUrl=https://xs.chxz.com/fv/onlinePreview
# ???????dzxs????
dzxs.httpContextPath=http://10.198.0.197
# ???????dzxs????????
dzxs.wxContextPath=http://xs.chxz.com/qywx
# ================= ??????? (BOLI) =================
# ???????boli????http url
boli.commitUrl=/pscs/mobile/doSubmit
# ???????boli????????http url
boli.commentUrl=/pscs/mobile/getApproveCommentById/
# ???????boli????????http url
boli.todoUrl=/pscs/mobile/getApproveToDoByUserName/
# ???????boli????????http url
boli.doneUrl=/pscs/mobile/getApproveDoneByUserName/
# ???????boli????????http url
boli.finishUrl=/pscs/mobile/getApproveVariablesById/
# ???????boli??????http url
boli.storageUrl=http://cg.chncopper.com/storage/
# ???????boli????
boli.fineReportUrl=https://xs.chxz.com/FineReport/ReportServer
# ?????????
boli.fileStorageUrl=https://xs.chxz.com/storage/
# ???????
boli.fileViewUrl=https://xs.chxz.com/fv/onlinePreview
# ???????boli????
boli.httpContextPath=https://szh.chxz.com/boli
# ???????boli????????
boli.wxContextPath=http://xs.chxz.com/qywx
# ================= ??????? (LMPS) =================
# ???????????http url
lmps.commitUrl=/lmps/mobile/doSubmit
# ???????????????http url
lmps.commentUrl=/lmps/mobile/getApproveCommentById/
# ???????????????http url
lmps.todoUrl=/lmps/mobile/getApproveToDoByUserName/
# ???????????????http url
lmps.doneUrl=/lmps/mobile/getApproveDoneByUserName/
# ???????????????http url
lmps.finishUrl=/lmps/mobile/getApproveVariablesById/
# ?????????????http url
lmps.storageUrl=http://cg.chncopper.com/storage/
# ???????????
lmps.fineReportUrl=http://xs.chxz.com/FineReport/ReportServer
# ?????????
lmps.fileStorageUrl=https://xs.chxz.com/storage/
# ???????
lmps.fileViewUrl=https://xs.chxz.com/fv/onlinePreview
# ???????lmps????
lmps.httpContextPath=https://szh.chxz.com/lmps
# ???????lmps????????
lmps.wxContextPath=http://xs.chxz.com/qywx

View File

@@ -1,106 +0,0 @@
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<!-- 变量 zt.info.base-package基础业务包 -->
<springProperty scope="context" name="zt.info.base-package" source="zt.info.base-package"/>
<!-- 格式化输出:%d 表示日期,%X{tid} SkWalking 链路追踪编号,%thread 表示线程名,%-5level级别从左显示 5 个字符宽度,%msg日志消息%n是换行符 -->
<property name="PATTERN_DEFAULT" value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} | %highlight(${LOG_LEVEL_PATTERN:-%5p} ${PID:- }) | %boldYellow(%thread [%tid]) %boldGreen(%-40.40logger{39}) | %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<!--应用名称-->
<springProperty scope="context" name="spring.application.name" source="spring.application.name"/>
<!-- 日志输出路径 -->
<property name="LOG_DIR" value="${user.home}/logs/${spring.application.name}"/>
<!-- 控制台 Appender -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">     
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>${PATTERN_DEFAULT}</pattern>
</layout>
</encoder>
</appender>
<!-- 文件 Appender -->
<!-- 参考 Spring Boot 的 file-appender.xml 编写 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>${PATTERN_DEFAULT}</pattern>
</layout>
</encoder>
<!-- 日志文件名 -->
<file>${LOG_FILE}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 滚动后的日志文件名 -->
<fileNamePattern>${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}</fileNamePattern>
<!-- 启动服务时,是否清理历史日志,一般不建议清理 -->
<cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
<!-- 日志文件,到达多少容量,进行滚动 -->
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-50MB}</maxFileSize>
<!-- 日志文件的总大小0 表示不限制 -->
<totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
<!-- 日志文件的保留天数 -->
<maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30}</maxHistory>
</rollingPolicy>
</appender>
<!-- 异步写入日志,提升性能 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志。默认的,如果队列的 80% 已满,则会丢弃 TRACT、DEBUG、INFO 级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能。默认值为 256 -->
<queueSize>256</queueSize>
<appender-ref ref="FILE"/>
</appender>
<!-- SkyWalking GRPC 日志收集实现日志中心。注意SkyWalking 8.4.0 版本开始支持 -->
<appender name="GRPC" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>${PATTERN_DEFAULT}</pattern>
</layout>
</encoder>
</appender>
<!-- ERROR 级别日志 -->
<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}-error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory> <!-- 保留30天的日志 -->
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!--logback的日志级别 FATAL > ERROR > WARN > INFO > DEBUG-->
<!-- 本地环境 -->
<springProfile name="local,dev">
<root level="WARN">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ERROR"/>
<appender-ref ref="GRPC"/> <!-- 本地环境下,如果不想接入 SkyWalking 日志服务,可以注释掉本行 -->
<appender-ref ref="ASYNC"/> <!-- 本地环境下,如果不想打印日志,可以注释掉本行 -->
</root>
<!--针对不同的业务路径,配置dao层的sql打印日志级别为DEBUG-->
<logger name="com.zt.plat.module.system.dal.mysql" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
</springProfile>
<!-- 其它环境 -->
<springProfile name="dev,test,stage,prod,default">
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ERROR"/>
<appender-ref ref="ASYNC"/>
<appender-ref ref="GRPC"/>
</root>
</springProfile>
</configuration>

View File

@@ -0,0 +1,43 @@
# ================= ?????? =================
# ????-??access_token??url
qyAccessTokenUrl=https://qw.chinalco.com.cn:8443/cgi-bin/gettoken?corpid=CORP_ID&corpsecret=CORP_SECRET
# ????-??????url
qySendMsgUrl=https://qw.chinalco.com.cn:8443/cgi-bin/message/send?access_token=ACCESS_TOKEN
# ????-????????Oauth2????url
qyAuthorizeUrl=https://open.weixin.qq.com/connect/oauth2/authorize?appid=CORPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base&agentid=AGENTID&state=STATE#wechat_redirect
# ????-????????????url
qyUserInfoUrl=https://qw.chinalco.com.cn:8443/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
# ????url
qyUserUrl=https://qw.chinalco.com.cn:8443/cgi-bin/user/get?access_token=ACCESS_TOKEN&userid=USER_ID
# ????-??????url
qyUploadTempFileUrl=https://qw.chinalco.com.cn:8443/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE
# ????-????url
qyDeptCreateUrl=
# ????-????url
qyDeptUpdateUrl=
# ????-????url
qyDeptDeleteUrl=
# ????-??????url
qyDeptQueryListUrl=https://qw.chinalco.com.cn:8443/cgi-bin/department/list?access_token=ACCESS_TOKEN&id=DEPARTMENT_ID
# ????-??????url
qyMenuCreateUrl=
# ????-??????url
qyMenuDeleteUrl=
# ????-??????url
qyMenuQueryUrl=
# ????-ticket??url
ticketUrl=https://qw.chinalco.com.cn:8443/cgi-bin/get_jsapi_ticket?access_token=ACCESS_TOKEN

View File

@@ -0,0 +1,305 @@
package com.zt.plat.module.system.service.msg;
import cn.hutool.core.map.MapUtil;
import com.zt.plat.framework.common.core.KeyValue;
import com.zt.plat.framework.common.enums.CommonStatusEnum;
import com.zt.plat.framework.common.enums.UserTypeEnum;
import com.zt.plat.module.system.framework.sms.core.client.SmsClient;
import com.zt.plat.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.zt.plat.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import com.zt.plat.framework.test.core.ut.BaseMockitoUnitTest;
import com.zt.plat.module.system.dal.dataobject.sms.SmsChannelDO;
import com.zt.plat.module.system.dal.dataobject.sms.SmsTemplateDO;
import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO;
import com.zt.plat.module.system.mq.message.sms.SmsSendMessage;
import com.zt.plat.module.system.mq.producer.sms.SmsProducer;
import com.zt.plat.module.system.service.member.MemberService;
import com.zt.plat.module.system.service.sms.SmsChannelService;
import com.zt.plat.module.system.service.sms.SmsLogService;
import com.zt.plat.module.system.service.sms.SmsSendServiceImpl;
import com.zt.plat.module.system.service.sms.SmsTemplateService;
import com.zt.plat.module.system.service.user.AdminUserService;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static cn.hutool.core.util.RandomUtil.randomEle;
import static com.zt.plat.framework.test.core.util.AssertUtils.assertServiceException;
import static com.zt.plat.framework.test.core.util.RandomUtils.*;
import static com.zt.plat.module.system.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
@InjectMocks
private SmsSendServiceImpl smsSendService;
@Mock
private AdminUserService adminUserService;
@Mock
private MemberService memberService;
@Mock
private SmsChannelService smsChannelService;
@Mock
private SmsTemplateService smsTemplateService;
@Mock
private SmsLogService smsLogService;
@Mock
private SmsProducer smsProducer;
@Test
public void testSendSingleSmsToAdmin() {
// 准备参数
Long userId = randomLongId();
String templateCode = randomString();
Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234")
.put("op", "login").build();
// mock adminUserService 的方法
AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setMobile("15601691300"));
when(adminUserService.getUser(eq(userId))).thenReturn(user);
// mock SmsTemplateService 的方法
SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> {
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setContent("验证码为{code}, 操作为{op}");
o.setParams(Lists.newArrayList("code", "op"));
});
when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template);
String content = randomString();
when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams)))
.thenReturn(content);
// mock SmsChannelService 的方法
SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel);
// mock SmsLogService 的方法
Long smsLogId = randomLongId();
when(smsLogService.createSmsLog(eq(user.getMobile()), eq(userId), eq(UserTypeEnum.ADMIN.getValue()), eq(Boolean.TRUE), eq(template),
eq(content), eq(templateParams))).thenReturn(smsLogId);
// 调用
Long resultSmsLogId = smsSendService.sendSingleSmsToAdmin(null, userId, templateCode, templateParams);
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(user.getMobile()), eq(content),
eq(template.getChannelId()), eq(template.getApiTemplateId()),
eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login"))));
}
@Test
public void testSendSingleSmsToUser() {
// 准备参数
Long userId = randomLongId();
String templateCode = randomString();
Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234")
.put("op", "login").build();
// mock memberService 的方法
String mobile = "15601691300";
when(memberService.getMemberUserMobile(eq(userId))).thenReturn(mobile);
// mock SmsTemplateService 的方法
SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> {
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setContent("验证码为{code}, 操作为{op}");
o.setParams(Lists.newArrayList("code", "op"));
});
when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template);
String content = randomString();
when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams)))
.thenReturn(content);
// mock SmsChannelService 的方法
SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel);
// mock SmsLogService 的方法
Long smsLogId = randomLongId();
when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(UserTypeEnum.MEMBER.getValue()), eq(Boolean.TRUE), eq(template),
eq(content), eq(templateParams))).thenReturn(smsLogId);
// 调用
Long resultSmsLogId = smsSendService.sendSingleSmsToMember(null, userId, templateCode, templateParams);
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), eq(content),
eq(template.getChannelId()), eq(template.getApiTemplateId()),
eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login"))));
}
/**
* 发送成功,当短信模板开启时
*/
@Test
public void testSendSingleSms_successWhenSmsTemplateEnable() {
// 准备参数
String mobile = randomString();
Long userId = randomLongId();
Integer userType = randomEle(UserTypeEnum.values()).getValue();
String templateCode = randomString();
Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234")
.put("op", "login").build();
// mock SmsTemplateService 的方法
SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> {
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setContent("验证码为{code}, 操作为{op}");
o.setParams(Lists.newArrayList("code", "op"));
});
when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template);
String content = randomString();
when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams)))
.thenReturn(content);
// mock SmsChannelService 的方法
SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel);
// mock SmsLogService 的方法
Long smsLogId = randomLongId();
when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.TRUE), eq(template),
eq(content), eq(templateParams))).thenReturn(smsLogId);
// 调用
Long resultSmsLogId = smsSendService.sendSingleSms(mobile, userId, userType, templateCode, templateParams);
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), eq(content),
eq(template.getChannelId()), eq(template.getApiTemplateId()),
eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login"))));
}
/**
* 发送成功,当短信模板关闭时
*/
@Test
public void testSendSingleSms_successWhenSmsTemplateDisable() {
// 准备参数
String mobile = randomString();
Long userId = randomLongId();
Integer userType = randomEle(UserTypeEnum.values()).getValue();
String templateCode = randomString();
Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234")
.put("op", "login").build();
// mock SmsTemplateService 的方法
SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> {
o.setStatus(CommonStatusEnum.DISABLE.getStatus());
o.setContent("验证码为{code}, 操作为{op}");
o.setParams(Lists.newArrayList("code", "op"));
});
when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template);
String content = randomString();
when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams)))
.thenReturn(content);
// mock SmsChannelService 的方法
SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel);
// mock SmsLogService 的方法
Long smsLogId = randomLongId();
when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.FALSE), eq(template),
eq(content), eq(templateParams))).thenReturn(smsLogId);
// 调用
Long resultSmsLogId = smsSendService.sendSingleSms(mobile, userId, userType, templateCode, templateParams);
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsProducer, times(0)).sendSmsSendMessage(anyLong(), anyString(), anyString(),
anyLong(), any(), anyList());
}
@Test
public void testCheckSmsTemplateValid_notExists() {
// 准备参数
String templateCode = randomString();
// mock 方法
// 调用,并断言异常
assertServiceException(() -> smsSendService.validateSmsTemplate(templateCode),
SMS_SEND_TEMPLATE_NOT_EXISTS);
}
@Test
public void testBuildTemplateParams_paramMiss() {
// 准备参数
SmsTemplateDO template = randomPojo(SmsTemplateDO.class,
o -> o.setParams(Lists.newArrayList("code")));
Map<String, Object> templateParams = new HashMap<>();
// mock 方法
// 调用,并断言异常
assertServiceException(() -> smsSendService.buildTemplateParams(template, templateParams),
SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, "code");
}
@Test
public void testCheckMobile_notExists() {
// 准备参数
// mock 方法
// 调用,并断言异常
assertServiceException(() -> smsSendService.validateMobile(null),
SMS_SEND_MOBILE_NOT_EXISTS);
}
@Test
public void testSendBatchNotify() {
// 准备参数
// mock 方法
// 调用
UnsupportedOperationException exception = Assertions.assertThrows(
UnsupportedOperationException.class,
() -> smsSendService.sendBatchSms(null, null, null, null, null)
);
// 断言
assertEquals("暂时不支持该操作,感兴趣可以实现该功能哟!", exception.getMessage());
}
@Test
@SuppressWarnings("unchecked")
public void testDoSendSms() throws Throwable {
// 准备参数
SmsSendMessage message = randomPojo(SmsSendMessage.class, o -> o.setContent(randomString()));
// mock SmsClientFactory 的方法
SmsClient smsClient = spy(SmsClient.class);
when(smsChannelService.getSmsClient(eq(message.getChannelId()))).thenReturn(smsClient);
// mock SmsClient 的方法
SmsSendRespDTO sendResult = randomPojo(SmsSendRespDTO.class);
when(smsClient.sendSms(eq(message.getLogId()), eq(message.getMobile()), eq(message.getContent()), eq(message.getApiTemplateId()),
eq(message.getTemplateParams()))).thenReturn(sendResult);
// 调用
smsSendService.doSendSms(message);
// 断言
verify(smsLogService).updateSmsSendResult(eq(message.getLogId()),
eq(sendResult.getSuccess()), eq(sendResult.getApiCode()),
eq(sendResult.getApiMsg()), eq(sendResult.getApiRequestId()), eq(sendResult.getSerialNo()));
}
@Test
public void testReceiveSmsStatus() throws Throwable {
// 准备参数
String channelCode = randomString();
String text = randomString();
// mock SmsClientFactory 的方法
SmsClient smsClient = spy(SmsClient.class);
when(smsChannelService.getSmsClient(eq(channelCode))).thenReturn(smsClient);
// mock SmsClient 的方法
List<SmsReceiveRespDTO> receiveResults = randomPojoList(SmsReceiveRespDTO.class);
when(smsClient.parseSmsReceiveStatus(eq(text))).thenReturn(receiveResults);
// 调用
smsSendService.receiveSmsStatus(channelCode, text);
// 断言
for (SmsReceiveRespDTO result : receiveResults) {
verify(smsLogService).updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSuccess()),
eq(result.getReceiveTime()), eq(result.getErrorCode()), eq(result.getErrorMsg()));
}
}
}