diff --git a/backend/app/http/admin/medias.go b/backend/app/http/admin/medias.go
index 28ad3b8..03b499e 100644
--- a/backend/app/http/admin/medias.go
+++ b/backend/app/http/admin/medias.go
@@ -3,12 +3,15 @@ package admin
import (
"quyun/app/models"
"quyun/app/requests"
+ "quyun/providers/ali"
"github.com/gofiber/fiber/v3"
)
// @provider
-type medias struct{}
+type medias struct {
+ oss *ali.OSSClient
+}
// List medias
// @Router /v1/admin/medias [get]
@@ -18,3 +21,20 @@ func (ctl *medias) List(ctx fiber.Ctx, pagination *requests.Pagination, query *L
cond := models.Medias.BuildConditionWithKey(query.Keyword)
return models.Medias.List(ctx.Context(), pagination, cond)
}
+
+// Show media
+// @Router /v1/admin/medias/:id [get]
+// @Bind id path
+func (ctl *medias) Show(ctx fiber.Ctx, id int64) error {
+ media, err := models.Medias.GetByID(ctx.Context(), id)
+ if err != nil {
+ return ctx.SendString("Media not found")
+ }
+
+ url, err := ctl.oss.GetSignedUrl(ctx.Context(), media.Path)
+ if err != nil {
+ return err
+ }
+
+ return ctx.Redirect().To(url)
+}
diff --git a/backend/app/http/admin/posts.go b/backend/app/http/admin/posts.go
index 841e9c7..7f59ca9 100644
--- a/backend/app/http/admin/posts.go
+++ b/backend/app/http/admin/posts.go
@@ -28,6 +28,7 @@ func (ctl *posts) List(ctx fiber.Ctx, pagination *requests.Pagination, query *Li
type PostForm struct {
Title string `json:"title"`
+ HeadImage int64 `json:"head_image"`
Price int64 `json:"price"`
Discount int16 `json:"discount"`
Introduction string `json:"introduction"`
diff --git a/backend/app/http/admin/provider.gen.go b/backend/app/http/admin/provider.gen.go
index 61e8afd..938ed8e 100755
--- a/backend/app/http/admin/provider.gen.go
+++ b/backend/app/http/admin/provider.gen.go
@@ -23,8 +23,12 @@ func Provide(opts ...opt.Option) error {
}); err != nil {
return err
}
- if err := container.Container.Provide(func() (*medias, error) {
- obj := &medias{}
+ if err := container.Container.Provide(func(
+ oss *ali.OSSClient,
+ ) (*medias, error) {
+ obj := &medias{
+ oss: oss,
+ }
return obj, nil
}); err != nil {
diff --git a/backend/app/http/admin/routes.gen.go b/backend/app/http/admin/routes.gen.go
index cae2dc4..cdc5b81 100644
--- a/backend/app/http/admin/routes.gen.go
+++ b/backend/app/http/admin/routes.gen.go
@@ -45,6 +45,11 @@ func (r *Routes) Register(router fiber.Router) {
Query[ListQuery]("query"),
))
+ router.Get("/v1/admin/medias/:id", Func1(
+ r.medias.Show,
+ PathParam[int64]("id"),
+ ))
+
// 注册路由组: orders
router.Get("/v1/admin/orders", DataFunc2(
r.orders.List,
@@ -81,10 +86,11 @@ func (r *Routes) Register(router fiber.Router) {
))
// 注册路由组: uploads
- router.Get("/v1/admin/uploads/pre-uploaded-check/:md5.:ext", DataFunc2(
+ router.Get("/v1/admin/uploads/pre-uploaded-check/:md5.:ext", DataFunc3(
r.uploads.PreUploadCheck,
PathParam[string]("md5"),
PathParam[string]("ext"),
+ QueryParam[string]("mime"),
))
router.Post("/v1/admin/uploads/post-uploaded-action", Func1(
diff --git a/backend/app/http/admin/uploads.go b/backend/app/http/admin/uploads.go
index 6e65cee..abfd882 100644
--- a/backend/app/http/admin/uploads.go
+++ b/backend/app/http/admin/uploads.go
@@ -32,10 +32,11 @@ type PreCheckResp struct {
// @Router /v1/admin/uploads/pre-uploaded-check/:md5.:ext [get]
// @Bind md5 path
// @Bind ext path
-func (up *uploads) PreUploadCheck(ctx fiber.Ctx, md5, ext string) (*PreCheckResp, error) {
+// @Bind mime query
+func (up *uploads) PreUploadCheck(ctx fiber.Ctx, md5, ext, mime string) (*PreCheckResp, error) {
_, err := models.Medias.GetByHash(ctx.Context(), md5)
if err != nil && errors.Is(err, qrm.ErrNoRows) {
- preSign, err := up.oss.PreSignUpload(ctx.Context(), fmt.Sprintf("%s.%s", md5, ext))
+ preSign, err := up.oss.PreSignUpload(ctx.Context(), fmt.Sprintf("%s.%s", md5, ext), mime)
if err != nil {
return nil, err
}
diff --git a/backend/app/http/routes.gen.go b/backend/app/http/routes.gen.go
index dbb0dd8..8cb323b 100644
--- a/backend/app/http/routes.gen.go
+++ b/backend/app/http/routes.gen.go
@@ -68,9 +68,10 @@ func (r *Routes) Register(router fiber.Router) {
Query[ListQuery]("query"),
))
- router.Get("/api/posts/buy/:id", Func1(
+ router.Get("/api/posts/buy/:id", DataFunc2(
r.posts.Buy,
PathParam[int64]("id"),
+ Local[*model.Users]("user"),
))
}
diff --git a/backend/app/jobs/provider.gen.go b/backend/app/jobs/provider.gen.go
index 4982468..ce2eb79 100755
--- a/backend/app/jobs/provider.gen.go
+++ b/backend/app/jobs/provider.gen.go
@@ -2,6 +2,7 @@ package jobs
import (
"quyun/providers/ali"
+ "quyun/providers/app"
"quyun/providers/job"
"github.com/riverqueue/river"
@@ -40,9 +41,49 @@ func Provide(opts ...opt.Option) error {
}
if err := container.Container.Provide(func(
__job *job.Job,
+ app *app.Config,
+ job *job.Job,
oss *ali.OSSClient,
) (contracts.Initial, error) {
obj := &DownloadFromAliOSSWorker{
+ app: app,
+ job: job,
+ oss: oss,
+ }
+ if err := river.AddWorkerSafely(__job.Workers, obj); err != nil {
+ return nil, err
+ }
+
+ return obj, nil
+ }, atom.GroupInitial); err != nil {
+ return err
+ }
+ if err := container.Container.Provide(func(
+ __job *job.Job,
+ job *job.Job,
+ oss *ali.OSSClient,
+ ) (contracts.Initial, error) {
+ obj := &ExtractAudioFromVideoWorker{
+ job: job,
+ oss: oss,
+ }
+ if err := river.AddWorkerSafely(__job.Workers, obj); err != nil {
+ return nil, err
+ }
+
+ return obj, nil
+ }, atom.GroupInitial); err != nil {
+ return err
+ }
+ if err := container.Container.Provide(func(
+ __job *job.Job,
+ app *app.Config,
+ job *job.Job,
+ oss *ali.OSSClient,
+ ) (contracts.Initial, error) {
+ obj := &ExtractHeadImageFromVideoWorker{
+ app: app,
+ job: job,
oss: oss,
}
if err := river.AddWorkerSafely(__job.Workers, obj); err != nil {
diff --git a/backend/app/models/medias.go b/backend/app/models/medias.go
index 2e4215a..3189c7b 100644
--- a/backend/app/models/medias.go
+++ b/backend/app/models/medias.go
@@ -241,8 +241,8 @@ func (m *mediasModel) GetByID(ctx context.Context, id int64) (*model.Medias, err
m.log.Infof("sql: %s", stmt.DebugSql())
var media model.Medias
- err := stmt.QueryContext(ctx, db, &media)
- if err != nil {
+
+ if err := stmt.QueryContext(ctx, db, &media); err != nil {
m.log.Errorf("error querying media item by ID: %v", err)
return nil, err
}
diff --git a/backend/config.toml b/backend/config.toml
index 8312165..c2b6e6c 100644
--- a/backend/config.toml
+++ b/backend/config.toml
@@ -28,7 +28,7 @@ DB = 0
AccessKeyId = "LTAI5t86SjiP9zRd3q2w7jQN"
AccessKeySecret = "hV7spvJuWh8w0EEIXj8NFi2uBlF4aS"
Bucket ="rogee-test"
-#Host ="abc"
+Host ="https://assets.jdwan.com"
Region ="cn-beijing"
CallbackURL = "https://www.baidu.com"
diff --git a/backend/database/migrations/20250322100215_create_posts.sql b/backend/database/migrations/20250322100215_create_posts.sql
index 2b28a7f..4bd6b0f 100644
--- a/backend/database/migrations/20250322100215_create_posts.sql
+++ b/backend/database/migrations/20250322100215_create_posts.sql
@@ -7,6 +7,7 @@ CREATE TABLE posts(
deleted_at timestamp,
status int2 NOT NULL DEFAULT 0,
title varchar(128) NOT NULL,
+ head_image int8 NOT NULL DEFAULT 0,
description varchar(256) NOT NULL,
content text NOT NULL,
price int8 NOT NULL DEFAULT 0,
diff --git a/backend/providers/ali/config.go b/backend/providers/ali/config.go
index 5f19875..35de32a 100644
--- a/backend/providers/ali/config.go
+++ b/backend/providers/ali/config.go
@@ -41,7 +41,7 @@ func Provide(opts ...opt.Option) error {
WithRegion(config.Region)
return &OSSClient{
- client: oss.NewClient(cfg),
+ client: oss.NewClient(cfg.WithUseCName(true).WithEndpoint(*config.Host)),
internalClient: oss.NewClient(cfg.WithUseInternalEndpoint(true)),
config: &config,
}, nil
diff --git a/backend/providers/ali/oss_client.go b/backend/providers/ali/oss_client.go
index c98e942..47f1116 100644
--- a/backend/providers/ali/oss_client.go
+++ b/backend/providers/ali/oss_client.go
@@ -3,6 +3,7 @@ package ali
import (
"context"
"strings"
+ "time"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
)
@@ -17,11 +18,11 @@ func (c *OSSClient) GetClient() *oss.Client {
return c.client
}
-func (c *OSSClient) PreSignUpload(ctx context.Context, path string) (*oss.PresignResult, error) {
+func (c *OSSClient) PreSignUpload(ctx context.Context, path, mimeType string) (*oss.PresignResult, error) {
request := &oss.PutObjectRequest{
Bucket: oss.Ptr(c.config.Bucket),
Key: oss.Ptr("quyun/" + strings.Trim(path, "/")),
- ContentType: oss.Ptr("multipart/form-data"),
+ ContentType: oss.Ptr(mimeType),
}
return c.client.Presign(ctx, request)
}
@@ -38,3 +39,17 @@ func (c *OSSClient) Download(ctx context.Context, path, dest string) error {
}
return nil
}
+
+// GetSignedUrl
+func (c *OSSClient) GetSignedUrl(ctx context.Context, path string) (string, error) {
+ request := &oss.GetObjectRequest{
+ Bucket: oss.Ptr(c.config.Bucket),
+ Key: oss.Ptr(path),
+ }
+
+ preSign, err := c.client.Presign(ctx, request, oss.PresignExpires(time.Minute*5))
+ if err != nil {
+ return "", err
+ }
+ return preSign.URL, nil
+}
diff --git a/backend/test.http b/backend/test.http
index be8b5bc..11aa9d9 100644
--- a/backend/test.http
+++ b/backend/test.http
@@ -65,4 +65,9 @@ Content-Type: application/json
### precheck
GET {{host}}/v1/admin/uploads/pre-uploaded-check/abc.mp4 HTTP/1.1
Content-Type: application/json
-authorization: {{token}}
\ No newline at end of file
+authorization: {{token}}
+
+
+### get media url
+GET {{host}}/v1/admin/medias/6 HTTP/1.1
+Authorization: {{token}}
\ No newline at end of file
diff --git a/frontend/admin/src/api/mediaService.js b/frontend/admin/src/api/mediaService.js
index 0625f38..1c311cb 100644
--- a/frontend/admin/src/api/mediaService.js
+++ b/frontend/admin/src/api/mediaService.js
@@ -11,8 +11,10 @@ export const mediaService = {
return httpClient.post('/admin/medias', mediaInfo);
},
- preUploadedCheck(md5, ext) {
- return httpClient.get(`/admin/uploads/pre-uploaded-check/${md5}.${ext}`);
+ preUploadedCheck(md5, ext, mime) {
+ return httpClient.get(`/admin/uploads/pre-uploaded-check/${md5}.${ext}`, {
+ params: { mime }
+ });
},
uploadedSuccess(data) {
diff --git a/frontend/admin/src/pages/MediaUploadPage.vue b/frontend/admin/src/pages/MediaUploadPage.vue
index 79ef0ea..f2b63a9 100644
--- a/frontend/admin/src/pages/MediaUploadPage.vue
+++ b/frontend/admin/src/pages/MediaUploadPage.vue
@@ -99,8 +99,10 @@ const processNextUpload = async () => {
const md5Hash = await calculateMD5(nextFile.file);
// get file ext
const fileExt = nextFile.file.name.split('.').pop();
+
+ console.log(nextFile.file)
// Check if file exists before upload
- const checkResult = await mediaService.preUploadedCheck(md5Hash, fileExt);
+ const checkResult = await mediaService.preUploadedCheck(md5Hash, fileExt, nextFile.file.type);
if (checkResult.data.exists) {
// Skip upload and mark as completed
diff --git a/frontend/admin/src/pages/PostCreatePage.vue b/frontend/admin/src/pages/PostCreatePage.vue
index b419c0c..dc5ed63 100644
--- a/frontend/admin/src/pages/PostCreatePage.vue
+++ b/frontend/admin/src/pages/PostCreatePage.vue
@@ -30,6 +30,7 @@ const post = reactive({
selectedMedia: [],
medias: [],
status: 0,
+ head_image: null, // Add head image field
});
// Validation state
@@ -37,7 +38,8 @@ const errors = reactive({
title: '',
introduction: '',
selectedMedia: '',
- discount: ''
+ discount: '',
+ head_image: '', // Add head image error field
});
// Media selection dialog state
@@ -46,6 +48,7 @@ const selectedMediaItems = ref([]);
const mediaLoading = ref(false);
const mediaGlobalFilter = ref('');
const mediaItems = ref([]);
+const isHeadImageSelection = ref(false); // Track if we're selecting head image
// Add pagination state for media dialog
const mediaFirst = ref(0);
@@ -63,8 +66,18 @@ const statusOptions = [
{ label: '草稿', value: 0 }
];
+// Open media selection dialog for head image
+const openHeadImageDialog = () => {
+ isHeadImageSelection.value = true;
+ mediaDialogVisible.value = true;
+ mediaCurrentPage.value = 1;
+ mediaFirst.value = 0;
+ loadMediaItems();
+};
+
// Open media selection dialog
const openMediaDialog = () => {
+ isHeadImageSelection.value = false;
mediaDialogVisible.value = true;
mediaCurrentPage.value = 1;
mediaFirst.value = 0;
@@ -99,10 +112,16 @@ const onMediaPage = (event) => {
// Confirm media selection
const confirmMediaSelection = () => {
- if (selectedMediaItems.value.length) {
+ if (isHeadImageSelection.value) {
+ if (selectedMediaItems.value.length) {
+ post.head_image = selectedMediaItems.value[0];
+ errors.head_image = '';
+ }
+ } else if (selectedMediaItems.value.length) {
post.selectedMedia = [...selectedMediaItems.value];
errors.selectedMedia = '';
}
+ selectedMediaItems.value = [];
mediaDialogVisible.value = false;
};
@@ -119,6 +138,11 @@ const removeMedia = (media) => {
}
};
+// Remove head image
+const removeHeadImage = () => {
+ post.head_image = null;
+};
+
// Save the post
const savePost = async () => {
// Reset errors
@@ -149,13 +173,19 @@ const savePost = async () => {
valid = false;
}
+ if (!post.head_image) {
+ errors.head_image = '请选择封面图片';
+ valid = false;
+ }
+
if (!valid) {
toast.add({ severity: 'error', summary: '表单错误', detail: '请检查表单中的错误并修正', life: 3000 });
return;
}
try {
- post.medias = post.selectedMedia.map(media => media.id)
+ post.medias = post.selectedMedia.map(media => media.id);
+ post.head_image_id = post.head_image.id; // Add head image ID to submission
const resp = await postService.createPost(post);
console.log(resp)
@@ -229,6 +259,34 @@ const formatFileSize = (bytes) => {
+
+
+
+
+
+
+
请选择封面图片
+
+
{{ errors.head_image }}
+
+
+
+
![]()
+
+
{{ post.head_image.name }}
+
+
+
+
+
+
+
+
+
+
@@ -316,15 +374,15 @@ const formatFileSize = (bytes) => {
-