From 2339e4c7257ff7643bd7da7267851256f487f693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=B6=9B?= Date: Sun, 11 Jun 2023 22:31:32 +0800 Subject: [PATCH] =?UTF-8?q?enhance(meter):=E5=AE=8C=E6=88=90=E5=A4=A7?= =?UTF-8?q?=E9=83=A8=E5=88=86=E8=A1=A8=E8=AE=A1=E7=9B=B8=E5=85=B3=E7=9A=84?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/meter.go | 381 +++++++++++++++++++++++++++++++++++++++++++- model/meter.go | 39 +++-- model/reading.go | 4 +- repository/meter.go | 57 +++++-- repository/park.go | 32 ++++ service/meter.go | 144 ++++++++++++++++- tools/utils.go | 8 + types/datetime.go | 6 + vo/meter.go | 6 + vo/reading.go | 59 +++++++ 10 files changed, 700 insertions(+), 36 deletions(-) diff --git a/controller/meter.go b/controller/meter.go index 886e9c5..f627863 100644 --- a/controller/meter.go +++ b/controller/meter.go @@ -2,15 +2,20 @@ package controller import ( "electricity_bill_calc/logger" + "electricity_bill_calc/model" "electricity_bill_calc/repository" "electricity_bill_calc/response" "electricity_bill_calc/security" "electricity_bill_calc/service" + "electricity_bill_calc/tools" + "electricity_bill_calc/types" "electricity_bill_calc/vo" "fmt" "net/http" "github.com/gofiber/fiber/v2" + "github.com/jinzhu/copier" + "github.com/samber/lo" "go.uber.org/zap" ) @@ -20,8 +25,17 @@ func InitializeMeterHandlers(router *fiber.App) { router.Get("/meter/:pid", security.EnterpriseAuthorize, searchMetersWithinPark) router.Post("/meter/:pid", security.EnterpriseAuthorize, createNewMeterManually) router.Get("/meter/:pid/template", security.EnterpriseAuthorize, downloadMeterArchiveTemplate) + router.Post("/meter/:pid/batch", security.EnterpriseAuthorize, uploadMeterArchive) router.Get("/meter/:pid/:code", security.EnterpriseAuthorize, retrieveSpecificMeterDetail) router.Put("/meter/:pid/:code", security.EnterpriseAuthorize, updateMeterManually) + router.Patch("/meter/:pid/:code", security.EnterpriseAuthorize, replaceMeter) + router.Get("/meter/:pid/:code/binding", security.EnterpriseAuthorize, listAssociatedMeters) + router.Post("/meter/:pid/:code/binding", security.EnterpriseAuthorize, bindAssociatedMeters) + router.Delete("/meter/:pid/:code/binding/:slave", security.EnterpriseAuthorize, unbindAssociatedMeters) + router.Get("/meter/:pid/pooled", security.EnterpriseAuthorize, listPooledMeters) + router.Get("/meter/choice", security.EnterpriseAuthorize, listUnboundMeters) + router.Get("/meter/choice/tenement", security.EnterpriseAuthorize, listUnboundTenementMeters) + router.Get("/reading/:pid", security.EnterpriseAuthorize, queryMeterReadings) } // 查询指定园区下的表计信息 @@ -154,7 +168,33 @@ func downloadMeterArchiveTemplate(c *fiber.Ctx) error { // 从Excel文件中导入表计档案 func uploadMeterArchive(c *fiber.Ctx) error { - return nil + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("无法从Excel文件中导入表计档案,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + parkId := c.Params("pid") + 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)) + return result.Error(http.StatusInternalServerError, err.Error()) + case err == nil && !ok: + parkLog.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.BatchImportMeters(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}) } // 更换系统中的表计 @@ -178,3 +218,342 @@ func replaceMeter(c *fiber.Ctx) error { } return nil } + +// 列出指定公摊表计下的所有关联表计 +func listAssociatedMeters(c *fiber.Ctx) error { + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("无法列出指定公摊表计下的所有关联表计,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + parkId := c.Params("pid") + 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)) + return result.Error(http.StatusInternalServerError, err.Error()) + case err == nil && !ok: + parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + return result.Forbidden("您无权访问该园区。") + } + meterId := c.Params("code") + meterLog.Info("列出指定公摊表计下的所有关联表计", zap.String("park id", parkId), zap.String("meter id", meterId)) + meters, err := service.MeterService.ListPooledMeterRelations(parkId, meterId) + if err != nil { + meterLog.Error("无法列出指定公摊表计下的所有关联表计,无法获取关联表计列表", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + return result.Success("已经取得指定公摊表计下的所有关联表计列表。", fiber.Map{"meters": meters}) +} + +// 向指定表计绑定关联表计 +func bindAssociatedMeters(c *fiber.Ctx) error { + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("无法向指定表计绑定关联表计,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + parkId := c.Params("pid") + 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)) + return result.Error(http.StatusInternalServerError, err.Error()) + case err == nil && !ok: + parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + return result.Forbidden("您无权访问该园区。") + } + meterId := c.Params("code") + meterLog.Info("向指定表计绑定关联表计", zap.String("park id", parkId), zap.String("meter id", meterId)) + var meters = make([]string, 0) + if err := c.BodyParser(&meters); err != nil { + meterLog.Error("无法向指定表计绑定关联表计,无法解析关联表计列表", zap.Error(err)) + return result.NotAccept(err.Error()) + } + ok, err = service.MeterService.BindMeter(parkId, meterId, meters) + if err != nil { + meterLog.Error("无法向指定表计绑定关联表计,无法绑定关联表计", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + if !ok { + meterLog.Warn("无法向指定表计绑定关联表计,表计关联失败。") + return result.NotAccept("表计关联失败。") + } + return result.Created("已经向指定表计绑定关联表计。") +} + +// 解除指定园区下两个表计之间的关联关系 +func unbindAssociatedMeters(c *fiber.Ctx) error { + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("无法解除指定园区下两个表计之间的关联关系,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + parkId := c.Params("pid") + 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)) + return result.Error(http.StatusInternalServerError, err.Error()) + case err == nil && !ok: + parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + return result.Forbidden("您无权访问该园区。") + } + masterMeter := c.Params("master") + slaveMeter := c.Params("slave") + if len(masterMeter) == 0 || len(slaveMeter) == 0 { + meterLog.Warn("无法解除指定园区下两个表计之间的关联关系,表计编号为空。") + return result.NotAccept("存在未给定要操作的表计编号。") + } + ok, err = service.MeterService.UnbindMeter(parkId, masterMeter, []string{slaveMeter}) + if err != nil { + meterLog.Error("无法解除指定园区下两个表计之间的关联关系,无法解除关联关系", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + if !ok { + meterLog.Warn("无法解除指定园区下两个表计之间的关联关系,表计关联解除失败。") + return result.NotAccept("表计关联解除失败。") + } + return result.Created("已经解除指定园区下两个表计之间的关联关系。") +} + +// 分页列出园区中的公摊表计 +func listPooledMeters(c *fiber.Ctx) error { + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("无法分页列出园区中的公摊表计,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + parkId := c.Params("pid") + 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)) + return result.Error(http.StatusInternalServerError, err.Error()) + case err == nil && !ok: + parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + return result.Forbidden("您无权访问该园区。") + } + page := c.QueryInt("page", 1) + keyword := c.Query("keyword") + meters, total, err := service.MeterService.SearchPooledMetersDetail(parkId, uint(page), &keyword) + if err != nil { + meterLog.Error("无法分页列出园区中的公摊表计,无法获取公摊表计列表", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + return result.Success( + "已经取得符合条件的公摊表计列表。", + response.NewPagedResponse(page, total).ToMap(), + fiber.Map{"meters": meters}, + ) +} + +// 列出指定园区中尚未绑定公摊表计的表计 +func listUnboundMeters(c *fiber.Ctx) error { + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("无法列出指定园区中尚未绑定公摊表计的表计,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + parkId := c.Params("pid") + 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)) + return result.Error(http.StatusInternalServerError, err.Error()) + case err == nil && !ok: + parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + return result.Forbidden("您无权访问该园区。") + } + keyword := c.Query("keyword") + limit := uint(c.QueryInt("limit", 6)) + meters, err := repository.MeterRepository.ListUnboundMeters(session.Uid, &parkId, &keyword, &limit) + if err != nil { + meterLog.Error("无法列出指定园区中尚未绑定公摊表计的表计,无法获取表计列表", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + var simplifiedMeters = make([]*vo.SimplifiedMeterQueryResponse, 0) + copier.Copy(&simplifiedMeters, &meters) + return result.Success("已经取得符合条件的表计列表。", fiber.Map{"meters": simplifiedMeters}) +} + +// 列出指定园区中尚未绑定商户的表计 +func listUnboundTenementMeters(c *fiber.Ctx) error { + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("无法列出指定园区中尚未绑定商户的表计,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + parkId := c.Query("park") + if len(parkId) == 0 { + meterLog.Error("无法列出指定园区中尚未绑定商户的表计,未指定要访问的园区ID") + return result.NotAccept("未指定要访问的园区。") + } + 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("您无权访问该园区。") + } + keyword := c.Query("keyword") + limit := uint(c.QueryInt("limit", 6)) + meters, err := repository.MeterRepository.ListUnboundTenementMeters(session.Uid, &parkId, &keyword, &limit) + if err != nil { + meterLog.Error("无法列出指定园区中尚未绑定商户的表计,无法获取表计列表", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + var simplifiedMeters = make([]*vo.SimplifiedMeterQueryResponse, 0) + copier.Copy(&simplifiedMeters, &meters) + return result.Success("已经取得符合条件的表计列表。", fiber.Map{"meters": simplifiedMeters}) +} + +// 查询指定园区中的表计读数 +func queryMeterReadings(c *fiber.Ctx) error { + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("查询指定园区中的表计读数,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + parkId := c.Params("pid") + 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)) + return result.Error(http.StatusInternalServerError, err.Error()) + case err == nil && !ok: + parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + return result.Forbidden("您无权访问该园区。") + } + keyword := tools.EmptyToNil(c.Query("keyword")) + page := c.QueryInt("page", 1) + building := tools.EmptyToNil(c.Query("building")) + start := c.Query("start_date") + var startDate *types.Date = nil + if len(start) > 0 { + if parsedDate, err := types.ParseDate(start); err != nil { + meterLog.Error("查询指定园区中的表计读数,无法解析开始日期", zap.Error(err)) + } else { + startDate = &parsedDate + } + } + end := c.Query("end_date") + var endDate *types.Date = nil + if len(end) > 0 { + if parsedDate, err := types.ParseDate(end); err != nil { + meterLog.Error("查询指定园区中的表计读数,无法解析结束日期", zap.Error(err)) + } else { + endDate = &parsedDate + } + } + readings, total, err := service.MeterService.SearchMeterReadings(parkId, building, startDate, endDate, uint(page), keyword) + if err != nil { + meterLog.Error("查询指定园区中的表计读数,无法获取表计读数列表", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + convertedReadings := lo.Map(readings, func(element *model.DetailedMeterReading, _ int) vo.MeterReadingDetailResponse { + return vo.FromDetailedMeterReading(*element) + }) + return result.Success( + "指定园区的表计读数已经列出。", + response.NewPagedResponse(page, total).ToMap(), + fiber.Map{"records": convertedReadings}, + ) +} + +// 记录一条新的表计抄表记录 +func recordMeterReading(c *fiber.Ctx) error { + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("记录一条新的表计抄表记录,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + parkId := c.Params("pid") + 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)) + return result.Error(http.StatusInternalServerError, err.Error()) + case err == nil && !ok: + parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + return result.Forbidden("您无权访问该园区。") + } + meterCode := c.Params("code") + var readingForm vo.MeterReadingForm + if err := c.BodyParser(&readingForm); err != nil { + meterLog.Error("记录一条新的表计抄表记录,无法解析表计抄表表单", zap.Error(err)) + return result.NotAccept(fmt.Sprintf("无法解析表计抄表表单,%s", err.Error())) + } + if !readingForm.Validate() { + meterLog.Warn("记录一条新的表计抄表记录,表计读数不能正常配平,尖、峰、谷电量和超过总电量。") + return result.NotAccept("表计读数不能正常配平,尖、峰、谷电量和超过总电量。") + } + err = service.MeterService.RecordReading(parkId, meterCode, &readingForm) + if err != nil { + meterLog.Error("记录一条新的表计抄表记录,无法记录表计抄表记录", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + return result.Created("表计抄表记录已经记录完成。") +} + +// 更新指定园区中指定表计的抄表记录 +func updateMeterReading(c *fiber.Ctx) error { + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("更新一条新的表计抄表记录,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + parkId := c.Params("pid") + 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)) + return result.Error(http.StatusInternalServerError, err.Error()) + case err == nil && !ok: + parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid)) + return result.Forbidden("您无权访问该园区。") + } + meterCode := c.Params("code") + readingAtMicro, err := c.ParamsInt("reading") + if err != nil { + meterLog.Error("更新一条新的表计抄表记录,无法解析抄表时间", zap.Error(err)) + return result.NotAccept(fmt.Sprintf("无法解析抄表时间,%s", err.Error())) + } + readingAt := types.FromUnixMicro(int64(readingAtMicro)) + var readingForm vo.MeterReadingForm + if err := c.BodyParser(&readingForm); err != nil { + meterLog.Error("更新一条新的表计抄表记录,无法解析表计抄表表单", zap.Error(err)) + return result.NotAccept(fmt.Sprintf("无法解析表计抄表表单,%s", err.Error())) + } + ok, err = repository.MeterRepository.UpdateMeterReading(parkId, meterCode, readingAt, &readingForm) + if err != nil { + meterLog.Error("更新一条新的表计抄表记录,无法更新表计抄表记录", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + if !ok { + meterLog.Warn("更新一条新的表计抄表记录,表计抄表更新失败。") + return result.NotAccept("表计抄表记录未能成功更新,可能指定抄表记录不存在。") + } + return result.Success("表计抄表记录已经更新完成。") +} + +// 下载指定园区的表计抄表模板 +func downlongMeterReadingTemplate(c *fiber.Ctx) error { + return nil +} + +// 处理上传的抄表记录文件 +func uploadMeterReadings(c *fiber.Ctx) error { + return nil +} diff --git a/model/meter.go b/model/meter.go index f38961f..e68b603 100644 --- a/model/meter.go +++ b/model/meter.go @@ -2,7 +2,6 @@ package model import ( "electricity_bill_calc/types" - "time" "github.com/shopspring/decimal" ) @@ -19,31 +18,31 @@ type MeterDetail struct { Ratio decimal.Decimal `json:"ratio" db:"ratio"` Seq int64 `json:"seq" db:"seq"` Enabled bool `json:"enabled" db:"enabled"` - AttachedAt *time.Time `json:"attachedAt" db:"attached_at"` - DetachedAt *time.Time `json:"detachedAt" db:"detached_at"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - LastModifiedAt time.Time `json:"lastModifiedAt" db:"last_modified_at"` + AttachedAt *types.DateTime `json:"attachedAt" db:"attached_at"` + DetachedAt *types.DateTime `json:"detachedAt" db:"detached_at"` + CreatedAt types.DateTime `json:"createdAt" db:"created_at"` + LastModifiedAt types.DateTime `json:"lastModifiedAt" db:"last_modified_at"` } type MeterRelation struct { - Id string `json:"id"` - Park string `json:"parkId" db:"park_id"` - MasterMeter string `json:"masterMeterId" db:"master_meter_id"` - SlaveMeter string `json:"slaveMeterId" db:"slave_meter_id"` - EstablishedAt time.Time `json:"establishedAt"` - SuspendedAt *time.Time `json:"suspendedAt"` - RevokeAt *time.Time `json:"revokeAt"` + Id string `json:"id"` + Park string `json:"parkId" db:"park_id"` + MasterMeter string `json:"masterMeterId" db:"master_meter_id"` + SlaveMeter string `json:"slaveMeterId" db:"slave_meter_id"` + EstablishedAt types.DateTime `json:"establishedAt"` + SuspendedAt *types.DateTime `json:"suspendedAt"` + RevokeAt *types.DateTime `json:"revokeAt"` } type MeterSynchronization struct { - Park string `json:"parkId" db:"park_id"` - Meter string `json:"meterId" db:"meter_id"` - ForeignMeter string `json:"foreignMeter"` - SystemType string `json:"systemType"` - SystemIdentity string `json:"systemIdentity"` - Enabled bool `json:"enabled"` - LastSynchronizedAt time.Time `json:"lastSynchronizedAt" db:"last_synchronized_at"` - RevokeAt *time.Time `json:"revokeAt" db:"revoke_at"` + Park string `json:"parkId" db:"park_id"` + Meter string `json:"meterId" db:"meter_id"` + ForeignMeter string `json:"foreignMeter"` + SystemType string `json:"systemType"` + SystemIdentity string `json:"systemIdentity"` + Enabled bool `json:"enabled"` + LastSynchronizedAt types.DateTime `json:"lastSynchronizedAt" db:"last_synchronized_at"` + RevokeAt *types.DateTime `json:"revokeAt" db:"revoke_at"` } type SimpleMeterDocument struct { diff --git a/model/reading.go b/model/reading.go index 03db697..8f28391 100644 --- a/model/reading.go +++ b/model/reading.go @@ -1,7 +1,7 @@ package model import ( - "time" + "electricity_bill_calc/types" "github.com/shopspring/decimal" ) @@ -38,7 +38,7 @@ func NewUnitaryReading(ratio, overall decimal.Decimal) *Reading { } type MeterReading struct { - ReadAt time.Time `json:"readAt"` + ReadAt types.DateTime `json:"readAt"` Park string `json:"parkId" db:"park_id"` Meter string `json:"meterId" db:"meter_id"` MeterType int16 `json:"meterType"` diff --git a/repository/meter.go b/repository/meter.go index d590d2a..f59700d 100644 --- a/repository/meter.go +++ b/repository/meter.go @@ -215,7 +215,7 @@ func (mr _MeterRepository) ListMetersByIDs(pid string, ids []string) ([]*model.M pid, strings.Join(ids, ","), } - if meters, err := cache.RetrieveSearch[[]*model.MeterDetail]("meter_slice", cacheConditions...); err == nil { + if meters, err := cache.RetrieveSearch[[]*model.MeterDetail]("meter_slice", cacheConditions...); err == nil && meters != nil { mr.log.Info("从缓存中获取到了指定园区中所需的表计信息", zap.Int("count", len(*meters))) return *meters, nil } @@ -231,7 +231,7 @@ func (mr _MeterRepository) ListMetersByIDs(pid string, ids []string) ([]*model.M ). Where( goqu.I("m.park_id").Eq(pid), - goqu.I("m.id").Eq(goqu.Func("any", ids)), + goqu.I("m.code").In(ids), ). Order(goqu.I("m.seq").Asc()). Prepared(true).ToSQL() @@ -311,6 +311,43 @@ func (mr _MeterRepository) CreateMeter(tx pgx.Tx, ctx context.Context, pid strin return ok.RowsAffected() > 0, nil } +// 创建或者更新一条表计的信息 +func (mr _MeterRepository) CreateOrUpdateMeter(tx pgx.Tx, ctx context.Context, pid string, meter vo.MeterCreationForm) (bool, error) { + mr.log.Info("创建或者更新一条表计的信息", zap.String("park id", pid), zap.String("meter code", meter.Code)) + timeNow := types.Now() + meterSql, meterArgs, _ := mr.ds. + Insert(goqu.T("meter_04kv")). + Cols( + "park_id", "code", "address", "ratio", "seq", "meter_type", "building", "on_floor", "area", "enabled", + "attached_at", "created_at", "last_modified_at", + ). + Vals( + goqu.Vals{pid, meter.Code, meter.Address, meter.Ratio, meter.Seq, meter.MeterType, meter.Building, meter.OnFloor, meter.Area, meter.Enabled, + timeNow, timeNow, timeNow, + }, + ). + OnConflict( + goqu.DoUpdate("meter_04kv_pkey", + goqu.Record{ + "address": goqu.I("excluded.address"), + "seq": goqu.I("excluded.seq"), + "ratio": goqu.I("excluded.ratio"), + "meter_type": goqu.I("excluded.meter_type"), + "building": goqu.I("excluded.building"), + "on_floor": goqu.I("excluded.on_floor"), + "area": goqu.I("excluded.area"), + "last_modified_at": goqu.I("excluded.last_modified_at"), + }), + ). + Prepared(true).ToSQL() + res, err := tx.Exec(ctx, meterSql, meterArgs...) + if err != nil { + mr.log.Error("创建或者更新表计信息失败", zap.Error(err)) + return false, err + } + return res.RowsAffected() > 0, nil +} + // 记录一条表计的抄表信息 func (mr _MeterRepository) RecordReading(tx pgx.Tx, ctx context.Context, pid, code string, meterType int16, ratio decimal.Decimal, reading *vo.MeterReadingForm) (bool, error) { mr.log.Info("记录一条表计的抄表信息", zap.String("park id", pid), zap.String("meter code", code)) @@ -848,19 +885,16 @@ func (mr _MeterRepository) ListUnboundTenementMeters(uid string, pid *string, ke // 查询指定园区中的符合条件的抄表记录 func (mr _MeterRepository) ListMeterReadings(pid string, keyword *string, page uint, start, end *types.Date, buidling *string) ([]*model.MeterReading, int64, error) { - mr.log.Info("查询指定园区中的符合条件的抄表记录", zap.String("park id", pid), zap.String("keyword", tools.DefaultTo(keyword, "")), zap.Uint("page", page), zap.Any("start", start), zap.Any("end", end), zap.String("building", tools.DefaultTo(buidling, ""))) + mr.log.Info("查询指定园区中的符合条件的抄表记录", zap.String("park id", pid), zap.String("keyword", tools.DefaultTo(keyword, "")), zap.Uint("page", page), logger.DateFieldp("start", start), logger.DateFieldp("end", end), zap.String("building", tools.DefaultTo(buidling, ""))) cacheConditions := []string{ pid, - tools.DefaultOrEmptyStr(keyword, "UNDEF"), + cache.NullableStringKey(keyword), fmt.Sprintf("%d", page), - tools.CondFn(func(val *types.Date) bool { - return val != nil - }, start, start.ToString(), "UNDEF"), - tools.CondFn(func(val *types.Date) bool { - return val != nil - }, end, end.ToString(), "UNDEF"), + cache.NullableConditionKey(start), + cache.NullableConditionKey(end), + cache.NullableStringKey(buidling), } - if readings, total, err := cache.RetrievePagedSearch[[]*model.MeterReading]("meter_reading", cacheConditions...); err == nil { + if readings, total, err := cache.RetrievePagedSearch[[]*model.MeterReading]("meter_reading", cacheConditions...); err == nil && readings != nil && total != -1 { mr.log.Info("从缓存中获取到了指定园区中的抄表记录", zap.Int("count", len(*readings)), zap.Int64("total", total)) return *readings, total, nil } @@ -870,6 +904,7 @@ func (mr _MeterRepository) ListMeterReadings(pid string, keyword *string, page u readingQuery := mr.ds. From(goqu.T("meter_reading").As("r")). LeftJoin(goqu.T("meter_04kv").As("m"), goqu.On(goqu.I("r.meter_id").Eq(goqu.I("m.code")))). + Select("r.*"). Where( goqu.I("r.park_id").Eq(pid), ) diff --git a/repository/park.go b/repository/park.go index 0e83697..c3a7a36 100644 --- a/repository/park.go +++ b/repository/park.go @@ -1,6 +1,7 @@ package repository import ( + "context" "electricity_bill_calc/cache" "electricity_bill_calc/global" "electricity_bill_calc/logger" @@ -14,6 +15,7 @@ import ( "github.com/doug-martin/goqu/v9" _ "github.com/doug-martin/goqu/v9/dialect/postgres" "github.com/georgysavva/scany/v2/pgxscan" + "github.com/jackc/pgx/v5" "go.uber.org/zap" ) @@ -388,6 +390,36 @@ func (pr _ParkRepository) CreateParkBuilding(pid, name string, floor *string) (b return rs.RowsAffected() > 0, nil } +// 在指定园区中创建一个建筑,这个方法会使用事务 +func (pr _ParkRepository) CreateParkBuildingWithTransaction(tx pgx.Tx, ctx context.Context, pid, name string, floor *string) (bool, error) { + timeNow := types.Now() + serial.StringSerialRequestChan <- 1 + code := serial.Prefix("B", <-serial.StringSerialResponseChan) + createSql, createArgs, _ := pr.ds. + Insert("park_building"). + Cols( + "id", "park_id", "name", "floors", "enabled", "created_at", "last_modified_at", + ). + Vals(goqu.Vals{ + code, + pid, name, floor, true, timeNow, timeNow, + }). + Prepared(true).ToSQL() + + rs, err := tx.Exec(ctx, createSql, createArgs...) + if err != nil { + pr.log.Error("在指定园区中创建一个新建筑失败!", zap.Error(err)) + return false, err + } + + if rs.RowsAffected() > 0 { + cache.AbolishRelation("park_building") + cache.AbolishRelation(fmt.Sprintf("park_building:%s", pid)) + } + + return rs.RowsAffected() > 0, nil +} + // 修改指定园区中指定建筑的信息 func (pr _ParkRepository) ModifyParkBuilding(id, pid, name string, floor *string) (bool, error) { pr.log.Info("修改指定园区中指定建筑的信息", zap.String("id", id), zap.String("pid", pid), zap.String("name", name), zap.Stringp("floor", floor)) diff --git a/service/meter.go b/service/meter.go index ebb428e..a6e6e06 100644 --- a/service/meter.go +++ b/service/meter.go @@ -2,6 +2,7 @@ package service import ( "electricity_bill_calc/cache" + "electricity_bill_calc/excel" "electricity_bill_calc/global" "electricity_bill_calc/logger" "electricity_bill_calc/model" @@ -11,6 +12,7 @@ import ( "electricity_bill_calc/vo" "fmt" "mime/multipart" + "strings" "github.com/samber/lo" "github.com/shopspring/decimal" @@ -108,8 +110,146 @@ func (ms _MeterService) UpdateMeterRecord(pid string, code string, form *vo.Mete } // 处理上传的Excel格式表计档案文件,根据表号自动更新数据库 -func (ms _MeterService) BatchImportMeters(pid string, file multipart.FileHeader) error { - return nil +func (ms _MeterService) BatchImportMeters(pid string, file *multipart.FileHeader) ([]excel.ExcelAnalysisError, error) { + ms.log.Info("处理上传的Excel格式表计档案文件", zap.String("park id", pid)) + ctx, cancel := global.TimeoutContext(10) + defer cancel() + archiveFile, err := file.Open() + if err != nil { + ms.log.Error("无法打开上传的Excel格式表计档案文件。", zap.Error(err)) + return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法打开上传的文件,%w", err) + } + analyzer, err := excel.NewMeterArchiveExcelAnalyzer(archiveFile) + if err != nil { + ms.log.Error("无法根据上传的 Excel 文件创建表计档案分析器。", zap.Error(err)) + return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法创建表计档案解析器,%w", err) + } + records, errs := analyzer.Analysis(*new(model.MeterImportRow)) + if len(errs) > 0 { + ms.log.Error("表计档案分析器在解析上传的 Excel 文件时发生错误。", zap.Int("error count", len(errs))) + return errs, fmt.Errorf("表计档案分析器在解析上传的 Excel 文件时发生错误。") + } + // 步骤1:对目前已经解析到的数据进行重复检测,记录重复内容并直接返回 + var codeStat = make(map[string]int, 0) + for _, record := range records { + if _, ok := codeStat[record.Code]; !ok { + codeStat[record.Code] = 0 + } + codeStat[record.Code]++ + } + duplicatedCodes := make([]string, 0) + for code, count := range codeStat { + if count > 1 { + duplicatedCodes = append(duplicatedCodes, code) + } + } + if len(duplicatedCodes) > 0 { + ms.log.Error("表计档案分析器在解析上传的 Excel 文件时发现重复的表计编号。", zap.Strings("duplicated codes", duplicatedCodes)) + return []excel.ExcelAnalysisError{ + {Row: 0, Col: 0, Err: excel.AnalysisError{Err: fmt.Errorf("表计档案分析器在解析上传的 Excel 文件时发现重复的表计编号。(%s)", strings.Join(duplicatedCodes, ", "))}}, + }, fmt.Errorf("表计档案分析器在解析上传的 Excel 文件时发现重复的表计编号。(%s)", strings.Join(duplicatedCodes, ", ")) + } + // 步骤2:获取指定园区下的所有建筑信息 + buildings, err := repository.ParkRepository.RetrieveParkBuildings(pid) + if err != nil { + ms.log.Error("无法获取指定园区下的所有建筑信息。", zap.Error(err)) + return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法获取指定园区下的所有建筑信息,%w", err) + } + buildingNames := lo.Map(buildings, func(element *model.ParkBuilding, _ int) string { + return element.Name + }) + // 步骤2.1:获取表计档案中出现的所有建筑,并对档案中新出现的建筑进行创建操作 + unexistsBuildingNames := make([]string, 0) + for _, record := range records { + if !lo.Contains(buildingNames, *record.Building) { + unexistsBuildingNames = append(unexistsBuildingNames, *record.Building) + } + } + tx, err := global.DB.Begin(ctx) + if err != nil { + ms.log.Error("无法在自动导入建筑阶段启动数据库事务。", zap.Error(err)) + return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法在自动导入建筑阶段启动数据库事务,%w", err) + } + for _, name := range unexistsBuildingNames { + _, err := repository.ParkRepository.CreateParkBuildingWithTransaction(tx, ctx, pid, name, nil) + if err != nil { + ms.log.Error("无法在自动导入建筑阶段创建新的建筑。", zap.String("building name", name), zap.Error(err)) + tx.Rollback(ctx) + return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法在自动导入建筑阶段创建新的建筑,%w", err) + } + } + 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) + } + buildings, err = repository.ParkRepository.RetrieveParkBuildings(pid) + if err != nil { + ms.log.Error("无法重新获取指定园区下的所有建筑信息。", zap.Error(err)) + return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法重新获取指定园区下的所有建筑信息,%w", err) + } + // 步骤2.3:检测并替换表计档案中的建筑ID + for _, record := range records { + for _, building := range buildings { + if building.Name == *record.Building { + record.Building = &building.Id + break + } + } + } + // 步骤3:启动数据库事务,直接构建表计插入语句,但提供On Conflict Do Update功能 + tx, err = global.DB.Begin(ctx) + if err != nil { + ms.log.Error("无法启动数据插入阶段的数据库事务。", zap.Error(err)) + return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法启动数据插入阶段的数据库事务,%w", err) + } + meterCreationForms := lo.Map(records, func(element model.MeterImportRow, _ int) vo.MeterCreationForm { + return vo.MeterCreationForm{ + Code: element.Code, + Address: element.Address, + MeterType: element.MeterType, + Ratio: element.Ratio, + Seq: element.Seq, + Enabled: true, + Building: element.Building, + OnFloor: element.OnFloor, + Area: element.Area, + MeterReadingForm: vo.MeterReadingForm{ + ReadAt: &element.ReadAt, + Overall: element.Overall, + Critical: element.Critical.Decimal, + Peak: element.Peak.Decimal, + Flat: element.Flat.Decimal, + Valley: element.Valley.Decimal, + }, + } + }) + for _, record := range meterCreationForms { + _, err := repository.MeterRepository.CreateOrUpdateMeter(tx, ctx, pid, record) + if err != nil { + ms.log.Error("无法在数据插入阶段创建或更新表计。", zap.String("meter code", record.Code), zap.Error(err)) + tx.Rollback(ctx) + return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法在数据插入阶段创建或更新表计,%w", err) + } + } + // 步骤5:将全部抄表信息保存进入数据库 + for _, record := range meterCreationForms { + _, err := repository.MeterRepository.RecordReading(tx, ctx, pid, record.Code, record.MeterType, record.Ratio, &record.MeterReadingForm) + if err != nil { + ms.log.Error("无法在数据插入阶段保存抄表信息。", zap.String("meter code", record.Code), zap.Error(err)) + tx.Rollback(ctx) + return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法在数据插入阶段保存抄表信息,%w", err) + } + } + // 步骤6:执行事务,更新数据库 + 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 } // 更换系统中的表计 diff --git a/tools/utils.go b/tools/utils.go index 561f5e3..ad82f23 100644 --- a/tools/utils.go +++ b/tools/utils.go @@ -91,6 +91,14 @@ func CondFn[T, R any](exprFn func(val T) bool, value T, trueValue, falseValue R) return Cond(exprFn(value), trueValue, falseValue) } +// 使用给定的函数对指定的值进行判断,根据表达式的值返回指定的值。本函数为惰性求值。 +func CondFnElse[T, R any](exprFn func(val T) bool, value T, trueValueFn func(val T) R, falseValueFn func(val T) R) R { + if exprFn(value) { + return trueValueFn(value) + } + return falseValueFn(value) +} + // 使用给定的函数对指定的值进行判断,如果表达式为真,则返回指定的值,否则返回另一个值。 func CondOr[T any](exprFn func(val T) bool, value, elseValue T) T { return CondFn(exprFn, value, value, elseValue) diff --git a/types/datetime.go b/types/datetime.go index 78a4066..d6d8140 100644 --- a/types/datetime.go +++ b/types/datetime.go @@ -45,6 +45,12 @@ func FromTime(t time.Time) DateTime { } } +func FromUnixMicro(sec int64) DateTime { + return DateTime{ + Time: time.UnixMicro(sec).In(loc), + } +} + func ParseDateTime(t string) (DateTime, error) { if len(t) == 0 { return NewEmptyDateTime(), fmt.Errorf("不能解析空白的日期时间。") diff --git a/vo/meter.go b/vo/meter.go index 9671097..0c84e79 100644 --- a/vo/meter.go +++ b/vo/meter.go @@ -36,3 +36,9 @@ type MeterReplacingForm struct { OldReading MeterReadingForm `json:"oldReading"` NewMeter NewMeterForReplacingForm `json:"newMeter"` } + +type SimplifiedMeterQueryResponse struct { + Code string `json:"code"` + Address *string `json:"address"` + Park string `json:"parkId"` +} diff --git a/vo/reading.go b/vo/reading.go index 0ce357b..d3c3f1e 100644 --- a/vo/reading.go +++ b/vo/reading.go @@ -1,7 +1,9 @@ package vo import ( + "electricity_bill_calc/model" "electricity_bill_calc/types" + "fmt" "github.com/shopspring/decimal" ) @@ -14,3 +16,60 @@ type MeterReadingForm struct { Valley decimal.Decimal `json:"valley"` ReadAt *types.DateTime `json:"readAt"` } + +func (r MeterReadingForm) Validate() bool { + flat := r.Overall.Sub(r.Critical).Sub(r.Peak).Sub(r.Valley) + return flat.LessThan(decimal.Zero) +} + +type MeterReadingDetailResponse struct { + Code string `json:"code"` + Park string `json:"parkId"` + Address *string `json:"address"` + Seq int64 `json:"seq"` + Ratio decimal.Decimal `json:"ratio"` + MeterType int16 `json:"type"` + Enabled bool `json:"enabled"` + Building *string `json:"building"` + BuildingName *string `json:"buildingName"` + OnFloor *string `json:"onFloor"` + Area decimal.Decimal `json:"area"` + AttachedAt *types.DateTime `json:"attachedAt"` + DetachedAt *types.DateTime `json:"detachedAt"` + CreatedAt types.DateTime `json:"createdAt"` + LastModifiedAt *types.DateTime `json:"lastModifiedAt"` + ReadAt types.DateTime `json:"readAt"` + ReadAtTimestamp string `json:"readAtTimestamp"` + Overall decimal.Decimal `json:"overall"` + Critical decimal.Decimal `json:"critical"` + Peak decimal.Decimal `json:"peak"` + Flat decimal.Decimal `json:"flat"` + Valley decimal.Decimal `json:"valley"` +} + +func FromDetailedMeterReading(reading model.DetailedMeterReading) MeterReadingDetailResponse { + return MeterReadingDetailResponse{ + Code: reading.Detail.Code, + Park: reading.Detail.Park, + Address: reading.Detail.Address, + Seq: reading.Detail.Seq, + Ratio: reading.Detail.Ratio, + MeterType: reading.Detail.MeterType, + Enabled: reading.Detail.Enabled, + Building: reading.Detail.Building, + BuildingName: reading.Detail.BuildingName, + OnFloor: reading.Detail.OnFloor, + Area: reading.Detail.Area.Decimal, + AttachedAt: reading.Detail.AttachedAt, + DetachedAt: (*types.DateTime)(reading.Detail.DetachedAt), + CreatedAt: types.DateTime(reading.Detail.CreatedAt), + LastModifiedAt: (*types.DateTime)(&reading.Detail.LastModifiedAt), + ReadAt: reading.Reading.ReadAt, + ReadAtTimestamp: fmt.Sprintf("%d", reading.Reading.ReadAt.UnixMicro()), + Overall: reading.Reading.Overall, + Critical: reading.Reading.Critical, + Peak: reading.Reading.Peak, + Flat: reading.Reading.Flat, + Valley: reading.Reading.Valley, + } +}