feat: 分片上传断点续传、临时目录后台配置与清扫、宇恒云账号管理

- 管理端大文件分片上传与 sessionStorage 续传;Nginx 大请求体/超时
- .chunk-uploads 定期清扫;system_config 后台配置保留时长与扫描间隔
- 宇恒云 POST /register 对接与 yuheng_cloud_register_records 留痕;yuheng_cloud:manage 权限

Made-with: Cursor
This commit is contained in:
whm
2026-04-13 14:50:27 +08:00
parent 03f5fbb41a
commit 0800982224
20 changed files with 1413 additions and 47 deletions

View File

@@ -0,0 +1,182 @@
package handlers
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
"yh_web/server/config"
"yh_web/server/models"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
const yuhengCloudRegisterColl = "yuheng_cloud_register_records"
func cloudRegisterURL() string {
u := strings.TrimSpace(os.Getenv("YH_CLOUD_REGISTER_URL"))
if u != "" {
return strings.TrimSuffix(u, "/")
}
return "http://www.cloud.yuxindazhineng.com:3001/register"
}
// YuhengCloudRegisterInput 与云端 POST /register 一致email 仅用于调用云端,不写入 Mongo
type YuhengCloudRegisterInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Email string `json:"email" binding:"required"`
}
type cloudRegisterPayload struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
}
func postCloudRegister(ctx context.Context, payload cloudRegisterPayload) (int, string, error) {
body, err := json.Marshal(payload)
if err != nil {
return 0, "", err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, cloudRegisterURL(), bytes.NewReader(body))
if err != nil {
return 0, "", err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 45 * time.Second}
resp, err := client.Do(req)
if err != nil {
return 0, "", err
}
defer resp.Body.Close()
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return resp.StatusCode, strings.TrimSpace(string(b)), nil
}
// CreateYuhengCloudRegister 调用云端注册接口,成功后在 Mongo 写入一条记录(仅 username、password
func CreateYuhengCloudRegister(c *gin.Context) {
var in YuhengCloudRegisterInput
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写用户名、密码与邮箱(邮箱仅提交云端)"})
return
}
in.Username = strings.TrimSpace(in.Username)
in.Password = strings.TrimSpace(in.Password)
in.Email = strings.TrimSpace(in.Email)
if in.Username == "" || in.Password == "" || in.Email == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名、密码、邮箱不能为空"})
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 50*time.Second)
defer cancel()
status, bodySnippet, err := postCloudRegister(ctx, cloudRegisterPayload{
Username: in.Username,
Password: in.Password,
Email: in.Email,
})
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": "调用云端注册失败: " + err.Error()})
return
}
if status < 200 || status >= 300 {
msg := bodySnippet
if len(msg) > 500 {
msg = msg[:500] + "…"
}
if msg == "" {
msg = http.StatusText(status)
}
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("云端返回 %d: %s", status, msg)})
return
}
db := config.GetDB(config.DBName)
if db == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "数据库未连接,未写入本地记录"})
return
}
insCtx, insCancel := context.WithTimeout(context.Background(), 8*time.Second)
defer insCancel()
doc := bson.M{
"username": in.Username,
"password": in.Password,
"created_at": time.Now().UTC().Format(time.RFC3339),
}
res, err := db.Collection(yuhengCloudRegisterColl).InsertOne(insCtx, doc)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "云端已成功但本地记录失败: " + err.Error()})
return
}
idHex := ""
switch v := res.InsertedID.(type) {
case bson.ObjectID:
idHex = v.Hex()
}
c.JSON(http.StatusOK, gin.H{"id": idHex, "message": "已提交云端注册并写入本地记录"})
}
// ListYuhengCloudRegisterRecords 分页列出本地留痕(便于管理页展示)
func ListYuhengCloudRegisterRecords(c *gin.Context) {
db := config.GetDB(config.DBName)
if db == nil {
c.JSON(http.StatusOK, gin.H{"list": []any{}, "total": 0})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
skip := int64((page - 1) * pageSize)
limit := int64(pageSize)
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
coll := db.Collection(yuhengCloudRegisterColl)
total, err := coll.CountDocuments(ctx, bson.M{})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}}).SetSkip(skip).SetLimit(limit)
cur, err := coll.Find(ctx, bson.M{}, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cur.Close(ctx)
var list []models.YuhengCloudRegisterRecord
for cur.Next(ctx) {
var row struct {
ID bson.ObjectID `bson:"_id"`
Username string `bson:"username"`
Password string `bson:"password"`
CreatedAt string `bson:"created_at"`
}
if err := cur.Decode(&row); err != nil {
continue
}
list = append(list, models.YuhengCloudRegisterRecord{
ID: row.ID.Hex(),
Username: row.Username,
Password: row.Password,
CreatedAt: row.CreatedAt,
})
}
c.JSON(http.StatusOK, gin.H{"list": list, "total": total})
}