diff --git a/backend/internal/modules/file/storage.go b/backend/internal/modules/file/storage.go index 13f610e..5bf8fab 100644 --- a/backend/internal/modules/file/storage.go +++ b/backend/internal/modules/file/storage.go @@ -10,6 +10,7 @@ import ( "net/url" "path" "strings" + "sync" "time" "hfb_sys/backend/internal/config" @@ -19,8 +20,10 @@ import ( ) type Storage struct { - client *minio.Client - bucket string + client *minio.Client + bucket string + bucketMu sync.Mutex + bucketReady bool } type Object struct { @@ -43,7 +46,7 @@ func NewStorage(cfg config.StorageConfig) (*Storage, error) { } storage := &Storage{client: client, bucket: cfg.Bucket} if err := storage.ensureBucket(context.Background()); err != nil { - return nil, err + return storage, err } return storage, nil } @@ -62,6 +65,9 @@ func (s *Storage) Put(ctx context.Context, scene string, header *multipart.FileH } func (s *Storage) PutObject(ctx context.Context, key string, reader io.Reader, size int64, contentType string, metadata map[string]string) error { + if err := s.ensureBucketReady(ctx); err != nil { + return err + } _, err := s.client.PutObject(ctx, s.bucket, key, reader, size, minio.PutObjectOptions{ ContentType: contentType, UserMetadata: metadata, @@ -95,9 +101,23 @@ func (s *Storage) ensureBucket(ctx context.Context) error { } } if exists { + s.bucketReady = true return nil } - return s.client.MakeBucket(ctx, s.bucket, minio.MakeBucketOptions{}) + if err := s.client.MakeBucket(ctx, s.bucket, minio.MakeBucketOptions{}); err != nil { + return err + } + s.bucketReady = true + return nil +} + +func (s *Storage) ensureBucketReady(ctx context.Context) error { + s.bucketMu.Lock() + defer s.bucketMu.Unlock() + if s.bucketReady { + return nil + } + return s.ensureBucket(ctx) } func normalizeEndpoint(raw string) (string, bool, error) { diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 35aba6a..9f031aa 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -242,7 +242,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { var err error fileStorage, err = filemodule.NewStorage(cfg.Storage) if err != nil { - logger.Warn("file storage unavailable; file APIs will return 503", zap.Error(err)) + logger.Warn("file storage bucket is not ready; file APIs will retry on request", zap.Error(err)) } } fileService := filemodule.NewService(fileStorage) diff --git a/deploy/caddy/Caddyfile b/deploy/caddy/Caddyfile index dff2704..23dc088 100644 --- a/deploy/caddy/Caddyfile +++ b/deploy/caddy/Caddyfile @@ -17,7 +17,7 @@ X-Frame-Options "DENY" Referrer-Policy "strict-origin-when-cross-origin" Permissions-Policy "camera=(), microphone=(), geolocation=()" - Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'" + Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data: blob:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'" } # 聊天 SSE 长连接:关闭缓冲,逐条推送。 diff --git a/frontend/src/features/listings/views/ListingDetailView.vue b/frontend/src/features/listings/views/ListingDetailView.vue index f2d8d33..13f97b9 100644 --- a/frontend/src/features/listings/views/ListingDetailView.vue +++ b/frontend/src/features/listings/views/ListingDetailView.vue @@ -241,8 +241,8 @@ async function submitOrder() { ordering.value = true try { const order = await createOrder(listing.value.id) - ElMessage.success('订单已创建,请完成支付') - await router.push(`/orders/${order.id}`) + ElMessage.success('订单已创建,正在打开支付') + await router.push({ path: `/orders/${order.id}`, query: { pay: '1' } }) } catch (error) { ElMessage.error(readError(error, '下单失败')) } finally { diff --git a/frontend/src/features/listings/views/MobileListingDetailView.vue b/frontend/src/features/listings/views/MobileListingDetailView.vue index d18adae..639ed3c 100644 --- a/frontend/src/features/listings/views/MobileListingDetailView.vue +++ b/frontend/src/features/listings/views/MobileListingDetailView.vue @@ -231,9 +231,9 @@ async function submitOrder() { if (!listing.value) return ordering.value = true try { - await createOrder(listing.value.id) - showToast({ message: '订单已创建,请完成支付', icon: 'passed' }) - await router.push(`/m/orders`) + const order = await createOrder(listing.value.id) + showToast({ message: '订单已创建,正在打开支付', icon: 'passed' }) + await router.push({ path: `/m/orders/${order.id}`, query: { pay: '1' } }) } catch (error) { showToast({ message: readError(error, '下单失败'), icon: 'cross' }) } finally { diff --git a/frontend/src/features/orders/views/MobileOrderDetailView.vue b/frontend/src/features/orders/views/MobileOrderDetailView.vue index 9920696..490a19b 100644 --- a/frontend/src/features/orders/views/MobileOrderDetailView.vue +++ b/frontend/src/features/orders/views/MobileOrderDetailView.vue @@ -90,6 +90,7 @@ const activeNames = ref([]) const showDisputePopup = ref(false) const showCounterPopup = ref(false) const showRejectPopup = ref(false) +let autoPayHandled = false const isOwner = computed(() => order.value?.owner_id === session.userId) const isRenter = computed(() => order.value?.renter_id === session.userId) @@ -139,6 +140,16 @@ async function loadOrder() { handoffRecords.value = await fetchHandoffRecords(String(route.params.id)) hydrateResourceUsage() hydrateCounterForm() + if ( + !autoPayHandled && + route.query.pay === '1' && + order.value?.status === 'pending_payment' && + order.value?.renter_id === session.userId + ) { + autoPayHandled = true + void router.replace({ path: route.path }) + void handlePay() + } } catch { showToast({ message: '加载订单失败', icon: 'cross' }) } finally { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 86a87bf..cbff8d6 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -33,9 +33,28 @@ function elementPlusChunk(id: string) { return undefined } +function stripVantRemoteIconFont() { + return { + name: 'strip-vant-remote-icon-font', + generateBundle(_options, bundle) { + for (const chunk of Object.values(bundle)) { + if (chunk.type !== 'asset' || typeof chunk.source !== 'string') continue + if (!chunk.fileName.endsWith('.css')) continue + + // Vant 图标字体已经内联了 woff2,这里去掉远程 woff 备用地址,避免触发 CSP 外链字体拦截。 + chunk.source = chunk.source.replace( + /,url\(\/\/at\.alicdn\.com\/t\/c\/font_2553510_ciljc7axaw7\.woff\?t=1705587463221\) format\("woff"\)/g, + '' + ) + } + }, + } +} + export default defineConfig({ plugins: [ vue(), + stripVantRemoteIconFont(), AutoImport({ resolvers: [ElementPlusResolver()], }),