合并分支

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

View File

@ -1,4 +1,4 @@
FROM golang:1.19-alpine AS builder
FROM dockerproxy.com/library/golang:1.20-alpine AS builder
ENV GO111MODULE=on
ENV GOPROXY="https://goproxy.io"
@ -9,7 +9,7 @@ RUN go mod download && go mod verify
ADD . /app
RUN CGO_ENABLED=0 GOOS=linux go build -tags=jsoniter -v -o server .
FROM alpine:latest AS production
FROM dockerproxy.com/library/alpine:latest AS production
RUN echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.4/main/" > /etc/apk/repositories
RUN apk add --no-cache tzdata
RUN apk update \

183
_1.gitignore Normal file
View File

@ -0,0 +1,183 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# Block sensitive configuration files
settings.local.yaml
log/
__debug_bin

40
cache/abstract.go vendored
View File

@ -40,7 +40,7 @@ func Cache[T interface{}](key string, value *T, expires time.Duration) error {
}
// 从Redis缓存中获取一个数据
func Retreive[T interface{}](key string) (*T, error) {
func Retrieve[T interface{}](key string) (*T, error) {
getCmd := global.Rd.B().Get().Key(key).Build()
result := global.Rd.Do(global.Ctx, getCmd)
if result.Error() != nil {
@ -81,23 +81,23 @@ func Delete(key string) (bool, error) {
return count > 0, err
}
func dissembleScan(result rueidis.RedisResult) (int64, []string, error) {
func dissembleScan(result rueidis.RedisResult) (uint64, []string, error) {
var (
err error
cursor int64
cursor uint64
keys = make([]string, 0)
)
results, err := result.ToArray()
if err != nil {
return -1, keys, err
return 0, keys, err
}
cursor, err = results[0].AsInt64()
cursor, err = results[0].AsUint64()
if err != nil {
return -1, keys, err
return 0, keys, err
}
keys, err = results[1].AsStrSlice()
if err != nil {
return -1, keys, err
return 0, keys, err
}
return cursor, keys, err
}
@ -106,7 +106,7 @@ func dissembleScan(result rueidis.RedisResult) (int64, []string, error) {
func DeleteAll(pattern string) error {
var (
err error
cursor int64
cursor uint64
keys = make([]string, 0)
sKeys []string
)
@ -137,3 +137,27 @@ func CacheKey(category string, ids ...string) string {
}
return b.String()
}
type ToString[T any] interface {
ToString() string
*T
}
// 用于生成一个内容可以为空的Redis缓存键这个键的类型必须实现了`ToString`接口。
func NullableConditionKey[P any, T ToString[P]](value T, defaultStr ...string) string {
defaultStr = append(defaultStr, "UNDEF")
if value == nil {
return defaultStr[0]
} else {
return value.ToString()
}
}
// 用于生成一个内容可以为空的字符串指针类型的Redis缓存键。
func NullableStringKey(value *string, defaultStr ...string) string {
defaultStr = append(defaultStr, "UNDEF")
if value == nil {
return defaultStr[0]
}
return *value
}

10
cache/count.go vendored
View File

@ -10,7 +10,7 @@ type _CountRecord struct {
Count int64
}
func assembleCountKey(entityName string, additional ...string) string {
func AssembleCountKey(entityName string, additional ...string) string {
var keys = make([]string, 0)
keys = append(keys, strings.ToUpper(entityName))
keys = append(keys, additional...)
@ -24,7 +24,7 @@ func assembleCountKey(entityName string, additional ...string) string {
// 向缓存中缓存模型名称明确的包含指定条件的实体记录数量
func CacheCount(relationNames []string, entityName string, count int64, conditions ...string) error {
countKey := assembleCountKey(entityName, conditions...)
countKey := AssembleCountKey(entityName, conditions...)
cacheInstance := &_CountRecord{Count: count}
err := Cache(countKey, cacheInstance, 5*time.Minute)
for _, relationName := range relationNames {
@ -34,8 +34,8 @@ func CacheCount(relationNames []string, entityName string, count int64, conditio
}
// 从缓存中获取模型名称明确的,包含指定条件的实体记录数量
func RetreiveCount(entityName string, condtions ...string) (int64, error) {
countKey := assembleCountKey(entityName, condtions...)
func RetrieveCount(entityName string, condtions ...string) (int64, error) {
countKey := AssembleCountKey(entityName, condtions...)
exist, err := Exists(countKey)
if err != nil {
return -1, err
@ -43,7 +43,7 @@ func RetreiveCount(entityName string, condtions ...string) (int64, error) {
if !exist {
return -1, nil
}
instance, err := Retreive[_CountRecord](countKey)
instance, err := Retrieve[_CountRecord](countKey)
if instance != nil && err == nil {
return instance.Count, nil
} else {

12
cache/entity.go vendored
View File

@ -6,7 +6,7 @@ import (
"time"
)
func assembleEntityKey(entityName, id string) string {
func AssembleEntityKey(entityName, id string) string {
var keys = make([]string, 0)
keys = append(keys, strings.ToUpper(entityName), id)
var b strings.Builder
@ -19,7 +19,7 @@ func assembleEntityKey(entityName, id string) string {
// 缓存模型名称明确的使用ID进行检索的实体内容。
func CacheEntity[T any](instance T, relationNames []string, entityName, id string) error {
entityKey := assembleEntityKey(entityName, id)
entityKey := AssembleEntityKey(entityName, id)
err := Cache(entityKey, &instance, 5*time.Minute)
for _, relationName := range relationNames {
CacheRelation(relationName, STORE_TYPE_KEY, entityKey)
@ -28,15 +28,15 @@ func CacheEntity[T any](instance T, relationNames []string, entityName, id strin
}
// 从缓存中取出模型名称明确的使用ID进行检索的实体内容。
func RetreiveEntity[T any](entityName, id string) (*T, error) {
entityKey := assembleEntityKey(entityName, id)
instance, err := Retreive[T](entityKey)
func RetrieveEntity[T any](entityName, id string) (*T, error) {
entityKey := AssembleEntityKey(entityName, id)
instance, err := Retrieve[T](entityKey)
return instance, err
}
// 精确的从缓存中删除指定模型名称、指定ID的实体内容。
func AbolishSpecificEntity(entityName, id string) (bool, error) {
entityKey := assembleEntityKey(entityName, id)
entityKey := AssembleEntityKey(entityName, id)
return Delete(entityKey)
}

6
cache/exists.go vendored
View File

@ -8,7 +8,7 @@ import (
"github.com/samber/lo"
)
func assembleExistsKey(entityName string, additional ...string) string {
func AssembleExistsKey(entityName string, additional ...string) string {
var keys = make([]string, 0)
keys = append(keys, strings.ToUpper(entityName))
keys = append(keys, additional...)
@ -22,7 +22,7 @@ func assembleExistsKey(entityName string, additional ...string) string {
// 缓存模型名称明确的、包含指定ID以及一些附加条件的记录
func CacheExists(relationNames []string, entityName string, conditions ...string) error {
existskey := assembleExistsKey(entityName, conditions...)
existskey := AssembleExistsKey(entityName, conditions...)
err := Cache(existskey, lo.ToPtr(true), 5*time.Minute)
for _, relationName := range relationNames {
CacheRelation(relationName, STORE_TYPE_KEY, existskey)
@ -32,7 +32,7 @@ func CacheExists(relationNames []string, entityName string, conditions ...string
// 从缓存中获取模型名称明确、包含指定ID以及一些附加条件的实体是否存在的标记函数在返回false时不保证数据库中相关记录也不存在
func CheckExists(entityName string, condtions ...string) (bool, error) {
existsKey := assembleExistsKey(entityName, condtions...)
existsKey := AssembleExistsKey(entityName, condtions...)
return Exists(existsKey)
}

12
cache/relation.go vendored
View File

@ -15,13 +15,13 @@ const (
STORE_TYPE_HASH = "HASH"
)
func assembleRelationKey(relationName string) string {
func AssembleRelationKey(relationName string) string {
var keys = make([]string, 0)
keys = append(keys, strings.ToUpper(relationName))
return CacheKey(TAG_RELATION, keys...)
}
func assembleRelationIdentity(storeType, key string, field ...string) string {
func AssembleRelationIdentity(storeType, key string, field ...string) string {
var identity = make([]string, 0)
identity = append(identity, storeType, key)
identity = append(identity, field...)
@ -30,8 +30,8 @@ func assembleRelationIdentity(storeType, key string, field ...string) string {
// 向缓存中保存与指定关联名称相关联的键的名称以及键的类型和子字段的组成。
func CacheRelation(relationName, storeType, key string, field ...string) error {
relationKey := assembleRelationKey(relationName)
relationIdentity := assembleRelationIdentity(storeType, key, field...)
relationKey := AssembleRelationKey(relationName)
relationIdentity := AssembleRelationIdentity(storeType, key, field...)
cmd := global.Rd.B().Sadd().Key(relationKey).Member(relationIdentity).Build()
result := global.Rd.Do(global.Ctx, cmd)
return result.Error()
@ -39,7 +39,7 @@ func CacheRelation(relationName, storeType, key string, field ...string) error {
// 从缓存中清理指定的关联键
func AbolishRelation(relationName string) error {
relationKey := assembleRelationKey(relationName)
relationKey := AssembleRelationKey(relationName)
cmd := global.Rd.B().Smembers().Key(relationKey).Build()
relationItems, err := global.Rd.Do(global.Ctx, cmd).AsStrSlice()
if err != nil {
@ -74,7 +74,7 @@ func AbolishRelation(relationName string) error {
func ClearOrphanRelationItems() error {
var (
err error
cursor int64
cursor uint64
keys = make([]string, 0)
sKeys []string
)

62
cache/search.go vendored
View File

@ -1,12 +1,17 @@
package cache
import (
"electricity_bill_calc/logger"
"fmt"
"strings"
"time"
"go.uber.org/zap"
)
func assembleSearchKey(entityName string, additional ...string) string {
var log = logger.Named("Cache")
func AssembleSearchKey(entityName string, additional ...string) string {
var keys = make([]string, 0)
keys = append(keys, strings.ToUpper(entityName))
keys = append(keys, additional...)
@ -20,7 +25,7 @@ func assembleSearchKey(entityName string, additional ...string) string {
// 缓存模型名称明确的使用或者包含非ID检索条件的实体内容。
func CacheSearch[T any](instance T, relationNames []string, entityName string, conditions ...string) error {
searchKey := assembleSearchKey(entityName, conditions...)
searchKey := AssembleSearchKey(entityName, conditions...)
err := Cache(searchKey, &instance, 5*time.Minute)
for _, relationName := range relationNames {
CacheRelation(relationName, STORE_TYPE_KEY, searchKey)
@ -29,9 +34,9 @@ func CacheSearch[T any](instance T, relationNames []string, entityName string, c
}
// 从缓存中取得模型名称明确的使用或者包含非ID检索条件的实体内容。
func RetreiveSearch[T any](entityName string, conditions ...string) (*T, error) {
searchKey := assembleSearchKey(entityName, conditions...)
instance, err := Retreive[T](searchKey)
func RetrieveSearch[T any](entityName string, conditions ...string) (*T, error) {
searchKey := AssembleSearchKey(entityName, conditions...)
instance, err := Retrieve[T](searchKey)
return instance, err
}
@ -40,3 +45,50 @@ func AbolishSearch(entityName string) error {
pattern := fmt.Sprintf("%s:%s:*", TAG_SEARCH, strings.ToUpper(entityName))
return DeleteAll(pattern)
}
// 向缓存中保存指定模型名称的分页检索结果,会同时采用`CacheCount`中的方法保存检索结果的总数量。
func CachePagedSearch[T any](instance T, total int64, relationNames []string, entityName string, conditions ...string) error {
searchKey := AssembleSearchKey(entityName, conditions...)
countKey := AssembleCountKey(entityName, conditions...)
err := Cache(searchKey, &instance, 5*time.Minute)
if err != nil {
return err
}
cacheInstance := &_CountRecord{Count: total}
err = Cache(countKey, cacheInstance, 5*time.Minute)
for _, relationName := range relationNames {
CacheRelation(relationName, STORE_TYPE_KEY, searchKey)
CacheRelation(relationName, STORE_TYPE_KEY, countKey)
}
return err
}
// 从缓存中获取指定模型名称的分页检索结果,会同时采用`RetrieveCount`中的方法获取检索结果的总数量。
func RetrievePagedSearch[T any](entityName string, conditions ...string) (*T, int64, error) {
searchKey := AssembleSearchKey(entityName, conditions...)
countKey := AssembleCountKey(entityName, conditions...)
instance, err := Retrieve[T](searchKey)
if err != nil {
return nil, -1, err
}
count, err := Retrieve[_CountRecord](countKey)
if err != nil {
return nil, -1, err
}
if instance == nil || count == nil {
log.Warn("检索结果或者检索总数为空。", zap.String("searchKey", searchKey), zap.String("countKey", countKey))
return nil, -1, nil
}
return instance, count.Count, nil
}
// 从缓存中删除指定模型名称的分页检索结果,会同时采用`AbolishCountEntity`中的方法删除检索结果的总数量。
func AbolishPagedSearch(entityName string) error {
pattern := fmt.Sprintf("%s:%s:*", TAG_SEARCH, strings.ToUpper(entityName))
err := DeleteAll(pattern)
if err != nil {
return err
}
pattern = fmt.Sprintf("%s:%s:*", TAG_COUNT, strings.ToUpper(entityName))
return DeleteAll(pattern)
}

4
cache/session.go vendored
View File

@ -21,9 +21,9 @@ func CacheSession(session *model.Session) error {
return Cache(key, session, config.ServiceSettings.MaxSessionLife)
}
func RetreiveSession(token string) (*model.Session, error) {
func RetrieveSession(token string) (*model.Session, error) {
key := SessionKey(token)
return Retreive[model.Session](key)
return Retrieve[model.Session](key)
}
func HasSession(token string) (bool, error) {

View File

@ -31,8 +31,9 @@ type RedisSetting struct {
type ServiceSetting struct {
MaxSessionLife time.Duration
ItemsPageSize int
ItemsPageSize uint
CacheLifeTime time.Duration
HostSerial int64
}
// 定义全局变量

View File

@ -3,8 +3,12 @@ package controller
import (
"electricity_bill_calc/exceptions"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"electricity_bill_calc/response"
"net/http"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
func _retreiveSession(c *fiber.Ctx) (*model.Session, error) {
@ -18,3 +22,26 @@ func _retreiveSession(c *fiber.Ctx) (*model.Session, error) {
}
return userSession, nil
}
// 检查当前用户是否拥有指定园区,在判断完成之后直接产生响应
func checkParkBelongs(parkId string, logger *zap.Logger, c *fiber.Ctx, result *response.Result) (bool, error) {
session := c.Locals("session")
if session == nil {
logger.Error("用户会话不存在。")
return false, result.Unauthorized("用户会话不存在。")
}
userSession, ok := session.(*model.Session)
if !ok {
return false, result.Unauthorized("用户会话格式不正确,需要重新登录")
}
ok, err := repository.ParkRepository.IsParkBelongs(parkId, userSession.Uid)
switch {
case err != nil:
logger.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", userSession.Uid), zap.Error(err))
return false, result.Error(http.StatusInternalServerError, err.Error())
case err == nil && !ok:
logger.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", userSession.Uid))
return false, result.Forbidden("您无权访问该园区。")
}
return true, nil
}

View File

@ -1,93 +1,97 @@
package controller
import (
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"electricity_bill_calc/response"
"electricity_bill_calc/security"
"electricity_bill_calc/service"
"electricity_bill_calc/types"
"net/http"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
"github.com/shopspring/decimal"
"go.uber.org/zap"
)
func InitializeChargesController(app *fiber.App) {
app.Get("/charges", security.OPSAuthorize, listAllCharges)
app.Post("/charge", security.OPSAuthorize, recordNewCharge)
app.Put("/charge/:uid/:seq", security.OPSAuthorize, modifyChargeState)
var chargeLog = logger.Named("Handler", "Charge")
func InitializeChargeHandlers(router *fiber.App) {
router.Get("/charge", searchCharges)
router.Post("/charge", createNewUserChargeRecord)
router.Put("/charge/:uid/:seq", modifyUserChargeState)
}
func listAllCharges(c *fiber.Ctx) error {
// 检索用户的充值记录列表
func searchCharges(c *fiber.Ctx) error {
chargeLog.Info("检索用户的充值记录列表。")
result := response.NewResult(c)
requestPage, err := strconv.Atoi(c.Query("page", "1"))
keyword := c.Query("keyword", "")
page := c.QueryInt("page", 1)
beginTime := types.ParseDateWithDefault(c.Query("begin"), types.NewEmptyDate())
endTime := types.ParseDateWithDefault(c.Query("end"), types.MaxDate())
charges, total, err := repository.ChargeRepository.FindCharges(uint(page), &beginTime, &endTime, &keyword)
if err != nil {
return result.NotAccept("查询参数[page]格式不正确。")
chargeLog.Error("检索用户的充值记录列表失败。", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
requestKeyword := c.Query("keyword", "")
requestBeginDate := c.Query("begin", "")
requestEndDate := c.Query("end", "")
charges, total, err := service.ChargeService.ListPagedChargeRecord(requestKeyword, requestBeginDate, requestEndDate, requestPage)
if err != nil {
return result.NotFound(err.Error())
}
return result.Json(
http.StatusOK, "已获取到符合条件的计费记录。",
response.NewPagedResponse(requestPage, total).ToMap(),
return result.Success(
"已经获取到符合条件的计费记录。",
response.NewPagedResponse(page, total).ToMap(),
fiber.Map{"records": charges},
)
}
type _NewChargeFormData struct {
UserId string `json:"userId" form:"userId"`
Fee decimal.NullDecimal `json:"fee" form:"fee"`
Discount decimal.NullDecimal `json:"discount" form:"discount"`
Amount decimal.NullDecimal `json:"amount" form:"amount"`
ChargeTo model.Date `json:"chargeTo" form:"chargeTo"`
}
func recordNewCharge(c *fiber.Ctx) error {
// 创建一条新的用户充值记录
func createNewUserChargeRecord(c *fiber.Ctx) error {
chargeLog.Info("创建一条新的用户充值记录。")
result := response.NewResult(c)
formData := new(_NewChargeFormData)
if err := c.BodyParser(formData); err != nil {
return result.UnableToParse("无法解析提交的数据。")
createionForm := new(model.ChargeRecordCreationForm)
if err := c.BodyParser(createionForm); err != nil {
chargeLog.Error("无法解析创建充值记录的请求数据。", zap.Error(err))
return result.Error(http.StatusBadRequest, err.Error())
}
currentTime := time.Now()
newRecord := &model.UserCharge{
UserId: formData.UserId,
Fee: formData.Fee,
Discount: formData.Discount,
Amount: formData.Amount,
Settled: true,
SettledAt: &currentTime,
ChargeTo: formData.ChargeTo,
}
err := service.ChargeService.CreateChargeRecord(newRecord, true)
fee, _ := createionForm.Fee.Decimal.Float64()
discount, _ := createionForm.Discount.Decimal.Float64()
amount, _ := createionForm.Amount.Decimal.Float64()
ok, err := service.ChargeService.RecordUserCharge(
createionForm.UserId,
&fee,
&discount,
&amount,
createionForm.ChargeTo,
true,
)
if err != nil {
chargeLog.Error("创建用户充值记录失败。", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Created("指定用户的服务已延期。")
if !ok {
chargeLog.Error("创建用户充值记录失败。")
return result.NotAccept("创建用户充值记录失败。")
} else {
return result.Success("创建用户充值记录成功, 指定用户的服务已延期。")
}
}
type _StateChangeFormData struct {
Cancelled bool `json:"cancelled"`
}
func modifyChargeState(c *fiber.Ctx) error {
// 改变用户充值记录的状态
func modifyUserChargeState(c *fiber.Ctx) error {
chargeLog.Info("改变用户充值记录的状态。")
result := response.NewResult(c)
formData := new(_StateChangeFormData)
if err := c.BodyParser(formData); err != nil {
return result.UnableToParse("无法解析提交的数据。")
}
requestUserID := c.Params("uid")
requestChargeSeq, err := strconv.Atoi(c.Params("seq", "-1"))
if err != nil || requestChargeSeq == -1 {
return result.Error(http.StatusNotAcceptable, "参数[记录流水号]解析错误。")
}
err = service.ChargeService.CancelCharge(int64(requestChargeSeq), requestUserID)
uid := c.Params("uid")
seq, err := c.ParamsInt("seq")
if err != nil {
chargeLog.Error("无法解析请求参数。", zap.Error(err))
return result.Error(http.StatusBadRequest, err.Error())
}
ok, err := service.ChargeService.CancelUserCharge(uid, int64(seq))
if err != nil {
chargeLog.Error("取消用户充值记录失败。", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Updated("指定用户服务延期记录状态已经更新。")
if !ok {
chargeLog.Error("取消用户充值记录失败。")
return result.NotAccept("取消用户充值记录失败。")
} else {
return result.Success("取消用户充值记录成功。")
}
}

227
controller/invoice.go Normal file
View File

@ -0,0 +1,227 @@
package controller
import (
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"electricity_bill_calc/response"
"electricity_bill_calc/security"
"electricity_bill_calc/service"
"electricity_bill_calc/tools"
"electricity_bill_calc/types"
"electricity_bill_calc/vo"
"github.com/gofiber/fiber/v2"
"github.com/jinzhu/copier"
"go.uber.org/zap"
)
var invoiceLog = logger.Named("Controller", "Invoice")
func InitializeInvoiceHandler(router *fiber.App) {
router.Get("/invoice", security.MustAuthenticated, listInvoices)
router.Post("/invoice", security.EnterpriseAuthorize, createNewInvoiceRecord)
router.Post("/invoice/precalculate", security.EnterpriseAuthorize, testCalculateInvoice)
router.Get("/invoice/:code", security.EnterpriseAuthorize, getInvoiceDetail)
router.Delete("/invoice/:code", security.EnterpriseAuthorize, deleteInvoiceRecord)
router.Get("/uninvoiced/tenemennt/:tid/report", security.EnterpriseAuthorize, getUninvoicedTenementReports)
}
// 列出指定园区中的符合条件的发票记录
func listInvoices(c *fiber.Ctx) error {
invoiceLog.Info("列出指定园区中的符合条件的发票记录")
result := response.NewResult(c)
session, err := _retreiveSession(c)
if err != nil {
invoiceLog.Error("列出指定园区中的符合条件的发票记录失败,不能获取到有效的用户会话。", zap.Error(err))
return result.Unauthorized("未能获取到有效的用户会话。")
}
park := tools.EmptyToNil(c.Query("park"))
if session.Type == model.USER_TYPE_ENT && park != nil && len(*park) > 0 {
pass, err := checkParkBelongs(*park, invoiceLog, c, &result)
if err != nil || !pass {
return err
}
}
startDate, err := types.ParseDatep(c.Query("start_date"))
if err != nil {
invoiceLog.Error("列出指定园区中的符合条件的发票记录失败,开始日期参数解析错误。", zap.Error(err))
return result.BadRequest("开始日期参数解析错误。")
}
endDate, err := types.ParseDatep(c.Query("end_date"))
if err != nil {
invoiceLog.Error("列出指定园区中的符合条件的发票记录失败,结束日期参数解析错误。", zap.Error(err))
return result.BadRequest("结束日期参数解析错误。")
}
keyword := tools.EmptyToNil(c.Query("keyword"))
page := c.QueryInt("page", 1)
invoices, total, err := repository.InvoiceRepository.ListInvoice(park, startDate, endDate, keyword, uint(page))
if err != nil {
invoiceLog.Error("列出指定园区中的符合条件的发票记录失败,检索符合条件的发票记录出现错误。", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "检索符合条件的发票记录出现错误。")
}
invoiceResponse := make([]*vo.InvoiceResponse, 0)
copier.Copy(&invoiceResponse, &invoices)
return result.Success(
"已经获取到符合条件的发票列表。",
response.NewPagedResponse(page, total).ToMap(),
fiber.Map{
"invoices": invoiceResponse,
},
)
}
// 获取指定发票的详细信息
func getInvoiceDetail(c *fiber.Ctx) error {
result := response.NewResult(c)
invoiceNo := tools.EmptyToNil(c.Params("code"))
invoiceLog.Info("获取指定发票的详细信息", zap.Stringp("InvoiceNo", invoiceNo))
if invoiceNo == nil {
invoiceLog.Error("获取指定发票的详细信息失败,未指定发票编号。")
return result.BadRequest("未指定发票编号。")
}
session, err := _retreiveSession(c)
if err != nil {
invoiceLog.Error("获取指定发票的详细信息失败,不能获取到有效的用户会话。", zap.Error(err))
return result.Unauthorized("未能获取到有效的用户会话。")
}
pass, err := repository.InvoiceRepository.IsBelongsTo(*invoiceNo, session.Uid)
if err != nil {
invoiceLog.Error("获取指定发票的详细信息失败,检查发票所属权时出现错误。", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "检查发票所属权时出现错误。")
}
if !pass {
invoiceLog.Error("获取指定发票的详细信息失败,发票不属于当前用户。")
return result.Forbidden("不能访问不属于自己的发票。")
}
invoice, err := repository.InvoiceRepository.GetInvoiceDetail(*invoiceNo)
if err != nil {
invoiceLog.Error("获取指定发票的详细信息失败,检索发票信息时出现错误。", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "检索发票信息时出现错误。")
}
if invoice == nil {
invoiceLog.Error("获取指定发票的详细信息失败,指定发票不存在。")
return result.NotFound("指定发票不存在。")
}
var invoiceResponse vo.ExtendedInvoiceResponse
copier.Copy(&invoiceResponse, &invoice)
return result.Success(
"已经获取到指定发票的详细信息。",
fiber.Map{
"invoice": invoiceResponse,
},
)
}
// 获取指定商户下所有尚未开票的核算项目
func getUninvoicedTenementReports(c *fiber.Ctx) error {
result := response.NewResult(c)
tenement := tools.EmptyToNil(c.Params("tid"))
invoiceLog.Info("获取指定商户下所有尚未开票的核算项目", zap.Stringp("Tenement", tenement))
if tenement == nil {
invoiceLog.Error("获取指定商户下所有尚未开票的核算项目失败,未指定商户。")
return result.BadRequest("未指定商户。")
}
session, err := _retreiveSession(c)
if err != nil {
invoiceLog.Error("获取指定商户下所有尚未开票的核算项目失败,不能获取到有效的用户会话。", zap.Error(err))
return result.Unauthorized("未能获取到有效的用户会话。")
}
pass, err := repository.TenementRepository.IsTenementBelongs(*tenement, session.Uid)
if err != nil {
invoiceLog.Error("获取指定商户下所有尚未开票的核算项目失败,检查商户所属权时出现错误。", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "检查商户所属权时出现错误。")
}
if !pass {
invoiceLog.Error("获取指定商户下所有尚未开票的核算项目失败,商户不属于当前用户。")
return result.Forbidden("不能访问不属于自己的商户。")
}
reports, err := repository.InvoiceRepository.ListUninvoicedTenementCharges(*tenement)
if err != nil {
invoiceLog.Error("获取指定商户下所有尚未开票的核算项目失败,检索核算项目时出现错误。", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "检索核算项目时出现错误。")
}
return result.Success(
"已经获取到指定商户下所有尚未开票的核算项目。",
fiber.Map{
"records": reports,
},
)
}
// 试计算指定发票的票面信息
func testCalculateInvoice(c *fiber.Ctx) error {
result := response.NewResult(c)
var form vo.InvoiceCreationForm
if err := c.BodyParser(&form); err != nil {
invoiceLog.Error("试计算指定发票的票面信息失败,请求表单数据解析错误。", zap.Error(err))
return result.BadRequest("请求表单数据解析错误。")
}
invoiceLog.Info("试计算指定发票的票面信息", zap.String("Park", form.Park), zap.String("Tenement", form.Tenement))
if pass, err := checkParkBelongs(form.Park, invoiceLog, c, &result); err != nil || !pass {
return err
}
total, cargos, err := service.InvoiceService.TestCalculateInvoice(form.Park, form.Tenement, form.TaxMethod, form.TaxRate, form.Covers)
if err != nil {
invoiceLog.Error("试计算指定发票的票面信息失败,试计算发票时出现错误。", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "试计算发票时出现错误。")
}
return result.Success(
"已经计算出指定发票的票面信息。",
fiber.Map{
"total": total,
"cargos": cargos,
},
)
}
// 创建一个新的发票记录
func createNewInvoiceRecord(c *fiber.Ctx) error {
result := response.NewResult(c)
var form vo.ExtendedInvoiceCreationForm
if err := c.BodyParser(&form); err != nil {
invoiceLog.Error("创建一个新的发票记录失败,请求表单数据解析错误。", zap.Error(err))
return result.BadRequest("请求表单数据解析错误。")
}
invoiceLog.Info("创建一个新的发票记录", zap.String("Park", form.Park), zap.String("Tenement", form.Tenement))
if pass, err := checkParkBelongs(form.Park, invoiceLog, c, &result); err != nil || !pass {
return err
}
err := service.InvoiceService.SaveInvoice(form.Park, form.Tenement, form.InvoiceNo, form.InvoiceType, form.TaxMethod, form.TaxRate, form.Covers)
if err != nil {
invoiceLog.Error("创建一个新的发票记录失败,保存发票时出现错误。", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "保存发票时出现错误。")
}
return result.Created("已经创建了一个新的发票记录。")
}
// 删除指定的发票记录
func deleteInvoiceRecord(c *fiber.Ctx) error {
result := response.NewResult(c)
invoiceNo := tools.EmptyToNil(c.Params("code"))
invoiceLog.Info("删除指定的发票记录", zap.Stringp("InvoiceNo", invoiceNo))
if invoiceNo == nil {
invoiceLog.Error("删除指定的发票记录失败,未指定发票编号。")
return result.BadRequest("未指定发票编号。")
}
session, err := _retreiveSession(c)
if err != nil {
invoiceLog.Error("删除指定的发票记录失败,不能获取到有效的用户会话。", zap.Error(err))
return result.Unauthorized("未能获取到有效的用户会话。")
}
pass, err := repository.InvoiceRepository.IsBelongsTo(*invoiceNo, session.Uid)
if err != nil {
invoiceLog.Error("删除指定的发票记录失败,检查发票所属权时出现错误。", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "检查发票所属权时出现错误。")
}
if !pass {
invoiceLog.Error("删除指定的发票记录失败,发票不属于当前用户。")
return result.Forbidden("不能删除不属于自己的发票。")
}
err = service.InvoiceService.DeleteInvoice(*invoiceNo)
if err != nil {
invoiceLog.Error("删除指定的发票记录失败,删除发票时出现错误。", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "删除发票时出现错误。")
}
return result.Success("已经删除了指定的发票记录。")
}

502
controller/meter.go Normal file
View File

@ -0,0 +1,502 @@
package controller
import (
"electricity_bill_calc/excel"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"electricity_bill_calc/response"
"electricity_bill_calc/security"
"electricity_bill_calc/service"
"electricity_bill_calc/tools"
"electricity_bill_calc/types"
"electricity_bill_calc/vo"
"fmt"
"net/http"
"github.com/gofiber/fiber/v2"
"github.com/jinzhu/copier"
"github.com/samber/lo"
"go.uber.org/zap"
)
var meterLog = logger.Named("Handler", "Meter")
func InitializeMeterHandlers(router *fiber.App) {
router.Get("/meter/choice", security.EnterpriseAuthorize, listUnboundMeters)
router.Get("/meter/choice/tenement", security.EnterpriseAuthorize, listUnboundTenementMeters)
router.Get("/meter/:pid", security.EnterpriseAuthorize, searchMetersWithinPark)
router.Post("/meter/:pid", security.EnterpriseAuthorize, createNewMeterManually)
router.Get("/meter/:pid/template", security.EnterpriseAuthorize, downloadMeterArchiveTemplate)
router.Post("/meter/:pid/batch", security.EnterpriseAuthorize, uploadMeterArchive)
router.Get("/meter/:pid/pooled", security.EnterpriseAuthorize, listPooledMeters)
router.Get("/meter/:pid/:code", security.EnterpriseAuthorize, retrieveSpecificMeterDetail)
router.Put("/meter/:pid/:code", security.EnterpriseAuthorize, updateMeterManually)
router.Patch("/meter/:pid/:code", security.EnterpriseAuthorize, replaceMeter)
router.Get("/meter/:pid/:code/binding", security.EnterpriseAuthorize, listAssociatedMeters)
router.Post("/meter/:pid/:code/binding", security.EnterpriseAuthorize, bindAssociatedMeters)
router.Delete("/meter/:pid/:code/binding/:slave", security.EnterpriseAuthorize, unbindAssociatedMeters)
router.Get("/reading/:pid", security.EnterpriseAuthorize, queryMeterReadings)
router.Put("/reading/:pid/:code/:reading", security.EnterpriseAuthorize, updateMeterReading)
router.Get("/reading/:pid/template", security.EnterpriseAuthorize, downloadMeterReadingsTemplate)
router.Post("/reading/:pid/batch", security.EnterpriseAuthorize, uploadMeterReadings)
router.Post("/reading/:pid/:code", security.EnterpriseAuthorize, recordMeterReading)
}
// 查询指定园区下的表计信息
func searchMetersWithinPark(c *fiber.Ctx) error {
parkId := c.Params("pid")
meterLog.Info("查询指定园区下的表计信息", zap.String("park id", parkId))
result := response.NewResult(c)
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
keyword := c.Query("keyword")
page := c.QueryInt("page", 1)
meters, total, err := repository.MeterRepository.MetersIn(parkId, uint(page), &keyword)
if err != nil {
meterLog.Error("无法查询指定园区下的表计信息,无法获取表计列表", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Success(
"已经取得符合条件的0.4kV表计列表。",
response.NewPagedResponse(page, total).ToMap(),
fiber.Map{"meters": meters},
)
}
// 查询指定园区中指定表计的详细信息
func retrieveSpecificMeterDetail(c *fiber.Ctx) error {
parkId := c.Params("pid")
meterId := c.Params("code")
meterLog.Info("查询指定园区中指定表计的详细信息", zap.String("park id", parkId), zap.String("meter id", meterId))
result := response.NewResult(c)
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
meter, err := repository.MeterRepository.FetchMeterDetail(parkId, meterId)
if err != nil {
meterLog.Error("无法查询指定园区中指定表计的详细信息,无法获取表计信息", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
if meter == nil {
meterLog.Warn("无法查询指定园区中指定表计的详细信息,表计不存在")
return result.NotFound("指定的表计不存在。")
}
return result.Success("指定表计信息已经找到。", fiber.Map{"meter": meter})
}
// 手动添加一条0.4kV表计记录
func createNewMeterManually(c *fiber.Ctx) error {
parkId := c.Params("pid")
meterLog.Info("手动添加一条0.4kV表计记录", zap.String("park id", parkId))
result := response.NewResult(c)
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
var creationForm vo.MeterCreationForm
if err := c.BodyParser(&creationForm); err != nil {
meterLog.Error("无法手动添加一条0.4kV表计记录,无法解析表计创建表单", zap.Error(err))
return result.NotAccept(err.Error())
}
if err := service.MeterService.CreateMeterRecord(parkId, &creationForm); err != nil {
meterLog.Error("无法手动添加一条0.4kV表计记录,无法创建表计记录", zap.Error(err))
return result.NotAccept(err.Error())
}
return result.Created("新0.4kV表计已经添加完成。")
}
// 手动更新一条新的0.4kV表计记录
func updateMeterManually(c *fiber.Ctx) error {
parkId := c.Params("pid")
meterId := c.Params("code")
meterLog.Info("手动更新一条新的0.4kV表计记录", zap.String("park id", parkId), zap.String("meter id", meterId))
result := response.NewResult(c)
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
var updateForm vo.MeterModificationForm
if err := c.BodyParser(&updateForm); err != nil {
meterLog.Error("无法手动更新一条新的0.4kV表计记录,无法解析表计更新表单", zap.Error(err))
return result.NotAccept(err.Error())
}
if err := service.MeterService.UpdateMeterRecord(parkId, meterId, &updateForm); err != nil {
meterLog.Error("无法手动更新一条新的0.4kV表计记录,无法更新表计记录", zap.Error(err))
return result.NotAccept(err.Error())
}
return result.Updated("0.4kV表计已经更新完成。")
}
// 下载指定的园区表计登记模板
func downloadMeterArchiveTemplate(c *fiber.Ctx) error {
parkId := c.Params("pid")
meterLog.Info("下载指定的园区表计登记模板", zap.String("park id", parkId))
result := response.NewResult(c)
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
parkDetail, err := repository.ParkRepository.RetrieveParkDetail(parkId)
if err != nil {
meterLog.Error("无法下载指定的园区表计登记模板,无法获取园区信息", zap.Error(err))
return result.NotFound(err.Error())
}
buildings, err := repository.ParkRepository.RetrieveParkBuildings(parkId)
if err != nil {
meterLog.Error("无法下载指定的园区表计登记模板,无法获取园区建筑列表", zap.Error(err))
return result.NotFound(fmt.Sprintf("无法获取园区建筑列表,%s", err.Error()))
}
if err != nil {
meterLog.Error("无法下载指定的园区表计登记模板,无法生成表计登记模板", zap.Error(err))
return result.NotFound(fmt.Sprintf("无法生成表计登记模板,%s", err.Error()))
}
templateGenerator := excel.NewMeterArchiveExcelTemplateGenerator()
defer templateGenerator.Close()
err = templateGenerator.WriteTemplateData(buildings)
if err != nil {
meterLog.Error("无法下载指定的园区表计登记模板,无法生成表计登记模板", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, fmt.Sprintf("无法生成表计登记模板,%s", err.Error()))
}
c.Status(fiber.StatusOK)
c.Set(fiber.HeaderContentType, fiber.MIMEOctetStream)
c.Set("Content-Transfer-Encoding", "binary")
c.Set(fiber.HeaderContentDisposition, fmt.Sprintf("attachment; filename=%s-表计登记模板.xlsx", parkDetail.Name))
templateGenerator.WriteTo(c.Response().BodyWriter())
return nil
}
// 从Excel文件中导入表计档案
func uploadMeterArchive(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
uploadFile, err := c.FormFile("data")
if err != nil {
meterLog.Error("无法从Excel文件中导入表计档案无法获取上传的文件", zap.Error(err))
return result.NotAccept(fmt.Sprintf("没有接收到上传的文件,%s", err.Error()))
}
errs, err := service.MeterService.BatchImportMeters(parkId, uploadFile)
if err != nil {
meterLog.Error("无法从Excel文件中导入表计档案无法导入表计档案", zap.Error(err))
return result.Json(fiber.StatusNotAcceptable, "上传的表计档案存在错误。", fiber.Map{"errors": errs})
}
return result.Success("表计档案已经导入完成。", fiber.Map{"errors": errs})
}
// 更换系统中的表计
func replaceMeter(c *fiber.Ctx) error {
parkId := c.Params("pid")
meterId := c.Params("code")
meterLog.Info("更换系统中的表计", zap.String("park id", parkId), zap.String("meter id", meterId))
result := response.NewResult(c)
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
var replacementForm vo.MeterReplacingForm
if err := c.BodyParser(&replacementForm); err != nil {
meterLog.Error("无法更换系统中的表计,无法解析表计更换表单", zap.Error(err))
return result.NotAccept(err.Error())
}
return nil
}
// 列出指定公摊表计下的所有关联表计
func listAssociatedMeters(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
meterId := c.Params("code")
meterLog.Info("列出指定公摊表计下的所有关联表计", zap.String("park id", parkId), zap.String("meter id", meterId))
meters, err := service.MeterService.ListPooledMeterRelations(parkId, meterId)
if err != nil {
meterLog.Error("无法列出指定公摊表计下的所有关联表计,无法获取关联表计列表", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Success("已经取得指定公摊表计下的所有关联表计列表。", fiber.Map{"meters": meters})
}
// 向指定表计绑定关联表计
func bindAssociatedMeters(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
meterId := c.Params("code")
meterLog.Info("向指定表计绑定关联表计", zap.String("park id", parkId), zap.String("meter id", meterId))
var meters = make([]string, 0)
if err := c.BodyParser(&meters); err != nil {
meterLog.Error("无法向指定表计绑定关联表计,无法解析关联表计列表", zap.Error(err))
return result.NotAccept(err.Error())
}
ok, err := service.MeterService.BindMeter(parkId, meterId, meters)
if err != nil {
meterLog.Error("无法向指定表计绑定关联表计,无法绑定关联表计", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
if !ok {
meterLog.Warn("无法向指定表计绑定关联表计,表计关联失败。")
return result.NotAccept("表计关联失败。")
}
return result.Created("已经向指定表计绑定关联表计。")
}
// 解除指定园区下两个表计之间的关联关系
func unbindAssociatedMeters(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
masterMeter := c.Params("code")
slaveMeter := c.Params("slave")
if len(masterMeter) == 0 || len(slaveMeter) == 0 {
meterLog.Warn("无法解除指定园区下两个表计之间的关联关系,表计编号为空。")
return result.NotAccept("存在未给定要操作的表计编号。")
}
ok, err := service.MeterService.UnbindMeter(parkId, masterMeter, []string{slaveMeter})
if err != nil {
meterLog.Error("无法解除指定园区下两个表计之间的关联关系,无法解除关联关系", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
if !ok {
meterLog.Warn("无法解除指定园区下两个表计之间的关联关系,表计关联解除失败。")
return result.NotAccept("表计关联解除失败。")
}
return result.Created("已经解除指定园区下两个表计之间的关联关系。")
}
// 分页列出园区中的公摊表计
func listPooledMeters(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
page := c.QueryInt("page", 1)
keyword := c.Query("keyword")
meters, total, err := service.MeterService.SearchPooledMetersDetail(parkId, uint(page), &keyword)
if err != nil {
meterLog.Error("无法分页列出园区中的公摊表计,无法获取公摊表计列表", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Success(
"已经取得符合条件的公摊表计列表。",
response.NewPagedResponse(page, total).ToMap(),
fiber.Map{"meters": meters},
)
}
// 列出指定园区中尚未绑定公摊表计的表计
func listUnboundMeters(c *fiber.Ctx) error {
result := response.NewResult(c)
session, err := _retreiveSession(c)
if err != nil {
meterLog.Error("无法列出指定园区中尚未绑定公摊表计的表计,无法获取当前用户会话", zap.Error(err))
return result.Unauthorized(err.Error())
}
parkId := tools.EmptyToNil(c.Query("park"))
if pass, err := checkParkBelongs(*parkId, meterLog, c, &result); !pass {
return err
}
keyword := c.Query("keyword")
limit := uint(c.QueryInt("limit", 6))
meters, err := repository.MeterRepository.ListUnboundMeters(session.Uid, parkId, &keyword, &limit)
if err != nil {
meterLog.Error("无法列出指定园区中尚未绑定公摊表计的表计,无法获取表计列表", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
var simplifiedMeters = make([]*vo.SimplifiedMeterQueryResponse, 0)
copier.Copy(&simplifiedMeters, &meters)
return result.Success("已经取得符合条件的表计列表。", fiber.Map{"meters": simplifiedMeters})
}
// 列出指定园区中尚未绑定商户的表计
func listUnboundTenementMeters(c *fiber.Ctx) error {
result := response.NewResult(c)
session, err := _retreiveSession(c)
if err != nil {
meterLog.Error("无法列出指定园区中尚未绑定商户的表计,无法获取当前用户会话", zap.Error(err))
return result.Unauthorized(err.Error())
}
parkId := c.Query("park")
if len(parkId) == 0 {
meterLog.Error("无法列出指定园区中尚未绑定商户的表计未指定要访问的园区ID")
return result.NotAccept("未指定要访问的园区。")
}
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
keyword := c.Query("keyword")
limit := uint(c.QueryInt("limit", 6))
meters, err := repository.MeterRepository.ListUnboundTenementMeters(session.Uid, &parkId, &keyword, &limit)
if err != nil {
meterLog.Error("无法列出指定园区中尚未绑定商户的表计,无法获取表计列表", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
var simplifiedMeters = make([]*vo.SimplifiedMeterQueryResponse, 0)
copier.Copy(&simplifiedMeters, &meters)
return result.Success("已经取得符合条件的表计列表。", fiber.Map{"meters": simplifiedMeters})
}
// 查询指定园区中的表计读数
func queryMeterReadings(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
keyword := tools.EmptyToNil(c.Query("keyword"))
page := c.QueryInt("page", 1)
building := tools.EmptyToNil(c.Query("building"))
start := c.Query("start_date")
var startDate *types.Date = nil
if len(start) > 0 {
if parsedDate, err := types.ParseDate(start); err != nil {
meterLog.Error("查询指定园区中的表计读数,无法解析开始日期", zap.Error(err))
} else {
startDate = &parsedDate
}
}
end := c.Query("end_date")
var endDate *types.Date = nil
if len(end) > 0 {
if parsedDate, err := types.ParseDate(end); err != nil {
meterLog.Error("查询指定园区中的表计读数,无法解析结束日期", zap.Error(err))
} else {
endDate = &parsedDate
}
}
readings, total, err := service.MeterService.SearchMeterReadings(parkId, building, startDate, endDate, uint(page), keyword)
if err != nil {
meterLog.Error("查询指定园区中的表计读数,无法获取表计读数列表", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
convertedReadings := lo.Map(readings, func(element *model.DetailedMeterReading, _ int) vo.MeterReadingDetailResponse {
return vo.FromDetailedMeterReading(*element)
})
return result.Success(
"指定园区的表计读数已经列出。",
response.NewPagedResponse(page, total).ToMap(),
fiber.Map{"records": convertedReadings},
)
}
// 记录一条新的表计抄表记录
func recordMeterReading(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
meterCode := c.Params("code")
var readingForm vo.MeterReadingForm
if err := c.BodyParser(&readingForm); err != nil {
meterLog.Error("记录一条新的表计抄表记录,无法解析表计抄表表单", zap.Error(err))
return result.NotAccept(fmt.Sprintf("无法解析表计抄表表单,%s", err.Error()))
}
if !readingForm.Validate() {
meterLog.Warn("记录一条新的表计抄表记录,表计读数不能正常配平,尖、峰、谷电量和超过总电量。")
return result.NotAccept("表计读数不能正常配平,尖、峰、谷电量和超过总电量。")
}
err := service.MeterService.RecordReading(parkId, meterCode, &readingForm)
if err != nil {
meterLog.Error("记录一条新的表计抄表记录,无法记录表计抄表记录", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Created("表计抄表记录已经记录完成。")
}
// 更新指定园区中指定表计的抄表记录
func updateMeterReading(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
meterCode := c.Params("code")
readingAtMicro, err := c.ParamsInt("reading")
if err != nil {
meterLog.Error("更新一条新的表计抄表记录,无法解析抄表时间", zap.Error(err))
return result.NotAccept(fmt.Sprintf("无法解析抄表时间,%s", err.Error()))
}
readingAt := types.FromUnixMicro(int64(readingAtMicro))
var readingForm vo.MeterReadingForm
if err := c.BodyParser(&readingForm); err != nil {
meterLog.Error("更新一条新的表计抄表记录,无法解析表计抄表表单", zap.Error(err))
return result.NotAccept(fmt.Sprintf("无法解析表计抄表表单,%s", err.Error()))
}
ok, err := repository.MeterRepository.UpdateMeterReading(parkId, meterCode, readingAt, &readingForm)
if err != nil {
meterLog.Error("更新一条新的表计抄表记录,无法更新表计抄表记录", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
if !ok {
meterLog.Warn("更新一条新的表计抄表记录,表计抄表更新失败。")
return result.NotAccept("表计抄表记录未能成功更新,可能指定抄表记录不存在。")
}
return result.Success("表计抄表记录已经更新完成。")
}
// 下载指定园区的表计抄表模板
func downloadMeterReadingsTemplate(c *fiber.Ctx) error {
parkId := c.Params("pid")
meterLog.Info("下载指定的园区表计抄表模板", zap.String("park id", parkId))
result := response.NewResult(c)
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
parkDetail, err := repository.ParkRepository.RetrieveParkDetail(parkId)
if err != nil {
meterLog.Error("无法下载指定的园区表计登记模板,无法获取园区信息", zap.Error(err))
return result.NotFound(err.Error())
}
meterDocs, err := repository.MeterRepository.ListMeterDocForTemplate(parkId)
if err != nil {
meterLog.Error("无法下载指定的园区表计抄表模板,无法获取表计档案列表", zap.Error(err))
return result.NotFound(fmt.Sprintf("无法获取表计档案列表,%s", err.Error()))
}
if err != nil {
meterLog.Error("无法下载指定的园区表计登记模板,无法生成表计登记模板", zap.Error(err))
return result.NotFound(fmt.Sprintf("无法生成表计登记模板,%s", err.Error()))
}
templateGenerator := excel.NewMeterReadingsExcelTemplateGenerator()
defer templateGenerator.Close()
err = templateGenerator.WriteTemplateData(meterDocs)
if err != nil {
meterLog.Error("无法下载指定的园区表计抄表模板,无法生成表计抄表模板", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, fmt.Sprintf("无法生成表计抄表模板,%s", err.Error()))
}
c.Status(fiber.StatusOK)
c.Set(fiber.HeaderContentType, fiber.MIMEOctetStream)
c.Set("Content-Transfer-Encoding", "binary")
c.Set(fiber.HeaderContentDisposition, fmt.Sprintf("attachment; filename=%s-表计抄表模板.xlsx", parkDetail.Name))
templateGenerator.WriteTo(c.Response().BodyWriter())
return nil
}
// 处理上传的抄表记录文件
func uploadMeterReadings(c *fiber.Ctx) error {
parkId := c.Params("pid")
meterLog.Info("从Excel文件中导入抄表档案", zap.String("park id", parkId))
result := response.NewResult(c)
if pass, err := checkParkBelongs(parkId, meterLog, c, &result); !pass {
return err
}
uploadFile, err := c.FormFile("data")
if err != nil {
meterLog.Error("无法从Excel文件中导入抄表档案无法获取上传的文件", zap.Error(err))
return result.NotAccept(fmt.Sprintf("没有接收到上传的文件,%s", err.Error()))
}
errs, err := service.MeterService.BatchImportReadings(parkId, uploadFile)
if err != nil {
meterLog.Error("无法从Excel文件中导入抄表档案无法导入抄表档案", zap.Error(err))
return result.Json(fiber.StatusNotAcceptable, "上传的抄表档案存在错误。", fiber.Map{"errors": errs})
}
return result.Success("表计档案已经导入完成。", fiber.Map{"errors": errs})
}

View File

@ -1,185 +1,331 @@
package controller
import (
"electricity_bill_calc/model"
"electricity_bill_calc/logger"
"electricity_bill_calc/repository"
"electricity_bill_calc/response"
"electricity_bill_calc/security"
"electricity_bill_calc/service"
"electricity_bill_calc/tools"
"electricity_bill_calc/vo"
"net/http"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/jinzhu/copier"
"github.com/shopspring/decimal"
"go.uber.org/zap"
)
func InitializeParkController(router *fiber.App) {
router.Get("/parks", security.EnterpriseAuthorize, listAllParksUnderSessionUser)
router.Get("/parks/:uid", security.MustAuthenticated, listAllParksUnderSpecificUser)
router.Post("/park", security.EnterpriseAuthorize, createNewPark)
router.Put("/park/:pid", security.EnterpriseAuthorize, modifyPark)
var parkLog = logger.Named("Handler", "Park")
func InitializeParkHandlers(router *fiber.App) {
router.Get("/park", security.EnterpriseAuthorize, listParksBelongsToCurrentUser)
router.Post("/park", security.EnterpriseAuthorize, createPark)
router.Get("/park/belongs/:uid", security.OPSAuthorize, listParksBelongsTo)
router.Get("/park/:pid", security.EnterpriseAuthorize, fetchParkDetail)
router.Put("/park/:pid/enabled", security.EnterpriseAuthorize, changeParkEnableState)
router.Put("/park/:pid", security.EnterpriseAuthorize, modifySpecificPark)
router.Delete("/park/:pid", security.EnterpriseAuthorize, deleteSpecificPark)
router.Put("/park/:pid/enabled", security.EnterpriseAuthorize, modifyParkEnabling)
router.Get("/park/:pid/building", security.EnterpriseAuthorize, listBuildingsBelongsToPark)
router.Post("/park/:pid/building", security.EnterpriseAuthorize, createBuildingInPark)
router.Put("/park/:pid/building/:bid", security.EnterpriseAuthorize, modifySpecificBuildingInPark)
router.Delete("/park/:pid/building/:bid", security.EnterpriseAuthorize, deletedParkBuilding)
router.Put("/park/:pid/building/:bid/enabled", security.EnterpriseAuthorize, modifyParkBuildingEnabling)
}
func ensureParkBelongs(c *fiber.Ctx, result *response.Result, requestParkId string) (bool, error) {
userSession, err := _retreiveSession(c)
if err != nil {
return false, result.Unauthorized(err.Error())
}
sure, err := service.ParkService.EnsurePark(userSession.Uid, requestParkId)
if err != nil {
return false, result.Error(http.StatusInternalServerError, err.Error())
}
if !sure {
return false, result.Unauthorized("不能访问不属于自己的园区。")
}
return true, nil
}
func listAllParksUnderSessionUser(c *fiber.Ctx) error {
// 列出隶属于当前用户的全部园区
func listParksBelongsToCurrentUser(c *fiber.Ctx) error {
result := response.NewResult(c)
userSession, err := _retreiveSession(c)
session, err := _retreiveSession(c)
if err != nil {
parkLog.Error("列出当前用的全部园区,无法获取当前用户的会话。")
return result.Unauthorized(err.Error())
}
keyword := c.Query("keyword")
parks, err := service.ParkService.ListAllParkBelongsTo(userSession.Uid, keyword)
parkLog.Info("列出当前用户下的全部园区", zap.String("user id", session.Uid))
parks, err := repository.ParkRepository.ListAllParks(session.Uid)
if err != nil {
parkLog.Error("无法获取园区列表。", zap.String("user id", session.Uid))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Json(http.StatusOK, "已获取到指定用户下的园区。", fiber.Map{"parks": parks})
return result.Success("已获取到指定用户的下的园区", fiber.Map{"parks": parks})
}
func listAllParksUnderSpecificUser(c *fiber.Ctx) error {
// 列出隶属于指定用户的全部园区
func listParksBelongsTo(c *fiber.Ctx) error {
result := response.NewResult(c)
requestUserId := c.Params("uid")
keyword := c.Query("keyword")
parks, err := service.ParkService.ListAllParkBelongsTo(requestUserId, keyword)
userId := c.Params("uid")
parkLog.Info("列出指定用户下的全部园区", zap.String("user id", userId))
parks, err := repository.ParkRepository.ListAllParks(userId)
if err != nil {
parkLog.Error("无法获取园区列表。", zap.String("user id", userId))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Json(http.StatusOK, "已获取到指定用户下的园区。", fiber.Map{"parks": parks})
}
type _ParkInfoFormData struct {
Name string `json:"name" form:"name"`
Region *string `json:"region" form:"region"`
Address *string `json:"address" form:"address"`
Contact *string `json:"contact" form:"contact"`
Phone *string `json:"phone" from:"phone"`
Area decimal.NullDecimal `json:"area" from:"area"`
Capacity decimal.NullDecimal `json:"capacity" from:"capacity"`
TenementQuantity decimal.NullDecimal `json:"tenement" from:"tenement"`
Category int8 `json:"category" form:"category"`
SubmeterType int8 `json:"submeter" form:"submeter"`
}
func createNewPark(c *fiber.Ctx) error {
result := response.NewResult(c)
userSession, err := _retreiveSession(c)
if err != nil {
return result.Unauthorized(err.Error())
}
formData := new(_ParkInfoFormData)
if err := c.BodyParser(formData); err != nil {
return result.UnableToParse("无法解析提交的数据。")
}
newPark := new(model.Park)
copier.Copy(newPark, formData)
newPark.Id = uuid.New().String()
newPark.UserId = userSession.Uid
nameAbbr := tools.PinyinAbbr(newPark.Name)
newPark.Abbr = &nameAbbr
newPark.Enabled = true
err = service.ParkService.SaveNewPark(*newPark)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Created("新园区完成创建。")
}
func modifyPark(c *fiber.Ctx) error {
result := response.NewResult(c)
userSession, err := _retreiveSession(c)
if err != nil {
return result.Unauthorized(err.Error())
}
requestParkId := c.Params("pid")
formData := new(_ParkInfoFormData)
if err := c.BodyParser(formData); err != nil {
return result.UnableToParse("无法解析提交的数据。")
}
park, err := service.ParkService.FetchParkDetail(requestParkId)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
if userSession.Uid != park.UserId {
return result.Unauthorized("不能修改不属于自己的园区。")
}
copier.Copy(park, formData)
nameAbbr := tools.PinyinAbbr(formData.Name)
park.Abbr = &nameAbbr
err = service.ParkService.UpdateParkInfo(park)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Updated("指定园区资料已更新。")
return result.Success("已获取到指定用户的下的园区", fiber.Map{"parks": parks})
}
// 获取指定园区的详细信息
func fetchParkDetail(c *fiber.Ctx) error {
result := response.NewResult(c)
requestParkId := c.Params("pid")
if ensure, err := ensureParkBelongs(c, &result, requestParkId); !ensure {
return err
}
park, err := service.ParkService.FetchParkDetail(requestParkId)
parkId := c.Params("pid")
parkLog.Info("获取指定园区的详细信息", zap.String("park id", parkId))
park, err := repository.ParkRepository.RetrieveParkDetail(parkId)
if err != nil {
parkLog.Error("无法获取园区信息。", zap.String("park id", parkId))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Json(http.StatusOK, "已经获取到指定园区的信息。", fiber.Map{"park": park})
return result.Success("已获取到指定园区的详细信息", fiber.Map{"park": park})
}
type _ParkStateFormData struct {
Enabled bool `json:"enabled" form:"enabled"`
}
func changeParkEnableState(c *fiber.Ctx) error {
// 创建一个新的园区
func createPark(c *fiber.Ctx) error {
result := response.NewResult(c)
userSession, err := _retreiveSession(c)
session, err := _retreiveSession(c)
if err != nil {
parkLog.Error("创建一个新的园区,无法获取当前用户的会话。")
return result.Unauthorized(err.Error())
}
requestParkId := c.Params("pid")
if ensure, err := ensureParkBelongs(c, &result, requestParkId); !ensure {
return err
parkLog.Info("创建一个新的园区", zap.String("user id", session.Uid))
creationForm := new(vo.ParkInformationForm)
if err := c.BodyParser(creationForm); err != nil {
parkLog.Error("无法解析园区表单数据。", zap.String("user id", session.Uid), zap.Error(err))
return result.NotAccept(err.Error())
}
formData := new(_ParkStateFormData)
if err := c.BodyParser(formData); err != nil {
return result.UnableToParse("无法解析提交的数据。")
}
err = service.ParkService.ChangeParkState(userSession.Uid, requestParkId, formData.Enabled)
park, err := creationForm.TryIntoPark()
if err != nil {
parkLog.Error("无法将园区表单数据转换为园区对象。", zap.String("user id", session.Uid), zap.Error(err))
return result.NotAccept(err.Error())
}
ok, err := repository.ParkRepository.CreatePark(session.Uid, park)
switch {
case err == nil && !ok:
parkLog.Error("无法创建新的园区。", zap.String("user id", session.Uid))
return result.NotAccept("无法创建新的园区。")
case err != nil:
parkLog.Error("无法创建新的园区。", zap.String("user id", session.Uid), zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Updated("指定园区的可用性状态已成功更新。")
return result.Created("已创建一个新的园区")
}
// 修改指定园区的信息
func modifySpecificPark(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
session, err := _retreiveSession(c)
if err != nil {
parkLog.Error("修改指定园区的信息,无法获取当前用户的会话。")
return result.Unauthorized(err.Error())
}
if pass, err := checkParkBelongs(parkId, parkLog, c, &result); !pass {
return err
}
parkForm := new(vo.ParkInformationForm)
if err := c.BodyParser(parkForm); err != nil {
parkLog.Error("无法解析园区表单数据。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.NotAccept(err.Error())
}
park, err := parkForm.TryIntoPark()
if err != nil {
parkLog.Error("无法将园区表单数据转换为园区对象。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.NotAccept(err.Error())
}
ok, err := repository.ParkRepository.UpdatePark(parkId, park)
switch {
case err == nil && !ok:
parkLog.Error("无法更新园区信息。", zap.String("park id", parkId), zap.String("user id", session.Uid))
return result.NotAccept("无法更新园区信息。")
case err != nil:
parkLog.Error("无法更新园区信息。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Updated("已更新指定园区的详细信息")
}
// 修改指定园区的可用性
func modifyParkEnabling(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
session, err := _retreiveSession(c)
if err != nil {
parkLog.Error("修改指定园区的可用性,无法获取当前用户的会话。")
return result.Unauthorized(err.Error())
}
if pass, err := checkParkBelongs(parkId, parkLog, c, &result); !pass {
return err
}
stateForm := new(vo.StateForm)
if err := c.BodyParser(stateForm); err != nil {
parkLog.Error("无法解析园区表单数据。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.NotAccept(err.Error())
}
ok, err := repository.ParkRepository.EnablingPark(parkId, stateForm.Enabled)
switch {
case err == nil && !ok:
parkLog.Error("无法更新园区可用性。", zap.String("park id", parkId), zap.String("user id", session.Uid))
return result.NotAccept("无法更新园区可用性。")
case err != nil:
parkLog.Error("无法更新园区可用性。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Updated("已更新指定园区的可用性。")
}
// 删除指定的园区
func deleteSpecificPark(c *fiber.Ctx) error {
result := response.NewResult(c)
userSession, err := _retreiveSession(c)
parkId := c.Params("pid")
session, err := _retreiveSession(c)
if err != nil {
parkLog.Error("删除指定的园区,无法获取当前用户的会话。")
return result.Unauthorized(err.Error())
}
requestParkId := c.Params("pid")
if ensure, err := ensureParkBelongs(c, &result, requestParkId); !ensure {
if pass, err := checkParkBelongs(parkId, parkLog, c, &result); !pass {
return err
}
err = service.ParkService.DeletePark(userSession.Uid, requestParkId)
if err != nil {
ok, err := repository.ParkRepository.DeletePark(parkId)
switch {
case err == nil && !ok:
parkLog.Error("无法删除园区。", zap.String("park id", parkId), zap.String("user id", session.Uid))
return result.NotAccept("无法删除园区。")
case err != nil:
parkLog.Error("无法删除园区。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Deleted("指定园区已成功删除。")
return result.Deleted("已删除指定的园区")
}
// 列出指定园区中已经登记的建筑
func listBuildingsBelongsToPark(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
session, err := _retreiveSession(c)
if err != nil {
parkLog.Error("列出指定园区中已经登记的建筑,无法获取当前用户的会话。")
return result.Unauthorized(err.Error())
}
ok, err := repository.ParkRepository.IsParkBelongs(parkId, session.Uid)
switch {
case err != nil:
parkLog.Error("无法判断园区是否隶属于当前用户。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
case err == nil && !ok:
parkLog.Error("用户试图访问不属于自己的园区。", zap.String("park id", parkId), zap.String("user id", session.Uid))
return result.Forbidden("您无权访问该园区。")
}
buildings, err := repository.ParkRepository.RetrieveParkBuildings(parkId)
if err != nil {
parkLog.Error("无法获取园区中的建筑列表。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Success("已获取到指定园区中的建筑列表", fiber.Map{"buildings": buildings})
}
// 在指定园区中创建一个新的建筑
func createBuildingInPark(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
session, err := _retreiveSession(c)
if err != nil {
parkLog.Error("在指定园区中创建一个新的建筑,无法获取当前用户的会话。")
return result.Unauthorized(err.Error())
}
if pass, err := checkParkBelongs(parkId, parkLog, c, &result); !pass {
return err
}
buildingForm := new(vo.ParkBuildingInformationForm)
if err := c.BodyParser(buildingForm); err != nil {
parkLog.Error("无法解析建筑表单数据。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.NotAccept(err.Error())
}
ok, err := repository.ParkRepository.CreateParkBuilding(parkId, buildingForm.Name, &buildingForm.Floors)
switch {
case err == nil && !ok:
parkLog.Error("无法创建新的建筑。", zap.String("park id", parkId), zap.String("user id", session.Uid))
return result.NotAccept("无法创建新的建筑。")
case err != nil:
parkLog.Error("无法创建新的建筑。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Created("已创建一个新的建筑")
}
// 修改指定园区中的指定建筑的信息
func modifySpecificBuildingInPark(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
buildingId := c.Params("bid")
session, err := _retreiveSession(c)
if err != nil {
parkLog.Error("修改指定园区中的指定建筑的信息,无法获取当前用户的会话。")
return result.Unauthorized(err.Error())
}
if pass, err := checkParkBelongs(parkId, parkLog, c, &result); !pass {
return err
}
buildingForm := new(vo.ParkBuildingInformationForm)
if err := c.BodyParser(buildingForm); err != nil {
parkLog.Error("无法解析建筑表单数据。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.NotAccept(err.Error())
}
ok, err := repository.ParkRepository.ModifyParkBuilding(buildingId, parkId, buildingForm.Name, &buildingForm.Floors)
switch {
case err == nil && !ok:
parkLog.Error("无法更新建筑信息。", zap.String("park id", parkId), zap.String("user id", session.Uid))
return result.NotAccept("无法更新建筑信息。")
case err != nil:
parkLog.Error("无法更新建筑信息。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Updated("已更新指定建筑的信息")
}
// 修改指定园区中指定建筑的可用性
func modifyParkBuildingEnabling(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
buildingId := c.Params("bid")
session, err := _retreiveSession(c)
if err != nil {
parkLog.Error("修改指定园区中指定建筑的可用性,无法获取当前用户的会话。")
return result.Unauthorized(err.Error())
}
if pass, err := checkParkBelongs(parkId, parkLog, c, &result); !pass {
return err
}
stateForm := new(vo.StateForm)
if err := c.BodyParser(stateForm); err != nil {
parkLog.Error("无法解析建筑表单数据。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.NotAccept(err.Error())
}
ok, err := repository.ParkRepository.EnablingParkBuilding(buildingId, parkId, stateForm.Enabled)
switch {
case err == nil && !ok:
parkLog.Error("无法更新建筑可用性。", zap.String("park id", parkId), zap.String("user id", session.Uid))
return result.NotAccept("无法更新建筑可用性。")
case err != nil:
parkLog.Error("无法更新建筑可用性。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Updated("已更新指定建筑的可用性")
}
// 删除指定园区中的指定建筑
func deletedParkBuilding(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
buildingId := c.Params("bid")
session, err := _retreiveSession(c)
if err != nil {
parkLog.Error("删除指定园区中的指定建筑,无法获取当前用户的会话。")
return result.Unauthorized(err.Error())
}
if pass, err := checkParkBelongs(parkId, parkLog, c, &result); !pass {
return err
}
ok, err := repository.ParkRepository.DeleteParkBuilding(buildingId, parkId)
switch {
case err == nil && !ok:
parkLog.Error("无法删除建筑。", zap.String("park id", parkId), zap.String("user id", session.Uid))
return result.NotAccept("无法删除建筑。")
case err != nil:
parkLog.Error("无法删除建筑。", zap.String("park id", parkId), zap.String("user id", session.Uid), zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Deleted("已删除指定的建筑")
}

View File

@ -1,22 +1,22 @@
package controller
import (
"electricity_bill_calc/repository"
"electricity_bill_calc/response"
"electricity_bill_calc/service"
"net/http"
"github.com/gofiber/fiber/v2"
)
func InitializeRegionController(router *fiber.App) {
router.Get("/region/:rid", fetchRegions)
router.Get("/regions/:rid", fetchAllLeveledRegions)
func InitializeRegionHandlers(router *fiber.App) {
router.Get("/region/:rid", getSubRegions)
router.Get("/regions/:rid", getParentRegions)
}
func fetchRegions(c *fiber.Ctx) error {
func getSubRegions(c *fiber.Ctx) error {
result := response.NewResult(c)
requestParentId := c.Params("rid")
regions, err := service.RegionService.FetchSubRegions(requestParentId)
regions, err := repository.RegionRepository.FindSubRegions(requestParentId)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
@ -26,10 +26,10 @@ func fetchRegions(c *fiber.Ctx) error {
return result.Json(http.StatusOK, "已经获取到相关的行政区划。", fiber.Map{"regions": regions})
}
func fetchAllLeveledRegions(c *fiber.Ctx) error {
func getParentRegions(c *fiber.Ctx) error {
result := response.NewResult(c)
requestRegionCode := c.Params("rid")
regions, err := service.RegionService.FetchAllParentRegions(requestRegionCode)
regions, err := repository.RegionRepository.FindParentRegions(requestRegionCode)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}

View File

@ -1,301 +1,424 @@
package controller
import (
"electricity_bill_calc/exceptions"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"electricity_bill_calc/response"
"electricity_bill_calc/security"
"electricity_bill_calc/service"
"electricity_bill_calc/tools"
"net/http"
"strconv"
"time"
"electricity_bill_calc/types"
"electricity_bill_calc/vo"
"github.com/gofiber/fiber/v2"
"github.com/jinzhu/copier"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"go.uber.org/zap"
)
func InitializeReportController(router *fiber.App) {
router.Get("/reports/with/drafts", security.EnterpriseAuthorize, fetchNewestReportOfParkWithDraft)
router.Post("/park/:pid/report", security.EnterpriseAuthorize, initializeNewReport)
router.Get("/report/:rid/step/state", security.EnterpriseAuthorize, fetchReportStepStates)
router.Get("/report/:rid/summary", security.EnterpriseAuthorize, fetchReportParkSummary)
router.Put("/report/:rid/summary", security.EnterpriseAuthorize, fillReportSummary)
router.Get("/report/:rid/summary/calculate", security.EnterpriseAuthorize, testCalculateReportSummary)
router.Post("/report/:rid/summary/calculate", security.EnterpriseAuthorize, progressReportSummary)
router.Put("/report/:rid/step/meter/register", security.EnterpriseAuthorize, progressEndUserRegister)
var reportLog = logger.Named("Handler", "Report")
func InitializeReportHandlers(router *fiber.App) {
router.Get("/reports", security.MustAuthenticated, reportComprehensiveSearch)
router.Post("/report", security.EnterpriseAuthorize, initNewReportCalculateTask)
router.Get("/report/draft", security.EnterpriseAuthorize, listDraftReportIndicies)
router.Post("/report/calcualte", security.EnterpriseAuthorize, testCalculateReportSummary)
router.Get("/report/calculate/status", security.EnterpriseAuthorize, listCalculateTaskStatus)
router.Get("/report/:rid", security.EnterpriseAuthorize, getReportDetail)
router.Put("/report/:rid", security.EnterpriseAuthorize, updateReportCalculateTask)
router.Post("/report/:rid/publish", security.EnterpriseAuthorize, publishReport)
router.Get("/reports", security.MustAuthenticated, searchReports)
router.Get("/report/:rid", security.MustAuthenticated, fetchReportPublicity)
router.Post("/report/:rid/calculate", security.EnterpriseAuthorize, calculateReport)
router.Put("/report/:rid/calculate", security.EnterpriseAuthorize, initiateCalculateTask)
router.Get("/report/:rid/publics", security.MustAuthenticated, listPublicMetersInReport)
router.Get("/report/:rid/summary", security.MustAuthenticated, getReportSummary)
router.Get("/report/:rid/summary/filled", security.EnterpriseAuthorize, getParkFilledSummary)
router.Get("/report/:rid/pooled", security.MustAuthenticated, listPooledMetersInReport)
router.Get("/report/:rid/pooled/:code/submeter", security.MustAuthenticated, listSubmetersInPooledMeter)
router.Get("/report/:rid/tenement", security.MustAuthenticated, listTenementsInReport)
router.Get("/report/:rid/tenement/:tid", security.MustAuthenticated, getTenementDetailInReport)
}
func ensureReportBelongs(c *fiber.Ctx, result *response.Result, requestReportId string) (bool, error) {
_, err := _retreiveSession(c)
// 检查指定报表是否属于当前用户
func checkReportBelongs(reportId string, log *zap.Logger, c *fiber.Ctx, result *response.Result) (bool, error) {
session, err := _retreiveSession(c)
if err != nil {
return false, result.Unauthorized(err.Error())
log.Error("无法获取当前用户的会话信息", zap.Error(err))
return false, result.Unauthorized("无法获取当前用户的会话信息。")
}
requestReport, err := service.ReportService.RetreiveReportIndex(requestReportId)
ok, err := repository.ReportRepository.IsBelongsTo(reportId, session.Uid)
if err != nil {
return false, result.NotFound(err.Error())
log.Error("无法检查核算报表的所有权", zap.Error(err))
return false, result.Error(fiber.StatusInternalServerError, "无法检查核算报表的所有权。")
}
if requestReport == nil {
return false, result.NotFound("指定报表未能找到。")
if !ok {
log.Error("核算报表不属于当前用户")
return false, result.Forbidden("核算报表不属于当前用户。")
}
return ensureParkBelongs(c, result, requestReport.ParkId)
return true, nil
}
func fetchNewestReportOfParkWithDraft(c *fiber.Ctx) error {
// 获取当前登录用户下所有园区的尚未发布的核算报表索引
func listDraftReportIndicies(c *fiber.Ctx) error {
result := response.NewResult(c)
userSession, err := _retreiveSession(c)
session, err := _retreiveSession(c)
if err != nil {
return result.Unauthorized(err.Error())
reportLog.Error("无法获取当前用户的会话信息", zap.Error(err))
return result.Unauthorized("无法获取当前用户的会话信息。")
}
parks, err := service.ReportService.FetchParksWithNewestReport(userSession.Uid)
reportLog.Info("检索指定用户下的未发布核算报表索引", zap.String("User", session.Uid))
indicies, err := service.ReportService.ListDraftReportIndicies(session.Uid)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
reportLog.Error("无法获取当前用户的核算报表索引", zap.Error(err))
return result.NotFound("当前用户下未找到核算报表索引。")
}
return result.Json(http.StatusOK, "已获取到指定用户下所有园区的最新报表记录。", fiber.Map{"parks": parks})
return result.Success(
"已经获取到指定用户的报表索引。",
fiber.Map{"reports": indicies},
)
}
func initializeNewReport(c *fiber.Ctx) error {
// 初始化一个新的核算任务
func initNewReportCalculateTask(c *fiber.Ctx) error {
result := response.NewResult(c)
requestParkId := c.Params("pid")
userSession, err := _retreiveSession(c)
session, err := _retreiveSession(c)
if err != nil {
return result.Unauthorized(err.Error())
reportLog.Error("无法获取当前用户的会话信息", zap.Error(err))
return result.Unauthorized("无法获取当前用户的会话信息。")
}
if ensure, err := ensureParkBelongs(c, &result, requestParkId); !ensure {
reportLog.Info("初始化指定用户的一个新核算任务", zap.String("User", session.Uid))
var form vo.ReportCreationForm
if err := c.BodyParser(&form); err != nil {
reportLog.Error("无法解析创建核算报表的请求数据。", zap.Error(err))
return result.BadRequest("无法解析创建核算报表的请求数据。")
}
if pass, err := checkParkBelongs(form.Park, reportLog, c, &result); !pass {
return err
}
requestPeriod := c.Query("period")
reportPeriod, err := time.Parse("2006-01", requestPeriod)
ok, err := repository.ReportRepository.CreateReport(&form)
if err != nil {
return result.NotAccept("提供的初始化期数格式不正确。")
reportLog.Error("无法创建核算报表", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "无法创建核算报表。")
}
valid, err := service.ReportService.IsNewPeriodValid(userSession.Uid, requestParkId, reportPeriod)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
if !ok {
reportLog.Error("未能完成核算报表的保存。")
return result.NotAccept("未能完成核算报表的保存。")
}
if !valid {
return result.NotAccept("只能初始化已发布报表下一个月份的新报表。")
}
newId, err := service.ReportService.InitializeNewReport(requestParkId, reportPeriod)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Created("新一期报表初始化成功。", fiber.Map{"reportId": newId})
return result.Success("已经成功创建核算报表。")
}
func fetchReportStepStates(c *fiber.Ctx) error {
// 更新指定的核算任务
func updateReportCalculateTask(c *fiber.Ctx) error {
result := response.NewResult(c)
requestReportId := c.Params("rid")
if ensure, err := ensureReportBelongs(c, &result, requestReportId); !ensure {
reportId := c.Params("rid")
if pass, err := checkReportBelongs(reportId, reportLog, c, &result); !pass {
return err
}
requestReport, err := service.ReportService.RetreiveReportIndex(requestReportId)
if err != nil {
return result.NotFound(err.Error())
var form vo.ReportModifyForm
if err := c.BodyParser(&form); err != nil {
reportLog.Error("无法解析更新核算报表的请求数据。", zap.Error(err))
return result.BadRequest("无法解析更新核算报表的请求数据。")
}
return result.Json(http.StatusOK, "已经获取到指定报表的填写状态。", fiber.Map{"steps": requestReport.StepState})
ok, err := repository.ReportRepository.UpdateReportSummary(reportId, &form)
if err != nil {
reportLog.Error("无法更新核算报表", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "无法更新核算报表。")
}
if !ok {
reportLog.Error("未能完成核算报表的更新。")
return result.NotAccept("未能完成核算报表的更新。")
}
return result.Success("已经成功更新核算报表。")
}
func fetchReportParkSummary(c *fiber.Ctx) error {
// 启动指定的核算任务
func initiateCalculateTask(c *fiber.Ctx) error {
result := response.NewResult(c)
requestReportId := c.Params("rid")
if ensure, err := ensureReportBelongs(c, &result, requestReportId); !ensure {
reportId := c.Params("rid")
if pass, err := checkReportBelongs(reportId, reportLog, c, &result); !pass {
return err
}
summary, err := service.ReportService.RetreiveReportSummary(requestReportId)
err := service.ReportService.DispatchReportCalculate(reportId)
if err != nil {
return result.NotFound(err.Error())
reportLog.Error("无法启动核算报表计算任务", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "无法启动核算报表计算任务。")
}
return result.Success("已经成功启动核算报表计算任务。")
}
// 获取自己园区的已经填写的园区电量信息
func getParkFilledSummary(c *fiber.Ctx) error {
result := response.NewResult(c)
reportId := c.Params("rid")
if pass, err := checkReportBelongs(reportId, reportLog, c, &result); !pass {
return err
}
reportLog.Info("获取园区电量信息", zap.String("Report", reportId))
summary, err := repository.ReportRepository.RetrieveReportSummary(reportId)
if err != nil {
reportLog.Error("无法获取核算报表的园区电量信息", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "无法获取核算报表的园区电量信息。")
}
if summary == nil {
return result.NotFound("指定报表未能找到。")
reportLog.Error("未找到核算报表的园区电量信息")
return result.NotFound("未找到核算报表的园区电量信息。")
}
return result.Json(http.StatusOK, "已经获取到指定报表中的园区概况。", fiber.Map{"summary": summary})
}
type ReportSummaryFormData struct {
Overall decimal.Decimal `json:"overall" form:"overall"`
OverallFee decimal.Decimal `json:"overallFee" form:"overallFee"`
Critical decimal.Decimal `json:"critical" form:"critical"`
CriticalFee decimal.Decimal `json:"criticalFee" form:"criticalFee"`
Peak decimal.Decimal `json:"peak" form:"peak"`
PeakFee decimal.Decimal `json:"peakFee" form:"peakFee"`
Valley decimal.Decimal `json:"valley" form:"valley"`
ValleyFee decimal.Decimal `json:"valleyFee" form:"valleyFee"`
BasicFee decimal.Decimal `json:"basicFee" form:"basicFee"`
AdjustFee decimal.Decimal `json:"adjustFee" from:"adjustFee"`
}
func fillReportSummary(c *fiber.Ctx) error {
result := response.NewResult(c)
requestReportId := c.Params("rid")
if ensure, err := ensureReportBelongs(c, &result, requestReportId); !ensure {
return err
}
formData := new(ReportSummaryFormData)
if err := c.BodyParser(formData); err != nil {
return result.UnableToParse("无法解析提交的数据。")
}
originSummary, err := service.ReportService.RetreiveReportSummary(requestReportId)
if err != nil {
return result.NotFound(err.Error())
}
copier.Copy(originSummary, formData)
originSummary.ReportId = requestReportId
err = service.ReportService.UpdateReportSummary(originSummary)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Updated("指定电费公示报表中的园区概况基本数据已经完成更新。")
var summaryResponse vo.SimplifiedReportSummary
copier.Copy(&summaryResponse, summary)
return result.Success(
"已经获取到核算报表的园区电量信息。",
fiber.Map{"summary": summaryResponse},
)
}
// 对提供的园区电量信息进行试计算,返回试计算结果
func testCalculateReportSummary(c *fiber.Ctx) error {
result := response.NewResult(c)
requestReportId := c.Params("rid")
if ensure, err := ensureReportBelongs(c, &result, requestReportId); !ensure {
return err
reportLog.Info("试计算园区电量信息")
var form vo.TestCalculateForm
if err := c.BodyParser(&form); err != nil {
reportLog.Error("无法解析试计算核算报表的请求数据。", zap.Error(err))
return result.BadRequest("无法解析试计算核算报表的请求数据。")
}
summary, err := service.ReportService.RetreiveReportSummary(requestReportId)
return result.Success(
"电量电费试计算已经完成。",
fiber.Map{"summary": form.Calculate()},
)
}
// 获取指定园区中尚未发布的核算报表计算状态
func listCalculateTaskStatus(c *fiber.Ctx) error {
result := response.NewResult(c)
session, err := _retreiveSession(c)
if err != nil {
return result.NotFound(err.Error())
reportLog.Error("无法获取当前用户的会话信息", zap.Error(err))
return result.Unauthorized("无法获取当前用户的会话信息。")
}
summary.CalculatePrices()
calcResults := tools.ConvertStructToMap(summary)
return result.Json(
http.StatusOK,
"已完成园区概况的试计算。",
status, err := repository.ReportRepository.GetReportTaskStatus(session.Uid)
if err != nil {
reportLog.Error("无法获取核算报表计算状态", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "无法获取核算报表计算状态。")
}
statusResponse := make([]*vo.ReportCalculateTaskStatusResponse, 0)
copier.Copy(&statusResponse, &status)
return result.Success(
"已经获取到核算报表计算状态。",
fiber.Map{"status": statusResponse},
)
}
// 获取指定报表的详细信息
func getReportDetail(c *fiber.Ctx) error {
result := response.NewResult(c)
reportId := c.Params("rid")
reportLog.Info("获取核算报表的详细信息", zap.String("Report", reportId))
user, park, report, err := service.ReportService.RetrieveReportIndexDetail(reportId)
if err != nil {
reportLog.Error("无法获取核算报表的详细信息", zap.Error(err))
return result.NotFound("无法获取核算报表的详细信息。")
}
return result.Success(
"已经获取到核算报表的详细信息。",
fiber.Map{
"result": lo.PickByKeys(
calcResults,
[]string{"overallPrice", "criticalPrice", "peakPrice", "flat", "flatFee", "flatPrice", "valleyPrice", "consumptionFee"},
),
"detail": vo.NewReportDetailQueryResponse(user, park, report),
},
)
}
func progressReportSummary(c *fiber.Ctx) error {
// 获取指定核算报表的总览信息
func getReportSummary(c *fiber.Ctx) error {
result := response.NewResult(c)
requestReportId := c.Params("rid")
if ensure, err := ensureReportBelongs(c, &result, requestReportId); !ensure {
return err
}
err := service.ReportService.CalculateSummaryAndFinishStep(requestReportId)
reportId := c.Params("rid")
report, err := repository.ReportRepository.RetrieveReportSummary(reportId)
if err != nil {
if nfErr, ok := err.(exceptions.NotFoundError); ok {
return result.NotFound(nfErr.Error())
} else {
return result.Error(http.StatusInternalServerError, err.Error())
}
reportLog.Error("无法获取核算报表的总览信息", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "无法获取核算报表的总览信息。")
}
return result.Success("已经完成园区概况的计算,并可以进行到下一步骤。")
}
func progressEndUserRegister(c *fiber.Ctx) error {
result := response.NewResult(c)
requestReportId := c.Params("rid")
if ensure, err := ensureReportBelongs(c, &result, requestReportId); !ensure {
return err
}
report, err := service.ReportService.RetreiveReportIndex(requestReportId)
if err != nil {
return result.NotFound(err.Error())
}
err = service.ReportService.ProgressReportRegisterEndUser(*report)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Success("终端用户抄表编辑步骤已经完成。")
}
func publishReport(c *fiber.Ctx) error {
result := response.NewResult(c)
requestReportId := c.Params("rid")
if ensure, err := ensureReportBelongs(c, &result, requestReportId); !ensure {
return err
}
report, err := service.ReportService.RetreiveReportIndex(requestReportId)
if err != nil {
return result.NotFound(err.Error())
}
err = service.ReportService.PublishReport(*report)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Success("指定的公示报表已经发布。")
}
func searchReports(c *fiber.Ctx) error {
result := response.NewResult(c)
session, err := _retreiveSession(c)
if err != nil {
return result.Unauthorized(err.Error())
}
requestUser := lo.
If(session.Type == model.USER_TYPE_ENT, session.Uid).
Else(c.Query("user"))
requestPark := c.Query("park")
if len(requestPark) > 0 && session.Type == model.USER_TYPE_ENT {
if ensure, err := ensureParkBelongs(c, &result, requestPark); !ensure {
return err
}
}
requestPeriodString := c.Query("period")
var requestPeriod *time.Time = nil
if len(requestPeriodString) > 0 {
parsedPeriod, err := time.Parse("2006-01", requestPeriodString)
if err != nil {
return result.NotAccept("参数[period]的格式不正确。")
}
requestPeriod = lo.ToPtr(parsedPeriod)
}
requestKeyword := c.Query("keyword")
requestPage, err := strconv.Atoi(c.Query("page", "1"))
if err != nil {
return result.NotAccept("查询参数[page]格式不正确。")
}
requestAllReports, err := strconv.ParseBool(c.Query("all", "false"))
if err != nil {
return result.NotAccept("查询参数[all]格式不正确。")
}
records, totalItems, err := service.ReportService.SearchReport(requestUser, requestPark, requestKeyword, requestPeriod, requestPage, !requestAllReports)
if err != nil {
return result.NotFound(err.Error())
if report == nil {
reportLog.Error("未找到核算报表的总览信息")
return result.NotFound("未找到核算报表的总览信息。")
}
var summaryResponse vo.ParkSummaryResponse
copier.Copy(&summaryResponse, report)
return result.Success(
"已经取得符合条件的公示报表记录。",
response.NewPagedResponse(requestPage, totalItems).ToMap(),
fiber.Map{"reports": records},
"已经获取到核算报表的总览信息。",
fiber.Map{"summary": summaryResponse},
)
}
func fetchReportPublicity(c *fiber.Ctx) error {
// 获取指定报表中分页的公共表计的核算摘要信息
func listPublicMetersInReport(c *fiber.Ctx) error {
result := response.NewResult(c)
requestReportId := c.Params("rid")
publicity, err := service.ReportService.AssembleReportPublicity(requestReportId)
reportId := c.Params("rid")
reportLog.Info("获取核算报表中的公共表计信息", zap.String("Report", reportId))
page := c.QueryInt("page", 1)
keyword := tools.EmptyToNil(c.Query("keyword"))
meters, total, err := repository.ReportRepository.ListPublicMetersInReport(reportId, uint(page), keyword)
if err != nil {
if nfErr, ok := err.(exceptions.NotFoundError); ok {
return result.NotFound(nfErr.Error())
} else {
return result.Error(http.StatusInternalServerError, err.Error())
}
reportLog.Error("无法获取核算报表中的公共表计信息", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "无法获取核算报表中的公共表计信息。")
}
return result.Success("已经取得指定公示报表的发布版本。", tools.ConvertStructToMap(publicity))
meterResponse := lo.Map(meters, func(meter *model.ReportDetailedPublicConsumption, _ int) *vo.ReportPublicQueryResponse {
m := &vo.ReportPublicQueryResponse{}
m.FromReportDetailPublicConsumption(meter)
return m
})
return result.Success(
"已经获取到指定核算报表中的分页公共表计的核算信息。",
response.NewPagedResponse(page, total).ToMap(),
fiber.Map{"public": meterResponse},
)
}
func calculateReport(c *fiber.Ctx) error {
// 获取指定报表中的分页的公摊表计的核算摘要信息
func listPooledMetersInReport(c *fiber.Ctx) error {
result := response.NewResult(c)
requestReportId := c.Params("rid")
if ensure, err := ensureReportBelongs(c, &result, requestReportId); !ensure {
reportId := c.Params("rid")
reportLog.Info("获取核算报表中的公摊表计信息", zap.String("Report", reportId))
page := c.QueryInt("page", 1)
keyword := tools.EmptyToNil(c.Query("keyword"))
meters, total, err := repository.ReportRepository.ListPooledMetersInReport(reportId, uint(page), keyword)
if err != nil {
reportLog.Error("无法获取核算报表中的公摊表计信息", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "无法获取核算报表中的公摊表计信息。")
}
meterResponse := lo.Map(meters, func(meter *model.ReportDetailedPooledConsumption, _ int) *vo.ReportPooledQueryResponse {
m := &vo.ReportPooledQueryResponse{}
m.FromReportDetailPooledConsumption(meter)
return m
})
return result.Success(
"已经获取到指定核算报表中的分页公摊表计的核算信息。",
response.NewPagedResponse(page, total).ToMap(),
fiber.Map{"pooled": meterResponse},
)
}
// 列出指定报表中指定公共表计下各个分摊表计的消耗数据
func listSubmetersInPooledMeter(c *fiber.Ctx) error {
result := response.NewResult(c)
reportId := c.Params("rid")
meterId := c.Params("code")
if len(meterId) == 0 {
reportLog.Error("未提供公共表计的编号")
return result.BadRequest("未提供公共表计的编号。")
}
meters, err := repository.ReportRepository.ListPooledMeterDetailInReport(reportId, meterId)
if err != nil {
reportLog.Error("无法获取核算报表中的公共表计信息", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "无法获取核算报表中的公共表计信息。")
}
meterResponse := lo.Map(meters, func(meter *model.ReportDetailNestedMeterConsumption, _ int) *vo.ReportPooledQueryResponse {
m := &vo.ReportPooledQueryResponse{}
m.FromReportDetailNestedMeterConsumption(meter)
return m
})
return result.Success(
"已经获取到指定核算报表中的公共表计的核算信息。",
fiber.Map{"meters": meterResponse},
)
}
// 获取指定报表中分页的商户核算电量电费概要数据
func listTenementsInReport(c *fiber.Ctx) error {
result := response.NewResult(c)
reportId := c.Params("rid")
page := c.QueryInt("page", 1)
keyword := tools.EmptyToNil(c.Query("keyword"))
tenements, total, err := repository.ReportRepository.ListTenementInReport(reportId, uint(page), keyword)
if err != nil {
reportLog.Error("无法获取核算报表中的商户信息", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "无法获取核算报表中的商户信息。")
}
tenementsResponse := lo.Map(tenements, func(tenement *model.ReportTenement, _ int) *vo.ReportTenementSummaryResponse {
t := &vo.ReportTenementSummaryResponse{}
t.FromReportTenement(tenement)
return t
})
return result.Success(
"已经获取到指定核算报表中的分页商户的核算信息。",
response.NewPagedResponse(page, total).ToMap(),
fiber.Map{"tenements": tenementsResponse},
)
}
// 获取指定报表中指定商户的详细核算信息
func getTenementDetailInReport(c *fiber.Ctx) error {
result := response.NewResult(c)
reportId := c.Params("rid")
tenementId := c.Params("tid")
detail, err := repository.ReportRepository.GetTenementDetailInReport(reportId, tenementId)
if err != nil {
reportLog.Error("无法获取核算报表中的商户信息", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "无法获取核算报表中的商户信息。")
}
var detailResponse vo.ReportTenementDetailResponse
detailResponse.FromReportTenement(detail)
return result.Success(
"已经获取到指定核算报表中的商户的详细核算信息。",
fiber.Map{"detail": detailResponse},
)
}
// 发布指定的核算报表
func publishReport(c *fiber.Ctx) error {
result := response.NewResult(c)
reportId := c.Params("rid")
if pass, err := checkReportBelongs(reportId, reportLog, c, &result); !pass {
return err
}
err := service.CalculateService.ComprehensivelyCalculateReport(requestReportId)
ok, err := repository.ReportRepository.PublishReport(reportId)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
reportLog.Error("无法发布核算报表", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "发布核算报表出错。")
}
return result.Success("指定公示报表中的数据已经计算完毕。")
if !ok {
reportLog.Error("未能完成核算报表的发布。")
return result.NotAccept("未能完成核算报表的发布。")
}
return result.Success("已经成功发布核算报表。")
}
// 对核算报表进行综合检索
func reportComprehensiveSearch(c *fiber.Ctx) error {
result := response.NewResult(c)
user := tools.EmptyToNil(c.Query("user"))
session, err := _retreiveSession(c)
if err != nil {
reportLog.Error("无法获取当前用户的会话信息", zap.Error(err))
return result.Unauthorized("无法获取当前用户的会话信息。")
}
park := tools.EmptyToNil(c.Query("park_id"))
if session.Type == model.USER_TYPE_ENT && park != nil && len(*park) > 0 {
if pass, err := checkParkBelongs(*park, reportLog, c, &result); !pass {
return err
}
}
var requestUser *string
if session.Type == model.USER_TYPE_ENT {
requestUser = lo.ToPtr(tools.DefaultTo(user, session.Uid))
} else {
requestUser = user
}
page := c.QueryInt("page", 1)
keyword := tools.EmptyToNil(c.Query("keyword"))
startDate, err := types.ParseDatep(c.Query("period_start"))
if err != nil {
reportLog.Error("无法解析核算报表查询的开始日期", zap.Error(err))
return result.BadRequest("无法解析核算报表查询的开始日期。")
}
endDate, err := types.ParseDatep(c.Query("period_end"))
if err != nil {
reportLog.Error("无法解析核算报表查询的结束日期", zap.Error(err))
return result.BadRequest("无法解析核算报表查询的结束日期。")
}
reports, total, err := service.ReportService.QueryReports(requestUser, park, uint(page), keyword, startDate, endDate)
if err != nil {
reportLog.Error("无法查询核算报表", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "无法查询核算报表。")
}
return result.Success(
"已经获取到指定核算报表的分页信息。",
response.NewPagedResponse(page, total).ToMap(),
fiber.Map{"reports": reports},
)
}

121
controller/sync.go Normal file
View File

@ -0,0 +1,121 @@
package controller
import (
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"electricity_bill_calc/response"
"electricity_bill_calc/security"
"electricity_bill_calc/service"
"electricity_bill_calc/tools"
"electricity_bill_calc/vo"
"fmt"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
var synchronizeLog = logger.Named("Handler", "Synchronize")
func InitializeSynchronizeHandlers(router *fiber.App) {
router.Get("/synchronize/task", security.EnterpriseAuthorize, searchSynchronizeSchedules)
router.Get("/synchronize/configuration", security.EnterpriseAuthorize, getSynchronizeConfiguration)
router.Post("/synchronize/configuration", security.EnterpriseAuthorize, recordsynchronizeConfiguration)
}
// 查询当前平台中符合查询条件的同步任务企业用户无论传入什么用户ID条件都仅能看到自己的同步任务
func searchSynchronizeSchedules(c *fiber.Ctx) error {
result := response.NewResult(c)
session, err := _retreiveSession(c)
if err != nil {
synchronizeLog.Error("查询同步任务失败,未能获取当前用户会话信息", zap.Error(err))
return result.Unauthorized("未能获取当前用户会话信息。")
}
parkId := tools.EmptyToNil(c.Params("park"))
if parkId != nil && len(*parkId) > 0 {
if pass, err := checkParkBelongs(*parkId, reportLog, c, &result); !pass {
return err
}
}
userId := tools.EmptyToNil(c.Params("user"))
keyword := tools.EmptyToNil(c.Query("keyword"))
page := c.QueryInt("page", 1)
synchronizeLog.Info("查询当前平台中符合查询条件的同步任务。", zap.String("Ent", session.Uid), zap.Stringp("Park", parkId))
schedules, total, err := repository.SynchronizeRepository.SearchSynchronizeSchedules(userId, parkId, uint(page), keyword)
if err != nil {
reportLog.Error("无法获取同步任务", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "无法获取同步任务")
}
return result.Success(
" ",
response.NewPagedResponse(page, total).ToMap(),
fiber.Map{"tasks": schedules},
)
}
// 获取指定的同步任务配置
func getSynchronizeConfiguration(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Query("park")
userId := c.Query("user")
session, err := _retreiveSession(c)
if err != nil {
reportLog.Error("无法获取当前用户的会话信息", zap.Error(err))
return result.Unauthorized("无法获取当前用户的会话信息。")
}
var user_id string
if session.Type == model.USER_TYPE_ENT {
user_id = session.Uid
} else {
if userId != "" {
user_id = userId
} else {
return result.NotAccept(fmt.Sprintf("必须指定要记录同步任务的用户,%s", err.Error()))
}
}
fmt.Println("pppppppppppppppppppppppppppp", parkId, len(parkId))
if parkId == "" {
return result.NotAccept("必须指定要获取同步任务的园区。")
}
fmt.Printf(user_id)
configurations, err := repository.SynchronizeRepository.RetrieveSynchronizeConfiguration(user_id, parkId)
if err != nil {
reportLog.Error("无法获取同步任务", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "无法获取同步任务")
}
return result.Success(
" 123",
fiber.Map{"setup": configurations},
)
}
func recordsynchronizeConfiguration(c *fiber.Ctx) error {
userId := c.Query("user")
synchronizeLog.Info("记录一个新的同步任务配置", zap.String("user id", userId))
session, err := _retreiveSession(c)
result := response.NewResult(c)
if err != nil {
reportLog.Error("无法获取当前用户的会话信息", zap.Error(err))
return result.Unauthorized("无法获取当前用户的会话信息。")
}
var Form vo.SynchronizeConfigurationCreateForm
if err := c.BodyParser(&Form); err != nil {
meterLog.Error("无法更新同步配置,无法解析表计更新表单", zap.Error(err))
return result.NotAccept(err.Error())
}
var user_id string
if session.Type == model.USER_TYPE_ENT {
user_id = session.Uid
} else {
if userId != "" {
user_id = userId
} else {
return result.NotAccept(fmt.Sprintf("必须指定更新同步任务的用户,%s", err.Error()))
}
}
//configurations, err := repository.SynchronizeRepository.CreateSynchronizeConfiguration
if err := service.SynchronizeService.CreateSynchronizeConfiguration(user_id, &Form); err != nil {
synchronizeLog.Error("无法更新同步配置", zap.Error(err))
return result.NotAccept(err.Error())
}
return result.Success("更新完成。")
}

286
controller/tenement.go Normal file
View File

@ -0,0 +1,286 @@
package controller
import (
"electricity_bill_calc/logger"
"electricity_bill_calc/repository"
"electricity_bill_calc/response"
"electricity_bill_calc/security"
"electricity_bill_calc/service"
"electricity_bill_calc/tools"
"electricity_bill_calc/types"
"electricity_bill_calc/vo"
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/jinzhu/copier"
"github.com/samber/lo"
"go.uber.org/zap"
)
var tenementLog = logger.Named("Handler", "Tenement")
func InitializeTenementHandler(router *fiber.App) {
router.Get("/tenement/choice", security.EnterpriseAuthorize, listTenementForChoice)
router.Get("/tenement/:pid", security.EnterpriseAuthorize, listTenement)
router.Put("/tenement/:pid/:tid", security.EnterpriseAuthorize, updateTenement)
router.Get("/tenement/:pid/:tid", security.EnterpriseAuthorize, getTenementDetail)
router.Get("/tenement/:pid/:tid/meter", security.EnterpriseAuthorize, listMeters)
router.Post("/tenement/:pid/:tid/move/out", security.EnterpriseAuthorize, moveOutTenement)
router.Post("/tenement/:pid", security.EnterpriseAuthorize, addTenement)
router.Post("/tenement/:pid/:tid/binding", security.EnterpriseAuthorize, bindMeterToTenement)
router.Post("/tenement/:pid/:tid/binding/:code/unbind", security.EnterpriseAuthorize, unbindMeterFromTenement)
}
// 列出园区中的商户
func listTenement(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
tenementLog.Info("列出园区中的商户", zap.String("Park", parkId))
if pass, err := checkParkBelongs(parkId, tenementLog, c, &result); !pass {
return err
}
page := c.QueryInt("page", 1)
keyword := tools.EmptyToNil(c.Query("keyword"))
building := tools.EmptyToNil(c.Query("building"))
startDate, err := types.ParseDatep(c.Query("startDate"))
if err != nil {
tenementLog.Error("列出园区中的商户失败,未能解析查询开始日期", zap.Error(err))
return result.BadRequest(err.Error())
}
endDate, err := types.ParseDatep(c.Query("endDate"))
if err != nil {
tenementLog.Error("列出园区中的商户失败,未能解析查询结束日期", zap.Error(err))
return result.BadRequest(err.Error())
}
state := c.QueryInt("state", 0)
tenements, total, err := repository.TenementRepository.ListTenements(parkId, uint(page), keyword, building, startDate, endDate, state)
if err != nil {
tenementLog.Error("列出园区中的商户失败,未能获取商户列表", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, err.Error())
}
tenementsResponse := make([]*vo.TenementQueryResponse, 0)
copier.Copy(&tenementsResponse, &tenements)
return result.Success(
"已经获取到要查询的商户。",
response.NewPagedResponse(page, total).ToMap(),
fiber.Map{
"tenements": tenementsResponse,
},
)
}
// 列出指定商户下所有的表计
func listMeters(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, tenementLog, c, &result); !pass {
return err
}
tenementId := c.Params("tid")
tenementLog.Info("列出指定商户下所有的表计", zap.String("Park", parkId), zap.String("Tenement", tenementId))
meters, err := service.TenementService.ListMeter(parkId, tenementId)
if err != nil {
tenementLog.Error("列出指定商户下所有的表计失败,未能获取表计列表", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, err.Error())
}
return result.Success(
"已经获取到要查询的表计。",
fiber.Map{
"meters": meters,
},
)
}
// 增加一个新的商户
func addTenement(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, tenementLog, c, &result); !pass {
return err
}
tenementLog.Info("增加一个新的商户", zap.String("Park", parkId))
var form vo.TenementCreationForm
if err := c.BodyParser(&form); err != nil {
tenementLog.Error("增加一个新的商户失败,未能解析要添加的商户信息", zap.Error(err))
return result.BadRequest(fmt.Sprintf("无法解析要添加的商户信息,%s", err.Error()))
}
err := service.TenementService.CreateTenementRecord(parkId, &form)
if err != nil {
tenementLog.Error("增加一个新的商户失败,未能添加商户记录", zap.Error(err))
return result.NotAccept(fmt.Sprintf("无法添加商户记录,%s", err.Error()))
}
return result.Success("已经成功添加商户。")
}
// 给指定商户绑定一个新的表计
func bindMeterToTenement(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, tenementLog, c, &result); !pass {
return err
}
tenementId := c.Params("tid")
if len(tenementId) == 0 {
tenementLog.Error("给指定商户绑定一个新的表计失败,未指定商户。")
return result.BadRequest("未指定商户。")
}
tenementLog.Info("向指定商户绑定一个表计。", zap.String("Park", parkId), zap.String("Tenement", tenementId))
var form vo.MeterReadingFormWithCode
if err := c.BodyParser(&form); err != nil {
tenementLog.Error("给指定商户绑定一个新的表计失败,未能解析要绑定的表计信息", zap.Error(err))
return result.BadRequest(fmt.Sprintf("无法解析要绑定的表计信息,%s", err.Error()))
}
if !form.MeterReadingForm.Validate() {
tenementLog.Error("给指定商户绑定一个新的表计失败,表计读数不能正确配平,尖锋电量、峰电量、谷电量之和超过总电量。")
return result.NotAccept("表计读数不能正确配平,尖锋电量、峰电量、谷电量之和超过总电量。")
}
err := service.TenementService.BindMeter(parkId, tenementId, form.Code, &form.MeterReadingForm)
if err != nil {
tenementLog.Error("给指定商户绑定一个新的表计失败,未能绑定表计", zap.Error(err))
return result.NotAccept(fmt.Sprintf("无法绑定表计,%s", err.Error()))
}
return result.Success("已经成功绑定表计。")
}
// 从指定商户下解除一个表计的绑定
func unbindMeterFromTenement(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, tenementLog, c, &result); !pass {
return err
}
tenementId := c.Params("tid")
if len(tenementId) == 0 {
tenementLog.Error("从指定商户下解除一个表计的绑定失败,未指定商户。")
return result.BadRequest("未指定商户。")
}
meterCode := c.Params("code")
if len(meterCode) == 0 {
tenementLog.Error("从指定商户下解除一个表计的绑定失败,未指定表计。")
return result.BadRequest("未指定表计。")
}
tenementLog.Info("从指定商户处解绑一个表计。", zap.String("Park", parkId), zap.String("Tenement", tenementId), zap.String("Meter", meterCode))
var form vo.MeterReadingForm
if err := c.BodyParser(&form); err != nil {
tenementLog.Error("从指定商户下解除一个表计的绑定失败,未能解析要解除绑定的表计抄表数据。", zap.Error(err))
return result.BadRequest(fmt.Sprintf("无法解析要解除绑定的表计抄表数据,%s", err.Error()))
}
err := service.TenementService.UnbindMeter(parkId, tenementId, meterCode, &form)
if err != nil {
tenementLog.Error("从指定商户下解除一个表计的绑定失败,未能解除绑定表计。", zap.Error(err))
return result.NotAccept(fmt.Sprintf("无法解除绑定表计,%s", err.Error()))
}
return result.Success("已经成功解除表计绑定。")
}
// 修改指定商户的详细信息
func updateTenement(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, tenementLog, c, &result); !pass {
return err
}
tenementId := c.Params("tid")
if len(tenementId) == 0 {
tenementLog.Error("修改指定商户的详细信息失败,未指定商户。")
return result.BadRequest("未指定商户。")
}
tenementLog.Info("修改指定商户的详细信息。", zap.String("Park", parkId), zap.String("Tenement", tenementId))
var form vo.TenementCreationForm
if err := c.BodyParser(&form); err != nil {
tenementLog.Error("修改指定商户的详细信息失败,未能解析要修改的商户信息", zap.Error(err))
return result.BadRequest(fmt.Sprintf("无法解析要修改的商户信息,%s", err.Error()))
}
err := repository.TenementRepository.UpdateTenement(parkId, tenementId, &form)
if err != nil {
tenementLog.Error("修改指定商户的详细信息失败,未能修改商户信息", zap.Error(err))
return result.NotAccept(fmt.Sprintf("无法修改商户信息,%s", err.Error()))
}
return result.Success("商户信息修改成功。")
}
// 迁出指定园区中的商户
func moveOutTenement(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, tenementLog, c, &result); !pass {
return err
}
tenementId := c.Params("tid")
if len(tenementId) == 0 {
tenementLog.Error("迁出指定园区中的商户失败,未指定商户。")
return result.BadRequest("未指定商户。")
}
tenementLog.Info("迁出指定园区中的商户。", zap.String("Park", parkId), zap.String("Tenement", tenementId))
var readings []*vo.MeterReadingFormWithCode
if err := c.BodyParser(&readings); err != nil {
tenementLog.Error("迁出指定园区中的商户失败,未能解析要迁出商户的抄表数据。", zap.Error(err))
return result.BadRequest(fmt.Sprintf("无法解析要迁出商户的抄表数据,%s", err.Error()))
}
err := service.TenementService.MoveOutTenement(parkId, tenementId, readings)
if err != nil {
tenementLog.Error("迁出指定园区中的商户失败,未能迁出商户。", zap.Error(err))
return result.NotAccept(fmt.Sprintf("无法迁出商户,%s", err.Error()))
}
return result.Success("商户迁出成功。")
}
// 列出园区中的商户列表,主要用于下拉列表
func listTenementForChoice(c *fiber.Ctx) error {
result := response.NewResult(c)
session, err := _retreiveSession(c)
if err != nil {
tenementLog.Error("列出园区中的商户列表失败,未能获取当前用户会话信息", zap.Error(err))
return result.Unauthorized("未能获取当前用户会话信息。")
}
parkId := tools.EmptyToNil(c.Params("pid"))
if parkId != nil && len(*parkId) > 0 {
if pass, err := checkParkBelongs(*parkId, tenementLog, c, &result); !pass {
return err
}
}
tenementLog.Info("列出园区中的商户列表,主要用于下拉列表。", zap.String("Ent", session.Uid), zap.Stringp("Park", parkId))
keyword := tools.EmptyToNil(c.Query("keyword"))
limit := c.QueryInt("limit", 6)
tenements, err := repository.TenementRepository.ListForSelect(session.Uid, parkId, keyword, lo.ToPtr(uint(limit)))
if err != nil {
tenementLog.Error("列出园区中的商户列表失败,未能获取商户列表", zap.Error(err))
return result.NotFound(fmt.Sprintf("未能获取商户列表,%s", err.Error()))
}
var tenementsResponse []*vo.SimplifiedTenementResponse
copier.Copy(&tenementsResponse, &tenements)
return result.Success(
"已经获取到要查询的商户。",
fiber.Map{
"tenements": tenementsResponse,
},
)
}
// 获取指定园区中指定商户的详细信息
func getTenementDetail(c *fiber.Ctx) error {
result := response.NewResult(c)
parkId := c.Params("pid")
if pass, err := checkParkBelongs(parkId, tenementLog, c, &result); !pass {
return err
}
tenementId := c.Params("tid")
if len(tenementId) == 0 {
tenementLog.Error("获取指定园区中指定商户的详细信息失败,未指定商户。")
return result.BadRequest("未指定商户。")
}
tenementLog.Info("获取指定园区中指定商户的详细信息。", zap.String("Park", parkId), zap.String("Tenement", tenementId))
tenement, err := repository.TenementRepository.RetrieveTenementDetail(parkId, tenementId)
if err != nil {
tenementLog.Error("获取指定园区中指定商户的详细信息失败,未能获取商户信息", zap.Error(err))
return result.NotFound(fmt.Sprintf("未能获取商户信息,%s", err.Error()))
}
var detail vo.TenementDetailResponse
copier.Copy(&detail, &tenement)
return result.Success(
"已经获取到要查询的商户。",
fiber.Map{
"tenement": detail,
},
)
}

142
controller/top_up.go Normal file
View File

@ -0,0 +1,142 @@
package controller
import (
"electricity_bill_calc/logger"
"electricity_bill_calc/repository"
"electricity_bill_calc/response"
"electricity_bill_calc/security"
"electricity_bill_calc/tools"
"electricity_bill_calc/types"
"electricity_bill_calc/vo"
"github.com/gofiber/fiber/v2"
"github.com/jinzhu/copier"
"go.uber.org/zap"
)
var topUpLog = logger.Named("Controller", "TopUp")
func InitializeTopUpHandlers(router *fiber.App) {
router.Get("/topup/:pid", security.EnterpriseAuthorize, listTopUps)
router.Post("/topup/:pid", security.EnterpriseAuthorize, createTopUp)
router.Get("/topup/:pid/:code", security.EnterpriseAuthorize, getTopUp)
router.Delete("/topup/:pid/:code", security.EnterpriseAuthorize, deleteTopUp)
}
// 查询符合条件的商户充值记录
func listTopUps(c *fiber.Ctx) error {
result := response.NewResult(c)
park := tools.EmptyToNil(c.Params("pid"))
if park == nil {
topUpLog.Error("查询符合条件的商户充值记录,未指定要访问的园区")
return result.BadRequest("未指定要访问的园区")
}
if pass, err := checkParkBelongs(*park, topUpLog, c, &result); !pass {
return err
}
keyword := tools.EmptyToNil(c.Query("keyword"))
startDate, err := types.ParseDatep(c.Query("start_date"))
if err != nil {
topUpLog.Error("查询符合条件的商户充值记录,查询起始日期格式错误", zap.Error(err))
return result.BadRequest("查询起始日期格式错误")
}
endDate, err := types.ParseDatep(c.Query("end_date"))
if err != nil {
topUpLog.Error("查询符合条件的商户充值记录,查询结束日期格式错误", zap.Error(err))
return result.BadRequest("查询结束日期格式错误")
}
page := c.QueryInt("page", 1)
topUps, total, err := repository.TopUpRepository.ListTopUps(*park, startDate, endDate, keyword, uint(page))
if err != nil {
topUpLog.Error("查询符合条件的商户充值记录,查询失败", zap.Error(err))
return result.Error(fiber.StatusInternalServerError, "商户充值记录查询不成功")
}
topUpLog.Debug("检查获取到的数据", zap.Any("topUps", topUps), zap.Int64("total", total))
topUpDetails := make([]*vo.TopUpDetailQueryResponse, 0)
copier.Copy(&topUpDetails, &topUps)
topUpLog.Debug("检查转换后的数据", zap.Any("topUpDetails", topUpDetails))
return result.Success(
"已经获取到符合条件的商户充值记录",
response.NewPagedResponse(page, total).ToMap(),
fiber.Map{"topUps": topUpDetails},
)
}
// 获取指定充值记录的详细内容
func getTopUp(c *fiber.Ctx) error {
result := response.NewResult(c)
park := tools.EmptyToNil(c.Params("pid"))
if park == nil {
topUpLog.Error("获取指定充值记录的详细内容,未指定要访问的园区")
return result.BadRequest("未指定要访问的园区")
}
if pass, err := checkParkBelongs(*park, topUpLog, c, &result); !pass {
return err
}
topUpCode := tools.EmptyToNil(c.Params("code"))
if topUpCode == nil {
topUpLog.Error("获取指定充值记录的详细内容,未指定要查询的充值记录")
return result.BadRequest("未指定要查询的充值记录")
}
topUp, err := repository.TopUpRepository.GetTopUp(*park, *topUpCode)
if err != nil {
topUpLog.Error("获取指定充值记录的详细内容,查询失败", zap.Error(err))
return result.NotFound("未找到指定的商户充值记录")
}
var topUpDetail vo.TopUpDetailQueryResponse
copier.Copy(&topUpDetail, &topUp)
return result.Success(
"已经获取到指定充值记录的详细内容",
fiber.Map{"topup": topUpDetail},
)
}
// 创建一条新的商户充值记录
func createTopUp(c *fiber.Ctx) error {
result := response.NewResult(c)
park := tools.EmptyToNil(c.Params("pid"))
if park == nil {
topUpLog.Error("创建一条新的商户充值记录,未指定要访问的园区")
return result.BadRequest("未指定要访问的园区")
}
if pass, err := checkParkBelongs(*park, topUpLog, c, &result); !pass {
return err
}
var form vo.TopUpCreationForm
if err := c.BodyParser(&form); err != nil {
topUpLog.Error("创建一条新的商户充值记录,请求体解析失败", zap.Error(err))
return result.BadRequest("请求体解析失败")
}
if err := repository.TopUpRepository.CreateTopUp(*park, &form); err != nil {
topUpLog.Error("创建一条新的商户充值记录,创建失败", zap.Error(err))
return result.NotAccept("商户充值记录创建不成功")
}
return result.Created(
"已经创建一条新的商户充值记录",
)
}
// 删除一条指定的商户充值记录
func deleteTopUp(c *fiber.Ctx) error {
result := response.NewResult(c)
park := tools.EmptyToNil(c.Params("pid"))
if park == nil {
topUpLog.Error("删除一条指定的商户充值记录,未指定要访问的园区")
return result.BadRequest("未指定要访问的园区")
}
if pass, err := checkParkBelongs(*park, topUpLog, c, &result); !pass {
return err
}
topUpCode := tools.EmptyToNil(c.Params("code"))
if topUpCode == nil {
topUpLog.Error("删除一条指定的商户充值记录,未指定要删除的充值记录")
return result.BadRequest("未指定要删除的充值记录")
}
if err := repository.TopUpRepository.DeleteTopUp(*park, *topUpCode); err != nil {
topUpLog.Error("删除一条指定的商户充值记录,删除失败", zap.Error(err))
return result.NotAccept("商户充值记录删除不成功")
}
return result.Deleted(
"已经删除一条指定的商户充值记录",
)
}

View File

@ -3,51 +3,56 @@ package controller
import (
"electricity_bill_calc/cache"
"electricity_bill_calc/exceptions"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"electricity_bill_calc/response"
"electricity_bill_calc/security"
"electricity_bill_calc/service"
"electricity_bill_calc/tools"
"fmt"
"electricity_bill_calc/vo"
"net/http"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/shopspring/decimal"
"go.uber.org/zap"
)
func InitializeUserController(router *fiber.App) {
router.Delete("/password/:uid", security.OPSAuthorize, invalidUserPassword)
router.Delete("/login", security.MustAuthenticated, logout)
router.Put("/password", resetUserPassword)
router.Get("/accounts", security.ManagementAuthorize, listPagedUser)
router.Post("/login", login)
router.Put("/account/enabled/state", security.OPSAuthorize, switchUserEnabling)
router.Post("/account", security.OPSAuthorize, createOPSAndManagementAccount)
router.Get("/account/:uid", security.MustAuthenticated, getUserDetail)
var userLog = logger.Named("Handler", "User")
func InitializeUserHandlers(router *fiber.App) {
router.Delete("/login", security.MustAuthenticated, doLogout)
router.Post("/login", doLogin)
router.Get("/account", security.OPSAuthorize, searchUsers)
router.Post("/account", security.OPSAuthorize, createOPSAccount)
router.Get("/account/:uid", security.MustAuthenticated, fetchUserInformation)
router.Put("/account/:uid", security.OPSAuthorize, modifyUserInformation)
router.Put("/account/enabled/state", security.OPSAuthorize, changeUserState)
router.Get("/expiration", security.EnterpriseAuthorize, getAccountExpiration)
router.Post("/enterprise", security.OPSAuthorize, createEnterpriseAccount)
router.Put("/account/:uid", security.OPSAuthorize, modifyAccountDetail)
router.Get("/enterprise/quick/search", security.OPSAuthorize, quickSearchEnterprise)
router.Get("/expiration", security.EnterpriseAuthorize, fetchExpiration)
router.Put("/password", resetUserPassword)
router.Delete("/password/:uid", security.OPSAuthorize, invalidUserPassword)
}
type _LoginFormData struct {
type _LoginForm struct {
Username string `json:"uname"`
Password string `json:"upass"`
Type int8 `json:"type"`
Type int16 `json:"type"`
}
func login(c *fiber.Ctx) error {
func doLogin(c *fiber.Ctx) error {
result := response.NewResult(c)
loginData := new(_LoginFormData)
loginData := new(_LoginForm)
if err := c.BodyParser(loginData); err != nil {
userLog.Error("表单解析失败!", zap.Error(err))
return result.Error(http.StatusInternalServerError, "表单解析失败。")
}
var (
session *model.Session
err error
)
userLog.Info("有用户请求登录。", zap.String("username", loginData.Username), zap.Int16("type", loginData.Type))
if loginData.Type == model.USER_TYPE_ENT {
session, err = service.UserService.ProcessEnterpriseUserLogin(loginData.Username, loginData.Password)
} else {
@ -60,74 +65,28 @@ func login(c *fiber.Ctx) error {
}
return result.Error(int(authError.Code), authError.Message)
} else {
userLog.Error("用户登录请求处理失败!", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
}
return result.LoginSuccess(session)
}
func logout(c *fiber.Ctx) error {
func doLogout(c *fiber.Ctx) error {
result := response.NewResult(c)
session := c.Locals("session")
if session == nil {
session, err := _retreiveSession(c)
if err != nil {
return result.Success("用户会话已结束。")
}
_, err := cache.ClearSession(session.(*model.Session).Token)
_, err = cache.ClearSession(session.Token)
if err != nil {
userLog.Error("用户登出处理失败!", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Success("用户已成功登出系统。")
}
func invalidUserPassword(c *fiber.Ctx) error {
result := response.NewResult(c)
targetUserId := c.Params("uid")
verifyCode, err := service.UserService.InvalidUserPassword(targetUserId)
if _, ok := err.(exceptions.NotFoundError); ok {
return result.NotFound("未找到指定用户。")
}
if _, ok := err.(exceptions.UnsuccessfulOperationError); ok {
return result.NotAccept("未能成功更新用户的密码。")
}
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Json(http.StatusAccepted, "用户密码已经失效", fiber.Map{"verify": verifyCode})
}
type _ResetPasswordFormData struct {
VerifyCode string `json:"verifyCode"`
Username string `json:"uname"`
NewPassword string `json:"newPass"`
}
func resetUserPassword(c *fiber.Ctx) error {
result := response.NewResult(c)
resetForm := new(_ResetPasswordFormData)
if err := c.BodyParser(resetForm); err != nil {
return result.UnableToParse("无法解析提交的数据。")
}
verified, err := service.UserService.VerifyUserPassword(resetForm.Username, resetForm.VerifyCode)
if _, ok := err.(exceptions.NotFoundError); ok {
return result.NotFound("指定的用户不存在。")
}
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
if !verified {
return result.Error(http.StatusUnauthorized, "验证码不正确。")
}
completed, err := service.UserService.ResetUserPassword(resetForm.Username, resetForm.NewPassword)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
if completed {
return result.Updated("用户凭据已更新。")
}
return result.NotAccept("用户凭据未能成功更新。")
}
func listPagedUser(c *fiber.Ctx) error {
func searchUsers(c *fiber.Ctx) error {
result := response.NewResult(c)
requestPage, err := strconv.Atoi(c.Query("page", "1"))
if err != nil {
@ -145,213 +104,230 @@ func listPagedUser(c *fiber.Ctx) error {
} else {
requestUserStat = &state
}
users, total, err := service.UserService.ListUserDetail(requestKeyword, requestUserType, requestUserStat, requestPage)
users, total, err := repository.UserRepository.FindUser(
&requestKeyword,
int16(requestUserType),
requestUserStat,
uint(requestPage),
)
if err != nil {
return result.NotFound(err.Error())
}
return result.Json(
http.StatusOK,
return result.Success(
"已取得符合条件的用户集合。",
response.NewPagedResponse(requestPage, total).ToMap(),
fiber.Map{"accounts": users},
)
}
type _UserStateChangeFormData struct {
UserID string `json:"uid" form:"uid"`
Enabled bool `json:"enabled" form:"enabled"`
}
func switchUserEnabling(c *fiber.Ctx) error {
result := response.NewResult(c)
switchForm := new(_UserStateChangeFormData)
if err := c.BodyParser(switchForm); err != nil {
return result.UnableToParse("无法解析提交的数据。")
}
err := service.UserService.SwitchUserState(switchForm.UserID, switchForm.Enabled)
if err != nil {
if nfErr, ok := err.(*exceptions.NotFoundError); ok {
return result.NotFound(nfErr.Message)
} else {
return result.Error(http.StatusInternalServerError, err.Error())
}
}
return result.Updated("用户状态已经更新。")
}
type _OPSAccountCreationFormData struct {
Username string `json:"username" form:"username"`
Name string `json:"name" form:"name"`
Contact *string `json:"contact" form:"contact"`
Phone *string `json:"phone" form:"phone"`
Type int `json:"type" form:"type"`
}
func createOPSAndManagementAccount(c *fiber.Ctx) error {
result := response.NewResult(c)
creationForm := new(_OPSAccountCreationFormData)
if err := c.BodyParser(creationForm); err != nil {
return result.UnableToParse("无法解析提交的数据。")
}
exists, err := service.UserService.IsUsernameExists(creationForm.Username)
if exists {
return result.Conflict("指定的用户名已经被使用了。")
}
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
newUser := new(model.User)
newUser.Username = creationForm.Username
newUser.Type = int8(creationForm.Type)
newUser.Enabled = true
newUserDetail := new(model.UserDetail)
newUserDetail.Name = &creationForm.Name
newUserDetail.Contact = creationForm.Contact
newUserDetail.Phone = creationForm.Phone
newUserDetail.UnitServiceFee = decimal.Zero
newUserDetail.ServiceExpiration, _ = model.ParseDate("2099-12-31")
verifyCode, err := service.UserService.CreateUser(newUser, newUserDetail)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
cache.AbolishRelation("user")
return result.Json(http.StatusCreated, "用户已经成功创建。", fiber.Map{"verify": verifyCode})
}
func getUserDetail(c *fiber.Ctx) error {
result := response.NewResult(c)
targetUserId := c.Params("uid")
exists, err := service.UserService.IsUserExists(targetUserId)
if !exists {
return result.NotFound("指定的用户不存在。")
}
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
userDetail, err := service.UserService.FetchUserDetail(targetUserId)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Json(http.StatusOK, "用户详细信息已获取到。", fiber.Map{"user": userDetail})
}
type _EnterpriseCreationFormData struct {
Username string `json:"username" form:"username"`
Name string `json:"name" form:"name"`
Region *string `json:"region" form:"region"`
Address *string `json:"address" form:"address"`
Contact *string `json:"contact" form:"contact"`
Phone *string `json:"phone" form:"phone"`
UnitServiceFee *string `json:"unitServiceFee" form:"unitServiceFee"`
}
func createEnterpriseAccount(c *fiber.Ctx) error {
result := response.NewResult(c)
creationForm := new(_EnterpriseCreationFormData)
if err := c.BodyParser(creationForm); err != nil {
return result.UnableToParse("无法解析提交的数据。")
}
exists, err := service.UserService.IsUsernameExists(creationForm.Username)
if exists {
return result.Conflict("指定的用户名已经被使用了。")
}
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
newUser := new(model.User)
newUser.Username = creationForm.Username
newUser.Type = model.USER_TYPE_ENT
newUser.Enabled = true
newUserDetail := new(model.UserDetail)
newUserDetail.Name = &creationForm.Name
newUserDetail.Contact = creationForm.Contact
newUserDetail.Phone = creationForm.Phone
newUserDetail.UnitServiceFee, err = decimal.NewFromString(*creationForm.UnitServiceFee)
if err != nil {
return result.BadRequest("用户月服务费无法解析。")
}
newUserDetail.ServiceExpiration = model.NewEmptyDate()
verifyCode, err := service.UserService.CreateUser(newUser, newUserDetail)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
cache.AbolishRelation("user")
return result.Json(http.StatusCreated, "用户已经成功创建。", fiber.Map{"verify": verifyCode})
}
type _AccountModificationFormData struct {
Name string `json:"name" form:"name"`
Region *string `json:"region" form:"region"`
Address *string `json:"address" form:"address"`
Contact *string `json:"contact" form:"contact"`
Phone *string `json:"phone" form:"phone"`
UnitServiceFee *string `json:"unitServiceFee" form:"unitServiceFee"`
}
func modifyAccountDetail(c *fiber.Ctx) error {
result := response.NewResult(c)
targetUserId := c.Params("uid")
modForm := new(_AccountModificationFormData)
if err := c.BodyParser(modForm); err != nil {
return result.UnableToParse("无法解析提交的数据。")
}
exists, err := service.UserService.IsUserExists(targetUserId)
if !exists {
return result.NotFound("指定的用户不存在。")
}
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
newUserInfo := new(model.UserDetail)
newUserInfo.Id = targetUserId
newUserInfo.Name = &modForm.Name
if len(modForm.Name) > 0 {
abbr := tools.PinyinAbbr(modForm.Name)
newUserInfo.Abbr = &abbr
}
newUserInfo.Region = modForm.Region
newUserInfo.Address = modForm.Address
newUserInfo.Contact = modForm.Contact
newUserInfo.Phone = modForm.Phone
newUserInfo.UnitServiceFee, err = decimal.NewFromString(*modForm.UnitServiceFee)
if err != nil {
return result.BadRequest("用户月服务费无法解析。")
}
_, err = global.DB.NewUpdate().Model(newUserInfo).
WherePK().
Column("name", "abbr", "region", "address", "contact", "phone", "unit_service_fee").
Exec(c.Context())
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
cache.AbolishRelation(fmt.Sprintf("user:%s", targetUserId))
return result.Updated("指定用户的信息已经更新。")
}
func quickSearchEnterprise(c *fiber.Ctx) error {
result := response.NewResult(c)
keyword := c.Query("keyword")
searchResult, err := service.UserService.SearchLimitUsers(keyword, 6)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Json(http.StatusOK, "已查询到存在符合条件的企业", fiber.Map{"users": searchResult})
}
func fetchExpiration(c *fiber.Ctx) error {
func getAccountExpiration(c *fiber.Ctx) error {
result := response.NewResult(c)
session, err := _retreiveSession(c)
if err != nil {
return result.Unauthorized(err.Error())
userLog.Error("未找到有效的用户会话。", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
user, err := service.UserService.FetchUserDetail(session.Uid)
userDetail, err := repository.UserRepository.FindUserDetailById(session.Uid)
if err != nil {
return result.NotFound(err.Error())
return result.NotFound("未找到指定的用户档案")
}
return result.Json(
http.StatusOK,
return result.Success(
"已经取得用户的服务期限信息",
fiber.Map{"expiration": user.ServiceExpiration.Format("2006-01-02")},
fiber.Map{"expiration": userDetail.ServiceExpiration.Format("2006-01-02")},
)
}
func createOPSAccount(c *fiber.Ctx) error {
userLog.Info("请求创建运维或监管账户。")
result := response.NewResult(c)
creationForm := new(vo.MGTAndOPSAccountCreationForm)
if err := c.BodyParser(creationForm); err != nil {
userLog.Error("表单解析失败!", zap.Error(err))
return result.UnableToParse("无法解析提交的数据。")
}
exists, err := repository.UserRepository.IsUsernameExists(creationForm.Username)
if err != nil {
userLog.Error("检查用户名是否已经被使用时发生错误!", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
if exists {
return result.Conflict("指定的用户名已经被使用了。")
}
verifyCode, err := service.UserService.CreateUserAccount(creationForm.IntoUser(), creationForm.IntoUserDetail())
if err != nil {
userLog.Error("创建用户账户时发生错误!", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Created("用户已经成功创建。", fiber.Map{"verify": verifyCode})
}
func fetchUserInformation(c *fiber.Ctx) error {
userLog.Info("请求获取用户详细信息。")
result := response.NewResult(c)
targetUserId := c.Params("uid")
exists, err := repository.UserRepository.IsUserExists(targetUserId)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
if !exists {
return result.NotFound("指定的用户不存在。")
}
userDetail, err := repository.UserRepository.FindUserDetailById(targetUserId)
if err != nil {
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Success("用户详细信息已获取到。", fiber.Map{"user": userDetail})
}
func modifyUserInformation(c *fiber.Ctx) error {
userLog.Info("请求修改用户详细信息。")
session, _ := _retreiveSession(c)
result := response.NewResult(c)
targetUserId := c.Params("uid")
exists, err := repository.UserRepository.IsUserExists(targetUserId)
if err != nil {
userLog.Error("检查用户是否存在时发生错误!", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
if !exists {
return result.NotFound("指定的用户不存在。")
}
modificationForm := new(vo.UserDetailModificationForm)
if err := c.BodyParser(modificationForm); err != nil {
userLog.Error("表单解析失败!", zap.Error(err))
return result.UnableToParse("无法解析提交的数据。")
}
userLog.Debug("用户服务费修改表单:", zap.Any("form", modificationForm))
detailFormForUpdate, err := modificationForm.IntoModificationModel()
if err != nil {
userLog.Error("用户服务费解析转换失败!", zap.Error(err))
return result.UnableToParse("无法解析提交的数据,服务费格式不正确。")
}
if ok, err := repository.UserRepository.UpdateDetail(targetUserId, *detailFormForUpdate, &session.Uid); err != nil || !ok {
userLog.Error("更新用户详细信息失败!", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Updated("指定用户的信息已经更新。")
}
func changeUserState(c *fiber.Ctx) error {
userLog.Info("请求修改用户状态。")
result := response.NewResult(c)
modificationForm := new(vo.UserStateChangeForm)
if err := c.BodyParser(modificationForm); err != nil {
userLog.Error("表单解析失败!", zap.Error(err))
return result.UnableToParse("无法解析提交的数据。")
}
exists, err := repository.UserRepository.IsUserExists(modificationForm.Uid)
if err != nil {
userLog.Error("检查用户是否存在时发生错误!", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
if !exists {
return result.NotFound("指定的用户不存在。")
}
if ok, err := repository.UserRepository.ChangeState(modificationForm.Uid, modificationForm.Enabled); err != nil || !ok {
userLog.Error("更新用户状态失败!", zap.Error(err))
return result.NotAccept("无法更新用户状态。")
}
return result.Updated("用户的状态已经更新。")
}
func createEnterpriseAccount(c *fiber.Ctx) error {
userLog.Info("请求创建企业账户。")
result := response.NewResult(c)
creationForm := new(vo.EnterpriseAccountCreationForm)
if err := c.BodyParser(creationForm); err != nil {
userLog.Error("表单解析失败!", zap.Error(err))
return result.UnableToParse("无法解析提交的数据。")
}
exists, err := repository.UserRepository.IsUsernameExists(creationForm.Username)
if err != nil {
userLog.Error("检查用户名是否已经被使用时发生错误!", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
if exists {
return result.Conflict("指定的用户名已经被使用了。")
}
userDetail, err := creationForm.IntoUserDetail()
if err != nil {
userLog.Error("转换用户详细档案时发生错误!", zap.Error(err))
return result.UnableToParse("无法解析提交的数据,服务费格式不正确。")
}
verifyCode, err := service.UserService.CreateUserAccount(creationForm.IntoUser(), userDetail)
if err != nil {
userLog.Error("创建用户账户时发生错误!", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Created("用户已经成功创建。", fiber.Map{"verify": verifyCode})
}
func quickSearchEnterprise(c *fiber.Ctx) error {
userLog.Info("请求快速查询企业账户。")
result := response.NewResult(c)
keyword := c.Query("keyword")
limit := c.QueryInt("limit", 6)
if limit < 1 {
limit = 6
}
users, err := repository.UserRepository.SearchUsersWithLimit(nil, &keyword, uint(limit))
if err != nil {
userLog.Error("查询用户时发生错误!", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Success("已查询到存在符合条件的企业", fiber.Map{"users": users})
}
func resetUserPassword(c *fiber.Ctx) error {
userLog.Info("请求重置用户密码。")
result := response.NewResult(c)
repasswordForm := new(vo.RepasswordForm)
if err := c.BodyParser(repasswordForm); err != nil {
userLog.Error("表单解析失败!", zap.Error(err))
return result.UnableToParse("无法解析提交的数据。")
}
user, err := repository.UserRepository.FindUserByUsername(repasswordForm.Username)
if err != nil {
userLog.Error("检查用户是否存在时发生错误!", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
if user == nil {
return result.NotFound("指定的用户不存在。")
}
if !service.UserService.MatchUserPassword(user.Password, repasswordForm.VerifyCode) {
return result.Unauthorized("验证码不正确。")
}
ok, err := repository.UserRepository.UpdatePassword(user.Id, repasswordForm.NewPassword, false)
if err != nil {
userLog.Error("更新用户凭据时发生错误!", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
if !ok {
return result.NotAccept("无法更新用户凭据。")
}
return result.Updated("用户凭据已经更新。")
}
func invalidUserPassword(c *fiber.Ctx) error {
userLog.Info("请求使用户凭据失效。")
result := response.NewResult(c)
uid := c.Params("uid")
exists, err := repository.UserRepository.IsUserExists(uid)
if err != nil {
userLog.Error("检查用户是否存在时发生错误!", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
if !exists {
return result.NotFound("指定的用户不存在。")
}
verifyCode := tools.RandStr(10)
ok, err := repository.UserRepository.UpdatePassword(uid, verifyCode, true)
if err != nil {
userLog.Error("更新用户凭据时发生错误!", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
if !ok {
return result.NotAccept("未能更新用户凭据。")
}
return result.Updated("用户凭据已经更新。", fiber.Map{"verify": verifyCode})
}

View File

@ -1,81 +1,19 @@
package controller
import (
"electricity_bill_calc/exceptions"
"electricity_bill_calc/response"
"electricity_bill_calc/logger"
"electricity_bill_calc/security"
"electricity_bill_calc/service"
"net/http"
"strconv"
"github.com/gofiber/fiber/v2"
)
func InitializeWithdrawController(router *fiber.App) {
router.Delete("/publicity/:pid", security.EnterpriseAuthorize, applyReportWithdraw)
router.Get("/withdraws", security.OPSAuthorize, fetchWithdrawsWaitingAutdit)
router.Put("/withdraw/:rid", security.OPSAuthorize, auditWithdraw)
}
var withdrawLog = logger.Named("Handler", "Report")
func applyReportWithdraw(c *fiber.Ctx) error {
result := response.NewResult(c)
requestReportId := c.Params("pid")
if ensure, err := ensureReportBelongs(c, &result, requestReportId); !ensure {
return err
}
deleted, err := service.WithdrawService.ApplyWithdraw(requestReportId)
if err != nil {
if nfErr, ok := err.(exceptions.NotFoundError); ok {
return result.NotFound(nfErr.Error())
} else if ioErr, ok := err.(exceptions.ImproperOperateError); ok {
return result.NotAccept(ioErr.Error())
} else {
return result.Error(http.StatusInternalServerError, err.Error())
}
}
if !deleted {
return result.Error(http.StatusInternalServerError, "未能完成公示报表的申请撤回操作。")
}
return result.Success("指定的公示报表已经申请撤回。")
func InitializewithdrawHandlers(router *fiber.App) {
router.Put("/withdraw/:rid", security.EnterpriseAuthorize, changeReportWithdraw)
}
func changeReportWithdraw(ctx *fiber.Ctx) error {
//result := response.NewResult(ctx)
//reportId := ctx.Params("rid")
return nil
func fetchWithdrawsWaitingAutdit(c *fiber.Ctx) error {
result := response.NewResult(c)
keyword := c.Query("keyword")
requestPage, err := strconv.Atoi(c.Query("page", "1"))
if err != nil {
return result.NotAccept("查询参数[page]格式不正确。")
}
reports, totalitems, err := service.WithdrawService.FetchPagedWithdrawApplies(requestPage, keyword)
if err != nil {
return result.NotFound(err.Error())
}
return result.Json(
http.StatusOK,
"已经取得符合条件的等待审核的撤回申请。",
response.NewPagedResponse(requestPage, totalitems).ToMap(),
fiber.Map{"records": reports},
)
}
type WithdrawAuditFormData struct {
Audit bool `json:"audit" form:"audit"`
}
func auditWithdraw(c *fiber.Ctx) error {
result := response.NewResult(c)
requestReportId := c.Params("rid")
formData := new(WithdrawAuditFormData)
if err := c.BodyParser(formData); err != nil {
return result.UnableToParse("无法解析提交的数据。")
}
err := service.WithdrawService.AuditWithdraw(requestReportId, formData.Audit)
if err != nil {
if nfErr, ok := err.(exceptions.NotFoundError); ok {
return result.NotFound(nfErr.Error())
} else {
return result.NotAccept(err.Error())
}
}
return result.Success("指定公示报表的撤回申请已经完成审核")
}

View File

@ -1,8 +1,8 @@
package excel
import (
"electricity_bill_calc/model"
"electricity_bill_calc/tools"
"electricity_bill_calc/types"
"encoding/json"
"errors"
"fmt"
@ -19,11 +19,10 @@ import (
type ExcelTemplateGenerator interface {
Close()
WriteTo(w io.Writer) (int64, error)
WriteMeterData(meters []model.EndUserDetail) error
}
type ColumnRecognizer struct {
Pattern []string
Pattern [][]string
Tag string
MatchIndex int
MustFill bool
@ -45,7 +44,7 @@ type ExcelAnalysisError struct {
Err AnalysisError `json:"error"`
}
func NewColumnRecognizer(tag string, patterns ...string) ColumnRecognizer {
func NewColumnRecognizer(tag string, patterns ...[]string) ColumnRecognizer {
return ColumnRecognizer{
Pattern: patterns,
Tag: tag,
@ -67,9 +66,17 @@ func (e ExcelAnalysisError) Error() string {
func (r *ColumnRecognizer) Recognize(cellValue string) bool {
matches := make([]bool, 0)
for _, p := range r.Pattern {
matches = append(matches, strings.Contains(cellValue, p))
for _, pG := range r.Pattern {
groupMatch := make([]bool, 0)
for _, p := range pG {
groupMatch = append(groupMatch, strings.Contains(cellValue, p))
}
// 这句表示在每一个匹配组中,只要有一个匹配项,就算匹配成功
matches = append(matches, lo.Reduce(groupMatch, func(acc, elem bool, index int) bool {
return acc || elem
}, false))
}
// 这句表示在尊有的匹配组中,必须全部的匹配组都完成匹配,才算匹配成功
return lo.Reduce(matches, func(acc, elem bool, index int) bool {
return acc && elem
}, true)
@ -189,6 +196,54 @@ func (a *ExcelAnalyzer[T]) Analysis(bean T) ([]T, []ExcelAnalysisError) {
} else {
actualField.SetBool(false)
}
case "types.Date":
if len(matchValue) == 0 {
actualField.Set(reflect.ValueOf(types.NewEmptyDate()))
} else {
v, err := types.ParseDate(matchValue)
if err != nil {
errs = append(errs, ExcelAnalysisError{Row: rowIndex + 1, Col: recognizer.MatchIndex + 1, Err: AnalysisError{Err: fmt.Errorf("单元格内容应为日期格式。%w", err)}})
actualField.Set(reflect.ValueOf(types.NewEmptyDate()))
} else {
actualField.Set(reflect.ValueOf(v))
}
}
case "*types.Date":
if len(matchValue) == 0 {
actualField.Set(reflect.ValueOf(nil))
} else {
v, err := types.ParseDate(matchValue)
if err != nil {
errs = append(errs, ExcelAnalysisError{Row: rowIndex + 1, Col: recognizer.MatchIndex + 1, Err: AnalysisError{Err: fmt.Errorf("单元格内容应为日期格式。%w", err)}})
actualField.Set(reflect.ValueOf(nil))
} else {
actualField.Set(reflect.ValueOf(&v))
}
}
case "types.DateTime":
if len(matchValue) == 0 {
actualField.Set(reflect.ValueOf(types.NewEmptyDateTime()))
} else {
v, err := types.ParseDateTime(matchValue)
if err != nil {
errs = append(errs, ExcelAnalysisError{Row: rowIndex + 1, Col: recognizer.MatchIndex + 1, Err: AnalysisError{Err: fmt.Errorf("单元格内容应为日期时间格式。%w", err)}})
actualField.Set(reflect.ValueOf(types.NewEmptyDateTime()))
} else {
actualField.Set(reflect.ValueOf(v))
}
}
case "*types.DateTime":
if len(matchValue) == 0 {
actualField.Set(reflect.ValueOf(nil))
} else {
v, err := types.ParseDateTime(matchValue)
if err != nil {
errs = append(errs, ExcelAnalysisError{Row: rowIndex + 1, Col: recognizer.MatchIndex + 1, Err: AnalysisError{Err: fmt.Errorf("单元格内容应为日期时间格式。%w", err)}})
actualField.Set(reflect.ValueOf(nil))
} else {
actualField.Set(reflect.ValueOf(&v))
}
}
}
}
}

View File

@ -1,21 +1,139 @@
package excel
import (
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"fmt"
"io"
"github.com/samber/lo"
"github.com/xuri/excelize/v2"
"go.uber.org/zap"
)
var meter04kVExcelRecognizers = []*ColumnRecognizer{
{Pattern: []string{"表号"}, Tag: "code", MatchIndex: -1, MustFill: true},
{Pattern: []string{"户名"}, Tag: "name", MatchIndex: -1},
{Pattern: []string{"户址"}, Tag: "address", MatchIndex: -1},
{Pattern: []string{"联系人"}, Tag: "contact", MatchIndex: -1},
{Pattern: []string{"电话"}, Tag: "phone", MatchIndex: -1},
{Pattern: []string{"倍率"}, Tag: "ratio", MatchIndex: -1, MustFill: true},
{Pattern: []string{"序号"}, Tag: "seq", MatchIndex: -1, MustFill: true},
{Pattern: []string{"公用设备"}, Tag: "public", MatchIndex: -1, MustFill: true},
var meterArchiveRecognizers = []*ColumnRecognizer{
{Pattern: [][]string{{"表号"}}, Tag: "code", MatchIndex: -1, MustFill: true},
{Pattern: [][]string{{"表址", "地址", "户址"}}, Tag: "address", MatchIndex: -1},
{Pattern: [][]string{{"类型"}}, Tag: "meterType", MatchIndex: -1, MustFill: true},
{Pattern: [][]string{{"建筑"}}, Tag: "building", MatchIndex: -1},
{Pattern: [][]string{{"楼层"}}, Tag: "onFloor", MatchIndex: -1},
{Pattern: [][]string{{"面积"}}, Tag: "area", MatchIndex: -1},
{Pattern: [][]string{{"倍率"}}, Tag: "ratio", MatchIndex: -1, MustFill: true},
{Pattern: [][]string{{"序号"}}, Tag: "seq", MatchIndex: -1, MustFill: true},
{Pattern: [][]string{{"抄表"}, {"时间", "日期"}}, Tag: "readAt", MatchIndex: -1, MustFill: true},
{Pattern: [][]string{{"有功", "表底", "底数"}, {"总"}}, Tag: "overall", MatchIndex: -1, MustFill: true},
{Pattern: [][]string{{"有功", "表底", "底数"}, {"尖"}}, Tag: "critical", MatchIndex: -1},
{Pattern: [][]string{{"有功", "表底", "底数"}, {"峰"}}, Tag: "peak", MatchIndex: -1},
{Pattern: [][]string{{"有功", "表底", "底数"}, {"平"}}, Tag: "flat", MatchIndex: -1},
{Pattern: [][]string{{"有功", "表底", "底数"}, {"谷"}}, Tag: "valley", MatchIndex: -1},
}
func NewMeterArchiveExcelAnalyzer(file io.Reader) (*ExcelAnalyzer[model.Meter04KV], error) {
return NewExcelAnalyzer[model.Meter04KV](file, meter04kVExcelRecognizers)
func NewMeterArchiveExcelAnalyzer(file io.Reader) (*ExcelAnalyzer[model.MeterImportRow], error) {
return NewExcelAnalyzer[model.MeterImportRow](file, meterArchiveRecognizers)
}
type MeterArchiveExcelTemplateGenerator struct {
file *excelize.File
log *zap.Logger
}
func NewMeterArchiveExcelTemplateGenerator() *MeterArchiveExcelTemplateGenerator {
return &MeterArchiveExcelTemplateGenerator{
file: excelize.NewFile(),
log: logger.Named("Excel", "MeterArchive"),
}
}
func (MeterArchiveExcelTemplateGenerator) titles() *[]interface{} {
return &[]interface{}{
"序号",
"表址",
"表号",
"表计类型",
"倍率",
"所在建筑",
"所在楼层",
"辖盖面积",
"抄表时间",
"有功(总)",
"有功(尖)",
"有功(峰)",
"有功(平)",
"有功(谷)",
}
}
func (g *MeterArchiveExcelTemplateGenerator) Close() {
g.file.Close()
}
func (g MeterArchiveExcelTemplateGenerator) WriteTo(w io.Writer) (int64, error) {
return g.file.WriteTo(w)
}
func (g *MeterArchiveExcelTemplateGenerator) WriteTemplateData(buildings []*model.ParkBuilding) error {
var err error
defaultSheet := g.file.GetSheetName(0)
g.log.Debug("选定默认输出表格", zap.String("sheet", defaultSheet))
err = g.file.SetColWidth(defaultSheet, "B", "I", 20)
if err != nil {
g.log.Error("未能设定长型列宽。", zap.Error(err))
return fmt.Errorf("未能设定长型列宽,%w", err)
}
err = g.file.SetColWidth(defaultSheet, "J", "N", 15)
if err != nil {
g.log.Error("未能设定短型列宽。", zap.Error(err))
return fmt.Errorf("未能设定短型列宽,%w", err)
}
err = g.file.SetSheetRow(defaultSheet, "A1", g.titles())
if err != nil {
g.log.Error("未能输出模板标题。", zap.Error(err))
return fmt.Errorf("未能输出模板标题,%w", err)
}
err = g.file.SetRowHeight(defaultSheet, 1, 20)
if err != nil {
g.log.Error("未能设定标题行高度。", zap.Error(err))
return fmt.Errorf("未能设定标题行高度,%w", err)
}
dateTimeExp := "yyyy-mm-dd hh:mm"
dateTimeColStyle, err := g.file.NewStyle(&excelize.Style{
CustomNumFmt: &dateTimeExp,
})
if err != nil {
g.log.Error("未能创建日期时间格式。", zap.Error(err))
return fmt.Errorf("未能创建日期时间格式,%w", err)
}
g.file.SetCellStyle(defaultSheet, "I2", "I1048576", dateTimeColStyle)
numExp := "0.0000"
numColStyle, err := g.file.NewStyle(&excelize.Style{
CustomNumFmt: &numExp,
})
if err != nil {
g.log.Error("未能创建抄表数字格式。", zap.Error(err))
return fmt.Errorf("未能创建抄表数字格式,%w", err)
}
g.file.SetCellStyle(defaultSheet, "J2", "N1048576", numColStyle)
meterInstallationTypeValidation := excelize.NewDataValidation(false)
meterInstallationTypeValidation.SetDropList([]string{"商户表", "公共表", "楼道表"})
meterInstallationTypeValidation.Sqref = "D2:D1048576"
err = g.file.AddDataValidation(defaultSheet, meterInstallationTypeValidation)
if err != nil {
g.log.Error("未能设定表计类型选择器。", zap.Error(err))
return fmt.Errorf("未能设定表计类型选择器,%w", err)
}
buildingValidation := excelize.NewDataValidation(true)
buildingNames := lo.Map(buildings, func(b *model.ParkBuilding, _ int) string {
return b.Name
})
buildingValidation.SetDropList(buildingNames)
buildingValidation.Sqref = "F2:F1048576"
err = g.file.AddDataValidation(defaultSheet, buildingValidation)
if err != nil {
g.log.Error("未能设定所在建筑选择器。", zap.Error(err))
return fmt.Errorf("未能设定所在建筑选择器,%w", err)
}
return nil
}

131
excel/meter_reading.go Normal file
View File

@ -0,0 +1,131 @@
package excel
import (
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/tools"
"fmt"
"io"
"github.com/xuri/excelize/v2"
"go.uber.org/zap"
)
var meterReadingsRecognizers = []*ColumnRecognizer{
{Pattern: [][]string{{"表", "表计"}, {"编号"}}, Tag: "code", MatchIndex: -1, MustFill: true},
{Pattern: [][]string{{"抄表", "结束"}, {"时间", "日期"}}, Tag: "readAt", MatchIndex: -1, MustFill: true},
{Pattern: [][]string{{"用电", "有功", "表底", "底数"}, {"总", "量"}}, Tag: "overall", MatchIndex: -1, MustFill: true},
{Pattern: [][]string{{"有功", "表底", "底数"}, {"尖"}}, Tag: "critical", MatchIndex: -1},
{Pattern: [][]string{{"有功", "表底", "底数"}, {"峰"}}, Tag: "peak", MatchIndex: -1},
{Pattern: [][]string{{"有功", "表底", "底数"}, {"谷"}}, Tag: "valley", MatchIndex: -1},
}
func NewMeterReadingsExcelAnalyzer(file io.Reader) (*ExcelAnalyzer[model.ReadingImportRow], error) {
return NewExcelAnalyzer[model.ReadingImportRow](file, meterReadingsRecognizers)
}
type MeterReadingsExcelTemplateGenerator struct {
file *excelize.File
log *zap.Logger
}
func NewMeterReadingsExcelTemplateGenerator() *MeterReadingsExcelTemplateGenerator {
return &MeterReadingsExcelTemplateGenerator{
file: excelize.NewFile(),
log: logger.Named("Excel", "MeterReadings"),
}
}
func (MeterReadingsExcelTemplateGenerator) titles() *[]interface{} {
return &[]interface{}{
"抄表序号",
"抄表时间",
"表计编号",
"表计名称",
"商户名称",
"倍率",
"有功(总)",
"有功(尖)",
"有功(峰)",
"有功(谷)",
}
}
func (g MeterReadingsExcelTemplateGenerator) Close() {
g.file.Close()
}
func (g MeterReadingsExcelTemplateGenerator) WriteTo(w io.Writer) (int64, error) {
return g.file.WriteTo(w)
}
func (g MeterReadingsExcelTemplateGenerator) WriteTemplateData(meters []*model.SimpleMeterDocument) error {
var err error
defaultSheet := g.file.GetSheetName(0)
g.log.Debug("选定默认输出表格", zap.String("sheet", defaultSheet))
err = g.file.SetColWidth(defaultSheet, "A", "E", 30)
if err != nil {
g.log.Error("未能设定长型单元格的宽度。", zap.Error(err))
return fmt.Errorf("未能设定长型单元格的宽度,%w", err)
}
err = g.file.SetColWidth(defaultSheet, "F", "F", 10)
if err != nil {
g.log.Error("未能设定倍率单元格的宽度。", zap.Error(err))
return fmt.Errorf("未能设定倍率单元格的宽度,%w", err)
}
err = g.file.SetColWidth(defaultSheet, "G", "J", 20)
if err != nil {
g.log.Error("未能设定短型单元格的宽度。", zap.Error(err))
return fmt.Errorf("未能设定短型单元格的宽度,%w", err)
}
err = g.file.SetSheetRow(defaultSheet, "A1", g.titles())
if err != nil {
g.log.Error("未能输出模板标题。", zap.Error(err))
return fmt.Errorf("未能输出模板标题,%w", err)
}
err = g.file.SetRowHeight(defaultSheet, 1, 30)
if err != nil {
g.log.Error("未能设定标题行的高度。", zap.Error(err))
return fmt.Errorf("未能设定标题行的高度,%w", err)
}
dateTimeExp := "yyyy-mm-dd hh:mm"
dateTimeColStyle, err := g.file.NewStyle(&excelize.Style{
CustomNumFmt: &dateTimeExp,
})
if err != nil {
g.log.Error("未能创建日期时间格式。", zap.Error(err))
return fmt.Errorf("未能创建日期时间格式,%w", err)
}
endCellCoord, _ := excelize.CoordinatesToCellName(2, len(meters)+1)
g.file.SetCellStyle(defaultSheet, "B2", endCellCoord, dateTimeColStyle)
numExp := "0.0000"
numColStyle, err := g.file.NewStyle(&excelize.Style{
CustomNumFmt: &numExp,
})
if err != nil {
g.log.Error("未能创建抄表数字格式。", zap.Error(err))
return fmt.Errorf("未能创建抄表数字格式,%w", err)
}
endCellCoord, _ = excelize.CoordinatesToCellName(9, len(meters)+1)
g.file.SetCellStyle(defaultSheet, "F2", endCellCoord, numColStyle)
for i, meter := range meters {
cellCoord, _ := excelize.CoordinatesToCellName(1, i+2)
ratio, _ := meter.Ratio.Float64()
if err := g.file.SetSheetRow(defaultSheet, cellCoord, &[]interface{}{
meter.Seq,
"",
meter.Code,
tools.DefaultTo(meter.Address, ""),
tools.DefaultTo(meter.TenementName, ""),
ratio,
}); err != nil {
g.log.Error("向模板写入数据出现错误。", zap.Error(err))
return fmt.Errorf("向模板写入数据出现错误,%w", err)
}
}
return err
}

27
excel/tenement.go Normal file
View File

@ -0,0 +1,27 @@
package excel
import (
"electricity_bill_calc/model"
"io"
)
var tenementRecognizers = []*ColumnRecognizer{
{Pattern: [][]string{{"商户全称"}}, Tag: "fullName", MatchIndex: -1},
{Pattern: [][]string{{"联系地址"}}, Tag: "address", MatchIndex: -1},
{Pattern: [][]string{{"入驻时间"}}, Tag: "movedInAt", MatchIndex: -1},
{Pattern: [][]string{{"商铺名称"}}, Tag: "shortName", MatchIndex: -1},
{Pattern: [][]string{{"联系人"}}, Tag: "contactName", MatchIndex: -1},
{Pattern: [][]string{{"电话"}}, Tag: "contactPhone", MatchIndex: -1},
{Pattern: [][]string{{"USCI"}}, Tag: "usci", MatchIndex: -1},
{Pattern: [][]string{{"开票地址"}}, Tag: "invoiceAddress", MatchIndex: -1},
{Pattern: [][]string{{"账号"}}, Tag: "account", MatchIndex: -1},
{Pattern: [][]string{{"开户行"}}, Tag: "bank", MatchIndex: -1},
}
func NewTenementExcelAnalyzer(file io.Reader) (*ExcelAnalyzer[model.TenementImportRow], error) {
return NewExcelAnalyzer[model.TenementImportRow](file, tenementRecognizers)
}
//func NewMeterArchiveExcelAnalyzer(file io.Reader) (*ExcelAnalyzer[model.MeterImportRow], error) {
// return NewExcelAnalyzer[model.MeterImportRow](file, meterArchiveRecognizers)
//}

View File

@ -31,5 +31,5 @@ func NewUnauthorizedError(msg string) *UnauthorizedError {
}
func (e UnauthorizedError) Error() string {
return fmt.Sprintf("Unauthorized: %s", e.Message)
return fmt.Sprintf("用户未获得授权: %s", e.Message)
}

View File

@ -15,5 +15,5 @@ func NewIllegalArgumentsError(msg string, arguments ...string) IllegalArgumentsE
}
func (e IllegalArgumentsError) Error() string {
return fmt.Sprintf("Illegal Arguments, %s", e.Message)
return fmt.Sprintf("使用了非法参数, %s", e.Message)
}

View File

@ -15,5 +15,5 @@ func NewImproperOperateError(msg string, arguments ...string) ImproperOperateErr
}
func (e ImproperOperateError) Error() string {
return fmt.Sprintf("Improper Operate, %s", e.Message)
return fmt.Sprintf("操作不恰当, %s", e.Message)
}

View File

@ -0,0 +1,16 @@
package exceptions
import "fmt"
type InsufficientDataError struct {
Field string
Message string
}
func NewInsufficientDataError(field, msg string) *InsufficientDataError {
return &InsufficientDataError{Field: field, Message: msg}
}
func (e InsufficientDataError) Error() string {
return fmt.Sprintf("字段 [%s] 数据不足,%s", e.Field, e.Message)
}

View File

@ -11,7 +11,7 @@ func NewNotFoundError(msg string) *NotFoundError {
}
func NewNotFoundErrorFromError(msg string, err error) *NotFoundError {
return &NotFoundError{Message: fmt.Sprintf("%s%v", msg, err)}
return &NotFoundError{Message: fmt.Sprintf("所需数据未找到,%s%v", msg, err)}
}
func (e NotFoundError) Error() string {

View File

@ -1,11 +1,130 @@
package exceptions
type UnsuccessfulOperationError struct{}
import (
"strings"
)
func NewUnsuccessfulOperationError() *UnsuccessfulOperationError {
return &UnsuccessfulOperationError{}
type OperationType int16
const (
OPERATE_CREATE OperationType = iota
OPERATE_UPDATE
OPERATE_DELETE
OEPRATE_QUERY
OPERATE_CALCULATE
OPERATE_DB
OPERATE_DB_TRANSACTION
OPERATE_CUSTOM OperationType = 98
OPERATE_OTHER OperationType = 99
)
type UnsuccessfulOperationError struct {
Operate OperationType
Description string
Message string
}
func (UnsuccessfulOperationError) Error() string {
return "Unsuccessful Operation"
func NewUnsuccessfulOperationError(oeprate OperationType, describe, message string) *UnsuccessfulOperationError {
return &UnsuccessfulOperationError{
Operate: oeprate,
Description: describe,
Message: message,
}
}
func NewUnsuccessCreateError(message string) *UnsuccessfulOperationError {
return &UnsuccessfulOperationError{
Operate: OPERATE_CREATE,
Message: message,
}
}
func NewUnsuccessUpdateError(message string) *UnsuccessfulOperationError {
return &UnsuccessfulOperationError{
Operate: OPERATE_UPDATE,
Message: message,
}
}
func NewUnsuccessDeleteError(message string) *UnsuccessfulOperationError {
return &UnsuccessfulOperationError{
Operate: OPERATE_DELETE,
Message: message,
}
}
func NewUnsuccessQueryError(message string) *UnsuccessfulOperationError {
return &UnsuccessfulOperationError{
Operate: OEPRATE_QUERY,
Message: message,
}
}
func NewUnsuccessCalculateError(message string) *UnsuccessfulOperationError {
return &UnsuccessfulOperationError{
Operate: OPERATE_CALCULATE,
Message: message,
}
}
func NewUnsuccessDBError(message string) *UnsuccessfulOperationError {
return &UnsuccessfulOperationError{
Operate: OPERATE_DB,
Message: message,
}
}
func NewUnsuccessDBTransactionError(message string) *UnsuccessfulOperationError {
return &UnsuccessfulOperationError{
Operate: OPERATE_DB_TRANSACTION,
Message: message,
}
}
func NewUnsuccessCustomError(describe, message string) *UnsuccessfulOperationError {
return &UnsuccessfulOperationError{
Operate: OPERATE_CUSTOM,
Description: describe,
Message: message,
}
}
func NewUnsuccessOtherError(message string) *UnsuccessfulOperationError {
return &UnsuccessfulOperationError{
Operate: OPERATE_OTHER,
Message: message,
}
}
func (e UnsuccessfulOperationError) Error() string {
var builder strings.Builder
switch e.Operate {
case OPERATE_CREATE:
builder.WriteString("创建")
case OPERATE_UPDATE:
builder.WriteString("更新")
case OPERATE_DELETE:
builder.WriteString("删除")
case OEPRATE_QUERY:
builder.WriteString("查询")
case OPERATE_CALCULATE:
builder.WriteString("计算")
case OPERATE_DB:
builder.WriteString("数据库")
case OPERATE_DB_TRANSACTION:
builder.WriteString("数据库事务")
case OPERATE_CUSTOM:
builder.WriteString(e.Description)
case OPERATE_OTHER:
builder.WriteString("其他")
default:
builder.WriteString("未知")
}
builder.WriteString("操作不成功,")
if len(e.Message) > 0 {
builder.WriteString(e.Message)
} else {
builder.WriteString("未知原因")
}
return builder.String()
}

View File

@ -1,60 +1,88 @@
package global
import (
"database/sql"
"context"
"fmt"
"time"
"electricity_bill_calc/config"
"electricity_bill_calc/logger"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
"go.uber.org/zap/zapcore"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/samber/lo"
"go.uber.org/zap"
)
var (
DB *bun.DB
DB *pgxpool.Pool
)
func SetupDatabaseConnection() error {
// connStr := fmt.Sprintf(
// "host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai connect_timeout=0 tcp_user_timeout=180000",
// config.DatabaseSettings.Host,
// config.DatabaseSettings.User,
// config.DatabaseSettings.Pass,
// config.DatabaseSettings.DB,
// config.DatabaseSettings.Port,
// )
pgconn := pgdriver.NewConnector(
pgdriver.WithNetwork("tcp"),
pgdriver.WithAddr(fmt.Sprintf("%s:%d", config.DatabaseSettings.Host,
config.DatabaseSettings.Port)),
pgdriver.WithUser(config.DatabaseSettings.User),
pgdriver.WithInsecure(true),
pgdriver.WithPassword(config.DatabaseSettings.Pass),
pgdriver.WithDatabase(config.DatabaseSettings.DB),
pgdriver.WithDialTimeout(30*time.Second),
pgdriver.WithReadTimeout(3*time.Minute),
pgdriver.WithWriteTimeout(10*time.Minute),
connString := fmt.Sprintf(
"postgres://%s:%s@%s:%d/%s?sslmode=disable&connect_timeout=%d&application_name=%s&pool_max_conns=%d&pool_min_conns=%d&pool_max_conn_lifetime=%s&pool_max_conn_idle_time=%s&pool_health_check_period=%s",
config.DatabaseSettings.User,
config.DatabaseSettings.Pass,
config.DatabaseSettings.Host,
config.DatabaseSettings.Port,
config.DatabaseSettings.DB,
0,
"elec_service_go",
config.DatabaseSettings.MaxOpenConns,
config.DatabaseSettings.MaxIdleConns,
"60m",
"10m",
"10s",
)
sqldb := sql.OpenDB(pgconn)
DB = bun.NewDB(sqldb, pgdialect.New())
DB.AddQueryHook(logger.NewQueryHook(logger.QueryHookOptions{
LogSlow: 3 * time.Second,
Logger: logger.Named("PG"),
QueryLevel: zapcore.DebugLevel,
ErrorLevel: zapcore.ErrorLevel,
SlowLevel: zapcore.WarnLevel,
ErrorTemplate: "{{.Operation}}[{{.Duration}}]: {{.Query}}: {{.Error}}",
MessageTemplate: "{{.Operation}}[{{.Duration}}]: {{.Query}}",
}))
DB.SetMaxIdleConns(config.DatabaseSettings.MaxIdleConns)
DB.SetMaxOpenConns(config.DatabaseSettings.MaxOpenConns)
DB.SetConnMaxIdleTime(10 * time.Minute)
DB.SetConnMaxLifetime(60 * time.Minute)
DB.Ping()
poolConfig, err := pgxpool.ParseConfig(connString)
if err != nil {
logger.Named("DB INIT").Error("数据库连接初始化失败。", zap.Error(err))
return err
}
poolConfig.ConnConfig.Tracer = QueryLogger{logger: logger.Named("PG")}
DB, _ = pgxpool.NewWithConfig(context.Background(), poolConfig)
return nil
}
type QueryLogger struct {
logger *zap.Logger
}
func (ql QueryLogger) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context {
ql.logger.Info(fmt.Sprintf("将要执行查询: %s", data.SQL))
ql.logger.Info("查询参数", lo.Map(data.Args, func(elem any, index int) zap.Field {
return zap.Any(fmt.Sprintf("[Arg %d]: ", index), elem)
})...)
return ctx
}
func (ql QueryLogger) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) {
var logFunc func(string, ...zap.Field)
var templateString string
if data.Err != nil {
logFunc = ql.logger.Error
templateString = "命令 [%s] 执行失败。"
} else {
logFunc = ql.logger.Info
templateString = "命令 [%s] 执行成功。"
}
switch {
case data.CommandTag.Update():
fallthrough
case data.CommandTag.Delete():
fallthrough
case data.CommandTag.Insert():
logFunc(
fmt.Sprintf(templateString, data.CommandTag.String()),
zap.Error(data.Err),
zap.Any("affected", data.CommandTag.RowsAffected()))
case data.CommandTag.Select():
logFunc(
fmt.Sprintf(templateString, data.CommandTag.String()),
zap.Error(data.Err))
default:
logFunc(
fmt.Sprintf(templateString, data.CommandTag.String()),
zap.Error(data.Err))
}
}

74
go.mod
View File

@ -4,67 +4,83 @@ go 1.19
require (
github.com/deckarep/golang-set/v2 v2.1.0
github.com/fufuok/utils v0.7.13
github.com/gofiber/fiber/v2 v2.38.1
github.com/fufuok/utils v0.10.2
github.com/georgysavva/scany/v2 v2.0.0
github.com/gofiber/fiber/v2 v2.46.0
github.com/google/uuid v1.3.0
github.com/jackc/pgx/v5 v5.3.1
github.com/jinzhu/copier v0.3.5
github.com/liamylian/jsontime/v2 v2.0.0
github.com/mozillazg/go-pinyin v0.19.0
github.com/rueian/rueidis v0.0.73
github.com/samber/lo v1.27.0
github.com/mozillazg/go-pinyin v0.20.0
github.com/rueian/rueidis v0.0.100
github.com/samber/lo v1.38.1
github.com/shopspring/decimal v1.3.1
github.com/spf13/viper v1.12.0
github.com/valyala/fasthttp v1.40.0
github.com/xuri/excelize/v2 v2.6.1
go.uber.org/zap v1.23.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
github.com/spf13/viper v1.16.0
github.com/valyala/fasthttp v1.47.0
github.com/xuri/excelize/v2 v2.7.1
go.uber.org/zap v1.24.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/klauspost/compress v1.15.0 // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/klauspost/compress v1.16.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/sync v0.2.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
mellium.im/sasl v0.3.0 // indirect
)
require (
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/doug-martin/goqu/v9 v9.18.0
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.3 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.3.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/uptrace/bun v1.1.8
github.com/uptrace/bun/dialect/pgdialect v1.1.8
github.com/uptrace/bun/driver/pgdriver v1.1.8
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.0.0-20220812174116-3211cb980234 // indirect
golang.org/x/sys v0.0.0-20220907062415-87db552b00fd // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 // indirect
github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

164
go.sum
View File

@ -39,8 +39,11 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@ -55,6 +58,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI=
github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/doug-martin/goqu/v9 v9.18.0 h1:/6bcuEtAe6nsSMVK/M+fOiXUNfyFF3yYtE07DBPFMYY=
github.com/doug-martin/goqu/v9 v9.18.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@ -62,15 +68,26 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fufuok/utils v0.7.13 h1:FGx8Mnfg0ZB8HdVz1X60JJ2kFu1rtcsFDYUxUTzNKkU=
github.com/fufuok/utils v0.7.13/go.mod h1:ztIaorPqZGdbvmW3YlwQp80K8rKJmEy6xa1KwpJSsmk=
github.com/fufuok/utils v0.10.2 h1:jXgE7yBSUW9z+sJs/VQq3o4MH+jN30PzIILVXFw73lE=
github.com/fufuok/utils v0.10.2/go.mod h1:87MJq0gAZDYBgUOpxSGoLkdv8VCuRNOL9vK02F7JC3s=
github.com/georgysavva/scany/v2 v2.0.0 h1:RGXqxDv4row7/FYoK8MRXAZXqoWF/NM+NP0q50k3DKU=
github.com/georgysavva/scany/v2 v2.0.0/go.mod h1:sigOdh+0qb/+aOs3TVhehVT10p8qJL7K/Zhyz8vWo38=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gofiber/fiber/v2 v2.38.1 h1:GEQ/Yt3Wsf2a30iTqtLXlBYJZso0JXPovt/tmj5H9jU=
github.com/gofiber/fiber/v2 v2.38.1/go.mod h1:t0NlbaXzuGH7I+7M4paE848fNWInZ7mfxI/Er1fTth8=
github.com/gofiber/fiber/v2 v2.46.0 h1:wkkWotblsGVlLjXj2dpgKQAYHtXumsK/HyFugQM68Ns=
github.com/gofiber/fiber/v2 v2.46.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -135,10 +152,20 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jackc/puddle/v2 v2.2.0 h1:RdcDk92EJBuBS55nQMMYFXTxwstHug4jkhT5pq8VxPk=
github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@ -147,17 +174,35 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/liamylian/jsontime/v2 v2.0.0 h1:3if2kDW/boymUdO+4Qj/m4uaXMBSF6np9KEgg90cwH0=
github.com/liamylian/jsontime/v2 v2.0.0/go.mod h1:UHp1oAPqCBfspokvGmaGe0IAl2IgOpgOgDaKPcvcGGY=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -171,10 +216,17 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/mozillazg/go-pinyin v0.19.0 h1:p+J8/kjJ558KPvVGYLvqBhxf8jbZA2exSLCs2uUVN8c=
github.com/mozillazg/go-pinyin v0.19.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
github.com/mozillazg/go-pinyin v0.20.0 h1:BtR3DsxpApHfKReaPO1fCqF4pThRwH9uwvXzm+GnMFQ=
github.com/mozillazg/go-pinyin v0.20.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw=
github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -187,26 +239,48 @@ github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rueian/rueidis v0.0.73 h1:+r0Z6C6HMnkquPgY3zaHVpTqmCyJL56Z36GSlyBrufk=
github.com/rueian/rueidis v0.0.73/go.mod h1:FwnfDILF2GETrvXcYFlhIiru/7NmSIm1f+7C5kutO0I=
github.com/rueian/rueidis v0.0.100 h1:22yp/+8YHuWc/vcrp8bkjeE7baD3vygoh2gZ2+xu1KQ=
github.com/rueian/rueidis v0.0.100/go.mod h1:ivvsRYRtAUcf9OnheuKc5Gpa8IebrkLT1P45Lr2jlXE=
github.com/samber/lo v1.27.0 h1:GOyDWxsblvqYobqsmUuMddPa2/mMzkKyojlXol4+LaQ=
github.com/samber/lo v1.27.0/go.mod h1:it33p9UtPMS7z72fP4gw/EIfQB2eI8ke7GR2wc6+Rhg=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -216,9 +290,18 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI=
github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/uptrace/bun v1.1.8 h1:slxuaP4LYWFbPRUmTtQhfJN+6eX/6ar2HDKYTcI50SA=
@ -231,6 +314,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.40.0 h1:CRq/00MfruPGFLTQKY8b+8SfdK60TxNztjRMnH0t1Yc=
github.com/valyala/fasthttp v1.40.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
@ -239,14 +324,21 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c=
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 h1:ge5g8vsTQclA5lXDi+PuiAFw5GMIlMHOB/5e1hsf96E=
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.6.1 h1:ICBdtw803rmhLN3zfvyEGH3cwSmZv+kde7LhTDT659k=
github.com/xuri/excelize/v2 v2.6.1/go.mod h1:tL+0m6DNwSXj/sILHbQTYsLi9IF4TW59H2EF3Yrx1AU=
github.com/xuri/excelize/v2 v2.7.1 h1:gm8q0UCAyaTt3MEF5wWMjVdmthm2EHAWesGSKS9tdVI=
github.com/xuri/excelize/v2 v2.7.1/go.mod h1:qc0+2j4TvAUrBw36ATtcTeC1VCM0fFdAXZOmcF4nTpY=
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M=
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83 h1:xVwnvkzzi+OiwhIkWOXvh1skFI6bagk8OvGuazM80Rw=
github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@ -256,22 +348,41 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -284,10 +395,14 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE=
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -309,6 +424,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -342,8 +460,17 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E=
golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -363,6 +490,11 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -401,11 +533,27 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220907062415-87db552b00fd h1:AZeIEzg+8RCELJYq8w+ODLVxFgLMMigSwO/ffKPEd9U=
golang.org/x/sys v0.0.0-20220907062415-87db552b00fd/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -415,6 +563,14 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -459,12 +615,16 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -564,8 +724,12 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View File

@ -1,8 +1,10 @@
package logger
import (
"electricity_bill_calc/types"
"os"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
@ -40,7 +42,7 @@ func init() {
logger = zap.New(core).Named("App")
sugaredLogger = logger.Sugar()
logger.Info("Logger initialized.")
logger.Info("日志系统初始化完成。")
}
func GetLogger() *zap.Logger {
@ -130,3 +132,53 @@ func With(fields ...zap.Field) *zap.Logger {
func WithSugar(fields ...zap.Field) *zap.SugaredLogger {
return logger.With(fields...).Sugar()
}
func DecimalField(key string, val decimal.Decimal) zap.Field {
return zap.String(key, val.String())
}
func DecimalFieldp(key string, val *decimal.Decimal) zap.Field {
if val == nil {
return zap.Stringp(key, nil)
}
return DecimalField(key, *val)
}
func NullDecimalField(key string, val decimal.NullDecimal) zap.Field {
if val.Valid {
return DecimalField(key, val.Decimal)
}
return zap.Stringp(key, nil)
}
func NullDecimalFieldp(key string, val *decimal.NullDecimal) zap.Field {
if val == nil {
return zap.Stringp(key, nil)
}
if val.Valid {
return DecimalField(key, val.Decimal)
}
return zap.Stringp(key, nil)
}
func DateField(key string, val types.Date) zap.Field {
return val.Log(key)
}
func DateFieldp(key string, val *types.Date) zap.Field {
if val == nil {
return zap.Stringp(key, nil)
}
return DateField(key, *val)
}
func DateTimeField(key string, val types.DateTime) zap.Field {
return val.Log(key)
}
func DateTimeFieldp(key string, val *types.DateTime) zap.Field {
if val == nil {
return zap.Stringp(key, nil)
}
return DateTimeField(key, *val)
}

View File

@ -62,9 +62,13 @@ func NewLogMiddleware(config LogMiddlewareConfig) fiber.Handler {
fields := []zap.Field{
zap.Namespace("context"),
zap.String("pid", strconv.Itoa(os.Getpid())),
zap.String("method", c.Method()),
zap.String("remote", c.IP()),
zap.Strings("forwarded", c.IPs()),
zap.String("url", c.OriginalURL()),
zap.String("time", stop.Sub(start).String()),
zap.Object("response", Resp(c.Response())),
zap.Object("request", Req(c)),
// zap.Object("response", Resp(c.Response())),
// zap.Object("request", Req(c)),
}
if u := c.Locals("userId"); u != nil {

View File

@ -1,9 +1,12 @@
package logger
import (
"fmt"
"io"
"log"
"math"
"os"
"time"
"gopkg.in/natefinch/lumberjack.v2"
)
@ -14,10 +17,11 @@ func newRollingWriter() io.Writer {
return nil
}
now := time.Now()
return &lumberjack.Logger{
Filename: "log/service.log",
MaxBackups: 366 * 10, // files
MaxSize: 200, // megabytes
MaxAge: 366 * 10, // days
Filename: fmt.Sprintf("log/service_%s.log", now.Format("2006-01-02_15")),
MaxBackups: math.MaxInt, // files
MaxSize: 200, // megabytes
MaxAge: math.MaxInt, // days
}
}

152
main.go
View File

@ -1,25 +1,18 @@
package main
import (
"database/sql"
"electricity_bill_calc/cache"
"electricity_bill_calc/config"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/migration"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"electricity_bill_calc/router"
"electricity_bill_calc/service"
"encoding/csv"
"electricity_bill_calc/types"
"fmt"
"io"
"os"
"strconv"
"time"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"github.com/uptrace/bun/migrate"
"go.uber.org/zap"
)
@ -27,145 +20,70 @@ func init() {
l := logger.Named("Init")
err := config.SetupSetting()
if err != nil {
l.Fatal("Configuration load failed.", zap.Error(err))
l.Fatal("服务配置文件加载失败!", zap.Error(err))
}
l.Info("Configuration loaded!")
l.Info("服务配置已经完成加载。")
err = global.SetupDatabaseConnection()
if err != nil {
l.Fatal("Main Database connect failed.", zap.Error(err))
}
l.Info("Main Database connected!")
migrator := migrate.NewMigrator(global.DB, migration.Migrations)
ctx, cancel := global.TimeoutContext(12)
defer cancel()
err = migrator.Init(ctx)
if err != nil {
l.Fatal("Database migration unable to initialized.", zap.Error(err))
}
group, err := migrator.Migrate(ctx)
if err != nil {
l.Fatal("Database migrate failed.", zap.Error(err))
}
if group.IsZero() {
l.Info("There are no new migrations to run (database is up to date)")
l.Fatal("主数据库连接失败!", zap.Error(err))
}
l.Info("主数据库已经连接。")
err = global.SetupRedisConnection()
if err != nil {
l.Fatal("Main Cache Database connect failed.", zap.Error(err))
l.Fatal("主缓存数据库连接失败!", zap.Error(err))
}
l.Info("Main Cache Database connected!")
err = initializeRegions()
if err != nil {
l.Fatal("Regions initialize failed.", zap.Error(err))
}
l.Info("Regions synchronized.")
l.Info("主缓存数据库已经连接。")
err = intializeSingularity()
if err != nil {
l.Fatal("Singularity account intialize failed.", zap.Error(err))
l.Fatal("奇点账号初始化失败。", zap.Error(err))
}
l.Info("Singularity account intialized.")
}
func initializeRegions() error {
ctx, cancel := global.TimeoutContext()
defer cancel()
logger.Info("Synchronize regions...")
regionCsvFile, err := os.Open("regions.csv")
if err != nil {
return fmt.Errorf("region initialize file is not found: %w", err)
}
defer regionCsvFile.Close()
var existRegions = make([]string, 0)
err = global.DB.NewSelect().Model((*model.Region)(nil)).
Column("code").
Scan(ctx, &existRegions)
if err != nil {
return fmt.Errorf("unable to retreive regions from database: %w", err)
}
regionCsv := csv.NewReader(regionCsvFile)
transaction, err := global.DB.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return fmt.Errorf("unable to intiate database transaction: %w", err)
}
for {
record, err := regionCsv.Read()
if err == io.EOF {
break
}
if lo.Contains(existRegions, record[0]) {
continue
}
level, err := strconv.Atoi(record[2])
if err != nil {
continue
}
if _, err := transaction.NewInsert().Model(&model.Region{
Code: record[0],
Name: record[1],
Level: level,
Parent: record[3],
}).Exec(ctx); err != nil {
return fmt.Errorf("region synchronize in failed: %v, %w", record, err)
}
}
if err = transaction.Commit(); err != nil {
return fmt.Errorf("synchronize regions to database failed: %w", err)
}
return nil
l.Info("奇点账号已经完成初始化。")
}
func intializeSingularity() error {
singularityExists, err := service.UserService.IsUserExists("000")
l := logger.Named("Init", "Singularity")
singularityExists, err := repository.UserRepository.IsUserExists("000")
if err != nil {
return fmt.Errorf("singularity detect failed: %w", err)
l.Error("检测奇点账号失败。", zap.Error(err))
return fmt.Errorf("检测奇点账号失败: %w", err)
}
if singularityExists {
l.Info("奇点账号已经存在,跳过剩余初始化步骤。")
return nil
}
singularity := &model.User{
Id: "000",
singularityId := "000"
singularityExpires, err := types.ParseDate("2099-12-31")
if err != nil {
l.Error("奇点用户账号过期时间解析失败。", zap.Error(err))
return fmt.Errorf("奇点用户账号过期时间解析失败: %w", err)
}
singularity := &model.ManagementAccountCreationForm{
Id: &singularityId,
Username: "singularity",
Name: "Singularity",
Type: 2,
Enabled: true,
Expires: singularityExpires,
}
singularityName := "Singularity"
singularityExpires, err := model.ParseDate("2099-12-31")
verifyCode, err := service.UserService.CreateUserAccount(
singularity.IntoUser(),
singularity.IntoUserDetail())
if err != nil {
return fmt.Errorf("singularity expires time parse failed: %w", err)
}
singularityDetail := &model.UserDetail{
Name: &singularityName,
UnitServiceFee: decimal.Zero,
ServiceExpiration: singularityExpires,
}
verifyCode, err := service.UserService.CreateUser(singularity, singularityDetail)
if err != nil {
return fmt.Errorf("singularity account failed to create: %w", err)
l.Error("创建奇点账号失败。", zap.Error(err))
return fmt.Errorf("创建奇点账号失败: %w", err)
}
logger.Info(
fmt.Sprintf("Singularity account created, use %s as verify code to reset password.", verifyCode),
zap.String("account", "singularity"),
zap.String("verifyCode", verifyCode),
fmt.Sprintf("奇点账号已经完成创建, 首次登录需要使用验证码 [%s] 重置密码。", *verifyCode),
zap.String("账号名称", "singularity"),
zap.String("验证码", *verifyCode),
)
return nil
}
func DBConnectionKeepLive() {
for range time.Tick(30 * time.Second) {
err := global.DB.Ping()
if err != nil {
continue
}
}
}
// 清理Redis缓存中的孤儿键。
func RedisOrphanCleanup() {
cleanLogger := logger.Named("Cache").With(zap.String("function", "Cleanup"))
for range time.Tick(2 * time.Minute) {
@ -179,8 +97,6 @@ func RedisOrphanCleanup() {
}
func main() {
// 本次停用检测的原因是使用Ping来保持数据库链接看起来没有什么用处。
// go DBConnectionKeepLive()
go RedisOrphanCleanup()
app := router.App()
app.Listen(fmt.Sprintf(":%d", config.ServerSettings.HttpPort))

View File

@ -0,0 +1,92 @@
package calculate
import (
"electricity_bill_calc/model"
"electricity_bill_calc/types"
"github.com/shopspring/decimal"
)
type Reading struct {
ReadAt types.DateTime
Ratio decimal.Decimal
Overall decimal.Decimal
Critical decimal.Decimal
Peak decimal.Decimal
Flat decimal.Decimal
Valley decimal.Decimal
}
type Pooling struct {
Code string
Detail model.ConsumptionUnit
}
type Meter struct {
Code string
Detail model.MeterDetail
CoveredArea decimal.Decimal
LastTermReading *Reading
CurrentTermReading *Reading
Overall model.ConsumptionUnit
Critical model.ConsumptionUnit
Peak model.ConsumptionUnit
Flat model.ConsumptionUnit
Valley model.ConsumptionUnit
AdjustLoss model.ConsumptionUnit
PooledBasic model.ConsumptionUnit
PooledAdjust model.ConsumptionUnit
PooledLoss model.ConsumptionUnit
PooledPublic model.ConsumptionUnit
SharedPoolingProportion decimal.Decimal
Poolings []*Pooling
}
type TenementCharge struct {
Tenement string
Overall model.ConsumptionUnit
Critical model.ConsumptionUnit
Peak model.ConsumptionUnit
Flat model.ConsumptionUnit
Valley model.ConsumptionUnit
BasicFee decimal.Decimal
AdjustFee decimal.Decimal
LossPooled decimal.Decimal
PublicPooled decimal.Decimal
FinalCharges decimal.Decimal
Submeters []*Meter
Poolings []*Meter
}
type Summary struct {
ReportId string
OverallArea decimal.Decimal
Overall model.ConsumptionUnit
ConsumptionFee decimal.Decimal
Critical model.ConsumptionUnit
Peak model.ConsumptionUnit
Flat model.ConsumptionUnit
Valley model.ConsumptionUnit
Loss decimal.Decimal
LossFee decimal.Decimal
LossProportion decimal.Decimal
AuthoizeLoss model.ConsumptionUnit
BasicFee decimal.Decimal
BasicPooledPriceConsumption decimal.Decimal
BasicPooledPriceArea decimal.Decimal
AdjustFee decimal.Decimal
AdjustPooledPriceConsumption decimal.Decimal
AdjustPooledPriceArea decimal.Decimal
LossDilutedPrice decimal.Decimal
TotalConsumption decimal.Decimal
FinalDilutedOverall decimal.Decimal
}
type PoolingSummary struct {
Tenement string
Meter string
TargetMeter string
Area decimal.NullDecimal
OverallAmount decimal.Decimal
PoolingProportion decimal.Decimal
}

32
model/charge.go Normal file
View File

@ -0,0 +1,32 @@
package model
import (
"electricity_bill_calc/types"
"github.com/shopspring/decimal"
)
type UserChargeDetail struct {
Seq int64 `json:"seq"`
UserId string `json:"userId" db:"user_id"`
Name string `json:"name"`
Fee *float64 `json:"fee"`
Discount *float64 `json:"discount"`
Amount *float64 `json:"amount"`
ChargeTo types.Date `json:"chargeTo"`
Settled bool `json:"settled"`
SettledAt *types.DateTime `json:"settledAt"`
Cancelled bool `json:"cancelled"`
CancelledAt *types.DateTime `json:"cancelledAt"`
Refunded bool `json:"refunded"`
RefundedAt *types.DateTime `json:"refundedAt"`
CreatedAt types.DateTime `json:"createdAt"`
}
type ChargeRecordCreationForm struct {
UserId string `json:"userId"`
Fee decimal.NullDecimal `json:"fee"`
Discount decimal.NullDecimal `json:"discount"`
Amount decimal.NullDecimal `json:"amount"`
ChargeTo types.Date `json:"chargeTo"`
}

10
model/cunsumption.go Normal file
View File

@ -0,0 +1,10 @@
package model
import "github.com/shopspring/decimal"
type ConsumptionUnit struct {
Amount decimal.Decimal `json:"amount"`
Fee decimal.Decimal `json:"fee"`
Price decimal.Decimal `json:"price"`
Proportion decimal.Decimal `json:"proportion"`
}

90
model/enums.go Normal file
View File

@ -0,0 +1,90 @@
package model
import (
"fmt"
"strings"
)
const (
ELECTRICITY_CATE_TWO_PART int16 = iota
ELECTRICITY_CATE_UNITARY_PV
ELECTRICITY_CATE_FULL_PV
)
const (
METER_TYPE_UNITARY int16 = iota
METER_TYPE_PV
)
const (
METER_INSTALLATION_TENEMENT int16 = iota
METER_INSTALLATION_PARK
METER_INSTALLATION_POOLING
)
func ParseMeterInstallationType(s string) (int16, error) {
switch {
case strings.Contains(s, "商户"):
return METER_INSTALLATION_TENEMENT, nil
case strings.Contains(s, "公共"):
return METER_INSTALLATION_PARK, nil
case strings.Contains(s, "楼道"):
return METER_INSTALLATION_POOLING, nil
default:
return -1, fmt.Errorf("提供了一个无法识别的表计类型: %s", s)
}
}
const (
PRICING_POLICY_CONSUMPTION int16 = iota
PRICING_POLICY_ALL
)
const (
POOLING_MODE_NONE int16 = iota
POOLING_MODE_CONSUMPTION
POOLING_MODE_AREA
)
const (
PAYMENT_CASH int16 = iota
PAYMENT_BANK_CARD
PAYMENT_ALIPAY
PAYMENT_WECHAT
PAYMENT_UNION_PAY
PAYMENT_OTHER int16 = 99
)
const (
METER_TELEMETER_HYBRID int16 = iota
METER_TELEMETER_AUTOMATIC
METER_TELEMETER_MANUAL
)
const (
RETRY_INTERVAL_ALGORITHM_EXPONENTIAL_BACKOFF int16 = iota
RETRY_INTERVAL_ALGORITHM_DOUBLE_LINEAR_BACKOFF
RETRY_INTERVAL_ALGORITHM_TRIPLE_LINEAR_BACKOFF
RETRY_INTERVAL_ALGORITHM_FIXED
)
const (
TAX_METHOD_INCLUSIVE int16 = iota
TAX_METHOD_EXCLUSIVE
)
const (
REPORT_CALCULATE_TASK_STATUS_PENDING int16 = iota
REPORT_CALCULATE_TASK_STATUS_SUCCESS
REPORT_CALCULATE_TASK_STATUS_INSUFICIENT_DATA
REPORT_CALCULATE_TASK_STATUS_SUSPENDED
REPORT_CALCULATE_TASK_STATUS_UNKNOWN_ERROR
REPORT_CALCULATE_TASK_STATUS_UNEXISTS = 99
)
const (
REPORT_WITHDRAW_NON int16 = iota
REPORT_WITHDRAW_APPLYING
REPORT_WITHDRAW_DENIED
REPORT_WITHDRAW_GRANTED
)

45
model/invoice.go Normal file
View File

@ -0,0 +1,45 @@
package model
import (
"electricity_bill_calc/tools"
"electricity_bill_calc/types"
"github.com/shopspring/decimal"
)
type InvoiceTitle struct {
Name string `json:"name"`
USCI string `json:"usci"`
Address string `json:"address"`
Phone string `json:"phone"`
Bank string `json:"bank"`
Account string `json:"account"`
}
type InvoiceCargo struct {
Name string `json:"name"`
Price decimal.Decimal `json:"price"`
Unit string `json:"unit"`
Quantity decimal.Decimal `json:"quantity"`
TaxRate decimal.Decimal `json:"taxRate"`
Tax decimal.Decimal `json:"tax"`
Total decimal.Decimal `json:"total"`
}
type Invoice struct {
InvoiceNo string `json:"invoiceNo"`
Park string `json:"parkId" db:"park_id"`
Tenement string `json:"tenementId" db:"tenement_id"`
InvoiceType *string `json:"type" db:"type"`
Info InvoiceTitle `json:"invoiceInfo" db:"invoice_info"`
Cargos []InvoiceCargo `json:"cargos"`
TaxRate decimal.Decimal `json:"taxRate" db:"tax_rate"`
TaxMethod int16 `json:"taxMethod" db:"tax_method"`
Total decimal.Decimal `json:"total" db:"total"`
IssuedAt types.DateTime `json:"issuedAt" db:"issued_at"`
Covers []string `json:"covers"`
}
func (i Invoice) Type() string {
return tools.DefaultOrEmptyStr(i.InvoiceType, "")
}

106
model/meter.go Normal file
View File

@ -0,0 +1,106 @@
package model
import (
"electricity_bill_calc/types"
"github.com/shopspring/decimal"
)
type MeterDetail struct {
Code string `json:"code" db:"code"`
Park string `json:"parkId" db:"park_id"`
Address *string `json:"address" db:"address"`
MeterType int16 `json:"type" db:"meter_type"`
Building *string `json:"building" db:"building"`
BuildingName *string `json:"buildingName" db:"building_name"`
OnFloor *string `json:"onFloor" db:"on_floor" `
Area decimal.NullDecimal `json:"area" db:"area"`
Ratio decimal.Decimal `json:"ratio" db:"ratio"`
Seq int64 `json:"seq" db:"seq"`
Enabled bool `json:"enabled" db:"enabled"`
AttachedAt *types.DateTime `json:"attachedAt" db:"attached_at"`
DetachedAt *types.DateTime `json:"detachedAt" db:"detached_at"`
CreatedAt types.DateTime `json:"createdAt" db:"created_at"`
LastModifiedAt types.DateTime `json:"lastModifiedAt" db:"last_modified_at"`
}
type MeterRelation struct {
Id string `json:"id"`
Park string `json:"parkId" db:"park_id"`
MasterMeter string `json:"masterMeterId" db:"master_meter_id"`
SlaveMeter string `json:"slaveMeterId" db:"slave_meter_id"`
EstablishedAt types.DateTime `json:"establishedAt"`
SuspendedAt *types.DateTime `json:"suspendedAt"`
RevokedAt *types.DateTime `json:"revokedAt"`
}
type MeterSynchronization struct {
Park string `json:"parkId" db:"park_id"`
Meter string `json:"meterId" db:"meter_id"`
ForeignMeter string `json:"foreignMeter"`
SystemType string `json:"systemType"`
SystemIdentity string `json:"systemIdentity"`
Enabled bool `json:"enabled"`
LastSynchronizedAt types.DateTime `json:"lastSynchronizedAt" db:"last_synchronized_at"`
RevokeAt *types.DateTime `json:"revokeAt" db:"revoke_at"`
}
type SimpleMeterDocument struct {
Code string `json:"code"`
Seq int64 `json:"seq"`
Address *string `json:"address"`
Ratio decimal.Decimal `json:"ratio"`
TenementName *string `json:"tenementName"`
}
type NestedMeter struct {
MeterId string `json:"meterId"`
MeterDetail MeterDetail `json:"meterDetail"`
LastTermReadings Reading `json:"lastTermReadings"`
CurrentTermReadings Reading `json:"currentTermReadings"`
Overall ConsumptionUnit `json:"overall"`
Critical ConsumptionUnit `json:"critical"`
Peak ConsumptionUnit `json:"peak"`
Flat ConsumptionUnit `json:"flat"`
Valley ConsumptionUnit `json:"valley"`
BasicPooled decimal.Decimal `json:"basicPooled"`
AdjustPooled decimal.Decimal `json:"adjustPooled"`
LossPooled decimal.Decimal `json:"lossPooled"`
PublicPooled decimal.Decimal `json:"publicPooled"`
FinalTotal decimal.Decimal `json:"finalTotal"`
Area decimal.Decimal `json:"area"`
Proportion decimal.Decimal `json:"proportion"`
}
type PooledMeterDetailCompound struct {
MeterDetail
BindMeters []MeterDetail `json:"bindedMeters"`
}
// 以下结构体用于导入表计档案数据
type MeterImportRow struct {
Code string `json:"code" excel:"code"`
Address *string `json:"address" excel:"address"`
MeterType *string `json:"meterType" excel:"meterType"`
Building *string `json:"building" excel:"building"`
OnFloor *string `json:"onFloor" excel:"onFloor"`
Area decimal.NullDecimal `json:"area" excel:"area"`
Ratio decimal.Decimal `json:"ratio" excel:"ratio"`
Seq int64 `json:"seq" excel:"seq"`
ReadAt types.DateTime `json:"readAt" excel:"readAt"`
Overall decimal.Decimal `json:"overall" excel:"overall"`
Critical decimal.NullDecimal `json:"critical" excel:"critical"`
Peak decimal.NullDecimal `json:"peak" excel:"peak"`
Flat decimal.NullDecimal `json:"flat" excel:"flat"`
Valley decimal.NullDecimal `json:"valley" excel:"valley"`
}
// 以下结构体用于导入表计抄表数据
type ReadingImportRow struct {
Code string `json:"code" excel:"code"`
ReadAt types.DateTime `json:"readAt" excel:"readAt"`
Overall decimal.Decimal `json:"overall" excel:"overall"`
Critical decimal.NullDecimal `json:"critical" excel:"critical"`
Peak decimal.NullDecimal `json:"peak" excel:"peak"`
Valley decimal.NullDecimal `json:"valley" excel:"valley"`
}

View File

@ -1,89 +1,33 @@
package model
import (
"context"
"time"
"github.com/jinzhu/copier"
"github.com/shopspring/decimal"
"github.com/uptrace/bun"
)
const (
CATEGORY_TWO_PART int8 = iota
CATEGORY_SINGLE_PV
CATEGORY_SINGLE_NON_PV
)
const (
CUSTOMER_METER_NON_PV int8 = iota
CUSTOMER_METER_PV
)
type Park struct {
bun.BaseModel `bun:"table:park,alias:p"`
CreatedAndModified `bun:"extend"`
Deleted `bun:"extend"`
Id string `bun:",pk,notnull" json:"id"`
UserId string `bun:",notnull" json:"userId"`
Name string `bun:",notnull" json:"name"`
Abbr *string `json:"abbr"`
Area decimal.NullDecimal `bun:"type:numeric" json:"area"`
TenementQuantity decimal.NullDecimal `bun:"type:numeric" json:"tenement"`
Capacity decimal.NullDecimal `bun:"type:numeric" json:"capacity"`
Category int8 `bun:"type:smallint,notnull" json:"category"`
SubmeterType int8 `bun:"meter_04kv_type,type:smallint,notnull" json:"meter04kvType"`
Region *string `json:"region"`
Address *string `json:"address"`
Contact *string `json:"contact"`
Phone *string `json:"phone"`
Enabled bool `bun:",notnull" json:"enabled"`
EnterpriseIndex *User `bun:"rel:belongs-to,join:user_id=id" json:"-"`
Enterprise *UserDetail `bun:"rel:belongs-to,join:user_id=id" json:"-"`
MaintenanceFees []*MaintenanceFee `bun:"rel:has-many,join:id=park_id" json:"-"`
Meters []*Meter04KV `bun:"rel:has-many,join:id=park_id" json:"-"`
Reports []*Report `bun:"rel:has-many,join:id=park_id" json:"-"`
}
type ParkSimplified struct {
bun.BaseModel `bun:"table:park,alias:p"`
Id string `bun:",pk,notnull" json:"id"`
UserId string `bun:",notnull" json:"userId"`
Name string `bun:",notnull" json:"name"`
Abbr *string `json:"abbr"`
Id string `json:"id"`
UserId string `json:"userId"`
Name string `json:"name"`
Abbr string `json:"-"`
Area decimal.NullDecimal `json:"area"`
TenementQuantity decimal.NullDecimal `json:"tenement"`
Capacity decimal.NullDecimal `json:"capacity"`
Category int8 `bun:"type:smallint,notnull" json:"category"`
SubmeterType int8 `bun:"meter_04kv_type,type:smallint,notnull" json:"meter04kvType"`
Category int16 `json:"category"`
MeterType int16 `json:"meter04kvType" db:"meter_04kv_type"`
PricePolicy int16 `json:"pricePolicy"`
BasicPooled int16 `json:"basicDiluted"`
AdjustPooled int16 `json:"adjustDiluted"`
LossPooled int16 `json:"lossDiluted"`
PublicPooled int16 `json:"publicDiluted"`
TaxRate decimal.NullDecimal `json:"taxRate"`
Region *string `json:"region"`
Address *string `json:"address"`
Contact *string `json:"contact"`
Phone *string `json:"phone"`
}
type ParkPeriodStatistics struct {
Id string `bun:"park__id,notnull" json:"id"`
Name string `bun:"park__name,notnull" json:"name"`
Period *Date `bun:"type:date" json:"period"`
}
func FromPark(park Park) ParkSimplified {
dest := ParkSimplified{}
copier.Copy(&dest, park)
return dest
}
var _ bun.BeforeAppendModelHook = (*Park)(nil)
func (p *Park) BeforeAppendModel(ctx context.Context, query bun.Query) error {
oprTime := time.Now()
switch query.(type) {
case *bun.InsertQuery:
p.CreatedAt = oprTime
p.LastModifiedAt = &oprTime
case *bun.UpdateQuery:
p.LastModifiedAt = &oprTime
}
return nil
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"createdAt"`
LastModifiedAt time.Time `json:"lastModifiedAt"`
DeletedAt *time.Time `json:"deletedAt"`
}

14
model/park_building.go Normal file
View File

@ -0,0 +1,14 @@
package model
import "time"
type ParkBuilding struct {
Id string `json:"id"`
Park string `json:"parkId" db:"park_id"`
Name string `json:"name"`
Floors *string `json:"floors"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"createdAt"`
LastModifiedAt time.Time `json:"lastModifiedAt"`
DeletedAt *time.Time `json:"deletedAt"`
}

56
model/reading.go Normal file
View File

@ -0,0 +1,56 @@
package model
import (
"electricity_bill_calc/types"
"github.com/shopspring/decimal"
)
type Reading struct {
Ratio decimal.Decimal `json:"ratio"`
Overall decimal.Decimal `json:"overall"`
Critical decimal.Decimal `json:"critical"`
Peak decimal.Decimal `json:"peak"`
Flat decimal.Decimal `json:"flat"`
Valley decimal.Decimal `json:"valley"`
}
func NewPVReading(ratio, overall, critical, peak, flat, valley decimal.Decimal) *Reading {
return &Reading{
Ratio: ratio,
Overall: overall,
Critical: critical,
Peak: peak,
Flat: flat,
Valley: valley,
}
}
func NewUnitaryReading(ratio, overall decimal.Decimal) *Reading {
return &Reading{
Ratio: ratio,
Overall: overall,
Critical: decimal.Zero,
Peak: decimal.Zero,
Flat: overall,
Valley: decimal.Zero,
}
}
type MeterReading struct {
ReadAt types.DateTime `json:"readAt"`
Park string `json:"parkId" db:"park_id"`
Meter string `json:"meterId" db:"meter_id"`
MeterType int16 `json:"meterType"`
Ratio decimal.Decimal `json:"ratio"`
Overall decimal.Decimal `json:"overall"`
Critical decimal.Decimal `json:"critical"`
Peak decimal.Decimal `json:"peak"`
Flat decimal.Decimal `json:"flat"`
Valley decimal.Decimal `json:"valley"`
}
type DetailedMeterReading struct {
Detail MeterDetail `json:"detail"`
Reading MeterReading `json:"reading"`
}

View File

@ -1,11 +1,8 @@
package model
import "github.com/uptrace/bun"
type Region struct {
bun.BaseModel `bun:"table:region,alias:r"`
Code string `bun:",pk,notnull" json:"code"`
Name string `bun:",notnull" json:"name"`
Level int `bun:",notnull" json:"level"`
Parent string `bun:",notnull" json:"parent"`
Code string `json:"code"`
Name string `json:"name"`
Level int32 `json:"level"`
Parent string `json:"parent"`
}

View File

@ -1,99 +1,137 @@
package model
import (
"context"
"time"
"electricity_bill_calc/types"
"github.com/uptrace/bun"
"github.com/shopspring/decimal"
)
const (
REPORT_NOT_WITHDRAW int8 = iota
REPORT_WITHDRAW_APPLIED
REPORT_WITHDRAW_DENIED
REPORT_WITHDRAW_GRANTED
)
type Report struct {
bun.BaseModel `bun:"table:report,alias:r"`
CreatedAndModified `bun:"extend"`
Id string `bun:",pk,notnull" json:"id"`
ParkId string `bun:",notnull" json:"parkId"`
Period time.Time `bun:"type:date,notnull" json:"period" time_format:"simple_date" time_location:"shanghai"`
Category int8 `bun:"type:smallint,notnull" json:"category"`
SubmeterType int8 `bun:"meter_04kv_type,type:smallint,notnull" json:"meter04kvType"`
StepState Steps `bun:"type:jsonb,notnull" json:"stepState"`
Published bool `bun:",notnull" json:"published"`
PublishedAt *time.Time `bun:"type:timestamptz,nullzero" json:"publishedAt" time_format:"simple_datetime" time_location:"shanghai"`
Withdraw int8 `bun:"type:smallint,notnull" json:"withdraw"`
LastWithdrawAppliedAt *time.Time `bun:"type:timestamptz,nullzero" json:"lastWithdrawAppliedAt" time_format:"simple_datetime" time_location:"shanghai"`
LastWithdrawAuditAt *time.Time `bun:"type:timestamptz,nullzero" json:"lastWithdrawAuditAt" time_format:"simple_datetime" time_location:"shanghai"`
Park *Park `bun:"rel:belongs-to,join:park_id=id" json:"-"`
Summary *ReportSummary `bun:"rel:has-one,join:id=report_id" json:"-"`
WillDilutedFees []*WillDilutedFee `bun:"rel:has-many,join:id=report_id" json:"-"`
EndUsers []*EndUserDetail `bun:"rel:has-many,join:id=report_id,join:park_id=park_id" json:"-"`
type ReportIndex struct {
Id string `json:"id"`
Park string `json:"parkId" db:"park_id"`
Period types.DateRange `json:"period"`
Category int16 `json:"category"`
MeterType int16 `json:"meter04kvType" db:"meter_04kv_type"`
PricePolicy int16 `json:"pricePolicy"`
BasisPooled int16 `json:"basisPooled"`
AdjustPooled int16 `json:"adjustPooled"`
LossPooled int16 `json:"lossPooled"`
PublicPooled int16 `json:"publicPooled"`
Published bool `json:"published"`
PublishedAt *types.DateTime `json:"publishedAt" db:"published_at"`
Withdraw int16 `json:"withdraw"`
LastWithdrawAppliedAt *types.DateTime `json:"lastWithdrawAppliedAt" db:"last_withdraw_applied_at"`
LastWithdrawAuditAt *types.DateTime `json:"lastWithdrawAuditAt" db:"last_withdraw_audit_at"`
Status *int16 `json:"status"`
Message *string `json:"message"`
CreatedAt types.DateTime `json:"createdAt" db:"created_at"`
LastModifiedAt types.DateTime `json:"lastModifiedAt" db:"last_modified_at"`
}
type Steps struct {
Summary bool `json:"summary"`
WillDiluted bool `json:"willDiluted"`
Submeter bool `json:"submeter"`
Calculate bool `json:"calculate"`
Preview bool `json:"preview"`
Publish bool `json:"publish"`
type ReportSummary struct {
ReportId string `json:"reportId" db:"report_id"`
OverallArea decimal.Decimal `json:"overallArea" db:"overall_area"`
Overall ConsumptionUnit `json:"overall"`
ConsumptionFee decimal.NullDecimal `json:"consumptionFee" db:"consumption_fee"`
Critical ConsumptionUnit `json:"critical"`
Peak ConsumptionUnit `json:"peak"`
Flat ConsumptionUnit `json:"flat"`
Valley ConsumptionUnit `json:"valley"`
Loss decimal.NullDecimal `json:"loss"`
LossFee decimal.NullDecimal `json:"lossFee" db:"loss_fee"`
LossProportion decimal.NullDecimal `json:"lossProportion" db:"loss_proportion"`
AuthorizeLoss *ConsumptionUnit `json:"authorizeLoss" db:"authorize_loss"`
BasicFee decimal.Decimal `json:"basicFee" db:"basic_fee"`
BasicPooledPriceConsumption decimal.NullDecimal `json:"basicPooledPriceConsumption" db:"basic_pooled_price_consumption"`
BasicPooledPriceArea decimal.NullDecimal `json:"basicPooledPriceArea" db:"basic_pooled_price_area"`
AdjustFee decimal.Decimal `json:"adjustFee" db:"adjust_fee"`
AdjustPooledPriceConsumption decimal.NullDecimal `json:"adjustPooledPriceConsumption" db:"adjust_pooled_price_consumption"`
AdjustPooledPriceArea decimal.NullDecimal `json:"adjustPooledPriceArea" db:"adjust_pooled_price_area"`
LossDilutedPrice decimal.NullDecimal `json:"lossDilutedPrice" db:"loss_diluted_price"`
TotalConsumption decimal.Decimal `json:"totalConsumption" db:"total_consumption"`
FinalDilutedOverall decimal.NullDecimal `json:"finalDilutedOverall" db:"final_diluted_overall"`
}
func NewSteps() Steps {
return Steps{
Summary: false,
WillDiluted: false,
Submeter: false,
Calculate: false,
Preview: false,
Publish: false,
func (rs ReportSummary) GetConsumptionFee() decimal.Decimal {
if !rs.ConsumptionFee.Valid {
return rs.Overall.Fee.Sub(rs.BasicFee).Sub(rs.AdjustFee)
}
return rs.ConsumptionFee.Decimal
}
type ParkNewestReport struct {
Park Park `bun:"extends" json:"park"`
Report *Report `bun:"extends" json:"report"`
type ReportPublicConsumption struct {
ReportId string `json:"reportId" db:"report_id"`
MeterId string `json:"parkMeterId" db:"park_meter_id"`
Overall ConsumptionUnit `json:"overall"`
Critical ConsumptionUnit `json:"critical"`
Peak ConsumptionUnit `json:"peak"`
Flat ConsumptionUnit `json:"flat"`
Valley ConsumptionUnit `json:"valley"`
LossAdjust ConsumptionUnit `json:"lossAdjust"`
ConsumptionTotal decimal.Decimal `json:"consumptionTotal" db:"consumption_total"`
LossAdjustTotal decimal.Decimal `json:"lossAdjustTotal" db:"loss_adjust_total"`
FinalTotal decimal.Decimal `json:"finalTotal" db:"final_total"`
PublicPooled int16 `json:"publicPooled" db:"public_pooled"`
}
func (p *ParkNewestReport) AfterLoad() {
if p.Report != nil && len(p.Report.Id) == 0 {
p.Report = nil
}
type ReportDetailedPublicConsumption struct {
MeterDetail
ReportPublicConsumption
}
type ReportIndexSimplified struct {
bun.BaseModel `bun:"table:report,alias:r"`
Id string `bun:",pk,notnull" json:"id"`
ParkId string `bun:",notnull" json:"parkId"`
Period Date `bun:"type:date,notnull" json:"period"`
StepState Steps `bun:"type:jsonb,notnull" json:"stepState"`
Published bool `bun:",notnull" json:"published"`
PublishedAt *time.Time `bun:"type:timestampz" json:"publishedAt" time_format:"simple_datetime" time_location:"shanghai"`
Withdraw int8 `bun:"type:smallint,notnull" json:"withdraw"`
LastWithdrawAppliedAt *time.Time `bun:"type:timestamptz" json:"lastWithdrawAppliedAt" time_format:"simple_datetime" time_location:"shanghai"`
LastWithdrawAuditAt *time.Time `bun:"type:timestamptz" json:"lastWithdrawAuditAt" time_format:"simple_datetime" time_location:"shanghai"`
type ReportPooledConsumption struct {
ReportId string `json:"reportId" db:"report_id"`
MeterId string `json:"pooledMeterId" db:"pooled_meter_id"`
Overall ConsumptionUnit `json:"overall"`
Critical ConsumptionUnit `json:"critical"`
Peak ConsumptionUnit `json:"peak"`
Flat ConsumptionUnit `json:"flat"`
Valley ConsumptionUnit `json:"valley"`
PooledArea decimal.Decimal `json:"pooledArea" db:"pooled_area"`
Diluted []NestedMeter `json:"diluted"`
}
type JoinedReportForWithdraw struct {
Report Report `bun:"extends" json:"report"`
Park ParkSimplified `bun:"extends" json:"park"`
User UserDetailSimplified `bun:"extends" json:"user"`
type ReportDetailedPooledConsumption struct {
MeterDetail
ReportPooledConsumption
PublicPooled int16 `json:"publicPooled"`
}
var _ bun.BeforeAppendModelHook = (*Report)(nil)
func (p *Report) BeforeAppendModel(ctx context.Context, query bun.Query) error {
oprTime := time.Now()
switch query.(type) {
case *bun.InsertQuery:
p.CreatedAt = oprTime
p.LastModifiedAt = &oprTime
case *bun.UpdateQuery:
p.LastModifiedAt = &oprTime
}
return nil
type ReportDetailNestedMeterConsumption struct {
Meter MeterDetail `json:"meter"`
Consumption NestedMeter `json:"consumption"`
}
type ReportTenement struct {
ReportId string `json:"reportId" db:"report_id"`
Tenement string `json:"tenementId" db:"tenement_id"`
Detail Tenement `json:"tenementDetail" db:"tenement_detail"`
Period types.DateRange `json:"calcPeriod" db:"calc_period"`
Overall ConsumptionUnit `json:"overall"`
Critical ConsumptionUnit `json:"critical"`
Peak ConsumptionUnit `json:"peak"`
Flat ConsumptionUnit `json:"flat"`
Valley ConsumptionUnit `json:"valley"`
BasicFeePooled decimal.Decimal `json:"basicFeePooled" db:"basic_fee_pooled"`
AdjustFeePooled decimal.Decimal `json:"adjustFeePooled" db:"adjust_fee_pooled"`
LossFeePooled decimal.Decimal `json:"lossFeePooled" db:"loss_fee_pooled"`
FinalPooled decimal.Decimal `json:"finalPooled" db:"final_pooled"`
FinalCharge decimal.Decimal `json:"finalCharge" db:"final_charge"`
Invoice []string `json:"invoice" db:"invoice"`
Meters []NestedMeter `json:"meters" db:"meters"`
Pooled []NestedMeter `json:"pooled" db:"pooled"`
}
type ReportTask struct {
Id string `json:"id"`
LastModifiedAt types.DateTime `json:"lastModifiedAt" db:"last_modified_at"`
Status int16 `json:"status"`
Message *string `json:"message"`
}
type SimplifiedTenementCharge struct {
ReportId string `json:"reportId" db:"report_id"`
Period types.DateRange `json:"period"`
TotalConsumption decimal.Decimal `json:"totalConsumption" db:"total_consumption"`
FinalCharge decimal.Decimal `json:"finalCharge" db:"final_charge"`
}

View File

@ -5,7 +5,7 @@ import "time"
type Session struct {
Uid string `json:"uid"`
Name string `json:"name"`
Type int8 `json:"type"`
Type int16 `json:"type"`
Token string `json:"token"`
ExpiresAt time.Time `json:"expiresAt" time_format:"simple_datetime" time_location:"shanghai"`
}

38
model/synchronize.go Normal file
View File

@ -0,0 +1,38 @@
package model
import (
"electricity_bill_calc/types"
_ "github.com/shopspring/decimal"
"time"
)
type SynchronizeConfiguration struct {
User string `json:"user" db:"user_id"`
Park string `json:"park" db:"park_id"`
MeterReadingType int16 `json:"meter_reading_type"`
ImrsType string `json:"imrs_type"`
AuthorizationAccount string `json:"authorization_account" db:"imrs_authorization_account"`
AuthorizationSecret string `json:"authorization_secret" db:"imrs_authorization_secret"`
AuthorizationKey []byte `json:"authorization_key,omitempty" db:"imrs_authorization_key"`
Interval int16 `json:"interval"`
CollectAt time.Time `json:"collect_at" db:"-"`
MaxRetries int16 `json:"max_retries"`
RetryInterval int16 `json:"retry_interval"`
RetryIntervalAlgorithm int16 `json:"retry_interval_algorithm"`
}
type SynchronizeSchedule struct {
User string `json:"userId" db:"user_id"`
UserName string `json:"userName" db:"user_name"`
Park string `json:"parkId" db:"park_id"`
ParkName string `json:"parkName" db:"park_name"`
TaskIdentity string `json:"taskIdentity" db:"task_identity"`
TaskName string `json:"taskName" db:"task_name"`
TaskDescription string `json:"taskDescription" db:"task_description"`
CreatedAt types.DateTime `json:"createdAt" db:"created_at"`
LastModifiedAt types.DateTime `json:"lastModifiedAt" db:"last_modified_at"`
LastDispatchedAt types.DateTime `json:"lastDispatchedAt" db:"last_dispatched_at"`
LastDispatchStatus int16 `json:"lastDispatchStatus" db:"last_dispatch_status"`
NextDispatchAt types.DateTime `json:"nextDispatchAt" db:"next_dispatch_at"`
CurrentRetries int16 `json:"currentRetries" db:"current_retries"`
}

23
model/tenement.go Normal file
View File

@ -0,0 +1,23 @@
package model
import "electricity_bill_calc/types"
type Tenement struct {
Id string `json:"id"`
Park string `json:"parkId" db:"park_id"`
FullName string `json:"fullName" db:"full_name"`
ShortName *string `json:"shortName" db:"short_name"`
Abbr string `json:"-"`
Address string `json:"address"`
ContactName string `json:"contactName" db:"contact_name"`
ContactPhone string `json:"contactPhone" db:"contact_phone"`
Building *string `json:"building"`
BuildingName *string `json:"buildingName" db:"building_name"`
OnFloor *string `json:"onFloor" db:"on_floor"`
InvoiceInfo *InvoiceTitle `json:"invoiceInfo" db:"invoice_info"`
MovedInAt *types.DateTime `json:"movedInAt" db:"moved_in_at"`
MovedOutAt *types.DateTime `json:"movedOutAt" db:"moved_out_at"`
CreatedAt types.DateTime `json:"createdAt" db:"created_at"`
LastModifiedAt types.DateTime `json:"lastModifiedAt" db:"last_modified_at"`
DeletedAt *types.DateTime `json:"deletedAt" db:"deleted_at"`
}

33
model/top_up.go Normal file
View File

@ -0,0 +1,33 @@
package model
import (
"electricity_bill_calc/types"
"github.com/shopspring/decimal"
)
type TopUp struct {
TopUpCode string `json:"topUpCode" db:"top_up_code"`
Park string `json:"parkId" db:"park_id"`
Tenement string `json:"tenementId" db:"tenement_id"`
TenementName string `json:"tenementName" db:"tenement_name"`
Meter string `json:"meterId" db:"meter_id"`
MeterAddress *string `json:"meterAddress" db:"meter_address"`
ToppedUpAt types.DateTime `json:"toppedUpAt" db:"topped_up_at"`
Amount decimal.Decimal `json:"amount" db:"amount"`
PaymentType int16 `json:"paymentType" db:"payment_type"`
SuccessfulSynchronized bool `json:"successfulSynchronized" db:"successful_synchronized"`
SynchronizedAt *types.DateTime `json:"synchronizedAt" db:"synchronized_at"`
CancelledAt *types.DateTime `json:"cancelledAt" db:"cancelled_at"`
}
func (t TopUp) SyncStatus() int16 {
switch {
case t.SuccessfulSynchronized && t.SynchronizedAt != nil:
return 1
case !t.SuccessfulSynchronized && t.SynchronizedAt != nil:
return 2
default:
return 0
}
}

View File

@ -1,48 +1,114 @@
package model
import (
"context"
"electricity_bill_calc/types"
"time"
"github.com/uptrace/bun"
"github.com/shopspring/decimal"
)
const (
USER_TYPE_ENT int8 = iota
USER_TYPE_ENT int16 = iota
USER_TYPE_SUP
USER_TYPE_OPS
)
type User struct {
bun.BaseModel `bun:"table:user,alias:u"`
Created `bun:"extend"`
Id string `bun:",pk,notnull" json:"id"`
Username string `bun:",notnull" json:"username"`
Password string `bun:",notnull" json:"-"`
ResetNeeded bool `bun:",notnull" json:"resetNeeded"`
Type int8 `bun:"type:smallint,notnull" json:"type"`
Enabled bool `bun:",notnull" json:"enabled"`
Detail *UserDetail `bun:"rel:has-one,join:id=id" json:"-"`
Charges []*UserCharge `bun:"rel:has-many,join:id=user_id" json:"-"`
type ManagementAccountCreationForm struct {
Id *string `json:"id"`
Username string `json:"username"`
Name string `json:"name"`
Contact *string `json:"contact"`
Phone *string `json:"phone"`
Type int16 `json:"type"`
Enabled bool `json:"enabled"`
Expires types.Date `json:"expires"`
}
type UserWithCredentials struct {
bun.BaseModel `bun:"table:user,alias:u"`
Created `bun:"extend"`
Id string `bun:",pk,notnull" json:"id"`
Username string `bun:",notnull" json:"username"`
Password string `bun:",notnull" json:"credential"`
ResetNeeded bool `bun:",notnull" json:"resetNeeded"`
Type int8 `bun:"type:smallint,notnull" json:"type"`
Enabled bool `bun:",notnull" json:"enabled"`
}
var _ bun.BeforeAppendModelHook = (*User)(nil)
func (u *User) BeforeAppendModel(ctx context.Context, query bun.Query) error {
switch query.(type) {
case *bun.InsertQuery:
u.CreatedAt = time.Now()
func (m ManagementAccountCreationForm) IntoUser() *User {
return &User{
Id: *m.Id,
Username: m.Username,
Password: "",
ResetNeeded: false,
UserType: m.Type,
Enabled: m.Enabled,
CreatedAt: nil,
}
return nil
}
func (m ManagementAccountCreationForm) IntoUserDetail() *UserDetail {
return &UserDetail{
Id: *m.Id,
Name: &m.Name,
Abbr: nil,
Region: nil,
Address: nil,
Contact: m.Contact,
Phone: m.Phone,
UnitServiceFee: decimal.Zero,
ServiceExpiration: m.Expires,
CreatedAt: types.Now(),
CreatedBy: nil,
LastModifiedAt: types.Now(),
LastModifiedBy: nil,
DeletedAt: nil,
DeletedBy: nil,
}
}
type UserModificationForm struct {
Name string `json:"name"`
Region *string `json:"region"`
Address *string `json:"address"`
Contact *string `json:"contact"`
Phone *string `json:"phone"`
UnitServiceFee *decimal.Decimal `json:"unitServiceFee"`
}
type User struct {
Id string `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
ResetNeeded bool `json:"resetNeeded"`
UserType int16 `db:"type"`
Enabled bool `json:"enabled"`
CreatedAt *time.Time `json:"createdAt"`
}
type UserDetail struct {
Id string `json:"id"`
Name *string `json:"name"`
Abbr *string `json:"abbr"`
Region *string `json:"region"`
Address *string `json:"address"`
Contact *string `json:"contact"`
Phone *string `json:"phone"`
UnitServiceFee decimal.Decimal `db:"unit_service_fee" json:"unitServiceFee"`
ServiceExpiration types.Date `json:"serviceExpiration"`
CreatedAt types.DateTime `json:"createdAt"`
CreatedBy *string `json:"createdBy"`
LastModifiedAt types.DateTime `json:"lastModifiedAt"`
LastModifiedBy *string `json:"lastModifiedBy"`
DeletedAt *types.DateTime `json:"deletedAt"`
DeletedBy *string `json:"deletedBy"`
}
type UserWithDetail struct {
Id string `json:"id"`
Username string `json:"username"`
ResetNeeded bool `json:"resetNeeded"`
UserType int16 `db:"type" json:"type"`
Enabled bool `json:"enabled"`
Name *string `json:"name"`
Abbr *string `json:"abbr"`
Region *string `json:"region"`
Address *string `json:"address"`
Contact *string `json:"contact"`
Phone *string `json:"phone"`
UnitServiceFee decimal.Decimal `db:"unit_service_fee" json:"unitServiceFee"`
ServiceExpiration types.Date `json:"serviceExpiration"`
CreatedAt types.DateTime `json:"createdAt"`
CreatedBy *string `json:"createdBy"`
LastModifiedAt types.DateTime `json:"lastModifiedAt"`
LastModifiedBy *string `json:"lastModifiedBy"`
}

70
repository/calculate.go Normal file
View File

@ -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
}

152
repository/charge.go Normal file
View File

@ -0,0 +1,152 @@
package repository
import (
"context"
"electricity_bill_calc/config"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/types"
"fmt"
"github.com/doug-martin/goqu/v9"
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
"github.com/samber/lo"
"go.uber.org/zap"
)
type _ChargeRepository struct {
log *zap.Logger
ds goqu.DialectWrapper
}
var ChargeRepository = &_ChargeRepository{
log: logger.Named("Repository", "Charge"),
ds: goqu.Dialect("postgres"),
}
// 分页查询用户的充值记录
func (cr _ChargeRepository) FindCharges(page uint, beginTime, endTime *types.Date, keyword *string) ([]*model.UserChargeDetail, int64, error) {
cr.log.Info("查询用户的充值记录。", logger.DateFieldp("beginTime", beginTime), logger.DateFieldp("endTime", endTime), zap.Stringp("keyword", keyword), zap.Uint("page", page))
ctx, cancel := global.TimeoutContext()
defer cancel()
chargeQuery := cr.ds.
From(goqu.T("user_charge").As("c")).
Join(goqu.T("user_detail").As("ud"), goqu.On(goqu.I("c.user_id").Eq(goqu.I("ud.id")))).
Join(goqu.T("user").As("u"), goqu.On(goqu.I("ud.id").Eq(goqu.I("u.id")))).
Select(
"c.seq", "c.user_id", "ud.name", "c.fee", "c.discount", "c.amount", "c.charge_to",
"c.settled", "c.settled_at", "c.cancelled", "c.cancelled_at", "c.refunded", "c.refunded_at", "c.created_at",
)
countQuery := cr.ds.
From(goqu.T("user_charge").As("c")).
Join(goqu.T("user_detail").As("ud"), goqu.On(goqu.I("c.user_id").Eq(goqu.I("ud.id")))).
Join(goqu.T("user").As("u"), goqu.On(goqu.I("ud.id").Eq(goqu.I("u.id")))).
Select(goqu.COUNT("*"))
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
chargeQuery = chargeQuery.Where(goqu.Or(
goqu.I("ud.name").ILike(pattern),
goqu.I("ud.abbr").ILike(pattern),
goqu.I("u.username").ILike(pattern),
))
countQuery = countQuery.Where(goqu.Or(
goqu.I("ud.name").ILike(pattern),
goqu.I("ud.abbr").ILike(pattern),
goqu.I("u.username").ILike(pattern),
))
}
if beginTime != nil {
chargeQuery = chargeQuery.Where(goqu.I("c.created_at").Gte(beginTime.ToBeginningOfDate()))
countQuery = countQuery.Where(goqu.I("c.created_at").Gte(beginTime.ToBeginningOfDate()))
}
if endTime != nil {
chargeQuery = chargeQuery.Where(goqu.I("c.created_at").Lte(endTime.ToEndingOfDate()))
countQuery = countQuery.Where(goqu.I("c.created_at").Lte(endTime.ToEndingOfDate()))
}
chargeQuery = chargeQuery.Order(goqu.I("c.created_at").Desc())
currentPostion := (page - 1) * config.ServiceSettings.ItemsPageSize
chargeQuery = chargeQuery.Offset(currentPostion).Limit(config.ServiceSettings.ItemsPageSize)
chargeSql, chargeArgs, _ := chargeQuery.Prepared(true).ToSQL()
countSql, countArgs, _ := countQuery.Prepared(true).ToSQL()
var (
charges []*model.UserChargeDetail = make([]*model.UserChargeDetail, 0)
total int64
)
if err := pgxscan.Select(ctx, global.DB, &charges, chargeSql, chargeArgs...); err != nil {
cr.log.Error("查询用户的充值记录失败。", zap.Error(err))
return make([]*model.UserChargeDetail, 0), 0, err
}
if err := pgxscan.Get(ctx, global.DB, &total, countSql, countArgs...); err != nil {
cr.log.Error("查询用户的充值记录总数失败。", zap.Error(err))
return make([]*model.UserChargeDetail, 0), 0, err
}
return charges, total, nil
}
// 在用户充值记录中创建一条新的记录
func (cr _ChargeRepository) CreateChargeRecord(tx pgx.Tx, ctx context.Context, uid string, fee, discount, amount *float64, chargeTo types.Date) (bool, error) {
createQuery, createArgs, _ := cr.ds.
Insert(goqu.T("user_charge")).
Cols("user_id", "fee", "discount", "amount", "charge_to", "created_at").
Vals(goqu.Vals{uid, fee, discount, amount, chargeTo, types.Now()}).
Prepared(true).ToSQL()
rs, err := tx.Exec(ctx, createQuery, createArgs...)
if err != nil {
cr.log.Error("创建用户充值记录失败。", zap.Error(err))
return false, err
}
return rs.RowsAffected() > 0, nil
}
// 撤销用户的充值记录
func (cr _ChargeRepository) CancelCharge(tx pgx.Tx, ctx context.Context, uid string, seq int64) (bool, error) {
updateQuerySql, updateArgs, _ := cr.ds.
Update(goqu.T("user_charge")).
Set(goqu.Record{"cancelled": true, "cancelled_at": types.Now()}).
Where(goqu.I("user_id").Eq(uid), goqu.I("seq").Eq(seq)).
Prepared(true).ToSQL()
rs, err := tx.Exec(ctx, updateQuerySql, updateArgs...)
if err != nil {
cr.log.Error("撤销用户的充值记录失败。", zap.Error(err))
return false, err
}
return rs.RowsAffected() > 0, nil
}
// 检索用户最近有效的服务期限
func (cr _ChargeRepository) LatestValidChargeTo(tx pgx.Tx, ctx context.Context, uid string) (*types.Date, error) {
searchSql, searchArgs, _ := cr.ds.
From(goqu.T("user_charge")).
Select("charge_to").
Where(
goqu.I("settled").Eq(true),
goqu.I("cancelled").Eq(false),
goqu.I("refunded").Eq(false),
goqu.I("user_id").Eq(uid),
).
Prepared(true).ToSQL()
var chargeTo []*types.Date
if err := pgxscan.Select(ctx, tx, &chargeTo, searchSql, searchArgs...); err != nil {
cr.log.Error("检索用户有效服务期限列表失败。", zap.Error(err))
return nil, err
}
if len(chargeTo) == 0 {
return nil, fmt.Errorf("无法找到用户最近的有效服务期限。")
}
lastCharge := lo.MaxBy(chargeTo, func(a, b *types.Date) bool { return a.Time.After(b.Time) })
return lastCharge, nil
}

19
repository/god.go Normal file
View File

@ -0,0 +1,19 @@
package repository
import (
"electricity_bill_calc/logger"
"github.com/doug-martin/goqu/v9"
"go.uber.org/zap"
)
type _GodModRepository struct {
log *zap.Logger
ds goqu.DialectWrapper
}
var GodModRepository = _GodModRepository{
log: logger.Named("Repository", "GodMod"),
ds: goqu.Dialect("postgres"),
}
// 删除指定园区中的表计和商户的绑定关系

348
repository/invoice.go Normal file
View File

@ -0,0 +1,348 @@
package repository
import (
"context"
"electricity_bill_calc/config"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/types"
"errors"
"fmt"
"github.com/doug-martin/goqu/v9"
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
"github.com/shopspring/decimal"
"go.uber.org/zap"
)
type _InvoiceRepository struct {
log *zap.Logger
ds goqu.DialectWrapper
}
var InvoiceRepository = _InvoiceRepository{
log: logger.Named("Repository", "Invoice"),
ds: goqu.Dialect("postgres"),
}
// 查询指定园区中符合条件的发票
func (ir _InvoiceRepository) ListInvoice(pid *string, startDate, endDate *types.Date, keyword *string, page uint) ([]*model.Invoice, int64, error) {
ir.log.Info("查询指定园区的发票。", zap.Stringp("Park", pid), logger.DateFieldp("StartDate", startDate), logger.DateFieldp("EndDate", endDate), zap.Stringp("Keyword", keyword), zap.Uint("Page", page))
ctx, cancel := global.TimeoutContext()
defer cancel()
invoiceQuery := ir.ds.
From(goqu.T("invoice").As("i")).
Join(goqu.T("tenement").As("t"), goqu.On(goqu.I("i.tenement_id").Eq(goqu.I("t.id")))).
Select("i.*")
countQuery := ir.ds.
From(goqu.T("invoice").As("i")).
Join(goqu.T("tenement").As("t"), goqu.On(goqu.I("i.tenement_id").Eq(goqu.I("t.id")))).
Select(goqu.COUNT("*"))
if pid != nil && len(*pid) > 0 {
invoiceQuery = invoiceQuery.Where(goqu.I("t.park_id").Eq(*pid))
countQuery = countQuery.Where(goqu.I("t.park_id").Eq(*pid))
}
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
invoiceQuery = invoiceQuery.Where(goqu.Or(
goqu.I("i.invoice_no").ILike(pattern),
goqu.I("t.full_name").ILike(pattern),
goqu.I("t.short_name").ILike(pattern),
goqu.I("t.abbr").ILike(pattern),
goqu.I("t.contact_name").ILike(pattern),
goqu.I("t.contact_phone").ILike(pattern),
goqu.L("t.invoice_info->>'usci'").ILike(pattern),
))
countQuery = countQuery.Where(goqu.Or(
goqu.I("i.invoice_no").ILike(pattern),
goqu.I("t.full_name").ILike(pattern),
goqu.I("t.short_name").ILike(pattern),
goqu.I("t.abbr").ILike(pattern),
goqu.I("t.contact_name").ILike(pattern),
goqu.I("t.contact_phone").ILike(pattern),
goqu.L("t.invoice_info->>'usci'").ILike(pattern),
))
}
var queryRange = types.NewEmptyDateTimeRange()
if startDate != nil {
queryRange.SetLower(startDate.ToBeginningOfDate())
}
if endDate != nil {
queryRange.SetUpper(endDate.ToEndingOfDate())
}
if !queryRange.IsEmptyOrWild() {
invoiceQuery = invoiceQuery.Where(goqu.L("i.issued_at <@ ?", queryRange))
countQuery = countQuery.Where(goqu.L("i.issued_at <@ ?", queryRange))
}
startRow := (page - 1) * config.ServiceSettings.ItemsPageSize
invoiceQuery = invoiceQuery.
Order(goqu.I("i.issued_at").Desc()).
Offset(startRow).
Limit(config.ServiceSettings.ItemsPageSize)
var (
invoices []*model.Invoice = make([]*model.Invoice, 0)
total int64
)
querySql, queryArgs, _ := invoiceQuery.Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &invoices, querySql, queryArgs...); err != nil {
ir.log.Error("查询发票记录失败。", zap.Error(err))
return invoices, 0, err
}
countSql, countArgs, _ := countQuery.Prepared(true).ToSQL()
if err := pgxscan.Get(ctx, global.DB, &total, countSql, countArgs...); err != nil {
ir.log.Error("查询发票记录数失败。", zap.Error(err))
return invoices, 0, err
}
return invoices, total, nil
}
// 查询指定商户未开票的核算记录,改记录将只包括商户整体核算,不包括商户各个表计的详细
func (ir _InvoiceRepository) ListUninvoicedTenementCharges(tid string) ([]*model.SimplifiedTenementCharge, error) {
ir.log.Info("查询指定商户的未开票核算记录", zap.String("Tenement", tid))
ctx, cancel := global.TimeoutContext()
defer cancel()
chargeSql, chargeArgs, _ := ir.ds.
From(goqu.T("report_tenement").As("t")).
Join(goqu.T("report").As("r"), goqu.On(goqu.I("t.report_id").Eq(goqu.I("r.id")))).
Select(
goqu.I("t.report_id"),
goqu.I("r.period"),
goqu.L("(t.overall->>'amount')::numeric").As("amount"),
goqu.I("t.final_charge"),
).
Where(
goqu.I("t.tenement_id").Eq(tid),
goqu.I("t.invoice").IsNull(),
).
Prepared(true).ToSQL()
var charges []*model.SimplifiedTenementCharge
if err := pgxscan.Select(ctx, global.DB, &charges, chargeSql, chargeArgs...); err != nil {
ir.log.Error("查询未开票核算记录失败。", zap.Error(err))
return charges, err
}
return charges, nil
}
// 更新指定核算中指定商户的开票状态以及对应发票号。
// 如果给定了发票号,那么指定记录状态为已开票,如果给定的发票号为`nil`,啊么指定记录为未开票。
func (ir _InvoiceRepository) UpdateTenementInvoicedState(tx pgx.Tx, ctx context.Context, rid, tid string, invoiceNo *string) error {
ir.log.Info("更新指定核算中指定商户的开票状态和记录", zap.String("Report", rid), zap.String("Tenement", tid), zap.Stringp("InvoiceNo", invoiceNo))
updateSql, updateArgs, _ := ir.ds.
Update(goqu.T("report_tenement")).
Set(goqu.Record{
"invoice": invoiceNo,
}).
Where(
goqu.I("report_id").Eq(rid),
goqu.I("tenement_id").Eq(tid),
).
Prepared(true).ToSQL()
if _, err := tx.Exec(ctx, updateSql, updateArgs...); err != nil {
ir.log.Error("更新核算记录的开票状态失败。", zap.Error(err))
return err
}
return nil
}
// 查询指定发票的详细记录信息
func (ir _InvoiceRepository) GetInvoiceDetail(invoiceNo string) (*model.Invoice, error) {
ir.log.Info("查询指定发票的详细信息", zap.String("InvoiceNo", invoiceNo))
ctx, cancel := global.TimeoutContext()
defer cancel()
invoiceSql, invoiceArgs, _ := ir.ds.
From(goqu.T("invoice")).
Select("*").
Where(goqu.I("invoice_no").Eq(invoiceNo)).
Prepared(true).ToSQL()
var invoice model.Invoice
if err := pgxscan.Get(ctx, global.DB, &invoice, invoiceSql, invoiceArgs...); err != nil {
ir.log.Error("查询发票记录失败。", zap.Error(err))
return nil, err
}
return &invoice, nil
}
// 获取指定商户的简化核算记录
func (ir _InvoiceRepository) GetSimplifiedTenementCharges(tid string, rids []string) ([]*model.SimplifiedTenementCharge, error) {
ir.log.Info("查询庄园商户的简化核算记录", zap.String("Tenement", tid), zap.Strings("Reports", rids))
ctx, cancel := global.TimeoutContext()
defer cancel()
chargeSql, chargeArgs, _ := ir.ds.
From(goqu.T("report_tenement").As("t")).
Join(goqu.T("report").As("r"), goqu.On(goqu.I("t.report_id").Eq(goqu.I("r.id")))).
Select(
goqu.I("t.report_id"),
goqu.I("r.period"),
goqu.L("(t.overall->>'amount')::numeric").As("amount"),
goqu.I("t.final_charge"),
).
Where(
goqu.I("t.tenement_id").Eq(tid),
goqu.I("t.report_id").In(rids),
).
Prepared(true).ToSQL()
var charges []*model.SimplifiedTenementCharge
if err := pgxscan.Select(ctx, global.DB, &charges, chargeSql, chargeArgs...); err != nil {
ir.log.Error("查询简化核算记录失败。", zap.Error(err))
return charges, err
}
return charges, nil
}
// 查询发票号码对应的商户 ID
// ! 这个方法不能被加入缓存,这个方法存在的目的就是为了清除缓存。
func (ir _InvoiceRepository) GetInvoiceBelongs(invoiceNo string) ([]string, error) {
ir.log.Info("查询发票号码对应的商户 ID", zap.String("InvoiceNo", invoiceNo))
ctx, cancel := global.TimeoutContext()
defer cancel()
tenementSql, tenementArgs, _ := ir.ds.
From(goqu.T("invoice")).
Select("tenement_id").
Where(goqu.I("i.invoice_no").Eq(invoiceNo)).
Prepared(true).ToSQL()
var tenementIds []string
if err := pgxscan.Select(ctx, global.DB, &tenementIds, tenementSql, tenementArgs...); err != nil {
ir.log.Error("查询发票号码对应的商户 ID 失败。", zap.Error(err))
return tenementIds, err
}
return tenementIds, nil
}
// 删除指定的发票记录
func (ir _InvoiceRepository) Delete(tx pgx.Tx, ctx context.Context, invoiceNo string) error {
ir.log.Info("删除指定的发票记录", zap.String("InvoiceNo", invoiceNo))
deleteSql, deleteArgs, _ := ir.ds.
Delete(goqu.T("invoice")).
Where(goqu.I("invoice_no").Eq(invoiceNo)).
Prepared(true).ToSQL()
if _, err := tx.Exec(ctx, deleteSql, deleteArgs...); err != nil {
ir.log.Error("删除发票记录失败。", zap.Error(err))
return err
}
return nil
}
// 删除指定发票记录与指定核算记录之间的关联
func (ir _InvoiceRepository) DeleteInvoiceTenementRelation(tx pgx.Tx, ctx context.Context, invoiceNo string) error {
ir.log.Info("删除指定发票记录与指定核算记录之间的关联", zap.String("InvoiceNo", invoiceNo))
updateSql, updateArgs, _ := ir.ds.
Update(goqu.T("report_tenement")).
Set(goqu.Record{
"invoice": nil,
}).
Where(goqu.I("invoice").Eq(invoiceNo)).
Prepared(true).ToSQL()
if _, err := tx.Exec(ctx, updateSql, updateArgs...); err != nil {
ir.log.Error("删除发票记录与核算记录之间的关联失败。", zap.Error(err))
return err
}
return nil
}
// 确认发票的归属
func (ir _InvoiceRepository) IsBelongsTo(invoiceNo, uid string) (bool, error) {
ir.log.Info("确认发票的归属", zap.String("InvoiceNo", invoiceNo), zap.String("User", uid))
ctx, cancel := global.TimeoutContext()
defer cancel()
querySql, queryArgs, _ := ir.ds.
From(goqu.T("invoice").As("i")).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("i.park_id")))).
Select(goqu.COUNT("i.*")).
Where(
goqu.I("i.invoice_no").Eq(invoiceNo),
goqu.I("p.user_id").Eq(uid),
).
Prepared(true).ToSQL()
var count int64
if err := pgxscan.Get(ctx, global.DB, &count, querySql, queryArgs...); err != nil {
ir.log.Error("查询发票归属失败", zap.Error(err))
return false, err
}
return count > 0, nil
}
// 创建一条新的发票记录
func (ir _InvoiceRepository) Create(pid, tid, invoiceNo string, invoiceType *string, amount decimal.Decimal, issuedAt types.DateTime, taxMethod int16, taxRate decimal.Decimal, cargos *[]*model.InvoiceCargo, covers *[]string) error {
ir.log.Info("记录一个新的发票", zap.String("Park", pid), zap.String("Tenement", tid), zap.String("Invoice", invoiceNo))
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.Begin(ctx)
if err != nil {
ir.log.Error("开启事务失败。", zap.Error(err))
return err
}
tenemenetSql, tenementArgs, _ := ir.ds.
From(goqu.T("tenement").As("t")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("t.building").Eq(goqu.I("b.id")))).
Select(
"t.*", goqu.I("b.name").As("building_name"),
).
Where(goqu.I("t.id").Eq(tid)).
Prepared(true).ToSQL()
var tenement model.Tenement
if err := pgxscan.Get(ctx, global.DB, &tenement, tenemenetSql, tenementArgs...); err != nil {
ir.log.Error("查询商户信息失败。", zap.Error(err))
tx.Rollback(ctx)
return err
}
if tenement.InvoiceInfo == nil {
ir.log.Error("尚未设定商户的发票抬头信息")
tx.Rollback(ctx)
return errors.New("尚未设定商户的发票抬头信息")
}
createSql, createArgs, _ := ir.ds.
Insert(goqu.T("invoice")).
Cols(
"invoice_no", "park_id", "tenement_id", "invoice_type", "amount", "issued_at", "tax_method", "tax_rate", "cargos", "covers",
).
Vals(goqu.Vals{
invoiceNo, pid, tid, invoiceType, amount, issuedAt, taxMethod, taxRate, cargos, covers,
}).
Prepared(true).ToSQL()
if _, err := tx.Exec(ctx, createSql, createArgs...); err != nil {
ir.log.Error("创建发票记录失败。", zap.Error(err))
tx.Rollback(ctx)
return err
}
updateSql, updateArgs, _ := ir.ds.
Update(goqu.T("report_tenement")).
Set(goqu.Record{
"invoice": invoiceNo,
}).
Where(
goqu.I("tenement_id").Eq(tid),
goqu.I("report_id").In(*covers),
).
Prepared(true).ToSQL()
if _, err := tx.Exec(ctx, updateSql, updateArgs...); err != nil {
ir.log.Error("更新核算记录的开票状态失败。", zap.Error(err))
tx.Rollback(ctx)
return err
}
err = tx.Commit(ctx)
if err != nil {
ir.log.Error("提交事务失败。", zap.Error(err))
tx.Rollback(ctx)
return err
}
return nil
}

991
repository/meter.go Normal file
View File

@ -0,0 +1,991 @@
package repository
import (
"context"
"electricity_bill_calc/cache"
"electricity_bill_calc/config"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/tools"
"electricity_bill_calc/tools/serial"
"electricity_bill_calc/types"
"electricity_bill_calc/vo"
"fmt"
"github.com/doug-martin/goqu/v9"
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
"github.com/shopspring/decimal"
"go.uber.org/zap"
)
type _MeterRepository struct {
log *zap.Logger
ds goqu.DialectWrapper
}
var MeterRepository = _MeterRepository{
log: logger.Named("Repository", "Meter"),
ds: goqu.Dialect("postgres"),
}
// 获取指定园区中所有的表计信息
func (mr _MeterRepository) AllMeters(pid string) ([]*model.MeterDetail, error) {
mr.log.Info("列出指定园区中的所有表计", zap.String("park id", pid))
ctx, cancel := global.TimeoutContext()
defer cancel()
var meters []*model.MeterDetail
metersSql, metersArgs, _ := mr.ds.
From(goqu.T("meter_04kv").As("m")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))).
Select(
"m.*", goqu.I("b.name").As("building_name"),
).
Where(
goqu.I("m.park_id").Eq(pid),
goqu.I("m.detachedAt").IsNull(),
).
Order(goqu.I("m.seq").Asc()).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &meters, metersSql, metersArgs...); err != nil {
mr.log.Error("查询表计信息失败", zap.Error(err))
return make([]*model.MeterDetail, 0), err
}
return meters, nil
}
// 列出指定园区下的所有表计信息,包含已经拆除的表计
func (mr _MeterRepository) AllUsedMeters(pid string) ([]*model.MeterDetail, error) {
mr.log.Info("列出指定园区中的所有使用过的表计", zap.String("park id", pid))
ctx, cancel := global.TimeoutContext()
defer cancel()
var meters []*model.MeterDetail
metersSql, metersArgs, _ := mr.ds.
From(goqu.T("meter_04kv").As("m")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))).
Select(
"m.*", goqu.I("b.name").As("building_name"),
).
Where(
goqu.I("m.park_id").Eq(pid),
).
Order(goqu.I("m.seq").Asc()).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &meters, metersSql, metersArgs...); err != nil {
mr.log.Error("查询表计信息失败", zap.Error(err))
return make([]*model.MeterDetail, 0), err
}
return meters, nil
}
// 列出指定核算报表中所使用的所有表计,包含已经拆除的表计
func (mr _MeterRepository) AllUsedMetersInReport(rid string) ([]*model.MeterDetail, error) {
mr.log.Info("列出指定核算报表中所使用的所有表计", zap.String("report id", rid))
ctx, cancel := global.TimeoutContext()
defer cancel()
var meters []*model.MeterDetail
metersSql, metersArgs, _ := mr.ds.
From(goqu.T("meter_04kv").As("m")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))).
Join(goqu.T("report").As("r"), goqu.On(goqu.I("m.park_id").Eq(goqu.I("r.park_id")))).
Where(
goqu.I("r.id").Eq(rid),
goqu.I("m.enabled").Eq(true),
goqu.L("m.attached_at::date < upper(r.period)"),
goqu.Or(
goqu.I("m.detached_at").IsNull(),
goqu.L("m.detached_at::date >= lower(r.period)"),
),
).
Select(
"m.*", goqu.I("b.name").As("building_name"),
).
Order(goqu.I("m.seq").Asc()).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &meters, metersSql, metersArgs...); err != nil {
mr.log.Error("查询表计信息失败", zap.Error(err))
return make([]*model.MeterDetail, 0), err
}
return meters, nil
}
// 分页列出指定园区下的表计信息
func (mr _MeterRepository) MetersIn(pid string, page uint, keyword *string) ([]*model.MeterDetail, int64, error) {
mr.log.Info("分页列出指定园区下的表计信息", zap.String("park id", pid), zap.Uint("page", page), zap.String("keyword", tools.DefaultTo(keyword, "")))
ctx, cancel := global.TimeoutContext()
defer cancel()
meterQuery := mr.ds.
From(goqu.T("meter_04kv").As("m")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))).
Select(
"m.*", goqu.I("b.name").As("building_name"),
).
Where(
goqu.I("m.park_id").Eq(pid),
goqu.I("m.detached_at").IsNull(),
)
countQuery := mr.ds.
From(goqu.T("meter_04kv").As("m")).
Select(goqu.COUNT("*")).
Where(
goqu.I("m.park_id").Eq(pid),
goqu.I("m.detached_at").IsNull(),
)
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
meterQuery = meterQuery.Where(
goqu.Or(
goqu.I("m.code").ILike(pattern),
goqu.I("m.address").ILike(pattern),
),
)
countQuery = countQuery.Where(
goqu.Or(
goqu.I("m.code").ILike(pattern),
goqu.I("m.address").ILike(pattern),
),
)
}
startRow := (page - 1) * config.ServiceSettings.ItemsPageSize
meterQuery = meterQuery.Order(goqu.I("m.seq").Asc()).Offset(startRow).Limit(config.ServiceSettings.ItemsPageSize)
meterSql, meterArgs, _ := meterQuery.Prepared(true).ToSQL()
countSql, countArgs, _ := countQuery.Prepared(true).ToSQL()
var (
meters []*model.MeterDetail
total int64
)
if err := pgxscan.Select(ctx, global.DB, &meters, meterSql, meterArgs...); err != nil {
mr.log.Error("查询表计信息失败", zap.Error(err))
return make([]*model.MeterDetail, 0), 0, err
}
if err := pgxscan.Get(ctx, global.DB, &total, countSql, countArgs...); err != nil {
mr.log.Error("查询表计数量失败", zap.Error(err))
return make([]*model.MeterDetail, 0), 0, err
}
return meters, total, nil
}
// 列出指定园区中指定列表中所有表计的详细信息,将忽略所有表计的当前状态
func (mr _MeterRepository) ListMetersByIDs(pid string, ids []string) ([]*model.MeterDetail, error) {
mr.log.Info("列出指定园区中指定列表中所有表计的详细信息", zap.String("park id", pid), zap.Strings("meter ids", ids))
if len(ids) == 0 {
return make([]*model.MeterDetail, 0), nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
var meters []*model.MeterDetail
metersSql, metersArgs, _ := mr.ds.
From(goqu.T("meter_04kv").As("m")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))).
Select(
"m.*", goqu.I("b.name").As("building_name"),
).
Where(
goqu.I("m.park_id").Eq(pid),
goqu.I("m.code").In(ids),
).
Order(goqu.I("m.seq").Asc()).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &meters, metersSql, metersArgs...); err != nil {
mr.log.Error("查询表计信息失败", zap.Error(err))
return make([]*model.MeterDetail, 0), err
}
return meters, nil
}
// 获取指定表计的详细信息
func (mr _MeterRepository) FetchMeterDetail(pid, code string) (*model.MeterDetail, error) {
mr.log.Info("获取指定表计的详细信息", zap.String("park id", pid), zap.String("meter code", code))
ctx, cancel := global.TimeoutContext()
defer cancel()
var meter model.MeterDetail
meterSql, meterArgs, _ := mr.ds.
From(goqu.T("meter_04kv").As("m")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))).
Select(
"m.*", goqu.I("b.name").As("building_name"),
).
Where(
goqu.I("m.park_id").Eq(pid),
goqu.I("m.code").Eq(code),
).
Prepared(true).ToSQL()
if err := pgxscan.Get(ctx, global.DB, &meter, meterSql, meterArgs...); err != nil {
mr.log.Error("查询表计信息失败", zap.Error(err))
return nil, err
}
return &meter, nil
}
// 创建一条新的表计信息
func (mr _MeterRepository) CreateMeter(tx pgx.Tx, ctx context.Context, pid string, meter vo.MeterCreationForm) (bool, error) {
mr.log.Info("创建一条新的表计信息", zap.String("park id", pid), zap.String("meter code", meter.Code))
timeNow := types.Now()
meterSql, meterArgs, _ := mr.ds.
Insert(goqu.T("meter_04kv")).
Cols(
"park_id", "code", "address", "ratio", "seq", "meter_type", "building", "on_floor", "area", "enabled",
"attached_at", "created_at", "last_modified_at",
).
Vals(
goqu.Vals{pid, meter.Code, meter.Address, meter.Ratio, meter.Seq, meter.MeterType, meter.Building, meter.OnFloor, meter.Area, meter.Enabled,
timeNow, timeNow, timeNow,
},
).
Prepared(true).ToSQL()
ok, err := tx.Exec(ctx, meterSql, meterArgs...)
if err != nil {
mr.log.Error("创建表计信息失败", zap.Error(err))
return false, err
}
return ok.RowsAffected() > 0, nil
}
// 创建或者更新一条表计的信息
func (mr _MeterRepository) CreateOrUpdateMeter(tx pgx.Tx, ctx context.Context, pid string, meter vo.MeterCreationForm) (bool, error) {
mr.log.Info("创建或者更新一条表计的信息", zap.String("park id", pid), zap.String("meter code", meter.Code))
timeNow := types.Now()
meterSql, meterArgs, _ := mr.ds.
Insert(goqu.T("meter_04kv")).
Cols(
"park_id", "code", "address", "ratio", "seq", "meter_type", "building", "on_floor", "area", "enabled",
"attached_at", "created_at", "last_modified_at",
).
Vals(
goqu.Vals{pid, meter.Code, meter.Address, meter.Ratio, meter.Seq, meter.MeterType, meter.Building, meter.OnFloor, meter.Area, meter.Enabled,
timeNow, timeNow, timeNow,
},
).
OnConflict(
goqu.DoUpdate("code, park_id",
goqu.Record{
"address": goqu.I("excluded.address"),
"seq": goqu.I("excluded.seq"),
"ratio": goqu.I("excluded.ratio"),
"meter_type": goqu.I("excluded.meter_type"),
"building": goqu.I("excluded.building"),
"on_floor": goqu.I("excluded.on_floor"),
"area": goqu.I("excluded.area"),
"last_modified_at": goqu.I("excluded.last_modified_at"),
}),
).
Prepared(true).ToSQL()
res, err := tx.Exec(ctx, meterSql, meterArgs...)
if err != nil {
mr.log.Error("创建或者更新表计信息失败", zap.Error(err))
return false, err
}
return res.RowsAffected() > 0, nil
}
// 记录一条表计的抄表信息
func (mr _MeterRepository) RecordReading(tx pgx.Tx, ctx context.Context, pid, code string, meterType int16, ratio decimal.Decimal, reading *vo.MeterReadingForm) (bool, error) {
mr.log.Info("记录一条表计的抄表信息", zap.String("park id", pid), zap.String("meter code", code))
readAt := tools.DefaultTo(reading.ReadAt, types.Now())
readingSql, readingArgs, _ := mr.ds.
Insert(goqu.T("meter_reading")).
Cols(
"park_id", "meter_id", "read_at", "meter_type", "ratio", "overall", "critical", "peak", "flat", "valley",
).
Vals(
goqu.Vals{pid, code, readAt, meterType, ratio, reading.Overall, reading.Critical, reading.Peak, reading.Flat, reading.Valley},
).
Prepared(true).ToSQL()
ok, err := tx.Exec(ctx, readingSql, readingArgs...)
if err != nil {
mr.log.Error("记录表计抄表信息失败", zap.Error(err))
return false, err
}
return ok.RowsAffected() > 0, nil
}
// 更新一条表计的详细信息
func (mr _MeterRepository) UpdateMeter(tx pgx.Tx, ctx context.Context, pid, code string, detail *vo.MeterModificationForm) (bool, error) {
mr.log.Info("更新一条表计的详细信息", zap.String("park id", pid), zap.String("meter code", code))
timeNow := types.Now()
meterSql, meterArgs, _ := mr.ds.
Update(goqu.T("meter_04kv")).
Set(
goqu.Record{
"address": detail.Address,
"seq": detail.Seq,
"ratio": detail.Ratio,
"enabled": detail.Enabled,
"meter_type": detail.MeterType,
"building": detail.Building,
"on_floor": detail.OnFloor,
"area": detail.Area,
"last_modified_at": timeNow,
},
).
Where(
goqu.I("park_id").Eq(pid),
goqu.I("code").Eq(code),
).
Prepared(true).ToSQL()
ok, err := tx.Exec(ctx, meterSql, meterArgs...)
if err != nil {
mr.log.Error("更新表计信息失败", zap.Error(err))
return false, err
}
return ok.RowsAffected() > 0, nil
}
// 列出指定园区中已经存在的表计编号,无论该表计是否已经不再使用。
func (mr _MeterRepository) ListMeterCodes(pid string) ([]string, error) {
mr.log.Info("列出指定园区中已经存在的表计编号", zap.String("park id", pid))
cacheConditions := []string{pid}
if codes, err := cache.RetrieveSearch[[]string]("meter_codes", cacheConditions...); err == nil {
mr.log.Info("从缓存中获取到了指定园区中的表计编号", zap.Int("count", len(*codes)))
return *codes, nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
var codes []string
codesSql, codesArgs, _ := mr.ds.
From(goqu.T("meter_04kv")).
Select("code").
Where(
goqu.I("park_id").Eq(pid),
).
Order(goqu.I("seq").Asc()).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &codes, codesSql, codesArgs...); err != nil {
mr.log.Error("查询表计编号失败", zap.Error(err))
return make([]string, 0), err
}
return codes, nil
}
// 解除指定园区中指定表计的使用
func (mr _MeterRepository) DetachMeter(tx pgx.Tx, ctx context.Context, pid, code string) (bool, error) {
mr.log.Info("解除指定园区中指定表计的使用", zap.String("park id", pid), zap.String("meter code", code))
timeNow := types.Now()
meterSql, meterArgs, _ := mr.ds.
Update(goqu.T("meter_04kv")).
Set(
goqu.Record{
"detached_at": timeNow,
"last_modified_at": timeNow,
},
).
Where(
goqu.I("park_id").Eq(pid),
goqu.I("code").Eq(code),
).
Prepared(true).ToSQL()
ok, err := tx.Exec(ctx, meterSql, meterArgs...)
if err != nil {
mr.log.Error("解除表计使用失败", zap.Error(err))
return false, err
}
return ok.RowsAffected() > 0, nil
}
// 将商户表计绑定到公摊表计上
func (mr _MeterRepository) BindMeter(tx pgx.Tx, ctx context.Context, pid, masterMeter, slaveMeter string) (bool, error) {
mr.log.Info("将商户表计绑定到公摊表计上", zap.String("master meter code", masterMeter), zap.String("slave meter code", slaveMeter))
masterDetail, err := mr.FetchMeterDetail(pid, masterMeter)
if err != nil {
mr.log.Error("查询公摊表计信息失败", zap.Error(err))
return false, err
}
if masterDetail.MeterType != model.METER_INSTALLATION_POOLING {
mr.log.Error("给定的公摊表计不是公摊表计", zap.Error(err))
return false, fmt.Errorf("给定的公摊表计不是公摊表计")
}
slaveDetail, err := mr.FetchMeterDetail(pid, slaveMeter)
if err != nil {
mr.log.Error("查询商户表计信息失败", zap.Error(err))
return false, err
}
if slaveDetail.MeterType != model.METER_INSTALLATION_TENEMENT {
mr.log.Error("给定的商户表计不是商户表计", zap.Error(err))
return false, fmt.Errorf("给定的商户表计不是商户表计")
}
timeNow := types.Now()
serial.StringSerialRequestChan <- 1
code := serial.Prefix("PB", <-serial.StringSerialResponseChan)
relationSql, relationArgs, _ := mr.ds.
Insert(goqu.T("meter_relations")).
Cols(
"id", "park_id", "master_meter_id", "slave_meter_id", "established_at",
).
Vals(
goqu.Vals{
code,
pid,
masterMeter,
slaveMeter,
timeNow,
},
).
Prepared(true).ToSQL()
ok, err := tx.Exec(ctx, relationSql, relationArgs...)
if err != nil {
mr.log.Error("绑定表计关系失败", zap.Error(err))
return false, err
}
return ok.RowsAffected() > 0, nil
}
// 解除两个表计之间的关联
func (mr _MeterRepository) UnbindMeter(tx pgx.Tx, ctx context.Context, pid, masterMeter, slaveMeter string) (bool, error) {
mr.log.Info("解除两个表计之间的关联", zap.String("master meter code", masterMeter), zap.String("slave meter code", slaveMeter))
relationSql, relationArgs, _ := mr.ds.
Update(goqu.T("meter_relations")).
Set(
goqu.Record{
"revoked_at": types.Now(),
},
).
Where(
goqu.I("park_id").Eq(pid),
goqu.I("master_meter_id").Eq(masterMeter),
goqu.I("slave_meter_id").Eq(slaveMeter),
goqu.I("revoked_at").IsNull(),
).
Prepared(true).ToSQL()
ok, err := tx.Exec(ctx, relationSql, relationArgs...)
if err != nil {
mr.log.Error("解除表计关系失败", zap.Error(err))
return false, err
}
return ok.RowsAffected() > 0, nil
}
// 列出指定公摊表计的所有关联表计关系
func (mr _MeterRepository) ListPooledMeterRelations(pid, code string) ([]*model.MeterRelation, error) {
mr.log.Info("列出指定公摊表计的所有关联表计关系", zap.String("park id", pid), zap.String("meter code", code))
ctx, cancel := global.TimeoutContext()
defer cancel()
var relations []*model.MeterRelation
relationsSql, relationsArgs, _ := mr.ds.
From(goqu.T("meter_relations").As("r")).
Select("r.*").
Where(
goqu.I("r.park_id").Eq(pid),
goqu.I("r.master_meter_id").Eq(code),
goqu.I("r.revoked_at").IsNull(),
).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &relations, relationsSql, relationsArgs...); err != nil {
mr.log.Error("查询表计关系失败", zap.Error(err))
return make([]*model.MeterRelation, 0), err
}
return relations, nil
}
// 列出指定公摊表计列表所包含的全部关联表计关系
func (mr _MeterRepository) ListPooledMeterRelationsByCodes(pid string, codes []string) ([]*model.MeterRelation, error) {
mr.log.Info("列出指定公摊表计列表所包含的全部关联表计关系", zap.String("park id", pid), zap.Strings("meter codes", codes))
ctx, cancel := global.TimeoutContext()
defer cancel()
var relations []*model.MeterRelation
relationsSql, relationsArgs, _ := mr.ds.
From(goqu.T("meter_relations").As("r")).
Select("r.*").
Where(
goqu.I("r.park_id").Eq(pid),
goqu.I("r.master_meter_id").In(codes),
goqu.I("r.revoked_at").IsNull(),
).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &relations, relationsSql, relationsArgs...); err != nil {
mr.log.Error("查询表计关系失败", zap.Error(err))
return make([]*model.MeterRelation, 0), err
}
return relations, nil
}
// 列出指定商户表计、园区表计与公摊表计之间的关联关系
func (mr _MeterRepository) ListMeterRelations(pid, code string) ([]*model.MeterRelation, error) {
mr.log.Info("列出指定商户表计、园区表计与公摊表计之间的关联关系", zap.String("park id", pid), zap.String("meter code", code))
ctx, cancel := global.TimeoutContext()
defer cancel()
var relations []*model.MeterRelation
relationsSql, relationsArgs, _ := mr.ds.
From(goqu.T("meter_relations")).
Select("*").
Where(
goqu.I("r.park_id").Eq(pid),
goqu.I("r.slave_meter_id").Eq(code),
goqu.I("r.revoked_at").IsNull(),
).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &relations, relationsSql, relationsArgs...); err != nil {
mr.log.Error("查询表计关系失败", zap.Error(err))
return make([]*model.MeterRelation, 0), err
}
return relations, nil
}
// 列出指定园区中的所有公摊表计
func (mr _MeterRepository) ListPoolingMeters(pid string, page uint, keyword *string) ([]*model.MeterDetail, int64, error) {
mr.log.Info("列出指定园区中的所有公摊表计", zap.String("park id", pid), zap.Uint("page", page), zap.String("keyword", tools.DefaultTo(keyword, "")))
ctx, cancel := global.TimeoutContext()
defer cancel()
meterQuery := mr.ds.
From(goqu.T("meter_04kv").As("m")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))).
Select(
"m.*", goqu.I("b.name").As("building_name"),
).
Where(
goqu.I("m.park_id").Eq(pid),
goqu.I("m.enabled").IsTrue(),
goqu.I("m.meter_type").Eq(model.METER_INSTALLATION_POOLING),
)
countQuery := mr.ds.
From(goqu.T("meter_04kv").As("m")).
Select(goqu.COUNT("*")).
Where(
goqu.I("m.park_id").Eq(pid),
goqu.I("m.enabled").IsTrue(),
goqu.I("m.meter_type").Eq(model.METER_INSTALLATION_POOLING),
)
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
meterQuery = meterQuery.Where(
goqu.Or(
goqu.I("m.code").ILike(pattern),
goqu.I("m.address").ILike(pattern),
),
)
countQuery = countQuery.Where(
goqu.Or(
goqu.I("m.code").ILike(pattern),
goqu.I("m.address").ILike(pattern),
),
)
}
startRow := (page - 1) * config.ServiceSettings.ItemsPageSize
meterQuery = meterQuery.Order(goqu.I("m.code").Asc()).Offset(startRow).Limit(config.ServiceSettings.ItemsPageSize)
meterSql, meterArgs, _ := meterQuery.Prepared(true).ToSQL()
countSql, countArgs, _ := countQuery.Prepared(true).ToSQL()
var (
meters []*model.MeterDetail
total int64
)
if err := pgxscan.Select(ctx, global.DB, &meters, meterSql, meterArgs...); err != nil {
mr.log.Error("查询公摊表计信息失败", zap.Error(err))
return make([]*model.MeterDetail, 0), 0, err
}
if err := pgxscan.Get(ctx, global.DB, &total, countSql, countArgs...); err != nil {
mr.log.Error("查询公摊表计数量失败", zap.Error(err))
return make([]*model.MeterDetail, 0), 0, err
}
return meters, total, nil
}
// 列出目前尚未绑定到公摊表计的商户表计
func (mr _MeterRepository) ListUnboundMeters(uid string, pid *string, keyword *string, limit *uint) ([]*model.MeterDetail, error) {
mr.log.Info("列出目前尚未绑定到公摊表计的商户表计", zap.Stringp("park id", pid), zap.String("user id", uid), zap.String("keyword", tools.DefaultTo(keyword, "")), zap.Uint("limit", tools.DefaultTo(limit, 0)))
ctx, cancel := global.TimeoutContext()
defer cancel()
meterQuery := mr.ds.
From(goqu.T("meter_04kv").As("m")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))).
Select(
"m.*", goqu.I("b.name").As("building_name"),
).
Where(
goqu.I("m.meter_type").Eq(model.METER_INSTALLATION_TENEMENT),
goqu.I("m.enabled").IsTrue(),
)
if pid != nil && len(*pid) > 0 {
meterQuery = meterQuery.Where(
goqu.I("m.park_id").Eq(*pid),
)
}
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
meterQuery = meterQuery.Where(
goqu.Or(
goqu.I("m.code").ILike(pattern),
goqu.I("m.address").ILike(pattern),
),
)
}
slaveMeterQuery := mr.ds.
From("meter_relations").
Select("id")
if pid != nil && len(*pid) > 0 {
slaveMeterQuery = slaveMeterQuery.Where(
goqu.I("park_id").Eq(*pid),
)
} else {
slaveMeterQuery = slaveMeterQuery.Where(
goqu.I("park_id").In(
mr.ds.
From("park").
Select("id").
Where(goqu.I("user_id").Eq(uid)),
))
}
slaveMeterQuery = slaveMeterQuery.Where(
goqu.I("revoked_at").IsNull(),
)
meterQuery = meterQuery.Where(
goqu.I("m.code").NotIn(slaveMeterQuery),
).
Order(goqu.I("m.attached_at").Asc())
if limit != nil && *limit > 0 {
meterQuery = meterQuery.Limit(*limit)
}
meterSql, meterArgs, _ := meterQuery.Prepared(true).ToSQL()
var meters []*model.MeterDetail
if err := pgxscan.Select(ctx, global.DB, &meters, meterSql, meterArgs...); err != nil {
mr.log.Error("查询商户表计信息失败", zap.Error(err))
return make([]*model.MeterDetail, 0), err
}
return meters, nil
}
// 列出目前未绑定到商户的商户表计
func (mr _MeterRepository) ListUnboundTenementMeters(uid string, pid *string, keyword *string, limit *uint) ([]*model.MeterDetail, error) {
mr.log.Info("列出目前未绑定到商户的商户表计", zap.Stringp("park id", pid), zap.String("user id", uid), zap.String("keyword", tools.DefaultTo(keyword, "")), zap.Uint("limit", tools.DefaultTo(limit, 0)))
ctx, cancel := global.TimeoutContext()
defer cancel()
meterQuery := mr.ds.
From(goqu.T("meter_04kv").As("m")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("m.building").Eq(goqu.I("b.id")))).
Select(
"m.*", goqu.I("b.name").As("building_name"),
).
Where(
goqu.I("m.meter_type").Eq(model.METER_INSTALLATION_TENEMENT),
goqu.I("m.enabled").IsTrue(),
)
if pid != nil && len(*pid) > 0 {
meterQuery = meterQuery.Where(
goqu.I("m.park_id").Eq(*pid),
)
}
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
meterQuery = meterQuery.Where(
goqu.Or(
goqu.I("m.code").ILike(pattern),
goqu.I("m.address").ILike(pattern),
),
)
}
subMeterQuery := mr.ds.
From("tenement_meter").
Select("meter_id")
if pid != nil && len(*pid) > 0 {
subMeterQuery = subMeterQuery.Where(
goqu.I("park_id").Eq(*pid),
)
} else {
subMeterQuery = subMeterQuery.Where(
goqu.I("park_id").In(
mr.ds.
From("park").
Select("id").
Where(goqu.I("user_id").Eq(uid)),
))
}
subMeterQuery = subMeterQuery.Where(
goqu.I("disassociated_at").IsNull(),
)
meterQuery = meterQuery.Where(
goqu.I("m.code").NotIn(subMeterQuery),
).
Order(goqu.I("m.attached_at").Asc())
if limit != nil && *limit > 0 {
meterQuery = meterQuery.Limit(*limit)
}
meterSql, meterArgs, _ := meterQuery.Prepared(true).ToSQL()
var meters []*model.MeterDetail
if err := pgxscan.Select(ctx, global.DB, &meters, meterSql, meterArgs...); err != nil {
mr.log.Error("查询商户表计信息失败", zap.Error(err))
return make([]*model.MeterDetail, 0), err
}
return meters, nil
}
// 查询指定园区中的符合条件的抄表记录
func (mr _MeterRepository) ListMeterReadings(pid string, keyword *string, page uint, start, end *types.Date, buidling *string) ([]*model.MeterReading, int64, error) {
mr.log.Info("查询指定园区中的符合条件的抄表记录", zap.String("park id", pid), zap.String("keyword", tools.DefaultTo(keyword, "")), zap.Uint("page", page), logger.DateFieldp("start", start), logger.DateFieldp("end", end), zap.String("building", tools.DefaultTo(buidling, "")))
ctx, cancel := global.TimeoutContext()
defer cancel()
readingQuery := mr.ds.
From(goqu.T("meter_reading").As("r")).
LeftJoin(goqu.T("meter_04kv").As("m"), goqu.On(goqu.I("r.meter_id").Eq(goqu.I("m.code")))).
Select("r.*").
Where(
goqu.I("r.park_id").Eq(pid),
)
countQuery := mr.ds.
From(goqu.T("meter_reading").As("r")).
LeftJoin(goqu.T("meter_04kv").As("m"), goqu.On(goqu.I("r.meter_id").Eq(goqu.I("m.code")))).
Select(goqu.COUNT("*")).
Where(
goqu.I("r.park_id").Eq(pid),
)
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
readingQuery = readingQuery.Where(
goqu.Or(
goqu.I("m.code").ILike(pattern),
goqu.I("m.address").ILike(pattern),
),
)
countQuery = countQuery.Where(
goqu.Or(
goqu.I("m.code").ILike(pattern),
goqu.I("m.address").ILike(pattern),
),
)
}
if start != nil {
readingQuery = readingQuery.Where(
goqu.I("r.read_at").Gte(start.ToBeginningOfDate()),
)
countQuery = countQuery.Where(
goqu.I("r.read_at").Gte(start.ToBeginningOfDate()),
)
}
if end != nil {
readingQuery = readingQuery.Where(
goqu.I("r.read_at").Lte(end.ToEndingOfDate()),
)
countQuery = countQuery.Where(
goqu.I("r.read_at").Lte(end.ToEndingOfDate()),
)
}
if buidling != nil && len(*buidling) > 0 {
readingQuery = readingQuery.Where(
goqu.I("m.building").Eq(*buidling),
)
countQuery = countQuery.Where(
goqu.I("m.building").Eq(*buidling),
)
}
startRow := (page - 1) * config.ServiceSettings.ItemsPageSize
readingQuery = readingQuery.Order(goqu.I("r.read_at").Desc()).Offset(startRow).Limit(config.ServiceSettings.ItemsPageSize)
readingSql, readingArgs, _ := readingQuery.Prepared(true).ToSQL()
countSql, countArgs, _ := countQuery.Prepared(true).ToSQL()
var (
readings []*model.MeterReading
total int64
)
if err := pgxscan.Select(ctx, global.DB, &readings, readingSql, readingArgs...); err != nil {
mr.log.Error("查询抄表记录失败", zap.Error(err))
return make([]*model.MeterReading, 0), 0, err
}
if err := pgxscan.Get(ctx, global.DB, &total, countSql, countArgs...); err != nil {
mr.log.Error("查询抄表记录数量失败", zap.Error(err))
return make([]*model.MeterReading, 0), 0, err
}
return readings, total, nil
}
// 修改指定表计的指定抄表记录
func (mr _MeterRepository) UpdateMeterReading(pid, mid string, readAt types.DateTime, reading *vo.MeterReadingForm) (bool, error) {
mr.log.Info("修改指定表计的指定抄表记录", zap.String("park id", pid), zap.String("meter id", mid), logger.DateTimeField("read at", readAt), zap.Any("reading", reading))
ctx, cancel := global.TimeoutContext()
defer cancel()
updateSql, updateArgs, _ := mr.ds.
Update(goqu.T("meter_reading")).
Set(
goqu.Record{
"overall": reading.Overall,
"critical": reading.Critical,
"peak": reading.Peak,
"flat": reading.Flat,
"valley": reading.Valley,
},
).
Where(
goqu.I("park_id").Eq(pid),
goqu.I("meter_id").Eq(mid),
goqu.I("read_at").Eq(readAt),
).
Prepared(true).ToSQL()
ok, err := global.DB.Exec(ctx, updateSql, updateArgs...)
if err != nil {
mr.log.Error("更新抄表记录失败", zap.Error(err))
return false, err
}
return ok.RowsAffected() > 0, nil
}
// 列出指定园区中指定时间区域内的所有表计抄表记录
func (mr _MeterRepository) ListMeterReadingsByTimeRange(pid string, start, end types.Date) ([]*model.MeterReading, error) {
mr.log.Info("列出指定园区中指定时间区域内的所有表计抄表记录", zap.String("park id", pid), zap.Time("start", start.Time), zap.Time("end", end.Time))
ctx, cancel := global.TimeoutContext()
defer cancel()
var readings []*model.MeterReading
readingSql, readingArgs, _ := mr.ds.
From(goqu.T("meter_reading").As("r")).
Select("*").
Where(
goqu.I("r.park_id").Eq(pid),
goqu.I("r.read_at").Gte(start.ToBeginningOfDate()),
goqu.I("r.read_at").Lte(end.ToEndingOfDate()),
).
Order(goqu.I("r.read_at").Desc()).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &readings, readingSql, readingArgs...); err != nil {
mr.log.Error("查询抄表记录失败", zap.Error(err))
return make([]*model.MeterReading, 0), err
}
return readings, nil
}
// 列出指定园区中在指定日期之前的最后一次抄表记录
func (mr _MeterRepository) ListLastMeterReading(pid string, date types.Date) ([]*model.MeterReading, error) {
mr.log.Info("列出指定园区中在指定日期之前的最后一次抄表记录", zap.String("park id", pid), zap.Time("date", date.Time))
ctx, cancel := global.TimeoutContext()
defer cancel()
var readings []*model.MeterReading
readingSql, readingArgs, _ := mr.ds.
From(goqu.T("meter_reading")).
Select(
goqu.MAX("read_at").As("read_at"),
"park_id", "meter_id", "overall", "critical", "peak", "flat", "valley",
).
Where(
goqu.I("park_id").Eq(pid),
goqu.I("read_at").Lt(date.ToEndingOfDate()),
).
GroupBy("park_id", "meter_id", "overall", "critical", "peak", "flat", "valley").
Order(goqu.I("read_at").Desc()).
Limit(1).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &readings, readingSql, readingArgs...); err != nil {
mr.log.Error("查询抄表记录失败", zap.Error(err))
return make([]*model.MeterReading, 0), err
}
return readings, nil
}
// 列出指定园区中的表计与商户的关联详细记录用于写入Excel模板文件
func (mr _MeterRepository) ListMeterDocForTemplate(pid string) ([]*model.SimpleMeterDocument, error) {
mr.log.Info("列出指定园区中的表计与商户的关联详细记录", zap.String("park id", pid))
ctx, cancel := global.TimeoutContext()
defer cancel()
var docs []*model.SimpleMeterDocument
docSql, docArgs, _ := mr.ds.
From(goqu.T("meter_04kv").As("m")).
LeftJoin(
goqu.T("tenement_meter").As("tm"),
goqu.On(
goqu.I("m.code").Eq(goqu.I("tm.meter_id")),
goqu.I("m.park_id").Eq(goqu.I("tm.park_id")),
),
).
LeftJoin(
goqu.T("tenement").As("t"),
goqu.On(
goqu.I("tm.tenement_id").Eq(goqu.I("t.id")),
goqu.I("tm.park_id").Eq(goqu.I("t.park_id")),
),
).
Select(
"m.code", "m.address", "m.ratio", "m.seq", goqu.I("t.full_name").As("tenement_name"),
).
Where(
goqu.I("m.park_id").Eq(pid),
goqu.I("m.enabled").IsTrue(),
goqu.I("tm.disassociated_at").IsNull(),
).
Order(goqu.I("m.seq").Asc()).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &docs, docSql, docArgs...); err != nil {
mr.log.Error("查询表计与商户关联信息失败", zap.Error(err))
return make([]*model.SimpleMeterDocument, 0), err
}
return docs, nil
}

419
repository/park.go Normal file
View File

@ -0,0 +1,419 @@
package repository
import (
"context"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/tools"
"electricity_bill_calc/tools/serial"
"electricity_bill_calc/types"
"github.com/doug-martin/goqu/v9"
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
"go.uber.org/zap"
)
type _ParkRepository struct {
log *zap.Logger
ds goqu.DialectWrapper
}
var ParkRepository = _ParkRepository{
log: logger.Named("Repository", "Park"),
ds: goqu.Dialect("postgres"),
}
// 列出指定用户下的所有园区
func (pr _ParkRepository) ListAllParks(uid string) ([]*model.Park, error) {
pr.log.Info("列出指定用户下的所有园区", zap.String("uid", uid))
ctx, cancel := global.TimeoutContext()
defer cancel()
var parks = make([]*model.Park, 0)
parkQuerySql, parkParams, _ := pr.ds.
From("park").
Select(
"id", "user_id", "name", "area", "tenement_quantity", "capacity", "category",
"meter_04kv_type", "region", "address", "contact", "phone", "enabled", "price_policy", "tax_rate",
"basic_pooled", "adjust_pooled", "loss_pooled", "public_pooled", "created_at", "last_modified_at",
"deleted_at",
).
Where(
goqu.I("user_id").Eq(uid),
goqu.I("deleted_at").IsNull(),
).
Order(goqu.I("created_at").Asc()).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &parks, parkQuerySql, parkParams...); err != nil {
pr.log.Error("列出指定用户下的所有园区失败!", zap.Error(err))
return make([]*model.Park, 0), err
}
return parks, nil
}
// 检查并确定指定园区的归属情况
func (pr _ParkRepository) IsParkBelongs(pid, uid string) (bool, error) {
pr.log.Info("检查并确定指定园区的归属情况", zap.String("pid", pid), zap.String("uid", uid))
ctx, cancel := global.TimeoutContext()
defer cancel()
var count int64
parkQuerySql, parkParams, _ := pr.ds.
From("park").
Select(goqu.COUNT("*")).
Where(
goqu.I("id").Eq(pid),
goqu.I("user_id").Eq(uid),
).
Prepared(true).ToSQL()
if err := pgxscan.Get(ctx, global.DB, &count, parkQuerySql, parkParams...); err != nil {
pr.log.Error("检查并确定指定园区的归属情况失败!", zap.Error(err))
return false, err
}
return count > 0, nil
}
// 创建一个属于指定用户的新园区。该创建功能不会对园区的名称进行检查。
func (pr _ParkRepository) CreatePark(ownerId string, park *model.Park) (bool, error) {
pr.log.Info("创建一个属于指定用户的新园区", zap.String("ownerId", ownerId))
ctx, cancel := global.TimeoutContext()
defer cancel()
timeNow := types.Now()
serial.StringSerialRequestChan <- 1
code := serial.Prefix("P", <-serial.StringSerialResponseChan)
createSql, createArgs, _ := pr.ds.
Insert("park").
Cols(
"id", "user_id", "name", "abbr", "area", "tenement_quantity", "capacity", "category",
"meter_04kv_type", "region", "address", "contact", "phone", "enabled", "price_policy", "tax_rate",
"basic_pooled", "adjust_pooled", "loss_pooled", "public_pooled", "created_at", "last_modified_at",
).
Vals(goqu.Vals{
code,
ownerId, park.Name, tools.PinyinAbbr(park.Name),
park.Area, park.TenementQuantity, park.Capacity, park.Category,
park.MeterType, park.Region, park.Address, park.Contact, park.Phone, park.Enabled, park.PricePolicy, park.TaxRate,
park.BasicPooled, park.AdjustPooled, park.LossPooled, park.PublicPooled, timeNow, timeNow,
}).
Prepared(true).ToSQL()
rs, err := global.DB.Exec(ctx, createSql, createArgs...)
if err != nil {
pr.log.Error("创建一个属于指定用户的新园区失败!", zap.Error(err))
return false, err
}
return rs.RowsAffected() > 0, nil
}
// 获取指定园区的详细信息
func (pr _ParkRepository) RetrieveParkDetail(pid string) (*model.Park, error) {
pr.log.Info("获取指定园区的详细信息", zap.String("pid", pid))
ctx, cancel := global.TimeoutContext()
defer cancel()
var park model.Park
parkSql, parkArgs, _ := pr.ds.
From("park").
Select(
"id", "user_id", "name", "area", "tenement_quantity", "capacity", "category",
"meter_04kv_type", "region", "address", "contact", "phone", "enabled", "price_policy", "tax_rate",
"basic_pooled", "adjust_pooled", "loss_pooled", "public_pooled", "created_at", "last_modified_at",
"deleted_at",
).
Where(goqu.I("id").Eq(pid)).
Prepared(true).ToSQL()
if err := pgxscan.Get(ctx, global.DB, &park, parkSql, parkArgs...); err != nil {
pr.log.Error("获取指定园区的详细信息失败!", zap.Error(err))
return nil, err
}
return &park, nil
}
// 获取园区对应的用户ID
func (pr _ParkRepository) RetrieveParkBelongs(pid string) (string, error) {
pr.log.Info("获取园区对应的用户ID", zap.String("pid", pid))
ctx, cancel := global.TimeoutContext()
defer cancel()
var uid string
parkSql, parkArgs, _ := pr.ds.
From("park").
Select(goqu.I("user_id")).
Where(goqu.I("id").Eq(pid)).
Prepared(true).ToSQL()
if err := pgxscan.Get(ctx, global.DB, &uid, parkSql, parkArgs...); err != nil {
pr.log.Error("获取园区对应的用户ID失败", zap.Error(err))
return "", err
}
return uid, nil
}
// 更新指定园区的信息
func (pr _ParkRepository) UpdatePark(pid string, park *model.Park) (bool, error) {
pr.log.Info("更新指定园区的信息", zap.String("pid", pid))
ctx, cancel := global.TimeoutContext()
defer cancel()
timeNow := types.Now()
updateSql, updateArgs, _ := pr.ds.
Update("park").
Set(goqu.Record{
"name": park.Name,
"abbr": tools.PinyinAbbr(park.Name),
"area": park.Area,
"tenement_quantity": park.TenementQuantity,
"capacity": park.Capacity,
"category": park.Category,
"meter_04kv_type": park.MeterType,
"region": park.Region,
"address": park.Address,
"contact": park.Contact,
"phone": park.Phone,
"price_policy": park.PricePolicy,
"tax_rate": park.TaxRate,
"basic_pooled": park.BasicPooled,
"adjust_pooled": park.AdjustPooled,
"loss_pooled": park.LossPooled,
"public_pooled": park.PublicPooled,
"last_modified_at": timeNow,
}).
Where(goqu.I("id").Eq(pid)).
Prepared(true).ToSQL()
ok, err := global.DB.Exec(ctx, updateSql, updateArgs...)
if err != nil {
pr.log.Error("更新指定园区的信息失败!", zap.Error(err))
return false, err
}
return ok.RowsAffected() > 0, nil
}
// 设定园区的可用状态
func (pr _ParkRepository) EnablingPark(pid string, enabled bool) (bool, error) {
pr.log.Info("设定园区的可用状态", zap.String("pid", pid), zap.Bool("enabled", enabled))
ctx, cancel := global.TimeoutContext()
defer cancel()
timeNow := types.Now()
updateSql, updateArgs, _ := pr.ds.
Update("park").
Set(goqu.Record{
"enabled": enabled,
"last_modified_at": timeNow,
}).
Where(goqu.I("id").Eq(pid)).
Prepared(true).ToSQL()
ok, err := global.DB.Exec(ctx, updateSql, updateArgs...)
if err != nil {
pr.log.Error("设定园区的可用状态失败!", zap.Error(err))
return false, err
}
return ok.RowsAffected() > 0, nil
}
// 删除指定园区(软删除)
func (pr _ParkRepository) DeletePark(pid string) (bool, error) {
pr.log.Info("删除指定园区(软删除)", zap.String("pid", pid))
ctx, cancel := global.TimeoutContext()
defer cancel()
timeNow := types.Now()
updateSql, updateArgs, _ := pr.ds.
Update("park").
Set(goqu.Record{
"deleted_at": timeNow,
"last_modified_at": timeNow,
}).
Where(goqu.I("id").Eq(pid)).
Prepared(true).ToSQL()
ok, err := global.DB.Exec(ctx, updateSql, updateArgs...)
if err != nil {
pr.log.Error("删除指定园区(软删除)失败!", zap.Error(err))
return false, err
}
return ok.RowsAffected() > 0, nil
}
// 检索给定的园区详细信息列表
func (pr _ParkRepository) RetrieveParks(pids []string) ([]*model.Park, error) {
pr.log.Info("检索给定的园区详细信息列表", zap.Strings("pids", pids))
if len(pids) == 0 {
pr.log.Info("给定要检索的园区ID列表为空执行快速返回。")
return make([]*model.Park, 0), nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
var parks []*model.Park
parkSql, parkArgs, _ := pr.ds.
From("park").
Where(goqu.I("id").In(pids)).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &parks, parkSql, parkArgs...); err != nil {
pr.log.Error("检索给定的园区详细信息列表失败!", zap.Error(err))
return nil, err
}
return parks, nil
}
// 获取指定园区中的建筑
func (pr _ParkRepository) RetrieveParkBuildings(pid string) ([]*model.ParkBuilding, error) {
pr.log.Info("获取指定园区中的建筑", zap.String("pid", pid))
ctx, cancel := global.TimeoutContext()
defer cancel()
var buildings []*model.ParkBuilding
buildingSql, buildingArgs, _ := pr.ds.
From("park_building").
Where(
goqu.I("park_id").Eq(pid),
goqu.I("deleted_at").IsNull(),
).
Order(goqu.I("created_at").Asc()).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &buildings, buildingSql, buildingArgs...); err != nil {
pr.log.Error("获取指定园区中的建筑失败!", zap.Error(err))
return nil, err
}
return buildings, nil
}
// 在指定园区中创建一个新建筑
func (pr _ParkRepository) CreateParkBuilding(pid, name string, floor *string) (bool, error) {
pr.log.Info("在指定园区中创建一个新建筑", zap.String("pid", pid), zap.String("name", name), zap.Stringp("floor", floor))
ctx, cancel := global.TimeoutContext()
defer cancel()
timeNow := types.Now()
serial.StringSerialRequestChan <- 1
code := serial.Prefix("B", <-serial.StringSerialResponseChan)
createSql, createArgs, _ := pr.ds.
Insert("park_building").
Cols(
"id", "park_id", "name", "floors", "enabled", "created_at", "last_modified_at",
).
Vals(goqu.Vals{
code,
pid, name, floor, true, timeNow, timeNow,
}).
Prepared(true).ToSQL()
rs, err := global.DB.Exec(ctx, createSql, createArgs...)
if err != nil {
pr.log.Error("在指定园区中创建一个新建筑失败!", zap.Error(err))
return false, err
}
return rs.RowsAffected() > 0, nil
}
// 在指定园区中创建一个建筑,这个方法会使用事务
func (pr _ParkRepository) CreateParkBuildingWithTransaction(tx pgx.Tx, ctx context.Context, pid, name string, floor *string) (bool, error) {
timeNow := types.Now()
serial.StringSerialRequestChan <- 1
code := serial.Prefix("B", <-serial.StringSerialResponseChan)
createSql, createArgs, _ := pr.ds.
Insert("park_building").
Cols(
"id", "park_id", "name", "floors", "enabled", "created_at", "last_modified_at",
).
Vals(goqu.Vals{
code,
pid, name, floor, true, timeNow, timeNow,
}).
Prepared(true).ToSQL()
rs, err := tx.Exec(ctx, createSql, createArgs...)
if err != nil {
pr.log.Error("在指定园区中创建一个新建筑失败!", zap.Error(err))
return false, err
}
return rs.RowsAffected() > 0, nil
}
// 修改指定园区中指定建筑的信息
func (pr _ParkRepository) ModifyParkBuilding(id, pid, name string, floor *string) (bool, error) {
pr.log.Info("修改指定园区中指定建筑的信息", zap.String("id", id), zap.String("pid", pid), zap.String("name", name), zap.Stringp("floor", floor))
ctx, cancel := global.TimeoutContext()
defer cancel()
timeNow := types.Now()
updateSql, updateArgs, _ := pr.ds.
Update("park_building").
Set(goqu.Record{
"name": name,
"floors": floor,
"last_modified_at": timeNow,
}).
Where(
goqu.I("id").Eq(id),
goqu.I("park_id").Eq(pid),
).
Prepared(true).ToSQL()
rs, err := global.DB.Exec(ctx, updateSql, updateArgs...)
if err != nil {
pr.log.Error("修改指定园区中指定建筑的信息失败!", zap.Error(err))
return false, err
}
return rs.RowsAffected() > 0, nil
}
// 修改指定建筑的可以状态
func (pr _ParkRepository) EnablingParkBuilding(id, pid string, enabled bool) (bool, error) {
pr.log.Info("修改指定建筑的可以状态", zap.String("id", id), zap.String("pid", pid), zap.Bool("enabled", enabled))
ctx, cancel := global.TimeoutContext()
defer cancel()
timeNow := types.Now()
updateSql, updateArgs, _ := pr.ds.
Update("park_building").
Set(goqu.Record{
"enabled": enabled,
"last_modified_at": timeNow,
}).
Where(
goqu.I("id").Eq(id),
goqu.I("park_id").Eq(pid),
).
Prepared(true).ToSQL()
rs, err := global.DB.Exec(ctx, updateSql, updateArgs...)
if err != nil {
pr.log.Error("修改指定建筑的可以状态失败!", zap.Error(err))
return false, err
}
return rs.RowsAffected() > 0, nil
}
// 删除指定建筑(软删除)
func (pr _ParkRepository) DeleteParkBuilding(id, pid string) (bool, error) {
pr.log.Info("删除指定建筑(软删除)", zap.String("id", id), zap.String("pid", pid))
ctx, cancel := global.TimeoutContext()
defer cancel()
timeNow := types.Now()
updateSql, updateArgs, _ := pr.ds.
Update("park_building").
Set(goqu.Record{
"deleted_at": timeNow,
"last_modified_at": timeNow,
}).
Where(
goqu.I("id").Eq(id),
goqu.I("park_id").Eq(pid),
).
Prepared(true).ToSQL()
rs, err := global.DB.Exec(ctx, updateSql, updateArgs...)
if err != nil {
pr.log.Error("删除指定建筑(软删除)失败!", zap.Error(err))
return false, err
}
return rs.RowsAffected() > 0, nil
}

79
repository/region.go Normal file
View File

@ -0,0 +1,79 @@
package repository
import (
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"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 _RegionRepository struct {
log *zap.Logger
ds goqu.DialectWrapper
}
var RegionRepository = _RegionRepository{
log: logger.Named("Repository", "Region"),
ds: goqu.Dialect("postgres"),
}
// 获取指定行政区划下所有直接子级行政区划
func (r *_RegionRepository) FindSubRegions(parent string) ([]model.Region, error) {
r.log.Info("获取指定行政区划下所有直接子级行政区划", zap.String("parent", parent))
ctx, cancel := global.TimeoutContext()
defer cancel()
var regions []model.Region
regionQuerySql, regionParams, _ := r.ds.
From("region").
Where(goqu.Ex{"parent": parent}).
Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &regions, regionQuerySql, regionParams...); err != nil {
r.log.Error("获取指定行政区划下所有直接子级行政区划失败!", zap.Error(err))
return nil, err
}
return regions, nil
}
// 获取一个指定编号的行政区划详细信息
func (r *_RegionRepository) FindRegion(code string) (*model.Region, error) {
r.log.Info("获取指定行政区划信息", zap.String("code", code))
ctx, cancel := global.TimeoutContext()
defer cancel()
var region model.Region
regionQuerySql, regionParams, _ := r.ds.
From("region").
Where(goqu.Ex{"code": code}).
Prepared(true).ToSQL()
if err := pgxscan.Get(ctx, global.DB, &region, regionQuerySql, regionParams...); err != nil {
r.log.Error("获取指定行政区划信息失败!", zap.Error(err))
return nil, err
}
return &region, nil
}
// 获取指定行政区划的所有直接和非直接父级
func (r *_RegionRepository) FindParentRegions(code string) ([]*model.Region, error) {
r.log.Info("获取指定行政区划的所有直接和非直接父级", zap.String("code", code))
var (
regionsScanTask = []string{code}
regions = make([]*model.Region, 0)
)
for len(regionsScanTask) > 0 {
region, err := r.FindRegion(regionsScanTask[0])
regionsScanTask = append([]string{}, regionsScanTask[1:]...)
if err == nil && region != nil {
regions = append(regions, region)
if region.Parent != "0" {
regionsScanTask = append(regionsScanTask, region.Parent)
}
}
}
return regions, nil
}

846
repository/report.go Normal file
View File

@ -0,0 +1,846 @@
package repository
import (
"electricity_bill_calc/config"
"electricity_bill_calc/exceptions"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/tools/serial"
"electricity_bill_calc/types"
"electricity_bill_calc/vo"
"fmt"
"github.com/doug-martin/goqu/v9"
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/samber/lo"
"go.uber.org/zap"
)
type _ReportRepository struct {
log *zap.Logger
ds goqu.DialectWrapper
}
var ReportRepository = _ReportRepository{
log: logger.Named("Repository", "Report"),
ds: goqu.Dialect("postgres"),
}
// 检查指定核算报表的归属情况
func (rr _ReportRepository) IsBelongsTo(rid, uid string) (bool, error) {
rr.log.Info("检查指定核算报表的归属", zap.String("User", uid), zap.String("Report", rid))
ctx, cancel := global.TimeoutContext()
defer cancel()
querySql, queryParams, _ := rr.ds.
From(goqu.T("report").As("r")).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("r.park_id")))).
Select(goqu.COUNT("r.*")).
Where(
goqu.I("r.id").Eq(rid),
goqu.I("p.user_id").Eq(uid),
).
Prepared(true).ToSQL()
var count int64
if err := pgxscan.Get(ctx, global.DB, &count, querySql, queryParams...); err != nil {
rr.log.Error("检查指定核算报表的归属出现错误", zap.Error(err))
return false, err
}
return count > 0, nil
}
// 获取指定用户下所有园区的尚未发布的简易核算报表索引内容
func (rr _ReportRepository) ListDraftReportIndicies(uid string) ([]*model.ReportIndex, error) {
rr.log.Info("获取指定用户下的所有尚未发布的报表索引", zap.String("User", uid))
ctx, cancel := global.TimeoutContext()
defer cancel()
querySql, queryParams, _ := rr.ds.
From(goqu.T("report").As("r")).
Join(goqu.T("report_task").As("t"), goqu.On(goqu.I("t.id").Eq(goqu.I("r.id")))).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("r.park_id")))).
Select("r.*", goqu.I("t.status"), goqu.I("t.message")).
Where(
goqu.I("p.user_id").Eq(uid),
goqu.I("r.published").IsFalse(),
).
Order(goqu.I("r.created_at").Desc()).
Prepared(true).ToSQL()
var indicies []*model.ReportIndex = make([]*model.ReportIndex, 0)
if err := pgxscan.Select(ctx, global.DB, &indicies, querySql, queryParams...); err != nil {
rr.log.Error("获取指定用户下的所有尚未发布的报表索引出现错误", zap.Error(err))
return indicies, err
}
return indicies, nil
}
// 获取指定报表的详细索引内容
func (rr _ReportRepository) GetReportIndex(rid string) (*model.ReportIndex, error) {
rr.log.Info("获取指定报表的详细索引", zap.String("Report", rid))
ctx, cancel := global.TimeoutContext()
defer cancel()
querySql, queryParams, _ := rr.ds.
From(goqu.T("report").As("r")).
Join(goqu.T("report_task").As("t"), goqu.On(goqu.I("t.id").Eq(goqu.I("r.id")))).
Select("r.*", goqu.I("t.status"), goqu.I("t.message")).
Where(goqu.I("r.id").Eq(rid)).
Prepared(true).ToSQL()
var index model.ReportIndex
if err := pgxscan.Get(ctx, global.DB, &index, querySql, queryParams...); err != nil {
rr.log.Error("获取指定报表的详细索引出现错误", zap.Error(err))
return nil, err
}
return &index, nil
}
// 为指园区创建一个新的核算报表
func (rr _ReportRepository) CreateReport(form *vo.ReportCreationForm) (bool, error) {
rr.log.Info("为指定园区创建一个新的核算报表", zap.String("Park", form.Park))
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.Begin(ctx)
if err != nil {
rr.log.Error("未能开始一个数据库事务", zap.Error(err))
return false, err
}
park, err := ParkRepository.RetrieveParkDetail(form.Park)
if err != nil || park == nil {
rr.log.Error("未能获取指定园区的详细信息", zap.Error(err))
tx.Rollback(ctx)
return false, exceptions.NewNotFoundErrorFromError("未能获取指定园区的详细信息", err)
}
createTime := types.Now()
periodRange := types.NewDateRange(&form.PeriodBegin, &form.PeriodEnd)
serial.StringSerialRequestChan <- 1
reportId := serial.Prefix("R", <-serial.StringSerialResponseChan)
createSql, createArgs, _ := rr.ds.
Insert(goqu.T("report")).
Cols(
"id", "park_id", "period", "category", "meter_o4kv_type", "price_policy",
"basic_pooled", "adjust_pooled", "loss_pooled", "public_pooled", "created_at",
"last_modified_at",
).
Vals(goqu.Vals{
reportId, park.Id, periodRange, park.Category, park.MeterType, park.PricePolicy,
park.BasicPooled, park.AdjustPooled, park.LossPooled, park.PublicPooled, createTime,
createTime,
}).
Prepared(true).ToSQL()
summarySql, summaryArgs, _ := rr.ds.
Insert(goqu.T("report_summary")).
Cols(
"report_id", "overall", "critical", "peak", "flat", "valley", "basic_fee",
"adjust_fee",
).
Vals(goqu.Vals{
reportId,
model.ConsumptionUnit{
Amount: form.Overall,
Fee: form.OverallFee,
},
model.ConsumptionUnit{
Amount: form.Critical,
Fee: form.CriticalFee,
},
model.ConsumptionUnit{
Amount: form.Peak,
Fee: form.PeakFee,
},
model.ConsumptionUnit{
Amount: form.Flat,
Fee: form.FlatFee,
},
model.ConsumptionUnit{
Amount: form.Valley,
Fee: form.ValleyFee,
},
form.BasicFee,
form.AdjustFee,
}).
Prepared(true).ToSQL()
taskSql, taskArgs, _ := rr.ds.
Insert(goqu.T("report_task")).
Cols("id", "status", "last_modified_at").
Vals(goqu.Vals{reportId, model.REPORT_CALCULATE_TASK_STATUS_PENDING, createTime}).
Prepared(true).ToSQL()
resIndex, err := tx.Exec(ctx, createSql, createArgs...)
if err != nil {
rr.log.Error("创建核算报表索引时出现错误", zap.Error(err))
tx.Rollback(ctx)
return false, err
}
if resIndex.RowsAffected() == 0 {
rr.log.Error("保存核算报表索引时出现错误", zap.Error(err))
tx.Rollback(ctx)
return false, exceptions.NewUnsuccessCreateError("创建核算报表索引时出现错误")
}
resSummary, err := tx.Exec(ctx, summarySql, summaryArgs...)
if err != nil {
rr.log.Error("创建核算报表汇总时出现错误", zap.Error(err))
tx.Rollback(ctx)
return false, err
}
if resSummary.RowsAffected() == 0 {
rr.log.Error("保存核算报表汇总时出现错误", zap.Error(err))
tx.Rollback(ctx)
return false, exceptions.NewUnsuccessCreateError("创建核算报表汇总时出现错误")
}
resTask, err := tx.Exec(ctx, taskSql, taskArgs...)
if err != nil {
rr.log.Error("创建核算报表任务时出现错误", zap.Error(err))
tx.Rollback(ctx)
return false, err
}
if resTask.RowsAffected() == 0 {
rr.log.Error("保存核算报表任务时出现错误", zap.Error(err))
tx.Rollback(ctx)
return false, exceptions.NewUnsuccessCreateError("创建核算报表任务时出现错误")
}
err = tx.Commit(ctx)
if err != nil {
rr.log.Error("提交核算报表创建事务时出现错误", zap.Error(err))
tx.Rollback(ctx)
return false, err
}
return resIndex.RowsAffected() > 0 && resSummary.RowsAffected() > 0 && resTask.RowsAffected() > 0, nil
}
// 更新报表的基本信息
func (rr _ReportRepository) UpdateReportSummary(rid string, form *vo.ReportModifyForm) (bool, error) {
rr.log.Info("更新指定报表的基本信息", zap.String("Report", rid))
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.Begin(ctx)
if err != nil {
rr.log.Error("未能开始一个数据库事务", zap.Error(err))
return false, err
}
updateTime := types.Now()
newPeriod := types.NewDateRange(&form.PeriodBegin, &form.PeriodEnd)
udpateIndexSql, updateIndexArgs, _ := rr.ds.
Update(goqu.T("report")).
Set(goqu.Record{
"period": newPeriod,
"last_modified_at": updateTime,
}).
Where(goqu.I("id").Eq(rid)).
Prepared(true).ToSQL()
updateSummarySql, updateSummaryArgs, _ := rr.ds.
Update(goqu.T("report_summary")).
Set(goqu.Record{
"overall": model.ConsumptionUnit{Amount: form.Overall, Fee: form.OverallFee},
"critical": model.ConsumptionUnit{Amount: form.Critical, Fee: form.CriticalFee},
"peak": model.ConsumptionUnit{Amount: form.Peak, Fee: form.PeakFee},
"flat": model.ConsumptionUnit{Amount: form.Flat, Fee: form.FlatFee},
"valley": model.ConsumptionUnit{Amount: form.Valley, Fee: form.ValleyFee},
"basic_fee": form.BasicFee,
"adjust_fee": form.AdjustFee,
}).
Where(goqu.I("report_id").Eq(rid)).
Prepared(true).ToSQL()
resIndex, err := tx.Exec(ctx, udpateIndexSql, updateIndexArgs...)
if err != nil {
rr.log.Error("更新核算报表索引时出现错误", zap.Error(err))
tx.Rollback(ctx)
return false, err
}
if resIndex.RowsAffected() == 0 {
rr.log.Error("保存核算报表索引时出现错误", zap.Error(err))
tx.Rollback(ctx)
return false, exceptions.NewUnsuccessUpdateError("更新核算报表索引时出现错误")
}
resSummary, err := tx.Exec(ctx, updateSummarySql, updateSummaryArgs...)
if err != nil {
rr.log.Error("更新核算报表汇总时出现错误", zap.Error(err))
tx.Rollback(ctx)
return false, err
}
if resSummary.RowsAffected() == 0 {
rr.log.Error("保存核算报表汇总时出现错误", zap.Error(err))
tx.Rollback(ctx)
return false, exceptions.NewUnsuccessUpdateError("更新核算报表汇总时出现错误")
}
err = tx.Commit(ctx)
if err != nil {
rr.log.Error("提交核算报表更新事务时出现错误", zap.Error(err))
tx.Rollback(ctx)
return false, err
}
return resIndex.RowsAffected() > 0 && resSummary.RowsAffected() > 0, nil
}
// 获取指定报表的总览信息
func (rr _ReportRepository) RetrieveReportSummary(rid string) (*model.ReportSummary, error) {
rr.log.Info("获取指定报表的总览信息", zap.String("Report", rid))
ctx, cancel := global.TimeoutContext()
defer cancel()
querySql, queryParams, _ := rr.ds.
From(goqu.T("report_summary")).
Select("*").
Where(goqu.I("report_id").Eq(rid)).
Prepared(true).ToSQL()
var summary model.ReportSummary
if err := pgxscan.Get(ctx, global.DB, &summary, querySql, queryParams...); err != nil {
rr.log.Error("获取指定报表的总览信息时出现错误", zap.Error(err))
return nil, err
}
return &summary, nil
}
// 获取指定用户的尚未发布的核算报表的计算状态
func (rr _ReportRepository) GetReportTaskStatus(uid string) ([]*model.ReportTask, error) {
rr.log.Info("获取指定用户的尚未发布的核算报表的计算状态", zap.String("User", uid))
ctx, cancel := global.TimeoutContext()
defer cancel()
querySql, queryParams, _ := rr.ds.
From(goqu.T("report_task").As("t")).
Join(goqu.T("report").As("r"), goqu.On(goqu.I("r.id").Eq(goqu.I("t.id")))).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("r.park_id")))).
Select(
goqu.I("t.*"),
).
Where(
goqu.I("p.user_id").Eq(uid),
goqu.I("r.published").IsFalse(),
).
Prepared(true).ToSQL()
var tasks []*model.ReportTask = make([]*model.ReportTask, 0)
if err := pgxscan.Select(ctx, global.DB, &tasks, querySql, queryParams...); err != nil {
rr.log.Error("获取指定用户的尚未发布的核算报表的计算状态时出现错误", zap.Error(err))
return tasks, err
}
return tasks, nil
}
// 检索指定核算报表中园区公共表计的核算记录
func (rr _ReportRepository) ListPublicMetersInReport(rid string, page uint, keyword *string) ([]*model.ReportDetailedPublicConsumption, int64, error) {
rr.log.Info("检索指定核算报表中园区公共表计的核算记录", zap.String("Report", rid))
ctx, cancel := global.TimeoutContext()
defer cancel()
reportQuery := rr.ds.
From(goqu.T("report_public_consumption").As("r")).
Join(goqu.T("meter_04kv").As("m"), goqu.On(goqu.I("m.code").Eq(goqu.I("r.park_meter_id")))).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("m.park_id")))).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("b.id").Eq(goqu.I("m.building")))).
Select(
goqu.I("r.*"), goqu.I("b.name").As("building_name"), goqu.I("p.public_pooled"),
).
Where(goqu.I("r.report_id").Eq(rid))
countQuery := rr.ds.
From(goqu.T("report_public_consumption").As("r")).
Join(goqu.T("meter_04kv").As("m"), goqu.On(goqu.I("m.code").Eq(goqu.I("r.park_meter_id")))).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("m.park_id")))).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("b.id").Eq(goqu.I("m.building")))).
Select(goqu.COUNT(goqu.I("r.*"))).
Where(goqu.I("r.report_id").Eq(rid))
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
reportQuery = reportQuery.Where(goqu.Or(
goqu.I("m.code").ILike(pattern),
goqu.I("m.address").ILike(pattern),
))
countQuery = countQuery.Where(goqu.Or(
goqu.I("m.code").ILike(pattern),
goqu.I("m.address").ILike(pattern),
))
}
startRow := (page - 1) * config.ServiceSettings.ItemsPageSize
reportQuery = reportQuery.
Order(goqu.I("m.code").Asc()).
Offset(startRow).Limit(config.ServiceSettings.ItemsPageSize)
var (
consumptions []*model.ReportDetailedPublicConsumption = make([]*model.ReportDetailedPublicConsumption, 0)
count int64
)
querySql, queryArgs, _ := reportQuery.Prepared(true).ToSQL()
countSql, countArgs, _ := countQuery.Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &consumptions, querySql, queryArgs...); err != nil {
rr.log.Error("检索指定核算报表中园区公共表计的核算记录时出现错误", zap.Error(err))
return consumptions, 0, err
}
if err := pgxscan.Get(ctx, global.DB, &count, countSql, countArgs...); err != nil {
rr.log.Error("检索指定核算报表中园区公共表计的核算记录时出现错误", zap.Error(err))
return consumptions, 0, err
}
return consumptions, count, nil
}
// 检索指定核算报表中公摊表计的核算记录
func (rr _ReportRepository) ListPooledMetersInReport(rid string, page uint, keyword *string) ([]*model.ReportDetailedPooledConsumption, int64, error) {
rr.log.Info("检索指定核算报表中公摊表计的核算记录", zap.String("Report", rid))
ctx, cancel := global.TimeoutContext()
defer cancel()
reportQuery := rr.ds.
From(goqu.T("report_pooled_consumption").As("r")).
Join(goqu.T("meter_04kv").As("m"), goqu.On(goqu.I("m.code").Eq(goqu.I("r.pooled_meter_id")))).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("m.park_id")))).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("b.id").Eq(goqu.I("m.building")))).
Select(
goqu.I("r.*"), goqu.I("m.*"), goqu.I("b.name").As("building_name"), goqu.I("p.public_pooled"),
).
Where(goqu.I("r.report_id").Eq(rid))
countQuery := rr.ds.
From(goqu.T("report_pooled_consumption").As("r")).
Join(goqu.T("meter_04kv").As("m"), goqu.On(goqu.I("m.code").Eq(goqu.I("r.pooled_meter_id")))).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("m.park_id")))).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("b.id").Eq(goqu.I("m.building")))).
Select(goqu.COUNT(goqu.I("r.*"))).
Where(goqu.I("r.report_id").Eq(rid))
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
reportQuery = reportQuery.Where(goqu.Or(
goqu.I("m.code").ILike(pattern),
goqu.I("m.address").ILike(pattern),
))
countQuery = countQuery.Where(goqu.Or(
goqu.I("m.code").ILike(pattern),
goqu.I("m.address").ILike(pattern),
))
}
startRow := (page - 1) * config.ServiceSettings.ItemsPageSize
reportQuery = reportQuery.
Order(goqu.I("m.code").Asc()).
Offset(startRow).Limit(config.ServiceSettings.ItemsPageSize)
var (
consumptions []*model.ReportDetailedPooledConsumption = make([]*model.ReportDetailedPooledConsumption, 0)
count int64
)
querySql, queryArgs, _ := reportQuery.Prepared(true).ToSQL()
countSql, countArgs, _ := countQuery.Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &consumptions, querySql, queryArgs...); err != nil {
rr.log.Error("检索指定核算报表中公摊表计的核算记录时出现错误", zap.Error(err))
return consumptions, 0, err
}
if err := pgxscan.Get(ctx, global.DB, &count, countSql, countArgs...); err != nil {
rr.log.Error("检索指定核算报表中公摊表计的核算记录时出现错误", zap.Error(err))
return consumptions, 0, err
}
return consumptions, count, nil
}
// 列出指定核算报表中指定公共表计下参与公共表计费用分摊的表计及分摊详细
func (rr _ReportRepository) ListPooledMeterDetailInReport(rid, mid string) ([]*model.ReportDetailNestedMeterConsumption, error) {
rr.log.Info("列出指定核算报表中指定公共表计下参与公共表计费用分摊的表计及分摊详细", zap.String("Report", rid), zap.String("Meter", mid))
ctx, cancel := global.TimeoutContext()
defer cancel()
meterSql, meterArgs, _ := rr.ds.
From(goqu.T("report_pooled_consumption")).
Select("*").
Where(goqu.I("report_id").Eq(rid), goqu.I("pooled_meter_id").Eq(mid)).
Prepared(true).ToSQL()
var meter model.ReportPooledConsumption
if err := pgxscan.Get(ctx, global.DB, &meter, meterSql, meterArgs...); err != nil {
rr.log.Error("列出指定核算报表中指定公共表计下参与公共表计费用分摊的表计编号时出现错误", zap.Error(err))
return make([]*model.ReportDetailNestedMeterConsumption, 0), err
}
meterCodes := lo.Map(meter.Diluted, func(m model.NestedMeter, _ int) string {
return m.MeterId
})
meterSql, meterArgs, _ = rr.ds.
From(goqu.T("meter_04kv").As("m")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("b.id").Eq(goqu.I("m.building")))).
Select(
goqu.I("m.*"), goqu.I("b.name").As("building_name"),
).
Where(goqu.I("m.code").In(meterCodes)).
Prepared(true).ToSQL()
var meterDetails []*model.MeterDetail = make([]*model.MeterDetail, 0)
if err := pgxscan.Select(ctx, global.DB, &meterDetails, meterSql, meterArgs...); err != nil {
rr.log.Error("列出指定核算报表中指定公共表计下参与公共表计费用分摊的表计详细时出现错误", zap.Error(err))
return make([]*model.ReportDetailNestedMeterConsumption, 0), err
}
assembled := lo.Map(meter.Diluted, func(m model.NestedMeter, _ int) *model.ReportDetailNestedMeterConsumption {
meterDetail, _ := lo.Find(meterDetails, func(elem *model.MeterDetail) bool {
return elem.Code == m.MeterId
})
return &model.ReportDetailNestedMeterConsumption{
Meter: *meterDetail,
Consumption: m,
}
})
return assembled, nil
}
// 列出指定核算报表下商户的简要计费信息
func (rr _ReportRepository) ListTenementInReport(rid string, page uint, keyword *string) ([]*model.ReportTenement, int64, error) {
rr.log.Info("查询指定核算报表下的商户简要计费信息", zap.String("Report", rid))
ctx, cancel := global.TimeoutContext()
defer cancel()
reportQuery := rr.ds.
From(goqu.T("report_tenement").As("rt")).
Join(goqu.T("tenement").As("t"), goqu.On(goqu.I("t.id").Eq(goqu.I("rt.tenement_id")))).
Select("rt.*").
Where(goqu.I("rt.report_id").Eq(rid))
countQuery := rr.ds.
From(goqu.T("report_tenement").As("rt")).
Join(goqu.T("tenement").As("t"), goqu.On(goqu.I("t.id").Eq(goqu.I("rt.tenement_id")))).
Select(goqu.COUNT(goqu.I("rt.*"))).
Where(goqu.I("rt.report_id").Eq(rid))
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
reportQuery = reportQuery.Where(goqu.Or(
goqu.I("t.full_name").ILike(pattern),
goqu.T("t.short_name").ILike(pattern),
goqu.I("t.abbr").ILike(pattern),
goqu.I("t.address").ILike(pattern),
goqu.I("t.contact_name").ILike(pattern),
goqu.I("t.contact_phone").ILike(pattern),
))
countQuery = countQuery.Where(goqu.Or(
goqu.I("t.full_name").ILike(pattern),
goqu.T("t.short_name").ILike(pattern),
goqu.I("t.abbr").ILike(pattern),
goqu.I("t.address").ILike(pattern),
goqu.I("t.contact_name").ILike(pattern),
goqu.I("t.contact_phone").ILike(pattern),
))
}
startRow := (page - 1) * config.ServiceSettings.ItemsPageSize
reportQuery = reportQuery.
Order(goqu.I("t.moved_in_at").Asc()).
Offset(startRow).Limit(config.ServiceSettings.ItemsPageSize)
var (
tenements []*model.ReportTenement = make([]*model.ReportTenement, 0)
count int64
)
querySql, queryArgs, _ := reportQuery.Prepared(true).ToSQL()
countSql, countArgs, _ := countQuery.Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &tenements, querySql, queryArgs...); err != nil {
rr.log.Error("查询指定核算报表下的商户简要计费信息时出现错误", zap.Error(err))
return tenements, 0, err
}
if err := pgxscan.Get(ctx, global.DB, &count, countSql, countArgs...); err != nil {
rr.log.Error("查询指定核算报表下的商户简要计费信息总数量时出现错误", zap.Error(err))
return tenements, 0, err
}
return tenements, count, nil
}
// 获取指定核算报表中指定商户的详细核算信息
func (rr _ReportRepository) GetTenementDetailInReport(rid, tid string) (*model.ReportTenement, error) {
rr.log.Info("获取指定核算报表中指定商户的详细核算信息", zap.String("Report", rid), zap.String("Tenement", tid))
ctx, cancel := global.TimeoutContext()
defer cancel()
querySql, queryParams, _ := rr.ds.
From(goqu.T("report_tenement").As("rt")).
Select("rt.*").
Where(goqu.I("rt.report_id").Eq(rid), goqu.I("rt.tenement_id").Eq(tid)).
Prepared(true).ToSQL()
var tenement model.ReportTenement
if err := pgxscan.Get(ctx, global.DB, &tenement, querySql, queryParams...); err != nil {
rr.log.Error("获取指定核算报表中指定商户的详细核算信息时出现错误", zap.Error(err))
return nil, err
}
return &tenement, nil
}
// 更改指定核算报表的状态为已发布
func (rr _ReportRepository) PublishReport(rid string) (bool, error) {
rr.log.Info("更改指定核算报表的状态为已发布", zap.String("Report", rid))
ctx, cancel := global.TimeoutContext()
defer cancel()
currentTime := types.Now()
updateSql, updateArgs, _ := rr.ds.
Update(goqu.T("report")).
Set(goqu.Record{
"published": true,
"published_at": currentTime,
}).
Where(goqu.I("id").Eq(rid)).
Prepared(true).ToSQL()
res, err := global.DB.Exec(ctx, updateSql, updateArgs...)
if err != nil {
rr.log.Error("更改指定核算报表的状态为已发布时出现错误", zap.Error(err))
return false, err
}
return res.RowsAffected() > 0, nil
}
// 对指定核算报表进行综合检索
func (rr _ReportRepository) ComprehensiveReportSearch(uid, pid *string, page uint, keyword *string, start, end *types.Date) ([]*model.ReportIndex, int64, error) {
rr.log.Info("对指定核算报表进行综合检索", zap.Stringp("User", uid), zap.Stringp("Park", pid))
ctx, cancel := global.TimeoutContext()
defer cancel()
reportQuery := rr.ds.
From(goqu.T("report").As("r")).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("r.park_id")))).
Join(goqu.T("user_detail").As("ud"), goqu.On(goqu.I("ud.id").Eq(goqu.I("p.user_id")))).
LeftJoin(goqu.T("report_task").As("t"), goqu.On(goqu.I("t.id").Eq(goqu.I("r.id")))).
Select("r.*", goqu.I("t.status"), goqu.I("t.message"))
countQuery := rr.ds.
From(goqu.T("report").As("r")).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("r.park_id")))).
Join(goqu.T("user_detail").As("ud"), goqu.On(goqu.I("ud.id").Eq(goqu.I("p.user_id")))).
LeftJoin(goqu.T("report_task").As("t"), goqu.On(goqu.I("t.id").Eq(goqu.I("r.id")))).
Select(goqu.COUNT(goqu.I("r.*")))
if uid != nil && len(*uid) > 0 {
reportQuery = reportQuery.Where(goqu.I("ud.id").Eq(*uid))
countQuery = countQuery.Where(goqu.I("ud.id").Eq(*uid))
}
if pid != nil && len(*pid) > 0 {
reportQuery = reportQuery.Where(goqu.I("p.id").Eq(*pid))
countQuery = countQuery.Where(goqu.I("p.id").Eq(*pid))
}
queryDateRange := types.NewDateRange(start, end)
reportQuery = reportQuery.Where(goqu.L("r.period <@ ?", queryDateRange))
countQuery = countQuery.Where(goqu.L("r.period <@ ?", queryDateRange))
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
reportQuery = reportQuery.Where(goqu.Or(
goqu.I("p.name").ILike(pattern),
goqu.I("p.abbr").ILike(pattern),
goqu.I("p.address").ILike(pattern),
goqu.I("p.contact").ILike(pattern),
goqu.I("ud.name").ILike(pattern),
goqu.I("ud.abbr").ILike(pattern),
goqu.I("ud.contact").ILike(pattern),
))
countQuery = countQuery.Where(goqu.Or(
goqu.I("p.name").ILike(pattern),
goqu.I("p.abbr").ILike(pattern),
goqu.I("p.address").ILike(pattern),
goqu.I("p.contact").ILike(pattern),
goqu.I("ud.name").ILike(pattern),
goqu.I("ud.abbr").ILike(pattern),
goqu.I("ud.contact").ILike(pattern),
))
}
startRow := (page - 1) * config.ServiceSettings.ItemsPageSize
reportQuery = reportQuery.
Order(goqu.I("r.created_at").Desc()).
Offset(startRow).Limit(config.ServiceSettings.ItemsPageSize)
var (
reports []*model.ReportIndex = make([]*model.ReportIndex, 0)
count int64
)
querySql, queryArgs, _ := reportQuery.Prepared(true).ToSQL()
countSql, countArgs, _ := countQuery.Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &reports, querySql, queryArgs...); err != nil {
rr.log.Error("对指定核算报表进行综合检索时出现错误", zap.Error(err))
return reports, 0, err
}
if err := pgxscan.Get(ctx, global.DB, &count, countSql, countArgs...); err != nil {
rr.log.Error("对指定核算报表进行综合检索总数量时出现错误", zap.Error(err))
return reports, 0, err
}
return reports, count, nil
}
// 判断指定报表是否是当前园区的最后一张报表
func (rr _ReportRepository) IsLastReport(rid string) (bool, error) {
rr.log.Info("判断指定报表是否是当前园区的最后一张报表", zap.String("Report", rid))
ctx, cancel := global.TimeoutContext()
defer cancel()
checkSql, checkArgs, _ := rr.ds.
From(goqu.T("report")).
Select(goqu.COUNT("*")).
Where(
goqu.I("r.id").Eq(rid),
goqu.Func("lower", goqu.I("r.period")).Gte(rr.ds.
From(goqu.T("report").As("ri")).
Select(goqu.Func("max", goqu.Func("upper", goqu.I("ri.period")))).
Where(
goqu.I("ri.park_id").Eq(goqu.I("r.park_id")),
goqu.I("ri.id").Neq(rid),
),
),
goqu.I("r.published").IsTrue(),
).
Prepared(true).ToSQL()
var count int64
if err := pgxscan.Get(ctx, global.DB, &count, checkSql, checkArgs...); err != nil {
rr.log.Error("判断指定报表是否是当前园区的最后一张报表时出现错误", zap.Error(err))
return false, err
}
return count > 0, nil
}
// 申请撤回指定的核算报表
func (rr _ReportRepository) ApplyWithdrawReport(rid string) (bool, error) {
rr.log.Info("申请撤回指定的核算报表", zap.String("Report", rid))
ctx, cancel := global.TimeoutContext()
defer cancel()
currentTime := types.Now()
updateSql, updateArgs, _ := rr.ds.
Update(goqu.T("report")).
Set(goqu.Record{
"withdraw": model.REPORT_WITHDRAW_APPLYING,
"last_withdraw_applied_at": currentTime,
}).
Where(goqu.I("id").Eq(rid)).
Prepared(true).ToSQL()
res, err := global.DB.Exec(ctx, updateSql, updateArgs...)
if err != nil {
rr.log.Error("申请撤回指定的核算报表时出现错误", zap.Error(err))
return false, err
}
return res.RowsAffected() > 0, nil
}
// 批准核算报表的撤回申请
func (rr _ReportRepository) ApproveWithdrawReport(rid string) (bool, error) {
rr.log.Info("批准核算报表的撤回申请", zap.String("Report", rid))
ctx, cancel := global.TimeoutContext()
defer cancel()
currentTime := types.Now()
updateSql, updateArgs, _ := rr.ds.
Update(goqu.T("report")).
Set(goqu.Record{
"withdraw": model.REPORT_WITHDRAW_GRANTED,
"last_withdraw_audit_at": currentTime,
"published": false,
"published_at": nil,
}).
Where(goqu.I("id").Eq(rid)).
Prepared(true).ToSQL()
res, err := global.DB.Exec(ctx, updateSql, updateArgs...)
if err != nil {
rr.log.Error("批准核算报表的撤回申请时出现错误", zap.Error(err))
return false, err
}
return res.RowsAffected() > 0, nil
}
// 拒绝核算报表的撤回申请
func (rr _ReportRepository) RejectWithdrawReport(rid string) (bool, error) {
rr.log.Info("拒绝核算报表的撤回申请", zap.String("Report", rid))
ctx, cancel := global.TimeoutContext()
defer cancel()
currentTime := types.Now()
updateSql, updateArgs, _ := rr.ds.
Update(goqu.T("report")).
Set(goqu.Record{
"withdraw": model.REPORT_WITHDRAW_DENIED,
"last_withdraw_audit_at": currentTime,
}).
Where(goqu.I("id").Eq(rid)).
Prepared(true).ToSQL()
res, err := global.DB.Exec(ctx, updateSql, updateArgs...)
if err != nil {
rr.log.Error("拒绝核算报表的撤回申请时出现错误", zap.Error(err))
return false, err
}
return res.RowsAffected() > 0, nil
}
// 列出当前正在等待审核的已经申请撤回的核算报表
func (rr _ReportRepository) ListWithdrawAppliedReports(page uint, keyword *string) ([]*model.ReportIndex, int64, error) {
rr.log.Info("列出当前正在等待审核的已经申请撤回的核算报表")
ctx, cancel := global.TimeoutContext()
defer cancel()
reportQuery := rr.ds.
From(goqu.T("report").As("r")).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("r.park_id")))).
Join(goqu.T("user_detail").As("ud"), goqu.On(goqu.I("ud.id").Eq(goqu.I("p.user_id")))).
LeftJoin(goqu.T("report_task").As("t"), goqu.On(goqu.I("t.id").Eq(goqu.I("r.id")))).
Select("r.*", goqu.I("t.status"), goqu.I("t.message")).
Where(goqu.I("r.withdraw").Eq(model.REPORT_WITHDRAW_APPLYING))
countQuery := rr.ds.
From(goqu.T("report").As("r")).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("r.park_id")))).
Join(goqu.T("user_detail").As("ud"), goqu.On(goqu.I("ud.id").Eq(goqu.I("p.user_id")))).
LeftJoin(goqu.T("report_task").As("t"), goqu.On(goqu.I("t.id").Eq(goqu.I("r.id")))).
Select(goqu.COUNT(goqu.I("r.*"))).
Where(goqu.I("r.withdraw").Eq(model.REPORT_WITHDRAW_APPLYING))
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
reportQuery = reportQuery.Where(goqu.Or(
goqu.I("p.name").ILike(pattern),
goqu.I("p.abbr").ILike(pattern),
goqu.I("p.address").ILike(pattern),
goqu.I("p.contact").ILike(pattern),
goqu.I("p.phone").ILike(pattern),
goqu.I("ud.name").ILike(pattern),
goqu.I("ud.abbr").ILike(pattern),
goqu.I("ud.contact").ILike(pattern),
goqu.I("ud.phone").ILike(pattern),
))
countQuery = countQuery.Where(goqu.Or(
goqu.I("p.name").ILike(pattern),
goqu.I("p.abbr").ILike(pattern),
goqu.I("p.address").ILike(pattern),
goqu.I("p.contact").ILike(pattern),
goqu.I("p.phone").ILike(pattern),
goqu.I("ud.name").ILike(pattern),
goqu.I("ud.abbr").ILike(pattern),
goqu.I("ud.contact").ILike(pattern),
goqu.I("ud.phone").ILike(pattern),
))
}
startRow := (page - 1) * config.ServiceSettings.ItemsPageSize
reportQuery = reportQuery.
Order(goqu.I("r.last_withdraw_applied_at").Desc()).
Offset(startRow).Limit(config.ServiceSettings.ItemsPageSize)
var (
reports []*model.ReportIndex = make([]*model.ReportIndex, 0)
count int64
)
querySql, queryArgs, _ := reportQuery.Prepared(true).ToSQL()
countSql, countArgs, _ := countQuery.Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &reports, querySql, queryArgs...); err != nil {
rr.log.Error("列出当前正在等待审核的已经申请撤回的核算报表时出现错误", zap.Error(err))
return reports, 0, err
}
if err := pgxscan.Get(ctx, global.DB, &count, countSql, countArgs...); err != nil {
rr.log.Error("列出当前正在等待审核的已经申请撤回的核算报表总数量时出现错误", zap.Error(err))
return reports, 0, err
}
return reports, count, nil
}

208
repository/synchronize.go Normal file
View File

@ -0,0 +1,208 @@
package repository
import (
"context"
"electricity_bill_calc/config"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/tools"
"electricity_bill_calc/vo"
"fmt"
"github.com/doug-martin/goqu/v9"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
"go.uber.org/zap"
"strconv"
)
type _SynchronizeRepository struct {
log *zap.Logger
ds goqu.DialectWrapper
}
var SynchronizeRepository = _SynchronizeRepository{
log: logger.Named("Repository", "Synchronize"),
ds: goqu.Dialect("postgres"),
}
func (sr _SynchronizeRepository) SearchSynchronizeSchedules(userId *string, parkId *string, page uint, keyword *string) ([]*model.SynchronizeSchedule, int64, error) {
sr.log.Info("检索符合指定条件的同步记录", zap.String("user id", tools.DefaultTo(userId, "")),
zap.String("park id", tools.DefaultTo(parkId, "")), zap.Uint("page", page),
zap.String("keyword", tools.DefaultTo(keyword, "")))
ctx, cancelFunc := global.TimeoutContext()
defer cancelFunc()
//scheduleQuery := "select ss.*, ud.name as user_name, p.name as park_name from synchronize_schedule as ss
//join park as p on p.id=ss.park_id join user_detail as ud on ud.id=ss.user_id where 1=1"
schedulequery := sr.ds.From(goqu.T("synchronize_schedule").As("ss")).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("ss.park_id")))).
Join(goqu.T("user_detail").As("ud"), goqu.On(goqu.I("ud.id").Eq(goqu.I("ss.user_id")))).
Select("ss.*", goqu.I("ud.name").As("user_name"), goqu.I("p.name").As("park_name"))
//countQuery := "select count(ss.*) from synchronize_schedule as ss
//join park as p on p.id=ss.park_id join user_detail as ud on ud.id=ss.user_id where 1=1"
countquery := sr.ds.From(goqu.T("synchronize_schedule").As("ss")).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("ss.park_id")))).
Join(goqu.T("user_detail").As("ud"), goqu.On(goqu.I("ud.id").Eq(goqu.I("ss.user_id")))).
Select(goqu.COUNT(goqu.I("ss.*")))
if userId != nil && len(*userId) > 0 {
schedulequery = schedulequery.Where(goqu.I("ss.user_id").Eq(*userId))
countquery = countquery.Where(goqu.I("ss.user_id").Eq(*userId))
}
if parkId != nil && len(*parkId) > 0 {
schedulequery = schedulequery.Where(goqu.I("ss.park_id").Eq(*parkId))
countquery = countquery.Where(goqu.I("ss.park_id").Eq(*parkId))
}
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
schedulequery = schedulequery.Where(goqu.Or(
goqu.I("p.name").ILike(pattern),
goqu.I("p.abbr").ILike(pattern),
goqu.I("p.address").ILike(pattern),
goqu.I("p.contact").ILike(pattern),
goqu.I("p.phone").ILike(pattern),
goqu.I("ud.name").ILike(pattern),
goqu.I("ud.abbr").ILike(pattern),
goqu.I("ud.contact").ILike(pattern),
goqu.I("ud.phone").ILike(pattern),
goqu.I("ss.task_name").ILike(pattern),
goqu.I("ss.task_description").ILike(pattern),
))
countquery = countquery.Where(goqu.Or(
goqu.I("p.name").ILike(pattern),
goqu.I("p.abbr").ILike(pattern),
goqu.I("p.address").ILike(pattern),
goqu.I("p.contact").ILike(pattern),
goqu.I("ud.name").ILike(pattern),
goqu.I("ud.abbr").ILike(pattern),
goqu.I("ud.contact").ILike(pattern),
goqu.I("ud.phone").ILike(pattern),
goqu.I("ss.task_name").ILike(pattern),
goqu.I("ss.task_description").ILike(pattern),
))
}
startRow := (page - 1) * config.ServiceSettings.ItemsPageSize
schedulequery = schedulequery.
Order(goqu.I("ss.created_at").Desc()).
Offset(startRow).Limit(config.ServiceSettings.ItemsPageSize)
var (
schedule []*model.SynchronizeSchedule = make([]*model.SynchronizeSchedule, 0)
count int64
)
querySql, queryArgs, _ := schedulequery.Prepared(true).ToSQL()
countSql, countArgs, _ := countquery.Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &schedule, querySql, queryArgs...); err != nil {
sr.log.Error("获取同步任务时出现错误", zap.Error(err))
return schedule, 0, err
}
if err := pgxscan.Get(ctx, global.DB, &count, countSql, countArgs...); err != nil {
sr.log.Error("检索同步任务总数量时出现错误", zap.Error(err))
return schedule, 0, err
}
return schedule, count, nil
}
// From("synchronize_schedule").
//
// Select(
// goqu.I("synchronize_schedule.*"),
// goqu.I("user_detail.name").As("user_name"),
// goqu.I("park.name").As("park_name"),
// ).
// Join(
// goqu.T("park").On(goqu.I("park.id").Eq(goqu.I("synchronize_schedule.park_id"))),
// goqu.T("user_detail").On(goqu.I("user_detail.id").Eq(goqu.I("synchronize_schedule.user_id"))),
// ).
// Where(goqu.C("1").Eq(1))
//
// SELECT count(ss.*)
// FROM synchronize_schedule AS ss
// JOIN park AS p ON p.id = ss.park_id
// JOIN user_detail AS ud ON ud.id = ss.user_id
// WHERE true`
//
// var args []interface{}
//
// if uid != nil {
// scheduleQuery += " AND ss.user_id = $1"
// countQuery += " AND ss.user_id = $1"
// args = append(args, *uid)
// }
//
// if pid != nil {
// scheduleQuery += " AND ss.park_id = $2"
// countQuery += " AND ss.park_id = $2"
// args = append(args, *pid)
// }
//
// if keyword != nil {
// pattern := "%" + *keyword + "%"
// scheduleQuery += ` AND (p.name LIKE $3 OR p.abbr LIKE $3 OR p.address LIKE $3 OR p.contact LIKE $3 OR
//
// p.phone LIKE $3 OR ud.name LIKE $3 OR ud.abbr LIKE $3 OR ud.contact LIKE $3 OR
// ud.phone LIKE $3 OR ss.task_name LIKE $3 OR ss.task_description LIKE $3)`
//
// args = append(args, pattern)
// }
func (sr _SynchronizeRepository) RetrieveSynchronizeConfiguration(uId, pId string) (vo.SynchronizeConfiguration, error) {
sr.log.Info("检索符合指定条件的同步记录", zap.String("user id", uId), zap.String("park id", pId))
ctx, cancelFunc := global.TimeoutContext()
defer cancelFunc()
//select * from synchronize_config where user_id=$1 and park_id=$2
configSql, configArgs, _ := sr.ds.
From(goqu.T("synchronize_config")).
Where(goqu.I("user_id").Eq(uId)).
Where(goqu.I("park_id").Eq(pId)).
Prepared(true).Select("*").ToSQL()
fmt.Println(configSql)
var configs []model.SynchronizeConfiguration
if err := pgxscan.Select(ctx, global.DB, &configs, configSql, configArgs...); err != nil {
fmt.Println(err)
sr.log.Error("获取同步任务时出现错误", zap.Error(err))
return vo.SynchronizeConfiguration{}, err
}
if len(configs) <= 0 {
return vo.SynchronizeConfiguration{}, nil
}
maxr := strconv.Itoa(int(configs[0].MaxRetries))
retry := strconv.Itoa(int(configs[0].RetryInterval))
synconfig := vo.SynchronizeConfiguration{
CollectAt: configs[0].CollectAt.Format("15:04:05"),
EntID: configs[0].User,
Imrs: configs[0].ImrsType,
ImrsAccount: configs[0].AuthorizationAccount,
ImrsKey: string(configs[0].AuthorizationKey),
ImrsSecret: configs[0].AuthorizationSecret,
Interval: float64(configs[0].Interval),
MaxRetries: maxr,
ParkID: configs[0].Park,
ReadingType: float64(configs[0].MeterReadingType),
RetryAlgorithm: float64(configs[0].RetryIntervalAlgorithm),
RetryInterval: retry,
}
return synconfig, nil
}
func (sr _SynchronizeRepository) CreateSynchronizeConfiguration(tx pgx.Tx, ctx context.Context, uId string, form *vo.SynchronizeConfigurationCreateForm) (bool, error) {
sr.log.Info("创建新的同步用户配置", zap.String("user Id", uId))
ctx, cancel := global.TimeoutContext()
defer cancel()
//insert into synchronize_config (user_id, park_id, meter_reading_type, imrs_type, imrs_authorization_account,
// imrs_authorization_secret, imrs_authorization_key, interval, collect_at, max_retries, retry_interval, retry_interval_algorithm) values
configSql, configArgs, _ := sr.ds.
Insert(goqu.T("synchronize_config")).
Cols(
"user_id", "park_id", "meter_reading_type", "imrs_type", "imrs_authorization_account", "imrs_authorization_secret",
"imrs_authorization_key", "interval", "collect_at", "max_retries",
"retry_interval", "retry_interval_algorithm").
Vals(
goqu.Vals{uId, form.ParkID, form.ReadingType, form.Imrs, form.ImrsAccount, form.ImrsSecret, form.ImrsKey, form.Interval,
form.CollectAt, form.MaxRetries, form.RetryInterval, form.RetryAlgorithm,
},
).
Prepared(true).ToSQL()
ok, err := tx.Exec(ctx, configSql, configArgs...)
if err != nil {
sr.log.Error("创建同步配置信息失败", zap.Error(err))
return false, err
}
return ok.RowsAffected() > 0, nil
}

458
repository/tenement.go Normal file
View File

@ -0,0 +1,458 @@
package repository
import (
"context"
"electricity_bill_calc/config"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/tools"
"electricity_bill_calc/tools/serial"
"electricity_bill_calc/types"
"electricity_bill_calc/vo"
"fmt"
"github.com/doug-martin/goqu/v9"
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
"go.uber.org/zap"
)
type _TenementRepository struct {
log *zap.Logger
ds goqu.DialectWrapper
}
var TenementRepository = _TenementRepository{
log: logger.Named("Repository", "Tenement"),
ds: goqu.Dialect("postgres"),
}
// 判断指定商户是否属于指定用户的管辖
func (tr _TenementRepository) IsTenementBelongs(tid, uid string) (bool, error) {
tr.log.Info("检查指定商户是否属于指定企业管辖", zap.String("Tenement", tid), zap.String("Enterprise", uid))
ctx, cancel := global.TimeoutContext()
defer cancel()
countSql, countArgs, _ := tr.ds.
From(goqu.T("tenement").As("t")).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("t.park_id").Eq(goqu.I("p.id")))).
Select(goqu.COUNT("t.*")).
Where(
goqu.I("t.id").Eq(tid),
goqu.I("p.user_id").Eq(uid),
).
Prepared(true).ToSQL()
var count int
if err := pgxscan.Get(ctx, global.DB, &count, countSql, countArgs...); err != nil {
tr.log.Error("检查指定商户是否属于指定企业管辖失败", zap.Error(err))
return false, err
}
return count > 0, nil
}
// 列出指定园区中的所有商户
func (tr _TenementRepository) ListTenements(pid string, page uint, keyword, building *string, startDate, endDate *types.Date, state int) ([]*model.Tenement, int64, error) {
tr.log.Info(
"检索查询指定园区中符合条件的商户",
zap.String("Park", pid),
zap.Uint("Page", page),
zap.Stringp("Keyword", keyword),
zap.Stringp("Building", building),
logger.DateFieldp("StartDate", startDate),
logger.DateFieldp("EndDate", endDate),
zap.Int("State", state),
)
ctx, cancel := global.TimeoutContext()
defer cancel()
tenementQuery := tr.ds.
From(goqu.T("tenement").As("t")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("b.id").Eq(goqu.I("t.building")))).
Select("t.*", goqu.I("b.name").As("building_name")).
Where(goqu.I("t.park_id").Eq(pid))
countQuery := tr.ds.
From(goqu.T("tenement").As("t")).
Select(goqu.COUNT("t.*")).
Where(goqu.I("t.park_id").Eq(pid))
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
tenementQuery = tenementQuery.Where(
goqu.Or(
goqu.I("t.full_name").ILike(pattern),
goqu.I("t.short_name").ILike(pattern),
goqu.I("t.abbr").ILike(pattern),
goqu.I("t.contact_name").ILike(pattern),
goqu.I("t.contact_phone").ILike(pattern),
goqu.I("t.address").ILike(pattern),
),
)
countQuery = countQuery.Where(
goqu.Or(
goqu.I("t.full_name").ILike(pattern),
goqu.I("t.short_name").ILike(pattern),
goqu.I("t.abbr").ILike(pattern),
goqu.I("t.contact_name").ILike(pattern),
goqu.I("t.contact_phone").ILike(pattern),
goqu.I("t.address").ILike(pattern),
),
)
}
if building != nil && len(*building) > 0 {
tenementQuery = tenementQuery.Where(goqu.I("t.building").Eq(*building))
countQuery = countQuery.Where(goqu.I("t.building").Eq(*building))
}
if startDate != nil {
tenementQuery = tenementQuery.Where(
goqu.Or(
goqu.I("t.moved_in_at").Gte(startDate.ToBeginningOfDate()),
goqu.I("t.moved_out_at").Gte(startDate.ToBeginningOfDate()),
),
)
countQuery = countQuery.Where(
goqu.Or(
goqu.I("t.moved_in_at").Gte(startDate.ToBeginningOfDate()),
goqu.I("t.moved_out_at").Gte(startDate.ToBeginningOfDate()),
),
)
}
if endDate != nil {
tenementQuery = tenementQuery.Where(
goqu.Or(
goqu.I("t.moved_in_at").Lte(endDate.ToEndingOfDate()),
goqu.I("t.moved_out_at").Lte(endDate.ToEndingOfDate()),
),
)
countQuery = countQuery.Where(
goqu.Or(
goqu.I("t.moved_in_at").Lte(endDate.ToEndingOfDate()),
goqu.I("t.moved_out_at").Lte(endDate.ToEndingOfDate()),
),
)
}
if state == 0 {
tenementQuery = tenementQuery.Where(
goqu.I("t.moved_out_at").IsNull(),
)
countQuery = countQuery.Where(
goqu.I("t.moved_out_at").IsNull(),
)
} else {
tenementQuery = tenementQuery.Where(
goqu.I("t.moved_out_at").IsNotNull(),
)
countQuery = countQuery.Where(
goqu.I("t.moved_out_at").IsNotNull(),
)
}
startRow := (page - 1) * config.ServiceSettings.ItemsPageSize
tenementQuery = tenementQuery.Order(goqu.I("t.created_at").Desc()).Limit(config.ServiceSettings.ItemsPageSize).Offset(startRow)
tenementSql, tenementArgs, _ := tenementQuery.Prepared(true).ToSQL()
countSql, countArgs, _ := countQuery.Prepared(true).ToSQL()
var (
tenements []*model.Tenement = make([]*model.Tenement, 0)
total int64
)
if err := pgxscan.Select(ctx, global.DB, &tenements, tenementSql, tenementArgs...); err != nil {
tr.log.Error("检索查询指定园区中符合条件的商户失败", zap.Error(err))
return tenements, 0, err
}
if err := pgxscan.Get(ctx, global.DB, &total, countSql, countArgs...); err != nil {
tr.log.Error("检索查询指定园区中符合条件的商户总数量失败", zap.Error(err))
return tenements, 0, err
}
return tenements, total, nil
}
// 查询指定园区中某一商户下的所有表计编号,不包含公摊表计
func (tr _TenementRepository) ListMeterCodesBelongsTo(pid, tid string) ([]string, error) {
tr.log.Info("查询指定商户下所有的表计编号", zap.String("Park", pid), zap.String("Tenement", tid))
ctx, cancel := global.TimeoutContext()
defer cancel()
sql, args, _ := tr.ds.
From("tenement_meter").
Select("meter_id").
Where(
goqu.I("park_id").Eq(pid),
goqu.I("tenement_id").Eq(tid),
goqu.I("disassociated_at").IsNull(),
).
Prepared(true).ToSQL()
var meterCodes []string = make([]string, 0)
if err := pgxscan.Select(ctx, global.DB, &meterCodes, sql, args...); err != nil {
tr.log.Error("查询指定商户下所有的表计编号失败", zap.Error(err))
return meterCodes, err
}
return meterCodes, nil
}
// 在指定园区中创建一个新的商户
func (tr _TenementRepository) AddTenement(tx pgx.Tx, ctx context.Context, pid string, tenement *vo.TenementCreationForm) error {
tr.log.Info("在指定园区中创建一个新的商户", zap.String("Park", pid))
serial.StringSerialRequestChan <- 1
tenementId := serial.Prefix("T", <-serial.StringSerialResponseChan)
currentTime := types.Now()
if _, err := tx.Exec(
ctx,
"INSERT INTO tenement (id, park_id, full_name, short_name, abbr, address, contact_name, contact_phone, building, on_floor, invoice_info, moved_in_at, created_at, last_modified_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)",
[]interface{}{
tenementId,
pid,
tenement.Name,
tenement.ShortName,
tools.PinyinAbbr(tenement.Name),
tenement.Address,
tenement.Contact,
tenement.Phone,
tenement.Building,
tenement.OnFloor,
&model.InvoiceTitle{
Name: tenement.Name,
USCI: tenement.USCI,
Address: tools.DefaultOrEmptyStr(tenement.InvoiceAddress, ""),
Phone: tools.DefaultOrEmptyStr(tenement.InvoicePhone, ""),
Bank: tools.DefaultOrEmptyStr(tenement.Bank, ""),
Account: tools.DefaultOrEmptyStr(tenement.Account, ""),
},
currentTime,
currentTime,
currentTime,
}...,
); err != nil {
tr.log.Error("在指定园区中创建一个新的商户失败", zap.Error(err))
return err
}
return nil
}
// 向园区中指定商户下绑定一个新的表计
func (tr _TenementRepository) BindMeter(tx pgx.Tx, ctx context.Context, pid, tid, meter string) error {
tr.log.Info("向园区中指定商户下绑定一个新的表计", zap.String("Park", pid), zap.String("Tenement", tid), zap.String("Meter", meter))
createSql, createArgs, _ := tr.ds.
Insert("tenement_meter").
Cols(
"park_id", "tenement_id", "meter_id", "associated_at",
).
Vals(
goqu.Vals{
pid,
tid,
meter,
types.Now(),
},
).
Prepared(true).ToSQL()
if _, err := tx.Exec(ctx, createSql, createArgs...); err != nil {
tr.log.Error("向园区中指定商户下绑定一个新的表计失败", zap.Error(err))
return err
}
return nil
}
// 将指定商户与指定表计解绑
func (tr _TenementRepository) UnbindMeter(tx pgx.Tx, ctx context.Context, pid, tid, meter string) error {
tr.log.Info("将指定商户与指定表计解绑", zap.String("Park", pid), zap.String("Tenement", tid), zap.String("Meter", meter))
updateSql, updateArgs, _ := tr.ds.
Update("tenement_meter").
Set(
goqu.Record{
"disassociated_at": types.Now(),
},
).
Where(
goqu.I("park_id").Eq(pid),
goqu.I("tenement_id").Eq(tid),
goqu.I("meter_id").Eq(meter),
).
Prepared(true).ToSQL()
if _, err := tx.Exec(ctx, updateSql, updateArgs...); err != nil {
tr.log.Error("将指定商户与指定表计解绑失败", zap.Error(err))
return err
}
return nil
}
// 修改指定商户的信息
func (tr _TenementRepository) UpdateTenement(pid, tid string, tenement *vo.TenementCreationForm) error {
tr.log.Info("修改指定商户的信息", zap.String("Park", pid), zap.String("Tenement", tid))
ctx, cancel := global.TimeoutContext()
defer cancel()
updateSql, updateArgs, _ := tr.ds.
Update("tenement").
Set(
goqu.Record{
"full_name": tenement.Name,
"short_name": tenement.ShortName,
"abbr": tools.PinyinAbbr(tenement.Name),
"address": tenement.Address,
"contact_name": tenement.Contact,
"contact_phone": tenement.Phone,
"building": tenement.Building,
"on_floor": tenement.OnFloor,
"invoice_info": &model.InvoiceTitle{
Name: tenement.Name,
USCI: tenement.USCI,
Address: tools.DefaultOrEmptyStr(tenement.InvoiceAddress, ""),
Phone: tools.DefaultOrEmptyStr(tenement.InvoicePhone, ""),
Bank: tools.DefaultOrEmptyStr(tenement.Bank, ""),
Account: tools.DefaultOrEmptyStr(tenement.Account, ""),
},
"last_modified_at": types.Now(),
},
).
Where(
goqu.I("id").Eq(tid),
goqu.I("park_id").Eq(pid),
).
Prepared(true).ToSQL()
if _, err := global.DB.Exec(ctx, updateSql, updateArgs...); err != nil {
tr.log.Error("修改指定商户的信息失败", zap.Error(err))
return err
}
return nil
}
// 迁出指定商户
func (tr _TenementRepository) MoveOut(tx pgx.Tx, ctx context.Context, pid, tid string) error {
tr.log.Info("迁出指定商户", zap.String("Park", pid), zap.String("Tenement", tid))
updateSql, updateArgs, _ := tr.ds.
Update("tenement").
Set(
goqu.Record{
"moved_out_at": types.Now(),
},
).
Where(
goqu.I("id").Eq(tid),
goqu.I("park_id").Eq(pid),
).
Prepared(true).ToSQL()
if _, err := tx.Exec(ctx, updateSql, updateArgs...); err != nil {
tr.log.Error("迁出指定商户失败", zap.Error(err))
return err
}
return nil
}
// 列出用于下拉列表的符合指定条件的商户信息
func (tr _TenementRepository) ListForSelect(uid string, pid, keyword *string, limit *uint) ([]*model.Tenement, error) {
tr.log.Info("列出用于下拉列表的符合指定条件的商户信息", zap.String("Ent", uid), zap.String("Park", tools.DefaultOrEmptyStr(pid, "All")), zap.Stringp("Keyword", keyword), zap.Uintp("Limit", limit))
ctx, cancel := global.TimeoutContext()
defer cancel()
tenementQuery := tr.ds.
From(goqu.T("tenement").As("t")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("b.id").Eq(goqu.I("t.building")))).
Join(goqu.T("park").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("t.park_id")))).
Select(
"t.*", goqu.I("b.name").As("building_name"),
).
Where(
goqu.I("p.user_id").Eq(uid),
goqu.I("t.moved_out_at").IsNull(),
)
if pid != nil && len(*pid) > 0 {
tenementQuery = tenementQuery.Where(goqu.I("p.id").Eq(*pid))
}
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
tenementQuery = tenementQuery.Where(
goqu.Or(
goqu.I("t.full_name").ILike(pattern),
goqu.I("t.short_name").ILike(pattern),
goqu.I("t.abbr").ILike(pattern),
),
)
}
tenementQuery = tenementQuery.Order(goqu.I("t.created_at").Desc())
if limit != nil && *limit > 0 {
tenementQuery = tenementQuery.Limit(*limit)
}
tenementSql, tenementArgs, _ := tenementQuery.Prepared(true).ToSQL()
var tenements = make([]*model.Tenement, 0)
if err := pgxscan.Select(ctx, global.DB, &tenements, tenementSql, tenementArgs...); err != nil {
tr.log.Error("列出用于下拉列表的符合指定条件的商户信息失败", zap.Error(err))
return tenements, err
}
return tenements, nil
}
// 列出指定园区中在指定时间区间内存在过入住的商户
func (tr _TenementRepository) ListTenementsInTimeRange(pid string, start, end types.Date) ([]*model.Tenement, error) {
tr.log.Info("列出指定园区中在指定时间区间内存在过入住的商户", zap.String("Park", pid), logger.DateField("Start", start), logger.DateField("End", end))
ctx, cancel := global.TimeoutContext()
defer cancel()
tenementQuery := tr.ds.
From(goqu.T("tenement").As("t")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("b.id").Eq(goqu.I("t.building")))).
Select(
"t.*", goqu.I("b.name").As("building_name"),
).
Where(
goqu.I("t.park_id").Eq(pid),
goqu.I("t.moved_in_at").Lte(end.ToEndingOfDate()),
goqu.Or(
goqu.I("t.moved_out_at").IsNull(),
goqu.I("t.moved_out_at").Gte(start.ToBeginningOfDate()),
),
).
Order(goqu.I("t.created_at").Desc())
tenementSql, tenementArgs, _ := tenementQuery.Prepared(true).ToSQL()
var tenements = make([]*model.Tenement, 0)
if err := pgxscan.Select(ctx, global.DB, &tenements, tenementSql, tenementArgs...); err != nil {
tr.log.Error("列出指定园区中在指定时间区间内存在过入住的商户失败", zap.Error(err))
return tenements, err
}
return tenements, nil
}
// 获取指定园区中指定商户的详细信息
func (tr _TenementRepository) RetrieveTenementDetail(pid, tid string) (*model.Tenement, error) {
tr.log.Info("获取指定园区中指定商户的详细信息", zap.String("Park", pid), zap.String("Tenement", tid))
ctx, cancel := global.TimeoutContext()
defer cancel()
tenementSql, tenementArgs, _ := tr.ds.
From(goqu.T("tenement").As("t")).
LeftJoin(goqu.T("park_building").As("b"), goqu.On(goqu.I("b.id").Eq(goqu.I("t.building")))).
Select(
"t.*", goqu.I("b.name").As("building_name"),
).
Where(
goqu.I("t.id").Eq(tid),
goqu.I("t.park_id").Eq(pid),
).
Prepared(true).ToSQL()
var tenement model.Tenement
if err := pgxscan.Get(ctx, global.DB, &tenement, tenementSql, tenementArgs...); err != nil {
tr.log.Error("获取指定园区中指定商户的详细信息失败", zap.Error(err))
return nil, err
}
return &tenement, nil
}

166
repository/top_up.go Normal file
View File

@ -0,0 +1,166 @@
package repository
import (
"electricity_bill_calc/config"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/tools/serial"
"electricity_bill_calc/types"
"electricity_bill_calc/vo"
"fmt"
"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 _TopUpRepository struct {
log *zap.Logger
ds goqu.DialectWrapper
}
var TopUpRepository = _TopUpRepository{
log: logger.Named("Repository", "TopUp"),
ds: goqu.Dialect("postgres"),
}
// 检索符合条件的商户充值记录
func (tur _TopUpRepository) ListTopUps(pid string, startDate, endDate *types.Date, keyword *string, page uint) ([]*model.TopUp, int64, error) {
tur.log.Info("查询符合条件的商户充值记录", zap.String("Park", pid), logger.DateFieldp("StartDate", startDate), logger.DateFieldp("EndDate", endDate), zap.Stringp("keyword", keyword), zap.Uint("page", page))
ctx, cancel := global.TimeoutContext()
defer cancel()
topUpQuery := tur.ds.
From(goqu.T("tenement_top_ups").As("t")).
LeftJoin(goqu.T("tenement").As("te"), goqu.On(goqu.I("te.id").Eq(goqu.I("t.tenement_id")), goqu.I("te.park_id").Eq(goqu.I("t.park_id")))).
LeftJoin(goqu.T("meter_04kv").As("m"), goqu.On(goqu.I("m.code").Eq(goqu.I("t.meter_id")), goqu.I("m.park_id").Eq(goqu.I("t.park_id")))).
Select("t.*", goqu.I("te.full_name").As("tenement_name"), goqu.I("m.address").As("meter_address")).
Where(goqu.I("t.park_id").Eq(pid))
countQuery := tur.ds.
From(goqu.T("tenement_top_ups").As("t")).
LeftJoin(goqu.T("tenement").As("te"), goqu.On(goqu.I("te.id").Eq(goqu.I("t.tenement_id")), goqu.I("te.park_id").Eq(goqu.I("t.park_id")))).
LeftJoin(goqu.T("meter_04kv").As("m"), goqu.On(goqu.I("m.code").Eq(goqu.I("t.meter_id")), goqu.I("m.park_id").Eq(goqu.I("t.park_id")))).
Select(goqu.COUNT("t.*")).
Where(goqu.I("t.park_id").Eq(pid))
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
topUpQuery = topUpQuery.Where(goqu.Or(
goqu.I("te.full_name").ILike(pattern),
goqu.I("te.short_name").ILike(pattern),
goqu.I("te.abbr").ILike(pattern),
goqu.I("m.code").ILike(pattern),
goqu.I("m.address").ILike(pattern),
))
countQuery = countQuery.Where(goqu.Or(
goqu.I("te.full_name").ILike(pattern),
goqu.I("te.short_name").ILike(pattern),
goqu.I("te.abbr").ILike(pattern),
goqu.I("m.code").ILike(pattern),
goqu.I("m.address").ILike(pattern),
))
}
if startDate != nil {
topUpQuery = topUpQuery.Where(goqu.I("t.topped_up_at").Gte(startDate.ToBeginningOfDate()))
countQuery = countQuery.Where(goqu.I("t.topped_up_at").Gte(startDate.ToBeginningOfDate()))
}
if endDate != nil {
topUpQuery = topUpQuery.Where(goqu.I("t.topped_up_at").Lte(endDate.ToEndingOfDate()))
countQuery = countQuery.Where(goqu.I("t.topped_up_at").Lte(endDate.ToEndingOfDate()))
}
startRow := (page - 1) * config.ServiceSettings.ItemsPageSize
topUpQuery = topUpQuery.Order(goqu.I("t.topped_up_at").Desc()).Offset(startRow).Limit(config.ServiceSettings.ItemsPageSize)
topUpSql, topUpArgs, _ := topUpQuery.Prepared(true).ToSQL()
countSql, countArgs, _ := countQuery.Prepared(true).ToSQL()
var (
topUps []*model.TopUp = make([]*model.TopUp, 0)
total int64 = 0
)
if err := pgxscan.Select(ctx, global.DB, &topUps, topUpSql, topUpArgs...); err != nil {
tur.log.Error("查询商户充值记录失败", zap.Error(err))
return topUps, 0, err
}
if err := pgxscan.Get(ctx, global.DB, &total, countSql, countArgs...); err != nil {
tur.log.Error("查询商户充值记录总数失败", zap.Error(err))
return topUps, 0, err
}
return topUps, total, nil
}
// 取得一个充值记录的详细信息
func (tur _TopUpRepository) GetTopUp(pid, topUpCode string) (*model.TopUp, error) {
tur.log.Info("查询充值记录", zap.String("Park", pid), zap.String("TopUpCode", topUpCode))
ctx, cancel := global.TimeoutContext()
defer cancel()
topUpSql, topUpArgs, _ := tur.ds.
From(goqu.T("tenement_top_ups").As("t")).
LeftJoin(goqu.T("tenement").As("te"), goqu.On(goqu.I("te.id").Eq(goqu.I("t.tenement_id")), goqu.I("te.park_id").Eq(goqu.I("t.park_id")))).
LeftJoin(goqu.T("meter").As("m"), goqu.On(goqu.I("m.code").Eq(goqu.I("t.meter_id")), goqu.I("m.park_id").Eq(goqu.I("t.park_id")))).
Select("t.*", goqu.I("te.full_name").As("tenement_name"), goqu.I("m.address").As("meter_address")).
Where(goqu.I("t.park_id").Eq(pid), goqu.I("t.top_up_code").Eq(topUpCode)).
Prepared(true).ToSQL()
var topUp model.TopUp
if err := pgxscan.Get(ctx, global.DB, &topUp, topUpSql, topUpArgs...); err != nil {
tur.log.Error("查询充值记录失败", zap.Error(err))
return nil, err
}
return &topUp, nil
}
// 创建一条新的充值记录
func (tur _TopUpRepository) CreateTopUp(pid string, form *vo.TopUpCreationForm) error {
tur.log.Info("创建一条新的充值记录", zap.String("Park", pid), zap.String("Tenement", form.Tenement), zap.String("Meter", form.Meter))
ctx, cancel := global.TimeoutContext()
defer cancel()
serial.StringSerialRequestChan <- 1
topUpCode := serial.Prefix("U", <-serial.StringSerialResponseChan)
topUpTime := types.Now()
topUpSql, topUpArgs, _ := tur.ds.
Insert("tenement_top_ups").
Cols("top_up_code", "park_id", "tenement_id", "meter_id", "topped_up_at", "amount", "payment_type").
Vals(goqu.Vals{
topUpCode,
pid,
form.Tenement,
form.Meter,
topUpTime,
form.Amount,
model.PAYMENT_CASH,
}).
Prepared(true).ToSQL()
if _, err := global.DB.Exec(ctx, topUpSql, topUpArgs...); err != nil {
tur.log.Error("创建充值记录失败", zap.Error(err))
return err
}
return nil
}
// 删除一条充值记录
func (tur _TopUpRepository) DeleteTopUp(pid, topUpCode string) error {
tur.log.Info("删除一条充值记录", zap.String("Park", pid), zap.String("TopUpCode", topUpCode))
ctx, cancel := global.TimeoutContext()
defer cancel()
topUpSql, topUpArgs, _ := tur.ds.
Update("tenement_top_ups").
Set(goqu.Record{"cancelled_at": types.Now()}).
Where(goqu.I("park_id").Eq(pid), goqu.I("top_up_code").Eq(topUpCode)).
Prepared(true).ToSQL()
if _, err := global.DB.Exec(ctx, topUpSql, topUpArgs...); err != nil {
tur.log.Error("删除充值记录失败", zap.Error(err))
return err
}
return nil
}

419
repository/user.go Normal file
View File

@ -0,0 +1,419 @@
package repository
import (
"context"
"electricity_bill_calc/config"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/tools"
"electricity_bill_calc/types"
"fmt"
"time"
"github.com/doug-martin/goqu/v9"
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
"github.com/fufuok/utils/xhash"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
"github.com/samber/lo"
"go.uber.org/zap"
)
type _UserRepository struct {
log *zap.Logger
ds goqu.DialectWrapper
}
var UserRepository = _UserRepository{
log: logger.Named("Repository", "User"),
ds: goqu.Dialect("postgres"),
}
// 使用用户名查询指定用户的基本信息
func (ur _UserRepository) FindUserByUsername(username string) (*model.User, error) {
ur.log.Info("根据用户名查询指定用户的基本信息。", zap.String("username", username))
ctx, cancel := global.TimeoutContext()
defer cancel()
var user model.User
sql, params, _ := ur.ds.From("user").Where(goqu.Ex{"username": username}).Prepared(true).ToSQL()
if err := pgxscan.Get(ctx, global.DB, &user, sql, params...); err != nil {
ur.log.Error("从数据库查询指定用户名的用户基本信息失败。", zap.String("username", username), zap.Error(err))
return nil, err
}
return &user, nil
}
// 使用用户唯一编号查询指定用户的基本信息
func (ur _UserRepository) FindUserById(uid string) (*model.User, error) {
ur.log.Info("根据用户唯一编号查询指定用户的基本信息。", zap.String("user id", uid))
ctx, cancel := global.TimeoutContext()
defer cancel()
var user model.User
sql, params, _ := ur.ds.From("user").Where(goqu.Ex{"id": uid}).Prepared(true).ToSQL()
if err := pgxscan.Get(ctx, global.DB, &user, sql, params...); err != nil {
ur.log.Error("从数据库查询指定用户唯一编号的用户基本信息失败。", zap.String("user id", uid), zap.Error(err))
return nil, err
}
return &user, nil
}
// 使用用户的唯一编号获取用户的详细信息
func (ur _UserRepository) FindUserDetailById(uid string) (*model.UserDetail, error) {
ur.log.Info("根据用户唯一编号查询指定用户的详细信息。", zap.String("user id", uid))
ctx, cancel := global.TimeoutContext()
defer cancel()
var user model.UserDetail
sql, params, _ := ur.ds.From("user_detail").Where(goqu.Ex{"id": uid}).Prepared(true).ToSQL()
if err := pgxscan.Get(ctx, global.DB, &user, sql, params...); err != nil {
ur.log.Error("从数据库查询指定用户唯一编号的用户详细信息失败。", zap.String("user id", uid), zap.Error(err))
return nil, err
}
return &user, nil
}
// 使用用户唯一编号获取用户的综合详细信息
func (ur _UserRepository) FindUserInformation(uid string) (*model.UserWithDetail, error) {
ur.log.Info("根据用户唯一编号查询用户的综合详细信息", zap.String("user id", uid))
ctx, cancel := global.TimeoutContext()
defer cancel()
var user model.UserWithDetail
sql, params, _ := ur.ds.
From("user").As("u").
Join(
goqu.T("user_detail").As("ud"),
goqu.On(goqu.Ex{
"ud.id": goqu.I("u.id"),
})).
Select(
"u.id", "u.username", "u.reset_needed", "u.type", "u.enabled",
"ud.name", "ud.abbr", "ud.region", "ud.address", "ud.contact", "ud.phone",
"ud.unit_service_fee", "ud.service_expiration",
"ud.created_at", "ud.created_by", "ud.last_modified_at", "ud.last_modified_by").
Where(goqu.Ex{"u.id": uid}).
Prepared(true).ToSQL()
if err := pgxscan.Get(
ctx, global.DB, &user, sql, params...); err != nil {
ur.log.Error("从数据库查询指定用户唯一编号的用户详细信息失败。", zap.String("user id", uid), zap.Error(err))
return nil, err
}
return &user, nil
}
// 检查指定用户唯一编号是否存在对应的用户
func (ur _UserRepository) IsUserExists(uid string) (bool, error) {
ur.log.Info("检查指定用户唯一编号是否存在对应的用户。", zap.String("user id", uid))
ctx, cancel := global.TimeoutContext()
defer cancel()
var userCount int
sql, params, _ := ur.ds.From("user").Select(goqu.COUNT("*")).Where(goqu.Ex{"id": uid}).Prepared(true).ToSQL()
if err := pgxscan.Get(ctx, global.DB, &userCount, sql, params...); err != nil {
ur.log.Error("从数据库查询指定用户唯一编号的用户基本信息失败。", zap.String("user id", uid), zap.Error(err))
return false, err
}
return userCount > 0, nil
}
// 检查指定用户名在数据库中是否已经存在
func (ur _UserRepository) IsUsernameExists(username string) (bool, error) {
ur.log.Info("检查指定用户名在数据库中是否已经存在。", zap.String("username", username))
ctx, cancel := global.TimeoutContext()
defer cancel()
var userCount int
sql, params, _ := ur.ds.From("user").Select(goqu.COUNT("*")).Where(goqu.Ex{"username": username}).Prepared(true).ToSQL()
if err := pgxscan.Get(ctx, global.DB, &userCount, sql, params...); err != nil {
ur.log.Error("从数据库查询指定用户名的用户基本信息失败。", zap.String("username", username), zap.Error(err))
return false, err
}
return userCount > 0, nil
}
// 创建一个新用户
func (ur _UserRepository) CreateUser(user model.User, detail model.UserDetail, operator *string) (bool, error) {
ur.log.Info("创建一个新用户。", zap.String("username", user.Username))
ctx, cancel := global.TimeoutContext()
defer cancel()
tx, err := global.DB.Begin(ctx)
if err != nil {
ur.log.Error("启动数据库事务失败。", zap.Error(err))
return false, err
}
createdTime := types.Now()
userSql, userParams, _ := ur.ds.
Insert("user").
Rows(
goqu.Record{
"id": user.Id, "username": user.Username, "password": user.Password,
"reset_needed": user.ResetNeeded, "type": user.UserType, "enabled": user.Enabled,
"created_at": createdTime,
},
).
Prepared(true).ToSQL()
userResult, err := tx.Exec(ctx, userSql, userParams...)
if err != nil {
ur.log.Error("向数据库插入新用户基本信息失败。", zap.Error(err))
tx.Rollback(ctx)
return false, err
}
userDetailSql, userDetailParams, _ := ur.ds.
Insert("user_detail").
Rows(
goqu.Record{
"id": user.Id, "name": detail.Name, "abbr": tools.PinyinAbbr(*detail.Name), "region": detail.Region,
"address": detail.Address, "contact": detail.Contact, "phone": detail.Phone,
"unit_service_fee": detail.UnitServiceFee, "service_expiration": detail.ServiceExpiration,
"created_at": createdTime, "created_by": operator,
"last_modified_at": createdTime, "last_modified_by": operator,
},
).
Prepared(true).ToSQL()
detailResult, err := tx.Exec(ctx, userDetailSql, userDetailParams...)
if err != nil {
ur.log.Error("向数据库插入新用户详细信息失败。", zap.Error(err))
tx.Rollback(ctx)
return false, err
}
err = tx.Commit(ctx)
if err != nil {
ur.log.Error("提交数据库事务失败。", zap.Error(err))
tx.Rollback(ctx)
return false, err
}
return userResult.RowsAffected() > 0 && detailResult.RowsAffected() > 0, nil
}
// 根据给定的条件检索用户
func (ur _UserRepository) FindUser(keyword *string, userType int16, state *bool, page uint) ([]*model.UserWithDetail, int64, error) {
ur.log.Info("根据给定的条件检索用户。", zap.Uint("page", page), zap.Stringp("keyword", keyword), zap.Int16("user type", userType), zap.Boolp("state", state))
ctx, cancel := global.TimeoutContext()
defer cancel()
var (
userWithDetails []*model.UserWithDetail
userCount int64
)
userQuery := ur.ds.
From(goqu.T("user").As("u")).
Join(goqu.T("user_detail").As("ud"), goqu.On(goqu.Ex{"ud.id": goqu.I("u.id")})).
Select(
"u.id", "u.username", "u.reset_needed", "u.type", "u.enabled",
"ud.name", "ud.abbr", "ud.region", "ud.address", "ud.contact", "ud.phone",
"ud.unit_service_fee", "ud.service_expiration",
"ud.created_at", "ud.created_by", "ud.last_modified_at", "ud.last_modified_by",
)
countQuery := ur.ds.
From(goqu.T("user").As("u")).
Join(goqu.T("user_detail").As("ud"), goqu.On(goqu.Ex{"ud.id": goqu.I("u.id")})).
Select(goqu.COUNT("*"))
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
userQuery = userQuery.Where(
goqu.Or(
goqu.I("u.username").ILike(pattern),
goqu.I("ud.name").ILike(pattern),
goqu.I("ud.abbr").ILike(pattern),
),
)
countQuery = countQuery.Where(
goqu.Or(
goqu.I("u.username").ILike(pattern),
goqu.I("ud.name").ILike(pattern),
goqu.I("ud.abbr").ILike(pattern),
),
)
}
if userType != -1 {
userQuery = userQuery.Where(goqu.Ex{"u.type": userType})
countQuery = countQuery.Where(goqu.Ex{"u.type": userType})
}
if state != nil {
userQuery = userQuery.Where(goqu.Ex{"u.enabled": state})
countQuery = countQuery.Where(goqu.Ex{"u.enabled": state})
}
userQuery.Order(goqu.I("u.created_at").Desc())
currentPosition := (page - 1) * config.ServiceSettings.ItemsPageSize
userQuery = userQuery.Offset(currentPosition).Limit(config.ServiceSettings.ItemsPageSize)
userSql, userParams, _ := userQuery.Prepared(true).ToSQL()
countSql, countParams, _ := countQuery.Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &userWithDetails, userSql, userParams...); err != nil {
ur.log.Error("从数据库查询用户列表失败。", zap.Error(err))
return make([]*model.UserWithDetail, 0), 0, err
}
if err := pgxscan.Get(ctx, global.DB, &userCount, countSql, countParams...); err != nil {
ur.log.Error("从数据库查询用户列表总数失败。", zap.Error(err))
return make([]*model.UserWithDetail, 0), 0, err
}
return userWithDetails, userCount, nil
}
// 更新指定用户的详细信息
func (ur _UserRepository) UpdateDetail(uid string, userDetail model.UserModificationForm, operator *string) (bool, error) {
ur.log.Info("更新指定用户的详细信息。", zap.String("user id", uid))
ctx, cancel := global.TimeoutContext()
defer cancel()
updates := goqu.Record{
"name": userDetail.Name, "abbr": tools.PinyinAbbr(userDetail.Name), "region": userDetail.Region,
"address": userDetail.Address, "contact": userDetail.Contact, "phone": userDetail.Phone,
"last_modified_at": types.Now(), "last_modified_by": operator,
}
if userDetail.UnitServiceFee != nil {
updates = lo.Assign(updates, goqu.Record{"unit_service_fee": userDetail.UnitServiceFee})
}
userDetailUpdateQuery := ur.ds.
Update("user_detail").
Set(updates).
Where(goqu.Ex{"id": uid})
userDetailSql, userDetailParams, _ := userDetailUpdateQuery.
Prepared(true).ToSQL()
if res, err := global.DB.Exec(ctx, userDetailSql, userDetailParams...); err != nil {
ur.log.Error("向数据库更新指定用户的详细信息失败。", zap.String("user id", uid), zap.Error(err))
return false, err
} else {
return res.RowsAffected() > 0, nil
}
}
// 更新指定用户的登录凭据
func (ur _UserRepository) UpdatePassword(uid, newCredential string, needReset bool) (bool, error) {
ur.log.Info("更新指定用户的登录凭据。", zap.String("user id", uid))
ctx, cancel := global.TimeoutContext()
defer cancel()
userUpdateQuery := ur.ds.
Update("user").
Set(goqu.Record{"password": xhash.Sha512Hex(newCredential), "reset_needed": needReset}).
Where(goqu.Ex{"id": uid})
userSql, userParams, _ := userUpdateQuery.
Prepared(true).ToSQL()
if res, err := global.DB.Exec(ctx, userSql, userParams...); err != nil {
ur.log.Error("向数据库更新指定用户的登录凭据失败。", zap.String("user id", uid), zap.Error(err))
return false, err
} else {
return res.RowsAffected() > 0, nil
}
}
// 更新指定用户的可用性状态
func (ur _UserRepository) ChangeState(uid string, state bool) (bool, error) {
ur.log.Info("更新指定用户的可用性状态。", zap.String("user id", uid))
ctx, cancel := global.TimeoutContext()
defer cancel()
userUpdateQuery := ur.ds.
Update("user").
Set(goqu.Record{"enabled": state}).
Where(goqu.Ex{"id": uid})
userSql, userParams, _ := userUpdateQuery.
Prepared(true).ToSQL()
if res, err := global.DB.Exec(ctx, userSql, userParams...); err != nil {
ur.log.Error("向数据库更新指定用户的可用性状态失败。", zap.String("user id", uid), zap.Error(err))
return false, err
} else {
return res.RowsAffected() > 0, nil
}
}
// 检索条目数量有限的用户详细信息
func (ur _UserRepository) SearchUsersWithLimit(userType *int16, keyword *string, limit uint) ([]*model.UserWithDetail, error) {
ur.log.Info("检索条目数量有限的用户详细信息。", zap.Int16p("user type", userType), zap.Uint("limit", limit), zap.Stringp("keyword", keyword))
actualUserType := tools.DefaultTo(userType, model.USER_TYPE_ENT)
ctx, cancel := global.TimeoutContext()
defer cancel()
var users = make([]*model.UserWithDetail, 0)
userQuery := ur.ds.
From(goqu.T("user").As("u")).
Join(goqu.T("user_detail").As("ud"), goqu.On(goqu.Ex{"ud.id": goqu.I("u.id")})).
Select(
"u.id", "u.username", "u.reset_needed", "u.type", "u.enabled",
"ud.name", "ud.abbr", "ud.region", "ud.address", "ud.contact", "ud.phone",
"ud.unit_service_fee", "ud.service_expiration",
"ud.created_at", "ud.created_by", "ud.last_modified_at", "ud.last_modified_by",
)
if keyword != nil && len(*keyword) > 0 {
pattern := fmt.Sprintf("%%%s%%", *keyword)
userQuery = userQuery.Where(
goqu.Or(
goqu.I("u.username").ILike(pattern),
goqu.I("ud.name").ILike(pattern),
goqu.I("ud.abbr").ILike(pattern),
),
)
}
userQuery = userQuery.Where(goqu.Ex{"u.type": actualUserType})
userQuery.Order(goqu.I("u.created_at").Desc()).Limit(limit)
userSql, userParams, _ := userQuery.Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &users, userSql, userParams...); err != nil {
ur.log.Error("从数据库查询用户列表失败。", zap.Error(err))
return nil, err
}
return users, nil
}
// 更新指定用户的服务有效期限
func (ur _UserRepository) UpdateServiceExpiration(tx pgx.Tx, ctx context.Context, uid string, expiration time.Time) (bool, error) {
ur.log.Info("更新指定用户的服务有效期限。", zap.String("user id", uid))
userDetailUpdateQuery := ur.ds.
Update("user_detail").
Set(goqu.Record{"service_expiration": expiration}).
Where(goqu.Ex{"id": uid})
userDetailSql, userDetailParams, _ := userDetailUpdateQuery.
Prepared(true).ToSQL()
if res, err := tx.Exec(ctx, userDetailSql, userDetailParams...); err != nil {
ur.log.Error("向数据库更新指定用户的服务有效期限失败。", zap.String("user id", uid), zap.Error(err))
return false, err
} else {
return res.RowsAffected() > 0, nil
}
}
// 检索指定用户列表的详细信息
func (ur _UserRepository) RetrieveUsersDetail(uids []string) ([]*model.UserDetail, error) {
ur.log.Info("检索指定用户列表的详细信息。", zap.Strings("user ids", uids))
if len(uids) == 0 {
return make([]*model.UserDetail, 0), nil
}
ctx, cancel := global.TimeoutContext()
defer cancel()
var users []*model.UserDetail
userQuery := ur.ds.
From("user_detail").
Where(goqu.Ex{"id": uids})
userSql, userParams, _ := userQuery.Prepared(true).ToSQL()
if err := pgxscan.Select(ctx, global.DB, &users, userSql, userParams...); err != nil {
ur.log.Error("从数据库查询用户列表失败。", zap.Error(err))
return make([]*model.UserDetail, 0), err
}
return users, nil
}

View File

@ -17,7 +17,7 @@ type BaseResponse struct {
type PagedResponse struct {
Page int `json:"current"`
Size int `json:"pageSize"`
Size uint `json:"pageSize"`
Total int64 `json:"total"`
}
@ -61,7 +61,7 @@ func (r Result) UnableToParse(msg string) error {
// 用户未获得授权)响应
func (r *Result) Unauthorized(msg string) error {
return r.response(fiber.StatusOK, fiber.StatusUnauthorized, msg)
return r.response(fiber.StatusUnauthorized, fiber.StatusUnauthorized, msg)
}
// 简易操作成功信息
@ -71,37 +71,37 @@ func (r *Result) Success(msg string, payloads ...map[string]interface{}) error {
// 数据成功创建
func (r Result) Created(msg string, payloads ...map[string]interface{}) error {
return r.response(fiber.StatusOK, fiber.StatusCreated, msg, payloads...)
return r.response(fiber.StatusCreated, fiber.StatusCreated, msg, payloads...)
}
// 数据成功更新
func (r Result) Updated(msg string) error {
return r.response(fiber.StatusOK, fiber.StatusAccepted, msg)
func (r Result) Updated(msg string, payloads ...map[string]interface{}) error {
return r.response(fiber.StatusAccepted, fiber.StatusAccepted, msg, payloads...)
}
// 数据已成功删除
func (r Result) Deleted(msg string) error {
return r.response(fiber.StatusOK, fiber.StatusNoContent, msg)
return r.response(fiber.StatusNoContent, fiber.StatusNoContent, msg)
}
// 指定操作未被接受
func (r Result) BadRequest(msg string) error {
return r.response(fiber.StatusOK, fiber.StatusBadRequest, msg)
return r.response(fiber.StatusBadRequest, fiber.StatusBadRequest, msg)
}
// 指定操作未被接受
func (r Result) NotAccept(msg string) error {
return r.response(fiber.StatusOK, fiber.StatusNotAcceptable, msg)
return r.response(fiber.StatusNotAcceptable, fiber.StatusNotAcceptable, msg)
}
// 数据未找到
func (r Result) NotFound(msg string) error {
return r.response(fiber.StatusOK, fiber.StatusNotFound, msg)
return r.response(fiber.StatusNotFound, fiber.StatusNotFound, msg)
}
// 数据存在冲突
func (r Result) Conflict(msg string) error {
return r.response(fiber.StatusOK, fiber.StatusConflict, msg)
return r.response(fiber.StatusConflict, fiber.StatusConflict, msg)
}
// 快速自由JSON格式响应

View File

@ -26,7 +26,7 @@ func init() {
func App() *fiber.App {
app := fiber.New(fiber.Config{
BodyLimit: 10 * 1024 * 1024,
BodyLimit: 30 * 1024 * 1024,
EnablePrintRoutes: true,
EnableTrustedProxyCheck: false,
Prefork: false,
@ -44,17 +44,15 @@ func App() *fiber.App {
}))
app.Use(security.SessionRecovery)
controller.InitializeUserController(app)
controller.InitializeRegionController(app)
controller.InitializeChargesController(app)
controller.InitializeParkController(app)
controller.InitializeMaintenanceFeeController(app)
controller.InitializeMeter04kVController(app)
controller.InitializeReportController(app)
controller.InitializeEndUserController(app)
controller.InitializeWithdrawController(app)
controller.InitializeStatisticsController(app)
controller.InitializeGodModeController(app)
controller.InitializeUserHandlers(app)
controller.InitializeRegionHandlers(app)
controller.InitializeChargeHandlers(app)
controller.InitializeParkHandlers(app)
controller.InitializeTenementHandler(app)
controller.InitializeMeterHandlers(app)
controller.InitializeInvoiceHandler(app)
controller.InitializeTopUpHandlers(app)
controller.InitializeReportHandlers(app)
return app
}

View File

@ -14,8 +14,12 @@ import (
// ! 仅通过该中间件是不能保证上下文中一定保存有用户会话信息的。
func SessionRecovery(c *fiber.Ctx) error {
if auth := c.Get("Authorization", ""); len(auth) > 0 {
token := strings.Fields(auth)[1]
session, err := cache.RetreiveSession(token)
authFields := strings.Fields(auth)
if len(authFields) != 2 || strings.ToLower(authFields[0]) != "bearer" || len(authFields[1]) == 0 {
return c.Next()
}
token := authFields[1]
session, err := cache.RetrieveSession(token)
if err == nil && session != nil {
c.Locals("session", session)

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
}

View File

@ -19,4 +19,5 @@ Redis:
Service:
MaxSessionLife: 2h
ItemsPageSize: 20
CacheLifeTime: 5m
CacheLifeTime: 5m
HostSerial: 5

84
tools/serial/algorithm.go Normal file
View File

@ -0,0 +1,84 @@
package serial
import (
"electricity_bill_calc/config"
"electricity_bill_calc/logger"
"electricity_bill_calc/types"
"fmt"
"time"
)
var (
log = logger.Named("Algorithm", "Unique Serial")
SerialRequestChan = make(chan int64, 500)
StringSerialRequestChan = make(chan int64, 500)
SerialResponseChan = make(chan int64, 500)
StringSerialResponseChan = make(chan string, 500)
)
func init() {
go func() {
var (
lastTimestamp int64 = 0
lastSerial int64 = 0
)
log.Info("唯一序列号生成服务已经启动。")
for {
select {
case <-SerialRequestChan:
log.Info("收到生成数字型唯一序列号的请求。")
timestamp := generateTimestamp(lastTimestamp)
if timestamp != lastTimestamp {
lastSerial = 0
}
lastSerial = lastSerial + 1
uniqueId := generateSerial(timestamp, lastSerial)
SerialResponseChan <- uniqueId
lastTimestamp = timestamp
case <-StringSerialRequestChan:
log.Info("收到生成字符串型唯一序列号的请求。")
timestamp := generateTimestamp(lastTimestamp)
if timestamp != lastTimestamp {
lastSerial = 0
}
lastSerial = lastSerial + 1
uniqueId := generateStringSerial(timestamp, lastSerial)
StringSerialResponseChan <- uniqueId
lastTimestamp = timestamp
}
}
}()
}
// 生成一个能够对抗服务器时间回拨的时间戳
func generateTimestamp(base int64) int64 {
for {
timestamp := types.Timestamp()
if timestamp >= base {
return timestamp
}
time.Sleep(1 * time.Second)
}
}
// 生成一个唯一的数字型序列号
func generateSerial(timestamp, serial int64) int64 {
return (timestamp << 20) | ((config.ServiceSettings.HostSerial & 0xffff) << 16) | (serial & 0xffff_ffff)
}
// 生成一个唯一的字符串型序列号
func generateStringSerial(timestamp, serial int64) string {
return fmt.Sprintf("%017d", generateSerial(timestamp, serial))
}
// 生成一个带前缀字符串的唯一字符串型序列号
func Prefix(prefix string, serial interface{}) string {
switch serial := serial.(type) {
case int64:
return fmt.Sprintf("%s%017d", prefix, serial)
case string:
return fmt.Sprintf("%s%s", prefix, serial)
}
return ""
}

View File

@ -2,10 +2,12 @@ package tools
import (
"encoding/json"
"fmt"
"strings"
"github.com/mozillazg/go-pinyin"
"github.com/samber/lo"
"github.com/shopspring/decimal"
)
func ContainsInsensitive(element string, slice []string) bool {
@ -51,3 +53,94 @@ func PartitionSlice[T any](slice []T, chunkSize int) [][]T {
}
return divided
}
// 判断指定指针是否为空,如果为空,则返回指定默认值(指针形式)
func DefaultTo[T any](originValue *T, defaultValue T) T {
if originValue == nil {
return defaultValue
}
return *originValue
}
// 判断指定的指针是否为空,如果为空,则返回指定的默认字符串,或者返回指针所指内容的字符串形式。
func DefaultStrTo[T any](format string, originValue *T, defaultStr string) string {
if originValue == nil {
return defaultStr
}
return fmt.Sprintf(format, originValue)
}
// 判断指定字符串指针是否为`nil`或者字符串长度为空,如果是则返回给定的默认字符串,否则返回指针所指内容的字符串形式。
func DefaultOrEmptyStr(originValue *string, defaultStr string) string {
if originValue == nil || len(*originValue) == 0 {
return defaultStr
}
return *originValue
}
// 判断指定表达式的值,根据表达式的值返回指定的值。相当于其他语言中的三目运算符。
func Cond[T any](expr bool, trueValue, falseValue T) T {
if expr {
return trueValue
}
return falseValue
}
// 使用给定的函数对指定的值进行判断,根据表达式的值返回指定的值。
func CondFn[T, R any](exprFn func(val T) bool, value T, trueValue, falseValue R) R {
return Cond(exprFn(value), trueValue, falseValue)
}
// 使用给定的函数对指定的值进行判断,根据表达式的值返回指定的值。本函数为惰性求值。
func CondFnElse[T, R any](exprFn func(val T) bool, value T, trueValueFn func(val T) R, falseValueFn func(val T) R) R {
if exprFn(value) {
return trueValueFn(value)
}
return falseValueFn(value)
}
// 使用给定的函数对指定的值进行判断,如果表达式为真,则返回指定的值,否则返回另一个值。
func CondOr[T any](exprFn func(val T) bool, value, elseValue T) T {
return CondFn(exprFn, value, value, elseValue)
}
// 将指定的字符串指针解析为一个可空的`decimal.NullDecimal`类型的值。
func NewNullDecimalFromString(val *string) (decimal.NullDecimal, error) {
if val == nil {
return decimal.NullDecimal{Valid: false}, nil
}
nd, err := decimal.NewFromString(*val)
if err != nil {
return decimal.NullDecimal{Valid: false}, err
}
return decimal.NullDecimal{Decimal: nd, Valid: true}, nil
}
// 将指定的字符串指针解析为一个`decimal.Decimal`类型的值,必须提供一个默认值,以用来替换解析失败以及空指针的情况。
func NewDecimalFromString(val *string, defaultValue decimal.Decimal) decimal.Decimal {
if val == nil {
return defaultValue
}
nd, err := decimal.NewFromString(*val)
if err != nil {
return defaultValue
}
return nd
}
// 将空白字符串转换为空指针,同时字符串本身也将转换为指针类型。
func EmptyToNil(val string) *string {
if len(val) == 0 {
return nil
}
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]))
}

175
types/date.go Normal file
View File

@ -0,0 +1,175 @@
package types
import (
"database/sql"
"database/sql/driver"
"encoding/json"
"fmt"
"time"
"go.uber.org/zap"
)
var dateLayouts = []string{
"2006-01-02", "2006-1-2", "2006/01/02", "06-1-2", "6-01-02", "01/02/06", "1/2/06", "2006年01月02日", "06年1月2日",
}
type Date struct {
time.Time
}
func NewDate(year int, month time.Month, day int) Date {
return Date{
Time: time.Date(year, month, day, 0, 0, 0, 0, loc),
}
}
func NewEmptyDate() Date {
return Date{
Time: time.Time{}.In(loc),
}
}
func MinDate() Date {
return NewDate(1, 1, 1)
}
func MaxDate() Date {
return NewDate(9999, 12, 31)
}
func NowDate() Date {
return Now().Date()
}
func ParseDate(t string) (Date, error) {
if len(t) == 0 {
return NewEmptyDate(), fmt.Errorf("不能解析空白的日期时间。")
}
for _, layout := range dateLayouts {
d, err := time.ParseInLocation(layout, t, loc)
if err == nil {
return Date{
Time: d,
}, nil
}
}
return NewEmptyDate(), fmt.Errorf("无法解析给定的日期,格式不正确。")
}
func ParseDatep(t string) (*Date, error) {
if len(t) == 0 {
return nil, nil
}
for _, layout := range dateLayouts {
d, err := time.ParseInLocation(layout, t, loc)
if err == nil {
return &Date{
Time: d,
}, nil
}
}
return nil, fmt.Errorf("无法解析给定的日期,格式不正确。")
}
func ParseDateWithDefault(t string, defaultDate Date) Date {
if len(t) == 0 {
return defaultDate
}
d, err := ParseDate(t)
if err != nil {
return defaultDate
}
return d
}
var _ driver.Valuer = (*Date)(nil)
func (dt Date) Value() (driver.Value, error) {
return dt.In(loc).Format("2006-01-02"), nil
}
var _ sql.Scanner = (*Date)(nil)
func (d *Date) Scan(src interface{}) (err error) {
switch src := src.(type) {
case time.Time:
d.Time = src
case string:
t, err := time.ParseInLocation("2006-01-02", src, loc)
if err != nil {
return err
}
*d = Date{Time: t}
case []byte:
d.Time, err = time.ParseInLocation("2006-01-02", string(src), loc)
return err
case nil:
d = nil
default:
return fmt.Errorf("该数据类型不支持解析到日期: %T", src)
}
return nil
}
var _ json.Marshaler = (*Date)(nil)
func (d Date) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Format("2006-01-02"))
}
var _ json.Unmarshaler = (*Date)(nil)
func (d *Date) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return fmt.Errorf("不能解析指定的日期时间值: %w", err)
}
t, err := time.ParseInLocation("2006-01-02", str, loc)
d.Time = t
return err
}
func (d Date) DifferenceInMonth(d2 *Date) int {
var differYear, differMonth int
differYear = d.Year() - d2.Year()
differMonth = int(d.Month() - d2.Month())
return differYear*12 + differMonth
}
func (d Date) IsNextMonth(d2 *Date) bool {
return d.DifferenceInMonth(d2) == 1
}
func (d Date) ToBeginningOfDate() DateTime {
return FromTime(time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, loc))
}
func (d Date) ToEndingOfDate() DateTime {
return FromTime(time.Date(d.Year(), d.Month(), d.Day(), 23, 59, 59, 999999, loc))
}
func (d Date) IsEmpty() bool {
return d.Time.IsZero()
}
func (d *Date) Parse(s string) error {
t, err := ParseDate(s)
if err != nil {
return err
}
d.Time = t.Time
return nil
}
func (d Date) ToString() string {
return d.Time.Format("2006-01-02")
}
func (d Date) ToDateTime() DateTime {
return FromTime(d.Time)
}
func (d Date) Log(fieldName string) zap.Field {
return zap.String(fieldName, d.ToString())
}

141
types/daterange.go Normal file
View File

@ -0,0 +1,141 @@
package types
import (
"database/sql"
"database/sql/driver"
"electricity_bill_calc/tools"
"encoding/json"
"errors"
"github.com/jackc/pgx/v5/pgtype"
)
type DateRange struct {
pgtype.Range[Date]
}
func NewEmptyDateRange() DateRange {
return DateRange{
Range: pgtype.Range[Date]{
Lower: MinDate(),
LowerType: pgtype.Unbounded,
Upper: MaxDate(),
UpperType: pgtype.Unbounded,
Valid: true,
},
}
}
func NewDateRange(lower *Date, upper *Date) DateRange {
return DateRange{
Range: pgtype.Range[Date]{
LowerType: tools.Cond(lower != nil, pgtype.Inclusive, pgtype.Unbounded),
Lower: tools.DefaultTo(lower, MinDate()),
UpperType: tools.Cond(upper != nil, pgtype.Inclusive, pgtype.Unbounded),
Upper: tools.DefaultTo(upper, MaxDate()),
Valid: true,
},
}
}
var _ driver.Valuer = (*DateRange)(nil)
func (dr DateRange) Value() (driver.Value, error) {
return assembleRange(dr.Range), nil
}
var _ sql.Scanner = (*DateRange)(nil)
func (dr *DateRange) Scan(src interface{}) (err error) {
switch src := src.(type) {
case pgtype.Range[Date]:
dr.Range = src
case string:
r, err := destructureToRange[Date](src)
if err != nil {
return err
}
dr.Range = r
case []byte:
r, err := destructureToRange[Date](string(src))
if err != nil {
return err
}
dr.Range = r
case nil:
dr = nil
default:
return errors.New("该数据类型不支持解析到日期范围。")
}
return
}
var _ json.Marshaler = (*DateRange)(nil)
func (dr DateRange) MarshalJSON() ([]byte, error) {
return json.Marshal(assembleRange(dr.Range))
}
var _ json.Unmarshaler = (*DateRange)(nil)
func (dr *DateRange) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return errors.New("不能解析指定的日期范围值。")
}
r, err := destructureToRange[Date](str)
if err != nil {
return err
}
dr.Range = r
return nil
}
func (dr *DateRange) SetLower(lower Date, bound ...pgtype.BoundType) {
bound = append(bound, pgtype.Inclusive)
dr.Range.Lower = lower
dr.Range.LowerType = bound[0]
}
func (dr *DateRange) SetLowerUnbounded() {
dr.Range.Lower = MinDate()
dr.Range.LowerType = pgtype.Unbounded
}
func (dr *DateRange) SetUpper(upper Date, bound ...pgtype.BoundType) {
bound = append(bound, pgtype.Inclusive)
dr.Range.Upper = upper
dr.Range.UpperType = bound[0]
}
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()
}
}
func (dr DateRange) IsEmptyOrWild() bool {
return (dr.LowerType == pgtype.Unbounded && dr.UpperType == pgtype.Unbounded) ||
(dr.LowerType == pgtype.Empty && dr.UpperType == pgtype.Empty)
}

189
types/datetime.go Normal file
View File

@ -0,0 +1,189 @@
package types
import (
"database/sql"
"database/sql/driver"
"encoding/json"
"fmt"
"time"
"go.uber.org/zap"
)
var (
loc *time.Location = time.FixedZone("+0800", 8*60*60)
datetimeLayouts = []string{
"2006-01-02 15:04:05", "2006-1-2 15:04:05", "2006/01/02 15:04:05", "06-1-2 15:04:05", "06-01-02 15:04:05", "01/02/06 15:04:05", "1/2/06 15:04:05", "2006年01月02日 15:04:05", "06年1月2日 15:04:05",
"2006-01-02 15:04", "2006-1-2 15:04", "2006/01/02 15:04", "06-1-2 15:04", "06-01-02 15:04", "01/02/06 15:04", "1/2/06 15:04", "2006年01月02日 15:04", "06年1月2日 15:04",
}
)
type DateTime struct {
time.Time
}
func Now() DateTime {
return DateTime{
Time: time.Now().In(loc),
}
}
func NewEmptyDateTime() DateTime {
return DateTime{
Time: time.Time{}.In(loc),
}
}
func MinDateTime() DateTime {
return DateTime{
Time: time.Date(1, 1, 1, 0, 0, 0, 0, loc),
}
}
func MaxDateTime() DateTime {
return DateTime{
Time: time.Date(9999, 12, 31, 23, 59, 59, 999999, loc),
}
}
func Timestamp() int64 {
startline := time.Date(2022, 2, 22, 22, 22, 22, 0, loc).Unix()
return Now().Unix() - startline
}
func FromTime(t time.Time) DateTime {
return DateTime{
Time: t,
}
}
func FromUnixMicro(sec int64) DateTime {
return DateTime{
Time: time.UnixMicro(sec).In(loc),
}
}
func ParseDateTime(t string) (DateTime, error) {
if len(t) == 0 {
return NewEmptyDateTime(), fmt.Errorf("不能解析空白的日期时间。")
}
for _, layout := range datetimeLayouts {
fmt.Printf("Parse: %s, Try layout: %s\n", t, layout)
d, err := time.ParseInLocation(layout, t, loc)
if err == nil {
return DateTime{
Time: d,
}, nil
}
}
return NewEmptyDateTime(), fmt.Errorf("无法解析给定的日期时间,格式不正确。")
}
func ParseDateTimep(t string) (*DateTime, error) {
if len(t) == 0 {
return nil, nil
}
for _, layout := range datetimeLayouts {
fmt.Printf("Parse: %s, Try layout: %s\n", t, layout)
d, err := time.ParseInLocation(layout, t, loc)
if err == nil {
return &DateTime{
Time: d,
}, nil
}
}
return nil, fmt.Errorf("无法解析给定的日期时间,格式不正确。")
}
var _ driver.Valuer = (*DateTime)(nil)
func (dt DateTime) Value() (driver.Value, error) {
return dt.In(loc).Format("2006-01-02 15:04:05"), nil
}
var _ sql.Scanner = (*DateTime)(nil)
func (dt *DateTime) Scan(src interface{}) (err error) {
switch src := src.(type) {
case time.Time:
dt.Time = src
case string:
t, err := time.ParseInLocation("2006-01-02 15:04:05", src, loc)
if err != nil {
return err
}
*dt = DateTime{Time: t}
case []byte:
dt.Time, err = time.ParseInLocation("2006-01-02 15:04:05", string(src), loc)
return err
case nil:
dt = nil
default:
return fmt.Errorf("该数据类型不支持解析到日期时间: %T", src)
}
return nil
}
var _ json.Marshaler = (*DateTime)(nil)
func (dt DateTime) MarshalJSON() ([]byte, error) {
return json.Marshal(dt.Format("2006-01-02 15:04:05"))
}
var _ json.Unmarshaler = (*DateTime)(nil)
func (dt *DateTime) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return fmt.Errorf("不能解析指定的日期时间值: %w", err)
}
t, err := time.ParseInLocation("2006-01-02 15:04:05", str, loc)
dt.Time = t
return err
}
func (dt DateTime) DifferenceInMonth(d DateTime) int {
var differYear, differMonth int
differYear = dt.Year() - d.Year()
differMonth = int(dt.Month() - d.Month())
return differYear*12 + differMonth
}
func (dt DateTime) IsNextMonth(target DateTime) bool {
return dt.DifferenceInMonth(target) == 1
}
func (dt *DateTime) ToBeginningOfDate() {
dt.Time = time.Date(dt.Year(), dt.Month(), dt.Day(), 0, 0, 0, 0, loc)
}
func (dt *DateTime) ToEndingOfDate() {
dt.Time = time.Date(dt.Year(), dt.Month(), dt.Day(), 23, 59, 59, 999999, loc)
}
func (dt DateTime) Date() Date {
return Date{
Time: time.Date(dt.Year(), dt.Month(), dt.Day(), 0, 0, 0, 0, loc),
}
}
func (dt DateTime) IsEmpty() bool {
return dt.Time.IsZero()
}
func (dt *DateTime) Parse(s string) error {
t, err := ParseDateTime(s)
if err != nil {
return err
}
dt.Time = t.Time
return nil
}
func (dt DateTime) ToString() string {
return dt.Time.Format("2006-01-02 15:04:05")
}
func (dt DateTime) Log(fieldName string) zap.Field {
return zap.String(fieldName, dt.ToString())
}

119
types/datetimerange.go Normal file
View File

@ -0,0 +1,119 @@
package types
import (
"database/sql"
"database/sql/driver"
"electricity_bill_calc/tools"
"encoding/json"
"errors"
"github.com/jackc/pgx/v5/pgtype"
)
type DateTimeRange struct {
pgtype.Range[DateTime]
}
func NewEmptyDateTimeRange() DateTimeRange {
return DateTimeRange{
Range: pgtype.Range[DateTime]{
Lower: MinDateTime(),
LowerType: pgtype.Unbounded,
Upper: MaxDateTime(),
UpperType: pgtype.Unbounded,
Valid: true,
},
}
}
func NewDateTimeRange(lower *DateTime, upper *DateTime) DateTimeRange {
return DateTimeRange{
Range: pgtype.Range[DateTime]{
LowerType: tools.Cond(lower != nil, pgtype.Inclusive, pgtype.Unbounded),
Lower: tools.DefaultTo(lower, MinDateTime()),
UpperType: tools.Cond(upper != nil, pgtype.Inclusive, pgtype.Unbounded),
Upper: tools.DefaultTo(upper, MaxDateTime()),
Valid: true,
},
}
}
var _ driver.Value = (*DateTimeRange)(nil)
func (dr DateTimeRange) Value() (driver.Value, error) {
return assembleRange(dr.Range), nil
}
var _ sql.Scanner = (*DateTimeRange)(nil)
func (dr *DateTimeRange) Scan(src interface{}) (err error) {
switch src := src.(type) {
case pgtype.Range[DateTime]:
dr.Range = src
case string:
r, err := destructureToRange[DateTime](src)
if err != nil {
return err
}
dr.Range = r
case []byte:
r, err := destructureToRange[DateTime](string(src))
if err != nil {
return err
}
dr.Range = r
case nil:
dr = nil
default:
return errors.New("该数据类型不支持解析到日期范围。")
}
return
}
var _ json.Marshaler = (*DateTimeRange)(nil)
func (dr DateTimeRange) MarshalJSON() ([]byte, error) {
return json.Marshal(assembleRange(dr.Range))
}
var _ json.Unmarshaler = (*DateTimeRange)(nil)
func (dr *DateTimeRange) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return errors.New("不能解析指定的日期范围值。")
}
r, err := destructureToRange[DateTime](str)
if err != nil {
return err
}
dr.Range = r
return nil
}
func (dr *DateTimeRange) SetLower(lower DateTime, bound ...pgtype.BoundType) {
bound = append(bound, pgtype.Inclusive)
dr.Range.Lower = lower
dr.Range.LowerType = bound[0]
}
func (dr *DateTimeRange) SetLowerUnbounded() {
dr.Range.Lower = MinDateTime()
dr.Range.LowerType = pgtype.Unbounded
}
func (dr *DateTimeRange) SetUpper(upper DateTime, bound ...pgtype.BoundType) {
bound = append(bound, pgtype.Inclusive)
dr.Range.Upper = upper
dr.Range.UpperType = bound[0]
}
func (dr *DateTimeRange) SetUpperUnbounded() {
dr.Range.Upper = MaxDateTime()
dr.Range.UpperType = pgtype.Unbounded
}
func (dr DateTimeRange) IsEmptyOrWild() bool {
return (dr.Range.LowerType == pgtype.Unbounded && dr.Range.UpperType == pgtype.Unbounded) ||
(dr.Range.LowerType == pgtype.Empty || dr.Range.UpperType == pgtype.Empty)
}

119
types/range.go Normal file
View File

@ -0,0 +1,119 @@
package types
import (
"errors"
"strings"
"github.com/jackc/pgx/v5/pgtype"
)
type Parse interface {
Parse(string) error
}
type ToString interface {
ToString() string
}
type CheckEmptyOrWild interface {
IsEmptyOrWild() bool
}
// 将一个字符串拆解解析为一个 Postgresql 范围类型的值。
func destructureToRange[T any, PT interface {
Parse
*T
}](s string) (pgtype.Range[T], error) {
var r pgtype.Range[T]
r.Valid = false
if len(s) == 0 {
r.LowerType = pgtype.Empty
r.UpperType = pgtype.Empty
return r, nil
}
rangeUnit := strings.Split(s, ",")
if len(rangeUnit) != 2 {
return r, errors.New("无法解析给定的范围值,格式不正确。")
}
if unit, found := strings.CutPrefix(rangeUnit[0], "["); found {
var t PT
if len(unit) > 0 {
r.LowerType = pgtype.Inclusive
err := t.Parse(unit)
if err != nil {
return r, errors.New("无法解析给定的最低范围值。")
}
} else {
r.LowerType = pgtype.Unbounded
}
r.Lower = *t
}
if unit, found := strings.CutPrefix(rangeUnit[0], "("); found {
var t PT
if len(unit) > 0 {
r.LowerType = pgtype.Exclusive
err := t.Parse(unit)
if err != nil {
return r, errors.New("无法解析给定的最低范围值。")
}
} else {
r.LowerType = pgtype.Unbounded
}
r.Lower = *t
}
if unit, found := strings.CutSuffix(rangeUnit[1], "]"); found {
var t PT
if len(unit) > 0 {
r.UpperType = pgtype.Inclusive
err := t.Parse(unit)
if err != nil {
return r, errors.New("无法解析给定的最高范围值。")
}
} else {
r.UpperType = pgtype.Unbounded
}
r.Upper = *t
}
if unit, found := strings.CutSuffix(rangeUnit[1], ")"); found {
var t PT
if len(unit) > 0 {
r.UpperType = pgtype.Exclusive
err := t.Parse(unit)
if err != nil {
return r, errors.New("无法解析给定的最高范围值。")
}
} else {
r.UpperType = pgtype.Unbounded
}
r.Upper = *t
}
r.Valid = true
return r, nil
}
// 将一个范围类型的值转换为一个字符串、
func assembleRange[T ToString](r pgtype.Range[T]) string {
var sb strings.Builder
if r.LowerType == pgtype.Empty || r.UpperType == pgtype.Empty {
return "empty"
}
if r.LowerType == pgtype.Inclusive {
sb.WriteString("[")
} else {
sb.WriteString("(")
}
if r.LowerType != pgtype.Unbounded {
sb.WriteString(r.Lower.ToString())
}
sb.WriteString(",")
if r.UpperType != pgtype.Unbounded {
sb.WriteString(r.Upper.ToString())
}
if r.UpperType == pgtype.Inclusive {
sb.WriteString("]")
} else {
sb.WriteString(")")
}
return sb.String()
}

39
vo/invoice.go Normal file
View File

@ -0,0 +1,39 @@
package vo
import (
"electricity_bill_calc/model"
"electricity_bill_calc/types"
"github.com/shopspring/decimal"
)
type InvoiceResponse struct {
No string `json:"no" copier:"InvoiceNo"`
Tenement string `json:"tenement"`
Title model.InvoiceTitle `json:"title" copier:"Info"`
IssuedAt types.DateTime `json:"issuedAt"`
Amount decimal.Decimal `json:"amount" copier:"Total"`
TaxMethod int16 `json:"taxMethod"`
TaxRate decimal.Decimal `json:"taxRate"`
InvoiceType string `json:"invoiceType" copier:"Type"`
}
type InvoiceCreationForm struct {
Park string `json:"park"`
Tenement string `json:"tenement"`
TaxMethod int16 `json:"taxMethod"`
TaxRate decimal.NullDecimal `json:"taxRate"`
Covers []string `json:"covers"`
}
type ExtendedInvoiceResponse struct {
InvoiceResponse
Cargos []*model.InvoiceCargo `json:"cargos"`
Covers []*model.SimplifiedTenementCharge `json:"covers"`
}
type ExtendedInvoiceCreationForm struct {
InvoiceCreationForm
InvoiceType *string `json:"invoiceType"`
InvoiceNo string `json:"invoiceNo"`
}

64
vo/meter.go Normal file
View File

@ -0,0 +1,64 @@
package vo
import (
"electricity_bill_calc/types"
"github.com/shopspring/decimal"
)
type MeterCreationForm struct {
Code string `json:"code"`
Address *string `json:"address"`
Ratio decimal.Decimal `json:"ratio"`
Seq int64 `json:"seq"`
MeterType int16 `json:"type"`
Building *string `json:"building"`
OnFloor *string `json:"onFloor"`
Area decimal.NullDecimal `json:"area"`
Enabled bool `json:"enabled"`
MeterReadingForm `json:"-"`
}
type MeterModificationForm struct {
Address *string `json:"address"`
Seq int64 `json:"seq"`
Ratio decimal.Decimal `json:"ratio"`
Enabled bool `json:"enabled"`
MeterType int16 `json:"type"`
Building *string `json:"building"`
OnFloor *string `json:"onFloor"`
Area decimal.NullDecimal `json:"area"`
}
type NewMeterForReplacingForm struct {
Code string `json:"code"`
Ratio decimal.Decimal `json:"ratio"`
Reading MeterReadingForm `json:"reading"`
}
type MeterReplacingForm struct {
OldReading MeterReadingForm `json:"oldReading"`
NewMeter NewMeterForReplacingForm `json:"newMeter"`
}
type SimplifiedMeterQueryResponse struct {
Code string `json:"code"`
Address *string `json:"address"`
Park string `json:"parkId"`
}
type SimplifiedMeterDetailResponse struct {
Code string `json:"code"`
Park string `json:"parkId"`
Address *string `json:"address"`
Seq int64 `json:"seq"`
Ratio decimal.Decimal `json:"ratio"`
Building *string `json:"building"`
BuildingName *string `json:"buildingName"`
OnFloor *string `json:"onFloor"`
Area decimal.Decimal `json:"area"`
Enabled bool `json:"enabled"`
MeterType int16 `json:"type"`
AttachedAt types.DateTime `json:"attachedAt"`
DetachedAt *types.DateTime `json:"detachedAt"`
}

96
vo/park.go Normal file
View File

@ -0,0 +1,96 @@
package vo
import (
"electricity_bill_calc/model"
"electricity_bill_calc/tools"
"github.com/shopspring/decimal"
)
type ParkInformationForm struct {
Name string `json:"name"`
Region *string `json:"region"`
Address *string `json:"address"`
Contact *string `json:"contact"`
Phone *string `json:"phone"`
Area *string `json:"area"`
Capacity *string `json:"capacity"`
TenementQuantity *string `json:"tenement"`
TaxRate *string `json:"taxRate"`
Category int16 `json:"category"`
MeterType int16 `json:"submeter"`
PricePolicy int16 `json:"pricePolicy"`
BasicPooled int16 `json:"basicDiluted"`
AdjustPooled int16 `json:"adjustDiluted"`
LossPooled int16 `json:"lossDiluted"`
PublicPooled int16 `json:"publicDiluted"`
}
func (pcf ParkInformationForm) TryIntoPark() (*model.Park, error) {
area, err := tools.NewNullDecimalFromString(pcf.Area)
if err != nil {
return nil, err
}
tenementQuantity, err := tools.NewNullDecimalFromString(pcf.TenementQuantity)
if err != nil {
return nil, err
}
capacity, err := tools.NewNullDecimalFromString(pcf.Capacity)
if err != nil {
return nil, err
}
taxRate, err := tools.NewNullDecimalFromString(pcf.TaxRate)
if err != nil {
return nil, err
}
return &model.Park{
Name: pcf.Name,
Region: pcf.Region,
Address: pcf.Address,
Contact: pcf.Contact,
Phone: pcf.Phone,
Area: area,
TenementQuantity: tenementQuantity,
Capacity: capacity,
Category: pcf.Category,
MeterType: pcf.MeterType,
PricePolicy: pcf.PricePolicy,
BasicPooled: pcf.BasicPooled,
AdjustPooled: pcf.AdjustPooled,
LossPooled: pcf.LossPooled,
PublicPooled: pcf.PublicPooled,
TaxRate: taxRate,
}, nil
}
type ParkBuildingInformationForm struct {
Name string `json:"name"`
Floors string `json:"floors"`
}
type SimplifiedParkDetail struct {
Id string `json:"id"`
UserId string `json:"userId"`
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)
}

80
vo/reading.go Normal file
View File

@ -0,0 +1,80 @@
package vo
import (
"electricity_bill_calc/model"
"electricity_bill_calc/types"
"fmt"
"github.com/shopspring/decimal"
)
type MeterReadingForm struct {
Overall decimal.Decimal `json:"overall"`
Critical decimal.Decimal `json:"critical"`
Peak decimal.Decimal `json:"peak"`
Flat decimal.Decimal `json:"flat"`
Valley decimal.Decimal `json:"valley"`
ReadAt *types.DateTime `json:"readAt"`
}
func (r MeterReadingForm) Validate() bool {
flat := r.Overall.Sub(r.Critical).Sub(r.Peak).Sub(r.Valley)
return flat.GreaterThanOrEqual(decimal.Zero)
}
type MeterReadingFormWithCode struct {
Code string `json:"code"`
MeterReadingForm
}
type MeterReadingDetailResponse struct {
Code string `json:"code"`
Park string `json:"parkId"`
Address *string `json:"address"`
Seq int64 `json:"seq"`
Ratio decimal.Decimal `json:"ratio"`
MeterType int16 `json:"type"`
Enabled bool `json:"enabled"`
Building *string `json:"building"`
BuildingName *string `json:"buildingName"`
OnFloor *string `json:"onFloor"`
Area decimal.Decimal `json:"area"`
AttachedAt *types.DateTime `json:"attachedAt"`
DetachedAt *types.DateTime `json:"detachedAt"`
CreatedAt types.DateTime `json:"createdAt"`
LastModifiedAt *types.DateTime `json:"lastModifiedAt"`
ReadAt types.DateTime `json:"readAt"`
ReadAtTimestamp string `json:"readAtTimestamp"`
Overall decimal.Decimal `json:"overall"`
Critical decimal.Decimal `json:"critical"`
Peak decimal.Decimal `json:"peak"`
Flat decimal.Decimal `json:"flat"`
Valley decimal.Decimal `json:"valley"`
}
func FromDetailedMeterReading(reading model.DetailedMeterReading) MeterReadingDetailResponse {
return MeterReadingDetailResponse{
Code: reading.Detail.Code,
Park: reading.Detail.Park,
Address: reading.Detail.Address,
Seq: reading.Detail.Seq,
Ratio: reading.Detail.Ratio,
MeterType: reading.Detail.MeterType,
Enabled: reading.Detail.Enabled,
Building: reading.Detail.Building,
BuildingName: reading.Detail.BuildingName,
OnFloor: reading.Detail.OnFloor,
Area: reading.Detail.Area.Decimal,
AttachedAt: reading.Detail.AttachedAt,
DetachedAt: (*types.DateTime)(reading.Detail.DetachedAt),
CreatedAt: types.DateTime(reading.Detail.CreatedAt),
LastModifiedAt: (*types.DateTime)(&reading.Detail.LastModifiedAt),
ReadAt: reading.Reading.ReadAt,
ReadAtTimestamp: fmt.Sprintf("%d", reading.Reading.ReadAt.UnixMicro()),
Overall: reading.Reading.Overall,
Critical: reading.Reading.Critical,
Peak: reading.Reading.Peak,
Flat: reading.Reading.Flat,
Valley: reading.Reading.Valley,
}
}

323
vo/report.go Normal file
View File

@ -0,0 +1,323 @@
package vo
import (
"electricity_bill_calc/model"
"electricity_bill_calc/types"
"github.com/jinzhu/copier"
"github.com/shopspring/decimal"
)
type ReportCreationForm struct {
Park string `json:"parkId"`
PeriodBegin types.Date `json:"periodBegin"`
PeriodEnd types.Date `json:"periodEnd"`
Overall decimal.Decimal `json:"overall"`
OverallFee decimal.Decimal `json:"overallFee"`
Critical decimal.Decimal `json:"critical"`
CriticalFee decimal.Decimal `json:"criticalFee"`
Peak decimal.Decimal `json:"peak"`
PeakFee decimal.Decimal `json:"peakFee"`
Flat decimal.Decimal `json:"flat,omitempty"`
FlatFee decimal.Decimal `json:"flatFee,omitempty"`
Valley decimal.Decimal `json:"valley"`
ValleyFee decimal.Decimal `json:"valleyFee"`
BasicFee decimal.Decimal `json:"basicFee"`
AdjustFee decimal.Decimal `json:"adjustFee"`
}
type ReportModifyForm struct {
PeriodBegin types.Date `json:"periodBegin"`
PeriodEnd types.Date `json:"periodEnd"`
Overall decimal.Decimal `json:"overall"`
OverallFee decimal.Decimal `json:"overallFee"`
Critical decimal.Decimal `json:"critical"`
CriticalFee decimal.Decimal `json:"criticalFee"`
Peak decimal.Decimal `json:"peak"`
PeakFee decimal.Decimal `json:"peakFee"`
Flat decimal.Decimal `json:"flat,omitempty"`
FlatFee decimal.Decimal `json:"flatFee,omitempty"`
Valley decimal.Decimal `json:"valley"`
ValleyFee decimal.Decimal `json:"valleyFee"`
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"`
}
type BasicReportIndexResponse struct {
Id string `json:"id"`
Park string `json:"parkId"`
PeriodBegin types.Date `json:"periodBegin"`
PeriodEnd types.Date `json:"periodEnd"`
Category int16 `json:"category"`
MeterType int16 `json:"meter04kvType"`
PricePolicy int16 `json:"pricePolicy"`
BasisPooled int16 `json:"basisDiluted"`
AdjustPooled int16 `json:"adjustDiluted"`
LossPooled int16 `json:"lossDiluted"`
PublicPooled int16 `json:"publicDiluted"`
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"`
CreatedAt types.DateTime `json:"createdAt"`
LastModifiedAt types.DateTime `json:"lastModifiedAt"`
}
func (bri *BasicReportIndexResponse) Period(p types.DateRange) {
bri.PeriodBegin = p.SafeLower()
bri.PeriodEnd = p.SafeUpper()
}
type ReportDetailQueryResponse struct {
Enterprise SimplifiedUserDetail `json:"enterprise"`
Park SimplifiedParkDetail `json:"park"`
Report BasicReportIndexResponse `json:"report"`
}
func NewReportDetailQueryResponse(user *model.UserDetail, park *model.Park, report *model.ReportIndex) ReportDetailQueryResponse {
var response ReportDetailQueryResponse
copier.Copy(&response.Enterprise, user)
copier.Copy(&response.Park, park)
copier.Copy(&response.Report, report)
return response
}
type ParkSummaryResponse struct {
ReportId string `json:"reportId"`
OverallDisplay ConsumptionDisplay `json:"overall"`
Area decimal.Decimal `json:"area"`
BasicFee decimal.Decimal `json:"basicFee"`
PooledBasicFeeByAmount decimal.Decimal `json:"pooledBasicFeeByAmount"`
PooledBasicFeeByArea decimal.Decimal `json:"pooledBasicFeeByArea"`
AdjustFee decimal.Decimal `json:"adjustFee"`
PooledAdjustFeeByAmount decimal.Decimal `json:"pooledAdjustFeeByAmount"`
PooledAdjustFeeByArea decimal.Decimal `json:"pooledAdjustFeeByArea"`
Consumption decimal.Decimal `json:"consumption"`
Loss decimal.Decimal `json:"loss"`
LossRate decimal.Decimal `json:"lossRate"`
}
func (psr *ParkSummaryResponse) Overall(value model.ConsumptionUnit) {
psr.OverallDisplay.FromConsumptionUnit(&value)
}
type SimplifiedReportSummary struct {
Overall model.ConsumptionUnit `json:"overall"`
Critical model.ConsumptionUnit `json:"critical"`
Peak model.ConsumptionUnit `json:"peak"`
Flat model.ConsumptionUnit `json:"flat"`
Valley model.ConsumptionUnit `json:"valley"`
BasicFee decimal.Decimal `json:"basicFee"`
AdjustFee decimal.Decimal `json:"adjustFee"`
ConsumptionFee decimal.Decimal `json:"consumptionFee" copier:"GetConsumptionFee"`
}
type TestCalculateForm struct {
Overall decimal.Decimal `json:"overall"`
OverallFee decimal.Decimal `json:"overallFee"`
Critical decimal.Decimal `json:"critical"`
CriticalFee decimal.Decimal `json:"criticalFee"`
Peak decimal.Decimal `json:"peak"`
PeakFee decimal.Decimal `json:"peakFee"`
Valley decimal.Decimal `json:"valley"`
ValleyFee decimal.Decimal `json:"valleyFee"`
BasicFee decimal.Decimal `json:"basicFee"`
AdjustFee decimal.Decimal `json:"adjustFee"`
}
type TestCalculateResult struct {
OverallPrice decimal.Decimal `json:"overallPrice"`
CriticalPrice decimal.Decimal `json:"criticalPrice"`
PeakPrice decimal.Decimal `json:"peakPrice"`
Flat decimal.Decimal `json:"flat"`
FlatFee decimal.Decimal `json:"flatFee"`
FlatPrice decimal.Decimal `json:"flatPrice"`
ValleyPrice decimal.Decimal `json:"valleyPrice"`
ConsumptionFee decimal.Decimal `json:"consumptionFee"`
}
func (t TestCalculateForm) Calculate() TestCalculateResult {
var r TestCalculateResult = TestCalculateResult{}
r.ConsumptionFee = t.OverallFee.Sub(t.BasicFee).Sub(t.AdjustFee)
if t.Overall.GreaterThan(decimal.Zero) {
r.OverallPrice = r.ConsumptionFee.Div(t.Overall).RoundBank(8)
}
if t.Critical.GreaterThan(decimal.Zero) {
r.CriticalPrice = t.CriticalFee.Div(t.Critical).RoundBank(8)
}
if t.Peak.GreaterThan(decimal.Zero) {
r.PeakPrice = t.PeakFee.Div(t.Peak).RoundBank(8)
}
r.Flat = t.Overall.Sub(t.Critical).Sub(t.Peak).Sub(t.Valley)
r.FlatFee = r.ConsumptionFee.Sub(t.CriticalFee).Sub(t.PeakFee).Sub(t.ValleyFee).RoundBank(8)
if r.Flat.GreaterThan(decimal.Zero) {
r.FlatPrice = r.FlatFee.Div(r.Flat).RoundBank(8)
}
r.ConsumptionFee = r.ConsumptionFee.RoundBank(8)
return r
}
type ReportCalculateTaskStatusResponse struct {
Id string `json:"id"`
Status int16 `json:"status"`
Message *string `json:"message"`
}
type ReportPublicQueryResponse struct {
SimplifiedMeterDetailResponse
Overall ConsumptionDisplay `json:"overall"`
AdjustLoss ConsumptionDisplay `json:"adjustLoss"`
}
func (rpqr *ReportPublicQueryResponse) FromReportDetailPublicConsumption(value *model.ReportDetailedPublicConsumption) {
copier.Copy(&rpqr.SimplifiedMeterDetailResponse, &value.MeterDetail)
rpqr.Overall.FromConsumptionUnit(&value.ReportPublicConsumption.Overall)
rpqr.Overall.Amount(value.ReportPublicConsumption.Overall.Amount.Add(value.ReportPublicConsumption.LossAdjust.Amount))
rpqr.AdjustLoss.FromConsumptionUnit(&value.ReportPublicConsumption.LossAdjust)
}
type ReportPooledQueryResponse struct {
SimplifiedMeterDetailResponse
Overall ConsumptionDisplay `json:"overall"`
PoolMethod int16 `json:"poolMethod"`
}
func (rpqr *ReportPooledQueryResponse) FromReportDetailPooledConsumption(value *model.ReportDetailedPooledConsumption) {
copier.Copy(&rpqr.SimplifiedMeterDetailResponse, &value.MeterDetail)
rpqr.Overall.FromConsumptionUnit(&value.ReportPooledConsumption.Overall)
rpqr.PoolMethod = value.PublicPooled
}
func (rpqr *ReportPooledQueryResponse) FromReportDetailNestedMeterConsumption(value *model.ReportDetailNestedMeterConsumption) {
copier.Copy(&rpqr.SimplifiedMeterDetailResponse, &value.Meter)
rpqr.Overall.FromConsumptionUnit(&value.Consumption.Overall)
rpqr.PoolMethod = -1
}
type ReportTenementSummaryResponse struct {
SimplifiedTenementDetailResponse
Consumption decimal.Decimal `json:"consumption"`
Fee decimal.Decimal `json:"fee"`
Pooled decimal.Decimal `json:"pooled"`
Total decimal.Decimal `json:"final"`
}
func (rtsr *ReportTenementSummaryResponse) FromReportTenement(value *model.ReportTenement) {
copier.Copy(&rtsr.SimplifiedTenementDetailResponse, &value.Detail)
fee := value.BasicFeePooled.Add(value.AdjustFeePooled).Add(value.LossFeePooled)
rtsr.Consumption = value.Overall.Amount
rtsr.Fee = fee
rtsr.Pooled = value.FinalPooled
rtsr.Total = value.FinalCharge
}
type ReportTenementComprehensiveDetailResponse struct {
Consumption decimal.Decimal `json:"consumption"`
Fee decimal.Decimal `json:"fee"`
Price decimal.Decimal `json:"price"`
BasicPooled decimal.Decimal `json:"basicPooled"`
AdjustPooled decimal.Decimal `json:"adjustPooled"`
LossPooled decimal.Decimal `json:"lossPooled"`
PublicPooled decimal.Decimal `json:"publicPooled"`
Total decimal.Decimal `json:"total"`
}
func (rtcdr *ReportTenementComprehensiveDetailResponse) FromReportTenement(value *model.ReportTenement) {
rtcdr.Consumption = value.Overall.Amount
rtcdr.Fee = value.Overall.Fee
rtcdr.Price = value.Overall.Price
rtcdr.BasicPooled = value.BasicFeePooled
rtcdr.AdjustPooled = value.AdjustFeePooled
rtcdr.LossPooled = value.LossFeePooled
rtcdr.PublicPooled = value.FinalPooled
rtcdr.Total = value.FinalCharge
}
type ReportMeterDetailResponse struct {
SimplifiedMeterDetailResponse
Overall ConsumptionDisplay `json:"overall"`
Critical ConsumptionDisplay `json:"critical"`
Peak ConsumptionDisplay `json:"peak"`
Flat ConsumptionDisplay `json:"flat"`
Valley ConsumptionDisplay `json:"valley"`
}
func (rmdr *ReportMeterDetailResponse) FromNestedMeter(value *model.NestedMeter) {
copier.Copy(&rmdr.SimplifiedMeterDetailResponse, &value.MeterDetail)
rmdr.Overall.FromConsumptionUnit(&value.Overall)
rmdr.Critical.FromConsumptionUnit(&value.Critical)
rmdr.Peak.FromConsumptionUnit(&value.Peak)
rmdr.Flat.FromConsumptionUnit(&value.Flat)
rmdr.Valley.FromConsumptionUnit(&value.Valley)
}
type ReportMeterExtendedDetailResponse struct {
ReportMeterDetailResponse
BasicPooled decimal.Decimal `json:"basicPooled"`
AdjustPooled decimal.Decimal `json:"adjustPooled"`
LossPooled decimal.Decimal `json:"lossPooled"`
PublicPooled decimal.Decimal `json:"publicPooled"`
FinalTotal decimal.Decimal `json:"finalTotal"`
}
func (rmedr *ReportMeterExtendedDetailResponse) FromNestedMeter(value *model.NestedMeter) {
rmedr.ReportMeterDetailResponse.FromNestedMeter(value)
rmedr.BasicPooled = value.BasicPooled
rmedr.AdjustPooled = value.AdjustPooled
rmedr.LossPooled = value.LossPooled
rmedr.PublicPooled = value.PublicPooled
rmedr.FinalTotal = value.FinalTotal
}
type ReportTenementDetailResponse struct {
Tenement SimplifiedTenementDetailResponse `json:"tenement"`
Comprehensive ReportTenementComprehensiveDetailResponse `json:"comprehensive"`
Meters []ReportMeterExtendedDetailResponse `json:"meters"`
Pooled []ReportMeterDetailResponse `json:"pooled"`
}
func (rtdr *ReportTenementDetailResponse) FromReportTenement(value *model.ReportTenement) {
copier.Copy(&rtdr.Tenement, &value.Detail)
rtdr.Comprehensive.FromReportTenement(value)
rtdr.Meters = make([]ReportMeterExtendedDetailResponse, len(value.Meters))
for i, v := range value.Meters {
rtdr.Meters[i].FromNestedMeter(&v)
}
rtdr.Pooled = make([]ReportMeterDetailResponse, len(value.Pooled))
for i, v := range value.Pooled {
rtdr.Pooled[i].FromNestedMeter(&v)
}
}

43
vo/shares.go Normal file
View File

@ -0,0 +1,43 @@
package vo
import (
"electricity_bill_calc/model"
"github.com/shopspring/decimal"
)
type StateForm struct {
Enabled bool `json:"enabled"`
}
type ConsumptionDisplay struct {
AmountStr string `json:"amount"`
FeeStr string `json:"fee"`
PriceStr string `json:"price"`
ProportionStr string `json:"proportion"`
}
func (cd *ConsumptionDisplay) Amount(a decimal.Decimal) *ConsumptionDisplay {
cd.AmountStr = a.StringFixedBank(4)
return cd
}
func (cd *ConsumptionDisplay) Fee(f decimal.Decimal) *ConsumptionDisplay {
cd.FeeStr = f.StringFixedBank(4)
return cd
}
func (cd *ConsumptionDisplay) Price(p decimal.Decimal) *ConsumptionDisplay {
cd.PriceStr = p.StringFixedBank(8)
return cd
}
func (cd *ConsumptionDisplay) Proportion(p decimal.Decimal) *ConsumptionDisplay {
cd.ProportionStr = p.StringFixedBank(8)
return cd
}
func (cd *ConsumptionDisplay) FromConsumptionUnit(cu *model.ConsumptionUnit) ConsumptionDisplay {
cd.Amount(cu.Amount).Fee(cu.Fee).Price(cu.Price).Proportion(cu.Proportion)
return *cd
}

29
vo/synchronize.go Normal file
View File

@ -0,0 +1,29 @@
package vo
type SynchronizeConfiguration struct {
CollectAt string `json:"collectAt"` // 采集时间格式HH:mm
EntID string `json:"entId"` // 企业ID
Imrs string `json:"imrs"` // 采集系统型号
ImrsAccount string `json:"imrsAccount"` // 同步登录账号
ImrsKey string `json:"imrsKey"` // 同步登录私钥Base64或者私钥文件内容
ImrsSecret string `json:"imrsSecret"` // 同步登录密钥,加盐双向加密
Interval float64 `json:"interval"` // 采集周期0每小时1每日2每周3每月
MaxRetries string `json:"maxRetries"` // 最大重试次数
ParkID string `json:"parkId"` // 园区ID
ReadingType float64 `json:"readingType"` // 采集方式0自动+人工1自动2人工
RetryAlgorithm float64 `json:"retryAlgorithm"` // 重试间隔算法0指数退避12倍线性间隔23倍线性间隔3固定间隔
RetryInterval string `json:"retryInterval"` // 重试间隔,基础间隔时间,根据间隔算法不同会产生不同的间隔
}
type SynchronizeConfigurationCreateForm struct {
CollectAt string `json:"collectAt"` // 采集时间格式HH:mm
Imrs string `json:"imrs"` // 采集系统型号,为空的时候表示不同步
ImrsAccount string `json:"imrsAccount"` // 同步登录账号
ImrsKey string `json:"imrsKey"` // 同步登录私钥Base64或者私钥文件内容
ImrsSecret string `json:"imrsSecret"` // 同步登录密钥,加盐双向加密
Interval float64 `json:"interval"` // 采集周期0每小时1每日2每周3每月
MaxRetries string `json:"maxRetries"` // 最大重试次数
ParkID string `json:"parkId"` // 园区ID
ReadingType float64 `json:"readingType"` // 采集方式0自动+人工1自动2人工
RetryAlgorithm float64 `json:"retryAlgorithm"` // 重试间隔算法0指数退避12倍线性间隔23倍线性间隔3固定间隔
RetryInterval string `json:"retryInterval"` // 重试间隔,基础间隔时间,根据间隔算法不同会产生不同的间隔
}

75
vo/tenement.go Normal file
View File

@ -0,0 +1,75 @@
package vo
import (
"electricity_bill_calc/model"
"electricity_bill_calc/types"
)
type TenementCreationForm struct {
Name string `json:"name"`
ShortName *string `json:"shortName"`
Address string `json:"address"`
Contact string `json:"contact"`
Phone string `json:"phone"`
Building *string `json:"building"`
OnFloor *string `json:"onFloor"`
USCI string `json:"usci"`
InvoiceAddress *string `json:"invoiceAddress"`
InvoicePhone *string `json:"invoicePhone"`
Bank *string `json:"bank"`
Account *string `json:"bankAccount"`
}
type TenementQueryResponse struct {
Id string `json:"id"`
FullName string `json:"fullName"`
ShortName *string `json:"shortName"`
Address *string `json:"address"`
Contact *string `json:"contact" copier:"ContactName"`
Phone *string `json:"phone" copier:"ContactPhone"`
Building *string `json:"building"`
BuildingName *string `json:"buildingName"`
OnFloor *string `json:"onFloor"`
MovedInAt *types.Date `json:"movedInAt"`
MovedOutAt *types.Date `json:"movedOutAt"`
CreatedAt types.DateTime `json:"createdAt"`
LastModifiedAt *types.DateTime `json:"lastModifiedAt"`
}
type SimplifiedTenementResponse struct {
Id string `json:"id"`
FullName string `json:"fullName"`
ShortName *string `json:"shortName"`
Park string `json:"park"`
}
type TenementDetailResponse struct {
Id string `json:"id"`
FullName string `json:"fullName"`
ShortName *string `json:"shortName"`
Address string `json:"address"`
Contact string `json:"contact" copier:"ContactName"`
Phone string `json:"phone" copier:"ContactPhone"`
Building string `json:"building"`
BuildingName *string `json:"buildingName"`
OnFloor *string `json:"onFloor"`
InvoiceInfo *model.InvoiceTitle `json:"invoiceInfo"`
MovedInAt *types.Date `json:"movedInAt"`
MovedOutAt *types.Date `json:"movedOutAt"`
CreatedAt types.DateTime `json:"createdAt"`
LastModifiedAt *types.DateTime `json:"lastModifiedAt"`
}
type SimplifiedTenementDetailResponse struct {
Id string `json:"id"`
FullName string `json:"fullName"`
ShortName *string `json:"shortName"`
Address string `json:"address"`
Contact string `json:"contact" copier:"ContactName"`
Phone string `json:"phone" copier:"ContactPhone"`
Building string `json:"building"`
BuildingName *string `json:"buildingName"`
OnFloor *string `json:"onFloor"`
MovedInAt *types.Date `json:"movedInAt"`
MovedOutAt *types.Date `json:"movedOutAt"`
}

25
vo/top_up.go Normal file
View File

@ -0,0 +1,25 @@
package vo
import (
"electricity_bill_calc/types"
"github.com/shopspring/decimal"
)
type TopUpCreationForm struct {
Tenement string `json:"tenement"`
Meter string `json:"meter"`
Amount decimal.Decimal `json:"amount"`
}
type TopUpDetailQueryResponse struct {
Id string `json:"id" copier:"TopUpCode"`
Tenement string `json:"tenement"`
TenementName string `json:"tenementName"`
Meter string `json:"meter"`
MeterAddress string `json:"meterAddress"`
ToppedUpAt types.DateTime `json:"toppedUpAt"`
Amount decimal.Decimal `json:"amount"`
PaymentType int16 `json:"paymentType"`
SyncStatus int16 `json:"syncStatus" copier:"SyncStatus"`
}

141
vo/user.go Normal file
View File

@ -0,0 +1,141 @@
package vo
import (
"electricity_bill_calc/model"
"electricity_bill_calc/tools"
"electricity_bill_calc/types"
"time"
"github.com/shopspring/decimal"
)
type MGTAndOPSAccountCreationForm struct {
Username string `json:"username"`
Name string `json:"name"`
Contact *string `json:"contact"`
Phone *string `json:"phone"`
UserType int16 `json:"type"`
}
func (u MGTAndOPSAccountCreationForm) IntoUser() *model.User {
return &model.User{
Username: u.Username,
Password: "",
ResetNeeded: false,
UserType: u.UserType,
Enabled: true,
CreatedAt: nil,
}
}
func (u MGTAndOPSAccountCreationForm) IntoUserDetail() *model.UserDetail {
return &model.UserDetail{
Id: "",
Name: &u.Name,
Abbr: nil,
Region: nil,
Address: nil,
Contact: u.Contact,
Phone: u.Phone,
UnitServiceFee: decimal.Zero,
ServiceExpiration: types.NewDate(2099, time.December, 31),
CreatedAt: types.Now(),
CreatedBy: nil,
LastModifiedAt: types.Now(),
LastModifiedBy: nil,
DeletedAt: nil,
DeletedBy: nil,
}
}
type EnterpriseAccountCreationForm struct {
Username string `json:"username"`
Name string `json:"name"`
Region *string `json:"region"`
Address *string `json:"address"`
Contact *string `json:"contact"`
Phone *string `json:"phone"`
UnitServiceFee string `json:"unitServiceFee"`
}
func (u EnterpriseAccountCreationForm) IntoUser() *model.User {
return &model.User{
Username: u.Username,
Password: "",
ResetNeeded: false,
UserType: model.USER_TYPE_ENT,
Enabled: true,
CreatedAt: nil,
}
}
func (u EnterpriseAccountCreationForm) IntoUserDetail() (*model.UserDetail, error) {
unitServiceFee, err := decimal.NewFromString(u.UnitServiceFee)
if err != nil {
return nil, err
}
return &model.UserDetail{
Name: &u.Name,
Abbr: nil,
Region: u.Region,
Address: u.Address,
Contact: u.Contact,
Phone: u.Phone,
UnitServiceFee: unitServiceFee,
ServiceExpiration: types.NewDate(2000, time.January, 1),
CreatedAt: types.Now(),
CreatedBy: nil,
LastModifiedAt: types.Now(),
LastModifiedBy: nil,
DeletedAt: nil,
DeletedBy: nil,
}, nil
}
type UserDetailModificationForm struct {
Name string `json:"name"`
Region *string `json:"region"`
Address *string `json:"address"`
Contact *string `json:"contact"`
Phone *string `json:"phone"`
UnitServiceFee *string `json:"unitServiceFee"`
}
func (u UserDetailModificationForm) IntoModificationModel() (*model.UserModificationForm, error) {
unitServiceFee, err := decimal.NewFromString(*u.UnitServiceFee)
if err != nil {
return nil, err
}
return &model.UserModificationForm{
Name: u.Name,
Region: u.Region,
Address: u.Address,
Contact: u.Contact,
Phone: u.Phone,
UnitServiceFee: &unitServiceFee,
}, nil
}
type UserStateChangeForm struct {
Uid string `json:"uid"`
Enabled bool `json:"enabled"`
}
type RepasswordForm struct {
VerifyCode string `json:"verifyCode"`
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, "")
}