From 7da5bd1112d1fa423cec28e1147dc7bafb4765e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=B6=9B?= Date: Tue, 13 Jun 2023 21:01:14 +0800 Subject: [PATCH] =?UTF-8?q?enhance(meter):=E5=9F=BA=E6=9C=AC=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E8=A1=A8=E8=AE=A1=E6=8A=84=E8=A1=A8=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E8=A7=A3=E6=9E=90=EF=BC=8C=E5=BE=85=E6=B5=8B?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/meter.go | 68 +++++++++++++++++-------- service/meter.go | 117 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 150 insertions(+), 35 deletions(-) diff --git a/controller/meter.go b/controller/meter.go index 7f3c368..28f0e57 100644 --- a/controller/meter.go +++ b/controller/meter.go @@ -37,9 +37,10 @@ func InitializeMeterHandlers(router *fiber.App) { router.Get("/meter/choice", security.EnterpriseAuthorize, listUnboundMeters) router.Get("/meter/choice/tenement", security.EnterpriseAuthorize, listUnboundTenementMeters) router.Get("/reading/:pid", security.EnterpriseAuthorize, queryMeterReadings) - router.Post("/reading/:pid/:code", security.EnterpriseAuthorize, recordMeterReading) router.Put("/reading/:pid/:code/:reading", security.EnterpriseAuthorize, updateMeterReading) router.Get("/reading/:pid/template", security.EnterpriseAuthorize, downloadMeterReadingsTemplate) + router.Post("/reading/:pid/batch", security.EnterpriseAuthorize, uploadMeterReadings) + router.Post("/reading/:pid/:code", security.EnterpriseAuthorize, recordMeterReading) } // 查询指定园区下的表计信息 @@ -202,10 +203,10 @@ func uploadMeterArchive(c *fiber.Ctx) error { ok, err := repository.ParkRepository.IsParkBelongs(parkId, session.Uid) switch { case err != nil: - parkLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) + meterLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) return result.Error(http.StatusInternalServerError, err.Error()) case err == nil && !ok: - parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + meterLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) return result.Forbidden("您无权访问该园区。") } uploadFile, err := c.FormFile("data") @@ -255,10 +256,10 @@ func listAssociatedMeters(c *fiber.Ctx) error { ok, err := repository.ParkRepository.IsParkBelongs(parkId, session.Uid) switch { case err != nil: - parkLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) + meterLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) return result.Error(http.StatusInternalServerError, err.Error()) case err == nil && !ok: - parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + meterLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) return result.Forbidden("您无权访问该园区。") } meterId := c.Params("code") @@ -283,10 +284,10 @@ func bindAssociatedMeters(c *fiber.Ctx) error { ok, err := repository.ParkRepository.IsParkBelongs(parkId, session.Uid) switch { case err != nil: - parkLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) + meterLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) return result.Error(http.StatusInternalServerError, err.Error()) case err == nil && !ok: - parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + meterLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) return result.Forbidden("您无权访问该园区。") } meterId := c.Params("code") @@ -320,10 +321,10 @@ func unbindAssociatedMeters(c *fiber.Ctx) error { ok, err := repository.ParkRepository.IsParkBelongs(parkId, session.Uid) switch { case err != nil: - parkLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) + meterLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) return result.Error(http.StatusInternalServerError, err.Error()) case err == nil && !ok: - parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + meterLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) return result.Forbidden("您无权访问该园区。") } masterMeter := c.Params("master") @@ -356,10 +357,10 @@ func listPooledMeters(c *fiber.Ctx) error { ok, err := repository.ParkRepository.IsParkBelongs(parkId, session.Uid) switch { case err != nil: - parkLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) + meterLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) return result.Error(http.StatusInternalServerError, err.Error()) case err == nil && !ok: - parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + meterLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) return result.Forbidden("您无权访问该园区。") } page := c.QueryInt("page", 1) @@ -388,10 +389,10 @@ func listUnboundMeters(c *fiber.Ctx) error { ok, err := repository.ParkRepository.IsParkBelongs(parkId, session.Uid) switch { case err != nil: - parkLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) + meterLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) return result.Error(http.StatusInternalServerError, err.Error()) case err == nil && !ok: - parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + meterLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) return result.Forbidden("您无权访问该园区。") } keyword := c.Query("keyword") @@ -452,10 +453,10 @@ func queryMeterReadings(c *fiber.Ctx) error { ok, err := repository.ParkRepository.IsParkBelongs(parkId, session.Uid) switch { case err != nil: - parkLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) + meterLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) return result.Error(http.StatusInternalServerError, err.Error()) case err == nil && !ok: - parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + meterLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) return result.Forbidden("您无权访问该园区。") } keyword := tools.EmptyToNil(c.Query("keyword")) @@ -506,10 +507,10 @@ func recordMeterReading(c *fiber.Ctx) error { ok, err := repository.ParkRepository.IsParkBelongs(parkId, session.Uid) switch { case err != nil: - parkLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) + meterLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) return result.Error(http.StatusInternalServerError, err.Error()) case err == nil && !ok: - parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + meterLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) return result.Forbidden("您无权访问该园区。") } meterCode := c.Params("code") @@ -542,10 +543,10 @@ func updateMeterReading(c *fiber.Ctx) error { ok, err := repository.ParkRepository.IsParkBelongs(parkId, session.Uid) switch { case err != nil: - parkLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) + meterLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) return result.Error(http.StatusInternalServerError, err.Error()) case err == nil && !ok: - parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + meterLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) return result.Forbidden("您无权访问该园区。") } meterCode := c.Params("code") @@ -616,5 +617,32 @@ func downloadMeterReadingsTemplate(c *fiber.Ctx) error { // 处理上传的抄表记录文件 func uploadMeterReadings(c *fiber.Ctx) error { - return nil + parkId := c.Params("pid") + meterLog.Info("从Excel文件中导入抄表档案", zap.String("park id", parkId)) + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("无法从Excel文件中导入抄表档案,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + ok, err := repository.ParkRepository.IsParkBelongs(parkId, session.Uid) + switch { + case err != nil: + meterLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + case err == nil && !ok: + meterLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + return result.Forbidden("您无权访问该园区。") + } + uploadFile, err := c.FormFile("data") + if err != nil { + meterLog.Error("无法从Excel文件中导入抄表档案,无法获取上传的文件", zap.Error(err)) + return result.NotAccept(fmt.Sprintf("没有接收到上传的文件,%s", err.Error())) + } + errs, err := service.MeterService.BatchImportReadings(parkId, uploadFile) + if err != nil { + meterLog.Error("无法从Excel文件中导入抄表档案,无法导入抄表档案", zap.Error(err)) + return result.Json(fiber.StatusNotAcceptable, "上传的抄表档案存在错误。", fiber.Map{"errors": errs}) + } + return result.Success("表计档案已经导入完成。", fiber.Map{"errors": errs}) } diff --git a/service/meter.go b/service/meter.go index b08515d..1eb32ec 100644 --- a/service/meter.go +++ b/service/meter.go @@ -686,28 +686,115 @@ func (ms _MeterService) RecordReading(pid, meterCode string, form *vo.MeterReadi return nil } -// 获取指定园区的全部待抄表计列表,并将其输出到Excel文件模板中,提供生成文件的二进制内容 -func (ms _MeterService) GenerateParkMeterReadingTemplate(pid string, meters []*model.SimpleMeterDocument) ([]byte, error) { - // 步骤1:复制公用模板文件到临时文件夹,文件以园区ID命名 - - // 步骤2:打开复制后的园区文件 - // 步骤3:获取园区中需要抄表的表计列表 - // 步骤4:循环表计列表,将表计信息写入到Excel文件中 - // 步骤5:将生成的临时文件的具体二进制内容返回给调用者 - return nil, nil -} - // 处理上传的Excel格式的表计抄表记录,所有满足审查条件的记录都将被保存到数据库中。 // 无论峰谷表计还是普通表计,只要抄表记录中不存在峰谷数据,都将自动使用平段配平。 -func (ms _MeterService) BatchImportReadings(pid string, uploadContent []byte) error { +func (ms _MeterService) BatchImportReadings(pid string, file *multipart.FileHeader) ([]excel.ExcelAnalysisError, error) { + ms.log.Info("处理上传的Excel格式的表计抄表记录", zap.String("park id", pid)) + ctx, cancel := global.TimeoutContext() + defer cancel() // 步骤1:将解析到的数据转换成创建表单数据 + activeFile, err := file.Open() + if err != nil { + ms.log.Error("无法打开上传的抄表数据文件。", zap.Error(err)) + return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法打开上传的抄表数据文件,%w", err) + } + analyzer, err := excel.NewMeterReadingsExcelAnalyzer(activeFile) + if err != nil { + ms.log.Error("无法根据上传的 Excel 文件创建表计抄表数据解析器。", zap.Error(err)) + return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法根据上传的 Excel 文件创建表计抄表数据解析器,%w", err) + } + records, errs := analyzer.Analysis(*new(model.ReadingImportRow)) + if len(errs) > 0 { + ms.log.Error("表计抄表数据解析器在解析上传的 Excel 文件时发生错误。", zap.Int("error count", len(errs))) + return errs, fmt.Errorf("表计抄表数据解析器在解析上传的 Excel 文件时发生错误。") + } + ms.log.Debug("已经解析到的上传数据", zap.Any("records", records)) // 步骤2:对目前已经解析到的数据进行合法性检测,检测包括表计编号在同一抄表时间是否重复 + var collectRecords = make(map[types.DateTime][]string, 0) + for _, record := range records { + if _, ok := collectRecords[record.ReadAt]; !ok { + collectRecords[record.ReadAt] = []string{} + } + collectRecords[record.ReadAt] = append(collectRecords[record.ReadAt], record.Code) + } + for readAt, codes := range collectRecords { + valCounts := lo.CountValues(codes) + for code, count := range valCounts { + if count > 1 { + errs = append(errs, excel.ExcelAnalysisError{ + Row: 0, + Col: 0, + Err: excel.AnalysisError{ + Err: fmt.Errorf("表计编号 %s 在同一抄表时间 %s 内重复出现 %d 次", code, readAt.ToString(), count), + }, + }) + } + } + } + if len(errs) > 0 { + ms.log.Error("表计抄表数据解析器在解析上传的 Excel 文件时发生错误。", zap.Int("error count", len(errs))) + return errs, fmt.Errorf("表计抄表数据解析器在解析上传的 Excel 文件时发生错误。") + } // 步骤3:从数据库中获取当前园区中已有的表计编号 + meters, err := repository.MeterRepository.AllMeters(pid) + if err != nil { + ms.log.Error("无法从数据库中获取当前园区中已有的表计编号。", zap.Error(err)) + return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法从数据库中获取当前园区中已有的表计编号,%w", err) + } // 步骤4.0:启动数据库事务 + tx, err := global.DB.Begin(ctx) + if err != nil { + ms.log.Error("无法启动数据库事务。", zap.Error(err)) + return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法启动数据库事务,%w", err) + } // 步骤4.1:对比检查数据库中的表计编号与上传文件中的表计编号是否存在差异。非差异内容将直接保存 - // 步骤4.1.1:抄表的表计在数据库中已经存在,可以直接保存起数据。 - // 步骤4.1.2:抄表表计在数据库中不存在,需要将其记录进入错误。 + for row, record := range records { + meter, exists := lo.Find(meters, func(element *model.MeterDetail) bool { + return element.Code == record.Code + }) + if exists { + // 步骤4.1.1:抄表的表计在数据库中已经存在,可以直接保存起数据。 + _, err := repository.MeterRepository.RecordReading(tx, ctx, pid, record.Code, meter.MeterType, meter.Ratio, &vo.MeterReadingForm{ + ReadAt: lo.ToPtr(record.ReadAt), + Overall: record.Overall, + Critical: record.Critical.Decimal, + Peak: record.Peak.Decimal, + Flat: record.Overall.Sub(record.Peak.Decimal).Sub(record.Valley.Decimal).Sub(record.Critical.Decimal), + Valley: record.Valley.Decimal, + }) + if err != nil { + ms.log.Error("无法在数据插入阶段保存抄表信息。", zap.String("meter code", record.Code), zap.Error(err)) + errs = append(errs, excel.ExcelAnalysisError{ + Row: row + 1, + Col: 0, + Err: excel.AnalysisError{ + Err: fmt.Errorf("无法在数据插入阶段保存抄表信息,%w", err), + }, + }) + } + } else { + // 步骤4.1.2:抄表表计在数据库中不存在,需要将其记录进入错误。 + errs = append(errs, excel.ExcelAnalysisError{ + Row: row + 1, + Col: 0, + Err: excel.AnalysisError{ + Err: fmt.Errorf("表计编号 %s 在系统中不存在", record.Code), + }, + }) + } + } // 步骤4.3:如果批处理过程中存在错误,撤销全部导入动作。 + if len(errs) > 0 { + ms.log.Error("表计抄表数据解析器在解析上传的 Excel 文件时发生错误。", zap.Int("error count", len(errs))) + tx.Rollback(ctx) + return errs, fmt.Errorf("表计抄表数据解析器在解析上传的 Excel 文件时发生错误。") + } // 步骤5:执行事务,更新数据库,获取完成更改的行数。 - return nil + err = tx.Commit(ctx) + if err != nil { + ms.log.Error("无法在数据插入阶段提交数据库事务。", zap.Error(err)) + tx.Rollback(ctx) + return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法在数据插入阶段提交数据库事务,%w", err) + } + return make([]excel.ExcelAnalysisError, 0), nil }