- 数据概览:去掉代付统计独立卡片,总收入中以小标签显示代付金额 - 数据概览:移除余额统计区块(余额改在用户管理中展示) - 数据概览:恢复转化率卡片(唯一付费用户/总用户) - 用户管理:用户列表新增「余额/提现」列,显示钱包余额和已提现金额 - 后端:DBUsersList 增加 user_balances 查询,返回 walletBalance 字段 - 后端:User model 添加 WalletBalance 非数据库字段 - 包含之前的小程序埋点和管理后台点击统计面板 Made-with: Cursor
96 lines
2.9 KiB
Go
96 lines
2.9 KiB
Go
package handler
|
||
|
||
import (
|
||
"fmt"
|
||
"math/rand"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
const uploadDir = "uploads"
|
||
const maxUploadBytes = 5 * 1024 * 1024 // 5MB
|
||
var allowedTypes = map[string]bool{"image/jpeg": true, "image/png": true, "image/gif": true, "image/webp": true}
|
||
|
||
// UploadPost POST /api/upload 上传图片(表单 file),优先 OSS
|
||
func UploadPost(c *gin.Context) {
|
||
file, err := c.FormFile("file")
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请选择要上传的文件"})
|
||
return
|
||
}
|
||
if file.Size > maxUploadBytes {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "文件大小不能超过5MB"})
|
||
return
|
||
}
|
||
ct := file.Header.Get("Content-Type")
|
||
if !allowedTypes[ct] && !strings.HasPrefix(ct, "image/") {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "仅支持图片格式"})
|
||
return
|
||
}
|
||
ext := filepath.Ext(file.Filename)
|
||
if ext == "" {
|
||
ext = ".jpg"
|
||
}
|
||
folder := c.PostForm("folder")
|
||
if folder == "" {
|
||
folder = "avatars"
|
||
}
|
||
name := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), randomStrUpload(6), ext)
|
||
|
||
// 尝试 OSS 上传
|
||
if ossCfg := getOssConfig(); ossCfg != nil {
|
||
src, err := file.Open()
|
||
if err == nil {
|
||
defer src.Close()
|
||
if ossURL, err := ossUploadFile(src, folder, name); err == nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "url": ossURL, "data": gin.H{"url": ossURL, "fileName": name, "size": file.Size, "type": ct, "storage": "oss"}})
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// 回退本地存储
|
||
dir := filepath.Join(uploadDir, folder)
|
||
_ = os.MkdirAll(dir, 0755)
|
||
dst := filepath.Join(dir, name)
|
||
if err := c.SaveUploadedFile(file, dst); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "保存失败"})
|
||
return
|
||
}
|
||
url := "/" + filepath.ToSlash(filepath.Join(uploadDir, folder, name))
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "url": url, "data": gin.H{"url": url, "fileName": name, "size": file.Size, "type": ct, "storage": "local"}})
|
||
}
|
||
|
||
func randomStrUpload(n int) string {
|
||
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||
b := make([]byte, n)
|
||
for i := range b {
|
||
b[i] = letters[rand.Intn(len(letters))]
|
||
}
|
||
return string(b)
|
||
}
|
||
|
||
// UploadDelete DELETE /api/upload
|
||
func UploadDelete(c *gin.Context) {
|
||
path := c.Query("path")
|
||
if path == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请指定 path"})
|
||
return
|
||
}
|
||
if !strings.HasPrefix(path, "/uploads/") && !strings.HasPrefix(path, "uploads/") {
|
||
c.JSON(http.StatusForbidden, gin.H{"success": false, "error": "无权限删除此文件"})
|
||
return
|
||
}
|
||
fullPath := strings.TrimPrefix(path, "/")
|
||
if err := os.Remove(fullPath); err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "文件不存在或删除失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "删除成功"})
|
||
}
|