faultRecordServices.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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. }
  65. if site.Name != nil {
  66. out.SiteName = *site.Name
  67. }
  68. if site.OnLineTime != nil {
  69. out.OnlineTime = *site.OnLineTime
  70. }
  71. // aPower 及 PE 硬件版本来自 getDeviceInfo;aHub 来自 selectGatewayPower
  72. var devInfo *domain.FwhDeviceInfo
  73. var power *domain.FwhGatewayPower
  74. var devErr, powerErr error
  75. if env == fwh_mqtt.EnvTest {
  76. devInfo, devErr = fwh_test.QueryDeviceInfo(site.Id)
  77. power, powerErr = fwh_test.QueryGatewayPower(site.Id)
  78. } else {
  79. devInfo, devErr = fwh_pro.QueryDeviceInfo(site.Id)
  80. power, powerErr = fwh_pro.QueryGatewayPower(site.Id)
  81. }
  82. if devErr != nil {
  83. out.Warnings = append(out.Warnings, "获取 aPower 硬件版本失败,无法判断 aPower-MPPT:"+devErr.Error())
  84. devInfo = &domain.FwhDeviceInfo{FhpSn: site.FhpSn} // 退回站点信息中的 aPower 列表
  85. }
  86. if powerErr != nil {
  87. out.Warnings = append(out.Warnings, "获取 aHub 列表失败:"+powerErr.Error())
  88. }
  89. out.Devices = BuildFaultRecordDevices(site.Id, devInfo, power)
  90. return out, nil
  91. }
  92. // BuildFaultRecordDevices 生成可操作的设备列表:aGate/MCA、各 aPower-PE、支持 MPPT 的 aPower-MPPT、各 aHub。
  93. // peHwVerList、apowerList 与 fhpSn 中的 aPower 顺序一一对应;PE 硬件版本为 21、24 的 aPower 带 MPPT。
  94. func BuildFaultRecordDevices(gatewaySn string, devInfo *domain.FwhDeviceInfo, power *domain.FwhGatewayPower) []dto.FaultRecordDevice {
  95. newDevice := func(kind, sn string) dto.FaultRecordDevice {
  96. d := dto.FaultRecordDevice{Kind: kind, Type: proto.DeviceKindProtocolType[kind], Sn: sn}
  97. d.Supported = d.Type != ""
  98. if !d.Supported {
  99. d.Note = "该设备类型暂不支持"
  100. }
  101. return d
  102. }
  103. devices := []dto.FaultRecordDevice{newDevice(proto.DeviceKindAGate, gatewaySn)}
  104. if devInfo != nil {
  105. i := 0
  106. for _, sn := range strings.Split(devInfo.FhpSn, ",") {
  107. if sn = strings.TrimSpace(sn); sn == "" {
  108. continue
  109. }
  110. pe := newDevice(proto.DeviceKindAPowerPE, sn)
  111. if i < len(devInfo.ApowerList) && devInfo.ApowerList[i].Id == sn {
  112. pe.Model = devInfo.ApowerList[i].ModelName
  113. }
  114. if i < len(devInfo.PeHwVerList) {
  115. ver := devInfo.PeHwVerList[i]
  116. pe.HwVer = &ver
  117. }
  118. devices = append(devices, pe)
  119. if pe.HwVer != nil && proto.SupportsMPPT(*pe.HwVer) {
  120. mppt := newDevice(proto.DeviceKindAPowerMPPT, sn)
  121. mppt.Model, mppt.HwVer = pe.Model, pe.HwVer
  122. devices = append(devices, mppt)
  123. }
  124. i++
  125. }
  126. }
  127. if power != nil {
  128. for _, a := range power.Apbox20List {
  129. if sn := strings.TrimSpace(a.Apbox20Sn); sn != "" {
  130. devices = append(devices, newDevice(proto.DeviceKindAHub, sn))
  131. }
  132. }
  133. }
  134. return devices
  135. }
  136. /************** 创建/取消 **************/
  137. // CreateFaultRecordOperation 校验并创建操作,提交异步执行
  138. func CreateFaultRecordOperation(ctx context.Context, req dto.FaultRecordCreateOpReq, operator string) (string, error) {
  139. env, err := fwh_mqtt.ParseEnv(req.Env)
  140. if err != nil {
  141. return "", faultRecordInvalid("%v", err)
  142. }
  143. if _, err := fwh_mqtt.GetClient(env); err != nil {
  144. return "", fmt.Errorf("%s 环境 MQTT 不可用:%w", env, err)
  145. }
  146. gatewaySn := strings.TrimSpace(req.GatewaySn)
  147. if gatewaySn == "" {
  148. return "", faultRecordInvalid("gatewaySn is empty")
  149. }
  150. deviceType, err := proto.NormalizeDeviceType(req.DeviceType)
  151. if err != nil {
  152. return "", faultRecordInvalid("%v", err)
  153. }
  154. deviceSn := strings.TrimSpace(req.DeviceSn)
  155. if deviceType == proto.DeviceTypeAGate && deviceSn == "" {
  156. deviceSn = gatewaySn
  157. }
  158. kind := domain.FaultRecordOpKind(req.Kind)
  159. op := &domain.FaultRecordOperation{
  160. Env: string(env),
  161. GatewaySn: gatewaySn,
  162. DeviceType: deviceType,
  163. DeviceSn: deviceSn,
  164. Kind: kind,
  165. Cmd: kind.Cmd(),
  166. Operator: operator,
  167. Status: domain.FaultRecordStatusPending,
  168. Request: domain.FaultRecordOpRequest{
  169. CodeList: req.CodeList,
  170. RecordIndexes: recordIndexesOf(req),
  171. IdList: req.IdList,
  172. ParaList: req.ParaList,
  173. },
  174. }
  175. if op.Cmd < 0 {
  176. return "", faultRecordInvalid("unknown kind %s", req.Kind)
  177. }
  178. // 与执行器使用同一构造函数校验参数,尽早返回错误
  179. if _, err := faultRecordQueue.BuildRequest(op); err != nil {
  180. return "", faultRecordInvalid("%v", err)
  181. }
  182. if err := mongoRepo.InsertFaultRecordOp(ctx, op); err != nil {
  183. return "", err
  184. }
  185. if err := faultRecordQueue.Enqueue(op.ID); err != nil {
  186. _, _ = mongoRepo.UpdateFaultRecordOpStatus(ctx, op.ID, domain.FaultRecordStatusError, map[string]any{"error": "提交任务失败:" + err.Error()})
  187. return "", err
  188. }
  189. return op.ID.Hex(), nil
  190. }
  191. // CancelFaultRecordOperation 请求取消;执行器在 1 秒内响应
  192. func CancelFaultRecordOperation(ctx context.Context, idStr string) error {
  193. id, err := primitive.ObjectIDFromHex(idStr)
  194. if err != nil {
  195. return faultRecordInvalid("invalid id")
  196. }
  197. ok, err := mongoRepo.RequestFaultRecordOpCancel(ctx, id)
  198. if err != nil {
  199. return err
  200. }
  201. if !ok {
  202. return faultRecordInvalid("操作已结束,无法取消")
  203. }
  204. return nil
  205. }
  206. // RetryFaultRecordDownload 上传已完成但下载失败时,手动重新下载(按 15 秒间隔重试至总超时)
  207. func RetryFaultRecordDownload(ctx context.Context, idStr, operator string) error {
  208. op, err := getFaultRecordOp(ctx, idStr)
  209. if err != nil {
  210. return err
  211. }
  212. if !op.CanRetryDownload() {
  213. return faultRecordInvalid("当前状态不能重试下载(需上传已完成且下载失败或超时)")
  214. }
  215. ok, err := mongoRepo.ReopenFaultRecordOpForDownloadRetry(ctx, op.ID)
  216. if err != nil {
  217. return err
  218. }
  219. if !ok {
  220. return faultRecordInvalid("操作状态已变化,请刷新后再试")
  221. }
  222. // 在原始报文中记录本次手动重试(先于提交任务,保证执行器读到的报文条数包含它)
  223. attempt := op.DownloadRetryCount + 1
  224. raw, _ := json.Marshal(map[string]any{
  225. "action": "retry_download",
  226. "operator": operator,
  227. "attempt": attempt,
  228. "prevStatus": op.Status,
  229. "prevError": op.Error,
  230. })
  231. if err := mongoRepo.AppendFaultRecordOpMessage(ctx, op.ID, domain.FaultRecordOpMessage{
  232. Direction: "action", Topic: "手动重试下载", At: time.Now(), Raw: string(raw),
  233. }); err != nil {
  234. zap.L().Warn("fault record append retry action failed", zap.String("id", op.ID.Hex()), zap.Error(err))
  235. }
  236. if err := faultRecordQueue.EnqueueDownloadRetry(op.ID, attempt); err != nil {
  237. _, _ = mongoRepo.UpdateFaultRecordOpStatus(ctx, op.ID, domain.FaultRecordStatusDownloadFailed, map[string]any{"error": "提交重试下载失败:" + err.Error()})
  238. return err
  239. }
  240. return nil
  241. }
  242. /************** 查询 **************/
  243. func ListFaultRecordOperations(ctx context.Context, f mongoRepo.FaultRecordOpFilter, page, size int64) (*dto.ResPage[dto.FaultRecordOpSummary], error) {
  244. list, total, err := mongoRepo.ListFaultRecordOps(ctx, f, page, size)
  245. if err != nil {
  246. return nil, err
  247. }
  248. out := &dto.ResPage[dto.FaultRecordOpSummary]{
  249. Data: make([]dto.FaultRecordOpSummary, 0, len(list)),
  250. PageNum: uint(page),
  251. PageSize: uint(size),
  252. Total: uint(total),
  253. }
  254. for i := range list {
  255. out.Data = append(out.Data, toFaultRecordSummary(&list[i]))
  256. }
  257. return out, nil
  258. }
  259. func GetFaultRecordOperation(ctx context.Context, idStr string) (*dto.FaultRecordOpDetail, error) {
  260. op, err := getFaultRecordOp(ctx, idStr)
  261. if err != nil {
  262. return nil, err
  263. }
  264. d := &dto.FaultRecordOpDetail{
  265. FaultRecordOpSummary: toFaultRecordSummary(op),
  266. Request: op.Request,
  267. Items: []dto.FaultRecordResultItem{},
  268. Messages: make([]dto.FaultRecordOpMessageView, 0, len(op.Messages)),
  269. }
  270. for _, m := range op.Messages {
  271. d.Messages = append(d.Messages, dto.FaultRecordOpMessageView(m))
  272. }
  273. if op.Reply != nil {
  274. d.ReplyResult = op.Reply.Result
  275. d.ReplyDesc = proto.DescribeReplyResult(op.Reply.Result)
  276. if info, ok := op.Reply.FindInfo(op.DeviceType, op.DeviceSn); ok {
  277. d.Items = faultRecordResultItems(op, info)
  278. }
  279. }
  280. return d, nil
  281. }
  282. // GetFaultRecordRecord 读取并解析已保存的录波文件
  283. func GetFaultRecordRecord(ctx context.Context, idStr string) (*dto.FaultRecordRecordView, error) {
  284. op, data, err := readFaultRecordFile(ctx, idStr)
  285. if err != nil {
  286. return nil, err
  287. }
  288. records, err := faultRecordFile.ParseArchive(data, op.DeviceType)
  289. if err != nil {
  290. return nil, fmt.Errorf("解析录波文件失败:%w", err)
  291. }
  292. return &dto.FaultRecordRecordView{OperationID: op.ID.Hex(), Records: records}, nil
  293. }
  294. // ExportFaultRecordFile 导出原始压缩包或 CSV(zip),返回文件名和内容
  295. func ExportFaultRecordFile(ctx context.Context, idStr, format string) (string, []byte, error) {
  296. op, data, err := readFaultRecordFile(ctx, idStr)
  297. if err != nil {
  298. return "", nil, err
  299. }
  300. switch format {
  301. case "", "raw":
  302. return proto.UploadFileName(op.FileName), data, nil
  303. case "csv":
  304. records, err := faultRecordFile.ParseArchive(data, op.DeviceType)
  305. if err != nil {
  306. return "", nil, fmt.Errorf("解析录波文件失败:%w", err)
  307. }
  308. zipData, err := faultRecordFile.BuildCSVZip(records)
  309. if err != nil {
  310. return "", nil, err
  311. }
  312. return proto.BatchFromUploadFileName(op.FileName) + "_csv.zip", zipData, nil
  313. }
  314. return "", nil, faultRecordInvalid("unknown format %s", format)
  315. }
  316. func recordIndexesOf(req dto.FaultRecordCreateOpReq) []int {
  317. if len(req.RecordIndexes) > 0 {
  318. return req.RecordIndexes
  319. }
  320. if req.RecordIndex > 0 {
  321. return []int{req.RecordIndex}
  322. }
  323. return nil
  324. }
  325. func getFaultRecordOp(ctx context.Context, idStr string) (*domain.FaultRecordOperation, error) {
  326. id, err := primitive.ObjectIDFromHex(idStr)
  327. if err != nil {
  328. return nil, faultRecordInvalid("invalid id")
  329. }
  330. return mongoRepo.GetFaultRecordOp(ctx, id)
  331. }
  332. func readFaultRecordFile(ctx context.Context, idStr string) (*domain.FaultRecordOperation, []byte, error) {
  333. op, err := getFaultRecordOp(ctx, idStr)
  334. if err != nil {
  335. return nil, nil, err
  336. }
  337. if op.MinioObject == "" {
  338. return nil, nil, faultRecordInvalid("该操作没有已保存的录波文件")
  339. }
  340. data, err := minioRepo.ReadFaultRecordFile(ctx, op.MinioObject)
  341. if err != nil {
  342. return nil, nil, err
  343. }
  344. return op, data, nil
  345. }
  346. /************** 视图转换 **************/
  347. func toFaultRecordSummary(op *domain.FaultRecordOperation) dto.FaultRecordOpSummary {
  348. s := dto.FaultRecordOpSummary{
  349. ID: op.ID.Hex(),
  350. Env: op.Env,
  351. GatewaySn: op.GatewaySn,
  352. DeviceType: op.DeviceType,
  353. DeviceSn: op.DeviceSn,
  354. Kind: string(op.Kind),
  355. Summary: faultRecordRequestSummary(op),
  356. Operator: op.Operator,
  357. Status: string(op.Status),
  358. TimeoutStage: string(op.TimeoutStage),
  359. Error: op.Error,
  360. StageTimes: op.StageTimes,
  361. FileName: op.FileName,
  362. FaultRecordID: op.FaultRecordID,
  363. CanCancel: !op.Status.Terminal() && !op.CancelReq,
  364. CanRetryDownload: op.CanRetryDownload(),
  365. HasFile: op.MinioObject != "",
  366. CreatedAt: op.CreatedAt,
  367. UpdatedAt: op.UpdatedAt,
  368. }
  369. return s
  370. }
  371. func joinInts(list []int) string {
  372. parts := make([]string, len(list))
  373. for i, v := range list {
  374. parts[i] = fmt.Sprint(v)
  375. }
  376. return strings.Join(parts, ",")
  377. }
  378. func faultRecordRequestSummary(op *domain.FaultRecordOperation) string {
  379. r := op.Request
  380. switch op.Kind {
  381. case domain.FaultRecordOpDeviceInfo:
  382. return "code=" + joinInts(r.CodeList)
  383. case domain.FaultRecordOpRecordFetch:
  384. return "记录序号 " + joinInts(r.Indexes())
  385. case domain.FaultRecordOpParamRead:
  386. return "功能码 " + joinInts(r.IdList)
  387. case domain.FaultRecordOpParamWrite:
  388. parts := make([]string, len(r.ParaList))
  389. for i, p := range r.ParaList {
  390. parts[i] = fmt.Sprintf("%d=%s", p.Id, p.SetVal)
  391. }
  392. return strings.Join(parts, "; ")
  393. }
  394. return ""
  395. }
  396. func faultRecordResultItems(op *domain.FaultRecordOperation, info *proto.ReplyInfo) []dto.FaultRecordResultItem {
  397. items := []dto.FaultRecordResultItem{}
  398. result := func(it *dto.FaultRecordResultItem, resultOne string) {
  399. it.ResultOne = resultOne
  400. it.ResultDesc = proto.DescribeResultOne(resultOne)
  401. it.Ok = resultOne == proto.ResultOk
  402. }
  403. switch op.Kind {
  404. case domain.FaultRecordOpDeviceInfo:
  405. for _, h := range info.HexList {
  406. it := dto.FaultRecordResultItem{Code: h.Code, Name: proto.RealtimeCodeNames[h.Code], Raw: h.HexData}
  407. result(&it, h.ResultOne)
  408. if it.Ok && h.HexData != "" {
  409. if v, err := faultRecordFile.ParseRealtime(h.Code, h.HexData); err != nil {
  410. it.DecodeErr = err.Error()
  411. } else {
  412. it.Decoded = v
  413. }
  414. }
  415. items = append(items, it)
  416. }
  417. case domain.FaultRecordOpRecordFetch:
  418. // 每条记录展开为 4(故障数据)、5(波形数据)两项,按请求顺序对应(待实测确认)
  419. indexes := op.Request.Indexes()
  420. for i, x := range info.IndexInfo {
  421. name := map[int]string{4: "故障数据", 5: "波形数据"}[x.Index]
  422. if len(indexes) > 1 && len(info.IndexInfo) == 2*len(indexes) {
  423. name = fmt.Sprintf("记录 %d %s", indexes[i/2], name)
  424. }
  425. it := dto.FaultRecordResultItem{Code: x.Index, Name: name}
  426. result(&it, x.ResultOne)
  427. items = append(items, it)
  428. }
  429. case domain.FaultRecordOpParamRead:
  430. for _, x := range info.IdInfo {
  431. it := dto.FaultRecordResultItem{Id: x.Id, Name: proto.FunctionCodeNames[x.Id], Raw: x.IdData}
  432. result(&it, x.ResultOne)
  433. if it.Ok {
  434. it.Decoded = proto.DecodeIdData(x.Id, x.IdData)
  435. }
  436. items = append(items, it)
  437. }
  438. case domain.FaultRecordOpParamWrite:
  439. // 同一功能码可重复(如多个 id=6),按请求顺序对应(待实测确认)
  440. for i, x := range info.SetResultList {
  441. it := dto.FaultRecordResultItem{Id: x.Id, Name: proto.FunctionCodeNames[x.Id]}
  442. if i < len(op.Request.ParaList) && op.Request.ParaList[i].Id == x.Id {
  443. it.SetVal = op.Request.ParaList[i].SetVal
  444. }
  445. result(&it, x.ResultOne)
  446. items = append(items, it)
  447. }
  448. }
  449. return items
  450. }