谷粒学苑项目实战(十三):课程管理模块搭建

本文涉及的产品
视频点播 VOD,流量+存储+转码
简介: 谷粒学苑项目实战(十三):课程管理模块搭建

一、课程相关表的关系




8aa055db4c2c4623add46663fb2b7e92.png


二、实现添加课程基本信息功能



1、使用代码生成器生成相关代码

     

还是只需要改这里


142aa3ed52e0478b8bf2de0eb641fd11.png


2、创建vo类封装表单提交的数据


@Data
public class CourseInfoVo {
    @ApiModelProperty(value = "课程ID")
    private String id;
    @ApiModelProperty(value = "课程讲师ID")
    private String teacherId;
    @ApiModelProperty(value = "课程专业ID")
    private String subjectId;
    @ApiModelProperty(value = "课程标题")
    private String title;
    @ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")
    // 0.01
    private BigDecimal price;
    @ApiModelProperty(value = "总课时")
    private Integer lessonNum;
    @ApiModelProperty(value = "课程封面图片路径")
    private String cover;
    @ApiModelProperty(value = "课程简介")
    private String description;
}


3、controller


@RestController
@RequestMapping("/eduservice/course")
@CrossOrigin
public class EduCourseController {
    @Autowired
    private EduCourseService eduCourseService;
    /**
     * 添加课程的基本信息
     * @param courseInfoVo 封装好的course信息
     * @return
     */
    @PostMapping("/addCourseInfo")
    public R addCourseInfo(@RequestBody CourseInfoVo courseInfoVo){
        eduCourseService.saveCourseInfo(courseInfoVo);
        return R.ok();
    }
}


4、service


public interface EduCourseService extends IService<EduCourse> {
    /**
     * 添加课程的基本信息
     * @param courseInfoVo
     */
    void saveCourseInfo(CourseInfoVo courseInfoVo);
}


5、serviceImpl


@Service
public class EduCourseServiceImpl extends ServiceImpl<EduCourseMapper, EduCourse> implements EduCourseService {
    @Autowired
    private EduCourseDescriptionService eduCourseDescriptionService;
    /**
     * 添加课程的基本信息
     * @param courseInfoVo
     */
    @Override
    public void saveCourseInfo(CourseInfoVo courseInfoVo) {
        //向课程表中添加课程基本信息
        //因为传入的courseInfoVo 和 eduCourse 不一样,所以要转换一下
        EduCourse eduCourse = new EduCourse();
        BeanUtils.copyProperties(courseInfoVo, eduCourse);
        int insert = baseMapper.insert(eduCourse);
        if (insert <= 0){
            throw new  GuliException(20001, "添加课程信息失败");
        }
        //获取课程id,用来和课程描述进行关联
        String cid = eduCourse.getId();
        //向课程描述表中加课程描述信息
        EduCourseDescription eduCourseDescription = new EduCourseDescription();
        eduCourseDescription.setDescription(courseInfoVo.getDescription());
        eduCourseDescription.setId(cid);
        eduCourseDescriptionService.save(eduCourseDescription);
    }
}


这里需要注意的点是:由于课程和课程描述是一对一关系,所以我们直接把课程描述的id值设置为课程的id,所以需要把描述的主键生成策略改为INPUT(其实不改也行)


6、启动测试


启动后,打开swagger-ui

f94f668b47bd4798a57ff91dd7dd382d.png557f301bbaf04d94b081e09eddaf1231.png


再看数据库中,edu_course 和 edu_course_description中都插入了数据。

三、实现修改课程基本信息功能


     

实现修改功能,首先要实现修改的时候信息回显(查询)功能;然后实现修改功能。


1、controller


/**
     * 根据课程id查询课程信息
     * @param courseId
     * @return
     */
    @GetMapping("/getCourseInfo/{courseId}")
    public R getCourseInfo(@PathVariable String courseId){
        CourseInfoVo courseInfoVo = eduCourseService.getCourseInfo(courseId);
        return R.ok().data("courseInfoVo", courseInfoVo);
    }
    /**
     * 修改课程信息
     * @param courseInfoVo
     * @return
     */
    @PostMapping("/updateCourseInfo")
    public R updateCourseInfo(@RequestBody CourseInfoVo courseInfoVo){
        eduCourseService.updateCourseInfo(courseInfoVo);
        return R.ok();
    }


2、service


/**
     * 根据课程id查询课程信息
     * @param courseId
     * @return
     */
    CourseInfoVo getCourseInfo(String courseId);
    /**
     * 修改课程信息
     * @param courseInfoVo
     */
    void updateCourseInfo(CourseInfoVo courseInfoVo);


3、serviceImpl


/**
     * 根据课程id查询课程信息
     * @param courseId
     * @return
     */
    @Override
    public CourseInfoVo getCourseInfo(String courseId) {
        //根据id查课程表
        EduCourse course = baseMapper.selectById(courseId);
        CourseInfoVo courseInfoVo = new CourseInfoVo();
        BeanUtils.copyProperties(course, courseInfoVo);
        //根据id查描述表
        EduCourseDescription description = eduCourseDescriptionService.getById(courseId);
        courseInfoVo.setDescription(description.getDescription());
        return courseInfoVo;
    }
    /**
     * 修改课程信息
     * @param courseInfoVo
     */
    @Override
    public void updateCourseInfo(CourseInfoVo courseInfoVo) {
        //根据id修改课程表
        EduCourse eduCourse = new EduCourse();
        BeanUtils.copyProperties(courseInfoVo, eduCourse);
        int update = baseMapper.updateById(eduCourse);
        if (update <= 0){
            throw new GuliException(20001, "修改课程信息失败");
        }
        //根据id修改描述表
        EduCourseDescription eduCourseDescription = new EduCourseDescription();
        eduCourseDescription.setId(courseInfoVo.getId());
        eduCourseDescription.setDescription(courseInfoVo.getDescription());
        eduCourseDescriptionService.updateById(eduCourseDescription);
    }


四、实现课程章节的大纲显示功能


     

逻辑上与课程分类的显示基本相同(都是树形显示)


1、controller


@RestController
@RequestMapping("/eduservice/chapter")
@CrossOrigin
public class EduChapterController {
    @Autowired
    private EduChapterService chapterService;
    /**
     * 课程大纲列表,根据课程id进行查询
     * @param courseId
     * @return
     */
    @GetMapping("getChapterVideo/{courseId}")
    public R getChapterVideo(@PathVariable String courseId){
        List<ChapterVo> list = chapterService.getChapterVideoByCourseId(courseId);
        return R.ok().data("allChapterVideo", list);
    }
}


2、service


public interface EduChapterService extends IService<EduChapter> {
    /**
     * 课程大纲列表,根据课程id查询课程
     * @param courseId
     * @return
     */
    List<ChapterVo> getChapterVideoByCourseId(String courseId);
}


3、serviceImpl


@Service
public class EduChapterServiceImpl extends ServiceImpl<EduChapterMapper, EduChapter> implements EduChapterService {
    //注入小节的service,才能查询小节信息
    @Autowired
    private EduVideoService videoService;
    /**
     * 课程大纲列表,根据课程id查询课程
     * @param courseId
     * @return
     */
    @Override
    public List<ChapterVo> getChapterVideoByCourseId(String courseId) {
        //1 根据课程id查询课程里面所有的章节
        QueryWrapper<EduChapter> wrapperChapter = new QueryWrapper<>();
        wrapperChapter.eq("course_id",courseId);
        List<EduChapter> eduChapterList = baseMapper.selectList(wrapperChapter);
        //2 根据课程id查询课程里面所有的小节
        QueryWrapper<EduVideo> wrapperVideo = new QueryWrapper<>();
        wrapperVideo.eq("course_id",courseId);
        List<EduVideo> eduVideoList = videoService.list(wrapperVideo);
        //创建list集合,用于最终封装数据
        List<ChapterVo> finalList = new ArrayList<>();
        //3 遍历查询章节list集合进行封装
        //遍历查询章节list集合
        for (int i = 0; i < eduChapterList.size(); i++) {
            //每个章节
            EduChapter eduChapter = eduChapterList.get(i);
            //eduChapter对象值复制到ChapterVo里面
            ChapterVo chapterVo = new ChapterVo();
            BeanUtils.copyProperties(eduChapter,chapterVo);
            //把chapterVo放到最终list集合
            finalList.add(chapterVo);
            //创建集合,用于封装章节的小节
            List<VideoVo> videoList = new ArrayList<>();
            //4 遍历查询小节list集合,进行封装
            for (int m = 0; m < eduVideoList.size(); m++) {
                //得到每个小节
                EduVideo eduVideo = eduVideoList.get(m);
                //判断:小节里面chapterid和章节里面id是否一样
                if(eduVideo.getChapterId().equals(eduChapter.getId())) {
                    //进行封装
                    VideoVo videoVo = new VideoVo();
                    BeanUtils.copyProperties(eduVideo,videoVo);
                    //放到小节封装集合
                    videoList.add(videoVo);
                }
            }
            //把封装之后小节list集合,放到章节对象里面
            chapterVo.setChildren(videoList);
        }
        return finalList;
    }
}


五、实现课程章节的添加、修改功能



1、controller


/**
     * 添加章节
     * @param eduChapter
     * @return
     */
    @PostMapping("/addChapter")
    public R addChapter(@RequestBody EduChapter eduChapter){
        chapterService.save(eduChapter);
        return R.ok();
    }
    /**
     * 根据id查询章节   用于修改时的回显
     * @param chapterId
     * @return
     */
    @GetMapping("getChapterInfo/{chapterId}")
    public R getChapterInfo(@PathVariable String chapterId){
        EduChapter chapter = chapterService.getById(chapterId);
        return R.ok().data("chapter", chapter);
    }
    /**
     * 修改章节信息
     * @param eduChapter
     * @return
     */
    @PostMapping("updateChapter")
    public R updateChapter(@RequestBody EduChapter eduChapter){
        chapterService.updateById(eduChapter);
        return R.ok();
    }


因为没有什么特殊的业务要求,所以直接用MP里提供的方法就可以。


六、 实现课程章节的删除功能



删除章节的时候需要注意:章节下还有小节,如果直接删除章节,小节还是会存在,并且成为无用的数据,所以不能直接用MP提供的删除方法。


我们有两种解决思路:


       1、在删除章节的同时将下面的所有小节删掉;

       2、如果章节下面有小节,就不能删除(必须等小节删完了才能删章节)


这次我们采用第二种方法处理,在下面删除课程(第十步)中也会采用第一种方法。实际工作中具体用哪一种,取决于业务需求。


1、controller


/**
     * 删除章节
     * @param chapterId
     * @return
     */
    @DeleteMapping("/deleteChapter/{chapterId}")
    public R deleteChapter(@PathVariable String chapterId){
        //这里要注意的问题是,章节下有小节,如果直接删除章节,小节还是会存在,所以不能直接用MP提供的删除方法
        boolean delete = chapterService.deleteChapter(chapterId);
        if (delete){
            return R.ok();
        } else{
            return R.error();
        }
    }


2、service  


1. /**
2.      * 删除章节
3.      * @param chapterId
4.      */
5. boolean deleteChapter(String chapterId);


3、serviceimpl


/**
     * 删除章节
     * @param chapterId
     */
    @Override
    public boolean deleteChapter(String chapterId) {
        //如果章节下面有小节,就不能删除(必须等小节删完了才能删章节)
        QueryWrapper<EduVideo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("chapter_id", chapterId);
        int count = videoService.count(queryWrapper);
        if (count > 0){ //章节下有小节
            throw new GuliException(20001, "不能删除");
        } else {
            int result = baseMapper.deleteById(chapterId);
            //result=1说明成功删除,result=0说明失败
            return result>0;
        }
    }


七、实现小节的增加、删除和修改功能



每个课程对应的章节下有若干个小节。


代码比较简单,就直接贴controller了。


@RestController
@RequestMapping("/eduservice/video")
@CrossOrigin
public class EduVideoController {
    @Autowired
    private EduVideoService eduVideoService;
    /**
     * 增加小节
     * @param eduVideo
     * @return
     */
    @PostMapping("/addVideo")
    public R addVideo(@RequestBody EduVideo eduVideo){
        eduVideoService.save(eduVideo);
        return R.ok();
    }
    /**
     * 删除小节(后续需要将小节中的视频也删掉)
     * @param id
     * @return
     */
    @DeleteMapping("{id}")
    public R deleteVideo(@PathVariable String id){
        eduVideoService.removeById(id);
        return R.ok();
    }
    /**
     * 根据id查找小节   用于修改小节时的回显
     * @param id
     * @return
     */
    @GetMapping("{id}")
    public R getVideoById(@PathVariable String id){
        EduVideo video = eduVideoService.getById(id);
        return R.ok().data("video", video);
    }
    /**
     * 修改小节
     * @param eduVideo
     * @return
     */
    @PostMapping("/updateVideo")
    public R updateVideo(@RequestBody EduVideo eduVideo){
        eduVideoService.updateById(eduVideo);
        return R.ok();
    }
}


八、实现课程信息确认功能


     

在增加课程信息时,在发布课程之前,需要确认课程信息是否正确 。

     

因为课程的所有信息需要查询多张表才能得到,所以需要我们自己编写SQL语句来实现。


1、controller


/**
     * 根据课程id查询课程确认信息
     * @param id
     * @return
     */
    @GetMapping("/getPublishCourseInfo/{id}")
    public R getPublishCourseInfo(@PathVariable String id){
        CoursePublishVo coursePublishVo = eduCourseService.publishCourseInfo(id);
        return R.ok().data("publishCourse", coursePublishVo);
    }


2、service


/**
     * 根据课程id查询课程确认信息
     * @param id
     * @return
     */
    CoursePublishVo publishCourseInfo(String id);


3、mapper


public interface EduCourseMapper extends BaseMapper<EduCourse> {
    public CoursePublishVo getPublishCourseInfo(String courseId);
}


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.shang.eduservice.mapper.EduCourseMapper">
    <!-- 根据课程id查询课程确认信息 -->
    <select id="getPublishCourseInfo" resultType="com.shang.eduservice.entity.vo.CoursePublishVo">
        SELECT ec.id, ec.title, ec.price, ec.lesson_num as lessonNum,ec.cover,
               et.name as teacherName,
               es1.title AS subjectLevelOne,
               es2.title AS subjectLevelTwo
        FROM edu_course ec LEFT OUTER JOIN edu_course_description ecd ON ec.`id`=ecd.`id`
                           LEFT OUTER JOIN edu_teacher et ON ec.`teacher_id`=et.id
                           LEFT OUTER JOIN edu_subject es1 ON ec.`subject_parent_id`=es1.`id`
                           LEFT OUTER JOIN edu_subject es2 ON ec.`subject_id`=es2.`id`
        WHERE ec.`id`=#{couseId};
    </select>
</mapper>


4、servceImpl


@Override
    public CoursePublishVo publishCourseInfo(String id) {
        //调用自己写的mapper
        return baseMapper.getPublishCourseInfo(id);
    }


5、swagger测试


发现报错:Invalidbound statement (not found)

具体原因和解决方法我已经写在另一篇博客里了,传送门:


 org.apache.ibatis.binding.BindingException: Invalidbound statement (not found)的解决方案和造成原因分析(超详细) 

3772b3597d0f42ef86bdc180ba7e39f1.png57f23788da1c4af089ed5ee2bd4e6276.png


查到数据。


九、课程最终确认


     

在course表中,有一个字段用来标记是否发布。

97297186814e4ca4ace357bc05bdc6d8.png


只要修改一下这个字段的属性值就可以完成发布了。


/**
     * 课程最终发布
     * @param id
     * @return
     */
    @PostMapping("/publishCourse/{id}")
    public R publishCourse(@PathVariable String id){
        EduCourse eduCourse = new EduCourse();
        eduCourse.setId(id);
        //这里如果把状态定义为枚举类型  会更好一些
        eduCourse.setStatus("Normal");
        eduCourseService.updateById(eduCourse);
        return R.ok();
    }


十、实现课程大纲列表显示


       

逻辑上和之前的讲师列表一样,就不多赘述了。


十一、删除课程



首先我们要知道,在处理表与表一对多的关系时,通常是将多的那一张表创建字段作为外键,指向为一的那张表的主键。其实,在实际工作中,一般是不将外键设置出来的,程序员只要心里清楚这是外键就可以了,因为设置外键之后必须保持数据一致性,会带来一些问题(比如想要删除课程,就必须先删除他下面的所有章节;想要删除章节,就必须先删除他下面的所有小节)


这次我们删除采用的方法是,删除课程时,先把他下面的所有东西删掉。


1、controller


/**
     * 删除课程
     * @param courseId
     * @return
     */
    @DeleteMapping("{courseId}")
    public R deleteCourse(@PathVariable String courseId){
        eduCourseService.removeCourse(courseId);
        return R.ok();
    }


2、service


/**
     * 删除课程
     * @param courseId
     */
    void removeCourse(String courseId);


3、serviceImpl


@Override
    public void removeCourse(String courseId) {
        //根据课程id删除小节
        eduVideoService.removeVideoByCourseId(courseId);
        //根据课程id删除章节
        eduChapterService.removeChapterByCourseId(courseId);
        //根据课程id删除描述(课程和描述是一对一关系)
        eduCourseDescriptionService.removeById(courseId);
        //根据课程id删除课程
        int result = baseMapper.deleteById(courseId);
        if (result <= 0){
            throw new GuliException(20001, "删除课程失败");
        }
    }


4、小节中


public interface EduVideoService extends IService<EduVideo> {
    /**
     * 根据课程id删除小节
     * @param courseId
     */
    void removeVideoByCourseId(String courseId);
}


@Service
public class EduVideoServiceImpl extends ServiceImpl<EduVideoMapper, EduVideo> implements EduVideoService {
    /**
     * 根据课程id删除小节
     * @param courseId
     */
    @Override
    public void removeVideoByCourseId(String courseId) {
        // TODO 删除小节的时候,还要删除对应的视频
        QueryWrapper<EduVideo> queryWrapper = new QueryWrapper();
        queryWrapper.eq("course_id", courseId);
        baseMapper.delete(queryWrapper);
    }
}

5、章节中


/**
     * 根据课程id删除章节
     * @param courseId
     */
    void removeChapterByCourseId(String courseId);
/**
     * 根据课程id删除其下面的所有章节
     * @param courseId
     */
    @Override
    public void removeChapterByCourseId(String courseId) {
        QueryWrapper<EduChapter> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("course_id", courseId);
        baseMapper.delete(queryWrapper);
    }


十二、实现添加小节时上传视频的功能


       

上传视频使用阿里云视频点播功能,具体操作参考我的博客:

     

谷粒学苑项目实战(十四):实现阿里云视频点播功能(java编码实现)


1、引入依赖


2、创建application配置文件


# 服务端口
server.port=8003
# 服务名
spring.application.name=service-vod
# 环境设置:dev、test、prod
spring.profiles.active=dev
#阿里云 vod
#不同的服务器,地址不同
aliyun.vod.file.keyid=xxxxxx
aliyun.vod.file.keysecret=xxxxx
# 最大上传单个文件大小:默认1M
spring.servlet.multipart.max-file-size=1024MB
# 最大置总上传的数据大小 :默认10M
spring.servlet.multipart.max-request-size=1024MB
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#开启熔断机制
feign.hystrix.enabled=true
# 设置hystrix超时时间,默认1000ms
#hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=6000


3、启动类


@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@ComponentScan(basePackages = {"com.shang"})
public class VodApplication {
    public static void main(String[] args) {
        SpringApplication.run(VodApplication.class);
    }
}


4、controller


/**
     * 上传视频到阿里云
     * @param file
     * @return
     */
    @PostMapping("uploadAlyivideo")
    public R uploadAlyivideo(MultipartFile file){
        //获取要上传的视频id
        String videoId = vodService.uploadVideoAly(file);
        return R.ok();
    }


5、service


 /**
     * 上传视频到阿里云
     * @param file
     * @return
     */
    String uploadVideoAly(MultipartFile file);


6、serviceImpl


/**
     * 上传文件到阿里云
     * @param file
     * @return
     */
    @Override
    public String uploadVideoAly(MultipartFile file) {
        try {
            String accessKeyId = ConstantVodUtils.ACCESS_KEY_ID;
            String accessKeySecret = ConstantVodUtils.ACCESS_KEY_SECRET;
            String title = "哈哈哈";  //上传后的名称
            String fileName = file.getOriginalFilename();   //文件原始名称
            InputStream inputStream = file.getInputStream();
            UploadStreamRequest request = new UploadStreamRequest(accessKeyId, accessKeySecret, title, fileName, inputStream);
            /* 是否使用默认水印(可选),指定模板组ID时,根据模板组配置确定是否使用默认水印*/
            //request.setShowWaterMark(true);
            /* 自定义消息回调设置,参数说明参考文档 https://help.aliyun.com/document_detail/86952.html#UserData */
            //request.setUserData(""{\"Extend\":{\"test\":\"www\",\"localId\":\"xxxx\"},\"MessageCallback\":{\"CallbackURL\":\"http://test.test.com\"}}"");
            /* 视频分类ID(可选) */
            //request.setCateId(0);
            /* 视频标签,多个用逗号分隔(可选) */
            //request.setTags("标签1,标签2");
            /* 视频描述(可选) */
            //request.setDescription("视频描述");
            /* 封面图片(可选) */
            //request.setCoverURL("http://cover.sample.com/sample.jpg");
            /* 模板组ID(可选) */
            //request.setTemplateGroupId("8c4792cbc8694e7084fd5330e56a33d");
            /* 工作流ID(可选) */
            //request.setWorkflowId("d4430d07361f0*be1339577859b0177b");
            /* 存储区域(可选) */
            //request.setStorageLocation("in-201703232118266-5sejdln9o.oss-cn-shanghai.aliyuncs.com");
            /* 开启默认上传进度回调 */
            // request.setPrintProgress(true);
            /* 设置自定义上传进度回调 (必须继承 VoDProgressListener) */
            // request.setProgressListener(new PutObjectProgressListener());
            /* 设置应用ID*/
            //request.setAppId("app-1000000");
            /* 点播服务接入点 */
            //request.setApiRegionId("cn-shanghai");
            /* ECS部署区域*/
            // request.setEcsRegionId("cn-shanghai");
            UploadVideoImpl uploader = new UploadVideoImpl();
            UploadStreamResponse response = uploader.uploadStream(request);
            System.out.print("RequestId=" + response.getRequestId() + "\n");  //请求视频点播服务的请求ID
            if (response.isSuccess()) {
                System.out.print("VideoId=" + response.getVideoId() + "\n");
            } else { //如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因
                System.out.print("VideoId=" + response.getVideoId() + "\n");
                System.out.print("ErrorCode=" + response.getCode() + "\n");
                System.out.print("ErrorMessage=" + response.getMessage() + "\n");
            }
            return response.getVideoId();
        } catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }



相关文章
|
3月前
|
SQL 安全 前端开发
毕设答辩问题讲解说明:基于SpringBoot+Vue的汉服文化交流社区平台设计与开发
这篇文章是关于一个基于SpringBoot+Vue的汉服文化交流社区平台的毕业设计答辩问题讲解,涵盖了系统功能、亮点创新、数据库设计、积分领取机制、数据库安全和个人密码修改功能等方面的答辩问题和回答要点。
|
小程序 Java 关系型数据库
0012Java程序设计-springboot基于微信小程序的校园智慧帮系统的设计与实现
0012Java程序设计-springboot基于微信小程序的校园智慧帮系统的设计与实现
146 0
|
5月前
|
监控 JavaScript 安全
杨校老师课堂之基于SpringBoot + Vue 的智能停车场平台设计
杨校老师课堂之基于SpringBoot + Vue 的智能停车场平台设计
35 0
|
6月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的校园家教兼职信息交流平台的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的校园家教兼职信息交流平台的详细设计和实现(源码+lw+部署文档+讲解等)
|
小程序 测试技术 数据库
0022Java程序设计-ssm微信小程序社区互助平台
0022Java程序设计-ssm微信小程序社区互助平台
51 0
|
Dubbo 前端开发 Java
【尚好房项目实战】:第一章项目架构介绍
【尚好房项目实战】:第一章项目架构介绍
|
人工智能 小程序 Java
基于Springboot+Vue2前后端分离框架的智慧校园电子班牌系统源码,智慧学校源码+微信小程序+SaaS运营平台源码
技术开发环境:Java+springboot+vue+element-ui+mysql 用的是最新的技术栈,完全满足开发要求。
314 0
基于Springboot+Vue2前后端分离框架的智慧校园电子班牌系统源码,智慧学校源码+微信小程序+SaaS运营平台源码
|
Java 测试技术 数据库
谷粒学苑项目实战(二):讲师管理模块搭建(上)
谷粒学苑项目实战(二):讲师管理模块搭建
227 0
谷粒学苑项目实战(二):讲师管理模块搭建(上)
|
JSON 数据库 数据格式
谷粒学苑项目实战(二):讲师管理模块搭建(下)
谷粒学苑项目实战(二):讲师管理模块搭建
155 0
谷粒学苑项目实战(二):讲师管理模块搭建(下)
|
JSON 前端开发 easyexcel
谷粒学苑项目实战(十二):课程分类管理模块搭建
谷粒学苑项目实战(十二):课程分类管理模块搭建
191 0
谷粒学苑项目实战(十二):课程分类管理模块搭建