mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 23:56:37 +08:00
裁剪
swagger 移除 merchant swagger 改造首页内容为通用后台管理系统定位 - 修改首页标题从 'LINUX DO Credit' 改为 'Modern Platform' - 更新副标题为 '为二次开发而生' - 更新首页描述为通用平台的特点 - 更新首页特性标签为 '开箱即用、高度可扩展、工业级基建' - 修改展示卡片为技术栈和二次开发相关 - 更新开发者示例代码为通用的注册和 API Key 获取示例 - 更新页脚品牌名为 'Modern Platform' - 调整页脚导航链接为通用平台相关内容 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 去除遗留 裁剪 移除 /api/v1/user/pay-key 相关代码 - 删除后端 UpdatePayKey 处理器函数和 UpdatePayKeyRequest 结构体 - 删除 User 模型中的 PayKey 字段 - 删除 User.VerifyPayKey 方法 - 删除 EncryptPayKeyFailed 错误常量 - 删除 /api/v1/user/pay-key PUT 路由 - 删除 OAuth 返回中的 IsPayKey 字段 - 删除前端 UserService.updatePayKey 方法 - 删除前端所有支付密钥 UI 和逻辑 - 更新相关的导出和注释 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 去除遗留 api 修正 系统配置 前端裁剪 后端裁剪 init
This commit is contained in:
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 linux.do
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package upload
|
||||
|
||||
// 封面类型
|
||||
const (
|
||||
CoverTypeCover = "cover"
|
||||
CoverTypeHeterotypic = "heterotypic"
|
||||
)
|
||||
|
||||
// 最大文件大小
|
||||
const (
|
||||
MaxFileSize = 2 * 1024 * 1024 // 2MB
|
||||
)
|
||||
@@ -1,233 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 linux.do
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package upload
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/linux-do/credit/internal/apps/oauth"
|
||||
"github.com/linux-do/credit/internal/db"
|
||||
"github.com/linux-do/credit/internal/db/idgen"
|
||||
"github.com/linux-do/credit/internal/model"
|
||||
"github.com/linux-do/credit/internal/storage"
|
||||
"github.com/linux-do/credit/internal/util"
|
||||
_ "golang.org/x/image/webp"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UploadResponse 上传响应
|
||||
type UploadResponse struct {
|
||||
ID uint64 `json:"id,string"`
|
||||
}
|
||||
|
||||
// UploadRedEnvelopeCover 上传红包封面
|
||||
// @Tags upload
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param file formData file true "图片文件"
|
||||
// @Param type formData string true "封面类型 (cover/heterotypic)"
|
||||
// @Success 200 {object} util.ResponseAny
|
||||
// @Router /api/v1/upload/redenvelope/cover [post]
|
||||
func UploadRedEnvelopeCover(c *gin.Context) {
|
||||
currentUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
|
||||
// 获取上传的文件
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(ErrNoFileSelected))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取封面类型
|
||||
coverType := c.PostForm("type")
|
||||
if coverType != CoverTypeCover && coverType != CoverTypeHeterotypic {
|
||||
c.JSON(http.StatusBadRequest, util.Err(ErrInvalidCoverType))
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
if file.Size > int64(MaxFileSize) {
|
||||
c.JSON(http.StatusBadRequest, util.Err(ErrFileTooLarge))
|
||||
return
|
||||
}
|
||||
|
||||
// 打开上传的文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(ErrOpenFileFailed))
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 验证文件确实是图片并获取尺寸
|
||||
_, format, err := image.DecodeConfig(src)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(ErrInvalidImage))
|
||||
return
|
||||
}
|
||||
|
||||
// 验证图片类型
|
||||
norm := strings.ToLower(format)
|
||||
if norm == "jpeg" {
|
||||
norm = "jpg"
|
||||
}
|
||||
var sc model.SystemConfig
|
||||
if err := sc.GetByKey(c.Request.Context(), model.ConfigKeyUploadAllowedExtensions); err != nil || strings.TrimSpace(sc.Value) == "" {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(ErrUploadExtensionsNotConfigured))
|
||||
return
|
||||
}
|
||||
v := strings.ToLower(strings.ReplaceAll(sc.Value, " ", ""))
|
||||
v = strings.ReplaceAll(v, "jpeg", "jpg")
|
||||
if !strings.Contains(","+v+",", ","+norm+",") {
|
||||
c.JSON(http.StatusBadRequest, util.Err(ErrUnsupportedFormat))
|
||||
return
|
||||
}
|
||||
|
||||
// 重置文件指针
|
||||
src.Close()
|
||||
src, _ = file.Open()
|
||||
defer src.Close()
|
||||
|
||||
// 计算文件 MD5 以避免重复上传
|
||||
hash := md5.New()
|
||||
if _, err := io.Copy(hash, src); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(ErrProcessFileFailed))
|
||||
return
|
||||
}
|
||||
md5Sum := hex.EncodeToString(hash.Sum(nil))
|
||||
|
||||
// 重置文件指针
|
||||
src.Close()
|
||||
src, _ = file.Open()
|
||||
defer src.Close()
|
||||
|
||||
// 生成安全的文件名: 用户ID_类型_MD5.扩展名
|
||||
// 使用完整 MD5 实现去重,同一用户上传相同图片会命中已有文件
|
||||
safeExt := "." + format
|
||||
if format == "jpeg" {
|
||||
safeExt = ".jpg"
|
||||
}
|
||||
filename := fmt.Sprintf("%s%s", md5Sum, safeExt)
|
||||
|
||||
// 构建 S3 object key: {prefix}{type}/{date}/{userID}/{filename}
|
||||
now := time.Now()
|
||||
objectPath := fmt.Sprintf("%s/%s/%d/%s", coverType, now.Format("2006/01/02"), currentUser.ID, filename)
|
||||
s3Key := storage.BuildKey(objectPath)
|
||||
|
||||
// validate S3 key
|
||||
if err := ValidateS3Key(s3Key); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(ErrInvalidFilePath))
|
||||
return
|
||||
}
|
||||
|
||||
// Content type for S3
|
||||
contentType := "image/" + format
|
||||
|
||||
var recordID uint64
|
||||
|
||||
if err := db.DB(c.Request.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
var existing model.Upload
|
||||
if err := tx.Where("file_path = ?", s3Key).First(&existing).Error; err == nil {
|
||||
recordID = existing.ID
|
||||
return nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Upload to S3
|
||||
if err := storage.PutObject(c.Request.Context(), s3Key, src, file.Size, contentType); err != nil {
|
||||
return errors.New(ErrSaveFileFailed)
|
||||
}
|
||||
|
||||
upload := model.Upload{
|
||||
ID: idgen.NextUint64ID(),
|
||||
UserID: currentUser.ID,
|
||||
FilePath: s3Key,
|
||||
FileSize: file.Size,
|
||||
Type: coverType,
|
||||
Status: model.UploadStatusPending,
|
||||
}
|
||||
|
||||
if err := tx.Create(&upload).Error; err != nil {
|
||||
// 如果数据库保存失败,尝试删除已上传的文件以避免垃圾数据
|
||||
_ = storage.DeleteObject(c.Request.Context(), s3Key)
|
||||
return errors.New(ErrSaveUploadRecordFailed)
|
||||
}
|
||||
|
||||
recordID = upload.ID
|
||||
return nil
|
||||
}); err != nil {
|
||||
if err.Error() == ErrSaveFileFailed {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(ErrSaveFileFailed))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, util.Err(ErrSaveUploadRecordFailed))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(UploadResponse{
|
||||
ID: recordID,
|
||||
}))
|
||||
}
|
||||
|
||||
// ListRedEnvelopeCovers 获取用户历史红包封面
|
||||
// @Tags redenvelope
|
||||
// @Produce json
|
||||
// @Param type query string true "封面类型 (cover/heterotypic)"
|
||||
// @Success 200 {object} util.ResponseAny
|
||||
// @Router /api/v1/redenvelope/covers [get]
|
||||
func ListRedEnvelopeCovers(c *gin.Context) {
|
||||
currentUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
|
||||
coverType := c.Query("type")
|
||||
if coverType != CoverTypeCover && coverType != CoverTypeHeterotypic {
|
||||
c.JSON(http.StatusBadRequest, util.Err(ErrInvalidCoverType))
|
||||
return
|
||||
}
|
||||
|
||||
var uploads []model.Upload
|
||||
if err := db.DB(c.Request.Context()).
|
||||
Where("user_id = ? AND status = ? AND type = ?",
|
||||
currentUser.ID, model.UploadStatusUsed, coverType).
|
||||
Order("created_at DESC").
|
||||
Limit(20).
|
||||
Find(&uploads).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(ErrQueryHistoryCoverFailed))
|
||||
return
|
||||
}
|
||||
|
||||
var results []UploadResponse
|
||||
for _, u := range uploads {
|
||||
results = append(results, UploadResponse{
|
||||
ID: u.ID,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(results))
|
||||
}
|
||||
Reference in New Issue
Block a user