From e8c6f987e2532b85748a03025866c595ab811ccb Mon Sep 17 00:00:00 2001
From: cagoya <2561694274@qq.com>
Date: Thu, 4 Dec 2025 10:36:12 +0800
Subject: [PATCH 1/5] =?UTF-8?q?docs=EF=BC=9A=E6=9B=B4=E6=96=B0README?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 88 ++++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 61 insertions(+), 27 deletions(-)
diff --git a/README.md b/README.md
index db53637..3d9d5fe 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,7 @@
+
## 简介
@@ -12,43 +13,81 @@
搭建了一个存储图片的Web应用,实现的功能如下:
-1. 用户注册登录和修改个人信息
+1. 用户注册、登录和修改个人信息
2. 支持用户登录后上传图片,会自动从图片中提取拍摄时间、地点和像素等信息
3. 支持用户自定义标签和AI识图添加标签
4. 支持在浏览器中对图片进行编辑
5. 显示图片缩略图,支持图片轮播
6. 支持按照多种条件搜索,支持AI对话搜索
+7. 可以删除和下载图片
### 技术栈
- 前端:Vue(ts) + Element Plus
-- 后端:gin(golang) + gorm
-- 数据库:MySQL + Redis
+- 后端:gin(golang) + gorm + viper + zap
+- 数据库:PostgreSQL + Redis + Qdrant
- 鉴权:JWT
## 本地开发指南
+### 环境推荐
+
+- **go:1.24+**
+- **node:v22 / npm:10**
+
### clone 本项目
```bash
git clone git@github.com:cagoya/ImageHub.git
```
-### 安装 mysql
+### 安装 PostgreSQL
+
+> 这里的docker部署都没启用映射卷,如认为有必要请自行添加。
-**开发环境**的数据库可以跑在docker容器中,这里使用docker部署mysql
+**开发和测试环境**的数据库可以选择使用docker部署
```bash
# 或者指定版本
-docker pull mysql:latest
-docker run -d --name mysql-server -p 3306:3306 -e MYSQL_ROOT_PASSWORD="123456" mysql
+docker pull postgres
+docker run -d \
+ --name postgre-server\
+ -p 5432:5432\
+ -e POSTGRES_PASSWORD="123456"\
+ postgres
+```
+
+为了方便查询,可一并拉取PgAdmin,这里的邮箱随意,只起到一个用户名的作用。
+
+```bash
+docker pull dpage/pgadmin4
+docker run -d \
+ --name pgadmin4 \
+ -p 5433:80 \
+ -e 'PGADMIN_DEFAULT_EMAIL=user@example.com' \
+ -e 'PGADMIN_DEFAULT_PASSWORD=123456' \
+ dpage/pgadmin4
```
### 安装Redis
+还是使用docker部署,需要启动键空间通知(Key-space Notifications)机制
+
```bash
docker pull redis
-docker run -d --name redis-server -p 6379:6379 redis
+docker run -d\
+ --name redis-server\
+ -p 6379:6379\
+ --notify-keyspace-events Ex\
+ redis
+```
+
+### 安装Qdrant
+
+```bash
+docker pull qdrant/qdrant
+docker run -p 6333:6333 -p 6334:6334 \
+ qdrant/qdrant
```
### 启动后端
@@ -73,11 +112,18 @@ npm install
npm run dev
```
+### 启动mcp server
+
+```bash
+cd ./mcp
+go run main.go
+```
+
## Docker 部署指南
-### 使用 Docker Compose 一键部署
+### 使用 Docker Compose
-项目已配置好 Docker Compose,可以一键启动所有服务(MySQL、Redis、后端、前端)。
+项目已配置好 Docker Compose,可以一键启动所有服务
```bash
# 在项目根目录执行
@@ -100,30 +146,18 @@ docker-compose down -v
- **前端**: http://localhost:5173
- **后端 API**: http://localhost:8888/api
-- **MySQL**: localhost:3306
+- **PostgreSQL**: localhost:5432
- **Redis**: localhost:6379
-
-### 单独构建镜像
-
-如果需要单独构建前后端镜像:
-
-```bash
-# 构建后端镜像
-cd server
-docker build -t imagehub-backend:latest .
-
-# 构建前端镜像
-cd web
-docker build -t imagehub-frontend:latest .
-```
+- **Qdrant**:localhost:6334
### 环境变量配置
-后端服务支持通过环境变量 `CONFIG_FILE` 指定配置文件,默认为 `server.yaml`。Docker 环境中会自动使用 `docker.yaml` 配置,该配置已设置好容器间的服务名称(mysql、redis)。
+后端服务支持通过环境变量 `CONFIG_FILE` 指定配置文件,开发环境为 `server.yaml`。Docker 环境中会自动使用 `docker.yaml` 配置,该配置已设置好容器间的服务名称。
### 数据持久化
Docker Compose 已配置数据卷持久化:
-- MySQL 数据存储在 `mysql_data` 卷中
+- PostgreSQL 数据存储在 `mysql_data` 卷中
- Redis 数据存储在 `redis_data` 卷中
+- Qdrant 数据存储在 `qdrant_data` 卷中
- 后端上传的图片和日志通过挂载目录持久化
\ No newline at end of file
From d6ff7d5904b6767c4ca4035f4c64ee337c4d3a9b Mon Sep 17 00:00:00 2001
From: cagoya <2561694274@qq.com>
Date: Thu, 4 Dec 2025 11:13:28 +0800
Subject: [PATCH 2/5] =?UTF-8?q?deploy=EF=BC=9A=E8=B0=83=E6=95=B4docker?=
=?UTF-8?q?=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docker-compose.yml | 52 ++++++++++++++++++++++++++++++++++-----
mcp/Dockerfile | 38 ++++++++++++++++++++++++++++
mcp/main.go | 18 ++++++++++++--
server/config/docker.yaml | 11 ++++++++-
server/config/server.yaml | 9 ++++++-
server/config/test.yaml | 4 ++-
server/global/qdrant.go | 6 ++---
server/main.go | 2 +-
server/utils/ali.go | 12 ++++++---
server/utils/baidu.go | 17 ++++++++++---
server/utils/location.go | 9 ++++++-
web/Dockerfile | 23 +++++++++++++----
web/nginx.conf | 44 +++++++++++++++++++++++++++++++++
13 files changed, 218 insertions(+), 27 deletions(-)
create mode 100644 mcp/Dockerfile
create mode 100644 web/nginx.conf
diff --git a/docker-compose.yml b/docker-compose.yml
index cc1a9d2..7beb854 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -37,6 +37,43 @@ services:
timeout: 3s
retries: 5
+ # Qdrant 向量数据库服务
+ qdrant:
+ image: qdrant/qdrant:latest
+ container_name: imagehub-qdrant
+ restart: unless-stopped
+ ports:
+ - "6333:6333" # HTTP API
+ - "6334:6334" # gRPC API
+ volumes:
+ - qdrant_data:/qdrant/storage
+ networks:
+ - imagehub-network
+ healthcheck:
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:6333/health"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ # MCP 服务
+ mcp:
+ build:
+ context: ./mcp
+ dockerfile: Dockerfile
+ container_name: imagehub-mcp
+ restart: unless-stopped
+ depends_on:
+ qdrant:
+ condition: service_healthy
+ ports:
+ - "7777:7777"
+ environment:
+ - TZ=Asia/Shanghai
+ - QDRANT_HOST=qdrant
+ - QDRANT_PORT=6334
+ networks:
+ - imagehub-network
+
# 后端服务
backend:
build:
@@ -49,6 +86,8 @@ services:
condition: service_healthy
redis:
condition: service_healthy
+ qdrant:
+ condition: service_healthy
ports:
- "8888:8888"
volumes:
@@ -60,7 +99,7 @@ services:
networks:
- imagehub-network
- # 前端服务
+ # 前端服务 (Nginx)
frontend:
build:
context: ./web
@@ -70,13 +109,14 @@ services:
depends_on:
- backend
ports:
- - "5173:5173"
- volumes:
- - ./web:/app
- - /app/node_modules
+ - "80:80"
networks:
- imagehub-network
networks:
imagehub-network:
- driver: bridge
\ No newline at end of file
+ driver: bridge
+
+volumes:
+ qdrant_data:
+ driver: local
\ No newline at end of file
diff --git a/mcp/Dockerfile b/mcp/Dockerfile
new file mode 100644
index 0000000..c2c79a9
--- /dev/null
+++ b/mcp/Dockerfile
@@ -0,0 +1,38 @@
+# 构建阶段
+FROM golang:1.24-alpine AS builder
+
+# 设置工作目录
+WORKDIR /app
+
+# 安装必要的构建工具
+RUN apk add --no-cache git
+
+# 复制 go mod 文件
+COPY go.mod go.sum ./
+
+# 下载依赖
+RUN go mod download
+
+# 复制源代码
+COPY . .
+
+# 构建应用
+RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o mcp-server main.go
+
+# 运行阶段
+FROM alpine:latest
+
+# 安装必要的运行时依赖
+RUN apk --no-cache add ca-certificates tzdata
+
+WORKDIR /app
+
+# 从构建阶段复制二进制文件
+COPY --from=builder /app/mcp-server .
+
+# 暴露端口
+EXPOSE 7777
+
+# 运行应用
+CMD ["./mcp-server"]
+
diff --git a/mcp/main.go b/mcp/main.go
index 850cd87..6260c5c 100644
--- a/mcp/main.go
+++ b/mcp/main.go
@@ -5,6 +5,8 @@ import (
"fmt"
"log"
"net/http"
+ "os"
+ "strconv"
"mcp_server/utils"
@@ -40,9 +42,21 @@ func QdrantSearch(ctx context.Context, _ *mcp.CallToolRequest, in QdrantSearchPa
}
// 3. 连接 Qdrant
+ // 从环境变量获取 Qdrant 配置,默认为 localhost:6334
+ qdrantHost := os.Getenv("QDRANT_HOST")
+ if qdrantHost == "" {
+ qdrantHost = "localhost"
+ }
+ qdrantPort := 6334
+ if portEnv := os.Getenv("QDRANT_PORT"); portEnv != "" {
+ if port, err := strconv.Atoi(portEnv); err == nil {
+ qdrantPort = port
+ }
+ }
+
qdrantClient, err := qdrant.NewClient(&qdrant.Config{
- Host: "localhost",
- Port: 6334,
+ Host: qdrantHost,
+ Port: qdrantPort,
})
if err != nil {
return nil, QdrantSearchResult{}, fmt.Errorf("连接 Qdrant 失败: %v", err)
diff --git a/server/config/docker.yaml b/server/config/docker.yaml
index d3dce0c..2626a98 100644
--- a/server/config/docker.yaml
+++ b/server/config/docker.yaml
@@ -9,6 +9,9 @@ redis:
port: 6379
password: "123456"
db: 0
+qdrant:
+ host: qdrant
+ port: 6334
zap:
prefix: image_hub
timeFormat: 2006/01/02 - 15:04:05.00000
@@ -24,4 +27,10 @@ zap:
compress: true
output:
- "./log/image_hub.log"
-
+gaode:
+ apiKey: "eaf59456283e912bb394b3f4970027e1"
+baidu:
+ apiKey: "siOl6J2rzuM4CIgD3ZuDpVKY"
+ secretKey: "YyFRinKgfSMOKR8MtowoA0os8gxPpphv"
+ali:
+ apikey: "sk-9ef56cbc5d944b65b1bb612fd7864877"
\ No newline at end of file
diff --git a/server/config/server.yaml b/server/config/server.yaml
index 468e62e..1505d1d 100644
--- a/server/config/server.yaml
+++ b/server/config/server.yaml
@@ -26,4 +26,11 @@ zap:
backups: 10
compress: true
output:
- - "./log/image_hub.log"
\ No newline at end of file
+ - "./log/image_hub.log"
+gaode:
+ apiKey: "eaf59456283e912bb394b3f4970027e1"
+baidu:
+ apiKey: "siOl6J2rzuM4CIgD3ZuDpVKY"
+ secretKey: "YyFRinKgfSMOKR8MtowoA0os8gxPpphv"
+ali:
+ apikey: "sk-9ef56cbc5d944b65b1bb612fd7864877"
\ No newline at end of file
diff --git a/server/config/test.yaml b/server/config/test.yaml
index d120583..c928829 100644
--- a/server/config/test.yaml
+++ b/server/config/test.yaml
@@ -11,4 +11,6 @@ redis:
db: 1
qdrant:
host: localhost
- port: 6334
\ No newline at end of file
+ port: 6334
+gaode:
+ apiKey: "eaf59456283e912bb394b3f4970027e1"
\ No newline at end of file
diff --git a/server/global/qdrant.go b/server/global/qdrant.go
index df619fd..fb0d506 100644
--- a/server/global/qdrant.go
+++ b/server/global/qdrant.go
@@ -10,10 +10,10 @@ import (
var qdrantClient *qdrant.Client
-func InitChroma() error {
+func InitQdrant() error {
var err error
- host := viper.GetString("chroma.host")
- port := viper.GetInt("chroma.port")
+ host := viper.GetString("qdrant.host")
+ port := viper.GetInt("qdrant.port")
qdrantClient, err = qdrant.NewClient(&qdrant.Config{
Host: host,
Port: port,
diff --git a/server/main.go b/server/main.go
index ea3bf1f..c217fa1 100644
--- a/server/main.go
+++ b/server/main.go
@@ -40,7 +40,7 @@ func main() {
}
logger.Info("redis初始化成功")
// 初始化Qdrant
- err = global.InitChroma()
+ err = global.InitQdrant()
if err != nil {
panic(err)
}
diff --git a/server/utils/ali.go b/server/utils/ali.go
index b136674..6a27c49 100644
--- a/server/utils/ali.go
+++ b/server/utils/ali.go
@@ -10,9 +10,15 @@ import (
"os"
"path/filepath"
"strings"
+
+ "github.com/spf13/viper"
)
-const ALI_APIKEY = "sk-9ef56cbc5d944b65b1bb612fd7864877"
+// getAliApiKey 获取阿里云API密钥,在函数调用时动态读取配置
+func getAliApiKey() string {
+ return viper.GetString("ali.apikey")
+}
+
const API_URL = "https://dashscope.aliyuncs.com/api/v1/services/embeddings/multimodal-embedding/multimodal-embedding"
const MODEL = "tongyi-embedding-vision-plus"
@@ -144,7 +150,7 @@ func VectorizeImage(imageURL string) ([]float64, error) {
}
// 设置请求头
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ALI_APIKEY))
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", getAliApiKey()))
req.Header.Set("Content-Type", "application/json")
// 发送请求
@@ -215,7 +221,7 @@ func VectorizeText(text string) ([]float64, error) {
}
// 设置请求头
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ALI_APIKEY))
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", getAliApiKey()))
req.Header.Set("Content-Type", "application/json")
// 发送请求
diff --git a/server/utils/baidu.go b/server/utils/baidu.go
index d5f804e..9fc5b55 100644
--- a/server/utils/baidu.go
+++ b/server/utils/baidu.go
@@ -15,10 +15,21 @@ import (
"github.com/redis/go-redis/v9"
"server/global"
+
+ "github.com/spf13/viper"
)
-const BAIDU_APIKEY = "siOl6J2rzuM4CIgD3ZuDpVKY"
-const SECRET_KEY = "YyFRinKgfSMOKR8MtowoA0os8gxPpphv"
+// getBaiduApiKey 获取百度AI API密钥,在函数调用时动态读取配置
+func getBaiduApiKey() string {
+ return viper.GetString("baidu.apiKey")
+}
+
+// getBaiduSecretKey 获取百度AI Secret Key,在函数调用时动态读取配置
+func getBaiduSecretKey() string {
+ return viper.GetString("baidu.secretKey")
+}
+
+var SECRET_KEY = viper.GetString("baidu.secretKey")
/**
* 使用 AK,SK 生成鉴权签名(Access Token)
@@ -34,7 +45,7 @@ func GetAccessToken() string {
// 如果Redis中没有AccessToken,则重新获取
tokenUrl := "https://aip.baidubce.com/oauth/2.0/token"
- postData := fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s", BAIDU_APIKEY, SECRET_KEY)
+ postData := fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s", getBaiduApiKey(), getBaiduSecretKey())
resp, err := http.Post(tokenUrl, "application/x-www-form-urlencoded", strings.NewReader(postData))
if err != nil {
diff --git a/server/utils/location.go b/server/utils/location.go
index cacbdc7..e625055 100644
--- a/server/utils/location.go
+++ b/server/utils/location.go
@@ -6,8 +6,15 @@ import (
"io"
"net/http"
"net/url"
+
+ "github.com/spf13/viper"
)
+// getGaodeApiKey 获取高德地图API密钥,在函数调用时动态读取配置
+func getGaodeApiKey() string {
+ return viper.GetString("gaode.apiKey")
+}
+
// 完整的响应结构体,这个只在本文件内使用
type apiResponse struct {
Status string `json:"status"`
@@ -43,7 +50,7 @@ func ReverseGeocode(longitude float64, latitude float64) *GeoInfo {
// 构建查询参数
params := url.Values{}
- params.Add("key", "eaf59456283e912bb394b3f4970027e1")
+ params.Add("key", getGaodeApiKey())
params.Add("location", fmt.Sprintf("%f,%f", longitude, latitude))
params.Add("extensions", "base")
diff --git a/web/Dockerfile b/web/Dockerfile
index ef478fd..e3f8ed3 100644
--- a/web/Dockerfile
+++ b/web/Dockerfile
@@ -1,4 +1,5 @@
-FROM node:20-alpine
+# 构建阶段
+FROM node:20-alpine AS builder
# 设置工作目录
WORKDIR /app
@@ -12,8 +13,20 @@ RUN npm ci
# 复制源代码
COPY . .
-# 暴露 Vite 默认端口
-EXPOSE 5173
+# 构建生产版本
+RUN npm run build
-# 启动开发服务器
-CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
\ No newline at end of file
+# 运行阶段 - 使用 Nginx
+FROM nginx:alpine
+
+# 复制构建好的文件到 nginx 目录
+COPY --from=builder /app/dist /usr/share/nginx/html
+
+# 复制 nginx 配置文件
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+# 暴露端口
+EXPOSE 80
+
+# 启动 nginx
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/web/nginx.conf b/web/nginx.conf
new file mode 100644
index 0000000..98e1157
--- /dev/null
+++ b/web/nginx.conf
@@ -0,0 +1,44 @@
+server {
+ listen 80;
+ server_name localhost;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # 启用 gzip 压缩
+ gzip on;
+ gzip_vary on;
+ gzip_min_length 1024;
+ gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;
+
+ # SPA 路由支持 - 所有路由都返回 index.html
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # 静态资源缓存
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+
+ # API 代理到后端服务
+ location /api {
+ proxy_pass http://backend:8888;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_cache_bypass $http_upgrade;
+ }
+
+ # 健康检查
+ location /health {
+ access_log off;
+ return 200 "healthy\n";
+ add_header Content-Type text/plain;
+ }
+}
+
From edb0caf84c3ecde3eebbb9ec5bf85023ee057fa7 Mon Sep 17 00:00:00 2001
From: cagoya <2561694274@qq.com>
Date: Thu, 4 Dec 2025 11:51:40 +0800
Subject: [PATCH 3/5] =?UTF-8?q?add=EF=BC=9A=E8=B0=83=E6=95=B4=E5=89=8D?=
=?UTF-8?q?=E7=AB=AF=E6=A0=B7=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
web/src/components/AppHeader.vue | 4 +--
web/src/components/ImageCard.vue | 6 -----
web/src/views/404.vue | 45 +++++++++++++++++++++++---------
web/src/views/Chat.vue | 34 +++++++-----------------
4 files changed, 44 insertions(+), 45 deletions(-)
diff --git a/web/src/components/AppHeader.vue b/web/src/components/AppHeader.vue
index 8e8da1d..4bcab22 100644
--- a/web/src/components/AppHeader.vue
+++ b/web/src/components/AppHeader.vue
@@ -13,7 +13,7 @@
支持上传多张图片,系统将自动处理您的图片