diff --git a/.gitignore b/.gitignore index 9154f4c..9a7f05e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,45 @@ -# ---> Java -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ +!**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + + +### custom ### +.flattened-pom.xml +/**/application-local.yml +/logs/ + +/log/ +*/log/ + diff --git a/README.md b/README.md index 51b19ee..9d907bf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # marketing-scrm +百业营销 diff --git a/admin/.flattened-pom.xml b/admin/.flattened-pom.xml new file mode 100644 index 0000000..db1808a --- /dev/null +++ b/admin/.flattened-pom.xml @@ -0,0 +1,128 @@ + + + 4.0.0 + + com.baiye + marketing-scrm + 1.0.0 + + com.baiye + admin + 1.0.0 + + 3.0.3 + + + + com.baiye + security-oauth2-authorization-server + ${revision} + + + com.baiye + admin-websocket + + + com.baiye + marketing-scrm-starter-job + + + com.baiye + admin-core + ${revision} + + + org.springframework.boot + spring-boot-starter-web + + + com.baiye + common-redis + ${revision} + + + com.mysql + mysql-connector-j + + + org.springdoc + springdoc-openapi-ui + + + org.springdoc + springdoc-openapi-security + + + com.github.xiaoymin + knife4j-springdoc-ui + ${knife4j.version} + + + com.github.whvcse + easy-captcha + + + jakarta.xml.bind + jakarta.xml.bind-api + + + org.glassfish.jaxb + jaxb-runtime + + + cn.hutool + hutool-all + + + com.alibaba + easyexcel + + + org.springframework.boot + spring-boot-test + + + org.springframework + spring-test + test + + + junit + junit + test + + + com.google.code.gson + gson + + + com.baiye + marketing-scrm-starter-easyexcel + ${revision} + + + com.baiye + common-idempotent + + + com.alipay.sdk + alipay-easysdk + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + diff --git a/admin/pom.xml b/admin/pom.xml new file mode 100644 index 0000000..8f69cbc --- /dev/null +++ b/admin/pom.xml @@ -0,0 +1,170 @@ + + + + marketing-scrm + com.baiye + ${revision} + + 4.0.0 + + admin + + + 3.0.3 + + + + + + com.baiye + security-oauth2-authorization-server + ${revision} + + + + com.baiye + admin-websocket + + + + com.baiye + marketing-scrm-starter-job + + + + + com.baiye + admin-core + ${revision} + + + + org.springframework.boot + spring-boot-starter-web + + + + com.baiye + common-redis + ${revision} + + + + + com.mysql + mysql-connector-j + + + + + + + + + + + org.springdoc + springdoc-openapi-ui + + + org.springdoc + springdoc-openapi-security + + + + + com.github.xiaoymin + knife4j-springdoc-ui + ${knife4j.version} + + + + + com.github.whvcse + easy-captcha + + + + + + jakarta.xml.bind + jakarta.xml.bind-api + + + org.glassfish.jaxb + jaxb-runtime + + + + + cn.hutool + hutool-all + + + + com.alibaba + easyexcel + + + + + org.springframework.boot + spring-boot-test + + + + org.springframework + spring-test + test + + + + junit + junit + test + + + + com.google.code.gson + gson + + + + com.baiye + marketing-scrm-starter-easyexcel + ${revision} + + + + com.baiye + common-idempotent + + + + com.alipay.sdk + alipay-easysdk + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + diff --git a/admin/src/main/java/com/baiye/AdminApplication.java b/admin/src/main/java/com/baiye/AdminApplication.java new file mode 100644 index 0000000..11f0bff --- /dev/null +++ b/admin/src/main/java/com/baiye/AdminApplication.java @@ -0,0 +1,26 @@ +package com.baiye; + +import org.ballcat.springsecurity.oauth2.server.authorization.annotation.EnableOauth2AuthorizationServer; +import org.ballcat.springsecurity.oauth2.server.resource.annotation.EnableOauth2ResourceServer; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author Hccake + */ +@EnableTransactionManagement +@EnableOauth2AuthorizationServer +@EnableOauth2ResourceServer +@MapperScan({ "com.baiye.**.mapper" }) +@SpringBootApplication +@EnableScheduling +public class AdminApplication { + + public static void main(String[] args) { + SpringApplication.run(AdminApplication.class, args); + } + +} diff --git a/admin/src/main/java/com/baiye/aspect/SysUserAspect.java b/admin/src/main/java/com/baiye/aspect/SysUserAspect.java new file mode 100644 index 0000000..7a9d136 --- /dev/null +++ b/admin/src/main/java/com/baiye/aspect/SysUserAspect.java @@ -0,0 +1,118 @@ +//package com.baiye.aspect; +// +//import cn.hutool.core.collection.CollUtil; +//import com.baiye.modules.distribute.entity.*; +//import com.baiye.modules.distribute.mapper.ClueMapper; +//import com.baiye.modules.distribute.mapper.ClueStageMapper; +//import com.baiye.modules.distribute.mapper.LabelMapper; +//import com.baiye.modules.distribute.mapper.LabelOrganizeMapper; +//import com.baiye.modules.distribute.service.ClueRecordService; +//import com.baiye.modules.distribute.service.DistributeTaskService; +//import com.baiye.security.util.SecurityUtils; +//import com.baiye.system.mapper.SysUserMapper; +//import com.baiye.system.model.dto.SysUserDTO; +//import com.baiye.system.model.entity.SysUser; +//import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +//import com.baomidou.mybatisplus.core.mapper.BaseMapper; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.aspectj.lang.JoinPoint; +//import org.aspectj.lang.annotation.AfterReturning; +//import org.aspectj.lang.annotation.Aspect; +//import org.aspectj.lang.annotation.Before; +//import org.aspectj.lang.annotation.Pointcut; +//import org.springframework.stereotype.Component; +// +//import java.util.List; +//import java.util.stream.Collectors; +// +///** +// * 系统用户方法AOP +// */ +//@Aspect +//@Component +//@Slf4j +//@RequiredArgsConstructor +//public class SysUserAspect { +// +// private final ClueRecordService clueRecordService; +// +// private final ClueMapper clueMapper; +// +// private final SysUserMapper sysUserMapper; +// +// private final DistributeTaskService distributeTaskService; +// +// private final LabelMapper labelMapper; +// +// private final LabelOrganizeMapper labelOrganizeMapper; +// +// private final ClueStageMapper clueStageMapper; +// +// /** +// * 配置切入点 +// */ +// @Pointcut("execution(* com.baiye.system.service.impl.SysUserServiceImpl.addSysUser(..))") +// public void asyncAddSysUser() { +// } +// +// /** +// * 配置切入点 +// */ +// @Pointcut("execution(* com.baiye.system.service.impl.SysUserServiceImpl.deleteByUserId(..))") +// public void asyncDeleteByUserId() { +// } +// +// @AfterReturning("asyncAddSysUser()") +// public void beforeAddSysUser(JoinPoint joinPoint) { +// log.info("==========创建用户开始执行AOP切入业务=========="); +// // 获取目标对象方法参数,添加业务管理员账号的默认上传记录 +// if (SecurityUtils.getCurrentUserId() == 1) { +// Object[] args = joinPoint.getArgs(); +// if (args[0] != null && args[0] instanceof SysUserDTO) { +// SysUserDTO sysUserDTO = (SysUserDTO) args[0]; +// String username = sysUserDTO.getUsername(); +// SysUser sysUser = sysUserMapper.selectByUsername(username); +// if (sysUser != null && sysUser.getUserId() != null) { +// // 创建默认的文件记录 +// ClueRecordEntity entity = clueRecordService.addDefaultRecordService(sysUser.getUserId(), "资源推送", +// "资源推送"); +// // 创建默认的执行任务 +// distributeTaskService.addDefaultTask("资源推送", null, entity.getClueRecordId().toString(), +// sysUser.getUserId(), null); +// } +// } +// } +// } +// +// @Before("asyncDeleteByUserId()") +// public void beforeDeleteByUserId(JoinPoint joinPoint) { +// log.info("==========删除用户开始执行AOP切入业务=========="); +// Object[] args = joinPoint.getArgs(); +// Object obj = args[0]; +// if (obj instanceof Long) { +// // 查询下级是否有子用户 +// List userList = sysUserMapper +// .selectList(new LambdaQueryWrapper().eq(SysUser::getWhichUserId, obj)); +// if (CollUtil.isNotEmpty(userList)) { +// List userIdList = userList.stream().map(SysUser::getUserId).collect(Collectors.toList()); +// userIdList.add((Long) obj); +// clueMapper.delete(new LambdaQueryWrapper().in(ClueEntity::getAssignedBy, userIdList)); +// } +// clueMapper.delete(new LambdaQueryWrapper().eq(ClueEntity::getAssignedBy, obj)); +// +// // 删除用户的任务和上传记录 +// BaseMapper taskMapper = distributeTaskService.getBaseMapper(); +// taskMapper +// .delete(new LambdaQueryWrapper().eq(DistributeTaskEntity::getCreateBy, obj)); +// BaseMapper clueRecordMapper = clueRecordService.getBaseMapper(); +// clueRecordMapper.delete(new LambdaQueryWrapper().eq(ClueRecordEntity::getCreateBy, obj)); +// // 删除用户标签和标签组 - 阶段信息 +// labelMapper.delete(new LambdaQueryWrapper().eq(LabelEntity::getCreateBy, obj)); +// labelOrganizeMapper +// .delete(new LambdaQueryWrapper().eq(LabelOrganizeEntity::getCreateBy, obj)); +// clueStageMapper.delete(new LambdaQueryWrapper().eq(ClueStageEntity::getCreateBy, obj)); +// } +// } +// +//} diff --git a/admin/src/main/java/com/baiye/captcha/CaptchaController.java b/admin/src/main/java/com/baiye/captcha/CaptchaController.java new file mode 100644 index 0000000..fdce238 --- /dev/null +++ b/admin/src/main/java/com/baiye/captcha/CaptchaController.java @@ -0,0 +1,37 @@ +package com.baiye.captcha; + +import com.baiye.exception.BadRequestException; +import com.baiye.result.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.ballcat.security.captcha.IValidateCodeService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +/** + * 验证码生成 + */ +@RequiredArgsConstructor +@RestController +@RequestMapping("/captcha") +@Tag(name = "验证码") +public class CaptchaController { + + private final IValidateCodeService codeService; + + @GetMapping("/code") + @Operation(summary = "获取验证码") + public R getCode() { + Map captcha; + try { + captcha = this.codeService.createCapcha(); + } catch (Exception e) { + throw new BadRequestException("生成验证码错误"); + } + return R.ok(captcha); + } +} diff --git a/admin/src/main/java/com/baiye/captcha/config/ArithmeticCaptchaAbstractRewrite.java b/admin/src/main/java/com/baiye/captcha/config/ArithmeticCaptchaAbstractRewrite.java new file mode 100644 index 0000000..7c60499 --- /dev/null +++ b/admin/src/main/java/com/baiye/captcha/config/ArithmeticCaptchaAbstractRewrite.java @@ -0,0 +1,51 @@ +package com.baiye.captcha.config; + +import com.wf.captcha.base.Captcha; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +/** + * 验证码源码复制重写{@link ArithmeticCaptchaRewrite} 待修改 + */ +public abstract class ArithmeticCaptchaAbstractRewrite extends Captcha { + private String arithmeticString; + + public ArithmeticCaptchaAbstractRewrite() { + this.setLen(2); + } + + protected char[] alphas() { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < this.len; ++i) { + sb.append(num(10)); + if (i < this.len - 1) { + sb.append("+"); + } + } + + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName("javascript"); + + try { + this.chars = String.valueOf(engine.eval(sb.toString().replaceAll("x", "*"))); + } catch (ScriptException var5) { + var5.printStackTrace(); + } + + sb.append("=?"); + this.arithmeticString = sb.toString(); + return this.chars.toCharArray(); + } + + public String getArithmeticString() { + this.checkAlpha(); + return this.arithmeticString; + } + + public void setArithmeticString(String arithmeticString) { + this.arithmeticString = arithmeticString; + } +} diff --git a/admin/src/main/java/com/baiye/captcha/config/ArithmeticCaptchaRewrite.java b/admin/src/main/java/com/baiye/captcha/config/ArithmeticCaptchaRewrite.java new file mode 100644 index 0000000..09d90a4 --- /dev/null +++ b/admin/src/main/java/com/baiye/captcha/config/ArithmeticCaptchaRewrite.java @@ -0,0 +1,78 @@ +package com.baiye.captcha.config; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; + +/** + * 验证码源码复制重写{@link ArithmeticCaptchaAbstractRewrite} 待修改 + */ +public class ArithmeticCaptchaRewrite extends ArithmeticCaptchaAbstractRewrite { + public ArithmeticCaptchaRewrite() { + } + + public ArithmeticCaptchaRewrite(int width, int height) { + this(); + this.setWidth(width); + this.setHeight(height); + } + + public ArithmeticCaptchaRewrite(int width, int height, int len) { + this(width, height); + this.setLen(len); + } + + public ArithmeticCaptchaRewrite(int width, int height, int len, Font font) { + this(width, height, len); + this.setFont(font); + } + + public boolean out(OutputStream out) { + this.checkAlpha(); + return this.graphicsImage(this.getArithmeticString().toCharArray(), out); + } + + public String toBase64() { + return this.toBase64("data:image/png;base64,"); + } + + private boolean graphicsImage(char[] strs, OutputStream out) { + try { + BufferedImage bi = new BufferedImage(this.width, this.height, 1); + Graphics2D g2d = (Graphics2D)bi.getGraphics(); + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, this.width, this.height); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + this.drawOval(2, g2d); + g2d.setFont(this.getFont()); + FontMetrics fontMetrics = g2d.getFontMetrics(); + int fW = this.width / strs.length; + int fSp = (fW - (int)fontMetrics.getStringBounds("8", g2d).getWidth()) / 2; + + for(int i = 0; i < strs.length; ++i) { + g2d.setColor(this.color()); + int fY = this.height - (this.height - (int)fontMetrics.getStringBounds(String.valueOf(strs[i]), g2d).getHeight() >> 1); + g2d.drawString(String.valueOf(strs[i]), i * fW + fSp + 3, fY - 3); + } + + g2d.dispose(); + ImageIO.write(bi, "png", out); + out.flush(); + boolean var20 = true; + return var20; + } catch (IOException var18) { + var18.printStackTrace(); + } finally { + try { + out.close(); + } catch (IOException var17) { + var17.printStackTrace(); + } + + } + + return false; + } +} diff --git a/admin/src/main/java/com/baiye/captcha/config/CaptchaConfig.java b/admin/src/main/java/com/baiye/captcha/config/CaptchaConfig.java new file mode 100644 index 0000000..d14372b --- /dev/null +++ b/admin/src/main/java/com/baiye/captcha/config/CaptchaConfig.java @@ -0,0 +1,47 @@ +package com.baiye.captcha.config; + +import com.baiye.captcha.enums.CaptchaEnum; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.annotation.Configuration; + +/** + * 验证码配置 + */ +@Getter +@Setter +@Configuration +public class CaptchaConfig { + /** + * 验证码配置 + */ + private CaptchaEnum codeType = CaptchaEnum.arithmetic; + /** + * 验证码有效期 分钟 + */ + private Long expiration = 2L; + /** + * 验证码内容长度 + */ + private int length = 2; + /** + * 验证码宽度 + */ + private int width = 111; + /** + * 验证码高度 + */ + private int height = 36; + /** + * 验证码字体 + */ + private String fontName; + /** + * 字体大小 + */ + private int fontSize = 25; + + public CaptchaEnum getCodeType() { + return codeType; + } +} diff --git a/admin/src/main/java/com/baiye/captcha/config/KaptchaTextCreator.java b/admin/src/main/java/com/baiye/captcha/config/KaptchaTextCreator.java new file mode 100644 index 0000000..da5ec65 --- /dev/null +++ b/admin/src/main/java/com/baiye/captcha/config/KaptchaTextCreator.java @@ -0,0 +1,80 @@ +package com.baiye.captcha.config; + +import com.baiye.captcha.enums.CaptchaEnum; +import com.wf.captcha.*; +import com.wf.captcha.base.Captcha; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import java.awt.*; +import java.util.Objects; + +/** + * @description 验证码文本生成器 + */ +@Component +public class KaptchaTextCreator { + + private final CaptchaConfig captchaConfig; + + public KaptchaTextCreator(CaptchaConfig captchaConfig) { + this.captchaConfig = captchaConfig; + } + + /** + * 获取验证码生产类 + * + * @return / + */ + public Captcha getCaptcha() { + if (Objects.isNull(captchaConfig)) { + if (Objects.isNull(captchaConfig.getCodeType())) { + captchaConfig.setCodeType(CaptchaEnum.arithmetic); + } + } + return switchCaptcha(captchaConfig); + } + + /** + * 依据配置信息生产验证码 + * + * @param captchaConfig 验证码配置信息 + * @return / + */ + private Captcha switchCaptcha(CaptchaConfig captchaConfig) { + Captcha captcha; + synchronized (this) { + switch (captchaConfig.getCodeType()) { + case arithmetic: + // 算术类型 https://gitee.com/whvse/EasyCaptcha + //captcha = new ArithmeticCaptcha(captchaConfig.getWidth(), captchaConfig.getHeight()); + captcha = new ArithmeticCaptchaRewrite(captchaConfig.getWidth(), captchaConfig.getHeight()); + // 几位数运算,默认是两位 + captcha.setLen(captchaConfig.getLength()); + break; + case chinese: + captcha = new ChineseCaptcha(captchaConfig.getWidth(), captchaConfig.getHeight()); + captcha.setLen(captchaConfig.getLength()); + break; + case chinese_gif: + captcha = new ChineseGifCaptcha(captchaConfig.getWidth(), captchaConfig.getHeight()); + captcha.setLen(captchaConfig.getLength()); + break; + case gif: + captcha = new GifCaptcha(captchaConfig.getWidth(), captchaConfig.getHeight()); + captcha.setLen(captchaConfig.getLength()); + break; + case spec: + captcha = new SpecCaptcha(captchaConfig.getWidth(), captchaConfig.getHeight()); + captcha.setLen(captchaConfig.getLength()); + break; + default: + throw new IllegalArgumentException("验证码配置信息错误!正确配置查看 CaptchaEnum "); + } + } + if(StringUtils.isNotBlank(captchaConfig.getFontName())){ + captcha.setFont(new Font(captchaConfig.getFontName(), Font.PLAIN, captchaConfig.getFontSize())); + } + return captcha; + } +} diff --git a/admin/src/main/java/com/baiye/captcha/enums/CaptchaEnum.java b/admin/src/main/java/com/baiye/captcha/enums/CaptchaEnum.java new file mode 100644 index 0000000..7a880f7 --- /dev/null +++ b/admin/src/main/java/com/baiye/captcha/enums/CaptchaEnum.java @@ -0,0 +1,22 @@ +package com.baiye.captcha.enums; + +public enum CaptchaEnum { + /** + * 算数 + */ + arithmetic, + /** + * 中文 + */ + chinese, + /** + * 中文闪图 + */ + chinese_gif, + /** + * 闪图 + */ + gif, + + spec +} diff --git a/admin/src/main/java/com/baiye/captcha/impl/ValidateCodeServiceImpl.java b/admin/src/main/java/com/baiye/captcha/impl/ValidateCodeServiceImpl.java new file mode 100644 index 0000000..7a8f78c --- /dev/null +++ b/admin/src/main/java/com/baiye/captcha/impl/ValidateCodeServiceImpl.java @@ -0,0 +1,60 @@ +package com.baiye.captcha.impl; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.IdUtil; +import com.baiye.constant.CaptchaConstant; +import com.baiye.captcha.config.KaptchaTextCreator; +import com.baiye.exception.BadRequestException; +import com.baiye.util.RedisUtils; +import com.wf.captcha.base.Captcha; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ballcat.security.captcha.IValidateCodeService; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * @description 验证码业务处理实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ValidateCodeServiceImpl implements IValidateCodeService { + + private final RedisUtils redisUtils; + + private final KaptchaTextCreator kaptchaTextCreator; + + + @Override + public Map createCapcha() { + // 获取验证码 + Captcha captcha = this.kaptchaTextCreator.getCaptcha(); + // 生成一个唯一的id + String uuid = CaptchaConstant.CAPTCHA_CODE_KEY + IdUtil.simpleUUID(); + // 保存到redis中 + this.redisUtils.set(uuid, captcha.text(), CaptchaConstant.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); + Map map = new HashMap<>(); + map.put("uuid", uuid); + map.put("img", captcha.toBase64()); + return map; + } + + @Override + public void checkCapcha(String key, String code) { + if (CharSequenceUtil.isBlank(code)) { + throw new BadRequestException("验证码不能为空"); + } + String captcha = (String) redisUtils.get(key); + if (CharSequenceUtil.isBlank(captcha)) { + throw new BadRequestException("验证码已失效"); + } + redisUtils.del(key); + if (!code.equalsIgnoreCase(captcha)) { + throw new BadRequestException("验证码错误"); + } + } +} diff --git a/admin/src/main/java/com/baiye/constant/CaptchaConstant.java b/admin/src/main/java/com/baiye/constant/CaptchaConstant.java new file mode 100644 index 0000000..55b6e13 --- /dev/null +++ b/admin/src/main/java/com/baiye/constant/CaptchaConstant.java @@ -0,0 +1,20 @@ +package com.baiye.constant; + +/** + * 通用常量信息 + * + * @author ruoyi + */ +public final class CaptchaConstant { + + /** + * 验证码 redis key + */ + public static final String CAPTCHA_CODE_KEY = "captcha_codes:"; + + /** + * 验证码有效期(分钟) + */ + public static final Integer CAPTCHA_EXPIRATION = 2; + +} diff --git a/admin/src/main/java/com/baiye/constant/PayConstants.java b/admin/src/main/java/com/baiye/constant/PayConstants.java new file mode 100644 index 0000000..ef2b6a3 --- /dev/null +++ b/admin/src/main/java/com/baiye/constant/PayConstants.java @@ -0,0 +1,26 @@ +package com.baiye.constant; + +/** + * 支付相关 常量定义 + * + * @author Enzo + * @date : 2021/6/16 + */ +public class PayConstants { + + private PayConstants() { + } + + /** + * 付款信息 反馈中文字段补充前缀 + */ + public static final String PAY_TITLE = "回跟营销系统支付订单:"; + + /** + * 返回的信息字段内容对应 key + */ + public static final String OUT_TRADE_NO = "out_trade_no"; + + + +} diff --git a/admin/src/main/java/com/baiye/constant/PrefixKeyConstant.java b/admin/src/main/java/com/baiye/constant/PrefixKeyConstant.java new file mode 100644 index 0000000..31cec0d --- /dev/null +++ b/admin/src/main/java/com/baiye/constant/PrefixKeyConstant.java @@ -0,0 +1,11 @@ +package com.baiye.constant; + +/** + * @Author YQY + * @Date 2023/8/17 + */ +public class PrefixKeyConstant { + + public static final String JOB_KEY = "ad_distribute_distributeTask_id_"; + +} diff --git a/admin/src/main/java/com/baiye/constant/ResponseConstant.java b/admin/src/main/java/com/baiye/constant/ResponseConstant.java new file mode 100644 index 0000000..432adb5 --- /dev/null +++ b/admin/src/main/java/com/baiye/constant/ResponseConstant.java @@ -0,0 +1,17 @@ +package com.baiye.constant; + +/** + * @author Enzo + * @date : 2023/11/6 + */ +public class ResponseConstant { + + public static final int SUCCESS = 200; + + + + public static final int OCENA_NO_PERMISSION = 40002; + + public static final int OCEAN_FAIL = 200; + +} diff --git a/admin/src/main/java/com/baiye/constant/UploadTemplateHeadConstant.java b/admin/src/main/java/com/baiye/constant/UploadTemplateHeadConstant.java new file mode 100644 index 0000000..eaf3c6d --- /dev/null +++ b/admin/src/main/java/com/baiye/constant/UploadTemplateHeadConstant.java @@ -0,0 +1,16 @@ +package com.baiye.constant; + +/** + * 上传文件模板头信息常量 + */ +public class UploadTemplateHeadConstant { + + public static final String HEAD_NID = "手机号"; + + public static final String HEAD_ORIGIN = "渠道来源"; + + public static final String HEAD_REMARK = "备注"; + + public static final String HEAD_DATE = "日期(****/**/** 00:00:00)"; + +} diff --git a/admin/src/main/java/com/baiye/constant/UrlConstant.java b/admin/src/main/java/com/baiye/constant/UrlConstant.java new file mode 100644 index 0000000..324d553 --- /dev/null +++ b/admin/src/main/java/com/baiye/constant/UrlConstant.java @@ -0,0 +1,18 @@ +package com.baiye.constant; + +/** + * @author Enzo + */ +public class UrlConstant { + + public static String CONNECT_OAUTH2_AUTHORIZE_URL = "https://ad.oceanengine.com/openapi/audit/oauth.html?app_id=%s&state=%s&material_auth=1&redirect_uri=%s&material_auth=1"; + public static String OAUTH2_ACCESS_TOKEN_URL = "https://ad.oceanengine.com/open_api/oauth2/access_token/"; + public static String OAUTH2_REFRESH_TOKEN_URL = "https://ad.oceanengine.com/open_api/oauth2/refresh_token/"; + public static String OAUTH2_AUTH_ADVERTISER_URL = "https://ad.oceanengine.com/open_api/oauth2/advertiser/get/?access_token=%s&app_id=%s&secret=%s"; + + public static String SYNC_CLUE_URL = "https://ad.oceanengine.com/open_api/2/tools/clue/get/"; + public static String GET_ADVERTISER = "https://ad.oceanengine.com/open_api/2/majordomo/advertiser/select/"; + + public static String CALL_BACK_CLUE_URL="https://ad.oceanengine.com/open_api/2/tools/clue/callback/"; + +} diff --git a/admin/src/main/java/com/baiye/constant/WeChatRequestConstants.java b/admin/src/main/java/com/baiye/constant/WeChatRequestConstants.java new file mode 100644 index 0000000..6675028 --- /dev/null +++ b/admin/src/main/java/com/baiye/constant/WeChatRequestConstants.java @@ -0,0 +1,57 @@ + +package com.baiye.constant; + +/** + * @author Enzo + * @date 2024-3-27 + */ +public interface WeChatRequestConstants { + + + /** + * 获取二维码 + */ + String GET_QR_CODE = "/api/qrCodeLogin/"; + + /** + * 批量下线 + */ + String ROBOT_LOGOUT = "/api/batchLogout/"; + + + + + + /** + * 全局设置 + */ + String GLOBAL_SETTING = "/api/global-settings/"; + + + + /** + * 弹窗登录 + */ + String GET_PUSH_LOGIN = "/api/pushLogin/"; + + /** + * 同步状态 + */ + String GET_ROBOT_TYPE = "/api/syncRobot/"; + + + + /** + * 创建设备 + */ + String CREATE_EQUIPMENT= "/api/addToScanRobotInfo/"; + + + + /** + * 添加好友 + */ + String WE_CHAT_ADD_FRIEND = "/api/add-now-friend-phone/"; + + +} diff --git a/admin/src/main/java/com/baiye/event/system/RootApplication.java b/admin/src/main/java/com/baiye/event/system/RootApplication.java new file mode 100644 index 0000000..46bc034 --- /dev/null +++ b/admin/src/main/java/com/baiye/event/system/RootApplication.java @@ -0,0 +1,27 @@ +package com.baiye.event.system; + +import com.baiye.schedule.handler.ScanDynamicJobHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +/** + * @author Enzo + * @date : 2024/5/28 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class RootApplication implements CommandLineRunner { + + private final ScanDynamicJobHandler scanDynamicJobHandler; + + + @Override + public void run(String... args) throws Exception { + log.info(">>>>>>>>>>>>>>>服务启动执行,扫描动态任务列表,并添加任务<<<<<<<<<<<<<"); + scanDynamicJobHandler.scanAddJob(); + } +} + diff --git a/admin/src/main/java/com/baiye/modules/distribute/controller/AliPayController.java b/admin/src/main/java/com/baiye/modules/distribute/controller/AliPayController.java new file mode 100644 index 0000000..599b7aa --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/controller/AliPayController.java @@ -0,0 +1,57 @@ +package com.baiye.modules.distribute.controller; + + +import com.baiye.modules.distribute.dto.AliPayPcDTO; +import com.baiye.modules.distribute.entity.PayOrder; +import com.baiye.modules.distribute.service.AliPayService; +import com.baiye.result.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * @author Enzo + */ +@Tag(name = "支付宝支付") +@Slf4j +@RestController +@RequestMapping(value = "/pay/aliPay") +@AllArgsConstructor +public class AliPayController { + + private final AliPayService aliPayService; + + @Operation(summary = "创建pc订单") + @PostMapping(value = "/pcPay") + public R> pcPay(@Validated @RequestBody AliPayPcDTO aliPayPcDTO) { + return R.ok(aliPayService.aliPayPc(aliPayPcDTO)); + } + + + @Operation(summary = "订单号查找") + @GetMapping(value = "/find/order") + public R frontRcvResponse(@RequestParam("orderNo") String orderNo) { + return R.ok(aliPayService.findOrderByNo(orderNo)); + } + + + /** + * pc 回调接收参数 + * + * @param request request + * @return 返回结果 + */ + @Operation(summary = "订单回调") + @RequestMapping(value = "/pay-notify", produces = "text/html;charset=utf-8") + public String frontRcvResponse(HttpServletRequest request) { + return aliPayService.pcNotifyResponse(request); + } + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/controller/LoginEquipmentController.java b/admin/src/main/java/com/baiye/modules/distribute/controller/LoginEquipmentController.java new file mode 100644 index 0000000..62576eb --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/controller/LoginEquipmentController.java @@ -0,0 +1,42 @@ +package com.baiye.modules.distribute.controller; + +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.modules.distribute.qo.AccountQo; +import com.baiye.modules.distribute.service.LoginEquipmentService; +import com.baiye.modules.distribute.vo.WeChatAccountVO; +import com.baiye.result.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Enzo + * @date : 2024/4/1 + */ + +@Tag(name = "微信设备") +@Slf4j +@RestController +@RequestMapping(value = "/equipment") +@AllArgsConstructor +public class LoginEquipmentController { + + private LoginEquipmentService loginEquipmentService; + + @GetMapping("/page") + @Operation(summary = "分页查询记录") + public R> getClueRecordPage(@Validated PageParam pageParam, AccountQo qo) { + return R.ok(loginEquipmentService.queryPage(pageParam, qo)); + } + + + + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/controller/PayOrderController.java b/admin/src/main/java/com/baiye/modules/distribute/controller/PayOrderController.java new file mode 100644 index 0000000..436466c --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/controller/PayOrderController.java @@ -0,0 +1,36 @@ +package com.baiye.modules.distribute.controller; + +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.modules.distribute.qo.PayOrderQo; +import com.baiye.modules.distribute.service.PayOrderService; +import com.baiye.modules.distribute.vo.PayOrderVO; +import com.baiye.result.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Enzo + * @date + */ +@RequiredArgsConstructor +@RestController +@Tag(name = "订单记录") +@RequestMapping("/pay/order") +public class PayOrderController { + + private final PayOrderService payOrderService; + + @GetMapping("/page") + @Operation(summary = "分页查询记录") + public R> getClueRecordPage(@Validated PageParam pageParam, PayOrderQo qo) { + return R.ok(payOrderService.queryPage(pageParam, qo)); + } + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/controller/WeChatController.java b/admin/src/main/java/com/baiye/modules/distribute/controller/WeChatController.java new file mode 100644 index 0000000..25e0492 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/controller/WeChatController.java @@ -0,0 +1,133 @@ +package com.baiye.modules.distribute.controller; + +import cn.hutool.http.HttpUtil; +import com.baiye.constant.WeChatRequestConstants; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.modules.distribute.dto.CreateCodeDTO; +import com.baiye.modules.distribute.dto.PushCodeDTO; +import com.baiye.modules.distribute.dto.WeChatAddFriendDTO; +import com.baiye.modules.distribute.dto.WeChatStatisticsDTO; +import com.baiye.modules.distribute.qo.AccountQo; +import com.baiye.modules.distribute.service.WeChatService; +import com.baiye.modules.distribute.service.WechatFriendService; +import com.baiye.modules.distribute.vo.AccountStatisticsVO; +import com.baiye.modules.distribute.vo.AddFriendVo; +import com.baiye.modules.distribute.vo.StatisticsFriendVO; +import com.baiye.modules.distribute.vo.WeChatAccountVO; +import com.baiye.result.R; +import com.baiye.security.util.SecurityUtils; +import com.google.common.collect.Maps; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * @author Enzo + * @date : 2024/3/27 + */ +@RestController +@Tag(name = "微信账号") +@RequiredArgsConstructor +@RequestMapping("/wechat") +public class WeChatController { + + + private final WeChatService weChatService; + + private final WechatFriendService wechatFriendService; + + + @GetMapping("/page") + @Operation(summary = "分页查询账号") + public R> getClueRecordPage(@Validated PageParam pageParam, AccountQo qo) { + return R.ok(weChatService.queryPage(pageParam, qo)); + } + + + @GetMapping("/global/setting") + @Operation(summary = "全局设置") + public R getClueRecordPage(String callbackUrl) { + return R.ok(weChatService.globalSetting(callbackUrl)); + } + + + @PostMapping("/createCode") + @Operation(description = "创建二维码") + public R createCode(@RequestBody CreateCodeDTO createCodeDTO) { + return R.ok(weChatService.createQrCode(createCodeDTO)); + } + + + @Operation(description = "弹窗登录接口") + @PostMapping("/push/code") + public R pushCode(@Validated @RequestBody PushCodeDTO pushCodeDTO) { + return R.ok(weChatService.pushCode(pushCodeDTO)); + } + + @Operation(description = "添加好友") + @PostMapping("/addFriend") + public R addFriend(@Validated @RequestBody WeChatAddFriendDTO weChatFriendDTO) { + return R.ok(weChatService.addFriend(weChatFriendDTO)); + } + + + @GetMapping("/create/equipment") + @Operation(summary = "创建设备") + public R createEquipment(Integer num, Integer packageType) { + return Boolean.TRUE.equals(weChatService.createEquipment(num, packageType, SecurityUtils.getCurrentUserId())) ? R.ok() : R.failed("创建设备失败"); + } + + + @GetMapping("/friend/page") + @Operation(summary = "添加好友记录") + public R> getData(@Validated PageParam pageParam, AccountQo qo) { + return R.ok(weChatService.queryFirendData(pageParam, qo)); + } + + + @GetMapping("/query/friend") + @Operation(summary = "时间统计统计粉丝记录") + public R> getDateBy(Integer queryType) { + return R.ok(wechatFriendService.statisticsFriendByType(queryType)); + } + + + @PostMapping("/query/user/statistics") + @Operation(summary = "统计用户加粉数量") + public R> getDateBy(@RequestBody WeChatStatisticsDTO statisticsDTO) { + return R.ok(wechatFriendService.statisticsFriendByUserId(statisticsDTO)); + } + + + @GetMapping("/query/account/statistics") + @Operation(summary = "统计账号信息数量") + public R accountStatistics() { + return R.ok(weChatService.statisticsAccountByUserId(SecurityUtils.getCurrentUserId())); + } + + + @GetMapping("/update/remark") + @Operation(summary = "修改备注") + public R accountStatistics(String wxId, String remark) { + return R.ok(weChatService.updateRemarkByWechatId(wxId, remark)); + } + + + @GetMapping("/robot/logout") + @Operation(summary = "退出登录") + public R robotLogout(Integer robotId) { + return R.ok(weChatService.logoutByRobotId(robotId)); + } + + + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/controller/WechatCallbackController.java b/admin/src/main/java/com/baiye/modules/distribute/controller/WechatCallbackController.java new file mode 100644 index 0000000..487c7be --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/controller/WechatCallbackController.java @@ -0,0 +1,36 @@ +package com.baiye.modules.distribute.controller; + +import cn.hutool.json.JSONUtil; +import com.baiye.modules.distribute.service.WechatCallbackService; +import com.baiye.result.R; +import com.baiye.result.WeChatResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Enzo + * @date 2022-8-11 + */ +@Slf4j +@RestController +@RequestMapping("/api/wechat") +@RequiredArgsConstructor +public class WechatCallbackController { + + private final WechatCallbackService wechatCallbackService; + + /** + * 请求三方微信操作回调 + */ + @PostMapping("/callback") + public R wechatCallback(@RequestBody WeChatResponse weChatResponse) { + log.info("================== the response {} ==================", JSONUtil.toJsonStr(weChatResponse)); + wechatCallbackService.analyticalData(weChatResponse); + return R.ok(); + } + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/converter/PayOrderConverter.java b/admin/src/main/java/com/baiye/modules/distribute/converter/PayOrderConverter.java new file mode 100644 index 0000000..5b3bd94 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/converter/PayOrderConverter.java @@ -0,0 +1,23 @@ +package com.baiye.modules.distribute.converter; + +import com.baiye.modules.distribute.entity.PayOrder; +import com.baiye.modules.distribute.vo.PayOrderVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Enzo + * @date : 2024/3/28 + */ +@Mapper +public interface PayOrderConverter { + + PayOrderConverter INSTANCE = Mappers.getMapper(PayOrderConverter.class); + + /** + * 实体转vo + * @param payOrder + * @return + */ + PayOrderVO entityToVo(PayOrder payOrder); +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/converter/WeChatAccountConverter.java b/admin/src/main/java/com/baiye/modules/distribute/converter/WeChatAccountConverter.java new file mode 100644 index 0000000..305bdf0 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/converter/WeChatAccountConverter.java @@ -0,0 +1,23 @@ +package com.baiye.modules.distribute.converter; + +import com.baiye.modules.distribute.entity.WeChatAccount; +import com.baiye.modules.distribute.vo.WeChatAccountVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Enzo + * @date : 2024/3/29 + */ +@Mapper +public interface WeChatAccountConverter { + + WeChatAccountConverter INSTANCE = Mappers.getMapper(WeChatAccountConverter.class); + + /** + * 实体转vo + * @param weChatAccount + * @return + */ + WeChatAccountVO entityToVo(WeChatAccount weChatAccount); +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/dto/AliPayPcDTO.java b/admin/src/main/java/com/baiye/modules/distribute/dto/AliPayPcDTO.java new file mode 100644 index 0000000..3aedb42 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/dto/AliPayPcDTO.java @@ -0,0 +1,34 @@ +package com.baiye.modules.distribute.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @author Enzo + * @date : 2022/3/16 + */ +@Data +@NoArgsConstructor +public class AliPayPcDTO implements Serializable { + + private static final long serialVersionUID = 6500373305956248683L; + + @NotNull(message = "套餐类型不能为空") + private Integer packageType; + + @NotNull(message = "数量不能为空") + @Min(value = 1,message = "最小为1") + private Integer num; + + @NotNull(message = "续费类型不能为空") + private Boolean isRenew; + + private Integer robotId; + + private String remark; + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/dto/CreateCodeDTO.java b/admin/src/main/java/com/baiye/modules/distribute/dto/CreateCodeDTO.java new file mode 100644 index 0000000..0ae3b51 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/dto/CreateCodeDTO.java @@ -0,0 +1,21 @@ +package com.baiye.modules.distribute.dto; + +import lombok.Data; + +/** + * @author Enzo + * @date : 2024/3/27 + */ +@Data +public class CreateCodeDTO { + + private String robotId; + + private String wxId; + + private String cityInfo; + + private String changedUid; + + private String robotVersion; +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/dto/EquipmentDTO.java b/admin/src/main/java/com/baiye/modules/distribute/dto/EquipmentDTO.java new file mode 100644 index 0000000..1b023bf --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/dto/EquipmentDTO.java @@ -0,0 +1,33 @@ +package com.baiye.modules.distribute.dto; + +import lombok.*; + +import java.util.List; + +/** + * @author Enzo + * @date : 2024/4/1 + */ +@Data +public class EquipmentDTO { + + private Integer status; + + private List data; + + + /** + * + */ + @AllArgsConstructor + @NoArgsConstructor + @ToString + @Getter + @Setter + public static class RoBotDTO { + + private String robotAccount; + + private Integer robotId; + } +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/dto/PushCodeDTO.java b/admin/src/main/java/com/baiye/modules/distribute/dto/PushCodeDTO.java new file mode 100644 index 0000000..3edfb2f --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/dto/PushCodeDTO.java @@ -0,0 +1,18 @@ +package com.baiye.modules.distribute.dto; + +import lombok.Data; + +/** + * @author Enzo + * @date : 2024/3/27 + */ +@Data +public class PushCodeDTO { + + private Integer robotId; + + private String wxId; + + private String cityInfo; + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatAccountDTO.java b/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatAccountDTO.java new file mode 100644 index 0000000..e216871 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatAccountDTO.java @@ -0,0 +1,29 @@ +package com.baiye.modules.distribute.dto; + +import lombok.Data; + +/** + * @author Enzo + * @date : 2024/4/10 + */ +@Data +public class WeChatAccountDTO { + private String account; + + private String wxid; + + private String nickname; + + private String smallHeadImgUrl; + + private String pit; + + private String wxVersion; + + private String province; + + + private Integer status; + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatAddFriendDTO.java b/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatAddFriendDTO.java new file mode 100644 index 0000000..ed65c3b --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatAddFriendDTO.java @@ -0,0 +1,40 @@ +package com.baiye.modules.distribute.dto; + +import lombok.*; + +/** + * @author Enzo + * @date : 2022/6/27 + */ +@Data +@NoArgsConstructor +public class WeChatAddFriendDTO { + + @NonNull + private String robotWxId; + + + private AddFriendDTO addData; + + /** + * 实体 + */ + @AllArgsConstructor + @NoArgsConstructor + @ToString + @Getter + @Setter + public static class AddFriendDTO { + + private String scene; + + private String addKey; + + private String content; + + } + + + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatFriendDTO.java b/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatFriendDTO.java new file mode 100644 index 0000000..6eca382 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatFriendDTO.java @@ -0,0 +1,42 @@ +package com.baiye.modules.distribute.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @author Enzo + * @date : 2022/6/27 + */ +@Data +public class WeChatFriendDTO { + + @Schema(title = "机器人微信ID") + private String robotWxId; + + @Schema(title = "添加好友方式") + private Integer addFriendType; + + @Schema(title = "省") + private String province; + + @Schema(title = "城市") + private String city; + + @Schema(title = "昵称") + private String nickName; + + @Schema(title = "性别") + private Integer sex; + + @Schema(title = "通过时间") + private Long addAcceptTime; + + + @Schema(title = "微信ID") + private String friendId; + + @Schema(title = "头像") + private String bigHeadImgUrl; + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatStatisticsDTO.java b/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatStatisticsDTO.java new file mode 100644 index 0000000..634d886 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatStatisticsDTO.java @@ -0,0 +1,25 @@ +package com.baiye.modules.distribute.dto; + +import lombok.*; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; +import java.util.List; + +/** + * @author Enzo + * @date : 2022/6/27 + */ +@Data +public class WeChatStatisticsDTO { + + private List weChatIdList; + + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date startTime; + + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatUserLoginDTO.java b/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatUserLoginDTO.java new file mode 100644 index 0000000..7c523f8 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/dto/WeChatUserLoginDTO.java @@ -0,0 +1,24 @@ +package com.baiye.modules.distribute.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @author Enzo + * @date : 2022/6/30 + */ +@Data +public class WeChatUserLoginDTO implements Serializable { + + + private String pit; + + private String wxId; + + private String nickname; + + private String bigHeadImgUrl; + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/entity/ClueEntity.java b/admin/src/main/java/com/baiye/modules/distribute/entity/ClueEntity.java new file mode 100644 index 0000000..9db956d --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/entity/ClueEntity.java @@ -0,0 +1,93 @@ +package com.baiye.modules.distribute.entity; + +import com.baiye.entity.LogicDeletedBaseEntity; +import com.baiye.extend.mybatis.plus.alias.TableAlias; +import com.baiye.extend.mybatis.plus.converter.JsonStringArrayTypeHandler; +import com.baiye.validation.group.UpdateGroup; +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Getter +@Setter +@ToString +@TableName(value = "tb_clue", autoResultMap = true) +@Schema(title = "线索") +@TableAlias("ce") +public class ClueEntity extends LogicDeletedBaseEntity { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.INPUT) + @Schema(title = "线索ID") + @NotNull(message = "分发ID不能为空", groups = { UpdateGroup.class }) + private Long clueId; + + @Schema(title = "线索来源") + private String originName; + + @Schema(title = "手机号") + private String nid; + + @Schema(title = "线索时间") + private String clueTime; + + @Schema(title = "线索备注") + private String remark; + + @Schema(title = "记录ID") + private Long clueRecordId; + + @Schema(title = "其它线索(jsonStr格式)") + private String otherClue; + + @Schema(title = "分配人用户ID") + private Long assignedBy; + + @Schema(title = "分配人用户名") + private String assignedName; + + @Schema(title = "资源标签关联ID") + private String clueLabel; + + @Schema(title = "资源所打标签信息(jsonStr格式)") + private String clueLabelName; + + @Schema(title = "资源阶段关联ID") + private Long clueStageId; + + @Schema(title = "资源所打的阶段名称") + private String clueStageName; + + @Schema(title = "是否新线索") + private Boolean isNewClue; + + @Schema(title = "是否有效") + private Boolean isEffective; + + @Schema(title = "太空猫传递的线索ID") + private Long variable; + + @Schema(title = "公司管理员用户ID") + private Long companyId; + + @Schema(title = "性别(0-男,1-女)") + private Integer sex; + + @Schema(title = "客户资料") + @TableField(updateStrategy = FieldStrategy.IGNORED) + private String customInformation; + + @TableField(value = "channel_identifying", typeHandler = JsonStringArrayTypeHandler.class) + @Schema(title = "渠道标识") + private List channelIdentifying; + + @Schema(title = "渠道类型渠道类型(1 手动创建 2文件上传 3飞鱼回传 4话单回传 5 api回传)") + private Integer channelType; + + @Schema(title = "客户ID") + private Long customId; +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/entity/LoginEquipment.java b/admin/src/main/java/com/baiye/modules/distribute/entity/LoginEquipment.java new file mode 100644 index 0000000..1b0aca8 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/entity/LoginEquipment.java @@ -0,0 +1,50 @@ +package com.baiye.modules.distribute.entity; + +import com.baiye.entity.BaseEntity; +import com.baiye.extend.mybatis.plus.alias.TableAlias; +import com.baiye.validation.group.UpdateGroup; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotNull; +import java.util.Date; + +/** + * @author Enzo + * @date : 2024/4/1 + */ +@Getter +@Setter +@TableAlias("le") +@TableName(value = "tb_login_equipment") +public class LoginEquipment extends BaseEntity { + + @TableId(type = IdType.AUTO) + @Schema(title = "id") + @NotNull(message = "id", groups = {UpdateGroup.class}) + private Long id; + + + @Schema(name = "user_id") + private Long userId; + + @Schema(name = "pit") + private String pit; + + @Schema(name = "package_type") + private Integer packageType; + + @Schema(name = "robot_id") + private Integer robotId; + + @Schema(name = "device_number") + private String deviceNumber; + + @Schema(name = "expiration_time") + private Date expirationTime; + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/entity/PayOrder.java b/admin/src/main/java/com/baiye/modules/distribute/entity/PayOrder.java new file mode 100644 index 0000000..0a4d82b --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/entity/PayOrder.java @@ -0,0 +1,72 @@ +package com.baiye.modules.distribute.entity; + +import cn.hutool.core.date.DatePattern; +import com.baiye.entity.BaseEntity; +import com.baiye.extend.mybatis.plus.alias.TableAlias; +import com.baiye.validation.group.UpdateGroup; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * @author Enzo + * @date 2021-10-28 + */ +@Getter +@Setter +@TableAlias("po") +@TableName(value = "tb_pay_order") +public class PayOrder extends BaseEntity implements Serializable { + private static final long serialVersionUID = 2056458673691527927L; + + + @TableId(type = IdType.AUTO) + @Schema(title = "id") + @NotNull(message = "id", groups = {UpdateGroup.class}) + private Long id; + + @Schema(name = "pay_type") + private Integer payType; + + @Schema(name = "order_no") + private String orderNo; + + + @Schema(name = "status") + private Integer status; + + @Schema(name = "num") + private Integer num; + + @Schema(name = "purchaser") + private String purchaser; + + @Schema(name = "pay_time") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DatePattern.NORM_DATETIME_PATTERN, timezone = "GMT+8") + private Date payTime; + + @Schema(name = "amount") + private BigDecimal amount; + + @Schema(name = "user_id") + private Long userId; + + + @Schema(name = "robot_id") + private Integer robotId; + + @Schema(name = "is_renew") + private Boolean isRenew; + + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/entity/WeChatAccount.java b/admin/src/main/java/com/baiye/modules/distribute/entity/WeChatAccount.java new file mode 100644 index 0000000..5224517 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/entity/WeChatAccount.java @@ -0,0 +1,74 @@ +package com.baiye.modules.distribute.entity; + +import com.baiye.entity.LogicDeletedBaseEntity; +import com.baiye.extend.mybatis.plus.alias.TableAlias; +import com.baiye.validation.group.UpdateGroup; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.util.Date; + +/** + * @author Enzo + * @date : 2024/3/26 + */ + +@Getter +@Setter +@ToString +@TableAlias("wa") +@TableName(value = "tb_wechat_account") +public class WeChatAccount extends LogicDeletedBaseEntity { + + @TableId(type = IdType.INPUT) + @Schema(title = "id") + @NotNull(message = "id", groups = {UpdateGroup.class}) + private Long id; + + @Schema(title = "用户id") + private Long userId; + + @Schema(title = "设备信息") + private String deviceInformation; + + @Schema(title = "用户名称") + private String username; + + @Schema(title = "用户id") + private String nickname; + + @Schema(title = "城市信息") + private String cityInfo; + + @Schema(title = "登录类型") + private Integer loginType; + + @Schema(name = "登录时间") + private Date loginDate; + + @Schema(name = "头像") + private String headerUrl; + + @Schema(name = "微信ID") + private String wxId; + + @Schema(name = "性别") + private Integer sex; + + + @Schema(title = "在线状态") + private Integer status; + + @Schema(title = "备注") + private String remark; + + @Schema(title = "微信号") + private String weChatNo; + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/entity/WeChatEquipment.java b/admin/src/main/java/com/baiye/modules/distribute/entity/WeChatEquipment.java new file mode 100644 index 0000000..7146408 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/entity/WeChatEquipment.java @@ -0,0 +1,46 @@ +package com.baiye.modules.distribute.entity; + +import com.baiye.extend.mybatis.plus.alias.TableAlias; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * + * @author Enzo + * + * + */ +@Data +@TableAlias("we") +@TableName("tb_wechat_equipment") +@Schema(title = "微信账号设备") +public class WeChatEquipment implements Serializable { + + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + @Schema(title = "用户id") + private Long accountId; + + /** + * 角色Code + */ + @Schema(title = "设备id") + private Long equipmentId; + + + @Schema(title = "用户ID") + private Long userId; + + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/entity/WeChatFriedRecord.java b/admin/src/main/java/com/baiye/modules/distribute/entity/WeChatFriedRecord.java new file mode 100644 index 0000000..b0ed566 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/entity/WeChatFriedRecord.java @@ -0,0 +1,56 @@ +package com.baiye.modules.distribute.entity; + +import com.baiye.extend.mybatis.plus.alias.TableAlias; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author Enzo + * @date : 2024/4/2 + */ +@Data +@TableAlias("we") +@TableName("tb_wechat_friend") +@Schema(title = "微信好友记录") +public class WeChatFriedRecord implements Serializable { + + @TableId + private Long id; + + @Schema(title = "机器人微信ID") + private String robotWxId; + + @Schema(title = "添加好友方式") + private Integer addFriendType; + + @Schema(title = "省") + private String province; + + @Schema(title = "城市") + private String city; + + @Schema(title = "昵称") + private String nickName; + + @Schema(title = "性别") + private Integer sex; + + @Schema(title = "通过时间") + private Long addAcceptTime; + + + @Schema(title = "微信ID") + private String friendId; + + @Schema(title = "头像") + private String bigHeadImgUrl; + + @Schema(title = "userId") + private Long userId; + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/mapper/LoginEquipmentMapper.java b/admin/src/main/java/com/baiye/modules/distribute/mapper/LoginEquipmentMapper.java new file mode 100644 index 0000000..2878408 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/mapper/LoginEquipmentMapper.java @@ -0,0 +1,108 @@ +package com.baiye.modules.distribute.mapper; + +import cn.hutool.core.date.DateTime; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.conditions.query.LambdaQueryWrapperX; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; +import com.baiye.modules.distribute.entity.LoginEquipment; +import com.baiye.modules.distribute.entity.WeChatAccount; +import com.baiye.modules.distribute.qo.AccountQo; +import com.baiye.modules.distribute.vo.WeChatAccountVO; +import com.baiye.modules.distribute.vo.WeChatAddFriendVo; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @author Enzo + * @date 2024-4-1 + */ +@Mapper +public interface LoginEquipmentMapper extends ExtendMapper { + + + default PageResult queryPage(PageParam pageParam, AccountQo qo) { + IPage page = this.prodPage(pageParam); + + LambdaQueryWrapperX wrapperX = WrappersX.lambdaQueryX(WeChatAccount.class); + if (StringUtils.isNotBlank(qo.getStartTime()) && StringUtils.isNotBlank(qo.getEndTime())) { + wrapperX.between(WeChatAccount::getCreateTime, qo.getStartTime(), qo.getEndTime()); + } + wrapperX.likeIfPresent(WeChatAccount::getNickname, qo.getNickname()).likeIfPresent + (WeChatAccount::getWxId, qo.getWxId()).eqIfPresent(WeChatAccount::getUserId, qo.getUserId()).likeIfPresent(WeChatAccount::getWxId, qo.getWxId()); + this.selectByPage(page, qo.getStartTime(), qo.getEndTime(), qo.getNickname(), qo.getWxId(), qo.getUserId()); + return new PageResult<>(page.getRecords(), page.getTotal()); + } + + /** + * 查询分页记录 + * + * @param page + * @param startTime + * @param endTime + * @param username + * @param wxId + * @param userId + * @return + */ + IPage selectByPage(IPage page, + @Param("startTime") String startTime, @Param("endTime") String endTime, @Param("username") String username, @Param("wxId") String wxId, @Param("userId") Long userId); + + + /** + * 天数查询数据 + * + * @param dayNum + * @param currentUserId + * @param wxId + * @return + */ + List queryByDate(@Param("dayNum") Integer dayNum, @Param("userId") Long currentUserId, @Param("wxId") String wxId); + + /** + * 用户ID查询 + * + * @param currentUserId + * @return + */ + List queryByUserId(Long currentUserId); + + /** + * 统计数据 + * + * @param currentUserId + * @param number + * @return + */ + default Long countFreeEquipment(Long currentUserId, Integer number) { + return this.selectCount + (WrappersX.lambdaQueryX(LoginEquipment.class).eq + (LoginEquipment::getUserId, currentUserId).eq(LoginEquipment::getPackageType, number)); + + } + + + /** + * 机器人查询设备 + * + * @param wxId + * @return + */ + List findEquipmentByAccountId(@Param("wxId") String wxId); + + /** + * 时间查询设备 + * + * @param date + * @return + */ + default List selectListByTime(DateTime date) { + return this.selectList(WrappersX.lambdaQueryX(LoginEquipment.class).gt(LoginEquipment::getExpirationTime, date)); + } +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/mapper/PayOrderMapper.java b/admin/src/main/java/com/baiye/modules/distribute/mapper/PayOrderMapper.java new file mode 100644 index 0000000..0327015 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/mapper/PayOrderMapper.java @@ -0,0 +1,55 @@ +package com.baiye.modules.distribute.mapper; + +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.conditions.query.LambdaQueryWrapperX; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; +import com.baiye.modules.distribute.converter.PayOrderConverter; +import com.baiye.modules.distribute.entity.PayOrder; +import com.baiye.modules.distribute.qo.PayOrderQo; +import com.baiye.modules.distribute.vo.PayOrderVO; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * @author Enzo + * @date 2024-4-1 + */ +@Mapper +public interface PayOrderMapper extends ExtendMapper { + + + /** + * 订单号查询订单 + * + * @param orderNo + * @return + */ + default List findByOrderNumber(String orderNo) { + LambdaQueryWrapperX wrapperX = new LambdaQueryWrapperX<>(); + return this.selectList(wrapperX.eq(PayOrder::getOrderNo, orderNo).orderByDesc(PayOrder::getId)); + } + + /** + * 条件查询分页 + * + * @param pageParam + * @param qo + * @return + */ + default PageResult queryPage(PageParam pageParam, PayOrderQo qo) { + IPage page = this.prodPage(pageParam); + LambdaQueryWrapperX wrapperX = WrappersX.lambdaQueryX(PayOrder.class); + if (StringUtils.isNotBlank(qo.getStartTime()) && StringUtils.isNotBlank(qo.getEndTime())) { + wrapperX.between(PayOrder::getCreateTime, qo.getStartTime(), qo.getEndTime()); + } + wrapperX.eqIfPresent(PayOrder::getOrderNo, qo.getOrderNo()).eqIfPresent(PayOrder::getUserId, qo.getUserId()).orderByDesc(PayOrder::getId); + this.selectPage(page, wrapperX); + IPage voPage = page.convert(PayOrderConverter.INSTANCE::entityToVo); + return new PageResult<>(voPage.getRecords(), voPage.getTotal()); + } +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/mapper/WeChatAccountMapper.java b/admin/src/main/java/com/baiye/modules/distribute/mapper/WeChatAccountMapper.java new file mode 100644 index 0000000..0dbc2c2 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/mapper/WeChatAccountMapper.java @@ -0,0 +1,91 @@ +package com.baiye.modules.distribute.mapper; + +import cn.hutool.core.collection.CollUtil; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.conditions.query.LambdaQueryWrapperX; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; +import com.baiye.modules.distribute.converter.WeChatAccountConverter; +import com.baiye.modules.distribute.entity.WeChatAccount; +import com.baiye.modules.distribute.qo.AccountQo; +import com.baiye.modules.distribute.vo.WeChatAccountVO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * @author Enzo + * @date 2024-3-27 + */ +@Mapper +public interface WeChatAccountMapper extends ExtendMapper { + /** + * 微信id查找微信用户 + * + * @param wxId + * @return + */ + default WeChatAccount findByWxId(String wxId) { + List selectList = this.selectList(WrappersX.lambdaQueryX(WeChatAccount.class).eqIfPresent + (WeChatAccount::getWxId, wxId).eqIfPresent(WeChatAccount::getDeleted, DefaultNumberConstants.ZERO_NUMBER).orderByDesc(WeChatAccount::getId)); + if (CollUtil.isNotEmpty(selectList)) { + return selectList.get(DefaultNumberConstants.ZERO_NUMBER); + } + return new WeChatAccount(); + } + + + /** + * 分页查询数据 + * + * @param pageParam + * @param qo + * @return + */ + default PageResult queryPage(PageParam pageParam, AccountQo qo) { + IPage page = this.prodPage(pageParam); + LambdaQueryWrapperX wrapperX = WrappersX.lambdaQueryX(WeChatAccount.class); + if (StringUtils.isNotBlank(qo.getStartTime()) && StringUtils.isNotBlank(qo.getEndTime())) { + wrapperX.between(WeChatAccount::getCreateTime, qo.getStartTime(), qo.getEndTime()); + } + wrapperX.likeIfPresent(WeChatAccount::getNickname, qo.getNickname()).likeIfPresent(WeChatAccount::getWxId, qo.getWxId()). + eqIfPresent(WeChatAccount::getUserId, qo.getUserId()).orderByDesc(WeChatAccount::getId); + this.selectPage(page, wrapperX); + IPage voPage = page.convert(WeChatAccountConverter.INSTANCE::entityToVo); + return new PageResult<>(voPage.getRecords(), voPage.getTotal()); + } + + /** + * ID查询信息 + * + * @param currentUserId + * @return + */ + default List queryByUserId(Long currentUserId) { + LambdaQueryWrapperX wrapperX = WrappersX.lambdaQueryX(WeChatAccount.class); + return this.selectList(wrapperX.eq(WeChatAccount::getUserId, currentUserId)); + } + + /** + * 修改备注 + * + * @param wxId + * @param remark + * @return + */ + default Boolean updateRemark(String wxId, String remark) { + + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate() + .set(WeChatAccount::getRemark, remark) + .eq(WeChatAccount::getWxId, wxId) + .eq(WeChatAccount::getDeleted, DefaultNumberConstants.ZERO_NUMBER); + return SqlHelper.retBool(this.update(null, wrapper)); + } +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/mapper/WeChatEquipmentMapper.java b/admin/src/main/java/com/baiye/modules/distribute/mapper/WeChatEquipmentMapper.java new file mode 100644 index 0000000..a2116d7 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/mapper/WeChatEquipmentMapper.java @@ -0,0 +1,31 @@ +package com.baiye.modules.distribute.mapper; + +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; +import com.baiye.modules.distribute.entity.WeChatEquipment; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author Enzo + * @date 2024-4-1 + */ +@Mapper +public interface WeChatEquipmentMapper extends ExtendMapper { + + + /** + * 统计数据 + * + * @param accountId + * @param equipmentId + * @return + */ + default Long countEquipment(Long accountId, Long equipmentId) { + return this.selectCount + (WrappersX.lambdaQueryX(WeChatEquipment.class).eq + (WeChatEquipment::getAccountId, accountId).eq(WeChatEquipment::getEquipmentId, equipmentId)); + + } + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/mapper/WeChatFriendMapper.java b/admin/src/main/java/com/baiye/modules/distribute/mapper/WeChatFriendMapper.java new file mode 100644 index 0000000..5db5262 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/mapper/WeChatFriendMapper.java @@ -0,0 +1,64 @@ +package com.baiye.modules.distribute.mapper; + +import cn.hutool.core.date.DateTime; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; +import com.baiye.modules.distribute.entity.WeChatFriedRecord; +import com.baiye.modules.distribute.vo.StatisticsFriendVO; +import com.baiye.modules.distribute.vo.WeChatAddFriendVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * @author Enzo + * @date : 2024/4/2 + */ +@Mapper +public interface WeChatFriendMapper extends ExtendMapper { + + /** + * 时间类型查询 + * + * @param beginOfDay + * @param now + * @param currentUserId + * @return + */ + List queryByDate(@Param("startTime") DateTime beginOfDay, @Param("endTime") DateTime now, @Param("userId") Long currentUserId); + + /** + * 用户数据查询 + * + * @param weChatIdList + * @param dayNum + * @param currentUserId + * @param startTime + * @param endTime + * @return + */ + List queryByUserList(@Param("weChatIdList") List weChatIdList, + @Param("dayNum") Long dayNum, @Param("userId") Long currentUserId, + @Param("startTime") Date startTime, @Param("endTime") Date endTime); + + /** + * 用户id查找 + * + * @param currentUserId + * @return + */ + default Long queryByUserId(Long currentUserId) { + return this.selectCount(WrappersX.lambdaQueryX(WeChatFriedRecord.class).eq + (WeChatFriedRecord::getUserId, currentUserId)); + + } + + /** + * 账号id查询设备 + * @param id + * @return + */ + List findEquipmentByAccountId(Long id); +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/qo/AccountQo.java b/admin/src/main/java/com/baiye/modules/distribute/qo/AccountQo.java new file mode 100644 index 0000000..09fbb8c --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/qo/AccountQo.java @@ -0,0 +1,30 @@ +package com.baiye.modules.distribute.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @author Enzo + * @date : 2024/3/29 + */ +@Data +public class AccountQo { + + + @Parameter(description = "开始时间") + private String startTime; + + @Parameter(description = "结束时间") + private String endTime; + + @Parameter(description = "用户昵称") + private String nickname; + + @Schema(title = "用户ID") + private Long userId; + + @Schema(title = "微信id") + private String wxId; + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/qo/PayOrderQo.java b/admin/src/main/java/com/baiye/modules/distribute/qo/PayOrderQo.java new file mode 100644 index 0000000..d3cb6d7 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/qo/PayOrderQo.java @@ -0,0 +1,21 @@ +package com.baiye.modules.distribute.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class PayOrderQo { + + @Schema(title = "订单号") + private String orderNo; + + @Parameter(description = "开始时间") + private String startTime; + + @Parameter(description = "结束时间") + private String endTime; + + @Schema(title = "用户ID") + private Long userId; +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/AliPayService.java b/admin/src/main/java/com/baiye/modules/distribute/service/AliPayService.java new file mode 100644 index 0000000..80e4534 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/AliPayService.java @@ -0,0 +1,42 @@ +package com.baiye.modules.distribute.service; + + +import com.baiye.extend.mybatis.plus.service.ExtendService; +import com.baiye.modules.distribute.dto.AliPayPcDTO; +import com.baiye.modules.distribute.entity.PayOrder; +import com.baiye.modules.distribute.entity.WeChatAccount; +import com.baiye.result.R; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +/** + * @author Enzo + * @date : 2021/6/16 + */ +public interface AliPayService extends ExtendService { + + + /** + * pc端创建支付宝订单 + * @param aliPayPcDTO + * @return + */ + Map aliPayPc(AliPayPcDTO aliPayPcDTO); + + /** + * 支付宝订单回调 + * + * @param request + * @return + */ + String pcNotifyResponse(HttpServletRequest request); + + /** + * 订单编号查询 + * @param orderNo + * @return + */ + PayOrder findOrderByNo(String orderNo); +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/LoginEquipmentService.java b/admin/src/main/java/com/baiye/modules/distribute/service/LoginEquipmentService.java new file mode 100644 index 0000000..348f41d --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/LoginEquipmentService.java @@ -0,0 +1,74 @@ +package com.baiye.modules.distribute.service; + +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.ExtendService; +import com.baiye.modules.distribute.entity.LoginEquipment; +import com.baiye.modules.distribute.qo.AccountQo; +import com.baiye.modules.distribute.vo.WeChatAccountVO; +import com.baiye.modules.distribute.vo.WeChatAddFriendVo; + +import java.util.List; + +/** + * @author Enzo + * @date : 2024/4/1 + */ +public interface LoginEquipmentService extends ExtendService { + + /** + * 分页查询数据 + * @param pageParam + * @param qo + * @return + */ + + PageResult queryPage(PageParam pageParam, AccountQo qo); + + /** + * 设备号查询用户ID + * @param pit + * @return + */ + LoginEquipment queryByRobot(String pit); + + + /** + * 时间查询数据 + * + * @param dayNum + * @param currentUserId + * @param wxId + * @return + */ + List queryByDate(Integer dayNum, Long currentUserId, String wxId); + + /** + * 机器人查询 + * @param robotId + * @return + */ + LoginEquipment queryByRobotId(Integer robotId); + + /** + * 用户查询账号 + * @param currentUserId + * @return + */ + List queryByUserId(Long currentUserId); + + /** + * 判断免费设备有多少 + * @param currentUserId + * @param number + * @return + */ + Long countFreeEquipment(Long currentUserId, Integer number); + + + /** + * 查询已经过期的设备 + * @return + */ + List findExpiredAccount(); +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/PayOrderService.java b/admin/src/main/java/com/baiye/modules/distribute/service/PayOrderService.java new file mode 100644 index 0000000..ac82bc5 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/PayOrderService.java @@ -0,0 +1,23 @@ +package com.baiye.modules.distribute.service; + +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.ExtendService; +import com.baiye.modules.distribute.entity.PayOrder; +import com.baiye.modules.distribute.qo.PayOrderQo; +import com.baiye.modules.distribute.vo.PayOrderVO; + +/** + * @author Enzo + * @date : 2024/3/28 + */ +public interface PayOrderService extends ExtendService { + + /** + * 分页查询数据 + * @param pageParam + * @param qo + * @return + */ + PageResult queryPage(PageParam pageParam, PayOrderQo qo); +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/WeChatService.java b/admin/src/main/java/com/baiye/modules/distribute/service/WeChatService.java new file mode 100644 index 0000000..74c861f --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/WeChatService.java @@ -0,0 +1,131 @@ +package com.baiye.modules.distribute.service; + +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.ExtendService; +import com.baiye.modules.distribute.dto.CreateCodeDTO; +import com.baiye.modules.distribute.dto.PushCodeDTO; +import com.baiye.modules.distribute.dto.WeChatAddFriendDTO; +import com.baiye.modules.distribute.dto.WeChatUserLoginDTO; +import com.baiye.modules.distribute.entity.WeChatAccount; +import com.baiye.modules.distribute.qo.AccountQo; +import com.baiye.modules.distribute.vo.AccountStatisticsVO; +import com.baiye.modules.distribute.vo.AddFriendVo; +import com.baiye.modules.distribute.vo.WeChatAccountVO; + +/** + * @author Enzo + * @date 2023/9/5 + */ +public interface WeChatService extends ExtendService { + + + /** + * 创建二维码 + * + * @param createCodeDTO + * @return + */ + String createQrCode(CreateCodeDTO createCodeDTO); + + /** + * 添加好友 + * + * @param weChatFriendDTO + * @return + */ + Boolean addFriend(WeChatAddFriendDTO weChatFriendDTO); + + /** + * 分页查询账号信息 + * @param pageParam + * @param qo + * @return + */ + PageResult queryPage(PageParam pageParam, AccountQo qo); + + /** + * 创建设备 + * + * @param num + * @param packageType + * @param userId + * @return + */ + Boolean createEquipment(Integer num, Integer packageType, Long userId); + + /** + * 弹窗登录 + * @param pushCodeDTO + * @return + */ + String pushCode(PushCodeDTO pushCodeDTO); + + /** + * 全局回调设置 + * @param callbackUrl + * @return + */ + String globalSetting(String callbackUrl); + + /** + * 登录微信账号 + * + * @param userLoginDTO + * @param status + * @param userId + * @param id + * @return + */ + Boolean weChatAccountLoginByRespon(WeChatUserLoginDTO userLoginDTO, Integer status, Long userId, Long id); + + + /** + * 分页数据 + * @param pageParam + * @param qo + * @return + */ + PageResult queryFirendData(PageParam pageParam, AccountQo qo); + + /** + * + * 设备续费 + * @param robotId + * @param payType + * @return + */ + Boolean equipmentRenewal(Integer robotId, Integer payType); + + + /** + * 微信ID查询 + * @param robotWxId + * @return + */ + WeChatAccount findByWxId(String robotWxId); + + /** + * 统计账号信息 + * @param currentUserId + * @return + */ + AccountStatisticsVO statisticsAccountByUserId(Long currentUserId); + + + /** + * 修改备注 + * @param wxId + * @param remark + * @return + */ + + Boolean updateRemarkByWechatId(String wxId, String remark); + + /** + * 退出登录 + * @param robotId + * @return + */ + Boolean logoutByRobotId(Integer robotId); +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/WechatCallbackService.java b/admin/src/main/java/com/baiye/modules/distribute/service/WechatCallbackService.java new file mode 100644 index 0000000..a987486 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/WechatCallbackService.java @@ -0,0 +1,17 @@ +package com.baiye.modules.distribute.service; + +import com.baiye.result.WeChatResponse; + +/** + * @author Enzo + * @date : 2024/4/1 + */ +public interface WechatCallbackService { + + + /** + * 微信回调数据 + * @param body + */ + void analyticalData(WeChatResponse body); +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/WechatFriendService.java b/admin/src/main/java/com/baiye/modules/distribute/service/WechatFriendService.java new file mode 100644 index 0000000..f77ed1e --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/WechatFriendService.java @@ -0,0 +1,49 @@ +package com.baiye.modules.distribute.service; + +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.ExtendService; +import com.baiye.modules.distribute.dto.WeChatFriendDTO; +import com.baiye.modules.distribute.dto.WeChatStatisticsDTO; +import com.baiye.modules.distribute.entity.WeChatFriedRecord; +import com.baiye.modules.distribute.vo.AddFriendVo; +import com.baiye.modules.distribute.vo.StatisticsFriendVO; + +import java.util.List; + +/** + * @author Enzo + * @date : 2024/4/2 + */ +public interface WechatFriendService extends ExtendService { + + /** + * 添加好友 + * + * @param weChatFriendDTO + */ + void addFriendByResponse(WeChatFriendDTO weChatFriendDTO); + + /** + * 根据类型查询数据 + * @param queryType + * @return + */ + List statisticsFriendByType(Integer queryType); + + + + /** + * 用户统计数量 + * @param currentUserId + * @return + */ + Long queryByUserId(Long currentUserId); + + /** + * 统计数据 + * + * @param statisticsDTO + * @return + */ + PageResult statisticsFriendByUserId(WeChatStatisticsDTO statisticsDTO); +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/impl/AliPayServiceImpl.java b/admin/src/main/java/com/baiye/modules/distribute/service/impl/AliPayServiceImpl.java new file mode 100644 index 0000000..a393389 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/impl/AliPayServiceImpl.java @@ -0,0 +1,205 @@ +package com.baiye.modules.distribute.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alipay.easysdk.factory.Factory; +import com.alipay.easysdk.kernel.util.ResponseChecker; +import com.alipay.easysdk.payment.page.models.AlipayTradePagePayResponse; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.constant.PayConstants; +import com.baiye.constant.enums.ResponseCode; +import com.baiye.exception.BadRequestException; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import com.baiye.modules.distribute.dto.AliPayPcDTO; +import com.baiye.modules.distribute.entity.PayOrder; +import com.baiye.modules.distribute.mapper.PayOrderMapper; +import com.baiye.modules.distribute.service.AliPayService; +import com.baiye.modules.distribute.service.LoginEquipmentService; +import com.baiye.modules.distribute.service.WeChatService; +import com.baiye.security.util.SecurityUtils; +import com.baiye.system.properties.AliPayProperties; +import com.google.common.collect.Maps; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.servlet.http.HttpServletRequest; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Enzo + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AliPayServiceImpl extends ExtendServiceImpl implements AliPayService { + + + @Value("${snowflake.workerId}") + private int workerId; + + @Value("${snowflake.datacenterId}") + private int datacenterId; + + + private final AliPayProperties aliPay; + + + private final WeChatService weChatService; + + + private final LoginEquipmentService loginEquipmentService; + + + @Override + @Transactional(rollbackFor = Exception.class) + public String pcNotifyResponse(HttpServletRequest request) { + log.info("================ the pay notify enter date as {} ================", DateUtil.date()); + //获取支付宝GET过来反馈信息 + Map params = new HashMap<>(DefaultNumberConstants.SIXTEEN_NUMBER); + // 处理乱码问题 + checkParam(request, params); + Boolean flag; + try { + flag = Factory.Payment.Common().verifyNotify(params); + //商户订单号 + String orderNo = new String(request.getParameter(PayConstants.OUT_TRADE_NO).getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); + //前台回调验证签名 v1 or v2 + if (flag != null && flag) { + log.info("========== order verify signature success the orderNumber as {} ==========", orderNo); + List orderList = baseMapper.findByOrderNumber(orderNo); + if (CollUtil.isNotEmpty(orderList) && orderList.get(DefaultNumberConstants.ZERO_NUMBER).getStatus() == DefaultNumberConstants.ZERO_NUMBER) { + PayOrder order = orderList.get(DefaultNumberConstants.ZERO_NUMBER); + log.info("========== the order notify success date as {} ==========", DateUtil.date()); + order.setPayTime(DateUtil.date()); + order.setStatus(DefaultNumberConstants.ONE_NUMBER); + if (Boolean.FALSE.equals(order.getIsRenew())) { + // 创建设备 + weChatService.createEquipment(order.getNum(), order.getPayType(), order.getUserId()); + } + if (Boolean.TRUE.equals(order.getIsRenew())) { + // 续费操作设备 + weChatService.equipmentRenewal(order.getRobotId(), order.getPayType()); + } + + return this.updateById(order) ? ResponseCode.SUCCESS.getDesc() : ResponseCode.FAILURE.getDesc(); + } + } + } catch (Exception e) { + log.error("the pc response error time {}", LocalTime.now()); + throw new BadRequestException(ResponseCode.CALLBACK_FAILED.getDesc()); + } + return ResponseCode.FAILURE.getDesc(); + } + + + private void checkParam(HttpServletRequest request, Map params) { + Map requestParams = request.getParameterMap(); + if (CollectionUtils.isNotEmpty(requestParams.keySet())) { + for (Map.Entry stringEntry : requestParams.entrySet()) { + String[] values = stringEntry.getValue(); + String valueStr = CharSequenceUtil.EMPTY; + for (int i = 0; i < values.length; i++) { + valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + StrPool.COMMA; + } + /* //乱码解决,这段代码在出现乱码时使用 + valueStr = new String(valueStr.getBytes + (StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);*/ + params.put(stringEntry.getKey(), valueStr); + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Map aliPayPc(AliPayPcDTO aliPayPcDTO) { + Map map = Maps.newHashMap(); + Long userId = SecurityUtils.getCurrentUserId(); + BigDecimal amount = new BigDecimal(DefaultNumberConstants.ZERO_NUMBER); + Integer num = aliPayPcDTO.getNum(); + Integer robotId = aliPayPcDTO.getRobotId(); + Boolean isRenew = aliPayPcDTO.getIsRenew(); + Integer packageType = aliPayPcDTO.getPackageType(); + // 结算金额 + if (packageType == DefaultNumberConstants.TWO_NUMBER) { + // 季度 + amount = NumberUtil.mul + (String.valueOf(DefaultNumberConstants.THIRTY), String.valueOf(DefaultNumberConstants.THREE_NUMBER)); + } + if (packageType == DefaultNumberConstants.THREE_NUMBER) { + // 年度八折 + amount = NumberUtil.mul(NumberUtil.mul + (String.valueOf(DefaultNumberConstants.THIRTY), String.valueOf(DefaultNumberConstants.TWELVE_NUMBER)), 0.8); + } + amount = NumberUtil.mul(amount, num); + // 雪花算法id + String orderNo = String.valueOf(IdUtil.getSnowflake(workerId, datacenterId).nextId()); + // 创建订单 + savePayOrder(num, isRenew, robotId, packageType, amount, orderNo); + + if (packageType == DefaultNumberConstants.ONE_NUMBER) { + Long countFreeEquipment = loginEquipmentService.countFreeEquipment(userId, DefaultNumberConstants.ONE_NUMBER); + if (ObjectUtil.isNotNull(countFreeEquipment) + && countFreeEquipment + num > DefaultNumberConstants.THREE_NUMBER) { + throw new BadRequestException("至多创建三台免费设备!"); + } + // 创建设备 + weChatService.createEquipment(num, packageType, userId); + map.put("orderNo", orderNo); + return map; + } + + // 查询记录 + try { + // 生成返回对象 + AlipayTradePagePayResponse response = Factory.Payment.Page().pay(PayConstants.PAY_TITLE + orderNo, orderNo, String.valueOf(amount), aliPay.getReturnUrl()); + if (ResponseChecker.success(response)) { + map.put("payUrl", response.getBody()); + map.put("orderNo", orderNo); + return map; + } + } catch (Exception e) { + log.error("the order failed purchaser {} time {}", aliPayPcDTO.getRemark(), LocalTime.now()); + throw new BadRequestException(ResponseCode.ALI_PAY_ERROR.getDesc()); + } + throw new BadRequestException(ResponseCode.ALI_PAY_ERROR.getDesc()); + } + + private Boolean savePayOrder(Integer num, Boolean isRenew, Integer robotId, Integer packageType, BigDecimal amount, String orderNo) { + PayOrder order = new PayOrder(); + order.setNum(num); + order.setAmount(amount); + order.setRobotId(robotId); + order.setIsRenew(isRenew); + order.setOrderNo(orderNo); + order.setPayType(packageType); + order.setUserId(SecurityUtils.getCurrentUserId()); + order.setStatus(packageType > DefaultNumberConstants.ONE_NUMBER + ? DefaultNumberConstants.ZERO_NUMBER : DefaultNumberConstants.ONE_NUMBER); + return this.save(order); + } + + + @Override + public PayOrder findOrderByNo(String orderNo) { + List payOrderList = baseMapper.findByOrderNumber(orderNo); + if (CollUtil.isNotEmpty(payOrderList)) { + return payOrderList.get(DefaultNumberConstants.ZERO_NUMBER); + } + return new PayOrder(); + } + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/impl/LoginEquipmentServiceImpl.java b/admin/src/main/java/com/baiye/modules/distribute/service/impl/LoginEquipmentServiceImpl.java new file mode 100644 index 0000000..d8ed376 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/impl/LoginEquipmentServiceImpl.java @@ -0,0 +1,110 @@ +package com.baiye.modules.distribute.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.http.HttpStatus; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.constant.WeChatRequestConstants; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; +import com.baiye.modules.distribute.dto.WeChatAccountDTO; +import com.baiye.modules.distribute.entity.LoginEquipment; +import com.baiye.modules.distribute.mapper.LoginEquipmentMapper; +import com.baiye.modules.distribute.qo.AccountQo; +import com.baiye.modules.distribute.service.LoginEquipmentService; +import com.baiye.modules.distribute.vo.WeChatAccountVO; +import com.baiye.modules.distribute.vo.WeChatAddFriendVo; +import com.baiye.result.WeChatResponse; +import com.baiye.security.util.SecurityUtils; +import com.baiye.system.properties.WeChatProperties; +import com.google.common.collect.Maps; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +/** + * @author Enzo + * @date : 2024/4/1 + */ +@Service +@RequiredArgsConstructor +public class LoginEquipmentServiceImpl extends ExtendServiceImpl implements + LoginEquipmentService { + + private final WeChatProperties weChatProperties; + + @Override + public PageResult queryPage(PageParam pageParam, AccountQo qo) { + Map map = Maps.newHashMap(); + qo.setUserId(SecurityUtils.getCurrentUserId()); + PageResult queryPage = baseMapper.queryPage(pageParam, qo); + queryPage.getRecords().forEach(accountVO -> { + // 默认离线状态 + accountVO.setStatus(DefaultNumberConstants.ZERO_NUMBER); + map.put("robotId", accountVO.getRobotId()); + String robotResult = HttpUtil.get + (weChatProperties.getGatewayHost().concat(WeChatRequestConstants.GET_ROBOT_TYPE) + .concat(weChatProperties.getAppKey()), map); + if (JSONUtil.isTypeJSON(robotResult)) { + WeChatResponse weChatResponse = JSONUtil.toBean(robotResult, WeChatResponse.class); + if (weChatResponse.getStatus() == HttpStatus.HTTP_OK) { + WeChatAccountDTO dto = BeanUtil.toBean(weChatResponse.getData(), WeChatAccountDTO.class); + if (ObjectUtil.isNotNull(dto) && dto.getPit().equals(accountVO.getPit())) { + accountVO.setCityInfo(dto.getProvince()); + accountVO.setStatus(dto.getStatus()); + } + } + } + }); + return queryPage; + } + + @Override + public LoginEquipment queryByRobot(String pit) { + LoginEquipment loginEquipment = baseMapper.selectOne + (WrappersX.lambdaQueryX(LoginEquipment.class).eq(LoginEquipment::getPit, pit)); + if (ObjectUtil.isNotNull(loginEquipment) && ObjectUtil.isNotNull(loginEquipment.getUserId())) { + return loginEquipment; + } + return new LoginEquipment(); + } + + @Override + public List queryByDate(Integer dayNum, Long currentUserId, String wxId) { + return baseMapper.queryByDate(dayNum, currentUserId, wxId); + } + + @Override + public LoginEquipment queryByRobotId(Integer robotId) { + LoginEquipment loginEquipment = baseMapper.selectOne + (WrappersX.lambdaQueryX(LoginEquipment.class).eq(LoginEquipment::getRobotId, robotId)); + if (ObjectUtil.isNotNull(loginEquipment) && ObjectUtil.isNotNull(loginEquipment.getUserId())) { + return loginEquipment; + } + return new LoginEquipment(); + } + + @Override + public List queryByUserId(Long currentUserId) { + return baseMapper.queryByUserId(currentUserId); + } + + @Override + public Long countFreeEquipment(Long currentUserId, Integer number) { + return baseMapper.countFreeEquipment(currentUserId, number); + } + + @Override + public List findExpiredAccount() { + return baseMapper.selectListByTime(DateUtil.date()); + } + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/impl/PayOrderServiceImpl.java b/admin/src/main/java/com/baiye/modules/distribute/service/impl/PayOrderServiceImpl.java new file mode 100644 index 0000000..f7c2c02 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/impl/PayOrderServiceImpl.java @@ -0,0 +1,29 @@ +package com.baiye.modules.distribute.service.impl; + +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import com.baiye.modules.distribute.entity.PayOrder; +import com.baiye.modules.distribute.mapper.PayOrderMapper; +import com.baiye.modules.distribute.qo.PayOrderQo; +import com.baiye.modules.distribute.service.PayOrderService; +import com.baiye.modules.distribute.vo.PayOrderVO; +import com.baiye.security.util.SecurityUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author Enzo + * @date : 2024/3/28 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PayOrderServiceImpl extends ExtendServiceImpl implements PayOrderService { + @Override + public PageResult queryPage(PageParam pageParam, PayOrderQo qo) { + qo.setUserId(SecurityUtils.getCurrentUserId()); + return baseMapper.queryPage(pageParam, qo); + } +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/impl/WeChatServiceImpl.java b/admin/src/main/java/com/baiye/modules/distribute/service/impl/WeChatServiceImpl.java new file mode 100644 index 0000000..ee91bc7 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/impl/WeChatServiceImpl.java @@ -0,0 +1,340 @@ +package com.baiye.modules.distribute.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.http.HttpStatus; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.constant.ResponseConstant; +import com.baiye.constant.WeChatRequestConstants; +import com.baiye.constant.WechatCallbackConstant; +import com.baiye.constant.enums.ResponseCode; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.enums.PackageEnum; +import com.baiye.exception.BadRequestException; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import com.baiye.modules.distribute.dto.*; +import com.baiye.modules.distribute.entity.LoginEquipment; +import com.baiye.modules.distribute.entity.WeChatAccount; +import com.baiye.modules.distribute.entity.WeChatEquipment; +import com.baiye.modules.distribute.mapper.WeChatAccountMapper; +import com.baiye.modules.distribute.mapper.WeChatEquipmentMapper; +import com.baiye.modules.distribute.qo.AccountQo; +import com.baiye.modules.distribute.service.LoginEquipmentService; +import com.baiye.modules.distribute.service.WeChatService; +import com.baiye.modules.distribute.service.WechatFriendService; +import com.baiye.modules.distribute.vo.AccountStatisticsVO; +import com.baiye.modules.distribute.vo.AddFriendVo; +import com.baiye.modules.distribute.vo.WeChatAccountVO; +import com.baiye.modules.distribute.vo.WeChatAddFriendVo; +import com.baiye.result.WeChatResponse; +import com.baiye.security.util.SecurityUtils; +import com.baiye.system.properties.WeChatProperties; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Enzo + * @date 2024-3-27 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WeChatServiceImpl extends ExtendServiceImpl implements WeChatService { + + + @Value("${snowflake.workerId}") + private int workerId; + + @Value("${snowflake.datacenterId}") + private int datacenterId; + + private final WeChatProperties weChatProperties; + + private final WechatFriendService wechatFriendService; + + private final LoginEquipmentService loginEquipmentService; + + private final WeChatEquipmentMapper weChatEquipmentMapper; + + + @Override + public String createQrCode(CreateCodeDTO createCodeDTO) { + Map beanToMap = BeanUtil.beanToMap(createCodeDTO); + String getQrCodeResult = HttpUtil.get + (weChatProperties.getGatewayHost().concat(WeChatRequestConstants.GET_QR_CODE) + .concat(weChatProperties.getAppKey()), beanToMap); + log.info("=============== the response as {} =============", getQrCodeResult); + if (StringUtils.isNotBlank(getQrCodeResult)) { + WeChatResponse weChatResponse = JSONUtil.toBean(getQrCodeResult, WeChatResponse.class); + return weChatResponse.getMsg(); + } + throw new BadRequestException("创建失败"); + } + + @Override + public Boolean addFriend(WeChatAddFriendDTO weChatFriendDTO) { + String addedFriendResult = HttpUtil.post(weChatProperties.getGatewayHost().concat + (WeChatRequestConstants.WE_CHAT_ADD_FRIEND).concat(weChatProperties.getAppKey()), JSONUtil.toJsonStr(weChatFriendDTO)); + log.info("=============== the response as {} =============", addedFriendResult); + if (addedFriendResult.contains(ResponseCode.SUCCESS.getDesc())) { + WeChatResponse weChatResponse = JSONUtil.toBean(addedFriendResult, WeChatResponse.class); + if (weChatResponse.getStatus() == HttpStatus.HTTP_OK) { + return Boolean.TRUE; + } + throw new BadRequestException(weChatResponse.getMsg()); + } + return Boolean.FALSE; + } + + @Override + public PageResult queryPage(PageParam pageParam, AccountQo qo) { + qo.setUserId(SecurityUtils.getCurrentUserId()); + return baseMapper.queryPage(pageParam, qo); + } + + @Override + public Boolean createEquipment(Integer num, Integer packageType, Long userId) { + Map map = ImmutableMap.of("num", num); + String createEquipmentResult = HttpUtil.get + (weChatProperties.getGatewayHost().concat(WeChatRequestConstants.CREATE_EQUIPMENT) + .concat(weChatProperties.getAppKey()), map); + log.info("=============== the response as {} =============", createEquipmentResult); + if (StringUtils.isNotBlank(createEquipmentResult)) { + WeChatResponse weChatResponse = JSONUtil.toBean(createEquipmentResult, WeChatResponse.class); + if (weChatResponse.getStatus() == ResponseConstant.SUCCESS) { + Integer dayNum = PackageEnum.find(packageType); + if (dayNum > DefaultNumberConstants.ZERO_NUMBER) { + EquipmentDTO dto = BeanUtil.toBean(weChatResponse.getData(), EquipmentDTO.class); + DateTime dateTime = DateUtil.endOfDay(DateUtil.offsetDay(DateUtil.date(), dayNum)); + for (EquipmentDTO.RoBotDTO botDTO : dto.getData()) { + // 创建账号 + LoginEquipment equipment = new LoginEquipment(); + equipment.setUserId(userId); + equipment.setPackageType(packageType); + equipment.setExpirationTime(dateTime); + equipment.setRobotId(botDTO.getRobotId()); + equipment.setPit(botDTO.getRobotAccount()); + // equipment.setDeviceNumber(botDTO.getRobotAccount()); + loginEquipmentService.save(equipment); + } + } + return Boolean.TRUE; + } + } + return Boolean.FALSE; + } + + @Override + public String pushCode(PushCodeDTO pushCodeDTO) { + LoginEquipment loginEquipment = loginEquipmentService.queryByRobotId(pushCodeDTO.getRobotId()); + if (ObjectUtil.isNotNull(loginEquipment) && ObjectUtil.isNotNull(loginEquipment.getExpirationTime())) { + if (loginEquipment.getExpirationTime().before(DateUtil.date())) { + throw new BadRequestException("当前设备已过期请续费后使用!"); + } + } + Map beanToMap = BeanUtil.beanToMap(pushCodeDTO); + String pushQrCodeResult = HttpUtil.get + (weChatProperties.getGatewayHost().concat(WeChatRequestConstants.GET_PUSH_LOGIN) + .concat(weChatProperties.getAppKey()), beanToMap); + log.info("=============== the response as {} =============", pushQrCodeResult); + if (StringUtils.isNotBlank(pushQrCodeResult)) { + WeChatResponse weChatResponse = JSONUtil.toBean(pushQrCodeResult, WeChatResponse.class); + return weChatResponse.getMsg(); + } + return CharSequenceUtil.EMPTY; + } + + @Override + public String globalSetting(String callbackUrl) { + List strings = Lists.newArrayList(callbackUrl); + Map map = ImmutableMap.of("callbackUrls", strings); + String globalSetting = HttpUtil.post + (weChatProperties.getGatewayHost().concat + (WeChatRequestConstants.GLOBAL_SETTING).concat(weChatProperties.getAppKey()), JSONUtil.toJsonStr(map)); + if (StringUtils.isNotBlank(globalSetting)) { + WeChatResponse weChatResponse = JSONUtil.toBean(globalSetting, WeChatResponse.class); + return weChatResponse.getMsg(); + } + return CharSequenceUtil.EMPTY; + } + + @Override + public Boolean weChatAccountLoginByRespon(WeChatUserLoginDTO userLoginDTO, Integer status, Long userId, Long id) { + WeChatAccount byWxId = this.baseMapper.findByWxId(userLoginDTO.getWxId()); + if (ObjectUtil.isNull(byWxId) || ObjectUtil.isNull(byWxId.getId())) { + Long saveId = IdUtil.getSnowflake(workerId, datacenterId).nextId(); + WeChatEquipment equipment = new WeChatEquipment(); + byWxId = new WeChatAccount(); + BeanUtil.copyProperties(userLoginDTO, byWxId); + byWxId.setHeaderUrl(userLoginDTO.getBigHeadImgUrl()); + byWxId.setId(saveId); + byWxId.setUserId(userId); + equipment.setUserId(userId); + equipment.setEquipmentId(id); + equipment.setAccountId(saveId); + weChatEquipmentMapper.insert(equipment); + return this.save(byWxId); + } + BeanUtil.copyProperties(userLoginDTO, byWxId); + byWxId.setUserId(userId); + Long counted = weChatEquipmentMapper.countEquipment(byWxId.getId(), id); + if (counted == DefaultNumberConstants.ZERO_NUMBER) { + WeChatEquipment equipment = new WeChatEquipment(); + equipment.setEquipmentId(id); + equipment.setUserId(userId); + equipment.setAccountId(byWxId.getId()); + weChatEquipmentMapper.insert(equipment); + } + byWxId.setStatus(status == WechatCallbackConstant.LOGIN_SUCCESS ? + DefaultNumberConstants.ONE_NUMBER : DefaultNumberConstants.MINUS_ONE_NUMBER); + return this.updateById(byWxId); + } + + + @Override + public PageResult queryFirendData(PageParam pageParam, AccountQo qo) { + qo.setUserId(SecurityUtils.getCurrentUserId()); + List addFriendVos = Lists.newArrayList(); + PageResult accountVo = baseMapper.queryPage(pageParam, qo); + for (WeChatAccountVO vo : accountVo.getRecords()) { + AddFriendVo addFriendVo = new AddFriendVo(); + BeanUtil.copyProperties(vo, addFriendVo); + // 查询数据天数据 + List weChatAddFriendVos = + loginEquipmentService.queryByDate(DefaultNumberConstants.FIFTEEN_NUMBER, SecurityUtils.getCurrentUserId(), vo.getWxId()); + // 转为map + Map> map = weChatAddFriendVos.stream().collect + (Collectors.groupingBy(WeChatAddFriendVo::getDayStr)); + Integer newFriendsInitiativeNum = DefaultNumberConstants.ZERO_NUMBER; + Integer newPassiveNum = DefaultNumberConstants.ZERO_NUMBER; + List newArrayList = Lists.newArrayList(); + for (int i = 0; i < DefaultNumberConstants.FIFTEEN_NUMBER; i++) { + // 时间格式 + String dateTime = DateUtil.format(DateUtil.offsetDay(DateUtil.date(), -i), "MM-dd"); + AddFriendVo.FriendDTO dto = new AddFriendVo.FriendDTO(); + dto.setDayStr(dateTime); + if (map.containsKey(dateTime)) { + List voList = map.get(dateTime); + Map voMap = + voList.stream().collect(Collectors.toMap + (WeChatAddFriendVo::getAddFriendType, chatAddFriendVo -> chatAddFriendVo)); + if (voMap.get(DefaultNumberConstants.ONE_NUMBER) != null) { + WeChatAddFriendVo weChatAddFriendVo = voMap.get(DefaultNumberConstants.ONE_NUMBER); + Integer num = weChatAddFriendVo.getNum(); + newFriendsInitiativeNum += num; + dto.setNewDayFriendsInitiativeNum(num); + } + if (voMap.get(DefaultNumberConstants.TWO_NUMBER) != null) { + WeChatAddFriendVo weChatAddFriendVo = voMap.get(DefaultNumberConstants.TWO_NUMBER); + Integer num = weChatAddFriendVo.getNum(); + newPassiveNum += num; + dto.setNewDayPassiveNum(num); + } + } + newArrayList.add(dto); + addFriendVo.setList(newArrayList); + } + + addFriendVo.setNewFriendsInitiativeNum(newFriendsInitiativeNum); + addFriendVo.setNewPassiveNum(newPassiveNum); + addFriendVos.add(addFriendVo); + } + return new PageResult<>(addFriendVos, accountVo.getTotal()); + } + + @Override + public Boolean equipmentRenewal(Integer robotId, Integer payType) { + // TODO 设备续费 + Integer dayNum = PackageEnum.find(payType); + if (dayNum > DefaultNumberConstants.ZERO_NUMBER) { + LoginEquipment loginEquipment = loginEquipmentService.queryByRobotId(robotId); + if (ObjectUtil.isNotNull(loginEquipment) && ObjectUtil.isNotNull(loginEquipment.getId())) { + DateTime dateTime = DateUtil.endOfDay(DateUtil.offsetDay(loginEquipment.getExpirationTime(), dayNum)); + loginEquipment.setExpirationTime(dateTime); + return loginEquipmentService.updateById(loginEquipment); + } + } + return Boolean.FALSE; + } + + @Override + public WeChatAccount findByWxId(String robotWxId) { + return baseMapper.findByWxId(robotWxId); + } + + + @Override + public AccountStatisticsVO statisticsAccountByUserId(Long currentUserId) { + List onlineList = Lists.newArrayList(); + AccountStatisticsVO vo = new AccountStatisticsVO(); + List weChatAccounts = loginEquipmentService.queryByUserId(currentUserId); + Long friendNum = wechatFriendService.queryByUserId(currentUserId); + Integer abnormalNum = DefaultNumberConstants.ZERO_NUMBER; + int onlineNum = DefaultNumberConstants.ZERO_NUMBER; + Map map = Maps.newHashMap(); + for (WeChatAccountVO weChatAccount : weChatAccounts) { + map.put("robotId", weChatAccount.getRobotId()); + String robotResult = HttpUtil.get + (weChatProperties.getGatewayHost().concat(WeChatRequestConstants.GET_ROBOT_TYPE) + .concat(weChatProperties.getAppKey()), map); + if (JSONUtil.isTypeJSON(robotResult)) { + WeChatResponse weChatResponse = JSONUtil.toBean(robotResult, WeChatResponse.class); + if (weChatResponse.getStatus() == HttpStatus.HTTP_OK) { + WeChatAccountDTO dto = BeanUtil.toBean(weChatResponse.getData(), WeChatAccountDTO.class); + if (ObjectUtil.isNotNull(dto) && + dto.getStatus() == DefaultNumberConstants.ONE_NUMBER + && !onlineList.contains(dto.getPit())) { + onlineList.add(dto.getPit()); + onlineNum += DefaultNumberConstants.ONE_NUMBER; + } + } + } + } + vo.setFriendNum(friendNum); + vo.setOnlineNum(onlineNum); + vo.setAbnormalNum(abnormalNum); + vo.setAccountNum(weChatAccounts.size()); + return vo; + } + + @Override + public Boolean updateRemarkByWechatId(String wxId, String remark) { + return this.baseMapper.updateRemark(wxId, remark); + } + + @Override + public Boolean logoutByRobotId(Integer robotId) { + List arrayList = Lists.newArrayList(robotId); + Map map = Maps.newHashMap(); + map.put("robotIds", arrayList); + String result = HttpUtil.post + (weChatProperties.getGatewayHost().concat(WeChatRequestConstants.ROBOT_LOGOUT) + .concat(weChatProperties.getAppKey()), map); + log.info("=============== the response as {} =============", result); + if (StringUtils.isNotBlank(result) && result.contains(ResponseCode.SUCCESS.getDesc())) { + WeChatResponse weChatResponse = JSONUtil.toBean(result, WeChatResponse.class); + if (weChatResponse.getStatus() == HttpStatus.HTTP_OK) { + return Boolean.TRUE; + } + } + return Boolean.FALSE; + } + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/impl/WechatCallbackServiceImpl.java b/admin/src/main/java/com/baiye/modules/distribute/service/impl/WechatCallbackServiceImpl.java new file mode 100644 index 0000000..5993de4 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/impl/WechatCallbackServiceImpl.java @@ -0,0 +1,91 @@ +package com.baiye.modules.distribute.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.constant.WechatCallbackConstant; +import com.baiye.modules.distribute.dto.WeChatFriendDTO; +import com.baiye.modules.distribute.dto.WeChatUserLoginDTO; +import com.baiye.modules.distribute.entity.LoginEquipment; +import com.baiye.modules.distribute.entity.WeChatAccount; +import com.baiye.modules.distribute.service.LoginEquipmentService; +import com.baiye.modules.distribute.service.WeChatService; +import com.baiye.modules.distribute.service.WechatCallbackService; +import com.baiye.modules.distribute.service.WechatFriendService; +import com.baiye.notify.event.StationNotifyPushEvent; +import com.baiye.notify.model.domain.AnnouncementNotifyInfo; +import com.baiye.result.WeChatResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Collections; + +/** + * @author Enzo + * @date : 2024/4/1 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatCallbackServiceImpl implements WechatCallbackService { + + + private final WeChatService weChatService; + + private final ApplicationContext publisher; + + + private final WechatFriendService wechatFriendService; + + private final LoginEquipmentService loginEquipmentService; + + + @Override + public void analyticalData(WeChatResponse weChatResponse) { + Integer status = weChatResponse.getStatus(); + switch (status) { + case WechatCallbackConstant.LOGIN_OUT: + case WechatCallbackConstant.LOGIN_SUCCESS: + wechatLogin(weChatResponse); + break; + case WechatCallbackConstant.LOGIN_FAIL: + // 登录失败接口 + break; + case WechatCallbackConstant.THROUGH_FRIENDS: + viaFriendRequest(weChatResponse); + break; + default: + } + } + + + + public void wechatLogin(WeChatResponse weChatResponse) { + WeChatUserLoginDTO userLoginDTO = + JSONUtil.toBean(JSONUtil.toJsonStr(weChatResponse.getData()), WeChatUserLoginDTO.class); + LoginEquipment robot = loginEquipmentService.queryByRobot(userLoginDTO.getPit()); + if (ObjectUtil.isNotNull(robot) && ObjectUtil.isNotNull(robot.getUserId())) { + + weChatService.weChatAccountLoginByRespon(userLoginDTO, weChatResponse.getStatus(), robot.getUserId(), robot.getId()); + AnnouncementNotifyInfo notifyInfo = new AnnouncementNotifyInfo(); + notifyInfo.setTitle("新消息通知"); + notifyInfo.setMessageKey(String.valueOf(robot.getRobotId())); + notifyInfo.setContent("用户".concat(userLoginDTO.getNickname()).concat("登录成功")); + notifyInfo.setCreateTime(LocalDateTime.now()); + notifyInfo.setMessageType(DefaultNumberConstants.ONE_NUMBER); + log.info("============ send message {} ==============", JSONUtil.toJsonStr(notifyInfo)); + // 发送消息 + publisher.publishEvent(new StationNotifyPushEvent(notifyInfo, Collections.singletonList(robot.getUserId()))); + } + } + + + private void viaFriendRequest(WeChatResponse weChatResponse) { + WeChatFriendDTO weChatFriendDTO = + JSONUtil.toBean(JSONUtil.toJsonStr(weChatResponse.getData()), WeChatFriendDTO.class); + wechatFriendService.addFriendByResponse(weChatFriendDTO); + } +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/impl/WechatFriendServiceImpl.java b/admin/src/main/java/com/baiye/modules/distribute/service/impl/WechatFriendServiceImpl.java new file mode 100644 index 0000000..0a64da6 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/impl/WechatFriendServiceImpl.java @@ -0,0 +1,137 @@ +package com.baiye.modules.distribute.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.domain.PageResult; +import com.baiye.enums.StatisticsEnum; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; +import com.baiye.modules.distribute.dto.WeChatFriendDTO; +import com.baiye.modules.distribute.dto.WeChatStatisticsDTO; +import com.baiye.modules.distribute.entity.WeChatFriedRecord; +import com.baiye.modules.distribute.mapper.LoginEquipmentMapper; +import com.baiye.modules.distribute.mapper.WeChatFriendMapper; +import com.baiye.modules.distribute.service.WechatFriendService; +import com.baiye.modules.distribute.vo.AddFriendVo; +import com.baiye.modules.distribute.vo.StatisticsFriendVO; +import com.baiye.modules.distribute.vo.WeChatAccountVO; +import com.baiye.modules.distribute.vo.WeChatAddFriendVo; +import com.baiye.security.util.SecurityUtils; +import com.google.common.collect.Lists; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Enzo + * @date : 2024/3/28 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatFriendServiceImpl extends ExtendServiceImpl + implements WechatFriendService { + + + private final LoginEquipmentMapper loginEquipmentMapper; + + + @Override + public void addFriendByResponse(WeChatFriendDTO weChatFriendDTO) { + if (StringUtils.isNotBlank(weChatFriendDTO.getNickName())) { + List voList = loginEquipmentMapper.findEquipmentByAccountId(weChatFriendDTO.getRobotWxId()); + if (CollUtil.isNotEmpty(voList)) { + for (WeChatAccountVO vo : voList) { + if (ObjectUtil.isNotNull(vo.getExpirationTime()) && vo.getExpirationTime().after(DateUtil.date())) { + Long count = this.baseMapper.selectCount(WrappersX.lambdaQueryX(WeChatFriedRecord.class).eq + (WeChatFriedRecord::getRobotWxId, weChatFriendDTO.getRobotWxId()).eq + (WeChatFriedRecord::getFriendId, weChatFriendDTO.getFriendId()).eq(WeChatFriedRecord::getUserId, vo.getUserId())); + if (count == DefaultNumberConstants.ZERO_NUMBER && + vo.getWxId().equals(weChatFriendDTO.getRobotWxId())) { + WeChatFriedRecord weChatFriedRecord = new WeChatFriedRecord(); + BeanUtil.copyProperties(weChatFriendDTO, weChatFriedRecord); + weChatFriedRecord.setUserId(vo.getUserId()); + this.baseMapper.insert(weChatFriedRecord); + } + } + } + } + } + } + + @Override + public List statisticsFriendByType(Integer queryType) { + Integer num = StatisticsEnum.find(queryType); + if (num > DefaultNumberConstants.ZERO_NUMBER) { + DateTime now = DateUtil.date(); + DateTime beginOfDay = DateUtil.beginOfDay(DateUtil.offsetDay(DateUtil.date(), -num)); + return baseMapper.queryByDate(beginOfDay, now, SecurityUtils.getCurrentUserId()); + } + return Lists.newArrayList(); + } + + @Override + public PageResult statisticsFriendByUserId(WeChatStatisticsDTO statisticsDTO) { + Date endTime = statisticsDTO.getEndTime(); + Date startTime = statisticsDTO.getStartTime(); + List weChatIdList = statisticsDTO.getWeChatIdList(); + List addFriendVos = Lists.newArrayList(); + Long dayNum = DateUtil.betweenDay(startTime, endTime, Boolean.TRUE); + AddFriendVo addFriendVo = new AddFriendVo(); + List weChatAddFriendVos = baseMapper.queryByUserList(weChatIdList, dayNum, SecurityUtils.getCurrentUserId(), startTime, endTime); + + // 转为map + Map> map = weChatAddFriendVos.stream().collect + (Collectors.groupingBy(WeChatAddFriendVo::getDayStr)); + Integer newFriendsInitiativeNum = DefaultNumberConstants.ZERO_NUMBER; + Integer newPassiveNum = DefaultNumberConstants.ZERO_NUMBER; + List newArrayList = Lists.newArrayList(); + for (int i = 0; i <= dayNum; i++) { + // 时间格式 + String dateTime = DateUtil.format(DateUtil.offsetDay(startTime, i), "MM-dd"); + AddFriendVo.FriendDTO dto = new AddFriendVo.FriendDTO(); + dto.setDayStr(dateTime); + if (map.containsKey(dateTime)) { + List voList = map.get(dateTime); + Map voMap = + voList.stream().collect(Collectors.toMap + (WeChatAddFriendVo::getAddFriendType, chatAddFriendVo -> chatAddFriendVo)); + if (voMap.get(DefaultNumberConstants.ONE_NUMBER) != null) { + WeChatAddFriendVo weChatAddFriendVo = voMap.get(DefaultNumberConstants.ONE_NUMBER); + Integer num = weChatAddFriendVo.getNum(); + newFriendsInitiativeNum += num; + dto.setNewDayFriendsInitiativeNum(num); + } + if (voMap.get(DefaultNumberConstants.TWO_NUMBER) != null) { + WeChatAddFriendVo weChatAddFriendVo = voMap.get(DefaultNumberConstants.TWO_NUMBER); + Integer num = weChatAddFriendVo.getNum(); + newPassiveNum += num; + dto.setNewDayPassiveNum(num); + } + } + newArrayList.add(dto); + addFriendVo.setList(newArrayList); + } + addFriendVo.setNewFriendsInitiativeNum(newFriendsInitiativeNum); + addFriendVo.setNewPassiveNum(newPassiveNum); + addFriendVos.add(addFriendVo); + return new PageResult<>(addFriendVos, addFriendVos.size()); + } + + @Override + public Long queryByUserId(Long currentUserId) { + return this.baseMapper.queryByUserId(currentUserId); + } + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/vo/AccountStatisticsVO.java b/admin/src/main/java/com/baiye/modules/distribute/vo/AccountStatisticsVO.java new file mode 100644 index 0000000..2037309 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/vo/AccountStatisticsVO.java @@ -0,0 +1,22 @@ +package com.baiye.modules.distribute.vo; + +import lombok.Data; + +/** + * @author Enzo + * @date : 2024/4/7 + */ +@Data +public class AccountStatisticsVO { + + private Integer accountNum; + + private Long friendNum; + + private Integer onlineNum; + + private Integer abnormalNum; + + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/vo/AddFriendVo.java b/admin/src/main/java/com/baiye/modules/distribute/vo/AddFriendVo.java new file mode 100644 index 0000000..b05d706 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/vo/AddFriendVo.java @@ -0,0 +1,49 @@ +package com.baiye.modules.distribute.vo; + +import com.baiye.constant.DefaultNumberConstants; +import lombok.*; + +import java.util.List; + +/** + * @author Enzo + * @date : 2024/4/3 + */ +@Data +public class AddFriendVo { + + private String wxId; + + private Integer robotId; + + private String username; + + private String nickname; + + private String robotWxNick; + + private String headerUrl; + + private Integer newPassiveNum; + + private List list; + + private Integer newFriendsInitiativeNum; + + /** + * list + */ + @AllArgsConstructor + @NoArgsConstructor + @ToString + @Getter + @Setter + public static class FriendDTO { + + private String dayStr; + + private Integer newDayFriendsInitiativeNum = DefaultNumberConstants.ZERO_NUMBER; + + private Integer newDayPassiveNum = DefaultNumberConstants.ZERO_NUMBER; + } +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/vo/PayOrderVO.java b/admin/src/main/java/com/baiye/modules/distribute/vo/PayOrderVO.java new file mode 100644 index 0000000..1e3c359 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/vo/PayOrderVO.java @@ -0,0 +1,43 @@ +package com.baiye.modules.distribute.vo; + +import cn.hutool.core.date.DatePattern; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Date; + +@Data +public class PayOrderVO { + + @Schema(name = "pay_type") + private Integer payType; + + @Schema(name = "order_number") + private String orderNumber; + + @Schema(name = "status") + private Integer status; + + @Schema(name = "purchaser") + private String purchaser; + + @Schema(name = "pay_time") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DatePattern.NORM_DATETIME_PATTERN, timezone = "GMT+8") + private Date payTime; + + @Schema(name = "amount") + private Double amount; + + @Schema(name = "user_id") + private Long userId; + + + @Schema(title = "创建时间") + private LocalDateTime createTime; + + @Schema(title = "修改时间") + private LocalDateTime updateTime; + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/vo/StatisticsFriendVO.java b/admin/src/main/java/com/baiye/modules/distribute/vo/StatisticsFriendVO.java new file mode 100644 index 0000000..9aa058a --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/vo/StatisticsFriendVO.java @@ -0,0 +1,16 @@ +package com.baiye.modules.distribute.vo; + +import lombok.Data; + +/** + * @author Enzo + * @date : 2024/4/3 + */ +@Data +public class StatisticsFriendVO { + + + private Integer addFriendType; + + private Integer count; +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/vo/WeChatAccountVO.java b/admin/src/main/java/com/baiye/modules/distribute/vo/WeChatAccountVO.java new file mode 100644 index 0000000..78d6839 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/vo/WeChatAccountVO.java @@ -0,0 +1,86 @@ +package com.baiye.modules.distribute.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Date; + +/** + * @author Enzo + * @date : 2024/3/26 + */ +@Data +public class WeChatAccountVO { + + @Schema(title = "id") + private Long id; + + + @Schema(title = "用户id") + private Long userId; + + + @Schema(title = "设备信息") + private String deviceInformation; + + @Schema(title = "用户名称") + private String username; + + @Schema(title = "用户id") + private String nickname; + + @Schema(title = "城市信息") + private String cityInfo; + + @Schema(title = "登录类型") + private Integer loginType; + + @Schema(name = "登录时间") + private Date loginDate; + + @Schema(name = "头像") + private String headerUrl; + + @Schema(name = "微信ID") + private String wxId; + + @Schema(name = "性别") + private Integer sex; + + @Schema(name = "拥有者") + private Long ownerId; + + + + @Schema(title = "创建时间") + private LocalDateTime createTime; + + + @Schema(title = "修改时间") + private LocalDateTime updateTime; + + @Schema(title = "在线状态") + private Integer status; + + + @Schema(title = "备注") + private String remark; + + @Schema(title = "pit") + private String pit; + + + @Schema(title = "微信号") + private String weChatNo; + + @Schema(title = "设备号") + private Integer robotId; + + private String deviceNumber; + + @Schema(title = "过期时间") + private Date expirationTime; + + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/vo/WeChatAddFriendVo.java b/admin/src/main/java/com/baiye/modules/distribute/vo/WeChatAddFriendVo.java new file mode 100644 index 0000000..10e7ec4 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/vo/WeChatAddFriendVo.java @@ -0,0 +1,18 @@ +package com.baiye.modules.distribute.vo; + +import lombok.Data; + +/** + * @author Enzo + * @date : 2024/4/3 + */ +@Data +public class WeChatAddFriendVo { + + private String dayStr; + + private Integer num; + + private Integer addFriendType; + +} diff --git a/admin/src/main/java/com/baiye/properties/PayConfig.java b/admin/src/main/java/com/baiye/properties/PayConfig.java new file mode 100644 index 0000000..6e11e51 --- /dev/null +++ b/admin/src/main/java/com/baiye/properties/PayConfig.java @@ -0,0 +1,40 @@ +package com.baiye.properties; + +import com.alipay.easysdk.factory.Factory; +import com.alipay.easysdk.kernel.Config; +import com.baiye.system.properties.AliPayProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +/** + * @author Enzo + */ +@Component +@Configuration +@EnableConfigurationProperties({AliPayProperties.class}) +public class PayConfig { + + private final AliPayProperties aliPay; + + public PayConfig(AliPayProperties aliPay) { + this.aliPay = aliPay; + Config config = new Config(); + config.protocol = aliPay.getProtocol(); + config.gatewayHost = aliPay.getGatewayHost(); + config.signType = aliPay.getSignType(); + config.appId = aliPay.getAppId(); + /** + * 为避免私钥随源码泄露,推荐从文件中读取私钥字符串而不是写入源码中 + */ + config.merchantPrivateKey = aliPay.getMerchantPrivateKey(); + config.alipayPublicKey = aliPay.getAliPayPublicKey(); + config.notifyUrl = aliPay.getNotifyUrl(); + config.encryptKey = aliPay.getEncryptKey(); + Factory.setOptions(config); + } + + public AliPayProperties getConfig(){ + return aliPay; + } +} diff --git a/admin/src/main/java/com/baiye/schedule/AccountTask.java b/admin/src/main/java/com/baiye/schedule/AccountTask.java new file mode 100644 index 0000000..c47188b --- /dev/null +++ b/admin/src/main/java/com/baiye/schedule/AccountTask.java @@ -0,0 +1,64 @@ +package com.baiye.schedule; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.http.HttpUtil; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.constant.WeChatRequestConstants; +import com.baiye.modules.distribute.entity.LoginEquipment; +import com.baiye.modules.distribute.service.LoginEquipmentService; +import com.baiye.system.properties.WeChatProperties; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @author Enzo + * @date : 2024/4/10 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class AccountTask { + + + private final WeChatProperties weChatProperties; + + private final LoginEquipmentService loginEquipmentService; + + + /** + * 批量下线 + */ + @Scheduled(cron = "0 0 1 * * ? ") + public void updateAccountMassNum() { + List arrayList = Lists.newArrayList(); + List equipmentList = + loginEquipmentService.findExpiredAccount(); + if (CollUtil.isNotEmpty(equipmentList)) { + for (LoginEquipment equipment : equipmentList) { + DateTime dateTime = DateUtil.offsetDay + (equipment.getExpirationTime(), DefaultNumberConstants.FIFTEEN_NUMBER); + if (dateTime.before(DateUtil.date())) { + arrayList.add(equipment.getRobotId()); + } + } + if (CollUtil.isNotEmpty(arrayList)) { + Map map = Maps.newHashMap(); + map.put("robotIds", arrayList); + String result = HttpUtil.post + (weChatProperties.getGatewayHost().concat(WeChatRequestConstants.ROBOT_LOGOUT) + .concat(weChatProperties.getAppKey()), map); + log.info("========== the response as {} ==========", result); + } + } + + } +} diff --git a/admin/src/main/java/com/baiye/schedule/DynamicController.java b/admin/src/main/java/com/baiye/schedule/DynamicController.java new file mode 100644 index 0000000..a2e5690 --- /dev/null +++ b/admin/src/main/java/com/baiye/schedule/DynamicController.java @@ -0,0 +1,79 @@ +package com.baiye.schedule; + +import com.baiye.common.job.handler.ElasticJobHandler; +import com.baiye.result.R; +import com.baiye.schedule.handler.DynamicJob; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; +import java.util.Objects; + +/** + * @author Enzo + * @date : 2024/5/28 + */ +@Slf4j +@RestController +@RequestMapping("/dynamic") +public class DynamicController { + + @Autowired + private ElasticJobHandler elasticJobHandler; + + /** + * 创建动态定时任务 + * jobName 任务名称 + * cron cron表达式 0 * * * * ? * + * + * @param params + * @return + */ + @GetMapping("/createJob") + public R createJob(@RequestBody Map params) { + elasticJobHandler.addJob(params.get("jobName").toString(), params.get("cron").toString(), 1, new DynamicJob(), Objects.isNull(params.get("params")) ? "" : params.get("params").toString(), Objects.isNull(params.get("description")) ? "" : params.get("description").toString()); + return R.ok(); + } + + + /** + * 更新定时任务(似乎,好像,他内内的这个方法没用!!!) + * jobName + * cron cron表达式 0 0/5 * * * ? + * + * @return + */ + @GetMapping("/updateJob") + public R updateJob(@RequestBody Map params) { + if (Objects.isNull(params.get("jobName"))) { + return R.failed("jobName不能为空"); + } + + if (Objects.isNull(params.get("cron"))) { + return R.failed("cron不能为空"); + } + elasticJobHandler.updateJob(params.get("jobName").toString(), params.get("cron").toString()); + return R.ok("请求成功"); + } + + /** + * 删除定时任务 + * jobName 任务名称 + * + * @return + */ + @GetMapping("/removeJob") + public R removeJob(@RequestBody Map params) { + if (Objects.isNull(params.get("jobName"))) { + return R.failed("jobName不能为空"); + } + elasticJobHandler.removeJob(params.get("jobName").toString()); + return R.ok("请求成功"); + } + + +} diff --git a/admin/src/main/java/com/baiye/schedule/entity/JobDynamicTask.java b/admin/src/main/java/com/baiye/schedule/entity/JobDynamicTask.java new file mode 100644 index 0000000..c7b62c3 --- /dev/null +++ b/admin/src/main/java/com/baiye/schedule/entity/JobDynamicTask.java @@ -0,0 +1,46 @@ +package com.baiye.schedule.entity; + +import com.baiye.entity.BaseEntity; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +/** + * @author Enzo + * @date : 2024/5/28 + */ +@Getter +@Setter +@TableName(value = "tb_job_dynamic_task") +@Schema(title = "定时任务") +public class JobDynamicTask extends BaseEntity implements Serializable { + /** + * ID + */ + @TableId + @Schema(title = "ID") + private Long id; + + @Schema(title = "is_delete") + private Integer isDelete; + + @Schema(title = "job_name") + private String jobName; + + @Schema(title = "cron") + private String cron; + + @Schema(title = "description") + private String description; + + @Schema(title = "parameters") + private String parameters; + + @Schema(title = "status") + private Integer status; +} diff --git a/admin/src/main/java/com/baiye/schedule/handler/DynamicJob.java b/admin/src/main/java/com/baiye/schedule/handler/DynamicJob.java new file mode 100644 index 0000000..2fac3c1 --- /dev/null +++ b/admin/src/main/java/com/baiye/schedule/handler/DynamicJob.java @@ -0,0 +1,34 @@ +package com.baiye.schedule.handler; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import com.dangdang.ddframe.job.api.ShardingContext; +import com.dangdang.ddframe.job.api.simple.SimpleJob; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; + +/** + * @author Enzo + * @date : 2024/5/28 + */ +@Slf4j +public class DynamicJob implements SimpleJob { + + /** + * 业务执行逻辑 + * + * @param shardingContext + */ + @Override + public void execute(ShardingContext shardingContext) { + log.info("{}动态定时任务执行逻辑start...", DateUtil.format(new Date(), DatePattern.NORM_DATETIME_MS_PATTERN)); + String jobName = shardingContext.getJobName(); + String jobParameter = shardingContext.getJobParameter(); + log.info("---------DynamicJob---------动态定时任务正在执行:jobName = {}, jobParameter = {}", jobName, jobParameter); + + //根据参数调用不同的业务接口处理,请远程调用业务模块处理,避免本服务与业务依赖过重... + + log.info("{}动态定时任务执行逻辑end...", DateUtil.format(new Date(), DatePattern.NORM_DATETIME_MS_PATTERN)); + } +} diff --git a/admin/src/main/java/com/baiye/schedule/handler/ScanDynamicJobHandler.java b/admin/src/main/java/com/baiye/schedule/handler/ScanDynamicJobHandler.java new file mode 100644 index 0000000..8327381 --- /dev/null +++ b/admin/src/main/java/com/baiye/schedule/handler/ScanDynamicJobHandler.java @@ -0,0 +1,41 @@ +package com.baiye.schedule.handler; + +import com.baiye.common.job.handler.ElasticJobHandler; +import com.baiye.schedule.entity.JobDynamicTask; +import com.baiye.schedule.service.ElasticJobService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @author Enzo + * @date : 2024/5/28 + */ +@Slf4j +@Service +public class ScanDynamicJobHandler { + @Resource + private ElasticJobHandler elasticJobHandler; + @Resource + private ElasticJobService elasticJobService; + + /** + * 扫描动态任务列表,并添加任务 + *

+ * 循环执行的动态任务,本服务重启的时候,需要重新加载任务 + * + * @author songfayuan + * @date 2021/4/26 9:15 下午 + */ + public void scanAddJob() { + // 这里为从MySQL数据库读取job_dynamic_task表的数据,微服务项目中建议使用feign从业务服务获取,避免本服务过度依赖业务的问题,然后业务服务新增动态任务也通过feign调取本服务JobOperateController实现,从而相对独立本服务模块 + List jobDynamicTaskList = this.elasticJobService.getAllList(); + log.info("扫描动态任务列表,并添加任务:本次共扫描到{}条任务。", jobDynamicTaskList.size()); + for (JobDynamicTask jobDynamicTask : jobDynamicTaskList) { + // 创建任务 + elasticJobHandler.addJob(jobDynamicTask.getJobName(), jobDynamicTask.getCron(), 1, new DynamicJob(), jobDynamicTask.getParameters(), jobDynamicTask.getDescription()); + } + } +} diff --git a/admin/src/main/java/com/baiye/schedule/mapper/JobDynamicTaskMapper.java b/admin/src/main/java/com/baiye/schedule/mapper/JobDynamicTaskMapper.java new file mode 100644 index 0000000..dadcb03 --- /dev/null +++ b/admin/src/main/java/com/baiye/schedule/mapper/JobDynamicTaskMapper.java @@ -0,0 +1,14 @@ +package com.baiye.schedule.mapper; + +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.schedule.entity.JobDynamicTask; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author Enzo + * @date : 2024/5/28 + */ +@Mapper +public interface JobDynamicTaskMapper extends ExtendMapper { + +} diff --git a/admin/src/main/java/com/baiye/schedule/service/ElasticJobService.java b/admin/src/main/java/com/baiye/schedule/service/ElasticJobService.java new file mode 100644 index 0000000..46e4644 --- /dev/null +++ b/admin/src/main/java/com/baiye/schedule/service/ElasticJobService.java @@ -0,0 +1,19 @@ +package com.baiye.schedule.service; + +import com.baiye.extend.mybatis.plus.service.ExtendService; +import com.baiye.schedule.entity.JobDynamicTask; + +import java.util.List; + +/** + * @author Enzo + * @date : 2024/5/28 + */ +public interface ElasticJobService extends ExtendService { + + /** + * 获取所有正在运行的 + * @return + */ + List getAllList(); +} diff --git a/admin/src/main/java/com/baiye/schedule/service/impl/ElasticJobServiceImpl.java b/admin/src/main/java/com/baiye/schedule/service/impl/ElasticJobServiceImpl.java new file mode 100644 index 0000000..737671e --- /dev/null +++ b/admin/src/main/java/com/baiye/schedule/service/impl/ElasticJobServiceImpl.java @@ -0,0 +1,28 @@ +package com.baiye.schedule.service.impl; + +import com.baiye.schedule.mapper.JobDynamicTaskMapper; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; +import com.baiye.schedule.entity.JobDynamicTask; +import com.baiye.schedule.service.ElasticJobService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author Enzo + * @date : 2024/5/28 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ElasticJobServiceImpl extends ExtendServiceImpl implements ElasticJobService { + @Override + public List getAllList() { + return baseMapper.selectList(WrappersX.lambdaQueryX(JobDynamicTask.class).eq(JobDynamicTask::getStatus, DefaultNumberConstants.ONE_NUMBER)); + } + +} diff --git a/admin/src/main/resources/application-dev.yml b/admin/src/main/resources/application-dev.yml new file mode 100644 index 0000000..ecceb42 --- /dev/null +++ b/admin/src/main/resources/application-dev.yml @@ -0,0 +1,48 @@ +spring: + datasource: + url: jdbc:mysql://39.100.77.21:3306/marketing-scrm?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true + username: root + password: y7z7noq2 + redis: + host: 39.100.77.21 + password: sC33HXphkHBRj4Jb + port: 6379 + timeout: 5000 + database: 1 + + +alipay: + protocol: https + # 不需要加/gateway.do,这是新旧SDK的区别,切记 + # gatewayHost: openapi.alipaydev.com + gatewayHost: openapi.alipay.com + signType: RSA2 + # 填APPID + appId: 2021003125644167 + # 填应用私钥,注意是应用私钥,不要填成公钥了_(:з」∠)_ + merchantPrivateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCNIuelR5MHMY3vtl2KEgWUbhHMHuBwxHMVONuUYKlt4grOGJtaPKitgh9MxrvaxmAvH0LilQQKd5T07os9jC2XpNmH0BA6rNVNgBwjWNllqKo+jT8OiKvog9uBuZYfHdkLRhjObrTkSo97jO5Y32591GZHG+cQ/5bgMC0KXlQMOv5piLqUfKYDJ5pWBdS5gqcCkUABZsrW+C8nD7lvOuVglN1AGQiXQzj+Iu3K5xcUg+d3q0nWXhKPpotSolFGSakLEqUja/MnsILVHVw8MYMuq6LxzQ7DQ5fCzMPMIF5kYTynsAV5SoE90ilj1vmmI4aa7dc+9OmG/vXyFuoOEK1bAgMBAAECggEAH0K/9EfqNQmw2ouWJGLhgYLvxjqAk/mvU+AIItFWNdR/eC7TGiWdZvEPZb4PFeIio81Uz0MaZgceozHC/Zry7kfBNufK8HQfus7JbLrdTDsTmk9GzD1RdmreT9l/etztmiWokPDMeFRbe443rM+wdYZ6MP3pLEawcG+7SjSigSKrcpltAJ49NhHyfJxnnCye4mM8PevVGXe4nGv/vKJbAfzl5V8MsSY+SVktj6jOUTSa6nypISvQElyv8jeNH/bTDjg0DOHzL4tUHaVbTqHhtqbNo3n9VuKz4shVAp3I+9PpcTQTueTsM9L0Y+j84bV3VZlTIGGzDUD4qWDhUKPJ4QKBgQDQFNB2vLDJLeGfn+SnCncnr/wQo1xJez+deBqJXoDhDWDuZVcWNaEOWgfDronm8dIedWMYHD//WN0GtA5foAUdlaNq/jeKdJFgzJ/hoOtOveHY1OWqJHiJ/YMRBMuLn/E4Jit7q215JU6Jur1T+70HxrUFTQfY8GWdMvSDFHCKDwKBgQCto24hqgcEDKoR3/sm5sUKqYZTHLeQbniM0D5EcOkBNw7UXGYQSbz7mJ34XQKTfrOL71A+LkjxdMfs5q56yxeOQxR4EBO/Fpz8xPRlud5uIp4eTQVJ8I01meZyvojkF6mO5C4tbcXJOpnpU8ohoeTRu7th0oUYObGUYmn0qzuj9QKBgDCOOtsKSwKXF0hFanjkQ0vakCpdxIJNJVoclayqhc5+bbkTos/G8e9EaP1rtDhVA6Ah6l7M8M4oMWOIDraXw7nUmk60RcekTexVs5VWFLLKMnKDs5gRbKNeqgAFq23Ig+SDW7A/H4uefgY7skRvwPuYjdNP113zMvMM2evgkCZXAoGAP5przvz/EOaqrV2EG93QM3WhdHRCcS9mDP6CsINDdmR6lCM8Z577EJX4128KcIiqsAl7NSuzIG8MhKSDKQuXl07PAqOw+AAKhTSH6XNKHMGldaf01f69WvMCzOkqL5LTUzoWCCH7nxhOJH/CvMsWjBTeMJjyk8seVyIteaf3crkCgYEAmY8OC+Vdo/yop+ooedCnvBC7GVMfcqR6+rIQ+ZnKom7jhByIN6w7eEVAnmNa9dCv/pig7HaYbitWmCNf9tbqFfcdY6e5Svele3amgDM9iqtdJ9Rf1PpDPZw+gmjRIxGs6W8wnl1Sqvpk8pFUA/2xlT6sXpfxra9N09LS0ttU+cU= + # 填支付宝公钥,注意不是生成的应用公钥 + aliPayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyvegkGCrz1i5/K7VzQxvoZm4G73pSPNu9R5ET8YonIuDVoHkHaYvQQizLdRkTyBsYIik8FXsSDmVsw5MLMQ7OAlZ97nQBkz5TxOX6Px766nUpSVoWXoAz6cpIlWnwgir4t1ph88Ph56l+qmqW3gUj/U6MIuzjxBJlijTMHl96its95Nd4cEFx4j+sFuRYob6D0kcemC7xEFuty7bdupda51Z56GYI1YjuUTryTlFOHZbOSThc2ZMzNC1gPG25bn2Lx6sDuPByk4KW4rQ2v7mSfeUuZZRdjtVSC0WV2M2Cv5L8eLFvZRgNYnXrUJYhnRpT+OBAvJZXaWU2nv/bNe/UQIDAQAB + # 回调地址 + notifyUrl: http://39.100.77.21:8088/pay/aliPay/pay-notify + # 支付宝成功支付跳转页面 + returnUrl: https://count.byffp.top/statePage/success + # 可设置AES密钥,调用AES加解密相关接口时需要(可选) + encryptKey: + + +springdoc: + swagger-ui: + urls: + - { name: 'admin', url: '/v3/api-docs' } + - { name: 'api', url: 'http://ballcat-api/v3/api-docs' } + +#mybatis plus 设置 +mybatis-plus: + configuration: +# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志 + log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl #关闭sql日志 + +wechat: + gatewayHost: https://fission-server.scrm-ai.com + appKey: SFkWRAued71GvCeClj8efDhAG6bJ2rzT diff --git a/admin/src/main/resources/application-prod.yml b/admin/src/main/resources/application-prod.yml new file mode 100644 index 0000000..dbfe044 --- /dev/null +++ b/admin/src/main/resources/application-prod.yml @@ -0,0 +1,39 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/marketing-scrm?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true + username: root + password: CK8KyUngFSY + redis: + host: localhost + password: + port: 6379 + + + +# 生产环境关闭文档 +ballcat: + openapi: + enabled: false + +alipay: + protocol: https + # 不需要加/gateway.do,这是新旧SDK的区别,切记 + # gatewayHost: openapi.alipaydev.com + gatewayHost: openapi.alipay.com + signType: RSA2 + # 填APPID + appId: 2021003125644167 + # 填应用私钥,注意是应用私钥,不要填成公钥了_(:з」∠)_ + merchantPrivateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCNIuelR5MHMY3vtl2KEgWUbhHMHuBwxHMVONuUYKlt4grOGJtaPKitgh9MxrvaxmAvH0LilQQKd5T07os9jC2XpNmH0BA6rNVNgBwjWNllqKo+jT8OiKvog9uBuZYfHdkLRhjObrTkSo97jO5Y32591GZHG+cQ/5bgMC0KXlQMOv5piLqUfKYDJ5pWBdS5gqcCkUABZsrW+C8nD7lvOuVglN1AGQiXQzj+Iu3K5xcUg+d3q0nWXhKPpotSolFGSakLEqUja/MnsILVHVw8MYMuq6LxzQ7DQ5fCzMPMIF5kYTynsAV5SoE90ilj1vmmI4aa7dc+9OmG/vXyFuoOEK1bAgMBAAECggEAH0K/9EfqNQmw2ouWJGLhgYLvxjqAk/mvU+AIItFWNdR/eC7TGiWdZvEPZb4PFeIio81Uz0MaZgceozHC/Zry7kfBNufK8HQfus7JbLrdTDsTmk9GzD1RdmreT9l/etztmiWokPDMeFRbe443rM+wdYZ6MP3pLEawcG+7SjSigSKrcpltAJ49NhHyfJxnnCye4mM8PevVGXe4nGv/vKJbAfzl5V8MsSY+SVktj6jOUTSa6nypISvQElyv8jeNH/bTDjg0DOHzL4tUHaVbTqHhtqbNo3n9VuKz4shVAp3I+9PpcTQTueTsM9L0Y+j84bV3VZlTIGGzDUD4qWDhUKPJ4QKBgQDQFNB2vLDJLeGfn+SnCncnr/wQo1xJez+deBqJXoDhDWDuZVcWNaEOWgfDronm8dIedWMYHD//WN0GtA5foAUdlaNq/jeKdJFgzJ/hoOtOveHY1OWqJHiJ/YMRBMuLn/E4Jit7q215JU6Jur1T+70HxrUFTQfY8GWdMvSDFHCKDwKBgQCto24hqgcEDKoR3/sm5sUKqYZTHLeQbniM0D5EcOkBNw7UXGYQSbz7mJ34XQKTfrOL71A+LkjxdMfs5q56yxeOQxR4EBO/Fpz8xPRlud5uIp4eTQVJ8I01meZyvojkF6mO5C4tbcXJOpnpU8ohoeTRu7th0oUYObGUYmn0qzuj9QKBgDCOOtsKSwKXF0hFanjkQ0vakCpdxIJNJVoclayqhc5+bbkTos/G8e9EaP1rtDhVA6Ah6l7M8M4oMWOIDraXw7nUmk60RcekTexVs5VWFLLKMnKDs5gRbKNeqgAFq23Ig+SDW7A/H4uefgY7skRvwPuYjdNP113zMvMM2evgkCZXAoGAP5przvz/EOaqrV2EG93QM3WhdHRCcS9mDP6CsINDdmR6lCM8Z577EJX4128KcIiqsAl7NSuzIG8MhKSDKQuXl07PAqOw+AAKhTSH6XNKHMGldaf01f69WvMCzOkqL5LTUzoWCCH7nxhOJH/CvMsWjBTeMJjyk8seVyIteaf3crkCgYEAmY8OC+Vdo/yop+ooedCnvBC7GVMfcqR6+rIQ+ZnKom7jhByIN6w7eEVAnmNa9dCv/pig7HaYbitWmCNf9tbqFfcdY6e5Svele3amgDM9iqtdJ9Rf1PpDPZw+gmjRIxGs6W8wnl1Sqvpk8pFUA/2xlT6sXpfxra9N09LS0ttU+cU= + # 填支付宝公钥,注意不是生成的应用公钥 + aliPayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyvegkGCrz1i5/K7VzQxvoZm4G73pSPNu9R5ET8YonIuDVoHkHaYvQQizLdRkTyBsYIik8FXsSDmVsw5MLMQ7OAlZ97nQBkz5TxOX6Px766nUpSVoWXoAz6cpIlWnwgir4t1ph88Ph56l+qmqW3gUj/U6MIuzjxBJlijTMHl96its95Nd4cEFx4j+sFuRYob6D0kcemC7xEFuty7bdupda51Z56GYI1YjuUTryTlFOHZbOSThc2ZMzNC1gPG25bn2Lx6sDuPByk4KW4rQ2v7mSfeUuZZRdjtVSC0WV2M2Cv5L8eLFvZRgNYnXrUJYhnRpT+OBAvJZXaWU2nv/bNe/UQIDAQAB + # 回调地址 + notifyUrl: https://count.byffp.top/api/pay/aliPay/pay-notify + # 支付宝成功支付跳转页面 + returnUrl: https://count.byffp.top/statePage/success + # 可设置AES密钥,调用AES加解密相关接口时需要(可选) + encryptKey: + +wechat: + gatewayHost: https://fission-server.scrm-ai.com + appKey: uNE5DXdjwQa1iE8cBVZJbuvGfr5t9R73 diff --git a/admin/src/main/resources/application-test.yml b/admin/src/main/resources/application-test.yml new file mode 100644 index 0000000..04e7fd4 --- /dev/null +++ b/admin/src/main/resources/application-test.yml @@ -0,0 +1,26 @@ +spring: + datasource: + url: jdbc:mysql://39.100.77.21:3306/ad_distribute?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true + username: root + password: y7z7noq2 + redis: + host: 39.100.77.21 + password: sC33HXphkHBRj4Jb + port: 6379 + timeout: 5000 + database: 7 + + + +#mybatis plus 设置 +mybatis-plus: + configuration: + #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志 + log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl #关闭sql日志 + +springdoc: + swagger-ui: + urls: + - { name: 'admin', url: '/v3/api-docs' } + - { name: 'api', url: 'http://ballcat-api/v3/api-docs' } + diff --git a/admin/src/main/resources/application.yml b/admin/src/main/resources/application.yml new file mode 100644 index 0000000..81dd8c3 --- /dev/null +++ b/admin/src/main/resources/application.yml @@ -0,0 +1,106 @@ +server: + port: 8099 + +spring: + application: + name: @artifactId@ + profiles: + active: dev + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + + +# 天爱图形验证码 +captcha: + secondary: + enabled: true + +# mybatis-plus相关配置 +mybatis-plus: + mapper-locations: classpath*:/mapper/**/*Mapper.xml + global-config: + banner: false + db-config: + id-type: auto + insert-strategy: not_empty + update-strategy: not_empty + logic-delete-value: "NOW()" # 逻辑已删除值(使用当前时间标识) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + + +# BallCat 相关配置 +ballcat: + security: + # 前端传输密码的 AES 加密密钥 + password-secret-key: '==market-scrm===' + oauth2: + authorizationserver: + # 登陆验证码是否开启 + login-captcha-enabled: true + # 内嵌的表单登陆页是否开启 + login-page-enabled: false + resourceserver: + ## 忽略鉴权的 url 列表 + ignore-urls: + - /public/** + - /actuator/** + - /doc.html + - /v2/api-docs/** + - /v3/api-docs/** + - /swagger-resources/** + - /swagger-ui/** + - /webjars/** + - /bycdao-ui/** + - /favicon.ico + - /captcha/** + - /outside/** + - /wechat/global/setting + - /pay/aliPay/pay-notify + # 项目 redis 缓存的 key 前缀 + redis: + key-prefix: 'marketing:' + +springdoc: + # 开启 oauth2 端点显示 + show-oauth2-endpoints: true + swagger-ui: + oauth: + client-id: test + client-secret: test + display-request-duration: true + disable-swagger-default-url: true + persist-authorization: true + +# 文件存储路径 +file: + mac: + path: ~/file/ + avatar: ~/avatar/ + clueFilePath: ~/cluefile/ + systemSeparator: / + linux: + path: /home/marketing-scrm/file/ + avatar: /home/marketing-scrm/avatar/ + clueFilePath: /home/marketing-scrm/cluefile/ + systemSeparator: / + windows: + path: C:\marketing-scrm\file\ + avatar: C:\marketing-scrm\avatar\ + clueFilePath: C:\marketing-scrm\cluefile\ + systemSeparator: \ + # 文件大小 /M + maxSize: 300 + avatarMaxSize: 5 + + +elasticjob: + zookeeper: + namespace: springboot-elasticjob + server-list: localhost:2181 + +snowflake: + workerId: 10 + datacenterId: 10 + + diff --git a/admin/src/main/resources/file/template.xlsx b/admin/src/main/resources/file/template.xlsx new file mode 100644 index 0000000..2847f8f Binary files /dev/null and b/admin/src/main/resources/file/template.xlsx differ diff --git a/admin/src/main/resources/logback-spring.xml b/admin/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..77340d5 --- /dev/null +++ b/admin/src/main/resources/logback-spring.xml @@ -0,0 +1,88 @@ + + + elAdmin + + + + + + + + + + + %highlight([%-5level]) %cyan(%d{yyyy-MM-dd#HH:mm:ss.SSS}) %yellow([Thread:%thread]) %magenta([Logger:%logger]) -> %msg%n + utf-8 + + + + + + ${LOG_DIR}/log.log + + + + ${LOG_DIR}/history/%d{yyyy-MM-dd}.gz + 30 + + + true + + + ${LOG_PATTERN} + utf-8 + + + + + INFO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/admin/src/main/resources/mapper/LoginEquipmentMapper.xml b/admin/src/main/resources/mapper/LoginEquipmentMapper.xml new file mode 100644 index 0000000..840935b --- /dev/null +++ b/admin/src/main/resources/mapper/LoginEquipmentMapper.xml @@ -0,0 +1,80 @@ + + + + + wa.username, + wa.nickname, + wa.city_info, + wa.login_type, + wa.header_url, + wa.wx_id, + wa.sex, + wa.status, + wa.we_chat_no, + wa.remark, + wa.create_time, + wa.update_time, + wa.user_id, + le.id, + le.pit, + le.robot_id, + le.device_number, + le.expiration_time + + + + + + + + + diff --git a/admin/src/main/resources/mapper/WeChatFriendMapper.xml b/admin/src/main/resources/mapper/WeChatFriendMapper.xml new file mode 100644 index 0000000..414333d --- /dev/null +++ b/admin/src/main/resources/mapper/WeChatFriendMapper.xml @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/marketing-scrm-admin/.flattened-pom.xml b/marketing-scrm-admin/.flattened-pom.xml new file mode 100644 index 0000000..6b2ddb9 --- /dev/null +++ b/marketing-scrm-admin/.flattened-pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + com.baiye + marketing-scrm + 1.0.0 + + com.baiye + marketing-scrm-admin + 1.0.0 + pom + + admin-core + admin-websocket + + diff --git a/marketing-scrm-admin/admin-core/.flattened-pom.xml b/marketing-scrm-admin/admin-core/.flattened-pom.xml new file mode 100644 index 0000000..585a7aa --- /dev/null +++ b/marketing-scrm-admin/admin-core/.flattened-pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + com.baiye + marketing-scrm-admin + 1.0.0 + + com.baiye + admin-core + 1.0.0 + + + com.baomidou + mybatis-plus-boot-starter + + + com.baiye + common-desensitize + + + com.baiye + common-model + + + com.baiye + security-oauth2-authorization-server + compile + + + com.baiye + security-oauth2-resource-server + + + com.baiye + system-controller + + + com.baiye + marketing-scrm-starter-web + ${revision} + + + com.baiye + marketing-scrm-notify-controller + ${revision} + + + diff --git a/marketing-scrm-admin/admin-core/pom.xml b/marketing-scrm-admin/admin-core/pom.xml new file mode 100644 index 0000000..bef4e74 --- /dev/null +++ b/marketing-scrm-admin/admin-core/pom.xml @@ -0,0 +1,55 @@ + + + + marketing-scrm-admin + com.baiye + ${revision} + + 4.0.0 + admin-core + + + + + com.baomidou + mybatis-plus-boot-starter + + + + com.baiye + common-desensitize + + + com.baiye + common-model + + + + com.baiye + security-oauth2-authorization-server + compile + + + com.baiye + security-oauth2-resource-server + + + com.baiye + system-controller + + + + com.baiye + marketing-scrm-starter-web + ${revision} + + + + + com.baiye + marketing-scrm-notify-controller + ${revision} + + + diff --git a/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/UpmsAutoConfiguration.java b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/UpmsAutoConfiguration.java new file mode 100644 index 0000000..ad346cc --- /dev/null +++ b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/UpmsAutoConfiguration.java @@ -0,0 +1,106 @@ +package com.baiye.upms; + +//import com.hccake.ballcat.admin.upms.log.LogConfiguration; +import com.baiye.system.authentication.BallcatOAuth2TokenResponseEnhancer; +import com.baiye.system.authentication.DefaultUserInfoCoordinatorImpl; +import com.baiye.system.authentication.SysUserDetailsServiceImpl; +import com.baiye.system.authentication.UserInfoCoordinator; +import com.baiye.system.properties.SystemProperties; +import com.baiye.system.service.SysUserService; +import org.ballcat.security.properties.SecurityProperties; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2TokenResponseEnhancer; +import org.ballcat.springsecurity.oauth2.server.resource.introspection.SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +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.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; + +/** + * @author Hccake 2020/5/25 21:01 + */ +@EnableAsync +@AutoConfiguration +@MapperScan("com.baiye.**.mapper") +// @ComponentScan({ "com.hccake.ballcat.admin.upms", "com.hccake.ballcat.system", +// "com.hccake.ballcat.log", +// "com.hccake.ballcat.file", "com.hccake.ballcat.notify" }) +@ComponentScan({ "com.baiye.upms", "com.baiye.system" }) +@EnableConfigurationProperties({ SystemProperties.class, SecurityProperties.class }) +public class UpmsAutoConfiguration { + + /** + * 用户详情处理类 + * + * @author hccake + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(SysUserService.class) + @ConditionalOnMissingBean(UserDetailsService.class) + static class UserDetailsServiceConfiguration { + + /** + * 用户详情处理类 + * @return SysUserDetailsServiceImpl + */ + @Bean + @ConditionalOnMissingBean + public UserDetailsService userDetailsService(SysUserService sysUserService, + UserInfoCoordinator userInfoCoordinator) { + return new SysUserDetailsServiceImpl(sysUserService, userInfoCoordinator); + } + + /** + * 用户信息协调者 + * @return UserInfoCoordinator + */ + @Bean + @ConditionalOnMissingBean + public UserInfoCoordinator userInfoCoordinator() { + return new DefaultUserInfoCoordinatorImpl(); + } + + } + + /** + * 新版本 spring-security-oauth2-authorization-server 使用配置类 + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(OAuth2Authorization.class) + static class SpringOAuth2AuthorizationServerConfiguration { + + /** + * token 端点响应增强,追加一些自定义信息 + * @return TokenEnhancer Token增强处理器 + */ + @Bean + @ConditionalOnMissingBean + public OAuth2TokenResponseEnhancer oAuth2TokenResponseEnhancer() { + return new BallcatOAuth2TokenResponseEnhancer(); + } + + /** + * 当资源服务器和授权服务器的 token 共享存储时,直接使用 OAuth2AuthorizationService 读取 token 信息 + * @return SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "ballcat.security.oauth2.resourceserver", name = "shared-stored-token", + havingValue = "true", matchIfMissing = true) + public OpaqueTokenIntrospector sharedStoredOpaqueTokenIntrospector( + OAuth2AuthorizationService authorizationService) { + return new SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector(authorizationService); + } + + } + +} diff --git a/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/config/mybatis/FillMetaObjectHandle.java b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/config/mybatis/FillMetaObjectHandle.java new file mode 100644 index 0000000..b8549e9 --- /dev/null +++ b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/config/mybatis/FillMetaObjectHandle.java @@ -0,0 +1,42 @@ +package com.baiye.upms.config.mybatis; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baiye.constant.GlobalConstants; +import com.baiye.security.userdetails.User; +import com.baiye.security.util.SecurityUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.reflection.MetaObject; + +import java.time.LocalDateTime; + +/** + * @author Hccake 2019/7/26 14:41 + */ +@Slf4j +public class FillMetaObjectHandle implements MetaObjectHandler { + + @Override + public void insertFill(MetaObject metaObject) { + // 逻辑删除标识 + this.strictInsertFill(metaObject, "deleted", Long.class, GlobalConstants.NOT_DELETED_FLAG); + // 创建时间 + this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); + // 创建人 + User user = SecurityUtils.getUser(); + if (user != null) { + this.strictInsertFill(metaObject, "createBy", Long.class, user.getUserId()); + } + } + + @Override + public void updateFill(MetaObject metaObject) { + // 修改时间 + this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); + // 修改人 + User user = SecurityUtils.getUser(); + if (user != null) { + this.strictUpdateFill(metaObject, "updateBy", Long.class, user.getUserId()); + } + } + +} diff --git a/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/config/mybatis/MybatisPlusConfig.java b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/config/mybatis/MybatisPlusConfig.java new file mode 100644 index 0000000..e982996 --- /dev/null +++ b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/config/mybatis/MybatisPlusConfig.java @@ -0,0 +1,62 @@ +package com.baiye.upms.config.mybatis; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.injector.ISqlInjector; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.baiye.extend.mybatis.plus.injector.CustomSqlInjector; +import com.baiye.extend.mybatis.plus.methods.InsertBatchSomeColumnByCollection; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author hccake + * @date 2020/04/19 默认配置MybatisPlus分页插件,通过conditional注解达到覆盖效用 + */ +@Configuration +public class MybatisPlusConfig { + + /** + * MybatisPlusInterceptor 插件,默认提供分页插件
+ * 如需其他MP内置插件,则需自定义该Bean + * @return MybatisPlusInterceptor + */ + @Bean + @ConditionalOnMissingBean(MybatisPlusInterceptor.class) + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + return interceptor; + } + + /** + * 自动填充处理类 + * @return FillMetaObjectHandle + */ + @Bean + @ConditionalOnMissingBean(MetaObjectHandler.class) + public MetaObjectHandler fillMetaObjectHandle() { + return new FillMetaObjectHandle(); + } + + /** + * 自定义批量插入方法注入 + * @return ISqlInjector + */ + @Bean + @ConditionalOnMissingBean(ISqlInjector.class) + public ISqlInjector customSqlInjector() { + List list = new ArrayList<>(); + // 对于只在更新时进行填充的字段不做插入处理 + list.add(new InsertBatchSomeColumnByCollection(t -> t.getFieldFill() != FieldFill.UPDATE)); + return new CustomSqlInjector(list); + } + +} diff --git a/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/config/task/MdcTaskDecorator.java b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/config/task/MdcTaskDecorator.java new file mode 100644 index 0000000..d32fa76 --- /dev/null +++ b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/config/task/MdcTaskDecorator.java @@ -0,0 +1,34 @@ +package com.baiye.upms.config.task; + +import cn.hutool.core.map.MapUtil; +import org.slf4j.MDC; +import org.springframework.core.task.TaskDecorator; + +import java.util.Map; + +/** + * 使async异步任务支持traceId + * + * @author huyuanzhi 2021-11-06 23:14:27 + */ +public class MdcTaskDecorator implements TaskDecorator { + + @Override + public Runnable decorate(Runnable runnable) { + final Map copyOfContextMap = MDC.getCopyOfContextMap(); + return () -> { + if (MapUtil.isNotEmpty(copyOfContextMap)) { + // 现在:@Async线程上下文! 恢复Web线程上下文的MDC数据 + MDC.setContextMap(copyOfContextMap); + } + + try { + runnable.run(); + } + finally { + MDC.clear(); + } + }; + } + +} diff --git a/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/config/task/TaskExecutionConfiguration.java b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/config/task/TaskExecutionConfiguration.java new file mode 100644 index 0000000..fa3c9c6 --- /dev/null +++ b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/config/task/TaskExecutionConfiguration.java @@ -0,0 +1,35 @@ +package com.baiye.upms.config.task; + +import org.springframework.boot.task.TaskExecutorCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 线程池构建,原本的线程池的拒绝策略为直接抛出异常,不太友好 + * + * @see org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration + * @author hccake + */ +@Configuration(proxyBeanMethods = false) +public class TaskExecutionConfiguration { + + /** + * 修改 springboot 默认配置的 taskExecutor 的拒绝策略为使用当前线程执行 + * @return TaskExecutorCustomizer + */ + @Bean + public TaskExecutorCustomizer taskExecutorCustomizer() { + // AbortPolicy: 直接抛出java.util.concurrent.RejectedExecutionException异常 + // CallerRunsPolicy: 主线程直接执行该任务,执行完之后尝试添加下一个任务到线程池中,可以有效降低向线程池内添加任务的速度 + // DiscardOldestPolicy: 抛弃旧的任务、暂不支持;会导致被丢弃的任务无法再次被执行 + // DiscardPolicy: 抛弃当前任务、暂不支持;会导致被丢弃的任务无法再次被执行 + // 这里使用主线程直接执行该任务 + return (taskExecutor -> { + taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + taskExecutor.setTaskDecorator(new MdcTaskDecorator()); + }); + } + +} diff --git a/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/log/LogConfiguration.java b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/log/LogConfiguration.java new file mode 100644 index 0000000..806f551 --- /dev/null +++ b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/log/LogConfiguration.java @@ -0,0 +1,78 @@ +// package com.hccake.ballcat.admin.upms.log; +// +// import com.hccake.ballcat.common.log.access.handler.AccessLogHandler; +// import com.hccake.ballcat.common.log.operation.handler.OperationLogHandler; +// import com.hccake.ballcat.log.handler.CustomAccessLogHandler; +// import com.hccake.ballcat.log.handler.CustomOperationLogHandler; +// import com.hccake.ballcat.log.model.entity.AccessLog; +// import com.hccake.ballcat.log.model.entity.OperationLog; +// import com.hccake.ballcat.log.service.AccessLogService; +// import com.hccake.ballcat.log.service.LoginLogService; +// import com.hccake.ballcat.log.service.OperationLogService; +// import com.hccake.ballcat.log.thread.AccessLogSaveThread; +// import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +// import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +// import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +// import org.springframework.context.annotation.Bean; +// import org.springframework.context.annotation.Configuration; +// import +// org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +// import +// org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +// +/// ** +// * @author hccake +// */ +// @Configuration(proxyBeanMethods = false) +// @ConditionalOnClass(LoginLogService.class) +// public class LogConfiguration { +// +// /** +// * 访问日志保存 +// * @param accessLogService 访问日志Service +// * @return CustomAccessLogHandler +// */ +// @Bean +// @ConditionalOnBean(AccessLogService.class) +// @ConditionalOnMissingBean(AccessLogHandler.class) +// public AccessLogHandler customAccessLogHandler(AccessLogService +// accessLogService) { +// return new CustomAccessLogHandler(new AccessLogSaveThread(accessLogService)); +// } +// +// /** +// * 操作日志处理器 +// * @param operationLogService 操作日志Service +// * @return CustomOperationLogHandler +// */ +// @Bean +// @ConditionalOnBean(OperationLogService.class) +// @ConditionalOnMissingBean(OperationLogHandler.class) +// public OperationLogHandler customOperationLogHandler(OperationLogService +// operationLogService) { +// return new CustomOperationLogHandler(operationLogService); +// } +// +// @ConditionalOnClass(OAuth2AuthorizationServerConfigurer.class) +// @ConditionalOnBean(LoginLogService.class) +// @ConditionalOnMissingBean(LoginLogHandler.class) +// @Configuration(proxyBeanMethods = false) +// static class SpringAuthorizationServerLoginLogConfiguration { +// +// /** +// * Spring Authorization Server 的登录日志处理,监听登录事件记录登录登出 +// * @param loginLogService 操作日志Service +// * @param authorizationServerSettings 授权服务器设置 +// * @return SpringAuthorizationServerLoginLogHandler +// */ +// @Bean +// public LoginLogHandler springAuthorizationServerLoginLogHandler(LoginLogService +// loginLogService, +// AuthorizationServerSettings authorizationServerSettings) { +// return new SpringAuthorizationServerLoginLogHandler(loginLogService, +// authorizationServerSettings); +// } +// +// } +// +// } \ No newline at end of file diff --git a/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/log/LoginLogHandler.java b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/log/LoginLogHandler.java new file mode 100644 index 0000000..646b19c --- /dev/null +++ b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/log/LoginLogHandler.java @@ -0,0 +1,8 @@ +package com.baiye.upms.log; + +/** + * @author hccake + */ +public interface LoginLogHandler { + +} diff --git a/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/log/SpringAuthorizationServerLoginLogHandler.java b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/log/SpringAuthorizationServerLoginLogHandler.java new file mode 100644 index 0000000..dd3cd6e --- /dev/null +++ b/marketing-scrm-admin/admin-core/src/main/java/com/baiye/upms/log/SpringAuthorizationServerLoginLogHandler.java @@ -0,0 +1,140 @@ +// package com.hccake.ballcat.admin.upms.log; +// +// import com.hccake.ballcat.common.core.util.WebUtils; +//// import com.hccake.ballcat.common.log.operation.enums.LogStatusEnum; +// import com.hccake.ballcat.common.security.util.SecurityUtils; +//// import com.hccake.ballcat.log.enums.LoginEventTypeEnum; +//// import com.hccake.ballcat.log.model.entity.LoginLog; +//// import com.hccake.ballcat.log.service.LoginLogService; +// import lombok.RequiredArgsConstructor; +// import +// org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken; +// import org.springframework.context.event.EventListener; +// import org.springframework.security.authentication.ProviderNotFoundException; +// import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +// import +// org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; +// import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +// import org.springframework.security.authentication.event.LogoutSuccessEvent; +// import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +// import +// org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +// import +// org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken; +// import +// org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +// +// import javax.servlet.http.HttpServletRequest; +// +//// import static com.hccake.ballcat.log.handler.LoginLogUtils.prodLoginLog; +// +/// ** +// * spring 授权服务器的登录日志处理器 +// * +// * @author hccake +// */ +// @RequiredArgsConstructor +// public class SpringAuthorizationServerLoginLogHandler implements LoginLogHandler { +// +//// private final LoginLogService loginLogService; +// +// private final AuthorizationServerSettings authorizationServerSettings; +// +// /** +// * 登录成功事件监听 记录用户登录日志 +// * @param event 登录成功 event +// */ +// @EventListener(AuthenticationSuccessEvent.class) +// public void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) { +// Object source = event.getSource(); +// String username = null; +// +// String tokenEndpoint = authorizationServerSettings.getTokenEndpoint(); +// HttpServletRequest request = WebUtils.getRequest(); +// boolean isOauth2LoginRequest = request.getRequestURI().equals(tokenEndpoint); +// +// // Oauth2登录 和表单登录 处理分开 +// if (isOauth2LoginRequest && source instanceof OAuth2AccessTokenAuthenticationToken) { +// username = SecurityUtils.getAuthentication().getName(); +// } +// else if (!isOauth2LoginRequest && source instanceof +// UsernamePasswordAuthenticationToken) { +// username = ((UsernamePasswordAuthenticationToken) source).getName(); +// } +// +//// if (username != null) { +//// LoginLog loginLog = prodLoginLog(username).setMsg("登录成功") +//// .setStatus(LogStatusEnum.SUCCESS.getValue()) +//// .setEventType(LoginEventTypeEnum.LOGIN.getValue()); +//// loginLogService.save(loginLog); +//// } +// } +// +// /** +// * 监听鉴权失败事件,记录登录失败日志 +// * @param event the event +// */ +// @EventListener(AbstractAuthenticationFailureEvent.class) +// public void onAuthenticationFailureEvent(AbstractAuthenticationFailureEvent event) { +// if (event.getException().getClass().isAssignableFrom(ProviderNotFoundException.class)) +// { +// return; +// } +// +// Object source = event.getSource(); +// String username = null; +// +// String tokenEndpoint = authorizationServerSettings.getTokenEndpoint(); +// HttpServletRequest request = WebUtils.getRequest(); +// boolean isOauth2LoginRequest = request.getRequestURI().equals(tokenEndpoint); +// +// // Oauth2登录 和表单登录 处理分开 +// if (isOauth2LoginRequest && source instanceof +// OAuth2AuthorizationGrantAuthenticationToken) { +// username = ((OAuth2AuthorizationGrantAuthenticationToken) source).getName(); +// } +// else if (!isOauth2LoginRequest && source instanceof +// UsernamePasswordAuthenticationToken) { +// username = ((UsernamePasswordAuthenticationToken) source).getName(); +// } +// +//// if (username != null) { +//// LoginLog loginLog = prodLoginLog(username).setMsg(event.getException().getMessage()) +//// .setEventType(LoginEventTypeEnum.LOGIN.getValue()) +//// .setStatus(LogStatusEnum.FAIL.getValue()); +//// loginLogService.save(loginLog); +//// } +// } +// +// /** +// * 登出成功事件监听 +// * @param event the event +// */ +// @EventListener(LogoutSuccessEvent.class) +// public void onLogoutSuccessEvent(LogoutSuccessEvent event) { +// Object source = event.getSource(); +// String username = null; +// +// String tokenRevocationEndpoint = +// authorizationServerSettings.getTokenRevocationEndpoint(); +// HttpServletRequest request = WebUtils.getRequest(); +// boolean isOauth2Login = request.getRequestURI().equals(tokenRevocationEndpoint); +// +// // Oauth2撤销令牌 和表单登出 处理分开 +// if (isOauth2Login && source instanceof OAuth2TokenRevocationAuthenticationToken) { +// OAuth2Authorization authorization = ((OAuth2TokenRevocationAuthenticationToken) +// source).getAuthorization(); +// username = authorization.getPrincipalName(); +// } +// else if (!isOauth2Login && source instanceof UsernamePasswordAuthenticationToken) { +// username = ((UsernamePasswordAuthenticationToken) source).getName(); +// } +// +//// if (username != null) { +//// LoginLog loginLog = prodLoginLog(username).setMsg("登出成功") +//// .setEventType(LoginEventTypeEnum.LOGOUT.getValue()); +//// loginLogService.save(loginLog); +//// } +// } +// +// } diff --git a/marketing-scrm-admin/admin-core/src/main/resources/META-INF/spring.factories b/marketing-scrm-admin/admin-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..44aa29b --- /dev/null +++ b/marketing-scrm-admin/admin-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.baiye.upms.UpmsAutoConfiguration \ No newline at end of file diff --git a/marketing-scrm-admin/admin-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/marketing-scrm-admin/admin-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..8b53834 --- /dev/null +++ b/marketing-scrm-admin/admin-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.baiye.upms.UpmsAutoConfiguration \ No newline at end of file diff --git a/marketing-scrm-admin/admin-websocket/.flattened-pom.xml b/marketing-scrm-admin/admin-websocket/.flattened-pom.xml new file mode 100644 index 0000000..e2e2687 --- /dev/null +++ b/marketing-scrm-admin/admin-websocket/.flattened-pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + com.baiye + marketing-scrm-admin + 1.0.0 + + com.baiye + admin-websocket + 1.0.0 + + + com.baiye + admin-core + 1.0.0 + + + com.baiye + marketing-scrm-starter-websocket + 1.0.0 + + + com.baiye + distribute-notify-biz + 1.0.0 + compile + + + diff --git a/marketing-scrm-admin/admin-websocket/pom.xml b/marketing-scrm-admin/admin-websocket/pom.xml new file mode 100644 index 0000000..3cd0d34 --- /dev/null +++ b/marketing-scrm-admin/admin-websocket/pom.xml @@ -0,0 +1,32 @@ + + + + marketing-scrm-admin + com.baiye + ${revision} + + 4.0.0 + admin-websocket + + + + com.baiye + admin-core + 1.0.0 + + + com.baiye + marketing-scrm-starter-websocket + 1.0.0 + + + + com.baiye + distribute-notify-biz + 1.0.0 + compile + + + + diff --git a/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/AdminWebSocketAutoConfiguration.java b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/AdminWebSocketAutoConfiguration.java new file mode 100644 index 0000000..5c8a6be --- /dev/null +++ b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/AdminWebSocketAutoConfiguration.java @@ -0,0 +1,34 @@ +package com.baiye; + +import com.baiye.component.UserAttributeHandshakeInterceptor; +import com.baiye.component.UserSessionKeyGenerator; +import com.baiye.session.SessionKeyGenerator; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.web.socket.server.HandshakeInterceptor; + +/** + * @author Hccake 2021/1/5 + * @version 1.0 + */ +@Import({ SystemWebsocketEventListenerConfiguration.class}) +@Configuration +@RequiredArgsConstructor +public class AdminWebSocketAutoConfiguration { + + + + @Bean + @ConditionalOnMissingBean(SessionKeyGenerator.class) + public SessionKeyGenerator userSessionKeyGenerator() { + return new UserSessionKeyGenerator(); + } + + @Bean + public HandshakeInterceptor authenticationHandshakeInterceptor() { + return new UserAttributeHandshakeInterceptor(); + } +} diff --git a/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/SystemWebsocketEventListenerConfiguration.java b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/SystemWebsocketEventListenerConfiguration.java new file mode 100644 index 0000000..9675c05 --- /dev/null +++ b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/SystemWebsocketEventListenerConfiguration.java @@ -0,0 +1,25 @@ +package com.baiye; + +import com.baiye.listener.SystemWebsocketEventListener; +import com.baiye.distribute.MessageDistributor; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Hccake + */ +@RequiredArgsConstructor +@ConditionalOnClass(SystemWebsocketEventListener.class) +@Configuration(proxyBeanMethods = false) +public class SystemWebsocketEventListenerConfiguration { + + private final MessageDistributor messageDistributor; + + @Bean + public SystemWebsocketEventListener systemWebsocketEventListener() { + return new SystemWebsocketEventListener(messageDistributor); + } + +} diff --git a/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/component/UserAttributeHandshakeInterceptor.java b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/component/UserAttributeHandshakeInterceptor.java new file mode 100644 index 0000000..902e30b --- /dev/null +++ b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/component/UserAttributeHandshakeInterceptor.java @@ -0,0 +1,65 @@ +package com.baiye.component; + +import com.baiye.constant.AdminWebSocketConstants; +import com.baiye.security.userdetails.User; +import com.baiye.security.util.SecurityUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.Map; + +/** + * WebSocket 握手拦截器 在握手时记录下当前 session 对应的用户Id和token信息 + * + * @author Hccake 2021/1/4 + * @version 1.0 + */ +@RequiredArgsConstructor +public class UserAttributeHandshakeInterceptor implements HandshakeInterceptor { + + /** + * Invoked before the handshake is processed. + * @param request the current request + * @param response the current response + * @param wsHandler the target WebSocket handler + * @param attributes the attributes from the HTTP handshake to associate with the + * WebSocket session; the provided attributes are copied, the original map is not + * used. + * @return whether to proceed with the handshake ({@code true}) or abort + * ({@code false}) + */ + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, + Map attributes) { + String accessToken = null; + // 获得 accessToken + if (request instanceof ServletServerHttpRequest) { + ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request; + accessToken = serverRequest.getServletRequest().getParameter(AdminWebSocketConstants.TOKEN_ATTR_NAME); + } + // 由于 WebSocket 握手是由 http 升级的,携带 token 已经被 Security 拦截验证了,所以可以直接获取到用户 + User user = SecurityUtils.getUser(); + attributes.put(AdminWebSocketConstants.TOKEN_ATTR_NAME, accessToken); + attributes.put(AdminWebSocketConstants.USER_KEY_ATTR_NAME, user.getUserId()); + return true; + } + + /** + * Invoked after the handshake is done. The response status and headers indicate the + * results of the handshake, i.e. whether it was successful or not. + * @param request the current request + * @param response the current response + * @param wsHandler the target WebSocket handler + * @param exception an exception raised during the handshake, or {@code null} if none + */ + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, + Exception exception) { + // doNothing + } + +} diff --git a/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/component/UserSessionKeyGenerator.java b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/component/UserSessionKeyGenerator.java new file mode 100644 index 0000000..d8760f0 --- /dev/null +++ b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/component/UserSessionKeyGenerator.java @@ -0,0 +1,32 @@ +package com.baiye.component; + +import com.baiye.constant.AdminWebSocketConstants; +import com.baiye.session.SessionKeyGenerator; +import lombok.RequiredArgsConstructor; +import org.springframework.web.socket.WebSocketSession; + +/** + *

+ * 用户 WebSocketSession 唯一标识生成器 + *

+ * + * 此类主要使用当前 session 对应用户的唯一标识做为 session 的唯一标识 方便系统快速通过用户获取对应 session + * + * @author Hccake 2021/1/5 + * @version 1.0 + */ +@RequiredArgsConstructor +public class UserSessionKeyGenerator implements SessionKeyGenerator { + + /** + * 获取当前session的唯一标识,用户的唯一标识已经通过 + * @see UserAttributeHandshakeInterceptor 存储在当前 session 的属性中 + * @param webSocketSession 当前session + * @return session唯一标识 + */ + @Override + public Object sessionKey(WebSocketSession webSocketSession) { + return webSocketSession.getAttributes().get(AdminWebSocketConstants.USER_KEY_ATTR_NAME); + } + +} diff --git a/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/constant/AdminWebSocketConstants.java b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/constant/AdminWebSocketConstants.java new file mode 100644 index 0000000..183b2d1 --- /dev/null +++ b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/constant/AdminWebSocketConstants.java @@ -0,0 +1,22 @@ +package com.baiye.constant; + +/** + * @author Hccake 2021/1/5 + * @version 1.0 + */ +public final class AdminWebSocketConstants { + + private AdminWebSocketConstants() { + } + + /** + * 存储在 WebSocketSession Attribute 中的 token 属性名 + */ + public static final String TOKEN_ATTR_NAME = "access_token"; + + /** + * 存储在 WebSocketSession Attribute 中的 用户唯一标识 属性名 + */ + public static final String USER_KEY_ATTR_NAME = "userId"; + +} diff --git a/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/listener/NotifyWebsocketEventListener.java b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/listener/NotifyWebsocketEventListener.java new file mode 100644 index 0000000..e8aeadd --- /dev/null +++ b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/listener/NotifyWebsocketEventListener.java @@ -0,0 +1,86 @@ +package com.baiye.listener; + + +import com.baiye.distribute.MessageDO; +import com.baiye.distribute.MessageDistributor; +import com.baiye.message.AnnouncementCloseMessage; +import com.baiye.notify.event.AnnouncementCloseEvent; +import com.baiye.notify.event.StationNotifyPushEvent; +import com.baiye.notify.handler.NotifyInfoDelegateHandler; +import com.baiye.notify.model.domain.NotifyInfo; +import com.baiye.util.JsonUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.time.LocalTime; +import java.util.List; + +/** + * @author Hccake 2021/1/5 + * @version 1.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class NotifyWebsocketEventListener { + + private final MessageDistributor messageDistributor; + + private final NotifyInfoDelegateHandler notifyInfoDelegateHandler; + + /** + * 公告关闭事件监听 + * + * @param event the AnnouncementCloseEvent + */ + @Async + @EventListener(AnnouncementCloseEvent.class) + public void onAnnouncementCloseEvent(AnnouncementCloseEvent event) { + // 构建公告关闭的消息体 + AnnouncementCloseMessage message = new AnnouncementCloseMessage(); + message.setId(event.getId()); + String msg = JsonUtils.toJson(message); + + // 广播公告关闭信息 + MessageDO messageDO = new MessageDO().setMessageText(msg).setNeedBroadcast(true); + messageDistributor.distribute(messageDO); + } + + /** + * 站内通知推送事件 + * + * @param event the StationNotifyPushEvent + */ + @Async + @EventListener(StationNotifyPushEvent.class) + public void onAnnouncementPublishEvent(StationNotifyPushEvent event) { + log.info("============ onAnnouncementPublishEvent time {} ============", LocalTime.now()); + NotifyInfo notifyInfo = event.getNotifyInfo(); + List userList = event.getUserIdList(); + notifyInfoDelegateHandler.handle(userList, notifyInfo); + } + + + + + /** + * 公告关闭事件监听 + * + * @param event the AnnouncementCloseEvent + */ + @Async + @EventListener(AnnouncementCloseEvent.class) + public void read(AnnouncementCloseEvent event) { + // 构建公告关闭的消息体 + AnnouncementCloseMessage message = new AnnouncementCloseMessage(); + message.setId(event.getId()); + String msg = JsonUtils.toJson(message); + + // 广播公告关闭信息 + MessageDO messageDO = new MessageDO().setMessageText(msg).setNeedBroadcast(true); + messageDistributor.distribute(messageDO); + } +} diff --git a/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/listener/SystemWebsocketEventListener.java b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/listener/SystemWebsocketEventListener.java new file mode 100644 index 0000000..3bd2718 --- /dev/null +++ b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/listener/SystemWebsocketEventListener.java @@ -0,0 +1,34 @@ +package com.baiye.listener; + +import com.baiye.util.JsonUtils; +import com.baiye.distribute.MessageDO; +import com.baiye.distribute.MessageDistributor; +import com.baiye.system.event.DictChangeEvent; +import com.baiye.message.DictChangeMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; + +@RequiredArgsConstructor +public class SystemWebsocketEventListener { + + private final MessageDistributor messageDistributor; + + /** + * 字典修改事件监听 + * @param event the `DictChangeEvent` + */ + @Async + @EventListener(DictChangeEvent.class) + public void onDictChangeEvent(DictChangeEvent event) { + // 构建字典修改的消息体 + DictChangeMessage dictChangeMessage = new DictChangeMessage(); + dictChangeMessage.setDictCode(event.getDictCode()); + String msg = JsonUtils.toJson(dictChangeMessage); + + // 广播修改信息 + MessageDO messageDO = new MessageDO().setMessageText(msg).setNeedBroadcast(true); + messageDistributor.distribute(messageDO); + } + +} diff --git a/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/message/AnnouncementCloseMessage.java b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/message/AnnouncementCloseMessage.java new file mode 100644 index 0000000..3381100 --- /dev/null +++ b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/message/AnnouncementCloseMessage.java @@ -0,0 +1,23 @@ +package com.baiye.message; + +import lombok.Getter; +import lombok.Setter; + +/** + * @author Hccake 2021/1/7 + * @version 1.0 + */ +@Getter +@Setter +public class AnnouncementCloseMessage extends JsonWebSocketMessage { + + public AnnouncementCloseMessage() { + super("announcement-close"); + } + + /** + * ID + */ + private Long id; + +} diff --git a/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/message/DictChangeMessage.java b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/message/DictChangeMessage.java new file mode 100644 index 0000000..3f4ba64 --- /dev/null +++ b/marketing-scrm-admin/admin-websocket/src/main/java/com/baiye/message/DictChangeMessage.java @@ -0,0 +1,25 @@ +package com.baiye.message; + +import lombok.Getter; +import lombok.Setter; + +/** + * 字典修改消息 + * + * @author Hccake 2021/1/5 + * @version 1.0 + */ +@Getter +@Setter +public class DictChangeMessage extends JsonWebSocketMessage { + + public DictChangeMessage() { + super("dict-change"); + } + + /** + * 改动的字典标识 + */ + private String dictCode; + +} diff --git a/marketing-scrm-admin/admin-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/marketing-scrm-admin/admin-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..385abad --- /dev/null +++ b/marketing-scrm-admin/admin-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.baiye.AdminWebSocketAutoConfiguration \ No newline at end of file diff --git a/marketing-scrm-admin/pom.xml b/marketing-scrm-admin/pom.xml new file mode 100644 index 0000000..46e7497 --- /dev/null +++ b/marketing-scrm-admin/pom.xml @@ -0,0 +1,17 @@ + + + + com.baiye + marketing-scrm + ${revision} + + 4.0.0 + marketing-scrm-admin + pom + + + admin-core + admin-websocket + + diff --git a/marketing-scrm-common/common-core/pom.xml b/marketing-scrm-common/common-core/pom.xml new file mode 100644 index 0000000..759c894 --- /dev/null +++ b/marketing-scrm-common/common-core/pom.xml @@ -0,0 +1,85 @@ + + + + marketing-scrm-common + com.baiye + ${revision} + + 4.0.0 + common-core + + + + + cn.hutool + hutool-all + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.baiye + common-model + ${revision} + + + com.baiye + common-util + ${revision} + + + jakarta.servlet + jakarta.servlet-api + compile + + + org.glassfish + jakarta.el + 3.0.3 + test + + + + org.slf4j + slf4j-api + + + org.springframework + spring-context + + + + org.springframework + spring-web + + + + com.google.guava + guava + 23.0 + + + + + org.springframework.boot + spring-boot-starter-data-redis + 2.3.2.RELEASE + + + + net.lingala.zip4j + zip4j + + + + + + + diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/compose/ContextComponent.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/compose/ContextComponent.java new file mode 100644 index 0000000..ba16307 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/compose/ContextComponent.java @@ -0,0 +1,26 @@ +package com.baiye.compose; + +/** + * 上下文组件, 在接入对应的上下文时(如: spring 的 bean) 便于在 开始和结束时执行对应的方法 + *

+ * 默认自动接入spring + *

+ *

+ * 一般用于线程类实例达成接入到对应的上下文环境时自动开启和结束线程 + *

+ * + * @author lingting 2022/10/15 17:55 + */ +public interface ContextComponent { + + /** + * 上下文准备好之后调用, 内部做一些线程的初始化以及线程启动 + */ + void onApplicationStart(); + + /** + * 在上下文销毁前调用, 内部做线程停止和数据缓存相关 + */ + void onApplicationStop(); + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/config/RedisCacheConfig.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/config/RedisCacheConfig.java new file mode 100644 index 0000000..2692869 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/config/RedisCacheConfig.java @@ -0,0 +1,46 @@ +package com.baiye.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + + +/** + * redis 缓存配置 + */ +@Slf4j +@Configuration +public class RedisCacheConfig{ + //编写我们自己的RedisTemplate + // 自己定义了一个 RedisTemplate + @Bean + @SuppressWarnings("all") + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + // 我们为了自己开发方便,一般直接使用 + RedisTemplate template = new RedisTemplate(); + template.setConnectionFactory(factory); + // Json序列化配置 + Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); + ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); + // String 的序列化 + StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); + // key采用String的序列化方式 + template.setKeySerializer(stringRedisSerializer); + // hash的key也采用String的序列化方式 + template.setHashKeySerializer(stringRedisSerializer); + // value序列化方式采用jackson + template.setValueSerializer(jackson2JsonRedisSerializer); + // hash的value序列化方式采用jackson + template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); + return template; + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/DefaultNumberConstants.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/DefaultNumberConstants.java new file mode 100644 index 0000000..94d2c4b --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/DefaultNumberConstants.java @@ -0,0 +1,244 @@ +package com.baiye.constant; + +/** + * 数值常量定义 + * + * @author Enzo + * @date : 2021/6/17 + */ +public class DefaultNumberConstants { + + private DefaultNumberConstants() { + + } + + /** + * -3 + */ + public static final int MINUS_THREE_NUMBER = -3; + + /** + * + */ + public static final int MINUS_FIFTEEN_NUMBER = -15; + + /** + * -2 + */ + public static final int MINUS_TWO_NUMBER = -2; + + /** + * -1 + */ + public static final int MINUS_ONE_NUMBER = -1; + + /** + * 0 + */ + public static final int ZERO_NUMBER = 0; + + /** + * 1 + */ + public static final int ONE_NUMBER = 1; + + /** + * 2 + */ + public static final int TWO_NUMBER = 2; + + /** + * 3 + */ + public static final int THREE_NUMBER = 3; + + /** + * 4 + */ + public static final int FOUR_NUMBER = 4; + + /** + * 5 + */ + public static final int FIVE_NUMBER = 5; + + /** + * 6 + */ + public static final int SIX_NUMBER = 6; + + /** + * 7 + */ + public static final int SEVEN_NUMBER = 7; + + /** + * 8 + */ + public static final int EIGHT_NUMBER = 8; + + /** + * 9 + */ + public static final int NINE_NUMBER = 9; + + /** + * 10 + */ + public static final int TEN_NUMBER = 10; + + /** + * 11 + */ + public static final int ELEVEN_NUMBER = 11; + + /** + * 21 + */ + public static final int TWELVE_NUMBER = 12; + + /** + * 14 + */ + public static final int FOURTEEN_NUMBER = 14; + + /** + * 15 + */ + public static final int FIFTEEN_NUMBER = 15; + + /** + * 16 + */ + public static final int SIXTEEN_NUMBER = 16; + + /** + * 20 + */ + public static final int TWENTY_NUMBER = 20; + + public static final int TWENTY_ONE = 21; + + /** + * 23 + */ + public static final int TWENTY_THREE = 23; + + /** + * 24 + */ + public static final int TWENTY_FOUR = 24; + + /** + * 30 + */ + public static final int THIRTY = 30; + + public static final int FORTY_EIGHT = 48; + + /** + * 60 + */ + public static final int FIFTY = 50; + + /** + * 60 + */ + public static final int SIXTY = 60; + + /** + * 90 + */ + public static final int NINETY = 90; + + /** + * 98 + */ + public static final int NINETY_EIGHT = 98; + + /** + * 99 + */ + public static final int NINETY_NINE = 99; + + /** + * 100 + */ + public static final int ONE_HUNDRED = 100; + + /** + * 200 + */ + public static final int TWO_HUNDRED = 200; + + /** + * 500 + */ + public static final int FIVE_HUNDRED = 500; + + /** + * 201 + */ + public static final int TWO_HUNDRED_ONE = 201; + + /** + * 1005 + */ + public static final int ONE_THOUSAND_AND_FIVE = 1005; + + /** + * 1000 + */ + public static final int ONE_THOUSAND = 1000; + + /** + * 5000 + */ + public static final int FIVE_THOUSAND = 5000; + + /** + * 1万 + */ + public static final int TEN_THOUSAND = 10000; + + /** + * 五万 + */ + public static final int FIFTY_THOUSAND = 50000; + + /** + * 十万 + */ + public static final int ONE_HUNDRED_THOUSAND = 100000; + + /** + * 999999 + */ + public static final int LESS_THAN_ONE_MILLION = 999999; + + /** + * 一百万 + */ + public static final int ONE_MILLION = 1000000; + + /** + * 9999999 + */ + public static final int TEN_MILLION = 9999999; + + /** + * @ + */ + public static final String ATTR_DELIMITER = "@"; + + /** + * % + */ + public static final String NAT_DELIM = "%"; + + /** + * % + */ + public static final String DOLLAR_DELIM = "${"; + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/FileConstant.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/FileConstant.java new file mode 100644 index 0000000..72261a7 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/FileConstant.java @@ -0,0 +1,112 @@ +package com.baiye.constant; + +/** + * @author + * 文件相关 常量类 + */ +public class FileConstant { + + /** + * 0,(自定义模板) + */ + public static final int ZERO_NUMBER = 0; + + /** + * 1,(UC) + */ + public static final int ONE_NUMBER = 1; + + /** + * 2,(快手) + */ + public static final int TWO_NUMBER = 2; + + /** + * 3,(抖音) + */ + public static final int THREE_NUMBER = 3; + + /** + * 4,(百度) + */ + public static final int FOUR_NUMBER = 4; + + /** + * 5,(动态任务自定义模板) + */ + public static final int FIVE_NUMBER = 5; + + /** + * 6,(拓客动态任务) + */ + public static final int SIX_NUMBER = 6; + + /** + * 1000 + */ + public static final int ONE_THOUSAND_NUMBER = 1000; + + /** + * 1000000 + */ + public static final int ONE_MILLION_NUMBER = 1000000; + + /** + * BY + */ + public static final String BY = "BY"; + + /** + * MM + */ + public static final String MM = "MM"; + + /** + * uc + */ + public static final String UC = "uc"; + + /** + * 快手 + */ + public static final String KS = "ks"; + + /** + * 抖音 + */ + public static final String DY = "dy"; + + /** + * 其它 + */ + public static final String QT = "qt"; + + + /** + * 以 xlsx 结尾的文件 + */ + public static final String XLSX_FILE_SUB_NAME = "xlsx"; + + /** + * 以 xls 结尾的文件 + */ + public static final String XLS_FILE_SUB_NAME = "xls"; + + /** + * 以 txt 结尾的文件 + */ + public static final String TXT_FILE_SUB_NAME = "txt"; + + /** + * 以 csv 结尾的文件 + */ + public static final String CSV_FILE_SUB_NAME = "csv"; + + + /** + * 以 zip 结尾的文件 + */ + public static final String ZIP_FILE_SUB_NAME = ".zip"; + + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/GlobalConstants.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/GlobalConstants.java new file mode 100644 index 0000000..56de7ed --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/GlobalConstants.java @@ -0,0 +1,32 @@ +package com.baiye.constant; + +/** + * @author Hccake 2020/6/9 17:17 + */ +public final class GlobalConstants { + + private GlobalConstants() { + } + + /** + * 未被逻辑删除的标识,即有效数据标识 逻辑删除标识,普通情况下可以使用 1 标识删除,0 标识存活 但在有唯一索引的情况下,会导致索引冲突,所以用 0 标识存活, + * 已删除数据记录为删除时间戳 + */ + public static final Long NOT_DELETED_FLAG = 0L; + + /** + * 生产环境 + */ + public static final String ENV_PROD = "prod"; + + /** + * 树根节点ID + */ + public static final Integer TREE_ROOT_ID = 0; + + /** + * 树根节点ID + */ + public static final Long TREE_ROOT_ID_LONG = Long.valueOf(TREE_ROOT_ID); + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/HeaderConstants.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/HeaderConstants.java new file mode 100644 index 0000000..17c49d7 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/HeaderConstants.java @@ -0,0 +1,28 @@ +package com.baiye.constant; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/31 11:55 + */ +public final class HeaderConstants { + + private HeaderConstants() { + } + + /** + * 请求时间戳 + */ + public static final String REQ_TIME = "reqTime"; + + /** + * 请求sign + */ + public static final String SIGN = "sign"; + + /** + * SECRET_ID + */ + public static final String SECRET_ID = "secretId"; + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/HttpsConstants.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/HttpsConstants.java new file mode 100644 index 0000000..ae9f67f --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/HttpsConstants.java @@ -0,0 +1,100 @@ +package com.baiye.constant; + +import com.baiye.https.CompatibleSSLFactory; +import com.baiye.https.SSLSocketFactoryInitException; +import lombok.experimental.UtilityClass; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * @author lingting + */ +@UtilityClass +@SuppressWarnings("java:S4830") +public class HttpsConstants { + + public static final String SSL = "SSL"; + + public static final String SSL_V2 = "SSLv2"; + + public static final String SSL_V3 = "SSLv3"; + + public static final String TLS = "TLS"; + + public static final String TLS_V1 = "TLSv1"; + + public static final String TLS_V11 = "TLSv1.1"; + + public static final String TLS_V12 = "TLSv1.2"; + + public static final String DALVIK = "dalvik"; + + public static final String VM_NAME = "java.vm.name"; + + /** + * Android低版本不重置的话某些SSL访问就会失败 + */ + private static final String[] ANDROID_PROTOCOLS = { SSL_V3, TLS_V1, TLS_V11, TLS_V12 }; + + /** + * 默认信任全部的域名校验器 + */ + @SuppressWarnings("java:S5527") + public static final HostnameVerifier HOSTNAME_VERIFIER = (s, sslSession) -> true; + + public static final KeyManager[] KEY_MANAGERS = null; + + public static final X509TrustManager TRUST_MANAGER; + + public static final TrustManager[] TRUST_MANAGERS; + + static { + TRUST_MANAGER = new X509TrustManager() { + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + // + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + // + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + TRUST_MANAGERS = new TrustManager[] { TRUST_MANAGER }; + } + + /** + * 默认的SSLSocketFactory,区分安卓 + */ + public static final SSLSocketFactory SSF; + + static { + try { + if (DALVIK.equalsIgnoreCase(System.getProperty(VM_NAME))) { + // 兼容android低版本SSL连接 + SSF = new CompatibleSSLFactory(ANDROID_PROTOCOLS); + } + else { + SSF = new CompatibleSSLFactory(); + } + } + catch (KeyManagementException | NoSuchAlgorithmException e) { + throw new SSLSocketFactoryInitException("ssf 创建失败!", e); + } + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/MDCConstants.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/MDCConstants.java new file mode 100644 index 0000000..5fb56d4 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/MDCConstants.java @@ -0,0 +1,19 @@ +package com.baiye.constant; + +/** + * MDC 相关常量 + * + * @author hccake + * @since 1.3.1 + */ +public final class MDCConstants { + + private MDCConstants() { + } + + /** + * 跟踪ID,用于一次请求或执行方法时,产生的各种日志间的数据关联 + */ + public static final String TRACE_ID_KEY = "traceId"; + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/SecretConstant.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/SecretConstant.java new file mode 100644 index 0000000..ba2ed87 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/SecretConstant.java @@ -0,0 +1,27 @@ +package com.baiye.constant; + +/** + * 有关加解密的常量类 + */ +public class SecretConstant { + + /** + * yuyou address + */ + public static final String SECRET_DECRYPT_YY_URL = "https://bd.hzdaba.cn:8085/v3/Accounts/yuyou_bd/BigData/DecryptTel"; + + /** + * yuyou sign + */ + public static final String SECRET_DECRYPT_YY_SIG = "7f49e3f9c17c0f3c8fca84168161ed37"; + + /** + * yuyou id + */ + public static final String SECRET_DECRYPT_YY_ID = "yuyou_bd"; + + /** + * 时间戳格式 + */ + public static final String SECRET_DECRYPT_TIME_FORMATE = "yyyyMMddHHmmss"; +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/WechatCallbackConstant.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/WechatCallbackConstant.java new file mode 100644 index 0000000..6948434 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/WechatCallbackConstant.java @@ -0,0 +1,46 @@ +package com.baiye.constant; + +import lombok.Getter; + +/** + * @author Enzo + * @date : 2024/4/1 + */ + +@Getter +public class WechatCallbackConstant { + private WechatCallbackConstant() { + + } + + /** + * 扫码返回 + */ + public static final int SCAN_CODE = 222; + + + /** + * 登录成功 + */ + public static final int LOGIN_SUCCESS = 201; + + + /** + * 登录失败 + */ + public static final int LOGIN_FAIL = 212; + + + /** + * 退出登录 + */ + public static final int LOGIN_OUT = 210; + + + /** + * 通过好友 + */ + public static final int THROUGH_FRIENDS = 1003; + + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/enums/BooleanEnum.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/enums/BooleanEnum.java new file mode 100644 index 0000000..b1619e2 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/enums/BooleanEnum.java @@ -0,0 +1,35 @@ +package com.baiye.constant.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Boolean 类型常量 + * + * @author Hccake 2020/4/6 21:52 + */ +@AllArgsConstructor +public enum BooleanEnum { + + /** + * 是 + */ + TRUE(true, 1), + /** + * 否 + */ + FALSE(false, 0); + + private final Boolean booleanValue; + + private final Integer intValue; + + public Boolean booleanValue() { + return booleanValue; + } + + public Integer intValue() { + return intValue; + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/enums/ImportModeEnum.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/enums/ImportModeEnum.java new file mode 100644 index 0000000..17b3cbb --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/enums/ImportModeEnum.java @@ -0,0 +1,25 @@ +package com.baiye.constant.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 当数据以存在时的导入动作 + * + * @author Hccake + */ +@Getter +@AllArgsConstructor +public enum ImportModeEnum { + + /** + * 跳过已存在的数据 + */ + SKIP_EXISTING, + + /** + * 覆盖已存在的数据 + */ + OVERWRITE_EXISTING; + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/enums/ResponseCode.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/enums/ResponseCode.java new file mode 100644 index 0000000..93bd4cd --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/constant/enums/ResponseCode.java @@ -0,0 +1,225 @@ +package com.baiye.constant.enums; + +import lombok.Getter; + +/** + * @author q + */ +@Getter +public enum ResponseCode { + + /** + * 成功 + */ + SUCCESS("1", "success"), + + /** + * 失败 + */ + FAILURE("0", "failure "), + + /** + * 充值成功 + */ + RECHARGE_SUCCESS("1020", "充值成功"), + + /** + * 手机号码不正确 + */ + PHONE_NUMBER_IS_INCORRECT("1022", "手机号码不正确"), + + /** + * 验证码错误 + */ + VERIFICATION_CODE_PARAMETER_ERROR("1023", "验证码错误"), + + /** + * 验证码不存在或已过期 + */ + VERIFICATION_NOT_EXIST_HAS_EXPIRED("1024", "验证码不存在或已过期"), + + /** + * 读取文件失败 + */ + READ_FILE_ERROR("1021", "读取文件失败"), + /** + * 参数错误 + */ + PARAMETER_ERROR("1001", "参数错误"), + + /** + * 用户信息错误 + */ + USER_INFORMATION_ERROR("1002", "用户信息错误"), + + /** + * 账户已被禁用 + */ + ACCOUNT_DISABLED("1025", "账户已被禁用"), + + /** + * 套餐不存在或已过期 + */ + PACKAGE_ERROR("1026", "套餐不存在或已过期"), + + /** + * 支付宝支付失败 + */ + ALI_PAY_ERROR("1016", "创建订单失败"), + + /** + * 支付宝支付回调失败 + */ + CALLBACK_FAILED("1017", "支付宝支付回调失败"), + + /** + * 账号已过期 + */ + ACCOUNT_EXPIRED("1020", "账号已过期,无法激活"), + /** + * 呼叫失败 + */ + CALL_ERROR("1014", "呼叫失败"), + + /** + * axb参数配置 + */ + AXB_CONFIGURATION_ERROR("1015", "axb参数配置错误"), + + + /** + * 数据解析失败 + */ + DECRYPTION_FAILED("1012", "数据解析失败"), + + /** + * 比率不足100%,无法分配 + */ + RATIO_FAILED("1013", "比率不足100%,无法分配"), + + + /** + * axb参数配置 + */ + WRONG_USER_NAME_PASSWORD("1016", "用户名或密码不正确"), + + /** + * 账户余额不足 + */ + INSUFFICIENT_ACCOUNT_BALANCE("1019", "账户余额不足"), + + /** + * 连接成功 + */ + CONNECTION_SUCCEEDED("1027", "连接成功"), + + /** + * 获取二维码失败 + */ + FAILED_GET_QR_CODE("1028", "获取二维码失败"), + + /** + * 获取模板失败 + */ + FAILED_GET_TEMPLATE("1029", "获取模板失败,请先配置发送模板"), + + /** + * 发送消息至少配置一条 + */ + TEMPLATE_CHANGE_ERROR("1031", "发送消息至少配置一条!"), + + /** + * 添加好友失败 + */ + FAILED_ADD_FRIEND("1032", "添加好友失败!"), + + /** + * 当前账户未登录或不在线 + */ + ACCOUNT_NOT_LOGGED_OR_NOT_ONLINE("1030", "当前账户未登录或不在线"), + + /** + * 修改套餐失败 + */ + UPDATE_COMBO_FRIEND("1033", "修改套餐失败!"), + + /** + * 发送邮件失败 + */ + SEND_MAIL_FRIEND("1034", "发送邮件失败!"), + + /** + * 发送邮件失败 + */ + SEND_MAIL_NUM_FRIEND("1035", "发送数量不足100!"), + + /** + * 用户被禁11005用 + */ + USER_DISABLED("11004", "用户被禁用"), + /** + * 用户被锁定 + */ + USER_LOCKED("11005", "用户被锁定"), + /** + * 用户名或密码错误 + */ + USER_PASSWORD_ERROR("11006", "用户名或密码错误"), + /** + * 用户密码过期 + */ + USER_PASSWORD_EXPIRED("11007", "用户密码过期"), + /** + * 用户账号过期 + */ + USER_ACCOUNT_EXPIRED("11008", "用户账号已过期"), + /** + * 没有该用户 + */ + USER_NOT_EXIST("11009", "没有该用户"), + /** + * 用户登录失败 + */ + USER_LOGIN_FAIL("11010", "用户登录失败"), + /** + * 验证码错误 + */ + VERIFY_CODE_ERROR("11011", "验证码错误"), + + /** + * 修改用户信息失败 + */ + UPDATE_USER_ERROR("11012", "修改用户信息失败"), + + /** + * 兑换数据失败 + */ + DATA_CONVERSION_FAILURE("11013", "兑换数据失败,请再次重试"), + + /** + * 发送邮件失败 + */ + SEND_MAIL_MAX_NUM_FRIEND("1036", "发送数量超出最大范围!"), + + /** + * 该批次号已经存在 + */ + BATCH_NUMBER_ALREADY_EXISTS("1037", "该批次号已经存在!"), + + /** + * 非超级管理员不能修改数据 + */ + NON_SUPER_ADMINISTRATORS_CANNOT_MODIFY_DATA("1018", "非管理员不能修改数据"); + + + private final String code; + + private final String desc; + + ResponseCode(String code, String desc) { + this.code = code; + this.desc = desc; + } + + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/enums/ChannelTypeEnum.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/enums/ChannelTypeEnum.java new file mode 100644 index 0000000..bea881d --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/enums/ChannelTypeEnum.java @@ -0,0 +1,50 @@ +package com.baiye.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Enzo + * @date 2024-4-1 + */ +@Getter +@AllArgsConstructor +public enum ChannelTypeEnum { + /** + * 手动创建 + */ + MANUAL_CREATE(1, "手动创建"), + /** + * 文件上传 + */ + FILE_UPLOAD(2, "文件上传"), + + /** + * 飞鱼回传 + */ + FEI_YU(3, "飞鱼回传"), + + /** + * 外呼接入 + */ + OUT_BOUND(4, "外呼接入"), + + /** + * api回传 + */ + API(5, "api回传"), + ; + + + private final Integer key; + private final String name; + + public static String find(Integer key) { + for (ChannelTypeEnum channelTypeEnum : ChannelTypeEnum.values()) { + if (channelTypeEnum.key.equals(key)) { + return channelTypeEnum.name; + } + } + return null; + } +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/enums/PackageEnum.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/enums/PackageEnum.java new file mode 100644 index 0000000..acc004a --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/enums/PackageEnum.java @@ -0,0 +1,43 @@ +package com.baiye.enums; + +import com.baiye.constant.DefaultNumberConstants; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * @author Enzo + * @date : 2024/4/1 + */ +@Getter +@AllArgsConstructor +public enum PackageEnum { + /** + * 月度 + */ + MONTH(1, 3), + + /** + * 季度 + */ + QUARTER(2, 90), + + /** + * 年度 + */ + YEAR(3, 365); + + + private final Integer type; + private final Integer num; + + public static Integer find(Integer val) { + for (PackageEnum packageEnum : PackageEnum.values()) { + if (Objects.equals(packageEnum.getType(), val)) { + return packageEnum.getNum(); + } + } + return DefaultNumberConstants.MINUS_ONE_NUMBER; + } +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/enums/SalesManEnum.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/enums/SalesManEnum.java new file mode 100644 index 0000000..feb1b4b --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/enums/SalesManEnum.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.baiye.enums; + +import com.baiye.constant.DefaultNumberConstants; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + *

+ * 验证码业务场景对应的 Redis 中的 key + *

+ * + * @author Zheng Jie + * @date 2020-05-02 + */ +@Getter +@AllArgsConstructor +public enum SalesManEnum { + + /** + * 录入员 + */ + ENTRY_CLERK(19L, 1), + + /** + * 初审员 + */ + FIRST_INSTANCE(20L, 2), + + /** + * 业务员 + */ + SALESMAN(23L, 4), + + /** + * 业务主管 + */ + EXECUTIVE(22L, 0), + + /** + * 复审员 + */ + RETRIAL(21L, 3); + + + private final Long id; + private final Integer type; + + + + public static Integer find(Long val) { + for (SalesManEnum salesManEnum : SalesManEnum.values()) { + if (Objects.equals(salesManEnum.getId(), val)) { + return salesManEnum.getType(); + } + } + return DefaultNumberConstants.MINUS_ONE_NUMBER; + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/enums/StatisticsEnum.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/enums/StatisticsEnum.java new file mode 100644 index 0000000..2c6fcb5 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/enums/StatisticsEnum.java @@ -0,0 +1,48 @@ +package com.baiye.enums; + +import com.baiye.constant.DefaultNumberConstants; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * @author Enzo + * @date : 2024/4/3 + */ +@Getter +@AllArgsConstructor +public enum StatisticsEnum { + + /** + * 当天 + */ + TODAY(1, 0), + + /** + * 七天 + */ + SEVEN_DAYS(2, 7), + + /** + * 三十日 + */ + THIRTY_DAYS(3, 30); + + + + private final Integer type; + + private final Integer num; + + + public static Integer find(Integer val) { + for (StatisticsEnum salesManEnum : StatisticsEnum.values()) { + if (Objects.equals(salesManEnum.getType(), val)) { + return salesManEnum.getNum(); + } + } + return DefaultNumberConstants.MINUS_ONE_NUMBER; + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/BadRequestException.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/BadRequestException.java new file mode 100644 index 0000000..cdd4b20 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/BadRequestException.java @@ -0,0 +1,25 @@ +package com.baiye.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +/** + * 统一异常处理 + */ +@Getter +public class BadRequestException extends RuntimeException { + + private Integer status = BAD_REQUEST.value(); + + public BadRequestException(String msg) { + super(msg); + } + + public BadRequestException(HttpStatus status, String msg) { + super(msg); + this.status = status.value(); + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/BusinessException.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/BusinessException.java new file mode 100644 index 0000000..629d6b2 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/BusinessException.java @@ -0,0 +1,57 @@ +package com.baiye.exception; + +import cn.hutool.core.text.CharSequenceUtil; +import com.baiye.result.ResultCode; +import lombok.Getter; + +/** + * 通用业务异常 + * + * @author Hccake + */ +@Getter +public class BusinessException extends RuntimeException { + + private final String message; + + private final int code; + + public BusinessException(ResultCode resultCode) { + super(resultCode.getMessage()); + this.code = resultCode.getCode(); + this.message = resultCode.getMessage(); + } + + /** + * 用于需要format返回结果的异常 + */ + public BusinessException(ResultCode resultCode, Object... args) { + this(resultCode.getCode(), CharSequenceUtil.format(resultCode.getMessage(), args)); + } + + public BusinessException(ResultCode resultCode, Throwable e) { + super(resultCode.getMessage(), e); + this.code = resultCode.getCode(); + this.message = resultCode.getMessage(); + } + + /** + * 用于需要format返回结果的异常 + */ + public BusinessException(ResultCode resultCode, Throwable e, Object... args) { + this(resultCode.getCode(), CharSequenceUtil.format(resultCode.getMessage(), args), e); + } + + public BusinessException(int code, String message) { + super(message); + this.message = message; + this.code = code; + } + + public BusinessException(int code, String message, Throwable e) { + super(message, e); + this.message = message; + this.code = code; + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/SqlCheckedException.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/SqlCheckedException.java new file mode 100644 index 0000000..238ef9d --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/SqlCheckedException.java @@ -0,0 +1,30 @@ +package com.baiye.exception; + +import com.baiye.result.SystemResultCode; + +/** + * sql防注入校验异常 + * + * @author Hccake + * @version 1.0 + * @date 2019/10/19 16:52 + */ +public class SqlCheckedException extends BusinessException { + + public SqlCheckedException(SystemResultCode systemResultMsg) { + super(systemResultMsg); + } + + public SqlCheckedException(SystemResultCode systemResultMsg, Throwable e) { + super(systemResultMsg, e); + } + + public SqlCheckedException(int code, String msg) { + super(code, msg); + } + + public SqlCheckedException(int code, String msg, Throwable e) { + super(code, msg, e); + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/handler/GlobalException.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/handler/GlobalException.java new file mode 100644 index 0000000..f2e8b20 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/handler/GlobalException.java @@ -0,0 +1,58 @@ +package com.baiye.exception.handler; + +import com.baiye.exception.BadRequestException; +import com.baiye.result.R; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Objects; + +/** + * @Author YQY + * @Date 2023/8/4 + */ +@Slf4j +@RestControllerAdvice +public class GlobalException { + + /** + * 处理自定义异常 + */ + @ExceptionHandler(value = BadRequestException.class) + public R badRequestException(BadRequestException e) { + log.error(getStackTrace(e)); + return R.failed(e.getStatus(), e.getMessage()); + } + + /** + * 接口数据验证异常(@Validated验证) + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public R handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + String[] str = Objects.requireNonNull(e.getBindingResult().getAllErrors().get(0).getCodes())[1].split("\\."); + String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); + String msg = "不能为空"; + if (msg.equals(message)) { + message = str[1] + ":" + message; + } + log.error(message); + return R.failed(HttpStatus.BAD_REQUEST.value(), message); + } + + /** + * 获取堆栈信息 + */ + public static String getStackTrace(Throwable throwable) { + StringWriter sw = new StringWriter(); + try (PrintWriter pw = new PrintWriter(sw)) { + throwable.printStackTrace(pw); + return sw.toString(); + } + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/handler/GlobalExceptionHandler.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..0b064fc --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,18 @@ +package com.baiye.exception.handler; + +/** + * 异常日志处理类 + * + * @author Hccake + * @version 1.0 + * @date 2019/10/18 17:05 + */ +public interface GlobalExceptionHandler { + + /** + * 在此处理错误信息 进行落库,入ES, 发送报警通知等信息 + * @param throwable 异常 + */ + void handle(Throwable throwable); + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/https/CompatibleSSLFactory.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/https/CompatibleSSLFactory.java new file mode 100644 index 0000000..2e8bf1a --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/https/CompatibleSSLFactory.java @@ -0,0 +1,95 @@ +package com.baiye.https; + +import com.baiye.constant.HttpsConstants; +import com.baiye.util.ArrayUtils; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +/** + * 用于兼容 android 使用 + * + * @author lingting + */ +public class CompatibleSSLFactory extends SSLSocketFactory { + + private final String[] protocols; + + private final SSLSocketFactory factory; + + public CompatibleSSLFactory(String... protocols) throws NoSuchAlgorithmException, KeyManagementException { + this(HttpsConstants.TLS, HttpsConstants.KEY_MANAGERS, HttpsConstants.TRUST_MANAGERS, new SecureRandom(), + protocols); + } + + public CompatibleSSLFactory(String protocol, KeyManager[] keyManagers, TrustManager[] trustManagers, + SecureRandom secureRandom, String... protocols) throws NoSuchAlgorithmException, KeyManagementException { + this.protocols = protocols; + final SSLContext context = SSLContext.getInstance(protocol); + context.init(keyManagers, trustManagers, secureRandom); + this.factory = context.getSocketFactory(); + } + + @Override + public String[] getDefaultCipherSuites() { + return factory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return factory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + return enabledProtocols(factory.createSocket()); + } + + @Override + public Socket createSocket(Socket socket, InputStream inputStream, boolean b) throws IOException { + return enabledProtocols(factory.createSocket(socket, inputStream, b)); + } + + @Override + public Socket createSocket(Socket socket, String s, int i, boolean b) throws IOException { + return enabledProtocols(factory.createSocket(socket, s, i, b)); + } + + @Override + public Socket createSocket(String s, int i) throws IOException { + return enabledProtocols(factory.createSocket(s, i)); + } + + @Override + public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws IOException { + return enabledProtocols(factory.createSocket(s, i, inetAddress, i1)); + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i) throws IOException { + return enabledProtocols(factory.createSocket(inetAddress, i)); + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) throws IOException { + return enabledProtocols(factory.createSocket(inetAddress, i, inetAddress1, i1)); + } + + private Socket enabledProtocols(Socket socket) { + if (!ArrayUtils.isEmpty(protocols) && socket instanceof SSLSocket) { + ((SSLSocket) socket).setEnabledProtocols(protocols); + } + return socket; + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/https/SSLSocketFactoryInitException.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/https/SSLSocketFactoryInitException.java new file mode 100644 index 0000000..bc728d0 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/https/SSLSocketFactoryInitException.java @@ -0,0 +1,12 @@ +package com.baiye.https; + +/** + * @author lingting 2023/2/1 14:29 + */ +public class SSLSocketFactoryInitException extends RuntimeException { + + public SSLSocketFactoryInitException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/CustomJavaTimeModule.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/CustomJavaTimeModule.java new file mode 100644 index 0000000..e94da30 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/CustomJavaTimeModule.java @@ -0,0 +1,45 @@ +package com.baiye.jackson; + +import cn.hutool.core.date.DatePattern; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.PackageVersion; +import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +/** + * 自定义java8新增时间类型的序列化 + * + * @see com.fasterxml.jackson.datatype.jsr310.JavaTimeModule + * @author Hccake + */ +public class CustomJavaTimeModule extends SimpleModule { + + public CustomJavaTimeModule() { + super(PackageVersion.VERSION); + + this.addSerializer(LocalDateTime.class, + new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))); + this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE)); + this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME)); + this.addSerializer(Instant.class, InstantSerializer.INSTANCE); + + this.addDeserializer(LocalDateTime.class, + new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))); + this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE)); + this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ISO_LOCAL_TIME)); + this.addDeserializer(Instant.class, InstantDeserializer.INSTANT); + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/NullArrayJsonSerializer.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/NullArrayJsonSerializer.java new file mode 100644 index 0000000..debe7db --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/NullArrayJsonSerializer.java @@ -0,0 +1,32 @@ +package com.baiye.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.io.Serializable; + +/** + * 空数组序列化处理器 如果 Array 为 null,则序列化为 [] + * + * @author Hccake + * @version 1.0 + * @date 2019/10/17 23:17 + */ +public class NullArrayJsonSerializer extends JsonSerializer implements Serializable { + + private static final long serialVersionUID = 1L; + + @Override + public void serialize(Object value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { + if (value == null) { + jsonGenerator.writeStartArray(); + jsonGenerator.writeEndArray(); + } + else { + jsonGenerator.writeObject(value); + } + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/NullMapJsonSerializer.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/NullMapJsonSerializer.java new file mode 100644 index 0000000..8f885d0 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/NullMapJsonSerializer.java @@ -0,0 +1,32 @@ +package com.baiye.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.io.Serializable; + +/** + * 空 Map 序列化处理器 Map 为 null,则序列化为 {} + * + * @author Hccake + * @version 1.0 + * @date 2019/10/17 23:17 + */ +public class NullMapJsonSerializer extends JsonSerializer implements Serializable { + + private static final long serialVersionUID = 1L; + + @Override + public void serialize(Object value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { + if (value == null) { + jsonGenerator.writeStartObject(); + jsonGenerator.writeEndObject(); + } + else { + jsonGenerator.writeObject(value); + } + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/NullSerializerProvider.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/NullSerializerProvider.java new file mode 100644 index 0000000..4f3361d --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/NullSerializerProvider.java @@ -0,0 +1,120 @@ +package com.baiye.jackson; + +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.DefaultSerializerProvider; +import com.fasterxml.jackson.databind.ser.SerializerFactory; + +import java.util.Collection; +import java.util.Map; + +/** + *

+ * 修改了 Null 值的序列化器提供者 + *

+ * + *
    + *
  • String 类型,null 值转为 '' 输出 + *
  • 集合、数组,null 值转为 [] 输出 + *
  • Map 类型,null 值转为 {} 输出 + *
      + * + * @author hccake + */ +public class NullSerializerProvider extends DefaultSerializerProvider { + + private static final long serialVersionUID = 1L; + + public NullSerializerProvider() { + super(); + } + + public NullSerializerProvider(NullSerializerProvider src) { + super(src); + } + + protected NullSerializerProvider(SerializerProvider src, SerializationConfig config, SerializerFactory f) { + super(src, config, f); + } + + /** + * null array 或 list,set 则转 '[]' + */ + private final JsonSerializer nullArrayJsonSerializer = new NullArrayJsonSerializer(); + + /** + * null Map 转 '{}' + */ + private final JsonSerializer nullMapJsonSerializer = new NullMapJsonSerializer(); + + /** + * null 字符串转 '' + */ + private final JsonSerializer nullStringJsonSerializer = new NullStringJsonSerializer(); + + @Override + public DefaultSerializerProvider copy() { + if (getClass() != NullSerializerProvider.class) { + return super.copy(); + } + return new NullSerializerProvider(this); + } + + @Override + public NullSerializerProvider createInstance(SerializationConfig config, SerializerFactory jsf) { + return new NullSerializerProvider(this, config, jsf); + } + + @Override + public JsonSerializer findNullValueSerializer(BeanProperty property) throws JsonMappingException { + JavaType propertyType = property.getType(); + if (isStringType(propertyType)) { + return this.nullStringJsonSerializer; + } + else if (isArrayOrCollectionTrype(propertyType)) { + return this.nullArrayJsonSerializer; + } + else if (isMapType(propertyType)) { + return this.nullMapJsonSerializer; + } + else { + return super.findNullValueSerializer(property); + } + } + + /** + * 是否是 String 类型 + * @param type JavaType + * @return boolean + */ + private boolean isStringType(JavaType type) { + Class clazz = type.getRawClass(); + return String.class.isAssignableFrom(clazz); + } + + /** + * 是否是Map类型 + * @param type JavaType + * @return boolean + */ + private boolean isMapType(JavaType type) { + Class clazz = type.getRawClass(); + return Map.class.isAssignableFrom(clazz); + } + + /** + * 是否是集合类型或者数组 + * @param type JavaType + * @return boolean + */ + private boolean isArrayOrCollectionTrype(JavaType type) { + Class clazz = type.getRawClass(); + return clazz.isArray() || Collection.class.isAssignableFrom(clazz); + + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/NullStringJsonSerializer.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/NullStringJsonSerializer.java new file mode 100644 index 0000000..200ab9e --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/jackson/NullStringJsonSerializer.java @@ -0,0 +1,26 @@ +package com.baiye.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.io.Serializable; + +/** + * jackson NULL值序列化为 "" + * + * @author Hccake + * @version 1.0 + * @date 2019/10/17 22:19 + */ +public class NullStringJsonSerializer extends JsonSerializer implements Serializable { + + private static final long serialVersionUID = 1L; + + @Override + public void serialize(Object value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { + jsonGenerator.writeString(""); + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/lock/JavaReentrantLock.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/lock/JavaReentrantLock.java new file mode 100644 index 0000000..c94dfe1 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/lock/JavaReentrantLock.java @@ -0,0 +1,91 @@ +package com.baiye.lock; + +import lombok.Getter; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @author lingting 2023-04-22 10:55 + */ +@Getter +public class JavaReentrantLock { + + /** + * 锁 + */ + protected final ReentrantLock lock = new ReentrantLock(); + + /** + * 激活与休眠线程 + */ + protected final Condition defaultCondition = lock.newCondition(); + + public Condition newCondition() { + return getLock().newCondition(); + } + + public void lock() { + getLock().lock(); + } + + public void lockInterruptibly() throws InterruptedException { + getLock().lockInterruptibly(); + } + + public void runLock(Runnable runnable) throws InterruptedException { + ReentrantLock reentrantLock = getLock(); + reentrantLock.lock(); + try { + runnable.run(); + } + finally { + reentrantLock.unlock(); + } + } + + public void runLockInterruptibly(Runnable runnable) throws InterruptedException { + ReentrantLock reentrantLock = getLock(); + reentrantLock.lockInterruptibly(); + try { + runnable.run(); + } + finally { + reentrantLock.unlock(); + } + } + + public void unlock() { + getLock().unlock(); + } + + public void signal() { + getDefaultCondition().signal(); + } + + public void signalAll() { + getDefaultCondition().signalAll(); + } + + public void await() throws InterruptedException { + getDefaultCondition().await(); + } + + /** + * @return 是否被唤醒 + */ + public boolean await(long time, TimeUnit timeUnit) throws InterruptedException { + return getDefaultCondition().await(time, timeUnit); + } + + /** + * @author lingting 2023-04-22 11:35 + */ + public interface Runnable { + + void run() throws InterruptedException; + + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/markdown/MarkdownBuilder.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/markdown/MarkdownBuilder.java new file mode 100644 index 0000000..9dba467 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/markdown/MarkdownBuilder.java @@ -0,0 +1,282 @@ +package com.baiye.markdown; + +import cn.hutool.core.convert.Convert; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.baiye.util.json.JacksonJsonToolAdapter; +import lombok.SneakyThrows; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * 生成 markdown 文本 + * + * @author lingting 2020/6/10 22:43 + */ +public class MarkdownBuilder { + + public static final String TITLE_PREFIX = "#"; + + public static final String QUOTE_PREFIX = "> "; + + public static final String CODE_PREFIX = "``` "; + + public static final String CODE_SUFFIX = "```"; + + public static final String BOLD_PREFIX = "**"; + + public static final String ITALIC_PREFIX = "*"; + + public static final String UNORDERED_LIST_PREFIX = "- "; + + public static final String ORDER_LIST_PREFIX = ". "; + + /** + * 存放内容 + */ + private final List content = new ArrayList<>(); + + /** + * 当前操作行文本 + */ + private StringBuilder lineTextBuilder; + + public MarkdownBuilder() { + this.lineTextBuilder = new StringBuilder(); + } + + /** + * 添加自定义内容 + * @param content 自定义内容 + */ + public MarkdownBuilder append(Object content) { + lineTextBuilder.append(toString(content)); + return this; + } + + /** + * 有序列表 自动生成 索引 + * @param content 文本 + */ + public MarkdownBuilder orderList(Object content) { + // 获取最后一个字符串 + String tmp = ""; + if (!this.content.isEmpty()) { + tmp = this.content.get(this.content.size() - 1); + } + // 索引 + int index = 1; + + // 校验 是否 为有序列表行的正则 + String isOrderListPattern = "^\\d\\. .*"; + if (Pattern.matches(isOrderListPattern, tmp)) { + // 如果是数字开头 + index = Convert.toInt(tmp.substring(0, tmp.indexOf(ORDER_LIST_PREFIX) - 1)); + } + return orderList(index, content); + } + + /** + * 有序列表 + * @param index 索引 + * @param content 文本 + */ + public MarkdownBuilder orderList(int index, Object content) { + lineBreak(); + lineTextBuilder.append(index).append(ORDER_LIST_PREFIX).append(toString(content)); + return this; + } + + /** + * 无序列表 - item1 - item2 + */ + public MarkdownBuilder unorderedList(Object content) { + // 换行 + lineBreak(); + lineTextBuilder.append(UNORDERED_LIST_PREFIX).append(toString(content)); + return this; + } + + /** + * 图片 + * @param url 图片链接 + */ + public MarkdownBuilder pic(String url) { + return pic("", url); + } + + /** + * 图片 + * @param title 图片标题 + * @param url 图片路径 + */ + public MarkdownBuilder pic(Object title, String url) { + lineTextBuilder.append("![").append(title).append("](").append(url).append(")"); + return this; + } + + /** + * 链接 + * @param title 标题 + * @param url http 路径 + */ + public MarkdownBuilder link(Object title, String url) { + lineTextBuilder.append("[").append(title).append("](").append(url).append(")"); + return this; + } + + /** + * 斜体 + */ + public MarkdownBuilder italic(Object content) { + lineTextBuilder.append(ITALIC_PREFIX).append(toString(content)).append(ITALIC_PREFIX); + return this; + } + + /** + * 加粗 + */ + public MarkdownBuilder bold(Object content) { + lineTextBuilder.append(BOLD_PREFIX).append(toString(content)).append(BOLD_PREFIX); + return this; + } + + /** + * 引用 > 文本 + * @param content 文本 + */ + public MarkdownBuilder quote(Object... content) { + lineBreak(); + lineTextBuilder.append(QUOTE_PREFIX); + for (Object o : content) { + lineTextBuilder.append(toString(o)); + } + return this; + } + + /** + * 添加引用后, 换行, 写入下一行引用 + */ + public MarkdownBuilder quoteBreak(Object... content) { + // 当前行引用内容 + quote(content); + // 空引用行 + return quote(); + } + + /** + * 代码 + */ + public MarkdownBuilder code(String type, Object... code) { + lineBreak(); + lineTextBuilder.append(CODE_PREFIX).append(type); + lineBreak(); + for (Object o : code) { + lineTextBuilder.append(toString(o)); + } + lineBreak(); + lineTextBuilder.append(CODE_SUFFIX); + return lineBreak(); + } + + /** + * 代码 + */ + public MarkdownBuilder json(Object obj) { + String json; + if (obj instanceof String) { + json = (String) obj; + } + else { + json = multiJson(obj); + } + return code("json", json); + } + + @SneakyThrows + private String multiJson(Object obj) { + ObjectMapper mapper = JacksonJsonToolAdapter.getMapper().copy().enable(SerializationFeature.INDENT_OUTPUT); + return mapper.writeValueAsString(obj); + } + + /** + * 强制换行 + */ + public MarkdownBuilder forceLineBreak() { + content.add(lineTextBuilder.toString()); + lineTextBuilder = new StringBuilder(); + return this; + } + + /** + * 换行 当已编辑文本长度不为0时换行 + */ + public MarkdownBuilder lineBreak() { + if (lineTextBuilder.length() != 0) { + return forceLineBreak(); + } + return this; + } + + /** + * 生成 i 级标题 + * + * @author lingting 2020-06-10 22:55:39 + */ + private MarkdownBuilder title(int i, Object content) { + // 如果当前操作行已有字符,需要换行 + lineBreak(); + for (int j = 0; j < i; j++) { + lineTextBuilder.append(TITLE_PREFIX); + } + this.content.add(lineTextBuilder.append(" ").append(toString(content)).toString()); + lineTextBuilder = new StringBuilder(); + return this; + } + + public MarkdownBuilder title1(Object text) { + return title(1, text); + } + + public MarkdownBuilder title2(Object text) { + return title(2, text); + } + + public MarkdownBuilder title3(Object text) { + return title(3, text); + } + + public MarkdownBuilder title4(Object text) { + return title(4, text); + } + + public MarkdownBuilder title5(Object text) { + return title(5, text); + } + + String toString(Object o) { + if (o == null) { + return ""; + + } + return o.toString(); + } + + @Override + public String toString() { + return build(); + } + + /** + * 构筑 Markdown 文本 + */ + public String build() { + lineBreak(); + StringBuilder res = new StringBuilder(); + content.forEach(line -> res.append(line).append(" \n")); + return res.toString(); + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/pojo/SecretResponseBean.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/pojo/SecretResponseBean.java new file mode 100644 index 0000000..5ee528a --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/pojo/SecretResponseBean.java @@ -0,0 +1,43 @@ +package com.baiye.pojo; + +public class SecretResponseBean { + + private String result; + + private String reason; + + private String tels; + + public String getResult() { + return result; + } + + public void setResult(String result) { + this.result = result; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public String getTels() { + return tels; + } + + public void setTels(String tels) { + this.tels = tels; + } + + @Override + public String toString() { + return "SecretResponseBean{" + + "result='" + result + '\'' + + ", reason='" + reason + '\'' + + ", tels='" + tels + '\'' + + '}'; + } +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/request/wrapper/ModifyParamMapRequestWrapper.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/request/wrapper/ModifyParamMapRequestWrapper.java new file mode 100644 index 0000000..76e6153 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/request/wrapper/ModifyParamMapRequestWrapper.java @@ -0,0 +1,28 @@ +package com.baiye.request.wrapper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.util.Map; + +/** + * 修改parameterMap + * + * @author Hccake + * @version 1.0 + * @date 2019/10/17 21:57 + */ +public class ModifyParamMapRequestWrapper extends HttpServletRequestWrapper { + + private final Map parameterMap; + + public ModifyParamMapRequestWrapper(HttpServletRequest request, Map parameterMap) { + super(request); + this.parameterMap = parameterMap; + } + + @Override + public Map getParameterMap() { + return this.parameterMap; + } + +} \ No newline at end of file diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/request/wrapper/RepeatBodyRequestWrapper.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/request/wrapper/RepeatBodyRequestWrapper.java new file mode 100644 index 0000000..007cfdd --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/request/wrapper/RepeatBodyRequestWrapper.java @@ -0,0 +1,89 @@ +package com.baiye.request.wrapper; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StreamUtils; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Map; + +/** + * Request包装类:允许 body 重复读取 + * + * @author Hccake + * @version 1.0 + * @date 2019/10/16 10:14 + */ +@Slf4j +public class RepeatBodyRequestWrapper extends HttpServletRequestWrapper { + + private final byte[] body; + + private final Map parameterMap; + + public RepeatBodyRequestWrapper(HttpServletRequest request) { + super(request); + this.parameterMap = super.getParameterMap(); + this.body = getByteBody(request); + } + + @Override + public BufferedReader getReader() { + return ObjectUtils.isEmpty(body) ? null : new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() { + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body); + return new ServletInputStream() { + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + // doNoting + } + + @Override + public int read() { + return byteArrayInputStream.read(); + } + }; + } + + private static byte[] getByteBody(HttpServletRequest request) { + byte[] body = new byte[0]; + try { + body = StreamUtils.copyToByteArray(request.getInputStream()); + } + catch (IOException e) { + log.error("解析流中数据异常", e); + } + return body; + } + + /** + * 重写 getParameterMap() 方法 解决 undertow 中流被读取后,会进行标记,从而导致无法正确获取 body 中的表单数据的问题 + * @see io.undertow.servlet.spec.HttpServletRequestImpl#readStarted + * @return Map parameterMap + */ + @Override + public Map getParameterMap() { + return parameterMap; + } + +} \ No newline at end of file diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/spring/BallcatBeanPostProcessor.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/spring/BallcatBeanPostProcessor.java new file mode 100644 index 0000000..79e9f7f --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/spring/BallcatBeanPostProcessor.java @@ -0,0 +1,64 @@ +package com.baiye.spring; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.Ordered; + +/** + * @author lingting 2022/10/22 15:11 + */ +public interface BallcatBeanPostProcessor extends BeanPostProcessor, Ordered { + + /** + * 判断是否处理该bean + * @param bean bean实例 + * @param beanName bean名称 + * @param isBefore true表示当前为初始化之前是否处理判断, false 表示当前为初始化之后是否处理判断 + * @return boolean true 表示要处理该bean + */ + boolean isProcess(Object bean, String beanName, boolean isBefore); + + /** + * 初始化之前处理 + * @param bean bean + * @param beanName bean名称 + * @return java.lang.Object + */ + default Object postProcessBefore(Object bean, String beanName) { + return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName); + } + + /** + * 初始化之后处理 + * @param bean bean + * @param beanName bean名称 + * @return java.lang.Object + */ + default Object postProcessAfter(Object bean, String beanName) { + return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName); + } + + @Override + default int getOrder() { + // 默认优先级 + return Ordered.HIGHEST_PRECEDENCE; + } + + @Override + default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (isProcess(bean, beanName, true)) { + return postProcessBefore(bean, beanName); + } + + return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName); + } + + @Override + default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (isProcess(bean, beanName, false)) { + return postProcessAfter(bean, beanName); + } + return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName); + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/spring/compose/ContextComposeBeanPostProcessor.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/spring/compose/ContextComposeBeanPostProcessor.java new file mode 100644 index 0000000..068c2d9 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/spring/compose/ContextComposeBeanPostProcessor.java @@ -0,0 +1,24 @@ +package com.baiye.spring.compose; + +import com.baiye.compose.ContextComponent; +import com.baiye.spring.BallcatBeanPostProcessor; +import org.springframework.stereotype.Component; + +/** + * @author lingting 2022/10/22 15:10 + */ +@Component +public class ContextComposeBeanPostProcessor implements BallcatBeanPostProcessor { + + @Override + public boolean isProcess(Object bean, String beanName, boolean isBefore) { + return bean != null && ContextComponent.class.isAssignableFrom(bean.getClass()); + } + + @Override + public Object postProcessAfter(Object bean, String beanName) { + ((ContextComponent) bean).onApplicationStart(); + return bean; + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/spring/compose/SpringContextClosed.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/spring/compose/SpringContextClosed.java new file mode 100644 index 0000000..3454e8e --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/spring/compose/SpringContextClosed.java @@ -0,0 +1,38 @@ +package com.baiye.spring.compose; + +import com.baiye.compose.ContextComponent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @author lingting 2022/10/22 17:45 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class SpringContextClosed implements ApplicationListener { + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + log.debug("spring context closed"); + ApplicationContext applicationContext = event.getApplicationContext(); + + // 上下文容器停止 + contextComponentStop(applicationContext); + } + + private static void contextComponentStop(ApplicationContext applicationContext) { + Map contextComponentMap = applicationContext.getBeansOfType(ContextComponent.class); + + for (Map.Entry entry : contextComponentMap.entrySet()) { + entry.getValue().onApplicationStop(); + } + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractBlockingQueueThread.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractBlockingQueueThread.java new file mode 100644 index 0000000..8635eb6 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractBlockingQueueThread.java @@ -0,0 +1,39 @@ +package com.baiye.thread; + +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * 抽象的线程类,主要用于汇聚详情数据 做一些基础的处理后 进行批量插入 + * + * @author lingting + */ +@Slf4j +public abstract class AbstractBlockingQueueThread extends AbstractQueueThread { + + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + + @Override + public void put(T t) { + if (t != null) { + try { + queue.put(t); + } + catch (InterruptedException e) { + currentThread().interrupt(); + } + catch (Exception e) { + log.error("{} put Object error, param: {}", this.getClass().toString(), t, e); + } + } + } + + @Override + protected T poll(long time) throws InterruptedException { + return queue.poll(time, TimeUnit.MILLISECONDS); + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractDynamicTimer.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractDynamicTimer.java new file mode 100644 index 0000000..410e270 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractDynamicTimer.java @@ -0,0 +1,102 @@ +package com.baiye.thread; + +import com.baiye.lock.JavaReentrantLock; +import lombok.extern.slf4j.Slf4j; + +import java.util.Comparator; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author lingting 2023-04-22 10:39 + */ +@Slf4j +@SuppressWarnings("java:S1066") +public abstract class AbstractDynamicTimer extends AbstractThreadContextComponent { + + private final JavaReentrantLock lock = new JavaReentrantLock(); + + protected final PriorityBlockingQueue queue = new PriorityBlockingQueue<>(defaultCapacity(), comparator()); + + public abstract Comparator comparator(); + + /** + * 默认大小 + */ + protected int defaultCapacity() { + return 11; + } + + /** + * 还有多久要处理该对象 + * @param t 对象 + * @return 具体处理该对象还要多久, 单位: 毫秒 + */ + protected abstract long sleepTime(T t); + + public void put(T t) { + if (t == null) { + return; + } + + try { + lock.runLockInterruptibly(() -> { + queue.add(t); + lock.signalAll(); + }); + } + catch (InterruptedException e) { + interrupt(); + } + catch (Exception e) { + log.error("{} put error, param: {}", this.getClass().toString(), t, e); + } + } + + @Override + public void run() { + init(); + while (isRun()) { + try { + T t = pool(); + lock.runLockInterruptibly(() -> { + if (t == null) { + lock.await(24, TimeUnit.HOURS); + return; + } + + long sleepTime = sleepTime(t); + // 需要休眠 + if (sleepTime > 0) { + // 如果是被唤醒 + if (lock.await(sleepTime, TimeUnit.MILLISECONDS)) { + put(t); + return; + } + } + + process(t); + }); + + } + catch (InterruptedException e) { + interrupt(); + shutdown(); + } + catch (Exception e) { + log.error("类: {}; 线程: {}; 运行异常! ", getSimpleName(), getId(), e); + } + } + } + + protected T pool() { + return queue.poll(); + } + + protected abstract void process(T t); + + protected void shutdown() { + log.warn("类: {}; 线程: {}; 被中断! 剩余数据: {}", getSimpleName(), getId(), queue.size()); + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractQueueThread.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractQueueThread.java new file mode 100644 index 0000000..4f33d2a --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractQueueThread.java @@ -0,0 +1,212 @@ +package com.baiye.thread; + +import com.baiye.compose.ContextComponent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * 顶级队列线程类 + * + * @author lingting 2021/3/2 15:07 + */ +@Slf4j +public abstract class AbstractQueueThread extends Thread implements ContextComponent { + + /** + * 默认缓存数据数量 + */ + private static final int DEFAULT_BATCH_SIZE = 500; + + /** + * 默认等待时长 30秒;单位 毫秒 + */ + private static final long DEFAULT_BATCH_TIMEOUT_MS = 30 * 1000L; + + /** + * 默认获取数据时的超时时间 + */ + private static final long POLL_TIMEOUT_MS = 5 * 1000L; + + /** + * 用于子类自定义缓存数据数量 + * @return long + */ + public int getBatchSize() { + return DEFAULT_BATCH_SIZE; + } + + /** + * 用于子类自定义等待时长 + * @return 返回时长,单位毫秒 + */ + public long getBatchTimeout() { + return DEFAULT_BATCH_TIMEOUT_MS; + } + + /** + * 用于子类自定义 获取数据的超时时间 + * @return 返回时长,单位毫秒 + */ + public long getPollTimeout() { + return POLL_TIMEOUT_MS; + } + + /** + * 往队列插入数据 + * @param e 数据 + */ + public abstract void put(E e); + + /** + * 运行前执行初始化 + */ + protected void init() { + } + + /** + * 是否可以继续运行 + * @return boolean true 表示可以继续运行 + */ + public boolean isRun() { + // 未被中断表示可以继续运行 + return !isInterrupted(); + } + + /** + * 数据处理前执行 + */ + protected void preProcess() { + } + + /** + * 从队列中取值 + * @param time 等待时长, 单位 毫秒 + * @return E + * @throws InterruptedException 线程中断 + */ + protected abstract E poll(long time) throws InterruptedException; + + /** + * 处理接收的数据 + * @param list 当前所有数据 + * @param e 接收的数据 + */ + protected void receiveProcess(List list, E e) { + list.add(e); + } + + /** + * 处理所有已接收的数据 + * @param list 所有已接收的数据 + * @throws Exception 异常 + */ + @SuppressWarnings("java:S112") + protected abstract void process(List list) throws Exception; + + @Override + @SuppressWarnings("java:S1181") + public void run() { + init(); + List list; + while (isRun()) { + list = new ArrayList<>(getBatchSize()); + + try { + preProcess(); + fillList(list); + + if (!isRun()) { + shutdown(list); + } + else { + process(list); + } + } + catch (InterruptedException e) { + shutdown(list); + Thread.currentThread().interrupt(); + } + catch (Exception e) { + error(e, list); + } + // Throwable 异常直接结束. 这里捕获用来保留信息. 方便排查问题 + catch (Throwable t) { + log.error("线程队列运行异常!", t); + throw t; + } + } + } + + protected void fillList(List list) { + long timestamp = 0; + int count = 0; + + while (count < getBatchSize()) { + E e = poll(); + + if (e != null) { + // 第一次插入数据 + if (count++ == 0) { + // 记录时间 + timestamp = System.currentTimeMillis(); + } + receiveProcess(list, e); + } + + // 无法继续运行 + final boolean isBreak = !isRun() + // 或者 已有数据且超过设定的等待时间 + || (!CollectionUtils.isEmpty(list) && System.currentTimeMillis() - timestamp >= getBatchTimeout()); + if (isBreak) { + break; + } + } + } + + public E poll() { + E e = null; + try { + e = poll(getPollTimeout()); + } + catch (InterruptedException ex) { + log.error("{} 类的poll线程被中断!id: {}", getClass().getSimpleName(), getId()); + interrupt(); + } + return e; + } + + /** + * 发生异常时处理异常 + * @param e 异常 + * @param list 当时的数据 + */ + protected abstract void error(Throwable e, List list); + + /** + * 线程被中断后的处理. 如果有缓存手段可以让数据进入缓存. + * @param list 当前数据 + */ + protected void shutdown(List list) { + log.warn("{} 线程: {} 被关闭. 数据:{}", this.getClass().getSimpleName(), getId(), list); + } + + @Override + public void onApplicationStart() { + // 默认配置线程名. 用来方便查询 + setName(this.getClass().getSimpleName()); + if (!this.isAlive()) { + this.start(); + } + } + + @Override + public void onApplicationStop() { + log.warn("{} 线程: {}; 开始关闭!", getClass().getSimpleName(), getId()); + // 通过中断线程唤醒当前线程. 让线程进入 shutdownHandler 方法处理数据 + interrupt(); + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractThreadContextComponent.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractThreadContextComponent.java new file mode 100644 index 0000000..4b13d22 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractThreadContextComponent.java @@ -0,0 +1,41 @@ +package com.baiye.thread; + +import com.baiye.compose.ContextComponent; +import lombok.extern.slf4j.Slf4j; + +/** + * @author lingting 2023-04-22 10:40 + */ +@Slf4j +public abstract class AbstractThreadContextComponent extends Thread implements ContextComponent { + + protected void init() { + + } + + public boolean isRun() { + return !isInterrupted() && isAlive(); + } + + @Override + public void onApplicationStart() { + setName(getClass().getSimpleName()); + if (!isAlive()) { + start(); + } + } + + @Override + public void onApplicationStop() { + log.warn("{} 线程: {}; 开始关闭!", getClass().getSimpleName(), getId()); + interrupt(); + } + + public String getSimpleName() { + return getClass().getSimpleName(); + } + + @Override + public abstract void run(); + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractTimer.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractTimer.java new file mode 100644 index 0000000..15de41c --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/thread/AbstractTimer.java @@ -0,0 +1,85 @@ +package com.baiye.thread; + +import com.baiye.compose.ContextComponent; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +/** + * @author lingting 2022/6/27 20:26 + */ +@Slf4j +public abstract class AbstractTimer extends Thread implements ContextComponent { + + /** + * 获取超时时间, 单位: 毫秒 + */ + public long getTimeout() { + return TimeUnit.SECONDS.toMillis(30); + } + + public boolean isRun() { + return !isInterrupted(); + } + + /** + * 运行前执行初始化 + */ + protected void init() { + } + + /** + * 执行任务 + */ + protected abstract void process(); + + /** + * 线程被中断触发. + */ + protected void shutdown() { + } + + protected void error(Exception e) { + log.error("{} 类 线程: {} 出现异常!", getClass().getSimpleName(), getId(), e); + } + + @Override + public void run() { + init(); + while (isRun()) { + try { + process(); + + // 已经停止运行, 结束 + if (!isRun()) { + shutdown(); + return; + } + + Thread.sleep(getTimeout()); + } + catch (InterruptedException e) { + interrupt(); + shutdown(); + } + catch (Exception e) { + error(e); + } + } + } + + @Override + public void onApplicationStart() { + setName(getClass().getSimpleName()); + if (!isAlive()) { + start(); + } + } + + @Override + public void onApplicationStop() { + log.warn("{} 线程: {}; 开始关闭!", getClass().getSimpleName(), getId()); + interrupt(); + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/util/AppUtils.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/AppUtils.java new file mode 100644 index 0000000..9baeb31 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/AppUtils.java @@ -0,0 +1,90 @@ +package com.baiye.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.UUID; + +/** + * @author Enzo + * @date : 2022/6/28 + */ +public class AppUtils { + + /** + * 生成 app_secret 密钥 + */ + private static final String SERVER_NAME = "rPpXNbQhBALkyA5xWHddmu2Bo5Ws8Yxhq4XB7ADMsiY6ii5Ed94ra7AU4TDtG7s2"; + + private static final String[] CHARS = new String[] { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", + "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", + "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", + "S", "T", "U", "V", "W", "X", "Y", "Z" }; + + /** + * @Description: + *

      + * 短8位UUID思想其实借鉴微博短域名的生成方式,但是其重复概率过高,而且每次生成4个,需要随即选取一个。 + * 本算法利用62个可打印字符,通过随机生成32位UUID,由于UUID都为十六进制,所以将UUID分成8组,每4个为一组,然后通过模62操作,结果作为索引取出字符, + * 这样重复率大大降低。 经测试,在生成一千万个数据也没有出现重复,完全满足大部分需求。 + *

      + * @author mazhq + * @date 2019/8/27 16:16 + */ + public static String getAppId() { + StringBuilder shortBuffer = new StringBuilder(); + String uuid = UUID.randomUUID().toString().replace("-", ""); + for (int i = 0; i < 8; i++) { + String str = uuid.substring(i * 4, i * 4 + 4); + int x = Integer.parseInt(str, 16); + shortBuffer.append(CHARS[x % 0x3E]); + } + return shortBuffer.toString(); + } + + /** + *

      + * 通过appId和内置关键词生成APP Secret + *

      + * + * @author mazhq + * @date 2019/8/27 16:32 + */ + public static String getAppSecret(String appId) { + try { + String[] array = new String[] { appId, SERVER_NAME }; + StringBuilder sb = new StringBuilder(); + // 字符串排序 + Arrays.sort(array); + for (String s : array) { + sb.append(s); + } + String str = sb.toString(); + MessageDigest md = MessageDigest.getInstance("SHA-1"); + md.update(str.getBytes()); + byte[] digest = md.digest(); + + StringBuilder hexstr = new StringBuilder(); + String shaHex; + for (byte b : digest) { + shaHex = Integer.toHexString(b & 0xFF); + if (shaHex.length() < 2) { + hexstr.append(0); + } + hexstr.append(shaHex); + } + return hexstr.toString(); + } + catch (NoSuchAlgorithmException e) { + throw new RuntimeException(); + } + } + + public static void main(String[] args) { + String appId = getAppId(); + String appSecret = getAppSecret(appId); + System.out.println("appId: " + appId); + System.out.println("appSecret: " + appSecret); + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/util/CompressUtil.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/CompressUtil.java new file mode 100644 index 0000000..ddaa1fc --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/CompressUtil.java @@ -0,0 +1,85 @@ +package com.baiye.util; + +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.IdUtil; +import net.lingala.zip4j.core.ZipFile; +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.model.ZipParameters; +import net.lingala.zip4j.util.Zip4jConstants; +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.util.ArrayList; + +/** + * @author Enzo + * @date : 2022/10/24 + */ +public class CompressUtil { + private CompressUtil() { + } + + /** + * @param zipPath 压缩文件路径 + * @param filepath 文件路径 + * @param password 压缩密码 + */ + public static void decryptionCompression(String zipPath, String filepath, String password) { + try { + //创建压缩文件 + ZipFile zipFile = new ZipFile(zipPath); + ArrayList files = new ArrayList<>(); + files.add(new File(filepath)); + + //设置压缩文件参数 + ZipParameters parameters = new ZipParameters(); + //设置压缩方法 + parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); + + //设置压缩级别 + //DEFLATE_LEVEL_FASTEST - Lowest compression level but higher speed of compression + //DEFLATE_LEVEL_FAST - Low compression level but higher speed of compression + //DEFLATE_LEVEL_NORMAL - Optimal balance between compression level/speed + //DEFLATE_LEVEL_MAXIMUM - High compression level with a compromise of speed + //DEFLATE_LEVEL_ULTRA - Highest compression level but low speed + parameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL); + + + //设置加密方法 + parameters.setEncryptionMethod(Zip4jConstants.ENC_METHOD_AES); + + //设置aes加密强度 + parameters.setAesKeyStrength(Zip4jConstants.AES_STRENGTH_256); + + if (StringUtils.isNotBlank(password)) { + //设置压缩文件加密 + parameters.setEncryptFiles(Boolean.TRUE); + //设置密码 + parameters.setPassword(password); + } + + //添加文件到压缩文件 + zipFile.addFiles(files, parameters); + } catch (ZipException e) { + e.printStackTrace(); + } + } + + public static String unzipFiles(String fileUrl, String zipPath, String password) throws ZipException { + File file = new File(zipPath); + ZipFile zipFile = new ZipFile(file); + //设置文件编码,根据实际场景 + zipFile.setFileNameCharset(CharsetUtil.GBK); + if (zipFile.isEncrypted()) { + zipFile.setPassword(password); + } + String uuid = IdUtil.randomUUID(); + String filePath + = fileUrl.concat(StrPool.SLASH).concat(uuid); + zipFile.extractAll(filePath); + return filePath; + } + + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/util/DecryptPnoUtil.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/DecryptPnoUtil.java new file mode 100644 index 0000000..517e3fd --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/DecryptPnoUtil.java @@ -0,0 +1,106 @@ +package com.baiye.util; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.http.Header; +import cn.hutool.http.HttpRequest; +import cn.hutool.json.JSONUtil; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.constant.SecretConstant; +import com.baiye.pojo.SecretResponseBean; +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; + +/** + * @author jt + */ +@Slf4j +public class DecryptPnoUtil { + private static final String HTTP_RESPONSE_JSON_FORMATE = "application/x-www-form-urlencoded"; + + public static String decryptPno(String origins) { + HashMap paramMap = new HashMap<>(); + paramMap.put("sig", createSig()); + paramMap.put("appid", SecretConstant.SECRET_DECRYPT_YY_ID); + paramMap.put("tels", Base64.encode(origins)); + //链式构建请求 + String result = HttpRequest.post(SecretConstant.SECRET_DECRYPT_YY_URL) + .header(Header.CONTENT_TYPE, HTTP_RESPONSE_JSON_FORMATE) + .form(paramMap) + .timeout(2_000) + .execute().body(); + SecretResponseBean secretResponseBean = JSONUtil.toBean(result, SecretResponseBean.class); + return Base64.decodeStr(secretResponseBean.getTels()); + } + + + /** + * 生成 sig 值 + * + * @return sig + */ + private static String createSig() { + return SecureUtil.md5( + SecretConstant.SECRET_DECRYPT_YY_ID + + SecretConstant.SECRET_DECRYPT_YY_SIG + + DateUtil.format(new Date(), SecretConstant.SECRET_DECRYPT_TIME_FORMATE)); + } + + + + /** + * 批量解密 + * + * @param origins 待处理的加密数据 + * @return 处理后的解密数据 + */ + public static SecretResponseBean batchDecryptPno(String origins) { + + + HashMap paramMap = new HashMap<>(); + paramMap.put("sig", createSig()); + paramMap.put("appid", SecretConstant.SECRET_DECRYPT_YY_ID); + paramMap.put("tels", origins); + + //链式构建请求 + String result = HttpRequest.post(SecretConstant.SECRET_DECRYPT_YY_URL) + .header(Header.CONTENT_TYPE, HTTP_RESPONSE_JSON_FORMATE) + .form(paramMap) + .timeout(2_000) + .execute().body(); + // 解析响应内容 + return JSONUtil.toBean(result, SecretResponseBean.class); + } + + public static List decryptPhoneList(List phoneSets) { + List decryptList = Lists.newArrayList(); + // 集合分割 + List> partitions = ListUtil.partition(phoneSets, DefaultNumberConstants.TWO_HUNDRED); + for (List partition : partitions) { + String join = Joiner.on(StrPool.COMMA).skipNulls().join(partition); + // 批量解析号码 + SecretResponseBean responseBean = DecryptPnoUtil.batchDecryptPno + (Base64.encode(StringUtils.substringBeforeLast(join, StrPool.COMMA))); + if (ObjectUtil.isNotNull(responseBean) && ObjectUtil.isNotNull(responseBean.getTels())) { + String decodeStr = Base64.decodeStr(responseBean.getTels()); + String[] split = decodeStr.split(StrPool.COMMA); + if (split.length > DefaultNumberConstants.ZERO_NUMBER) { + decryptList.addAll(Lists.newArrayList(Arrays.asList(split))); + } + } + } + return decryptList; + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/util/FileUtil.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/FileUtil.java new file mode 100644 index 0000000..72166c0 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/FileUtil.java @@ -0,0 +1,406 @@ +package com.baiye.util; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.poi.excel.BigExcelWriter; +import cn.hutool.poi.excel.ExcelUtil; +import com.baiye.exception.BusinessException; +import com.baiye.result.BaseResultCode; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.security.MessageDigest; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.*; + +/** + * File工具类,扩展 hutool 工具包 + */ +public class FileUtil extends cn.hutool.core.io.FileUtil { + + private static final Logger log = LoggerFactory.getLogger(FileUtil.class); + + /** + * 系统临时目录
      + * windows 包含路径分割符,但Linux 不包含, 在windows \\==\ 前提下, 为安全起见 同意拼装 路径分割符,
      +	 *       java.io.tmpdir
      +	 *       windows : C:\Users/xxx\AppData\Local\Temp\
      +	 *       linux: /temp
      +	 * 
      + */ + public static final String SYS_TEM_DIR = System.getProperty("java.io.tmpdir") + File.separator; + + /** + * 定义GB的计算常量 + */ + private static final int GB = 1024 * 1024 * 1024; + + /** + * 定义MB的计算常量 + */ + private static final int MB = 1024 * 1024; + + /** + * 定义KB的计算常量 + */ + private static final int KB = 1024; + + /** + * 默认的生成文件前缀 + */ + private static final String PRE_FILE = "out_task_"; + + /** + * 字段分隔符 (目前用于随机文件名中,...) [后续使用范围进行自行补充] + */ + private static final String SEPARATOR = "_"; + + /** + * 随机字符串的长度 (目前用于随机文件名中,...) [后续使用范围进行自行补充] + */ + private static final int RANDOM_STRING_LENGTH = 6; + + /** + * 格式化小数 + */ + private static final DecimalFormat DF = new DecimalFormat("0.00"); + + /** + * MultipartFile转File + */ + public static File toFile(MultipartFile multipartFile) { + // 获取文件名 + String fileName = multipartFile.getOriginalFilename(); + // 获取文件后缀 + String prefix = "." + getExtensionName(fileName); + File file = null; + try { + // 用uuid作为文件名,防止生成的临时文件重复 + file = File.createTempFile(IdUtil.simpleUUID(), prefix); + // MultipartFile to File + multipartFile.transferTo(file); + } + catch (IOException e) { + log.error(e.getMessage(), e); + } + return file; + } + + /** + * 获取文件扩展名,不带 . + */ + public static String getExtensionName(String filename) { + if ((filename != null) && (filename.length() > 0)) { + int dot = filename.lastIndexOf('.'); + if ((dot > -1) && (dot < (filename.length() - 1))) { + return filename.substring(dot + 1); + } + } + return filename; + } + + /** + * Java文件操作 获取不带扩展名的文件名 + */ + public static String getFileNameNoEx(String filename) { + if ((filename != null) && (filename.length() > 0)) { + int dot = filename.lastIndexOf('.'); + if ((dot > -1) && (dot < (filename.length()))) { + return filename.substring(0, dot); + } + } + return filename; + } + + /** + * 文件大小转换 + */ + public static String getSize(long size) { + String resultSize; + if (size / GB >= 1) { + // 如果当前Byte的值大于等于1GB + resultSize = DF.format(size / (float) GB) + "GB "; + } + else if (size / MB >= 1) { + // 如果当前Byte的值大于等于1MB + resultSize = DF.format(size / (float) MB) + "MB "; + } + else if (size / KB >= 1) { + // 如果当前Byte的值大于等于1KB + resultSize = DF.format(size / (float) KB) + "KB "; + } + else { + resultSize = size + "B "; + } + return resultSize; + } + + /** + * inputStream 转 File + */ + public static File inputStreamToFile(InputStream ins, String name) throws Exception { + File file = new File(SYS_TEM_DIR + name); + if (file.exists()) { + return file; + } + OutputStream os = new FileOutputStream(file); + int bytesRead; + int len = 8192; + byte[] buffer = new byte[len]; + while ((bytesRead = ins.read(buffer, 0, len)) != -1) { + os.write(buffer, 0, bytesRead); + } + os.close(); + ins.close(); + return file; + } + + /** + * 将文件名解析成文件的上传路径 + */ + public static File upload(MultipartFile file, String filePath) { + Date date = new Date(); + SimpleDateFormat format = new SimpleDateFormat("yyyyMMddhhmmssS"); + String name = getFileNameNoEx(file.getOriginalFilename()); + String suffix = getExtensionName(file.getOriginalFilename()); + String nowStr = "-" + format.format(date); + try { + String fileName = name + nowStr + "." + suffix; + String path = filePath + fileName; + // getCanonicalFile 可解析正确各种路径 + File dest = new File(path).getCanonicalFile(); + // 检测是否存在目录 + if (!dest.getParentFile().exists()) { + if (!dest.getParentFile().mkdirs()) { + System.out.println("was not successful."); + } + } + // 文件写入 + file.transferTo(dest); + return dest; + } + catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + /** + * 导出excel + */ + public static void downloadExcel(List> list, HttpServletResponse response) throws IOException { + String tempPath = SYS_TEM_DIR + IdUtil.fastSimpleUUID() + ".xlsx"; + File file = new File(tempPath); + BigExcelWriter writer = ExcelUtil.getBigWriter(file); + // 一次性写出内容,使用默认样式,强制输出标题 + writer.write(list, true); + // response为HttpServletResponse对象 + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"); + // test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码 + response.setHeader("Content-Disposition", "attachment;filename=file.xlsx"); + ServletOutputStream out = response.getOutputStream(); + // 终止后删除临时文件 + file.deleteOnExit(); + writer.flush(out, true); + // 此处记得关闭输出Servlet流 + IoUtil.close(out); + } + + /** + * 导入excel文件 + * @param file 传入服务器加载的文件 + * @param response + * @throws IOException + */ + public static void downloadExcel(File file, HttpServletResponse response) throws IOException { + BigExcelWriter writer = ExcelUtil.getBigWriter(file); + // response为HttpServletResponse对象 + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"); + // test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码 + response.setHeader("Content-Disposition", "attachment;filename=file.xlsx"); + ServletOutputStream out = response.getOutputStream(); + // 终止后删除临时文件 + file.deleteOnExit(); + writer.flush(out, true); + // 此处记得关闭输出Servlet流 + IoUtil.close(out); + } + + public static String getFileType(String type) { + String documents = "txt doc pdf ppt pps xlsx xls docx"; + String music = "mp3 wav wma mpa ram ra aac aif m4a"; + String video = "avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg"; + String image = "bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg"; + if (image.contains(type)) { + return "图片"; + } + else if (documents.contains(type)) { + return "文档"; + } + else if (music.contains(type)) { + return "音乐"; + } + else if (video.contains(type)) { + return "视频"; + } + else { + return "其他"; + } + } + + public static void checkSize(long maxSize, long size) { + // 1M + int len = 1024 * 1024; + if (size > (maxSize * len)) { + throw new BusinessException(BaseResultCode.FILE_UPLOAD_ERROR, "文件超出规定大小"); + } + } + + /** + * 判断两个文件是否相同 + */ + public static boolean check(File file1, File file2) { + String img1Md5 = getMd5(file1); + String img2Md5 = getMd5(file2); + return img1Md5.equals(img2Md5); + } + + /** + * 判断两个文件是否相同 + */ + public static boolean check(String file1Md5, String file2Md5) { + return file1Md5.equals(file2Md5); + } + + private static byte[] getByte(File file) { + // 得到文件长度 + byte[] b = new byte[(int) file.length()]; + try { + InputStream in = new FileInputStream(file); + try { + System.out.println(in.read(b)); + } + catch (IOException e) { + log.error(e.getMessage(), e); + } + } + catch (FileNotFoundException e) { + log.error(e.getMessage(), e); + return null; + } + return b; + } + + private static String getMd5(byte[] bytes) { + // 16进制字符 + char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + try { + MessageDigest mdTemp = MessageDigest.getInstance("MD5"); + mdTemp.update(bytes); + byte[] md = mdTemp.digest(); + int j = md.length; + char[] str = new char[j * 2]; + int k = 0; + // 移位 输出字符串 + for (byte byte0 : md) { + str[k++] = hexDigits[byte0 >>> 4 & 0xf]; + str[k++] = hexDigits[byte0 & 0xf]; + } + return new String(str); + } + catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + public static String getMd5(File file) { + return getMd5(getByte(file)); + } + + /** + * 构建随机文件名规则 + * @return 构建好的随机文件名 + */ + public static String buildOnlyFileNameRule() { + String fileName = PRE_FILE + RandomUtil.randomString(RANDOM_STRING_LENGTH) + SEPARATOR + + Instant.now().getEpochSecond(); + return fileName; + } + + /** + * MultipartFile转file + * @param multipartFile + * @return + */ + public static File multiToFile(MultipartFile multipartFile) { + // 选择用缓冲区来实现这个转换即使用java 创建的临时文件 使用 MultipartFile.transferto()方法 。 + File file = null; + try { + String originalFilename = multipartFile.getOriginalFilename(); + String[] filename = originalFilename.split("\\."); + file = File.createTempFile(filename[0], StrPool.DOT.concat(filename[1])); + multipartFile.transferTo(file); + file.deleteOnExit(); + } + catch (IOException e) { + log.error(e.getMessage()); + } + return file; + } + + /** + *

      获取指定文件夹下所有文件,不含文件夹

      + * @param dirFilePath 文件夹路径 + * @return + */ + public static List getAllFile(String dirFilePath) { + if (StringUtils.isBlank(dirFilePath)) + return null; + return getAllFile(new File(dirFilePath)); + } + + /** + *

      获取指定文件夹下所有文件,不含文件夹

      + * @param dirFile 文件夹 + * @return + */ + public static List getAllFile(File dirFile) { + // 如果文件夹不存在或着不是文件夹,则返回 null + if (Objects.isNull(dirFile) || !dirFile.exists() || dirFile.isFile()) + return null; + + File[] childrenFiles = dirFile.listFiles(); + if (Objects.isNull(childrenFiles) || childrenFiles.length == 0) + return null; + + List files = new ArrayList<>(); + for (File childFile : childrenFiles) { + + // 如果时文件,直接添加到结果集合 + if (childFile.isFile()) { + files.add(childFile); + } + else { + // 如果是文件夹,则将其内部文件添加进结果集合 + List cFiles = getAllFile(childFile); + if (Objects.isNull(cFiles) || cFiles.isEmpty()) + continue; + files.addAll(cFiles); + } + + } + + return files; + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/util/MobileUtil.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/MobileUtil.java new file mode 100644 index 0000000..7a99a50 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/MobileUtil.java @@ -0,0 +1,105 @@ +package com.baiye.util; + +import org.apache.commons.lang3.StringUtils; + +import java.util.regex.Pattern; + +/** + * @author Enzo + * @date : 2022/4/18 + */ +public class MobileUtil { + + /** + * 中国电信号码格式验证 手机段: 133,149,153,173,177,180,181,189,191,199,1349,1410,1700,1701,1702,193,===中国广电 192 + **/ + private static final String CHINA_TELECOM_PATTERN = "(?:^(?:\\+86)?1(?:33|92|49|53|7[37]|8[019]|9[139])\\d{8}$)|(?:^(?:\\+86)?1349\\d{7}$)|(?:^(?:\\+86)?1410\\d{7}$)|(?:^(?:\\+86)?170[0-2]\\d{7}$)"; + + /** + * 中国联通号码格式验证 手机段:130,131,132,145,146,155,156,166,171,175,176,185,186,1704,1707,1708,1709 + **/ + private static final String CHINA_UNICOM_PATTERN = "(?:^(?:\\+86)?1(?:3[0-2]|4[56]|5[56]|66|7[156]|8[56])\\d{8}$)|(?:^(?:\\+86)?170[47-9]\\d{7}$)"; + + /** + * 中国移动号码格式验证 + * 手机段:134,135,136,137,138,139,147,148,150,151,152,157,158,159,178,182,183,184,187,188,195,198,1440,1703,1705,1706 + **/ + private static final String CHINA_MOBILE_PATTERN = "(?:^(?:\\+86)?1(?:3[4-9]|4[78]|5[0-27-9]|6[5]|7[28]|8[2-478]|98|95)\\d{8}$)|(?:^(?:\\+86)?1440\\d{7}$)|(?:^(?:\\+86)?170[356]\\d{7}$)"; + + /** + * 中国大陆手机号码校验 + * @param phone + * @return + */ + public static boolean checkPhone(String phone) { + if (StringUtils.isNotBlank(phone)) { + if (checkChinaMobile(phone) || checkChinaUnicom(phone) || checkChinaTelecom(phone)) { + return true; + } + } + return false; + } + + public static void main(String[] args) { + // String s = "/home/eladmin/mail/MM_20221125_2.zip"; + // String filePath = s.substring(s.lastIndexOf(StrPool.SLASH) + + // DefaultNumberConstants.ONE_NUMBER); + System.out.println(checkPhone("17269788988")); + } + + /** + * 中国移动手机号码校验 + * @param phone + * @return + */ + public static boolean checkChinaMobile(String phone) { + if (StringUtils.isNotBlank(phone)) { + Pattern regexp = Pattern.compile(CHINA_MOBILE_PATTERN); + return regexp.matcher(phone).matches(); + } + return false; + } + + /** + * 中国联通手机号码校验 + * @param phone + * @return + */ + public static boolean checkChinaUnicom(String phone) { + if (StringUtils.isNotBlank(phone)) { + Pattern regexp = Pattern.compile(CHINA_UNICOM_PATTERN); + if (regexp.matcher(phone).matches()) { + return true; + } + } + return false; + } + + /** + * 中国电信手机号码校验 + * @param phone + * @return + */ + public static boolean checkChinaTelecom(String phone) { + if (StringUtils.isNotBlank(phone)) { + Pattern regexp = Pattern.compile(CHINA_TELECOM_PATTERN); + if (regexp.matcher(phone).matches()) { + return true; + } + } + return false; + } + + /** + * 隐藏手机号中间四位 + * @param phone + * @return java.lang.String + */ + public static String hideMiddleMobile(String phone) { + if (StringUtils.isNotBlank(phone)) { + phone = phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); + } + return phone; + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/util/RedisUtils.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/RedisUtils.java new file mode 100644 index 0000000..d68d631 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/RedisUtils.java @@ -0,0 +1,737 @@ +package com.baiye.util; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.ObjectUtil; +import com.baiye.constant.DefaultNumberConstants; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.GeoResults; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisGeoCommands; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisConnectionUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.TimeUnit; + +@Component +@SuppressWarnings({"unchecked", "all"}) +@Slf4j +public class RedisUtils { + @Resource + private RedisTemplate redisTemplate; + + + private String onlineKey = "online-token-"; + + /** + * 指定缓存失效时间 + * + * @param key 键 + * @param time 时间(秒) + */ + public boolean expire(String key, long time) { + try { + if (time > 0) { + redisTemplate.expire(key, time, TimeUnit.SECONDS); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + return true; + } + + /** + * 指定缓存失效时间 + * + * @param key 键 + * @param time 时间(秒) + * @param timeUnit 单位 + */ + public boolean expire(String key, long time, TimeUnit timeUnit) { + try { + if (time > 0) { + redisTemplate.expire(key, time, timeUnit); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + return true; + } + + /** + * 根据 key 获取过期时间 + * + * @param key 键 不能为null + * @return 时间(秒) 返回0代表为永久有效 + */ + public long getExpire(String key) { + return redisTemplate.getExpire(key, TimeUnit.SECONDS); + } + + /** + * 查找匹配key + * + * @param pattern key + * @return / + */ + public List scan(String pattern) { + ScanOptions options = ScanOptions.scanOptions().match(pattern).build(); + RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); + RedisConnection rc = Objects.requireNonNull(factory).getConnection(); + Cursor cursor = rc.scan(options); + List result = new ArrayList<>(); + while (cursor.hasNext()) { + result.add(new String(cursor.next())); + } + try { + RedisConnectionUtils.releaseConnection(rc, factory); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + /** + * 分页查询 key + * + * @param patternKey key + * @param page 页码 + * @param size 每页数目 + * @return / + */ + public List findKeysForPage(String patternKey, int page, int size) { + ScanOptions options = ScanOptions.scanOptions().match(patternKey).build(); + RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); + RedisConnection rc = Objects.requireNonNull(factory).getConnection(); + Cursor cursor = rc.scan(options); + List result = new ArrayList<>(size); + int tmpIndex = 0; + int fromIndex = page * size; + int toIndex = page * size + size; + while (cursor.hasNext()) { + if (tmpIndex >= fromIndex && tmpIndex < toIndex) { + result.add(new String(cursor.next())); + tmpIndex++; + continue; + } + // 获取到满足条件的数据后,就可以退出了 + if (tmpIndex >= toIndex) { + break; + } + tmpIndex++; + cursor.next(); + } + try { + RedisConnectionUtils.releaseConnection(rc, factory); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + /** + * 判断key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public boolean hasKey(String key) { + try { + return redisTemplate.hasKey(key); + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 删除缓存 + * + * @param key 可以传一个值 或多个 + */ + public void del(String... keys) { + if (keys != null && keys.length > 0) { + if (keys.length == 1) { + boolean result = redisTemplate.delete(keys[0]); + log.debug("--------------------------------------------"); + log.debug(new StringBuilder("删除缓存:").append(keys[0]).append(",结果:").append(result).toString()); + log.debug("--------------------------------------------"); + } else { + Set keySet = new HashSet<>(); + for (String key : keys) { + keySet.addAll(redisTemplate.keys(key)); + } + long count = redisTemplate.delete(keySet); + log.debug("--------------------------------------------"); + log.debug("成功删除缓存:" + keySet.toString()); + log.debug("缓存删除数量:" + count + "个"); + log.debug("--------------------------------------------"); + } + } + } + + // ============================String============================= + + /** + * 普通缓存获取 + * + * @param key 键 + * @return 值 + */ + public Object get(String key) { + return key == null ? null : redisTemplate.opsForValue().get(key); + } + + /** + * 批量获取 + * + * @param keys + * @return + */ + public List multiGet(List keys) { + List list = redisTemplate.opsForValue().multiGet(Sets.newHashSet(keys)); + List resultList = Lists.newArrayList(); + Optional.ofNullable(list).ifPresent(e -> list.forEach(ele -> Optional.ofNullable(ele).ifPresent(resultList::add))); + return resultList; + } + + /** + * 普通缓存放入 + * + * @param key 键 + * @param value 值 + * @return true成功 false失败 + */ + public boolean set(String key, Object value) { + try { + redisTemplate.opsForValue().set(key, value); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 普通缓存放入并设置时间 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 + * @return true成功 false 失败 + */ + public boolean set(String key, Object value, long time) { + try { + if (time > 0) { + redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); + } else { + set(key, value); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 普通缓存放入并设置时间 + * + * @param key 键 + * @param value 值 + * @param time 时间 + * @param timeUnit 类型 + * @return true成功 false 失败 + */ + public boolean set(String key, Object value, long time, TimeUnit timeUnit) { + try { + if (time > 0) { + redisTemplate.opsForValue().set(key, value, time, timeUnit); + } else { + set(key, value); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + // ================================Map================================= + + /** + * HashGet + * + * @param key 键 不能为null + * @param item 项 不能为null + * @return 值 + */ + public Object hget(String key, String item) { + return redisTemplate.opsForHash().get(key, item); + } + + /** + * 获取hashKey对应的所有键值 + * + * @param key 键 + * @return 对应的多个键值 + */ + public Map hmget(String key) { + return redisTemplate.opsForHash().entries(key); + + } + + /** + * HashSet + * + * @param key 键 + * @param map 对应多个键值 + * @return true 成功 false 失败 + */ + public boolean hmset(String key, Map map) { + try { + redisTemplate.opsForHash().putAll(key, map); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * HashSet 并设置时间 + * + * @param key 键 + * @param map 对应多个键值 + * @param time 时间(秒) + * @return true成功 false失败 + */ + public boolean hmset(String key, Map map, long time) { + try { + redisTemplate.opsForHash().putAll(key, map); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param item 项 + * @param value 值 + * @return true 成功 false失败 + */ + public boolean hset(String key, String item, Object value) { + try { + redisTemplate.opsForHash().put(key, item, value); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param item 项 + * @param value 值 + * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 + * @return true 成功 false失败 + */ + public boolean hset(String key, String item, Object value, long time) { + try { + redisTemplate.opsForHash().put(key, item, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 删除hash表中的值 + * + * @param key 键 不能为null + * @param item 项 可以使多个 不能为null + */ + public void hdel(String key, Object... item) { + redisTemplate.opsForHash().delete(key, item); + } + + /** + * 判断hash表中是否有该项的值 + * + * @param key 键 不能为null + * @param item 项 不能为null + * @return true 存在 false不存在 + */ + public boolean hHasKey(String key, String item) { + return redisTemplate.opsForHash().hasKey(key, item); + } + + /** + * hash递增 如果不存在,就会创建一个 并把新增后的值返回 + * + * @param key 键 + * @param item 项 + * @param by 要增加几(大于0) + * @return + */ + public double hincr(String key, String item, double by) { + return redisTemplate.opsForHash().increment(key, item, by); + } + + /** + * hash递减 + * + * @param key 键 + * @param item 项 + * @param by 要减少记(小于0) + * @return + */ + public double hdecr(String key, String item, double by) { + return redisTemplate.opsForHash().increment(key, item, -by); + } + + // ============================set============================= + + /** + * 根据key获取Set中的所有值 + * + * @param key 键 + * @return + */ + public Set sGet(String key) { + try { + return redisTemplate.opsForSet().members(key); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + /** + * 根据value从一个set中查询,是否存在 + * + * @param key 键 + * @param value 值 + * @return true 存在 false不存在 + */ + public boolean sHasKey(String key, Object value) { + try { + return redisTemplate.opsForSet().isMember(key, value); + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 将数据放入set缓存 + * + * @param key 键 + * @param values 值 可以是多个 + * @return 成功个数 + */ + public long sSet(String key, Object... values) { + try { + return redisTemplate.opsForSet().add(key, values); + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0; + } + } + + /** + * 将set数据放入缓存 + * + * @param key 键 + * @param time 时间(秒) + * @param values 值 可以是多个 + * @return 成功个数 + */ + public long sSetAndTime(String key, long time, Object... values) { + try { + Long count = redisTemplate.opsForSet().add(key, values); + if (time > 0) { + expire(key, time); + } + return count; + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0; + } + } + + /** + * 获取set缓存的长度 + * + * @param key 键 + * @return + */ + public long sGetSetSize(String key) { + try { + return redisTemplate.opsForSet().size(key); + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0; + } + } + + /** + * 移除值为value的 + * + * @param key 键 + * @param values 值 可以是多个 + * @return 移除的个数 + */ + public long setRemove(String key, Object... values) { + try { + Long count = redisTemplate.opsForSet().remove(key, values); + return count; + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0; + } + } + + // ===============================list================================= + + /** + * 获取list缓存的内容 + * + * @param key 键 + * @param start 开始 + * @param end 结束 0 到 -1代表所有值 + * @return + */ + public List lGet(String key, long start, long end) { + try { + return redisTemplate.opsForList().range(key, start, end); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + /** + * 获取list缓存的长度 + * + * @param key 键 + * @return + */ + public long lGetListSize(String key) { + try { + return redisTemplate.opsForList().size(key); + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0; + } + } + + /** + * 通过索引 获取list中的值 + * + * @param key 键 + * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 + * @return + */ + public Object lGetIndex(String key, long index) { + try { + return redisTemplate.opsForList().index(key, index); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @return + */ + public boolean lSet(String key, Object value) { + try { + redisTemplate.opsForList().rightPush(key, value); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) + * @return + */ + public boolean lSet(String key, Object value, long time) { + try { + redisTemplate.opsForList().rightPush(key, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @return + */ + public boolean lSet(String key, List value) { + try { + redisTemplate.opsForList().rightPushAll(key, value); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) + * @return + */ + public boolean lSet(String key, List value, long time) { + try { + redisTemplate.opsForList().rightPushAll(key, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 根据索引修改list中的某条数据 + * + * @param key 键 + * @param index 索引 + * @param value 值 + * @return / + */ + public boolean lUpdateIndex(String key, long index, Object value) { + try { + redisTemplate.opsForList().set(key, index, value); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 移除N个值为value + * + * @param key 键 + * @param count 移除多少个 + * @param value 值 + * @return 移除的个数 + */ + public long lRemove(String key, long count, Object value) { + try { + return redisTemplate.opsForList().remove(key, count, value); + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0; + } + } + + /** + * @param prefix 前缀 + * @param ids id + */ + public void delByKeys(String prefix, Set ids) { + Set keys = new HashSet<>(); + for (Long id : ids) { + keys.addAll(redisTemplate.keys(new StringBuffer(prefix).append(id).toString())); + } + long count = redisTemplate.delete(keys); + // 此处提示可自行删除 + log.debug("--------------------------------------------"); + log.debug("成功删除缓存:" + keys.toString()); + log.debug("缓存删除数量:" + count + "个"); + log.debug("--------------------------------------------"); + } + + + /** + * 存储经纬度 + * + * @param key id + * @param point 经维度 + * @param name 名称 + */ + public void set(String key, RedisGeoCommands.GeoLocation geoLocation) { + redisTemplate.opsForGeo().add(key, geoLocation); + } + + /** + * 两个人之间的距离 + * + * @param geoKey + * @param key1 + * @param key2 + * @return + */ + public Double distance(String geoKey, String key1, String key2) { + Distance distance = redisTemplate.opsForGeo().distance(geoKey, key1, key2); + return distance.getValue(); + } + + /** + * 查询距离某个人指定范围内的人 + * + * @param geoKey + * @param key + * @param distance + * @return + */ + public GeoResults> radius(String geoKey, String key, Distance distance) { + RedisGeoCommands.GeoRadiusCommandArgs geoRadiusCommandArgs = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs(); + GeoResults> radius = redisTemplate.opsForGeo().radius(geoKey, key, distance, geoRadiusCommandArgs.includeDistance()); + return radius; + } +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/util/RsaUtil.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/RsaUtil.java new file mode 100644 index 0000000..345b34a --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/RsaUtil.java @@ -0,0 +1,248 @@ +package com.baiye.util; + +import cn.hutool.json.JSONUtil; +import javax.crypto.Cipher; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.security.*; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.TreeMap; + +/** + * @author Enzo + * @date : 2023/5/9 RSA算法加密/解密和签名/验签工具类 生成密钥对(公钥和私钥) 加密内容与签名内容进行Base64加密解密(有利于HTTP协议下传输) + */ +public class RsaUtil { + + private RsaUtil() { + } + + /** + * 算法名称 + */ + private static final String ALGORITHM = "RSA"; + + /** + * 签名算法 MD5withRSA 或 SHA1WithRSA 等 + */ + public static final String SIGNATURE_ALGORITHM = "MD5withRSA"; + + /** + * 密钥长度默认是1024位: 加密的明文最大长度 = 密钥长度 - 11(单位是字节,即byte) + */ + private static final int KEY_SIZE = 1024; + + /** + * RSA最大加密明文大小 + */ + private static final int MAX_ENCRYPT_BLOCK = 117; + + /** + * RSA最大解密密文大小 + */ + private static final int MAX_DECRYPT_BLOCK = 128; + + /** + * 获取密钥对 + * @return 密钥对 + */ + public static KeyPair getKeyPair() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance(ALGORITHM); + generator.initialize(KEY_SIZE); + return generator.generateKeyPair(); + } + + /** + * 私钥字符串转PrivateKey实例 + * @param privateKey 私钥字符串 + * @return + */ + public static PrivateKey getPrivateKey(String privateKey) throws Exception { + KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); + // 对私钥进行Base64编码解密 + byte[] decodedKey = Base64.getDecoder().decode(privateKey.getBytes(StandardCharsets.UTF_8)); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey); + return keyFactory.generatePrivate(keySpec); + } + + /** + * 公钥字符串转PublicKey实例 + * @param publicKey 公钥字符串 + * @return + */ + public static PublicKey getPublicKey(String publicKey) throws Exception { + KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); + // 对公钥进行Base64编码解密 + byte[] decodedKey = Base64.getDecoder().decode(publicKey.getBytes(StandardCharsets.UTF_8)); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey); + return keyFactory.generatePublic(keySpec); + } + + /** + * 公钥加密 + * @param data 待加密数据 + * @param publicKey 公钥 + * @return + */ + public static String encryptByPublicKey(String data, PublicKey publicKey) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream();) { + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + int inputLen = data.getBytes(StandardCharsets.UTF_8).length; + int offset = 0; + byte[] cache; + int i = 0; + // 对数据分段加密 + while (inputLen - offset > 0) { + if (inputLen - offset > MAX_ENCRYPT_BLOCK) { + cache = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8), offset, MAX_ENCRYPT_BLOCK); + } + else { + cache = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8), offset, inputLen - offset); + } + out.write(cache, 0, cache.length); + i++; + offset = i * MAX_ENCRYPT_BLOCK; + } + byte[] encryptedData = out.toByteArray(); + // 获取加密内容使用Base64进行编码加密,并以UTF-8为标准转化成字符串 + // 加密后的字符串 + return new String(Base64.getEncoder().encode(encryptedData), StandardCharsets.UTF_8); + } + catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 私钥解密 + * @param data 待解密数据 + * @param privateKey 私钥 + * @return + */ + public static String decryptByPrivateKey(String data, PrivateKey privateKey) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + // 对待解密数据进行Base64编码解密 + byte[] dataBytes = Base64.getDecoder().decode(data.getBytes(StandardCharsets.UTF_8)); + int inputLen = dataBytes.length; + int offset = 0; + byte[] cache; + int i = 0; + // 对数据分段解密 + while (inputLen - offset > 0) { + if (inputLen - offset > MAX_DECRYPT_BLOCK) { + cache = cipher.doFinal(dataBytes, offset, MAX_DECRYPT_BLOCK); + } + else { + cache = cipher.doFinal(dataBytes, offset, inputLen - offset); + } + out.write(cache, 0, cache.length); + i++; + offset = i * MAX_DECRYPT_BLOCK; + } + // 解密后的内容 + return out.toString(StandardCharsets.UTF_8.toString()); + } + catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 私钥签名 + * @param data 待签名数据 + * @param privateKey 私钥 + * @return 签名 + */ + public static String sign(String data, PrivateKey privateKey) throws Exception { + byte[] keyBytes = privateKey.getEncoded(); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); + PrivateKey key = keyFactory.generatePrivate(keySpec); + Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); + signature.initSign(key); + signature.update(data.getBytes()); + // 对签名内容进行Base64编码加密 + return new String(Base64.getEncoder().encode(signature.sign())); + } + + /** + * 公钥验签 + * @param srcData 原始字符串 + * @param publicKey 公钥 + * @param sign 签名 + * @return 是否验签通过 + */ + public static boolean verify(String srcData, PublicKey publicKey, String sign) throws Exception { + byte[] keyBytes = publicKey.getEncoded(); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); + PublicKey key = keyFactory.generatePublic(keySpec); + Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); + signature.initVerify(key); + signature.update(srcData.getBytes()); + // 对验签结果进行Base64编码解密 + return signature.verify(Base64.getDecoder().decode(sign.getBytes())); + } + + private static TreeMap getParam() { + List list = new ArrayList<>(); + list.add("c"); + list.add("b"); + list.add("c"); + list.add("d"); + TreeMap map = new TreeMap<>(); + map.put("xxx", "10086"); + map.put("mobile", "18888888888"); + map.put("params", list); + return map; + } + + /** + * + */ + private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYvp89M7nUvuEofkpk50X84CTgSBMJS5SYXEYFGyJ7xcdN81IHczlKxrLw9jkzz7LJ2PHb/mYM9pFEhA2GllilLPlQJJVTqNLDsc0TGlH8B0QTdxKvTpKomw3q2H+llHb9ZNQSbF0iHU5c/Juq7VWzxdoc/Mf5GMiRc4TNiCkyEwIDAQAB"; + + /** + * + */ + public static final String PRIVATE_KEY = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJi+nz0zudS+4Sh+SmTnRfzgJOBIEwlLlJhcRgUbInvFx03zUgdzOUrGsvD2OTPPssnY8dv+Zgz2kUSEDYaWWKUs+VAklVOo0sOxzRMaUfwHRBN3Eq9OkqibDerYf6WUdv1k1BJsXSIdTlz8m6rtVbPF2hz8x/kYyJFzhM2IKTITAgMBAAECgYAgTZAXvWy7lXLAwZSyKkce57hkxllgSd+vKTSVt9tfGcDAt4jNkoy3R7ZoR2ppjq9dCMh9ohuq+ipWtya1I+6zC5sflk9HI/rf+5bq3JRJvxq3EJYe5DlSjQitLUMRP6PQorHnZZj/bdqKgRvrulI8XtK5Fv9Cd4jhkbSZtzgpYQJBAOJu0nu4qJCqIYLCmFWDpRzi9cu8/TFCBLVDH0xhNi28JL6G8xOfdzxsQa8ZlLOxPwn56VbS3+Korq34WCOAG5cCQQCssI4I7dshlA7kXeurVSFvui1YV/7ofFOxRs019+V88tfwNby5TAS9YjX7AuvGkobjpBBNEkE0JExf69m6VBzlAkB5te4HuLNKx1gp7CVr2c43n7tVHynNf1n+gKzjJmGz5ayuiOVBx/aUkPAhiZOHnx9uYlnNZJ4ZPGhgdNwTgPnTAkB61pQSMe/AMOtu8ogjNck1CoAa6W0/vsBhx/VNQGsTuEJ2ciMuw65TcLrpNKi2daBR6XBXAnczOebCDKix7AcpAkEArrU+bx6GwR+UbgoNAl1KwFLrV50pasK7Mlp0BkHM0cz4BGB53O5ng+TJHzen03OgC9I1W1WFAYHj03lb84qM/w=="; + + public static void main(String[] args) { + try { + // 生成密钥对 + KeyPair keyPair = getKeyPair(); + String privateKey = new String(Base64.getEncoder().encode(keyPair.getPrivate().getEncoded()), + StandardCharsets.UTF_8); + String publicKey = new String(Base64.getEncoder().encode(keyPair.getPublic().getEncoded()), + StandardCharsets.UTF_8); + TreeMap param = getParam(); + String encryptData = encryptByPublicKey(JSONUtil.toJsonStr(param), getPublicKey(publicKey)); + System.out.println("加密后内容:" + encryptData); + // RSA解密 + String decryptData = decryptByPrivateKey(encryptData, getPrivateKey(privateKey)); + System.out.println("解密后内容:" + decryptData); + + // RSA签名 + String sign = sign(JSONUtil.toJsonStr(param), getPrivateKey(privateKey)); + System.out.println("签名内容:" + sign); + // RSA验签 + boolean result = verify(JSONUtil.toJsonStr(param), getPublicKey(publicKey), sign); + System.out.print("验签结果:" + result); + + } + catch (Exception e) { + e.printStackTrace(); + System.out.print("加密解密异常"); + } + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/util/WebUtils.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/WebUtils.java new file mode 100644 index 0000000..72e5947 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/util/WebUtils.java @@ -0,0 +1,45 @@ +package com.baiye.util; + +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; + +/** + * WebUtils + * + * @author Hccake + */ +@Slf4j +@UtilityClass +public class WebUtils extends org.springframework.web.util.WebUtils { + + /** + * 获取 ServletRequestAttributes + * @return {ServletRequestAttributes} + */ + public ServletRequestAttributes getServletRequestAttributes() { + return (ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes()); + } + + /** + * 获取 HttpServletRequest + * @return {HttpServletRequest} + */ + public HttpServletRequest getRequest() { + return getServletRequestAttributes().getRequest(); + } + + /** + * 获取 HttpServletResponse + * @return {HttpServletResponse} + */ + public HttpServletResponse getResponse() { + return getServletRequestAttributes().getResponse(); + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/EmptyCurlyToDefaultMessageInterpolator.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/EmptyCurlyToDefaultMessageInterpolator.java new file mode 100644 index 0000000..553cb81 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/EmptyCurlyToDefaultMessageInterpolator.java @@ -0,0 +1,58 @@ +package com.baiye.validation; + +import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator; +import org.hibernate.validator.spi.resourceloading.ResourceBundleLocator; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Locale; + +/** + * 将消息中空的花括号替换为校验注解的默认值 + *

      + * 扩展自原有的 {@link ResourceBundleMessageInterpolator} 消息处理器 + * + * @author hccake + */ +public class EmptyCurlyToDefaultMessageInterpolator extends ResourceBundleMessageInterpolator { + + private static final String EMPTY_CURLY_BRACES = "{}"; + + public EmptyCurlyToDefaultMessageInterpolator() { + } + + public EmptyCurlyToDefaultMessageInterpolator(ResourceBundleLocator userResourceBundleLocator) { + super(userResourceBundleLocator); + } + + @Override + public String interpolate(String message, Context context, Locale locale) { + + // 如果包含花括号占位符 + if (message.contains(EMPTY_CURLY_BRACES)) { + // 获取注解类型 + Class annotationType = context.getConstraintDescriptor() + .getAnnotation() + .annotationType(); + + Method messageMethod; + try { + messageMethod = annotationType.getDeclaredMethod("message"); + } catch (NoSuchMethodException e) { + return super.interpolate(message, context, locale); + } + + // 找到对应 message 的默认值,将 {} 替换为默认值 + if (messageMethod.getDefaultValue() != null) { + Object defaultValue = messageMethod.getDefaultValue(); + if (defaultValue instanceof String) { + String defaultMessage = (String) defaultValue; + message = message.replace(EMPTY_CURLY_BRACES, defaultMessage); + } + } + } + + return super.interpolate(message, context, locale); + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/constraints/OneOfClasses.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/constraints/OneOfClasses.java new file mode 100644 index 0000000..ba5f9fc --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/constraints/OneOfClasses.java @@ -0,0 +1,48 @@ +package com.baiye.validation.constraints; + +import com.baiye.validation.validator.EnumValueValidatorOfClass; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author housl + * @version 1.0 + */ +@Target({METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +@Repeatable(OneOfClasses.List.class) +@Documented +@Constraint(validatedBy = {EnumValueValidatorOfClass.class}) +public @interface OneOfClasses { + + String message() default "value must match one of the values in the list: {value}"; + + Class[] value(); + + /** + * 允许值为 null, 默认不允许 + */ + boolean allowNull() default false; + + Class[] groups() default {}; + + Class[] payload() default {}; + + @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) + @Retention(RUNTIME) + @Documented + @interface List { + + OneOfClasses[] value(); + + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/constraints/OneOfInts.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/constraints/OneOfInts.java new file mode 100644 index 0000000..72e4576 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/constraints/OneOfInts.java @@ -0,0 +1,48 @@ +package com.baiye.validation.constraints; + +import com.baiye.validation.validator.EnumValueValidatorOfInt; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author housl + * @version 1.0 + */ +@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(OneOfInts.List.class) +@Documented +@Constraint(validatedBy = { EnumValueValidatorOfInt.class }) +public @interface OneOfInts { + + String message() default "value must match one of the values in the list: {value}"; + + int[] value(); + + /** + * 允许值为 null, 默认不允许 + */ + boolean allowNull() default false; + + Class[] groups() default {}; + + Class[] payload() default {}; + + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @Retention(RUNTIME) + @Documented + @interface List { + + OneOfInts[] value(); + + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/constraints/OneOfStrings.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/constraints/OneOfStrings.java new file mode 100644 index 0000000..9a16c64 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/constraints/OneOfStrings.java @@ -0,0 +1,48 @@ +package com.baiye.validation.constraints; + +import com.baiye.validation.validator.EnumValueValidatorOfString; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author housl + * @version 1.0 + */ +@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(OneOfStrings.List.class) +@Documented +@Constraint(validatedBy = { EnumValueValidatorOfString.class }) +public @interface OneOfStrings { + + String message() default "value must match one of the values in the list: {value}"; + + String[] value(); + + /** + * 允许值为 null, 默认不允许 + */ + boolean allowNull() default false; + + Class[] groups() default {}; + + Class[] payload() default {}; + + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @Retention(RUNTIME) + @Documented + @interface List { + + OneOfStrings[] value(); + + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/constraints/ValueOfEnum.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/constraints/ValueOfEnum.java new file mode 100644 index 0000000..23e329a --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/constraints/ValueOfEnum.java @@ -0,0 +1,50 @@ +package com.baiye.validation.constraints; + +import com.baiye.validation.validator.ValueOfEnumValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author housl + * @version 1.0 + */ +@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(ValueOfEnum.List.class) +@Documented +@Constraint(validatedBy = { ValueOfEnumValidator.class }) +public @interface ValueOfEnum { + + String message() default "value is not in enum {enumClass}"; + + Class[] enumClass(); + + String method() default "valueOf"; + + /** + * 允许值为 null, 默认不允许 + */ + boolean allowNull() default false; + + Class[] groups() default {}; + + Class[] payload() default {}; + + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @Retention(RUNTIME) + @Documented + @interface List { + + ValueOfEnum[] value(); + + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/group/CreateGroup.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/group/CreateGroup.java new file mode 100644 index 0000000..d4127aa --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/group/CreateGroup.java @@ -0,0 +1,10 @@ +package com.baiye.validation.group; + +/** + * Validation Group,新建时校验 + * + * @author hccake + */ +public interface CreateGroup { + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/group/SubmitGroup.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/group/SubmitGroup.java new file mode 100644 index 0000000..07964ee --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/group/SubmitGroup.java @@ -0,0 +1,7 @@ +package com.baiye.validation.group; + +/** + * Validation Group,提交时校验 + */ +public interface SubmitGroup { +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/group/UpdateGroup.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/group/UpdateGroup.java new file mode 100644 index 0000000..20da1e3 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/group/UpdateGroup.java @@ -0,0 +1,10 @@ +package com.baiye.validation.group; + +/** + * Validation Group,更新时校验 + * + * @author hccake + */ +public interface UpdateGroup { + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/validator/EnumValueValidatorOfClass.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/validator/EnumValueValidatorOfClass.java new file mode 100644 index 0000000..8479330 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/validator/EnumValueValidatorOfClass.java @@ -0,0 +1,37 @@ +package com.baiye.validation.validator; + +import com.baiye.validation.constraints.OneOfClasses; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @author hccake + * @version 1.0 枚举值 Validator + */ +public class EnumValueValidatorOfClass implements ConstraintValidator> { + + private Class[] classList; + + private boolean allowNull; + + @Override + public void initialize(OneOfClasses constraintAnnotation) { + classList = constraintAnnotation.value(); + allowNull = constraintAnnotation.allowNull(); + } + + @Override + public boolean isValid(Class value, ConstraintValidatorContext context) { + if (value == null) { + return allowNull; + } + for (Class clazz : classList) { + if (clazz.isAssignableFrom(value)) { + return true; + } + } + return false; + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/validator/EnumValueValidatorOfInt.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/validator/EnumValueValidatorOfInt.java new file mode 100644 index 0000000..f41bfb3 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/validator/EnumValueValidatorOfInt.java @@ -0,0 +1,37 @@ +package com.baiye.validation.validator; + +import com.baiye.validation.constraints.OneOfInts; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @author housl + * @version 1.0 枚举值 Validator + */ +public class EnumValueValidatorOfInt implements ConstraintValidator { + + private int[] ints; + + private boolean allowNull; + + @Override + public void initialize(OneOfInts constraintAnnotation) { + ints = constraintAnnotation.value(); + allowNull = constraintAnnotation.allowNull(); + } + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + if (value == null) { + return allowNull; + } + for (int anInt : ints) { + if (anInt == value) { + return true; + } + } + return false; + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/validator/EnumValueValidatorOfString.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/validator/EnumValueValidatorOfString.java new file mode 100644 index 0000000..c1a4f5e --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/validator/EnumValueValidatorOfString.java @@ -0,0 +1,37 @@ +package com.baiye.validation.validator; + +import com.baiye.validation.constraints.OneOfStrings; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @author housl + * @version 1.0 枚举值 Validator + */ +public class EnumValueValidatorOfString implements ConstraintValidator { + + private String[] stringList; + + private boolean allowNull; + + @Override + public void initialize(OneOfStrings constraintAnnotation) { + stringList = constraintAnnotation.value(); + allowNull = constraintAnnotation.allowNull(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return allowNull; + } + for (String strValue : stringList) { + if (strValue.equals(value)) { + return true; + } + } + return false; + } + +} diff --git a/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/validator/ValueOfEnumValidator.java b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/validator/ValueOfEnumValidator.java new file mode 100644 index 0000000..73f6404 --- /dev/null +++ b/marketing-scrm-common/common-core/src/main/java/com/baiye/validation/validator/ValueOfEnumValidator.java @@ -0,0 +1,58 @@ +package com.baiye.validation.validator; + +import com.baiye.validation.constraints.ValueOfEnum; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ClassUtils; +import org.apache.commons.lang3.reflect.MethodUtils; + +import javax.validation.ConstraintDefinitionException; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @author housl + * @version 1.0 枚举类 Validator + */ +@Slf4j +public class ValueOfEnumValidator implements ConstraintValidator { + + private Class[] targetEnum; + + private String checkMethod; + + private boolean allowNull; + + @Override + public void initialize(ValueOfEnum constraintAnnotation) { + targetEnum = constraintAnnotation.enumClass(); + checkMethod = constraintAnnotation.method(); + allowNull = constraintAnnotation.allowNull(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + if (value == null) { + return allowNull; + } + for (Class eClass : targetEnum) { + // 包装类和原始类型的处理 + Class clazz = ClassUtils.isPrimitiveWrapper(value.getClass()) + ? ClassUtils.wrapperToPrimitive(value.getClass()) : value.getClass(); + + try { + Object enumInstance = MethodUtils.invokeStaticMethod(eClass, checkMethod, new Object[] { value }, + new Class[] { clazz }); + return enumInstance != null; + } + catch (NoSuchMethodException ex) { + throw new ConstraintDefinitionException(ex); + } + catch (Exception ex) { + log.warn("check enum error: ", ex); + return false; + } + } + return false; + } + +} diff --git a/marketing-scrm-common/common-desensitize/pom.xml b/marketing-scrm-common/common-desensitize/pom.xml new file mode 100644 index 0000000..e2848f8 --- /dev/null +++ b/marketing-scrm-common/common-desensitize/pom.xml @@ -0,0 +1,22 @@ + + + + marketing-scrm-common + com.baiye + ${revision} + + 4.0.0 + common-desensitize + + + + cn.hutool + hutool-core + + + com.fasterxml.jackson.core + jackson-databind + + + diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/AnnotationHandlerHolder.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/AnnotationHandlerHolder.java new file mode 100644 index 0000000..27de361 --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/AnnotationHandlerHolder.java @@ -0,0 +1,100 @@ +package com.baiye; + +import cn.hutool.core.lang.Assert; +import com.baiye.functions.DesensitizeFunction; +import com.baiye.enums.RegexDesensitizationTypeEnum; +import com.baiye.enums.SlideDesensitizationTypeEnum; +import com.baiye.handler.RegexDesensitizationHandler; +import com.baiye.handler.SimpleDesensitizationHandler; +import com.baiye.handler.SlideDesensitizationHandler; +import com.baiye.json.annotation.JsonRegexDesensitize; +import com.baiye.json.annotation.JsonSimpleDesensitize; +import com.baiye.json.annotation.JsonSlideDesensitize; + +import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 注解处理方法 脱敏注解->处理逻辑 + * + * @author Yakir + */ +public final class AnnotationHandlerHolder { + + private static final AnnotationHandlerHolder INSTANCE = new AnnotationHandlerHolder(); + + /** + * 注解类型 处理函数映射 + */ + private final Map, DesensitizeFunction> annotationHandlers; + + private AnnotationHandlerHolder() { + annotationHandlers = new ConcurrentHashMap<>(); + + annotationHandlers.put(JsonSimpleDesensitize.class, (annotation, value) -> { + // Simple 类型处理 + JsonSimpleDesensitize an = (JsonSimpleDesensitize) annotation; + Class handlerClass = an.handler(); + SimpleDesensitizationHandler desensitizationHandler = DesensitizationHandlerHolder + .getSimpleHandler(handlerClass); + Assert.notNull(desensitizationHandler, "SimpleDesensitizationHandler can not be Null"); + return desensitizationHandler.handle(value); + }); + + annotationHandlers.put(JsonRegexDesensitize.class, (annotation, value) -> { + // 正则类型脱敏处理 + JsonRegexDesensitize an = (JsonRegexDesensitize) annotation; + RegexDesensitizationTypeEnum type = an.type(); + RegexDesensitizationHandler regexDesensitizationHandler = DesensitizationHandlerHolder + .getRegexDesensitizationHandler(); + return RegexDesensitizationTypeEnum.CUSTOM.equals(type) + ? regexDesensitizationHandler.handle(value, an.regex(), an.replacement()) + : regexDesensitizationHandler.handle(value, type); + }); + + annotationHandlers.put(JsonSlideDesensitize.class, (annotation, value) -> { + // 滑动类型脱敏处理 + JsonSlideDesensitize an = (JsonSlideDesensitize) annotation; + SlideDesensitizationTypeEnum type = an.type(); + SlideDesensitizationHandler slideDesensitizationHandler = DesensitizationHandlerHolder + .getSlideDesensitizationHandler(); + return SlideDesensitizationTypeEnum.CUSTOM.equals(type) ? slideDesensitizationHandler.handle(value, + an.leftPlainTextLen(), an.rightPlainTextLen(), an.maskString()) + : slideDesensitizationHandler.handle(value, type); + }); + } + + /** + * 得到注解类型处理函数 + * @param annotationType {@code annotationType} 注解类型 + * @return {@link DesensitizeFunction}脱敏处理函数|null + */ + public static DesensitizeFunction getHandleFunction(Class annotationType) { + return INSTANCE.annotationHandlers.get(annotationType); + } + + /** + * 添加注解处理函数 + * @param annotationType {@code annotationType}指定注解类型 + * @param desensitizeFunction {@link DesensitizeFunction}指定脱敏处理函数 + * @return 上一个key 对应的脱敏处理函数 | null + */ + public static DesensitizeFunction addHandleFunction(Class annotationType, + DesensitizeFunction desensitizeFunction) { + Assert.notNull(annotationType, "annotation cannot be null"); + Assert.notNull(desensitizeFunction, "desensitization function cannot be null"); + // 加入注解处理映射 + return INSTANCE.annotationHandlers.put(annotationType, desensitizeFunction); + } + + /** + * 得到当前支持的注解处理类 + * @return {@code 注解处理类列表} + */ + public static Set> getAnnotationClasses() { + return INSTANCE.annotationHandlers.keySet(); + } + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/DesensitizationHandlerHolder.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/DesensitizationHandlerHolder.java new file mode 100644 index 0000000..333ba48 --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/DesensitizationHandlerHolder.java @@ -0,0 +1,87 @@ +package com.baiye; + +import com.baiye.handler.DesensitizationHandler; +import com.baiye.handler.RegexDesensitizationHandler; +import com.baiye.handler.SimpleDesensitizationHandler; +import com.baiye.handler.SlideDesensitizationHandler; + +import java.util.Map; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 脱敏处理器持有者 + *

      + * - 默认提供 Regex 和 Slide 类型的脱敏处理器
      + * - Simple 脱敏处理器则使用SPI方式加载,便于用户扩展处理 + *

      + * + * @author Hccake 2021/1/22 + * @version 1.0 + */ +public final class DesensitizationHandlerHolder { + + private static final DesensitizationHandlerHolder INSTANCE = new DesensitizationHandlerHolder(); + + private final Map, DesensitizationHandler> desensitizationHandlerMap; + + private DesensitizationHandlerHolder() { + desensitizationHandlerMap = new ConcurrentHashMap<>(16); + // 滑动脱敏处理器 + desensitizationHandlerMap.put(SlideDesensitizationHandler.class, new SlideDesensitizationHandler()); + // 正则脱敏处理器 + desensitizationHandlerMap.put(RegexDesensitizationHandler.class, new RegexDesensitizationHandler()); + // SPI 加载所有的 Simple脱敏类型处理 + ServiceLoader loadedDrivers = ServiceLoader + .load(SimpleDesensitizationHandler.class); + for (SimpleDesensitizationHandler desensitizationHandler : loadedDrivers) { + desensitizationHandlerMap.put(desensitizationHandler.getClass(), desensitizationHandler); + } + } + + /** + * 获取 DesensitizationHandler + * @return 处理器实例 + */ + public static DesensitizationHandler getHandler(Class handlerClass) { + return INSTANCE.desensitizationHandlerMap.get(handlerClass); + } + + /** + * 获取 RegexDesensitizationHandler + * @return 处理器实例 + */ + public static RegexDesensitizationHandler getRegexDesensitizationHandler() { + return (RegexDesensitizationHandler) INSTANCE.desensitizationHandlerMap.get(RegexDesensitizationHandler.class); + } + + /** + * 获取 SlideDesensitizationHandler + * @return 处理器实例 + */ + public static SlideDesensitizationHandler getSlideDesensitizationHandler() { + return (SlideDesensitizationHandler) INSTANCE.desensitizationHandlerMap.get(SlideDesensitizationHandler.class); + } + + /** + * 获取指定的 SimpleDesensitizationHandler + * @param handlerClass SimpleDesensitizationHandler的实现类 + * @return 处理器实例 + */ + public static SimpleDesensitizationHandler getSimpleHandler( + Class handlerClass) { + return (SimpleDesensitizationHandler) INSTANCE.desensitizationHandlerMap.get(handlerClass); + } + + /** + * 添加Handler + * @param handlerClass DesensitizationHandler的实现类 + * @param handler 处理器实例 + * @return handler 处理器实例 + */ + public static DesensitizationHandler addHandler(Class handlerClass, + DesensitizationHandler handler) { + return INSTANCE.desensitizationHandlerMap.put(handlerClass, handler); + } + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/enums/RegexDesensitizationTypeEnum.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/enums/RegexDesensitizationTypeEnum.java new file mode 100644 index 0000000..e1e0877 --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/enums/RegexDesensitizationTypeEnum.java @@ -0,0 +1,39 @@ +package com.baiye.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * @author Hccake 2021/1/23 + * @version 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum RegexDesensitizationTypeEnum { + + /** + * 自定义类型 + */ + CUSTOM("^[\\s\\S]*$", "******"), + + /** + * 【邮箱】脱敏,保留邮箱第一个字符和'@'之后的原文显示,中间的显示为4个* eg. 12@qq.com -> 1****@qq.com + */ + EMAIL("(^.)[^@]*(@.*$)", "$1****$2"), + + /** + * 【对称密文的密码】脱敏,前3后2,中间替换为 4个 * + */ + ENCRYPTED_PASSWORD("(.{3}).*(.{2}$)", "$1****$2"); + + /** + * 匹配的正则表达式 + */ + private final String regex; + + /** + * 替换规则 + */ + private final String replacement; + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/enums/SlideDesensitizationTypeEnum.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/enums/SlideDesensitizationTypeEnum.java new file mode 100644 index 0000000..87bcc46 --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/enums/SlideDesensitizationTypeEnum.java @@ -0,0 +1,54 @@ +package com.baiye.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * @author Hccake 2021/1/23 + * @version 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum SlideDesensitizationTypeEnum { + + /** + * 自定义类型 + */ + CUSTOM(0, 0, "*"), + + /** + * 全部字符替换为 * + */ + ALL_ASTERISK(0, 0, "*"), + + /** + * 【银行卡号】, 前6位和后4位不脱敏,中间脱敏 eg. 330150******1234 + */ + BANK_CARD_NO(6, 4, "*"), + + /** + * 【身份证号】年月日脱敏,前6后4不脱敏 eg. 655356*******1234 + */ + ID_CARD_NO(6, 4, "*"), + + /** + * 【手机号】,某些国家手机号位数短,所以不做前三后四,使用前三后二 + */ + PHONE_NUMBER(3, 2, "*"); + + /** + * 左边的明文数 + */ + private final int leftPlainTextLen; + + /** + * 右边的明文数 + */ + private final int rightPlainTextLen; + + /** + * 剩余部分字符逐个替换的字符串 + */ + private final String maskString; + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/functions/DesensitizeFunction.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/functions/DesensitizeFunction.java new file mode 100644 index 0000000..3a6eac3 --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/functions/DesensitizeFunction.java @@ -0,0 +1,21 @@ +package com.baiye.functions; + +import java.lang.annotation.Annotation; + +/** + * 脱敏函数 + * + * @author Yakir + */ +@FunctionalInterface +public interface DesensitizeFunction { + + /** + * 脱敏函数 + * @param annotation 当前脱敏注解 + * @param value 原始值 + * @return 脱敏处理后的值 + */ + String desensitize(Annotation annotation, String value); + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/DesensitizationHandler.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/DesensitizationHandler.java new file mode 100644 index 0000000..519a8a9 --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/DesensitizationHandler.java @@ -0,0 +1,11 @@ +package com.baiye.handler; + +/** + * 脱敏处理器 + * + * @author Hccake 2021/1/22 + * @version 1.0 + */ +public interface DesensitizationHandler { + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/RegexDesensitizationHandler.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/RegexDesensitizationHandler.java new file mode 100644 index 0000000..14e2fd4 --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/RegexDesensitizationHandler.java @@ -0,0 +1,34 @@ +package com.baiye.handler; + +import com.baiye.enums.RegexDesensitizationTypeEnum; + +/** + * 正则替换脱敏处理器,使用正则匹配替换处理原数据 + * + * @author Hccake 2021/1/23 + * @version 1.0 + */ +public class RegexDesensitizationHandler implements DesensitizationHandler { + + /** + * 正则脱敏处理 + * @param origin 原文 + * @param regex 正则匹配规则 + * @param replacement 替换模板 + * @return 脱敏后的字符串 + */ + public String handle(String origin, String regex, String replacement) { + return origin.replaceAll(regex, replacement); + } + + /** + * 正则脱敏处理 + * @param origin 原文 + * @param typeEnum 正则脱敏枚举类型 + * @return 脱敏后的字符串 + */ + public String handle(String origin, RegexDesensitizationTypeEnum typeEnum) { + return origin.replaceAll(typeEnum.getRegex(), typeEnum.getReplacement()); + } + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/SimpleDesensitizationHandler.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/SimpleDesensitizationHandler.java new file mode 100644 index 0000000..fc326ca --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/SimpleDesensitizationHandler.java @@ -0,0 +1,18 @@ +package com.baiye.handler; + +/** + * 简单的脱敏处理器,传入源数据直接返回脱敏后的数据 + * + * @author Hccake 2021/1/23 + * @version 1.0 + */ +public interface SimpleDesensitizationHandler extends DesensitizationHandler { + + /** + * 脱敏处理 + * @param origin 原始字符串 + * @return 脱敏处理后的字符串 + */ + String handle(String origin); + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/SixAsteriskDesensitizationHandler.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/SixAsteriskDesensitizationHandler.java new file mode 100644 index 0000000..f64358e --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/SixAsteriskDesensitizationHandler.java @@ -0,0 +1,21 @@ +package com.baiye.handler; + +/** + * 【6*脱敏】,不管原文是什么,一律返回6个* eg. ****** + * + * @author Hccake 2021/1/22 + * @version 1.0 + */ +public class SixAsteriskDesensitizationHandler implements SimpleDesensitizationHandler { + + /** + * 脱敏处理 + * @param origin 原始字符串 + * @return 脱敏处理后的字符串 + */ + @Override + public String handle(String origin) { + return "******"; + } + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/SlideDesensitizationHandler.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/SlideDesensitizationHandler.java new file mode 100644 index 0000000..190750d --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/handler/SlideDesensitizationHandler.java @@ -0,0 +1,62 @@ +package com.baiye.handler; + +import com.baiye.enums.SlideDesensitizationTypeEnum; + +/** + * 滑动脱敏处理器,根据左右边界值滑动左右指针,中间处脱敏 + * + * @author Hccake 2021/1/23 + * @version 1.0 + */ +public class SlideDesensitizationHandler implements DesensitizationHandler { + + /** + * 滑动脱敏 + * @param origin 原文 + * @param leftPlainTextLen 处理完后左边的明文数 + * @param rightPlainTextLen 处理完后右边的明文数 + * @return 脱敏后的字符串 + */ + public String handle(String origin, int leftPlainTextLen, int rightPlainTextLen) { + return this.handle(origin, leftPlainTextLen, rightPlainTextLen, "*"); + } + + /** + * 滑动脱敏 + * @param origin 原文 + * @param leftPlainTextLen 处理完后左边的明文数 + * @param rightPlainTextLen 处理完后右边的明文数 + * @param maskString 原文窗口内每个字符被替换后的字符串 + * @return 脱敏后的字符串 + */ + public String handle(String origin, int leftPlainTextLen, int rightPlainTextLen, String maskString) { + if (origin == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + + char[] chars = origin.toCharArray(); + int length = chars.length; + for (int i = 0; i < length; i++) { + // 明文位内则明文显示 + if (i < leftPlainTextLen || i > (length - rightPlainTextLen - 1)) { + sb.append(chars[i]); + } + else { + sb.append(maskString); + } + } + return sb.toString(); + } + + /** + * 根据指定枚举类型进行滑动脱敏 + * @param value 原文 + * @param type 滑动脱敏类型 + * @return 脱敏后的字符串 + */ + public String handle(String value, SlideDesensitizationTypeEnum type) { + return this.handle(value, type.getLeftPlainTextLen(), type.getRightPlainTextLen(), type.getMaskString()); + } + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/DesensitizeStrategy.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/DesensitizeStrategy.java new file mode 100644 index 0000000..97c22a7 --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/DesensitizeStrategy.java @@ -0,0 +1,17 @@ +package com.baiye.json; + +/** + * 脱敏工具类 定义开启脱敏规则 + * + * @author Yakir + */ +public interface DesensitizeStrategy { + + /** + * 判断是否忽略字段 + * @param fieldName {@code 当前字段名称} + * @return @{code true 忽略 |false 不忽略} + */ + boolean ignoreField(String fieldName); + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/JsonDesensitizeModule.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/JsonDesensitizeModule.java new file mode 100644 index 0000000..8c95601 --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/JsonDesensitizeModule.java @@ -0,0 +1,17 @@ +package com.baiye.json; + +import com.fasterxml.jackson.databind.module.SimpleModule; + +/** + * Json 脱敏模块 + * + * @author hccake + */ +public class JsonDesensitizeModule extends SimpleModule { + + public JsonDesensitizeModule(JsonDesensitizeSerializerModifier jsonDesensitizeSerializerModifier) { + super(); + this.setSerializerModifier(jsonDesensitizeSerializerModifier); + } + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/JsonDesensitizeSerializer.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/JsonDesensitizeSerializer.java new file mode 100644 index 0000000..bf07124 --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/JsonDesensitizeSerializer.java @@ -0,0 +1,57 @@ +package com.baiye.json; + +import com.baiye.AnnotationHandlerHolder; +import com.baiye.functions.DesensitizeFunction; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.lang.annotation.Annotation; + +/** + * Jackson脱敏处理序列化器 + * + * @author Hccake 2021/1/22 + * @version 1.0 + */ +public class JsonDesensitizeSerializer extends JsonSerializer { + + /** + * json 脱敏处理注解 + */ + private final Annotation jsonDesensitizeAnnotation; + + /** + * 脱敏注解条件工具类 + */ + private final DesensitizeStrategy desensitizeStrategy; + + public JsonDesensitizeSerializer(Annotation jsonDesensitizeAnnotation, DesensitizeStrategy desensitizeStrategy) { + this.jsonDesensitizeAnnotation = jsonDesensitizeAnnotation; + this.desensitizeStrategy = desensitizeStrategy; + } + + @Override + public void serialize(Object value, JsonGenerator jsonGenerator, SerializerProvider serializers) + throws IOException { + if (value instanceof String) { + String str = (String) value; + + String fieldName = jsonGenerator.getOutputContext().getCurrentName(); + // 未开启脱敏 + if (desensitizeStrategy != null && desensitizeStrategy.ignoreField(fieldName)) { + jsonGenerator.writeString(str); + return; + } + DesensitizeFunction handleFunction = AnnotationHandlerHolder + .getHandleFunction(jsonDesensitizeAnnotation.annotationType()); + if (handleFunction == null) { + jsonGenerator.writeString(str); + return; + } + jsonGenerator.writeString(handleFunction.desensitize(jsonDesensitizeAnnotation, str)); + } + } + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/JsonDesensitizeSerializerModifier.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/JsonDesensitizeSerializerModifier.java new file mode 100644 index 0000000..92ac84a --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/JsonDesensitizeSerializerModifier.java @@ -0,0 +1,57 @@ +package com.baiye.json; + +import com.baiye.AnnotationHandlerHolder; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; + +import java.lang.annotation.Annotation; + +import java.util.List; + +/** + * json serial modifier + * + * @author Yakir + */ +public class JsonDesensitizeSerializerModifier extends BeanSerializerModifier { + + private DesensitizeStrategy desensitizeStrategy; + + public JsonDesensitizeSerializerModifier() { + } + + public JsonDesensitizeSerializerModifier(DesensitizeStrategy desensitizeStrategy) { + this.desensitizeStrategy = desensitizeStrategy; + } + + @Override + public List changeProperties(SerializationConfig config, BeanDescription beanDesc, + List beanProperties) { + for (BeanPropertyWriter beanProperty : beanProperties) { + Annotation annotation = getDesensitizeAnnotation(beanProperty); + + if (annotation != null && beanProperty.getType().isTypeOrSubTypeOf(String.class)) { + beanProperty.assignSerializer(new JsonDesensitizeSerializer(annotation, desensitizeStrategy)); + } + } + return beanProperties; + } + + /** + * 得到脱敏注解 + * @param beanProperty BeanPropertyWriter + * @return 返回第一个获取的脱敏注解 + */ + private Annotation getDesensitizeAnnotation(BeanPropertyWriter beanProperty) { + for (Class annotationClass : AnnotationHandlerHolder.getAnnotationClasses()) { + Annotation annotation = beanProperty.getAnnotation(annotationClass); + if (annotation != null) { + return annotation; + } + } + return null; + } + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/annotation/JsonRegexDesensitize.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/annotation/JsonRegexDesensitize.java new file mode 100644 index 0000000..3365e1b --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/annotation/JsonRegexDesensitize.java @@ -0,0 +1,37 @@ +package com.baiye.json.annotation; + +import com.baiye.enums.RegexDesensitizationTypeEnum; +import com.baiye.handler.RegexDesensitizationHandler; + +import java.lang.annotation.*; + +/** + * Jackson Filed 序列化脱敏注解, 对应使用正则脱敏处理器对值进行脱敏处理 + * + * @see RegexDesensitizationHandler + * @author Hccake 2021/1/22 + * @version 1.0 + */ +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface JsonRegexDesensitize { + + /** + * 脱敏类型,用于指定正则处理方式。 只有当值为 CUSTOM 时,以下两个个参数才有效 + * @see RegexDesensitizationTypeEnum#CUSTOM + * @return type + */ + RegexDesensitizationTypeEnum type(); + + /** + * 匹配的正则表达式,只有当type值为 CUSTOM 时,才生效 + */ + String regex() default "^[\\s\\S]*$"; + + /** + * 替换规则,只有当type值为 CUSTOM 时,才生效 + */ + String replacement() default "******"; + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/annotation/JsonSimpleDesensitize.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/annotation/JsonSimpleDesensitize.java new file mode 100644 index 0000000..87d0228 --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/annotation/JsonSimpleDesensitize.java @@ -0,0 +1,25 @@ +package com.baiye.json.annotation; + +import com.baiye.handler.SimpleDesensitizationHandler; + +import java.lang.annotation.*; + +/** + * Jackson Filed 序列化脱敏注解 使用脱敏处理器对值进行脱敏处理 + * + * @see SimpleDesensitizationHandler + * @author Hccake 2021/1/22 + * @version 1.0 + */ +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface JsonSimpleDesensitize { + + /** + * 脱敏类型,用于指定脱敏处理器 + * @return type + */ + Class handler(); + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/annotation/JsonSlideDesensitize.java b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/annotation/JsonSlideDesensitize.java new file mode 100644 index 0000000..f34ebc9 --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/java/com/baiye/json/annotation/JsonSlideDesensitize.java @@ -0,0 +1,42 @@ +package com.baiye.json.annotation; + +import com.baiye.enums.SlideDesensitizationTypeEnum; +import com.baiye.handler.SlideDesensitizationHandler; + +import java.lang.annotation.*; + +/** + * Jackson Filed 序列化脱敏注解, 对应使用滑动脱敏处理器对值进行脱敏处理 + * + * @see SlideDesensitizationHandler + * @author Hccake 2021/1/22 + * @version 1.0 + */ +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface JsonSlideDesensitize { + + /** + * 脱敏类型,只有当值为 CUSTOM 时,以下三个参数才有效 + * @see SlideDesensitizationTypeEnum#CUSTOM + * @return type + */ + SlideDesensitizationTypeEnum type(); + + /** + * 左边的明文数,只有当type值为 CUSTOM 时,才生效 + */ + int leftPlainTextLen() default 0; + + /** + * 右边的明文数,只有当type值为 CUSTOM 时,才生效 + */ + int rightPlainTextLen() default 0; + + /** + * 剩余部分字符逐个替换的字符串,只有当type值为 CUSTOM 时,才生效 + */ + String maskString() default "*"; + +} diff --git a/marketing-scrm-common/common-desensitize/src/main/resources/META-INF/services/com.baiye.upms.handler.SimpleDesensitizationHandler b/marketing-scrm-common/common-desensitize/src/main/resources/META-INF/services/com.baiye.upms.handler.SimpleDesensitizationHandler new file mode 100644 index 0000000..e731bd0 --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/main/resources/META-INF/services/com.baiye.upms.handler.SimpleDesensitizationHandler @@ -0,0 +1 @@ +com.baiye.upms.handler.SixAsteriskDesensitizationHandler diff --git a/marketing-scrm-common/common-desensitize/src/test/resources/META-INF/services/com.baiye.upms.handler.SimpleDesensitizationHandler b/marketing-scrm-common/common-desensitize/src/test/resources/META-INF/services/com.baiye.upms.handler.SimpleDesensitizationHandler new file mode 100644 index 0000000..b62c14e --- /dev/null +++ b/marketing-scrm-common/common-desensitize/src/test/resources/META-INF/services/com.baiye.upms.handler.SimpleDesensitizationHandler @@ -0,0 +1 @@ +com.baiye.common.core.test.desensite.TestDesensitizationHandler diff --git a/marketing-scrm-common/common-idempotent/pom.xml b/marketing-scrm-common/common-idempotent/pom.xml new file mode 100644 index 0000000..16bb572 --- /dev/null +++ b/marketing-scrm-common/common-idempotent/pom.xml @@ -0,0 +1,70 @@ + + + + marketing-scrm-common + com.baiye + ${revision} + + 4.0.0 + common-idempotent + + + + + cn.hutool + hutool-cache + + + com.baiye + common-core + ${revision} + + + com.baiye + common-util + ${revision} + + + jakarta.servlet + jakarta.servlet-api + compile + + + org.aspectj + aspectjweaver + + + org.awaitility + awaitility + test + + + + org.slf4j + slf4j-api + + + org.springframework.data + spring-data-redis + true + + + org.springframework + spring-aop + + + org.springframework + spring-context + + + org.springframework + spring-web + + + org.springframework + spring-webmvc + test + + + diff --git a/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/IdempotentAspect.java b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/IdempotentAspect.java new file mode 100644 index 0000000..f86ac8b --- /dev/null +++ b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/IdempotentAspect.java @@ -0,0 +1,54 @@ +package com.baiye; + +import cn.hutool.core.lang.Assert; +import com.baiye.annotation.Idempotent; +import com.baiye.exception.IdempotentException; +import com.baiye.key.generator.IdempotentKeyGenerator; +import com.baiye.key.store.IdempotentKeyStore; +import com.baiye.result.BaseResultCode; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +/** + * @author hccake + */ +@Aspect +@RequiredArgsConstructor +public class IdempotentAspect { + + private final IdempotentKeyStore idempotentKeyStore; + + private final IdempotentKeyGenerator idempotentKeyGenerator; + + @Around("@annotation(idempotentAnnotation)") + public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotentAnnotation) throws Throwable { + // 获取幂等标识 + String idempotentKey = idempotentKeyGenerator.generate(joinPoint, idempotentAnnotation); + + // 校验当前请求是否重复请求 + boolean saveSuccess = idempotentKeyStore.saveIfAbsent(idempotentKey, idempotentAnnotation.duration(), + idempotentAnnotation.timeUnit()); + Assert.isTrue(saveSuccess, () -> { + throw new IdempotentException(BaseResultCode.REPEATED_EXECUTE.getCode(), idempotentAnnotation.message()); + }); + + try { + Object result = joinPoint.proceed(); + if (idempotentAnnotation.removeKeyWhenFinished()) { + idempotentKeyStore.remove(idempotentKey); + } + return result; + } + catch (Throwable e) { + // 异常时,根据配置决定是否删除幂等 key + if (idempotentAnnotation.removeKeyWhenError()) { + idempotentKeyStore.remove(idempotentKey); + } + throw e; + } + + } + +} diff --git a/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/annotation/Idempotent.java b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/annotation/Idempotent.java new file mode 100644 index 0000000..d526192 --- /dev/null +++ b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/annotation/Idempotent.java @@ -0,0 +1,66 @@ +package com.baiye.annotation; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * 幂等控制注解 + * + * @author hccake + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Idempotent { + + String KEY_PREFIX = "idem"; + + /** + *

      + * 幂等标识的前缀,可用于区分服务和业务,防止 key 冲突 + *

      + * ps: 完整的幂等标识 = {prefix}:{uniqueExpression.value} + * @return 业务标识 + */ + String prefix() default KEY_PREFIX; + + /** + * 值为 SpEL 表达式,从上下文中提取幂等的唯一性标识。 + * @return Spring-EL expression + */ + String uniqueExpression() default ""; + + /** + *

      + * 幂等的控制时长,必须大于业务的处理耗时 + *

      + * 其值为幂等 key 的标记时长,超过标记时间,则幂等 key 可再次使用。 + * @return 标记时长,默认 10 min + */ + long duration() default 10 * 60; + + /** + * 控制时长单位,默认为 SECONDS 秒 + * @return {@link TimeUnit} + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + + /** + * 提示信息,正在执行中的提示 + * @return 提示信息 + */ + String message() default "重复请求,请稍后重试"; + + /** + * 是否在业务完成后立刻清除,幂等 key + * @return boolean true: 立刻清除 false: 不处理 + */ + boolean removeKeyWhenFinished() default false; + + /** + * 是否在业务执行异常时立刻清除幂等 key + * @return boolean true: 立刻清除 false: 不处理 + */ + boolean removeKeyWhenError() default false; + +} diff --git a/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/exception/IdempotentException.java b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/exception/IdempotentException.java new file mode 100644 index 0000000..f336732 --- /dev/null +++ b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/exception/IdempotentException.java @@ -0,0 +1,15 @@ +package com.baiye.exception; + +import lombok.EqualsAndHashCode; + +/** + * @author hccake + */ +@EqualsAndHashCode(callSuper = true) +public class IdempotentException extends BusinessException { + + public IdempotentException(int code, String message) { + super(code, message); + } + +} diff --git a/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/generator/DefaultIdempotentKeyGenerator.java b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/generator/DefaultIdempotentKeyGenerator.java new file mode 100644 index 0000000..2ab41bb --- /dev/null +++ b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/generator/DefaultIdempotentKeyGenerator.java @@ -0,0 +1,55 @@ +package com.baiye.key.generator; + +import com.baiye.annotation.Idempotent; +import com.baiye.util.SpelUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.lang.reflect.Method; + +/** + * 默认幂等key生成器 + * + * @author lishangbu + * @date 2022/10/18 + */ +public class DefaultIdempotentKeyGenerator implements IdempotentKeyGenerator { + + /** + * 生成幂等 key + * @param joinPoint 切点 + * @param idempotentAnnotation 幂等注解 + * @return String 幂等标识 + */ + @Override + public String generate(JoinPoint joinPoint, Idempotent idempotentAnnotation) { + String uniqueExpression = idempotentAnnotation.uniqueExpression(); + // 如果没有填写表达式,直接返回 prefix + if ("".equals(uniqueExpression)) { + return idempotentAnnotation.prefix(); + } + + // 获取当前方法以及方法参数 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + Object[] args = joinPoint.getArgs(); + + // 根据当前切点,获取到 spEL 上下文 + StandardEvaluationContext spelContext = SpelUtils.getSpelContext(joinPoint.getTarget(), method, args); + // 如果在 servlet 环境下,则将 request 信息放入上下文,便于获取请求参数 + ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder + .getRequestAttributes(); + if (requestAttributes != null) { + spelContext.setVariable(RequestAttributes.REFERENCE_REQUEST, requestAttributes.getRequest()); + } + // 解析出唯一标识 + String uniqueStr = SpelUtils.parseValueToString(spelContext, uniqueExpression); + // 和 prefix 拼接获得完整的 key + return idempotentAnnotation.prefix() + ":" + uniqueStr; + } + +} diff --git a/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/generator/IdempotentKeyGenerator.java b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/generator/IdempotentKeyGenerator.java new file mode 100644 index 0000000..b88ee94 --- /dev/null +++ b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/generator/IdempotentKeyGenerator.java @@ -0,0 +1,25 @@ +package com.baiye.key.generator; + +import com.baiye.annotation.Idempotent; +import org.aspectj.lang.JoinPoint; +import org.springframework.lang.NonNull; + +/** + * 幂等key生成器 + * + * @author lishangbu + * @date 2022/10/18 + */ +@FunctionalInterface +public interface IdempotentKeyGenerator { + + /** + * 生成幂等 key + * @param joinPoint 切点 + * @param idempotentAnnotation 幂等注解 + * @return 幂等key标识 + */ + @NonNull + String generate(JoinPoint joinPoint, Idempotent idempotentAnnotation); + +} diff --git a/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/store/IdempotentKeyStore.java b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/store/IdempotentKeyStore.java new file mode 100644 index 0000000..01f9b2f --- /dev/null +++ b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/store/IdempotentKeyStore.java @@ -0,0 +1,31 @@ +package com.baiye.key.store; + +import java.util.concurrent.TimeUnit; + +/** + *

      + * 幂等Key存储 + *

      + * + * 消费过的幂等 key 记录下来,再下次消费前校验 key 是否已记录,从而拒绝执行 + * + * @author hccake + */ +public interface IdempotentKeyStore { + + /** + * 当不存在有效 key 时将其存储下来 + * @param key idempotentKey + * @param duration key的有效时长 + * @param timeUnit 时长单位 + * @return boolean true: 存储成功 false: 存储失败 + */ + boolean saveIfAbsent(String key, long duration, TimeUnit timeUnit); + + /** + * 删除 key + * @param key idempotentKey + */ + void remove(String key); + +} diff --git a/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/store/InMemoryIdempotentKeyStore.java b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/store/InMemoryIdempotentKeyStore.java new file mode 100644 index 0000000..6fe9cf4 --- /dev/null +++ b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/store/InMemoryIdempotentKeyStore.java @@ -0,0 +1,38 @@ +package com.baiye.key.store; + +import cn.hutool.cache.CacheUtil; +import cn.hutool.cache.impl.TimedCache; + +import java.util.concurrent.TimeUnit; + +/** + * 基于内存的幂等Key存储组件 + * + * @author hccake + */ +public class InMemoryIdempotentKeyStore implements IdempotentKeyStore { + + private final TimedCache cache; + + public InMemoryIdempotentKeyStore() { + this.cache = CacheUtil.newTimedCache(Integer.MAX_VALUE); + cache.schedulePrune(1); + } + + @Override + public synchronized boolean saveIfAbsent(String key, long duration, TimeUnit timeUnit) { + Long value = cache.get(key, false); + if (value == null) { + long timeOut = TimeUnit.MILLISECONDS.convert(duration, timeUnit); + cache.put(key, System.currentTimeMillis(), timeOut); + return true; + } + return false; + } + + @Override + public void remove(String key) { + cache.remove(key); + } + +} diff --git a/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/store/RedisIdempotentKeyStore.java b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/store/RedisIdempotentKeyStore.java new file mode 100644 index 0000000..b8b49ab --- /dev/null +++ b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/key/store/RedisIdempotentKeyStore.java @@ -0,0 +1,34 @@ +package com.baiye.key.store; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import java.util.concurrent.TimeUnit; + +/** + * 基于 Redis 的幂等Key的存储器 + * + * @author hccake + */ +@RequiredArgsConstructor +public class RedisIdempotentKeyStore implements IdempotentKeyStore { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Override + public boolean saveIfAbsent(String key, long duration, TimeUnit timeUnit) { + ValueOperations opsForValue = stringRedisTemplate.opsForValue(); + Boolean saveSuccess = opsForValue.setIfAbsent(key, String.valueOf(System.currentTimeMillis()), duration, + timeUnit); + return saveSuccess != null && saveSuccess; + } + + @Override + public void remove(String key) { + stringRedisTemplate.delete(key); + } + +} diff --git a/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/polling/Balance.java b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/polling/Balance.java new file mode 100644 index 0000000..0504541 --- /dev/null +++ b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/polling/Balance.java @@ -0,0 +1,18 @@ +package com.baiye.polling; +import java.util.List; + + +/** + * @author Enzo + * @date : 2023/12/6 + */ + +public interface Balance { + + /** + * 轮询算法 + * @param list + * @return + */ + T chooseOne(List list); +} diff --git a/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/polling/QueueBalance.java b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/polling/QueueBalance.java new file mode 100644 index 0000000..fe03d31 --- /dev/null +++ b/marketing-scrm-common/common-idempotent/src/main/java/com/baiye/polling/QueueBalance.java @@ -0,0 +1,26 @@ +package com.baiye.polling; + +import cn.hutool.core.collection.CollUtil; + +import java.util.List; + +/** + * @author Enzo + * @date : 2023/12/6 + */ + +public class QueueBalance implements Balance { + + private volatile int index = 0; + + public synchronized T chooseOne(List list) { + if (CollUtil.isEmpty(list)) return null; + int sum = list.size(); + int temp = index % sum; + T t = list.get(temp); + index++; + return t; + } + +} + diff --git a/marketing-scrm-common/common-log/pom.xml b/marketing-scrm-common/common-log/pom.xml new file mode 100644 index 0000000..a15e628 --- /dev/null +++ b/marketing-scrm-common/common-log/pom.xml @@ -0,0 +1,32 @@ + + + + marketing-scrm-common + com.baiye + ${revision} + + 4.0.0 + common-log + + + + com.baiye + common-core + ${revision} + + + jakarta.servlet + jakarta.servlet-api + compile + + + org.aspectj + aspectjweaver + + + org.springframework + spring-context + + + diff --git a/marketing-scrm-common/common-log/src/main/java/com/baiye/access/filter/AccessLogFilter.java b/marketing-scrm-common/common-log/src/main/java/com/baiye/access/filter/AccessLogFilter.java new file mode 100644 index 0000000..1a46649 --- /dev/null +++ b/marketing-scrm-common/common-log/src/main/java/com/baiye/access/filter/AccessLogFilter.java @@ -0,0 +1,117 @@ +package com.baiye.access.filter; + +import com.baiye.constant.MDCConstants; +import com.baiye.request.wrapper.RepeatBodyRequestWrapper; +import com.baiye.access.handler.AccessLogHandler; +import com.baiye.util.LogUtils; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.MDC; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingResponseWrapper; +import org.springframework.web.util.UrlPathHelper; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * @author Hccake 2019/10/15 21:53 + */ +@RequiredArgsConstructor +public class AccessLogFilter extends OncePerRequestFilter { + + private final AccessLogHandler accessLogService; + + private final List ignoreUrlPatterns; + + /** + * 针对需忽略的Url的规则匹配器 + */ + private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher(); + + /** + * URL 路径匹配的帮助类 + */ + private static final UrlPathHelper URL_PATH_HELPER = new UrlPathHelper(); + + /** + * Same contract as for {@code doFilter}, but guaranteed to be just invoked once per + * request within a single request thread. See {@link #shouldNotFilterAsyncDispatch()} + * for details. + *

      + * Provides HttpServletRequest and HttpServletResponse arguments instead of the + * default ServletRequest and ServletResponse ones. + * @param request 请求信息 + * @param response 请求体 + * @param filterChain 过滤器链 + */ + @Override + @SuppressWarnings("java:S1181") + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + // 跳过部分忽略 url + String lookupPathForRequest = URL_PATH_HELPER.getLookupPathForRequest(request); + for (String ignoreUrlPattern : ignoreUrlPatterns) { + if (ANT_PATH_MATCHER.match(ignoreUrlPattern, lookupPathForRequest)) { + filterChain.doFilter(request, response); + return; + } + } + + // 包装request,以保证可以重复读取body 但不对文件上传请求body进行处理 + HttpServletRequest requestWrapper; + if (LogUtils.isMultipartContent(request)) { + requestWrapper = request; + } + else { + requestWrapper = new RepeatBodyRequestWrapper(request); + } + // 包装 response,便于重复获取 body + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + + // 开始时间 + Long startTime = System.currentTimeMillis(); + Throwable myThrowable = null; + final String traceId = MDC.get(MDCConstants.TRACE_ID_KEY); + try { + filterChain.doFilter(requestWrapper, responseWrapper); + } + catch (Throwable throwable) { + // 记录外抛异常 + myThrowable = throwable; + throw throwable; + } + finally { + // 这里抛BusinessException后会丢失traceId,需要重新设置 + if (StringUtils.isBlank(MDC.get(MDCConstants.TRACE_ID_KEY))) { + MDC.put(MDCConstants.TRACE_ID_KEY, traceId); + } + // 结束时间 + Long endTime = System.currentTimeMillis(); + // 执行时长 + Long executionTime = endTime - startTime; + // 记录在doFilter里被程序处理过后的异常,可参考 + // http://www.runoob.com/servlet/servlet-exception-handling.html + Throwable throwable = (Throwable) requestWrapper.getAttribute("javax.servlet.error.exception"); + if (throwable != null) { + myThrowable = throwable; + } + // 生产一个日志并记录 + try { + accessLogService.handleLog(requestWrapper, responseWrapper, executionTime, myThrowable); + } + catch (Exception e) { + logger.error("logging access_log error!", e); + } + // 重新写入数据到响应信息中 + responseWrapper.copyBodyToResponse(); + } + } + +} diff --git a/marketing-scrm-common/common-log/src/main/java/com/baiye/access/handler/AccessLogHandler.java b/marketing-scrm-common/common-log/src/main/java/com/baiye/access/handler/AccessLogHandler.java new file mode 100644 index 0000000..c080109 --- /dev/null +++ b/marketing-scrm-common/common-log/src/main/java/com/baiye/access/handler/AccessLogHandler.java @@ -0,0 +1,42 @@ +package com.baiye.access.handler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 访问日志处理器 + * + * @author Hccake 2019/10/15 22:21 + */ +public interface AccessLogHandler { + + /** + * 记录日志 + * @param request 请求信息 + * @param response 响应信息 + * @param executionTime 执行时长 + * @param throwable 异常 + */ + default void handleLog(HttpServletRequest request, HttpServletResponse response, Long executionTime, + Throwable throwable) { + T log = buildLog(request, response, executionTime, throwable); + saveLog(log); + } + + /** + * 生产一个日志 + * @return accessLog + * @param request 请求信息 + * @param response 响应信息 + * @param executionTime 执行时长 + * @param throwable 异常 + */ + T buildLog(HttpServletRequest request, HttpServletResponse response, Long executionTime, Throwable throwable); + + /** + * 保存日志 落库/或输出到文件等 + * @param accessLog 访问日志 + */ + void saveLog(T accessLog); + +} diff --git a/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/CreateOperationLogging.java b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/CreateOperationLogging.java new file mode 100644 index 0000000..a0144e2 --- /dev/null +++ b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/CreateOperationLogging.java @@ -0,0 +1,44 @@ +package com.baiye.operation.annotation; + +import com.baiye.operation.enums.OperationTypes; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 18:09 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@OperationLogging(type = OperationTypes.CREATE) +public @interface CreateOperationLogging { + + /** + * 日志信息 + * @return 日志描述信息 + */ + @AliasFor(annotation = OperationLogging.class) + String msg(); + + /** + * 是否保存方法入参 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordParams() default true; + + /** + * 是否保存方法返回值 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordResult() default true; + +} diff --git a/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/DeleteOperationLogging.java b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/DeleteOperationLogging.java new file mode 100644 index 0000000..b43bc63 --- /dev/null +++ b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/DeleteOperationLogging.java @@ -0,0 +1,44 @@ +package com.baiye.operation.annotation; + +import com.baiye.operation.enums.OperationTypes; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 18:09 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@OperationLogging(type = OperationTypes.DELETE) +public @interface DeleteOperationLogging { + + /** + * 日志信息 + * @return 日志描述信息 + */ + @AliasFor(annotation = OperationLogging.class) + String msg(); + + /** + * 是否保存方法入参 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordParams() default true; + + /** + * 是否保存方法返回值 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordResult() default true; + +} diff --git a/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/OperationLogging.java b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/OperationLogging.java new file mode 100644 index 0000000..bea1907 --- /dev/null +++ b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/OperationLogging.java @@ -0,0 +1,43 @@ +package com.baiye.operation.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 18:09 + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface OperationLogging { + + /** + * 日志信息 + * @return 日志描述信息 + */ + String msg() default ""; + + /** + * 日志操作类型 + * @return 日志操作类型枚举 + */ + int type(); + + /** + * 是否保存方法入参 + * @return boolean + */ + boolean recordParams() default true; + + /** + * 是否保存方法返回值 + * @return boolean + */ + boolean recordResult() default true; + +} diff --git a/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/ReadOperationLogging.java b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/ReadOperationLogging.java new file mode 100644 index 0000000..c7aba86 --- /dev/null +++ b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/ReadOperationLogging.java @@ -0,0 +1,44 @@ +package com.baiye.operation.annotation; + +import com.baiye.operation.enums.OperationTypes; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 18:09 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@OperationLogging(type = OperationTypes.READ) +public @interface ReadOperationLogging { + + /** + * 日志信息 + * @return 日志描述信息 + */ + @AliasFor(annotation = OperationLogging.class) + String msg(); + + /** + * 是否保存方法入参 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordParams() default true; + + /** + * 是否保存方法返回值 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordResult() default true; + +} diff --git a/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/UpdateOperationLogging.java b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/UpdateOperationLogging.java new file mode 100644 index 0000000..ac163de --- /dev/null +++ b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/annotation/UpdateOperationLogging.java @@ -0,0 +1,44 @@ +package com.baiye.operation.annotation; + +import com.baiye.operation.enums.OperationTypes; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 18:09 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@OperationLogging(type = OperationTypes.UPDATE) +public @interface UpdateOperationLogging { + + /** + * 日志信息 + * @return 日志描述信息 + */ + @AliasFor(annotation = OperationLogging.class) + String msg(); + + /** + * 是否保存方法入参 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordParams() default true; + + /** + * 是否保存方法返回值 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordResult() default true; + +} diff --git a/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/aspect/OperationLogAspect.java b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/aspect/OperationLogAspect.java new file mode 100644 index 0000000..fd8d680 --- /dev/null +++ b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/aspect/OperationLogAspect.java @@ -0,0 +1,80 @@ +package com.baiye.operation.aspect; + +import com.baiye.operation.annotation.OperationLogging; +import com.baiye.operation.handler.OperationLogHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.Assert; + +import java.lang.reflect.Method; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 18:16 操作日志切面类 + */ +@Slf4j +@Aspect +@RequiredArgsConstructor +public class OperationLogAspect { + + private final OperationLogHandler operationLogHandler; + + @Around("execution(@(@com.baiye.operation.annotation.OperationLogging *) * *(..)) " + + "|| @annotation(com.baiye.operation.annotation.OperationLogging)") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + // 开始时间 + long startTime = System.currentTimeMillis(); + + // 获取目标方法 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + + // 获取操作日志注解 备注: AnnotationUtils.findAnnotation 方法无法获取继承的属性 + OperationLogging operationLogging = AnnotatedElementUtils.findMergedAnnotation(method, OperationLogging.class); + + // 获取操作日志 DTO + Assert.notNull(operationLogging, "operationLogging annotation must not be null!"); + + T operationLog = operationLogHandler.buildLog(operationLogging, joinPoint); + + Throwable throwable = null; + Object result = null; + try { + result = joinPoint.proceed(); + return result; + } + catch (Throwable e) { + throwable = e; + throw e; + } + finally { + // 是否保存响应内容 + boolean isSaveResult = operationLogging.recordResult(); + // 操作日志记录处理 + handleLog(joinPoint, startTime, operationLog, throwable, isSaveResult, result); + } + } + + private void handleLog(ProceedingJoinPoint joinPoint, long startTime, T operationLog, Throwable throwable, + boolean isSaveResult, Object result) { + try { + // 结束时间 + long executionTime = System.currentTimeMillis() - startTime; + // 记录执行信息 + operationLogHandler.recordExecutionInfo(operationLog, joinPoint, executionTime, throwable, isSaveResult, + result); + // 处理操作日志 + operationLogHandler.handleLog(operationLog); + } + catch (Exception e) { + log.error("记录操作日志异常:{}", operationLog); + } + } + +} diff --git a/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/enums/LogStatusEnum.java b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/enums/LogStatusEnum.java new file mode 100644 index 0000000..22020ef --- /dev/null +++ b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/enums/LogStatusEnum.java @@ -0,0 +1,26 @@ +package com.baiye.operation.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Hccake + * @version 1.0 + * @date 2020/5/15 16:47 操作状态枚举 + */ +@Getter +@AllArgsConstructor +public enum LogStatusEnum { + + /** + * 成功 + */ + SUCCESS(1), + /** + * 失败 + */ + FAIL(0); + + private final int value; + +} diff --git a/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/enums/OperationTypes.java b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/enums/OperationTypes.java new file mode 100644 index 0000000..5928395 --- /dev/null +++ b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/enums/OperationTypes.java @@ -0,0 +1,48 @@ +package com.baiye.operation.enums; + +/** + * 操作类型的接口,用户可以继承此接口以便自定义类型 + * + * @author hccake + */ +public final class OperationTypes { + + private OperationTypes() { + } + + /** + * 其他操作 + */ + public static final int OTHER = 0; + + /** + * 导入操作 + */ + public static final int IMPORT = 1; + + /** + * 导出操作 + */ + public static final int EXPORT = 2; + + /** + * 查看操作,主要用于敏感数据查询记录 + */ + public static final int READ = 3; + + /** + * 新建操作 + */ + public static final int CREATE = 4; + + /** + * 修改操作 + */ + public static final int UPDATE = 5; + + /** + * 删除操作 + */ + public static final int DELETE = 6; + +} diff --git a/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/handler/AbstractOperationLogHandler.java b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/handler/AbstractOperationLogHandler.java new file mode 100644 index 0000000..50bdb4a --- /dev/null +++ b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/handler/AbstractOperationLogHandler.java @@ -0,0 +1,89 @@ +package com.baiye.operation.handler; + +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ArrayUtil; +import com.baiye.util.JsonUtils; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author hccake + */ +@Slf4j +public abstract class AbstractOperationLogHandler implements OperationLogHandler { + + /** + *

      + * 忽略记录的参数类型列表 + *

      + * 忽略判断时只针对方法入参类型,如果入参为对象,其某个属性需要忽略的无法处理,可以使用 @JsonIgnore 进行忽略。 + */ + private final List> ignoredParamClasses = ListUtil.toList(ServletRequest.class, ServletResponse.class, + MultipartFile.class); + + /** + * 添加忽略记录的参数类型 + * @param clazz 参数类型 + */ + public void addIgnoredParamClass(Class clazz) { + ignoredParamClasses.add(clazz); + } + + /** + * 获取方法参数 + * @param joinPoint 切点 + * @return 当前方法入参的Json Str + */ + public String getParams(ProceedingJoinPoint joinPoint) { + // 获取方法签名 + Signature signature = joinPoint.getSignature(); + String strClassName = joinPoint.getTarget().getClass().getName(); + String strMethodName = signature.getName(); + MethodSignature methodSignature = (MethodSignature) signature; + log.debug("[getParams],获取方法参数[类名]:{},[方法]:{}", strClassName, strMethodName); + + String[] parameterNames = methodSignature.getParameterNames(); + Object[] args = joinPoint.getArgs(); + if (ArrayUtil.isEmpty(parameterNames)) { + return null; + } + Map paramsMap = new HashMap<>(); + for (int i = 0; i < parameterNames.length; i++) { + Object arg = args[i]; + if (arg == null) { + paramsMap.put(parameterNames[i], null); + continue; + } + Class argClass = arg.getClass(); + // 忽略部分类型的参数记录 + for (Class ignoredParamClass : ignoredParamClasses) { + if (ignoredParamClass.isAssignableFrom(argClass)) { + arg = "ignored param type: " + argClass; + break; + } + } + paramsMap.put(parameterNames[i], arg); + } + + String params = ""; + try { + // 入参类中的属性可以通过注解进行数据落库脱敏以及忽略等操作 + params = JsonUtils.toJson(paramsMap); + } + catch (Exception e) { + log.error("[getParams],获取方法参数异常,[类名]:{},[方法]:{}", strClassName, strMethodName, e); + } + + return params; + } + +} diff --git a/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/handler/OperationLogHandler.java b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/handler/OperationLogHandler.java new file mode 100644 index 0000000..92dd4e2 --- /dev/null +++ b/marketing-scrm-common/common-log/src/main/java/com/baiye/operation/handler/OperationLogHandler.java @@ -0,0 +1,42 @@ +package com.baiye.operation.handler; + +import com.baiye.operation.annotation.OperationLogging; +import org.aspectj.lang.ProceedingJoinPoint; + +/** + * 操作日志业务类 + * + * @author Hccake + * @version 1.0 + * @date 2019/10/15 19:57 + */ +public interface OperationLogHandler { + + /** + * 创建操作日志 + * @param operationLogging 操作日志注解 + * @param joinPoint 当前执行方法的切点信息 + * @return T 操作日志对象 + */ + T buildLog(OperationLogging operationLogging, ProceedingJoinPoint joinPoint); + + /** + * 目标方法执行完成后进行信息补充记录, 如执行时长,异常信息,还可以通过切点记录返回值,如果需要的话 + * @param log 操作日志对象 {@link #buildLog} + * @param joinPoint 当前执行方法的切点信息 + * @param executionTime 方法执行时长 + * @param throwable 方法执行的异常,为 null 则表示无异常 + * @param isSaveResult 是否记录返回值 + * @param result 方法执行的返回值 + * @return T 操作日志对象 + */ + T recordExecutionInfo(T log, ProceedingJoinPoint joinPoint, long executionTime, Throwable throwable, + boolean isSaveResult, Object result); + + /** + * 处理日志,可以在这里进行存储,或者输出 + * @param operationLog 操作日志 + */ + void handleLog(T operationLog); + +} diff --git a/marketing-scrm-common/common-log/src/main/java/com/baiye/util/LogUtils.java b/marketing-scrm-common/common-log/src/main/java/com/baiye/util/LogUtils.java new file mode 100644 index 0000000..5f9b59d --- /dev/null +++ b/marketing-scrm-common/common-log/src/main/java/com/baiye/util/LogUtils.java @@ -0,0 +1,78 @@ +package com.baiye.util; + +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 22:53 + */ +@Slf4j +@UtilityClass +public class LogUtils { + + /** + * 获取请求体 + * @param request 请求体 + * @return requestBody + */ + public String getRequestBody(HttpServletRequest request) { + String body = null; + if (!request.getMethod().equals(HttpMethod.GET.name())) { + try { + BufferedReader reader = request.getReader(); + if (reader != null) { + body = reader.lines().collect(Collectors.joining(System.lineSeparator())); + } + } + catch (Exception e) { + log.error("读取请求体异常:", e); + } + } + return body; + } + + /** + * 获取响应体 防止在 {@link org.springframework.web.context.request.RequestContextHolder} + * 设置内容之前或清空内容之后使用,从而导致获取不到响应体的问题 + * @param request 请求信息 + * @param response 响应信息 + * @return responseBody 响应体 + */ + public String getResponseBody(HttpServletRequest request, HttpServletResponse response) { + try { + if (response instanceof ContentCachingResponseWrapper) { + ContentCachingResponseWrapper responseWrapper = (ContentCachingResponseWrapper) response; + // 获取响应体 + byte[] contentAsByteArray = responseWrapper.getContentAsByteArray(); + return new String(contentAsByteArray, StandardCharsets.UTF_8); + } + log.warn("对于未包装的响应体,默认不进行读取请求体,请求 uri: [{}]", request.getRequestURI()); + } + catch (Exception exception) { + log.error("获取响应体信息失败,请求 uri: [{}]", request.getRequestURI()); + } + return ""; + } + + /** + * 判断是否是multipart/form-data请求 + * @param request 请求信息 + * @return 是否是multipart/form-data请求 + */ + public boolean isMultipartContent(HttpServletRequest request) { + // 获取Content-Type + String contentType = request.getContentType(); + return (contentType != null) && (contentType.toLowerCase().startsWith("multipart/")); + } + +} diff --git a/marketing-scrm-common/common-model/pom.xml b/marketing-scrm-common/common-model/pom.xml new file mode 100644 index 0000000..f2991db --- /dev/null +++ b/marketing-scrm-common/common-model/pom.xml @@ -0,0 +1,40 @@ + + + + marketing-scrm-common + com.baiye + ${revision} + + 4.0.0 + common-model + + + + com.baomidou + mybatis-plus-annotation + + + + com.baiye + common-i18n + + + + io.swagger.core.v3 + swagger-annotations + + + org.hibernate.validator + hibernate-validator + + + org.springdoc + springdoc-openapi-common + + + cn.hutool + hutool-core + + + diff --git a/marketing-scrm-common/common-model/src/main/java/com/baiye/domain/PageParam.java b/marketing-scrm-common/common-model/src/main/java/com/baiye/domain/PageParam.java new file mode 100644 index 0000000..ad7c202 --- /dev/null +++ b/marketing-scrm-common/common-model/src/main/java/com/baiye/domain/PageParam.java @@ -0,0 +1,58 @@ +package com.baiye.domain; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.Pattern; +import java.util.ArrayList; +import java.util.List; + +/** + * 分页查询参数 + * + * @author Hccake 2021/1/18 + * @version 1.0 + */ +@Data +@Schema(title = "分页查询参数") +public class PageParam { + + /** + * @deprecated 使用 page 属性 + */ + @Deprecated + @Schema(title = "当前页码, 现在建议使用 page 参数", description = "从 1 开始", defaultValue = "1", example = "1") + @Min(value = 1, message = "当前页不能小于 1") + private long current = 1; + + @Schema(title = "当前页码", description = "从 1 开始", defaultValue = "1", example = "1") + @Min(value = 1, message = "当前页不能小于 1") + private long page = 1; + + @Schema(title = "每页显示条数", description = "最大值为系统设置,默认 100", defaultValue = "10") + @Min(value = 1, message = "每页显示条数不能小于1") + private long size = 10; + + @Schema(title = "排序规则") + @Valid + private List sorts = new ArrayList<>(); + + @Schema(title = "排序元素载体") + @Getter + @Setter + public static class Sort { + + @Schema(title = "排序字段", example = "id") + @Pattern(regexp = PageableConstants.SORT_FILED_REGEX, message = "排序字段格式非法") + private String field; + + @Schema(title = "是否正序排序", example = "false") + private boolean asc; + + } + +} diff --git a/marketing-scrm-common/common-model/src/main/java/com/baiye/domain/PageResult.java b/marketing-scrm-common/common-model/src/main/java/com/baiye/domain/PageResult.java new file mode 100644 index 0000000..96f5175 --- /dev/null +++ b/marketing-scrm-common/common-model/src/main/java/com/baiye/domain/PageResult.java @@ -0,0 +1,44 @@ +package com.baiye.domain; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.util.Collections; +import java.util.List; + +/** + * 分页返回结果 + * + * @author Hccake 2021/1/18 + * @version 1.0 + */ +@Data +@Schema(title = "分页返回结果") +public class PageResult { + + /** + * 查询数据列表 + */ + @Schema(title = "分页数据") + protected List records = Collections.emptyList(); + + /** + * 总数 + */ + @Schema(title = "数据总量") + protected Long total = 0L; + + public PageResult() { + } + + public PageResult(long total) { + this.total = total; + } + + public PageResult(List records, long total) { + this.records = records; + this.total = total; + } + +} diff --git a/marketing-scrm-common/common-model/src/main/java/com/baiye/domain/PageableConstants.java b/marketing-scrm-common/common-model/src/main/java/com/baiye/domain/PageableConstants.java new file mode 100644 index 0000000..21ecc29 --- /dev/null +++ b/marketing-scrm-common/common-model/src/main/java/com/baiye/domain/PageableConstants.java @@ -0,0 +1,76 @@ +package com.baiye.domain; + +import cn.hutool.core.collection.CollUtil; + +import java.util.Set; + +/** + * @author hccake + */ +public final class PageableConstants { + + private PageableConstants() { + } + + /** + * 排序的 Field 部分的正则 + */ + public static final String SORT_FILED_REGEX = "(([A-Za-z0-9_]{1,10}\\.)?[A-Za-z0-9_]{1,64})"; + + /** + * 排序的 order 部分的正则 + */ + public static final String SORT_FILED_ORDER = "(desc|asc)"; + + /** + * 完整的排序规则正则 + */ + public static final String SORT_REGEX = "^" + PageableConstants.SORT_FILED_REGEX + "(," + + PageableConstants.SORT_FILED_ORDER + ")*$"; + + /** + * 默认的当前页数的参数名 + */ + public static final String DEFAULT_PAGE_PARAMETER = "page"; + + /** + * 默认的单页条数的参数名 + */ + public static final String DEFAULT_SIZE_PARAMETER = "size"; + + /** + * 默认的排序参数的参数名 + */ + public static final String DEFAULT_SORT_PARAMETER = "sort"; + + /** + * 默认的最大单页条数 + */ + public static final int DEFAULT_MAX_PAGE_SIZE = 100; + + /** + * 升序关键字 + */ + public static final String ASC = "asc"; + + /** + * SQL 关键字 + */ + public static final Set SQL_KEYWORDS = CollUtil.newHashSet("master", "truncate", "insert", "select", + "delete", "update", "declare", "alter", "drop", "sleep"); + + /** + * 升序关键字 + * @deprecated 已过期,使用 sort 进行传参 + */ + @Deprecated + public static final String SORT_ORDERS = "sortOrders"; + + /** + * 升序关键字 + * @deprecated 已过期,使用 sort 进行传参 + */ + @Deprecated + public static final String SORT_FIELDS = "sortFields"; + +} diff --git a/marketing-scrm-common/common-model/src/main/java/com/baiye/domain/SelectData.java b/marketing-scrm-common/common-model/src/main/java/com/baiye/domain/SelectData.java new file mode 100644 index 0000000..188e968 --- /dev/null +++ b/marketing-scrm-common/common-model/src/main/java/com/baiye/domain/SelectData.java @@ -0,0 +1,51 @@ +package com.baiye.domain; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 下拉框所对应的视图类 + * + * @author Hccake + */ +@Data +@Schema(title = "下拉框数据") +public class SelectData { + + /** + * 显示的数据 + */ + @Schema(title = "显示的数据", required = true) + private String name; + + /** + * 选中获取的属性 + */ + @Schema(title = "选中获取的属性", required = true) + private Object value; + + /** + * 是否被选中 + */ + @Schema(title = "是否被选中") + private Boolean selected; + + /** + * 是否禁用 + */ + @Schema(title = "是否禁用") + private Boolean disabled; + + /** + * 分组标识 + */ + @Schema(title = "分组标识") + private String type; + + /** + * 附加属性 + */ + @Schema(title = "附加属性") + private T attributes; + +} diff --git a/marketing-scrm-common/common-model/src/main/java/com/baiye/entity/BaseEntity.java b/marketing-scrm-common/common-model/src/main/java/com/baiye/entity/BaseEntity.java new file mode 100644 index 0000000..979c493 --- /dev/null +++ b/marketing-scrm-common/common-model/src/main/java/com/baiye/entity/BaseEntity.java @@ -0,0 +1,49 @@ +package com.baiye.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 实体类基类 + * + * @author hccake + */ +@Getter +@Setter +public abstract class BaseEntity implements Serializable { + + /** + * 创建者 + */ + @TableField(fill = FieldFill.INSERT) + @Schema(title = "创建者") + private Long createBy; + + /** + * 更新者 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @Schema(title = "更新者") + private Long updateBy; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 修改时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @Schema(title = "修改时间") + private LocalDateTime updateTime; + +} diff --git a/marketing-scrm-common/common-model/src/main/java/com/baiye/entity/BaseMapStruct.java b/marketing-scrm-common/common-model/src/main/java/com/baiye/entity/BaseMapStruct.java new file mode 100644 index 0000000..02d02c1 --- /dev/null +++ b/marketing-scrm-common/common-model/src/main/java/com/baiye/entity/BaseMapStruct.java @@ -0,0 +1,37 @@ +package com.baiye.entity; + +import java.util.List; + +/** + * @author Enzo + * @date 2022-05-14 + */ +public interface BaseMapStruct { + /** + * DTO转Entity + * @param dto / + * @return / + */ + E toEntity(D dto); + + /** + * Entity转DTO + * @param entity / + * @return / + */ + D toDto(E entity); + + /** + * DTO集合转Entity集合 + * @param dtoList / + * @return / + */ + List toEntity(List dtoList); + + /** + * Entity集合转DTO集合 + * @param entityList / + * @return / + */ + List toDto(List entityList); +} diff --git a/marketing-scrm-common/common-model/src/main/java/com/baiye/entity/LogicDeletedBaseEntity.java b/marketing-scrm-common/common-model/src/main/java/com/baiye/entity/LogicDeletedBaseEntity.java new file mode 100644 index 0000000..3c58473 --- /dev/null +++ b/marketing-scrm-common/common-model/src/main/java/com/baiye/entity/LogicDeletedBaseEntity.java @@ -0,0 +1,27 @@ +package com.baiye.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableLogic; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * 逻辑删除的实体类基类 + * + * @author hccake + */ +@Getter +@Setter +public abstract class LogicDeletedBaseEntity extends BaseEntity { + + /** + * 逻辑删除标识,已删除: 删除时间戳,未删除: 0 + */ + @TableLogic + @TableField(fill = FieldFill.INSERT) + @Schema(title = "逻辑删除标识,已删除: 删除时间戳,未删除: 0") + private Long deleted; + +} diff --git a/marketing-scrm-common/common-model/src/main/java/com/baiye/result/BaseResultCode.java b/marketing-scrm-common/common-model/src/main/java/com/baiye/result/BaseResultCode.java new file mode 100644 index 0000000..814e70e --- /dev/null +++ b/marketing-scrm-common/common-model/src/main/java/com/baiye/result/BaseResultCode.java @@ -0,0 +1,50 @@ +package com.baiye.result; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/12 12:19 + */ +@Getter +@AllArgsConstructor +public enum BaseResultCode implements ResultCode { + + /** + * 数据库保存/更新异常 + */ + UPDATE_DATABASE_ERROR(90001, "Update Database Error"), + + /** + * 通用的逻辑校验异常 + */ + LOGIC_CHECK_ERROR(400, "Logic Check Error"), +// LOGIC_CHECK_ERROR(90004, "Logic Check Error"), + + /** + * 恶意请求 + */ + MALICIOUS_REQUEST(90005, "Malicious Request"), + + /** + * 文件上传异常 + */ + FILE_UPLOAD_ERROR(90006, "File Upload Error"), + + /** + * 重复执行 + */ + REPEATED_EXECUTE(90007, "Repeated execute"), + + /** + * 未知异常 + */ + UNKNOWN_ERROR(99999, "Unknown Error"); + + private final Integer code; + + private final String message; + +} diff --git a/marketing-scrm-common/common-model/src/main/java/com/baiye/result/R.java b/marketing-scrm-common/common-model/src/main/java/com/baiye/result/R.java new file mode 100644 index 0000000..402699f --- /dev/null +++ b/marketing-scrm-common/common-model/src/main/java/com/baiye/result/R.java @@ -0,0 +1,65 @@ +package com.baiye.result; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import lombok.experimental.Accessors; +import org.springframework.http.HttpStatus; + +import java.io.Serializable; + +/** + * 响应信息主体 + * + * @param + * @author Hccake + */ +// @I18nClass +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Schema(title = "返回体结构") +public class R implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(title = "返回状态码", defaultValue = "0") + private int code; + + @Schema(title = "返回信息", defaultValue = "Success") + private String message; + + @Schema(title = "数据", nullable = true, defaultValue = "null") + private T data; + + public static R ok() { + return ok(null); + } + + public static R ok(T data) { + return ok(data, SystemResultCode.SUCCESS.getMessage()); + } + + public static R ok(T data, String message) { + return new R().setCode(SystemResultCode.SUCCESS.getCode()).setData(data).setMessage(message); + } + + + public static R failed(String message) { + return new R().setCode(HttpStatus.BAD_REQUEST.value()).setMessage(message); + } + public static R failed(int code, String message) { + return new R().setCode(code).setMessage(message); + } + + public static R failed(ResultCode failMsg) { + return failed(failMsg.getCode(), failMsg.getMessage()); + } + + public static R failed(ResultCode failMsg, String message) { + return failed(failMsg.getCode(), message); + } + +} diff --git a/marketing-scrm-common/common-model/src/main/java/com/baiye/result/ResultCode.java b/marketing-scrm-common/common-model/src/main/java/com/baiye/result/ResultCode.java new file mode 100644 index 0000000..f2b0e39 --- /dev/null +++ b/marketing-scrm-common/common-model/src/main/java/com/baiye/result/ResultCode.java @@ -0,0 +1,22 @@ +package com.baiye.result; + +/** + * @author Hccake + * @version 1.0 + * @date 2020/3/20 14:45 + */ +public interface ResultCode { + + /** + * 获取业务码 + * @return 业务码 + */ + Integer getCode(); + + /** + * 获取信息 + * @return 返回结构体中的信息 + */ + String getMessage(); + +} diff --git a/marketing-scrm-common/common-model/src/main/java/com/baiye/result/SystemResultCode.java b/marketing-scrm-common/common-model/src/main/java/com/baiye/result/SystemResultCode.java new file mode 100644 index 0000000..968f748 --- /dev/null +++ b/marketing-scrm-common/common-model/src/main/java/com/baiye/result/SystemResultCode.java @@ -0,0 +1,88 @@ +package com.baiye.result; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/12 12:19 + */ +@Getter +@AllArgsConstructor +public enum SystemResultCode implements ResultCode { + + // ================ 基础部分,参考 HttpStatus ============= + + // region --- 2xx Success --- + /** + * 成功 + * @see HTTP/1.1: + * Semantics and Content, section 6.3.1 + * + */ + SUCCESS(200, "Success"), + // endregion + + // region --- 4xx Client Error --- + + /** + * 参数错误 + * @see HTTP/1.1: + * Semantics and Content, section 6.5.1 + */ + BAD_REQUEST(400, "Bad Request"), + /** + * 未认证 + * @see HTTP/1.1: + * Authentication, section 3.1 + * + */ + UNAUTHORIZED(401, "Unauthorized"), + /** + * 未授权 + * @see HTTP/1.1: + * Semantics and Content, section 6.5.3 + * + */ + FORBIDDEN(403, "Forbidden"), + + /** + * {@code 404 Not Found}. + * @see HTTP/1.1: + * Semantics and Content, section 6.5.4 + */ + NOT_FOUND(404, "Not Found"), + + /** + * {@code 405 Method Not Allowed}. + * @see HTTP/1.1: + * Semantics and Content, section 6.5.5 + */ + METHOD_NOT_ALLOWED(405, "Method Not Allowed"), + + // endregion + + // region --- 5xx Server Error --- + + /** + * 服务异常 + * @see HTTP/1.1: + * Semantics and Content, section 6.6.1 + */ + SERVER_ERROR(500, "Internal Server Error"), + + /** + * {@code 502 Bad Gateway}. + * @see HTTP/1.1: + * Semantics and Content, section 6.6.3 + */ + BAD_GATEWAY(502, "Bad Gateway"); + + // endregion + + private final Integer code; + + private final String message; + +} diff --git a/marketing-scrm-common/common-model/src/main/java/com/baiye/result/WeChatResponse.java b/marketing-scrm-common/common-model/src/main/java/com/baiye/result/WeChatResponse.java new file mode 100644 index 0000000..9da46eb --- /dev/null +++ b/marketing-scrm-common/common-model/src/main/java/com/baiye/result/WeChatResponse.java @@ -0,0 +1,29 @@ +package com.baiye.result; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; + +/** + * @author Enzo + * @date : 2022/6/23 + */ +@Getter +@Setter +@ToString +public class WeChatResponse implements Serializable { + + private static final long serialVersionUID = 6763956796736587193L; + + private T data; + + private String msg; + + private Integer status; + + private Boolean success; + + +} diff --git a/marketing-scrm-common/common-redis/pom.xml b/marketing-scrm-common/common-redis/pom.xml new file mode 100644 index 0000000..33f13b1 --- /dev/null +++ b/marketing-scrm-common/common-redis/pom.xml @@ -0,0 +1,45 @@ + + + + marketing-scrm-common + com.baiye + ${revision} + + 4.0.0 + common-redis + + + true + + + + + com.baiye + common-core + ${revision} + + + com.baiye + common-util + ${revision} + + + io.lettuce + lettuce-core + test + + + org.aspectj + aspectjweaver + + + org.springframework.boot + spring-boot + + + org.springframework.data + spring-data-redis + + + diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/RedisHelper.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/RedisHelper.java new file mode 100644 index 0000000..76ab09f --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/RedisHelper.java @@ -0,0 +1,1854 @@ +package com.baiye; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisStreamCommands; +import org.springframework.data.redis.connection.RedisZSetCommands; +import org.springframework.data.redis.connection.stream.*; +import org.springframework.data.redis.core.*; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.lang.Nullable; + +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * Redis 操作的辅助类 + * + * @author lingting + * @author svn + * @author hccake + */ +@SuppressWarnings("ConstantConditions") +@Slf4j +public class RedisHelper { + + @SuppressWarnings("InstantiationOfUtilityClass") + public static final RedisHelper INSTANCE = new RedisHelper(); + + private RedisHelper() { + } + + /** + * 自增并设置过期时间的 lua 脚本 + */ + private static final DefaultRedisScript INCR_BY_EXPIRE_LUA_SCRIPT = new DefaultRedisScript<>( + "local r = redis.call('INCRBY', KEYS[1], ARGV[1]) redis.call('EXPIRE', KEYS[1], ARGV[2]) return r", + Long.class); + + static RedisTemplate redisTemplate; + + public static RedisTemplate getRedisTemplate() { + return redisTemplate; + } + + public static void setRedisTemplate(RedisTemplate redisTemplate) { + RedisHelper.redisTemplate = redisTemplate; + } + + @SuppressWarnings("all") + private static RedisSerializer getKeySerializer() { + return (RedisSerializer) redisTemplate.getKeySerializer(); + } + + @SuppressWarnings("all") + private static RedisSerializer getValueSerializer() { + return (RedisSerializer) redisTemplate.getValueSerializer(); + } + + public static HashOperations hashOps() { + return redisTemplate.opsForHash(); + } + + public static ValueOperations valueOps() { + return redisTemplate.opsForValue(); + } + + public static ListOperations listOps() { + return redisTemplate.opsForList(); + } + + public static SetOperations setOps() { + return redisTemplate.opsForSet(); + } + + public static ZSetOperations zSetOps() { + return redisTemplate.opsForZSet(); + } + + public static StreamOperations streamOps() { + return redisTemplate.opsForStream(); + } + + // --------------------- key command start ----------------- + @Deprecated + public static boolean hasKey(String key) { + Boolean b = redisTemplate.hasKey(key); + return b != null && b; + } + + /** + * @deprecated use {@link #del(String)} + */ + @Deprecated + public static boolean delete(String key) { + Boolean b = redisTemplate.delete(key); + return b != null && b; + } + + /** + * @deprecated use {@link #del(Collection)} + */ + @Deprecated + public static long delete(Collection keys) { + Long l = redisTemplate.delete(keys); + return l == null ? 0 : l; + } + + /** + * 删除指定的 key + * @param key 要删除的 key + * @return 删除成功返回 true, 如果 key 不存在则返回 false + * @see Del Command + */ + public static boolean del(String key) { + return Boolean.TRUE.equals(redisTemplate.delete(key)); + } + + /** + * 删除指定的 keys + * @param keys 要删除的 key 数组 + * @return 如果删除了一个或多个 key,则为大于 0 的整数,如果指定的 key 都不存在,则为 0 + */ + public static long del(String... keys) { + return del(Arrays.asList(keys)); + } + + public static long del(Collection keys) { + Long deleteNumber = redisTemplate.delete(keys); + return deleteNumber == null ? 0 : deleteNumber; + } + + /** + * 判断 key 是否存在 + * @param key 待判断的 key + * @return 如果 key 存在 {@code true} , 否则返回 {@code false} + * @see Exists Command + */ + public static boolean exists(String key) { + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + } + + /** + * 判断指定的 key 是否存在. + * @param keys 待判断的数组 + * @return 指定的 keys 在 redis 中存在的的数量 + * @see Exists Command + */ + public static long exists(String... keys) { + return exists(Arrays.asList(keys)); + } + + public static long exists(Collection keys) { + Long number = redisTemplate.countExistingKeys(keys); + return number == null ? 0 : number; + } + + /** + * 设置过期时间 + * @param key 待修改过期时间的 key + * @param timeout 过期时长,单位 秒 + * @see Expire Command + */ + public static boolean expire(String key, long timeout) { + return expire(key, timeout, TimeUnit.SECONDS); + } + + /** + * 设置过期时间 + * @param key 待修改过期时间的 key + * @param timeout 时长 + * @param timeUnit 时间单位 + */ + public static boolean expire(String key, long timeout, TimeUnit timeUnit) { + return Boolean.TRUE.equals(redisTemplate.expire(key, timeout, timeUnit)); + } + + /** + * 设置 key 的过期时间到指定的日期 + * @param key 待修改过期时间的 key + * @param date 过期时间 + * @return 修改成功返回 true + * @see ExpireAt Command + */ + public static boolean expireAt(String key, Date date) { + return Boolean.TRUE.equals(redisTemplate.expireAt(key, date)); + } + + public static boolean expireAt(String key, Instant expireAt) { + return Boolean.TRUE.equals(redisTemplate.expireAt(key, expireAt)); + } + + /** + * 获取所有符合指定表达式的 key + * @param pattern 表达式 + * @return java.util.Set + * @see Keys Command + */ + public static Set keys(String pattern) { + return redisTemplate.keys(pattern); + } + + /** + * TTL 命令返回 {@link RedisHelper#expire(String, long) EXPIRE} 命令设置的剩余生存时间(以秒为单位).。 + *

      + * 时间复杂度: O(1) + * @param key 待查询的 key + * @return TTL 以秒为单位,或负值以指示错误 + * @see TTL Command + */ + public static long ttl(String key) { + return redisTemplate.getExpire(key); + } + + /** + * 使用 Cursor 遍历指定规则的 keys + * @param scanOptions scan 的配置 + * @return Cursor,一个可迭代对象 + * @see Scan Command + */ + public static Cursor scan(ScanOptions scanOptions) { + return redisTemplate.scan(scanOptions); + } + + /** + * 使用 Cursor 遍历指定规则的 keys + * @param patten key 的规则 + * @return Cursor,一个可迭代对象 + */ + public static Cursor scan(String patten) { + ScanOptions scanOptions = ScanOptions.scanOptions().match(patten).build(); + return scan(scanOptions); + } + + /** + * 使用 Cursor 遍历指定规则的 keys + * @param patten key 的规则 + * @param count 一次扫描获取的 key 数量, 默认为 10 + * @return Cursor,一个可迭代对象 + * @see Scan Command + */ + public static Cursor scan(String patten, long count) { + ScanOptions scanOptions = ScanOptions.scanOptions().match(patten).count(count).build(); + return scan(scanOptions); + } + // ====================== key command end ================== + + // ---------------------- string command start --------------- + + /** + * 当 key 存在时,对其值进行自减操作 (自减步长为 1),当 key 不存在时,则先赋值为 0 再进行自减 + * @param key key + * @return 自减之后的 value 值 + * @see #decrBy(String, long) + */ + public static long decr(String key) { + return valueOps().decrement(key); + } + + /** + * 当 key 存在时,对其值进行自减操作,当 key 不存在时,则先赋值为 0 再进行自减 + * @param key key + * @param delta 自减步长 + * @return 自减之后的 value 值 + * @see DecrBy Command + */ + public static long decrBy(String key, long delta) { + return valueOps().decrement(key, delta); + } + + /** + * 获取指定 key 的 value 值 + * @param key 指定的 key + * @return 当 key 不存在时返回 null + * @see Get Command + */ + public static String get(String key) { + return valueOps().get(key); + } + + /** + * 获取指定 key 的 value 值,并将指定的 key 进行删除 + * @param key 指定的 key + * @return 当 key 不存在时返回 null + * @see GetDel Command + */ + public static String getDel(String key) { + return valueOps().getAndDelete(key); + } + + /** + * 获取指定 key 的 value 值,并对 key 设置指定的过期时间 + * @param key 指定的 key + * @param timeout 过期时间,单位时间秒 + * @return 当 key 不存在时返回 null + * @see GetEx Command + */ + public static String getEx(String key, long timeout) { + return getEx(key, timeout, TimeUnit.SECONDS); + } + + /** + * 获取指定 key 的 value 值,并对 key 设置指定的过期时间 + * @param key 指定的 key + * @param timeout 过期时间,单位时间秒 + * @param timeUnit 时间单位 + * @return 当 key 不存在时返回 null + * @see GetEx Command + */ + public static String getEx(String key, long timeout, TimeUnit timeUnit) { + return valueOps().getAndExpire(key, timeout, timeUnit); + } + + /** + * 获取指定的 key 的 value 值,并同时使用指定的 value 值进行覆盖操作 + * @param key 指定的 key + * @param value 新的 value 值 + * @return 当 key 存在时返回其 value 值,否则返回 null + * @see GetSet Command + */ + public static String getSet(String key, String value) { + return valueOps().getAndSet(key, value); + } + + /** + * 对 key 进行自增,自增步长为 1 + * @param key 需要自增的 key + * @return 自增后的 value 值 + * @see #incrBy(String, long) + */ + public static long incr(String key) { + return valueOps().increment(key); + } + + /** + * 对 key 进行自增 (步长为 1),并设置过期时间 + * @see #incrByAndExpire(String, long, long) + */ + public static long incrAndExpire(String key, long timeout) { + return incrByAndExpire(key, 1, timeout); + } + + /** + * 对 key 进行自增,并指定自增步长, 当 key 不存在时先创建一个值为 0 的 key,再进行自增 + * @param key 需要自增的 key + * @param delta 自增的步长 + * @return 自增后的 value 值 + * @see IncrBy Command + */ + public static long incrBy(String key, long delta) { + return valueOps().increment(key, delta); + } + + /** + * 对 key 进行自增并设置过期时间,指定自增步长, 当 key 不存在时先创建一个值为 0 的 key,再进行自增 + * @param key 需要自增的 key + * @param delta 自增的步长 + * @param timeout 过期时间(单位:秒) + * @return 自增后的 value 值 + */ + public static long incrByAndExpire(String key, long delta, long timeout) { + return redisTemplate.execute(INCR_BY_EXPIRE_LUA_SCRIPT, Collections.singletonList(key), String.valueOf(delta), + String.valueOf(timeout)); + } + + /** + * @see #incrBy(String, long) + */ + public static double incrByFloat(String key, double delta) { + return valueOps().increment(key, delta); + } + + /** + * 给 key +1 + * @deprecated {@link #incr(String)} + */ + @Deprecated + public static Long increment(String key) { + return valueOps().increment(key); + } + + /** + * 给 key 增加 指定数值 + * @deprecated {@link #incrBy(String, long)} + */ + @Deprecated + public static Long increment(String key, long delta) { + return valueOps().increment(key, delta); + } + + /** + * @deprecated {@link #incrAndExpire(String, long)}} + */ + @Deprecated + public static Long incrementAndExpire(String key, long time) { + return incrementAndExpire(key, 1, time); + } + + /** + * @deprecated {@link #incrByAndExpire(String, long, long)}} + */ + @Deprecated + public static Long incrementAndExpire(String key, long delta, long time) { + Long increment = valueOps().increment(key, delta); + expire(key, time); + return increment; + } + + /** + * 从指定的 keys 批量获取 values + * @param keys keys + * @return values list,当值为空时,该 key 对应的 value 为 null + * @see MGet Command + */ + public static List mGet(Collection keys) { + return valueOps().multiGet(keys); + } + + /** + * @see #mGet(Collection) + */ + public static List mGet(String... keys) { + return mGet(Arrays.asList(keys)); + } + + /** + * 批量获取 keys 的值,并返回一个 map + * @param keys keys + * @return map,key 和 value 的键值对集合,当 value 获取为 null 时,不存入此 map + */ + public static Map mGetToMap(Collection keys) { + List values = valueOps().multiGet(keys); + Map map = new HashMap<>(keys.size()); + if (values == null || values.isEmpty()) { + return map; + } + + Iterator keysIterator = keys.iterator(); + Iterator valuesIterator = values.iterator(); + while (keysIterator.hasNext()) { + String key = keysIterator.next(); + String value = valuesIterator.next(); + if (value != null) { + map.put(key, value); + } + } + return map; + } + + /** + * @see #mGetToMap(Collection) + */ + public static Map mGetToMap(String... keys) { + return mGetToMap(Arrays.asList(keys)); + } + + /** + * @deprecated {@link #mGet(Collection)} + */ + @Deprecated + public static List multiGet(Collection keys) { + List list = valueOps().multiGet(keys); + return list == null ? new ArrayList<>() : new ArrayList<>(list); + } + + /** + * 设置 value for key + * @param key 指定的 key + * @param value 值 + * @see Set Command + */ + public static void set(String key, String value) { + valueOps().set(key, value); + } + + /** + * 设置 value for key, 同时为其设置过期时间 + * @param key key + * @param value value + * @param timeout 过期时间 单位:秒 + * @see #setEx(String, String, long) + */ + public static void set(String key, String value, long timeout) { + set(key, value, timeout, TimeUnit.SECONDS); + } + + /** + * 设置 value for key, 同时为其设置过期时间 + * @param key key + * @param value value + * @param timeout 过期时间 单位:秒 + * @param timeUnit 过期时间单位 + * @see #setEx(String, String, long,TimeUnit) + */ + public static void set(String key, String value, long timeout, TimeUnit timeUnit) { + setEx(key, value, timeout, timeUnit); + } + + /** + * 缓存数据 + * @param key key + * @param val val + * @param instant 在指定时间过期 + * @deprecated {{@link #setExAt(String, String, Instant)}} + */ + @Deprecated + public static void set(String key, String val, Instant instant) { + valueOps().set(key, val); + getRedisTemplate().expireAt(key, instant); + } + + /** + * 设置 value for key, 同时为其设置过期时间 + * @param key 指定的 key + * @param value 值 + * @param timeout 过期时间 + * @see SetEx Command + */ + public static void setEx(String key, String value, long timeout) { + setEx(key, value, timeout, TimeUnit.SECONDS); + } + + /** + * 设置 value for key, 同时为其设置过期时间 + * @param key 指定的 key + * @param value 值 + * @param timeout 过期时间 + * @param timeUnit 时间单位 + * @see SetEx Command + */ + public static void setEx(String key, String value, long timeout, TimeUnit timeUnit) { + valueOps().set(key, value, timeout, timeUnit); + } + + /** + * 设置 value for key, 同时为其设置其在指定时间过期 + * @param key key + * @param value value + * @param expireTime 在指定时间过期 + */ + public static void setExAt(String key, String value, Instant expireTime) { + long timeout = expireTime.getEpochSecond() - Instant.now().getEpochSecond(); + setEx(key, value, timeout); + } + + /** + * 当 key 不存在时,进行 value 设置,当 key 存在时不执行操作 + * @param key key + * @param value value + * @return boolean + * @see SetNX Command + */ + public static boolean setNx(String key, String value) { + return Boolean.TRUE.equals(valueOps().setIfAbsent(key, value)); + } + + /** + * 如果 key 不存在,则设置 key为 val + * @param key key + * @param value val + * @return boolean + * @deprecated {@link #setNx(String, String)} + */ + @Deprecated + public static boolean setIfAbsent(String key, String value) { + Boolean b = valueOps().setIfAbsent(key, value); + return b != null && b; + } + + /** + * 当 key 不存在时,进行 value 设置并添加过期时间,当 key 存在时不执行操作 + * @param key key + * @param value value + * @param timeout 过期时间 + * @return boolean 操作是否成功 + * @see SetNX Command + */ + public static boolean setNxEx(String key, String value, long timeout) { + return setNxEx(key, value, timeout, TimeUnit.SECONDS); + } + + /** + * 当 key 不存在时,进行 value 设置并添加过期时间,当 key 存在时不执行操作 + * @param key key + * @param value value + * @param timeout 过期时间 + * @param timeUnit 时间单位 + * @return boolean 操作是否成功 + * @see SetNX Command + */ + public static boolean setNxEx(String key, String value, long timeout, TimeUnit timeUnit) { + return Boolean.TRUE.equals(valueOps().setIfAbsent(key, value, timeout, timeUnit)); + } + + /** + * 如果key存在则设置 + * @param key key + * @param value 值 + * @param time 过期时间, 单位 秒 + * @return boolean + * @deprecated {@link #setNxEx(String, String, long)} + */ + @Deprecated + public static boolean setIfAbsent(String key, String value, long time) { + Boolean b = valueOps().setIfAbsent(key, value, Duration.ofSeconds(time)); + return b != null && b; + } + // ----------------------- string command end ------------- + + // ---------------------- hash command start --------------- + /** + * 删除指定 hash 的 fields + * @param key hash 的 key + * @param fields hash 元素的 field 集合 + * @return 删除的 field 数量 + * @see HDel Command + */ + public static long hDel(String key, String... fields) { + return hashOps().delete(key, (Object[]) fields); + } + + /** + * 判断指定 hash 的 指定 field 是否存在 + * @param key hash 的 key + * @param field 元素的 field + * @return 存在返回 {@code true}, 否则返回 {@code false} + * @see HExists Command + */ + public static boolean hExists(String key, String field) { + return hashOps().hasKey(key, field); + } + + /** + * 获取 hash 中的指定 field 对应的 value 值 + * @param key hash 的 key + * @param field 元素的 field + * @see HGet Command + */ + public static String hGet(String key, String field) { + return hashOps().get(key, field); + } + + /** + * 获取 hash 中所有的 fields 和 values, 并已键值对的方式返回 + * @param key hash 的 key + * @see HGetAll Command + */ + public static Map hGetAll(String key) { + return hashOps().entries(key); + } + + /** + * 对 hash 中指定的 field 进行自增 + *

      + * 若 field 不存在则,先设置为 0 再进行自增,若 hash 不存在则先创建 hash 再进行上述步骤 + * @param key key + * @param field field + * @param delta 自增步长 + * @return 自增后的 value 值 + * @see HIncrBy Command + */ + public static long hIncrBy(String key, String field, long delta) { + return hashOps().increment(key, field, delta); + } + + /** + * 对 hash 中指定的 field 进行自增 + * @see #hIncrBy(String, String, long) + */ + public static Long hIncrBy(String key, String field) { + return hIncrBy(key, field, 1); + } + + /** + * 对 hash 中指定的 field 进行自增 + *

      + * 若 field 不存在则,先设置为 0 再进行自增,若 hash 不存在则先创建 hash 再进行上述步骤 + * @param key key + * @param field field + * @param delta 自增步长 + * @return 自增后的 value 值 + * @see HIncrByFloat Command + */ + public static double hIncrByFloat(String key, String field, double delta) { + return hashOps().increment(key, field, delta); + } + + /** + * 返回 hash 中的所有 fields + * @param key hash 的 key + * @return Set of fields in hash + * @see HKeys Command + */ + public static Set hKeys(String key) { + return hashOps().keys(key); + } + + /** + * 返回 hash 中 fields 的数量 + * @param key hash 的 key + * @return fields size + * @see HLen Command + */ + public static long hLen(String key) { + return hashOps().size(key); + } + + /** + * 返回 hash 中指定 fields 的值集合 + * @param key hash 的 key + * @return fields value list, 按传入的 fields 顺序排列 + * @see HKeys Command + */ + public static List hMGet(String key, Collection fields) { + return hashOps().multiGet(key, fields); + } + + /** + * 返回 hash 中指定 fields 的值集合 + * @param key hash 的 key + * @return fields value list, 按传入的 fields 顺序排列 + * @see HKeys Command + */ + public static List hMGet(String key, String... fields) { + return hashOps().multiGet(key, Arrays.asList(fields)); + } + + /** + * 修改 hash 中的 field 的值,有则覆盖,无则添加 + * @param key hash 的 key + * @param field field + * @param value value + * @see HSet Command + */ + public static void hSet(String key, String field, String value) { + hashOps().put(key, field, value); + } + + /** + * @deprecated {@link #hSet(String, String, String)} + */ + @Deprecated + public static void hashSet(String key, String field, String value) { + hashOps().put(key, field, value); + } + + /** + * 修改 hash 中的 field 的值,有则不进行操作,无则添加 + * @param key hash 的 key + * @param field field + * @param value value + * @see HSetNx Command + */ + public static void hSetNx(String key, String field, String value) { + hashOps().putIfAbsent(key, field, value); + } + + /** + * 返回 hash 中的所有 values + * @param key hash 的 key + * @return List of fields in hash + * @see HVals Command + */ + public static List hVals(String key) { + return hashOps().values(key); + } + + /** + * 获取 指定 key 中 指定 field 的值 + * @param key key + * @param field field + * @return java.lang.Object + * @deprecated {@link #hGet(String, String)} + */ + @Deprecated + public static String hashGet(String key, String field) { + Object o = hashOps().get(key, field); + return o == null ? null : o.toString(); + } + + /** + * 移除指定 key中的 字段 + * @param key key + * @param fields 字段 + * @return java.lang.Long + * @deprecated {@link #hDel(String, String...)} + */ + @Deprecated + public static Long hashDelete(String key, String... fields) { + return hashOps().delete(key, (Object[]) fields); + } + // -------------------------- hash command end -------------------------------- + + // -------------------------- list command start -------------------------------- + + /** + * 获取指定 list 指定索引位置的元素 + * @param key list 的 key + * @param index 索引位置,0 表示第一个元素,负数索引用于指定从尾部开始计数,-1 表示最后一个元素,-2 倒数第二个 + * @return 返回对应索引位置的元素,不存在时为 null + * @see LIndex Command + */ + public static String lIndex(String key, long index) { + return listOps().index(key, index); + } + + /** + * 获知指定key中指定索引的值 + * @deprecated {@link #lIndex(String, long)} + */ + @Deprecated + public static String listIndex(String key, long index) { + return listOps().index(key, index); + } + + /** + * 获取指定 list 的元素个数即长度 + * @param key list 的 key + * @return 返回 list 的长度,当 list 不存在时返回 0 + * @see LLen Command + */ + public static long lLen(String key) { + return listOps().size(key); + } + + /** + * @deprecated {@link #lLen(String)} + */ + @Deprecated + public static long listSize(String key) { + Long size = listOps().size(key); + return size == null ? 0 : size; + } + + /** + * 以原子方式返回并删除列表的第一个元素,例如列表包含元素 "a", "b", "c" LPOP 操作将返回 ”a“ 并将其删除,list 中元素变为 ”b“, "c" + * @param key list 的 key + * @return 返回弹出的元素 + * @see LPop Command + */ + public static String lPop(String key) { + return listOps().leftPop(key); + } + + /** + * 以原子方式返回并删除列表的多个元素 + * @param key list 的 key + * @param count 弹出的个数 + * @return 返回弹出的元素列表,key 不存在时为 null + * @see LPop Command + * @since Redis 版本大于等于 6.2.0 + */ + public static List lPop(String key, long count) { + return listOps().leftPop(key, count); + } + + /** + * @deprecated {@link #lPop(String)} + */ + @Deprecated + public static String listLeftPop(String key) { + return listOps().leftPop(key); + } + + /** + * 该命令返回 list 匹配元素的索引。它会从头到尾扫描列表,寻找 “element” 的第一个匹配项。 + * @param key list 的 key + * @param element 查找的元素 + * @return 指定元素正向第一个匹配项的索引,如果找不到,返回 null + * @see LPos Command + * @since Redis 版本大于等于 6.0.6 + */ + public static Long lPos(String key, String element) { + return listOps().indexOf(key, element); + } + + /** + * 获取指定值在指定key中的索引 + * @deprecated {@link #lPos(String, String)} + */ + @Deprecated + public static Long listIndexOf(String key, String val) { + return listOps().indexOf(key, val); + } + + /** + * 将指定的元素插入 list 的头部,若 list 不存在,则先指向创建一个空的 list + * @param key list 的 key + * @param elements 插入的元素 + * @return 插入后的 list 长度 + * @see LPush Command + */ + public static long lPush(String key, String... elements) { + return listOps().leftPushAll(key, elements); + } + + /** + * 将指定的值插入 list 的头部,若 list 不存在,则先指向创建一个空的 list + * @param key list 的 key + * @param elements 插入的元素 + * @return 插入后的 list 长度 + * @see LPush Command + */ + public static long lPush(String key, List elements) { + return listOps().leftPushAll(key, elements); + } + + /** + * 插入列表 + * @param key key + * @param val val + * @deprecated {@link #lPush(String, String...)} + */ + @Deprecated + public static Long listLeftPush(String key, String val) { + return listOps().leftPush(key, val); + } + + /** + * 获取 list 指定 offset 间的元素。 + * @param key list 的 key + * @param start begin offset, 从 0 开始,0 表示列表第一个元素,也可以为负数,表示从 list 末尾开始的偏移量, -1 + * 是列表最后第一个元素 + * @param end end offset,值规则 同 start + * @return 元素集合 + * @see LRange Command + */ + public static List lRange(String key, long start, long end) { + return listOps().range(key, start, end); + } + + /** + * 获取 list 中的所有元素 + * @deprecated use lRange(key, 0, -1) {@link #lRange(String, long, long)} + */ + @Deprecated + public static List listGet(String key) { + return listOps().range(key, 0, listSize(key) - 1); + } + + /** + * 删除 list 中的元素 + *

        + *
      • count > 0: 从 list 头部向尾部查找并删除 n 个和指定值相等的元素,n 为 count + *
      • count < 0: 从 list 尾部向头部查找并删除 n 个和指定值相等的元素,n 为 count 的绝对值 + *
      • count = 0: 删除 list 中所有和指定值相等的元素 + *
      + * @param key list 的 key + * @param count 删除的数量以及规则 + * @param value 待删除的元素值 + * @return 移除元素的数量 + * @see LRem Command + */ + public static long lRem(String key, long count, String value) { + return listOps().remove(key, count, value); + } + + /** + * use lrem(key, 1, value) {@link #lRem(String, long, String)} + */ + @Deprecated + public static Long listRemove(String key, String val) { + return listRemove(key, 1, val); + } + + /** + * @param count 删除多少个 + */ + @Deprecated + public static Long listRemove(String key, long count, String val) { + return listOps().remove(key, count, val); + } + + @Deprecated + private static long listSet(String key, List list) { + long l = 0; + for (String str : list) { + l += listLeftPush(key, str); + } + return l; + } + + /** + * 插入list 并设置过期时间 + * @param key key + * @param list list 值 + * @param time 过期时间 + * @return long + */ + @Deprecated + public static long listSet(String key, List list, long time) { + long l = listSet(key, list); + expire(key, time); + return l; + } + + /** + * 将 list 指定 index 位置的元素设置为当前值 + * @param key list 的 key + * @param index 索引位置,0 表示第一个元素,负数索引用于指定从尾部开始计数,-1 表示最后一个元素,-2 倒数第二个 + * @param value 值 + * @see LSet Command + */ + public static void lSet(String key, long index, String value) { + listOps().set(key, index, value); + } + + /** + * 裁剪 list,只保留 start 到 end 之间的元素值,包含 start 和 end + * @param key list 的 key + * @param start 开始索引位置,0 表示第一个元素,负数索引用于指定从尾部开始计数,-1 表示最后一个元素,-2 倒数第二个 + * @param end 结束的索引位置 + * @see LTrim Command + */ + public static void lTrim(String key, long start, long end) { + listOps().trim(key, start, end); + } + + /** + * 以原子方式返回并删除列表的最后一个元素。 + *

      + * 例如 list 包含元素 "a"、"b"、"c", RPOP 操作将返回 ”c“ 并将其删除,list 中元素变为 ”a“, "b" + * @param key list 的 key + * @return 弹出的元素 + * @see RPOP Command + */ + public static String rPop(String key) { + return listOps().rightPop(key); + } + + /** + * 从 list 尾部,以原子方式返回并删除列表中指定数量的元素。 + * @param key list 的 key + * @param count 待弹出的元素数量 + * @return 弹出的元素集合 + * @see RPOP Command + * @since Redis 6.2.0 + */ + public static List rPop(String key, long count) { + return listOps().rightPop(key, count); + } + + /** + * 将指定的值插入 list 的尾部,若 list 不存在,则先指向创建一个空的 list + * @param key list 的 key + * @param values 插入的元素 + * @return 插入后的 list 长度 + * @see RPush Command + */ + public static long rPush(String key, String... values) { + return listOps().rightPushAll(key, values); + } + + /** + * 将指定的值插入 list 的尾部,若 list 不存在,则先指向创建一个空的 list + * @param key list 的 key + * @param values 插入的元素 + * @return 插入后的 list 长度 + * @see RPush Command + */ + public static long rPush(String key, List values) { + return listOps().rightPushAll(key, values); + } + + /** + * @deprecated {@link #rPush(String, String...)} + */ + @Deprecated + public static Long listRightPush(String key, String val) { + return listOps().rightPush(key, val); + } + + /** + * @deprecated {@link #rPop(String)} + */ + @Deprecated + public static String listRightPop(String key) { + return listOps().rightPop(key); + } + // -------------------------- list command end -------------------------------- + + // -------------------------- Set command start -------------------------------- + + /** + * 将指定的 member 添加到 Set 中,如果 Set 中已有该 member 则忽略。如果 Set 不存在,则先创建一个新的 Set,再进行添加 + *

      + * Time complexity O(1) + * @param key Set 的 key + * @param members 添加的成员 + * @return 添加到集合中的元素数量,不包括集合中已经存在的所有元素 + * @see SAdd Command + */ + public static long sAdd(String key, String... members) { + return setOps().add(key, members); + } + + /** + * 将指定的 member 添加到 Set 中,如果 Set 中已有该 member 则忽略。如果 Set 不存在,则先创建一个新的 Set,再进行添加 + *

      + * Time complexity O(1) + * @param key Set 的 key + * @param members 添加的成员 + * @return 添加到集合中的元素数量,不包括集合中已经存在的所有元素 + * @see SAdd Command + */ + public static long sAdd(String key, List members) { + return setOps().add(key, members.toArray(new String[0])); + } + + /** + * Set中添加数据 + * @deprecated {@link #sAdd(String, String...)} + */ + @Deprecated + public static Long setAdd(String key, String... values) { + return setOps().add(key, values); + } + + /** + * 返回 Set 中的元素数,如果 set 不存在则返回 0 + * @param key Set 的 key + * @return The cardinality (number of elements) of the set + * @see SCard Command + */ + public static long sCard(String key) { + return setOps().size(key); + } + + /** + * 获取集合中元素数量 + * @deprecated {@link #sCard(String)} + */ + @Deprecated + public static Long setSize(String key) { + return setOps().size(key); + } + + /** + * 判断指定的值是否是 Set 中的元素 + *

      + * Time complexity O(1) + * @param key Set 的 key + * @param value 待判断的值 + * @return 如果是 Set 中的元素返回{@code true}, 否则返回{@code false} + * @see SIsMember Command + */ + public static boolean sIsMember(String key, String value) { + return setOps().isMember(key, value); + } + + /** + * 获取 Set 中的所有元素 + *

      + * Time complexity O(N) + * @param key Set 的 key + * @return Set 中的所有元素 + * @see SMembers Command + */ + public static Set sMembers(String key) { + return setOps().members(key); + } + + /** + * 判断指定的值是否是 Set 中的元素 + *

      + * Time complexity O(N) + * @param key Set 的 key + * @param values 待判断的值集合 + * @return 一个 Map, key 为待判断的值,value 为结果 + * @see SMIsMember Command + * @since Redis 6.2.0 + */ + public static Map sMIsMember(String key, String... values) { + return setOps().isMember(key, (Object[]) values); + } + + /** + * 随机从 Set 中删除一个元素,并返回它,如果 Set 为空,则返回 null + *

      + * Time complexity O(1) + * @param key Set 的 key + * @return 弹出的元素,或者 null + * @see SPop Command + */ + public static String sPop(String key) { + return setOps().pop(key); + } + + /** + * 随机弹出一个元素 + * @deprecated {@link #sPop(String)} + */ + @Deprecated + public static String setPop(String key) { + return setOps().pop(key); + } + + /** + * 随机从 Set 中返回一个元素,但不删除,如果 Set 为空,则返回 null + *

      + * Time complexity O(1) + * @param key Set 的 key + * @return 随机选中的元素或者 null + * @see SRandMember Command + */ + public static String sRandMember(String key) { + return setOps().randomMember(key); + } + + /** + * 随机从 Set 中返回 count 个元素,但不删除,如果 Set 为空,则返回 null + *

      + * Time complexity O(1) + * @param key Set 的 key + * @param count 随机返回的元素数量 + * @return 随机选中的元素或者 null + * @see SRandMember Command + */ + public static Set sRandMember(String key, long count) { + return setOps().distinctRandomMembers(key, count); + } + + /** + * 从 Set 中删除指定的 member,如果给的值不是 Set 的 member 则不进行操作 + *

      + * Time complexity O(1) + * @param key Set 的 key + * @param members 待删除的成员 + * @return The number of members that were removed from the set, not including + * non-existing members + * @see SRem Command + */ + public static long sRem(String key, String... members) { + return setOps().remove(key, (Object[]) members); + } + + /** + * 移除集合中的元素 + * @deprecated {@link #sRem(String, String...)} + */ + @Deprecated + public static Long setRemove(String key, String... values) { + return setOps().remove(key, (Object[]) values); + } + + /** + * 使用 Cursor 遍历指定 Set 中的所有元素 + * @param scanOptions scan 的配置 + * @return Cursor,一个可迭代对象 + * @see SScan Command + */ + public static Cursor sScan(String key, ScanOptions scanOptions) { + return setOps().scan(key, scanOptions); + } + + // -------------------------- Set command end -------------------------------- + + // ---------------------- Sorted Set command start ---------------------------- + + /** + * 添加拥有指定 score 的 member 到 Sorted Set 中。如果 member 在 Sorted Set 中已存在,则更新 score,并进行重排序。 + * 如果 key 不存在,则先创建一个空的 Sorted Set 再进行添加操作。 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @param score 分数 + * @param member 成员 + * @return 当元素被成功添加时返回 true,当元素存在时返回 false(分数会更新) + * @see ZAdd Command + */ + public static boolean zAdd(String key, double score, String member) { + return zSetOps().add(key, member, score); + } + + /** + * 批量添加拥有指定 score 的 member 到 Sorted Set 中。如果 member 在 Sorted Set 中已存在,则更新 + * score,并进行重排序。 如果 key 不存在,则先创建一个空的 Sorted Set 再进行添加操作。 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @param scoreMembers 成员和分数的键值对 + * @return 返回被成功添加的成员数 + * @see ZAdd Command + */ + public static long zAdd(String key, Map scoreMembers) { + Set> tuples = scoreMembers.entrySet() + .stream() + .map(x -> ZSetOperations.TypedTuple.of(x.getKey(), x.getValue())) + .collect(Collectors.toSet()); + return zSetOps().add(key, tuples); + } + + /** + * zset中添加数据 + * @deprecated {@link #zAdd(String, double, String)} + */ + @Deprecated + public static Boolean zSetAdd(String key, String value, double score) { + return zSetOps().add(key, value, score); + } + + /** + * 返回 Sorted Set 的元素数量,若 key 不存在则返回 0 + *

      + * Time complexity O(1) + * @param key Sorted Set 的 key + * @return Sorted Set 中的元素数量 + * @see ZCard Command + */ + public static long zCard(String key) { + return zSetOps().size(key); + } + + /** + * 获取有序集合中元素数量 + * @deprecated {@link #zCard(String)} + */ + @Deprecated + public static Long zSetSize(String key) { + return zSetOps().size(key); + } + + /** + * 如果 member 存在于 Sorted Set 中,则对其 score 和 increment 进行相加运算,并重排序。
      + * 如果 member 不存在,则先添加一个 score 为 0 的 member 再进行相加操作。
      + * 如果 key 不存在,则先创建一个 Sorted Set,再进行上述操作。 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @param increment 增长步长,可以为负数 + * @param member 成员 + * @return The new score + * @see ZIncrBy Command + */ + public static double zIncrBy(String key, double increment, String member) { + return zSetOps().incrementScore(key, member, increment); + } + + /** + * 返回并删除 Sorted Set 中分数最高的那个元素 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @return 弹出的 member 和 score + * @see ZPopMax Command + * @since Redis 5.0.0 + */ + public static ZSetOperations.TypedTuple zPopMax(String key) { + return zSetOps().popMax(key); + } + + /** + * 返回并删除 Sorted Set 中分数最高的 n 个元素 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @param count 弹出的个数 + * @return 弹出的 member 和 score + * @see ZPopMax Command + * @since Redis 5.0.0 + */ + public static Set> zPopMax(String key, long count) { + return zSetOps().popMax(key, count); + } + + /** + * 返回并删除 Sorted Set 中分数最低的那个元素 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @return 弹出的 member 和 score + * @see ZPopMin Command + * @since Redis 5.0.0 + */ + public static ZSetOperations.TypedTuple zPopMin(String key) { + return zSetOps().popMin(key); + } + + /** + * 返回并删除 Sorted Set 中分数最低的 n 个元素 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @param count 弹出的个数 + * @return 弹出的 member 和 score + * @see ZPopMin Command + * @since Redis 5.0.0 + */ + public static Set> zPopMin(String key, long count) { + return zSetOps().popMin(key, count); + } + + /** + * 随机从 Sorted Set 中返回一个 member + *

      + * Time complexity O(N) where N is the number of elements returned + * @param key Sorted Set 的 Key + * @return Random String from the set + * @see ZRandMember Command + * @since Redis 6.2.0 + */ + public static String zRandMember(String key) { + return zSetOps().randomMember(key); + } + + /** + * 随机弹出一个元素 + * @deprecated {@link #zRandMember(String)} + */ + @Deprecated + public static String zSetRandom(String key) { + return zSetOps().randomMember(key); + } + + /** + * 返回 Sorted Set 中指定索引范围内的 member. + *

      + * Time complexity O(log(N)+M) with N being the number of elements in the sorted set + * and M the number of elements returned. + * @param key the key to query + * @param start the minimum index + * @param end the maximum index + * @return A Set of Strings in the specified range + * @see ZRange Command + */ + public static Set zRange(String key, long start, long end) { + return zSetOps().range(key, start, end); + } + + /** + * 返回 Sorted Set 中指定 score 间的所有元素(包括 score 等于 min 和 max 的元素) + *

      + * Time complexity O(log(N)+M) with N being the number of elements in the sorted set + * and M the number of elements being returned. + * @param key the key to query + * @param min minimum score + * @param max maximum score + * @return A List of elements in the specified score range + * @see ZRangeByScore Command + */ + public static Set zRangeByScore(String key, double min, double max) { + return zSetOps().rangeByScore(key, min, max); + } + + /** + * 返回 Sorted Set 中指定 score 间的所有元素(包括 score 等于 min 和 max 的元素) + * @param key the key to query + * @param min minimum score + * @param max maximum score + * @param offset 偏移量 + * @param count 获取的元素数 + * @return A List of elements in the specified score range + * @see ZRangeByScore Command + */ + public static Set zRangeByScore(String key, double min, double max, long offset, long count) { + return zSetOps().rangeByScore(key, min, max, offset, count); + } + + /** + * 返回 Sorted Set 中指定 score 间的所有元素和其分数(包括 score 等于 min 和 max 的元素) + * @param key the key to query + * @param min minimum score + * @param max maximum score + * @return A List of elements in the specified score range + * @see ZRangeByScore Command + */ + public static Set> zRangeByScoreWithScores(String key, double min, double max) { + return zSetOps().rangeByScoreWithScores(key, min, max); + } + + /** + * 返回 member 的排名(索引)。排名从 0 开始,按分数从低到高的顺序. + *

      + * Time complexity O(log(N)) + * @param key Sorted Set 的 key + * @param member 成员 + * @return 如果 member 存在的话返回其排名,否则返回 null + * @see ZRank Command + */ + public static Long zRank(String key, String member) { + return zSetOps().rank(key, member); + } + + /** + * 从 Sorted Set 中删除指定的 member。不存在的 member 将被忽略。 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @param members 待删除的成员 + * @return 从排序集中删除的 member 数,不包括不存在的 member 数 + * @see ZRem Command + */ + public static long zRem(String key, String... members) { + return zSetOps().remove(key, (Object[]) members); + } + + /** + * 移除有序集合中的元素 + * @deprecated {@link #zRem(String, String...)} + */ + @Deprecated + public static Long zSetRemove(String key, String... values) { + return zSetOps().remove(key, (Object[]) values); + } + + /** + * 在有序集合中的排名 + * @deprecated {@link #zRank(String, String)} + */ + @Deprecated + public static Long zSetRank(String key, String value) { + return zSetOps().rank(key, value); + } + + /** + * 返回 Sorted Set 中 index 在 start 和 end 之前的所有成员(包括 start 和 end)。 + *

      + * 与默认的排序规则相反,元素的顺序是按分数从高到低进行的,具有相同分数的元素以相反的字典顺序排序 + *

      + * Time complexity O(log(N)+M) with N being the number of elements in the sorted set + * and M the number of elements returned. + * @param key the key to query + * @param start the minimum index + * @param end the maximum index + * @return A List of Strings in the specified range + * @see ZRevRange Commad + */ + public static Set zRevRange(String key, long start, long end) { + return zSetOps().reverseRange(key, start, end); + } + + /** + * 返回 Sorted Set 中分数在 min 和 max 之前的所有成员(包括 min 和 max)。 + *

      + * 与默认的排序规则相反,元素的顺序是按分数从高到低进行的。 + *

      + * 具有相同分数的元素以相反的字典顺序返回. + *

      + * Time complexity O(log(N)+M) with N being the number of elements in the sorted set + * and M the number of elements being returned. + * @param key the key to query + * @param min minimum score + * @param max maximum score + * @return A List of elements in the specified score range + * @see ZRevRangeByScore + * Commad + */ + public static Set zRevRangeByScore(String key, double min, double max) { + return zSetOps().reverseRangeByScore(key, min, max); + } + + /** + * 返回 Sorted Set 中指定 member 的分数。如果指定的 member 在 Sorted Set 中不存在,或者 Key 根本不存在,则返回 null。 + *

      + * Time complexity: O(1) + * @param key Sorted Set Key + * @param member Sorted Set Member + * @return the score + * @see ZSCORE Commad + */ + public static Double zScore(String key, String member) { + return zSetOps().score(key, member); + } + + /** + * 在有序集合中的排名, 从小到大 + * @deprecated {@link #zRange(String, long, long)} + */ + @Deprecated + public static Set zSetRange(String key, int start, int end) { + return zSetOps().range(key, start, end); + } + + /** + * 在有序集合中的排名, 从大到小 + * @deprecated {@link #zRevRange(String, long, long)} + */ + @Deprecated + public static Set zSetReverseRange(String key, int start, int end) { + return zSetOps().reverseRange(key, start, end); + } + + /** + * 在有序集合中的排名, 分数区间, 从小到大 + * @deprecated {@link #zRangeByScore(String, double, double)} + */ + @Deprecated + public static Set zSetRangeByScore(String key, double min, double max) { + return zSetOps().rangeByScore(key, min, max); + } + + /** + * 在有序集合中的排名, 分数区间, 从大到小 + * @deprecated {@link #zRevRangeByScore(String, double, double)} + */ + @Deprecated + public static Set zSetReverseRangeByScore(String key, double min, double max) { + return zSetOps().reverseRangeByScore(key, min, max); + } + + // -------------------------------- lua 脚本 -------------------------- + /** + * 执行 lua脚本 + * @param action redis 操作 + * @return T + */ + public static T execute(RedisCallback action) { + return redisTemplate.execute(action); + } + + @Nullable + public static T execute(RedisCallback action, boolean exposeConnection) { + return execute(action, exposeConnection, false); + } + + @Nullable + public static T execute(RedisCallback action, boolean exposeConnection, boolean pipeline) { + return redisTemplate.execute(action, exposeConnection, pipeline); + } + + public static T execute(SessionCallback session) { + return redisTemplate.execute(session); + } + + public static T execute(RedisScript script, List keys, Object... args) { + return redisTemplate.execute(script, keys, args); + } + + public static T execute(RedisScript script, RedisSerializer argsSerializer, + RedisSerializer resultSerializer, List keys, Object... args) { + return redisTemplate.execute(script, argsSerializer, resultSerializer, keys, args); + } + + /** + * @deprecated RedisScript 应该做为单例,而不是每次生成,不推荐直接使用此方法 + */ + @Deprecated + public static Object evalLua(String lua, List key, Object... argv) { + String[] arg = Arrays.stream(argv).map(String::valueOf).toArray(String[]::new); + + try { + RedisScript redisScript = new DefaultRedisScript<>(lua, String.class); + return redisTemplate.execute(redisScript, RedisSerializer.string(), RedisSerializer.string(), key, arg); + } + catch (Exception e) { + log.error("redis evalLua execute fail:lua[{}]", lua, e); + return "false"; + } + } + + // ----------------------- pipelined 操作 -------------------- + + public static List executePipelined(SessionCallback session) { + return redisTemplate.executePipelined(session); + } + + public static List executePipelined(SessionCallback session, + @Nullable RedisSerializer resultSerializer) { + return redisTemplate.executePipelined(session, resultSerializer); + } + + public static List executePipelined(RedisCallback action) { + return redisTemplate.executePipelined(action); + } + + public static List executePipelined(RedisCallback action, + @Nullable RedisSerializer resultSerializer) { + return redisTemplate.executePipelined(action, resultSerializer); + } + + // =================== PUB/SUB command start ================= + + /** + * Publish 一条消息 + * @param channel 渠道 + * @param message 消息 + */ + public static void publish(String channel, String message) { + redisTemplate.convertAndSend(channel, message); + } + + /** + * Publish 一条消息 + * @param channel 渠道 + * @param message 消息 + */ + public static void publish(String channel, byte[] message) { + redisTemplate.convertAndSend(channel, message); + } + + // =================== PUB/SUB command end ================= + + // =================== Stream command start ================= + + /** + * XACK key group ID [ID ...] + * @param key key of stream + * @param group consume group + * @param ids record ids + * @see XACK Command + * @since Redis 5.0.0 + */ + public static long xAck(String key, String group, String... ids) { + return streamOps().acknowledge(key, group, ids); + } + + public static long xAck(String key, String group, RecordId... ids) { + return streamOps().acknowledge(key, group, ids); + } + + /** + * XADD key ID field string [field string ...] + * @param key key of stream + * @param content record content + * @return the ID of the added entry + * @see XADD Command + * @since Redis 5.0.0 + */ + public static RecordId xAdd(String key, Map content) { + return streamOps().add(StreamRecords.newRecord().in(key).ofMap(content)); + } + + public static RecordId xAdd(String key, Map content, RedisStreamCommands.XAddOptions xAddOptions) { + return xAdd(Record.of(content).withStreamKey(key), xAddOptions); + } + + public static RecordId xAdd(MapRecord mapRecord, + RedisStreamCommands.XAddOptions xAddOptions) { + RedisSerializer keySerializer = getKeySerializer(); + RedisSerializer valueSerializer = getValueSerializer(); + + byte[] rawKey = keySerializer.serialize(mapRecord.getStream()); + + Map content = mapRecord.getValue(); + Map rawContent = new LinkedHashMap<>(content.size()); + + for (Map.Entry entry : content.entrySet()) { + rawContent.put(keySerializer.serialize(entry.getKey()), valueSerializer.serialize(entry.getValue())); + } + + return redisTemplate.execute((RedisConnection conn) -> conn.streamCommands() + .xAdd(Record.of(rawContent).withStreamKey(rawKey), xAddOptions)); + } + + /** + * XDEL key ID [ID ...] + * @param key key of stream + * @param ids record ids + * @see XDEL Command + * @since Redis 5.0.0 + */ + public static long xDel(String key, String... ids) { + return streamOps().delete(key, ids); + } + + public static long xDel(String key, RecordId... ids) { + return streamOps().delete(key, ids); + } + + /** + * XGROUP CREATE + * @param key key of stream + * @param groupName group name + * @see XGROUP CREATE Command + * @since Redis 5.0.0 + */ + public static String xGroupCreate(String key, String groupName, ReadOffset readOffset, boolean makeStream) { + RedisSerializer keySerializer = getKeySerializer(); + byte[] rawKey = keySerializer.serialize(key); + + return redisTemplate.execute((RedisConnection conn) -> conn.streamCommands() + .xGroupCreate(rawKey, groupName, readOffset, makeStream)); + } + + public static String xGroupCreate(String key, String groupName) { + return xGroupCreate(key, groupName, ReadOffset.latest(), true); + } + + /** + * XLEN key + * @param key key of stream + * @return length of stream + * @see XLEN Command + * @since Redis 5.0.0 + */ + public static long xLen(String key) { + return streamOps().size(key); + } + + /** + * XRANGE key start end COUNT count + * @param key key of stream + * @param range start and end + * @return The entries with IDs matching the specified range. + * @see XRANGE Command + * @since Redis 5.0.0 + */ + public static List> xRange(String key, Range range) { + return streamOps().range(key, range); + } + + public static List> xRange(String key, Range range, + RedisZSetCommands.Limit limit) { + return streamOps().range(key, range, limit); + } + + /** + * XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...] + * + * @see XREAD Command + * @since Redis 5.0.0 + */ + public static List> xRead(StreamOffset... streams) { + return streamOps().read(streams); + } + + public static List> xRead(StreamReadOptions streamReadOptions, + StreamOffset... streams) { + return streamOps().read(streamReadOptions, streams); + } + + /** + * XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS + * key [key ...] id [id ...] + * + * @see XREADGROUP Command + * @since Redis 5.0.0 + */ + public static List> xReadGroup(Consumer consumer, + StreamOffset... streams) { + return streamOps().read(consumer, streams); + } + + public static List> xReadGroup(Consumer consumer, + StreamReadOptions streamReadOptions, StreamOffset... streams) { + return streamOps().read(consumer, streamReadOptions, streams); + } + + public static List> xReadGroup(String group, String consumer, + StreamOffset... streams) { + return streamOps().read(Consumer.from(group, consumer), streams); + } + + public static List> xReadGroup(String group, String consumer, + StreamReadOptions streamReadOptions, StreamOffset... streams) { + return streamOps().read(Consumer.from(group, consumer), streamReadOptions, streams); + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/CacheStringAspect.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/CacheStringAspect.java new file mode 100644 index 0000000..388b144 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/CacheStringAspect.java @@ -0,0 +1,263 @@ +package com.baiye.core; + +import com.baiye.RedisHelper; +import com.baiye.upms.config.CachePropertiesHolder; +import com.baiye.core.annotation.Cached; +import com.baiye.lock.DistributedLock; +import com.baiye.operation.CacheDelOps; +import com.baiye.operation.CacheDelsOps; +import com.baiye.operation.CachePutOps; +import com.baiye.operation.CachedOps; +import com.baiye.operation.function.VoidMethod; +import com.baiye.serialize.CacheSerializer; +import com.baiye.core.annotation.CacheDel; +import com.baiye.core.annotation.CacheDels; +import com.baiye.core.annotation.CachePut; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * 为保证缓存更新无异常,该切面优先级必须高于事务切面 + * + * @author Hccake + * @date 2019/8/31 18:01 + * @version 1.0 + */ +@Aspect +@Order(Ordered.LOWEST_PRECEDENCE - 1) +public class CacheStringAspect { + + Logger log = LoggerFactory.getLogger(CacheStringAspect.class); + + private final CacheSerializer cacheSerializer; + + private final StringRedisTemplate redisTemplate; + + public CacheStringAspect(StringRedisTemplate redisTemplate, CacheSerializer cacheSerializer) { + this.redisTemplate = redisTemplate; + this.cacheSerializer = cacheSerializer; + } + + @Pointcut("execution(@(@com.baiye.core.annotation.MetaCacheAnnotation *) * *(..))") + public void pointCut() { + // do nothing + } + + @Around("pointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + + // 获取目标方法 + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + + if (log.isTraceEnabled()) { + log.trace("=======The string cache aop is executed! method : {}", method.getName()); + } + + // 根据方法的参数 以及当前类对象获得 keyGenerator + Object target = point.getTarget(); + Object[] arguments = point.getArgs(); + KeyGenerator keyGenerator = new KeyGenerator(target, method, arguments); + + ValueOperations valueOperations = redisTemplate.opsForValue(); + + // 缓存处理 + Cached cachedAnnotation = AnnotationUtils.getAnnotation(method, Cached.class); + if (cachedAnnotation != null) { + // 缓存key + String key = keyGenerator.getKey(cachedAnnotation.key(), cachedAnnotation.keyJoint()); + // redis 分布式锁的 key + String lockKey = key + CachePropertiesHolder.lockKeySuffix(); + Supplier cacheQuery = () -> valueOperations.get(key); + // 失效时间控制 + Consumer cachePut = prodCachePutFunction(valueOperations, key, cachedAnnotation.ttl(), + cachedAnnotation.timeUnit()); + return cached(new CachedOps(point, lockKey, cacheQuery, cachePut, method.getGenericReturnType())); + + } + + // 缓存更新处理 + CachePut cachePutAnnotation = AnnotationUtils.getAnnotation(method, CachePut.class); + if (cachePutAnnotation != null) { + // 缓存key + String key = keyGenerator.getKey(cachePutAnnotation.key(), cachePutAnnotation.keyJoint()); + // 失效时间控制 + Consumer cachePut = prodCachePutFunction(valueOperations, key, cachePutAnnotation.ttl(), + cachePutAnnotation.timeUnit()); + return cachePut(new CachePutOps(point, cachePut)); + } + + // 缓存删除处理 + CacheDel cacheDelAnnotation = AnnotationUtils.getAnnotation(method, CacheDel.class); + if (cacheDelAnnotation != null) { + return cacheDel(new CacheDelOps(point, buildCacheDelExecution(cacheDelAnnotation, keyGenerator))); + } + + // 多个缓存删除处理 + CacheDels cacheDelsAnnotation = AnnotationUtils.getAnnotation(method, CacheDels.class); + if (cacheDelsAnnotation != null) { + int annotationCount = cacheDelsAnnotation.value().length; + VoidMethod[] cacheDels = new VoidMethod[annotationCount]; + for (int i = 0; i < annotationCount; i++) { + cacheDels[i] = buildCacheDelExecution(cacheDelsAnnotation.value()[i], keyGenerator); + } + return cacheDels(new CacheDelsOps(point, cacheDels)); + } + + return point.proceed(); + } + + private Consumer prodCachePutFunction(ValueOperations valueOperations, String key, long ttl, + TimeUnit unit) { + Consumer cachePut; + if (ttl < 0) { + cachePut = value -> valueOperations.set(key, (String) value); + } + else if (ttl == 0) { + cachePut = value -> valueOperations.set(key, (String) value, CachePropertiesHolder.expireTime(), unit); + } + else { + cachePut = value -> valueOperations.set(key, (String) value, ttl, unit); + } + return cachePut; + } + + /** + * cached 类型的模板方法 1. 先查缓存 若有数据则直接返回 2. 尝试获取锁 若成功执行目标方法(一般是去查数据库) 3. 将数据库获取到数据同步至缓存 + * @param ops 缓存操作类 + * @return result + * @throws IOException IO 异常 + */ + public Object cached(CachedOps ops) throws Throwable { + + // 缓存查询方法 + Supplier cacheQuery = ops.cacheQuery(); + // 返回数据类型 + Type dataClazz = ops.returnType(); + + // 1.==================尝试从缓存获取数据========================== + String cacheData = cacheQuery.get(); + // 如果是空值 则return null | 不是空值且不是null 则直接返回 + if (ops.nullValue(cacheData)) { + return null; + } + else if (cacheData != null) { + return cacheSerializer.deserialize(cacheData, dataClazz); + } + + // 2.==========如果缓存为空 则需查询数据库并更新=============== + cacheData = DistributedLock.instance().action(ops.lockKey(), () -> { + String cacheValue = cacheQuery.get(); + if (cacheValue == null) { + // 从数据库查询数据 + Object dbValue = ops.joinPoint().proceed(); + // 如果数据库中没数据,填充一个String,防止缓存击穿 + cacheValue = dbValue == null ? CachePropertiesHolder.nullValue() : cacheSerializer.serialize(dbValue); + // 设置缓存 + ops.cachePut().accept(cacheValue); + } + return cacheValue; + }).onLockFail(cacheQuery).lock(); + // 自旋时间内未获取到锁,或者数据库中数据为空,返回null + if (cacheData == null || ops.nullValue(cacheData)) { + return null; + } + return cacheSerializer.deserialize(cacheData, dataClazz); + } + + /** + * 缓存操作模板方法 + */ + private Object cachePut(CachePutOps ops) throws Throwable { + + // 先执行目标方法 并拿到返回值 + Object data = ops.joinPoint().proceed(); + + // 将返回值放置入缓存中 + String cacheData = data == null ? CachePropertiesHolder.nullValue() : cacheSerializer.serialize(data); + ops.cachePut().accept(cacheData); + + return data; + } + + /** + * 缓存删除的模板方法 在目标方法执行后 执行删除 + */ + private Object cacheDel(CacheDelOps ops) throws Throwable { + + // 先执行目标方法 并拿到返回值 + Object data = ops.joinPoint().proceed(); + // 将删除缓存 + ops.cacheDel().run(); + + return data; + } + + /** + * 缓存批量删除的模板方法 在目标方法执行后 执行删除 + */ + private Object cacheDels(CacheDelsOps ops) throws Throwable { + + // 先执行目标方法 并拿到返回值 + Object data = ops.joinPoint().proceed(); + // 将删除缓存 + for (VoidMethod voidMethod : ops.cacheDel()) { + voidMethod.run(); + } + return data; + } + + /** + * 构建缓存删除执行方法 + * @param cacheDelAnnotation 缓存删除注解 + * @param keyGenerator 缓存键生成器 + * @return 用于执行的无返回值方法 + */ + private VoidMethod buildCacheDelExecution(CacheDel cacheDelAnnotation, KeyGenerator keyGenerator) { + VoidMethod cacheDel; + if (cacheDelAnnotation.allEntries()) { + // 优先判断是否是删除名称空间下所有的键值对 + cacheDel = () -> { + Cursor scan = RedisHelper.scan(cacheDelAnnotation.key().concat("*")); + while (scan.hasNext()) { + redisTemplate.delete(scan.next()); + } + if (!scan.isClosed()) { + scan.close(); + } + }; + } + else { + if (cacheDelAnnotation.multiDel()) { + Collection keys = keyGenerator.getKeys(cacheDelAnnotation.key(), cacheDelAnnotation.keyJoint()); + cacheDel = () -> redisTemplate.delete(keys); + } + else { + // 缓存key + String key = keyGenerator.getKey(cacheDelAnnotation.key(), cacheDelAnnotation.keyJoint()); + cacheDel = () -> redisTemplate.delete(key); + } + } + return cacheDel; + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/KeyGenerator.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/KeyGenerator.java new file mode 100644 index 0000000..1109c98 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/KeyGenerator.java @@ -0,0 +1,87 @@ +package com.baiye.core; + +import cn.hutool.core.lang.Assert; +import com.baiye.upms.config.CachePropertiesHolder; +import com.baiye.util.SpelUtils; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 缓存key的生成工具类,主要用于解析spel, 进行拼接key的生成 + * + * @author Hccake + * @version 1.0 + * @date 2019/9/3 9:58 + */ +public class KeyGenerator { + + /** + * SpEL 上下文 + */ + StandardEvaluationContext spelContext; + + public KeyGenerator(Object target, Method method, Object[] arguments) { + this.spelContext = SpelUtils.getSpelContext(target, method, arguments); + } + + /** + * 根据 keyPrefix 和 keyJoint 获取完整的 key 信息 + * @param keyPrefix key 前缀 + * @param keyJoint key 拼接元素,值为 spel 表达式,可为空 + * @return 拼接完成的 key + */ + public String getKey(String keyPrefix, String keyJoint) { + // 根据 keyJoint 判断是否需要拼接 + if (keyJoint == null || keyJoint.length() == 0) { + return keyPrefix; + } + // 获取所有需要拼接的元素, 组装进集合中 + String joint = SpelUtils.parseValueToString(spelContext, keyJoint); + Assert.notNull(joint, "Key joint cannot be null!"); + + if (!StringUtils.hasText(keyPrefix)) { + return joint; + } + // 拼接后返回 + return jointKey(keyPrefix, joint); + } + + public List getKeys(String keyPrefix, String keyJoint) { + // keyJoint 必须有值 + Assert.notEmpty(keyJoint, "[getKeys] keyJoint cannot be null"); + + // 获取所有需要拼接的元素, 组装进集合中 + List joints = SpelUtils.parseValueToStringList(spelContext, keyJoint); + Assert.notEmpty(joints, "[getKeys] keyJoint must be resolved to a non-empty collection!"); + + if (!StringUtils.hasText(keyPrefix)) { + return joints; + } + // 拼接后返回 + return joints.stream().map(x -> jointKey(keyPrefix, x)).collect(Collectors.toList()); + } + + /** + * 拼接key, 默认使用 :作为分隔符 + * @param keyItems 用于拼接 key 的元素列表 + * @return 拼接完成的 key + */ + public String jointKey(List keyItems) { + return String.join(CachePropertiesHolder.delimiter(), keyItems); + } + + /** + * 拼接key, 默认使用 :作为分隔符 + * @param keyItems 用于拼接 key 的元素列表 + * @return 拼接完成的 key + */ + public String jointKey(String... keyItems) { + return jointKey(Arrays.asList(keyItems)); + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/CacheDel.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/CacheDel.java new file mode 100644 index 0000000..f61e6df --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/CacheDel.java @@ -0,0 +1,46 @@ +package com.baiye.core.annotation; + +import java.lang.annotation.*; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/8/31 16:08 利用Aop, 在方法执行后执行缓存删除操作 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@MetaCacheAnnotation +@Repeatable(CacheDels.class) +public @interface CacheDel { + + /** + * redis 存储的Key名 + */ + String key(); + + /** + * 如果需要在key 后面拼接参数 则传入一个拼接数据的 SpEL 表达式 + */ + String keyJoint() default ""; + + /** + * 清除多个 key,当值为 true 时,强制要求 keyJoint 有值,且 Spel 表达式解析结果为 Collection + * @return boolean + */ + boolean multiDel() default false; + + /** + *

      + * 是否删除缓存空间{@link #key}下的所有条目 + *

      + *

      + * 默认情况下,只删除相关键下的值。 + *

      + *

      + * 注意,设置该参数为{@code true}时,指定的 {@link #keyJoint}与{@link #multiDel} 将被忽略. + *

      + */ + boolean allEntries() default false; + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/CacheDels.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/CacheDels.java new file mode 100644 index 0000000..83cd7ed --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/CacheDels.java @@ -0,0 +1,19 @@ +package com.baiye.core.annotation; + +import java.lang.annotation.*; + +/** + * 删除 + * + * @author lishangbu + * @date 2022/10/29 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@MetaCacheAnnotation +public @interface CacheDels { + + CacheDel[] value(); + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/CachePut.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/CachePut.java new file mode 100644 index 0000000..66cff9e --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/CachePut.java @@ -0,0 +1,38 @@ +package com.baiye.core.annotation; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/8/31 16:08 利用Aop, 在方法执行后执行缓存put操作 将方法的返回值置入缓存中,若方法返回null,则会默认置入一个nullValue + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@MetaCacheAnnotation +public @interface CachePut { + + /** + * redis 存储的Key名 + */ + String key(); + + /** + * 如果需要在key 后面拼接参数 则传入一个拼接数据的 SpEL 表达式 + */ + String keyJoint() default ""; + + /** + * 超时时间(S) ttl = 0 使用全局配置值 ttl < 0 : 不超时 ttl > 0 : 使用此超时间 + */ + long ttl() default 0; + + /** + * 控制时长单位,默认为 SECONDS 秒 + * @return {@link TimeUnit} + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/Cached.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/Cached.java new file mode 100644 index 0000000..a673742 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/Cached.java @@ -0,0 +1,38 @@ +package com.baiye.core.annotation; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/8/31 16:08 利用Aop, 在方法调用前先查询缓存 若缓存中没有数据,则调用方法本身,并将方法返回值放置入缓存中 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@MetaCacheAnnotation +public @interface Cached { + + /** + * redis 存储的Key名 + */ + String key(); + + /** + * 如果需要在key 后面拼接参数 则传入一个拼接数据的 SpEL 表达式 + */ + String keyJoint() default ""; + + /** + * 超时时间(S) ttl = 0 使用全局配置值 ttl < 0 : 不超时 ttl > 0 : 使用此超时间 + */ + long ttl() default 0; + + /** + * 控制时长单位,默认为 SECONDS 秒 + * @return {@link TimeUnit} + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/MetaCacheAnnotation.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/MetaCacheAnnotation.java new file mode 100644 index 0000000..c9aefe1 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/core/annotation/MetaCacheAnnotation.java @@ -0,0 +1,15 @@ +package com.baiye.core.annotation; + +import java.lang.annotation.*; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/8/31 16:08 用于标识切点的元注解 + */ +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MetaCacheAnnotation { + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/AbstractDeletedKeyEventMessageListener.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/AbstractDeletedKeyEventMessageListener.java new file mode 100644 index 0000000..8110aae --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/AbstractDeletedKeyEventMessageListener.java @@ -0,0 +1,30 @@ +package com.baiye.keyevent.listener; + +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.Topic; + +/** + * key deleted event + * + * @author lishangbu 2023/1/12 + */ +public abstract class AbstractDeletedKeyEventMessageListener extends AbstractKeySpaceEventMessageListener { + + private static final Topic KEYEVENT_DELETED_TOPIC = new PatternTopic("__keyevent@*__:del"); + + /** + * Creates new {@link MessageListener} for specific messages. + * @param listenerContainer must not be {@literal null}. + */ + protected AbstractDeletedKeyEventMessageListener(RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + @Override + public Topic getKeyEventTopic() { + return KEYEVENT_DELETED_TOPIC; + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/AbstractExpiredKeyEventMessageListener.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/AbstractExpiredKeyEventMessageListener.java new file mode 100644 index 0000000..6dbca3e --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/AbstractExpiredKeyEventMessageListener.java @@ -0,0 +1,34 @@ +package com.baiye.keyevent.listener; + +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.Topic; + +/** + * key expired event + *

      + * similar with {@link KeyExpirationEventMessageListener} + *

      + * + * @author lishangbu 2023/1/12 + */ +public abstract class AbstractExpiredKeyEventMessageListener extends AbstractKeySpaceEventMessageListener { + + private static final Topic KEYEVENT_EXPIRED_TOPIC = new PatternTopic("__keyevent@*__:expired"); + + /** + * Creates new {@link MessageListener} for specific messages. + * @param listenerContainer must not be {@literal null}. + */ + protected AbstractExpiredKeyEventMessageListener(RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + @Override + public Topic getKeyEventTopic() { + return KEYEVENT_EXPIRED_TOPIC; + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/AbstractKeySpaceEventMessageListener.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/AbstractKeySpaceEventMessageListener.java new file mode 100644 index 0000000..6d85071 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/AbstractKeySpaceEventMessageListener.java @@ -0,0 +1,75 @@ +package com.baiye.keyevent.listener; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.core.RedisKeyExpiredEvent; +import org.springframework.data.redis.core.RedisKeyspaceEvent; +import org.springframework.data.redis.listener.KeyspaceEventMessageListener; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.Topic; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +/** + * {@link MessageListener} publishing {@link RedisKeyspaceEvent}s via + * {@link ApplicationEventPublisher} by listening to Redis keyspace notifications for + * specific key event. + * + * @author lishangbu 2023/1/12 + */ +public abstract class AbstractKeySpaceEventMessageListener extends KeyspaceEventMessageListener + implements ApplicationEventPublisherAware { + + @Nullable + protected ApplicationEventPublisher publisher; + + /** + * Creates new {@link MessageListener} for specific messages. + * @param listenerContainer must not be {@literal null}. + */ + protected AbstractKeySpaceEventMessageListener(@NonNull RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + /** + * Register instance within the container. + * @param listenerContainer never {@literal null}. + */ + @Override + protected void doRegister(@NonNull RedisMessageListenerContainer listenerContainer) { + listenerContainer.addMessageListener(this, getKeyEventTopic()); + } + + /** + * Handle the actual message + * @param message never {@literal null}. + */ + @Override + protected void doHandleMessage(@NonNull Message message) { + publishEvent(new RedisKeyExpiredEvent<>(message.getBody())); + } + + /** + * Publish the event in case an {@link ApplicationEventPublisher} is set. + * @param event can be {@literal null}. + */ + protected void publishEvent(RedisKeyExpiredEvent event) { + if (publisher != null) { + this.publisher.publishEvent(event); + } + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.publisher = applicationEventPublisher; + } + + /** + * Creates new {@link Topic} for listening + * @return Topic + */ + public abstract Topic getKeyEventTopic(); + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/AbstractSetKeyEventMessageListener.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/AbstractSetKeyEventMessageListener.java new file mode 100644 index 0000000..25ecb1c --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/AbstractSetKeyEventMessageListener.java @@ -0,0 +1,30 @@ +package com.baiye.keyevent.listener; + +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.Topic; + +/** + * key set event + * + * @author lishangbu 2023/1/12 + */ +public abstract class AbstractSetKeyEventMessageListener extends AbstractKeySpaceEventMessageListener { + + private static final Topic KEYEVENT_SET_TOPIC = new PatternTopic("__keyevent@*__:set"); + + /** + * Creates new {@link MessageListener} for specific messages. + * @param listenerContainer must not be {@literal null}. + */ + protected AbstractSetKeyEventMessageListener(RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + @Override + public Topic getKeyEventTopic() { + return KEYEVENT_SET_TOPIC; + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/DefaultDeletedKeyEventMessageListener.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/DefaultDeletedKeyEventMessageListener.java new file mode 100644 index 0000000..4163789 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/DefaultDeletedKeyEventMessageListener.java @@ -0,0 +1,59 @@ +package com.baiye.keyevent.listener; + +import com.baiye.keyevent.template.KeyDeletedEventMessageTemplate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * default key deleted event handler + * + * @author lishangbu + * @date 2023/1/12 + */ +@Slf4j +public class DefaultDeletedKeyEventMessageListener extends AbstractDeletedKeyEventMessageListener { + + protected List keyDeletedEventMessageTemplates; + + /** + * Creates new {@link MessageListener} for specific messages. + * @param listenerContainer must not be {@literal null}. + */ + public DefaultDeletedKeyEventMessageListener(RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + public DefaultDeletedKeyEventMessageListener(RedisMessageListenerContainer listenerContainer, + ObjectProvider> objectProvider) { + super(listenerContainer); + objectProvider.ifAvailable(templates -> this.keyDeletedEventMessageTemplates = new ArrayList<>(templates)); + } + + @Override + public void onMessage(Message message, byte[] pattern) { + if (CollectionUtils.isEmpty(keyDeletedEventMessageTemplates)) { + return; + } + super.onMessage(message, pattern); + String setKey = message.toString(); + // 监听key信息新增/修改事件 + for (KeyDeletedEventMessageTemplate keyDeletedEventMessageTemplate : keyDeletedEventMessageTemplates) { + if (keyDeletedEventMessageTemplate.support(setKey)) { + if (log.isTraceEnabled()) { + log.trace("use template [{}] handle key deleted event,the deleted key is [{}]", + keyDeletedEventMessageTemplate.getClass().getName(), setKey); + } + keyDeletedEventMessageTemplate.handleMessage(setKey); + } + } + + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/DefaultExpiredKeyEventMessageListener.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/DefaultExpiredKeyEventMessageListener.java new file mode 100644 index 0000000..0ed57df --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/DefaultExpiredKeyEventMessageListener.java @@ -0,0 +1,59 @@ +package com.baiye.keyevent.listener; + +import com.baiye.keyevent.template.KeyExpiredEventMessageTemplate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * default key expired event handler + * + * @author lishangbu + * @date 2023/1/12 + */ +@Slf4j +public class DefaultExpiredKeyEventMessageListener extends AbstractExpiredKeyEventMessageListener { + + protected List keyExpiredEventMessageTemplates; + + /** + * Creates new {@link MessageListener} for specific messages. + * @param listenerContainer must not be {@literal null}. + */ + public DefaultExpiredKeyEventMessageListener(RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + public DefaultExpiredKeyEventMessageListener(RedisMessageListenerContainer listenerContainer, + ObjectProvider> objectProvider) { + super(listenerContainer); + objectProvider.ifAvailable(templates -> this.keyExpiredEventMessageTemplates = new ArrayList<>(templates)); + } + + @Override + public void onMessage(Message message, byte[] pattern) { + if (CollectionUtils.isEmpty(keyExpiredEventMessageTemplates)) { + return; + } + super.onMessage(message, pattern); + String expiredKey = message.toString(); + // listening key expired event + for (KeyExpiredEventMessageTemplate keyExpiredEventMessageTemplate : keyExpiredEventMessageTemplates) { + if (keyExpiredEventMessageTemplate.support(expiredKey)) { + if (log.isTraceEnabled()) { + log.trace("use template[{}]handle key expired event,the expired key is [{}]", + keyExpiredEventMessageTemplate.getClass().getName(), expiredKey); + } + keyExpiredEventMessageTemplate.handleMessage(expiredKey); + } + } + + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/DefaultSetKeyEventMessageListener.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/DefaultSetKeyEventMessageListener.java new file mode 100644 index 0000000..10c9efb --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/listener/DefaultSetKeyEventMessageListener.java @@ -0,0 +1,59 @@ +package com.baiye.keyevent.listener; + +import com.baiye.keyevent.template.KeySetEventMessageTemplate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * default key set event handler + * + * @author lishangbu + * @date 2023/1/12 + */ +@Slf4j +public class DefaultSetKeyEventMessageListener extends AbstractSetKeyEventMessageListener { + + protected List keySetEventMessageTemplates; + + /** + * Creates new {@link MessageListener} for specific messages. + * @param listenerContainer must not be {@literal null}. + */ + public DefaultSetKeyEventMessageListener(RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + public DefaultSetKeyEventMessageListener(RedisMessageListenerContainer listenerContainer, + ObjectProvider> objectProvider) { + super(listenerContainer); + objectProvider.ifAvailable(templates -> this.keySetEventMessageTemplates = new ArrayList<>(templates)); + } + + @Override + public void onMessage(Message message, byte[] pattern) { + if (CollectionUtils.isEmpty(keySetEventMessageTemplates)) { + return; + } + super.onMessage(message, pattern); + String setKey = message.toString(); + // 监听key信息新增/修改事件 + for (KeySetEventMessageTemplate keySetEventMessageTemplate : keySetEventMessageTemplates) { + if (keySetEventMessageTemplate.support(setKey)) { + if (log.isTraceEnabled()) { + log.trace("use template[{}]handle key set event,the set key is[{}]", + keySetEventMessageTemplate.getClass().getName(), setKey); + } + keySetEventMessageTemplate.handleMessage(setKey); + } + } + + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/package-info.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/package-info.java new file mode 100644 index 0000000..7cc949f --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/package-info.java @@ -0,0 +1,4 @@ +/** + * 对redis key 新增/修改/删除/过期事件监听的封装 + */ +package com.baiye.keyevent; \ No newline at end of file diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/template/KeyDeletedEventMessageTemplate.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/template/KeyDeletedEventMessageTemplate.java new file mode 100644 index 0000000..eb708df --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/template/KeyDeletedEventMessageTemplate.java @@ -0,0 +1,11 @@ +package com.baiye.keyevent.template; + +/** + * key event message for redis key deleted event + * + * @author lishangbu + * @date 2023/1/12 + */ +public interface KeyDeletedEventMessageTemplate extends KeyEventMessageTemplate { + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/template/KeyEventMessageTemplate.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/template/KeyEventMessageTemplate.java new file mode 100644 index 0000000..48a90a7 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/template/KeyEventMessageTemplate.java @@ -0,0 +1,26 @@ +package com.baiye.keyevent.template; + +import org.springframework.lang.Nullable; + +/** + * common key event message template + * + * @author lishangbu + * @date 2023/1/12 + */ +public interface KeyEventMessageTemplate { + + /** + * handle actual message with notified key + * @param key notified key from redis server + */ + void handleMessage(@Nullable String key); + + /** + * support this template or not + * @param key notified key from redis server + * @return {@code true} is support and {@code false} is not support + */ + boolean support(@Nullable String key); + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/template/KeyExpiredEventMessageTemplate.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/template/KeyExpiredEventMessageTemplate.java new file mode 100644 index 0000000..ac7cc3f --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/template/KeyExpiredEventMessageTemplate.java @@ -0,0 +1,11 @@ +package com.baiye.keyevent.template; + +/** + * key event message for redis key expired event + * + * @author lishangbu + * @date 2023/1/12 + */ +public interface KeyExpiredEventMessageTemplate extends KeyEventMessageTemplate { + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/template/KeySetEventMessageTemplate.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/template/KeySetEventMessageTemplate.java new file mode 100644 index 0000000..b5c2e52 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/keyevent/template/KeySetEventMessageTemplate.java @@ -0,0 +1,11 @@ +package com.baiye.keyevent.template; + +/** + * key event message for redis set key event + * + * @author lishangbu + * @date 2023/1/12 + */ +public interface KeySetEventMessageTemplate extends KeyEventMessageTemplate { + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/listener/AbstractMessageEventListener.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/listener/AbstractMessageEventListener.java new file mode 100644 index 0000000..ee0a43b --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/listener/AbstractMessageEventListener.java @@ -0,0 +1,51 @@ +package com.baiye.listener; + +import com.baiye.util.JsonUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * redis消息监听处理器 + * + * @author huyuanzhi + */ +public abstract class AbstractMessageEventListener implements MessageEventListener { + + @Autowired + protected StringRedisTemplate stringRedisTemplate; + + protected final Class clz; + + @SuppressWarnings("unchecked") + protected AbstractMessageEventListener() { + Type superClass = getClass().getGenericSuperclass(); + ParameterizedType type = (ParameterizedType) superClass; + clz = (Class) type.getActualTypeArguments()[0]; + } + + @Override + public void onMessage(Message message, byte[] pattern) { + byte[] channelBytes = message.getChannel(); + RedisSerializer stringSerializer = stringRedisTemplate.getStringSerializer(); + String channelTopic = stringSerializer.deserialize(channelBytes); + String topic = topic().getTopic(); + if (topic.equals(channelTopic)) { + byte[] bodyBytes = message.getBody(); + String body = stringSerializer.deserialize(bodyBytes); + T decodeMessage = JsonUtils.toObj(body, clz); + handleMessage(decodeMessage); + } + } + + /** + * 处理消息 + * @param decodeMessage 反系列化之后的消息 + */ + protected abstract void handleMessage(T decodeMessage); + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/listener/MessageEventListener.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/listener/MessageEventListener.java new file mode 100644 index 0000000..593d0a4 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/listener/MessageEventListener.java @@ -0,0 +1,19 @@ +package com.baiye.listener; + +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.listener.Topic; + +/** + * PUB/SUB 模式中的消息监听者 + * + * @author hccake + */ +public interface MessageEventListener extends MessageListener { + + /** + * 订阅者订阅的话题 + * @return topic + */ + Topic topic(); + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/Action.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/Action.java new file mode 100644 index 0000000..59112ed --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/Action.java @@ -0,0 +1,46 @@ +package com.baiye.lock; + +import com.baiye.upms.config.CachePropertiesHolder; +import com.baiye.lock.function.ThrowingExecutor; + +import java.util.concurrent.TimeUnit; + +/** + * @author huyuanzhi 锁住的方法 + * @param 返回类型 + */ +@FunctionalInterface +public interface Action { + + /** + * 执行方法,采用配置文件中的默认过期时间 + * @param lockKey 待锁定的 key + * @param supplier 执行方法 + * @return 状态处理器 + */ + default StateHandler action(String lockKey, ThrowingExecutor supplier) { + return action(lockKey, CachePropertiesHolder.defaultLockTimeout(), TimeUnit.SECONDS, supplier); + } + + /** + * 执行方法 + * @param lockKey 待锁定的 key + * @param timeout 锁定过期时间,默认单位秒 + * @param supplier 执行方法 + * @return 状态处理器 + */ + default StateHandler action(String lockKey, long timeout, ThrowingExecutor supplier) { + return action(lockKey, timeout, TimeUnit.SECONDS, supplier); + } + + /** + * 执行方法 + * @param lockKey 待锁定的 key + * @param timeout 锁定过期时间 + * @param timeUnit 锁定过期时间单位 + * @param supplier 执行方法 + * @return 状态处理器 + */ + StateHandler action(String lockKey, long timeout, TimeUnit timeUnit, ThrowingExecutor supplier); + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/CacheLock.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/CacheLock.java new file mode 100644 index 0000000..994df1b --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/CacheLock.java @@ -0,0 +1,86 @@ +package com.baiye.lock; + +import com.baiye.RedisHelper; +import com.baiye.upms.config.CachePropertiesHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.script.DefaultRedisScript; + +import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * 缓存锁的操作类 + * + * @author Hccake 2020/3/27 21:15 + */ +@Slf4j +public final class CacheLock { + + private CacheLock() { + } + + /** + * 释放锁lua脚本 KEYS【1】:key值是为要加的锁定义的字符串常量 ARGV【1】:value值是 request id, 用来防止解除了不该解除的锁. 可用 + * UUID + */ + private static final DefaultRedisScript RELEASE_LOCK_LUA_SCRIPT = new DefaultRedisScript<>( + "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", + Long.class); + + /** + * 释放锁成功返回值 + */ + private static final Long RELEASE_LOCK_SUCCESS_RESULT = 1L; + + /** + * 上锁 + * @param lockKey 锁定标记 + * @param requestId 请求id + * @return Boolean 是否成功获得锁 + */ + public static Boolean lock(String lockKey, String requestId) { + return lock(lockKey, requestId, CachePropertiesHolder.defaultLockTimeout(), TimeUnit.SECONDS); + } + + /** + * 上锁 + * @param lockKey 锁定标记 + * @param requestId 请求id + * @param timeout 锁超时时间,单位秒 + * @return Boolean 是否成功获得锁 + */ + public static Boolean lock(String lockKey, String requestId, long timeout) { + return lock(lockKey, requestId, timeout, TimeUnit.SECONDS); + } + + /** + * 上锁 + * @param lockKey 锁定标记 + * @param requestId 请求id + * @param timeout 锁超时时间 + * @param timeUnit 时间过期单位 + * @return Boolean 是否成功获得锁 + */ + public static Boolean lock(String lockKey, String requestId, long timeout, TimeUnit timeUnit) { + if (log.isTraceEnabled()) { + log.trace("lock: {key:{}, clientId:{}}", lockKey, requestId); + } + return RedisHelper.setNxEx(lockKey, requestId, timeout, timeUnit); + } + + /** + * 释放锁 + * @param key 锁ID + * @param requestId 请求ID + * @return 是否成功 + */ + public static boolean releaseLock(String key, String requestId) { + if (log.isTraceEnabled()) { + log.trace("release lock: {key:{}, clientId:{}}", key, requestId); + } + Long result = RedisHelper.execute(RELEASE_LOCK_LUA_SCRIPT, Collections.singletonList(key), requestId); + return Objects.equals(result, RELEASE_LOCK_SUCCESS_RESULT); + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/DistributedLock.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/DistributedLock.java new file mode 100644 index 0000000..855add4 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/DistributedLock.java @@ -0,0 +1,106 @@ +package com.baiye.lock; + +import com.baiye.lock.function.ExceptionHandler; +import com.baiye.lock.function.ThrowingExecutor; +import org.springframework.util.Assert; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +/** + * @author huyuanzhi + * @version 1.0 + * @date 2021/11/16 分布式锁操作类 + */ +public final class DistributedLock implements Action, StateHandler { + + T result; + + String key; + + Long timeout; + + TimeUnit timeUnit; + + ThrowingExecutor executeAction; + + UnaryOperator successAction; + + Supplier lockFailAction; + + ExceptionHandler exceptionHandler = DistributedLock::throwException; + + public static Action instance() { + return new DistributedLock<>(); + } + + @Override + public StateHandler action(String lockKey, long timeout, TimeUnit timeUnit, ThrowingExecutor action) { + Assert.isTrue(this.executeAction == null, "execute action has been already set"); + Assert.notNull(action, "execute action cant be null"); + Assert.hasText(lockKey, "lock key cant be blank"); + this.executeAction = action; + this.key = lockKey; + this.timeout = timeout; + this.timeUnit = timeUnit; + return this; + } + + @Override + public StateHandler onSuccess(UnaryOperator action) { + Assert.isTrue(this.successAction == null, "success action has been already set"); + Assert.notNull(action, "success action cant be null"); + this.successAction = action; + return this; + } + + @Override + public StateHandler onLockFail(Supplier action) { + Assert.isTrue(this.lockFailAction == null, "lock fail action has been already set"); + Assert.notNull(action, "lock fail action cant be null"); + this.lockFailAction = action; + return this; + } + + @Override + public StateHandler onException(ExceptionHandler exceptionHandler) { + Assert.notNull(exceptionHandler, "exception handler cant be null"); + this.exceptionHandler = exceptionHandler; + return this; + } + + @Override + public T lock() { + String requestId = UUID.randomUUID().toString(); + if (Boolean.TRUE.equals(CacheLock.lock(this.key, requestId, this.timeout, this.timeUnit))) { + T value = null; + boolean exResolved = false; + try { + value = executeAction.execute(); + this.result = value; + } + catch (Throwable e) { + this.exceptionHandler.handle(e); + exResolved = true; + } + finally { + CacheLock.releaseLock(this.key, requestId); + } + if (!exResolved && this.successAction != null) { + this.result = this.successAction.apply(value); + } + } + else if (lockFailAction != null) { + this.result = lockFailAction.get(); + } + return this.result; + } + + @SuppressWarnings("unchecked") + private static void throwException(Throwable t) throws E { + throw (E) t; + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/StateHandler.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/StateHandler.java new file mode 100644 index 0000000..1d859a4 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/StateHandler.java @@ -0,0 +1,41 @@ +package com.baiye.lock; + +import com.baiye.lock.function.ExceptionHandler; + +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +/** + * @author huyuanzhi 状态处理器 + * @param 返回类型 + */ +public interface StateHandler { + + /** + * 获取锁成功,业务方法执行成功回调 + * @param action 回调方法引用 + * @return 状态处理器 + */ + StateHandler onSuccess(UnaryOperator action); + + /** + * 获取锁失败回调 + * @param action 回调方法引用 + * @return 状态处理器 + */ + StateHandler onLockFail(Supplier action); + + /** + * 获取锁成功,执行业务方法异常回调 + * @param action 回调方法引用 + * @return 状态处理器 + */ + StateHandler onException(ExceptionHandler action); + + /** + * 终态,获取锁 + * @return result + */ + T lock(); + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/function/ExceptionHandler.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/function/ExceptionHandler.java new file mode 100644 index 0000000..e51d5ba --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/function/ExceptionHandler.java @@ -0,0 +1,17 @@ +package com.baiye.lock.function; + +/** + * 异常处理器,可在处理完异常后再次抛出异常 + * + * @author hccake + */ +@FunctionalInterface +public interface ExceptionHandler { + + /** + * 处理异常 + * @param throwable 待处理的异常 + */ + void handle(Throwable throwable); + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/function/ThrowingExecutor.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/function/ThrowingExecutor.java new file mode 100644 index 0000000..57c894a --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/lock/function/ThrowingExecutor.java @@ -0,0 +1,18 @@ +package com.baiye.lock.function; + +/** + * 允许抛出异常的执行器 + * + * @author huyuanzhi + */ +public interface ThrowingExecutor { + + /** + * 可抛异常的supplier + * @return T + * @throws Throwable 异常 + */ + @SuppressWarnings("java:S112") + T execute() throws Throwable; + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/AbstractCacheOps.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/AbstractCacheOps.java new file mode 100644 index 0000000..6545855 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/AbstractCacheOps.java @@ -0,0 +1,36 @@ +package com.baiye.operation; + +import com.baiye.upms.config.CachePropertiesHolder; +import org.aspectj.lang.ProceedingJoinPoint; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/2 15:19 + */ +public abstract class AbstractCacheOps { + + protected AbstractCacheOps(ProceedingJoinPoint joinPoint) { + this.joinPoint = joinPoint; + } + + private final ProceedingJoinPoint joinPoint; + + /** + * 织入方法 + * @return ProceedingJoinPoint + */ + public ProceedingJoinPoint joinPoint() { + return joinPoint; + } + + /** + * 检查缓存数据是否是空值 + * @param cacheData 缓存数据 + * @return true: 是空值 + */ + public boolean nullValue(Object cacheData) { + return CachePropertiesHolder.nullValue().equals(cacheData); + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/CacheDelOps.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/CacheDelOps.java new file mode 100644 index 0000000..ace425f --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/CacheDelOps.java @@ -0,0 +1,27 @@ +package com.baiye.operation; + +import com.baiye.operation.function.VoidMethod; +import org.aspectj.lang.ProceedingJoinPoint; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/2 15:19 + */ +public class CacheDelOps extends AbstractCacheOps { + + /** + * 删除缓存数据 + */ + private final VoidMethod cacheDel; + + public CacheDelOps(ProceedingJoinPoint joinPoint, VoidMethod cacheDel) { + super(joinPoint); + this.cacheDel = cacheDel; + } + + public VoidMethod cacheDel() { + return cacheDel; + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/CacheDelsOps.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/CacheDelsOps.java new file mode 100644 index 0000000..98955a6 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/CacheDelsOps.java @@ -0,0 +1,27 @@ +package com.baiye.operation; + +import com.baiye.operation.function.VoidMethod; +import org.aspectj.lang.ProceedingJoinPoint; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/2 15:19 + */ +public class CacheDelsOps extends AbstractCacheOps { + + /** + * 删除缓存数据 + */ + private final VoidMethod[] cacheDels; + + public CacheDelsOps(ProceedingJoinPoint joinPoint, VoidMethod[] cacheDels) { + super(joinPoint); + this.cacheDels = cacheDels; + } + + public VoidMethod[] cacheDel() { + return cacheDels; + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/CachePutOps.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/CachePutOps.java new file mode 100644 index 0000000..6131413 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/CachePutOps.java @@ -0,0 +1,28 @@ +package com.baiye.operation; + +import org.aspectj.lang.ProceedingJoinPoint; + +import java.util.function.Consumer; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/2 15:19 + */ +public class CachePutOps extends AbstractCacheOps { + + /** + * 向缓存写入数据 + */ + private final Consumer cachePut; + + public CachePutOps(ProceedingJoinPoint joinPoint, Consumer cachePut) { + super(joinPoint); + this.cachePut = cachePut; + } + + public Consumer cachePut() { + return cachePut; + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/CachedOps.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/CachedOps.java new file mode 100644 index 0000000..db3112a --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/CachedOps.java @@ -0,0 +1,69 @@ +package com.baiye.operation; + +import org.aspectj.lang.ProceedingJoinPoint; + +import java.lang.reflect.Type; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/2 15:19 + */ +public class CachedOps extends AbstractCacheOps { + + /** + * 数据类型 + */ + private final Type returnType; + + /** + * 缓存分布式锁的key + */ + private final String lockKey; + + /** + * 从Redis中获取缓存数据的操作 + */ + private final Supplier cacheQuery; + + /** + * 向缓存写入数据 + */ + private final Consumer cachePut; + + /** + * 基本构造函数 + * @param joinPoint 织入方法 + * @param lockKey 分布式锁key + * @param cacheQuery 查询缓存函数 + * @param cachePut 更新缓存函数 + * @param returnType 返回数据类型 + */ + public CachedOps(ProceedingJoinPoint joinPoint, String lockKey, Supplier cacheQuery, + Consumer cachePut, Type returnType) { + super(joinPoint); + this.lockKey = lockKey; + this.cacheQuery = cacheQuery; + this.cachePut = cachePut; + this.returnType = returnType; + } + + public Supplier cacheQuery() { + return cacheQuery; + } + + public Consumer cachePut() { + return cachePut; + } + + public Type returnType() { + return returnType; + } + + public String lockKey() { + return lockKey; + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/function/ResultMethod.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/function/ResultMethod.java new file mode 100644 index 0000000..8b29cd8 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/function/ResultMethod.java @@ -0,0 +1,17 @@ +package com.baiye.operation.function; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/2 20:22 + */ +@FunctionalInterface +public interface ResultMethod { + + /** + * 执行并返回一个结果 + * @return result + */ + T run(); + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/function/VoidMethod.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/function/VoidMethod.java new file mode 100644 index 0000000..ab2020c --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/operation/function/VoidMethod.java @@ -0,0 +1,16 @@ +package com.baiye.operation.function; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/2 20:15 + */ +@FunctionalInterface +public interface VoidMethod { + + /** + * 只执行 无返回值 + */ + void run(); + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/prefix/IRedisPrefixConverter.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/prefix/IRedisPrefixConverter.java new file mode 100644 index 0000000..eee9c18 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/prefix/IRedisPrefixConverter.java @@ -0,0 +1,76 @@ +package com.baiye.prefix; + +import cn.hutool.core.text.CharSequenceUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; + +/** + * redis key前缀生成器 + * + * @author huyuanzhi + */ +public interface IRedisPrefixConverter { + + Logger LOGGER = LoggerFactory.getLogger(IRedisPrefixConverter.class); + + /** + * 生成前缀 + * @return 前缀 + */ + String getPrefix(); + + /** + * 前置匹配,是否走添加前缀规则 + * @return 是否匹配 + */ + boolean match(); + + /** + * 去除key前缀 + * @param bytes key字节数组 + * @return 原始key + */ + default byte[] unwrap(byte[] bytes) { + int wrapLen; + if (!match() || bytes == null || (wrapLen = bytes.length) == 0) { + return bytes; + } + String prefix = getPrefix(); + if (CharSequenceUtil.isBlank(prefix)) { + LOGGER.warn("prefix converter is enabled,but method getPrefix returns blank result,check your implement!"); + return bytes; + } + byte[] prefixBytes = prefix.getBytes(StandardCharsets.UTF_8); + int prefixLen = prefixBytes.length; + int originLen = wrapLen - prefixLen; + byte[] originBytes = new byte[originLen]; + System.arraycopy(bytes, prefixLen, originBytes, 0, originLen); + return originBytes; + } + + /** + * 给key加上固定前缀 + * @param bytes 原始key字节数组 + * @return 加前缀之后的key + */ + default byte[] wrap(byte[] bytes) { + int originLen; + if (!match() || bytes == null || (originLen = bytes.length) == 0) { + return bytes; + } + String prefix = getPrefix(); + if (CharSequenceUtil.isBlank(prefix)) { + LOGGER.warn("prefix converter is enabled,but method getPrefix returns blank result,check your implement!"); + return bytes; + } + byte[] prefixBytes = prefix.getBytes(StandardCharsets.UTF_8); + int prefixLen = prefixBytes.length; + byte[] wrapBytes = new byte[prefixLen + originLen]; + System.arraycopy(prefixBytes, 0, wrapBytes, 0, prefixLen); + System.arraycopy(bytes, 0, wrapBytes, prefixLen, originLen); + return wrapBytes; + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/prefix/impl/DefaultRedisPrefixConverter.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/prefix/impl/DefaultRedisPrefixConverter.java new file mode 100644 index 0000000..e11fb99 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/prefix/impl/DefaultRedisPrefixConverter.java @@ -0,0 +1,28 @@ +package com.baiye.prefix.impl; + +import com.baiye.prefix.IRedisPrefixConverter; + +/** + * redis key前缀默认转换器 + * + * @author huyuanzhi + */ +public class DefaultRedisPrefixConverter implements IRedisPrefixConverter { + + private final String prefix; + + public DefaultRedisPrefixConverter(String prefix) { + this.prefix = prefix; + } + + @Override + public String getPrefix() { + return prefix; + } + + @Override + public boolean match() { + return true; + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/serialize/CacheSerializer.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/serialize/CacheSerializer.java new file mode 100644 index 0000000..572c1d3 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/serialize/CacheSerializer.java @@ -0,0 +1,62 @@ +package com.baiye.serialize; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/9 11:09 + */ +public interface CacheSerializer { + + /** + * 序列化方法 + * @param cacheData 缓存数据 + * @return String 序列化后的字符串 + * @throws IOException 序列化异常 + */ + String serialize(Object cacheData) throws IOException; + + /** + * 反序列化方法 + * @param cacheData 缓存数据 + * @param type Java 对象类型 + * @return java 对象实例 + * @throws IOException 序列化异常 + */ + Object deserialize(String cacheData, Type type) throws IOException; + + /** + * Type转JavaType + * @param type Java 对象类型 + * @return jackson 中的对象类型抽象 + */ + @SuppressWarnings("java:S3878") + static JavaType getJavaType(Type type) { + // 判断是否带有泛型 + if (type instanceof ParameterizedType) { + Type[] actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments(); + // 获取泛型类型 + Class rowClass = (Class) ((ParameterizedType) type).getRawType(); + + JavaType[] javaTypes = new JavaType[actualTypeArguments.length]; + + for (int i = 0; i < actualTypeArguments.length; i++) { + // 泛型也可能带有泛型,递归获取 + javaTypes[i] = getJavaType(actualTypeArguments[i]); + } + return TypeFactory.defaultInstance().constructParametricType(rowClass, javaTypes); + } + else { + // 简单类型直接用该类构建JavaType + Class cla = (Class) type; + return TypeFactory.defaultInstance().constructParametricType(cla, new JavaType[0]); + } + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/serialize/JacksonSerializer.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/serialize/JacksonSerializer.java new file mode 100644 index 0000000..3a8510d --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/serialize/JacksonSerializer.java @@ -0,0 +1,42 @@ +package com.baiye.serialize; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; + +import java.io.IOException; +import java.lang.reflect.Type; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/9 11:07 + */ +@RequiredArgsConstructor +public class JacksonSerializer implements CacheSerializer { + + private final ObjectMapper objectMapper; + + /** + * 反序列化方法 + * @param cacheData 缓存中的数据 + * @param type 反序列化目标类型 + * @return 反序列化后的对象 + * @throws IOException IO异常 + */ + @Override + public Object deserialize(String cacheData, Type type) throws IOException { + return objectMapper.readValue(cacheData, CacheSerializer.getJavaType(type)); + } + + /** + * 序列化方法 + * @param cacheData 待缓存的数据 + * @return 序列化后的数据 + * @throws IOException IO异常 + */ + @Override + public String serialize(Object cacheData) throws IOException { + return objectMapper.writeValueAsString(cacheData); + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/serialize/PrefixJdkRedisSerializer.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/serialize/PrefixJdkRedisSerializer.java new file mode 100644 index 0000000..62f7f10 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/serialize/PrefixJdkRedisSerializer.java @@ -0,0 +1,33 @@ +package com.baiye.serialize; + +import com.baiye.prefix.IRedisPrefixConverter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; + +/** + * @author Hccake + * @version 1.0 + * @date 2020/3/27 22:57 自定义Key序列化工具,添加全局key前缀 + */ +@Slf4j +public class PrefixJdkRedisSerializer extends JdkSerializationRedisSerializer { + + private final IRedisPrefixConverter redisPrefixConverter; + + public PrefixJdkRedisSerializer(IRedisPrefixConverter redisPrefixConverter) { + this.redisPrefixConverter = redisPrefixConverter; + } + + @Override + public Object deserialize(byte[] bytes) { + byte[] unwrap = redisPrefixConverter.unwrap(bytes); + return super.deserialize(unwrap); + } + + @Override + public byte[] serialize(Object object) { + byte[] originBytes = super.serialize(object); + return redisPrefixConverter.wrap(originBytes); + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/serialize/PrefixStringRedisSerializer.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/serialize/PrefixStringRedisSerializer.java new file mode 100644 index 0000000..83142ea --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/serialize/PrefixStringRedisSerializer.java @@ -0,0 +1,36 @@ +package com.baiye.serialize; + +import com.baiye.prefix.IRedisPrefixConverter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.nio.charset.StandardCharsets; + +/** + * @author Hccake + * @version 1.0 + * @date 2020/3/27 22:57 自定义Key序列化工具,添加全局key前缀 + */ +@Slf4j +public class PrefixStringRedisSerializer extends StringRedisSerializer { + + private final IRedisPrefixConverter iRedisPrefixConverter; + + public PrefixStringRedisSerializer(IRedisPrefixConverter iRedisPrefixConverter) { + super(StandardCharsets.UTF_8); + this.iRedisPrefixConverter = iRedisPrefixConverter; + } + + @Override + public String deserialize(byte[] bytes) { + byte[] unwrap = iRedisPrefixConverter.unwrap(bytes); + return super.deserialize(unwrap); + } + + @Override + public byte[] serialize(String key) { + byte[] originBytes = super.serialize(key); + return iRedisPrefixConverter.wrap(originBytes); + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/thread/AbstractRedisThread.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/thread/AbstractRedisThread.java new file mode 100644 index 0000000..8f7feab --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/thread/AbstractRedisThread.java @@ -0,0 +1,166 @@ +package com.baiye.thread; + +import cn.hutool.core.text.CharSequenceUtil; +import com.baiye.RedisHelper; +import com.baiye.util.JsonUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import javax.validation.constraints.NotNull; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @see java.util.concurrent.LinkedBlockingDeque + * @author lingting 2021/3/2 21:09 + */ +@Slf4j +public abstract class AbstractRedisThread extends AbstractQueueThread { + + @Autowired + protected RedisHelper redisHelper; + + /** + * 是否正在运行 + */ + protected boolean run = true; + + /** + * 锁 + */ + protected final ReentrantLock lock = new ReentrantLock(); + + /** + * 激活与休眠线程 + */ + protected final Condition condition = lock.newCondition(); + + /** + * 获取数据存储的key + * @return java.lang.String + */ + public abstract String getKey(); + + /** + * 对象 转换成 string. 把String 存入redis + * @param e 对象 + * @return java.lang.String + */ + protected String convertToString(@NotNull E e) { + return JsonUtils.toJson(e); + } + + /** + * 获取目标对象的type , 即 E 的实际类型. 如果获取失败, 请重写此方法 + * @return java.lang.reflect.Type + */ + protected Type getObjType() { + return ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + } + + /** + * string 转换成 对象 + * @param str string + * @return java.lang.String + */ + @Nullable + protected E convertToObj(String str) { + if (CharSequenceUtil.isBlank(str)) { + return null; + } + return JsonUtils.toObj(str, getObjType()); + } + + @Override + public void put(E e) { + // 不插入空值 + if (e != null) { + try { + lock.lockInterruptibly(); + try { + // 线程被中断后无法执行Redis命令 + RedisHelper.rPush(getKey(), convertToString(e)); + // 激活线程 + condition.signal(); + } + finally { + lock.unlock(); + } + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + catch (Exception ex) { + log.error("{} put error, param: {}", this.getClass().toString(), e, ex); + } + } + } + + /** + * 从redis中获取数据 + *

      + * 忽略sonar 警告. 子类有可能需要在get时做其他操作. + * @return java.lang.String + */ + @SuppressWarnings("java:S2177") + protected String get() { + return RedisHelper.lPop(getKey()); + } + + @Override + @Nullable + public E poll(long time) throws InterruptedException { + if (!isRun()) { + // 停止运行时返回null, 让数据待在redis里面 + return null; + } + // 上锁 + lock.lockInterruptibly(); + try { + // 设置等待时长 + long nanos = TimeUnit.MILLISECONDS.toNanos(time); + String pop; + do { + // 获取数据 + pop = get(); + if (StringUtils.hasText(pop)) { + break; + } + + // 休眠. 返回剩余的休眠时间 + nanos = condition.awaitNanos(nanos); + } + while (isRun() && nanos > 0); + + return convertToObj(pop); + } + finally { + lock.unlock(); + } + + } + + @Override + protected void shutdown(List list) { + // 修改运行标志 + run = false; + for (E e : list) { + // 所有数据插入redis + put(e); + log.error("{}", e); + } + } + + @Override + public boolean isRun() { + // 运行中 且 未被中断 + return run && !isInterrupted(); + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/upms/config/CacheProperties.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/upms/config/CacheProperties.java new file mode 100644 index 0000000..fac57d1 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/upms/config/CacheProperties.java @@ -0,0 +1,55 @@ +package com.baiye.upms.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +/** + * @author Hccake 2020/3/20 16:56 + */ +@Data +@ConfigurationProperties(prefix = CacheProperties.PREFIX) +public class CacheProperties { + + public static final String PREFIX = "ballcat.redis"; + + /** + * 通用的key前缀 + */ + private String keyPrefix = ""; + + /** + * redis锁 后缀 + */ + private String lockKeySuffix = "locked"; + + /** + * 默认分隔符 + */ + private String delimiter = ":"; + + /** + * 空值标识 + */ + private String nullValue = "N_V"; + + /** + * 默认缓存数据的超时时间(s) + */ + private long expireTime = 86400L; + + /** + * 默认锁的超时时间(s) + */ + private long defaultLockTimeout = 10L; + + @NestedConfigurationProperty + private KeyEventConfig keyExpiredEvent = new KeyEventConfig(); + + @NestedConfigurationProperty + private KeyEventConfig keyDeletedEvent = new KeyEventConfig(); + + @NestedConfigurationProperty + private KeyEventConfig keySetEvent = new KeyEventConfig(); + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/upms/config/CachePropertiesHolder.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/upms/config/CachePropertiesHolder.java new file mode 100644 index 0000000..15be731 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/upms/config/CachePropertiesHolder.java @@ -0,0 +1,46 @@ +package com.baiye.upms.config; + +/** + * 缓存配置持有者,方便静态获取配置信息 + * + * @author Hccake 2020/3/20 16:56 + */ +public final class CachePropertiesHolder { + + private static final CachePropertiesHolder INSTANCE = new CachePropertiesHolder(); + + private CacheProperties cacheProperties; + + public void setCacheProperties(CacheProperties cacheProperties) { + INSTANCE.cacheProperties = cacheProperties; + } + + private static CacheProperties cacheProperties() { + return INSTANCE.cacheProperties; + } + + public static String keyPrefix() { + return cacheProperties().getKeyPrefix(); + } + + public static String lockKeySuffix() { + return cacheProperties().getLockKeySuffix(); + } + + public static String delimiter() { + return cacheProperties().getDelimiter(); + } + + public static String nullValue() { + return cacheProperties().getNullValue(); + } + + public static long expireTime() { + return cacheProperties().getExpireTime(); + } + + public static long defaultLockTimeout() { + return cacheProperties().getDefaultLockTimeout(); + } + +} diff --git a/marketing-scrm-common/common-redis/src/main/java/com/baiye/upms/config/KeyEventConfig.java b/marketing-scrm-common/common-redis/src/main/java/com/baiye/upms/config/KeyEventConfig.java new file mode 100644 index 0000000..c2edaa6 --- /dev/null +++ b/marketing-scrm-common/common-redis/src/main/java/com/baiye/upms/config/KeyEventConfig.java @@ -0,0 +1,18 @@ +package com.baiye.upms.config; + +import lombok.Getter; +import lombok.Setter; + +/** + * redis key event common config + * + * @author lishangbu + * @date 2023/1/12 + */ +@Getter +@Setter +public class KeyEventConfig { + + private Boolean enabled = false; + +} diff --git a/marketing-scrm-common/common-util/pom.xml b/marketing-scrm-common/common-util/pom.xml new file mode 100644 index 0000000..91ee2b6 --- /dev/null +++ b/marketing-scrm-common/common-util/pom.xml @@ -0,0 +1,52 @@ + + + + marketing-scrm-common + com.baiye + ${revision} + + 4.0.0 + common-util + + + + + cn.hutool + hutool-all + + + com.alibaba + fastjson + true + + + + com.fasterxml.jackson.core + jackson-databind + + + com.google.code.gson + gson + true + + + jakarta.servlet + jakarta.servlet-api + compile + + + org.jsoup + jsoup + + + org.springframework + spring-context + true + + + org.slf4j + slf4j-api + + + diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/charset/GSMCharset.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/charset/GSMCharset.java new file mode 100644 index 0000000..f9722b7 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/charset/GSMCharset.java @@ -0,0 +1,126 @@ +/** + * Copyright (C) 2011 Twitter, Inc. + *

      + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + *

      + * http://www.apache.org/licenses/LICENSE-2.0 + *

      + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.baiye.charset; + +/** + * This class encodes and decodes Java Strings to and from the SMS default alphabet. It + * also supports the default extension table. The default alphabet and it's extension + * table is defined in GSM 03.38. + * + * @author joelauer + * @author hccake + */ +public final class GSMCharset { + + private GSMCharset() { + } + + /** + * The extension character uses this character as a precharacter + */ + public static final int EXTENDED_ESCAPE = 0x1b; + + /** + * Page break (extended table). + */ + public static final int PAGE_BREAK = 0x0a; + + public static final char[] CHAR_TABLE = { '@', '\u00a3', '$', '\u00a5', '\u00e8', '\u00e9', '\u00f9', '\u00ec', + '\u00f2', '\u00c7', '\n', '\u00d8', '\u00f8', '\r', '\u00c5', '\u00e5', '\u0394', '_', '\u03a6', '\u0393', + '\u039b', '\u03a9', '\u03a0', '\u03a8', '\u03a3', '\u0398', '\u039e', ' ', '\u00c6', '\u00e6', '\u00df', + '\u00c9', ' ', '!', '"', '#', '\u00a4', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', + '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '\u00a1', 'A', 'B', 'C', 'D', 'E', + 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + '\u00c4', '\u00d6', '\u00d1', '\u00dc', '\u00a7', '\u00bf', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', + 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '\u00e4', '\u00f6', + '\u00f1', '\u00fc', '\u00e0', }; + + /** + * Extended character table. Characters in this table are accessed by the 'escape' + * character in the base table. It is important that none of the 'inactive' characters + * ever be matchable with a valid base-table character as this breaks the encoding + * loop. + * + * @see #EXTENDED_ESCAPE + */ + public static final char[] EXT_CHAR_TABLE = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '^', 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '{', '}', 0, 0, 0, 0, 0, '\\', 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, '[', '~', ']', 0, '|', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '\u20ac', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, }; + + /** + * Verifies that this charset can represent every character in the Java String (char + * sequence). + * @param str0 The String to verfiy + * @return True if the charset can represent every character in the Java String, + * otherwise false. + */ + public static boolean canRepresent(CharSequence str0) { + return need7bitsNum(str0) >= 0; + } + + /** + * Gets the number of 7bits of the string required under GSM-7 encoding + * @return Returns -1 if the string cannot be encoded in GSM-7, otherwise returns the + * number of 7bits of the string required under GSM-7 encoding + */ + public static int need7bitsNum(CharSequence str0) { + if (str0 == null) { + return 0; + } + + int need7bitsNum = 0; + int len = str0.length(); + for (int i = 0; i < len; i++) { + // get the char in this string + char c = str0.charAt(i); + // a very easy check a-z, A-Z, and 0-9 are always valid + if (c >= 'A' && c <= 'z') { + need7bitsNum++; + continue; + } + if (c >= '0' && c <= '9') { + need7bitsNum++; + continue; + } + // gsm-7 maximum codepoint supported is € + if (c > '€') { + return -1; + } + + // search both charmaps (if char is in either, we're good!) + boolean found = false; + for (int j = 0; j < CHAR_TABLE.length; j++) { + if (c == CHAR_TABLE[j]) { + need7bitsNum++; + found = true; + break; + } + else if (c == EXT_CHAR_TABLE[j]) { + need7bitsNum = need7bitsNum + 2; + found = true; + break; + } + } + // if we searched both charmaps and didn't find it, then its bad + if (!found) { + return -1; + } + + } + return need7bitsNum; + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/exception/CommandTimeoutException.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/exception/CommandTimeoutException.java new file mode 100644 index 0000000..3aaf9c9 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/exception/CommandTimeoutException.java @@ -0,0 +1,8 @@ +package com.baiye.exception; + +/** + * @author lingting 2022/7/15 15:33 + */ +public class CommandTimeoutException extends Exception { + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/queue/WaitQueue.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/queue/WaitQueue.java new file mode 100644 index 0000000..8d2f518 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/queue/WaitQueue.java @@ -0,0 +1,53 @@ +package com.baiye.queue; + +import java.util.Collection; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author lingting 2023/1/29 10:52 + */ +public class WaitQueue { + + private final LinkedBlockingQueue queue; + + public WaitQueue() { + this(new LinkedBlockingQueue<>()); + } + + public WaitQueue(LinkedBlockingQueue queue) { + this.queue = queue; + } + + public V get() { + return queue.poll(); + } + + public V poll() throws InterruptedException { + return poll(10, TimeUnit.HOURS); + } + + public V poll(long timeout, TimeUnit unit) throws InterruptedException { + V v; + do { + v = queue.poll(timeout, unit); + } + while (v == null); + return v; + } + + public void clear() { + queue.clear(); + } + + public void add(V seat) { + queue.add(seat); + } + + public void addAll(Collection accounts) { + for (V account : accounts) { + add(account); + } + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/system/Command.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/system/Command.java new file mode 100644 index 0000000..6fdd543 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/system/Command.java @@ -0,0 +1,161 @@ +package com.baiye.system; + +import com.baiye.util.FileUtils; +import com.baiye.exception.CommandTimeoutException; +import com.baiye.util.SystemUtils; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.time.LocalDateTime; +import java.util.StringTokenizer; +import java.util.concurrent.TimeUnit; + +/** + * @author lingting 2022/6/25 11:55 + */ +public class Command { + + public static final String NEXT_LINE = SystemUtils.lineSeparator(); + + public static final String EXIT_COMMAND = "exit"; + + private final Process process; + + private final OutputStream stdIn; + + /** + * 标准输出 + */ + private final File stdOut; + + private final File stdErr; + + private final String nextLine; + + private final String exit; + + private final Charset charset; + + private final LocalDateTime startTime; + + private Command(String init, String nextLine, String exit, Charset charset) throws IOException { + if (!StringUtils.hasText(init)) { + throw new IllegalArgumentException("Empty init"); + } + StringTokenizer st = new StringTokenizer(init); + String[] cmdArray = new String[st.countTokens()]; + for (int i = 0; st.hasMoreTokens(); i++) { + cmdArray[i] = st.nextToken(); + } + + this.stdOut = FileUtils.createTemp(); + this.stdErr = FileUtils.createTemp(); + + // 重定向标准输出和标准错误到文件, 避免写入到缓冲区然后占满导致 waitFor 死锁 + ProcessBuilder builder = new ProcessBuilder(cmdArray).redirectError(stdErr).redirectOutput(stdOut); + this.process = builder.start(); + this.stdIn = process.getOutputStream(); + this.nextLine = nextLine; + this.exit = exit; + this.charset = charset; + this.startTime = LocalDateTime.now(); + } + + /** + * 获取命令操作实例. 此实例默认使用系统字符集, 如果发现部分带非英文字符和特殊符号命令执行异常, 建议使用 + * {@link Command#of(String, Charset)} 自定义对应的字符集 + * @param init 初始命令 + */ + public static Command of(String init) throws IOException { + return of(init, SystemUtils.charset()); + } + + /** + * 推荐使用此实例 + */ + public static Command of(String init, Charset charset) throws IOException { + return of(init, NEXT_LINE, EXIT_COMMAND, charset); + } + + public static Command of(String init, String nextLine, String exit, Charset charset) throws IOException { + return new Command(init, nextLine, exit, charset); + } + + public Command write(String str) throws IOException { + stdIn.write(str.getBytes(charset)); + stdIn.flush(); + return this; + } + + /** + * 换到下一行 + */ + public Command line() throws IOException { + return write(nextLine); + } + + /** + * 写入通道退出指令 + */ + public Command exit() throws IOException { + write(exit); + return line(); + } + + /** + * 写入并执行一行指令 + * @param str 单行指令 + */ + public Command exec(String str) throws IOException { + write(str); + return line(); + } + + /** + * 获取执行结果, 并退出 + *

      + * 注意: 如果套娃了多个通道, 则需要手动退出套娃的通道 + *

      + *

      + * 例如: eg: exec("ssh ssh.lingting.live").exec("ssh ssh.lingting.live").exec("ssh + * ssh.lingting.live") + *

      + *

      + * 需要: eg: exit().exit().exit() + *

      + */ + public CommandResult result() throws InterruptedException { + process.waitFor(); + return CommandResult.of(stdOut, stdErr, startTime, LocalDateTime.now(), charset); + } + + /** + * 等待命令执行完成 + *

      如果 process 是通过 {@link Runtime#exec}方法构建的, 那么{@link Process#waitFor}方法可能会导致线程卡死, + * 具体原因如下

      + *

      + * 终端缓冲区大小有限, 在缓冲区被写满之后, 会子线程会挂起,等待缓冲区内容被读, 然后才继续写. 如果此时主线程也在waitFor()等待子线程结束, 就卡死了 + *

      + *

      + * 即便是先读取返回结果在调用此方法也可能会导致卡死. 比如: 先读取标准输出流, 还没读完, 缓冲区被错误输出流写满了. + *

      + * @param millis 等待时间, 单位: 毫秒 + * @return live.lingting.tools.system.CommandResult + */ + public CommandResult result(long millis) throws InterruptedException, CommandTimeoutException { + if (process.waitFor(millis, TimeUnit.MILLISECONDS)) { + return result(); + } + // 超时. 强行杀死子线程 + process.destroyForcibly(); + throw new CommandTimeoutException(); + } + + public void close() { + process.destroy(); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/system/CommandResult.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/system/CommandResult.java new file mode 100644 index 0000000..401fb0f --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/system/CommandResult.java @@ -0,0 +1,96 @@ +package com.baiye.system; + +import com.baiye.util.StreamUtils; +import lombok.Getter; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.time.LocalDateTime; + +/** + * @author lingting 2022/6/25 12:01 + */ +public class CommandResult { + + protected File stdOut; + + protected File stdErr; + + private Charset charset; + + @Getter + protected LocalDateTime startTime; + + @Getter + protected LocalDateTime endTime; + + protected String strOutput = null; + + protected String strError = null; + + public static CommandResult of(File stdOut, File stdErr, LocalDateTime startTime, LocalDateTime endTime, + Charset charset) { + CommandResult result = new CommandResult(); + result.stdOut = stdOut; + result.stdErr = stdErr; + result.charset = charset; + result.startTime = startTime; + result.endTime = endTime; + return result; + } + + public File stdOut() { + return stdOut; + } + + public File stdErr() { + return stdErr; + } + + public String stdOutStr() throws IOException { + if (!StringUtils.hasText(strOutput)) { + try (FileInputStream output = new FileInputStream(stdOut)) { + strOutput = StreamUtils.toString(output, StreamUtils.DEFAULT_SIZE, charset); + } + } + return strOutput; + } + + public String stdErrStr() throws IOException { + if (!StringUtils.hasText(strError)) { + try (FileInputStream error = new FileInputStream(stdErr)) { + strError = StreamUtils.toString(error, StreamUtils.DEFAULT_SIZE, charset); + } + } + return strError; + } + + public InputStream stdOutStream() throws IOException { + return Files.newInputStream(stdOut.toPath()); + } + + public InputStream stdErrStream() throws IOException { + return Files.newInputStream(stdErr.toPath()); + } + + public void clean() { + try { + Files.delete(stdOut.toPath()); + } + catch (Exception e) { + // + } + try { + Files.delete(stdErr.toPath()); + } + catch (Exception e) { + // + } + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/system/StopWatch.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/system/StopWatch.java new file mode 100644 index 0000000..73ebd47 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/system/StopWatch.java @@ -0,0 +1,72 @@ +package com.baiye.system; + +import java.util.concurrent.TimeUnit; + +/** + * 参考 org.springframework.util.StopWatch + *

      + * 线程不安全 + *

      + * + * @author lingting 2023-02-15 16:47 + */ +public class StopWatch { + + private Long startTimeNanos; + + /** + * 耗时, 单位: 纳秒 + */ + private Long durationNanos; + + /** + * 开始计时, 如果已开始, 则从延续之前的计时 + */ + public void start() { + if (startTimeNanos != null) { + return; + } + durationNanos = null; + startTimeNanos = System.nanoTime(); + } + + /** + * 是否正在运行 + */ + public boolean isRunning() { + return startTimeNanos != null; + } + + public void stop() { + if (startTimeNanos != null) { + durationNanos = System.nanoTime() - startTimeNanos; + } + startTimeNanos = null; + } + + public void restart() { + stop(); + start(); + } + + /** + * 获取执行时长 + *

      + * 如果已开始, 则是开始时间到当前时长 + *

      + *

      + * 如果已结束, 则是上一次统计时长 + *

      + */ + public long timeNanos() { + if (durationNanos == null) { + return startTimeNanos == null ? 0 : System.nanoTime() - startTimeNanos; + } + return durationNanos; + } + + public long timeMillis() { + return TimeUnit.NANOSECONDS.toMillis(timeNanos()); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/thread/ThreadPool.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/thread/ThreadPool.java new file mode 100644 index 0000000..9200b4a --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/thread/ThreadPool.java @@ -0,0 +1,92 @@ +package com.baiye.thread; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * @author lingting 2022/11/17 20:15 + */ +@Slf4j +@SuppressWarnings("java:S6548") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class ThreadPool { + + protected static final ThreadPool INSTANCE; + + protected static final Integer QUEUE_MAX = 100; + + @Getter + protected final ThreadPoolExecutor pool; + + static { + INSTANCE = new ThreadPool(new ThreadPoolExecutor( + // 核心线程数大小. 不论是否空闲都存在的线程 + 300, + // 最大线程数 - 10万个 + 100000, + // 存活时间. 非核心线程数如果空闲指定时间. 就回收 + // 存活时间不宜过长. 避免任务量遇到尖峰情况时. 大量空闲线程占用资源 + 10, + // 存活时间的单位 + TimeUnit.SECONDS, + // 等待任务存放队列 - 队列最大值 + // 这样配置. 当积压任务数量为 队列最大值 时. 会创建新线程来执行任务. 直到线程总数达到 最大线程数 + new LinkedBlockingQueue<>(QUEUE_MAX), + // 新线程创建工厂 - LinkedBlockingQueue 不支持线程优先级. 所以直接新增线程就可以了 + runnable -> new Thread(null, runnable), + // 拒绝策略 - 在主线程继续执行. + new ThreadPoolExecutor.CallerRunsPolicy())); + } + + public static ThreadPool instance() { + return INSTANCE; + } + + /** + * 线程池是否活跃 + */ + public boolean isRunning() { + return getCount() > 0; + } + + /** + * 线程当前活跃数量 + */ + public long getCount() { + return getPool().getTaskCount(); + } + + public void execute(Runnable runnable) { + execute(null, runnable); + } + + public void execute(String name, Runnable runnable) { + getPool().execute(() -> { + Thread thread = Thread.currentThread(); + String oldName = thread.getName(); + if (StringUtils.hasText(name)) { + thread.setName(name); + } + try { + runnable.run(); + } + catch (Throwable throwable) { + log.error("线程发生异常!", throwable); + if (!(throwable instanceof Exception)) { + throw throwable; + } + } + finally { + thread.setName(oldName); + } + }); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/AESUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/AESUtils.java new file mode 100644 index 0000000..84da256 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/AESUtils.java @@ -0,0 +1,234 @@ +package com.baiye.util; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.crypto.symmetric.SymmetricAlgorithm; +import cn.hutool.crypto.symmetric.SymmetricCrypto; +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Base64; + +/** + * AES加解密 + */ +@Slf4j +public class AESUtils { + + /** + * 密钥长度: 128, 192 or 256 + */ + private static final int KEY_SIZE = 128; + + /** + * 加密/解密算法名称 + */ + private static final String ALGORITHM = "AES"; + + /** + * Key + */ + private static final String HEX = "0123456789ABCDEF"; + + /** + * 随机数生成器(RNG)算法名称 + */ + private static final String RNG_ALGORITHM = "SHA1PRNG"; + + /** + * 生成密钥对象 + */ + private static SecretKey generateKey(byte[] key) throws Exception { + // 创建安全随机数生成器 + SecureRandom random = SecureRandom.getInstance(RNG_ALGORITHM); + // 设置 密钥key的字节数组 作为安全随机数生成器的种子 + random.setSeed(key); + + // 创建 AES算法生成器 + KeyGenerator gen = KeyGenerator.getInstance(ALGORITHM); + // 初始化算法生成器 + gen.init(KEY_SIZE, random); + + // 生成 AES密钥对象, 也可以直接创建密钥对象: return new SecretKeySpec(key, ALGORITHM); + return gen.generateKey(); + } + + /** + * 数据加密: 明文 -> 密文 + */ + public static String encrypt(String plainBytes, String key) { + try { + // 生成密钥对象 + SecretKey secKey = generateKey(key.getBytes(StandardCharsets.UTF_8)); + + // 获取 AES 密码器 + Cipher cipher = Cipher.getInstance(ALGORITHM); + // 初始化密码器(加密模型) + cipher.init(Cipher.ENCRYPT_MODE, secKey); + + // 数据反转加密 + String s = new StringBuilder(plainBytes).reverse().toString(); + // 加密数据, 返回密文 + byte[] cipherBytes = cipher.doFinal(s.getBytes(StandardCharsets.UTF_8)); + + // base64加密 + String str = Base64.getEncoder().encodeToString(cipherBytes); + return str; + } + catch (Exception e) { + log.error("加密失败" + e.fillInStackTrace()); + return null; + } + + } + + /** + * 数据解密: 密文 -> 明文 + */ + public static String decrypt(String cipherBytes, String key) { + try { + // 生成密钥对象 + SecretKey secKey = generateKey(key.getBytes(StandardCharsets.UTF_8)); + + // 获取 AES 密码器 + Cipher cipher = Cipher.getInstance(ALGORITHM); + // 初始化密码器(解密模型) + cipher.init(Cipher.DECRYPT_MODE, secKey); + + // base64解密 + byte[] bytes = Base64.getDecoder().decode(cipherBytes); + + // 解密数据, 返回明文 + byte[] plainBytes = cipher.doFinal(bytes); + + // 反转解密的结果返回 + return new StringBuilder(new String(plainBytes, StandardCharsets.UTF_8)).reverse().toString(); + } + catch (Exception e) { + log.error("解密失败" + e.fillInStackTrace()); + return null; + } + } + + /** + * 加密 + * @param str 加密之前的字符串 + * @return + */ + public static String encryptHex(String str, String key) { + + SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, key.getBytes(StandardCharsets.UTF_8)); + return aes.encryptHex(str); + } + + /** + * 解密 + * @param str 加密后的字符串 + * @return + */ + public static String decryptStr(String str, String key) { + SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, key.getBytes(StandardCharsets.UTF_8)); + return aes.decryptStr(str); + } + + /** + * AES加密 + */ + public static String dbEncrypt(String seed, String cleartext) { + byte[] rawKey = getRawKey(seed.getBytes()); + byte[] result = encrypt(rawKey, cleartext.getBytes()); + return toHex(result); + } + + /** + * AES解密 + */ + public static String dbDecrypt(String seed, String encrypted) { + try { + byte[] rawKey = getRawKey(seed.getBytes()); + byte[] enc = toByte(encrypted); + byte[] result = decrypt(rawKey, enc); + return new String(result); + } + catch (Exception e) { + return CharSequenceUtil.EMPTY; + } + } + + private static byte[] getRawKey(byte[] seed) { + try { + KeyGenerator kgen = KeyGenerator.getInstance(ALGORITHM); + SecureRandom sr = SecureRandom.getInstance(RNG_ALGORITHM); + sr.setSeed(seed); + // 192 and 256 bits may not be available + kgen.init(256, sr); + SecretKey skey = kgen.generateKey(); + return skey.getEncoded(); + } + catch (Exception e) { + return new byte[0]; + } + } + + private static byte[] encrypt(byte[] raw, byte[] clear) { + try { + SecretKeySpec skeySpec = new SecretKeySpec(raw, ALGORITHM); + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, skeySpec); + return cipher.doFinal(clear); + } + catch (Exception e) { + return new byte[0]; + } + } + + private static byte[] decrypt(byte[] raw, byte[] encrypted) { + SecretKeySpec skeSpec = new SecretKeySpec(raw, ALGORITHM); + try { + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, skeSpec); + return cipher.doFinal(encrypted); + } + catch (Exception e) { + return new byte[0]; + } + } + + public static String toHex(String txt) { + return toHex(txt.getBytes()); + } + + public static String fromHex(String hex) { + return new String(toByte(hex)); + } + + public static byte[] toByte(String hexString) { + int len = hexString.length() >> 1; + + byte[] result = new byte[len]; + for (int i = 0; i < len; i++) { + result[i] = Integer.valueOf(hexString.substring(2 * i, 2 * i + 2), 16).byteValue(); + } + return result; + } + + public static String toHex(byte[] buf) { + if (buf.length == 0) { + return CharSequenceUtil.EMPTY; + } + StringBuilder result = new StringBuilder(buf.length >> 1); + for (byte b : buf) { + appendHex(result, b); + } + return result.toString(); + } + + private static void appendHex(StringBuilder sb, byte b) { + sb.append(HEX.charAt((b >> 4) & 0x0f)).append(HEX.charAt(b & 0x0f)); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/ArrayUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/ArrayUtils.java new file mode 100644 index 0000000..90ae6ce --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/ArrayUtils.java @@ -0,0 +1,35 @@ +package com.baiye.util; + +import lombok.experimental.UtilityClass; + +import java.util.Objects; + +/** + * @author lingting + */ +@UtilityClass +public class ArrayUtils { + + public static final int NOT_FOUNT = -1; + + public static boolean isEmpty(T[] array) { + return array == null || array.length == 0; + } + + public static int indexOf(T[] array, T val) { + if (!isEmpty(array)) { + for (int i = 0; i < array.length; i++) { + T t = array[i]; + if (Objects.equals(t, val)) { + return i; + } + } + } + return NOT_FOUNT; + } + + public static boolean contains(T[] array, T val) { + return indexOf(array, val) > NOT_FOUNT; + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/BooleanUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/BooleanUtils.java new file mode 100644 index 0000000..f782650 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/BooleanUtils.java @@ -0,0 +1,41 @@ +package com.baiye.util; + +import lombok.experimental.UtilityClass; + +/** + * @author lingting 2023-05-06 14:16 + */ +@UtilityClass +public class BooleanUtils { + + private static final String[] STR_TRUE = { "1", "true", "yes", "ok", "y" }; + + private static final String[] STR_FALSE = { "0", "false", "no", "n" }; + + public static boolean isTrue(Object obj) { + if (obj instanceof String) { + return ArrayUtils.contains(STR_TRUE, obj); + } + if (obj instanceof Number) { + return ((Number) obj).doubleValue() > 0; + } + if (obj instanceof Boolean) { + return Boolean.TRUE.equals(obj); + } + return false; + } + + public static boolean isFalse(Object obj) { + if (obj instanceof String) { + return ArrayUtils.contains(STR_FALSE, obj); + } + if (obj instanceof Number) { + return ((Number) obj).doubleValue() <= 0; + } + if (obj instanceof Boolean) { + return Boolean.FALSE.equals(obj); + } + return false; + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/ClassUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/ClassUtils.java new file mode 100644 index 0000000..eecd755 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/ClassUtils.java @@ -0,0 +1,182 @@ +package com.baiye.util; + +import cn.hutool.core.util.ClassUtil; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * @author lingting 2021/2/25 21:17 + */ +public class ClassUtils extends ClassUtil { + + private static final Map CACHE = new ConcurrentHashMap<>(8); + + private static final Map, Field[]> CACHE_FIELDS = new ConcurrentHashMap<>(16); + + /** + * 确定class是否可以被加载 + * @param className 完整类名 + * @param classLoader 类加载 + */ + public static boolean isPresent(String className, ClassLoader classLoader) { + if (CACHE.containsKey(className)) { + return CACHE.get(className); + } + try { + Class.forName(className, true, classLoader); + CACHE.put(className, true); + return true; + } + catch (Exception ex) { + CACHE.put(className, false); + return false; + } + } + + public static Set> scan(String basePack, Class cls) throws IOException { + return scan(basePack, tClass -> cls == null || cls.isAssignableFrom(tClass), (s, e) -> { + }); + } + + /** + * 扫描指定包下, 所有继承指定类的class + * @param basePack 指定包 eg: live.lingting.wirelesstools.item + * @param filter 过滤指定类 + * @param error 获取类时发生异常处理 + * @return java.util.Set> + */ + public static Set> scan(String basePack, Function, Boolean> filter, + BiConsumer error) throws IOException { + List classNames = new ArrayList<>(); + String clsPath = basePack.replace(".", "/"); + URL url = Thread.currentThread().getContextClassLoader().getResource(clsPath); + if (url == null) { + return new HashSet<>(); + } + if ("file".equals(url.getProtocol())) { + String path = url.getFile(); + for (String file : FileUtils.scanFile(path, true)) { + if (file.endsWith(".class")) { + String className = basePack + "." + + file.substring(path.length(), file.length() - 6).replace(File.separator, "."); + + classNames.add(className); + } + } + } + else { + URLConnection connection = url.openConnection(); + if (connection instanceof JarURLConnection) { + JarURLConnection jarURLConnection = (JarURLConnection) connection; + JarFile jarFile = jarURLConnection.getJarFile(); + + Enumeration entries = jarFile.entries(); + + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + + if (entryName.endsWith(".class") && entryName.startsWith(clsPath)) { + classNames.add(entryName.substring(0, entryName.length() - 6).replace("/", ".")); + } + } + + } + } + + Set> classes = new HashSet<>(); + for (String className : classNames) { + try { + Class aClass = (Class) Class.forName(className); + + if (filter.apply(aClass)) { + classes.add(aClass); + } + } + catch (Exception e) { + error.accept(className, e); + } + } + return classes; + } + + /** + * 把指定对象的所有字段和对应的值组成Map + * @param o 需要转化的对象 + * @return java.util.Map + */ + public static Map toMap(Object o) { + return toMap(o, field -> true, Field::getName, (field, v) -> v); + } + + /** + * 把指定对象的所有字段和对应的值组成Map + * @param o 需要转化的对象 + * @param filter 过滤不存入Map的字段, 返回false表示不存入Map + * @param toKey 设置存入Map的key + * @param toVal 自定义指定字段值的存入Map的数据 + * @return java.util.Map + */ + public static Map toMap(Object o, Function filter, Function toKey, + BiFunction toVal) { + if (o == null) { + return Collections.emptyMap(); + } + HashMap map = new HashMap<>(); + for (Field field : fields(o.getClass())) { + if (filter.apply(field)) { + Object val = null; + + try { + val = field.get(o); + } + catch (IllegalAccessException e) { + // + } + + map.put(toKey.apply(field), toVal.apply(field, val)); + } + } + return map; + } + + /** + * 获取指定类及其父类的所有字段, 并且设置成可访问. + * @param cls class + * @return java.lang.reflect.Field[] + */ + @SuppressWarnings("java:S3011") + public static Field[] fields(Class cls) { + return CACHE_FIELDS.computeIfAbsent(cls, k -> { + + List fields = new ArrayList<>(); + while (k != null && !k.isAssignableFrom(Object.class)) { + for (Field field : k.getDeclaredFields()) { + field.setAccessible(true); + fields.add(field); + } + k = k.getSuperclass(); + } + return fields.toArray(new Field[0]); + }); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/EnvironmentUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/EnvironmentUtils.java new file mode 100644 index 0000000..1924b52 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/EnvironmentUtils.java @@ -0,0 +1,159 @@ +package com.baiye.util; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.UtilityClass; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.env.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * @author lingting 2022/10/15 11:33 + */ +@UtilityClass +public class EnvironmentUtils { + + public static final String ENVIRONMENT_NAME_REPLACE = "replaceEnvironment"; + + @Getter + @Setter + private static ConfigurableEnvironment environment; + + public static boolean containsProperty(String key) { + return environment.containsProperty(key); + } + + public static String getProperty(String key) { + return environment.getProperty(key); + } + + public static String getProperty(String key, String defaultValue) { + return environment.getProperty(key, defaultValue); + } + + public static T getProperty(String key, Class targetType) { + return environment.getProperty(key, targetType); + } + + public static T getProperty(String key, Class targetType, T defaultValue) { + return environment.getProperty(key, targetType, defaultValue); + } + + public static String getRequiredProperty(String key) throws IllegalStateException { + return environment.getRequiredProperty(key); + } + + public static T getRequiredProperty(String key, Class targetType) throws IllegalStateException { + return environment.getRequiredProperty(key, targetType); + } + + public static String resolvePlaceholders(String text) { + return environment.resolvePlaceholders(text); + } + + public static String resolveRequiredPlaceholders(String text) throws IllegalArgumentException { + return environment.resolveRequiredPlaceholders(text); + } + + public static void setActiveProfiles(String... profiles) { + environment.setActiveProfiles(profiles); + } + + public static void addActiveProfile(String profile) { + environment.addActiveProfile(profile); + } + + public static void setDefaultProfiles(String... profiles) { + environment.setDefaultProfiles(profiles); + } + + public static MutablePropertySources getPropertySources() { + return environment.getPropertySources(); + } + + public static Map getSystemProperties() { + return environment.getSystemProperties(); + } + + public static Map getSystemEnvironment() { + return environment.getSystemEnvironment(); + } + + public static void merge(ConfigurableEnvironment parent) { + environment.merge(parent); + } + + public static ConfigurableConversionService getConversionService() { + return environment.getConversionService(); + } + + public static void setConversionService(ConfigurableConversionService conversionService) { + environment.setConversionService(conversionService); + } + + public static void setPlaceholderPrefix(String placeholderPrefix) { + environment.setPlaceholderPrefix(placeholderPrefix); + } + + public static void setPlaceholderSuffix(String placeholderSuffix) { + environment.setPlaceholderSuffix(placeholderSuffix); + } + + public static void setValueSeparator(String valueSeparator) { + environment.setValueSeparator(valueSeparator); + } + + public static void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders) { + environment.setIgnoreUnresolvableNestedPlaceholders(ignoreUnresolvableNestedPlaceholders); + } + + public static void setRequiredProperties(String... requiredProperties) { + environment.setRequiredProperties(requiredProperties); + } + + public static void validateRequiredProperties() throws MissingRequiredPropertiesException { + environment.validateRequiredProperties(); + } + + public static String[] getActiveProfiles() { + return environment.getActiveProfiles(); + } + + public static String[] getDefaultProfiles() { + return environment.getDefaultProfiles(); + } + + public static boolean acceptsProfiles(Profiles profiles) { + return environment.acceptsProfiles(profiles); + } + + public static Map getReplaceMapPropertySource() { + MutablePropertySources propertySources = getPropertySources(); + MapPropertySource target = null; + + if (propertySources.contains(ENVIRONMENT_NAME_REPLACE)) { + PropertySource source = propertySources.get(ENVIRONMENT_NAME_REPLACE); + if (source instanceof MapPropertySource) { + target = (MapPropertySource) source; + } + } + else { + target = new MapPropertySource(ENVIRONMENT_NAME_REPLACE, new HashMap<>(16)); + propertySources.addFirst(target); + } + + return Optional.ofNullable(target).map(MapPropertySource::getSource).orElse(null); + } + + /** + * 判断当前是否为指定环境 + * @param profile 指定环境 + */ + public static boolean isProfile(String profile) { + return ArrayUtils.contains(getActiveProfiles(), profile); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/FileUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/FileUtils.java new file mode 100644 index 0000000..cab9cf0 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/FileUtils.java @@ -0,0 +1,216 @@ +package com.baiye.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.CopyOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author lingting 2021/4/16 14:33 + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class FileUtils { + + /** + * 系统的临时文件夹 + */ + private static File tempDir = SystemUtils.tmpDirBallcat(); + + /** + * 更新临时文件路径 + */ + public static void updateTmpDir(String dirName) { + tempDir = new File(SystemUtils.tmpDir(), dirName); + } + + /** + * 获取临时文件, 不会创建文件 + */ + public static File getTemplateFile(String name) throws IOException { + if (!tempDir.exists()) { + tempDir.mkdirs(); + } + + File file; + + do { + file = new File(tempDir, System.currentTimeMillis() + "." + name); + } + while (!file.createNewFile()); + + return file; + } + + /** + * 根据网络路径获取文件输入流 + */ + public static InputStream getInputStreamByUrlPath(String path) throws IOException { + // 从文件链接里获取文件流 + URL url = new URL(path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + // 设置超时间为3秒 + conn.setConnectTimeout(5 * 1000); + // 得到输入流 + return conn.getInputStream(); + } + + /** + * 扫描指定路径下所有文件 + * @param path 指定路径 + * @param recursive 是否递归 + * @return java.util.List + */ + public static List scanFile(String path, boolean recursive) { + List list = new ArrayList<>(); + File file = new File(path); + if (!file.exists()) { + return list; + } + + if (file.isFile()) { + list.add(file.getAbsolutePath()); + } + // 文件夹 + else { + File[] files = file.listFiles(); + + if (files == null || files.length < 1) { + return list; + } + + for (File childFile : files) { + // 如果递归 + if (recursive && childFile.isDirectory()) { + list.addAll(scanFile(childFile.getAbsolutePath(), true)); + } + // 是文件 + else if (childFile.isFile()) { + list.add(childFile.getAbsolutePath()); + } + } + } + + return list; + } + + /** + * 创建指定文件夹, 已存在时不会重新创建 + * @param dir 文件夹. + * @throws IOException 创建失败时抛出 + */ + public static void createDir(File dir) throws IOException { + if (dir.exists()) { + return; + } + + if (!dir.mkdirs()) { + throw new IOException("文件夹创建失败! 文件夹路径: " + dir.getAbsolutePath()); + } + } + + /** + * 创建指定文件, 已存在时不会重新创建 + * @param file 文件. + * @throws IOException 创建失败时抛出 + */ + public static void createFile(File file) throws IOException { + if (file.exists()) { + return; + } + + if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) { + throw new IOException("父文件创建失败! 文件路径: " + file.getAbsolutePath()); + } + + if (!file.createNewFile()) { + throw new IOException("文件创建失败! 文件路径: " + file.getAbsolutePath()); + } + } + + /** + * 创建临时文件 + */ + public static File createTemp() throws IOException { + return createTemp("ballcat"); + } + + /** + * 创建临时文件 + * @param trait 文件特征 + * @return 临时文件对象 + */ + public static File createTemp(String trait) throws IOException { + return createTemp(trait, tempDir); + } + + /** + * 创建临时文件 + * @param trait 文件特征 + * @param dir 文件存放位置 + * @return 临时文件对象 + */ + public static File createTemp(String trait, File dir) throws IOException { + try { + createDir(dir); + } + catch (IOException e) { + throw new IOException("临时文件夹创建失败! 文件夹地址: " + dir.getAbsolutePath(), e); + } + + return File.createTempFile(trait, "tmp", dir); + } + + public static File createTemp(InputStream in) throws IOException { + File file = createTemp(); + + try (FileOutputStream out = new FileOutputStream(file)) { + StreamUtils.write(in, out); + } + + return file; + } + + /** + * 复制文件 + * @param source 源文件 + * @param target 目标文件 + * @param override 如果目标文件已存在是否覆盖 + * @param options 其他文件复制选项 {@link StandardCopyOption} + * @return 目标文件地址 + */ + public static Path copy(File source, File target, boolean override, CopyOption... options) throws IOException { + List list = new ArrayList<>(); + if (override) { + list.add(StandardCopyOption.REPLACE_EXISTING); + } + + if (options != null && options.length > 0) { + list.addAll(Arrays.asList(options)); + } + + return Files.copy(source.toPath(), target.toPath(), list.toArray(new CopyOption[0])); + } + + public static boolean delete(File file) { + try { + Files.delete(file.toPath()); + return true; + } + catch (IOException e) { + return false; + } + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/HtmlUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/HtmlUtils.java new file mode 100644 index 0000000..9750224 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/HtmlUtils.java @@ -0,0 +1,55 @@ +package com.baiye.util; + +import cn.hutool.core.text.CharSequenceUtil; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +/** + * @author Hccake 2020/12/21 + * @version 1.0 + */ +public final class HtmlUtils { + + private HtmlUtils() { + } + + /** + * html 转字符串,保留换行样式 + * @link https://stackoverflow.com/questions/5640334/how-do-i-preserve-line-breaks-when-using-jsoup-to-convert-html-to-plain-text + * @param html html字符串 + * @param mergeLineBreak 是否合并换行符 + * @return 保留换行格式的纯文本 + */ + public static String toText(String html, boolean mergeLineBreak) { + if (CharSequenceUtil.isBlank(html)) { + return html; + } + Document document = Jsoup.parse(html); + // makes html() preserve linebreaks and spacing + document.outputSettings(new Document.OutputSettings().prettyPrint(true)); + String result = document.wholeText(); + + // 合并多个换行 + if (mergeLineBreak) { + int oldLength; + do { + oldLength = result.length(); + result = result.replace('\r', '\n'); + result = result.replace("\n\n", "\n"); + } + while (result.length() != oldLength); + } + + return result; + } + + /** + * html 转字符串,保留换行样式,默认合并换行符 + * @param html html字符串 + * @return 保留换行格式的纯文本 + */ + public static String toText(String html) { + return toText(html, true); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/ImageUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/ImageUtils.java new file mode 100644 index 0000000..b3c7f2e --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/ImageUtils.java @@ -0,0 +1,297 @@ +package com.baiye.util; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.UtilityClass; +import org.springframework.util.StringUtils; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +/** + * @author lingting 2021/7/22 13:44 + */ +@UtilityClass +public class ImageUtils { + + private static final List RESOLVER_LIST; + + static { + RESOLVER_LIST = new ArrayList<>(16); + // gif + RESOLVER_LIST.add(new ImageResolver() { + @Override + public boolean isSupport(int r1, int r2, int r3) { + return r1 == 'G' && r2 == 'I' && r3 == 'F'; + } + + @Override + public void resolve(ImageInfo info, InputStream stream) throws IOException { + stream.skip(3); + info.setWidth(StreamUtils.readInt(stream, 2, false)); + info.setHeight(StreamUtils.readInt(stream, 2, false)); + info.setType("image/gif"); + } + }); + // jpg + RESOLVER_LIST.add(new ImageResolver() { + @Override + public boolean isSupport(int r1, int r2, int r3) { + return r1 == 0xFF && r2 == 0xD8 && r3 == 255; + } + + @Override + public void resolve(ImageInfo info, InputStream stream) throws IOException { + int r3; + do { + int marker = stream.read(); + int len = StreamUtils.readInt(stream, 2, true); + if (marker == 192 || marker == 193 || marker == 194) { + stream.skip(1); + info.setHeight(StreamUtils.readInt(stream, 2, true)); + info.setWidth(StreamUtils.readInt(stream, 2, true)); + info.setType("image/jpeg"); + break; + } + stream.skip(len - 2); + r3 = stream.read(); + } + while (r3 == 255); + } + }); + // png + RESOLVER_LIST.add(new ImageResolver() { + @Override + public boolean isSupport(int r1, int r2, int r3) { + return r1 == 137 && r2 == 80 && r3 == 78; + } + + @Override + public void resolve(ImageInfo info, InputStream stream) throws IOException { + stream.skip(15); + info.setWidth(StreamUtils.readInt(stream, 2, true)); + stream.skip(2); + info.setHeight(StreamUtils.readInt(stream, 2, true)); + info.setType("image/png"); + } + }); + // bmp + RESOLVER_LIST.add(new ImageResolver() { + @Override + public boolean isSupport(int r1, int r2, int r3) { + return r1 == 66 && r2 == 77; + } + + @Override + public void resolve(ImageInfo info, InputStream stream) throws IOException { + stream.skip(15); + info.setWidth(StreamUtils.readInt(stream, 2, false)); + stream.skip(2); + info.setHeight(StreamUtils.readInt(stream, 2, false)); + info.setType("image/bmp"); + } + }); + + } + + /** + * 注册解析方案 + * @param resolver 解析方案 + * @param index 所在位置. 越小越先执行, 最小值为0 + */ + public void registerResolver(ImageResolver resolver, int index) { + RESOLVER_LIST.add(Math.max(index, 0), resolver); + } + + /** + *

      解析速度快, 但是如果图片格式不在预期内会无法解析

      + *

      图片格式不在预期内, 会抛出异常, 可以使用普通方法解析

      + *

      + * 本方法会克隆传入的流, 并返回一个指针在起始位置的流对象 + *

      + *

      + * 快速解析图片数据, 返回值携带可用的流 + *

      + * @throws IOException 流异常时抛出 + */ + public ImageInfo quickResolveClone(InputStream stream) throws IOException { + final InputStream[] streams = StreamUtils.clone(stream, 2); + final ImageInfo info = new ImageInfo(); + // 返回可用的流 + info.setStream(streams[0]); + stream.close(); + // 用于解析流 + stream = streams[1]; + + int r1 = stream.read(); + int r2 = stream.read(); + int r3 = stream.read(); + for (ImageResolver resolver : RESOLVER_LIST) { + if (resolver.isSupport(r1, r2, r3)) { + resolver.resolve(info, stream); + break; + } + } + + if (!StringUtils.hasText(info.getType())) { + int r4 = stream.read(); + final boolean isTiff = (r1 == 'M' && r2 == 'M' && r3 == 0 && r4 == 42) + || (r1 == 'I' && r2 == 'I' && r3 == 42 && r4 == 0); + // TIFF + if (isTiff) { + tiffResolver(stream, info, r1); + } + + } + + stream.close(); + if (!StringUtils.hasText(info.getType())) { + info.getStream().close(); + throw new IOException("未知图片类型"); + } + return info; + } + + /** + *

      解析图片 - 此方法会把图片直接载入内存, 谨慎处理大图片

      + *

      + * 本方法会克隆传入的流, 并返回一个指针在起始位置的流对象 + *

      + * @param stream 流 + * @return com.cloud.core.util.ImageUtils.ImageInfo + * @throws IOException 流异常时抛出 + */ + public ImageInfo resolveClone(InputStream stream) throws IOException { + final InputStream[] streams = StreamUtils.clone(stream, 2); + final ImageInfo info = new ImageInfo(); + // 返回可用的流 + info.setStream(streams[0]); + stream.close(); + // 用于解析流 + stream = streams[1]; + try { + final ImageInputStream io = ImageIO.createImageInputStream(stream); + final Iterator readers = ImageIO.getImageReaders(io); + while (readers.hasNext()) { + final ImageReader reader = readers.next(); + info.setType("image/" + reader.getFormatName().toLowerCase(Locale.ROOT)); + reader.setInput(io); + info.setWidth(reader.getWidth(0)); + info.setHeight(reader.getHeight(0)); + } + + stream.close(); + } + catch (Exception e) { + info.getStream().close(); + throw e; + } + + if (!StringUtils.hasText(info.getType())) { + info.getStream().close(); + throw new IOException("未知图片类型"); + } + + return info; + } + + /** + *

      混合解析, 先进去快速解析, 无结果再进行直接解析

      + *

      解析图片 - 此方法有可能会把图片直接载入内存, 谨慎处理大图片

      + *

      + * 本方法会多次克隆传入的流, 并返回一个指针在起始位置的流对象 + *

      + * @param stream 流 + * @return com.cloud.core.util.ImageUtils.ImageInfo + */ + public ImageInfo mixResolveClone(InputStream stream) throws IOException { + InputStream[] streams = StreamUtils.clone(stream, 2); + + try { + return quickResolveClone(streams[0]); + } + catch (Exception e) { + return resolveClone(streams[1]); + } + + } + + @Getter + @Setter + public static class ImageInfo { + + private ImageInfo() { + } + + private InputStream stream; + + private long width; + + private long height; + + private String type; + + } + + public interface ImageResolver { + + /** + * 是否支持解析流 + * @param r1 流1 + * @param r2 流2 + * @param r3 流3 + * @return boolean + */ + boolean isSupport(int r1, int r2, int r3); + + /** + * 解析图片 + * @param info 详情 + * @param stream 图片流 + * @throws IOException 流异常时抛出 + */ + void resolve(ImageInfo info, InputStream stream) throws IOException; + + } + + /** + * tiff格式处理 + */ + private void tiffResolver(InputStream stream, ImageInfo info, int r1) throws IOException { + boolean bigEndian = r1 == 'M'; + int ifd = StreamUtils.readInt(stream, 4, bigEndian); + int entries; + stream.skip(ifd - 8); + entries = StreamUtils.readInt(stream, 2, bigEndian); + for (int i = 1; i <= entries; i++) { + int tag = StreamUtils.readInt(stream, 2, bigEndian); + int fieldType = StreamUtils.readInt(stream, 2, bigEndian); + int valOffset; + if ((fieldType == 3 || fieldType == 8)) { + valOffset = StreamUtils.readInt(stream, 2, bigEndian); + stream.skip(2); + } + else { + valOffset = StreamUtils.readInt(stream, 4, bigEndian); + } + if (tag == 256) { + info.setWidth(valOffset); + } + else if (tag == 257) { + info.setHeight(valOffset); + } + if (info.getWidth() != -1 && info.getHeight() != -1) { + info.setType("image/tiff"); + break; + } + } + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/IpUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/IpUtils.java new file mode 100644 index 0000000..9d4d7bc --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/IpUtils.java @@ -0,0 +1,69 @@ +package com.baiye.util; + +import cn.hutool.core.net.NetUtil; +import cn.hutool.core.util.ArrayUtil; + +import javax.servlet.http.HttpServletRequest; + +/** + * IP 工具类 + * + * @author Hccake + */ +public final class IpUtils { + + private IpUtils() { + } + + /** + * 如果在前端和服务端中间还有一层Node服务 在Node对前端数据进行处理并发起新请求时,需携带此头部信息 便于获取真实IP + */ + public static final String NODE_FORWARDED_IP = "Node-Forwarded-IP"; + + /** + * 获取客户端IP + */ + public static String getIpAddr(HttpServletRequest request) { + return getIpAddr(request, NODE_FORWARDED_IP); + } + + /** + * 获取客户端IP + *

      + * 参考 huTool 稍微调整了下headers 顺序 + */ + public static String getIpAddr(HttpServletRequest request, String... otherHeaderNames) { + String[] headers = { "X-Real-IP", "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR" }; + if (ArrayUtil.isNotEmpty(otherHeaderNames)) { + headers = ArrayUtil.addAll(headers, otherHeaderNames); + } + return getClientIpByHeader(request, headers); + } + + /** + * 获取客户端IP + * + *

      + * headerNames参数用于自定义检测的Header
      + * 需要注意的是,使用此方法获取的客户IP地址必须在Http服务器(例如Nginx)中配置头信息,否则容易造成IP伪造。 + *

      + * @param request 请求对象{@link HttpServletRequest} + * @param headerNames 自定义头,通常在Http服务器(例如Nginx)中配置 + * @return IP地址 + * @since 4.4.1 + */ + public static String getClientIpByHeader(HttpServletRequest request, String... headerNames) { + String ip; + for (String header : headerNames) { + ip = request.getHeader(header); + if (!NetUtil.isUnknown(ip)) { + return NetUtil.getMultistageReverseProxyIp(ip); + } + } + + ip = request.getRemoteAddr(); + return NetUtil.getMultistageReverseProxyIp(ip); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/JsonUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/JsonUtils.java new file mode 100644 index 0000000..817b798 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/JsonUtils.java @@ -0,0 +1,102 @@ +package com.baiye.util; + +import com.baiye.util.json.FastjsonJsonToolAdapter; +import com.baiye.util.json.GsonJsonToolAdapter; +import com.baiye.util.json.HuToolJsonToolAdapter; +import com.baiye.util.json.JacksonJsonToolAdapter; +import com.baiye.util.json.JsonTool; +import com.baiye.util.json.TypeReference; +import lombok.Getter; + +import java.lang.reflect.Type; + +/** + * @author lingting 2021/2/25 20:38 + */ +public final class JsonUtils { + + private static final String JACKSON_CLASS = "com.fasterxml.jackson.databind.ObjectMapper"; + + private static final String GSON_CLASS = "com.google.gson.Gson"; + + private static final String HUTOOL_JSON_CLASS = "cn.hutool.json.JSONConfig"; + + private static final String HUTOOL_JSON_TYPE_REFERENCE_CLASS = "cn.hutool.core.lang.TypeReference"; + + private static final String FAST_JSON_CLASS = "com.alibaba.fastjson.JSON"; + + private JsonUtils() { + } + + @Getter + private static JsonTool jsonTool; + + static { + if (classIsPresent(JACKSON_CLASS)) { + jsonTool = new JacksonJsonToolAdapter(); + } + else if (classIsPresent(GSON_CLASS)) { + jsonTool = new GsonJsonToolAdapter(); + } + else if (classIsPresent(HUTOOL_JSON_CLASS)) { + jsonTool = new HuToolJsonToolAdapter(); + } + else if (classIsPresent(FAST_JSON_CLASS)) { + jsonTool = new FastjsonJsonToolAdapter(); + } + } + + /** + * 切换适配器. 请注意 本方法全局生效 + */ + public static void switchAdapter(JsonTool jsonTool) { + JsonUtils.jsonTool = jsonTool; + } + + public static String toJson(Object obj) { + return jsonTool.toJson(obj); + } + + public static T toObj(String json, Class r) { + return jsonTool.toObj(json, r); + } + + public static T toObj(String json, Type t) { + // 防止误传入其他类型的 typeReference 走这个方法然后转换出错 + if (classIsPresent(HUTOOL_JSON_TYPE_REFERENCE_CLASS) && t instanceof cn.hutool.core.lang.TypeReference) { + return toObj(json, new TypeReference() { + @Override + public Type getType() { + return ((cn.hutool.core.lang.TypeReference) t).getType(); + } + }); + } + else if (classIsPresent(FAST_JSON_CLASS) && t instanceof com.alibaba.fastjson.TypeReference) { + return toObj(json, new TypeReference() { + @Override + public Type getType() { + return ((com.alibaba.fastjson.TypeReference) t).getType(); + } + }); + } + else if (classIsPresent(JACKSON_CLASS) && t instanceof com.fasterxml.jackson.core.type.TypeReference) { + return toObj(json, new TypeReference() { + @Override + public Type getType() { + return ((com.fasterxml.jackson.core.type.TypeReference) t).getType(); + } + }); + } + + return jsonTool.toObj(json, t); + } + + public static T toObj(String json, TypeReference t) { + return jsonTool.toObj(json, t); + } + + private static boolean classIsPresent(String className) { + return ClassUtils.isPresent(className, JsonUtils.class.getClassLoader()); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/LocalDateTimeUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/LocalDateTimeUtils.java new file mode 100644 index 0000000..9d3d209 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/LocalDateTimeUtils.java @@ -0,0 +1,120 @@ +package com.baiye.util; + +import lombok.experimental.UtilityClass; + +import java.time.*; +import java.time.format.DateTimeFormatter; + +/** + * @author lingting 2022/11/28 10:12 + */ +@UtilityClass +public class LocalDateTimeUtils { + + public static final ZoneOffset DEFAULT_ZONE_OFFSET = ZoneOffset.of("+8"); + + public static final ZoneId DEFAULT_ZONE_ID = DEFAULT_ZONE_OFFSET.normalized(); + + public static final String STRING_FORMATTER_YMD_HMS = "yyyy-MM-dd HH:mm:ss"; + + public static final DateTimeFormatter FORMATTER_YMD_HMS = DateTimeFormatter.ofPattern(STRING_FORMATTER_YMD_HMS); + + public static final String STRING_FORMATTER_YMD = "yyyy-MM-dd"; + + public static final DateTimeFormatter FORMATTER_YMD = DateTimeFormatter.ofPattern(STRING_FORMATTER_YMD); + + public static final String STRING_FORMATTER_HMS = "HH:mm:ss"; + + public static final DateTimeFormatter FORMATTER_HMS = DateTimeFormatter.ofPattern(STRING_FORMATTER_HMS); + + // region LocalDateTime + + /** + * 字符串转时间 + * @param str yyyy-MM-dd HH:mm:ss 格式字符串 + * @return java.time.LocalDateTime 时间 + */ + public static LocalDateTime parse(String str) { + return LocalDateTime.parse(str, FORMATTER_YMD_HMS); + } + + /** + * 时间戳转时间, 使用 GMT+8 时区 + * @param timestamp 时间戳 - 毫秒 + * @return java.time.LocalDateTime + */ + public static LocalDateTime parse(Long timestamp) { + return parse(timestamp, DEFAULT_ZONE_ID); + } + + /** + * 时间戳转时间 + * @param timestamp 时间戳 - 毫秒 + * @param zoneId 时区 + * @return java.time.LocalDateTime + */ + public static LocalDateTime parse(Long timestamp, ZoneId zoneId) { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), zoneId); + } + + public static Long toTimestamp(LocalDateTime dateTime) { + return toTimestamp(dateTime, DEFAULT_ZONE_OFFSET); + } + + public static Long toTimestamp(LocalDateTime dateTime, ZoneOffset offset) { + return dateTime.toInstant(offset).toEpochMilli(); + } + + public static String format(LocalDateTime dateTime) { + return format(dateTime, FORMATTER_YMD_HMS); + } + + public static String format(LocalDateTime dateTime, String formatter) { + return format(dateTime, DateTimeFormatter.ofPattern(formatter)); + } + + public static String format(LocalDateTime dateTime, DateTimeFormatter formatter) { + return formatter.format(dateTime); + } + + // endregion + + // region LocalDate + public static LocalDate parseDate(String str) { + return LocalDate.parse(str, FORMATTER_YMD); + } + + public static String format(LocalDate date) { + return format(date, FORMATTER_YMD); + } + + public static String format(LocalDate date, String formatter) { + return format(date, DateTimeFormatter.ofPattern(formatter)); + } + + public static String format(LocalDate date, DateTimeFormatter formatter) { + return formatter.format(date); + } + + // endregion + + // region LocalTime + public static LocalTime parseTime(String str) { + return LocalTime.parse(str, FORMATTER_HMS); + } + + public static String format(LocalTime time) { + return format(time, FORMATTER_HMS); + } + + public static String format(LocalTime time, String formatter) { + return format(time, DateTimeFormatter.ofPattern(formatter)); + } + + public static String format(LocalTime time, DateTimeFormatter formatter) { + return formatter.format(time); + } + + // endregion + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/SmsUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/SmsUtils.java new file mode 100644 index 0000000..4b88666 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/SmsUtils.java @@ -0,0 +1,78 @@ +package com.baiye.util; + +import com.baiye.charset.GSMCharset; + +import java.nio.charset.StandardCharsets; + +/** + * 短信的工具类 + * + * @author hccake + */ +public final class SmsUtils { + + private SmsUtils() { + } + + /** + * 短信载荷可用字节数 + */ + public static final int SMS_PAYLOAD_BYTE_NUM = 140; + + /** + * 在 GSM-7 编码下,短信的最大有效字数:(140 * 8) / 7 = 160 GSM-7 编码使用 7 个 bit 表示一个标准字符,对于 €^ {} []〜| + * 这些扩展字符会使用 2 个 7bit位 展示 + */ + public static final int MAX_WORLD_NUM_IN_GSM = 160; + + /** + * 在 UCS-2 编码下,长短信的各部分消息需要占用 7 个bit来记录 UDH + */ + public static final int MAX_WORLD_NUM_IN_UCS2 = 70; + + /** + * UDH 占用 6 Byte / 48 bit + */ + public static final int UDH_BYTE_NUM = 6; + + /** + * 根据短信内容,获得对应的短信条数 + *

      + * 每条短信的有效载荷为 140 个字节,如果消息文本长于 140 字节,则将会串联成为多条消息,又称长短信。 + * 长短信的各部分消息的载荷中会划分一部分字节用于创建用户数据头(UDH),用于接受设备对接收到的消息进行排序处理。 + *

      + * UDH占用 6个字节 或 48位。这减少了每个消息部分中可以包含多少个字符的空间。 + * @param smsContent 短信内容 + * @return 短信条数 + */ + public static int smsNumber(String smsContent) { + int wordsNum = GSMCharset.need7bitsNum(smsContent); + if (wordsNum == 0) { + return 0; + } + + // wordsNum > 0, 表示当前短信支持 GSM-7 编码 + if (wordsNum > 0) { + // 短短信没有 UDH,在 GSM-7 编码下,最大支持 160 个字符 + if (wordsNum <= MAX_WORLD_NUM_IN_GSM) { + return 1; + } + // 长短信,需要占用 48 个 bit,所以最大只能支持 (1120 - 48)/ 7 = 153 个字符 + return wordsNum % 153 == 0 ? wordsNum / 153 : wordsNum / 153 + 1; + } + + // 当不符合 GSM-7 编码时,使用 UCS-2 编码(默认大端序) + // UTF-16 兼容 UCS-2,所以这里可以用 UTF_16BE 来解码获得字节 + byte[] bytes = smsContent.getBytes(StandardCharsets.UTF_16BE); + // 两个字节表示一个字符 + wordsNum = bytes.length / 2; + // 短短信没有 UDH,在 UCS-2 编码下,最大支持 70 个字符 + if (wordsNum <= MAX_WORLD_NUM_IN_UCS2) { + return 1; + } + // 长短信,需要占用 6 个字节,所以最大只能支持 (140 - 6)/ 2 = 67 个字符 + return wordsNum % 67 == 0 ? wordsNum / 67 : wordsNum / 67 + 1; + + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/SpelUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/SpelUtils.java new file mode 100644 index 0000000..fb631ea --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/SpelUtils.java @@ -0,0 +1,89 @@ +package com.baiye.util; + +import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; +import java.util.List; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/3 10:29 + */ +@SuppressWarnings("SpellCheckingInspection") +public final class SpelUtils { + + private SpelUtils() { + } + + /** + * SpEL 解析器 + */ + public static final ExpressionParser PARSER = new SpelExpressionParser(); + + /** + * 方法参数获取 + */ + public static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new LocalVariableTableParameterNameDiscoverer(); + + /** + * 支持 #p0 参数索引的表达式解析 + * @param rootObject 根对象, method 所在类的对象实例 + * @param spelExpression spel 表达式 + * @param method 目标方法 + * @param args 方法入参 + * @return 解析后的字符串 + */ + public static String parseValueToString(Object rootObject, Method method, Object[] args, String spelExpression) { + StandardEvaluationContext context = getSpelContext(rootObject, method, args); + return parseValueToString(context, spelExpression); + } + + /** + * 支持 #p0 参数索引的表达式解析 + * @param rootObject 根对象, method 所在的对象 + * @param method 目标方法 + * @param args 方法实际入参 + * @return StandardEvaluationContext spel 上下文 + */ + public static StandardEvaluationContext getSpelContext(Object rootObject, Method method, Object[] args) { + // spel 上下文 + StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject, method, args, + PARAMETER_NAME_DISCOVERER); + // 方法参数名数组 + String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); + // 把方法参数放入 spel 上下文中 + if (parameterNames != null && parameterNames.length > 0) { + for (int i = 0; i < parameterNames.length; i++) { + context.setVariable(parameterNames[i], args[i]); + } + } + return context; + } + + /** + * 解析 spel 表达式 + * @param context spel 上下文 + * @param spelExpression spel 表达式 + * @return String 解析后的字符串 + */ + public static String parseValueToString(StandardEvaluationContext context, String spelExpression) { + return PARSER.parseExpression(spelExpression).getValue(context, String.class); + } + + /** + * 解析 spel 表达式 + * @param context spel 上下文 + * @param spelExpression spel 表达式 + * @return 解析后的 List + */ + public static List parseValueToStringList(StandardEvaluationContext context, String spelExpression) { + return PARSER.parseExpression(spelExpression).getValue(context, List.class); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/SpringUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/SpringUtils.java new file mode 100644 index 0000000..4b79738 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/SpringUtils.java @@ -0,0 +1,123 @@ +package com.baiye.util; + +import lombok.Setter; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @author lingting 2020/6/12 16:36 + */ +@Component +public class SpringUtils implements ApplicationContextAware { + + /** + * Spring应用上下文环境 + */ + @Setter + private static ApplicationContext context; + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + setContext(context); + } + + /** + * 通过name获取 Bean + * @param Bean类型 + * @param name Bean名称 + * @return Bean + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) { + return (T) context.getBean(name); + } + + /** + * 通过class获取Bean + * @param Bean类型 + * @param clazz Bean类 + * @return Bean对象 + */ + public static T getBean(Class clazz) { + return context.getBean(clazz); + } + + /** + * 通过name,以及Clazz返回指定的Bean + * @param bean类型 + * @param name Bean名称 + * @param clazz bean类型 + * @return Bean对象 + */ + public static T getBean(String name, Class clazz) { + return context.getBean(name, clazz); + } + + /** + * 获取指定类型对应的所有Bean,包括子类 + * @param Bean类型 + * @param type 类、接口,null表示获取所有bean + * @return 类型对应的bean,key是bean注册的name,value是Bean + */ + public static Map getBeansOfType(Class type) { + return context.getBeansOfType(type); + } + + /** + * 获取指定类型对应的Bean名称,包括子类 + * @param type 类、接口,null表示获取所有bean名称 + * @return bean名称 + */ + public static String[] getBeanNamesForType(Class type) { + return context.getBeanNamesForType(type); + } + + /** + * 获取配置文件配置项的值 + * @param key 配置项key + * @return 属性值 + */ + public static String getProperty(String key) { + return context.getEnvironment().getProperty(key); + } + + /** + * 获取当前的环境配置,无配置返回null + * @return 当前的环境配置 + */ + public static String[] getActiveProfiles() { + return context.getEnvironment().getActiveProfiles(); + } + + /** + * 获取环境 + */ + public static Environment getEnvironment() { + return context.getEnvironment(); + } + + /** + * 获取{@link ApplicationContext} + * @return {@link ApplicationContext} + */ + public static ApplicationContext getContext() { + return context; + } + + /** + * 发布事件 Spring 4.2+ 版本事件可以不再是{@link ApplicationEvent}的子类 + * @param event 待发布的事件 + */ + public static void publishEvent(Object event) { + if (context != null) { + context.publishEvent(event); + } + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/StreamUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/StreamUtils.java new file mode 100644 index 0000000..0ffe23b --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/StreamUtils.java @@ -0,0 +1,146 @@ +package com.baiye.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.io.*; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +/** + * @author lingting 2021/4/21 17:45 + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class StreamUtils { + + /** + * 默认大小 1024 * 1024 * 8 + */ + public static final int DEFAULT_SIZE = 10485760; + + public static byte[] read(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + write(in, out); + try { + return out.toByteArray(); + } + finally { + close(out); + } + } + + public static void write(InputStream in, OutputStream out) throws IOException { + write(in, out, DEFAULT_SIZE); + } + + public static void write(InputStream in, OutputStream out, int size) throws IOException { + byte[] bytes = new byte[size]; + int len; + + while (true) { + len = in.read(bytes); + if (len <= 0) { + break; + } + + out.write(bytes, 0, len); + } + } + + public static String toString(InputStream in) throws IOException { + return toString(in, DEFAULT_SIZE, StandardCharsets.UTF_8); + } + + public static String toString(InputStream in, int size, Charset charset) throws IOException { + byte[] bytes = new byte[size]; + int len; + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + while (true) { + len = in.read(bytes); + if (len <= 0) { + break; + } + outputStream.write(bytes, 0, len); + } + + return outputStream.toString(charset.name()); + } + + /** + * 从流中读取 int + * @author lingting 2021-07-22 14:54 + */ + public static int readInt(InputStream is, int noOfBytes, boolean bigEndian) throws IOException { + int ret = 0; + int sv = bigEndian ? ((noOfBytes - 1) * 8) : 0; + int cnt = bigEndian ? -8 : 8; + for (int i = 0; i < noOfBytes; i++) { + ret |= is.read() << sv; + sv += cnt; + } + return ret; + } + + public static void close(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } + catch (Exception e) { + // + } + } + + /** + * 克隆文件流 + *

      + * 注意: 在使用后及时关闭复制流 + *

      + * @param stream 源流 + * @param amounts 数量 + * @return 返回指定数量的从源流复制出来的只读流 + * @author lingting 2021-04-16 16:18 + */ + public static InputStream[] clone(InputStream stream, Integer amounts) throws IOException { + return clone(stream, amounts, DEFAULT_SIZE); + } + + public static InputStream[] clone(InputStream stream, Integer amounts, int size) throws IOException { + InputStream[] streams = new InputStream[amounts]; + File[] files = new File[amounts]; + FileOutputStream[] outs = new FileOutputStream[amounts]; + + byte[] buffer = new byte[size < 1 ? DEFAULT_SIZE : size]; + int len; + + while ((len = stream.read(buffer)) > -1) { + for (int i = 0, outsLength = outs.length; i < outsLength; i++) { + FileOutputStream out = outs[i]; + if (out == null) { + files[i] = FileUtils.createTemp("clone." + i + "." + System.currentTimeMillis()); + out = new FileOutputStream(files[i]); + outs[i] = out; + } + out.write(buffer, 0, len); + } + } + + for (int i = 0; i < files.length; i++) { + try { + outs[i].close(); + } + catch (IOException e) { + // + } + + streams[i] = Files.newInputStream(files[i].toPath()); + } + + return streams; + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/SystemUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/SystemUtils.java new file mode 100644 index 0000000..5bed394 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/SystemUtils.java @@ -0,0 +1,91 @@ +package com.baiye.util; + +import lombok.experimental.UtilityClass; + +import java.io.File; +import java.nio.charset.Charset; + +/** + * @author lingting 2022/6/25 12:10 + */ +@UtilityClass +public class SystemUtils { + + /** + * 当前系统是否为Windows系统, 参考以下系统API + * @see sun.awt.OSInfo#getOSType() + * @return boolean + */ + public static boolean isWindows() { + return osName().contains("Windows"); + } + + public static boolean isLinux() { + return osName().contains("Linux"); + } + + public static boolean isMacX() { + return osName().contains("OS X"); + } + + public static boolean isMac() { + return osName().contains("Mac OS"); + } + + public static boolean isAix() { + return osName().contains("AIX"); + } + + public static String osName() { + return System.getProperty("os.name"); + } + + /** + * 获取系统字符集 + */ + public static Charset charset() { + return Charset.forName(System.getProperty("sun.jnu.encoding")); + } + + public static String lineSeparator() { + return System.lineSeparator(); + } + + public static String fileSeparator() { + return File.separator; + } + + /** + * @see SystemUtils#tmpDir() + * @deprecated 更换了方法名 + */ + @Deprecated + public static File tempDir() { + return tmpDir(); + } + + public static File tmpDir() { + return new File(System.getProperty("java.io.tmpdir")); + } + + public static File tmpDirBallcat() { + return new File(System.getProperty("java.io.tmpdir"), "ballcat"); + } + + public static File homeDir() { + return new File(System.getProperty("user.home")); + } + + public static File homeDirBallcat() { + return new File(System.getProperty("user.home"), ".ballcat"); + } + + public static File workDir() { + return new File(System.getProperty("user.dir")); + } + + public static String username() { + return System.getProperty("user.name"); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/FastjsonJsonToolAdapter.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/FastjsonJsonToolAdapter.java new file mode 100644 index 0000000..1df3b6c --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/FastjsonJsonToolAdapter.java @@ -0,0 +1,67 @@ +package com.baiye.util.json; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.parser.Feature; +import com.alibaba.fastjson.serializer.SerializerFeature; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +/** + * @author lingting 2021/2/26 10:32 + */ +public class FastjsonJsonToolAdapter implements JsonTool { + + /** + * json str -> obj 时 + */ + static final List FEATURES = new ArrayList<>(Feature.values().length); + + /** + * obj -> json str + */ + static final List SERIALIZER_FEATURES = new ArrayList<>(SerializerFeature.values().length); + + private static Feature[] features = new Feature[0]; + + private static SerializerFeature[] serializerFeatures = new SerializerFeature[0]; + + /** + * 不要使用 config 以外的形式更新配置 + */ + public void config(BiConsumer, List> consumer) { + consumer.accept(FEATURES, SERIALIZER_FEATURES); + features = FEATURES.toArray(new Feature[0]); + serializerFeatures = SERIALIZER_FEATURES.toArray(new SerializerFeature[0]); + } + + @Override + public String toJson(Object obj) { + return JSON.toJSONString(obj, serializerFeatures); + } + + @Override + public T toObj(String json, Class r) { + return JSON.parseObject(json, r, features); + } + + @Override + public T toObj(String json, Type t) { + return JSON.parseObject(json, t, features); + } + + @Override + public T toObj(String json, TypeReference t) { + /* + * 由于 fastjson 下面这个方法 com.alibaba.fastjson.JSON.parseObject(java.lang.String, + * com.alibaba.fastjson.TypeReference, com.alibaba .fastjson.parser.Feature...) + * 直接调用了 type.type, 而不是使用的 getType()方法. 所以使用 + * com.debug.json.FastjsonAdapter.toObj(java.lang.String, java.lang .reflect.Type) + * 方法 + */ + + return toObj(json, t.getType()); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/GsonJsonToolAdapter.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/GsonJsonToolAdapter.java new file mode 100644 index 0000000..7796968 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/GsonJsonToolAdapter.java @@ -0,0 +1,53 @@ +package com.baiye.util.json; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import lombok.Getter; + +import java.lang.reflect.Type; +import java.util.function.Consumer; + +/** + * @author lingting 2021/2/26 10:22 + */ +public class GsonJsonToolAdapter implements JsonTool { + + @Getter + static Gson gson; + + static { + // 初始化gson配置 + gson = new GsonBuilder().create(); + } + + /** + * 由于 gson 实例不能更新. 需要 create 之后生成新的实例. 请避免在运行中更新配置. + */ + public static void config(Consumer consumer) { + GsonBuilder builder = gson.newBuilder(); + consumer.accept(builder); + // 更新 gson + gson = builder.create(); + } + + @Override + public String toJson(Object obj) { + return gson.toJson(obj); + } + + @Override + public T toObj(String json, Class r) { + return gson.fromJson(json, r); + } + + @Override + public T toObj(String json, Type t) { + return gson.fromJson(json, t); + } + + @Override + public T toObj(String json, TypeReference t) { + return gson.fromJson(json, t.getType()); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/HuToolJsonToolAdapter.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/HuToolJsonToolAdapter.java new file mode 100644 index 0000000..34783a5 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/HuToolJsonToolAdapter.java @@ -0,0 +1,47 @@ +package com.baiye.util.json; + +import cn.hutool.json.JSONConfig; +import cn.hutool.json.JSONUtil; +import lombok.Getter; + +import java.lang.reflect.Type; +import java.util.function.Consumer; + +/** + * @author lingting 2021/2/26 10:00 + */ +public class HuToolJsonToolAdapter implements JsonTool { + + @Getter + static JSONConfig jsonConfig = JSONConfig.create(); + + public void config(Consumer consumer) { + consumer.accept(jsonConfig); + } + + @Override + public String toJson(Object obj) { + return JSONUtil.parse(obj, jsonConfig).toJSONString(0); + } + + @Override + public T toObj(String json, Class r) { + return JSONUtil.parse(json, jsonConfig).toBean(r); + } + + @Override + public T toObj(String json, Type t) { + return JSONUtil.parse(json, jsonConfig).toBean(t); + } + + @Override + public T toObj(String json, TypeReference t) { + return JSONUtil.parse(json, jsonConfig).toBean(new cn.hutool.core.lang.TypeReference() { + @Override + public Type getType() { + return t.getType(); + } + }); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/JacksonJsonToolAdapter.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/JacksonJsonToolAdapter.java new file mode 100644 index 0000000..3e3d827 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/JacksonJsonToolAdapter.java @@ -0,0 +1,60 @@ +package com.baiye.util.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; + +import java.lang.reflect.Type; +import java.util.function.Consumer; + +/** + * @author lingting 2021/2/25 21:04 + */ +public class JacksonJsonToolAdapter implements JsonTool { + + @Getter + @Setter + static ObjectMapper mapper = new ObjectMapper(); + + static { + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + } + + public void config(Consumer consumer) { + consumer.accept(mapper); + } + + @SneakyThrows(JsonProcessingException.class) + @Override + public String toJson(Object obj) { + return mapper.writeValueAsString(obj); + } + + @SneakyThrows({ JsonMappingException.class, JsonProcessingException.class }) + @Override + public T toObj(String json, Class r) { + return mapper.readValue(json, r); + } + + @SneakyThrows({ JsonMappingException.class, JsonProcessingException.class }) + @Override + public T toObj(String json, Type t) { + return mapper.readValue(json, mapper.constructType(t)); + } + + @SneakyThrows({ JsonMappingException.class, JsonProcessingException.class }) + @Override + public T toObj(String json, TypeReference t) { + return mapper.readValue(json, new com.fasterxml.jackson.core.type.TypeReference() { + @Override + public Type getType() { + return t.getType(); + } + }); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/JsonTool.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/JsonTool.java new file mode 100644 index 0000000..41fa30b --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/JsonTool.java @@ -0,0 +1,44 @@ +package com.baiye.util.json; + +import java.lang.reflect.Type; + +/** + * json 相关 util类需实现本类 + * + * @author lingting 2021/2/25 20:43 + */ +public interface JsonTool { + + /** + * obj -> jsonStr + * @param obj obj + * @return java.lang.String + */ + String toJson(Object obj); + + /** + * jsonStr -> obj + * @param json json str + * @param r obj.class + * @return T + */ + T toObj(String json, Class r); + + /** + * jsonStr -> obj + * @param json json str + * @param t (obj.class)type + * @return T + */ + T toObj(String json, Type t); + + /** + * + * jsonStr -> obj + * @param json json str + * @param t TypeReference + * @return T + */ + T toObj(String json, TypeReference t); + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/TypeReference.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/TypeReference.java new file mode 100644 index 0000000..1f31aa9 --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/json/TypeReference.java @@ -0,0 +1,41 @@ +package com.baiye.util.json; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * @author lingting 2021/2/25 21:46 + */ +public abstract class TypeReference implements Comparable> { + + protected final Type type; + + protected TypeReference() { + Type superClass = getClass().getGenericSuperclass(); + if (superClass instanceof Class) { + throw new IllegalArgumentException( + "Internal error: TypeReference constructed without actual type information"); + } + type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; + } + + public Type getType() { + return type; + } + + @Override + public int compareTo(TypeReference o) { + return 0; + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/tree/TreeNode.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/tree/TreeNode.java new file mode 100644 index 0000000..0292cad --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/tree/TreeNode.java @@ -0,0 +1,36 @@ +package com.baiye.util.tree; + +import java.util.List; + +/** + * @author Hccake + * @version 1.0 + * @date 2020/6/21 17:05 + */ +public interface TreeNode { + + /** + * 获取节点 key + * @return 树节点 key + */ + I getKey(); + + /** + * 获取该节点的父节点 key + * @return 父节点 key + */ + I getParentKey(); + + /** + * 设置节点的子节点列表 + * @param children 子节点 + */ + > void setChildren(List children); + + /** + * 获取所有子节点 + * @return 子节点列表 + */ + > List getChildren(); + +} diff --git a/marketing-scrm-common/common-util/src/main/java/com/baiye/util/tree/TreeUtils.java b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/tree/TreeUtils.java new file mode 100644 index 0000000..50a0d9a --- /dev/null +++ b/marketing-scrm-common/common-util/src/main/java/com/baiye/util/tree/TreeUtils.java @@ -0,0 +1,328 @@ +package com.baiye.util.tree; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import lombok.experimental.UtilityClass; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author Hccake 2020/6/21 17:21 + * @version 1.0 + */ +@UtilityClass +public class TreeUtils { + + /** + * 根据一个TreeNode集合,返回构建好的树列表 + * @param nodes TreeNode集合 + * @param rootId 根节点Id + * @param TreeNode的子类 + * @param TreeNodeId的类型 + * @return 树列表 + */ + public , I> List buildTree(List nodes, I rootId) { + return TreeUtils.buildTree(nodes, rootId, Function.identity(), null); + } + + /** + * 根据一个TreeNode集合,返回构建好的树列表 + * @param nodes TreeNode集合 + * @param rootId 根节点Id + * @param comparator 树节点排序规则 + * @param TreeNode的子类 + * @param TreeNodeId的类型 + * @return 树列表 + */ + public , I> List buildTree(List nodes, I rootId, Comparator comparator) { + return TreeUtils.buildTree(nodes, rootId, Function.identity(), comparator); + } + + /** + * 根据源数据列表转换为树 + * @param list 源数据列表 + * @param rootId 根节点Id + * @param convertToTree 转换方法 + * @param TreeNode的子类 + * @param TreeNodeId的类型 + * @param 源数据类型 + * @return 树列表 + */ + public , I, R> List buildTree(List list, I rootId, Function convertToTree) { + return TreeUtils.buildTree(list, rootId, convertToTree, null); + } + + /** + * 根据源数据列表转换为树 + * @param list 源数据列表 + * @param rootId 根节点Id + * @param convertToTree 转换方法 + * @param comparator 树节点排序规则 + * @param TreeNode的子类 + * @param TreeNodeId的类型 + * @param 源数据类型 + * @return 树列表 + */ + public , I, R> List buildTree(List list, I rootId, Function convertToTree, + Comparator comparator) { + if (list == null || list.isEmpty()) { + return new ArrayList<>(); + } + + // 转换为 TreeNode + Stream tStream = list.stream().map(convertToTree); + // 如果需要排序,则在收集时进行排序处理 + if (comparator != null) { + tStream = tStream.sorted(comparator); + } + // 根据 parentId 进行分组 + Map> childrenMap = tStream + .collect(Collectors.groupingBy(T::getParentKey, LinkedHashMap::new, Collectors.toList())); + + // 根据根节点ID拿到一级节点 + List treeList = childrenMap.get(rootId); + // 异常数据校验 + Assert.notEmpty(treeList, "错误的数据,找不到根节点的子节点"); + // 遍历所有一级节点,赋值其子节点 + treeList.forEach(node -> TreeUtils.setChildren(node, childrenMap)); + return treeList; + } + + /** + * 从所有节点列表中查找并设置parent的所有子节点 + * @param parent 父节点 + * @param childrenMap 子节点集合Map(k: parentId, v: Node) + */ + public , I> void setChildren(T parent, Map> childrenMap) { + I parentId = parent.getKey(); + List children = childrenMap.get(parentId); + // 如果有孩子节点则赋值,且给孩子节点的孩子节点赋值 + if (CollUtil.isNotEmpty(children)) { + parent.setChildren(children); + children.forEach(node -> TreeUtils.setChildren(node, childrenMap)); + } + else { + parent.setChildren(new ArrayList<>()); + } + } + + /** + * 获取指定树节点下的所有叶子节点 + * @param parent 父节点 + * @param 树节点的类型 + * @param 树节点的 id 类型 + * @return 叶子节点 + */ + public , I> List getLeafs(T parent) { + List leafs = new ArrayList<>(); + fillLeaf(parent, leafs); + return leafs; + } + + /** + * 将parent的所有叶子节点填充至leafs列表中 + * @param parent 父节点 + * @param leafs 叶子节点列表 + * @param 实际节点类型 + */ + public , I> void fillLeaf(T parent, List leafs) { + List children = parent.getChildren(); + // 如果节点没有子节点则说明为叶子节点 + if (CollUtil.isEmpty(children)) { + leafs.add(parent); + return; + } + // 递归调用子节点,查找叶子节点 + for (T child : children) { + fillLeaf(child, leafs); + } + } + + /** + * 获取树节点Id + * @param treeList 树列表 + * @param TreeNode实现类 + * @param TreeNodeId 类型 + * @return List 节点Id列表 + */ + public , I> List getTreeNodeIds(List treeList) { + List ids = new ArrayList<>(); + fillTreeNodeIds(ids, treeList); + return ids; + } + + /** + * 填充树节点Id + * @param ids 节点Id列表 + * @param treeList 树列表 + * @param TreeNode实现类 + * @param TreeNodeId 类型 + */ + public , I> void fillTreeNodeIds(List ids, List treeList) { + // 如果节点没有子节点则说明为叶子节点 + if (CollUtil.isEmpty(treeList)) { + return; + } + for (T treeNode : treeList) { + ids.add(treeNode.getKey()); + List children = treeNode.getChildren(); + if (CollUtil.isNotEmpty(children)) { + fillTreeNodeIds(ids, children); + } + } + } + + /** + * 将一颗树的所有节点平铺到一个 list 中 + * @param treeNode 树节点 + * @param 树节点的类型 + * @param 树节点的 id 类型 + * @return 所有树节点组成的列表 + */ + public , I> List treeToList(T treeNode) { + return treeToList(treeNode, Function.identity()); + } + + /** + * 将一颗树的所有节点平铺到 list 中 + * @param treeNode 树节点 + * @param converter 转换器,用于将树节点的类型进行转换,再存储到 list 中 + * @param 树节点的类型 + * @param 树节点的 id 类型 + * @param 转换器转换后的类型 + * @return List + */ + public , I, R> List treeToList(T treeNode, Function converter) { + List list = new ArrayList<>(); + + // 使用队列存储未处理的树节点 + Queue queue = new LinkedList<>(); + queue.add(treeNode); + + while (!queue.isEmpty()) { + // 弹出一个树节点 + T node = queue.poll(); + if (node == null) { + continue; + } + + // 如果当前节点的含有子节点,则添加到队列中 + List children = node.getChildren(); + if (CollUtil.isNotEmpty(children)) { + queue.addAll(children); + } + + // 不再保留对子节点的引用 + node.setChildren(null); + // 转换树节点,并将结果添加到 list 中 + list.add(converter.apply(node)); + } + return list; + } + + /** + * 将一组树的所有节点平铺到一个 list 中 + * @param treeNodes 树节点集合 + * @param 树节点的类型 + * @param 树节点的 id 类型 + * @return 所有树节点组成的列表 + */ + public , I> List treeToList(List treeNodes) { + return treeToList(treeNodes, Function.identity()); + } + + /** + * 将一组树的所有节点平铺到一个 list 中 + * @param treeNodes 树节点集合 + * @param converter 转换器,用于将树节点的类型进行转换,再存储到 list 中 + * @param 树节点的类型 + * @param 树节点的 id 类型 + * @param 转换器转换后的类型 + * @return 所有树节点组成的列表 + */ + public , I, R> List treeToList(List treeNodes, Function converter) { + return treeNodes.stream() + .map(node -> treeToList(node, converter)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + /** + * 根据指定规则进行树剪枝 + * @param treeNodes 待剪枝的树节点列表 + * @param TreeNode + * @param matcher 匹配规则 + * @return 剪枝完成后的树节点列表 + */ + public , I> List pruneTree(List treeNodes, Predicate matcher) { + List result = new ArrayList<>(); + if (CollUtil.isEmpty(treeNodes)) { + return result; + } + for (T treeNode : treeNodes) { + List children = pruneTree(treeNode.getChildren(), matcher); + if (CollUtil.isNotEmpty(children)) { + treeNode.setChildren(children); + result.add(treeNode); + } + else if (matcher.test(treeNode)) { + treeNode.setChildren(null); + result.add(treeNode); + } + } + return result; + } + + /** + * 根据指定规则进行树剪枝 + * @param treeNode 待剪枝的树节点 + * @param TreeNode + * @param matcher 匹配规则 + * @return 剪枝完成后的树节点 + */ + public , I> T pruneTree(T treeNode, Predicate matcher) { + List children = pruneTree(treeNode.getChildren(), matcher); + boolean childrenMatched = CollUtil.isNotEmpty(children); + if (childrenMatched) { + treeNode.setChildren(children); + } + boolean nodeMatched = matcher.test(treeNode); + return (nodeMatched || childrenMatched) ? treeNode : null; + } + + /** + * 遍历树节点(深度优先) + */ + public , I> void forEachDFS(T treeNode, T parentTreeNode, BiConsumer action) { + action.accept(treeNode, parentTreeNode); + List children = treeNode.getChildren(); + forEachDFS(children, parentTreeNode, action); + } + + /** + * 遍历树节点(深度优先) + */ + public , I> void forEachDFS(List treeNodes, T parentTreeNode, BiConsumer action) { + if (treeNodes == null || treeNodes.isEmpty()) { + return; + } + for (T treeNode : treeNodes) { + List children = treeNode.getChildren(); + action.accept(treeNode, parentTreeNode); + forEachDFS(children, treeNode, action); + } + } + +} diff --git a/marketing-scrm-common/common-websocket/pom.xml b/marketing-scrm-common/common-websocket/pom.xml new file mode 100644 index 0000000..2e1373e --- /dev/null +++ b/marketing-scrm-common/common-websocket/pom.xml @@ -0,0 +1,45 @@ + + + + marketing-scrm-common + com.baiye + ${revision} + + 4.0.0 + common-websocket + + + + com.baiye + common-util + ${revision} + + + jakarta.annotation + jakarta.annotation-api + + + + org.apache.rocketmq + rocketmq-spring-boot-starter + true + + + + org.slf4j + slf4j-api + + + + + org.springframework.data + spring-data-redis + true + + + org.springframework + spring-websocket + + + diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/WebSocketMessageSender.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/WebSocketMessageSender.java new file mode 100644 index 0000000..14830aa --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/WebSocketMessageSender.java @@ -0,0 +1,44 @@ +package com.baiye; + +import com.baiye.message.JsonWebSocketMessage; +import com.baiye.util.JsonUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.io.IOException; + +/** + * @author Hccake 2021/1/4 + * @version 1.0 + */ +@Slf4j +public final class WebSocketMessageSender { + + private WebSocketMessageSender() { + } + + public static void send(WebSocketSession session, JsonWebSocketMessage message) { + send(session, JsonUtils.toJson(message)); + } + + public static boolean send(WebSocketSession session, String message) { + if (session == null) { + log.error("[send] session 为 null"); + return false; + } + if (!session.isOpen()) { + log.error("[send] session 已经关闭"); + return false; + } + try { + session.sendMessage(new TextMessage(message)); + } + catch (IOException e) { + log.error("[send] session({}) 发送消息({}) 异常", session, message, e); + return false; + } + return true; + } + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/AbstractMessageDistributor.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/AbstractMessageDistributor.java new file mode 100644 index 0000000..f7a3fa7 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/AbstractMessageDistributor.java @@ -0,0 +1,65 @@ +package com.baiye.distribute; + +import cn.hutool.core.collection.CollUtil; +import com.baiye.WebSocketMessageSender; +import com.baiye.session.WebSocketSessionStore; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Collection; + +/** + * @author hccake + */ +@Slf4j +public abstract class AbstractMessageDistributor implements MessageDistributor { + + private final WebSocketSessionStore webSocketSessionStore; + + protected AbstractMessageDistributor(WebSocketSessionStore webSocketSessionStore) { + this.webSocketSessionStore = webSocketSessionStore; + } + + /** + * 对当前服务中的 websocket 连接做消息推送 + * @param messageDO 消息实体 + */ + protected void doSend(MessageDO messageDO) { + + // 是否广播发送 + Boolean needBroadcast = messageDO.getNeedBroadcast(); + + // 获取待发送的 sessionKeys + Collection sessionKeys; + if (needBroadcast != null && needBroadcast) { + sessionKeys = webSocketSessionStore.getSessionKeys(); + } + else { + sessionKeys = messageDO.getSessionKeys(); + } + if (CollUtil.isEmpty(sessionKeys)) { + log.warn("发送 websocket 消息,却没有找到对应 sessionKeys, messageDo: {}", messageDO); + return; + } + + String messageText = messageDO.getMessageText(); + Boolean onlyOneClientInSameKey = messageDO.getOnlyOneClientInSameKey(); + + for (Object sessionKey : sessionKeys) { + Collection sessions = webSocketSessionStore.getSessions(sessionKey); + if (CollUtil.isNotEmpty(sessions)) { + // 相同 sessionKey 的客户端只推送一次操作 + if (onlyOneClientInSameKey != null && onlyOneClientInSameKey) { + WebSocketSession wsSession = CollUtil.get(sessions, 0); + WebSocketMessageSender.send(wsSession, messageText); + continue; + } + for (WebSocketSession wsSession : sessions) { + log.info("============ send message time {} body {} ============", System.currentTimeMillis(), messageText); + WebSocketMessageSender.send(wsSession, messageText); + } + } + } + } + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/LocalMessageDistributor.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/LocalMessageDistributor.java new file mode 100644 index 0000000..67994c3 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/LocalMessageDistributor.java @@ -0,0 +1,26 @@ +package com.baiye.distribute; + +import com.baiye.session.WebSocketSessionStore; + +/** + * 本地消息分发,直接进行发送 + * + * @author Hccake 2021/1/12 + * @version 1.0 + */ +public class LocalMessageDistributor extends AbstractMessageDistributor { + + public LocalMessageDistributor(WebSocketSessionStore webSocketSessionStore) { + super(webSocketSessionStore); + } + + /** + * 消息分发 + * @param messageDO 发送的消息 + */ + @Override + public void distribute(MessageDO messageDO) { + doSend(messageDO); + } + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/MessageDO.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/MessageDO.java new file mode 100644 index 0000000..634e3db --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/MessageDO.java @@ -0,0 +1,36 @@ +package com.baiye.distribute; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * @author Hccake 2021/1/12 + * @version 1.0 + */ +@Data +@Accessors(chain = true) +public class MessageDO { + + /** + * 是否广播 + */ + private Boolean needBroadcast; + + /** + * 对于拥有相同 sessionKey 的客户端,仅对其中的一个进行发送 + */ + private Boolean onlyOneClientInSameKey; + + /** + * 需要发送的 sessionKeys 集合,当广播时,不需要 + */ + private List sessionKeys; + + /** + * 需要发送的消息文本 + */ + private String messageText; + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/MessageDistributor.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/MessageDistributor.java new file mode 100644 index 0000000..a5abda6 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/MessageDistributor.java @@ -0,0 +1,17 @@ +package com.baiye.distribute; + +/** + * 消息分发器 + * + * @author Hccake 2021/1/12 + * @version 1.0 + */ +public interface MessageDistributor { + + /** + * 消息分发 + * @param messageDO 发送的消息 + */ + void distribute(MessageDO messageDO); + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/RedisMessageDistributor.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/RedisMessageDistributor.java new file mode 100644 index 0000000..1a01869 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/RedisMessageDistributor.java @@ -0,0 +1,56 @@ +package com.baiye.distribute; + +import com.baiye.util.JsonUtils; +import com.baiye.session.WebSocketSessionStore; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; + +/** + * 基于 redis PUB/SUB 的消息分发器, 订阅 websocket 发送消息,接收到消息时进行推送 + * + * @author Hccake 2021/1/12 + * @version 1.0 + */ +@Slf4j +public class RedisMessageDistributor extends AbstractMessageDistributor implements MessageListener { + + public static final String CHANNEL = "websocket-send"; + + private final StringRedisTemplate stringRedisTemplate; + + public RedisMessageDistributor(WebSocketSessionStore webSocketSessionStore, + StringRedisTemplate stringRedisTemplate) { + super(webSocketSessionStore); + this.stringRedisTemplate = stringRedisTemplate; + } + + /** + * 消息分发 + * @param messageDO 发送的消息 + */ + @Override + public void distribute(MessageDO messageDO) { + String str = JsonUtils.toJson(messageDO); + stringRedisTemplate.convertAndSend(CHANNEL, str); + } + + @Override + public void onMessage(Message message, byte[] bytes) { + log.info("redis channel Listener message send {}", message); + byte[] channelBytes = message.getChannel(); + RedisSerializer stringSerializer = stringRedisTemplate.getStringSerializer(); + String channel = stringSerializer.deserialize(channelBytes); + + // 这里没有使用通配符,所以一定是true + if (CHANNEL.equals(channel)) { + byte[] bodyBytes = message.getBody(); + String body = stringSerializer.deserialize(bodyBytes); + MessageDO messageDO = JsonUtils.toObj(body, MessageDO.class); + doSend(messageDO); + } + } + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/RedisMessageListenerInitializer.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/RedisMessageListenerInitializer.java new file mode 100644 index 0000000..8754d8a --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/RedisMessageListenerInitializer.java @@ -0,0 +1,27 @@ +package com.baiye.distribute; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +import javax.annotation.PostConstruct; + +/** + * 初始化 redis 消息的监听器 + * + * @author Hccake + */ +@RequiredArgsConstructor +public class RedisMessageListenerInitializer { + + private final RedisMessageListenerContainer redisMessageListenerContainer; + + private final RedisMessageDistributor redisWebsocketMessageListener; + + @PostConstruct + public void addMessageListener() { + redisMessageListenerContainer.addMessageListener(redisWebsocketMessageListener, + new PatternTopic(RedisMessageDistributor.CHANNEL)); + } + +} \ No newline at end of file diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/RocketmqMessageDistributor.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/RocketmqMessageDistributor.java new file mode 100644 index 0000000..65b5aae --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/distribute/RocketmqMessageDistributor.java @@ -0,0 +1,77 @@ +package com.baiye.distribute; + +import com.baiye.util.JsonUtils; +import com.baiye.exception.ErrorJsonMessageException; +import com.baiye.session.WebSocketSessionStore; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.spring.annotation.MessageModel; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.beans.factory.annotation.Value; + +import java.nio.charset.StandardCharsets; + +/** + * MQ发送消息,接收到消息时进行推送, 广播模式 + *

      + * + * @author liu_yx 2022年06月30日 14:10:10 + * @since 0.9.0 + */ +@Slf4j +@RocketMQMessageListener( + consumerGroup = "${spring.application.name:default-ballcat-application}-${spring.profiles.active:dev}", + topic = "${spring.application.name:default-ballcat-application}-${spring.profiles.active:dev}", + selectorExpression = "${ballcat.websocket.mq.tag}", messageModel = MessageModel.BROADCASTING) +public class RocketmqMessageDistributor extends AbstractMessageDistributor implements RocketMQListener { + + @Value("${spring.application.name}") + private String appName; + + @Value("${ballcat.websocket.mq.tag}") + private String tag; + + private final RocketMQTemplate template; + + public RocketmqMessageDistributor(WebSocketSessionStore webSocketSessionStore, RocketMQTemplate template) { + super(webSocketSessionStore); + this.template = template; + } + + /** + * 消息分发 + * @param messageDO 发送的消息 + */ + @Override + public void distribute(MessageDO messageDO) { + log.info("the send message body is [{}]", messageDO); + String destination = this.appName + ":" + this.tag; + SendResult sendResult = this.template.sendAndReceive(destination, JsonUtils.toJson(messageDO), + SendResult.class); + if (log.isDebugEnabled()) { + log.debug("send message to `{}` finished. result:{}", destination, sendResult); + } + } + + /** + * 消息消费 + * @param message 接收的消息 + */ + @Override + public void onMessage(MessageExt message) { + String body = new String(message.getBody(), StandardCharsets.UTF_8); + MessageDO event = JsonUtils.toObj(body, MessageDO.class); + log.info("the content is [{}]", event); + try { + this.doSend(event); + } + catch (Exception e) { + log.error("MQ消费信息处理异常: {}", e.getMessage(), e); + throw new ErrorJsonMessageException("MQ消费信息处理异常, " + e.getMessage()); + } + } + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/exception/ErrorJsonMessageException.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/exception/ErrorJsonMessageException.java new file mode 100644 index 0000000..b9dcf0f --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/exception/ErrorJsonMessageException.java @@ -0,0 +1,14 @@ +package com.baiye.exception; + +/** + * 错误的 json 消息 + * + * @author hccake + */ +public class ErrorJsonMessageException extends RuntimeException { + + public ErrorJsonMessageException(String message) { + super(message); + } + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/ConcurrentWebSocketSessionOptions.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/ConcurrentWebSocketSessionOptions.java new file mode 100644 index 0000000..8a98b0d --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/ConcurrentWebSocketSessionOptions.java @@ -0,0 +1,34 @@ +package com.baiye.handler; + +import lombok.Data; +import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator; + +/** + * 并发使用 WebSocketSession 的相关配置 + * + * @author hccake + */ +@Data +public class ConcurrentWebSocketSessionOptions { + + /** + * 是否在多线程环境下进行发送 + */ + private boolean enable = false; + + /** + * 发送时间的限制(ms) + */ + private int sendTimeLimit = 1000 * 5; + + /** + * 发送消息缓冲上限 (byte) + */ + private int bufferSizeLimit = 1024 * 100; + + /** + * 溢出时的执行策略 + */ + ConcurrentWebSocketSessionDecorator.OverflowStrategy overflowStrategy = ConcurrentWebSocketSessionDecorator.OverflowStrategy.TERMINATE; + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/CustomWebSocketHandler.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/CustomWebSocketHandler.java new file mode 100644 index 0000000..393a4d6 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/CustomWebSocketHandler.java @@ -0,0 +1,103 @@ +package com.baiye.handler; + +import com.baiye.message.JsonWebSocketMessage; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.baiye.exception.ErrorJsonMessageException; +import com.baiye.holder.JsonMessageHandlerHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +/** + * @author Hccake 2020/12/31 + * @version 1.0 + */ +@Slf4j +public class CustomWebSocketHandler extends TextWebSocketHandler { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + static { + // 有特殊需要转义字符, 不报错 + MAPPER.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()); + } + + private PlanTextMessageHandler planTextMessageHandler; + + public CustomWebSocketHandler() { + } + + public CustomWebSocketHandler(PlanTextMessageHandler planTextMessageHandler) { + this.planTextMessageHandler = planTextMessageHandler; + } + + @Override + public void handleTextMessage(WebSocketSession session, TextMessage message) { + // 空消息不处理 + if (message.getPayloadLength() == 0) { + return; + } + + // 获取消息载荷 + String payload = message.getPayload(); + try { + // 尝试使用 json 处理 + handleWithJson(session, payload); + } + catch (ErrorJsonMessageException ex) { + log.debug("消息载荷 [{}] 回退使用 PlanTextMessageHandler,原因:{}", payload, ex.getMessage()); + // fallback 使用普通文本处理 + if (planTextMessageHandler != null) { + planTextMessageHandler.handle(session, payload); + } + else { + log.error("[handleTextMessage] 普通文本消息({})没有对应的消息处理器", payload); + } + } + + } + + private void handleWithJson(WebSocketSession session, String payload) { + // 消息类型必有一属性type,先解析,获取该属性 + JsonNode jsonNode = null; + try { + jsonNode = MAPPER.readTree(payload); + } + catch (JsonProcessingException e) { + throw new ErrorJsonMessageException("json 解析异常"); + } + + // 必须是 object 类型 + if (!jsonNode.isObject()) { + throw new ErrorJsonMessageException("json 格式异常!非 object 类型!"); + } + + JsonNode typeNode = jsonNode.get(JsonWebSocketMessage.TYPE_FIELD); + String messageType = typeNode.asText(); + if (messageType == null) { + throw new ErrorJsonMessageException("json 无 type 属性"); + } + + // 获得对应的消息处理器 + JsonMessageHandler jsonMessageHandler = JsonMessageHandlerHolder.getHandler(messageType); + if (jsonMessageHandler == null) { + log.error("[handleTextMessage] 消息类型({})不存在对应的消息处理器", messageType); + return; + } + // 消息处理 + Class messageClass = jsonMessageHandler.getMessageClass(); + JsonWebSocketMessage websocketMessageJson; + try { + websocketMessageJson = MAPPER.treeToValue(jsonNode, messageClass); + } + catch (JsonProcessingException e) { + throw new ErrorJsonMessageException("消息序列化异常,class " + messageClass); + } + jsonMessageHandler.handle(session, websocketMessageJson); + } + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/JsonMessageHandler.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/JsonMessageHandler.java new file mode 100644 index 0000000..ca62e18 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/JsonMessageHandler.java @@ -0,0 +1,31 @@ +package com.baiye.handler; + +import com.baiye.message.JsonWebSocketMessage; +import org.springframework.web.socket.WebSocketSession; + +/** + * @author Hccake 2021/1/4 + * @version 1.0 + */ +public interface JsonMessageHandler { + + /** + * JsonWebSocketMessage 类型消息处理 + * @param session 当前接收 session + * @param message 当前接收到的 message + */ + void handle(WebSocketSession session, T message); + + /** + * 当前处理器处理的消息类型 + * @return messageType + */ + String type(); + + /** + * 当前处理器对应的消息Class + * @return Class + */ + Class getMessageClass(); + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/PingJsonMessageHandler.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/PingJsonMessageHandler.java new file mode 100644 index 0000000..741a52e --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/PingJsonMessageHandler.java @@ -0,0 +1,34 @@ +package com.baiye.handler; + +import com.baiye.message.JsonWebSocketMessage; +import com.baiye.message.PingJsonWebSocketMessage; +import com.baiye.message.PongJsonWebSocketMessage; +import com.baiye.message.WebSocketMessageTypeEnum; +import com.baiye.WebSocketMessageSender; +import org.springframework.web.socket.WebSocketSession; + +/** + * 心跳处理,接收到客户端的ping时,立刻回复一个pong + * + * @author Hccake 2021/1/4 + * @version 1.0 + */ +public class PingJsonMessageHandler implements JsonMessageHandler { + + @Override + public void handle(WebSocketSession session, PingJsonWebSocketMessage message) { + JsonWebSocketMessage pongJsonWebSocketMessage = new PongJsonWebSocketMessage(); + WebSocketMessageSender.send(session, pongJsonWebSocketMessage); + } + + @Override + public String type() { + return WebSocketMessageTypeEnum.PING.getValue(); + } + + @Override + public Class getMessageClass() { + return PingJsonWebSocketMessage.class; + } + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/PlanTextMessageHandler.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/PlanTextMessageHandler.java new file mode 100644 index 0000000..3d5f347 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/handler/PlanTextMessageHandler.java @@ -0,0 +1,22 @@ +package com.baiye.handler; + +import com.baiye.message.JsonWebSocketMessage; +import org.springframework.web.socket.WebSocketSession; + +/** + * 普通文本类型(非指定json类型)的消息处理器 即消息不满足于我们定义的Json类型消息时,所使用的处理器 + * + * @see JsonWebSocketMessage + * @author Hccake 2021/1/5 + * @version 1.0 + */ +public interface PlanTextMessageHandler { + + /** + * 普通文本消息处理 + * @param session 当前接收消息的session + * @param message 文本消息 + */ + void handle(WebSocketSession session, String message); + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/holder/JsonMessageHandlerHolder.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/holder/JsonMessageHandlerHolder.java new file mode 100644 index 0000000..26aa82c --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/holder/JsonMessageHandlerHolder.java @@ -0,0 +1,28 @@ +package com.baiye.holder; + +import com.baiye.handler.JsonMessageHandler; +import com.baiye.message.JsonWebSocketMessage; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Hccake 2021/1/4 + * @version 1.0 + */ +public final class JsonMessageHandlerHolder { + + private JsonMessageHandlerHolder() { + } + + private static final Map> MESSAGE_HANDLER_MAP = new ConcurrentHashMap<>(); + + public static JsonMessageHandler getHandler(String type) { + return MESSAGE_HANDLER_MAP.get(type); + } + + public static void addHandler(JsonMessageHandler jsonMessageHandler) { + MESSAGE_HANDLER_MAP.put(jsonMessageHandler.type(), jsonMessageHandler); + } + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/holder/JsonMessageHandlerInitializer.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/holder/JsonMessageHandlerInitializer.java new file mode 100644 index 0000000..7b069e1 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/holder/JsonMessageHandlerInitializer.java @@ -0,0 +1,31 @@ +package com.baiye.holder; + +import com.baiye.handler.JsonMessageHandler; +import com.baiye.message.JsonWebSocketMessage; +import lombok.RequiredArgsConstructor; + +import javax.annotation.PostConstruct; +import java.util.List; + +/** + *

      + * JsonMessageHandler 初始化器 + *

      + * 将所有的 jsonMessageHandler 收集到 JsonMessageHandlerHolder 中 + * + * @author Hccake + */ +@RequiredArgsConstructor +public class JsonMessageHandlerInitializer { + + private final List> jsonMessageHandlerList; + + @SuppressWarnings("unchecked") + @PostConstruct + public void initJsonMessageHandlerHolder() { + for (JsonMessageHandler jsonMessageHandler : jsonMessageHandlerList) { + JsonMessageHandlerHolder.addHandler((JsonMessageHandler) jsonMessageHandler); + } + } + +} \ No newline at end of file diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/message/JsonWebSocketMessage.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/message/JsonWebSocketMessage.java new file mode 100644 index 0000000..51ee647 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/message/JsonWebSocketMessage.java @@ -0,0 +1,31 @@ +package com.baiye.message; + +/** + *

      + * BallCat 自定义的 Json 类型的消息 + *

      + * + *
        + *
      • 要求消息内容必须是一个 Json 对象 + *
      • Json 对象中必须有一个属性 type + *
          + * + * @author Hccake 2021/1/4 + * @version 1.0 + */ +@SuppressWarnings("AlibabaAbstractClassShouldStartWithAbstractNaming") +public abstract class JsonWebSocketMessage { + + public static final String TYPE_FIELD = "type"; + + private final String type; + + protected JsonWebSocketMessage(String type) { + this.type = type; + } + + public String getType() { + return type; + } + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/message/PingJsonWebSocketMessage.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/message/PingJsonWebSocketMessage.java new file mode 100644 index 0000000..8d042d4 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/message/PingJsonWebSocketMessage.java @@ -0,0 +1,13 @@ +package com.baiye.message; + +/** + * @author Hccake 2021/1/4 + * @version 1.0 + */ +public class PingJsonWebSocketMessage extends JsonWebSocketMessage { + + public PingJsonWebSocketMessage() { + super(WebSocketMessageTypeEnum.PING.getValue()); + } + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/message/PongJsonWebSocketMessage.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/message/PongJsonWebSocketMessage.java new file mode 100644 index 0000000..32e9e12 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/message/PongJsonWebSocketMessage.java @@ -0,0 +1,13 @@ +package com.baiye.message; + +/** + * @author Hccake 2021/1/4 + * @version 1.0 + */ +public class PongJsonWebSocketMessage extends JsonWebSocketMessage { + + public PongJsonWebSocketMessage() { + super(WebSocketMessageTypeEnum.PONG.getValue()); + } + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/message/WebSocketMessageTypeEnum.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/message/WebSocketMessageTypeEnum.java new file mode 100644 index 0000000..135f3e8 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/message/WebSocketMessageTypeEnum.java @@ -0,0 +1,18 @@ +package com.baiye.message; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * @author Hccake 2021/1/4 + * @version 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum WebSocketMessageTypeEnum { + + PING("ping"), PONG("pong"); + + private final String value; + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/session/DefaultWebSocketSessionStore.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/session/DefaultWebSocketSessionStore.java new file mode 100644 index 0000000..0907d81 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/session/DefaultWebSocketSessionStore.java @@ -0,0 +1,102 @@ +package com.baiye.session; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * 默认的 WebSocketSession 存储器 + * + * @author hccake + */ +@Slf4j +public class DefaultWebSocketSessionStore implements WebSocketSessionStore { + + private final SessionKeyGenerator sessionKeyGenerator; + + private final ConcurrentHashMap> sessionKeyToWsSessions = new ConcurrentHashMap<>(); + + public DefaultWebSocketSessionStore(SessionKeyGenerator sessionKeyGenerator) { + this.sessionKeyGenerator = sessionKeyGenerator; + } + + /** + * 添加一个 wsSession + * @param wsSession 待添加的 WebSocketSession + */ + @Override + public void addSession(WebSocketSession wsSession) { + Object sessionKey = sessionKeyGenerator.sessionKey(wsSession); + Map sessions = this.sessionKeyToWsSessions.get(sessionKey); + if (sessions == null) { + sessions = new ConcurrentHashMap<>(); + this.sessionKeyToWsSessions.putIfAbsent(sessionKey, sessions); + sessions = this.sessionKeyToWsSessions.get(sessionKey); + } + sessions.put(wsSession.getId(), wsSession); + } + + /** + * 删除一个 session + * @param session WebSocketSession + */ + @Override + public void removeSession(WebSocketSession session) { + Object sessionKey = sessionKeyGenerator.sessionKey(session); + String wsSessionId = session.getId(); + + Map sessions = this.sessionKeyToWsSessions.get(sessionKey); + if (sessions != null) { + boolean result = sessions.remove(wsSessionId) != null; + if (log.isDebugEnabled()) { + log.debug("Removal of " + wsSessionId + " was " + result); + } + if (sessions.isEmpty()) { + this.sessionKeyToWsSessions.remove(sessionKey); + if (log.isDebugEnabled()) { + log.debug("Removed the corresponding HTTP Session for " + wsSessionId + + " since it contained no WebSocket mappings"); + } + } + } + } + + /** + * 获取当前所有在线的 session + * @return Collection session集合 + */ + @Override + public Collection getSessions() { + return sessionKeyToWsSessions.values().stream().flatMap(x -> x.values().stream()).collect(Collectors.toList()); + } + + /** + * 根据指定的 sessionKey 获取对应的 wsSessions + * @param sessionKey wsSession 标识 + * @return Collection websocket session集合 + */ + @Override + public Collection getSessions(Object sessionKey) { + Map sessions = this.sessionKeyToWsSessions.get(sessionKey); + if (sessions == null) { + log.warn("根据指定的sessionKey: {} 获取对应的wsSessions为空!", sessionKey); + return Collections.emptyList(); + } + return sessions.values(); + } + + /** + * 获取所有的 sessionKeys + * @return sessionKey 集合 + */ + @Override + public Collection getSessionKeys() { + return sessionKeyToWsSessions.keySet(); + } + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/session/MapSessionWebSocketHandlerDecorator.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/session/MapSessionWebSocketHandlerDecorator.java new file mode 100644 index 0000000..bc1908c --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/session/MapSessionWebSocketHandlerDecorator.java @@ -0,0 +1,57 @@ +package com.baiye.session; + +import com.baiye.handler.ConcurrentWebSocketSessionOptions; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator; +import org.springframework.web.socket.handler.WebSocketHandlerDecorator; + +/** + * WebSocketHandler 装饰器,该装饰器主要用于在开启和关闭连接时,进行session的映射存储与释放 + * + * @author Hccake 2020/12/31 + * @version 1.0 + */ +public class MapSessionWebSocketHandlerDecorator extends WebSocketHandlerDecorator { + + private final WebSocketSessionStore webSocketSessionStore; + + private final ConcurrentWebSocketSessionOptions concurrentWebSocketSessionOptions; + + public MapSessionWebSocketHandlerDecorator(WebSocketHandler delegate, WebSocketSessionStore webSocketSessionStore, + ConcurrentWebSocketSessionOptions concurrentWebSocketSessionOptions) { + super(delegate); + this.webSocketSessionStore = webSocketSessionStore; + this.concurrentWebSocketSessionOptions = concurrentWebSocketSessionOptions; + } + + /** + * websocket 连接时执行的动作 + * @param wsSession websocket session 对象 + * @throws Exception 异常对象 + */ + @Override + public void afterConnectionEstablished(WebSocketSession wsSession) throws Exception { + // 包装一层,防止并发发送出现问题 + if (Boolean.TRUE.equals(concurrentWebSocketSessionOptions.isEnable())) { + wsSession = new ConcurrentWebSocketSessionDecorator(wsSession, + concurrentWebSocketSessionOptions.getSendTimeLimit(), + concurrentWebSocketSessionOptions.getBufferSizeLimit(), + concurrentWebSocketSessionOptions.getOverflowStrategy()); + } + webSocketSessionStore.addSession(wsSession); + } + + /** + * websocket 关闭连接时执行的动作 + * @param wsSession websocket session 对象 + * @param closeStatus 关闭状态对象 + * @throws Exception 异常对象 + */ + @Override + public void afterConnectionClosed(WebSocketSession wsSession, CloseStatus closeStatus) throws Exception { + webSocketSessionStore.removeSession(wsSession); + } + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/session/SessionKeyGenerator.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/session/SessionKeyGenerator.java new file mode 100644 index 0000000..1ac2698 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/session/SessionKeyGenerator.java @@ -0,0 +1,20 @@ +package com.baiye.session; + +import org.springframework.web.socket.WebSocketSession; + +/** + * WebSocketSession 唯一标识生成器 + * + * @author Hccake 2021/1/5 + * @version 1.0 + */ +public interface SessionKeyGenerator { + + /** + * 获取当前session的唯一标识 + * @param webSocketSession 当前session + * @return session唯一标识 + */ + Object sessionKey(WebSocketSession webSocketSession); + +} diff --git a/marketing-scrm-common/common-websocket/src/main/java/com/baiye/session/WebSocketSessionStore.java b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/session/WebSocketSessionStore.java new file mode 100644 index 0000000..7a58381 --- /dev/null +++ b/marketing-scrm-common/common-websocket/src/main/java/com/baiye/session/WebSocketSessionStore.java @@ -0,0 +1,43 @@ +package com.baiye.session; + +import org.springframework.web.socket.WebSocketSession; + +import java.util.Collection; + +/** + * @author hccake + */ +public interface WebSocketSessionStore { + + /** + * 添加一个 session + * @param session 待添加的 WebSocketSession + */ + void addSession(WebSocketSession session); + + /** + * 删除一个 session + * @param session WebSocketSession + */ + void removeSession(WebSocketSession session); + + /** + * 获取当前所有在线的 wsSessions + * @return Collection websocket session集合 + */ + Collection getSessions(); + + /** + * 根据指定的 sessionKey 获取对应的 wsSessions + * @param sessionKey wsSession 标识 + * @return Collection websocket session集合 + */ + Collection getSessions(Object sessionKey); + + /** + * 获取所有的 sessionKeys + * @return sessionKey 集合 + */ + Collection getSessionKeys(); + +} diff --git a/marketing-scrm-common/pom.xml b/marketing-scrm-common/pom.xml new file mode 100644 index 0000000..dab219d --- /dev/null +++ b/marketing-scrm-common/pom.xml @@ -0,0 +1,48 @@ + + + + com.baiye + marketing-scrm + ${revision} + + 4.0.0 + marketing-scrm-common + pom + + + common-core + common-desensitize + common-idempotent + common-log + common-model + common-redis + common-util + common-websocket + + + + + org.projectlombok + lombok + + + + + install + + + org.apache.maven.plugins + maven-clean-plugin + 3.1.0 + + + clean + none + + + + + + + diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/pom.xml b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/pom.xml new file mode 100644 index 0000000..9f96d2b --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/pom.xml @@ -0,0 +1,56 @@ + + + + marketing-scrm-extends + com.baiye + ${revision} + + 4.0.0 + marketing-scrm-extend-mybatis-plus + + + + + cn.hutool + hutool-core + + + + com.baomidou + mybatis-plus-core + + + com.baomidou + mybatis-plus-extension + + + com.baiye + common-core + ${revision} + + + com.baiye + common-model + ${revision} + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework + spring-tx + + + org.projectlombok + lombok + + + diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/alias/TableAlias.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/alias/TableAlias.java new file mode 100644 index 0000000..6d879d1 --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/alias/TableAlias.java @@ -0,0 +1,25 @@ +package com.baiye.extend.mybatis.plus.alias; + +import com.baiye.extend.mybatis.plus.conditions.query.LambdaAliasQueryWrapperX; +import java.lang.annotation.*; + +/** + * 表别名注解,注解在 entity 上,便于构建带别名的查询条件或者查询列 + * + * @see LambdaAliasQueryWrapperX + * @see TableAliasHelper + * @author Hccake 2021/1/14 + * @version 1.0 + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TableAlias { + + /** + * 当前实体对应的表别名 + * @return String 表别名 + */ + String value(); + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/alias/TableAliasHelper.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/alias/TableAliasHelper.java new file mode 100644 index 0000000..e733e2d --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/alias/TableAliasHelper.java @@ -0,0 +1,79 @@ +package com.baiye.extend.mybatis.plus.alias; + +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import org.springframework.core.annotation.AnnotationUtils; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 表别名辅助类 + * + * @author Hccake 2021/1/14 + * @version 1.0 + */ +public final class TableAliasHelper { + + private TableAliasHelper() { + } + + private static final String COMMA = ","; + + private static final String DOT = "."; + + /** + * 存储类对应的表别名 + */ + private static final Map, String> TABLE_ALIAS_CACHE = new ConcurrentHashMap<>(); + + /** + * 储存类对应的带别名的查询字段 + */ + private static final Map, String> TABLE_ALIAS_SELECT_COLUMNS_CACHE = new ConcurrentHashMap<>(); + + /** + * 带别名的查询字段sql + * @param clazz 实体类class + * @return sql片段 + */ + public static String tableAliasSelectSql(Class clazz) { + String tableAliasSelectSql = TABLE_ALIAS_SELECT_COLUMNS_CACHE.get(clazz); + if (tableAliasSelectSql == null) { + String tableAlias = tableAlias(clazz); + + TableInfo tableInfo = TableInfoHelper.getTableInfo(clazz); + String allSqlSelect = tableInfo.getAllSqlSelect(); + String[] split = allSqlSelect.split(COMMA); + StringBuilder stringBuilder = new StringBuilder(); + for (String column : split) { + stringBuilder.append(tableAlias).append(DOT).append(column).append(COMMA); + } + stringBuilder.deleteCharAt(stringBuilder.length() - 1); + tableAliasSelectSql = stringBuilder.toString(); + + TABLE_ALIAS_SELECT_COLUMNS_CACHE.put(clazz, tableAliasSelectSql); + } + return tableAliasSelectSql; + } + + /** + * 获取实体类对应别名 + * @param clazz 实体类 + * @return 表别名 + */ + public static String tableAlias(Class clazz) { + String tableAlias = TABLE_ALIAS_CACHE.get(clazz); + if (tableAlias == null) { + TableAlias annotation = AnnotationUtils.findAnnotation(clazz, TableAlias.class); + if (annotation == null) { + throw new TableAliasNotFoundException( + "[tableAliasSelectSql] No TableAlias annotations found in class: " + clazz); + } + tableAlias = annotation.value(); + TABLE_ALIAS_CACHE.put(clazz, tableAlias); + } + return tableAlias; + } + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/alias/TableAliasNotFoundException.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/alias/TableAliasNotFoundException.java new file mode 100644 index 0000000..c5cecd6 --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/alias/TableAliasNotFoundException.java @@ -0,0 +1,26 @@ +package com.baiye.extend.mybatis.plus.alias; + +/** + * TableAlias 注解没有找到时抛出的异常 + * + * @see TableAlias + * @author hccake + */ +public class TableAliasNotFoundException extends RuntimeException { + + public TableAliasNotFoundException() { + } + + public TableAliasNotFoundException(String message) { + super(message); + } + + public TableAliasNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public TableAliasNotFoundException(Throwable cause) { + super(cause); + } + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/conditions/query/ColumnFunction.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/conditions/query/ColumnFunction.java new file mode 100644 index 0000000..687b483 --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/conditions/query/ColumnFunction.java @@ -0,0 +1,22 @@ +package com.baiye.extend.mybatis.plus.conditions.query; + +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; + +/** + * 连表查询时,从其他表获取的查询条件 + * + * @author hccake + */ +@FunctionalInterface +public interface ColumnFunction extends SFunction { + + /** + * 快捷的创建一个 ColumnFunction + * @param columnString 实际的 column + * @return ColumnFunction + */ + static ColumnFunction create(String columnString) { + return o -> columnString; + } + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/conditions/query/LambdaAliasQueryWrapperX.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/conditions/query/LambdaAliasQueryWrapperX.java new file mode 100644 index 0000000..dd28cd3 --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/conditions/query/LambdaAliasQueryWrapperX.java @@ -0,0 +1,91 @@ +package com.baiye.extend.mybatis.plus.conditions.query; + +import com.baiye.extend.mybatis.plus.alias.TableAlias; +import com.baiye.extend.mybatis.plus.alias.TableAliasHelper; +import com.baomidou.mybatisplus.core.conditions.SharedString; +import com.baomidou.mybatisplus.core.conditions.segments.MergeSegments; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 生成可携带表别名的查询条件 当前实体必须被配置表列名注解 + * + * @author Hccake 2021/1/14 + * @version 1.0 + * @see TableAlias + */ +public class LambdaAliasQueryWrapperX extends LambdaQueryWrapperX { + + private final String tableAlias; + + /** + * 带别名的查询列 sql 片段,默认为null,第一次使用时加载
          + * eg. t.id,t.name + */ + private String allAliasSqlSelect = null; + + public LambdaAliasQueryWrapperX(T entity) { + super(entity); + this.tableAlias = TableAliasHelper.tableAlias(getEntityClass()); + } + + public LambdaAliasQueryWrapperX(Class entityClass) { + super(entityClass); + this.tableAlias = TableAliasHelper.tableAlias(getEntityClass()); + } + + /** + * 不建议直接 new 该实例,使用 Wrappers.lambdaQuery(...) + */ + LambdaAliasQueryWrapperX(T entity, Class entityClass, SharedString sqlSelect, AtomicInteger paramNameSeq, + Map paramNameValuePairs, MergeSegments mergeSegments, SharedString lastSql, + SharedString sqlComment, SharedString sqlFirst) { + super(entity, entityClass, sqlSelect, paramNameSeq, paramNameValuePairs, mergeSegments, lastSql, sqlComment, + sqlFirst); + this.tableAlias = TableAliasHelper.tableAlias(getEntityClass()); + } + + /** + * 获取查询带别名的查询字段 TODO 暂时没有想到好的方法进行查询字段注入 本来的想法是 自定义注入 SqlFragment 但是目前 mybatis-plus 的 + * TableInfo 解析在 xml 解析之后进行,导致 include 标签被提前替换, 先在 wrapper 中做简单处理吧 + * @return String allAliasSqlSelect + */ + public String getAllAliasSqlSelect() { + if (allAliasSqlSelect == null) { + allAliasSqlSelect = TableAliasHelper.tableAliasSelectSql(getEntityClass()); + } + return allAliasSqlSelect; + } + + /** + * 用于生成嵌套 sql + *

          + * 故 sqlSelect 不向下传递 + *

          + */ + @Override + protected LambdaAliasQueryWrapperX instance() { + return new LambdaAliasQueryWrapperX<>(getEntity(), getEntityClass(), null, paramNameSeq, paramNameValuePairs, + new MergeSegments(), SharedString.emptyString(), SharedString.emptyString(), + SharedString.emptyString()); + } + + /** + * 查询条件构造时添加上表别名 + * @param column 字段Lambda + * @return 表别名.字段名,如:t.id + */ + @Override + protected String columnToString(SFunction column) { + if (column instanceof ColumnFunction) { + @SuppressWarnings("unchecked") + ColumnFunction columnFunction = (ColumnFunction) column; + return columnFunction.apply(null); + } + String columnName = super.columnToString(column, true); + return tableAlias == null ? columnName : tableAlias + "." + columnName; + } + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/conditions/query/LambdaQueryWrapperX.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/conditions/query/LambdaQueryWrapperX.java new file mode 100644 index 0000000..e6dd104 --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/conditions/query/LambdaQueryWrapperX.java @@ -0,0 +1,234 @@ +package com.baiye.extend.mybatis.plus.conditions.query; + +import com.baomidou.mybatisplus.core.conditions.AbstractLambdaWrapper; +import com.baomidou.mybatisplus.core.conditions.SharedString; +import com.baomidou.mybatisplus.core.conditions.query.Query; +import com.baomidou.mybatisplus.core.conditions.segments.MergeSegments; +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; +import com.baomidou.mybatisplus.core.toolkit.Assert; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +/** + * 增加了一些简单条件的 IfPresent 条件 支持,Collection String Object 等等判断是否为空,或者是否为null + * + * @author Hccake 2021/1/14 + * @version 1.0 + */ +public class LambdaQueryWrapperX extends AbstractLambdaWrapper> + implements Query, T, SFunction> { + + /** + * 查询字段 + */ + private SharedString sqlSelect = new SharedString(); + + /** + * 不建议直接 new 该实例,使用 WrappersX.lambdaQueryX(entity) + */ + public LambdaQueryWrapperX() { + this((T) null); + } + + /** + * 不建议直接 new 该实例,使用 WrappersX.lambdaQueryX(entity) + */ + public LambdaQueryWrapperX(T entity) { + super.setEntity(entity); + super.initNeed(); + } + + /** + * 不建议直接 new 该实例,使用 Wrappers.lambdaQuery(entity) + */ + public LambdaQueryWrapperX(Class entityClass) { + super.setEntityClass(entityClass); + super.initNeed(); + } + + /** + * 不建议直接 new 该实例,使用 Wrappers.lambdaQuery(...) + */ + LambdaQueryWrapperX(T entity, Class entityClass, SharedString sqlSelect, AtomicInteger paramNameSeq, + Map paramNameValuePairs, MergeSegments mergeSegments, SharedString lastSql, + SharedString sqlComment, SharedString sqlFirst) { + super.setEntity(entity); + super.setEntityClass(entityClass); + this.paramNameSeq = paramNameSeq; + this.paramNameValuePairs = paramNameValuePairs; + this.expression = mergeSegments; + this.sqlSelect = sqlSelect; + this.lastSql = lastSql; + this.sqlComment = sqlComment; + this.sqlFirst = sqlFirst; + } + + /** + * SELECT 部分 SQL 设置 + * @param columns 查询字段 + */ + @SafeVarargs + @Override + public final LambdaQueryWrapperX select(SFunction... columns) { + if (ArrayUtils.isNotEmpty(columns)) { + this.sqlSelect.setStringValue(columnsToString(false, columns)); + } + return typedThis; + } + + /** + * 过滤查询的字段信息(主键除外!) + *

          + * 例1: 只要 java 字段名以 "test" 开头的 -> select(i -> i.getProperty().startsWith("test")) + *

          + *

          + * 例2: 只要 java 字段属性是 CharSequence 类型的 -> select(TableFieldInfo::isCharSequence) + *

          + *

          + * 例3: 只要 java 字段没有填充策略的 -> select(i -> i.getFieldFill() == FieldFill.DEFAULT) + *

          + *

          + * 例4: 要全部字段 -> select(i -> true) + *

          + *

          + * 例5: 只要主键字段 -> select(i -> false) + *

          + * @param predicate 过滤方式 + * @return this + */ + @Override + public LambdaQueryWrapperX select(Class entityClass, Predicate predicate) { + if (entityClass == null) { + entityClass = getEntityClass(); + } + else { + setEntityClass(entityClass); + } + Assert.notNull(entityClass, "entityClass can not be null"); + this.sqlSelect.setStringValue(TableInfoHelper.getTableInfo(entityClass).chooseSelect(predicate)); + return typedThis; + } + + @Override + public String getSqlSelect() { + return sqlSelect.getStringValue(); + } + + /** + * 用于生成嵌套 sql + *

          + * 故 sqlSelect 不向下传递 + *

          + */ + @Override + protected LambdaQueryWrapperX instance() { + return new LambdaQueryWrapperX<>(getEntity(), getEntityClass(), null, paramNameSeq, paramNameValuePairs, + new MergeSegments(), SharedString.emptyString(), SharedString.emptyString(), + SharedString.emptyString()); + } + + @Override + public void clear() { + super.clear(); + sqlSelect.toNull(); + } + + // ======= 分界线,以上 copy 自 mybatis-plus 源码 ===== + + /** + * 当前条件只是否非null,且不为空 + * @param obj 值 + * @return boolean 不为空返回true + */ + private boolean isPresent(Object obj) { + if (obj == null) { + return false; + } + + if (obj instanceof Optional) { + return ((Optional) obj).isPresent(); + } + if (obj instanceof CharSequence) { + // 字符串比较特殊,如果是空字符串也不行 + return StringUtils.hasText((CharSequence) obj); + } + if (obj instanceof Collection) { + return !((Collection) obj).isEmpty(); + } + if (obj.getClass().isArray()) { + return Array.getLength(obj) != 0; + } + if (obj instanceof Map) { + return !((Map) obj).isEmpty(); + } + + // else + return true; + } + + public LambdaQueryWrapperX eqIfPresent(SFunction column, Object val) { + return super.eq(isPresent(val), column, val); + } + + public LambdaQueryWrapperX neIfPresent(SFunction column, Object val) { + return super.ne(isPresent(val), column, val); + } + + public LambdaQueryWrapperX gtIfPresent(SFunction column, Object val) { + return super.gt(isPresent(val), column, val); + } + + public LambdaQueryWrapperX geIfPresent(SFunction column, Object val) { + return super.ge(isPresent(val), column, val); + } + + public LambdaQueryWrapperX ltIfPresent(SFunction column, Object val) { + return super.lt(isPresent(val), column, val); + } + + public LambdaQueryWrapperX leIfPresent(SFunction column, Object val) { + return super.le(isPresent(val), column, val); + } + + public LambdaQueryWrapperX likeIfPresent(SFunction column, Object val) { + return super.like(isPresent(val), column, val); + } + + public LambdaQueryWrapperX notLikeIfPresent(SFunction column, Object val) { + return super.notLike(isPresent(val), column, val); + } + + public LambdaQueryWrapperX likeLeftIfPresent(SFunction column, Object val) { + return super.likeLeft(isPresent(val), column, val); + } + + public LambdaQueryWrapperX likeRightIfPresent(SFunction column, Object val) { + return super.likeRight(isPresent(val), column, val); + } + + public LambdaQueryWrapperX inIfPresent(SFunction column, Object... values) { + return super.in(isPresent(values), column, values); + } + + public LambdaQueryWrapperX inIfPresent(SFunction column, Collection values) { + return super.in(isPresent(values), column, values); + } + + public LambdaQueryWrapperX notInIfPresent(SFunction column, Object... values) { + return super.notIn(isPresent(values), column, values); + } + + public LambdaQueryWrapperX notInIfPresent(SFunction column, Collection values) { + return super.notIn(isPresent(values), column, values); + } + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/converter/JsonStringArrayTypeHandler.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/converter/JsonStringArrayTypeHandler.java new file mode 100644 index 0000000..cc07f06 --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/converter/JsonStringArrayTypeHandler.java @@ -0,0 +1,62 @@ +package com.baiye.extend.mybatis.plus.converter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +/** + * @author Enzo + * @date : 2023/11/2 + */ +@MappedJdbcTypes({JdbcType.VARCHAR}) +public class JsonStringArrayTypeHandler extends BaseTypeHandler> { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, List parameter, JdbcType jdbcType) throws SQLException { + ps.setString(i, toJson(parameter)); + } + + @Override + public List getNullableResult(ResultSet rs, String columnName) throws SQLException { + return this.toObject(rs.getString(columnName)); + } + + @Override + public List getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return this.toObject(rs.getString(columnIndex)); + } + + @Override + public List getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return this.toObject(cs.getString(columnIndex)); + } + + private String toJson(List params) { + try { + return MAPPER.writeValueAsString(params); + } catch (Exception e) { + e.printStackTrace(); + } + return "[]"; + } + + private List toObject(String content) { + if (content != null && !content.isEmpty()) { + try { + return MAPPER.readValue(content, List.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + return null; + } + } +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/converter/ListIntToListLongTypeHandler.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/converter/ListIntToListLongTypeHandler.java new file mode 100644 index 0000000..11f1650 --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/converter/ListIntToListLongTypeHandler.java @@ -0,0 +1,74 @@ +package com.baiye.extend.mybatis.plus.converter; + +import cn.hutool.json.JSONUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.CollectionType; +import org.apache.commons.lang3.StringUtils; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Enzo + * @date : 2024/3/14 + */ +@MappedJdbcTypes(JdbcType.VARCHAR) // 数据库中该字段存储的类型 +@MappedTypes(List.class) // 需要转换的对象 +public class ListIntToListLongTypeHandler extends BaseTypeHandler> { + + private static ObjectMapper mObjectMapper = new ObjectMapper(); + + @Override + public void setNonNullParameter(PreparedStatement preparedStatement, int i, List longs, JdbcType jdbcType) throws SQLException { + String json = JSONUtil.toJsonStr(longs); + preparedStatement.setObject(i, json); + } + + @Override + public List getNullableResult(ResultSet resultSet, String columnName) throws SQLException { + String value = resultSet.getString(columnName); + return getLongs(value); + } + + + @Override + public List getNullableResult(ResultSet resultSet, int i) throws SQLException { + String value = resultSet.getString(i); + + return getLongs(value); + } + + @Override + public List getNullableResult(CallableStatement callableStatement, int i) throws SQLException { + String value = callableStatement.getString(i); + + return getLongs(value); + } + + private List getLongs(String value) { + if (StringUtils.isNotBlank(value)) { + try { + + CollectionType type = mObjectMapper.getTypeFactory().constructCollectionType(ArrayList.class, Long.class); + List longs = mObjectMapper.readValue(value , type); + + //List longs = JsonUtil.parseArray(value, Long.class); + return longs; + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + + } + + return null; + } +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/injector/CustomSqlInjector.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/injector/CustomSqlInjector.java new file mode 100644 index 0000000..d4bf1fc --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/injector/CustomSqlInjector.java @@ -0,0 +1,27 @@ +package com.baiye.extend.mybatis.plus.injector; + +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +/** + * 默认的注入器,提供属性来注入自定义方法 + * + * @author lingting 2020/5/27 11:46 + */ +@RequiredArgsConstructor +public class CustomSqlInjector extends DefaultSqlInjector { + + private final List methods; + + @Override + public List getMethodList(Class mapperClass, TableInfo tableInfo) { + List list = super.getMethodList(mapperClass, tableInfo); + list.addAll(this.methods); + return list; + } + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/mapper/ExtendMapper.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/mapper/ExtendMapper.java new file mode 100644 index 0000000..f90eade --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/mapper/ExtendMapper.java @@ -0,0 +1,36 @@ +package com.baiye.extend.mybatis.plus.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn; +import com.baiye.domain.PageParam; +import com.baiye.extend.mybatis.plus.toolkit.PageUtil; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; + +/** + * 所有的 Mapper接口 都需要继承当前接口 如果想自己定义其他的全局方法, 您的全局 BaseMapper 需要继承当前接口 + * + * @author lingting 2020/5/27 11:39 + */ +public interface ExtendMapper extends BaseMapper { + + /** + * 根据 PageParam 生成一个 IPage 实例 + * @param pageParam 分页参数 + * @param 返回的 Record 对象 + * @return IPage + */ + default IPage prodPage(PageParam pageParam) { + return PageUtil.prodPage(pageParam); + } + + /** + * 批量插入数据 实现类 {@link InsertBatchSomeColumn} + * @param list 数据列表 + * @return int 改动行 + */ + int insertBatchSomeColumn(@Param("collection") Collection list); + +} \ No newline at end of file diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/BaseInsertBatch.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/BaseInsertBatch.java new file mode 100644 index 0000000..71547af --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/BaseInsertBatch.java @@ -0,0 +1,98 @@ +package com.baiye.extend.mybatis.plus.methods; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.executor.keygen.NoKeyGenerator; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlSource; + +/** + * 所有插入自定义方法的父类 + * + * @author lingting 2020/5/27 15:14 + */ +public abstract class BaseInsertBatch extends AbstractMethod { + + protected BaseInsertBatch(String methodName) { + super(methodName); + } + + @Override + public MappedStatement injectMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo) { + SqlSource sqlSource = languageDriver.createSqlSource(configuration, String.format(getSql(), + tableInfo.getTableName(), prepareFieldSql(tableInfo), prepareValuesSqlForMysqlBatch(tableInfo)), + modelClass); + + // === mybatis 主键逻辑处理:主键生成策略,以及主键回填======= + KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE; + String keyColumn = null; + String keyProperty = null; + // 如果需要回填主键 + if (backFillKey() && tableInfo.getKeyProperty() != null && !"".equals(tableInfo.getKeyProperty())) { + // 表包含主键处理逻辑,如果不包含主键当普通字段处理 + if (tableInfo.getIdType() == IdType.AUTO) { + /* 自增主键 */ + keyGenerator = Jdbc3KeyGenerator.INSTANCE; + keyProperty = getKeyProperty(tableInfo); + keyColumn = tableInfo.getKeyColumn(); + } + else { + if (null != tableInfo.getKeySequence()) { + keyGenerator = TableInfoHelper.genKeyGenerator(this.methodName, tableInfo, builderAssistant); + keyProperty = getKeyProperty(tableInfo); + keyColumn = tableInfo.getKeyColumn(); + } + } + } + + return this.addInsertMappedStatement(mapperClass, modelClass, this.methodName, sqlSource, keyGenerator, + keyProperty, keyColumn); + } + + private String getKeyProperty(TableInfo tableInfo) { + return "collection." + tableInfo.getKeyProperty(); + } + + /** + * 是否回填主键 + */ + public boolean backFillKey() { + return false; + } + + protected String prepareFieldSql(TableInfo tableInfo) { + StringBuilder fieldSql = new StringBuilder(); + fieldSql.append(tableInfo.getKeyColumn()).append(","); + tableInfo.getFieldList().forEach(x -> fieldSql.append(x.getColumn()).append(",")); + fieldSql.delete(fieldSql.length() - 1, fieldSql.length()); + fieldSql.insert(0, "("); + fieldSql.append(")"); + return fieldSql.toString(); + } + + /** + * 获取注册的脚本 + * @return java.lang.String + */ + protected abstract String getSql(); + + protected String prepareValuesSqlForMysqlBatch(TableInfo tableInfo) { + return prepareValuesBuildSqlForMysqlBatch(tableInfo).toString(); + } + + protected StringBuilder prepareValuesBuildSqlForMysqlBatch(TableInfo tableInfo) { + final StringBuilder valueSql = new StringBuilder(); + valueSql.append( + ""); + valueSql.append("#{item.").append(tableInfo.getKeyProperty()).append("},"); + tableInfo.getFieldList().forEach(field -> valueSql.append("#{item.").append(field.getProperty()).append("},")); + valueSql.delete(valueSql.length() - 1, valueSql.length()); + valueSql.append(""); + return valueSql; + } + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/InsertBatchSomeColumnByCollection.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/InsertBatchSomeColumnByCollection.java new file mode 100644 index 0000000..3a990b8 --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/InsertBatchSomeColumnByCollection.java @@ -0,0 +1,92 @@ +package com.baiye.extend.mybatis.plus.methods; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.core.enums.SqlMethod; +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils; +import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.executor.keygen.NoKeyGenerator; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlSource; + +import java.util.List; +import java.util.function.Predicate; + +/** + * 从 {@link InsertBatchSomeColumn} 复制 + * + * @author lingting 2021/2/23 15:32 + */ +public class InsertBatchSomeColumnByCollection extends AbstractMethod { + + private static final String DEFAULT_METHOD_NAME = "insertBatchSomeColumn"; + + public InsertBatchSomeColumnByCollection() { + super(DEFAULT_METHOD_NAME); + } + + public InsertBatchSomeColumnByCollection(Predicate predicate) { + this(DEFAULT_METHOD_NAME, predicate); + } + + /** + * 自定义 mapper 方法名 + */ + public InsertBatchSomeColumnByCollection(String methodName, Predicate predicate) { + super(methodName); + this.predicate = predicate; + } + + /** + * 字段筛选条件 + */ + @Setter + @Accessors(chain = true) + private Predicate predicate; + + @Override + public MappedStatement injectMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo) { + KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE; + SqlMethod sqlMethod = SqlMethod.INSERT_ONE; + List fieldList = tableInfo.getFieldList(); + String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(true, false) + + this.filterTableFieldInfo(fieldList, predicate, TableFieldInfo::getInsertSqlColumn, EMPTY); + String columnScript = LEFT_BRACKET + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + RIGHT_BRACKET; + String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(true, ENTITY_DOT, false) + + this.filterTableFieldInfo(fieldList, predicate, i -> i.getInsertSqlProperty(ENTITY_DOT), EMPTY); + insertSqlProperty = LEFT_BRACKET + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + + RIGHT_BRACKET; + // 从 list 改为 collection. 允许传入除 list外的参数类型 + String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "collection", null, ENTITY, COMMA); + String keyProperty = null; + String keyColumn = null; + // 表包含主键处理逻辑,如果不包含主键当普通字段处理 + if (tableInfo.havePK()) { + if (tableInfo.getIdType() == IdType.AUTO) { + /* 自增主键 */ + keyGenerator = Jdbc3KeyGenerator.INSTANCE; + keyProperty = tableInfo.getKeyProperty(); + keyColumn = tableInfo.getKeyColumn(); + } + else { + if (null != tableInfo.getKeySequence()) { + keyGenerator = TableInfoHelper.genKeyGenerator(this.methodName, tableInfo, builderAssistant); + keyProperty = tableInfo.getKeyProperty(); + keyColumn = tableInfo.getKeyColumn(); + } + } + } + String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript); + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass); + return this.addInsertMappedStatement(mapperClass, modelClass, this.methodName, sqlSource, keyGenerator, + keyProperty, keyColumn); + } + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/InsertIgnoreByBatch.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/InsertIgnoreByBatch.java new file mode 100644 index 0000000..7b453af --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/InsertIgnoreByBatch.java @@ -0,0 +1,21 @@ +package com.baiye.extend.mybatis.plus.methods; + +/** + * @author lingting 2020/5/27 11:47 + */ +public class InsertIgnoreByBatch extends BaseInsertBatch { + + protected InsertIgnoreByBatch() { + super("insertIgnoreByBatch"); + } + + protected InsertIgnoreByBatch(String methodName) { + super(methodName); + } + + @Override + protected String getSql() { + return ""; + } + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/InsertOrUpdateByBatch.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/InsertOrUpdateByBatch.java new file mode 100644 index 0000000..a9966f0 --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/InsertOrUpdateByBatch.java @@ -0,0 +1,67 @@ +package com.baiye.extend.mybatis.plus.methods; + +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.function.Predicate; + +/** + * @author lingting 2020/5/27 11:47 + */ +public class InsertOrUpdateByBatch extends BaseInsertBatch { + + protected InsertOrUpdateByBatch() { + super("insertOrUpdateByBatch"); + } + + protected InsertOrUpdateByBatch(String methodName) { + super(methodName); + } + + /** + * 字段筛选条件 + */ + @Setter + @Accessors(chain = true) + private Predicate predicate; + + @Override + protected String getSql() { + return ""; + } + + @Override + protected String prepareValuesSqlForMysqlBatch(TableInfo tableInfo) { + StringBuilder sql = super.prepareValuesBuildSqlForMysqlBatch(tableInfo); + sql.append(" ON DUPLICATE KEY UPDATE "); + StringBuilder ignore = new StringBuilder(); + + tableInfo.getFieldList().forEach(field -> { + // 默认忽略逻辑删除字段 + if (!field.isLogicDelete()) { + // 默认忽略字段 + if (!predicate.test(field)) { + sql.append(field.getColumn()).append("=").append("VALUES(").append(field.getColumn()).append("),"); + } + else { + ignore.append(",") + .append(field.getColumn()) + .append("=") + .append("VALUES(") + .append(field.getColumn()) + .append(")"); + } + } + }); + + // 删除最后一个多余的逗号 + sql.delete(sql.length() - 1, sql.length()); + + // 配置不忽略全局配置字段时的sql部分 + sql.append("").append(ignore).append(""); + return sql.toString(); + } + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/InsertOrUpdateFieldByBatch.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/InsertOrUpdateFieldByBatch.java new file mode 100644 index 0000000..dc86849 --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/methods/InsertOrUpdateFieldByBatch.java @@ -0,0 +1,45 @@ +package com.baiye.extend.mybatis.plus.methods; + +import com.baomidou.mybatisplus.core.metadata.TableInfo; + +/** + * @author lingting 2020/5/27 11:47 + */ +public class InsertOrUpdateFieldByBatch extends BaseInsertBatch { + + private static final String SQL = ""; + + protected InsertOrUpdateFieldByBatch() { + super("insertOrUpdateFieldByBatch"); + } + + protected InsertOrUpdateFieldByBatch(String methodName) { + super(methodName); + } + + @Override + protected String getSql() { + return SQL; + } + + @Override + protected String prepareValuesSqlForMysqlBatch(TableInfo tableInfo) { + StringBuilder sql = super.prepareValuesBuildSqlForMysqlBatch(tableInfo); + sql.append(" ON DUPLICATE KEY UPDATE ") + // 如果模式为 不忽略设置的字段 + .append("") + .append("") + .append("${item.name}=${item.val}") + .append("") + .append(""); + + // 如果模式为 忽略设置的字段 + sql.append("") + .append("") + .append("${item}=VALUES(${item})") + .append("") + .append(""); + return sql.toString(); + } + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/service/ExtendService.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/service/ExtendService.java new file mode 100644 index 0000000..7ec71d8 --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/service/ExtendService.java @@ -0,0 +1,257 @@ +package com.baiye.extend.mybatis.plus.service; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; + +/** + * 以前继承 com.baomidou.mybatisplus.extension.service.IService 的实现类,现在继承当前类 + * + * @author lingting 2020/7/21 9:58 + */ +public interface ExtendService { + + // ======= Copy From com.baomidou.mybatisplus.extension.service.IService 开始 ======= + + /** + * 默认批次提交数量 + */ + int DEFAULT_BATCH_SIZE = 1000; + + /** + * 插入一条记录(选择字段,策略插入) + * @param entity 实体对象 + */ + default boolean save(T entity) { + return SqlHelper.retBool(getBaseMapper().insert(entity)); + } + + /** + * 插入(批量) + * @param entityList 实体对象集合 + */ + @Transactional(rollbackFor = Exception.class) + default boolean saveBatch(Collection entityList) { + return saveBatch(entityList, DEFAULT_BATCH_SIZE); + } + + /** + * 插入(批量) + * @param entityList 实体对象集合 + * @param batchSize 插入批次数量 + */ + boolean saveBatch(Collection entityList, int batchSize); + + /** + * 批量修改插入 + * @param entityList 实体对象集合 + */ + @Transactional(rollbackFor = Exception.class) + default boolean saveOrUpdateBatch(Collection entityList) { + return saveOrUpdateBatch(entityList, DEFAULT_BATCH_SIZE); + } + + /** + * 批量修改插入 + * @param entityList 实体对象集合 + * @param batchSize 每次的数量 + */ + boolean saveOrUpdateBatch(Collection entityList, int batchSize); + + /** + * 根据 ID 删除 + * @param id 主键ID + */ + default boolean removeById(Serializable id) { + return SqlHelper.retBool(getBaseMapper().deleteById(id)); + } + + /** + * 根据 ID 删除 + * @param id 主键(类型必须与实体类型字段保持一致) + * @param useFill 是否启用填充(为true的情况,会将入参转换实体进行delete删除) + * @return 删除结果 + * @since 3.5.0 + */ + default boolean removeById(Serializable id, boolean useFill) { + throw new UnsupportedOperationException("不支持的方法!"); + } + + /** + * 根据实体(ID)删除 + * @param entity 实体 + * @since 3.4.4 + * @return 删除结果 + */ + default boolean removeById(T entity) { + return SqlHelper.retBool(getBaseMapper().deleteById(entity)); + } + + /** + * 删除(根据ID 批量删除) + * @param list 主键ID或实体列表 + * @return 删除结果 + */ + default boolean removeByIds(Collection list) { + if (CollectionUtils.isEmpty(list)) { + return false; + } + return SqlHelper.retBool(getBaseMapper().deleteBatchIds(list)); + } + + /** + * 批量删除 + * @param list 主键ID或实体列表 + * @param useFill 是否填充(为true的情况,会将入参转换实体进行delete删除) + * @return 删除结果 + * @since 3.5.0 + */ + @Transactional(rollbackFor = Exception.class) + default boolean removeByIds(Collection list, boolean useFill) { + if (CollectionUtils.isEmpty(list)) { + return false; + } + if (useFill) { + return removeBatchByIds(list, true); + } + return SqlHelper.retBool(getBaseMapper().deleteBatchIds(list)); + } + + /** + * 批量删除(jdbc批量提交) + * @param list 主键ID或实体列表(主键ID类型必须与实体类型字段保持一致) + * @return 删除结果 + * @since 3.5.0 + */ + @Transactional(rollbackFor = Exception.class) + default boolean removeBatchByIds(Collection list) { + return removeBatchByIds(list, DEFAULT_BATCH_SIZE); + } + + /** + * 批量删除(jdbc批量提交) + * @param list 主键ID或实体列表(主键ID类型必须与实体类型字段保持一致) + * @param useFill 是否启用填充(为true的情况,会将入参转换实体进行delete删除) + * @return 删除结果 + * @since 3.5.0 + */ + @Transactional(rollbackFor = Exception.class) + default boolean removeBatchByIds(Collection list, boolean useFill) { + return removeBatchByIds(list, DEFAULT_BATCH_SIZE, useFill); + } + + /** + * 批量删除(jdbc批量提交) + * @param list 主键ID或实体列表 + * @param batchSize 批次大小 + * @return 删除结果 + * @since 3.5.0 + */ + default boolean removeBatchByIds(Collection list, int batchSize) { + throw new UnsupportedOperationException("不支持的方法!"); + } + + /** + * 批量删除(jdbc批量提交) + * @param list 主键ID或实体列表 + * @param batchSize 批次大小 + * @param useFill 是否启用填充(为true的情况,会将入参转换实体进行delete删除) + * @return 删除结果 + * @since 3.5.0 + */ + default boolean removeBatchByIds(Collection list, int batchSize, boolean useFill) { + throw new UnsupportedOperationException("不支持的方法!"); + } + + /** + * 根据 ID 选择修改 + * @param entity 实体对象 + */ + default boolean updateById(T entity) { + return SqlHelper.retBool(getBaseMapper().updateById(entity)); + } + + /** + * 根据ID 批量更新 + * @param entityList 实体对象集合 + */ + @Transactional(rollbackFor = Exception.class) + default boolean updateBatchById(Collection entityList) { + return updateBatchById(entityList, DEFAULT_BATCH_SIZE); + } + + /** + * 根据ID 批量更新 + * @param entityList 实体对象集合 + * @param batchSize 更新批次数量 + */ + boolean updateBatchById(Collection entityList, int batchSize); + + /** + * TableId 注解存在更新记录,否插入一条记录 + * @param entity 实体对象 + */ + boolean saveOrUpdate(T entity); + + /** + * 根据 ID 查询 + * @param id 主键ID + */ + default T getById(Serializable id) { + return getBaseMapper().selectById(id); + } + + /** + * 查询(根据ID 批量查询) + * @param idList 主键ID列表 + */ + default List listByIds(Collection idList) { + return getBaseMapper().selectBatchIds(idList); + } + + /** + * 查询所有 + * + */ + default List list() { + return getBaseMapper().selectList(null); + } + + /** + * 获取对应 entity 的 BaseMapper + * @return BaseMapper + */ + BaseMapper getBaseMapper(); + + /** + * 获取 entity 的 class + * @return {@link Class} + */ + Class getEntityClass(); + + // ^^^^^^ Copy From com.baomidou.mybatisplus.extension.service.IService end ^^^^^^ + + /** + * 批量插入数据 + * @param list 数据列表 + * @return int 改动行 + */ + @Transactional(rollbackFor = Exception.class) + default boolean saveBatchSomeColumn(Collection list) { + return this.saveBatchSomeColumn(list, DEFAULT_BATCH_SIZE); + } + + /** + * 批量插入数据 + * @param list 数据列表 + * @param batchSize 批次插入数据量 + * @return int 改动行 + */ + boolean saveBatchSomeColumn(Collection list, int batchSize); + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/service/impl/ExtendServiceImpl.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/service/impl/ExtendServiceImpl.java new file mode 100644 index 0000000..dc9aa0f --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/service/impl/ExtendServiceImpl.java @@ -0,0 +1,251 @@ +package com.baiye.extend.mybatis.plus.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.service.ExtendService; +import com.baomidou.mybatisplus.core.enums.SqlMethod; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.core.toolkit.Assert; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.core.toolkit.ReflectionKit; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import org.apache.ibatis.binding.MapperMethod; +import org.apache.ibatis.logging.Log; +import org.apache.ibatis.logging.LogFactory; +import org.apache.ibatis.session.SqlSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.BiConsumer; + +/** + * 以前继承 com.baomidou.mybatisplus.extension.service.impl.ServiceImpl 的实现类,现在继承本类 + * + * @author lingting 2020/7/21 10:00 + */ +@SuppressWarnings("unchecked") +public class ExtendServiceImpl, T> implements ExtendService { + + // ======= Copy From com.baomidou.mybatisplus.extension.service.impl.ServiceImpl 开始 + // ======= + + protected Log log = LogFactory.getLog(getClass()); + + @Autowired + protected M baseMapper; + + @Override + public M getBaseMapper() { + return baseMapper; + } + + protected Class entityClass = currentModelClass(); + + @Override + public Class getEntityClass() { + return entityClass; + } + + protected Class mapperClass = currentMapperClass(); + + /** + * 判断数据库操作是否成功 + * @param result 数据库操作返回影响条数 + * @return boolean + * @deprecated 3.3.1 + */ + @Deprecated + protected boolean retBool(Integer result) { + return SqlHelper.retBool(result); + } + + protected Class currentMapperClass() { + return (Class) ReflectionKit.getSuperClassGenericType(this.getClass(), ExtendServiceImpl.class, 0); + } + + protected Class currentModelClass() { + return (Class) ReflectionKit.getSuperClassGenericType(this.getClass(), ExtendServiceImpl.class, 1); + } + + /** + * 批量插入 + * @param entityList ignore + * @param batchSize ignore + * @return ignore + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean saveBatch(Collection entityList, int batchSize) { + String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE); + return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity)); + } + + /** + * 获取mapperStatementId + * @param sqlMethod 方法名 + * @return 命名id + * @since 3.4.0 + */ + protected String getSqlStatement(SqlMethod sqlMethod) { + return SqlHelper.getSqlStatement(mapperClass, sqlMethod); + } + + /** + * TableId 注解存在更新记录,否插入一条记录 + * @param entity 实体对象 + * @return boolean + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean saveOrUpdate(T entity) { + if (null != entity) { + TableInfo tableInfo = TableInfoHelper.getTableInfo(this.entityClass); + Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!"); + String keyProperty = tableInfo.getKeyProperty(); + Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!"); + Object idVal = tableInfo.getPropertyValue(entity, tableInfo.getKeyProperty()); + return StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal)) ? save(entity) + : updateById(entity); + } + return false; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean saveOrUpdateBatch(Collection entityList, int batchSize) { + TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass); + Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!"); + String keyProperty = tableInfo.getKeyProperty(); + Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!"); + return SqlHelper.saveOrUpdateBatch(this.entityClass, this.mapperClass, this.log, entityList, batchSize, + (sqlSession, entity) -> { + Object idVal = tableInfo.getPropertyValue(entity, keyProperty); + return StringUtils.checkValNull(idVal) || CollectionUtils + .isEmpty(sqlSession.selectList(getSqlStatement(SqlMethod.SELECT_BY_ID), entity)); + }, (sqlSession, entity) -> { + MapperMethod.ParamMap param = new MapperMethod.ParamMap<>(); + param.put(Constants.ENTITY, entity); + sqlSession.update(getSqlStatement(SqlMethod.UPDATE_BY_ID), param); + }); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean updateBatchById(Collection entityList, int batchSize) { + String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID); + return executeBatch(entityList, batchSize, (sqlSession, entity) -> { + MapperMethod.ParamMap param = new MapperMethod.ParamMap<>(); + param.put(Constants.ENTITY, entity); + sqlSession.update(sqlStatement, param); + }); + } + + /** + * 执行批量操作 + * @param list 数据集合 + * @param batchSize 批量大小 + * @param consumer 执行方法 + * @param 泛型 + * @return 操作结果 + * @since 3.3.1 + */ + protected boolean executeBatch(Collection list, int batchSize, BiConsumer consumer) { + return SqlHelper.executeBatch(this.entityClass, this.log, list, batchSize, consumer); + } + + @Override + public boolean removeById(Serializable id) { + TableInfo tableInfo = TableInfoHelper.getTableInfo(getEntityClass()); + if (tableInfo.isWithLogicDelete() && tableInfo.isWithUpdateFill()) { + return removeById(id, true); + } + return SqlHelper.retBool(getBaseMapper().deleteById(id)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean removeByIds(Collection list) { + if (CollectionUtils.isEmpty(list)) { + return false; + } + TableInfo tableInfo = TableInfoHelper.getTableInfo(getEntityClass()); + if (tableInfo.isWithLogicDelete() && tableInfo.isWithUpdateFill()) { + return removeBatchByIds(list, true); + } + return SqlHelper.retBool(getBaseMapper().deleteBatchIds(list)); + } + + @Override + public boolean removeById(Serializable id, boolean useFill) { + TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass); + if (useFill && tableInfo.isWithLogicDelete()) { + if (entityClass.isAssignableFrom(id.getClass())) { + return SqlHelper.retBool(getBaseMapper().deleteById(id)); + } + T instance = tableInfo.newInstance(); + tableInfo.setPropertyValue(instance, tableInfo.getKeyProperty(), id); + return removeById(instance); + } + return SqlHelper.retBool(getBaseMapper().deleteById(id)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean removeBatchByIds(Collection list, int batchSize) { + TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass); + return removeBatchByIds(list, batchSize, tableInfo.isWithLogicDelete() && tableInfo.isWithUpdateFill()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean removeBatchByIds(Collection list, int batchSize, boolean useFill) { + String sqlStatement = getSqlStatement(SqlMethod.DELETE_BY_ID); + TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass); + return executeBatch(list, batchSize, (sqlSession, e) -> { + if (useFill && tableInfo.isWithLogicDelete()) { + if (entityClass.isAssignableFrom(e.getClass())) { + sqlSession.update(sqlStatement, e); + } + else { + T instance = tableInfo.newInstance(); + tableInfo.setPropertyValue(instance, tableInfo.getKeyProperty(), e); + sqlSession.update(sqlStatement, instance); + } + } + else { + sqlSession.update(sqlStatement, e); + } + }); + } + + // ^^^^^^ Copy From com.baomidou.mybatisplus.extension.service.impl.ServiceImpl end + // ^^^^^^ + + /** + * 批量插入数据 + * @param list 数据列表 + * @param batchSize 批次插入数据量 + * @return int 改动行 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean saveBatchSomeColumn(Collection list, int batchSize) { + if (CollUtil.isEmpty(list)) { + return false; + } + List> segmentDataList = CollectionUtil.split(list, batchSize); + for (List data : segmentDataList) { + baseMapper.insertBatchSomeColumn(data); + } + return true; + } + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/toolkit/PageUtil.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/toolkit/PageUtil.java new file mode 100644 index 0000000..d887396 --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/toolkit/PageUtil.java @@ -0,0 +1,35 @@ +package com.baiye.extend.mybatis.plus.toolkit; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baiye.domain.PageParam; + +import java.util.List; + +/** + * @author Hccake 2021/1/19 + * @version 1.0 + */ +public final class PageUtil { + + private PageUtil() { + } + + /** + * 根据 PageParam 生成一个 IPage 实例 + * @param pageParam 分页参数 + * @param 返回的 Record 对象 + * @return IPage + */ + public static IPage prodPage(PageParam pageParam) { + Page page = new Page<>(pageParam.getPage(), pageParam.getSize()); + List sorts = pageParam.getSorts(); + for (PageParam.Sort sort : sorts) { + OrderItem orderItem = sort.isAsc() ? OrderItem.asc(sort.getField()) : OrderItem.desc(sort.getField()); + page.addOrder(orderItem); + } + return page; + } + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/toolkit/WrappersX.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/toolkit/WrappersX.java new file mode 100644 index 0000000..dccc877 --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/toolkit/WrappersX.java @@ -0,0 +1,88 @@ +package com.baiye.extend.mybatis.plus.toolkit; + +import com.baiye.extend.mybatis.plus.conditions.query.LambdaQueryWrapperX; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baiye.extend.mybatis.plus.conditions.query.LambdaAliasQueryWrapperX; + +/** + * @author Hccake 2021/1/14 + * @version 1.0 + */ +public final class WrappersX { + + private WrappersX() { + } + + /** + * 获取 LambdaQueryWrapperX<T> + * @param 实体类泛型 + * @return LambdaQueryWrapperX<T> + */ + public static LambdaQueryWrapperX lambdaQueryX() { + return new LambdaQueryWrapperX<>(); + } + + /** + * 获取 LambdaQueryWrapperX<T> + * @param entity 实体类 + * @param 实体类泛型 + * @return LambdaQueryWrapperX<T> + */ + public static LambdaQueryWrapperX lambdaQueryX(T entity) { + return new LambdaQueryWrapperX<>(entity); + } + + /** + * 获取 LambdaQueryWrapperX<T> + * @param entityClass 实体类class + * @param 实体类泛型 + * @return LambdaQueryWrapperX<T> + * @since 3.3.1 + */ + public static LambdaQueryWrapperX lambdaQueryX(Class entityClass) { + return new LambdaQueryWrapperX<>(entityClass); + } + + /** + * 获取 LambdaAliasQueryWrapper<T> + * @param entity 实体类 + * @param 实体类泛型 + * @return LambdaAliasQueryWrapper<T> + */ + public static LambdaAliasQueryWrapperX lambdaAliasQueryX(T entity) { + return new LambdaAliasQueryWrapperX<>(entity); + } + + /** + * 获取 LambdaAliasQueryWrapper<T> + * @param entityClass 实体类class + * @param 实体类泛型 + * @return LambdaAliasQueryWrapper<T> + * @since 3.3.1 + */ + public static LambdaAliasQueryWrapperX lambdaAliasQueryX(Class entityClass) { + return new LambdaAliasQueryWrapperX<>(entityClass); + } + + /** + * 获取 LambdaUpdateWrapper<T> 复制 com.baomidou.mybatisplus.core.toolkit.Wrappers + * @param entity 实体类 + * @param 实体类泛型 + * @return LambdaUpdateWrapper<T> + */ + public static LambdaUpdateWrapper lambdaUpdate(T entity) { + return new LambdaUpdateWrapper<>(entity); + } + + /** + * 获取 LambdaUpdateWrapper<T> 复制 com.baomidou.mybatisplus.core.toolkit.Wrappers + * @param entityClass 实体类class + * @param 实体类泛型 + * @return LambdaUpdateWrapper<T> + * @since 3.3.1 + */ + public static LambdaUpdateWrapper lambdaUpdate(Class entityClass) { + return new LambdaUpdateWrapper<>(entityClass); + } + +} diff --git a/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/type/EnumNameTypeHandler.java b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/type/EnumNameTypeHandler.java new file mode 100644 index 0000000..334ac8c --- /dev/null +++ b/marketing-scrm-extends/marketing-scrm-extend-mybatis-plus/src/main/java/com/baiye/extend/mybatis/plus/type/EnumNameTypeHandler.java @@ -0,0 +1,92 @@ +package com.baiye.extend.mybatis.plus.type; + +import com.baomidou.mybatisplus.annotation.IEnum; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * 普通枚举类型处理. 根据 name() 返回值判断枚举值 + * + * @author lingting 2021/6/7 13:49 + */ +public class EnumNameTypeHandler> extends BaseTypeHandler { + + private final Class type; + + public EnumNameTypeHandler(Class type) { + if (type == null) { + throw new IllegalArgumentException("Type argument cannot be null"); + } + this.type = type; + } + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { + if (parameter == null) { + ps.setString(i, ""); + } + else { + Object val = getValByEnum(parameter); + if (jdbcType == null) { + ps.setString(i, val == null ? null : val.toString()); + } + else { + ps.setObject(i, val, jdbcType.TYPE_CODE); + } + } + } + + @Override + public E getNullableResult(ResultSet rs, String columnName) throws SQLException { + return getEnumByName(rs.getString(columnName)); + } + + @Override + public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return getEnumByName(rs.getString(columnIndex)); + } + + @Override + public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return getEnumByName(cs.getString(columnIndex)); + } + + boolean isIEnum(E e) { + return IEnum.class.isAssignableFrom(e.getClass()); + } + + Object getValByEnum(E e) { + // IEnum + if (isIEnum(e)) { + return ((IEnum) e).getValue(); + } + return e.name(); + } + + /** + * 根据枚举 name() 获取枚举 + * @author lingting 2021-06-07 13:50 + */ + E getEnumByName(String val) { + for (E e : type.getEnumConstants()) { + Object ev = getValByEnum(e); + if (ev == null) { + if (val == null) { + return e; + } + continue; + } + + if (val.equals(ev.toString())) { + return e; + } + } + return null; + } + +} diff --git a/marketing-scrm-extends/pom.xml b/marketing-scrm-extends/pom.xml new file mode 100644 index 0000000..4de6059 --- /dev/null +++ b/marketing-scrm-extends/pom.xml @@ -0,0 +1,20 @@ + + + + + com.baiye + marketing-scrm + ${revision} + + 4.0.0 + + marketing-scrm-extends + pom + + + marketing-scrm-extend-mybatis-plus + + + diff --git a/marketing-scrm-notify/distribute-notify-biz/pom.xml b/marketing-scrm-notify/distribute-notify-biz/pom.xml new file mode 100644 index 0000000..973f867 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/pom.xml @@ -0,0 +1,29 @@ + + + + com.baiye + marketing-scrm-notify + ${revision} + + 4.0.0 + marketing-scrm-notify-biz + + + + com.baiye + common-websocket + + + + com.baiye + distribute-notify-model + ${revision} + + + + com.baiye + system-biz + + + diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/event/AnnouncementCloseEvent.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/event/AnnouncementCloseEvent.java new file mode 100644 index 0000000..ecbbd68 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/event/AnnouncementCloseEvent.java @@ -0,0 +1,23 @@ +package com.baiye.notify.event; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +/** + * 公告关闭事件 + * + * @author Hccake 2021/1/7 + * @version 1.0 + */ +@Getter +@ToString +@AllArgsConstructor +public class AnnouncementCloseEvent { + + /** + * ID + */ + private final Long id; + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/event/NotifyPublishEvent.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/event/NotifyPublishEvent.java new file mode 100644 index 0000000..094e091 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/event/NotifyPublishEvent.java @@ -0,0 +1,26 @@ +package com.baiye.notify.event; + +import com.baiye.notify.model.domain.NotifyInfo; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +/** + * 通知发布事件 + * + * @author Hccake 2020/12/17 + * @version 1.0 + */ +@Getter +public class NotifyPublishEvent extends ApplicationEvent { + + /** + * 通知信息 + */ + private final NotifyInfo notifyInfo; + + public NotifyPublishEvent(NotifyInfo notifyInfo) { + super(notifyInfo); + this.notifyInfo = notifyInfo; + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/event/StationNotifyPushEvent.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/event/StationNotifyPushEvent.java new file mode 100644 index 0000000..ffbd36f --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/event/StationNotifyPushEvent.java @@ -0,0 +1,28 @@ +package com.baiye.notify.event; + +import com.baiye.notify.model.domain.NotifyInfo; +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +/** + * @author Hccake 2021/1/5 + * @version 1.0 + */ +@Data +@RequiredArgsConstructor +public class StationNotifyPushEvent { + + /** + * 通知信息 + */ + private final NotifyInfo notifyInfo; + + /** + * 推送用户列表 + */ + private final List userIdList; + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/event/UserAnnouncementReadEvent.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/event/UserAnnouncementReadEvent.java new file mode 100644 index 0000000..84fdd85 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/event/UserAnnouncementReadEvent.java @@ -0,0 +1,28 @@ +package com.baiye.notify.event; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +/** + * 公告关闭事件 + * + * @author Hccake 2021/1/7 + * @version 1.0 + */ +@Getter +@ToString +@AllArgsConstructor +public class UserAnnouncementReadEvent { + + /** + * ID + */ + private final Long id; + + /** + * userId + */ + private final Long userId; + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/AbstractNotifyInfoHandler.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/AbstractNotifyInfoHandler.java new file mode 100644 index 0000000..cf09276 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/AbstractNotifyInfoHandler.java @@ -0,0 +1,71 @@ +package com.baiye.notify.handler; + +import cn.hutool.core.collection.CollUtil; +import com.baiye.distribute.MessageDO; +import com.baiye.distribute.MessageDistributor; +import com.baiye.message.JsonWebSocketMessage; +import com.baiye.notify.model.domain.NotifyInfo; +import com.baiye.system.model.entity.SysUser; +import com.baiye.util.JsonUtils; +import org.springframework.beans.factory.annotation.Autowired; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 公告通知 + * + * @author huyuanzhi + * @param event消息对象 + * @param websocket发送消息对象 + */ +public abstract class AbstractNotifyInfoHandler + implements NotifyInfoHandler { + + @Autowired + private MessageDistributor messageDistributor; + + protected final Class clz; + + @SuppressWarnings("unchecked") + protected AbstractNotifyInfoHandler() { + Type superClass = getClass().getGenericSuperclass(); + ParameterizedType type = (ParameterizedType) superClass; + clz = (Class) type.getActualTypeArguments()[0]; + } + + @Override + public void handle(List userIdList, T notifyInfo) { + M message = createMessage(notifyInfo); + String msg = JsonUtils.toJson(message); + List sessionKeys = new ArrayList<>(userIdList); + persistMessage(userIdList, notifyInfo); + MessageDO messageDO = new MessageDO().setMessageText(msg) + .setSessionKeys(sessionKeys) + .setNeedBroadcast(CollUtil.isEmpty(sessionKeys)); + messageDistributor.distribute(messageDO); + } + + @Override + public Class getNotifyClass() { + return this.clz; + } + + /** + * 持久化通知 + * @param userIdList 通知用户列表 + * @param notifyInfo 消息内容 + */ + protected abstract void persistMessage(List userIdList, T notifyInfo); + + /** + * 产生推送消息 + * @param notifyInfo 消息内容 + * @return 分发消息 + */ + protected abstract M createMessage(T notifyInfo); + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/NotifyInfoDelegateHandler.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/NotifyInfoDelegateHandler.java new file mode 100644 index 0000000..24267c4 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/NotifyInfoDelegateHandler.java @@ -0,0 +1,53 @@ +package com.baiye.notify.handler; + +import com.baiye.notify.model.domain.NotifyInfo; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 消息处理代理 + * + * @param 消息类型 + * @author huyuanzhi + */ +@Slf4j +@Component +@AllArgsConstructor +public class NotifyInfoDelegateHandler { + + private final List> notifyInfoHandlers; + + private Map, NotifyInfoHandler> handlerMap; + + @PostConstruct + public void init() { + handlerMap = new HashMap<>(notifyInfoHandlers.size()); + for (NotifyInfoHandler handler : notifyInfoHandlers) { + handlerMap.put(handler.getNotifyClass(), handler); + } + } + + /** + * 代理方法 + * + * @param userIdList 发送用户列表 + * @param info 消息 + */ + public void handle(List userIdList, T info) { + Assert.notNull(info, "event message cant be null!"); + NotifyInfoHandler notifyInfoHandler = handlerMap.get(info.getClass()); + if (notifyInfoHandler == null) { + log.warn("no notifyHandler bean for class:{},please check!", info.getClass().getName()); + return; + } + notifyInfoHandler.handle(userIdList, info); + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/NotifyInfoHandler.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/NotifyInfoHandler.java new file mode 100644 index 0000000..33b9ed0 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/NotifyInfoHandler.java @@ -0,0 +1,28 @@ +package com.baiye.notify.handler; + + +import com.baiye.notify.model.domain.NotifyInfo; + +import java.util.List; + +/** + * event消息处理接口 + * + * @author huyuanzhi + */ +public interface NotifyInfoHandler { + + /** + * 处理消息 + * @param userIdList 发送用户列表 + * @param info 消息 + */ + void handle(List userIdList, T info); + + /** + * 获取消息类型 + * @return 消息类型 + */ + Class getNotifyClass(); + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/impl/AnnouncementNotifyInfoHandler.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/impl/AnnouncementNotifyInfoHandler.java new file mode 100644 index 0000000..f7b5f55 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/impl/AnnouncementNotifyInfoHandler.java @@ -0,0 +1,54 @@ +package com.baiye.notify.handler.impl; + +import com.baiye.notify.handler.AbstractNotifyInfoHandler; +import com.baiye.notify.model.domain.AnnouncementNotifyInfo; +import com.baiye.notify.model.entity.UserAnnouncement; +import com.baiye.notify.service.UserAnnouncementService; +import com.baiye.system.model.entity.SysUser; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 公告通知消息处理器 + * + * @author huyuanzhi + */ +@Component +@RequiredArgsConstructor +public class AnnouncementNotifyInfoHandler + extends AbstractNotifyInfoHandler { + + private final UserAnnouncementService userAnnouncementService; + + @Override + protected void persistMessage(List userIdList, AnnouncementNotifyInfo announcementNotifyInfo) { + List userAnnouncements = new ArrayList<>(userIdList.size()); + // 向指定用户推送 + for (Long userId : userIdList) { + UserAnnouncement userAnnouncement = userAnnouncementService.prodUserAnnouncement + (userId, announcementNotifyInfo.getId(), announcementNotifyInfo.getState(), announcementNotifyInfo.getMessageType(), + announcementNotifyInfo.getMessageKey(), announcementNotifyInfo.getTitle(), announcementNotifyInfo.getContent()); + userAnnouncements.add(userAnnouncement); + } + userAnnouncementService.saveBatch(userAnnouncements); + } + + @Override + protected AnnouncementPushMessage createMessage(AnnouncementNotifyInfo announcementNotifyInfo) { + AnnouncementPushMessage message = new AnnouncementPushMessage(); + message.setId(announcementNotifyInfo.getId()); + message.setState(announcementNotifyInfo.getState()); + message.setTitle(announcementNotifyInfo.getTitle()); + message.setContent(announcementNotifyInfo.getContent()); + message.setImmortal(announcementNotifyInfo.getImmortal()); + message.setDeadline(announcementNotifyInfo.getDeadline()); + message.setMessageKey(announcementNotifyInfo.getMessageKey()); + message.setCreateTime(announcementNotifyInfo.getCreateTime()); + message.setMessageType(announcementNotifyInfo.getMessageType()); + return message; + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/impl/AnnouncementPushMessage.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/impl/AnnouncementPushMessage.java new file mode 100644 index 0000000..ff4784b --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/handler/impl/AnnouncementPushMessage.java @@ -0,0 +1,74 @@ +package com.baiye.notify.handler.impl; + +import com.baiye.message.JsonWebSocketMessage; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +/** + * 公告发布消息 + * + * @author Hccake 2021/1/5 + * @version 1.0 + */ +@Getter +@Setter +public class AnnouncementPushMessage extends JsonWebSocketMessage { + + public AnnouncementPushMessage() { + super("announcement-push"); + } + + /** + * ID + */ + private Long id; + + /** + * 标题 + */ + private String title; + + /** + * 内容 + */ + private String content; + + /** + * 内容 + */ + private Integer state; + + + /** + * 永久有效的 + * @see com.baiye.ballcat.common.core.constant.enums.BooleanEnum + */ + private Integer immortal; + + + + + /** + * message + */ + private String messageKey; + + /** + * 消息类型 + */ + private Integer messageType; + + /** + * 截止日期 + */ + private LocalDateTime deadline; + + + /** + * 拉取时间 + */ + private LocalDateTime createTime; + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/listener/AnnouncementLoginEventListener.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/listener/AnnouncementLoginEventListener.java new file mode 100644 index 0000000..0c0f886 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/listener/AnnouncementLoginEventListener.java @@ -0,0 +1,93 @@ +package com.baiye.notify.listener; + +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.notify.enums.NotifyChannelEnum; +import com.baiye.notify.model.entity.Announcement; +import com.baiye.notify.model.entity.UserAnnouncement; +import com.baiye.notify.recipient.RecipientHandler; +import com.baiye.notify.service.AnnouncementService; +import com.baiye.notify.service.UserAnnouncementService; +import com.baiye.security.userdetails.User; +import com.baiye.system.model.entity.SysUser; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Hccake 2020/12/23 + * @version 1.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class AnnouncementLoginEventListener { + + private final AnnouncementService announcementService; + + private final RecipientHandler recipientHandler; + + private final UserAnnouncementService userAnnouncementService; + + /** + * 登录成功时间监听 用户未读公告生成 + * @param event 登录成功 event + */ + @EventListener(AuthenticationSuccessEvent.class) + public void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) throws InterruptedException { + + AbstractAuthenticationToken source = (AbstractAuthenticationToken) event.getSource(); + Object details = source.getDetails(); + if (!(details instanceof HashMap)) { + return; + } + // https://github.com/spring-projects-experimental/spring-authorization-server + if ("password".equals(((HashMap) details).get("grant_type"))) { + User user = (User) source.getPrincipal(); + SysUser sysUser = getSysUser(user); + + // 获取当前用户未拉取过的公告信息 + Long userId = sysUser.getUserId(); + List announcements = announcementService.listUnPulled(userId); + // 获取当前用户的各个过滤属性 + Map filterAttrs = recipientHandler.getFilterAttrs(sysUser); + // 获取符合当前用户条件的,且接收类型包含站内的公告,保存其关联关系 + List userAnnouncements = announcements.stream() + .filter(x -> x.getReceiveMode().contains(NotifyChannelEnum.STATION.getValue())) + .filter(x -> filterMatched(x, filterAttrs)) + .map(x -> userAnnouncementService.prodUserAnnouncement + (userId, x.getId(), DefaultNumberConstants.ZERO_NUMBER, x.getType(), null, x.getTitle(), x.getContent())) + .collect(Collectors.toList()); + try { + userAnnouncementService.saveBatch(userAnnouncements); + } + catch (Exception exception) { + log.error("用户公告保存失败:[{}]", userAnnouncements, exception); + } + } + } + + private SysUser getSysUser(User user) { + SysUser sysUser = new SysUser(); + sysUser.setUserId(user.getUserId()); + sysUser.setUsername(user.getUsername()); + sysUser.setNickname(user.getNickname()); + sysUser.setAvatar(user.getAvatar()); + sysUser.setOrganizationId(user.getOrganizationId()); + sysUser.setType(user.getType()); + return sysUser; + } + + private boolean filterMatched(Announcement announ, Map filterAttrs) { + Integer type = announ.getRecipientFilterType(); + return recipientHandler.match(type, filterAttrs.get(type), announ.getRecipientFilterCondition()); + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/listener/NotifyPublishEventListener.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/listener/NotifyPublishEventListener.java new file mode 100644 index 0000000..92ad3a2 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/listener/NotifyPublishEventListener.java @@ -0,0 +1,37 @@ +package com.baiye.notify.listener; + +import com.baiye.notify.event.NotifyPublishEvent; +import com.baiye.notify.model.domain.NotifyInfo; +import com.baiye.notify.push.NotifyPushExecutor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 通知发布事件监听器 + * + * @author Hccake 2020/12/17 + * @version 1.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class NotifyPublishEventListener { + + private final NotifyPushExecutor notifyPushExecutor; + + /** + * 通知发布事件 + * @param event the NotifyPublishEvent + */ + @Async + @EventListener(NotifyPublishEvent.class) + public void onNotifyPublishEvent(NotifyPublishEvent event) { + NotifyInfo notifyInfo = event.getNotifyInfo(); + // 推送通知 + notifyPushExecutor.push(notifyInfo); + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/mapper/AnnouncementMapper.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/mapper/AnnouncementMapper.java new file mode 100644 index 0000000..aaa9ffb --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/mapper/AnnouncementMapper.java @@ -0,0 +1,76 @@ +package com.baiye.notify.mapper; + +import com.baiye.constant.GlobalConstants; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.notify.enums.AnnouncementStatusEnum; +import com.baiye.notify.model.entity.Announcement; +import com.baiye.notify.model.qo.AnnouncementQO; +import com.baiye.notify.model.vo.AnnouncementPageVO; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.baiye.extend.mybatis.plus.conditions.query.LambdaQueryWrapperX; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 公告信息 + * + * @author hccake 2020-12-15 17:01:15 + */ +public interface AnnouncementMapper extends ExtendMapper { + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param qo 查询对象 + * @return 分页结果数据 PageResult + */ + default PageResult queryPage(PageParam pageParam, AnnouncementQO qo) { + IPage page = this.prodPage(pageParam); + LambdaQueryWrapperX wrapperX = WrappersX.lambdaAliasQueryX(Announcement.class) + .likeIfPresent(Announcement::getTitle, qo.getTitle()) + .inIfPresent(Announcement::getStatus, (Object[]) qo.getStatus()) + .eqIfPresent(Announcement::getRecipientFilterType, qo.getRecipientFilterType()) + .eq(Announcement::getDeleted, GlobalConstants.NOT_DELETED_FLAG); + IPage voPage = this.selectByPage(page, wrapperX); + return new PageResult<>(voPage.getRecords(), voPage.getTotal()); + } + + /** + * 分页查询通知 + * @param page 分页封装对象 + * @param wrapper 条件构造器 + * @return 分页封装对象 + */ + IPage selectByPage(IPage page, + @Param(Constants.WRAPPER) Wrapper wrapper); + + /** + * 更新公共(限制只能更新未发布的公共) + * @param announcement 公共信息 + * @return 更新是否成功 + */ + default boolean updateIfUnpublished(Announcement announcement) { + int flag = this.update(announcement, + Wrappers.lambdaUpdate() + .eq(Announcement::getId, announcement.getId()) + .eq(Announcement::getStatus, AnnouncementStatusEnum.UNPUBLISHED.getValue())); + return SqlHelper.retBool(flag); + } + + /** + * 根据参数获取当前用户拉取过,或者未拉取过的有效的公告信息 + * @param userId 用户ID + * @param pulled 当前用户是否拉取过 + * @return 公告信息列表 + */ + List listUserAnnouncements(@Param("userId") Long userId, @Param("pulled") boolean pulled); + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/mapper/UserAnnouncementMapper.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/mapper/UserAnnouncementMapper.java new file mode 100644 index 0000000..9f605f5 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/mapper/UserAnnouncementMapper.java @@ -0,0 +1,76 @@ +package com.baiye.notify.mapper; + +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.conditions.query.LambdaQueryWrapperX; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; +import com.baiye.notify.converter.UserAnnouncementConverter; +import com.baiye.notify.enums.UserAnnouncementStateEnum; +import com.baiye.notify.model.entity.UserAnnouncement; +import com.baiye.notify.model.qo.UserAnnouncementQO; +import com.baiye.notify.model.vo.UserAnnouncementPageVO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; + +import java.time.LocalDateTime; + +/** + * 用户公告表 + * + * @author hccake 2020-12-25 08:04:53 + */ +public interface UserAnnouncementMapper extends ExtendMapper { + + /** + * 分页查询 + * + * @param pageParam 分页参数 + * @param qo 查询对象 + * @return 分页结果数据 PageResult + */ + default PageResult queryPage(PageParam pageParam, UserAnnouncementQO qo) { + IPage page = this.prodPage(pageParam); + LambdaQueryWrapperX wrapperX = WrappersX.lambdaQueryX(UserAnnouncement.class); + if (StringUtils.isNotBlank(qo.getStartTime()) && StringUtils.isNotBlank(qo.getEndTime())) { + wrapperX.between(UserAnnouncement::getCreateTime, qo.getStartTime(), qo.getEndTime()); + } + wrapperX.eqIfPresent(UserAnnouncement::getId, qo.getId()).eqIfPresent(UserAnnouncement::getUserId,qo.getUserId()).likeIfPresent + (UserAnnouncement::getContent, qo.getContent()).orderByAsc(UserAnnouncement::getState).orderByDesc(UserAnnouncement::getId); + this.selectPage(page, wrapperX); + IPage voPage = page.convert(UserAnnouncementConverter.INSTANCE::poToPageVo); + return new PageResult<>(voPage.getRecords(), voPage.getTotal()); + } + + /** + * 更新用户公共信息至已读状态 + * + * @param userId 用户ID + * @param announcementId 公告ID + */ + default void updateToReadState(Long userId, Long announcementId) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate() + .set(UserAnnouncement::getState, UserAnnouncementStateEnum.READ.getValue()) + .set(UserAnnouncement::getReadTime, LocalDateTime.now()) + .eq(UserAnnouncement::getId, announcementId) + .eq(UserAnnouncement::getUserId, userId); + this.update(null, wrapper); + } + + /** + * 更新用户公共信息至已读状态 + * + * @param userId 用户ID + * @param id 公告ID + */ + default void updateToReadStateByMessageKey(Long userId, Long id) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate() + .set(UserAnnouncement::getState, UserAnnouncementStateEnum.READ.getValue()) + .set(UserAnnouncement::getReadTime, LocalDateTime.now()) + .eq(UserAnnouncement::getMessageKey, id.toString()) + .eq(UserAnnouncement::getUserId, userId); + this.update(null, wrapper); + } +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/MailNotifyPusher.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/MailNotifyPusher.java new file mode 100644 index 0000000..ad775b9 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/MailNotifyPusher.java @@ -0,0 +1,52 @@ +/* +package com.baiye.notify.push; + +import cn.hutool.core.util.StrUtil; +import com.baiye.notify.enums.NotifyChannelEnum; +import com.baiye.notify.model.domain.NotifyInfo; +import com.baiye.system.model.entity.SysUser; +import lombok.RequiredArgsConstructor; +import org.springframework.mail.MailSender; + +import java.util.List; + +*/ +/** + * 通知邮件发布 + * + * @author Hccake 2020/12/21 + * @version 1.0 + *//* + +@RequiredArgsConstructor +public class MailNotifyPusher implements NotifyPusher { + + private final MailSender mailSender; + + */ +/** + * 当前发布者的推送方式 + * @see NotifyChannelEnum + * @return 推送方式 + *//* + + @Override + public Integer notifyChannel() { + return NotifyChannelEnum.MAIL.getValue(); + } + + @Override + public void push(NotifyInfo notifyInfo, List userList) { + String[] emails = userList.stream().map(SysUser::getEmail).filter(StrUtil::isNotBlank).toArray(String[]::new); + + // 密送群发,不展示其他收件人 + MailDetails mailDetails = new MailDetails(); + mailDetails.setShowHtml(true); + mailDetails.setSubject(notifyInfo.getTitle()); + mailDetails.setContent(notifyInfo.getContent()); + mailDetails.setBcc(emails); + mailSender.sendMail(mailDetails); + } + +} +*/ diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/NotifyPushExecutor.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/NotifyPushExecutor.java new file mode 100644 index 0000000..36f8969 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/NotifyPushExecutor.java @@ -0,0 +1,77 @@ +package com.baiye.notify.push; + +import cn.hutool.core.collection.CollUtil; +import com.baiye.notify.model.domain.NotifyInfo; +import com.baiye.notify.recipient.RecipientHandler; +import com.baiye.system.model.entity.SysUser; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 通知消息推送执行器 + * + * @author Hccake 2020/12/21 + * @version 1.0 + */ +@Slf4j +@Component +public class NotifyPushExecutor { + + private final RecipientHandler recipientHandler; + + private final Map notifyPusherMap = new LinkedHashMap<>(); + + public NotifyPushExecutor(RecipientHandler recipientHandler, List notifyPusherList) { + this.recipientHandler = recipientHandler; + if (CollUtil.isNotEmpty(notifyPusherList)) { + for (NotifyPusher notifyPusher : notifyPusherList) { + this.addNotifyPusher(notifyPusher); + } + } + } + + /** + * 添加通知推送者 + * @param notifyPusher 通知推送者 + */ + public void addNotifyPusher(NotifyPusher notifyPusher) { + this.notifyPusherMap.put(notifyPusher.notifyChannel(), notifyPusher); + } + + /** + * 执行通知推送 + * @param notifyInfo 通知信息 + * + */ + public void push(NotifyInfo notifyInfo) { + // 获取通知接收人 + Integer recipientFilterType = notifyInfo.getRecipientFilterType(); + List recipientFilterCondition = notifyInfo.getRecipientFilterCondition(); + List userList = recipientHandler.query(recipientFilterType, recipientFilterCondition); + + // 执行推送 + // TODO 返回推送失败渠道信息,以便重试 + for (Integer notifyChannel : notifyInfo.getReceiveMode()) { + try { + NotifyPusher notifyPusher = notifyPusherMap.get(notifyChannel); + + if (notifyPusher == null) { + log.error("Unknown notify channel:[{}],notifyInfo title:[{}]", notifyChannel, + notifyInfo.getTitle()); + } + else { + notifyPusher.push(notifyInfo, userList); + } + } + catch (Exception e) { + log.error("push notify error in channel:[{}],notifyInfo title:[{}]", notifyChannel, + notifyInfo.getTitle(), e); + } + } + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/NotifyPusher.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/NotifyPusher.java new file mode 100644 index 0000000..5b6ddb4 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/NotifyPusher.java @@ -0,0 +1,33 @@ +package com.baiye.notify.push; + + + +import com.baiye.notify.enums.NotifyChannelEnum; +import com.baiye.notify.model.domain.NotifyInfo; +import com.baiye.system.model.entity.SysUser; + +import java.util.List; + +/** + * 通知发布者 + * + * @author Hccake 2020/12/21 + * @version 1.0 + */ +public interface NotifyPusher { + + /** + * 当前发布者对应的推送渠道 + * @see NotifyChannelEnum + * @return 推送方式对应的标识 + */ + Integer notifyChannel(); + + /** + * 推送通知 + * @param notifyInfo 通知信息 + * @param userList 用户列表 + */ + void push(NotifyInfo notifyInfo, List userList); + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/SmsNotifyPusher.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/SmsNotifyPusher.java new file mode 100644 index 0000000..3c90c4f --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/SmsNotifyPusher.java @@ -0,0 +1,45 @@ +package com.baiye.notify.push; + +import cn.hutool.core.util.StrUtil; + +import com.baiye.notify.enums.NotifyChannelEnum; +import com.baiye.notify.model.domain.NotifyInfo; +import com.baiye.system.model.entity.SysUser; +import com.baiye.util.HtmlUtils; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 短信通知发布 + * + * @author Hccake 2020/12/21 + * @version 1.0 + */ +@Component +public class SmsNotifyPusher implements NotifyPusher { + + /** + * 当前发布者对应的接收方式 + * @see NotifyChannelEnum + * @return 推送方式 + */ + @Override + public Integer notifyChannel() { + return NotifyChannelEnum.SMS.getValue(); + } + + @Override + public void push(NotifyInfo notifyInfo, List userList) { + List phoneList = userList.stream() + .map(SysUser::getPhoneNumber) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + // 短信文本去除 html 标签 + String content = HtmlUtils.toText(notifyInfo.getContent()); + // TODO 对接短信发送平台 + System.out.println("短信推送"); + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/StationNotifyPusher.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/StationNotifyPusher.java new file mode 100644 index 0000000..818aede --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/push/StationNotifyPusher.java @@ -0,0 +1,45 @@ +package com.baiye.notify.push; + +; +import com.baiye.notify.enums.NotifyChannelEnum; +import com.baiye.notify.event.StationNotifyPushEvent; +import com.baiye.notify.model.domain.NotifyInfo; +import com.baiye.system.model.entity.SysUser; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 消息通知站内推送 + * + * @author Hccake 2020/12/21 + * @version 1.0 + */ +@Component +@RequiredArgsConstructor +public class StationNotifyPusher implements NotifyPusher { + + private final ApplicationEventPublisher publisher; + + /** + * 当前发布者对应的接收方式 + * + * @return 推送方式 + * @see NotifyChannelEnum + */ + @Override + public Integer notifyChannel() { + return NotifyChannelEnum.STATION.getValue(); + } + + @Override + public void push(NotifyInfo notifyInfo, List userList) { + List userIdList = userList.stream().map(SysUser::getUserId).collect(Collectors.toList()); + // 发布事件,监听者进行实际的 websocket 推送 + publisher.publishEvent(new StationNotifyPushEvent(notifyInfo, userIdList)); + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/AllRecipientFilter.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/AllRecipientFilter.java new file mode 100644 index 0000000..c5ee2a2 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/AllRecipientFilter.java @@ -0,0 +1,62 @@ +package com.baiye.notify.recipient; + +import com.baiye.notify.enums.NotifyRecipientFilterTypeEnum; +import com.baiye.system.model.entity.SysUser; +import com.baiye.system.service.SysUserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author Hccake 2020/12/21 + * @version 1.0 + */ +@Component +@RequiredArgsConstructor +public class AllRecipientFilter implements RecipientFilter { + + private final SysUserService sysUserService; + + /** + * 当前筛选器对应的筛选类型 + * @return 筛选类型对应的标识 + * @see NotifyRecipientFilterTypeEnum + */ + @Override + public Integer filterType() { + return NotifyRecipientFilterTypeEnum.ALL.getValue(); + } + + /** + * 接收者筛选 + * @param filterCondition 筛选条件 + * @return 接收者集合 + */ + @Override + public List filter(List filterCondition) { + return sysUserService.list(); + } + + /** + * 获取当前用户的过滤属性 + * @param sysUser 系统用户 + * @return 该用户所对应筛选条件的属性 + */ + @Override + public Object getFilterAttr(SysUser sysUser) { + return null; + } + + /** + * 是否匹配当前用户 + * @param filterAttr 筛选属性 + * @param filterCondition 筛选条件 + * @return boolean true: 是否匹配 + */ + @Override + public boolean match(Object filterAttr, List filterCondition) { + return true; + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/RecipientFilter.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/RecipientFilter.java new file mode 100644 index 0000000..a7bd1a3 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/RecipientFilter.java @@ -0,0 +1,46 @@ +package com.baiye.notify.recipient; + + +import com.baiye.notify.enums.NotifyRecipientFilterTypeEnum; +import com.baiye.system.model.entity.SysUser; + +import java.util.List; + +/** + * 接收者筛选器 + * + * @author Hccake 2020/12/21 + * @version 1.0 + */ +public interface RecipientFilter { + + /** + * 当前筛选器对应的筛选类型 + * @see NotifyRecipientFilterTypeEnum + * @return 筛选类型对应的标识 + */ + Integer filterType(); + + /** + * 接收者筛选 + * @param filterCondition 筛选条件 + * @return 接收者集合 + */ + List filter(List filterCondition); + + /** + * 获取当前用户的过滤属性 + * @param sysUser 系统用户 + * @return 该用户所对应筛选条件的属性 + */ + Object getFilterAttr(SysUser sysUser); + + /** + * 是否匹配当前用户 + * @param filterAttr 筛选属性 + * @param filterCondition 筛选条件 + * @return boolean true: 是否匹配 + */ + boolean match(Object filterAttr, List filterCondition); + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/RecipientHandler.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/RecipientHandler.java new file mode 100644 index 0000000..465926b --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/RecipientHandler.java @@ -0,0 +1,60 @@ +package com.baiye.notify.recipient; + +import com.baiye.system.model.entity.SysUser; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.*; + +/** + * @author Hccake 2020/12/21 + * @version 1.0 + */ +@Slf4j +@Component +public class RecipientHandler { + + private final Map recipientFilterMap = new LinkedHashMap<>(); + + public RecipientHandler(List recipientFilterList) { + for (RecipientFilter recipientFilter : recipientFilterList) { + this.recipientFilterMap.put(recipientFilter.filterType(), recipientFilter); + } + } + + public List query(Integer filterType, List filterCondition) { + RecipientFilter recipientFilter = recipientFilterMap.get(filterType); + if (recipientFilter == null) { + log.error("Unknown recipient filter:[{}],filterCondition:{}", filterType, filterCondition); + return new ArrayList<>(); + } + return recipientFilter.filter(filterCondition); + } + + /** + * 判断当前是否匹配 + * @param recipientFilterType 筛选类型 + * @param filterAttr 筛选属性 + * @param recipientFilterCondition 筛选条件 + * @return boolean true:匹配 + */ + public boolean match(Integer recipientFilterType, Object filterAttr, List recipientFilterCondition) { + RecipientFilter recipientFilter = recipientFilterMap.get(recipientFilterType); + return recipientFilter != null && recipientFilter.match(filterAttr, recipientFilterCondition); + } + + /** + * 获取当前用户的各个筛选器对应的属性 + * @param sysUser 系统用户 + * @return 属性Map key:filterType value: attr + */ + public Map getFilterAttrs(SysUser sysUser) { + Map map = new HashMap<>(recipientFilterMap.size()); + for (RecipientFilter recipientFilter : recipientFilterMap.values()) { + Object obj = recipientFilter.getFilterAttr(sysUser); + map.put(recipientFilter.filterType(), obj); + } + return map; + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/SpecifyOrganizationRecipientFilter.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/SpecifyOrganizationRecipientFilter.java new file mode 100644 index 0000000..806a805 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/SpecifyOrganizationRecipientFilter.java @@ -0,0 +1,65 @@ +package com.baiye.notify.recipient; + +import com.baiye.notify.enums.NotifyRecipientFilterTypeEnum; +import com.baiye.system.model.entity.SysUser; +import com.baiye.system.service.SysUserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Hccake 2020/12/21 + * @version 1.0 + */ +@Component +@RequiredArgsConstructor +public class SpecifyOrganizationRecipientFilter implements RecipientFilter { + + private final SysUserService sysUserService; + + /** + * 当前筛选器对应的筛选类型 + * @return 筛选类型对应的标识 + * @see NotifyRecipientFilterTypeEnum + */ + @Override + public Integer filterType() { + return NotifyRecipientFilterTypeEnum.SPECIFY_ORGANIZATION.getValue(); + } + + /** + * 接收者筛选 + * @param filterCondition 筛选条件 + * @return 接收者集合 + */ + @Override + public List filter(List filterCondition) { + List organizationIds = filterCondition.stream().map(Long.class::cast).collect(Collectors.toList()); + return sysUserService.listByOrganizationIds(organizationIds); + } + + /** + * 获取当前用户的过滤属性 + * @param sysUser 系统用户 + * @return 该用户所对应筛选条件的属性 + */ + @Override + public Object getFilterAttr(SysUser sysUser) { + return sysUser.getOrganizationId(); + } + + /** + * 是否匹配当前用户 + * @param filterAttr 筛选属性 + * @param filterCondition 筛选条件 + * @return boolean true: 是否匹配 + */ + @Override + public boolean match(Object filterAttr, List filterCondition) { + Integer organizationId = (Integer) filterAttr; + return filterCondition.stream().map(Integer.class::cast).anyMatch(x -> x.equals(organizationId)); + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/SpecifyRoleRecipientFilter.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/SpecifyRoleRecipientFilter.java new file mode 100644 index 0000000..ce33b25 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/SpecifyRoleRecipientFilter.java @@ -0,0 +1,79 @@ +package com.baiye.notify.recipient; + +import cn.hutool.core.collection.CollUtil; +import com.baiye.notify.enums.NotifyRecipientFilterTypeEnum; +import com.baiye.system.model.entity.SysUser; +import com.baiye.system.service.SysUserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Hccake 2020/12/21 + * @version 1.0 + */ +@Component +@RequiredArgsConstructor +public class SpecifyRoleRecipientFilter implements RecipientFilter { + + private final SysUserService sysUserService; + + /** + * 当前筛选器对应的筛选类型 + * @return 筛选类型对应的标识 + * @see NotifyRecipientFilterTypeEnum + */ + @Override + public Integer filterType() { + return NotifyRecipientFilterTypeEnum.SPECIFY_ROLE.getValue(); + } + + /** + * 接收者筛选 + * @param filterCondition 筛选条件 + * @return 接收者集合 + */ + @Override + public List filter(List filterCondition) { + List roleCodes = filterCondition.stream().map(String.class::cast).collect(Collectors.toList()); + return sysUserService.listByRoleCodes(roleCodes); + } + + /** + * 获取当前用户的过滤属性 + * @param sysUser 系统用户 + * @return 该用户所对应筛选条件的属性 + */ + @Override + public Object getFilterAttr(SysUser sysUser) { + return sysUserService.listRoleCodes(sysUser.getUserId()); + } + + /** + * 是否匹配当前用户 + * @param filterAttr 筛选属性 + * @param filterCondition 筛选条件 + * @return boolean true: 是否匹配 + */ + @Override + @SuppressWarnings("unchecked") + public boolean match(Object filterAttr, List filterCondition) { + if (!(filterAttr instanceof List)) { + return false; + } + List roleCodes = (List) filterAttr; + if (CollUtil.isEmpty(roleCodes)) { + return false; + } + for (Object roleCode : roleCodes) { + boolean matched = filterCondition.stream().map(String.class::cast).anyMatch(x -> x.equals(roleCode)); + if (matched) { + return true; + } + } + return false; + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/SpecifyUserRecipientFilter.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/SpecifyUserRecipientFilter.java new file mode 100644 index 0000000..1d2248a --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/SpecifyUserRecipientFilter.java @@ -0,0 +1,65 @@ +package com.baiye.notify.recipient; + +import com.baiye.notify.enums.NotifyRecipientFilterTypeEnum; +import com.baiye.system.model.entity.SysUser; +import com.baiye.system.service.SysUserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Hccake 2020/12/21 + * @version 1.0 + */ +@Component +@RequiredArgsConstructor +public class SpecifyUserRecipientFilter implements RecipientFilter { + + private final SysUserService sysUserService; + + /** + * 当前筛选器对应的筛选类型 + * @return 筛选类型对应的标识 + * @see NotifyRecipientFilterTypeEnum + */ + @Override + public Integer filterType() { + return NotifyRecipientFilterTypeEnum.SPECIFY_USER.getValue(); + } + + /** + * 接收者筛选 + * @param filterCondition 筛选条件 用户ID集合 + * @return 接收者集合 + */ + @Override + public List filter(List filterCondition) { + List userIds = filterCondition.stream().map(Long.class::cast).collect(Collectors.toList()); + return sysUserService.listByUserIds(userIds); + } + + /** + * 获取当前用户的过滤属性 + * @param sysUser 系统用户 + * @return 该用户所对应筛选条件的属性 + */ + @Override + public Object getFilterAttr(SysUser sysUser) { + return sysUser.getUserId(); + } + + /** + * 是否匹配当前用户 + * @param filterAttr 筛选属性 + * @param filterCondition 筛选条件 + * @return boolean true: 是否匹配 + */ + @Override + public boolean match(Object filterAttr, List filterCondition) { + Long userId = (Long) filterAttr; + return filterCondition.stream().map(Long.class::cast).anyMatch(x -> x.equals(userId)); + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/SpecifyUserTypeRecipientFilter.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/SpecifyUserTypeRecipientFilter.java new file mode 100644 index 0000000..4cf45e2 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/recipient/SpecifyUserTypeRecipientFilter.java @@ -0,0 +1,65 @@ +package com.baiye.notify.recipient; + +import com.baiye.notify.enums.NotifyRecipientFilterTypeEnum; +import com.baiye.system.model.entity.SysUser; +import com.baiye.system.service.SysUserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Hccake 2020/12/21 + * @version 1.0 + */ +@Component +@RequiredArgsConstructor +public class SpecifyUserTypeRecipientFilter implements RecipientFilter { + + private final SysUserService sysUserService; + + /** + * 当前筛选器对应的筛选类型 + * @return 筛选类型对应的标识 + * @see NotifyRecipientFilterTypeEnum + */ + @Override + public Integer filterType() { + return NotifyRecipientFilterTypeEnum.SPECIFY_USER_TYPE.getValue(); + } + + /** + * 接收者筛选 + * @param filterCondition 筛选条件 + * @return 接收者集合 + */ + @Override + public List filter(List filterCondition) { + List userTypes = filterCondition.stream().map(Integer.class::cast).collect(Collectors.toList()); + return sysUserService.listByUserTypes(userTypes); + } + + /** + * 获取当前用户的过滤属性 + * @param sysUser 系统用户 + * @return 该用户所对应筛选条件的属性 + */ + @Override + public Object getFilterAttr(SysUser sysUser) { + return sysUser.getType(); + } + + /** + * 是否匹配当前用户 + * @param filterAttr 筛选属性 + * @param filterCondition 筛选条件 + * @return boolean true: 是否匹配 + */ + @Override + public boolean match(Object filterAttr, List filterCondition) { + Integer type = (Integer) filterAttr; + return filterCondition.stream().map(Integer.class::cast).anyMatch(x -> x.equals(type)); + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/AnnouncementService.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/AnnouncementService.java new file mode 100644 index 0000000..1fec1bb --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/AnnouncementService.java @@ -0,0 +1,79 @@ +package com.baiye.notify.service; + + +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.ExtendService; +import com.baiye.notify.model.dto.AnnouncementDTO; +import com.baiye.notify.model.entity.Announcement; +import com.baiye.notify.model.qo.AnnouncementQO; +import com.baiye.notify.model.vo.AnnouncementPageVO; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 公告信息 + * + * @author hccake 2020-12-15 17:01:15 + */ +public interface AnnouncementService extends ExtendService { + + /** + * 根据QueryObject查询分页数据 + * @param page 分页参数 + * @param qo 查询参数对象 + * @return PageResult 分页数据 + */ + PageResult queryPage(PageParam page, AnnouncementQO qo); + + /** + * 创建公告 + * @param announcementDTO 公告信息 + * @return boolean + */ + boolean addAnnouncement(AnnouncementDTO announcementDTO); + + /** + * 更新公告信息 + * @param announcementDTO announcementDTO + * @return boolean + */ + boolean updateAnnouncement(AnnouncementDTO announcementDTO); + + /** + * 发布公告信息 + * @param announcementId 公告ID + * @return boolean + */ + boolean publish(Long announcementId); + + /** + * 关闭公告信息 + * @param announcementId 公告ID + * @return boolean + */ + boolean close(Long announcementId); + + /** + * 批量上传公告图片 + * @param files 图片文件 + * @return 上传后的图片相对路径集合 + */ + List uploadImages(List files); + + /** + * 当前用户未拉取过的发布中,且满足失效时间的公告信息 + * @param userId 用户id + * @return List + */ + List listUnPulled(Long userId); + + /** + * 获取用户拉取过的发布中,且满足失效时间的公告信息 + * @param userId 用户id + * @return List + */ + List listActiveAnnouncements(Long userId); + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/UserAnnouncementService.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/UserAnnouncementService.java new file mode 100644 index 0000000..3e3926f --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/UserAnnouncementService.java @@ -0,0 +1,61 @@ +package com.baiye.notify.service; + + +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.ExtendService; +import com.baiye.notify.model.entity.UserAnnouncement; +import com.baiye.notify.model.qo.UserAnnouncementQO; +import com.baiye.notify.model.vo.UserAnnouncementPageVO; + +/** + * 用户公告表 + * + * @author hccake 2020-12-25 08:04:53 + */ +public interface UserAnnouncementService extends ExtendService { + + /** + * 根据QueryObject查询分页数据 + * @param pageParam 分页参数 + * @param qo 查询参数对象 + * @return PageResult 分页数据 + */ + PageResult queryPage(PageParam pageParam, UserAnnouncementQO qo); + + /** + * 根据用户ID和公告id初始化一个新的用户公告关联对象 + * + * @param userId 用户ID + * @param announcementId 公告ID + * @param state + * @param type + * @param messageKey + * @param title + * @param content + * @return UserAnnouncement + */ + UserAnnouncement prodUserAnnouncement(Long userId, Long announcementId, Integer state, Integer type, String messageKey, String title, String content); + + /** + * 对用户公告进行已读标记 + * @param userId 用户id + * @param announcementId 公告id + */ + void readAnnouncement(Long userId, Long announcementId); + + /** + * 查询未读数量 + * @param currentUserId + * @return + */ + Long queryNotReadNum(Long currentUserId); + + + /** + * 对用户公告进行已读标记 + * @param userId 用户id + * @param id 公告id + */ + void readAnnouncementByMessageKey(Long userId, Long id); +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/impl/AnnouncementServiceImpl.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/impl/AnnouncementServiceImpl.java new file mode 100644 index 0000000..d96b83a --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/impl/AnnouncementServiceImpl.java @@ -0,0 +1,214 @@ +package com.baiye.notify.service.impl; + +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.IdUtil; +import com.baiye.constant.enums.BooleanEnum; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.exception.BusinessException; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import com.baiye.notify.converter.AnnouncementConverter; +import com.baiye.notify.converter.NotifyInfoConverter; +import com.baiye.notify.enums.AnnouncementStatusEnum; +import com.baiye.notify.event.AnnouncementCloseEvent; +import com.baiye.notify.event.NotifyPublishEvent; +import com.baiye.notify.mapper.AnnouncementMapper; +import com.baiye.notify.model.domain.NotifyInfo; +import com.baiye.notify.model.dto.AnnouncementDTO; +import com.baiye.notify.model.entity.Announcement; +import com.baiye.notify.model.qo.AnnouncementQO; +import com.baiye.notify.model.vo.AnnouncementPageVO; +import com.baiye.notify.service.AnnouncementService; +import com.baiye.result.SystemResultCode; +import com.baiye.util.FileUtil; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +/** + * 公告信息 + * + * @author hccake 2020-12-15 17:01:15 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AnnouncementServiceImpl extends ExtendServiceImpl + implements AnnouncementService { + + private final ApplicationEventPublisher publisher; + + // private final FileService fileService; + + /** + * 根据QueryObject查询分页数据 + * + * @param pageParam 分页参数 + * @param qo 查询参数对象 + * @return PageResult 分页数据 + */ + @Override + public PageResult queryPage(PageParam pageParam, AnnouncementQO qo) { + return baseMapper.queryPage(pageParam, qo); + } + + /** + * 创建公告 + * + * @param announcementDTO 公告信息 + * @return boolean + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean addAnnouncement(AnnouncementDTO announcementDTO) { + Announcement announcement = AnnouncementConverter.INSTANCE.dtoToPo(announcementDTO); + announcement.setId(null); + int flag = baseMapper.insert(announcement); + boolean inserted = SqlHelper.retBool(flag); + // 公告发布事件 + boolean isPublishStatus = announcement.getStatus() == AnnouncementStatusEnum.ENABLED.getValue(); + if (inserted && isPublishStatus) { + this.onAnnouncementPublish(announcement); + } + return inserted; + } + + /** + * 更新公告信息 + * + * @param announcementDTO announcementDTO + * @return boolean + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateAnnouncement(AnnouncementDTO announcementDTO) { + Announcement oldAnnouncement = baseMapper.selectById(announcementDTO.getId()); + if (oldAnnouncement.getStatus() != AnnouncementStatusEnum.UNPUBLISHED.getValue()) { + throw new BusinessException(SystemResultCode.BAD_REQUEST.getCode(), "不允许修改已经发布过的公告!"); + } + + Announcement announcement = AnnouncementConverter.INSTANCE.dtoToPo(announcementDTO); + // 不允许修改为《发布中》以外的状态 + boolean isPublishStatus = announcement.getStatus() == AnnouncementStatusEnum.ENABLED.getValue(); + if (!isPublishStatus) { + announcement.setStatus(null); + } + // 保证当前状态未被修改过 + boolean isUpdated = baseMapper.updateIfUnpublished(announcement); + // 公告发布事件 + if (isUpdated && isPublishStatus) { + this.onAnnouncementPublish(announcement); + } + return isUpdated; + } + + /** + * 发布公告信息 + * + * @param announcementId 公告ID + * @return boolean + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean publish(Long announcementId) { + Announcement announcement = baseMapper.selectById(announcementId); + if (announcement.getStatus() != AnnouncementStatusEnum.UNPUBLISHED.getValue()) { + throw new BusinessException(SystemResultCode.BAD_REQUEST.getCode(), "不允许修改已经发布过的公告!"); + } + if (BooleanEnum.TRUE.intValue().equals(announcement.getImmortal()) + && LocalDateTime.now().isAfter(announcement.getDeadline())) { + throw new BusinessException(SystemResultCode.BAD_REQUEST.getCode(), "公告失效时间必须迟于当前时间!"); + } + + // 更新公共至发布状态 + Announcement entity = new Announcement(); + entity.setId(announcementId); + entity.setStatus(AnnouncementStatusEnum.ENABLED.getValue()); + boolean isUpdated = baseMapper.updateIfUnpublished(entity); + if (isUpdated) { + announcement.setStatus(AnnouncementStatusEnum.ENABLED.getValue()); + this.onAnnouncementPublish(announcement); + } + return isUpdated; + } + + /** + * 关闭公告信息 + * + * @param announcementId 公告ID + * @return boolean + */ + @Override + public boolean close(Long announcementId) { + Announcement announcement = new Announcement(); + announcement.setId(announcementId); + announcement.setStatus(AnnouncementStatusEnum.DISABLED.getValue()); + int flag = baseMapper.updateById(announcement); + boolean isUpdated = SqlHelper.retBool(flag); + if (isUpdated) { + publisher.publishEvent(new AnnouncementCloseEvent(announcementId)); + } + return isUpdated; + } + + /** + * 批量上传公告图片 + * + * @param files 图片文件 + * @return 上传后的图片相对路径集合 + */ + @Override + public List uploadImages(List files) { + List objectNames = new ArrayList<>(); + for (MultipartFile file : files) { + String objectName = "announcement/" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + + StrPool.SLASH + IdUtil.fastSimpleUUID() + StrPool.DOT + + FileUtil.extName(file.getOriginalFilename()); + FileUtil.upload(file, objectName); + objectNames.add(objectName); + } + return objectNames; + } + + /** + * 当前用户未拉取过的发布中,且满足失效时间的公告信息 + * + * @return List + */ + @Override + public List listUnPulled(Long userId) { + return baseMapper.listUserAnnouncements(userId, false); + } + + /** + * 获取用户拉取过的发布中,且满足失效时间的公告信息 + * + * @param userId 用户id + * @return List + */ + @Override + public List listActiveAnnouncements(Long userId) { + return baseMapper.listUserAnnouncements(userId, true); + } + + /** + * 公告发布事件 + * + * @param announcement 公告信息 + */ + private void onAnnouncementPublish(Announcement announcement) { + NotifyInfo notifyInfo = NotifyInfoConverter.INSTANCE.fromAnnouncement(announcement); + publisher.publishEvent(new NotifyPublishEvent(notifyInfo)); + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/impl/UserAnnouncementEventListener.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/impl/UserAnnouncementEventListener.java new file mode 100644 index 0000000..3c6940e --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/impl/UserAnnouncementEventListener.java @@ -0,0 +1,34 @@ +package com.baiye.notify.service.impl; + + +import com.baiye.notify.event.UserAnnouncementReadEvent; +import com.baiye.notify.service.UserAnnouncementService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * @author Hccake 2021/1/5 + * @version 1.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class UserAnnouncementEventListener { + + private final UserAnnouncementService userAnnouncementService; + + + /** + * 公告关闭事件监听 + * + * @param event the AnnouncementCloseEvent + */ + @Async + @EventListener(UserAnnouncementReadEvent.class) + public void read(UserAnnouncementReadEvent event) { + userAnnouncementService.readAnnouncementByMessageKey(event.getUserId(), event.getId()); + } +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/impl/UserAnnouncementServiceImpl.java b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/impl/UserAnnouncementServiceImpl.java new file mode 100644 index 0000000..101e0a3 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/java/com/baiye/notify/service/impl/UserAnnouncementServiceImpl.java @@ -0,0 +1,87 @@ +package com.baiye.notify.service.impl; + + +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import com.baiye.notify.enums.UserAnnouncementStateEnum; +import com.baiye.notify.mapper.UserAnnouncementMapper; +import com.baiye.notify.model.entity.UserAnnouncement; +import com.baiye.notify.model.qo.UserAnnouncementQO; +import com.baiye.notify.model.vo.UserAnnouncementPageVO; +import com.baiye.notify.service.UserAnnouncementService; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +/** + * 用户公告表 + * + * @author hccake 2020-12-25 08:04:53 + */ +@Service +public class UserAnnouncementServiceImpl extends ExtendServiceImpl + implements UserAnnouncementService { + + /** + * 根据QueryObject查询分页数据 + * @param pageParam 分页参数 + * @param qo 查询参数对象 + * @return PageResult 分页数据 + */ + @Override + public PageResult queryPage(PageParam pageParam, UserAnnouncementQO qo) { + return baseMapper.queryPage(pageParam, qo); + } + + /** + * 根据用户ID和公告id初始化一个新的用户公告关联对象 + * + * @param userId 用户ID + * @param announcementId 公告ID + * @param state + * @param type + * @param messageKey + * @param title + * @param content + * @return UserAnnouncement + */ + @Override + public UserAnnouncement prodUserAnnouncement(Long userId, Long announcementId, Integer state, Integer type, String messageKey, String title, String content) { + UserAnnouncement userAnnouncement = new UserAnnouncement(); + userAnnouncement.setState(state); + userAnnouncement.setTitle(title); + userAnnouncement.setUserId(userId); + userAnnouncement.setContent(content); + userAnnouncement.setMessageType(type); + userAnnouncement.setMessageKey(messageKey); + userAnnouncement.setAnnouncementId(announcementId); + userAnnouncement.setCreateTime(LocalDateTime.now()); + userAnnouncement.setState(UserAnnouncementStateEnum.UNREAD.getValue()); + return userAnnouncement; + } + + /** + * 对用户公告进行已读标记 + * @param userId 用户id + * @param announcementId 公告id + */ + @Override + public void readAnnouncement(Long userId, Long announcementId) { + baseMapper.updateToReadState(userId, announcementId); + } + + @Override + public Long queryNotReadNum(Long currentUserId) { + return baseMapper.selectCount + (Wrappers.lambdaQuery(UserAnnouncement.class).eq + (UserAnnouncement::getUserId, currentUserId).eq(UserAnnouncement::getState, 0)); + } + + @Override + public void readAnnouncementByMessageKey(Long userId, Long id) { + baseMapper.updateToReadStateByMessageKey(userId, id); + } + +} diff --git a/marketing-scrm-notify/distribute-notify-biz/src/main/resources/mapper/AnnouncementMapper.xml b/marketing-scrm-notify/distribute-notify-biz/src/main/resources/mapper/AnnouncementMapper.xml new file mode 100644 index 0000000..fca8e41 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-biz/src/main/resources/mapper/AnnouncementMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + id, title, content, recipient_filter_type, recipient_filter_condition, receive_mode, + status, immortal, deadline, create_by, create_time, update_time + + + + a.id, a.title, a.content, a.recipient_filter_type, a.recipient_filter_condition, a.receive_mode, + a.status, a.immortal, a.deadline, a.create_by, a.create_time, a.update_time + + + + + + + diff --git a/marketing-scrm-notify/distribute-notify-controller/pom.xml b/marketing-scrm-notify/distribute-notify-controller/pom.xml new file mode 100644 index 0000000..5173303 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-controller/pom.xml @@ -0,0 +1,26 @@ + + + + com.baiye + marketing-scrm-notify + ${revision} + + 4.0.0 + marketing-scrm-notify-controller + + + + + com.baiye + common-log + + + + com.baiye + distribute-notify-biz + ${revision} + + + + diff --git a/marketing-scrm-notify/distribute-notify-controller/src/main/java/com/baiye/notify/controller/AnnouncementController.java b/marketing-scrm-notify/distribute-notify-controller/src/main/java/com/baiye/notify/controller/AnnouncementController.java new file mode 100644 index 0000000..6ac53eb --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-controller/src/main/java/com/baiye/notify/controller/AnnouncementController.java @@ -0,0 +1,139 @@ +package com.baiye.notify.controller; + + +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.notify.model.dto.AnnouncementDTO; +import com.baiye.notify.model.entity.Announcement; +import com.baiye.notify.model.qo.AnnouncementQO; +import com.baiye.notify.model.vo.AnnouncementPageVO; +import com.baiye.notify.service.AnnouncementService; +import com.baiye.operation.annotation.CreateOperationLogging; +import com.baiye.operation.annotation.DeleteOperationLogging; +import com.baiye.operation.annotation.UpdateOperationLogging; +import com.baiye.result.BaseResultCode; +import com.baiye.result.R; +import com.baiye.security.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.Valid; +import java.util.List; + +/** + * 公告信息 + * + * @author hccake 2020-12-15 17:01:15 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/notify/announcement") +@Tag(name = "公告信息管理") +public class AnnouncementController { + + private final AnnouncementService announcementService; + + /** + * 分页查询 + * @param pageParam 分页对象 + * @param announcementQO 公告信息查询对象 + * @return R 通用返回体 + */ + @GetMapping("/page") + // @PreAuthorize("@per.hasPermission('notify:announcement:read')") + @Operation(summary = "分页查询", description = "分页查询") + public R> getAnnouncementPage(@Validated PageParam pageParam, + AnnouncementQO announcementQO) { + return R.ok(announcementService.queryPage(pageParam, announcementQO)); + } + + /** + * 新增公告信息 + * @param announcementDTO 公告信息 + * @return R 通用返回体 + */ + @CreateOperationLogging(msg = "新增公告信息") + @PostMapping + // @PreAuthorize("@per.hasPermission('notify:announcement:add')") + @Operation(summary = "新增公告信息", description = "新增公告信息") + public R save(@Valid @RequestBody AnnouncementDTO announcementDTO) { + return announcementService.addAnnouncement(announcementDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "新增公告信息失败"); + } + + /** + * 修改公告信息 + * @param announcementDTO 公告信息 + * @return R 通用返回体 + */ + @UpdateOperationLogging(msg = "修改公告信息") + @PutMapping + // @PreAuthorize("@per.hasPermission('notify:announcement:edit')") + @Operation(summary = "修改公告信息", description = "修改公告信息") + public R updateById(@Valid @RequestBody AnnouncementDTO announcementDTO) { + return announcementService.updateAnnouncement(announcementDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "修改公告信息失败"); + } + + /** + * 通过id删除公告信息 + * @param id id + * @return R 通用返回体 + */ + @DeleteOperationLogging(msg = "通过id删除公告信息") + @DeleteMapping("/{id}") + // @PreAuthorize("@per.hasPermission('notify:announcement:del')") + @Operation(summary = "通过id删除公告信息", description = "通过id删除公告信息") + public R removeById(@PathVariable("id") Long id) { + return announcementService.removeById(id) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "通过id删除公告信息失败"); + } + + /** + * 发布公告信息 + * @return R 通用返回体 + */ + @UpdateOperationLogging(msg = "发布公告信息") + @PatchMapping("/publish/{announcementId}") + // @PreAuthorize("@per.hasPermission('notify:announcement:edit')") + @Operation(summary = "发布公告信息", description = "发布公告信息") + public R enableAnnouncement(@PathVariable("announcementId") Long announcementId) { + return announcementService.publish(announcementId) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "发布公告信息失败"); + } + + /** + * 关闭公告信息 + * @return R 通用返回体 + */ + @UpdateOperationLogging(msg = "关闭公告信息") + @PatchMapping("/close/{announcementId}") + // @PreAuthorize("@per.hasPermission('notify:announcement:edit')") + @Operation(summary = "关闭公告信息", description = "关闭公告信息") + public R disableAnnouncement(@PathVariable("announcementId") Long announcementId) { + return announcementService.close(announcementId) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "关闭公告信息失败"); + } + + @UpdateOperationLogging(msg = "公告内容图片上传", recordParams = false) + // @PreAuthorize("@per.hasPermission('notify:announcement:edit')") + @PostMapping("/image") + @Operation(summary = "公告内容图片上传", description = "公告内容图片上传") + public R> uploadImages(@RequestParam("files") List files) { + List objectNames = announcementService.uploadImages(files); + return R.ok(objectNames); + } + + @GetMapping("/user") + // @PreAuthorize("@per.hasPermission('notify:userannouncement:read')") + @Operation(summary = "用户公告信息", description = "用户公告信息") + public R> getUserAnnouncements() { + Long userId = SecurityUtils.getUser().getUserId(); + return R.ok(announcementService.listActiveAnnouncements(userId)); + } + +} diff --git a/marketing-scrm-notify/distribute-notify-controller/src/main/java/com/baiye/notify/controller/UserAnnouncementController.java b/marketing-scrm-notify/distribute-notify-controller/src/main/java/com/baiye/notify/controller/UserAnnouncementController.java new file mode 100644 index 0000000..4d440d6 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-controller/src/main/java/com/baiye/notify/controller/UserAnnouncementController.java @@ -0,0 +1,67 @@ +package com.baiye.notify.controller; + + +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.notify.model.qo.UserAnnouncementQO; +import com.baiye.notify.model.vo.UserAnnouncementPageVO; +import com.baiye.notify.service.UserAnnouncementService; +import com.baiye.result.R; +import com.baiye.security.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * 用户公告表 + * + * @author hccake 2020-12-25 08:04:53 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/notify/user-announcement") +@Tag(name = "用户公告表管理") +public class UserAnnouncementController { + + private final UserAnnouncementService userAnnouncementService; + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param userAnnouncementQO 用户公告表查询对象 + * @return R 通用返回体 + */ + @GetMapping("/page") + // @PreAuthorize("@per.hasPermission('notify:userannouncement:read')") + @Operation(summary = "分页查询", description = "分页查询") + public R> getUserAnnouncementPage(@Validated PageParam pageParam, + UserAnnouncementQO userAnnouncementQO) { + userAnnouncementQO.setUserId(SecurityUtils.getCurrentUserId()); + return R.ok(userAnnouncementService.queryPage(pageParam, userAnnouncementQO)); + } + + + /** + * 查询未读数量 + * @return + */ + @GetMapping("/not/read/count") + @Operation(summary = "查询未读数量", description = "查询未读数量") + public R queryNotReadNum() { + Long currentUserId = SecurityUtils.getCurrentUserId(); + return R.ok(userAnnouncementService.queryNotReadNum(currentUserId)); + } + + + @GetMapping("/read/{announcementId}") + // @PreAuthorize("@per.hasPermission('notify:userannouncement:read')") + @Operation(summary = "用户公告已读上报", description = "用户公告已读上报") + public R readAnnouncement(@PathVariable("announcementId") Long announcementId) { + Long userId = SecurityUtils.getUser().getUserId(); + userAnnouncementService.readAnnouncement(userId, announcementId); + return R.ok(); + } + +} diff --git a/marketing-scrm-notify/distribute-notify-model/pom.xml b/marketing-scrm-notify/distribute-notify-model/pom.xml new file mode 100644 index 0000000..6af2b52 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/pom.xml @@ -0,0 +1,41 @@ + + + + com.baiye + marketing-scrm-notify + ${revision} + + 4.0.0 + distribute-notify-model + + + + com.baiye + marketing-scrm-extend-mybatis-plus + + + + io.swagger.core.v3 + swagger-annotations + + + + jakarta.validation + jakarta.validation-api + + + + + org.mapstruct + mapstruct + + + + + org.mapstruct + mapstruct-processor + 1.5.3.Final + + + diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/converter/AnnouncementConverter.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/converter/AnnouncementConverter.java new file mode 100644 index 0000000..aa2a4c2 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/converter/AnnouncementConverter.java @@ -0,0 +1,36 @@ +package com.baiye.notify.converter; + +import com.baiye.notify.model.dto.AnnouncementDTO; +import com.baiye.notify.model.entity.Announcement; +import com.baiye.notify.model.vo.AnnouncementPageVO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Hccake 2020/12/16 + * @version 1.0 + */ +@Mapper +public interface AnnouncementConverter { + + AnnouncementConverter INSTANCE = Mappers.getMapper(AnnouncementConverter.class); + + /** + * PO 转 PageVO + * @param announcement 公告表 + * @return AnnouncementPageVO 公告表PageVO + */ + AnnouncementPageVO poToPageVo(Announcement announcement); + + /** + * AnnouncementDTO 转 Announcement实体 + * @param dto AnnouncementDTO + * @return Announcement + */ + @Mapping(target = "updateTime", ignore = true) + @Mapping(target = "createTime", ignore = true) + @Mapping(target = "createBy", ignore = true) + Announcement dtoToPo(AnnouncementDTO dto); + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/converter/NotifyInfoConverter.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/converter/NotifyInfoConverter.java new file mode 100644 index 0000000..5a2e725 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/converter/NotifyInfoConverter.java @@ -0,0 +1,24 @@ +package com.baiye.notify.converter; + +import com.baiye.notify.model.domain.AnnouncementNotifyInfo; +import com.baiye.notify.model.entity.Announcement; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Hccake 2020/12/23 + * @version 1.0 + */ +@Mapper +public interface NotifyInfoConverter { + + NotifyInfoConverter INSTANCE = Mappers.getMapper(NotifyInfoConverter.class); + + /** + * 公告转通知实体 + * @param announcement 公告信息 + * @return 通知信息 + */ + AnnouncementNotifyInfo fromAnnouncement(Announcement announcement); + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/converter/UserAnnouncementConverter.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/converter/UserAnnouncementConverter.java new file mode 100644 index 0000000..cb3d0c5 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/converter/UserAnnouncementConverter.java @@ -0,0 +1,25 @@ +package com.baiye.notify.converter; + +import com.baiye.notify.model.entity.UserAnnouncement; +import com.baiye.notify.model.vo.UserAnnouncementPageVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 用户公告表 + * + * @author hccake 2021-03-22 20:16:12 + */ +@Mapper +public interface UserAnnouncementConverter { + + UserAnnouncementConverter INSTANCE = Mappers.getMapper(UserAnnouncementConverter.class); + + /** + * PO 转 PageVO + * @param userAnnouncement 用户公告表 + * @return UserAnnouncementPageVO 用户公告表PageVO + */ + UserAnnouncementPageVO poToPageVo(UserAnnouncement userAnnouncement); + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/enums/AnnouncementStatusEnum.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/enums/AnnouncementStatusEnum.java new file mode 100644 index 0000000..e699296 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/enums/AnnouncementStatusEnum.java @@ -0,0 +1,33 @@ +package com.baiye.notify.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 公告状态 + * + * @author Hccake 2020/12/17 + * @version 1.0 + */ +@Getter +@AllArgsConstructor +public enum AnnouncementStatusEnum { + + /** + * 关闭的 + */ + DISABLED(0), + + /** + * 开启的 + */ + ENABLED(1), + + /** + * 待发布 + */ + UNPUBLISHED(2); + + private final int value; + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/enums/NotifyChannelEnum.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/enums/NotifyChannelEnum.java new file mode 100644 index 0000000..4685f82 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/enums/NotifyChannelEnum.java @@ -0,0 +1,25 @@ +package com.baiye.notify.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 通知接收方式 + * + * @author Hccake 2020/12/21 + * @version 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum NotifyChannelEnum { + + // 站内 + STATION(1), + // 短信 + SMS(2), + // 邮件 + MAIL(3); + + private final int value; + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/enums/NotifyRecipientFilterTypeEnum.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/enums/NotifyRecipientFilterTypeEnum.java new file mode 100644 index 0000000..ada50a3 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/enums/NotifyRecipientFilterTypeEnum.java @@ -0,0 +1,29 @@ +package com.baiye.notify.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 通知接收者筛选类型 + * + * @author Hccake 2020/12/21 + * @version 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum NotifyRecipientFilterTypeEnum { + + // 全部 + ALL(1), + // 指定用户 + SPECIFY_ROLE(2), + // 指定组织 + SPECIFY_ORGANIZATION(3), + // 指定用户类型 + SPECIFY_USER_TYPE(4), + // 指定用户 + SPECIFY_USER(5),; + + private final int value; + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/enums/UserAnnouncementStateEnum.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/enums/UserAnnouncementStateEnum.java new file mode 100644 index 0000000..72c93d1 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/enums/UserAnnouncementStateEnum.java @@ -0,0 +1,27 @@ +package com.baiye.notify.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 用户公告状态 + * + * @author Hccake 2021/1/19 + * @version 1.0 + */ +@Getter +@AllArgsConstructor +public enum UserAnnouncementStateEnum { + + /** + * 未读 + */ + UNREAD(0), + /** + * 已读 + */ + READ(1); + + private final int value; + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/domain/AnnouncementNotifyInfo.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/domain/AnnouncementNotifyInfo.java new file mode 100644 index 0000000..8bbd234 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/domain/AnnouncementNotifyInfo.java @@ -0,0 +1,106 @@ +package com.baiye.notify.model.domain; + +import com.baiye.notify.enums.NotifyChannelEnum; +import com.baiye.notify.enums.NotifyRecipientFilterTypeEnum; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 公告通知信息 + * + * @author Hccake 2020/12/23 + * @version 1.0 + */ +@Data +@Accessors(chain = true) +@Schema(title = "公告通知信息") +public class AnnouncementNotifyInfo implements NotifyInfo { + + /** + * ID + */ + @TableId(type = IdType.INPUT) + @Schema(title = "公告ID") + private Long id; + + /** + * 标题 + */ + @Schema(title = "标题") + private String title; + + + /** + * 公告类型 + */ + @Schema(title = "公告类型") + private Integer messageType; + + + /** + * 状态,已读(1)|未读(0) + */ + @Schema(title = "状态,已读(1)|未读(0)") + private Integer state; + + + /** + * 关键信息 + */ + @Schema(title = "关键信息") + private String messageKey; + + + /** + * 内容 + */ + @Schema(title = "内容") + private String content; + + /** + * 接收人筛选方式 + * @see NotifyRecipientFilterTypeEnum + */ + @Schema(title = "接收人筛选方式") + private Integer recipientFilterType; + + /** + * 对应接收人筛选方式的条件信息 + */ + @Schema(title = "对应接收人筛选方式的条件信息") + private List recipientFilterCondition; + + /** + * 接收方式,值与通知渠道一一对应 + * @see NotifyChannelEnum + */ + @Schema(title = "接收方式") + private List receiveMode; + + /** + * 永久有效的 + * @see com.baiye.ballcat.common.core.constant.enums.BooleanEnum + */ + @Schema(title = "永久有效的") + private Integer immortal; + + /** + * 截止日期 + */ + @Schema(title = "截止日期") + private LocalDateTime deadline; + + + /** + * 拉取时间 + */ + @Schema(title = "创建时间") + private LocalDateTime createTime; + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/domain/NotifyInfo.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/domain/NotifyInfo.java new file mode 100644 index 0000000..d7c0b26 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/domain/NotifyInfo.java @@ -0,0 +1,46 @@ +package com.baiye.notify.model.domain; + +import com.baiye.notify.enums.NotifyChannelEnum; +import com.baiye.notify.enums.NotifyRecipientFilterTypeEnum; + +import java.util.List; + +/** + * @author Hccake 2020/12/23 + * @version 1.0 + */ +public interface NotifyInfo { + + /** + * 标题 + * @return String 当前通知标题 + */ + String getTitle(); + + /** + * 内容 + * @return String 当前通知内容 + */ + String getContent(); + + /** + * 接收人筛选方式 + * @see NotifyRecipientFilterTypeEnum + * @return Integer 接收人筛选方式 + */ + Integer getRecipientFilterType(); + + /** + * 对应接收人筛选方式的条件信息 + * @return List + */ + List getRecipientFilterCondition(); + + /** + * 接收方式,值与通知渠道一一对应 + * @see NotifyChannelEnum + * @return List + */ + List getReceiveMode(); + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/dto/AnnouncementDTO.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/dto/AnnouncementDTO.java new file mode 100644 index 0000000..65821cc --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/dto/AnnouncementDTO.java @@ -0,0 +1,85 @@ +package com.baiye.notify.model.dto; + +import com.baiye.notify.enums.NotifyChannelEnum; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 公告信息 + * + * @author hccake 2020-12-15 17:01:15 + */ +@Data +@Schema(title = "公告信息") +public class AnnouncementDTO implements Serializable { + + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(title = "ID") + private Long id; + + /** + * 标题 + */ + @NotBlank(message = "标题不能为空") + @Schema(title = "标题") + private String title; + + /** + * 内容 + */ + @NotBlank(message = "内容不能为空") + @Schema(title = "内容") + private String content; + + /** + * 接收人筛选方式,1:全部 2:用户角色 3:组织机构 4:用户类型 5:自定义用户 + */ + @NotNull(message = "接收人范围不能为空") + @Schema(title = "接收人范围") + private Integer recipientFilterType; + + /** + * 对应接收人筛选方式的条件信息,多个用逗号分割。如角色标识,组织ID,用户类型,用户ID等 + */ + @Schema(title = "对应接收人筛选方式的条件信息。如角色标识,组织ID,用户类型,用户ID等") + private List recipientFilterCondition; + + /** + * 接收方式,值与通知渠道一一对应 + * @see NotifyChannelEnum + */ + @Schema(title = "接收方式") + private List receiveMode; + + /** + * 永久有效的 + * @see com.baiye.ballcat.common.core.constant.enums.BooleanEnum + */ + @Schema(title = "永久有效的") + private Integer immortal; + + /** + * 截止日期 + */ + @Schema(title = "截止日期") + private LocalDateTime deadline; + + /** + * 状态 + */ + @Schema(title = "状态") + private Integer status; + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/entity/Announcement.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/entity/Announcement.java new file mode 100644 index 0000000..526e7d4 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/entity/Announcement.java @@ -0,0 +1,104 @@ +package com.baiye.notify.model.entity; + +import com.baiye.entity.LogicDeletedBaseEntity; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.baiye.notify.enums.AnnouncementStatusEnum; +import com.baiye.notify.enums.NotifyChannelEnum; +import com.baiye.notify.enums.NotifyRecipientFilterTypeEnum; +import com.baiye.extend.mybatis.plus.alias.TableAlias; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 公告信息 + * + * @author hccake 2020-12-15 17:01:15 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableAlias(Announcement.TABLE_ALIAS) +@TableName(value = "notify_announcement", autoResultMap = true) +@Schema(title = "公告信息") +public class Announcement extends LogicDeletedBaseEntity { + + private static final long serialVersionUID = 1L; + + public static final String TABLE_ALIAS = "a"; + + /** + * ID + */ + @TableId + @Schema(title = "ID") + private Long id; + + /** + * 标题 + */ + @Schema(title = "标题") + private String title; + + /** + * 内容 + */ + @Schema(title = "内容") + private String content; + + /** + * 接收人筛选方式 + * @see NotifyRecipientFilterTypeEnum + */ + @Schema(title = "接收人筛选方式") + private Integer recipientFilterType; + + + /** + * 公告类型 + */ + @Schema(title = "公告类型") + private Integer type; + + /** + * 对应接收人筛选方式的条件信息,多个用逗号分割。如角色标识,组织ID,用户类型,用户ID等 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + @Schema(title = "对应接收人筛选方式的条件信息。如角色标识,组织ID,用户类型,用户ID等") + private List recipientFilterCondition; + + /** + * 接收方式,值与通知渠道一一对应 + * @see NotifyChannelEnum + */ + @TableField(typeHandler = JacksonTypeHandler.class) + @Schema(title = "接收方式") + private List receiveMode; + + /** + * 状态 + * @see AnnouncementStatusEnum + */ + @Schema(title = "状态") + private Integer status; + + /** + * 永久有效的 + * @see com.baiye.ballcat.common.core.constant.enums.BooleanEnum + */ + @Schema(title = "永久有效的") + private Integer immortal; + + /** + * 截止日期 + */ + @Schema(title = "截止日期") + private LocalDateTime deadline; + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/entity/UserAnnouncement.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/entity/UserAnnouncement.java new file mode 100644 index 0000000..1b6fd14 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/entity/UserAnnouncement.java @@ -0,0 +1,96 @@ +package com.baiye.notify.model.entity; + +import com.baiye.extend.mybatis.plus.alias.TableAlias; +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 用户公告表 + * + * @author hccake 2020-12-25 08:04:53 + */ +@Data +@TableAlias(UserAnnouncement.TABLE_ALIAS) +@TableName("tb_notify_user_announcement") +@Schema(title = "用户公告表") +public class UserAnnouncement implements Serializable { + + private static final long serialVersionUID = 1L; + + + public static final String TABLE_ALIAS = "ua"; + + /** + * ID + */ + @TableId("id") + @Schema(title = "ID") + private Long id; + + /** + * 公告id + */ + @Schema(title = "公告id") + private Long announcementId; + + /** + * 公告类型 + */ + @Schema(title = "公告类型") + private Integer messageType; + + + /** + * 用户ID + */ + @Schema(title = "用户ID") + private Long userId; + + + /** + * 关键信息 + */ + @Schema(title = "关键信息") + private String messageKey; + + + /** + * 标题 + */ + @Schema(title = "标题") + private String title; + + /** + * 内容 + */ + @Schema(title = "内容") + private String content; + + /** + * 状态,已读(1)|未读(0) + */ + @Schema(title = "状态,已读(1)|未读(0)") + @TableField("state") + private Integer state; + + /** + * 阅读时间 + */ + @Schema(title = "阅读时间") + private LocalDateTime readTime; + + /** + * 拉取时间 + */ + @TableField(fill = FieldFill.INSERT) + @Schema(title = "拉取时间") + private LocalDateTime createTime; + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/qo/AnnouncementQO.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/qo/AnnouncementQO.java new file mode 100644 index 0000000..0fdc5bb --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/qo/AnnouncementQO.java @@ -0,0 +1,37 @@ +package com.baiye.notify.model.qo; + +import com.baiye.notify.enums.NotifyRecipientFilterTypeEnum; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +/** + * 公告信息 查询对象 + * + * @author hccake 2020-12-15 17:01:15 + */ +@Data +@ParameterObject +@Schema(title = "公告信息查询对象") +public class AnnouncementQO { + + private static final long serialVersionUID = 1L; + + /** + * 标题 + */ + @Parameter(description = "标题") + private String title; + + /** + * 接收人筛选方式 + * @see NotifyRecipientFilterTypeEnum + */ + @Parameter(description = "接收人筛选方式") + private Integer recipientFilterType; + + @Parameter(description = "状态") + private Integer[] status; + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/qo/UserAnnouncementQO.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/qo/UserAnnouncementQO.java new file mode 100644 index 0000000..cb44472 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/qo/UserAnnouncementQO.java @@ -0,0 +1,44 @@ +package com.baiye.notify.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +/** + * 用户公告表 查询对象 + * + * @author hccake 2020-12-25 08:04:53 + */ +@Data +@Schema(title = "用户公告表查询对象") +@ParameterObject +public class UserAnnouncementQO { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Parameter(description = "ID") + private Long id; + + /** + * ID + */ + @Parameter(description = "ID") + private Long userId; + + + @Parameter(description = "创建人") + private String content; + + + @Parameter(description = "开始时间") + private String startTime; + + @Parameter(description = "结束时间") + private String endTime; + + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/vo/AnnouncementPageVO.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/vo/AnnouncementPageVO.java new file mode 100644 index 0000000..8dcbbd9 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/vo/AnnouncementPageVO.java @@ -0,0 +1,110 @@ +package com.baiye.notify.model.vo; + +import com.baiye.notify.enums.AnnouncementStatusEnum; +import com.baiye.notify.enums.NotifyChannelEnum; +import com.baiye.notify.enums.NotifyRecipientFilterTypeEnum; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 公告信息 + * + * @author hccake 2020-12-15 17:01:15 + */ +@Data +@Schema(title = "公告信息分页VO") +public class AnnouncementPageVO { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(title = "ID") + private Long id; + + /** + * 标题 + */ + @Schema(title = "标题") + private String title; + + /** + * 内容 + */ + @Schema(title = "内容") + private String content; + + /** + * 接收人筛选方式,1:全部 2:用户角色 3:组织机构 4:用户类型 5:自定义用户 + * @see NotifyRecipientFilterTypeEnum + */ + @Schema(title = "接收人筛选方式") + private Integer recipientFilterType; + + /** + * 对应接收人筛选方式的条件信息,多个用逗号分割。如角色标识,组织ID,用户类型,用户ID等 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + @Schema(title = "对应接收人筛选方式的条件信息。如角色标识,组织ID,用户类型,用户ID等") + private List recipientFilterCondition; + + /** + * 接收方式,值与通知渠道一一对应 + * @see NotifyChannelEnum + */ + @TableField(typeHandler = JacksonTypeHandler.class) + @Schema(title = "接收方式") + private List receiveMode; + + /** + * 状态 + * @see AnnouncementStatusEnum + */ + @Schema(title = "状态") + private Integer status; + + /** + * 永久有效的 + * @see com.baiye.ballcat.common.core.constant.enums.BooleanEnum + */ + @Schema(title = "永久有效的") + private Integer immortal; + + /** + * 截止日期 + */ + @Schema(title = "截止日期") + private LocalDateTime deadline; + + /** + * 创建人ID + */ + @Schema(title = "创建人ID") + private Integer createBy; + + /** + * 创建人名称 + */ + @Schema(title = "创建人名称") + private String createUsername; + + /** + * 创建时间 + */ + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @Schema(title = "更新时间") + private LocalDateTime updateTime; + +} diff --git a/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/vo/UserAnnouncementPageVO.java b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/vo/UserAnnouncementPageVO.java new file mode 100644 index 0000000..8e80df2 --- /dev/null +++ b/marketing-scrm-notify/distribute-notify-model/src/main/java/com/baiye/notify/model/vo/UserAnnouncementPageVO.java @@ -0,0 +1,84 @@ +package com.baiye.notify.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 用户公告表 + * + * @author hccake 2020-12-25 08:04:53 + */ +@Data +@Schema(title = "用户公告分页VO") +public class UserAnnouncementPageVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(title = "ID") + private Long id; + + /** + * 公告id + */ + @Schema(title = "公告id") + private Long announcementId; + + /** + * 用户ID + */ + @Schema(title = "用户ID") + private Long userId; + + /** + * 状态,已读(1)|未读(0) + */ + @Schema(title = "状态,已读(1)|未读(0)") + private Integer state; + + /** + * 标题 + */ + @Schema(title = "关键信息") + private String messageKey; + + /** + * 公告类型 + */ + @Schema(title = "消息类型") + private Integer messageType; + + /** + * 标题 + */ + @Schema(title = "标题") + private String title; + + /** + * 内容 + */ + @Schema(title = "内容") + private String content; + + + + + /** + * 阅读时间 + */ + @Schema(title = "阅读时间") + private LocalDateTime readTime; + + /** + * 拉取时间 + */ + @Schema(title = "拉取时间") + private LocalDateTime createTime; + +} diff --git a/marketing-scrm-notify/pom.xml b/marketing-scrm-notify/pom.xml new file mode 100644 index 0000000..5b94692 --- /dev/null +++ b/marketing-scrm-notify/pom.xml @@ -0,0 +1,18 @@ + + + + com.baiye + marketing-scrm + ${revision} + + 4.0.0 + marketing-scrm-notify + pom + + + distribute-notify-biz + distribute-notify-controller + distribute-notify-model + + diff --git a/marketing-scrm-security/pom.xml b/marketing-scrm-security/pom.xml new file mode 100644 index 0000000..044dc34 --- /dev/null +++ b/marketing-scrm-security/pom.xml @@ -0,0 +1,25 @@ + + + + com.baiye + marketing-scrm + ${revision} + + 4.0.0 + marketing-scrm-security + pom + + + security-core + security-oauth2-authorization-server + security-oauth2-core + security-oauth2-resource-server + + + + org.projectlombok + lombok + + + diff --git a/marketing-scrm-security/security-core/pom.xml b/marketing-scrm-security/security-core/pom.xml new file mode 100644 index 0000000..1394892 --- /dev/null +++ b/marketing-scrm-security/security-core/pom.xml @@ -0,0 +1,24 @@ + + + + marketing-scrm-security + com.baiye + ${revision} + + 4.0.0 + security-core + + + + jakarta.servlet + jakarta.servlet-api + compile + + + org.springframework.boot + spring-boot + compile + + + diff --git a/marketing-scrm-security/security-core/src/main/java/org/ballcat/security/captcha/CaptchaValidateResult.java b/marketing-scrm-security/security-core/src/main/java/org/ballcat/security/captcha/CaptchaValidateResult.java new file mode 100644 index 0000000..2c8be13 --- /dev/null +++ b/marketing-scrm-security/security-core/src/main/java/org/ballcat/security/captcha/CaptchaValidateResult.java @@ -0,0 +1,46 @@ +package org.ballcat.security.captcha; + +import lombok.Getter; +import lombok.experimental.Accessors; + +/** + * 验证码的校验结果 + * + * @author hccake + */ +@Getter +@Accessors(chain = true) +public class CaptchaValidateResult { + + /** + * 是否成功 + */ + private final boolean success; + + /** + * 信息 + */ + private final String message; + + public CaptchaValidateResult(boolean success, String message) { + this.success = success; + this.message = message; + } + + public static CaptchaValidateResult success() { + return success("success"); + } + + public static CaptchaValidateResult success(String successMessage) { + return new CaptchaValidateResult(true, successMessage); + } + + public static CaptchaValidateResult failure() { + return failure("captcha validate failure"); + } + + public static CaptchaValidateResult failure(String failureMessage) { + return new CaptchaValidateResult(false, failureMessage); + } + +} diff --git a/marketing-scrm-security/security-core/src/main/java/org/ballcat/security/captcha/IValidateCodeService.java b/marketing-scrm-security/security-core/src/main/java/org/ballcat/security/captcha/IValidateCodeService.java new file mode 100644 index 0000000..9c32c87 --- /dev/null +++ b/marketing-scrm-security/security-core/src/main/java/org/ballcat/security/captcha/IValidateCodeService.java @@ -0,0 +1,24 @@ +package org.ballcat.security.captcha; + +import java.io.IOException; +import java.util.Map; + +public interface IValidateCodeService { + + /** + * 生成验证码 + * + * @return + * @throws IOException + */ + @SuppressWarnings("rawtypes") + Map createCapcha() throws IOException; + + /** + * 校验验证码 + * + * @param key + * @param value + */ + void checkCapcha(String key, String value); +} diff --git a/marketing-scrm-security/security-core/src/main/java/org/ballcat/security/properties/SecurityProperties.java b/marketing-scrm-security/security-core/src/main/java/org/ballcat/security/properties/SecurityProperties.java new file mode 100644 index 0000000..11c2d36 --- /dev/null +++ b/marketing-scrm-security/security-core/src/main/java/org/ballcat/security/properties/SecurityProperties.java @@ -0,0 +1,24 @@ +package org.ballcat.security.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Hccake + * @version 1.0 + * @date 2020/2/18 10:55 安全相关配置 + */ +@Getter +@Setter +@ConfigurationProperties(prefix = SecurityProperties.PREFIX) +public class SecurityProperties { + + public static final String PREFIX = "ballcat.security"; + + /** + * 前后端交互使用的对称加密算法的密钥,必须 16 位字符 + */ + private String passwordSecretKey; + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/pom.xml b/marketing-scrm-security/security-oauth2-authorization-server/pom.xml new file mode 100644 index 0000000..ac325af --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/pom.xml @@ -0,0 +1,54 @@ + + + + marketing-scrm-security + com.baiye + ${revision} + + 4.0.0 + security-oauth2-authorization-server + + + + com.baiye + common-core + ${revision} + + + com.baiye + security-core + ${revision} + + + com.baiye + security-oauth2-core + ${revision} + + + jakarta.servlet + jakarta.servlet-api + compile + + + + org.slf4j + slf4j-api + + + + org.springframework.security + spring-security-oauth2-authorization-server + + + org.springframework.boot + spring-boot-starter-jdbc + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/OAuth2AuthorizationObjectMapperCustomizer.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/OAuth2AuthorizationObjectMapperCustomizer.java new file mode 100644 index 0000000..a9126cf --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/OAuth2AuthorizationObjectMapperCustomizer.java @@ -0,0 +1,15 @@ +package org.ballcat.springsecurity.oauth2.server.authorization; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * 用于序列化 OAuth2Authorization 的专用 ObjectMapper 定制器 + * + * @author hccake + */ +@FunctionalInterface +public interface OAuth2AuthorizationObjectMapperCustomizer { + + void customize(ObjectMapper objectMapper); + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/annotation/EnableOauth2AuthorizationServer.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/annotation/EnableOauth2AuthorizationServer.java new file mode 100644 index 0000000..a1651b8 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/annotation/EnableOauth2AuthorizationServer.java @@ -0,0 +1,20 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.annotation; + +import org.ballcat.springsecurity.oauth2.server.authorization.autoconfigure.OAuth2AuthorizationServerAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; + +import java.lang.annotation.*; + +/** + * 开启 Oauth2 授权服务器 + * + * @author hccake + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ImportAutoConfiguration(OAuth2AuthorizationServerAutoConfiguration.class) +public @interface EnableOauth2AuthorizationServer { + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/AbstractOAuth2ResourceOwnerAuthenticationProvider.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/AbstractOAuth2ResourceOwnerAuthenticationProvider.java new file mode 100644 index 0000000..0ec6cd2 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/AbstractOAuth2ResourceOwnerAuthenticationProvider.java @@ -0,0 +1,183 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.authentication; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.*; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; +import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.security.Principal; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient; + +/** + * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Resource Owner + * + * @author Hccake + */ +@Slf4j +public abstract class AbstractOAuth2ResourceOwnerAuthenticationProvider + implements AuthenticationProvider { + + private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; + + private final OAuth2AuthorizationService authorizationService; + + private final OAuth2TokenGenerator tokenGenerator; + + /** + * Constructs an {@code AbstractOAuth2ResourceOwnerAuthenticationProvider} using the + * provided parameters. + * @param authorizationService the authorization service + * @param tokenGenerator the token generator + * @since 1.0.0 + */ + protected AbstractOAuth2ResourceOwnerAuthenticationProvider(OAuth2AuthorizationService authorizationService, + OAuth2TokenGenerator tokenGenerator) { + Assert.notNull(authorizationService, "authorizationService cannot be null"); + Assert.notNull(tokenGenerator, "tokenGenerator cannot be null"); + this.authorizationService = authorizationService; + this.tokenGenerator = tokenGenerator; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + + @SuppressWarnings("unchecked") + T resourceOwnerAuthentication = (T) authentication; + + OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient( + resourceOwnerAuthentication); + + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + if (registeredClient == null) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } + + AuthorizationGrantType grantType = resourceOwnerAuthentication.getGrantType(); + if (!registeredClient.getAuthorizationGrantTypes().contains(grantType)) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); + } + + // 获取认证后的 Authentication + Authentication authenticatedAuthentication; + try { + authenticatedAuthentication = getAuthenticatedAuthentication(resourceOwnerAuthentication); + } + catch (AuthenticationException ex) { + log.error("OAuth2 authentication error by grant type: " + grantType.getValue() + ", reason: " + + ex.getMessage()); + throw new OAuth2AuthenticationException(ex.getMessage()); + } + + // Default to configured scopes + Set authorizedScopes = registeredClient.getScopes(); + Set requestedScopes = resourceOwnerAuthentication.getScopes(); + if (!CollectionUtils.isEmpty(requestedScopes)) { + Set unauthorizedScopes = requestedScopes.stream() + .filter(requestedScope -> !registeredClient.getScopes().contains(requestedScope)) + .collect(Collectors.toSet()); + if (!CollectionUtils.isEmpty(unauthorizedScopes)) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE); + } + + authorizedScopes = new LinkedHashSet<>(requestedScopes); + } + + DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .principal(authenticatedAuthentication) + .authorizationServerContext(AuthorizationServerContextHolder.getContext()) + .authorizedScopes(authorizedScopes) + .authorizationGrantType(grantType) + .authorizationGrant(resourceOwnerAuthentication); + + OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) + .principalName(authenticatedAuthentication.getName()) + .authorizationGrantType(grantType) + .authorizedScopes(authorizedScopes) + .attribute(Principal.class.getName(), authenticatedAuthentication); + + // ----- Access token ----- + OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build(); + OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext); + if (generatedAccessToken == null) { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, + "The token generator failed to generate the access token.", ERROR_URI); + throw new OAuth2AuthenticationException(error); + } + + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), + generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); + + if (generatedAccessToken instanceof ClaimAccessor) { + authorizationBuilder.token(accessToken, + metadata -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, + ((ClaimAccessor) generatedAccessToken).getClaims())); + } + else { + authorizationBuilder.accessToken(accessToken); + } + + // ----- Refresh token ----- + OAuth2RefreshToken refreshToken = null; + if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) && + // Do not issue refresh token to public client + !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) { + + tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build(); + OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext); + if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, + "The token generator failed to generate the refresh token.", ERROR_URI); + throw new OAuth2AuthenticationException(error); + } + refreshToken = (OAuth2RefreshToken) generatedRefreshToken; + authorizationBuilder.refreshToken(refreshToken); + } + + OAuth2Authorization authorization = authorizationBuilder.build(); + + this.authorizationService.save(authorization); + log.debug("OAuth2Authorization saved successfully, then returning OAuth2AccessTokenAuthenticationToken"); + + // 切换当前 Authentication 为 User + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authenticatedAuthentication); + SecurityContextHolder.setContext(context); + + Map additionalParameters = new HashMap<>(8); + return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, + additionalParameters); + } + + /** + * 根据请求的 authentication 转换为认证后的 authentication + * @param authentication 认证前的 authentication + * @return 认证后的 authentication + * @throws AuthenticationException 当认证校验不通过时进行异常抛出 + */ + protected abstract Authentication getAuthenticatedAuthentication(T authentication) throws AuthenticationException; + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/AbstractOAuth2ResourceOwnerAuthenticationToken.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/AbstractOAuth2ResourceOwnerAuthenticationToken.java new file mode 100644 index 0000000..e1fd31a --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/AbstractOAuth2ResourceOwnerAuthenticationToken.java @@ -0,0 +1,50 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.authentication; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.lang.Nullable; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * An abstract {@link Authentication} implementation used for the OAuth 2.0 Resource Owner + * Grant. + * + * @author Hccake + * @since 1.0.0 + * @see OAuth2AuthorizationGrantAuthenticationToken + * @see OAuth2ResourceOwnerPasswordAuthenticationProvider + */ +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AbstractOAuth2ResourceOwnerAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { + + private final Set scopes; + + /** + * Constructs an {@code OAuth2ClientCredentialsAuthenticationToken} using the provided + * parameters. + * @param clientPrincipal the authenticated client principal + */ + public AbstractOAuth2ResourceOwnerAuthenticationToken(AuthorizationGrantType authorizationGrantType, + Authentication clientPrincipal, @Nullable Map additionalParameters, + @Nullable Set scopes) { + super(authorizationGrantType, clientPrincipal, additionalParameters); + this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet()); + } + + /** + * Returns the requested scope(s). + * @return the requested scope(s), or an empty {@code Set} if not available + */ + public Set getScopes() { + return this.scopes; + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2AuthenticationProviderUtils.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2AuthenticationProviderUtils.java new file mode 100644 index 0000000..59a5569 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2AuthenticationProviderUtils.java @@ -0,0 +1,84 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ballcat.springsecurity.oauth2.server.authorization.authentication; + +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; + +/** + * Utility methods for the OAuth 2.0 {@link AuthenticationProvider}'s. + * + * @author Joe Grandja + * @author hccake + * @since 0.0.3 + */ +public final class OAuth2AuthenticationProviderUtils { + + private OAuth2AuthenticationProviderUtils() { + } + + public static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient( + Authentication authentication) { + if (authentication == null) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } + if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getClass())) { + return (OAuth2ClientAuthenticationToken) authentication; + } + OAuth2ClientAuthenticationToken clientPrincipal = null; + if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) { + clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal(); + } + if (clientPrincipal != null && clientPrincipal.isAuthenticated()) { + return clientPrincipal; + } + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } + + static OAuth2Authorization invalidate(OAuth2Authorization authorization, T token) { + + // @formatter:off + OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization) + .token(token, metadata -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true)); + + if (OAuth2RefreshToken.class.isAssignableFrom(token.getClass())) { + authorizationBuilder.token( + authorization.getAccessToken().getToken(), + metadata -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true) + ); + + OAuth2Authorization.Token authorizationCode = + authorization.getToken(OAuth2AuthorizationCode.class); + if (authorizationCode != null && !authorizationCode.isInvalidated()) { + authorizationBuilder.token( + authorizationCode.getToken(), + metadata -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true) + ); + } + } + // @formatter:on + + return authorizationBuilder.build(); + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2ResourceOwnerPasswordAuthenticationProvider.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2ResourceOwnerPasswordAuthenticationProvider.java new file mode 100644 index 0000000..b942927 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2ResourceOwnerPasswordAuthenticationProvider.java @@ -0,0 +1,70 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.authentication; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.util.Assert; + +import java.util.Map; + +/** + * @author Hccake + */ +@Slf4j +public class OAuth2ResourceOwnerPasswordAuthenticationProvider + extends AbstractOAuth2ResourceOwnerAuthenticationProvider { + + private final DaoAuthenticationProvider daoAuthenticationProvider; + + /** + * Constructs an {@code OAuth2ResourceOwnerPasswordAuthenticationProviderNew} using + * the provided parameters. + * @param userDetailsService the userDetails service + * @param authorizationService the authorization service + * @param tokenGenerator the token generator + * @since 1.0.0 + */ + public OAuth2ResourceOwnerPasswordAuthenticationProvider(OAuth2AuthorizationService authorizationService, + OAuth2TokenGenerator tokenGenerator, UserDetailsService userDetailsService) { + super(authorizationService, tokenGenerator); + Assert.notNull(userDetailsService, "userDetailsService cannot be null"); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService); + this.daoAuthenticationProvider = provider; + } + + public void setPasswordEncoder(PasswordEncoder passwordEncoder) { + Assert.notNull(passwordEncoder, "passwordEncoder cannot be null"); + this.daoAuthenticationProvider.setPasswordEncoder(passwordEncoder); + } + + @Override + public boolean supports(Class authentication) { + boolean supports = OAuth2ResourceOwnerPasswordAuthenticationToken.class.isAssignableFrom(authentication); + log.debug("supports authentication={}} returning {}", authentication, supports); + return supports; + } + + @Override + protected Authentication getAuthenticatedAuthentication( + OAuth2ResourceOwnerPasswordAuthenticationToken resourceOwnerPasswordAuthentication) { + Map additionalParameters = resourceOwnerPasswordAuthentication.getAdditionalParameters(); + + String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME); + String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD); + + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( + username, password); + log.debug("got usernamePasswordAuthenticationToken={}", usernamePasswordAuthenticationToken); + + return daoAuthenticationProvider.authenticate(usernamePasswordAuthenticationToken); + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2ResourceOwnerPasswordAuthenticationToken.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2ResourceOwnerPasswordAuthenticationToken.java new file mode 100644 index 0000000..9f884ee --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2ResourceOwnerPasswordAuthenticationToken.java @@ -0,0 +1,45 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.authentication; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.lang.Nullable; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.Assert; + +import java.util.Map; +import java.util.Set; + +/** + * An {@link Authentication} implementation used for the OAuth 2.0 Resource Owner Password + * Grant. + * + * @author Hccake + * @since 1.0.0 + * @see AbstractOAuth2ResourceOwnerAuthenticationToken + * @see OAuth2ResourceOwnerPasswordAuthenticationProvider + */ +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ResourceOwnerPasswordAuthenticationToken extends AbstractOAuth2ResourceOwnerAuthenticationToken { + + private final String username; + + /** + * Constructs an {@code OAuth2ClientCredentialsAuthenticationToken} using the provided + * parameters. + * @param clientPrincipal the authenticated client principal + */ + public OAuth2ResourceOwnerPasswordAuthenticationToken(String username, Authentication clientPrincipal, + @Nullable Map additionalParameters, @Nullable Set scopes) { + super(AuthorizationGrantType.PASSWORD, clientPrincipal, additionalParameters, scopes); + Assert.hasText(username, "username cannot be empty"); + this.username = username; + } + + @Override + public String getName() { + return username; + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationProvider.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationProvider.java new file mode 100644 index 0000000..6312155 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationProvider.java @@ -0,0 +1,100 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ballcat.springsecurity.oauth2.server.authorization.authentication; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.util.Assert; + +import static org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient; + +/** + * An {@link AuthenticationProvider} implementation for OAuth 2.0 Token Revocation. + * + * @author Vivek Babu + * @author Joe Grandja + * @since 0.0.3 + * @see OAuth2TokenRevocationAuthenticationToken + * @see OAuth2AuthorizationService + * @see Section + * 2.1 Revocation Request + */ +@Slf4j +public final class OAuth2TokenRevocationAuthenticationProvider implements AuthenticationProvider { + + private final OAuth2AuthorizationService authorizationService; + + /** + * Constructs an {@code OAuth2TokenRevocationAuthenticationProvider} using the + * provided parameters. + * @param authorizationService the authorization service + */ + public OAuth2TokenRevocationAuthenticationProvider(OAuth2AuthorizationService authorizationService) { + Assert.notNull(authorizationService, "authorizationService cannot be null"); + this.authorizationService = authorizationService; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + OAuth2TokenRevocationAuthenticationToken tokenRevocationAuthentication = (OAuth2TokenRevocationAuthenticationToken) authentication; + + OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient( + tokenRevocationAuthentication); + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + OAuth2Authorization authorization = this.authorizationService + .findByToken(tokenRevocationAuthentication.getToken(), null); + if (authorization == null) { + if (log.isTraceEnabled()) { + log.trace("Did not authenticate token revocation request since token was not found"); + } + // Return the authentication request when token not found + return tokenRevocationAuthentication; + } + + if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } + + OAuth2Authorization.Token token = authorization.getToken(tokenRevocationAuthentication.getToken()); + authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, token.getToken()); + this.authorizationService.save(authorization); + + if (log.isTraceEnabled()) { + log.trace("Saved authorization with revoked token"); + // This log is kept separate for consistency with other providers + log.trace("Authenticated token revocation request"); + } + + // 返回自定义的 token,携带上注销的 token 对应的 authorization + return new OAuth2TokenRevocationAuthenticationToken(authorization, token.getToken(), clientPrincipal); + } + + @Override + public boolean supports(Class authentication) { + return OAuth2TokenRevocationAuthenticationToken.class.isAssignableFrom(authentication); + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationToken.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationToken.java new file mode 100644 index 0000000..2b12715 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationToken.java @@ -0,0 +1,123 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ballcat.springsecurity.oauth2.server.authorization.authentication; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider; +import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion; +import org.springframework.util.Assert; + +import java.util.Collections; + +/** + * An {@link Authentication} implementation used for OAuth 2.0 Token Revocation. + * + * @author Vivek Babu + * @author Joe Grandja + * @author hccake + * @since 0.0.3 + * @see AbstractAuthenticationToken + * @see OAuth2TokenRevocationAuthenticationProvider + */ +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2TokenRevocationAuthenticationToken extends AbstractAuthenticationToken { + + private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID; + + private final OAuth2Authorization authorization; + + private final String token; + + private final Authentication clientPrincipal; + + private final String tokenTypeHint; + + /** + * Constructs an {@code OAuth2TokenRevocationAuthenticationToken} using the provided + * parameters. + * @param token the token + * @param clientPrincipal the authenticated client principal + * @param tokenTypeHint the token type hint + */ + public OAuth2TokenRevocationAuthenticationToken(String token, Authentication clientPrincipal, + @Nullable String tokenTypeHint) { + super(Collections.emptyList()); + Assert.hasText(token, "token cannot be empty"); + Assert.notNull(clientPrincipal, "clientPrincipal cannot be null"); + this.token = token; + this.clientPrincipal = clientPrincipal; + this.tokenTypeHint = tokenTypeHint; + this.authorization = null; + } + + /** + * Constructs an {@code OAuth2TokenRevocationAuthenticationToken} using the provided + * parameters. + * @param revokedToken the revoked token + * @param clientPrincipal the authenticated client principal + */ + public OAuth2TokenRevocationAuthenticationToken(OAuth2Authorization authorization, OAuth2Token revokedToken, + Authentication clientPrincipal) { + super(Collections.emptyList()); + Assert.notNull(authorization, "authorization cannot be null"); + Assert.notNull(revokedToken, "revokedToken cannot be null"); + Assert.notNull(clientPrincipal, "clientPrincipal cannot be null"); + this.authorization = authorization; + this.token = revokedToken.getTokenValue(); + this.clientPrincipal = clientPrincipal; + this.tokenTypeHint = null; + setAuthenticated(true); // Indicates that the token was authenticated and revoked + } + + @Override + public Object getPrincipal() { + return this.clientPrincipal; + } + + @Override + public Object getCredentials() { + return ""; + } + + /** + * Returns the token. + * @return the token + */ + public String getToken() { + return this.token; + } + + /** + * Returns the token type hint. + * @return the token type hint + */ + @Nullable + public String getTokenTypeHint() { + return this.tokenTypeHint; + } + + public OAuth2Authorization getAuthorization() { + return authorization; + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerAutoConfiguration.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerAutoConfiguration.java new file mode 100644 index 0000000..ce2fccc --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerAutoConfiguration.java @@ -0,0 +1,186 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.autoconfigure; + +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.baiye.security.authentication.OAuth2UserAuthenticationToken; +import com.baiye.security.jackson2.LongMixin; +import com.baiye.security.jackson2.OAuth2UserAuthenticationTokenMixin; +import com.baiye.security.jackson2.UserMixin; +import com.baiye.security.userdetails.User; +import com.baiye.security.util.PasswordUtils; +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.authorization.OAuth2AuthorizationObjectMapperCustomizer; +import org.ballcat.springsecurity.oauth2.server.authorization.config.BallcatOAuth2AuthorizationServerSecurityFilterChainBuilder; +import org.ballcat.springsecurity.oauth2.server.authorization.config.OAuth2AuthorizationServerSecurityFilterChainBuilder; +import org.ballcat.springsecurity.oauth2.server.authorization.config.configurer.OAuth2AuthorizationServerExtensionConfigurer; +import org.ballcat.springsecurity.oauth2.server.authorization.config.customizer.OAuth2AuthorizationServerConfigurerCustomizer; +import org.ballcat.springsecurity.oauth2.server.authorization.properties.OAuth2AuthorizationServerProperties; +import org.ballcat.springsecurity.oauth2.server.authorization.token.BallcatOAuth2TokenCustomizer; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2TokenRevocationResponseHandler; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.Order; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; +import org.springframework.security.web.SecurityFilterChain; + +import java.util.List; + +/** + * OAuth2授权服务器自动配置类 + * + * @author Hccake + */ +@Import({ OAuth2AuthorizationServerConfigurerCustomizerConfiguration.class, + OAuth2AuthorizationServerExtensionConfigurerConfiguration.class }) +@Configuration(proxyBeanMethods = false) +@RequiredArgsConstructor +@EnableConfigurationProperties(OAuth2AuthorizationServerProperties.class) +public class OAuth2AuthorizationServerAutoConfiguration { + + public static final String OAUTH2_AUTHORIZATION_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME = "oauth2AuthorizationServerSecurityFilterChain"; + + /** + * OAuth2AuthorizationServerConfigurer 的适配器 + * @param oAuth2AuthorizationServerConfigurerCustomizers + * OAuth2AuthorizationServerConfigurer 的定制器列表 + * @param oAuth2AuthorizationServerExtensionConfigurers + * oAuth2AuthorizationServerExtensionConfigurer 的配置扩展器列表 + * @return OAuth2AuthorizationServerConfigurerAdapter + */ + @Bean + @ConditionalOnMissingBean(name = OAUTH2_AUTHORIZATION_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME, + value = OAuth2AuthorizationServerSecurityFilterChainBuilder.class) + public OAuth2AuthorizationServerSecurityFilterChainBuilder oAuth2AuthorizationServerSecurityFilterChainBuilder( + List oAuth2AuthorizationServerConfigurerCustomizers, + List> oAuth2AuthorizationServerExtensionConfigurers) { + return new BallcatOAuth2AuthorizationServerSecurityFilterChainBuilder( + oAuth2AuthorizationServerConfigurerCustomizers, oAuth2AuthorizationServerExtensionConfigurers); + } + + /** + * OAuth2 授权服务器的安全过滤器链,如果和资源服务器共存,需要将其放在资源服务器之前 + */ + @Bean(name = OAUTH2_AUTHORIZATION_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME) + @Order(1) + @ConditionalOnMissingBean(name = OAUTH2_AUTHORIZATION_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME) + public SecurityFilterChain oauth2AuthorizationServerSecurityFilterChain( + OAuth2AuthorizationServerSecurityFilterChainBuilder builder, HttpSecurity httpSecurity) throws Exception { + return builder.build(httpSecurity); + } + + /** + * OAuth2 授权服务器中注册的 client 仓库 + */ + @Bean + @ConditionalOnMissingBean + public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { + return new JdbcRegisteredClientRepository(jdbcTemplate); + } + + /** + * OAuth2 授权管理Service + */ + @Bean + @ConditionalOnMissingBean + public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, + RegisteredClientRepository registeredClientRepository, + ObjectProvider objectMapperCustomizerObjectProvider) { + JdbcOAuth2AuthorizationService oAuth2AuthorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, + registeredClientRepository); + + // 需要注册自己的 mixin 来处理类型转换 + // link + // https://github.com/spring-projects/spring-authorization-server/issues/397#issuecomment-900148920 + JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper( + registeredClientRepository); + + ObjectMapper objectMapper = new ObjectMapper(); + ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader(); + List securityModules = SecurityJackson2Modules.getModules(classLoader); + objectMapper.registerModules(securityModules); + objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); + + // You will need to write the Mixin for your class so Jackson can marshall it. + objectMapper.addMixIn(Long.class, LongMixin.class); + objectMapper.addMixIn(User.class, UserMixin.class); + objectMapper.addMixIn(OAuth2UserAuthenticationToken.class, OAuth2UserAuthenticationTokenMixin.class); + + // 定制 objectMapper + objectMapperCustomizerObjectProvider.ifAvailable(customizer -> customizer.customize(objectMapper)); + + rowMapper.setObjectMapper(objectMapper); + + oAuth2AuthorizationService.setAuthorizationRowMapper(rowMapper); + + return oAuth2AuthorizationService; + } + + /** + * OAuth2AuthorizationConsentService + */ + @Bean + @ConditionalOnMissingBean + public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, + RegisteredClientRepository registeredClientRepository) { + return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository); + } + + /** + * 授权服务器基本端点地址配置 + * @return AuthorizationServerSettings + */ + @Bean + @ConditionalOnMissingBean + public AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().build(); + } + + /** + * OAuth2 Token 撤销响应处理器 + * @param publisher 事件发布器 + * @return OAuth2TokenRevocationResponseHandler + */ + @Bean + @ConditionalOnMissingBean + public OAuth2TokenRevocationResponseHandler oAuth2TokenRevocationResponseHandler( + ApplicationEventPublisher publisher) { + return new OAuth2TokenRevocationResponseHandler(publisher); + } + + /** + * 密码管理器 + */ + @Bean + @ConditionalOnMissingBean + public PasswordEncoder passwordEncoder() { + return PasswordUtils.createDelegatingPasswordEncoder(); + } + + /** + * 对于使用不透明令牌的 client,需要存储对应的用户信息,以便在后续的请求中获取用户信息 + */ + @Bean + @ConditionalOnMissingBean(OAuth2TokenCustomizer.class) + public OAuth2TokenCustomizer oAuth2TokenCustomizer() { + return new BallcatOAuth2TokenCustomizer(); + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerConfigurerCustomizerConfiguration.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerConfigurerCustomizerConfiguration.java new file mode 100644 index 0000000..a5976c4 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerConfigurerCustomizerConfiguration.java @@ -0,0 +1,81 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.autoconfigure; + +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.authorization.config.customizer.FormLoginConfigurerCustomizer; +import org.ballcat.springsecurity.oauth2.server.authorization.config.customizer.OAuth2ResourceOwnerPasswordConfigurerCustomizer; +import org.ballcat.springsecurity.oauth2.server.authorization.config.customizer.OAuth2TokenResponseEnhanceConfigurerCustomizer; +import org.ballcat.springsecurity.oauth2.server.authorization.config.customizer.OAuth2TokenRevocationEndpointConfigurerCustomizer; +import org.ballcat.springsecurity.oauth2.server.authorization.properties.OAuth2AuthorizationServerProperties; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2TokenResponseEnhancer; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2TokenRevocationResponseHandler; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; + +/** + * OAuth2 授权服务器配置定制器的配置类 + * + * @author Hccake + */ +@RequiredArgsConstructor +@Configuration(proxyBeanMethods = false) +public class OAuth2AuthorizationServerConfigurerCustomizerConfiguration { + + private final OAuth2AuthorizationServerProperties oAuth2AuthorizationServerProperties; + + private final OAuth2AuthorizationService oAuth2AuthorizationService; + + /** + * 表单登录支持 + * @return FormLoginConfigurerCustomizer + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = OAuth2AuthorizationServerProperties.PREFIX, name = "form-login-enabled", + havingValue = "true") + public FormLoginConfigurerCustomizer formLoginConfigurerCustomizer(UserDetailsService userDetailsService) { + return new FormLoginConfigurerCustomizer(oAuth2AuthorizationServerProperties, userDetailsService); + } + + /** + * 添加 resource owner password 模式支持配置定制器 + * @param applicationContext spring 容器 + * @return OAuth2ResourceOwnerPasswordConfigurerCustomizer + */ + @Bean + public OAuth2ResourceOwnerPasswordConfigurerCustomizer oAuth2ResourceOwnerPasswordConfigurerCustomizer( + ApplicationContext applicationContext) { + return new OAuth2ResourceOwnerPasswordConfigurerCustomizer(applicationContext); + } + + /** + * token endpoint 响应增强配置定制器 + * @param oauth2TokenResponseEnhancer OAuth2TokenResponseEnhancer + * @return OAuth2TokenResponseEnhanceConfigurerCustomizer + */ + @Bean + @ConditionalOnBean(OAuth2TokenResponseEnhancer.class) + public OAuth2TokenResponseEnhanceConfigurerCustomizer oAuth2TokenResponseEnhanceConfigurerCustomizer( + OAuth2TokenResponseEnhancer oauth2TokenResponseEnhancer) { + return new OAuth2TokenResponseEnhanceConfigurerCustomizer(oauth2TokenResponseEnhancer); + } + + /** + * token 撤销响应处理器配置定制器 + * @param oAuth2TokenRevocationResponseHandler token 撤销响应处理器 + * @return OAuth2TokenRevocationResponseHandler + */ + @Bean + @ConditionalOnBean(OAuth2TokenRevocationResponseHandler.class) + public OAuth2TokenRevocationEndpointConfigurerCustomizer oAuth2TokenRevocationEndpointConfigurerCustomizer( + OAuth2TokenRevocationResponseHandler oAuth2TokenRevocationResponseHandler) { + return new OAuth2TokenRevocationEndpointConfigurerCustomizer(oAuth2AuthorizationService, + oAuth2TokenRevocationResponseHandler); + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerExtensionConfigurerConfiguration.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerExtensionConfigurerConfiguration.java new file mode 100644 index 0000000..3bb9a84 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerExtensionConfigurerConfiguration.java @@ -0,0 +1,44 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.autoconfigure; + +import org.ballcat.security.captcha.IValidateCodeService; +import org.ballcat.security.properties.SecurityProperties; +import org.ballcat.springsecurity.oauth2.server.authorization.config.configurer.OAuth2LoginCaptchaConfigurer; +import org.ballcat.springsecurity.oauth2.server.authorization.config.configurer.OAuth2LoginPasswordDecoderConfigurer; +import org.ballcat.springsecurity.oauth2.server.authorization.properties.OAuth2AuthorizationServerProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * OAuth2 授权服务器的 HttpSecurity 的扩展配置器 + * + * @author Hccake + */ +@Configuration(proxyBeanMethods = false) +public class OAuth2AuthorizationServerExtensionConfigurerConfiguration { + + /** + * 登录验证码配置 + * @param iValidateCodeService 验证码验证器 + * @return FilterRegistrationBean + */ + @Bean + @ConditionalOnProperty(prefix = OAuth2AuthorizationServerProperties.PREFIX, name = "login-captcha-enabled", + havingValue = "true") + public OAuth2LoginCaptchaConfigurer oAuth2LoginCaptchaConfigurer(IValidateCodeService iValidateCodeService) { + return new OAuth2LoginCaptchaConfigurer(iValidateCodeService); + } + + /** + * password 模式下,密码入参要求 AES 加密。 在进入令牌端点前,通过过滤器进行解密处理。 + * @param securityProperties 安全配置相关 + * @return FilterRegistrationBean + */ + @Bean + @ConditionalOnProperty(prefix = SecurityProperties.PREFIX, name = "password-secret-key") + public OAuth2LoginPasswordDecoderConfigurer oAuth2LoginPasswordDecoderConfigurer( + SecurityProperties securityProperties) { + return new OAuth2LoginPasswordDecoderConfigurer(securityProperties.getPasswordSecretKey()); + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/BallcatOAuth2AuthorizationServerSecurityFilterChainBuilder.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/BallcatOAuth2AuthorizationServerSecurityFilterChainBuilder.java new file mode 100644 index 0000000..360bec0 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/BallcatOAuth2AuthorizationServerSecurityFilterChainBuilder.java @@ -0,0 +1,76 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config; + +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.authorization.config.configurer.OAuth2AuthorizationServerExtensionConfigurer; +import org.ballcat.springsecurity.oauth2.server.authorization.config.customizer.OAuth2AuthorizationServerConfigurerCustomizer; +import org.springframework.http.MediaType; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AndRequestMatcher; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; +import org.springframework.security.web.util.matcher.NegatedRequestMatcher; +import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.accept.ContentNegotiationStrategy; +import org.springframework.web.accept.HeaderContentNegotiationStrategy; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * OAuth2 的授权服务配置 + *

          + * 当实例既是授权服务器又是资源服务器时,Order 必须高于资源服务器 + * + * @author hccake + */ +@RequiredArgsConstructor +public class BallcatOAuth2AuthorizationServerSecurityFilterChainBuilder + implements OAuth2AuthorizationServerSecurityFilterChainBuilder { + + private final List oAuth2AuthorizationServerConfigurerCustomizerList; + + private final List> oAuth2AuthorizationServerExtensionConfigurers; + + @Override + public SecurityFilterChain build(HttpSecurity http) throws Exception { + // 授权服务器配置 + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); + for (OAuth2AuthorizationServerConfigurerCustomizer customizer : oAuth2AuthorizationServerConfigurerCustomizerList) { + customizer.customize(authorizationServerConfigurer, http); + } + + // @formatter:off + RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); + http.requestMatchers() + .requestMatchers(endpointsMatcher) + .and() + .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated()) + .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) + .apply(authorizationServerConfigurer); + // @formatter:off + + for (OAuth2AuthorizationServerExtensionConfigurer configurer : oAuth2AuthorizationServerExtensionConfigurers) { + http.apply(configurer); + } + + return http.build(); + } + + + protected final RequestMatcher getAuthenticationEntryPointMatcher(HttpSecurity http) { + ContentNegotiationStrategy contentNegotiationStrategy = http.getSharedObject(ContentNegotiationStrategy.class); + if (contentNegotiationStrategy == null) { + contentNegotiationStrategy = new HeaderContentNegotiationStrategy(); + } + MediaTypeRequestMatcher mediaMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, + MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML, + MediaType.TEXT_PLAIN); + mediaMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + RequestMatcher notXRequestedWith = new NegatedRequestMatcher( + new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); + return new AndRequestMatcher(Arrays.asList(notXRequestedWith, mediaMatcher)); + } +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/OAuth2AuthorizationServerSecurityFilterChainBuilder.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/OAuth2AuthorizationServerSecurityFilterChainBuilder.java new file mode 100644 index 0000000..42145bb --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/OAuth2AuthorizationServerSecurityFilterChainBuilder.java @@ -0,0 +1,21 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +/** + * OAuth2 Authorization Server 的 SecurityFilterChain 构造器 + * + * @author hccake + */ +public interface OAuth2AuthorizationServerSecurityFilterChainBuilder { + + /** + * 构建 OAuth2 Authorization Server 的 SecurityFilterChain + * @param http HttpSecurity + * @return SecurityFilterChain + * @throws Exception 构建异常 + */ + SecurityFilterChain build(HttpSecurity http) throws Exception; + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2AuthorizationServerExtensionConfigurer.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2AuthorizationServerExtensionConfigurer.java new file mode 100644 index 0000000..92f430a --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2AuthorizationServerExtensionConfigurer.java @@ -0,0 +1,14 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.configurer; + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; + +/** + * 对 OAuth2 授权服务器的 SecurityConfigurer 进行扩展的配置类 + * + * @author hccake + */ +public abstract class OAuth2AuthorizationServerExtensionConfigurer, H extends HttpSecurityBuilder> + extends AbstractHttpConfigurer { + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2ConfigurerUtils.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2ConfigurerUtils.java new file mode 100644 index 0000000..d37a459 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2ConfigurerUtils.java @@ -0,0 +1,207 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.configurer; + +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator; +import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; +import org.springframework.security.oauth2.server.authorization.token.JwtGenerator; +import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator; +import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.util.StringUtils; + +import java.util.Map; + +public final class OAuth2ConfigurerUtils { + + private OAuth2ConfigurerUtils() { + } + + public static RegisteredClientRepository getRegisteredClientRepository(HttpSecurity httpSecurity) { + RegisteredClientRepository registeredClientRepository = httpSecurity + .getSharedObject(RegisteredClientRepository.class); + if (registeredClientRepository == null) { + registeredClientRepository = getBean(httpSecurity, RegisteredClientRepository.class); + httpSecurity.setSharedObject(RegisteredClientRepository.class, registeredClientRepository); + } + return registeredClientRepository; + } + + public static OAuth2AuthorizationService getAuthorizationService(HttpSecurity httpSecurity) { + OAuth2AuthorizationService authorizationService = httpSecurity + .getSharedObject(OAuth2AuthorizationService.class); + if (authorizationService == null) { + authorizationService = getOptionalBean(httpSecurity, OAuth2AuthorizationService.class); + if (authorizationService == null) { + authorizationService = new InMemoryOAuth2AuthorizationService(); + } + httpSecurity.setSharedObject(OAuth2AuthorizationService.class, authorizationService); + } + return authorizationService; + } + + public static OAuth2AuthorizationConsentService getAuthorizationConsentService(HttpSecurity httpSecurity) { + OAuth2AuthorizationConsentService authorizationConsentService = httpSecurity + .getSharedObject(OAuth2AuthorizationConsentService.class); + if (authorizationConsentService == null) { + authorizationConsentService = getOptionalBean(httpSecurity, OAuth2AuthorizationConsentService.class); + if (authorizationConsentService == null) { + authorizationConsentService = new InMemoryOAuth2AuthorizationConsentService(); + } + httpSecurity.setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService); + } + return authorizationConsentService; + } + + @SuppressWarnings("unchecked") + public static OAuth2TokenGenerator getTokenGenerator(HttpSecurity httpSecurity) { + OAuth2TokenGenerator tokenGenerator = httpSecurity + .getSharedObject(OAuth2TokenGenerator.class); + if (tokenGenerator == null) { + tokenGenerator = getOptionalBean(httpSecurity, OAuth2TokenGenerator.class); + if (tokenGenerator == null) { + JwtGenerator jwtGenerator = getJwtGenerator(httpSecurity); + OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator(); + OAuth2TokenCustomizer accessTokenCustomizer = getAccessTokenCustomizer( + httpSecurity); + if (accessTokenCustomizer != null) { + accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer); + } + OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); + if (jwtGenerator != null) { + tokenGenerator = new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, + refreshTokenGenerator); + } + else { + tokenGenerator = new DelegatingOAuth2TokenGenerator(accessTokenGenerator, refreshTokenGenerator); + } + } + httpSecurity.setSharedObject(OAuth2TokenGenerator.class, tokenGenerator); + } + return tokenGenerator; + } + + private static JwtGenerator getJwtGenerator(HttpSecurity httpSecurity) { + JwtGenerator jwtGenerator = httpSecurity.getSharedObject(JwtGenerator.class); + if (jwtGenerator == null) { + JwtEncoder jwtEncoder = getJwtEncoder(httpSecurity); + if (jwtEncoder != null) { + jwtGenerator = new JwtGenerator(jwtEncoder); + OAuth2TokenCustomizer jwtCustomizer = getJwtCustomizer(httpSecurity); + if (jwtCustomizer != null) { + jwtGenerator.setJwtCustomizer(jwtCustomizer); + } + httpSecurity.setSharedObject(JwtGenerator.class, jwtGenerator); + } + } + return jwtGenerator; + } + + private static JwtEncoder getJwtEncoder(HttpSecurity httpSecurity) { + JwtEncoder jwtEncoder = httpSecurity.getSharedObject(JwtEncoder.class); + if (jwtEncoder == null) { + jwtEncoder = getOptionalBean(httpSecurity, JwtEncoder.class); + if (jwtEncoder == null) { + JWKSource jwkSource = getJwkSource(httpSecurity); + if (jwkSource != null) { + jwtEncoder = new NimbusJwtEncoder(jwkSource); + } + } + if (jwtEncoder != null) { + httpSecurity.setSharedObject(JwtEncoder.class, jwtEncoder); + } + } + return jwtEncoder; + } + + @SuppressWarnings("unchecked") + public static JWKSource getJwkSource(HttpSecurity httpSecurity) { + JWKSource jwkSource = httpSecurity.getSharedObject(JWKSource.class); + if (jwkSource == null) { + ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class); + jwkSource = getOptionalBean(httpSecurity, type); + if (jwkSource != null) { + httpSecurity.setSharedObject(JWKSource.class, jwkSource); + } + } + return jwkSource; + } + + private static OAuth2TokenCustomizer getJwtCustomizer(HttpSecurity httpSecurity) { + ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, + JwtEncodingContext.class); + return getOptionalBean(httpSecurity, type); + } + + private static OAuth2TokenCustomizer getAccessTokenCustomizer(HttpSecurity httpSecurity) { + ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, + OAuth2TokenClaimsContext.class); + return getOptionalBean(httpSecurity, type); + } + + public static AuthorizationServerSettings getAuthorizationServerSettings(HttpSecurity httpSecurity) { + AuthorizationServerSettings authorizationServerSettings = httpSecurity + .getSharedObject(AuthorizationServerSettings.class); + if (authorizationServerSettings == null) { + authorizationServerSettings = getBean(httpSecurity, AuthorizationServerSettings.class); + httpSecurity.setSharedObject(AuthorizationServerSettings.class, authorizationServerSettings); + } + return authorizationServerSettings; + } + + public static T getBean(HttpSecurity httpSecurity, Class type) { + return httpSecurity.getSharedObject(ApplicationContext.class).getBean(type); + } + + @SuppressWarnings("unchecked") + public static T getBean(HttpSecurity httpSecurity, ResolvableType type) { + ApplicationContext context = httpSecurity.getSharedObject(ApplicationContext.class); + String[] names = context.getBeanNamesForType(type); + if (names.length == 1) { + return (T) context.getBean(names[0]); + } + if (names.length > 1) { + throw new NoUniqueBeanDefinitionException(type, names); + } + throw new NoSuchBeanDefinitionException(type); + } + + public static T getOptionalBean(HttpSecurity httpSecurity, Class type) { + Map beansMap = BeanFactoryUtils + .beansOfTypeIncludingAncestors(httpSecurity.getSharedObject(ApplicationContext.class), type); + if (beansMap.size() > 1) { + throw new NoUniqueBeanDefinitionException(type, beansMap.size(), + "Expected single matching bean of type '" + type.getName() + "' but found " + beansMap.size() + ": " + + StringUtils.collectionToCommaDelimitedString(beansMap.keySet())); + } + return (!beansMap.isEmpty() ? beansMap.values().iterator().next() : null); + } + + @SuppressWarnings("unchecked") + public static T getOptionalBean(HttpSecurity httpSecurity, ResolvableType type) { + ApplicationContext context = httpSecurity.getSharedObject(ApplicationContext.class); + String[] names = context.getBeanNamesForType(type); + if (names.length > 1) { + throw new NoUniqueBeanDefinitionException(type, names); + } + return names.length == 1 ? (T) context.getBean(names[0]) : null; + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2LoginCaptchaConfigurer.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2LoginCaptchaConfigurer.java new file mode 100644 index 0000000..3545c96 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2LoginCaptchaConfigurer.java @@ -0,0 +1,44 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.configurer; + +import cn.hutool.core.lang.Assert; +import org.ballcat.security.captcha.IValidateCodeService; +import org.ballcat.springsecurity.oauth2.server.authorization.web.filter.LoginCaptchaFilter; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +/** + * 登录验证码校验 + * + * @author hccake + */ +@Order(90) +public class OAuth2LoginCaptchaConfigurer + extends OAuth2AuthorizationServerExtensionConfigurer { + + private final IValidateCodeService iValidateCodeService; + + public OAuth2LoginCaptchaConfigurer(IValidateCodeService iValidateCodeService) { + Assert.notNull(iValidateCodeService, "验证码不能为空"); + this.iValidateCodeService = iValidateCodeService; + } + + @Override + public void configure(HttpSecurity httpSecurity) { + // 获取授权服务器配置 + AuthorizationServerSettings authorizationServerSettings = httpSecurity + .getSharedObject(AuthorizationServerSettings.class); + + // 只处理登录接口 + AntPathRequestMatcher requestMatcher = new AntPathRequestMatcher(authorizationServerSettings.getTokenEndpoint(), + HttpMethod.POST.name()); + + // 验证码,必须在 OAuth2ClientAuthenticationFilter 过滤器之后,方便获取当前客户端 + httpSecurity.addFilterAfter(new LoginCaptchaFilter(requestMatcher, iValidateCodeService), + OAuth2ClientAuthenticationFilter.class); + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2LoginPasswordDecoderConfigurer.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2LoginPasswordDecoderConfigurer.java new file mode 100644 index 0000000..d0b7824 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2LoginPasswordDecoderConfigurer.java @@ -0,0 +1,43 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.configurer; + +import cn.hutool.core.lang.Assert; +import org.ballcat.springsecurity.oauth2.server.authorization.web.filter.LoginPasswordDecoderFilter; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +/** + * 登录时的密码解密配置 + * + * @author hccake + */ +@Order(100) +public class OAuth2LoginPasswordDecoderConfigurer + extends OAuth2AuthorizationServerExtensionConfigurer { + + private final String passwordSecretKey; + + public OAuth2LoginPasswordDecoderConfigurer(String passwordSecretKey) { + Assert.notEmpty(passwordSecretKey, "passwordSecretKey can not be null"); + this.passwordSecretKey = passwordSecretKey; + } + + @Override + public void configure(HttpSecurity httpSecurity) { + // 获取授权服务器配置 + AuthorizationServerSettings authorizationServerSettings = httpSecurity + .getSharedObject(AuthorizationServerSettings.class); + + // 只处理登录接口 + AntPathRequestMatcher requestMatcher = new AntPathRequestMatcher(authorizationServerSettings.getTokenEndpoint(), + HttpMethod.POST.name()); + + // 密码解密,必须在 OAuth2ClientAuthenticationFilter 过滤器之后,方便获取当前客户端 + httpSecurity.addFilterAfter(new LoginPasswordDecoderFilter(requestMatcher, passwordSecretKey), + OAuth2ClientAuthenticationFilter.class); + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/FormLoginConfigurerCustomizer.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/FormLoginConfigurerCustomizer.java new file mode 100644 index 0000000..c214864 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/FormLoginConfigurerCustomizer.java @@ -0,0 +1,46 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.customizer; + +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.authorization.properties.OAuth2AuthorizationServerProperties; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; + +import static org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL; + +/** + * 表单登录配置项 + * + * @author hccake + */ +@RequiredArgsConstructor +public class FormLoginConfigurerCustomizer implements OAuth2AuthorizationServerConfigurerCustomizer { + + private final OAuth2AuthorizationServerProperties oAuth2AuthorizationServerProperties; + + private final UserDetailsService userDetailsService; + + @Override + public void customize(OAuth2AuthorizationServerConfigurer oAuth2AuthorizationServerConfigurer, + HttpSecurity httpSecurity) throws Exception { + + if (oAuth2AuthorizationServerProperties.isLoginPageEnabled()) { + String loginPage = oAuth2AuthorizationServerProperties.getLoginPage(); + + HttpSecurity.RequestMatcherConfigurer requestMatcherConfigurer = httpSecurity.requestMatchers(); + if (loginPage == null) { + requestMatcherConfigurer.antMatchers(DEFAULT_LOGIN_PAGE_URL); + httpSecurity.formLogin(); + } + else { + requestMatcherConfigurer.antMatchers(loginPage); + httpSecurity.formLogin(form -> form.loginPage(loginPage).permitAll()); + } + + // 需要 userDetailsService 对应生成 DaoAuthenticationProvider + httpSecurity.userDetailsService(userDetailsService); + } + + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2AuthorizationEndpointConfigurerCustomizer.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2AuthorizationEndpointConfigurerCustomizer.java new file mode 100644 index 0000000..a23b7de --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2AuthorizationEndpointConfigurerCustomizer.java @@ -0,0 +1,64 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.customizer; + +import org.ballcat.springsecurity.oauth2.server.authorization.config.configurer.OAuth2ConfigurerUtils; +import org.ballcat.springsecurity.oauth2.server.authorization.properties.OAuth2AuthorizationServerProperties; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2LoginUrlAuthenticationEntryPoint; +import org.ballcat.springsecurity.oauth2.server.authorization.web.context.OAuth2SecurityContextRepository; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +/** + * OAuth2 授权码流程端点的扩展处理器 + * + * @author hccake + */ +public class OAuth2AuthorizationEndpointConfigurerCustomizer implements OAuth2AuthorizationServerConfigurerCustomizer { + + private final OAuth2AuthorizationServerProperties properties; + + private final OAuth2SecurityContextRepository oAuth2SecurityContextRepository; + + public OAuth2AuthorizationEndpointConfigurerCustomizer(OAuth2AuthorizationServerProperties properties, + OAuth2SecurityContextRepository oAuth2SecurityContextRepository) { + this.properties = properties; + this.oAuth2SecurityContextRepository = oAuth2SecurityContextRepository; + } + + @SuppressWarnings("unchecked") + @Override + public void customize(OAuth2AuthorizationServerConfigurer oAuth2AuthorizationServerConfigurer, + HttpSecurity httpSecurity) throws Exception { + + // 使用无状态登录时,需要配合自定义的 SecurityContextRepository + if (properties.isStateless()) { + httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + httpSecurity + .securityContext(security -> security.securityContextRepository(oAuth2SecurityContextRepository)); + } + + // 设置鉴权失败时的跳转地址 + String loginPage = properties.getLoginPage(); + AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils + .getAuthorizationServerSettings(httpSecurity); + ExceptionHandlingConfigurer exceptionHandling = httpSecurity + .getConfigurer(ExceptionHandlingConfigurer.class); + if (exceptionHandling != null) { + exceptionHandling.defaultAuthenticationEntryPointFor(new OAuth2LoginUrlAuthenticationEntryPoint(loginPage), + new AntPathRequestMatcher(authorizationServerSettings.getAuthorizationEndpoint(), + HttpMethod.GET.name())); + } + + // 设置 OAuth2 Consent 地址 + String consentPage = properties.getConsentPage(); + if (consentPage != null) { + oAuth2AuthorizationServerConfigurer + .authorizationEndpoint(configurer -> configurer.consentPage(consentPage)); + } + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2AuthorizationServerConfigurerCustomizer.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2AuthorizationServerConfigurerCustomizer.java new file mode 100644 index 0000000..bf89f0b --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2AuthorizationServerConfigurerCustomizer.java @@ -0,0 +1,22 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.customizer; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; + +/** + * 对 OAuth2授权服务器配置({@link OAuth2AuthorizationServerConfigurer}) 进行个性化配置的的定制器 + * + * @author hccake + */ +@FunctionalInterface +public interface OAuth2AuthorizationServerConfigurerCustomizer { + + /** + * 对授权服务器配置进行自定义 + * @param oAuth2AuthorizationServerConfigurer OAuth2AuthorizationServerConfigurer + * @param httpSecurity security configuration + */ + void customize(OAuth2AuthorizationServerConfigurer oAuth2AuthorizationServerConfigurer, HttpSecurity httpSecurity) + throws Exception; + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2ResourceOwnerPasswordConfigurerCustomizer.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2ResourceOwnerPasswordConfigurerCustomizer.java new file mode 100644 index 0000000..8b05ada --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2ResourceOwnerPasswordConfigurerCustomizer.java @@ -0,0 +1,61 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.customizer; + +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2ResourceOwnerPasswordAuthenticationProvider; +import org.ballcat.springsecurity.oauth2.server.authorization.config.configurer.OAuth2ConfigurerUtils; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2ResourceOwnerPasswordAuthenticationConverter; +import org.springframework.context.ApplicationContext; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; + +/** + * 密码模式支持 + * + * @author hccake + */ +@RequiredArgsConstructor +public class OAuth2ResourceOwnerPasswordConfigurerCustomizer implements OAuth2AuthorizationServerConfigurerCustomizer { + + private final ApplicationContext context; + + @Override + public void customize(OAuth2AuthorizationServerConfigurer oAuth2AuthorizationServerConfigurer, + HttpSecurity httpSecurity) { + // 添加 resource owner password 模式支持 + oAuth2AuthorizationServerConfigurer.tokenEndpoint(tokenEndpoint -> { + + UserDetailsService userDetailsService = getBeanOrNull(UserDetailsService.class); + OAuth2AuthorizationService authorizationService = getBeanOrNull(OAuth2AuthorizationService.class); + OAuth2TokenGenerator tokenGenerator = OAuth2ConfigurerUtils + .getTokenGenerator(httpSecurity); + PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); + + OAuth2ResourceOwnerPasswordAuthenticationProvider authenticationProvider = new OAuth2ResourceOwnerPasswordAuthenticationProvider( + authorizationService, tokenGenerator, userDetailsService); + if (passwordEncoder != null) { + authenticationProvider.setPasswordEncoder(passwordEncoder); + } + + tokenEndpoint.authenticationProvider(authenticationProvider); + tokenEndpoint.accessTokenRequestConverter(new OAuth2ResourceOwnerPasswordAuthenticationConverter()); + }); + } + + /** + * @return a bean of the requested class if there's just a single registered + * component, null otherwise. + */ + private T getBeanOrNull(Class type) { + String[] beanNames = this.context.getBeanNamesForType(type); + if (beanNames.length != 1) { + return null; + } + return this.context.getBean(beanNames[0], type); + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2TokenResponseEnhanceConfigurerCustomizer.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2TokenResponseEnhanceConfigurerCustomizer.java new file mode 100644 index 0000000..728aff6 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2TokenResponseEnhanceConfigurerCustomizer.java @@ -0,0 +1,114 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.customizer; + +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2TokenResponseEnhancer; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.util.CollectionUtils; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.temporal.ChronoUnit; +import java.util.Map; + +/** + * OAuth2 Token 响应增强配置 + * + * @author chengbohua + */ +public class OAuth2TokenResponseEnhanceConfigurerCustomizer implements OAuth2AuthorizationServerConfigurerCustomizer { + + private final OAuth2TokenResponseEnhancer oauth2TokenResponseEnhancer; + + private final HttpMessageConverter accessTokenHttpResponseConverter; + + private final boolean setAccessTokenCookie; + + public OAuth2TokenResponseEnhanceConfigurerCustomizer(OAuth2TokenResponseEnhancer oauth2TokenResponseEnhancer) { + this(oauth2TokenResponseEnhancer, new OAuth2AccessTokenResponseHttpMessageConverter()); + } + + public OAuth2TokenResponseEnhanceConfigurerCustomizer(OAuth2TokenResponseEnhancer oauth2TokenResponseEnhancer, + HttpMessageConverter accessTokenHttpResponseConverter) { + this(oauth2TokenResponseEnhancer, accessTokenHttpResponseConverter, false); + } + + public OAuth2TokenResponseEnhanceConfigurerCustomizer(OAuth2TokenResponseEnhancer oauth2TokenResponseEnhancer, + boolean setAccessTokenCookie) { + this(oauth2TokenResponseEnhancer, new OAuth2AccessTokenResponseHttpMessageConverter(), setAccessTokenCookie); + } + + public OAuth2TokenResponseEnhanceConfigurerCustomizer(OAuth2TokenResponseEnhancer oauth2TokenResponseEnhancer, + HttpMessageConverter accessTokenHttpResponseConverter, + boolean setAccessTokenCookie) { + this.oauth2TokenResponseEnhancer = oauth2TokenResponseEnhancer; + this.accessTokenHttpResponseConverter = accessTokenHttpResponseConverter; + this.setAccessTokenCookie = setAccessTokenCookie; + } + + @Override + public void customize(OAuth2AuthorizationServerConfigurer oAuth2AuthorizationServerConfigurer, + HttpSecurity httpSecurity) { + oAuth2AuthorizationServerConfigurer.tokenEndpoint(oAuth2TokenEndpointConfigurer -> oAuth2TokenEndpointConfigurer + .accessTokenResponseHandler(this::sendAccessTokenResponse)); + } + + private void sendAccessTokenResponse(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException { + + OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication; + + OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken(); + OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken(); + + OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue()) + .tokenType(accessToken.getTokenType()) + .scopes(accessToken.getScopes()); + if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) { + builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt())); + } + if (refreshToken != null) { + builder.refreshToken(refreshToken.getTokenValue()); + } + Map additionalParameters = oauth2TokenResponseEnhancer.enhance(accessTokenAuthentication); + if (!CollectionUtils.isEmpty(additionalParameters)) { + builder.additionalParameters(additionalParameters); + } + OAuth2AccessTokenResponse accessTokenResponse = builder.build(); + ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); + + // 添加 cookie, 配合无状态登录使用 + if (setAccessTokenCookie) { + setCookie(request, response, OAuth2TokenType.ACCESS_TOKEN.getValue(), accessToken.getTokenValue(), 86400); + } + + this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse); + } + + public static void setCookie(HttpServletRequest request, HttpServletResponse response, String name, String value, + int maxAge) { + final Cookie cookie = new Cookie(name, value); + cookie.setMaxAge(maxAge); + cookie.setPath(getRequestContext(request)); + cookie.setSecure(request.isSecure()); + cookie.setHttpOnly(true); + + response.addCookie(cookie); + } + + private static String getRequestContext(HttpServletRequest request) { + String contextPath = request.getContextPath(); + return (contextPath.length() > 0) ? contextPath : "/"; + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2TokenRevocationEndpointConfigurerCustomizer.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2TokenRevocationEndpointConfigurerCustomizer.java new file mode 100644 index 0000000..5af7a8e --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2TokenRevocationEndpointConfigurerCustomizer.java @@ -0,0 +1,31 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.customizer; + +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2TokenRevocationResponseHandler; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; + +/** + * 令牌撤销端点配置的自定义扩展 + * + * @author hccake + */ +@RequiredArgsConstructor +public class OAuth2TokenRevocationEndpointConfigurerCustomizer + implements OAuth2AuthorizationServerConfigurerCustomizer { + + private final OAuth2AuthorizationService authorizationService; + + private final OAuth2TokenRevocationResponseHandler oAuth2TokenRevocationResponseHandler; + + @Override + public void customize(OAuth2AuthorizationServerConfigurer oAuth2AuthorizationServerConfigurer, + HttpSecurity httpSecurity) { + oAuth2AuthorizationServerConfigurer.tokenRevocationEndpoint( + tokenRevocation -> tokenRevocation.revocationResponseHandler(oAuth2TokenRevocationResponseHandler) + .authenticationProvider(new OAuth2TokenRevocationAuthenticationProvider(authorizationService))); + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/properties/OAuth2AuthorizationServerProperties.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/properties/OAuth2AuthorizationServerProperties.java new file mode 100644 index 0000000..f4d843f --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/properties/OAuth2AuthorizationServerProperties.java @@ -0,0 +1,50 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationEndpointConfigurer; + +/** + * 授权服务器的配置文件 + * + * @author hccake + */ +@Getter +@Setter +@ConfigurationProperties(prefix = OAuth2AuthorizationServerProperties.PREFIX) +public class OAuth2AuthorizationServerProperties { + + public static final String PREFIX = "ballcat.security.oauth2.authorizationserver"; + + /** + * 登录验证码开关 + */ + private boolean loginCaptchaEnabled = false; + + /** + * 开启服务端登录页 + */ + private boolean loginPageEnabled = false; + + /** + * 登录地址 + *

          + * - 不配置将使用 security 默认的登录页:/login
          + * - 配置后则必须自己提供登录页面 + */ + private String loginPage = null; + + /** + * 用户同意授权页面 + * + * @see OAuth2AuthorizationEndpointConfigurer#consentPage + */ + private String consentPage; + + /** + * 无状态登录 + */ + private boolean stateless = false; + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/token/BallcatOAuth2TokenCustomizer.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/token/BallcatOAuth2TokenCustomizer.java new file mode 100644 index 0000000..eb7f2e5 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/token/BallcatOAuth2TokenCustomizer.java @@ -0,0 +1,62 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.token; + +import com.baiye.security.constant.TokenAttributeNameConstants; +import com.baiye.security.constant.UserInfoFiledNameConstants; +import com.baiye.security.userdetails.User; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsSet; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; + +import java.util.HashMap; +import java.util.Map; + +/** + * 自定义 OAuth2TokenCustomizer,处理 BallCat 提供的 User 属性存储,以便在自省时返回对应信息 + * + * @see User + * @author hccake + */ +public class BallcatOAuth2TokenCustomizer implements OAuth2TokenCustomizer { + + @Override + public void customize(OAuth2TokenClaimsContext context) { + OAuth2TokenClaimsSet.Builder claims = context.getClaims(); + Authentication authentication = context.getPrincipal(); + + // client token + if (authentication instanceof OAuth2ClientAuthenticationToken) { + claims.claim(TokenAttributeNameConstants.IS_CLIENT, true); + return; + } + + Object principal = authentication.getPrincipal(); + if (principal instanceof User) { + User user = (User) principal; + Map attributes = user.getAttributes(); + claims.claim(TokenAttributeNameConstants.ATTRIBUTES, attributes); + HashMap userInfoMap = getUserInfoMap(user); + claims.claim(TokenAttributeNameConstants.INFO, userInfoMap); + claims.claim(TokenAttributeNameConstants.IS_CLIENT, false); + } + } + + private static HashMap getUserInfoMap(User user) { + HashMap userInfo = new HashMap<>(12); + userInfo.put(UserInfoFiledNameConstants.USER_ID, user.getUserId()); + userInfo.put(UserInfoFiledNameConstants.TYPE, user.getType()); + userInfo.put(UserInfoFiledNameConstants.SALESMAN_TYPE, user.getSalesmanType()); + userInfo.put(UserInfoFiledNameConstants.ORGANIZATION_ID, user.getOrganizationId()); + userInfo.put(UserInfoFiledNameConstants.USERNAME, user.getUsername()); + userInfo.put(UserInfoFiledNameConstants.NICKNAME, user.getNickname()); + userInfo.put(UserInfoFiledNameConstants.AVATAR, user.getAvatar()); + userInfo.put(UserInfoFiledNameConstants.EMAIL, user.getEmail()); + userInfo.put(UserInfoFiledNameConstants.GENDER, user.getGender()); + userInfo.put(UserInfoFiledNameConstants.PHONE_NUMBER, user.getPhoneNumber()); + userInfo.put(UserInfoFiledNameConstants.STATUS, user.getStatus()); + userInfo.put(UserInfoFiledNameConstants.WHICH_USER_ID, user.getWhichUserId()); + return userInfo; + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/CookieBearerTokenResolver.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/CookieBearerTokenResolver.java new file mode 100644 index 0000000..6e04744 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/CookieBearerTokenResolver.java @@ -0,0 +1,48 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web; + +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.util.Assert; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +/** + * Resolving + * Bearer + * Tokens from the HTTP Cookie. + * + * @author Hccake + * @see HttpServletRequest#getCookies() + * @see RFC 6750 + * Section 2: Authenticated Requests + */ +public class CookieBearerTokenResolver implements BearerTokenResolver { + + private String cookieName = OAuth2TokenType.ACCESS_TOKEN.getValue(); + + public CookieBearerTokenResolver() { + } + + public CookieBearerTokenResolver(String cookieName) { + Assert.hasText(cookieName, "cookie name cannot be empty"); + this.cookieName = cookieName; + } + + @Override + public String resolve(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + return null; + } + + for (Cookie cookie : cookies) { + if (this.cookieName.equals(cookie.getName())) { + return cookie.getValue(); + } + } + + return null; + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java new file mode 100644 index 0000000..5094111 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ballcat.springsecurity.oauth2.server.authorization.web.authentication; + +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * Utility methods for the OAuth 2.0 Protocol Endpoints. copy from SAS + * + * @author Joe Grandja + * @since 0.0.1 + */ +public class OAuth2EndpointUtils { + + static final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; + + private OAuth2EndpointUtils() { + } + + public static MultiValueMap getParameters(HttpServletRequest request) { + Map parameterMap = request.getParameterMap(); + MultiValueMap parameters = new LinkedMultiValueMap<>(parameterMap.size()); + parameterMap.forEach((key, values) -> { + for (String value : values) { + parameters.add(key, value); + } + }); + return parameters; + } + + public static void throwError(String errorCode, String parameterName, String errorUri) { + OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri); + throw new OAuth2AuthenticationException(error); + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2LoginUrlAuthenticationEntryPoint.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2LoginUrlAuthenticationEntryPoint.java new file mode 100644 index 0000000..000729f --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2LoginUrlAuthenticationEntryPoint.java @@ -0,0 +1,74 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web.authentication; + +import cn.hutool.core.net.url.UrlBuilder; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * OAuth2 登录失败的处理器,跳转到登录页,同时携带 return_to 参数,值为当前 request fullUrl + * + * @author hccake + */ +public class OAuth2LoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint { + + /** + * @param loginFormUrl URL where the login page can be found. Should either be + * relative to the web-app context path (include a leading {@code /}) or an absolute + * URL. + */ + public OAuth2LoginUrlAuthenticationEntryPoint(String loginFormUrl) { + super(loginFormUrl); + } + + @Override + protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) { + String loginForm = super.buildRedirectUrlToLoginPage(request, response, authException); + return addReturnToParameter(request, loginForm); + } + + private static String addReturnToParameter(HttpServletRequest request, String loginForm) { + return UrlBuilder.of(loginForm).addQuery("return_to", getFullUrl(request)).build(); + } + + private static String getFullUrl(HttpServletRequest req) { + + String scheme = req.getScheme(); + String serverName = req.getServerName(); + int serverPort = req.getServerPort(); + String contextPath = req.getContextPath(); + String servletPath = req.getServletPath(); + String pathInfo = req.getPathInfo(); + String queryString = req.getQueryString(); + + // Reconstruct original requesting URL + StringBuilder url = new StringBuilder(); + url.append(scheme).append("://").append(serverName); + + if (serverPort != 80 && serverPort != 443) { + url.append(":").append(serverPort); + } + + url.append(contextPath).append(servletPath); + + if (pathInfo != null) { + url.append(pathInfo); + } + if (queryString != null) { + url.append("?").append(queryString); + } + return url.toString(); + } + + @Override + protected String buildHttpsRedirectUrlForRequest(HttpServletRequest request) throws IOException, ServletException { + String loginForm = super.buildHttpsRedirectUrlForRequest(request); + return addReturnToParameter(request, loginForm); + } + +} \ No newline at end of file diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2ResourceOwnerPasswordAuthenticationConverter.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2ResourceOwnerPasswordAuthenticationConverter.java new file mode 100644 index 0000000..b7419a0 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2ResourceOwnerPasswordAuthenticationConverter.java @@ -0,0 +1,87 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web.authentication; + +import org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2ResourceOwnerPasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Attempts to extract an Access Token Request from {@link HttpServletRequest} for the + * OAuth 2.0 Owner Password Authentication Grant and then converts it to an + * {@link OAuth2ResourceOwnerPasswordAuthenticationToken} used for authenticating the + * authorization grant. + * + * @author Hccake + * @since 1.0.0 + * @see AuthenticationConverter + * @see OAuth2ResourceOwnerPasswordAuthenticationToken + * @see OAuth2TokenEndpointFilter + */ +public class OAuth2ResourceOwnerPasswordAuthenticationConverter implements AuthenticationConverter { + + @Override + public Authentication convert(HttpServletRequest request) { + // grant_type (REQUIRED) + String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); + if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) { + return null; + } + + MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); + + // scope (OPTIONAL) + Set scopes = null; + String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE); + if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) { + OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE, + OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); + } + if (StringUtils.hasText(scope)) { + scopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); + } + + // username (REQUIRED) + String username = parameters.getFirst(OAuth2ParameterNames.USERNAME); + if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) { + OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.USERNAME, + OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); + } + + // password (REQUIRED) + String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD); + if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) { + OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.PASSWORD, + OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); + } + + Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); + if (clientPrincipal == null) { + OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ErrorCodes.INVALID_CLIENT, + OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); + } + + Map additionalParameters = parameters.entrySet() + .stream() + .filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) + && !e.getKey().equals(OAuth2ParameterNames.SCOPE)) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))); + + return new OAuth2ResourceOwnerPasswordAuthenticationToken(username, clientPrincipal, additionalParameters, + scopes); + + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2TokenResponseEnhancer.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2TokenResponseEnhancer.java new file mode 100644 index 0000000..8ebe098 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2TokenResponseEnhancer.java @@ -0,0 +1,25 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web.authentication; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; + +import java.util.Map; + +/** + * Customize additional parameters for OAuth2 Token Response + * + * @see org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse + * @author hccake + */ +@FunctionalInterface +public interface OAuth2TokenResponseEnhancer { + + /** + * Provide an additional parameter map to enhance OAuth2 Token Response + * @param accessTokenAuthentication An {@link Authentication} implementation used when + * issuing an * OAuth 2.0 Access Token and (optional) Refresh Token. + * @return an additional parameter map + */ + Map enhance(OAuth2AccessTokenAuthenticationToken accessTokenAuthentication); + +} \ No newline at end of file diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2TokenRevocationResponseHandler.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2TokenRevocationResponseHandler.java new file mode 100644 index 0000000..209f4bd --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2TokenRevocationResponseHandler.java @@ -0,0 +1,36 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web.authentication; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.event.LogoutSuccessEvent; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * OAuth2 撤销令牌成功的处理,发布登出事件,以及响应 200 + * + * @author hccake + * @see Section 2 + * Token Revocation + * @see Section + * 2.1 Revocation Request + */ +@RequiredArgsConstructor +public class OAuth2TokenRevocationResponseHandler implements AuthenticationSuccessHandler { + + private final ApplicationEventPublisher publisher; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) { + // 发布用户登出事件 + publisher.publishEvent(new LogoutSuccessEvent(authentication)); + // 返回 200 响应 + response.setStatus(HttpStatus.OK.value()); + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/context/OAuth2SecurityContextRepository.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/context/OAuth2SecurityContextRepository.java new file mode 100644 index 0000000..e5a5afd --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/context/OAuth2SecurityContextRepository.java @@ -0,0 +1,70 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web.context; + +import org.ballcat.springsecurity.oauth2.server.authorization.web.CookieBearerTokenResolver; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.security.web.context.HttpRequestResponseHolder; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * OAuth2 下使用的 SecurityContextRepository,从 bearerTokenResolver 中解析对应的 access_token, 然后利用 + * oAuth2AuthorizationService 来获取对应的 SecurityContext + * + * @author hccake + */ +public class OAuth2SecurityContextRepository implements SecurityContextRepository { + + private final BearerTokenResolver bearerTokenResolver; + + private final OAuth2AuthorizationService authorizationService; + + public OAuth2SecurityContextRepository(OAuth2AuthorizationService authorizationService) { + this.bearerTokenResolver = new CookieBearerTokenResolver(); + this.authorizationService = authorizationService; + } + + public OAuth2SecurityContextRepository(BearerTokenResolver bearerTokenResolver, + OAuth2AuthorizationService authorizationService) { + this.bearerTokenResolver = bearerTokenResolver; + this.authorizationService = authorizationService; + } + + @Override + public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { + HttpServletRequest request = requestResponseHolder.getRequest(); + String bearerToken = this.bearerTokenResolver.resolve(request); + if (!StringUtils.hasText(bearerToken)) { + return SecurityContextHolder.createEmptyContext(); + } + OAuth2Authorization oAuth2Authorization = authorizationService.findByToken(bearerToken, + OAuth2TokenType.ACCESS_TOKEN); + if (oAuth2Authorization == null) { + return SecurityContextHolder.createEmptyContext(); + } + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) oAuth2Authorization + .getAttributes() + .get("java.security.Principal"); + return new SecurityContextImpl(usernamePasswordAuthenticationToken); + } + + @Override + public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { + + } + + @Override + public boolean containsContext(HttpServletRequest request) { + return this.loadContext(request) != null; + } + +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/filter/LoginCaptchaFilter.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/filter/LoginCaptchaFilter.java new file mode 100644 index 0000000..64d5c6a --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/filter/LoginCaptchaFilter.java @@ -0,0 +1,83 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web.filter; + +import cn.hutool.core.text.CharSequenceUtil; +import com.baiye.result.R; +import com.baiye.result.SystemResultCode; +import com.baiye.security.ScopeNames; +import com.baiye.util.JsonUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ballcat.security.captcha.IValidateCodeService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient; + +@RequiredArgsConstructor +@Slf4j +public class LoginCaptchaFilter extends OncePerRequestFilter { + + private final RequestMatcher requestMatcher; + + private final IValidateCodeService iValidateCodeService; + + private static final String CODE = "code"; + + private static final String UUID = "uuid"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + if (!this.requestMatcher.matches(request)) { + filterChain.doFilter(request, response); + return; + } + + // 只对 password 的 grant_type 进行拦截处理 + String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); + if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) { + filterChain.doFilter(request, response); + return; + } + + // 获取当前客户端 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(authentication); + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + // 测试客户端 跳过验证码(swagger 或 postman测试时使用) + if (registeredClient != null && registeredClient.getScopes().contains(ScopeNames.SKIP_CAPTCHA)) { + filterChain.doFilter(request, response); + return; + } + // 处理验证码 + String code = request.getParameter(CODE); + String uuid = request.getParameter(UUID); + try { + iValidateCodeService.checkCapcha(uuid, code); + filterChain.doFilter(request, response); + } catch (Exception e) { + log.info("验证码有误"); + response.setHeader("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + R r = R.failed(SystemResultCode.UNAUTHORIZED, + CharSequenceUtil.blankToDefault("", "验证码错误")); + response.getWriter().write(JsonUtils.toJson(r)); + } + } +} diff --git a/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/filter/LoginPasswordDecoderFilter.java b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/filter/LoginPasswordDecoderFilter.java new file mode 100644 index 0000000..48e3a55 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/filter/LoginPasswordDecoderFilter.java @@ -0,0 +1,102 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web.filter; + +import com.baiye.request.wrapper.ModifyParamMapRequestWrapper; +import com.baiye.result.R; +import com.baiye.result.SystemResultCode; +import com.baiye.security.ScopeNames; +import com.baiye.security.util.PasswordUtils; +import com.baiye.util.JsonUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient; + +/** + * 前端传递过来的加密密码,需要在登录之前先解密 + * + * @author Hccake 2019/9/28 16:57 + */ +@Slf4j +@RequiredArgsConstructor +public class LoginPasswordDecoderFilter extends OncePerRequestFilter { + + private final RequestMatcher requestMatcher; + + private final String passwordSecretKey; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + if (!this.requestMatcher.matches(request)) { + filterChain.doFilter(request, response); + return; + } + + // 未配置密码密钥时,直接跳过 + if (passwordSecretKey == null) { + log.warn("passwordSecretKey not configured, skip password decoder"); + filterChain.doFilter(request, response); + return; + } + + // 非密码模式下,直接跳过 + String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); + if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) { + filterChain.doFilter(request, response); + return; + } + + // 获取当前客户端 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(authentication); + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + // 测试客户端密码不加密,直接跳过(swagger 或 postman测试时使用) + if (registeredClient != null && registeredClient.getScopes().contains(ScopeNames.SKIP_PASSWORD_DECODE)) { + filterChain.doFilter(request, response); + return; + } + + // 解密前台加密后的密码 + Map parameterMap = new HashMap<>(request.getParameterMap()); + String passwordAes = request.getParameter(OAuth2ParameterNames.PASSWORD); + + try { + String password = PasswordUtils.decodeAES(passwordAes, passwordSecretKey); + parameterMap.put(OAuth2ParameterNames.PASSWORD, new String[] { password }); + } + catch (Exception e) { + log.error("[doFilterInternal] password decode aes error,passwordAes: {},passwordSecretKey: {}", passwordAes, + passwordSecretKey, e); + response.setHeader("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE); + response.setStatus(HttpStatus.BAD_REQUEST.value()); + R r = R.failed(SystemResultCode.UNAUTHORIZED, "用户名或密码错误!"); + response.getWriter().write(JsonUtils.toJson(r)); + return; + } + + // SpringSecurity 默认从ParameterMap中获取密码参数 + // 由于原生的request中对parameter加锁了,无法修改,所以使用包装类 + filterChain.doFilter(new ModifyParamMapRequestWrapper(request, parameterMap), response); + } + +} diff --git a/marketing-scrm-security/security-oauth2-core/pom.xml b/marketing-scrm-security/security-oauth2-core/pom.xml new file mode 100644 index 0000000..7168b30 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/pom.xml @@ -0,0 +1,33 @@ + + + + marketing-scrm-security + com.baiye + ${revision} + + 4.0.0 + security-oauth2-core + + + + + cn.hutool + hutool-crypto + + + com.fasterxml.jackson.core + jackson-databind + compile + + + + org.slf4j + slf4j-api + + + org.springframework.security + spring-security-oauth2-core + + + diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/ScopeNames.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/ScopeNames.java new file mode 100644 index 0000000..509ebe1 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/ScopeNames.java @@ -0,0 +1,21 @@ +package com.baiye.security; + +/** + * @author hccake + */ +public final class ScopeNames { + + private ScopeNames() { + } + + /** + * 跳过验证码 + */ + public static final String SKIP_CAPTCHA = "skip_captcha"; + + /** + * 跳过密码解密 (使用明文传输) + */ + public static final String SKIP_PASSWORD_DECODE = "skip_password_decode"; + +} diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/authentication/OAuth2UserAuthenticationToken.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/authentication/OAuth2UserAuthenticationToken.java new file mode 100644 index 0000000..b437770 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/authentication/OAuth2UserAuthenticationToken.java @@ -0,0 +1,40 @@ +package com.baiye.security.authentication; + +import lombok.EqualsAndHashCode; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.Assert; + +import java.util.Collection; + +/** + * @author chengbohua + */ +@EqualsAndHashCode(callSuper = true) +public class OAuth2UserAuthenticationToken extends AbstractAuthenticationToken { + + private final Object principal; + + /** + * Constructs an {@code Oauth2UserInfoAuthenticationToken} using the provided + * parameters. + * @param principal the principal + */ + public OAuth2UserAuthenticationToken(Object principal, Collection authorities) { + super(authorities); + Assert.notNull(principal, "principal cannot be null"); + this.principal = principal; + setAuthenticated(true); + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + @Override + public Object getCredentials() { + return ""; + } + +} diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/component/CustomPermissionEvaluator.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/component/CustomPermissionEvaluator.java new file mode 100644 index 0000000..74f6cae --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/component/CustomPermissionEvaluator.java @@ -0,0 +1,36 @@ +package com.baiye.security.component; + +import cn.hutool.core.text.CharSequenceUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.PatternMatchUtils; +import org.springframework.util.StringUtils; + +import java.util.Collection; + +@Slf4j +public class CustomPermissionEvaluator { + + /** + * 判断接口是否有xxx:xxx权限 + * @param permission 权限 + * @return {boolean} + */ + public boolean hasPermission(String permission) { + if (CharSequenceUtil.isBlank(permission)) { + return false; + } + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + return false; + } + Collection authorities = authentication.getAuthorities(); + return authorities.stream() + .map(GrantedAuthority::getAuthority) + .filter(StringUtils::hasText) + .anyMatch(x -> PatternMatchUtils.simpleMatch(permission, x)); + } + +} diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/constant/TokenAttributeNameConstants.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/constant/TokenAttributeNameConstants.java new file mode 100644 index 0000000..4c31caa --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/constant/TokenAttributeNameConstants.java @@ -0,0 +1,26 @@ +package com.baiye.security.constant; + +/** + * @author hccake + */ +public final class TokenAttributeNameConstants { + + private TokenAttributeNameConstants() { + } + + /** + * 用户基本信息属性 + */ + public static final String INFO = "info"; + + /** + * 用户附属属性 + */ + public static final String ATTRIBUTES = "attributes"; + + /** + * 是否是客户端 + */ + public static final String IS_CLIENT = "is_client"; + +} diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/constant/UserAttributeNameConstants.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/constant/UserAttributeNameConstants.java new file mode 100644 index 0000000..e1dd9e7 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/constant/UserAttributeNameConstants.java @@ -0,0 +1,26 @@ +package com.baiye.security.constant; + +/** + * @author hccake + */ +public final class UserAttributeNameConstants { + + private UserAttributeNameConstants() { + } + + /** + * 用户角色集合属性 + */ + public static final String ROLE_CODES = "roleCodes"; + + /** + * 用户权限集合属性 + */ + public static final String PERMISSIONS = "permissions"; + + /** + * 用户数据权限相关信息 + */ + public static final String USER_DATA_SCOPE = "userDataScope"; + +} diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/constant/UserInfoFiledNameConstants.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/constant/UserInfoFiledNameConstants.java new file mode 100644 index 0000000..fb7325c --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/constant/UserInfoFiledNameConstants.java @@ -0,0 +1,71 @@ +package com.baiye.security.constant; + +/** + * @author hccake + */ +public final class UserInfoFiledNameConstants { + + private UserInfoFiledNameConstants() { + } + + /** + * 用户ID + */ + public static final String USER_ID = "userId"; + + /** + * 用户类型 + */ + public static final String TYPE = "type"; + + /** + * 业务员类型 + */ + public static final String SALESMAN_TYPE = "salesmanType"; + + /** + * 用户组织ID + */ + public static final String ORGANIZATION_ID = "organizationId"; + + /** + * 用户名 + */ + public static final String USERNAME = "username"; + + /** + * 用户昵称 + */ + public static final String NICKNAME = "nickname"; + + /** + * 用户头像地址 + */ + public static final String AVATAR = "avatar"; + + /** + * 用户邮箱 + */ + public static final String EMAIL = "email"; + + /** + * 用户性别 + */ + public static final String GENDER = "gender"; + + /** + * 用户手机号 + */ + public static final String PHONE_NUMBER = "phoneNumber"; + + /** + * 用户状态 + */ + public static final String STATUS = "status"; + + /** + * 父用户ID + */ + public static final String WHICH_USER_ID = "whichUserId"; + +} diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/LongMixin.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/LongMixin.java new file mode 100644 index 0000000..b370e0d --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/LongMixin.java @@ -0,0 +1,20 @@ +package com.baiye.security.jackson2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * This mixin class is used to serialize/deserialize {@link Long}. + * + * @author Hccake + * @since 1.3.0 + * @see Long + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +public abstract class LongMixin { + + @JsonCreator + LongMixin(Long value) { + } + +} diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/OAuth2UserAuthenticationTokenDeserializer.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/OAuth2UserAuthenticationTokenDeserializer.java new file mode 100644 index 0000000..843c4ba --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/OAuth2UserAuthenticationTokenDeserializer.java @@ -0,0 +1,55 @@ +package com.baiye.security.jackson2; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.baiye.security.authentication.OAuth2UserAuthenticationToken; +import com.baiye.security.userdetails.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.io.IOException; +import java.util.Collection; + +/** + * 自定义的 OAuth2UserAuthenticationToken jackson 反序列化器 + *

          + * 参考 {@link org.springframework.security.jackson2.UserDeserializer} + * + * @author hccake + */ +public class OAuth2UserAuthenticationTokenDeserializer extends JsonDeserializer { + + private static final TypeReference> SIMPLE_GRANTED_AUTHORITY_SET = new TypeReference>() { + }; + + /** + * This method will create {@link org.springframework.security.core.userdetails.User} + * object. It will ensure successful object creation even if password key is null in + * serialized json, because credentials may be removed from the + * {@link org.springframework.security.core.userdetails.User} by invoking + * {@link org.springframework.security.core.userdetails.User#eraseCredentials()}. In + * that case there won't be any password key in serialized json. + * @param jp the JsonParser + * @param ctxt the DeserializationContext + * @return the user + * @throws IOException if a exception during IO occurs + * @throws JsonProcessingException if an error during JSON processing occurs + */ + @Override + public OAuth2UserAuthenticationToken deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + ObjectMapper mapper = (ObjectMapper) jp.getCodec(); + JsonNode jsonNode = mapper.readTree(jp); + + User principal = mapper.treeToValue(jsonNode.get("principal"), User.class); + Collection authorities = mapper.convertValue(jsonNode.get("authorities"), + SIMPLE_GRANTED_AUTHORITY_SET); + + return new OAuth2UserAuthenticationToken(principal, authorities); + } + +} diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/OAuth2UserAuthenticationTokenMixin.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/OAuth2UserAuthenticationTokenMixin.java new file mode 100644 index 0000000..5403374 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/OAuth2UserAuthenticationTokenMixin.java @@ -0,0 +1,18 @@ +package com.baiye.security.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + * @author hccake + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(using = OAuth2UserAuthenticationTokenDeserializer.class) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class OAuth2UserAuthenticationTokenMixin { + +} \ No newline at end of file diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/UserDeserializer.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/UserDeserializer.java new file mode 100644 index 0000000..17c7d10 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/UserDeserializer.java @@ -0,0 +1,100 @@ +package com.baiye.security.jackson2; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.MissingNode; +import com.baiye.security.userdetails.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; + +/** + * 自定义的 User jackson 反序列化器 + *

          + * 参考 {@link org.springframework.security.jackson2.UserDeserializer} + * + * @author hccake + */ +public class UserDeserializer extends JsonDeserializer { + + private static final TypeReference> SIMPLE_GRANTED_AUTHORITY_SET = new TypeReference>() { + }; + + private static final TypeReference> ATTRIBUTE_MAP = new TypeReference>() { + }; + + /** + * This method will create {@link org.springframework.security.core.userdetails.User} + * object. It will ensure successful object creation even if password key is null in + * serialized json, because credentials may be removed from the + * {@link org.springframework.security.core.userdetails.User} by invoking + * {@link org.springframework.security.core.userdetails.User#eraseCredentials()}. In + * that case there won't be any password key in serialized json. + * + * @param jp the JsonParser + * @param ctxt the DeserializationContext + * @return the user + * @throws IOException if a exception during IO occurs + * @throws JsonProcessingException if an error during JSON processing occurs + */ + @Override + public User deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + ObjectMapper mapper = (ObjectMapper) jp.getCodec(); + JsonNode jsonNode = mapper.readTree(jp); + + JsonNode passwordNode = readJsonNode(jsonNode, "password"); + long userId = readJsonNode(jsonNode, "userId").asLong(); + int salesmanType = readJsonNode(jsonNode, "salesmanType").asInt(); + long whichUserId = readJsonNode(jsonNode, "whichUserId").asLong(); + String username = readJsonNode(jsonNode, "username").asText(""); + String nickname = readJsonNode(jsonNode, "nickname").asText(""); + String avatar = readJsonNode(jsonNode, "avatar").asText(""); + int status = readJsonNode(jsonNode, "status").asInt(); + long organizationId = readJsonNode(jsonNode, "organizationId").asLong(); + int type = readJsonNode(jsonNode, "type").asInt(); + String email = readJsonNode(jsonNode, "email").asText(""); + String phoneNumber = readJsonNode(jsonNode, "phoneNumber").asText(""); + int gender = readJsonNode(jsonNode, "gender").asInt(); + + String password = passwordNode.asText(""); + if (passwordNode.asText(null) == null) { + password = null; + } + + Collection authorities = mapper.convertValue(jsonNode.get("authorities"), + SIMPLE_GRANTED_AUTHORITY_SET); + + Map attributes = mapper.convertValue(jsonNode.get("attributes"), ATTRIBUTE_MAP); + + return User.builder() + .userId(userId) + .whichUserId(whichUserId) + .username(username) + .password(password) + .nickname(nickname) + .avatar(avatar) + .status(status) + .organizationId(organizationId) + .email(email) + .phoneNumber(phoneNumber) + .gender(gender) + .type(type) + .salesmanType(salesmanType) + .authorities(authorities) + .attributes(attributes) + .build(); + } + + private JsonNode readJsonNode(JsonNode jsonNode, String field) { + return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance(); + } + +} diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/UserMixin.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/UserMixin.java new file mode 100644 index 0000000..11006f1 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/jackson2/UserMixin.java @@ -0,0 +1,18 @@ +package com.baiye.security.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + * @author hccake + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(using = UserDeserializer.class) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class UserMixin { + +} \ No newline at end of file diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/userdetails/ClientPrincipal.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/userdetails/ClientPrincipal.java new file mode 100644 index 0000000..bec4d1c --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/userdetails/ClientPrincipal.java @@ -0,0 +1,63 @@ +package com.baiye.security.userdetails; + +import org.springframework.lang.NonNull; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * OAuth2 Client 实体封装对象 + * + * @author hccake + */ +public class ClientPrincipal implements OAuth2AuthenticatedPrincipal, Serializable { + + private final String clientId; + + private final Map attributes; + + private final Collection authorities; + + private Set scope = new HashSet<>(); + + @NonNull + public Set getScope() { + return scope; + } + + public void setScope(Collection scope) { + this.scope = Collections.unmodifiableSet(scope == null ? new LinkedHashSet<>() : new LinkedHashSet<>(scope)); + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getName() { + return clientId; + } + + public ClientPrincipal(String clientId, Map attributes, + Collection authorities) { + this.clientId = clientId; + this.attributes = attributes; + this.authorities = (authorities != null) ? Collections.unmodifiableCollection(authorities) + : AuthorityUtils.NO_AUTHORITIES; + } + +} diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/userdetails/User.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/userdetails/User.java new file mode 100644 index 0000000..7fdf4f2 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/userdetails/User.java @@ -0,0 +1,131 @@ +package com.baiye.security.userdetails; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.Map; + +/** + * @author Hccake 2019/9/25 21:03 + */ +@ToString +@Getter +@Builder +public class User implements UserDetails, OAuth2User { + + /** + * 用户ID + */ + private final Long userId; + + /** + * 创建者(父ID) + */ + private final Long whichUserId; + + /** + * 登录账号 + */ + private final String username; + + /** + * 密码 + */ + private final String password; + + /** + * 昵称 + */ + private final String nickname; + + /** + * 头像 + */ + private final String avatar; + + /** + * 状态(1-正常,0-冻结) + */ + private final Integer status; + + /** + * 组织机构ID + */ + private final Long organizationId; + + /** + * 性别(0-默认未知,1-男,2-女) + */ + private final Integer gender; + + /** + * 电子邮件 + */ + private final String email; + + /** + * 手机号 + */ + private final String phoneNumber; + + /** + * 用户类型 + */ + private final Integer type; + + /** + * 用户类型 + */ + private final Integer salesmanType; + + /** + * 权限信息列表 + */ + private final Collection authorities; + + /** + * OAuth2User 必须有属性字段 + */ + private final Map attributes; + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return this.status != null && this.status == 1; + } + + @Override + public A getAttribute(String name) { + return OAuth2User.super.getAttribute(name); + } + + @Override + public Map getAttributes() { + return this.attributes; + } + + @Override + public String getName() { + return this.username; + } + +} diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/util/PasswordUtils.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/util/PasswordUtils.java new file mode 100644 index 0000000..5f5df96 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/util/PasswordUtils.java @@ -0,0 +1,86 @@ +package com.baiye.security.util; + +import cn.hutool.core.codec.Base64; +import cn.hutool.crypto.Mode; +import cn.hutool.crypto.Padding; +import cn.hutool.crypto.symmetric.AES; +import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.DelegatingPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; +import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +/** + * 前后端交互中密码使用 AES 加密,模式: CBC,padding: PKCS5,偏移量暂不定制和密钥相同。
          + * 服务端OAuth2中,密码使用BCrypt方式加密 + * + * @author Hccake + * @version 1.0 + * @date 2019/9/25 15:14 + */ +public final class PasswordUtils { + + private PasswordUtils() { + } + + /** + * 创建一个密码加密的代理,方便后续切换密码的加密算法 + * @see PasswordEncoderFactories#createDelegatingPasswordEncoder() + * @return DelegatingPasswordEncoder + */ + @SuppressWarnings("deprecation") + public static PasswordEncoder createDelegatingPasswordEncoder() { + String encodingId = "bcrypt"; + Map encoders = new HashMap<>(10); + BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); + encoders.put(encodingId, bCryptPasswordEncoder); + encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder()); + encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder()); + encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5")); + encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()); + encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); + encoders.put("scrypt", new SCryptPasswordEncoder()); + encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1")); + encoders.put("SHA-256", + new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256")); + encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder()); + encoders.put("argon2", new Argon2PasswordEncoder()); + DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(encodingId, encoders); + + // 设置默认的密码解析器,以便兼容历史版本的密码 + delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(bCryptPasswordEncoder); + return delegatingPasswordEncoder; + } + + /** + * 将前端传递过来的密文解密为明文 + * @param aesPass AES加密后的密文 + * @param secretKey 密钥 + * @return 明文密码 + */ + public static String decodeAES(String aesPass, String secretKey) { + byte[] secretKeyBytes = secretKey.getBytes(); + AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, secretKeyBytes, secretKeyBytes); + byte[] result = aes.decrypt(Base64.decode(aesPass.getBytes(StandardCharsets.UTF_8))); + return new String(result, StandardCharsets.UTF_8); + } + + /** + * 将明文密码加密为密文 + * @param password 明文密码 + * @param secretKey 密钥 + * @return AES加密后的密文 + */ + public static String encodeAESBase64(String password, String secretKey) { + byte[] secretKeyBytes = secretKey.getBytes(); + AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, secretKeyBytes, secretKeyBytes); + return aes.encryptBase64(password, StandardCharsets.UTF_8); + } + +} diff --git a/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/util/SecurityUtils.java b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/util/SecurityUtils.java new file mode 100644 index 0000000..4cda564 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-core/src/main/java/com/baiye/security/util/SecurityUtils.java @@ -0,0 +1,80 @@ +package com.baiye.security.util; + +import com.baiye.security.userdetails.ClientPrincipal; +import com.baiye.security.userdetails.User; +import lombok.experimental.UtilityClass; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 11:19 + */ +@UtilityClass +public class SecurityUtils { + + /** + * 获取Authentication + */ + public Authentication getAuthentication() { + return SecurityContextHolder.getContext().getAuthentication(); + } + + /** + * 获取系统用户Details + * @param authentication 令牌 + * @return User + *

          + */ + public User getUser(Authentication authentication) { + if (authentication == null) { + return null; + } + Object principal = authentication.getPrincipal(); + if (principal instanceof User) { + return (User) principal; + } + return null; + } + + /** + * 获取用户详情 + */ + public User getUser() { + Authentication authentication = getAuthentication(); + return getUser(authentication); + } + + /** + * 获取客户端信息 + */ + public ClientPrincipal getClientPrincipal() { + Authentication authentication = getAuthentication(); + if (authentication == null) { + return null; + } + Object principal = authentication.getPrincipal(); + if (principal instanceof ClientPrincipal) { + return (ClientPrincipal) principal; + } + return null; + } + + /** + * 获取系统用户ID + * @return 系统用户ID + */ + public Long getCurrentUserId() { + return getUser().getUserId(); + } + + /** + * 获取用户父ID + * @return 用户父ID + */ + public Long getWhichUserId() { + return getUser().getWhichUserId(); + } + +} diff --git a/marketing-scrm-security/security-oauth2-resource-server/pom.xml b/marketing-scrm-security/security-oauth2-resource-server/pom.xml new file mode 100644 index 0000000..c69b259 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-resource-server/pom.xml @@ -0,0 +1,56 @@ + + + + marketing-scrm-security + com.baiye + ${revision} + + 4.0.0 + security-oauth2-resource-server + + + + com.baiye + common-model + ${revision} + + + com.baiye + common-util + ${revision} + + + com.baiye + security-oauth2-core + ${revision} + + + jakarta.servlet + jakarta.servlet-api + compile + + + + org.slf4j + slf4j-api + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.security + spring-security-config + + + org.springframework.security + spring-security-oauth2-authorization-server + compile + + + org.springframework.security + spring-security-oauth2-resource-server + + + diff --git a/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/authentication/AnonymousForeverAuthenticationProvider.java b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/authentication/AnonymousForeverAuthenticationProvider.java new file mode 100644 index 0000000..921eb47 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/authentication/AnonymousForeverAuthenticationProvider.java @@ -0,0 +1,114 @@ +package org.ballcat.springsecurity.authentication; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.server.PathContainer; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.util.pattern.PathPattern; +import org.springframework.web.util.pattern.PathPatternParser; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 于控制部分接口,需要同时支持匿名登录和授权登录的场景。 + *

          + * 与 SpringSecurity 提供的 AnonymousAuthenticationProvider 不同的是,该类不管任何类型的 + * Authentication,都返回匿名对象,以便用户携带了过期或者错误的 token 时转换为匿名用户身份兜底, 可配合 + * {@link GlobalAuthenticationConfigurerAdapter} 使用 + * + * @see org.springframework.security.authentication.AnonymousAuthenticationProvider + * @see org.springframework.security.web.authentication.AnonymousAuthenticationFilter + * @author hccake + */ +@Slf4j +public class AnonymousForeverAuthenticationProvider implements AuthenticationProvider { + + private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); + + private final String key; + + private final Object principal; + + private final List authorities; + + private final List pathPatterns; + + public AnonymousForeverAuthenticationProvider(List pathList) { + if (CollectionUtils.isEmpty(pathList)) { + pathPatterns = new ArrayList<>(); + } + else { + pathPatterns = pathList.stream().map(PathPatternParser.defaultInstance::parse).collect(Collectors.toList()); + } + + this.key = UUID.randomUUID().toString(); + this.principal = "anonymousUser"; + this.authorities = AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"); + } + + public AnonymousForeverAuthenticationProvider(String key, Object principal, List authorities, + List pathPatterns) { + this.key = key; + this.principal = principal; + this.authorities = authorities; + this.pathPatterns = pathPatterns; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + HttpServletRequest request = Optional.ofNullable(RequestContextHolder.getRequestAttributes()) + .map(x -> (ServletRequestAttributes) x) + .map(ServletRequestAttributes::getRequest) + .orElse(null); + if (request == null) { + return null; + } + + String requestUri = request.getRequestURI(); + PathContainer pathContainer = PathContainer.parsePath(requestUri); + + boolean anyMatch = pathPatterns.stream().anyMatch(x -> x.matches(pathContainer)); + if (anyMatch) { + Authentication anonymousAuthentication = createAuthentication(request); + log.debug("Set SecurityContextHolder to anonymous SecurityContext"); + return anonymousAuthentication; + } + + return null; + } + + @Override + public boolean supports(Class authentication) { + return true; + } + + protected Authentication createAuthentication(HttpServletRequest request) { + AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(this.key, this.principal, + this.authorities); + token.setDetails(this.authenticationDetailsSource.buildDetails(request)); + return token; + } + + public void setAuthenticationDetailsSource( + AuthenticationDetailsSource authenticationDetailsSource) { + Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required"); + this.authenticationDetailsSource = authenticationDetailsSource; + } + +} diff --git a/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/OAuth2ResourceServerAutoConfiguration.java b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/OAuth2ResourceServerAutoConfiguration.java new file mode 100644 index 0000000..73d56c7 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/OAuth2ResourceServerAutoConfiguration.java @@ -0,0 +1,128 @@ +package org.ballcat.springsecurity.oauth2.server.resource; + +import com.baiye.security.component.CustomPermissionEvaluator; +import org.ballcat.springsecurity.oauth2.server.resource.configurer.BallcatOauth2ResourceServerSecurityFilterChainBuilder; +import org.ballcat.springsecurity.oauth2.server.resource.configurer.OAuth2ResourceServerConfigurerCustomizer; +import org.ballcat.springsecurity.oauth2.server.resource.configurer.OAuth2ResourceServerExtensionConfigurer; +import org.ballcat.springsecurity.oauth2.server.resource.configurer.Oauth2ResourceServerSecurityFilterChainBuilder; +import org.ballcat.springsecurity.oauth2.server.resource.introspection.BallcatRemoteOpaqueTokenIntrospector; +import org.ballcat.springsecurity.oauth2.server.resource.web.CustomAuthenticationEntryPoint; +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.resource.properties.OAuth2ResourceServerProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +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 org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; + +import java.util.List; + +/** + * 资源服务需要的一些 bean 配置 + * + * @author hccake + */ +@RequiredArgsConstructor +@Configuration(proxyBeanMethods = false) +@EnableGlobalMethodSecurity(prePostEnabled = true) +@EnableConfigurationProperties(OAuth2ResourceServerProperties.class) +public class OAuth2ResourceServerAutoConfiguration { + + public static final String OAUTH2_RESOURCE_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME = "oauth2ResourceServerSecurityFilterChain"; + + /** + * 资源服务器的过滤器链构建器 + */ + @Bean + @ConditionalOnMissingBean(name = OAUTH2_RESOURCE_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME, + value = Oauth2ResourceServerSecurityFilterChainBuilder.class) + public Oauth2ResourceServerSecurityFilterChainBuilder oauth2ResourceServerSecurityFilterChainBuilder( + OAuth2ResourceServerProperties oAuth2ResourceServerProperties, + AuthenticationEntryPoint authenticationEntryPoint, BearerTokenResolver bearerTokenResolver, + ObjectProvider> configurerCustomizersProvider, + ObjectProvider>> extensionConfigurersProvider) { + return new BallcatOauth2ResourceServerSecurityFilterChainBuilder(oAuth2ResourceServerProperties, + authenticationEntryPoint, bearerTokenResolver, configurerCustomizersProvider, + extensionConfigurersProvider); + } + + /** + * OAuth2 授权服务器的安全过滤器链,如果和资源服务器共存,需要将其放在资源服务器之前 + */ + @Bean(name = OAUTH2_RESOURCE_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME) + @Order(99) + @ConditionalOnMissingBean(name = OAUTH2_RESOURCE_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME) + public SecurityFilterChain oauth2ResourceServerSecurityFilterChain( + Oauth2ResourceServerSecurityFilterChainBuilder builder, HttpSecurity httpSecurity) throws Exception { + return builder.build(httpSecurity); + } + + /** + * 自定义的权限判断组件 + * @return CustomPermissionEvaluator + */ + @Bean(name = "per") + @ConditionalOnMissingBean(CustomPermissionEvaluator.class) + public CustomPermissionEvaluator customPermissionEvaluator() { + return new CustomPermissionEvaluator(); + } + + /** + * 当资源服务器和授权服务器的 token 存储无法共享时,通过远程调用的方式,向授权服务鉴定 token,并同时获取对应的授权信息 + * @return NimbusOpaqueTokenIntrospector + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "ballcat.security.oauth2.resourceserver", name = "shared-stored-token", + havingValue = "false") + public OpaqueTokenIntrospector opaqueTokenIntrospector( + OAuth2ResourceServerProperties oAuth2ResourceServerProperties) { + OAuth2ResourceServerProperties.Opaquetoken opaqueToken = oAuth2ResourceServerProperties.getOpaqueToken(); + return new BallcatRemoteOpaqueTokenIntrospector(opaqueToken.getIntrospectionUri(), opaqueToken.getClientId(), + opaqueToken.getClientSecret()); + } + + /** + * spring-security 5.x 中开启资源服务器功能,需要的不透明令牌的支持 + * @return OpaqueTokenAuthenticationProvider + */ + @Bean + @ConditionalOnMissingBean + public OpaqueTokenAuthenticationProvider opaqueTokenAuthenticationProvider( + OpaqueTokenIntrospector opaqueTokenIntrospector) { + return new OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector); + } + + /** + * 自定义异常处理 + * @return AuthenticationEntryPoint + */ + @Bean + @ConditionalOnMissingBean + public AuthenticationEntryPoint authenticationEntryPoint() { + return new CustomAuthenticationEntryPoint(); + } + + /** + * BearTokenResolve 允许使用 url 传参,方便 ws 连接 ps: 使用 url 传参不安全,待改进 + * @return DefaultBearerTokenResolver + */ + @Bean + @ConditionalOnMissingBean + public BearerTokenResolver bearerTokenResolver() { + DefaultBearerTokenResolver defaultBearerTokenResolver = new DefaultBearerTokenResolver(); + defaultBearerTokenResolver.setAllowUriQueryParameter(true); + return defaultBearerTokenResolver; + } + +} diff --git a/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/annotation/EnableOauth2ResourceServer.java b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/annotation/EnableOauth2ResourceServer.java new file mode 100644 index 0000000..75af35c --- /dev/null +++ b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/annotation/EnableOauth2ResourceServer.java @@ -0,0 +1,20 @@ +package org.ballcat.springsecurity.oauth2.server.resource.annotation; + +import org.ballcat.springsecurity.oauth2.server.resource.OAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; + +import java.lang.annotation.*; + +/** + * 开启 Oauth2 资源服务器 + * + * @author hccake + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ImportAutoConfiguration(OAuth2ResourceServerAutoConfiguration.class) +public @interface EnableOauth2ResourceServer { + +} diff --git a/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/BallcatOauth2ResourceServerSecurityFilterChainBuilder.java b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/BallcatOauth2ResourceServerSecurityFilterChainBuilder.java new file mode 100644 index 0000000..320b6d5 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/BallcatOauth2ResourceServerSecurityFilterChainBuilder.java @@ -0,0 +1,77 @@ +package org.ballcat.springsecurity.oauth2.server.resource.configurer; + +import cn.hutool.core.util.ArrayUtil; +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.resource.properties.OAuth2ResourceServerProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; + +import java.util.Collections; +import java.util.List; + +/** + * 资源服务器的配置 + * + * @author hccake + */ +@RequiredArgsConstructor +public class BallcatOauth2ResourceServerSecurityFilterChainBuilder + implements Oauth2ResourceServerSecurityFilterChainBuilder { + + private final OAuth2ResourceServerProperties oAuth2ResourceServerProperties; + + private final AuthenticationEntryPoint authenticationEntryPoint; + + private final BearerTokenResolver bearerTokenResolver; + + private final ObjectProvider> configurerCustomizersProvider; + + private final ObjectProvider>> extensionConfigurersProvider; + + @Override + public SecurityFilterChain build(HttpSecurity http) throws Exception { + // @formatter:off + http + // 拦截 url 配置 + .authorizeRequests() + .antMatchers(ArrayUtil.toArray(oAuth2ResourceServerProperties.getIgnoreUrls(), String.class)) + .permitAll() + .anyRequest().authenticated() + + // 关闭 csrf 跨站攻击防护 + .and().csrf().disable() + + // 开启 OAuth2 资源服务 + .oauth2ResourceServer().authenticationEntryPoint(authenticationEntryPoint) + // bearToken 解析器 + .bearerTokenResolver(bearerTokenResolver) + // 不透明令牌, + .opaqueToken(); + // @formatter:on + + // 允许嵌入iframe + if (!oAuth2ResourceServerProperties.isIframeDeny()) { + http.headers().frameOptions().disable(); + } + + // 自定义处理 + List configurerCustomizers = configurerCustomizersProvider + .getIfAvailable(Collections::emptyList); + for (OAuth2ResourceServerConfigurerCustomizer configurerCustomizer : configurerCustomizers) { + configurerCustomizer.customize(http); + } + + // 扩展配置 + List> extensionConfigurers = extensionConfigurersProvider + .getIfAvailable(Collections::emptyList); + for (OAuth2ResourceServerExtensionConfigurer extensionConfigurer : extensionConfigurers) { + http.apply(extensionConfigurer); + } + + return http.build(); + } + +} \ No newline at end of file diff --git a/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/OAuth2ResourceServerConfigurerCustomizer.java b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/OAuth2ResourceServerConfigurerCustomizer.java new file mode 100644 index 0000000..517e26e --- /dev/null +++ b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/OAuth2ResourceServerConfigurerCustomizer.java @@ -0,0 +1,18 @@ +package org.ballcat.springsecurity.oauth2.server.resource.configurer; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; + +/** + * 对 Ballcat 默认的配置的 OAuth2ResourceServerConfigurer 进行自定义处理 + * + * @author hccake + */ +public interface OAuth2ResourceServerConfigurerCustomizer { + + /** + * 对资源服务器配置进行自定义 + * @param httpSecurity security configuration + */ + void customize(HttpSecurity httpSecurity) throws Exception; + +} diff --git a/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/OAuth2ResourceServerExtensionConfigurer.java b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/OAuth2ResourceServerExtensionConfigurer.java new file mode 100644 index 0000000..b22a8ce --- /dev/null +++ b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/OAuth2ResourceServerExtensionConfigurer.java @@ -0,0 +1,14 @@ +package org.ballcat.springsecurity.oauth2.server.resource.configurer; + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; + +/** + * OAuth2资源服务器的扩展配置 + * + * @author hccake + */ +public abstract class OAuth2ResourceServerExtensionConfigurer> + extends AbstractHttpConfigurer, H> { + +} diff --git a/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/Oauth2ResourceServerSecurityFilterChainBuilder.java b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/Oauth2ResourceServerSecurityFilterChainBuilder.java new file mode 100644 index 0000000..9f37b8e --- /dev/null +++ b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/Oauth2ResourceServerSecurityFilterChainBuilder.java @@ -0,0 +1,21 @@ +package org.ballcat.springsecurity.oauth2.server.resource.configurer; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +/** + * OAuth2 Resource Server 的 SecurityFilterChain 构造器 + * + * @author hccake + */ +public interface Oauth2ResourceServerSecurityFilterChainBuilder { + + /** + * 构建 OAuth2 Resource Server 的 SecurityFilterChain + * @param http HttpSecurity + * @return SecurityFilterChain + * @throws Exception 构建异常 + */ + SecurityFilterChain build(HttpSecurity http) throws Exception; + +} diff --git a/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/introspection/BallcatRemoteOpaqueTokenIntrospector.java b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/introspection/BallcatRemoteOpaqueTokenIntrospector.java new file mode 100644 index 0000000..0ad38ec --- /dev/null +++ b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/introspection/BallcatRemoteOpaqueTokenIntrospector.java @@ -0,0 +1,326 @@ +package org.ballcat.springsecurity.oauth2.server.resource.introspection; + +import cn.hutool.core.collection.CollUtil; +import com.baiye.security.constant.TokenAttributeNameConstants; +import com.baiye.security.constant.UserAttributeNameConstants; +import com.baiye.security.constant.UserInfoFiledNameConstants; +import com.baiye.security.userdetails.ClientPrincipal; +import com.baiye.security.userdetails.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.*; +import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; +import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * copy from {@link SpringOpaqueTokenIntrospector},重写了 OAuth2AuthenticatedPrincipal + * 的构建,保持项目内统一使用 {@link User} + * + * A Spring implementation of {@link OpaqueTokenIntrospector} that verifies and + * introspects a token using the configured + * OAuth 2.0 Introspection + * Endpoint. + * + * @author Josh Cummings + * @author Hccake + * @since 1.1.0 + */ +public class BallcatRemoteOpaqueTokenIntrospector implements OpaqueTokenIntrospector { + + private static final String AUTHORITY_PREFIX = "SCOPE_"; + + private static final ParameterizedTypeReference> STRING_OBJECT_MAP = new ParameterizedTypeReference>() { + }; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final RestOperations restOperations; + + private Converter> requestEntityConverter; + + private static final List INTROSPECTION_CLAIM_NAMES = Arrays.asList( + OAuth2TokenIntrospectionClaimNames.ACTIVE, OAuth2TokenIntrospectionClaimNames.USERNAME, + OAuth2TokenIntrospectionClaimNames.CLIENT_ID, OAuth2TokenIntrospectionClaimNames.SCOPE, + OAuth2TokenIntrospectionClaimNames.TOKEN_TYPE, OAuth2TokenIntrospectionClaimNames.EXP, + OAuth2TokenIntrospectionClaimNames.IAT, OAuth2TokenIntrospectionClaimNames.NBF, + OAuth2TokenIntrospectionClaimNames.SUB, OAuth2TokenIntrospectionClaimNames.AUD, + OAuth2TokenIntrospectionClaimNames.ISS, OAuth2TokenIntrospectionClaimNames.JTI); + + /** + * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters + * @param introspectionUri The introspection endpoint uri + * @param clientId The client id authorized to introspect + * @param clientSecret The client's secret + */ + public BallcatRemoteOpaqueTokenIntrospector(String introspectionUri, String clientId, String clientSecret) { + Assert.notNull(introspectionUri, "introspectionUri cannot be null"); + Assert.notNull(clientId, "clientId cannot be null"); + Assert.notNull(clientSecret, "clientSecret cannot be null"); + this.requestEntityConverter = this.defaultRequestEntityConverter(URI.create(introspectionUri)); + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret)); + this.restOperations = restTemplate; + } + + /** + * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters + *

          + * The given {@link RestOperations} should perform its own client authentication + * against the introspection endpoint. + * @param introspectionUri The introspection endpoint uri + * @param restOperations The client for performing the introspection request + */ + public BallcatRemoteOpaqueTokenIntrospector(String introspectionUri, RestOperations restOperations) { + Assert.notNull(introspectionUri, "introspectionUri cannot be null"); + Assert.notNull(restOperations, "restOperations cannot be null"); + this.requestEntityConverter = this.defaultRequestEntityConverter(URI.create(introspectionUri)); + this.restOperations = restOperations; + } + + private Converter> defaultRequestEntityConverter(URI introspectionUri) { + return token -> { + HttpHeaders headers = requestHeaders(); + MultiValueMap body = requestBody(token); + return new RequestEntity<>(body, headers, HttpMethod.POST, introspectionUri); + }; + } + + private HttpHeaders requestHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + return headers; + } + + private MultiValueMap requestBody(String token) { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("token", token); + return body; + } + + @Override + public OAuth2AuthenticatedPrincipal introspect(String token) { + RequestEntity requestEntity = this.requestEntityConverter.convert(token); + if (requestEntity == null) { + throw new OAuth2IntrospectionException("requestEntityConverter returned a null entity"); + } + ResponseEntity> responseEntity = makeRequest(requestEntity); + Map claims = adaptToNimbusResponse(responseEntity); + return convertClaimsSet(claims); + } + + /** + * Sets the {@link Converter} used for converting the OAuth 2.0 access token to a + * {@link RequestEntity} representation of the OAuth 2.0 token introspection request. + * @param requestEntityConverter the {@link Converter} used for converting to a + * {@link RequestEntity} representation of the token introspection request + */ + public void setRequestEntityConverter(Converter> requestEntityConverter) { + Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null"); + this.requestEntityConverter = requestEntityConverter; + } + + private ResponseEntity> makeRequest(RequestEntity requestEntity) { + try { + return this.restOperations.exchange(requestEntity, STRING_OBJECT_MAP); + } + catch (Exception ex) { + throw new OAuth2IntrospectionException(ex.getMessage(), ex); + } + } + + private Map adaptToNimbusResponse(ResponseEntity> responseEntity) { + if (responseEntity.getStatusCode() != HttpStatus.OK) { + throw new OAuth2IntrospectionException( + "Introspection endpoint responded with " + responseEntity.getStatusCode()); + } + Map claims = responseEntity.getBody(); + // relying solely on the authorization server to validate this token (not checking + // 'exp', for example) + if (claims == null) { + return Collections.emptyMap(); + } + + boolean active = (boolean) claims.compute(OAuth2TokenIntrospectionClaimNames.ACTIVE, (k, v) -> { + if (v instanceof String) { + return Boolean.parseBoolean((String) v); + } + if (v instanceof Boolean) { + return v; + } + return false; + }); + if (!active) { + this.logger.trace("Did not validate token since it is inactive"); + // throw new BadOpaqueTokenException("Provided token isn't active"); + throw new BadOpaqueTokenException("当前登录状态已过期,请重新登录!"); + } + return claims; + } + + private OAuth2AuthenticatedPrincipal convertClaimsSet(Map claims) { + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> { + if (v instanceof String) { + return Collections.singletonList(v); + } + return v; + }); + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString()); + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP, + (k, v) -> Instant.ofEpochSecond(((Number) v).longValue())); + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT, + (k, v) -> Instant.ofEpochSecond(((Number) v).longValue())); + // RFC-7662 page 7 directs users to RFC-7519 for defining the values of these + // issuer fields. + // https://datatracker.ietf.org/doc/html/rfc7662#page-7 + // + // RFC-7519 page 9 defines issuer fields as being 'case-sensitive' strings + // containing + // a 'StringOrURI', which is defined on page 5 as being any string, but strings + // containing ':' + // should be treated as valid URIs. + // https://datatracker.ietf.org/doc/html/rfc7519#section-2 + // + // It is not defined however as to whether-or-not normalized URIs should be + // treated as the same literal + // value. It only defines validation itself, so to avoid potential ambiguity or + // unwanted side effects that + // may be awkward to debug, we do not want to manipulate this value. Previous + // versions of Spring Security + // would *only* allow valid URLs, which is not what we wish to achieve here. + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString()); + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF, + (k, v) -> Instant.ofEpochSecond(((Number) v).longValue())); + Collection authorities = new ArrayList<>(); + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> { + if (v instanceof String) { + Collection scopes = Arrays.asList(((String) v).split(" ")); + for (String scope : scopes) { + authorities.add(new SimpleGrantedAuthority(AUTHORITY_PREFIX + scope)); + } + return scopes; + } + return v; + }); + + boolean isClient = (boolean) claims.compute(TokenAttributeNameConstants.IS_CLIENT, (k, v) -> { + if (v instanceof String) { + return Boolean.parseBoolean((String) v); + } + if (v instanceof Boolean) { + return v; + } + logger.warn("自定端点返回的 {} 属性解析异常, 值为: {},", TokenAttributeNameConstants.IS_CLIENT, v); + return false; + }); + + return isClient ? buildClient(claims, authorities) : buildUser(claims); + } + + @SuppressWarnings("unchecked") + private ClientPrincipal buildClient(Map claims, Collection authorities) { + String clientId = (String) claims.get(OAuth2TokenIntrospectionClaimNames.CLIENT_ID); + Collection scopes = (Collection) claims.getOrDefault(OAuth2TokenIntrospectionClaimNames.SCOPE, + new ArrayList<>()); + + ClientPrincipal clientPrincipal = new ClientPrincipal(clientId, claims, authorities); + clientPrincipal.setScope(scopes); + + return clientPrincipal; + } + + /** + * 根据返回值信息,反向构建出 User 对象 + * @param claims claims + * @return User + */ + @SuppressWarnings("unchecked") + private User buildUser(Map claims) { + User.UserBuilder builder = User.builder(); + + LinkedHashMap info = (LinkedHashMap) claims + .getOrDefault(TokenAttributeNameConstants.INFO, new LinkedHashMap<>()); + + Object userIdObject = info.get(UserInfoFiledNameConstants.USER_ID); + if (userIdObject != null) { + builder.userId(Long.parseLong(userIdObject.toString())); + } + Object whichUserIdObj = info.get(UserInfoFiledNameConstants.WHICH_USER_ID); + if (whichUserIdObj != null) { + builder.whichUserId(Long.parseLong(whichUserIdObj.toString())); + } + Object organizationIdObject = info.get(UserInfoFiledNameConstants.ORGANIZATION_ID); + if (organizationIdObject != null) { + builder.organizationId(Long.parseLong(organizationIdObject.toString())); + } + + builder.username(getOrDefault(info, UserInfoFiledNameConstants.USERNAME, "")) + .nickname(getOrDefault(info, UserInfoFiledNameConstants.NICKNAME, "")) + .avatar(getOrDefault(info, UserInfoFiledNameConstants.AVATAR, "")) + .email(getOrDefault(info, UserInfoFiledNameConstants.EMAIL, "")) + .phoneNumber(getOrDefault(info, UserInfoFiledNameConstants.PHONE_NUMBER, "")) + .gender(getOrDefault(info, UserInfoFiledNameConstants.GENDER, null)) + .type(getOrDefault(info, UserInfoFiledNameConstants.TYPE, null)) + .salesmanType(getOrDefault(info, UserInfoFiledNameConstants.SALESMAN_TYPE, 0)) + .status(getOrDefault(info, UserInfoFiledNameConstants.STATUS, null)); + + Collection authorities = null; + List authoritiesJsonArray = (List) claims.getOrDefault("authorities", new ArrayList<>()); + if (authoritiesJsonArray != null) { + authorities = AuthorityUtils.createAuthorityList((authoritiesJsonArray).toArray(new String[0])); + builder.authorities(authorities); + } + + Map attributesMap = (Map) claims + .getOrDefault(TokenAttributeNameConstants.ATTRIBUTES, new HashMap<>(0)); + if (!CollectionUtils.isEmpty(attributesMap)) { + claims.putAll(attributesMap); + // 暂时做下兼容,SAS 不返回 authorities 信息了 + if (CollUtil.isEmpty(authorities)) { + Collection roleCodes = (Collection) attributesMap + .getOrDefault(UserAttributeNameConstants.ROLE_CODES, Collections.emptySet()); + Collection permissions = (Collection) attributesMap + .getOrDefault(UserAttributeNameConstants.PERMISSIONS, Collections.emptySet()); + authorities = Stream.of(roleCodes, permissions) + .flatMap(Collection::stream) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + builder.authorities(authorities); + } + } + + // 从 claims 中提取 oauth2 中的必须属性 + for (String claimName : INTROSPECTION_CLAIM_NAMES) { + attributesMap.put(claimName, claims.get(claimName)); + } + + return builder.attributes(attributesMap).build(); + } + + private static T getOrDefault(LinkedHashMap info, String key, T defaultValue) { + Object value = info.get(key); + return value == null ? defaultValue : (T) value; + } + +} diff --git a/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/introspection/SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector.java b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/introspection/SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector.java new file mode 100644 index 0000000..94a1eef --- /dev/null +++ b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/introspection/SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector.java @@ -0,0 +1,93 @@ +package org.ballcat.springsecurity.oauth2.server.resource.introspection; + +import cn.hutool.core.collection.CollUtil; +import com.baiye.security.userdetails.ClientPrincipal; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Set; + +/** + * 共享存储的不透明令牌的处理器 + *

          + * 当资源服务器与授权服务器共享了 TokenStore, 比如使用 RedisTokenStore 时共享了 redis,或者 jdbcTokenStore 时,共享了数据库。 + * 此时可以直接使用 spring-authorization-server 中的 OAuth2AuthorizationService 进行读取。 + * + * @author hccake + */ +@Slf4j +public class SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector implements OpaqueTokenIntrospector { + + private static final String AUTHORITY_SCOPE_PREFIX = "SCOPE_"; + + private final OAuth2AuthorizationService authorizationService; + + public SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector( + OAuth2AuthorizationService authorizationService) { + this.authorizationService = authorizationService; + } + + /** + * @param accessTokenValue token + * @return OAuth2AuthenticatedPrincipal + */ + @Override + public OAuth2AuthenticatedPrincipal introspect(String accessTokenValue) { + OAuth2Authorization authorization = authorizationService.findByToken(accessTokenValue, null); + if (authorization == null) { + if (log.isTraceEnabled()) { + log.trace("Did not authenticate token introspection request since token was not found"); + } + throw new BadOpaqueTokenException("Invalid access token: " + accessTokenValue); + } + + OAuth2Authorization.Token authorizedToken = authorization.getToken(accessTokenValue); + if (authorizedToken == null || !authorizedToken.isActive()) { + if (log.isTraceEnabled()) { + log.trace("Did not validate token since it is inactive"); + } + // throw new BadOpaqueTokenException("Provided token isn't active"); + throw new BadOpaqueTokenException("当前登录状态已过期,请重新登录!"); + } + + AuthorizationGrantType authorizationGrantType = authorization.getAuthorizationGrantType(); + if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(authorizationGrantType)) { + return getClientPrincipal(authorization); + } + + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) authorization + .getAttributes() + .get("java.security.Principal"); + return (OAuth2AuthenticatedPrincipal) usernamePasswordAuthenticationToken.getPrincipal(); + } + + private ClientPrincipal getClientPrincipal(OAuth2Authorization oAuth2Authentication) { + ClientPrincipal clientPrincipal; + + Set authorizedScopes = oAuth2Authentication.getAuthorizedScopes(); + + Collection authorities = new ArrayList<>(); + if (CollUtil.isNotEmpty(authorizedScopes)) { + for (String scope : authorizedScopes) { + authorities.add(new SimpleGrantedAuthority(AUTHORITY_SCOPE_PREFIX + scope)); + } + } + clientPrincipal = new ClientPrincipal(oAuth2Authentication.getPrincipalName(), new HashMap<>(8), authorities); + clientPrincipal.setScope(authorizedScopes); + + return clientPrincipal; + } + +} diff --git a/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/properties/OAuth2ResourceServerProperties.java b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/properties/OAuth2ResourceServerProperties.java new file mode 100644 index 0000000..748bb6f --- /dev/null +++ b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/properties/OAuth2ResourceServerProperties.java @@ -0,0 +1,62 @@ +package org.ballcat.springsecurity.oauth2.server.resource.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +/** + * 资源服务器的配置文件,用于配置 token 鉴定方式。由于目前 ballcat 授权服务器使用 不透明令牌,所以这里也暂时不做 jwt令牌支持的扩展 + * + * @see org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties + * @author hccake + */ +@Getter +@Setter +@ConfigurationProperties(prefix = "ballcat.security.oauth2.resourceserver") +public class OAuth2ResourceServerProperties { + + /** + * 忽略鉴权的 url 列表 + */ + private List ignoreUrls = new ArrayList<>(); + + /** + * 是否禁止嵌入iframe + */ + private boolean iframeDeny = true; + + /** + * 共享存储的token,这种情况下,利用 tokenStore 可以直接获取 token 信息 + */ + private boolean sharedStoredToken = true; + + /** + * 当 sharedStoredToken 为 false 时生效。 主要用于配置远程端点 + */ + private final Opaquetoken opaqueToken = new Opaquetoken(); + + @Getter + @Setter + public static class Opaquetoken { + + /** + * Client id used to authenticate with the token introspection endpoint. + */ + private String clientId; + + /** + * Client secret used to authenticate with the token introspection endpoint. + */ + private String clientSecret; + + /** + * OAuth 2.0 endpoint through which token introspection is accomplished. + */ + private String introspectionUri; + + } + +} diff --git a/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/web/CustomAuthenticationEntryPoint.java b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/web/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..6da2184 --- /dev/null +++ b/marketing-scrm-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/web/CustomAuthenticationEntryPoint.java @@ -0,0 +1,37 @@ +package org.ballcat.springsecurity.oauth2.server.resource.web; + +import com.baiye.result.R; +import com.baiye.result.SystemResultCode; +import com.baiye.util.JsonUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/25 22:04 + */ +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, + AuthenticationException e) throws IOException, ServletException { + + String utf8 = StandardCharsets.UTF_8.toString(); + + httpServletResponse.setHeader("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE); + httpServletResponse.setCharacterEncoding(utf8); + httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); + R r = R.failed(SystemResultCode.UNAUTHORIZED, e.getMessage()); + httpServletResponse.getWriter().write(JsonUtils.toJson(r)); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/pom.xml b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/pom.xml new file mode 100644 index 0000000..e8cb87c --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/pom.xml @@ -0,0 +1,78 @@ + + + + marketing-scrm-starters + com.baiye + ${revision} + + 4.0.0 + marketing-scrm-starter-easyexcel + + + + com.alibaba + easyexcel + + + org.apache.commons + commons-compress + + + + + jakarta.servlet + jakarta.servlet-api + provided + + + + org.apache.commons + commons-compress + 1.21 + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework + spring-web + + + org.springframework + spring-webmvc + + + + cn.hutool + hutool-all + + + + com.baiye + common-util + + + + + org.springframework.boot + spring-boot-starter-web + test + + + com.baiye + common-core + + + diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/alibaba/excel/read/processor/DefaultAnalysisEventProcessor.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/alibaba/excel/read/processor/DefaultAnalysisEventProcessor.java new file mode 100644 index 0000000..7694fe5 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/alibaba/excel/read/processor/DefaultAnalysisEventProcessor.java @@ -0,0 +1,157 @@ +package com.alibaba.excel.read.processor; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.enums.HeadKindEnum; +import com.alibaba.excel.enums.RowTypeEnum; +import com.alibaba.excel.exception.ExcelAnalysisException; +import com.alibaba.excel.exception.ExcelAnalysisStopException; +import com.alibaba.excel.metadata.Head; +import com.alibaba.excel.metadata.data.ReadCellData; +import com.alibaba.excel.read.listener.ReadListener; +import com.alibaba.excel.read.metadata.holder.ReadRowHolder; +import com.alibaba.excel.read.metadata.property.ExcelReadHeadProperty; +import com.alibaba.excel.util.ConverterUtils; +import com.alibaba.excel.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Analysis event + * + * @author jipengfei + */ +public class DefaultAnalysisEventProcessor implements AnalysisEventProcessor { + + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultAnalysisEventProcessor.class); + + @Override + public void extra(AnalysisContext analysisContext) { + dealExtra(analysisContext); + } + + @Override + public void endRow(AnalysisContext analysisContext) { + if (RowTypeEnum.EMPTY.equals(analysisContext.readRowHolder().getRowType())) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Empty row!"); + } + if (analysisContext.readWorkbookHolder().getIgnoreEmptyRow()) { + return; + } + } + dealData(analysisContext); + } + + @Override + public void endSheet(AnalysisContext analysisContext) { + for (ReadListener readListener : analysisContext.currentReadHolder().readListenerList()) { + readListener.doAfterAllAnalysed(analysisContext); + } + } + + private void dealExtra(AnalysisContext analysisContext) { + for (ReadListener readListener : analysisContext.currentReadHolder().readListenerList()) { + try { + readListener.extra(analysisContext.readSheetHolder().getCellExtra(), analysisContext); + } + catch (Exception e) { + onException(analysisContext, e); + break; + } + if (!readListener.hasNext(analysisContext)) { + throw new ExcelAnalysisStopException(); + } + } + } + + private void onException(AnalysisContext analysisContext, Exception e) { + for (ReadListener readListenerException : analysisContext.currentReadHolder().readListenerList()) { + try { + readListenerException.onException(e, analysisContext); + } + catch (RuntimeException re) { + throw re; + } + catch (Exception e1) { + throw new ExcelAnalysisException(e1.getMessage(), e1); + } + } + } + + private void dealData(AnalysisContext analysisContext) { + ReadRowHolder readRowHolder = analysisContext.readRowHolder(); + Map> cellDataMap = (Map) readRowHolder.getCellMap(); + readRowHolder.setCurrentRowAnalysisResult(cellDataMap); + int rowIndex = readRowHolder.getRowIndex(); + int currentHeadRowNumber = analysisContext.readSheetHolder().getHeadRowNumber(); + + boolean isData = rowIndex >= currentHeadRowNumber; + + // Now is data + for (ReadListener readListener : analysisContext.currentReadHolder().readListenerList()) { + try { + if (isData) { + readListener.invoke(readRowHolder.getCurrentRowAnalysisResult(), analysisContext); + } + else { + readListener.invokeHead(cellDataMap, analysisContext); + } + } + catch (Exception e) { + onException(analysisContext, e); + break; + } + if (!readListener.hasNext(analysisContext)) { + throw new ExcelAnalysisStopException(); + } + } + + // Last head column + if (!isData && currentHeadRowNumber == rowIndex + 1) { + buildHead(analysisContext, cellDataMap); + } + } + + private void buildHead(AnalysisContext analysisContext, Map> cellDataMap) { + if (!HeadKindEnum.CLASS.equals(analysisContext.currentReadHolder().excelReadHeadProperty().getHeadKind())) { + return; + } + Map dataMap = ConverterUtils.convertToStringMap(cellDataMap, analysisContext); + ExcelReadHeadProperty excelHeadPropertyData = analysisContext.readSheetHolder().excelReadHeadProperty(); + Map headMapData = excelHeadPropertyData.getHeadMap(); + Map tmpHeadMap = new HashMap<>(headMapData.size() * 4 / 3 + 1); + for (Map.Entry entry : headMapData.entrySet()) { + Head headData = entry.getValue(); + if (headData.getForceIndex() || !headData.getForceName()) { + tmpHeadMap.put(entry.getKey(), headData); + continue; + } + List headNameList = headData.getHeadNameList(); + String headName = headNameList.get(headNameList.size() - 1); + for (Map.Entry stringEntry : dataMap.entrySet()) { + if (stringEntry == null) { + continue; + } + String headString = stringEntry.getValue(); + Integer stringKey = stringEntry.getKey(); + if (StringUtils.isEmpty(headString)) { + continue; + } + if (analysisContext.currentReadHolder().globalConfiguration().getAutoTrim()) { + headString = headString.trim(); + } + if (headName.equals(headString)) { + headData.setColumnIndex(stringKey); + tmpHeadMap.put(stringKey, headData); + break; + } + } + } + excelHeadPropertyData.setHeadMap(tmpHeadMap); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/ExcelHandlerConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/ExcelHandlerConfiguration.java new file mode 100644 index 0000000..87dc2e2 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/ExcelHandlerConfiguration.java @@ -0,0 +1,85 @@ +package com.baiye.common.excel; + +import com.alibaba.excel.converters.Converter; +import com.baiye.common.excel.aop.ResponseExcelReturnValueHandler; +import com.baiye.common.excel.config.ExcelConfigProperties; +import com.baiye.common.excel.enhance.DefaultWriterBuilderEnhancer; +import com.baiye.common.excel.enhance.WriterBuilderEnhancer; +import com.baiye.common.excel.handler.ManySheetWriteHandler; +import com.baiye.common.excel.handler.SheetWriteHandler; +import com.baiye.common.excel.handler.SingleSheetWriteHandler; +import com.baiye.common.excel.head.I18nHeaderCellWriteHandler; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * @author Hccake 2020/10/28 + * @version 1.0 + */ +@RequiredArgsConstructor +@Configuration +public class ExcelHandlerConfiguration { + + private final ExcelConfigProperties configProperties; + + private final ObjectProvider>> converterProvider; + + /** + * ExcelBuild增强 + * @return DefaultWriterBuilderEnhancer 默认什么也不做的增强器 + */ + @Bean + @ConditionalOnMissingBean + public WriterBuilderEnhancer writerBuilderEnhancer() { + return new DefaultWriterBuilderEnhancer(); + } + + /** + * 单sheet 写入处理器 + */ + @Bean + @ConditionalOnMissingBean + public SingleSheetWriteHandler singleSheetWriteHandler() { + return new SingleSheetWriteHandler(configProperties, converterProvider, writerBuilderEnhancer()); + } + + /** + * 多sheet 写入处理器 + */ + @Bean + @ConditionalOnMissingBean + public ManySheetWriteHandler manySheetWriteHandler() { + return new ManySheetWriteHandler(configProperties, converterProvider, writerBuilderEnhancer()); + } + + /** + * 返回Excel文件的 response 处理器 + * @param sheetWriteHandlerList 页签写入处理器集合 + * @return ResponseExcelReturnValueHandler + */ + @Bean + @ConditionalOnMissingBean + public ResponseExcelReturnValueHandler responseExcelReturnValueHandler( + List sheetWriteHandlerList) { + return new ResponseExcelReturnValueHandler(sheetWriteHandlerList); + } + + /** + * excel 头的国际化处理器 + * @param messageSource 国际化源 + */ + @Bean + @ConditionalOnBean(MessageSource.class) + @ConditionalOnMissingBean + public I18nHeaderCellWriteHandler i18nHeaderCellWriteHandler(MessageSource messageSource) { + return new I18nHeaderCellWriteHandler(messageSource); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/ResponseExcelAutoConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/ResponseExcelAutoConfiguration.java new file mode 100644 index 0000000..64047e9 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/ResponseExcelAutoConfiguration.java @@ -0,0 +1,98 @@ +package com.baiye.common.excel; + +import com.baiye.common.excel.aop.DynamicNameAspect; +import com.baiye.common.excel.aop.RequestExcelArgumentResolver; +import com.baiye.common.excel.aop.ResponseExcelReturnValueHandler; +import com.baiye.common.excel.config.ExcelConfigProperties; +import com.baiye.common.excel.head.EmptyHeadGenerator; +import com.baiye.common.excel.processor.NameProcessor; +import com.baiye.common.excel.processor.NameSpelExpressionProcessor; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; + +/** + * @author lengleng + * @date 2020/3/29 + *

          + * 配置初始化 + */ +@AutoConfiguration +@Import(ExcelHandlerConfiguration.class) +@RequiredArgsConstructor +@EnableConfigurationProperties(ExcelConfigProperties.class) +public class ResponseExcelAutoConfiguration { + + private final RequestMappingHandlerAdapter requestMappingHandlerAdapter; + + private final ResponseExcelReturnValueHandler responseExcelReturnValueHandler; + + /** + * SPEL 解析处理器 + * @return NameProcessor excel名称解析器 + */ + @Bean + @ConditionalOnMissingBean + public NameProcessor nameProcessor() { + return new NameSpelExpressionProcessor(); + } + + /** + * Excel名称解析处理切面 + * @param nameProcessor SPEL 解析处理器 + * @return DynamicNameAspect + */ + @Bean + @ConditionalOnMissingBean + public DynamicNameAspect dynamicNameAspect(NameProcessor nameProcessor) { + return new DynamicNameAspect(nameProcessor); + } + + /** + * 空的 Excel 头生成器 + * @return EmptyHeadGenerator + */ + @Bean + @ConditionalOnMissingBean + public EmptyHeadGenerator emptyHeadGenerator() { + return new EmptyHeadGenerator(); + } + + /** + * 追加 Excel返回值处理器 到 springmvc 中 + */ + @PostConstruct + public void setReturnValueHandlers() { + List returnValueHandlers = requestMappingHandlerAdapter + .getReturnValueHandlers(); + + List newHandlers = new ArrayList<>(); + newHandlers.add(responseExcelReturnValueHandler); + assert returnValueHandlers != null; + newHandlers.addAll(returnValueHandlers); + requestMappingHandlerAdapter.setReturnValueHandlers(newHandlers); + } + + /** + * 追加 Excel 请求处理器 到 springmvc 中 + */ + @PostConstruct + public void setRequestExcelArgumentResolver() { + List argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers(); + List resolverList = new ArrayList<>(); + resolverList.add(new RequestExcelArgumentResolver()); + resolverList.addAll(argumentResolvers); + requestMappingHandlerAdapter.setArgumentResolvers(resolverList); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/annotation/RequestExcel.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/annotation/RequestExcel.java new file mode 100644 index 0000000..471fa1e --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/annotation/RequestExcel.java @@ -0,0 +1,47 @@ +package com.baiye.common.excel.annotation; + +import com.alibaba.excel.read.builder.AbstractExcelReaderParameterBuilder; +import com.baiye.common.excel.handler.DefaultAnalysisEventListener; +import com.baiye.common.excel.handler.ListAnalysisEventListener; + +import java.lang.annotation.*; + +/** + * 导入excel + * + * @author lengleng + * @author L.cm + * @date 2021/4/16 + */ +@Documented +@Target({ ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface RequestExcel { + + /** + * 前端上传字段名称 file + */ + String fileName() default "file"; + + /** + * 读取的监听器类 + * @return readListener + */ + Class> readListener() default DefaultAnalysisEventListener.class; + + /** + * 是否跳过空行 + * @return 默认跳过 + */ + boolean ignoreEmptyRow() default false; + + /** + * Count the number of added heads when read sheet. 0 - This Sheet has no head ,since + * the first row are the data 1 - This Sheet has one row head , this is the default 2 + * - This Sheet has two row head ,since the third row is the data + * @see AbstractExcelReaderParameterBuilder#headRowNumber + * @return headRowNumber + */ + int headRowNumber() default 1; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/annotation/ResponseExcel.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/annotation/ResponseExcel.java new file mode 100644 index 0000000..53582ac --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/annotation/ResponseExcel.java @@ -0,0 +1,97 @@ +package com.baiye.common.excel.annotation; + +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.support.ExcelTypeEnum; +import com.alibaba.excel.write.handler.WriteHandler; +import com.baiye.common.excel.head.HeadGenerator; + +import java.lang.annotation.*; + +/** + * `@ResponseExcel 注解` + * + * @author lengleng + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ResponseExcel { + + /** + * 文件名称 + * @return string + */ + String name() default ""; + + /** + * 文件类型 (xlsx xls) + * @return string + */ + ExcelTypeEnum suffix() default ExcelTypeEnum.XLSX; + + /** + * 文件密码 + * @return password + */ + String password() default ""; + + /** + * sheet 名称,支持多个 + * @return String[] + */ + Sheet[] sheets() default {}; + + /** + * 内存操作 + */ + boolean inMemory() default false; + + /** + * excel 模板 + * @return String + */ + String template() default ""; + + /** + * + 包含字段 + * @return String[] + */ + String[] include() default {}; + + /** + * 排除字段 + * @return String[] + */ + String[] exclude() default {}; + + /** + * 拦截器,自定义样式等处理器 + * @return WriteHandler[] + */ + Class[] writeHandler() default {}; + + /** + * 转换器 + * @return Converter[] + */ + Class[] converter() default {}; + + /** + * 自定义Excel头生成器 + * @return HeadGenerator + */ + Class headGenerator() default HeadGenerator.class; + + /** + * excel 头信息国际化 + * @return boolean + */ + boolean i18nHeader() default false; + + /** + * 填充模式 + * @return boolean + */ + boolean fill() default false; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/annotation/Sheet.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/annotation/Sheet.java new file mode 100644 index 0000000..d392c88 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/annotation/Sheet.java @@ -0,0 +1,39 @@ +package com.baiye.common.excel.annotation; + +import com.baiye.common.excel.head.HeadGenerator; + +import java.lang.annotation.*; + +/** + * 用于指定导入导出的 excel 的 sheet 属性 + * + * @author Yakir 2021/4/29 15:03 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Sheet { + + int sheetNo() default -1; + + /** + * sheet name + */ + String sheetName(); + + /** + * 包含字段 + */ + String[] includes() default {}; + + /** + * 排除字段 + */ + String[] excludes() default {}; + + /** + * 头生成器 + */ + Class headGenerateClass() default HeadGenerator.class; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/aop/DynamicNameAspect.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/aop/DynamicNameAspect.java new file mode 100644 index 0000000..9c7fa5c --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/aop/DynamicNameAspect.java @@ -0,0 +1,46 @@ +package com.baiye.common.excel.aop; + +import com.baiye.common.excel.annotation.ResponseExcel; +import com.baiye.common.excel.processor.NameProcessor; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * @author lengleng + * @date 2020/3/29 + */ +@Aspect +@RequiredArgsConstructor +public class DynamicNameAspect { + + public static final String EXCEL_NAME_KEY = "__EXCEL_NAME_KEY__"; + + private final NameProcessor processor; + + @Before("@annotation(excel)") + public void before(JoinPoint point, ResponseExcel excel) { + MethodSignature ms = (MethodSignature) point.getSignature(); + + String name = excel.name(); + // 当配置的 excel 名称为空时,取当前时间 + if (!StringUtils.hasText(name)) { + name = LocalDateTime.now().toString(); + } + else { + name = processor.doDetermineName(point.getArgs(), ms.getMethod(), excel.name()); + } + + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + Objects.requireNonNull(requestAttributes).setAttribute(EXCEL_NAME_KEY, name, RequestAttributes.SCOPE_REQUEST); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/aop/RequestExcelArgumentResolver.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/aop/RequestExcelArgumentResolver.java new file mode 100644 index 0000000..a0977a1 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/aop/RequestExcelArgumentResolver.java @@ -0,0 +1,92 @@ +package com.baiye.common.excel.aop; + +import com.alibaba.excel.EasyExcel; +import com.baiye.common.excel.annotation.RequestExcel; +import com.baiye.common.excel.converters.LocalDateStringConverter; +import com.baiye.common.excel.converters.LocalDateTimeStringConverter; +import com.baiye.common.excel.handler.ListAnalysisEventListener; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.ui.ModelMap; +import org.springframework.util.Assert; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartRequest; + +import javax.servlet.http.HttpServletRequest; +import java.io.InputStream; +import java.util.List; + +/** + * 上传excel 解析注解 + * + * @author lengleng + * @author L.cm + * @date 2021/4/16 + */ +@Slf4j +public class RequestExcelArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(RequestExcel.class); + } + + @SneakyThrows(Exception.class) + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer modelAndViewContainer, + NativeWebRequest webRequest, WebDataBinderFactory webDataBinderFactory) { + Class parameterType = parameter.getParameterType(); + if (!parameterType.isAssignableFrom(List.class)) { + throw new IllegalArgumentException( + "Excel upload request resolver error, @RequestExcel parameter is not List " + parameterType); + } + + // 处理自定义 readListener + RequestExcel requestExcel = parameter.getParameterAnnotation(RequestExcel.class); + assert requestExcel != null; + Class> readListenerClass = requestExcel.readListener(); + ListAnalysisEventListener readListener = BeanUtils.instantiateClass(readListenerClass); + + // 获取请求文件流 + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + assert request != null; + InputStream inputStream; + if (request instanceof MultipartRequest) { + MultipartFile file = ((MultipartRequest) request).getFile(requestExcel.fileName()); + Assert.notNull(file, "excel import: file can not be null!"); + inputStream = file.getInputStream(); + } + else { + inputStream = request.getInputStream(); + } + + // 获取目标类型 + Class excelModelClass = ResolvableType.forMethodParameter(parameter).getGeneric(0).resolve(); + + // 这里需要指定读用哪个 class 去读,然后读取第一个 sheet 文件流会自动关闭 + EasyExcel.read(inputStream, excelModelClass, readListener) + .registerConverter(LocalDateStringConverter.INSTANCE) + .registerConverter(LocalDateTimeStringConverter.INSTANCE) + .ignoreEmptyRow(requestExcel.ignoreEmptyRow()) + .sheet() + .headRowNumber(requestExcel.headRowNumber()) + .doRead(); + + // 校验失败的数据处理 交给 BindResult + WebDataBinder dataBinder = webDataBinderFactory.createBinder(webRequest, readListener.getErrors(), "excel"); + ModelMap model = modelAndViewContainer.getModel(); + model.put(BindingResult.MODEL_KEY_PREFIX + "excel", dataBinder.getBindingResult()); + + return readListener.getList(); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/aop/ResponseExcelReturnValueHandler.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/aop/ResponseExcelReturnValueHandler.java new file mode 100644 index 0000000..028444c --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/aop/ResponseExcelReturnValueHandler.java @@ -0,0 +1,61 @@ +package com.baiye.common.excel.aop; + +import com.baiye.common.excel.annotation.ResponseExcel; +import com.baiye.common.excel.handler.SheetWriteHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.util.Assert; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.ModelAndViewContainer; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 处理@ResponseExcel 返回值 + * + * @author lengleng + */ +@Slf4j +@RequiredArgsConstructor +public class ResponseExcelReturnValueHandler implements HandlerMethodReturnValueHandler { + + private final List sheetWriteHandlerList; + + /** + * 只处理@ResponseExcel 声明的方法 + * @param parameter 方法签名 + * @return 是否处理 + */ + @Override + public boolean supportsReturnType(MethodParameter parameter) { + return parameter.getMethodAnnotation(ResponseExcel.class) != null; + } + + /** + * 处理逻辑 + * @param o 返回参数 + * @param parameter 方法签名 + * @param mavContainer 上下文容器 + * @param nativeWebRequest 上下文 + * @throws Exception 处理异常 + */ + @Override + public void handleReturnValue(Object o, MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest nativeWebRequest) throws Exception { + /* check */ + HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class); + Assert.state(response != null, "No HttpServletResponse"); + ResponseExcel responseExcel = parameter.getMethodAnnotation(ResponseExcel.class); + Assert.state(responseExcel != null, "No @ResponseExcel"); + mavContainer.setRequestHandled(true); + + sheetWriteHandlerList.stream() + .filter(handler -> handler.support(o)) + .findFirst() + .ifPresent(handler -> handler.export(o, response, responseExcel)); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/config/ExcelConfigProperties.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/config/ExcelConfigProperties.java new file mode 100644 index 0000000..ab13dea --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/config/ExcelConfigProperties.java @@ -0,0 +1,20 @@ +package com.baiye.common.excel.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author lengleng 2020/3/29 + */ +@Data +@ConfigurationProperties(prefix = ExcelConfigProperties.PREFIX) +public class ExcelConfigProperties { + + static final String PREFIX = "excel"; + + /** + * 模板路径 + */ + private String templatePath = "excel"; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/BooleanStringConverter.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/BooleanStringConverter.java new file mode 100644 index 0000000..0114b28 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/BooleanStringConverter.java @@ -0,0 +1,47 @@ +package com.baiye.common.excel.converters; + +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.converters.ReadConverterContext; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; + +/** + * @author Enzo + * @date : 2023/12/8 + */ +public class BooleanStringConverter implements Converter { + @Override + public Class supportJavaTypeKey() { + return Boolean.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.BOOLEAN; + } + + /** + * 这里读的时候会调用 + * + * @param context + * @return + */ + @Override + public Boolean convertToJavaData(ReadConverterContext context) { + return "是".equals(context.getReadCellData().getStringValue()) ? Boolean.TRUE : Boolean.FALSE; + } + + /** + * 这里是写的时候会调用 不用管 + * + * @return + */ + @Override + public WriteCellData convertToExcelData(Boolean value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + return new WriteCellData<>(value.equals(Boolean.TRUE) ? "是" : "否"); + } + + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/ChannelConverter.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/ChannelConverter.java new file mode 100644 index 0000000..fcc0773 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/ChannelConverter.java @@ -0,0 +1,47 @@ +package com.baiye.common.excel.converters; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.ReadCellData; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.enums.ChannelTypeEnum; +import org.apache.commons.lang3.StringUtils; + +/** + * @author Enzo + * @date : 2023/12/9 + */ +public class ChannelConverter implements Converter { + + + @Override + public Class supportJavaTypeKey() { + return Integer.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public Integer convertToJavaData(ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + if (StringUtils.isNotBlank(cellData.getStringValue())) { + return Integer.parseInt(cellData.getStringValue()); + } + return DefaultNumberConstants.MINUS_ONE_NUMBER; + } + + + @Override + public WriteCellData convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception { + return ObjectUtil.isNotNull(value) ? new WriteCellData<>(ChannelTypeEnum.find(value)) : new WriteCellData<>(CharSequenceUtil.EMPTY); + + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/CustomStringStringConverter.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/CustomStringStringConverter.java new file mode 100644 index 0000000..02e7cb9 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/CustomStringStringConverter.java @@ -0,0 +1,45 @@ +package com.baiye.common.excel.converters; + +import cn.hutool.json.JSONUtil; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.converters.ReadConverterContext; +import com.alibaba.excel.converters.WriteConverterContext; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.data.WriteCellData; + +/** + * @author Enzo + * @date : 2023/12/8 + */ +public class CustomStringStringConverter implements Converter { + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + /** + * 这里读的时候会调用 + * + * @param context + * @return + */ + @Override + public String convertToJavaData(ReadConverterContext context) { + return "自定义:" + context.getReadCellData().getStringValue(); + } + + /** + * 这里是写的时候会调用 不用管 + * + * @return + */ + @Override + public WriteCellData convertToExcelData(WriteConverterContext context) { + return new WriteCellData<>(JSONUtil.toJsonStr(context.getValue())); + } +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/LocalDateStringConverter.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/LocalDateStringConverter.java new file mode 100644 index 0000000..a432175 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/LocalDateStringConverter.java @@ -0,0 +1,62 @@ +package com.baiye.common.excel.converters; + +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.ReadCellData; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; + +import java.text.ParseException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * LocalDate and string converter + * + * @author L.cm + */ +public enum LocalDateStringConverter implements Converter { + + /** + * 实例 + */ + INSTANCE; + + @Override + public Class supportJavaTypeKey() { + return LocalDate.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public LocalDate convertToJavaData(ReadCellData cellData, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) throws ParseException { + if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { + return LocalDate.parse(cellData.getStringValue()); + } + else { + DateTimeFormatter formatter = DateTimeFormatter + .ofPattern(contentProperty.getDateTimeFormatProperty().getFormat()); + return LocalDate.parse(cellData.getStringValue(), formatter); + } + } + + @Override + public WriteCellData convertToExcelData(LocalDate value, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + DateTimeFormatter formatter; + if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { + formatter = DateTimeFormatter.ISO_LOCAL_DATE; + } + else { + formatter = DateTimeFormatter.ofPattern(contentProperty.getDateTimeFormatProperty().getFormat()); + } + return new WriteCellData<>(value.format(formatter)); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/LocalDateTimeConverter.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/LocalDateTimeConverter.java new file mode 100644 index 0000000..1bbe543 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/LocalDateTimeConverter.java @@ -0,0 +1,41 @@ +package com.baiye.common.excel.converters; + +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.ReadCellData; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; +import com.baiye.util.LocalDateTimeUtils; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * @author Enzo + * @date : 2023/12/9 + */ +public class LocalDateTimeConverter implements Converter { + + @Override + public Class supportJavaTypeKey() { + return LocalDateTime.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public LocalDateTime convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + return LocalDateTime.parse(readCellData.getStringValue(), DateTimeFormatter.ofPattern(LocalDateTimeUtils.STRING_FORMATTER_YMD_HMS)); + } + + @Override + public WriteCellData convertToExcelData(LocalDateTime value, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + return new WriteCellData<>(value.format(DateTimeFormatter.ofPattern(LocalDateTimeUtils.STRING_FORMATTER_YMD))); + } +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/LocalDateTimeStringConverter.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/LocalDateTimeStringConverter.java new file mode 100644 index 0000000..3bda543 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/LocalDateTimeStringConverter.java @@ -0,0 +1,93 @@ +package com.baiye.common.excel.converters; + +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.ReadCellData; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; +import com.alibaba.excel.util.DateUtils; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * LocalDateTime and string converter + * + * @author L.cm + */ +public enum LocalDateTimeStringConverter implements Converter { + + /** + * 实例 + */ + INSTANCE; + + private static final String MINUS = "-"; + + @Override + public Class supportJavaTypeKey() { + return LocalDateTime.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public LocalDateTime convertToJavaData(ReadCellData cellData, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + String stringValue = cellData.getStringValue(); + String pattern; + if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { + pattern = switchDateFormat(stringValue); + } + else { + pattern = contentProperty.getDateTimeFormatProperty().getFormat(); + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return LocalDateTime.parse(cellData.getStringValue(), formatter); + } + + @Override + public WriteCellData convertToExcelData(LocalDateTime value, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + String pattern; + if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { + pattern = DateUtils.DATE_FORMAT_19; + } + else { + pattern = contentProperty.getDateTimeFormatProperty().getFormat(); + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return new WriteCellData<>(value.format(formatter)); + } + + /** + * switch date format + * @param dateString dateString + * @return pattern + */ + private static String switchDateFormat(String dateString) { + int length = dateString.length(); + switch (length) { + case 19: + if (dateString.contains(MINUS)) { + return DateUtils.DATE_FORMAT_19; + } + else { + return DateUtils.DATE_FORMAT_19_FORWARD_SLASH; + } + case 17: + return DateUtils.DATE_FORMAT_17; + case 14: + return DateUtils.DATE_FORMAT_14; + case 10: + return DateUtils.DATE_FORMAT_10; + default: + throw new IllegalArgumentException("can not find date format for:" + dateString); + } + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/NidStringConverter.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/NidStringConverter.java new file mode 100644 index 0000000..0089cd6 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/NidStringConverter.java @@ -0,0 +1,54 @@ +package com.baiye.common.excel.converters; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.converters.ReadConverterContext; +import com.alibaba.excel.converters.WriteConverterContext; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.util.AESUtils; + +import java.util.Objects; + +/** + * @author Enzo + * @date : 2023/12/8 + */ +public class NidStringConverter implements Converter { + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + /** + * 这里读的时候会调用 + * + * @param context + * @return + */ + @Override + public String convertToJavaData(ReadConverterContext context) { + return Objects.requireNonNull(AESUtils.encrypt(context.getReadCellData().getStringValue(), "==marketing-scrm=")); + } + + /** + * 这里是写的时候会调用 不用管 + * + * @return + */ + @Override + public WriteCellData convertToExcelData(WriteConverterContext context) { + return ObjectUtil.isNotNull(context.getValue()) ? new WriteCellData<> + (context.getValue().length() == DefaultNumberConstants.ELEVEN_NUMBER + ? context.getValue() : AESUtils.decrypt(context.getValue(), "==marketing-scrm=")) + : new WriteCellData<>(CharSequenceUtil.EMPTY); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/SexConverter.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/SexConverter.java new file mode 100644 index 0000000..dcc232c --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/converters/SexConverter.java @@ -0,0 +1,35 @@ +package com.baiye.common.excel.converters; + +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.ReadCellData; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; + +/** + * @author Enzo + * @date : 2023/12/9 + */ +public class SexConverter implements Converter { + @Override + public Class supportJavaTypeKey() { + return Integer.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public Integer convertToJavaData(ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + return "男".equals(cellData.getStringValue()) ? 1 : 0; + } + + + @Override + public WriteCellData convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception { + return new WriteCellData<>(value.equals(1) ? "男" : "女"); + } +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/domain/ErrorMessage.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/domain/ErrorMessage.java new file mode 100644 index 0000000..2e72888 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/domain/ErrorMessage.java @@ -0,0 +1,41 @@ +package com.baiye.common.excel.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.HashSet; +import java.util.Set; + +/** + * 校验错误信息 + * + * @author lengleng + * @date 2021/8/4 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ErrorMessage { + + /** + * 行号 + */ + private Long lineNum; + + /** + * 错误信息 + */ + private Set errors = new HashSet<>(); + + public ErrorMessage(Set errors) { + this.errors = errors; + } + + public ErrorMessage(String error) { + HashSet objects = new HashSet<>(); + objects.add(error); + this.errors = objects; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/domain/SheetBuildProperties.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/domain/SheetBuildProperties.java new file mode 100644 index 0000000..8602623 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/domain/SheetBuildProperties.java @@ -0,0 +1,53 @@ +package com.baiye.common.excel.domain; + +import com.baiye.common.excel.annotation.Sheet; +import com.baiye.common.excel.head.HeadGenerator; +import lombok.Data; + +/** + * Sheet Build Properties + * + * @author chengbohua + */ +@Data +public class SheetBuildProperties { + + /** + * sheet 编号 + */ + private int sheetNo = -1; + + /** + * sheet name + */ + private String sheetName; + + /** + * 包含字段 + */ + private String[] includes = new String[0]; + + /** + * 排除字段 + */ + private String[] excludes = new String[0]; + + /** + * 头生成器 + */ + private Class headGenerateClass = HeadGenerator.class; + + public SheetBuildProperties(Sheet sheetAnnotation) { + this.sheetNo = sheetAnnotation.sheetNo(); + this.sheetName = sheetAnnotation.sheetName(); + this.includes = sheetAnnotation.includes(); + this.excludes = sheetAnnotation.excludes(); + this.headGenerateClass = sheetAnnotation.headGenerateClass(); + } + + public SheetBuildProperties(int index) { + this.sheetNo = index; + this.sheetName = "sheet" + (sheetNo + 1); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/enhance/DefaultWriterBuilderEnhancer.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/enhance/DefaultWriterBuilderEnhancer.java new file mode 100644 index 0000000..8e4bb96 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/enhance/DefaultWriterBuilderEnhancer.java @@ -0,0 +1,48 @@ +package com.baiye.common.excel.enhance; + +import com.alibaba.excel.write.builder.ExcelWriterBuilder; +import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder; +import com.baiye.common.excel.annotation.ResponseExcel; +import com.baiye.common.excel.head.HeadGenerator; + +import javax.servlet.http.HttpServletResponse; + +/** + * @author Hccake 2020/12/18 + * @version 1.0 + */ +public class DefaultWriterBuilderEnhancer implements WriterBuilderEnhancer { + + /** + * ExcelWriterBuilder 增强 + * @param writerBuilder ExcelWriterBuilder + * @param response HttpServletResponse + * @param responseExcel ResponseExcel + * @param templatePath 模板地址 + * @return ExcelWriterBuilder + */ + @Override + public ExcelWriterBuilder enhanceExcel(ExcelWriterBuilder writerBuilder, HttpServletResponse response, + ResponseExcel responseExcel, String templatePath) { + // doNothing + return writerBuilder; + } + + /** + * ExcelWriterSheetBuilder 增强 + * @param writerSheetBuilder ExcelWriterSheetBuilder + * @param sheetNo sheet角标 + * @param sheetName sheet名,有模板时为空 + * @param dataClass 当前写入的数据所属类 + * @param template 模板文件 + * @param headEnhancerClass 当前指定的自定义头处理器 + * @return ExcelWriterSheetBuilder + */ + @Override + public ExcelWriterSheetBuilder enhanceSheet(ExcelWriterSheetBuilder writerSheetBuilder, Integer sheetNo, + String sheetName, Class dataClass, String template, Class headEnhancerClass) { + // doNothing + return writerSheetBuilder; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/enhance/WriterBuilderEnhancer.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/enhance/WriterBuilderEnhancer.java new file mode 100644 index 0000000..9f742f6 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/enhance/WriterBuilderEnhancer.java @@ -0,0 +1,42 @@ +package com.baiye.common.excel.enhance; + +import com.alibaba.excel.write.builder.ExcelWriterBuilder; +import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder; +import com.baiye.common.excel.annotation.ResponseExcel; +import com.baiye.common.excel.head.HeadGenerator; + +import javax.servlet.http.HttpServletResponse; + +/** + * ExcelWriterBuilder 增强 + * + * @author Hccake 2020/12/18 + * @version 1.0 + */ +public interface WriterBuilderEnhancer { + + /** + * ExcelWriterBuilder 增强 + * @param writerBuilder ExcelWriterBuilder + * @param response HttpServletResponse + * @param responseExcel ResponseExcel + * @param templatePath 模板地址 + * @return ExcelWriterBuilder + */ + ExcelWriterBuilder enhanceExcel(ExcelWriterBuilder writerBuilder, HttpServletResponse response, + ResponseExcel responseExcel, String templatePath); + + /** + * ExcelWriterSheetBuilder 增强 + * @param writerSheetBuilder ExcelWriterSheetBuilder + * @param sheetNo sheet角标 + * @param sheetName sheet名,有模板时为空 + * @param dataClass 当前写入的数据所属类 + * @param template 模板文件 + * @param headEnhancerClass 当前指定的自定义头处理器 + * @return ExcelWriterSheetBuilder + */ + ExcelWriterSheetBuilder enhanceSheet(ExcelWriterSheetBuilder writerSheetBuilder, Integer sheetNo, String sheetName, + Class dataClass, String template, Class headEnhancerClass); + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/AbstractSheetWriteHandler.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/AbstractSheetWriteHandler.java new file mode 100644 index 0000000..a95c313 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/AbstractSheetWriteHandler.java @@ -0,0 +1,257 @@ +package com.baiye.common.excel.handler; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.write.builder.ExcelWriterBuilder; +import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder; +import com.alibaba.excel.write.handler.WriteHandler; +import com.alibaba.excel.write.metadata.WriteSheet; +import com.baiye.common.excel.annotation.ResponseExcel; +import com.baiye.common.excel.aop.DynamicNameAspect; +import com.baiye.common.excel.config.ExcelConfigProperties; +import com.baiye.common.excel.converters.LocalDateStringConverter; +import com.baiye.common.excel.converters.LocalDateTimeStringConverter; + +import com.baiye.common.excel.domain.SheetBuildProperties; +import com.baiye.common.excel.enhance.WriterBuilderEnhancer; +import com.baiye.common.excel.head.HeadGenerator; +import com.baiye.common.excel.head.HeadMeta; +import com.baiye.common.excel.head.I18nHeaderCellWriteHandler; +import com.baiye.common.excel.kit.ExcelException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.SneakyThrows; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.MediaTypeFactory; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Modifier; +import java.net.URLEncoder; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * @author lengleng + * @author L.cm + * @author Hccake + * @date 2020/3/31 + */ +@RequiredArgsConstructor +public abstract class AbstractSheetWriteHandler implements SheetWriteHandler, ApplicationContextAware { + + private final ExcelConfigProperties configProperties; + + private final ObjectProvider>> converterProvider; + + private final WriterBuilderEnhancer excelWriterBuilderEnhance; + + private ApplicationContext applicationContext; + + @Getter + @Setter + @Autowired(required = false) + private I18nHeaderCellWriteHandler i18nHeaderCellWriteHandler; + + @Override + public void check(ResponseExcel responseExcel) { + if (responseExcel.fill() && !StringUtils.hasText(responseExcel.template())) { + throw new ExcelException("@ResponseExcel fill 必须配合 template 使用"); + } + } + + @Override + @SneakyThrows(UnsupportedEncodingException.class) + public void export(Object o, HttpServletResponse response, ResponseExcel responseExcel) { + check(responseExcel); + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + String name = (String) Objects.requireNonNull(requestAttributes) + .getAttribute(DynamicNameAspect.EXCEL_NAME_KEY, RequestAttributes.SCOPE_REQUEST); + if (name == null) { + name = UUID.randomUUID().toString(); + } + String fileName = String.format("%s%s", URLEncoder.encode(name, "UTF-8"), responseExcel.suffix().getValue()) + .replaceAll("\\+", "%20"); + // 根据实际的文件类型找到对应的 contentType + String contentType = MediaTypeFactory.getMediaType(fileName) + .map(MediaType::toString) + .orElse("application/vnd.ms-excel"); + response.setContentType(contentType); + response.setCharacterEncoding("utf-8"); + response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename*=utf-8''" + fileName); + write(o, response, responseExcel); + } + + /** + * 通用的获取ExcelWriter方法 + * @param response HttpServletResponse + * @param responseExcel ResponseExcel注解 + * @return ExcelWriter + */ + @SneakyThrows(IOException.class) + public ExcelWriter getExcelWriter(HttpServletResponse response, ResponseExcel responseExcel) { + ExcelWriterBuilder writerBuilder = EasyExcel.write(response.getOutputStream()) + .registerConverter(LocalDateStringConverter.INSTANCE) + .registerConverter(LocalDateTimeStringConverter.INSTANCE) + .autoCloseStream(true) + .excelType(responseExcel.suffix()) + .inMemory(responseExcel.inMemory()); + + if (StringUtils.hasText(responseExcel.password())) { + writerBuilder.password(responseExcel.password()); + } + + if (responseExcel.include().length != 0) { + writerBuilder.includeColumnFieldNames(Arrays.asList(responseExcel.include())); + } + + if (responseExcel.exclude().length != 0) { + writerBuilder.excludeColumnFieldNames(Arrays.asList(responseExcel.exclude())); + } + + for (Class clazz : responseExcel.writeHandler()) { + writerBuilder.registerWriteHandler(BeanUtils.instantiateClass(clazz)); + } + + // 开启国际化头信息处理 + if (responseExcel.i18nHeader() && i18nHeaderCellWriteHandler != null) { + writerBuilder.registerWriteHandler(i18nHeaderCellWriteHandler); + } + + // 自定义注入的转换器 + registerCustomConverter(writerBuilder); + + for (Class clazz : responseExcel.converter()) { + writerBuilder.registerConverter(BeanUtils.instantiateClass(clazz)); + } + + String templatePath = configProperties.getTemplatePath(); + if (StringUtils.hasText(responseExcel.template())) { + ClassPathResource classPathResource = new ClassPathResource( + templatePath + File.separator + responseExcel.template()); + InputStream inputStream = classPathResource.getInputStream(); + writerBuilder.withTemplate(inputStream); + } + + writerBuilder = excelWriterBuilderEnhance.enhanceExcel(writerBuilder, response, responseExcel, templatePath); + + return writerBuilder.build(); + } + + /** + * 自定义注入转换器 如果有需要,子类自己重写 + * @param builder ExcelWriterBuilder + */ + public void registerCustomConverter(ExcelWriterBuilder builder) { + converterProvider.ifAvailable(converters -> converters.forEach(builder::registerConverter)); + } + + /** + * 构建一个 空的 WriteSheet 对象 + * @param sheetBuildProperties sheet build 属性 + * @param template 模板信息 + * @return WriteSheet + */ + public WriteSheet emptySheet(SheetBuildProperties sheetBuildProperties, String template) { + // Sheet 编号和名称 + Integer sheetNo = sheetBuildProperties.getSheetNo() >= 0 ? sheetBuildProperties.getSheetNo() : null; + String sheetName = sheetBuildProperties.getSheetName(); + + // 是否模板写入 + ExcelWriterSheetBuilder writerSheetBuilder = StringUtils.hasText(template) ? EasyExcel.writerSheet(sheetNo) + : EasyExcel.writerSheet(sheetNo, sheetName); + + return writerSheetBuilder.build(); + } + + /** + * 获取 WriteSheet 对象 + * @param sheetBuildProperties sheet annotation info + * @param dataClass 数据类型 + * @param template 模板 + * @param bookHeadEnhancerClass 自定义头处理器 + * @return WriteSheet + */ + public WriteSheet emptySheet(SheetBuildProperties sheetBuildProperties, Class dataClass, String template, + Class bookHeadEnhancerClass) { + + // Sheet 编号和名称 + Integer sheetNo = sheetBuildProperties.getSheetNo() >= 0 ? sheetBuildProperties.getSheetNo() : null; + String sheetName = sheetBuildProperties.getSheetName(); + + // 是否模板写入 + ExcelWriterSheetBuilder writerSheetBuilder = StringUtils.hasText(template) ? EasyExcel.writerSheet(sheetNo) + : EasyExcel.writerSheet(sheetNo, sheetName); + + // 头信息增强 1. 优先使用 sheet 指定的头信息增强 2. 其次使用 @ResponseExcel 中定义的全局头信息增强 + Class headGenerateClass = null; + if (isNotInterface(sheetBuildProperties.getHeadGenerateClass())) { + headGenerateClass = sheetBuildProperties.getHeadGenerateClass(); + } + else if (isNotInterface(bookHeadEnhancerClass)) { + headGenerateClass = bookHeadEnhancerClass; + } + // 定义头信息增强则使用其生成头信息,否则使用 dataClass 来自动获取 + if (headGenerateClass != null) { + fillCustomHeadInfo(dataClass, bookHeadEnhancerClass, writerSheetBuilder); + } + else if (dataClass != null) { + writerSheetBuilder.head(dataClass); + if (sheetBuildProperties.getExcludes().length > 0) { + writerSheetBuilder.excludeColumnFieldNames(Arrays.asList(sheetBuildProperties.getExcludes())); + } + if (sheetBuildProperties.getIncludes().length > 0) { + writerSheetBuilder.includeColumnFieldNames(Arrays.asList(sheetBuildProperties.getIncludes())); + } + } + + // sheetBuilder 增强 + writerSheetBuilder = excelWriterBuilderEnhance.enhanceSheet(writerSheetBuilder, sheetNo, sheetName, dataClass, + template, headGenerateClass); + + return writerSheetBuilder.build(); + } + + private void fillCustomHeadInfo(Class dataClass, Class headEnhancerClass, + ExcelWriterSheetBuilder writerSheetBuilder) { + HeadGenerator headGenerator = this.applicationContext.getBean(headEnhancerClass); + Assert.notNull(headGenerator, "The header generated bean does not exist."); + HeadMeta head = headGenerator.head(dataClass); + writerSheetBuilder.head(head.getHead()); + writerSheetBuilder.excludeColumnFieldNames(head.getIgnoreHeadFields()); + } + + /** + * 是否为Null Head Generator + * @param headGeneratorClass 头生成器类型 + * @return true 已指定 false 未指定(默认值) + */ + private boolean isNotInterface(Class headGeneratorClass) { + return !Modifier.isInterface(headGeneratorClass.getModifiers()); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/DefaultAnalysisEventListener.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/DefaultAnalysisEventListener.java new file mode 100644 index 0000000..a0b67e5 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/DefaultAnalysisEventListener.java @@ -0,0 +1,63 @@ +package com.baiye.common.excel.handler; + +import com.alibaba.excel.context.AnalysisContext; +import com.baiye.common.excel.kit.Validators; +import com.baiye.common.excel.domain.ErrorMessage; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.validation.ConstraintViolation; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 默认的 AnalysisEventListener + * + * @author lengleng + * @author L.cm + * @date 2021/4/16 + */ +@Slf4j +public class DefaultAnalysisEventListener extends ListAnalysisEventListener { + + private final List list = new ArrayList<>(); + + private final List errorMessageList = new ArrayList<>(); + + @Setter + private Long lineNum = 1L; + + @Override + public void invoke(Object o, AnalysisContext analysisContext) { + lineNum++; + + Set> violations = Validators.validate(o); + if (!violations.isEmpty()) { + Set messageSet = violations.stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.toSet()); + errorMessageList.add(new ErrorMessage(lineNum, messageSet)); + } + else { + list.add(o); + } + } + + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + log.debug("Excel read analysed"); + } + + @Override + public List getList() { + return list; + } + + @Override + public List getErrors() { + return errorMessageList; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/ListAnalysisEventListener.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/ListAnalysisEventListener.java new file mode 100644 index 0000000..98bc002 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/ListAnalysisEventListener.java @@ -0,0 +1,27 @@ +package com.baiye.common.excel.handler; + +import com.alibaba.excel.event.AnalysisEventListener; +import com.baiye.common.excel.domain.ErrorMessage; + +import java.util.List; + +/** + * list analysis EventListener + * + * @author L.cm + */ +public abstract class ListAnalysisEventListener extends AnalysisEventListener { + + /** + * 获取 excel 解析的对象列表 + * @return 集合 + */ + public abstract List getList(); + + /** + * 获取异常校验结果 + * @return 集合 + */ + public abstract List getErrors(); + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/ManySheetWriteHandler.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/ManySheetWriteHandler.java new file mode 100644 index 0000000..8a1d3f3 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/ManySheetWriteHandler.java @@ -0,0 +1,104 @@ +package com.baiye.common.excel.handler; + +import com.alibaba.excel.ExcelWriter; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.write.metadata.WriteSheet; +import com.baiye.common.excel.annotation.ResponseExcel; +import com.baiye.common.excel.annotation.Sheet; +import com.baiye.common.excel.config.ExcelConfigProperties; +import com.baiye.common.excel.domain.SheetBuildProperties; +import com.baiye.common.excel.enhance.WriterBuilderEnhancer; +import com.baiye.common.excel.kit.ExcelException; +import org.springframework.beans.factory.ObjectProvider; + +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; + +/** + * @author lengleng + * @date 2020/3/29 + */ +public class ManySheetWriteHandler extends AbstractSheetWriteHandler { + + public ManySheetWriteHandler(ExcelConfigProperties configProperties, + ObjectProvider>> converterProvider, WriterBuilderEnhancer excelWriterBuilderEnhance) { + super(configProperties, converterProvider, excelWriterBuilderEnhance); + } + + /** + * 当且仅当List不为空且List中的元素也是List 才返回true + * @param obj 返回对象 + * @return boolean + */ + @Override + public boolean support(Object obj) { + if (obj instanceof List) { + List objList = (List) obj; + return !objList.isEmpty() && objList.get(0) instanceof List; + } + else { + throw new ExcelException("@ResponseExcel 返回值必须为List类型"); + } + } + + @Override + public void write(Object obj, HttpServletResponse response, ResponseExcel responseExcel) { + List objList = (List) obj; + int objListSize = objList.size(); + + String template = responseExcel.template(); + + ExcelWriter excelWriter = getExcelWriter(response, responseExcel); + List sheetBuildPropertiesList = getSheetBuildProperties(responseExcel, objListSize); + + for (int i = 0; i < sheetBuildPropertiesList.size(); i++) { + SheetBuildProperties sheetBuildProperties = sheetBuildPropertiesList.get(i); + // 创建sheet + WriteSheet sheet; + List eleList; + if (objListSize <= i) { + eleList = new ArrayList<>(); + sheet = this.emptySheet(sheetBuildProperties, template); + } + else { + eleList = (List) objList.get(i); + if (eleList.isEmpty()) { + sheet = this.emptySheet(sheetBuildProperties, template); + } + else { + Class dataClass = eleList.get(0).getClass(); + sheet = this.emptySheet(sheetBuildProperties, dataClass, template, responseExcel.headGenerator()); + } + } + + if (responseExcel.fill()) { + // 填充 sheet + excelWriter.fill(eleList, sheet); + } + else { + // 写入 sheet + excelWriter.write(eleList, sheet); + } + } + + excelWriter.finish(); + } + + private static List getSheetBuildProperties(ResponseExcel responseExcel, int objListSize) { + List sheetBuildPropertiesList = new ArrayList<>(); + Sheet[] sheets = responseExcel.sheets(); + if (sheets != null && sheets.length > 0) { + for (Sheet sheet : sheets) { + sheetBuildPropertiesList.add(new SheetBuildProperties(sheet)); + } + } + else { + for (int i = 0; i < objListSize; i++) { + sheetBuildPropertiesList.add(new SheetBuildProperties(i)); + } + } + return sheetBuildPropertiesList; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/SheetWriteHandler.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/SheetWriteHandler.java new file mode 100644 index 0000000..5c024a0 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/SheetWriteHandler.java @@ -0,0 +1,44 @@ +package com.baiye.common.excel.handler; + +import com.baiye.common.excel.annotation.ResponseExcel; + +import javax.servlet.http.HttpServletResponse; + +/** + * @author lengleng + * @date 2020/3/29 + *

          + * sheet 写出处理器 + */ +public interface SheetWriteHandler { + + /** + * 是否支持 + * @param obj 返回对象 + * @return boolean + */ + boolean support(Object obj); + + /** + * 校验 + * @param responseExcel 注解 + */ + void check(ResponseExcel responseExcel); + + /** + * 返回的对象 + * @param o obj + * @param response 输出对象 + * @param responseExcel 注解 + */ + void export(Object o, HttpServletResponse response, ResponseExcel responseExcel); + + /** + * 写成对象 + * @param o obj + * @param response 输出对象 + * @param responseExcel 注解 + */ + void write(Object o, HttpServletResponse response, ResponseExcel responseExcel); + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/SingleSheetWriteHandler.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/SingleSheetWriteHandler.java new file mode 100644 index 0000000..d47f626 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/handler/SingleSheetWriteHandler.java @@ -0,0 +1,86 @@ +package com.baiye.common.excel.handler; + +import com.alibaba.excel.ExcelWriter; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.write.metadata.WriteSheet; +import com.baiye.common.excel.annotation.ResponseExcel; +import com.baiye.common.excel.annotation.Sheet; +import com.baiye.common.excel.config.ExcelConfigProperties; +import com.baiye.common.excel.domain.SheetBuildProperties; +import com.baiye.common.excel.enhance.WriterBuilderEnhancer; +import com.baiye.common.excel.kit.ExcelException; +import org.springframework.beans.factory.ObjectProvider; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * @author lengleng + * @date 2020/3/29 + *

          + * 处理单sheet 页面 + */ +public class SingleSheetWriteHandler extends AbstractSheetWriteHandler { + + public SingleSheetWriteHandler(ExcelConfigProperties configProperties, + ObjectProvider>> converterProvider, WriterBuilderEnhancer excelWriterBuilderEnhance) { + super(configProperties, converterProvider, excelWriterBuilderEnhance); + } + + /** + * obj 是List 且list不为空同时list中的元素不是是List 才返回true + * @param obj 返回对象 + * @return boolean + */ + @Override + public boolean support(Object obj) { + if (obj instanceof List) { + List objList = (List) obj; + return !objList.isEmpty() && !(objList.get(0) instanceof List); + } + else { + throw new ExcelException("@ResponseExcel 返回值必须为List类型"); + } + } + + @Override + public void write(Object obj, HttpServletResponse response, ResponseExcel responseExcel) { + List eleList = (List) obj; + ExcelWriter excelWriter = getExcelWriter(response, responseExcel); + + // 获取 Sheet 配置 + SheetBuildProperties sheetBuildProperties; + Sheet[] sheets = responseExcel.sheets(); + if (sheets != null && sheets.length > 0) { + sheetBuildProperties = new SheetBuildProperties(sheets[0]); + } + else { + sheetBuildProperties = new SheetBuildProperties(0); + } + + // 模板信息 + String template = responseExcel.template(); + + // 创建sheet + WriteSheet sheet; + if (eleList.isEmpty()) { + sheet = this.emptySheet(sheetBuildProperties, template); + } + else { + Class dataClass = eleList.get(0).getClass(); + sheet = this.emptySheet(sheetBuildProperties, dataClass, template, responseExcel.headGenerator()); + } + + if (responseExcel.fill()) { + // 填充 sheet + excelWriter.fill(eleList, sheet); + } + else { + // 写入 sheet + excelWriter.write(eleList, sheet); + } + + excelWriter.finish(); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/head/EmptyHeadGenerator.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/head/EmptyHeadGenerator.java new file mode 100644 index 0000000..c0d4d20 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/head/EmptyHeadGenerator.java @@ -0,0 +1,15 @@ +package com.baiye.common.excel.head; + +/** + * 空的 excel 头生成器,用来忽略 excel 头生成 + * + * @author Hccake + */ +public class EmptyHeadGenerator implements HeadGenerator { + + @Override + public HeadMeta head(Class clazz) { + return new HeadMeta(); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/head/HeadGenerator.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/head/HeadGenerator.java new file mode 100644 index 0000000..6904163 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/head/HeadGenerator.java @@ -0,0 +1,22 @@ +package com.baiye.common.excel.head; + +/** + * Excel头生成器,用于自定义生成头部信息 + * + * @author Hccake 2020/10/27 + * @version 1.0 + */ +public interface HeadGenerator { + + /** + *

          + * 自定义头部信息 + *

          + * 实现类根据数据的class信息,定制Excel头
          + * 具体方法使用参考:https://www.yuque.com/easyexcel/doc/write#b4b9de00 + * @param clazz 当前sheet的数据类型 + * @return List> Head头信息 + */ + HeadMeta head(Class clazz); + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/head/HeadMeta.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/head/HeadMeta.java new file mode 100644 index 0000000..13f20f3 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/head/HeadMeta.java @@ -0,0 +1,31 @@ +package com.baiye.common.excel.head; + +import lombok.Data; + +import java.util.List; +import java.util.Set; + +/** + * HeadMetaInfo + * + * @author Yakir 2021/4/26 10:58 + * @version 1.0 + */ +@Data +public class HeadMeta { + + /** + *

          + * 自定义头部信息 + *

          + * 实现类根据数据的class信息,定制Excel头
          + * 具体方法使用参考 + */ + private List> head; + + /** + * 忽略头对应字段名称 + */ + private Set ignoreHeadFields; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/head/I18nHeaderCellWriteHandler.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/head/I18nHeaderCellWriteHandler.java new file mode 100644 index 0000000..51577f6 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/head/I18nHeaderCellWriteHandler.java @@ -0,0 +1,61 @@ +package com.baiye.common.excel.head; + +import com.alibaba.excel.metadata.Head; +import com.alibaba.excel.write.handler.CellWriteHandler; +import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; +import com.alibaba.excel.write.metadata.holder.WriteTableHolder; +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.poi.ss.usermodel.Row; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.util.PropertyPlaceholderHelper; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 对表头进行国际化处理 + * + * @author hccake + */ +@RequiredArgsConstructor +public class I18nHeaderCellWriteHandler implements CellWriteHandler { + + /** + * 国际化消息源 + */ + private final MessageSource messageSource; + + /** + * 国际化翻译 + */ + private final PropertyPlaceholderHelper.PlaceholderResolver placeholderResolver; + + public I18nHeaderCellWriteHandler(MessageSource messageSource) { + this.messageSource = messageSource; + this.placeholderResolver = placeholderName -> this.messageSource.getMessage(placeholderName, null, + LocaleContextHolder.getLocale()); + } + + /** + * 占位符处理 + */ + private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("{", "}"); + + @Override + public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, + Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) { + if (isHead != null && isHead) { + List originHeadNameList = head.getHeadNameList(); + if (CollectionUtils.isNotEmpty(originHeadNameList)) { + // 国际化处理 + List i18nHeadNames = originHeadNameList.stream() + .map(headName -> propertyPlaceholderHelper.replacePlaceholders(headName, placeholderResolver)) + .collect(Collectors.toList()); + head.setHeadNameList(i18nHeadNames); + } + } + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/kit/ExcelException.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/kit/ExcelException.java new file mode 100644 index 0000000..7bf3b33 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/kit/ExcelException.java @@ -0,0 +1,15 @@ +package com.baiye.common.excel.kit; + +/** + * @author lengleng + * @date 2020/3/31 + */ +public class ExcelException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ExcelException(String message) { + super(message); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/kit/Validators.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/kit/Validators.java new file mode 100644 index 0000000..bcec811 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/kit/Validators.java @@ -0,0 +1,37 @@ +package com.baiye.common.excel.kit; + +import javax.validation.*; +import java.util.Set; + +/** + * 校验工具 + * + * @author L.cm + */ +public final class Validators { + + private Validators() { + } + + private static final Validator VALIDATOR; + + static { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + VALIDATOR = factory.getValidator(); + } + + /** + * Validates all constraints on {@code object}. + * @param object object to validate + * @param the type of the object to validate + * @return constraint violations or an empty set if none + * @throws IllegalArgumentException if object is {@code null} or if {@code null} is + * passed to the varargs groups + * @throws ValidationException if a non recoverable error happens during the + * validation process + */ + public static Set> validate(T object) { + return VALIDATOR.validate(object); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/processor/NameProcessor.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/processor/NameProcessor.java new file mode 100644 index 0000000..7578fbd --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/processor/NameProcessor.java @@ -0,0 +1,20 @@ +package com.baiye.common.excel.processor; + +import java.lang.reflect.Method; + +/** + * @author lengleng + * @date 2020/3/29 + */ +public interface NameProcessor { + + /** + * 解析名称 + * @param args 拦截器对象 + * @param method 当前拦截方法 + * @param key 表达式 + * @return String 根据表达式解析后的字符串 + */ + String doDetermineName(Object[] args, Method method, String key); + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/processor/NameSpelExpressionProcessor.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/processor/NameSpelExpressionProcessor.java new file mode 100644 index 0000000..758f002 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/java/com/baiye/common/excel/processor/NameSpelExpressionProcessor.java @@ -0,0 +1,40 @@ +package com.baiye.common.excel.processor; + +import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +import java.lang.reflect.Method; + +/** + * @author lengleng + * @date 2020/3/29 + */ +public class NameSpelExpressionProcessor implements NameProcessor { + + /** + * 参数发现器 + */ + private static final ParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); + + /** + * Express语法解析器 + */ + private static final ExpressionParser PARSER = new SpelExpressionParser(); + + @Override + public String doDetermineName(Object[] args, Method method, String key) { + + if (!key.contains("#")) { + return key; + } + + EvaluationContext context = new MethodBasedEvaluationContext(null, method, args, NAME_DISCOVERER); + final Object value = PARSER.parseExpression(key).getValue(context); + return value == null ? null : value.toString(); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/resources/META-INF/spring.factories b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..ab613aa --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.baiye.common.excel.ResponseExcelAutoConfiguration + diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..da1f9c3 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.baiye.common.excel.ResponseExcelAutoConfiguration diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/DemoData.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/DemoData.java new file mode 100644 index 0000000..bbe8dd2 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/DemoData.java @@ -0,0 +1,13 @@ +package hccake.ballcat.excel.application; + +import lombok.Data; + +// 实体对象 +@Data +public class DemoData { + + private String username; + + private String password; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/ExcelExportTestController.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/ExcelExportTestController.java new file mode 100644 index 0000000..35fb360 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/ExcelExportTestController.java @@ -0,0 +1,49 @@ +package hccake.ballcat.excel.application; + +import com.baiye.common.excel.annotation.ResponseExcel; +import com.baiye.common.excel.annotation.Sheet; +import com.baiye.common.excel.head.EmptyHeadGenerator; +import org.apache.commons.compress.utils.Lists; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author Hccake + */ +@RestController +@RequestMapping("export") +public class ExcelExportTestController { + + @ResponseExcel(name = "test", sheets = @Sheet(sheetName = "testSheet1")) + @GetMapping("/simple") + public List simple() { + List dataList = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + DemoData data = new DemoData(); + data.setUsername("username" + i); + data.setPassword("password" + i); + dataList.add(data); + } + return new ArrayList<>(Collections.singletonList(new DemoData())); + } + + @ResponseExcel(name = "test-export", template = "template.xlsx", headGenerator = EmptyHeadGenerator.class) + @GetMapping("/templateExportIgnoreHeader") + public List templateExportIgnoreHeader() { + List dataList = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + DemoData data = new DemoData(); + data.setUsername("username" + i); + data.setPassword("password" + i); + dataList.add(data); + } + return dataList; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/ExcelFillTestController.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/ExcelFillTestController.java new file mode 100644 index 0000000..ba2253d --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/ExcelFillTestController.java @@ -0,0 +1,31 @@ +package hccake.ballcat.excel.application; + +import com.baiye.common.excel.annotation.ResponseExcel; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Hccake + */ +@RestController +@RequestMapping("fill") +public class ExcelFillTestController { + + @ResponseExcel(name = "simple-fill", fill = true, template = "fill-template.xlsx") + @GetMapping("/simple") + public List simple() { + List dataList = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + DemoData data = new DemoData(); + data.setUsername("username" + i); + data.setPassword("password" + i); + dataList.add(data); + } + return dataList; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/ExcelImportTestController.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/ExcelImportTestController.java new file mode 100644 index 0000000..54a6d36 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/ExcelImportTestController.java @@ -0,0 +1,22 @@ +package hccake.ballcat.excel.application; + +import com.baiye.common.excel.annotation.RequestExcel; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * @author Hccake + */ +@RestController +@RequestMapping("/import") +public class ExcelImportTestController { + + @PostMapping(value = "/simple") + public List simple(@RequestExcel List list) { + return list; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/ExcelTestApplication.java b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/ExcelTestApplication.java new file mode 100644 index 0000000..3597dca --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/java/hccake/ballcat/excel/application/ExcelTestApplication.java @@ -0,0 +1,16 @@ +package hccake.ballcat.excel.application; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Hccake + */ +@SpringBootApplication +public class ExcelTestApplication { + + public static void main(String[] args) { + SpringApplication.run(ExcelTestApplication.class, args); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/resources/excel/fill-template.xlsx b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/resources/excel/fill-template.xlsx new file mode 100644 index 0000000..3e37499 Binary files /dev/null and b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/resources/excel/fill-template.xlsx differ diff --git a/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/resources/excel/template.xlsx b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/resources/excel/template.xlsx new file mode 100644 index 0000000..7a39f6e Binary files /dev/null and b/marketing-scrm-starters/marketing-scrm-starter-easyexcel/src/test/resources/excel/template.xlsx differ diff --git a/marketing-scrm-starters/marketing-scrm-starter-file/pom.xml b/marketing-scrm-starters/marketing-scrm-starter-file/pom.xml new file mode 100644 index 0000000..1711f43 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-file/pom.xml @@ -0,0 +1,44 @@ + + + + marketing-scrm-starters + com.baiye + ${revision} + + 4.0.0 + marketing-scrm-starter-file + + + + com.baiye + common-util + ${revision} + + + cn.hutool + hutool-extra + + + + commons-net + commons-net + + + + org.apache.ftpserver + ftpserver-core + 1.2.0 + test + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-configuration-processor + true + + + diff --git a/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/FileAutoConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/FileAutoConfiguration.java new file mode 100644 index 0000000..32b615f --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/FileAutoConfiguration.java @@ -0,0 +1,39 @@ +package com.baiye.starter.file; + +import com.baiye.starter.file.ftp.FtpFileClient; +import com.baiye.starter.file.core.FileClient; +import com.baiye.starter.file.local.LocalFileClient; +import lombok.AllArgsConstructor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +import java.io.IOException; + +/** + * @author lingting 2021/10/17 19:40 + * @author 疯狂的狮子Li 2022-04-24 + */ +@AutoConfiguration +@AllArgsConstructor +@EnableConfigurationProperties(FileProperties.class) +public class FileAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(FileClient.class) + @ConditionalOnProperty(prefix = FileProperties.PREFIX_FTP, name = "ip") + public FileClient ballcatFileFtpClient(FileProperties properties) { + return new FtpFileClient(properties.getFtp()); + } + + @Bean + @ConditionalOnMissingBean(FileClient.class) + public FileClient ballcatFileLocalClient(FileProperties properties) throws IOException { + FileProperties.LocalProperties localProperties = properties == null || properties.getLocal() == null + ? new FileProperties.LocalProperties() : properties.getLocal(); + return new LocalFileClient(localProperties); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/FileProperties.java b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/FileProperties.java new file mode 100644 index 0000000..d878839 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/FileProperties.java @@ -0,0 +1,77 @@ +package com.baiye.starter.file; + +import com.baiye.starter.file.ftp.FtpMode; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author lingting 2021/10/17 19:37 + */ +@Data +@ConfigurationProperties(prefix = FileProperties.PREFIX) +public class FileProperties { + + public static final String PREFIX = "ballcat.file"; + + public static final String PREFIX_FTP = PREFIX + ".ftp"; + + public static final String PREFIX_LOCAL = PREFIX + ".local"; + + private LocalProperties local; + + private FtpProperties ftp; + + @Data + public static class LocalProperties { + + /** + *

          本地文件存放原始路径

          + *

          + * 如果为Null, 默认存放在系统临时目录下 + *

          + */ + private String path; + + } + + @Data + public static class FtpProperties { + + /** + * 目标ip + */ + private String ip; + + /** + * ftp端口 + */ + private Integer port = 21; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * ftp路径 + */ + private String path = "/"; + + /** + * 模式 + */ + private FtpMode mode; + + /** + * 字符集 + */ + private String encoding = "UTF-8"; + + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/core/AbstractFileClient.java b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/core/AbstractFileClient.java new file mode 100644 index 0000000..63c92eb --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/core/AbstractFileClient.java @@ -0,0 +1,30 @@ +package com.baiye.starter.file.core; + +/** + * @author 疯狂的狮子Li 2022-04-24 + */ +public abstract class AbstractFileClient implements FileClient { + + protected String slash = "/"; + + protected String rootPath; + + /** + * 获取操作的根路径 + */ + public String getRoot() { + return rootPath; + } + + /** + * 获取完整路径 + * @param relativePath 文件相对 getRoot() 的路径 + */ + public String getWholePath(String relativePath) { + if (relativePath.startsWith(slash)) { + return getRoot() + relativePath.substring(1); + } + return getRoot() + relativePath; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/core/FileClient.java b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/core/FileClient.java new file mode 100644 index 0000000..1d249d2 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/core/FileClient.java @@ -0,0 +1,36 @@ +package com.baiye.starter.file.core; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author lingting 2021/10/17 20:11 + * @author 疯狂的狮子Li 2022-04-24 + */ +public interface FileClient { + + /** + * + * 文件上传 + * @param relativePath 文件相对 getRoot() 的路径 + * @param stream 文件输入流 + * @return 文件绝对路径 + */ + String upload(InputStream stream, String relativePath) throws IOException; + + /** + * 下载文件 + * @param relativePath 文件相对 getRoot() 的路径 + * @return java.io.FileOutputStream 文件流 + */ + File download(String relativePath) throws IOException; + + /** + * 删除文件 + * @param relativePath 文件相对 getRoot() 的路径 + * @return boolean + */ + boolean delete(String relativePath) throws IOException; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/exception/FileException.java b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/exception/FileException.java new file mode 100644 index 0000000..684a1ef --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/exception/FileException.java @@ -0,0 +1,27 @@ +package com.baiye.starter.file.exception; + +import java.io.IOException; + +/** + * 文件系统异常 + * + * @author 疯狂的狮子Li 2022-04-24 + */ +public class FileException extends IOException { + + public FileException() { + } + + public FileException(String message) { + super(message); + } + + public FileException(String message, Throwable cause) { + super(message, cause); + } + + public FileException(Throwable cause) { + super(cause); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/ftp/FtpFileClient.java b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/ftp/FtpFileClient.java new file mode 100644 index 0000000..581f17a --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/ftp/FtpFileClient.java @@ -0,0 +1,101 @@ +package com.baiye.starter.file.ftp; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.extra.ftp.Ftp; +import cn.hutool.extra.ftp.FtpConfig; +import com.baiye.starter.file.FileProperties; +import com.baiye.starter.file.core.AbstractFileClient; +import com.baiye.starter.file.exception.FileException; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +/** + * @author lingting 2021/10/17 20:11 + * @author 疯狂的狮子Li 2022-04-24 + */ +public class FtpFileClient extends AbstractFileClient { + + private final Ftp client; + + public FtpFileClient(FileProperties.FtpProperties properties) { + FtpConfig config = new FtpConfig().setHost(properties.getIp()) + .setPort(properties.getPort()) + .setUser(properties.getUsername()) + .setPassword(properties.getPassword()) + .setCharset(Charset.forName(properties.getEncoding())); + + final FtpMode mode = properties.getMode(); + if (mode == FtpMode.ACTIVE) { + client = new Ftp(config, cn.hutool.extra.ftp.FtpMode.Active); + } + else if (mode == FtpMode.PASSIVE) { + client = new Ftp(config, cn.hutool.extra.ftp.FtpMode.Passive); + } + else { + client = new Ftp(config, null); + } + + if (!StringUtils.hasText(properties.getPath())) { + throw new NullPointerException("ftp文件根路径不能为空!"); + } + + super.rootPath = properties.getPath().endsWith(super.slash) ? properties.getPath() + : properties.getPath() + super.slash; + } + + /** + * 上传文件 - 不会关闭流. 请在成功后手动关闭 + * @param stream 文件流 + * @param relativePath 文件相对 getRoot() 的路径 + * @return java.lang.String 文件完整路径 + */ + @Override + public String upload(InputStream stream, String relativePath) throws IOException { + final String path = getWholePath(relativePath); + final String fileName = FileUtil.getName(path); + final String dir = CharSequenceUtil.removeSuffix(path, fileName); + // 上传失败 + if (!client.upload(dir, fileName, stream)) { + throw new FileException( + String.format("文件上传失败! 相对路径: %s; 根路径: %s; 请检查此路径是否存在以及登录用户是否拥有操作权限!", relativePath, path)); + } + return path; + } + + /** + * 下载文件 + * @param relativePath 文件相对 getRoot() 的路径 + * @return java.io.FileOutputStream 文件流 + */ + @Override + public File download(String relativePath) throws IOException { + final String path = getWholePath(relativePath); + final String fileName = FileUtil.getName(path); + final String dir = CharSequenceUtil.removeSuffix(path, fileName); + // 临时文件 + File tmpFile = FileUtil.createTempFile(); + tmpFile = FileUtil.rename(tmpFile, fileName, true); + // 输出流 + try (FileOutputStream outputStream = new FileOutputStream(tmpFile)) { + client.download(dir, fileName, outputStream); + } + return tmpFile; + } + + /** + * 删除文件 + * @param relativePath 文件相对 getRoot() 的路径 + * @return boolean + */ + @Override + public boolean delete(String relativePath) { + return client.delFile(getWholePath(relativePath)); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/ftp/FtpMode.java b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/ftp/FtpMode.java new file mode 100644 index 0000000..4978bfa --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/ftp/FtpMode.java @@ -0,0 +1,24 @@ +package com.baiye.starter.file.ftp; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author lingting 2021/10/18 10:03 + */ +@Getter +@AllArgsConstructor +public enum FtpMode { + + /** + * 主动模式 + */ + ACTIVE, + /** + * 被动模式 + */ + PASSIVE + + ; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/local/LocalFileClient.java b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/local/LocalFileClient.java new file mode 100644 index 0000000..abc02c3 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/java/com/baiye/starter/file/local/LocalFileClient.java @@ -0,0 +1,83 @@ +package com.baiye.starter.file.local; + +import cn.hutool.core.io.FileUtil; +import com.baiye.starter.file.FileProperties; +import com.baiye.starter.file.core.AbstractFileClient; +import com.baiye.starter.file.exception.FileException; +import com.baiye.util.StreamUtils; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author lingting 2021/10/17 20:11 + * @author 疯狂的狮子Li 2022-04-24 + */ +public class LocalFileClient extends AbstractFileClient { + + private final File parentDir; + + public LocalFileClient(FileProperties.LocalProperties properties) throws IOException { + final File dir = StringUtils.hasText(properties.getPath()) ? new File(properties.getPath()) + : FileUtil.getTmpDir(); + // 不存在且创建失败 + if (!dir.exists() && !dir.mkdirs()) { + throw new FileException(String.format("路径: %s; 不存在且创建失败! 请检查是否拥有对该路径的操作权限!", dir.getPath())); + } + parentDir = dir; + super.rootPath = dir.getPath(); + super.slash = File.separator; + } + + /** + * + * 文件上传 + * @param relativePath 文件相对 getRoot() 的路径 + * @param stream 文件输入流 + * @return 文件绝对路径 + */ + @Override + public String upload(InputStream stream, String relativePath) throws IOException { + // 目标文件 + final File file = new File(parentDir, relativePath); + + if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) { + throw new FileException("文件上传失败! 创建父级文件夹失败! 父级路径: " + file.getParentFile().getPath()); + } + + if (!file.exists() && !file.createNewFile()) { + throw new FileException("文件上传失败! 创建文件失败! 文件路径: " + file.getPath()); + } + + try (FileOutputStream outputStream = new FileOutputStream(file)) { + StreamUtils.write(stream, outputStream); + } + + return relativePath; + } + + /** + * 下载文件 + * @param relativePath 文件相对 getRoot() 的路径 + * @return java.io.FileOutputStream 文件流 + */ + @Override + public File download(String relativePath) throws IOException { + return new File(parentDir, relativePath); + } + + /** + * 删除文件 + * @param relativePath 文件相对 getRoot() 的路径 + * @return boolean + */ + @Override + public boolean delete(String relativePath) throws IOException { + final File file = new File(parentDir, relativePath); + return file.exists() && file.delete(); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-file/src/main/resources/META-INF/spring.factories b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..dcae179 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.baiye.starter.file.FileAutoConfiguration \ No newline at end of file diff --git a/marketing-scrm-starters/marketing-scrm-starter-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..e972ea0 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.baiye.starter.file.FileAutoConfiguration \ No newline at end of file diff --git a/marketing-scrm-starters/marketing-scrm-starter-job/pom.xml b/marketing-scrm-starters/marketing-scrm-starter-job/pom.xml new file mode 100644 index 0000000..ae761be --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-job/pom.xml @@ -0,0 +1,103 @@ + + + + marketing-scrm-starters + com.baiye + ${revision} + + 4.0.0 + + marketing-scrm-starter-job + + + 8 + 8 + + + + + + + com.example + elastic-job-springboot-core + 3.0-SNAPSHOT + + + org.apache.curator + curator-framework + + + org.apache.curator + curator-recipes + + + + + + com.baiye + marketing-scrm-extend-mybatis-plus + + + + + org.apache.curator + curator-framework + 2.12.0 + + + + org.apache.curator + curator-client + 2.12.0 + + + + org.apache.curator + curator-recipes + + + org.apache.zookeeper + zookeeper + + + + + + org.apache.curator + curator-framework + 2.12.0 + + + + org.apache.curator + curator-client + 2.12.0 + + + + org.apache.curator + curator-recipes + 2.12.0 + + + org.apache.zookeeper + zookeeper + + + + + + org.apache.zookeeper + zookeeper + 3.4.13 + + + org.slf4j + slf4j-log4j12 + + + + + diff --git a/marketing-scrm-starters/marketing-scrm-starter-job/src/main/java/com/baiye/common/job/config/DynamicElasticJobConfig.java b/marketing-scrm-starters/marketing-scrm-starter-job/src/main/java/com/baiye/common/job/config/DynamicElasticJobConfig.java new file mode 100644 index 0000000..bae4d92 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-job/src/main/java/com/baiye/common/job/config/DynamicElasticJobConfig.java @@ -0,0 +1,66 @@ +package com.baiye.common.job.config; + +import com.baiye.common.job.lintener.ElasticJobListener; +import com.dangdang.ddframe.job.event.JobEventConfiguration; +import com.dangdang.ddframe.job.event.rdb.JobEventRdbConfiguration; +import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperConfiguration; +import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperRegistryCenter; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.annotation.Order; + +import javax.annotation.Resource; +import javax.sql.DataSource; + +/** + * @author Enzo + * @date : 2024/5/28 + */ +@Configuration +public class DynamicElasticJobConfig { + /** + * zookeeper的服务地址 + */ + @Value("${elasticjob.zookeeper.server-list}") + private String serverLists; + /** + * Elastic-Job的命名空间 + */ + @Value("${elasticjob.zookeeper.namespace}") + private String namespace; + @Resource + private DataSource dataSource; + + @Bean + public ZookeeperConfiguration zookeeperConfiguration() { + return new ZookeeperConfiguration(serverLists, namespace); + } + @Primary + @Bean(initMethod = "init") + @ConditionalOnMissingBean(ZookeeperRegistryCenter.class) + public ZookeeperRegistryCenter zookeeperRegistryCenter(ZookeeperConfiguration zookeeperConfiguration) { + return new ZookeeperRegistryCenter(zookeeperConfiguration); + } + + + @Bean + public ElasticJobListener elasticJobListener(){ + return new ElasticJobListener(100, 100); + } + + + /** + * 将作业运行的痕迹进行持久化到DB + * + * @return + */ + @Bean + public JobEventConfiguration jobEventConfiguration() { + return new JobEventRdbConfiguration(dataSource); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-job/src/main/java/com/baiye/common/job/handler/ElasticJobHandler.java b/marketing-scrm-starters/marketing-scrm-starter-job/src/main/java/com/baiye/common/job/handler/ElasticJobHandler.java new file mode 100644 index 0000000..1bc9aa9 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-job/src/main/java/com/baiye/common/job/handler/ElasticJobHandler.java @@ -0,0 +1,79 @@ +package com.baiye.common.job.handler; + +import com.dangdang.ddframe.job.api.simple.SimpleJob; +import com.dangdang.ddframe.job.config.JobCoreConfiguration; +import com.dangdang.ddframe.job.config.simple.SimpleJobConfiguration; +import com.dangdang.ddframe.job.event.JobEventConfiguration; +import com.dangdang.ddframe.job.lite.api.listener.ElasticJobListener; +import com.dangdang.ddframe.job.lite.config.LiteJobConfiguration; +import com.dangdang.ddframe.job.lite.internal.schedule.JobRegistry; +import com.dangdang.ddframe.job.lite.spring.api.SpringJobScheduler; +import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperRegistryCenter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * @author Enzo + * @date : 2024/5/28 + */ +@Slf4j +@Component +public class ElasticJobHandler { + + + @Autowired + private ZookeeperRegistryCenter zkRegistryCenter; + @Resource + private JobEventConfiguration jobEventConfiguration; + @Resource + private ElasticJobListener elasticJobListener; + + /*** + * 动态创建定时任务 + * @param jobName:定时任务名称 + * @param cron:表达式 + * @param shardingTotalCount:分片数量 + * @param instance:定时任务实例 + * @param parameters:参数 + * @param description:作业描述 + */ + public void addJob(String jobName, String cron, int shardingTotalCount, SimpleJob instance, String parameters, String description) { + log.info("动态创建定时任务:jobName = {}, cron = {}, shardingTotalCount = {}, parameters = {}", jobName, cron, shardingTotalCount, parameters); + + LiteJobConfiguration.Builder builder = LiteJobConfiguration.newBuilder(new SimpleJobConfiguration( + JobCoreConfiguration.newBuilder( + jobName, + cron, + shardingTotalCount + ).jobParameter(parameters).description(description).build(), + instance.getClass().getName() + )).overwrite(true); + LiteJobConfiguration liteJobConfiguration = builder.build(); + + new SpringJobScheduler(instance, zkRegistryCenter, liteJobConfiguration, jobEventConfiguration, elasticJobListener).init(); + } + + /** + * 更新定时任务 + * + * @param jobName + * @param cron + */ + public void updateJob(String jobName, String cron) { + log.info("更新定时任务:jobName = {}, cron = {}", jobName, cron); + JobRegistry.getInstance().getJobScheduleController(jobName).rescheduleJob(cron); + } + + /** + * 删除定时任务 + * + * @param jobName + */ + public void removeJob(String jobName) { + log.info("删除定时任务:jobName = {}", jobName); + JobRegistry.getInstance().getJobScheduleController(jobName).shutdown(); + } +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-job/src/main/java/com/baiye/common/job/lintener/ElasticJobListener.java b/marketing-scrm-starters/marketing-scrm-starter-job/src/main/java/com/baiye/common/job/lintener/ElasticJobListener.java new file mode 100644 index 0000000..5f0c6d7 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-job/src/main/java/com/baiye/common/job/lintener/ElasticJobListener.java @@ -0,0 +1,25 @@ +package com.baiye.common.job.lintener; + +import com.dangdang.ddframe.job.executor.ShardingContexts; +import com.dangdang.ddframe.job.lite.api.listener.AbstractDistributeOnceElasticJobListener; + +/** + * @author Enzo + * @date : 2024/5/28 + */ +public class ElasticJobListener extends AbstractDistributeOnceElasticJobListener { + + public ElasticJobListener(long startedTimeoutMilliseconds, long completedTimeoutMilliseconds) { + super(startedTimeoutMilliseconds,completedTimeoutMilliseconds); + } + + @Override + public void doBeforeJobExecutedAtLastStarted(ShardingContexts shardingContexts) { + // 任务执行前完成后更新状态为已执行,当前未处理 + } + + @Override + public void doAfterJobExecutedAtLastCompleted(ShardingContexts shardingContexts) { + // 任务执行完成后更新状态为已执行,当前未处理 + } +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-redis/pom.xml b/marketing-scrm-starters/marketing-scrm-starter-redis/pom.xml new file mode 100644 index 0000000..6fbc824 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-redis/pom.xml @@ -0,0 +1,44 @@ + + + + marketing-scrm-starters + com.baiye + ${revision} + + 4.0.0 + marketing-scrm-starter-redis + + + + com.fasterxml.jackson.core + jackson-databind + true + + + com.baiye + common-redis + ${revision} + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.boot + spring-boot-starter-data-redis + + + diff --git a/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/java/com/baiye/autoconfigure/redis/AddMessageEventListenerToContainer.java b/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/java/com/baiye/autoconfigure/redis/AddMessageEventListenerToContainer.java new file mode 100644 index 0000000..4e99248 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/java/com/baiye/autoconfigure/redis/AddMessageEventListenerToContainer.java @@ -0,0 +1,32 @@ +package com.baiye.autoconfigure.redis; + +import com.baiye.listener.MessageEventListener; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +import javax.annotation.PostConstruct; +import java.util.List; + +/** + * @author hccake + */ +@RequiredArgsConstructor +public class AddMessageEventListenerToContainer { + + private final RedisMessageListenerContainer listenerContainer; + + private final List listenerList; + + /** + * 将所有的 MessageEventListener 注册到 + * RedisMessageListenerContainer 中 + */ + @PostConstruct + public void addMessageListener() { + // 注册监听器 + for (MessageEventListener messageEventListener : listenerList) { + listenerContainer.addMessageListener(messageEventListener, messageEventListener.topic()); + } + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/java/com/baiye/autoconfigure/redis/BallcatRedisAutoConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/java/com/baiye/autoconfigure/redis/BallcatRedisAutoConfiguration.java new file mode 100644 index 0000000..4fd013d --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/java/com/baiye/autoconfigure/redis/BallcatRedisAutoConfiguration.java @@ -0,0 +1,118 @@ +package com.baiye.autoconfigure.redis; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.baiye.RedisHelper; +import com.baiye.upms.config.CacheProperties; +import com.baiye.upms.config.CachePropertiesHolder; +import com.baiye.core.CacheStringAspect; +import com.baiye.prefix.IRedisPrefixConverter; +import com.baiye.prefix.impl.DefaultRedisPrefixConverter; +import com.baiye.serialize.CacheSerializer; +import com.baiye.serialize.JacksonSerializer; +import com.baiye.serialize.PrefixJdkRedisSerializer; +import com.baiye.serialize.PrefixStringRedisSerializer; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.DependsOn; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * Redis 自动配置类 + * + * @author Hccake + * @version 1.0 + * @date 2019/9/2 14:13 + */ +@AutoConfiguration(before = RedisAutoConfiguration.class) +@RequiredArgsConstructor +@EnableConfigurationProperties(CacheProperties.class) +public class BallcatRedisAutoConfiguration { + + private final RedisConnectionFactory redisConnectionFactory; + + /** + * 初始化配置类 + * @return GlobalCacheProperties + */ + @Bean + @ConditionalOnMissingBean + public CachePropertiesHolder cachePropertiesHolder(CacheProperties cacheProperties) { + CachePropertiesHolder cachePropertiesHolder = new CachePropertiesHolder(); + cachePropertiesHolder.setCacheProperties(cacheProperties); + return cachePropertiesHolder; + } + + /** + * 默认使用 Jackson 序列化 + * @param objectMapper objectMapper + * @return JacksonSerializer + */ + @Bean + @ConditionalOnMissingBean + public CacheSerializer cacheSerializer(ObjectMapper objectMapper) { + return new JacksonSerializer(objectMapper); + } + + /** + * redis key 前缀处理器 + * @return IRedisPrefixConverter + */ + @Bean + @DependsOn("cachePropertiesHolder") + @ConditionalOnProperty(prefix = "ballcat.redis", name = "key-prefix") + @ConditionalOnMissingBean(IRedisPrefixConverter.class) + public IRedisPrefixConverter redisPrefixConverter() { + return new DefaultRedisPrefixConverter(CachePropertiesHolder.keyPrefix()); + } + + @Bean + @ConditionalOnBean(IRedisPrefixConverter.class) + @ConditionalOnMissingBean + public StringRedisTemplate stringRedisTemplate(IRedisPrefixConverter redisPrefixConverter) { + StringRedisTemplate template = new StringRedisTemplate(); + template.setConnectionFactory(redisConnectionFactory); + template.setKeySerializer(new PrefixStringRedisSerializer(redisPrefixConverter)); + return template; + } + + @Bean + @ConditionalOnBean(IRedisPrefixConverter.class) + @ConditionalOnMissingBean(name = "redisTemplate") + public RedisTemplate redisTemplate(IRedisPrefixConverter redisPrefixConverter) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + template.setKeySerializer(new PrefixJdkRedisSerializer(redisPrefixConverter)); + return template; + } + + @Bean + @ConditionalOnMissingBean(RedisHelper.class) + public RedisHelper redisHelper(StringRedisTemplate template) { + RedisHelper.setRedisTemplate(template); + return RedisHelper.INSTANCE; + } + + /** + * 缓存注解操作切面
          + * 必须在 redisHelper 初始化之后使用 + * @param stringRedisTemplate 字符串存储的Redis操作类 + * @param cacheSerializer 缓存序列化器 + * @return CacheStringAspect 缓存注解操作切面 + */ + @Bean + @DependsOn("redisHelper") + @ConditionalOnMissingBean + public CacheStringAspect cacheStringAspect(StringRedisTemplate stringRedisTemplate, + CacheSerializer cacheSerializer) { + return new CacheStringAspect(stringRedisTemplate, cacheSerializer); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/java/com/baiye/autoconfigure/redis/MessageEventListenerAutoConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/java/com/baiye/autoconfigure/redis/MessageEventListenerAutoConfiguration.java new file mode 100644 index 0000000..dc9d6b5 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/java/com/baiye/autoconfigure/redis/MessageEventListenerAutoConfiguration.java @@ -0,0 +1,30 @@ +package com.baiye.autoconfigure.redis; + +import com.baiye.listener.MessageEventListener; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +/** + * @author hccake + */ +@Import(AddMessageEventListenerToContainer.class) +@ConditionalOnBean(MessageEventListener.class) +@Configuration(proxyBeanMethods = false) +@RequiredArgsConstructor +public class MessageEventListenerAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(RedisMessageListenerContainer.class) + public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + return container; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/java/com/baiye/autoconfigure/redis/RedisKeyEventAutoConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/java/com/baiye/autoconfigure/redis/RedisKeyEventAutoConfiguration.java new file mode 100644 index 0000000..e258fc8 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/java/com/baiye/autoconfigure/redis/RedisKeyEventAutoConfiguration.java @@ -0,0 +1,116 @@ +package com.baiye.autoconfigure.redis; + +import com.baiye.upms.config.CacheProperties; +import com.baiye.keyevent.listener.*; +import com.baiye.keyevent.template.KeyDeletedEventMessageTemplate; +import com.baiye.keyevent.template.KeyExpiredEventMessageTemplate; +import com.baiye.keyevent.template.KeySetEventMessageTemplate; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +import java.util.List; + +/** + * redis key event auto configuration + * + *

          + * Tips: + *

          + *

          + * extract message listener and message handler bean name that developers can cover with + * same bean name if that behavior can make them happy + *

          + * + * @author lishangbu 2023/1/12 + */ +@AutoConfiguration +public class RedisKeyEventAutoConfiguration { + + public static final String KEY_DELETED_EVENT_PREFIX = CacheProperties.PREFIX + ".key-deleted-event"; + + public static final String KEY_SET_EVENT_PREFIX = CacheProperties.PREFIX + ".key-set-event"; + + public static final String KEY_EXPIRED_EVENT_PREFIX = CacheProperties.PREFIX + ".key-expired-event"; + + @ConditionalOnProperty(prefix = KEY_DELETED_EVENT_PREFIX, name = "enabled", havingValue = "true") + public static class RedisKeyDeletedEventConfiguration { + + public static final String CONTAINER_NAME = "keyDeleteEventRedisMessageListenerContainer"; + + public static final String LISTENER_NAME = "keyDeletedEventMessageListener"; + + @Bean(name = CONTAINER_NAME) + @ConditionalOnMissingBean(name = CONTAINER_NAME) + public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + return container; + } + + @Bean(name = LISTENER_NAME) + @ConditionalOnMissingBean(name = LISTENER_NAME) + public AbstractDeletedKeyEventMessageListener keyDeletedEventMessageListener( + @Qualifier(value = CONTAINER_NAME) RedisMessageListenerContainer listenerContainer, + ObjectProvider> objectProvider) { + return new DefaultDeletedKeyEventMessageListener(listenerContainer, objectProvider); + } + + } + + @ConditionalOnProperty(prefix = KEY_SET_EVENT_PREFIX, name = "enabled", havingValue = "true") + public static class RedisKeySetEventConfiguration { + + public static final String CONTAINER_NAME = "keySetEventRedisMessageListenerContainer"; + + public static final String LISTENER_NAME = "keySetEventMessageListener"; + + @Bean(name = CONTAINER_NAME) + @ConditionalOnMissingBean(name = CONTAINER_NAME) + public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + return container; + } + + @Bean(name = LISTENER_NAME) + @ConditionalOnMissingBean(name = LISTENER_NAME) + public AbstractSetKeyEventMessageListener keySetEventMessageListener( + @Qualifier(value = CONTAINER_NAME) RedisMessageListenerContainer listenerContainer, + ObjectProvider> objectProvider) { + return new DefaultSetKeyEventMessageListener(listenerContainer, objectProvider); + } + + } + + @ConditionalOnProperty(prefix = KEY_EXPIRED_EVENT_PREFIX, name = "enabled", havingValue = "true") + public static class RedisKeyExpiredEventConfiguration { + + public static final String CONTAINER_NAME = "keyExpiredEventRedisMessageListenerContainer"; + + public static final String LISTENER_NAME = "keyExpiredEventMessageListener"; + + @Bean(name = CONTAINER_NAME) + @ConditionalOnMissingBean(name = CONTAINER_NAME) + public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + return container; + } + + @Bean(name = LISTENER_NAME) + @ConditionalOnMissingBean(name = LISTENER_NAME) + public AbstractExpiredKeyEventMessageListener keyExpiredEventMessageListener( + @Qualifier(value = CONTAINER_NAME) RedisMessageListenerContainer listenerContainer, + ObjectProvider> objectProvider) { + return new DefaultExpiredKeyEventMessageListener(listenerContainer, objectProvider); + } + + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/resources/META-INF/spring.factories b/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..ba1acd2 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.baiye.autoconfigure.redis.BallcatRedisAutoConfiguration,\ + com.baiye.autoconfigure.redis.MessageEventListenerAutoConfiguration,\ + com.baiye.autoconfigure.redis.RedisKeyEventAutoConfiguration diff --git a/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..2850be1 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +BallcatRedisAutoConfiguration +MessageEventListenerAutoConfiguration +RedisKeyEventAutoConfiguration \ No newline at end of file diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/pom.xml b/marketing-scrm-starters/marketing-scrm-starter-swagger/pom.xml new file mode 100644 index 0000000..eecf9a8 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/pom.xml @@ -0,0 +1,45 @@ + + + + marketing-scrm-starters + com.baiye + ${revision} + + 4.0.0 + marketing-scrm-starter-swagger + + + + cn.hutool + hutool-core + + + io.springfox + springfox-boot-starter + + + jakarta.servlet + jakarta.servlet-api + compile + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework + spring-web + + + org.springframework + spring-webmvc + true + + + diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SpringfoxHandlerProviderBeanPostProcessor.java b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SpringfoxHandlerProviderBeanPostProcessor.java new file mode 100644 index 0000000..809c921 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SpringfoxHandlerProviderBeanPostProcessor.java @@ -0,0 +1,53 @@ +package com.baiye.swagger; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.stereotype.Component; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; +import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider; +import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.stream.Collectors; + +/** + * springFox 与 springboot 2.6.x 不兼容问题的处理 + *

          + * 相关 issues 地址 + * + * @author hccake + */ +@Component +public class SpringfoxHandlerProviderBeanPostProcessor implements BeanPostProcessor { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) { + customizeSpringfoxHandlerMappings(getHandlerMappings(bean)); + } + return bean; + } + + private void customizeSpringfoxHandlerMappings(List mappings) { + List copy = mappings.stream() + .filter(mapping -> mapping.getPatternParser() == null) + .collect(Collectors.toList()); + mappings.clear(); + mappings.addAll(copy); + } + + @SuppressWarnings("unchecked") + private List getHandlerMappings(Object bean) { + try { + Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings"); + field.setAccessible(true); + return (List) field.get(bean); + } + catch (IllegalArgumentException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SwaggerAggregatorAutoConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SwaggerAggregatorAutoConfiguration.java new file mode 100644 index 0000000..aec1733 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SwaggerAggregatorAutoConfiguration.java @@ -0,0 +1,57 @@ +package com.baiye.swagger; + +import com.baiye.swagger.property.SwaggerAggregatorProperties; +import com.baiye.swagger.property.SwaggerProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.util.CollectionUtils; +import springfox.documentation.swagger.web.InMemorySwaggerResourcesProvider; +import springfox.documentation.swagger.web.SwaggerResource; +import springfox.documentation.swagger.web.SwaggerResourcesProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/11/1 20:03 + */ +@Import(SwaggerConfiguration.class) +@ConditionalOnProperty(prefix = SwaggerProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) +public class SwaggerAggregatorAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public SwaggerAggregatorProperties swaggerAggregatorProperties() { + return new SwaggerAggregatorProperties(); + } + + /** + * 聚合文档 + * @param defaultResourcesProvider 本地内存的资源提供者 + * @return SwaggerResourcesProvider + */ + @Primary + @Bean + @ConditionalOnBean(SwaggerAggregatorProperties.class) + public SwaggerResourcesProvider swaggerResourcesProvider(InMemorySwaggerResourcesProvider defaultResourcesProvider, + SwaggerAggregatorProperties swaggerAggregatorProperties) { + + return () -> { + // 聚合者自己的 Resources + List resources = new ArrayList<>(defaultResourcesProvider.get()); + // 提供者的 Resources + List providerResources = swaggerAggregatorProperties.getProviderResources(); + if (!CollectionUtils.isEmpty(providerResources)) { + resources.addAll(providerResources); + } + return resources; + }; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SwaggerConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SwaggerConfiguration.java new file mode 100644 index 0000000..64820d0 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SwaggerConfiguration.java @@ -0,0 +1,57 @@ +package com.baiye.swagger; + +import cn.hutool.core.text.CharSequenceUtil; +import com.baiye.swagger.builder.DocketBuildHelper; +import com.baiye.swagger.builder.MultiRequestHandlerSelectors; +import com.baiye.swagger.property.SwaggerProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import springfox.documentation.spring.web.plugins.ApiSelectorBuilder; +import springfox.documentation.spring.web.plugins.Docket; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/11/1 19:43 + */ +@RequiredArgsConstructor +@EnableConfigurationProperties(SwaggerProperties.class) +public class SwaggerConfiguration { + + private final SwaggerProperties swaggerProperties; + + @Bean + @ConditionalOnMissingBean + public Docket api() { + + DocketBuildHelper helper = new DocketBuildHelper(swaggerProperties); + + // @formatter:off + // 1. 文档信息构建 + Docket docket = new Docket(swaggerProperties.getDocumentationType().getType()) + .host(swaggerProperties.getHost()) + .apiInfo(helper.apiInfo()) + .groupName(swaggerProperties.getGroupName()) + .enable(swaggerProperties.getEnabled()); + + // 2. 安全配置 + docket.securitySchemes(helper.securitySchema()) + .securityContexts(helper.securityContext()); + + // 3. 文档筛选 + String basePackage = swaggerProperties.getBasePackage(); + ApiSelectorBuilder select = docket.select(); + if(CharSequenceUtil.isEmpty(basePackage)){ + select.apis(MultiRequestHandlerSelectors.any()); + }else { + select.apis(MultiRequestHandlerSelectors.basePackage(basePackage)); + } + select.paths(helper.paths()).build(); + + return docket; + // @formatter:on + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SwaggerEnabledStatusReplaceEnvironmentPostProcessor.java b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SwaggerEnabledStatusReplaceEnvironmentPostProcessor.java new file mode 100644 index 0000000..bba2507 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SwaggerEnabledStatusReplaceEnvironmentPostProcessor.java @@ -0,0 +1,69 @@ +package com.baiye.swagger; + +import com.baiye.swagger.property.SwaggerProperties; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * 将 ballcat swagger 的开关配置同步至 springfox + * + * @author hccake + */ +public class SwaggerEnabledStatusReplaceEnvironmentPostProcessor implements EnvironmentPostProcessor { + + /** + * 资源名称 + */ + private static final String REPLACE_SOURCE_NAME = "replaceEnvironment"; + + private static final String SPRINGFOX_SWAGGER_ENABLED_KEY = "springfox.documentation.enabled"; + + private static final String BALLCAT_SWAGGER_ENABLED_KEY = SwaggerProperties.PREFIX + ".enabled"; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + // 如果已经独立配置了 springfox 的开关信息,则不处理 + String springFoxSwaggerEnabledValue = environment.getProperty(SPRINGFOX_SWAGGER_ENABLED_KEY); + if (StringUtils.hasText(springFoxSwaggerEnabledValue)) { + return; + } + + // 获取 ballcat 的 swagger 开关状态 + boolean ballcatEnabledSwagger = true; + String ballcatSwaggerEnabledValue = environment.getProperty(BALLCAT_SWAGGER_ENABLED_KEY); + if (StringUtils.hasText(ballcatSwaggerEnabledValue)) { + ballcatEnabledSwagger = "true".equalsIgnoreCase(ballcatSwaggerEnabledValue); + } + + // 将 ballcat swagger 的开关状态同步至 springfox + Map map = new HashMap<>(1); + map.put(SPRINGFOX_SWAGGER_ENABLED_KEY, ballcatEnabledSwagger); + replace(environment.getPropertySources(), map); + } + + private void replace(MutablePropertySources propertySources, Map map) { + MapPropertySource target = null; + if (propertySources.contains(REPLACE_SOURCE_NAME)) { + PropertySource source = propertySources.get(REPLACE_SOURCE_NAME); + if (source instanceof MapPropertySource) { + target = (MapPropertySource) source; + target.getSource().putAll(map); + } + } + if (target == null) { + target = new MapPropertySource(REPLACE_SOURCE_NAME, map); + } + if (!propertySources.contains(REPLACE_SOURCE_NAME)) { + propertySources.addFirst(target); + } + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SwaggerProviderAutoConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SwaggerProviderAutoConfiguration.java new file mode 100644 index 0000000..24ef3a5 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/SwaggerProviderAutoConfiguration.java @@ -0,0 +1,60 @@ +package com.baiye.swagger; + +import com.baiye.swagger.property.SwaggerProperties; +import com.baiye.swagger.property.SwaggerProviderProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/11/1 20:03 + */ +@Import(SwaggerConfiguration.class) +@ConditionalOnProperty(prefix = SwaggerProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) +public class SwaggerProviderAutoConfiguration { + + private static final String ALL = "*"; + + @Bean + @ConditionalOnMissingBean + public SwaggerProviderProperties swaggerProviderProperties() { + return new SwaggerProviderProperties(); + } + + /** + * 允许swagger文档跨域访问 解决聚合文档导致的跨域问题 + * @return FilterRegistrationBean + */ + @Bean + @ConditionalOnBean(SwaggerProviderProperties.class) + public FilterRegistrationBean corsFilterRegistrationBean( + SwaggerProviderProperties swaggerProviderProperties) { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + String aggregatorOrigin = swaggerProviderProperties.getAggregatorOrigin(); + config.setAllowCredentials(true); + // 在 springmvc 5.3 版本之后,跨域来源使用 * 号进行匹配的方式进行调整 + if (ALL.equals(aggregatorOrigin)) { + config.addAllowedOriginPattern(ALL); + } + else { + config.addAllowedOrigin(aggregatorOrigin); + } + config.addAllowedHeader(ALL); + config.addAllowedMethod(ALL); + source.registerCorsConfiguration("/**", config); + FilterRegistrationBean bean = new FilterRegistrationBean<>(new CorsFilter(source)); + bean.setOrder(Ordered.HIGHEST_PRECEDENCE); + return bean; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/annotation/EnableSwagger2Aggregator.java b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/annotation/EnableSwagger2Aggregator.java new file mode 100644 index 0000000..e791a4b --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/annotation/EnableSwagger2Aggregator.java @@ -0,0 +1,22 @@ +package com.baiye.swagger.annotation; + +import com.baiye.swagger.SwaggerAggregatorAutoConfiguration; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + * 聚合者的swagger开启注解 + * + * @author Hccake + * @version 1.0 + * @date 2019/11/1 19:43 + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import({ SwaggerAggregatorAutoConfiguration.class }) +public @interface EnableSwagger2Aggregator { + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/annotation/EnableSwagger2Provider.java b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/annotation/EnableSwagger2Provider.java new file mode 100644 index 0000000..cf2897f --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/annotation/EnableSwagger2Provider.java @@ -0,0 +1,22 @@ +package com.baiye.swagger.annotation; + +import com.baiye.swagger.SwaggerProviderAutoConfiguration; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + * 提供者的swagger开启注解 + * + * @author Hccake + * @version 1.0 + * @date 2019/11/1 19:43 + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import({ SwaggerProviderAutoConfiguration.class }) +public @interface EnableSwagger2Provider { + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/builder/DocketBuildHelper.java b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/builder/DocketBuildHelper.java new file mode 100644 index 0000000..713cd59 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/builder/DocketBuildHelper.java @@ -0,0 +1,126 @@ +package com.baiye.swagger.builder; + +import com.baiye.swagger.constant.SwaggerConstants; +import com.baiye.swagger.property.SwaggerProperties; +import lombok.RequiredArgsConstructor; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.service.*; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * @author hccake + */ +@RequiredArgsConstructor +public class DocketBuildHelper { + + private final SwaggerProperties swaggerProperties; + + public Predicate paths() { + // base-path 和 exclude-path 的默认值处理 + if (swaggerProperties.getBasePath().isEmpty()) { + swaggerProperties.getBasePath().add(SwaggerConstants.DEFAULT_BASE_PATH); + } + if (swaggerProperties.getExcludePath().isEmpty()) { + swaggerProperties.getExcludePath().addAll(SwaggerConstants.DEFAULT_EXCLUDE_PATH); + } + + List> basePath = new ArrayList<>(); + for (String path : swaggerProperties.getBasePath()) { + basePath.add(PathSelectors.ant(path)); + } + List> excludePath = new ArrayList<>(); + for (String path : swaggerProperties.getExcludePath()) { + excludePath.add(PathSelectors.ant(path)); + } + // 必须满足basePath 且不满足 exclude-path + return s -> basePath.stream().anyMatch(x -> x.test(s)) && excludePath.stream().noneMatch(x -> x.test(s)); + } + + public ApiInfo apiInfo() { + // @formatter:off + return new ApiInfoBuilder() + .title(swaggerProperties.getTitle()) + .description(swaggerProperties.getDescription()) + .license(swaggerProperties.getLicense()) + .licenseUrl(swaggerProperties.getLicenseUrl()) + .termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl()) + .contact(new Contact(swaggerProperties.getContact().getName(), + swaggerProperties.getContact().getUrl(), + swaggerProperties.getContact().getEmail())) + .version(swaggerProperties.getVersion()) + .build(); + // @formatter:on + } + + public List securitySchema() { + SwaggerProperties.Authorization authorizationProps = swaggerProperties.getAuthorization(); + + List authorizationScopeList = authorizationProps.getAuthorizationScopeList() + .stream() + .map(scope -> new AuthorizationScope(scope.getScope(), scope.getDescription())) + .collect(Collectors.toList()); + + String tokenUrl = authorizationProps.getTokenUrl(); + + DocumentationType documentationType = swaggerProperties.getDocumentationType().getType(); + SecurityScheme securityScheme; + if (documentationType.equals(DocumentationType.SWAGGER_2)) { + // swagger2 OAuth2 + List grantTypes = Collections.singletonList(new ResourceOwnerPasswordCredentialsGrant(tokenUrl)); + securityScheme = new OAuth(authorizationProps.getName(), authorizationScopeList, grantTypes); + } + else { + // Swagger3 Oauth2 + securityScheme = OAuth2Scheme.OAUTH2_PASSWORD_FLOW_BUILDER.name(authorizationProps.getName()) + .tokenUrl(tokenUrl) + .scopes(authorizationScopeList) + .build(); + } + + return Collections.singletonList(securityScheme); + } + + /** + * 配置默认的全局鉴权策略的开关,通过正则表达式进行匹配;默认匹配所有URL + * @return SecurityContext + */ + public List securityContext() { + // @formatter:off + SecurityContext securityContext = SecurityContext.builder() + .securityReferences(defaultAuth()) + .build(); + // @formatter:on + + return Collections.singletonList(securityContext); + } + + /** + * 默认的全局鉴权策略 + * @return List + */ + public List defaultAuth() { + SwaggerProperties.Authorization authorization = swaggerProperties.getAuthorization(); + List authorizationScopeList = authorization.getAuthorizationScopeList() + .stream() + .map(scope -> new AuthorizationScope(scope.getScope(), scope.getDescription())) + .collect(Collectors.toList()); + + AuthorizationScope[] authorizationScopes = new AuthorizationScope[authorizationScopeList.size()]; + + SecurityReference securityReference = SecurityReference.builder() + .reference(authorization.getName()) + .scopes(authorizationScopeList.toArray(authorizationScopes)) + .build(); + + return Collections.singletonList(securityReference); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/builder/MultiRequestHandlerSelectors.java b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/builder/MultiRequestHandlerSelectors.java new file mode 100644 index 0000000..63338bf --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/builder/MultiRequestHandlerSelectors.java @@ -0,0 +1,92 @@ +package com.baiye.swagger.builder; + +import org.springframework.util.ClassUtils; +import springfox.documentation.RequestHandler; + +import java.lang.annotation.Annotation; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * 多路径选择 + * + * @author hccake + */ +public class MultiRequestHandlerSelectors { + + private MultiRequestHandlerSelectors() { + throw new UnsupportedOperationException(); + } + + private static final String PACKAGE_SEPARATOR = ";"; + + /** + * Any RequestHandler satisfies this condition + * @return predicate that is always true + */ + public static Predicate any() { + return each -> true; + } + + /** + * No RequestHandler satisfies this condition + * @return predicate that is always false + */ + public static Predicate none() { + return each -> false; + } + + /** + * Predicate that matches RequestHandler with handlers methods annotated with given + * annotation + * @param annotation - annotation to check + * @return this + */ + public static Predicate withMethodAnnotation(final Class annotation) { + return input -> input.isAnnotatedWith(annotation); + } + + /** + * Predicate that matches RequestHandler with given annotation on the declaring class + * of the handler method + * @param annotation - annotation to check + * @return this + */ + public static Predicate withClassAnnotation(final Class annotation) { + return input -> declaringClass(input).map(annotationPresent(annotation)).orElse(false); + } + + private static Function, Boolean> annotationPresent(final Class annotation) { + return input -> input.isAnnotationPresent(annotation); + } + + private static Function, Boolean> handlerPackage(final String basePackage) { + return input -> { + String inputPackageName = ClassUtils.getPackageName(input); + String[] basePackages = basePackage.split(PACKAGE_SEPARATOR); + for (String packageName : basePackages) { + if (inputPackageName.startsWith(packageName)) { + return true; + } + } + return false; + }; + } + + /** + * Predicate that matches RequestHandler with given base package name for the class of + * the handler method. This predicate includes all request handlers matching the + * provided basePackage + * @param basePackage - base package of the classes + * @return this + */ + public static Predicate basePackage(String basePackage) { + return input -> declaringClass(input).map(handlerPackage(basePackage)).orElse(true); + } + + private static Optional> declaringClass(RequestHandler input) { + return Optional.ofNullable(input.declaringClass()); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/constant/SwaggerConstants.java b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/constant/SwaggerConstants.java new file mode 100644 index 0000000..3cb7973 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/constant/SwaggerConstants.java @@ -0,0 +1,26 @@ +package com.baiye.swagger.constant; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/11/1 20:03 + */ +public final class SwaggerConstants { + + private SwaggerConstants() { + } + + /** + * 默认的排除路径,排除Spring Boot默认的错误处理路径和端点 + */ + public static final List DEFAULT_EXCLUDE_PATH = Arrays.asList("/error", "/actuator/**"); + + /** + * 默认扫描路径 + */ + public static final String DEFAULT_BASE_PATH = "/**"; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/property/DocumentationTypeEnum.java b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/property/DocumentationTypeEnum.java new file mode 100644 index 0000000..871edc5 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/property/DocumentationTypeEnum.java @@ -0,0 +1,29 @@ +package com.baiye.swagger.property; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import springfox.documentation.spi.DocumentationType; + +/** + * 文档 swagger 版本 + * + * @author Hccake 2021/1/21 + * @version 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum DocumentationTypeEnum { + + /** + * swagger2.0 + */ + SWAGGER_2(DocumentationType.SWAGGER_2), + + /** + * swagger 3.0 openApi + */ + OAS_30(DocumentationType.OAS_30); + + private final DocumentationType type; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/property/SwaggerAggregatorProperties.java b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/property/SwaggerAggregatorProperties.java new file mode 100644 index 0000000..9a530ae --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/property/SwaggerAggregatorProperties.java @@ -0,0 +1,24 @@ +package com.baiye.swagger.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import springfox.documentation.swagger.web.SwaggerResource; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/11/1 20:05 + */ +@Data +@ConfigurationProperties("ballcat.swagger.aggregator") +public class SwaggerAggregatorProperties { + + /** + * 聚合文档源信息 + */ + private List providerResources = new ArrayList<>(); + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/property/SwaggerProperties.java b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/property/SwaggerProperties.java new file mode 100644 index 0000000..85c251c --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/property/SwaggerProperties.java @@ -0,0 +1,150 @@ +package com.baiye.swagger.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/11/1 19:37 + */ +@Data +@ConfigurationProperties(SwaggerProperties.PREFIX) +public class SwaggerProperties { + + public static final String PREFIX = "ballcat.swagger"; + + /** + * 是否开启swagger + */ + private Boolean enabled = true; + + /** + * 分组名称 + */ + private String groupName; + + /** + * 文档版本,默认使用 2.0 + */ + private DocumentationTypeEnum documentationType = DocumentationTypeEnum.SWAGGER_2; + + /** + * swagger会解析的包路径 + **/ + private String basePackage = ""; + + /** + * swagger会解析的url规则 + **/ + private List basePath = new ArrayList<>(); + + /** + * 在basePath基础上需要排除的url规则 + **/ + private List excludePath = new ArrayList<>(); + + /** + * 标题 + **/ + private String title = ""; + + /** + * 描述 + **/ + private String description = ""; + + /** + * 版本 + **/ + private String version = ""; + + /** + * 许可证 + **/ + private String license = ""; + + /** + * 许可证URL + **/ + private String licenseUrl = ""; + + /** + * 服务条款URL + **/ + private String termsOfServiceUrl = ""; + + /** + * host信息 + **/ + private String host = ""; + + /** + * 联系人信息 + */ + private Contact contact = new Contact(); + + /** + * 全局统一鉴权配置 + **/ + private Authorization authorization = new Authorization(); + + @Data + public static class Contact { + + /** + * 联系人 + **/ + private String name = ""; + + /** + * 联系人url + **/ + private String url = ""; + + /** + * 联系人email + **/ + private String email = ""; + + } + + @Data + public static class Authorization { + + /** + * 鉴权策略ID,需要和SecurityReferences ID保持一致 + */ + private String name = ""; + + /** + * 鉴权作用域列表 + */ + private List authorizationScopeList = new ArrayList<>(); + + /** + * token请求地址,如需开启OAuth2 password 类型登录则必传此参数 + */ + private String tokenUrl = ""; + + } + + @Data + public static class AuthorizationScope { + + /** + * 作用域名称 + */ + private String scope = ""; + + /** + * 作用域描述 + */ + private String description = ""; + + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/property/SwaggerProviderProperties.java b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/property/SwaggerProviderProperties.java new file mode 100644 index 0000000..e6de880 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/java/com/baiye/swagger/property/SwaggerProviderProperties.java @@ -0,0 +1,20 @@ +package com.baiye.swagger.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/11/1 20:05 + */ +@Data +@ConfigurationProperties("ballcat.swagger.provider") +public class SwaggerProviderProperties { + + /** + * 聚合者的来源,用于控制跨域放行 + */ + private String aggregatorOrigin = "*"; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/resources/META-INF/spring.factories b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..e4eeb52 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Environment Post Processor +org.springframework.boot.env.EnvironmentPostProcessor=\ +com.baiye.swagger.SwaggerEnabledStatusReplaceEnvironmentPostProcessor \ No newline at end of file diff --git a/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/resources/META-INF/spring/org.springframework.boot.env.EnvironmentPostProcessor.imports b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/resources/META-INF/spring/org.springframework.boot.env.EnvironmentPostProcessor.imports new file mode 100644 index 0000000..c734418 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-swagger/src/main/resources/META-INF/spring/org.springframework.boot.env.EnvironmentPostProcessor.imports @@ -0,0 +1 @@ +SwaggerEnabledStatusReplaceEnvironmentPostProcessor \ No newline at end of file diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/pom.xml b/marketing-scrm-starters/marketing-scrm-starter-web/pom.xml new file mode 100644 index 0000000..5b008ba --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/pom.xml @@ -0,0 +1,69 @@ + + + + marketing-scrm-starters + com.baiye + ${revision} + + 4.0.0 + marketing-scrm-starter-web + + + + com.baiye + common-core + ${revision} + + + + com.baiye + common-desensitize + ${revision} + + + + com.baiye + common-model + ${revision} + + + com.baiye + common-util + ${revision} + + + + + jakarta.validation + jakarta.validation-api + + + org.hibernate.validator + hibernate-validator + + + + org.springframework.boot + spring-boot-actuator-autoconfigure + true + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.security + spring-security-core + true + + + diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/actuate/ActuatorSecurityAutoConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/actuate/ActuatorSecurityAutoConfiguration.java new file mode 100644 index 0000000..aca9f6f --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/actuate/ActuatorSecurityAutoConfiguration.java @@ -0,0 +1,52 @@ +package com.baiye.web.actuate; + +import cn.hutool.core.text.CharSequenceUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; + +/** + * 当开启 Actuator 时,注册 Actuator 安全过滤器 + * + * @author Hccake 2019/10/17 20:26 + */ +@Slf4j +@AutoConfiguration +@RequiredArgsConstructor +@ConditionalOnWebApplication +@ConditionalOnClass(WebEndpointProperties.class) +@ConditionalOnProperty(prefix = ActuatorSecurityProperties.PREFIX, name = "auth", havingValue = "true") +@EnableConfigurationProperties(ActuatorSecurityProperties.class) +public class ActuatorSecurityAutoConfiguration { + + private final WebEndpointProperties webEndpointProperties; + + @Bean + public FilterRegistrationBean actuatorFilterRegistrationBean( + ActuatorSecurityProperties actuatorSecurityProperties) { + log.debug("Actuator 过滤器已开启===="); + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + if (actuatorSecurityProperties.isAuth()) { + // 监控开启 + ActuatorSecurityFilter filter = new ActuatorSecurityFilter(actuatorSecurityProperties.getSecretId(), + actuatorSecurityProperties.getSecretKey()); + registrationBean.setFilter(filter); + String basePath = webEndpointProperties.getBasePath(); + if (CharSequenceUtil.isBlank(basePath)) { + basePath = "/actuator"; + } + registrationBean.addUrlPatterns(basePath + "/*"); + registrationBean.setOrder(0); + } + + return registrationBean; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/actuate/ActuatorSecurityFilter.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/actuate/ActuatorSecurityFilter.java new file mode 100644 index 0000000..0579e4a --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/actuate/ActuatorSecurityFilter.java @@ -0,0 +1,98 @@ +package com.baiye.web.actuate; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import com.baiye.constant.HeaderConstants; +import com.baiye.result.R; +import com.baiye.result.SystemResultCode; +import com.baiye.util.JsonUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Actuator 安全过滤器,做一个签名认证,校验通过才允许访问 + * + * @author Hccake + * @version 1.0 + * @date 2019 /10/17 20:28 + */ +public class ActuatorSecurityFilter extends OncePerRequestFilter { + + private final String secretId; + + private final String secretKey; + + /** + * Instantiates a new Actuator filter. + * @param secretId the secret id + * @param secretKey the secret key + */ + public ActuatorSecurityFilter(String secretId, String secretKey) { + this.secretId = secretId; + this.secretKey = secretKey; + } + + /** + * Same contract as for {@code doFilter}, but guaranteed to be just invoked once per + * request within a single request thread. See {@link #shouldNotFilterAsyncDispatch()} + * for details. + *

          + * Provides HttpServletRequest and HttpServletResponse arguments instead of the + * default ServletRequest and ServletResponse ones. + * @param request 请求信息 + * @param response 响应信息 + * @param filterChain 过滤器链 + */ + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + // 检验签名是否正确 + String reqSecretId = request.getHeader(HeaderConstants.SECRET_ID); + String sign = request.getHeader(HeaderConstants.SIGN); + String reqTime = request.getHeader(HeaderConstants.REQ_TIME); + if (verifySign(reqSecretId, sign, reqTime)) { + filterChain.doFilter(request, response); + } + else { + response.setHeader("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + R r = R.failed(SystemResultCode.UNAUTHORIZED); + response.getWriter().write(JsonUtils.toJson(r)); + } + } + + /** + * 校验sign + * @param reqSecretId secretId + * @param sign 签名 + * @param reqTime 请求时间戳 ms + * @return boolean 通过返回true + */ + private boolean verifySign(String reqSecretId, String sign, String reqTime) { + if (CharSequenceUtil.isNotBlank(sign) && CharSequenceUtil.isNotBlank(reqTime) + && CharSequenceUtil.isNotBlank(reqSecretId)) { + if (!reqSecretId.equals(this.secretId)) { + return false; + } + // 过期时间 30秒失效 + long expireTime = 30 * 1000L; + long nowTime = System.currentTimeMillis(); + if (nowTime - Long.parseLong(reqTime) <= expireTime) { + String reverse = StrUtil.reverse(reqTime); + String checkSign = SecureUtil.md5(reverse + this.secretId + this.secretKey); + return CharSequenceUtil.equalsIgnoreCase(checkSign, sign); + } + } + return false; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/actuate/ActuatorSecurityProperties.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/actuate/ActuatorSecurityProperties.java new file mode 100644 index 0000000..3601905 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/actuate/ActuatorSecurityProperties.java @@ -0,0 +1,24 @@ +package com.baiye.web.actuate; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author lingting 2020-10-13 22:39 + */ +@Data +@ConfigurationProperties(prefix = ActuatorSecurityProperties.PREFIX) +public class ActuatorSecurityProperties { + + public static final String PREFIX = "ballcat.actuator"; + + /** + * 是否开启鉴权 + */ + private boolean auth = false; + + private String secretId; + + private String secretKey; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/event/ApplicationContextInitialized.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/event/ApplicationContextInitialized.java new file mode 100644 index 0000000..8bcc03b --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/event/ApplicationContextInitialized.java @@ -0,0 +1,23 @@ +package com.baiye.web.event; + +import com.baiye.util.SpringUtils; +import org.springframework.boot.context.event.ApplicationContextInitializedEvent; +import org.springframework.context.ApplicationListener; + +/** + * 监听spring上下文初始化完成事件 + * + * @author lingting 2022/10/15 15:27 + */ +public class ApplicationContextInitialized implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationContextInitializedEvent event) { + /* + * 给 SpringUtils 注入 spring 上下文, 这样子就在任何地方都可以直接使用 SpringUtils了, 不需要额外声明依赖 处于兼容考虑, + * 未删除 SpringUtils 中的 @Component 注解, 且由于此实现依赖 spring-boot 所以先放在 starter-web 这里. + */ + SpringUtils.setContext(event.getApplicationContext()); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/event/EnvironmentPost.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/event/EnvironmentPost.java new file mode 100644 index 0000000..acdefa8 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/event/EnvironmentPost.java @@ -0,0 +1,23 @@ +package com.baiye.web.event; + +import com.baiye.util.EnvironmentUtils; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * 高优先级, 用于注入环境配置到工具类 + * + * @author lingting 2022/10/15 11:33 + */ +@Order(Ordered.HIGHEST_PRECEDENCE) +public class EnvironmentPost implements EnvironmentPostProcessor { + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + EnvironmentUtils.setEnvironment(environment); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/ExceptionAutoConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/ExceptionAutoConfiguration.java new file mode 100644 index 0000000..b605ef2 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/ExceptionAutoConfiguration.java @@ -0,0 +1,142 @@ +// package com.baiye.web.exception; +// +// import com.baiye.exception.handler.GlobalExceptionHandler; +// import com.baiye.web.exception.resolver.GlobalHandlerExceptionResolver; +// import com.baiye.web.exception.resolver.SecurityHandlerExceptionResolver; +// import com.baiye.web.exception.handler.DefaultGlobalExceptionHandler; +// import lombok.RequiredArgsConstructor; +// import org.springframework.beans.factory.annotation.Value; +// import org.springframework.boot.autoconfigure.AutoConfiguration; +// import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +// import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +// import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +// import org.springframework.boot.context.properties.EnableConfigurationProperties; +// import org.springframework.context.ApplicationContext; +// import org.springframework.context.annotation.Bean; +// import org.springframework.security.access.AccessDeniedException; +// import org.springframework.util.CollectionUtils; +// +/// ** +// * @author Hccake +// * @version 1.0 +// * @date 2019/10/15 18:20 +// */ +// @RequiredArgsConstructor +// @AutoConfiguration +// @EnableConfigurationProperties(ExceptionHandleProperties.class) +// public class ExceptionAutoConfiguration { +// +// @Value("${spring.application.name: unknown-application}") +// private String applicationName; +// +// /** +// * 默认的日志处理器 +// * @return DefaultExceptionHandler +// * @deprecated 使用 enabled 来进行配置 +// */ +// @Bean +// @Deprecated +// @ConditionalOnMissingBean(GlobalExceptionHandler.class) +// @ConditionalOnProperty(prefix = ExceptionHandleProperties.PREFIX, matchIfMissing = +// true, name = "type", +// havingValue = "NONE") +// public GlobalExceptionHandler defaultGlobalExceptionHandler() { +// return new DefaultGlobalExceptionHandler(); +// } +// +// @Bean +// @ConditionalOnMissingBean(GlobalExceptionHandler.class) +// public GlobalExceptionHandler multiGlobalExceptionHandler(ExceptionHandleProperties +// properties, +// ApplicationContext context) { +// // 旧代码逻辑 +// if (properties.getType() != null) { +// switch (properties.getType()) { +// case NONE: +// return new DefaultGlobalExceptionHandler(); +// case MAIL: +// return new MailGlobalExceptionHandler(properties, context.getBean(MailSender.class), +// context.getApplicationName()); +// default: +// return new DingTalkGlobalExceptionHandler(properties, +// context.getBean(DingTalkSender.class), +// context.getApplicationName()); +// } +// } +// // 为空 或者 为 false +// if (!Boolean.TRUE.equals(properties.getEnabled())) { +// return new DefaultGlobalExceptionHandler(); +// } +// +// final Object mailSender; +// if (CollectionUtils.isEmpty(properties.getReceiveEmails())) { +// mailSender = null; +// } +// else { +// mailSender = context.getBean(MailSender.class); +// } +// return new MultiGlobalExceptionHandler(properties, context.getApplicationName(), +// mailSender); +// } +// +// /** +// * 钉钉消息通知的日志处理器 +// */ +// @Bean +// @ConditionalOnMissingBean(GlobalExceptionHandler.class) +// @ConditionalOnProperty(prefix = "ballcat.exception", name = "type", havingValue = +// "DING_TALK") +// public GlobalExceptionHandler dingTalkGlobalExceptionHandler(ExceptionHandleProperties +// exceptionHandleProperties, +// ApplicationContext context) { +// return new DingTalkGlobalExceptionHandler(exceptionHandleProperties, +// context.getBean(DingTalkSender.class), +// applicationName); +// } +// +// /** +// * 邮件消息通知的日志处理器 +// */ +// @Bean +// @ConditionalOnMissingBean(GlobalExceptionHandler.class) +// @ConditionalOnProperty(prefix = "ballcat.exception", name = "type", havingValue = +// "MAIL") +// public GlobalExceptionHandler mailGlobalExceptionHandler(ExceptionHandleProperties +// exceptionHandleProperties, +// ApplicationContext context) { +// return new MailGlobalExceptionHandler(exceptionHandleProperties, +// context.getBean(MailSender.class), +// applicationName); +// } +// +// /** +// * 默认的异常处理器 +// * @return GlobalHandlerExceptionResolver +// */ +// @Bean +// @ConditionalOnMissingBean(GlobalHandlerExceptionResolver.class) +// public GlobalHandlerExceptionResolver globalExceptionHandlerResolver( +// GlobalExceptionHandler globalExceptionHandler) { +// return new GlobalHandlerExceptionResolver(globalExceptionHandler); +// } +// +// /** +// * Security 异常处理,隔离出一个配置类 +// */ +// @ConditionalOnClass(AccessDeniedException.class) +// static class SecurityExceptionConfiguration { +// +// /** +// * security 相关的异常处理 +// * @return SecurityHandlerExceptionResolver +// */ +// @Bean +// @ConditionalOnMissingBean(SecurityHandlerExceptionResolver.class) +// public SecurityHandlerExceptionResolver securityHandlerExceptionResolver( +// GlobalExceptionHandler globalExceptionHandler) { +// return new SecurityHandlerExceptionResolver(globalExceptionHandler); +// } +// +// } +// +// } diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/ExceptionHandleProperties.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/ExceptionHandleProperties.java new file mode 100644 index 0000000..4e03406 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/ExceptionHandleProperties.java @@ -0,0 +1,103 @@ +package com.baiye.web.exception; + +import com.baiye.web.exception.enums.ExceptionHandleTypeEnum; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * @author lingting 2020/6/12 0:15 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = ExceptionHandleProperties.PREFIX) +public class ExceptionHandleProperties { + + public static final String PREFIX = "ballcat.exception"; + + /** + * 处理类型, 新版不需要指定异常处理类型了, 但是已经指定的旧配置依然按照旧配置的逻辑运行. + * @deprecated 下版本删除 + */ + @Deprecated + private ExceptionHandleTypeEnum type = null; + + /** + * 是否开启异常通知 + */ + private Boolean enabled = false; + + /** + * 是否同时忽略 配置的忽略异常类的子类, 默认忽略子类 + */ + private Boolean ignoreChild = true; + + /** + * 忽略指定异常 + */ + private Set> ignoreExceptions = new HashSet<>(); + + /** + * 通知间隔时间 单位秒 默认 5分钟 + */ + private long time = TimeUnit.MINUTES.toSeconds(5); + + /** + * 消息阈值 即便间隔时间没有到达设定的时间, 但是异常发生的数量达到阈值 则立即发送消息 + */ + private int max = 5; + + /** + * 堆栈转string 的长度 + */ + private int length = 3000; + + /** + * 接收异常通知邮件的邮箱 + */ + private Set receiveEmails = new HashSet<>(0); + + /** + * 接收异常的钉钉配置 + */ + private DingTalkProperties dingTalk; + + /** + * 异常通知 钉钉配置 + */ + @Data + public static class DingTalkProperties { + + /** + * 是否艾特所有人 + */ + private Boolean atAll = false; + + /** + * 发送配置 + */ + private List senders; + + @Data + public static class Sender { + + /** + * Web hook 地址 + */ + private String url; + + /** + * 密钥 + */ + private String secret; + + } + + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/domain/ExceptionMessage.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/domain/ExceptionMessage.java new file mode 100644 index 0000000..994ff6f --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/domain/ExceptionMessage.java @@ -0,0 +1,86 @@ +package com.baiye.web.exception.domain; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + * 异常通知消息 + * + * @author lingting 2020/6/12 16:07 + */ +@Getter +@Setter +@Accessors(chain = true) +public class ExceptionMessage { + + /** + * 用于筛选重复异常 + */ + private String key; + + /** + * 消息 + */ + private String message; + + /** + * 数量 + */ + private int number; + + /** + * 堆栈 + */ + private String stack; + + /** + * 最新的触发时间 + */ + private String time; + + /** + * 机器地址 + */ + private String mac; + + /** + * 线程id + */ + private long threadId; + + /** + * 服务名 + */ + private String applicationName; + + /** + * hostname + */ + private String hostname; + + /** + * ip + */ + private String ip; + + /** + * 请求地址 + */ + private String requestUri; + + /** + * 数量自增 + */ + public ExceptionMessage increment() { + number++; + return this; + } + + @Override + public String toString() { + return "服务名称:" + applicationName + "\nip:" + ip + "\nhostname:" + hostname + "\n机器地址:" + mac + "\n触发时间:" + time + + "\n请求地址:" + requestUri + "\n线程id:" + threadId + "\n数量:" + number + "\n堆栈:" + stack; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/domain/ExceptionNoticeResponse.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/domain/ExceptionNoticeResponse.java new file mode 100644 index 0000000..571477a --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/domain/ExceptionNoticeResponse.java @@ -0,0 +1,27 @@ +package com.baiye.web.exception.domain; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + * 异常消息通知响应 + * + * @author lingting 2020/6/12 19:07 + */ +@Getter +@Setter +@Accessors(chain = true) +public class ExceptionNoticeResponse { + + /** + * 是否成功 + */ + private boolean success; + + /** + * 错误信息 + */ + private String errMsg; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/enums/ExceptionHandleTypeEnum.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/enums/ExceptionHandleTypeEnum.java new file mode 100644 index 0000000..a3f133c --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/enums/ExceptionHandleTypeEnum.java @@ -0,0 +1,22 @@ +package com.baiye.web.exception.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 异常处理类型 + * + * @author lingting 2020/6/12 0:18 + */ +@Getter +@AllArgsConstructor +public enum ExceptionHandleTypeEnum { + + /** + * 异常处理通知类型 说明 + */ + NONE("不通知"), DING_TALK("通过钉钉通知"), MAIL("邮件通知"),; + + private final String text; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/AbstractNoticeGlobalExceptionHandler.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/AbstractNoticeGlobalExceptionHandler.java new file mode 100644 index 0000000..c25c871 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/AbstractNoticeGlobalExceptionHandler.java @@ -0,0 +1,199 @@ +package com.baiye.web.exception.handler; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.exceptions.ExceptionUtil; +import com.baiye.exception.handler.GlobalExceptionHandler; +import com.baiye.util.WebUtils; +import com.baiye.web.exception.ExceptionHandleProperties; +import com.baiye.web.exception.domain.ExceptionMessage; +import com.baiye.web.exception.domain.ExceptionNoticeResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author lingting 2020-09-03 20:09 + */ +@Slf4j +public abstract class AbstractNoticeGlobalExceptionHandler extends Thread + implements GlobalExceptionHandler, InitializingBean { + + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + + private static final String NULL_MESSAGE_KEY = ""; + + protected final ExceptionHandleProperties config; + + /** + * 通知消息存放 e.message 堆栈信息 + */ + private final Map messages; + + /** + * 本地物理地址 + */ + private String mac; + + /** + * 本地hostname + */ + private String hostname; + + /** + * 本地ip + */ + private String ip; + + /** + * 请求地址 + */ + private String requestUri; + + private final String applicationName; + + protected AbstractNoticeGlobalExceptionHandler(ExceptionHandleProperties config, String applicationName) { + this.config = config; + messages = new ConcurrentHashMap<>(config.getMax() * 2); + this.applicationName = applicationName; + try { + InetAddress ia = InetAddress.getLocalHost(); + hostname = ia.getHostName(); + ip = ia.getHostAddress(); + + byte[] macByte = NetworkInterface.getByInetAddress(ia).getHardwareAddress(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < macByte.length; i++) { + sb.append(String.format("%02X%s", macByte[i], (i < macByte.length - 1) ? "-" : "")); + } + this.mac = sb.toString(); + } + catch (Exception e) { + mac = "获取失败!"; + } + } + + @Override + @SuppressWarnings("all") + public void run() { + String key; + TimeInterval interval = new TimeInterval(); + long threadId = Thread.currentThread().getId(); + // 未被中断则一直运行 + while (!isInterrupted()) { + int i = 0; + while (i < config.getMax() && interval.intervalSecond() < config.getTime()) { + Throwable t = null; + try { + // 如果 i=0,即 当前未处理异常,则等待超时时间为 1 小时, 否则为 10 秒 + t = queue.poll(i == 0 ? TimeUnit.HOURS.toSeconds(1) : 10, TimeUnit.SECONDS); + } + catch (InterruptedException e) { + interrupt(); + } + if (t != null) { + key = t.getMessage() == null ? NULL_MESSAGE_KEY : t.getMessage(); + // i++ + if (i++ == 0) { + // 第一次收到数据, 重置计时 + interval.restart(); + messages.put(key, toMessage(t).setKey(key).setThreadId(threadId)); + } + else { + if (messages.containsKey(key)) { + messages.put(key, messages.get(key).increment()); + } + else { + messages.put(key, toMessage(t).setKey(key).setThreadId(threadId)); + } + } + } + } + // 一次处理结束 + if (messages.size() > 0) { + // 如果需要发送的消息不为空 + messages.forEach((k, v) -> { + try { + ExceptionNoticeResponse response = send(v); + if (!response.isSuccess()) { + log.error("消息通知发送失败! msg: {}", response.getErrMsg()); + } + } + catch (Exception e) { + log.error("消息通知时发生异常", e); + } + }); + messages.clear(); + } + interval.restart(); + } + } + + public ExceptionMessage toMessage(Throwable t) { + return new ExceptionMessage().setNumber(1) + .setMac(mac) + .setApplicationName(applicationName) + .setHostname(hostname) + .setIp(ip) + .setRequestUri(requestUri) + .setStack(ExceptionUtil.stacktraceToString(t, config.getLength()).replace("\\r", "")) + .setTime(DateUtil.now()); + } + + /** + * 发送通知 + * @param sendMessage 发送的消息 + * @return 返回消息发送状态,如果发送失败需要设置失败信息 + */ + public abstract ExceptionNoticeResponse send(ExceptionMessage sendMessage); + + @Override + public void handle(Throwable throwable) { + try { + this.requestUri = WebUtils.getRequest().getRequestURI(); + // 是否忽略该异常 + boolean ignore = false; + + // 只有不是忽略的异常类才会插入异常消息队列 + if (Boolean.FALSE.equals(config.getIgnoreChild())) { + // 不忽略子类 + ignore = config.getIgnoreExceptions().contains(throwable.getClass()); + } + else { + // 忽略子类 + for (Class ignoreException : config.getIgnoreExceptions()) { + // 属于子类 + if (ignoreException.isAssignableFrom(throwable.getClass())) { + ignore = true; + break; + } + } + } + + // 不忽略则插入队列 + if (!ignore) { + queue.put(throwable); + } + } + catch (InterruptedException e) { + interrupt(); + } + catch (Exception e) { + log.error("往异常消息队列插入新异常时出错", e); + } + } + + @Override + public void afterPropertiesSet() { + this.setName("exception-notice"); + this.start(); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/DefaultGlobalExceptionHandler.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/DefaultGlobalExceptionHandler.java new file mode 100644 index 0000000..1e65f8b --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/DefaultGlobalExceptionHandler.java @@ -0,0 +1,23 @@ +package com.baiye.web.exception.handler; + +import com.baiye.exception.handler.GlobalExceptionHandler; + +/** + * 默认的异常日志处理类 + * + * @author Hccake + * @version 1.0 + * @date 2019/10/18 17:06 + */ +public class DefaultGlobalExceptionHandler implements GlobalExceptionHandler { + + /** + * 在此处理日志 默认什么都不处理 + * @param throwable 异常信息 + */ + @Override + public void handle(Throwable throwable) { + // do nothing + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/DingTalkGlobalExceptionHandler.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/DingTalkGlobalExceptionHandler.java new file mode 100644 index 0000000..7e3baaf --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/DingTalkGlobalExceptionHandler.java @@ -0,0 +1,34 @@ +// package com.baiye.web.exception.handler; +// +// import com.baiye.web.exception.ExceptionHandleProperties; +// import com.baiye.web.exception.domain.ExceptionMessage; +// import com.baiye.web.exception.domain.ExceptionNoticeResponse; +// import lombok.extern.slf4j.Slf4j; +// +/// ** +// * 钉钉消息通知 +// * +// * @author lingting 2020/6/12 0:25 +// */ +// @Slf4j +// public class DingTalkGlobalExceptionHandler extends +// AbstractNoticeGlobalExceptionHandler { +// +// private final DingTalkSender sender; +// +// public DingTalkGlobalExceptionHandler(ExceptionHandleProperties config, DingTalkSender +// sender, +// String applicationName) { +// super(config, applicationName); +// this.sender = sender; +// } +// +// @Override +// public ExceptionNoticeResponse send(ExceptionMessage sendMessage) { +// DingTalkResponse response = sender +// .sendMessage(new DingTalkTextMessage().setContent(sendMessage.toString()).atAll()); +// return new +// ExceptionNoticeResponse().setErrMsg(response.getResponse()).setSuccess(response.isSuccess()); +// } +// +// } diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/MailGlobalExceptionHandler.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/MailGlobalExceptionHandler.java new file mode 100644 index 0000000..caf8cc1 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/MailGlobalExceptionHandler.java @@ -0,0 +1,35 @@ +// package com.baiye.web.exception.handler; +// +// import com.baiye.web.exception.ExceptionHandleProperties; +// import com.baiye.web.exception.domain.ExceptionNoticeResponse; +// import com.baiye.web.exception.domain.ExceptionMessage; +// import com.hccake.ballcat.common.mail.model.MailSendInfo; +// import com.hccake.ballcat.common.mail.sender.MailSender; +// import lombok.extern.slf4j.Slf4j; +// +/// ** +// * 异常邮件通知 +// * +// * @author lingting 2020/6/12 0:25 +// */ +// @Slf4j +// public class MailGlobalExceptionHandler extends AbstractNoticeGlobalExceptionHandler { +// +// private final MailSender sender; +// +// public MailGlobalExceptionHandler(ExceptionHandleProperties config, MailSender sender, +// String applicationName) { +// super(config, applicationName); +// this.sender = sender; +// } +// +// @Override +// public ExceptionNoticeResponse send(ExceptionMessage sendMessage) { +// String[] to = config.getReceiveEmails().toArray(new String[0]); +// MailSendInfo mailSendInfo = sender.sendTextMail("异常警告", sendMessage.toString(), to); +// // 邮箱发送失败会抛出异常,否则视作发送成功 +// return new ExceptionNoticeResponse().setSuccess(mailSendInfo.getSuccess()) +// .setErrMsg(mailSendInfo.getErrorMsg()); +// } +// +// } diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/MultiGlobalExceptionHandler.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/MultiGlobalExceptionHandler.java new file mode 100644 index 0000000..0f94731 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/handler/MultiGlobalExceptionHandler.java @@ -0,0 +1,88 @@ +// package com.baiye.web.exception.handler; +// +// import com.baiye.web.exception.ExceptionHandleProperties; +// import com.baiye.web.exception.domain.ExceptionNoticeResponse; +// import com.baiye.web.exception.domain.ExceptionMessage; +// import com.hccake.ballcat.common.mail.sender.MailSender; +// import com.hccake.extend.dingtalk.DingTalkSender; +// import com.hccake.extend.dingtalk.message.DingTalkTextMessage; +// import lombok.extern.slf4j.Slf4j; +// import org.springframework.util.CollectionUtils; +// +// import java.util.ArrayList; +// import java.util.Arrays; +// import java.util.List; +// +/// ** +// * 多渠道异常处理类 +// * +// * @author lingting 2022/10/28 10:12 +// */ +// @Slf4j +// public class MultiGlobalExceptionHandler extends AbstractNoticeGlobalExceptionHandler { +// +// private final Object mailSender; +// +// private final String[] receiveEmails; +// +// private final List dingTalkSenders; +// +// public MultiGlobalExceptionHandler(ExceptionHandleProperties config, String +// applicationName, Object mailSender) { +// super(config, applicationName); +// this.mailSender = mailSender; +// if (CollectionUtils.isEmpty(config.getReceiveEmails())) { +// this.receiveEmails = new String[0]; +// } +// else { +// this.receiveEmails = config.getReceiveEmails().toArray(new String[0]); +// } +// +// ExceptionHandleProperties.DingTalkProperties dingTalkProperties = config.getDingTalk(); +// if (dingTalkProperties == null || +// CollectionUtils.isEmpty(dingTalkProperties.getSenders())) { +// this.dingTalkSenders = new ArrayList<>(0); +// } +// else { +// this.dingTalkSenders = new ArrayList<>(dingTalkProperties.getSenders().size()); +// for (ExceptionHandleProperties.DingTalkProperties.Sender s : +// dingTalkProperties.getSenders()) { +// DingTalkSender sender = new DingTalkSender(s.getUrl()); +// sender.setSecret(s.getSecret()); +// dingTalkSenders.add(sender); +// } +// } +// +// } +// +// @Override +// public ExceptionNoticeResponse send(ExceptionMessage sendMessage) { +// if (receiveEmails != null && receiveEmails.length > 0) { +// try { +// String[] to = config.getReceiveEmails().toArray(new String[0]); +// ((MailSender) mailSender).sendTextMail("异常警告", sendMessage.toString(), to); +// } +// catch (Exception e) { +// log.error("邮箱异常通知发送异常! emails: {}", Arrays.toString(receiveEmails)); +// } +// } +// +// for (Object obj : dingTalkSenders) { +// DingTalkSender sender = (DingTalkSender) obj; +// try { +// DingTalkTextMessage message = new +// DingTalkTextMessage().setContent(sendMessage.toString()); +// if (Boolean.TRUE.equals(config.getDingTalk().getAtAll())) { +// message.atAll(); +// } +// sender.sendMessage(message); +// } +// catch (Exception e) { +// log.error("钉钉异常通知发送异常! webHook: {}", sender.getUrl(), e); +// } +// } +// +// return new ExceptionNoticeResponse().setSuccess(true); +// } +// +// } diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/resolver/GlobalHandlerExceptionResolver.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/resolver/GlobalHandlerExceptionResolver.java new file mode 100644 index 0000000..e5bb5a1 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/resolver/GlobalHandlerExceptionResolver.java @@ -0,0 +1,168 @@ +// package com.baiye.web.exception.resolver; +// +// import com.baiye.constant.GlobalConstants; +// import com.baiye.exception.BusinessException; +// import com.baiye.exception.handler.GlobalExceptionHandler; +// import com.baiye.result.R; +// import com.baiye.result.SystemResultCode; +// import lombok.RequiredArgsConstructor; +// import lombok.extern.slf4j.Slf4j; +// import org.springframework.beans.factory.annotation.Value; +// import org.springframework.core.annotation.Order; +// import org.springframework.http.HttpStatus; +// import org.springframework.validation.BindException; +// import org.springframework.validation.BindingResult; +// import org.springframework.web.HttpMediaTypeNotSupportedException; +// import org.springframework.web.HttpRequestMethodNotSupportedException; +// import org.springframework.web.bind.annotation.ExceptionHandler; +// import org.springframework.web.bind.annotation.ResponseStatus; +// import org.springframework.web.bind.annotation.RestControllerAdvice; +// import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +// +// import javax.servlet.http.HttpServletRequest; +// import javax.validation.ValidationException; +// +/// ** +// * 全局异常处理 +// * +// * @author Hccake +// */ +// @Order +// @Slf4j +// @RestControllerAdvice +// @RequiredArgsConstructor +// public class GlobalHandlerExceptionResolver { +// +// private final GlobalExceptionHandler globalExceptionHandler; +// +// @Value("${spring.profiles.active:prod}") +// private String profile; +// +// public static final String PROD_ERR_MSG = "系统异常,请联系管理员"; +// +// public static final String NLP_MSG = "空指针异常!"; +// +// /** +// * 全局异常捕获 +// * @param e the e +// * @return R +// */ +// @ExceptionHandler(Exception.class) +// @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) +// public R handleGlobalException(Exception e, HttpServletRequest request) { +// log.error("请求地址: {}, 全局异常信息 ex={}", request.getRequestURI(), e.getMessage(), e); +// globalExceptionHandler.handle(e); +// // 当为生产环境, 不适合把具体的异常信息展示给用户, 比如数据库异常信息. +// String errorMessage = GlobalConstants.ENV_PROD.equals(profile) ? PROD_ERR_MSG : +// e.getLocalizedMessage(); +// return R.failed(SystemResultCode.SERVER_ERROR, errorMessage); +// } +// +// /** +// * 空指针异常捕获 +// * @param e the e +// * @return R +// */ +// @ExceptionHandler(NullPointerException.class) +// @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) +// public R handleNullPointerException(NullPointerException e, HttpServletRequest +// request) { +// log.error("请求地址: {}, 空指针异常 ex={}", request.getRequestURI(), e.getMessage(), e); +// globalExceptionHandler.handle(e); +// // 当为生产环境, 不适合把具体的异常信息展示给用户, 比如数据库异常信息. +// String errorMessage = GlobalConstants.ENV_PROD.equals(profile) ? PROD_ERR_MSG : +// NLP_MSG; +// return R.failed(SystemResultCode.SERVER_ERROR, errorMessage); +// } +// +// /** +// * MethodArgumentTypeMismatchException 参数类型转换异常 +// * @param e the e +// * @return R +// */ +// @ResponseStatus(HttpStatus.BAD_REQUEST) +// @ExceptionHandler(MethodArgumentTypeMismatchException.class) +// public R handleMethodArgumentTypeMismatchException(Exception e, +// HttpServletRequest request) { +// log.error("请求地址: {}, 请求入参异常 ex={}", request.getRequestURI(), e.getMessage(), e); +// globalExceptionHandler.handle(e); +// String errorMessage = GlobalConstants.ENV_PROD.equals(profile) ? PROD_ERR_MSG : +// e.getMessage(); +// return R.failed(SystemResultCode.BAD_REQUEST, errorMessage); +// } +// +// /** +// * 请求方式有问题 - MediaType 异常 - Method 异常 +// * @return R +// */ +// @ExceptionHandler({ HttpMediaTypeNotSupportedException.class, +// HttpRequestMethodNotSupportedException.class }) +// public R requestNotSupportedException(Exception e, HttpServletRequest request) +// { +// log.error("请求地址: {}, 请求方式异常 ex={}", request.getRequestURI(), e.getMessage(), e); +// globalExceptionHandler.handle(e); +// return R.failed(SystemResultCode.BAD_REQUEST, e.getLocalizedMessage()); +// } +// +// /** +// * IllegalArgumentException 异常捕获,主要用于Assert +// * @param e the e +// * @return R +// */ +// @ExceptionHandler(IllegalArgumentException.class) +// @ResponseStatus(HttpStatus.BAD_REQUEST) +// public R handleIllegalArgumentException(IllegalArgumentException e, +// HttpServletRequest request) { +// log.error("请求地址: {}, 非法数据输入 ex={}", request.getRequestURI(), e.getMessage(), e); +// globalExceptionHandler.handle(e); +// return R.failed(SystemResultCode.BAD_REQUEST, e.getMessage()); +// } +// +// /** +// * validation Exception +// * @param e the e +// * @return R +// */ +// @ExceptionHandler(BindException.class) +// @ResponseStatus(HttpStatus.BAD_REQUEST) +// public R handleBodyValidException(BindException e, HttpServletRequest request) +// { +// BindingResult bindingResult = e.getBindingResult(); +// String errorMsg = bindingResult.getErrorCount() > 0 ? +// bindingResult.getAllErrors().get(0).getDefaultMessage() +// : "未获取到错误信息!"; +// +// log.error("请求地址: {}, 参数绑定异常 ex={}", request.getRequestURI(), errorMsg); +// globalExceptionHandler.handle(e); +// return R.failed(SystemResultCode.BAD_REQUEST, errorMsg); +// } +// +// /** +// * 单体参数校验异常 validation Exception +// * @param e the e +// * @return R +// */ +// @ExceptionHandler(ValidationException.class) +// @ResponseStatus(HttpStatus.BAD_REQUEST) +// public R handleValidationException(ValidationException e, HttpServletRequest +// request) { +// log.error("请求地址: {}, 参数校验异常 ex={}", request.getRequestURI(), e.getMessage()); +// globalExceptionHandler.handle(e); +// return R.failed(SystemResultCode.BAD_REQUEST, e.getLocalizedMessage()); +// } +// +// /** +// * 自定义业务异常捕获 业务异常响应码推荐使用200 用 result 结构中的code做为业务错误码标识 +// * @param e the e +// * @return R +// */ +// @ExceptionHandler(BusinessException.class) +// @ResponseStatus(HttpStatus.OK) +// public R handleBallCatException(BusinessException e, HttpServletRequest +// request) { +// log.error("请求地址: {}, 业务异常信息 ex={}", request.getRequestURI(), e.getMessage()); +// globalExceptionHandler.handle(e); +// return R.failed(e.getCode(), e.getMessage()); +// } +// +// } diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/resolver/SecurityHandlerExceptionResolver.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/resolver/SecurityHandlerExceptionResolver.java new file mode 100644 index 0000000..a6338cd --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/exception/resolver/SecurityHandlerExceptionResolver.java @@ -0,0 +1,42 @@ +// package com.baiye.web.exception.resolver; +// +// import com.baiye.exception.handler.GlobalExceptionHandler; +// import com.baiye.result.R; +// import com.baiye.result.SystemResultCode; +// import lombok.RequiredArgsConstructor; +// import lombok.extern.slf4j.Slf4j; +// import org.springframework.core.annotation.Order; +// import org.springframework.http.HttpStatus; +// import org.springframework.security.access.AccessDeniedException; +// import org.springframework.security.core.SpringSecurityMessageSource; +// import org.springframework.web.bind.annotation.ExceptionHandler; +// import org.springframework.web.bind.annotation.ResponseStatus; +// import org.springframework.web.bind.annotation.RestControllerAdvice; +// +/// ** +// * @author hccake +// */ +// @Order(-1000) +// @Slf4j +// @RequiredArgsConstructor +// @RestControllerAdvice +// public class SecurityHandlerExceptionResolver { +// +// private final GlobalExceptionHandler globalExceptionHandler; +// +// /** +// * AccessDeniedException +// * @param e the e +// * @return R +// */ +// @ExceptionHandler(AccessDeniedException.class) +// @ResponseStatus(HttpStatus.FORBIDDEN) +// public R handleAccessDeniedException(AccessDeniedException e) { +// String msg = SpringSecurityMessageSource.getAccessor() +// .getMessage("AbstractAccessDecisionManager.accessDenied", e.getMessage()); +// log.error("拒绝授权异常信息 ex={}", msg); +// globalExceptionHandler.handle(e); +// return R.failed(SystemResultCode.FORBIDDEN, e.getLocalizedMessage()); +// } +// +// } diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/jackson/CustomJacksonAutoConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/jackson/CustomJacksonAutoConfiguration.java new file mode 100644 index 0000000..f82eac2 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/jackson/CustomJacksonAutoConfiguration.java @@ -0,0 +1,88 @@ +package com.baiye.web.jackson; + +import com.baiye.jackson.CustomJavaTimeModule; +import com.baiye.jackson.NullSerializerProvider; +import com.baiye.json.DesensitizeStrategy; +import com.baiye.json.JsonDesensitizeModule; +import com.baiye.json.JsonDesensitizeSerializerModifier; +import com.baiye.util.json.JacksonJsonToolAdapter; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/17 22:14 + */ +@AutoConfiguration(before = JacksonAutoConfiguration.class) +public class CustomJacksonAutoConfiguration { + + /** + * 自定义objectMapper + * @return ObjectMapper + */ + @Bean + @ConditionalOnClass(ObjectMapper.class) + @ConditionalOnMissingBean(ObjectMapper.class) + public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { + // org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.JacksonObjectMapperConfiguration + ObjectMapper objectMapper = builder.createXmlMapper(false).build(); + + // 对于空对象的序列化不抛异常 + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + // 序列化时忽略未知属性 + objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + // NULL值修改 + objectMapper.setSerializerProvider(new NullSerializerProvider()); + // 有特殊需要转义字符, 不报错 + objectMapper.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()); + // 更新 JsonUtils 中的 ObjectMapper,保持容器和工具类中的 ObjectMapper 对象一致 + JacksonJsonToolAdapter.setMapper(objectMapper); + + return objectMapper; + } + + /** + * 注册自定义 的 jackson 时间格式,高优先级,用于覆盖默认的时间格式 + * @return CustomJavaTimeModule + */ + @Bean + @ConditionalOnMissingBean(CustomJavaTimeModule.class) + public CustomJavaTimeModule customJavaTimeModule() { + return new CustomJavaTimeModule(); + } + + /** + * 注册 Jackson 的脱敏模块 + * @return Jackson2ObjectMapperBuilderCustomizer + */ + @Bean + @ConditionalOnMissingBean({ JsonDesensitizeModule.class, DesensitizeStrategy.class }) + public JsonDesensitizeModule jsonDesensitizeModule() { + JsonDesensitizeSerializerModifier desensitizeModifier = new JsonDesensitizeSerializerModifier(); + return new JsonDesensitizeModule(desensitizeModifier); + } + + /** + * 注册 Jackson 的脱敏模块 + * @return Jackson2ObjectMapperBuilderCustomizer + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnBean(DesensitizeStrategy.class) + public JsonDesensitizeModule jsonDesensitizeModule(DesensitizeStrategy desensitizeStrategy) { + JsonDesensitizeSerializerModifier desensitizeModifier = new JsonDesensitizeSerializerModifier( + desensitizeStrategy); + return new JsonDesensitizeModule(desensitizeModifier); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/pageable/DefaultPageParamArgumentResolver.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/pageable/DefaultPageParamArgumentResolver.java new file mode 100644 index 0000000..950c53e --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/pageable/DefaultPageParamArgumentResolver.java @@ -0,0 +1,67 @@ +// package com.baiye.web.pageable; +// +// import com.baiye.domain.PageParam; +// import lombok.extern.slf4j.Slf4j; +// import org.springframework.core.MethodParameter; +// import org.springframework.web.bind.support.WebDataBinderFactory; +// import org.springframework.web.context.request.NativeWebRequest; +// import org.springframework.web.method.support.ModelAndViewContainer; +// +// import javax.servlet.http.HttpServletRequest; +// +/// ** +// * @author lengleng +// * @author hccake +// * @date 2019-06-24 +// *

          +// * 解决Mybatis Plus Order By SQL注入问题 +// */ +// @Slf4j +// public class DefaultPageParamArgumentResolver extends PageParamArgumentResolverSupport +// implements PageParamArgumentResolver { +// +// public DefaultPageParamArgumentResolver(PageableProperties pageableProperties) { +// setMaxPageSize(pageableProperties.getMaxPageSize()); +// setPageParameterName(pageableProperties.getPageParameterName()); +// setSizeParameterName(pageableProperties.getSizeParameterName()); +// setSortParameterName(pageableProperties.getSortParameterName()); +// } +// +// /** +// * 判断Controller是否包含page 参数 +// * @param parameter 参数 +// * @return 是否过滤 +// */ +// @Override +// public boolean supportsParameter(MethodParameter parameter) { +// return PageParam.class.isAssignableFrom(parameter.getParameterType()); +// } +// +// /** +// * @param parameter 入参集合 +// * @param mavContainer model 和 view +// * @param webRequest web相关 +// * @param binderFactory 入参解析 +// * @return 检查后新的page对象 +// * @throws Exception ex +// *

          +// * page 只支持查询 GET .如需解析POST获取请求报文体处理 +// */ +// @Override +// public PageParam resolveArgument(MethodParameter parameter, ModelAndViewContainer +// mavContainer, +// NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { +// +// HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); +// if (request == null) { +// return new PageParam(); +// } +// +// PageParam pageParam = getPageParam(parameter, request); +// +// paramValidate(parameter, mavContainer, webRequest, binderFactory, pageParam); +// +// return pageParam; +// } +// +// } diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/pageable/PageParamArgumentResolver.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/pageable/PageParamArgumentResolver.java new file mode 100644 index 0000000..0a4d2db --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/pageable/PageParamArgumentResolver.java @@ -0,0 +1,39 @@ +// package com.baiye.web.pageable; +// +// import com.hccake.ballcat.common.model.domain.PageParam; +// import org.springframework.core.MethodParameter; +// import org.springframework.lang.NonNull; +// import org.springframework.lang.Nullable; +// import org.springframework.web.bind.WebDataBinder; +// import org.springframework.web.bind.support.WebDataBinderFactory; +// import org.springframework.web.context.request.NativeWebRequest; +// import org.springframework.web.method.support.HandlerMethodArgumentResolver; +// import org.springframework.web.method.support.ModelAndViewContainer; +// +/// ** +// * 分页参数解析器 +// * +// * @author hccake +// */ +// public interface PageParamArgumentResolver extends HandlerMethodArgumentResolver { +// +// /** +// * Resolves a {@link PageParam} method parameter into an argument value from a given +// * request. +// * @param parameter the method parameter to resolve. This parameter must have +// * previously been passed to {@link #supportsParameter} which must have returned +// * {@code true}. +// * @param mavContainer the ModelAndViewContainer for the current request +// * @param webRequest the current request +// * @param binderFactory a factory for creating {@link WebDataBinder} instances +// * @return the resolved argument value. +// * @throws Exception ex +// */ +// @NonNull +// @Override +// PageParam resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer +// mavContainer, +// NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws +// Exception; +// +// } diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/pageable/PageParamArgumentResolverSupport.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/pageable/PageParamArgumentResolverSupport.java new file mode 100644 index 0000000..dc0722b --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/pageable/PageParamArgumentResolverSupport.java @@ -0,0 +1,244 @@ +// package com.baiye.web.pageable; +// +// import cn.hutool.core.text.CharSequenceUtil; +// import cn.hutool.core.text.StrPool; +// import cn.hutool.core.util.ArrayUtil; +// import com.hccake.ballcat.common.model.domain.PageParam; +// import lombok.Getter; +// import lombok.Setter; +// import lombok.extern.slf4j.Slf4j; +// import org.springframework.core.MethodParameter; +// import org.springframework.validation.BindingResult; +// import org.springframework.validation.Errors; +// import org.springframework.validation.ObjectError; +// import org.springframework.validation.annotation.ValidationAnnotationUtils; +// import org.springframework.web.bind.MethodArgumentNotValidException; +// import org.springframework.web.bind.WebDataBinder; +// import org.springframework.web.bind.support.WebDataBinderFactory; +// import org.springframework.web.context.request.NativeWebRequest; +// import org.springframework.web.method.support.ModelAndViewContainer; +// +// import javax.servlet.http.HttpServletRequest; +// import java.lang.annotation.Annotation; +// import java.util.ArrayList; +// import java.util.List; +// import java.util.Map; +// +// import static com.hccake.ballcat.common.model.domain.PageableConstants.*; +// +/// ** +// * @author hccake +// */ +// @Slf4j +// public abstract class PageParamArgumentResolverSupport { +// +// @Setter +// @Getter +// private String pageParameterName = DEFAULT_PAGE_PARAMETER; +// +// @Setter +// @Getter +// private String sizeParameterName = DEFAULT_SIZE_PARAMETER; +// +// @Setter +// @Getter +// private String sortParameterName = DEFAULT_SORT_PARAMETER; +// +// @Setter +// @Getter +// private int maxPageSize = DEFAULT_MAX_PAGE_SIZE; +// +// protected PageParam getPageParam(MethodParameter parameter, HttpServletRequest request) +// { +// String pageParameterValue = request.getParameter(pageParameterName); +// String sizeParameterValue = request.getParameter(sizeParameterName); +// +// PageParam pageParam; +// try { +// pageParam = (PageParam) parameter.getParameterType().newInstance(); +// } +// catch (InstantiationException | IllegalAccessException e) { +// pageParam = new PageParam(); +// } +// +// long pageValue = parseValueFormString(pageParameterValue, 1); +// pageParam.setPage(pageValue); +// pageParam.setCurrent(pageValue); +// +// long sizeValue = parseValueFormString(sizeParameterValue, 10); +// pageParam.setSize(sizeValue); +// +// // ========== 排序处理 =========== +// Map parameterMap = request.getParameterMap(); +// // sort 可以传多个,所以同时支持 sort 和 sort[] +// String[] sort = parameterMap.get(sortParameterName); +// if (ArrayUtil.isEmpty(sort)) { +// sort = parameterMap.get(sortParameterName + "[]"); +// } +// +// List sorts; +// if (ArrayUtil.isNotEmpty(sort)) { +// sorts = getSortList(sort); +// } +// else { +// String sortFields = request.getParameter(SORT_FIELDS); +// String sortOrders = request.getParameter(SORT_ORDERS); +// sorts = getSortList(sortFields, sortOrders); +// } +// pageParam.setSorts(sorts); +// +// return pageParam; +// } +// +// private long parseValueFormString(String currentParameterValue, long defaultValue) { +// try { +// return Long.parseLong(currentParameterValue); +// } +// catch (NumberFormatException e) { +// return defaultValue; +// } +// } +// +// /** +// * 封装排序规则 +// * @param sort 排序规则字符串 +// * @return List +// */ +// protected List getSortList(String[] sort) { +// List sorts = new ArrayList<>(); +// +// // 将排序规则转换为 Sort 对象 +// for (String sortRule : sort) { +// if (sortRule == null) { +// continue; +// } +// +// // 切割后最多两位,第一位 field 第二位 order +// String[] sortRuleArr = sortRule.split(","); +// +// // 字段 +// String field = sortRuleArr[0]; +// +// // 排序规则,默认正序 +// String order; +// if (sortRuleArr.length < 2) { +// order = ASC; +// } +// else { +// order = sortRuleArr[1]; +// } +// +// fillValidSort(field, order, sorts); +// } +// +// return sorts; +// } +// +// /** +// * 封装排序规则 +// * @param sortFields 排序字段,使用英文逗号分割 +// * @param sortOrders 排序规则,使用英文逗号分割,与排序字段一一对应 +// * @return List +// */ +// @Deprecated +// protected List getSortList(String sortFields, String sortOrders) { +// List sorts = new ArrayList<>(); +// +// // 字段和规则都不能为空 +// if (CharSequenceUtil.isBlank(sortFields) || CharSequenceUtil.isBlank(sortOrders)) { +// return sorts; +// } +// +// // 字段和规则不一一对应则不处理 +// String[] fieldArr = sortFields.split(StrPool.COMMA); +// String[] orderArr = sortOrders.split(StrPool.COMMA); +// if (fieldArr.length != orderArr.length) { +// return sorts; +// } +// +// for (int i = 0; i < fieldArr.length; i++) { +// String field = fieldArr[i]; +// String order = orderArr[i]; +// fillValidSort(field, order, sorts); +// } +// +// return sorts; +// } +// +// /** +// * 校验并填充有效的 sort 对象到指定集合忠 +// * @param field 排序列 +// * @param order 排序顺序 +// * @param sorts sorts 集合 +// */ +// protected void fillValidSort(String field, String order, List sorts) { +// if (validFieldName(field)) { +// PageParam.Sort sort = new PageParam.Sort(); +// // 驼峰转下划线 +// sort.setAsc(ASC.equalsIgnoreCase(order)); +// // 正序/倒序 +// sort.setField(CharSequenceUtil.toUnderlineCase(field)); +// sorts.add(sort); +// } +// } +// +// /** +// * 判断排序字段名是否非法 字段名只允许数字字母下划线,且不能是 sql 关键字 +// * @param filedName 字段名 +// * @return 是否非法 +// */ +// protected boolean validFieldName(String filedName) { +// boolean isValid = CharSequenceUtil.isNotBlank(filedName) && +// filedName.matches(SORT_FILED_REGEX) +// && !SQL_KEYWORDS.contains(filedName); +// if (!isValid) { +// log.warn("异常的分页查询排序字段:{}", filedName); +// } +// return isValid; +// } +// +// protected void paramValidate(MethodParameter parameter, ModelAndViewContainer +// mavContainer, +// NativeWebRequest webRequest, WebDataBinderFactory binderFactory, PageParam pageParam) +// throws Exception { +// // 数据校验处理 +// if (binderFactory != null) { +// WebDataBinder binder = binderFactory.createBinder(webRequest, pageParam, "pageParam"); +// validateIfApplicable(binder, parameter); +// BindingResult bindingResult = binder.getBindingResult(); +// +// long size = pageParam.getSize(); +// if (size > maxPageSize) { +// bindingResult.addError(new ObjectError("size", "分页条数不能大于" + maxPageSize)); +// } +// +// if (bindingResult.hasErrors() && isBindExceptionRequired(binder, parameter)) { +// throw new MethodArgumentNotValidException(parameter, bindingResult); +// } +// if (mavContainer != null) { +// mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + "pageParam", bindingResult); +// } +// } +// } +// +// protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { +// Annotation[] annotations = parameter.getParameterAnnotations(); +// for (Annotation ann : annotations) { +// Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); +// if (validationHints != null) { +// binder.validate(validationHints); +// break; +// } +// } +// } +// +// protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter +// parameter) { +// int i = parameter.getParameterIndex(); +// Class[] paramTypes = parameter.getExecutable().getParameterTypes(); +// boolean hasBindingResult = (paramTypes.length > (i + 1) && +// Errors.class.isAssignableFrom(paramTypes[i + 1])); +// return !hasBindingResult; +// } +// +// } diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/pageable/PageableProperties.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/pageable/PageableProperties.java new file mode 100644 index 0000000..3c95c29 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/pageable/PageableProperties.java @@ -0,0 +1,35 @@ +// package com.baiye.web.pageable; +// +// import lombok.Data; +// import org.springframework.boot.context.properties.ConfigurationProperties; +// +// import static com.hccake.ballcat.common.model.domain.PageableConstants.*; +// +/// ** +// * @author hccake +// */ +// @Data +// @ConfigurationProperties("ballcat.pageable") +// public class PageableProperties { +// +// /** +// * 当前页的参数名 +// */ +// private String pageParameterName = DEFAULT_PAGE_PARAMETER; +// +// /** +// * 每页数据量的参数名 +// */ +// private String sizeParameterName = DEFAULT_SIZE_PARAMETER; +// +// /** +// * 排序规则的参数名 +// */ +// private String sortParameterName = DEFAULT_SORT_PARAMETER; +// +// /** +// * 分页查询的每页最大数据量 +// */ +// private int maxPageSize = DEFAULT_MAX_PAGE_SIZE; +// +// } diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/servlet/WebMvcAutoConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/servlet/WebMvcAutoConfiguration.java new file mode 100644 index 0000000..6e4cc6c --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/servlet/WebMvcAutoConfiguration.java @@ -0,0 +1,78 @@ +// package com.baiye.web.servlet; +// +// import cn.hutool.core.util.IdUtil; +// import com.baiye.web.pageable.DefaultPageParamArgumentResolver; +// import com.baiye.web.trace.TraceIdFilter; +// import com.baiye.web.trace.TraceIdGenerator; +// import com.baiye.web.pageable.PageParamArgumentResolver; +// import com.baiye.web.pageable.PageableProperties; +// import lombok.RequiredArgsConstructor; +// import org.springframework.boot.autoconfigure.AutoConfiguration; +// import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +// import org.springframework.boot.context.properties.EnableConfigurationProperties; +// import org.springframework.boot.web.servlet.FilterRegistrationBean; +// import org.springframework.context.annotation.Bean; +// import org.springframework.context.annotation.Configuration; +// import org.springframework.core.Ordered; +// import org.springframework.web.method.support.HandlerMethodArgumentResolver; +// import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +// +// import java.util.List; +// +/// ** +// * @author Hccake 2019/10/19 17:10 +// */ +// @AutoConfiguration +// @RequiredArgsConstructor +// @EnableConfigurationProperties({ WebProperties.class, PageableProperties.class }) +// public class WebMvcAutoConfiguration { +// +// @Configuration(proxyBeanMethods = false) +// static class TraceIdConfiguration { +// +// @Bean +// @ConditionalOnMissingBean(TraceIdGenerator.class) +// public TraceIdGenerator traceIdGenerator() { +// return IdUtil::objectId; +// } +// +// @Bean +// public FilterRegistrationBean +// traceIdFilterRegistrationBean(WebProperties webProperties, +// TraceIdGenerator traceIdGenerator) { +// String traceIdHeaderName = webProperties.getTraceIdHeaderName(); +// TraceIdFilter traceIdFilter = new TraceIdFilter(traceIdHeaderName, traceIdGenerator); +// FilterRegistrationBean registrationBean = new +// FilterRegistrationBean<>(traceIdFilter); +// registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); +// return registrationBean; +// } +// +// } +// +// @Bean +// @ConditionalOnMissingBean +// public PageParamArgumentResolver pageParamArgumentResolver(PageableProperties +// pageableProperties) { +// return new DefaultPageParamArgumentResolver(pageableProperties); +// } +// +// @RequiredArgsConstructor +// @Configuration(proxyBeanMethods = false) +// static class CustomWebMvcConfigurer implements WebMvcConfigurer { +// +// private final PageParamArgumentResolver pageParamArgumentResolver; +// +// /** +// * Page Sql注入过滤 +// * @param argumentResolvers 方法参数解析器集合 +// */ +// @Override +// public void addArgumentResolvers(List argumentResolvers) +// { +// argumentResolvers.add(pageParamArgumentResolver); +// } +// +// } +// +// } diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/servlet/WebProperties.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/servlet/WebProperties.java new file mode 100644 index 0000000..a119d47 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/servlet/WebProperties.java @@ -0,0 +1,18 @@ +// package com.baiye.web.servlet; +// +// import lombok.Data; +// import org.springframework.boot.context.properties.ConfigurationProperties; +// +/// ** +// * @author hccake +// */ +// @Data +// @ConfigurationProperties(prefix = "ballcat.web") +// public class WebProperties { +// +// /** +// * traceId 的 http 头名称 +// */ +// private String traceIdHeaderName = "X-Trace-Id"; +// +// } diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/trace/TraceIdFilter.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/trace/TraceIdFilter.java new file mode 100644 index 0000000..4711b9b --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/trace/TraceIdFilter.java @@ -0,0 +1,48 @@ +package com.baiye.web.trace; + +import com.baiye.constant.MDCConstants; +import lombok.RequiredArgsConstructor; +import org.slf4j.MDC; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * TraceId 过滤器 + *

          + * 利用 Slf4J 的 MDC 功能,将 traceId 放入 MDC 中,方便日志打印 + * + * @author Hccake 2020/5/25 17:35 + */ +@RequiredArgsConstructor +public class TraceIdFilter extends OncePerRequestFilter { + + private final String traceIdHeaderName; + + private final TraceIdGenerator traceIdGenerator; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + // 先获取请求头中的 traceId,如果没有,则生成一个 + String traceId = request.getHeader(traceIdHeaderName); + if (traceId == null || traceId.isEmpty()) { + traceId = traceIdGenerator.generate(); + } + + MDC.put(MDCConstants.TRACE_ID_KEY, traceId); + try { + // 响应头中添加 traceId 参数,方便排查问题 + response.setHeader(traceIdHeaderName, traceId); + filterChain.doFilter(request, response); + } + finally { + MDC.remove(MDCConstants.TRACE_ID_KEY); + } + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/trace/TraceIdGenerator.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/trace/TraceIdGenerator.java new file mode 100644 index 0000000..7eaeb92 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/trace/TraceIdGenerator.java @@ -0,0 +1,20 @@ +package com.baiye.web.trace; + +import javax.servlet.http.HttpServletRequest; + +/** + * TraceId 转换器 + * + * @author hccake + * @since 1.3.1 + */ +@FunctionalInterface +public interface TraceIdGenerator { + + /** + * 生成 traceId + * @return traceId + */ + String generate(); + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/validation/BallcatValidationAutoConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/validation/BallcatValidationAutoConfiguration.java new file mode 100644 index 0000000..cd36464 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/java/com/baiye/web/validation/BallcatValidationAutoConfiguration.java @@ -0,0 +1,45 @@ +package com.baiye.web.validation; + +import com.baiye.validation.EmptyCurlyToDefaultMessageInterpolator; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Role; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +import javax.validation.MessageInterpolator; +import javax.validation.Validator; +import javax.validation.executable.ExecutableValidator; + +/** + * Validation 自动配置类,扩展支持使用 {} 占位替换默认消息 + * + * @author hccake + */ +@AutoConfiguration(before = ValidationAutoConfiguration.class) +@ConditionalOnClass(ExecutableValidator.class) +@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider") +public class BallcatValidationAutoConfiguration { + + @Bean + @ConditionalOnMissingBean({ Validator.class, MessageInterpolator.class }) + public EmptyCurlyToDefaultMessageInterpolator messageInterpolator() { + return new EmptyCurlyToDefaultMessageInterpolator(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @ConditionalOnMissingBean(Validator.class) + @ConditionalOnBean(MessageInterpolator.class) + public static LocalValidatorFactoryBean defaultValidator(MessageInterpolator messageInterpolator) { + LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean(); + factoryBean.setMessageInterpolator(messageInterpolator); + return factoryBean; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/resources/META-INF/spring.factories b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..6ba11d5 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/resources/META-INF/spring.factories @@ -0,0 +1,8 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.baiye.web.actuate.ActuatorSecurityAutoConfiguration,\ + com.baiye.web.jackson.CustomJacksonAutoConfiguration,\ + com.baiye.web.validation.BallcatValidationAutoConfiguration + + +org.springframework.context.ApplicationListener=\ + com.baiye.web.event.ApplicationContextInitialized \ No newline at end of file diff --git a/marketing-scrm-starters/marketing-scrm-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..2de0680 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +com.baiye.web.actuate.ActuatorSecurityAutoConfiguration +com.baiye.web.jackson.CustomJacksonAutoConfiguration +com.baiye.web.validation.BallcatValidationAutoConfiguration \ No newline at end of file diff --git a/marketing-scrm-starters/marketing-scrm-starter-websocket/pom.xml b/marketing-scrm-starters/marketing-scrm-starter-websocket/pom.xml new file mode 100644 index 0000000..103dd0b --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-websocket/pom.xml @@ -0,0 +1,51 @@ + + + + marketing-scrm-starters + com.baiye + ${revision} + + 4.0.0 + marketing-scrm-starter-websocket + + + + com.baiye + common-util + ${revision} + + + com.baiye + common-websocket + ${revision} + + + + org.apache.rocketmq + rocketmq-spring-boot-starter + true + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-websocket + + + + org.springframework.data + spring-data-redis + true + + + + + diff --git a/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/MessageDistributorTypeConstants.java b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/MessageDistributorTypeConstants.java new file mode 100644 index 0000000..1228471 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/MessageDistributorTypeConstants.java @@ -0,0 +1,33 @@ +package com.baiye.autoconfigure.websocket; + +/** + * websocket 消息分发器类型常量类 + * + * @author hccake + */ +public final class MessageDistributorTypeConstants { + + private MessageDistributorTypeConstants() { + } + + /** + * 本地 + */ + public static final String LOCAL = "local"; + + /** + * 基于 Redis PUB/SUB + */ + public static final String REDIS = "redis"; + + /** + * 基于 rocketmq 广播 + */ + public static final String ROCKETMQ = "rocketmq"; + + /** + * 自定义 + */ + public static final String CUSTOM = "custom"; + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/SockJsServiceConfigurer.java b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/SockJsServiceConfigurer.java new file mode 100644 index 0000000..466e16e --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/SockJsServiceConfigurer.java @@ -0,0 +1,18 @@ +package com.baiye.autoconfigure.websocket; + +import org.springframework.web.socket.config.annotation.SockJsServiceRegistration; + +/** + * SockJsService 配置类 + * + * @author hccake + */ +public interface SockJsServiceConfigurer { + + /** + * 配置 sockjs 相关 + * @param sockJsServiceRegistration sockJsService 注册类 + */ + void config(SockJsServiceRegistration sockJsServiceRegistration); + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/WebSocketAutoConfiguration.java b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/WebSocketAutoConfiguration.java new file mode 100644 index 0000000..759925e --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/WebSocketAutoConfiguration.java @@ -0,0 +1,95 @@ +package com.baiye.autoconfigure.websocket; + +import com.baiye.autoconfigure.websocket.config.*; +import com.baiye.handler.JsonMessageHandler; +import com.baiye.handler.PingJsonMessageHandler; +import com.baiye.holder.JsonMessageHandlerInitializer; +import com.baiye.message.JsonWebSocketMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +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.Import; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.SockJsServiceRegistration; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistration; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.List; + +/** + * websocket自动配置 + * + * @author Yakir Hccake + */ +@AutoConfiguration +@Import({WebSocketHandlerConfig.class, LocalMessageDistributorConfig.class, RedisMessageDistributorConfig.class, + RocketMqMessageDistributorConfig.class}) +@EnableWebSocket +@RequiredArgsConstructor +@EnableConfigurationProperties(WebSocketProperties.class) +public class WebSocketAutoConfiguration { + + private final WebSocketProperties webSocketProperties; + + + @Bean + @ConditionalOnMissingBean + public WebSocketConfigurer webSocketConfigurer(List handshakeInterceptor, + WebSocketHandler webSocketHandler, + @Autowired(required = false) SockJsServiceConfigurer sockJsServiceConfigurer) { + return registry -> { + WebSocketHandlerRegistration registration = registry + .addHandler(webSocketHandler, webSocketProperties.getPath()) + .addInterceptors(handshakeInterceptor.toArray(new HandshakeInterceptor[0])); + + String[] allowedOrigins = webSocketProperties.getAllowedOrigins(); + if (allowedOrigins != null && allowedOrigins.length > 0) { + registration.setAllowedOrigins(allowedOrigins); + } + + String[] allowedOriginPatterns = webSocketProperties.getAllowedOriginPatterns(); + if (allowedOriginPatterns != null && allowedOriginPatterns.length > 0) { + registration.setAllowedOriginPatterns(allowedOriginPatterns); + } + + if (webSocketProperties.isWithSockjs()) { + SockJsServiceRegistration sockJsServiceRegistration = registration.withSockJS(); + if (sockJsServiceConfigurer != null) { + sockJsServiceConfigurer.config(sockJsServiceRegistration); + } + } + }; + } + + /** + * 心跳处理器 + * + * @return PingJsonMessageHandler + */ + @Bean + @ConditionalOnProperty(prefix = WebSocketProperties.PREFIX, name = "heartbeat", havingValue = "true", + matchIfMissing = true) + public PingJsonMessageHandler pingJsonMessageHandler() { + return new PingJsonMessageHandler(); + } + + /** + * 注册 JsonMessageHandlerInitializer 收集所有的 json 类型消息处理器 + * + * @param jsonMessageHandlerList json 类型消息处理器 + * @return JsonMessageHandlerInitializer + */ + @Bean + @ConditionalOnMissingBean + public JsonMessageHandlerInitializer jsonMessageHandlerInitializer( + List> jsonMessageHandlerList) { + return new JsonMessageHandlerInitializer(jsonMessageHandlerList); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/WebSocketProperties.java b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/WebSocketProperties.java new file mode 100644 index 0000000..3884832 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/WebSocketProperties.java @@ -0,0 +1,89 @@ +package com.baiye.autoconfigure.websocket; + +import com.baiye.handler.ConcurrentWebSocketSessionOptions; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +/** + * websocket props + * + * @author Yakir + */ +@Data +@ConfigurationProperties(WebSocketProperties.PREFIX) +public class WebSocketProperties { + + public static final String PREFIX = "distribute.websocket"; + + /** + * 路径: 无参: /ws 有参: PathVariable: 单参: /ws/{test} 多参: /ws/{test1}/{test2} query: + * /ws?uid=1&name=test + * + */ + private String path = "/ws"; + + /** + * 允许跨域的源 + */ + private String[] allowedOrigins; + + /** + * 允许跨域来源的匹配规则 + */ + private String[] allowedOriginPatterns = new String[] { "*" }; + + /** + * 是否支持部分消息 + */ + private boolean supportPartialMessages = false; + + /** + * 心跳处理 + */ + private boolean heartbeat = true; + + /** + * 是否开启对session的映射记录 + */ + private boolean mapSession = true; + + /** + * 是否开启 sockJs 支持 + */ + private boolean withSockjs = false; + + /** + * 多线程发送相关配置 + */ + @NestedConfigurationProperty + private ConcurrentWebSocketSessionOptions concurrent = new ConcurrentWebSocketSessionOptions(); + + /** + * 消息分发器:local | redis,默认 local, 如果自定义的话,可以配置为其他任意值 + */ + private MessageDistributorTypeEnum messageDistributor = MessageDistributorTypeEnum.LOCAL; + + /** + * 消息分发器类型,用于解决集群场景下的消息跨节点推送问题 + */ + enum MessageDistributorTypeEnum { + + /** + * 本地缓存,不做跨节点分发 + */ + LOCAL, + + /** + * redis,利用 redis pub/sub 处理 + */ + REDIS, + + /** + * 自定义,用户自己实现一个 MessageDistributor + */ + CUSTOM; + + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/config/LocalMessageDistributorConfig.java b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/config/LocalMessageDistributorConfig.java new file mode 100644 index 0000000..d8705bb --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/config/LocalMessageDistributorConfig.java @@ -0,0 +1,37 @@ +package com.baiye.autoconfigure.websocket.config; + +import com.baiye.autoconfigure.websocket.MessageDistributorTypeConstants; +import com.baiye.autoconfigure.websocket.WebSocketProperties; +import com.baiye.distribute.LocalMessageDistributor; +import com.baiye.distribute.MessageDistributor; +import com.baiye.session.WebSocketSessionStore; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 本地的消息分发器配置 + * + * @author hccake + */ +@ConditionalOnProperty(prefix = WebSocketProperties.PREFIX, name = "message-distributor", + havingValue = MessageDistributorTypeConstants.LOCAL, matchIfMissing = true) +@Configuration(proxyBeanMethods = false) +@RequiredArgsConstructor +public class LocalMessageDistributorConfig { + + private final WebSocketSessionStore webSocketSessionStore; + + /** + * 本地基于内存的消息分发,不支持集群 + * @return LocalMessageDistributor + */ + @Bean + @ConditionalOnMissingBean(MessageDistributor.class) + public LocalMessageDistributor messageDistributor() { + return new LocalMessageDistributor(webSocketSessionStore); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/config/RedisMessageDistributorConfig.java b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/config/RedisMessageDistributorConfig.java new file mode 100644 index 0000000..66bc4cd --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/config/RedisMessageDistributorConfig.java @@ -0,0 +1,57 @@ +package com.baiye.autoconfigure.websocket.config; + +import com.baiye.autoconfigure.websocket.MessageDistributorTypeConstants; +import com.baiye.autoconfigure.websocket.WebSocketProperties; +import com.baiye.distribute.MessageDistributor; +import com.baiye.distribute.RedisMessageDistributor; +import com.baiye.distribute.RedisMessageListenerInitializer; +import com.baiye.session.WebSocketSessionStore; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +/** + * 基于 Redis Pub/Sub 的消息分发器 + * + * @author hccake + */ +@ConditionalOnClass(StringRedisTemplate.class) +@ConditionalOnProperty(prefix = WebSocketProperties.PREFIX, name = "message-distributor", + havingValue = MessageDistributorTypeConstants.REDIS) +@Configuration(proxyBeanMethods = false) +@RequiredArgsConstructor +public class RedisMessageDistributorConfig { + + private final WebSocketSessionStore webSocketSessionStore; + + @Bean + @ConditionalOnMissingBean(MessageDistributor.class) + public RedisMessageDistributor messageDistributor(StringRedisTemplate stringRedisTemplate) { + return new RedisMessageDistributor(webSocketSessionStore, stringRedisTemplate); + } + + @Bean + @ConditionalOnBean(RedisMessageDistributor.class) + @ConditionalOnMissingBean + public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + return container; + } + + @Bean + @ConditionalOnMissingBean + public RedisMessageListenerInitializer redisMessageListenerInitializer( + RedisMessageListenerContainer redisMessageListenerContainer, + RedisMessageDistributor redisWebsocketMessageListener) { + return new RedisMessageListenerInitializer(redisMessageListenerContainer, redisWebsocketMessageListener); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/config/RocketMqMessageDistributorConfig.java b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/config/RocketMqMessageDistributorConfig.java new file mode 100644 index 0000000..e371dc0 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/config/RocketMqMessageDistributorConfig.java @@ -0,0 +1,35 @@ +package com.baiye.autoconfigure.websocket.config; + +import com.baiye.autoconfigure.websocket.MessageDistributorTypeConstants; +import com.baiye.autoconfigure.websocket.WebSocketProperties; +import com.baiye.distribute.MessageDistributor; +import com.baiye.distribute.RocketmqMessageDistributor; +import com.baiye.session.WebSocketSessionStore; +import lombok.RequiredArgsConstructor; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * MQ的消息分发器配置 + * + * @author liu_yx + * @since 0.9.0 2022年06月30日 14:11:34 + */ +@ConditionalOnProperty(prefix = WebSocketProperties.PREFIX, name = "message-distributor", + havingValue = MessageDistributorTypeConstants.ROCKETMQ) +@Configuration(proxyBeanMethods = false) +@RequiredArgsConstructor +public class RocketMqMessageDistributorConfig { + + private final WebSocketSessionStore webSocketSessionStore; + + @Bean + @ConditionalOnMissingBean(MessageDistributor.class) + public RocketmqMessageDistributor messageDistributor(RocketMQTemplate template) { + return new RocketmqMessageDistributor(webSocketSessionStore, template); + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/config/WebSocketHandlerConfig.java b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/config/WebSocketHandlerConfig.java new file mode 100644 index 0000000..c3ee484 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/java/com/baiye/autoconfigure/websocket/config/WebSocketHandlerConfig.java @@ -0,0 +1,48 @@ +package com.baiye.autoconfigure.websocket.config; + +import com.baiye.autoconfigure.websocket.WebSocketProperties; +import com.baiye.handler.CustomWebSocketHandler; +import com.baiye.handler.PlanTextMessageHandler; +import com.baiye.session.DefaultWebSocketSessionStore; +import com.baiye.session.MapSessionWebSocketHandlerDecorator; +import com.baiye.session.SessionKeyGenerator; +import com.baiye.session.WebSocketSessionStore; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.web.socket.WebSocketHandler; + +/** + * @author Hccake 2021/1/5 + * @version 1.0 + */ +@RequiredArgsConstructor +public class WebSocketHandlerConfig { + + private final WebSocketProperties webSocketProperties; + + /** + * WebSocket session 存储器 + * @return DefaultWebSocketSessionStore + */ + @Bean + @ConditionalOnMissingBean + public WebSocketSessionStore webSocketSessionStore( + @Autowired(required = false) SessionKeyGenerator sessionKeyGenerator) { + return new DefaultWebSocketSessionStore(sessionKeyGenerator); + } + + @Bean + @ConditionalOnMissingBean(WebSocketHandler.class) + public WebSocketHandler webSocketHandler(WebSocketSessionStore webSocketSessionStore, + @Autowired(required = false) PlanTextMessageHandler planTextMessageHandler) { + CustomWebSocketHandler customWebSocketHandler = new CustomWebSocketHandler(planTextMessageHandler); + if (webSocketProperties.isMapSession()) { + return new MapSessionWebSocketHandlerDecorator(customWebSocketHandler, webSocketSessionStore, + webSocketProperties.getConcurrent()); + } + return customWebSocketHandler; + } + +} diff --git a/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..b734cc0 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,32 @@ +{ + "properties": [ + { + "name": "ballcat.websocket.concurrent.buffer-size-limit", + "type": "java.lang.Integer", + "sourceType": "com.baiye.handler.ConcurrentWebSocketSessionOptions", + "description": "发送消息缓冲上限 (byte)", + "defaultValue": 102400 + }, + { + "name": "ballcat.websocket.concurrent.enable", + "type": "java.lang.Boolean", + "sourceType": "com.baiye.handler.ConcurrentWebSocketSessionOptions", + "description": "是否在多线程环境下进行发送", + "defaultValue": false + }, + { + "name": "ballcat.websocket.concurrent.overflow-strategy", + "type": "org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator$OverflowStrategy", + "sourceType": "com.baiye.handler.ConcurrentWebSocketSessionOptions", + "description": "缓冲区溢出时的执行策略", + "defaultValue": "org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator.OverflowStrategy.TERMINATE" + }, + { + "name": "ballcat.websocket.concurrent.send-time-limit", + "type": "java.lang.Integer", + "sourceType": "com.baiye.handler.ConcurrentWebSocketSessionOptions", + "description": "发送时间的限制(ms)", + "defaultValue": 5000 + } + ] +} \ No newline at end of file diff --git a/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/resources/META-INF/spring.factories b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..377bb6f --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.baiye.autoconfigure.websocket.WebSocketAutoConfiguration diff --git a/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..6b18572 --- /dev/null +++ b/marketing-scrm-starters/marketing-scrm-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.baiye.autoconfigure.websocket.WebSocketAutoConfiguration diff --git a/marketing-scrm-starters/pom.xml b/marketing-scrm-starters/pom.xml new file mode 100644 index 0000000..817de12 --- /dev/null +++ b/marketing-scrm-starters/pom.xml @@ -0,0 +1,34 @@ + + + + com.baiye + marketing-scrm + ${revision} + + 4.0.0 + pom + + marketing-scrm-starters + + + + marketing-scrm-starter-easyexcel + marketing-scrm-starter-redis + marketing-scrm-starter-swagger + marketing-scrm-starter-file + marketing-scrm-starter-websocket + marketing-scrm-starter-job + marketing-scrm-starter-web + + + + + org.projectlombok + lombok + + + + + diff --git a/marketing-scrm-system/pom.xml b/marketing-scrm-system/pom.xml new file mode 100644 index 0000000..414adc0 --- /dev/null +++ b/marketing-scrm-system/pom.xml @@ -0,0 +1,46 @@ + + + + com.baiye + marketing-scrm + ${revision} + + 4.0.0 + marketing-scrm-system + pom + + + system-biz + system-controller + system-model + + + + + org.projectlombok + lombok + + + org.mapstruct + mapstruct + + + + com.baiye + marketing-scrm-extend-mybatis-plus + + + + org.mapstruct + mapstruct + + + + org.mapstruct + mapstruct-processor + 1.5.3.Final + + + + diff --git a/marketing-scrm-system/system-biz/pom.xml b/marketing-scrm-system/system-biz/pom.xml new file mode 100644 index 0000000..f4678fb --- /dev/null +++ b/marketing-scrm-system/system-biz/pom.xml @@ -0,0 +1,52 @@ + + + + marketing-scrm-system + com.baiye + ${revision} + + 4.0.0 + system-biz + + + + com.baiye + common-redis + ${revision} + + + com.baiye + security-core + ${revision} + + + com.baiye + security-oauth2-core + ${revision} + + + com.baiye + system-model + ${revision} + + + org.springframework.boot + spring-boot-configuration-processor + true + + + com.baiye + security-oauth2-authorization-server + true + ${revision} + + + com.baiye + marketing-scrm-starter-file + ${revision} + + + + + diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/authentication/BallcatOAuth2TokenResponseEnhancer.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/authentication/BallcatOAuth2TokenResponseEnhancer.java new file mode 100644 index 0000000..aa8b2b5 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/authentication/BallcatOAuth2TokenResponseEnhancer.java @@ -0,0 +1,77 @@ +package com.baiye.system.authentication; + +import com.baiye.security.constant.TokenAttributeNameConstants; +import com.baiye.security.constant.UserAttributeNameConstants; +import com.baiye.security.userdetails.User; +import com.baiye.system.model.vo.SysUserInfo; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2TokenResponseEnhancer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * token 响应增强 + * + * @author Hccake + */ +public class BallcatOAuth2TokenResponseEnhancer implements OAuth2TokenResponseEnhancer { + + @Override + public Map enhance(OAuth2AccessTokenAuthenticationToken accessTokenAuthentication) { + Object principal = Optional.ofNullable(SecurityContextHolder.getContext()) + .map(SecurityContext::getAuthentication) + .map(Authentication::getPrincipal) + .orElse(null); + + // token 附属信息 + Map additionalParameters = accessTokenAuthentication.getAdditionalParameters(); + if (additionalParameters == null) { + additionalParameters = new HashMap<>(8); + } + + if (principal instanceof User) { + User user = (User) principal; + // 用户基本信息 + SysUserInfo sysUserInfo = getSysUserInfo(user); + additionalParameters.put(TokenAttributeNameConstants.INFO, sysUserInfo); + + // 默认在登录时只把角色和权限的信息返回 + Map resultAttributes = new HashMap<>(2); + Map attributes = user.getAttributes(); + resultAttributes.put(UserAttributeNameConstants.ROLE_CODES, + attributes.get(UserAttributeNameConstants.ROLE_CODES)); + resultAttributes.put(UserAttributeNameConstants.PERMISSIONS, + attributes.get(UserAttributeNameConstants.PERMISSIONS)); + additionalParameters.put(TokenAttributeNameConstants.ATTRIBUTES, resultAttributes); + } + + return additionalParameters; + } + + /** + * 根据 User 对象获取 SysUserInfo + * @param user User + * @return SysUserInfo + */ + public SysUserInfo getSysUserInfo(User user) { + SysUserInfo sysUserInfo = new SysUserInfo(); + sysUserInfo.setUserId(user.getUserId()); + sysUserInfo.setUsername(user.getUsername()); + sysUserInfo.setNickname(user.getNickname()); + sysUserInfo.setAvatar(user.getAvatar()); + sysUserInfo.setOrganizationId(user.getOrganizationId()); + sysUserInfo.setType(user.getType()); + sysUserInfo.setPhoneNumber(user.getPhoneNumber()); + sysUserInfo.setEmail(user.getEmail()); + sysUserInfo.setGender(user.getGender()); + sysUserInfo.setSalesmanType(user.getSalesmanType()); + sysUserInfo.setWhichUserId(user.getWhichUserId()); + return sysUserInfo; + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/authentication/DefaultUserInfoCoordinatorImpl.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/authentication/DefaultUserInfoCoordinatorImpl.java new file mode 100644 index 0000000..deb943e --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/authentication/DefaultUserInfoCoordinatorImpl.java @@ -0,0 +1,20 @@ +package com.baiye.system.authentication; + +import com.baiye.system.model.dto.UserInfoDTO; + +import java.util.Map; + +/** + * 默认的用户信息协调者 + * + * @author Hccake 2020/9/28 + * @version 1.0 + */ +public class DefaultUserInfoCoordinatorImpl implements UserInfoCoordinator { + + @Override + public Map coordinateAttribute(UserInfoDTO userInfoDTO, Map attribute) { + return attribute; + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/authentication/SysUserDetailsServiceImpl.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/authentication/SysUserDetailsServiceImpl.java new file mode 100644 index 0000000..fde6656 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/authentication/SysUserDetailsServiceImpl.java @@ -0,0 +1,92 @@ +package com.baiye.system.authentication; + +import com.baiye.security.constant.UserAttributeNameConstants; +import com.baiye.security.userdetails.User; +import com.baiye.system.model.dto.UserInfoDTO; +import com.baiye.system.model.entity.SysUser; +import com.baiye.system.service.SysUserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.util.CollectionUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; + +/** + * @author Hccake 2019/9/25 20:44 + */ +@Slf4j +@RequiredArgsConstructor +public class SysUserDetailsServiceImpl implements UserDetailsService { + + private final SysUserService sysUserService; + + private final UserInfoCoordinator userInfoCoordinator; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser sysUser = sysUserService.getByUsername(username); + if (sysUser == null) { + log.error("登录:用户名错误,用户名:{}", username); + throw new UsernameNotFoundException("username error!"); + } + UserInfoDTO userInfoDTO = sysUserService.findUserInfo(sysUser); + return getUserDetailsByUserInfo(userInfoDTO); + } + + /** + * 根据UserInfo 获取 UserDetails + * @param userInfoDTO 用户信息DTO + * @return UserDetails + */ + public UserDetails getUserDetailsByUserInfo(UserInfoDTO userInfoDTO) { + + SysUser sysUser = userInfoDTO.getSysUser(); + Collection roleCodes = userInfoDTO.getRoleCodes(); + Collection permissions = userInfoDTO.getPermissions(); + + Collection dbAuthsSet = new HashSet<>(); + if (!CollectionUtils.isEmpty(roleCodes)) { + // 获取角色 + dbAuthsSet.addAll(roleCodes); + // 获取资源 + dbAuthsSet.addAll(permissions); + + } + Collection authorities = AuthorityUtils + .createAuthorityList(dbAuthsSet.toArray(new String[0])); + + // 默认将角色和权限放入属性中 + HashMap attributes = new HashMap<>(8); + attributes.put(UserAttributeNameConstants.ROLE_CODES, roleCodes); + attributes.put(UserAttributeNameConstants.PERMISSIONS, permissions); + + // 用户额外属性 + userInfoCoordinator.coordinateAttribute(userInfoDTO, attributes); + + return User.builder() + .userId(sysUser.getUserId()) + .whichUserId(sysUser.getWhichUserId()) + .username(sysUser.getUsername()) + .password(sysUser.getPassword()) + .nickname(sysUser.getNickname()) + .avatar(sysUser.getAvatar()) + .status(sysUser.getStatus()) + .organizationId(sysUser.getOrganizationId()) + .email(sysUser.getEmail()) + .phoneNumber(sysUser.getPhoneNumber()) + .gender(sysUser.getGender()) + .type(sysUser.getType()) + .salesmanType(userInfoDTO.getSalesmanType()) + .authorities(authorities) + .attributes(attributes) + .build(); + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/authentication/UserInfoCoordinator.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/authentication/UserInfoCoordinator.java new file mode 100644 index 0000000..6453e55 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/authentication/UserInfoCoordinator.java @@ -0,0 +1,23 @@ +package com.baiye.system.authentication; + +import com.baiye.system.model.dto.UserInfoDTO; + +import java.util.Map; + +/** + * 用户附属属性协调 + * + * @author author.zero + * @version 1.0 2021/11/16 20:02 + */ +public interface UserInfoCoordinator { + + /** + * 对于不同类型的用户,可能在业务上需要获取到不同的用户属性 实现此接口,进行用户属性的增强 + * @param userInfoDTO 系统用户信息 + * @param attribute 用户属性,默认添加了 roles 和 permissions 属性 + * @return attribute + */ + Map coordinateAttribute(UserInfoDTO userInfoDTO, Map attribute); + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/checker/AdminUserChecker.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/checker/AdminUserChecker.java new file mode 100644 index 0000000..7882723 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/checker/AdminUserChecker.java @@ -0,0 +1,42 @@ +package com.baiye.system.checker; + +import com.baiye.system.model.entity.SysUser; + +import java.util.Set; + +/** + * 超级管理员账户规则配置 + * + * @author lingting 2020-06-24 21:00:15 + */ +public interface AdminUserChecker { + + /** + * 校验用户是否为超级管理员 + * @param user 用户信息 + * @return boolean + */ + boolean isAdminUser(SysUser user); + + /** + * 校验用户是否为超级管理员 + * @param userId 用户ID + * @return boolean + */ + boolean isAdminUser(Long userId); + + /** + * 修改权限校验 + * @param targetUser 目标用户 + * @return 是否有权限修改目标用户 + */ + boolean hasModifyPermission(SysUser targetUser); + + /** + * 判断是否包含业务员 + * @param roleIdSet + * @return + */ + boolean isSalesman(Set roleIdSet); + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/checker/AdminUserCheckerImpl.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/checker/AdminUserCheckerImpl.java new file mode 100644 index 0000000..38b7770 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/checker/AdminUserCheckerImpl.java @@ -0,0 +1,54 @@ +package com.baiye.system.checker; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.text.CharSequenceUtil; +import com.baiye.security.util.SecurityUtils; +import com.baiye.system.model.entity.SysUser; +import com.baiye.system.properties.SystemProperties; +import com.google.common.collect.Sets; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Set; + +/** + * 超级管理员账户规则配置 + */ +@Service +@RequiredArgsConstructor +public class AdminUserCheckerImpl implements AdminUserChecker { + + private final SystemProperties systemProperties; + + @Override + public boolean isAdminUser(SysUser user) { + SystemProperties.Administrator administrator = systemProperties.getAdministrator(); + if (administrator.getUserId() == user.getUserId()) { + return true; + } + return CharSequenceUtil.isNotEmpty(administrator.getUsername()) + && administrator.getUsername().equals(user.getUsername()); + } + + @Override + public boolean isAdminUser(Long userId) { + SystemProperties.Administrator administrator = systemProperties.getAdministrator(); + return administrator.getUserId() == userId; + } + + @Override + public boolean hasModifyPermission(SysUser targetUser) { + // 如果需要修改的用户是超级管理员,则只能本人修改 + if (this.isAdminUser(targetUser)) { + return SecurityUtils.getUser().getUsername().equals(targetUser.getUsername()); + } + return true; + } + + @Override + public boolean isSalesman(Set roleIdSet) { + Set salesman = systemProperties.getSalesman(); + return CollUtil.isNotEmpty(Sets.intersection(roleIdSet, salesman)); + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/component/PasswordHelper.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/component/PasswordHelper.java new file mode 100644 index 0000000..d6d26b8 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/component/PasswordHelper.java @@ -0,0 +1,77 @@ +package com.baiye.system.component; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.crypto.CryptoException; +import com.baiye.exception.BusinessException; +import com.baiye.security.util.PasswordUtils; +import com.baiye.system.properties.SystemProperties; +import org.ballcat.security.properties.SecurityProperties; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 密码相关的操作的辅助类 + * + * @author hccake + */ +@Component +public class PasswordHelper { + + private final SecurityProperties securityProperties; + + private final PasswordEncoder passwordEncoder; + + private final Pattern passwordPattern; + + public PasswordHelper(SecurityProperties securityProperties, SystemProperties systemProperties, + PasswordEncoder passwordEncoder) { + this.securityProperties = securityProperties; + this.passwordEncoder = passwordEncoder; + String passwordRule = systemProperties.getPasswordRule(); + this.passwordPattern = CharSequenceUtil.isEmpty(passwordRule) ? null : Pattern.compile(passwordRule); + } + + /** + * 密码加密,单向加密,不可逆 + * @param rawPassword 明文密码 + * @return 加密后的密文 + */ + public String encode(String rawPassword) { + return passwordEncoder.encode(rawPassword); + } + + /** + * 将前端传递过来的密文解密为明文 + * @param aesPass AES加密后的密文 + * @return 明文密码 + */ + public String decodeAes(String aesPass) { + try { + return PasswordUtils.decodeAES(aesPass, securityProperties.getPasswordSecretKey()); + } + catch (CryptoException ex) { + throw new BusinessException(400, "密码密文解密异常!"); + } + } + + /** + * 校验密码是否符合规则 + * @param rawPassword 明文密码 + * @return 符合返回 true + */ + public boolean validateRule(String rawPassword) { + if (passwordPattern == null) { + return true; + } + Matcher matcher = passwordPattern.matcher(rawPassword); + return matcher.matches(); + } + + public boolean matches(String oldPass, String password) { + + return passwordEncoder.matches(oldPass, password); + } +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/manager/SysDictManager.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/manager/SysDictManager.java new file mode 100644 index 0000000..0d4997b --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/manager/SysDictManager.java @@ -0,0 +1,253 @@ +package com.baiye.system.manager; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.IdUtil; +import com.baiye.exception.BusinessException; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.result.BaseResultCode; +import com.baiye.system.converter.SysDictItemConverter; +import com.baiye.system.event.DictChangeEvent; +import com.baiye.system.model.dto.SysDictItemDTO; +import com.baiye.system.model.entity.SysDict; +import com.baiye.system.model.entity.SysDictItem; +import com.baiye.system.model.qo.SysDictQO; +import com.baiye.system.model.vo.DictDataVO; +import com.baiye.system.model.vo.DictItemVO; +import com.baiye.system.model.vo.SysDictItemPageVO; +import com.baiye.system.model.vo.SysDictPageVO; +import com.baiye.system.service.SysDictItemService; +import com.baiye.system.service.SysDictService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Hccake 2020/3/27 19:50 + */ +@Service +@RequiredArgsConstructor +public class SysDictManager { + + private final SysDictService sysDictService; + + private final SysDictItemService sysDictItemService; + + private final ApplicationEventPublisher eventPublisher; + + /** + * 字典表分页 + * @param pageParam 分页参数 + * @param sysDictQO 查询参数 + * @return 字典表分页数据 + */ + public PageResult dictPage(PageParam pageParam, SysDictQO sysDictQO) { + return sysDictService.queryPage(pageParam, sysDictQO); + } + + /** + * 保存字典 + * @param sysDict 字典对象 + * @return 执行是否成功 + */ + public boolean dictSave(SysDict sysDict) { + sysDict.setHashCode(IdUtil.fastSimpleUUID()); + return sysDictService.save(sysDict); + } + + /** + * 更新字典 + * @param sysDict 字典对象 + * @return 执行是否成功 + */ + public boolean updateDictById(SysDict sysDict) { + // 查询现有数据 + SysDict dict = sysDictService.getById(sysDict.getId()); + sysDict.setHashCode(IdUtil.fastSimpleUUID()); + boolean result = sysDictService.updateById(sysDict); + if (result) { + eventPublisher.publishEvent(new DictChangeEvent(dict.getCode())); + } + return result; + } + + /** + * 删除字典 + * @param id 字典id + */ + @Transactional(rollbackFor = Exception.class) + public void removeDictById(Long id) { + // 查询现有数据 + SysDict dict = sysDictService.getById(id); + // 字典标识 + String dictCode = dict.getCode(); + + // 有关联字典项则不允许删除 + Assert.isFalse(sysDictItemService.exist(dictCode), + () -> new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), "该字典下存在字典项,不允许删除!")); + + // 删除字典 + Assert.isTrue(sysDictService.removeById(id), + () -> new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "字典删除异常")); + } + + /** + * 更新字典项状态 + * @param itemId 字典项id + */ + @Transactional(rollbackFor = Exception.class) + public void updateDictItemStatusById(Long itemId, Integer status) { + // 获取字典项 + SysDictItem dictItem = sysDictItemService.getById(itemId); + Assert.notNull(dictItem, + () -> new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), "错误的字典项 id:" + itemId)); + + // 更新字典项状态 + SysDictItem entity = new SysDictItem(); + entity.setId(itemId); + entity.setStatus(status); + Assert.isTrue(sysDictItemService.updateById(entity), + () -> new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "字典项状态更新异常")); + + // 更新字典 hash + String dictCode = dictItem.getDictCode(); + Assert.isTrue(sysDictService.updateHashCode(dictCode), + () -> new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "字典 Hash 更新异常")); + + // 发布字典更新事件 + eventPublisher.publishEvent(new DictChangeEvent(dictCode)); + } + + /** + * 字典项分页 + * @param pageParam 分页属性 + * @param dictCode 字典标识 + * @return 字典项分页数据 + */ + public PageResult dictItemPage(PageParam pageParam, String dictCode) { + return sysDictItemService.queryPage(pageParam, dictCode); + } + + /** + * 新增字典项 + * @param sysDictItemDTO 字典项 + * @return 执行是否成功 + */ + @Transactional(rollbackFor = Exception.class) + public boolean saveDictItem(SysDictItemDTO sysDictItemDTO) { + // 更新字典项Hash值 + String dictCode = sysDictItemDTO.getDictCode(); + if (!sysDictService.updateHashCode(dictCode)) { + return false; + } + + SysDictItem sysDictItem = SysDictItemConverter.INSTANCE.dtoToPo(sysDictItemDTO); + boolean result = sysDictItemService.save(sysDictItem); + if (result) { + eventPublisher.publishEvent(new DictChangeEvent(dictCode)); + } + return result; + } + + /** + * 更新字典项 + * @param sysDictItemDTO 字典项 + * @return 执行是否成功 + */ + @Transactional(rollbackFor = Exception.class) + public boolean updateDictItemById(SysDictItemDTO sysDictItemDTO) { + // 根据ID查询字典 + String dictCode = sysDictItemDTO.getDictCode(); + // 更新字典项Hash值 + if (!sysDictService.updateHashCode(dictCode)) { + return false; + } + + SysDictItem sysDictItem = SysDictItemConverter.INSTANCE.dtoToPo(sysDictItemDTO); + boolean result = sysDictItemService.updateById(sysDictItem); + if (result) { + eventPublisher.publishEvent(new DictChangeEvent(dictCode)); + } + return result; + } + + /** + * 删除字典项 + * @param id 字典项 + * @return 执行是否成功 + */ + @Transactional(rollbackFor = Exception.class) + public boolean removeDictItemById(Long id) { + // 根据ID查询字典 + SysDictItem dictItem = sysDictItemService.getById(id); + String dictCode = dictItem.getDictCode(); + // 更新字典项Hash值 + if (!sysDictService.updateHashCode(dictCode)) { + return false; + } + boolean result = sysDictItemService.removeById(id); + if (result) { + eventPublisher.publishEvent(new DictChangeEvent(dictCode)); + } + return true; + } + + /** + * 查询字典数据 + * @param dictCodes 字典标识 + * @return DictDataAndHashVO + */ + public List queryDictDataAndHashVO(String[] dictCodes) { + // 查询对应hash值,以及字典项数据 + List sysDictList = sysDictService.listByCodes(dictCodes); + if (CollUtil.isEmpty(sysDictList)) { + return new ArrayList<>(); + } + + // 填充字典项 + List list = new ArrayList<>(); + for (SysDict sysDict : sysDictList) { + List dictItems = sysDictItemService.listByDictCode(sysDict.getCode()); + // 排序并转换为VO + List setDictItems = dictItems.stream() + .sorted(Comparator.comparingInt(SysDictItem::getSort)) + .map(SysDictItemConverter.INSTANCE::poToItemVo) + .collect(Collectors.toList()); + // 组装DataVO + DictDataVO dictDataVO = new DictDataVO(); + dictDataVO.setValueType(sysDict.getValueType()); + dictDataVO.setDictCode(sysDict.getCode()); + dictDataVO.setHashCode(sysDict.getHashCode()); + dictDataVO.setDictItems(setDictItems); + + list.add(dictDataVO); + } + return list; + } + + /** + * 返回失效的Hash + * @param dictHashCode 校验的hashCodeMap + * @return List 失效的字典标识集合 + */ + public List invalidDictHash(Map dictHashCode) { + // @formatter:off + List byCode = sysDictService.listByCodes(dictHashCode.keySet() + .toArray(new String[] {})); + // 过滤相等Hash值的字典项,并返回需要修改的字典项的Code + return byCode.stream() + .filter(x -> !dictHashCode.get(x.getCode()).equals(x.getHashCode())) + .map(SysDict::getCode) + .collect(Collectors.toList()); + // @formatter:on + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysConfigMapper.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysConfigMapper.java new file mode 100644 index 0000000..b26a65f --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysConfigMapper.java @@ -0,0 +1,72 @@ +package com.baiye.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.system.converter.SysConfigConverter; +import com.baiye.system.model.entity.SysConfig; +import com.baiye.system.model.qo.SysConfigQO; +import com.baiye.system.model.vo.SysConfigPageVO; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; + +/** + * 系统配置表 + * + * @author ballcat code generator 2019-10-14 17:42:23 + */ +public interface SysConfigMapper extends ExtendMapper { + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param sysConfigQO 查询参数 + * @return PageResult + */ + default PageResult queryPage(PageParam pageParam, SysConfigQO sysConfigQO) { + IPage page = this.prodPage(pageParam); + Wrapper wrapper = WrappersX.lambdaQueryX(SysConfig.class) + .likeIfPresent(SysConfig::getConfKey, sysConfigQO.getConfKey()) + .likeIfPresent(SysConfig::getName, sysConfigQO.getName()) + .likeIfPresent(SysConfig::getCategory, sysConfigQO.getCategory()); + this.selectPage(page, wrapper); + IPage voPage = page.convert(SysConfigConverter.INSTANCE::poToPageVo); + return new PageResult<>(voPage.getRecords(), voPage.getTotal()); + } + + /** + * 根据配置key查询配置信息 + * @param confKey 配置key + * @return SysConfig 配置信息 + */ + default SysConfig selectByKey(String confKey) { + return this.selectOne(Wrappers.lambdaQuery().eq(SysConfig::getConfKey, confKey)); + } + + /** + * 根据 confKey 进行更新 + * @param sysConfig 系统配置 + * @return 更新是否成功 + */ + default boolean updateByKey(SysConfig sysConfig) { + Wrapper wrapper = Wrappers.lambdaUpdate(SysConfig.class) + .eq(SysConfig::getConfKey, sysConfig.getConfKey()); + int flag = this.update(sysConfig, wrapper); + return SqlHelper.retBool(flag); + } + + /** + * 根据 confKey 进行删除 + * @param confKey 配置key + * @return 删除是否成功 + */ + default boolean deleteByKey(String confKey) { + Wrapper wrapper = Wrappers.lambdaQuery(SysConfig.class).eq(SysConfig::getConfKey, confKey); + int flag = this.delete(wrapper); + return SqlHelper.retBool(flag); + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysDictItemMapper.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysDictItemMapper.java new file mode 100644 index 0000000..d979bef --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysDictItemMapper.java @@ -0,0 +1,66 @@ +package com.baiye.system.mapper; + +import com.baiye.system.converter.SysDictItemConverter; +import com.baiye.system.model.entity.SysDictItem; +import com.baiye.system.model.vo.SysDictItemPageVO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; + +import java.util.List; + +/** + * 字典项 + * + * @author hccake 2020-03-26 18:40:20 + */ +public interface SysDictItemMapper extends ExtendMapper { + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param dictCode 字典标识 + * @return PageResult + */ + default PageResult queryPage(PageParam pageParam, String dictCode) { + IPage page = this.prodPage(pageParam); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(SysDictItem.class) + .eq(SysDictItem::getDictCode, dictCode); + this.selectPage(page, wrapper); + IPage voPage = page.convert(SysDictItemConverter.INSTANCE::poToPageVo); + return new PageResult<>(voPage.getRecords(), voPage.getTotal()); + } + + /** + * 根据字典标识查询对应字典项集合 + * @param dictCode 字典标识 + * @return List 字典项集合 + */ + default List listByDictCode(String dictCode) { + return this.selectList(Wrappers.lambdaQuery().eq(SysDictItem::getDictCode, dictCode)); + } + + /** + * 根据字典标识删除对应字典项 + * @param dictCode 字典标识 + * @return 是否删除成功 + */ + default boolean deleteByDictCode(String dictCode) { + int i = this.delete(Wrappers.lambdaUpdate().eq(SysDictItem::getDictCode, dictCode)); + return SqlHelper.retBool(i); + } + + /** + * 判断是否存在指定字典标识的字典项 + * @param dictCode 字典标识 + * @return boolean 存在:true + */ + default boolean existsDictItem(String dictCode) { + return this.exists(Wrappers.lambdaQuery(SysDictItem.class).eq(SysDictItem::getDictCode, dictCode)); + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysDictMapper.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysDictMapper.java new file mode 100644 index 0000000..36856e5 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysDictMapper.java @@ -0,0 +1,71 @@ +package com.baiye.system.mapper; + +import com.baiye.system.converter.SysDictConverter; +import com.baiye.system.model.entity.SysDict; +import com.baiye.system.model.qo.SysDictQO; +import com.baiye.system.model.vo.SysDictPageVO; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.conditions.query.LambdaQueryWrapperX; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; + +import java.util.List; + +/** + * 字典表 + * + * @author hccake 2020-03-26 18:40:20 + */ +public interface SysDictMapper extends ExtendMapper { + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param qo 查询对象 + * @return PageResult + */ + default PageResult queryPage(PageParam pageParam, SysDictQO qo) { + IPage page = this.prodPage(pageParam); + LambdaQueryWrapperX wrapper = WrappersX.lambdaQueryX(SysDict.class) + .likeIfPresent(SysDict::getCode, qo.getCode()) + .likeIfPresent(SysDict::getTitle, qo.getTitle()); + this.selectPage(page, wrapper); + IPage voPage = page.convert(SysDictConverter.INSTANCE::poToPageVo); + return new PageResult<>(voPage.getRecords(), voPage.getTotal()); + } + + /** + * 根据字典标识查询对应字典 + * @param dictCode 字典标识 + * @return SysDict 字典 + */ + default SysDict getByCode(String dictCode) { + return this.selectOne(Wrappers.lambdaQuery().eq(SysDict::getCode, dictCode)); + } + + /** + * 根据字典标识数组查询对应字典集合 + * @param dictCodes 字典标识数组 + * @return List 字典集合 + */ + default List listByCodes(String[] dictCodes) { + return this.selectList(Wrappers.lambdaQuery().in(SysDict::getCode, (Object[]) dictCodes)); + } + + /** + * 更新字典的HashCode + * @param dictCode 字典标识 + * @param hashCode 哈希值 + * @return boolean 是否更新成功 + */ + default boolean updateHashCode(String dictCode, String hashCode) { + int flag = this.update(null, + Wrappers.lambdaUpdate().set(SysDict::getHashCode, hashCode).eq(SysDict::getCode, dictCode)); + return SqlHelper.retBool(flag); + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysMenuMapper.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysMenuMapper.java new file mode 100644 index 0000000..98ccb4d --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysMenuMapper.java @@ -0,0 +1,85 @@ +package com.baiye.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.baiye.system.model.entity.SysMenu; +import com.baiye.system.model.qo.SysMenuQO; +import com.baiye.extend.mybatis.plus.conditions.query.LambdaQueryWrapperX; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; + +import java.io.Serializable; +import java.util.List; + +/** + * 菜单权限 + * + * @author hccake 2021-04-01 22:08:13 + */ +public interface SysMenuMapper extends ExtendMapper { + + /** + * 查询权限集合,并按sort排序(升序) + * @param sysMenuQO 查询条件 + * @return List + */ + default List listOrderBySort(SysMenuQO sysMenuQO) { + LambdaQueryWrapperX wrapper = WrappersX.lambdaQueryX(SysMenu.class) + .likeIfPresent(SysMenu::getId, sysMenuQO.getId()) + .likeIfPresent(SysMenu::getTitle, sysMenuQO.getTitle()) + .likeIfPresent(SysMenu::getPermission, sysMenuQO.getPermission()) + .likeIfPresent(SysMenu::getPath, sysMenuQO.getPath()); + wrapper.orderByAsc(SysMenu::getSort); + return this.selectList(wrapper); + } + + /** + * 根据角色标识查询对应的菜单 + * @param roleCode 角色标识 + * @return List + */ + List listByRoleCode(String roleCode); + + /** + * 查询指定权限的下级权限总数 + * @param id 权限ID + * @return 下级权限总数 + */ + default Long countSubMenu(Serializable id) { + return this.selectCount(Wrappers.query().lambda().eq(SysMenu::getParentId, id)); + } + + /** + * 根据指定的 id 更新 菜单权限(便于修改其 id) + * @param sysMenu 系统菜单 + * @param originalId 原菜单ID + * @return 更新成功返回 true + */ + default boolean updateMenuAndId(Long originalId, SysMenu sysMenu) { + // @formatter:off + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(SysMenu.class) + .set(SysMenu::getId, sysMenu.getId()) + .eq(SysMenu::getId, originalId); + // @formatter:on + int flag = this.update(sysMenu, wrapper); + return SqlHelper.retBool(flag); + } + + /** + * 根据指定的 parentId 找到对应的菜单,更新其 parentId + * @param originalParentId 原 parentId + * @param parentId 现 parentId + * @return 更新条数不为 0 时,返回 true + */ + default boolean updateParentId(Long originalParentId, Long parentId) { + // @formatter:off + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(SysMenu.class) + .set(SysMenu::getParentId, parentId) + .eq(SysMenu::getParentId, originalParentId); + // @formatter:on + int flag = this.update(null, wrapper); + return SqlHelper.retBool(flag); + } + +} \ No newline at end of file diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysOrganizationMapper.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysOrganizationMapper.java new file mode 100644 index 0000000..8f534ab --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysOrganizationMapper.java @@ -0,0 +1,68 @@ +package com.baiye.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baiye.system.model.dto.OrganizationMoveChildParam; +import com.baiye.system.model.entity.SysOrganization; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.lang.Nullable; + +import java.util.List; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 12:09:43 + */ +public interface SysOrganizationMapper extends ExtendMapper { + + /** + * 根据组织ID 查询除该组织下的所有儿子组织 + * @param organizationId 组织机构ID + * @return List 该组织的儿子组织 + */ + default List listSubOrganization(Long organizationId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(SysOrganization::getParentId, organizationId); + return this.selectList(wrapper); + } + + /** + * 跟随父节点移动子节点 + * @param param OrganizationMoveChildParam 跟随移动子节点的参数对象 + */ + void followMoveChildNode(@Param("param") OrganizationMoveChildParam param); + + /** + * 根据组织机构Id,查询该组织下的所有子部门 + * @param organizationId 组织机构ID + * @return 子部门集合 + */ + List listChildOrganization(@Param("organizationId") Long organizationId); + + /** + * 批量更新节点层级和深度 + * @param depth 深度 + * @param hierarchy 层级 + * @param organizationIds 组织id集合 + */ + default void updateHierarchyAndPathBatch(int depth, String hierarchy, List organizationIds) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(SysOrganization.class) + .set(SysOrganization::getDepth, depth) + .set(SysOrganization::getHierarchy, hierarchy) + .in(SysOrganization::getId, organizationIds); + this.update(null, wrapper); + + } + + /** + * 检查指定机构下是否存在子机构 + * @param organizationId 机构id + * @return 存在返回 true + */ + @Nullable + Boolean existsChildOrganization(Long organizationId); + +} \ No newline at end of file diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysRoleMapper.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysRoleMapper.java new file mode 100644 index 0000000..212583a --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysRoleMapper.java @@ -0,0 +1,63 @@ +package com.baiye.system.mapper; + +import cn.hutool.core.text.CharSequenceUtil; +import com.baiye.system.converter.SysRoleConverter; +import com.baiye.system.model.entity.SysRole; +import com.baiye.system.model.qo.SysRoleQO; +import com.baiye.system.model.vo.SysRolePageVO; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.domain.SelectData; +import com.baiye.extend.mybatis.plus.conditions.query.LambdaQueryWrapperX; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; + +import java.util.List; + +/** + *

          + * Mapper 接口 + *

          + * + * @author ballcat + * @since 2017-10-29 + */ +public interface SysRoleMapper extends ExtendMapper { + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param qo 查询对象 + * @return PageResult + */ + default PageResult queryPage(PageParam pageParam, SysRoleQO qo) { + IPage page = this.prodPage(pageParam); + LambdaQueryWrapperX wrapper = WrappersX.lambdaQueryX(SysRole.class) + .likeIfPresent(SysRole::getName, qo.getName()) + .likeIfPresent(SysRole::getCode, qo.getCode()) + .between(CharSequenceUtil.isNotBlank(qo.getStartTime()) && CharSequenceUtil.isNotBlank(qo.getEndTime()), + SysRole::getCreateTime, qo.getStartTime(), qo.getEndTime()); + this.selectPage(page, wrapper); + IPage voPage = page.convert(SysRoleConverter.INSTANCE::poToPageVo); + return new PageResult<>(voPage.getRecords(), voPage.getTotal()); + } + + /** + * 获取角色下拉框数据 + * @return 下拉选择框数据集合 + */ + List> listSelectData(); + + /** + * 是否存在角色code + * @param roleCode 角色code + * @return boolean 是否存在 + */ + default boolean existsRoleCode(String roleCode) { + LambdaQueryWrapperX wrapperX = new LambdaQueryWrapperX<>(); + wrapperX.eq(SysRole::getCode, roleCode); + return this.selectCount(wrapperX) > 0L; + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysRoleMenuMapper.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysRoleMenuMapper.java new file mode 100644 index 0000000..5c2d4f5 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysRoleMenuMapper.java @@ -0,0 +1,52 @@ +package com.baiye.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baiye.system.model.entity.SysRoleMenu; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; + +import java.io.Serializable; + +/** + *

          + * 角色菜单表 Mapper 接口 + *

          + * + * @author hccake + * @since 2017-10-29 + */ +public interface SysRoleMenuMapper extends ExtendMapper { + + /** + * 根据权限ID删除角色权限关联关系 + * @param menuId 权限ID + */ + default void deleteByMenuId(Serializable menuId) { + this.delete(Wrappers.query().lambda().eq(SysRoleMenu::getMenuId, menuId)); + } + + /** + * 根据角色标识删除角色权限关联关系 + * @param roleCode 角色标识 + */ + default void deleteByRoleCode(String roleCode) { + this.delete(Wrappers.query().lambda().eq(SysRoleMenu::getRoleCode, roleCode)); + } + + /** + * 更新某个菜单的 id + * @param originalId 原菜单ID + * @param menuId 修改后的菜单Id + * @return 被更新的条数 + */ + default int updateMenuId(Long originalId, Long menuId) { + // @formatter:off + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(SysRoleMenu.class) + .set(SysRoleMenu::getMenuId, menuId) + .eq(SysRoleMenu::getMenuId, originalId); + // @formatter:on + + return this.update(null, wrapper); + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysUserMapper.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysUserMapper.java new file mode 100644 index 0000000..b4ee335 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysUserMapper.java @@ -0,0 +1,247 @@ +package com.baiye.system.mapper; + +import com.baiye.constant.GlobalConstants; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.domain.SelectData; +import com.baiye.extend.mybatis.plus.conditions.query.LambdaAliasQueryWrapperX; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; +import com.baiye.system.model.dto.SysUserNameRoleDTO; +import com.baiye.system.model.entity.SysUser; +import com.baiye.system.model.qo.SysUserQO; +import com.baiye.system.model.vo.SysUserBalanceVO; +import com.baiye.system.model.vo.SysUserPageVO; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 系统用户表 + * + * @author Hccake + */ +public interface SysUserMapper extends ExtendMapper { + + /** + * 分页查询 + * + * @param pageParam 分页参数 + * @param qo 查询对象 + * @return PageResult + */ + default PageResult queryPage(PageParam pageParam, SysUserQO qo) { + IPage page = this.prodPage(pageParam); + LambdaAliasQueryWrapperX wrapperX = WrappersX.lambdaAliasQueryX(SysUser.class); + wrapperX.eq(SysUser::getDeleted, GlobalConstants.NOT_DELETED_FLAG) + .likeIfPresent(SysUser::getUsername, qo.getUsername()) + .likeIfPresent(SysUser::getEmail, qo.getEmail()) + .likeIfPresent(SysUser::getPhoneNumber, qo.getPhoneNumber()) + .likeIfPresent(SysUser::getNickname, qo.getNickname()) + .eqIfPresent(SysUser::getStatus, qo.getStatus()) + .eqIfPresent(SysUser::getGender, qo.getGender()) + .eqIfPresent(SysUser::getType, qo.getType()) + .eqIfPresent(SysUser::getWhichUserId, qo.getWhichUserId()) + .inIfPresent(SysUser::getOrganizationId, qo.getOrganizationId()); + if (StringUtils.isNotBlank(qo.getStartTime()) && StringUtils.isNotBlank(qo.getEndTime())) { + wrapperX.between(SysUser::getCreateTime, qo.getStartTime(), qo.getEndTime()); + } + this.selectByPage(page, wrapperX); + return new PageResult<>(page.getRecords(), page.getTotal()); + } + + /** + * 分页查询用户 + * + * @param page 分页封装对象 + * @param wrapper 条件构造器 + * @return 分页封装对象 + */ + IPage selectByPage(IPage page, @Param(Constants.WRAPPER) Wrapper wrapper); + + /** + * 批量更新用户状态 + * + * @param userIds 用户ID集合 + * @param status 状态 + * @return 是否更新成功 + */ + default boolean updateUserStatusBatch(Collection userIds, Integer status) { + int i = this.update(null, + Wrappers.lambdaUpdate(SysUser.class).set(SysUser::getStatus, status).in(SysUser::getUserId, userIds)); + return SqlHelper.retBool(i); + } + + /** + * 根据用户名查询用户 + * + * @param username 用户名 + * @return 系统用户 + */ + default SysUser selectByUsername(String username) { + return this.selectOne(Wrappers.lambdaQuery().eq(SysUser::getUsername, username)); + } + + /** + * 更新指定用户的密码 + * + * @param userId 用户 + * @param password 密码 + * @return 更新条数 + */ + default boolean updatePassword(Long userId, String password) { + int i = this.update(null, + Wrappers.lambdaUpdate().eq(SysUser::getUserId, userId).set(SysUser::getPassword, password)); + return SqlHelper.retBool(i); + } + + /** + * 根据组织机构ID查询用户 + * + * @param organizationIds 组织机构id集合 + * @return 用户集合 + */ + default List listByOrganizationIds(Collection organizationIds) { + return this.selectList(Wrappers.lambdaQuery().in(SysUser::getOrganizationId, organizationIds)); + } + + /** + * 根据用户类型查询用户 + * + * @param userTypes 用户类型集合 + * @return 用户集合 + */ + default List listByUserTypes(Collection userTypes) { + return this.selectList(Wrappers.lambdaQuery().in(SysUser::getType, userTypes)); + } + + /** + * 根据用户Id集合查询用户 + * + * @param userIds 用户Id集合 + * @return 用户集合 + */ + default List listByUserIds(Collection userIds) { + return this.selectList(Wrappers.lambdaQuery().in(SysUser::getUserId, userIds)); + } + + /** + * 根据RoleCode 查询对应用户 + * + * @param roleCodes 角色标识 + * @return List 该角色标识对应的用户列表 + */ + List listByRoleCodes(@Param("roleCodes") Collection roleCodes); + + /** + * 根据RoleCode和 id 查询对应用户 + * + * @param roleCode 角色标识 + * @param userId userId + * @return List + */ + List listByRoleCodeAndUserId(@Param("roleCode") String roleCode, @Param("userId") Long userId); + + /** + * 根据用户id查询用户名称 + * + * @param userIds ids + * @return + */ + List listUserNameByUserIds(@Param("userIds") List userIds); + + /** + * 返回用户的select数据 name=> username value => userId + * + * @param userTypes 用户类型 + * @return List + */ + List> listSelectData(@Param("userTypes") Collection userTypes); + + /** + * 是否存在指定组织的用户 + * + * @param organizationId 组织 id + * @return boolean 存在返回 true + */ + default boolean existsForOrganization(Long organizationId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(SysUser.class) + .eq(SysUser::getOrganizationId, organizationId); + Long count = this.selectCount(wrapper); + return SqlHelper.retBool(count); + } + + /** + * 查询子用户 根据RoleCode + * + * @param roleCodes 角色标识 + * @return List 该角色标识对应的用户列表 + */ + List> listSelectSonListData(@Param("userId") Long userId, @Param("roleCodes") Collection roleCodes); + + /** + * 余额充值 + */ + int recharge(@Param("userId") Long userId, @Param("rechargeClueNum") Integer rechargeClueNum); + + /** + * 余额扣费 + */ + int charging(@Param("map") Map map); + + /** + * 查询用户余额 + */ + Integer findUserBalance(@Param("userId") Long userId); + + /** + * 查询用户余额 + */ + List listUserBalance(@Param("userIdList") Collection userIdList); + + + /** + * 查询用户余额 + * + * @param userId + * @return + */ + SysUserBalanceVO userBalance(@Param("userId") Long userId); + + + /** + * 余额扣费 + */ + int charging(@Param("userId") Long userId); + + void updateDistributeNum(@Param("map") Map map); + + + /** + * 操作数量 + * + * @param userId + * @param distributeNum + * @param num + * @return + */ + int operationsNum(@Param("userId") Long userId,@Param("distributeNum") Integer distributeNum, @Param("num") Integer num); + + /** + * 修改数量 + * + * @param userId + * @param distributeNum + * @return + */ + int operationsDistributeNum(@Param("userId") Long userId,@Param("distributeNum") Integer distributeNum); +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysUserRoleMapper.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..5cc6fc8 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysUserRoleMapper.java @@ -0,0 +1,110 @@ +package com.baiye.system.mapper; + +import com.baiye.system.model.entity.SysUserRole; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.baiye.system.model.entity.SysRole; +import com.baiye.system.model.qo.RoleBindUserQO; +import com.baiye.system.model.vo.RoleBindUserVO; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; +import org.apache.ibatis.annotations.Param; + +import java.util.ArrayList; +import java.util.List; + +/** + *

          + * 用户角色表 Mapper 接口 + *

          + * + * @author hccake + * @since 2017-10-29 + */ +public interface SysUserRoleMapper extends ExtendMapper { + + /** + * 删除用户关联关系 + * @param userId 用户ID + * @return boolean 删除是否成功 + */ + default boolean deleteByUserId(Long userId) { + int i = this.delete(Wrappers.lambdaQuery(SysUserRole.class).eq(SysUserRole::getUserId, userId)); + return SqlHelper.retBool(i); + } + + /** + * 插入用户角色关联关系 + * @param list 用户角色关联集合 + * @return boolean 插入是否成功 + */ + default boolean insertUserRoles(List list) { + int i = this.insertBatchSomeColumn(list); + return SqlHelper.retBool(i); + } + + /** + * 用户是否存在角色绑定关系 + * @param userId 用户ID + * @param roleCode 角色标识,可为空 + * @return 存在:true + */ + default boolean existsRoleBind(Long userId, String roleCode) { + Long num = this.selectCount(WrappersX.lambdaQueryX(SysUserRole.class) + .eq(SysUserRole::getUserId, userId) + .eqIfPresent(SysUserRole::getRoleCode, roleCode)); + return SqlHelper.retBool(num); + } + + /** + * 通过角色标识,查询用户列表 + * @param pageParam 分页参数 + * @param roleBindUserQO 角色标识 + * @return List 角色授权的用户列表 + */ + default PageResult queryUserPageByRoleCode(PageParam pageParam, RoleBindUserQO roleBindUserQO) { + // TODO 连表查询排序,这里暂时禁用 + pageParam.setSorts(new ArrayList<>()); + IPage page = this.prodPage(pageParam); + this.queryUserPageByRoleCode(page, roleBindUserQO); + return new PageResult<>(page.getRecords(), page.getTotal()); + } + + /** + * 删除角色和用户关系 + * @param userId 用户ID + * @param roleCode 角色标识 + * @return 删除成功:true + */ + default boolean deleteUserRole(Long userId, String roleCode) { + int i = this.delete(Wrappers.lambdaQuery(SysUserRole.class) + .eq(SysUserRole::getUserId, userId) + .eq(SysUserRole::getRoleCode, roleCode)); + return SqlHelper.retBool(i); + } + + /** + * 通过用户ID,查询角色 + * @param userId 用户ID + * @return 用户拥有的角色集合 + */ + List listRoleByUserId(Long userId); + + /** + * 通过角色标识,查询用户列表 + * @param roleCode 角色标识 + * @return List 角色授权的用户列表 + */ + IPage queryUserPageByRoleCode(IPage page, @Param("qo") RoleBindUserQO roleCode); + + /** + * 根据id与code值查询 + * @param baseUserId + * @param roleCode + * @return + */ + List selectListByUserIdAndRoleCode(Long baseUserId, String roleCode); +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysUserRolePromiseMapper.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysUserRolePromiseMapper.java new file mode 100644 index 0000000..972083e --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/mapper/SysUserRolePromiseMapper.java @@ -0,0 +1,86 @@ +package com.baiye.system.mapper; + +import com.baiye.extend.mybatis.plus.mapper.ExtendMapper; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; +import com.baiye.system.model.entity.SysUserRolePromise; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; + +import java.util.List; + +/** + *

          + * 用户角色表 Mapper 接口 + *

          + * + * @author hccake + * @since 2017-10-29 + */ +public interface SysUserRolePromiseMapper extends ExtendMapper { + + /** + * 删除用户关联关系 + * + * @param userId 用户ID + * @return boolean 删除是否成功 + */ + default boolean deleteByUserId(Long userId) { + int i = this.delete(Wrappers.lambdaQuery(SysUserRolePromise.class).eq(SysUserRolePromise::getUserId, userId)); + return SqlHelper.retBool(i); + } + + + /** + * 根据用户id和code删除 + * + * @param userId + * @param roleCode + * @return + */ + default boolean deleteByUserIdAndCode(Long userId, String roleCode) { + int i = this.delete(Wrappers.lambdaQuery(SysUserRolePromise.class).eq(SysUserRolePromise::getUserId, userId).eq(SysUserRolePromise::getRoleCodes, roleCode)); + return SqlHelper.retBool(i); + } + + /** + * 插入用户角色关联关系 + * + * @param list 用户角色关联集合 + * @return boolean 插入是否成功 + */ + default boolean insertUserRoles(List list) { + int i = this.insertBatchSomeColumn(list); + return SqlHelper.retBool(i); + } + + /** + * 用户是否存在角色绑定关系 + * + * @param userId 用户ID + * @param roleCode 角色标识,可为空 + * @return 存在:true + */ + default boolean existsRoleBind(Long userId, String roleCode) { + Long num = this.selectCount(WrappersX.lambdaQueryX(SysUserRolePromise.class) + .eq(SysUserRolePromise::getUserId, userId) + .eqIfPresent(SysUserRolePromise::getRoleCodes, roleCode)); + return SqlHelper.retBool(num); + } + + + /** + * 删除角色和用户关系 + * + * @param userId 用户ID + * @param roleCode 角色标识 + * @return 删除成功:true + */ + default boolean deleteUserRole(Long userId, String roleCode) { + int i = this.delete(Wrappers.lambdaQuery(SysUserRolePromise.class) + .eq(SysUserRolePromise::getUserId, userId) + .eq(SysUserRolePromise::getRoleCodes, roleCode)); + return SqlHelper.retBool(i); + } + + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/AliPayProperties.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/AliPayProperties.java new file mode 100644 index 0000000..e639b56 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/AliPayProperties.java @@ -0,0 +1,30 @@ +package com.baiye.system.properties; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Enzo + */ +@Data +@ConfigurationProperties(prefix = "alipay") +public class AliPayProperties { + + private String protocol; + + private String gatewayHost; + + private String signType; + + private String appId; + + private String merchantPrivateKey; + + private String aliPayPublicKey; + + private String notifyUrl; + + private String encryptKey; + + private String returnUrl; + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/BusinessProperties.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/BusinessProperties.java new file mode 100644 index 0000000..86dc051 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/BusinessProperties.java @@ -0,0 +1,23 @@ +package com.baiye.system.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * @author Enzo + * @date : 2022/10/30 + */ +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "business") +public class BusinessProperties { + + private List roleCodes; + + private String storeCode; +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/FileProperties.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/FileProperties.java new file mode 100644 index 0000000..088a701 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/FileProperties.java @@ -0,0 +1,54 @@ +package com.baiye.system.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "file") +public class FileProperties { + + /** + * 文件大小限制 + */ + private Long maxSize; + + /** + * 头像大小限制 + */ + private Long avatarMaxSize; + + private ElPath mac; + + private ElPath linux; + + private ElPath windows; + + private String downUrl; + + public ElPath getPath() { + String os = System.getProperty("os.name"); + if (os.toLowerCase().startsWith("win")) { + return windows; + } + else if (os.toLowerCase().startsWith("mac")) { + return mac; + } + return linux; + } + + @Data + public static class ElPath { + + private String path; + + private String avatar; + + private String clueFilePath; + + private String systemSeparator; + + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/SystemProperties.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/SystemProperties.java new file mode 100644 index 0000000..b3e784f --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/SystemProperties.java @@ -0,0 +1,60 @@ +package com.baiye.system.properties; + +import com.google.common.collect.Sets; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Set; + +/** + * 权限管理系统相关的基础配置 + * + * @author Hccake 2021/4/23 + * @version 1.0 + */ +@Getter +@Setter +@ConfigurationProperties(prefix = SystemProperties.PREFIX) +public class SystemProperties { + + public static final String PREFIX = "ballcat.system"; + + /** + * 超级管理员的配置 + */ + private Administrator administrator = new Administrator(); + + + /** + * 业务员配置 + * + */ + private Set salesman = Sets.newHashSet(19L, 20L, 21L); + + /** + * 密码的规则:值为正则表达式,当为空时,不对密码规则进行校验 + */ + private String passwordRule; + + @Getter + @Setter + public static class Administrator { + + + + + /** + * 指定id的用户为超级管理员 + */ + private int userId = 1; + + /** + * 指定 username 为超级管理员 + */ + private String username; + + + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/WeChatProperties.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/WeChatProperties.java new file mode 100644 index 0000000..f4d210b --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/properties/WeChatProperties.java @@ -0,0 +1,23 @@ +package com.baiye.system.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @author Enzo + * @date : 2022/6/24 + */ +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "wechat") +public class WeChatProperties { + + private String gatewayHost; + + + private String appKey; + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysConfigService.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysConfigService.java new file mode 100644 index 0000000..605e79b --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysConfigService.java @@ -0,0 +1,47 @@ +package com.baiye.system.service; + +import com.baiye.system.model.entity.SysConfig; +import com.baiye.system.model.qo.SysConfigQO; +import com.baiye.system.model.vo.SysConfigPageVO; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.ExtendService; + +/** + * 系统配置表 + * + * @author ballcat code generator + * @date 2019-10-14 17:42:23 + */ +public interface SysConfigService extends ExtendService { + + /** + * 根据QueryObject查询分页数据 + * @param pageParam 分页参数 + * @param sysConfigQO 查询参数对象 + * @return 分页数据 + */ + PageResult queryPage(PageParam pageParam, SysConfigQO sysConfigQO); + + /** + * 根据配置key获取对应value + * @param confKey 配置key + * @return confValue + */ + String getConfValueByKey(String confKey); + + /** + * 根据 confKey 进行更新 + * @param sysConfig 系统配置 + * @return 更新是否成功 + */ + boolean updateByKey(SysConfig sysConfig); + + /** + * 根据 confKey 进行删除 + * @param confKey 配置key + * @return 删除是否成功 + */ + boolean removeByKey(String confKey); + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysDictItemService.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysDictItemService.java new file mode 100644 index 0000000..640195b --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysDictItemService.java @@ -0,0 +1,48 @@ +package com.baiye.system.service; + +import com.baiye.system.model.entity.SysDictItem; +import com.baiye.system.model.vo.SysDictItemPageVO; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.ExtendService; + +import java.util.List; + +/** + * 字典项 + * + * @author hccake + * @date 2020-03-26 18:40:20 + */ +public interface SysDictItemService extends ExtendService { + + /** + * 根据QueryObject查询分页数据 + * @param pageParam 分页参数 + * @param dictCode 查询参数对象 + * @return 分页数据 + */ + PageResult queryPage(PageParam pageParam, String dictCode); + + /** + * 根据Code查询对应字典项数据 + * @param dictCode 字典标识 + * @return 该字典对应的字典项集合 + */ + List listByDictCode(String dictCode); + + /** + * 根据字典标识删除对应字典项 + * @param dictCode 字典标识 + * @return 是否删除成功 + */ + boolean removeByDictCode(String dictCode); + + /** + * 根据字典标识判断是否存在对应字典项 + * @param dictCode 字典标识 + * @return boolean 存在返回 true + */ + boolean exist(String dictCode); + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysDictService.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysDictService.java new file mode 100644 index 0000000..9b18560 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysDictService.java @@ -0,0 +1,49 @@ +package com.baiye.system.service; + +import com.baiye.system.model.qo.SysDictQO; +import com.baiye.system.model.vo.SysDictPageVO; +import com.baiye.system.model.entity.SysDict; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.ExtendService; + +import java.util.List; + +/** + * 字典表 + * + * @author hccake + * @date 2020-03-26 18:40:20 + */ +public interface SysDictService extends ExtendService { + + /** + * 根据QueryObject查询分页数据 + * @param pageParam 分页参数 + * @param qo 查询参数对象 + * @return PageResult 分页数据 + */ + PageResult queryPage(PageParam pageParam, SysDictQO qo); + + /** + * 根据字典标识查询 + * @param dictCode 字典标识 + * @return 字典数据 + */ + SysDict getByCode(String dictCode); + + /** + * 根据字典标识数组查询对应字典集合 + * @param dictCodes 字典标识数组 + * @return List 字典集合 + */ + List listByCodes(String[] dictCodes); + + /** + * 更新字典HashCode + * @param dictCode 字典标识 + * @return 更新状态 成功(true) or 失败 (false) + */ + boolean updateHashCode(String dictCode); + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysMenuService.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysMenuService.java new file mode 100644 index 0000000..f5dbc82 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysMenuService.java @@ -0,0 +1,45 @@ +package com.baiye.system.service; + +import com.baiye.system.model.dto.SysMenuCreateDTO; +import com.baiye.system.model.dto.SysMenuUpdateDTO; +import com.baiye.system.model.qo.SysMenuQO; +import com.baiye.system.model.entity.SysMenu; +import com.baiye.extend.mybatis.plus.service.ExtendService; + +import java.util.List; + +/** + * 菜单权限 + * + * @author hccake 2021-04-06 17:59:51 + */ +public interface SysMenuService extends ExtendService { + + /** + * 更新菜单权限 + * @param sysMenuUpdateDTO 菜单权限修改DTO + */ + void update(SysMenuUpdateDTO sysMenuUpdateDTO); + + /** + * 查询权限集合,并按sort排序(升序) + * @param sysMenuQO 查询条件 + * @return List + */ + List listOrderBySort(SysMenuQO sysMenuQO); + + /** + * 根据角色标识查询对应的菜单 + * @param roleCode 角色标识 + * @return List + */ + List listByRoleCode(String roleCode); + + /** + * 新建菜单权限 + * @param sysMenuCreateDTO 菜单全新新建传输对象 + * @return 新建成功返回 true + */ + boolean create(SysMenuCreateDTO sysMenuCreateDTO); + +} \ No newline at end of file diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysOrganizationService.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysOrganizationService.java new file mode 100644 index 0000000..6a82272 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysOrganizationService.java @@ -0,0 +1,59 @@ +package com.baiye.system.service; + +import com.baiye.system.model.qo.SysOrganizationQO; +import com.baiye.system.model.vo.SysOrganizationTree; +import com.baiye.system.model.dto.SysOrganizationDTO; +import com.baiye.system.model.entity.SysOrganization; +import com.baiye.extend.mybatis.plus.service.ExtendService; + +import java.util.List; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 12:09:43 + */ +public interface SysOrganizationService extends ExtendService { + + /** + * 返回组织架构的树形结构 + * @param sysOrganizationQO 组织机构查询条件 + * @return OrganizationTree + */ + List listTree(SysOrganizationQO sysOrganizationQO); + + /** + * 创建一个新的组织机构 + * @param sysOrganizationDTO 组织机构DTO + * @return boolean 创建成功/失败 + */ + boolean create(SysOrganizationDTO sysOrganizationDTO); + + /** + * 更新一个已有的组织机构 + * @param sysOrganizationDTO 组织机构DTO + * @return boolean 更新成功/失败 + */ + boolean update(SysOrganizationDTO sysOrganizationDTO); + + /** + * 根据组织ID 查询除该组织下的所有儿子组织 + * @param organizationId 组织机构ID + * @return List 该组织的儿子组织 + */ + List listSubOrganization(Long organizationId); + + /** + * 根据组织ID 查询除该组织下的所有孩子(子孙)组织 + * @param organizationId 组织机构ID + * @return List 该组织的孩子组织 + */ + List listChildOrganization(Long organizationId); + + /** + * 校正组织机构层级和深度 + * @return 校正是否成功 + */ + boolean revisedHierarchyAndPath(); + +} \ No newline at end of file diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysRoleMenuService.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysRoleMenuService.java new file mode 100644 index 0000000..9d8ebce --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysRoleMenuService.java @@ -0,0 +1,46 @@ +package com.baiye.system.service; + +import com.baiye.system.model.entity.SysRoleMenu; +import com.baiye.extend.mybatis.plus.service.ExtendService; + +import java.io.Serializable; + +/** + *

          + * 角色菜单表 服务类 + *

          + * + * @author hccake + * @since 2017-10-29 + */ +public interface SysRoleMenuService extends ExtendService { + + /** + * 更新角色菜单 + * @param roleCode 角色 + * @param menuIds 权限ID数组 + * @return 更新角色权限关联关系是否成功 + */ + Boolean saveRoleMenus(String roleCode, Long[] menuIds); + + /** + * 根据权限ID删除角色权限关联数据 + * @param menuId 权限ID + */ + void deleteByMenuId(Serializable menuId); + + /** + * 根据角色标识删除角色权限关联关系 + * @param roleCode 角色标识 + */ + void deleteByRoleCode(String roleCode); + + /** + * 更新某个菜单的 id + * @param originalId 原菜单ID + * @param menuId 修改后的菜单Id + * @return 被更新的菜单数 + */ + int updateMenuId(Long originalId, Long menuId); + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysRoleService.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysRoleService.java new file mode 100644 index 0000000..ccb29bf --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysRoleService.java @@ -0,0 +1,44 @@ +package com.baiye.system.service; + +import com.baiye.system.model.entity.SysRole; +import com.baiye.system.model.qo.SysRoleQO; +import com.baiye.system.model.vo.SysRolePageVO; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.domain.SelectData; +import com.baiye.extend.mybatis.plus.service.ExtendService; + +import java.util.List; + +/** + *

          + * 系统角色服务类 + *

          + * + * @author hccake + * @since 2020-01-12 + */ +public interface SysRoleService extends ExtendService { + + /** + * 查询系统角色列表 + * @param pageParam 分页参数 + * @param qo 查询参数 + * @return 分页对象 + */ + PageResult queryPage(PageParam pageParam, SysRoleQO qo); + + /** + * 角色的选择数据 + * @return 角色下拉列表数据集合 + */ + List> listSelectData(); + + /** + * 是否存在角色code + * @param roleCode 角色code + * @return boolean 是否存在 + */ + boolean existsRoleCode(String roleCode); + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysUserRolePromiseService.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysUserRolePromiseService.java new file mode 100644 index 0000000..5f3036f --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysUserRolePromiseService.java @@ -0,0 +1,57 @@ +package com.baiye.system.service; + +import com.baiye.extend.mybatis.plus.service.ExtendService; +import com.baiye.system.model.dto.SysUserScope; +import com.baiye.system.model.entity.SysUserRolePromise; + +import java.util.List; + +/** + * @author Hccake + * + * + * 用户角色关联表 + */ +public interface SysUserRolePromiseService extends ExtendService { + + + /** + * 删除用户的角色 + * @param userId 用户ID + * @return 删除是否程 + */ + boolean deleteByUserId(Long userId); + + /** + * 添加用户角色关联关系 + * + * @param userId 用户ID + * @param roleCodes 角色标识集合 + * @param isAutoAudits + * @param isShowNid + * @param num + * @return 插入是否成功 + */ + boolean addUserRolesPromise(Long userId, List roleCodes, Boolean isAutoAudits, Boolean isShowNid, Integer num); + + + + /** + * 更新用户关联关系 + * + * @param userId 用户ID + * @param scope 角色标识集合 + * @return boolean + */ + boolean updateUserRolePromise(Long userId, SysUserScope scope); + + /** + * 用户id查询数据 + * @param userId + * @return + */ + SysUserRolePromise queryIsShowByUserId(Long userId); + + + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysUserRoleService.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysUserRoleService.java new file mode 100644 index 0000000..9456bd7 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysUserRoleService.java @@ -0,0 +1,77 @@ +package com.baiye.system.service; + +import com.baiye.system.model.dto.SysUserScope; +import com.baiye.system.model.entity.SysRole; +import com.baiye.system.model.entity.SysUserRole; +import com.baiye.system.model.vo.RoleBindUserVO; +import com.baiye.system.model.qo.RoleBindUserQO; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.ExtendService; + +import java.util.List; + +/** + * @author Hccake + * + * 用户角色关联表 + */ +public interface SysUserRoleService extends ExtendService { + + /** + * 删除用户的角色 + * @param userId 用户ID + * @return 删除是否程 + */ + boolean deleteByUserId(Long userId); + + /** + * 更新用户关联关系 + * + * @param userId 用户ID + * @param scope 角色标识集合 + * @return boolean + */ + boolean updateUserRoles(Long userId, SysUserScope scope); + + /** + * 添加用户角色关联关系 + * @param userId 用户ID + * @param roleCodes 角色标识集合 + * @return 插入是否成功 + */ + boolean addUserRoles(Long userId, List roleCodes); + + /** + * 通过用户ID,查询角色列表 + * @param userId 用户ID + * @return List + */ + List listRoles(Long userId); + + /** + * 通过角色标识,查询用户列表 + * @param pageParam 分页参数 + * @param roleCode 角色标识 + * @return PageResult 角色授权的用户列表 + */ + PageResult queryUserPageByRoleCode(PageParam pageParam, RoleBindUserQO roleCode); + + /** + * 解绑角色和用户关系 + * @param userId 用户ID + * @param roleCode 角色标识 + * @return 解绑成功:true + */ + boolean unbindRoleUser(Long userId, String roleCode); + + + + /** + * 用户id查询指定 + * @param baseUserId + * @param roleCode + * @return + */ + List selectUserIdByRoleCode(Long baseUserId, String roleCode); +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysUserService.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysUserService.java new file mode 100644 index 0000000..8014f9c --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/SysUserService.java @@ -0,0 +1,239 @@ +package com.baiye.system.service; + +import com.baiye.system.model.dto.SysUserDTO; +import com.baiye.system.model.dto.SysUserNameRoleDTO; +import com.baiye.system.model.dto.SysUserScope; +import com.baiye.system.model.dto.UserInfoDTO; +import com.baiye.system.model.qo.SysUserQO; +import com.baiye.system.model.vo.SysUserPageVO; +import com.baiye.system.model.entity.SysUser; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.domain.SelectData; +import com.baiye.extend.mybatis.plus.service.ExtendService; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 系统用户表 + * + * @author ballcat code generator 2019-09-12 20:39:31 + */ +public interface SysUserService extends ExtendService { + + /** + * 根据QueryObject查询系统用户列表 + * + * @param pageParam 分页参数 + * @param qo 查询参数对象 + * @return PageResult 分页数据 + */ + PageResult queryPage(PageParam pageParam, SysUserQO qo); + + PageResult queryAll(PageParam pageParam, SysUserQO qo); + + /** + * 根据用户名查询用户 + * + * @param username 用户名 + * @return SysUser + */ + SysUser getByUsername(String username); + + /** + * 获取用户详情信息 + * + * @param user SysUser + * @return UserInfoDTO + */ + UserInfoDTO findUserInfo(SysUser user); + + /** + * 新增系统用户 + * + * @param sysUserDto SysUserDTO + * @return boolean + */ + boolean addSysUser(SysUserDTO sysUserDto); + + /** + * 更新系统用户信息 + * + * @param sysUserDTO 用户DTO + * @return boolean + */ + boolean updateSysUser(SysUserDTO sysUserDTO); + + /** + * 更新用户权限信息 + * + * @param userId 用户ID + * @param sysUserScope 用户权限域 + * @return boolean + */ + boolean updateUserScope(Long userId, SysUserScope sysUserScope); + + /** + * 根据userId删除 用户 + * + * @param userId 用户ID + * @return boolean + */ + boolean deleteByUserId(Long userId); + + /** + * 修改用户密码 + * + * @param userId 用户ID + * @param password 明文密码 + * @return boolean + */ + boolean updatePassword(Long userId, String password); + + /** + * 批量修改用户状态 + * + * @param userIds 用户ID集合 + * @param status 状态 + * @return boolean + */ + boolean updateUserStatusBatch(Collection userIds, Integer status); + + /** + * 修改系统用户头像 + * + * @param file 头像文件 + * @param userId 用户ID + * @return 文件相对路径 + * @throws IOException IO异常 + */ + String updateAvatar(MultipartFile file, Long userId) throws IOException; + + /** + * 根据角色查询用户 + * + * @param roleCode 角色标识 + * @return List + */ + List listByRoleCode(String roleCode); + + /** + * 查询用户下角色子用户 + * + * @param roleCode 角色标识 + * @param userId 用户id + * @return List + */ + List listByRoleCodeAndUserId(String roleCode, Long userId); + + /** + * 根据用户id查询用户名称 + * + * @param userIds 用户id + * @return List + */ + List listUserNameByUserIds(List userIds); + + /** + * 根据角色查询用户 + * + * @param roleCodes 角色标识集合 + * @return List 用户集合 + */ + List listByRoleCodes(Collection roleCodes); + + /** + * 根据组织机构ID查询用户 + * + * @param organizationIds 组织机构id集合 + * @return 用户集合 + */ + List listByOrganizationIds(Collection organizationIds); + + /** + * 根据用户类型查询用户 + * + * @param userTypes 用户类型集合 + * @return 用户集合 + */ + List listByUserTypes(Collection userTypes); + + /** + * 根据用户Id集合查询用户 + * + * @param userIds 用户Id集合 + * @return 用户集合 + */ + List listByUserIds(Collection userIds); + + /** + * 返回用户的select数据 + * + * @param type 为空时返回所有客户为1返回系统客户 name=> username value => userId + * @return List + */ + List> listSelectData(Collection type); + + /** + * 获取用户的角色Code集合 + * + * @param userId 用户id + * @return List + */ + List listRoleCodes(Long userId); + + /** + * 是否存在指定组织的用户 + * + * @param organizationId 组织 id + * @return boolean 存在返回 true + */ + boolean existsForOrganization(Long organizationId); + + /** + * 获取用户下级下拉列表数据 + */ + List> listSelectSonListData(); + + /** + * 根据id查询 + * + * @param id + * @return + */ + SysUser findById(Long id); + + /** + * 余额数量检测-扣减 总分发量计数 + * @param map 用户分发数量 + * @return true余额充足 false余额不足 + */ + boolean detectionAvailableNumUpdate(Map map); + + /** + * 余额数量检测 + * @return nickname:余额不足的用户昵称 null:查询用户都有余额 + */ + String detectionAvailableNum(List userIds); + + /** + * 余额充值 + */ + boolean recharge(SysUserDTO sysUserDTO); + + /** + * 扣减数量 + */ + Boolean deductionQuantity(Long userId, String storeName); + + /** + * 回撤数量 + * @param assignedBy + * @return + */ + Boolean addQuantity(Long assignedBy); +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysConfigServiceImpl.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysConfigServiceImpl.java new file mode 100644 index 0000000..df2f08d --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysConfigServiceImpl.java @@ -0,0 +1,61 @@ +package com.baiye.system.service.impl; + +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.core.annotation.CacheDel; +import com.baiye.core.annotation.Cached; +import com.baiye.system.constant.SystemRedisKeyConstants; +import com.baiye.system.mapper.SysConfigMapper; +import com.baiye.system.model.entity.SysConfig; +import com.baiye.system.model.qo.SysConfigQO; +import com.baiye.system.model.vo.SysConfigPageVO; +import com.baiye.system.service.SysConfigService; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import org.springframework.stereotype.Service; + +/** + * 系统配置表 + * + * @author ballcat code generator + * @date 2019-10-14 17:42:23 + */ +@Service +public class SysConfigServiceImpl extends ExtendServiceImpl implements SysConfigService { + + @Override + public PageResult queryPage(PageParam pageParam, SysConfigQO sysConfigQO) { + return baseMapper.queryPage(pageParam, sysConfigQO); + } + + @Cached(key = SystemRedisKeyConstants.SYSTEM_CONFIG_PREFIX, keyJoint = "#confKey") + @Override + public String getConfValueByKey(String confKey) { + SysConfig sysConfig = baseMapper.selectByKey(confKey); + return sysConfig == null ? null : sysConfig.getConfValue(); + } + + /** + * 保存系统配置,由于查询不到时会缓存空值,所以新建时也需要删除对应 key,防止之前误存了空值数据 + * @param entity 实体对象 + * @return 保存成功 true + */ + @CacheDel(key = SystemRedisKeyConstants.SYSTEM_CONFIG_PREFIX, keyJoint = "#p0.confKey") + @Override + public boolean save(SysConfig entity) { + return SqlHelper.retBool(getBaseMapper().insert(entity)); + } + + @CacheDel(key = SystemRedisKeyConstants.SYSTEM_CONFIG_PREFIX, keyJoint = "#sysConfig.confKey") + @Override + public boolean updateByKey(SysConfig sysConfig) { + return baseMapper.updateByKey(sysConfig); + } + + @CacheDel(key = SystemRedisKeyConstants.SYSTEM_CONFIG_PREFIX, keyJoint = "#confKey") + @Override + public boolean removeByKey(String confKey) { + return baseMapper.deleteByKey(confKey); + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysDictItemServiceImpl.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysDictItemServiceImpl.java new file mode 100644 index 0000000..ad5b807 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysDictItemServiceImpl.java @@ -0,0 +1,69 @@ +package com.baiye.system.service.impl; + +import com.baiye.system.mapper.SysDictItemMapper; +import com.baiye.system.model.entity.SysDictItem; +import com.baiye.system.model.vo.SysDictItemPageVO; +import com.baiye.system.service.SysDictItemService; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 字典项 + * + * @author hccake + * @date 2020-03-26 18:40:20 + */ +@Service +public class SysDictItemServiceImpl extends ExtendServiceImpl + implements SysDictItemService { + + /** + * 根据QueryObject查询分页数据 + * @param pageParam 分页参数 + * @param dictCode 字典标识 + * @return 分页数据 + */ + @Override + public PageResult queryPage(PageParam pageParam, String dictCode) { + return baseMapper.queryPage(pageParam, dictCode); + } + + /** + * 根据Code查询对应字典项数据 + * @param dictCode 字典标识 + * @return 字典项集合 + */ + @Override + public List listByDictCode(String dictCode) { + return baseMapper.listByDictCode(dictCode); + } + + /** + * 根据字典标识删除对应字典项 + * @param dictCode 字典标识 + * @return 是否删除成功 + */ + @Override + public boolean removeByDictCode(String dictCode) { + // 如果存在字典项则进行删除 + if (baseMapper.existsDictItem(dictCode)) { + return baseMapper.deleteByDictCode(dictCode); + } + return true; + } + + /** + * 判断字典项是否存在 + * @param dictCode 字典标识 + * @return 存在返回 true + */ + @Override + public boolean exist(String dictCode) { + return baseMapper.existsDictItem(dictCode); + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysDictServiceImpl.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysDictServiceImpl.java new file mode 100644 index 0000000..f135308 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysDictServiceImpl.java @@ -0,0 +1,70 @@ +package com.baiye.system.service.impl; + +import cn.hutool.core.util.IdUtil; +import com.baiye.system.mapper.SysDictMapper; +import com.baiye.system.model.entity.SysDict; +import com.baiye.system.model.qo.SysDictQO; +import com.baiye.system.model.vo.SysDictPageVO; +import com.baiye.system.service.SysDictService; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * 字典表 + * + * @author hccake + * @date 2020-03-26 18:40:20 + */ +@Service +public class SysDictServiceImpl extends ExtendServiceImpl implements SysDictService { + + /** + * 根据QueryObject查询分页数据 + * @param pageParam 分页参数 + * @param qo 查询参数对象 + * @return PageResult 分页数据 + */ + @Override + public PageResult queryPage(PageParam pageParam, SysDictQO qo) { + return baseMapper.queryPage(pageParam, qo); + } + + /** + * 根据字典标识查询 + * @param dictCode 字典标识 + * @return 字典数据 + */ + @Override + public SysDict getByCode(String dictCode) { + return baseMapper.getByCode(dictCode); + } + + /** + * 根据字典标识数组查询对应字典集合 + * @param dictCodes 字典标识数组 + * @return List 字典集合 + */ + @Override + public List listByCodes(String[] dictCodes) { + if (dictCodes == null || dictCodes.length == 0) { + return new ArrayList<>(); + } + return baseMapper.listByCodes(dictCodes); + } + + /** + * 更新字典HashCode + * @param dictCode 字典标识 + * @return 更新状态: 成功(true) 失败(false) + */ + @Override + public boolean updateHashCode(String dictCode) { + return baseMapper.updateHashCode(dictCode, IdUtil.fastSimpleUUID()); + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysMenuServiceImpl.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysMenuServiceImpl.java new file mode 100644 index 0000000..b28bebd --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,146 @@ +package com.baiye.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.baiye.system.converter.SysMenuConverter; +import com.baiye.system.mapper.SysMenuMapper; +import com.baiye.system.model.dto.SysMenuCreateDTO; +import com.baiye.system.model.dto.SysMenuUpdateDTO; +import com.baiye.system.model.qo.SysMenuQO; +import com.baiye.system.service.SysMenuService; +import com.baiye.system.service.SysRoleMenuService; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.baiye.exception.BusinessException; +import com.baiye.I18nMessage; +import com.baiye.I18nMessageCreateEvent; +import com.baiye.result.BaseResultCode; +import com.baiye.system.model.entity.SysMenu; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.List; + +/** + * 菜单权限 + * + * @author hccake 2021-04-06 17:59:51 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysMenuServiceImpl extends ExtendServiceImpl implements SysMenuService { + + private final SysRoleMenuService sysRoleMenuService; + + private final ApplicationEventPublisher eventPublisher; + + /** + * 插入一条记录(选择字段,策略插入) + * @param sysMenu 实体对象 + */ + @Override + public boolean save(SysMenu sysMenu) { + Long menuId = sysMenu.getId(); + SysMenu existingMenu = baseMapper.selectById(menuId); + if (existingMenu != null) { + String errorMessage = String.format("ID [%s] 已被菜单 [%s] 使用,请更换其他菜单ID", menuId, existingMenu.getTitle()); + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), errorMessage); + } + return SqlHelper.retBool(baseMapper.insert(sysMenu)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean create(SysMenuCreateDTO sysMenuCreateDTO) { + + SysMenu sysMenu = SysMenuConverter.INSTANCE.createDtoToPo(sysMenuCreateDTO); + Long menuId = sysMenu.getId(); + if (menuId != null) { + SysMenu existingMenu = baseMapper.selectById(menuId); + if (existingMenu != null) { + String errorMessage = String.format("ID [%s] 已被菜单 [%s] 使用,请更换其他菜单ID", menuId, existingMenu.getTitle()); + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), errorMessage); + } + } + + boolean saveSuccess = SqlHelper.retBool(baseMapper.insert(sysMenu)); + Assert.isTrue(saveSuccess, () -> { + log.error("[create] 创建菜单失败,sysMenuCreateDTO: {}", sysMenuCreateDTO); + return new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "创建菜单失败"); + }); + + // 多语言保存事件发布 + List i18nMessages = sysMenuCreateDTO.getI18nMessages(); + if (CollUtil.isNotEmpty(i18nMessages)) { + eventPublisher.publishEvent(new I18nMessageCreateEvent(i18nMessages)); + } + + return saveSuccess; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean removeById(Serializable id) { + // 查询当前权限是否有子权限 + Long subMenu = baseMapper.countSubMenu(id); + if (subMenu != null && subMenu > 0) { + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), "菜单含有下级不能删除"); + } + // 删除角色权限关联数据 + sysRoleMenuService.deleteByMenuId(id); + // 删除当前菜单及其子菜单 + return SqlHelper.retBool(baseMapper.deleteById(id)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(SysMenuUpdateDTO sysMenuUpdateDTO) { + // 原来的菜单 Id + Long originalId = sysMenuUpdateDTO.getOriginalId(); + SysMenu sysMenu = SysMenuConverter.INSTANCE.updateDtoToPo(sysMenuUpdateDTO); + + // 更新菜单信息 + boolean updateSuccess = baseMapper.updateMenuAndId(originalId, sysMenu); + Assert.isTrue(updateSuccess, () -> { + log.error("[update] 更新菜单权限时,sql 执行异常,originalId:{},sysMenu:{}", originalId, sysMenu); + return new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "更新菜单权限时,sql 执行异常"); + }); + + // 如果未修改过 菜单id 直接返回 + Long menuId = sysMenuUpdateDTO.getId(); + if (originalId.equals(menuId)) { + return; + } + + // 修改过菜单id,则需要对应修改角色菜单的关联表信息,这里不需要 check,因为可能没有授权过该菜单,所以返回值为 0 + sysRoleMenuService.updateMenuId(originalId, menuId); + // 更新子菜单的 parentId + baseMapper.updateParentId(originalId, menuId); + } + + /** + * 查询权限集合,并按sort排序(升序) + * @param sysMenuQO 查询条件 + * @return List + */ + @Override + public List listOrderBySort(SysMenuQO sysMenuQO) { + return baseMapper.listOrderBySort(sysMenuQO); + } + + /** + * 根据角色标识查询对应的菜单 + * @param roleCode 角色标识 + * @return List + */ + @Override + public List listByRoleCode(String roleCode) { + return baseMapper.listByRoleCode(roleCode); + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysOrganizationServiceImpl.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysOrganizationServiceImpl.java new file mode 100644 index 0000000..8db68f3 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysOrganizationServiceImpl.java @@ -0,0 +1,237 @@ +package com.baiye.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ArrayUtil; +import com.baiye.system.mapper.SysOrganizationMapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.baiye.constant.GlobalConstants; +import com.baiye.exception.BusinessException; +import com.baiye.result.BaseResultCode; +import com.baiye.util.tree.TreeUtils; +import com.baiye.system.converter.SysOrganizationConverter; +import com.baiye.system.model.dto.OrganizationMoveChildParam; +import com.baiye.system.model.dto.SysOrganizationDTO; +import com.baiye.system.model.entity.SysOrganization; +import com.baiye.system.model.qo.SysOrganizationQO; +import com.baiye.system.model.vo.SysOrganizationTree; +import com.baiye.system.service.SysOrganizationService; +import com.baiye.system.service.SysUserService; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 12:09:43 + */ +@Service +@RequiredArgsConstructor +public class SysOrganizationServiceImpl extends ExtendServiceImpl + implements SysOrganizationService { + + private final SysUserService sysUserService; + + /** + * 返回组织架构的树形结构 + * @param sysOrganizationQO 组织机构查询条件 + * @return OrganizationTree + */ + @Override + public List listTree(SysOrganizationQO sysOrganizationQO) { + List list = this.list(); + List tree = TreeUtils.buildTree(list, GlobalConstants.TREE_ROOT_ID_LONG, + SysOrganizationConverter.INSTANCE::poToTree); + + // 如果有名称的查询条件,则进行剪枝操作 + String name = sysOrganizationQO.getName(); + if (CharSequenceUtil.isNotEmpty(name)) { + return TreeUtils.pruneTree(tree, node -> node.getName() != null && node.getName().contains(name)); + } + + return tree; + } + + /** + * 创建一个新的组织机构 + * @param sysOrganizationDTO 组织机构DTO + * @return boolean 创建成功/失败 + */ + @Override + public boolean create(SysOrganizationDTO sysOrganizationDTO) { + sysOrganizationDTO.setId(null); + SysOrganization sysOrganization = SysOrganizationConverter.INSTANCE.dtoToPo(sysOrganizationDTO); + + // 如果父级为根节点则直接设置深度和层级,否则根据父节点数据动态设置 + Long parentId = sysOrganizationDTO.getParentId(); + // 填充层级和深度 + fillDepthAndHierarchy(sysOrganization, parentId); + + return SqlHelper.retBool(baseMapper.insert(sysOrganization)); + } + + /** + * 更新一个已有的组织机构 + * @param sysOrganizationDTO 组织机构DTO + * @return boolean 更新成功/失败 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean update(SysOrganizationDTO sysOrganizationDTO) { + // TODO 防止并发问题 + SysOrganization newSysOrganization = SysOrganizationConverter.INSTANCE.dtoToPo(sysOrganizationDTO); + Long organizationId = newSysOrganization.getId(); + SysOrganization originSysOrganization = baseMapper.selectById(organizationId); + + // 如果没有移动父节点,则直接更新 + Long targetParentId = sysOrganizationDTO.getParentId(); + if (originSysOrganization.getParentId().equals(targetParentId)) { + return SqlHelper.retBool(baseMapper.updateById(newSysOrganization)); + } + + // 移动了父节点,先判断不是选择自己作为父节点 + Assert.isFalse(targetParentId.equals(organizationId), "父节点不能是自己!"); + // 再判断是否是自己的子节点,根节点跳过判断 + if (!GlobalConstants.TREE_ROOT_ID_LONG.equals(targetParentId)) { + SysOrganization targetParentOrganization = baseMapper.selectById(targetParentId); + String[] targetParentHierarchy = targetParentOrganization.getHierarchy().split("-"); + if (ArrayUtil.contains(targetParentHierarchy, String.valueOf(organizationId))) { + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), "父节点不能是自己的子节点!"); + } + } + + // 填充目标层级和深度 + fillDepthAndHierarchy(newSysOrganization, targetParentId); + // 更新其子节点的数据 + OrganizationMoveChildParam param = getOrganizationMoveChildParam(newSysOrganization, originSysOrganization); + baseMapper.followMoveChildNode(param); + // 更新组织节点信息 + return SqlHelper.retBool(baseMapper.updateById(newSysOrganization)); + } + + private OrganizationMoveChildParam getOrganizationMoveChildParam(SysOrganization newSysOrganization, + SysOrganization originSysOrganization) { + // 父组织 id + Long parentId = newSysOrganization.getId(); + // 父节点原来的层级 + String originParentHierarchy = originSysOrganization.getHierarchy(); + // 修改后的父节点层级 + String targetParentHierarchy = newSysOrganization.getHierarchy(); + // 父节点移动后的深度差 + int depthDiff = originSysOrganization.getDepth() - newSysOrganization.getDepth(); + + OrganizationMoveChildParam param = new OrganizationMoveChildParam(); + param.setParentId(parentId); + param.setOriginParentHierarchy(originParentHierarchy); + param.setOriginParentHierarchyLengthPlusOne(originParentHierarchy.length() + 1); + param.setTargetParentHierarchy(targetParentHierarchy); + param.setDepthDiff(depthDiff); + param.setGrandsonConditionalStatement(originParentHierarchy + "-" + parentId + "-%"); + return param; + } + + /** + * 根据组织ID 查询除该组织下的所有儿子组织 + * @param organizationId 组织机构ID + * @return List 该组织的儿子组织 + */ + @Override + public List listSubOrganization(Long organizationId) { + return baseMapper.listSubOrganization(organizationId); + } + + /** + * 根据组织ID 查询除该组织下的所有孩子(子孙)组织 + * @param organizationId 组织机构ID + * @return List 该组织的孩子组织 + */ + @Override + public List listChildOrganization(Long organizationId) { + return baseMapper.listChildOrganization(organizationId); + } + + /** + * 校正组织机构层级和深度 + * @return 校正是否成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean revisedHierarchyAndPath() { + // 组织机构一般数据量不多,一次性查询出来缓存到内存中,减少查询开销 + List sysOrganizations = baseMapper.selectList(Wrappers.emptyWrapper()); + Map> map = sysOrganizations.stream() + .collect(Collectors.groupingBy(SysOrganization::getParentId)); + // 默认的父节点为根节点, + Long parentId = GlobalConstants.TREE_ROOT_ID_LONG; + int depth = 1; + String hierarchy = "0"; + updateChildHierarchyAndPath(map, parentId, depth, hierarchy); + + return true; + } + + private void updateChildHierarchyAndPath(Map> map, Long parentId, int depth, + String hierarchy) { + // 获取对应 parentId 下的所有子节点 + List sysOrganizations = map.get(parentId); + if (CollUtil.isEmpty(sysOrganizations)) { + return; + } + // 递归更新子节点数据 + List childrenIds = new ArrayList<>(); + for (SysOrganization sysOrganization : sysOrganizations) { + Long organizationId = sysOrganization.getId(); + updateChildHierarchyAndPath(map, organizationId, depth + 1, hierarchy + "-" + organizationId); + childrenIds.add(organizationId); + } + baseMapper.updateHierarchyAndPathBatch(depth, hierarchy, childrenIds); + } + + /** + * 根据组织ID 删除组织机构 + * @param id 组织机构ID + * @return 删除成功: true + */ + @Override + public boolean removeById(Serializable id) { + Long organizationId = (Long) id; + Boolean existsChildOrganization = baseMapper.existsChildOrganization(organizationId); + if (Boolean.TRUE.equals(existsChildOrganization)) { + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), "该组织机构拥有下级组织,不能删除!"); + } + if (sysUserService.existsForOrganization(organizationId)) { + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), "该组织机构拥有关联用户,不能删除!"); + } + return SqlHelper.retBool(baseMapper.deleteById(id)); + } + + /** + * 根据父级ID填充当前组织机构实体的深度和层级 + * @param sysOrganization 组织机构实体 + * @param parentId 父级ID + */ + private void fillDepthAndHierarchy(SysOrganization sysOrganization, Long parentId) { + if (GlobalConstants.TREE_ROOT_ID_LONG.equals(parentId)) { + sysOrganization.setDepth(1); + sysOrganization.setHierarchy(GlobalConstants.TREE_ROOT_ID_LONG.toString()); + } + else { + SysOrganization parentSysOrganization = baseMapper.selectById(parentId); + Assert.notNull(parentSysOrganization, "不存在的父级组织机构!"); + + sysOrganization.setDepth(parentSysOrganization.getDepth() + 1); + sysOrganization.setHierarchy(parentSysOrganization.getHierarchy() + "-" + parentSysOrganization.getId()); + } + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysRoleMenuServiceImpl.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysRoleMenuServiceImpl.java new file mode 100644 index 0000000..bd9b615 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysRoleMenuServiceImpl.java @@ -0,0 +1,79 @@ + +package com.baiye.system.service.impl; + +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.baiye.system.mapper.SysRoleMenuMapper; +import com.baiye.system.model.entity.SysRoleMenu; +import com.baiye.system.service.SysRoleMenuService; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + *

          + * 角色菜单表 服务实现类 + *

          + * + * @author hccake + */ +@Service +public class SysRoleMenuServiceImpl extends ExtendServiceImpl + implements SysRoleMenuService { + + /** + * @param roleCode 角色 + * @param menuIds 权限ID集合 + * @return boolean + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean saveRoleMenus(String roleCode, Long[] menuIds) { + // 1、先删除旧数据 + baseMapper.deleteByRoleCode(roleCode); + if (menuIds == null || menuIds.length == 0) { + return Boolean.TRUE; + } + + // 2、再批量插入新数据 + List list = Arrays.stream(menuIds) + .map(menuId -> new SysRoleMenu(roleCode, menuId)) + .collect(Collectors.toList()); + int i = baseMapper.insertBatchSomeColumn(list); + return SqlHelper.retBool(i); + } + + /** + * 根据权限ID删除角色权限关联数据 + * @param menuId 权限ID + */ + @Override + public void deleteByMenuId(Serializable menuId) { + baseMapper.deleteByMenuId(menuId); + } + + /** + * 根据角色标识删除角色权限关联关系 + * @param roleCode 角色标识 + */ + @Override + public void deleteByRoleCode(String roleCode) { + baseMapper.deleteByRoleCode(roleCode); + } + + /** + * 更新某个菜单的 id + * @param originalId 原菜单ID + * @param menuId 修改后的菜单Id + * @return 被更新的菜单数 + */ + @Override + public int updateMenuId(Long originalId, Long menuId) { + return baseMapper.updateMenuId(originalId, menuId); + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysRoleServiceImpl.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..254358b --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,94 @@ +package com.baiye.system.service.impl; + +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.domain.SelectData; +import com.baiye.exception.BusinessException; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import com.baiye.result.BaseResultCode; +import com.baiye.system.mapper.SysRoleMapper; +import com.baiye.system.model.entity.SysRole; +import com.baiye.system.model.qo.SysRoleQO; +import com.baiye.system.model.vo.SysRolePageVO; +import com.baiye.system.service.SysRoleMenuService; +import com.baiye.system.service.SysRoleService; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class SysRoleServiceImpl extends ExtendServiceImpl implements SysRoleService { + + private final SysRoleMenuService sysRoleMenuService; + + + /** + * 查询系统角色列表 + * + * @param pageParam 分页对象 + * @param qo 查询参数 + * @return 分页对象 + */ + @Override + public PageResult queryPage(PageParam pageParam, SysRoleQO qo) { + return baseMapper.queryPage(pageParam, qo); + } + + /** + * 通过角色ID,删除角色,并清空角色菜单缓存 + * + * @param id 角色ID + * @return boolean + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean removeById(Serializable id) { + SysRole role = getById(id); + sysRoleMenuService.deleteByRoleCode(role.getCode()); + return SqlHelper.retBool(baseMapper.deleteById(id)); + } + + /** + * 角色的选择数据 + * + * @return List> + */ + @Override + public List> listSelectData() { + List> selectData = baseMapper.listSelectData(); + + return selectData.stream().filter(c -> "2".equals(c.getType())).collect(Collectors.toList()); + } + + /** + * 是否存在角色code + * + * @param roleCode 角色code + * @return boolean 是否存在 + */ + @Override + public boolean existsRoleCode(String roleCode) { + return baseMapper.existsRoleCode(roleCode); + } + + /** + * 新增角色 + * + * @param sysRole 角色对象 + * @return boolean 是否新增成功 + */ + @Override + public boolean save(SysRole sysRole) { + if (existsRoleCode(sysRole.getCode())) { + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR, "角色标识已存在!"); + } + return SqlHelper.retBool(getBaseMapper().insert(sysRole)); + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysUserRolePromiseServiceImpl.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysUserRolePromiseServiceImpl.java new file mode 100644 index 0000000..659e201 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysUserRolePromiseServiceImpl.java @@ -0,0 +1,126 @@ +package com.baiye.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.exception.BusinessException; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import com.baiye.extend.mybatis.plus.toolkit.WrappersX; +import com.baiye.result.BaseResultCode; +import com.baiye.security.util.SecurityUtils; +import com.baiye.system.mapper.SysUserRolePromiseMapper; +import com.baiye.system.model.dto.SysUserScope; +import com.baiye.system.model.entity.SysUserRolePromise; +import com.baiye.system.service.SysUserRolePromiseService; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 用户角色关联关系表 + * + * @author Hccake + */ +@Slf4j +@Service +public class SysUserRolePromiseServiceImpl extends ExtendServiceImpl + implements SysUserRolePromiseService { + + /** + * 根据UserId删除该用户角色关联关系 + * + * @param userId 用户ID + * @return boolean + */ + @Override + public boolean deleteByUserId(Long userId) { + return baseMapper.deleteByUserId(userId); + } + + /** + * 更新用户关联关系 + * + * @param userId 用户ID + * @param scope 角色标识集合 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateUserRolePromise(@NonNull Long userId, @NonNull SysUserScope scope) { + // 是否存在用户角色绑定关系,存在则先清空 + boolean existsRoleBind = baseMapper.existsRoleBind(userId, null); + if (existsRoleBind) { + boolean deleteSuccess = baseMapper.deleteByUserId(userId); + Assert.isTrue(deleteSuccess, () -> { + log.error("[updateUserRoles] 删除用户角色关联关系失败,userId:{},roleCodes:{}", userId, scope); + return new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "删除用户角色关联关系失败"); + }); + } + + if (Boolean.TRUE.equals(scope.getIsAutoAudits()) || Boolean.TRUE.equals(scope.getIsShowNid())) { + return addUserRolesPromise(userId, scope.getRoleCodes(), scope.getIsAutoAudits(), scope.getIsShowNid(), Boolean.TRUE.equals + (scope.getIsAutoAudits()) ? DefaultNumberConstants.ZERO_NUMBER : DefaultNumberConstants.ONE_NUMBER); + } + return Boolean.FALSE; + } + + @Override + public SysUserRolePromise queryIsShowByUserId(Long userId) { + List sysUserRolePromises = baseMapper.selectList + (WrappersX.lambdaQueryX().eq(SysUserRolePromise::getUserId, userId).orderByDesc(SysUserRolePromise::getId)); + if (CollUtil.isNotEmpty(sysUserRolePromises)){ + return sysUserRolePromises.get(DefaultNumberConstants.ZERO_NUMBER); + } + return new SysUserRolePromise(); + } + + /** + * 插入用户角色关联关系 + * + * @param userId 用户ID + * @param roleCodes 角色标识集合 + * @param isAutoAudits + * @param isShowNid + * @param num + * @return boolean + */ + @Override + public boolean addUserRolesPromise(@NonNull Long userId, @NonNull List roleCodes, Boolean isAutoAudits, Boolean isShowNid, Integer num) { + SysUserRolePromise promise = prodSysUserRoles(userId, roleCodes, isAutoAudits, isShowNid, num); + // 批量插入 + boolean insertSuccess = SqlHelper.retBool(baseMapper.insert(promise)); + Assert.isTrue(insertSuccess, () -> { + log.error("[addUserRoles] 插入用户角色权限表失败,userId:{},roleCodes:{}", userId, roleCodes); + return new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "插入用户角色权限表失败"); + }); + return insertSuccess; + } + + /** + * 根据用户ID 和 角色Code 生成SysUserRole实体集合 + * + * @param userId 用户ID + * @param roleCodes 角色标识集合 + * @param isAutoAudits + * @param isShowNid + * @param num + * @return List + */ + private SysUserRolePromise prodSysUserRoles(Long userId, List roleCodes, Boolean isAutoAudits, Boolean isShowNid, Integer num) { + // 转换为 SysUserRole 实体集合 + SysUserRolePromise promise = new SysUserRolePromise(); + Long currentUserId = SecurityUtils.getCurrentUserId(); + promise.setUserId(userId); + promise.setRoleType(num); + promise.setRoleCodes(roleCodes); + promise.setIsShowNid(isShowNid); + promise.setCompanyId(currentUserId); + promise.setIsAutoAudits(isAutoAudits); + return promise; + } + + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysUserRoleServiceImpl.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysUserRoleServiceImpl.java new file mode 100644 index 0000000..45011fc --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysUserRoleServiceImpl.java @@ -0,0 +1,157 @@ +package com.baiye.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import com.baiye.system.mapper.SysUserRoleMapper; +import com.baiye.system.model.dto.SysUserScope; +import com.baiye.system.model.entity.SysRole; +import com.baiye.system.model.entity.SysUserRole; +import com.baiye.system.model.qo.RoleBindUserQO; +import com.baiye.system.model.vo.RoleBindUserVO; +import com.baiye.system.service.SysUserRolePromiseService; +import com.baiye.system.service.SysUserRoleService; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.baiye.exception.BusinessException; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.result.BaseResultCode; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import com.google.common.collect.Lists; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +/** + * 用户角色关联关系表 + * + * @author Hccake + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysUserRoleServiceImpl extends ExtendServiceImpl + implements SysUserRoleService { + + + private final SysUserRolePromiseService sysUserRolePromiseService; + + /** + * 根据UserId删除该用户角色关联关系 + * @param userId 用户ID + * @return boolean + */ + @Override + public boolean deleteByUserId(Long userId) { + return baseMapper.deleteByUserId(userId); + } + + /** + * 更新用户关联关系 + * + * @param userId 用户ID + * @param scope 角色标识集合 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateUserRoles(@NonNull Long userId, @NonNull SysUserScope scope) { + // 是否存在用户角色绑定关系,存在则先清空 + boolean existsRoleBind = baseMapper.existsRoleBind(userId, null); + if (existsRoleBind) { + boolean deleteSuccess = baseMapper.deleteByUserId(userId); + Assert.isTrue(deleteSuccess, () -> { + log.error("[updateUserRoles] 删除用户角色关联关系失败,userId:{},roleCodes:{}", userId, scope); + return new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "删除用户角色关联关系失败"); + }); + } + + // 没有的新授权的角色直接返回 + if (CollectionUtil.isEmpty(scope.getRoleCodes())) { + return true; + } + sysUserRolePromiseService.updateUserRolePromise(userId, scope); + + // 保存新的用户角色关联关系 + return addUserRoles(userId, scope.getRoleCodes()); + } + + /** + * 插入用户角色关联关系 + * @param userId 用户ID + * @param roleCodes 角色标识集合 + * @return boolean + */ + @Override + public boolean addUserRoles(@NonNull Long userId, @NonNull List roleCodes) { + List list = prodSysUserRoles(userId, roleCodes); + // 批量插入 + boolean insertSuccess = SqlHelper.retBool(baseMapper.insertBatchSomeColumn(list)); + Assert.isTrue(insertSuccess, () -> { + log.error("[addUserRoles] 插入用户角色关联关系失败,userId:{},roleCodes:{}", userId, roleCodes); + return new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "插入用户角色关联关系失败"); + }); + return insertSuccess; + } + + /** + * 根据用户ID 和 角色Code 生成SysUserRole实体集合 + * @param userId 用户ID + * @param roleCodes 角色标识集合 + * @return List + */ + private List prodSysUserRoles(Long userId, List roleCodes) { + // 转换为 SysUserRole 实体集合 + List list = new ArrayList<>(); + for (String roleCode : roleCodes) { + SysUserRole sysUserRole = new SysUserRole(); + sysUserRole.setUserId(userId); + sysUserRole.setRoleCode(roleCode); + list.add(sysUserRole); + } + return list; + } + + /** + * 通过用户ID 获取用户所有角色ID + * @param userId 用户ID + * @return 用户拥有的角色集合 + */ + @Override + public List listRoles(Long userId) { + return baseMapper.listRoleByUserId(userId); + } + + /** + * 通过角色标识,查询用户列表 + * @param pageParam 分页参数 + * @param roleBindUserQO 查询条件 + * @return PageResult 角色授权的用户列表 + */ + @Override + public PageResult queryUserPageByRoleCode(PageParam pageParam, RoleBindUserQO roleBindUserQO) { + return baseMapper.queryUserPageByRoleCode(pageParam, roleBindUserQO); + } + + /** + * 解绑角色和用户关系 + * @param userId 用户ID + * @param roleCode 角色标识 + * @return 解绑成功:true + */ + @Override + public boolean unbindRoleUser(Long userId, String roleCode) { + // 不存在则不需要进行删除,直接返回true + return !baseMapper.existsRoleBind(userId, roleCode) || baseMapper.deleteUserRole(userId, roleCode); + } + + + @Override + public List selectUserIdByRoleCode(Long baseUserId, String roleCode) { + return baseMapper.selectListByUserIdAndRoleCode(baseUserId, roleCode); + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysUserServiceImpl.java b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000..91965ce --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/java/com/baiye/system/service/impl/SysUserServiceImpl.java @@ -0,0 +1,589 @@ +package com.baiye.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.domain.SelectData; +import com.baiye.enums.SalesManEnum; +import com.baiye.exception.BadRequestException; +import com.baiye.exception.BusinessException; +import com.baiye.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import com.baiye.result.BaseResultCode; +import com.baiye.security.util.SecurityUtils; +import com.baiye.system.checker.AdminUserChecker; +import com.baiye.system.component.PasswordHelper; +import com.baiye.system.constant.SysUserConst; +import com.baiye.system.converter.SysUserConverter; +import com.baiye.system.enums.RoleCodeEnum; +import com.baiye.system.event.UserCreatedEvent; +import com.baiye.system.event.UserDelEvent; +import com.baiye.system.event.UserOrganizationChangeEvent; +import com.baiye.system.mapper.SysUserMapper; +import com.baiye.system.model.dto.SysUserDTO; +import com.baiye.system.model.dto.SysUserNameRoleDTO; +import com.baiye.system.model.dto.SysUserScope; +import com.baiye.system.model.dto.UserInfoDTO; +import com.baiye.system.model.entity.SysMenu; +import com.baiye.system.model.entity.SysRole; +import com.baiye.system.model.entity.SysUser; +import com.baiye.system.model.qo.SysUserQO; +import com.baiye.system.model.vo.SysUserBalanceVO; +import com.baiye.system.model.vo.SysUserPageVO; +import com.baiye.system.properties.FileProperties; +import com.baiye.system.service.SysMenuService; +import com.baiye.system.service.SysUserRolePromiseService; +import com.baiye.system.service.SysUserRoleService; +import com.baiye.system.service.SysUserService; +import com.baiye.util.FileUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 系统用户表 + * + * @author ballcat code generator 2019-09-12 20:39:31 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysUserServiceImpl extends ExtendServiceImpl implements SysUserService { + + private final SysMenuService sysMenuService; + + private final SysUserRoleService sysUserRoleService; + + private final SysUserRolePromiseService sysUserRolePromiseService; + + private final AdminUserChecker adminUserChecker; + + private final ApplicationEventPublisher publisher; + + private final PasswordHelper passwordHelper; + + private final FileProperties fileProperties; + + + /** + * 根据QueryObject查询分页数据 + * + * @param pageParam 分页参数 + * @param qo 查询参数对象 + * @return PageResult 分页数据 + */ + @Override + public PageResult queryPage(PageParam pageParam, SysUserQO qo) { + PageResult result = baseMapper.queryPage(pageParam, qo); + + List sysUsers = baseMapper.selectList(new LambdaQueryWrapper<>()); + List userAll = Convert.toList(SysUserPageVO.class, sysUsers); + Map> userGroup = userAll.stream().collect(Collectors.groupingBy(SysUserPageVO::getWhichUserId)); + + for (SysUserPageVO user : result.getRecords()) { + List byWhichUserId = userGroup.get(user.getUserId()); + if (CollUtil.isNotEmpty(byWhichUserId)) { + user.setChildren(byWhichUserId); + } + user.setRoleCode(null); + } + return result; + } + + @Override + public PageResult queryAll(PageParam pageParam, SysUserQO qo) { + PageResult result = baseMapper.queryPage(pageParam, qo); + + for (SysUserPageVO user : result.getRecords()) { + List of = ListUtil.of(RoleCodeEnum.find(15L), RoleCodeEnum.find(23L)); + if (of.contains(user.getRoleCode())) user.setIsSalesman(true); + user.setRoleCode(null); + } + return result; + } + + /** + * 根据用户名查询用户 + * + * @param username 用户名 + * @return 系统用户 + */ + @Override + public SysUser getByUsername(String username) { + return baseMapper.selectByUsername(username); + } + + /** + * 通过查用户的全部信息 + * + * @param sysUser 用户 + * @return 用户信息 + */ + @Override + public UserInfoDTO findUserInfo(SysUser sysUser) { + UserInfoDTO userInfoDTO = new UserInfoDTO(); + userInfoDTO.setSysUser(sysUser); + userInfoDTO.setSalesmanType(DefaultNumberConstants.MINUS_ONE_NUMBER); + Boolean isAdmin = Boolean.FALSE; + // 超级管理员拥有所有角色 + List roleList; + if (adminUserChecker.isAdminUser(sysUser)) { + isAdmin = Boolean.TRUE; + } + roleList = sysUserRoleService.listRoles(sysUser.getUserId()); + // 判断用户角色 + Set roleIdSet = roleList.stream().map(SysRole::getId).collect(Collectors.toSet()); + if (Boolean.FALSE.equals(isAdmin)) { + for (Long id : roleIdSet) { + Integer type = SalesManEnum.find(id); + if (ObjectUtil.isNotNull(type) + && type > DefaultNumberConstants.MINUS_ONE_NUMBER) { + userInfoDTO.setSalesmanType(type); + break; + } + } + } + + // 设置角色标识 + Set roleCodes = new HashSet<>(); + for (SysRole role : roleList) { + roleCodes.add(role.getCode()); + } + userInfoDTO.setRoles(new HashSet<>(roleList)); + userInfoDTO.setRoleCodes(roleCodes); + + // 设置权限列表(permission) + Set permissions = new HashSet<>(); + Set menus = new HashSet<>(); + for (String roleCode : roleCodes) { + List sysMenuList = sysMenuService.listByRoleCode(roleCode); + menus.addAll(sysMenuList); + List permissionList = sysMenuList.stream() + .map(SysMenu::getPermission) + .filter(StrUtil::isNotEmpty) + .collect(Collectors.toList()); + permissions.addAll(permissionList); + } + userInfoDTO.setMenus(menus); + userInfoDTO.setPermissions(permissions); + + return userInfoDTO; + } + + /** + * 新增系统用户 + * + * @param sysUserDto 系统用户DTO + * @return 添加成功:true , 失败:false + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean addSysUser(SysUserDTO sysUserDto) { + SysUser sysUser = SysUserConverter.INSTANCE.dtoToPo(sysUserDto); + sysUser.setType(SysUserConst.Type.SYSTEM.getValue()); + // 对密码进行加密 + String rawPassword = sysUserDto.getPassword(); + String encodedPassword = passwordHelper.encode(rawPassword); + sysUser.setPassword(encodedPassword); + + // 保存用户 + sysUser.setWhichUserId(SecurityUtils.getCurrentUserId()); + sysUser.setChargingStatus(0); + boolean insertSuccess = SqlHelper.retBool(baseMapper.insert(sysUser)); + Assert.isTrue(insertSuccess, () -> { + log.error("[addSysUser] 数据插入系统用户表失败,user:{}", sysUserDto); + return new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "数据插入系统用户表失败"); + }); + + // 新增用户角色关联 + List roleCodes = sysUserDto.getRoleCodes(); + if (!CollectionUtils.isEmpty(roleCodes)) { + boolean addUserRoleSuccess = sysUserRoleService.addUserRoles(sysUser.getUserId(), roleCodes); + Assert.isTrue(addUserRoleSuccess, () -> { + log.error("[addSysUser] 更新用户角色信息失败,user:{}, roleCodes: {}", sysUserDto, roleCodes); + return new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "更新用户角色信息失败"); + }); + if (Boolean.TRUE.equals(sysUserDto.getIsAutoAudits()) || Boolean.TRUE.equals(sysUserDto.getIsShowNid())) { + sysUserRolePromiseService.addUserRolesPromise(sysUser.getUserId(), roleCodes, sysUserDto.getIsAutoAudits(), sysUserDto.getIsShowNid(), Boolean.TRUE.equals(sysUserDto.getIsAutoAudits()) ? DefaultNumberConstants.ZERO_NUMBER : DefaultNumberConstants.ONE_NUMBER); + } + } + // 发布用户创建事件 + publisher.publishEvent(new UserCreatedEvent(sysUser, sysUserDto.getRoleCodes())); + + return true; + } + + /** + * 更新系统用户信息 + * + * @param sysUserDTO 系统用户DTO + * @return 更新成功 true: 更新失败 false + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateSysUser(SysUserDTO sysUserDTO) { + SysUser entity = SysUserConverter.INSTANCE.dtoToPo(sysUserDTO); + Assert.isTrue(adminUserChecker.hasModifyPermission(entity), "当前用户不允许修改!"); + + // 如果不更新组织,直接执行 + Long currentOrganizationId = entity.getOrganizationId(); + if (currentOrganizationId == null) { + return SqlHelper.retBool(baseMapper.updateById(entity)); + } + + // 查询出当前库中用户 + Long userId = entity.getUserId(); + SysUser oldUser = baseMapper.selectById(userId); + Assert.notNull(oldUser, "修改用户失败,当前用户不存在:{}", userId); + + // 是否修改了组织 + Long originOrganizationId = oldUser.getOrganizationId(); + boolean organizationIdModified = !currentOrganizationId.equals(originOrganizationId); + // 是否更改成功 + boolean isUpdateSuccess = SqlHelper.retBool(baseMapper.updateById(entity)); + // 如果修改了组织且修改成功,则发送用户组织更新事件 + if (isUpdateSuccess && organizationIdModified) { + publisher.publishEvent(new UserOrganizationChangeEvent(userId, originOrganizationId, currentOrganizationId)); + } + + return isUpdateSuccess; + } + + /** + * 更新用户权限信息 + * + * @param userId 用户Id + * @param sysUserScope 系统用户权限范围 + * @return 更新成功:true + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateUserScope(Long userId, SysUserScope sysUserScope) { + // 更新用户角色关联关系 + return sysUserRoleService.updateUserRoles(userId, sysUserScope); + } + + /** + * 根据userId删除 用户 + * + * @param userId 用户ID + * @return 删除成功:true + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteByUserId(Long userId) { + Assert.isFalse(adminUserChecker.isAdminUser(getById(userId)), "管理员不允许删除!"); + // 如果有子用户 删除子用户 + List delUserIdList = new ArrayList<>(); + delUserIdList.add(userId); + List userList = baseMapper.selectList(new LambdaQueryWrapper().eq(SysUser::getWhichUserId, userId)); + if (CollUtil.isNotEmpty(userList)) { + List userIdList = userList.stream().map(SysUser::getUserId).collect(Collectors.toList()); + delUserIdList.addAll(userIdList); + } + int bool = baseMapper.deleteBatchIds(delUserIdList); + // 发布用户删除事件 + publisher.publishEvent(new UserDelEvent(delUserIdList)); + return SqlHelper.retBool(bool); + } + + /** + * 修改用户密码 + * + * @param userId 用户ID + * @param rawPassword 明文密码 + * @return 更新成功:true + */ + @Override + public boolean updatePassword(Long userId, String rawPassword) { + Assert.isTrue(adminUserChecker.hasModifyPermission(getById(userId)), "当前用户不允许修改!"); + // 密码加密加密 + String encodedPassword = passwordHelper.encode(rawPassword); + return baseMapper.updatePassword(userId, encodedPassword); + } + + /** + * 批量修改用户状态 + * + * @param userIds 用户ID集合 + * @return 更新成功:true + */ + @Override + public boolean updateUserStatusBatch(Collection userIds, Integer status) { + + List userList = baseMapper.listByUserIds(userIds); + Assert.notEmpty(userList, "更新用户状态失败,待更新用户列表为空"); + + // 移除无权限更改的用户id + Map userMap = userList.stream() + .collect(Collectors.toMap(SysUser::getUserId, Function.identity())); + userIds.removeIf(id -> !adminUserChecker.hasModifyPermission(userMap.get(id))); + Assert.notEmpty(userIds, "更新用户状态失败,无权限更新用户"); + + return baseMapper.updateUserStatusBatch(userIds, status); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String updateAvatar(MultipartFile multipartFile, Long userId) throws IOException { + Assert.isTrue(adminUserChecker.hasModifyPermission(getById(userId)), "当前用户不允许修改!"); + // 文件大小验证 + FileUtil.checkSize(fileProperties.getAvatarMaxSize(), multipartFile.getSize()); + // 验证文件上传的格式 + String image = "gif jpg png jpeg"; + String fileType = FileUtil.getExtensionName(multipartFile.getOriginalFilename()); + if (fileType != null && !image.contains(fileType)) { + throw new BusinessException(BaseResultCode.FILE_UPLOAD_ERROR.getCode(), "文件格式错误!, 仅支持 " + image + " 格式"); + } + SysUser sysUser = baseMapper.selectById(userId); + String oldPath = sysUser.getAvatar(); + File file = FileUtil.upload(multipartFile, fileProperties.getPath().getAvatar()); + sysUser.setAvatar(Objects.requireNonNull(file).getName()); + baseMapper.updateById(sysUser); + if (StringUtils.isNotBlank(oldPath)) { + FileUtil.del(oldPath); + } + return file.getName(); + } + + /** + * 根据角色查询用户 + * + * @param roleCode 角色标识 + * @return 系统用户集合 + */ + @Override + public List listByRoleCode(String roleCode) { + return listByRoleCodes(Collections.singletonList(roleCode)); + } + + /** + * 根据角色查询用户 + * + * @param roleCode 角色标识 + * @return 系统用户集合 + */ + @Override + public List listByRoleCodeAndUserId(String roleCode, Long userId) { + return baseMapper.listByRoleCodeAndUserId(roleCode, userId); + } + + @Override + public List listUserNameByUserIds(List userIds) { + return baseMapper.listUserNameByUserIds(userIds); + } + + /** + * 根据角色查询用户 + * + * @param roleCodes 角色标识集合 + * @return List + */ + @Override + public List listByRoleCodes(Collection roleCodes) { + return baseMapper.listByRoleCodes(roleCodes); + } + + /** + * 根据组织机构ID查询用户 + * + * @param organizationIds 组织机构id集合 + * @return 用户集合 + */ + @Override + public List listByOrganizationIds(Collection organizationIds) { + return baseMapper.listByOrganizationIds(organizationIds); + } + + /** + * 根据用户类型查询用户 + * + * @param userTypes 用户类型集合 + * @return 用户集合 + */ + @Override + public List listByUserTypes(Collection userTypes) { + return baseMapper.listByUserTypes(userTypes); + } + + /** + * 根据用户Id集合查询用户 + * + * @param userIds 用户Id集合 + * @return 用户集合 + */ + @Override + public List listByUserIds(Collection userIds) { + return baseMapper.listByUserIds(userIds); + + } + + /** + * 返回用户的select数据 name=> username value => userId + * + * @param userTypes 用户类型 + * @return List + */ + @Override + public List> listSelectData(Collection userTypes) { + return baseMapper.listSelectData(userTypes); + } + + /** + * 获取用户的角色Code集合 + * + * @param userId 用户id + * @return List + */ + @Override + public List listRoleCodes(Long userId) { + return sysUserRoleService.listRoles(userId).stream().map(SysRole::getCode).collect(Collectors.toList()); + } + + /** + * 是否存在指定组织的用户 + * + * @param organizationId 组织 id + * @return boolean 存在返回 true + */ + @Override + public boolean existsForOrganization(Long organizationId) { + return baseMapper.existsForOrganization(organizationId); + } + + @Override + public List> listSelectSonListData() { + Long userId = SecurityUtils.getCurrentUserId(); + if (adminUserChecker.isAdminUser(userId)) { + return baseMapper.listSelectSonListData(userId, null); + } + List list = ListUtil.of(RoleCodeEnum.find(15L), RoleCodeEnum.find(23L)); + return baseMapper.listSelectSonListData(userId, list); + } + + @Override + public SysUser findById(Long id) { + return baseMapper.selectById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public synchronized boolean detectionAvailableNumUpdate(Map map) { + if (CollUtil.isEmpty(map)) throw new BadRequestException("用户不能为空"); + List sysUserList = baseMapper.listUserBalance(map.keySet()); + Long whichUserId = sysUserList.get(0).getWhichUserId(); + + if (!adminUserChecker.isAdminUser(whichUserId)) { + SysUser sysUser = baseMapper.selectById(whichUserId); + if (sysUser.getChargingStatus() == 1) { + for (SysUserBalanceVO user : sysUserList) { + Integer clueNum = map.get(user.getUserId()); + Integer residueClueNum = user.getResidueClueNum(); + if (residueClueNum < clueNum) throw new BadRequestException(user.getUsername() + "-用户余额不足"); + } + // 扣费 + baseMapper.charging(map); + } + } + baseMapper.updateDistributeNum(map); + return true; + } + + @Override + public String detectionAvailableNum(List userIds) { + if (CollUtil.isEmpty(userIds)) throw new BadRequestException("用户不能为空"); + List sysUsers = baseMapper.listUserBalance(userIds); + Long whichUserId = sysUsers.get(0).getWhichUserId(); + SysUser whichUser = baseMapper.selectById(whichUserId); + for (SysUserBalanceVO sysUser : sysUsers) { + if (!adminUserChecker.isAdminUser(whichUserId) && whichUser.getChargingStatus() == 1 && sysUser.getResidueClueNum() <= 0) { + return sysUser.getUsername(); + } + } + return null; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public synchronized boolean recharge(SysUserDTO sysUserDTO) { + Long currentUserId = SecurityUtils.getCurrentUserId(); + Integer rechargeClueNum = sysUserDTO.getRechargeClueNum(); + Long userId = sysUserDTO.getUserId(); + if (rechargeClueNum == null || rechargeClueNum < 0) throw new BadRequestException("错误的数量值"); + if (userId == null) throw new BadRequestException("请选择充值用户"); + + if (!adminUserChecker.isAdminUser(currentUserId)) { + Integer userBalance = baseMapper.findUserBalance(currentUserId); + if (userBalance < rechargeClueNum) throw new BadRequestException("余额不足"); + + Map map = new HashMap<>(); + map.put(currentUserId, rechargeClueNum); + baseMapper.charging(map); + } + log.info("用户{},给{}充值,条数:{}", currentUserId, userId, rechargeClueNum); + return SqlHelper.retBool(baseMapper.recharge(userId, rechargeClueNum)); + } + + @Override + public Boolean deductionQuantity(Long userId, String storeName) { + SysUserBalanceVO balanceVO = baseMapper.userBalance(userId); + if (!adminUserChecker.isAdminUser(balanceVO.getWhichUserId())) { + SysUser sysUser = baseMapper.selectById(balanceVO.getWhichUserId()); + if (sysUser.getChargingStatus() == DefaultNumberConstants.ONE_NUMBER) { + Integer residueClueNum = balanceVO.getResidueClueNum(); + if (residueClueNum < DefaultNumberConstants.ONE_NUMBER) { + if (StringUtils.isNotBlank(storeName)) { + throw new BadRequestException("用户" + balanceVO.getUsername() + "\n店铺:" + storeName + "\n可用数量不足!"); + } else { + throw new BadRequestException("用户" + balanceVO.getUsername() + "\n可用数量不足!"); + } + } + // 余额与数量进行增加 + return baseMapper.operationsNum(userId, balanceVO.getDistributeNum() + DefaultNumberConstants.ONE_NUMBER, + balanceVO.getResidueClueNum() - DefaultNumberConstants.ONE_NUMBER) > DefaultNumberConstants.ZERO_NUMBER; + } + // 对数量进行增加 + if (sysUser.getChargingStatus() == DefaultNumberConstants.ZERO_NUMBER) { + return baseMapper.operationsDistributeNum(userId, balanceVO.getDistributeNum() + DefaultNumberConstants.ONE_NUMBER) > DefaultNumberConstants.ZERO_NUMBER; + } + } + return Boolean.FALSE; + } + + @Override + public Boolean addQuantity(Long userId) { + SysUserBalanceVO balanceVO = baseMapper.userBalance(userId); + if (!adminUserChecker.isAdminUser(balanceVO.getWhichUserId())) { + SysUser sysUser = baseMapper.selectById(balanceVO.getWhichUserId()); + if (sysUser.getChargingStatus() == DefaultNumberConstants.ONE_NUMBER) { + // 余额与数量进行增加 + return baseMapper.operationsNum(userId, balanceVO.getDistributeNum() - DefaultNumberConstants.ONE_NUMBER, + balanceVO.getResidueClueNum() + DefaultNumberConstants.ONE_NUMBER) > DefaultNumberConstants.ZERO_NUMBER; + } + // 对数量进行增加 + if (sysUser.getChargingStatus() == DefaultNumberConstants.ZERO_NUMBER) { + return baseMapper.operationsDistributeNum(userId, balanceVO.getDistributeNum() - DefaultNumberConstants.ONE_NUMBER) > 0; + } + } + return Boolean.FALSE; + } + +} diff --git a/marketing-scrm-system/system-biz/src/main/resources/mapper/SysMenuMapper.xml b/marketing-scrm-system/system-biz/src/main/resources/mapper/SysMenuMapper.xml new file mode 100644 index 0000000..9c74978 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/resources/mapper/SysMenuMapper.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + id, + parent_id, + title, + icon, + permission, + path, + target_type, + uri, + sort, + keep_alive, + hidden, + type, + remarks, + deleted, + create_time, + update_time + + + + sm.id, + sm.parent_id, + sm.title, + sm.icon, + sm.permission, + sm.path, + sm.target_type, + sm.uri, + sm.sort, + sm.keep_alive, + sm.hidden, + sm.type, + sm.remarks, + sm.deleted, + sm.create_time, + sm.update_time + + + + + \ No newline at end of file diff --git a/marketing-scrm-system/system-biz/src/main/resources/mapper/SysOrganizationMapper.xml b/marketing-scrm-system/system-biz/src/main/resources/mapper/SysOrganizationMapper.xml new file mode 100644 index 0000000..8857655 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/resources/mapper/SysOrganizationMapper.xml @@ -0,0 +1,68 @@ + + + + + + id, + name, + parent_id, + hierarchy, + depth, + sort, + remarks, + deleted, + create_by, + update_by, + create_time, + update_time + + + + + UPDATE + sys_organization + SET + hierarchy = CONCAT(#{param.targetParentHierarchy}, + SUBSTR(hierarchy, ${param.originParentHierarchyLengthPlusOne})), + depth = depth - #{param.depthDiff} + WHERE + deleted = 0 + AND ( + parent_id = #{param.parentId} -- 儿子节点 + OR + hierarchy like #{param.grandsonConditionalStatement} -- 孙子节点 + ) + + + + + + + + \ No newline at end of file diff --git a/marketing-scrm-system/system-biz/src/main/resources/mapper/SysRoleMapper.xml b/marketing-scrm-system/system-biz/src/main/resources/mapper/SysRoleMapper.xml new file mode 100644 index 0000000..5e6d7c4 --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/resources/mapper/SysRoleMapper.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/marketing-scrm-system/system-biz/src/main/resources/mapper/SysUserMapper.xml b/marketing-scrm-system/system-biz/src/main/resources/mapper/SysUserMapper.xml new file mode 100644 index 0000000..f69926d --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/resources/mapper/SysUserMapper.xml @@ -0,0 +1,212 @@ + + + + + + su + . + user_id + , + su.username, + su.nickname, + su.avatar, + su.gender, + su.email, + su.phone_number, + su.status, + su.type, + su.organization_id, + su.create_time, + su.update_time, + su.which_user_id, + su.distribute_num, + su.charging_status, + su.residue_clue_num + + + + + + + + + + + + + + + + update sys_user + set residue_clue_num = residue_clue_num + #{rechargeClueNum} + where user_id = #{userId} + + + + + + UPDATE sys_user SET residue_clue_num = residue_clue_num - #{value} WHERE user_id = #{key} + + + + + + UPDATE sys_user SET distribute_num = distribute_num + #{value} WHERE user_id = #{key} + + + + + + + + + + + + UPDATE sys_user + SET distribute_num = #{distributeNum}, + residue_clue_num = #{num} + WHERE user_id = #{userId}; + + + + + + UPDATE sys_user SET distribute_num = #{distributeNum} WHERE user_id = #{userId} + + + + diff --git a/marketing-scrm-system/system-biz/src/main/resources/mapper/SysUserRoleMapper.xml b/marketing-scrm-system/system-biz/src/main/resources/mapper/SysUserRoleMapper.xml new file mode 100644 index 0000000..1ad9def --- /dev/null +++ b/marketing-scrm-system/system-biz/src/main/resources/mapper/SysUserRoleMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/marketing-scrm-system/system-controller/pom.xml b/marketing-scrm-system/system-controller/pom.xml new file mode 100644 index 0000000..469333c --- /dev/null +++ b/marketing-scrm-system/system-controller/pom.xml @@ -0,0 +1,24 @@ + + + + marketing-scrm-system + com.baiye + ${revision} + + 4.0.0 + system-controller + + + + com.baiye + common-log + ${revision} + + + com.baiye + system-biz + ${revision} + + + diff --git a/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysConfigController.java b/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysConfigController.java new file mode 100644 index 0000000..2fa5f5c --- /dev/null +++ b/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysConfigController.java @@ -0,0 +1,93 @@ +package com.baiye.system.controller; + +import com.baiye.operation.annotation.CreateOperationLogging; +import com.baiye.operation.annotation.DeleteOperationLogging; +import com.baiye.operation.annotation.UpdateOperationLogging; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.result.R; +import com.baiye.system.model.qo.SysConfigQO; +import com.baiye.system.model.vo.SysConfigPageVO; +import com.baiye.system.model.entity.SysConfig; +import com.baiye.system.service.SysConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 系统配置 + * + * @author ballcat code generator + * @date 2019-10-14 17:42:23 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/config") +@Tag(name = "系统配置") +public class SysConfigController { + + private final SysConfigService sysConfigService; + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param sysConfigQO 系统配置 + * @return R> + */ + @GetMapping("/page") + @PreAuthorize("@per.hasPermission('system:config:read')") + @Operation(summary = "分页查询", description = "分页查询") + public R> getSysConfigPage(@Validated PageParam pageParam, SysConfigQO sysConfigQO) { + return R.ok(sysConfigService.queryPage(pageParam, sysConfigQO)); + } + + /** + * 新增系统配置 + * @param sysConfig 系统配置 + * @return R + */ + @CreateOperationLogging(msg = "新增系统配置") + @PostMapping + @PreAuthorize("@per.hasPermission('system:config:add')") + @Operation(summary = "新增系统配置", description = "新增系统配置") + public R save(@RequestBody SysConfig sysConfig) { + return R.ok(sysConfigService.save(sysConfig)); + } + + /** + * 修改系统配置 + * @param sysConfig 系统配置 + * @return R + */ + @UpdateOperationLogging(msg = "修改系统配置") + @PutMapping + @PreAuthorize("@per.hasPermission('system:config:edit')") + @Operation(summary = "修改系统配置") + public R updateById(@RequestBody SysConfig sysConfig) { + return R.ok(sysConfigService.updateByKey(sysConfig)); + } + + /** + * 删除系统配置 + * @param confKey confKey + * @return R + */ + @DeleteOperationLogging(msg = "删除系统配置") + @DeleteMapping + @PreAuthorize("@per.hasPermission('system:config:del')") + @Operation(summary = "删除系统配置") + public R removeById(@RequestParam("confKey") String confKey) { + return R.ok(sysConfigService.removeByKey(confKey)); + } + +} diff --git a/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysDictController.java b/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysDictController.java new file mode 100644 index 0000000..45cf811 --- /dev/null +++ b/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysDictController.java @@ -0,0 +1,199 @@ +package com.baiye.system.controller; + +import com.baiye.system.manager.SysDictManager; +import com.baiye.system.model.dto.SysDictItemDTO; +import com.baiye.system.model.entity.SysDict; +import com.baiye.system.model.qo.SysDictQO; +import com.baiye.system.model.vo.DictDataVO; +import com.baiye.system.model.vo.SysDictItemPageVO; +import com.baiye.system.model.vo.SysDictPageVO; +import com.baiye.validation.group.CreateGroup; +import com.baiye.validation.group.UpdateGroup; +import com.baiye.operation.annotation.CreateOperationLogging; +import com.baiye.operation.annotation.DeleteOperationLogging; +import com.baiye.operation.annotation.UpdateOperationLogging; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.result.BaseResultCode; +import com.baiye.result.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.groups.Default; +import java.util.List; +import java.util.Map; + +/** + * 字典表 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/dict") +@Tag(name = "字典表管理") +public class SysDictController { + + private final SysDictManager sysDictManager; + + /** + * 通过字典标识查找对应字典项 + * @param dictCodes 字典标识列表 + * @return 同类型字典 + */ + @GetMapping("/data") + public R> getDictData(@RequestParam("dictCodes") String[] dictCodes) { + return R.ok(sysDictManager.queryDictDataAndHashVO(dictCodes)); + } + + /** + * 通过字典标识查找对应字典项 + * @param dictHashCode 字典标识 + * @return 同类型字典 + */ + @PostMapping("/invalid-hash") + public R> invalidDictHash(@RequestBody Map dictHashCode) { + return R.ok(sysDictManager.invalidDictHash(dictHashCode)); + } + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param sysDictQO 字典查询参数 + * @return R> + */ + @GetMapping("/page") + @PreAuthorize("@per.hasPermission('system:dict:read')") + @Operation(summary = "分页查询", description = "分页查询") + public R> getSysDictPage(@Validated PageParam pageParam, SysDictQO sysDictQO) { + return R.ok(sysDictManager.dictPage(pageParam, sysDictQO)); + } + + /** + * 新增字典表 + * @param sysDict 字典表 + * @return R + */ + @CreateOperationLogging(msg = "新增字典表") + @PostMapping + @PreAuthorize("@per.hasPermission('system:dict:add')") + @Operation(summary = "新增字典表", description = "新增字典表") + public R save(@RequestBody SysDict sysDict) { + return sysDictManager.dictSave(sysDict) ? R.ok() : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "新增字典表失败"); + } + + /** + * 修改字典表 + * @param sysDict 字典表 + * @return R + */ + @UpdateOperationLogging(msg = "修改字典表") + @PutMapping + @PreAuthorize("@per.hasPermission('system:dict:edit')") + @Operation(summary = "修改字典表", description = "修改字典表") + public R updateById(@RequestBody SysDict sysDict) { + return sysDictManager.updateDictById(sysDict) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "修改字典表失败"); + } + + /** + * 通过id删除字典表 + * @param id id + * @return R + */ + @DeleteOperationLogging(msg = "通过id删除字典表") + @DeleteMapping("/{id}") + @PreAuthorize("@per.hasPermission('system:dict:del')") + @Operation(summary = "通过id删除字典表", description = "通过id删除字典表") + public R removeById(@PathVariable("id") Long id) { + sysDictManager.removeDictById(id); + return R.ok(); + } + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param dictCode 字典标识 + * @return R + */ + @GetMapping("/item/page") + @PreAuthorize("@per.hasPermission('system:dict:read')") + @Operation(summary = "分页查询", description = "分页查询") + public R> getSysDictItemPage(PageParam pageParam, + @RequestParam("dictCode") String dictCode) { + return R.ok(sysDictManager.dictItemPage(pageParam, dictCode)); + } + + /** + * 新增字典项 + * @param sysDictItemDTO 字典项 + * @return R + */ + @CreateOperationLogging(msg = "新增字典项") + @PostMapping("item") + @PreAuthorize("@per.hasPermission('system:dict:add')") + @Operation(summary = "新增字典项", description = "新增字典项") + public R saveItem( + @Validated({ Default.class, CreateGroup.class }) @RequestBody SysDictItemDTO sysDictItemDTO) { + return sysDictManager.saveDictItem(sysDictItemDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "新增字典项失败"); + } + + /** + * 修改字典项 + * @param sysDictItemDTO 字典项 + * @return R + */ + @UpdateOperationLogging(msg = "修改字典项") + @PutMapping("item") + @PreAuthorize("@per.hasPermission('system:dict:edit')") + @Operation(summary = "修改字典项", description = "修改字典项") + public R updateItemById( + @Validated({ Default.class, UpdateGroup.class }) @RequestBody SysDictItemDTO sysDictItemDTO) { + return sysDictManager.updateDictItemById(sysDictItemDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "修改字典项失败"); + } + + /** + * 通过id删除字典项 + * @param id id + * @return R + */ + @DeleteOperationLogging(msg = "通过id删除字典项") + @DeleteMapping("/item/{id}") + @PreAuthorize("@per.hasPermission('system:dict:del')") + @Operation(summary = "通过id删除字典项", description = "通过id删除字典项") + public R removeItemById(@PathVariable("id") Long id) { + return sysDictManager.removeDictItemById(id) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "通过id删除字典项失败"); + } + + /** + * 通过id修改字典项状态 + * @param id id + * @return R + */ + @UpdateOperationLogging(msg = "通过id修改字典项状态") + @PatchMapping("/item/{id}") + @PreAuthorize("@per.hasPermission('system:dict:edit')") + @Operation(summary = "通过id修改字典项状态", description = "通过id修改字典项状态") + public R updateDictItemStatusById(@PathVariable("id") Long id, @RequestParam("status") Integer status) { + sysDictManager.updateDictItemStatusById(id, status); + return R.ok(); + } + +} diff --git a/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysMenuController.java b/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysMenuController.java new file mode 100644 index 0000000..7354160 --- /dev/null +++ b/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysMenuController.java @@ -0,0 +1,159 @@ +package com.baiye.system.controller; + +import cn.hutool.core.collection.CollUtil; +import com.baiye.operation.annotation.CreateOperationLogging; +import com.baiye.operation.annotation.DeleteOperationLogging; +import com.baiye.operation.annotation.UpdateOperationLogging; +import com.baiye.result.BaseResultCode; +import com.baiye.result.R; +import com.baiye.security.constant.UserAttributeNameConstants; +import com.baiye.security.userdetails.User; +import com.baiye.security.util.SecurityUtils; +import com.baiye.system.converter.SysMenuConverter; +import com.baiye.system.enums.SysMenuType; +import com.baiye.system.model.dto.SysMenuCreateDTO; +import com.baiye.system.model.dto.SysMenuUpdateDTO; +import com.baiye.system.model.entity.SysMenu; +import com.baiye.system.model.qo.SysMenuQO; +import com.baiye.system.model.vo.SysMenuGrantVO; +import com.baiye.system.model.vo.SysMenuPageVO; +import com.baiye.system.model.vo.SysMenuRouterVO; +import com.baiye.system.service.SysMenuService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 菜单权限 + * + * @author hccake 2021-04-06 17:59:51 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/menu") +@Tag(name = "菜单权限管理") +public class SysMenuController { + + private final SysMenuService sysMenuService; + + /** + * 返回当前用户的路由集合 + * @return 当前用户的路由 + */ + @GetMapping("/router") + @Operation(summary = "动态路由", description = "动态路由") + public R> getUserPermission() { + // 获取角色Code + User user = SecurityUtils.getUser(); + Map attributes = user.getAttributes(); + + Object rolesObject = attributes.get(UserAttributeNameConstants.ROLE_CODES); + if (!(rolesObject instanceof Collection)) { + return R.ok(new ArrayList<>()); + } + + @SuppressWarnings("unchecked") + Collection roleCodes = (Collection) rolesObject; + if (CollUtil.isEmpty(roleCodes)) { + return R.ok(new ArrayList<>()); + } + + // 获取符合条件的权限 + Set all = new HashSet<>(); + roleCodes.forEach(roleCode -> all.addAll(sysMenuService.listByRoleCode(roleCode))); + + // 筛选出菜单 + List menuVOList = all.stream() + .filter(menuVo -> SysMenuType.BUTTON.getValue() != menuVo.getType()) + .sorted(Comparator.comparingInt(SysMenu::getSort)) + .map(SysMenuConverter.INSTANCE::poToRouterVo) + .collect(Collectors.toList()); + + return R.ok(menuVOList); + } + + /** + * 查询菜单列表 + * @param sysMenuQO 菜单权限查询对象 + * @return R 通用返回体 + */ + @GetMapping("/list") + @PreAuthorize("@per.hasPermission('system:menu:read')") + @Operation(summary = "查询菜单列表", description = "查询菜单列表") + public R> getSysMenuPage(SysMenuQO sysMenuQO) { + List sysMenus = sysMenuService.listOrderBySort(sysMenuQO); + if (CollUtil.isEmpty(sysMenus)) { + R.ok(new ArrayList<>()); + } + List voList = sysMenus.stream() + .map(SysMenuConverter.INSTANCE::poToPageVo) + .collect(Collectors.toList()); + return R.ok(voList); + } + + /** + * 查询授权菜单列表 + * @return R 通用返回体 + */ + @GetMapping("/grant-list") + @PreAuthorize("@per.hasPermission('system:menu:read')") + @Operation(summary = "查询授权菜单列表", description = "查询授权菜单列表") + public R> getSysMenuGrantList() { + List sysMenus = sysMenuService.list(); + if (CollUtil.isEmpty(sysMenus)) { + R.ok(new ArrayList<>()); + } + List voList = sysMenus.stream() + .map(SysMenuConverter.INSTANCE::poToGrantVo) + .collect(Collectors.toList()); + return R.ok(voList); + } + + /** + * 新增菜单权限 + * @param sysMenuCreateDTO 菜单权限 + * @return R 通用返回体 + */ + @CreateOperationLogging(msg = "新增菜单权限") + @PostMapping + @PreAuthorize("@per.hasPermission('system:menu:add')") + @Operation(summary = "新增菜单权限", description = "新增菜单权限") + public R save(@Valid @RequestBody SysMenuCreateDTO sysMenuCreateDTO) { + return sysMenuService.create(sysMenuCreateDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "新增菜单权限失败"); + } + + /** + * 修改菜单权限 + * @param sysMenuUpdateDTO 菜单权限修改DTO + * @return R 通用返回体 + */ + @UpdateOperationLogging(msg = "修改菜单权限") + @PutMapping + @PreAuthorize("@per.hasPermission('system:menu:edit')") + @Operation(summary = "修改菜单权限", description = "修改菜单权限") + public R updateById(@RequestBody SysMenuUpdateDTO sysMenuUpdateDTO) { + sysMenuService.update(sysMenuUpdateDTO); + return R.ok(); + } + + /** + * 通过id删除菜单权限 + * @param id id + * @return R 通用返回体 + */ + @DeleteOperationLogging(msg = "通过id删除菜单权限") + @DeleteMapping("/{id}") + @PreAuthorize("@per.hasPermission('system:menu:del')") + @Operation(summary = "通过id删除菜单权限", description = "通过id删除菜单权限") + public R removeById(@PathVariable("id") Long id) { + return sysMenuService.removeById(id) ? R.ok() : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "通过id删除菜单权限失败"); + } + +} \ No newline at end of file diff --git a/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysOrganizationController.java b/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysOrganizationController.java new file mode 100644 index 0000000..ca8a4b4 --- /dev/null +++ b/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysOrganizationController.java @@ -0,0 +1,126 @@ +package com.baiye.system.controller; + +import cn.hutool.core.collection.CollUtil; +import com.baiye.operation.annotation.CreateOperationLogging; +import com.baiye.operation.annotation.DeleteOperationLogging; +import com.baiye.operation.annotation.UpdateOperationLogging; +import com.baiye.result.BaseResultCode; +import com.baiye.result.R; +import com.baiye.system.converter.SysOrganizationConverter; +import com.baiye.system.model.dto.SysOrganizationDTO; +import com.baiye.system.model.qo.SysOrganizationQO; +import com.baiye.system.model.vo.SysOrganizationTree; +import com.baiye.system.model.vo.SysOrganizationVO; +import com.baiye.system.service.SysOrganizationService; +import com.baiye.system.model.entity.SysOrganization; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 12:09:43 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/organization") +@Tag(name = "组织架构管理") +public class SysOrganizationController { + + private final SysOrganizationService sysOrganizationService; + + /** + * 组织架构列表查询 + * @return R 通用返回体 + */ + @GetMapping("/list") + @PreAuthorize("@per.hasPermission('system:organization:read')") + @Operation(summary = "组织架构列表查询") + public R> listOrganization() { + List list = sysOrganizationService.list(); + if (CollUtil.isEmpty(list)) { + return R.ok(new ArrayList<>()); + } + List voList = list.stream() + .sorted(Comparator.comparingInt(SysOrganization::getSort)) + .map(SysOrganizationConverter.INSTANCE::poToVo) + .collect(Collectors.toList()); + return R.ok(voList); + } + + /** + * 组织架构树查询 + * @param qo 组织机构查询条件 + * @return R 通用返回体 + */ + @GetMapping("/tree") + @PreAuthorize("@per.hasPermission('system:organization:read')") + @Operation(summary = "组织架构树查询") + public R> getOrganizationTree(SysOrganizationQO qo) { + return R.ok(sysOrganizationService.listTree(qo)); + } + + /** + * 新增组织架构 + * @param sysOrganizationDTO 组织机构DTO + * @return R 通用返回体 + */ + @CreateOperationLogging(msg = "新增组织架构") + @PostMapping + @PreAuthorize("@per.hasPermission('system:organization:add')") + @Operation(summary = "新增组织架构") + public R save(@RequestBody SysOrganizationDTO sysOrganizationDTO) { + return sysOrganizationService.create(sysOrganizationDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "新增组织架构失败"); + } + + /** + * 修改组织架构 + * @param sysOrganizationDTO 组织机构DTO + * @return R 通用返回体 + */ + @UpdateOperationLogging(msg = "修改组织架构") + @PutMapping + @PreAuthorize("@per.hasPermission('system:organization:edit')") + @Operation(summary = "修改组织架构") + public R updateById(@RequestBody SysOrganizationDTO sysOrganizationDTO) { + return sysOrganizationService.update(sysOrganizationDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "修改组织架构失败"); + } + + /** + * 通过id删除组织架构 + * @param id id + * @return R 通用返回体 + */ + @DeleteOperationLogging(msg = "通过id删除组织架构") + @DeleteMapping("/{id}") + @PreAuthorize("@per.hasPermission('system:organization:del')") + @Operation(summary = "通过id删除组织架构") + public R removeById(@PathVariable("id") Long id) { + return sysOrganizationService.removeById(id) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "通过id删除组织架构失败"); + } + + /** + * 校正组织机构层级和深度 + * @return R 通用返回体 + */ + @UpdateOperationLogging(msg = "校正组织机构层级和深度") + @PatchMapping("/revised") + @PreAuthorize("@per.hasPermission('system:organization:revised')") + @Operation(summary = "校正组织机构层级和深度") + public R revisedHierarchyAndPath() { + return sysOrganizationService.revisedHierarchyAndPath() ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "校正组织机构层级和深度失败"); + } + +} \ No newline at end of file diff --git a/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysRoleController.java b/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysRoleController.java new file mode 100644 index 0000000..4fd371d --- /dev/null +++ b/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysRoleController.java @@ -0,0 +1,197 @@ +package com.baiye.system.controller; + +import com.baiye.operation.annotation.CreateOperationLogging; +import com.baiye.operation.annotation.DeleteOperationLogging; +import com.baiye.operation.annotation.UpdateOperationLogging; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.domain.SelectData; +import com.baiye.result.BaseResultCode; +import com.baiye.result.R; +import com.baiye.system.constant.SysRoleConst; +import com.baiye.system.converter.SysRoleConverter; +import com.baiye.system.model.dto.SysRoleUpdateDTO; +import com.baiye.system.model.entity.SysMenu; +import com.baiye.system.model.entity.SysRole; +import com.baiye.system.model.qo.RoleBindUserQO; +import com.baiye.system.model.qo.SysRoleQO; +import com.baiye.system.model.vo.RoleBindUserVO; +import com.baiye.system.model.vo.SysRolePageVO; +import com.baiye.system.service.SysMenuService; +import com.baiye.system.service.SysRoleMenuService; +import com.baiye.system.service.SysUserRoleService; +import com.baiye.system.service.SysRoleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Hccake + */ +@RestController +@RequestMapping("/system/role") +@RequiredArgsConstructor +@Tag(name = "角色管理模块") +public class SysRoleController { + + private final SysRoleService sysRoleService; + + private final SysMenuService sysMenuService; + + private final SysUserRoleService sysUserRoleService; + + private final SysRoleMenuService sysRoleMenuService; + + /** + * 分页查询角色信息 + * @param pageParam 分页参数 + * @return PageResult 分页结果 + */ + @GetMapping("/page") + @PreAuthorize("@per.hasPermission('system:role:read')") + public R> getRolePage(@Validated PageParam pageParam, SysRoleQO sysRoleQo) { + return R.ok(sysRoleService.queryPage(pageParam, sysRoleQo)); + } + + /** + * 通过ID查询角色信息 + * @param id ID + * @return 角色信息 + */ + @GetMapping("/{id}") + @PreAuthorize("@per.hasPermission('system:role:read')") + public R getById(@PathVariable("id") Long id) { + return R.ok(sysRoleService.getById(id)); + } + + /** + * 新增系统角色表 + * @param sysRole 系统角色表 + * @return R + */ + @CreateOperationLogging(msg = "新增系统角色") + @PostMapping + @PreAuthorize("@per.hasPermission('system:role:add')") + @Operation(summary = "新增系统角色", description = "新增系统角色") + public R save(@Valid @RequestBody SysRole sysRole) { + return sysRoleService.save(sysRole) ? R.ok() : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "新建角色失败"); + } + + /** + * 修改角色 + * @param roleUpdateDTO 角色修改DTO + * @return success/false + */ + @UpdateOperationLogging(msg = "修改系统角色") + @PutMapping + @PreAuthorize("@per.hasPermission('system:role:edit')") + @Operation(summary = "修改系统角色", description = "修改系统角色") + public R update(@Valid @RequestBody SysRoleUpdateDTO roleUpdateDTO) { + SysRole sysRole = SysRoleConverter.INSTANCE.dtoToPo(roleUpdateDTO); + return R.ok(sysRoleService.updateById(sysRole)); + } + + /** + * 删除角色 + * @param id id + * @return 结果信息 + */ + @DeleteMapping("/{id}") + @DeleteOperationLogging(msg = "通过id删除系统角色") + @PreAuthorize("@per.hasPermission('system:role:del')") + @Operation(summary = "通过id删除系统角色", description = "通过id删除系统角色") + public R removeById(@PathVariable("id") Long id) { + SysRole oldRole = sysRoleService.getById(id); + if (oldRole == null) { + return R.ok(); + } + if (SysRoleConst.Type.SYSTEM.getValue().equals(oldRole.getType())) { + return R.failed(BaseResultCode.LOGIC_CHECK_ERROR, "系统角色不允许被删除!"); + } + return R.ok(sysRoleService.removeById(id)); + } + + /** + * 获取角色列表 + * @return 角色列表 + */ + @GetMapping("/list") + public R> listRoles() { + return R.ok(sysRoleService.list()); + } + + /** + * 更新角色权限 + * @param roleCode 角色Code + * @param permissionIds 权限ID数组 + * @return success、false + */ + @PutMapping("/permission/code/{roleCode}") + @UpdateOperationLogging(msg = "更新角色权限") + @PreAuthorize("@per.hasPermission('system:role:grant')") + @Operation(summary = "更新角色权限", description = "更新角色权限") + public R savePermissionIds(@PathVariable("roleCode") String roleCode, @RequestBody Long[] permissionIds) { + return R.ok(sysRoleMenuService.saveRoleMenus(roleCode, permissionIds)); + } + + /** + * 返回角色的菜单集合 + * @param roleCode 角色ID + * @return 属性集合 + */ + @GetMapping("/permission/code/{roleCode}") + public R> getPermissionIds(@PathVariable("roleCode") String roleCode) { + List sysMenus = sysMenuService.listByRoleCode(roleCode); + List menuIds = sysMenus.stream().map(SysMenu::getId).collect(Collectors.toList()); + return R.ok(menuIds); + } + + /** + * 获取角色列表 + * @return 角色列表 + */ + @GetMapping("/select") + public R>> listSelectData() { + return R.ok(sysRoleService.listSelectData()); + } + + /** + * 分页查询已授权指定角色的用户列表 + * @param roleBindUserQO 角色绑定用户的查询条件 + * @return R + */ + @GetMapping("/user/page") + @PreAuthorize("@per.hasPermission('system:role:grant')") + @Operation(summary = "查看已授权指定角色的用户列表", description = "查看已授权指定角色的用户列表") + public R> queryUserPageByRoleCode(PageParam pageParam, + @Valid RoleBindUserQO roleBindUserQO) { + return R.ok(sysUserRoleService.queryUserPageByRoleCode(pageParam, roleBindUserQO)); + } + + /** + * 解绑与用户绑定关系 + * @return R + */ + @DeleteMapping("/user") + @PreAuthorize("@per.hasPermission('system:role:grant')") + @Operation(summary = "解绑与用户绑定关系", description = "解绑与用户绑定关系") + public R unbindRoleUser(@RequestParam("userId") Long userId, @RequestParam("roleCode") String roleCode) { + return R.ok(sysUserRoleService.unbindRoleUser(userId, roleCode)); + } + +} diff --git a/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysUserController.java b/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysUserController.java new file mode 100644 index 0000000..139bc1b --- /dev/null +++ b/marketing-scrm-system/system-controller/src/main/java/com/baiye/system/controller/SysUserController.java @@ -0,0 +1,332 @@ +package com.baiye.system.controller; + +import cn.hutool.core.util.ObjectUtil; +import com.baiye.constant.DefaultNumberConstants; +import com.baiye.domain.PageParam; +import com.baiye.domain.PageResult; +import com.baiye.domain.SelectData; +import com.baiye.exception.BusinessException; +import com.baiye.operation.annotation.CreateOperationLogging; +import com.baiye.operation.annotation.DeleteOperationLogging; +import com.baiye.operation.annotation.UpdateOperationLogging; +import com.baiye.result.BaseResultCode; +import com.baiye.result.R; +import com.baiye.result.SystemResultCode; +import com.baiye.security.util.SecurityUtils; +import com.baiye.system.component.PasswordHelper; +import com.baiye.system.constant.SysUserConst; +import com.baiye.system.converter.SysUserConverter; +import com.baiye.system.enums.RoleCodeEnum; +import com.baiye.system.model.dto.SysUserDTO; +import com.baiye.system.model.dto.SysUserNameRoleDTO; +import com.baiye.system.model.dto.SysUserPassDTO; +import com.baiye.system.model.dto.SysUserScope; +import com.baiye.system.model.entity.SysRole; +import com.baiye.system.model.entity.SysUser; +import com.baiye.system.model.entity.SysUserRolePromise; +import com.baiye.system.model.qo.SysUserQO; +import com.baiye.system.model.vo.SysUserInfo; +import com.baiye.system.model.vo.SysUserPageVO; +import com.baiye.system.service.SysUserRolePromiseService; +import com.baiye.system.service.SysUserRoleService; +import com.baiye.system.service.SysUserService; +import com.baiye.validation.group.CreateGroup; +import com.baiye.validation.group.UpdateGroup; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.CollectionUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.ValidationException; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.groups.Default; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 组织架构 + * + * @author hccake 2020-09-24 20:16:15 + */ +@Slf4j +@Validated +@RestController +@RequestMapping("/system/user") +@RequiredArgsConstructor +@Tag(name = "用户管理模块") +public class SysUserController { + + private final SysUserService sysUserService; + + private final SysUserRoleService sysUserRoleService; + + private final SysUserRolePromiseService sysUserRolePromiseService; + + private final PasswordHelper passwordHelper; + + /** + * 分页查询用户 + * + * @param pageParam 参数集 + * @return 用户集合 + */ + @GetMapping("/page") + @PreAuthorize("@per.hasPermission('system:user:read')") + @Operation(summary = "分页查询系统用户(tree树结构)") + public R> getUserPage(@Validated PageParam pageParam, SysUserQO qo) { + if (SecurityUtils.getCurrentUserId() != 1) { + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR, "非超级管理员不能查询"); + } + qo.setWhichUserId((long) DefaultNumberConstants.ONE_NUMBER); + return R.ok(sysUserService.queryPage(pageParam, qo)); + } + + @GetMapping("/pageList") + @Operation(summary = "分页查询子用户") + public R> pageList(@Validated PageParam pageParam, SysUserQO qo) { + qo.setWhichUserId(SecurityUtils.getCurrentUserId()); + return R.ok(sysUserService.queryAll(pageParam, qo)); + } + + @GetMapping("/selectSonList") + @Operation(summary = "获取用户下级下拉列表数据") + public R>> listSelectSonListData() { + return R.ok(sysUserService.listSelectSonListData()); + } + + /** + * 获取用户Select + * + * @return 用户SelectData + */ + @GetMapping("/select") + @PreAuthorize("@per.hasPermission('system:user:read')") + @Operation(summary = "获取用户下拉列表数据") + public R>> listSelectData( + @RequestParam(value = "userTypes", required = false) List userTypes) { + return R.ok(sysUserService.listSelectData(userTypes)); + } + + /** + * 获取指定用户的基本信息 + * + * @param userId 用户ID + * @return SysUserInfo + */ + @GetMapping("/{userId}") + // @PreAuthorize("@per.hasPermission('system:user:read')") + @Operation(summary = "获取指定用户的基本信息") + public R getSysUserInfo(@PathVariable("userId") Long userId) { + SysUser sysUser = sysUserService.getById(userId); + if (sysUser == null) { + return R.ok(); + } + SysUserInfo sysUserInfo = SysUserConverter.INSTANCE.poToInfo(sysUser); + List roleList = sysUserRoleService.listRoles(userId); + sysUserInfo.setRoleList(roleList); + return R.ok(sysUserInfo); + } + + /** + * 新增用户 + * + * @param sysUserDTO userInfo + * @return success/false + */ + @PostMapping + @CreateOperationLogging(msg = "新增系统用户") + @PreAuthorize("@per.hasPermission('system:user:add')") + @Operation(summary = "新增系统用户", description = "新增系统用户") + public R addSysUser(@Validated({Default.class, CreateGroup.class}) @RequestBody SysUserDTO sysUserDTO) { + SysUser user = sysUserService.getByUsername(sysUserDTO.getUsername()); + if (user != null) { + return R.failed(BaseResultCode.LOGIC_CHECK_ERROR, "用户名已存在"); + } + + // 明文密码 + String rawPassword = passwordHelper.decodeAes(sysUserDTO.getPass()); + sysUserDTO.setPassword(rawPassword); + + // 密码规则校验 + if (passwordHelper.validateRule(rawPassword)) { + return sysUserService.addSysUser(sysUserDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "新增系统用户失败"); + } else { + return R.failed(SystemResultCode.BAD_REQUEST, "密码格式不符合规则!"); + } + } + + /** + * 修改用户个人信息 + * + * @param sysUserDto userInfo + * @return success/false + */ + @PutMapping + @UpdateOperationLogging(msg = "修改系统用户") + // @PreAuthorize("@per.hasPermission('system:user:edit')") + @Operation(summary = "修改系统用户", description = "修改系统用户") + public R updateUserInfo(@Validated({Default.class, UpdateGroup.class}) @RequestBody SysUserDTO sysUserDto) { + return sysUserService.updateSysUser(sysUserDto) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "修改系统用户失败"); + } + + /** + * 删除用户信息 + */ + @DeleteMapping("/{userId}") + @DeleteOperationLogging(msg = "通过id删除系统用户") + @PreAuthorize("@per.hasPermission('system:user:del')") + @Operation(summary = "通过id删除系统用户", description = "通过id删除系统用户") + public R deleteByUserId(@PathVariable("userId") Long userId) { + return sysUserService.deleteByUserId(userId) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "删除系统用户失败"); + } + + /** + * 获取用户 所拥有的角色ID + * + * @param userId userId + */ + @GetMapping("/scope/{userId}") + // @PreAuthorize("@per.hasPermission('system:user:grant')") + public R getUserRoleIds(@PathVariable("userId") Long userId) { + List roleList = sysUserRoleService.listRoles(userId); + List roleCodes = new ArrayList<>(); + if (!CollectionUtils.isEmpty(roleList)) { + roleList.forEach(role -> roleCodes.add(role.getCode())); + } + SysUserScope sysUserScope = new SysUserScope(); + sysUserScope.setRoleCodes(roleCodes); + SysUserRolePromise promise = sysUserRolePromiseService.queryIsShowByUserId(userId); + if (ObjectUtil.isNotNull(promise)){ + sysUserScope.setIsAutoAudits(promise.getIsAutoAudits()); + sysUserScope.setIsShowNid(promise.getIsShowNid()); + } + return R.ok(sysUserScope); + } + + + /** + * 修改用户权限信息 比如角色 数据权限等 + * + * @param sysUserScope sysUserScope + * @return success/false + */ + @PutMapping("/scope/{userId}") + @UpdateOperationLogging(msg = "系统用户授权") + @PreAuthorize("@per.hasPermission('system:user:grant')") + @Operation(summary = "系统用户授权", description = "系统用户授权") + public R updateUserScope(@PathVariable("userId") Long userId, @RequestBody SysUserScope sysUserScope) { + return sysUserService.updateUserScope(userId, sysUserScope) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "系统用户授权失败"); + } + + /** + * 修改用户密码 + */ + @PutMapping("/pass/{userId}") + @UpdateOperationLogging(msg = "修改系统用户密码") + // @PreAuthorize("@per.hasPermission('system:user:pass')") + @Operation(summary = "修改系统用户密码", description = "修改系统用户密码") + public R updateUserPass(@PathVariable("userId") Long userId, @RequestBody SysUserPassDTO sysUserPassDTO) { + String pass = sysUserPassDTO.getPass(); + SysUser byId = sysUserService.getById(userId); + if (ObjectUtil.isNull(byId)) { + return R.failed(SystemResultCode.BAD_REQUEST, "该用户不存在"); + } + if (!passwordHelper.matches(passwordHelper.decodeAes(sysUserPassDTO.getOldPass()), byId.getPassword())) { + return R.failed(SystemResultCode.BAD_REQUEST, "修改失败,旧密码错误!"); + } + if (!pass.equals(sysUserPassDTO.getConfirmPass())) { + return R.failed(SystemResultCode.BAD_REQUEST, "两次密码输入不一致!"); + } + + + // 解密明文密码 + String rawPassword = passwordHelper.decodeAes(pass); + // 密码规则校验 + if (passwordHelper.validateRule(rawPassword)) { + return sysUserService.updatePassword(userId, rawPassword) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "修改用户密码失败!"); + } else { + return R.failed(SystemResultCode.BAD_REQUEST, "密码格式不符合规则!"); + } + } + + /** + * 批量修改用户状态 + */ + @PutMapping("/status") + @UpdateOperationLogging(msg = "批量修改用户状态") + @PreAuthorize("@per.hasPermission('system:user:edit')") + @Operation(summary = "批量修改用户状态", description = "批量修改用户状态") + public R updateUserStatus(@NotEmpty(message = "用户ID不能为空") @RequestBody List userIds, + @NotNull(message = "用户状态不能为空") @RequestParam("status") Integer status) { + + if (!SysUserConst.Status.NORMAL.getValue().equals(status) + && !SysUserConst.Status.LOCKED.getValue().equals(status)) { + throw new ValidationException("不支持的用户状态!"); + } + return sysUserService.updateUserStatusBatch(userIds, status) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "批量修改用户状态!"); + } + + @UpdateOperationLogging(msg = "修改系统用户头像") + // @PreAuthorize("@per.hasPermission('system:user:edit')") + @PostMapping("/avatar") + @Operation(summary = "修改系统用户头像", description = "修改系统用户头像") + public R updateAvatar(@RequestParam("file") MultipartFile file) { + String objectName; + try { + objectName = sysUserService.updateAvatar(file, SecurityUtils.getCurrentUserId()); + } catch (IOException e) { + log.error("修改系统用户头像异常", e); + return R.failed(BaseResultCode.FILE_UPLOAD_ERROR); + } + return R.ok(objectName); + } + + @Operation(summary = "充值", description = "充值") + @PostMapping("/recharge") + public R recharge(@RequestBody SysUserDTO sysUserDTO){ + return sysUserService.recharge(sysUserDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "充值失败"); + } + + @Operation(summary = "获取父用户下的指定角色的人员", description = "获取父用户下的指定角色的人员") + @GetMapping("/findSalesman/{roleId}") + public R findSalesman(@PathVariable("roleId") Long roleId) { + String directSalesmanCode = RoleCodeEnum.find(roleId); + List dtoList = sysUserService.listByRoleCodeAndUserId + (directSalesmanCode, SecurityUtils.getWhichUserId()).stream().filter + (vo -> vo.getStatus() == DefaultNumberConstants.ONE_NUMBER).collect(Collectors.toList()); + return R.ok(dtoList); + } + + @Operation(summary = "查询当前用户的指定角色子用户", description = "查询当前用户的指定角色子用户") + @GetMapping("/findRoleSonUser/{roleId}") + public R findRoleSonUser(@PathVariable("roleId") Long roleId) { + String directSalesmanCode = RoleCodeEnum.find(roleId); + return R.ok(sysUserService.listByRoleCodeAndUserId(directSalesmanCode, SecurityUtils.getCurrentUserId())); + } + + + + @Operation(summary = "获取所有用户", description = "根据角色id查所有用户") + @GetMapping("/findAll/{roleId}") + public R findAllUserByRoleId(@PathVariable("roleId") Long roleId) { + String directSalesmanCode = RoleCodeEnum.find(roleId); + List dtoList = sysUserService.listByRoleCodeAndUserId + (directSalesmanCode, null).stream().filter(vo -> vo.getStatus() == DefaultNumberConstants.ONE_NUMBER).collect(Collectors.toList()); + return R.ok(dtoList); + } +} diff --git a/marketing-scrm-system/system-model/pom.xml b/marketing-scrm-system/system-model/pom.xml new file mode 100644 index 0000000..b3640aa --- /dev/null +++ b/marketing-scrm-system/system-model/pom.xml @@ -0,0 +1,38 @@ + + + + marketing-scrm-system + com.baiye + ${revision} + + 4.0.0 + system-model + + + + com.baomidou + mybatis-plus-annotation + + + com.baiye + common-desensitize + ${revision} + + + + + com.baiye + common-i18n + + + + io.swagger.core.v3 + swagger-annotations + + + jakarta.validation + jakarta.validation-api + + + diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/DateConst.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/DateConst.java new file mode 100644 index 0000000..4cd81b1 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/DateConst.java @@ -0,0 +1,14 @@ +package com.baiye.system.constant; + +/** + * @author Enzo + * @date : 2022/8/4 + */ +public class DateConst { + private DateConst(){ + + } + + + public static final String YYYY_MM_DD = "yyyyMMdd"; +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/LetterConst.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/LetterConst.java new file mode 100644 index 0000000..98f0fae --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/LetterConst.java @@ -0,0 +1,17 @@ +package com.baiye.system.constant; + +/** + * @author Enzo + * @date : 2022/8/4 + */ +public class LetterConst { + private LetterConst(){ + + } + + + public static final String D = "D"; + + + public static final String ONE = "01"; +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/SysRoleConst.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/SysRoleConst.java new file mode 100644 index 0000000..1d06dd5 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/SysRoleConst.java @@ -0,0 +1,32 @@ +package com.baiye.system.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Hccake 2020/7/6 + * @version 1.0 + */ +public final class SysRoleConst { + + /** + * 角色类型,1系统角色,2 业务角色 + */ + @Getter + @AllArgsConstructor + public enum Type { + + /** + * 系统角色 + */ + SYSTEM(1), + /** + * 业务角色 + */ + BUSINESS(2); + + private final Integer value; + + } + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/SysUserConst.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/SysUserConst.java new file mode 100644 index 0000000..4dc504d --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/SysUserConst.java @@ -0,0 +1,51 @@ +package com.baiye.system.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Hccake 2019/9/17 14:54 + */ +public final class SysUserConst { + + private SysUserConst() { + } + + @Getter + @AllArgsConstructor + public enum Status { + + /** + * 正常 + */ + NORMAL(1), + /** + * 锁定的 + */ + LOCKED(0); + + private final Integer value; + + } + + /** + * 用户类型,1系统用户,2平台用户 + */ + @Getter + @AllArgsConstructor + public enum Type { + + /** + * 系统用户 + */ + SYSTEM(1), + /** + * 平台用户 + */ + CUSTOMER(2); + + private final Integer value; + + } + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/SystemRedisKeyConstants.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/SystemRedisKeyConstants.java new file mode 100644 index 0000000..30fb354 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/constant/SystemRedisKeyConstants.java @@ -0,0 +1,19 @@ +package com.baiye.system.constant; + +/** + * Redis 缓存 Key 的常量类 + * + * @author Hccake + */ +public final class SystemRedisKeyConstants { + + private SystemRedisKeyConstants() { + + } + + /** + * 系统配置类的缓存前缀 + */ + public static final String SYSTEM_CONFIG_PREFIX = "system-config"; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysConfigConverter.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysConfigConverter.java new file mode 100644 index 0000000..d0d14d6 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysConfigConverter.java @@ -0,0 +1,25 @@ +package com.baiye.system.converter; + +import com.baiye.system.model.vo.SysConfigPageVO; +import com.baiye.system.model.entity.SysConfig; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 基础配置 + * + * @author hccake 2021-03-22 18:58:36 + */ +@Mapper +public interface SysConfigConverter { + + SysConfigConverter INSTANCE = Mappers.getMapper(SysConfigConverter.class); + + /** + * PO 转 PageVO + * @param sysConfig 基础配置 + * @return SysConfigPageVO 基础配置分页VO + */ + SysConfigPageVO poToPageVo(SysConfig sysConfig); + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysDictConverter.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysDictConverter.java new file mode 100644 index 0000000..06adf1d --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysDictConverter.java @@ -0,0 +1,25 @@ +package com.baiye.system.converter; + +import com.baiye.system.model.vo.SysDictPageVO; +import com.baiye.system.model.entity.SysDict; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Hccake + * @version 1.0 + * @date 2020/4/9 16:32 + */ +@Mapper +public interface SysDictConverter { + + SysDictConverter INSTANCE = Mappers.getMapper(SysDictConverter.class); + + /** + * 字典实体转VO + * @param sysDict 字典 + * @return SysDictPageVO 字典分页VO + */ + SysDictPageVO poToPageVo(SysDict sysDict); + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysDictItemConverter.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysDictItemConverter.java new file mode 100644 index 0000000..f0d1661 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysDictItemConverter.java @@ -0,0 +1,41 @@ +package com.baiye.system.converter; + +import com.baiye.system.model.dto.SysDictItemDTO; +import com.baiye.system.model.entity.SysDictItem; +import com.baiye.system.model.vo.DictItemVO; +import com.baiye.system.model.vo.SysDictItemPageVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 字典项 + * + * @author hccake 2021-03-22 19:55:41 + */ +@Mapper +public interface SysDictItemConverter { + + SysDictItemConverter INSTANCE = Mappers.getMapper(SysDictItemConverter.class); + + /** + * PO 转 分页VO + * @param sysDictItem 字典项 + * @return SysDictItemPageVO 字典项分页VO + */ + SysDictItemPageVO poToPageVo(SysDictItem sysDictItem); + + /** + * 字典项实体 转 VO + * @param sysDictItem 字典项 + * @return 字典项VO + */ + DictItemVO poToItemVo(SysDictItem sysDictItem); + + /** + * 字典项传输对象转实体 + * @param sysDictItemDTO 传输对象 + * @return SysDictItem + */ + SysDictItem dtoToPo(SysDictItemDTO sysDictItemDTO); + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysMenuConverter.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysMenuConverter.java new file mode 100644 index 0000000..472a453 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysMenuConverter.java @@ -0,0 +1,59 @@ +package com.baiye.system.converter; + +import com.baiye.system.model.dto.SysMenuCreateDTO; +import com.baiye.system.model.dto.SysMenuUpdateDTO; +import com.baiye.system.model.entity.SysMenu; +import com.baiye.system.model.vo.SysMenuGrantVO; +import com.baiye.system.model.vo.SysMenuRouterVO; +import com.baiye.system.model.vo.SysMenuPageVO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * 菜单权限模型转换器 + * + * @author hccake 2021-04-06 17:59:51 + */ +@Mapper +public interface SysMenuConverter { + + SysMenuConverter INSTANCE = Mappers.getMapper(SysMenuConverter.class); + + /** + * PO 转 PageVO + * @param sysMenu 菜单权限实体 + * @return SysMenuPageVO 菜单权限PageVO + */ + @Mapping(target = "i18nTitle", source = "title") + SysMenuPageVO poToPageVo(SysMenu sysMenu); + + /** + * PO 转 GrantVo + * @param sysMenu 菜单权限实体 + * @return SysMenuPageVO 菜单权限GrantVO + */ + SysMenuGrantVO poToGrantVo(SysMenu sysMenu); + + /** + * PO 转 VO + * @param sysMenu 菜单权限实体 + * @return SysMenuVO + */ + SysMenuRouterVO poToRouterVo(SysMenu sysMenu); + + /** + * createDto 转 Po + * @param sysMenuCreateDTO 菜单新建对象 + * @return SysMenu 菜单权限的持久化对象 + */ + SysMenu createDtoToPo(SysMenuCreateDTO sysMenuCreateDTO); + + /** + * updateDto 转 Po + * @param sysMenuUpdateDTO 菜单修改对象 + * @return SysMenu 菜单权限的持久化对象 + */ + SysMenu updateDtoToPo(SysMenuUpdateDTO sysMenuUpdateDTO); + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysOrganizationConverter.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysOrganizationConverter.java new file mode 100644 index 0000000..8357ed3 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysOrganizationConverter.java @@ -0,0 +1,40 @@ +package com.baiye.system.converter; + +import com.baiye.system.model.dto.SysOrganizationDTO; +import com.baiye.system.model.entity.SysOrganization; +import com.baiye.system.model.vo.SysOrganizationTree; +import com.baiye.system.model.vo.SysOrganizationVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Hccake 2020/9/23 + * @version 1.0 + */ +@Mapper +public interface SysOrganizationConverter { + + SysOrganizationConverter INSTANCE = Mappers.getMapper(SysOrganizationConverter.class); + + /** + * 实体转树节点实体 + * @param sysOrganization 组织架构实体 + * @return 组织架构树类型 + */ + SysOrganizationTree poToTree(SysOrganization sysOrganization); + + /** + * 实体转组织架构VO + * @param sysOrganization 组织架构实体 + * @return SysOrganizationVO + */ + SysOrganizationVO poToVo(SysOrganization sysOrganization); + + /** + * 新增传输对象转实体 + * @param sysOrganizationDTO 组织机构DTO + * @return 组织机构实体 + */ + SysOrganization dtoToPo(SysOrganizationDTO sysOrganizationDTO); + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysRoleConverter.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysRoleConverter.java new file mode 100644 index 0000000..0bf5b0e --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysRoleConverter.java @@ -0,0 +1,34 @@ +package com.baiye.system.converter; + +import com.baiye.system.model.entity.SysRole; +import com.baiye.system.model.dto.SysRoleUpdateDTO; +import com.baiye.system.model.vo.SysRolePageVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 系统角色POJO转换器 + * + * @author Hccake 2020/7/6 + * @version 1.0 + */ +@Mapper +public interface SysRoleConverter { + + SysRoleConverter INSTANCE = Mappers.getMapper(SysRoleConverter.class); + + /** + * PO 转 PageVO + * @param sysRole 系统角色 + * @return SysRolePageVO 系统角色分页VO + */ + SysRolePageVO poToPageVo(SysRole sysRole); + + /** + * 修改DTO 转 PO + * @param dto 修改DTO + * @return SysRole PO + */ + SysRole dtoToPo(SysRoleUpdateDTO dto); + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysUserConverter.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysUserConverter.java new file mode 100644 index 0000000..40ba51b --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/converter/SysUserConverter.java @@ -0,0 +1,43 @@ +package com.baiye.system.converter; + +import com.baiye.system.model.dto.SysUserDTO; +import com.baiye.system.model.entity.SysUser; +import com.baiye.system.model.vo.SysUserInfo; +import com.baiye.system.model.vo.SysUserPageVO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/17 15:26 + */ +@Mapper +public interface SysUserConverter { + + SysUserConverter INSTANCE = Mappers.getMapper(SysUserConverter.class); + + /** + * 转换DTO 为 PO + * @param sysUserDTO 系统用户DTO + * @return SysUser 系统用户 + */ + @Mapping(target = "password", ignore = true) + SysUser dtoToPo(SysUserDTO sysUserDTO); + + /** + * PO 转 PageVO + * @param sysUser 系统用户 + * @return SysUserPageVO 系统用户PageVO + */ + SysUserPageVO poToPageVo(SysUser sysUser); + + /** + * PO 转 Info + * @param sysUser 系统用户 + * @return SysUserInfo 用户信息 + */ + SysUserInfo poToInfo(SysUser sysUser); + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/enums/RoleCodeEnum.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/enums/RoleCodeEnum.java new file mode 100644 index 0000000..e57c462 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/enums/RoleCodeEnum.java @@ -0,0 +1,82 @@ +package com.baiye.system.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 角色code对应表 + */ +@Getter +@AllArgsConstructor +public enum RoleCodeEnum { + + /** + * admin超级管理员 + */ + ROLE_ADMIN(1L, "ROLE_ADMIN"), + + /** + * 业务管理员 + */ + ROLE_SALES_EXECUTIVE(14L, "ROLE_SALES_EXECUTIVE"), + + /** + * 业务专员 + */ + ROLE_SALESMAN(15L, "ROLE_SALESMAN"), + + /** + * 录入员 + */ + ROLE_ENTRY_CLERK(19L, "ROLE_ENTRY_CLERK"), + + /** + * 初审员 + */ + ROLE_PRELIMINARY_EXAMINER(20L, "ROLE_PRELIMINARY_EXAMINER"), + + /** + * 复审员 + */ + ROLE_REVIEWER(21L, "ROLE_REVIEWER"), + + /** + * 门店主管 + */ + ROLE_STORE_EXECUTIVE(22L, "ROLE_STORE_EXECUTIVE"), + + /** + * 门店专员 + */ + ROLE_STORE_SALESMAN(23L, "ROLE_STORE_SALESMAN"), + + /** + * 直达管理员 + */ + ROLE_DIRECT_EXECUTIVE(24L, "ROLE_DIRECT_EXECUTIVE"), + + /** + * 直达分发员 + */ + ROLE_DIRECT_DISTRIBUTE(25L, "ROLE_DIRECT_DISTRIBUTE"), + + /** + * 直达业务员 + */ + ROLE_DIRECT_SALESMAN(26L, "ROLE_DIRECT_SALESMAN"); + + private final Long id; + + private final String roleCode; + + public static String find(Long roleId) { + for (RoleCodeEnum salesManEnum : RoleCodeEnum.values()) { + if (Objects.equals(salesManEnum.getId(), roleId)) { + return salesManEnum.getRoleCode(); + } + } + return ""; + } +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/enums/SysMenuType.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/enums/SysMenuType.java new file mode 100644 index 0000000..6b7f97d --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/enums/SysMenuType.java @@ -0,0 +1,30 @@ +package com.baiye.system.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 系统菜单类型 + * + * @author cheng + */ +@Getter +@AllArgsConstructor +public enum SysMenuType { + + /** + * 目录 + */ + DIRECTORY(0), + /** + * 菜单 + */ + MENU(1), + /** + * 按钮/权限 + */ + BUTTON(2); + + private final int value; + +} \ No newline at end of file diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/enums/TagEnum.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/enums/TagEnum.java new file mode 100644 index 0000000..bb01659 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/enums/TagEnum.java @@ -0,0 +1,18 @@ +package com.baiye.system.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author lingting 2020/7/6 10:04 + */ +@Getter +@AllArgsConstructor +public enum TagEnum { + + /** + * 标签类型 + */ + INPUT_TEXT, INPUT_NUMBER, SELECT, DICT_SELECT; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/event/DictChangeEvent.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/event/DictChangeEvent.java new file mode 100644 index 0000000..35b2a3a --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/event/DictChangeEvent.java @@ -0,0 +1,24 @@ +package com.baiye.system.event; + +import lombok.Getter; +import lombok.ToString; +import org.springframework.context.ApplicationEvent; + +/** + * 字典修改事件 + * + * @author Hccake 2021/1/5 + * @version 1.0 + */ +@Getter +@ToString +public class DictChangeEvent extends ApplicationEvent { + + private final String dictCode; + + public DictChangeEvent(String dictCode) { + super(dictCode); + this.dictCode = dictCode; + } + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/event/UserCreatedEvent.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/event/UserCreatedEvent.java new file mode 100644 index 0000000..1b32cb5 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/event/UserCreatedEvent.java @@ -0,0 +1,27 @@ +package com.baiye.system.event; + +import com.baiye.system.model.entity.SysUser; +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +/** + * 用户创建事件 + * + * @author Yakir + */ +@Getter +@ToString +public class UserCreatedEvent { + + private final SysUser sysUser; + + private final List roleCodes; + + public UserCreatedEvent(SysUser sysUser, List roleCodes) { + this.sysUser = sysUser; + this.roleCodes = roleCodes; + } + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/event/UserDelEvent.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/event/UserDelEvent.java new file mode 100644 index 0000000..6d6bf7b --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/event/UserDelEvent.java @@ -0,0 +1,21 @@ +package com.baiye.system.event; + +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +/** + * 用户删除事件 + */ +@Getter +@ToString +public class UserDelEvent{ + + private final List userIdList; + + public UserDelEvent(List userIdList) { + this.userIdList = userIdList; + } + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/event/UserOrganizationChangeEvent.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/event/UserOrganizationChangeEvent.java new file mode 100644 index 0000000..dcaea2b --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/event/UserOrganizationChangeEvent.java @@ -0,0 +1,23 @@ +package com.baiye.system.event; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * 用户组织变更事件 + * + * @author hccake + */ +@Getter +@ToString +@RequiredArgsConstructor +public class UserOrganizationChangeEvent { + + private final Long userId; + + private final Long originOrganizationId; + + private final Long currentOrganizationId; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/OrganizationMoveChildParam.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/OrganizationMoveChildParam.java new file mode 100644 index 0000000..4cf2464 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/OrganizationMoveChildParam.java @@ -0,0 +1,43 @@ +package com.baiye.system.model.dto; + +import lombok.Data; + +/** + * 组织机构移动子节点时的参数封装对象 + * + * @author hccake + */ +@Data +public class OrganizationMoveChildParam { + + /** + * 父级id + */ + private Long parentId; + + /** + * 父级节点原始的层级信息 + */ + private String originParentHierarchy; + + /** + * 父级节点原始的层级信息长度 + 1 + */ + private int originParentHierarchyLengthPlusOne; + + /** + * 父级节点移动后的层级信息 + */ + private String targetParentHierarchy; + + /** + * 移动前后的节点深度差 + */ + private Integer depthDiff; + + /** + * 查询孙子节点的条件语句 + */ + private String grandsonConditionalStatement; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysDictItemDTO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysDictItemDTO.java new file mode 100644 index 0000000..1b3c889 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysDictItemDTO.java @@ -0,0 +1,86 @@ +package com.baiye.system.model.dto; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.baiye.validation.constraints.OneOfInts; +import com.baiye.validation.group.CreateGroup; +import com.baiye.validation.group.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Null; +import java.util.Map; + +/** + * 字典项 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Data +@Schema(title = "字典项") +public class SysDictItemDTO { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Null(message = "id {}", groups = CreateGroup.class) + @NotNull(message = "id {}", groups = UpdateGroup.class) + @Schema(title = "ID") + private Long id; + + /** + * 字典标识 + */ + @NotBlank(message = "dictCode {}") + @Schema(title = "字典标识") + private String dictCode; + + /** + * 数据值 + */ + @NotBlank(message = "value {}") + @Schema(title = "数据值") + private String value; + + /** + * 文本值 + */ + @NotBlank(message = "name {}") + @Schema(title = "文本值") + private String name; + + /** + * 状态 + */ + @NotNull(message = "status {}", groups = CreateGroup.class) + @OneOfInts(value = { 1, 0 }, message = "status {}", allowNull = true) + @Schema(title = "状态", description = "1:启用 0:禁用") + private Integer status; + + /** + * 附加属性值 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + @Schema(title = "附加属性值") + private Map attributes; + + /** + * 排序(升序) + */ + @NotNull() + @Min(value = 0, message = "sort {}") + @Schema(title = "排序(升序)") + private Integer sort; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysMenuCreateDTO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysMenuCreateDTO.java new file mode 100644 index 0000000..75e8d53 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysMenuCreateDTO.java @@ -0,0 +1,108 @@ +package com.baiye.system.model.dto; + +import com.baiye.I18nMessage; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 菜单权限新建的DTO + * + * @author hccake 2021-04-06 17:59:51 + */ +@Data +@Schema(title = "菜单权限新建的DTO") +public class SysMenuCreateDTO { + + private static final long serialVersionUID = 1L; + + /** + * 菜单ID + */ + @Schema(title = "菜单ID") + private Long id; + + /** + * 父级ID + */ + @NotNull(message = "parentId:{}") + @Schema(title = "父级ID") + private Long parentId; + + /** + * 菜单名称 + */ + @Schema(title = "菜单名称") + private String title; + + /** + * 菜单图标 + */ + @Schema(title = "菜单图标") + private String icon; + + /** + * 授权标识 + */ + @Schema(title = "授权标识") + private String permission; + + /** + * 路由地址 + */ + @Schema(title = "路由地址") + private String path; + + /** + * 打开方式 (1组件 2内链 3外链) + */ + @Schema(title = "打开方式 (1组件 2内链 3外链)") + private Integer targetType; + + /** + * 定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址) + */ + @Schema(title = "定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址)") + private String uri; + + /** + * 显示排序 + */ + @Schema(title = "显示排序") + private Integer sort; + + /** + * 组件缓存:0-开启,1-关闭 + */ + @Schema(title = "组件缓存:0-开启,1-关闭") + private Integer keepAlive; + + /** + * 隐藏菜单: 0-否,1-是 + */ + @Schema(title = "隐藏菜单: 0-否,1-是") + private Integer hidden; + + /** + * 菜单类型 (0目录,1菜单,2按钮) + */ + @Schema(title = "菜单类型 (0目录,1菜单,2按钮)") + private Integer type; + + /** + * 备注信息 + */ + @Schema(title = "备注信息") + private String remarks; + + /** + * 菜单标题对应的国际化信息 + */ + @Valid + @Schema(title = "菜单标题对应的国际化信息") + private List i18nMessages; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysMenuUpdateDTO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysMenuUpdateDTO.java new file mode 100644 index 0000000..171f135 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysMenuUpdateDTO.java @@ -0,0 +1,106 @@ +package com.baiye.system.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 菜单权限 + * + * @author hccake 2021-04-06 17:59:51 + */ +@Data +@Schema(title = "菜单权限修改DTO") +public class SysMenuUpdateDTO { + + private static final long serialVersionUID = 1L; + + /** + * 菜单ID + */ + @NotNull(message = "菜单ID不能为空") + @Schema(title = "菜单ID") + private Long id; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 菜单名称 + */ + @Schema(title = "菜单名称") + private String title; + + /** + * 菜单图标 + */ + @Schema(title = "菜单图标") + private String icon; + + /** + * 授权标识 + */ + @Schema(title = "授权标识") + private String permission; + + /** + * 路由地址 + */ + @Schema(title = "路由地址") + private String path; + + /** + * 打开方式 (1组件 2内链 3外链) + */ + @Schema(title = "打开方式 (1组件 2内链 3外链)") + private Integer targetType; + + /** + * 定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址) + */ + @Schema(title = "定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址)") + private String uri; + + /** + * 显示排序 + */ + @Schema(title = "显示排序") + private Integer sort; + + /** + * 组件缓存:0-开启,1-关闭 + */ + @Schema(title = "组件缓存:0-开启,1-关闭") + private Integer keepAlive; + + /** + * 隐藏菜单: 0-否,1-是 + */ + @Schema(title = "隐藏菜单: 0-否,1-是") + private Integer hidden; + + /** + * 菜单类型 (0目录,1菜单,2按钮) + */ + @Schema(title = "菜单类型 (0目录,1菜单,2按钮)") + private Integer type; + + /** + * 备注信息 + */ + @Schema(title = "备注信息") + private String remarks; + + /** + * 原菜单ID + */ + @NotNull(message = "原菜单ID不能为空") + @Schema(title = "原菜单ID") + private Long originalId; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysOrganizationDTO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysOrganizationDTO.java new file mode 100644 index 0000000..311c179 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysOrganizationDTO.java @@ -0,0 +1,48 @@ +package com.baiye.system.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 20:39:40 + */ +@Data +@Schema(title = "组织架构DTO") +public class SysOrganizationDTO { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(title = "ID") + private Long id; + + /** + * 组织名称 + */ + @Schema(title = "组织名称") + private String name; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 排序字段,由小到大 + */ + @Schema(title = "排序字段,由小到大") + private Integer sort; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysRoleUpdateDTO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysRoleUpdateDTO.java new file mode 100644 index 0000000..c205e7a --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysRoleUpdateDTO.java @@ -0,0 +1,38 @@ +package com.baiye.system.model.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * 角色修改DTO + * + * @author Hccake 2020-07-06 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +@Schema(title = "角色修改DTO") +public class SysRoleUpdateDTO { + + private static final long serialVersionUID = 1L; + + @Schema(title = "角色编号") + private Long id; + + @NotBlank(message = "角色名称不能为空") + @Schema(title = "角色名称") + private String name; + + @Schema(title = "角色备注") + private String remarks; + + @Schema(title = "数据权限") + private Integer scopeType; + + @Schema(title = "数据范围资源,当数据范围类型为自定义时使用") + private String scopeResources; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysUserDTO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysUserDTO.java new file mode 100644 index 0000000..19a920e --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysUserDTO.java @@ -0,0 +1,114 @@ +package com.baiye.system.model.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.baiye.validation.group.CreateGroup; +import com.baiye.validation.group.UpdateGroup; +import com.baiye.enums.RegexDesensitizationTypeEnum; +import com.baiye.json.annotation.JsonRegexDesensitize; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 系统用户表 + * + * @author ballcat code generator 2019-09-12 20:39:31 + */ +@Data +@Schema(title = "系统用户DTO") +public class SysUserDTO { + + /** + * 主键id + */ + @NotNull(message = "userId {}", groups = UpdateGroup.class) + @Schema(title = "主键id") + private Long userId; + + /** + * 前端传入密码 + */ + @NotEmpty(message = "pass {}", groups = CreateGroup.class) + @JsonRegexDesensitize(type = RegexDesensitizationTypeEnum.ENCRYPTED_PASSWORD) + @Schema(title = "前端传入密码") + private String pass; + + /** + * 用户明文密码, 不参与前后端交互 + */ + @JsonIgnore + private String password; + + /** + * 登录账号 + */ + @NotEmpty(message = "username {}") + @Schema(title = "登录账号") + private String username; + + /** + * 昵称 + */ + @NotEmpty(message = "nickname {}") + @Schema(title = "昵称") + private String nickname; + + /** + * 头像 + */ + @Schema(title = "头像") + private String avatar; + + /** + * 性别(0-默认未知,1-男,2-女) + */ + @Schema(title = "性别(0-默认未知,1-男,2-女)") + private Integer gender; + + /** + * 电子邮件 + */ + @Schema(title = "电子邮件") + private String email; + + /** + * 手机号 + */ + @Schema(title = "手机号") + private String phoneNumber; + + /** + * 状态(1-正常,2-冻结) + */ + @Range(message = "status {}", min = 0, max = 1) + @Schema(title = "状态(1-正常,2-冻结)") + private Integer status; + + /** + * 组织机构ID + */ + @Schema(title = "组织机构ID") + private Long organizationId; + + /** + * 角色标识列表 + */ + @Schema(title = "角色标识列表") + private List roleCodes; + + @Schema(title = "计费状态 0不计费 1计费") + private Integer chargingStatus; + + @Schema(title = "充值数量") + private Integer rechargeClueNum; + + private Boolean isAutoAudits; + + private Boolean isShowNid; + + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysUserNameRoleDTO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysUserNameRoleDTO.java new file mode 100644 index 0000000..87b461b --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysUserNameRoleDTO.java @@ -0,0 +1,18 @@ +package com.baiye.system.model.dto; + +import lombok.Data; + +/** + * @author Enzo + * @date 2024-4-1 + */ +@Data +public class SysUserNameRoleDTO { + private Long userId; + + private Integer status; + + private String userName; + + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysUserPassDTO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysUserPassDTO.java new file mode 100644 index 0000000..0eb0294 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysUserPassDTO.java @@ -0,0 +1,43 @@ +package com.baiye.system.model.dto; + +import com.baiye.enums.RegexDesensitizationTypeEnum; +import com.baiye.json.annotation.JsonRegexDesensitize; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * 用户密码传输DTO,字段序列化时忽略,防止记录 + * + * @author Hccake 2021/1/22 + */ +@Data +@Schema(title = "系统用户密码传输实体") +public class SysUserPassDTO { + + + /** + * 旧密码 + */ + @NotBlank(message = "The password cannot be empty!") + @JsonRegexDesensitize(type = RegexDesensitizationTypeEnum.ENCRYPTED_PASSWORD) + @Schema(title = "前端输入旧密码") + private String oldPass; + /** + * 前端传入密码 + */ + @NotBlank(message = "The password cannot be empty!") + @JsonRegexDesensitize(type = RegexDesensitizationTypeEnum.ENCRYPTED_PASSWORD) + @Schema(title = "前端输入密码") + private String pass; + + /** + * 前端确认密码 + */ + @NotBlank(message = "The confirm password cannot be empty!") + @JsonRegexDesensitize(type = RegexDesensitizationTypeEnum.ENCRYPTED_PASSWORD) + @Schema(title = "前端确认密码") + private String confirmPass; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysUserScope.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysUserScope.java new file mode 100644 index 0000000..b05a753 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/SysUserScope.java @@ -0,0 +1,24 @@ +package com.baiye.system.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * + * 用户权限信息,基础只有roleIds 后续业务相关的授权 按需扩展 + * + * @author Hccake 2019/9/24 10:13 + */ +@Data +public class SysUserScope { + + + private Boolean isAutoAudits; + + private Boolean isShowNid; + + private List roleCodes; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/UserInfoDTO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/UserInfoDTO.java new file mode 100644 index 0000000..7ece100 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/dto/UserInfoDTO.java @@ -0,0 +1,58 @@ +package com.baiye.system.model.dto; + +import com.baiye.system.model.entity.SysMenu; +import com.baiye.system.model.entity.SysRole; +import com.baiye.system.model.entity.SysUser; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.util.Collection; + +/** + * 用户信息 + * + * @author Hccake + */ +@Data +@Schema(title = "用户信息") +public class UserInfoDTO { + + /** + * 用户基本信息 + */ + @Schema(title = "用户基本信息") + private SysUser sysUser; + + /** + * 权限标识集合 + */ + @Schema(title = "权限标识集合") + private Collection permissions; + + /** + * 角色标识集合 + */ + @Schema(title = "角色标识集合") + private Collection roleCodes; + + /** + * 菜单对象集合 + */ + @Schema(title = "菜单对象集合") + private Collection menus; + + /** + * 角色对象集合 + */ + @Schema(title = "角色对象集合") + private Collection roles; + + /** + * 业务员类型 + */ + @Schema(title = "业务员类型") + private Integer salesmanType; + + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysConfig.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysConfig.java new file mode 100644 index 0000000..68b9479 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysConfig.java @@ -0,0 +1,59 @@ +package com.baiye.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baiye.entity.LogicDeletedBaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 系统配置表 + * + * @author ballcat code generator 2019-10-14 17:42:23 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_config") +@Schema(title = "基础配置") +public class SysConfig extends LogicDeletedBaseEntity { + + /** + * 主键 + */ + @TableId + @Schema(title = "主键ID") + private Long id; + + /** + * 配置名称 + */ + @Schema(title = "配置名称") + private String name; + + /** + * 配置在缓存中的key名 + */ + @Schema(title = "配置在缓存中的key名") + private String confKey; + + /** + * 配置值 + */ + @Schema(title = "配置值") + private String confValue; + + /** + * 分类 + */ + @Schema(title = "分类") + private String category; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysDict.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysDict.java new file mode 100644 index 0000000..d830bd9 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysDict.java @@ -0,0 +1,61 @@ +package com.baiye.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baiye.entity.LogicDeletedBaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 字典表 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_dict") +@Schema(title = "字典表") +public class SysDict extends LogicDeletedBaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 编号 + */ + @TableId + @Schema(title = "编号") + private Long id; + + /** + * 标识 + */ + @Schema(title = "标识") + private String code; + + /** + * 名称 + */ + @Schema(title = "名称") + private String title; + + /** + * Hash值 + */ + @Schema(title = "Hash值") + private String hashCode; + + /** + * 数据类型 + */ + @Schema(title = "数据类型", description = "1:Number 2:String 3:Boolean") + private Integer valueType; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysDictItem.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysDictItem.java new file mode 100644 index 0000000..9cc1ee9 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysDictItem.java @@ -0,0 +1,78 @@ +package com.baiye.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.baiye.entity.LogicDeletedBaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Map; + +/** + * 字典项 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "sys_dict_item", autoResultMap = true) +@Schema(title = "字典项") +public class SysDictItem extends LogicDeletedBaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId + @Schema(title = "ID") + private Long id; + + /** + * 字典标识 + */ + @Schema(title = "字典标识") + private String dictCode; + + /** + * 数据值 + */ + @Schema(title = "数据值") + private String value; + + /** + * 文本值 + */ + @Schema(title = "文本值") + private String name; + + /** + * 状态 + */ + @Schema(title = "状态", description = "1:启用 0:禁用") + private Integer status; + + /** + * 附加属性值 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + @Schema(title = "附加属性值") + private Map attributes; + + /** + * 排序(升序) + */ + @Schema(title = "排序(升序)") + private Integer sort; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysMenu.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysMenu.java new file mode 100644 index 0000000..6b2acd8 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysMenu.java @@ -0,0 +1,124 @@ +package com.baiye.system.model.entity; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baiye.entity.LogicDeletedBaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.Objects; + +/** + * 菜单权限 + * + * @author hccake 2021-04-06 17:59:51 + */ +@Getter +@Setter +@ToString +@TableName("sys_menu") +@Schema(title = "菜单权限") +public class SysMenu extends LogicDeletedBaseEntity { + + /** + * 菜单ID + */ + @TableId + @Schema(title = "菜单ID") + private Long id; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 菜单名称 + */ + @Schema(title = "菜单名称") + private String title; + + /** + * 菜单图标 + */ + @TableField(updateStrategy = FieldStrategy.NOT_NULL) + @Schema(title = "菜单图标") + private String icon; + + /** + * 授权标识 + */ + @Schema(title = "授权标识") + private String permission; + + /** + * 路由地址 + */ + @Schema(title = "路由地址") + private String path; + + /** + * 打开方式 (1组件 2内链 3外链) + */ + @Schema(title = "打开方式 (1组件 2内链 3外链)") + private Integer targetType; + + /** + * 定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址) + */ + @Schema(title = "定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址)") + private String uri; + + /** + * 显示排序 + */ + @Schema(title = "显示排序") + private Integer sort; + + /** + * 组件缓存:0-开启,1-关闭 + */ + @Schema(title = "组件缓存:0-开启,1-关闭") + private Integer keepAlive; + + /** + * 隐藏菜单: 0-否,1-是 + */ + @Schema(title = "隐藏菜单: 0-否,1-是") + private Integer hidden; + + /** + * 菜单类型 (0目录,1菜单,2按钮) + */ + @Schema(title = "菜单类型 (0目录,1菜单,2按钮)") + private Integer type; + + /** + * 备注信息 + */ + @Schema(title = "备注信息") + private String remarks; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SysMenu sysMenu = (SysMenu) o; + return id.equals(sysMenu.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysOrganization.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysOrganization.java new file mode 100644 index 0000000..2414b0a --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysOrganization.java @@ -0,0 +1,66 @@ +package com.baiye.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baiye.entity.LogicDeletedBaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 20:39:40 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_organization") +@Schema(title = "组织架构") +public class SysOrganization extends LogicDeletedBaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId + @Schema(title = "ID") + private Long id; + + /** + * 组织名称 + */ + @Schema(title = "组织名称") + private String name; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 层级信息,从根节点到当前节点的最短路径,使用-分割节点ID + */ + @Schema(title = "层级信息,从根节点到当前节点的最短路径,使用-分割节点ID") + private String hierarchy; + + /** + * 当前节点深度 + */ + @Schema(title = "当前节点深度") + private Integer depth; + + /** + * 排序字段,由小到大 + */ + @Schema(title = "排序字段,由小到大") + private Integer sort; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysRole.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysRole.java new file mode 100644 index 0000000..5e4b2c9 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysRole.java @@ -0,0 +1,69 @@ +package com.baiye.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baiye.entity.LogicDeletedBaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import javax.validation.constraints.NotBlank; +import java.util.Objects; + +/** + * 角色 + * + * @author ballcat code generator 2019-10-14 17:42:23 + */ +@Getter +@Setter +@ToString +@TableName("sys_role") +@Schema(title = "角色") +public class SysRole extends LogicDeletedBaseEntity { + + private static final long serialVersionUID = 1L; + + @TableId + @Schema(title = "角色编号") + private Long id; + + @NotBlank(message = "角色名称不能为空") + @Schema(title = "角色名称") + private String name; + + @NotBlank(message = "角色标识不能为空") + @Schema(title = "角色标识") + private String code; + + @Schema(title = "角色类型,1:系统角色 2:业务角色") + private Integer type; + + @Schema(title = "数据权限") + private Integer scopeType; + + @Schema(title = "数据范围资源,当数据范围类型为自定义时使用") + private String scopeResources; + + @Schema(title = "角色备注") + private String remarks; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SysRole sysRole = (SysRole) o; + return code.equals(sysRole.code); + } + + @Override + public int hashCode() { + return Objects.hash(code); + } + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysRoleMenu.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysRoleMenu.java new file mode 100644 index 0000000..f45adbb --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysRoleMenu.java @@ -0,0 +1,43 @@ +package com.baiye.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 角色菜单表 + * + * @author ballcat code generator 2019-10-14 17:42:23 + */ +@Data +@TableName("sys_role_menu") +@Schema(title = "角色菜单") +public class SysRoleMenu { + + private static final long serialVersionUID = 1L; + + public SysRoleMenu() { + } + + public SysRoleMenu(String roleCode, Long menuId) { + this.roleCode = roleCode; + this.menuId = menuId; + } + + @TableId + private Long id; + + /** + * 角色 Code + */ + @Schema(title = "角色 Code") + private String roleCode; + + /** + * 权限ID + */ + @Schema(title = "菜单id") + private Long menuId; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysUser.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysUser.java new file mode 100644 index 0000000..60e4207 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysUser.java @@ -0,0 +1,110 @@ +package com.baiye.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baiye.entity.LogicDeletedBaseEntity; +import com.baiye.extend.mybatis.plus.alias.TableAlias; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 系统用户表 + * + * @author ballcat code generator 2019-09-12 20:39:31 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableAlias("su") +@TableName("sys_user") +@Schema(title = "系统用户表") +public class SysUser extends LogicDeletedBaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + @TableId + @Schema(title = "用户ID") + private Long userId; + + /** + * 登录账号 + */ + @Schema(title = "登录账号") + private String username; + + /** + * 昵称 + */ + @Schema(title = "昵称") + private String nickname; + + /** + * 密码 + */ + @Schema(title = "密码") + private String password; + + /** + * md5密码盐 + */ + @Schema(title = "md5密码盐") + private String salt; + + /** + * 头像 + */ + @Schema(title = "头像") + private String avatar; + + /** + * 性别(0-默认未知,1-男,2-女) + */ + @Schema(title = "性别(0-默认未知,1-男,2-女)") + private Integer gender; + + /** + * 电子邮件 + */ + @Schema(title = "电子邮件") + private String email; + + /** + * 手机号 + */ + @Schema(title = "手机号") + private String phoneNumber; + + /** + * 状态(1-正常,0-冻结) + */ + @Schema(title = "状态(1-正常, 0-冻结)") + private Integer status; + + /** + * 组织机构ID + */ + @Schema(title = "组织机构ID") + private Long organizationId; + + /** + * 用户类型 + */ + @Schema(title = "1:系统用户, 2:客户用户") + private Integer type; + + /** + * 创建者(父ID) + */ + @Schema(title = "创建者(父ID)") + private Long whichUserId; + + @Schema(title = "分发线索总量") + private Integer distributeNum; + + @Schema(title = "计费状态 0不计费 1计费") + private Integer chargingStatus; +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysUserRole.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysUserRole.java new file mode 100644 index 0000000..d88fd9b --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysUserRole.java @@ -0,0 +1,37 @@ +package com.baiye.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baiye.extend.mybatis.plus.alias.TableAlias; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 用户角色表 + * + * @author ballcat code generator 2019-10-14 17:42:23 + */ +@Data +@TableAlias("ur") +@TableName("sys_user_role") +@Schema(title = "用户角色") +public class SysUserRole { + + private static final long serialVersionUID = 1L; + + @TableId + private Long id; + + /** + * 用户ID + */ + @Schema(title = "用户id") + private Long userId; + + /** + * 角色Code + */ + @Schema(title = "角色Code") + private String roleCode; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysUserRolePromise.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysUserRolePromise.java new file mode 100644 index 0000000..a3c4a1f --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/entity/SysUserRolePromise.java @@ -0,0 +1,64 @@ +package com.baiye.system.model.entity; + +import com.baiye.extend.mybatis.plus.alias.TableAlias; +import com.baiye.extend.mybatis.plus.converter.JsonStringArrayTypeHandler; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * @author Enzo + * @date : 2023/12/5 + */ + +@Data +@TableAlias("urp") +@TableName(value = "sys_user_role_promise", autoResultMap = true) +@Schema(title = "用户角色") +public class SysUserRolePromise implements Serializable { + + private static final long serialVersionUID = 355851270267008536L; + + + @TableId + private Long id; + + /** + * 用户ID + */ + @Schema(title = "用户id") + private Long userId; + + /** + * 公司id + */ + @Schema(title = "公司id") + private Long companyId; + + /** + * 角色Code + */ + @Schema(title = "角色Code") + @TableField(value = "role_codes", typeHandler = JsonStringArrayTypeHandler.class) + private List roleCodes; + + /** + * 权限类型 1 审核 2权限 + */ + @Schema(title = "权限类型") + private Integer roleType; + + + @Schema(title = "是否审核") + private Boolean isAutoAudits; + + + @Schema(title = "ni是否可见") + private Boolean isShowNid; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/RoleBindUserQO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/RoleBindUserQO.java new file mode 100644 index 0000000..295f358 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/RoleBindUserQO.java @@ -0,0 +1,34 @@ +package com.baiye.system.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +import javax.validation.constraints.NotNull; + +/** + * 角色绑定用户查询对象 + * + * @author Hccake + */ +@Data +@Schema(title = "角色绑定用户查询对象") +@ParameterObject +public class RoleBindUserQO { + + @NotNull(message = "角色标识不能为空!") + @Parameter(description = "角色标识") + private String roleCode; + + @Parameter(description = "用户ID") + private Long userId; + + @Parameter(description = "用户名") + private String username; + + @Parameter(description = "组织ID") + private Long organizationId; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysConfigQO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysConfigQO.java new file mode 100644 index 0000000..8e1c143 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysConfigQO.java @@ -0,0 +1,37 @@ +package com.baiye.system.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +/** + * 系统配置表 + * + * @author ballcat code generator 2019-10-14 17:42:23 + */ +@Data +@Schema(title = "基础配置") +@ParameterObject +public class SysConfigQO { + + /** + * 配置名称 + */ + @Parameter(description = "配置名称") + private String name; + + /** + * 配置在缓存中的key名 + */ + @Parameter(description = "配置在缓存中的key名") + private String confKey; + + /** + * 分类 + */ + @Parameter(description = "分类") + private String category; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysDictQO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysDictQO.java new file mode 100644 index 0000000..173cfb2 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysDictQO.java @@ -0,0 +1,33 @@ +package com.baiye.system.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +/** + * 字典表 查询对象 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Data +@Schema(title = "字典表查询对象") +@ParameterObject +public class SysDictQO { + + private static final long serialVersionUID = 1L; + + /** + * 字典标识 + */ + @Parameter(description = "字典标识") + private String code; + + /** + * 字典名称 + */ + @Parameter(description = "字典名称") + private String title; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysMenuQO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysMenuQO.java new file mode 100644 index 0000000..bda1ad8 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysMenuQO.java @@ -0,0 +1,44 @@ +package com.baiye.system.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +/** + * 菜单权限 查询对象 + * + * @author hccake 2021-04-06 17:59:51 + */ +@Data +@Schema(title = "菜单权限查询对象") +@ParameterObject +public class SysMenuQO { + + private static final long serialVersionUID = 1L; + + /** + * 菜单ID + */ + @Parameter(description = "菜单ID") + private Long id; + + /** + * 菜单名称 + */ + @Parameter(description = "菜单名称") + private String title; + + /** + * 授权标识 + */ + @Parameter(description = "授权标识") + private String permission; + + /** + * 路由地址 + */ + @Parameter(description = "路由地址") + private String path; + +} \ No newline at end of file diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysOrganizationQO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysOrganizationQO.java new file mode 100644 index 0000000..1c60241 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysOrganizationQO.java @@ -0,0 +1,27 @@ +package com.baiye.system.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +/** + * 组织架构 查询对象 + * + * @author hccake 2020-09-23 12:09:43 + */ +@Data +@Schema(title = "组织架构查询对象") +@ParameterObject +public class SysOrganizationQO { + + private static final long serialVersionUID = 1L; + + /** + * 组织名称 + */ + @Parameter(description = "组织名称") + private String name; + +} \ No newline at end of file diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysRoleQO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysRoleQO.java new file mode 100644 index 0000000..42b5ac7 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysRoleQO.java @@ -0,0 +1,33 @@ +package com.baiye.system.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +/** + * 角色查询对象 + * + * @author Hccake + */ +@Data +@Schema(title = "角色查询对象") +@ParameterObject +public class SysRoleQO { + + private static final long serialVersionUID = 1L; + + @Parameter(description = "角色名称") + private String name; + + @Parameter(description = "角色标识") + private String code; + + @Parameter(description = "开始时间") + private String startTime; + + @Parameter(description = "结束时间") + private String endTime; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysUserQO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysUserQO.java new file mode 100644 index 0000000..5bcf8a5 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/qo/SysUserQO.java @@ -0,0 +1,75 @@ +package com.baiye.system.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +import java.util.List; + +/** + * @author Hccake 2019/9/22 17:22 + */ +@Data +@Schema(title = "系统用户查询对象") +@ParameterObject +public class SysUserQO { + + /** + * 登录账号 + */ + @Parameter(description = "登录账号") + private String username; + + /** + * 昵称 + */ + @Parameter(description = "昵称") + private String nickname; + + /** + * 性别(0-默认未知,1-男,2-女) + */ + @Parameter(description = "性别(0-默认未知,1-男,2-女)") + private Integer gender; + + /** + * 电子邮件 + */ + @Parameter(description = "电子邮件") + private String email; + + /** + * 手机号 + */ + @Parameter(description = "手机号") + private String phoneNumber; + + /** + * 状态(1-正常,2-冻结) + */ + @Parameter(description = "状态(1-正常,2-冻结)") + private Integer status; + + /** + * 组织机构ID + */ + @Parameter(description = "organizationId") + private List organizationId; + + @Parameter(description = "用户类型:1:系统用户, 2:客户用户") + private Integer type; + + @Parameter(description = "开始时间") + private String startTime; + + @Parameter(description = "结束时间") + private String endTime; + + /** + * 创建者(父ID) + */ + @Parameter(description = "创建者(父ID)") + private Long whichUserId; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/DictDataVO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/DictDataVO.java new file mode 100644 index 0000000..46078ab --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/DictDataVO.java @@ -0,0 +1,39 @@ +package com.baiye.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * @author Hccake 2020/4/9 15:48 + */ +@Data +@Schema(title = "字典数据VO") +public class DictDataVO { + + /** + * 字典标识 + */ + @Schema(title = "字典标识") + private String dictCode; + + /** + * 字典值类型 + */ + @Schema(title = "字典值类型") + private Integer valueType; + + /** + * 字典Hash值 + */ + @Schema(title = "字典Hash值") + private String hashCode; + + /** + * 字典项列表 + */ + @Schema(title = "字典项列表") + private List dictItems; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/DictItemVO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/DictItemVO.java new file mode 100644 index 0000000..565eee6 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/DictItemVO.java @@ -0,0 +1,47 @@ +package com.baiye.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.util.Map; + +/** + * 字典项 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Data +@Schema(title = "字典项VO") +public class DictItemVO { + + private static final long serialVersionUID = 1L; + + @Schema(title = "id") + private Long id; + + /** + * 数据值 + */ + @Schema(title = "数据值") + private String value; + + /** + * 标签 + */ + @Schema(title = "文本值") + private String name; + + /** + * 状态 + */ + @Schema(title = "状态", description = "1:启用 0:禁用") + private Integer status; + + /** + * 附加属性值 + */ + @Schema(title = "附加属性值") + private Map attributes; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/RoleBindUserVO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/RoleBindUserVO.java new file mode 100644 index 0000000..5314f3b --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/RoleBindUserVO.java @@ -0,0 +1,38 @@ +package com.baiye.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 角色绑定的用户 + * + * @author ballcat code generator 2019-09-12 20:39:31 + */ +@Data +@Schema(title = "角色绑定的用户VO") +public class RoleBindUserVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(title = "用户ID") + private Long userId; + + @Schema(title = "登录账号") + private String username; + + @Schema(title = "昵称") + private String nickname; + + @Schema(title = "1:系统用户, 2:客户用户") + private Integer type; + + @Schema(title = "组织机构ID") + private Long organizationId; + + @Schema(title = "组织机构名称") + private String organizationName; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysConfigPageVO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysConfigPageVO.java new file mode 100644 index 0000000..948e50c --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysConfigPageVO.java @@ -0,0 +1,66 @@ +package com.baiye.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 系统配置表 + * + * @author ballcat code generator 2019-10-14 17:42:23 + */ +@Data +@Schema(title = "基础配置") +public class SysConfigPageVO { + + /** + * 主键 + */ + @Schema(title = "主键ID") + private Long id; + + /** + * 配置名称 + */ + @Schema(title = "配置名称") + private String name; + + /** + * 配置在缓存中的key名 + */ + @Schema(title = "配置在缓存中的key名") + private String confKey; + + /** + * 配置值 + */ + @Schema(title = "配置值") + private String confValue; + + /** + * 分类 + */ + @Schema(title = "分类") + private String category; + + /** + * 描述 + */ + @Schema(title = "描述") + private String remarks; + + /** + * 创建时间 + */ + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 修改时间 + */ + @Schema(title = "修改时间") + private LocalDateTime updateTime; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysDictItemPageVO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysDictItemPageVO.java new file mode 100644 index 0000000..3ee1677 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysDictItemPageVO.java @@ -0,0 +1,84 @@ +package com.baiye.system.model.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 字典项 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Data +@Schema(title = "字典项") +public class SysDictItemPageVO { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(title = "ID") + private Long id; + + /** + * 字典标识 + */ + @Schema(title = "字典标识") + private String dictCode; + + /** + * 数据值 + */ + @Schema(title = "数据值") + private String value; + + /** + * 文本值 + */ + @Schema(title = "文本值") + private String name; + + /** + * 状态 + */ + @Schema(title = "状态", description = "1:启用 0:禁用") + private Integer status; + + /** + * 附加属性值 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + @Schema(title = "附加属性值") + private Map attributes; + + /** + * 排序(升序) + */ + @Schema(title = "排序(升序)") + private Integer sort; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + + /** + * 创建时间 + */ + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @Schema(title = "更新时间") + private LocalDateTime updateTime; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysDictPageVO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysDictPageVO.java new file mode 100644 index 0000000..7dbb9ee --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysDictPageVO.java @@ -0,0 +1,67 @@ +package com.baiye.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 字典表 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Data +@Schema(title = "字典表") +public class SysDictPageVO { + + private static final long serialVersionUID = 1L; + + /** + * 编号 + */ + @Schema(title = "编号") + private Long id; + + /** + * 标识 + */ + @Schema(title = "标识") + private String code; + + /** + * 名称 + */ + @Schema(title = "名称") + private String title; + + /** + * Hash值 + */ + @Schema(title = "Hash值") + private String hashCode; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + + /** + * 数据类型 + */ + @Schema(title = "数据类型", description = "1:Number 2:String 3:Boolean") + private Integer valueType; + + /** + * 创建时间 + */ + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @Schema(title = "更新时间") + private LocalDateTime updateTime; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysMenuGrantVO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysMenuGrantVO.java new file mode 100644 index 0000000..de01257 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysMenuGrantVO.java @@ -0,0 +1,46 @@ +package com.baiye.system.model.vo; + +import com.baiye.I18nClass; +import com.baiye.I18nField; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +/** + * 菜单权限授权对象 + * + * @author hccake 2021-04-06 17:59:51 + */ +@Data +@I18nClass +@Schema(title = "菜单权限授权对象") +public class SysMenuGrantVO { + + private static final long serialVersionUID = 1L; + + /** + * 菜单ID + */ + @Schema(title = "菜单ID") + private Long id; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 菜单名称 + */ + @I18nField(condition = "type != 2") + @Schema(title = "菜单名称") + private String title; + + /** + * 菜单类型 (0目录,1菜单,2按钮) + */ + @Schema(title = "菜单类型 (0目录,1菜单,2按钮)") + private Integer type; + +} \ No newline at end of file diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysMenuPageVO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysMenuPageVO.java new file mode 100644 index 0000000..0976c37 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysMenuPageVO.java @@ -0,0 +1,120 @@ +package com.baiye.system.model.vo; + +import com.baiye.I18nClass; +import com.baiye.I18nField; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 菜单权限分页视图对象 + * + * @author hccake 2021-04-06 17:59:51 + */ +@I18nClass +@Data +@Schema(title = "菜单权限分页视图对象") +public class SysMenuPageVO { + + private static final long serialVersionUID = 1L; + + /** + * 菜单ID + */ + @Schema(title = "菜单ID") + private Long id; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 菜单名称 + */ + @Schema(title = "菜单名称") + private String title; + + /** + * 菜单名称 + */ + @I18nField(condition = "type != 2") + @Schema(title = "菜单名称") + private String i18nTitle; + + /** + * 菜单图标 + */ + @Schema(title = "菜单图标") + private String icon; + + /** + * 授权标识 + */ + @Schema(title = "授权标识") + private String permission; + + /** + * 路由地址 + */ + @Schema(title = "路由地址") + private String path; + + /** + * 打开方式 (1组件 2内链 3外链) + */ + @Schema(title = "打开方式 (1组件 2内链 3外链)") + private Integer targetType; + + /** + * 定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址) + */ + @Schema(title = "定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址)") + private String uri; + + /** + * 显示排序 + */ + @Schema(title = "显示排序") + private Integer sort; + + /** + * 组件缓存:0-开启,1-关闭 + */ + @Schema(title = "组件缓存:0-开启,1-关闭") + private Integer keepAlive; + + /** + * 隐藏菜单: 0-否,1-是 + */ + @Schema(title = "隐藏菜单: 0-否,1-是") + private Integer hidden; + + /** + * 菜单类型 (0目录,1菜单,2按钮) + */ + @Schema(title = "菜单类型 (0目录,1菜单,2按钮)") + private Integer type; + + /** + * 备注信息 + */ + @Schema(title = "备注信息") + private String remarks; + + /** + * 创建时间 + */ + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @Schema(title = "更新时间") + private LocalDateTime updateTime; + +} \ No newline at end of file diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysMenuRouterVO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysMenuRouterVO.java new file mode 100644 index 0000000..b8a680d --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysMenuRouterVO.java @@ -0,0 +1,88 @@ +package com.baiye.system.model.vo; + +import com.baiye.I18nClass; +import com.baiye.I18nField; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +/** + * 菜单权限视图对象 + * + * @author hccake 2021-04-06 17:59:51 + */ +@Data +@I18nClass +@Schema(title = "菜单权限视图对象") +public class SysMenuRouterVO { + + private static final long serialVersionUID = 1L; + + /** + * 菜单ID + */ + @Schema(title = "菜单ID") + private Long id; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 菜单名称 + */ + @I18nField(condition = "type != 2") + @Schema(title = "菜单名称") + private String title; + + /** + * 菜单图标 + */ + @Schema(title = "菜单图标") + private String icon; + + /** + * 路由地址 + */ + @Schema(title = "路由地址") + private String path; + + /** + * 打开方式 (1组件 2内链 3外链) + */ + @Schema(title = "打开方式 (1组件 2内链 3外链)") + private Integer targetType; + + /** + * 定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址) + */ + @Schema(title = "定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址)") + private String uri; + + /** + * 组件缓存:0-开启,1-关闭 + */ + @Schema(title = "组件缓存:0-开启,1-关闭") + private Integer keepAlive; + + /** + * 隐藏菜单: 0-否,1-是 + */ + @Schema(title = "隐藏菜单: 0-否,1-是") + private Integer hidden; + + /** + * 菜单类型 (0目录,1菜单,2按钮) + */ + @Schema(title = "菜单类型 (0目录,1菜单,2按钮)") + private Integer type; + + /** + * 备注信息 + */ + @Schema(title = "备注信息") + private String remarks; + +} \ No newline at end of file diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysOrganizationTree.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysOrganizationTree.java new file mode 100644 index 0000000..c716821 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysOrganizationTree.java @@ -0,0 +1,103 @@ +package com.baiye.system.model.vo; + +import com.baiye.util.tree.TreeNode; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 12:09:43 + */ +@Data +@Schema(title = "组织架构") +public class SysOrganizationTree implements TreeNode { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(title = "ID") + private Long id; + + /** + * 组织名称 + */ + @Schema(title = "组织名称") + private String name; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 层级信息,从根节点到当前节点的最短路径,使用-分割节点ID + */ + @Schema(title = "层级信息,从根节点到当前节点的最短路径,使用-分割节点ID") + private String hierarchy; + + /** + * 当前节点深度 + */ + @Schema(title = "当前节点深度") + private Integer depth; + + /** + * 排序字段,由小到大 + */ + @Schema(title = "排序字段,由小到大") + private Integer sort; + + /** + * 描述信息 + */ + @Schema(title = "描述信息") + private String remarks; + + /** + * 创建时间 + */ + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @Schema(title = "更新时间") + private LocalDateTime updateTime; + + /** + * 下级组织 + */ + @Schema(title = "下级组织") + List children = new ArrayList<>(); + + /** + * 设置节点的子节点列表 + * @param children 子节点 + */ + @Override + @SuppressWarnings("unchecked") + public > void setChildren(List children) { + this.children = (List) children; + } + + @Override + public Long getKey() { + return this.id; + } + + @Override + public Long getParentKey() { + return this.parentId; + } + +} \ No newline at end of file diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysOrganizationVO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysOrganizationVO.java new file mode 100644 index 0000000..6e841d7 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysOrganizationVO.java @@ -0,0 +1,60 @@ +package com.baiye.system.model.vo; + +import com.baiye.entity.BaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 20:39:40 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(title = "组织架构") +public class SysOrganizationVO extends BaseEntity { + + /** + * ID + */ + @Schema(title = "ID") + private Long id; + + /** + * 组织名称 + */ + @Schema(title = "组织名称") + private String name; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 层级信息,从根节点到当前节点的最短路径,使用-分割节点ID + */ + @Schema(title = "层级信息,从根节点到当前节点的最短路径,使用-分割节点ID") + private String hierarchy; + + /** + * 当前节点深度 + */ + @Schema(title = "当前节点深度") + private Integer depth; + + /** + * 排序字段,由小到大 + */ + @Schema(title = "排序字段,由小到大") + private Integer sort; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysRolePageVO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysRolePageVO.java new file mode 100644 index 0000000..8a28dc4 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysRolePageVO.java @@ -0,0 +1,50 @@ +package com.baiye.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + *

          + * 角色表 + *

          + * + * @author ballcat + * @since 2017-10-29 + */ +@Data +@Schema(title = "角色") +public class SysRolePageVO { + + private static final long serialVersionUID = 1L; + + @Schema(title = "角色编号") + private Long id; + + @Schema(title = "角色名称") + private String name; + + @Schema(title = "角色标识") + private String code; + + @Schema(title = "角色类型,1:系统角色 2:业务角色") + private Integer type; + + @Schema(title = "数据权限类型") + private Integer scopeType; + + @Schema(title = "数据范围资源,当数据范围类型为自定义时使用") + private String scopeResources; + + @Schema(title = "角色备注") + private String remarks; + + @Schema(title = "创建时间") + private LocalDateTime createTime; + + @Schema(title = "更新时间") + private LocalDateTime updateTime; + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysUserBalanceVO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysUserBalanceVO.java new file mode 100644 index 0000000..20844fb --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysUserBalanceVO.java @@ -0,0 +1,37 @@ +package com.baiye.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 用户余额VO + */ +@Data +@Schema(title = "用户余额VO") +public class SysUserBalanceVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(title = "用户ID") + private Long userId; + + @Schema(title = "登录账号") + private String username; + + @Schema(title = "创建者(父ID)") + private Long whichUserId; + + @Schema(title = "计费状态 0不计费 1计费") + private Integer chargingStatus; + + @Schema(title = "剩余线索数量") + private Integer residueClueNum; + + @Schema(title = "操作数量") + private Integer distributeNum; + + + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysUserInfo.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysUserInfo.java new file mode 100644 index 0000000..da74c31 --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysUserInfo.java @@ -0,0 +1,93 @@ +package com.baiye.system.model.vo; + +import com.baiye.system.model.entity.SysRole; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 系统用户信息 + * + * @author ballcat code generator 2019-09-12 20:39:31 + */ +@Data +@Schema(title = "系统用户信息") +public class SysUserInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + @Schema(title = "用户ID") + private Long userId; + + /** + * 登录账号 + */ + @Schema(title = "登录账号") + private String username; + + /** + * 昵称 + */ + @Schema(title = "昵称") + private String nickname; + + /** + * 头像 + */ + @Schema(title = "头像") + private String avatar; + + /** + * 组织机构ID + */ + @Schema(title = "组织机构ID") + private Long organizationId; + + /** + * 性别(0-默认未知,1-男,2-女) + */ + @Schema(title = "性别(0-默认未知,1-男,2-女)") + private Integer gender; + + /** + * 电子邮件 + */ + @Schema(title = "电子邮件") + private String email; + + /** + * 手机号 + */ + @Schema(title = "手机号") + private String phoneNumber; + + /** + * 用户类型 + */ + @Schema(title = "用户类型:1-系统用户,2-客户用户") + private Integer type; + + + /** + * 业务员类型 + */ + @Schema(title = "业务员类型") + private Integer salesmanType; + + /** + * 创建者(父ID) + */ + @Schema(title = "创建者(父ID)") + private Long whichUserId; + + + private List roleList; + + +} diff --git a/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysUserPageVO.java b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysUserPageVO.java new file mode 100644 index 0000000..86e0e6a --- /dev/null +++ b/marketing-scrm-system/system-model/src/main/java/com/baiye/system/model/vo/SysUserPageVO.java @@ -0,0 +1,118 @@ +package com.baiye.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 系统用户表 + */ +@Data +@Schema(title = "系统用户VO") +public class SysUserPageVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + @Schema(title = "用户ID") + private Long userId; + + /** + * 登录账号 + */ + @Schema(title = "登录账号") + private String username; + + /** + * 昵称 + */ + @Schema(title = "昵称") + private String nickname; + + /** + * 头像 + */ + @Schema(title = "头像") + private String avatar; + + /** + * 性别(0-默认未知,1-男,2-女) + */ + @Schema(title = "性别(0-默认未知,1-男,2-女)") + private Integer gender; + + /** + * 电子邮件 + */ + @Schema(title = "电子邮件") + private String email; + + /** + * 手机号 + */ + @Schema(title = "手机号") + private String phoneNumber; + + /** + * 状态(1-正常,0-冻结) + */ + @Schema(title = "状态(1-正常, 0-冻结)") + private Integer status; + + @Schema(title = "用户类型:1-系统用户,2-客户用户") + private Integer type; + + /** + * 创建者(父ID) + */ + @Schema(title = "创建者(父ID)") + private Long whichUserId; + + @Schema(title = "分发线索总量") + private Integer distributeNum; + + /** + * 组织机构ID + */ + @Schema(title = "组织机构ID") + private Long organizationId; + + /** + * 组织机构名称 + */ + @Schema(title = "组织机构名称") + private String organizationName; + + /** + * 创建时间 + */ + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @Schema(title = "更新时间") + private LocalDateTime updateTime; + + private List children; + + @Schema(title = "角色标识") + private String roleCode; + + @Schema(title = "是否是专员") + private Boolean isSalesman = false; + + @Schema(title = "计费状态 0不计费 1计费") + private Integer chargingStatus; + + @Schema(title = "剩余线索数量") + private Integer residueClueNum; + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..195a40d --- /dev/null +++ b/pom.xml @@ -0,0 +1,551 @@ + + + 4.0.0 + com.baiye + marketing-scrm + ${revision} + pom + + + admin + marketing-scrm-system + marketing-scrm-common + marketing-scrm-security + marketing-scrm-admin + marketing-scrm-notify + marketing-scrm-starters + marketing-scrm-extends + + + + 1.0.0 + 1.8 + UTF-8 + UTF-8 + + + 1.5.0 + 3.11.0 + 1.6 + 3.3.1 + 3.0.0 + 3.5.0 + 3.3.0 + 3.1.0 + 1.6.13 + 0.0.39 + + + 4.11.28.ALL + 3.3.2 + 1.2.83 + 5.8.16 + 2.2.7 + 2.7.0 + 1.15.3 + 4.3 + 2.6.3 + 1.18.26 + 1.5.3.Final + 3.5.3.1 + 3.5.10 + 4.1.2 + 2.2.0 + 1.3.2 + 2.2.0 + 2.20.70 + 0.4.2 + 2.7.12 + 1.7.0 + 3.0.0 + 1.6.8 + 0.4.2 + 2.4.0 + 1.6.2 + + 1.53.0 + 3.21.7 + 0.6.1 + 1.7.1 + 1.3.2 + + + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + import + pom + + + + cn.hutool + hutool-bom + ${hutool.version} + import + pom + + + + + cn.hutool + hutool-all + ${hutool.version} + + + + + org.springdoc + springdoc-openapi + ${springdoc-openapi.verison} + import + pom + + + + software.amazon.awssdk + bom + ${software.amazon.awssdk.version} + import + pom + + + + + + com.alibaba + easyexcel + ${easyexcel.version} + + + + com.alibaba + fastjson + ${fastjson.version} + true + + + com.baomidou + mybatis-plus-annotation + ${mybatis-plus.version} + + + + com.baiye + common-i18n + 1.1.0 + + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-core + ${mybatis-plus.version} + + + + com.baomidou + mybatis-plus-extension + ${mybatis-plus.version} + + + + com.github.jsqlparser + jsqlparser + ${jsqlparser.version} + + + com.nimbusds + nimbus-jose-jwt + 9.15.2 + + + + commons-net + commons-net + 3.9.0 + + + io.springfox + springfox-boot-starter + ${springfox.version} + + + io.springfox + springfox-swagger-ui + ${springfox.version} + + + + io.springfox + springfox-swagger2 + ${springfox.version} + + + + io.swagger.core.v3 + swagger-annotations + ${io.swagger.v3.version} + + + io.swagger.core.v3 + swagger-models + ${io.swagger.v3.version} + + + + io.swagger + swagger-annotations + ${swagger.version} + + + io.swagger + swagger-models + ${swagger.version} + + + + org.apache.poi + poi-ooxml + ${poi.version} + + + org.apache.rocketmq + rocketmq-spring-boot-starter + ${rocketmq.version} + + + + org.jsoup + jsoup + ${jsoup.version} + + + + org.lionsoul + ip2region + ${ip2region.version} + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + org.mybatis + mybatis + ${mybatis.version} + + + + org.projectlombok + lombok + ${lombok.version} + + + + org.springframework.security + spring-security-oauth2-authorization-server + ${spring-authorization-server.version} + + + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + io.grpc + grpc-netty + ${grpc.version} + + + + + com.baiye + admin-websocket + ${revision} + + + + com.baiye + marketing-scrm-starter-job + ${revision} + + + + com.baiye + common-core + ${revision} + + + com.baiye + common-desensitize + ${revision} + + + com.baiye + common-idempotent + ${revision} + + + com.baiye + common-log + ${revision} + + + com.baiye + common-model + ${revision} + + + com.baiye + common-redis + ${revision} + + + com.baiye + common-util + ${revision} + + + com.baiye + common-websocket + ${revision} + + + + com.baiye + marketing-scrm-extend-mybatis-plus + ${revision} + + + + com.baiye + security-core + ${revision} + + + + com.baiye + marketing-scrm-starter-file + ${revision} + + + + + com.baiye + marketing-scrm-starter-redis + ${revision} + + + + com.baiye + marketing-scrm-starter-swagger + ${revision} + + + + + com.baiye + security-oauth2-authorization-server + ${revision} + + + com.baiye + security-oauth2-core + ${revision} + + + com.baiye + security-oauth2-resource-server + ${revision} + + + + com.baiye + system-biz + ${revision} + + + com.baiye + system-controller + ${revision} + + + com.baiye + system-model + ${revision} + + + + + + com.github.whvcse + easy-captcha + ${captcha.version} + + + + net.lingala.zip4j + zip4j + ${zip4j.version} + + + + + + com.alipay.sdk + alipay-easysdk + ${alipay.version} + + + + + + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + + sign + + sign-artifacts + verify + + + + + + ossrh + + + + + + + + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + ossrh + + + https://oss.sonatype.org/content/repositories/snapshots + ossrh + + + + + + + + true + src/main/resources + + **/*.yml + **/*.yaml + logback-spring.xml + + + + false + src/main/resources + + **/*.yml + **/*.yaml + logback-spring.xml + + + + + + + org.codehaus.mojo + flatten-maven-plugin + + resolveCiFriendliesOnly + true + + + + + flatten + + flatten + process-resources + + + + clean + + flatten.clean + clean + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.verison} + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.codehaus.mojo + flatten-maven-plugin + ${flatten-maven-plugin.version} + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + + +