directory.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. package handlers
  2. import (
  3. "Vali-Tools/utils"
  4. "bytes"
  5. "context"
  6. "encoding/base64"
  7. "encoding/json"
  8. "fmt"
  9. "github.com/wailsapp/wails/v2/pkg/runtime"
  10. "io"
  11. "mime/multipart"
  12. "net/http"
  13. "os"
  14. "path/filepath"
  15. "strings"
  16. )
  17. // ProcessResult 处理结果结构体
  18. type ProcessResult struct {
  19. Success bool `json:"success"`
  20. Message string `json:"message"`
  21. Progress int `json:"progress"`
  22. }
  23. // ProcessCallback 处理过程回调函数类型
  24. type ProcessCallback func(result ProcessResult)
  25. type DirectoryHandler struct {
  26. ctx context.Context
  27. }
  28. func NewDirectoryHandler(ctx context.Context) *DirectoryHandler {
  29. return &DirectoryHandler{ctx: ctx}
  30. }
  31. // HandlerDirectory 处理目录复制的主要方法
  32. func (dh *DirectoryHandler) HandlerDirectory(sourceDir, targetDir string) error {
  33. // 检查源目录是否存在
  34. if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
  35. return fmt.Errorf("源目录不存在: %s", sourceDir)
  36. }
  37. // 检查目标目录,如果不存在则创建
  38. if _, err := os.Stat(targetDir); os.IsNotExist(err) {
  39. // 源目录不存在时不需要创建目录,但目标目录可以创建
  40. err := os.MkdirAll(targetDir, os.ModePerm)
  41. if err != nil {
  42. return fmt.Errorf("创建目标目录失败: %v", err)
  43. }
  44. }
  45. // 执行具体的处理逻辑
  46. return dh.processDirectories(sourceDir, targetDir)
  47. }
  48. // processDirectories 实际处理目录的逻辑
  49. func (dh *DirectoryHandler) processDirectories(sourceDir, targetDir string) error {
  50. // 发送初始进度
  51. dh.sendProgress(ProcessResult{
  52. Success: true,
  53. Message: "开始处理目录...",
  54. Progress: 0,
  55. })
  56. // 遍历和处理逻辑
  57. err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
  58. if err != nil {
  59. dh.sendProgress(ProcessResult{
  60. Success: false,
  61. Message: fmt.Sprintf("访问路径出错: %v", err),
  62. Progress: 0,
  63. })
  64. return err
  65. }
  66. fmt.Printf("正在处理: %s\n", path)
  67. // 处理 800x800 目录的逻辑
  68. if info.IsDir() && strings.Contains(info.Name(), "800x800") {
  69. fmt.Printf("👍👍👍符合条件: %s\n", path)
  70. // 获取上级目录名称
  71. parentDirName := filepath.Base(filepath.Dir(path))
  72. // 在目标目录中创建以父目录命名的子目录
  73. newTargetDir := filepath.Join(targetDir, parentDirName)
  74. // 检查目标目录,如果不存在则创建
  75. if _, err := os.Stat(newTargetDir); os.IsNotExist(err) {
  76. // 源目录不存在时不需要创建目录,但目标目录可以创建
  77. err := os.MkdirAll(newTargetDir, os.ModePerm)
  78. if err != nil {
  79. return fmt.Errorf("创建目标目录失败: %v", err)
  80. }
  81. }
  82. dh.sendProgress(ProcessResult{
  83. Success: true,
  84. Message: fmt.Sprintf("找到目录: %s,将在目标目录创建子目录: %s", info.Name(), parentDirName),
  85. Progress: 50,
  86. })
  87. // 执行复制操作到新创建的子目录
  88. err := dh.copyFilesFromDir(path, newTargetDir)
  89. if err != nil {
  90. dh.sendProgress(ProcessResult{
  91. Success: false,
  92. Message: fmt.Sprintf("复制文件到:%v 失败: %v", newTargetDir, err),
  93. Progress: 50,
  94. })
  95. return err
  96. }
  97. }
  98. return nil
  99. })
  100. // 发送完成进度
  101. if err != nil {
  102. dh.sendProgress(ProcessResult{
  103. Success: false,
  104. Message: fmt.Sprintf("处理失败: %v", err),
  105. Progress: 100,
  106. })
  107. } else {
  108. dh.sendProgress(ProcessResult{
  109. Success: true,
  110. Message: "处理完成",
  111. Progress: 100,
  112. })
  113. }
  114. return err
  115. }
  116. // copyFilesFromDir 从指定目录复制所有文件到目标目录
  117. func (dh *DirectoryHandler) copyFilesFromDir(sourceDir, targetDir string) error {
  118. entries, err := os.ReadDir(sourceDir)
  119. if err != nil {
  120. return fmt.Errorf("读取目录失败 %s: %v", sourceDir, err)
  121. }
  122. for _, entry := range entries {
  123. // 只处理文件,跳过子目录
  124. if entry.IsDir() {
  125. continue // 忽略目录
  126. }
  127. // 检查是否为有效的图片文件
  128. if !dh.isValidImageFile(entry.Name()) {
  129. continue // 跳过非图片文件
  130. }
  131. sourcePath := filepath.Join(sourceDir, entry.Name())
  132. targetPath := filepath.Join(targetDir, entry.Name())
  133. err := dh.copyFile(sourcePath, targetPath)
  134. if err != nil {
  135. return fmt.Errorf("复制文件失败 %s 到 %s: %v", sourcePath, targetPath, err)
  136. }
  137. }
  138. return nil
  139. }
  140. // copyFile 复制单个文件
  141. func (dh *DirectoryHandler) copyFile(src, dst string) error {
  142. sourceFile, err := os.Open(src)
  143. if err != nil {
  144. return err
  145. }
  146. defer sourceFile.Close()
  147. // 创建目标文件
  148. destFile, err := os.Create(dst)
  149. if err != nil {
  150. return err
  151. }
  152. defer destFile.Close()
  153. // 复制文件内容
  154. _, err = io.Copy(destFile, sourceFile)
  155. if err != nil {
  156. return err
  157. }
  158. // 同步文件内容到磁盘
  159. return destFile.Sync()
  160. }
  161. // isValidImageFile 检查文件是否为有效的图片文件
  162. func (dh *DirectoryHandler) isValidImageFile(filename string) bool {
  163. validExtensions := []string{".avif", ".bmp", ".png", ".jpg", ".jpeg"}
  164. ext := strings.ToLower(filepath.Ext(filename))
  165. for _, validExt := range validExtensions {
  166. if ext == validExt {
  167. return true
  168. }
  169. }
  170. return false
  171. }
  172. type ImageResult struct {
  173. GoodsArtNo string `json:"goods_art_no"`
  174. ImagePath string `json:"image_path"`
  175. Category string `json:"category"`
  176. Description string `json:"description"`
  177. Price string `json:"price"`
  178. ImageBase64 string `json:"image_base64"`
  179. }
  180. // UploadSkuResult SKU图片上传结果
  181. type UploadSkuResult struct {
  182. FileName string `json:"FileName"`
  183. ImageType string `json:"ImageType"`
  184. Size int64 `json:"Size"`
  185. }
  186. // UploadSkuResponse SKU图片上传响应
  187. type UploadSkuResponse struct {
  188. Data []UploadSkuResult `json:"data"`
  189. Success bool `json:"success"`
  190. Code int `json:"code"`
  191. }
  192. // BatchFileItem 批量上传文件项
  193. type BatchFileItem struct {
  194. FilePath string
  195. ImageType string
  196. FileName string
  197. }
  198. // ActionNamesResponse 动作名称响应
  199. type ActionNamesResponse struct {
  200. Code int `json:"code"`
  201. Msg string `json:"msg"`
  202. Data []string `json:"data"`
  203. }
  204. // sendProgress 发送进度更新
  205. func (dh *DirectoryHandler) sendProgress(result ProcessResult) {
  206. runtime.EventsEmit(dh.ctx, "copy800-progress", result)
  207. }
  208. // HandlerOutPutDirectory 处理输出目录的主要方法
  209. func (dh *DirectoryHandler) HandlerOutPutDirectory(sourceDir string) ([]ImageResult, error) {
  210. // 检查源目录是否存在
  211. if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
  212. return nil, fmt.Errorf("源目录不存在: %s", sourceDir)
  213. }
  214. var Results []ImageResult
  215. // 执行具体的处理逻辑
  216. // 遍历和处理逻辑
  217. err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
  218. if err != nil {
  219. return err
  220. }
  221. fmt.Printf("正在处理: %s\n", path)
  222. // 处理 800x800 目录的逻辑
  223. if info.IsDir() && strings.Contains(info.Name(), "800x800") {
  224. fmt.Printf("👍👍👍符合条件: %s\n", path)
  225. // 获取父目录名作为 goods_art_no
  226. parentDirName := filepath.Base(filepath.Dir(path))
  227. goodsArtNo := parentDirName
  228. // 查找该目录下的第一张有效图片
  229. imagePath, err := dh.findFirstImageInDir(path)
  230. if err == nil {
  231. toBase64, err := dh.ImageToBase64(imagePath)
  232. if err != nil {
  233. toBase64 = ""
  234. }
  235. // 添加到结果数组
  236. Results = append(Results, ImageResult{
  237. GoodsArtNo: goodsArtNo,
  238. ImagePath: imagePath,
  239. Description: "",
  240. Category: "",
  241. Price: "0",
  242. ImageBase64: toBase64,
  243. })
  244. }
  245. }
  246. return nil
  247. })
  248. if err != nil {
  249. return nil, err
  250. }
  251. return Results, nil
  252. }
  253. // findFirstImageInDir 查找目录下的第一张有效图片
  254. func (dh *DirectoryHandler) findFirstImageInDir(dirPath string) (string, error) {
  255. entries, err := os.ReadDir(dirPath)
  256. if err != nil {
  257. return "", fmt.Errorf("读取目录失败: %v", err)
  258. }
  259. for _, entry := range entries {
  260. if entry.IsDir() {
  261. continue
  262. }
  263. if dh.isValidImageFile(entry.Name()) {
  264. return filepath.Join(dirPath, entry.Name()), nil
  265. }
  266. }
  267. return "", fmt.Errorf("未找到有效图片文件")
  268. }
  269. // ImageToBase64 将图片文件转换为Base64编码
  270. func (dh *DirectoryHandler) ImageToBase64(imagePath string) (string, error) {
  271. // 读取图片文件
  272. fileData, err := os.ReadFile(imagePath)
  273. if err != nil {
  274. return "", fmt.Errorf("读取图片文件失败: %v", err)
  275. }
  276. // 获取文件的MIME类型
  277. mimeType := getMimeType(imagePath)
  278. // 将文件数据编码为Base64
  279. encoded := base64.StdEncoding.EncodeToString(fileData)
  280. // 返回带有数据URI方案的Base64字符串
  281. return fmt.Sprintf("data:%s;base64,%s", mimeType, encoded), nil
  282. }
  283. // getMimeType 根据文件扩展名获取MIME类型
  284. func getMimeType(filePath string) string {
  285. ext := strings.ToLower(filePath[strings.LastIndex(filePath, "."):])
  286. mimeTypes := map[string]string{
  287. ".png": "image/png",
  288. ".jpg": "image/jpeg",
  289. ".jpeg": "image/jpeg",
  290. ".gif": "image/gif",
  291. ".bmp": "image/bmp",
  292. ".webp": "image/webp",
  293. }
  294. if mime, exists := mimeTypes[ext]; exists {
  295. return mime
  296. }
  297. return "application/octet-stream"
  298. }
  299. // GetCurrentWorkingDirectory 获取当前工作目录
  300. func (dh *DirectoryHandler) GetCurrentWorkingDirectory() (string, error) {
  301. return os.Getwd()
  302. }
  303. // GetApplicationDirectory 获取应用程序所在目录
  304. func (dh *DirectoryHandler) GetApplicationDirectory() (string, error) {
  305. execPath, err := os.Executable()
  306. if err != nil {
  307. return "", err
  308. }
  309. return filepath.Dir(execPath), nil
  310. }
  311. // HandlerUploadSkuImages 处理SKU图片上传
  312. func (dh *DirectoryHandler) HandlerUploadSkuImages(sourceDir string) error {
  313. // 检查源目录是否存在
  314. if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
  315. return fmt.Errorf("源目录不存在: %s", sourceDir)
  316. }
  317. // 发送初始进度
  318. dh.sendProgress(ProcessResult{
  319. Success: true,
  320. Message: "开始扫描目录...",
  321. Progress: 0,
  322. })
  323. // 遍历和处理逻辑
  324. err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
  325. if err != nil {
  326. dh.sendProgress(ProcessResult{
  327. Success: false,
  328. Message: fmt.Sprintf("访问路径出错: %v", err),
  329. Progress: 0,
  330. })
  331. return err
  332. }
  333. // 处理 800x800 目录的逻辑
  334. if info.IsDir() && strings.Contains(info.Name(), "800x800") {
  335. fmt.Printf("👍👍👍找到800x800目录: %s\n", path)
  336. // 获取上级目录名称作为Key参数
  337. parentDirName := filepath.Base(filepath.Dir(path))
  338. // 读取800x800目录下的所有文件
  339. files, err := os.ReadDir(path)
  340. if err != nil {
  341. dh.sendProgress(ProcessResult{
  342. Success: false,
  343. Message: fmt.Sprintf("读取目录失败 %s: %v", path, err),
  344. Progress: 0,
  345. })
  346. return err
  347. }
  348. // 尝试从接口获取动作名称列表
  349. var actionNames []string
  350. actionNamesResponse, err := dh.getActionNamesByGoods(parentDirName)
  351. if err == nil && actionNamesResponse.Code == 0 && len(actionNamesResponse.Data) > 0 {
  352. actionNames = actionNamesResponse.Data
  353. fmt.Printf("📋 获取到动作名称列表 [%s]: %v\n", parentDirName, actionNames)
  354. } else {
  355. fmt.Printf("️ 获取动作名称失败或为空,将使用默认逻辑 [%s]\n", parentDirName)
  356. }
  357. // 收集所有有效的图片文件
  358. var validFiles []BatchFileItem
  359. fileIndex := 0
  360. for _, file := range files {
  361. if file.IsDir() {
  362. continue // 跳过子目录
  363. }
  364. // 检查是否为有效的图片文件
  365. if !dh.isValidImageFile(file.Name()) {
  366. continue // 跳过非图片文件
  367. }
  368. filePath := filepath.Join(path, file.Name())
  369. // 确定ImageType:如果接口返回了数据且索引有效,则使用接口返回的值
  370. var imageType string
  371. if len(actionNames) > 0 && fileIndex < len(actionNames) {
  372. imageType = actionNames[fileIndex]
  373. fmt.Printf("️ 使用接口返回的ImageType [%s]: %s\n", file.Name(), imageType)
  374. } else {
  375. // 使用文件名(不含扩展名)作为ImageType参数
  376. imageType = strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
  377. fmt.Printf("🏷️ 使用默认ImageType [%s]: %s\n", file.Name(), imageType)
  378. }
  379. fileIndex++
  380. validFiles = append(validFiles, BatchFileItem{
  381. FilePath: filePath,
  382. ImageType: imageType,
  383. FileName: file.Name(),
  384. })
  385. }
  386. // 如果没有有效文件,跳过
  387. if len(validFiles) == 0 {
  388. fmt.Printf("⚠️ 目录 %s 中没有有效的图片文件\n", path)
  389. return nil
  390. }
  391. fmt.Printf("📦 开始批量上传 [%s]: 共 %d 个文件\n", parentDirName, len(validFiles))
  392. // 批量上传该目录下的所有文件(一次性请求)
  393. var response UploadSkuResponse
  394. err = dh.uploadSkuImagesBatch(validFiles, parentDirName, &response)
  395. if err != nil {
  396. dh.sendProgress(ProcessResult{
  397. Success: false,
  398. Message: fmt.Sprintf("[%s] 批量上传失败: %v", parentDirName, err),
  399. Progress: 0,
  400. })
  401. fmt.Printf("❌ 批量上传失败: %v\n", err)
  402. } else if !response.Success {
  403. dh.sendProgress(ProcessResult{
  404. Success: false,
  405. Message: fmt.Sprintf("[%s] 批量上传失败: API返回错误 code=%d", parentDirName, response.Code),
  406. Progress: 0,
  407. })
  408. fmt.Printf("❌ 批量上传失败: API返回错误 code=%d\n", response.Code)
  409. } else {
  410. dh.sendProgress(ProcessResult{
  411. Success: true,
  412. Message: fmt.Sprintf("[%s] 批量上传成功: 共 %d 个文件", parentDirName, len(response.Data)),
  413. Progress: 0,
  414. })
  415. fmt.Printf("✅ 批量上传成功: %+v\n", response)
  416. }
  417. }
  418. return nil
  419. })
  420. // 发送完成进度
  421. if err != nil {
  422. dh.sendProgress(ProcessResult{
  423. Success: false,
  424. Message: fmt.Sprintf("处理失败: %v", err),
  425. Progress: 100,
  426. })
  427. } else {
  428. dh.sendProgress(ProcessResult{
  429. Success: true,
  430. Message: "所有文件处理完成",
  431. Progress: 100,
  432. })
  433. }
  434. return err
  435. }
  436. // uploadSkuImagesBatch 批量上传SKU图片(同一个Key下的多个文件一次性上传)
  437. func (dh *DirectoryHandler) uploadSkuImagesBatch(files []BatchFileItem, key string, result interface{}) error {
  438. // 创建multipart表单
  439. var buf bytes.Buffer
  440. writer := multipart.NewWriter(&buf)
  441. // 添加Key字段
  442. err := writer.WriteField("Key", key)
  443. if err != nil {
  444. return fmt.Errorf("写入Key字段失败: %w", err)
  445. }
  446. // 添加多个ImageType和File字段对
  447. for i, file := range files {
  448. // 添加ImageType字段
  449. err = writer.WriteField("ImageType", file.ImageType)
  450. if err != nil {
  451. return fmt.Errorf("写入ImageType字段失败 [%d]: %w", i, err)
  452. }
  453. // 打开文件
  454. fileHandle, err := os.Open(file.FilePath)
  455. if err != nil {
  456. return fmt.Errorf("打开文件失败 [%s]: %w", file.FilePath, err)
  457. }
  458. // 添加文件字段
  459. fileWriter, err := writer.CreateFormFile("File", file.FileName)
  460. if err != nil {
  461. fileHandle.Close()
  462. return fmt.Errorf("创建表单文件字段失败 [%s]: %w", file.FileName, err)
  463. }
  464. // 复制文件内容到表单
  465. _, err = io.Copy(fileWriter, fileHandle)
  466. fileHandle.Close()
  467. if err != nil {
  468. return fmt.Errorf("复制文件内容失败 [%s]: %w", file.FileName, err)
  469. }
  470. fmt.Printf("📤 已添加文件 [%d/%d]: %s (ImageType: %s)\n", i+1, len(files), file.FileName, file.ImageType)
  471. }
  472. // 关闭表单写入器
  473. err = writer.Close()
  474. if err != nil {
  475. return fmt.Errorf("关闭表单写入器失败: %w", err)
  476. }
  477. // 创建请求
  478. req, err := http.NewRequestWithContext(dh.ctx, "POST", utils.UploadSkuImage, &buf)
  479. if err != nil {
  480. return fmt.Errorf("创建请求失败: %w", err)
  481. }
  482. // 设置内容类型为multipart表单
  483. req.Header.Set("Content-Type", writer.FormDataContentType())
  484. // 创建HTTP客户端并执行请求
  485. client := &http.Client{}
  486. resp, err := client.Do(req)
  487. if err != nil {
  488. return fmt.Errorf("请求失败: %w", err)
  489. }
  490. defer resp.Body.Close()
  491. // 读取响应体
  492. body, err := io.ReadAll(resp.Body)
  493. if err != nil {
  494. return fmt.Errorf("读取响应体失败: %w", err)
  495. }
  496. // 检查HTTP状态码
  497. if resp.StatusCode >= 400 {
  498. return fmt.Errorf("HTTP请求失败 status=%d body=%s", resp.StatusCode, string(body))
  499. }
  500. // 解析JSON响应
  501. if err := json.Unmarshal(body, result); err != nil {
  502. return fmt.Errorf("解析JSON失败: %w, body=%s", err, string(body))
  503. }
  504. return nil
  505. }
  506. // getActionNamesByGoods 获取动作名称列表
  507. func (dh *DirectoryHandler) getActionNamesByGoods(goodsArtNo string) (*ActionNamesResponse, error) {
  508. // 创建请求
  509. url := fmt.Sprintf("%s?goods_art_no=%s", utils.GetActionNames, goodsArtNo)
  510. req, err := http.NewRequestWithContext(dh.ctx, "GET", url, nil)
  511. if err != nil {
  512. return nil, fmt.Errorf("创建请求失败: %w", err)
  513. }
  514. // 设置请求头
  515. req.Header.Set("Content-Type", "application/json")
  516. // 创建HTTP客户端并执行请求
  517. client := &http.Client{}
  518. resp, err := client.Do(req)
  519. if err != nil {
  520. return nil, fmt.Errorf("请求失败: %w", err)
  521. }
  522. defer resp.Body.Close()
  523. // 读取响应体
  524. body, err := io.ReadAll(resp.Body)
  525. if err != nil {
  526. return nil, fmt.Errorf("读取响应体失败: %w", err)
  527. }
  528. // 检查HTTP状态码
  529. if resp.StatusCode >= 400 {
  530. return nil, fmt.Errorf("HTTP请求失败 status=%d body=%s", resp.StatusCode, string(body))
  531. }
  532. // 解析JSON响应
  533. var response ActionNamesResponse
  534. if err := json.Unmarshal(body, &response); err != nil {
  535. return nil, fmt.Errorf("解析JSON失败: %w, body=%s", err, string(body))
  536. }
  537. return &response, nil
  538. }