当前位置: 首页 > wzjs >正文

织梦统计网站访问量代码做网站需要学习多久

织梦统计网站访问量代码,做网站需要学习多久,城乡住房和城乡建设部网站,张掖高端网站建设公司文章目录 Softhub软件下载站实战开发(十):实现图片视频上传下载接口 🖼️🎥系统架构图核心功能设计 🛠️1. 文件上传流程2. 关键技术实现2.1 雪花算法2.2 文件校验机制 ✅2.3 文件去重机制 🔍2.…

文章目录

  • Softhub软件下载站实战开发(十):实现图片视频上传下载接口 🖼️🎥
    • 系统架构图
    • 核心功能设计 🛠️
      • 1. 文件上传流程
      • 2. 关键技术实现
        • 2.1 雪花算法
        • 2.2 文件校验机制 ✅
        • 2.3 文件去重机制 🔍
        • 2.4 视频封面提取 🎞️
        • 2.5 文件存储策略 📂
        • 2.6 视频上传示例
      • 3. 文件查看实现 ⬇️

Softhub软件下载站实战开发(十):实现图片视频上传下载接口 🖼️🎥

在上一篇文章中,我们实现了软件配置面板,实现了ai配置信息的存储,为后续富文本编辑器的ai功能提供了基础,本文致力于解决在富文本编辑器中图片和视频的上传查看功能。

系统架构图

上传文件
下载文件
读取
客户端
API接口
文件处理层
存储服务
MinIO存储
数据库
MySQL

核心功能设计 🛠️

1. 文件上传流程

客户端 服务端 MinIO 数据库 上传文件请求 验证文件类型和大小 计算文件MD5 检查文件是否已存在 返回已存在记录 直接返回文件URL 上传文件到MinIO 返回成功 保存文件元信息 返回成功 返回文件URL alt [文件已存在] [文件不存在] 客户端 服务端 MinIO 数据库

2. 关键技术实现

2.1 雪花算法

关键数据不能采取自增id方案,采用md5也会有碰撞和页分裂的问题,这里采用雪花算法来解决这一问题

安装

go get -u "github.com/bwmarrin/snowflake"

初始化

var node *snowflake.Nodefunc init() {var err errornode, err = snowflake.NewNode(1)
}

使用

id := node.Generate().Int64()
2.2 文件校验机制 ✅
// 检查文件类型
fileType := strings.ToLower(filepath.Ext(req.File.Filename))
allowedTypes := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
isAllowed := false
for _, t := range allowedTypes {if t == fileType {isAllowed = truebreak}
}
if !isAllowed {return fmt.Errorf("不支持的文件类型:%s", fileType)
}// 检查文件大小
if req.File.Size > 10*1024*1024 { // 10MBreturn fmt.Errorf("文件大小不能超过10MB")
}
2.3 文件去重机制 🔍

通过计算文件MD5值实现文件去重:

// 计算文件MD5
fileBytes, _ := io.ReadAll(file)
md5 := gmd5.MustEncryptBytes(fileBytes)// 检查是否已存在
var existFile *model.DsImageInfo
err = dao.DsImage.Ctx(ctx).Where(dao.DsImage.Columns().Md5, md5).Scan(&existFile)
if existFile != nil {// 直接返回已有文件信息return existFile, nil
}
2.4 视频封面提取 🎞️

需要ffmpeg添加到环境变量中

使用FFmpeg提取视频首帧作为封面:

cmd := exec.Command("ffmpeg","-y",                 // 覆盖输出文件"-loglevel", "error", // 只输出错误信息"-i", tempVideoPath,  // 输入文件"-vframes", "1",      // 只提取一帧"-an",                // 不处理音频"-vf", "scale='-1:min(720,ih)'", // 限制最大高度为720"-c:v", "mjpeg",      // 使用mjpeg编码器"-f", "image2",       // 输出格式"-q:v", "2",          // 高质量输出tempFramePath)        // 输出文件
2.5 文件存储策略 📂

采用分层目录结构存储文件:

pic/2024/05/07/abc123def456.pic
video/2024/05/07/xyz789uvw012.video

代码实现:

now := gtime.Now()
year := now.Year()
month := int(now.Month())
day := now.Day()
objectName := fmt.Sprintf("pic/%d/%02d/%02d/%s.pic", year, month, day, md5)
2.6 视频上传示例
func (s *sDsIUpload) VideoUpload(ctx context.Context, req *api.DsVideoUploadReq) (res *api.DsVideoUploadRes, err error) {res = &api.DsVideoUploadRes{}err = g.Try(ctx, func(ctx context.Context) {// 检查文件类型fileType := strings.ToLower(filepath.Ext(req.File.Filename))allowedTypes := []string{".mp4", ".avi", ".mov", ".mkv"}isAllowed := falsefor _, t := range allowedTypes {if t == fileType {isAllowed = truebreak}}if !isAllowed {liberr.ErrIsNil(ctx, fmt.Errorf("不支持的文件类型:%s", fileType))}// 检查文件大小(如限制20MB)if req.File.Size > 20*1024*1024 {liberr.ErrIsNil(ctx, fmt.Errorf("文件大小不能超过20MB"))}// 计算MD5file, err := req.File.Open()liberr.ErrIsNil(ctx, err, "打开文件失败")defer file.Close()fileBytes, err := io.ReadAll(file)liberr.ErrIsNil(ctx, err, "读取文件失败")md5 := gmd5.MustEncryptBytes(fileBytes)// 检查是否已存在var existVideo *model.DsVideoInfoerr = dao.DsVideo.Ctx(ctx).Where(dao.DsVideo.Columns().Md5, md5).Scan(&existVideo)liberr.ErrIsNil(ctx, err, "查询视频信息失败")if existVideo != nil {res.Id = existVideo.Idres.Url = fmt.Sprintf("/api/v1/admin/ds/dsVideo/view?id=%d", existVideo.Id)// 获取首帧图片URLimageInfo, err := s.GetImageInfo(ctx, &api.DsImageInfoReq{Id: existVideo.PosterId})if err == nil && imageInfo != nil {res.Poster = fmt.Sprintf("/api/v1/admin/ds/dsImage/view?id=%d", imageInfo.Id)}return}// 创建临时目录tempDir := filepath.Join(os.TempDir(), "upload", md5)if _, err := os.Stat(tempDir); os.IsNotExist(err) {err = os.MkdirAll(tempDir, 0755)liberr.ErrIsNil(ctx, err, "创建临时目录失败")}// 生成临时文件路径tempVideoPath := filepath.Join(tempDir, fmt.Sprintf("video%s", fileType))tempFramePath := filepath.Join(tempDir, "frame.jpg")g.Log().Debugf(ctx, "临时视频文件路径: %s", tempVideoPath)g.Log().Debugf(ctx, "临时帧图片路径: %s", tempFramePath)// 保存视频到临时文件file.Seek(0, 0)tempFile, err := os.OpenFile(tempVideoPath, os.O_WRONLY|os.O_CREATE, 0644)liberr.ErrIsNil(ctx, err, "创建临时文件失败")_, err = io.Copy(tempFile, file)tempFile.Close()liberr.ErrIsNil(ctx, err, "保存临时文件失败")// 确保临时文件存在且可读if _, err := os.Stat(tempVideoPath); err != nil {liberr.ErrIsNil(ctx, fmt.Errorf("临时视频文件不存在或无法访问: %v", err))}// 使用ffmpeg提取首帧cmd := exec.Command("ffmpeg","-y",                 // 覆盖输出文件"-loglevel", "error", // 只输出错误信息"-i", tempVideoPath, // 输入文件"-vframes", "1", // 只提取一帧"-an",                           // 不处理音频"-vf", "scale='-1:min(720,ih)'", // 限制最大高度为720,保持宽高比"-c:v", "mjpeg", // 使用 mjpeg 编码器"-f", "image2", // 输出格式"-q:v", "2", // 高质量输出tempFramePath) // 输出文件output, err := cmd.CombinedOutput()if err != nil {// 清理临时文件os.RemoveAll(tempDir)liberr.ErrIsNil(ctx, fmt.Errorf("提取视频首帧失败: %v, 输出: %s", err, string(output)))}// 获取MinIO客户端drive := storage.MinioDrive{}client, err := drive.GetClient()liberr.ErrIsNil(ctx, err, "获取MinIO客户端失败")// 生成存储路径now := gtime.Now()year := now.Year()month := int(now.Month())day := now.Day()frameObjectName := fmt.Sprintf("pic/%d/%02d/%02d/%s.jpg", year, month, day, md5)// 读取首帧图片frameFile, err := os.Open(tempFramePath)liberr.ErrIsNil(ctx, err, "打开首帧图片失败")defer frameFile.Close()// 获取首帧图片信息frameInfo, err := frameFile.Stat()liberr.ErrIsNil(ctx, err, "获取首帧图片信息失败")// 检查是否已存在相同MD5的图片var existingImage *model.DsImageInfoerr = dao.DsImage.Ctx(ctx).Where(dao.DsImage.Columns().Md5, md5).Scan(&existingImage)liberr.ErrIsNil(ctx, err, "查询图片信息失败")var imageId int64if existingImage != nil {// 使用已存在的图片记录imageId = existingImage.Id} else {// 获取图片尺寸frameFile.Seek(0, 0)img, _, err := image.DecodeConfig(frameFile)if err != nil {g.Log().Warningf(ctx, "获取图片尺寸失败: %v", err)}// 重新定位到文件开始位置用于上传frameFile.Seek(0, 0)// 上传首帧图片到MinIO_, err = client.PutObject(ctx, config.MINIO_BUCKET, frameObjectName, frameFile, frameInfo.Size(), minio.PutObjectOptions{ContentType: "image/jpeg",})liberr.ErrIsNil(ctx, err, "上传首帧图片失败")// 保存首帧图片信息imageInfo := &model.DsImageInfo{Id:        node.Generate().Int64(),Md5:       md5,Name:      fmt.Sprintf("%s_frame.jpg", req.File.Filename),Path:      frameObjectName,Size:      frameInfo.Size(),MimeType:  "image/jpeg",Width:     img.Width,Height:    img.Height,CreatedBy: 0,CreatedAt: gtime.Now(),UpdatedBy: 0,UpdatedAt: gtime.Now(),}// 保存首帧图片信息到数据库_, err = dao.DsImage.Ctx(ctx).Insert(imageInfo)liberr.ErrIsNil(ctx, err, "保存首帧图片信息失败")imageId = imageInfo.Id}// 获取视频元数据cmd = exec.Command("ffprobe","-v", "quiet","-print_format", "json","-show_format","-show_streams",tempVideoPath)output, err = cmd.Output()liberr.ErrIsNil(ctx, err, "获取视频信息失败")var probeData struct {Streams []struct {Width    int    `json:"width"`Height   int    `json:"height"`Duration string `json:"duration"`} `json:"streams"`}err = json.Unmarshal(output, &probeData)liberr.ErrIsNil(ctx, err, "解析视频信息失败")width := 0height := 0duration := 0if len(probeData.Streams) > 0 {width = probeData.Streams[0].Widthheight = probeData.Streams[0].Heightif d, err := strconv.ParseFloat(probeData.Streams[0].Duration, 64); err == nil {duration = int(d)}}// 保存视频文件到MinIOvideoObjectName := fmt.Sprintf("video/%d/%02d/%02d/%s.video", year, month, day, md5)file.Seek(0, 0)err = drive.UploadWithPath(ctx, req.File, videoObjectName)liberr.ErrIsNil(ctx, err, "保存文件失败")// 保存视频信息videoInfo := &model.DsVideoInfo{Id:        node.Generate().Int64(),PosterId:  imageId,Md5:       md5,Name:      req.File.Filename,Path:      videoObjectName,Size:      req.File.Size,MimeType:  req.File.Header.Get("Content-Type"),Duration:  duration,Width:     width,Height:    height,CreatedBy: 0,CreatedAt: gtime.Now(),UpdatedBy: 0,UpdatedAt: gtime.Now(),}_, err = dao.DsVideo.Ctx(ctx).Insert(videoInfo)liberr.ErrIsNil(ctx, err, "保存视频信息失败")// 清理临时目录os.RemoveAll(tempDir)res.Id = videoInfo.Idres.Url = fmt.Sprintf("/api/v1/admin/ds/dsVideo/view?id=%d", videoInfo.Id)res.Poster = fmt.Sprintf("/api/v1/admin/ds/dsImage/view?id=%d", imageId)})return
}

3. 文件查看实现 ⬇️

获取文件信息:返回JSON格式的元数据,前端根据返回的路径进行接口请求

以视频为例

// GetVideoInfo 获取视频信息
func (c *dsUploadController) GetVideoInfo(ctx context.Context, req *api.DsVideoInfoReq) (res *api.DsVideoInfoRes, err error) {// 查询视频信息videoInfo, err := service.DsUpload().GetVideoInfo(ctx, req)if err != nil {return nil, err}// 直接从 MinIO 读取视频内容drive := storage.MinioDrive{}client, err := drive.GetClient()if err != nil {return nil, err}obj, err := client.GetObject(ctx, config.MINIO_BUCKET, videoInfo.Path, minio.GetObjectOptions{})if err != nil {return nil, err}defer obj.Close()// 设置响应头writer := g.RequestFromCtx(ctx).Response.ResponseWriterwriter.Header().Set("Content-Type", videoInfo.MimeType)writer.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", videoInfo.Name))// 写入视频流_, err = io.Copy(writer, obj)return nil, err // 不返回JSON
}// ViewVideo 返回视频二进制流
func (c *dsUploadController) ViewVideo(ctx context.Context, req *api.DsVideoViewReq) (res *api.DsVideoViewRes, err error) {// 查询视频信息videoInfo, err := service.DsUpload().GetVideoInfo(ctx, &api.DsVideoInfoReq{Id: req.Id})if err != nil {return nil, err}// 直接从 MinIO 读取视频内容drive := storage.MinioDrive{}client, err := drive.GetClient()if err != nil {return nil, err}obj, err := client.GetObject(ctx, config.MINIO_BUCKET, videoInfo.Path, minio.GetObjectOptions{})if err != nil {return nil, err}defer obj.Close()// 设置响应头writer := g.RequestFromCtx(ctx).Response.ResponseWriterwriter.Header().Set("Content-Type", videoInfo.MimeType)writer.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", videoInfo.Name))// 写入视频流_, err = io.Copy(writer, obj)return nil, err // 不返回JSON
}

softhub系列往期文章

  1. Softhub软件下载站实战开发(一):项目总览
  2. Softhub软件下载站实战开发(二):项目基础框架搭建
  3. Softhub软件下载站实战开发(三):平台管理模块实战
  4. Softhub软件下载站实战开发(四):代码生成器设计与实现
  5. Softhub软件下载站实战开发(五):分类模块实现
  6. Softhub软件下载站实战开发(六):软件配置面板实现
  7. Softhub软件下载站实战开发(七):集成MinIO实现文件存储功能
  8. Softhub软件下载站实战开发(八):编写软件后台管理
  9. Softhub软件下载站实战开发(九):编写软件配置管理界面

文章转载自:

http://434VNTTt.nhdmh.cn
http://SzrOXFEa.nhdmh.cn
http://5LBUr5zx.nhdmh.cn
http://dC7d4rvz.nhdmh.cn
http://2TOKfxvX.nhdmh.cn
http://32In5KLO.nhdmh.cn
http://DWu1e3Sq.nhdmh.cn
http://OdWujZKr.nhdmh.cn
http://tMMQsYkA.nhdmh.cn
http://9J94Nakn.nhdmh.cn
http://kDSIS6nz.nhdmh.cn
http://GxUHjEMT.nhdmh.cn
http://Cpvn1voC.nhdmh.cn
http://BvYVpSIf.nhdmh.cn
http://flGXEXGU.nhdmh.cn
http://7rn3re4t.nhdmh.cn
http://8PWnWNSo.nhdmh.cn
http://4WvLUREA.nhdmh.cn
http://HDcx9axt.nhdmh.cn
http://xR82I8Nq.nhdmh.cn
http://87PmQpfe.nhdmh.cn
http://SCBSPMGI.nhdmh.cn
http://5ypVRahH.nhdmh.cn
http://oxrTdvLv.nhdmh.cn
http://o2ZOvV22.nhdmh.cn
http://H29pFDbK.nhdmh.cn
http://chcUcZ8S.nhdmh.cn
http://LxXJ3Vqk.nhdmh.cn
http://xoTWhlgr.nhdmh.cn
http://Bn81p59G.nhdmh.cn
http://www.dtcms.com/wzjs/624615.html

相关文章:

  • 延边网站开发局域网站开发
  • 网站网络营销诸城建设局网站
  • 中小型企业查询网站深圳app开发公司鑫酷
  • 找网站的方法哪个手机网站 有app
  • 12380网站建设的意见建议高校建设思政教育网站案例
  • 263云通信官方网站商城网站html模板
  • 溧水做网站wordpress迅雷下载
  • 如何在手机上做自己的网站wordpress 视差效果
  • 百度统计网站怎么进入wordpress修改界面
  • 如何让自己网站排名提高建官网公司
  • 东莞网站建设培训一级造价工程师报名时间2023年
  • 网站建设的软文素材wordpress搜索全站
  • 贷款类网站怎样做建个网站的电话
  • phpstudy做网站如何制作app的页面
  • 深圳网站制作必选祥奔科技网站页面分析作业
  • 漫画门户网站怎么做的百度seo公司整站优化
  • 富阳科技网站有哪些自己做网站要学前端和后端
  • 如何给网站做权重大连 网站
  • 组织建设一百年心得体会苏州搜索引擎优化
  • 企业网站怎么做排名diy网站建设源码
  • 网站建设课程设计报告浙江省建设厅建筑信息网官网
  • 网站建设个人网上银行济南seo整站外包
  • 站长统计app进入网址新版小猪腾讯企点聊天记录怎么恢复
  • 免费的编程自学网站百度商城
  • 云南省建设网站手机小说网站建设
  • iis 7.5 网站网站运营暂停
  • 网站建设栏目设置表格餐饮设计公司名字
  • 给企业做网站用什么程序注册境外服务公司
  • 宁波网站制作哪家优惠多wordpress 文章排名
  • 泉州市住房与城乡建设局网站重庆网站备案