02.尚医通 Mybatis-Plus

news2024/11/18 3:38:00
1、前期准备
a. 创建数据库
CREATE TABLE USER
(
  id BIGINT(20)NOT NULL COMMENT '主键ID',
  NAME VARCHAR(30)NULL DEFAULT NULL COMMENT '姓名',
  age INT(11)NULL DEFAULT NULL COMMENT '年龄',
  email VARCHAR(50)NULL DEFAULT NULL COMMENT '邮箱',
  PRIMARY KEY (id)
);

INSERT INTO user (id, name, age, email)VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');
b. 配置项目环境

配置Java编译器:

项目和文件的编码:

c. 添加依赖
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!--mybatis-plus-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.1</version>
    </dependency>

    <!--mysql依赖-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!--lombok用来简化实体类-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
d. 配置文件
# 配置MySQL
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/syt_mp?serverTimezone=GMT%2B8
    username: root
    password: 0903he0419
  • 这里的 url 使用了 ?serverTimezone=GMT%2B8 后缀,因为8.0版本的jdbc驱动需要添加这个后缀,否则运行测试用例报告如下错误:java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more
  • 这里的 driver-class-name 使用了 com.mysql.cj.jdbc.Driver ,在 jdbc 8 中 建议使用这个驱动,否则运行测试用例的时候会有 WARN 信息
e. 启动类
@SpringBootApplication
@MapperScan("com.lemon.demomp.mapper")
public class DemompApplication {

    public static void main(String[] args) {

        SpringApplication.run(DemompApplication.class, args);
    }

}
  • 在启动类添加注解:@MapperScan("com.lemon.demomp.mapper")
f. 添加实体
@Data
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}
g. 添加mapper
@Repository
public interface UserMapper extends BaseMapper<User> {

}
  • 为了避免报错,可以在dao 层 的接口上添加 @Repository 注
h. 查看 sql 输出日志
# mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
2、主键策略

插入操作:

//添加
@Test
public void testAdd() {
    User user = new User();
    user.setName("lucy");
    user.setAge(20);
    user.setEmail("1243@qq.com");
    int insert = userMapper.insert(user);
    System.out.println(insert);
}
a. ASSIGN_ID

MyBatis-Plus默认的主键策略是:ASSIGN_ID (使用了雪花算法)

@TableId(type = IdType.ASSIGN_ID)
private Long id;
b. AUTO 自增策略

需要在创建数据表的时候设置主键自增

实体字段中配置@TableId(type = IdType.AUTO)

@TableId(type = IdType.AUTO)
private Long id;

要想影响所有实体的配置,可以设置全局主键配置

#全局设置主键生成策略
mybatis-plus.global-config.db-config.id-type=auto

3、自动填充

注意:update时生成的sql自动是动态sql:UPDATE user SET age=? WHERE id=?

更新操作:

// 修改
@Test
public void testUpdate() {
    User user = new User();
    user.setId(1722996412305653764L);
    user.setName("朱棣");
    int count = userMapper.updateById(user);
    System.out.println(count);
}

需求描述:项目中经常会遇到一些数据,每次都使用相同的方式填充,例如记录的创建时间,更新时间等。我们可以使用MyBatis Plus的自动填充功能,完成这些字段的赋值工作

a. 修改数据库

在User表中添加datetime类型的新的字段 create_time、update_time

b. 修改实体类

实体上增加字段并添加自动填充注解:

@TableField(fill = FieldFill.INSERT)
private Date createTime;  //create_time

@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime; //update_time
c. 实现元对象处理接口

src/hander/MyMetaObjectHandler

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    // mp执行添加操作,这个方法执行
    @Override
    public void insertFill(MetaObject metaObject) {
        this.setFieldValByName("createTime", new Date(), metaObject);
        this.setFieldValByName("updateTime", new Date(), metaObject);
    }

    // mp 执行修改操作,这个方法执行
    @Override
    public void updateFill(MetaObject metaObject) {
        this.setFieldValByName("updateTime", new Date(), metaObject);
    }
}
4、乐观锁

主要适用场景:当要更新一条记录的时候,希望这条记录没有被别人更新,也就是说实现线程安全的数据更新。

乐观锁实现方式:

  • 取出记录时,获取当前version
  • 更新时,带上这个version
  • 执行更新时,set version = newVersion where version = oldVersion
  • 如果version不对,就更新失败

乐观锁实现流程:

a. 修改数据库

在user表中新增version字段数据类型为int,给其设定一个默认值(假设为1)

b. 修改实体类

添加 @Version 注解

@Version
private Integer version;
c. 创建配置类

创建包config,创建文件MybatisPlusConfig.java

此时可以删除主类中的 @MapperScan 扫描注解

@Configuration
@MapperScan("com.lemon.demomp.mapper")
public class MpConfig {
    /**
     * 乐观锁插件
     */
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
        return new OptimisticLockerInterceptor();
    }
}
d. 测试方法

根据输出结果可以看到,首先经过查询,user的version是1,然后在更改user的name属性,此时version变为2。自此乐观锁已经生效。

@Test
public void testOptimisticLocker() {
    // 1. 根据id进行查询
    User user = userMapper.selectById(1722996412305653768L);
    user.setName("张三");
    int count = userMapper.updateById(user);
    System.out.println(count);
}
5、查询
a. 批量查询

通过多个id进行批量查询

SELECT id,name,age,email,create_time,update_time,version FROM user WHERE id IN ( ? , ? , ? )

==> Parameters: 1(Integer), 2(Integer), 3(Integer)

/*
* 批量查询
* */
@Test
public void testSelectByIds() {
    List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
    System.out.println(users);
}
b. 简单条件查询

SELECT id,name,age,email,create_time,update_time,version FROM user WHERE name = ? AND age = ?

==> Parameters: Jack(String), 20(Integer)

/*
* 简单条件查询
* */
@Test
public void testSelectByMap() {
    Map<String, Object> columnMap = new HashMap<>();
    columnMap.put("name", "Jack");
    columnMap.put("age", 20);
    List<User> users = userMapper.selectByMap(columnMap);
    System.out.println(users);
}
6、分页查询
a. 添加分页插件

在配置类中注册分页插件:src/config/MpConfig

/**
 * 分页插件
 */
@Bean
public PaginationInterceptor paginationInterceptor() {
    return new PaginationInterceptor();
}
b. 测试selectPage

Preparing: SELECT id,name,age,email,create_time,update_time,version FROM user LIMIT ?,?

==> Parameters: 0(Long), 3(Long)

/*
* 测试selectPage分页
* */
@Test
public void testSelectPage() {
    Page<User> page = new Page<>(1, 3);
    Page<User> userPage = userMapper.selectPage(page, null);
    // 返回对象得到分页所有数据
    long pages = userPage.getPages(); //总页数
    long current = userPage.getCurrent(); //当前页
    List<User> records = userPage.getRecords();//查询数据集合
    long total = userPage.getTotal();//总记录数
    boolean hasNext = userPage.hasNext();//是否有下一页
    boolean hasPrevious = userPage.hasPrevious();//是否有上一页

    System.out.println(pages);
    System.out.println(current);
    System.out.println(records);
    System.out.println(total);
    System.out.println(hasNext);
    System.out.println(hasPrevious);

}
c. 测试selectMapsSelect

Preparing: SELECT id,name,age,email,create_time,update_time,version FROM user LIMIT ?,?

==> Parameters: 0(Long), 5(Long)

/*
* 测试selectMapsPage
* */
@Test
public void testSelectMapsPage() {
    //Page不需要泛型
    Page<Map<String, Object>> page = new Page<>(1, 5);
    Page<Map<String, Object>> pageParam = userMapper.selectMapsPage(page, null);
    List<Map<String, Object>> records = pageParam.getRecords();
    records.forEach(System.out::println);
    System.out.println(pageParam.getCurrent());
    System.out.println(pageParam.getPages());
    System.out.println(pageParam.getSize());
    System.out.println(pageParam.getTotal());
    System.out.println(pageParam.hasNext());
    System.out.println(pageParam.hasPrevious());
}
7、删除
a. 根据id删除记录

Preparing: DELETE FROM user WHERE id=?

==> Parameters: 1(Long)

/*
* 根据id删除记录
* */
@Test
public void testDelById() {
    int isDeleted = userMapper.deleteById(1L);
    System.out.println(isDeleted);
}
b. 批量删除

Preparing: DELETE FROM user WHERE id IN ( ? , ? , ? )

==> Parameters: 2(Integer), 3(Integer), 4(Integer)

/*
* 批量删除
* */
@Test
public void testDelBatchByIds() {
    int isDeleted = userMapper.deleteBatchIds(Arrays.asList(2, 3, 4));
    System.out.println(isDeleted);
}
c. 简单条件删除

Preparing: DELETE FROM user WHERE name = ? AND age = ?

==> Parameters: Billie(String), 24(Integer)

/*
*
* 简单条件删除
* */
@Test
public void testDelByMap() {
    Map<String, Object> map = new HashMap<>();
    map.put("name", "Billie");
    map.put("age", 24);
    int isDeleted = userMapper.deleteByMap(map);
    System.out.println(isDeleted);
}
8、逻辑删除

物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除数据

逻辑删除:假删除,将对应数据中代表是否被删除字段状态修改为“被删除状态”,之后在数据库中仍旧能看到此条数据记录

逻辑删除的使用场景

  • 可以进行数据恢复
  • 有关联数据,不便删除
a. 修改数据库

添加逻辑删除字段

ALTER TABLE `user` ADD COLUMN deleted BOOLEAN DEFAULT FALSE COMMENT '逻辑删除'
b. 修改实体类

添加deleted 字段,并加上 @TableLogic 注解

@TableLogic
private Integer deleted;
c. 配置文件(可选)

application.properties 加入以下配置,此为默认值,如果你的默认值和mp默认的一样,该配置可无

mybatis-plus:
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0
d. 测试逻辑删除

Preparing: UPDATE user SET deleted=1 WHERE id=? AND deleted=0

==> Parameters: 6(Long)

测试后发现,数据并没有被删除,deleted字段的值由0变成了1

测试后分析打印的sql语句,是一条update

注意:被删除前,数据的deleted 字段的值必须是 0,才能被选取出来执行逻辑删除的操作

/*
* 逻辑删除
* */
@Test
public void testLogicDelete() {
    int isDeleted = userMapper.deleteById(6L);
    System.out.println(isDeleted);
}
e. 测试逻辑删除后的查询

MyBatis Plus中查询操作也会自动添加逻辑删除字段的判断

SELECT id,name,age,email,create_time,update_time,version,deleted FROM user WHERE deleted=0

/*
* 测试逻辑删除后的查询
* */
@Test
public void testLogicDeleteSelect() {
    List<User> users = userMapper.selectList(null);
    users.forEach(System.out::println);
}
9、条件构造器

Wrapper

条件构造抽象类,最顶端父类

AbstractWrapper

用于查询条件封装,生成 sql 的 where 条件

QueryWrapper

查询条件封装

UpdateWrapper

Update 条件封装

AbstractLambdaWrapper

使用Lambda 语法

LambdaQueryWrapper

用于Lambda语法使用的查询Wrapper

LambdaUpdateWrapper

Lambda 更新封装Wrapper

a. ge、gt、le、lt、isNull、isNotNull

UPDATE user SET deleted=1 WHERE deleted=0 AND (name IS NOT NULL AND age >= ? AND email IS NOT NULL)

==> Parameters: 50(Integer)

/*
* ge、gt、le、lt、isNull、isNotNull
* delete
* */
@Test
public void testQuery() {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.isNotNull("name")
            .ge("age",50)
            .isNotNull("email");
    int deleted = userMapper.delete(queryWrapper);
    System.out.println("deleted return count= " + deleted);
}
b. eq、ne

SELECT id,name,age,email,create_time,update_time,version,deleted FROM user WHERE deleted=0 AND (name = ?)

==> Parameters: 张三(String)

注意:seletOne()返回的是一条实体记录,当出现多条时会报错

/*
* eq、ne
* selectOne
* */
@Test
public void testSelectOne() {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("name", "张三");
    User user = userMapper.selectOne(queryWrapper);
    System.out.println(user);
}
c. between、notBetween

SELECT COUNT( 1 ) FROM user WHERE deleted=0 AND (age BETWEEN ? AND ?)

==> Parameters: 60(Integer), 80(Integer)

/*
* between,notBetween
* selectCount
* */
@Test
public void testSelectCount() {
    QueryWrapper<User> queryWrapper  = new QueryWrapper<>();
    queryWrapper.between("age", 60, 80);
    Integer count = userMapper.selectCount(queryWrapper);
    System.out.println(count);
}
d. like、notLike、likeLeft、likeRight

SELECT name,age FROM user WHERE deleted=0 AND (name LIKE ? AND email LIKE ?)

==> Parameters: %e%(String), 5%(String)

selectMaps()返回Map集合列表,通常配合select()使用

/*
* like,notLike,likeLeft,likeRight
* selectMaps
* */
@Test
public void testSelectMaps() {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.select("name", "age")
            .like("name", "e")
            .likeRight("email", "5");
    List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
    maps.forEach(System.out::println);
}
e. orderBy、orderByDesc、orderByAsc

SELECT id,name,age,email,create_time,update_time,version,deleted FROM user WHERE deleted=0 ORDER BY age DESC,id DESC

/*
* orderBy、orderByDesc、orderByAsc
* selectList
* */
@Test
public void testSelectList() {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.orderByDesc("age", "id");
    List<User> users = userMapper.selectList(queryWrapper);
    users.forEach(System.out::println);
}
10、查询方式

setSqlSelect

设置 SELECT 查询字段

where

WHERE 语句,拼接 + WHERE 条件

and

AND 语句,拼接 + AND 字段=值

andNew

AND 语句,拼接 + AND (字段=值)

or

OR 语句,拼接 + OR 字段=值

orNew

OR 语句,拼接 + OR (字段=值)

eq

等于=

allEq

基于 map 内容等于=

ne

不等于<>

gt

大于>

ge

大于等于>=

lt

小于<

le

小于等于<=

like

模糊查询 LIKE

notLike

模糊查询 NOT LIKE

in

IN 查询

notIn

NOT IN 查询

isNull

NULL 值查询

isNotNull

IS NOT NULL

groupBy

分组 GROUP BY

having

HAVING 关键词

orderBy

排序 ORDER BY

orderAsc

ASC 排序 ORDER BY

orderDesc

DESC 排序 ORDER BY

exists

EXISTS 条件语句

notExists

NOT EXISTS 条件语句

between

BETWEEN 条件语句

notBetween

NOT BETWEEN 条件语句

addFilter

自由拼接 SQL

last

拼接在最后,例如:last(“LIMIT 1”)

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

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

相关文章

jdk+zookeeper+kafka 搭建kafka集群

环境准备 环境资源包&#xff1a; jdk-8u341-linux-x64.tar.gz kafka_2.12-2.2.0.tgz zookeeper-3.4.14.tar.gz server-idip状态server110.206.120.10leaderserver210.206.120.2followerserver310.206.120.3follower 一、安装jdk 因为kafka需要Java环境&#xff0c;所以优先…

Liunx高级系统设计9-线程间同步与互斥

同步与互斥的概念 互斥&#xff1a;同一时间&#xff0c;只能有一个任务&#xff08;进程或线程&#xff09;执行&#xff0c;谁先执行不确定。 同步&#xff1a;同一时间&#xff0c;只能有一个任务&#xff08;进程或线程&#xff09;执行&#xff0c;有顺序的执行。 同步…

CSS的基本选择器及高级选择器(附详细示例以及效果图)

Hi i,m JinXiang ⭐ 前言 ⭐ 本篇文章主要介绍HTML中CSS的基础选择及高级选择器&#xff08;详解&#xff09;以及部分理论知识 &#x1f349;欢迎点赞 &#x1f44d; 收藏 ⭐留言评论 &#x1f4dd;私信必回哟&#x1f601; &#x1f349;博主收将持续更新学习记录获&#xf…

螺丝厂家:什么是钣金螺丝?

金属板由常见的金属或合金&#xff08;钢、铜、镍、锡、钛等&#xff09;组成&#xff0c;比木材更硬、更坚固。因此&#xff0c;它需要使用特殊的螺钉。您通常无法将传统螺钉拧入钣金中。值得庆幸的是&#xff0c;有专为钣金设计的特殊类型的螺钉。被称为钣金螺钉&#xff0c;…

对多个 App 设计工具组件使用一个回调

当要在App 中提供多种方法来执行某个操作时&#xff0c;在组件间共享回调非常有用。例如&#xff0c;当用户点击按钮或在编辑字段中按下 Enter 键时&#xff0c;App 可以用同样的方式响应。 共享回调的示例 此示例说明如何创建一个 App&#xff0c;其中包含共享一个回调的两个…

运筹优化 | 模拟退火求解旅行商问题 | Python实现

"""模拟退火旅行商""" import random import numpy as np import math import time import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False location np.loadtxt(city_location.t…

蓝牙插座_风扇_灯

项目需求 通过蓝牙模块&#xff0c;实现手机控制蓝牙插座 / 风扇 / 灯。 本质&#xff1a; 1. 采用蓝牙的透传功能&#xff1b; 2. 控制 IO 口的输出。 HC01_TX -- RX1 HC01_RX -- TX1 项目实现 1、串口非中断法 2. 串口中断法

【UML】第4篇 UML公共机制(补扩展机制)

目录 一、扩展机制 1.1 构造型 1.2 标记值&#xff08;Tagged Value&#xff09; 1.3 约束&#xff08;Constraint&#xff09; 上节扩展机制没有讲完&#xff0c;如上图。 一、扩展机制 1.1 构造型 UML中的扩展机制包括约束、构造型和标记值&#xff0c;其中的构造型定义…

Github、Gitee优秀的开源项目分享

先赞后看&#xff0c;养成习惯&#xff01;&#xff01;&#xff01;❤️ ❤️ ❤️ 资源收集不易&#xff0c;如果喜欢可以关注我哦&#xff01; ​如果本篇内容对你有所启发&#xff0c;欢迎访问我的个人博客了解更多内容&#xff1a;链接地址 ​ Java 项目 javacore - Java …

电子印章法律风险点及安全防范措施

公章是公司处理内外部事务的印鉴&#xff0c;公司对外的正式信函、文件、报告使用公章&#xff0c;盖了公章的文件具有法律效力。公章由公司的法定代表人执掌&#xff0c;法定代表人如果把法定代表人章与公章一同使用就代表公司行为。 随着社会数字化转型&#xff0c;电子印章及…

SLAM算法与工程实践——相机篇:传统相机使用(1)

SLAM算法与工程实践系列文章 下面是SLAM算法与工程实践系列文章的总链接&#xff0c;本人发表这个系列的文章链接均收录于此 SLAM算法与工程实践系列文章链接 下面是专栏地址&#xff1a; SLAM算法与工程实践系列专栏 文章目录 SLAM算法与工程实践系列文章SLAM算法与工程实践…

登录/验证码/注册

登录 pom文件 <!--hutool工具类--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.9</version></dependency><!--jwt--><dependency><groupId>io.jsonw…

maui 调用文心一言开发的聊天APP 3

主要是对代码进行了优化 上一个版本写死了帐号跟密码 &#xff0c;这一个帐本有户可以直接设置对相关的key以及secret如果设置错时&#xff0c;在聊天中也会返回提示。注册帐号时同时也设置了key及secrete升级到了net.8.0导出APK&#xff0c;上一个版本是导出abb.解决了变型问…

Redis新数据类型-Bitmaps

目录 Bitmaps 简介 命令 1. setbit (1) 格式 (2) 实例 2. getbit (1) 格式 (2) 实例 3. bitcount (1) 格式 (2) 实例 4. bitop (1) 格式 (2) 实例 我的其他博客 Bitmaps 简介 Bitmaps 是 Redis 的一种新数据类型&#xff0c;它是一种用于存储位信息的数据结构&…

Netty详细文档

Netty教程 文章目录 Netty教程 Netty简介Netty 的介绍Netty 的应用场景互联网行业游戏行业大数据领域其它开源项目使用到 Netty Netty 的学习资料参考 Java BIO编程I/O 模型BIO、NIO、AIO 使用场景分析Java BIO 基本介绍Java BIO 工作机制Java BIO 应用实例问题分析 Java NIO编…

腾讯云Elasticsearch Service产品体验

基本介绍 产品概述 腾讯云 Elasticsearch Service&#xff08;ES&#xff09;是云端全托管海量数据检索分析服务&#xff0c;拥有高性能自研内核&#xff0c;集成X-Pack。ES 支持通过自治索引、存算分离、集群巡检等特性轻松管理集群&#xff0c;也支持免运维、自动弹性、按需…

iOS使用CoreText完成txt阅读器

CoreText是一个高效处理字符和字形转换和进行文字排版的框架&#xff0c;API基于C语言。 常见的CoreText类介绍 &#xff08;1&#xff09;、CFAttributedStringRef 属性字符串&#xff0c;用于存储需要绘制的文字字符和字符属性 &#xff08;2&#xff09;、CTFramesetterR…

Layui实现自定义的table列悬停事件并气泡提示信息

1、概要 使用layui组件实现table的指定列悬停时提示信息&#xff0c;因为layui组件中没有鼠标悬停事件支持&#xff0c;所以需要结合js原生事件来实现这个功能&#xff0c;并结合layui的tips和列的templte属性气泡提示实现效果。 2、效果图 3、代码案例 <!DOCTYPE html&g…

Spark编程入门

1.8 Spark编程入门 1.8.1 通过IDEA创建Spark工程 ps:工程创建之前步骤省略,在scala中已经讲解,直接默认是创建好工程的 导入Pom文件依赖 <!-- 声明公有的属性 --><properties><maven.compiler.source>1.8</maven.compiler.source><maven.compiler…

企业电子招投标采购系统源码之鸿鹄电子招投标系统+电子招投标的组成

招投标管理系统是一款适用于招标代理、政府采购、企业采购和工程交易等领域的企业级应用平台。该平台以项目为主线&#xff0c;从项目立项到项目归档&#xff0c;实现了全流程的高效沟通和协作。通过该平台&#xff0c;用户可以实时共享项目数据信息&#xff0c;实现规范化管理…