feat: 数据概览简化 + 用户管理增加余额/提现列

- 数据概览:去掉代付统计独立卡片,总收入中以小标签显示代付金额
- 数据概览:移除余额统计区块(余额改在用户管理中展示)
- 数据概览:恢复转化率卡片(唯一付费用户/总用户)
- 用户管理:用户列表新增「余额/提现」列,显示钱包余额和已提现金额
- 后端:DBUsersList 增加 user_balances 查询,返回 walletBalance 字段
- 后端:User model 添加 WalletBalance 非数据库字段
- 包含之前的小程序埋点和管理后台点击统计面板

Made-with: Cursor
This commit is contained in:
卡若
2026-03-15 15:57:09 +08:00
parent 991e17698c
commit 708547d0dd
52 changed files with 3161 additions and 1103 deletions

View File

@@ -34,11 +34,11 @@ var (
"image/jpeg": true, "image/png": true, "image/gif": true, "image/webp": true,
}
allowedVideoTypes = map[string]bool{
"video/mp4": true, "video/quicktime": true, "video/x-msvideo": true,
"video/mp4": true, "video/quicktime": true, "video/webm": true, "video/x-msvideo": true,
}
)
// UploadImagePost POST /api/miniprogram/upload/image 小程序-图片上传(支持压缩)
// UploadImagePost POST /api/miniprogram/upload/image 小程序-图片上传(支持压缩),优先 OSS
// 表单file必填, folder可选默认 images, quality可选 1-100默认 85
func UploadImagePost(c *gin.Context) {
file, err := c.FormFile("file")
@@ -65,14 +65,11 @@ func UploadImagePost(c *gin.Context) {
if folder == "" {
folder = "images"
}
dir := filepath.Join(uploadDirContent, folder)
_ = os.MkdirAll(dir, 0755)
ext := filepath.Ext(file.Filename)
if ext == "" {
ext = ".jpg"
}
name := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), randomStrContent(6), ext)
dst := filepath.Join(dir, name)
src, err := file.Open()
if err != nil {
@@ -85,61 +82,58 @@ func UploadImagePost(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "读取文件失败"})
return
}
// JPEG支持质量压缩
// JPEG 压缩
var finalData []byte
finalCt := ct
if strings.Contains(ct, "jpeg") || strings.Contains(ct, "jpg") {
img, err := jpeg.Decode(bytes.NewReader(data))
if err == nil {
if img, err := jpeg.Decode(bytes.NewReader(data)); err == nil {
var buf bytes.Buffer
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}); err == nil {
if err := os.WriteFile(dst, buf.Bytes(), 0644); err == nil {
url := "/" + filepath.ToSlash(filepath.Join(uploadDirContent, folder, name))
c.JSON(http.StatusOK, gin.H{
"success": true, "url": url,
"data": gin.H{"url": url, "fileName": name, "size": int64(buf.Len()), "type": ct, "quality": quality},
})
return
}
finalData = buf.Bytes()
}
}
}
// PNG/GIF解码后原样保存
if strings.Contains(ct, "png") {
img, err := png.Decode(bytes.NewReader(data))
if err == nil {
} else if strings.Contains(ct, "png") {
if img, err := png.Decode(bytes.NewReader(data)); err == nil {
var buf bytes.Buffer
if err := png.Encode(&buf, img); err == nil {
if err := os.WriteFile(dst, buf.Bytes(), 0644); err == nil {
url := "/" + filepath.ToSlash(filepath.Join(uploadDirContent, folder, name))
c.JSON(http.StatusOK, gin.H{"success": true, "url": url, "data": gin.H{"url": url, "fileName": name, "size": int64(buf.Len()), "type": ct}})
return
}
finalData = buf.Bytes()
}
}
}
if strings.Contains(ct, "gif") {
img, err := gif.Decode(bytes.NewReader(data))
if err == nil {
} else if strings.Contains(ct, "gif") {
if img, err := gif.Decode(bytes.NewReader(data)); err == nil {
var buf bytes.Buffer
if err := gif.Encode(&buf, img, nil); err == nil {
if err := os.WriteFile(dst, buf.Bytes(), 0644); err == nil {
url := "/" + filepath.ToSlash(filepath.Join(uploadDirContent, folder, name))
c.JSON(http.StatusOK, gin.H{"success": true, "url": url, "data": gin.H{"url": url, "fileName": name, "size": int64(buf.Len()), "type": ct}})
return
}
finalData = buf.Bytes()
}
}
}
if finalData == nil {
finalData = data
}
// 其他格式或解析失败时直接写入
if err := os.WriteFile(dst, data, 0644); err != nil {
// 优先 OSS 上传
if ossURL, err := ossUploadBytes(finalData, folder, name, finalCt); err == nil {
c.JSON(http.StatusOK, gin.H{
"success": true, "url": ossURL,
"data": gin.H{"url": ossURL, "fileName": name, "size": int64(len(finalData)), "type": ct, "quality": quality, "storage": "oss"},
})
return
}
// 回退本地存储
dir := filepath.Join(uploadDirContent, folder)
_ = os.MkdirAll(dir, 0755)
dst := filepath.Join(dir, name)
if err := os.WriteFile(dst, finalData, 0644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "保存失败"})
return
}
url := "/" + filepath.ToSlash(filepath.Join(uploadDirContent, folder, name))
c.JSON(http.StatusOK, gin.H{"success": true, "url": url, "data": gin.H{"url": url, "fileName": name, "size": int64(len(data)), "type": ct}})
c.JSON(http.StatusOK, gin.H{"success": true, "url": url, "data": gin.H{"url": url, "fileName": name, "size": int64(len(finalData)), "type": ct, "quality": quality, "storage": "local"}})
}
// UploadVideoPost POST /api/miniprogram/upload/video 小程序-视频上传(指定目录)
// UploadVideoPost POST /api/miniprogram/upload/video 小程序-视频上传,优先 OSS
// 表单file必填, folder可选默认 videos
func UploadVideoPost(c *gin.Context) {
file, err := c.FormFile("file")
@@ -160,13 +154,30 @@ func UploadVideoPost(c *gin.Context) {
if folder == "" {
folder = "videos"
}
dir := filepath.Join(uploadDirContent, folder)
_ = os.MkdirAll(dir, 0755)
ext := filepath.Ext(file.Filename)
if ext == "" {
ext = ".mp4"
}
name := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), randomStrContent(8), 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, "folder": folder, "storage": "oss"},
})
return
}
}
}
// 回退本地存储
dir := filepath.Join(uploadDirContent, 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": "保存失败"})
@@ -175,7 +186,7 @@ func UploadVideoPost(c *gin.Context) {
url := "/" + filepath.ToSlash(filepath.Join(uploadDirContent, folder, name))
c.JSON(http.StatusOK, gin.H{
"success": true, "url": url,
"data": gin.H{"url": url, "fileName": name, "size": file.Size, "type": ct, "folder": folder},
"data": gin.H{"url": url, "fileName": name, "size": file.Size, "type": ct, "folder": folder, "storage": "local"},
})
}