shudaooss.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  1. package controllers
  2. import (
  3. "bytes"
  4. "crypto/hmac"
  5. "crypto/sha256"
  6. "encoding/base64"
  7. "encoding/hex"
  8. "encoding/json"
  9. "fmt"
  10. "image"
  11. "image/jpeg"
  12. "io"
  13. "math"
  14. "net/http"
  15. neturl "net/url"
  16. "path/filepath"
  17. "shudao-chat-go/utils"
  18. "strings"
  19. "time"
  20. "github.com/aws/aws-sdk-go/aws"
  21. "github.com/aws/aws-sdk-go/aws/credentials"
  22. "github.com/aws/aws-sdk-go/aws/session"
  23. "github.com/aws/aws-sdk-go/service/s3"
  24. "github.com/beego/beego/v2/server/web"
  25. )
  26. type ShudaoOssController struct {
  27. web.Controller
  28. }
  29. // OSS配置信息 - 延迟初始化
  30. var (
  31. ossBucket string
  32. ossAccessKey string
  33. ossSecretKey string
  34. ossEndpoint string
  35. ossRegion = "us-east-1"
  36. ossInited = false
  37. )
  38. // initOSSConfig 延迟初始化OSS配置
  39. func initOSSConfig() {
  40. if ossInited {
  41. return
  42. }
  43. ossConfig := utils.GetOSSConfig()
  44. ossBucket = ossConfig["bucket"]
  45. ossAccessKey = ossConfig["access_key"]
  46. ossSecretKey = ossConfig["secret_key"]
  47. ossEndpoint = ossConfig["endpoint"]
  48. ossInited = true
  49. }
  50. // 图片压缩配置
  51. const (
  52. // 目标文件大小(字节)
  53. TargetFileSize = 200 * 1024 // 200KB
  54. // 最大图片尺寸(像素)- 作为备选方案
  55. MaxImageWidth = 1920
  56. MaxImageHeight = 1080
  57. // JPEG压缩质量范围
  58. MinJPEGQuality = 10 // 最低质量
  59. MaxJPEGQuality = 95 // 最高质量
  60. // 是否启用图片压缩
  61. EnableImageCompression = true
  62. )
  63. // 上传响应结构
  64. type UploadResponse struct {
  65. StatusCode int `json:"statusCode"`
  66. Message string `json:"message"`
  67. FileURL string `json:"fileUrl"`
  68. FileName string `json:"fileName"`
  69. FileSize int64 `json:"fileSize"`
  70. }
  71. // getS3Session 获取S3会话
  72. func getS3Session() (*session.Session, error) {
  73. initOSSConfig()
  74. s3Config := &aws.Config{
  75. Credentials: credentials.NewStaticCredentials(ossAccessKey, ossSecretKey, ""),
  76. Endpoint: aws.String(ossEndpoint),
  77. Region: aws.String(ossRegion),
  78. S3ForcePathStyle: aws.Bool(true),
  79. DisableSSL: aws.Bool(true),
  80. MaxRetries: aws.Int(3),
  81. }
  82. sess, err := session.NewSession(s3Config)
  83. if err != nil {
  84. return nil, err
  85. }
  86. if _, err = sess.Config.Credentials.Get(); err != nil {
  87. return nil, fmt.Errorf("凭据验证失败: %v", err)
  88. }
  89. return sess, nil
  90. }
  91. // getUTCS3Session 获取UTC时间同步的S3会话
  92. func getUTCS3Session() (*session.Session, error) {
  93. initOSSConfig()
  94. s3Config := &aws.Config{
  95. Credentials: credentials.NewStaticCredentials(ossAccessKey, ossSecretKey, ""),
  96. Endpoint: aws.String(ossEndpoint),
  97. Region: aws.String(ossRegion),
  98. S3ForcePathStyle: aws.Bool(true), // 强制使用路径样式(OSS兼容性需要)
  99. // 移除DisableSSL,因为endpoint已经包含http://
  100. // 移除LogLevel,减少调试输出
  101. // 移除MaxRetries,使用默认值
  102. // 移除S3DisableContentMD5Validation,使用默认值
  103. }
  104. // 创建会话
  105. sess, err := session.NewSession(s3Config)
  106. if err != nil {
  107. return nil, err
  108. }
  109. // 验证凭据
  110. _, err = sess.Config.Credentials.Get()
  111. if err != nil {
  112. return nil, fmt.Errorf("凭据验证失败: %v", err)
  113. }
  114. return sess, nil
  115. }
  116. // 判断是否为图片文件
  117. func isImageFile(ext string) bool {
  118. imageExts := map[string]bool{
  119. ".jpg": true,
  120. ".jpeg": true,
  121. ".png": true,
  122. ".gif": true,
  123. ".bmp": true,
  124. ".webp": true,
  125. ".tiff": true,
  126. ".svg": true,
  127. ".ico": true,
  128. }
  129. return imageExts[ext]
  130. }
  131. // 图片上传接口
  132. func (c *ShudaoOssController) UploadImage() {
  133. c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Origin", "*")
  134. c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
  135. c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Headers", "Content-Type")
  136. if c.Ctx.Request.Method == "OPTIONS" {
  137. c.Ctx.ResponseWriter.WriteHeader(200)
  138. return
  139. }
  140. // 获取上传的图片文件
  141. file, header, err := c.GetFile("image")
  142. if err != nil {
  143. c.Data["json"] = UploadResponse{
  144. StatusCode: 400,
  145. Message: "获取上传图片失败: " + err.Error(),
  146. }
  147. c.ServeJSON()
  148. return
  149. }
  150. defer file.Close()
  151. // 检查文件扩展名
  152. ext := strings.ToLower(filepath.Ext(header.Filename))
  153. if !isImageFile(ext) {
  154. c.Data["json"] = UploadResponse{
  155. StatusCode: 400,
  156. Message: "不支持的文件格式,请上传图片文件(jpg, png, gif, bmp, webp等)",
  157. }
  158. c.ServeJSON()
  159. return
  160. }
  161. // 图片文件大小限制(10MB)
  162. if header.Size > 10*1024*1024 {
  163. c.Data["json"] = UploadResponse{
  164. StatusCode: 400,
  165. Message: "图片文件大小超过限制(10MB)",
  166. }
  167. c.ServeJSON()
  168. return
  169. }
  170. // 生成图片文件名(使用UTC时间)
  171. utcNow := time.Now().UTC()
  172. timestamp := utcNow.Unix()
  173. // 压缩后的图片统一使用.jpg扩展名
  174. fileName := fmt.Sprintf("images/%d/%s_%d.jpg",
  175. utcNow.Year(),
  176. utcNow.Format("0102"),
  177. timestamp)
  178. // 读取图片内容
  179. fileBytes, err := io.ReadAll(file)
  180. if err != nil {
  181. c.Data["json"] = UploadResponse{
  182. StatusCode: 500,
  183. Message: "读取图片内容失败: " + err.Error(),
  184. }
  185. c.ServeJSON()
  186. return
  187. }
  188. // 压缩图片
  189. if EnableImageCompression {
  190. compressedBytes, err := compressImage(fileBytes, MaxImageWidth, MaxImageHeight, 0)
  191. if err == nil {
  192. fileBytes = compressedBytes
  193. }
  194. }
  195. // 获取UTC时间同步的S3会话(解决时区问题)
  196. sess, err := getUTCS3Session()
  197. if err != nil {
  198. c.Data["json"] = UploadResponse{
  199. StatusCode: 500,
  200. Message: "创建S3会话失败: " + err.Error(),
  201. }
  202. c.ServeJSON()
  203. return
  204. }
  205. // 创建S3服务
  206. s3Client := s3.New(sess)
  207. // 上传图片到S3
  208. _, err = s3Client.PutObject(&s3.PutObjectInput{
  209. Bucket: aws.String(ossBucket),
  210. Key: aws.String(fileName),
  211. Body: aws.ReadSeekCloser(strings.NewReader(string(fileBytes))), // 使用与测试文件相同的方式
  212. ACL: aws.String("public-read"),
  213. })
  214. if err != nil {
  215. c.Data["json"] = UploadResponse{
  216. StatusCode: 500,
  217. Message: "上传图片到OSS失败: " + err.Error(),
  218. }
  219. c.ServeJSON()
  220. return
  221. }
  222. // // 生成预签名URL(1小时有效期)
  223. // req, _ := s3Client.GetObjectRequest(&s3.GetObjectInput{
  224. // Bucket: aws.String(ossBucket),
  225. // Key: aws.String(fileName),
  226. // })
  227. // presignedURL, err := req.Presign(24 * time.Hour)
  228. // if err != nil {
  229. // fmt.Printf("生成预签名URL失败: %v\n", err)
  230. // // 如果预签名URL生成失败,使用简单URL作为备选
  231. // imageURL := fmt.Sprintf("%s/%s", ossEndpoint, fileName)
  232. // c.Data["json"] = UploadResponse{
  233. // StatusCode: 200,
  234. // Message: "图片上传成功,但预签名URL生成失败",
  235. // FileURL: imageURL,
  236. // FileName: fileName,
  237. // FileSize: header.Size,
  238. // }
  239. // c.ServeJSON()
  240. // return
  241. // }
  242. permanentURL := fmt.Sprintf("%s/%s/%s", ossEndpoint, ossBucket, fileName)
  243. proxyURL := utils.GetProxyURL(permanentURL)
  244. c.Data["json"] = UploadResponse{
  245. StatusCode: 200,
  246. Message: "图片上传成功",
  247. FileURL: proxyURL,
  248. FileName: fileName,
  249. FileSize: int64(len(fileBytes)), // 使用压缩后的文件大小
  250. }
  251. c.ServeJSON()
  252. }
  253. // 上传PPTjson文件
  254. func (c *ShudaoOssController) UploadPPTJson() {
  255. c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Origin", "*")
  256. c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
  257. c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Headers", "Content-Type")
  258. if c.Ctx.Request.Method == "OPTIONS" {
  259. c.Ctx.ResponseWriter.WriteHeader(200)
  260. return
  261. }
  262. // 获取上传的JSON文件
  263. file, header, err := c.GetFile("json")
  264. if err != nil {
  265. c.Data["json"] = UploadResponse{
  266. StatusCode: 400,
  267. Message: "获取上传JSON文件失败: " + err.Error(),
  268. }
  269. c.ServeJSON()
  270. return
  271. }
  272. defer file.Close()
  273. // 检查文件扩展名
  274. ext := strings.ToLower(filepath.Ext(header.Filename))
  275. if ext != ".json" {
  276. c.Data["json"] = UploadResponse{
  277. StatusCode: 400,
  278. Message: "不支持的文件格式,请上传JSON文件(.json)",
  279. }
  280. c.ServeJSON()
  281. return
  282. }
  283. // JSON文件大小限制(50MB)
  284. if header.Size > 50*1024*1024 {
  285. c.Data["json"] = UploadResponse{
  286. StatusCode: 400,
  287. Message: "JSON文件大小超过限制(50MB)",
  288. }
  289. c.ServeJSON()
  290. return
  291. }
  292. // 生成JSON文件名(使用UTC时间)
  293. utcNow := time.Now().UTC()
  294. timestamp := utcNow.Unix()
  295. fileName := fmt.Sprintf("json/%d/%s_%d%s",
  296. utcNow.Year(),
  297. utcNow.Format("0102"),
  298. timestamp,
  299. ext)
  300. // 读取JSON内容
  301. fileBytes, err := io.ReadAll(file)
  302. if err != nil {
  303. c.Data["json"] = UploadResponse{
  304. StatusCode: 500,
  305. Message: "读取JSON内容失败: " + err.Error(),
  306. }
  307. c.ServeJSON()
  308. return
  309. }
  310. // 验证JSON格式
  311. var jsonData interface{}
  312. if err := json.Unmarshal(fileBytes, &jsonData); err != nil {
  313. c.Data["json"] = UploadResponse{
  314. StatusCode: 400,
  315. Message: "JSON格式无效: " + err.Error(),
  316. }
  317. c.ServeJSON()
  318. return
  319. }
  320. // 获取UTC时间同步的S3会话
  321. sess, err := getUTCS3Session()
  322. if err != nil {
  323. c.Data["json"] = UploadResponse{
  324. StatusCode: 500,
  325. Message: "创建S3会话失败: " + err.Error(),
  326. }
  327. c.ServeJSON()
  328. return
  329. }
  330. // 创建S3服务
  331. s3Client := s3.New(sess)
  332. // 上传JSON到S3
  333. _, err = s3Client.PutObject(&s3.PutObjectInput{
  334. Bucket: aws.String(ossBucket),
  335. Key: aws.String(fileName),
  336. Body: aws.ReadSeekCloser(strings.NewReader(string(fileBytes))),
  337. ACL: aws.String("public-read"),
  338. ContentType: aws.String("application/json"),
  339. })
  340. if err != nil {
  341. c.Data["json"] = UploadResponse{
  342. StatusCode: 500,
  343. Message: "上传JSON文件到OSS失败: " + err.Error(),
  344. }
  345. c.ServeJSON()
  346. return
  347. }
  348. // 生成永久URL
  349. permanentURL := fmt.Sprintf("%s/%s/%s", ossEndpoint, ossBucket, fileName)
  350. proxyURL := utils.GetProxyURL(permanentURL)
  351. c.Data["json"] = UploadResponse{
  352. StatusCode: 200,
  353. Message: "JSON文件上传成功",
  354. FileURL: proxyURL,
  355. FileName: fileName,
  356. FileSize: header.Size,
  357. }
  358. c.ServeJSON()
  359. }
  360. // ParseOSS OSS代理解析接口,用于代理转发OSS URL请求
  361. func (c *ShudaoOssController) ParseOSS() {
  362. // 设置CORS头
  363. c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Origin", "*")
  364. c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
  365. c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Headers", "Content-Type")
  366. // 处理OPTIONS预检请求
  367. if c.Ctx.Request.Method == "OPTIONS" {
  368. c.Ctx.ResponseWriter.WriteHeader(200)
  369. return
  370. }
  371. // 获取URL参数(加密的)
  372. encryptedURL := c.GetString("url")
  373. if encryptedURL == "" {
  374. c.Ctx.ResponseWriter.WriteHeader(400)
  375. c.Ctx.WriteString("缺少url参数")
  376. return
  377. }
  378. // 解密URL
  379. decryptedURL, err := utils.DecryptURL(encryptedURL)
  380. if err != nil {
  381. c.Ctx.ResponseWriter.WriteHeader(400)
  382. c.Ctx.WriteString("URL解密失败: " + err.Error())
  383. return
  384. }
  385. // URL解码,处理可能的编码问题
  386. decodedURL, err := neturl.QueryUnescape(decryptedURL)
  387. if err != nil {
  388. decodedURL = decryptedURL
  389. }
  390. var actualOSSURL string
  391. // 检查是否是代理URL格式(包含?url=参数)
  392. if strings.Contains(decodedURL, "?url=") {
  393. parsedProxyURL, err := neturl.Parse(decodedURL)
  394. if err != nil {
  395. c.Ctx.ResponseWriter.WriteHeader(400)
  396. c.Ctx.WriteString("代理URL格式无效: " + err.Error())
  397. return
  398. }
  399. actualOSSURL = parsedProxyURL.Query().Get("url")
  400. if actualOSSURL == "" {
  401. c.Ctx.ResponseWriter.WriteHeader(400)
  402. c.Ctx.WriteString("代理URL中缺少url参数")
  403. return
  404. }
  405. } else {
  406. actualOSSURL = decodedURL
  407. }
  408. // 验证实际OSS URL格式
  409. parsedOSSURL, err := neturl.Parse(actualOSSURL)
  410. if err != nil {
  411. c.Ctx.ResponseWriter.WriteHeader(400)
  412. c.Ctx.WriteString("OSS URL格式无效: " + err.Error())
  413. return
  414. }
  415. if parsedOSSURL.Scheme == "" {
  416. c.Ctx.ResponseWriter.WriteHeader(400)
  417. c.Ctx.WriteString("OSS URL缺少协议方案")
  418. return
  419. }
  420. // 创建HTTP客户端,设置超时时间
  421. client := &http.Client{
  422. Timeout: 30 * time.Second,
  423. }
  424. // 发送GET请求到实际的OSS URL
  425. resp, err := client.Get(actualOSSURL)
  426. if err != nil {
  427. c.Ctx.ResponseWriter.WriteHeader(502)
  428. c.Ctx.WriteString("无法连接到OSS: " + err.Error())
  429. return
  430. }
  431. defer resp.Body.Close()
  432. // 检查HTTP状态码
  433. if resp.StatusCode != http.StatusOK {
  434. c.Ctx.ResponseWriter.WriteHeader(resp.StatusCode)
  435. c.Ctx.WriteString(fmt.Sprintf("OSS返回错误: %d", resp.StatusCode))
  436. return
  437. }
  438. // 读取响应内容
  439. content, err := io.ReadAll(resp.Body)
  440. if err != nil {
  441. c.Ctx.ResponseWriter.WriteHeader(500)
  442. c.Ctx.WriteString("读取OSS响应失败: " + err.Error())
  443. return
  444. }
  445. // 获取原始的content-type
  446. contentType := resp.Header.Get("content-type")
  447. if contentType == "" {
  448. contentType = "application/octet-stream"
  449. }
  450. // 如果OSS返回的是binary/octet-stream或application/octet-stream,
  451. // 尝试根据URL文件扩展名推断正确的MIME类型
  452. if contentType == "binary/octet-stream" || contentType == "application/octet-stream" {
  453. // 解析URL获取文件路径
  454. parsedURL, err := neturl.Parse(actualOSSURL)
  455. if err == nil {
  456. filePath := parsedURL.Path
  457. // URL解码,处理中文文件名
  458. filePath, err = neturl.QueryUnescape(filePath)
  459. if err == nil {
  460. // 根据文件扩展名猜测MIME类型
  461. if strings.HasSuffix(strings.ToLower(filePath), ".jpg") || strings.HasSuffix(strings.ToLower(filePath), ".jpeg") {
  462. contentType = "image/jpeg"
  463. } else if strings.HasSuffix(strings.ToLower(filePath), ".png") {
  464. contentType = "image/png"
  465. } else if strings.HasSuffix(strings.ToLower(filePath), ".gif") {
  466. contentType = "image/gif"
  467. } else if strings.HasSuffix(strings.ToLower(filePath), ".pdf") {
  468. contentType = "application/pdf"
  469. } else if strings.HasSuffix(strings.ToLower(filePath), ".json") {
  470. contentType = "application/json"
  471. } else if strings.HasSuffix(strings.ToLower(filePath), ".txt") {
  472. contentType = "text/plain"
  473. }
  474. }
  475. }
  476. }
  477. // 设置响应头
  478. c.Ctx.ResponseWriter.Header().Set("Content-Type", contentType)
  479. c.Ctx.ResponseWriter.Header().Set("Content-Length", fmt.Sprintf("%d", len(content)))
  480. // 转发重要的响应头
  481. importantHeaders := []string{
  482. "content-disposition",
  483. "cache-control",
  484. "etag",
  485. "last-modified",
  486. "accept-ranges",
  487. }
  488. for _, header := range importantHeaders {
  489. if value := resp.Header.Get(header); value != "" {
  490. c.Ctx.ResponseWriter.Header().Set(header, value)
  491. }
  492. }
  493. // 写入响应内容
  494. c.Ctx.ResponseWriter.WriteHeader(200)
  495. c.Ctx.ResponseWriter.Write(content)
  496. }
  497. // compressImage 压缩图片到目标大小
  498. func compressImage(imageData []byte, maxWidth, maxHeight int, quality int) ([]byte, error) {
  499. img, _, err := image.Decode(bytes.NewReader(imageData))
  500. if err != nil {
  501. return nil, fmt.Errorf("解码图片失败: %v", err)
  502. }
  503. originalSize := len(imageData)
  504. if originalSize <= TargetFileSize {
  505. return imageData, nil
  506. }
  507. return compressToTargetSize(img, originalSize)
  508. }
  509. // compressToTargetSize 压缩到目标文件大小
  510. func compressToTargetSize(img image.Image, originalSize int) ([]byte, error) {
  511. bounds := img.Bounds()
  512. originalWidth := bounds.Dx()
  513. originalHeight := bounds.Dy()
  514. // 策略1: 先尝试调整质量,不改变尺寸
  515. compressedData, err := compressByQuality(img)
  516. if err == nil && len(compressedData) <= TargetFileSize {
  517. return compressedData, nil
  518. }
  519. // 策略2: 如果质量压缩不够,尝试缩小尺寸
  520. targetRatio := float64(TargetFileSize) / float64(originalSize)
  521. sizeRatio := math.Sqrt(targetRatio * 0.8)
  522. newWidth := int(float64(originalWidth) * sizeRatio)
  523. newHeight := int(float64(originalHeight) * sizeRatio)
  524. if newWidth < 100 {
  525. newWidth = 100
  526. }
  527. if newHeight < 100 {
  528. newHeight = 100
  529. }
  530. resizedImg := resizeImage(img, newWidth, newHeight)
  531. return compressByQuality(resizedImg)
  532. }
  533. // compressByQuality 通过调整质量压缩图片
  534. func compressByQuality(img image.Image) ([]byte, error) {
  535. var bestResult []byte
  536. var bestSize int = math.MaxInt32
  537. qualities := []int{85, 70, 60, 50, 40, 30, 25, 20, 15, 10}
  538. for _, quality := range qualities {
  539. var buf bytes.Buffer
  540. if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}); err != nil {
  541. continue
  542. }
  543. currentSize := buf.Len()
  544. if currentSize <= TargetFileSize {
  545. return buf.Bytes(), nil
  546. }
  547. if currentSize < bestSize {
  548. bestSize = currentSize
  549. bestResult = buf.Bytes()
  550. }
  551. }
  552. if bestResult != nil {
  553. return bestResult, nil
  554. }
  555. return nil, fmt.Errorf("压缩失败")
  556. }
  557. // resizeImage 调整图片尺寸
  558. func resizeImage(img image.Image, newWidth, newHeight int) image.Image {
  559. // 创建新的图片
  560. resized := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
  561. // 简单的最近邻插值缩放
  562. bounds := img.Bounds()
  563. for y := 0; y < newHeight; y++ {
  564. for x := 0; x < newWidth; x++ {
  565. // 计算原始图片中的对应位置
  566. srcX := int(float64(x) * float64(bounds.Dx()) / float64(newWidth))
  567. srcY := int(float64(y) * float64(bounds.Dy()) / float64(newHeight))
  568. // 确保不超出边界
  569. if srcX >= bounds.Dx() {
  570. srcX = bounds.Dx() - 1
  571. }
  572. if srcY >= bounds.Dy() {
  573. srcY = bounds.Dy() - 1
  574. }
  575. resized.Set(x, y, img.At(bounds.Min.X+srcX, bounds.Min.Y+srcY))
  576. }
  577. }
  578. return resized
  579. }
  580. // S3策略文档结构
  581. type S3PolicyDocument struct {
  582. Expiration string `json:"expiration"`
  583. Conditions []interface{} `json:"conditions"`
  584. }
  585. // S3响应结构
  586. type S3PolicyToken struct {
  587. URL string `json:"url"`
  588. Fields map[string]string `json:"fields"`
  589. Expire int64 `json:"expire"`
  590. StatusCode int `json:"statusCode"`
  591. }
  592. // Upload 生成S3预签名上传凭证
  593. func (c *ShudaoOssController) Upload() {
  594. initOSSConfig()
  595. c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Origin", "*")
  596. c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
  597. c.Ctx.ResponseWriter.Header().Set("Access-Control-Allow-Headers", "Content-Type")
  598. if c.Ctx.Request.Method == "OPTIONS" {
  599. c.Ctx.ResponseWriter.WriteHeader(200)
  600. return
  601. }
  602. userInfo, err := utils.GetUserInfoFromContext(c.Ctx.Input.GetData("userInfo"))
  603. if err != nil {
  604. c.Data["json"] = map[string]interface{}{"statusCode": 401, "error": "获取用户信息失败"}
  605. c.ServeJSON()
  606. return
  607. }
  608. userID := int(userInfo.ID)
  609. if userID == 0 {
  610. userID = 1
  611. }
  612. now := time.Now().UTC()
  613. expireTime := int64(1800)
  614. expireEnd := now.Unix() + expireTime
  615. dateStamp := now.Format("20060102")
  616. amzDate := now.Format("20060102T150405Z")
  617. expiration := now.Add(time.Duration(expireTime) * time.Second).Format("2006-01-02T15:04:05.000Z")
  618. credential := fmt.Sprintf("%s/%s/%s/s3/aws4_request", ossAccessKey, dateStamp, ossRegion)
  619. uploadDir := fmt.Sprintf("uploads/%s/%d/", now.Format("0102"), userID)
  620. host := fmt.Sprintf("%s/%s", ossEndpoint, ossBucket)
  621. policy := S3PolicyDocument{
  622. Expiration: expiration,
  623. Conditions: []interface{}{
  624. map[string]string{"bucket": ossBucket},
  625. []interface{}{"starts-with", "$key", uploadDir},
  626. map[string]string{"x-amz-algorithm": "AWS4-HMAC-SHA256"},
  627. map[string]string{"x-amz-credential": credential},
  628. map[string]string{"x-amz-date": amzDate},
  629. []interface{}{"content-length-range", "0", "104857600"},
  630. },
  631. }
  632. policyJSON, _ := json.Marshal(policy)
  633. policyBase64 := base64.StdEncoding.EncodeToString(policyJSON)
  634. signature := generateAWS4Signature(ossSecretKey, dateStamp, ossRegion, policyBase64)
  635. c.Data["json"] = S3PolicyToken{
  636. StatusCode: 200,
  637. URL: host,
  638. Expire: expireEnd,
  639. Fields: map[string]string{
  640. "key": uploadDir + "${filename}",
  641. "policy": policyBase64,
  642. "x-amz-algorithm": "AWS4-HMAC-SHA256",
  643. "x-amz-credential": credential,
  644. "x-amz-date": amzDate,
  645. "x-amz-signature": signature,
  646. },
  647. }
  648. c.ServeJSON()
  649. }
  650. // generateAWS4Signature 生成AWS4签名
  651. func generateAWS4Signature(secretKey, dateStamp, region, stringToSign string) string {
  652. kDate := hmacSHA256([]byte("AWS4"+secretKey), dateStamp)
  653. kRegion := hmacSHA256(kDate, region)
  654. kService := hmacSHA256(kRegion, "s3")
  655. kSigning := hmacSHA256(kService, "aws4_request")
  656. return hex.EncodeToString(hmacSHA256(kSigning, stringToSign))
  657. }
  658. // hmacSHA256 HMAC-SHA256计算
  659. func hmacSHA256(key []byte, data string) []byte {
  660. mac := hmac.New(sha256.New, key)
  661. mac.Write([]byte(data))
  662. return mac.Sum(nil)
  663. }