Ver código fonte

Update:系统重构

XieXing 3 meses atrás
pai
commit
44178908eb
33 arquivos alterados com 1154 adições e 1396 exclusões
  1. 1 1
      DEPLOY.md
  2. 402 0
      INTEGRATION_GUIDE.md
  3. 0 36
      README.en.md
  4. 33 59
      shudao-go-backend/conf/app.conf
  5. 50 0
      shudao-go-backend/controllers/chroma.go
  6. 1 55
      shudao-go-backend/controllers/hazard.go
  7. 0 186
      shudao-go-backend/controllers/oss.go
  8. 127 135
      shudao-go-backend/controllers/shudaooss.go
  9. 9 6
      shudao-go-backend/controllers/test.go
  10. 6 45
      shudao-go-backend/main.go
  11. 0 107
      shudao-go-backend/models/chroma.go
  12. 0 188
      shudao-go-backend/models/tools.go
  13. 1 1
      shudao-go-backend/routers/router.go
  14. BIN
      shudao-go-backend/static/image/test_output.jpg
  15. 61 138
      shudao-go-backend/utils/auth_middleware.go
  16. 7 13
      shudao-go-backend/utils/config.go
  17. 79 0
      shudao-go-backend/utils/response.go
  18. 0 131
      shudao-go-backend/utils/test_file_match.go
  19. 0 149
      shudao-go-backend/utils/test_string_match.go
  20. 10 63
      shudao-go-backend/utils/token.go
  21. 169 0
      shudao-go-backend/utils/tools.go
  22. 5 8
      shudao-vue-frontend/src/components/ExportButton.vue
  23. 1 20
      shudao-vue-frontend/src/main.js
  24. 17 6
      shudao-vue-frontend/src/request/axios.js
  25. 2 11
      shudao-vue-frontend/src/services/audioTranscription.js
  26. 104 20
      shudao-vue-frontend/src/utils/apiConfig.js
  27. 50 5
      shudao-vue-frontend/src/utils/ticketAuth.js
  28. 2 2
      shudao-vue-frontend/src/views/Chat.vue
  29. 2 1
      shudao-vue-frontend/src/views/Login.vue
  30. 7 1
      shudao-vue-frontend/src/views/NotFound.vue
  31. 2 1
      shudao-vue-frontend/src/views/PolicyDocument.vue
  32. 3 3
      shudao-vue-frontend/src/views/mobile/m-Chat.vue
  33. 3 5
      shudao-vue-frontend/vite.config.js

+ 1 - 1
DEPLOY.md

@@ -1,4 +1,4 @@
-# 蜀道安全管理系统 - 部署指南
+# 蜀道安全管理AI智能助手 - 部署指南
 
 ## 1. 环境准备
 

+ 402 - 0
INTEGRATION_GUIDE.md

@@ -0,0 +1,402 @@
+# 4A统一API网关集成指南
+
+本文档说明如何在其他系统中调用4A统一API网关服务。
+
+## 服务地址
+
+| 环境 | 地址 |
+|------|------|
+| 本地开发 | http://localhost:28004 |
+| 测试环境 | http://localhost:28004 |
+| 线上环境 | https://aqai.shudaodsj.com:22000/auth |
+
+## 接口调用示例
+
+### 1. 票据获取
+
+获取SSO单点登录票据。
+
+```python
+import requests
+
+# 本地/测试环境
+url = "http://localhost:28004/api/ticket/get"
+# 线上环境
+# url = "https://aqai.shudaodsj.com:22000/auth/api/ticket/get"
+
+response = requests.post(url, json={
+    "mobile": "17800000001",  # 可选,不传使用默认值
+    "app_code": "SDJD_AQAI"   # 可选,不传使用默认值
+})
+
+result = response.json()
+# 返回: {"retCode": "1000", "ssoTicket": "..."}
+```
+
+### 2. 票据解析
+
+解析票据获取用户信息。
+
+```python
+url = "http://localhost:28004/api/ticket/analyze"
+
+response = requests.post(url, json={
+    "ticket_data": "oXlMiYSOLhJAYP3Qo7Zwps..."  # 从票据获取接口得到的ssoTicket
+})
+
+result = response.json()
+# 返回: {"retCode": "1000", "token": "...", "userInfo": {...}}
+```
+
+### 3. 票据处理+JWT生成(推荐)
+
+一步完成票据解析和JWT Token生成。
+
+```python
+url = "http://localhost:28004/api/ticket/process"
+
+response = requests.post(url, json={
+    "ticket_data": "oXlMiYSOLhJAYP3Qo7Zwps..."
+})
+
+result = response.json()
+# 返回:
+# {
+#     "retCode": "1000",
+#     "message": "处理成功",
+#     "ticket_token": "...",
+#     "username": "张三",
+#     "refresh_token": "eyJ...",
+#     "token_type": "bearer",
+#     "expires_in": 43200
+# }
+```
+
+### 4. JWT Token生成
+
+根据用户信息生成JWT Token。
+
+```python
+url = "http://localhost:28004/auth/tokens"
+
+response = requests.post(url, json={
+    "accountID": "user001",
+    "name": "张三",
+    "userCode": "N1234567",
+    "contactNumber": "13800138000"
+})
+
+result = response.json()
+# 返回:
+# {
+#     "access_token": "eyJ...",
+#     "refresh_token": "eyJ...",
+#     "token_type": "bearer",
+#     "expires_in": 43200
+# }
+```
+
+### 5. JWT Token验证
+
+验证Token是否有效。
+
+```python
+url = "http://localhost:28004/auth/verify"
+
+response = requests.post(url, json={
+    "token": "eyJ..."
+})
+
+result = response.json()
+# 返回:
+# {
+#     "valid": true,
+#     "token_type": "refresh",
+#     "accountID": "user001",
+#     "name": "张三",
+#     "userCode": "N1234567",
+#     "contactNumber": "13800138000",
+#     "exp": 1702857600
+# }
+```
+
+### 6. 账号查询
+
+分页查询从账号。
+
+```python
+url = "http://localhost:28004/api/account/query"
+
+response = requests.post(url, json={
+    "cur_page": 1,
+    "page_size": 10,
+    "user_code": "N1234567",  # 可选
+    "user_name": "张三",       # 可选,支持模糊匹配
+    "org_code": "NG5596477"   # 可选
+})
+
+result = response.json()
+# 返回: {"success": true, "data": [...], "recordsTotal": 100, ...}
+```
+
+### 7. 账号添加
+
+```python
+url = "http://localhost:28004/api/account/add"
+
+response = requests.post(url, json={
+    "user_code": "N1234567",
+    "org_code": "NG5596477",
+    "org_name": "蜀道投资集团有限责任公司"
+})
+```
+
+### 8. 账号修改
+
+```python
+url = "http://localhost:28004/api/account/modify"
+
+response = requests.post(url, json={
+    "account_id": "aizscs001",
+    "org_code": "NG5596477"
+})
+```
+
+### 9. 账号删除
+
+```python
+url = "http://localhost:28004/api/account/delete"
+
+response = requests.post(url, json={
+    "account_id": "aizscs001"
+})
+```
+
+## 线上环境注意事项
+
+线上环境通过Nginx代理,路径前缀为 `/auth`:
+
+```python
+# 本地/测试
+base_url = "http://localhost:28004"
+
+# 线上
+base_url = "https://aqai.shudaodsj.com:22000/auth"
+
+# 调用示例
+ticket_url = f"{base_url}/api/ticket/get"
+auth_url = f"{base_url}/auth/tokens"
+```
+
+## 错误处理
+
+所有接口返回统一格式:
+
+```json
+// 成功
+{"retCode": "1000", "msg": "成功", ...}
+
+// 失败
+{"detail": "错误信息"}
+```
+
+HTTP状态码:
+- 200: 成功
+- 400: 请求参数错误
+- 422: 参数验证失败
+- 500: 服务器内部错误
+
+## 健康检查
+
+```python
+response = requests.get("http://localhost:28004/health")
+# 返回: {"status": "healthy", "service": "unified-api-gateway"}
+```
+
+---
+
+## Go调用示例
+
+### 1. 票据获取
+
+```go
+package main
+
+import (
+    "bytes"
+    "encoding/json"
+    "fmt"
+    "io"
+    "net/http"
+)
+
+const baseURL = "http://localhost:28004" // 线上: https://aqai.shudaodsj.com:22000/auth
+
+type TicketRequest struct {
+    Mobile  string `json:"mobile,omitempty"`
+    AppCode string `json:"app_code,omitempty"`
+}
+
+type TicketResponse struct {
+    RetCode   string `json:"retCode"`
+    SSOTicket string `json:"ssoTicket"`
+}
+
+func GetTicket() (*TicketResponse, error) {
+    reqBody, _ := json.Marshal(TicketRequest{})
+    resp, err := http.Post(baseURL+"/api/ticket/get", "application/json", bytes.NewBuffer(reqBody))
+    if err != nil {
+        return nil, err
+    }
+    defer resp.Body.Close()
+
+    body, _ := io.ReadAll(resp.Body)
+    var result TicketResponse
+    json.Unmarshal(body, &result)
+    return &result, nil
+}
+```
+
+### 2. 票据处理+JWT生成
+
+```go
+type TicketProcessRequest struct {
+    TicketData string `json:"ticket_data"`
+}
+
+type TicketProcessResponse struct {
+    RetCode      string `json:"retCode"`
+    Message      string `json:"message"`
+    TicketToken  string `json:"ticket_token"`
+    Username     string `json:"username"`
+    RefreshToken string `json:"refresh_token"`
+    TokenType    string `json:"token_type"`
+    ExpiresIn    int    `json:"expires_in"`
+}
+
+func ProcessTicket(ticketData string) (*TicketProcessResponse, error) {
+    reqBody, _ := json.Marshal(TicketProcessRequest{TicketData: ticketData})
+    resp, err := http.Post(baseURL+"/api/ticket/process", "application/json", bytes.NewBuffer(reqBody))
+    if err != nil {
+        return nil, err
+    }
+    defer resp.Body.Close()
+
+    body, _ := io.ReadAll(resp.Body)
+    var result TicketProcessResponse
+    json.Unmarshal(body, &result)
+    return &result, nil
+}
+```
+
+### 3. JWT Token生成
+
+```go
+type UserInfo struct {
+    AccountID     string `json:"accountID"`
+    Name          string `json:"name"`
+    UserCode      string `json:"userCode"`
+    ContactNumber string `json:"contactNumber"`
+}
+
+type TokenResponse struct {
+    AccessToken  string `json:"access_token"`
+    RefreshToken string `json:"refresh_token"`
+    TokenType    string `json:"token_type"`
+    ExpiresIn    int    `json:"expires_in"`
+}
+
+func CreateTokens(user UserInfo) (*TokenResponse, error) {
+    reqBody, _ := json.Marshal(user)
+    resp, err := http.Post(baseURL+"/auth/tokens", "application/json", bytes.NewBuffer(reqBody))
+    if err != nil {
+        return nil, err
+    }
+    defer resp.Body.Close()
+
+    body, _ := io.ReadAll(resp.Body)
+    var result TokenResponse
+    json.Unmarshal(body, &result)
+    return &result, nil
+}
+```
+
+### 4. JWT Token验证
+
+```go
+type VerifyRequest struct {
+    Token string `json:"token"`
+}
+
+type VerifyResponse struct {
+    Valid         bool   `json:"valid"`
+    TokenType     string `json:"token_type"`
+    AccountID     string `json:"accountID"`
+    Name          string `json:"name"`
+    UserCode      string `json:"userCode"`
+    ContactNumber string `json:"contactNumber"`
+    Exp           int64  `json:"exp"`
+}
+
+func VerifyToken(token string) (*VerifyResponse, error) {
+    reqBody, _ := json.Marshal(VerifyRequest{Token: token})
+    resp, err := http.Post(baseURL+"/auth/verify", "application/json", bytes.NewBuffer(reqBody))
+    if err != nil {
+        return nil, err
+    }
+    defer resp.Body.Close()
+
+    body, _ := io.ReadAll(resp.Body)
+    var result VerifyResponse
+    json.Unmarshal(body, &result)
+    return &result, nil
+}
+```
+
+### 5. 账号查询
+
+```go
+type AccountQueryRequest struct {
+    CurPage  int    `json:"cur_page"`
+    PageSize int    `json:"page_size"`
+    UserCode string `json:"user_code,omitempty"`
+    UserName string `json:"user_name,omitempty"`
+    OrgCode  string `json:"org_code,omitempty"`
+}
+
+func QueryAccounts(req AccountQueryRequest) (map[string]interface{}, error) {
+    reqBody, _ := json.Marshal(req)
+    resp, err := http.Post(baseURL+"/api/account/query", "application/json", bytes.NewBuffer(reqBody))
+    if err != nil {
+        return nil, err
+    }
+    defer resp.Body.Close()
+
+    body, _ := io.ReadAll(resp.Body)
+    var result map[string]interface{}
+    json.Unmarshal(body, &result)
+    return result, nil
+}
+```
+
+### 6. 完整调用示例
+
+```go
+func main() {
+    // 1. 获取票据
+    ticket, _ := GetTicket()
+    fmt.Printf("票据: %s\n", ticket.SSOTicket[:50])
+
+    // 2. 处理票据获取JWT
+    jwt, _ := ProcessTicket(ticket.SSOTicket)
+    fmt.Printf("用户: %s, Token: %s\n", jwt.Username, jwt.RefreshToken[:50])
+
+    // 3. 验证Token
+    verify, _ := VerifyToken(jwt.RefreshToken)
+    fmt.Printf("验证结果: %v, 用户: %s\n", verify.Valid, verify.Name)
+
+    // 4. 查询账号
+    accounts, _ := QueryAccounts(AccountQueryRequest{CurPage: 1, PageSize: 10})
+    fmt.Printf("账号总数: %v\n", accounts["recordsTotal"])
+}
+```

+ 0 - 36
README.en.md

@@ -1,36 +0,0 @@
-# shudao-vue
-
-#### Description
-{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}
-
-#### Software Architecture
-Software architecture description
-
-#### Installation
-
-1.  xxxx
-2.  xxxx
-3.  xxxx
-
-#### Instructions
-
-1.  xxxx
-2.  xxxx
-3.  xxxx
-
-#### Contribution
-
-1.  Fork the repository
-2.  Create Feat_xxx branch
-3.  Commit your code
-4.  Create Pull Request
-
-
-#### Gitee Feature
-
-1.  You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
-2.  Gitee blog [blog.gitee.com](https://blog.gitee.com)
-3.  Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
-4.  The most valuable open source project [GVP](https://gitee.com/gvp)
-5.  The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
-6.  The most popular members  [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

+ 33 - 59
shudao-go-backend/conf/app.conf

@@ -2,66 +2,40 @@ appname = shudao-chat-go
 httpport = 22001
 runmode = dev
 
-# mysql配置
-#mysqluser = "shudao"
-#mysqlpass = "root"
-#mysqlpass = "YDdYntHtC7h5bniB"
-#mysqlurls = "47.109.37.38"
-#mysqlhttpport = "3306"
-#mysqldb = "shudao"
-
-# shudao-chat-go配置
-mysqluser = "root"
-mysqlpass = "88888888"
-mysqlurls = "172.16.35.57"
-# 测试环境配置
-# mysqlurls = "172.16.29.101"
-mysqlhttpport = "21000"
-mysqldb = "shudao"
-
-
-#deepseek配置
-deepseek_api_key = "sk-28625cb3738844e190cee62b2bcb25bf"
-deepseek_api_url = "https://api.deepseek.com"
+# ==================== MySQL配置 ====================
+mysqluser = root
+mysqlpass = 88888888
+mysqlurls = 172.16.29.101
+mysqlhttpport = 21000
+mysqldb = shudao
+
+# ==================== AI模型配置 ====================
+# DeepSeek配置
+deepseek_api_key = sk-28625cb3738844e190cee62b2bcb25bf
+deepseek_api_url = https://api.deepseek.com
 
 # 阿里大模型配置
-qwen3_api_url = "http://172.16.35.50:8000"
-qwen3_model = "Qwen3-30B-A3B-Instruct-2507"
+qwen3_api_url = http://172.16.35.50:8000
+qwen3_model = Qwen3-30B-A3B-Instruct-2507
 
 # 意图识别模型配置
-intent_api_url = "http://172.16.35.50:8000"
-intent_model = "Qwen2.5-1.5B-Instruct"
-
-# YOLO API配置
-yolo_base_url = "http://172.16.35.50:18080"
-
-# # Chroma数据库配置
-# chroma_host = "172.16.35.57"
-# chroma_port = "23000"
-# chroma_collection_name = "my_rag_collection"
-
-# 搜索API配置
-search_api_url = "http://localhost:24000/api/search"
-#心包连接
-heartbeat_api_url = "http://localhost:24000/api/health"
-
-# # 基础URL配置 - 手动切换
-# # 本地环境:localhost:22000
-# # 测试环境: https://172.16.29.101:22000
-# # 生产环境: https://aqai.shudaodsj.com:22000
-base_url = "https://aqai.shudaodsj.com:22000"
-
-# Token验证API配置
-# 生产环境:使用外部认证服务
-# auth_api_url = "https://aqai.shudaodsj.com:22000/api/auth/verify"
-
-# 测试环境:如果认证服务在本地
-auth_api_url = "http://127.0.0.1:28004/api/auth/verify"
-
-# oss配置
-
-OSS_ACCESS_KEY_ID="fnyfi2f368pbic74d8ll"
-OSS_ACCESS_KEY_SECRET="jgqwk7sirqlz2602x2k7yx2eor0vii19wah6ywlv"
-OSS_BUCKET="gdsc-ai-aqzs"
-OSS_END_POINT="172.16.17.52:8060"
-OSS_PARSSE_ENCRYPT_KEY="jgqwk7sirqlz2602"
+intent_api_url = http://172.16.35.50:8000
+intent_model = Qwen2.5-1.5B-Instruct
+
+# ==================== 外部服务配置 ====================
+# YOLO隐患检测服务
+yolo_base_url = http://172.16.35.50:18080
+
+# 知识库搜索API (ChromaDB) - 本地环境内网直连
+search_api_url = http://127.0.0.1:24000/api/search
+heartbeat_api_url = http://127.0.0.1:24000/api/health
+
+# Token验证API (认证网关) - 本地环境
+auth_api_url = http://127.0.0.1:28004/api/auth/verify
+
+# ==================== OSS存储配置 ====================
+oss_access_key_id = fnyfi2f368pbic74d8ll
+oss_access_key_secret = jgqwk7sirqlz2602x2k7yx2eor0vii19wah6ywlv
+oss_bucket = gdsc-ai-aqzs
+oss_endpoint = http://172.16.17.52:8060
+oss_parse_encrypt_key = jgqwk7sirqlz2602

+ 50 - 0
shudao-go-backend/controllers/chroma.go

@@ -13,6 +13,14 @@ import (
 	"github.com/beego/beego/v2/server/web"
 )
 
+// chromaSearchHeartbeatTask 心跳任务
+type chromaSearchHeartbeatTask struct {
+	url        string
+	interval   time.Duration
+	httpClient *http.Client
+	stopChan   chan struct{}
+}
+
 // ChromaController 知识库搜索控制器
 type ChromaController struct {
 	web.Controller
@@ -142,3 +150,45 @@ func (c *ChromaController) callAdvancedSearchAPI(req SearchRequest) (*SearchResp
 
 	return &searchResp, nil
 }
+
+// StartChromaSearchHeartbeatTask 启动ChromaDB搜索服务心跳任务
+func StartChromaSearchHeartbeatTask() {
+	heartbeatURL := utils.GetConfigString("heartbeat_api_url", "")
+	if heartbeatURL == "" {
+		return
+	}
+
+	task := &chromaSearchHeartbeatTask{
+		url:        heartbeatURL,
+		interval:   10 * time.Minute,
+		httpClient: &http.Client{Timeout: 30 * time.Second},
+		stopChan:   make(chan struct{}),
+	}
+
+	go task.run()
+}
+
+func (h *chromaSearchHeartbeatTask) run() {
+	ticker := time.NewTicker(h.interval)
+	defer ticker.Stop()
+
+	h.sendHeartbeat()
+
+	for {
+		select {
+		case <-h.stopChan:
+			return
+		case <-ticker.C:
+			h.sendHeartbeat()
+		}
+	}
+}
+
+func (h *chromaSearchHeartbeatTask) sendHeartbeat() {
+	resp, err := h.httpClient.Get(h.url)
+	if err != nil {
+		return
+	}
+	defer resp.Body.Close()
+	io.ReadAll(resp.Body)
+}

+ 1 - 55
shudao-go-backend/controllers/hazard.go

@@ -789,61 +789,7 @@ func uploadImageToOSS(imageData []byte, fileName string) (string, error) {
 	return proxyURL, nil
 }
 
-// TestWatermarkFunction 测试文字水印功能的简单函数
-func TestWatermarkFunction() {
-	fmt.Println("开始测试文字水印功能...")
-
-	// 读取测试图片
-	imagePath := "static/image/1.png"
-	imageFile, err := os.Open(imagePath)
-	if err != nil {
-		fmt.Printf("无法打开测试图片: %v\n", err)
-		return
-	}
-	defer imageFile.Close()
-
-	// 解码图片
-	img, err := png.Decode(imageFile)
-	if err != nil {
-		fmt.Printf("解码测试图片失败: %v\n", err)
-		return
-	}
-
-	fmt.Printf("成功读取测试图片,尺寸: %dx%d\n", img.Bounds().Dx(), img.Bounds().Dy())
-
-	// 转换为可绘制的RGBA图像
-	bounds := img.Bounds()
-	drawableImg := image.NewRGBA(bounds)
-	draw.Draw(drawableImg, bounds, img, image.Point{}, draw.Src)
-
-	// 添加logo
-	fmt.Println("添加logo...")
-	drawableImg = addLogoToImage(drawableImg)
-
-	// 添加文字水印
-	fmt.Println("添加文字水印...")
-	drawableImg = addTextWatermark(drawableImg, "测试用户", "1234", "2025/01/15")
-
-	// 将处理后的图片保存到static/image目录
-	outputPath := "static/image/test_output.jpg"
-	outputFile, err := os.Create(outputPath)
-	if err != nil {
-		fmt.Printf("创建输出文件失败: %v\n", err)
-		return
-	}
-	defer outputFile.Close()
-
-	// 编码为JPEG
-	if err := jpeg.Encode(outputFile, drawableImg, &jpeg.Options{Quality: 95}); err != nil {
-		fmt.Printf("编码图片失败: %v\n", err)
-		return
-	}
-
-	fmt.Printf("测试完成!处理后的图片已保存到: %s\n", outputPath)
-	fmt.Println("请查看图片效果,确认logo和文字水印是否正确显示")
-}
-
-// addTextWatermark 使用gg库添加45度角的文字水印(改进版)
+// addTextWatermark 使用gg库添加45度角的文字水印
 func addTextWatermark(img *image.RGBA, username, account, date string) *image.RGBA {
 	// 水印文本 - 使用传入的动态数据
 	watermarks := []string{username, account, date}

+ 0 - 186
shudao-go-backend/controllers/oss.go

@@ -1,186 +0,0 @@
-// Package controllers - oss.go
-//
-// ⚠️ DEPRECATED NOTICE (弃用说明)
-// ================================================================================
-// 本文件包含旧版OSS上传实现,已被 shudaooss.go 替代。
-// 建议使用 ShudaoOssController 进行文件上传操作。
-// ================================================================================
-package controllers
-
-import (
-	"crypto/hmac"
-	"crypto/sha256"
-	"encoding/base64"
-	"encoding/hex"
-	"encoding/json"
-	"fmt"
-	"shudao-chat-go/utils"
-	"strconv"
-	"time"
-
-	"github.com/beego/beego/v2/server/web"
-)
-
-type OssController struct {
-	web.Controller
-}
-
-// S3配置信息
-// S3配置信息
-var (
-	accessKeyId     string
-	accessKeySecret string
-	bucket          string
-	endpoint        string
-	region          string = "raoxi" // S3区域,可根据实际情况调整
-	host            string
-)
-
-func init() {
-	config := utils.GetOSSConfig()
-	accessKeyId = config["access_key"]
-	accessKeySecret = config["secret_key"]
-	bucket = config["bucket"]
-	endpoint = config["endpoint"]
-
-	// 移除endpoint中的http://或https://前缀用于host拼接(如果需要)
-	// 但原代码是: var host string = "http://" + endpoint + "/" + bucket
-	// utils.GetOSSConfig返回的endpoint可能带http
-	// 这里做个简单处理
-	cleanEndpoint := endpoint
-	if len(cleanEndpoint) > 7 && cleanEndpoint[:7] == "http://" {
-		cleanEndpoint = cleanEndpoint[7:]
-	} else if len(cleanEndpoint) > 8 && cleanEndpoint[:8] == "https://" {
-		cleanEndpoint = cleanEndpoint[8:]
-	}
-
-	host = "http://" + cleanEndpoint + "/" + bucket
-}
-
-// S3服务地址 - 标准S3格式
-// var host string = "http://" + endpoint + "/" + bucket // 已在init中初始化
-
-// 用户上传文件时指定的前缀
-var upload_dir string = "uploads/"
-var expire_time int64 = 1800 // 30分钟
-
-// S3策略文档结构
-type S3PolicyDocument struct {
-	Expiration string        `json:"expiration"`
-	Conditions []interface{} `json:"conditions"`
-}
-
-// S3响应结构
-type S3PolicyToken struct {
-	URL        string            `json:"url"`
-	Fields     map[string]string `json:"fields"`
-	Expire     int64             `json:"expire"`
-	StatusCode int               `json:"statusCode"`
-}
-
-// 生成AWS4签名
-func generateAWS4Signature(secretKey, dateStamp, region, stringToSign string) string {
-	kDate := hmacSHA256([]byte("AWS4"+secretKey), dateStamp)
-	kRegion := hmacSHA256(kDate, region)
-	kService := hmacSHA256(kRegion, "s3")
-	kSigning := hmacSHA256(kService, "aws4_request")
-	signature := hmacSHA256(kSigning, stringToSign)
-	return hex.EncodeToString(signature)
-}
-
-// HMAC-SHA256计算
-func hmacSHA256(key []byte, data string) []byte {
-	mac := hmac.New(sha256.New, key)
-	mac.Write([]byte(data))
-	return mac.Sum(nil)
-}
-
-func (c *OssController) Upload() {
-	// 设置CORS头
-	c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Origin", "*")
-	c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
-	c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Headers", "Content-Type")
-
-	// 处理OPTIONS预检请求
-	if c.Ctx.Request.Method == "OPTIONS" {
-		c.Ctx.ResponseWriter.WriteHeader(200)
-		return
-	}
-
-	// 从token中获取用户信息
-	userInfo, err := utils.GetUserInfoFromContext(c.Ctx.Input.GetData("userInfo"))
-	if err != nil {
-		c.Data["json"] = map[string]interface{}{
-			"statusCode": 401,
-			"error":      "获取用户信息失败: " + err.Error(),
-		}
-		c.ServeJSON()
-		return
-	}
-	user_id := int(userInfo.ID)
-	if user_id == 0 {
-		user_id = 1
-	}
-
-	// 生成时间相关字符串
-	now := time.Now().UTC()
-	expire_end := now.Unix() + expire_time
-	dateStamp := now.Format("20060102")
-	amzDate := now.Format("20060102T150405Z")
-	expiration := now.Add(time.Duration(expire_time) * time.Second).Format("2006-01-02T15:04:05.000Z")
-
-	// 生成credential
-	credential := fmt.Sprintf("%s/%s/%s/s3/aws4_request", accessKeyId, dateStamp, region)
-
-	// 生成上传目录
-	uploadDir := upload_dir + now.Format("01") + now.Format("02") + "/" + strconv.Itoa(user_id) + "/"
-
-	// 创建S3策略文档
-	policy := S3PolicyDocument{
-		Expiration: expiration,
-		Conditions: []interface{}{
-			map[string]string{"bucket": bucket},
-			[]interface{}{"starts-with", "$key", uploadDir},
-			map[string]string{"x-amz-algorithm": "AWS4-HMAC-SHA256"},
-			map[string]string{"x-amz-credential": credential},
-			map[string]string{"x-amz-date": amzDate},
-			[]interface{}{"content-length-range", "0", "104857600"}, // 最大100MB
-		},
-	}
-
-	// 将策略文档转换为JSON并进行Base64编码
-	policyJSON, err := json.Marshal(policy)
-	if err != nil {
-		c.Data["json"] = map[string]interface{}{
-			"statusCode": 500,
-			"error":      "Failed to create policy",
-		}
-		c.ServeJSON()
-		return
-	}
-
-	policyBase64 := base64.StdEncoding.EncodeToString(policyJSON)
-
-	// 生成AWS4签名
-	signature := generateAWS4Signature(accessKeySecret, dateStamp, region, policyBase64)
-
-	// 构建表单字段
-	fields := map[string]string{
-		"key":              uploadDir + "${filename}",
-		"policy":           policyBase64,
-		"x-amz-algorithm":  "AWS4-HMAC-SHA256",
-		"x-amz-credential": credential,
-		"x-amz-date":       amzDate,
-		"x-amz-signature":  signature,
-	}
-
-	// 构建响应
-	var policyToken S3PolicyToken
-	policyToken.StatusCode = 200
-	policyToken.URL = host
-	policyToken.Fields = fields
-	policyToken.Expire = expire_end
-
-	c.Data["json"] = policyToken
-	c.ServeJSON()
-}

+ 127 - 135
shudao-go-backend/controllers/shudaooss.go

@@ -2,6 +2,10 @@ package controllers
 
 import (
 	"bytes"
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/hex"
 	"encoding/json"
 	"fmt"
 	"image"
@@ -26,23 +30,27 @@ type ShudaoOssController struct {
 	web.Controller
 }
 
-// OSS配置信息 - 从配置文件读取
-// 注意: 这些变量在init()函数中初始化
+// OSS配置信息 - 延迟初始化
 var (
 	ossBucket    string
 	ossAccessKey string
 	ossSecretKey string
 	ossEndpoint  string
-	ossRegion    = "us-east-1" // 固定值
+	ossRegion    = "us-east-1"
+	ossInited    = false
 )
 
-// init 初始化OSS配置
-func init() {
+// initOSSConfig 延迟初始化OSS配置
+func initOSSConfig() {
+	if ossInited {
+		return
+	}
 	ossConfig := utils.GetOSSConfig()
 	ossBucket = ossConfig["bucket"]
 	ossAccessKey = ossConfig["access_key"]
 	ossSecretKey = ossConfig["secret_key"]
 	ossEndpoint = ossConfig["endpoint"]
+	ossInited = true
 }
 
 // 图片压缩配置
@@ -68,60 +76,33 @@ type UploadResponse struct {
 	FileSize   int64  `json:"fileSize"`
 }
 
-// 获取S3会话
+// getS3Session 获取S3会话
 func getS3Session() (*session.Session, error) {
-	// 检查时间同步
-	now := time.Now()
-	utcNow := now.UTC()
-	fmt.Printf("=== S3会话创建调试信息 ===\n")
-	fmt.Printf("本地时间: %s\n", now.Format("2006-01-02 15:04:05"))
-	fmt.Printf("UTC时间: %s\n", utcNow.Format("2006-01-02 15:04:05"))
-	fmt.Printf("时间差: %v\n", now.Sub(utcNow))
-	fmt.Printf("Bucket: %s\n", ossBucket)
-	fmt.Printf("Endpoint: %s\n", ossEndpoint)
-	fmt.Printf("Region: %s\n", ossRegion)
-	fmt.Printf("AccessKey: %s***\n", ossAccessKey[:8])
-
+	initOSSConfig()
 	s3Config := &aws.Config{
 		Credentials:      credentials.NewStaticCredentials(ossAccessKey, ossSecretKey, ""),
 		Endpoint:         aws.String(ossEndpoint),
 		Region:           aws.String(ossRegion),
-		S3ForcePathStyle: aws.Bool(true), // 使用路径样式而不是虚拟主机样式
-		DisableSSL:       aws.Bool(true), // 如果endpoint是http则禁用SSL
-		// 添加更多配置选项
-		LogLevel: aws.LogLevel(aws.LogDebug), // 启用调试日志
-		// 其他配置选项
-		MaxRetries: aws.Int(3),
-		// 其他配置选项
+		S3ForcePathStyle: aws.Bool(true),
+		DisableSSL:       aws.Bool(true),
+		MaxRetries:       aws.Int(3),
 	}
 
-	// 创建会话
 	sess, err := session.NewSession(s3Config)
 	if err != nil {
 		return nil, err
 	}
 
-	// 验证凭据
-	_, err = sess.Config.Credentials.Get()
-	if err != nil {
+	if _, err = sess.Config.Credentials.Get(); err != nil {
 		return nil, fmt.Errorf("凭据验证失败: %v", err)
 	}
 
 	return sess, nil
 }
 
-// 获取UTC时间同步的S3会话(根据测试文件优化配置)
+// getUTCS3Session 获取UTC时间同步的S3会话
 func getUTCS3Session() (*session.Session, error) {
-	// 强制使用UTC时间
-	utcNow := time.Now().UTC()
-	fmt.Printf("=== UTC S3会话创建调试信息 ===\n")
-	fmt.Printf("强制使用UTC时间: %s\n", utcNow.Format("2006-01-02 15:04:05"))
-	fmt.Printf("Bucket: %s\n", ossBucket)
-	fmt.Printf("Endpoint: %s\n", ossEndpoint)
-	fmt.Printf("Region: %s\n", ossRegion)
-	fmt.Printf("AccessKey: %s***\n", ossAccessKey[:8])
-
-	// 创建S3配置 - 使用与测试文件相同的配置
+	initOSSConfig()
 	s3Config := &aws.Config{
 		Credentials:      credentials.NewStaticCredentials(ossAccessKey, ossSecretKey, ""),
 		Endpoint:         aws.String(ossEndpoint),
@@ -230,17 +211,10 @@ func (c *ShudaoOssController) UploadImage() {
 
 	// 压缩图片
 	if EnableImageCompression {
-		fmt.Printf("开始压缩图片到200KB以下...\n")
-		compressedBytes, err := compressImage(fileBytes, MaxImageWidth, MaxImageHeight, 0) // 参数不再使用
-		if err != nil {
-			fmt.Printf("图片压缩失败,使用原始图片: %v\n", err)
-			// 压缩失败时使用原始图片
-		} else {
-			fmt.Printf("图片压缩完成,最终大小: %.2f KB\n", float64(len(compressedBytes))/1024)
+		compressedBytes, err := compressImage(fileBytes, MaxImageWidth, MaxImageHeight, 0)
+		if err == nil {
 			fileBytes = compressedBytes
 		}
-	} else {
-		fmt.Printf("图片压缩已禁用,使用原始图片\n")
 	}
 
 	// 获取UTC时间同步的S3会话(解决时区问题)
@@ -257,12 +231,7 @@ func (c *ShudaoOssController) UploadImage() {
 	// 创建S3服务
 	s3Client := s3.New(sess)
 
-	// 打印上传信息
-	fmt.Printf("正在上传图片: %s\n", fileName)
-	fmt.Printf("文件大小: %.2f KB\n", float64(len(fileBytes))/1024)
-	fmt.Printf("ContentType: %s\n", header.Header.Get("Content-Type"))
-
-	// 上传图片到S3 - 使用与测试文件相同的上传方式
+	// 上传图片到S3
 	_, err = s3Client.PutObject(&s3.PutObjectInput{
 		Bucket: aws.String(ossBucket),
 		Key:    aws.String(fileName),
@@ -271,7 +240,6 @@ func (c *ShudaoOssController) UploadImage() {
 	})
 
 	if err != nil {
-		fmt.Printf("上传图片失败: %v\n", err)
 		c.Data["json"] = UploadResponse{
 			StatusCode: 500,
 			Message:    "上传图片到OSS失败: " + err.Error(),
@@ -302,11 +270,7 @@ func (c *ShudaoOssController) UploadImage() {
 	// 	return
 	// }
 	permanentURL := fmt.Sprintf("%s/%s/%s", ossEndpoint, ossBucket, fileName)
-	// 使用代理接口包装URL,前端需要预览图片
 	proxyURL := utils.GetProxyURL(permanentURL)
-	fmt.Printf("图片上传成功: %s\n", fileName)
-	fmt.Printf("文件URL: %s\n", permanentURL)
-	fmt.Printf("代理URL: %s\n", proxyURL)
 
 	c.Data["json"] = UploadResponse{
 		StatusCode: 200,
@@ -407,11 +371,6 @@ func (c *ShudaoOssController) UploadPPTJson() {
 	// 创建S3服务
 	s3Client := s3.New(sess)
 
-	// 打印上传信息
-	fmt.Printf("正在上传JSON文件: %s\n", fileName)
-	fmt.Printf("文件大小: %.2f KB\n", float64(len(fileBytes))/1024)
-	fmt.Printf("ContentType: %s\n", header.Header.Get("Content-Type"))
-
 	// 上传JSON到S3
 	_, err = s3Client.PutObject(&s3.PutObjectInput{
 		Bucket:      aws.String(ossBucket),
@@ -422,7 +381,6 @@ func (c *ShudaoOssController) UploadPPTJson() {
 	})
 
 	if err != nil {
-		fmt.Printf("上传JSON文件失败: %v\n", err)
 		c.Data["json"] = UploadResponse{
 			StatusCode: 500,
 			Message:    "上传JSON文件到OSS失败: " + err.Error(),
@@ -433,11 +391,7 @@ func (c *ShudaoOssController) UploadPPTJson() {
 
 	// 生成永久URL
 	permanentURL := fmt.Sprintf("%s/%s/%s", ossEndpoint, ossBucket, fileName)
-	// 使用代理接口包装URL,前端需要访问文件
 	proxyURL := utils.GetProxyURL(permanentURL)
-	fmt.Printf("JSON文件上传成功: %s\n", fileName)
-	fmt.Printf("文件URL: %s\n", permanentURL)
-	fmt.Printf("代理URL: %s\n", proxyURL)
 
 	c.Data["json"] = UploadResponse{
 		StatusCode: 200,
@@ -473,7 +427,6 @@ func (c *ShudaoOssController) ParseOSS() {
 	// 解密URL
 	decryptedURL, err := utils.DecryptURL(encryptedURL)
 	if err != nil {
-		fmt.Printf("URL解密失败: %v, 加密URL: %s\n", err, encryptedURL)
 		c.Ctx.ResponseWriter.WriteHeader(400)
 		c.Ctx.WriteString("URL解密失败: " + err.Error())
 		return
@@ -482,8 +435,6 @@ func (c *ShudaoOssController) ParseOSS() {
 	// URL解码,处理可能的编码问题
 	decodedURL, err := neturl.QueryUnescape(decryptedURL)
 	if err != nil {
-		fmt.Printf("URL解码失败: %v, 解密后URL: %s\n", err, decryptedURL)
-		// 如果解码失败,使用解密后的URL
 		decodedURL = decryptedURL
 	}
 
@@ -491,48 +442,37 @@ func (c *ShudaoOssController) ParseOSS() {
 
 	// 检查是否是代理URL格式(包含?url=参数)
 	if strings.Contains(decodedURL, "?url=") {
-		// 解析代理URL,提取实际的OSS URL
 		parsedProxyURL, err := neturl.Parse(decodedURL)
 		if err != nil {
-			fmt.Printf("代理URL解析失败: %v, 解码后URL: %s\n", err, decodedURL)
 			c.Ctx.ResponseWriter.WriteHeader(400)
 			c.Ctx.WriteString("代理URL格式无效: " + err.Error())
 			return
 		}
 
-		// 获取实际的OSS URL
 		actualOSSURL = parsedProxyURL.Query().Get("url")
 		if actualOSSURL == "" {
-			fmt.Printf("代理URL中缺少url参数: %s\n", decodedURL)
 			c.Ctx.ResponseWriter.WriteHeader(400)
 			c.Ctx.WriteString("代理URL中缺少url参数")
 			return
 		}
 	} else {
-		// 直接使用传入的URL作为OSS URL
 		actualOSSURL = decodedURL
-		fmt.Printf("检测到直接OSS URL: %s\n", actualOSSURL)
 	}
 
 	// 验证实际OSS URL格式
 	parsedOSSURL, err := neturl.Parse(actualOSSURL)
 	if err != nil {
-		fmt.Printf("OSS URL解析失败: %v, OSS URL: %s\n", err, actualOSSURL)
 		c.Ctx.ResponseWriter.WriteHeader(400)
 		c.Ctx.WriteString("OSS URL格式无效: " + err.Error())
 		return
 	}
 
 	if parsedOSSURL.Scheme == "" {
-		fmt.Printf("OSS URL缺少协议方案: %s\n", actualOSSURL)
 		c.Ctx.ResponseWriter.WriteHeader(400)
 		c.Ctx.WriteString("OSS URL缺少协议方案")
 		return
 	}
 
-	fmt.Printf("代理请求 - 加密URL: %s, 解密后URL: %s, 解码后URL: %s, 实际OSS URL: %s, 协议: %s\n",
-		encryptedURL, decryptedURL, decodedURL, actualOSSURL, parsedOSSURL.Scheme)
-
 	// 创建HTTP客户端,设置超时时间
 	client := &http.Client{
 		Timeout: 30 * time.Second,
@@ -622,42 +562,17 @@ func (c *ShudaoOssController) ParseOSS() {
 
 // compressImage 压缩图片到目标大小
 func compressImage(imageData []byte, maxWidth, maxHeight int, quality int) ([]byte, error) {
-	// 解码图片
-	img, format, err := image.Decode(bytes.NewReader(imageData))
+	img, _, err := image.Decode(bytes.NewReader(imageData))
 	if err != nil {
 		return nil, fmt.Errorf("解码图片失败: %v", err)
 	}
 
-	// 获取原始尺寸
-	bounds := img.Bounds()
-	originalWidth := bounds.Dx()
-	originalHeight := bounds.Dy()
 	originalSize := len(imageData)
-
-	fmt.Printf("原始图片: %dx%d, 格式: %s, 大小: %.2f KB\n",
-		originalWidth, originalHeight, format, float64(originalSize)/1024)
-
-	// 如果原始文件已经小于目标大小,直接返回
 	if originalSize <= TargetFileSize {
-		fmt.Printf("文件已小于目标大小,无需压缩\n")
 		return imageData, nil
 	}
 
-	// 尝试不同的压缩策略
-	compressedData, err := compressToTargetSize(img, originalSize)
-	if err != nil {
-		return nil, err
-	}
-
-	finalSize := len(compressedData)
-	compressionRatio := float64(finalSize) / float64(originalSize) * 100
-
-	fmt.Printf("压缩完成: %.2f KB -> %.2f KB (压缩率: %.1f%%)\n",
-		float64(originalSize)/1024,
-		float64(finalSize)/1024,
-		compressionRatio)
-
-	return compressedData, nil
+	return compressToTargetSize(img, originalSize)
 }
 
 // compressToTargetSize 压缩到目标文件大小
@@ -667,24 +582,18 @@ func compressToTargetSize(img image.Image, originalSize int) ([]byte, error) {
 	originalHeight := bounds.Dy()
 
 	// 策略1: 先尝试调整质量,不改变尺寸
-	fmt.Printf("策略1: 调整质量压缩...\n")
-	compressedData, err := compressByQuality(img, originalSize)
+	compressedData, err := compressByQuality(img)
 	if err == nil && len(compressedData) <= TargetFileSize {
-		fmt.Printf("质量压缩成功,达到目标大小\n")
 		return compressedData, nil
 	}
 
 	// 策略2: 如果质量压缩不够,尝试缩小尺寸
-	fmt.Printf("策略2: 尺寸+质量压缩...\n")
-
-	// 计算需要缩小的比例
 	targetRatio := float64(TargetFileSize) / float64(originalSize)
-	sizeRatio := math.Sqrt(targetRatio * 0.8) // 留一些余量给质量调整
+	sizeRatio := math.Sqrt(targetRatio * 0.8)
 
 	newWidth := int(float64(originalWidth) * sizeRatio)
 	newHeight := int(float64(originalHeight) * sizeRatio)
 
-	// 确保最小尺寸
 	if newWidth < 100 {
 		newWidth = 100
 	}
@@ -692,21 +601,15 @@ func compressToTargetSize(img image.Image, originalSize int) ([]byte, error) {
 		newHeight = 100
 	}
 
-	fmt.Printf("调整尺寸: %dx%d -> %dx%d\n", originalWidth, originalHeight, newWidth, newHeight)
-
-	// 调整图片尺寸
 	resizedImg := resizeImage(img, newWidth, newHeight)
-
-	// 再次尝试质量压缩
-	return compressByQuality(resizedImg, originalSize)
+	return compressByQuality(resizedImg)
 }
 
 // compressByQuality 通过调整质量压缩图片
-func compressByQuality(img image.Image, originalSize int) ([]byte, error) {
+func compressByQuality(img image.Image) ([]byte, error) {
 	var bestResult []byte
-	var bestSize int = originalSize
+	var bestSize int = math.MaxInt32
 
-	// 从高质量到低质量尝试
 	qualities := []int{85, 70, 60, 50, 40, 30, 25, 20, 15, 10}
 
 	for _, quality := range qualities {
@@ -716,24 +619,17 @@ func compressByQuality(img image.Image, originalSize int) ([]byte, error) {
 		}
 
 		currentSize := buf.Len()
-		fmt.Printf("  质量 %d: %.2f KB\n", quality, float64(currentSize)/1024)
-
-		// 如果达到目标大小,直接返回
 		if currentSize <= TargetFileSize {
-			fmt.Printf("  达到目标大小,质量: %d\n", quality)
 			return buf.Bytes(), nil
 		}
 
-		// 记录最佳结果
 		if currentSize < bestSize {
 			bestSize = currentSize
 			bestResult = buf.Bytes()
 		}
 	}
 
-	// 如果没有达到目标大小,返回最佳结果
 	if bestResult != nil {
-		fmt.Printf("  未达到目标大小,使用最佳结果: %.2f KB\n", float64(bestSize)/1024)
 		return bestResult, nil
 	}
 
@@ -767,3 +663,99 @@ func resizeImage(img image.Image, newWidth, newHeight int) image.Image {
 
 	return resized
 }
+
+// S3策略文档结构
+type S3PolicyDocument struct {
+	Expiration string        `json:"expiration"`
+	Conditions []interface{} `json:"conditions"`
+}
+
+// S3响应结构
+type S3PolicyToken struct {
+	URL        string            `json:"url"`
+	Fields     map[string]string `json:"fields"`
+	Expire     int64             `json:"expire"`
+	StatusCode int               `json:"statusCode"`
+}
+
+// Upload 生成S3预签名上传凭证
+func (c *ShudaoOssController) Upload() {
+	initOSSConfig()
+	c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Origin", "*")
+	c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
+	c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Headers", "Content-Type")
+
+	if c.Ctx.Request.Method == "OPTIONS" {
+		c.Ctx.ResponseWriter.WriteHeader(200)
+		return
+	}
+
+	userInfo, err := utils.GetUserInfoFromContext(c.Ctx.Input.GetData("userInfo"))
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{"statusCode": 401, "error": "获取用户信息失败"}
+		c.ServeJSON()
+		return
+	}
+	userID := int(userInfo.ID)
+	if userID == 0 {
+		userID = 1
+	}
+
+	now := time.Now().UTC()
+	expireTime := int64(1800)
+	expireEnd := now.Unix() + expireTime
+	dateStamp := now.Format("20060102")
+	amzDate := now.Format("20060102T150405Z")
+	expiration := now.Add(time.Duration(expireTime) * time.Second).Format("2006-01-02T15:04:05.000Z")
+
+	credential := fmt.Sprintf("%s/%s/%s/s3/aws4_request", ossAccessKey, dateStamp, ossRegion)
+	uploadDir := fmt.Sprintf("uploads/%s/%d/", now.Format("0102"), userID)
+	host := fmt.Sprintf("%s/%s", ossEndpoint, ossBucket)
+
+	policy := S3PolicyDocument{
+		Expiration: expiration,
+		Conditions: []interface{}{
+			map[string]string{"bucket": ossBucket},
+			[]interface{}{"starts-with", "$key", uploadDir},
+			map[string]string{"x-amz-algorithm": "AWS4-HMAC-SHA256"},
+			map[string]string{"x-amz-credential": credential},
+			map[string]string{"x-amz-date": amzDate},
+			[]interface{}{"content-length-range", "0", "104857600"},
+		},
+	}
+
+	policyJSON, _ := json.Marshal(policy)
+	policyBase64 := base64.StdEncoding.EncodeToString(policyJSON)
+	signature := generateAWS4Signature(ossSecretKey, dateStamp, ossRegion, policyBase64)
+
+	c.Data["json"] = S3PolicyToken{
+		StatusCode: 200,
+		URL:        host,
+		Expire:     expireEnd,
+		Fields: map[string]string{
+			"key":              uploadDir + "${filename}",
+			"policy":           policyBase64,
+			"x-amz-algorithm":  "AWS4-HMAC-SHA256",
+			"x-amz-credential": credential,
+			"x-amz-date":       amzDate,
+			"x-amz-signature":  signature,
+		},
+	}
+	c.ServeJSON()
+}
+
+// generateAWS4Signature 生成AWS4签名
+func generateAWS4Signature(secretKey, dateStamp, region, stringToSign string) string {
+	kDate := hmacSHA256([]byte("AWS4"+secretKey), dateStamp)
+	kRegion := hmacSHA256(kDate, region)
+	kService := hmacSHA256(kRegion, "s3")
+	kSigning := hmacSHA256(kService, "aws4_request")
+	return hex.EncodeToString(hmacSHA256(kSigning, stringToSign))
+}
+
+// hmacSHA256 HMAC-SHA256计算
+func hmacSHA256(key []byte, data string) []byte {
+	mac := hmac.New(sha256.New, key)
+	mac.Write([]byte(data))
+	return mac.Sum(nil)
+}

+ 9 - 6
shudao-go-backend/controllers/test.go

@@ -32,29 +32,31 @@ import (
 	"github.com/aws/aws-sdk-go/service/s3"
 )
 
-// OSS配置信息
-// OSS配置信息
+// OSS配置信息 - 延迟初始化
 var (
 	testOssBucket    string
 	testOssAccessKey string
 	testOssSecretKey string
 	testOssEndpoint  string
 	testOssRegion    = "us-east-1"
+	testOssInited    = false
 )
 
-func init() {
-	// 注意: 这里复用主配置的OSS设置
-	// 如果测试需要独立的OSS配置,建议在app.conf中添加 test_oss_... 配置项
-	// 目前为了简化,直接使用主OSS配置
+func initTestOSSConfig() {
+	if testOssInited {
+		return
+	}
 	config := utils.GetOSSConfig()
 	testOssBucket = config["bucket"]
 	testOssAccessKey = config["access_key"]
 	testOssSecretKey = config["secret_key"]
 	testOssEndpoint = config["endpoint"]
+	testOssInited = true
 }
 
 // 批量上传文件到OSS
 func BatchUploadFilesToOSS() {
+	initTestOSSConfig()
 	fmt.Println("=== 开始批量上传文件到OSS ===")
 
 	// 设置文件文件夹路径
@@ -382,6 +384,7 @@ func extractIDFromFilename(filename string) (int, error) {
 
 // 创建S3会话
 func createS3Session() (*session.Session, error) {
+	initTestOSSConfig()
 	s3Config := &aws.Config{
 		Credentials:      credentials.NewStaticCredentials(testOssAccessKey, testOssSecretKey, ""),
 		Endpoint:         aws.String(testOssEndpoint),

+ 6 - 45
shudao-go-backend/main.go

@@ -1,15 +1,8 @@
 package main
 
 import (
-	"fmt"
-
-	// "shudao-chat-go/controllers"
-
 	_ "shudao-chat-go/routers"
-
-	// "shudao-chat-go/tests"
-
-	"shudao-chat-go/models"
+	"shudao-chat-go/controllers"
 	"shudao-chat-go/utils"
 
 	beego "github.com/beego/beego/v2/server/web"
@@ -17,29 +10,13 @@ import (
 )
 
 func main() {
-	// 启用JSON body解析 - Beego v2的配置方式
 	beego.BConfig.CopyRequestBody = true
 
-	// // 转换template_1.json为PPTX
-	// if err := tests.ConvertTemplate1ToPPTX(); err != nil {
-	// 	fmt.Printf("PPT转换失败: %v\n", err)
-	// }
-
-	// // 智能转换:根据大纲内容匹配模板并填充
-	// fmt.Println("\n=== 开始智能转换 ===")
-	// if err := tests.ConvertToSmartPresentation(); err != nil {
-	// 	fmt.Printf("智能转换失败: %v\n", err)
-	// }
-
-	// ============ 重要:先注册认证中间件,再注册CORS ============
-	fmt.Println("🔧 正在注册Token认证中间件...")
+	// 注册认证中间件
 	beego.InsertFilter("*", beego.BeforeRouter, utils.AuthMiddleware)
-	fmt.Println("✅ Token认证中间件注册完成 (BeforeRouter)")
-
 	beego.InsertFilter("*", beego.BeforeExec, utils.AuthMiddleware)
-	fmt.Println("✅ Token认证中间件注册完成 (BeforeExec)")
 
-	//解决跨域问题
+	// CORS配置
 	beego.InsertFilter("*", beego.BeforeRouter, cors.Allow(&cors.Options{
 		AllowOrigins:     []string{"*"},
 		AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
@@ -47,30 +24,14 @@ func main() {
 		ExposeHeaders:    []string{"Content-Length", "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Content-Type"},
 		AllowCredentials: true,
 	}))
-	fmt.Println("✅ CORS中间件注册完成")
 
-	//迁移total中所有表
-	// models.DB.AutoMigrate(&models.RecognitionRecord{})
-	// 设置静态文件路径
+	// 静态文件路径
 	beego.SetStaticPath("/assets", "assets")
 	beego.SetStaticPath("/static", "static")
-	beego.SetStaticPath("/src", "assets") // 将/src路径映射到assets目录
+	beego.SetStaticPath("/src", "assets")
 
 	// 启动心跳任务
-	models.StartHeartbeatTask()
-
-	// 批量匹配图片(取消注释下面的行来运行)
-	// fmt.Println("开始批量匹配图片...")
-	// controllers.BatchMatchImages()
-
-	// 批量上传图片(取消注释下面的行来运行)
-	// fmt.Println("开始批量上传图片...")
-	// controllers.BatchUploadImages()
-
-	// 批量上传文件(取消注释下面的行来运行)
-	// fmt.Println("开始批量上传文件...")
-	// controllers.BatchUploadFilesToOSS()
+	controllers.StartChromaSearchHeartbeatTask()
 
-	// 启动服务器
 	beego.Run()
 }

+ 0 - 107
shudao-go-backend/models/chroma.go

@@ -1,107 +0,0 @@
-package models
-
-import (
-	"context"
-	"fmt"
-	"io"
-	"net/http"
-	"time"
-
-	"github.com/beego/beego/v2/server/web"
-)
-
-// HeartbeatTask 心跳任务结构体
-type HeartbeatTask struct {
-	URL        string
-	Interval   time.Duration
-	HTTPClient *http.Client
-	ctx        context.Context
-	cancel     context.CancelFunc
-}
-
-// NewHeartbeatTask 创建新的心跳任务
-func NewHeartbeatTask(url string, interval time.Duration) *HeartbeatTask {
-	ctx, cancel := context.WithCancel(context.Background())
-	return &HeartbeatTask{
-		URL:        url,
-		Interval:   interval,
-		HTTPClient: &http.Client{Timeout: 30 * time.Second},
-		ctx:        ctx,
-		cancel:     cancel,
-	}
-}
-
-// Start 启动心跳任务
-func (h *HeartbeatTask) Start() {
-	go h.run()
-}
-
-// Stop 停止心跳任务
-func (h *HeartbeatTask) Stop() {
-	h.cancel()
-}
-
-// run 运行心跳任务
-func (h *HeartbeatTask) run() {
-	ticker := time.NewTicker(h.Interval)
-	defer ticker.Stop()
-
-	// 立即执行一次
-	h.sendHeartbeat()
-
-	for {
-		select {
-		case <-h.ctx.Done():
-			fmt.Println("心跳任务已停止")
-			return
-		case <-ticker.C:
-			h.sendHeartbeat()
-		}
-	}
-}
-
-// sendHeartbeat 发送心跳请求
-func (h *HeartbeatTask) sendHeartbeat() {
-	req, err := http.NewRequestWithContext(h.ctx, "GET", h.URL, nil)
-	if err != nil {
-		fmt.Printf("创建心跳请求失败: %v\n", err)
-		return
-	}
-
-	resp, err := h.HTTPClient.Do(req)
-	if err != nil {
-		fmt.Printf("心跳请求失败: %v\n", err)
-		return
-	}
-	defer resp.Body.Close()
-
-	body, err := io.ReadAll(resp.Body)
-	if err != nil {
-		fmt.Printf("读取心跳响应失败: %v\n", err)
-		return
-	}
-
-	if resp.StatusCode == http.StatusOK {
-		fmt.Printf("心跳成功 [%s] - 状态码: %d, 响应: %s\n",
-			time.Now().Format("2006-01-02 15:04:05"), resp.StatusCode, string(body))
-	} else {
-		fmt.Printf("心跳失败 [%s] - 状态码: %d, 响应: %s\n",
-			time.Now().Format("2006-01-02 15:04:05"), resp.StatusCode, string(body))
-	}
-}
-
-// StartHeartbeatTask 启动心跳任务(从配置中读取URL)
-func StartHeartbeatTask() {
-	// 从配置文件中读取心跳API URL
-	heartbeatURL, err := web.AppConfig.String("heartbeat_api_url")
-	if err != nil || heartbeatURL == "" {
-		fmt.Println("未配置心跳API URL,跳过心跳任务")
-		return
-	}
-
-	// 创建心跳任务,每10分钟执行一次
-	heartbeatTask := NewHeartbeatTask(heartbeatURL, 10*time.Minute)
-
-	fmt.Printf("启动心跳任务,目标URL: %s,间隔: 10分钟\n", heartbeatURL)
-	heartbeatTask.Start()
-}

+ 0 - 188
shudao-go-backend/models/tools.go

@@ -1,188 +0,0 @@
-package models
-
-import (
-	"bytes"
-	"crypto/md5"
-	"encoding/hex"
-	"encoding/json"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"math/rand"
-	"net/http"
-	"strconv"
-	"strings"
-	"time"
-)
-
-// 10位时间戳时间格式化
-func UnixToDate(timestamp int) string {
-	t := time.Unix(int64(timestamp), 0)
-	return t.Format("2006-01-02 15:04:05")
-}
-
-// 2006-01-02 15:04:05转换成10位时间戳
-func DateToUnix(str string) int64 {
-	template := "2006-01-02 15:04:05"
-	t, err := time.ParseInLocation(template, str, time.Local)
-	if err != nil {
-		fmt.Println(err)
-		return 0
-	}
-	return t.Unix()
-}
-
-// 10位时间戳
-func GetUnixTimestamp() int64 {
-	return time.Now().Unix()
-}
-
-// 13位时间戳
-func GetUnixNano() int64 {
-	return time.Now().UnixNano() / 1e6
-}
-
-// 2006-01-02 15:04:05
-func GetDate() string {
-	template := "2006-01-02 15:04:05"
-	return time.Now().Format(template)
-}
-
-// 一分钟后
-func AfterOne() string {
-	now := time.Now()
-	t, _ := time.ParseDuration("1m")
-	t1 := now.Add(t).Format("20060102150405")
-	return t1
-}
-
-// Md5加密
-func Md5(str string) string {
-	m := md5.New()
-	m.Write([]byte(str))
-	return string(hex.EncodeToString(m.Sum(nil)))
-}
-
-// 邀请码生成
-const (
-	BASE    = "E8S2DZX9WYLTN6BQF7CP5IK3MJUAR4HV"
-	DECIMAL = 32
-	PAD     = "A"
-	LEN     = 8
-)
-
-func Encode(uid uint64) string {
-	id := uid
-	mod := uint64(0)
-	res := ""
-	for id != 0 {
-		mod = id % DECIMAL
-		id = id / DECIMAL
-		res += string(BASE[mod])
-	}
-	resLen := len(res)
-	if resLen < LEN {
-		res += PAD
-		for i := 0; i < LEN-resLen-1; i++ {
-			res += string(BASE[(int(uid)+i)%DECIMAL])
-		}
-	}
-	return res
-}
-
-// 封装一个生产随机数的方法
-func GetRandomNum() string {
-	var str string
-	for i := 0; i < 6; i++ {
-		current := rand.Intn(10) //0-9   "math/rand"
-		str += strconv.Itoa(current)
-	}
-	return str
-}
-
-// 发送GET请求
-// url:请求地址
-// response:请求返回的内容
-func Get(url string) (response string) {
-	client := http.Client{Timeout: 5 * time.Second}
-	resp, error := client.Get(url)
-	defer resp.Body.Close()
-	if error != nil {
-		panic(error)
-	}
-
-	var buffer [512]byte
-	result := bytes.NewBuffer(nil)
-	for {
-		n, err := resp.Body.Read(buffer[0:])
-		result.Write(buffer[0:n])
-		if err != nil && err == io.EOF {
-			break
-		} else if err != nil {
-			panic(err)
-		}
-	}
-
-	response = result.String()
-	return
-}
-
-// post请求封装
-func Post(url string, data interface{}, contentType string) (content string) {
-	jsonStr, _ := json.Marshal(data)
-	req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
-	req.Header.Add("content-type", contentType)
-	if err != nil {
-		panic(err)
-	}
-	defer req.Body.Close()
-
-	client := &http.Client{Timeout: 5 * time.Second}
-	resp, error := client.Do(req)
-	if error != nil {
-		panic(error)
-	}
-	defer resp.Body.Close()
-
-	result, _ := ioutil.ReadAll(resp.Body)
-	content = string(result)
-	return
-}
-
-// 生成随机字符串
-func GetRandomString(length int) string {
-	str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
-	bytes := []byte(str)
-	result := []byte{}
-	r := rand.New(rand.NewSource(time.Now().UnixNano()))
-	for i := 0; i < length; i++ {
-		result = append(result, bytes[r.Intn(len(bytes))])
-	}
-	return string(result)
-}
-
-// 生成随机6位数
-func GenValidateCode(width int) string {
-	numeric := [10]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
-	r := len(numeric)
-	rand.Seed(time.Now().UnixNano())
-
-	var sb strings.Builder
-	for i := 0; i < width; i++ {
-		fmt.Fprintf(&sb, "%d", numeric[rand.Intn(r)])
-	}
-	return sb.String()
-}
-
-// 去重
-func Unique(s []string) []string {
-	m := make(map[string]struct{}, 0)
-	newS := make([]string, 0)
-	for _, i2 := range s {
-		if _, ok := m[i2]; !ok {
-			newS = append(newS, i2)
-			m[i2] = struct{}{}
-		}
-	}
-	return newS
-}

+ 1 - 1
shudao-go-backend/routers/router.go

@@ -27,7 +27,7 @@ func init() {
 		beego.NSRouter("/send_deepseek_message", &controllers.ChatController{}, "post:SendDeepSeekMessage"),
 
 		// OSS上传相关路由
-		beego.NSRouter("/oss/upload", &controllers.OssController{}, "post:Upload"),
+		beego.NSRouter("/oss/upload", &controllers.ShudaoOssController{}, "post:Upload"),
 		// 新的OSS接口路由
 		beego.NSRouter("/oss/shudao/upload_image", &controllers.ShudaoOssController{}, "post:UploadImage"),
 		// 上传JSON文件接口

BIN
shudao-go-backend/static/image/test_output.jpg


+ 61 - 138
shudao-go-backend/utils/auth_middleware.go

@@ -2,163 +2,86 @@ package utils
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/beego/beego/v2/server/web/context"
 )
 
+// 不需要认证的路径
+var skipPaths = []string{
+	"/stream-test",
+	"/simple-stream-test",
+	"/stream-chat-with-db-test",
+	"/assets/",
+	"/static/",
+	"/src/",
+	"/apiv1/oss/parse",
+	"/apiv1/auth/local_login",
+}
+
 // AuthMiddleware Token认证中间件
 func AuthMiddleware(ctx *context.Context) {
-	// ============ 最优先:立即打印,证明中间件被调用 ============
-	fmt.Printf("\n\n🚀🚀🚀🚀🚀 [AUTH中间件] 开始执行!\n")
-	fmt.Printf("   请求方法: %s\n", ctx.Request.Method)
-	fmt.Printf("   请求路径: %s\n", ctx.Request.URL.Path)
-	fmt.Printf("   完整URL: %s\n", ctx.Request.URL.String())
+	path := ctx.Request.URL.Path
 
-	// 添加一个 defer 来捕获任何 panic
-	defer func() {
-		if r := recover(); r != nil {
-			fmt.Printf("❌❌❌ [中间件] 发生panic: %v\n", r)
-		}
-	}()
-
-	// 跳过某些不需要认证的路由
-	skipPaths := []string{
-		"/stream-test",
-		"/simple-stream-test",
-		"/stream-chat-with-db-test",
-		"/assets/",
-		"/static/",
-		"/src/",
-		"/apiv1/oss/parse",        // OSS代理解析接口,用于图片/文件资源访问,不需要token认证
-		"/apiv1/auth/local_login", // 本地登录接口,不需要token认证
-	}
-
-	// 特殊处理:精确匹配根路径 "/"
-	if ctx.Request.URL.Path == "/" {
-		fmt.Printf("⏭️  [中间件] 跳过路径: / (根路径)\n\n")
+	// 跳过根路径
+	if path == "/" {
 		return
 	}
 
-	// 检查其他跳过路径
-	for _, path := range skipPaths {
-		// 精确匹配或前缀匹配(对于目录路径)
-		if ctx.Request.URL.Path == path || (len(ctx.Request.URL.Path) > len(path) && ctx.Request.URL.Path[:len(path)] == path) {
-			fmt.Printf("⏭️  [中间件] 跳过路径: %s (匹配规则: %s)\n\n", ctx.Request.URL.Path, path)
+	// 检查跳过路径
+	for _, skip := range skipPaths {
+		if path == skip || strings.HasPrefix(path, skip) {
 			return
 		}
 	}
 
-	// 对于API请求,验证token
-	if len(ctx.Request.URL.Path) >= 6 && ctx.Request.URL.Path[:6] == "/apiv1" {
-		// 打印所有请求头,帮助调试
-		fmt.Printf("\n========== Token认证中间件 ==========\n")
-		fmt.Printf("📍 请求路径: %s\n", ctx.Request.URL.Path)
-		fmt.Printf("📋 所有请求头:\n")
-		for key, values := range ctx.Request.Header {
-			fmt.Printf("   %s: %v\n", key, values)
-		}
-
-		// 获取token - 添加详细的调试信息
-		fmt.Printf("\n🔍 开始提取Token:\n")
-
-		token1 := ctx.Input.Header("token")
-		fmt.Printf("   尝试 'token': %s (长度: %d)\n", token1, len(token1))
-
-		token2 := ctx.Input.Header("Token")
-		fmt.Printf("   尝试 'Token': %s (长度: %d)\n", token2, len(token2))
-
-		token3 := ctx.Input.Header("Authorization")
-		fmt.Printf("   尝试 'Authorization': %s (长度: %d)\n", token3, len(token3))
-
-		// 确定最终使用的token
-		token := token1
-		if token == "" {
-			token = token2
-		}
-		if token == "" {
-			token = token3
-			// 如果是Bearer token格式,去掉"Bearer "前缀
-			if len(token) > 7 && token[:7] == "Bearer " {
-				oldToken := token
-				token = token[7:]
-				fmt.Printf("   去除Bearer前缀: %s -> %s\n", oldToken[:20]+"...", token[:20]+"...")
-			}
-		}
-
-		fmt.Printf("\n🔑 最终提取到的Token: %s (长度: %d)\n", token, len(token))
-
-		// 如果没有token,返回401
-		if token == "" {
-			fmt.Printf("❌❌❌ Token为空,拒绝请求\n")
-			fmt.Printf("❌ 原因:请求头中没有找到 token、Token 或 Authorization 字段\n")
-			fmt.Printf("=====================================\n\n")
-			ctx.Output.SetStatus(401)
-			ctx.Output.JSON(map[string]interface{}{
-				"statusCode": 401,
-				"msg":        "未提供认证token,请在请求头中添加 token 字段",
-			}, false, false)
-			return
-		}
-
-		// ========== 优先验证本地token ==========
-		fmt.Printf("\n🔍 [中间件] 尝试验证本地token...\n")
-		localClaims, err := VerifyLocalToken(token)
-		if err == nil && localClaims != nil {
-			// 本地token验证成功
-			fmt.Printf("✅ [中间件] 本地Token验证成功\n")
-			fmt.Printf("   - UserID: %d\n", localClaims.UserID)
-			fmt.Printf("   - Username: %s\n", localClaims.Username)
-			fmt.Printf("   - Role: %s\n", localClaims.Role)
+	// 仅对API请求验证token
+	if !strings.HasPrefix(path, "/apiv1") {
+		return
+	}
 
-			// 转换为TokenUserInfo格式
-			userInfo := ConvertLocalClaimsToTokenUserInfo(localClaims)
+	// 提取token
+	token := extractToken(ctx)
+	if token == "" {
+		ctx.Output.SetStatus(401)
+		ctx.Output.JSON(map[string]interface{}{
+			"statusCode": 401,
+			"msg":        "未提供认证token",
+		}, false, false)
+		return
+	}
 
-			// 存储到context
-			fmt.Printf("\n💾 [中间件] 将本地用户信息存储到context中...\n")
-			ctx.Input.SetData("userInfo", userInfo)
+	// 优先验证本地token
+	if localClaims, err := VerifyLocalToken(token); err == nil && localClaims != nil {
+		ctx.Input.SetData("userInfo", ConvertLocalClaimsToTokenUserInfo(localClaims))
+		return
+	}
 
-			fmt.Printf("=====================================\n\n")
-			return
-		}
+	// 统一认证token验证
+	userInfo, err := VerifyToken(token)
+	if err != nil {
+		ctx.Output.SetStatus(401)
+		ctx.Output.JSON(map[string]interface{}{
+			"statusCode": 401,
+			"msg":        fmt.Sprintf("token验证失败: %v", err),
+		}, false, false)
+		return
+	}
 
-		fmt.Printf("⚠️  [中间件] 本地token验证失败: %v, 尝试统一认证...\n", err)
+	ctx.Input.SetData("userInfo", userInfo)
+}
 
-		// ========== 统一认证token验证 ==========
-		// 验证token
-		userInfo, err := VerifyToken(token)
-		if err != nil {
-			fmt.Printf("❌❌❌ Token验证失败: %v\n", err)
-			fmt.Printf("❌ Token内容: %s\n", token)
-			fmt.Printf("❌ 请检查token是否正确或已过期\n")
-			fmt.Printf("=====================================\n\n")
-			ctx.Output.SetStatus(401)
-			ctx.Output.JSON(map[string]interface{}{
-				"statusCode": 401,
-				"msg":        fmt.Sprintf("token验证失败: %v", err),
-			}, false, false)
-			return
+// extractToken 从请求头提取token
+func extractToken(ctx *context.Context) string {
+	token := ctx.Input.Header("token")
+	if token == "" {
+		token = ctx.Input.Header("Token")
+	}
+	if token == "" {
+		token = ctx.Input.Header("Authorization")
+		if strings.HasPrefix(token, "Bearer ") {
+			token = token[7:]
 		}
-
-		// 打印解析出的用户信息
-		fmt.Printf("✅ Token验证成功,解析出的用户信息:\n")
-		fmt.Printf("   - AccountID: %s\n", userInfo.AccountID)
-		fmt.Printf("   - ID: %d\n", userInfo.ID)
-		fmt.Printf("   - Name: %s\n", userInfo.Name)
-		fmt.Printf("   - UserCode: %s\n", userInfo.UserCode)
-		fmt.Printf("   - ContactNumber: %s\n", userInfo.ContactNumber)
-		fmt.Printf("   - TokenType: %s\n", userInfo.TokenType)
-
-		// 将用户信息存储到context中,供后续handler使用
-		fmt.Printf("\n💾 [中间件] 将用户信息存储到context中...\n")
-		fmt.Printf("   存储的指针地址: %p\n", userInfo)
-		fmt.Printf("   存储的值: %+v\n", userInfo)
-		ctx.Input.SetData("userInfo", userInfo)
-
-		// 验证是否存储成功
-		storedData := ctx.Input.GetData("userInfo")
-		fmt.Printf("   验证存储: %T, 值=%+v\n", storedData, storedData)
-		fmt.Printf("=====================================\n\n")
-	} else {
-		fmt.Printf("⏭️  [中间件] 非API路径,不需要token验证: %s\n\n", ctx.Request.URL.Path)
 	}
+	return token
 }

+ 7 - 13
shudao-go-backend/utils/config.go

@@ -34,13 +34,8 @@ func GetConfigInt(key string, defaultValue int) int {
 	return value
 }
 
-// GetBaseURL 获取系统基础URL
-func GetBaseURL() string {
-	return strings.TrimRight(MustGetConfigString("base_url"), "/")
-}
-
 // GetProxyURL 生成OSS代理URL(加密版本)
-// 不再硬编码base_url,改为从配置读取
+// 返回相对路径,让前端自动使用当前域名
 func GetProxyURL(originalURL string) string {
 	if originalURL == "" {
 		return ""
@@ -51,18 +46,17 @@ func GetProxyURL(originalURL string) string {
 		return ""
 	}
 
-	baseURL := GetBaseURL()
-	return baseURL + "/apiv1/oss/parse/?url=" + encryptedURL
+	return "/apiv1/oss/parse/?url=" + encryptedURL
 }
 
 // GetMySQLConfig 获取MySQL配置
 func GetMySQLConfig() map[string]string {
 	return map[string]string{
-		"user": MustGetConfigString("mysql_user"),
-		"pass": MustGetConfigString("mysql_pass"),
-		"urls": MustGetConfigString("mysql_urls"),
-		"port": MustGetConfigString("mysql_port"),
-		"db":   MustGetConfigString("mysql_db"),
+		"user": MustGetConfigString("mysqluser"),
+		"pass": MustGetConfigString("mysqlpass"),
+		"urls": MustGetConfigString("mysqlurls"),
+		"port": MustGetConfigString("mysqlhttpport"),
+		"db":   MustGetConfigString("mysqldb"),
 	}
 }
 

+ 79 - 0
shudao-go-backend/utils/response.go

@@ -0,0 +1,79 @@
+package utils
+
+import (
+	"github.com/beego/beego/v2/server/web"
+)
+
+// Response 统一响应结构
+type Response struct {
+	StatusCode int         `json:"statusCode"`
+	Msg        string      `json:"msg"`
+	Data       interface{} `json:"data,omitempty"`
+}
+
+// BaseController 基础控制器,提供统一响应方法
+type BaseController struct {
+	web.Controller
+}
+
+// Success 成功响应
+func (c *BaseController) Success(data interface{}) {
+	c.Data["json"] = Response{
+		StatusCode: 200,
+		Msg:        "success",
+		Data:       data,
+	}
+	c.ServeJSON()
+}
+
+// SuccessMsg 成功响应(仅消息)
+func (c *BaseController) SuccessMsg(msg string) {
+	c.Data["json"] = Response{
+		StatusCode: 200,
+		Msg:        msg,
+	}
+	c.ServeJSON()
+}
+
+// Error 错误响应
+func (c *BaseController) Error(code int, msg string) {
+	c.Data["json"] = Response{
+		StatusCode: code,
+		Msg:        msg,
+	}
+	c.ServeJSON()
+}
+
+// ErrorWithData 错误响应(带数据)
+func (c *BaseController) ErrorWithData(code int, msg string, data interface{}) {
+	c.Data["json"] = Response{
+		StatusCode: code,
+		Msg:        msg,
+		Data:       data,
+	}
+	c.ServeJSON()
+}
+
+// GetUserID 从context获取用户ID,失败返回错误响应
+func (c *BaseController) GetUserID() (int, bool) {
+	userInfo, err := GetUserInfoFromContext(c.Ctx.Input.GetData("userInfo"))
+	if err != nil {
+		c.Error(401, "获取用户信息失败: "+err.Error())
+		return 0, false
+	}
+	userID := int(userInfo.ID)
+	if userID == 0 {
+		userID = 1
+	}
+	return userID, true
+}
+
+// GetUserInfo 从context获取完整用户信息
+func (c *BaseController) GetUserInfo() (*TokenUserInfo, bool) {
+	userInfo, err := GetUserInfoFromContext(c.Ctx.Input.GetData("userInfo"))
+	if err != nil {
+		c.Error(401, "获取用户信息失败: "+err.Error())
+		return nil, false
+	}
+	return userInfo, true
+}

+ 0 - 131
shudao-go-backend/utils/test_file_match.go

@@ -1,131 +0,0 @@
-package utils
-
-import (
-	"fmt"
-)
-
-// TestFileMatching 测试文件匹配功能
-func TestFileMatching() {
-	fmt.Println("=== 文件匹配测试 ===")
-
-	// 模拟数据库中的文件列表
-	mockFiles := []struct {
-		fileName string
-		filePath string
-	}{
-		{"安全生产管理制度.pdf", "/files/safety/安全生产管理制度.pdf"},
-		{"消防安全管理规定.docx", "/files/fire/消防安全管理规定.docx"},
-		{"职业健康安全管理手册.pdf", "/files/health/职业健康安全管理手册.pdf"},
-		{"环境保护管理制度.doc", "/files/env/环境保护管理制度.doc"},
-		{"生产安全操作规程.pdf", "/files/production/生产安全操作规程.pdf"},
-		{"安全培训教材.pptx", "/files/training/安全培训教材.pptx"},
-		{"应急预案模板.docx", "/files/emergency/应急预案模板.docx"},
-		{"安全检查表.xlsx", "/files/inspection/安全检查表.xlsx"},
-		{"事故调查报告.pdf", "/files/incident/事故调查报告.pdf"},
-		{"安全技术规范.pdf", "/files/tech/安全技术规范.pdf"},
-	}
-
-	// 测试用例
-	testCases := []struct {
-		query    string
-		expected string
-	}{
-		{"安全生产管理", "安全生产管理制度.pdf"},
-		{"消防安全", "消防安全管理规定.docx"},
-		{"职业健康", "职业健康安全管理手册.pdf"},
-		{"环境保护", "环境保护管理制度.doc"},
-		{"生产安全", "生产安全操作规程.pdf"},
-		{"安全培训", "安全培训教材.pptx"},
-		{"应急预案", "应急预案模板.docx"},
-		{"安全检查", "安全检查表.xlsx"},
-		{"事故调查", "事故调查报告.pdf"},
-		{"安全技术", "安全技术规范.pdf"},
-		{"完全不匹配的文件", ""}, // 应该返回相似度最低的
-	}
-
-	for i, tc := range testCases {
-		fmt.Printf("\n测试用例 %d:\n", i+1)
-		fmt.Printf("查询: %s\n", tc.query)
-
-		// 提取文件名作为候选列表
-		var candidates []string
-		for _, file := range mockFiles {
-			candidates = append(candidates, file.fileName)
-		}
-
-		// 使用编辑距离算法找到最相似的文件名
-		bestMatch, bestScore := FindBestMatch(tc.query, candidates)
-
-		// 找到对应的文件路径
-		var matchedPath string
-		for _, file := range mockFiles {
-			if file.fileName == bestMatch {
-				matchedPath = file.filePath
-				break
-			}
-		}
-
-		fmt.Printf("最佳匹配: %s (相似度: %.3f)\n", bestMatch, bestScore)
-		fmt.Printf("文件路径: %s\n", matchedPath)
-
-		// 显示所有匹配结果(按相似度排序)
-		allMatches := FindBestMatches(tc.query, candidates)
-		fmt.Println("所有匹配结果(前5个):")
-		for j, match := range allMatches {
-			if j >= 5 { // 只显示前5个
-				break
-			}
-			fmt.Printf("  %d. %s (相似度: %.3f)\n", j+1, match.Text, match.Score)
-		}
-
-		// 设置阈值测试
-		threshold := 0.3
-		if bestScore >= threshold {
-			fmt.Printf("✅ 相似度 %.3f >= %.1f,匹配成功\n", bestScore, threshold)
-		} else {
-			fmt.Printf("❌ 相似度 %.3f < %.1f,匹配失败\n", bestScore, threshold)
-		}
-	}
-}
-
-// TestSimilarityThresholds 测试不同相似度阈值的效果
-func TestSimilarityThresholds() {
-	fmt.Println("\n=== 相似度阈值测试 ===")
-
-	query := "安全生产管理"
-	candidates := []string{
-		"安全生产管理制度.pdf",
-		"消防安全管理规定.docx",
-		"职业健康安全管理手册.pdf",
-		"环境保护管理制度.doc",
-		"生产安全操作规程.pdf",
-		"安全培训教材.pptx",
-		"应急预案模板.docx",
-		"安全检查表.xlsx",
-		"事故调查报告.pdf",
-		"安全技术规范.pdf",
-	}
-
-	thresholds := []float64{0.1, 0.3, 0.5, 0.7, 0.9}
-
-	for _, threshold := range thresholds {
-		fmt.Printf("\n阈值 %.1f 的匹配结果:\n", threshold)
-		allMatches := FindBestMatches(query, candidates)
-		count := 0
-		for _, match := range allMatches {
-			if match.Score >= threshold {
-				fmt.Printf("  %s (相似度: %.3f)\n", match.Text, match.Score)
-				count++
-			}
-		}
-		if count == 0 {
-			fmt.Printf("  没有找到相似度 >= %.1f 的匹配\n", threshold)
-		}
-	}
-}
-
-// RunAllFileTests 运行所有文件匹配测试
-func RunAllFileTests() {
-	TestFileMatching()
-	TestSimilarityThresholds()
-}

+ 0 - 149
shudao-go-backend/utils/test_string_match.go

@@ -1,149 +0,0 @@
-package utils
-
-import (
-	"fmt"
-)
-
-// TestStringMatching 测试字符串匹配算法
-func TestStringMatching() {
-	fmt.Println("=== 字符串模糊匹配测试 ===")
-
-	// 测试用例1:安全生产相关
-	fmt.Println("\n测试用例1:安全生产相关")
-	target1 := "安全生产管理"
-	candidates1 := []string{
-		"安全生产管理制度",
-		"安全管理规定",
-		"生产安全规范",
-		"安全生产标准",
-		"安全管理制度",
-		"生产管理规定",
-		"安全生产条例",
-	}
-
-	bestMatch1, bestScore1 := FindBestMatch(target1, candidates1)
-	fmt.Printf("目标: %s\n", target1)
-	fmt.Printf("最佳匹配: %s (相似度: %.3f)\n", bestMatch1, bestScore1)
-
-	allMatches1 := FindBestMatches(target1, candidates1)
-	fmt.Println("所有匹配结果(按相似度排序):")
-	for i, match := range allMatches1 {
-		fmt.Printf("  %d. %s (相似度: %.3f)\n", i+1, match.Text, match.Score)
-	}
-
-	// 测试用例2:消防安全相关
-	fmt.Println("\n测试用例2:消防安全相关")
-	target2 := "消防安全"
-	candidates2 := []string{
-		"消防安全管理制度",
-		"消防管理规定",
-		"消防安全规范",
-		"消防设备管理",
-		"消防安全检查",
-		"消防应急预案",
-		"消防安全培训",
-	}
-
-	bestMatch2, bestScore2 := FindBestMatch(target2, candidates2)
-	fmt.Printf("目标: %s\n", target2)
-	fmt.Printf("最佳匹配: %s (相似度: %.3f)\n", bestMatch2, bestScore2)
-
-	allMatches2 := FindBestMatches(target2, candidates2)
-	fmt.Println("所有匹配结果(按相似度排序):")
-	for i, match := range allMatches2 {
-		fmt.Printf("  %d. %s (相似度: %.3f)\n", i+1, match.Text, match.Score)
-	}
-
-	// 测试用例3:完全不同的字符串
-	fmt.Println("\n测试用例3:完全不同的字符串")
-	target3 := "环境保护"
-	candidates3 := []string{
-		"安全生产管理",
-		"消防安全制度",
-		"职业健康安全",
-		"环境保护管理",
-		"环境监测制度",
-		"环境保护规定",
-		"环境安全管理",
-	}
-
-	bestMatch3, bestScore3 := FindBestMatch(target3, candidates3)
-	fmt.Printf("目标: %s\n", target3)
-	fmt.Printf("最佳匹配: %s (相似度: %.3f)\n", bestMatch3, bestScore3)
-
-	allMatches3 := FindBestMatches(target3, candidates3)
-	fmt.Println("所有匹配结果(按相似度排序):")
-	for i, match := range allMatches3 {
-		fmt.Printf("  %d. %s (相似度: %.3f)\n", i+1, match.Text, match.Score)
-	}
-}
-
-// TestSimilarityCalculation 测试相似度计算
-func TestSimilarityCalculation() {
-	fmt.Println("\n=== 相似度计算测试 ===")
-
-	testCases := []struct {
-		s1, s2 string
-		desc   string
-	}{
-		{"安全生产管理", "安全生产管理制度", "部分匹配"},
-		{"安全管理", "安全生产管理", "包含关系"},
-		{"生产安全", "安全生产", "顺序不同"},
-		{"完全不同的字符串", "另一个完全不同的字符串", "完全不同"},
-		{"相同的字符串", "相同的字符串", "完全相同"},
-		{"", "", "空字符串"},
-		{"短", "很长的字符串", "长度差异很大"},
-		{"安全生产", "安全生产", "完全相同"},
-		{"安全", "安全生产管理", "子字符串"},
-	}
-
-	for _, tc := range testCases {
-		similarity := StringSimilarity(tc.s1, tc.s2)
-		fmt.Printf("%s: '%s' vs '%s' = %.3f\n", tc.desc, tc.s1, tc.s2, similarity)
-	}
-}
-
-// TestWithThreshold 测试带阈值的匹配
-func TestWithThreshold() {
-	fmt.Println("\n=== 带阈值的匹配测试 ===")
-
-	target := "安全管理"
-	candidates := []string{
-		"安全生产管理制度",
-		"安全管理规定",
-		"生产安全规范",
-		"安全生产标准",
-		"安全管理制度",
-		"生产管理规定",
-		"安全生产条例",
-		"职业健康安全管理",
-		"消防安全管理",
-		"环境安全管理",
-		"完全不同的内容",
-		"其他不相关的内容",
-	}
-
-	thresholds := []float64{0.3, 0.5, 0.7, 0.9}
-
-	for _, threshold := range thresholds {
-		fmt.Printf("\n阈值 %.1f 的匹配结果:\n", threshold)
-		allMatches := FindBestMatches(target, candidates)
-		count := 0
-		for _, match := range allMatches {
-			if match.Score >= threshold {
-				fmt.Printf("  %s (相似度: %.3f)\n", match.Text, match.Score)
-				count++
-			}
-		}
-		if count == 0 {
-			fmt.Printf("  没有找到相似度 >= %.1f 的匹配\n", threshold)
-		}
-	}
-}
-
-// RunAllTests 运行所有测试
-func RunAllTests() {
-	TestStringMatching()
-	TestSimilarityCalculation()
-	TestWithThreshold()
-}

+ 10 - 63
shudao-go-backend/utils/token.go

@@ -6,9 +6,8 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"strings"
 	"time"
-
-	"github.com/beego/beego/v2/server/web"
 )
 
 // TokenUserInfo 从token验证API返回的用户信息
@@ -29,91 +28,55 @@ func VerifyToken(token string) (*TokenUserInfo, error) {
 		return nil, fmt.Errorf("token不能为空")
 	}
 
-	// 从配置文件读取token验证API URL
-	authAPIURL, err := web.AppConfig.String("auth_api_url")
-	if err != nil || authAPIURL == "" {
-		// 如果配置不存在,使用默认值
-		authAPIURL = "https://aqai.shudaodsj.com:22000/api/auth/verify"
-	}
-
-	fmt.Printf("🔐 开始验证Token...\n")
-	fmt.Printf("   验证API: %s\n", authAPIURL)
+	authAPIURL := GetConfigString("auth_api_url", "")
 
-	// 构建请求体
-	requestBody := map[string]string{
-		"token": token,
-	}
+	jsonData, _ := json.Marshal(map[string]string{"token": token})
 
-	jsonData, err := json.Marshal(requestBody)
-	if err != nil {
-		return nil, fmt.Errorf("序列化请求体失败: %v", err)
-	}
-
-	// 创建HTTP请求
 	req, err := http.NewRequest("POST", authAPIURL, bytes.NewBuffer(jsonData))
 	if err != nil {
 		return nil, fmt.Errorf("创建请求失败: %v", err)
 	}
 
 	req.Header.Set("Content-Type", "application/json")
-	// 重要:认证API需要在请求头中也携带 Authorization
 	req.Header.Set("Authorization", "Bearer "+token)
 
-	fmt.Printf("   请求头 Authorization: Bearer %s...\n", token[:20])
-
-	// 发送请求
 	client := &http.Client{Timeout: 10 * time.Second}
 	resp, err := client.Do(req)
 	if err != nil {
-		fmt.Printf("❌ 请求token验证API失败: %v\n", err)
 		return nil, fmt.Errorf("请求token验证API失败: %v", err)
 	}
 	defer resp.Body.Close()
 
-	// 读取响应体
 	body, err := io.ReadAll(resp.Body)
 	if err != nil {
-		fmt.Printf("❌ 读取响应失败: %v\n", err)
 		return nil, fmt.Errorf("读取响应失败: %v", err)
 	}
 
-	fmt.Printf("   响应状态码: %d\n", resp.StatusCode)
-	fmt.Printf("   响应内容: %s\n", string(body))
-
-	// 检查HTTP状态码
 	if resp.StatusCode != http.StatusOK {
-		return nil, fmt.Errorf("token验证失败,状态码: %d, 响应: %s", resp.StatusCode, string(body))
+		return nil, fmt.Errorf("token验证失败,状态码: %d", resp.StatusCode)
 	}
 
-	// 解析响应
 	var userInfo TokenUserInfo
 	if err := json.Unmarshal(body, &userInfo); err != nil {
-		fmt.Printf("❌ 解析响应失败: %v\n", err)
-		return nil, fmt.Errorf("解析token验证响应失败: %v", err)
+		return nil, fmt.Errorf("解析响应失败: %v", err)
 	}
 
-	// 检查token是否过期
 	if userInfo.Exp > 0 && time.Now().Unix() > userInfo.Exp {
-		fmt.Printf("❌ Token已过期 (exp: %d, now: %d)\n", userInfo.Exp, time.Now().Unix())
 		return nil, fmt.Errorf("token已过期")
 	}
 
-	fmt.Printf("✅ Token验证成功\n")
 	return &userInfo, nil
 }
 
-// GetUserInfoFromToken 从请求头中获取token并验证,返回用户信息
+// GetUserInfoFromToken 从请求头中获取token并验证
 func GetUserInfoFromToken(headerFunc func(string) string) (*TokenUserInfo, error) {
-	// 尝试从多个可能的header字段获取token
 	token := headerFunc("token")
-	fmt.Print("token", token)
 	if token == "" {
 		token = headerFunc("Token")
 	}
 	if token == "" {
 		token = headerFunc("Authorization")
-		// 如果是Bearer token格式,去掉"Bearer "前缀
-		if len(token) > 7 && token[:7] == "Bearer " {
+		if strings.HasPrefix(token, "Bearer ") {
 			token = token[7:]
 		}
 	}
@@ -125,36 +88,20 @@ func GetUserInfoFromToken(headerFunc func(string) string) (*TokenUserInfo, error
 	return VerifyToken(token)
 }
 
-// GetUserInfoFromContext 从Beego Context中获取已验证的用户信息
-// 该函数假定中间件已经验证过token并将用户信息存储在context中
+// GetUserInfoFromContext 从Context中获取已验证的用户信息
 func GetUserInfoFromContext(input interface{}) (*TokenUserInfo, error) {
-	// 添加调试信息
-	fmt.Printf("\n🔍 [GetUserInfoFromContext] 开始解析用户信息\n")
-	fmt.Printf("   输入类型: %T\n", input)
-	fmt.Printf("   输入值: %+v\n", input)
-
-	// 检查input是否为nil
 	if input == nil {
-		fmt.Printf("❌ [GetUserInfoFromContext] input为nil\n\n")
-		return nil, fmt.Errorf("未找到用户信息,context中userInfo为nil,请确保已经过token认证")
+		return nil, fmt.Errorf("未找到用户信息")
 	}
 
-	// 从context中获取userInfo
 	userInfo, ok := input.(*TokenUserInfo)
 	if !ok {
-		fmt.Printf("❌ [GetUserInfoFromContext] 类型断言失败,期望 *TokenUserInfo,实际得到 %T\n\n", input)
-		return nil, fmt.Errorf("用户信息类型错误,期望 *TokenUserInfo,实际得到 %T", input)
+		return nil, fmt.Errorf("用户信息类型错误")
 	}
 
 	if userInfo == nil {
-		fmt.Printf("❌ [GetUserInfoFromContext] userInfo为nil\n\n")
 		return nil, fmt.Errorf("用户信息为空")
 	}
 
-	fmt.Printf("✅ [GetUserInfoFromContext] 成功解析用户信息\n")
-	fmt.Printf("   - AccountID: %s\n", userInfo.AccountID)
-	fmt.Printf("   - ID: %d\n", userInfo.ID)
-	fmt.Printf("   - Name: %s\n\n", userInfo.Name)
-
 	return userInfo, nil
 }

+ 169 - 0
shudao-go-backend/utils/tools.go

@@ -0,0 +1,169 @@
+package utils
+
+import (
+	"bytes"
+	"crypto/md5"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"io"
+	"math/rand"
+	"net/http"
+	"strings"
+	"time"
+)
+
+// UnixToDate 10位时间戳转日期字符串
+func UnixToDate(timestamp int) string {
+	return time.Unix(int64(timestamp), 0).Format("2006-01-02 15:04:05")
+}
+
+// DateToUnix 日期字符串转10位时间戳
+func DateToUnix(str string) int64 {
+	t, err := time.ParseInLocation("2006-01-02 15:04:05", str, time.Local)
+	if err != nil {
+		return 0
+	}
+	return t.Unix()
+}
+
+// GetUnixTimestamp 获取10位时间戳
+func GetUnixTimestamp() int64 {
+	return time.Now().Unix()
+}
+
+// GetUnixNano 获取13位时间戳
+func GetUnixNano() int64 {
+	return time.Now().UnixNano() / 1e6
+}
+
+// GetDate 获取当前日期时间字符串
+func GetDate() string {
+	return time.Now().Format("2006-01-02 15:04:05")
+}
+
+// Md5 MD5加密
+func Md5(str string) string {
+	m := md5.New()
+	m.Write([]byte(str))
+	return hex.EncodeToString(m.Sum(nil))
+}
+
+// 邀请码生成常量
+const (
+	inviteCodeBase    = "E8S2DZX9WYLTN6BQF7CP5IK3MJUAR4HV"
+	inviteCodeDecimal = 32
+	inviteCodePad     = "A"
+	inviteCodeLen     = 8
+)
+
+// EncodeInviteCode 生成邀请码
+func EncodeInviteCode(uid uint64) string {
+	id := uid
+	res := ""
+	for id != 0 {
+		mod := id % inviteCodeDecimal
+		id = id / inviteCodeDecimal
+		res += string(inviteCodeBase[mod])
+	}
+	if len(res) < inviteCodeLen {
+		res += inviteCodePad
+		for i := 0; i < inviteCodeLen-len(res)-1; i++ {
+			res += string(inviteCodeBase[(int(uid)+i)%inviteCodeDecimal])
+		}
+	}
+	return res
+}
+
+// GetRandomCode 生成指定长度的随机数字字符串
+func GetRandomCode(length int) string {
+	var sb strings.Builder
+	r := rand.New(rand.NewSource(time.Now().UnixNano()))
+	for i := 0; i < length; i++ {
+		fmt.Fprintf(&sb, "%d", r.Intn(10))
+	}
+	return sb.String()
+}
+
+// GetRandomString 生成指定长度的随机字符串
+func GetRandomString(length int) string {
+	const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+	result := make([]byte, length)
+	r := rand.New(rand.NewSource(time.Now().UnixNano()))
+	for i := 0; i < length; i++ {
+		result[i] = chars[r.Intn(len(chars))]
+	}
+	return string(result)
+}
+
+// UniqueStrings 字符串切片去重
+func UniqueStrings(s []string) []string {
+	seen := make(map[string]struct{})
+	result := make([]string, 0, len(s))
+	for _, v := range s {
+		if _, ok := seen[v]; !ok {
+			seen[v] = struct{}{}
+			result = append(result, v)
+		}
+	}
+	return result
+}
+
+// HTTPGet 发送GET请求
+func HTTPGet(url string) (string, error) {
+	client := &http.Client{Timeout: 5 * time.Second}
+	resp, err := client.Get(url)
+	if err != nil {
+		return "", err
+	}
+	defer resp.Body.Close()
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return "", err
+	}
+	return string(body), nil
+}
+
+// HTTPPost 发送POST请求
+func HTTPPost(url string, data interface{}, contentType string) (string, error) {
+	jsonData, err := json.Marshal(data)
+	if err != nil {
+		return "", err
+	}
+
+	req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
+	if err != nil {
+		return "", err
+	}
+	req.Header.Set("Content-Type", contentType)
+
+	client := &http.Client{Timeout: 5 * time.Second}
+	resp, err := client.Do(req)
+	if err != nil {
+		return "", err
+	}
+	defer resp.Body.Close()
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return "", err
+	}
+	return string(body), nil
+}
+
+// Min 返回两个整数中的较小值
+func Min(a, b int) int {
+	if a < b {
+		return a
+	}
+	return b
+}
+
+// Max 返回两个整数中的较大值
+func Max(a, b int) int {
+	if a > b {
+		return a
+	}
+	return b
+}

+ 5 - 8
shudao-vue-frontend/src/components/ExportButton.vue

@@ -26,7 +26,7 @@
 import { ref } from 'vue'
 import { ElMessage } from 'element-plus'
 import { Download, ArrowDown, Document } from '@element-plus/icons-vue'
-import { buildApiUrl } from '@/utils/apiConfig'
+import { buildApiUrl, REPORT_API_PREFIX, SSE_API_PREFIX } from '@/utils/apiConfig'
 
 const props = defineProps({
   reports: {
@@ -74,14 +74,11 @@ const handleExport = async (format) => {
     const result = await response.json()
     
     // 使用返回的下载URL下载文件
-    // 处理不同格式的URL:
-    // 1. 如果是完整URL(http/https开头),直接使用
-    // 2. 如果是相对路径且以/api/v1开头,需要在生产环境添加/chatwithai前缀
-    // 3. 如果已经包含/chatwithai,直接使用
+    // 后端返回的是相对路径,需要根据环境添加正确的前缀
     let downloadUrl = result.download_url
-    if (downloadUrl.startsWith('/api/v1') && import.meta.env.PROD) {
-      // 后端返回的是 /api/v1/xxx 格式,在生产环境需要添加 /chatwithai 前缀
-      downloadUrl = `/chatwithai${downloadUrl}`
+    if (downloadUrl.startsWith('/api/v1')) {
+      // 替换为当前环境的正确前缀
+      downloadUrl = downloadUrl.replace('/api/v1', SSE_API_PREFIX)
     }
     const downloadResponse = await fetch(downloadUrl)
     if (!downloadResponse.ok) {

+ 1 - 20
shudao-vue-frontend/src/main.js

@@ -87,40 +87,21 @@ import { handleTicketAuth, clearTicketFromUrl } from '@/utils/ticketAuth.js'
 
 // ===== 新增:异步初始化应用 =====
 async function initApp() {
-  // ===== 已删除:认证配置加载(不再需要) =====
-  
-  // ===== 票据认证(完全依赖票据) =====
-  // 1. 处理票据认证
-  // 认证策略:
-  //   - 有票据 + 处理成功 → 正常进入
-  //   - 有票据 + 处理失败 → 跳转404
-  //   - 无票据 + 有本地令牌 → 使用本地令牌进入
-  //   - 无票据 + 无本地令牌 → 跳转404
   let authResult = null
   let authError = null
   
+  // 执行票据认证(开发模式会自动获取默认票据)
   try {
     authResult = await handleTicketAuth()
     
     if (authResult && authResult.success) {
       console.log('✅ 票据认证成功')
-      console.log('🔑 Token类型:', authResult.token?.tokenType)
-      console.log('🔑 Refresh Token:', authResult.token?.refreshToken?.substring(0, 50) + '...')
-      
-      // 显示认证来源
       if (authResult.fromTicket) {
-        console.log('🎫 认证来源: 票据处理')
-        console.log('🧹 票据认证成功,开始清理 URL 参数')
         clearTicketFromUrl()
-      } else if (authResult.fromCache) {
-        console.log('💾 认证来源: 本地令牌')
       }
     }
   } catch (error) {
     console.error('❌ 票据认证失败:', error)
-    console.error('❌ 错误类型:', error.message)
-    
-    // 保存错误信息,稍后跳转
     authError = error
   }
   

+ 17 - 6
shudao-vue-frontend/src/request/axios.js

@@ -6,10 +6,10 @@ import { isAuthenticating } from '../utils/ticketAuth'
 // ===== 已删除:用户ID管理工具(不再需要,改用token) =====
 // import { getCurrentUserId } from '../utils/userManager'
 
+import { BACKEND_API_PREFIX } from '../utils/apiConfig'
+
 const http = axios.create({
-    // baseURL: "http://127.0.0.1:22000/apiv1",//测试环境
-    // baseURL: "http://172.16.29.101:22000/apiv1",//生产环境
-    baseURL: window.location.origin + "/apiv1",//生产环境2
+    baseURL: window.location.origin + BACKEND_API_PREFIX,
     timeout: 600000,
 })
 
@@ -51,7 +51,7 @@ async function recordTrackingAsync(apiPath, method) {
         }
         
         // 使用 fetch 发送埋点请求,避免使用 axios 造成循环
-        fetch(window.location.origin + '/apiv1/tracking/record', {
+        fetch(window.location.origin + BACKEND_API_PREFIX + '/tracking/record', {
             method: 'POST',
             headers: headers,
             body: JSON.stringify(trackingData)
@@ -78,10 +78,16 @@ http.interceptors.request.use((config) => {
     const token = getToken()
     const tokenType = getTokenType()
     
-    if (token) {
+    // 开发模式下,跳过特定接口的 token 添加
+    const skipTokenPaths = ['/report/', '/sse/', '/chatwithai/']
+    const shouldSkipToken = import.meta.env.DEV && skipTokenPaths.some(path => config.url?.includes(path))
+    
+    if (token && !shouldSkipToken) {
         // 格式:Authorization: Bearer {refresh_token}
         config.headers['Authorization'] = `${tokenType.charAt(0).toUpperCase() + tokenType.slice(1)} ${token}`
         console.log('🔑 已添加 Authorization 头:', `${tokenType} ${token.substring(0, 50)}...`)
+    } else if (shouldSkipToken) {
+        console.log('🔧 开发模式:跳过 token 添加')
     }
     
     if (config.method === 'get') {
@@ -95,7 +101,7 @@ http.interceptors.request.use((config) => {
     
     // ===== 新增:记录埋点 =====
     // 获取完整的接口路径
-    const baseURL = config.baseURL || window.location.origin + "/apiv1"
+    const baseURL = config.baseURL || window.location.origin + BACKEND_API_PREFIX
     const url = config.url
     const fullPath = url.startsWith('http') ? url : baseURL.replace(/\/$/, '') + (url.startsWith('/') ? url : '/' + url)
     const method = (config.method || 'GET').toUpperCase()
@@ -117,6 +123,11 @@ http.interceptors.response.use((res) => {
 }, error => {
     // ===== 处理 401(Token 过期或无效) =====
     if (error.response && error.response.status === 401) {
+        // 开发模式下不跳转
+        if (import.meta.env.DEV) {
+            console.log('🔧 开发模式:忽略401错误')
+            return Promise.reject(error)
+        }
         if (isAuthenticating) {
             console.log('⏳ 正在认证中,延迟处理 401 错误')
             return Promise.reject(error)

+ 2 - 11
shudao-vue-frontend/src/services/audioTranscription.js

@@ -1,18 +1,9 @@
 import axios from 'axios'
 import { getToken, getTokenType } from '@/utils/auth'
-
-const DEFAULT_TEST_BASE = 'http://172.16.35.50:8000'
-const DEFAULT_PROD_BASE = 'https://aqai.shudaodsj.com:22000'
-
-function resolveBaseURL() {
-  if (import.meta.env?.VITE_AUDIO_API_BASE) {
-    return import.meta.env.VITE_AUDIO_API_BASE
-  }
-  return import.meta.env.PROD ? DEFAULT_PROD_BASE : DEFAULT_TEST_BASE
-}
+import { getAudioTranscriptionBase } from '@/utils/apiConfig'
 
 const audioClient = axios.create({
-  baseURL: resolveBaseURL(),
+  baseURL: import.meta.env?.VITE_AUDIO_API_BASE || getAudioTranscriptionBase(),
   timeout: 120000
 })
 

+ 104 - 20
shudao-vue-frontend/src/utils/apiConfig.js

@@ -1,41 +1,125 @@
 /**
  * API 配置工具
- * 根据环境自动处理 API 路径前缀
+ * 统一管理所有需要环境隔离的 API 地址配置
+ * 
+ * 环境说明:
+ * - 开发/测试环境 (npm run dev): 通过 vite 代理转发到本地服务
+ * - 生产环境 (npm run build): 通过 nginx 代理访问
+ * 
+ * 生产环境 nginx 代理路径:
+ * - /apiv1 → 系统后端 (shudao-go-backend)
+ * - /chatwithai/ → AI对话服务后端 (ReportGenerator等)
+ * - /auth → 认证网关服务
+ * - /tts → TTS语音合成服务
  */
 
+const isDev = import.meta.env.DEV
+
+// ==================== 服务地址配置 ====================
+
+/**
+ * 系统后端服务 (shudao-go-backend)
+ * 开发环境: 127.0.0.1:22001 (通过 vite 代理)
+ * 生产环境: /apiv1 (通过 nginx 代理)
+ */
+export const BACKEND_API_PREFIX = '/apiv1'
+
+/**
+ * AI对话服务 - 报告生成 (ReportGenerator)
+ * 开发环境: 127.0.0.1:28002 (通过 vite 代理)
+ * 生产环境: /chatwithai/api/v1 (通过 nginx 代理)
+ */
+export const REPORT_API_PREFIX = isDev 
+  ? '/api/v1' 
+  : '/chatwithai/api/v1'
+
+/**
+ * AI对话服务 - SSE 流式
+ * 开发环境: 127.0.0.1:28002 (通过 vite 代理)
+ * 生产环境: /chatwithai/api/v1 (通过 nginx 代理)
+ */
+export const SSE_API_PREFIX = isDev 
+  ? '/api/v1' 
+  : '/chatwithai/api/v1'
+
+/**
+ * 认证网关服务 (4A统一API网关)
+ * 开发环境: 127.0.0.1:28004 (通过 vite 代理)
+ * 生产环境: /auth/api (通过 nginx 代理)
+ */
+export const AUTH_GATEWAY_URL = isDev 
+  ? '/api' 
+  : '/auth/api'
+
+/**
+ * TTS 语音合成服务
+ * 开发环境: 172.16.29.101:22000 (通过 vite 代理)
+ * 生产环境: /tts (通过 nginx 代理)
+ */
+export const TTS_API_PREFIX = isDev 
+  ? '/api/tts' 
+  : '/tts'
+
+/**
+ * 音频转录服务 (语音转文字)
+ * 开发/测试环境: 172.16.35.50:8000 (直连)
+ * 生产环境: https://aqai.shudaodsj.com:22000 (直连)
+ */
+export const AUDIO_TRANSCRIPTION_BASE = isDev 
+  ? 'http://172.16.35.50:8000' 
+  : 'https://aqai.shudaodsj.com:22000'
+
+// ==================== 便捷函数 ====================
+
 /**
- * 获取 API 路径前缀
- * 开发环境:/api/v1
- * 生产环境:/chatwithai/api/v1
+ * 获取业务 API 路径前缀
  */
 export function getApiPrefix() {
-  // 在生产环境下,生产环境的 Nginx 会将 /chatwithai/ 开头的请求代理到后端
-  // 在开发环境下,Vite 的 proxy 会将 /api/v1 开头的请求代理到本地后端
-  return import.meta.env.PROD ? '/chatwithai/api/v1' : '/api/v1'
+  return BACKEND_API_PREFIX
 }
 
 /**
- * 构建完整的 API URL
- * @param {string} path - API 路径(例如:'/report/export')
- * @returns {string} 完整的 API URL
+ * 获取报告 API 前缀
  */
-export function buildApiUrl(path) {
-  const prefix = getApiPrefix()
-  // 确保路径以 / 开头
-  const normalizedPath = path.startsWith('/') ? path : `/${path}`
-  return `${prefix}${normalizedPath}`
+export function getReportApiPrefix() {
+  return REPORT_API_PREFIX
 }
 
 /**
  * 获取 SSE API 前缀
  */
 export function getSSEApiPrefix() {
-  return getApiPrefix()
+  return SSE_API_PREFIX
 }
 
 /**
- * 获取报告 API 前缀
+ * 获取认证网关 API 前缀
  */
-export function getReportApiPrefix() {
-  return getApiPrefix()
-}
+export function getAuthGatewayUrl() {
+  return AUTH_GATEWAY_URL
+}
+
+/**
+ * 获取 TTS API 前缀
+ */
+export function getTTSApiPrefix() {
+  return TTS_API_PREFIX
+}
+
+/**
+ * 获取音频转录服务地址
+ */
+export function getAudioTranscriptionBase() {
+  return AUDIO_TRANSCRIPTION_BASE
+}
+
+/**
+ * 构建完整的 API URL
+ * @param {string} path - API 路径(例如:'/report/export')
+ * @param {string} prefix - API 前缀,默认使用业务后端
+ * @returns {string} 完整的 API URL
+ */
+export function buildApiUrl(path, prefix = BACKEND_API_PREFIX) {
+  const normalizedPath = path.startsWith('/') ? path : `/${path}`
+  return `${prefix}${normalizedPath}`
+}

+ 50 - 5
shudao-vue-frontend/src/utils/ticketAuth.js

@@ -3,8 +3,13 @@
  * 处理从门户传递过来的票据,获取访问令牌
  */
 
-// 票据处理接口(直接使用完整URL)测试环境
-const TICKET_PROCESS_API = 'https://aqai.shudaodsj.com:22000/api/ticket/process'
+import { getAuthGatewayUrl } from './apiConfig'
+
+// 从统一配置获取认证服务地址
+const isDev = import.meta.env.DEV
+const AUTH_GATEWAY_URL = getAuthGatewayUrl()
+const TICKET_GET_API = `${AUTH_GATEWAY_URL}/ticket/get`
+const TICKET_PROCESS_API = `${AUTH_GATEWAY_URL}/ticket/process`
 
 // ===== 关键修复:在模块加载时立即保存原始 URL =====
 // 防止其他请求(如 axios 拦截器)在认证完成前跳转导致票据丢失
@@ -260,6 +265,34 @@ export function clearTicketFromUrl() {
   }
 }
 
+/**
+ * 获取默认用户票据(仅开发/测试环境使用)
+ */
+async function getDefaultTicket() {
+  try {
+    console.log('🔧 开发模式:获取默认用户票据...')
+    const response = await fetch(TICKET_GET_API, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({})
+    })
+    
+    if (!response.ok) {
+      throw new Error(`获取票据失败: ${response.status}`)
+    }
+    
+    const data = await response.json()
+    if (data.retCode === '1000' && data.ssoTicket) {
+      console.log('✅ 获取默认票据成功')
+      return data.ssoTicket
+    }
+    throw new Error('票据响应格式错误')
+  } catch (error) {
+    console.error('❌ 获取默认票据失败:', error)
+    throw error
+  }
+}
+
 /**
  * 处理票据,获取访问令牌
  */
@@ -512,10 +545,10 @@ export async function handleTicketAuth() {
   try {
     // 1. 获取票据
     addDebugLog('info', '步骤1: 获取票据')
-    const ticket = getTicketFromUrl()
+    let ticket = getTicketFromUrl()
     
     if (!ticket) {
-      addDebugLog('warning', '⚠️ 未找到票据')
+      addDebugLog('warning', '⚠️ 未找到URL票据')
       
       // 检查本地是否已有令牌
       if (hasLocalToken()) {
@@ -523,8 +556,20 @@ export async function handleTicketAuth() {
         addDebugLog('success', '✅ 本地已有令牌,使用本地令牌')
         const tokenData = getLocalToken()
         return { success: true, token: tokenData, fromCache: true }
+      }
+      
+      // 开发/测试模式:自动获取默认票据
+      if (isDev) {
+        addDebugLog('info', '🔧 开发模式:自动获取默认用户票据')
+        try {
+          ticket = await getDefaultTicket()
+          addDebugLog('success', '✅ 获取默认票据成功')
+        } catch (e) {
+          addDebugLog('error', `❌ 获取默认票据失败: ${e.message}`)
+          throw new Error('DEV_TICKET_FAILED')
+        }
       } else {
-        // 未找到票据且本地无令牌,抛出错误
+        // 生产环境:未找到票据且本地无令牌,抛出错误
         addDebugLog('error', '❌ 未找到票据且本地无令牌')
         throw new Error('TICKET_NOT_FOUND')
       }

+ 2 - 2
shudao-vue-frontend/src/views/Chat.vue

@@ -583,7 +583,7 @@ import WebSearchSidebar from '@/components/WebSearchSidebar.vue'
 import WebSearchSummary from '@/components/WebSearchSummary.vue'
 import StatusAvatar from '@/components/StatusAvatar.vue'
 import { createSSEConnection, closeSSEConnection } from '@/utils/sse'
-import { getApiPrefix } from '@/utils/apiConfig'
+import { getApiPrefix, getReportApiPrefix } from '@/utils/apiConfig'
 import { Document } from '@element-plus/icons-vue'
 
 // 导入发送按钮图标
@@ -2549,7 +2549,7 @@ const handleReportGeneratorSubmit = async (data) => {
   })
   
   try {
-    const apiPrefix = getApiPrefix()
+    const apiPrefix = getReportApiPrefix()
     const url = `${apiPrefix}/report/complete-flow`
 
     // 构建 POST 请求体

+ 2 - 1
shudao-vue-frontend/src/views/Login.vue

@@ -129,6 +129,7 @@ import { setToken, setUserInfo } from '@/utils/auth'
 import { RouterConfig } from '@/config/authConfig'
 import authRequest from '@/utils/authRequest'
 import { getFingerprint } from '@/utils/fingerprint'
+import { BACKEND_API_PREFIX } from '@/utils/apiConfig'
 
 const router = useRouter()
 
@@ -255,7 +256,7 @@ const handleLogin = async () => {
       
       // 调用本地登录API (使用axios直接请求,不经过authRequest)
       const axios = (await import('axios')).default
-      const res = await axios.post('/apiv1/auth/local_login', {
+      const res = await axios.post(`${BACKEND_API_PREFIX}/auth/local_login`, {
         username: formData.value.account,
         password: formData.value.password
       })

+ 7 - 1
shudao-vue-frontend/src/views/NotFound.vue

@@ -16,9 +16,15 @@
 export default {
   name: 'NotFound',
   mounted() {
-    // 记录日志
     const reason = this.$route.query.reason
     console.log('🚫 进入404页面,原因:', reason || '未知')
+
+    // 开发模式下不跳转
+    if (import.meta.env.DEV) {
+      console.log('🔧 开发模式:不跳转到登录门户')
+      return
+    }
+
     console.log('🔄 即将重定向到登录门户...')
 
     // 清除本地存储的认证信息

+ 2 - 1
shudao-vue-frontend/src/views/PolicyDocument.vue

@@ -203,6 +203,7 @@
 import { ref, onMounted, onUnmounted } from "vue";
 import { useRouter } from "vue-router";
 import { apis } from "@/request/apis.js";
+import { BACKEND_API_PREFIX } from "@/utils/apiConfig";
 
 const router = useRouter();
 
@@ -396,7 +397,7 @@ const downloadPolicy = async (file) => {
 
 // 最简单的下载方式
 const downloadFileViaBackend = (fileUrl, fileName) => {
-    const downloadUrl = `/apiv1/download_file?pdf_oss_download_link=${encodeURIComponent(
+    const downloadUrl = `${BACKEND_API_PREFIX}/download_file?pdf_oss_download_link=${encodeURIComponent(
         fileUrl
     )}&file_name=${encodeURIComponent(fileName)}`;
 

+ 3 - 3
shudao-vue-frontend/src/views/mobile/m-Chat.vue

@@ -471,7 +471,7 @@ import { apis } from '@/request/apis.js'
 // import { getUserId } from '@/utils/userManager.js'
 import { useSpeechRecognition } from '@/composables/useSpeechRecognition'
 import { createSSEConnection, closeSSEConnection } from '@/utils/sse'
-import { getApiPrefix } from '@/utils/apiConfig'
+import { getApiPrefix, getReportApiPrefix, BACKEND_API_PREFIX } from '@/utils/apiConfig'
 import { renderMarkdown } from '@/utils/markdown'
 import { stopSSEStream, updateAIMessageContent } from '@/utils/api.js'
 import { getToken, getTokenType } from '@/utils/auth.js'
@@ -3240,7 +3240,7 @@ const handleReportGeneratorSubmit = async (data) => {
   })
 
   try {
-    const apiPrefix = getApiPrefix()
+    const apiPrefix = getReportApiPrefix()
     const url = `${apiPrefix}/report/complete-flow`
 
     // 构建 POST 请求体
@@ -3570,7 +3570,7 @@ const callStreamChatWithDB = async (messageToAI, aiMessage, userMessage, current
     aiMessage.isTyping = true
     aiMessage.isStreaming = true // 确保标记为流式
 
-    const response = await fetch('/apiv1/stream/chat-with-db', {
+    const response = await fetch(BACKEND_API_PREFIX + '/stream/chat-with-db', {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',

+ 3 - 5
shudao-vue-frontend/vite.config.js

@@ -52,17 +52,15 @@ export default defineConfig({
         target: 'http://127.0.0.1:28002',
         changeOrigin: true
       },
-      // ===== 新增:认证 API 代理(/api/auth/*, /api/user/*, /api/logs/*, /api/captcha/*) =====
+      // ===== 认证网关代理(本地/测试环境使用本地网关) =====
       '^/api/(auth|user|logs|captcha|ticket)': {
-        target: 'https://aqai.shudaodsj.com:22000/',  // 后端认证服务地址
+        target: 'http://127.0.0.1:28004',  // 本地认证网关
         changeOrigin: true,
-        // 不重写路径,直接转发
       },
       // 业务 API 代理
       '/apiv1': {
-        target: 'http://127.0.0.1:22000',
+        target: 'http://127.0.0.1:22001',
         changeOrigin: true,
-        rewrite: (path) => path.replace(/^\/apiv1/, '/apiv1'),
       }
     }
   }