dataDownloadPackageService.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. package services
  2. import (
  3. "archive/zip"
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "fmt"
  8. "io"
  9. "path"
  10. "strings"
  11. "time"
  12. "fwh-toolbox-server/cache"
  13. "fwh-toolbox-server/domain"
  14. minioRepo "fwh-toolbox-server/repositories/minio"
  15. mongoRepo "fwh-toolbox-server/repositories/mongo"
  16. "go.uber.org/zap"
  17. )
  18. func BuildDataDownloadPackage(ctx context.Context, job *domain.DataDownloadPackageJob, progress func(done, total int64) error) ([]byte, int64, error) {
  19. type input struct {
  20. taskID, logicalPath, objectKey, filename string
  21. data []byte
  22. document *domain.DataDownloadDocument
  23. artifact *domain.DataDownloadArtifact
  24. }
  25. var inputs []input
  26. type reportFailure struct {
  27. TaskID string `json:"taskId"`
  28. ItemID string `json:"itemId"`
  29. DocumentID string `json:"documentId"`
  30. SourceFileID string `json:"sourceFileId"`
  31. FileName string `json:"fileName,omitempty"`
  32. FailureStage string `json:"failureStage"`
  33. FailureReason string `json:"failureReason"`
  34. AttemptCount int `json:"attemptCount"`
  35. DeviceSN string `json:"deviceSn,omitempty"`
  36. DeviceType string `json:"deviceType,omitempty"`
  37. Date string `json:"date,omitempty"`
  38. }
  39. type reportTask struct {
  40. TaskID string `json:"taskId"`
  41. Status domain.DataDownloadStatus `json:"status"`
  42. Succeeded int64 `json:"succeeded"`
  43. Failed int64 `json:"failed"`
  44. }
  45. var failures []reportFailure
  46. var reportTasks []reportTask
  47. for _, taskID := range job.TaskIDs {
  48. task, err := mongoRepo.GetDataDownloadTask(ctx, taskID)
  49. if err != nil {
  50. return nil, 0, err
  51. }
  52. reportTasks = append(reportTasks, reportTask{TaskID: taskID.Hex(), Status: task.Status, Succeeded: task.Succeeded, Failed: task.Failed})
  53. items, err := mongoRepo.ListDataDownloadItems(ctx, taskID)
  54. if err != nil {
  55. return nil, 0, err
  56. }
  57. itemByID := make(map[string]domain.DataDownloadItem, len(items))
  58. for i := range items {
  59. itemByID[items[i].ID.Hex()] = items[i]
  60. }
  61. attempts, err := mongoRepo.ListDataDownloadFileAttempts(ctx, taskID)
  62. if err != nil {
  63. return nil, 0, err
  64. }
  65. for i := range attempts {
  66. if attempts[i].Status != domain.DataDownloadFailed {
  67. continue
  68. }
  69. item := itemByID[attempts[i].ItemID.Hex()]
  70. failures = append(failures, reportFailure{
  71. TaskID: taskID.Hex(), ItemID: attempts[i].ItemID.Hex(), DocumentID: attempts[i].DocumentID,
  72. SourceFileID: attempts[i].SourceFileID, FileName: attempts[i].FileName,
  73. FailureStage: attempts[i].FailureStage, FailureReason: attempts[i].FailureReason,
  74. AttemptCount: attempts[i].AttemptCount, DeviceSN: item.DeviceSN, DeviceType: item.DeviceType, Date: item.Date,
  75. })
  76. }
  77. links, err := mongoRepo.ListDataDownloadTaskDocuments(ctx, taskID)
  78. if err != nil {
  79. return nil, 0, err
  80. }
  81. for _, link := range links {
  82. if link.Empty {
  83. continue
  84. }
  85. document, err := mongoRepo.GetDataDownloadDocument(ctx, link.DocumentID)
  86. if err != nil {
  87. return nil, 0, err
  88. }
  89. if document.Kind == domain.DataDownloadFaultRecord {
  90. lock, err := cache.Lock(ctx, "data-download:content:"+document.ID, 10*time.Minute, 100*time.Millisecond, 30*time.Second)
  91. if err != nil {
  92. return nil, 0, err
  93. }
  94. entries, err := mongoRepo.ListDataDownloadFaultRecordEntries(ctx, document.ID)
  95. if err != nil {
  96. lock.SafeRelease(context.Background())
  97. return nil, 0, err
  98. }
  99. for i := range entries {
  100. reader, openErr := minioRepo.Open(ctx, entries[i].ObjectKey)
  101. if openErr != nil {
  102. lock.SafeRelease(context.Background())
  103. return nil, 0, openErr
  104. }
  105. data, readErr := io.ReadAll(reader)
  106. _ = reader.Close()
  107. if readErr != nil {
  108. lock.SafeRelease(context.Background())
  109. return nil, 0, readErr
  110. }
  111. filename := fmt.Sprintf("%d-%d-%s", entries[i].FaultID, entries[i].SubRecordID, entries[i].FileName)
  112. inputs = append(inputs, input{taskID: taskID.Hex(), logicalPath: link.LogicalPath, objectKey: entries[i].ObjectKey, filename: filename, data: data, document: document})
  113. }
  114. lock.SafeRelease(context.Background())
  115. continue
  116. }
  117. entry := input{taskID: taskID.Hex(), logicalPath: link.LogicalPath, document: document}
  118. if document.BinaryObjectKey != "" {
  119. lock, err := cache.Lock(ctx, "data-download:content:"+document.ID, 10*time.Minute, 100*time.Millisecond, 30*time.Second)
  120. if err != nil {
  121. return nil, 0, err
  122. }
  123. current, err := mongoRepo.GetDataDownloadDocument(ctx, document.ID)
  124. if err != nil {
  125. lock.SafeRelease(context.Background())
  126. return nil, 0, err
  127. }
  128. reader, err := minioRepo.Open(ctx, current.BinaryObjectKey)
  129. if err != nil {
  130. lock.SafeRelease(context.Background())
  131. return nil, 0, err
  132. }
  133. entry.data, err = io.ReadAll(reader)
  134. _ = reader.Close()
  135. lock.SafeRelease(context.Background())
  136. if err != nil {
  137. return nil, 0, err
  138. }
  139. entry.objectKey = current.BinaryObjectKey
  140. } else {
  141. artifact, data, err := ReadRenderedDataDownloadDocument(ctx, document.ID, defaultDataDownloadFormat(document.Kind))
  142. if err != nil {
  143. return nil, 0, err
  144. }
  145. entry.data = data
  146. entry.artifact, entry.objectKey = artifact, artifact.ObjectKey
  147. }
  148. inputs = append(inputs, entry)
  149. }
  150. }
  151. if len(inputs) == 0 && len(failures) == 0 {
  152. return nil, 0, fmt.Errorf("no readable non-empty documents")
  153. }
  154. if len(failures) > 0 {
  155. report, err := json.MarshalIndent(struct {
  156. GeneratedAt time.Time `json:"generatedAt"`
  157. Tasks []reportTask `json:"tasks"`
  158. Failures []reportFailure `json:"failures"`
  159. }{GeneratedAt: time.Now().UTC(), Tasks: reportTasks, Failures: failures}, "", " ")
  160. if err != nil {
  161. return nil, 0, err
  162. }
  163. inputs = append(inputs, input{logicalPath: "", filename: "DataDownloadReport.json", data: report})
  164. }
  165. if progress != nil {
  166. if err := progress(0, int64(len(inputs))); err != nil {
  167. return nil, 0, err
  168. }
  169. }
  170. zap.L().Debug("[DataDownload] 打包收集完成,开始写 ZIP",
  171. zap.Int("fileCount", len(inputs)),
  172. zap.Int("failureCount", len(failures)),
  173. zap.Int("taskCount", len(job.TaskIDs)),
  174. )
  175. var buffer bytes.Buffer
  176. archive := zip.NewWriter(&buffer)
  177. for i, entry := range inputs {
  178. var reader io.ReadCloser
  179. if entry.data != nil {
  180. reader = io.NopCloser(bytes.NewReader(entry.data))
  181. } else {
  182. opened, err := minioRepo.Open(ctx, entry.objectKey)
  183. if err != nil {
  184. zap.L().Warn("[DataDownload] 打包读取对象失败", zap.String("objectKey", entry.objectKey), zap.Int("index", i), zap.String("err", err.Error()))
  185. _ = archive.Close()
  186. return nil, int64(i), err
  187. }
  188. reader = opened
  189. }
  190. filename := entry.filename
  191. if filename == "" {
  192. filename = path.Base(entry.objectKey)
  193. }
  194. if entry.artifact != nil {
  195. filename = entry.artifact.Filename
  196. }
  197. logical := cleanZipPath(entry.logicalPath)
  198. if len(job.TaskIDs) > 1 {
  199. logical = path.Join(entry.taskID, logical)
  200. }
  201. writer, err := archive.Create(path.Join(logical, filename))
  202. if err == nil {
  203. _, err = io.Copy(writer, reader)
  204. }
  205. _ = reader.Close()
  206. if err != nil {
  207. _ = archive.Close()
  208. return nil, int64(i), err
  209. }
  210. if progress != nil {
  211. if err := progress(int64(i+1), int64(len(inputs))); err != nil {
  212. _ = archive.Close()
  213. return nil, int64(i + 1), err
  214. }
  215. }
  216. }
  217. if err := archive.Close(); err != nil {
  218. return nil, 0, err
  219. }
  220. return buffer.Bytes(), int64(len(inputs)), nil
  221. }
  222. func defaultDataDownloadFormat(kind domain.DataDownloadKind) string {
  223. switch kind {
  224. case domain.DataDownloadPowerFlow:
  225. return "html"
  226. case domain.DataDownloadMqttLog, domain.DataDownloadEventAlarm, domain.DataDownloadUpdateLog:
  227. return "xlsx"
  228. default:
  229. return ""
  230. }
  231. }
  232. func cleanZipPath(value string) string {
  233. value = strings.ReplaceAll(value, "\\", "/")
  234. value = strings.TrimPrefix(path.Clean("/"+value), "/")
  235. if value == "." {
  236. return "documents"
  237. }
  238. return value
  239. }