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 @@
+
+
+
+
+ 拖拽文件到此处上传
+
+
+
+
{{ status }}
+
+
+
+
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"
+ }
+ ]
+}