@Transactional详解(作用、失效场景与解决方法)

news2024/9/25 9:40:35

一、了解@Transactional注解,先要知道事务是什么,但既然知道了这个注解,不知道事务是什么,那就重新再去学习一遍数据库吧,这里讲解开发中代码实现事务的方式

1、编程式事务(开发用的很少了)

基于底层的API,如PlatformTransactionManagerTransactionDefinitionTransactionTemplate 等核心接口,开发者完全可以通过编程的方式来进行事务管理。

@SpringBootTest
@Slf4j
public class Test {
    @Autowired
    PlatformTransactionManager transactionManager;


    @Autowired
    private TransactionTemplate transactionTemplate;

    @Autowired
    UserService userService;

    @Test
    public void transactionManagerTest(){
        //事务基础信息 超时时间、隔离级别、传播属性等
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();; // 定义事务属性
        // 设置传播行为属性, 默认事务级别。 当前无事务则新建事务,已有事务则加入事务。
        def.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED);
        // 获得事务状态
        TransactionStatus status = transactionManager.getTransaction(def);

        try {
            log.info("执行事务方法");
            // 这里用到 lombok 的链式编程,很简单的原理,set一般不是返回void吗,只需要改为返回当前对象,就是this,就可以实现此功能了
            User user = new User().setName("张三");
            // 事务操作
            // 数据库操作
            userService.insertUser(user);
            //手动设置异常
            int i = 1 / 0;
            // 事务提交
            transactionManager.commit(status);// 提交事务
        } catch (Exception e) {
            // 事务提交
            transactionManager.rollback(status);
            log.error("执行事务异常,需要回滚");
        }
    }

    @Test
    public void transactionTemplateTest() {
        Object execute = transactionTemplate.execute(new TransactionCallback() {
            /**
             * @param transactionStatus 事务运行状态信息 是否是新事务、是否已被标记为回滚等
             * @return
             */
            @Override
            public Object doInTransaction(TransactionStatus transactionStatus) {
                try {
                    log.info("执行事务方法");
                    User user = new User().setName("张三");
                    userService.insertUser(user);
                    // 手动设置异常
                    int i = 1 / 0;
                    log.info("正常执行完成");
                    return "成功";
                } catch (Exception e) {
                    log.error("执行事务异常,需要回滚",e);
                    transactionStatus.setRollbackOnly();
                    return "失败";
                }
            }
        });
    }
}

2、声明式事务(主流,也是本文的主角==@Transactional==)

声明式事务管理方法允许开发者配置的帮助下来管理事务,而不需要依赖底层API进行硬编码。开发者可以只使用注解或基于配置的 XML 来管理事务。

    @Test
    // 这个注解还是有很多坑需要注意,这在后文中会讲
    @Transactional(rollbackFor = Exception.class)
    public void transactionalTest(){
        // 只用正常些业务代码就可以了,一切交给 Spring 来处理,这里 @Transactional 注解用到的知识就是 AOP
        try {
            User user = new User().setName("张三");
            // 数据库操作
            userService.insertUser(user);
            // 手动设置异常
            int i = 1 / 0;
        } catch (Exception e) {
            log.error("执行事务异常,需要回滚",e);
            // 如果异常被 try 、 catch 吞了是不会回滚的,所有手动抛出去
            throw e;
        }
    }

二、@Transactional介绍

1、@Transactional注解可以作用于哪些地方?

@Transactional 可以作用在接口、类、类方法

  • 作用于类:当把@Transactional 注解放在类上时,表示所有该类的public方法都配置相同的事务属性信息。

  • 作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。

  • 作用于接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效

@Transactional(rollbackFor = Exception.class)
@RestController
@Slf4j
public class TestController {
        @Autowired
        UserService userService;

        @RequestMapping("/test")
        // 这个注解还是有很多坑需要注意,这在后文中会讲
        @Transactional(rollbackFor = Exception.class)
        public void transactionalTest(){
            // 只用正常些业务代码就可以了,一切交给 Spring 来处理,这里 @Transactional 注解用到的知识就是 AOP
            try {
                User user = new User().setName("张三");
                // 数据库操作
                userService.insertUser(user);
                // 手动设置异常
                int i = 1 / 0;
            } catch (Exception e) {
                log.error("执行事务异常,需要回滚",e);
                // 如果异常被 try 、 catch 吞了是不会回滚的,所有手动抛出去
                throw e;
            }
        }
}

2、@Transactional注有哪些属性?

propagation属性

propagation 代表事务的传播行为,默认值为 Propagation.REQUIRED,其他的属性信息如下:

  • Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。( 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务

  • Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。

  • Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。

  • Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。( 当类A中的 a 方法用默认Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务 )

  • Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务。

  • Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。

  • Propagation.NESTED :和 Propagation.REQUIRED 效果一样。

isolation 属性

isolation :事务的隔离级别,默认值为Isolation.DEFAULT

  • Isolation.DEFAULT:使用底层数据库默认的隔离级别。
  • Isolation.READ_UNCOMMITTED:读未提交
  • Isolation.READ_COMMITTED:读已提交
  • Isolation.REPEATABLE_READ:可重复读
  • Isolation.SERIALIZABLE:串行化

timeout 属性

timeout :事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务。

readOnly 属性

readOnly :指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。

rollbackFor 属性

rollbackFor :用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。

noRollbackFor属性

noRollbackFor:抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。

三、@Transactional失效场景与解决办法

1、@Transactional 应用在非 public 修饰的方法上

首先JDK动态代理肯定只能是public,因为接口的权限修饰符只能是public,所以其他的修饰符不可以使用,cglib代理的方式是可以代理protected方法的如果支持protected,大概率会造成当切换代理的实现方式时表现不同,所以会出现很多的bug,然后造成一系列的问题

在这里插入图片描述

之所以会失效是因为在Spring AOP 代理时,如上图所示 TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute方法,获取Transactional 注解的事务配置信息。

AbstractFallbackTransactionAttributeSource类:

protected TransactionAttribute computeTransactionAttribute(Method method,
    Class<?> targetClass) {
   
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
   
	return null;
        }

这个方法会检查目标方法的修饰符是不是 public,若不是 public,就不会获取@Transactional 的属性配置信息,最终会造成不会用 TransactionInterceptor 来拦截该目标方法进行事务管理。

2、@Transactional 注解属性 propagation 设置错误

这种失效是由于配置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚。

  • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

3、@Transactional 注解属性 rollbackFor 设置错误

rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。

在这里插入图片描述

若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。Spring源码如下:

private int getDepth(Class<?> exceptionClass, int depth) {
        if (exceptionClass.getName().contains(this.exceptionName)) {
            // Found it!
            return depth;
}
        // If we've gone as far as we can go and haven't found it...
        if (exceptionClass == Throwable.class) {
            return -1;
}
return getDepth(exceptionClass.getSuperclass(), depth + 1);
}

所以一般使用 @Transactional(rollbackFor = Exception.class) 就可以了,但可以根据业务自定义异常类

4、同一个类中方法调用,导致@Transactional失效

开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。

那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。

错误入下:(但反过来(transactionalTest方法(@Transactional在transactionalTest方法上)调用Test方法)可以让@Transactional生效)

@RestController
@Slf4j
public class TestController {
    @Autowired
    UserService userService;

    @RequestMapping("/test")
    public void Test(){
        // 这里就相当于使用了 this.transactionalTest(); 使用的是本地对象的方法,没有用到spring容器,就无法触发AOP,导致事务失效
        transactionalTest();
    }
    
    // 这个注解还是有很多坑需要注意,这在后文中会讲
    @Transactional(rollbackFor = Exception.class)
    public void transactionalTest(){
        // 只用正常些业务代码就可以了,一切交给 Spring 来处理,这里 @Transactional 注解用到的知识就是 AOP
        try {
            User user = new User().setName("张三");
            // 数据库操作
            userService.insertUser(user);
            // 手动设置异常
            int i = 1 / 0;
        } catch (Exception e) {
            log.error("执行事务异常,需要回滚",e);
            // 如果异常被 try 、 catch 吞了是不会回滚的,所有手动抛出去
            throw e;
        }
    }
}

解决方法:使用该类的Spring代理对象就可以了

1、AopContext.currentProxy()获得当前代理对象,注意:类上也需要加@EnableAspectJAutoProxy注解(推荐
@RestController
@Slf4j
@EnableAspectJAutoProxy(proxyTargetClass = true,exposeProxy = true)
public class TestController {
    @Autowired
    UserService userService;



    @RequestMapping("/test")
    public void Test(){
        // 这里就相当于使用了 this.transactionalTest(); 使用的是本地对象的方法,没有用到spring容器,就无法触发AOP,导致事务失效
        ((TestController) AopContext.currentProxy()).transactionalTest();
    }

    // 这个注解还是有很多坑需要注意,这在后文中会讲
    @Transactional(rollbackFor = Exception.class)
    public void transactionalTest(){
        // 只用正常些业务代码就可以了,一切交给 Spring 来处理,这里 @Transactional 注解用到的知识就是 AOP
        try {
            User user = new User().setName("张三");
            // 数据库操作
            userService.insertUser(user);
            // 手动设置异常
            int i = 1 / 0;
        } catch (Exception e) {
            log.error("执行事务异常,需要回滚",e);
            // 如果异常被 try 、 catch 吞了是不会回滚的,所有手动抛出去
            throw e;
        }
    }
}
2、写到另一个类中使用@Autowired调用(这个就不细说了)
3、@Autowired本类(没试过,听说可以)
@RestController
@Slf4j
public class TestController {
    @Autowired
    UserService userService;
    @Autowired
    TestController testController;



    @RequestMapping("/test")
    public void Test(){
        testController.transactionalTest();
    }

    // 这个注解还是有很多坑需要注意,这在后文中会讲
    @Transactional(rollbackFor = Exception.class)
    public void transactionalTest(){
        // 只用正常些业务代码就可以了,一切交给 Spring 来处理,这里 @Transactional 注解用到的知识就是 AOP
        try {
            User user = new User().setName("张三");
            // 数据库操作
            userService.insertUser(user);
            // 手动设置异常
            int i = 1 / 0;
        } catch (Exception e) {
            log.error("执行事务异常,需要回滚",e);
            // 如果异常被 try 、 catch 吞了是不会回滚的,所有手动抛出去
            throw e;
        }
    }
}
4、实现ApplicationContextAware接口,获取Bean
@RestController
@Slf4j
public class TestController implements ApplicationContextAware {
    @Autowired
    UserService userService;

    private static ApplicationContext applicationContext;


    @RequestMapping("/test")
    public void Test(){
        TestController bean = applicationContext.getBean(TestController.class);
        bean.transactionalTest();
    }

    // 这个注解还是有很多坑需要注意,这在后文中会讲
    @Transactional(rollbackFor = Exception.class)
    public void transactionalTest(){
        // 只用正常些业务代码就可以了,一切交给 Spring 来处理,这里 @Transactional 注解用到的知识就是 AOP
        try {
            User user = new User().setName("张三");
            // 数据库操作
            userService.insertUser(user);
            // 手动设置异常
            int i = 1 / 0;
        } catch (Exception e) {
            log.error("执行事务异常,需要回滚",e);
            // 如果异常被 try 、 catch 吞了是不会回滚的,所有手动抛出去
            throw e;
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        TestController.applicationContext = applicationContext;
    }
}
5、一切可以获取Spring对象的应该都可以 ╭(๑´^`๑)╮

5、异常被你的 catch“吃了”导致@Transactional失效

AOP 可以说是生成一个代理类,可以在这个方法的头和尾添加代码,比如代理类使用一个try、catch把这个方法给套起来了,只有这个方法抛出异常之后,调用了代理类中catch的事务回滚代码才能回滚,所以你没抛出异常的话,@Transactional认为你是正常运行的代码所以不回滚

    @Transactional(rollbackFor = Exception.class)
    public void transactionalTest(){
        // 只用正常些业务代码就可以了,一切交给 Spring 来处理,这里 @Transactional 注解用到的知识就是 AOP
        try {
            User user = new User().setName("张三");
            // 数据库操作
            userService.insertUser(user);
            // 手动设置异常
            int i = 1 / 0;
        } catch (Exception e) {
            // 为啥写 catch ,还要写 throw e 抛出异常呢,因为要记日志啊,不记日志的话也可以不用,但哪个不写报错日志,出bug不是给自己挖坑吗
            log.error("执行事务异常,需要回滚",e);
            // 如果异常被 try 、 catch 吞了是不会回滚的
//            throw e;
        }
    }

6、数据库引擎不支持事务

这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。

总结

@Transactional非常的好用,只需要一个注解就可以不用管事务操作了,只需要注重自己的业务代码就可以了

@Transactional也有缺点,比如你要注意上诉的几种失效情况,他有个局限性,他的最小粒度要作用在方法上,也就是说,如果想要给一部分代码块增加事务的话,那就需要把这个部分代码块单独独立出来作为一个方法。而且AOP要生成一个动态代理类,所以会慢一点

但是谁会管这些啊,好用就行了,也不是不能忍的问题,这跟他的好处根本不成正比,当然如果要使用编程式事务也可以,看个人爱好,反之小编一直用的是@Transactional,Spring官方也推荐使用@Transactional,所以了解了怎么使用之后,大胆的用就好了

这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。

借鉴文章:一口气说出 6种,@Transactional注解的失效场景

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/833151.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【火炬之光-召唤装备】

头部胸甲手套鞋子武器盾牌项链戒指腰带神格备注*邪龙头冠无限要塞/血抗血抗血抗***终焉复临任意攻速单手武器/黑峡烬盾1召唤等级血抗*原点的寒冬1召唤等级1.刷钢铁炼境监视者-无垢之墙升级。2.不能用典狱官的胸针参考视频机械领主无限要塞––***终焉复临––求生之欲––参考视…

Zoho CRM有哪些功能?使用体验如何?

市场上有很多CRM品牌&#xff0c;但是Zoho CRM是比较亮眼的存在&#xff0c;不仅屡获CRM国际大奖&#xff0c;更是连续十一年入选Gartner魔力象限。不论是功能、价格还是使用体验&#xff0c;都备受用户青睐。下面我们就来说说&#xff0c;Zoho CRM的使用体验评价。 Zoho CRM有…

Codeforces Round 888 (Div. 3)F题题解

文章目录 [Lisa and the Martians](https://codeforces.com/contest/1851/problem/F)问题建模问题分析1.分析给定的运算操作2.方法1使用Trie树来查找最符合的数代码 方法2通过性质&#xff0c;排序后找运算的最大值代码 证明 Lisa and the Martians 问题建模 ​ 给定n个数和k&…

如何搭建一个口才培训的网站?需要具备哪些条件?

论文题目&#xff1a;如何搭建一个口才培训的网站及所需条件 摘要&#xff1a; 本文探讨了如何搭建一个口才培训的网站&#xff0c;并详细分析了所需的关键条件。口才培训作为一种重要的社交技能&#xff0c;能够帮助人们提升自信和影响力&#xff0c;因此具有广阔的市场前景。…

【yolov5训练报错Wandb wandb.errors.UsageError: api_key not configured (no-tty).】

跑yolov5训练官方代码出现下面的问题&#xff0c;第一个方法就是关闭代理网络vpn&#xff0c;直接运行&#xff0c;阻止数据上传。 D:\anaconda\envs\yolov5\python.exe C:\Users\ljx\Desktop\yoloair-main\yoloair-main\yolov7\train.py YOLOR 2022-11-8 torch 1.8.1cu111…

Nginx 高性能内存池 ----【学习笔记】

跟着这篇文章学习&#xff1a; c代码实现一个高性能内存池&#xff08;超详细版本&#xff09;_c 内存池库_linux大本营的博客-CSDN博客https://blog.csdn.net/qq_40989769/article/details/130874660以及这个视频学习&#xff1a; nginx的内存池_哔哩哔哩_bilibilihttps://w…

TikTok马来西亚站变动,指定物流服务商!

8月2日&#xff0c;据TechinAsia报道&#xff0c;TikTok已将百世快递在马来西亚的子公司BestExpressMalaysia&#xff0c;指定为其在马来西亚的物流服务商。目前&#xff0c;百世快递已在越南、泰国与TikTok展开类似合作。 合作后&#xff0c;百世马来子公司将为TikTokShop卖家…

js去除字符串中特殊字符并将数据转为数组格式

let str今-天-空-气-很-清-新;let liststr.split(-);console.log(list)使用split将特殊字符去除并将数据转为数组

解决vite+vue3项目npm装包失败

报错如下&#xff1a; Failed to remove some directories [ npm WARN cleanup [ npm WARN cleanup D:\\V3Work\\v3project\\node_modules\\vue, npm WARN cleanup [Error: EPERM: operation not permitted, rmdir D:\V3Work\v3project\node_modules\vue\reactivity\…

Spring @Scheduled单线程单实例的坑

文章目录 前言背景验证解决方案 前言 在 Java Spring 项目中经常会用 Scheduled 来实现一些定时任务的场景&#xff0c;有必要了解一些它使用时的问题和内部实现机制。本文是偶然间发现的一个问题&#xff0c;刷新了我的认知&#xff0c;分享给大家。 其他相关文章&#xff1…

Pytorch深度学习-----神经网络之Sequential的详细使用及实战详解

系列文章目录 PyTorch深度学习——Anaconda和PyTorch安装 Pytorch深度学习-----数据模块Dataset类 Pytorch深度学习------TensorBoard的使用 Pytorch深度学习------Torchvision中Transforms的使用&#xff08;ToTensor&#xff0c;Normalize&#xff0c;Resize &#xff0c;Co…

产品经理之路:从C端到B端的崭新旅程

理解C端和B端 在我们开始探讨从C端产品经理到B端产品经理的转变之前&#xff0c;我们首先需要理解C端和B端是什么&#xff0c;以及它们之间有什么区别。 C端产品与B端产品的基本区别 C端产品&#xff08;Consumer端产品&#xff09;是面向普通消费者的产品&#xff0c;如社交…

大数据课程F4——HIve的其他操作

文章作者邮箱&#xff1a;yugongshiyesina.cn 地址&#xff1a;广东惠州 ▲ 本章节目的 ⚪ 掌握HIve的join&#xff1b; ⚪ 掌握HIve的查询和排序 ⚪ 掌握HIve的beeline ⚪ 掌握HIve的文件格式 ⚪ 掌握HIve的基本架构 ⚪ 掌握HIve的优化&#xff1b; 一、jo…

一个对象的内存图,两个对象使用同一个方法的内存图

一个对象的内存图 public class Phone {String brand; //品牌double price; //价格String color; //颜色public void call(String who) {System.out.println("给" who "打电话");}public void sendMessage() {System.out.println("群发短信");…

【第一阶段】kotlin的range表达式

range:范围&#xff1a;从哪里到哪里的意思 in:表示在 !in&#xff1a;表示不在 … :表示range表达式 代码示例&#xff1a; fun main() {var num:Int20if(num in 0..9){println("差劲")}else if(num in 10..59){println("不及格")}else if(num in 60..89…

2023年自动化测试已成为标配?一篇彻底打通自动化测试...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 首先我们从招聘岗…

CompletableFuture原理与实践

文章目录 1 为何需要并行加载2 并行加载的实现方式2.1 同步模型2.2 NIO异步模型2.3 为什么会选择CompletableFuture&#xff1f; 3 CompletableFuture使用与原理3.1 CompletableFuture的背景和定义3.1.1 CompletableFuture解决的问题3.1.2 CompletableFuture的定义 3.2 Complet…

Shell脚本学习-for循环结构1

for循环语句主要用于执行次数有限的循环&#xff0c;而不是用于守护进程和无限循环。 语法结构&#xff1a; for 变量名 in 变量取值列表 do指令... done 提示&#xff1a;in 变量值列表 可以省略&#xff0c;省略就相当于 in "$"&#xff0c;也就是使用for i in &…

企业大数据可视化案例专题分享-入门

一、什么是数据可视化&#xff1f; 基本概念&#xff1a;数据可视化是以图示或图形格式表示的数据。让决策者可以看到以直观方式呈现的分析&#xff0c;以便他们可以掌握困难的概念或识别新的模式。借助交互式可视化&#xff0c;可以使用技术深入挖掘图表和图形以获取更多详细…

Java版工程项目管理系统平台+企业工程系统源码+助力工程企业实现数字化管理 em

鸿鹄工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离构建工程项目管理系统 1. 项目背景 一、随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大。为了提高工程管理效率、减轻劳动强度、提高信息处理速度和准确性&#xff0c;公司对内部工程…