enhance(meter):完成大部分表计相关的接口。

This commit is contained in:
徐涛 2023-06-11 22:31:32 +08:00
parent e366888608
commit 2339e4c725
10 changed files with 700 additions and 36 deletions

View File

@ -2,15 +2,20 @@ package controller
import ( import (
"electricity_bill_calc/logger" "electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/repository" "electricity_bill_calc/repository"
"electricity_bill_calc/response" "electricity_bill_calc/response"
"electricity_bill_calc/security" "electricity_bill_calc/security"
"electricity_bill_calc/service" "electricity_bill_calc/service"
"electricity_bill_calc/tools"
"electricity_bill_calc/types"
"electricity_bill_calc/vo" "electricity_bill_calc/vo"
"fmt" "fmt"
"net/http" "net/http"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/jinzhu/copier"
"github.com/samber/lo"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -20,8 +25,17 @@ func InitializeMeterHandlers(router *fiber.App) {
router.Get("/meter/:pid", security.EnterpriseAuthorize, searchMetersWithinPark) router.Get("/meter/:pid", security.EnterpriseAuthorize, searchMetersWithinPark)
router.Post("/meter/:pid", security.EnterpriseAuthorize, createNewMeterManually) router.Post("/meter/:pid", security.EnterpriseAuthorize, createNewMeterManually)
router.Get("/meter/:pid/template", security.EnterpriseAuthorize, downloadMeterArchiveTemplate) 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.Get("/meter/:pid/:code", security.EnterpriseAuthorize, retrieveSpecificMeterDetail)
router.Put("/meter/:pid/:code", security.EnterpriseAuthorize, updateMeterManually) 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文件中导入表计档案 // 从Excel文件中导入表计档案
func uploadMeterArchive(c *fiber.Ctx) error { 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 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
}

View File

@ -2,7 +2,6 @@ package model
import ( import (
"electricity_bill_calc/types" "electricity_bill_calc/types"
"time"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
) )
@ -19,10 +18,10 @@ type MeterDetail struct {
Ratio decimal.Decimal `json:"ratio" db:"ratio"` Ratio decimal.Decimal `json:"ratio" db:"ratio"`
Seq int64 `json:"seq" db:"seq"` Seq int64 `json:"seq" db:"seq"`
Enabled bool `json:"enabled" db:"enabled"` Enabled bool `json:"enabled" db:"enabled"`
AttachedAt *time.Time `json:"attachedAt" db:"attached_at"` AttachedAt *types.DateTime `json:"attachedAt" db:"attached_at"`
DetachedAt *time.Time `json:"detachedAt" db:"detached_at"` DetachedAt *types.DateTime `json:"detachedAt" db:"detached_at"`
CreatedAt time.Time `json:"createdAt" db:"created_at"` CreatedAt types.DateTime `json:"createdAt" db:"created_at"`
LastModifiedAt time.Time `json:"lastModifiedAt" db:"last_modified_at"` LastModifiedAt types.DateTime `json:"lastModifiedAt" db:"last_modified_at"`
} }
type MeterRelation struct { type MeterRelation struct {
@ -30,9 +29,9 @@ type MeterRelation struct {
Park string `json:"parkId" db:"park_id"` Park string `json:"parkId" db:"park_id"`
MasterMeter string `json:"masterMeterId" db:"master_meter_id"` MasterMeter string `json:"masterMeterId" db:"master_meter_id"`
SlaveMeter string `json:"slaveMeterId" db:"slave_meter_id"` SlaveMeter string `json:"slaveMeterId" db:"slave_meter_id"`
EstablishedAt time.Time `json:"establishedAt"` EstablishedAt types.DateTime `json:"establishedAt"`
SuspendedAt *time.Time `json:"suspendedAt"` SuspendedAt *types.DateTime `json:"suspendedAt"`
RevokeAt *time.Time `json:"revokeAt"` RevokeAt *types.DateTime `json:"revokeAt"`
} }
type MeterSynchronization struct { type MeterSynchronization struct {
@ -42,8 +41,8 @@ type MeterSynchronization struct {
SystemType string `json:"systemType"` SystemType string `json:"systemType"`
SystemIdentity string `json:"systemIdentity"` SystemIdentity string `json:"systemIdentity"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
LastSynchronizedAt time.Time `json:"lastSynchronizedAt" db:"last_synchronized_at"` LastSynchronizedAt types.DateTime `json:"lastSynchronizedAt" db:"last_synchronized_at"`
RevokeAt *time.Time `json:"revokeAt" db:"revoke_at"` RevokeAt *types.DateTime `json:"revokeAt" db:"revoke_at"`
} }
type SimpleMeterDocument struct { type SimpleMeterDocument struct {

View File

@ -1,7 +1,7 @@
package model package model
import ( import (
"time" "electricity_bill_calc/types"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
) )
@ -38,7 +38,7 @@ func NewUnitaryReading(ratio, overall decimal.Decimal) *Reading {
} }
type MeterReading struct { type MeterReading struct {
ReadAt time.Time `json:"readAt"` ReadAt types.DateTime `json:"readAt"`
Park string `json:"parkId" db:"park_id"` Park string `json:"parkId" db:"park_id"`
Meter string `json:"meterId" db:"meter_id"` Meter string `json:"meterId" db:"meter_id"`
MeterType int16 `json:"meterType"` MeterType int16 `json:"meterType"`

View File

@ -215,7 +215,7 @@ func (mr _MeterRepository) ListMetersByIDs(pid string, ids []string) ([]*model.M
pid, pid,
strings.Join(ids, ","), 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))) mr.log.Info("从缓存中获取到了指定园区中所需的表计信息", zap.Int("count", len(*meters)))
return *meters, nil return *meters, nil
} }
@ -231,7 +231,7 @@ func (mr _MeterRepository) ListMetersByIDs(pid string, ids []string) ([]*model.M
). ).
Where( Where(
goqu.I("m.park_id").Eq(pid), 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()). Order(goqu.I("m.seq").Asc()).
Prepared(true).ToSQL() Prepared(true).ToSQL()
@ -311,6 +311,43 @@ func (mr _MeterRepository) CreateMeter(tx pgx.Tx, ctx context.Context, pid strin
return ok.RowsAffected() > 0, nil 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) { 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)) 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) { 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{ cacheConditions := []string{
pid, pid,
tools.DefaultOrEmptyStr(keyword, "UNDEF"), cache.NullableStringKey(keyword),
fmt.Sprintf("%d", page), fmt.Sprintf("%d", page),
tools.CondFn(func(val *types.Date) bool { cache.NullableConditionKey(start),
return val != nil cache.NullableConditionKey(end),
}, start, start.ToString(), "UNDEF"), cache.NullableStringKey(buidling),
tools.CondFn(func(val *types.Date) bool {
return val != nil
}, end, end.ToString(), "UNDEF"),
} }
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)) mr.log.Info("从缓存中获取到了指定园区中的抄表记录", zap.Int("count", len(*readings)), zap.Int64("total", total))
return *readings, total, nil return *readings, total, nil
} }
@ -870,6 +904,7 @@ func (mr _MeterRepository) ListMeterReadings(pid string, keyword *string, page u
readingQuery := mr.ds. readingQuery := mr.ds.
From(goqu.T("meter_reading").As("r")). 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")))). LeftJoin(goqu.T("meter_04kv").As("m"), goqu.On(goqu.I("r.meter_id").Eq(goqu.I("m.code")))).
Select("r.*").
Where( Where(
goqu.I("r.park_id").Eq(pid), goqu.I("r.park_id").Eq(pid),
) )

View File

@ -1,6 +1,7 @@
package repository package repository
import ( import (
"context"
"electricity_bill_calc/cache" "electricity_bill_calc/cache"
"electricity_bill_calc/global" "electricity_bill_calc/global"
"electricity_bill_calc/logger" "electricity_bill_calc/logger"
@ -14,6 +15,7 @@ import (
"github.com/doug-martin/goqu/v9" "github.com/doug-martin/goqu/v9"
_ "github.com/doug-martin/goqu/v9/dialect/postgres" _ "github.com/doug-martin/goqu/v9/dialect/postgres"
"github.com/georgysavva/scany/v2/pgxscan" "github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -388,6 +390,36 @@ func (pr _ParkRepository) CreateParkBuilding(pid, name string, floor *string) (b
return rs.RowsAffected() > 0, nil 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) { 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)) pr.log.Info("修改指定园区中指定建筑的信息", zap.String("id", id), zap.String("pid", pid), zap.String("name", name), zap.Stringp("floor", floor))

View File

@ -2,6 +2,7 @@ package service
import ( import (
"electricity_bill_calc/cache" "electricity_bill_calc/cache"
"electricity_bill_calc/excel"
"electricity_bill_calc/global" "electricity_bill_calc/global"
"electricity_bill_calc/logger" "electricity_bill_calc/logger"
"electricity_bill_calc/model" "electricity_bill_calc/model"
@ -11,6 +12,7 @@ import (
"electricity_bill_calc/vo" "electricity_bill_calc/vo"
"fmt" "fmt"
"mime/multipart" "mime/multipart"
"strings"
"github.com/samber/lo" "github.com/samber/lo"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
@ -108,8 +110,146 @@ func (ms _MeterService) UpdateMeterRecord(pid string, code string, form *vo.Mete
} }
// 处理上传的Excel格式表计档案文件根据表号自动更新数据库 // 处理上传的Excel格式表计档案文件根据表号自动更新数据库
func (ms _MeterService) BatchImportMeters(pid string, file multipart.FileHeader) error { func (ms _MeterService) BatchImportMeters(pid string, file *multipart.FileHeader) ([]excel.ExcelAnalysisError, error) {
return nil 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
} }
// 更换系统中的表计 // 更换系统中的表计

View File

@ -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) 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 { func CondOr[T any](exprFn func(val T) bool, value, elseValue T) T {
return CondFn(exprFn, value, value, elseValue) return CondFn(exprFn, value, value, elseValue)

View File

@ -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) { func ParseDateTime(t string) (DateTime, error) {
if len(t) == 0 { if len(t) == 0 {
return NewEmptyDateTime(), fmt.Errorf("不能解析空白的日期时间。") return NewEmptyDateTime(), fmt.Errorf("不能解析空白的日期时间。")

View File

@ -36,3 +36,9 @@ type MeterReplacingForm struct {
OldReading MeterReadingForm `json:"oldReading"` OldReading MeterReadingForm `json:"oldReading"`
NewMeter NewMeterForReplacingForm `json:"newMeter"` NewMeter NewMeterForReplacingForm `json:"newMeter"`
} }
type SimplifiedMeterQueryResponse struct {
Code string `json:"code"`
Address *string `json:"address"`
Park string `json:"parkId"`
}

View File

@ -1,7 +1,9 @@
package vo package vo
import ( import (
"electricity_bill_calc/model"
"electricity_bill_calc/types" "electricity_bill_calc/types"
"fmt"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
) )
@ -14,3 +16,60 @@ type MeterReadingForm struct {
Valley decimal.Decimal `json:"valley"` Valley decimal.Decimal `json:"valley"`
ReadAt *types.DateTime `json:"readAt"` 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,
}
}