0. 前言

因为太久太久没有碰项目了(上一次还是2021年8月开发个人博客的时候),所以这一次打算从头到尾把整个学习和搭建的过程记录下来。

1. 简介

这是一个基于SpringBootMybatis的企业级文件上传下载的实战项目,没有太多前端和UI的东西。

2. 链接

【编程不良人】基于SpringBoot和Mybatis企业级文件上传下载项目实战

1. 设计

1. 需求

  1. 用户登陆,展示用户的所有文件(文件如果是图片则在页面中显示图片)
  2. 完成文件的下载在线打开(在线打开不计入下载次数)
  3. 在一张页面中完成文件的上传功能,上传的目录要根据日期,每天创建一个文件夹(文件夹统一命名格式:“yyyy-MM-dd”),上传文成后要跳转到查询所有页面

2. 页面图

3. 库表设计

根据需求,可以简单把库表设计成:1. 用户表(用户信息:ID,用户名,密码);2. 文件表(文件信息:用户ID,文件名…)

DROP TABLE IF EXISTS `t_files`;
CREATE TABLE `t_files` (`id` int(8) NOT NULL,`oldFileName` varchar(200) DEFAULT NULL,`newFileName` varchar(300) DEFAULT NULL,`ext` varchar(20) DEFAULT NULL,`path` varchar(300) DEFAULT NULL,`size` varchar(200) DEFAULT NULL,`type` varchar(120) DEFAULT NULL,`isImg` varchar(8) DEFAULT NULL,`downcounts` int(6) DEFAULT NULL,`uploadTime` datetime DEFAULT NULL,PRIMARY KEY (`id`),KEY `userId_idx` (`userId`),CONSTRAINT `userId` FOREIGN KEY (`userId`) REFERENCES `t_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (`id` int(8) NOT NULL,`username` varchar(80) DEFAULT NULL,`password` varchar(80) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

为后续方便,可以把db.sql放到resources/下面

2. 项目创建

因为不用华丽的前端,所以只需要创建一个简单的Springboot项目即可。

1. 依赖

在创建项目的时候导入基本依赖:

2. 没用文件

  • 把用不到的文件删掉

  • 本项目为了快捷演示,没有作任何测试,下面把spring-boot-starter-test依赖删掉了,所以可以把/test/java下的文件都删掉

3. pom.xml(版本

  • SpringBoot版本,视频教程比较老,里面用的版本是2.2.5,现在用的是2.6.3
  • MySQL版本一般用5.x版本,这里改成5.1.47,同时还可以把<scope>runtime</scope>去掉,scope区别
  • Lombok版本用1.8.20,同时去掉<optional>true</optional>
  • spring-boot-starter-test,关于测试也可以去掉(不知道为什么)

3. 配置

application.properties

# 应用名在微服务架构中至关重要
spring.application.name=files
server.port=8989
server.servlet.context-path=/filesspring.thymeleaf.cache=false
spring.thymeleaf.suffix=.html
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.prefix=classpath:/templates/
spring.web.resources.static-locations=classpath:/templates/,classpath:/static/spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/files?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=passwordmybatis.mapper-locations=classpath:/com/zzw/mapper/*.xml
mybatis.type-aliases-package=com.zzw.entity# 日志
logging.level.root=info
logging.level.com.zzw.dao=debug
  • 记得添加一下数据库连接池druid的依赖

  • 数据库usernamepassword换成自己的

  • url里的数据库名称使用之前创建的

  • 需要创建dao,entity,mapper

  • 此时运行项目,若能正常启动,说明环境没有问题

  • 在main所在类上面添加@MapperScan("com.zzw.dao"),为了之后扫描dao所在的包

4. 三层简单开发

这个项目主要是学习文件上传和下载,所以不在前端和UI花费太多时间,一切从简。

以下的三层简单开发,代码块中实现了文件查询文件上传功能。为避免重复粘贴,文件下载在线打开文件删除功能的代码不在这一章节贴出来。

1. Entity

1. User

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)
public class User {private Integer id;private String username;private String password;
}

注意@Accessors(chain = true)这个注解可以实现set的链式编程:
Lombok插件@Accessors(chain = true)开启链式开发

2. UserFile

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)
public class UserFile {private Integer id;private String oldFileName;private String newFileName;private String ext;private String path;private String size;private String type;private String isImg;private Integer downcounts;private Date uploadTime;private Integer userId;
}

2. DAO

注意DAO都是interface,并且要记得添加@Mapper

1. UserDAO

@Mapper
public interface UserDAO {User login(User user);
}

2. UserFileDAO

public interface UserFileDAO {// 根据用户的ID获取用户的文件列表List<UserFile> findByUserId(Integer id);// 存储图片信息void save(UserFile userFile);
}

3. Mapper

1. UserDAOMapper

<?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.zzw.dao.UserDAO"><!--  login  --><select id="login" parameterType="User" resultType="User">select id, username, passwordfrom files.t_user where username = #{username} and password = #{password}</select></mapper>

注意namespace要写需要map的DAO所在位置

2. UserFileDAOMapper

<?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.zzw.dao.UserFileDAO"><!--  find user file list  --><select id="findByUserId" parameterType="Integer" resultType="UserFile">select id, oldFileName, newFileName, ext, path, size, type, isImg, downcounts, uploadTime, userIdfrom files.t_fileswhere userId=#{id}</select><!--  save image  --><insert id="save" parameterType="UserFile" useGeneratedKeys="true" keyProperty="id">insert into files.t_filesvalues (#{id}, #{oldFileName}, #{newFileName}, #{ext},#{path}, #{size}, #{type}, #{isImg},#{downcounts}, #{uploadTime}, #{userId})</insert></mapper>

4. Service

注意service有service interface和serviceImpl,impl上需要加@Service注解

1. UserService

UserService

public interface UserService {User login(User user);
}

UserServiceImpl

@Service
@Transactional
public class UserServiceImpl implements UserService {@Autowiredprivate UserDAO userDAO;@Override@Transactional(propagation = Propagation.SUPPORTS)public User login(User user) {return userDAO.login(user);}
}

这里有一个点需要注意:

userDAO在使用@Autowired来自动注入时,Intellij会提示:

而如果在DAO上添加@Repository,这个错误提示就会自动消除。尽管我们都知道 Dao 层的 Bean 实际上都是有的,并且可以设置关闭这恼人的提示,但是我们有没有想过为什么 Intellij 就找不到这个 Bean 呢?可参考以下文章:

https://blog.csdn.net/tianshan2010/article/details/105889133/

正确做法是:DAO层不需要加@Repository注解

2. UserFileService

UserFileService

public interface UserFileService {List<UserFile> findByUserId(Integer id);void save(UserFile userFile);
}

UserFileServiceImpl

@Service
@Transactional
public class UserFileServiceImpl implements UserFileService{@Autowiredprivate UserFileDAO userFileDAO;@Overridepublic List<UserFile> findByUserId(Integer id) {return userFileDAO.findByUserId(id);}@Overridepublic void save(UserFile userFile) {String isImg = userFile.getType().startsWith("image") ? "Yes" : "No";userFile.setIsImg(isImg);userFile.setDowncounts(0);userFile.setUploadTime(new Date());userFileDAO.save(userFile);}
}

5. Controller

1. UserController

@Controller
@RequestMapping("user")
public class UserController {@Autowiredprivate UserService userService;/*** 登陆方法*/@PostMapping("login")public String login(User user, HttpSession session) {User userDB = userService.login(user);if (userDB != null) {session.setAttribute("user", userDB);return "redirect:/file/showAll";} else {// 这里需要使用redirect来避免表单的重复提交return "redirect:/index";        // 注意这里不能重定向到 /login.html,而是/login }}
}

2. IndexController

对一些index的映射

@Controller
public class IndexController {@GetMapping("index")public String toLogin() {return "login"; // 这里自动由Spring来解析}
}

注意访问路径/login/login.html的区别:

/login:会被indexController解析,然后返回login.html,同时里面的thymeleaf语句也会被解析

/login.html:直接返回login.html,里面的thymeleaf语句不会被解析

3. FileController

@Controller
@RequestMapping("file")
public class FileController {@Autowiredprivate UserFileService userFileService;/*** 展示所有文件信息*/@GetMapping("showAll")public String findAll() {System.out.println("查询所有进入......");return "showAll";}}

5. 功能开发

0. 文件查询

上面简单的三层开发已经把这个业务功能实现了,这里不重点讨论。下面截图是实现文件上传后所截。

1. 文件上传

所有上传的文件都会被存在``/static/files`目录下,又根据需求,上传的文件需要根据日期放在不同的文件中。

先添加依赖,主要用来获取上传文件的后缀:

<dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.4</version>
</dependency>
// 获取原文件的后缀名
String extension = "." + FilenameUtils.getExtension(aaa.getOriginalFilename());

controller开发:

  1. 前端通过表单来上传文件,这里上传文件有大小限制

    <h3>上传文件:</h3>
    <form th:action="@{/file/upload}" method="post" enctype="multipart/form-data"><input type="file" name="aaa"> <input type="submit" value="上传文件">
    </form>
    
  2. Controller层通过MultipartFile来接受文件,注意变量名必须与前端表单中的name属性中的值一致,否则接受不到

    /*** 处理上传文件:保存文件信息到数据库* 注意参数MultipartFile的变量名必须和表单中的name属性一致,这样才可以接收到文件*/
    @PostMapping("upload")
    public String upload(MultipartFile aaa) {// 获取上传文件用户的idUser user = (User) session.getAttribute("user");// 获取原文件名String oldFilename = aaa.getOriginalFilename();// 获取原文件的后缀名String extension = "." + FilenameUtils.getExtension(aaa.getOriginalFilename());// 生成新文件名String newFileName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + UUID.randomUUID().toString() + extension;// 文件大小long size = aaa.getSize();// 文件类型String type = aaa.getContentType();// 以下属性放到业务层Service去做// 图片ID// 是否是图片// 下载次数// 上传时间// 根据日期生成目录String realPath = ResourceUtils.getURL("classpath:").getPath() + "/static/files";String dateFormat = new SimpleDateFormat("yyyy-MM-dd").format(new Date());String dateDirPath = realPath + "/" + dateFormat;File dateDir = new File(dateDirPath);if (!dateDir.exists()) {dateDir.mkdirs();    // mkdirs()可以创建多级目录}// 处理上传文件aaa.transferTo(new File(dateDirPath, newFileName));// 将文件信息放入数据库中保存// 注意,我们现在在控制层,某一些业务需要放到业务层去做UserFile userFile = new UserFile();userFile.setOldFileName(oldFilename).setNewFileName(newFileName).setExt(extension).setSize(String.valueOf(size)).setType(type).setPath("/files/" + dateFormat).setUserId(user.getId());userFileService.save(userFile);return "redirect:/file/showAll";    // 文件上传后,重定向到showAll页面
    }
    
  3. 每次上传文件后重定向到showAll,来展示用户的文件列表

  4. 其中关于文件的id、是否是图片、下载次数、上传时间放在业务层中处理,而剩余文件信息(主要是路径、文件名)放在控制层处理。关于是否是图片,其实文件的type有已经判断过是否为图片,如果为图片就会返回"image/xxx"。

    业务层:

    @Override
    public void save(UserFile userFile) {String isImg = userFile.getType().startsWith("image") ? "Yes" : "No";userFile.setIsImg(isImg);userFile.setDowncounts(0);userFile.setUploadTime(new Date()); // 注意这个Date用的是 java.util.DateuserFileDAO.save(userFile);
    }
    
  5. 此外,如果文件是图片,我们还应该直接显示图片,这里主要需要在前端作修改:

    <table border="1px"><tr><th>ID</th><th>文件原始名称</th><th>文件新名称</th><th>文件后缀</th><th>存储路径</th><th>文件大小</th><th>类型</th><th>是否是图片</th><th>下载次数</th><th>上传时间</th><th>操作</th></tr><tr th:each="file, fileStat : ${files}"><td><span th:text="${file.id}"></span></td><td><span th:text="${file.oldFileName}"></span></td><td><span th:text="${file.newFileName}"></span></td><td><span th:text="${file.ext}"></span></td><td><span th:text="${file.path}"></span></td><td><span th:text="${file.size}"></span></td><td><span th:text="${file.type}"></span></td><td><span th:if="${file.isImg} != 'Yes'" th:text="${file.isImg}"></span><img th:if="${file.isImg} == 'Yes'" style="height: 80px; width: 80px"  th:src="${#servletContext.contextPath} + ${file.path} + '/' + ${file.newFileName}" alt=""></td><td><span th:id="${file.id}" th:text="${file.downcounts}"></span></td><td><span th:text="${#dates.format(file.uploadTime, 'yyyy-MM-dd HH:mm:ss')}"></span></td><th><a th:href="@{/file/download(id=${file.id})}">下载</a><a th:href="@{/file/download(id=${file.id}, openStyle='inline')}">在线打开</a><a th:href="@{/file/delete(id=${file.id})}">删除</a></th></tr>
    </table>
    

    显示图片时,项目路径获取:

    1. 通常是在js中获取

    2. @{/}:获取项目路径,但是需要拼接/,并且会报错

    3. ${#servletContext.contextPath}:无需拼接/,这是servlet里的技术

2. 文件下载

关于下载和删除,我们在取文件时需要知道文件名,但是每个文件(每条文件信息记录)都有唯一的id绑定,所以我们可以使用id

FileController

/*** 文件下载*/
@GetMapping("download")
public void download(String id, HttpServletResponse response) throws IOException {// 根据文件id获取对应的文件信息UserFile userFile = userFileService.findById(Integer.valueOf(id));// 更新下载次数userFile.setDowncounts(userFile.getDowncounts() + 1);userFileService.update(userFile);// 获取文件路径System.out.println("File: " + userFile);String realPath = ResourceUtils.getURL("classpath:").getPath() + "/static/" + userFile.getPath();// 获取文件输入流(把磁盘上的文件通过IO加载到程序(内存)中FileInputStream fis = new FileInputStream(new File(realPath, userFile.getNewFileName()));// 附件下载// 注意:为防止中文乱码,需要使用UTF-8response.setHeader("content-disposition", "attachment;fileName=" + URLEncoder.encode(userFile.getOldFileName(), "UTF-8"));// 获取响应流(找到后需要通过Response发送回给用户ServletOutputStream os = response.getOutputStream();// 文件拷贝IOUtils.copy(fis, os);// 关闭流IOUtils.closeQuietly(fis);IOUtils.closeQuietly(os);
}
  • 下载次数:更新次数虽然+1,但是这个更新是发生在后台的,我们单次点击“下载”,只向服务器进行了一次请求,对下载次数以及文件信息并没有在前端页面进行更新(如果使用重定向,页面会产生刷新的效果,用户体验不好)。
  • 路径:数据库中保存的文件路径都是相对路径,我们需要使用绝对路径(带上项目名)和/static
  • 注意输入流和输出流的使用
  • 注意附件的使用
  • 注意编码格式和中文乱码的问题
  • 这里相应的Service层和DAO层(Mapper层和Mybatis)这里就不贴代码,相对简单。

下载次数更新

注意如果要做到即时的“下载次数更新”,需要结合WebSocket的技术。 这里采用一个稍有延迟的数据更新,即用Ajax:

  1. 引入JQuery

  2. 写一个Ajax,每3秒查询一次下载数。(个人觉得这个方案不太好:1.轮询有延迟;2.当用户数量多、文件数量多、并发高时,数据库访问压力过大)

    $(function () {// 周期函数setInterval(function(){$.get("[[@{/file/showAllJSON}]]", function (res) {$.each(res, function (index, file) {$('#'+file.id).text(file.downcounts);})})}, 3000);
    })
    
  3. 写后端代码,返回JSON格式文件列表,和findAll方法一样,但是使用@ResponseBody

    /*** 展示所有文件信息,并以JSON格式返回*/
    @GetMapping("showAllJSON")
    @ResponseBody
    public List<UserFile> findAllJSON(HttpSession session) {User user = (User)session.getAttribute("user");List<UserFile> files = userFileService.findByUserId(user.getId());return files;
    }
    

3. 在线打开

业务分析:

在线打开不会计入下载次数,并且该功能只能打开可以打开的文件,无法被在线打开的文件还是会下载。

在线打开其实和附件下载的代码几乎一样,区别就是:

附件下载(attachment)

response.setHeader("content-disposition", "attachment;fileName=" + URLEncoder.encode(userFile.getOldFileName(), "UTF-8"));

在线打开(inline)

response.setHeader("content-disposition", "inline;fileName=" + URLEncoder.encode(userFile.getOldFileName(), "UTF-8"));

所以这里其实可以代码复用

showAll.html

<th><a th:href="@{/file/download(id=${file.id})}">下载</a><a th:href="@{/file/download(id=${file.id}, openStyle='inline')}">在线打开</a><a th:href="@{/file/delete(id=${file.id})}">删除</a>
</th>

FileController

/*** 文件下载*/
@GetMapping("download")
public void download(String openStyle, String id, HttpServletResponse response) throws IOException {openStyle = openStyle == null ? "attachment" : openStyle;// 根据文件id获取对应的文件信息UserFile userFile = userFileService.findById(id);if ("attachment".equals(openStyle)) {// 更新下载次数userFile.setDowncounts(userFile.getDowncounts() + 1);userFileService.update(userFile);}// 获取文件路径System.out.println("File: " + userFile);String realPath = ResourceUtils.getURL("classpath:").getPath() + "/static/" + userFile.getPath();// 获取文件输入流(把磁盘上的文件通过IO加载到程序(内存)中FileInputStream fis = new FileInputStream(new File(realPath, userFile.getNewFileName()));// 附件下载// 注意:为防止中文乱码,需要使用UTF-8response.setHeader("content-disposition", openStyle + ";fileName=" + URLEncoder.encode(userFile.getOldFileName(), "UTF-8"));// 获取响应流(找到后需要通过Response发送回给用户ServletOutputStream os = response.getOutputStream();// 文件拷贝IOUtils.copy(fis, os);// 关闭流IOUtils.closeQuietly(fis);IOUtils.closeQuietly(os);
}

做一个openStyle字符串的判断:判断是文件下载还是在线打开,以及是否增加下载次数

4. 文件删除

文件删除主要包括:查询文件,删除文件,删除数据库中的文件信息。

/*** 文件删除,包括数据库的文件信息和文件本身* @param id 要删除文件的id* @return 重定向至用户文件列表首页*/
@GetMapping("delete")
public String delete(String id) throws FileNotFoundException {// 根据id查询信息UserFile userFile = userFileService.findById(id);// 删除文件:// 获取绝对路径String realPath = ResourceUtils.getURL("classpath:").getPath() + "/static" + userFile.getPath();// 获得文件File file = new File(realPath, userFile.getNewFileName());if (file.exists()) {file.delete();  // 立即删除}// 删除数据库中的文件信息userFileService.delete(id);return "redirect:/file/showAll";
}

6. 效果

基于Springboot和Mybatis的文件上传与下载相关推荐

  1. Struts2.3.5+Hibernate3+Spring3.1基于注解实现的多文件上传,下载

    Struts2.3.5+Hibernate3+Spring3.1基于注解实现的的多文件上传,下载,这里是上传文件到数据库中,上传控件可以增加和删除,有需要的朋友可以看看. 以下是源码下载地址:http ...

  2. Springboot中常用的文件上传和下载通用接口

    记录下通用的文件上传和下载接口,一般的开发中都是会使用到的,不过写的最简单的版本. 文章目录 程序测试 本文小结 程序测试 在yml中配置一个文件保存的路径 #保存文件的路径 common:file: ...

  3. 基于阿里云的OSS文件上传和下载

    OSS概述 OSS是基于阿里云的一个云平台文件保存的系统,我们可以将服务器的文件上传至云端从而减轻服务器的压力. 初体验 首先创建一个bucket (给你的云储存器配置名字等基本信息) 生成Asses ...

  4. Springboot实现简单的文件上传和下载功能

    一.第一步, 第一步依然是创建数据库,我简单设计了三个字段(file_id,file_name,create_time) CREATE TABLE `txtfile` (`file_id` int N ...

  5. 基于FTP协议的Excel文件上传与下载

    1.关于FTP协议 FTP(文件传输协议)是TCP/IP协议组中的协议之一,作为网络共享文件的传输协议,在网络应用软件中具有广泛的应用.FTP协议的全称为File Transfer Protocol, ...

  6. 基于华为云obs实现文件上传下载(技术栈mysql+springboot+Maven+jsp+java)的技术分享

    基于华为云obs实现文件上传下载(技术栈mysql+springboot+jsp+java)的技术分享 obs实现文件上传下载 前言 一.OBS是什么? 二.使用步骤 1.1 前期准备 2 工具的内容 ...

  7. SpringBoot整合阿里云OSS文件上传、下载、查看、删除

    SpringBoot整合阿里云OSS文件上传.下载.查看.删除 该项目源码地址:https://github.com/ggb2312/springboot-integration-examples ( ...

  8. SpringBoot实现文件上传和下载

    文件上传需要使用到 MultipartResolver接口. Spring MVC 使用 MultipartResolver接口的实现类:CommonsMultipartResolver .Commo ...

  9. 基于springboot和vue实现oss上传

    基于springboot实现oss上传 1 OSS对象存储 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量.安全.低成本.高可靠的云存储服务.OS ...

  10. SpringBoot下文件上传与下载的实现

    原文:http://blog.csdn.net/colton_null/article/details/76696674 SpringBoot后台如何实现文件上传下载? 最近做的一个项目涉及到文件上传 ...

最新文章

  1. SLAM前端 ---------特征提取之ORB(ORB与SIFT与SURF)
  2. 计算机术语表达祝福,考研祝福| | 计算机 愿你们历经千帆,终达彼岸
  3. Leet Code OJ 203. Remove Linked List Elements [Difficulty: Easy]
  4. 图片加到json中,提交到服务器端处理异常问题。
  5. 面试薪资这样谈,让你的月薪加倍!
  6. 手机型号大全_双十一高价位华为手机推荐,2020年哪款更值得入手
  7. 转贴:Google Reader:信息背后的信息,无可替代的伟大
  8. Linux命令使用练习三
  9. unity透明通道加颜色_Unity的Gamma颜色空间和Linear颜色空间的小研究
  10. ad7705c语言程序,基于51单片机的的AD7705的运用
  11. Linux - 增加用户、添加用户组
  12. 百度云直链下载-IDM+网页解析(三)
  13. python实现多人聊天论文_Python基于Socket实现简易多人聊天室的示例代码
  14. 计算机语言与语法,编程语言中语法和语义有什么区别?
  15. 细说——sqlmap
  16. 【C语言】杨辉三角(数组)
  17. linux的chmod与chown
  18. 无人机激光雷达的路径规划仿真
  19. 通过邮件收发传真的方法与步骤
  20. 互联网摸鱼日报(2022-11-22)

热门文章

  1. 个人电脑php漏洞怎么修复,PHP版 6.0 漏洞 要怎么修复
  2. 微信支付-企业付款到零钱
  3. 三种代码生成炫酷代码雨(推荐)
  4. 基数排序-LSD-golang
  5. fastDFS图片服务器的一些常见错误
  6. 摄影构图学83年绝版_学手机摄影最好要知道的81条忠告!都是大实话
  7. 【企业】奥卡姆剃刀定律,把握环境的价值
  8. 圣诞献礼 | AI、微服务、DevOps、企业架构文章合集
  9. windows 无法停止ics_多种方法解决Win10系统ICS服务启动后停止问题
  10. 软件工程师能力自我评价表