feat:修改docekt配置添加ossCROS的配置
This commit is contained in:
parent
a96c9cf04f
commit
64b501102b
@ -118,4 +118,4 @@ OPENAI_API_KEY=sk-eIOujD5rUugIRIPecFi3I2rFr6Bhxx1jsRzRm6phyNeeKrCI
|
||||
# 微达API BaseURL(必须含 /v1 后缀,代码会拼成 /v1/images/edits)
|
||||
OPENAI_BASE_URL=https://api.weda.cc/v1
|
||||
# 中转站实际暴露的 image 模型
|
||||
OPENAI_MODEL=gpt-image-2
|
||||
OPENAI_MODEL=gpt-image-2
|
||||
|
||||
@ -113,3 +113,7 @@ require (
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
// 让 backend/scripts/set-oss-cors 能 import gateway/config(纯 stdlib,无新增依赖)。
|
||||
// Dockerfile.services 是单 module 编译,不走 go.work,需要这个 replace 桥接。
|
||||
replace github.com/topfans/backend/gateway => ./gateway
|
||||
|
||||
100
backend/scripts/set-oss-cors/main.go
Normal file
100
backend/scripts/set-oss-cors/main.go
Normal file
@ -0,0 +1,100 @@
|
||||
// set-oss-cors/main.go
|
||||
// 给 OSS bucket 设置 CORS 规则,允许浏览器跨域读取预签名 URL。
|
||||
// 背景:
|
||||
// - 前端 useLaserSegment.downloadToLocal 通过 fetch 直连 OSS 拉取 cutout/png
|
||||
// - OSS bucket 未配 CORS 时,浏览器在 CORS 预检后中断响应,
|
||||
// 表现为 net::ERR_CONTENT_LENGTH_MISMATCH 200 (OK) 或 TypeError: Failed to fetch
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// go run scripts/set-oss-cors/main.go
|
||||
//
|
||||
// (会自动从 backend/.env / docker/.env.prod 读取 OSS_REGION / OSS_BUCKET_NAME /
|
||||
// OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET)
|
||||
//
|
||||
// 注意:aliyun-oss-go-sdk v3.0.2 的 CORS 方法挂在 *Client 上,不是 *Bucket。
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/topfans/backend/gateway/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 多路径 fallback:本地 .env / docker/.env.prod / docker/.env / .env
|
||||
for _, p := range []string{
|
||||
"backend/.env",
|
||||
"../.env",
|
||||
".env",
|
||||
"docker/.env.prod",
|
||||
"docker/.env",
|
||||
"../docker/.env.prod",
|
||||
"../docker/.env",
|
||||
} {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
_ = godotenv.Load(p)
|
||||
}
|
||||
}
|
||||
|
||||
cfg := config.Load()
|
||||
|
||||
fmt.Println("=== SetBucketCORS ===")
|
||||
fmt.Printf("Bucket: %s Region: %s\n", cfg.OSS.BucketName, cfg.OSS.Region)
|
||||
fmt.Printf("AK: %s\n", mask(cfg.OSS.AccessKeyID))
|
||||
|
||||
endpoint := fmt.Sprintf("https://oss-%s.aliyuncs.com", cfg.OSS.Region)
|
||||
client, err := oss.New(endpoint, cfg.OSS.AccessKeyID, cfg.OSS.AccessKeySecret)
|
||||
if err != nil {
|
||||
fmt.Printf("FAIL 创建 OSS 客户端: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 全域名 * 放行 (用户确认: top-fans-test 为测试桶, 预签名 URL 仍需签名才能访问)
|
||||
//
|
||||
// ⚠️ 重要: 阿里云 OSS 的 AllowedMethod 不支持 OPTIONS。
|
||||
// 浏览器 CORS 预检的 OPTIONS 请求由 OSS 隐式处理(无需显式列出)。
|
||||
// 加上 OPTIONS 会触发 400 InvalidArgument: "CORS Rule AllowedMethod Format Error"。
|
||||
//
|
||||
// Method 集合:
|
||||
// GET - H5 fetch 拉 cutout / variant PNG (浏览器读图)
|
||||
// HEAD - 上传后取文件 size
|
||||
// POST - H5 PostObject 直传(uni-app H5 在浏览器跑时必须走 POST)
|
||||
rule := oss.CORSRule{
|
||||
AllowedOrigin: []string{"*"},
|
||||
AllowedMethod: []string{"GET", "HEAD", "POST"},
|
||||
AllowedHeader: []string{"*"},
|
||||
ExposeHeader: []string{"ETag", "Content-Length", "Content-Type", "x-oss-request-id"},
|
||||
MaxAgeSeconds: 3600,
|
||||
}
|
||||
|
||||
// SDK v3.0.2: CORS 方法在 *Client 上
|
||||
if err := client.SetBucketCORS(cfg.OSS.BucketName, []oss.CORSRule{rule}); err != nil {
|
||||
fmt.Printf("FAIL SetBucketCORS: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 立即读回验证
|
||||
got, gErr := client.GetBucketCORS(cfg.OSS.BucketName)
|
||||
if gErr != nil {
|
||||
fmt.Printf("WARN GetBucketCORS 回读失败: %v\n", gErr)
|
||||
} else {
|
||||
fmt.Printf("OK 当前 bucket CORS 规则数: %d\n", len(got.CORSRules))
|
||||
for i, r := range got.CORSRules {
|
||||
fmt.Printf(" [%d] Origin=%v Method=%v Header=%v MaxAge=%d\n",
|
||||
i, r.AllowedOrigin, r.AllowedMethod, r.AllowedHeader, r.MaxAgeSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("✅ CORS 配置已生效(可能有几秒传播延迟)")
|
||||
}
|
||||
|
||||
func mask(s string) string {
|
||||
if len(s) <= 8 {
|
||||
return "***"
|
||||
}
|
||||
return s[:4] + "****" + s[len(s)-4:]
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
# ===================================================================
|
||||
# TopFans Docker - 本机开发环境真实凭据(untracked,不进 git)
|
||||
# 由 docker-compose --env-file .env.local --env-file .env.local.dev 叠加读入
|
||||
# ===================================================================
|
||||
|
||||
# 宿主机手起的 topfans-postgres
|
||||
DB_USER=haihuizhu
|
||||
DB_PASSWORD=admin
|
||||
DB_NAME=top-fans
|
||||
DB_PORT=5432
|
||||
DB_HOST=host.docker.internal
|
||||
|
||||
# 宿主机手起的 topfans-redis(重建后通过容器名 topfans-redis:6379 直连)
|
||||
REDIS_HOST=topfans-redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=123456
|
||||
|
||||
# ==================== 镭射卡 OpenAI 中转站路径(覆盖 .env.local 的默认值)====================
|
||||
# 前端 VITE_API_BASE_URL=http://localhost:18090 → 打到 docker gateway (18090)
|
||||
# 所以 LASER_GEN_PROVIDER 必须在这里改,docker-compose.override.yml 第 92 行
|
||||
# LASER_GEN_PROVIDER: ${LASER_GEN_PROVIDER:-minimax}
|
||||
# 会把这里注入到容器,覆盖 .env.local 的 minimax 默认值
|
||||
LASER_GEN_PROVIDER=openai
|
||||
# 微达API BaseURL(必须含 /v1 后缀,代码会拼成 /v1/images/edits)
|
||||
# 直连 OpenAI 官方会被墙,这里走微达API中转
|
||||
OPENAI_BASE_URL=https://api.weda.cc/v1
|
||||
# openai_client.go 的 buildEditFields() 根据 model 名前缀自动选参数集
|
||||
# - gpt-image-* → 完整参数(transparent + 1024x1536 竖版)
|
||||
# - 其他 → 基础参数(1024x1024 square,无 transparent,保守路径)
|
||||
OPENAI_MODEL=gpt-image-2
|
||||
# ⚠️ 微达API key —— 务必先在 https://api.weda.cc 撤销旧 key 再填新的
|
||||
OPENAI_API_KEY=sk-eIOujD5rUugIRIPecFi3I2rFr6Bhxx1jsRzRm6phyNeeKrCI
|
||||
|
||||
# ==================== Dify 配置保留(暂未切回 dify,留着方便回滚)====================
|
||||
# gateway 容器的 DIFY_API_BASE 默认是 https://api.dify.ai/v1(生产 SaaS)
|
||||
# 本机要走自起的 Dify(project=dify,nginx 暴露在 host:80 → /v1 路由到 api:5001)
|
||||
#
|
||||
# ⚠️ 不能用 host.docker.internal!Docker Desktop 的 com.docker.backend.exe
|
||||
# 抢占了 host 的 0.0.0.0:80,host.docker.internal (192.168.65.254) 走不通。
|
||||
# 用 host 真实 IP 172.23.0.1 直连 host:80(Dify nginx bind 在这)
|
||||
DIFY_API_BASE=http://172.23.0.1/v1
|
||||
# 从 Dify 数据库里 laser_card_variants_v1 这个 app 的 api_tokens 表里取出来的
|
||||
DIFY_API_KEY=app-Ibs7reARanyuYGZ7zrLyiM6e
|
||||
|
||||
# ==================== JWT_SECRET ====================
|
||||
# 本机用生产同款 secret,让生产签发的 token 在本机 gateway 也能验签通过
|
||||
# ⚠️ 本地开发用,绝对不要把生产 secret 提交到 git
|
||||
JWT_SECRET=topfans-secret-key-please-change-in-production
|
||||
@ -60,7 +60,10 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \
|
||||
echo "Built notificationservice" && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \
|
||||
-o /tmp/moderationservice services/moderationService/main.go && \
|
||||
echo "Built moderationservice"
|
||||
echo "Built moderationservice" && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \
|
||||
-o /tmp/set-oss-cors scripts/set-oss-cors/main.go && \
|
||||
echo "Built set-oss-cors"
|
||||
|
||||
# ---- Runtime Stage: Gateway ----
|
||||
FROM --platform=linux/amd64 alpine:3.19 AS gateway
|
||||
@ -245,3 +248,13 @@ HEALTHCHECK --interval=10s --timeout=5s --start-period=15s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:20011 || exit 1
|
||||
|
||||
ENTRYPOINT ["/app/moderationservice"]
|
||||
|
||||
# ---- Runtime Stage: OSS CORS Init (一次性,跑完即退,设置 bucket CORS) ----
|
||||
FROM --platform=linux/amd64 alpine:3.19 AS oss-cors-init
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /tmp/set-oss-cors /app/set-oss-cors
|
||||
|
||||
ENTRYPOINT ["/app/set-oss-cors"]
|
||||
|
||||
@ -78,7 +78,7 @@ while [[ $# -gt 0 ]]; do
|
||||
echo "服务名 (可选):"
|
||||
echo " gateway, userService, socialService, assetService,"
|
||||
echo " galleryService, activityService, taskService, starbookService, aiChatService,"
|
||||
echo " laserCompositor, statisticService"
|
||||
echo " statisticService"
|
||||
echo " notificationService, moderationService"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
@ -104,6 +104,7 @@ while [[ $# -gt 0 ]]; do
|
||||
statistic|statisticService|statisticservice) SERVICES+=("statisticservice") ;;
|
||||
notification|notificationService|notificationservice) SERVICES+=("notificationservice") ;;
|
||||
moderation|moderationService|moderationservice) SERVICES+=("moderationservice") ;;
|
||||
oss-cors-init|osS-cors-init|ossCorsInit) SERVICES+=("oss-cors-init") ;;
|
||||
all)
|
||||
# all 关键字,构建所有服务
|
||||
SERVICES=()
|
||||
@ -122,7 +123,7 @@ done
|
||||
|
||||
# ==================== 服务列表 ====================
|
||||
# 所有可用服务及其配置(使用小写 target 名)
|
||||
ALL_SERVICES_NAME=("gateway" "userservice" "socialservice" "assetservice" "galleryservice" "activityservice" "taskservice" "starbookservice" "aichatservice" "lasercompositor" "statisticservice" "notificationservice" "moderationservice")
|
||||
ALL_SERVICES_NAME=("gateway" "userservice" "socialservice" "assetservice" "galleryservice" "activityservice" "taskservice" "starbookservice" "aichatservice" "statisticservice" "notificationservice" "moderationservice" "oss-cors-init")
|
||||
|
||||
# 确定要构建的服务
|
||||
if [ ${#SERVICES[@]} -eq 0 ]; then
|
||||
@ -212,10 +213,10 @@ main() {
|
||||
taskservice) docker_target="taskservice" ;;
|
||||
starbookservice) docker_target="starbookservice" ;;
|
||||
aichatservice) docker_target="aichatservice" ;;
|
||||
lasercompositor) docker_target="lasercompositor" ;;
|
||||
statisticservice) docker_target="statisticservice" ;;
|
||||
notificationservice) docker_target="notificationservice" ;;
|
||||
moderationservice) docker_target="moderationservice" ;;
|
||||
oss-cors-init) docker_target="oss-cors-init" ;;
|
||||
# 兼容旧的大写服务名
|
||||
userService) docker_target="userservice" ;;
|
||||
socialService) docker_target="socialservice" ;;
|
||||
@ -228,6 +229,7 @@ main() {
|
||||
statisticservice) docker_target="statisticservice" ;;
|
||||
notificationService) docker_target="notificationservice" ;;
|
||||
moderationService) docker_target="moderationservice" ;;
|
||||
ossCorsInit) docker_target="oss-cors-init" ;;
|
||||
*) docker_target="$service" ;;
|
||||
esac
|
||||
|
||||
|
||||
@ -79,10 +79,10 @@ SERVICES=(
|
||||
"taskservice"
|
||||
"starbookservice"
|
||||
"aichatservice"
|
||||
"lasercompositor"
|
||||
"statisticservice"
|
||||
"notificationservice"
|
||||
"moderationservice"
|
||||
"oss-cors-init"
|
||||
)
|
||||
|
||||
# ==================== 服务器配置 ====================
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
# ===================================================================
|
||||
# TopFans Docker - 本机基础设施 (postgres + redis)
|
||||
# untracked,不进 git。和 docker-compose.local.yml 一起用:
|
||||
# docker compose \
|
||||
# -f docker-compose.local.yml \
|
||||
# -f docker-compose.override.yml \
|
||||
# -f docker-compose.infra.yml \
|
||||
# --env-file .env --env-file .env.local --env-file .env.local.dev \
|
||||
# up -d
|
||||
# ===================================================================
|
||||
|
||||
# 用 named volume(topfans-postgres-data / topfans-redis-data),
|
||||
# 启动前需要从匿名 volume 迁移数据:
|
||||
# docker volume create topfans-postgres-data
|
||||
# docker run --rm -v 9d506bae...:/from -v topfans-postgres-data:/to \
|
||||
# alpine sh -c "cp -a /from/. /to/ && rm -f /to/postmaster.pid"
|
||||
# docker volume create topfans-redis-data
|
||||
# docker run --rm -v 06828e1c...:/from -v topfans-redis-data:/to \
|
||||
# alpine sh -c "cp -a /from/. /to/"
|
||||
|
||||
volumes:
|
||||
# external: true 因为这两个 volume 是手动创建并迁移数据的
|
||||
# 后续如果想让 compose 管理,可以去掉 external 并 docker volume rm 后重启
|
||||
topfans-postgres-data:
|
||||
name: topfans-postgres-data
|
||||
external: true
|
||||
topfans-redis-data:
|
||||
name: topfans-redis-data
|
||||
external: true
|
||||
|
||||
networks:
|
||||
topfans-net:
|
||||
name: docker_topfans-net # 跟 business compose 共享同一个网络
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
container_name: topfans-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: haihuizhu
|
||||
POSTGRES_PASSWORD: admin
|
||||
POSTGRES_DB: top-fans
|
||||
ports:
|
||||
# 保留 host port 5432 映射,兼容 .env.local.dev 里 DB_HOST=host.docker.internal:5432
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- topfans-postgres-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- topfans-net
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U haihuizhu -d top-fans"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
redis:
|
||||
image: redis:7.4.9
|
||||
container_name: topfans-redis
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--requirepass", "123456"]
|
||||
# 注意:不映射 host port 6379(被 txw-cloud-redis 占了)
|
||||
# 业务容器通过 docker_topfans-net 网络用容器名 topfans-redis:6379 直连
|
||||
volumes:
|
||||
- topfans-redis-data:/data
|
||||
networks:
|
||||
- topfans-net
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "123456", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
@ -1,96 +0,0 @@
|
||||
# ===================================================================
|
||||
# TopFans Docker - 本机开发环境 service 级 override(untracked,不进 git)
|
||||
# 与 docker-compose.local.yml 合并;只覆盖本机需要的字段
|
||||
# 用法:
|
||||
# docker compose -f docker-compose.local.yml -f docker-compose.override.yml \
|
||||
# --env-file .env --env-file .env.local --env-file .env.local.dev up -d
|
||||
# ===================================================================
|
||||
|
||||
# 覆盖所有业务的 DB/REDIS 凭据(指向本机手起的容器)
|
||||
# 注意:这里只覆盖 environment,不重建容器,其它字段都继承自 compose
|
||||
x-override-env: &override-env
|
||||
DB_HOST: ${DB_HOST}
|
||||
DB_PORT: ${DB_PORT}
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
DB_NAME: ${DB_NAME}
|
||||
REDIS_HOST: ${REDIS_HOST}
|
||||
REDIS_PORT: ${REDIS_PORT}
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
# SMS dummy (本机不发短信,绕过启动期 FATAL)
|
||||
SMS_ACCESS_KEY_ID: ${SMS_ACCESS_KEY_ID:-dummy}
|
||||
SMS_ACCESS_KEY_SECRET: ${SMS_ACCESS_KEY_SECRET:-dummy}
|
||||
# JWT_SECRET:让所有 Dubbo service(userservice 等签 token 的)
|
||||
# 跟 gateway 验签用同一个 secret,否则会 401
|
||||
# ⚠️ 必须跟 .env.local.dev 里 JWT_SECRET 的值匹配
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
services:
|
||||
userservice:
|
||||
environment:
|
||||
<<: *override-env
|
||||
|
||||
assetservice:
|
||||
environment:
|
||||
<<: *override-env
|
||||
# assetService 读 USER_SERVICE_URL(不是 DUBBO_USER_SERVICE_URL),
|
||||
# compose 原件设的是 DUBBO_USER_SERVICE_URL,导致 fallback 到
|
||||
# tri://localhost:20000 连不上 userservice → crystal_balance 返回 0
|
||||
USER_SERVICE_URL: tri://userservice:20000
|
||||
|
||||
socialservice:
|
||||
environment:
|
||||
<<: *override-env
|
||||
|
||||
galleryservice:
|
||||
environment:
|
||||
<<: *override-env
|
||||
|
||||
activityservice:
|
||||
environment:
|
||||
<<: *override-env
|
||||
|
||||
taskservice:
|
||||
environment:
|
||||
<<: *override-env
|
||||
|
||||
starbookservice:
|
||||
environment:
|
||||
<<: *override-env
|
||||
|
||||
aichatservice:
|
||||
environment:
|
||||
<<: *override-env
|
||||
|
||||
|
||||
statisticservice:
|
||||
# statisticservice 用自己一套 STATISTIC_* 变量,跟 x-common-env 不同
|
||||
environment:
|
||||
STATISTIC_DB_HOST: ${DB_HOST}
|
||||
STATISTIC_DB_PORT: ${DB_PORT}
|
||||
STATISTIC_DB_USER: ${DB_USER}
|
||||
STATISTIC_DB_PASSWORD: ${DB_PASSWORD}
|
||||
STATISTIC_DB_NAME: ${DB_NAME}
|
||||
STATISTIC_REDIS_HOST: ${REDIS_HOST}
|
||||
STATISTIC_REDIS_PORT: ${REDIS_PORT}
|
||||
STATISTIC_REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
|
||||
gateway:
|
||||
environment:
|
||||
<<: *override-env
|
||||
# gateway 决定镭射卡走哪条路径。compose 原件没引用这个变量,
|
||||
# 这里补上,让 .env.local.dev 里的 LASER_GEN_PROVIDER 真正进容器
|
||||
# (LASER_GEN_PROVIDER 的值由 .env.local.dev 控制,默认 openai)
|
||||
LASER_GEN_PROVIDER: ${LASER_GEN_PROVIDER:-openai}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.weda.cc/v1}
|
||||
OPENAI_MODEL: ${OPENAI_MODEL:-gpt-image-2}
|
||||
# 本机用生产同款 JWT_SECRET(生产 token 能直接用本机 gateway 验签)
|
||||
# ⚠️ 仅本地开发用,生产不要用这个 secret
|
||||
JWT_SECRET: ${JWT_SECRET:-topfans-secret-key-local-dev-only}
|
||||
# 覆盖 host port 映射:宿主 8080/8090 被 Windows/Docker Desktop 锁
|
||||
# (PowerShell/netstat 都看不到进程,但 docker run 也 bind 失败),
|
||||
# 改用 18090:8080。18080 被 txw-cloud-nacos 占了,18090 跟它成一对好记。
|
||||
# compose 默认会 merge list,用 !override 强制替换 base 的 ["8080:8080"]
|
||||
ports: !override
|
||||
- "18090:8080"
|
||||
@ -564,6 +564,24 @@ services:
|
||||
memory: 128M
|
||||
cpus: '0.25'
|
||||
|
||||
# ==================== OSS CORS Init (一次性,跑完即退) ====================
|
||||
# 部署时自动调阿里云 OSS API 给 bucket 推 CORS 规则(POST 直传必需)。
|
||||
# restart: "no" 保证只跑一次,失败会显式退出非 0,deploy.sh 能看到错误。
|
||||
# 复用 .env.prod 里现有的 OSS_REGION / OSS_BUCKET_NAME / OSS_ACCESS_KEY_*,
|
||||
# 无需新增任何环境变量。
|
||||
oss-cors-init:
|
||||
image: topfans/oss-cors-init:latest
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile.services
|
||||
target: oss-cors-init
|
||||
container_name: topfans-oss-cors-init
|
||||
restart: "no"
|
||||
env_file:
|
||||
- .env.prod
|
||||
networks:
|
||||
- topfans-net
|
||||
|
||||
# ==================== API Gateway ====================
|
||||
gateway:
|
||||
image: topfans/gateway:latest
|
||||
@ -589,11 +607,12 @@ services:
|
||||
DUBBO_STATISTIC_SERVICE_URL: tri://statisticservice:20009
|
||||
DUBBO_NOTIFICATION_SERVICE_URL: tri://notificationservice:20010
|
||||
DUBBO_MODERATION_SERVICE_URL: tri://moderationservice:20011
|
||||
LASER_COMPOSITOR_URL: http://lasercompositor:7002
|
||||
# 镭射卡 AI 生成(OpenAI 中转站 — 微达API,通过 .env.prod 注入 API Key)
|
||||
# 注意:不能在 environment: 块里用 ${OPENAI_API_KEY:-} 这种 shell 变量展开语法,
|
||||
# 因为这会读取 HOST shell 的环境变量,而不是 .env.prod 里的值,且会覆盖 env_file。
|
||||
# OPENAI_API_KEY 必须且只能从 env_file: .env.prod 读取。
|
||||
LASER_GEN_PROVIDER: ${LASER_GEN_PROVIDER:-openai}
|
||||
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.weda.cc/v1}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||
OPENAI_MODEL: ${OPENAI_MODEL:-gpt-image-2}
|
||||
# 抠图(人像扣底)、OSS、Dify、JWT、Redis 全部走 env_file: .env.prod
|
||||
REDIS_HOST: topfans-redis
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
# 开发环境配置
|
||||
# HBuilderX「运行」时自动加载;CLI 用 --mode development
|
||||
VITE_API_BASE_URL=http://192.168.110.60:8080
|
||||
# VITE_API_BASE_URL=https://api.topfans.online
|
||||
# VITE_API_BASE_URL=http://192.168.110.60:8080
|
||||
VITE_API_BASE_URL=https://api.topfans.online
|
||||
# WebSocket 地址:如与 API 同源可省略(自动从 VITE_API_BASE_URL 推导 http→ws、https→wss)
|
||||
# 独立部署时直接覆盖,例如:ws://192.168.110.60:8081
|
||||
VITE_WS_BASE_URL=ws://192.168.110.60:8080
|
||||
# VITE_WS_BASE_URL=wss://api.topfans.online
|
||||
# VITE_WS_BASE_URL=ws://192.168.110.60:8080
|
||||
VITE_WS_BASE_URL=wss://api.topfans.online
|
||||
# WebSocket 路径:用于 Nginx 反向代理(前端连接的完整 URL = VITE_WS_BASE_URL + VITE_WS_AI_CHAT_PATH)
|
||||
# 需与后端 backend/.env 的 WS_AI_CHAT_PATH 保持一致
|
||||
# Nginx 示例:location /ai-chat { proxy_pass http://gateway:8080; ... }
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
{
|
||||
"name" : "TopFans",
|
||||
"appid" : "__UNI__8CBE431",
|
||||
"appid" : "__UNI__F199FF4",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.5",
|
||||
"versionCode" : 115,
|
||||
"versionCode" : 117,
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
|
||||
@ -738,7 +738,11 @@ function originalFileName(filePath) {
|
||||
if (filePath.startsWith('data:')) {
|
||||
return 'image.jpg'
|
||||
}
|
||||
return filePath.split('/').pop() || 'image.jpg'
|
||||
// 先剥掉 query string 和 hash 片段,避免传入 signed URL 时
|
||||
// (?Expires=&Signature=&security-token=...) 把 original_name 撑爆
|
||||
// 触发 materials.original_name VARCHAR(255) 越界 (SQLSTATE 22001)
|
||||
const cleaned = filePath.split('?')[0].split('#')[0]
|
||||
return cleaned.split('/').pop() || 'image.jpg'
|
||||
} catch {
|
||||
return 'image.jpg'
|
||||
}
|
||||
|
||||
@ -1,26 +1,16 @@
|
||||
/**
|
||||
* H5 本地开发:将 OSS Post 地址换为同源 /dev-oss-proxy,由 vite.config.js 中中间件转发到 OSS 桶根 POST /,
|
||||
* 避免浏览器从 localhost / 局域网端口直连 *.aliyuncs.com 触发 CORS。
|
||||
* H5 OSS PostObject 直传 URL 解析
|
||||
*
|
||||
* 正式 H5 部署到业务域名时仍直连 ossData.host;若遇 CORS,请在 OSS 控制台配置 CORS 或使用与站点同域的反代。
|
||||
* 项目为 uni-app 移动端,本就走 OSS 直传(uni.uploadFile / fetch 直连 OSS 桶)。
|
||||
* 原 dev 模式下的 /dev-oss-proxy Vite 中间件转发方案已废弃(无 vite.config.js),
|
||||
* 统一在 OSS 桶侧配 CORS 放行 POST 即可(Origin=*, Method=GET/HEAD/POST, Header=*)。
|
||||
*
|
||||
* 保留本文件作为单一改写入口,后续如需按环境切换 URL 在这里集中处理。
|
||||
*
|
||||
* @param {string} ossHost 签名接口返回的 host,如 https://bucket.oss-cn-shanghai.aliyuncs.com
|
||||
* @returns {string} 实际用于 fetch POST 的 URL(开发态为同源代理前缀)
|
||||
* @returns {string} 实际用于 POST 的 URL(目前恒等于 ossHost)
|
||||
*/
|
||||
export function resolveH5OssPostUrl(ossHost) {
|
||||
if (!ossHost || typeof location === 'undefined') {
|
||||
return ossHost
|
||||
}
|
||||
const port = String(location.port || '')
|
||||
const hn = location.hostname || ''
|
||||
const devPortOk = ['5173', '5174', '8080', '9528'].includes(port)
|
||||
const devHostOk =
|
||||
hn === 'localhost' ||
|
||||
hn === '127.0.0.1' ||
|
||||
/^192\.168\./.test(hn) ||
|
||||
/^10\./.test(hn)
|
||||
if (devPortOk && devHostOk) {
|
||||
return `${location.protocol}//${location.host}/dev-oss-proxy`
|
||||
}
|
||||
if (!ossHost) return ossHost
|
||||
return ossHost
|
||||
}
|
||||
|
||||
@ -1,90 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import https from 'node:https'
|
||||
import { createRequire } from 'node:module'
|
||||
// @dcloudio/vite-plugin-uni v3 alpha 的 dist 是 CJS,
|
||||
// 且 __esModule 标志为非枚举,Node 的 CJS-to-ESM 互操作
|
||||
// 不会把它当 TS 编译产物,于是 `import uni from ...` 拿到的
|
||||
// 是整个 module.exports 对象而不是函数,调用就报
|
||||
// "uni is not a function"。用 createRequire 直走 CJS 拿 default。
|
||||
const require = createRequire(import.meta.url)
|
||||
const uni = require('@dcloudio/vite-plugin-uni').default
|
||||
|
||||
/** 与 upload-signature 返回的 OSS 虚拟域名一致;换桶时请同步 */
|
||||
const OSS_DEV_HOST = 'top-fans-test.oss-cn-shanghai.aliyuncs.com'
|
||||
|
||||
/**
|
||||
* 部分环境下 Vite 内置 server.proxy 的 rewrite 对 POST 未生效,OSS 仍收到路径
|
||||
* /dev-oss-proxy,从而 405(ResourceType: Object)。此处用中间件固定转发到桶根 POST /。
|
||||
*/
|
||||
function ossDevPostProxyPlugin() {
|
||||
return {
|
||||
name: 'oss-dev-post-proxy',
|
||||
configureServer(server) {
|
||||
server.middlewares.use((req, res, next) => {
|
||||
const raw = req.url || ''
|
||||
if (!raw.startsWith('/dev-oss-proxy')) {
|
||||
return next()
|
||||
}
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.statusCode = 204
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
|
||||
res.setHeader(
|
||||
'Access-Control-Allow-Headers',
|
||||
String(req.headers['access-control-request-headers'] || '*')
|
||||
)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
if (req.method !== 'POST') {
|
||||
res.statusCode = 405
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
||||
res.end('OSS dev proxy only allows POST')
|
||||
return
|
||||
}
|
||||
const hop = new Set([
|
||||
'connection',
|
||||
'keep-alive',
|
||||
'proxy-authenticate',
|
||||
'proxy-authorization',
|
||||
'te',
|
||||
'trailers',
|
||||
'transfer-encoding',
|
||||
'upgrade'
|
||||
])
|
||||
/** @type {import('node:http').OutgoingHttpHeaders} */
|
||||
const headers = {}
|
||||
for (const [k, v] of Object.entries(req.headers)) {
|
||||
if (v === undefined || hop.has(k.toLowerCase())) continue
|
||||
headers[k] = v
|
||||
}
|
||||
headers.host = OSS_DEV_HOST
|
||||
const proxyReq = https.request(
|
||||
{
|
||||
hostname: OSS_DEV_HOST,
|
||||
port: 443,
|
||||
method: 'POST',
|
||||
path: '/',
|
||||
headers
|
||||
},
|
||||
(proxyRes) => {
|
||||
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers)
|
||||
proxyRes.pipe(res)
|
||||
}
|
||||
)
|
||||
proxyReq.on('error', (err) => {
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 502
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
||||
}
|
||||
res.end(err.message)
|
||||
})
|
||||
req.pipe(proxyReq)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [ossDevPostProxyPlugin(), uni()]
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user