diff --git a/backend/app/http/medias/controller.go b/backend/app/http/medias/controller.go index c2f9d75..517653d 100644 --- a/backend/app/http/medias/controller.go +++ b/backend/app/http/medias/controller.go @@ -1,10 +1,25 @@ package medias import ( - "github.com/gofiber/fiber" + "crypto/md5" + "encoding/hex" + "errors" + "fmt" + "io" + "mime/multipart" + "os" + "path/filepath" + "time" + + "github.com/gofiber/fiber/v3" log "github.com/sirupsen/logrus" ) +const ( + uploadTempDir = "./temp/chunks" // 临时分片目录 + uploadStorageDir = "./uploads" // 最终文件存储目录 +) + // @provider type Controller struct { svc *Service @@ -19,6 +34,117 @@ func (ctl *Controller) Prepare() error { // Upload // @Router /api/v1/medias/upload [post] // @Bind req body -func (ctl *Controller) Upload(ctx fiber.Ctx, req *UploadReq) (*UploadResp, error) { - return ctl.svc.Upload(ctx.Context(), req) +// @Bind file file +func (ctl *Controller) Upload(ctx fiber.Ctx, file *multipart.FileHeader, req *UploadReq) (*UploadResp, error) { + // 使用MD5创建唯一的临时目录 + tempDir := filepath.Join(uploadTempDir, req.FileMD5) + if err := os.MkdirAll(tempDir, 0o755); err != nil { + return nil, err + } + + chunkPath := filepath.Join(tempDir, fmt.Sprintf("chunk_%d", req.ChunkNumber)) + if err := ctx.SaveFile(file, chunkPath); err != nil { + return nil, err + } + + // 如果是最后一个分片 + if req.ChunkNumber == req.TotalChunks-1 { + // 生成唯一的文件存储路径 + ext := filepath.Ext(req.FileName) + storageDir := filepath.Join(uploadStorageDir, time.Now().Format("2006/01/02")) + if err := os.MkdirAll(storageDir, 0o755); err != nil { + os.RemoveAll(tempDir) + return nil, err + } + + finalPath := filepath.Join(storageDir, req.FileMD5+ext) + + // 计算所有分片的实际大小总和 + totalSize, err := calculateTotalSize(tempDir, req.TotalChunks) + if err != nil { + os.RemoveAll(tempDir) + return nil, fmt.Errorf("计算文件大小失败: %w", err) + } + + // 合并文件 + if err := combineChunks(tempDir, finalPath, req.TotalChunks); err != nil { + os.RemoveAll(tempDir) + return nil, fmt.Errorf("合并文件失败: %w", err) + } + + // 验证MD5 + calculatedMD5, err := calculateFileMD5(finalPath) + if err != nil || calculatedMD5 != req.FileMD5 { + os.RemoveAll(tempDir) + os.Remove(finalPath) + return nil, errors.New("文件MD5验证失败") + } + + // 清理临时目录 + os.RemoveAll(tempDir) + + return &UploadResp{ + Files: []UploadFile{ + { + HashID: calculatedMD5, + Name: req.FileName, + Path: finalPath, + Size: totalSize, + MimeType: file.Header.Get("Content-Type"), + }, + }, + }, nil + } + + return &UploadResp{}, nil +} + +// 计算所有分片的实际大小总和 +func calculateTotalSize(tempDir string, totalChunks int) (int64, error) { + var totalSize int64 + for i := 0; i < totalChunks; i++ { + chunkPath := filepath.Join(tempDir, fmt.Sprintf("chunk_%d", i)) + info, err := os.Stat(chunkPath) + if err != nil { + return 0, err + } + totalSize += info.Size() + } + return totalSize, nil +} + +func combineChunks(tempDir, finalPath string, totalChunks int) error { + finalFile, err := os.Create(finalPath) + if err != nil { + return err + } + defer finalFile.Close() + + for i := 0; i < totalChunks; i++ { + chunkPath := fmt.Sprintf("%s/chunk_%d", tempDir, i) + chunk, err := os.ReadFile(chunkPath) + if err != nil { + return err + } + if _, err := finalFile.Write(chunk); err != nil { + return err + } + } + + return nil +} + +func calculateFileMD5(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", err + } + defer file.Close() + + hash := md5.New() + if _, err := io.Copy(hash, file); err != nil { + return "", err + } + + return hex.EncodeToString(hash.Sum(nil)), nil } diff --git a/backend/app/http/medias/dto.go b/backend/app/http/medias/dto.go index 3d1f14f..188a13d 100644 --- a/backend/app/http/medias/dto.go +++ b/backend/app/http/medias/dto.go @@ -1,7 +1,10 @@ package medias type UploadReq struct { - Files []string `json:"files"` + FileName string `form:"file_name"` + ChunkNumber int `form:"chunk_number"` + TotalChunks int `form:"total_chunks"` + FileMD5 string `form:"file_md5"` } type UploadResp struct { diff --git a/backend/app/http/medias/provider.gen.go b/backend/app/http/medias/provider.gen.go new file mode 100755 index 0000000..fecc87f --- /dev/null +++ b/backend/app/http/medias/provider.gen.go @@ -0,0 +1,56 @@ +package medias + +import ( + "database/sql" + + "git.ipao.vip/rogeecn/atom" + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/contracts" + "git.ipao.vip/rogeecn/atom/utils/opt" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func( + svc *Service, + ) (*Controller, error) { + obj := &Controller{ + svc: svc, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + controller *Controller, + ) (contracts.HttpRoute, error) { + obj := &Routes{ + controller: controller, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }, atom.GroupRoutes); err != nil { + return err + } + if err := container.Container.Provide(func( + db *sql.DB, + ) (*Service, error) { + obj := &Service{ + db: db, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }); err != nil { + return err + } + return nil +} diff --git a/backend/app/http/medias/routes.gen.go b/backend/app/http/medias/routes.gen.go new file mode 100644 index 0000000..ead2c9a --- /dev/null +++ b/backend/app/http/medias/routes.gen.go @@ -0,0 +1,37 @@ +// Code generated by the atomctl ; DO NOT EDIT. + +package medias + +import ( + . "backend/pkg/f" + + _ "git.ipao.vip/rogeecn/atom" + _ "git.ipao.vip/rogeecn/atom/contracts" + "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" +) + +// @provider contracts.HttpRoute atom.GroupRoutes +type Routes struct { + log *log.Entry `inject:"false"` + controller *Controller +} + +func (r *Routes) Prepare() error { + r.log = log.WithField("module", "routes.medias") + return nil +} + +func (r *Routes) Name() string { + return "medias" +} + +func (r *Routes) Register(router fiber.Router) { + // 注册路由组: Controller + router.Post("/api/v1/medias/upload", DataFunc2( + r.controller.Upload, + File("file"), + Body[UploadReq]("req"), + )) + +} diff --git a/backend/app/middlewares/middlewares.go b/backend/app/middlewares/middlewares.go index 644395b..060fb5a 100644 --- a/backend/app/middlewares/middlewares.go +++ b/backend/app/middlewares/middlewares.go @@ -3,7 +3,6 @@ package middlewares import ( "backend/providers/app" "backend/providers/jwt" - "backend/providers/storage" "backend/providers/wechat" log "github.com/sirupsen/logrus" @@ -13,10 +12,9 @@ import ( type Middlewares struct { log *log.Entry `inject:"false"` - app *app.Config - storagePath *storage.Config - jwt *jwt.JWT - client *wechat.Client + app *app.Config + jwt *jwt.JWT + client *wechat.Client } func (f *Middlewares) Prepare() error { diff --git a/backend/app/middlewares/provider.gen.go b/backend/app/middlewares/provider.gen.go index ba3ace9..2ea904d 100755 --- a/backend/app/middlewares/provider.gen.go +++ b/backend/app/middlewares/provider.gen.go @@ -3,7 +3,6 @@ package middlewares import ( "backend/providers/app" "backend/providers/jwt" - "backend/providers/storage" "backend/providers/wechat" "git.ipao.vip/rogeecn/atom/container" @@ -15,13 +14,11 @@ func Provide(opts ...opt.Option) error { app *app.Config, client *wechat.Client, jwt *jwt.JWT, - storagePath *storage.Config, ) (*Middlewares, error) { obj := &Middlewares{ - app: app, - client: client, - jwt: jwt, - storagePath: storagePath, + app: app, + client: client, + jwt: jwt, } if err := obj.Prepare(); err != nil { return nil, err diff --git a/backend/app/service/http/http.go b/backend/app/service/http/http.go index a6b2533..c3bfd2b 100644 --- a/backend/app/service/http/http.go +++ b/backend/app/service/http/http.go @@ -3,6 +3,13 @@ package http import ( "backend/app/errorx" "backend/app/events/subscribers" + "backend/app/http/auth" + "backend/app/http/medias" + "backend/app/http/orders" + "backend/app/http/posts" + "backend/app/http/storages" + "backend/app/http/tenants" + "backend/app/http/users" "backend/app/jobs" "backend/app/middlewares" "backend/app/service" @@ -14,7 +21,9 @@ import ( "backend/providers/http/swagger" "backend/providers/job" "backend/providers/jwt" + "backend/providers/pay" "backend/providers/postgres" + "backend/providers/wechat" "git.ipao.vip/rogeecn/atom" "git.ipao.vip/rogeecn/atom/container" @@ -46,6 +55,18 @@ func Command() atom.Option { With( jobs.Provide, subscribers.Provide, + middlewares.Provide, + wechat.Provide, + pay.Provide, + ). + With( + users.Provide, + tenants.Provide, + posts.Provide, + orders.Provide, + auth.Provide, + medias.Provide, + storages.Provide, ), ), ) diff --git a/backend/config.toml b/backend/config.toml index b8eb168..fbef09b 100644 --- a/backend/config.toml +++ b/backend/config.toml @@ -31,10 +31,9 @@ Asset = "/projects/qvyun/frontend/dist" [Pay] -[Pay.WeChat] -AppId = "wx45745a8c51091ae0" -MechID = "" -SubMechID = "" -SerialNo = "" -ApiV3Key = "" -PrivateKey = "" +WechatAppId = "wx45745a8c51091ae0" +WechatMechID = "" +WechatSubMechID = "" +WechatSerialNo = "" +WechatApiV3Key = "" +WechatPrivateKey = "" diff --git a/backend/fixtures/test.http b/backend/fixtures/test.http new file mode 100644 index 0000000..38f5412 --- /dev/null +++ b/backend/fixtures/test.http @@ -0,0 +1,19 @@ + +### Upload File + +POST http://localhost:9600/api/v1/medias/upload +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="chunk_number" + +1 +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="total_chunks" + +1 +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="file"; filename="1.png" +Content-Type: image/png + +< ./upload.png +------WebKitFormBoundary7MA4YWxkTrZu0gW-- \ No newline at end of file diff --git a/backend/fixtures/upload.png b/backend/fixtures/upload.png new file mode 100644 index 0000000..798e9c0 Binary files /dev/null and b/backend/fixtures/upload.png differ diff --git a/backend/pkg/f/bind.go b/backend/pkg/f/bind.go index 308c13f..4e8ac44 100644 --- a/backend/pkg/f/bind.go +++ b/backend/pkg/f/bind.go @@ -1,10 +1,18 @@ package f import ( + "mime/multipart" + "github.com/gofiber/fiber/v3" "github.com/pkg/errors" ) +func File(key string) func(fiber.Ctx) (*multipart.FileHeader, error) { + return func(ctx fiber.Ctx) (*multipart.FileHeader, error) { + return ctx.FormFile(key) + } +} + func Local[T any](key string) func(fiber.Ctx) (T, error) { return func(ctx fiber.Ctx) (T, error) { v := fiber.Locals[T](ctx, key) diff --git a/backend/providers/pay/config.go b/backend/providers/pay/config.go index 6c05ab0..be3a307 100644 --- a/backend/providers/pay/config.go +++ b/backend/providers/pay/config.go @@ -17,14 +17,10 @@ func DefaultProvider() container.ProviderContainer { } type Config struct { - WeChat *wechatPay -} - -type wechatPay struct { - AppId string - MechID string - SubMechID string - SerialNo string - ApiV3Key string - PrivateKey string + WechatAppId string + WechatMechID string + WechatSubMechID string + WechatSerialNo string + WechatApiV3Key string + WechatPrivateKey string } diff --git a/backend/providers/pay/provider.go b/backend/providers/pay/provider.go index ed9a1a2..cc1209e 100644 --- a/backend/providers/pay/provider.go +++ b/backend/providers/pay/provider.go @@ -16,11 +16,12 @@ func Provide(opts ...opt.Option) error { return err } return container.Container.Provide(func(app *app.Config) (*Client, error) { + return nil, nil wechatPay, err := wechat.NewClientV3( - config.WeChat.MechID, - config.WeChat.SerialNo, - config.WeChat.ApiV3Key, - config.WeChat.PrivateKey, + config.WechatMechID, + config.WechatSerialNo, + config.WechatApiV3Key, + config.WechatPrivateKey, ) if err != nil { return nil, err diff --git a/backend/providers/pay/wechat.go b/backend/providers/pay/wechat.go index 2987768..0b32ee4 100644 --- a/backend/providers/pay/wechat.go +++ b/backend/providers/pay/wechat.go @@ -16,9 +16,9 @@ func (client *Client) WeChat_JSApiPayRequest(ctx context.Context, payerOpenID, o bm := make(gopay.BodyMap) bm. - Set("sp_appid", client.conf.WeChat.AppId). - Set("sp_mchid", client.conf.WeChat.MechID). - Set("sub_mchid", client.conf.WeChat.SubMechID). + Set("sp_appid", client.conf.WechatAppId). + Set("sp_mchid", client.conf.WechatMechID). + Set("sub_mchid", client.conf.WechatSubMechID). Set("description", title). Set("out_trade_no", orderNo). Set("time_expire", expire). @@ -45,5 +45,5 @@ func (client *Client) WeChat_JSApiPayRequest(ctx context.Context, payerOpenID, o return nil, errors.New("获取预支付ID失败") } - return client.WeChat.PaySignOfJSAPI(client.conf.WeChat.AppId, resp.Response.PrepayId) + return client.WeChat.PaySignOfJSAPI(client.conf.WechatAppId, resp.Response.PrepayId) } diff --git a/frontend/src/components/ChunkUpload.vue b/frontend/src/components/ChunkUpload.vue new file mode 100644 index 0000000..046fc32 --- /dev/null +++ b/frontend/src/components/ChunkUpload.vue @@ -0,0 +1,140 @@ + + + diff --git a/qvyun.code-workspace b/qvyun.code-workspace new file mode 100644 index 0000000..0998f9a --- /dev/null +++ b/qvyun.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "frontend" + }, + { + "path": "backend" + } + ] +}