diff --git a/repository/calculate.go b/repository/calculate.go new file mode 100644 index 0000000..440d9ef --- /dev/null +++ b/repository/calculate.go @@ -0,0 +1,70 @@ +package repository + +import ( + "electricity_bill_calc/global" + "electricity_bill_calc/logger" + "electricity_bill_calc/model" + "electricity_bill_calc/types" + + "github.com/doug-martin/goqu/v9" + _ "github.com/doug-martin/goqu/v9/dialect/postgres" + "github.com/georgysavva/scany/v2/pgxscan" + "go.uber.org/zap" +) + +type _CalculateRepository struct { + log *zap.Logger + ds goqu.DialectWrapper +} + +var CalculateRepository = _CalculateRepository{ + log: logger.Named("Repository", "Calculate"), + ds: goqu.Dialect("postgres"), +} + +// 获取当前正在等待计算的核算任务ID列表 +func (cr _CalculateRepository) ListPendingTasks() ([]string, error) { + cr.log.Info("获取当前正在等待计算的核算任务ID列表") + ctx, cancel := global.TimeoutContext() + defer cancel() + + var ids []string + querySql, queryArgs, _ := cr.ds. + From("report_task"). + Select("id"). + Where(goqu.C("status").Eq(model.REPORT_CALCULATE_TASK_STATUS_PENDING)). + Prepared(true).ToSQL() + if err := pgxscan.Select(ctx, global.DB, &ids, querySql, queryArgs...); err != nil { + cr.log.Error("未能获取到当前正在等待计算的核算任务ID列表", zap.Error(err)) + return nil, err + } + return ids, nil +} + +// 更新指定报表的核算状态 +func (cr _CalculateRepository) UpdateReportTaskStatus(rid string, status int16, message *string) (bool, error) { + cr.log.Info("更新指定报表的核算状态", zap.String("Report", rid), zap.Int16("Status", status)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + currentTime := types.Now() + updateSql, updateArgs, _ := cr.ds. + Update("report_task"). + Set(goqu.Record{ + "status": status, + "last_modified_at": currentTime, + "message": message, + }). + Where(goqu.C("id").Eq(rid)). + Prepared(true).ToSQL() + res, err := global.DB.Exec(ctx, updateSql, updateArgs...) + if err != nil { + cr.log.Error("未能更新指定报表的核算状态", zap.Error(err)) + return false, err + } + if res.RowsAffected() == 0 { + cr.log.Warn("未能保存指定报表的核算状态", zap.String("Report", rid)) + return false, nil + } + return res.RowsAffected() > 0, nil +} diff --git a/service/report.go b/service/report.go new file mode 100644 index 0000000..2380e83 --- /dev/null +++ b/service/report.go @@ -0,0 +1,198 @@ +package service + +import ( + "electricity_bill_calc/exceptions" + "electricity_bill_calc/logger" + "electricity_bill_calc/model" + "electricity_bill_calc/repository" + "electricity_bill_calc/types" + "electricity_bill_calc/vo" + + "github.com/doug-martin/goqu/v9" + _ "github.com/doug-martin/goqu/v9/dialect/postgres" + "github.com/jinzhu/copier" + "github.com/samber/lo" + "go.uber.org/zap" +) + +type _ReportService struct { + log *zap.Logger + ds goqu.DialectWrapper +} + +var ReportService = _ReportService{ + log: logger.Named("Service", "Report"), + ds: goqu.Dialect("postgres"), +} + +// 将指定报表列入计算任务 +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 { + rs.log.Error("未能将指定报表列入计算任务", zap.Error(err)) + return err + } + return nil +} + +// 列出指定用户下的所有尚未发布的报表索引 +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 + }) + parks, err := repository.ParkRepository.RetrieveParks(parkIds) + if err != nil { + rs.log.Error("未能获取到相应报表对应的园区详细信息", zap.Error(err)) + return make([]*vo.ReportIndexQueryResponse, 0), err + } + 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 + ) + 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 (rs _ReportService) RetrieveReportIndexDetail(rid string) (*model.UserDetail, *model.Park, *model.ReportIndex, error) { + index, err := repository.ReportRepository.GetReportIndex(rid) + if err != nil { + rs.log.Error("未能获取到指定报表的索引", zap.Error(err)) + return nil, nil, nil, exceptions.NewNotFoundErrorFromError("未能获取到指定报表的索引", err) + } + 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 +} + +// 根据给定的园区ID列表,查询园区以及用户的详细信息 +func (rs _ReportService) queryParkAndUserDetails(pids []string) ([]*model.Park, []*model.UserDetail, error) { + parks, err := repository.ParkRepository.RetrieveParks(pids) + if err != nil { + rs.log.Error("未能获取到相应报表对应的园区详细信息", zap.Error(err)) + return make([]*model.Park, 0), make([]*model.UserDetail, 0), exceptions.NewNotFoundErrorFromError("未能获取到相应报表对应的园区详细信息", err) + } + userIds := lo.Map(parks, func(elem *model.Park, _ int) string { + return elem.UserId + }) + users, err := repository.UserRepository.RetrieveUsersDetail(userIds) + if err != nil { + rs.log.Error("未能获取到相应报表对应的用户详细信息", zap.Error(err)) + return make([]*model.Park, 0), make([]*model.UserDetail, 0), exceptions.NewNotFoundErrorFromError("未能获取到相应报表对应的用户详细信息", err) + } + 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 { + 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 +} + +// 查询当前待审核的核算报表撤回申请列表 +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 +} diff --git a/tools/utils.go b/tools/utils.go index ad82f23..6312dd5 100644 --- a/tools/utils.go +++ b/tools/utils.go @@ -135,3 +135,12 @@ func EmptyToNil(val string) *string { } return &val } + +// 将一个`decimal.NullDecimal`类型的值转换为字符串指针,并且在转换的过程中设定其展示位数,默认使用银行进位法。 +func NullDecimalToString(d decimal.NullDecimal, precision ...int32) *string { + precision = append(precision, 2) + if !d.Valid { + return nil + } + return lo.ToPtr(d.Decimal.StringFixedBank(precision[0])) +} diff --git a/types/daterange.go b/types/daterange.go index ef46d3f..6b34a8a 100644 --- a/types/daterange.go +++ b/types/daterange.go @@ -112,3 +112,25 @@ func (dr *DateRange) SetUpperUnbounded() { dr.Range.Upper = MaxDate() dr.Range.UpperType = pgtype.Unbounded } + +func (dr DateRange) SafeUpper() Date { + switch dr.Range.UpperType { + case pgtype.Inclusive: + return dr.Range.Upper + case pgtype.Exclusive: + return Date{dr.Range.Upper.AddDate(0, 0, -1)} + default: + return MaxDate() + } +} + +func (dr DateRange) SafeLower() Date { + switch dr.Range.LowerType { + case pgtype.Inclusive: + return dr.Range.Lower + case pgtype.Exclusive: + return Date{dr.Range.Lower.AddDate(0, 0, 1)} + default: + return MinDate() + } +} diff --git a/vo/park.go b/vo/park.go index a557f4b..cbcc8a6 100644 --- a/vo/park.go +++ b/vo/park.go @@ -3,6 +3,8 @@ package vo import ( "electricity_bill_calc/model" "electricity_bill_calc/tools" + + "github.com/shopspring/decimal" ) type ParkInformationForm struct { @@ -65,3 +67,30 @@ type ParkBuildingInformationForm struct { Name string `json:"name"` Floors string `json:"floors"` } + +type SimplifiedParkDetail struct { + Id string `json:"id"` + UserId string `json:"user_id"` + Name string `json:"name"` + TenementStr *string `json:"tenement"` + AreaStr *string `json:"area"` + CapacityStr *string `json:"capacity"` + Category int16 `json:"category"` + MeterType int16 `json:"meter04kvType"` + Region *string `json:"region"` + Address *string `json:"address"` + Contact *string `json:"contact"` + Phone *string `json:"phone"` +} + +func (spd *SimplifiedParkDetail) TenementQuantity(tq decimal.NullDecimal) { + spd.TenementStr = tools.NullDecimalToString(tq) +} + +func (spd *SimplifiedParkDetail) Area(area decimal.NullDecimal) { + spd.AreaStr = tools.NullDecimalToString(area) +} + +func (spd *SimplifiedParkDetail) Capacity(capacity decimal.NullDecimal) { + spd.CapacityStr = tools.NullDecimalToString(capacity) +} diff --git a/vo/report.go b/vo/report.go index d97def7..9bb1c6f 100644 --- a/vo/report.go +++ b/vo/report.go @@ -40,3 +40,33 @@ type ReportModifyForm struct { BasicFee decimal.Decimal `json:"basicFee"` AdjustFee decimal.Decimal `json:"adjustFee"` } + +type SimplifiedReportIndex struct { + Id string `json:"id"` + Park string `json:"parkId"` + PeriodBegin types.Date `json:"periodBegin"` + PeriodEnd types.Date `json:"periodEnd"` + Published bool `json:"published"` + PublishedAt *types.DateTime `json:"publishedAt"` + Withdraw int16 `json:"withdraw"` + LastWithdrawAppliedAt *types.DateTime `json:"lastWithdrawAppliedAt"` + LastWithdrawAuditAt *types.DateTime `json:"lastWithdrawAuditAt"` + Status int16 `json:"status"` + Message *string `json:"message"` +} + +func (sri *SimplifiedReportIndex) Period(p types.DateRange) { + sri.PeriodBegin = p.SafeLower() + sri.PeriodEnd = p.SafeUpper() +} + +type ReportIndexQueryResponse struct { + Park SimplifiedParkDetail `json:"park"` + Report *SimplifiedReportIndex `json:"report"` +} + +type ComprehensiveReportQueryResponse struct { + Report SimplifiedReportIndex `json:"report"` + Park SimplifiedParkDetail `json:"park"` + User SimplifiedUserDetail `json:"user"` +} diff --git a/vo/user.go b/vo/user.go index 14f2724..c7d430a 100644 --- a/vo/user.go +++ b/vo/user.go @@ -2,6 +2,7 @@ package vo import ( "electricity_bill_calc/model" + "electricity_bill_calc/tools" "electricity_bill_calc/types" "time" @@ -125,3 +126,16 @@ type RepasswordForm struct { Username string `json:"uname"` NewPassword string `json:"newPass"` } + +type SimplifiedUserDetail struct { + Id string `json:"id"` + NameStr string `json:"name"` + Contact *string `json:"contact"` + Phone *string `json:"phone"` + Region *string `json:"region"` + Address *string `json:"address"` +} + +func (sud *SimplifiedUserDetail) Name(n *string) { + sud.NameStr = tools.DefaultTo(n, "") +}