合并分支

This commit is contained in:
2023-08-04 17:11:10 +08:00
parent 12ec8d26bf
commit 020e76b901
100 changed files with 12692 additions and 2574 deletions

455
service/calculate/meters.go Normal file
View File

@@ -0,0 +1,455 @@
package calculate
import (
"electricity_bill_calc/model"
"electricity_bill_calc/model/calculate"
"electricity_bill_calc/repository"
"errors"
"fmt"
"github.com/shopspring/decimal"
)
// / 合并所有的表计
type Key struct {
Code string
TenementID string
}
type MeterMap map[Key]calculate.Meter
func CollectMeters(tenements []calculate.PrimaryTenementStatistics, poolings []calculate.Meter, publics []calculate.Meter) (MeterMap, error) {
meters := make(MeterMap)
// Collect tenement meters
for _, t := range tenements {
for _, m := range t.Meters {
key := Key{TenementID: t.Tenement.Id, Code: m.Code}
meters[key] = m
}
}
// Collect poolings
for _, m := range poolings {
key := Key{TenementID: "", Code: m.Code}
meters[key] = m
}
// Collect publics
for _, m := range publics {
key := Key{TenementID: "", Code: m.Code}
meters[key] = m
}
return meters, nil
}
// / 计算基本电费摊薄
func CalculateBasicPooling(report *model.ReportIndex, summary *calculate.Summary, meters *MeterMap) error {
switch report.BasisPooled {
case model.POOLING_MODE_AREA:
if summary.OverallArea.IsZero() {
return fmt.Errorf("园区中表计覆盖总面积为零,无法按面积摊薄")
}
for _, meter := range *meters {
meterFee := meter.Overall.Amount.InexactFloat64() * summary.BasicPooledPriceArea.InexactFloat64()
meter.PooledBasic = model.ConsumptionUnit{
Amount: meter.Overall.Amount,
Fee: decimal.NewFromFloat(meterFee),
Price: summary.BasicPooledPriceArea,
Proportion: summary.BasicFee,
}
}
case model.POOLING_MODE_CONSUMPTION:
for _, meter := range *meters {
meterFee := meter.Overall.Amount.InexactFloat64() * summary.BasicPooledPriceConsumption.InexactFloat64()
meter.PooledBasic = model.ConsumptionUnit{
Amount: meter.Overall.Amount,
Fee: decimal.NewFromFloat(meterFee),
Price: summary.BasicPooledPriceConsumption,
Proportion: summary.BasicFee,
}
}
default:
}
return nil
}
/// 计算调整电费摊薄
func CalculateAdjustPooling(report model.ReportIndex, summary calculate.Summary, meters MeterMap) error {
var p decimal.Decimal
switch report.AdjustPooled {
case model.POOLING_MODE_AREA:
if summary.OverallArea.IsZero() {
return fmt.Errorf("园区中表计覆盖总面积为零,无法按面积摊薄")
}
for _, meter := range meters {
meterFee := meter.Overall.Amount.Mul(summary.AdjustPooledPriceArea)
if summary.AdjustFee.IsZero() {
p = decimal.Zero
} else {
p = meterFee.Div(summary.AdjustFee)
}
meter.PooledAdjust = model.ConsumptionUnit{
Amount: meter.Overall.Amount,
Fee: meterFee,
Price: summary.AdjustPooledPriceArea,
Proportion: p,
}
}
case model.POOLING_MODE_CONSUMPTION:
for _, meter := range meters {
meterFee := meter.Overall.Amount.Mul(summary.AdjustPooledPriceConsumption)
if summary.AdjustFee.IsZero() {
p = decimal.Zero
} else {
p = meterFee.Div(summary.AdjustFee)
}
meter.PooledAdjust = model.ConsumptionUnit{
Amount: meter.Overall.Amount,
Fee: meterFee,
Price: summary.AdjustPooledPriceConsumption,
Proportion: p,
}
}
default:
}
return nil
}
// 除数问题
func CalculateLossPooling(report model.ReportIndex, summary calculate.Summary, meters MeterMap) error {
switch report.LossPooled {
case model.POOLING_MODE_AREA:
if summary.OverallArea.IsZero() {
return fmt.Errorf("园区中表计覆盖总面积为零,无法按面积摊薄")
}
for _, meter := range meters {
pooledLossAmount1 := meter.Detail.Area.Decimal.Div(summary.OverallArea)
pooledLossAmount := pooledLossAmount1.Mul(summary.AuthoizeLoss.Amount)
meter.PooledLoss = model.ConsumptionUnit{
Amount: pooledLossAmount,
Fee: pooledLossAmount.Mul(summary.LossDilutedPrice),
Price: summary.LossDilutedPrice,
Proportion: meter.Detail.Area.Decimal.Div(summary.OverallArea),
}
}
case model.POOLING_MODE_CONSUMPTION:
for _, meter := range meters {
pooledLossAmount1 := meter.Detail.Area.Decimal.Div(summary.OverallArea)
pooledLossAmount := pooledLossAmount1.Mul(summary.AuthoizeLoss.Amount)
meter.PooledLoss = model.ConsumptionUnit{
Amount: pooledLossAmount,
Fee: pooledLossAmount.Mul(summary.LossDilutedPrice),
Price: summary.LossDilutedPrice,
Proportion: meter.Overall.Amount.Div(summary.Overall.Amount),
}
}
default:
// 其他情况下不做处理
}
return nil
}
/// 计算所有商户类型表计的全周期电量。
func CalculateTenementConsumptions(meters MeterMap) (map[string]decimal.Decimal, error) {
consumptions := make(map[string]decimal.Decimal)
for _, meter := range meters {
if meter.Detail.MeterType == model.METER_INSTALLATION_TENEMENT {
amount, ok := consumptions[meter.Code]
if !ok {
amount = decimal.Decimal{}
}
amount.Add(meter.Overall.Amount).Add(amount)
consumptions[meter.Code] = amount
}
}
for _, meter := range meters {
if meter.Detail.MeterType == model.METER_INSTALLATION_TENEMENT {
amount, ok := consumptions[meter.Code]
if !ok {
return nil, errors.New("meter code not found in consumptions")
}
if amount.GreaterThan(decimal.Zero) {
meter.SharedPoolingProportion = meter.Overall.Amount.Div(amount)
} else if amount.IsZero() {
meter.SharedPoolingProportion = decimal.NewFromFloat(1.0)
} else {
meter.SharedPoolingProportion = decimal.NewFromFloat(1.0)
}
}
}
return consumptions, nil
}
/*
/// 计算商户表计的公摊分摊
func CalculateTenementPoolings(report model.ReportIndex, summary calculate.Summary, meters MeterMap, meterRelations []model.MeterRelation) error {
for _, meter := range meters {
if meter.Detail.MeterType == model.METER_INSTALLATION_TENEMENT {
switch report.PublicPooled {
case model.POOLING_MODE_AREA:
for _, relation := range meterRelations {
if relation.SlaveMeter == meter.Code {
key := Key{
Code: relation.MasterMeter,
}
parentMeter, ok := meters[key]
if !ok {
return errors.New("父级表记未找到")
}
poolingAmount := meter.Detail.Area.Decimal.Div(parentMeter.CoveredArea).
Mul(meter.SharedPoolingProportion).
Mul(parentMeter.Overall.Amount).Mul(summary.Overall.Price)
pooling := calculate.Pooling{
Code: parentMeter.Code,
Detail: model.ConsumptionUnit{
Amount: poolingAmount,
Fee: poolingAmount.Mul(summary.Overall.Price),
Price: summary.Overall.Price,
//后续debug此处需要判断
Proportion: poolingAmount.Div(parentMeter.Overall.Amount),
},
}
pooling := calculate.Pooling{
Code: parentMeter.Code,
Detail: model.ConsumptionUnit{
Amount: poolingAmount,
Fee: poolingAmount.Mul(summary.Overall.Price),
Price: summary.Overall.Price,
Proportion: poolingAmount.Div(parentMeter.Overall.Amount),
},
}
meter.PooledPublic = &ConsumptionUnit{
Amount: poolingAmount,
Fee: new(big.Rat).Mul(poolingAmount, summary.Overall.Price),
Price: summary.Overall.Price,
Proportion: new(big.Rat).Quo(poolingAmount, parentAmount),
}
meter.Poolings = append(meter.Poolings, pooling)
}
}
case Consumption:
for _, relation := range meterRelations {
if relation.SlaveMeter == meter.Code {
parentMeter, ok := meters[relation.MasterMeter]
if !ok {
return errors.New("parent meter not found")
}
if parentMeter.Overall.Amount.Cmp(new(big.Rat)) == 0 {
poolingAmount := new(big.Rat)
parentAmount := new(big.Rat)
pooling := &Pooling{
Code: parentMeter.Code,
Detail: &ConsumptionUnit{
Amount: poolingAmount,
Fee: new(big.Rat),
Price: summary.Overall.Price,
Proportion: new(big.Rat),
},
}
meter.PooledPublic = &ConsumptionUnit{
Amount: poolingAmount,
Fee: new(big.Rat),
Price: summary.Overall.Price,
Proportion: new(big.Rat),
}
meter.Poolings = append(meter.Poolings, pooling)
} else {
poolingAmount := new(big.Rat).Mul(meter.Overall.Amount, new(big.Rat).Quo(parentMeter.Overall.Amount, parentMeter.Overall.Amount))
parentAmount := parentMeter.Overall.Amount
pooling := &Pooling{
Code: parentMeter.Code,
Detail: &ConsumptionUnit{
Amount: poolingAmount,
Fee: new(big.Rat).Mul(poolingAmount, summary.Overall.Price),
Price: summary.Overall.Price,
Proportion: new(big.Rat).Quo(poolingAmount, parentAmount),
},
}
meter.PooledPublic = &ConsumptionUnit{
Amount: poolingAmount,
Fee: new(big.Rat).Mul(poolingAmount, summary.Overall.Price),
Price: summary.Overall.Price,
Proportion: new(big.Rat).Quo(poolingAmount, parentAmount),
}
meter.Poolings = append(meter.Poolings, pooling)
}
}
}
default:
// handle other pooling modes...
}
}
}
return nil
}
*/
// 计算商户表计的公摊分摊
func CalculateTenementPoolings(report model.ReportIndex, summary calculate.Summary, meters MeterMap, meterRelations []model.MeterRelation) error {
switch report.PublicPooled {
case model.POOLING_MODE_AREA:
for _, meter := range meters {
if meter.Detail.MeterType == model.METER_INSTALLATION_TENEMENT {
var pooleds []struct {
PooledAmount decimal.Decimal
ParentAmount decimal.Decimal
ParentCode string
}
for _, relation := range meterRelations {
if relation.SlaveMeter == meter.Code {
key := Key{
Code: relation.MasterMeter,
}
parentMeter, ok := meters[key]
if !ok {
// 处理未找到父级表计的情况
continue
}
// 计算分摊电量和父级表电量
pooledAmount := meter.Detail.Area.Decimal.Div(parentMeter.CoveredArea).Mul(parentMeter.Overall.Amount).Mul(meter.SharedPoolingProportion)
pooleds = append(pooleds, struct {
PooledAmount decimal.Decimal
ParentAmount decimal.Decimal
ParentCode string
}{
PooledAmount: pooledAmount,
ParentAmount: parentMeter.Overall.Amount,
ParentCode: parentMeter.Code,
})
}
}
// 计算总分摊电量和总父级电量
var consumptions, total decimal.Decimal
for _, p := range pooleds {
consumptions = consumptions.Add(p.PooledAmount)
total = total.Add(p.ParentAmount)
}
// 计算并更新公摊分摊信息
for _, p := range pooleds {
poolingAmount := p.PooledAmount
proportion := p.PooledAmount.Div(p.ParentAmount)
fee := poolingAmount.Mul(summary.Overall.Price)
// 更新父级表计的公摊分摊信息
key := Key{
Code: p.ParentCode,
}
parentMeter := meters[key]
parentMeter.PooledPublic.Amount = consumptions
parentMeter.PooledPublic.Fee = consumptions.Mul(summary.Overall.Price)
parentMeter.PooledPublic.Proportion = consumptions.Div(total)
meters[Key{Code: p.ParentCode}] = parentMeter
// 创建并更新分摊信息
pooling := calculate.Pooling{
Code: p.ParentCode,
Detail: model.ConsumptionUnit{
Amount: poolingAmount,
Fee: fee,
Price: summary.Overall.Price,
Proportion: proportion,
},
}
meter.Poolings = append(meter.Poolings, &pooling)
}
}
}
case model.POOLING_MODE_CONSUMPTION:
for _, meter := range meters {
if meter.Detail.MeterType == model.METER_INSTALLATION_TENEMENT {
var pooled []struct {
PooledAmount decimal.Decimal
ParentAmount decimal.Decimal
ParentCode string
}
for _, relation := range meterRelations {
if relation.SlaveMeter == meter.Code {
parentMeter, ok := meters[Key{Code: relation.MasterMeter}]
if !ok {
// 处理未找到父级表计的情况
continue
}
// 计算分摊电量和父级电量
var pooledAmount decimal.Decimal
if parentMeter.Overall.Amount.IsZero() {
relations, err := repository.MeterRepository.ListPooledMeterRelations(report.Park, meter.Code)
if err != nil {
return err
}
//此处rust版本有误更新后的解决办法
pooledAmount = meter.Overall.Amount.Div(decimal.NewFromInt(int64(len(relations)))).Mul(parentMeter.Overall.Amount)
}
pooled = append(pooled, struct {
PooledAmount decimal.Decimal
ParentAmount decimal.Decimal
ParentCode string
}{
PooledAmount: pooledAmount,
ParentAmount: parentMeter.Overall.Amount,
ParentCode: parentMeter.Code,
})
}
}
// 计算总分摊电量和总父级表记电量
var consumptions, total decimal.Decimal
for _, p := range pooled {
consumptions = consumptions.Add(p.PooledAmount)
total = total.Add(p.ParentAmount)
}
// 计算并更新公摊分摊信息
for _, p := range pooled {
poolingAmount := p.PooledAmount
proportion := p.PooledAmount.Div(p.ParentAmount)
fee := poolingAmount.Mul(summary.Overall.Price)
// 更新父级表计的公摊分摊信息
parentMeter := meters[Key{Code: p.ParentCode}]
parentMeter.PooledPublic.Amount = consumptions
parentMeter.PooledPublic.Fee = consumptions.Mul(summary.Overall.Price)
parentMeter.PooledPublic.Proportion = consumptions.Div(total)
meters[Key{Code: p.ParentCode}] = parentMeter
// 创建并更新分摊信息
pooling := calculate.Pooling{
Code: p.ParentCode,
Detail: model.ConsumptionUnit{
Amount: poolingAmount,
Fee: fee,
Price: summary.Overall.Price,
Proportion: proportion,
},
}
meter.Poolings = append(meter.Poolings, &pooling)
}
}
}
default:
// 处理其他分摊模式
}
return nil
}

72
service/calculate/mod.go Normal file
View File

@@ -0,0 +1,72 @@
package calculate
import (
"electricity_bill_calc/global"
"electricity_bill_calc/repository"
"github.com/doug-martin/goqu/v9"
)
type _ModService struct {
ds goqu.DialectWrapper
}
var ModService = _ModService{
ds: goqu.Dialect("postgres"),
}
func mainCalculateProcess(rid string) error {
// 计算所有已经启用的商铺面积总和,仅计算所有未迁出的商户的所有表计对应的商铺面积。
err := CalculateEnabledArea(tenementreports, &summary)
// 计算基本电费分摊、调整电费分摊、电费摊薄单价。
err = CalculatePrices(&summary)
// 收集目前所有已经处理的表计,统一对其进行摊薄计算。
collectMeters, err := CollectMeters(tenementreports, poolingmetersreports, parkmetersreports)
meters, err := collectMeters, err
if err != nil {
return err
}
// 根据核算报表中设置的摊薄内容,逐个表计进行计算
CalculateBasicPooling(report, summary, meters)
CalculateAdjustPooling(report, summary, meters)
CalculateLossPooling(report, summary, meters)
// 计算所有商户类型表计的全周期电量,并根据全周期电量计算共用过同一表计的商户的二次分摊比例。
CalculateTenementConsumptions(meters)
CalculateTenementPoolings(report, summary, meters, metersrelations)
// 计算商户的合计电费信息,并归总与商户相关联的表计记录
tenementCharges, err := CalculateTenementCharge(tenementReports, summary, meters, meterRelations)
if err != nil {
// 处理错误
}
// 从此处开始向数据库保存全部计算结果。
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, _ := global.DB.Begin(ctx)
err = repository.CalculateRepository.ClearReportContent(tx, report.Id)
if err != nil {
tx.Rollback(ctx)
return err
}
err = SaveSummary(tx, summary)
if err != nil {
tx.Rollback(ctx)
return err
}
err = SavePublics(tx, report, meters)
if err != nil {
tx.Rollback(ctx)
return err
}
err = SavePoolings(tx)
if err != nil {
tx.Rollback(ctx)
return err
}
err = SaveTenements(tx)
if err != nil {
tx.Rollback(ctx)
return err
}
tx.Commit(ctx)
}

View File

@@ -0,0 +1,75 @@
package calculate
import (
"electricity_bill_calc/global"
"electricity_bill_calc/model"
"electricity_bill_calc/model/calculate"
"electricity_bill_calc/repository"
"github.com/jackc/pgx/v5"
)
// 向数据库保存核算概况结果
func SaveSummary(tx pgx.Tx, summary calculate.Summary) error {
ctx, cancel := global.TimeoutContext()
defer cancel()
// 保存核算概况结果到数据库
err := repository.CalculateRepository.SaveReportSummary(tx, summary)
if err != nil {
return err
}
tx.Commit(ctx)
return nil
}
// type MeterMap map[string]map[string]calculate.Meter
// 向数据库保存公共表计的计算结果
func SavePublics(tx pgx.Tx, report model.ReportIndex, meters MeterMap) error {
ctx, cancel := global.TimeoutContext()
defer cancel()
var filteredMeters []calculate.Meter
for _, m := range meters {
if m.Detail.MeterType == model.METER_INSTALLATION_PARK {
filteredMeters = append(filteredMeters, m)
}
}
err := repository.CalculateRepository.SaveReportPublics(tx, report.Id, filteredMeters)
if err != nil {
return err
}
tx.Commit(ctx)
return nil
}
func SavePoolings(tx pgx.Tx, report model.ReportIndex, meters MeterMap, relations []model.MeterRelation) error {
ctx, cancel := global.TimeoutContext()
defer cancel()
var poolingMeters []calculate.Meter
var tenementMeters []calculate.Meter
// 根据条件筛选 Meter 并保存到对应的数组中
for _, m := range meters {
if m.Detail.MeterType == model.METER_INSTALLATION_POOLING {
poolingMeters = append(poolingMeters, m)
} else if m.Detail.MeterType == model.METER_INSTALLATION_TENEMENT {
tenementMeters = append(tenementMeters, m)
}
}
err := repository.CalculateRepository.SaveReportPoolings(tx, report.Id, poolingMeters, relations, tenementMeters)
if err != nil {
return err
}
return nil
}
func SaveTenements(tx pgx.Tx, report model.ReportIndex, tenement []calculate.PrimaryTenementStatistics, tc []*calculate.TenementCharge) error {
var ts []model.Tenement
for _, r := range tenement {
ts = append(ts, r.Tenement)
}
err := repository.CalculateRepository.SaveReportTenement(tx, report, ts, tc)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,55 @@
package calculate
import (
"electricity_bill_calc/model/calculate"
"errors"
"github.com/shopspring/decimal"
)
// / 计算已经启用的商铺面积和
// /
// / - `tenements`:所有商户的电量信息
// / - `summary`:核算报表的摘要信息
func CalculateEnabledArea(tenements []calculate.PrimaryTenementStatistics, summary *calculate.Summary) error {
var areaMeters []calculate.Meter
for _, t := range tenements {
areaMeters = append(areaMeters, t.Meters...)
}
// 去重
uniqueAreaMeters := make(map[string]calculate.Meter)
for _, meter := range areaMeters {
uniqueAreaMeters[meter.Code] = meter
}
var areaTotal decimal.Decimal
for _, meter := range uniqueAreaMeters {
areaTotal = areaTotal.Add(meter.Detail.Area.Decimal)
}
if summary != nil {
summary.OverallArea = areaTotal
} else {
return nil, errors.New("summary is nil")
}
return &areaTotal, nil
}
// / 计算基本电费分摊、调整电费分摊以及电费摊薄单价。
// /
// / - `summary`:核算报表的摘要信息
func CalculatePrices(summary *calculate.Summary) error {
if summary.TotalConsumption.IsZero() {
return nil
}
summary.BasicPooledPriceConsumption = summary.BasicFee.Div(summary.TotalConsumption)
if summary.OverallArea.IsZero() {
summary.BasicPooledPriceArea = decimal.Zero
} else {
summary.BasicPooledPriceArea = summary.BasicFee.Div(summary.OverallArea)
}
summary.AdjustPooledPriceConsumption = summary.AdjustFee.Div(summary.TotalConsumption)
if summary.OverallArea.IsZero() {
summary.AdjustPooledPriceArea = decimal.Zero
} else {
summary.AdjustPooledPriceArea = summary.AdjustFee.Div(summary.OverallArea)
}
return nil
}

View File

@@ -0,0 +1,221 @@
package calculate
import (
"electricity_bill_calc/model"
"electricity_bill_calc/model/calculate"
"errors"
"github.com/shopspring/decimal"
"math/big"
)
// / 计算各个商户的合计信息,并归总与商户关联的表计记录
func CalculateTenementCharge(tenements []calculate.PrimaryTenementStatistics, summary calculate.Summary, meters MeterMap, relations []model.MeterRelation) ([]calculate.TenementCharge, error) {
tenementCharges := make([]calculate.TenementCharge, 0)
for _, tenement := range tenements {
relatedMeters := make([]calculate.Meter, 0)
for _, meter := range tenement.Meters {
code := meter.Code
if meter.Detail.MeterType == model.METER_INSTALLATION_TENEMENT {
code = meter.Detail.Code
}
relatedMeter, ok := meters[Key{Code: code}]
if !ok {
return nil, errors.New("related meter not found")
}
relatedMeters = append(relatedMeters, relatedMeter)
}
// Calculate overall, critical, peak, flat, valley, etc.
//var overall, critical, peak, flat, valley model.ConsumptionUnit
basicPooled, adjustPooled, lossPooled, publicPooled := new(big.Rat), new(big.Rat), new(big.Rat), new(big.Rat)
lossAmount := new(big.Rat)
for _, meter := range relatedMeters {
overall.Add(overall, meter.Overall)
critical.Add(critical, meter.Critical)
peak.Add(peak, meter.Peak)
flat.Add(flat, meter.Flat)
valley.Add(valley, meter.Valley)
basicPooled.Add(basicPooled, meter.PooledBasic.Fee)
adjustPooled.Add(adjustPooled, meter.PooledAdjust.Fee)
lossAmount.Add(lossAmount, meter.AdjustLoss.Amount)
lossPooled.Add(lossPooled, meter.PooledLoss.Fee)
publicPooled.Add(publicPooled, meter.PooledPublic.Fee)
}
// Update proportions and other data for related meters
for _, meter := range relatedMeters {
meter.Overall.Proportion = new(big.Rat).Quo(meter.Overall.Amount, overall.Amount)
meter.Critical.Proportion = new(big.Rat).Quo(meter.Critical.Amount, critical.Amount)
meter.Peak.Proportion = new(big.Rat).Quo(meter.Peak.Amount, peak.Amount)
meter.Flat.Proportion = new(big.Rat).Quo(meter.Flat.Amount, flat.Amount)
meter.Valley.Proportion = new(big.Rat).Quo(meter.Valley.Amount, valley.Amount)
meter.PooledBasic.Proportion = new(big.Rat).Quo(meter.PooledBasic.Fee, basicPooled)
meter.PooledAdjust.Proportion = new(big.Rat).Quo(meter.PooledAdjust.Fee, adjustPooled)
meter.PooledLoss.Proportion = new(big.Rat).Quo(meter.PooledLoss.Fee, lossPooled)
meter.PooledPublic.Proportion = new(big.Rat).Quo(meter.PooledPublic.Fee, publicPooled)
}
tenementCharges = append(tenementCharges, TenementCharges{
Tenement: tenement.Tenement.ID,
Overall: ConsumptionUnit{
Amount: overall.Amount,
Fee: new(big.Rat).Mul(overall.Amount, summary.Overall.Price),
Price: summary.Overall.Price,
Proportion: new(big.Rat).Quo(overall.Amount, summary.Overall.Amount),
},
Critical: ConsumptionUnit{
Amount: critical.Amount,
Fee: new(big.Rat).Mul(critical.Amount, summary.Critical.Price),
Price: summary.Critical.Price,
Proportion: new(big.Rat).Quo(critical.Amount, summary.Critical.Amount),
},
Peak: ConsumptionUnit{
Amount: peak.Amount,
Fee: new(big.Rat).Mul(peak.Amount, summary.Peak.Price),
Price: summary.Peak.Price,
Proportion: new(big.Rat).Quo(peak.Amount, summary.Peak.Amount),
},
Flat: ConsumptionUnit{
Amount: flat.Amount,
Fee: new(big.Rat).Mul(flat.Amount, summary.Flat.Price),
Price: summary.Flat.Price,
Proportion: new(big.Rat).Quo(flat.Amount, summary.Flat.Amount),
},
Valley: ConsumptionUnit{
Amount: valley.Amount,
Fee: new(big.Rat).Mul(valley.Amount, summary.Valley.Price),
Price: summary.Valley.Price,
Proportion: new(big.Rat).Quo(valley.Amount, summary.Valley.Amount),
},
Loss: ConsumptionUnit{
Amount: lossAmount,
Fee: new(big.Rat).Mul(lossPooled, summary.AuthorizeLoss.Price),
Price: summary.AuthorizeLoss.Price,
Proportion: new(big.Rat).Quo(lossAmount, summary.AuthorizeLoss.Amount),
},
BasicFee: basicPooled,
AdjustFee: adjustPooled,
LossPooled: lossPooled,
PublicPooled: publicPooled,
// ... 其他字段的初始化
})
}
return tenementCharges, nil
}
func calculateTenementCharge(
tenements []*PrimaryTenementStatistics,
summary *Summary,
meters MeterMap,
_relations []MeterRelation,
) ([]TenementCharges, error) {
var tenementCharges []TenementCharges
for _, t := range tenements {
meterCodes := make([]string, len(t.Meters))
for i, m := range t.Meters {
meterCodes[i] = m.Code
}
relatedMeters := make([]*Meter, len(meterCodes))
for i, code := range meterCodes {
relatedMeter, ok := meters[Key{Code: code, TenementID: t.Tenement.ID}]
if !ok {
// 处理未找到相关表计的情况
continue
}
relatedMeters[i] = relatedMeter
}
var overall, critical, peak, flat, valley ConsumptionUnit
var basicPooled, adjustPooled, lossAmount, lossPooled, publicPooled decimal.Decimal
for _, meter := range relatedMeters {
overall.Amount = overall.Amount.Add(meter.Overall.Amount)
overall.Fee = overall.Fee.Add(meter.Overall.Fee)
critical.Amount = critical.Amount.Add(meter.Critical.Amount)
critical.Fee = critical.Fee.Add(meter.Critical.Fee)
peak.Amount = peak.Amount.Add(meter.Peak.Amount)
peak.Fee = peak.Fee.Add(meter.Peak.Fee)
flat.Amount = flat.Amount.Add(meter.Flat.Amount)
flat.Fee = flat.Fee.Add(meter.Flat.Fee)
valley.Amount = valley.Amount.Add(meter.Valley.Amount)
valley.Fee = valley.Fee.Add(meter.Valley.Fee)
basicPooled = basicPooled.Add(meter.PooledBasic.Fee)
adjustPooled = adjustPooled.Add(meter.PooledAdjust.Fee)
lossAmount = lossAmount.Add(meter.AdjustLoss.Amount)
lossPooled = lossPooled.Add(meter.PooledLoss.Fee)
publicPooled = publicPooled.Add(meter.PooledPublic.Fee)
}
// 反写商户表计的统计数据
for _, meter := range relatedMeters {
meter.Overall.Proportion = meter.Overall.Amount.Div(overall.Amount)
meter.Critical.Proportion = meter.Critical.Amount.Div(critical.Amount)
meter.Peak.Proportion = meter.Peak.Amount.Div(peak.Amount)
meter.Flat.Proportion = meter.Flat.Amount.Div(flat.Amount)
meter.Valley.Proportion = meter.Valley.Amount.Div(valley.Amount)
meter.PooledBasic.Proportion = meter.PooledBasic.Fee.Div(basicPooled)
meter.PooledAdjust.Proportion = meter.PooledAdjust.Fee.Div(adjustPooled)
meter.PooledLoss.Proportion = meter.PooledLoss.Fee.Div(lossPooled)
meter.PooledPublic.Proportion = meter.PooledPublic.Fee.Div(publicPooled)
}
// 构造并添加商户的合计信息
tenementCharges = append(tenementCharges, TenementCharges{
Tenement: t.Tenement.ID,
Overall: ConsumptionUnit{
Price: summary.Overall.Price,
Proportion: overall.Amount.Div(summary.Overall.Amount),
Amount: overall.Amount,
Fee: overall.Fee,
},
Critical: ConsumptionUnit{
Price: summary.Critical.Price,
Proportion: critical.Amount.Div(summary.Critical.Amount),
Amount: critical.Amount,
Fee: critical.Fee,
},
Peak: ConsumptionUnit{
Price: summary.Peak.Price,
Proportion: peak.Amount.Div(summary.Peak.Amount),
Amount: peak.Amount,
Fee: peak.Fee,
},
Flat: ConsumptionUnit{
Price: summary.Flat.Price,
Proportion: flat.Amount.Div(summary.Flat.Amount),
Amount: flat.Amount,
Fee: flat.Fee,
},
Valley: ConsumptionUnit{
Price: summary.Valley.Price,
Proportion: valley.Amount.Div(summary.Valley.Amount),
Amount: valley.Amount,
Fee: valley.Fee,
},
Loss: ConsumptionUnit{
Price: summary.AuthorizeLoss.Price,
Proportion: lossAmount.Div(summary.AuthorizeLoss.Amount),
Amount: lossAmount,
Fee: lossPooled,
},
BasicFee: basicPooled,
AdjustFee: adjustPooled,
LossPooled: lossPooled,
PublicPooled: publicPooled,
FinalCharges: overall.Fee.Add(basicPooled).Add(adjustPooled).Add(lossPooled).Add(publicPooled),
Submeters: relatedMeters,
Poolings: make([]Meter, 0), // TODO: Add pooling logic here
})
}
return tenementCharges, nil
}

View File

@@ -1,284 +1,115 @@
package service
import (
"context"
"database/sql"
"electricity_bill_calc/cache"
"electricity_bill_calc/config"
"electricity_bill_calc/exceptions"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"electricity_bill_calc/types"
"fmt"
"strconv"
"time"
"github.com/fufuok/utils"
"github.com/samber/lo"
"github.com/uptrace/bun"
"go.uber.org/zap"
)
type _ChargeService struct {
l *zap.Logger
log *zap.Logger
}
var ChargeService = _ChargeService{
l: logger.Named("Service", "Charge"),
var ChargeService = &_ChargeService{
log: logger.Named("Service", "Charge"),
}
func (c _ChargeService) CreateChargeRecord(charge *model.UserCharge, extendWithIgnoreSettle bool) error {
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return err
}
_, err = tx.NewInsert().Model(charge).Exec(ctx)
if err != nil {
tx.Rollback()
return err
}
if extendWithIgnoreSettle {
err := c.updateUserExpiration(&tx, ctx, charge.UserId, charge.ChargeTo)
if err != nil {
return err
}
}
err = tx.Commit()
if err != nil {
tx.Rollback()
return err
}
cache.AbolishRelation("charge")
return nil
}
func (c _ChargeService) SettleCharge(seq int64, uid string) error {
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return err
}
var record = new(model.UserCharge)
err = tx.NewSelect().Model(&record).
Where("seq = ?", seq).
Where("user_id = ?", uid).
Scan(ctx)
if err != nil {
return nil
}
if record == nil {
return exceptions.NewNotFoundError("未找到匹配指定条件的计费记录。")
}
currentTime := time.Now()
_, err = tx.NewUpdate().Model((*model.UserCharge)(nil)).
Where("seq = ?", seq).
Where("user_id = ?", uid).
Set("settled = ?", true).
Set("settled_at = ?", currentTime).
Exec(ctx)
if err != nil {
tx.Rollback()
return err
}
err = c.updateUserExpiration(&tx, ctx, uid, record.ChargeTo)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
tx.Rollback()
return err
}
cache.AbolishRelation(fmt.Sprintf("charge:%s:%d", uid, seq))
return nil
}
func (c _ChargeService) RefundCharge(seq int64, uid string) error {
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return err
}
currentTime := time.Now()
res, err := tx.NewUpdate().Model((*model.UserCharge)(nil)).
Where("seq = ?", seq).
Where("user_id = ?", uid).
Set("refunded = ?", true).
Set("refunded_at = ?", currentTime).
Exec(ctx)
if err != nil {
tx.Rollback()
return err
}
if rows, _ := res.RowsAffected(); rows == 0 {
tx.Rollback()
return exceptions.NewNotFoundError("未找到匹配指定条件的计费记录。")
}
lastValidExpriation, err := c.lastValidChargeTo(&tx, &ctx, uid)
if err != nil {
tx.Rollback()
return exceptions.NewNotFoundError("未找到最后合法的计费时间。")
}
err = c.updateUserExpiration(&tx, ctx, uid, lastValidExpriation)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
tx.Rollback()
return err
}
cache.AbolishRelation(fmt.Sprintf("charge:%s:%d", uid, seq))
return nil
}
func (c _ChargeService) CancelCharge(seq int64, uid string) error {
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return err
}
currentTime := time.Now()
res, err := tx.NewUpdate().Model((*model.UserCharge)(nil)).
Where("seq = ?", seq).
Where("user_id = ?", uid).
Set("cancelled = ?", true).
Set("cancelled_at = ?", currentTime).
Exec(ctx)
if err != nil {
tx.Rollback()
return err
}
if rows, _ := res.RowsAffected(); rows == 0 {
tx.Rollback()
return exceptions.NewNotFoundError("未找到匹配指定条件的计费记录。")
}
err = tx.Commit()
if err != nil {
tx.Rollback()
return err
}
tx, err = global.DB.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return err
}
lastValidExpriation, err := c.lastValidChargeTo(&tx, &ctx, uid)
if err != nil {
return exceptions.NewNotFoundError("未找到最后合法的计费时间。")
}
err = c.updateUserExpiration(&tx, ctx, uid, lastValidExpriation)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
tx.Rollback()
return err
}
cache.AbolishRelation("user")
cache.AbolishRelation(fmt.Sprintf("user:%s", uid))
cache.AbolishRelation("charge")
cache.AbolishRelation(fmt.Sprintf("charge:%s:%d", uid, seq))
return nil
}
func (ch _ChargeService) updateUserExpiration(tx *bun.Tx, ctx context.Context, uid string, expiration model.Date) error {
_, err := tx.NewUpdate().Model((*model.UserDetail)(nil)).
Set("service_expiration = ?", expiration).
Where("id = ?", uid).
Exec(ctx)
if err != nil {
tx.Rollback()
}
cache.AbolishRelation(fmt.Sprintf("user:%s", uid))
return err
}
func (_ChargeService) ListPagedChargeRecord(keyword, beginDate, endDate string, page int) ([]model.ChargeWithName, int64, error) {
var (
cond = global.DB.NewSelect()
condition = make([]string, 0)
charges = make([]model.UserCharge, 0)
// 创建一条新的用户充值记录,同时更新用户的服务期限
func (cs _ChargeService) RecordUserCharge(uid string, fee, discount, amount *float64, chargeTo types.Date, extendExpriationIgnoringSettle bool) (bool, error) {
cs.log.Info(
"创建一条新的用户充值记录。",
zap.String("uid", uid),
zap.Float64p("fee", fee),
zap.Float64p("discount", discount),
zap.Float64p("amount", amount),
logger.DateField("chargeTo", chargeTo),
zap.Bool("extendExpriationIgnoringSettle", extendExpriationIgnoringSettle),
)
cond = cond.Model(&charges).Relation("Detail")
condition = append(condition, strconv.Itoa(page))
if len(keyword) != 0 {
keywordCond := "%" + keyword + "%"
cond = cond.WhereGroup(" and ", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("detail.name like ?", keywordCond).
WhereOr("detail.abbr like ?", keywordCond)
})
condition = append(condition, keyword)
}
if len(beginDate) != 0 {
beginTime, err := time.ParseInLocation("2006-01-02", beginDate, time.Local)
beginTime = utils.BeginOfDay(beginTime)
if err != nil {
return make([]model.ChargeWithName, 0), 0, err
}
cond = cond.Where("c.created_at >= ?", beginTime)
condition = append(condition, strconv.FormatInt(beginTime.Unix(), 10))
}
if len(endDate) != 0 {
endTime, err := time.ParseInLocation("2006-01-02", endDate, time.Local)
endTime = utils.EndOfDay(endTime)
if err != nil {
return make([]model.ChargeWithName, 0), 0, err
}
cond = cond.Where("c.created_at <= ?", endTime)
condition = append(condition, strconv.FormatInt(endTime.Unix(), 10))
}
if cachedTotal, err := cache.RetreiveCount("charge_with_name", condition...); cachedTotal != -1 && err == nil {
if cachedCharges, _ := cache.RetreiveSearch[[]model.ChargeWithName]("charge_with_name", condition...); cachedCharges != nil {
return *cachedCharges, cachedTotal, nil
}
}
startItem := (page - 1) * config.ServiceSettings.ItemsPageSize
var (
total int
err error
)
ctx, cancel := global.TimeoutContext()
defer cancel()
total, err = cond.Limit(config.ServiceSettings.ItemsPageSize).Offset(startItem).ScanAndCount(ctx)
relations := []string{"charge"}
chargesWithName := make([]model.ChargeWithName, 0)
for _, c := range charges {
chargesWithName = append(chargesWithName, model.ChargeWithName{
UserCharge: c,
UserDetail: *c.Detail,
})
relations = append(relations, fmt.Sprintf("charge:%s:%d", c.UserId, c.Seq))
}
cache.CacheCount(relations, "charge_with_name", int64(total), condition...)
cache.CacheSearch(chargesWithName, relations, "charge_with_name", condition...)
return chargesWithName, int64(total), err
}
func (_ChargeService) lastValidChargeTo(tx *bun.Tx, ctx *context.Context, uid string) (model.Date, error) {
var records []model.Date
err := tx.NewSelect().Table("user_charge").
Where("settled = ? and cancelled = ? and refunded = ? and user_id = ?", true, false, false, uid).
Column("charge_to").
Scan(*ctx, &records)
tx, err := global.DB.Begin(ctx)
if err != nil {
return model.NewEmptyDate(), nil
cs.log.Error("开启数据库事务失败。", zap.Error(err))
return false, err
}
lastValid := lo.Reduce(records, func(acc, elem model.Date, index int) model.Date {
if elem.Time.After(acc.Time) {
return elem
} else {
return acc
ok, err := repository.ChargeRepository.CreateChargeRecord(tx, ctx, uid, fee, discount, amount, chargeTo)
switch {
case err == nil && !ok:
cs.log.Error("未能成功创建用户充值记录", zap.Error(err))
tx.Rollback(ctx)
return false, fmt.Errorf("未能成功创建用户充值记录")
case err != nil:
cs.log.Error("创建用户充值记录失败。", zap.Error(err))
tx.Rollback(ctx)
return false, err
}
if extendExpriationIgnoringSettle {
ok, err = repository.UserRepository.UpdateServiceExpiration(tx, ctx, uid, chargeTo.Time)
switch {
case err != nil:
cs.log.Error("更新用户服务期限失败。", zap.Error(err))
tx.Rollback(ctx)
return false, err
case !ok:
cs.log.Error("未能成功更新用户服务期限", zap.Error(err))
tx.Rollback(ctx)
return false, fmt.Errorf("未能成功更新用户服务期限")
}
}, model.NewEmptyDate())
return lastValid, nil
}
err = tx.Commit(ctx)
if err != nil {
cs.log.Error("提交数据库事务失败。", zap.Error(err))
return false, err
}
return true, nil
}
// 撤销用户的某一条充值记录,同时重新设置用户的服务期限
func (cs _ChargeService) CancelUserCharge(uid string, seq int64) (bool, error) {
cs.log.Info("撤销用户的充值记录。", zap.String("uid", uid), zap.Int64("seq", seq))
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.Begin(ctx)
if err != nil {
cs.log.Error("开启数据库事务失败。", zap.Error(err))
return false, err
}
ok, err := repository.ChargeRepository.CancelCharge(tx, ctx, uid, seq)
switch {
case err == nil && !ok:
cs.log.Error("未能成功撤销用户充值记录", zap.Error(err))
tx.Rollback(ctx)
return false, fmt.Errorf("未能成功撤销用户充值记录")
case err != nil:
cs.log.Error("撤销用户充值记录失败。", zap.Error(err))
tx.Rollback(ctx)
return false, err
}
if ok {
lastValidCharge, err := repository.ChargeRepository.LatestValidChargeTo(tx, ctx, uid)
if err != nil {
cs.log.Error("查询用户最近一次有效的充值记录失败。", zap.Error(err))
tx.Rollback(ctx)
return false, err
}
ok, err = repository.UserRepository.UpdateServiceExpiration(tx, ctx, uid, lastValidCharge.Time)
if err != nil || !ok {
cs.log.Error("更新用户服务期限失败。", zap.Error(err))
tx.Rollback(ctx)
return false, err
}
}
err = tx.Commit(ctx)
if err != nil {
cs.log.Error("提交数据库事务失败。", zap.Error(err))
return false, err
}
return true, nil
}

180
service/invoice.go Normal file
View File

@@ -0,0 +1,180 @@
package service
import (
"electricity_bill_calc/exceptions"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"electricity_bill_calc/types"
"github.com/doug-martin/goqu/v9"
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"go.uber.org/zap"
)
type _InvoiceSerivce struct {
log *zap.Logger
ds goqu.DialectWrapper
}
var InvoiceService = _InvoiceSerivce{
log: logger.Named("Service", "Invoice"),
ds: goqu.Dialect("postgres"),
}
// 获取指定的发票信息,包括发票覆盖的商户核算信息
func (is _InvoiceSerivce) GetInvoice(invoiceNo string) (*model.Invoice, []*model.SimplifiedTenementCharge, error) {
is.log.Info("获取指定发票的信息", zap.String("InvoiceNo", invoiceNo))
invoice, err := repository.InvoiceRepository.GetInvoiceDetail(invoiceNo)
if err != nil || invoice == nil {
is.log.Error("获取指定发票的信息失败", zap.Error(err))
return nil, nil, exceptions.NewNotFoundError("指定发票信息不存在。")
}
charges, err := repository.InvoiceRepository.GetSimplifiedTenementCharges(invoice.Tenement, invoice.Covers)
if err != nil {
is.log.Error("获取指定发票的信息失败", zap.Error(err))
return nil, nil, err
}
return invoice, charges, nil
}
// 根据给定的商户核算记录和发票基本信息,计算发票中的货物信息
func (is _InvoiceSerivce) CalculateInvoiceAmount(method int16, rate decimal.Decimal, reports []*model.SimplifiedTenementCharge) (decimal.Decimal, []*model.InvoiceCargo, error) {
is.log.Info("计算指定商户发票中的货物信息", zap.Int16("Method", method), logger.DecimalField("Rate", rate))
tenementConsumptionTotal := lo.Reduce(reports, func(agg decimal.Decimal, r *model.SimplifiedTenementCharge, _ int) decimal.Decimal {
return agg.Add(r.TotalConsumption)
}, decimal.Zero)
tenementChargeTotal := lo.Reduce(reports, func(agg decimal.Decimal, r *model.SimplifiedTenementCharge, _ int) decimal.Decimal {
return agg.Add(r.FinalCharge)
}, decimal.Zero)
if tenementConsumptionTotal.IsZero() {
err := exceptions.NewInsufficientDataError("TotalConsumption", "商户核算记录中没有电量消耗数据。")
is.log.Warn("计算指定商户发票中的货物信息失败", zap.Error(err))
return decimal.Zero, nil, err
}
var tenementTaxTotal, chargePrice, cargoTotal decimal.Decimal
switch method {
case model.TAX_METHOD_INCLUSIVE:
tenementTaxTotal = tenementChargeTotal.Div(rate.Add(decimal.NewFromInt(1))).Mul(rate)
chargePrice = (tenementChargeTotal.Sub(tenementTaxTotal)).Div(tenementConsumptionTotal)
cargoTotal = tenementChargeTotal
case model.TAX_METHOD_EXCLUSIVE:
tenementTaxTotal = tenementChargeTotal.Mul(rate)
chargePrice = tenementChargeTotal.Div(tenementConsumptionTotal)
cargoTotal = tenementChargeTotal.Add(tenementTaxTotal)
default:
return decimal.Zero, make([]*model.InvoiceCargo, 0), exceptions.NewIllegalArgumentsError("不支持的税率计算方式。")
}
cargos := []*model.InvoiceCargo{
{
Name: "电费",
Unit: "千瓦时",
Quantity: tenementConsumptionTotal,
Price: chargePrice.RoundBank(2),
Total: tenementChargeTotal.RoundBank(2),
TaxRate: rate.RoundBank(2),
Tax: tenementTaxTotal.RoundBank(2),
},
}
return cargoTotal.RoundBank(2), cargos, nil
}
// 利用用户提供的内容对发票数据进行试计算
func (is _InvoiceSerivce) TestCalculateInvoice(pid, tid string, method int16, rate decimal.NullDecimal, covers []string) (decimal.Decimal, []*model.InvoiceCargo, error) {
is.log.Info("试计算发票票面数据", zap.String("Park", pid), zap.String("Tenement", tid), zap.Int16("Method", method), logger.DecimalField("Rate", rate.Decimal))
park, err := repository.ParkRepository.RetrieveParkDetail(pid)
if err != nil || park == nil {
is.log.Error("试计算发票票面数据失败,未能获取到指定园区的信息", zap.Error(err))
return decimal.Zero, nil, exceptions.NewNotFoundError("指定的园区不存在。")
}
if !rate.Valid && !park.TaxRate.Valid {
is.log.Error("试计算发票票面数据失败,必须要设定发票税率")
return decimal.Zero, nil, exceptions.NewIllegalArgumentsError("必须要设定发票税率。")
}
taxRate := park.TaxRate.Decimal
if rate.Valid {
taxRate = rate.Decimal
}
reports, err := repository.InvoiceRepository.GetSimplifiedTenementCharges(tid, covers)
if err != nil {
is.log.Error("试计算发票票面数据失败,未能获取到指定商户的核算记录", zap.Error(err))
return decimal.Zero, nil, err
}
return is.CalculateInvoiceAmount(method, taxRate, reports)
}
// 记录一个新的发票信息
func (is _InvoiceSerivce) SaveInvoice(pid, tid, invoiceNo string, invoiceType *string, method int16, rate decimal.NullDecimal, covers []string) error {
is.log.Info("记录一个新的发票信息", zap.String("Park", pid), zap.String("Tenement", tid), zap.String("InvoiceNo", invoiceNo))
park, err := repository.ParkRepository.RetrieveParkDetail(pid)
if err != nil || park == nil {
is.log.Error("记录一个新的发票信息失败,未能获取到指定园区的信息", zap.Error(err))
return exceptions.NewNotFoundError("指定的园区不存在。")
}
if !rate.Valid && park.TaxRate.Valid {
is.log.Error("记录一个新的发票信息失败,必须要设定发票税率")
return exceptions.NewIllegalArgumentsError("必须要设定发票税率。")
}
taxRate := park.TaxRate.Decimal
if rate.Valid {
taxRate = rate.Decimal
}
reports, err := repository.InvoiceRepository.GetSimplifiedTenementCharges(tid, covers)
if err != nil {
is.log.Error("记录一个新的发票信息失败,未能获取到指定商户的核算记录", zap.Error(err))
return exceptions.NewUnsuccessQueryError("未能获取到指定商户的核算记录。")
}
total, cargos, err := is.CalculateInvoiceAmount(method, taxRate, reports)
if err != nil {
is.log.Error("记录一个新的发票信息失败,未能计算发票票面数据", zap.Error(err))
return exceptions.NewUnsuccessCalculateError("未能计算发票票面数据。")
}
issuedAt := types.Now()
err = repository.InvoiceRepository.Create(pid, tid, invoiceNo, invoiceType, total, issuedAt, method, taxRate, &cargos, &covers)
if err != nil {
is.log.Error("记录一个新的发票信息失败,未能保存发票信息", zap.Error(err))
return exceptions.NewUnsuccessCreateError("未能保存发票信息。")
}
return nil
}
// 删除指定的发票信息
func (is _InvoiceSerivce) DeleteInvoice(invoiceNo string) error {
is.log.Info("删除指定的发票信息", zap.String("InvoiceNo", invoiceNo))
invoice, err := repository.InvoiceRepository.GetInvoiceDetail(invoiceNo)
if err != nil || invoice == nil {
is.log.Error("删除指定的发票信息失败,未能获取到指定发票的信息", zap.Error(err))
return exceptions.NewNotFoundError("指定的发票信息不存在。")
}
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.Begin(ctx)
if err != nil {
is.log.Error("删除指定的发票信息失败,未能开启事务", zap.Error(err))
return exceptions.NewUnsuccessDBTransactionError("未能开启事务。")
}
err = repository.InvoiceRepository.Delete(tx, ctx, invoiceNo)
if err != nil {
is.log.Error("删除指定的发票信息失败,未能删除发票信息", zap.Error(err))
tx.Rollback(ctx)
return exceptions.NewUnsuccessDeleteError("未能删除发票信息。")
}
err = repository.InvoiceRepository.DeleteInvoiceTenementRelation(tx, ctx, invoiceNo)
if err != nil {
is.log.Error("删除指定的发票信息失败,未能删除发票与商户核算记录之间的关联", zap.Error(err))
tx.Rollback(ctx)
return exceptions.NewUnsuccessDeleteError("未能删除发票与商户核算记录之间的关联。")
}
err = tx.Commit(ctx)
if err != nil {
is.log.Error("删除指定的发票信息失败,未能提交事务", zap.Error(err))
tx.Rollback(ctx)
return exceptions.NewUnsuccessDBTransactionError("未能提交事务。")
}
return nil
}

779
service/meter.go Normal file
View File

@@ -0,0 +1,779 @@
package service
import (
"electricity_bill_calc/excel"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"electricity_bill_calc/types"
"electricity_bill_calc/vo"
"fmt"
"mime/multipart"
"strings"
"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
}
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
}
return nil
}
// 处理上传的Excel格式表计档案文件根据表号自动更新数据库
func (ms _MeterService) BatchImportMeters(pid string, file *multipart.FileHeader) ([]excel.ExcelAnalysisError, error) {
ms.log.Info("处理上传的Excel格式表计档案文件", zap.String("park id", pid))
ctx, cancel := global.TimeoutContext(10)
defer cancel()
archiveFile, err := file.Open()
if err != nil {
ms.log.Error("无法打开上传的Excel格式表计档案文件。", zap.Error(err))
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法打开上传的文件,%w", err)
}
analyzer, err := excel.NewMeterArchiveExcelAnalyzer(archiveFile)
if err != nil {
ms.log.Error("无法根据上传的 Excel 文件创建表计档案分析器。", zap.Error(err))
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法创建表计档案解析器,%w", err)
}
records, errs := analyzer.Analysis(*new(model.MeterImportRow))
if len(errs) > 0 {
ms.log.Error("表计档案分析器在解析上传的 Excel 文件时发生错误。", zap.Int("error count", len(errs)))
return errs, fmt.Errorf("表计档案分析器在解析上传的 Excel 文件时发生错误。")
}
// 步骤1对目前已经解析到的数据进行重复检测记录重复内容并直接返回
var codeStat = make(map[string]int, 0)
for _, record := range records {
if _, ok := codeStat[record.Code]; !ok {
codeStat[record.Code] = 0
}
codeStat[record.Code]++
}
duplicatedCodes := make([]string, 0)
for code, count := range codeStat {
if count > 1 {
duplicatedCodes = append(duplicatedCodes, code)
}
}
if len(duplicatedCodes) > 0 {
ms.log.Error("表计档案分析器在解析上传的 Excel 文件时发现重复的表计编号。", zap.Strings("duplicated codes", duplicatedCodes))
return []excel.ExcelAnalysisError{
{Row: 0, Col: 0, Err: excel.AnalysisError{Err: fmt.Errorf("表计档案分析器在解析上传的 Excel 文件时发现重复的表计编号。(%s)", strings.Join(duplicatedCodes, ", "))}},
}, fmt.Errorf("表计档案分析器在解析上传的 Excel 文件时发现重复的表计编号。(%s)", strings.Join(duplicatedCodes, ", "))
}
// 步骤2获取指定园区下的所有建筑信息
buildings, err := repository.ParkRepository.RetrieveParkBuildings(pid)
if err != nil {
ms.log.Error("无法获取指定园区下的所有建筑信息。", zap.Error(err))
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法获取指定园区下的所有建筑信息,%w", err)
}
buildingNames := lo.Map(buildings, func(element *model.ParkBuilding, _ int) string {
return element.Name
})
// 步骤2.1:获取表计档案中出现的所有建筑,并对档案中新出现的建筑进行创建操作
unexistsBuildingNames := make([]string, 0)
for _, record := range records {
if record.Building != nil && !lo.Contains(buildingNames, *record.Building) {
unexistsBuildingNames = append(unexistsBuildingNames, *record.Building)
}
}
tx, err := global.DB.Begin(ctx)
if err != nil {
ms.log.Error("无法在自动导入建筑阶段启动数据库事务。", zap.Error(err))
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法在自动导入建筑阶段启动数据库事务,%w", err)
}
for _, name := range unexistsBuildingNames {
_, err := repository.ParkRepository.CreateParkBuildingWithTransaction(tx, ctx, pid, name, nil)
if err != nil {
ms.log.Error("无法在自动导入建筑阶段创建新的建筑。", zap.String("building name", name), zap.Error(err))
tx.Rollback(ctx)
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法在自动导入建筑阶段创建新的建筑,%w", err)
}
}
err = tx.Commit(ctx)
if err != nil {
ms.log.Error("无法在自动导入建筑阶段提交数据库事务。", zap.Error(err))
tx.Rollback(ctx)
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法在自动导入建筑阶段提交数据库事务,%w", err)
}
buildings, err = repository.ParkRepository.RetrieveParkBuildings(pid)
if err != nil {
ms.log.Error("无法重新获取指定园区下的所有建筑信息。", zap.Error(err))
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法重新获取指定园区下的所有建筑信息,%w", err)
}
// 步骤2.3检测并替换表计档案中的建筑ID
for _, record := range records {
for _, building := range buildings {
if record.Building != nil && building.Name == *record.Building {
record.Building = &building.Id
break
}
}
}
// 步骤3启动数据库事务直接构建表计插入语句但提供On Conflict Do Update功能
tx, err = global.DB.Begin(ctx)
if err != nil {
ms.log.Error("无法启动数据插入阶段的数据库事务。", zap.Error(err))
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法启动数据插入阶段的数据库事务,%w", err)
}
var meterCreationForms = make([]vo.MeterCreationForm, 0)
for row, element := range records {
if element.MeterType != nil {
meterType, err := model.ParseMeterInstallationType(*element.MeterType)
if err != nil {
ms.log.Error("无法识别表计类型。", zap.Int("record_index", row), zap.Error(err))
errs = append(errs, excel.ExcelAnalysisError{
Row: row + 1,
Col: 3,
Err: excel.AnalysisError{
Err: fmt.Errorf("表计类型无法识别"),
},
})
}
meterCreationForms = append(meterCreationForms, vo.MeterCreationForm{
Code: element.Code,
Address: element.Address,
MeterType: meterType,
Ratio: element.Ratio,
Seq: element.Seq,
Enabled: true,
Building: element.Building,
OnFloor: element.OnFloor,
Area: element.Area,
MeterReadingForm: vo.MeterReadingForm{
ReadAt: &element.ReadAt,
Overall: element.Overall,
Critical: element.Critical.Decimal,
Peak: element.Peak.Decimal,
Flat: element.Flat.Decimal,
Valley: element.Valley.Decimal,
},
})
} else {
ms.log.Error("表计类型不能为空。", zap.Int("record_index", row))
errs = append(errs, excel.ExcelAnalysisError{
Row: row + 1,
Col: 3,
Err: excel.AnalysisError{
Err: fmt.Errorf("表计类型不能为空"),
},
})
}
}
if len(errs) > 0 {
ms.log.Error("表计档案分析器在解析上传的 Excel 文件时发生错误。", zap.Int("error count", len(errs)))
tx.Rollback(ctx)
return errs, fmt.Errorf("表计档案分析器在解析上传的 Excel 文件时发生错误。")
}
for _, record := range meterCreationForms {
_, err := repository.MeterRepository.CreateOrUpdateMeter(tx, ctx, pid, record)
if err != nil {
ms.log.Error("无法在数据插入阶段创建或更新表计。", zap.String("meter code", record.Code), zap.Error(err))
tx.Rollback(ctx)
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法在数据插入阶段创建或更新表计,%w", err)
}
}
// 步骤5将全部抄表信息保存进入数据库
for _, record := range meterCreationForms {
_, err := repository.MeterRepository.RecordReading(tx, ctx, pid, record.Code, record.MeterType, record.Ratio, &record.MeterReadingForm)
if err != nil {
ms.log.Error("无法在数据插入阶段保存抄表信息。", zap.String("meter code", record.Code), zap.Error(err))
tx.Rollback(ctx)
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法在数据插入阶段保存抄表信息,%w", err)
}
}
// 步骤6执行事务更新数据库
err = tx.Commit(ctx)
if err != nil {
ms.log.Error("无法在数据插入阶段提交数据库事务。", zap.Error(err))
tx.Rollback(ctx)
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法在数据插入阶段提交数据库事务,%w", err)
}
return make([]excel.ExcelAnalysisError, 0), nil
}
// 更换系统中的表计
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
}
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))
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,
})
}
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
}
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
}
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) BatchImportReadings(pid string, file *multipart.FileHeader) ([]excel.ExcelAnalysisError, error) {
ms.log.Info("处理上传的Excel格式的表计抄表记录", zap.String("park id", pid))
ctx, cancel := global.TimeoutContext()
defer cancel()
// 步骤1将解析到的数据转换成创建表单数据
activeFile, err := file.Open()
if err != nil {
ms.log.Error("无法打开上传的抄表数据文件。", zap.Error(err))
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法打开上传的抄表数据文件,%w", err)
}
analyzer, err := excel.NewMeterReadingsExcelAnalyzer(activeFile)
if err != nil {
ms.log.Error("无法根据上传的 Excel 文件创建表计抄表数据解析器。", zap.Error(err))
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法根据上传的 Excel 文件创建表计抄表数据解析器,%w", err)
}
records, errs := analyzer.Analysis(*new(model.ReadingImportRow))
if len(errs) > 0 {
ms.log.Error("表计抄表数据解析器在解析上传的 Excel 文件时发生错误。", zap.Int("error count", len(errs)))
return errs, fmt.Errorf("表计抄表数据解析器在解析上传的 Excel 文件时发生错误。")
}
ms.log.Debug("已经解析到的上传数据", zap.Any("records", records))
// 步骤2对目前已经解析到的数据进行合法性检测检测包括表计编号在同一抄表时间是否重复
var collectRecords = make(map[types.DateTime][]string, 0)
for _, record := range records {
if _, ok := collectRecords[record.ReadAt]; !ok {
collectRecords[record.ReadAt] = []string{}
}
collectRecords[record.ReadAt] = append(collectRecords[record.ReadAt], record.Code)
}
for readAt, codes := range collectRecords {
valCounts := lo.CountValues(codes)
for code, count := range valCounts {
if count > 1 {
errs = append(errs, excel.ExcelAnalysisError{
Row: 0,
Col: 0,
Err: excel.AnalysisError{
Err: fmt.Errorf("表计编号 %s 在同一抄表时间 %s 内重复出现 %d 次", code, readAt.ToString(), count),
},
})
}
}
}
if len(errs) > 0 {
ms.log.Error("表计抄表数据解析器在解析上传的 Excel 文件时发生错误。", zap.Int("error count", len(errs)))
return errs, fmt.Errorf("表计抄表数据解析器在解析上传的 Excel 文件时发生错误。")
}
// 步骤3从数据库中获取当前园区中已有的表计编号
meters, err := repository.MeterRepository.AllMeters(pid)
if err != nil {
ms.log.Error("无法从数据库中获取当前园区中已有的表计编号。", zap.Error(err))
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法从数据库中获取当前园区中已有的表计编号,%w", err)
}
// 步骤4.0:启动数据库事务
tx, err := global.DB.Begin(ctx)
if err != nil {
ms.log.Error("无法启动数据库事务。", zap.Error(err))
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法启动数据库事务,%w", err)
}
// 步骤4.1:对比检查数据库中的表计编号与上传文件中的表计编号是否存在差异。非差异内容将直接保存
for row, record := range records {
meter, exists := lo.Find(meters, func(element *model.MeterDetail) bool {
return element.Code == record.Code
})
if exists {
// 步骤4.1.1:抄表的表计在数据库中已经存在,可以直接保存起数据。
_, err := repository.MeterRepository.RecordReading(tx, ctx, pid, record.Code, meter.MeterType, meter.Ratio, &vo.MeterReadingForm{
ReadAt: lo.ToPtr(record.ReadAt),
Overall: record.Overall,
Critical: record.Critical.Decimal,
Peak: record.Peak.Decimal,
Flat: record.Overall.Sub(record.Peak.Decimal).Sub(record.Valley.Decimal).Sub(record.Critical.Decimal),
Valley: record.Valley.Decimal,
})
if err != nil {
ms.log.Error("无法在数据插入阶段保存抄表信息。", zap.String("meter code", record.Code), zap.Error(err))
errs = append(errs, excel.ExcelAnalysisError{
Row: row + 1,
Col: 0,
Err: excel.AnalysisError{
Err: fmt.Errorf("无法在数据插入阶段保存抄表信息,%w", err),
},
})
}
} else {
// 步骤4.1.2:抄表表计在数据库中不存在,需要将其记录进入错误。
errs = append(errs, excel.ExcelAnalysisError{
Row: row + 1,
Col: 0,
Err: excel.AnalysisError{
Err: fmt.Errorf("表计编号 %s 在系统中不存在", record.Code),
},
})
}
}
// 步骤4.3:如果批处理过程中存在错误,撤销全部导入动作。
if len(errs) > 0 {
ms.log.Error("表计抄表数据解析器在解析上传的 Excel 文件时发生错误。", zap.Int("error count", len(errs)))
tx.Rollback(ctx)
return errs, fmt.Errorf("表计抄表数据解析器在解析上传的 Excel 文件时发生错误。")
}
// 步骤5执行事务更新数据库获取完成更改的行数。
err = tx.Commit(ctx)
if err != nil {
ms.log.Error("无法在数据插入阶段提交数据库事务。", zap.Error(err))
tx.Rollback(ctx)
return make([]excel.ExcelAnalysisError, 0), fmt.Errorf("无法在数据插入阶段提交数据库事务,%w", err)
}
return make([]excel.ExcelAnalysisError, 0), nil
}

View File

@@ -1,742 +1,198 @@
package service
import (
"database/sql"
"electricity_bill_calc/cache"
"electricity_bill_calc/config"
"electricity_bill_calc/exceptions"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/tools"
"fmt"
"strconv"
"time"
"electricity_bill_calc/repository"
"electricity_bill_calc/types"
"electricity_bill_calc/vo"
"github.com/fufuok/utils"
"github.com/google/uuid"
"github.com/doug-martin/goqu/v9"
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
"github.com/jinzhu/copier"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"github.com/uptrace/bun"
"go.uber.org/zap"
)
type _ReportService struct {
l *zap.Logger
log *zap.Logger
ds goqu.DialectWrapper
}
var ReportService = _ReportService{
l: logger.Named("Service", "Report"),
log: logger.Named("Service", "Report"),
ds: goqu.Dialect("postgres"),
}
func (_ReportService) FetchParksWithNewestReport(uid string) ([]model.ParkNewestReport, error) {
if cachedParks, _ := cache.RetreiveSearch[[]model.ParkNewestReport]("park_newest_report", uid); cachedParks != nil {
return *cachedParks, nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
parks := make([]model.Park, 0)
err := global.DB.NewSelect().Model(&parks).Relation("Reports").
Where("user_id = ?", uid).
Where("enabled = ?", true).
Order("created_at asc").
Scan(ctx)
// 将指定报表列入计算任务
func (rs _ReportService) DispatchReportCalculate(rid string) error {
rs.log.Info("将指定报表列入计算任务", zap.String("Report", rid))
_, err := repository.CalculateRepository.UpdateReportTaskStatus(rid, model.REPORT_CALCULATE_TASK_STATUS_PENDING, nil)
if err != nil {
return make([]model.ParkNewestReport, 0), err
rs.log.Error("未能将指定报表列入计算任务", zap.Error(err))
return err
}
return nil
}
reducedParks := lo.Reduce(
parks,
func(acc map[string]model.ParkNewestReport, elem model.Park, index int) map[string]model.ParkNewestReport {
if _, ok := acc[elem.Id]; !ok {
newestReport := lo.MaxBy(elem.Reports, func(a, b *model.Report) bool {
return a.Period.After(b.Period)
})
acc[elem.Id] = model.ParkNewestReport{
Report: newestReport,
Park: elem,
}
}
return acc
},
make(map[string]model.ParkNewestReport, 0),
)
relations := lo.Map(parks, func(r model.Park, _ int) string {
return fmt.Sprintf("park:%s", r.Id)
// 列出指定用户下的所有尚未发布的报表索引
func (rs _ReportService) ListDraftReportIndicies(uid string) ([]*vo.ReportIndexQueryResponse, error) {
rs.log.Info("列出指定用户下的所有尚未发布的报表", zap.String("User", uid))
indicies, err := repository.ReportRepository.ListDraftReportIndicies(uid)
if err != nil {
rs.log.Error("未能获取指定用户下所有未发布报表的索引", zap.Error(err))
return make([]*vo.ReportIndexQueryResponse, 0), err
}
parkIds := lo.Map(indicies, func(elem *model.ReportIndex, _ int) string {
return elem.Park
})
relations = append(relations, "park", "report")
cache.CacheSearch(reducedParks, relations, "park_newest_report", uid)
return lo.Values(reducedParks), nil
}
func (_ReportService) IsNewPeriodValid(uid, pid string, period time.Time) (bool, error) {
ctx, cancel := global.TimeoutContext()
defer cancel()
reports := make([]model.Report, 0)
if cachedReport, _ := cache.RetreiveSearch[[]model.Report]("report", "user", uid, "park", pid); cachedReport != nil {
reports = *cachedReport
} else {
err := global.DB.NewSelect().Model(&reports).Relation("Park").
Where("park.user_id = ?", uid).
Where("r.park_id = ?", pid).
Scan(ctx)
if err != nil {
return false, err
}
cache.CacheSearch(reports, []string{"report", "park"}, "park", "user", uid, "park", pid)
}
// 检查给定的期数在目前的记录中是否已经存在
exists := lo.Reduce(
reports,
func(acc bool, elem model.Report, index int) bool {
if elem.Period.Equal(period) {
return acc || true
} else {
return acc || false
}
},
false,
)
if exists {
return false, nil
}
// 检查给定的期数与目前已发布的最大期数的关系
maxPublished := lo.Reduce(
reports,
func(acc *time.Time, elem model.Report, index int) *time.Time {
if elem.Published {
if acc == nil || (acc != nil && elem.Period.After(*acc)) {
return &elem.Period
}
}
return acc
},
nil,
)
// 检查给定的期数与目前未发布的最大期数的关系
maxUnpublished := lo.Reduce(
reports,
func(acc *time.Time, elem model.Report, index int) *time.Time {
if acc == nil || (acc != nil && elem.Period.After(*acc)) {
return &elem.Period
}
return acc
},
nil,
)
if maxUnpublished == nil {
return true, nil
}
if maxPublished != nil && maxUnpublished.Equal(*maxPublished) {
// 此时不存在未发布的报表
return tools.IsNextMonth(*maxPublished, period), nil
} else {
// 存在未发布的报表
return false, nil
}
}
func (_ReportService) InitializeNewReport(parkId string, period time.Time) (string, error) {
ctx, cancel := global.TimeoutContext(120)
defer cancel()
periods := make([]model.Report, 0)
err := global.DB.NewSelect().Model(&periods).
Where("park_id = ?", parkId).
Where("published = ?", true).
Order("period asc").
Scan(ctx)
parks, err := repository.ParkRepository.RetrieveParks(parkIds)
if err != nil {
return "", err
rs.log.Error("未能获取到相应报表对应的园区详细信息", zap.Error(err))
return make([]*vo.ReportIndexQueryResponse, 0), err
}
// 获取上一期的报表索引信息
maxPublishedReport := lo.Reduce(
periods,
func(acc *model.Report, elem model.Report, index int) *model.Report {
if acc == nil || (acc != nil && elem.Period.After(acc.Period)) {
return &elem
}
return acc
},
nil,
)
var indexedLastPeriodCustomers map[string]model.EndUserDetail
if maxPublishedReport != nil {
// 获取上一期的所有户表信息,并获取当前已启用的所有用户
lastPeriodCustomers := make([]model.EndUserDetail, 0)
err = global.DB.NewSelect().Model(&lastPeriodCustomers).
Where("report_id = ?", maxPublishedReport.Id).
Scan(ctx)
if err != nil {
return "", err
}
indexedLastPeriodCustomers = lo.Reduce(
lastPeriodCustomers,
func(acc map[string]model.EndUserDetail, elem model.EndUserDetail, index int) map[string]model.EndUserDetail {
acc[elem.MeterId] = elem
return acc
},
make(map[string]model.EndUserDetail, 0),
assembled := lo.Reduce(indicies, func(acc []*vo.ReportIndexQueryResponse, elem *model.ReportIndex, _ int) []*vo.ReportIndexQueryResponse {
park, _ := lo.Find(parks, func(park *model.Park) bool {
return park.Id == elem.Park
})
var (
simplifiedPark vo.SimplifiedParkDetail
simplifiedReport vo.SimplifiedReportIndex
)
} else {
indexedLastPeriodCustomers = make(map[string]model.EndUserDetail, 0)
}
currentActivatedCustomers := make([]model.Meter04KV, 0)
err = global.DB.NewSelect().Model(&currentActivatedCustomers).
Where("park_id = ?", parkId).
Where("enabled = ?", true).
Scan(ctx)
if err != nil {
return "", err
}
var parkInfo = new(model.Park)
err = global.DB.NewSelect().Model(parkInfo).
Where("id = ?", parkId).
Scan(ctx)
if err != nil || parkInfo == nil {
return "", exceptions.NewNotFoundError(fmt.Sprintf("指定园区未找到, %v", err))
}
// 生成新一期的报表
tx, err := global.DB.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return "", err
}
// 插入已经生成的报表索引信息和园区概况信息
newReport := model.Report{
Id: uuid.New().String(),
ParkId: parkId,
Period: period,
Category: parkInfo.Category,
SubmeterType: parkInfo.SubmeterType,
StepState: model.NewSteps(),
Published: false,
Withdraw: model.REPORT_NOT_WITHDRAW,
}
newReportSummary := model.ReportSummary{
ReportId: newReport.Id,
}
_, err = tx.NewInsert().Model(&newReport).Exec(ctx)
if err != nil {
tx.Rollback()
return "", err
}
_, err = tx.NewInsert().Model(&newReportSummary).Exec(ctx)
if err != nil {
tx.Rollback()
return "", err
}
// 生成并插入户表信息
var inserts = make([]model.EndUserDetail, 0)
for _, customer := range currentActivatedCustomers {
newEndUser := model.EndUserDetail{
ReportId: newReport.Id,
ParkId: parkId,
MeterId: customer.Code,
Seq: customer.Seq,
Ratio: customer.Ratio,
Address: customer.Address,
CustomerName: customer.CustomerName,
ContactName: customer.ContactName,
ContactPhone: customer.ContactPhone,
IsPublicMeter: customer.IsPublicMeter,
LastPeriodOverall: decimal.Zero,
LastPeriodCritical: decimal.Zero,
LastPeriodPeak: decimal.Zero,
LastPeriodFlat: decimal.Zero,
LastPeriodValley: decimal.Zero,
}
if lastPeriod, ok := indexedLastPeriodCustomers[customer.Code]; ok {
newEndUser.LastPeriodOverall = lastPeriod.CurrentPeriodOverall
newEndUser.LastPeriodCritical = lastPeriod.CurrentPeriodCritical
newEndUser.LastPeriodPeak = lastPeriod.CurrentPeriodPeak
newEndUser.LastPeriodFlat = lastPeriod.CurrentPeriodFlat
newEndUser.LastPeriodValley = lastPeriod.CurrentPeriodValley
}
inserts = append(inserts, newEndUser)
}
if len(inserts) > 0 {
_, err = tx.NewInsert().Model(&inserts).Exec(ctx)
if err != nil {
tx.Rollback()
return "", err
}
}
err = tx.Commit()
if err != nil {
tx.Rollback()
return "", err
}
cache.AbolishRelation("report")
return newReport.Id, nil
copier.Copy(&simplifiedPark, park)
copier.Copy(&simplifiedReport, elem)
acc = append(acc, &vo.ReportIndexQueryResponse{
Park: simplifiedPark,
Report: lo.ToPtr(simplifiedReport),
})
return acc
}, make([]*vo.ReportIndexQueryResponse, 0))
return assembled, nil
}
func (_ReportService) RetreiveReportIndex(rid string) (*model.Report, error) {
if cachedReport, _ := cache.RetreiveEntity[model.Report]("report", rid); cachedReport != nil {
return cachedReport, nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
var report = new(model.Report)
err := global.DB.NewSelect().Model(report).
Where("id = ?", rid).
Scan(ctx)
// 获取指定报表中的包含索引、园区以及用户信息的详细信息
func (rs _ReportService) RetrieveReportIndexDetail(rid string) (*model.UserDetail, *model.Park, *model.ReportIndex, error) {
index, err := repository.ReportRepository.GetReportIndex(rid)
if err != nil {
return nil, err
rs.log.Error("未能获取到指定报表的索引", zap.Error(err))
return nil, nil, nil, exceptions.NewNotFoundErrorFromError("未能获取到指定报表的索引", err)
}
cache.CacheEntity(report, []string{fmt.Sprintf("report:%s", rid), "park"}, "report", rid)
return report, nil
park, err := repository.ParkRepository.RetrieveParkDetail(index.Park)
if err != nil {
rs.log.Error("未能获取到指定报表对应的园区详细信息", zap.Error(err))
return nil, nil, nil, exceptions.NewNotFoundErrorFromError("未能获取到指定报表对应的园区详细信息", err)
}
user, err := repository.UserRepository.FindUserDetailById(park.UserId)
if err != nil {
rs.log.Error("未能获取到指定报表对应的用户详细信息", zap.Error(err))
return nil, nil, nil, exceptions.NewNotFoundErrorFromError("未能获取到指定报表对应的用户详细信息", err)
}
return user, park, index, nil
}
func (_ReportService) RetreiveReportSummary(rid string) (*model.ReportSummary, error) {
if cachedSummary, _ := cache.RetreiveEntity[model.ReportSummary]("report_summary", rid); cachedSummary != nil {
return cachedSummary, nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
var summary = new(model.ReportSummary)
err := global.DB.NewSelect().Model(summary).
Where("report_id = ?", rid).
Scan(ctx)
// 根据给定的园区ID列表查询园区以及用户的详细信息
func (rs _ReportService) queryParkAndUserDetails(pids []string) ([]*model.Park, []*model.UserDetail, error) {
parks, err := repository.ParkRepository.RetrieveParks(pids)
if err != nil {
return nil, err
rs.log.Error("未能获取到相应报表对应的园区详细信息", zap.Error(err))
return make([]*model.Park, 0), make([]*model.UserDetail, 0), exceptions.NewNotFoundErrorFromError("未能获取到相应报表对应的园区详细信息", err)
}
cache.CacheEntity(summary, []string{fmt.Sprintf("report:%s", rid), "park"}, "report_summary", rid)
return summary, nil
}
func (_ReportService) UpdateReportSummary(summary *model.ReportSummary) error {
ctx, cancel := global.TimeoutContext()
defer cancel()
_, err := global.DB.NewUpdate().Model(summary).
WherePK().
Column("overall", "overall_fee", "critical", "critical_fee", "peak", "peak_fee", "valley", "valley_fee", "basic_fee", "adjust_fee").
Exec(ctx)
if err == nil {
cache.AbolishRelation(fmt.Sprintf("report:%s", summary.ReportId))
}
return err
}
func (_ReportService) CalculateSummaryAndFinishStep(reportId string) error {
ctx, cancel := global.TimeoutContext()
defer cancel()
var report = new(model.Report)
err := global.DB.NewSelect().Model(report).Relation("Summary").
Where("r.id = ?", reportId).
Scan(ctx)
if err != nil || report == nil {
return exceptions.NewNotFoundErrorFromError("未找到指定报表", err)
}
tx, err := global.DB.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return err
}
report.Summary.CalculatePrices()
_, err = tx.NewUpdate().Model(report.Summary).
WherePK().
Column("overall_price", "critical_price", "peak_price", "flat", "flat_fee", "flat_price", "valley_price", "consumption_fee").
Exec(ctx)
if err != nil {
tx.Rollback()
return err
}
report.StepState.Summary = true
_, err = tx.NewUpdate().Model(report).
WherePK().
Column("step_state").
Exec(ctx)
if err != nil {
tx.Rollback()
return err
}
err = tx.Commit()
if err != nil {
tx.Rollback()
return err
}
cache.AbolishRelation(fmt.Sprintf("report:%s", reportId))
return nil
}
func (_ReportService) FetchWillDulutedMaintenanceFees(reportId string) ([]model.WillDilutedFee, error) {
if cachedFees, _ := cache.RetreiveSearch[[]model.WillDilutedFee]("will_diluted_fee", "report", reportId); cachedFees != nil {
return *cachedFees, nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
fees := make([]model.WillDilutedFee, 0)
err := global.DB.NewSelect().Model(&fees).
Where("report_id = ?", reportId).
Order("created_at asc").
Scan(ctx)
if err != nil {
return make([]model.WillDilutedFee, 0), nil
}
relations := lo.Map(fees, func(f model.WillDilutedFee, _ int) string {
return fmt.Sprintf("will_diluted_fee:%s", f.Id)
userIds := lo.Map(parks, func(elem *model.Park, _ int) string {
return elem.UserId
})
relations = append(relations, fmt.Sprintf("report:will_diluted_fee:%s", reportId), fmt.Sprintf("report:%s", reportId), "park")
cache.CacheSearch(fees, relations, "will_diluted_fee", "report", reportId)
return fees, nil
}
func (_ReportService) CreateTemporaryWillDilutedMaintenanceFee(fee model.WillDilutedFee) error {
ctx, cancel := global.TimeoutContext()
defer cancel()
fee.Id = utils.UUIDString()
_, err := global.DB.NewInsert().Model(&fee).Exec(ctx)
cache.AbolishRelation(fmt.Sprintf("report:will_diluted_fee:%s", fee.ReportId))
return err
}
func (_ReportService) BatchSaveMaintenanceFee(reportId string, fees []model.WillDilutedFee) error {
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.BeginTx(ctx, &sql.TxOptions{})
users, err := repository.UserRepository.RetrieveUsersDetail(userIds)
if err != nil {
return err
rs.log.Error("未能获取到相应报表对应的用户详细信息", zap.Error(err))
return make([]*model.Park, 0), make([]*model.UserDetail, 0), exceptions.NewNotFoundErrorFromError("未能获取到相应报表对应的用户详细信息", err)
}
// 首先删除所有预定义的部分条件是指定报表IDSourceID不为空。
_, err = tx.NewDelete().Model((*model.WillDilutedFee)(nil)).
Where("report_id = ?", reportId).
Where("source_id is not null").
Exec(ctx)
return parks, users, nil
}
// 查询指定的核算报表列表
func (rs _ReportService) QueryReports(uid, pid *string, page uint, keyword *string, periodBegin, periodEnd *types.Date) ([]*vo.ComprehensiveReportQueryResponse, int64, error) {
rs.log.Info("查询指定的核算报表列表", zap.Stringp("User", uid), zap.Stringp("Park", pid), zap.Uint("Page", page), zap.Stringp("Keyword", keyword), logger.DateFieldp("PeriodBegin", periodBegin), logger.DateFieldp("PeriodEnd", periodEnd))
reports, total, err := repository.ReportRepository.ComprehensiveReportSearch(uid, pid, page, keyword, periodBegin, periodEnd)
if err != nil {
tx.Rollback()
return err
rs.log.Error("未能查询到指定的核算报表列表", zap.Error(err))
return make([]*vo.ComprehensiveReportQueryResponse, 0), 0, err
}
// 然后插入新的记录
_, err = tx.NewInsert().Model(&fees).Exec(ctx)
parkIds := lo.Map(reports, func(elem *model.ReportIndex, _ int) string {
return elem.Park
})
parks, users, err := rs.queryParkAndUserDetails(parkIds)
if err != nil {
return err
return make([]*vo.ComprehensiveReportQueryResponse, 0), 0, err
}
err = tx.Commit()
if err != nil {
tx.Rollback()
return err
}
cache.AbolishRelation(fmt.Sprintf("report:will_diluted_fee:%s", reportId))
return nil
}
func (_ReportService) UpdateMaintenanceFee(feeId string, updates map[string]interface{}) (err error) {
ctx, cancel := global.TimeoutContext()
defer cancel()
updates["last_modified_at"] = lo.ToPtr(time.Now())
_, err = global.DB.NewUpdate().Model(&updates).TableExpr("will_diluted_fee").
Where("id = ?", feeId).
Exec(ctx)
cache.AbolishRelation(fmt.Sprintf("will_diluted_fee:%s", feeId))
return
}
func (_ReportService) DeleteWillDilutedFee(fee string) (err error) {
ctx, cancel := global.TimeoutContext()
defer cancel()
_, err = global.DB.NewDelete().Model((*model.WillDilutedFee)(nil)).
Where("id = ?", fee).
Exec(ctx)
cache.AbolishRelation(fmt.Sprintf("will_diluted_fee:%s", fee))
return
}
func (_ReportService) ProgressReportWillDilutedFee(report model.Report) (err error) {
ctx, cancel := global.TimeoutContext()
defer cancel()
report.StepState.WillDiluted = true
_, err = global.DB.NewUpdate().Model(&report).
WherePK().
Column("step_state").
Exec(ctx)
cache.AbolishRelation(fmt.Sprintf("report:%s", report.Id))
return
}
func (_ReportService) ProgressReportRegisterEndUser(report model.Report) (err error) {
ctx, cancel := global.TimeoutContext()
defer cancel()
report.StepState.Submeter = true
_, err = global.DB.NewUpdate().Model(&report).
WherePK().
Column("step_state").
Exec(ctx)
cache.AbolishRelation(fmt.Sprintf("report:%s", report.Id))
return
}
func (_ReportService) ProgressReportCalculate(report model.Report) (err error) {
ctx, cancel := global.TimeoutContext()
defer cancel()
report.StepState.Calculate = true
_, err = global.DB.NewUpdate().Model(&report).
WherePK().
Column("step_state").
Exec(ctx)
cache.AbolishRelation(fmt.Sprintf("report:%s", report.Id))
return
}
func (_ReportService) RetreiveParkEndUserMeterType(reportId string) (int, error) {
if cachedType, _ := cache.RetreiveEntity[int]("park_end_user_meter_type", fmt.Sprintf("report_%s", reportId)); cachedType != nil {
return *cachedType, nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
var mType int
err := global.DB.NewSelect().Model((*model.Report)(nil)).
Relation("Park", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Column("meter_04kv_type")
}).
ExcludeColumn("*").
Where("r.id = ?", reportId).
Scan(ctx, &mType)
if err != nil {
return -1, err
}
cache.CacheEntity(mType, []string{fmt.Sprintf("report:%s", reportId), "park"}, "park_end_user_meter_type", fmt.Sprintf("report_%s", reportId))
return mType, nil
}
func (_ReportService) PublishReport(report model.Report) (err error) {
ctx, cancel := global.TimeoutContext()
defer cancel()
report.Published = true
report.PublishedAt = lo.ToPtr(time.Now())
report.StepState.Publish = true
_, err = global.DB.NewUpdate().Model(&report).
WherePK().
Column("step_state", "published", "published_at").
Exec(ctx)
cache.AbolishRelation(fmt.Sprintf("report:%s", report.Id))
return
}
func (_ReportService) SearchReport(requestUser, requestPark, requestKeyword string, requestPeriod *time.Time, requestPage int, onlyPublished bool) ([]model.JoinedReportForWithdraw, int64, error) {
var (
conditions = make([]string, 0)
reports = make([]model.Report, 0)
cond = global.DB.NewSelect().
Model(&reports).
Relation("Park").Relation("Park.Enterprise")
)
conditions = append(conditions, strconv.Itoa(requestPage))
if onlyPublished {
cond = cond.Where("r.published = ?", true)
}
conditions = append(conditions, strconv.FormatBool(onlyPublished))
if len(requestUser) > 0 {
cond = cond.Where("park.user_id = ?", requestUser)
conditions = append(conditions, requestUser)
}
if len(requestPark) > 0 {
cond = cond.Where("park.id = ?", requestPark)
conditions = append(conditions, requestPark)
}
if requestPeriod != nil {
cond = cond.Where("r.period = ?", *requestPeriod)
conditions = append(conditions, strconv.FormatInt(requestPeriod.Unix(), 10))
}
if len(requestKeyword) > 0 {
keywordCond := "%" + requestKeyword + "%"
cond = cond.WhereGroup(" and ", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("park.name like ?", keywordCond).
WhereOr("park__enterprise.name like ?", keywordCond).
WhereOr("park__enterprise.abbr like ?", keywordCond).
WhereOr("park.abbr like ?", keywordCond).
WhereOr("park__enterprise.address like ?", keywordCond).
WhereOr("park.address like ?", keywordCond)
})
conditions = append(conditions, requestKeyword)
}
if cachedTotal, err := cache.RetreiveCount("join_report_for_withdraw", conditions...); cachedTotal != -1 && err == nil {
if cachedRecords, _ := cache.RetreiveSearch[[]model.JoinedReportForWithdraw]("join_report_for_withdraw", conditions...); cachedRecords != nil {
return *cachedRecords, cachedTotal, nil
}
}
ctx, cancel := global.TimeoutContext()
defer cancel()
startItem := (requestPage - 1) * config.ServiceSettings.ItemsPageSize
total, err := cond.Limit(config.ServiceSettings.ItemsPageSize).
Offset(startItem).
ScanAndCount(ctx)
records := make([]model.JoinedReportForWithdraw, 0)
relations := []string{"report", "park"}
for _, r := range reports {
records = append(records, model.JoinedReportForWithdraw{
Report: r,
Park: model.FromPark(*r.Park),
User: model.FromUserDetail(*r.Park.Enterprise),
})
relations = append(relations, fmt.Sprintf("report:%s", r.Id))
}
cache.CacheCount(relations, "join_report_for_withdraw", int64(total), conditions...)
cache.CacheSearch(records, relations, "join_report_for_withdraw", conditions...)
return records, int64(total), err
}
func (_ReportService) AssembleReportPublicity(reportId string) (*model.Publicity, error) {
if cachedPublicity, _ := cache.RetreiveEntity[model.Publicity]("publicity", reportId); cachedPublicity != nil {
return cachedPublicity, nil
}
// 资料准备
ctx, cancel := global.TimeoutContext()
defer cancel()
var report = new(model.Report)
err := global.DB.NewSelect().Model(report).
Relation("Summary").Relation("WillDilutedFees").Relation("EndUsers").
Relation("Park").Relation("Park.Enterprise").
Where("r.id = ?", reportId).
Scan(ctx)
if err != nil {
return nil, exceptions.NewNotFoundErrorFromError("未找到指定的公示报表", err)
}
// 组合数据
paidPart := model.PaidPart{
Overall: report.Summary.Overall,
OverallPrice: report.Summary.OverallPrice.Decimal,
ConsumptionFee: report.Summary.ConsumptionFee.Decimal,
OverallFee: report.Summary.OverallFee,
Critical: decimal.NewNullDecimal(report.Summary.Critical),
CriticalPrice: report.Summary.CriticalPrice,
CriticalFee: decimal.NewNullDecimal(report.Summary.CriticalFee),
Peak: decimal.NewNullDecimal(report.Summary.Peak),
PeakPrice: report.Summary.PeakPrice,
PeakFee: decimal.NewNullDecimal(report.Summary.PeakFee),
Flat: decimal.NewNullDecimal(report.Summary.Flat),
FlatPrice: report.Summary.FlatPrice,
FlatFee: decimal.NewNullDecimal(report.Summary.FlatFee),
Valley: decimal.NewNullDecimal(report.Summary.Valley),
ValleyPrice: report.Summary.ValleyPrice,
ValleyFee: decimal.NewNullDecimal(report.Summary.ValleyFee),
BasicFee: report.Summary.BasicFee,
AdjustFee: report.Summary.AdjustFee,
}
endUserSummary := model.ConsumptionOverallPart{
Overall: report.Summary.Customers.Consumption.Decimal,
OverallPrice: report.Summary.OverallPrice.Decimal,
ConsumptionFee: report.Summary.Customers.ConsumptionFee.Decimal,
OverallFee: report.Summary.Customers.ConsumptionFee.Decimal,
Critical: report.Summary.Customers.Critical,
CriticalPrice: report.Summary.CriticalPrice,
CriticalFee: report.Summary.Customers.CriticalFee,
Peak: report.Summary.Customers.Peak,
PeakPrice: report.Summary.PeakPrice,
PeakFee: report.Summary.Customers.PeakFee,
Flat: report.Summary.Customers.Flat,
FlatPrice: report.Summary.FlatPrice,
FlatFee: report.Summary.Customers.FlatFee,
Valley: report.Summary.Customers.Valley,
ValleyPrice: report.Summary.ValleyPrice,
ValleyFee: report.Summary.Customers.ValleyFee,
Proportion: report.Summary.Customers.Proportion.Decimal,
}
lossPart := model.LossPart{
Quantity: report.Summary.Loss.Decimal,
Price: report.Summary.OverallPrice.Decimal,
ConsumptionFee: report.Summary.LossFee.Decimal,
Proportion: report.Summary.LossProportion.Decimal,
AuthorizeQuantity: report.Summary.AuthorizeLoss.Decimal,
AuthorizeConsumptionFee: report.Summary.AuthorizeLossFee.Decimal,
}
publicSummary := model.ConsumptionOverallPart{
Overall: report.Summary.Publics.Consumption.Decimal,
OverallPrice: report.Summary.OverallPrice.Decimal,
ConsumptionFee: report.Summary.Publics.ConsumptionFee.Decimal,
OverallFee: report.Summary.Publics.ConsumptionFee.Decimal,
Critical: report.Summary.Publics.Critical,
CriticalPrice: report.Summary.CriticalPrice,
CriticalFee: report.Summary.Publics.CriticalFee,
Peak: report.Summary.Publics.Peak,
PeakPrice: report.Summary.PeakPrice,
PeakFee: report.Summary.Publics.PeakFee,
Flat: report.Summary.Publics.Flat,
FlatPrice: report.Summary.FlatPrice,
FlatFee: report.Summary.Publics.FlatFee,
Valley: report.Summary.Publics.Valley,
ValleyPrice: report.Summary.ValleyPrice,
ValleyFee: report.Summary.Publics.ValleyFee,
Proportion: report.Summary.Publics.Proportion.Decimal,
}
otherCollection := model.OtherShouldCollectionPart{
LossFee: report.Summary.AuthorizeLossFee,
BasicFees: report.Summary.BasicFee.Add(report.Summary.AdjustFee),
}
finalAdjustFee := lossPart.AuthorizeConsumptionFee.Add(otherCollection.BasicFees)
var adjustPrice = decimal.Zero
if !endUserSummary.Overall.Equal(decimal.Zero) {
adjustPrice = finalAdjustFee.Div(endUserSummary.Overall).RoundBank(8)
}
var adjustProportion = decimal.Zero
if !paidPart.OverallFee.Equal(decimal.Zero) {
adjustProportion = finalAdjustFee.Div(paidPart.OverallFee.Add(finalAdjustFee)).RoundBank(8)
}
maintenanceFees := model.MaintenancePart{
BasicFees: otherCollection.BasicFees,
LossFee: lossPart.AuthorizeConsumptionFee,
AdjustFee: finalAdjustFee,
LossProportion: lossPart.Proportion,
AdjustPrice: adjustPrice,
AdjustProportion: adjustProportion,
}
if maintenanceFees.LossProportion.GreaterThan(decimal.NewFromFloat(0.1)) {
maintenanceFees.LossProportion = decimal.NewFromFloat(0.1)
}
endUsers := lo.Map(
report.EndUsers,
func(elem *model.EndUserDetail, index int) model.EndUserSummary {
return model.EndUserSummary{
CustomerName: elem.CustomerName,
Address: elem.Address,
MeterId: elem.MeterId,
IsPublicMeter: elem.IsPublicMeter,
Overall: elem.Overall.Decimal,
OverallPrice: report.Summary.OverallPrice.Decimal,
OverallFee: elem.OverallFee.Decimal,
Critical: elem.Critical,
CriticalFee: elem.CriticalFee,
Peak: elem.Peak,
PeakFee: elem.PeakFee,
Valley: elem.Valley,
ValleyFee: elem.ValleyFee,
Loss: elem.LossDiluted.Decimal,
LossFee: elem.LossFeeDiluted.Decimal,
}
assembled := lo.Reduce(
reports,
func(acc []*vo.ComprehensiveReportQueryResponse, elem *model.ReportIndex, _ int) []*vo.ComprehensiveReportQueryResponse {
park, _ := lo.Find(parks, func(park *model.Park) bool {
return park.Id == elem.Park
})
user, _ := lo.Find(users, func(user *model.UserDetail) bool {
return user.Id == park.UserId
})
var (
simplifiedUser vo.SimplifiedUserDetail
simplifiedPark vo.SimplifiedParkDetail
simplifiedReport vo.SimplifiedReportIndex
)
copier.Copy(&simplifiedUser, user)
copier.Copy(&simplifiedPark, park)
copier.Copy(&simplifiedReport, elem)
acc = append(acc, &vo.ComprehensiveReportQueryResponse{
User: simplifiedUser,
Park: simplifiedPark,
Report: simplifiedReport,
})
return acc
},
make([]*vo.ComprehensiveReportQueryResponse, 0),
)
publicity := &model.Publicity{
Report: *report,
Park: *report.Park,
User: *report.Park.Enterprise,
Paid: paidPart,
EndUser: endUserSummary,
Loss: lossPart,
PublicConsumptionOverall: publicSummary,
OtherCollections: otherCollection,
Maintenance: maintenanceFees,
EndUserDetails: endUsers,
}
cache.CacheEntity(publicity, []string{fmt.Sprintf("publicity:%s", reportId), fmt.Sprintf("report:%s", reportId), "report", "park"}, "publicity", reportId)
return publicity, nil
return assembled, total, nil
}
// 查询当前待审核的核算报表撤回申请列表
func (rs _ReportService) ListWithdrawalRequests(page uint, keyword *string) ([]*vo.ComprehensiveReportQueryResponse, int64, error) {
rs.log.Info("查询当前待审核的核算报表撤回申请列表", zap.Uint("Page", page), zap.Stringp("Keyword", keyword))
reports, total, err := repository.ReportRepository.ListWithdrawAppliedReports(page, keyword)
if err != nil {
rs.log.Error("未能查询到当前待审核的核算报表撤回申请列表", zap.Error(err))
return make([]*vo.ComprehensiveReportQueryResponse, 0), 0, err
}
parkIds := lo.Map(reports, func(elem *model.ReportIndex, _ int) string {
return elem.Park
})
parks, users, err := rs.queryParkAndUserDetails(parkIds)
if err != nil {
return make([]*vo.ComprehensiveReportQueryResponse, 0), 0, err
}
assembled := lo.Reduce(
reports,
func(acc []*vo.ComprehensiveReportQueryResponse, elem *model.ReportIndex, _ int) []*vo.ComprehensiveReportQueryResponse {
park, _ := lo.Find(parks, func(park *model.Park) bool {
return park.Id == elem.Park
})
user, _ := lo.Find(users, func(user *model.UserDetail) bool {
return user.Id == park.UserId
})
var (
simplifiedUser vo.SimplifiedUserDetail
simplifiedPark vo.SimplifiedParkDetail
simplifiedReport vo.SimplifiedReportIndex
)
copier.Copy(&simplifiedUser, user)
copier.Copy(&simplifiedPark, park)
copier.Copy(&simplifiedReport, elem)
acc = append(acc, &vo.ComprehensiveReportQueryResponse{
User: simplifiedUser,
Park: simplifiedPark,
Report: simplifiedReport,
})
return acc
},
make([]*vo.ComprehensiveReportQueryResponse, 0),
)
return assembled, total, nil
}

51
service/synchronize.go Normal file
View File

@@ -0,0 +1,51 @@
package service
import (
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/repository"
"electricity_bill_calc/vo"
"github.com/doug-martin/goqu/v9"
"go.uber.org/zap"
)
type _SynchronizeService struct {
log *zap.Logger
ds goqu.DialectWrapper
}
var SynchronizeService = _SynchronizeService{
log: logger.Named("Service", "Synchronize"),
ds: goqu.Dialect("postgres"),
}
func (ss _SynchronizeService) CreateSynchronizeConfiguration(userId string, form *vo.SynchronizeConfigurationCreateForm) error {
ss.log.Info("创建一条新的同步配置", zap.String("user id", userId))
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.Begin(ctx)
if err != nil {
ss.log.Error("无法启动数据库事务。", zap.Error(err))
return err
}
ok, err := repository.SynchronizeRepository.CreateSynchronizeConfiguration(tx, ctx, userId, form)
if err != nil {
ss.log.Error("无法创建新的同步配置。", zap.Error(err))
tx.Rollback(ctx)
return err
}
if !ok {
ss.log.Error("数据库未能记录新的同步配置。")
tx.Rollback(ctx)
return err
}
err = tx.Commit(ctx)
if err != nil {
ss.log.Error("未能成功提交数据库事务。", zap.Error(err))
tx.Rollback(ctx)
return err
}
return nil
}

241
service/tenement.go Normal file
View File

@@ -0,0 +1,241 @@
package service
import (
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"electricity_bill_calc/vo"
"fmt"
"github.com/samber/lo"
"go.uber.org/zap"
)
type _TenementService struct {
log *zap.Logger
}
var TenementService = _TenementService{
log: logger.Named("Service", "Tenement"),
}
// 列出指定商户下的全部计量表计,不包含公摊表计
func (ts _TenementService) ListMeter(pid, tid string) ([]*model.MeterDetail, error) {
ts.log.Info("列出指定商户下的全部表计", zap.String("Park", pid), zap.String("Tenement", tid))
meterCodes, err := repository.TenementRepository.ListMeterCodesBelongsTo(pid, tid)
if err != nil {
ts.log.Error("列出指定商户下的全部表计失败,未能获取属于商户的表计编号", zap.Error(err))
return make([]*model.MeterDetail, 0), err
}
meters, err := repository.MeterRepository.ListMetersByIDs(pid, meterCodes)
if err != nil {
ts.log.Error("列出指定商户下的全部表计失败,未能获取表计编号对应的表计详细信息", zap.Error(err))
return make([]*model.MeterDetail, 0), err
}
return meters, nil
}
// 增加一个新的商户
func (ts _TenementService) CreateTenementRecord(pid string, creationForm *vo.TenementCreationForm) error {
ts.log.Info("增加一个新的商户", zap.String("Park", pid), zap.Any("Form", creationForm))
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.Begin(ctx)
if err != nil {
ts.log.Error("增加一个新商户失败的,未能启动数据库事务", zap.Error(err))
return fmt.Errorf("未能启动数据库事务,%w", err)
}
err = repository.TenementRepository.AddTenement(tx, ctx, pid, creationForm)
if err != nil {
ts.log.Error("增加一个新商户失败的,未能增加商户记录", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("未能增加商户记录,%w", err)
}
err = tx.Commit(ctx)
if err != nil {
ts.log.Error("增加一个新商户失败的,未能提交数据库事务", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("未能提交数据库事务,%w", err)
}
return nil
}
// 向商户绑定一个新表计
func (ts _TenementService) BindMeter(pid, tid, meterCode string, reading *vo.MeterReadingForm) error {
ts.log.Info("向商户绑定一个新表计", zap.String("Park", pid), zap.String("Tenement", tid), zap.String("Meter", meterCode))
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.Begin(ctx)
if err != nil {
ts.log.Error("向商户绑定一个新表计失败,未能启动数据库事务", zap.Error(err))
return fmt.Errorf("未能启动数据库事务,%w", err)
}
meterDetail, err := repository.MeterRepository.FetchMeterDetail(pid, meterCode)
if err != nil {
ts.log.Error("向商户绑定一个新表计失败,未能获取表计详细信息", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("未能获取表计详细信息,%w", err)
}
err = repository.TenementRepository.BindMeter(tx, ctx, pid, tid, meterCode)
if err != nil {
ts.log.Error("向商户绑定一个新表计失败,未能绑定表计", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("未能绑定表计,%w", err)
}
ok, err := repository.MeterRepository.RecordReading(tx, ctx, pid, meterCode, meterDetail.MeterType, meterDetail.Ratio, reading)
if err != nil {
ts.log.Error("向商户绑定一个新表计失败,记录表计读数出现错误", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("记录表计读数出现错误,%w", err)
}
if !ok {
ts.log.Error("向商户绑定一个新表计失败,记录表计读数失败")
tx.Rollback(ctx)
return fmt.Errorf("记录表计读数失败")
}
err = tx.Commit(ctx)
if err != nil {
ts.log.Error("向商户绑定一个新表计失败,未能提交数据库事务", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("未能提交数据库事务,%w", err)
}
return nil
}
// 解除商户与指定表计的绑定
func (ts _TenementService) UnbindMeter(pid, tid, meterCode string, reading *vo.MeterReadingForm) error {
ts.log.Info("解除商户与指定表计的绑定", zap.String("Park", pid), zap.String("Tenement", tid), zap.String("Meter", meterCode))
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.Begin(ctx)
if err != nil {
ts.log.Error("解除商户与指定表计的绑定失败,未能启动数据库事务", zap.Error(err))
return fmt.Errorf("未能启动数据库事务,%w", err)
}
meterDetail, err := repository.MeterRepository.FetchMeterDetail(pid, meterCode)
if err != nil {
ts.log.Error("解除商户与指定表计的绑定失败,未能获取表计详细信息", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("未能获取表计详细信息,%w", err)
}
err = repository.TenementRepository.UnbindMeter(tx, ctx, pid, tid, meterCode)
if err != nil {
ts.log.Error("解除商户与指定表计的绑定失败,未能解除绑定", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("未能解除绑定,%w", err)
}
ok, err := repository.MeterRepository.RecordReading(tx, ctx, pid, meterCode, meterDetail.MeterType, meterDetail.Ratio, reading)
if err != nil {
ts.log.Error("解除商户与指定表计的绑定失败,记录表计读数出现错误", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("记录表计读数出现错误,%w", err)
}
if !ok {
ts.log.Error("解除商户与指定表计的绑定失败,记录表计读数失败")
tx.Rollback(ctx)
return fmt.Errorf("记录表计读数失败")
}
err = tx.Commit(ctx)
if err != nil {
ts.log.Error("解除商户与指定表计的绑定失败,未能提交数据库事务", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("未能提交数据库事务,%w", err)
}
return nil
}
// 迁出指定商户
func (ts _TenementService) MoveOutTenement(pid, tid string, reading []*vo.MeterReadingFormWithCode) error {
ts.log.Info("迁出指定商户", zap.String("Park", pid), zap.String("Tenement", tid))
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.Begin(ctx)
if err != nil {
ts.log.Error("迁出指定商户失败,未能启动数据库事务", zap.Error(err))
return fmt.Errorf("未能启动数据库事务,%w", err)
}
meterCodes, err := repository.TenementRepository.ListMeterCodesBelongsTo(pid, tid)
if err != nil {
ts.log.Error("迁出指定商户失败,未能获取属于商户的表计编号", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("未能获取属于商户的表计编号,%w", err)
}
meters, err := repository.MeterRepository.ListMetersByIDs(pid, meterCodes)
if err != nil {
ts.log.Error("迁出指定商户失败,未能获取表涉及计编号对应的表计详细信息", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("未能获取涉及表计编号对应的表计详细信息,%w", err)
}
for _, meterCode := range meterCodes {
meterDetail, exists := lo.Find(meters, func(m *model.MeterDetail) bool {
return m.Code == meterCode
})
if !exists {
ts.log.Error("迁出指定商户失败,找不到指定表计的详细信息", zap.String("Meter", meterCode))
tx.Rollback(ctx)
return fmt.Errorf("找不到指定表计[%s]的详细信息,%w", meterCode, err)
}
if meterDetail.MeterType != model.METER_INSTALLATION_TENEMENT {
ts.log.Error("迁出指定商户失败,需要解绑的表计不是商户表计", zap.String("Meter", meterCode))
tx.Rollback(ctx)
return fmt.Errorf("需要解绑的表计[%s]不是商户表计,%w", meterCode, err)
}
reading, exists := lo.Find(reading, func(r *vo.MeterReadingFormWithCode) bool {
return r.Code == meterCode
})
if !exists {
ts.log.Error("迁出指定商户失败,找不到指定表计的抄表信息", zap.String("Meter", meterCode))
tx.Rollback(ctx)
return fmt.Errorf("找不到指定表计[%s]的抄表信息,%w", meterCode, err)
}
if reading.Validate() {
ts.log.Error("迁出指定商户失败,表计读数不能正确配平,尖锋电量、峰电量、谷电量之和超过总电量。", zap.String("Meter", meterCode))
tx.Rollback(ctx)
return fmt.Errorf("表计[%s]读数不能正确配平,尖锋电量、峰电量、谷电量之和超过总电量。", meterCode)
}
err = repository.TenementRepository.UnbindMeter(tx, ctx, pid, tid, meterCode)
if err != nil {
ts.log.Error("迁出指定商户失败,未能解除表计绑定", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("未能解除表计[%s]绑定,%w", meterCode, err)
}
ok, err := repository.MeterRepository.RecordReading(tx, ctx, pid, meterCode, meterDetail.MeterType, meterDetail.Ratio, &reading.MeterReadingForm)
if err != nil {
ts.log.Error("迁出指定商户失败,记录表计抄表信息出现错误", zap.String("Meter", meterCode), zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("记录表计[%s]抄表信息出现错误,%w", meterCode, err)
}
if !ok {
ts.log.Error("迁出指定商户失败,记录表计抄表数据失败", zap.String("Meter", meterCode))
tx.Rollback(ctx)
return fmt.Errorf("记录表计[%s]抄表数据失败", meterCode)
}
}
err = repository.TenementRepository.MoveOut(tx, ctx, pid, tid)
if err != nil {
ts.log.Error("迁出指定商户失败,未能迁出指定商户", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("未能迁出指定商户,%w", err)
}
err = tx.Commit(ctx)
if err != nil {
ts.log.Error("迁出指定商户失败,未能提交数据库事务", zap.Error(err))
tx.Rollback(ctx)
return fmt.Errorf("未能提交数据库事务,%w", err)
}
return nil
}

View File

@@ -1,447 +1,150 @@
package service
import (
"database/sql"
"electricity_bill_calc/cache"
"electricity_bill_calc/config"
"electricity_bill_calc/exceptions"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"electricity_bill_calc/tools"
"fmt"
"strconv"
"electricity_bill_calc/tools/serial"
"electricity_bill_calc/types"
"time"
"github.com/fufuok/utils"
"github.com/fufuok/utils/xhash"
"github.com/google/uuid"
"github.com/uptrace/bun"
"github.com/samber/lo"
"go.uber.org/zap"
)
type _UserService struct {
l *zap.Logger
log *zap.Logger
}
var UserService = _UserService{
l: logger.Named("Service", "User"),
log: logger.Named("Service", "User"),
}
func (u _UserService) ProcessEnterpriseUserLogin(username, password string) (*model.Session, error) {
user, err := u.findUserWithCredentialsByUsername(username)
func (us _UserService) MatchUserPassword(controlCode, testCode string) bool {
hashedCode := xhash.Sha512Hex(testCode)
return controlCode == hashedCode
}
// 处理用户登录的通用过程。
func (us _UserService) processUserLogin(username, password string, userType []int16) (*model.User, *model.UserDetail, error) {
us.log.Info("处理用户登录。", zap.String("username", username))
user, err := repository.UserRepository.FindUserByUsername(username)
if err != nil {
return nil, err
us.log.Error("处理用户登录失败。", zap.String("username", username), zap.Error(err))
return nil, nil, err
}
if user == nil {
return nil, exceptions.NewAuthenticationError(404, "用户不存在。")
us.log.Warn("处理用户登录失败,用户不存在。", zap.String("username", username))
return nil, nil, exceptions.NewAuthenticationError(404, "用户不存在。")
}
if user.Type != 0 {
return nil, exceptions.NewAuthenticationError(400, "用户类型不正确。")
if !lo.Contains(userType, user.UserType) {
us.log.Warn("处理用户登录失败,用户类型错误。", zap.String("username", username), zap.Int16s("user type", userType))
return nil, nil, exceptions.NewAuthenticationError(400, "用户类型不正确。")
}
if !user.Enabled {
return nil, exceptions.NewAuthenticationError(403, "用户已被禁用。")
}
hashedPassword := utils.Sha512Hex(password)
if hashedPassword != user.Password {
return nil, exceptions.NewAuthenticationError(402, "用户凭据不正确。")
us.log.Warn("处理用户登录失败,用户已被禁用。", zap.String("username", username))
return nil, nil, exceptions.NewAuthenticationError(403, "用户已被禁用。")
}
if user.ResetNeeded {
us.log.Warn("处理用户登录失败,用户需要重置密码。", zap.String("username", username))
authErr := exceptions.NewAuthenticationError(401, "用户凭据已失效。")
authErr.NeedReset = true
return nil, authErr
return nil, nil, authErr
}
userDetial, _ := u.retreiveUserDetail(user.Id)
if userDetial.ServiceExpiration.Time.Before(time.Now()) {
return nil, exceptions.NewAuthenticationError(406, "用户服务期限已过。")
if !us.MatchUserPassword(user.Password, password) {
us.log.Warn("处理用户登录失败,密码错误。", zap.String("username", username))
return nil, nil, exceptions.NewAuthenticationError(402, "用户凭据不正确。")
}
session := &model.Session{
Token: uuid.New().String(),
Uid: user.Id,
Type: user.Type,
Name: user.Username,
ExpiresAt: time.Now().Add(config.ServiceSettings.MaxSessionLife),
userDetail, err := repository.UserRepository.FindUserDetailById(user.Id)
if err != nil {
us.log.Error("处理企业用户登录失败,查询用户详细信息失败。", zap.String("username", username), zap.Error(err))
return nil, nil, err
}
if userDetial != nil {
session.Name = *userDetial.Name
if userDetail.ServiceExpiration.Before(time.Now()) {
us.log.Warn("处理企业用户登录失败,用户服务已过期。", zap.String("username", username))
return nil, nil, exceptions.NewAuthenticationError(406, "用户服务期限已过。")
}
cache.CacheSession(session)
return session, nil
return user, userDetail, nil
}
func (u _UserService) ProcessManagementUserLogin(username, password string) (*model.Session, error) {
user, err := u.findUserWithCredentialsByUsername(username)
// 处理企业用户登录
func (us _UserService) ProcessEnterpriseUserLogin(username, password string) (*model.Session, error) {
user, userDetail, err := us.processUserLogin(username, password, []int16{model.USER_TYPE_ENT})
if err != nil {
us.log.Error("处理企业用户登录失败。", zap.String("username", username), zap.Error(err))
return nil, err
}
if user == nil {
return nil, exceptions.NewAuthenticationError(404, "用户不存在。")
}
if user.Type != 1 && user.Type != 2 {
return nil, exceptions.NewAuthenticationError(400, "用户类型不正确。")
}
if !user.Enabled {
return nil, exceptions.NewAuthenticationError(403, "用户已被禁用。")
}
hashedPassword := utils.Sha512Hex(password)
if hashedPassword != user.Password {
return nil, exceptions.NewAuthenticationError(402, "用户凭据不正确。")
}
if user.ResetNeeded {
authErr := exceptions.NewAuthenticationError(401, "用户凭据已失效。")
authErr.NeedReset = true
return nil, authErr
}
session := &model.Session{
Token: uuid.New().String(),
token, _ := uuid.NewRandom()
userSession := &model.Session{
Uid: user.Id,
Type: user.Type,
Name: user.Username,
Type: user.UserType,
Token: token.String(),
ExpiresAt: types.Now().Add(config.ServiceSettings.MaxSessionLife),
}
if userDetail != nil && userDetail.Name != nil {
userSession.Name = *userDetail.Name
}
err = cache.CacheSession(userSession)
if err != nil {
us.log.Error("处理企业用户登录失败,缓存用户会话失败。", zap.String("username", username), zap.Error(err))
return nil, err
}
return userSession, nil
}
// 处理运维、监管用户登录
func (us _UserService) ProcessManagementUserLogin(username, password string) (*model.Session, error) {
user, userDetail, err := us.processUserLogin(username, password, []int16{model.USER_TYPE_OPS, model.USER_TYPE_SUP})
if err != nil {
us.log.Error("处理运维、监管用户登录失败。", zap.String("username", username), zap.Error(err))
return nil, err
}
token, _ := uuid.NewRandom()
userSession := &model.Session{
Uid: user.Id,
Name: user.Username,
Type: user.UserType,
Token: token.String(),
ExpiresAt: time.Now().Add(config.ServiceSettings.MaxSessionLife),
}
userDetial, _ := u.retreiveUserDetail(user.Id)
if userDetial != nil {
session.Name = *userDetial.Name
if userDetail != nil {
userSession.Name = *userDetail.Name
}
cache.CacheSession(session)
return session, nil
}
func (u _UserService) InvalidUserPassword(uid string) (string, error) {
user, err := u.findUserByID(uid)
if user == nil && err != nil {
return "", exceptions.NewNotFoundError("指定的用户不存在。")
}
ctx, cancel := global.TimeoutContext()
defer cancel()
verifyCode := tools.RandStr(10)
user.Password = utils.Sha512Hex(verifyCode)
user.ResetNeeded = true
res, err := global.DB.NewUpdate().Model(user).WherePK().Column("password", "reset_needed").Exec(ctx)
err = cache.CacheSession(userSession)
if err != nil {
return "", err
}
if affected, _ := res.RowsAffected(); affected > 0 {
// ! 清除与此用户所有相关的记录。
cache.AbolishRelation(fmt.Sprintf("user:%s", uid))
return verifyCode, nil
} else {
return "", exceptions.NewUnsuccessfulOperationError()
us.log.Error("处理运维、监管用户登录失败,缓存用户会话失败。", zap.String("username", username), zap.Error(err))
return nil, err
}
return userSession, nil
}
func (u _UserService) VerifyUserPassword(username, verifyCode string) (bool, error) {
user, err := u.findUserByUsername(username)
if user == nil || err != nil {
return false, exceptions.NewNotFoundError("指定的用户不存在。")
}
hashedVerifyCode := utils.Sha512Hex(verifyCode)
if hashedVerifyCode != user.Password {
return false, nil
} else {
return true, nil
}
}
func (u _UserService) ResetUserPassword(username, password string) (bool, error) {
user, err := u.findUserByUsername(username)
if user == nil || err != nil {
return false, exceptions.NewNotFoundError("指定的用户不存在。")
}
ctx, cancel := global.TimeoutContext()
defer cancel()
user.Password = utils.Sha512Hex(password)
user.ResetNeeded = false
res, err := global.DB.NewUpdate().Model(user).WherePK().Column("password", "reset_needed").Exec(ctx)
if err != nil {
return false, err
}
if affected, _ := res.RowsAffected(); affected > 0 {
cache.AbolishRelation(fmt.Sprintf("user:%s", user.Id))
return true, nil
} else {
return false, nil
}
}
func (_UserService) IsUserExists(uid string) (bool, error) {
if has, _ := cache.CheckExists("user", uid); has {
return has, nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
has, err := global.DB.NewSelect().Model((*model.User)(nil)).Where("id = ?", uid).Exists(ctx)
if has {
cache.CacheExists([]string{"user", fmt.Sprintf("user_%s", uid)}, "user", uid)
}
return has, err
}
func (_UserService) IsUsernameExists(username string) (bool, error) {
if has, _ := cache.CheckExists("user", username); has {
return has, nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
has, err := global.DB.NewSelect().Model((*model.User)(nil)).Where("username = ?", username).Exists(ctx)
if has {
cache.CacheExists([]string{"user"}, "user", username)
}
return has, err
}
func (u _UserService) CreateUser(user *model.User, detail *model.UserDetail) (string, error) {
if len(user.Id) == 0 {
user.Id = uuid.New().String()
}
exists, err := u.IsUserExists(user.Id)
if exists {
return "", exceptions.NewNotFoundError("user already exists")
}
if err != nil {
return "", nil
}
detail.Id = user.Id
verifyCode := tools.RandStr(10)
user.Password = utils.Sha512Hex(verifyCode)
user.ResetNeeded = true
if detail.Name != nil {
finalAbbr := tools.PinyinAbbr(*detail.Name)
detail.Abbr = &finalAbbr
}
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return "", err
}
_, err = tx.NewInsert().Model(user).Exec(ctx)
if err != nil {
tx.Rollback()
return "", fmt.Errorf("user create failed: %w", err)
}
_, err = tx.NewInsert().Model(detail).Exec(ctx)
if err != nil {
tx.Rollback()
return "", fmt.Errorf("user Detail create failed: %w", err)
}
err = tx.Commit()
if err != nil {
tx.Rollback()
return "", fmt.Errorf("transaction commit unsuccessful: %w", err)
}
// ! 广谱关联关系的废除必须是在有新记录加入或者有记录被删除的情况下。
cache.AbolishRelation("user")
return verifyCode, nil
}
func (u _UserService) SwitchUserState(uid string, enabled bool) error {
exists, err := u.IsUserExists(uid)
if !exists {
return exceptions.NewNotFoundError("user not exists")
}
if err != nil {
return err
}
newStateUser := new(model.User)
newStateUser.Id = uid
newStateUser.Enabled = enabled
ctx, cancel := global.TimeoutContext()
defer cancel()
res, err := global.DB.NewUpdate().Model(newStateUser).WherePK().Column("enabled").Exec(ctx)
if affected, _ := res.RowsAffected(); err == nil && affected > 0 {
cache.AbolishRelation(fmt.Sprintf("user:%s", uid))
}
return err
}
func (us _UserService) SearchLimitUsers(keyword string, limit int) ([]model.JoinedUserDetail, error) {
if cachedUsers, _ := cache.RetreiveSearch[[]model.JoinedUserDetail]("join_user_detail", keyword, strconv.Itoa(limit)); cachedUsers != nil {
return *cachedUsers, nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
var users = make([]model.User, 0)
keywordCond := "%" + keyword + "%"
err := global.DB.NewSelect().Model(&users).Relation("Detail").
Where("u.type = ?", model.USER_TYPE_ENT).
WhereGroup(" and ", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("u.username like ?", keywordCond).
WhereOr("detail.name like ?", keywordCond).
WhereOr("detail.abbr like ?", keywordCond).
WhereOr("detail.contact like ?", keywordCond).
WhereOr("detail.address like ?", keywordCond)
}).
Order("u.created_at asc").
Limit(limit).
Offset(0).
Scan(ctx)
if err != nil {
return make([]model.JoinedUserDetail, 0), err
}
var detailedUsers = make([]model.JoinedUserDetail, 0)
var relations = make([]string, 0)
// ! 这里的转换是为了兼容之前使用Xorm时构建的关联关系而存在的
for _, u := range users {
detailedUsers = append(detailedUsers, model.JoinedUserDetail{
UserDetail: *u.Detail,
Id: u.Id,
Username: u.Username,
Type: u.Type,
Enabled: u.Enabled,
})
relations = append(relations, fmt.Sprintf("user:%s", u.Id))
}
relations = append(relations, "user")
cache.CacheSearch(detailedUsers, relations, "join_user_detail", keyword, strconv.Itoa(limit))
return detailedUsers, nil
}
func (_UserService) findUserWithCredentialsByUsername(username string) (*model.UserWithCredentials, error) {
if cachedUser, _ := cache.RetreiveSearch[model.UserWithCredentials]("user_with_credentials", username); cachedUser != nil {
return cachedUser, nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
user := new(model.UserWithCredentials)
err := global.DB.NewSelect().Model(user).Where("username = ?", username).Scan(ctx)
if err == nil {
cache.CacheSearch(*user, []string{fmt.Sprintf("user:%s", user.Id)}, "user_with_credentials", username)
}
return user, err
}
func (u _UserService) findUserByUsername(username string) (*model.User, error) {
if cachedUser, _ := cache.RetreiveSearch[model.User]("user", username); cachedUser != nil {
return cachedUser, nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
user := new(model.User)
err := global.DB.NewSelect().Model(user).Where("username = ?", username).Scan(ctx)
if err == nil {
cache.CacheSearch(*user, []string{fmt.Sprintf("user:%s", user.Id)}, "user", username)
}
return user, err
}
func (_UserService) retreiveUserDetail(uid string) (*model.UserDetail, error) {
if cachedUser, _ := cache.RetreiveEntity[model.UserDetail]("user_detail", uid); cachedUser != nil {
return cachedUser, nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
user := &model.UserDetail{
Id: uid,
}
err := global.DB.NewSelect().Model(user).WherePK().Scan(ctx)
if err == nil {
cache.CacheEntity(*user, []string{fmt.Sprintf("user:%s", uid)}, "user_detail", uid)
}
return user, err
}
func (_UserService) findUserByID(uid string) (*model.User, error) {
cachedUser, _ := cache.RetreiveEntity[model.User]("user", uid)
if cachedUser != nil {
return cachedUser, nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
user := &model.User{
Id: uid,
}
err := global.DB.NewSelect().Model(&user).WherePK().Scan(ctx)
if err == nil {
cache.CacheEntity(*user, []string{fmt.Sprintf("user:%s", uid)}, "user", uid)
}
return user, err
}
func (_UserService) ListUserDetail(keyword string, userType int, userState *bool, page int) ([]model.JoinedUserDetail, int64, error) {
var (
cond = global.DB.NewSelect()
cacheConditions = make([]string, 0)
users = make([]model.User, 0)
)
cond = cond.Model(&users).Relation("Detail")
cacheConditions = append(cacheConditions, strconv.Itoa(page))
cond = cond.Where("detail.id <> ?", "000")
if len(keyword) != 0 {
keywordCond := "%" + keyword + "%"
cond = cond.WhereGroup(" and ", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("u.username like ?", keywordCond).
WhereOr("detail.name like ?", keywordCond)
})
cacheConditions = append(cacheConditions, keyword)
}
if userType != -1 {
cond = cond.Where("u.type = ?", userType)
cacheConditions = append(cacheConditions, strconv.Itoa(userType))
}
if userState != nil {
cond = cond.Where("u.enabled = ?", *userState)
cacheConditions = append(cacheConditions, strconv.FormatBool(*userState))
}
startItem := (page - 1) * config.ServiceSettings.ItemsPageSize
// * 这里利用已经构建完成的条件集合从缓存中获取数据,如果所有数据都可以从缓存中获取,那么就直接返回了。
if cacheCounts, err := cache.RetreiveCount("join_user_detail", cacheConditions...); cacheCounts != -1 && err == nil {
if cachedUsers, _ := cache.RetreiveSearch[[]model.JoinedUserDetail]("join_user_detail", cacheConditions...); cachedUsers != nil {
return *cachedUsers, cacheCounts, nil
// 创建用户账号的通用方法。
func (us _UserService) CreateUserAccount(user *model.User, detail *model.UserDetail) (*string, error) {
if lo.IsEmpty(user.Id) {
var prefix string
if user.UserType == model.USER_TYPE_ENT {
prefix = "E"
} else {
prefix = "S"
}
serial.StringSerialRequestChan <- 1
user.Id = serial.Prefix(prefix, <-serial.StringSerialResponseChan)
detail.Id = user.Id
}
ctx, cancel := global.TimeoutContext()
defer cancel()
total, err := cond.
Limit(config.ServiceSettings.ItemsPageSize).Offset(startItem).
ScanAndCount(ctx)
var (
joinedUsers = make([]model.JoinedUserDetail, 0)
relations = []string{"user"}
)
for _, u := range users {
joinedUsers = append(joinedUsers, model.JoinedUserDetail{
UserDetail: *u.Detail,
Id: u.Id,
Username: u.Username,
Type: u.Type,
Enabled: u.Enabled,
})
relations = append(relations, fmt.Sprintf("user:%s", u.Id))
verifyCode := tools.RandStr(10)
user.Password = xhash.Sha512Hex(verifyCode)
user.ResetNeeded = true
res, err := repository.UserRepository.CreateUser(*user, *detail, nil)
if err != nil || !res {
us.log.Error("创建用户账号失败。", zap.String("username", user.Username), zap.Error(err))
return nil, err
}
cache.CacheCount(relations, "join_user_detail", int64(total), cacheConditions...)
cache.CacheSearch(joinedUsers, relations, "join_user_detail", cacheConditions...)
return joinedUsers, int64(total), err
}
func (_UserService) FetchUserDetail(uid string) (*model.FullJoinedUserDetail, error) {
if cachedUser, _ := cache.RetreiveEntity[model.FullJoinedUserDetail]("full_join_user_detail", uid); cachedUser != nil {
return cachedUser, nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
user := &model.User{}
err := global.DB.NewSelect().Model(user).Relation("Detail").
Where("u.id = ?", uid).
Scan(ctx)
if err == nil {
fullJoinedUser := &model.FullJoinedUserDetail{
User: *user,
UserDetail: *user.Detail,
}
cache.CacheEntity(*fullJoinedUser, []string{fmt.Sprintf("user:%s", uid)}, "full_join_user_detail", uid)
return fullJoinedUser, nil
}
return nil, err
return &verifyCode, nil
}