From 28b1478e9ab52758490bae3c32a3a5e7d8ce1e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=B6=9B?= Date: Thu, 8 Jun 2023 20:55:56 +0800 Subject: [PATCH] =?UTF-8?q?enhance(meter):=E5=9F=BA=E6=9C=AC=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E8=A1=A8=E8=AE=A1=E5=A4=A7=E9=83=A8=E5=88=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E7=9A=84=E6=94=BE=E7=BD=AE=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/meter_04kv_template.xlsx | Bin 10523 -> 10453 bytes controller/meter.go | 180 +++++ model/cunsumption.go | 10 + model/meter.go | 78 +++ model/reading.go | 56 ++ repository/meter.go | 1091 +++++++++++++++++++++++++++++++ router/router.go | 1 + service/meter.go | 539 +++++++++++++++ vo/meter.go | 38 ++ vo/reading.go | 16 + 10 files changed, 2009 insertions(+) create mode 100644 controller/meter.go create mode 100644 model/cunsumption.go create mode 100644 model/meter.go create mode 100644 model/reading.go create mode 100644 repository/meter.go create mode 100644 service/meter.go create mode 100644 vo/meter.go create mode 100644 vo/reading.go diff --git a/assets/meter_04kv_template.xlsx b/assets/meter_04kv_template.xlsx index c1cccde54f09ba21f12ffaac6b217d1de07bf315..687fafc903bee1abf9a6ebbca146e2d7b063a7bf 100644 GIT binary patch delta 6442 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 delta 6461 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 jS}5F(m4W7;V`u= 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"` +}