mybatisplus的使用

MybatisPlus的使用

MybatisPlus官方文档



前言

mybatis是持久层框架,对数据库进行增删改查。

mybatis-plus简称MP,MP是对mybatis(MB)的一个增强工具,在MB的基础上增强功能,不会对MB的原有代码做修改。

一、特性:

  1. 无侵入。在MB的基础上增强功能,不会对MB的原有代码做修改

  2. 损耗小:启动即会自动注入基本 CURD(就是对数据库表的操作代码和第三条一样),性能基本无损耗

  3. 强大的 CRUD 操作:内置通用 Mapper、通用 Service。对于寻常、条件简单的 增删改查单表操作不用写代码。

  4. 支持 Lambda 形式调用:可以用 Lambda 表达式编写各类查询条件

  5. 主键自动生成:四种主键(id)的生成策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解 决主键问题

  6. 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询

  7. 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码, 支持模板引擎

  8. 支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、 SQLServer2005、SQLServer 等多种数据库

  9. 支持 XML 热加载:Mapper 对应的 XML 支持热加载,对于简单的 CRUD 操作,甚至可以无 XML 启动


提示:以下是本篇文章正文内容

一、MP快速入门

使用步骤

  1. 引入MP依赖,该依赖还集成了MB和MP的所有功能,只需要引入这个就可以同时使用MP和MB
      <!-- mybatis-plus插件依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>3.1.1</version>
        </dependency>
  1. 定义mapper
    自定义mapper继承MybatisPlus提供的BaseMapper接口,这是可以直接调用UserMapper的方法(MP会基于动态代理实现BaseMapper接口方法)
//泛型使用你想进行增删改查的po类
public interface UserMapper extends BaseMapper<User> {
}

BaseMapper接口里面有很多已经实现了的CRUD方法
新增:insert
删除:delete
查询:select
在这里插入图片描述

  1. 在启动类或者配置类上上加上包扫描
@MapperScan("*.mapper")
  • 问题:MP怎么实现单表的增删改查,什么也没写MP怎么知道要访问那张表,字段是什么?

常用注解

MP通过扫描实体类(是你自定义mapper继承BaseMapper时指定的泛型),并基于反射来获取实体类信息来作为数据库表信息

并且约定大于配置

  • 类名驼峰转下划线作为表名
  • 名为ID的字段作为主键
  • 变量名驼峰转下划线作为表的字段名
    在这里插入图片描述

当类名、变量名和表名、字段名不一致时,可以使用以下注解来指定
MybatisPlus中比较常用的几个注解如下:

  1. @TableName:用来指定表名
@TableName("tb_user")
public class User{}
  1. @Tableld:用来指定表中的主键字段信息
	@TableId(type = IdType.AUTO)
	private Long id;

MP实现id生成策略
ldType枚举:

  • AUTO: 数据库自增长

  • INPUT: 通过set方法程序员自行输入

  • ASSIGN_ID: MP分配ID。接口ldentifierGenerator的方法nextld来生成id,默认实现类为DefaultldentifierGenerator雪花算法,也可以自定义

  1. @TableField:用来指定表中的普通字段信息

需要使用@TableField的几种特殊情况

	
	@TableField(value = "email") //变量名和字段名不一致
	private String mail;

    @TableField(exist = false)
    private String address; //在数据库表中是不存在的

	@TableField("is_married")
    private Boolean isMarried;//变量名以is开头且是布尔值

    @TableField("`order`")
    private Integer order;//成员变量名与数据库关键字冲突
    
    // 插入数据时进行填充
    @TableField(select = false, fill = FieldFill.INSERT) //查询时不返回该字段的值
    private String password;

常用配置

这些配置除了别名扫描包都是默认的

mybatis-plus:
  type-aliases-package: com.itheima.mp.domain.po #别名扫描包:在定义map.xml写实体类类型时不用写全路径,写类简化名就行
  mapper-locations: "classpath* :/mapper/**/*.xml" # Mapper.xml文件地址,默认值
  configuration:
    map-underscore-to-camel-case: true  #是否开启下划线和驼峰的映射
    cache-enabled: false   #是否开启二级缓存
  global-config:
    db-config:
      id-type: assign_id  # id为雪花算法生成
      update-strategy: not_null #更新策略:只更新非空字段

快速入门总结

MyBatisPlus使用的基本流程

  1. 引入起步依赖
  2. 自定义Mapper基础BaseMapper
  3. 在实体类上添加注解声明表信息
  4. 在application.yml中根据需要添加配置

二、核心功能

1.SQL条件构造器

  • 前言:
    由于很多业务增删改查的条件都很复杂,基于MP基本的增删改查方法(一般是基于ID)无法完成业务,所以需要使用条件构造器来构造条件

在这里插入图片描述

条件构造器类关系图:

在这里插入图片描述

AbstractWrapper类:

where条件

  1. eq():等于
  2. ne():不等于
  3. gt():大于
  4. ge():大于等于
  5. lt():小于
  6. le():小于等于
  7. like():模糊
  8. nolike():非模糊
  9. in():在…内,如(1,2,3)
  10. between():如(1~3)

在这里插入图片描述
QueryWrapper类和UpdateWrapper类都继承了AbstractWrapper类所以都可以使用where条件的构造
事例:

        QueryWrapper<User> wrapper = new QueryWrapper<>();
        //查询条件
        wrapper.eq("password", "123456");//password等于123456记录
        wrapper.ge("age","10");//age大于等于10记录
        wrapper.like("name","李");//name里有李的记录
        wrapper.in("id","1,2,3");//ID在1,2,3内的记录
        // 查询的数据超过一条时,会抛出异常
        User user = this.userMapper.selectOne(wrapper);
  • QueryWrapper类:
    select部分
    select哪些字段:
    在这里插入图片描述

事例:

        wrapper.select("id","name","age"); //指定查询的字段
  • UpdateWrapper类
    set部分
    在这里插入图片描述

事例:

        UpdateWrapper<User> wrapper = new UpdateWrapper<>();
        wrapper.set("age", 21).set("password", "999999") //更新的字段
        .eq("user_name", "zhangsan"); //更新的条件

        //根据条件做更新
        int result = this.userMapper.update(null, wrapper);

由于这些条件构造器类都是硬编码,直接写字符串魔法值,不符合规范,所以一般使用LambdaAbstractWrapper类

LambdaAbstractWrapper类

这些类用法和AbstractWrapper类 一样,不过是使用Lambda来构造条件,不在硬编码

  • LambdaAbstractWrapper类 和LambdaQueryWrapper类
    事例:
        LambdaQueryWrapper<User> queryWrapper = new LambdaQuerWrapper<>();
        queryWrapper
                .ge(User::getAge,"10")// age>=10
                .like(User::getUserName,"l")// %l%
                .select(User::getUserName,User::getAddress);// 查询username和address字段
                //.selectOne();这是条件不能直接写crud方法,可以用lambdaQuery或lambdaUpdate类这些类不用new Wrapper条件


  • LambdaUpdateWrapper类
    事例:
        LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper
                .set(User::getPassword,"123")//设置密码为123
                .setSql("age = age - 10");//将age-10 自定义set


2.自定义SQL

原因:

一条SQL语句有多个部分(select、set、where等)组成,在一些复杂的业务场景中使用MP生成SQL语句是要在业务层上编写SQL语句(如setSql()这个方法就是),不符合规范。只允许在map或者map.xml中定义SQL,此时就得自定义SQL
如:在这里插入图片描述
这就是在业务层编写SQL语句

        LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper
                .set(User::getPassword,"123")//设置密码为123
                .setSql("age = age - 10");//将age-10 自定义set

解决:

用MP(业务层上写)构建复杂where条件,剩下的SQL语句自定义,然后在map或map.xml文件上拼接

使用步骤:

需求: 将id在指定范围的用户(例如1、2、4)的余额扣减指定值

  1. 基于Wrapper构建where条件,在业务层
        List<Long> ids = List.of(1L,2L,3L);
        int amount = 200;
        //1.构建where条件
        LambdaQueryWrapper<User> Wrapper = new LambdaQueryWrapper<User>().in(User::getId,ids);
        //2.自定义SQL方法调用
        userMapper.updateBalanceByIds(Wrapper,amount);
        
  1. 在mapper方法参数中用Param注解声明变量名称,wrapper条件参数必须是"ew",其他参数根据业务定义,持久层
    void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);

  1. 在map.xml(map)自定义SQL,并使用Wrapper条件,用${ew.customSqlSegment}拼接
    <update id="updateBalanceByIds">
        update tb_user set balance = balance - #{amount} ${ew.customSqlSegment}
    </update>

3.使用Service接口

原因:

平时service接口还需要自定义增删改查方法来调用map的方法。而对于基本的增删改查service方法也可以不用编写,可以使用MP的Service接口的方法直接调用

使用步骤

####1. 自定义Service接口继承IService接口(MP的)

继承了MP的Service方法没有实现,所以在自定义Service实现类再继承MP的ServiceImpl类
//泛型为实体类类型
public interface ITbUserService extends IService<TbUser> {

}
  1. 自定义Service实现类,实现自定义接口并继承ServiceImpl类(MP的)

    指定泛型自定义map是Service底层自己调用

@Service
//                            继承MPServiceImpl   自定义map    实体类类型            实现自定义Service
public class TbUserServiceImpl extends ServiceImpl<TbUserMapper, TbUser> implements ITbUserService {

}
}
  1. 在Controller类中注入使用

一般在Controller中注入都是使用@AutoWrite注解,但这样的方式spring不推荐,spring推荐使用构造函数注入。

不想写构造函数,用lombok的注解来生成构造函数

在使用Controller时先思考MP有没有提供对应的Service方法给我们调用,没有在自己写

@RestController
@RequestMapping("/open")
//@AllArgsConstructor//该注解构造函数所有成员变量都注入
@RequiredArgsConstructor//部分参数注入,如加了final的常量成员变量
public class CourseOpenController {

	//lombok注解构造函数注入
	private final CourseBaseInfoService courseBaseInfoService;
	//自动注入
	//@Autowired
    //private CourseBaseInfoService courseBaseInfoService;
    
}

IService接口各类方法

  1. 新增
    saveOrUpdate()在传入的参数中有ID时为更新,没有为新增
    在这里插入图片描述
  2. 删除
  • removeByIds()和removeBatchByIds()区别:
    removeByIds()是使用delete 条件 in()来一个个删除
    removeBatchByIds()是使用delete 条件 id = 来,但gdbc的批处理来批量提交批量删,在数据量大时效率更高
    在这里插入图片描述
  1. 更新
    在这里插入图片描述

  2. 查询一个
    在这里插入图片描述

  3. 查询多个
    在这里插入图片描述

  4. 查询数量
    在这里插入图片描述

  5. 分页查询
    在这里插入图片描述

9. lambda链式编程实现查询条件,不用再new wrapper条件

在这里插入图片描述

例:
在这里插入图片描述
用MB,xml文件写的SQL语句

在这里插入图片描述

使用lambdaQuery():
返回值类型 返回值 = lambdaQuery()
.where条件(条件,参数类型,参数)
多个.list()一个.one(),分页查询.page(),是否存在.exists(),计数.count();

@Service
//                            继承MPServiceImpl   自定义map    实体类类型            实现自定义Service
public class TbUserServiceImpl extends ServiceImpl<TbUserMapper, TbUser> implements ITbUserService {


    @Override
    public List<TbUser> queryUsers(String name,Integer status,Integer minBalance,Integer maxBalance) {

        List<TbUser> list = lambdaQuery()
                .like(name != null, TbUser::getUsername, name)
                .eq(status != null, TbUser::getStatus, status)
                .ge(minBalance != null, TbUser::getMinBlance, maxBalance)
                .le(maxBalance != null, TbUser::getMaxBalance, maxBalance)
                .list();//一个.one(),分页查询.page(),是否存在.exists(),计数.count()
        return list;
    }
}

使用lambdaUpdate():
lambdaUpdate()
.set(条件,参数类型,参数)
.where条件(条件,参数类型,参数)
.update();

//.update();是一定要加的要不不能更新。

批处理方案

1.普通for循环逐条插入速度极差﹐不推荐

  1. 每一次请求只提交一条SQL语句,数据量越多,网络请求就越多,耗时就越多
  2. 每一条SQL都是逐条执行的,执行性能也差

2.MP的批量新增·基于预编译的批处理·性能不错

  1. 比如一次性编译1000条SQL一次性提交,减少网络请求的次数
  2. 但每次也只是一条SQL执行一行数据,不是真正的批处理,只是批提交

3.MySql数据库配置jdbc参数,开启预编译rewriteBatchedStatements=true参数

  1. 会把一批SQL语句处理成一条SQL语句
  2. 在配置数据库URL上添加

三、扩展功能

1.po,service,map,controller代码生成

1.使用官方提供的mybatisx,编写代码生成模版来生成代码
官方代码生成

2.使用mybatisplus插件,使用这个插件只需要简单配置就能生成代码

  1. 下载mybatisplus插件
    在这里插入图片描述

在下载完成后会出现一个other标签

  1. 点击other标签,配置数据库信息,要选择连接的数据库

在这里插入图片描述
在这里插入图片描述

  1. 为生成代码填写信息

在这里插入图片描述
在这里插入图片描述
选择要查询出的字段
在这里插入图片描述

2.Db静态工具(IService的静态方法)

在开发业务时会出现多个Service之间互相调用,采用@AutoWrite的方式注入,那Service之间相互注入会出现循环依赖,给开发带来麻烦。所以在出现Service互相调用时,可以使用Db静态工具来去调用。

在这里插入图片描述
用法:
和IService的使用方法类似,不同的是在调用时要指定Class类型

Db.lambdaQuery(Setmeal.class).eq(Setmeal::getName,name).one();

3.逻辑删除

在有些业务中如:用户在购买商品后产生了订单,有使用户会把订单删除,但对于商家而言订单是重要数据,不用删除。所以这时就需要使用逻辑删除。

逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:

  • 在表中添加一个字段标记数据是否被删除
  • 当删除数据时把标记置为1
  • 查询时只查询标记为0的数据
//删除操作
UPDATE user SET deleted = 1 WHERE id = 1 AND deleted = 0
//查询操作
SELECT * FROM user WHERE deleted = 0

自己实现要在每条SQ语句上增加一个判断 deleted = 0,很麻烦,而MP提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。

MP逻辑删除方法:

  1. 在application.yaml文件中配置逻辑删除的字段名称和值即可:
mybatis-plus:
	global-config:db-config:
	logic-delete-field: flag # 全局逻辑删除的实体字段名,字段类型可以是boolean、integer
	logic-delete-value: 1 #逻辑已删除值(默认为1)
	logic-not-delete-value: 0#逻辑未删除值(默认为0)
  1. 或者在需要逻辑删除的字段上加@TableLogic即可

注意:

逻辑删除本身也有自己的问题,比如:

  • 会导致数据库表垃圾数据越来越多,影响查询效率.
  • SQL中全都需要对逻辑删除字段做判断,影响查询效率。因此,我不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。

4.枚举处理器

mybatis已经实现了其他Java数据类型和数据库中字段之间的相互转换

作用: JavaPO类中的枚举类型和数据库中字段之间的相互转换

方法:

  1. 给枚举中的与数据库对应value值添加@EnumValue注解

@JsonValue//用于告诉springmvc返回前端的是哪一个值,默认是枚举的名字

@Getter
public enum SexEnum implements IEnum<Integer> {

    MAN(1,"男"),
    WOMAN(2,"女");

    @EnumValue
    @JsonValue//用于告诉springmvc返回前端的是哪一个值,默认是枚举的名字
    private int value;
    private String desc;

    SexEnum(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }
    @Override
    public Integer getValue() {
        return this.value;
    }
    @Override
    public String toString() {
        return this.desc;
    }
}
  1. 在配置文件中配置统一的枚举处理器,实现类型转换
mybatis-plus:
	configuration:
		default-enum-type-handler: com.baomidou .mybatisplus.core.handlers.MybatisEnumTypeHandler

5.JSON处理器

实现Java实体类中的成员变量(对象)和数据库中字段(JSON类型)之间的相互转换

  1. 为对象变量加上@TableField(TypeHandler = JacksonTypeHandler.class)
@TableField(TypeHandler = JacksonTypeHandler.class)
private UserInfo userInfo;
  1. @TableName加上自动映射,因为在类内使用对象成员变量会出现对象的嵌套,要在xml文件上加上ResultMap映射(通用查询映射结果)
@TableName("tb_user",autoResultMap = true)

但在一般的业务开发中,JAVA a类内还是用string来存储b类对象信息,但是JSON格式,数据库中a类对应的a表存储b类对象信息是text类型。而b类信息用另一个b表来存储,使用id逻辑外键关联a表,查出该b表信息再转为JSON。而不是对象的嵌套。

四、插件

1.分页插件

步骤:

1. 编写分页插件配置类

分页插件底层是基于拦截器来完成的,通过拦截SQL语句来实现分页

@Configuration
@MapperScan("com.xuecheng.content.mapper")
public class MybatisPlusConfig {
 /**
  * 定义分页拦截器
  */
 @Bean
 public MybatisPlusInterceptor mybatisPlusInterceptor() {
 //核心拦截器
  MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
  //分页插件
  PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
  //设置分页参数
  pageInterceptor.setMaxLimit(1000L);//分页上限
  //还可以添加MP的其他插件如乐观锁插件等
  interceptor.addInnerInterceptor(pageInterceptor);
  return interceptor;
 }


}
2. 使用分页插件API
Page<PO> page = new Page<Setmeal>(pageNo, pageSize);
page = ServiceImpl.page(page,wrapper);
page.getTotal();
page.getPages();

例子

		// 1.查询
        int pageNo = 1, pageSize = 5;
        //1.1.分页参数
        Page<Setmeal> page = new Page<Setmeal>(pageNo, pageSize);
        // 1.2.排序参数,通过orderItem来指定
        page.addOrder(new OrderItem("balance", false));
        // 1.3.分页查询
        Page<Setmeal> p = this.page(page);
        //2.总条数
        System.out.println("total = " + p.getTotal());
        // 3.总页数
        System.out.println("pages = " + p.getPages());
        //4.分页数据
        List<Setmeal> records = p.getRecords();
        records.forEach(System.out::println);
3. 编写通用page请求参数,page返回值

通用分页参数

@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
    @ApiModelProperty("当前页码")
    private Integer pageNo;
    @ApiModelProperty("当前页记录数")
    private Integer pageSize;
    @ApiModelProperty("排序字段");
    private string sortBy;
    @ApiModelProperty("是否升序")
    private Boolean isAsc;


/**
因为每一个分页查询都需要构建分页条件,并且和业务不相关。所以可以在通用分页参数实现方法,直接调用就能返回分页条件
*/
    //构建分页条件
    public<T> Page<T> toMpPage(OrderItem...items){
        // 1.分页条件
        Page<T> page=new Page<T>(pageNo,pageSize);
        //2.排序条件
        if(Strutil.isNotBlank(sortBy)){
        //sortBy不为空
        	page.addOrder(new OrderItem(sortBy,isAsc));
        }else if(items!=null){
			//items不为空,默认排序
        	page.addorder(items);
        }
        return page;
}
	//传入排序字段和是否升序构建分页条件
	public<T> Page<T> toMpPage(String defaultSortBy,Boolean defaultAsc){
        return toMpPage(new OrderItem(defaultSortBy,defaultAsc));
    }
    //默认以创建时间为排序字段,降序构建分页条件
	public<T> Page<T> toMpPageDefaultsortByCreateTime(){
        return toMpPage(new OrderItem("create_time",false));
     }
     //默认以更新时间为排序字段,降序构建分页条件
	public<T> Page<T> toMpPageDefaultsortByUpdateTime(){
        return toMpPage(new OrderItem("update_time",false));
    }

}

分页查询条件,继承通用分页参数

@EqualsAndHashCode(callsuper = true)
@Data
@ApiModel(description = "用户查询条件实体")
public class userQuery extends PageQuery {
    //查询条件
    
    @ApiModelProperty("用户名关键字")
    private String name;
    @ApiModelProperty("用户状态:1-正常,2-冻结")
    private Integer status;
    @ApiModelProperty("余额最小值")
    private Integer minBalance;
    @ApiModelProperty("余额最大值")
    private Integer maxBalance;
}

通用返回值

@Data
@ApiModel(description = "分页结果")
public class PageDTO<T> {
    @ApiModelProperty("总条数")
    private Long total;
    @ApiModelProperty("总页数")
    private Long Pges;
    @ApiModelProperty("集合")
    private List<T> list;

	    public static <PO,VO> PageDTO<VO> of(Page<PO> p,Class<VO> clazz Function<PO,VO> convertor) {
        PageDTO<VO> dto = new PageDTO<>();
        //1.总条数
        dto.setTotal(p.getTotal());
        // 2. 总页数
        dto.setPages(p.getPages());
        // 3. 当前数据
        List<PO> records = p.getRecords();
        if (CollectionUtils.isEmpty(records)) {
            dto.setList(Collections.emptyList());
            return dto;
        }
        //4. 拷贝user的vo
        //dto.setList(BeanUtil.copyToList(records,clazz));
        / / 4.拷贝user 的VO
		dto.setList(records.stream().map(convertor).Collect(collectors.tolist()));

        //5. 返回
        return dto;
    }
}
/ /3.封装Vo结果
I
return PageDTo.of(p,user -> {
// 1.拷贝基础属性
Uservo vo = Beanutil.copyProperties(user,Uservo.class);
//2.处理特殊逻辑
vo.setUsername(vo.getUsername( ).substring(0,vo.getUsername().length() - 2) + "**");
return vo;
});

of()方法是别的对象转为当前对象
to*()方法是把当前对象转为别的对象

  • 39
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值