faultRecordServices.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. package services
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "strings"
  8. "time"
  9. "fwh-toolbox-server/domain"
  10. "fwh-toolbox-server/dto"
  11. faultRecordFile "fwh-toolbox-server/function/FaultRecordFile"
  12. proto "fwh-toolbox-server/function/FaultRecordProtocol"
  13. "fwh-toolbox-server/repositories/api/fwh_pro"
  14. "fwh-toolbox-server/repositories/api/fwh_test"
  15. minioRepo "fwh-toolbox-server/repositories/minio"
  16. mongoRepo "fwh-toolbox-server/repositories/mongo"
  17. "fwh-toolbox-server/repositories/mqtt/fwh_mqtt"
  18. "fwh-toolbox-server/task/queue/faultRecordQueue"
  19. "go.mongodb.org/mongo-driver/bson/primitive"
  20. "go.uber.org/zap"
  21. )
  22. var ErrFaultRecordInvalid = errors.New("invalid request")
  23. func faultRecordInvalid(format string, a ...any) error {
  24. return fmt.Errorf("%w: %s", ErrFaultRecordInvalid, fmt.Sprintf(format, a...))
  25. }
  26. /************** 设备列表 **************/
  27. // GetFaultRecordSiteDevices 按环境查询站点信息,返回 aGate 及各 aPower(pe)。
  28. // 只调用 Repository,不写站点信息表(测试服数据不能写入)。
  29. func GetFaultRecordSiteDevices(envStr, gatewaySn string) (*dto.FaultRecordSiteDevices, error) {
  30. env, err := fwh_mqtt.ParseEnv(envStr)
  31. if err != nil {
  32. return nil, faultRecordInvalid("%v", err)
  33. }
  34. gatewaySn = strings.TrimSpace(gatewaySn)
  35. if gatewaySn == "" {
  36. return nil, faultRecordInvalid("gatewaySn is empty")
  37. }
  38. para := dto.SiteInfoPara{SN: &gatewaySn, PageSize: 10, PageNum: 1}
  39. var res *domain.FwhPageResponse[[]domain.FwhSiteInfo]
  40. if env == fwh_mqtt.EnvTest {
  41. res, err = fwh_test.QuerySiteInfoList(para)
  42. } else {
  43. res, err = fwh_pro.QuerySiteInfoList(para)
  44. }
  45. if err != nil {
  46. return nil, err
  47. }
  48. if res.Result == nil || len(*res.Result) == 0 {
  49. return nil, fmt.Errorf("站点不存在(%s)", gatewaySn)
  50. }
  51. var site *domain.FwhSiteInfo
  52. for i := range *res.Result {
  53. if (*res.Result)[i].Id == gatewaySn {
  54. site = &(*res.Result)[i]
  55. }
  56. }
  57. if site == nil {
  58. return nil, fmt.Errorf("站点不存在(%s)", gatewaySn)
  59. }
  60. out := &dto.FaultRecordSiteDevices{
  61. Env: string(env),
  62. GatewaySn: site.Id,
  63. Online: site.Status == 1,
  64. Devices: []dto.FaultRecordDevice{{Type: proto.DeviceTypeAGate, Sn: site.Id}},
  65. }
  66. if site.Name != nil {
  67. out.SiteName = *site.Name
  68. }
  69. if site.OnLineTime != nil {
  70. out.OnlineTime = *site.OnLineTime
  71. }
  72. for _, sn := range strings.Split(site.FhpSn, ",") {
  73. if sn = strings.TrimSpace(sn); sn != "" {
  74. out.Devices = append(out.Devices, dto.FaultRecordDevice{Type: proto.DeviceTypePE, Sn: sn}) // aPower SN 即 pe SN
  75. }
  76. }
  77. return out, nil
  78. }
  79. /************** 创建/取消 **************/
  80. // CreateFaultRecordOperation 校验并创建操作,提交异步执行
  81. func CreateFaultRecordOperation(ctx context.Context, req dto.FaultRecordCreateOpReq, operator string) (string, error) {
  82. env, err := fwh_mqtt.ParseEnv(req.Env)
  83. if err != nil {
  84. return "", faultRecordInvalid("%v", err)
  85. }
  86. if _, err := fwh_mqtt.GetClient(env); err != nil {
  87. return "", fmt.Errorf("%s 环境 MQTT 不可用:%w", env, err)
  88. }
  89. gatewaySn := strings.TrimSpace(req.GatewaySn)
  90. if gatewaySn == "" {
  91. return "", faultRecordInvalid("gatewaySn is empty")
  92. }
  93. deviceType, err := proto.NormalizeDeviceType(req.DeviceType)
  94. if err != nil {
  95. return "", faultRecordInvalid("%v", err)
  96. }
  97. deviceSn := strings.TrimSpace(req.DeviceSn)
  98. if deviceType == proto.DeviceTypeAGate && deviceSn == "" {
  99. deviceSn = gatewaySn
  100. }
  101. kind := domain.FaultRecordOpKind(req.Kind)
  102. op := &domain.FaultRecordOperation{
  103. Env: string(env),
  104. GatewaySn: gatewaySn,
  105. DeviceType: deviceType,
  106. DeviceSn: deviceSn,
  107. Kind: kind,
  108. Cmd: kind.Cmd(),
  109. Operator: operator,
  110. Status: domain.FaultRecordStatusPending,
  111. Request: domain.FaultRecordOpRequest{
  112. CodeList: req.CodeList,
  113. RecordIndexes: recordIndexesOf(req),
  114. IdList: req.IdList,
  115. ParaList: req.ParaList,
  116. },
  117. }
  118. if op.Cmd < 0 {
  119. return "", faultRecordInvalid("unknown kind %s", req.Kind)
  120. }
  121. // 与执行器使用同一构造函数校验参数,尽早返回错误
  122. if _, err := faultRecordQueue.BuildRequest(op); err != nil {
  123. return "", faultRecordInvalid("%v", err)
  124. }
  125. if err := mongoRepo.InsertFaultRecordOp(ctx, op); err != nil {
  126. return "", err
  127. }
  128. if err := faultRecordQueue.Enqueue(op.ID); err != nil {
  129. _, _ = mongoRepo.UpdateFaultRecordOpStatus(ctx, op.ID, domain.FaultRecordStatusError, map[string]any{"error": "提交任务失败:" + err.Error()})
  130. return "", err
  131. }
  132. return op.ID.Hex(), nil
  133. }
  134. // CancelFaultRecordOperation 请求取消;执行器在 1 秒内响应
  135. func CancelFaultRecordOperation(ctx context.Context, idStr string) error {
  136. id, err := primitive.ObjectIDFromHex(idStr)
  137. if err != nil {
  138. return faultRecordInvalid("invalid id")
  139. }
  140. ok, err := mongoRepo.RequestFaultRecordOpCancel(ctx, id)
  141. if err != nil {
  142. return err
  143. }
  144. if !ok {
  145. return faultRecordInvalid("操作已结束,无法取消")
  146. }
  147. return nil
  148. }
  149. // RetryFaultRecordDownload 上传已完成但下载失败时,手动重新下载(按 15 秒间隔重试至总超时)
  150. func RetryFaultRecordDownload(ctx context.Context, idStr, operator string) error {
  151. op, err := getFaultRecordOp(ctx, idStr)
  152. if err != nil {
  153. return err
  154. }
  155. if !op.CanRetryDownload() {
  156. return faultRecordInvalid("当前状态不能重试下载(需上传已完成且下载失败或超时)")
  157. }
  158. ok, err := mongoRepo.ReopenFaultRecordOpForDownloadRetry(ctx, op.ID)
  159. if err != nil {
  160. return err
  161. }
  162. if !ok {
  163. return faultRecordInvalid("操作状态已变化,请刷新后再试")
  164. }
  165. // 在原始报文中记录本次手动重试(先于提交任务,保证执行器读到的报文条数包含它)
  166. attempt := op.DownloadRetryCount + 1
  167. raw, _ := json.Marshal(map[string]any{
  168. "action": "retry_download",
  169. "operator": operator,
  170. "attempt": attempt,
  171. "prevStatus": op.Status,
  172. "prevError": op.Error,
  173. })
  174. if err := mongoRepo.AppendFaultRecordOpMessage(ctx, op.ID, domain.FaultRecordOpMessage{
  175. Direction: "action", Topic: "手动重试下载", At: time.Now(), Raw: string(raw),
  176. }); err != nil {
  177. zap.L().Warn("fault record append retry action failed", zap.String("id", op.ID.Hex()), zap.Error(err))
  178. }
  179. if err := faultRecordQueue.EnqueueDownloadRetry(op.ID, attempt); err != nil {
  180. _, _ = mongoRepo.UpdateFaultRecordOpStatus(ctx, op.ID, domain.FaultRecordStatusDownloadFailed, map[string]any{"error": "提交重试下载失败:" + err.Error()})
  181. return err
  182. }
  183. return nil
  184. }
  185. /************** 查询 **************/
  186. func ListFaultRecordOperations(ctx context.Context, f mongoRepo.FaultRecordOpFilter, page, size int64) (*dto.ResPage[dto.FaultRecordOpSummary], error) {
  187. list, total, err := mongoRepo.ListFaultRecordOps(ctx, f, page, size)
  188. if err != nil {
  189. return nil, err
  190. }
  191. out := &dto.ResPage[dto.FaultRecordOpSummary]{
  192. Data: make([]dto.FaultRecordOpSummary, 0, len(list)),
  193. PageNum: uint(page),
  194. PageSize: uint(size),
  195. Total: uint(total),
  196. }
  197. for i := range list {
  198. out.Data = append(out.Data, toFaultRecordSummary(&list[i]))
  199. }
  200. return out, nil
  201. }
  202. func GetFaultRecordOperation(ctx context.Context, idStr string) (*dto.FaultRecordOpDetail, error) {
  203. op, err := getFaultRecordOp(ctx, idStr)
  204. if err != nil {
  205. return nil, err
  206. }
  207. d := &dto.FaultRecordOpDetail{
  208. FaultRecordOpSummary: toFaultRecordSummary(op),
  209. Request: op.Request,
  210. Items: []dto.FaultRecordResultItem{},
  211. Messages: make([]dto.FaultRecordOpMessageView, 0, len(op.Messages)),
  212. }
  213. for _, m := range op.Messages {
  214. d.Messages = append(d.Messages, dto.FaultRecordOpMessageView(m))
  215. }
  216. if op.Reply != nil {
  217. d.ReplyResult = op.Reply.Result
  218. d.ReplyDesc = proto.DescribeReplyResult(op.Reply.Result)
  219. if info, ok := op.Reply.FindInfo(op.DeviceType, op.DeviceSn); ok {
  220. d.Items = faultRecordResultItems(op, info)
  221. }
  222. }
  223. return d, nil
  224. }
  225. // GetFaultRecordRecord 读取并解析已保存的录波文件
  226. func GetFaultRecordRecord(ctx context.Context, idStr string) (*dto.FaultRecordRecordView, error) {
  227. op, data, err := readFaultRecordFile(ctx, idStr)
  228. if err != nil {
  229. return nil, err
  230. }
  231. records, err := faultRecordFile.ParseArchive(data, op.DeviceType)
  232. if err != nil {
  233. return nil, fmt.Errorf("解析录波文件失败:%w", err)
  234. }
  235. return &dto.FaultRecordRecordView{OperationID: op.ID.Hex(), Records: records}, nil
  236. }
  237. // ExportFaultRecordFile 导出原始压缩包或 CSV(zip),返回文件名和内容
  238. func ExportFaultRecordFile(ctx context.Context, idStr, format string) (string, []byte, error) {
  239. op, data, err := readFaultRecordFile(ctx, idStr)
  240. if err != nil {
  241. return "", nil, err
  242. }
  243. switch format {
  244. case "", "raw":
  245. return proto.UploadFileName(op.FileName), data, nil
  246. case "csv":
  247. records, err := faultRecordFile.ParseArchive(data, op.DeviceType)
  248. if err != nil {
  249. return "", nil, fmt.Errorf("解析录波文件失败:%w", err)
  250. }
  251. zipData, err := faultRecordFile.BuildCSVZip(records)
  252. if err != nil {
  253. return "", nil, err
  254. }
  255. return proto.BatchFromUploadFileName(op.FileName) + "_csv.zip", zipData, nil
  256. }
  257. return "", nil, faultRecordInvalid("unknown format %s", format)
  258. }
  259. func recordIndexesOf(req dto.FaultRecordCreateOpReq) []int {
  260. if len(req.RecordIndexes) > 0 {
  261. return req.RecordIndexes
  262. }
  263. if req.RecordIndex > 0 {
  264. return []int{req.RecordIndex}
  265. }
  266. return nil
  267. }
  268. func getFaultRecordOp(ctx context.Context, idStr string) (*domain.FaultRecordOperation, error) {
  269. id, err := primitive.ObjectIDFromHex(idStr)
  270. if err != nil {
  271. return nil, faultRecordInvalid("invalid id")
  272. }
  273. return mongoRepo.GetFaultRecordOp(ctx, id)
  274. }
  275. func readFaultRecordFile(ctx context.Context, idStr string) (*domain.FaultRecordOperation, []byte, error) {
  276. op, err := getFaultRecordOp(ctx, idStr)
  277. if err != nil {
  278. return nil, nil, err
  279. }
  280. if op.MinioObject == "" {
  281. return nil, nil, faultRecordInvalid("该操作没有已保存的录波文件")
  282. }
  283. data, err := minioRepo.ReadFaultRecordFile(ctx, op.MinioObject)
  284. if err != nil {
  285. return nil, nil, err
  286. }
  287. return op, data, nil
  288. }
  289. /************** 视图转换 **************/
  290. func toFaultRecordSummary(op *domain.FaultRecordOperation) dto.FaultRecordOpSummary {
  291. s := dto.FaultRecordOpSummary{
  292. ID: op.ID.Hex(),
  293. Env: op.Env,
  294. GatewaySn: op.GatewaySn,
  295. DeviceType: op.DeviceType,
  296. DeviceSn: op.DeviceSn,
  297. Kind: string(op.Kind),
  298. Summary: faultRecordRequestSummary(op),
  299. Operator: op.Operator,
  300. Status: string(op.Status),
  301. TimeoutStage: string(op.TimeoutStage),
  302. Error: op.Error,
  303. StageTimes: op.StageTimes,
  304. FileName: op.FileName,
  305. FaultRecordID: op.FaultRecordID,
  306. CanCancel: !op.Status.Terminal() && !op.CancelReq,
  307. CanRetryDownload: op.CanRetryDownload(),
  308. HasFile: op.MinioObject != "",
  309. CreatedAt: op.CreatedAt,
  310. UpdatedAt: op.UpdatedAt,
  311. }
  312. return s
  313. }
  314. func joinInts(list []int) string {
  315. parts := make([]string, len(list))
  316. for i, v := range list {
  317. parts[i] = fmt.Sprint(v)
  318. }
  319. return strings.Join(parts, ",")
  320. }
  321. func faultRecordRequestSummary(op *domain.FaultRecordOperation) string {
  322. r := op.Request
  323. switch op.Kind {
  324. case domain.FaultRecordOpDeviceInfo:
  325. return "code=" + joinInts(r.CodeList)
  326. case domain.FaultRecordOpRecordFetch:
  327. return "记录序号 " + joinInts(r.Indexes())
  328. case domain.FaultRecordOpParamRead:
  329. return "功能码 " + joinInts(r.IdList)
  330. case domain.FaultRecordOpParamWrite:
  331. parts := make([]string, len(r.ParaList))
  332. for i, p := range r.ParaList {
  333. parts[i] = fmt.Sprintf("%d=%s", p.Id, p.SetVal)
  334. }
  335. return strings.Join(parts, "; ")
  336. }
  337. return ""
  338. }
  339. func faultRecordResultItems(op *domain.FaultRecordOperation, info *proto.ReplyInfo) []dto.FaultRecordResultItem {
  340. items := []dto.FaultRecordResultItem{}
  341. result := func(it *dto.FaultRecordResultItem, resultOne string) {
  342. it.ResultOne = resultOne
  343. it.ResultDesc = proto.DescribeResultOne(resultOne)
  344. it.Ok = resultOne == proto.ResultOk
  345. }
  346. switch op.Kind {
  347. case domain.FaultRecordOpDeviceInfo:
  348. for _, h := range info.HexList {
  349. it := dto.FaultRecordResultItem{Code: h.Code, Name: proto.RealtimeCodeNames[h.Code], Raw: h.HexData}
  350. result(&it, h.ResultOne)
  351. if it.Ok && h.HexData != "" {
  352. if v, err := faultRecordFile.ParseRealtime(h.Code, h.HexData); err != nil {
  353. it.DecodeErr = err.Error()
  354. } else {
  355. it.Decoded = v
  356. }
  357. }
  358. items = append(items, it)
  359. }
  360. case domain.FaultRecordOpRecordFetch:
  361. // 每条记录展开为 4(故障数据)、5(波形数据)两项,按请求顺序对应(待实测确认)
  362. indexes := op.Request.Indexes()
  363. for i, x := range info.IndexInfo {
  364. name := map[int]string{4: "故障数据", 5: "波形数据"}[x.Index]
  365. if len(indexes) > 1 && len(info.IndexInfo) == 2*len(indexes) {
  366. name = fmt.Sprintf("记录 %d %s", indexes[i/2], name)
  367. }
  368. it := dto.FaultRecordResultItem{Code: x.Index, Name: name}
  369. result(&it, x.ResultOne)
  370. items = append(items, it)
  371. }
  372. case domain.FaultRecordOpParamRead:
  373. for _, x := range info.IdInfo {
  374. it := dto.FaultRecordResultItem{Id: x.Id, Name: proto.FunctionCodeNames[x.Id], Raw: x.IdData}
  375. result(&it, x.ResultOne)
  376. if it.Ok {
  377. it.Decoded = proto.DecodeIdData(x.Id, x.IdData)
  378. }
  379. items = append(items, it)
  380. }
  381. case domain.FaultRecordOpParamWrite:
  382. // 同一功能码可重复(如多个 id=6),按请求顺序对应(待实测确认)
  383. for i, x := range info.SetResultList {
  384. it := dto.FaultRecordResultItem{Id: x.Id, Name: proto.FunctionCodeNames[x.Id]}
  385. if i < len(op.Request.ParaList) && op.Request.ParaList[i].Id == x.Id {
  386. it.SetVal = op.Request.ParaList[i].SetVal
  387. }
  388. result(&it, x.ResultOne)
  389. items = append(items, it)
  390. }
  391. }
  392. return items
  393. }