forked from free-lancers/electricity_bill_calc_service
		
	合并分支
This commit is contained in:
		| @@ -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
									
								
							
							
						
						
									
										183
									
								
								_1.gitignore
									
									
									
									
									
										Normal 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
									
									
								
							
							
						
						
									
										40
									
								
								cache/abstract.go
									
									
									
									
										vendored
									
									
								
							| @@ -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
									
									
								
							
							
						
						
									
										10
									
								
								cache/count.go
									
									
									
									
										vendored
									
									
								
							| @@ -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
									
									
								
							
							
						
						
									
										12
									
								
								cache/entity.go
									
									
									
									
										vendored
									
									
								
							| @@ -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
									
									
								
							
							
						
						
									
										6
									
								
								cache/exists.go
									
									
									
									
										vendored
									
									
								
							| @@ -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
									
									
								
							
							
						
						
									
										12
									
								
								cache/relation.go
									
									
									
									
										vendored
									
									
								
							| @@ -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
									
									
								
							
							
						
						
									
										62
									
								
								cache/search.go
									
									
									
									
										vendored
									
									
								
							| @@ -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
									
									
								
							
							
						
						
									
										4
									
								
								cache/session.go
									
									
									
									
										vendored
									
									
								
							| @@ -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) { | ||||
|   | ||||
| @@ -31,8 +31,9 @@ type RedisSetting struct { | ||||
|  | ||||
| type ServiceSetting struct { | ||||
| 	MaxSessionLife time.Duration | ||||
| 	ItemsPageSize  int | ||||
| 	ItemsPageSize  uint | ||||
| 	CacheLifeTime  time.Duration | ||||
| 	HostSerial     int64 | ||||
| } | ||||
|  | ||||
| // 定义全局变量 | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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: ¤tTime, | ||||
| 		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
									
								
							
							
						
						
									
										227
									
								
								controller/invoice.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										502
									
								
								controller/meter.go
									
									
									
									
									
										Normal 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}) | ||||
| } | ||||
| @@ -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("已删除指定的建筑") | ||||
| } | ||||
|   | ||||
| @@ -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()) | ||||
| 	} | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										121
									
								
								controller/sync.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										286
									
								
								controller/tenement.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										142
									
								
								controller/top_up.go
									
									
									
									
									
										Normal 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( | ||||
| 		"已经删除一条指定的商户充值记录", | ||||
| 	) | ||||
| } | ||||
| @@ -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}) | ||||
| } | ||||
|   | ||||
| @@ -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("指定公示报表的撤回申请已经完成审核") | ||||
| } | ||||
|   | ||||
| @@ -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)) | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										131
									
								
								excel/meter_reading.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										27
									
								
								excel/tenement.go
									
									
									
									
									
										Normal 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) | ||||
| //} | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
							
								
								
									
										16
									
								
								exceptions/insufficient_data.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								exceptions/insufficient_data.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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() | ||||
| } | ||||
|   | ||||
							
								
								
									
										112
									
								
								global/db.go
									
									
									
									
									
								
							
							
						
						
									
										112
									
								
								global/db.go
									
									
									
									
									
								
							| @@ -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
									
									
									
									
									
								
							
							
						
						
									
										74
									
								
								go.mod
									
									
									
									
									
								
							| @@ -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
									
									
									
									
									
								
							
							
						
						
									
										164
									
								
								go.sum
									
									
									
									
									
								
							| @@ -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= | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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
									
									
									
									
									
								
							
							
						
						
									
										152
									
								
								main.go
									
									
									
									
									
								
							| @@ -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)) | ||||
|   | ||||
							
								
								
									
										92
									
								
								model/calculate/calculate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								model/calculate/calculate.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										32
									
								
								model/charge.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										10
									
								
								model/cunsumption.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										90
									
								
								model/enums.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										45
									
								
								model/invoice.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										106
									
								
								model/meter.go
									
									
									
									
									
										Normal 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"` | ||||
| } | ||||
| @@ -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
									
								
							
							
						
						
									
										14
									
								
								model/park_building.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										56
									
								
								model/reading.go
									
									
									
									
									
										Normal 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"` | ||||
| } | ||||
| @@ -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"` | ||||
| } | ||||
|   | ||||
							
								
								
									
										192
									
								
								model/report.go
									
									
									
									
									
								
							
							
						
						
									
										192
									
								
								model/report.go
									
									
									
									
									
								
							| @@ -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"` | ||||
| } | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										38
									
								
								model/synchronize.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										23
									
								
								model/tenement.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										33
									
								
								model/top_up.go
									
									
									
									
									
										Normal 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 | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										130
									
								
								model/user.go
									
									
									
									
									
								
							
							
						
						
									
										130
									
								
								model/user.go
									
									
									
									
									
								
							| @@ -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
									
								
							
							
						
						
									
										70
									
								
								repository/calculate.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										152
									
								
								repository/charge.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										19
									
								
								repository/god.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										348
									
								
								repository/invoice.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										991
									
								
								repository/meter.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										419
									
								
								repository/park.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										79
									
								
								repository/region.go
									
									
									
									
									
										Normal 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, ®ions, 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, ®ion, regionQuerySql, regionParams...); err != nil { | ||||
| 		r.log.Error("获取指定行政区划信息失败!", zap.Error(err)) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return ®ion, 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
									
								
							
							
						
						
									
										846
									
								
								repository/report.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										208
									
								
								repository/synchronize.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										458
									
								
								repository/tenement.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										166
									
								
								repository/top_up.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										419
									
								
								repository/user.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
| @@ -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格式响应 | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										455
									
								
								service/calculate/meters.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										72
									
								
								service/calculate/mod.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										75
									
								
								service/calculate/persist.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								service/calculate/persist.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										55
									
								
								service/calculate/summary.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								service/calculate/summary.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										221
									
								
								service/calculate/tenement.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								service/calculate/tenement.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
| @@ -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
									
								
							
							
						
						
									
										180
									
								
								service/invoice.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										779
									
								
								service/meter.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
| @@ -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(¤tActivatedCustomers). | ||||
| 		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) | ||||
| 	} | ||||
| 	// 首先删除所有预定义的部分,条件是指定报表ID,SourceID不为空。 | ||||
| 	_, 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
									
								
							
							
						
						
									
										51
									
								
								service/synchronize.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										241
									
								
								service/tenement.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										489
									
								
								service/user.go
									
									
									
									
									
								
							
							
						
						
									
										489
									
								
								service/user.go
									
									
									
									
									
								
							| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -19,4 +19,5 @@ Redis: | ||||
| Service: | ||||
|   MaxSessionLife: 2h | ||||
|   ItemsPageSize: 20 | ||||
|   CacheLifeTime: 5m | ||||
|   CacheLifeTime: 5m | ||||
|   HostSerial: 5 | ||||
|   | ||||
							
								
								
									
										84
									
								
								tools/serial/algorithm.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								tools/serial/algorithm.go
									
									
									
									
									
										Normal 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 "" | ||||
| } | ||||
| @@ -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
									
								
							
							
						
						
									
										175
									
								
								types/date.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										141
									
								
								types/daterange.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										189
									
								
								types/datetime.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										119
									
								
								types/datetimerange.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										119
									
								
								types/range.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										39
									
								
								vo/invoice.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										64
									
								
								vo/meter.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										96
									
								
								vo/park.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										80
									
								
								vo/reading.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										323
									
								
								vo/report.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										43
									
								
								vo/shares.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										29
									
								
								vo/synchronize.go
									
									
									
									
									
										Normal 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:指数退避,1:2倍线性间隔,2:3倍线性间隔,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:指数退避,1:2倍线性间隔,2:3倍线性间隔,3:固定间隔 | ||||
| 	RetryInterval  string  `json:"retryInterval"`  // 重试间隔,基础间隔时间,根据间隔算法不同会产生不同的间隔 | ||||
| } | ||||
							
								
								
									
										75
									
								
								vo/tenement.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								vo/tenement.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										25
									
								
								vo/top_up.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										141
									
								
								vo/user.go
									
									
									
									
									
										Normal 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, "") | ||||
| } | ||||
		Reference in New Issue
	
	Block a user