From 32d88f42e7b9ced025333ff7d003f4baca298006 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 15 Jul 2026 12:39:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20MinIO=20=E7=AB=AF=E5=8F=A3?= =?UTF-8?q?=E5=86=B2=E7=AA=81=E5=B9=B6=E4=BC=98=E5=8C=96=E9=A1=B6=E6=A0=8F?= =?UTF-8?q?=20IP=20=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MinIO 宿主机端口改为 9100/9101,避免与 hfb-minio 冲突 - 测试与配置默认 PublicBase 同步为 9100 - 工作台顶栏固定展示 IP|地区,详情回填会话元数据 - 种子数据补充访客 IP/地区示例 --- Makefile | 2 +- docker-compose.dev.yml | 9 ++-- server/cmd/seed/main.go | 12 ++--- server/internal/config/config.go | 9 ++-- .../handler/message_validation_test.go | 4 +- .../handler/security_integration_test.go | 4 +- server/internal/storage/s3.go | 2 +- web/src/pages/agent/Dashboard.tsx | 53 +++++++++---------- 8 files changed, 49 insertions(+), 46 deletions(-) diff --git a/Makefile b/Makefile index 4cfda94..a2b2fac 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ db-up: docker compose -f docker-compose.dev.yml up -d @sleep 3 @echo "PostgreSQL: localhost:5433" - @echo "MinIO API: localhost:9000 (控制台 :9001 minioadmin/minioadmin)" + @echo "MinIO API: localhost:9100 (控制台 :9101 minioadmin/minioadmin)" # 停止数据库 db-down: diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 565bf58..2d2ffc1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -18,18 +18,21 @@ services: environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin + # 避开本机其它 MinIO(如 hfb-minio 占用 9000/9001) ports: - - "9000:9000" - - "9001:9001" + - "9100:9000" + - "9101:9001" volumes: - miniodata:/data - # 创建 bucket 并开放下载(开发环境) + # 一次性初始化:创建 bucket 并开放公共下载(跑完即退出,Exited 0 属正常) + # 注意:容器网络内仍访问 minio:9000(服务端口),宿主机映射为 9100 minio-init: image: minio/mc:latest container_name: kefu-minio-init depends_on: - minio + restart: "no" entrypoint: > /bin/sh -c " sleep 3; diff --git a/server/cmd/seed/main.go b/server/cmd/seed/main.go index 8a1ba8a..4d7b597 100644 --- a/server/cmd/seed/main.go +++ b/server/cmd/seed/main.go @@ -85,13 +85,13 @@ func seed() { } model.DB.Create(&customers) - // Sessions + // Sessions(含访客 IP/地区,与工作台顶栏展示一致) sessions := []model.Session{ - {TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[0].ID, AgentID: &agent1.ID, Status: "active", Priority: "urgent"}, - {TenantID: tenants[0].ID, ChannelID: channels[1].ID, CustomerID: customers[1].ID, AgentID: &agent2.ID, Status: "active", Priority: "normal"}, - {TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[2].ID, AgentID: nil, Status: "waiting", Priority: "normal"}, - {TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[3].ID, AgentID: &agent3.ID, Status: "ended", Priority: "normal", SatisfactionScore: ptr(5)}, - {TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[4].ID, AgentID: &agent1.ID, Status: "ended", Priority: "normal", SatisfactionScore: ptr(4)}, + {TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[0].ID, AgentID: &agent1.ID, Status: "active", Priority: "urgent", VisitorIP: "192.168.1.105", VisitorRegion: "北京", UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0"}, + {TenantID: tenants[0].ID, ChannelID: channels[1].ID, CustomerID: customers[1].ID, AgentID: &agent2.ID, Status: "active", Priority: "normal", VisitorIP: "10.0.0.23", VisitorRegion: "内网", UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0) Safari/604.1"}, + {TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[2].ID, AgentID: nil, Status: "waiting", Priority: "normal", VisitorIP: "223.5.5.5", VisitorRegion: "中国", UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X) Chrome/120.0.0.0"}, + {TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[3].ID, AgentID: &agent3.ID, Status: "ended", Priority: "normal", SatisfactionScore: ptr(5), VisitorIP: "114.114.114.114", VisitorRegion: "中国"}, + {TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[4].ID, AgentID: &agent1.ID, Status: "ended", Priority: "normal", SatisfactionScore: ptr(4), VisitorIP: "8.8.8.8", VisitorRegion: "美国"}, } model.DB.Create(&sessions) diff --git a/server/internal/config/config.go b/server/internal/config/config.go index 40dc678..6ff130d 100644 --- a/server/internal/config/config.go +++ b/server/internal/config/config.go @@ -34,13 +34,13 @@ type JWTConfig struct { // StorageConfig S3 兼容对象存储(MinIO / OSS / COS / S3 共用字段) type StorageConfig struct { - Endpoint string // 如 localhost:9000 或 oss-cn-hangzhou.aliyuncs.com + Endpoint string // 如 localhost:9100(开发 MinIO)或 oss-cn-hangzhou.aliyuncs.com AccessKey string SecretKey string Bucket string UseSSL bool Region string - PublicBase string // 浏览器可访问前缀,如 http://localhost:9000/kefu + PublicBase string // 浏览器可访问前缀,如 http://localhost:9100/kefu MaxUploadMB int MaxImageEdge int // 最长边像素 WebPQuality float32 @@ -65,13 +65,14 @@ func Load() *Config { ExpireTime: 24 * time.Hour, }, Storage: StorageConfig{ - Endpoint: getEnv("STORAGE_ENDPOINT", "localhost:9000"), + // 默认映射宿主机 9100→容器 9000,避免与其它项目 MinIO 冲突 + Endpoint: getEnv("STORAGE_ENDPOINT", "localhost:9100"), AccessKey: getEnv("STORAGE_ACCESS_KEY", "minioadmin"), SecretKey: getEnv("STORAGE_SECRET_KEY", "minioadmin"), Bucket: getEnv("STORAGE_BUCKET", "kefu"), UseSSL: getEnvBool("STORAGE_USE_SSL", false), Region: getEnv("STORAGE_REGION", "us-east-1"), - PublicBase: getEnv("STORAGE_PUBLIC_BASE_URL", "http://localhost:9000/kefu"), + PublicBase: getEnv("STORAGE_PUBLIC_BASE_URL", "http://localhost:9100/kefu"), MaxUploadMB: getEnvInt("STORAGE_MAX_UPLOAD_MB", 10), MaxImageEdge: getEnvInt("STORAGE_MAX_IMAGE_EDGE", 1920), WebPQuality: float32(getEnvInt("STORAGE_WEBP_QUALITY", 80)), diff --git a/server/internal/handler/message_validation_test.go b/server/internal/handler/message_validation_test.go index 1870ece..29b329c 100644 --- a/server/internal/handler/message_validation_test.go +++ b/server/internal/handler/message_validation_test.go @@ -3,7 +3,7 @@ package handler import "testing" func TestValidateMessageContent(t *testing.T) { - SetImagePublicBase("http://localhost:9000/kefu") + SetImagePublicBase("http://localhost:9100/kefu") if _, err := validateMessageContent("text", "hello"); err != nil { t.Fatalf("文本消息应通过: %v", err) @@ -12,7 +12,7 @@ func TestValidateMessageContent(t *testing.T) { t.Fatal("空文本应失败") } - validURL := "http://localhost:9000/kefu/tenants/1/chat/a.webp" + validURL := "http://localhost:9100/kefu/tenants/1/chat/a.webp" if content, err := validateMessageContent("image", validURL); err != nil || content != validURL { t.Fatalf("合法图片 URL 校验失败: content=%q err=%v", content, err) } diff --git a/server/internal/handler/security_integration_test.go b/server/internal/handler/security_integration_test.go index 3aa4d15..31588af 100644 --- a/server/internal/handler/security_integration_test.go +++ b/server/internal/handler/security_integration_test.go @@ -36,9 +36,9 @@ func setupRouter(t *testing.T) *gin.Engine { model.DB = db middleware.InitJWT("test-secret") router := gin.New() - mem := storage.NewMemory("http://localhost:9000/kefu") + mem := storage.NewMemory("http://localhost:9100/kefu") handler.SetupRoutes(router, mem, config.StorageConfig{ - PublicBase: "http://localhost:9000/kefu", + PublicBase: "http://localhost:9100/kefu", MaxUploadMB: 10, MaxImageEdge: 1920, WebPQuality: 80, diff --git a/server/internal/storage/s3.go b/server/internal/storage/s3.go index 4a35242..73531df 100644 --- a/server/internal/storage/s3.go +++ b/server/internal/storage/s3.go @@ -84,7 +84,7 @@ func (s *S3Storage) Delete(ctx context.Context, key string) error { func (s *S3Storage) PublicBase() string { return s.base } func (s *S3Storage) objectURL(key string) string { - // PublicBase 形如 http://localhost:9000/kefu + // PublicBase 形如 http://localhost:9100/kefu(宿主机映射端口) return s.base + "/" + strings.TrimPrefix(key, "/") } diff --git a/web/src/pages/agent/Dashboard.tsx b/web/src/pages/agent/Dashboard.tsx index 22913c0..177cf29 100644 --- a/web/src/pages/agent/Dashboard.tsx +++ b/web/src/pages/agent/Dashboard.tsx @@ -175,9 +175,24 @@ const Dashboard = () => { const response = await getSession(id) const data = response.data setDetail({ messages: data.messages || [], events: data.events || [], pendingCount: data.pending_count || 0 }) + // 用详情接口的完整会话字段(含 IP/地区)回填列表,保证顶栏展示准确 + if (data.session) { + setSessions(previous => previous.map(session => + session.id === id + ? { + ...session, + ...data.session, + unread_count: markRead ? 0 : session.unread_count, + last_message: session.last_message, + last_message_at: session.last_message_at, + } + : session, + )) + } else if (markRead) { + setSessions(previous => previous.map(session => session.id === id ? { ...session, unread_count: 0 } : session)) + } if (markRead) { await markSessionRead(id) - setSessions(previous => previous.map(session => session.id === id ? { ...session, unread_count: 0 } : session)) } } catch { antMsg.error('加载消息失败') @@ -615,10 +630,7 @@ const Dashboard = () => {
{selected && selectedCustomer ? ( <> -
+
{ {selectedCustomer.status === 'online' ? '在线' : selectedCustomer.status === 'busy' ? '忙碌' : '离线'}
-
- {selected.visitor_ip ? ( - IP: {selected.visitor_ip} - ) : ( - 会话 #{selected.id} - )} - {selected.visitor_region && ( - <> - | - {selected.visitor_region} - - )} - {!selected.visitor_ip && selectedCustomer.source && ( - <> - | - {selectedCustomer.source} - - )} + {/* 与设计稿一致:姓名下方展示 IP | 地区 */} +
+ + IP: {selected.visitor_ip || '—'} + + | + + {selected.visitor_region || '未知'} +
@@ -898,10 +901,6 @@ const Dashboard = () => { 地区 {selected?.visitor_region || '—'}
-
- IP - {selected?.visitor_ip || '—'} -