diff --git a/assets/meter_04kv_template.xlsx b/assets/meter_04kv_template.xlsx index c1cccde..687fafc 100644 Binary files a/assets/meter_04kv_template.xlsx and b/assets/meter_04kv_template.xlsx differ diff --git a/controller/meter.go b/controller/meter.go new file mode 100644 index 0000000..886e9c5 --- /dev/null +++ b/controller/meter.go @@ -0,0 +1,180 @@ +package controller + +import ( + "electricity_bill_calc/logger" + "electricity_bill_calc/repository" + "electricity_bill_calc/response" + "electricity_bill_calc/security" + "electricity_bill_calc/service" + "electricity_bill_calc/vo" + "fmt" + "net/http" + + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +var meterLog = logger.Named("Handler", "Meter") + +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.Get("/meter/:pid/:code", security.EnterpriseAuthorize, retrieveSpecificMeterDetail) + router.Put("/meter/:pid/:code", security.EnterpriseAuthorize, updateMeterManually) +} + +// 查询指定园区下的表计信息 +func searchMetersWithinPark(c *fiber.Ctx) error { + parkId := c.Params("parkId") + meterLog.Info("查询指定园区下的表计信息", zap.String("park id", parkId)) + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("无法查询指定园区下的表计信息,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + if ok, err := checkParkBelongs(meterLog, parkId, session, &result); !ok { + return err + } + keyword := c.Query("keyword") + page := c.QueryInt("page", 1) + meters, total, err := repository.MeterRepository.MetersIn(parkId, uint(page), &keyword) + if err != nil { + meterLog.Error("无法查询指定园区下的表计信息,无法获取表计列表", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + return result.Success( + "已经取得符合条件的0.4kV表计列表。", + response.NewPagedResponse(page, total).ToMap(), + fiber.Map{"meters": meters}, + ) +} + +// 查询指定园区中指定表计的详细信息 +func retrieveSpecificMeterDetail(c *fiber.Ctx) error { + parkId := c.Params("pid") + meterId := c.Params("code") + meterLog.Info("查询指定园区中指定表计的详细信息", zap.String("park id", parkId), zap.String("meter id", meterId)) + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("无法查询指定园区中指定表计的详细信息,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + if ok, err := checkParkBelongs(meterLog, parkId, session, &result); !ok { + return err + } + meter, err := repository.MeterRepository.FetchMeterDetail(parkId, meterId) + if err != nil { + meterLog.Error("无法查询指定园区中指定表计的详细信息,无法获取表计信息", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + if meter == nil { + meterLog.Warn("无法查询指定园区中指定表计的详细信息,表计不存在") + return result.NotFound("指定的表计不存在。") + } + return result.Success("指定表计信息已经找到。", fiber.Map{"meter": meter}) +} + +// 手动添加一条0.4kV表计记录 +func createNewMeterManually(c *fiber.Ctx) error { + parkId := c.Params("pid") + meterLog.Info("手动添加一条0.4kV表计记录", zap.String("park id", parkId)) + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("无法手动添加一条0.4kV表计记录,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + if ok, err := checkParkBelongs(meterLog, parkId, session, &result); !ok { + return err + } + var creationForm vo.MeterCreationForm + if err := c.BodyParser(&creationForm); err != nil { + meterLog.Error("无法手动添加一条0.4kV表计记录,无法解析表计创建表单", zap.Error(err)) + return result.NotAccept(err.Error()) + } + if err := service.MeterService.CreateMeterRecord(parkId, &creationForm); err != nil { + meterLog.Error("无法手动添加一条0.4kV表计记录,无法创建表计记录", zap.Error(err)) + return result.NotAccept(err.Error()) + } + return result.Created("新0.4kV表计已经添加完成。") +} + +// 手动更新一条新的0.4kV表计记录 +func updateMeterManually(c *fiber.Ctx) error { + parkId := c.Params("pid") + meterId := c.Params("code") + meterLog.Info("手动更新一条新的0.4kV表计记录", zap.String("park id", parkId), zap.String("meter id", meterId)) + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("无法手动更新一条新的0.4kV表计记录,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + if ok, err := checkParkBelongs(meterLog, parkId, session, &result); !ok { + return err + } + var updateForm vo.MeterModificationForm + if err := c.BodyParser(&updateForm); err != nil { + meterLog.Error("无法手动更新一条新的0.4kV表计记录,无法解析表计更新表单", zap.Error(err)) + return result.NotAccept(err.Error()) + } + if err := service.MeterService.UpdateMeterRecord(parkId, meterId, &updateForm); err != nil { + meterLog.Error("无法手动更新一条新的0.4kV表计记录,无法更新表计记录", zap.Error(err)) + return result.NotAccept(err.Error()) + } + return result.Updated("0.4kV表计已经更新完成。") +} + +// 下载指定的园区表计登记模板 +func downloadMeterArchiveTemplate(c *fiber.Ctx) error { + parkId := c.Params("pid") + meterLog.Info("下载指定的园区表计登记模板", zap.String("park id", parkId)) + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("无法下载指定的园区表计登记模板,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + if ok, err := checkParkBelongs(meterLog, parkId, session, &result); !ok { + return err + } + parkDetail, err := repository.ParkRepository.RetrieveParkDetail(parkId) + if err != nil { + meterLog.Error("无法下载指定的园区表计登记模板,无法获取园区信息", zap.Error(err)) + return result.NotFound(err.Error()) + } + return c.Download( + "./assets/meter_04kv_template.xlsx", + fmt.Sprintf("%s_表计档案.xlsx", parkDetail.Name), + ) +} + +// 从Excel文件中导入表计档案 +func uploadMeterArchive(c *fiber.Ctx) error { + return nil +} + +// 更换系统中的表计 +func replaceMeter(c *fiber.Ctx) error { + parkId := c.Params("pid") + meterId := c.Params("code") + meterLog.Info("更换系统中的表计", zap.String("park id", parkId), zap.String("meter id", meterId)) + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + meterLog.Error("无法更换系统中的表计,无法获取当前用户会话", zap.Error(err)) + return result.Unauthorized(err.Error()) + } + if ok, err := checkParkBelongs(meterLog, parkId, session, &result); !ok { + return err + } + var replacementForm vo.MeterReplacingForm + if err := c.BodyParser(&replacementForm); err != nil { + meterLog.Error("无法更换系统中的表计,无法解析表计更换表单", zap.Error(err)) + return result.NotAccept(err.Error()) + } + return nil +} diff --git a/model/cunsumption.go b/model/cunsumption.go new file mode 100644 index 0000000..4f853d9 --- /dev/null +++ b/model/cunsumption.go @@ -0,0 +1,10 @@ +package model + +import "github.com/shopspring/decimal" + +type ConsumptionUnit struct { + Amount decimal.Decimal `json:"amount"` + Fee decimal.Decimal `json:"fee"` + Price decimal.Decimal `json:"price"` + Proportion decimal.Decimal `json:"proportion"` +} diff --git a/model/meter.go b/model/meter.go new file mode 100644 index 0000000..3a3cdad --- /dev/null +++ b/model/meter.go @@ -0,0 +1,78 @@ +package model + +import ( + "time" + + "github.com/shopspring/decimal" +) + +type MeterDetail struct { + Code string `json:"code" db:"code"` + Park string `json:"parkId" db:"park_id"` + Address *string `json:"address" db:"address"` + MeterType int16 `json:"meterType" db:"meter_type"` + Building *string `json:"building" db:"building"` + BuildingName *string `json:"buildingName" db:"building_name"` + OnFloor *string `json:"onFloor" db:"on_floor"` + Area decimal.NullDecimal `json:"area" db:"area"` + 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"` +} + +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"` +} + +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"` +} + +type SimpleMeterDocument struct { + Code string `json:"code"` + Seq int64 `json:"seq"` + Address *string `json:"address"` + Ratio decimal.Decimal `json:"ratio"` + TenementName *string `json:"tenementName"` +} + +type NestedMeter struct { + MeterId string `json:"meterId"` + MeterDetail MeterDetail `json:"meterDetail"` + LastTermReadings Reading `json:"lastTermReadings"` + CurrentTermReadings Reading `json:"currentTermReadings"` + Overall ConsumptionUnit `json:"overall"` + Critical ConsumptionUnit `json:"critical"` + Peak ConsumptionUnit `json:"peak"` + Flat ConsumptionUnit `json:"flat"` + Valley ConsumptionUnit `json:"valley"` + BasicPooled decimal.Decimal `json:"basicPooled"` + AdjustPooled decimal.Decimal `json:"adjustPooled"` + LossPooled decimal.Decimal `json:"lossPooled"` + PublicPooled decimal.Decimal `json:"publicPooled"` + FinalTotal decimal.Decimal `json:"finalTotal"` + Area decimal.Decimal `json:"area"` + Proportion decimal.Decimal `json:"proportion"` +} + +type PooledMeterDetailCompound struct { + MeterDetail + BindMeters []MeterDetail `json:"bindMeters"` +} diff --git a/model/reading.go b/model/reading.go new file mode 100644 index 0000000..03db697 --- /dev/null +++ b/model/reading.go @@ -0,0 +1,56 @@ +package model + +import ( + "time" + + "github.com/shopspring/decimal" +) + +type Reading struct { + Ratio decimal.Decimal `json:"ratio"` + 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 NewPVReading(ratio, overall, critical, peak, flat, valley decimal.Decimal) *Reading { + return &Reading{ + Ratio: ratio, + Overall: overall, + Critical: critical, + Peak: peak, + Flat: flat, + Valley: valley, + } +} + +func NewUnitaryReading(ratio, overall decimal.Decimal) *Reading { + return &Reading{ + Ratio: ratio, + Overall: overall, + Critical: decimal.Zero, + Peak: decimal.Zero, + Flat: overall, + Valley: decimal.Zero, + } +} + +type MeterReading struct { + ReadAt time.Time `json:"readAt"` + Park string `json:"parkId" db:"park_id"` + Meter string `json:"meterId" db:"meter_id"` + MeterType int16 `json:"meterType"` + Ratio decimal.Decimal `json:"ratio"` + 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"` +} + +type DetailedMeterReading struct { + Detail MeterDetail `json:"detail"` + Reading MeterReading `json:"reading"` +} diff --git a/repository/meter.go b/repository/meter.go new file mode 100644 index 0000000..d590d2a --- /dev/null +++ b/repository/meter.go @@ -0,0 +1,1091 @@ +package repository + +import ( + "context" + "electricity_bill_calc/cache" + "electricity_bill_calc/config" + "electricity_bill_calc/global" + "electricity_bill_calc/logger" + "electricity_bill_calc/model" + "electricity_bill_calc/tools" + "electricity_bill_calc/tools/serial" + "electricity_bill_calc/types" + "electricity_bill_calc/vo" + "fmt" + "strings" + + "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" + "github.com/shopspring/decimal" + "go.uber.org/zap" +) + +type _MeterRepository struct { + log *zap.Logger + ds goqu.DialectWrapper +} + +var MeterRepository = _MeterRepository{ + log: logger.Named("Repository", "Meter"), + ds: goqu.Dialect("postgres"), +} + +// 获取指定园区中所有的表计信息 +func (mr _MeterRepository) AllMeters(pid string) ([]*model.MeterDetail, error) { + mr.log.Info("列出指定园区中的所有表计", zap.String("park id", pid)) + cacheConditions := []string{pid} + if meters, err := cache.RetrieveSearch[[]*model.MeterDetail]("all_meters_in", cacheConditions...); err == nil { + mr.log.Info("从缓存中获取到了指定园区中的表计信息", zap.Int("count", len(*meters))) + return *meters, nil + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + var meters []*model.MeterDetail + metersSql, metersArgs, _ := mr.ds. + From(goqu.T("meter_04kv").As("m")). + LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))). + Select( + "m.*", goqu.I("b.name").As("building_name"), + ). + Where( + goqu.I("m.park_id").Eq(pid), + goqu.I("m.detachedAt").IsNull(), + ). + Order(goqu.I("m.seq").Asc()). + Prepared(true).ToSQL() + + if err := pgxscan.Select(ctx, global.DB, &meters, metersSql, metersArgs...); err != nil { + mr.log.Error("查询表计信息失败", zap.Error(err)) + return make([]*model.MeterDetail, 0), err + } + + cache.CacheSearch(meters, []string{fmt.Sprintf("meter:%s", pid)}, "all_meters_in", cacheConditions...) + + return meters, nil +} + +// 列出指定园区下的所有表计信息,包含已经拆除的表计 +func (mr _MeterRepository) AllUsedMeters(pid string) ([]*model.MeterDetail, error) { + mr.log.Info("列出指定园区中的所有使用过的表计", zap.String("park id", pid)) + cacheConditions := []string{pid} + if meters, err := cache.RetrieveSearch[[]*model.MeterDetail]("all_used_meters_in", cacheConditions...); err == nil { + mr.log.Info("从缓存中获取到了指定园区中的表计信息", zap.Int("count", len(*meters))) + return *meters, nil + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + var meters []*model.MeterDetail + metersSql, metersArgs, _ := mr.ds. + From(goqu.T("meter_04kv").As("m")). + LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))). + Select( + "m.*", goqu.I("b.name").As("building_name"), + ). + Where( + goqu.I("m.park_id").Eq(pid), + ). + Order(goqu.I("m.seq").Asc()). + Prepared(true).ToSQL() + + if err := pgxscan.Select(ctx, global.DB, &meters, metersSql, metersArgs...); err != nil { + mr.log.Error("查询表计信息失败", zap.Error(err)) + return make([]*model.MeterDetail, 0), err + } + + cache.CacheSearch(meters, []string{fmt.Sprintf("meter:%s", pid)}, "all_used_meters_in", cacheConditions...) + + return meters, nil +} + +// 列出指定核算报表中所使用的所有表计,包含已经拆除的表计 +func (mr _MeterRepository) AllUsedMetersInReport(rid string) ([]*model.MeterDetail, error) { + mr.log.Info("列出指定核算报表中所使用的所有表计", zap.String("report id", rid)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + var meters []*model.MeterDetail + metersSql, metersArgs, _ := mr.ds. + From(goqu.T("meter_04kv").As("m")). + LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))). + Join(goqu.T("report").As("r"), goqu.On(goqu.I("m.park_id").Eq(goqu.I("r.park_id")))). + Where( + goqu.I("r.id").Eq(rid), + goqu.I("m.enabled").Eq(true), + goqu.L("m.attached_at::date < upper(r.period)"), + goqu.Or( + goqu.I("m.detached_at").IsNull(), + goqu.L("m.detached_at::date >= lower(r.period)"), + ), + ). + Select( + "m.*", goqu.I("b.name").As("building_name"), + ). + Order(goqu.I("m.seq").Asc()). + Prepared(true).ToSQL() + + if err := pgxscan.Select(ctx, global.DB, &meters, metersSql, metersArgs...); err != nil { + mr.log.Error("查询表计信息失败", zap.Error(err)) + return make([]*model.MeterDetail, 0), err + } + + return meters, nil +} + +// 分页列出指定园区下的表计信息 +func (mr _MeterRepository) MetersIn(pid string, page uint, keyword *string) ([]*model.MeterDetail, int64, error) { + mr.log.Info("分页列出指定园区下的表计信息", zap.String("park id", pid), zap.Uint("page", page), zap.String("keyword", tools.DefaultTo(keyword, ""))) + cacheConditions := []string{ + pid, + tools.DefaultOrEmptyStr(keyword, "UNDEF"), + fmt.Sprintf("%d", page), + } + if meters, total, err := cache.RetrievePagedSearch[[]*model.MeterDetail]("meter", cacheConditions...); err == nil { + mr.log.Info("从缓存中获取到了指定园区中的表计信息", zap.Int("count", len(*meters)), zap.Int64("total", total)) + return *meters, total, nil + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + meterQuery := mr.ds. + From(goqu.T("meter_04kv").As("m")). + LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))). + Select( + "m.*", goqu.I("b.name").As("building_name"), + ). + Where( + goqu.I("m.park_id").Eq(pid), + goqu.I("m.detachedAt").IsNull(), + ) + countQuery := mr.ds. + From(goqu.T("meter_04kv").As("m")). + Select(goqu.COUNT("*")). + Where( + goqu.I("m.park_id").Eq(pid), + goqu.I("m.detachedAt").IsNull(), + ) + + if keyword != nil && len(*keyword) > 0 { + pattern := fmt.Sprintf("%%%s%%", *keyword) + meterQuery = meterQuery.Where( + goqu.Or( + goqu.I("m.code").ILike(pattern), + goqu.I("m.address").ILike(pattern), + ), + ) + countQuery = countQuery.Where( + goqu.Or( + goqu.I("m.code").ILike(pattern), + goqu.I("m.address").ILike(pattern), + ), + ) + } + + startRow := (page - 1) * config.ServiceSettings.ItemsPageSize + meterQuery = meterQuery.Order(goqu.I("m.seq").Asc()).Offset(startRow).Limit(config.ServiceSettings.ItemsPageSize) + + meterSql, meterArgs, _ := meterQuery.Prepared(true).ToSQL() + countSql, countArgs, _ := countQuery.Prepared(true).ToSQL() + + var ( + meters []*model.MeterDetail + total int64 + ) + if err := pgxscan.Select(ctx, global.DB, &meters, meterSql, meterArgs...); err != nil { + mr.log.Error("查询表计信息失败", zap.Error(err)) + return make([]*model.MeterDetail, 0), 0, err + } + if err := pgxscan.Get(ctx, global.DB, &total, countSql, countArgs...); err != nil { + mr.log.Error("查询表计数量失败", zap.Error(err)) + return make([]*model.MeterDetail, 0), 0, err + } + + cache.CachePagedSearch(meters, total, []string{fmt.Sprintf("meter:%s", pid)}, "meter", cacheConditions...) + + return meters, total, nil +} + +// 列出指定园区中指定列表中所有表计的详细信息,将忽略所有表计的当前状态 +func (mr _MeterRepository) ListMetersByIDs(pid string, ids []string) ([]*model.MeterDetail, error) { + mr.log.Info("列出指定园区中指定列表中所有表计的详细信息", zap.String("park id", pid), zap.Strings("meter ids", ids)) + cacheConditions := []string{ + pid, + strings.Join(ids, ","), + } + if meters, err := cache.RetrieveSearch[[]*model.MeterDetail]("meter_slice", cacheConditions...); err == nil { + mr.log.Info("从缓存中获取到了指定园区中所需的表计信息", zap.Int("count", len(*meters))) + return *meters, nil + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + var meters []*model.MeterDetail + metersSql, metersArgs, _ := mr.ds. + From(goqu.T("meter_04kv").As("m")). + LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))). + Select( + "m.*", goqu.I("b.name").As("building_name"), + ). + Where( + goqu.I("m.park_id").Eq(pid), + goqu.I("m.id").Eq(goqu.Func("any", ids)), + ). + Order(goqu.I("m.seq").Asc()). + Prepared(true).ToSQL() + + if err := pgxscan.Select(ctx, global.DB, &meters, metersSql, metersArgs...); err != nil { + mr.log.Error("查询表计信息失败", zap.Error(err)) + return make([]*model.MeterDetail, 0), err + } + + cache.CacheSearch(meters, []string{fmt.Sprintf("meter:%s", pid), fmt.Sprintf("meter_slice:%s", pid)}, "meter_slice", cacheConditions...) + + return meters, nil +} + +// 获取指定表计的详细信息 +func (mr _MeterRepository) FetchMeterDetail(pid, code string) (*model.MeterDetail, error) { + mr.log.Info("获取指定表计的详细信息", zap.String("park id", pid), zap.String("meter code", code)) + cacheConditions := fmt.Sprintf("%s:%s", pid, code) + if meter, err := cache.RetrieveEntity[*model.MeterDetail]("meter", cacheConditions); err == nil { + mr.log.Info("从缓存中获取到了指定表计的详细信息", zap.String("code", (**meter).Code)) + return *meter, nil + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + var meter model.MeterDetail + meterSql, meterArgs, _ := mr.ds. + From(goqu.T("meter_04kv").As("m")). + LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))). + Select( + "m.*", goqu.I("b.name").As("building_name"), + ). + Where( + goqu.I("m.park_id").Eq(pid), + goqu.I("m.code").Eq(code), + ). + Prepared(true).ToSQL() + + if err := pgxscan.Get(ctx, global.DB, &meter, meterSql, meterArgs...); err != nil { + mr.log.Error("查询表计信息失败", zap.Error(err)) + return nil, err + } + + cache.CacheEntity(meter, []string{fmt.Sprintf("meter:%s", pid), "park"}, "meter", cacheConditions) + + return &meter, nil +} + +// 创建一条新的表计信息 +func (mr _MeterRepository) CreateMeter(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, + }, + ). + Prepared(true).ToSQL() + + ok, err := tx.Exec(ctx, meterSql, meterArgs...) + if err != nil { + mr.log.Error("创建表计信息失败", zap.Error(err)) + return false, err + } + + if ok.RowsAffected() > 0 { + cache.AbolishRelation(fmt.Sprintf("meter:%s", pid)) + cache.AbolishRelation(fmt.Sprintf("meter_relations:%s:%s", pid, meter.Code)) + } + + return ok.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)) + readAt := tools.DefaultTo(reading.ReadAt, types.Now()) + readingSql, readingArgs, _ := mr.ds. + Insert(goqu.T("meter_reading")). + Cols( + "park_id", "meter_id", "read_at", "meter_type", "ratio", "overall", "critical", "peak", "flat", "valley", + ). + Vals( + goqu.Vals{pid, code, readAt, meterType, ratio, reading.Overall, reading.Critical, reading.Peak, reading.Flat, reading.Valley}, + ). + Prepared(true).ToSQL() + + ok, err := tx.Exec(ctx, readingSql, readingArgs...) + if err != nil { + mr.log.Error("记录表计抄表信息失败", zap.Error(err)) + return false, err + } + + if ok.RowsAffected() > 0 { + cache.AbolishRelation(fmt.Sprintf("meter_reading:%s", pid)) + } + + return ok.RowsAffected() > 0, nil +} + +// 更新一条表计的详细信息 +func (mr _MeterRepository) UpdateMeter(tx pgx.Tx, ctx context.Context, pid, code string, detail *vo.MeterModificationForm) (bool, error) { + mr.log.Info("更新一条表计的详细信息", zap.String("park id", pid), zap.String("meter code", code)) + timeNow := types.Now() + meterSql, meterArgs, _ := mr.ds. + Update(goqu.T("meter_04kv")). + Set( + goqu.Record{ + "address": detail.Address, + "seq": detail.Seq, + "ratio": detail.Ratio, + "enabled": detail.Enabled, + "meter_type": detail.MeterType, + "building": detail.Building, + "on_floor": detail.OnFloor, + "area": detail.Area, + "last_modified_at": timeNow, + }, + ). + Where( + goqu.I("park_id").Eq(pid), + goqu.I("code").Eq(code), + ). + Prepared(true).ToSQL() + + ok, err := tx.Exec(ctx, meterSql, meterArgs...) + if err != nil { + mr.log.Error("更新表计信息失败", zap.Error(err)) + return false, err + } + + if ok.RowsAffected() > 0 { + cache.AbolishRelation(fmt.Sprintf("meter:%s", pid)) + } + + return ok.RowsAffected() > 0, nil +} + +// 列出指定园区中已经存在的表计编号,无论该表计是否已经不再使用。 +func (mr _MeterRepository) ListMeterCodes(pid string) ([]string, error) { + mr.log.Info("列出指定园区中已经存在的表计编号", zap.String("park id", pid)) + cacheConditions := []string{pid} + if codes, err := cache.RetrieveSearch[[]string]("meter_codes", cacheConditions...); err == nil { + mr.log.Info("从缓存中获取到了指定园区中的表计编号", zap.Int("count", len(*codes))) + return *codes, nil + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + var codes []string + codesSql, codesArgs, _ := mr.ds. + From(goqu.T("meter_04kv")). + Select("code"). + Where( + goqu.I("park_id").Eq(pid), + ). + Order(goqu.I("seq").Asc()). + Prepared(true).ToSQL() + + if err := pgxscan.Select(ctx, global.DB, &codes, codesSql, codesArgs...); err != nil { + mr.log.Error("查询表计编号失败", zap.Error(err)) + return make([]string, 0), err + } + + cache.CacheSearch(codes, []string{fmt.Sprintf("meter:%s", pid), fmt.Sprintf("park:%s", pid)}, "meter_codes", cacheConditions...) + + return codes, nil +} + +// 解除指定园区中指定表计的使用 +func (mr _MeterRepository) DetachMeter(tx pgx.Tx, ctx context.Context, pid, code string) (bool, error) { + mr.log.Info("解除指定园区中指定表计的使用", zap.String("park id", pid), zap.String("meter code", code)) + timeNow := types.Now() + meterSql, meterArgs, _ := mr.ds. + Update(goqu.T("meter_04kv")). + Set( + goqu.Record{ + "detached_at": timeNow, + "last_modified_at": timeNow, + }, + ). + Where( + goqu.I("park_id").Eq(pid), + goqu.I("code").Eq(code), + ). + Prepared(true).ToSQL() + + ok, err := tx.Exec(ctx, meterSql, meterArgs...) + if err != nil { + mr.log.Error("解除表计使用失败", zap.Error(err)) + return false, err + } + + if ok.RowsAffected() > 0 { + cache.AbolishRelation(fmt.Sprintf("meter:%s", pid)) + cache.AbolishRelation(fmt.Sprintf("meter_relations:%s:%s", pid, code)) + } + + return ok.RowsAffected() > 0, nil +} + +// 将商户表计绑定到公摊表计上 +func (mr _MeterRepository) BindMeter(tx pgx.Tx, ctx context.Context, pid, masterMeter, slaveMeter string) (bool, error) { + mr.log.Info("将商户表计绑定到公摊表计上", zap.String("master meter code", masterMeter), zap.String("slave meter code", slaveMeter)) + masterDetail, err := mr.FetchMeterDetail(pid, masterMeter) + if err != nil { + mr.log.Error("查询公摊表计信息失败", zap.Error(err)) + return false, err + } + if masterDetail.MeterType != model.METER_INSTALLATION_POOLING { + mr.log.Error("给定的公摊表计不是公摊表计", zap.Error(err)) + return false, fmt.Errorf("给定的公摊表计不是公摊表计") + } + slaveDetail, err := mr.FetchMeterDetail(pid, slaveMeter) + if err != nil { + mr.log.Error("查询商户表计信息失败", zap.Error(err)) + return false, err + } + if slaveDetail.MeterType != model.METER_INSTALLATION_TENEMENT { + mr.log.Error("给定的商户表计不是商户表计", zap.Error(err)) + return false, fmt.Errorf("给定的商户表计不是商户表计") + } + + timeNow := types.Now() + serial.StringSerialRequestChan <- 1 + code := serial.Prefix("PB", <-serial.StringSerialResponseChan) + relationSql, relationArgs, _ := mr.ds. + Insert(goqu.T("meter_relations")). + Cols( + "id", "park_id", "master_meter_id", "slave_meter_id", "established_at", + ). + Vals( + goqu.Vals{ + code, + pid, + masterMeter, + slaveMeter, + timeNow, + }, + ). + Prepared(true).ToSQL() + + ok, err := tx.Exec(ctx, relationSql, relationArgs...) + if err != nil { + mr.log.Error("绑定表计关系失败", zap.Error(err)) + return false, err + } + + if ok.RowsAffected() > 0 { + cache.AbolishRelation(fmt.Sprintf("meter_relations:%s:%s", pid, masterMeter)) + } + + return ok.RowsAffected() > 0, nil +} + +// 解除两个表计之间的关联 +func (mr _MeterRepository) UnbindMeter(tx pgx.Tx, ctx context.Context, pid, masterMeter, slaveMeter string) (bool, error) { + mr.log.Info("解除两个表计之间的关联", zap.String("master meter code", masterMeter), zap.String("slave meter code", slaveMeter)) + relationSql, relationArgs, _ := mr.ds. + Update(goqu.T("meter_relations")). + Set( + goqu.Record{ + "revoked_at": types.Now(), + }, + ). + Where( + goqu.I("park_id").Eq(pid), + goqu.I("master_meter_id").Eq(masterMeter), + goqu.I("slave_meter_id").Eq(slaveMeter), + goqu.I("revoke_at").IsNull(), + ). + Prepared(true).ToSQL() + + ok, err := tx.Exec(ctx, relationSql, relationArgs...) + if err != nil { + mr.log.Error("解除表计关系失败", zap.Error(err)) + return false, err + } + + if ok.RowsAffected() > 0 { + cache.AbolishRelation(fmt.Sprintf("meter_relations:%s:%s", pid, masterMeter)) + } + + return ok.RowsAffected() > 0, nil +} + +// 列出指定公摊表计的所有关联表计关系 +func (mr _MeterRepository) ListPooledMeterRelations(pid, code string) ([]*model.MeterRelation, error) { + mr.log.Info("列出指定公摊表计的所有关联表计关系", zap.String("park id", pid), zap.String("meter code", code)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + var relations []*model.MeterRelation + relationsSql, relationsArgs, _ := mr.ds. + From(goqu.T("meter_relations")). + Select("*"). + Where( + goqu.I("r.park_id").Eq(pid), + goqu.I("r.master_meter_id").Eq(code), + goqu.I("r.revoke_at").IsNull(), + ). + Prepared(true).ToSQL() + + if err := pgxscan.Select(ctx, global.DB, &relations, relationsSql, relationsArgs...); err != nil { + mr.log.Error("查询表计关系失败", zap.Error(err)) + return make([]*model.MeterRelation, 0), err + } + + return relations, nil +} + +// 列出指定公摊表计列表所包含的全部关联表计关系 +func (mr _MeterRepository) ListPooledMeterRelationsByCodes(pid string, codes []string) ([]*model.MeterRelation, error) { + mr.log.Info("列出指定公摊表计列表所包含的全部关联表计关系", zap.String("park id", pid), zap.Strings("meter codes", codes)) + cacheConditions := []string{ + pid, + strings.Join(codes, ","), + } + if relations, err := cache.RetrieveSearch[[]*model.MeterRelation]("meter_relations", cacheConditions...); err == nil { + mr.log.Info("从缓存中获取到了所需的关联表计信息", zap.Int("count", len(*relations))) + return *relations, nil + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + var relations []*model.MeterRelation + relationsSql, relationsArgs, _ := mr.ds. + From(goqu.T("meter_relations")). + Select("*"). + Where( + goqu.I("r.park_id").Eq(pid), + goqu.I("r.master_meter_id").Eq(goqu.Func("any", codes)), + goqu.I("r.revoke_at").IsNull(), + ). + Prepared(true).ToSQL() + + if err := pgxscan.Select(ctx, global.DB, &relations, relationsSql, relationsArgs...); err != nil { + mr.log.Error("查询表计关系失败", zap.Error(err)) + return make([]*model.MeterRelation, 0), err + } + + return relations, nil +} + +// 列出指定商户表计、园区表计与公摊表计之间的关联关系 +func (mr _MeterRepository) ListMeterRelations(pid, code string) ([]*model.MeterRelation, error) { + mr.log.Info("列出指定商户表计、园区表计与公摊表计之间的关联关系", zap.String("park id", pid), zap.String("meter code", code)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + var relations []*model.MeterRelation + relationsSql, relationsArgs, _ := mr.ds. + From(goqu.T("meter_relations")). + Select("*"). + Where( + goqu.I("r.park_id").Eq(pid), + goqu.I("r.slave_meter_id").Eq(code), + goqu.I("r.revoke_at").IsNull(), + ). + Prepared(true).ToSQL() + + if err := pgxscan.Select(ctx, global.DB, &relations, relationsSql, relationsArgs...); err != nil { + mr.log.Error("查询表计关系失败", zap.Error(err)) + return make([]*model.MeterRelation, 0), err + } + + return relations, nil +} + +// 列出指定园区中的所有公摊表计 +func (mr _MeterRepository) ListPoolingMeters(pid string, page uint, keyword *string) ([]*model.MeterDetail, int64, error) { + mr.log.Info("列出指定园区中的所有公摊表计", zap.String("park id", pid), zap.Uint("page", page), zap.String("keyword", tools.DefaultTo(keyword, ""))) + cacheConditions := []string{ + pid, + tools.DefaultOrEmptyStr(keyword, "UNDEF"), + fmt.Sprintf("%d", page), + } + if meters, total, err := cache.RetrievePagedSearch[[]*model.MeterDetail]("pooling_meters", cacheConditions...); err == nil { + mr.log.Info("从缓存中获取到了指定园区中的公摊表计信息", zap.Int("count", len(*meters)), zap.Int64("total", total)) + return *meters, total, nil + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + meterQuery := mr.ds. + From(goqu.T("meter_04kv").As("m")). + LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))). + Select( + "m.*", goqu.I("b.name").As("building_name"), + ). + Where( + goqu.I("m.park_id").Eq(pid), + goqu.I("m.enabled").IsTrue(), + goqu.I("m.meter_type").Eq(model.METER_INSTALLATION_POOLING), + ) + countQuery := mr.ds. + From(goqu.T("meter_04kv").As("m")). + Select(goqu.COUNT("*")). + Where( + goqu.I("m.park_id").Eq(pid), + goqu.I("m.enabled").IsTrue(), + goqu.I("m.meter_type").Eq(model.METER_INSTALLATION_POOLING), + ) + + if keyword != nil && len(*keyword) > 0 { + pattern := fmt.Sprintf("%%%s%%", *keyword) + meterQuery = meterQuery.Where( + goqu.Or( + goqu.I("m.code").ILike(pattern), + goqu.I("m.address").ILike(pattern), + ), + ) + countQuery = countQuery.Where( + goqu.Or( + goqu.I("m.code").ILike(pattern), + goqu.I("m.address").ILike(pattern), + ), + ) + } + + startRow := (page - 1) * config.ServiceSettings.ItemsPageSize + meterQuery = meterQuery.Order(goqu.I("m.code").Asc()).Offset(startRow).Limit(config.ServiceSettings.ItemsPageSize) + + meterSql, meterArgs, _ := meterQuery.Prepared(true).ToSQL() + countSql, countArgs, _ := countQuery.Prepared(true).ToSQL() + + var ( + meters []*model.MeterDetail + total int64 + ) + if err := pgxscan.Select(ctx, global.DB, &meters, meterSql, meterArgs...); err != nil { + mr.log.Error("查询公摊表计信息失败", zap.Error(err)) + return make([]*model.MeterDetail, 0), 0, err + } + if err := pgxscan.Get(ctx, global.DB, &total, countSql, countArgs...); err != nil { + mr.log.Error("查询公摊表计数量失败", zap.Error(err)) + return make([]*model.MeterDetail, 0), 0, err + } + + cache.CachePagedSearch(meters, total, []string{fmt.Sprintf("meter:%s", pid), "park"}, "pooling_meters", cacheConditions...) + + return meters, total, nil +} + +// 列出目前尚未绑定到公摊表计的商户表计 +func (mr _MeterRepository) ListUnboundMeters(uid string, pid *string, keyword *string, limit *uint) ([]*model.MeterDetail, error) { + mr.log.Info("列出目前尚未绑定到公摊表计的商户表计", zap.Stringp("park id", pid), zap.String("user id", uid), zap.String("keyword", tools.DefaultTo(keyword, "")), zap.Uint("limit", tools.DefaultTo(limit, 0))) + cacheConditions := []string{ + tools.DefaultTo(pid, "UNDEF"), + tools.DefaultOrEmptyStr(keyword, "UNDEF"), + tools.DefaultStrTo("%d", limit, "0"), + } + if meters, err := cache.RetrieveSearch[[]*model.MeterDetail]("unbound_pooling_meters", cacheConditions...); err == nil { + mr.log.Info("从缓存中获取到了指定园区中的商户表计信息", zap.Int("count", len(*meters))) + return *meters, nil + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + meterQuery := mr.ds. + From(goqu.T("meter_04kv").As("m")). + LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))). + Select( + "m.*", goqu.I("b.name").As("building_name"), + ). + Where( + goqu.I("m.meter_type").Eq(model.METER_INSTALLATION_TENEMENT), + goqu.I("m.enabled").IsTrue(), + ) + + if pid != nil && len(*pid) > 0 { + meterQuery = meterQuery.Where( + goqu.I("m.park_id").Eq(*pid), + ) + } + + if keyword != nil && len(*keyword) > 0 { + pattern := fmt.Sprintf("%%%s%%", *keyword) + meterQuery = meterQuery.Where( + goqu.Or( + goqu.I("m.code").ILike(pattern), + goqu.I("m.address").ILike(pattern), + ), + ) + } + + slaveMeterQuery := mr.ds. + From("meter_relations"). + Select("id") + if pid != nil && len(*pid) > 0 { + slaveMeterQuery = slaveMeterQuery.Where( + goqu.I("park_id").Eq(*pid), + ) + } else { + slaveMeterQuery = slaveMeterQuery.Where( + goqu.I("park_id").In( + mr.ds. + From("park"). + Select("id"). + Where(goqu.I("user_id").Eq(uid)), + )) + } + slaveMeterQuery = slaveMeterQuery.Where( + goqu.I("revoke_at").IsNull(), + ) + meterQuery = meterQuery.Where( + goqu.I("m.code").NotIn(slaveMeterQuery), + ). + Order(goqu.I("m.attached_at").Asc()) + + if limit != nil && *limit > 0 { + meterQuery = meterQuery.Limit(*limit) + } + + meterSql, meterArgs, _ := meterQuery.Prepared(true).ToSQL() + var meters []*model.MeterDetail + if err := pgxscan.Select(ctx, global.DB, &meters, meterSql, meterArgs...); err != nil { + mr.log.Error("查询商户表计信息失败", zap.Error(err)) + return make([]*model.MeterDetail, 0), err + } + + cache.CacheSearch(meters, []string{fmt.Sprintf("meter:%s", tools.DefaultTo(pid, "ALL")), "park"}, "unbound_pooling_meters", cacheConditions...) + + return meters, nil +} + +// 列出目前未绑定到商户的商户表计 +func (mr _MeterRepository) ListUnboundTenementMeters(uid string, pid *string, keyword *string, limit *uint) ([]*model.MeterDetail, error) { + mr.log.Info("列出目前未绑定到商户的商户表计", zap.Stringp("park id", pid), zap.String("user id", uid), zap.String("keyword", tools.DefaultTo(keyword, "")), zap.Uint("limit", tools.DefaultTo(limit, 0))) + cacheConditions := []string{ + tools.DefaultTo(pid, "UNDEF"), + tools.DefaultOrEmptyStr(keyword, "UNDEF"), + tools.DefaultStrTo("%d", limit, "0"), + } + if meters, err := cache.RetrieveSearch[[]*model.MeterDetail]("unbound_tenement_meters", cacheConditions...); err == nil { + mr.log.Info("从缓存中获取到了指定园区中的商户表计信息", zap.Int("count", len(*meters))) + return *meters, nil + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + meterQuery := mr.ds. + From(goqu.T("meter_04kv").As("m")). + LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))). + Select( + "m.*", goqu.I("b.name").As("building_name"), + ). + Where( + goqu.I("m.meter_type").Eq(model.METER_INSTALLATION_TENEMENT), + goqu.I("m.enabled").IsTrue(), + ) + + if pid != nil && len(*pid) > 0 { + meterQuery = meterQuery.Where( + goqu.I("m.park_id").Eq(*pid), + ) + } + + if keyword != nil && len(*keyword) > 0 { + pattern := fmt.Sprintf("%%%s%%", *keyword) + meterQuery = meterQuery.Where( + goqu.Or( + goqu.I("m.code").ILike(pattern), + goqu.I("m.address").ILike(pattern), + ), + ) + } + + subMeterQuery := mr.ds. + From("tenement_meter"). + Select("meter_id") + if pid != nil && len(*pid) > 0 { + subMeterQuery = subMeterQuery.Where( + goqu.I("park_id").Eq(*pid), + ) + } else { + subMeterQuery = subMeterQuery.Where( + goqu.I("park_id").In( + mr.ds. + From("park"). + Select("id"). + Where(goqu.I("user_id").Eq(uid)), + )) + } + subMeterQuery = subMeterQuery.Where( + goqu.I("disassociated_at").IsNull(), + ) + meterQuery = meterQuery.Where( + goqu.I("m.code").NotIn(subMeterQuery), + ). + Order(goqu.I("m.attached_at").Asc()) + + if limit != nil && *limit > 0 { + meterQuery = meterQuery.Limit(*limit) + } + + meterSql, meterArgs, _ := meterQuery.Prepared(true).ToSQL() + var meters []*model.MeterDetail + if err := pgxscan.Select(ctx, global.DB, &meters, meterSql, meterArgs...); err != nil { + mr.log.Error("查询商户表计信息失败", zap.Error(err)) + return make([]*model.MeterDetail, 0), err + } + + cache.CacheSearch(meters, []string{fmt.Sprintf("meter:%s", tools.DefaultTo(pid, "ALL")), "park"}, "unbound_tenement_meters", cacheConditions...) + + return meters, nil +} + +// 查询指定园区中的符合条件的抄表记录 +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, ""))) + cacheConditions := []string{ + pid, + tools.DefaultOrEmptyStr(keyword, "UNDEF"), + 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"), + } + if readings, total, err := cache.RetrievePagedSearch[[]*model.MeterReading]("meter_reading", cacheConditions...); err == nil { + mr.log.Info("从缓存中获取到了指定园区中的抄表记录", zap.Int("count", len(*readings)), zap.Int64("total", total)) + return *readings, total, nil + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + 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")))). + Where( + goqu.I("r.park_id").Eq(pid), + ) + countQuery := 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(goqu.COUNT("*")). + Where( + goqu.I("r.park_id").Eq(pid), + ) + + if keyword != nil && len(*keyword) > 0 { + pattern := fmt.Sprintf("%%%s%%", *keyword) + readingQuery = readingQuery.Where( + goqu.Or( + goqu.I("m.code").ILike(pattern), + goqu.I("m.address").ILike(pattern), + ), + ) + countQuery = countQuery.Where( + goqu.Or( + goqu.I("m.code").ILike(pattern), + goqu.I("m.address").ILike(pattern), + ), + ) + } + + if start != nil { + readingQuery = readingQuery.Where( + goqu.I("r.read_at").Gte(start.ToBeginningOfDate()), + ) + countQuery = countQuery.Where( + goqu.I("r.read_at").Gte(start.ToBeginningOfDate()), + ) + } + + if end != nil { + readingQuery = readingQuery.Where( + goqu.I("r.read_at").Lte(end.ToEndingOfDate()), + ) + countQuery = countQuery.Where( + goqu.I("r.read_at").Lte(end.ToEndingOfDate()), + ) + } + + if buidling != nil && len(*buidling) > 0 { + readingQuery = readingQuery.Where( + goqu.I("m.building").Eq(*buidling), + ) + countQuery = countQuery.Where( + goqu.I("m.building").Eq(*buidling), + ) + } + + startRow := (page - 1) * config.ServiceSettings.ItemsPageSize + readingQuery = readingQuery.Order(goqu.I("r.read_at").Desc()).Offset(startRow).Limit(config.ServiceSettings.ItemsPageSize) + + readingSql, readingArgs, _ := readingQuery.Prepared(true).ToSQL() + countSql, countArgs, _ := countQuery.Prepared(true).ToSQL() + + var ( + readings []*model.MeterReading + total int64 + ) + if err := pgxscan.Select(ctx, global.DB, &readings, readingSql, readingArgs...); err != nil { + mr.log.Error("查询抄表记录失败", zap.Error(err)) + return make([]*model.MeterReading, 0), 0, err + } + if err := pgxscan.Get(ctx, global.DB, &total, countSql, countArgs...); err != nil { + mr.log.Error("查询抄表记录数量失败", zap.Error(err)) + return make([]*model.MeterReading, 0), 0, err + } + + cache.CachePagedSearch(readings, total, []string{fmt.Sprintf("meter_reading:%s", pid), "park"}, "meter_reading", cacheConditions...) + + return readings, total, nil +} + +// 修改指定表计的指定抄表记录 +func (mr _MeterRepository) UpdateMeterReading(pid, mid string, readAt types.DateTime, reading *vo.MeterReadingForm) (bool, error) { + mr.log.Info("修改指定表计的指定抄表记录", zap.String("park id", pid), zap.String("meter id", mid), logger.DateTimeField("read at", readAt), zap.Any("reading", reading)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + updateSql, updateArgs, _ := mr.ds. + Update(goqu.T("meter_reading")). + Set( + goqu.Record{ + "overall": reading.Overall, + "critical": reading.Critical, + "peak": reading.Peak, + "flat": reading.Flat, + "valley": reading.Valley, + }, + ). + Where( + goqu.I("park_id").Eq(pid), + goqu.I("meter_id").Eq(mid), + goqu.I("read_at").Eq(readAt), + ). + Prepared(true).ToSQL() + + ok, err := global.DB.Exec(ctx, updateSql, updateArgs...) + if err != nil { + mr.log.Error("更新抄表记录失败", zap.Error(err)) + return false, err + } + + if ok.RowsAffected() > 0 { + cache.AbolishRelation(fmt.Sprintf("meter_reading:%s", pid)) + } + + return ok.RowsAffected() > 0, nil +} + +// 列出指定园区中指定时间区域内的所有表计抄表记录 +func (mr _MeterRepository) ListMeterReadingsByTimeRange(pid string, start, end types.Date) ([]*model.MeterReading, error) { + mr.log.Info("列出指定园区中指定时间区域内的所有表计抄表记录", zap.String("park id", pid), zap.Time("start", start.Time), zap.Time("end", end.Time)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + var readings []*model.MeterReading + readingSql, readingArgs, _ := mr.ds. + From(goqu.T("meter_reading").As("r")). + Select("*"). + Where( + goqu.I("r.park_id").Eq(pid), + goqu.I("r.read_at").Gte(start.ToBeginningOfDate()), + goqu.I("r.read_at").Lte(end.ToEndingOfDate()), + ). + Order(goqu.I("r.read_at").Desc()). + Prepared(true).ToSQL() + + if err := pgxscan.Select(ctx, global.DB, &readings, readingSql, readingArgs...); err != nil { + mr.log.Error("查询抄表记录失败", zap.Error(err)) + return make([]*model.MeterReading, 0), err + } + + return readings, nil +} + +// 列出指定园区中在指定日期之前的最后一次抄表记录 +func (mr _MeterRepository) ListLastMeterReading(pid string, date types.Date) ([]*model.MeterReading, error) { + mr.log.Info("列出指定园区中在指定日期之前的最后一次抄表记录", zap.String("park id", pid), zap.Time("date", date.Time)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + var readings []*model.MeterReading + readingSql, readingArgs, _ := mr.ds. + From(goqu.T("meter_reading")). + Select( + goqu.MAX("read_at").As("read_at"), + "park_id", "meter_id", "overall", "critical", "peak", "flat", "valley", + ). + Where( + goqu.I("park_id").Eq(pid), + goqu.I("read_at").Lt(date.ToEndingOfDate()), + ). + GroupBy("park_id", "meter_id", "overall", "critical", "peak", "flat", "valley"). + Order(goqu.I("read_at").Desc()). + Limit(1). + Prepared(true).ToSQL() + + if err := pgxscan.Select(ctx, global.DB, &readings, readingSql, readingArgs...); err != nil { + mr.log.Error("查询抄表记录失败", zap.Error(err)) + return make([]*model.MeterReading, 0), err + } + + return readings, nil +} + +// 列出指定园区中的表计与商户的关联详细记录,用于写入Excel模板文件 +func (mr _MeterRepository) ListMeterDocForTemplate(pid string) ([]*model.SimpleMeterDocument, error) { + mr.log.Info("列出指定园区中的表计与商户的关联详细记录", zap.String("park id", pid)) + cacheConditions := []string{pid} + if docs, err := cache.RetrieveSearch[[]*model.SimpleMeterDocument]("simple_meter_doc", cacheConditions...); err == nil { + mr.log.Info("从缓存中获取到了指定园区中的表计与商户的关联详细记录", zap.Int("count", len(*docs))) + return *docs, nil + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + var docs []*model.SimpleMeterDocument + docSql, docArgs, _ := mr.ds. + From(goqu.T("meter_04kv").As("m")). + LeftJoin( + goqu.T("tenement_meter").As("tm"), + goqu.On( + goqu.I("m.code").Eq(goqu.I("tm.meter_id")), + goqu.I("m.park_id").Eq(goqu.I("tm.park_id")), + ), + ). + LeftJoin( + goqu.T("tenement").As("t"), + goqu.On( + goqu.I("tm.tenement_id").Eq(goqu.I("t.id")), + goqu.I("tm.park_id").Eq(goqu.I("t.park_id")), + ), + ). + Select( + "m.code", "m.address", "m.ratio", "m.seq", goqu.I("t.full_name").As("tenement_name"), + ). + Where( + goqu.I("m.park_id").Eq(pid), + goqu.I("m.enabled").IsTrue(), + goqu.I("m.disassociated_at").IsNull(), + ). + Order(goqu.I("m.seq").Asc()). + Prepared(true).ToSQL() + + if err := pgxscan.Select(ctx, global.DB, &docs, docSql, docArgs...); err != nil { + mr.log.Error("查询表计与商户关联信息失败", zap.Error(err)) + return make([]*model.SimpleMeterDocument, 0), err + } + + cache.CacheSearch(docs, []string{fmt.Sprintf("park:%s", pid), fmt.Sprintf("meter:%s", pid), "park"}, "simple_meter_doc", cacheConditions...) + + return docs, nil +} diff --git a/router/router.go b/router/router.go index 73e779f..02b8f2f 100644 --- a/router/router.go +++ b/router/router.go @@ -48,6 +48,7 @@ func App() *fiber.App { controller.InitializeRegionHandlers(app) controller.InitializeChargeHandlers(app) controller.InitializeParkHandlers(app) + controller.InitializeMeterHandlers(app) return app } diff --git a/service/meter.go b/service/meter.go new file mode 100644 index 0000000..ebb428e --- /dev/null +++ b/service/meter.go @@ -0,0 +1,539 @@ +package service + +import ( + "electricity_bill_calc/cache" + "electricity_bill_calc/global" + "electricity_bill_calc/logger" + "electricity_bill_calc/model" + "electricity_bill_calc/repository" + "electricity_bill_calc/tools" + "electricity_bill_calc/types" + "electricity_bill_calc/vo" + "fmt" + "mime/multipart" + + "github.com/samber/lo" + "github.com/shopspring/decimal" + "go.uber.org/zap" +) + +type _MeterService struct { + log *zap.Logger +} + +var MeterService = _MeterService{ + log: logger.Named("Service", "Meter"), +} + +// 创建一条新的表计记录 +func (ms _MeterService) CreateMeterRecord(pid string, form *vo.MeterCreationForm) error { + ms.log.Info("创建一条新的表计记录", zap.String("park id", pid)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + tx, err := global.DB.Begin(ctx) + if err != nil { + ms.log.Error("无法启动数据库事务。", zap.Error(err)) + return err + } + + ok, err := repository.MeterRepository.CreateMeter(tx, ctx, pid, *form) + if err != nil { + ms.log.Error("无法创建一条新的表计记录。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + if !ok { + ms.log.Error("数据库未能记录新的表计记录。") + tx.Rollback(ctx) + return err + } + + ok, err = repository.MeterRepository.RecordReading(tx, ctx, pid, form.Code, form.MeterType, form.Ratio, &form.MeterReadingForm) + if err != nil { + ms.log.Error("无法记录表计读数。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + if !ok { + ms.log.Error("数据库未能记录表计读数。") + tx.Rollback(ctx) + return err + } + + err = tx.Commit(ctx) + if err != nil { + ms.log.Error("未能成功提交数据库事务。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + + cache.AbolishRelation(fmt.Sprintf("meter:%s", pid)) + return nil +} + +// 更新指定表计的信息 +func (ms _MeterService) UpdateMeterRecord(pid string, code string, form *vo.MeterModificationForm) error { + ms.log.Info("更新指定表计的信息", zap.String("park id", pid), zap.String("meter code", code)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + tx, err := global.DB.Begin(ctx) + if err != nil { + ms.log.Error("无法启动数据库事务。", zap.Error(err)) + return err + } + + ok, err := repository.MeterRepository.UpdateMeter(tx, ctx, pid, code, form) + if err != nil { + ms.log.Error("无法更新指定表计的信息。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + if !ok { + ms.log.Error("数据库未能更新指定表计的信息。") + tx.Rollback(ctx) + return err + } + + err = tx.Commit(ctx) + if err != nil { + ms.log.Error("未能成功提交数据库事务。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + + cache.AbolishRelation(fmt.Sprintf("meter:%s", pid)) + return nil +} + +// 处理上传的Excel格式表计档案文件,根据表号自动更新数据库 +func (ms _MeterService) BatchImportMeters(pid string, file multipart.FileHeader) error { + return nil +} + +// 更换系统中的表计 +func (ms _MeterService) ReplaceMeter( + pid string, + oldMeterCode string, + oldMeterReading *vo.MeterReadingForm, + newMeterCode string, + newMeterRatio decimal.Decimal, + newMeterReading *vo.MeterReadingForm, +) error { + ms.log.Info("更换系统中的表计", zap.String("park id", pid), zap.String("old meter code", oldMeterCode), zap.String("new meter code", newMeterCode)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + tx, err := global.DB.Begin(ctx) + if err != nil { + ms.log.Error("无法启动数据库事务。", zap.Error(err)) + return err + } + + // 步骤1:读取旧表信息 + oldMeter, err := repository.MeterRepository.FetchMeterDetail(pid, oldMeterCode) + if err != nil { + ms.log.Error("无法读取旧表信息。", zap.Error(err)) + tx.Rollback(ctx) + return fmt.Errorf("要替换的旧表计不存在:%w", err) + } + + // 步骤2:写入旧表读数 + ok, err := repository.MeterRepository.RecordReading(tx, ctx, pid, oldMeterCode, oldMeter.MeterType, oldMeter.Ratio, oldMeterReading) + switch { + case err != nil: + ms.log.Error("无法写入旧表读数。", zap.Error(err)) + tx.Rollback(ctx) + return err + case !ok: + ms.log.Error("数据库未能写入旧表读数。") + tx.Rollback(ctx) + return fmt.Errorf("旧表计读数未能成功保存到数据库。") + } + + // 步骤3:从系统移除旧表计 + ok, err = repository.MeterRepository.DetachMeter(tx, ctx, pid, oldMeterCode) + switch { + case err != nil: + ms.log.Error("无法从系统移除旧表计。", zap.Error(err)) + tx.Rollback(ctx) + return err + case !ok: + ms.log.Error("未能从系统移除旧表计。") + tx.Rollback(ctx) + return fmt.Errorf("旧表计未能成功从系统移除。") + } + + // 步骤4:获取旧表计的关联信息 + var oldRelations []*model.MeterRelation + switch oldMeter.MeterType { + case model.METER_INSTALLATION_POOLING: + oldRelations, err = repository.MeterRepository.ListPooledMeterRelations(pid, oldMeterCode) + if err != nil { + ms.log.Error("无法获取旧表计的关联信息。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + default: + oldRelations, err = repository.MeterRepository.ListMeterRelations(pid, oldMeterCode) + if err != nil { + ms.log.Error("无法获取旧表计的关联信息。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + } + + // 步骤5:将旧表计的关联信息设置为解除 + for _, relation := range oldRelations { + ok, err = repository.MeterRepository.UnbindMeter(tx, ctx, pid, relation.MasterMeter, relation.SlaveMeter) + switch { + case err != nil: + ms.log.Error("无法将旧表计的关联信息设置为解除。", zap.String("master meter", relation.MasterMeter), zap.String("slave meter", relation.SlaveMeter), zap.Error(err)) + tx.Rollback(ctx) + return err + case !ok: + ms.log.Error("未能将旧表计的关联信息设置为解除。", zap.String("master meter", relation.MasterMeter), zap.String("slave meter", relation.SlaveMeter)) + tx.Rollback(ctx) + return fmt.Errorf("旧表计的关联信息未能成功设置为解除。") + } + } + + // 步骤6:将旧表计的部分信息赋予新表计 + newMeterCreationForm := vo.MeterCreationForm{ + Code: newMeterCode, + Address: oldMeter.Address, + MeterType: oldMeter.MeterType, + Ratio: newMeterRatio, + Seq: oldMeter.Seq, + Enabled: oldMeter.Enabled, + Building: oldMeter.Building, + OnFloor: oldMeter.OnFloor, + Area: oldMeter.Area, + MeterReadingForm: *newMeterReading, + } + + // 步骤7:将新表计写入系统 + ok, err = repository.MeterRepository.CreateMeter(tx, ctx, pid, newMeterCreationForm) + switch { + case err != nil: + ms.log.Error("无法将新表计写入系统。", zap.Error(err)) + tx.Rollback(ctx) + return err + case !ok: + ms.log.Error("未能将新表计写入系统。") + tx.Rollback(ctx) + return fmt.Errorf("新表计未能成功写入系统。") + } + + // 步骤8:将新表计的读数写入系统 + ok, err = repository.MeterRepository.RecordReading(tx, ctx, pid, newMeterCode, newMeterCreationForm.MeterType, newMeterCreationForm.Ratio, &newMeterCreationForm.MeterReadingForm) + switch { + case err != nil: + ms.log.Error("无法将新表计的读数写入系统。", zap.Error(err)) + tx.Rollback(ctx) + return err + case !ok: + ms.log.Error("未能将新表计的读数写入系统。") + tx.Rollback(ctx) + return fmt.Errorf("新表计的读数未能成功写入系统。") + } + + // 步骤9:将旧表计的关联信息复制一份赋予新表计 + switch oldMeter.MeterType { + case model.METER_INSTALLATION_POOLING: + for _, relation := range oldRelations { + ok, err = repository.MeterRepository.BindMeter(tx, ctx, pid, newMeterCode, relation.SlaveMeter) + switch { + case err != nil: + ms.log.Error("无法将旧表计的关联信息赋予新表计。", zap.String("master meter", newMeterCode), zap.String("slave meter", relation.SlaveMeter), zap.Error(err)) + tx.Rollback(ctx) + return err + case !ok: + ms.log.Error("未能将旧表计的关联信息赋予新表计。", zap.String("master meter", newMeterCode), zap.String("slave meter", relation.SlaveMeter)) + tx.Rollback(ctx) + return fmt.Errorf("旧表计的关联信息未能成功赋予新表计。") + } + } + default: + for _, relation := range oldRelations { + ok, err = repository.MeterRepository.BindMeter(tx, ctx, pid, relation.MasterMeter, newMeterCode) + switch { + case err != nil: + ms.log.Error("无法将旧表计的关联信息赋予新表计。", zap.String("master meter", relation.MasterMeter), zap.String("slave meter", newMeterCode), zap.Error(err)) + tx.Rollback(ctx) + return err + case !ok: + ms.log.Error("未能将旧表计的关联信息赋予新表计。", zap.String("master meter", relation.MasterMeter), zap.String("slave meter", newMeterCode)) + tx.Rollback(ctx) + return fmt.Errorf("旧表计的关联信息未能成功赋予新表计。") + } + } + } + + // 步骤10:提交事务 + err = tx.Commit(ctx) + if err != nil { + ms.log.Error("未能成功提交数据库事务。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + cache.AbolishRelation(fmt.Sprintf("meter:%s", pid)) + + return nil +} + +// 列出园区中指定公摊表计下的所有关联表计 +func (ms _MeterService) ListPooledMeterRelations(pid, masterMeter string) ([]*model.MeterDetail, error) { + ms.log.Info("列出园区中指定公摊表计下的所有关联表计", zap.String("park id", pid), zap.String("meter code", masterMeter)) + relations, err := repository.MeterRepository.ListPooledMeterRelations(pid, masterMeter) + if err != nil { + ms.log.Error("无法列出园区中指定公摊表计下的所有关联关系。", zap.Error(err)) + return make([]*model.MeterDetail, 0), err + } + relatedMeterCodes := lo.Map(relations, func(element *model.MeterRelation, _ int) string { + return element.SlaveMeter + }) + meters, err := repository.MeterRepository.ListMetersByIDs(pid, relatedMeterCodes) + if err != nil { + ms.log.Error("无法列出园区中指定公摊表计下的所有关联表计详细信息。", zap.Error(err)) + return make([]*model.MeterDetail, 0), err + } + + return meters, nil +} + +// 列出指定园区中所有的公摊表计 +func (ms _MeterService) SearchPooledMetersDetail(pid string, page uint, keyword *string) ([]*model.PooledMeterDetailCompound, int64, error) { + ms.log.Info("列出指定园区中所有的公摊表计", zap.String("park id", pid), zap.Uint("page", page), zap.String("keyword", *keyword)) + cacheConditions := []string{ + pid, + fmt.Sprintf("%d", page), + tools.DefaultTo(keyword, "UNDEFINED"), + } + if meters, total, err := cache.RetrievePagedSearch[[]*model.PooledMeterDetailCompound]("assemble_pooled_meters_detail", cacheConditions...); err == nil { + ms.log.Info("已经从缓存中获取到了指定园区中所有的公摊表计。", zap.Int("count", len(*meters)), zap.Int64("total", total)) + return *meters, total, nil + } + + poolingMeters, total, err := repository.MeterRepository.ListPoolingMeters(pid, page, keyword) + if err != nil { + ms.log.Error("无法列出指定园区中所有的公摊表计。", zap.Error(err)) + return make([]*model.PooledMeterDetailCompound, 0), 0, err + } + poolingMeterIds := lo.Map(poolingMeters, func(element *model.MeterDetail, _ int) string { + return element.Code + }) + relations, err := repository.MeterRepository.ListPooledMeterRelationsByCodes(pid, poolingMeterIds) + if err != nil { + ms.log.Error("无法列出指定园区中所有的公摊表计关联关系。", zap.Error(err)) + return make([]*model.PooledMeterDetailCompound, 0), 0, err + } + slaveMeters, err := repository.MeterRepository.ListMetersByIDs(pid, lo.Map(relations, func(element *model.MeterRelation, _ int) string { + return element.SlaveMeter + })) + if err != nil { + ms.log.Error("无法列出指定园区中所有的公摊表计的关联表计详细信息。", zap.Error(err)) + return make([]*model.PooledMeterDetailCompound, 0), 0, err + } + var assembled []*model.PooledMeterDetailCompound = make([]*model.PooledMeterDetailCompound, 0) + for _, meter := range poolingMeters { + slaveIDs := lo.Map(lo.Filter( + relations, + func(element *model.MeterRelation, _ int) bool { + return element.MasterMeter == meter.Code + }), + func(element *model.MeterRelation, _ int) string { + return element.SlaveMeter + }, + ) + slaves := lo.Map(lo.Filter( + slaveMeters, + func(element *model.MeterDetail, _ int) bool { + return lo.Contains(slaveIDs, element.Code) + }), + func(element *model.MeterDetail, _ int) model.MeterDetail { + return *element + }, + ) + assembled = append(assembled, &model.PooledMeterDetailCompound{ + MeterDetail: *meter, + BindMeters: slaves, + }) + } + + cache.CachePagedSearch(assembled, total, []string{fmt.Sprintf("meter:%s", pid), fmt.Sprintf("meter_relation:%s", pid)}, "assemble_pooled_meter_detail", cacheConditions...) + + return assembled, total, nil +} + +// 批量向园区中指定公摊表计下绑定关联表计 +func (ms _MeterService) BindMeter(pid, masterMeter string, slaveMeters []string) (bool, error) { + ms.log.Info("批量向园区中指定公摊表计下绑定关联表计", zap.String("park id", pid), zap.String("master meter", masterMeter), zap.Strings("slave meters", slaveMeters)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + tx, err := global.DB.Begin(ctx) + if err != nil { + ms.log.Error("无法启动数据库事务。", zap.Error(err)) + return false, err + } + + for _, slave := range slaveMeters { + ok, err := repository.MeterRepository.BindMeter(tx, ctx, pid, masterMeter, slave) + switch { + case err != nil: + ms.log.Error("无法向园区中指定公摊表计下绑定关联表计。", zap.String("master meter", masterMeter), zap.String("slave meter", slave), zap.Error(err)) + tx.Rollback(ctx) + return false, err + case !ok: + ms.log.Error("未能向园区中指定公摊表计下绑定关联表计。", zap.String("master meter", masterMeter), zap.String("slave meter", slave)) + tx.Rollback(ctx) + return false, fmt.Errorf("未能成功向园区中指定公摊表计下绑定关联表计。") + } + } + + err = tx.Commit(ctx) + if err != nil { + ms.log.Error("未能成功提交数据库事务。", zap.Error(err)) + tx.Rollback(ctx) + return false, err + } + cache.AbolishRelation(fmt.Sprintf("meter:%s", pid)) + return true, nil +} + +// 批量解绑园区中指定表计下的指定表计 +func (ms _MeterService) UnbindMeter(pid, masterMeter string, slaveMeters []string) (bool, error) { + ms.log.Info("批量解绑园区中指定表计下的指定表计", zap.String("park id", pid), zap.String("master meter", masterMeter), zap.Strings("slave meters", slaveMeters)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + tx, err := global.DB.Begin(ctx) + if err != nil { + ms.log.Error("无法启动数据库事务。", zap.Error(err)) + return false, err + } + + for _, slave := range slaveMeters { + ok, err := repository.MeterRepository.UnbindMeter(tx, ctx, pid, masterMeter, slave) + switch { + case err != nil: + ms.log.Error("无法解绑园区中指定表计下的指定表计。", zap.String("master meter", masterMeter), zap.String("slave meter", slave), zap.Error(err)) + tx.Rollback(ctx) + return false, err + case !ok: + ms.log.Error("未能解绑园区中指定表计下的指定表计。", zap.String("master meter", masterMeter), zap.String("slave meter", slave)) + tx.Rollback(ctx) + return false, fmt.Errorf("未能成功解绑园区中指定表计下的指定表计。") + } + } + + err = tx.Commit(ctx) + if err != nil { + ms.log.Error("未能成功提交数据库事务。", zap.Error(err)) + tx.Rollback(ctx) + return false, err + } + cache.AbolishRelation(fmt.Sprintf("meter:%s", pid)) + return true, nil +} + +// 查询符合条件的表计读数记录 +func (ms _MeterService) SearchMeterReadings(pid string, building *string, start, end *types.Date, page uint, keyword *string) ([]*model.DetailedMeterReading, int64, error) { + ms.log.Info( + "查询符合条件的表计读数记录", + zap.String("park id", pid), + zap.Stringp("building", building), + logger.DateFieldp("start", start), + logger.DateFieldp("end", end), + zap.Uint("page", page), + zap.Stringp("keyword", keyword), + ) + readings, total, err := repository.MeterRepository.ListMeterReadings(pid, keyword, page, start, end, building) + if err != nil { + ms.log.Error("无法查询符合条件的表计读数记录。", zap.Error(err)) + return make([]*model.DetailedMeterReading, 0), 0, err + } + + meterCodes := lo.Map(readings, func(element *model.MeterReading, _ int) string { + return element.Meter + }) + meterDetails, err := repository.MeterRepository.ListMetersByIDs(pid, meterCodes) + if err != nil { + ms.log.Error("无法查询符合条件的表计读数记录的表计详细信息。", zap.Error(err)) + return make([]*model.DetailedMeterReading, 0), 0, err + } + assembles := lo.Map( + readings, + func(element *model.MeterReading, _ int) *model.DetailedMeterReading { + meter, _ := lo.Find(meterDetails, func(detail *model.MeterDetail) bool { + return detail.Code == element.Meter + }) + return &model.DetailedMeterReading{ + Detail: *meter, + Reading: *element, + } + }, + ) + + return assembles, total, nil +} + +// 创建一条新的表计抄表记录 +func (ms _MeterService) RecordReading(pid, meterCode string, form *vo.MeterReadingForm) error { + ms.log.Info("创建一条新的表计抄表记录", zap.String("park id", pid), zap.String("meter code", meterCode)) + meter, err := repository.MeterRepository.FetchMeterDetail(pid, meterCode) + if err != nil || meter == nil { + ms.log.Error("无法找到指定的表计", zap.Error(err)) + return fmt.Errorf("无法找到指定的表计:%w", err) + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + tx, err := global.DB.Begin(ctx) + if err != nil { + ms.log.Error("无法启动数据库事务。", zap.Error(err)) + return err + } + ok, err := repository.MeterRepository.RecordReading(tx, ctx, pid, meterCode, meter.MeterType, meter.Ratio, form) + if err != nil { + ms.log.Error("无法创建一条新的表计抄表记录。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + if !ok { + ms.log.Error("未能创建一条新的表计抄表记录。") + tx.Rollback(ctx) + return fmt.Errorf("未能成功创建一条新的表计抄表记录。") + } + + err = tx.Commit(ctx) + if err != nil { + ms.log.Error("未能成功提交数据库事务。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + + return nil +} + +// 获取指定园区的全部待抄表计列表,并将其输出到Excel文件模板中,提供生成文件的二进制内容 +func (ms _MeterService) GenerateParkMeterReadingTemplate(pid string, meters []*model.SimpleMeterDocument) ([]byte, error) { + return nil, nil +} + +// 处理上传的Excel格式的表计抄表记录,所有满足审查条件的记录都将被保存到数据库中。 +// 无论峰谷表计还是普通表计,只要抄表记录中不存在峰谷数据,都将自动使用平段配平。 +func (ms _MeterService) BatchImportReadings(pid string, uploadContent []byte) error { + // 步骤1:将解析到的数据转换成创建表单数据 + // 步骤2:对目前已经解析到的数据进行合法性检测,检测包括表计编号在同一抄表时间是否重复 + // 步骤3:从数据库中获取当前园区中已有的表计编号 + // 步骤4.0:启动数据库事务 + // 步骤4.1:对比检查数据库中的表计编号与上传文件中的表计编号是否存在差异。非差异内容将直接保存 + // 步骤4.1.1:抄表的表计在数据库中已经存在,可以直接保存起数据。 + // 步骤4.1.2:抄表表计在数据库中不存在,需要将其记录进入错误。 + // 步骤4.3:如果批处理过程中存在错误,撤销全部导入动作。 + // 步骤5:执行事务,更新数据库,获取完成更改的行数。 + return nil +} diff --git a/vo/meter.go b/vo/meter.go new file mode 100644 index 0000000..9671097 --- /dev/null +++ b/vo/meter.go @@ -0,0 +1,38 @@ +package vo + +import "github.com/shopspring/decimal" + +type MeterCreationForm struct { + Code string `json:"code"` + Address *string `json:"address"` + Ratio decimal.Decimal `json:"ratio"` + Seq int64 `json:"seq"` + MeterType int16 `json:"meterType"` + Building *string `json:"building"` + OnFloor *string `json:"onFloor"` + Area decimal.NullDecimal `json:"area"` + Enabled bool `json:"enabled"` + MeterReadingForm `json:"-"` +} + +type MeterModificationForm struct { + Address *string `json:"address"` + Seq int64 `json:"seq"` + Ratio decimal.Decimal `json:"ratio"` + Enabled bool `json:"enabled"` + MeterType int16 `json:"meterType"` + Building *string `json:"building"` + OnFloor *string `json:"onFloor"` + Area decimal.NullDecimal `json:"area"` +} + +type NewMeterForReplacingForm struct { + Code string `json:"code"` + Ratio decimal.Decimal `json:"ratio"` + Reading MeterReadingForm `json:"reading"` +} + +type MeterReplacingForm struct { + OldReading MeterReadingForm `json:"oldReading"` + NewMeter NewMeterForReplacingForm `json:"newMeter"` +} diff --git a/vo/reading.go b/vo/reading.go new file mode 100644 index 0000000..0ce357b --- /dev/null +++ b/vo/reading.go @@ -0,0 +1,16 @@ +package vo + +import ( + "electricity_bill_calc/types" + + "github.com/shopspring/decimal" +) + +type MeterReadingForm struct { + 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"` + ReadAt *types.DateTime `json:"readAt"` +}