解决⽂件过⼤,上传失败的问题——⼤⽂件分割分⽚上传
⼀、背景
  平时在移动和客户端有普通的⽂件上传,但这种⽂件⼤多不⼤,从⼏k到⼏⼗兆,平时完全可以满⾜。但是对于有些终端系统(pc端、移动端),有时候存在⽂件过⼤,如拍摄的⾼清视频,导出上传不了(内存过⼩或响应时间过长)的问题,⽤户体验及不佳。这⾥上传不了的原因是前端也是需要将⽂件加载到内存后再上传。但⽂件过⼤,内存过⼩时问题就⿇烦了。针对这种情景,特提供⽂件分⽚上传的功能。不仅可以提⾼上传速率,⽽且对普通和⼤⽂件都适⽤。并且对于⽂件实现断点续传、秒传的功能。
⼆、解决思路
  ⾸先,前端制定分⽚的规则。⽐如对于⼿机移动端,当上传⽂件⼤于10M时,采⽤分⽚的⽅式上传,并切割⼀⽚就上传,不⽤等待每⽚响应的结果。
  对于前端,当前端上传⽂件时,前端边加载的时候边分割⽂件,每分割⼀⽚就上传。如前端加载完5M就直接上传,完成上传动作后释放上传的那块内存。防⽌占⽤内存的问题,减少了内存占⽤。⽽分⽚可以采⽤线程、异步的⽅式,缩短了上传的时间,也解决了⽤户等待时间过长的问题。
  对于后端,每次上传的⽂件,获取⽂件的md5值,保存md5值⾄数据库中。对于完整的⽂件md5值,作
为⽂件信息存储;对于分⽚⽂件的md5值,保存在分⽚信息中。当上传⼀个⽂件时,⾸先是根据完整的md5值查是否有上传的记录,有则说明上传的⽂件有上传的记录,若成功过直接返回url(⽂件秒传);没有成功过,但有上传记录,则有可能之前上传过部分,则需要继续上传未上传的⽂件(断电续传);没有则按照完整的流程上传。上传完成后,合并分⽚⽂件,更新并保存信息。
  但是在开发的过长中,遇到⼏个问题:
  ①:对于⽂件md5值,前端如何获取到?因为⽂件md5值是通过⽂件中的内容确定的,每个不同的⽂件md5值是不⼀样的,⽽⽂件本⾝不可能加载全量⽂件再获取的。
  ②:如何判断⽂件是否全部上传完,并是否可以进⾏合并了?
  ③:上传的某⽚⽂件若出错了,怎么让该⽚⽂件重新上传?
  ④:合并⽂件时,如何保证合并的顺序?坏账准备借贷方向
  针对上述问题,在开发的过程都⼀⼀解决了。对于
申请电动车牌照  问题①:经过斟酌,做了⼀些取舍,舍弃了⽂件秒传的精确度。采⽤⽂件的属性(如⽂件名、类型、⼤⼩等)加第⼀个分⽚的内容作为确定md5值;
  问题②:在后端的表结构中,会记录这个⽂件以及这个分⽚⽂件的状态,前端也会告诉后端分了多少个⽂件。当上传⼀个分⽚时,会更新分⽚⽂件的状态,同时分⽚⽂件上传的数量会+1;当⽂件的状态已经成功并且上传成功的数量和需要上传的数量相同时就可以进⾏合并了。
  问题③:在⽣成md5值后且在上传前,通过md5值去调⽤另外⼀个接⼝,获取上传信息,检测是否上传过。
  问题④:每个上传的分⽚⽂件名和第⼏个分⽚都会记录下来,合并⽂件的时候按照这个顺序进⾏合并。
三、功能实现
  ①实现前端并发、异步调⽤后端接⼝上传;
  ②实现秒传、断点续传功能;
  ③⽀持失败分⽚重新上传;
  ④上传过程中,可以查询上传状态、进度
  ⑤上传权限校验,通过具有时效的token上传。
  1.数据库的设计
主要创建三个表:⽂件分⽚上传信息表(t_file_fragment)、⽂件上传分⽚明细表(t_file_fragment_detail)、⽂件信息表(t_file_upload_info);各表直接的关联通过t_file_upload_info中的id进⾏关联
建表sql语句:
create table t_file_upload_info(
`id` int(11) NOT NULL AUTO_INCREMENT,
file_md5 varchar(100) not null comment '⽂件MD5',
`file_url` varchar(400) DEFAULT NULL COMMENT '⽂件存放url路径',
`file_name` varchar(100) NOT NULL COMMENT '⽂件名称',
file_type varchar(64) not null comment '⽂件类型',
`file_size` float DEFAULT NULL COMMENT '⽂件⼤⼩',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
create_id int(11) comment '创建⼈id',
PRIMARY KEY (`id`),
index idx_file_name(file_name)
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='⽂件信息表';
create table t_file_fragment(
`id` int(11) NOT NULL AUTO_INCREMENT,
`file_info_id` int(11)  DEFAULT NULL comment '外键:⽂件信息表主键',
`file_up_sum` int(6) DEFAULT NULL COMMENT '上传总的个数',
`up_status` int(2) DEFAULT0 COMMENT '上传状态:0:未完成;1:已完成;2:已存在;3:上传出错;',      `last_operator_id` int(11) DEFAULT NULL COMMENT '上传者id',
`last_operator_name` varchar(24) DEFAULT NULL COMMENT '上传者名称',
`create_time` datetime DEFAULT NULL COMMENT '上传开始时间',
`end_time` datetime DEFAULT NULL COMMENT '上传结束时间',成龙真名
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
index idx_up_status(up_status)
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='⽂件分⽚上传信息表';
create table t_file_fragment_detail(
`id` int(11) NOT NULL AUTO_INCREMENT,
`file_fragment_id` int(11) DEFAULT NULL comment '外键:⽂件分⽚上传信息id',
fragment_md5 varchar(100) not null comment '分⽚⽂件MD5',
`fragment_num` int(6) NOT NULL COMMENT '分⽚号',
`fragment_size` int(11) DEFAULT0 COMMENT '分⽚⼤⼩',
`up_status` int(2) DEFAULT0 COMMENT '上传状态:0:未完成;1:已完成;3:上传出错;',
`create_time` datetime DEFAULT NULL COMMENT '上传时间',
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
点火线圈故障`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
index idx_up_status(up_status)
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='⽂件上传分⽚明细表';
  2.时序图
  3.流程图
  3.代码实现
  总共分为三个接⼝:
  ①前端传⼊⽂件相关的参数⽣成token;
  ②获取上传信息,检测是否上传过的接⼝;
  ③⽂件(碎⽚)上传接⼝
注:仅贴出部分demo代码
  引⼊pom⽂件,主要为以下⼏个:
    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>        </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>            <scope>test</scope>
</dependency>
<dependency>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId&batis</groupId>
<artifactId>mapper</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.41</version>
</dependency>
  ⼯具类,包括jwt加密⼯具、⽂件⼯具类
public class JwtUtils {
protected static final Logger logger = Logger(Name());
private static final String SECRET = "secLang";//你⾃⼰定的字符串别让别⼈知道,加密时候⽤是对称的秘钥盐public static final String FUNCTS = "FUNCTS";//获取⽤户的功能使⽤的key
public static final String USERINFO = "USER";//获取⽤户使⽤的key
private static final long EXPIRATION = 1800L;// token的⽣命周期30分
/**
* 创建token令牌以下为参数都是⾃定义信息
* @param mark  ⼀般放的唯⼀标识
* @param functs 当前⽤户的功能集合
* @param entity 实体类对象(如当前⽤户 Users user)
* @return
*/
public static String createToken(String mark, List<Object> functs, Object entity) {
Map<String, Object> map = new HashMap<>();
//当前⽤户拥有的功能
map.put(FUNCTS, json(functs));
/
/当前⽤户信息
map.put(USERINFO, entity);
//
return Jwts.builder()
//主题主⾓是谁?赋值登录名
.setSubject(mark)
.setClaims(map)
//设置发布时间,也是⽣成时间
.setIssuedAt(new Date())
//设置过期时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
/
/设置HS256加密,并且把你的盐放⾥,这⾥推荐使⽤SH256证书加密
.signWith(SignatureAlgorithm.HS256, SECRET)
//创建完成
pact();
}
/**
* token是否过期
* @param token
* @return过期返回true 否则为false
*/
public static boolean isExpiration(String token) {
try {
return getTokenBody(token).getExpiration().before(new Date());
} catch (Exception e) {
return true;
}
}
// 获取主⾓,登录名
public static String getMark(String token) {
return getTokenBody(token).getSubject();
}
// 获取token中存储的功能
public static List<Object> getFuncts(String token) {
String str = getTokenBody(token).get(FUNCTS).toString();
List<Object> list = JSON.parseArray(str);
return list;
}
// 获取token存储的⽤户
public static Object getEntity(String token) {
return getTokenBody(token).get(USERINFO);
全智贤个人资料}
// 公共获取⾃定义数据
public static Claims getTokenBody(String token) {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
}
// 刷新token
public static String refreshToken(String token) {
if (isExpiration(token)) {
logger.info("token刷新失败!!过期了!!");
}
// 获取实体权限信息
String functs = getTokenBody(token).get(FUNCTS).toString();
String entityStr = getTokenBody(token).get(USERINFO).toString();
String mark = getTokenBody(token).getSubject();
Map<String, Object> map = new HashMap<>();
map.put(FUNCTS, functs);
map.put(USERINFO, entityStr);
token = Jwts.builder().signWith(SignatureAlgorithm.HS256, SECRET).setClaims(map).setSubject(mark)
.setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))                pact();
return token;
}
}
  (1)controller层
@Controller
@RequestMapping(value = "/bigfile")
public class BigFileController {
private final static Logger logger = Logger(BigFileController.class);
@Autowired
private BigFileService bigFileSerivce;
/**
* 获取token,并将token保存在缓存中
* @param requestObj
* @return
*/
@GetMapping(value = "/gettoken")
public Object getUploadFileToken(FileUpInfoRequest requestObj) {
BaseResonse response = new BaseResonse();
try {
//保存⽂件信息并获取token
String token = Token(requestObj);
JSONObject obj = new JSONObject();
obj.put("token", token);
response.setData(obj);
} catch (Exception e) {
<("Exception: " + e.getMessage());
//            response.setError(MainErrorType.BUSINESS_LOGIC_ERROR, Constants.EXCEPTION_DEFAULT);        }
return response;
}
/**
* 检验整个是否上传过以及上传分⽚列表 Authentication
* @param request盖玥希个人资料简介
* @return
*/
@PostMapping(value = "/checkAndListFragmentDetail")
public Object checkAndListFragmentDetail(@RequestBody FileUpInfoRequest request) {
BaseResonse response = new BaseResonse();
Map<String, Object> resultMap = null;
try {
resultMap = bigFileSerivce.checkAndListFragmentDetail(request);
response.setData(resultMap);
} catch (ServiceException e) {
e.printStackTrace();
response.Code(), e.getMessage());
}
return response;
}
/**
* ⽂件(碎⽚)上传
* @param request
* @return
*/
@PostMapping(value = "/uploadFile")
public Object uploadFile(@RequestBody FileUpInfoRequest request, @Param("file") MultipartFile file) {
BaseResonse response = new BaseResonse();
try {
//校验参数
checkValidateParams(request);
//            byte[] content = Bytes();
UploadFileResponse uploadFileRes = bigFileSerivce.uploadFile(request, file);
response.setData(uploadFileRes);
} catch (ServiceException e) {
e.printStackTrace();
response.Code(), e.getMessage());
} catch (IOException e) {
e.printStackTrace();
}
return response;
}
/**