【V3.5.0】1、【新增】轻量级定时任务 SmartJob;2、【新增】站内信;3、【新增】个人中心;4、【新增】岗位管理;5、【优化】部门员工管理

This commit is contained in:
zhuoda 2024-07-16 00:20:02 +08:00
parent 23e8ea55e1
commit 716b6303e3
504 changed files with 59745 additions and 1110 deletions

View File

@ -36,6 +36,7 @@
<commons-lang3.version>3.12.0</commons-lang3.version> <commons-lang3.version>3.12.0</commons-lang3.version>
<commons-collections4.version>4.4</commons-collections4.version> <commons-collections4.version>4.4</commons-collections4.version>
<commons-codec.version>1.13</commons-codec.version> <commons-codec.version>1.13</commons-codec.version>
<commons-text.version>1.9</commons-text.version>
<xerces.version>2.12.0</xerces.version> <xerces.version>2.12.0</xerces.version>
<easy-excel.version>3.3.2</easy-excel.version> <easy-excel.version>3.3.2</easy-excel.version>
<poi.version>5.2.4</poi.version> <poi.version>5.2.4</poi.version>
@ -52,6 +53,7 @@
<bcprov.version>1.59</bcprov.version> <bcprov.version>1.59</bcprov.version>
<jackson-datatype-jsr310.version>2.13.4</jackson-datatype-jsr310.version> <jackson-datatype-jsr310.version>2.13.4</jackson-datatype-jsr310.version>
<smartdb.version>1.2.0</smartdb.version> <smartdb.version>1.2.0</smartdb.version>
<redisson.version>3.25.0</redisson.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
@ -193,6 +195,12 @@
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${commons-text.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.logging.log4j</groupId> <groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-spring-boot</artifactId> <artifactId>log4j-spring-boot</artifactId>
@ -306,6 +314,33 @@
<artifactId>smartdb</artifactId> <artifactId>smartdb</artifactId>
<version>${smartdb.version}</version> <version>${smartdb.version}</version>
</dependency> </dependency>
<!-- redisson 排除和依赖data-27 为了springboot2.x 和 java8 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</exclusion>
<exclusion>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-32</artifactId>
</exclusion>
<exclusion>
<artifactId>objenesis</artifactId>
<groupId>org.objenesis</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-27</artifactId>
<version>${redisson.version}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

View File

@ -25,6 +25,7 @@ public class AdminSwaggerTagConst extends SwaggerTagConst {
public static final String OA_INVOICE = "OA办公-发票信息"; public static final String OA_INVOICE = "OA办公-发票信息";
public static final String OA_NOTICE = "OA办公-通知公告"; public static final String OA_NOTICE = "OA办公-通知公告";
} }
@ -48,6 +49,8 @@ public class AdminSwaggerTagConst extends SwaggerTagConst {
public static final String SYSTEM_ROLE_MENU = "系统-角色-菜单"; public static final String SYSTEM_ROLE_MENU = "系统-角色-菜单";
public static final String SYSTEM_POSITION = "系统-职务管理";
} }

View File

@ -3,6 +3,8 @@ package net.lab1024.sa.admin.module.system.department.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
/** /**
* 部门 * 部门
* *
@ -33,4 +35,10 @@ public class DepartmentVO {
@Schema(description = "排序") @Schema(description = "排序")
private Integer sort; private Integer sort;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "创建时间")
private LocalDateTime createTime;
} }

View File

@ -52,6 +52,20 @@ public class EmployeeController {
return employeeService.updateEmployee(employeeUpdateForm); return employeeService.updateEmployee(employeeUpdateForm);
} }
@Operation(summary = "更新登录人信息 @author 善逸")
@PostMapping("/employee/update/login")
public ResponseDTO<String> updateByLogin(@Valid @RequestBody EmployeeUpdateForm employeeUpdateForm) {
employeeUpdateForm.setEmployeeId(SmartRequestUtil.getRequestUserId());
return employeeService.updateEmployee(employeeUpdateForm);
}
@Operation(summary = "更新登录人头像 @author 善逸")
@PostMapping("/employee/update/avatar")
public ResponseDTO<String> updateAvatar(@Valid @RequestBody EmployeeUpdateAvatarForm employeeUpdateAvatarForm) {
employeeUpdateAvatarForm.setEmployeeId(SmartRequestUtil.getRequestUserId());
return employeeService.updateAvatar(employeeUpdateAvatarForm);
}
@Operation(summary = "更新员工禁用/启用状态 @author 卓大") @Operation(summary = "更新员工禁用/启用状态 @author 卓大")
@GetMapping("/employee/update/disabled/{employeeId}") @GetMapping("/employee/update/disabled/{employeeId}")
@SaCheckPermission("system:employee:disabled") @SaCheckPermission("system:employee:disabled")

View File

@ -38,6 +38,11 @@ public class EmployeeEntity {
*/ */
private String actualName; private String actualName;
/**
* 头像
*/
private String avatar;
/** /**
* 性别 * 性别
*/ */
@ -53,6 +58,11 @@ public class EmployeeEntity {
*/ */
private Long departmentId; private Long departmentId;
/**
* 职务级别ID
*/
private Long positionId;
/** /**
* 是否为超级管理员: 0 不是1是 * 是否为超级管理员: 0 不是1是
*/ */

View File

@ -53,4 +53,12 @@ public class EmployeeAddForm {
@Schema(description = "角色列表") @Schema(description = "角色列表")
private List<Long> roleIdList; private List<Long> roleIdList;
@Schema(description = "备注")
@Length(max = 30, message = "备注最多200字符")
private String remark;
@Schema(description = "职务级别ID")
private Long positionId;
} }

View File

@ -0,0 +1,28 @@
package net.lab1024.sa.admin.module.system.employee.domain.form;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import net.lab1024.sa.base.common.util.SmartVerificationUtil;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
/**
* 修改登录人头像
*
* @Author 1024创新实验室: 善逸
* @Date 2024年6月30日00:26:35
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Data
public class EmployeeUpdateAvatarForm {
@Schema(hidden = true)
private Long employeeId;
@Schema(description = "头像")
@NotBlank(message = "头像不能为空哦")
private String avatar;
}

View File

@ -55,4 +55,11 @@ public class EmployeeVO {
@Schema(description = "角色名称列表") @Schema(description = "角色名称列表")
private List<String> roleNameList; private List<String> roleNameList;
@Schema(description = "职务ID")
private Long positionId;
@Schema(description = "职务名称")
private String positionName;
} }

View File

@ -12,6 +12,9 @@ import net.lab1024.sa.admin.module.system.employee.domain.entity.EmployeeEntity;
import net.lab1024.sa.admin.module.system.employee.domain.form.*; import net.lab1024.sa.admin.module.system.employee.domain.form.*;
import net.lab1024.sa.admin.module.system.employee.domain.vo.EmployeeVO; import net.lab1024.sa.admin.module.system.employee.domain.vo.EmployeeVO;
import net.lab1024.sa.admin.module.system.employee.manager.EmployeeManager; import net.lab1024.sa.admin.module.system.employee.manager.EmployeeManager;
import net.lab1024.sa.admin.module.system.login.service.LoginService;
import net.lab1024.sa.admin.module.system.position.dao.PositionDao;
import net.lab1024.sa.admin.module.system.position.domain.entity.PositionEntity;
import net.lab1024.sa.admin.module.system.role.dao.RoleEmployeeDao; import net.lab1024.sa.admin.module.system.role.dao.RoleEmployeeDao;
import net.lab1024.sa.admin.module.system.role.domain.vo.RoleEmployeeVO; import net.lab1024.sa.admin.module.system.role.domain.vo.RoleEmployeeVO;
import net.lab1024.sa.base.common.code.UserErrorCode; import net.lab1024.sa.base.common.code.UserErrorCode;
@ -24,6 +27,7 @@ import net.lab1024.sa.base.common.util.SmartPageUtil;
import net.lab1024.sa.base.module.support.securityprotect.service.ProtectPasswordService; import net.lab1024.sa.base.module.support.securityprotect.service.ProtectPasswordService;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
@ -62,6 +66,13 @@ public class EmployeeService {
@Resource @Resource
private ProtectPasswordService protectPasswordService; private ProtectPasswordService protectPasswordService;
@Resource
@Lazy
private LoginService loginService;
@Resource
private PositionDao positionDao;
public EmployeeEntity getById(Long employeeId) { public EmployeeEntity getById(Long employeeId) {
return employeeDao.selectById(employeeId); return employeeDao.selectById(employeeId);
} }
@ -69,7 +80,6 @@ public class EmployeeService {
/** /**
* 查询员工列表 * 查询员工列表
*
*/ */
public ResponseDTO<PageResult<EmployeeVO>> queryEmployee(EmployeeQueryForm employeeQueryForm) { public ResponseDTO<PageResult<EmployeeVO>> queryEmployee(EmployeeQueryForm employeeQueryForm) {
employeeQueryForm.setDeletedFlag(false); employeeQueryForm.setDeletedFlag(false);
@ -86,16 +96,22 @@ public class EmployeeService {
return ResponseDTO.ok(pageResult); return ResponseDTO.ok(pageResult);
} }
List<Long> employeeIdList = employeeList.stream().map(EmployeeVO::getEmployeeId).collect(Collectors.toList());
// 查询员工角色 // 查询员工角色
List<RoleEmployeeVO> roleEmployeeEntityList = roleEmployeeDao.selectRoleByEmployeeIdList(employeeIdList); List<Long> employeeIdList = employeeList.stream().map(EmployeeVO::getEmployeeId).collect(Collectors.toList());
List<RoleEmployeeVO> roleEmployeeEntityList = employeeIdList.isEmpty() ? Collections.emptyList() : roleEmployeeDao.selectRoleByEmployeeIdList(employeeIdList);
Map<Long, List<Long>> employeeRoleIdListMap = roleEmployeeEntityList.stream().collect(Collectors.groupingBy(RoleEmployeeVO::getEmployeeId, Collectors.mapping(RoleEmployeeVO::getRoleId, Collectors.toList()))); Map<Long, List<Long>> employeeRoleIdListMap = roleEmployeeEntityList.stream().collect(Collectors.groupingBy(RoleEmployeeVO::getEmployeeId, Collectors.mapping(RoleEmployeeVO::getRoleId, Collectors.toList())));
Map<Long, List<String>> employeeRoleNameListMap = roleEmployeeEntityList.stream().collect(Collectors.groupingBy(RoleEmployeeVO::getEmployeeId, Collectors.mapping(RoleEmployeeVO::getRoleName, Collectors.toList()))); Map<Long, List<String>> employeeRoleNameListMap = roleEmployeeEntityList.stream().collect(Collectors.groupingBy(RoleEmployeeVO::getEmployeeId, Collectors.mapping(RoleEmployeeVO::getRoleName, Collectors.toList())));
// 查询员工职位
List<Long> positionIdList = employeeList.stream().map(EmployeeVO::getPositionId).filter(Objects::nonNull).collect(Collectors.toList());
List<PositionEntity> positionEntityList = positionIdList.isEmpty() ? Collections.emptyList() : positionDao.selectBatchIds(positionIdList);
Map<Long, String> positionNameMap = positionEntityList.stream().collect(Collectors.toMap(PositionEntity::getPositionId, PositionEntity::getPositionName));
employeeList.forEach(e -> { employeeList.forEach(e -> {
e.setRoleIdList(employeeRoleIdListMap.getOrDefault(e.getEmployeeId(), Lists.newArrayList())); e.setRoleIdList(employeeRoleIdListMap.getOrDefault(e.getEmployeeId(), Lists.newArrayList()));
e.setRoleNameList(employeeRoleNameListMap.getOrDefault(e.getEmployeeId(), Lists.newArrayList())); e.setRoleNameList(employeeRoleNameListMap.getOrDefault(e.getEmployeeId(), Lists.newArrayList()));
e.setDepartmentName(departmentService.getDepartmentPath(e.getDepartmentId())); e.setDepartmentName(departmentService.getDepartmentPath(e.getDepartmentId()));
e.setPositionName(positionNameMap.get(e.getPositionId()));
}); });
PageResult<EmployeeVO> pageResult = SmartPageUtil.convert2PageResult(pageParam, employeeList); PageResult<EmployeeVO> pageResult = SmartPageUtil.convert2PageResult(pageParam, employeeList);
return ResponseDTO.ok(pageResult); return ResponseDTO.ok(pageResult);
@ -103,7 +119,6 @@ public class EmployeeService {
/** /**
* 新增员工 * 新增员工
*
*/ */
public synchronized ResponseDTO<String> addEmployee(EmployeeAddForm employeeAddForm) { public synchronized ResponseDTO<String> addEmployee(EmployeeAddForm employeeAddForm) {
// 校验名称是否重复 // 校验名称是否重复
@ -143,7 +158,6 @@ public class EmployeeService {
/** /**
* 更新员工 * 更新员工
*
*/ */
public synchronized ResponseDTO<String> updateEmployee(EmployeeUpdateForm employeeUpdateForm) { public synchronized ResponseDTO<String> updateEmployee(EmployeeUpdateForm employeeUpdateForm) {
@ -183,12 +197,38 @@ public class EmployeeService {
// 更新数据 // 更新数据
employeeManager.updateEmployee(entity, employeeUpdateForm.getRoleIdList()); employeeManager.updateEmployee(entity, employeeUpdateForm.getRoleIdList());
// 清除员工缓存
loginService.clearLoginEmployeeCache(employeeId);
return ResponseDTO.ok();
}
/**
* 更新登录人头像
*
* @param employeeUpdateAvatarForm
* @return
*/
public ResponseDTO<String> updateAvatar(EmployeeUpdateAvatarForm employeeUpdateAvatarForm) {
Long employeeId = employeeUpdateAvatarForm.getEmployeeId();
EmployeeEntity employeeEntity = employeeDao.selectById(employeeId);
if (employeeEntity == null) {
return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST);
}
// 更新头像
EmployeeEntity updateEntity = new EmployeeEntity();
updateEntity.setEmployeeId(employeeId);
updateEntity.setAvatar(employeeUpdateAvatarForm.getAvatar());
employeeDao.updateById(updateEntity);
// 清除员工缓存
loginService.clearLoginEmployeeCache(employeeId);
return ResponseDTO.ok(); return ResponseDTO.ok();
} }
/** /**
* 更新禁用/启用状态 * 更新禁用/启用状态
*
*/ */
public ResponseDTO<String> updateDisableFlag(Long employeeId) { public ResponseDTO<String> updateDisableFlag(Long employeeId) {
if (null == employeeId) { if (null == employeeId) {
@ -210,7 +250,6 @@ public class EmployeeService {
/** /**
* 批量删除员工 * 批量删除员工
*
*/ */
public ResponseDTO<String> batchUpdateDeleteFlag(List<Long> employeeIdList) { public ResponseDTO<String> batchUpdateDeleteFlag(List<Long> employeeIdList) {
if (CollectionUtils.isEmpty(employeeIdList)) { if (CollectionUtils.isEmpty(employeeIdList)) {
@ -239,7 +278,6 @@ public class EmployeeService {
/** /**
* 批量更新部门 * 批量更新部门
*
*/ */
public ResponseDTO<String> batchUpdateDepartment(EmployeeBatchUpdateDepartmentForm batchUpdateDepartmentForm) { public ResponseDTO<String> batchUpdateDepartment(EmployeeBatchUpdateDepartmentForm batchUpdateDepartmentForm) {
List<Long> employeeIdList = batchUpdateDepartmentForm.getEmployeeIdList(); List<Long> employeeIdList = batchUpdateDepartmentForm.getEmployeeIdList();
@ -262,7 +300,6 @@ public class EmployeeService {
/** /**
* 更新密码 * 更新密码
*
*/ */
public ResponseDTO<String> updatePassword(EmployeeUpdatePasswordForm updatePasswordForm) { public ResponseDTO<String> updatePassword(EmployeeUpdatePasswordForm updatePasswordForm) {
Long employeeId = updatePasswordForm.getEmployeeId(); Long employeeId = updatePasswordForm.getEmployeeId();
@ -299,7 +336,6 @@ public class EmployeeService {
/** /**
* 获取某个部门的员工信息 * 获取某个部门的员工信息
*
*/ */
public ResponseDTO<List<EmployeeVO>> getAllEmployeeByDepartmentId(Long departmentId, Boolean disabledFlag) { public ResponseDTO<List<EmployeeVO>> getAllEmployeeByDepartmentId(Long departmentId, Boolean disabledFlag) {
List<EmployeeEntity> employeeEntityList = employeeDao.selectByDepartmentId(departmentId, disabledFlag); List<EmployeeEntity> employeeEntityList = employeeDao.selectByDepartmentId(departmentId, disabledFlag);
@ -326,7 +362,6 @@ public class EmployeeService {
/** /**
* 重置密码 * 重置密码
*
*/ */
public ResponseDTO<String> resetPassword(Integer employeeId) { public ResponseDTO<String> resetPassword(Integer employeeId) {
String password = protectPasswordService.randomPassword(); String password = protectPasswordService.randomPassword();
@ -336,7 +371,6 @@ public class EmployeeService {
/** /**
* 获取 加密后 的密码 * 获取 加密后 的密码
*
*/ */
public static String getEncryptPwd(String password) { public static String getEncryptPwd(String password) {
return DigestUtils.md5Hex(String.format(PASSWORD_SALT_FORMAT, password)); return DigestUtils.md5Hex(String.format(PASSWORD_SALT_FORMAT, password));
@ -345,7 +379,6 @@ public class EmployeeService {
/** /**
* 查询全部员工 * 查询全部员工
*
*/ */
public ResponseDTO<List<EmployeeVO>> queryAllEmployee(Boolean disabledFlag) { public ResponseDTO<List<EmployeeVO>> queryAllEmployee(Boolean disabledFlag) {
List<EmployeeVO> employeeList = employeeDao.selectEmployeeByDisabledAndDeleted(disabledFlag, Boolean.FALSE); List<EmployeeVO> employeeList = employeeDao.selectEmployeeByDisabledAndDeleted(disabledFlag, Boolean.FALSE);
@ -354,7 +387,6 @@ public class EmployeeService {
/** /**
* 根据登录名获取员工 * 根据登录名获取员工
*
*/ */
public EmployeeEntity getByLoginName(String loginName) { public EmployeeEntity getByLoginName(String loginName) {
return employeeDao.getByLoginName(loginName, null); return employeeDao.getByLoginName(loginName, null);

View File

@ -31,6 +31,9 @@ public class RequestEmployee implements RequestUser {
@Schema(description = "员工名称") @Schema(description = "员工名称")
private String actualName; private String actualName;
@Schema(description = "头像")
private String avatar;
@SchemaEnum(GenderEnum.class) @SchemaEnum(GenderEnum.class)
private Integer gender; private Integer gender;
@ -43,9 +46,15 @@ public class RequestEmployee implements RequestUser {
@Schema(description = "部门名称") @Schema(description = "部门名称")
private String departmentName; private String departmentName;
@Schema(description = "是否禁用")
private Boolean disabledFlag;
@Schema(description = "是否为超管") @Schema(description = "是否为超管")
private Boolean administratorFlag; private Boolean administratorFlag;
@Schema(description = "备注")
private String remark;
@Schema(description = "请求ip") @Schema(description = "请求ip")
private String ip; private String ip;

View File

@ -32,6 +32,7 @@ import net.lab1024.sa.base.module.support.captcha.CaptchaService;
import net.lab1024.sa.base.module.support.captcha.domain.CaptchaVO; import net.lab1024.sa.base.module.support.captcha.domain.CaptchaVO;
import net.lab1024.sa.base.module.support.config.ConfigKeyEnum; import net.lab1024.sa.base.module.support.config.ConfigKeyEnum;
import net.lab1024.sa.base.module.support.config.ConfigService; import net.lab1024.sa.base.module.support.config.ConfigService;
import net.lab1024.sa.base.module.support.file.service.IFileStorageService;
import net.lab1024.sa.base.module.support.loginlog.LoginLogResultEnum; import net.lab1024.sa.base.module.support.loginlog.LoginLogResultEnum;
import net.lab1024.sa.base.module.support.loginlog.LoginLogService; import net.lab1024.sa.base.module.support.loginlog.LoginLogService;
import net.lab1024.sa.base.module.support.loginlog.domain.LoginLogEntity; import net.lab1024.sa.base.module.support.loginlog.domain.LoginLogEntity;
@ -39,6 +40,7 @@ import net.lab1024.sa.base.module.support.loginlog.domain.LoginLogVO;
import net.lab1024.sa.base.module.support.securityprotect.domain.LoginFailEntity; import net.lab1024.sa.base.module.support.securityprotect.domain.LoginFailEntity;
import net.lab1024.sa.base.module.support.securityprotect.service.ProtectLoginService; import net.lab1024.sa.base.module.support.securityprotect.service.ProtectLoginService;
import net.lab1024.sa.base.module.support.securityprotect.service.ProtectPasswordService; import net.lab1024.sa.base.module.support.securityprotect.service.ProtectPasswordService;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -110,6 +112,9 @@ public class LoginService implements StpInterface {
@Resource @Resource
private ProtectPasswordService profectPasswordService; private ProtectPasswordService profectPasswordService;
@Resource
private IFileStorageService fileStorageService;
/** /**
* 获取验证码 * 获取验证码
*/ */
@ -252,6 +257,15 @@ public class LoginService implements StpInterface {
DepartmentVO department = departmentService.getDepartmentById(employeeEntity.getDepartmentId()); DepartmentVO department = departmentService.getDepartmentById(employeeEntity.getDepartmentId());
requestEmployee.setDepartmentName(null == department ? StringConst.EMPTY : department.getName()); requestEmployee.setDepartmentName(null == department ? StringConst.EMPTY : department.getName());
// 头像信息
String avatar = employeeEntity.getAvatar();
if(StringUtils.isNotBlank(avatar)){
ResponseDTO<String> getFileUrl = fileStorageService.getFileUrl(avatar);
if(BooleanUtils.isTrue(getFileUrl.getOk())){
requestEmployee.setAvatar(getFileUrl.getData());
}
}
return requestEmployee; return requestEmployee;
} }
@ -341,6 +355,15 @@ public class LoginService implements StpInterface {
return ResponseDTO.ok(); return ResponseDTO.ok();
} }
/**
* 清除员工登录缓存
* @param employeeId
*/
public void clearLoginEmployeeCache(Long employeeId){
// 清空登录信息缓存
loginEmployeeCache.remove(employeeId);
}
/** /**
* 保存登录日志 * 保存登录日志
*/ */

View File

@ -0,0 +1,75 @@
package net.lab1024.sa.admin.module.system.position.controller;
import net.lab1024.sa.admin.constant.AdminSwaggerTagConst;
import net.lab1024.sa.admin.module.system.position.domain.form.PositionAddForm;
import net.lab1024.sa.admin.module.system.position.domain.form.PositionQueryForm;
import net.lab1024.sa.admin.module.system.position.domain.form.PositionUpdateForm;
import net.lab1024.sa.admin.module.system.position.domain.vo.PositionVO;
import net.lab1024.sa.admin.module.system.position.service.PositionService;
import net.lab1024.sa.base.common.domain.ValidateList;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.domain.PageResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
/**
* 职务表 Controller
*
* @Author kaiyun
* @Date 2024-06-23 23:31:38
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@RestController
@Tag(name = AdminSwaggerTagConst.System.SYSTEM_POSITION)
public class PositionController {
@Resource
private PositionService positionService;
@Operation(summary = "分页查询 @author kaiyun")
@PostMapping("/position/queryPage")
public ResponseDTO<PageResult<PositionVO>> queryPage(@RequestBody @Valid PositionQueryForm queryForm) {
return ResponseDTO.ok(positionService.queryPage(queryForm));
}
@Operation(summary = "添加 @author kaiyun")
@PostMapping("/position/add")
public ResponseDTO<String> add(@RequestBody @Valid PositionAddForm addForm) {
return positionService.add(addForm);
}
@Operation(summary = "更新 @author kaiyun")
@PostMapping("/position/update")
public ResponseDTO<String> update(@RequestBody @Valid PositionUpdateForm updateForm) {
return positionService.update(updateForm);
}
@Operation(summary = "批量删除 @author kaiyun")
@PostMapping("/position/batchDelete")
public ResponseDTO<String> batchDelete(@RequestBody ValidateList<Long> idList) {
return positionService.batchDelete(idList);
}
@Operation(summary = "单个删除 @author kaiyun")
@GetMapping("/position/delete/{positionId}")
public ResponseDTO<String> batchDelete(@PathVariable Long positionId) {
return positionService.delete(positionId);
}
@Operation(summary = "不分页查询 @author kaiyun")
@GetMapping("/position/queryList")
public ResponseDTO<List<PositionVO>> queryList() {
return ResponseDTO.ok(positionService.queryList());
}
}

View File

@ -0,0 +1,41 @@
package net.lab1024.sa.admin.module.system.position.dao;
import java.util.List;
import net.lab1024.sa.admin.module.system.position.domain.entity.PositionEntity;
import net.lab1024.sa.admin.module.system.position.domain.form.PositionQueryForm;
import net.lab1024.sa.admin.module.system.position.domain.vo.PositionVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Component;
/**
* 职务表 Dao
*
* @Author kaiyun
* @Date 2024-06-23 23:31:38
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Mapper
@Component
public interface PositionDao extends BaseMapper<PositionEntity> {
/**
* 分页 查询
*
* @param page
* @param queryForm
* @return
*/
List<PositionVO> queryPage(Page page, @Param("queryForm") PositionQueryForm queryForm);
/**
* 查询
* @param deletedFlag
* @return
*/
List<PositionVO> queryList(@Param("deletedFlag") Boolean deletedFlag);
}

View File

@ -0,0 +1,61 @@
package net.lab1024.sa.admin.module.system.position.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 职务表 实体类
*
* @Author kaiyun
* @Date 2024-06-23 23:31:38
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Data
@TableName("t_position")
public class PositionEntity {
/**
* 职务ID
*/
@TableId(type = IdType.AUTO)
private Long positionId;
/**
* 职务名称
*/
private String positionName;
/**
* 职级
*/
private String level;
/**
* 排序
*/
private Integer sort;
/**
* 备注
*/
private String remark;
private Boolean deletedFlag;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,34 @@
package net.lab1024.sa.admin.module.system.position.domain.form;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Data;
/**
* 职务表 新建表单
*
* @Author kaiyun
* @Date 2024-06-23 23:31:38
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Data
public class PositionAddForm {
@Schema(description = "职务名称", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "职务名称 不能为空")
private String positionName;
@Schema(description = "职级")
private String level;
@Schema(description = "排序")
@NotNull(message = "排序不能为空")
private Integer sort;
@Schema(description = "备注")
private String remark;
}

View File

@ -0,0 +1,23 @@
package net.lab1024.sa.admin.module.system.position.domain.form;
import net.lab1024.sa.base.common.domain.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 职务表 分页查询表单
*
* @Author kaiyun
* @Date 2024-06-23 23:31:38
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Data
public class PositionQueryForm extends PageParam{
@Schema(description = "关键字查询")
private String keywords;
@Schema(hidden = true)
private Boolean deletedFlag;
}

View File

@ -0,0 +1,24 @@
package net.lab1024.sa.admin.module.system.position.domain.form;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotNull;
import lombok.Data;
/**
* 职务表 更新表单
*
* @Author kaiyun
* @Date 2024-06-23 23:31:38
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Data
public class PositionUpdateForm extends PositionAddForm {
@Schema(description = "职务ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "职务ID 不能为空")
private Long positionId;
}

View File

@ -0,0 +1,40 @@
package net.lab1024.sa.admin.module.system.position.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 职务表 列表VO
*
* @Author kaiyun
* @Date 2024-06-23 23:31:38
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Data
public class PositionVO {
@Schema(description = "职务ID")
private Long positionId;
@Schema(description = "职务名称")
private String positionName;
@Schema(description = "职级")
private String level;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "备注")
private String remark;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,20 @@
package net.lab1024.sa.admin.module.system.position.manager;
import net.lab1024.sa.admin.module.system.position.dao.PositionDao;
import net.lab1024.sa.admin.module.system.position.domain.entity.PositionEntity;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* 职务表 Manager
*
* @Author kaiyun
* @Date 2024-06-23 23:31:38
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Service
public class PositionManager extends ServiceImpl<PositionDao, PositionEntity> {
}

View File

@ -0,0 +1,105 @@
package net.lab1024.sa.admin.module.system.position.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import net.lab1024.sa.admin.module.system.position.dao.PositionDao;
import net.lab1024.sa.admin.module.system.position.domain.entity.PositionEntity;
import net.lab1024.sa.admin.module.system.position.domain.form.PositionAddForm;
import net.lab1024.sa.admin.module.system.position.domain.form.PositionQueryForm;
import net.lab1024.sa.admin.module.system.position.domain.form.PositionUpdateForm;
import net.lab1024.sa.admin.module.system.position.domain.vo.PositionVO;
import net.lab1024.sa.base.common.domain.PageResult;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartBeanUtil;
import net.lab1024.sa.base.common.util.SmartPageUtil;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* 职务表 Service
*
* @Author kaiyun
* @Date 2024-06-23 23:31:38
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Service
public class PositionService {
@Resource
private PositionDao positionDao;
/**
* 分页查询
*
* @param queryForm
* @return
*/
public PageResult<PositionVO> queryPage(PositionQueryForm queryForm) {
queryForm.setDeletedFlag(Boolean.FALSE);
Page<?> page = SmartPageUtil.convert2PageQuery(queryForm);
List<PositionVO> list = positionDao.queryPage(page, queryForm);
PageResult<PositionVO> pageResult = SmartPageUtil.convert2PageResult(page, list);
return pageResult;
}
/**
* 添加
*/
public ResponseDTO<String> add(PositionAddForm addForm) {
PositionEntity positionEntity = SmartBeanUtil.copy(addForm, PositionEntity.class);
positionDao.insert(positionEntity);
return ResponseDTO.ok();
}
/**
* 更新
*
* @param updateForm
* @return
*/
public ResponseDTO<String> update(PositionUpdateForm updateForm) {
PositionEntity positionEntity = SmartBeanUtil.copy(updateForm, PositionEntity.class);
positionDao.updateById(positionEntity);
return ResponseDTO.ok();
}
/**
* 批量删除
*
* @param idList
* @return
*/
public ResponseDTO<String> batchDelete(List<Long> idList) {
if (CollectionUtils.isEmpty(idList)){
return ResponseDTO.ok();
}
positionDao.deleteBatchIds(idList);
return ResponseDTO.ok();
}
/**
* 单个删除
*/
public ResponseDTO<String> delete(Long positionId) {
if (null == positionId){
return ResponseDTO.ok();
}
positionDao.deleteById(positionId);
return ResponseDTO.ok();
}
/**
* 分页查询
*
* @return
*/
public List<PositionVO> queryList() {
List<PositionVO> list = positionDao.queryList(Boolean.FALSE);
return list;
}
}

View File

@ -2,10 +2,10 @@ package net.lab1024.sa.admin.module.system.role.manager;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import net.lab1024.sa.admin.module.system.role.dao.RoleEmployeeDao; import net.lab1024.sa.admin.module.system.role.dao.RoleEmployeeDao;
import net.lab1024.sa.admin.module.system.role.domain.entity.RoleEmployeeEntity;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import net.lab1024.sa.admin.module.system.role.domain.entity.RoleEmployeeEntity;
import java.util.List; import java.util.List;

View File

@ -5,7 +5,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import net.lab1024.sa.base.common.controller.SupportBaseController; import net.lab1024.sa.base.common.controller.SupportBaseController;
import net.lab1024.sa.base.common.domain.PageResult; import net.lab1024.sa.base.common.domain.PageResult;
import net.lab1024.sa.base.common.domain.RequestUser;
import net.lab1024.sa.base.common.domain.ResponseDTO; import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartRequestUtil;
import net.lab1024.sa.base.constant.SwaggerTagConst; import net.lab1024.sa.base.constant.SwaggerTagConst;
import net.lab1024.sa.base.module.support.loginlog.LoginLogService; import net.lab1024.sa.base.module.support.loginlog.LoginLogService;
import net.lab1024.sa.base.module.support.loginlog.domain.LoginLogQueryForm; import net.lab1024.sa.base.module.support.loginlog.domain.LoginLogQueryForm;
@ -39,5 +41,14 @@ public class AdminLoginLogController extends SupportBaseController {
return loginLogService.queryByPage(queryForm); return loginLogService.queryByPage(queryForm);
} }
@Operation(summary = "分页查询当前登录人信息 @author 善逸")
@PostMapping("/loginLog/page/query/login")
public ResponseDTO<PageResult<LoginLogVO>> queryByPageLogin(@RequestBody LoginLogQueryForm queryForm) {
RequestUser requestUser = SmartRequestUtil.getRequestUser();
queryForm.setUserId(requestUser.getUserId());
queryForm.setUserType(requestUser.getUserType().getValue());
return loginLogService.queryByPage(queryForm);
}
} }

View File

@ -5,7 +5,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import net.lab1024.sa.base.common.controller.SupportBaseController; import net.lab1024.sa.base.common.controller.SupportBaseController;
import net.lab1024.sa.base.common.domain.PageResult; import net.lab1024.sa.base.common.domain.PageResult;
import net.lab1024.sa.base.common.domain.RequestUser;
import net.lab1024.sa.base.common.domain.ResponseDTO; import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartRequestUtil;
import net.lab1024.sa.base.constant.SwaggerTagConst; import net.lab1024.sa.base.constant.SwaggerTagConst;
import net.lab1024.sa.base.module.support.operatelog.OperateLogService; import net.lab1024.sa.base.module.support.operatelog.OperateLogService;
import net.lab1024.sa.base.module.support.operatelog.domain.OperateLogQueryForm; import net.lab1024.sa.base.module.support.operatelog.domain.OperateLogQueryForm;
@ -44,4 +46,13 @@ public class AdminOperateLogController extends SupportBaseController {
return operateLogService.detail(operateLogId); return operateLogService.detail(operateLogId);
} }
@Operation(summary = "分页查询当前登录人信息 @author 善逸")
@PostMapping("/operateLog/page/query/login")
public ResponseDTO<PageResult<OperateLogVO>> queryByPageLogin(@RequestBody OperateLogQueryForm queryForm) {
RequestUser requestUser = SmartRequestUtil.getRequestUser();
queryForm.setOperateUserId(requestUser.getUserId());
queryForm.setOperateUserType(requestUser.getUserType().getValue());
return operateLogService.queryByPage(queryForm);
}
} }

View File

@ -0,0 +1,79 @@
package net.lab1024.sa.admin.module.system.support;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import net.lab1024.sa.base.common.controller.SupportBaseController;
import net.lab1024.sa.base.common.domain.PageResult;
import net.lab1024.sa.base.common.domain.RequestUser;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartRequestUtil;
import net.lab1024.sa.base.constant.SwaggerTagConst;
import net.lab1024.sa.base.module.support.job.api.SmartJobService;
import net.lab1024.sa.base.module.support.job.api.domain.*;
import net.lab1024.sa.base.module.support.job.config.SmartJobAutoConfiguration;
import net.lab1024.sa.base.module.support.repeatsubmit.annoation.RepeatSubmit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* 定时任务 管理接口
*
* @author huke
* @date 2024/6/17 20:41
*/
@Tag(name = SwaggerTagConst.Support.JOB)
@RestController
@ConditionalOnBean(SmartJobAutoConfiguration.class)
public class AdminSmartJobController extends SupportBaseController {
@Autowired
private SmartJobService jobService;
@Operation(summary = "定时任务-立即执行 @huke")
@PostMapping("/job/execute")
@RepeatSubmit
public ResponseDTO<String> execute(@RequestBody @Valid SmartJobExecuteForm executeForm) {
RequestUser requestUser = SmartRequestUtil.getRequestUser();
executeForm.setUpdateName(requestUser.getUserName());
return jobService.execute(executeForm);
}
@Operation(summary = "定时任务-查询详情 @huke")
@GetMapping("/job/{jobId}")
public ResponseDTO<SmartJobVO> queryJobInfo(@PathVariable Integer jobId) {
return jobService.queryJobInfo(jobId);
}
@Operation(summary = "定时任务-分页查询 @huke")
@PostMapping("/job/query")
public ResponseDTO<PageResult<SmartJobVO>> queryJob(@RequestBody @Valid SmartJobQueryForm queryForm) {
return jobService.queryJob(queryForm);
}
@Operation(summary = "定时任务-更新-任务信息 @huke")
@PostMapping("/job/update")
@RepeatSubmit
public ResponseDTO<String> updateJob(@RequestBody @Valid SmartJobUpdateForm updateForm) {
RequestUser requestUser = SmartRequestUtil.getRequestUser();
updateForm.setUpdateName(requestUser.getUserName());
return jobService.updateJob(updateForm);
}
@Operation(summary = "定时任务-更新-开启状态 @huke")
@PostMapping("/job/update/enabled")
@RepeatSubmit
public ResponseDTO<String> updateJobEnabled(@RequestBody @Valid SmartJobEnabledUpdateForm updateForm) {
RequestUser requestUser = SmartRequestUtil.getRequestUser();
updateForm.setUpdateName(requestUser.getUserName());
return jobService.updateJobEnabled(updateForm);
}
@Operation(summary = "定时任务-执行记录-分页查询 @huke")
@PostMapping("/job/log/query")
public ResponseDTO<PageResult<SmartJobLogVO>> queryJobLog(@RequestBody @Valid SmartJobLogQueryForm queryForm) {
return jobService.queryJobLog(queryForm);
}
}

View File

@ -8,7 +8,7 @@
# 项目配置: 名称、日志目录 # 项目配置: 名称、日志目录
project: project:
name: sa-admin name: sa-admin
log-directory: /home/logs/smart_admin_v3/${project.name}/${spring.profiles.active} log-directory: ${localPath:/home}/logs/smart_admin_v3/${project.name}/${spring.profiles.active}
# 项目端口和url根路径 # 项目端口和url根路径
server: server:
@ -21,12 +21,6 @@ spring:
profiles: profiles:
active: '@profiles.active@' active: '@profiles.active@'
# swagger文档
swagger:
host: localhost:${server.port}
tag-class: net.lab1024.sa.admin.constant.AdminSwaggerTagConst
####################################### 安全等级保护 相关配置 ################################################## ####################################### 安全等级保护 相关配置 ##################################################
# # # #
# 建议开启 "三级等保" 所要求的配置,具体如下: # # 建议开启 "三级等保" 所要求的配置,具体如下: #

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.lab1024.sa.admin.module.system.position.dao.PositionDao">
<!-- 分页查询 -->
<select id="queryPage" resultType="net.lab1024.sa.admin.module.system.position.domain.vo.PositionVO">
SELECT
*
FROM t_position
<where>
deleted_flag = #{queryForm.deletedFlag}
<!--关键字查询-->
<if test="queryForm.keywords != null and queryForm.keywords != ''">
AND INSTR(t_position.position_name,#{queryForm.keywords})
</if>
</where>
</select>
<select id="queryList" resultType="net.lab1024.sa.admin.module.system.position.domain.vo.PositionVO">
SELECT *
FROM t_position
where deleted_flag = #{deletedFlag}
</select>
</mapper>

View File

@ -21,12 +21,6 @@ spring:
profiles: profiles:
active: '@profiles.active@' active: '@profiles.active@'
# swagger文档
swagger:
host: localhost:${server.port}
tag-class: net.lab1024.sa.admin.constant.AdminSwaggerTagConst
####################################### 安全等级保护 相关配置 ################################################## ####################################### 安全等级保护 相关配置 ##################################################
# # # #
# 建议开启 "三级等保" 所要求的配置,具体如下: # # 建议开启 "三级等保" 所要求的配置,具体如下: #

View File

@ -21,12 +21,6 @@ spring:
profiles: profiles:
active: '@profiles.active@' active: '@profiles.active@'
# swagger文档
swagger:
host: localhost:${server.port}
tag-class: net.lab1024.sa.admin.constant.AdminSwaggerTagConst
####################################### 安全等级保护 相关配置 ################################################## ####################################### 安全等级保护 相关配置 ##################################################
# # # #
# 建议开启 "三级等保" 所要求的配置,具体如下: # # 建议开启 "三级等保" 所要求的配置,具体如下: #

View File

@ -8,11 +8,11 @@
# 项目配置: 名称、日志目录 # 项目配置: 名称、日志目录
project: project:
name: sa-admin name: sa-admin
log-directory: /home/logs/smart_admin_v3/${project.name}/${spring.profiles.active} log-directory: /home/project/smartadmin/sit/log
# 项目端口和url根路径 # 项目端口和url根路径
server: server:
port: 1024 port: 11024
servlet: servlet:
context-path: / context-path: /
@ -21,12 +21,6 @@ spring:
profiles: profiles:
active: '@profiles.active@' active: '@profiles.active@'
# swagger文档
swagger:
host: localhost:${server.port}
tag-class: net.lab1024.sa.admin.constant.AdminSwaggerTagConst
####################################### 安全等级保护 相关配置 ################################################## ####################################### 安全等级保护 相关配置 ##################################################
# # # #
# 建议开启 "三级等保" 所要求的配置,具体如下: # # 建议开启 "三级等保" 所要求的配置,具体如下: #

View File

@ -113,6 +113,11 @@
<artifactId>commons-pool2</artifactId> <artifactId>commons-pool2</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
<artifactId>spring-mock</artifactId> <artifactId>spring-mock</artifactId>
@ -264,6 +269,14 @@
<version>${smartdb.version}</version> <version>${smartdb.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-27</artifactId>
</dependency>
</dependencies> </dependencies>

View File

@ -40,7 +40,7 @@ public class DictValueVoSerializer extends JsonSerializer<String> {
List<DictValueVO> dictValueVOList = Lists.newArrayList(); List<DictValueVO> dictValueVOList = Lists.newArrayList();
valueCodeList.forEach(e->{ valueCodeList.forEach(e->{
if(StringUtils.isNotBlank(e)){ if(StringUtils.isNotBlank(e)){
DictValueVO dictValueVO = dictCacheService.selectValueByValueCode(value); DictValueVO dictValueVO = dictCacheService.selectValueByValueCode(e);
if(dictValueVO != null){ if(dictValueVO != null){
dictValueVOList.add(dictValueVO); dictValueVOList.add(dictValueVO);
} }

View File

@ -0,0 +1,25 @@
package net.lab1024.sa.base.common.json.serializer.enumeration;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import net.lab1024.sa.base.common.enumeration.BaseEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 枚举类 序列化 注解
*
* @author huke
* @date 2024年6月29日
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = EnumSerializer.class, nullsUsing = EnumSerializer.class)
public @interface EnumSerialize {
Class<? extends BaseEnum> value();
}

View File

@ -0,0 +1,53 @@
package net.lab1024.sa.base.common.json.serializer.enumeration;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import net.lab1024.sa.base.common.constant.StringConst;
import net.lab1024.sa.base.common.enumeration.BaseEnum;
import net.lab1024.sa.base.common.util.SmartEnumUtil;
import net.lab1024.sa.base.common.util.SmartStringUtil;
import java.io.IOException;
import java.util.stream.Collectors;
/**
* 枚举 序列化
*
* @author huke
* @date 2024年6月29日
*/
public class EnumSerializer extends JsonSerializer<Object> implements ContextualSerializer {
private Class<? extends BaseEnum> enumClazz;
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeObject(value);
String fieldName = gen.getOutputContext().getCurrentName() + "Desc";
Object desc;
// 多个枚举类 逗号分割
if (value instanceof String && String.valueOf(value).contains(StringConst.SEPARATOR)) {
desc = SmartStringUtil.splitConvertToIntList(String.valueOf(value), StringConst.SEPARATOR)
.stream().map(e -> SmartEnumUtil.getEnumDescByValue(e, enumClazz)).collect(Collectors.toList());
} else {
BaseEnum anEnum = SmartEnumUtil.getEnumByValue(value, enumClazz);
desc = null != anEnum ? anEnum.getDesc() : null;
}
gen.writeObjectField(fieldName, desc);
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
EnumSerialize annotation = property.getAnnotation(EnumSerialize.class);
if (null == annotation) {
return prov.findValueSerializer(property.getType(), property);
}
enumClazz = annotation.value();
return this;
}
}

View File

@ -4,8 +4,12 @@ import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.constant.StringConst; import net.lab1024.sa.base.common.constant.StringConst;
import org.lionsoul.ip2region.xdb.Searcher; import org.lionsoul.ip2region.xdb.Searcher;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Enumeration;
import java.util.List; import java.util.List;
/** /**
@ -81,4 +85,39 @@ public class SmartIpUtil {
} }
} }
/**
* 获取本机第一个ip
*
* @return
*/
public static String getLocalFirstIp() {
List<String> list = getLocalIp();
return list.size() > 0 ? list.get(0) : null;
}
/**
* 获取本机ip
*
* @return
*/
public static List<String> getLocalIp() {
List<String> ipList = new ArrayList<>();
try {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface networkInterface = networkInterfaces.nextElement();
Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
while (inetAddresses.hasMoreElements()) {
InetAddress inetAddress = inetAddresses.nextElement();
// 排除回环地址和IPv6地址
if (!inetAddress.isLoopbackAddress() && !inetAddress.getHostAddress().contains(StringConst.COLON)) {
ipList.add(inetAddress.getHostAddress());
}
}
}
} catch (SocketException e) {
e.printStackTrace();
}
return ipList;
}
} }

View File

@ -1,5 +1,6 @@
package net.lab1024.sa.base.config; package net.lab1024.sa.base.config;
import com.google.common.collect.Lists;
import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Contact;
@ -10,14 +11,25 @@ import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.constant.RequestHeaderConst; import net.lab1024.sa.base.common.constant.RequestHeaderConst;
import net.lab1024.sa.base.common.swagger.SmartOperationCustomizer; import net.lab1024.sa.base.common.swagger.SmartOperationCustomizer;
import net.lab1024.sa.base.constant.SwaggerTagConst; import net.lab1024.sa.base.constant.SwaggerTagConst;
import org.springdoc.core.GroupedOpenApi; import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.*;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.providers.JavadocProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Optional;
/** /**
* springdoc-openapi 配置 * springdoc-openapi 配置
* * nginx配置前缀时如果需要访问/swagger-ui/index.html需添加额外nginx配置
* location /v3/api-docs/ {
* proxy_pass http://127.0.0.1:11024/v3/api-docs/;
* }
* @Author 1024创新实验室-主任: 卓大 * @Author 1024创新实验室-主任: 卓大
* @Date 2020-03-25 22:54:46 * @Date 2020-03-25 22:54:46
* @Wechat zhuoda1024 * @Wechat zhuoda1024
@ -28,6 +40,11 @@ import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
@Conditional(SystemEnvironmentConfig.class) @Conditional(SystemEnvironmentConfig.class)
public class SwaggerConfig { public class SwaggerConfig {
/**
* 用于解决/swagger-ui/index.html页面ServersUrl 测试环境部署错误问题
*/
@Value("${springdoc.swagger-ui.server-base-url:''}")
private String serverBaseUrl;
public static final String[] SWAGGER_WHITELIST = { public static final String[] SWAGGER_WHITELIST = {
"/swagger-ui/**", "/swagger-ui/**",
@ -78,4 +95,37 @@ public class SwaggerConfig {
.addOperationCustomizer(new SmartOperationCustomizer()) .addOperationCustomizer(new SmartOperationCustomizer())
.build(); .build();
} }
/**
* 以下代码可以用于设置 /swagger-ui/index.html 的serverBaseUrl
* 如果使用knife4j则不需要
* @param openAPI
* @param securityParser
* @param springDocConfigProperties
* @param propertyResolverUtils
* @param openApiBuilderCustomizers
* @param serverBaseUrlCustomizers
* @param javadocProvider
* @return
*/
@Bean
public OpenAPIService openApiBuilder(Optional<OpenAPI> openAPI,
SecurityService securityParser,
SpringDocConfigProperties springDocConfigProperties,
PropertyResolverUtils propertyResolverUtils,
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
Optional<JavadocProvider> javadocProvider) {
List<ServerBaseUrlCustomizer> list = Lists.newArrayList(new ServerBaseUrlCustomizer() {
@Override
public String customize(String baseUrl) {
if (StringUtils.isNotBlank(serverBaseUrl)) {
return serverBaseUrl;
}
return baseUrl;
}
});
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties,
propertyResolverUtils, openApiBuilderCustomizers, Optional.of(list), javadocProvider);
}
} }

View File

@ -48,6 +48,10 @@ public class SwaggerTagConst {
public static final String TABLE_COLUMN = "业务支撑-列自定义"; public static final String TABLE_COLUMN = "业务支撑-列自定义";
public static final String PROTECT = "业务支撑-网络安全"; public static final String PROTECT = "业务支撑-网络安全";
public static final String JOB = "业务支撑-定时任务";
public static final String MESSAGE = "业务支撑-消息";
} }
} }

View File

@ -9,7 +9,6 @@ import net.lab1024.sa.base.common.domain.SystemEnvironment;
import net.lab1024.sa.base.common.enumeration.SystemEnvironmentEnum; import net.lab1024.sa.base.common.enumeration.SystemEnvironmentEnum;
import net.lab1024.sa.base.common.exception.BusinessException; import net.lab1024.sa.base.common.exception.BusinessException;
import org.springframework.beans.TypeMismatchException; import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException; import org.springframework.validation.BindException;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
@ -59,10 +58,6 @@ public class GlobalExceptionHandler {
@ResponseBody @ResponseBody
@ExceptionHandler({TypeMismatchException.class, BindException.class}) @ExceptionHandler({TypeMismatchException.class, BindException.class})
public ResponseDTO<?> paramExceptionHandler(Exception e) { public ResponseDTO<?> paramExceptionHandler(Exception e) {
if (!systemEnvironment.isProd()) {
log.error("全局参数异常,URL:{}", getCurrentRequestUrl(), e);
}
if (e instanceof BindException) { if (e instanceof BindException) {
if (e instanceof MethodArgumentNotValidException) { if (e instanceof MethodArgumentNotValidException) {
List<FieldError> fieldErrors = ((MethodArgumentNotValidException) e).getBindingResult().getFieldErrors(); List<FieldError> fieldErrors = ((MethodArgumentNotValidException) e).getBindingResult().getFieldErrors();
@ -75,7 +70,6 @@ public class GlobalExceptionHandler {
String errorMsg = UserErrorCode.PARAM_ERROR.getMsg() + ":" + error; String errorMsg = UserErrorCode.PARAM_ERROR.getMsg() + ":" + error;
return ResponseDTO.error(UserErrorCode.PARAM_ERROR, errorMsg); return ResponseDTO.error(UserErrorCode.PARAM_ERROR, errorMsg);
} }
return ResponseDTO.error(UserErrorCode.PARAM_ERROR); return ResponseDTO.error(UserErrorCode.PARAM_ERROR);
} }

View File

@ -86,14 +86,15 @@ public class WebServerListener implements ApplicationListener<WebServerInitializ
* 初始化reload * 初始化reload
*/ */
private void initReload(WebServerApplicationContext applicationContext) { private void initReload(WebServerApplicationContext applicationContext) {
//将applicationContext转换为ConfigurableApplicationContext // 将applicationContext转换为ConfigurableApplicationContext
ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext; // ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
//
//获取BeanFactory //
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getAutowireCapableBeanFactory(); // //获取BeanFactory
// DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getAutowireCapableBeanFactory();
//动态注册bean //
SmartReloadManager reloadManager = new SmartReloadManager(applicationContext.getBean(ReloadCommand.class), intervalSeconds); // //动态注册bean
defaultListableBeanFactory.registerSingleton("smartReloadManager", reloadManager); // SmartReloadManager reloadManager = new SmartReloadManager(applicationContext.getBean(ReloadCommand.class), intervalSeconds);
// defaultListableBeanFactory.registerSingleton("smartReloadManager", reloadManager);
} }
} }

View File

@ -37,7 +37,7 @@ public class ConfigService {
/** /**
* 一个简单的系统配置缓存 * 一个简单的系统配置缓存
*/ */
private final ConcurrentHashMap<String, ConfigEntity> configCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, ConfigEntity> CONFIG_CACHE = new ConcurrentHashMap<>();
@Resource @Resource
private ConfigDao configDao; private ConfigDao configDao;
@ -52,13 +52,13 @@ public class ConfigService {
*/ */
@PostConstruct @PostConstruct
private void loadConfigCache() { private void loadConfigCache() {
configCache.clear(); CONFIG_CACHE.clear();
List<ConfigEntity> entityList = configDao.selectList(null); List<ConfigEntity> entityList = configDao.selectList(null);
if (CollectionUtils.isEmpty(entityList)) { if (CollectionUtils.isEmpty(entityList)) {
return; return;
} }
entityList.forEach(entity -> this.configCache.put(entity.getConfigKey().toLowerCase(), entity)); entityList.forEach(entity -> this.CONFIG_CACHE.put(entity.getConfigKey().toLowerCase(), entity));
log.info("################# 系统配置缓存初始化完毕:{} ###################", configCache.size()); log.info("################# 系统配置缓存初始化完毕:{} ###################", CONFIG_CACHE.size());
} }
/** /**
@ -70,7 +70,7 @@ public class ConfigService {
if (null == configEntity) { if (null == configEntity) {
return; return;
} }
this.configCache.put(configEntity.getConfigKey().toLowerCase(), configEntity); this.CONFIG_CACHE.put(configEntity.getConfigKey().toLowerCase(), configEntity);
} }
/** /**
@ -100,7 +100,7 @@ public class ConfigService {
if (StrUtil.isBlank(configKey)) { if (StrUtil.isBlank(configKey)) {
return null; return null;
} }
ConfigEntity entity = this.configCache.get(configKey.toLowerCase()); ConfigEntity entity = this.CONFIG_CACHE.get(configKey.toLowerCase());
return SmartBeanUtil.copy(entity, ConfigVO.class); return SmartBeanUtil.copy(entity, ConfigVO.class);
} }

View File

@ -0,0 +1,164 @@
package net.lab1024.sa.base.module.support.job.api;
import cn.hutool.core.util.IdUtil;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.module.support.job.api.domain.SmartJobMsg;
import net.lab1024.sa.base.module.support.job.config.SmartJobAutoConfiguration;
import net.lab1024.sa.base.module.support.job.core.SmartJob;
import net.lab1024.sa.base.module.support.job.core.SmartJobExecutor;
import net.lab1024.sa.base.module.support.job.core.SmartJobLauncher;
import net.lab1024.sa.base.module.support.job.repository.SmartJobRepository;
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobEntity;
import org.redisson.api.RLock;
import org.redisson.api.RTopic;
import org.redisson.api.RedissonClient;
import org.redisson.api.listener.MessageListener;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Service;
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* smart job 执行端管理
* 分布式系统之间 用发布/订阅消息的形式 来管理多个job
*
* @author huke
* @date 2024/6/22 20:31
*/
@ConditionalOnBean(SmartJobAutoConfiguration.class)
@Slf4j
@Service
public class SmartJobClientManager {
private final SmartJobLauncher jobLauncher;
private final SmartJobRepository jobRepository;
private final List<SmartJob> jobInterfaceList;
private static final String EXECUTE_LOCK = "smart-job-lock-msg-execute-";
private static final String TOPIC = "smart-job-instance";
private final RedissonClient redissonClient;
private final RTopic topic;
private final SmartJobMsgListener jobMsgListener;
public SmartJobClientManager(SmartJobLauncher jobLauncher,
SmartJobRepository jobRepository,
List<SmartJob> jobInterfaceList,
RedissonClient redissonClient) {
this.jobLauncher = jobLauncher;
this.jobRepository = jobRepository;
this.jobInterfaceList = jobInterfaceList;
this.redissonClient = redissonClient;
// 添加监听器
this.topic = redissonClient.getTopic(TOPIC);
this.jobMsgListener = new SmartJobMsgListener();
topic.addListener(SmartJobMsg.class, jobMsgListener);
log.info("==== SmartJob ==== client-manager init");
}
/**
* 发布消息
*/
public void publishToClient(SmartJobMsg msgDTO) {
msgDTO.setMsgId(IdUtil.fastSimpleUUID());
topic.publish(msgDTO);
}
/**
* 处理消息
*/
private class SmartJobMsgListener implements MessageListener<SmartJobMsg> {
@Override
public void onMessage(CharSequence channel, SmartJobMsg msg) {
log.info("==== SmartJob ==== on-message :{}", msg);
// 判断消息类型 业务简单就直接判断 复杂的话可以策略模式
SmartJobMsg.MsgTypeEnum msgType = msg.getMsgType();
// 更新任务
if (SmartJobMsg.MsgTypeEnum.UPDATE_JOB == msgType) {
updateJob(msg.getJobId());
}
// 执行任务
if (SmartJobMsg.MsgTypeEnum.EXECUTE_JOB == msgType) {
executeJob(msg);
}
}
}
/**
* 获取任务执行类
*
* @param jobClass
* @return
*/
private Optional<SmartJob> queryJobImpl(String jobClass) {
return jobInterfaceList.stream().filter(e -> Objects.equals(e.getClassName(), jobClass)).findFirst();
}
/**
* 更新任务
*
* @param jobId
*/
private void updateJob(Integer jobId) {
SmartJobEntity jobEntity = jobRepository.getJobDao().selectById(jobId);
if (null == jobEntity) {
return;
}
jobLauncher.startOrRefreshJob(Lists.newArrayList(jobEntity));
}
/**
* 立即执行任务
*
* @param msg
*/
private void executeJob(SmartJobMsg msg) {
Integer jobId = msg.getJobId();
SmartJobEntity jobEntity = jobRepository.getJobDao().selectById(jobId);
if (null == jobEntity) {
return;
}
// 获取定时任务实现类
Optional<SmartJob> optional = this.queryJobImpl(jobEntity.getJobClass());
if (!optional.isPresent()) {
return;
}
// 获取执行锁 无需主动释放
RLock rLock = redissonClient.getLock(EXECUTE_LOCK + msg.getMsgId());
try {
boolean getLock = rLock.tryLock(0, 20, TimeUnit.SECONDS);
if (!getLock) {
return;
}
} catch (InterruptedException e) {
log.error("==== SmartJob ==== msg execute err:", e);
return;
}
// 通过执行器 执行任务
jobEntity.setParam(msg.getParam());
SmartJobExecutor jobExecutor = new SmartJobExecutor(jobEntity, jobRepository, optional.get(), redissonClient);
jobExecutor.execute(msg.getUpdateName());
}
@PreDestroy
public void destroy() {
topic.removeListener(jobMsgListener);
}
}

View File

@ -0,0 +1,246 @@
package net.lab1024.sa.base.module.support.job.api;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Lists;
import net.lab1024.sa.base.common.code.UserErrorCode;
import net.lab1024.sa.base.common.domain.PageResult;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartBeanUtil;
import net.lab1024.sa.base.common.util.SmartPageUtil;
import net.lab1024.sa.base.module.support.job.api.domain.*;
import net.lab1024.sa.base.module.support.job.config.SmartJobAutoConfiguration;
import net.lab1024.sa.base.module.support.job.constant.SmartJobTriggerTypeEnum;
import net.lab1024.sa.base.module.support.job.constant.SmartJobUtil;
import net.lab1024.sa.base.module.support.job.repository.SmartJobDao;
import net.lab1024.sa.base.module.support.job.repository.SmartJobLogDao;
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobEntity;
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobLogEntity;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 定时任务 接口业务管理
* 如果不需要通过接口管理定时任务 可以删除此类
*
* @author huke
* @date 2024/6/17 20:41
*/
@ConditionalOnBean(SmartJobAutoConfiguration.class)
@Service
public class SmartJobService {
@Resource
private SmartJobDao jobDao;
@Resource
private SmartJobLogDao jobLogDao;
@Resource
private SmartJobClientManager jobClientManager;
/**
* 查询 定时任务详情
*
* @param jobId
* @return
*/
public ResponseDTO<SmartJobVO> queryJobInfo(Integer jobId) {
SmartJobEntity jobEntity = jobDao.selectById(jobId);
if (null == jobEntity) {
return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST);
}
SmartJobVO jobVO = SmartBeanUtil.copy(jobEntity, SmartJobVO.class);
// 处理设置job详情
this.handleJobInfo(Lists.newArrayList(jobVO));
return ResponseDTO.ok(jobVO);
}
/**
* 分页查询 定时任务
*
* @param queryForm
* @return
*/
public ResponseDTO<PageResult<SmartJobVO>> queryJob(SmartJobQueryForm queryForm) {
Page<?> page = SmartPageUtil.convert2PageQuery(queryForm);
List<SmartJobVO> jobList = jobDao.query(page, queryForm);
PageResult<SmartJobVO> pageResult = SmartPageUtil.convert2PageResult(page, jobList);
// 处理设置job详情
this.handleJobInfo(jobList);
return ResponseDTO.ok(pageResult);
}
/**
* 处理设置 任务信息
*
* @param jobList
*/
private void handleJobInfo(List<SmartJobVO> jobList) {
if (CollectionUtils.isEmpty(jobList)) {
return;
}
// 查询最后一次执行记录
List<Long> logIdList = jobList.stream().map(SmartJobVO::getLastExecuteLogId).filter(Objects::nonNull).collect(Collectors.toList());
Map<Long, SmartJobLogVO> lastLogMap = Collections.emptyMap();
if (CollectionUtils.isNotEmpty(logIdList)) {
lastLogMap = jobLogDao.selectBatchIds(logIdList)
.stream()
.collect(Collectors.toMap(SmartJobLogEntity::getLogId, e -> SmartBeanUtil.copy(e, SmartJobLogVO.class)));
}
// 循环处理任务信息
for (SmartJobVO jobVO : jobList) {
// 设置最后一次执行记录
Long lastExecuteLogId = jobVO.getLastExecuteLogId();
if (null != lastExecuteLogId) {
jobVO.setLastJobLog(lastLogMap.get(lastExecuteLogId));
}
// 计算未来5次执行时间
if (jobVO.getEnabledFlag()) {
List<LocalDateTime> nextTimeList = SmartJobUtil.queryNextTimeFromNow(jobVO.getTriggerType(), jobVO.getTriggerValue(), jobVO.getLastExecuteTime(), 5);
jobVO.setNextJobExecuteTimeList(nextTimeList);
}
}
}
/**
* 分页查询 定时任务-执行记录
*
* @param queryForm
* @return
*/
public ResponseDTO<PageResult<SmartJobLogVO>> queryJobLog(SmartJobLogQueryForm queryForm) {
Page<?> page = SmartPageUtil.convert2PageQuery(queryForm);
List<SmartJobLogVO> jobList = jobLogDao.query(page, queryForm);
PageResult<SmartJobLogVO> pageResult = SmartPageUtil.convert2PageResult(page, jobList);
return ResponseDTO.ok(pageResult);
}
/**
* 更新定时任务
*
* @param updateForm
* @return
*/
public ResponseDTO<String> updateJob(SmartJobUpdateForm updateForm) {
// 校验参数
Integer jobId = updateForm.getJobId();
SmartJobEntity jobEntity = jobDao.selectById(jobId);
if (null == jobEntity) {
return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST);
}
// 校验触发时间配置
String triggerType = updateForm.getTriggerType();
String triggerValue = updateForm.getTriggerValue();
if (SmartJobTriggerTypeEnum.CRON.equalsValue(triggerType) && !SmartJobUtil.checkCron(triggerValue)) {
return ResponseDTO.userErrorParam("cron表达式错误");
}
if (SmartJobTriggerTypeEnum.FIXED_DELAY.equalsValue(triggerType) && !SmartJobUtil.checkFixedDelay(triggerValue)) {
return ResponseDTO.userErrorParam("固定间隔错误整数且大于0");
}
// 更新数据
jobEntity = SmartBeanUtil.copy(updateForm, SmartJobEntity.class);
jobDao.updateById(jobEntity);
// 更新执行端
SmartJobMsg jobMsg = new SmartJobMsg();
jobMsg.setJobId(jobId);
jobMsg.setMsgType(SmartJobMsg.MsgTypeEnum.UPDATE_JOB);
jobMsg.setUpdateName(updateForm.getUpdateName());
jobClientManager.publishToClient(jobMsg);
return ResponseDTO.ok();
}
/**
* 更新定时任务-是否开启
*
* @param updateForm
* @return
*/
public ResponseDTO<String> updateJobEnabled(SmartJobEnabledUpdateForm updateForm) {
Integer jobId = updateForm.getJobId();
SmartJobEntity jobEntity = jobDao.selectById(jobId);
if (null == jobEntity) {
return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST);
}
Boolean enabledFlag = updateForm.getEnabledFlag();
if (Objects.equals(enabledFlag, jobEntity.getEnabledFlag())) {
return ResponseDTO.ok();
}
// 更新数据
jobEntity = new SmartJobEntity();
jobEntity.setJobId(jobId);
jobEntity.setEnabledFlag(enabledFlag);
jobEntity.setUpdateName(updateForm.getUpdateName());
jobDao.updateById(jobEntity);
// 更新执行端
SmartJobMsg jobMsg = new SmartJobMsg();
jobMsg.setJobId(jobId);
jobMsg.setMsgType(SmartJobMsg.MsgTypeEnum.UPDATE_JOB);
jobMsg.setUpdateName(updateForm.getUpdateName());
jobClientManager.publishToClient(jobMsg);
return ResponseDTO.ok();
}
/**
* 执行定时任务
* 忽略任务的开启状态,立即执行一次
*
* @param executeForm
* @return
*/
public ResponseDTO<String> execute(SmartJobExecuteForm executeForm) {
Integer jobId = executeForm.getJobId();
SmartJobEntity jobEntity = jobDao.selectById(jobId);
if (null == jobEntity) {
return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST);
}
// 更新执行端
SmartJobMsg jobMsg = new SmartJobMsg();
jobMsg.setJobId(jobId);
jobMsg.setParam(executeForm.getParam());
jobMsg.setMsgType(SmartJobMsg.MsgTypeEnum.EXECUTE_JOB);
jobMsg.setUpdateName(executeForm.getUpdateName());
jobClientManager.publishToClient(jobMsg);
return ResponseDTO.ok();
}
/**
* 新增定时任务
* ps:目前没有业务场景需要通过接口 添加任务
* 因为新增定时任务无论如何都需要 手动编码
* 需要时手动给数据库增加一条就行
*
* @return
* @author huke
*/
public ResponseDTO<String> addJob() {
return ResponseDTO.userErrorParam("暂未支持");
}
/**
* 移除定时任务
* ps目前没有业务场景需要通过接口移除理由同 {@link SmartJobService#addJob}
* 彻底移除始终都需要手动删除代码
* 如果只是想暂停任务执行可以调用 {@link SmartJobService#updateJobEnabled}
*
* @return
* @author huke
*/
public ResponseDTO<String> delJob() {
return ResponseDTO.userErrorParam("暂未支持");
}
}

View File

@ -0,0 +1,27 @@
package net.lab1024.sa.base.module.support.job.api.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 定时任务-更新-开启状态
*
* @author huke
* @date 2024/6/17 21:30
*/
@Data
public class SmartJobEnabledUpdateForm {
@Schema(description = "任务id")
@NotNull(message = "任务id不能为空")
private Integer jobId;
@Schema(description = "是否启用")
@NotNull(message = "是否启用不能为空")
private Boolean enabledFlag;
@Schema(hidden = true)
private String updateName;
}

View File

@ -0,0 +1,28 @@
package net.lab1024.sa.base.module.support.job.api.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotNull;
/**
* 定时任务-手动执行
*
* @author huke
* @date 2024/6/18 20:30
*/
@Data
public class SmartJobExecuteForm {
@Schema(description = "任务id")
@NotNull(message = "任务id不能为空")
private Integer jobId;
@Schema(description = "定时任务参数|可选")
@Length(max = 2000, message = "定时任务参数最多2000字符")
private String param;
@Schema(hidden = true)
private String updateName;
}

View File

@ -0,0 +1,34 @@
package net.lab1024.sa.base.module.support.job.api.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import net.lab1024.sa.base.common.domain.PageParam;
import org.hibernate.validator.constraints.Length;
import java.time.LocalDate;
/**
* 定时任务-执行记录 分页查询
*
* @author huke
* @date 2024/6/17 20:50
*/
@Data
public class SmartJobLogQueryForm extends PageParam {
@Schema(description = "搜索词|可选")
@Length(max = 50, message = "搜索词最多50字符")
private String searchWord;
@Schema(description = "任务id|可选")
private Integer jobId;
@Schema(description = "是否成功|可选")
private Boolean successFlag;
@Schema(description = "开始时间|可选", example = "2024-06-06")
private LocalDate startTime;
@Schema(description = "截止时间|可选", example = "2025-10-15")
private LocalDate endTime;
}

View File

@ -0,0 +1,56 @@
package net.lab1024.sa.base.module.support.job.api.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 定时任务-执行记录 vo
*
* @author huke
* @date 2024/6/17 21:30
*/
@Data
public class SmartJobLogVO {
@Schema(description = "logId")
private Long logId;
@Schema(description = "任务id")
private Integer jobId;
@Schema(description = "任务名称")
private String jobName;
@Schema(description = "定时任务参数|可选")
private String param;
@Schema(description = "执行结果是否成功")
private Boolean successFlag;
@Schema(description = "开始执行时间")
private LocalDateTime executeStartTime;
@Schema(description = "执行时长-毫秒")
private Long executeTimeMillis;
@Schema(description = "执行结果描述")
private String executeResult;
@Schema(description = "执行结束时间")
private LocalDateTime executeEndTime;
@Schema(description = "ip")
private String ip;
@Schema(description = "进程id")
private String processId;
@Schema(description = "程序目录")
private String programPath;
private String createName;
private LocalDateTime createTime;
}

View File

@ -0,0 +1,59 @@
package net.lab1024.sa.base.module.support.job.api.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import net.lab1024.sa.base.common.enumeration.BaseEnum;
/**
* 定时任务 发布/订阅消息对象
*
* @author huke
* @date 2024/6/20 21:10
*/
@Data
public class SmartJobMsg {
/**
* 消息id 无需设置
*/
private String msgId;
/**
* 任务id
*/
private Integer jobId;
/**
* 任务参数
*/
private String param;
/**
* 消息类型
*/
private MsgTypeEnum msgType;
/**
* 更新人
*/
private String updateName;
@Getter
@AllArgsConstructor
public enum MsgTypeEnum implements BaseEnum {
/**
* 1 更新任务
*/
UPDATE_JOB(1, "更新任务"),
EXECUTE_JOB(2, "执行任务"),
;
private final Integer value;
private final String desc;
}
}

View File

@ -0,0 +1,30 @@
package net.lab1024.sa.base.module.support.job.api.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import net.lab1024.sa.base.common.domain.PageParam;
import net.lab1024.sa.base.common.swagger.SchemaEnum;
import net.lab1024.sa.base.common.validator.enumeration.CheckEnum;
import net.lab1024.sa.base.module.support.job.constant.SmartJobTriggerTypeEnum;
import org.hibernate.validator.constraints.Length;
/**
* 定时任务 分页查询
*
* @author huke
* @date 2024/6/17 20:50
*/
@Data
public class SmartJobQueryForm extends PageParam {
@Schema(description = "搜索词|可选")
@Length(max = 50, message = "搜索词最多50字符")
private String searchWord;
@SchemaEnum(desc = "触发类型", value = SmartJobTriggerTypeEnum.class)
@CheckEnum(value = SmartJobTriggerTypeEnum.class, message = "触发类型错误")
private String triggerType;
@Schema(description = "是否启用|可选")
private Boolean enabledFlag;
}

View File

@ -0,0 +1,63 @@
package net.lab1024.sa.base.module.support.job.api.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import net.lab1024.sa.base.common.swagger.SchemaEnum;
import net.lab1024.sa.base.common.validator.enumeration.CheckEnum;
import net.lab1024.sa.base.module.support.job.constant.SmartJobTriggerTypeEnum;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 定时任务 更新
*
* @author huke
* @date 2024/6/17 21:30
*/
@Data
public class SmartJobUpdateForm {
@Schema(description = "任务id")
@NotNull(message = "任务id不能为空")
private Integer jobId;
@Schema(description = "任务名称")
@NotBlank(message = "任务名称不能为空")
@Length(max = 100, message = "任务名称最多100字符")
private String jobName;
@Schema(description = "任务执行类")
@NotBlank(message = "任务执行类不能为空")
@Length(max = 200, message = "任务执行类最多200字符")
private String jobClass;
@SchemaEnum(desc = "触发类型", value = SmartJobTriggerTypeEnum.class)
@CheckEnum(value = SmartJobTriggerTypeEnum.class, required = true, message = "触发类型错误")
private String triggerType;
@Schema(description = "触发配置")
@NotBlank(message = "触发配置不能为空")
@Length(max = 100, message = "触发配置最多100字符")
private String triggerValue;
@Schema(description = "定时任务参数|可选")
@Length(max = 1000, message = "定时任务参数最多1000字符")
private String param;
@Schema(description = "是否开启")
@NotNull(message = "是否开启不能为空")
private Boolean enabledFlag;
@Schema(description = "备注")
@Length(max = 250, message = "任务备注最多250字符")
private String remark;
@NotNull(message = "排序不能为空")
@Schema(description = "排序")
private Integer sort;
@Schema(hidden = true)
private String updateName;
}

View File

@ -0,0 +1,66 @@
package net.lab1024.sa.base.module.support.job.api.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import net.lab1024.sa.base.common.json.serializer.enumeration.EnumSerialize;
import net.lab1024.sa.base.common.swagger.SchemaEnum;
import net.lab1024.sa.base.module.support.job.constant.SmartJobTriggerTypeEnum;
import java.time.LocalDateTime;
import java.util.List;
/**
* 定时任务 vo
*
* @author huke
* @date 2024/6/17 21:30
*/
@Data
public class SmartJobVO {
@Schema(description = "任务id")
private Integer jobId;
@Schema(description = "任务名称")
private String jobName;
@Schema(description = "执行类")
private String jobClass;
@SchemaEnum(desc = "触发类型", value = SmartJobTriggerTypeEnum.class)
@EnumSerialize(SmartJobTriggerTypeEnum.class)
private String triggerType;
@Schema(description = "触发配置")
private String triggerValue;
@Schema(description = "定时任务参数|可选")
private String param;
@Schema(description = "是否启用")
private Boolean enabledFlag;
@Schema(description = "最后一执行时间")
private LocalDateTime lastExecuteTime;
@Schema(description = "最后一次执行记录id")
private Long lastExecuteLogId;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序")
private Integer sort;
private String updateName;
private LocalDateTime updateTime;
private LocalDateTime createTime;
@Schema(description = "上次执行记录")
private SmartJobLogVO lastJobLog;
@Schema(description = "未来N次任务执行时间")
private List<LocalDateTime> nextJobExecuteTimeList;
}

View File

@ -0,0 +1,52 @@
package net.lab1024.sa.base.module.support.job.config;
import net.lab1024.sa.base.module.support.job.core.SmartJob;
import net.lab1024.sa.base.module.support.job.core.SmartJobLauncher;
import net.lab1024.sa.base.module.support.job.repository.SmartJobRepository;
import org.redisson.api.RedissonClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* 定时任务 配置
*
* @author huke
* @date 2024/6/17 21:30
*/
@Configuration
@EnableConfigurationProperties(SmartJobConfig.class)
@ConditionalOnProperty(
prefix = SmartJobConfig.CONFIG_PREFIX,
name = "enabled",
havingValue = "true"
)
public class SmartJobAutoConfiguration {
private final SmartJobConfig jobConfig;
private final SmartJobRepository jobRepository;
private final List<SmartJob> jobInterfaceList;
public SmartJobAutoConfiguration(SmartJobConfig jobConfig,
SmartJobRepository jobRepository,
List<SmartJob> jobInterfaceList) {
this.jobConfig = jobConfig;
this.jobRepository = jobRepository;
this.jobInterfaceList = jobInterfaceList;
}
/**
* 定时任务启动器
*
* @return
*/
@Bean
public SmartJobLauncher initJobLauncher(RedissonClient redissonClient) {
return new SmartJobLauncher(jobConfig, jobRepository, jobInterfaceList, redissonClient);
}
}

View File

@ -0,0 +1,39 @@
package net.lab1024.sa.base.module.support.job.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* smart job 配置
* 与配置文件参数对应
*
* @author huke
* @date 2024/6/17 21:30
*/
@ConfigurationProperties(prefix = SmartJobConfig.CONFIG_PREFIX)
@Data
public class SmartJobConfig {
public static final String CONFIG_PREFIX = "smart.job";
/**
* 任务执行核心线程数 偶数 默认2
*/
private Integer corePoolSize = 2;
/**
* 任务延迟初始化 默认30秒
*/
private Integer initDelay = 30;
/**
* 数据库配置检测-开关 默认开启
*/
private Boolean dbRefreshEnabled = true;
/**
* 数据库配置检测-执行间隔 默认120秒
*/
private Integer dbRefreshInterval = 120;
}

View File

@ -0,0 +1,22 @@
package net.lab1024.sa.base.module.support.job.constant;
/**
* smart job 常量
*
* @author huke
* @date 2024/6/19 20:25
*/
public class SmartJobConst {
public static final String SYSTEM_NAME = "system";
public static final String LOGO = " _____ __ __ __ \n" +
" / ___/____ ___ ____ ______/ /_ / /___ / /_ \n" +
" \\__ \\/ __ `__ \\/ __ `/ ___/ __/ __ / / __ \\/ __ \\\n" +
" ___/ / / / / / / /_/ / / / /_ / /_/ / /_/ / /_/ /\n" +
"/____/_/ /_/ /_/\\__,_/_/ \\__/ \\____/\\____/_.___/ \n" +
"-->任务执行线程池:%s\n" +
"-->任务初始化延迟:%s秒\n" +
"-->数据库配置检测:%s\n\n";
}

View File

@ -0,0 +1,30 @@
package net.lab1024.sa.base.module.support.job.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.lab1024.sa.base.common.enumeration.BaseEnum;
/**
* job 任务触发类型 枚举类
*
* @author huke
* @date 2024年6月29日
**/
@AllArgsConstructor
@Getter
public enum SmartJobTriggerTypeEnum implements BaseEnum {
/**
* 1 cron表达式
*/
CRON("cron", "cron表达式"),
FIXED_DELAY("fixed_delay", "固定间隔"),
;
private final String value;
private final String desc;
}

View File

@ -0,0 +1,188 @@
package net.lab1024.sa.base.module.support.job.constant;
import org.springframework.scheduling.support.CronExpression;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* smart job util
*
* @author huke
* @date 2024/6/18 20:00
*/
public class SmartJobUtil {
private SmartJobUtil() {
}
/**
* 校验cron表达式 是否合法
*
* @param cron
* @return
*/
public static boolean checkCron(String cron) {
return CronExpression.isValidExpression(cron);
}
/**
* 校验固定间隔 值是否合法
*
* @param val
* @return
*/
public static boolean checkFixedDelay(String val) {
int intVal;
try {
intVal = Integer.parseInt(val);
} catch (NumberFormatException e) {
return false;
}
return intVal > 0;
}
/**
* 打印一些展示信息到控制台
* 环保绿
*
* @param info
*/
public static void printInfo(String info) {
System.out.printf("\033[32;1m %s \033[0m", info);
}
/**
* 查询未来N次执行时间 从最后一次时间时间 开始计算
*
* @param triggerType
* @param triggerVal
* @param lastExecuteTime
* @param num
* @return
*/
public static List<LocalDateTime> queryNextTimeFromLast(String triggerType,
String triggerVal,
LocalDateTime lastExecuteTime,
int num) {
List<LocalDateTime> nextTimeList = null;
if (SmartJobTriggerTypeEnum.CRON.equalsValue(triggerType)) {
nextTimeList = SmartJobUtil.queryNextTime(triggerVal, lastExecuteTime, num);
} else if (SmartJobTriggerTypeEnum.FIXED_DELAY.equalsValue(triggerType)) {
nextTimeList = SmartJobUtil.queryNextTime(getFixedDelayVal(triggerVal), lastExecuteTime, num);
}
return nextTimeList;
}
/**
* 查询未来N次执行时间 从当前时间 开始计算
*
* @param triggerType
* @param triggerVal
* @param lastExecuteTime
* @param num
* @return
*/
public static List<LocalDateTime> queryNextTimeFromNow(String triggerType,
String triggerVal,
LocalDateTime lastExecuteTime,
int num) {
LocalDateTime nowTime = LocalDateTime.now();
List<LocalDateTime> nextTimeList = null;
if (SmartJobTriggerTypeEnum.CRON.equalsValue(triggerType)) {
nextTimeList = SmartJobUtil.queryNextTime(triggerVal, nowTime, num);
} else if (SmartJobTriggerTypeEnum.FIXED_DELAY.equalsValue(triggerType)) {
Integer fixedDelay = getFixedDelayVal(triggerVal);
LocalDateTime startTime = null == lastExecuteTime || lastExecuteTime.plusSeconds(fixedDelay).isBefore(nowTime)
? nowTime : lastExecuteTime;
nextTimeList = SmartJobUtil.queryNextTime(fixedDelay, startTime, num);
}
return nextTimeList;
}
/**
* 根据cron表达式 计算N次执行时间
*
* @param cron
* @param startTime
* @param num
* @return
*/
public static List<LocalDateTime> queryNextTime(String cron, LocalDateTime startTime, int num) {
if (null == startTime) {
return Collections.emptyList();
}
CronExpression parse = CronExpression.parse(cron);
List<LocalDateTime> timeList = new ArrayList<>(num);
for (int i = 0; i < num; i++) {
startTime = parse.next(startTime);
timeList.add(startTime);
}
return timeList;
}
/**
* 根据 固定间隔 计算N次执行时间
*
* @param fixDelaySecond
* @param startTime
* @param num
* @return
*/
public static List<LocalDateTime> queryNextTime(Integer fixDelaySecond, LocalDateTime startTime, int num) {
if (null == startTime) {
return Collections.emptyList();
}
List<LocalDateTime> timeList = new ArrayList<>(num);
for (int i = 0; i < num; i++) {
startTime = startTime.plusSeconds(fixDelaySecond);
timeList.add(startTime);
}
return timeList;
}
/**
* 获取固定间隔时间
*
* @param val
* @return
*/
public static Integer getFixedDelayVal(String val) {
return Integer.parseInt(val);
}
/**
* 获取当前 Java 应用程序的工作目录
*
* @return
*/
public static String getProgramPath() {
return System.getProperty("user.dir");
}
/**
* 获取当前 Java 应用程序的进程id
*
* @return
*/
public static String getProcessId() {
RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
return runtime.getName().split("@")[0];
}
public static void main(String[] args) {
LocalDateTime startTime = LocalDateTime.now();
List<LocalDateTime> timeList = SmartJobUtil.queryNextTime("5 * * * * *", startTime, 3);
System.out.println(timeList);
timeList = SmartJobUtil.queryNextTime(10, startTime, 3);
System.out.println(timeList);
System.out.println("project path ->" + getProgramPath());
System.out.println("project process id ->" + getProcessId());
}
}

View File

@ -0,0 +1,28 @@
package net.lab1024.sa.base.module.support.job.core;
/**
* 定时任务 执行接口
*
* @author huke
* @date 2024/6/17 21:30
*/
public interface SmartJob {
/**
* 默认方法
* 获取当前任务类名
*
* @return
*/
default String getClassName() {
return this.getClass().getName();
}
/**
* 执行定时任务
*
* @param param 可选参数 任务不需要时不用管
* @return 可null, 自行组织语言描述执行结果例如本次处理数据N条
*/
String run(String param);
}

View File

@ -0,0 +1,147 @@
package net.lab1024.sa.base.module.support.job.core;
import cn.hutool.core.exceptions.ExceptionUtil;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.util.SmartIpUtil;
import net.lab1024.sa.base.module.support.job.constant.SmartJobConst;
import net.lab1024.sa.base.module.support.job.constant.SmartJobUtil;
import net.lab1024.sa.base.module.support.job.repository.SmartJobRepository;
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobEntity;
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobLogEntity;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.util.StopWatch;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
/**
* 定时任务 执行器
*
* @author huke
* @date 2024/6/17 21:30
*/
@Slf4j
public class SmartJobExecutor implements Runnable {
private final SmartJobEntity jobEntity;
private final SmartJobRepository jobRepository;
private final SmartJob jobInterface;
private final RedissonClient redissonClient;
private static final String EXECUTE_LOCK = "smart-job-lock-execute-";
public SmartJobExecutor(SmartJobEntity jobEntity,
SmartJobRepository jobRepository,
SmartJob jobInterface,
RedissonClient redissonClient) {
this.jobEntity = jobEntity;
this.jobRepository = jobRepository;
this.jobInterface = jobInterface;
this.redissonClient = redissonClient;
}
/**
* 系统线程执行
*/
@Override
public void run() {
// 获取当前任务执行锁 最多持有30s自动释放
Integer jobId = jobEntity.getJobId();
RLock rLock = redissonClient.getLock(EXECUTE_LOCK + jobId);
try {
boolean lock = rLock.tryLock(0, 30, TimeUnit.SECONDS);
if (!lock) {
return;
}
// 查询上次执行时间 校验执行间隔
SmartJobEntity dbJobEntity = jobRepository.getJobDao().selectById(jobId);
if (null == dbJobEntity) {
return;
}
LocalDateTime lastExecuteTime = dbJobEntity.getLastExecuteTime();
if (null != lastExecuteTime) {
LocalDateTime nextTime = SmartJobUtil.queryNextTimeFromLast(jobEntity.getTriggerType(), jobEntity.getTriggerValue(), lastExecuteTime, 1).get(0);
if (LocalDateTime.now().isBefore(nextTime)) {
return;
}
}
// 执行任务
SmartJobLogEntity logEntity = this.execute(SmartJobConst.SYSTEM_NAME);
log.info("==== SmartJob ==== execute job->{},time-millis->{}ms", jobEntity.getJobName(), logEntity.getExecuteTimeMillis());
} catch (Throwable t) {
log.error("==== SmartJob ==== execute err:", t);
} finally {
if (rLock.isHeldByCurrentThread()) {
rLock.unlock();
}
}
}
/**
* 执行任务
*
* @param executorName
*/
public SmartJobLogEntity execute(String executorName) {
// 执行计时
LocalDateTime startTime = LocalDateTime.now();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 执行任务
boolean successFlag = true;
String executeResult;
try {
executeResult = jobInterface.run(jobEntity.getParam());
stopWatch.stop();
} catch (Throwable t) {
stopWatch.stop();
successFlag = false;
// ps:异常信息不大于数据库字段长度限制
executeResult = ExceptionUtil.stacktraceToString(t, 1800);
log.error("==== SmartJob ==== execute err:", t);
}
// 保存执行记录
Integer jobId = jobEntity.getJobId();
SmartJobLogEntity logEntity = new SmartJobLogEntity();
logEntity.setJobId(jobId);
logEntity.setJobName(jobEntity.getJobName());
logEntity.setParam(jobEntity.getParam());
logEntity.setSuccessFlag(successFlag);
// 执行开始 结束时间
logEntity.setExecuteStartTime(startTime);
long totalTimeMillis = stopWatch.getTotalTimeMillis();
logEntity.setExecuteTimeMillis(totalTimeMillis);
logEntity.setExecuteEndTime(startTime.plus(totalTimeMillis, ChronoUnit.MILLIS));
// 执行结果
logEntity.setExecuteResult(executeResult);
logEntity.setIp(SmartIpUtil.getLocalFirstIp());
logEntity.setProcessId(SmartJobUtil.getProcessId());
logEntity.setProgramPath(SmartJobUtil.getProgramPath());
logEntity.setCreateName(executorName);
// 更新上次执行
SmartJobEntity updateJobEntity = new SmartJobEntity();
updateJobEntity.setJobId(jobId);
updateJobEntity.setLastExecuteTime(startTime);
// 持久化数据
jobRepository.saveLog(logEntity, updateJobEntity);
return logEntity;
}
/**
* 查询 当前任务信息
*
* @return
*/
public SmartJobEntity getJob() {
return jobEntity;
}
}

View File

@ -0,0 +1,148 @@
package net.lab1024.sa.base.module.support.job.core;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.module.support.job.config.SmartJobConfig;
import net.lab1024.sa.base.module.support.job.constant.SmartJobConst;
import net.lab1024.sa.base.module.support.job.constant.SmartJobUtil;
import net.lab1024.sa.base.module.support.job.repository.SmartJobRepository;
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobEntity;
import org.redisson.api.RedissonClient;
import org.springframework.util.CollectionUtils;
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 定时任务 作业启动类
*
* @author huke
* @date 2024/6/17 21:30
*/
@Slf4j
public class SmartJobLauncher {
private final SmartJobRepository jobRepository;
private final List<SmartJob> jobInterfaceList;
private final RedissonClient redissonClient;
public SmartJobLauncher(SmartJobConfig jobConfig,
SmartJobRepository jobRepository,
List<SmartJob> jobInterfaceList,
RedissonClient redissonClient) {
this.jobRepository = jobRepository;
this.jobInterfaceList = jobInterfaceList;
this.redissonClient = redissonClient;
// init job scheduler
SmartJobScheduler.init(jobConfig);
// 任务自动检测配置 固定1个线程
Integer initDelay = jobConfig.getInitDelay();
Boolean refreshEnabled = jobConfig.getDbRefreshEnabled();
Integer refreshInterval = jobConfig.getDbRefreshInterval();
ThreadFactory factory = new ThreadFactoryBuilder().setNameFormat("SmartJobLauncher-%d").build();
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, factory);
Runnable launcherRunnable = () -> {
try {
// 查询所有任务
List<SmartJobEntity> smartJobList = this.queryJob();
this.startOrRefreshJob(smartJobList);
} catch (Throwable t) {
log.error("SmartJob Error:", t);
}
// 只在启动时 执行一次
if (!refreshEnabled) {
executor.shutdown();
}
};
executor.scheduleWithFixedDelay(launcherRunnable, initDelay, refreshInterval, TimeUnit.SECONDS);
// 打印信息
String refreshDesc = refreshEnabled ? "开启|检测间隔" + refreshInterval + "" : "关闭";
String format = String.format(SmartJobConst.LOGO, jobConfig.getCorePoolSize(), initDelay, refreshDesc);
SmartJobUtil.printInfo(format);
}
/**
* 查询数据库
* 启动/刷新任务
*/
public void startOrRefreshJob(List<SmartJobEntity> smartJobList) {
// 查询任务配置
if (CollectionUtils.isEmpty(smartJobList) || CollectionUtils.isEmpty(jobInterfaceList)) {
log.info("==== SmartJob ==== job list empty");
return;
}
// 任务实现类
Map<String, SmartJob> jobImplMap = jobInterfaceList.stream().collect(Collectors.toMap(SmartJob::getClassName, Function.identity()));
for (SmartJobEntity jobEntity : smartJobList) {
// 任务是否存在 判断是否需要更新
Integer jobId = jobEntity.getJobId();
SmartJobEntity oldJobEntity = SmartJobScheduler.getJobInfo(jobId);
if (null != oldJobEntity) {
// 不需要更新
if (!isNeedUpdate(oldJobEntity, jobEntity)) {
continue;
}
// 需要更新 移除原任务
SmartJobScheduler.removeJob(jobId);
}
// 任务未开启
if (!jobEntity.getEnabledFlag()) {
continue;
}
// 查找任务实现类
SmartJob jobImpl = jobImplMap.get(jobEntity.getJobClass());
if (null == jobImpl) {
continue;
}
// 添加任务
SmartJobExecutor jobExecute = new SmartJobExecutor(jobEntity, jobRepository, jobImpl, redissonClient);
SmartJobScheduler.addJob(jobExecute);
}
List<SmartJobEntity> runjJobList = SmartJobScheduler.getJobInfo();
List<String> jobNameList = runjJobList.stream().map(SmartJobEntity::getJobName).collect(Collectors.toList());
log.info("==== SmartJob ==== start/refresh job num:{}->{}", runjJobList.size(), jobNameList);
}
/**
* 查询全部任务
*
* @return
*/
private List<SmartJobEntity> queryJob() {
return jobRepository.getJobDao().selectList(null);
}
/**
* 手动判断 任务配置 是否需要更新
* 新增字段的话 在这个方法里增加判断
*
* @return
*/
private static boolean isNeedUpdate(SmartJobEntity oldJob, SmartJobEntity newJob) {
// cron为空时 fixedDelay 才有意义
return !Objects.equals(oldJob.getEnabledFlag(), newJob.getEnabledFlag())
|| !Objects.equals(oldJob.getTriggerType(), newJob.getTriggerType())
|| !Objects.equals(oldJob.getTriggerValue(), newJob.getTriggerValue())
|| !Objects.equals(oldJob.getJobClass(), newJob.getJobClass());
}
@PreDestroy
public void destroy() {
SmartJobScheduler.destroy();
log.info("==== SmartJob ==== destroy job");
}
}

View File

@ -0,0 +1,178 @@
package net.lab1024.sa.base.module.support.job.core;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.module.support.job.config.SmartJobConfig;
import net.lab1024.sa.base.module.support.job.constant.SmartJobTriggerTypeEnum;
import net.lab1024.sa.base.module.support.job.constant.SmartJobUtil;
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobEntity;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.scheduling.support.PeriodicTrigger;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 定时任务 调度管理
*
* @author huke
* @date 2024/6/22 21:30
*/
@Slf4j
public class SmartJobScheduler {
/**
* Spring线程池任务调度器
*/
private static ThreadPoolTaskScheduler TASK_SCHEDULER;
/**
* 定时任务 map
*/
private static Map<Integer, Pair<SmartJobEntity, ScheduledFuture<?>>> JOB_FUTURE_MAP;
private SmartJobScheduler() {
}
/**
* 初始化任务调度配置
*/
public static void init(SmartJobConfig config) {
TASK_SCHEDULER = new ThreadPoolTaskScheduler();
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("SmartJobExecutor-%d").build();
TASK_SCHEDULER.setThreadFactory(threadFactory);
TASK_SCHEDULER.setPoolSize(config.getCorePoolSize());
// 线程池在关闭时会等待所有任务完成
TASK_SCHEDULER.setWaitForTasksToCompleteOnShutdown(true);
// 在调用shutdown方法后等待任务完成的最长时间
TASK_SCHEDULER.setAwaitTerminationSeconds(10);
// 错误处理
TASK_SCHEDULER.setErrorHandler((t) -> log.error("SmartJobExecute Err:", t));
// 当一个任务在被调度执行前被取消时是否应该从线程池的任务队列中移除
TASK_SCHEDULER.setRemoveOnCancelPolicy(true);
TASK_SCHEDULER.initialize();
JOB_FUTURE_MAP = new ConcurrentHashMap<>();
}
/**
* 获取任务执行对象
*
* @param jobId
* @return
*/
public static ScheduledFuture<?> getJobFuture(Integer jobId) {
Pair<SmartJobEntity, ScheduledFuture<?>> pair = JOB_FUTURE_MAP.get(jobId);
if (null == pair) {
return null;
}
return pair.getRight();
}
/**
* 获取当前所有执行任务
*
* @return
*/
public static List<SmartJobEntity> getJobInfo() {
return JOB_FUTURE_MAP.values().stream().map(Pair::getLeft).collect(Collectors.toList());
}
/**
* 获取任务执行实体类
*
* @param jobId
* @return
*/
public static SmartJobEntity getJobInfo(Integer jobId) {
Pair<SmartJobEntity, ScheduledFuture<?>> pair = JOB_FUTURE_MAP.get(jobId);
if (null == pair) {
return null;
}
return pair.getLeft();
}
/**
* 添加任务
*
* @param jobExecute
* @return
*/
public static void addJob(SmartJobExecutor jobExecute) {
// 任务是否存在
SmartJobEntity jobEntity = jobExecute.getJob();
Integer jobId = jobEntity.getJobId();
if (JOB_FUTURE_MAP.containsKey(jobId)) {
// 移除任务
removeJob(jobId);
}
// 任务触发类型
Trigger trigger = null;
String triggerType = jobEntity.getTriggerType();
String triggerValue = jobEntity.getTriggerValue();
// 优先 cron 表达式
if (SmartJobTriggerTypeEnum.CRON.equalsValue(triggerType)) {
trigger = new CronTrigger(triggerValue);
} else if (SmartJobTriggerTypeEnum.FIXED_DELAY.equalsValue(triggerType)) {
trigger = new PeriodicTrigger(SmartJobUtil.getFixedDelayVal(triggerValue), TimeUnit.SECONDS);
}
String jobName = jobEntity.getJobName();
if (null == trigger) {
log.error("==== SmartJob ==== trigger-value not null {}", jobName);
return;
}
// 执行任务
ScheduledFuture<?> schedule = TASK_SCHEDULER.schedule(jobExecute, trigger);
JOB_FUTURE_MAP.put(jobId, Pair.of(jobEntity, schedule));
log.info("==== SmartJob ==== add job:{}", jobName);
}
/**
* 移除任务
* 等待任务执行完成后移除
*
* @param jobId
*/
public static void removeJob(Integer jobId) {
ScheduledFuture<?> jobFuture = getJobFuture(jobId);
if (null == jobFuture) {
return;
}
// 结束任务
stopJob(jobFuture);
JOB_FUTURE_MAP.remove(jobId);
log.info("==== SmartJob ==== remove job:{}", jobId);
}
/**
* 停止所有定时任务
*/
public static void destroy() {
// 启动一个有序的关闭过程,在这个过程中,不再接受新的任务提交,但已提交的任务包括正在执行的和队列中等待的会被允许执行完成
TASK_SCHEDULER.destroy();
JOB_FUTURE_MAP.clear();
}
/**
* 结束任务
* 如果任务还没有开始执行会直接被取消
* 如果任务已经开始执行此时不会中断执行中的线程任务会执行完成再被取消
*
* @param scheduledFuture
*/
private static void stopJob(ScheduledFuture<?> scheduledFuture) {
if (null == scheduledFuture || scheduledFuture.isCancelled()) {
return;
}
scheduledFuture.cancel(false);
}
}

View File

@ -0,0 +1,32 @@
package net.lab1024.sa.base.module.support.job.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import net.lab1024.sa.base.module.support.job.api.domain.SmartJobQueryForm;
import net.lab1024.sa.base.module.support.job.api.domain.SmartJobVO;
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 定时任务 dao
*
* @author huke
* @date 2024/6/17 21:30
*/
@Mapper
@Component
public interface SmartJobDao extends BaseMapper<SmartJobEntity> {
/**
* 定时任务-分页查询
*
* @param page
* @param queryForm
* @return
*/
List<SmartJobVO> query(Page<?> page, @Param("query") SmartJobQueryForm queryForm);
}

View File

@ -0,0 +1,32 @@
package net.lab1024.sa.base.module.support.job.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import net.lab1024.sa.base.module.support.job.api.domain.SmartJobLogQueryForm;
import net.lab1024.sa.base.module.support.job.api.domain.SmartJobLogVO;
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobLogEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 定时任务-执行记录 dao
*
* @author huke
* @date 2024/6/17 21:30
*/
@Mapper
@Component
public interface SmartJobLogDao extends BaseMapper<SmartJobLogEntity> {
/**
* 定时任务-执行记录-分页查询
*
* @param page
* @param queryForm
* @return
*/
List<SmartJobLogVO> query(Page<?> page, @Param("query") SmartJobLogQueryForm queryForm);
}

View File

@ -0,0 +1,42 @@
package net.lab1024.sa.base.module.support.job.repository;
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobEntity;
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobLogEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* job 持久化业务
*
* @author huke
* @date 2024/6/22 22:28
*/
@Service
public class SmartJobRepository {
@Autowired
private SmartJobDao jobDao;
@Autowired
private SmartJobLogDao jobLogDao;
public SmartJobDao getJobDao() {
return jobDao;
}
/**
* 保存执行记录
*
* @param logEntity
* @param jobEntity
*/
@Transactional(rollbackFor = Throwable.class)
public void saveLog(SmartJobLogEntity logEntity, SmartJobEntity jobEntity) {
jobLogDao.insert(logEntity);
jobEntity.setLastExecuteLogId(logEntity.getLogId());
jobDao.updateById(jobEntity);
}
}

View File

@ -0,0 +1,84 @@
package net.lab1024.sa.base.module.support.job.repository.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import net.lab1024.sa.base.module.support.job.constant.SmartJobTriggerTypeEnum;
import java.time.LocalDateTime;
/**
* 定时任务 实体类
*
* @author huke
* @date 2024/6/17 21:30
*/
@Data
@TableName("t_smart_job")
public class SmartJobEntity {
/**
* 任务id
*/
@TableId(type = IdType.AUTO)
private Integer jobId;
/**
* 任务名称
*/
private String jobName;
/**
* 执行类
*/
private String jobClass;
/**
* 触发类型
*
* @see SmartJobTriggerTypeEnum
*/
private String triggerType;
/**
* 触发配置
*/
private String triggerValue;
/**
* 定时任务参数 可选
*/
private String param;
/**
* 是否启用
*/
private Boolean enabledFlag;
/**
* 最后一执行时间
*/
private LocalDateTime lastExecuteTime;
/**
* 最后一次执行记录id
*/
private Long lastExecuteLogId;
/**
* 备注描述 可选
*/
private String remark;
/**
* 排序
*/
private Integer sort;
private String updateName;
private LocalDateTime updateTime;
private LocalDateTime createTime;
}

View File

@ -0,0 +1,81 @@
package net.lab1024.sa.base.module.support.job.repository.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 定时任务 执行记录 实体类
*
* @author huke
* @date 2024/6/17 21:30
*/
@Data
@TableName("t_smart_job_log")
public class SmartJobLogEntity {
@TableId(type = IdType.AUTO)
private Long logId;
/**
* 任务id
*/
private Integer jobId;
/**
* 任务名称
*/
private String jobName;
/**
* 定时任务参数 可选
*/
private String param;
/**
* 执行结果 是否成功
*/
private Boolean successFlag;
/**
* 开始执行时间
*/
private LocalDateTime executeStartTime;
/**
* 执行时长-毫秒
*/
private Long executeTimeMillis;
/**
* 执行结束时间
*/
private LocalDateTime executeEndTime;
/**
* 执行结果 描述 可选
*/
private String executeResult;
/**
* ip
*/
private String ip;
/**
* 进程id
*/
private String processId;
/**
* 程序目录
*/
private String programPath;
private String createName;
private LocalDateTime createTime;
}

View File

@ -0,0 +1,29 @@
package net.lab1024.sa.base.module.support.job.sample;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.module.support.job.core.SmartJob;
import org.springframework.stereotype.Service;
/**
* 定时任务 示例1
*
* @author huke
* @date 2024/6/17 21:30
*/
@Slf4j
@Service
public class SmartJobSample1 implements SmartJob {
/**
* 定时任务示例
*
* @param param 可选参数 任务不需要时不用管
* @return
*/
@Override
public String run(String param) {
// 写点什么业务逻辑
return "执行完毕,随便说点什么吧";
}
}

View File

@ -0,0 +1,48 @@
package net.lab1024.sa.base.module.support.job.sample;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.module.support.config.ConfigDao;
import net.lab1024.sa.base.module.support.config.domain.ConfigEntity;
import net.lab1024.sa.base.module.support.job.core.SmartJob;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 定时任务 示例2
*
* @author huke
* @date 2024/6/17 21:30
*/
@Slf4j
@Service
public class SmartJobSample2 implements SmartJob {
@Autowired
private ConfigDao configDao;
/**
* 定时任务示例
* 需要事务时 添加 @Transactional 注解
*
* @param param 可选参数 任务不需要时不用管
* @return
*/
@Transactional(rollbackFor = Throwable.class)
@Override
public String run(String param) {
// 随便更新点什么东西
ConfigEntity configEntity = new ConfigEntity();
configEntity.setConfigId(1L);
configEntity.setRemark(param);
configDao.updateById(configEntity);
configEntity = new ConfigEntity();
configEntity.setConfigId(2L);
configEntity.setRemark("SmartJob Sample2 update");
configDao.updateById(configEntity);
return "执行成功,本次处理数据1条";
}
}

View File

@ -0,0 +1,5 @@
/**
* 定时任务 示例包
* 可以删除
*/
package net.lab1024.sa.base.module.support.job.sample;

View File

@ -16,6 +16,12 @@ import net.lab1024.sa.base.common.domain.PageParam;
@Data @Data
public class LoginLogQueryForm extends PageParam { public class LoginLogQueryForm extends PageParam {
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "用户类型")
private Integer userType;
@Schema(description = "开始日期") @Schema(description = "开始日期")
private String startDate; private String startDate;

View File

@ -0,0 +1,31 @@
package net.lab1024.sa.base.module.support.message.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.lab1024.sa.base.common.enumeration.BaseEnum;
/**
* 消息模板类型
*
* @author luoyi
* @date 2024/06/22 20:20
*/
@Getter
@AllArgsConstructor
public enum MessageTemplateEnum implements BaseEnum {
ORDER_AUDIT(1000, "订单审批", MessageTypeEnum.ORDER, "您有一个订单等待审批,订单号【${orderNumber}】"),
;
private final Integer value;
private final String desc;
private final MessageTypeEnum messageTypeEnum;
private final String content;
}

View File

@ -0,0 +1,27 @@
package net.lab1024.sa.base.module.support.message.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.lab1024.sa.base.common.enumeration.BaseEnum;
/**
* 消息类型
*
* @author luoyi
* @date 2024/06/22 20:20
*/
@Getter
@AllArgsConstructor
public enum MessageTypeEnum implements BaseEnum {
MAIL(1, "站内信"),
ORDER(2, "订单"),
;
private final Integer value;
private final String desc;
}

View File

@ -0,0 +1,68 @@
package net.lab1024.sa.base.module.support.message.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import net.lab1024.sa.base.common.controller.SupportBaseController;
import net.lab1024.sa.base.common.domain.PageResult;
import net.lab1024.sa.base.common.domain.RequestUser;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartRequestUtil;
import net.lab1024.sa.base.constant.SwaggerTagConst;
import net.lab1024.sa.base.module.support.message.domain.MessageQueryForm;
import net.lab1024.sa.base.module.support.message.domain.MessageVO;
import net.lab1024.sa.base.module.support.message.service.MessageService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
/**
* 消息
*
* @author luoyi
* @date 2024/06/22 20:20
*/
@RestController
@Tag(name = SwaggerTagConst.Support.MESSAGE)
public class MessageController extends SupportBaseController {
@Resource
private MessageService messageService;
@Operation(summary = "分页查询我的消息 @luoyi")
@PostMapping("/message/queryMyMessage")
public ResponseDTO<PageResult<MessageVO>> query(@RequestBody @Valid MessageQueryForm queryForm) {
RequestUser user = SmartRequestUtil.getRequestUser();
if(user == null){
return ResponseDTO.userErrorParam("用户未登录");
}
queryForm.setSearchCount(false);
queryForm.setReceiverUserId(user.getUserId());
queryForm.setReceiverUserType(user.getUserType().getValue());
return ResponseDTO.ok(messageService.query(queryForm));
}
@Operation(summary = "查询未读消息数量 @luoyi")
@GetMapping("/message/getUnreadCount")
public ResponseDTO<Long> getUnreadCount() {
RequestUser user = SmartRequestUtil.getRequestUser();
if(user == null){
return ResponseDTO.userErrorParam("用户未登录");
}
return ResponseDTO.ok(messageService.getUnreadCount(user.getUserType(), user.getUserId()));
}
@Operation(summary = "更新已读 @luoyi")
@GetMapping("/message/read/{messageId}")
public ResponseDTO<String> updateReadFlag(@PathVariable Long messageId) {
RequestUser user = SmartRequestUtil.getRequestUser();
if(user == null){
return ResponseDTO.userErrorParam("用户未登录");
}
messageService.updateReadFlag(messageId, user.getUserType(), user.getUserId());
return ResponseDTO.ok();
}
}

View File

@ -0,0 +1,46 @@
package net.lab1024.sa.base.module.support.message.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import net.lab1024.sa.base.module.support.message.domain.MessageEntity;
import net.lab1024.sa.base.module.support.message.domain.MessageQueryForm;
import net.lab1024.sa.base.module.support.message.domain.MessageVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 消息 接受者类型枚举
*
* @author luoyi
* @date 2024/06/22 20:20
*/
@Component
@Mapper
public interface MessageDao extends BaseMapper<MessageEntity> {
/**
* 分页查询消息
*
*/
List<MessageVO> query(Page<?> page, @Param("query") MessageQueryForm queryForm);
/**
* 更新已读状态
*/
Integer updateReadFlag(@Param("messageId") Long messageId,
@Param("receiverUserType") Integer receiverUserType,
@Param("receiverUserId") Long receiverUserId,
@Param("readFlag") Boolean readFlag);
/**
* 查询未读消息数
*/
Long getUnreadCount( @Param("receiverUserType") Integer receiverUserType,
@Param("receiverUserId") Long receiverUserId);
}

View File

@ -0,0 +1,70 @@
package net.lab1024.sa.base.module.support.message.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import net.lab1024.sa.base.module.support.message.constant.MessageTypeEnum;
import java.time.LocalDateTime;
/**
* 消息实体
*
* @author luoyi
* @date 2024/06/22 20:20
*/
@Data
@TableName("t_message")
public class MessageEntity {
@TableId(type = IdType.AUTO)
private Long messageId;
/**
* 消息类型
*
* @see MessageTypeEnum
*/
private Integer messageType;
/**
* 接收者类型
*
* @see net.lab1024.sa.base.common.enumeration.UserTypeEnum
*/
private Integer receiverUserType;
/**
* 接收者id
*/
private Long receiverUserId;
/**
* 相关业务id
*/
private String dataId;
/**
* 消息标题
*/
private String title;
/**
* 消息内容
*/
private String content;
/**
* 是否已读
*/
private Boolean readFlag;
/**
* 已读时间
*/
private LocalDateTime readTime;
private LocalDateTime updateTime;
private LocalDateTime createTime;
}

View File

@ -0,0 +1,44 @@
package net.lab1024.sa.base.module.support.message.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import net.lab1024.sa.base.common.domain.PageParam;
import net.lab1024.sa.base.common.swagger.SchemaEnum;
import net.lab1024.sa.base.common.validator.enumeration.CheckEnum;
import net.lab1024.sa.base.module.support.message.constant.MessageTypeEnum;
import org.hibernate.validator.constraints.Length;
import java.time.LocalDate;
/**
* 消息查询form
*
* @author luoyi
* @date 2024/06/22 20:20
*/
@Data
public class MessageQueryForm extends PageParam {
@Schema(description = "搜索词")
@Length(max = 50, message = "搜索词最多50字符")
private String searchWord;
@SchemaEnum(value = MessageTypeEnum.class)
@CheckEnum(value = MessageTypeEnum.class, message = "消息类型")
private Integer messageType;
@Schema(description = "是否已读")
private Boolean readFlag;
@Schema(description = "查询开始时间")
private LocalDate startDate;
@Schema(description = "查询结束时间")
private LocalDate endDate;
@Schema(hidden = true)
private Long receiverUserId;
@Schema(hidden = true)
private Integer receiverUserType;
}

View File

@ -0,0 +1,45 @@
package net.lab1024.sa.base.module.support.message.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import net.lab1024.sa.base.common.enumeration.UserTypeEnum;
import net.lab1024.sa.base.common.swagger.SchemaEnum;
import net.lab1024.sa.base.module.support.message.constant.MessageTypeEnum;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 消息发送form
*
* @author luoyi
* @date 2024/06/22 20:20
*/
@Data
public class MessageSendForm {
@SchemaEnum(value = MessageTypeEnum.class, desc = "消息类型")
@NotNull(message = "消息类型不能为空")
private Integer messageType;
@SchemaEnum(value = UserTypeEnum.class, desc = "接收者类型")
@NotNull(message = "接收者类型不能为空")
private Integer receiverUserType;
@Schema(description = "接收者id")
@NotNull(message = "接收者id不能为空")
private Long receiverUserId;
@Schema(description = "标题")
@NotBlank(message = "标题")
private String title;
@Schema(description = "内容")
@NotBlank(message = "内容")
private String content;
/**
* 相关业务id | 可选
*/
private Object dataId;
}

View File

@ -0,0 +1,41 @@
package net.lab1024.sa.base.module.support.message.domain;
import lombok.Data;
import net.lab1024.sa.base.common.enumeration.UserTypeEnum;
import net.lab1024.sa.base.module.support.message.constant.MessageTemplateEnum;
import javax.validation.constraints.NotNull;
import java.util.Map;
/**
* 消息发送form
*
* @author luoyi
* @date 2024/06/22 20:20
*/
@Data
public class MessageTemplateSendForm {
@NotNull(message = "消息子类型不能为空")
private MessageTemplateEnum messageTemplateEnum;
@NotNull(message = "接收者类型不能为空")
private UserTypeEnum receiverUserType;
@NotNull(message = "接收者id不能为空")
private Long receiverUserId;
/**
* 相关业务id | 可选
* 用于跳转具体业务
*/
private Object dataId;
/**
* 消息参数 | 可选
* 订单号{orderId}{time}所提交的对账单被作废请核实信息重新提交~
* {orderId} {time} 就是消息的参数变量
* 发送消息时 需要在map中放入k->orderId k->time
*/
private Map<String, Object> contentParam;
}

View File

@ -0,0 +1,48 @@
package net.lab1024.sa.base.module.support.message.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import net.lab1024.sa.base.common.enumeration.UserTypeEnum;
import net.lab1024.sa.base.common.swagger.SchemaEnum;
import net.lab1024.sa.base.module.support.message.constant.MessageTypeEnum;
import java.time.LocalDateTime;
/**
* 消息
*
* @author luoyi
* @date 2024/06/22 20:20
*/
@Data
public class MessageVO {
private Long messageId;
@SchemaEnum(value = MessageTypeEnum.class)
private Integer messageType;
@SchemaEnum(value = UserTypeEnum.class)
private Integer receiverUserType;
@Schema(description = "接收者id")
private Long receiverUserId;
@Schema(description = "相关业务id")
private String dataId;
@Schema(description = "消息标题")
private String title;
@Schema(description = "消息内容")
private String content;
@Schema(description = "是否已读")
private Boolean readFlag;
@Schema(description = "已读时间")
private LocalDateTime readTime;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}

View File

@ -0,0 +1,18 @@
package net.lab1024.sa.base.module.support.message.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import net.lab1024.sa.base.module.support.message.dao.MessageDao;
import net.lab1024.sa.base.module.support.message.domain.MessageEntity;
import org.springframework.stereotype.Service;
/**
* 消息manager
*
* @author luoyi
* @date 2024/06/22 20:20
*/
@Service
public class MessageManager extends ServiceImpl<MessageDao, MessageEntity> {
}

View File

@ -0,0 +1,108 @@
package net.lab1024.sa.base.module.support.message.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Lists;
import net.lab1024.sa.base.common.domain.PageResult;
import net.lab1024.sa.base.common.enumeration.UserTypeEnum;
import net.lab1024.sa.base.common.util.SmartBeanUtil;
import net.lab1024.sa.base.common.util.SmartPageUtil;
import net.lab1024.sa.base.module.support.message.constant.MessageTemplateEnum;
import net.lab1024.sa.base.module.support.message.dao.MessageDao;
import net.lab1024.sa.base.module.support.message.domain.*;
import org.apache.commons.text.StringSubstitutor;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author luoyi
* @date 2024/6/27 12:14 上午
*/
@Service
public class MessageService {
@Resource
private MessageDao messageDao;
@Resource
private MessageManager messageManager;
/**
* 分页查询 消息
*/
public PageResult<MessageVO> query(MessageQueryForm queryForm) {
Page page = SmartPageUtil.convert2PageQuery(queryForm);
List<MessageVO> messageVOList = messageDao.query(page, queryForm);
return SmartPageUtil.convert2PageResult(page, messageVOList);
}
/**
* 查询未读消息数量
*/
public Long getUnreadCount(UserTypeEnum userType, Long userId) {
return messageDao.getUnreadCount(userType.getValue(), userId);
}
/**
* 更新已读状态
*/
public void updateReadFlag(Long messageId, UserTypeEnum userType, Long receiverUserId) {
messageDao.updateReadFlag(messageId, userType.getValue(), receiverUserId, true);
}
/**
* 发送模板消息
*/
public void sendTemplateMessage(MessageTemplateSendForm... sendTemplateForms) {
List<MessageSendForm> sendFormList = Lists.newArrayList();
for (MessageTemplateSendForm sendTemplateForm : sendTemplateForms) {
MessageTemplateEnum msgTemplateTypeEnum = sendTemplateForm.getMessageTemplateEnum();
StringSubstitutor stringSubstitutor = new StringSubstitutor(sendTemplateForm.getContentParam());
String content = stringSubstitutor.replace(msgTemplateTypeEnum.getContent());
MessageSendForm messageSendForm = new MessageSendForm();
messageSendForm.setMessageType(msgTemplateTypeEnum.getMessageTypeEnum().getValue());
messageSendForm.setReceiverUserType(sendTemplateForm.getReceiverUserType().getValue());
messageSendForm.setReceiverUserId(sendTemplateForm.getReceiverUserId());
messageSendForm.setTitle(msgTemplateTypeEnum.getDesc());
messageSendForm.setContent(content);
messageSendForm.setDataId(sendTemplateForm.getDataId());
sendFormList.add(messageSendForm);
}
this.sendMessage(sendFormList);
}
/**
* 发送消息
*/
public void sendMessage(MessageSendForm... sendForms) {
this.sendMessage(Lists.newArrayList(sendForms));
}
/**
* 批量发送通知消息
*/
public void sendMessage(List<MessageSendForm> sendList) {
for (MessageSendForm sendDTO : sendList) {
String verify = SmartBeanUtil.verify(sendDTO);
if (null != verify) {
throw new RuntimeException("send msg error: " + verify);
}
}
List<MessageEntity> messageEntityList = sendList.stream().map(e -> {
MessageEntity messageEntity = new MessageEntity();
messageEntity.setMessageType(e.getMessageType());
messageEntity.setReceiverUserType(e.getReceiverUserType());
messageEntity.setReceiverUserId(e.getReceiverUserId());
messageEntity.setDataId(String.valueOf(e.getDataId()));
messageEntity.setTitle(e.getTitle());
messageEntity.setContent(e.getContent());
return messageEntity;
}).collect(Collectors.toList());
messageManager.saveBatch(messageEntityList);
}
}

View File

@ -16,6 +16,11 @@ import lombok.Data;
@Data @Data
public class OperateLogQueryForm extends PageParam { public class OperateLogQueryForm extends PageParam {
@Schema(description = "用户ID")
private Long operateUserId;
@Schema(description = "用户类型")
private Integer operateUserType;
@Schema(description = "开始日期") @Schema(description = "开始日期")
private String startDate; private String startDate;

View File

@ -0,0 +1,139 @@
package net.lab1024.sa.base.module.support.redis;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.exception.BusinessException;
import org.redisson.api.RBucket;
import org.redisson.api.RIdGenerator;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
* Redisson 业务
*
* @author huke
* @date 2024/6/19 20:39
*/
@Slf4j
@Service
public class RedissonService {
@Autowired
private final RedissonClient redissonClient;
public RedissonService(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
public RedissonClient getRedissonClient() {
return redissonClient;
}
/**
* 获取锁 执行程序
*
* @param lockKey
* @param waitTime 毫秒
* @param lockTime 毫秒
* @param supplier
*/
public <T> T executeWithLock(String lockKey, long waitTime, long lockTime, Supplier<T> supplier) {
// 获取锁
RLock lock = this.tryLock(lockKey, waitTime, lockTime);
try {
return supplier.get();
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 获取锁 执行程序
*
* @param lockKey
* @param waitTime 毫秒
* @param lockTime 毫秒
* @param runnable
*/
public void executeWithLock(String lockKey, long waitTime, long lockTime, Runnable runnable) {
// 获取锁
RLock lock = this.tryLock(lockKey, waitTime, lockTime);
try {
runnable.run();
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 尝试获取锁
* 最多等待 waitTime 毫秒
* 获取锁成功后占用 lockTime 毫秒
* ps:需要手动解锁 lock.unlock()
*
* @param lockKey
* @param waitTime 毫秒
* @param lockTime 毫秒
* @return
*/
public RLock tryLock(String lockKey, long waitTime, long lockTime) {
RLock lock = redissonClient.getLock(lockKey);
try {
boolean getLock = lock.tryLock(waitTime, lockTime, TimeUnit.MILLISECONDS);
if (getLock) {
return lock;
}
} catch (InterruptedException e) {
log.error("Redisson tryLock", e);
}
throw new BusinessException("业务繁忙,请稍后重试~");
}
/**
* 获取 id 生成器
* nextId 可生成连续不重复的id
*
* @param key
* @return
*/
public RIdGenerator idGenerator(String key) {
return redissonClient.getIdGenerator(key);
}
/**
* 存放任意数据类型
*
* @param key
* @param v
* @param duration
* @param <T>
*/
public <T> void putObj(String key, T v, Duration duration) {
redissonClient.getBucket(key).set(v, duration);
}
/**
* 获取任意数据类型
*
* @param key
* @param clazz
* @param <T>
* @return 如果没有找到则返回null
*/
public <T> T getObj(String key, Class<T> clazz) {
RBucket<T> bucket = redissonClient.getBucket(key);
return bucket.get();
}
}

View File

@ -6,9 +6,15 @@ import net.lab1024.sa.base.module.support.reload.core.annoation.SmartReload;
import net.lab1024.sa.base.module.support.reload.core.domain.SmartReloadObject; import net.lab1024.sa.base.module.support.reload.core.domain.SmartReloadObject;
import net.lab1024.sa.base.module.support.reload.core.thread.SmartReloadRunnable; import net.lab1024.sa.base.module.support.reload.core.thread.SmartReloadRunnable;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -27,16 +33,29 @@ import java.util.concurrent.TimeUnit;
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/ */
@Slf4j @Slf4j
@Service
public class SmartReloadManager implements BeanPostProcessor { public class SmartReloadManager implements BeanPostProcessor {
private static final String THREAD_NAME_PREFIX = "smart-reload"; private static final String THREAD_NAME_PREFIX = "smart-reload";
private static final int THREAD_COUNT = 1; private static final int THREAD_COUNT = 1;
private Map<String, SmartReloadObject> reloadObjectMap = new ConcurrentHashMap<>(); @Value("${reload.interval-seconds}")
private Integer intervalSeconds;
@Resource
private AbstractSmartReloadCommand reloadCommand;
private final Map<String, SmartReloadObject> reloadObjectMap = new ConcurrentHashMap<>();
private ScheduledThreadPoolExecutor threadPoolExecutor; private ScheduledThreadPoolExecutor threadPoolExecutor;
public SmartReloadManager(AbstractSmartReloadCommand reloadCommand, int intervalSeconds) {
@PostConstruct
public void init() {
if (threadPoolExecutor != null) {
return;
}
this.threadPoolExecutor = new ScheduledThreadPoolExecutor(THREAD_COUNT, r -> { this.threadPoolExecutor = new ScheduledThreadPoolExecutor(THREAD_COUNT, r -> {
Thread t = new Thread(r, THREAD_NAME_PREFIX); Thread t = new Thread(r, THREAD_NAME_PREFIX);
if (!t.isDaemon()) { if (!t.isDaemon()) {
@ -44,17 +63,23 @@ public class SmartReloadManager implements BeanPostProcessor {
} }
return t; return t;
}); });
this.threadPoolExecutor.scheduleWithFixedDelay(new SmartReloadRunnable(reloadCommand), 10, intervalSeconds, TimeUnit.SECONDS); this.threadPoolExecutor.scheduleWithFixedDelay(new SmartReloadRunnable(this.reloadCommand), 10, this.intervalSeconds, TimeUnit.SECONDS);
reloadCommand.setReloadManager(this); this.reloadCommand.setReloadManager(this);
}
@PreDestroy
public void shutdown() {
if (this.threadPoolExecutor != null) {
this.threadPoolExecutor.shutdownNow();
this.threadPoolExecutor = null;
}
} }
@Override @Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(bean.getClass()); Method[] methods = ReflectionUtils.getAllDeclaredMethods(bean.getClass());
if (methods == null) {
return bean;
}
for (Method method : methods) { for (Method method : methods) {
SmartReload smartReload = method.getAnnotation(SmartReload.class); SmartReload smartReload = method.getAnnotation(SmartReload.class);
if (smartReload == null) { if (smartReload == null) {

View File

@ -140,3 +140,16 @@ sa-token:
is-print: false is-print: false
# 是否从cookie读取token # 是否从cookie读取token
is-read-cookie: false is-read-cookie: false
# SmartJob 定时任务配置(不需要可以直接删除以下配置详细文档请看https://www.xxxxxx.com)
smart:
job:
enabled: true
# 任务初始化延迟 默认30秒 可选
init-delay: 10
# 定时任务执行线程池数量 默认2 可选
core-pool-size: 2
# 数据库配置检测-开关 默认开启 可选(作用是固定间隔读取数据库配置更新任务,关闭后只能重启服务或通过接口修改定时任务,建议开启)
db-refresh-enabled: true
# 数据库配置检测-执行间隔 默认120秒 可选
db-refresh-interval: 60

View File

@ -7,6 +7,15 @@
* *
from t_login_log from t_login_log
<where> <where>
<if test="query.userId != null">
AND user_id = #{query.userId}
</if>
<if test="query.userType != null">
AND user_type = #{query.userType}
</if>
<if test="query.ip != null">
AND INSTR(login_ip,#{query.ip})
</if>
<if test="query.startDate != null and query.startDate != ''"> <if test="query.startDate != null and query.startDate != ''">
AND DATE_FORMAT(create_time, '%Y-%m-%d') &gt;= #{query.startDate} AND DATE_FORMAT(create_time, '%Y-%m-%d') &gt;= #{query.startDate}
</if> </if>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.lab1024.sa.base.module.support.message.dao.MessageDao">
<!-- 更新已读状态 -->
<update id="updateReadFlag">
UPDATE t_message
SET read_flag = #{readFlag},
read_time = now()
WHERE message_id = #{messageId}
AND receiver_user_type = #{receiverUserType}
AND receiver_user_id = #{receiverUserId}
AND read_flag != #{readFlag}
</update>
<!-- 分页查询消息 -->
<select id="query" resultType="net.lab1024.sa.base.module.support.message.domain.MessageVO">
SELECT * FROM t_message
<where>
<if test="query.receiverUserType != null">
AND receiver_user_type = #{query.receiverUserType}
</if>
<if test="query.receiverUserId != null">
AND receiver_user_id = #{query.receiverUserId}
</if>
<if test="query.messageType != null">
AND message_type = #{query.messageType}
</if>
<if test="query.searchWord != null and query.searchWord !=''">
AND ( INSTR(title,#{query.searchWord})
OR INSTR(content,#{query.searchWord})
)
</if>
<if test="query.readFlag != null">
AND read_flag = #{query.readFlag}
</if>
<if test="query.startDate != null">
AND DATE_FORMAT(create_time, '%Y-%m-%d') &gt;= DATE_FORMAT(#{query.startDate}, '%Y-%m-%d')
</if>
<if test="query.endDate != null">
AND DATE_FORMAT(create_time, '%Y-%m-%d') &lt;= DATE_FORMAT(#{query.endDate}, '%Y-%m-%d')
</if>
</where>
<if test="query.sortItemList == null or query.sortItemList.size == 0">
ORDER BY message_id DESC
</if>
</select>
<select id="getUnreadCount" resultType="java.lang.Long">
SELECT count(1)
FROM t_message
where receiver_user_type = #{receiverUserType}
AND receiver_user_id = #{receiverUserId}
AND read_flag = false
</select>
</mapper>

View File

@ -7,6 +7,12 @@
* *
from t_operate_log from t_operate_log
<where> <where>
<if test="query.operateUserId != null">
AND operate_user_id = #{query.operateUserId}
</if>
<if test="query.operateUserType != null">
AND operate_user_type = #{query.operateUserType}
</if>
<if test="query.startDate != null and query.startDate != ''"> <if test="query.startDate != null and query.startDate != ''">
AND DATE_FORMAT(create_time, '%Y-%m-%d') &gt;= #{query.startDate} AND DATE_FORMAT(create_time, '%Y-%m-%d') &gt;= #{query.startDate}
</if> </if>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.lab1024.sa.base.module.support.job.repository.SmartJobLogDao">
<!-- 定时任务-执行记录-分页查询 -->
<select id="query" resultType="net.lab1024.sa.base.module.support.job.api.domain.SmartJobLogVO">
SELECT *
FROM t_smart_job_log
<where>
<if test="query.searchWord != null and query.searchWord != ''">
AND ( INSTR(job_name,#{query.searchWord})
OR INSTR(param,#{query.searchWord})
OR INSTR(execute_result,#{query.searchWord})
OR INSTR(create_name,#{query.searchWord})
)
</if>
<if test="query.jobId != null">
AND job_id = #{query.jobId}
</if>
<if test="query.successFlag != null">
AND success_flag = #{query.successFlag}
</if>
<if test="query.startTime != null">
AND DATE_FORMAT(execute_start_time, '%Y-%m-%d') &gt;= #{query.startTime}
</if>
<if test="query.endTime != null">
AND DATE_FORMAT(execute_start_time, '%Y-%m-%d') &lt;= #{query.endTime}
</if>
</where>
<if test="query.sortItemList == null or query.sortItemList.size == 0">
ORDER BY log_id DESC
</if>
</select>
</mapper>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.lab1024.sa.base.module.support.job.repository.SmartJobDao">
<!-- 定时任务-分页查询 -->
<select id="query" resultType="net.lab1024.sa.base.module.support.job.api.domain.SmartJobVO">
SELECT *
FROM t_smart_job
<where>
<if test="query.searchWord != null and query.searchWord != ''">
AND ( INSTR(job_name,#{query.searchWord})
OR INSTR(job_class,#{query.searchWord})
OR INSTR(trigger_value,#{query.searchWord})
)
</if>
<if test="query.triggerType != null">
AND trigger_type = #{query.triggerType}
</if>
<if test="query.enabledFlag != null">
AND enabled_flag = #{query.enabledFlag}
</if>
</where>
<if test="query.sortItemList == null or query.sortItemList.size == 0">
ORDER BY sort ASC,job_id DESC
</if>
</select>
</mapper>

View File

@ -142,3 +142,16 @@ sa-token:
is-print: false is-print: false
# 是否从cookie读取token # 是否从cookie读取token
is-read-cookie: false is-read-cookie: false
# SmartJob 定时任务配置(不需要可以直接删除以下配置详细文档请看https://www.xxxxxx.com)
smart:
job:
enabled: true
# 任务初始化延迟 默认30秒 可选
init-delay: 10
# 定时任务执行线程池数量 默认2 可选
core-pool-size: 2
# 数据库配置检测-开关 默认开启 可选(作用是固定间隔读取数据库配置更新任务,关闭后只能重启服务或通过接口修改定时任务,建议开启)
db-refresh-enabled: true
# 数据库配置检测-执行间隔 默认120秒 可选
db-refresh-interval: 60

View File

@ -1,9 +1,9 @@
spring: spring:
# 数据库连接信息 # 数据库连接信息
datasource: datasource:
url: jdbc:mysql://127.0.0.1:3306/smart_admin_v3?autoReconnect=true&useServerPreparedStmts=false&rewriteBatchedStatements=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai url: jdbc:mysql://127.0.0.1:3306/smart_admin_v3_dev?autoReconnect=true&useServerPreparedStmts=false&rewriteBatchedStatements=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai
username: root username: root
password: Zhuoda1024lab password: Zhuoda#1024lab
initial-size: 10 initial-size: 10
min-idle: 10 min-idle: 10
max-active: 100 max-active: 100
@ -108,7 +108,7 @@ http:
keep-alive: 300000 keep-alive: 300000
# 跨域配置 # 跨域配置
access-control-allow-origin: 'smartadmin.vip' access-control-allow-origin: 'https://preview.smartadmin.vip'
# 心跳配置 # 心跳配置
heart-beat: heart-beat:
@ -142,3 +142,16 @@ sa-token:
is-print: false is-print: false
# 是否从cookie读取token # 是否从cookie读取token
is-read-cookie: false is-read-cookie: false
# SmartJob 定时任务配置(不需要可以直接删除以下配置详细文档请看https://www.xxxxxx.com)
smart:
job:
enabled: true
# 任务初始化延迟 默认30秒 可选
init-delay: 10
# 定时任务执行线程池数量 默认2 可选
core-pool-size: 2
# 数据库配置检测-开关 默认开启 可选(作用是固定间隔读取数据库配置更新任务,关闭后只能重启服务或通过接口修改定时任务,建议开启)
db-refresh-enabled: true
# 数据库配置检测-执行间隔 默认120秒 可选
db-refresh-interval: 60

View File

@ -74,9 +74,9 @@ file:
upload-path: /home/smart_admin_v3/upload/ #文件上传目录 upload-path: /home/smart_admin_v3/upload/ #文件上传目录
url-prefix: url-prefix:
cloud: cloud:
region: oss-cn-qingdao region: oss-cn-hangzhou
endpoint: oss-cn-qingdao.aliyuncs.com endpoint: oss-cn-hangzhou.aliyuncs.com
bucket-name: common bucket-name: 1024lab-smart-admin
access-key: access-key:
secret-key: secret-key:
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/ url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
@ -89,6 +89,7 @@ springdoc:
enabled: true # 开关 enabled: true # 开关
doc-expansion: none #关闭展开 doc-expansion: none #关闭展开
tags-sorter: alpha tags-sorter: alpha
server-base-url: http://smartadmin.dev.1024lab.net/api/
api-docs: api-docs:
enabled: true # 开关 enabled: true # 开关
knife4j: knife4j:
@ -142,3 +143,16 @@ sa-token:
is-print: false is-print: false
# 是否从cookie读取token # 是否从cookie读取token
is-read-cookie: false is-read-cookie: false
# SmartJob 定时任务配置(不需要可以直接删除以下配置详细文档请看https://www.xxxxxx.com)
smart:
job:
enabled: true
# 任务初始化延迟 默认30秒 可选
init-delay: 10
# 定时任务执行线程池数量 默认2 可选
core-pool-size: 2
# 数据库配置检测-开关 默认开启 可选(作用是固定间隔读取数据库配置更新任务,关闭后只能重启服务或通过接口修改定时任务,建议开启)
db-refresh-enabled: true
# 数据库配置检测-执行间隔 默认120秒 可选
db-refresh-interval: 60

View File

@ -1,3 +1,3 @@
NODE_ENV=development NODE_ENV=development
VITE_APP_TITLE='SmartAdmin 开发环境(Dev)' VITE_APP_TITLE='SmartAdmin 开发环境(Dev)'
VITE_APP_API_URL='http://127.0.0.1:1024' VITE_APP_API_URL='http://smartadmin.dev.1024lab.net/api/'

View File

@ -1,3 +1,3 @@
NODE_ENV=production NODE_ENV=production
VITE_APP_TITLE='SmartAdmin 测试环境(Test)' VITE_APP_TITLE='SmartAdmin 测试环境(Test)'
VITE_APP_API_URL='http://127.0.0.1:1024' VITE_APP_API_URL='http://smartadmin.dev.1024lab.net/api/'

View File

@ -11,15 +11,16 @@
"scripts": { "scripts": {
"localhost": "vite --mode localhost", "localhost": "vite --mode localhost",
"dev": "vite", "dev": "vite",
"build:test": "vite build --mode test", "build:test": "vite build --base=/admin/ --mode test",
"build:pre": "vite build --mode pre", "build:pre": "vite build --mode pre",
"build:prod": "vite build --mode production" "build:prod": "vite build --mode production"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^6.1.0",
"@wangeditor/editor": "5.1.14", "@wangeditor/editor": "5.1.14",
"@wangeditor/editor-for-vue": "5.1.12", "@wangeditor/editor-for-vue": "5.1.12",
"ant-design-vue": "4.2.0", "ant-design-vue": "4.2.1",
"axios": "1.6.8", "axios": "1.6.8",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"crypto-js": "4.1.1", "crypto-js": "4.1.1",
@ -37,14 +38,15 @@
"sortablejs": "1.15.0", "sortablejs": "1.15.0",
"ua-parser-js": "1.0.35", "ua-parser-js": "1.0.35",
"v-viewer": "~1.6.4", "v-viewer": "~1.6.4",
"vue": "3.3.13", "vue": "3.4.27",
"vue-i18n": "9.10.2", "vue-i18n": "9.13.1",
"vue-router": "4.3.0", "vue-router": "4.3.2",
"vue3-json-viewer": "2.2.2" "vue3-json-viewer": "2.2.2",
"vue3-tabs-chrome": "^0.3.3"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"@vue/compiler-sfc": "3.4.21", "@vue/compiler-sfc": "3.4.27",
"eslint": "^8.16.0", "eslint": "^8.16.0",
"eslint-config-prettier": "~9.0.0", "eslint-config-prettier": "~9.0.0",
"eslint-plugin-prettier": "~5.0.0", "eslint-plugin-prettier": "~5.0.0",
@ -58,7 +60,7 @@
"stylelint-config-standard": "~25.0.0", "stylelint-config-standard": "~25.0.0",
"stylelint-order": "~5.0.0", "stylelint-order": "~5.0.0",
"terser": "~5.29.2", "terser": "~5.29.2",
"vite": "5.2.6", "vite": "5.2.12",
"vue-eslint-parser": "~9.4.2" "vue-eslint-parser": "~9.4.2"
}, },
"engines": { "engines": {

View File

@ -0,0 +1,34 @@
/*
* job api
*
* @Author: huke
* @Date: 2024/06/25
*/
import { postRequest, getRequest } from '/src/lib/axios';
export const jobApi = {
// 分页查询 @huke
queryJob: (param) => {
return postRequest('/support/job/query', param);
},
// 定时任务-查询详情 @huke
queryJobInfo: (param) => {
return getRequest(`/support/job/${param}`);
},
// 执行任务 @huke
executeJob: (param) => {
return postRequest('/support/job/execute', param);
},
// 定时任务-更新-任务信息 @huke
updateJob: (param) => {
return postRequest('/support/job/update', param);
},
// 定时任务-更新-开启状态 @huke
updateJobEnabled: (param) => {
return postRequest('/support/job/update/enabled', param);
},
// 定时任务-执行记录-分页查询 @huke
queryJobLog: (param) => {
return postRequest('/support/job/log/query', param);
},
};

View File

@ -14,4 +14,8 @@ export const loginLogApi = {
queryList: (param) => { queryList: (param) => {
return postRequest('/support/loginLog/page/query', param); return postRequest('/support/loginLog/page/query', param);
}, },
// 分页查询当前登录人信息 @author 善逸
queryListLogin: (param) => {
return postRequest('/support/loginLog/page/query/login', param);
},
}; };

View File

@ -0,0 +1,16 @@
import { getRequest, postRequest } from '/src/lib/axios';
export const messageApi = {
// 通知消息-分页查询
queryMessage: (param) => {
return postRequest('/support/message/queryMyMessage', param);
},
// 通知消息-查询未读消息数
queryUnreadCount: () => {
return getRequest('/support/message/getUnreadCount');
},
// 通知消息-标记已读
updateReadFlag: (messageId) => {
return getRequest(`/support/message/read/${messageId}`);
},
};

View File

@ -18,4 +18,8 @@ export const operateLogApi = {
detail: (id) => { detail: (id) => {
return getRequest(`/support/operateLog/detail/${id}`); return getRequest(`/support/operateLog/detail/${id}`);
}, },
// 分页查询当前登录人信息 @author 善逸
queryListLogin: (param) => {
return postRequest('/support/operateLog/page/query/login', param);
},
}; };

View File

@ -35,6 +35,18 @@ export const employeeApi = {
updateEmployee: (params) => { updateEmployee: (params) => {
return postRequest('/employee/update', params); return postRequest('/employee/update', params);
}, },
/**
* 更新登录人信息
*/
updateByLogin: (params) => {
return postRequest('/employee/update/login', params);
},
/**
* 更新登录人头像
*/
updateAvatar: (params) => {
return postRequest('/employee/update/avatar', params);
},
/** /**
* 删除员工 * 删除员工
*/ */

View File

@ -0,0 +1,55 @@
/**
* 职务表 api 封装
*
* @Author: kaiyun
* @Date: 2024-06-23 23:31:38
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
import { postRequest, getRequest } from '/@/lib/axios';
export const positionApi = {
/**
* 分页查询 @author kaiyun
*/
queryPage : (param) => {
return postRequest('/position/queryPage', param);
},
/**
* 增加 @author kaiyun
*/
add: (param) => {
return postRequest('/position/add', param);
},
/**
* 修改 @author kaiyun
*/
update: (param) => {
return postRequest('/position/update', param);
},
/**
* 删除 @author kaiyun
*/
delete: (id) => {
return getRequest(`/position/delete/${id}`);
},
/**
* 批量删除 @author kaiyun
*/
batchDelete: (idList) => {
return postRequest('/position/batchDelete', idList);
},
/**
* 查询列表 @author kaiyun
*/
queryList: () => {
return getRequest('/position/queryList');
},
};

View File

@ -0,0 +1,79 @@
<!--
* 职位
*
* @Author: 开云
* @Date: 2024-06-27 23:09:02
* @Wechat: kaiyun
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-select
v-model:value="selectValue"
:style="`width: ${width}`"
:placeholder="props.placeholder"
:showSearch="true"
:allowClear="true"
:size="size"
@change="onChange"
>
<a-select-option v-for="item in positionList" :key="item.positionId" :value="item.positionId">
{{ item.positionName }}
</a-select-option>
</a-select>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue';
import { smartSentry } from '/@/lib/smart-sentry';
import { positionApi } from '/@/api/system/position-api.js';
// =========== =============
const props = defineProps({
value: [Number, Array],
placeholder: {
type: String,
default: '请选择',
},
width: {
type: String,
default: '100%',
},
size: {
type: String,
default: 'default',
},
});
const emit = defineEmits(['update:value', 'change']);
// =========== =============
//
const positionList = ref([]);
async function query() {
try {
let resp = await positionApi.queryList();
positionList.value = resp.data;
} catch (e) {
smartSentry.captureError(e);
}
}
onMounted(query);
// =========== =============
const selectValue = ref(props.value);
watch(
() => props.value,
(newValue) => {
selectValue.value = newValue;
}
);
function onChange(value) {
emit('update:value', value);
emit('change', value);
}
</script>

Some files were not shown because too many files have changed in this diff Show More