事务
事务的理解
事务是访问数据库的一个操作序列,数据库应用系统通过事务集来完成对数据库的存取。事务的正确执行使得数据库从一种状态转换成另一种状态。
事务必须服从ISO/IEC所制定的ACID原则。ACID是原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)的缩写事务必须服从ISO/IEC所制定的ACID原则。ACID是原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)的缩写。
- 原子性。即不可分割性,事务要么全部被执行,要么就全部不被执行。如果事务的所有子事务全部提交成功,则所有的数据库操作被提交,数据库状态发生转换;如果有子事务失败,则其他子事务的数据库操作被回滚,即数据库回到事务执行前的状态,不会发生状态转换。
- 一致性。事务的执行使得数据库从一种正确状态转换成另一种正确状态。
- 隔离性。在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何其他事务,即在事务正确提交之前,它可能的结果不应显示给任何其他事务。
持久性。事务正确提交后,其结果将永久保存在数据库中,即使在事务提交后有了其他故障,事务的处理结果也会得到保存。
事务的使用
Spring采用注解的方式控制事务,一般在service层面上进行事务的控制,控制事务的注解为@Transactional
即在service层中需要控制事务的类(不是接口类,而是实现类)或者方法(要求是public方法)上添加注解@Transactional,系统设计要求尽量放在需要进行事务管理的方法上,而不是放在所有接口实现类上。
只针对写操作,读操作就没必要了
示例代码:/*** 事务控制在service层面* 加上注解:@Transactional,声明的方法就是一个独立的事务(有异常DB操作全部回滚)*/@Transactionalpublic void testTran() {JeecgDemo pp = new JeecgDemo();pp.setAge(1111);pp.setName("测试事务 小白兔 1");jeecgDemoMapper.insert(pp);JeecgDemo pp2 = new JeecgDemo();pp2.setAge(2222);pp2.setName("测试事务 小白兔 2");jeecgDemoMapper.insert(pp2);Integer.parseInt("hello");//自定义异常JeecgDemo pp3 = new JeecgDemo();pp3.setAge(3333);pp3.setName("测试事务 小白兔 3");jeecgDemoMapper.insert(pp3);}
错误的使用
接口中A、B两个方法,A无@Transactional标签,B有,上层通过A间接调用B,此时事务不生效。
- 接口中异常(运行时异常)被捕获而没有被抛出。默认配置下,spring 只有在抛出的异常为运行时 unchecked 异常时才回滚该事务,也就是抛出的异常为RuntimeException 的子类(Errors也会导致事务回滚),而抛出 checked 异常则不会导致事务回滚 。可通过 @Transactional rollbackFor进行配置。
多线程下事务管理因为线程不属于 spring 托管,故线程不能够默认使用 spring 的事务,也不能获取spring 注入的 bean 。在被 spring 声明式事务管理的方法内开启多线程,多线程内的方法不被事务控制。一个使用了@Transactional 的方法,如果方法内包含多线程的使用,方法内部出现异常,不会回滚线程中调用方法的事务。
@Transactional注解
@Transactional 实质是使用了 JDBC 的事务来进行事务控制的,基于 Spring 的动态代理的机制事务开始时,通过AOP机制,生成一个代理connection对象,并将其放入 DataSource 实例的某个与 DataSourceTransactionManager 相关的某处容器中。在接下来的整个事务中,客户代码都应该使用该connection 连接数据库,执行所有数据库命令。[不使用该 connection 连接数据库执行的数据库命令,在本事务回滚的时候得不到回滚](物理连接 connection 逻辑上新建一个会话session;DataSource 与TransactionManager 配置相同的数据源)
- 事务结束时,回滚在第1步骤中得到的代理 connection 对象上执行的数据库命令,然后关闭该代理 connection 对象。(事务结束后,回滚操作不会对已执行完毕的SQL操作命令起作用)
@Transactional属性配置

字段说明:
- value :主要用来指定不同的事务管理器;主要用来满足在同一个系统中,存在不同的事务管理器。比如在Spring中,声明了两种事务管理器txManager1, txManager2。然后,用户可以根据这个参数来根据需要指定特定的txManager.
- value 适用场景:在一个系统中,需要访问多个数据源或者多个数据库,则必然会配置多个事务管理器的
- REQUIRED_NEW:内部的事务独立运行,在各自的作用域中,可以独立的回滚或者提交;而外部的事务将不受内部事务的回滚状态影响。
- ESTED 的事务,基于单一的事务来管理,提供了多个保存点。这种多个保存点的机制允许内部事务的变更触发外部事务的回滚。而外部事务在混滚之后,仍能继续进行事务处理,即使部分操作已经被混滚。 由于这个设置基于 JDBC 的保存点,所以只能工作在 JDB C的机制。
- rollbackFor:让受检查异常回滚;即让本来不应该回滚的进行回滚操作。
- noRollbackFor:忽略非检查异常;即让本来应该回滚的不进行回滚操作。
多数据源的事务使用
# 数据源spring:datasource:# master数据源配置mysql:driverClassName: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8username: rootpassword: root# cluster数据源配置oracle:driver-class-name: oracle.jdbc.driver.OracleDriverurl: jdbc:oracle:thin:@localhost:1521:orclusername: rootpassword: root
根据不同的数据库创建对应的事务管理器
@Bean(name = "mysqlTransactionManager")@Bean(name = "oracleTransactionManager")
@Configuration@MapperScan(basePackages = "com.spring.boot.mapper.mysql",sqlSessionTemplateRef = ",mysqlSqlSessionTemplate")public class ClusterDataSourceConfig {/*** 创建数据源*@return DataSource*/@Bean(name = "mysqlDataSource")@ConfigurationProperties(prefix = "spring.datasource.mysql")public DataSource masterDataSource() {return DataSourceBuilder.create().build();}/*** 创建工厂*@param dataSource*@throws Exception*@return SqlSessionFactory*/@Bean(name = "mysqlSqlSessionFactory")public SqlSessionFactory masterSqlSessionFactory(@Qualifier("mysqlDataSource") DataSource dataSource) throws Exception {SqlSessionFactoryBean bean = new SqlSessionFactoryBean();bean.setDataSource(dataSource);bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/mysql/*.xml"));return bean.getObject();}/*** 创建事务*@param dataSource*@return DataSourceTransactionManager*/@Bean(name = "mysqlTransactionManager")public DataSourceTransactionManager masterDataSourceTransactionManager(@Qualifier("mysqlDataSource") DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}/*** 创建模板*@param sqlSessionFactory*@return SqlSessionTemplate*/@Bean(name = "clusterSqlSessionTemplate")public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("clusterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {return new SqlSessionTemplate(sqlSessionFactory);}}
@Configuration@MapperScan(basePackages = "com.spring.boot.mapper.oracle",sqlSessionTemplateRef = "oracleSessionTemplate")public class MasterDataSourceConfig {/*** 创建数据源*@return DataSource*/@Bean(name = "oracleDataSource")@ConfigurationProperties(prefix = "spring.datasource.oracle")@Primarypublic DataSource masterDataSource() {return DataSourceBuilder.create().build();}/*** 创建工厂*@param dataSource*@throws Exception*@return SqlSessionFactory*/@Bean(name = "oracleSessionFactory")@Primarypublic SqlSessionFactory masterSqlSessionFactory(@Qualifier("oracleDataSource") DataSource dataSource) throws Exception {SqlSessionFactoryBean bean = new SqlSessionFactoryBean();bean.setDataSource(dataSource);bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/oracle/*.xml"));return bean.getObject();}/*** 创建事务*@param dataSource*@return DataSourceTransactionManager*/@Bean(name = "oracleTransactionManager")@Primarypublic DataSourceTransactionManager masterDataSourceTransactionManager(@Qualifier("oracleDataSource") DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}/*** 创建模板*@param sqlSessionFactory*@return SqlSessionTemplate*/@Bean(name = "oracleSessionTemplate")@Primarypublic SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("oracleSessionFactory") SqlSessionFactory sqlSessionFactory) {return new SqlSessionTemplate(sqlSessionFactory);}}
在service层添加@Transactional注解
@Transactional(readOnly = true,rollbackFor = Exception.class,transactionManager = "clusterTransactionManager",isolation = Isolation.READ_UNCOMMITTED,propagation = Propagation.REQUIRED)
拓展
在微服务架构中可使用springcloud alibaba中的微服务seata中的@GlobalTransactional注解,其功能和用法与@Transactional差不多,不过@GlobalTransactional是做全局事务控制的,而@Transactional是非全局的。
JeecgBoot的定时任务
不带参的任务
- 首先新建一个任务类并实现Job接口,在其实现的方法上写上相应的任务,用于定时执行
示例代码:
/*** 示例不带参定时任务** @author Scott*/@Slf4jpublic class SampleJob implements Job {@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {log.info(String.format(" Jeecg-Boot 普通定时任务 SampleJob ! 时间:" + DateUtils.getTimestamp()));}}
- 然后进入JeecgBoot后台添加定时任务

- 任务参数设置
带参的任务
/*** 示例带参定时任务** @Author Scott*/@Slf4jpublic class SampleParamJob implements Job {/*** 若参数变量名修改 QuartzJobController中也需对应修改*/private String parameter;public void setParameter(String parameter) {this.parameter = parameter;}@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {log.info(" Job Execution key:"+jobExecutionContext.getJobDetail().getKey());log.info( String.format("welcome %s! Jeecg-Boot 带参数定时任务 SampleParamJob ! 时间:" + DateUtils.now(), this.parameter));}}
实现类
package org.jeecg.modules.quartz.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import lombok.extern.slf4j.Slf4j;import org.jeecg.common.constant.CommonConstant;import org.jeecg.common.exception.JeecgBootException;import org.jeecg.common.util.DateUtils;import org.jeecg.modules.quartz.entity.QuartzJob;import org.jeecg.modules.quartz.mapper.QuartzJobMapper;import org.jeecg.modules.quartz.service.IQuartzJobService;import org.quartz.*;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.util.Date;import java.util.List;/*** @Description: 定时任务在线管理* @Author: jeecg-boot* @Date: 2019-04-28* @Version: V1.1*/@Slf4j@Servicepublic class QuartzJobServiceImpl extends ServiceImpl<QuartzJobMapper, QuartzJob> implements IQuartzJobService {@Autowiredprivate QuartzJobMapper quartzJobMapper;@Autowiredprivate Scheduler scheduler;/*** 立即执行的任务分组*/private static final String JOB_TEST_GROUP = "test_group";@Overridepublic List<QuartzJob> findByJobClassName(String jobClassName) {return quartzJobMapper.findByJobClassName(jobClassName);}/*** 保存&启动定时任务*/@Override@Transactional(rollbackFor = JeecgBootException.class)public boolean saveAndScheduleJob(QuartzJob quartzJob) {// DB设置修改quartzJob.setDelFlag(CommonConstant.DEL_FLAG_0);boolean success = this.save(quartzJob);if (success) {if (CommonConstant.STATUS_NORMAL.equals(quartzJob.getStatus())) {// 定时器添加this.schedulerAdd(quartzJob.getId(), quartzJob.getJobClassName().trim(), quartzJob.getCronExpression().trim(), quartzJob.getParameter());}}return success;}/*** 恢复定时任务*/@Override@Transactional(rollbackFor = JeecgBootException.class)public boolean resumeJob(QuartzJob quartzJob) {schedulerDelete(quartzJob.getId());schedulerAdd(quartzJob.getId(), quartzJob.getJobClassName().trim(), quartzJob.getCronExpression().trim(), quartzJob.getParameter());quartzJob.setStatus(CommonConstant.STATUS_NORMAL);return this.updateById(quartzJob);}/*** 编辑&启停定时任务* @throws SchedulerException*/@Override@Transactional(rollbackFor = JeecgBootException.class)public boolean editAndScheduleJob(QuartzJob quartzJob) throws SchedulerException {if (CommonConstant.STATUS_NORMAL.equals(quartzJob.getStatus())) {schedulerDelete(quartzJob.getId());schedulerAdd(quartzJob.getId(), quartzJob.getJobClassName().trim(), quartzJob.getCronExpression().trim(), quartzJob.getParameter());}else{scheduler.pauseJob(JobKey.jobKey(quartzJob.getId()));}return this.updateById(quartzJob);}/*** 删除&停止删除定时任务*/@Override@Transactional(rollbackFor = JeecgBootException.class)public boolean deleteAndStopJob(QuartzJob job) {schedulerDelete(job.getId());boolean ok = this.removeById(job.getId());return ok;}@Overridepublic void execute(QuartzJob quartzJob) throws Exception {String jobName = quartzJob.getJobClassName().trim();Date startDate = new Date();String ymd = DateUtils.date2Str(startDate,DateUtils.yyyymmddhhmmss.get());String identity = jobName + ymd;//3秒后执行 只执行一次// update-begin--author:sunjianlei ---- date:20210511--- for:定时任务立即执行,延迟3秒改成0.1秒-------startDate.setTime(startDate.getTime() + 100L);// update-end--author:sunjianlei ---- date:20210511--- for:定时任务立即执行,延迟3秒改成0.1秒-------// 定义一个TriggerSimpleTrigger trigger = (SimpleTrigger)TriggerBuilder.newTrigger().withIdentity(identity, JOB_TEST_GROUP).startAt(startDate).build();// 构建job信息JobDetail jobDetail = JobBuilder.newJob(getClass(jobName).getClass()).withIdentity(identity).usingJobData("parameter", quartzJob.getParameter()).build();// 将trigger和 jobDetail 加入这个调度scheduler.scheduleJob(jobDetail, trigger);// 启动schedulerscheduler.start();}@Override@Transactional(rollbackFor = JeecgBootException.class)public void pause(QuartzJob quartzJob){schedulerDelete(quartzJob.getId());quartzJob.setStatus(CommonConstant.STATUS_DISABLE);this.updateById(quartzJob);}/*** 添加定时任务** @param jobClassName* @param cronExpression* @param parameter*/private void schedulerAdd(String id, String jobClassName, String cronExpression, String parameter) {try {// 启动调度器scheduler.start();// 构建job信息JobDetail jobDetail = JobBuilder.newJob(getClass(jobClassName).getClass()).withIdentity(id).usingJobData("parameter", parameter).build();// 表达式调度构建器(即任务执行的时间)CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);// 按新的cronExpression表达式构建一个新的triggerCronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(id).withSchedule(scheduleBuilder).build();scheduler.scheduleJob(jobDetail, trigger);} catch (SchedulerException e) {throw new JeecgBootException("创建定时任务失败", e);} catch (RuntimeException e) {throw new JeecgBootException(e.getMessage(), e);}catch (Exception e) {throw new JeecgBootException("后台找不到该类名:" + jobClassName, e);}}/*** 删除定时任务** @param id*/private void schedulerDelete(String id) {try {scheduler.pauseTrigger(TriggerKey.triggerKey(id));scheduler.unscheduleJob(TriggerKey.triggerKey(id));scheduler.deleteJob(JobKey.jobKey(id));} catch (Exception e) {log.error(e.getMessage(), e);throw new JeecgBootException("删除定时任务失败");}}private static Job getClass(String classname) throws Exception {Class<?> class1 = Class.forName(classname);return (Job) class1.newInstance();}}
Cron表达式

七个参数,从第一个到最后一个分表表示秒、分、时、日、月、周、年
-:区间
/:循环
,:第几
?:不设置
*:每
日和周只能设置其中之一
JeecgBoot的系统通知接口
短信通知接口
DySmsHelper.sendSms(mobile, obj,DySmsEnum.REGISTER_TEMPLATE_CODE)
mobile:短信接收方的手机号
obj:模板中的变量替换JSON串,如模板内容为”亲爱的${name},您的验证码为${code}”
DySmsEnum.REGISTER_TEMPLATE_CODE:枚举类中的变量,包含短信签名(signName)、短信模板code(templateCode)、短信模板必需的数据名称(keys),多个key以逗号分隔
需要注意的是这里的签名和code是短信服务里的,而不是下图中的code,目前jeecgboot使用的短信服务是阿里的
/*** Created on 17/6/7.* 短信API产品的DEMO程序,工程中包含了一个SmsDemo类,直接通过* 执行main函数即可体验短信产品API功能(只需要将AK替换成开通了云通信-短信产品功能的AK即可)* 工程依赖了2个jar包(存放在工程的libs目录下)* 1:aliyun-java-sdk-core.jar* 2:aliyun-java-sdk-dysmsapi.jar** 备注:Demo工程编码采用UTF-8* 国际短信发送请勿参照此DEMO*/public class DySmsHelper {private final static Logger logger=LoggerFactory.getLogger(DySmsHelper.class);//产品名称:云通信短信API产品,开发者无需替换static final String product = "Dysmsapi";//产品域名,开发者无需替换static final String domain = "dysmsapi.aliyuncs.com";// TODO 此处需要替换成开发者自己的AK(在阿里云访问控制台寻找)static String accessKeyId;static String accessKeySecret;public static void setAccessKeyId(String accessKeyId) {DySmsHelper.accessKeyId = accessKeyId;}public static void setAccessKeySecret(String accessKeySecret) {DySmsHelper.accessKeySecret = accessKeySecret;}public static String getAccessKeyId() {return accessKeyId;}public static String getAccessKeySecret() {return accessKeySecret;}public static boolean sendSms(String phone,JSONObject templateParamJson,DySmsEnum dySmsEnum) throws ClientException {//可自助调整超时时间System.setProperty("sun.net.client.defaultConnectTimeout", "10000");System.setProperty("sun.net.client.defaultReadTimeout", "10000");//update-begin-author:taoyan date:20200811 for:配置类数据获取StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);setAccessKeyId(staticConfig.getAccessKeyId());setAccessKeySecret(staticConfig.getAccessKeySecret());//update-end-author:taoyan date:20200811 for:配置类数据获取//初始化acsClient,暂不支持region化IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);IAcsClient acsClient = new DefaultAcsClient(profile);//验证json参数validateParam(templateParamJson,dySmsEnum);//组装请求对象-具体描述见控制台-文档部分内容SendSmsRequest request = new SendSmsRequest();//必填:待发送手机号request.setPhoneNumbers(phone);//必填:短信签名-可在短信控制台中找到request.setSignName(dySmsEnum.getSignName());//必填:短信模板-可在短信控制台中找到request.setTemplateCode(dySmsEnum.getTemplateCode());//可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为request.setTemplateParam(templateParamJson.toJSONString());//选填-上行短信扩展码(无特殊需求用户请忽略此字段)//request.setSmsUpExtendCode("90997");//可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者//request.setOutId("yourOutId");boolean result = false;//hint 此处可能会抛出异常,注意catchSendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);logger.info("短信接口返回的数据----------------");logger.info("{Code:" + sendSmsResponse.getCode()+",Message:" + sendSmsResponse.getMessage()+",RequestId:"+ sendSmsResponse.getRequestId()+",BizId:"+sendSmsResponse.getBizId()+"}");if ("OK".equals(sendSmsResponse.getCode())) {result = true;}return result;}private static void validateParam(JSONObject templateParamJson,DySmsEnum dySmsEnum) {String keys = dySmsEnum.getKeys();String [] keyArr = keys.split(",");for(String item :keyArr) {if(!templateParamJson.containsKey(item)) {throw new RuntimeException("模板缺少参数:"+item);}}}// public static void main(String[] args) throws ClientException, InterruptedException {// JSONObject obj = new JSONObject();// obj.put("code", "1234");// sendSms("13800138000", obj, DySmsEnum.FORGET_PASSWORD_TEMPLATE_CODE);// }}
邮件通知接口
JavaMailSender mailSender = (JavaMailSender) SpringContextUtils.getBean("mailSender");SimpleMailMessage message = new SimpleMailMessage();// 设置发送方邮箱地址message.setFrom(emailFrom);//发件人邮箱message.setTo(es_receiver);//收件人邮箱message.setSubject(es_title);//标题message.setText(es_content);//内容mailSender.send(message);
或者
EmailSendMsgHandle emailSendMsgHandle = new EmailSendMsgHandle();String es_receiver = "rongcw@upcif.com";String es_title = "jeecg测试邮件";String es_content = "测试内容";emailSendMsgHandle.SendMsg(es_receiver ,es_title ,es_content );
系统消息提醒
调用ISysBaseAPI接口发送系统消息,message中发送方和接收方为系统用户的账户名
/*** 1发送系统消息* @param message 使用构造器赋值参数 如果不设置category(消息类型)则默认为2 发送系统消息*/void sendSysAnnouncement(MessageDTO message);/*** 2发送消息 附带业务参数* @param message 使用构造器赋值参数*/void sendBusAnnouncement(BusMessageDTO message);/*** 3通过模板发送消息* @param message 使用构造器赋值参数*/void sendTemplateAnnouncement(TemplateMessageDTO message);/*** 4通过模板发送消息 附带业务参数* @param message 使用构造器赋值参数*/void sendBusTemplateAnnouncement(BusTemplateMessageDTO message);/*** 5通过消息中心模板,生成推送内容* @param templateDTO 使用构造器赋值参数* @return*/String parseTemplateByCode(TemplateDTO templateDTO);
MessageDTO message2 = new MessageDTO();message2.setFromUser("jeecg");message2.setToUser("admin,张小红");message2.setTitle("测试");try {sysBaseAPI.sendSysAnnouncement(message2);//sysBaseAPI.sendTemplateAnnouncement(message);}catch (Exception e){e.printStackTrace();}


也可通过模板进行系统消息发送
TemplateMessageDTO message = new TemplateMessageDTO();HashMap<String,String> map = new HashMap<String,String>();map.put("code","78946");message.setTemplateCode("SMS_TEST"); //模板codemessage.setFromUser("jeecg");message.setToUser("admin,张小红");message.setTemplateParam(map);try {sysBaseAPI.sendTemplateAnnouncement(message);}catch (Exception e){e.printStackTrace();}

public void sendTemplateAnnouncement(TemplateMessageDTO message) {String templateCode = message.getTemplateCode();String title = message.getTitle();Map<String,String> map = message.getTemplateParam();String fromUser = message.getFromUser();String toUser = message.getToUser();List<SysMessageTemplate> sysSmsTemplates = sysMessageTemplateService.selectByCode(templateCode);if(sysSmsTemplates==null||sysSmsTemplates.size()==0){throw new JeecgBootException("消息模板不存在,模板编码:"+templateCode);}SysMessageTemplate sysSmsTemplate = sysSmsTemplates.get(0);//模板标题title = title==null?sysSmsTemplate.getTemplateName():title;//模板内容String content = sysSmsTemplate.getTemplateContent();if(map!=null) {for (Map.Entry<String, String> entry : map.entrySet()) {String str = "${" + entry.getKey() + "}";if(oConvertUtils.isNotEmpty(title)){title = title.replace(str, entry.getValue());}content = content.replace(str, entry.getValue());}}SysAnnouncement announcement = new SysAnnouncement();announcement.setTitile(title);announcement.setMsgContent(content);announcement.setSender(fromUser);announcement.setPriority(CommonConstant.PRIORITY_M);announcement.setMsgType(CommonConstant.MSG_TYPE_UESR);announcement.setSendStatus(CommonConstant.HAS_SEND);announcement.setSendTime(new Date());announcement.setMsgCategory(CommonConstant.MSG_CATEGORY_2);announcement.setDelFlag(String.valueOf(CommonConstant.DEL_FLAG_0));sysAnnouncementMapper.insert(announcement);// 2.插入用户通告阅读标记表记录String userId = toUser;String[] userIds = userId.split(",");String anntId = announcement.getId();for(int i=0;i<userIds.length;i++) {if(oConvertUtils.isNotEmpty(userIds[i])) {SysUser sysUser = userMapper.getUserByName(userIds[i]);if(sysUser==null) {continue;}SysAnnouncementSend announcementSend = new SysAnnouncementSend();announcementSend.setAnntId(anntId);announcementSend.setUserId(sysUser.getId());announcementSend.setReadFlag(CommonConstant.NO_READ_FLAG);sysAnnouncementSendMapper.insert(announcementSend);JSONObject obj = new JSONObject();obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_USER);obj.put(WebsocketConst.MSG_USER_ID, sysUser.getId());obj.put(WebsocketConst.MSG_ID, announcement.getId());obj.put(WebsocketConst.MSG_TXT, announcement.getTitile());webSocket.sendMessage(sysUser.getId(), obj.toJSONString());}}try {// 同步企业微信、钉钉的消息通知dingtalkService.sendActionCardMessage(announcement, true);wechatEnterpriseService.sendTextCardMessage(announcement, true);} catch (Exception e) {log.error("同步发送第三方APP消息失败!", e);}}
JeecgBoot的消息推送接口
JeecgBoot提供了根据消息模板实现消息推送的功能,类似消息中间件功能,数据推送添加至消息表中,定时任务自动推送。
使用步骤
- 在消息中心-模板管理中创建所需模板

- 引入推送工具类
org.jeecg.modules.message.util.PushMsgUtil.java
- 调用接口推送消息
//当模板内容中有参数时,需添加map为内容中参数赋值,如果模板消息中没有参数,可省略
Map<String, String> map = new HashMap();map.put("bpm_name","请假审批");map.put("bpm_task","部门经理审批");map.put("remark","");//调用消息推送保存接口boolean is_sendSuccess = pushMsgUtil.sendMessage("2", "789456", map,"rongcw@upcif.com");


参数说明:
需要注意的是,这里的receiver接收人,以什么样的消息类型就填写什么样的方式,比如邮箱类型的就填邮箱,否则会失败。
消息保存后之后,需要在定时任务中启动消息发送任务,才可进一步发送消息
package org.jeecg.modules.message.job;import java.util.List;import org.jeecg.common.util.DateUtils;import org.jeecg.modules.message.entity.SysMessage;import org.jeecg.modules.message.handle.ISendMsgHandle;import org.jeecg.modules.message.handle.enums.SendMsgStatusEnum;import org.jeecg.modules.message.handle.enums.SendMsgTypeEnum;import org.jeecg.modules.message.service.ISysMessageService;import org.quartz.Job;import org.quartz.JobExecutionContext;import org.quartz.JobExecutionException;import org.springframework.beans.factory.annotation.Autowired;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import lombok.extern.slf4j.Slf4j;/*** 发送消息任务*/@Slf4jpublic class SendMsgJob implements Job {@Autowiredprivate ISysMessageService sysMessageService;@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {log.info(String.format(" Jeecg-Boot 发送消息任务 SendMsgJob ! 时间:" + DateUtils.getTimestamp()));// 1.读取消息中心数据,只查询未发送的和发送失败不超过次数的QueryWrapper<SysMessage> queryWrapper = new QueryWrapper<SysMessage>();queryWrapper.eq("es_send_status", SendMsgStatusEnum.WAIT.getCode()).or(i -> i.eq("es_send_status", SendMsgStatusEnum.FAIL.getCode()).lt("es_send_num", 6));List<SysMessage> sysMessages = sysMessageService.list(queryWrapper);System.out.println(sysMessages);// 2.根据不同的类型走不通的发送实现类for (SysMessage sysMessage : sysMessages) {ISendMsgHandle sendMsgHandle = null;try {if (sysMessage.getEsType().equals(SendMsgTypeEnum.EMAIL.getType())) {sendMsgHandle = (ISendMsgHandle) Class.forName(SendMsgTypeEnum.EMAIL.getImplClass()).newInstance();} else if (sysMessage.getEsType().equals(SendMsgTypeEnum.SMS.getType())) {sendMsgHandle = (ISendMsgHandle) Class.forName(SendMsgTypeEnum.SMS.getImplClass()).newInstance();} else if (sysMessage.getEsType().equals(SendMsgTypeEnum.WX.getType())) {sendMsgHandle = (ISendMsgHandle) Class.forName(SendMsgTypeEnum.WX.getImplClass()).newInstance();}} catch (Exception e) {log.error(e.getMessage(),e);}Integer sendNum = sysMessage.getEsSendNum();try {sendMsgHandle.SendMsg(sysMessage.getEsReceiver(), sysMessage.getEsTitle(),sysMessage.getEsContent().toString());// 发送消息成功sysMessage.setEsSendStatus(SendMsgStatusEnum.SUCCESS.getCode());} catch (Exception e) {e.printStackTrace();// 发送消息出现异常sysMessage.setEsSendStatus(SendMsgStatusEnum.FAIL.getCode());}sysMessage.setEsSendNum(++sendNum);// 发送结果回写到数据库sysMessageService.updateById(sysMessage);}}}
其他实现类
| org.jeecg.modules.message.handle.impl.EmailSendMsgHandle | 邮件推送 | 已实现 |
|---|---|---|
| org.jeecg.modules.message.handle.impl.SmsSendMsgHandle | 短信推送 | 未实现 |
| org.jeecg.modules.message.handle.impl.WxSendMsgHandle | 微信推送 | 未实现 |
短信接口配置
阿里短信服务文档
可通过支付宝或者淘宝账户注册阿里云账户,同时提供了专门阿里云服务app
获取阿里短信key、签名和模板
获取AccessKey
进入阿里短信服务工作台,点击箭头指向的按钮
进入如下界面,查看创建的key,若没有则需创建所需的key

可使用手机发送验证码创建,保存Ak信息则会生成一个csv文件在本地磁盘上,文件内容对应着key id 和 secret

创建短信签名和短信模板

根据相关资料填写完信息后,等待审核成功


审核成功后,进入签名列表和模板列表查看,需要记住的是签名名称和模板CODE,后续配置会用到
SpringBoot集成阿里短信服务
在pom文件中导入相关依赖
<repositories><repository><id>aliyun</id><name>aliyun Repository</name><url>https://maven.aliyun.com/repository/public</url><snapshots><enabled>false</enabled></snapshots></repository></repositories>
或者
<dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-core</artifactId><version>4.1.0</version><exclusions><exclusion><artifactId>commons-codec</artifactId><groupId>commons-codec</groupId></exclusion><exclusion><artifactId>activation</artifactId><groupId>javax.activation</groupId></exclusion></exclusions></dependency><dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-dysmsapi</artifactId><version>1.1.0</version></dependency>
修改配置文件
jeecg:#阿里云oss存储和大鱼短信秘钥配置oss:accessKey: **********************secretKey: **********************endpoint: oss-cn-beijing.aliyuncs.combucketName: jeecgdevstaticDomain: https://static.jeecg.com
获取配置文件短信配置信息
package org.jeecg.config;import lombok.Data;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;/*** 设置静态参数初始化*/@Component@Datapublic class StaticConfig {@Value("${jeecg.oss.accessKey}")private String accessKeyId;@Value("${jeecg.oss.secretKey}")private String accessKeySecret;@Value(value = "${spring.mail.username}")private String emailFrom;/*** 签名密钥串*/@Value(value = "${jeecg.signatureSecret}")private String signatureSecret;/*@Beanpublic void initStatic() {DySmsHelper.setAccessKeyId(accessKeyId);DySmsHelper.setAccessKeySecret(accessKeySecret);EmailSendMsgHandle.setEmailFrom(emailFrom);}*/}
短信发送接口实现类
package org.jeecg.common.util;import org.jeecg.config.StaticConfig;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import com.alibaba.fastjson.JSONObject;import com.aliyuncs.DefaultAcsClient;import com.aliyuncs.IAcsClient;import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;import com.aliyuncs.exceptions.ClientException;import com.aliyuncs.profile.DefaultProfile;import com.aliyuncs.profile.IClientProfile;/*** Created on 17/6/7.* 短信API产品的DEMO程序,工程中包含了一个SmsDemo类,直接通过* 执行main函数即可体验短信产品API功能(只需要将AK替换成开通了云通信-短信产品功能的AK即可)* 工程依赖了2个jar包(存放在工程的libs目录下)* 1:aliyun-java-sdk-core.jar* 2:aliyun-java-sdk-dysmsapi.jar** 备注:Demo工程编码采用UTF-8* 国际短信发送请勿参照此DEMO*/public class DySmsHelper {private final static Logger logger=LoggerFactory.getLogger(DySmsHelper.class);//产品名称:云通信短信API产品,开发者无需替换static final String product = "Dysmsapi";//产品域名,开发者无需替换static final String domain = "dysmsapi.aliyuncs.com";// TODO 此处需要替换成开发者自己的AK(在阿里云访问控制台寻找)static String accessKeyId;static String accessKeySecret;public static void setAccessKeyId(String accessKeyId) {DySmsHelper.accessKeyId = accessKeyId;}public static void setAccessKeySecret(String accessKeySecret) {DySmsHelper.accessKeySecret = accessKeySecret;}public static String getAccessKeyId() {return accessKeyId;}public static String getAccessKeySecret() {return accessKeySecret;}public static boolean sendSms(String phone,JSONObject templateParamJson,DySmsEnum dySmsEnum) throws ClientException {//可自助调整超时时间System.setProperty("sun.net.client.defaultConnectTimeout", "10000");System.setProperty("sun.net.client.defaultReadTimeout", "10000");//update-begin-author:taoyan date:20200811 for:配置类数据获取StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);setAccessKeyId(staticConfig.getAccessKeyId());setAccessKeySecret(staticConfig.getAccessKeySecret());//update-end-author:taoyan date:20200811 for:配置类数据获取//初始化acsClient,暂不支持region化IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);IAcsClient acsClient = new DefaultAcsClient(profile);//验证json参数validateParam(templateParamJson,dySmsEnum);//组装请求对象-具体描述见控制台-文档部分内容SendSmsRequest request = new SendSmsRequest();//必填:待发送手机号request.setPhoneNumbers(phone);//必填:短信签名-可在短信控制台中找到request.setSignName(dySmsEnum.getSignName());//必填:短信模板-可在短信控制台中找到request.setTemplateCode(dySmsEnum.getTemplateCode());//可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为request.setTemplateParam(templateParamJson.toJSONString());//选填-上行短信扩展码(无特殊需求用户请忽略此字段)//request.setSmsUpExtendCode("90997");//可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者//request.setOutId("yourOutId");boolean result = false;//hint 此处可能会抛出异常,注意catchSendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);logger.info("短信接口返回的数据----------------");logger.info("{Code:" + sendSmsResponse.getCode()+",Message:" + sendSmsResponse.getMessage()+",RequestId:"+ sendSmsResponse.getRequestId()+",BizId:"+sendSmsResponse.getBizId()+"}");if ("OK".equals(sendSmsResponse.getCode())) {result = true;}return result;}private static void validateParam(JSONObject templateParamJson,DySmsEnum dySmsEnum) {String keys = dySmsEnum.getKeys();String [] keyArr = keys.split(",");for(String item :keyArr) {if(!templateParamJson.containsKey(item)) {throw new RuntimeException("模板缺少参数:"+item);}}}// public static void main(String[] args) throws ClientException, InterruptedException {// JSONObject obj = new JSONObject();// obj.put("code", "1234");// sendSms("13800138000", obj, DySmsEnum.FORGET_PASSWORD_TEMPLATE_CODE);// }}
调用短信发送接口
JSONObject templateParamJson = new JSONObject();templateParamJson.put("code","123456");try {DySmsHelper.sendSms("17687475079", templateParamJson,DySmsEnum.TEST_CODE);}catch (Exception e){e.printStackTrace();}
接口上送参数说明
| 名称 | 类型 | 说明 |
|---|---|---|
| phone | String | 手机号 |
| templateParamJson | JSONObject | 短信内容 |
| dySmsEnum | DySmsEnum | 短信模板 |
注意这里的短信模板是指短信服务上的模板,而不是系统中的消息模板,否则会因找不到模板而报
dySmsEnum短信枚举信息类
package org.jeecg.common.util;import org.apache.commons.lang3.StringUtils;public enum DySmsEnum {TEST_CODE("SMS_205466924","小伟考勤","code"),LOGIN_TEMPLATE_CODE("SMS_175435174","JEECG","code"),FORGET_PASSWORD_TEMPLATE_CODE("SMS_175435174","JEECG","code"),REGISTER_TEMPLATE_CODE("SMS_175430166","JEECG","code"),/**会议通知*/MEET_NOTICE_TEMPLATE_CODE("SMS_201480469","H5活动之家","username,title,minute,time"),/**我的计划通知*/PLAN_NOTICE_TEMPLATE_CODE("SMS_201470515","H5活动之家","username,title,time");/*** 短信模板编码*/private String templateCode;/*** 签名*/private String signName;/*** 短信模板必需的数据名称,多个key以逗号分隔,此处配置作为校验*/private String keys;private DySmsEnum(String templateCode,String signName,String keys) {this.templateCode = templateCode;this.signName = signName;this.keys = keys;}public String getTemplateCode() {return templateCode;}public void setTemplateCode(String templateCode) {this.templateCode = templateCode;}public String getSignName() {return signName;}public void setSignName(String signName) {this.signName = signName;}public String getKeys() {return keys;}public void setKeys(String keys) {this.keys = keys;}public static DySmsEnum toEnum(String templateCode) {if(StringUtils.isEmpty(templateCode)){return null;}for(DySmsEnum item : DySmsEnum.values()) {if(item.getTemplateCode().equals(templateCode)) {return item;}}return null;}}
jeecgboot实现短信推送功能
jeecgboot的推送功能实际是将要推送的消息保存到数据库中,然后通过一个定时任务去定时的将数据库表中未发送或者发送失败次数超过六次以上的消息进行定时发送,保存的消息可通过自定义的模板,讲所需要的参数传进去生成消息,jeecgboot未能实现短信推送的原因就在这里,因为保存的是一整条完整消息,不需要通过传参传入到短信服务给我们的模板中
解决办法
- 方法一:在短信服务申请一个全局变量的短信模板,后端只需要将短信模板和短信签名写死,即可将数据库的消息进行推送问题,各个短信运营商不支持全局变量的短信模板,而且即便添加了文字 如:系统消息:{message}这样的模板也是行不通的,客服给的说法是{message}这个变量的意义太广泛
- 方法二:可通过调用某些云服务平台提供的短信接口来实现短信发送,只需要我们将发送的内容传过去即可发送短信,目前这种方法是满足jeecgboot的短信推送功能的
邮箱接口配置
获取邮箱服务密钥(以QQ邮箱服务为例)
进入QQ邮箱

开启相关服务获取密钥,或通过点击生成授权码获取密钥

SpringBoot集成邮箱服务
在pom文件中引入邮箱服务依赖
<resources><resource><directory>src/main/resources</directory><filtering>true</filtering></resource></resources>
或者
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency>
修改配置文件
spring:mail:host: smtp.qq.com #邮箱服务类型,我这选的是QQ邮箱服务username: ***************** #邮箱地址password: ***************** #邮箱密钥properties:mail:smtp:auth: truestarttls:enable: truerequired: true
获取配置文件邮箱配置信息
package org.jeecg.config;import lombok.Data;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;/*** 设置静态参数初始化*/@Component@Datapublic class StaticConfig {@Value("${jeecg.oss.accessKey}")private String accessKeyId;@Value("${jeecg.oss.secretKey}")private String accessKeySecret;@Value(value = "${spring.mail.username}")private String emailFrom;/*** 签名密钥串*/@Value(value = "${jeecg.signatureSecret}")private String signatureSecret;/*@Beanpublic void initStatic() {DySmsHelper.setAccessKeyId(accessKeyId);DySmsHelper.setAccessKeySecret(accessKeySecret);EmailSendMsgHandle.setEmailFrom(emailFrom);}*/}
这里需要注意的是,获取密钥是通过mailSender这个bean类去获取的
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {String beanName = this.transformedBeanName(name);Object sharedInstance = this.getSingleton(beanName);Object bean;if (sharedInstance != null && args == null) {if (this.logger.isTraceEnabled()) {if (this.isSingletonCurrentlyInCreation(beanName)) {this.logger.trace("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference");} else {this.logger.trace("Returning cached instance of singleton bean '" + beanName + "'");}}bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, (RootBeanDefinition)null);} else {if (this.isPrototypeCurrentlyInCreation(beanName)) {throw new BeanCurrentlyInCreationException(beanName);}BeanFactory parentBeanFactory = this.getParentBeanFactory();if (parentBeanFactory != null && !this.containsBeanDefinition(beanName)) {String nameToLookup = this.originalBeanName(name);if (parentBeanFactory instanceof AbstractBeanFactory) {return ((AbstractBeanFactory)parentBeanFactory).doGetBean(nameToLookup, requiredType, args, typeCheckOnly);}if (args != null) {return parentBeanFactory.getBean(nameToLookup, args);}if (requiredType != null) {return parentBeanFactory.getBean(nameToLookup, requiredType);}return parentBeanFactory.getBean(nameToLookup);}if (!typeCheckOnly) {this.markBeanAsCreated(beanName);}try {RootBeanDefinition mbd = this.getMergedLocalBeanDefinition(beanName);this.checkMergedBeanDefinition(mbd, beanName, args);String[] dependsOn = mbd.getDependsOn();String[] var11;if (dependsOn != null) {var11 = dependsOn;int var12 = dependsOn.length;for(int var13 = 0; var13 < var12; ++var13) {String dep = var11[var13];if (this.isDependent(beanName, dep)) {throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");}this.registerDependentBean(dep, beanName);try {this.getBean(dep);} catch (NoSuchBeanDefinitionException var24) {throw new BeanCreationException(mbd.getResourceDescription(), beanName, "'" + beanName + "' depends on missing bean '" + dep + "'", var24);}}}if (mbd.isSingleton()) {sharedInstance = this.getSingleton(beanName, () -> {try {return this.createBean(beanName, mbd, args);} catch (BeansException var5) {this.destroySingleton(beanName);throw var5;}});bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);} else if (mbd.isPrototype()) {var11 = null;Object prototypeInstance;try {this.beforePrototypeCreation(beanName);prototypeInstance = this.createBean(beanName, mbd, args);} finally {this.afterPrototypeCreation(beanName);}bean = this.getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);} else {String scopeName = mbd.getScope();if (!StringUtils.hasLength(scopeName)) {throw new IllegalStateException("No scope name defined for bean ´" + beanName + "'");}Scope scope = (Scope)this.scopes.get(scopeName);if (scope == null) {throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");}try {Object scopedInstance = scope.get(beanName, () -> {this.beforePrototypeCreation(beanName);Object var4;try {var4 = this.createBean(beanName, mbd, args);} finally {this.afterPrototypeCreation(beanName);}return var4;});bean = this.getObjectForBeanInstance(scopedInstance, name, beanName, mbd);} catch (IllegalStateException var23) {throw new BeanCreationException(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton", var23);}}} catch (BeansException var26) {this.cleanupAfterBeanCreationFailure(beanName);throw var26;}}if (requiredType != null && !requiredType.isInstance(bean)) {try {T convertedBean = this.getTypeConverter().convertIfNecessary(bean, requiredType);if (convertedBean == null) {throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());} else {return convertedBean;}} catch (TypeMismatchException var25) {if (this.logger.isTraceEnabled()) {this.logger.trace("Failed to convert bean '" + name + "' to required type '" + ClassUtils.getQualifiedName(requiredType) + "'", var25);}throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());}} else {return bean;}}
@Nullableprotected Object getSingleton(String beanName, boolean allowEarlyReference) {Object singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {synchronized(this.singletonObjects) {singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null) {ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);if (singletonFactory != null) {singletonObject = singletonFactory.getObject();this.earlySingletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName);}}}}}}return singletonObject;}
/*** Returns the value to which the specified key is mapped,* or {@code null} if this map contains no mapping for the key.** <p>More formally, if this map contains a mapping from a key* {@code k} to a value {@code v} such that {@code (key==null ? k==null :* key.equals(k))}, then this method returns {@code v}; otherwise* it returns {@code null}. (There can be at most one such mapping.)** <p>If this map permits null values, then a return value of* {@code null} does not <i>necessarily</i> indicate that the map* contains no mapping for the key; it's also possible that the map* explicitly maps the key to {@code null}. The {@link #containsKey* containsKey} operation may be used to distinguish these two cases.** @param key the key whose associated value is to be returned* @return the value to which the specified key is mapped, or* {@code null} if this map contains no mapping for the key* @throws ClassCastException if the key is of an inappropriate type for* this map* (<a href="{@docRoot}/java/util/Collection.html#optional-restrictions">optional</a>)* @throws NullPointerException if the specified key is null and this map* does not permit null keys* (<a href="{@docRoot}/java/util/Collection.html#optional-restrictions">optional</a>)*//*** 如果这个映射包含从密钥k到值v的映射,那么(密钥==null?k==null : key.equals(k)),那么这个方法返回v;否则返回null。(这样的映射最多只能有一个。)* 如果此映射允许空值,则空返回值不一定表示映射不包含键的映射;也有可能映射将键显式映射为null。*/V get(Object key);


发送邮件接口实现类
package org.jeecg.modules.message.handle.impl;import org.jeecg.common.util.SpringContextUtils;import org.jeecg.common.util.oConvertUtils;import org.jeecg.config.StaticConfig;import org.jeecg.modules.message.handle.ISendMsgHandle;import org.springframework.mail.SimpleMailMessage;import org.springframework.mail.javamail.JavaMailSender;import org.springframework.mail.javamail.MimeMessageHelper;import javax.mail.MessagingException;import javax.mail.internet.MimeMessage;public class EmailSendMsgHandle implements ISendMsgHandle {static String emailFrom;public static void setEmailFrom(String emailFrom) {EmailSendMsgHandle.emailFrom = emailFrom;}@Overridepublic void SendMsg(String es_receiver, String es_title, String es_content) {JavaMailSender mailSender = (JavaMailSender) SpringContextUtils.getBean("mailSender");MimeMessage message = mailSender.createMimeMessage();MimeMessageHelper helper = null;//update-begin-author:taoyan date:20200811 for:配置类数据获取if(oConvertUtils.isEmpty(emailFrom)){StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);setEmailFrom(staticConfig.getEmailFrom());}//update-end-author:taoyan date:20200811 for:配置类数据获取try {helper = new MimeMessageHelper(message, true);// 设置发送方邮箱地址helper.setFrom(emailFrom);helper.setTo(es_receiver);helper.setSubject(es_title);helper.setText(es_content, true);mailSender.send(message);} catch (MessagingException e) {e.printStackTrace();}}}
实际调用的是spring提供的JavaMailSender进行邮件发送
调用邮件发送接口发送邮件
EmailSendMsgHandle emailSendMsgHandle = new EmailSendMsgHandle();String es_receiver = "rongcw@upcif.com";String es_title = "jeecg测试邮件";String es_content = "测试内容"emailSendMsgHandle.SendMsg(es_receiver ,es_title ,es_content );
发送邮件接口上送参数说明
| 名称 | 类型 | 说明 |
|---|---|---|
| es_receiver | String | 接收邮件邮箱 |
| es_title | String | 邮件标题 |
| es_content | String | 邮件发送内容 |
自定义注解禁止重复提交
注解接口类
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface LimitSubmit {String key() ;/*** 默认 10s*/int limit() default 10;/*** 请求完成后 是否一直等待* true则等待* @return*/boolean needAllWait() default true;}
注解接口实现类
@Component@Aspect@Slf4jpublic class LimitSubmitAspect {//封装了redis操作各种方法@Autowiredprivate RedisUtil redisUtil;@Pointcut("@annotation(org.jeecg.common.aspect.annotation.LimitSubmit)")private void pointcut() {}@Around("pointcut()")public Object handleSubmit(ProceedingJoinPoint joinPoint) throws Throwable {LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal();Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();//获取注解信息LimitSubmit limitSubmit = method.getAnnotation(LimitSubmit.class);int submitTimeLimiter = limitSubmit.limit();String redisKey = limitSubmit.key();boolean needAllWait = limitSubmit.needAllWait();String key = getRedisKey(sysUser,joinPoint, redisKey);Object result = redisUtil.get(key);if (result != null) {throw new JeecgBootException("请勿重复访问!");}redisUtil.set( key, sysUser.getId(),submitTimeLimiter);try {Object proceed = joinPoint.proceed();return proceed;} catch (Throwable e) {log.error("Exception in {}.{}() with cause = \'{}\' and exception = \'{}\'", joinPoint.getSignature().getDeclaringTypeName(),joinPoint.getSignature().getName(), e.getCause() != null? e.getCause() : "NULL", e.getMessage(), e);throw e;}finally {if(!needAllWait) {redisUtil.del(redisKey);}}}/*** 支持多参数,从请求参数进行处理*/private String getRedisKey(LoginUser sysUser,ProceedingJoinPoint joinPoint ,String key ){if(key.contains("%s")) {key = String.format(key, sysUser.getId());}Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();String[] parameterNames = discoverer.getParameterNames(method);if (parameterNames != null) {for (int i=0; i < parameterNames.length; i++ ) {String item = parameterNames[i];if(key.contains("#"+item)){key = key.replace("#"+item, joinPoint.getArgs()[i].toString());}}}return key.toString();}}
注解的使用
在请求接口方法上加入@LimitSubmit(key = “testLimit:%s:#orderId”,limit = 10,needAllWait = true)
testLimit:将存入到redis中的key生成在testLimit文件中
%S:代表当前登录人
#:参数,代表从参数中获取,支持多个参数,生成的redis key:testLimit:e9ca23d68d884d4ebb19d07889727dae:order1123123
1.限制对某个接口的访问,针对所有人 ,则去除%S
2.限制某个人对某个接口的访问,则 %S
3.限制某个人对某个接口的业务参数的访问,则 %S:#参数1:#参数2
注意
写入的参数需包含在请求接口方法接收前端发送的参数中,否则会报空指针异常
#写入的参数可以写多个,用逗号隔开
#写入的参数需是参数名,会自动通过参数名获取参数值
批量插入效率研究建议
验证每种插入方法所消耗的时间
初始化插入数据
public List<JeecgDemo> initDemos(){List<JeecgDemo> demos = new ArrayList<>();for (int i = 0; i < 10000; i++) {JeecgDemo demo = new JeecgDemo();demo.setSysOrgCode(i+"");demo.setName(i+"name");demo.setKeyWord(i+"keyWord");demo.setPunchTime(new Date());demo.setSalaryMoney(BigDecimal.ONE);demo.setBonusMoney(1d);demo.setSex("1");demo.setAge(10);demo.setBirthday(new Date());demo.setEmail("fad@qq.com");demo.setContent("fad@qq.com");demos.add(demo);}return demos;}public List<Object[]> initJDBCDemos(){List<Object[]> demos = new ArrayList<>();for (int i = 0; i < 10000; i++) {Object[] demo = new Object[11];demo[0] =i+""+new Date();demo[1] =i+"name";demo[2] =i+"keyWord";demo[3] =new Date();demo[4] =BigDecimal.ONE;demo[5] =1d;demo[6] ="1";demo[7] =10;demo[8] =new Date();demo[9] ="fad@qq.com";demo[10] ="fad@qq.com";demos.add(demo);}return demos;}
默认情况下,循环插入
@Test@Transactional //单元测试中,默认回滚,防止污染数据库表public void testMybatisInsert100000Save() {List<JeecgDemo> jeecgDemoList = initDemos();long start = System.currentTimeMillis();jeecgDemoList.forEach(jeecgDemo -> {jeecgDemoMapper.insert(jeecgDemo);});long end = System.currentTimeMillis();System.out.println("默认情况下,循环插入耗时为:"+(end-start));}
批量保存的情况下插入
@Test@Transactionalpublic void testMybatisInsert100000BatchSave() {List<JeecgDemo> jeecgDemoList = initDemos();long start = System.currentTimeMillis();jeecgDemoMapper.insertBatch(jeecgDemoList);long end = System.currentTimeMillis();System.out.println("批量保存,插入耗时为:"+(end-start));}
Mybatis 自带批量保存
@Test@Transactionalpublic void testMybatisInsert100000SqlSessionBatchSave() {List<JeecgDemo> jeecgDemoList = initDemos();SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH.BATCH, false);JeecgDemoMapper jeecgDemoMapper = sqlSession.getMapper(JeecgDemoMapper.class);long start = System.currentTimeMillis();jeecgDemoList.forEach(jeecgDemo -> {jeecgDemoMapper.insert(jeecgDemo);});sqlSession.commit();long end = System.currentTimeMillis();System.out.println("Mybatis 自带批量保存,插入耗时为:"+(end-start));}
SpringJDBC批量保存(此方式最快)
@Test@Transactionalpublic void testJdbcInsert100000BatchSave() {List<Object[]> jeecgDemoList = initJDBCDemos();DruidDataSource dataSource = DynamicDBUtil.getDbSourceByDbKey("master");try {System.out.println(dataSource.getConnection().toString());}catch (Exception e){e.printStackTrace();}JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);String sql ="INSERT INTO `demo`( `id`, `name`,\n" +"\t\t`key_word`,\n" +"\t\t`punch_time`,\n" +"\t\t `salary_money`,\n" +"\t\t `bonus_money`,\n" +"\t\t `sex`, `age`, `birthday`,\n" +"\t\t `email`, `content`)\n" +"\t\tVALUES (?,?,?,?,?,?,?,?,?,?,?)";long start = System.currentTimeMillis();jdbcTemplate.batchUpdate(sql,jeecgDemoList);long end = System.currentTimeMillis();System.out.println(" SpringJDBC批量保存,插入耗时为:"+(end-start));}
SpringJDBC批量保存,插入耗时为:818
因此可得出结论SpringJDBC批量插入是最快的
接口敏感数据安全
注解方案
敏感数据不允许传递给前端,展示在接口JSON结果中 =>
例如: 用户表的密码和加密盐属于敏感信息,不能在查询用户列表结果JSON中展示,但是平台用的是Mybatis-plus,分页会直接查询全部字段,怎么排除敏感字段呢?
在需要排除的敏感字段的实体属性类上的敏感属性上添加以下注解
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
导入的包
import com.fasterxml.jackson.annotation.JsonProperty;
参数说明
value:将trueName属性数序列化为name,即{“name”:””}
@JsonProperty(value="name")private String trueName;
access:
JsonProperty.Access.WRITE_ONLY 接口请求时该属性忽略,也就是序列化时忽略属性
JsonProperty.Access.READ_ONLY 不受影响,接口接收不存在反序列化操作
积木报表设计器的使用
使用
第一步: 引入JimuReport 依赖
<dependency><groupId>org.jeecgframework.jimureport</groupId><artifactId>jimureport-spring-boot-starter</artifactId><version>1.3.78</version></dependency>
最新版本可以从 http://jimureport.com/doc/log 中查询到
第二步:修改配置application.yml
#minidao配置minidao :base-package: org.jeecg.modules.jmreport.desreport.dao*#静态资源加载配置spring:mvc:static-path-pattern: /**resource:static-locations: classpath:/static/,classpath:/public/
第三步: 初始化Sql脚本
jimureport.mysql5.7.create.sql
第四步:排除请求拦截
JimuReport自带权限控制,所以需要放开自己框架对JimuReport请求的权限拦截;JeecgBoot修改org.jeecg.config.shiro.ShiroConfig加入以下代码,其他项目参考修改即可。
//积木报表排除filterChainDefinitionMap.put("/jmreport/**", "anon");
第五步:增加spring扫码路径
@SpringBootApplication(scanBasePackages = {"org.jeecg.modules.jmreport"})
第六步: 访问积木报表
设计积木报表
- 点击积木报表设计,进入积木报表创建页面

- 选择报表类型,新建报表模板或者现在报表模板,进入报表设计页面
SQL数据集
- 在报表设计页面,在数据集管理中选择SQL数据集

- 需要注意填写的SQL必须是在项目数据库中查询到数据,SQL语句支持参数查询,传递的参数必须符合规范,填写完成后点击右边的SQL解析按钮

- 解析后的动态报表配置明细为SQL查询的实体字段名,报表参数为SQL语句中动态参数,在报表参数中参数和参数文本对应SQL语句中参数 填写默认值后预览可根据默认值展示数据

- 确定保存后退出,进入报表设计页面,选择左边数据集,拖拽到右边Excel表格中

- 保存后预览查看
API数据集
- 在报表设计页面,在数据集管理中选择api数据集

- 填写编码和名称后选择请求方式,在api地址中填写api的请求地址注意:api请求的参数必须是json格式并json开头为data,后面的数据为json数组或json对象如果没有data直接返回json数组或json对象

api请求的地址支持传递参数,在api地址后拼接参数后,需要在报表参数中手动添加参数以及参数文本和默认值
填写完成后点击api解析将解析的参数和api参数保存,即可在设计页面根据报表参数设计报表
通过sql数据集来设计一个钻取报表
建立主表的报表
添加sql数据集
SELECTb.sn,a.billing_month,b.name,b.party_type,a.social_security_orders,a.commision_month,a.level1_commision_month,a.level1_commision_orders,a.level2_commision_month,a.level2_commision_orders,a.member_month,a.member_total,a.child_num_month,a.child_num_totalFROMonekm_merchant_commision a,mopai_merchant bWHERE a.party_id = b.id<#if isNotEmpty(sn)>and b.sn = '${sn}'</#if><#if isNotEmpty(party_type)>and b.party_type in (${party_type})</#if><#if isNotEmpty(billing_month)>and a.billing_month = '${billing_month}'</#if>ORDER BY a.billing_month DESC
<#if isNotEmpty(sn)> and b.sn = ‘${sn}’</#if>,可以为我们提供动态传参,通过isNotEmpty方法判断传入的参数是否为空,是则为true,会在sql语句中拼接标签中的参数语句。
解析之前需要设计数据源,否则可能因为找不到表而报错,默认使用的数据源是在配置文件中已经配置好了的,如果需要其他数据源名,需根据下图点击对应的图标来添加数据源
然后
最后填写数据源相关的信息,填写完成之后可以点击测试是否能成功连接数据库,测试成功后点击确认保存,然后通过下拉单选选择对应的数据源就可以了
sql数据集的一些配置
设置查询条件,有两种方式,一种是通过报表字段明细,另一种是通过报表参数
报表字段明细:
成功解析sql后会自动出现。可勾选查询框,勾选后可作为查询条件进行查询,勾选查询后,可在对应的字段选择查询模式,例如下拉单选、下拉多选、输入等,如果是时间格式的查询条件,可通过查询日期格式来设置输入的时间格式,比如yyyy-MM,代表2021-10。
该设置的缺点是通过拼接sql语句来设置查询条件的,查询效率低,所以推荐使用报表参数来设置查询条件。
报表参数:
需要在sql语句中事先定义好where查询条件,字符串格式的参数设置为:’${参数}’,不是字符串的可以不用单引号
也是成功解析后才会出现,一样的可以设置是否是查询,查询默认,添加默认值等
设置好后点击确认保存即可,也可事先在数据预览中查看是否有相关的数据,如果是设置了报表参数,预览没有数据,这个不用担心,因为没有传值进去,参数都是空的,除非设置了默认值或者表中有记录,但对应的参数本事就是空的
设置好报表格式后,可点击数据集对应的字段拖拽到报表中
预览效果
建立钻取子表
添加钻取链接
首先在主表设计页面中添加钻取链接,如下图
然后填写相关信息
链接报表需要事先添加字表报表
弹出方式可页内,也可页外
参数设置即为点击跳转后需要传的参数,=A意思是该行的A列的值
最后点击确认即可
添加字表数据集
一样的跟主表一样需先选择数据源,默认不选即为配置文件中配置的数据源
SELECT c.`name`, a.`month_orders`, a.`reuse_orders_month`, a.`orders_total`, a.`commision_month`, a.`level1_commision_month`, a.`level1_commision_orders`, a.`level2_commision_month`, a.`level2_commision_orders` FROM `onekm_commision_detail` a, `mopai_merchant` b, `mopai_service` c WHERE a.`party_id` = b.`id` AND a.`service` = c.`id` AND b.`sn` = '${sn}' AND a.`billing_month` = '${billing_month}' ORDER BY c.`orders` ASC
子表必须要设置报表参数,因为我们需要接收主表传入的参数,所以sql语句需要写where条件
解析成功后点击确认保存即可
也是一样拖住字段设计报表格式,最后点击保存即可
然后在主表设计页面中点击预览,然后点击详情即可跳转到子表
设置字典表
在需要设置下拉单选或者下拉多选的时候常常需要用到字典表,在报表设计主页面可点击下图所示箭头的图标来添加字典表

添加后点击字典配置添加字段
名称为添加的字段名,数据值为字段的参数值,排序根据大小来排序,小的在前面,最后点击启用,保存就可以了
然后在报表字段明细或者报表参数中的字典code添加响应的字典code,就会在查询条件中显示想要的字段
给查询条件动态传值
通过添加js增强代码来给查询条件传默认值
在报表设计页面中点击其他设置,添加增强配置
选择JS,添加相关代码
该配置只能定义一个function方法,且方法名必须为init
然后通过this.updateSearchFormValue(‘ts0001’, ‘billing_month’,result);来设置默认值,第一个参数为添加数据集时设置的编码
,第二个参数为需要添加值的字段名,第三个参数为需要传入的值,最后点击确认保存即可
效果:
设置js带来的问题
- 在设计钻取报表如果是业内跳转,且需要传入值的参数名跟js需要设置参数的值的名一样的话,会把js设置的之传入字表中,造成字表查询的记录与原本的需求相悖
解决方案:在子表报表中设置不一样的参数,即a.billing_month = ‘${billing_month1}’,然后在主表报表中的连接参数配置中
修改对应的参数名就可
- 如果js传入的值查询后没有记录,预览的时候分页将会失效,只有当js传入的值能够查询到记录,分页才不会失效,目前已反馈给官方,已经解决,预计在下个版本中修复
通过sql数据集来设计一个条形图
添加sql数据集,一样的先选择数据源,然后添加相关信息,解析成功后点击确认保存即可
这里需要注意的是,积木报表规定必须有三个字段,且字段对应的值也有规定,比如SELECTDATE_FORMAT( a.`create_date`, '%Y%m%d' ) AS create_date,COUNT(*) AS total,'订单总数' AS typeFROMmopai_order aWHEREa.`create_date` >= '2021-07-21'AND a.`payment_date` >= '2021-07-21'AND a.`create_date` <= '2021-10-21 23:59:59'AND a.`payment_date` <= '2021-10-21 23:59:59'AND a.`order_status` NOT IN ( 6, 8 )AND a.`payment_status` IN ( 2, 3 )GROUP BYDATE_FORMAT( a.`create_date`, '%Y%m%d' ) UNIONSELECTDATE_FORMAT( c.`create_date`, '%Y%m%d' ) AS create_date,COUNT(*) AS total,'跑兔总数' AS typeFROMmopai_merchant cWHEREc.`create_date` >= '2021-07-21'AND c.`create_date` <= '2021-10-21 23:59:59'AND c.`audit_status` = 1AND c.`party_type` = 12GROUP BYDATE_FORMAT( c.`create_date`, '%Y%m%d' ) UNIONSELECTDATE_FORMAT( b.`create_date`, '%Y%m%d' ) AS create_date,COUNT(*) AS total,'会员总数' AS typeFROMmopai_member bWHEREb.`create_date` >= '2021-07-21'AND b.`create_date` <= '2021-10-21 23:59:59'AND b.`is_locked` = 0AND b.`is_enabled` = 1GROUP BYDATE_FORMAT( b.`create_date`, '%Y%m%d' ) ORDER BY create_date DESC

这里的字段名和值无规定,只需要格式满足就可以了
添加图形报表,选好后点击确认即可,可将其拖住到相应位置,或者调整大小
添加后,设置相关数据,选择对应的字段
- 绑定数据集:是左侧绑定过的数据集,可选择任意一个SQL类型的数据集;
- 分类属性:是X轴绑定数据库的字段;
- 值属性:是Y轴绑定数据库的字段;
- 系列属性:对应的是线的类型;
可定时刷新,最后点击运行即可
效果图
也可选择其他图形报表进行设计,步骤也是一样的
菜单路由报表
即在菜单页面中显示报表
第一步 需要在报表设计页面中点击预览查看预览页地址栏,然后记下红色框中选中的
第二步 在首页中系统管理中的菜单管理添加菜单
添加一级菜单,其中菜单名称和菜单路径可以自定义,不过菜单路径需是/开头,前端组件固定为layouts/RouteView
添加一级菜单后,点击更多添加二级菜单
配置二级菜单,主要是菜单路径{{ window._CONFIG[‘domianURL’] }}是项目路径,固定器,然后尾部是预览页中已经记下的路径地址
保存后,需在角色管理中授权
最后重新刷新页面
效果
其他相关配置

http://report.jeecg.com/2078898
积木报表和online报表是否通用
积木报表没有设计表单功能
online表单可以对接积木报表,目前遇到的问题是,在积木报表中配置online表单,通过API访问报错
报错信息:{“success”:false,”message”:”操作失败,Name for argument of type [java.lang.String] not specified, and parameter name information not found in class file either.”,”code”:500,”result”:null,”timestamp”:1634267793424}
目前官方已经解决,预计将会在下个版本中修复
通过online配置积木报表的表单,添加了对某条数据的打印功能,需现在积木报表中配置了对应表的online表单,然后通过打印,将选中的某条记录的值传过去
详情可看:http://doc.jeecg.com/2373083
积木报表应用场景
1.可通过报表来设计查询表
2.可导入Excel文件进行报表设计
3.可将报表导出excel、pdf等文件
4.可通过配置菜单来查询已设计好的报表
5.可设计各种类型的单据、大屏,如出入库单、销售单、财务报表、合同、监控大屏、旅游数据大屏等
6.打印各种数据单

