faultRecordServices.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  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. return createFaultRecordOperation(ctx, req, operator, true)
  140. }
  141. // SubmitFaultRecordOperation 供不连接 MQTT 的独立进程(fwh-agent-mcp)提交操作:只校验参数、保存并入队,
  142. // 由主服务的执行器收发 MQTT;主服务 MQTT 不可用时执行器把操作记为 error
  143. func SubmitFaultRecordOperation(ctx context.Context, req dto.FaultRecordCreateOpReq, operator string) (string, error) {
  144. return createFaultRecordOperation(ctx, req, operator, false)
  145. }
  146. func createFaultRecordOperation(ctx context.Context, req dto.FaultRecordCreateOpReq, operator string, localMQTT bool) (string, error) {
  147. env, err := fwh_mqtt.ParseEnv(req.Env)
  148. if err != nil {
  149. return "", faultRecordInvalid("%v", err)
  150. }
  151. if localMQTT {
  152. if _, err := fwh_mqtt.GetClient(env); err != nil {
  153. return "", fmt.Errorf("%s 环境 MQTT 不可用:%w", env, err)
  154. }
  155. }
  156. gatewaySn := strings.TrimSpace(req.GatewaySn)
  157. if gatewaySn == "" {
  158. return "", faultRecordInvalid("gatewaySn is empty")
  159. }
  160. deviceType, err := proto.NormalizeDeviceType(req.DeviceType)
  161. if err != nil {
  162. return "", faultRecordInvalid("%v", err)
  163. }
  164. deviceSn := strings.TrimSpace(req.DeviceSn)
  165. if deviceType == proto.DeviceTypeAGate && deviceSn == "" {
  166. deviceSn = gatewaySn
  167. }
  168. kind := domain.FaultRecordOpKind(req.Kind)
  169. op := &domain.FaultRecordOperation{
  170. Env: string(env),
  171. GatewaySn: gatewaySn,
  172. DeviceType: deviceType,
  173. DeviceSn: deviceSn,
  174. Kind: kind,
  175. Cmd: kind.Cmd(),
  176. Operator: operator,
  177. Status: domain.FaultRecordStatusPending,
  178. Request: domain.FaultRecordOpRequest{
  179. CodeList: req.CodeList,
  180. RecordIndexes: recordIndexesOf(req),
  181. IdList: req.IdList,
  182. ParaList: req.ParaList,
  183. },
  184. }
  185. if op.Cmd < 0 {
  186. return "", faultRecordInvalid("unknown kind %s", req.Kind)
  187. }
  188. // 与执行器使用同一构造函数校验参数,尽早返回错误
  189. if _, err := faultRecordQueue.BuildRequest(op); err != nil {
  190. return "", faultRecordInvalid("%v", err)
  191. }
  192. if err := mongoRepo.InsertFaultRecordOp(ctx, op); err != nil {
  193. return "", err
  194. }
  195. if err := faultRecordQueue.Enqueue(op.ID); err != nil {
  196. _, _ = mongoRepo.UpdateFaultRecordOpStatus(ctx, op.ID, domain.FaultRecordStatusError, map[string]any{"error": "提交任务失败:" + err.Error()})
  197. return "", err
  198. }
  199. return op.ID.Hex(), nil
  200. }
  201. // CancelFaultRecordOperation 请求取消;执行器在 1 秒内响应
  202. func CancelFaultRecordOperation(ctx context.Context, idStr string) error {
  203. id, err := primitive.ObjectIDFromHex(idStr)
  204. if err != nil {
  205. return faultRecordInvalid("invalid id")
  206. }
  207. ok, err := mongoRepo.RequestFaultRecordOpCancel(ctx, id)
  208. if err != nil {
  209. return err
  210. }
  211. if !ok {
  212. return faultRecordInvalid("操作已结束,无法取消")
  213. }
  214. return nil
  215. }
  216. // RetryFaultRecordDownload 上传已完成但下载失败时,手动重新下载(按 15 秒间隔重试至总超时)
  217. func RetryFaultRecordDownload(ctx context.Context, idStr, operator string) error {
  218. op, err := getFaultRecordOp(ctx, idStr)
  219. if err != nil {
  220. return err
  221. }
  222. if !op.CanRetryDownload() {
  223. return faultRecordInvalid("当前状态不能重试下载(需上传已完成且下载失败或超时)")
  224. }
  225. ok, err := mongoRepo.ReopenFaultRecordOpForDownloadRetry(ctx, op.ID)
  226. if err != nil {
  227. return err
  228. }
  229. if !ok {
  230. return faultRecordInvalid("操作状态已变化,请刷新后再试")
  231. }
  232. // 在原始报文中记录本次手动重试(先于提交任务,保证执行器读到的报文条数包含它)
  233. attempt := op.DownloadRetryCount + 1
  234. raw, _ := json.Marshal(map[string]any{
  235. "action": "retry_download",
  236. "operator": operator,
  237. "attempt": attempt,
  238. "prevStatus": op.Status,
  239. "prevError": op.Error,
  240. })
  241. if err := mongoRepo.AppendFaultRecordOpMessage(ctx, op.ID, domain.FaultRecordOpMessage{
  242. Direction: "action", Topic: "手动重试下载", At: time.Now(), Raw: string(raw),
  243. }); err != nil {
  244. zap.L().Warn("fault record append retry action failed", zap.String("id", op.ID.Hex()), zap.Error(err))
  245. }
  246. if err := faultRecordQueue.EnqueueDownloadRetry(op.ID, attempt); err != nil {
  247. _, _ = mongoRepo.UpdateFaultRecordOpStatus(ctx, op.ID, domain.FaultRecordStatusDownloadFailed, map[string]any{"error": "提交重试下载失败:" + err.Error()})
  248. return err
  249. }
  250. return nil
  251. }
  252. /************** 查询 **************/
  253. func ListFaultRecordOperations(ctx context.Context, f mongoRepo.FaultRecordOpFilter, page, size int64) (*dto.ResPage[dto.FaultRecordOpSummary], error) {
  254. list, total, err := mongoRepo.ListFaultRecordOps(ctx, f, page, size)
  255. if err != nil {
  256. return nil, err
  257. }
  258. out := &dto.ResPage[dto.FaultRecordOpSummary]{
  259. Data: make([]dto.FaultRecordOpSummary, 0, len(list)),
  260. PageNum: uint(page),
  261. PageSize: uint(size),
  262. Total: uint(total),
  263. }
  264. for i := range list {
  265. out.Data = append(out.Data, toFaultRecordSummary(&list[i]))
  266. }
  267. return out, nil
  268. }
  269. func GetFaultRecordOperation(ctx context.Context, idStr string) (*dto.FaultRecordOpDetail, error) {
  270. op, err := getFaultRecordOp(ctx, idStr)
  271. if err != nil {
  272. return nil, err
  273. }
  274. d := &dto.FaultRecordOpDetail{
  275. FaultRecordOpSummary: toFaultRecordSummary(op),
  276. Request: op.Request,
  277. Items: []dto.FaultRecordResultItem{},
  278. Messages: make([]dto.FaultRecordOpMessageView, 0, len(op.Messages)),
  279. }
  280. for _, m := range op.Messages {
  281. d.Messages = append(d.Messages, dto.FaultRecordOpMessageView(m))
  282. }
  283. if op.Reply != nil {
  284. d.ReplyResult = op.Reply.Result
  285. d.ReplyDesc = proto.DescribeReplyResult(op.Reply.Result)
  286. if info, ok := op.Reply.FindInfo(op.DeviceType, op.DeviceSn); ok {
  287. d.Items = faultRecordResultItems(op, info)
  288. }
  289. }
  290. return d, nil
  291. }
  292. // GetFaultRecordRecord 读取并解析已保存的录波文件
  293. func GetFaultRecordRecord(ctx context.Context, idStr string) (*dto.FaultRecordRecordView, error) {
  294. op, data, err := readFaultRecordFile(ctx, idStr)
  295. if err != nil {
  296. return nil, err
  297. }
  298. records, err := faultRecordFile.ParseArchive(data, op.DeviceType)
  299. if err != nil {
  300. return nil, fmt.Errorf("解析录波文件失败:%w", err)
  301. }
  302. return &dto.FaultRecordRecordView{OperationID: op.ID.Hex(), Records: records}, nil
  303. }
  304. // ExportFaultRecordFile 导出原始压缩包或 CSV(zip),返回文件名和内容
  305. func ExportFaultRecordFile(ctx context.Context, idStr, format string) (string, []byte, error) {
  306. op, data, err := readFaultRecordFile(ctx, idStr)
  307. if err != nil {
  308. return "", nil, err
  309. }
  310. switch format {
  311. case "", "raw":
  312. return proto.UploadFileName(op.FileName), data, nil
  313. case "csv":
  314. records, err := faultRecordFile.ParseArchive(data, op.DeviceType)
  315. if err != nil {
  316. return "", nil, fmt.Errorf("解析录波文件失败:%w", err)
  317. }
  318. zipData, err := faultRecordFile.BuildCSVZip(records)
  319. if err != nil {
  320. return "", nil, err
  321. }
  322. return proto.BatchFromUploadFileName(op.FileName) + "_csv.zip", zipData, nil
  323. }
  324. return "", nil, faultRecordInvalid("unknown format %s", format)
  325. }
  326. func recordIndexesOf(req dto.FaultRecordCreateOpReq) []int {
  327. if len(req.RecordIndexes) > 0 {
  328. return req.RecordIndexes
  329. }
  330. if req.RecordIndex > 0 {
  331. return []int{req.RecordIndex}
  332. }
  333. return nil
  334. }
  335. func getFaultRecordOp(ctx context.Context, idStr string) (*domain.FaultRecordOperation, error) {
  336. id, err := primitive.ObjectIDFromHex(idStr)
  337. if err != nil {
  338. return nil, faultRecordInvalid("invalid id")
  339. }
  340. return mongoRepo.GetFaultRecordOp(ctx, id)
  341. }
  342. func readFaultRecordFile(ctx context.Context, idStr string) (*domain.FaultRecordOperation, []byte, error) {
  343. op, err := getFaultRecordOp(ctx, idStr)
  344. if err != nil {
  345. return nil, nil, err
  346. }
  347. if op.MinioObject == "" {
  348. return nil, nil, faultRecordInvalid("该操作没有已保存的录波文件")
  349. }
  350. data, err := minioRepo.ReadFaultRecordFile(ctx, op.MinioObject)
  351. if err != nil {
  352. return nil, nil, err
  353. }
  354. return op, data, nil
  355. }
  356. /************** 视图转换 **************/
  357. func toFaultRecordSummary(op *domain.FaultRecordOperation) dto.FaultRecordOpSummary {
  358. s := dto.FaultRecordOpSummary{
  359. ID: op.ID.Hex(),
  360. Env: op.Env,
  361. GatewaySn: op.GatewaySn,
  362. DeviceType: op.DeviceType,
  363. DeviceSn: op.DeviceSn,
  364. Kind: string(op.Kind),
  365. Summary: faultRecordRequestSummary(op),
  366. Operator: op.Operator,
  367. Status: string(op.Status),
  368. TimeoutStage: string(op.TimeoutStage),
  369. Error: op.Error,
  370. StageTimes: op.StageTimes,
  371. FileName: op.FileName,
  372. FaultRecordID: op.FaultRecordID,
  373. CanCancel: !op.Status.Terminal() && !op.CancelReq,
  374. CanRetryDownload: op.CanRetryDownload(),
  375. HasFile: op.MinioObject != "",
  376. CreatedAt: op.CreatedAt,
  377. UpdatedAt: op.UpdatedAt,
  378. }
  379. return s
  380. }
  381. func joinInts(list []int) string {
  382. parts := make([]string, len(list))
  383. for i, v := range list {
  384. parts[i] = fmt.Sprint(v)
  385. }
  386. return strings.Join(parts, ",")
  387. }
  388. func faultRecordRequestSummary(op *domain.FaultRecordOperation) string {
  389. r := op.Request
  390. switch op.Kind {
  391. case domain.FaultRecordOpDeviceInfo:
  392. return "code=" + joinInts(r.CodeList)
  393. case domain.FaultRecordOpRecordFetch:
  394. return "记录序号 " + joinInts(r.Indexes())
  395. case domain.FaultRecordOpParamRead:
  396. return "功能码 " + joinInts(r.IdList)
  397. case domain.FaultRecordOpParamWrite:
  398. parts := make([]string, len(r.ParaList))
  399. for i, p := range r.ParaList {
  400. parts[i] = fmt.Sprintf("%d=%s", p.Id, p.SetVal)
  401. }
  402. return strings.Join(parts, "; ")
  403. }
  404. return ""
  405. }
  406. func faultRecordResultItems(op *domain.FaultRecordOperation, info *proto.ReplyInfo) []dto.FaultRecordResultItem {
  407. items := []dto.FaultRecordResultItem{}
  408. result := func(it *dto.FaultRecordResultItem, resultOne string) {
  409. it.ResultOne = resultOne
  410. it.ResultDesc = proto.DescribeResultOne(resultOne)
  411. it.Ok = resultOne == proto.ResultOk
  412. }
  413. switch op.Kind {
  414. case domain.FaultRecordOpDeviceInfo:
  415. for _, h := range info.HexList {
  416. it := dto.FaultRecordResultItem{Code: h.Code, Name: proto.RealtimeCodeNames[h.Code], Raw: h.HexData}
  417. result(&it, h.ResultOne)
  418. if it.Ok && h.HexData != "" {
  419. if v, err := faultRecordFile.ParseRealtime(h.Code, h.HexData); err != nil {
  420. it.DecodeErr = err.Error()
  421. } else {
  422. it.Decoded = v
  423. }
  424. }
  425. items = append(items, it)
  426. }
  427. case domain.FaultRecordOpRecordFetch:
  428. // 每条记录展开为 4(故障数据)、5(波形数据)两项,按请求顺序对应(待实测确认)
  429. indexes := op.Request.Indexes()
  430. for i, x := range info.IndexInfo {
  431. name := map[int]string{4: "故障数据", 5: "波形数据"}[x.Index]
  432. if len(indexes) > 1 && len(info.IndexInfo) == 2*len(indexes) {
  433. name = fmt.Sprintf("记录 %d %s", indexes[i/2], name)
  434. }
  435. it := dto.FaultRecordResultItem{Code: x.Index, Name: name}
  436. result(&it, x.ResultOne)
  437. items = append(items, it)
  438. }
  439. case domain.FaultRecordOpParamRead:
  440. for _, x := range info.IdInfo {
  441. it := dto.FaultRecordResultItem{Id: x.Id, Name: proto.FunctionCodeNames[x.Id], Raw: x.IdData}
  442. result(&it, x.ResultOne)
  443. if it.Ok {
  444. it.Decoded = proto.DecodeIdData(x.Id, x.IdData)
  445. }
  446. items = append(items, it)
  447. }
  448. case domain.FaultRecordOpParamWrite:
  449. // 同一功能码可重复(如多个 id=6),按请求顺序对应(待实测确认)
  450. for i, x := range info.SetResultList {
  451. it := dto.FaultRecordResultItem{Id: x.Id, Name: proto.FunctionCodeNames[x.Id]}
  452. if i < len(op.Request.ParaList) && op.Request.ParaList[i].Id == x.Id {
  453. it.SetVal = op.Request.ParaList[i].SetVal
  454. }
  455. result(&it, x.ResultOne)
  456. items = append(items, it)
  457. }
  458. }
  459. return items
  460. }