基于微服务的云相册项目,深入剖析原理

cn.dev33
sa-token-spring-aop
1.34.0

cn.dev33 sa-token-dao-redis-jackson 1.34.0

在application.yml编写配置

sa-token:

token 名称(同时也是 cookie 名称)

token-name: satoken

token 有效期(单位:秒) 默认30天,-1 代表永久有效

timeout: 2592000

token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结

active-timeout: -1

是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)

is-concurrent: true

在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)

is-share: true

token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)

token-style: uuid

是否输出操作日志

is-log: true

spring:
redis:
host: 127.0.0.1
port: 6379

编写登录方法,调用Sa-Token提供的StpUtil中的方法,详细可以参考官方文档

private ResultBody doLogin(UserBo bo, String phone) {
//没有该用户就把信息新增入数据库,有该用户就把修改登录次数
ResultBody loginResult = userFeignService.login(bo);
if (loginResult.getCode().equals(CommonEnum.SUCCESS.getResultCode())) {
//登陆成功 - 后台也执行登录
StpUtil.login(phone);
if (StpUtil.isLogin()) {
//后台也登录成功了就返回token到前端
//放入token请求头,方便访问后续
String tokenValue = StpUtil.getTokenInfo().getTokenValue();
return ResultBody.success(tokenValue);
}
} else {
return loginResult;
}
//登陆失败 - 直接返回登录失败信息
return loginResult;
}

在需要登录后才能访问的接口上加入@SaCheckLogin注解

@SaCheckLogin
@GetMapping(“/getUserInfo”)
public ResultBody getUserInfo() {
return loginService.getUserInfo();
}

退出登录时要进行清理缓存的操作

public ResultBody logout() {
if (StpUtil.isLogin()) {
//用户登录ID
String loginId = StpUtil.getLoginIdAsString();
//session
SaSession session = StpUtil.getSessionByLoginId(loginId, true);
//清空session
session.logout();
//退出登录
StpUtil.logout();
}
return ResultBody.success();
}

(2)Nacos服务注册

在父工程导入依赖

com.alibaba.nacos nacos-client 2.2.0 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config

所有微服务都编写配置类,然后启动所有微服务

spring:
cloud:

nacos config

nacos:
discovery:
username: nacos
password: nacos
server-addr: 127.0.0.1:8848

下图是本项目所有微服务注册到nacos截图

(3)openfeign微服务之间调用

当我们在某个微服务中如果需要调用另外一个微服务里面的功能,这个时候要怎么办呢?SpringCloud给我们提供了一个框架openfeign,它可以帮助我们解决这个问题。

使用步骤

在父工程导入依赖

org.springframework.cloud spring-cloud-starter-openfeign 2.2.10.RELEASE

在启动类添加@EnableFeignClients注解,开启openfeign并扫描feign文件

package com.cloud.photo.api;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {“com.cloud.photo”})
@MapperScan(basePackages = {“com.cloud.photo.api.mapper”})
@ComponentScan({“com.cloud.photo”})
public class CloudPhotoApiApplication {
public static void main(String[] args) {
SpringApplication.run(CloudPhotoApiApplication.class, args);
}
}

编写feign接口,@FeignClient后面添加要调用的微服务name,然后直接将所需用到的接口复制到下方即可,记住需要补全路径。

package com.cloud.photo.api.feign;

import com.cloud.photo.common.bo.UserBo;
import com.cloud.photo.common.common.ResultBody;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(“cloud-photo-user”)
public interface UserFeignService {

/**
*

  • @param phone 手机号
  • @return 用户信息
    */
    @GetMapping(“/user/getUserInfo”)
    ResultBody getUserInfo(@RequestParam(value = “phone”) String phone);

}

在需要用到该接口时注入feign服务,调用即可

package com.cloud.photo.api.service.impl;

import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.cloud.photo.api.feign.UserFeignService;
import com.cloud.photo.api.service.LoginService;
import com.cloud.photo.common.bo.UserBo;
import com.cloud.photo.common.common.CommonEnum;
import com.cloud.photo.common.common.ResultBody;
import com.cloud.photo.common.constant.CommonConstant;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.regex.Pattern;

@Service
public class LoginServiceImpl implements LoginService {

@Resource
private UserFeignService userFeignService;

@Override
//获得用户信息,先从缓存获取,没有则创建缓存
public ResultBody getUserInfo() {
if (StpUtil.isLogin()) {
//用户登录ID
String loginId = StpUtil.getLoginIdAsString();
//如果session里面有信息优先从里面拿
SaSession session = StpUtil.getSessionByLoginId(loginId);
if (session != null) {
String userInfo = session.getModel(CommonConstant.USER_INFO, String.class);
if (userInfo != null) {
return ResultBody.success(JSON.parseObject(userInfo, UserBo.class));
}
}
//否则访问user服务获取用户信息
ResultBody userInfo = userFeignService.getUserInfo(loginId);
if (userInfo.getCode().equals(CommonEnum.SUCCESS.getResultCode())){
//成功拿到用户信息就存缓存里面,true是有的话就存,没有的话就新建
SaSession saSession = StpUtil.getSessionByLoginId(loginId, true);
saSession.set(CommonConstant.USER_INFO, JSON.toJSONString(userInfo.getData()));
}
return userInfo;
}
//未登录
return ResultBody.error(CommonEnum.NO_LOGIN);
}

}

(4)图片上传

此部分未项目重点,请做好笔记!!以下我将介绍整个图片上传流程

使用步骤

在父工程导入依赖,部分依赖上面已经导入,这里仅展示未导入的

org.springframework.kafka spring-kafka cn.hutool hutool-all 4.1.1 com.alibaba fastjson 1.2.51 com.amazonaws aws-java-sdk-core 1.12.487 com.amazonaws aws-java-sdk-s3 1.12.487

图片上传流程分为两步:获取上传地址和提交上传记录

获取上传地址

1.这是获取上传地址接口,这里把用户id放入FileUploadBo就进入putuploadService业务层代码。前端传过来FileUploadBo 包括category图片分类,fileName文件名,fileSize图片大小,fileMd5图片的md5值。

@Autowired
PutuploadService putuploadService;

/**

  • 获取上传地址
  • @return 每一张图片一个上传地址
    */
    @SaCheckLogin
    @PostMapping(“getPutUploadUrl”)
    public ResultBody getPutUploadUrl(@RequestBody FileUploadBo bo) {

//从缓存拿到用户信息
UserBo userInfo = MyUtils.getUserInfo();
if (userInfo == null) {
return ResultBody.error(CommonEnum.USER_IS_NULL);
}
//用户ID
String userId = userInfo.getUserId();
//判断下上传的文件是否为空
if (bo == null) {
return ResultBody.error(CommonEnum.FILE_LIST_IS_NULL);
}
//拼接用户ID
bo.setUserId(userId);

// 业务实现
return putuploadService.getPutUploadUrl(bo);
}

2.我们先看trans服务里的代码,因为putuploadService需要远程调用它。首先根据图片的md5值来查询是否有相同的图片已经上传过,这样的话就可以节省资源。如果图片已存在则称为秒传。如果图片存在的话我们直接返回storageObjectId(图片的存储id,这是数据库的主键)即可,因为图片已经上传过了,已经不需要拿他的上传地址了。如果图片不存在则需要进行图片上传,调用S3Util来进行上传,然后返回S3Util生成的返回值,重点是图片上传地址。

***注意,秒传和非秒传返回值是不同的***

@Autowired
IFileMd5Service iFileService;

public String getPutUploadUrl(String fileName, String fileMd5, Long fileSize) {
//文件已存在 进行秒传 直接将存储id返回
FileMd5 fileMd5Entity = iFileService.getOne(new QueryWrapper().eq(“md5”, fileMd5));
if (fileMd5Entity != null) {
JSONObject jsonObject = new JSONObject();
jsonObject.put(“storageObjectId”, fileMd5Entity.getStorageObjectId());
return jsonObject.toJSONString();
}

//文件不存在
String suffixName = “”;
if (StringUtils.isNotBlank(fileName)) {//检查fileName为不为空
suffixName = fileName.substring(fileName.lastIndexOf(“.”) + 1, fileName.length());
}
return S3Util.getPutUploadUrl(suffixName, fileMd5);//上传图片,返回上传信息
}

3.然后我们看看S3Util的代码。主要就是把图片上传到Minio,也可以选择服务商的桶服务存储,原理是差不多的,我选择的是Minio。原理就是先建立连接然后将文件的md5转换为base64上传到Minio存储,然后返回上传信息。这里的代码会用即可。

package com.cloud.photo.common.util;

import cn.hutool.core.codec.Base64;
import com.alibaba.fastjson.JSONObject;
import com.amazonaws.HttpMethod;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.*;
import io.micrometer.core.instrument.util.StringUtils;
import org.apache.tomcat.util.buf.HexUtils;

import java.io.;
import java.net.URL;
import java.net.URLEncoder;
import java.util.
;

public class S3Util {

private static String accessKey = “minioadmin”;
private static String secretKey = “minioadmin”;
private static String bucketName = “cloud-photo”;
private static String serviceEndpoint = “http://127.0.0.1:9000”;

private static String containerId = “10001”;

/**

  • 获取上传地址
  • @param suffixName
  • @param fileMd5
  • @return
    */
    public static String getPutUploadUrl(String suffixName,String fileMd5) {

//链接过期时间
Date expiration = new Date();
long expTimeMillis = expiration.getTime();
expTimeMillis += 1000 * 60 * 10;
expiration.setTime(expTimeMillis);
String objectId = UUID.randomUUID().toString().replaceAll(“-”,“”);
String base64Md5 = “”;
if(StringUtils.isNotBlank(suffixName)){
objectId = objectId +“.” + suffixName;
}
//建立S3客户端,获取上传地址
AmazonS3 s3Client = getAmazonS3Client();
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucketName, objectId)
.withMethod(HttpMethod.PUT)
.withExpiration(expiration);
if(StringUtils.isNotBlank(fileMd5)){
base64Md5= Base64.encode(HexUtils.fromHexString(fileMd5));
generatePresignedUrlRequest = generatePresignedUrlRequest.withContentMd5(base64Md5);
}
URL url = s3Client.generatePresignedUrl(generatePresignedUrlRequest);

JSONObject jsonObject =new JSONObject();
jsonObject.put(“objectId”,objectId);
jsonObject.put(“url”,url);
jsonObject.put(“containerId”,containerId);
jsonObject.put(“base64Md5”,base64Md5);
return jsonObject.toJSONString();
}
}

4.这是第1中调用的putuploadService,首先需要通过Feign远程调用trans服务,2.3中已经介绍了,返回值可能有两个信息storageObjectId(秒传)和图片上传信息(非秒传)。然后根据是否为秒传设置fileUploadBo的属性, fileUploadBo的传输状态设置为传送中,然后存入redis中,后续可以查看传输进度。

@Autowired
private CloudPhotoTransService cloudPhotoTransService;

@Resource
private StringRedisTemplate stringRedisTemplate;

@Override
public ResultBody getPutUploadUrl(FileUploadBo fileUploadBo) {

ResultBody resultBody = cloudPhotoTransService.getPutUploadUrl(fileUploadBo.getUserId(),
fileUploadBo.getFileSize(),
fileUploadBo.getFileMd5(),
fileUploadBo.getFileName());

//存进缓存的Key
String key = fileUploadBo.getUserId() + “:” + fileUploadBo.getFileMd5();

if (resultBody.getCode().equals(CommonEnum.SUCCESS.getResultCode())) {
//成功
JSONObject obj = JSONUtil.parseObj(resultBody.getData());
if (obj.containsKey(“storageObjectId”)) {
//是秒传
fileUploadBo.setStorageObjectId(obj.getStr(“storageObjectId”));
} else {
//不是秒传
fileUploadBo.setContainerId(obj.getStr(“containerId”));
fileUploadBo.setObjectId(obj.getStr(“objectId”));
fileUploadBo.setUploadUrl(obj.getStr(“url”));
fileUploadBo.setBase64Md5(obj.getStr(“base64Md5”));
}

//成功了状态是传输中
fileUploadBo.setStatus(CommonConstant.FILE_UPLOAD_ING);
fileUploadBo.setUploadTime(DateUtil.now());
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(fileUploadBo), 1, TimeUnit.DAYS);
} else {
//失败了状态是传输失败
fileUploadBo.setStatus(CommonConstant.FILE_UPLOAD_FAIL);
fileUploadBo.setUploadTime(DateUtil.now());
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(fileUploadBo), 1, TimeUnit.DAYS);
}

return ResultBody.success(fileUploadBo);
}

获取上传地址这部分流程就结束了

提交上传记录

因为上一步我们只是将图片上传了,但相应记录还并未在数据库进行保存,所以这部分就是来解决这个问题的。

1.这是提交记录的接口,接受上一步中的FileUploadBo ,调用业务层commitUpload代码

/**

  • 提交
  • @return 结果
    */
    @SaCheckLogin
    @PostMapping(“commitUpload”)
    //将图片入数据库
    public ResultBody commitUpload(@RequestBody FileUploadBo bo) {
    //从缓存拿到用户信息
    UserBo userInfo = MyUtils.getUserInfo();
    if (userInfo == null) {
    return ResultBody.error(CommonEnum.USER_IS_NULL);
    }
    //用户ID
    String userId = userInfo.getUserId();
    //判断下上传的文件是否为空
    if (ObjectUtil.isEmpty(bo)) {
    return ResultBody.error(CommonEnum.FILE_LIST_IS_NULL);
    }
    //拼接用户ID
    bo.setUserId(userId);

// 业务实现
return putuploadService.commitUpload(bo);
}

2.这是commitUpload代码,远程调用trans服务commit接口,然后将传输状态设置一下返回。接下来重点看trans服务commit接口实现了什么。

@Override
public ResultBody commitUpload(FileUploadBo bo) {

//存进缓存的Key
String key = bo.getUserId() + “:” + bo.getFileMd5();

ResultBody commitResultBody = cloudPhotoTransService.commit(bo);

if (commitResultBody.getCode().equals(CommonEnum.SUCCESS.getResultCode())) {
//成功提交 - 状态更新为传输成功
bo.setStatus(CommonConstant.FILE_UPLOAD_SUCCESS);
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(bo), 1, TimeUnit.DAYS);
} else {
//提及失败 - 状态更新为传输失败
bo.setStatus(CommonConstant.FILE_UPLOAD_FAIL);
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(bo), 1, TimeUnit.DAYS);
}

return commitResultBody;
}

3.这是trans服务commit接口,判断是否为秒传来决定调用那个方法,因为上面介绍过了秒传的返回值FileUploadBo是有StorageObjectId的,非秒传是没有的,所以根据这点很容易判断出是否为秒传。

@RequestMapping(“/commit”)
public ResultBody commit(HttpServletRequest request, HttpServletResponse response,
@RequestBody FileUploadBo bo) {
//打印请求日志
String requestId = RequestUtil.getRequestId(request);
RequestUtil.printQequestInfo(request);

//返回值
CommonEnum result;

//判断文件是否秒传,如果有StorageObjectId则说明是秒传
if (StringUtils.isBlank(bo.getStorageObjectId())) {
//处理非秒传
result = putuploadService.commit(bo);
} else {
result = putuploadService.commitTransSecond(bo);
}

log.info(“getPutUploadUrl() userId =” + bo.getUserId() + “,result=” + result);
if (StringUtils.equals(result.getResultMsg(), CommonEnum.SUCCESS.getResultMsg())) {
return ResultBody.success(CommonEnum.SUCCESS.getResultMsg(), requestId);
} else {
return ResultBody.error(result.getResultCode(), result.getResultMsg(), requestId);
}

}

4.先看是非秒传调用的commit方法,这里的代码并不复杂,就是将各种信息存储到数据库里面。

/**

  • 处理非妙传的
  • @param bo
  • @return
    */
    public CommonEnum commit(FileUploadBo bo) {
    //获取文件资源池储存信息
    S3ObjectSummary s3ObjectSummary = S3Util.getObjectInfo(bo.getObjectId());//非秒传的必须有ObjectId

//文件未上传
if (s3ObjectSummary == null) {
return CommonEnum.FILE_NOT_UPLOADED;
}

//文件上传错误
if (!bo.getFileSize().equals(s3ObjectSummary.getSize()) || !StringUtils.equalsIgnoreCase(s3ObjectSummary.getETag(), bo.getFileMd5())) {
return CommonEnum.FILE_UPLOADED_ERROR;
}

//文件上传成功 - 文件存储信息入库
StorageObject storageObject = new StorageObject(“minio”, bo.getContainerId(), bo.getObjectId(), bo.getFileMd5(), bo.getFileSize());
iStorageObjectService.save(storageObject);

//文件md5入库,秒传用
FileMd5 fileMd5 = new FileMd5(bo.getFileMd5(), bo.getFileSize(), storageObject.getStorageObjectId());
iFileMd5Service.save(fileMd5);

//文件入库 - 用户文件列表 - 发送到审核、图片 kafka列表
bo.setStorageObjectId(storageObject.getStorageObjectId());
iUserFileService.saveAndFileDeal(bo);

return CommonEnum.SUCCESS;
}

5.再看commitTransSecond代码,这里只需要保存一到一个表里即可,因为秒传部分数据已经会存在数据库里面了。

/**

  • 处理妙传的
  • @param bo
  • @return
    */
    public CommonEnum commitTransSecond(FileUploadBo bo) {

//检验储存的ID是否正确
StorageObject storageObject = iStorageObjectService.getById(bo.getStorageObjectId());//秒传的必须有StorageObjectId

if (storageObject == null) {
return CommonEnum.FILE_UPLOADED_ERROR;
}

//检查秒传文件大小
if (!storageObject.getObjectSize().equals(bo.getFileSize()) || !StringUtils.equalsIgnoreCase(bo.getFileMd5(), storageObject.getMd5())) {
return CommonEnum.FILE_UPLOADED_ERROR;
}

//保存文件入库 - 用户文件列表 - 发送到审核、图片 kafka列表
boolean result = iUserFileService.saveAndFileDeal(bo);

if (result) {
return CommonEnum.SUCCESS;
} else {
return CommonEnum.FILE_UPLOADED_ERROR;
}
}

6.秒传和秒传都调用了saveAndFileDeal方法,我们看一下它的代码。除了将数据保存到数据库,这里还通过kafka发送了两条消息,这里下面将进行介绍。

@Override
public boolean saveAndFileDeal(FileUploadBo bo) {
UserFile userFile = new UserFile();
userFile.setUserId(bo.getUserId());
userFile.setFileStatus(CommonConstant.FILE_STATUS_NORMA);//文件正常
userFile.setCreateTime(LocalDateTime.now());
userFile.setFileName(bo.getFileName());
userFile.setIsFolder(CommonConstant.FILE_IS_FOLDER_NO);
userFile.setAuditStatus(CommonConstant.FILE_AUDIT_ACCESS);
userFile.setFileSize(bo.getFileSize());
userFile.setModifyTime(LocalDateTime.now());
userFile.setStorageObjectId(bo.getStorageObjectId());
userFile.setCategory(bo.getCategory());
Boolean result = this.save(userFile);

//文件审核处理
kafkaTemplate.send(CommonConstant.FILE_AUDIT_TOPIC, JSONObject.toJSONString(userFile));

//图片处理-格式分析
kafkaTemplate.send(CommonConstant.FILE_IMAGE_TOPIC, JSONObject.toJSONString(userFile));

return result;
}

(5)消息监听

上面最后通过kafka向两个topic(FILE_AUDIT_TOPIC和FILE_ IMAGE_TOPIC)各发送了一条消息,这里就介绍这里的流程。

FILE_AUDIT_TOPIC

这部分是有关文件审核处理的

AuditConsumer 用于消费file_audit_topic里面的消息,上面往file_audit_topic发送消息后,这个类监听到后获取图片的md5值,根据图片md5查询审核表之前是否有审核记录,没有审核记录的话就添加进审核列表,有审核记录并且之前已经通过的不需要再次审核,有审核记录但之前未通过的=就直接设置审核失败。

package com.cloud.photo.audit.consumer;

import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.cloud.photo.audit.entity.FileAudit;
import com.cloud.photo.audit.service.IFileAuditService;
import com.cloud.photo.common.bo.StorageObjectBo;
import com.cloud.photo.common.bo.UserFileBo;
import com.cloud.photo.common.constant.CommonConstant;
import com.cloud.photo.common.feign.CloudPhotoTransService;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Component
public class AuditConsumer {

@Autowired
IFileAuditService iFileAuditService;
@Autowired
CloudPhotoTransService cloudPhotoTransService;

// 消费监听
@KafkaListener(topics = {“file_audit_topic”})

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数大数据工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上大数据开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注大数据获取)
img

AuditService;
@Autowired
CloudPhotoTransService cloudPhotoTransService;

// 消费监听
@KafkaListener(topics = {“file_audit_topic”})

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数大数据工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
[外链图片转存中…(img-VIhO1I08-1712573455056)]
[外链图片转存中…(img-AbEohNHz-1712573455057)]
[外链图片转存中…(img-FHuAnZp8-1712573455057)]
[外链图片转存中…(img-5eRaX6Pk-1712573455057)]
[外链图片转存中…(img-xQQ77T2P-1712573455058)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上大数据开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注大数据获取)
[外链图片转存中…(img-8Sa2Rq49-1712573455058)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值