Skip to content

Commit

Permalink
Merge pull request #280 from xuewenG/thumb-img-url
Browse files Browse the repository at this point in the history
优化图片展示和上传
  • Loading branch information
xuewenG authored Feb 15, 2025
2 parents ec6f6a5 + 9b3d0b3 commit 5925f11
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 136 deletions.
43 changes: 23 additions & 20 deletions backend/db/memo.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,32 @@ package db

import (
"time"

"github.com/kingwrcy/moments/vo"
)

type Memo struct {
Id int32 `gorm:"column:id;primary_key;NOT NULL" json:"id,omitempty"`
Content string `gorm:"column:content" json:"content,omitempty"`
Imgs string `gorm:"column:imgs" json:"imgs,omitempty"`
FavCount int32 `gorm:"column:favCount;default:0;NOT NULL" json:"favCount,omitempty"`
CommentCount int32 `gorm:"column:commentCount;default:0;NOT NULL" json:"commentCount,omitempty"`
UserId int32 `gorm:"column:userId;NOT NULL" json:"userId,omitempty"`
CreatedAt *time.Time `gorm:"column:createdAt;default:CURRENT_TIMESTAMP;NOT NULL" json:"createdAt,omitempty"`
UpdatedAt *time.Time `gorm:"column:updatedAt;NOT NULL" json:"updatedAt,omitempty"`
Music163Url string `gorm:"column:music163Url" json:"music163Url,omitempty"`
BilibiliUrl string `gorm:"column:bilibiliUrl" json:"bilibiliUrl,omitempty"`
Location string `gorm:"column:location" json:"location,omitempty"`
ExternalUrl string `gorm:"column:externalUrl" json:"externalUrl,omitempty"`
ExternalTitle string `gorm:"column:externalTitle" json:"externalTitle,omitempty"`
ExternalFavicon string `gorm:"column:externalFavicon;default:/favicon.png;NOT NULL" json:"externalFavicon,omitempty"`
Pinned *bool `gorm:"column:pinned;default:false;NOT NULL" json:"pinned,omitempty"`
Ext string `gorm:"column:ext;default:{};NOT NULL" json:"ext,omitempty"`
ShowType *int32 `gorm:"column:showType;default:1;NOT NULL" json:"showType,omitempty"`
User *User `json:"user,omitempty"`
Comments []Comment `json:"comments,omitempty"`
Tags *string `json:"tags,omitempty"`
Id int32 `gorm:"column:id;primary_key;NOT NULL" json:"id,omitempty"`
Content string `gorm:"column:content" json:"content,omitempty"`
Imgs string `gorm:"column:imgs" json:"imgs,omitempty"`
FavCount int32 `gorm:"column:favCount;default:0;NOT NULL" json:"favCount,omitempty"`
CommentCount int32 `gorm:"column:commentCount;default:0;NOT NULL" json:"commentCount,omitempty"`
UserId int32 `gorm:"column:userId;NOT NULL" json:"userId,omitempty"`
CreatedAt *time.Time `gorm:"column:createdAt;default:CURRENT_TIMESTAMP;NOT NULL" json:"createdAt,omitempty"`
UpdatedAt *time.Time `gorm:"column:updatedAt;NOT NULL" json:"updatedAt,omitempty"`
Music163Url string `gorm:"column:music163Url" json:"music163Url,omitempty"`
BilibiliUrl string `gorm:"column:bilibiliUrl" json:"bilibiliUrl,omitempty"`
Location string `gorm:"column:location" json:"location,omitempty"`
ExternalUrl string `gorm:"column:externalUrl" json:"externalUrl,omitempty"`
ExternalTitle string `gorm:"column:externalTitle" json:"externalTitle,omitempty"`
ExternalFavicon string `gorm:"column:externalFavicon;default:/favicon.png;NOT NULL" json:"externalFavicon,omitempty"`
Pinned *bool `gorm:"column:pinned;default:false;NOT NULL" json:"pinned,omitempty"`
Ext string `gorm:"column:ext;default:{};NOT NULL" json:"ext,omitempty"`
ShowType *int32 `gorm:"column:showType;default:1;NOT NULL" json:"showType,omitempty"`
User *User `json:"user,omitempty"`
Comments []Comment `json:"comments,omitempty"`
Tags *string `json:"tags,omitempty"`
ImgConfigs *[]*vo.ImgConfig `gorm:"-" json:"imgConfigs,omitempty"`
}

func (m *Memo) TableName() string {
Expand Down
90 changes: 58 additions & 32 deletions backend/handler/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"io"
"os"
"path"
"path/filepath"
"strings"
"time"

Expand Down Expand Up @@ -45,45 +44,47 @@ func (f FileHandler) Upload(c echo.Context) error {
var (
result []string
)

form, err := c.MultipartForm()
if err != nil {
f.base.log.Error().Msgf("读取上传图片异常:%s", err)
return FailRespWithMsg(c, Fail, "上传图片异常")
}
files := form.File["files"]

if err := os.MkdirAll(f.base.cfg.UploadDir, 0755); err != nil {
f.base.log.Error().Msgf("创建父级目录异常:%s", err)
return FailRespWithMsg(c, Fail, "创建父级目录异常")
}

files := form.File["files"]
for _, file := range files {
// Source
// 原始图片
src, err := file.Open()
if err != nil {
f.base.log.Error().Msgf("打开上传图片异常:%s", err)
return FailRespWithMsg(c, Fail, "上传图片异常")
}
defer src.Close()
// Destination

// 创建原始图片
img_filename := strings.ReplaceAll(uuid.NewString(), "-", "")
img_filepath := path.Join(f.base.cfg.UploadDir, img_filename)

thumb_filename := img_filename + "_thumb"
thumb_filepath := path.Join(f.base.cfg.UploadDir, thumb_filename)

if err := os.MkdirAll(filepath.Dir(img_filepath), 0755); err != nil {
f.base.log.Error().Msgf("创建父级目录异常:%s", err)
return FailRespWithMsg(c, Fail, "创建父级目录异常")
}
dst, err := os.Create(img_filepath)
if err != nil {
f.base.log.Error().Msgf("打开目标图片异常:%s", err)
return FailRespWithMsg(c, Fail, "上传图片异常")
}
defer dst.Close()
// Copy

// 保存图片
if _, err = io.Copy(dst, src); err != nil {
f.base.log.Error().Msgf("复制图片异常:%s", err)
return FailRespWithMsg(c, Fail, "上传图片异常")
}

// compress image
// 生成并保存缩略图
thumb_filename := img_filename + "_thumb"
thumb_filepath := path.Join(f.base.cfg.UploadDir, thumb_filename)
if err := CompressImage(f, img_filepath, thumb_filepath, 30); err != nil {
f.base.log.Error().Msgf("压缩图片异常:%s", err)
}
Expand Down Expand Up @@ -113,7 +114,6 @@ type s3PresignedResp struct {
// @Success 200 {object} s3PresignedResp
// @Router /api/file/s3PreSigned [post]
func (f FileHandler) S3PreSigned(c echo.Context) error {

var (
req PreSignedReq
sysConfig db.SysConfig
Expand All @@ -126,15 +126,30 @@ func (f FileHandler) S3PreSigned(c echo.Context) error {
if err := f.base.db.First(&sysConfig).Error; errors.Is(err, gorm.ErrRecordNotFound) {
return FailResp(c, Fail)
}

if err := json.Unmarshal([]byte(sysConfig.Content), &sysConfigVo); err != nil {
f.base.log.Error().Msgf("无法反序列化系统配置, %s", err)
return FailRespWithMsg(c, Fail, err.Error())
}
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(sysConfigVo.S3.Region),
config.WithEndpointResolver(aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
return aws.Endpoint{URL: sysConfigVo.S3.Endpoint}, nil
})),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(sysConfigVo.S3.AccessKey, sysConfigVo.S3.SecretKey, "")))

cfg, err := config.LoadDefaultConfig(
context.TODO(),
config.WithRegion(sysConfigVo.S3.Region),
config.WithEndpointResolver(
aws.EndpointResolverFunc(
func(service, region string) (aws.Endpoint, error) {
return aws.Endpoint{URL: sysConfigVo.S3.Endpoint}, nil
},
),
),
config.WithCredentialsProvider(
credentials.NewStaticCredentialsProvider(
sysConfigVo.S3.AccessKey,
sysConfigVo.S3.SecretKey,
"",
),
),
)
if err != nil {
f.base.log.Error().Msgf("无法加载SDK配置, %s", err)
return FailRespWithMsg(c, Fail, err.Error())
Expand All @@ -143,22 +158,33 @@ func (f FileHandler) S3PreSigned(c echo.Context) error {
client := s3.NewFromConfig(cfg)
presignedClient := s3.NewPresignClient(client)

key := fmt.Sprintf("moments/%s/%s", time.Now().Format("2006/01/02"), strings.ReplaceAll(uuid.NewString(), "-", ""))
presignedResult, err := presignedClient.PresignPutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String(sysConfigVo.S3.Bucket),
Key: aws.String(key),
ContentType: aws.String(req.ContentType),
}, func(opts *s3.PresignOptions) {
opts.Expires = time.Minute * 5
})
key := fmt.Sprintf(
"moments/%s/%s",
time.Now().Format("2006/01/02"),
strings.ReplaceAll(uuid.NewString(), "-", ""),
)
presignedResult, err := presignedClient.PresignPutObject(
context.TODO(),
&s3.PutObjectInput{
Bucket: aws.String(sysConfigVo.S3.Bucket),
Key: aws.String(key),
ContentType: aws.String(req.ContentType),
},
func(opts *s3.PresignOptions) {
opts.Expires = time.Minute * 5
},
)

if err != nil {
f.base.log.Error().Msgf("无法获取预签名URL, %s", err)
return FailRespWithMsg(c, Fail, fmt.Sprintf("无法获取预签名URL, %s", err))
}

return SuccessResp(c, s3PresignedResp{
PreSignedUrl: presignedResult.URL,
ImageUrl: fmt.Sprintf("%s/%s", sysConfigVo.S3.Domain, key),
})
return SuccessResp(
c,
s3PresignedResp{
PreSignedUrl: presignedResult.URL,
ImageUrl: fmt.Sprintf("%s/%s", sysConfigVo.S3.Domain, key),
},
)
}
60 changes: 57 additions & 3 deletions backend/handler/memo.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
Expand Down Expand Up @@ -93,6 +94,47 @@ type memoListResp struct {
Total int64 `json:"total,omitempty"` //总数
}

/*
这里通过 memo.Imgs 生成 ImgConfigs,ImgConfig 包含图片的原始 Url 和 ThumbUrl
正常情况下这里应该是在上传图片的时候来保存 ImgConfig,而不是在获取 memo 时计算
但是由于历史实现的 memo 模型结构不支持保存额外的图片信息,所以这里先临时实现一个版本,有精力再重构
*/
func (m MemoHandler) handleImgConfigs(sysConfigVO *vo.FullSysConfigVO, memo *db.Memo) {
var imgConfigs []*vo.ImgConfig

imgs := strings.Split(memo.Imgs, ",")
for _, img := range imgs {
if img == "" {
continue
}

imgConfig := &vo.ImgConfig{
Url: &img,
ThumbUrl: &img,
}

if strings.HasPrefix(img, "/upload/") {
thumb_filename := img + "_thumb"
thumb_filepath := path.Join(m.base.cfg.UploadDir, path.Base(thumb_filename))
if fs_util.Exists(thumb_filepath) {
imgConfig.ThumbUrl = &thumb_filename
}
} else if sysConfigVO.S3.ThumbnailSuffix != "" && strings.HasPrefix(img, sysConfigVO.S3.Domain) {
thumbnailSuffix := sysConfigVO.S3.ThumbnailSuffix
if !strings.HasPrefix(thumbnailSuffix, "?") {
thumbnailSuffix = "?" + thumbnailSuffix
}

newThumbUrl := img + thumbnailSuffix
imgConfig.ThumbUrl = &newThumbUrl
}

imgConfigs = append(imgConfigs, imgConfig)
}

memo.ImgConfigs = &imgConfigs
}

// ListMemos godoc
//
// @Tags Memo
Expand Down Expand Up @@ -196,6 +238,10 @@ func (m MemoHandler) ListMemos(c echo.Context) error {
list[i].Comments = comments
}

for i := range list {
m.handleImgConfigs(&sysConfigVO, &list[i])
}

return SuccessResp(c, memoListResp{
List: list,
Total: total,
Expand Down Expand Up @@ -237,15 +283,17 @@ func (m MemoHandler) RemoveMemo(c echo.Context) error {
if memo.Imgs != "" {
imgs := strings.Split(memo.Imgs, ",")
for _, img := range imgs {
if !strings.HasPrefix(img, "/upload/") {
return SuccessResp(c, h{})
if img == "" || !strings.HasPrefix(img, "/upload/") {
continue
}

img := strings.ReplaceAll(img, "/upload/", "")
_ = os.Remove(filepath.Join(m.base.cfg.UploadDir, img))
thumbImg := strings.ReplaceAll(img+"_thumb", "/upload/", "")
_ = os.Remove(filepath.Join(m.base.cfg.UploadDir, thumbImg))
}
}

return SuccessResp(c, h{})
}

Expand Down Expand Up @@ -411,12 +459,16 @@ func (m MemoHandler) SaveMemo(c echo.Context) error {
// @Router /api/memo/get [post]
func (m MemoHandler) GetMemo(c echo.Context) error {
var (
memo db.Memo
memo db.Memo
sysConfig db.SysConfig
sysConfigVO vo.FullSysConfigVO
)

ctx := c.(CustomContext)
currentUser := ctx.CurrentUser()

m.base.db.First(&sysConfig)

id, err := strconv.Atoi(c.QueryParam("id"))
if err != nil {
return FailResp(c, ParamError)
Expand All @@ -443,6 +495,8 @@ func (m MemoHandler) GetMemo(c echo.Context) error {

memo.Comments = comments

m.handleImgConfigs(&sysConfigVO, &memo)

return SuccessResp(c, memo)
}

Expand Down
7 changes: 6 additions & 1 deletion backend/handler/rss.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,14 @@ func getContentWithExt(memo db.Memo, host string) string {
if memo.Imgs != "" {
imgs := strings.Split(memo.Imgs, ",")
for _, img := range imgs {
if img[:7] == "/upload" {
if img == "" {
continue
}

if strings.HasPrefix(img, "/upload/") {
img = host + img
}

content += fmt.Sprintf("\n\n![%s](%s)", img, img)
}
}
Expand Down
5 changes: 5 additions & 0 deletions backend/vo/memo_vo.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,8 @@ type DoubanBook struct {
PubDate string `json:"pubDate,omitempty"` //发布日期
Keywords string `json:"keywords,omitempty"` //关键字
}

type ImgConfig struct {
Url *string `json:"url,omitempty"`
ThumbUrl *string `json:"thumbUrl,omitempty"`
}
2 changes: 1 addition & 1 deletion front/components/Memo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
<div class="flex flex-col gap-2">
<external-url-preview :favicon="item.externalFavicon" :title="item.externalTitle" :url="item.externalUrl"
v-if="item.externalFavicon&&item.externalTitle&&item.externalUrl"/>
<upload-image-preview :imgs="item.imgs||''" :memo-id="item.id"/>
<upload-image-preview :imgs="item.imgs" :imgConfigs="item.imgConfigs" :memo-id="item.id"/>

<music-preview v-if="extJSON.music && extJSON.music.id" v-bind="extJSON.music"/>
<douban-book-preview v-if="extJSON.doubanBook && extJSON.doubanBook.title" :book="extJSON.doubanBook"/>
Expand Down
17 changes: 7 additions & 10 deletions front/components/MemoEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ const locationLabel = computed(() => {
})
const handleDragImage = (imgs: string[]) => {
state.imgs = imgs.join(",")
state.imgs = imgs.filter(Boolean).join(",")
}
const updateMusic = (music: MusicDTO) => {
Expand All @@ -186,13 +186,10 @@ const {y: windowY} = useWindowScroll()
const isOpen = ref(false)
const virtualElement = ref({getBoundingClientRect: () => ({})})
const handleRemoveImage = (img: string) => {
const imgs = state.imgs.split(",")
const index = imgs.findIndex(r => r === img)
if (index < 0) {
return
}
imgs.splice(index, 1)
state.imgs = imgs.join(",")
state.imgs = state.imgs
.split(",")
.filter(item => item && item != img)
.join(",")
}
function onContextMenu() {
Expand Down Expand Up @@ -278,9 +275,9 @@ const saveMemo = async () => {
externalFavicon: state.externalUrl ? state.externalFavicon : "",
externalTitle: state.externalTitle,
externalUrl: state.externalUrl,
imgs: state.imgs.split(','),
imgs: state.imgs.split(",").filter(Boolean),
location: state.location,
tags: selectedLabel.value
tags: selectedLabel.value,
})
toast.success("保存成功!")
await navigateTo('/')
Expand Down
Loading

0 comments on commit 5925f11

Please sign in to comment.