From 648fc0f370236aee50da98a127f1a0aca0ba38a6 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Tue, 18 Jul 2023 16:07:56 +0800 Subject: [PATCH 01/27] =?UTF-8?q?new=EF=BC=9A=E6=96=B0=E5=A2=9Ewithdraw?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=EF=BC=8C=E8=AF=A5=E6=9A=82=E6=97=A0=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/dataSources.xml | 11 ++++ .idea/electricity_bill_calc_service.iml | 8 +++ .idea/misc.xml | 6 ++ .idea/modules.xml | 8 +++ .idea/vcs.xml | 6 ++ controller/user.go | 27 ++++----- controller/withdraw.go | 74 +++++++++++++++++++++++++ doc/routerSetting.md | 13 +++++ global/db.go | 3 + global/redis.go | 7 ++- response/user_response.go | 2 +- router/router.go | 25 +++++---- service/user.go | 2 +- settings.yaml | 4 +- 14 files changed, 165 insertions(+), 31 deletions(-) create mode 100644 .idea/dataSources.xml create mode 100644 .idea/electricity_bill_calc_service.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 controller/withdraw.go create mode 100644 doc/routerSetting.md diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..0973c28 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,11 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://39.105.39.8:9432/postgres + + + \ No newline at end of file diff --git a/.idea/electricity_bill_calc_service.iml b/.idea/electricity_bill_calc_service.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/electricity_bill_calc_service.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..28a804d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..95c0f54 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/controller/user.go b/controller/user.go index c79385e..7240728 100644 --- a/controller/user.go +++ b/controller/user.go @@ -42,34 +42,35 @@ type _LoginForm struct { } func doLogin(c *fiber.Ctx) error { - result := response.NewResult(c) - loginData := new(_LoginForm) - if err := c.BodyParser(loginData); err != nil { + result := response.NewResult(c) //创建一个相应结果对象 + loginData := new(_LoginForm) //创建一个解析登录表单数据的实体 + if err := c.BodyParser(loginData); err != nil { //解析请求体中的Json数据到loginData里,如果解析出错就返回错误 userLog.Error("表单解析失败!", zap.Error(err)) - return result.Error(http.StatusInternalServerError, "表单解析失败。") + 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) + 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 { - session, err = service.UserService.ProcessManagementUserLogin(loginData.Username, loginData.Password) + userLog.Info("该用户是管理用户") + session, err = service.UserService.ProcessManagementUserLogin(loginData.Username, loginData.Password) //管理用户 } if err != nil { - if authError, ok := err.(*exceptions.AuthenticationError); ok { - if authError.NeedReset { + if authError, ok := err.(*exceptions.AuthenticationError); ok { //检查错误是否为身份验证错误 + if authError.NeedReset { //如果需要重置密码则返回对应结果 return result.LoginNeedReset() } - return result.Error(int(authError.Code), authError.Message) + return result.Error(int(authError.Code), authError.Message) //返回身份验证错误相应 } else { userLog.Error("用户登录请求处理失败!", zap.Error(err)) - return result.Error(http.StatusInternalServerError, err.Error()) + return result.Error(http.StatusInternalServerError, err.Error()) //返回内部服务器错误 } } - return result.LoginSuccess(session) + return result.LoginSuccess(session) //返回登录成功相应结果,包含会话信息 } func doLogout(c *fiber.Ctx) error { diff --git a/controller/withdraw.go b/controller/withdraw.go new file mode 100644 index 0000000..cc490ac --- /dev/null +++ b/controller/withdraw.go @@ -0,0 +1,74 @@ +package controller + +import ( + "electricity_bill_calc/logger" + "electricity_bill_calc/response" + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +var withdrawLog = logger.Named("Handler", "Withdraw") + +func InitializeWithdrawHandlers(router *fiber.App) { + router.Get("/withdraw", withdraw) +} + +//用于检索用户的核算报表 +func withdraw(c *fiber.Ctx) error { + //记录日志 + withdrawLog.Info("带分页的待审核的核算撤回申请列表") + //获取请求参数 + result := response.NewResult(c) + keyword := c.Query("keyword", "") + page := c.QueryInt("page", 1) + withdrawLog.Info("参数为: ", zap.String("keyword", keyword), zap.Int("page", page)) + //中间数据库操作暂且省略。。。。 + //首先进行核算报表的分页查询 + + //TODO: 2023-07-18 此处的data需要经过上面数据库查询后进行数据返回,此处只是作于演示 + data := fiber.Map{ + "report": fiber.Map{ + "id": "string", + "parkId": "string", + "periodBegin": "string", + "periodEnd": "string", + "published": true, + "publishedAt": "string", + "withdraw": 0, + "lastWithdrawAppliedAt": "string", + "lastWithdrawAuditAt": "string", + "status": 0, + "message": "string", + }, + "park": fiber.Map{ + "id": "string", + "userId": "string", + "name": "string", + "tenement": "string", + "area": "string", + "capacity": "string", + "category": 0, + "meter04kvType": 0, + "region": "string", + "address": "string", + "contact": "string", + "phone": "string", + }, + "user": fiber.Map{ + "id": "string", + "name": "string", + "contact": "string", + "phone": "string", + "region": "string", + "address": "string", + }, + } + datas := make([]interface{}, 0) + datas = append(datas, data) + //TODO: 2023-07-18 此处返回值是个示例,具体返回值需要查询数据库 + return result.Success( + "withdraw请求成功", + response.NewPagedResponse(page, 20).ToMap(), + fiber.Map{"records": datas}, + ) +} diff --git a/doc/routerSetting.md b/doc/routerSetting.md new file mode 100644 index 0000000..6e9c627 --- /dev/null +++ b/doc/routerSetting.md @@ -0,0 +1,13 @@ +## fiber +#### fiber实例 +- app(是fiber创建的实例通常用app表示,其中有可选配置选项) + - BodyLimit 设置请求正文允许的最大大小(默认为4 * 1024 * 1024) + - EnablePrintRoutes 不打印框架自带日志(默认false) + - EnableTrustedProxyCheck 禁用受信代理(默认false) + - Prefork 预处理配置(默认false) + - ErrorHandler 全局错误处理 (默认false) + - JSONEncoder json编码 (默认json.Marshal) + - JSONDecoder json解码 (默认json.Unmarshal) + - 。。。。。。。。(还有很多配置) +- Use(中间件设置,一个或者多个) +- Group(类似于gin框架中的路由分组) \ No newline at end of file diff --git a/global/db.go b/global/db.go index 1acdc74..a19c5f4 100644 --- a/global/db.go +++ b/global/db.go @@ -53,6 +53,9 @@ func (ql QueryLogger) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data ql.logger.Info("查询参数", lo.Map(data.Args, func(elem any, index int) zap.Field { return zap.Any(fmt.Sprintf("[Arg %d]: ", index), elem) })...) + // for index, arg := range data.Args { + // ql.logger.Info(fmt.Sprintf("[Arg %d]: %v", index, arg)) + // } return ctx } diff --git a/global/redis.go b/global/redis.go index e99c19f..8afb039 100644 --- a/global/redis.go +++ b/global/redis.go @@ -15,10 +15,13 @@ var ( func SetupRedisConnection() error { var err error + a := fmt.Sprintf("%s:%d", config.RedisSettings.Host, config.RedisSettings.Port) + fmt.Println(a) Rd, err = rueidis.NewClient(rueidis.ClientOption{ - InitAddress: []string{fmt.Sprintf("%s:%d", config.RedisSettings.Host, config.RedisSettings.Port)}, - Password: config.RedisSettings.Password, + InitAddress: []string{"127.0.0.1:6379"}, + Password: "", SelectDB: config.RedisSettings.DB, + DisableCache:true, }) if err != nil { return err diff --git a/response/user_response.go b/response/user_response.go index 9eaf458..a60086e 100644 --- a/response/user_response.go +++ b/response/user_response.go @@ -16,7 +16,7 @@ type LoginResponse struct { func (r Result) LoginSuccess(session *model.Session) error { res := &LoginResponse{} res.Code = http.StatusOK - res.Message = "用户已成功登录。" + res.Message = "用户已成功登录。"+ "👋!" res.NeedReset = false res.Session = session return r.Ctx.Status(fiber.StatusOK).JSON(res) diff --git a/router/router.go b/router/router.go index fe8c98f..908558d 100644 --- a/router/router.go +++ b/router/router.go @@ -25,24 +25,24 @@ func init() { } func App() *fiber.App { - app := fiber.New(fiber.Config{ - BodyLimit: 30 * 1024 * 1024, - EnablePrintRoutes: true, - EnableTrustedProxyCheck: false, - Prefork: false, - ErrorHandler: errorHandler, - JSONEncoder: json.Marshal, - JSONDecoder: json.Unmarshal, + app := fiber.New(fiber.Config{ //创建fiber实例的时候选择配置选项 + BodyLimit: 30 * 1024 * 1024, //设置请求正文允许的最大大小。 + EnablePrintRoutes: true, //自定义方案,用于启动消息 + EnableTrustedProxyCheck: false, //禁用受信代理 + Prefork: false, //禁止预处理(如果要启用预处理则需要通过shell脚本运行) + ErrorHandler: errorHandler, //相应全局处理错误 + JSONEncoder: json.Marshal, //json编码 + JSONDecoder: json.Unmarshal, //json解码 }) - app.Use(compress.New()) + app.Use(compress.New()) //压缩中间件 app.Use(recover.New(recover.Config{ EnableStackTrace: true, StackTraceHandler: stackTraceHandler, - })) + })) //恢复中间件 app.Use(logger.NewLogMiddleware(logger.LogMiddlewareConfig{ Logger: logger.Named("App"), - })) - app.Use(security.SessionRecovery) + })) //日志中间件 + app.Use(security.SessionRecovery) //会话恢复中间件 controller.InitializeUserHandlers(app) controller.InitializeRegionHandlers(app) @@ -53,6 +53,7 @@ func App() *fiber.App { controller.InitializeInvoiceHandler(app) controller.InitializeTopUpHandlers(app) controller.InitializeReportHandlers(app) + controller.InitializeWithdrawHandlers(app) return app } diff --git a/service/user.go b/service/user.go index a81dc3f..2474921 100644 --- a/service/user.go +++ b/service/user.go @@ -80,7 +80,7 @@ func (us _UserService) ProcessEnterpriseUserLogin(username, password string) (*m us.log.Error("处理企业用户登录失败。", zap.String("username", username), zap.Error(err)) return nil, err } - token, _ := uuid.NewRandom() + token, _ := uuid.NewRandom() //生成uuid作为会话的token使用 userSession := &model.Session{ Uid: user.Id, Name: user.Username, diff --git a/settings.yaml b/settings.yaml index 4a0e548..0c4d01c 100644 --- a/settings.yaml +++ b/settings.yaml @@ -1,8 +1,8 @@ Database: User: electricity Pass: nLgxPO5s8gK2tR0OL0Q - Host: postgres - Port: 5432 + Host: 39.105.39.8 + Port: 9432 DB: electricity MaxIdleConns: 0 MaxOpenConns: 20 From 61edef5c92b5bbb8c433f923c5be6547d417d796 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Thu, 20 Jul 2023 16:13:19 +0800 Subject: [PATCH 02/27] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E5=AE=8C?= =?UTF-8?q?=E5=96=84withdraw=E7=9A=84=E8=BF=94=E5=9B=9E=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/report.go | 3 +- controller/tenement.go | 2 + controller/withdraw.go | 50 ++--------- model/park.go | 5 ++ model/withdraw.go | 77 +++++++++++++++++ repository/withdraw.go | 184 +++++++++++++++++++++++++++++++++++++++++ router/router.go | 2 + tools/utils.go | 14 ++++ 8 files changed, 294 insertions(+), 43 deletions(-) create mode 100644 model/withdraw.go create mode 100644 repository/withdraw.go diff --git a/controller/report.go b/controller/report.go index a99cabe..b79efe5 100644 --- a/controller/report.go +++ b/controller/report.go @@ -23,7 +23,8 @@ 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) + //TODO: 2023-07-20将calcualte错误请求改为正确的calculate请求 + router.Post("/report/calculate", 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) diff --git a/controller/tenement.go b/controller/tenement.go index 04dab4c..0e1c98b 100644 --- a/controller/tenement.go +++ b/controller/tenement.go @@ -25,9 +25,11 @@ func InitializeTenementHandler(router *fiber.App) { 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) + //TODO: 2023-07-19再apiFox上该请求是个PUT请求,后端接收是个POST请求,不知道是否有误或是缺少对应请求(apiFox测试请求返回值为405) 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) + //TODO: 2023-07-19再apiFox上该请求是个PUT请求,后端接收是个POST请求,不知道是否有误或是缺少对应请求(apiFox测试请求返回值为405) router.Post("/tenement/:pid/:tid/binding/:code/unbind", security.EnterpriseAuthorize, unbindMeterFromTenement) } diff --git a/controller/withdraw.go b/controller/withdraw.go index cc490ac..e19d613 100644 --- a/controller/withdraw.go +++ b/controller/withdraw.go @@ -2,9 +2,11 @@ package controller import ( "electricity_bill_calc/logger" + "electricity_bill_calc/repository" "electricity_bill_calc/response" "github.com/gofiber/fiber/v2" "go.uber.org/zap" + "net/http" ) var withdrawLog = logger.Named("Handler", "Withdraw") @@ -24,51 +26,15 @@ func withdraw(c *fiber.Ctx) error { withdrawLog.Info("参数为: ", zap.String("keyword", keyword), zap.Int("page", page)) //中间数据库操作暂且省略。。。。 //首先进行核算报表的分页查询 - - //TODO: 2023-07-18 此处的data需要经过上面数据库查询后进行数据返回,此处只是作于演示 - data := fiber.Map{ - "report": fiber.Map{ - "id": "string", - "parkId": "string", - "periodBegin": "string", - "periodEnd": "string", - "published": true, - "publishedAt": "string", - "withdraw": 0, - "lastWithdrawAppliedAt": "string", - "lastWithdrawAuditAt": "string", - "status": 0, - "message": "string", - }, - "park": fiber.Map{ - "id": "string", - "userId": "string", - "name": "string", - "tenement": "string", - "area": "string", - "capacity": "string", - "category": 0, - "meter04kvType": 0, - "region": "string", - "address": "string", - "contact": "string", - "phone": "string", - }, - "user": fiber.Map{ - "id": "string", - "name": "string", - "contact": "string", - "phone": "string", - "region": "string", - "address": "string", - }, + withdraws, total, err := repository.WithdrawRepository.FindWithdraw(page, &keyword) + if err != nil { + withdrawLog.Error("检索用户核算报表失败。", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) } - datas := make([]interface{}, 0) - datas = append(datas, data) //TODO: 2023-07-18 此处返回值是个示例,具体返回值需要查询数据库 return result.Success( "withdraw请求成功", - response.NewPagedResponse(page, 20).ToMap(), - fiber.Map{"records": datas}, + response.NewPagedResponse(page, total).ToMap(), + fiber.Map{"records": withdraws}, ) } diff --git a/model/park.go b/model/park.go index 9dfd190..c00df57 100644 --- a/model/park.go +++ b/model/park.go @@ -31,3 +31,8 @@ type Park struct { LastModifiedAt time.Time `json:"lastModifiedAt"` DeletedAt *time.Time `json:"deletedAt"` } + +type Parks struct { + Park + NormAuthorizedLossRate float64 `json:"norm_authorized_loss_rate"` +} diff --git a/model/withdraw.go b/model/withdraw.go new file mode 100644 index 0000000..6b5b032 --- /dev/null +++ b/model/withdraw.go @@ -0,0 +1,77 @@ +package model + +import ( + "database/sql" + "electricity_bill_calc/types" + "time" +) + +type Withdraw struct { + Park SimplifiedPark `json:"park"` + Report SimplifiedReport `json:"report"` + User UserInfos `json:"user"` // 简易用户详细信息 +} + +// 简易园区信息 +type SimplifiedPark struct { + Address *string `json:"address"` // 园区地址 + Area *string `json:"area"` // 园区面积 + Capacity *string `json:"capacity"` // 供电容量 + Category int16 `json:"category"` // 用电分类,0:两部制,1:单一峰谷,2:单一单一 + Contact *string `json:"contact"` // 园区联系人 + ID string `json:"id"` // 园区ID + Meter04KvType int16 `json:"meter04kvType"` // 户表计量类型,0:非峰谷,1:峰谷 + Name string `json:"name"` // 园区名称 + Phone *string `json:"phone"` // 园区联系人电话 + Region *string `json:"region"` // 园区所在行政区划 + Tenement *string `json:"tenement"` // 园区住户数量 + UserID string `json:"userId"` // 园区所属用户ID +} + +// 简易核算报表信息 +type SimplifiedReport struct { + ID string `json:"id"` // 报表ID + LastWithdrawAppliedAt *string `json:"lastWithdrawAppliedAt"` // 最后一次申请撤回的时间,格式为 yyyy-MM-dd HH:mm:ss + LastWithdrawAuditAt *string `json:"lastWithdrawAuditAt"` // 最后一次申请审核的时间,格式为 yyyy-MM-dd HH:mm:ss + Message *string `json:"message"` // 当前状态的错误提示 + ParkID string `json:"parkId"` // 所属园区ID + PeriodBegin string `json:"periodBegin"` // 核算起始日期,格式为 yyyy-MM-dd + PeriodEnd string `json:"periodEnd"` // 核算结束日期,格式为 yyyy-MM-dd + Published bool `json:"published"` // 是否已发布 + PublishedAt *string `json:"publishedAt"` // 发布时间 + Status float64 `json:"status,omitempty"` // 当前状态,0:计算任务已队列,1:计算任务已完成,2:计算数据不足 + Withdraw int16 `json:"withdraw"` // 报表撤回状态,0:未撤回,1:申请撤回中,2:申请拒绝,3:申请批准 +} + +// 简易用户信息 +type UserInfos struct { + Address *string `json:"address"` // 用户地址 + Contact *string `json:"contact"` // 用户联系人 + ID string `json:"id"` // 用户ID + Name *string `json:"name"` // 用户名称 + Phone *string `json:"phone"` // 用户联系人电话 + Region *string `json:"region"` // 用户所在行政区划 +} + +//用于映射数据库的报表结构体 +type Report struct { + CreatedAt time.Time `db:"created_at"` + LastModifiedAt sql.NullTime `db:"last_modified_at"` + ID string `db:"id"` + ParkID string `db:"park_id"` + Period types.DateRange `db:"period"` + Published bool `db:"published"` + PublishedAt sql.NullTime `db:"published_at"` + Withdraw int16 `db:"withdraw"` + LastWithdrawAppliedAt sql.NullTime `db:"last_withdraw_applied_at"` + LastWithdrawAuditAt sql.NullTime `db:"last_withdraw_audit_at"` + Category int16 `db:"category"` + Meter04KVType int16 `db:"meter_04kv_type"` + PricePolicy int16 `db:"price_policy"` + BasisPooled int16 `db:"basis_pooled"` + AdjustPooled int16 `db:"adjust_pooled"` + LossPooled int16 `db:"loss_pooled"` + PublicPooled int16 `db:"public_pooled"` + AuthorizedLossRate float64 `db:"authorized_loss_rate"` + AuthorizedLossRateIncr float64 `db:"authorized_loss_rate_increment"` +} diff --git a/repository/withdraw.go b/repository/withdraw.go new file mode 100644 index 0000000..f316d7f --- /dev/null +++ b/repository/withdraw.go @@ -0,0 +1,184 @@ +package repository + +import ( + "electricity_bill_calc/global" + "electricity_bill_calc/logger" + "electricity_bill_calc/model" + "electricity_bill_calc/tools" + "fmt" + "github.com/doug-martin/goqu/v9" + "github.com/georgysavva/scany/v2/pgxscan" + "go.uber.org/zap" +) + +type _WithdrawRepository struct { + log *zap.Logger + ds goqu.DialectWrapper +} + +var WithdrawRepository = &_WithdrawRepository{ + log: logger.Named("Repository", "Withdraw"), + ds: goqu.Dialect("postgres"), +} + +/** + * @author: ZiHangQin + * 该方法用于分页查询核算报表 + * @param:page + * @param: keyword + * @return:[]object + * @return:total + * @return: error + */ +func (wd _WithdrawRepository) FindWithdraw(page int, keyword *string) ([]model.Withdraw, int64, error) { + wd.log.Info("查询用户的充值记录。", zap.Stringp("keyword", keyword), zap.Int("page", page)) + ctx, cancel := global.TimeoutContext() + defer cancel() + fmt.Println(ctx) + //TODO: 2023-07-18此处进行用户的核算报表分页查询的sql语句拼接逻辑。 + //1、SELECT * FROM report WHERE `withdraw` = 1(获取到所有的状态为申请撤回中的报表数据) + // + //2、循环遍历1中获取的数据{ + //查询需要的字段"id": "string", + // "parkId": "string", + // "periodBegin": "string", + // "periodEnd": "string", + // "published": true, + // "publishedAt": "string", + // "withdraw": 0, + // "lastWithdrawAppliedAt": "string", + // "lastWithdrawAuditAt": "string", + // "status": 0, + // "message": "string" + //----report简易核算报表信息获取完成 + // + //3、SELECT * FROM park WHERE `id` = report.park_id(获取园区信息) + //查询结果需要的字段 "id": "string", + // "userId": "string", + // "name": "string", + // "tenement": "string", + // "area": "string", + // "capacity": "string", + // "category": 0, + // "meter04kvType": 0, + // "region": "string", + // "address": "string", + // "contact": "string", + // "phone": "string" + //----park简易园区信息货物完成 + // + //4、SELECT * FROM user_detail WHERE `id` = park.user_id(获取用户信息) + //查询结果需要的字段 "id": "string", + // "name": "string", + // "contact": "string", + // "phone": "string", + // "region": "string", + // "address": "string" + //----user简易用户信息获取完成 + //} + + reportQuery, reportQueryArgs, _ := wd.ds. + From(goqu.T("report")). + Where(goqu.I("withdraw").Eq(1)). + Select("*").ToSQL() + + reports := make([]model.Report, 0) + + err := pgxscan.Select(ctx, global.DB, &reports, reportQuery, reportQueryArgs...) + if err != nil { + fmt.Println(err) + return []model.Withdraw{}, 0, err + } + fmt.Println("数据库中读取的指定数据:", reports) + + var withdrawReses []model.Withdraw + + for _, v := range reports { + lastWithdrawAppliedAtStr := tools.NullTime2PointerString(v.LastWithdrawAppliedAt) + lastWithdrawAuditAtStr := tools.NullTime2PointerString(v.LastWithdrawAuditAt) + publishAtStr := tools.NullTime2PointerString(v.PublishedAt) + + Begin := v.Period.SafeLower().Format("2006-01-02") + End := v.Period.SafeUpper().Format("2006-01-02") + var withdrawRes model.Withdraw + //构建简易报表信息 + simplifiedReport := model.SimplifiedReport{ + ID: v.ID, + LastWithdrawAppliedAt: lastWithdrawAppliedAtStr, + LastWithdrawAuditAt: lastWithdrawAuditAtStr, + Message: nil, + ParkID: v.ParkID, + PeriodBegin: Begin, + PeriodEnd: End, + Published: v.Published, + PublishedAt: publishAtStr, + Status: 0.00, + Withdraw: v.Withdraw, + } + + parkQuery, parkQueryArgs, _ := wd.ds. + From(goqu.T("park")). + Where(goqu.I("id").Eq(v.ParkID)). + Select("*").ToSQL() + + park := make([]model.Parks, 0) + err := pgxscan.Select(ctx, global.DB, &park, parkQuery, parkQueryArgs...) + fmt.Println("读到的园区数据:", park) + if err != nil { + fmt.Println(err) + return []model.Withdraw{}, 0, err + } + + areaStr := tools.NullDecimalToString(park[0].Area) + capacityStr := tools.NullDecimalToString(park[0].Capacity) + TenementQuantityStr := tools.NullDecimalToString(park[0].TenementQuantity) + //构建简易园区数据 + simplifiedPark := model.SimplifiedPark{ + Address: park[0].Address, + Area: areaStr, + Capacity: capacityStr, + Category: park[0].Category, + Contact: park[0].Contact, + ID: park[0].Id, + Meter04KvType: park[0].MeterType, + Name: park[0].Name, + Phone: park[0].Phone, + Region: park[0].Region, + Tenement: TenementQuantityStr, + UserID: park[0].UserId, + } + + userQuery, userQueryArgs, _ := wd.ds. + From(goqu.T("user_detail")). + Where(goqu.I("id").Eq(park[0].UserId)). + Select("*").ToSQL() + + userInfo := make([]model.UserDetail, 0) + + err = pgxscan.Select(ctx, global.DB, &userInfo, userQuery, userQueryArgs...) + fmt.Println("读到的用户数据:", userInfo) + if err != nil { + fmt.Println(err) + return []model.Withdraw{}, 0, err + } + + simplifiedUser := model.UserInfos{ + Address: userInfo[0].Address, + Contact: userInfo[0].Contact, + ID: userInfo[0].Id, + Name: userInfo[0].Name, + Phone: userInfo[0].Phone, + Region: userInfo[0].Region, + } + + withdrawRes.Report = simplifiedReport + withdrawRes.Park = simplifiedPark + withdrawRes.User = simplifiedUser + + withdrawReses = append(withdrawReses, withdrawRes) + } + + total := len(reports) + + return withdrawReses, int64(total), nil +} diff --git a/router/router.go b/router/router.go index 908558d..f1db451 100644 --- a/router/router.go +++ b/router/router.go @@ -53,6 +53,8 @@ func App() *fiber.App { controller.InitializeInvoiceHandler(app) controller.InitializeTopUpHandlers(app) controller.InitializeReportHandlers(app) + + controller.InitializeWithdrawHandlers(app) return app diff --git a/tools/utils.go b/tools/utils.go index 6312dd5..fceb81b 100644 --- a/tools/utils.go +++ b/tools/utils.go @@ -1,6 +1,7 @@ package tools import ( + "database/sql" "encoding/json" "fmt" "strings" @@ -144,3 +145,16 @@ func NullDecimalToString(d decimal.NullDecimal, precision ...int32) *string { } return lo.ToPtr(d.Decimal.StringFixedBank(precision[0])) } + +//将sql.NullTime转换为*string +func NullTime2PointerString(nullTime sql.NullTime) *string { + var strPtr *string + if nullTime.Valid { + str := nullTime.Time.String() + strPtr = &str + return strPtr + } else { + strPtr = nil + return strPtr + } +} From ab44ff5cc4a23bd54f99e579989b3f29f7d1fe09 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Tue, 25 Jul 2023 10:09:53 +0800 Subject: [PATCH 03/27] a --- .idea/codeStyles/Project.xml | 28 ++++++++++++++++++++++++++++ repository/withdraw.go | 1 - 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 .idea/codeStyles/Project.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..3cdc6ae --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/repository/withdraw.go b/repository/withdraw.go index f316d7f..5529731 100644 --- a/repository/withdraw.go +++ b/repository/withdraw.go @@ -89,7 +89,6 @@ func (wd _WithdrawRepository) FindWithdraw(page int, keyword *string) ([]model.W fmt.Println(err) return []model.Withdraw{}, 0, err } - fmt.Println("数据库中读取的指定数据:", reports) var withdrawReses []model.Withdraw From 6fece99e002268c8912a0bc59b1ba26849d2835a Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Tue, 25 Jul 2023 10:45:43 +0800 Subject: [PATCH 04/27] =?UTF-8?q?=E5=B8=A6=E5=88=86=E9=A1=B5=E7=9A=84?= =?UTF-8?q?=E5=BE=85=E5=AE=A1=E6=A0=B8=E7=9A=84=E6=A0=B8=E7=AE=97=E6=92=A4?= =?UTF-8?q?=E5=9B=9E=E7=94=B3=E8=AF=B7=E5=88=97=E8=A1=A8=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/withdraw.go | 2 +- model/withdraw.go | 75 +++++++------ repository/withdraw.go | 243 ++++++++++++++++++++--------------------- tools/utils.go | 13 +++ 4 files changed, 173 insertions(+), 160 deletions(-) diff --git a/controller/withdraw.go b/controller/withdraw.go index e19d613..d6c7510 100644 --- a/controller/withdraw.go +++ b/controller/withdraw.go @@ -26,7 +26,7 @@ func withdraw(c *fiber.Ctx) error { withdrawLog.Info("参数为: ", zap.String("keyword", keyword), zap.Int("page", page)) //中间数据库操作暂且省略。。。。 //首先进行核算报表的分页查询 - withdraws, total, err := repository.WithdrawRepository.FindWithdraw(page, &keyword) + withdraws, total, err := repository.WithdrawRepository.FindWithdraw(uint(page), &keyword) if err != nil { withdrawLog.Error("检索用户核算报表失败。", zap.Error(err)) return result.Error(http.StatusInternalServerError, err.Error()) diff --git a/model/withdraw.go b/model/withdraw.go index 6b5b032..b1da5f1 100644 --- a/model/withdraw.go +++ b/model/withdraw.go @@ -1,8 +1,8 @@ package model import ( - "database/sql" "electricity_bill_calc/types" + "github.com/shopspring/decimal" "time" ) @@ -17,10 +17,10 @@ type SimplifiedPark struct { Address *string `json:"address"` // 园区地址 Area *string `json:"area"` // 园区面积 Capacity *string `json:"capacity"` // 供电容量 - Category int16 `json:"category"` // 用电分类,0:两部制,1:单一峰谷,2:单一单一 + Category int16 `json:"category"` // 用电分类,0:两部制,1:单一峰谷,2:单一单一 Contact *string `json:"contact"` // 园区联系人 ID string `json:"id"` // 园区ID - Meter04KvType int16 `json:"meter04kvType"` // 户表计量类型,0:非峰谷,1:峰谷 + Meter04KvType int16 `json:"meter04kvType"` // 户表计量类型,0:非峰谷,1:峰谷 Name string `json:"name"` // 园区名称 Phone *string `json:"phone"` // 园区联系人电话 Region *string `json:"region"` // 园区所在行政区划 @@ -30,17 +30,17 @@ type SimplifiedPark struct { // 简易核算报表信息 type SimplifiedReport struct { - ID string `json:"id"` // 报表ID - LastWithdrawAppliedAt *string `json:"lastWithdrawAppliedAt"` // 最后一次申请撤回的时间,格式为 yyyy-MM-dd HH:mm:ss - LastWithdrawAuditAt *string `json:"lastWithdrawAuditAt"` // 最后一次申请审核的时间,格式为 yyyy-MM-dd HH:mm:ss - Message *string `json:"message"` // 当前状态的错误提示 - ParkID string `json:"parkId"` // 所属园区ID - PeriodBegin string `json:"periodBegin"` // 核算起始日期,格式为 yyyy-MM-dd - PeriodEnd string `json:"periodEnd"` // 核算结束日期,格式为 yyyy-MM-dd - Published bool `json:"published"` // 是否已发布 - PublishedAt *string `json:"publishedAt"` // 发布时间 + ID string `json:"id"` // 报表ID + LastWithdrawAppliedAt *string `json:"lastWithdrawAppliedAt"` // 最后一次申请撤回的时间,格式为 yyyy-MM-dd HH:mm:ss + LastWithdrawAuditAt *string `json:"lastWithdrawAuditAt"` // 最后一次申请审核的时间,格式为 yyyy-MM-dd HH:mm:ss + Message *string `json:"message"` // 当前状态的错误提示 + ParkID string `json:"parkId"` // 所属园区ID + PeriodBegin string `json:"periodBegin"` // 核算起始日期,格式为 yyyy-MM-dd + PeriodEnd string `json:"periodEnd"` // 核算结束日期,格式为 yyyy-MM-dd + Published bool `json:"published"` // 是否已发布 + PublishedAt *string `json:"publishedAt"` // 发布时间 Status float64 `json:"status,omitempty"` // 当前状态,0:计算任务已队列,1:计算任务已完成,2:计算数据不足 - Withdraw int16 `json:"withdraw"` // 报表撤回状态,0:未撤回,1:申请撤回中,2:申请拒绝,3:申请批准 + Withdraw int16 `json:"withdraw"` // 报表撤回状态,0:未撤回,1:申请撤回中,2:申请拒绝,3:申请批准 } // 简易用户信息 @@ -48,30 +48,37 @@ type UserInfos struct { Address *string `json:"address"` // 用户地址 Contact *string `json:"contact"` // 用户联系人 ID string `json:"id"` // 用户ID - Name *string `json:"name"` // 用户名称 + Name *string `json:"name"` // 用户名称 Phone *string `json:"phone"` // 用户联系人电话 Region *string `json:"region"` // 用户所在行政区划 } //用于映射数据库的报表结构体 -type Report struct { - CreatedAt time.Time `db:"created_at"` - LastModifiedAt sql.NullTime `db:"last_modified_at"` - ID string `db:"id"` - ParkID string `db:"park_id"` - Period types.DateRange `db:"period"` - Published bool `db:"published"` - PublishedAt sql.NullTime `db:"published_at"` - Withdraw int16 `db:"withdraw"` - LastWithdrawAppliedAt sql.NullTime `db:"last_withdraw_applied_at"` - LastWithdrawAuditAt sql.NullTime `db:"last_withdraw_audit_at"` - Category int16 `db:"category"` - Meter04KVType int16 `db:"meter_04kv_type"` - PricePolicy int16 `db:"price_policy"` - BasisPooled int16 `db:"basis_pooled"` - AdjustPooled int16 `db:"adjust_pooled"` - LossPooled int16 `db:"loss_pooled"` - PublicPooled int16 `db:"public_pooled"` - AuthorizedLossRate float64 `db:"authorized_loss_rate"` - AuthorizedLossRateIncr float64 `db:"authorized_loss_rate_increment"` +type ReportRes struct { + ReportId string `db:"report_id"` + LastWithdrawAppliedAt *time.Time `db:"last_withdraw_applied_at"` + LastWithdrawAuditAt *time.Time `db:"last_withdraw_audit_at"` + ParkID string `db:"report_park_id"` + Period types.DateRange `db:"period"` + Published bool `db:"published"` + PublishedAt *time.Time `db: "published_at"` + Withdraw int16 `db:"withdraw"` + ParkAddress *string `db:"park_address"` + Area decimal.NullDecimal `db:"area"` + Capacity decimal.NullDecimal `db:"capacity"` + Category int16 + ParkContact *string `db:"park_contact"` + ParkId string `db:"park_id"` + Meter04KVType int16 `db:"meter_04kv_type"` + ParkName string `db:"park_name"` + ParkPhone *string `db:"park_phone"` + ParkRegion string `db:"park_region"` + TenementQuantity decimal.NullDecimal `db:"tenement_quantity"` + UserID string `db:"user_id"` + Address *string + Contact string `db:"user_detail_contact"` + ID string `db:"ud_id"` + Name *string `db:"user_detail_name"` + Phone string `db:"user_detail_phone"` + Region *string `db:"user_detail_region"` } diff --git a/repository/withdraw.go b/repository/withdraw.go index 5529731..4fe000c 100644 --- a/repository/withdraw.go +++ b/repository/withdraw.go @@ -1,6 +1,7 @@ package repository import ( + "electricity_bill_calc/config" "electricity_bill_calc/global" "electricity_bill_calc/logger" "electricity_bill_calc/model" @@ -30,154 +31,146 @@ var WithdrawRepository = &_WithdrawRepository{ * @return:total * @return: error */ -func (wd _WithdrawRepository) FindWithdraw(page int, keyword *string) ([]model.Withdraw, int64, error) { - wd.log.Info("查询用户的充值记录。", zap.Stringp("keyword", keyword), zap.Int("page", page)) +func (wd _WithdrawRepository) FindWithdraw(page uint, keyword *string) ([]model.Withdraw, int64, error) { + wd.log.Info("查询用户的充值记录。", zap.Stringp("keyword", keyword), zap.Int("page", int(page))) ctx, cancel := global.TimeoutContext() defer cancel() - fmt.Println(ctx) - //TODO: 2023-07-18此处进行用户的核算报表分页查询的sql语句拼接逻辑。 - //1、SELECT * FROM report WHERE `withdraw` = 1(获取到所有的状态为申请撤回中的报表数据) - // - //2、循环遍历1中获取的数据{ - //查询需要的字段"id": "string", - // "parkId": "string", - // "periodBegin": "string", - // "periodEnd": "string", - // "published": true, - // "publishedAt": "string", - // "withdraw": 0, - // "lastWithdrawAppliedAt": "string", - // "lastWithdrawAuditAt": "string", - // "status": 0, - // "message": "string" - //----report简易核算报表信息获取完成 - // - //3、SELECT * FROM park WHERE `id` = report.park_id(获取园区信息) - //查询结果需要的字段 "id": "string", - // "userId": "string", - // "name": "string", - // "tenement": "string", - // "area": "string", - // "capacity": "string", - // "category": 0, - // "meter04kvType": 0, - // "region": "string", - // "address": "string", - // "contact": "string", - // "phone": "string" - //----park简易园区信息货物完成 - // - //4、SELECT * FROM user_detail WHERE `id` = park.user_id(获取用户信息) - //查询结果需要的字段 "id": "string", - // "name": "string", - // "contact": "string", - // "phone": "string", - // "region": "string", - // "address": "string" - //----user简易用户信息获取完成 - //} - reportQuery, reportQueryArgs, _ := wd.ds. - From(goqu.T("report")). + /** + 如果访问数据库次数过多出现时间过长的话可以用这个尝试优化,未测试的sql语句 + + wd.ds.From(goqu.T("report")). Where(goqu.I("withdraw").Eq(1)). - Select("*").ToSQL() + Select( + goqu.I("report.*"), + goqu.I("park.*"), + goqu.I("user_detail.*"), + ). + Join( + goqu.T("park"), goqu.On(goqu.I("report.park_id").Eq(goqu.I("park.id"))), + ). + Join( + goqu.T("user_detail"), goqu.On(goqu.I("park.user_id").Eq(goqu.I("user_detail.id"))), + ).ToSQL() - reports := make([]model.Report, 0) + SELECT report.*, park.*, user_detail.* + FROM report as r + JOIN park as p ON r.park_id = p.id + JOIN user_detail as ud ON p.user_id = ud.id + WHERE withdraw = 1 + AND p.name Like '%keyword%' + AND ud.name Like '%keyword%' + */ + reportQuery := wd.ds. + From(goqu.T("report").As("r")). + Where(goqu.I("withdraw").Eq(1)). + Join(goqu.T("park").As("p"), goqu.On(goqu.I("r.park_id").Eq(goqu.I("p.id")))). + Join(goqu.T("user_detail").As("ud"), goqu.On(goqu.I("p.user_id").Eq(goqu.I("ud.id")))). + Select( + goqu.I("r.id").As("report_id"), goqu.I("r.last_withdraw_applied_at"), goqu.I("r.last_withdraw_audit_at"), + goqu.I("r.park_id").As("report_park_id"), goqu.I("r.period"), goqu.I("r.published"), goqu.I("r.published_at"), goqu.I("r.withdraw"), + goqu.I("p.address").As("park_address"), goqu.I("p.area"), goqu.I("p.capacity"), goqu.I("p.category"), goqu.I("p.contact").As("park_contact"), + goqu.I("p.id").As("park_id"), goqu.I("p.meter_04kv_type"), goqu.I("p.name").As("park_name"), goqu.I("p.phone").As("park_phone"), goqu.I("p.region").As("park_region"), + goqu.I("p.tenement_quantity"), goqu.I("p.user_id"), goqu.I("ud.address"), goqu.I("ud.contact").As("user_detail_contact"), + goqu.I("ud.id").As("ud_id"), goqu.I("ud.name").As("user_detail_name"), goqu.I("ud.phone").As("user_detail_phone"), goqu.I("ud.region").As("user_detail_region"), + ) - err := pgxscan.Select(ctx, global.DB, &reports, reportQuery, reportQueryArgs...) - if err != nil { - fmt.Println(err) - return []model.Withdraw{}, 0, err + countReportQuery := wd.ds. + From(goqu.T("report").As("r")). + Where(goqu.I("withdraw").Eq(1)). + Join(goqu.T("park").As("p"), goqu.On(goqu.I("r.park_id").Eq(goqu.I("p.id")))). + Join(goqu.T("user_detail").As("ud"), goqu.On(goqu.I("p.user_id").Eq(goqu.I("ud.id")))). + Select(goqu.COUNT("*")) + + if keyword != nil && len(*keyword) > 0 { + pattern := fmt.Sprintf("%%%s%%", *keyword) + reportQuery = reportQuery.Where(goqu.Or( + goqu.I("p.name").ILike(pattern), + goqu.I("ud.name").ILike(pattern), + )) } + reportQuery = reportQuery.Order(goqu.I("r.created_at").Desc()) + + currentPostion := (page - 1) * config.ServiceSettings.ItemsPageSize + reportQuery = reportQuery.Offset(currentPostion).Limit(config.ServiceSettings.ItemsPageSize) + + reportSql, reportArgs, _ := reportQuery.Prepared(true).ToSQL() + + countReportQuerySql, countReportQueryArgs, _ := countReportQuery.Prepared(true).ToSQL() + + + var ( + reports []*model.ReportRes = make([]*model.ReportRes, 0) + total int64 + ) + var err error + + err = pgxscan.Select(ctx, global.DB, &reports, reportSql, reportArgs...) + if err != nil { + fmt.Println(err) + wd.log.Error("查询报表记录失败。", zap.Error(err)) + return make([]model.Withdraw, 0), 0, err + } + + if err = pgxscan.Get(ctx, global.DB, &total, countReportQuerySql, countReportQueryArgs...); err != nil { + wd.log.Error("查询报表记录总数失败。", zap.Error(err)) + return make([]model.Withdraw, 0), 0, err + } + + if len(reports) <= 0 { + return make([]model.Withdraw, 0), total, nil + } + + fmt.Println(&reports) var withdrawReses []model.Withdraw - + //TODO: 2023.07.24对查询到的数据进行拼接 for _, v := range reports { - lastWithdrawAppliedAtStr := tools.NullTime2PointerString(v.LastWithdrawAppliedAt) - lastWithdrawAuditAtStr := tools.NullTime2PointerString(v.LastWithdrawAuditAt) - publishAtStr := tools.NullTime2PointerString(v.PublishedAt) - Begin := v.Period.SafeLower().Format("2006-01-02") End := v.Period.SafeUpper().Format("2006-01-02") + var withdrawRes model.Withdraw - //构建简易报表信息 - simplifiedReport := model.SimplifiedReport{ - ID: v.ID, - LastWithdrawAppliedAt: lastWithdrawAppliedAtStr, - LastWithdrawAuditAt: lastWithdrawAuditAtStr, + report := model.SimplifiedReport{ + ID: v.ReportId, + LastWithdrawAppliedAt: tools.TimeToStringPtr(v.LastWithdrawAppliedAt), + LastWithdrawAuditAt: tools.TimeToStringPtr(v.LastWithdrawAuditAt), Message: nil, ParkID: v.ParkID, PeriodBegin: Begin, PeriodEnd: End, Published: v.Published, - PublishedAt: publishAtStr, - Status: 0.00, + PublishedAt: nil, + Status: 0., Withdraw: v.Withdraw, } - - parkQuery, parkQueryArgs, _ := wd.ds. - From(goqu.T("park")). - Where(goqu.I("id").Eq(v.ParkID)). - Select("*").ToSQL() - - park := make([]model.Parks, 0) - err := pgxscan.Select(ctx, global.DB, &park, parkQuery, parkQueryArgs...) - fmt.Println("读到的园区数据:", park) - if err != nil { - fmt.Println(err) - return []model.Withdraw{}, 0, err + park := model.SimplifiedPark{ + Address: v.ParkAddress, + Area: tools.NullDecimalToString(v.Area), + Capacity: tools.NullDecimalToString(v.Capacity), + Category: int16(v.Category), + Contact: v.ParkContact, + ID: v.ParkId, + Meter04KvType: v.Meter04KVType, + Name: v.ParkName, + Phone: v.ParkPhone, + Region: &v.ParkRegion, + Tenement: tools.NullDecimalToString(v.TenementQuantity), + UserID: v.UserID, + } + userInfo := model.UserInfos{ + Address: v.Address, + Contact: &v.Contact, + ID: v.ID, + Name: v.Name, + Phone: &v.Phone, + Region: v.Region, } - areaStr := tools.NullDecimalToString(park[0].Area) - capacityStr := tools.NullDecimalToString(park[0].Capacity) - TenementQuantityStr := tools.NullDecimalToString(park[0].TenementQuantity) - //构建简易园区数据 - simplifiedPark := model.SimplifiedPark{ - Address: park[0].Address, - Area: areaStr, - Capacity: capacityStr, - Category: park[0].Category, - Contact: park[0].Contact, - ID: park[0].Id, - Meter04KvType: park[0].MeterType, - Name: park[0].Name, - Phone: park[0].Phone, - Region: park[0].Region, - Tenement: TenementQuantityStr, - UserID: park[0].UserId, - } - - userQuery, userQueryArgs, _ := wd.ds. - From(goqu.T("user_detail")). - Where(goqu.I("id").Eq(park[0].UserId)). - Select("*").ToSQL() - - userInfo := make([]model.UserDetail, 0) - - err = pgxscan.Select(ctx, global.DB, &userInfo, userQuery, userQueryArgs...) - fmt.Println("读到的用户数据:", userInfo) - if err != nil { - fmt.Println(err) - return []model.Withdraw{}, 0, err - } - - simplifiedUser := model.UserInfos{ - Address: userInfo[0].Address, - Contact: userInfo[0].Contact, - ID: userInfo[0].Id, - Name: userInfo[0].Name, - Phone: userInfo[0].Phone, - Region: userInfo[0].Region, - } - - withdrawRes.Report = simplifiedReport - withdrawRes.Park = simplifiedPark - withdrawRes.User = simplifiedUser - + withdrawRes.Report = report + withdrawRes.Park = park + withdrawRes.User = userInfo withdrawReses = append(withdrawReses, withdrawRes) } - total := len(reports) - - return withdrawReses, int64(total), nil + return withdrawReses, total, nil } diff --git a/tools/utils.go b/tools/utils.go index fceb81b..af61a41 100644 --- a/tools/utils.go +++ b/tools/utils.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "strings" + "time" "github.com/mozillazg/go-pinyin" "github.com/samber/lo" @@ -158,3 +159,15 @@ func NullTime2PointerString(nullTime sql.NullTime) *string { return strPtr } } + + +//该方法用于将时间解析为字符串指针 +func TimeToStringPtr(t *time.Time) *string { + if t == nil { + return nil + } + + timeStr := t.Format("2006-01-02 15:04:05") + return &timeStr +} + From d8a29e7d17d3e54e7e2e7600318cab2449bab518 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Tue, 25 Jul 2023 15:31:35 +0800 Subject: [PATCH 05/27] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E6=A3=80=E7=B4=A2=E6=A0=B8=E7=AE=97=E6=8A=A5=E8=A1=A8=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E7=BB=86=E8=8A=82=EF=BC=8C=E5=AE=8C=E6=88=90=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E6=92=A4=E5=9B=9E=E6=8A=A5=E8=A1=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/withdraw.go | 56 +++++++++++++++++++++++--- repository/withdraw.go | 91 ++++++++++++++++++++++++++++++++++++------ vo/withdraw.go | 6 +++ 3 files changed, 134 insertions(+), 19 deletions(-) create mode 100644 vo/withdraw.go diff --git a/controller/withdraw.go b/controller/withdraw.go index d6c7510..ed14265 100644 --- a/controller/withdraw.go +++ b/controller/withdraw.go @@ -4,6 +4,8 @@ import ( "electricity_bill_calc/logger" "electricity_bill_calc/repository" "electricity_bill_calc/response" + "electricity_bill_calc/security" + "electricity_bill_calc/vo" "github.com/gofiber/fiber/v2" "go.uber.org/zap" "net/http" @@ -12,10 +14,11 @@ import ( var withdrawLog = logger.Named("Handler", "Withdraw") func InitializeWithdrawHandlers(router *fiber.App) { - router.Get("/withdraw", withdraw) + router.Get("/withdraw", security.OPSAuthorize, withdraw) + router.Put("/withdraw/:rid", ReviewRequestWithdraw) } -//用于检索用户的核算报表 +//用于分页检索用户的核算报表 func withdraw(c *fiber.Ctx) error { //记录日志 withdrawLog.Info("带分页的待审核的核算撤回申请列表") @@ -27,14 +30,55 @@ func withdraw(c *fiber.Ctx) error { //中间数据库操作暂且省略。。。。 //首先进行核算报表的分页查询 withdraws, total, err := repository.WithdrawRepository.FindWithdraw(uint(page), &keyword) - if err != nil { - withdrawLog.Error("检索用户核算报表失败。", zap.Error(err)) - return result.Error(http.StatusInternalServerError, err.Error()) + if err != nil { + withdrawLog.Error("检索用户核算报表失败。", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) } - //TODO: 2023-07-18 此处返回值是个示例,具体返回值需要查询数据库 + //TODO: 2023-07-18 此处返回值是个示例,具体返回值需要查询数据库(完成) return result.Success( "withdraw请求成功", response.NewPagedResponse(page, total).ToMap(), fiber.Map{"records": withdraws}, ) } + +//用于审核撤回报表 +func ReviewRequestWithdraw(c *fiber.Ctx) error { + Rid := c.Params("rid", "") + Data := new(vo.ReviewWithdraw) + result := response.NewResult(c) + + if err := c.BodyParser(&Data); err != nil { + withdrawLog.Error("无法解析审核指定报表的请求数据", zap.Error(err)) + return result.BadRequest("无法解析审核指定报表的请求数据。") + } + + if Data.Audit == true { //审核通过 + ok, err := repository.WithdrawRepository.ReviewTrueReportWithdraw(Rid) + if err != nil { + withdrawLog.Error("审核同意撤回报表失败") + return result.Error(http.StatusInternalServerError, err.Error()) + } + + if !ok { + withdrawLog.Error("审核同意撤回报表失败") + return result.NotAccept("审核同意撤回报表失败") + } else { + return result.Success("审核同意撤回报表成功!") + } + } else { //审核不通过 + ok, err := repository.WithdrawRepository.ReviewFalseReportWithdraw(Rid) + if err != nil { + withdrawLog.Error("审核拒绝撤回报表失败") + return result.Error(http.StatusInternalServerError, err.Error()) + } + + if !ok { + withdrawLog.Error("审核拒绝撤回报表失败") + return result.NotAccept("审核拒绝撤回报表失败") + } else { + return result.Success("审核拒绝撤回报表成功!") + } + } + +} diff --git a/repository/withdraw.go b/repository/withdraw.go index 4fe000c..540c5a3 100644 --- a/repository/withdraw.go +++ b/repository/withdraw.go @@ -6,6 +6,7 @@ import ( "electricity_bill_calc/logger" "electricity_bill_calc/model" "electricity_bill_calc/tools" + "electricity_bill_calc/types" "fmt" "github.com/doug-martin/goqu/v9" "github.com/georgysavva/scany/v2/pgxscan" @@ -22,17 +23,9 @@ var WithdrawRepository = &_WithdrawRepository{ ds: goqu.Dialect("postgres"), } -/** - * @author: ZiHangQin - * 该方法用于分页查询核算报表 - * @param:page - * @param: keyword - * @return:[]object - * @return:total - * @return: error - */ +//该方法用于分页查询核算报表 func (wd _WithdrawRepository) FindWithdraw(page uint, keyword *string) ([]model.Withdraw, int64, error) { - wd.log.Info("查询用户的充值记录。", zap.Stringp("keyword", keyword), zap.Int("page", int(page))) + wd.log.Info("查询核算报表", zap.Stringp("keyword", keyword), zap.Int("page", int(page))) ctx, cancel := global.TimeoutContext() defer cancel() @@ -66,6 +59,8 @@ func (wd _WithdrawRepository) FindWithdraw(page uint, keyword *string) ([]model. Where(goqu.I("withdraw").Eq(1)). Join(goqu.T("park").As("p"), goqu.On(goqu.I("r.park_id").Eq(goqu.I("p.id")))). Join(goqu.T("user_detail").As("ud"), goqu.On(goqu.I("p.user_id").Eq(goqu.I("ud.id")))). + Where(goqu.I("p.deleted_at").IsNull()). + Where(goqu.I("ud.deleted_at").IsNull()). Select( goqu.I("r.id").As("report_id"), goqu.I("r.last_withdraw_applied_at"), goqu.I("r.last_withdraw_audit_at"), goqu.I("r.park_id").As("report_park_id"), goqu.I("r.period"), goqu.I("r.published"), goqu.I("r.published_at"), goqu.I("r.withdraw"), @@ -99,7 +94,6 @@ func (wd _WithdrawRepository) FindWithdraw(page uint, keyword *string) ([]model. countReportQuerySql, countReportQueryArgs, _ := countReportQuery.Prepared(true).ToSQL() - var ( reports []*model.ReportRes = make([]*model.ReportRes, 0) total int64 @@ -122,9 +116,8 @@ func (wd _WithdrawRepository) FindWithdraw(page uint, keyword *string) ([]model. return make([]model.Withdraw, 0), total, nil } - fmt.Println(&reports) var withdrawReses []model.Withdraw - //TODO: 2023.07.24对查询到的数据进行拼接 + //TODO: 2023.07.24对查询到的数据进行拼接(完成) for _, v := range reports { Begin := v.Period.SafeLower().Format("2006-01-02") End := v.Period.SafeUpper().Format("2006-01-02") @@ -174,3 +167,75 @@ func (wd _WithdrawRepository) FindWithdraw(page uint, keyword *string) ([]model. return withdrawReses, total, nil } + +//该方法用于审核同意报表撤回 +func (wd _WithdrawRepository) ReviewTrueReportWithdraw( rid string) (bool, error) { + wd.log.Info("审核指定的报表", zap.String("rid", rid)) + ctx, cancel := global.TimeoutContext() + defer cancel() + //update report set withdraw=$2, + //last_withdraw_audit_at=$3, published=false, + //published_at=null where id=$1 + + tx, err := global.DB.Begin(ctx) + if err != nil { + wd.log.Error("开启数据库事务失败", zap.Error(err)) + } + updateQuerySql, updateArgs, _ := wd.ds. + Update(goqu.T("report")). + Set(goqu.Record{ + "withdraw": 3, + "last_withdraw_audit_at": types.Now(), + "published": false, + "published_at": nil, + }). + Where(goqu.I("id").Eq(rid)). + Prepared(true).ToSQL() + + rs, err := tx.Exec(ctx, updateQuerySql, updateArgs...) + if err != nil { + wd.log.Error("审核报表失败", zap.Error(err)) + return false, err + } + err = tx.Commit(ctx) + if err != nil { + wd.log.Error("提交数据库事务失败", zap.Error(err)) + return false, err + } + + return rs.RowsAffected() > 0, nil +} + +func (wd _WithdrawRepository) ReviewFalseReportWithdraw( rid string) (bool, error) { + wd.log.Info("审核指定的报表", zap.String("rid", rid)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + tx, err := global.DB.Begin(ctx) + if err != nil { + wd.log.Error("开启数据库事务失败", zap.Error(err)) + } + updateQuerySql, updateArgs, _ := wd.ds. + Update(goqu.T("report")). + Set(goqu.Record{ + "withdraw": 2, + "last_withdraw_audit_at": types.Now(), + "published": false, + "published_at": nil, + }). + Where(goqu.I("id").Eq(rid)). + Prepared(true).ToSQL() + + rs, err := tx.Exec(ctx, updateQuerySql, updateArgs...) + if err != nil { + wd.log.Error("审核报表失败", zap.Error(err)) + return false, err + } + err = tx.Commit(ctx) + if err != nil { + wd.log.Error("提交数据库事务失败", zap.Error(err)) + return false, err + } + + return rs.RowsAffected() > 0, nil +} \ No newline at end of file diff --git a/vo/withdraw.go b/vo/withdraw.go new file mode 100644 index 0000000..1aa6019 --- /dev/null +++ b/vo/withdraw.go @@ -0,0 +1,6 @@ +package vo + +//用于接收审核报表的参数 +type ReviewWithdraw struct { + Audit bool `json:"audit"` +} From b3032638fcddff72d95b8475a64af1186dc4dfa7 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Tue, 25 Jul 2023 15:34:30 +0800 Subject: [PATCH 06/27] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AE=A1=E6=A0=B8?= =?UTF-8?q?=E6=92=A4=E5=9B=9E=E6=8A=A5=E8=A1=A8=E7=94=A8=E6=88=B7=E9=89=B4?= =?UTF-8?q?=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/withdraw.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/withdraw.go b/controller/withdraw.go index ed14265..868bc72 100644 --- a/controller/withdraw.go +++ b/controller/withdraw.go @@ -15,7 +15,7 @@ var withdrawLog = logger.Named("Handler", "Withdraw") func InitializeWithdrawHandlers(router *fiber.App) { router.Get("/withdraw", security.OPSAuthorize, withdraw) - router.Put("/withdraw/:rid", ReviewRequestWithdraw) + router.Put("/withdraw/:rid",security.OPSAuthorize, ReviewRequestWithdraw) } //用于分页检索用户的核算报表 From 251c44049a3c52225e941397ba057a0fdebc804b Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Wed, 26 Jul 2023 08:52:24 +0800 Subject: [PATCH 07/27] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=AE=A1=E6=A0=B8?= =?UTF-8?q?=E6=92=A4=E5=9B=9E=E6=8A=A5=E8=A1=A8=E8=AF=B7=E6=B1=82=E7=BB=86?= =?UTF-8?q?=E8=8A=82=EF=BC=8C=E5=AE=8C=E6=88=90=E6=92=A4=E5=9B=9E=E7=94=B5?= =?UTF-8?q?=E8=B4=B9=E6=A0=B8=E7=AE=97=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/withdraw.go | 54 ++++++++++++++++++++++++++++++++++++++++-- repository/report.go | 2 +- repository/withdraw.go | 5 ++-- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/controller/withdraw.go b/controller/withdraw.go index 868bc72..f9d6695 100644 --- a/controller/withdraw.go +++ b/controller/withdraw.go @@ -15,7 +15,8 @@ var withdrawLog = logger.Named("Handler", "Withdraw") func InitializeWithdrawHandlers(router *fiber.App) { router.Get("/withdraw", security.OPSAuthorize, withdraw) - router.Put("/withdraw/:rid",security.OPSAuthorize, ReviewRequestWithdraw) + router.Put("/withdraw/:rid", security.OPSAuthorize, reviewRequestWithdraw) + router.Delete("/withdraw/:rid", security.EnterpriseAuthorize, recallReport) } //用于分页检索用户的核算报表 @@ -43,7 +44,7 @@ func withdraw(c *fiber.Ctx) error { } //用于审核撤回报表 -func ReviewRequestWithdraw(c *fiber.Ctx) error { +func reviewRequestWithdraw(c *fiber.Ctx) error { Rid := c.Params("rid", "") Data := new(vo.ReviewWithdraw) result := response.NewResult(c) @@ -82,3 +83,52 @@ func ReviewRequestWithdraw(c *fiber.Ctx) error { } } + +//用于撤回电费核算 +func recallReport(c *fiber.Ctx) error { + // 获取用户会话信息和参数 + rid := c.Params("rid", "") + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + withdrawLog.Error("无法获取当前用户的会话。") + return result.Unauthorized(err.Error()) + } + // 检查指定报表的所属情况 + isBelongsTo, err := repository.ReportRepository.IsBelongsTo(rid, session.Uid) + if err != nil { + withdrawLog.Error("检查报表所属情况出现错误。", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + + if err == nil && isBelongsTo == true { + // 判断指定报表是否是当前园区的最后一张报表 + isLastReport, err := repository.ReportRepository.IsLastReport(rid) + if err != nil { + withdrawLog.Error("判断指定报表是否为当前园区的最后一张报表时出错", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + + if err == nil && isLastReport == true { + // 申请撤回指定的核算报表 + //TODO: 2023.07.25 申请撤回指定核算报表,正确状态未处理(完成) + ok, err := repository.ReportRepository.ApplyWithdrawReport(rid) + if err != nil { + withdrawLog.Error("申请撤回指定核算报表出错", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + if ok { + withdrawLog.Info("申请撤回指定核算报表成功") + return result.Success("申请撤回指定核算报表成功") + } + } else { + withdrawLog.Info("当前报表不是当前园区的最后一张报表") + return result.Error(http.StatusForbidden, "当前报表不是当前园区的最后一张报表") + } + } else { + withdrawLog.Info("指定的核算报表不属于当前用户。") + return result.Error(http.StatusForbidden, "指定的核算报表不属于当前用户") + } + + return result.Error(http.StatusInternalServerError, "其他错误") +} diff --git a/repository/report.go b/repository/report.go index a881c97..b8a4478 100644 --- a/repository/report.go +++ b/repository/report.go @@ -678,7 +678,7 @@ func (rr _ReportRepository) IsLastReport(rid string) (bool, error) { defer cancel() checkSql, checkArgs, _ := rr.ds. - From(goqu.T("report")). + From(goqu.T("report").As("r")). Select(goqu.COUNT("*")). Where( goqu.I("r.id").Eq(rid), diff --git a/repository/withdraw.go b/repository/withdraw.go index 540c5a3..a83b43a 100644 --- a/repository/withdraw.go +++ b/repository/withdraw.go @@ -184,7 +184,7 @@ func (wd _WithdrawRepository) ReviewTrueReportWithdraw( rid string) (bool, error updateQuerySql, updateArgs, _ := wd.ds. Update(goqu.T("report")). Set(goqu.Record{ - "withdraw": 3, + "withdraw": model.REPORT_WITHDRAW_GRANTED, "last_withdraw_audit_at": types.Now(), "published": false, "published_at": nil, @@ -206,6 +206,7 @@ func (wd _WithdrawRepository) ReviewTrueReportWithdraw( rid string) (bool, error return rs.RowsAffected() > 0, nil } +//该方法用于审核拒绝报表撤回 func (wd _WithdrawRepository) ReviewFalseReportWithdraw( rid string) (bool, error) { wd.log.Info("审核指定的报表", zap.String("rid", rid)) ctx, cancel := global.TimeoutContext() @@ -218,7 +219,7 @@ func (wd _WithdrawRepository) ReviewFalseReportWithdraw( rid string) (bool, erro updateQuerySql, updateArgs, _ := wd.ds. Update(goqu.T("report")). Set(goqu.Record{ - "withdraw": 2, + "withdraw": model.REPORT_WITHDRAW_DENIED, "last_withdraw_audit_at": types.Now(), "published": false, "published_at": nil, From 39e404451edc2491516f04e4b9385c30c30617f0 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Wed, 26 Jul 2023 10:03:01 +0800 Subject: [PATCH 08/27] =?UTF-8?q?=E5=AE=8C=E6=88=90=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E5=9F=BA=E5=87=86=E7=BA=BF=E6=8D=9F=E7=8E=87?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.go | 11 +++++++++++ controller/foundation.go | 24 ++++++++++++++++++++++++ router/router.go | 1 + settings.yaml | 2 ++ 4 files changed, 38 insertions(+) create mode 100644 controller/foundation.go diff --git a/config/settings.go b/config/settings.go index b8403ec..b009064 100644 --- a/config/settings.go +++ b/config/settings.go @@ -36,12 +36,18 @@ type ServiceSetting struct { HostSerial int64 } +//读取基准线损率 +type BaseLossSetting struct { + Base string +} + // 定义全局变量 var ( ServerSettings *ServerSetting DatabaseSettings *DatabaseSetting RedisSettings *RedisSetting ServiceSettings *ServiceSetting + BaseLoss *BaseLossSetting ) // 读取配置到全局变量 @@ -69,5 +75,10 @@ func SetupSetting() error { if err != nil { return err } + + err = s.ReadSection("BaselineLineLossRatio", &BaseLoss) + if err != nil { + return err + } return nil } diff --git a/controller/foundation.go b/controller/foundation.go new file mode 100644 index 0000000..65cab6b --- /dev/null +++ b/controller/foundation.go @@ -0,0 +1,24 @@ +package controller + +import ( + "electricity_bill_calc/config" + "electricity_bill_calc/logger" + "electricity_bill_calc/response" + "electricity_bill_calc/security" + "github.com/gofiber/fiber/v2" +) + +var foundLog = logger.Named("Handler", "Foundation") + +func InitializeFoundationHandlers(router *fiber.App) { + router.Get("/norm/authorized/loss/rate", security.MustAuthenticated, getNormAuthorizedLossRate) +} + +func getNormAuthorizedLossRate(c *fiber.Ctx) error { + foundLog.Info("获取系统中定义的基准核定线损率") + result := response.NewResult(c) + BaseLoss := config.BaseLoss + return result.Success("已经获取到系统设置的基准核定线损率。", + fiber.Map{"normAuthorizedLossRate": BaseLoss}, + ) +} diff --git a/router/router.go b/router/router.go index f1db451..38af715 100644 --- a/router/router.go +++ b/router/router.go @@ -56,6 +56,7 @@ func App() *fiber.App { controller.InitializeWithdrawHandlers(app) + controller.InitializeFoundationHandlers(app) return app } diff --git a/settings.yaml b/settings.yaml index 0c4d01c..9968ebe 100644 --- a/settings.yaml +++ b/settings.yaml @@ -21,3 +21,5 @@ Service: ItemsPageSize: 20 CacheLifeTime: 5m HostSerial: 5 +BaselineLineLossRatio: + Base: 基准线损率 \ No newline at end of file From 9ad3415cdb3e8c559cf335894707767eb8c404eb Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Wed, 26 Jul 2023 13:43:30 +0800 Subject: [PATCH 09/27] =?UTF-8?q?=E5=AE=8C=E6=88=90=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E5=BD=93=E5=89=8D=E7=B3=BB=E7=BB=9F=E4=B8=AD=E5=BE=85=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E7=9A=84=E5=86=85=E5=AE=B9=E6=95=B0=E9=87=8F=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/statistics.go | 34 ++++++++++++++++++++++++++++++++++ router/router.go | 14 +++++++------- service/withdraw.go | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 controller/statistics.go create mode 100644 service/withdraw.go diff --git a/controller/statistics.go b/controller/statistics.go new file mode 100644 index 0000000..fb9ab37 --- /dev/null +++ b/controller/statistics.go @@ -0,0 +1,34 @@ +package controller + +import ( + "electricity_bill_calc/logger" + "electricity_bill_calc/response" + "electricity_bill_calc/security" + "electricity_bill_calc/service" + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" + "net/http" +) + +var StatisticsWithdrawLog = logger.Named("Handler", "StatisticsWithdraw") + +func InitializeStatisticsController(router *fiber.App) { + router.Get("/audits", security.OPSAuthorize, currentAuditAmount) + +} + +//获取当前系统中待审核的内容数量 +func currentAuditAmount(c *fiber.Ctx) error { + StatisticsWithdrawLog.Info("开始获取当前系统中待审核的内容数量") + result := response.NewResult(c) + amount, err := service.WithdrawService.AuditWaits() + if err != nil { + StatisticsWithdrawLog.Error("获取当前系统中待审核的内容数量出错", zap.Error(err)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + + return result.Success("已经获取到指定的统计信息", + fiber.Map{"withdraw": amount}) +} + + diff --git a/router/router.go b/router/router.go index 38af715..df7c003 100644 --- a/router/router.go +++ b/router/router.go @@ -34,15 +34,15 @@ func App() *fiber.App { JSONEncoder: json.Marshal, //json编码 JSONDecoder: json.Unmarshal, //json解码 }) - app.Use(compress.New()) //压缩中间件 + app.Use(compress.New()) //压缩中间件 app.Use(recover.New(recover.Config{ EnableStackTrace: true, StackTraceHandler: stackTraceHandler, - })) //恢复中间件 + })) //恢复中间件 app.Use(logger.NewLogMiddleware(logger.LogMiddlewareConfig{ Logger: logger.Named("App"), - })) //日志中间件 - app.Use(security.SessionRecovery) //会话恢复中间件 + })) //日志中间件 + app.Use(security.SessionRecovery) //会话恢复中间件 controller.InitializeUserHandlers(app) controller.InitializeRegionHandlers(app) @@ -54,9 +54,9 @@ func App() *fiber.App { controller.InitializeTopUpHandlers(app) controller.InitializeReportHandlers(app) - - controller.InitializeWithdrawHandlers(app) - controller.InitializeFoundationHandlers(app) + controller.InitializeWithdrawHandlers(app) // 公示撤回 + controller.InitializeFoundationHandlers(app) // 基础数据 + controller.InitializeStatisticsController(app) // 首页信息 return app } diff --git a/service/withdraw.go b/service/withdraw.go new file mode 100644 index 0000000..caa3f09 --- /dev/null +++ b/service/withdraw.go @@ -0,0 +1,39 @@ +package service + +import ( + "electricity_bill_calc/global" + "electricity_bill_calc/logger" + "electricity_bill_calc/model" + "github.com/doug-martin/goqu/v9" + "github.com/georgysavva/scany/v2/pgxscan" + "go.uber.org/zap" +) + +type _WithdrawService struct { + log *zap.Logger + ds goqu.DialectWrapper +} + +var WithdrawService = _WithdrawService{ + logger.Named("Service", "Withdraw"), + goqu.Dialect("postgres"), +} + +func (wd _WithdrawService) AuditWaits() (int64, error) { + wd.log.Info("获取当前系统中待审核的内容数量。") + ctx, cancel := global.TimeoutContext() + defer cancel() + + CountWithdrawQuery, CountWithdrawQueryArgs, _ := wd.ds. + From(goqu.T("report")). + Where(goqu.I("withdraw").Eq(model.REPORT_WITHDRAW_APPLYING)). + Select(goqu.COUNT("*")).ToSQL() + + var total int64 + err := pgxscan.Get(ctx, global.DB, &total, CountWithdrawQuery,CountWithdrawQueryArgs...) + if err != nil { + wd.log.Error("获取当前系统中待审核的内容数量出错。") + return 0,err + } + return total,nil +} From 5866882c2d1f4488fa1c88c0a6844e6719275ec1 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Wed, 26 Jul 2023 15:11:16 +0800 Subject: [PATCH 10/27] =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E4=B8=AD=E7=9A=84=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/statistics.go | 51 +++++++++++++++++++++++++++ model/park.go | 9 ++++- service/statistics.go | 74 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 service/statistics.go diff --git a/controller/statistics.go b/controller/statistics.go index fb9ab37..2d2c07e 100644 --- a/controller/statistics.go +++ b/controller/statistics.go @@ -2,6 +2,7 @@ package controller import ( "electricity_bill_calc/logger" + "electricity_bill_calc/model" "electricity_bill_calc/response" "electricity_bill_calc/security" "electricity_bill_calc/service" @@ -14,6 +15,7 @@ var StatisticsWithdrawLog = logger.Named("Handler", "StatisticsWithdraw") func InitializeStatisticsController(router *fiber.App) { router.Get("/audits", security.OPSAuthorize, currentAuditAmount) + router.Get("/stat/reports", statReports) } @@ -31,4 +33,53 @@ func currentAuditAmount(c *fiber.Ctx) error { fiber.Map{"withdraw": amount}) } +func statReports(c *fiber.Ctx) error { + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + return result.Unauthorized(err.Error()) + } + var ( + enterprises int64 = 0 + parks int64 = 0 + reports []model.ParkPeriodStatistics + ) + if session.Type != 0 { + enterprises, err = service.StatisticsService.EnabledEnterprises() + if err != nil { + StatisticsWithdrawLog.Error(err.Error()) + return result.Error(http.StatusInternalServerError, err.Error()) + } + parks, err = service.StatisticsService.EnabledParks() + if err != nil { + StatisticsWithdrawLog.Error(err.Error()) + return result.Error(http.StatusInternalServerError, err.Error()) + } + //TODO: 2023.07.26 报表数据库结构改变,此处逻辑复杂放在最后处理 + reports, err = service.StatisticsService.ParkNewestState() + if err != nil { + StatisticsWithdrawLog.Error(err.Error()) + return result.Error(http.StatusInternalServerError, err.Error()) + } + } else { + parks, err = service.StatisticsService.EnabledParks(session.Uid) + if err != nil { + StatisticsWithdrawLog.Error(err.Error()) + return result.Error(http.StatusInternalServerError, err.Error()) + } + //TODO: 2023.07.26 报表数据库结构改变,此处逻辑复杂放在最后处理 + reports, err = service.StatisticsService.ParkNewestState(session.Uid) + if err != nil { + StatisticsWithdrawLog.Error(err.Error()) + return result.Error(http.StatusInternalServerError, err.Error()) + } + } + return result.Success("已经完成园区报告的统计。", fiber.Map{ + "statistics": fiber.Map{ + "enterprises": enterprises, + "parks": parks, + "reports": reports, + }, + }) +} diff --git a/model/park.go b/model/park.go index c00df57..b4e3435 100644 --- a/model/park.go +++ b/model/park.go @@ -1,6 +1,7 @@ package model import ( + "electricity_bill_calc/types" "time" "github.com/shopspring/decimal" @@ -35,4 +36,10 @@ type Park struct { type Parks struct { Park NormAuthorizedLossRate float64 `json:"norm_authorized_loss_rate"` -} +} + +type ParkPeriodStatistics struct { + Id string `json:"id"` + Name string `json:"name"` + Period *types.DateRange +} diff --git a/service/statistics.go b/service/statistics.go new file mode 100644 index 0000000..7284984 --- /dev/null +++ b/service/statistics.go @@ -0,0 +1,74 @@ +package service + +import ( + "electricity_bill_calc/global" + "electricity_bill_calc/logger" + "electricity_bill_calc/model" + "github.com/doug-martin/goqu/v9" + "github.com/georgysavva/scany/v2/pgxscan" + "go.uber.org/zap" +) + +type _StatisticsService struct { + l *zap.Logger + ss goqu.DialectWrapper +} + +var StatisticsService = _StatisticsService{ + logger.Named("Service", "Stat"), + goqu.Dialect("postgres"), +} + +//用于统计企业用户数量 +func (ss _StatisticsService) EnabledEnterprises() (int64, error) { + ss.l.Info("开始统计企业数量。") + ctx, cancel := global.TimeoutContext() + defer cancel() + + UserCountQuery, UserCountQueryArgs, _ := ss.ss. + From(goqu.T("user")). + Where(goqu.I("type").Eq(model.USER_TYPE_ENT)). + Where(goqu.I("enabled").Eq(true)). + Select(goqu.COUNT("*")).ToSQL() + + var c int64 + err := pgxscan.Get(ctx, global.DB, &c, UserCountQuery, UserCountQueryArgs...) + if err != nil { + ss.l.Error("统计企业数量出错", zap.Error(err)) + return 0, err + } + return c, nil +} + +//用于统计园区数量 +func (ss _StatisticsService) EnabledParks(userIds ...string) (int64, error) { + ss.l.Info("开始统计园区数量", zap.Strings("userId", userIds)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + ParkCountQuery := ss.ss. + From(goqu.T("park")). + Where(goqu.I("enabled").Eq(true)) + + if len(userIds) > 0 { + ParkCountQuery = ParkCountQuery.Where(goqu.I("user_id").In(userIds)) + } + + ParkCountQuerySql, ParkCountQueryArgs, _ := ParkCountQuery.Select(goqu.COUNT("*")).ToSQL() + + var c int64 + err := pgxscan.Get(ctx, global.DB, &c, ParkCountQuerySql, ParkCountQueryArgs...) + if err != nil { + ss.l.Error("园区数量统计错误", zap.Error(err)) + return 0, err + } + + return c, nil +} + +//用户统计报表 +func (ss _StatisticsService) ParkNewestState(userIds ...string) ([]model.ParkPeriodStatistics, error) { + //TODO: 2023.07.26 报表数据库结构改变,此处逻辑复杂放在最后处理 + //return nil,errors.New("还未处理逻辑") + return []model.ParkPeriodStatistics{}, nil +} From 1099a7c335a57248356c3d5621a4df910d7cf8e3 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Thu, 27 Jul 2023 14:01:45 +0800 Subject: [PATCH 11/27] =?UTF-8?q?[=E5=A4=A9=E7=A5=9E=E6=A8=A1=E5=BC=8F]?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=8C=87=E5=AE=9A=E5=95=86=E6=88=B7=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/god_mode.go | 40 ++++++++++++++++++++++++ repository/god_mode.go | 70 ++++++++++++++++++++++++++++++++++++++++++ router/router.go | 1 + service/god_mode.go | 46 +++++++++++++++++++++++++++ 4 files changed, 157 insertions(+) create mode 100644 controller/god_mode.go create mode 100644 repository/god_mode.go create mode 100644 service/god_mode.go diff --git a/controller/god_mode.go b/controller/god_mode.go new file mode 100644 index 0000000..134dc14 --- /dev/null +++ b/controller/god_mode.go @@ -0,0 +1,40 @@ +package controller + +import ( + "electricity_bill_calc/logger" + "electricity_bill_calc/response" + "electricity_bill_calc/service" + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +var GmLog = logger.Named("Handler", "GM") + +func InitializeGmController(router *fiber.App) { + router.Delete("/gm/tenement", DeleteTenement) +} + +//用于将参数转化为切片 +func getQueryValues(c *fiber.Ctx, paramName string) []string { + values := c.Request().URI().QueryArgs().PeekMulti(paramName) + result := make([]string, len(values)) + for i, v := range values { + result[i] = string(v) + } + return result +} + +func DeleteTenement(c *fiber.Ctx) error { + park := c.Query("park", "") + tenements := getQueryValues(c, "tenements") + result := response.NewResult(c) + GmLog.Info("[天神模式]删除指定园区中的商户", zap.String("park", park), zap.Strings("tenements", tenements)) + + err := service.GMService.DeleteTenements(park, tenements) + if err != nil { + GmLog.Error("[天神模式]删除指定园区中的商户失败", zap.Error(err)) + return result.Error(500, err.Error()) + } + + return result.Success("指定商户已经删除。") +} diff --git a/repository/god_mode.go b/repository/god_mode.go new file mode 100644 index 0000000..38f5040 --- /dev/null +++ b/repository/god_mode.go @@ -0,0 +1,70 @@ +package repository + +import ( + "context" + "electricity_bill_calc/logger" + "fmt" + "github.com/doug-martin/goqu/v9" + "github.com/jackc/pgx/v5" + "go.uber.org/zap" +) + +type _GMRepository struct { + log *zap.Logger + ds goqu.DialectWrapper +} + +var GMRepository = &_GMRepository{ + log: logger.Named("Repository", "GM"), + ds: goqu.Dialect("postgres"), +} + +func (gm _GMRepository) DeleteMeterBinding(ctx context.Context, tx pgx.Tx, pid string, tenements []string, meterCodes ...[]string) error { + DeleteQuery := gm.ds.From(goqu.T("tenement_meter")). + Where(goqu.I("park_id").Eq(pid)). + Delete() + + if len(tenements) > 0 { + DeleteQuery = DeleteQuery. + Where(goqu.I("tenement_id").In(tenements)) + } + + if len(meterCodes) > 0 { + DeleteQuery = DeleteQuery. + Where(goqu.I("meter_id").In(meterCodes)) + } + + DeleteQuerySql, DeleteQueryArgs, _ := DeleteQuery.ToSQL() + + + _, err := tx.Exec(ctx, DeleteQuerySql, DeleteQueryArgs...) + if err != nil { + gm.log.Error("数据库在删除tenement_meter表数据中出错", zap.Error(err)) + tx.Rollback(ctx) + return err + } + return nil +} + +func (gm _GMRepository) DeleteTenements(ctx context.Context, tx pgx.Tx, pid string, tenements []string) error { + DeleteTenements := gm.ds. + From("tenement"). + Where(goqu.I("park_id").Eq(pid)). + Delete() + + fmt.Println(len(tenements)) + if len(tenements) > 0 { + DeleteTenements = DeleteTenements. + Where(goqu.I("id").In(tenements)) + } + + DeleteTenementsSql, DeleteTenementsArgs, _ := DeleteTenements.ToSQL() + + _, err := tx.Exec(ctx, DeleteTenementsSql, DeleteTenementsArgs...) + if err != nil { + tx.Rollback(ctx) + gm.log.Error("删除商户信息出错",zap.Error(err)) + return err + } + return nil +} diff --git a/router/router.go b/router/router.go index df7c003..e09f7b7 100644 --- a/router/router.go +++ b/router/router.go @@ -57,6 +57,7 @@ func App() *fiber.App { controller.InitializeWithdrawHandlers(app) // 公示撤回 controller.InitializeFoundationHandlers(app) // 基础数据 controller.InitializeStatisticsController(app) // 首页信息 + controller.InitializeGmController(app) // 天神模式 return app } diff --git a/service/god_mode.go b/service/god_mode.go new file mode 100644 index 0000000..462cf9c --- /dev/null +++ b/service/god_mode.go @@ -0,0 +1,46 @@ +package service + +import ( + "electricity_bill_calc/global" + "electricity_bill_calc/logger" + "electricity_bill_calc/repository" + "fmt" + "github.com/doug-martin/goqu/v9" + "go.uber.org/zap" +) + +type _GMService struct { + l *zap.Logger + gm goqu.DialectWrapper +} + +var GMService = _GMService{ + logger.Named("Service", "GM"), + goqu.Dialect("postgres"), +} + +func (gm _GMService) DeleteTenements(pid string, tenements []string) error { + var err error + ctx, cancel := global.TimeoutContext() + defer cancel() + + tx, err := global.DB.Begin(ctx) + if err != nil { + gm.l.Error("未能启动数据库事务", zap.Error(err)) + return fmt.Errorf("未能启动数据库事务,%w", err) + } + + err = repository.GMRepository.DeleteMeterBinding(ctx, tx, pid, tenements) + if err != nil { + tx.Rollback(ctx) + return err + } + + err = repository.GMRepository.DeleteTenements(ctx, tx, pid, tenements) + if err != nil { + tx.Rollback(ctx) + return err + } + tx.Commit(ctx) + return nil +} From b64929c10a06cc3243dd431c4d9715af55b23e54 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Thu, 27 Jul 2023 14:15:34 +0800 Subject: [PATCH 12/27] =?UTF-8?q?=E7=BB=99[=E5=A4=A9=E7=A5=9E=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F]=E5=88=A0=E9=99=A4=E6=8C=87=E5=AE=9A=E5=95=86?= =?UTF-8?q?=E6=88=B7=E5=8A=9F=E8=83=BD=E6=B7=BB=E5=8A=A0=E6=9D=83=E9=99=90?= =?UTF-8?q?=E8=AE=A4=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/god_mode.go | 3 ++- controller/statistics.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/controller/god_mode.go b/controller/god_mode.go index 134dc14..eb8fcfd 100644 --- a/controller/god_mode.go +++ b/controller/god_mode.go @@ -3,6 +3,7 @@ package controller import ( "electricity_bill_calc/logger" "electricity_bill_calc/response" + "electricity_bill_calc/security" "electricity_bill_calc/service" "github.com/gofiber/fiber/v2" "go.uber.org/zap" @@ -11,7 +12,7 @@ import ( var GmLog = logger.Named("Handler", "GM") func InitializeGmController(router *fiber.App) { - router.Delete("/gm/tenement", DeleteTenement) + router.Delete("/gm/tenement", security.SingularityAuthorize, DeleteTenement) } //用于将参数转化为切片 diff --git a/controller/statistics.go b/controller/statistics.go index 2d2c07e..fd0555c 100644 --- a/controller/statistics.go +++ b/controller/statistics.go @@ -15,7 +15,7 @@ var StatisticsWithdrawLog = logger.Named("Handler", "StatisticsWithdraw") func InitializeStatisticsController(router *fiber.App) { router.Get("/audits", security.OPSAuthorize, currentAuditAmount) - router.Get("/stat/reports", statReports) + router.Get("/stat/reports", security.OPSAuthorize, statReports) } From 8ab89bca3409279d6ed03638cb955bfacadff501 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Fri, 28 Jul 2023 16:25:49 +0800 Subject: [PATCH 13/27] =?UTF-8?q?[=E5=A4=A9=E7=A5=9E=E6=A8=A1=E5=BC=8F]?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=8C=87=E5=AE=9A=E7=9A=84=E5=9B=AD=E5=8C=BA?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/god_mode.go | 21 +++ repository/god_mode.go | 328 ++++++++++++++++++++++++++++++++++++++++- service/god_mode.go | 41 ++++++ 3 files changed, 387 insertions(+), 3 deletions(-) diff --git a/controller/god_mode.go b/controller/god_mode.go index eb8fcfd..b401838 100644 --- a/controller/god_mode.go +++ b/controller/god_mode.go @@ -5,14 +5,17 @@ import ( "electricity_bill_calc/response" "electricity_bill_calc/security" "electricity_bill_calc/service" + "errors" "github.com/gofiber/fiber/v2" "go.uber.org/zap" + "net/http" ) var GmLog = logger.Named("Handler", "GM") func InitializeGmController(router *fiber.App) { router.Delete("/gm/tenement", security.SingularityAuthorize, DeleteTenement) + router.Delete("/gm/park", security.SingularityAuthorize, DeletePark) } //用于将参数转化为切片 @@ -39,3 +42,21 @@ func DeleteTenement(c *fiber.Ctx) error { return result.Success("指定商户已经删除。") } + +func DeletePark(c *fiber.Ctx) error { + parks := getQueryValues(c, "parks") + result := response.NewResult(c) + GmLog.Info("[天神模式]删除指定园区", zap.Strings("parks", parks)) + + if len(parks) < 0 { + GmLog.Info("[天神模式]用户未指派园区参数或者未指定需要删除的园区。") + return result.Error(http.StatusBadRequest, error.Error(errors.New("必须至少指定一个需要删除的园区!"))) + } + + err := service.GMService.DeleteParks(parks) + if err != nil { + GmLog.Error("[天神模式]删除指定园区失败",zap.Error(err)) + return result.Error(500,err.Error()) + } + return result.Success("指定园区已经删除。") +} diff --git a/repository/god_mode.go b/repository/god_mode.go index 38f5040..5537efb 100644 --- a/repository/god_mode.go +++ b/repository/god_mode.go @@ -36,7 +36,6 @@ func (gm _GMRepository) DeleteMeterBinding(ctx context.Context, tx pgx.Tx, pid s DeleteQuerySql, DeleteQueryArgs, _ := DeleteQuery.ToSQL() - _, err := tx.Exec(ctx, DeleteQuerySql, DeleteQueryArgs...) if err != nil { gm.log.Error("数据库在删除tenement_meter表数据中出错", zap.Error(err)) @@ -46,7 +45,7 @@ func (gm _GMRepository) DeleteMeterBinding(ctx context.Context, tx pgx.Tx, pid s return nil } -func (gm _GMRepository) DeleteTenements(ctx context.Context, tx pgx.Tx, pid string, tenements []string) error { +func (gm _GMRepository) DeleteTenements(ctx context.Context, tx pgx.Tx, pid string, tenements ...[]string) error { DeleteTenements := gm.ds. From("tenement"). Where(goqu.I("park_id").Eq(pid)). @@ -63,7 +62,330 @@ func (gm _GMRepository) DeleteTenements(ctx context.Context, tx pgx.Tx, pid stri _, err := tx.Exec(ctx, DeleteTenementsSql, DeleteTenementsArgs...) if err != nil { tx.Rollback(ctx) - gm.log.Error("删除商户信息出错",zap.Error(err)) + gm.log.Error("删除商户信息出错", zap.Error(err)) + return err + } + return nil +} + +func (gm _GMRepository) DeleteInvoices(ctx context.Context, tx pgx.Tx, parks string, val ...[]string) error { + if len(val) > 0 { + updateQuery, updateQueryArgs, _ := gm.ds. + Update(goqu.T("report_tenement")). + Set(goqu.Record{"invoice": nil}). + Where(goqu.I("invoice").In(val)). + Where( + goqu.I("report_id"). + Eq( + gm.ds. + From(goqu.T("report")). + Where(goqu.I("park_id").Eq(parks)), + ), + ).ToSQL() + _, err := tx.Exec(ctx, updateQuery, updateQueryArgs...) + if err != nil { + tx.Rollback(ctx) + gm.log.Error("更新发票记录出错", zap.Error(err)) + return err + } + } else { + updateQuery, updateQueryArgs, _ := gm.ds. + Update(goqu.T("report_tenement")). + Set(goqu.Record{"invoice": nil}). + Where( + goqu.I("report_id"). + Eq(gm.ds. + From(goqu.T("report")). + Where(goqu.I("park_id").Eq(parks)), + )).ToSQL() + _, err := tx.Exec(ctx, updateQuery, updateQueryArgs...) + if err != nil { + tx.Rollback(ctx) + gm.log.Error("更新发票记录出错", zap.Error(err)) + return err + } + } + + deleteQuery := gm.ds. + From(goqu.T("invoices")). + Where(goqu.I("park_id").Eq(parks)). + Delete() + if len(val) > 0 { + deleteQuery.Where(goqu.I("invoice_code").In(val)) + } + deleteQuerySql, deleteQueryArgs, _ := deleteQuery.ToSQL() + + _, err := tx.Exec(ctx, deleteQuerySql, deleteQueryArgs...) + if err != nil { + tx.Rollback(ctx) + gm.log.Error("删除指定园区发票记录出错", zap.Error(err)) + return err + } + return nil + +} + +func (gm _GMRepository) DeleteMeterPookings(ctx context.Context, tx pgx.Tx, parks string, val ...[]string) error { + deleteQuery := gm.ds. + Delete(goqu.T("meter_relations")). + Where(goqu.I("park_id").Eq(parks)) + + if len(val) > 0 { + deleteQuery = deleteQuery. + Where( + goqu.I("master_meter_id").In(val), + goqu.Or(goqu.I("slave_meter_id").In(val)), + ) + } + deleteQuerySql, deleteQueryArgs, _ := deleteQuery.ToSQL() + _, err := tx.Exec(ctx, deleteQuerySql, deleteQueryArgs...) + if err != nil { + tx.Rollback(ctx) + gm.log.Error("删除指定园区中的表计分摊关系失败", zap.Error(err)) + return err + } + return nil +} + +func (gm _GMRepository) DeleteMeters(ctx context.Context, tx pgx.Tx, parks string, val ...[]string) error { + deleteQuery := gm.ds. + Delete(goqu.T("meter_04kv")). + Where(goqu.I("park_id").Eq(parks)) + + if len(val) > 0 { + deleteQuery = deleteQuery.Where(goqu.I("code").In(val)) + } + + deleteQuerySql, deleteQueryArgs, _ := deleteQuery.ToSQL() + + _, err := tx.Exec(ctx, deleteQuerySql, deleteQueryArgs...) + if err != nil { + tx.Rollback(ctx) + gm.log.Error("删除指定园区的符合条件的标记出错", zap.Error(err)) + return err + } + return nil +} + +func (gm _GMRepository) DeleteReports(ctx context.Context, tx pgx.Tx, parks string, val ...[]string) error { + var err error + + if len(val) > 0 { + deleteReportTenementQuerySql, deleteReportTenementQueryArgs, _ := gm.ds. + Delete(goqu.T("report_tenement")). + Where(goqu.I("report_id").In( + gm.ds. + From(goqu.T("report")). + Where(goqu.I("park_id").Eq(parks)). + Where(goqu.I("id").In(val)), + )).ToSQL() + _, err = tx.Exec(ctx, deleteReportTenementQuerySql, deleteReportTenementQueryArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + + deleteReportPooledConsumptionQuerySql, deleteReportPooledConsumptionQueryArgs, _ := gm.ds. + Delete(goqu.T("report_pooled_consumption")). + Where(goqu.I("report_id").In( + gm.ds. + From(goqu.T("report")). + Where(goqu.I("park_id").Eq(parks)). + Where(goqu.I("id").In(val)), + )).ToSQL() + _, err = tx.Exec(ctx, deleteReportPooledConsumptionQuerySql, deleteReportPooledConsumptionQueryArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + + deleteReportPublicConsumptionQuerySql, deleteReportPublicConsumptionQueryArgs, _ := gm.ds. + Delete(goqu.T("report_public_consumption")). + Where(goqu.I("report_id").In( + gm.ds. + From(goqu.T("report")). + Where(goqu.I("park_id").Eq(parks)). + Where(goqu.I("id").In(val)), + )).ToSQL() + _, err = tx.Exec(ctx, deleteReportPublicConsumptionQuerySql, deleteReportPublicConsumptionQueryArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + + deleteReportSummaryQuerySql, deleteReportSummaryQueryArgs, _ := gm.ds. + Delete(goqu.T("report_summary")). + Where(goqu.I("report_id").In( + gm.ds. + From(goqu.T("report")). + Where(goqu.I("park_id").Eq(parks)). + Where(goqu.I("id").In(val)), + )).ToSQL() + _, err = tx.Exec(ctx, deleteReportSummaryQuerySql, deleteReportSummaryQueryArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + + deleteReportTaskQuerySql, deleteReportTaskQueryArgs, _ := gm.ds. + Delete(goqu.T("report_task")). + Where(goqu.I("report_id").In( + gm.ds. + From(goqu.T("report")). + Where(goqu.I("park_id").Eq(parks)). + Where(goqu.I("id").In(val)), + )).ToSQL() + _, err = tx.Exec(ctx, deleteReportTaskQuerySql, deleteReportTaskQueryArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + + deleteReportQuerySql, deleteReportQueryArgs, _ := gm.ds. + Delete(goqu.T("report")). + Where(goqu.I("park_id").Eq(parks)). + Where(goqu.I("id").In(val)).ToSQL() + _, err = tx.Exec(ctx, deleteReportQuerySql, deleteReportQueryArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + + } else { + deleteReportTenementQuerySql, deleteReportTenementQueryArgs, _ := gm.ds. + Delete(goqu.T("report_tenement")). + Where(goqu.I("report_id").In( + gm.ds. + From(goqu.T("report")). + Where(goqu.I("park_id").Eq(parks)), + )).ToSQL() + _, err = tx.Exec(ctx, deleteReportTenementQuerySql, deleteReportTenementQueryArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + + deleteReportPooledConsumptionQuerySql, deleteReportPooledConsumptionQueryArgs, _ := gm.ds. + Delete(goqu.T("report_pooled_consumption")). + Where(goqu.I("report_id").In( + gm.ds. + From(goqu.T("report")). + Where(goqu.I("park_id").Eq(parks)), + )).ToSQL() + _, err = tx.Exec(ctx, deleteReportPooledConsumptionQuerySql, deleteReportPooledConsumptionQueryArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + + deleteReportPublicConsumptionQuerySql, deleteReportPublicConsumptionQueryArgs, _ := gm.ds. + Delete(goqu.T("report_public_consumption")). + Where(goqu.I("report_id").In( + gm.ds. + From(goqu.T("report")). + Where(goqu.I("park_id").Eq(parks)), + )).ToSQL() + _, err = tx.Exec(ctx, deleteReportPublicConsumptionQuerySql, deleteReportPublicConsumptionQueryArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + + deleteReportSummaryQuerySql, deleteReportSummaryQueryArgs, _ := gm.ds. + Delete(goqu.T("report_summary")). + Where(goqu.I("report_id").In( + gm.ds. + From(goqu.T("report")). + Where(goqu.I("park_id").Eq(parks)), + )).ToSQL() + _, err = tx.Exec(ctx, deleteReportSummaryQuerySql, deleteReportSummaryQueryArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + + deleteReportTaskQuerySql, deleteReportTaskQueryArgs, _ := gm.ds. + Delete(goqu.T("report_task")). + Where(goqu.I("report_id").In( + gm.ds. + From(goqu.T("report")). + Where(goqu.I("park_id").Eq(parks)), + )).ToSQL() + _, err = tx.Exec(ctx, deleteReportTaskQuerySql, deleteReportTaskQueryArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + + deleteReportQuerySql, deleteReportQueryArgs, _ := gm.ds. + Delete(goqu.T("report")). + Where(goqu.I("park_id").Eq(parks)).ToSQL() + _, err = tx.Exec(ctx, deleteReportQuerySql, deleteReportQueryArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + } + + return nil +} + +func (gm _GMRepository) DeleteBuildings(ctx context.Context, tx pgx.Tx, parks string, val ...[]string) error { + if len(val) > 0 { + updateBulidingSql, updateBlidingArgs, _ := gm.ds. + Update(goqu.T("tenement")). + Set(goqu.Record{"building": nil}). + Where(goqu.I("park_id").Eq(parks)). + Where(goqu.I("building").In( + gm.ds. + From(goqu.I("park_building")). + Where(goqu.I("park_id").Eq(parks)). + Where(goqu.I("id").In(val)). + Select(goqu.I("id")), + )).ToSQL() + _, err := tx.Exec(ctx, updateBulidingSql, updateBlidingArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + } else { + updateBulidingSql, updateBlidingArgs, _ := gm.ds. + Update(goqu.T("tenement")). + Set(goqu.Record{"building": nil}). + Where(goqu.I("park_id").Eq(parks)).ToSQL() + _, err := tx.Exec(ctx, updateBulidingSql, updateBlidingArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + } + + deleteQuery := gm.ds. + Delete(goqu.I("park_building")). + Where(goqu.I("park_id").Eq(parks)) + + if len(val) > 0 { + deleteQuery = deleteQuery. + Where(goqu.I("id").In(val)) + } + + deleteQuerySql, deleteQueryArgs, _ := deleteQuery.ToSQL() + _, err := tx.Exec(ctx, deleteQuerySql, deleteQueryArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + return nil +} + +func (gm _GMRepository) DeleteParks(ctx context.Context, tx pgx.Tx, park []string) error { + deleteParksSql, deleteParksArgs, _ := gm.ds. + Delete(goqu.T("park")). + Where(goqu.I("id").In(park)).ToSQL() + + _, err := tx.Exec(ctx, deleteParksSql, deleteParksArgs...) + if err != nil { + tx.Rollback(ctx) return err } return nil diff --git a/service/god_mode.go b/service/god_mode.go index 462cf9c..7623465 100644 --- a/service/god_mode.go +++ b/service/god_mode.go @@ -44,3 +44,44 @@ func (gm _GMService) DeleteTenements(pid string, tenements []string) error { tx.Commit(ctx) return nil } + +func (gm _GMService) DeleteParks(parks []string) error { + + var err error + ctx, cancel := global.TimeoutContext() + defer cancel() + + tx, err := global.DB.Begin(ctx) + if err != nil { + gm.l.Error("未能启动数据库事务", zap.Error(err)) + return fmt.Errorf("未能启动数据库事务,%w", err) + } + for _, pid := range parks { + //删除invoices + err = repository.GMRepository.DeleteInvoices(ctx, tx, pid) + //删除meter_binding + err = repository.GMRepository.DeleteMeterBinding(ctx, tx, pid, []string{}) + //删除meter_pookings + err = repository.GMRepository.DeleteMeterPookings(ctx, tx, pid) + //删除tenements + err = repository.GMRepository.DeleteTenements(ctx, tx, pid, []string{}) + //删除meters + err = repository.GMRepository.DeleteMeters(ctx, tx, pid) + //删除reports + err = repository.GMRepository.DeleteReports(ctx, tx, pid) + //删除buildings + err = repository.GMRepository.DeleteBuildings(ctx, tx, pid) + if err != nil { + gm.l.Error("删除关联表出错。", zap.Error(err)) + break + return err + } + } + err = repository.GMRepository.DeleteParks(ctx, tx, parks) + if err != nil { + gm.l.Error("指定园区删除失败。", zap.Error(err)) + return err + } + tx.Commit(ctx) + return nil +} From 18d48c7fea0a85f0207155b053c6fceba97bc5a7 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Fri, 28 Jul 2023 16:46:52 +0800 Subject: [PATCH 14/27] =?UTF-8?q?[=E5=A4=A9=E7=A5=9E=E6=A8=A1=E5=BC=8F]?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=AC=A6=E5=90=88=E6=9D=A1=E4=BB=B6=E7=9A=84?= =?UTF-8?q?=E6=8A=A5=E8=A1=A8=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/god_mode.go | 19 +++++++++++++++++-- service/god_mode.go | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/controller/god_mode.go b/controller/god_mode.go index b401838..26d5906 100644 --- a/controller/god_mode.go +++ b/controller/god_mode.go @@ -16,6 +16,7 @@ var GmLog = logger.Named("Handler", "GM") func InitializeGmController(router *fiber.App) { router.Delete("/gm/tenement", security.SingularityAuthorize, DeleteTenement) router.Delete("/gm/park", security.SingularityAuthorize, DeletePark) + router.Delete("/gm/report", security.SingularityAuthorize, DeleteReports) } //用于将参数转化为切片 @@ -55,8 +56,22 @@ func DeletePark(c *fiber.Ctx) error { err := service.GMService.DeleteParks(parks) if err != nil { - GmLog.Error("[天神模式]删除指定园区失败",zap.Error(err)) - return result.Error(500,err.Error()) + GmLog.Error("[天神模式]删除指定园区失败", zap.Error(err)) + return result.Error(500, err.Error()) } return result.Success("指定园区已经删除。") } + +func DeleteReports(c *fiber.Ctx) error { + pid := c.Query("park") + reports := getQueryValues(c, "reports") + result := response.NewResult(c) + GmLog.Info("[天神模式]删除符合条件的报表。", zap.Strings("reports", reports)) + + err := service.GMService.DeleteReports(pid, reports) + if err != nil { + GmLog.Error("[天神模式]删除指定园区中的报表失败。", zap.Error(err)) + return result.Error(500, err.Error()) + } + return result.Success("指定报表已经删除。") +} diff --git a/service/god_mode.go b/service/god_mode.go index 7623465..a5cd395 100644 --- a/service/god_mode.go +++ b/service/god_mode.go @@ -85,3 +85,22 @@ func (gm _GMService) DeleteParks(parks []string) error { tx.Commit(ctx) return nil } + +func (gm _GMService) DeleteReports(pid string, reports []string) error { + ctx, cancel := global.TimeoutContext() + defer cancel() + + tx, err := global.DB.Begin(ctx) + if err != nil { + gm.l.Error("未能启动数据库事务", zap.Error(err)) + return fmt.Errorf("未能启动数据库事务,%w", err) + } + + err = repository.GMRepository.DeleteReports(ctx, tx, pid, reports) + if err != nil { + tx.Rollback(ctx) + return err + } + tx.Commit(ctx) + return nil +} From 1dd5f1049d438d55b3efcbe461beed3f636d721e Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Fri, 28 Jul 2023 17:29:41 +0800 Subject: [PATCH 15/27] =?UTF-8?q?[=E5=A4=A9=E7=A5=9E=E6=A8=A1=E5=BC=8F]?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=AC=A6=E5=90=88=E6=9D=A1=E4=BB=B6=E7=9A=84?= =?UTF-8?q?=E5=95=86=E6=88=B7=E7=BB=91=E5=AE=9A=E7=9A=84=E8=A1=A8=E8=AE=A1?= =?UTF-8?q?=E5=85=B3=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/god_mode.go | 32 ++++++++++++++++++++++++++------ service/god_mode.go | 24 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/controller/god_mode.go b/controller/god_mode.go index 26d5906..9088b23 100644 --- a/controller/god_mode.go +++ b/controller/god_mode.go @@ -14,9 +14,10 @@ import ( var GmLog = logger.Named("Handler", "GM") func InitializeGmController(router *fiber.App) { - router.Delete("/gm/tenement", security.SingularityAuthorize, DeleteTenement) - router.Delete("/gm/park", security.SingularityAuthorize, DeletePark) - router.Delete("/gm/report", security.SingularityAuthorize, DeleteReports) + router.Delete("/gm/tenement", security.SingularityAuthorize, deleteTenement) + router.Delete("/gm/park", security.SingularityAuthorize, deletePark) + router.Delete("/gm/report", security.SingularityAuthorize, DeleteEnterprise) + router.Delete("/gm/tenement/meter",security.SingularityAuthorize, deleteTenementMeterRelations) } //用于将参数转化为切片 @@ -29,7 +30,7 @@ func getQueryValues(c *fiber.Ctx, paramName string) []string { return result } -func DeleteTenement(c *fiber.Ctx) error { +func deleteTenement(c *fiber.Ctx) error { park := c.Query("park", "") tenements := getQueryValues(c, "tenements") result := response.NewResult(c) @@ -44,7 +45,7 @@ func DeleteTenement(c *fiber.Ctx) error { return result.Success("指定商户已经删除。") } -func DeletePark(c *fiber.Ctx) error { +func deletePark(c *fiber.Ctx) error { parks := getQueryValues(c, "parks") result := response.NewResult(c) GmLog.Info("[天神模式]删除指定园区", zap.Strings("parks", parks)) @@ -62,7 +63,7 @@ func DeletePark(c *fiber.Ctx) error { return result.Success("指定园区已经删除。") } -func DeleteReports(c *fiber.Ctx) error { +func deleteReports(c *fiber.Ctx) error { pid := c.Query("park") reports := getQueryValues(c, "reports") result := response.NewResult(c) @@ -75,3 +76,22 @@ func DeleteReports(c *fiber.Ctx) error { } return result.Success("指定报表已经删除。") } + +func DeleteEnterprise(c *fiber.Ctx) error { + + return nil +} + + +func deleteTenementMeterRelations(c *fiber.Ctx) error { + result := response.NewResult(c) + parkId := c.Query("park") + tId := getQueryValues(c,"tenements") + metersId := getQueryValues(c, "meters") + GmLog.Info("删除指定园区中的商户与表计的关联关系", zap.String("park id", parkId)) + if err := service.GMService.DeleteTenementMeterRelations(parkId, tId, metersId); err != nil { + meterLog.Error("无法删除指定园区中的商户与表计的关联关系", zap.Error(err)) + return result.NotAccept(err.Error()) + } + return result.Success("删除成功") +} \ No newline at end of file diff --git a/service/god_mode.go b/service/god_mode.go index a5cd395..7025c06 100644 --- a/service/god_mode.go +++ b/service/god_mode.go @@ -104,3 +104,27 @@ func (gm _GMService) DeleteReports(pid string, reports []string) error { tx.Commit(ctx) return nil } + +func (gm _GMService) DeleteTenementMeterRelations(pId string, tId []string, mId []string) error { + gm.l.Info("删除商户表记关系", zap.String("tenement", pId)) + ctx, cancel := global.TimeoutContext(10) + defer cancel() + + tx, err := global.DB.Begin(ctx) + if err != nil { + gm.l.Error("开启数据库事务失败。", zap.Error(err)) + return err + } + if err := repository.GMRepository.DeleteMeterBinding(ctx, tx, pId, tId, mId); err != nil { + gm.l.Error("无法删除商户与表记关系。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + err = tx.Commit(ctx) + if err != nil { + gm.l.Error("未能成功提交数据库事务。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + return nil +} From b84c51b18efdcea1e380011cd3c563603de28561 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Fri, 28 Jul 2023 17:33:36 +0800 Subject: [PATCH 16/27] =?UTF-8?q?=E4=BF=AE=E6=AD=A3router=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E6=96=B9=E6=B3=95=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/god_mode.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/god_mode.go b/controller/god_mode.go index 9088b23..1a3537d 100644 --- a/controller/god_mode.go +++ b/controller/god_mode.go @@ -16,7 +16,7 @@ var GmLog = logger.Named("Handler", "GM") func InitializeGmController(router *fiber.App) { router.Delete("/gm/tenement", security.SingularityAuthorize, deleteTenement) router.Delete("/gm/park", security.SingularityAuthorize, deletePark) - router.Delete("/gm/report", security.SingularityAuthorize, DeleteEnterprise) + router.Delete("/gm/report", security.SingularityAuthorize, deleteReports) router.Delete("/gm/tenement/meter",security.SingularityAuthorize, deleteTenementMeterRelations) } @@ -77,7 +77,7 @@ func deleteReports(c *fiber.Ctx) error { return result.Success("指定报表已经删除。") } -func DeleteEnterprise(c *fiber.Ctx) error { +func deleteEnterprise(c *fiber.Ctx) error { return nil } From c36bfff05a2553a203c66ec553e2f526abc0c72b Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Mon, 31 Jul 2023 09:41:45 +0800 Subject: [PATCH 17/27] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=8C=87=E5=AE=9A?= =?UTF-8?q?=E7=9A=84=E4=BC=81=E4=B8=9A=E7=94=A8=E6=88=B7=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/god_mode.go | 18 +++++++++---- repository/god_mode.go | 59 +++++++++++++++++++++++++++++++++++++++++- service/god_mode.go | 51 ++++++++++++++++++++++++++++++++++-- 3 files changed, 120 insertions(+), 8 deletions(-) diff --git a/controller/god_mode.go b/controller/god_mode.go index 1a3537d..b7b4242 100644 --- a/controller/god_mode.go +++ b/controller/god_mode.go @@ -17,7 +17,8 @@ func InitializeGmController(router *fiber.App) { router.Delete("/gm/tenement", security.SingularityAuthorize, deleteTenement) router.Delete("/gm/park", security.SingularityAuthorize, deletePark) router.Delete("/gm/report", security.SingularityAuthorize, deleteReports) - router.Delete("/gm/tenement/meter",security.SingularityAuthorize, deleteTenementMeterRelations) + router.Delete("/gm/tenement/meter", security.SingularityAuthorize, deleteTenementMeterRelations) + router.Delete("/gm/enterprise", security.SingularityAuthorize, deleteEnterprise) } //用于将参数转化为切片 @@ -78,15 +79,22 @@ func deleteReports(c *fiber.Ctx) error { } func deleteEnterprise(c *fiber.Ctx) error { + uid := c.Query("uid") + result := response.NewResult(c) + GmLog.Info("[天神模式]删除指定企业用户", zap.String("uid", uid)) - return nil + err := service.GMService.DeleteEnterprises(uid) + if err != nil { + GmLog.Error("[天神模式]删除指定企业用户失败", zap.Error(err)) + return result.Error(500, "删除指定企业用户失败。") + } + return result.Success("指定企业用户已经删除。") } - func deleteTenementMeterRelations(c *fiber.Ctx) error { result := response.NewResult(c) parkId := c.Query("park") - tId := getQueryValues(c,"tenements") + tId := getQueryValues(c, "tenements") metersId := getQueryValues(c, "meters") GmLog.Info("删除指定园区中的商户与表计的关联关系", zap.String("park id", parkId)) if err := service.GMService.DeleteTenementMeterRelations(parkId, tId, metersId); err != nil { @@ -94,4 +102,4 @@ func deleteTenementMeterRelations(c *fiber.Ctx) error { return result.NotAccept(err.Error()) } return result.Success("删除成功") -} \ No newline at end of file +} diff --git a/repository/god_mode.go b/repository/god_mode.go index 5537efb..fed5fc8 100644 --- a/repository/god_mode.go +++ b/repository/god_mode.go @@ -2,9 +2,11 @@ package repository import ( "context" + "electricity_bill_calc/global" "electricity_bill_calc/logger" "fmt" "github.com/doug-martin/goqu/v9" + "github.com/georgysavva/scany/v2/pgxscan" "github.com/jackc/pgx/v5" "go.uber.org/zap" ) @@ -125,7 +127,7 @@ func (gm _GMRepository) DeleteInvoices(ctx context.Context, tx pgx.Tx, parks str } -func (gm _GMRepository) DeleteMeterPookings(ctx context.Context, tx pgx.Tx, parks string, val ...[]string) error { +func (gm _GMRepository) DeleteMeterPoolings(ctx context.Context, tx pgx.Tx, parks string, val ...[]string) error { deleteQuery := gm.ds. Delete(goqu.T("meter_relations")). Where(goqu.I("park_id").Eq(parks)) @@ -390,3 +392,58 @@ func (gm _GMRepository) DeleteParks(ctx context.Context, tx pgx.Tx, park []strin } return nil } + +func (gm _GMRepository) ListAllParkIdsInUser(ctx context.Context, tx pgx.Tx, uid string) ([]string, error) { + SearchParkIdsSql, SearchParkIdsArgs, _ := gm.ds. + From(goqu.T("park")). + Where(goqu.I("user_id").Eq(uid)). + Select(goqu.I("id")).ToSQL() + var pids []string + err := pgxscan.Select(ctx, global.DB, &pids, SearchParkIdsSql, SearchParkIdsArgs...) + if err != nil { + gm.log.Error("查询["+uid+"]用户下的所有园区失败", zap.Error(err)) + tx.Rollback(ctx) + return nil, err + } + + return pids, nil +} + +func (gm _GMRepository) DeleteUsers(ctx context.Context, tx pgx.Tx, uid string) error { + var err error + //删除用户关联 + DeleteUserChargeSql, DeleteUserChargeArgs, _ := gm.ds. + Delete(goqu.T("user_charge")). + Where(goqu.I("id").Eq(uid)).ToSQL() + + _, err = tx.Exec(ctx,DeleteUserChargeSql,DeleteUserChargeArgs...) + if err != nil { + gm.log.Error("user_charge表关联出错",zap.Error(err)) + tx.Rollback(ctx) + return err + } + + //删除用户详细信息 + DeleteUserDetailSql, DeleteUserDetailArgs,_ := gm.ds. + Delete(goqu.T("user_detail")). + Where(goqu.I("id").Eq(uid)).ToSQL() + _, err = tx.Exec(ctx,DeleteUserDetailSql,DeleteUserDetailArgs...) + if err != nil { + gm.log.Error("user_detail表详细信息出错",zap.Error(err)) + tx.Rollback(ctx) + return err + } + + //删除用户基础信息 + DeleteUserSql, DeleteUserArgs,_ := gm.ds. + Delete(goqu.T("users")). + Where(goqu.I("id").Eq(uid)).ToSQL() + _, err = tx.Exec(ctx,DeleteUserSql,DeleteUserArgs...) + if err != nil { + gm.log.Error("user表基础信息出错",zap.Error(err)) + tx.Rollback(ctx) + return err + } + + return nil +} diff --git a/service/god_mode.go b/service/god_mode.go index 7025c06..6c66439 100644 --- a/service/god_mode.go +++ b/service/god_mode.go @@ -61,8 +61,8 @@ func (gm _GMService) DeleteParks(parks []string) error { err = repository.GMRepository.DeleteInvoices(ctx, tx, pid) //删除meter_binding err = repository.GMRepository.DeleteMeterBinding(ctx, tx, pid, []string{}) - //删除meter_pookings - err = repository.GMRepository.DeleteMeterPookings(ctx, tx, pid) + //删除meter_poolings + err = repository.GMRepository.DeleteMeterPoolings(ctx, tx, pid) //删除tenements err = repository.GMRepository.DeleteTenements(ctx, tx, pid, []string{}) //删除meters @@ -128,3 +128,50 @@ func (gm _GMService) DeleteTenementMeterRelations(pId string, tId []string, mId } return nil } + +func (gm _GMService) DeleteEnterprises(uid string) error { + var err error + ctx, cancel := global.TimeoutContext() + defer cancel() + + tx, err := global.DB.Begin(ctx) + if err != nil { + gm.l.Error("未能启动数据库事务", zap.Error(err)) + return fmt.Errorf("未能启动数据库事务,%w", err) + } + parks, err := repository.GMRepository.ListAllParkIdsInUser(ctx, tx, uid) + if err != nil { + gm.l.Error("查询园区错误", zap.Error(err)) + tx.Rollback(ctx) + return err + } + + for _, pid := range parks { + err = repository.GMRepository.DeleteInvoices(ctx, tx, pid) + err = repository.GMRepository.DeleteMeterBinding(ctx, tx, pid, []string{}) + err = repository.GMRepository.DeleteMeterPookings(ctx, tx, pid) + err = repository.GMRepository.DeleteTenements(ctx, tx, pid) + err = repository.GMRepository.DeleteMeters(ctx, tx, pid) + err = repository.GMRepository.DeleteReports(ctx, tx, pid) + err = repository.GMRepository.DeleteBuildings(ctx, tx, pid) + + if err != nil { + gm.l.Error("删除用户下关联出错", zap.Error(err)) + return err + } + } + + err = repository.GMRepository.DeleteParks(ctx, tx, parks) + if err != nil { + gm.l.Error("删除用户关联园区错误", zap.Error(err)) + tx.Rollback(ctx) + return err + } + err = repository.GMRepository.DeleteUsers(ctx, tx, uid) + if err != nil { + gm.l.Error("删除用户信息出错", zap.Error(err)) + tx.Rollback(ctx) + return err + } + return nil +} From 9b899be33d199865c60c6864730435cd2ab7ed68 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Mon, 31 Jul 2023 09:50:41 +0800 Subject: [PATCH 18/27] =?UTF-8?q?[=E5=A4=A9=E7=A5=9E=E6=A8=A1=E5=BC=8F]?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=AC=A6=E5=90=88=E6=9D=A1=E4=BB=B6=E8=A1=A8?= =?UTF-8?q?=E8=AE=A1=E5=85=AC=E6=91=8A=E5=85=B3=E7=B3=BB=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/god_mode.go | 14 ++++++++++++++ service/god_mode.go | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/controller/god_mode.go b/controller/god_mode.go index b7b4242..40099cf 100644 --- a/controller/god_mode.go +++ b/controller/god_mode.go @@ -19,6 +19,7 @@ func InitializeGmController(router *fiber.App) { router.Delete("/gm/report", security.SingularityAuthorize, deleteReports) router.Delete("/gm/tenement/meter", security.SingularityAuthorize, deleteTenementMeterRelations) router.Delete("/gm/enterprise", security.SingularityAuthorize, deleteEnterprise) + router.Delete("/gm/meter/pooling", security.SingularityAuthorize, deleteMeterPoolingRelations) } //用于将参数转化为切片 @@ -103,3 +104,16 @@ func deleteTenementMeterRelations(c *fiber.Ctx) error { } return result.Success("删除成功") } + +func deleteMeterPoolingRelations(c *fiber.Ctx) error { + result := response.NewResult(c) + parkId := c.Query("park") + mId := getQueryValues(c, "meters") + GmLog.Info("[天神模式]删除指定园区中的表计公摊关系", zap.String("park id", parkId)) + if err := service.GMService.DeleteMeterPooling(parkId, mId); err != nil { + meterLog.Error("[天神模式]删除指定园区中的表计公摊关系失败", zap.Error(err)) + return result.Error(500, "删除指定园区中的表计公摊关系失败。") + } + return result.Success("指定表计公摊关系已经删除。") + +} diff --git a/service/god_mode.go b/service/god_mode.go index 6c66439..8001b08 100644 --- a/service/god_mode.go +++ b/service/god_mode.go @@ -175,3 +175,25 @@ func (gm _GMService) DeleteEnterprises(uid string) error { } return nil } + +func (gm _GMService) DeleteMeterPooling(pId string, mId []string) error { + ctx, cancel := global.TimeoutContext() + defer cancel() + tx, err := global.DB.Begin(ctx) + if err != nil { + gm.l.Error("开启数据库事务失败。", zap.Error(err)) + return err + } + if err := repository.GMRepository.DeleteMeterPoolings(ctx, tx, pId, mId); err != nil { + gm.l.Error("无法删除指定表记公摊关系。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + err = tx.Commit(ctx) + if err != nil { + gm.l.Error("未能成功提交数据库事务。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + return nil +} \ No newline at end of file From f254ec1f3a703523e0f52257993c1e09e3425f63 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Mon, 31 Jul 2023 10:15:14 +0800 Subject: [PATCH 19/27] =?UTF-8?q?[=E5=A4=A9=E7=A5=9E=E6=A8=A1=E5=BC=8F]?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=AC=A6=E5=90=88=E6=9D=A1=E4=BB=B6=E8=A1=A8?= =?UTF-8?q?=E8=AE=A1=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/god_mode.go | 13 +++++++++++++ service/god_mode.go | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/controller/god_mode.go b/controller/god_mode.go index 40099cf..db1a426 100644 --- a/controller/god_mode.go +++ b/controller/god_mode.go @@ -20,6 +20,7 @@ func InitializeGmController(router *fiber.App) { router.Delete("/gm/tenement/meter", security.SingularityAuthorize, deleteTenementMeterRelations) router.Delete("/gm/enterprise", security.SingularityAuthorize, deleteEnterprise) router.Delete("/gm/meter/pooling", security.SingularityAuthorize, deleteMeterPoolingRelations) + router.Delete("gm/meter", security.SingularityAuthorize, deleteMeters) } //用于将参数转化为切片 @@ -117,3 +118,15 @@ func deleteMeterPoolingRelations(c *fiber.Ctx) error { return result.Success("指定表计公摊关系已经删除。") } + +func deleteMeters(c *fiber.Ctx) error { + result := response.NewResult(c) + parkId := c.Query("park") + mId := getQueryValues(c, "meters") + GmLog.Info("[天神模式]删除指定园区中的表计", zap.String("park id", parkId)) + if err := service.GMService.DeleteMeters(parkId, mId); err != nil { + meterLog.Error("[天神模式]删除指定园区中的表计失败", zap.Error(err)) + return result.Error(500, "删除指定园区中的表计失败。") + } + return result.Success("指定表计已经删除。") +} diff --git a/service/god_mode.go b/service/god_mode.go index 8001b08..a493def 100644 --- a/service/god_mode.go +++ b/service/god_mode.go @@ -149,7 +149,7 @@ func (gm _GMService) DeleteEnterprises(uid string) error { for _, pid := range parks { err = repository.GMRepository.DeleteInvoices(ctx, tx, pid) err = repository.GMRepository.DeleteMeterBinding(ctx, tx, pid, []string{}) - err = repository.GMRepository.DeleteMeterPookings(ctx, tx, pid) + err = repository.GMRepository.DeleteMeterPoolings(ctx, tx, pid) err = repository.GMRepository.DeleteTenements(ctx, tx, pid) err = repository.GMRepository.DeleteMeters(ctx, tx, pid) err = repository.GMRepository.DeleteReports(ctx, tx, pid) @@ -189,6 +189,39 @@ func (gm _GMService) DeleteMeterPooling(pId string, mId []string) error { tx.Rollback(ctx) return err } + err = tx.Commit(ctx) + if err != nil { + gm.l.Error("未能成功提交数据库事务。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + return nil +} + +func (gm _GMService) DeleteMeters(pId string, mId []string) error { + ctx, cancel := global.TimeoutContext() + defer cancel() + tx, err := global.DB.Begin(ctx) + if err != nil { + gm.l.Error("开启数据库事务失败。", zap.Error(err)) + return err + } + if err := repository.GMRepository.DeleteMeterBinding(ctx, tx, pId, []string{}, mId); err != nil { + gm.l.Error("删除指定园区中的表计和商户的绑定关系", zap.Error(err)) + tx.Rollback(ctx) + return err + } + if err := repository.GMRepository.DeleteMeterPoolings(ctx, tx, pId, mId); err != nil { + gm.l.Error("无法删除指定表记公摊关系。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + if err := repository.GMRepository.DeleteMeters(ctx, tx, pId, mId); err != nil { + gm.l.Error("删除指定园区中符合条件的表计。", zap.Error(err)) + tx.Rollback(ctx) + return err + } + err = tx.Commit(ctx) if err != nil { gm.l.Error("未能成功提交数据库事务。", zap.Error(err)) From f688f50ecbd7fb058e92bea2da997ebbf6e8a605 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Thu, 3 Aug 2023 16:59:58 +0800 Subject: [PATCH 20/27] =?UTF-8?q?[=E8=AE=A1=E7=AE=97=E7=9B=B8=E5=85=B3]?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=89=80=E6=9C=89=E7=9A=84=E7=89=A9=E4=B8=9A?= =?UTF-8?q?=E8=A1=A8=E8=AE=A1=EF=BC=8C=E7=84=B6=E5=90=8E=E5=AF=B9=E6=89=80?= =?UTF-8?q?=E6=9C=89=E7=9A=84=E7=89=A9=E4=B8=9A=E8=A1=A8=E8=AE=A1=E7=94=B5?= =?UTF-8?q?=E9=87=8F=E8=BF=9B=E8=A1=8C=E8=AE=A1=E7=AE=97=E3=80=82(?= =?UTF-8?q?=E5=AE=8C=E6=88=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/routerSetting.md | 13 -- model/calculate/calculate.go | 121 ++++++++++++++ model/tenement.go | 10 ++ repository/calculate.go | 171 ++++++++++++++++++++ repository/withdraw.go | 2 +- service/calculate/checking.go | 32 ++++ service/calculate/park.go | 37 +++++ service/calculate/pooled.go | 111 +++++++++++++ service/calculate/shared.go | 105 +++++++++++++ service/calculate/summary.go | 1 + service/calculate/tenement.go | 288 ++++++++++++++++++++++++++++++++++ service/calculate/utils.go | 141 +++++++++++++++++ service/calculate/wattCost.go | 66 ++++++++ tools/utils.go | 1 - 14 files changed, 1084 insertions(+), 15 deletions(-) delete mode 100644 doc/routerSetting.md create mode 100644 service/calculate/checking.go create mode 100644 service/calculate/park.go create mode 100644 service/calculate/pooled.go create mode 100644 service/calculate/shared.go create mode 100644 service/calculate/summary.go create mode 100644 service/calculate/tenement.go create mode 100644 service/calculate/utils.go create mode 100644 service/calculate/wattCost.go diff --git a/doc/routerSetting.md b/doc/routerSetting.md deleted file mode 100644 index 6e9c627..0000000 --- a/doc/routerSetting.md +++ /dev/null @@ -1,13 +0,0 @@ -## fiber -#### fiber实例 -- app(是fiber创建的实例通常用app表示,其中有可选配置选项) - - BodyLimit 设置请求正文允许的最大大小(默认为4 * 1024 * 1024) - - EnablePrintRoutes 不打印框架自带日志(默认false) - - EnableTrustedProxyCheck 禁用受信代理(默认false) - - Prefork 预处理配置(默认false) - - ErrorHandler 全局错误处理 (默认false) - - JSONEncoder json编码 (默认json.Marshal) - - JSONDecoder json解码 (默认json.Unmarshal) - - 。。。。。。。。(还有很多配置) -- Use(中间件设置,一个或者多个) -- Group(类似于gin框架中的路由分组) \ No newline at end of file diff --git a/model/calculate/calculate.go b/model/calculate/calculate.go index da5177c..ff5ebb7 100644 --- a/model/calculate/calculate.go +++ b/model/calculate/calculate.go @@ -3,6 +3,7 @@ package calculate import ( "electricity_bill_calc/model" "electricity_bill_calc/types" + "fmt" "github.com/shopspring/decimal" ) @@ -42,6 +43,11 @@ type Meter struct { Poolings []*Pooling } +type PrimaryTenementStatistics struct { + Tenement model.Tenement + Meters []Meter +} + type TenementCharge struct { Tenement string Overall model.ConsumptionUnit @@ -90,3 +96,118 @@ type PoolingSummary struct { OverallAmount decimal.Decimal PoolingProportion decimal.Decimal } + +func FromReportSummary(summary *model.ReportSummary, pricingMode *model.ReportIndex) Summary { + var parkPrice float64 + switch pricingMode.PricePolicy { + case model.PRICING_POLICY_CONSUMPTION: + parkPrice = summary.ConsumptionFee.Decimal.InexactFloat64() / summary.Overall.Amount.InexactFloat64() + case model.PRICING_POLICY_ALL: + parkPrice = summary.Overall.Fee.InexactFloat64() / summary.Overall.Amount.InexactFloat64() + default: + fmt.Println("无法识别类型") + } + + flatAmount := summary.Overall.Amount.InexactFloat64() - + summary.Critical.Amount.InexactFloat64() - + summary.Peak.Amount.InexactFloat64() - + summary.Valley.Amount.InexactFloat64() + + flatFee := summary.Overall.Amount.InexactFloat64() - + summary.Critical.Fee.InexactFloat64() - + summary.Peak.Fee.InexactFloat64() - + summary.Valley.Fee.InexactFloat64() + + var OverallPrice float64 + if summary.Overall.Amount.GreaterThan(decimal.Zero) { + OverallPrice = parkPrice + } else { + OverallPrice = decimal.Zero.InexactFloat64() + } + + var CriticalPrice float64 + if summary.Critical.Amount.GreaterThan(decimal.Zero) { + CriticalPrice = summary.Critical.Fee.InexactFloat64() / summary.Critical.Amount.InexactFloat64() + } else { + CriticalPrice = decimal.Zero.InexactFloat64() + } + + var PeakPrice float64 + if summary.Peak.Amount.GreaterThan(decimal.Zero) { + PeakPrice = summary.Peak.Fee.InexactFloat64() / summary.Peak.Amount.InexactFloat64() + } else { + PeakPrice = decimal.Zero.InexactFloat64() + } + + var FlatPrice float64 + if decimal.NewFromFloat(flatAmount).GreaterThan(decimal.Zero) { + FlatPrice = flatFee / flatAmount + } else { + FlatPrice = decimal.Zero.InexactFloat64() + } + + var ValleyPrice float64 + if summary.Valley.Amount.GreaterThan(decimal.Zero) { + ValleyPrice = summary.Valley.Fee.InexactFloat64() / summary.Valley.Amount.InexactFloat64() + } else { + ValleyPrice = decimal.Zero.InexactFloat64() + } + + var LossDilutedPrice float64 + if summary.Overall.Amount.GreaterThan(decimal.Zero) { + LossDilutedPrice = parkPrice + } else { + LossDilutedPrice = decimal.Zero.InexactFloat64() + } + + _ = parkPrice + + return Summary{ + ReportId: summary.ReportId, + OverallArea: decimal.Zero, + Overall: model.ConsumptionUnit{ + Amount: summary.Overall.Amount, + Fee: summary.Overall.Fee, + Price: decimal.NewFromFloat(OverallPrice), + Proportion: decimal.NewFromFloat(1.0), + }, + ConsumptionFee: summary.ConsumptionFee.Decimal, + Critical: model.ConsumptionUnit{ + Amount: summary.Critical.Amount, + Fee: summary.Critical.Fee, + Price: decimal.NewFromFloat(CriticalPrice), + Proportion: decimal.NewFromFloat(summary.Critical.Amount.InexactFloat64() / summary.Overall.Amount.InexactFloat64()), + }, + Peak: model.ConsumptionUnit{ + Amount: summary.Peak.Amount, + Fee: summary.Peak.Fee, + Price: decimal.NewFromFloat(PeakPrice), + Proportion: decimal.NewFromFloat(summary.Peak.Amount.InexactFloat64() / summary.Overall.Amount.InexactFloat64()), + }, + Flat: model.ConsumptionUnit{ + Amount: decimal.NewFromFloat(flatAmount), + Fee: decimal.NewFromFloat(flatFee), + Price: decimal.NewFromFloat(FlatPrice), + Proportion: decimal.NewFromFloat(flatAmount / summary.Overall.Amount.InexactFloat64()), + }, + Valley: model.ConsumptionUnit{ + Amount: summary.Valley.Amount, + Fee: summary.Valley.Fee, + Price: decimal.NewFromFloat(ValleyPrice), + Proportion: decimal.NewFromFloat(summary.Valley.Amount.InexactFloat64() / summary.Overall.Amount.InexactFloat64()), + }, + Loss: decimal.Zero, + LossFee: decimal.Zero, + LossProportion: decimal.Zero, + AuthoizeLoss: model.ConsumptionUnit{}, + BasicFee: summary.BasicFee, + BasicPooledPriceConsumption: decimal.Zero, + BasicPooledPriceArea: decimal.Zero, + AdjustFee: summary.AdjustFee, + AdjustPooledPriceConsumption: decimal.Zero, + AdjustPooledPriceArea: decimal.Zero, + LossDilutedPrice: decimal.NewFromFloat(LossDilutedPrice), + TotalConsumption: decimal.Zero, + FinalDilutedOverall: decimal.Zero, + } +} diff --git a/model/tenement.go b/model/tenement.go index eda08f0..c5c0282 100644 --- a/model/tenement.go +++ b/model/tenement.go @@ -21,3 +21,13 @@ type Tenement struct { LastModifiedAt types.DateTime `json:"lastModifiedAt" db:"last_modified_at"` DeletedAt *types.DateTime `json:"deletedAt" db:"deleted_at"` } + +type TenementMeter struct { + ParkId string `db:"park_id"` + TenementId string `db:"tenement_id"` + MeterId string `db:"meter_id"` + ForeignRelation bool `db:"foreign_relation"` + AssociatedAt types.DateTime `db:"associated_at"` + DisassociatedAt types.DateTime `db:"disassociated_at"` + SynchronizedAt types.DateTime `db:"synchronized_at"` +} diff --git a/repository/calculate.go b/repository/calculate.go index 440d9ef..5f04d4c 100644 --- a/repository/calculate.go +++ b/repository/calculate.go @@ -5,6 +5,7 @@ import ( "electricity_bill_calc/logger" "electricity_bill_calc/model" "electricity_bill_calc/types" + "time" "github.com/doug-martin/goqu/v9" _ "github.com/doug-martin/goqu/v9/dialect/postgres" @@ -68,3 +69,173 @@ func (cr _CalculateRepository) UpdateReportTaskStatus(rid string, status int16, } return res.RowsAffected() > 0, nil } + +//获取当前园区中所有公摊表计与商户表计之间的关联关系,包括已经解除的 +func (cr _CalculateRepository) GetAllPoolingMeterRelations(pid string, revokedAfter time.Time) ([]model.MeterRelation, error) { + cr.log.Info("获取当前园区中所有公摊表计与商户表计之间的关联关系,包括已经解除的", zap.String("pid", pid), zap.Time("revokedAfter", revokedAfter)) + + ctx, cancel := global.TimeoutContext() + defer cancel() + relationsSql, relationsArgs, _ := cr.ds. + From(goqu.T("meter_relations")). + Where(goqu.I("park_id").Eq(pid)). + Where(goqu.Or( + goqu.I("revoked_at").IsNull(), + goqu.I("revoked_at").Gte(revokedAfter), + )).ToSQL() + + var meterRelation []model.MeterRelation + + err := pgxscan.Select(ctx, global.DB, meterRelation, relationsSql, relationsArgs...) + if err != nil { + cr.log.Error("获取当前园区中所有公摊表计与商户表计之间的关联关系,包括已经解除的出错", zap.Error(err)) + return nil, err + } + return meterRelation, nil +} + +//获取当前园区中所有的商户与表计的关联关系,包括已经解除的 +func (cr _CalculateRepository) GetAllTenementMeterRelations(pid string, associatedBefore time.Time, disassociatedAfter time.Time) ([]model.TenementMeter, error) { + cr.log.Info("获取当前园区中所有的商户与表计的关联关系,包括已经解除的", zap.String("pid", pid), zap.Time("associatedBefore", associatedBefore), zap.Time("disassociatedAfter", disassociatedAfter)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + relationsQuerySql, relationsQueryArgs, _ := cr.ds. + From(goqu.T("tenement_meter")). + Where(goqu.I("park_id").Eq(pid)). + Where(goqu.And( + goqu.I("associated_at").IsNull(), + goqu.I("associated_at").Lte(associatedBefore), + )). + Where(goqu.And( + goqu.I("associated_at").IsNull(), + goqu.I("associated_at").Gte(disassociatedAfter), + )).ToSQL() + + var tenementMeter []model.TenementMeter + + err := pgxscan.Select(ctx, global.DB, tenementMeter, relationsQuerySql, relationsQueryArgs...) + if err != nil { + cr.log.Error("获取当前园区中所有的商户与表计的关联关系,包括已经解除的", zap.Error(err)) + return nil, err + } + return tenementMeter, nil + +} + +//获取指定报表中所有涉及到的指定类型表计在核算时间段内的所有读数数据 +func (cr _CalculateRepository) GetMeterReadings(rid string, meterType int16) ([]model.MeterReading, error) { + cr.log.Info("获取指定报表中所有涉及到的指定类型表计在核算时间段内的所有读数数据", zap.String("rid", rid), zap.Int16("meterType", meterType)) + + ctx, cancel := global.TimeoutContext() + defer cancel() + + readingsQuerySql, readingsQueryArgs, _ := cr.ds. + From(goqu.T("meter_reading").As(goqu.I("mr"))). + Join( + goqu.T("report").As("r"), + goqu.On(goqu.I("r.park_id").Eq(goqu.I("mr.park_id"))), + ). + Where( + goqu.I("r.id").Eq(rid), + goqu.I("mr.meter_type").Eq(meterType), + // TODO:2023.08.02 此方法出错优先查看是否这里出问题 + goqu.I("mr.read_at::date <@ r.period"), + ). + Order(goqu.I("mr.read_at").Asc()).Select(goqu.I("mr.*")).ToSQL() + + var readings []model.MeterReading + + err := pgxscan.Select(ctx, global.DB, readings, readingsQuerySql, readingsQueryArgs...) + if err != nil { + cr.log.Error("获取指定报表中所有涉及到的指定类型表计在核算时间段内的所有读数数据出错", zap.Error(err)) + return nil, err + } + return readings, nil +} + +// 获取指定报表中所有涉及到的表计在核算起始日期前的最后一次读数 +func (cr _CalculateRepository) GetLastPeriodReadings(rid string, meterType int16) ([]model.MeterReading, error) { + cr.log.Info("获取指定报表中所有涉及到的表计在核算起始日期前的最后一次读数", zap.String("rid", rid), zap.Int16("meterType", meterType)) + + ctx, cancel := global.TimeoutContext() + defer cancel() + + readingsSql, readingsArgs, _ := cr.ds.From(goqu.T("meter_reading").As("mr")). + Select( + goqu.MAX("mr.read_at").As("read_at"), + goqu.I("mr.park_id"), + goqu.I("mr.meter_id"), + goqu.I("mr.meter_type"), + goqu.I("mr.ratio"), + goqu.I("mr.overall"), + goqu.I("mr.critical"), + goqu.I("mr.peak"), + goqu.I("mr.flat"), + goqu.I("mr.valley"), + ). + Join( + goqu.T("report").As("r"), + goqu.On(goqu.I("r.park_id").Eq(goqu.I("mr.park_id"))), + ). + Where( + goqu.I("r.id").Eq(rid), + goqu.I("mr.meter_type").Eq(meterType), + goqu.I(" mr.read_at::date <= lower(r.period)"), + ). + GroupBy( + goqu.I("mr.park_id"), + goqu.I("mr.meter_id"), + goqu.I("mr.meter_type"), + goqu.I("mr.ratio"), + goqu.I("mr.overall"), + goqu.I("mr.critical"), + goqu.I("mr.peak"), + goqu.I("mr.flat"), + goqu.I("mr.valley"), + goqu.I("r.period"), + ).ToSQL() + + var readings []model.MeterReading + err := pgxscan.Select(ctx, global.DB, readings, readingsSql, readingsArgs...) + if err != nil { + cr.log.Error("获取指定报表中所有涉及到的表计在核算起始日期前的最后一次读数出错", zap.Error(err)) + return nil, err + } + return readings, nil +} + +// 取得指定报表所涉及的所有商户信息 +func (cr _CalculateRepository) GetAllTenements(rid string) ([]model.Tenement, error) { + cr.log.Info("取得指定报表所涉及的所有商户信息", zap.String("rid", rid)) + + ctx, cancel := global.TimeoutContext() + defer cancel() + + tenementQuerySql, tenementQueryArgs, _ := cr.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("report").As("r"), + goqu.On(goqu.I("r.park_id").Eq(goqu.I("t.park_id"))), + ). + Select( + goqu.I("t.*"), + goqu.I("b.name").As("building_name"), + ). + Where( + goqu.I("r.id").Eq(rid), + goqu.I("t.moved_in_at <= upper(r.period)"), + ).ToSQL() + + var tenements []model.Tenement + err := pgxscan.Select(ctx, global.DB, tenements, tenementQuerySql, tenementQueryArgs...) + if err != nil { + cr.log.Error("取得指定报表所涉及的所有商户信息出错", zap.Error(err)) + return nil, err + } + return tenements, nil +} diff --git a/repository/withdraw.go b/repository/withdraw.go index a83b43a..c30bd19 100644 --- a/repository/withdraw.go +++ b/repository/withdraw.go @@ -132,7 +132,7 @@ func (wd _WithdrawRepository) FindWithdraw(page uint, keyword *string) ([]model. PeriodBegin: Begin, PeriodEnd: End, Published: v.Published, - PublishedAt: nil, + PublishedAt: tools.TimeToStringPtr(v.LastWithdrawAuditAt), Status: 0., Withdraw: v.Withdraw, } diff --git a/service/calculate/checking.go b/service/calculate/checking.go new file mode 100644 index 0000000..7169626 --- /dev/null +++ b/service/calculate/checking.go @@ -0,0 +1,32 @@ +package calculate +import ( + "electricity_bill_calc/model" + "fmt" + "sync/atomic" +) + +func CheckMeterArea(report *model.ReportIndex, meters []*model.MeterDetail) (bool, error) { + anyAreaOptions := report.BasisPooled == model.POOLING_MODE_AREA || + report.AdjustPooled == model.POOLING_MODE_AREA || + report.PublicPooled == model.POOLING_MODE_AREA || + report.LossPooled == model.POOLING_MODE_AREA + + if anyAreaOptions { + var meterWithoutArea int32 + + for _, m := range meters { + if (m.MeterType == model.METER_INSTALLATION_TENEMENT || m.MeterType == model.METER_INSTALLATION_POOLING) && + m.Area == nil { + atomic.AddInt32(&meterWithoutArea, 1) + } + } + + if meterWithoutArea != 0 { + return false, fmt.Errorf("园区中有 %d 个表计没有设置面积,无法进行按面积摊薄。", meterWithoutArea) + } + + return true, nil + } + + return false, nil +} diff --git a/service/calculate/park.go b/service/calculate/park.go new file mode 100644 index 0000000..1af4ee2 --- /dev/null +++ b/service/calculate/park.go @@ -0,0 +1,37 @@ +package calculate + +import ( + "electricity_bill_calc/model" + "electricity_bill_calc/model/calculate" + "electricity_bill_calc/repository" + "time" +) + +func MetersParkCalculate(report model.ReportIndex, periodStart time.Time, + periodEnd time.Time, meterDetail []*model.MeterDetail, + summary calculate.Summary) ([]calculate.Meter, error) { + parkMeterReadings, err := repository.CalculateRepository.GetMeterReadings(report.Id, model.METER_INSTALLATION_PARK) + if err != nil { + return nil, err + } + + lastTermParkMeterReadings, err := repository.CalculateRepository.GetLastPeriodReadings(report.Id, model.METER_INSTALLATION_PARK) + if err != nil { + return nil, err + } + + parkMeterReadings = append(parkMeterReadings, lastTermParkMeterReadings...) + + var parkMetersReports []calculate.Meter + for _, meter := range meterDetail { + if meter.MeterType == model.METER_INSTALLATION_PARK { + parkMetersReport, err := determinePublicMeterConsumptions(meter.Code, periodStart, periodEnd, parkMeterReadings, *meter, summary) + if err != nil { + return nil, err + } + parkMetersReports = append(parkMetersReports, parkMetersReport) + } + + } + return parkMetersReports, nil +} diff --git a/service/calculate/pooled.go b/service/calculate/pooled.go new file mode 100644 index 0000000..12ddc91 --- /dev/null +++ b/service/calculate/pooled.go @@ -0,0 +1,111 @@ +package calculate + +import ( + "electricity_bill_calc/model" + "electricity_bill_calc/model/calculate" + "electricity_bill_calc/repository" + "github.com/shopspring/decimal" + "time" + "unsafe" +) + +//核算园区中的全部公摊表计的电量用量 +func PooledMetersCalculate(report *model.ReportIndex, periodStart time.Time, + periodEnd time.Time, meterDetails []*model.MeterDetail, + summary calculate.Summary) ([]calculate.Meter, error) { + poolingMeterReadings, err := repository.CalculateRepository.GetMeterReadings(report.Id, model.METER_INSTALLATION_POOLING) + if err != nil { + return nil, err + } + + lastTermPoolingMeterReadings, err := repository.CalculateRepository.GetLastPeriodReadings(report.Id, model.METER_INSTALLATION_POOLING) + if err != nil { + return nil, err + } + + poolingMeterReadings = append(poolingMeterReadings, lastTermPoolingMeterReadings...) + + var poolingMetersReports []calculate.Meter + for _, meter := range meterDetails { + poolingMetersReport, err := determinePublicMeterConsumptions(meter.Code, periodStart, periodEnd, poolingMeterReadings, *meter, summary) + if err != nil { + return nil, err + } + poolingMetersReports = append(poolingMetersReports, poolingMetersReport) + } + return poolingMetersReports, nil +} + +// 确定指定非商户表计在指定时间段内的全部电量 +func determinePublicMeterConsumptions(meterId string, periodStart time.Time, + periodEnd time.Time, readings []model.MeterReading, + meterDetail model.MeterDetail, summary calculate.Summary) (calculate.Meter, error) { + startReading, err := DeterminePublicMeterStartReading(meterId, periodStart, meterDetail.DetachedAt.Time, readings) + if err != nil { + return calculate.Meter{}, err + } + + endReading, err := DeterminePublicMeterEndReading(meterId, periodEnd, meterDetail.DetachedAt.Time, readings) + if err != nil { + return calculate.Meter{}, err + } + + overall, err := ComputeOverall(*startReading, *endReading, summary) + if err != nil { + return calculate.Meter{}, err + } + + critical, err := ComputeCritical(*startReading, *endReading, summary) + if err != nil { + return calculate.Meter{}, err + } + + peak, err := ComputePeak(*startReading, *endReading, summary) + if err != nil { + return calculate.Meter{}, err + } + + flat, err := ComputeFlat(*startReading, *endReading, summary) + if err != nil { + return calculate.Meter{}, err + } + + valley, err := ComputeValley(*startReading, *endReading, summary) + if err != nil { + return calculate.Meter{}, err + } + + return calculate.Meter{ + Code: meterId, + Detail: meterDetail, + CoveredArea: meterDetail.Area.Decimal, + LastTermReading: (*calculate.Reading)(unsafe.Pointer(&model.Reading{ + Ratio: startReading.Ratio, + Overall: startReading.Overall, + Critical: startReading.Critical, + Peak: startReading.Peak, + Flat: startReading.Flat, + Valley: startReading.Valley, + })), + CurrentTermReading: (*calculate.Reading)(unsafe.Pointer(&model.Reading{ + Ratio: endReading.Ratio, + Overall: endReading.Overall, + Critical: endReading.Critical, + Peak: endReading.Peak, + Flat: endReading.Flat, + Valley: endReading.Valley, + })), + Overall: overall, + Critical: critical, + Peak: peak, + Flat: flat, + Valley: valley, + AdjustLoss: model.ConsumptionUnit{}, + PooledBasic: model.ConsumptionUnit{}, + PooledAdjust: model.ConsumptionUnit{}, + PooledLoss: model.ConsumptionUnit{}, + PooledPublic: model.ConsumptionUnit{}, + SharedPoolingProportion: decimal.Decimal{}, + Poolings: nil, + }, nil +} diff --git a/service/calculate/shared.go b/service/calculate/shared.go new file mode 100644 index 0000000..528d1fc --- /dev/null +++ b/service/calculate/shared.go @@ -0,0 +1,105 @@ +package calculate + +import ( + "electricity_bill_calc/model" + "electricity_bill_calc/types" + "errors" + "fmt" + "time" +) + +// 确定指定非商户表计的起始读数 +func DeterminePublicMeterStartReading(meterId string, periodStart time.Time, + attachedAt time.Time, meterReadings []model.MeterReading) (*model.MeterReading, error) { + periodBeginning := types.Date{Time: periodStart}.ToBeginningOfDate() + + if len(meterReadings) <= 0 { + return nil, errors.New(fmt.Sprintf("表计的抄表记录数据不足%s", meterId)) + } + + var minReading types.DateTime + for _, reading := range meterReadings { + if reading.ReadAt.Before(minReading.Time) { + minReading = reading.ReadAt + } + } + startTimes := []time.Time{ + minReading.Time, + periodBeginning.Time, + ShiftToAsiaShanghai(attachedAt), + } + if len(startTimes) < 0 { + return nil, errors.New(fmt.Sprintf("无法确定表计 {%s} 的计量的起始时间", meterId)) + } + + var startReading []model.MeterReading + for _, reading := range meterReadings { + readingAt := ShiftToAsiaShanghai(reading.ReadAt.UTC()) + for _, startTime := range startTimes { + if reading.Meter == meterId && readingAt.After(startTime) || readingAt.Equal(startTime) { + startReading = append(startReading, reading) + break + } + } + } + + if len(startReading) <= 0 { + return nil, errors.New(fmt.Sprintf("无法确定表计 %s 的计量的起始读数", meterId)) + } + + var startReadings *model.MeterReading + for _, readings := range startReading { + if startReadings == nil || readings.ReadAt.Before(startReadings.ReadAt.Time) { + startReadings = &readings + } + } + return startReadings, nil +} + +// 确定指定非商户表计的结束读数 +func DeterminePublicMeterEndReading(meterId string, periodEnd time.Time, + detachedAt time.Time, meterReadings []model.MeterReading) (*model.MeterReading, error) { + periodEnding := types.Date{Time: periodEnd}.ToEndingOfDate() + + if len(meterReadings) <= 0 { + return nil, errors.New(fmt.Sprintf("表计的抄表记录数据不足%s", meterId)) + } + + var minReading types.DateTime + for _, reading := range meterReadings { + if reading.ReadAt.Before(minReading.Time) { + minReading = reading.ReadAt + } + } + startTimes := []time.Time{ + minReading.Time, + periodEnding.Time, + ShiftToAsiaShanghai(detachedAt), + } + if len(startTimes) < 0 { + return nil, errors.New(fmt.Sprintf("无法确定表计 {%s} 的计量的终止时间", meterId)) + } + + var startReading []model.MeterReading + for _, reading := range meterReadings { + readingAt := ShiftToAsiaShanghai(reading.ReadAt.UTC()) + for _, startTime := range startTimes { + if reading.Meter == meterId && readingAt.After(startTime) || readingAt.Equal(startTime) { + startReading = append(startReading, reading) + break + } + } + } + + if len(startReading) <= 0 { + return nil, errors.New(fmt.Sprintf("无法确定表计 %s 的计量的终止读数", meterId)) + } + + var startReadings *model.MeterReading + for _, readings := range startReading { + if startReadings == nil || readings.ReadAt.Before(startReadings.ReadAt.Time) { + startReadings = &readings + } + } + return startReadings, nil +} diff --git a/service/calculate/summary.go b/service/calculate/summary.go new file mode 100644 index 0000000..8b0bbf7 --- /dev/null +++ b/service/calculate/summary.go @@ -0,0 +1 @@ +package calculate diff --git a/service/calculate/tenement.go b/service/calculate/tenement.go new file mode 100644 index 0000000..ca82dad --- /dev/null +++ b/service/calculate/tenement.go @@ -0,0 +1,288 @@ +package calculate + +import ( + "electricity_bill_calc/model" + "electricity_bill_calc/model/calculate" + "electricity_bill_calc/repository" + "errors" + "fmt" + "github.com/shopspring/decimal" + "strings" + "time" + "unsafe" +) + +// 核算园区中的全部商户表计电量用电 +func TenementMetersCalculate(report *model.ReportIndex, PeriodStart time.Time, + PeriodEnd time.Time, meterDetails []*model.MeterDetail, + summary calculate.Summary) ([]model.TenementMeter, error) { + tenements, err := repository.CalculateRepository.GetAllTenements(report.Id) + if err != nil { + fmt.Println("tenement 0", err) + return nil, err + } + + tenementMeterRelations, err := repository.CalculateRepository.GetAllTenementMeterRelations(report.Park, PeriodEnd, PeriodStart) + if err != nil { + fmt.Println("tenement 1", err) + return nil, err + } + + tenementMeterReadings, err := repository.CalculateRepository.GetMeterReadings(report.Id, model.METER_INSTALLATION_TENEMENT) + if err != nil { + fmt.Println("tenement 2", err) + return nil, err + } + + lastPeriodReadings, err := repository.CalculateRepository.GetLastPeriodReadings(report.Id, model.METER_INSTALLATION_TENEMENT) + if err != nil { + fmt.Println("tenement 3", err) + return nil, err + } + + var tenementReports []model.Tenement + + for _, tenement := range tenements { + var meters []model.TenementMeter + + for _, relation := range tenementMeterRelations { + if strings.EqualFold(relation.TenementId, tenement.Id) { + meters = append(meters, relation) + } + } + + pt, err := determineTenementConsumptions( + tenement, + meters, + PeriodStart, + PeriodEnd, + tenementMeterReadings, + lastPeriodReadings, + meterDetails, + summary, + ) + if err != nil { + return nil, err + } + + report := model.Tenement{ + Id: pt.Tenement.Id, + Park: pt.Tenement.Park, + FullName: pt.Tenement.FullName, + ShortName: pt.Tenement.ShortName, + Abbr: pt.Tenement.Abbr, + Address: pt.Tenement.Address, + ContactName: pt.Tenement.ContactName, + ContactPhone: pt.Tenement.ContactPhone, + Building: pt.Tenement.Building, + BuildingName: pt.Tenement.BuildingName, + OnFloor: pt.Tenement.OnFloor, + InvoiceInfo: pt.Tenement.InvoiceInfo, + MovedInAt: pt.Tenement.MovedInAt, + MovedOutAt: pt.Tenement.MovedOutAt, + CreatedAt: pt.Tenement.CreatedAt, + LastModifiedAt: pt.Tenement.LastModifiedAt, + DeletedAt: pt.Tenement.DeletedAt, + } + + tenementReports = append(tenementReports, report) + } + + return tenementMeterRelations, nil +} + +//TODO: 2023.08.02 此方法未完成此方法主要用于。确定指定商户在指定时间段内的所有表计读数(完成) +func determineTenementConsumptions(tenement model.Tenement, + relatedMeters []model.TenementMeter, periodStart time.Time, + periodEnd time.Time, currentTermReadings []model.MeterReading, lastPeriodReadings []model.MeterReading, + meterDetails []*model.MeterDetail, summary calculate.Summary) (calculate.PrimaryTenementStatistics, error) { + var meters []calculate.Meter + for _, meter := range relatedMeters { + startReading, err := determineTenementMeterStartReading(meter.MeterId, periodStart, ShiftToAsiaShanghai(tenement.MovedInAt.Time), meter, currentTermReadings, lastPeriodReadings) + if err != nil { + fmt.Println(err) + return calculate.PrimaryTenementStatistics{}, err + } + + endReading, err := determineTenementMeterEndReading(meter.MeterId, periodEnd, ShiftToAsiaShanghai(tenement.MovedOutAt.Time), meter, currentTermReadings) + if err != nil { + fmt.Println(err) + return calculate.PrimaryTenementStatistics{}, err + } + + detail, err := getMeterDetail(meterDetails, meter.MeterId) + if err != nil { + return calculate.PrimaryTenementStatistics{}, err + } + + overall, err := ComputeOverall(*startReading, *endReading, summary) + if err != nil { + return calculate.PrimaryTenementStatistics{}, err + } + + critical, err := ComputeCritical(*startReading, *endReading, summary) + if err != nil { + return calculate.PrimaryTenementStatistics{}, err + } + + peak, err := ComputePeak(*startReading, *endReading, summary) + if err != nil { + return calculate.PrimaryTenementStatistics{}, err + } + + flat, err := ComputeFlat(*startReading, *endReading, summary) + if err != nil { + return calculate.PrimaryTenementStatistics{}, err + } + + valley, err := ComputeValley(*startReading, *endReading, summary) + if err != nil { + return calculate.PrimaryTenementStatistics{}, err + } + + lastTermReading := model.Reading{ + Ratio: startReading.Ratio, + Overall: startReading.Overall, + Critical: startReading.Critical, + Peak: startReading.Peak, + Flat: startReading.Flat, + Valley: startReading.Valley, + } + + lastTermReadingPtr := &lastTermReading + + currentTermReading := model.Reading{ + Ratio: endReading.Ratio, + Overall: endReading.Overall, + Critical: endReading.Critical, + Peak: endReading.Peak, + Flat: endReading.Flat, + Valley: endReading.Valley, + } + + currentTermReadingPtr := ¤tTermReading + meter := calculate.Meter{ + Code: meter.MeterId, + Detail: detail, + CoveredArea: decimal.NewFromFloat(detail.Area.Decimal.InexactFloat64()), + + LastTermReading: (*calculate.Reading)(unsafe.Pointer(lastTermReadingPtr)), + CurrentTermReading: (*calculate.Reading)(unsafe.Pointer(currentTermReadingPtr)), + + Overall: overall, + Critical: critical, + Peak: peak, + Flat: flat, + Valley: valley, + + AdjustLoss: model.ConsumptionUnit{}, + PooledBasic: model.ConsumptionUnit{}, + PooledAdjust: model.ConsumptionUnit{}, + PooledLoss: model.ConsumptionUnit{}, + PooledPublic: model.ConsumptionUnit{}, + SharedPoolingProportion: decimal.Decimal{}, + Poolings: nil, + } + + meters = append(meters, meter) + } + + return calculate.PrimaryTenementStatistics{ + Tenement: tenement, + Meters: meters, + }, nil +} + +func getMeterDetail(meterDetails []*model.MeterDetail, code string) (model.MeterDetail, error) { + for _, detail := range meterDetails { + if detail.Code == code { + return *detail, nil + } + } + return model.MeterDetail{}, errors.New(fmt.Sprintf("表计 %s 的详细信息不存在", code)) +} + +//确定指定表计的起始读数 +func determineTenementMeterStartReading(meterId string, periodStart time.Time, tenementMovedInAt time.Time, + meterRelation model.TenementMeter, currentTermReadings []model.MeterReading, + lastPeriodReadings []model.MeterReading) (*model.MeterReading, error) { + var startTime time.Time + timeList := []time.Time{ + periodStart, + tenementMovedInAt, + meterRelation.AssociatedAt.Time, + } + + for _, t := range timeList { + if t.After(startTime) { + startTime = t + } + } + if startTime.IsZero() { + return nil, fmt.Errorf("无法确定表计 %s 的计量的起始时间", meterId) + } + + var startReading *model.MeterReading + if startTime.Equal(periodStart) { + for _, reading := range lastPeriodReadings { + if reading.Meter == meterId { + if startReading == nil || reading.ReadAt.After(startReading.ReadAt.Time) { + startReading = &reading + } + } + } + } else { + for _, reading := range currentTermReadings { + readingAt := ShiftToAsiaShanghai(reading.ReadAt.Time) + if reading.Meter == meterId && readingAt.After(startTime) { + if startReading == nil || readingAt.Before(startReading.ReadAt.Time) { + startReading = &reading + } + } + } + } + if startReading == nil { + return nil, errors.New("无法确定表计 " + meterId + " 的计量的起始读数") + } + return startReading, nil +} + +// 确定指定表计的终止读书 +func determineTenementMeterEndReading(meterId string, periodEnd time.Time, + TenementMovedOutAt time.Time, meterRelation model.TenementMeter, + currentTermReadings []model.MeterReading) (*model.MeterReading, error) { + var endTime time.Time + timeList := []time.Time{ + periodEnd, + TenementMovedOutAt, + ShiftToAsiaShanghai(meterRelation.DisassociatedAt.Time), + } + for _, t := range timeList { + if t.After(endTime) { + endTime = t + } + } + if endTime.IsZero() { + return nil, fmt.Errorf("无法确定表计 %s 的计量的结束时间", meterId) + } + + var endReading *model.MeterReading + + for _, reading := range currentTermReadings { + readingAt := ShiftToAsiaShanghai(reading.ReadAt.Time) + if reading.Meter == meterId && readingAt.Before(endTime) { + if endReading == nil || readingAt.After(ShiftToAsiaShanghai(endReading.ReadAt.Time)) { + endReading = &reading + } + } + } + if endReading == nil { + return nil, errors.New(fmt.Sprintf("无法确定表计 %s 的计量的结束读数", meterId)) + } + return endReading, nil +} + +func ShiftToAsiaShanghai(t time.Time) time.Time { + location, _ := time.LoadLocation("Asia/Shanghai") + return t.In(location) +} diff --git a/service/calculate/utils.go b/service/calculate/utils.go new file mode 100644 index 0000000..801a5c7 --- /dev/null +++ b/service/calculate/utils.go @@ -0,0 +1,141 @@ +package calculate + +import ( + "electricity_bill_calc/model" + "electricity_bill_calc/model/calculate" + "errors" + "fmt" + "github.com/shopspring/decimal" +) + +// 计算两个读书之间的有功(总)电量 +func ComputeOverall(startReading model.MeterReading, endReading model.MeterReading, summary calculate.Summary) (model.ConsumptionUnit, error) { + start := startReading.Overall.InexactFloat64() * startReading.Ratio.InexactFloat64() + end := endReading.Overall.InexactFloat64() * endReading.Ratio.InexactFloat64() + + if start > end { + return model.ConsumptionUnit{}, errors.New(fmt.Sprintf("表计 {%s} 有功(总)开始读数 {%x} 大于结束读数 {%x}", startReading.Meter, start, end)) + } + + amount := end - start + + var summaryAmount float64 + if summary.Overall.Amount == decimal.Zero { + summaryAmount = decimal.NewFromFloat(1.0).InexactFloat64() + } else { + summaryAmount = summary.Overall.Amount.InexactFloat64() + } + + return model.ConsumptionUnit{ + Amount: decimal.NewFromFloat(amount), + Fee: decimal.NewFromFloat(amount * summary.Overall.Price.InexactFloat64()), + Price: decimal.NewFromFloat(summary.Overall.Price.InexactFloat64()), + Proportion: decimal.NewFromFloat(amount / summaryAmount), + }, nil +} + +//计算两个读书之间的尖峰电量 +func ComputeCritical(startReading model.MeterReading, endReading model.MeterReading, summary calculate.Summary) (model.ConsumptionUnit, error) { + start := startReading.Critical.InexactFloat64() * startReading.Ratio.InexactFloat64() + end := endReading.Critical.InexactFloat64() * endReading.Ratio.InexactFloat64() + + if start > end { + return model.ConsumptionUnit{}, errors.New(fmt.Sprintf("尖峰开始读数 {%x} 大于结束读数 {%x}", start, end)) + } + + amount := end - start + var summaryAmount float64 + + if summary.Critical.Amount.Equal(decimal.Zero) { + summaryAmount = decimal.NewFromFloat(1.0).InexactFloat64() + } else { + summaryAmount = summary.Critical.Amount.InexactFloat64() + } + + return model.ConsumptionUnit{ + Amount: decimal.NewFromFloat(amount), + Fee: decimal.NewFromFloat(amount * summary.Critical.Amount.InexactFloat64()), + Price: decimal.NewFromFloat(summary.Critical.Price.InexactFloat64()), + Proportion: decimal.NewFromFloat(amount / summaryAmount), + }, nil +} + +// 计算两个读数之间的峰电量 +func ComputePeak(startReading model.MeterReading, endReading model.MeterReading, summary calculate.Summary) (model.ConsumptionUnit, error) { + start := startReading.Peak.InexactFloat64() * startReading.Ratio.InexactFloat64() + end := startReading.Peak.InexactFloat64() * endReading.Ratio.InexactFloat64() + + if start > end { + return model.ConsumptionUnit{}, errors.New(fmt.Sprintf("峰开始读数 {%x} 大于结束读数 {%x}", start, end)) + } + + amount := end - start + var summaryAmount float64 + + if summary.Peak.Amount.Equal(decimal.Zero) { + summaryAmount = decimal.NewFromFloat(1.0).InexactFloat64() + } else { + summaryAmount = summary.Peak.Amount.InexactFloat64() + } + + return model.ConsumptionUnit{ + Amount: decimal.NewFromFloat(amount), + Fee: decimal.NewFromFloat(amount * summary.Peak.Price.InexactFloat64()), + Price: decimal.NewFromFloat(summary.Peak.Price.InexactFloat64()), + Proportion: decimal.NewFromFloat(amount / summaryAmount), + }, nil + +} + +//计算两个读数之间的平电量 +func ComputeFlat(startReading model.MeterReading, endReading model.MeterReading, summary calculate.Summary) (model.ConsumptionUnit, error) { + start := startReading.Flat.InexactFloat64() * startReading.Ratio.InexactFloat64() + end := endReading.Flat.InexactFloat64() * endReading.Ratio.InexactFloat64() + + if start > end { + return model.ConsumptionUnit{}, errors.New(fmt.Sprintf("平开始读数 {%x} 大于结束读数 {%x}", start, end)) + } + + amount := end - start + var summaryAmount float64 + + if summary.Flat.Amount.Equal(decimal.Zero) { + summaryAmount = decimal.NewFromFloat(1.0).InexactFloat64() + } else { + summaryAmount = summary.Flat.Amount.InexactFloat64() + } + + return model.ConsumptionUnit{ + Amount: decimal.NewFromFloat(amount), + Fee: decimal.NewFromFloat(amount * summary.Flat.Price.InexactFloat64()), + Price: decimal.NewFromFloat(summary.Flat.Price.InexactFloat64()), + Proportion: decimal.NewFromFloat(amount / summaryAmount), + }, nil +} + +//计算两个读数之间的谷电量 +func ComputeValley(startReading model.MeterReading, endReading model.MeterReading, summary calculate.Summary) (model.ConsumptionUnit, error) { + start := startReading.Valley.InexactFloat64() * startReading.Ratio.InexactFloat64() + end := endReading.Valley.InexactFloat64() * endReading.Ratio.InexactFloat64() + + if start > end { + return model.ConsumptionUnit{}, errors.New(fmt.Sprintf("谷开始读数 {%x} 大于结束读数 {%x}", start, end)) + } + + amount := end - start + + var summaryAmount float64 + + if summary.Valley.Amount.Equal(decimal.Zero) { + summaryAmount = decimal.NewFromFloat(1.0).InexactFloat64() + } else { + summaryAmount = summary.Valley.Amount.InexactFloat64() + } + + return model.ConsumptionUnit{ + Amount: decimal.NewFromFloat(amount), + Fee: decimal.NewFromFloat(amount * summary.Valley.Price.InexactFloat64()), + Price: decimal.NewFromFloat(summary.Valley.Price.InexactFloat64()), + Proportion: decimal.NewFromFloat(amount / summaryAmount), + }, nil +} diff --git a/service/calculate/wattCost.go b/service/calculate/wattCost.go new file mode 100644 index 0000000..5d2956e --- /dev/null +++ b/service/calculate/wattCost.go @@ -0,0 +1,66 @@ +package calculate + +import ( + "electricity_bill_calc/model/calculate" + "electricity_bill_calc/repository" + "fmt" +) + +func MainCalculateProcess(rid string) { + report, err := repository.ReportRepository.GetReportIndex(rid) + if err != nil { + fmt.Println("1", err.Error()+"指定报表不存在") + return + } + + reportSummary, err := repository.ReportRepository.RetrieveReportSummary(rid) + if err != nil { + fmt.Println("2", err.Error()+"指定报表的基本电量电费数据不存在") + return + } + + summary := calculate.FromReportSummary(reportSummary, report) + + periodStart := report.Period.SafeLower() + periodEnd := report.Period.SafeUpper() + + meterDetails, err := repository.MeterRepository.AllUsedMetersInReport(report.Id) + if err != nil { + fmt.Println("3", err) + return + } + + meterRelations, err := repository.CalculateRepository.GetAllPoolingMeterRelations(report.Park, periodStart.Time) + if err != nil { + fmt.Println("4", err) + return + } + _, err = CheckMeterArea(report, meterDetails) + if err != nil { + fmt.Println("5", err) + return + } + + // 寻找每一个商户的所有表计读数,然后对分配到各个商户的表计读数进行初步的计算. + tenementReports, err := TenementMetersCalculate(report, periodStart.Time, periodEnd.Time, meterDetails, summary) + if err != nil { + fmt.Println("6", err) + return + } + + //取得所有公摊表计的读数,以及公摊表计对应的分摊表计 + poolingMetersReports, err := PooledMetersCalculate(report, periodStart.Time, periodEnd.Time, meterDetails, summary) + if err != nil { + fmt.Println("7", err) + return + } + + parkMetersReports, err := MetersParkCalculate(*report, periodStart.Time, periodEnd.Time, meterDetails, summary) + if err != nil { + fmt.Println("8", err) + return + } + + fmt.Println(meterRelations, tenementReports, poolingMetersReports, parkMetersReports) + +} diff --git a/tools/utils.go b/tools/utils.go index af61a41..325453f 100644 --- a/tools/utils.go +++ b/tools/utils.go @@ -160,7 +160,6 @@ func NullTime2PointerString(nullTime sql.NullTime) *string { } } - //该方法用于将时间解析为字符串指针 func TimeToStringPtr(t *time.Time) *string { if t == nil { From 8fc463bd9d53487d314bb9f2a69eda18cbeb407f Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Thu, 3 Aug 2023 17:30:00 +0800 Subject: [PATCH 21/27] =?UTF-8?q?[=E8=AE=A1=E7=AE=97=E7=9B=B8=E5=85=B3]?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E6=89=80=E6=9C=89=E8=A1=A8=E8=AE=A1=E7=9A=84?= =?UTF-8?q?=E6=80=BB=E7=94=B5=E9=87=8F(=E5=AE=8C=E6=88=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- service/calculate/summary.go | 38 +++++++++++++++++++++++++++++++++++ service/calculate/tenement.go | 28 ++++---------------------- service/calculate/wattCost.go | 6 +++++- 3 files changed, 47 insertions(+), 25 deletions(-) diff --git a/service/calculate/summary.go b/service/calculate/summary.go index 8b0bbf7..50bd708 100644 --- a/service/calculate/summary.go +++ b/service/calculate/summary.go @@ -1 +1,39 @@ package calculate + +import ( + "electricity_bill_calc/model/calculate" + "github.com/shopspring/decimal" +) + +// 计算已经启用的商铺面积和 +func TotalConsumptionCalculate(tenements []calculate.PrimaryTenementStatistics, + summary calculate.Summary) decimal.Decimal { + var areaMaters []calculate.Meter + for _, t := range tenements { + areaMaters = append(areaMaters, t.Meters...) + } + + areaMaters = removeDuplicates(areaMaters) + + var areaTotal float64 + for _, m := range areaMaters { + areaTotal += m.Detail.Area.Decimal.InexactFloat64() + } + + areaTotal += summary.OverallArea.InexactFloat64() + + return decimal.NewFromFloat(areaTotal) + +} + +func removeDuplicates(meters []calculate.Meter) []calculate.Meter { + result := make([]calculate.Meter, 0, len(meters)) + seen := make(map[string]bool) + for _, meter := range meters { + if !seen[meter.Code] { + seen[meter.Code] = true + result = append(result, meter) + } + } + return result +} diff --git a/service/calculate/tenement.go b/service/calculate/tenement.go index ca82dad..1749140 100644 --- a/service/calculate/tenement.go +++ b/service/calculate/tenement.go @@ -15,7 +15,7 @@ import ( // 核算园区中的全部商户表计电量用电 func TenementMetersCalculate(report *model.ReportIndex, PeriodStart time.Time, PeriodEnd time.Time, meterDetails []*model.MeterDetail, - summary calculate.Summary) ([]model.TenementMeter, error) { + summary calculate.Summary) ([]calculate.PrimaryTenementStatistics, error) { tenements, err := repository.CalculateRepository.GetAllTenements(report.Id) if err != nil { fmt.Println("tenement 0", err) @@ -40,7 +40,7 @@ func TenementMetersCalculate(report *model.ReportIndex, PeriodStart time.Time, return nil, err } - var tenementReports []model.Tenement + var tenementReports []calculate.PrimaryTenementStatistics for _, tenement := range tenements { var meters []model.TenementMeter @@ -65,30 +65,10 @@ func TenementMetersCalculate(report *model.ReportIndex, PeriodStart time.Time, return nil, err } - report := model.Tenement{ - Id: pt.Tenement.Id, - Park: pt.Tenement.Park, - FullName: pt.Tenement.FullName, - ShortName: pt.Tenement.ShortName, - Abbr: pt.Tenement.Abbr, - Address: pt.Tenement.Address, - ContactName: pt.Tenement.ContactName, - ContactPhone: pt.Tenement.ContactPhone, - Building: pt.Tenement.Building, - BuildingName: pt.Tenement.BuildingName, - OnFloor: pt.Tenement.OnFloor, - InvoiceInfo: pt.Tenement.InvoiceInfo, - MovedInAt: pt.Tenement.MovedInAt, - MovedOutAt: pt.Tenement.MovedOutAt, - CreatedAt: pt.Tenement.CreatedAt, - LastModifiedAt: pt.Tenement.LastModifiedAt, - DeletedAt: pt.Tenement.DeletedAt, - } - - tenementReports = append(tenementReports, report) + tenementReports = append(tenementReports, pt) } - return tenementMeterRelations, nil + return tenementReports, nil } //TODO: 2023.08.02 此方法未完成此方法主要用于。确定指定商户在指定时间段内的所有表计读数(完成) diff --git a/service/calculate/wattCost.go b/service/calculate/wattCost.go index 5d2956e..f9556f1 100644 --- a/service/calculate/wattCost.go +++ b/service/calculate/wattCost.go @@ -55,12 +55,16 @@ func MainCalculateProcess(rid string) { return } + // 获取所有的物业表计,然后对所有的物业表计电量进行计算。 parkMetersReports, err := MetersParkCalculate(*report, periodStart.Time, periodEnd.Time, meterDetails, summary) if err != nil { fmt.Println("8", err) return } - fmt.Println(meterRelations, tenementReports, poolingMetersReports, parkMetersReports) + //计算所有表计的总电量 + parkTotal := TotalConsumptionCalculate(tenementReports, summary) + + fmt.Println(meterRelations, poolingMetersReports, parkMetersReports, parkTotal) } From 6b3d3dd93c249d2dcf8eda57430d2301d05503fe Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Fri, 4 Aug 2023 09:39:59 +0800 Subject: [PATCH 22/27] =?UTF-8?q?[=E8=AE=A1=E7=AE=97=E7=9B=B8=E5=85=B3]?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E7=BA=BF=E6=8D=9F=E4=BB=A5=E5=8F=8A=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E7=BA=BF=E6=8D=9F=EF=BC=88=E5=AE=8C=E6=88=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model/report.go | 40 +++++++++++++++------------- service/calculate/summary.go | 50 +++++++++++++++++++++++++++++++++++ service/calculate/wattCost.go | 6 +++++ 3 files changed, 77 insertions(+), 19 deletions(-) diff --git a/model/report.go b/model/report.go index 2972647..1d7d207 100644 --- a/model/report.go +++ b/model/report.go @@ -7,25 +7,27 @@ import ( ) 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"` + 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"` + AuthorizedLossRate float64 `json:"authorized_loss_rate" db:"authorized_loss_rate"` + AuthorizedLossRateIncrement float64 `json:"authorized_loss_rate_increment" db:"authorized_loss_rate_increment"` } type ReportSummary struct { diff --git a/service/calculate/summary.go b/service/calculate/summary.go index 50bd708..85f22ea 100644 --- a/service/calculate/summary.go +++ b/service/calculate/summary.go @@ -1,7 +1,10 @@ package calculate import ( + "electricity_bill_calc/model" "electricity_bill_calc/model/calculate" + "errors" + "fmt" "github.com/shopspring/decimal" ) @@ -37,3 +40,50 @@ func removeDuplicates(meters []calculate.Meter) []calculate.Meter { } return result } + +//计算线损以及调整线损 +func LossCalculate(report *model.ReportIndex, Public []calculate.Meter, + publicTotal decimal.Decimal, summary calculate.Summary) error { + summary.Loss = summary.Overall.Amount.Sub(summary.TotalConsumption) + + var summaryAmount decimal.Decimal + if summary.Overall.Amount == decimal.Zero { + summaryAmount = decimal.NewFromFloat(1.0) + } else { + summaryAmount = summary.Overall.Amount + } + + summary.LossProportion = summary.Loss.Div(summaryAmount) + + var authorizedLossRate decimal.Decimal + if summary.LossProportion.InexactFloat64() > report.AuthorizedLossRate { + authorizedLossRate = summary.LossProportion + } else { + return errors.New(fmt.Sprintf("经过核算园区的线损率为:{%.8f}, 核定线损率为:{%.8f}", summary.LossProportion.InexactFloat64(),authorizedLossRate.InexactFloat64())) + } + + summary.AuthoizeLoss = model.ConsumptionUnit{ + Amount: decimal.NewFromFloat(summary.Overall.Amount.InexactFloat64() * authorizedLossRate.InexactFloat64()), + Fee: decimal.NewFromFloat((summary.Overall.Amount.InexactFloat64() * authorizedLossRate.InexactFloat64()) * summary.Overall.Price.InexactFloat64()), + Price: summary.Overall.Price, + Proportion: authorizedLossRate, + } + + differentialLoss := summary.LossDilutedPrice.Sub(summary.AuthoizeLoss.Amount) + + if publicTotal.InexactFloat64() <= decimal.Zero.InexactFloat64() { + return errors.New("园区公共表计的电量总和为非正值,或者园区未设置公共表计,无法计算核定线损") + } + + for _, meter := range Public { + amountProportion := meter.Overall.Amount.InexactFloat64() / publicTotal.InexactFloat64() + adjustAmount := differentialLoss.InexactFloat64() * decimal.NewFromFloat(-1.0).InexactFloat64() + meter.AdjustLoss = model.ConsumptionUnit{ + Amount: decimal.NewFromFloat(adjustAmount), + Fee: decimal.NewFromFloat(adjustAmount * summary.LossDilutedPrice.InexactFloat64()), + Price: summary.LossDilutedPrice, + Proportion: decimal.NewFromFloat(amountProportion), + } + } + return nil +} diff --git a/service/calculate/wattCost.go b/service/calculate/wattCost.go index f9556f1..f7ff373 100644 --- a/service/calculate/wattCost.go +++ b/service/calculate/wattCost.go @@ -65,6 +65,12 @@ func MainCalculateProcess(rid string) { //计算所有表计的总电量 parkTotal := TotalConsumptionCalculate(tenementReports, summary) + err = LossCalculate(report, parkMetersReports, parkTotal, summary) + if err != nil { + fmt.Println("9", err) + return + } + fmt.Println(meterRelations, poolingMetersReports, parkMetersReports, parkTotal) } From 5710a640e8bd0dd149134d96bd0e97401ae8d24c Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Fri, 4 Aug 2023 10:05:27 +0800 Subject: [PATCH 23/27] =?UTF-8?q?[=E8=AE=A1=E7=AE=97=E7=9B=B8=E5=85=B3]fix?= =?UTF-8?q?=20=E8=AE=A1=E7=AE=97=E7=BA=BF=E6=8D=9F=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=BA=BF=E6=8D=9F=E5=8F=82=E6=95=B0=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E3=80=82new=20=E8=AE=A1=E7=AE=97=E6=89=80=E6=9C=89?= =?UTF-8?q?=E5=B7=B2=E7=BB=8F=E5=90=AF=E7=94=A8=E7=9A=84=E5=95=86=E9=93=BA?= =?UTF-8?q?=E9=9D=A2=E7=A7=AF=E6=80=BB=E5=92=8C=EF=BC=8C=E4=BB=85=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E6=89=80=E6=9C=89=E6=9C=AA=E8=BF=81=E5=87=BA=E7=9A=84?= =?UTF-8?q?=E5=95=86=E6=88=B7=E7=9A=84=E6=89=80=E6=9C=89=E8=A1=A8=E8=AE=A1?= =?UTF-8?q?=E5=AF=B9=E5=BA=94=E7=9A=84=E5=95=86=E9=93=BA=E9=9D=A2=E7=A7=AF?= =?UTF-8?q?=E3=80=82(=E5=AE=8C=E6=88=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- service/calculate/summary.go | 32 ++++++++++++++++++++++++++++---- service/calculate/wattCost.go | 9 ++++++++- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/service/calculate/summary.go b/service/calculate/summary.go index 85f22ea..26d5620 100644 --- a/service/calculate/summary.go +++ b/service/calculate/summary.go @@ -42,8 +42,8 @@ func removeDuplicates(meters []calculate.Meter) []calculate.Meter { } //计算线损以及调整线损 -func LossCalculate(report *model.ReportIndex, Public []calculate.Meter, - publicTotal decimal.Decimal, summary calculate.Summary) error { +func LossCalculate(report *model.ReportIndex, Public *[]calculate.Meter, + publicTotal *decimal.Decimal, summary *calculate.Summary) error { summary.Loss = summary.Overall.Amount.Sub(summary.TotalConsumption) var summaryAmount decimal.Decimal @@ -56,10 +56,11 @@ func LossCalculate(report *model.ReportIndex, Public []calculate.Meter, summary.LossProportion = summary.Loss.Div(summaryAmount) var authorizedLossRate decimal.Decimal + //TODO: 2023.08.04 在此发现reportIndex结构体与数据库中的report表字段不对应缺少两个相应字段,在此添加的,如在其他地方有错误优先查找这里 if summary.LossProportion.InexactFloat64() > report.AuthorizedLossRate { authorizedLossRate = summary.LossProportion } else { - return errors.New(fmt.Sprintf("经过核算园区的线损率为:{%.8f}, 核定线损率为:{%.8f}", summary.LossProportion.InexactFloat64(),authorizedLossRate.InexactFloat64())) + return errors.New(fmt.Sprintf("经过核算园区的线损率为:{%.8f}, 核定线损率为:{%.8f}", summary.LossProportion.InexactFloat64(), authorizedLossRate.InexactFloat64())) } summary.AuthoizeLoss = model.ConsumptionUnit{ @@ -75,7 +76,7 @@ func LossCalculate(report *model.ReportIndex, Public []calculate.Meter, return errors.New("园区公共表计的电量总和为非正值,或者园区未设置公共表计,无法计算核定线损") } - for _, meter := range Public { + for _, meter := range *Public { amountProportion := meter.Overall.Amount.InexactFloat64() / publicTotal.InexactFloat64() adjustAmount := differentialLoss.InexactFloat64() * decimal.NewFromFloat(-1.0).InexactFloat64() meter.AdjustLoss = model.ConsumptionUnit{ @@ -87,3 +88,26 @@ func LossCalculate(report *model.ReportIndex, Public []calculate.Meter, } return nil } + +// 计算已经启用的商铺面积和 +func EnabledAreaCalculate(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 errors.New("summary is nil") + } + return nil +} diff --git a/service/calculate/wattCost.go b/service/calculate/wattCost.go index f7ff373..6fe261b 100644 --- a/service/calculate/wattCost.go +++ b/service/calculate/wattCost.go @@ -65,12 +65,19 @@ func MainCalculateProcess(rid string) { //计算所有表计的总电量 parkTotal := TotalConsumptionCalculate(tenementReports, summary) - err = LossCalculate(report, parkMetersReports, parkTotal, summary) + // 计算线损以及调整线损 + err = LossCalculate(report, &parkMetersReports, &parkTotal, &summary) if err != nil { fmt.Println("9", err) return } + err = EnabledAreaCalculate(&tenementReports, &summary) + if err != nil{ + fmt.Println("10",err) + return + } + fmt.Println(meterRelations, poolingMetersReports, parkMetersReports, parkTotal) } From c916301f6bb7d6ffe0b002b064ce7c2c027bb7d0 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Fri, 4 Aug 2023 10:24:09 +0800 Subject: [PATCH 24/27] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- service/calculate/wattCost.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/service/calculate/wattCost.go b/service/calculate/wattCost.go index 6fe261b..fe14ba6 100644 --- a/service/calculate/wattCost.go +++ b/service/calculate/wattCost.go @@ -48,7 +48,7 @@ func MainCalculateProcess(rid string) { return } - //取得所有公摊表计的读数,以及公摊表计对应的分摊表计 + // 取得所有公摊表计的读数,以及公摊表计对应的分摊表计 poolingMetersReports, err := PooledMetersCalculate(report, periodStart.Time, periodEnd.Time, meterDetails, summary) if err != nil { fmt.Println("7", err) @@ -62,7 +62,7 @@ func MainCalculateProcess(rid string) { return } - //计算所有表计的总电量 + // 计算所有表计的总电量 parkTotal := TotalConsumptionCalculate(tenementReports, summary) // 计算线损以及调整线损 @@ -72,6 +72,7 @@ func MainCalculateProcess(rid string) { return } + // 计算所有已经启用的商铺面积总和,仅计算所有未迁出的商户的所有表计对应的商铺面积。 err = EnabledAreaCalculate(&tenementReports, &summary) if err != nil{ fmt.Println("10",err) From ce4c483bcb810cb38d90f01120759f43f0a50bcb Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Fri, 4 Aug 2023 11:09:04 +0800 Subject: [PATCH 25/27] =?UTF-8?q?[=E8=AE=A1=E7=AE=97=E7=9B=B8=E5=85=B3]?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E5=B7=B2=E7=BB=8F=E5=90=AF=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E5=95=86=E9=93=BA=E9=9D=A2=E7=A7=AF=E5=92=8C=EF=BC=88=E5=AE=8C?= =?UTF-8?q?=E6=88=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- service/calculate/meter.go | 10 ++++++++++ service/calculate/summary.go | 23 ++++++++++++++++++++++- service/calculate/tenement.go | 28 ++++++++++++++++++++++++++++ service/calculate/wattCost.go | 9 +++++++-- 4 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 service/calculate/meter.go diff --git a/service/calculate/meter.go b/service/calculate/meter.go new file mode 100644 index 0000000..b30cb72 --- /dev/null +++ b/service/calculate/meter.go @@ -0,0 +1,10 @@ +package calculate + +import "electricity_bill_calc/model/calculate" + +// / 合并所有的表计 +type Key struct { + Code string + TenementID string +} +type MeterMap map[Key]calculate.Meter diff --git a/service/calculate/summary.go b/service/calculate/summary.go index 26d5620..13ba004 100644 --- a/service/calculate/summary.go +++ b/service/calculate/summary.go @@ -90,7 +90,8 @@ func LossCalculate(report *model.ReportIndex, Public *[]calculate.Meter, } // 计算已经启用的商铺面积和 -func EnabledAreaCalculate(tenements *[]calculate.PrimaryTenementStatistics, summary *calculate.Summary) error { +func EnabledAreaCalculate(tenements *[]calculate.PrimaryTenementStatistics, + summary *calculate.Summary) error { var areaMeters []calculate.Meter for _, t := range *tenements { areaMeters = append(areaMeters, t.Meters...) @@ -111,3 +112,23 @@ func EnabledAreaCalculate(tenements *[]calculate.PrimaryTenementStatistics, summ } return nil } + +// 计算基本电费分摊、调整电费分摊以及电费摊薄单价。 +func PricesCalculate(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 +} \ No newline at end of file diff --git a/service/calculate/tenement.go b/service/calculate/tenement.go index 1749140..fd13741 100644 --- a/service/calculate/tenement.go +++ b/service/calculate/tenement.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "github.com/shopspring/decimal" + "sort" "strings" "time" "unsafe" @@ -266,3 +267,30 @@ func ShiftToAsiaShanghai(t time.Time) time.Time { location, _ := time.LoadLocation("Asia/Shanghai") return t.In(location) } + +// 计算各个商户的合计信息,并归总与商户关联的表计记录 +func TenementChargeCalculate(tenements []calculate.PrimaryTenementStatistics, + summary calculate.Summary, meters MeterMap, _relations []model.MeterRelation) { + result := make(map[string][]string) + for _, t := range tenements { + meterCodes := make([]string, 0) + for _, m := range t.Meters { + meterCodes = append(meterCodes, m.Code) + } + sort.Strings(meterCodes) + result[t.Tenement.Id] = meterCodes + } + var Key Key + for tCode, meterCodes := range result { + relatedMeters := make([]calculate.Meter, 0) + for _, code := range meterCodes { + Key.Code = code + "_" + tCode + meter, ok := meters[Key] + if ok { + relatedMeters = append(relatedMeters, meter) + } + } + // 计算商户的合计电费信息 + + } +} diff --git a/service/calculate/wattCost.go b/service/calculate/wattCost.go index fe14ba6..735c5f6 100644 --- a/service/calculate/wattCost.go +++ b/service/calculate/wattCost.go @@ -74,8 +74,13 @@ func MainCalculateProcess(rid string) { // 计算所有已经启用的商铺面积总和,仅计算所有未迁出的商户的所有表计对应的商铺面积。 err = EnabledAreaCalculate(&tenementReports, &summary) - if err != nil{ - fmt.Println("10",err) + if err != nil { + fmt.Println("10", err) + return + } + err = PricesCalculate(&summary) + if err != nil { + fmt.Println("11", err) return } From af359f4429b7b9ff99681b531d5f94dfa4202514 Mon Sep 17 00:00:00 2001 From: ZiHangQin <1420014281@qq.com> Date: Fri, 4 Aug 2023 14:38:03 +0800 Subject: [PATCH 26/27] =?UTF-8?q?enhance(calculate):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=96=B9=E6=B3=95=EF=BC=9A=E8=AE=A1=E7=AE=97=E5=95=86=E6=88=B7?= =?UTF-8?q?=E7=9A=84=E5=90=88=E8=AE=A1=E7=94=B5=E8=B4=B9=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E5=BD=92=E6=80=BB=E4=B8=8E=E5=95=86=E6=88=B7?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=85=B3=E8=81=94=E7=9A=84=E8=A1=A8=E8=AE=A1?= =?UTF-8?q?=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- service/calculate/tenement.go | 164 +++++++++++++++++++++++++++++++++- service/calculate/wattCost.go | 8 +- 2 files changed, 170 insertions(+), 2 deletions(-) diff --git a/service/calculate/tenement.go b/service/calculate/tenement.go index fd13741..9bc9b74 100644 --- a/service/calculate/tenement.go +++ b/service/calculate/tenement.go @@ -270,7 +270,7 @@ func ShiftToAsiaShanghai(t time.Time) time.Time { // 计算各个商户的合计信息,并归总与商户关联的表计记录 func TenementChargeCalculate(tenements []calculate.PrimaryTenementStatistics, - summary calculate.Summary, meters MeterMap, _relations []model.MeterRelation) { + summary calculate.Summary, meters MeterMap) []calculate.TenementCharge { result := make(map[string][]string) for _, t := range tenements { meterCodes := make([]string, 0) @@ -281,6 +281,7 @@ func TenementChargeCalculate(tenements []calculate.PrimaryTenementStatistics, result[t.Tenement.Id] = meterCodes } var Key Key + var tc []calculate.TenementCharge for tCode, meterCodes := range result { relatedMeters := make([]calculate.Meter, 0) for _, code := range meterCodes { @@ -291,6 +292,167 @@ func TenementChargeCalculate(tenements []calculate.PrimaryTenementStatistics, } } // 计算商户的合计电费信息 + var overall model.ConsumptionUnit + var critical model.ConsumptionUnit + var peak model.ConsumptionUnit + var flat model.ConsumptionUnit + var valley model.ConsumptionUnit + var basicPooled decimal.Decimal + var adjustPooled decimal.Decimal + var lossAmount decimal.Decimal + var lossPooled decimal.Decimal + var publicPooled decimal.Decimal + + for _, meter := range relatedMeters { + overall.Amount.Add(meter.Overall.Amount) + overall.Fee.Add(meter.Overall.Fee) + + critical.Amount.Add(meter.Critical.Amount) + critical.Fee.Add(meter.Critical.Fee) + + peak.Amount.Add(meter.Peak.Amount) + peak.Fee.Add(meter.Peak.Fee) + + flat.Amount.Add(meter.Flat.Amount) + flat.Fee.Add(meter.Flat.Fee) + + valley.Amount.Add(meter.Valley.Amount) + valley.Fee.Add(meter.Valley.Fee) + + basicPooled.Add(meter.PooledBasic.Fee) + adjustPooled.Add(meter.PooledAdjust.Fee) + lossAmount.Add(meter.PooledLoss.Amount) + lossPooled.Add(meter.PooledLoss.Fee) + publicPooled.Add(meter.PooledPublic.Fee) + + // 反写商户表计的统计数据 + meter.Overall.Proportion = func() decimal.Decimal { + if overall.Amount.Equal(decimal.Zero) { + return decimal.Zero + } + return meter.Overall.Amount.Div(overall.Amount) + }() + meter.Critical.Proportion = func() decimal.Decimal { + if critical.Amount.Equal(decimal.Zero) { + return decimal.Zero + } + return meter.Critical.Amount.Div(critical.Amount) + }() + meter.Peak.Proportion = func() decimal.Decimal { + if peak.Amount.Equal(decimal.Zero) { + return decimal.Zero + } + return meter.Peak.Amount.Div(peak.Amount) + }() + meter.Flat.Proportion = func() decimal.Decimal { + if flat.Amount.Equal(decimal.Zero) { + return decimal.Zero + } + return meter.Flat.Amount.Div(flat.Amount) + }() + meter.Valley.Proportion = func() decimal.Decimal { + if valley.Amount.Equal(decimal.Zero) { + return decimal.Zero + } + return meter.Valley.Amount.Div(valley.Amount) + }() + meter.PooledBasic.Proportion = func() decimal.Decimal { + if basicPooled.Equal(decimal.Zero) { + return decimal.Zero + } + return meter.PooledBasic.Fee.Div(basicPooled) + }() + meter.PooledAdjust.Proportion = func() decimal.Decimal { + if adjustPooled.Equal(decimal.Zero) { + return decimal.Zero + } + return meter.PooledAdjust.Fee.Div(adjustPooled) + }() + meter.PooledLoss.Proportion = func() decimal.Decimal { + if lossPooled.Equal(decimal.Zero) { + return decimal.Zero + } + return meter.PooledLoss.Fee.Div(lossPooled) + }() + meter.PooledPublic.Proportion = func() decimal.Decimal { + if publicPooled.Equal(decimal.Zero) { + return decimal.Zero + } + return meter.PooledPublic.Fee.Div(publicPooled) + }() + + var OverallProportion decimal.Decimal + if summary.Overall.Amount == decimal.Zero { + OverallProportion = decimal.Zero + } else { + OverallProportion = decimal.NewFromFloat(overall.Amount.InexactFloat64() / summary.Overall.Amount.InexactFloat64()) + } + + var CriticalProportion decimal.Decimal + if summary.Critical.Amount == decimal.Zero { + CriticalProportion = decimal.Zero + } else { + CriticalProportion = decimal.NewFromFloat(critical.Amount.InexactFloat64() / summary.Critical.Amount.InexactFloat64()) + } + + var PeakProportion decimal.Decimal + if summary.Peak.Amount == decimal.Zero { + PeakProportion = decimal.Zero + } else { + PeakProportion = decimal.NewFromFloat(peak.Amount.InexactFloat64() / summary.Peak.Amount.InexactFloat64()) + } + + var FlatProportion decimal.Decimal + if summary.Flat.Amount == decimal.Zero { + FlatProportion = decimal.Zero + } else { + FlatProportion = decimal.NewFromFloat(flat.Amount.InexactFloat64() / summary.Flat.Amount.InexactFloat64()) + } + + var ValleyProportion decimal.Decimal + if summary.Valley.Amount == decimal.Zero { + ValleyProportion = decimal.Zero + } else { + ValleyProportion = decimal.NewFromFloat(valley.Amount.InexactFloat64() / summary.Valley.Amount.InexactFloat64()) + } + + tenementCharge := calculate.TenementCharge{ + Tenement: tCode, + Overall: model.ConsumptionUnit{ + Price: summary.Overall.Price, + Proportion: OverallProportion, + }, + Critical: model.ConsumptionUnit{ + Price: summary.Critical.Price, + Proportion: CriticalProportion, + }, + Peak: model.ConsumptionUnit{ + Price: summary.Overall.Price, + Proportion: PeakProportion, + }, + Flat: model.ConsumptionUnit{ + Price: summary.Overall.Price, + Proportion: FlatProportion, + }, + Valley: model.ConsumptionUnit{ + Price: summary.Overall.Price, + Proportion: ValleyProportion, + }, + BasicFee: basicPooled, + AdjustFee: adjustPooled, + LossPooled: lossPooled, + PublicPooled: publicPooled, + FinalCharges: decimal.NewFromFloat( + overall.Fee.InexactFloat64() + basicPooled.InexactFloat64() + + adjustPooled.InexactFloat64() + lossPooled.InexactFloat64() + + publicPooled.InexactFloat64()), + Submeters: nil, + Poolings: nil, + } + + tc = append(tc, tenementCharge) + } } + return tc } diff --git a/service/calculate/wattCost.go b/service/calculate/wattCost.go index 735c5f6..113eaab 100644 --- a/service/calculate/wattCost.go +++ b/service/calculate/wattCost.go @@ -84,6 +84,12 @@ func MainCalculateProcess(rid string) { return } - fmt.Println(meterRelations, poolingMetersReports, parkMetersReports, parkTotal) + //为获取值初始化一个空的,合并分支时可忽略 + var meters MeterMap + + // 计算商户的合计电费信息,并归总与商户相关联的表计记录 + tenementCharges := TenementChargeCalculate(tenementReports, summary, meters) + + fmt.Println(meterRelations, poolingMetersReports, tenementCharges) } From 020e76b901702457bb486facf197a928e123a61a Mon Sep 17 00:00:00 2001 From: DEKA_123 <1904876928@qq.com> Date: Fri, 4 Aug 2023 17:11:10 +0800 Subject: [PATCH 27/27] =?UTF-8?q?=E5=90=88=E5=B9=B6=E5=88=86=E6=94=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 +- _1.gitignore | 183 ++++++ cache/abstract.go | 40 +- cache/count.go | 10 +- cache/entity.go | 12 +- cache/exists.go | 6 +- cache/relation.go | 12 +- cache/search.go | 62 +- cache/session.go | 4 +- config/settings.go | 3 +- controller/abstract.go | 27 + controller/charge.go | 126 ++-- controller/invoice.go | 227 ++++++++ controller/meter.go | 502 ++++++++++++++++ controller/park.go | 404 ++++++++----- controller/region.go | 16 +- controller/report.go | 555 +++++++++++------- controller/sync.go | 121 ++++ controller/tenement.go | 286 +++++++++ controller/top_up.go | 142 +++++ controller/user.go | 502 ++++++++-------- controller/withdraw.go | 78 +-- excel/abstract.go | 67 ++- excel/meter_archive.go | 140 ++++- excel/meter_reading.go | 131 +++++ excel/tenement.go | 27 + exceptions/auth.go | 2 +- exceptions/illegal_arguments.go | 2 +- exceptions/improper_operate.go | 2 +- exceptions/insufficient_data.go | 16 + exceptions/not_found.go | 2 +- exceptions/unsuccessful.go | 129 ++++- global/db.go | 112 ++-- go.mod | 74 ++- go.sum | 164 ++++++ logger/logger.go | 54 +- logger/middleware.go | 8 +- logger/rolling.go | 12 +- main.go | 152 ++--- model/calculate/calculate.go | 92 +++ model/charge.go | 32 ++ model/cunsumption.go | 10 + model/enums.go | 90 +++ model/invoice.go | 45 ++ model/meter.go | 106 ++++ model/park.go | 88 +-- model/park_building.go | 14 + model/reading.go | 56 ++ model/region.go | 11 +- model/report.go | 192 ++++--- model/session.go | 2 +- model/synchronize.go | 38 ++ model/tenement.go | 23 + model/top_up.go | 33 ++ model/user.go | 130 +++-- repository/calculate.go | 70 +++ repository/charge.go | 152 +++++ repository/god.go | 19 + repository/invoice.go | 348 +++++++++++ repository/meter.go | 991 ++++++++++++++++++++++++++++++++ repository/park.go | 419 ++++++++++++++ repository/region.go | 79 +++ repository/report.go | 846 +++++++++++++++++++++++++++ repository/synchronize.go | 208 +++++++ repository/tenement.go | 458 +++++++++++++++ repository/top_up.go | 166 ++++++ repository/user.go | 419 ++++++++++++++ response/base_response.go | 20 +- router/router.go | 22 +- security/security.go | 8 +- service/calculate/meters.go | 455 +++++++++++++++ service/calculate/mod.go | 72 +++ service/calculate/persist.go | 75 +++ service/calculate/summary.go | 55 ++ service/calculate/tenement.go | 221 +++++++ service/charge.go | 351 +++-------- service/invoice.go | 180 ++++++ service/meter.go | 779 +++++++++++++++++++++++++ service/report.go | 856 +++++---------------------- service/synchronize.go | 51 ++ service/tenement.go | 241 ++++++++ service/user.go | 489 ++++------------ settings.yaml | 3 +- tools/serial/algorithm.go | 84 +++ tools/utils.go | 93 +++ types/date.go | 175 ++++++ types/daterange.go | 141 +++++ types/datetime.go | 189 ++++++ types/datetimerange.go | 119 ++++ types/range.go | 119 ++++ vo/invoice.go | 39 ++ vo/meter.go | 64 +++ vo/park.go | 96 ++++ vo/reading.go | 80 +++ vo/report.go | 323 +++++++++++ vo/shares.go | 43 ++ vo/synchronize.go | 29 + vo/tenement.go | 75 +++ vo/top_up.go | 25 + vo/user.go | 141 +++++ 100 files changed, 12692 insertions(+), 2574 deletions(-) create mode 100644 _1.gitignore create mode 100644 controller/invoice.go create mode 100644 controller/meter.go create mode 100644 controller/sync.go create mode 100644 controller/tenement.go create mode 100644 controller/top_up.go create mode 100644 excel/meter_reading.go create mode 100644 excel/tenement.go create mode 100644 exceptions/insufficient_data.go create mode 100644 model/calculate/calculate.go create mode 100644 model/charge.go create mode 100644 model/cunsumption.go create mode 100644 model/enums.go create mode 100644 model/invoice.go create mode 100644 model/meter.go create mode 100644 model/park_building.go create mode 100644 model/reading.go create mode 100644 model/synchronize.go create mode 100644 model/tenement.go create mode 100644 model/top_up.go create mode 100644 repository/calculate.go create mode 100644 repository/charge.go create mode 100644 repository/god.go create mode 100644 repository/invoice.go create mode 100644 repository/meter.go create mode 100644 repository/park.go create mode 100644 repository/region.go create mode 100644 repository/report.go create mode 100644 repository/synchronize.go create mode 100644 repository/tenement.go create mode 100644 repository/top_up.go create mode 100644 repository/user.go create mode 100644 service/calculate/meters.go create mode 100644 service/calculate/mod.go create mode 100644 service/calculate/persist.go create mode 100644 service/calculate/summary.go create mode 100644 service/calculate/tenement.go create mode 100644 service/invoice.go create mode 100644 service/meter.go create mode 100644 service/synchronize.go create mode 100644 service/tenement.go create mode 100644 tools/serial/algorithm.go create mode 100644 types/date.go create mode 100644 types/daterange.go create mode 100644 types/datetime.go create mode 100644 types/datetimerange.go create mode 100644 types/range.go create mode 100644 vo/invoice.go create mode 100644 vo/meter.go create mode 100644 vo/park.go create mode 100644 vo/reading.go create mode 100644 vo/report.go create mode 100644 vo/shares.go create mode 100644 vo/synchronize.go create mode 100644 vo/tenement.go create mode 100644 vo/top_up.go create mode 100644 vo/user.go diff --git a/Dockerfile b/Dockerfile index b4df38b..062c966 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ diff --git a/_1.gitignore b/_1.gitignore new file mode 100644 index 0000000..45e75ee --- /dev/null +++ b/_1.gitignore @@ -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 diff --git a/cache/abstract.go b/cache/abstract.go index cedab1f..63463c4 100644 --- a/cache/abstract.go +++ b/cache/abstract.go @@ -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 +} diff --git a/cache/count.go b/cache/count.go index b1baa54..78844b9 100644 --- a/cache/count.go +++ b/cache/count.go @@ -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 { diff --git a/cache/entity.go b/cache/entity.go index c9d4eb8..203133b 100644 --- a/cache/entity.go +++ b/cache/entity.go @@ -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) } diff --git a/cache/exists.go b/cache/exists.go index cae7ad7..150c0c7 100644 --- a/cache/exists.go +++ b/cache/exists.go @@ -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) } diff --git a/cache/relation.go b/cache/relation.go index d707edb..5b89af3 100644 --- a/cache/relation.go +++ b/cache/relation.go @@ -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 ) diff --git a/cache/search.go b/cache/search.go index 7468bd4..42ffcc9 100644 --- a/cache/search.go +++ b/cache/search.go @@ -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) +} diff --git a/cache/session.go b/cache/session.go index 30000d7..2dab0d2 100644 --- a/cache/session.go +++ b/cache/session.go @@ -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) { diff --git a/config/settings.go b/config/settings.go index ccd4124..b8403ec 100644 --- a/config/settings.go +++ b/config/settings.go @@ -31,8 +31,9 @@ type RedisSetting struct { type ServiceSetting struct { MaxSessionLife time.Duration - ItemsPageSize int + ItemsPageSize uint CacheLifeTime time.Duration + HostSerial int64 } // 定义全局变量 diff --git a/controller/abstract.go b/controller/abstract.go index 8d34e1c..3cb2fd0 100644 --- a/controller/abstract.go +++ b/controller/abstract.go @@ -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 +} diff --git a/controller/charge.go b/controller/charge.go index a2b8d43..ce5282c 100644 --- a/controller/charge.go +++ b/controller/charge.go @@ -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("取消用户充值记录成功。") + } } diff --git a/controller/invoice.go b/controller/invoice.go new file mode 100644 index 0000000..4e88146 --- /dev/null +++ b/controller/invoice.go @@ -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("已经删除了指定的发票记录。") +} diff --git a/controller/meter.go b/controller/meter.go new file mode 100644 index 0000000..eb34fcd --- /dev/null +++ b/controller/meter.go @@ -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}) +} diff --git a/controller/park.go b/controller/park.go index b7973a0..861ba19 100644 --- a/controller/park.go +++ b/controller/park.go @@ -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("已删除指定的建筑") } diff --git a/controller/region.go b/controller/region.go index f509c54..9e4af8c 100644 --- a/controller/region.go +++ b/controller/region.go @@ -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()) } diff --git a/controller/report.go b/controller/report.go index 6cf63c0..a99cabe 100644 --- a/controller/report.go +++ b/controller/report.go @@ -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}, + ) } diff --git a/controller/sync.go b/controller/sync.go new file mode 100644 index 0000000..dd95206 --- /dev/null +++ b/controller/sync.go @@ -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("更新完成。") +} diff --git a/controller/tenement.go b/controller/tenement.go new file mode 100644 index 0000000..04dab4c --- /dev/null +++ b/controller/tenement.go @@ -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, + }, + ) +} diff --git a/controller/top_up.go b/controller/top_up.go new file mode 100644 index 0000000..60bd017 --- /dev/null +++ b/controller/top_up.go @@ -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( + "已经删除一条指定的商户充值记录", + ) +} diff --git a/controller/user.go b/controller/user.go index 0689cfc..c79385e 100644 --- a/controller/user.go +++ b/controller/user.go @@ -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}) +} diff --git a/controller/withdraw.go b/controller/withdraw.go index 4021a93..46bbcd0 100644 --- a/controller/withdraw.go +++ b/controller/withdraw.go @@ -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("指定公示报表的撤回申请已经完成审核") } diff --git a/excel/abstract.go b/excel/abstract.go index 7d5c27a..932a760 100644 --- a/excel/abstract.go +++ b/excel/abstract.go @@ -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)) + } + } } } } diff --git a/excel/meter_archive.go b/excel/meter_archive.go index a8d8cd6..cc99647 100644 --- a/excel/meter_archive.go +++ b/excel/meter_archive.go @@ -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 } diff --git a/excel/meter_reading.go b/excel/meter_reading.go new file mode 100644 index 0000000..6ad33cc --- /dev/null +++ b/excel/meter_reading.go @@ -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 +} diff --git a/excel/tenement.go b/excel/tenement.go new file mode 100644 index 0000000..107857d --- /dev/null +++ b/excel/tenement.go @@ -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) +//} diff --git a/exceptions/auth.go b/exceptions/auth.go index e452ceb..af0ed46 100644 --- a/exceptions/auth.go +++ b/exceptions/auth.go @@ -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) } diff --git a/exceptions/illegal_arguments.go b/exceptions/illegal_arguments.go index 8760a4d..e6871ae 100644 --- a/exceptions/illegal_arguments.go +++ b/exceptions/illegal_arguments.go @@ -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) } diff --git a/exceptions/improper_operate.go b/exceptions/improper_operate.go index 4ac7626..f2fba06 100644 --- a/exceptions/improper_operate.go +++ b/exceptions/improper_operate.go @@ -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) } diff --git a/exceptions/insufficient_data.go b/exceptions/insufficient_data.go new file mode 100644 index 0000000..45a2047 --- /dev/null +++ b/exceptions/insufficient_data.go @@ -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) +} diff --git a/exceptions/not_found.go b/exceptions/not_found.go index 14065ba..4bc8ae1 100644 --- a/exceptions/not_found.go +++ b/exceptions/not_found.go @@ -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 { diff --git a/exceptions/unsuccessful.go b/exceptions/unsuccessful.go index 22a9da9..f2623a0 100644 --- a/exceptions/unsuccessful.go +++ b/exceptions/unsuccessful.go @@ -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() } diff --git a/global/db.go b/global/db.go index 17d01a3..1acdc74 100644 --- a/global/db.go +++ b/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)) + } +} diff --git a/go.mod b/go.mod index 9abd945..9f93995 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 846c8ed..62deb92 100644 --- a/go.sum +++ b/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= diff --git a/logger/logger.go b/logger/logger.go index de811e6..5ab4c6e 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -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) +} diff --git a/logger/middleware.go b/logger/middleware.go index 69cff6f..d826810 100644 --- a/logger/middleware.go +++ b/logger/middleware.go @@ -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 { diff --git a/logger/rolling.go b/logger/rolling.go index be8ff61..960dd4c 100644 --- a/logger/rolling.go +++ b/logger/rolling.go @@ -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 } } diff --git a/main.go b/main.go index cb7965f..ebe27c6 100644 --- a/main.go +++ b/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)) diff --git a/model/calculate/calculate.go b/model/calculate/calculate.go new file mode 100644 index 0000000..da5177c --- /dev/null +++ b/model/calculate/calculate.go @@ -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 +} diff --git a/model/charge.go b/model/charge.go new file mode 100644 index 0000000..f0d1c18 --- /dev/null +++ b/model/charge.go @@ -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"` +} diff --git a/model/cunsumption.go b/model/cunsumption.go new file mode 100644 index 0000000..4f853d9 --- /dev/null +++ b/model/cunsumption.go @@ -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"` +} diff --git a/model/enums.go b/model/enums.go new file mode 100644 index 0000000..79b5225 --- /dev/null +++ b/model/enums.go @@ -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 +) diff --git a/model/invoice.go b/model/invoice.go new file mode 100644 index 0000000..827bfac --- /dev/null +++ b/model/invoice.go @@ -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, "") +} diff --git a/model/meter.go b/model/meter.go new file mode 100644 index 0000000..857280c --- /dev/null +++ b/model/meter.go @@ -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"` +} diff --git a/model/park.go b/model/park.go index 326ee6d..9dfd190 100644 --- a/model/park.go +++ b/model/park.go @@ -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"` } diff --git a/model/park_building.go b/model/park_building.go new file mode 100644 index 0000000..339c9ed --- /dev/null +++ b/model/park_building.go @@ -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"` +} diff --git a/model/reading.go b/model/reading.go new file mode 100644 index 0000000..8f28391 --- /dev/null +++ b/model/reading.go @@ -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"` +} diff --git a/model/region.go b/model/region.go index 8fecddf..76315bd 100644 --- a/model/region.go +++ b/model/region.go @@ -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"` } diff --git a/model/report.go b/model/report.go index 0192b6d..2972647 100644 --- a/model/report.go +++ b/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"` } diff --git a/model/session.go b/model/session.go index 536d823..eade844 100644 --- a/model/session.go +++ b/model/session.go @@ -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"` } diff --git a/model/synchronize.go b/model/synchronize.go new file mode 100644 index 0000000..a42e5e9 --- /dev/null +++ b/model/synchronize.go @@ -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"` +} diff --git a/model/tenement.go b/model/tenement.go new file mode 100644 index 0000000..eda08f0 --- /dev/null +++ b/model/tenement.go @@ -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"` +} diff --git a/model/top_up.go b/model/top_up.go new file mode 100644 index 0000000..fe4c1c1 --- /dev/null +++ b/model/top_up.go @@ -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 + } +} diff --git a/model/user.go b/model/user.go index 3420fb7..7815c29 100644 --- a/model/user.go +++ b/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"` } diff --git a/repository/calculate.go b/repository/calculate.go new file mode 100644 index 0000000..440d9ef --- /dev/null +++ b/repository/calculate.go @@ -0,0 +1,70 @@ +package repository + +import ( + "electricity_bill_calc/global" + "electricity_bill_calc/logger" + "electricity_bill_calc/model" + "electricity_bill_calc/types" + + "github.com/doug-martin/goqu/v9" + _ "github.com/doug-martin/goqu/v9/dialect/postgres" + "github.com/georgysavva/scany/v2/pgxscan" + "go.uber.org/zap" +) + +type _CalculateRepository struct { + log *zap.Logger + ds goqu.DialectWrapper +} + +var CalculateRepository = _CalculateRepository{ + log: logger.Named("Repository", "Calculate"), + ds: goqu.Dialect("postgres"), +} + +// 获取当前正在等待计算的核算任务ID列表 +func (cr _CalculateRepository) ListPendingTasks() ([]string, error) { + cr.log.Info("获取当前正在等待计算的核算任务ID列表") + ctx, cancel := global.TimeoutContext() + defer cancel() + + var ids []string + querySql, queryArgs, _ := cr.ds. + From("report_task"). + Select("id"). + Where(goqu.C("status").Eq(model.REPORT_CALCULATE_TASK_STATUS_PENDING)). + Prepared(true).ToSQL() + if err := pgxscan.Select(ctx, global.DB, &ids, querySql, queryArgs...); err != nil { + cr.log.Error("未能获取到当前正在等待计算的核算任务ID列表", zap.Error(err)) + return nil, err + } + return ids, nil +} + +// 更新指定报表的核算状态 +func (cr _CalculateRepository) UpdateReportTaskStatus(rid string, status int16, message *string) (bool, error) { + cr.log.Info("更新指定报表的核算状态", zap.String("Report", rid), zap.Int16("Status", status)) + ctx, cancel := global.TimeoutContext() + defer cancel() + + currentTime := types.Now() + updateSql, updateArgs, _ := cr.ds. + Update("report_task"). + Set(goqu.Record{ + "status": status, + "last_modified_at": currentTime, + "message": message, + }). + Where(goqu.C("id").Eq(rid)). + Prepared(true).ToSQL() + res, err := global.DB.Exec(ctx, updateSql, updateArgs...) + if err != nil { + cr.log.Error("未能更新指定报表的核算状态", zap.Error(err)) + return false, err + } + if res.RowsAffected() == 0 { + cr.log.Warn("未能保存指定报表的核算状态", zap.String("Report", rid)) + return false, nil + } + return res.RowsAffected() > 0, nil +} diff --git a/repository/charge.go b/repository/charge.go new file mode 100644 index 0000000..3800217 --- /dev/null +++ b/repository/charge.go @@ -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 +} diff --git a/repository/god.go b/repository/god.go new file mode 100644 index 0000000..87855ff --- /dev/null +++ b/repository/god.go @@ -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"), +} + +// 删除指定园区中的表计和商户的绑定关系 diff --git a/repository/invoice.go b/repository/invoice.go new file mode 100644 index 0000000..6fc3885 --- /dev/null +++ b/repository/invoice.go @@ -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 +} diff --git a/repository/meter.go b/repository/meter.go new file mode 100644 index 0000000..e5c818d --- /dev/null +++ b/repository/meter.go @@ -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 +} diff --git a/repository/park.go b/repository/park.go new file mode 100644 index 0000000..91afbc4 --- /dev/null +++ b/repository/park.go @@ -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 +} diff --git a/repository/region.go b/repository/region.go new file mode 100644 index 0000000..e350b52 --- /dev/null +++ b/repository/region.go @@ -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 +} diff --git a/repository/report.go b/repository/report.go new file mode 100644 index 0000000..a881c97 --- /dev/null +++ b/repository/report.go @@ -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 +} diff --git a/repository/synchronize.go b/repository/synchronize.go new file mode 100644 index 0000000..c4b462d --- /dev/null +++ b/repository/synchronize.go @@ -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 +} diff --git a/repository/tenement.go b/repository/tenement.go new file mode 100644 index 0000000..beaffa7 --- /dev/null +++ b/repository/tenement.go @@ -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 +} diff --git a/repository/top_up.go b/repository/top_up.go new file mode 100644 index 0000000..9597936 --- /dev/null +++ b/repository/top_up.go @@ -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 +} diff --git a/repository/user.go b/repository/user.go new file mode 100644 index 0000000..ce14bd8 --- /dev/null +++ b/repository/user.go @@ -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 +} diff --git a/response/base_response.go b/response/base_response.go index c156f9f..54e1b9f 100644 --- a/response/base_response.go +++ b/response/base_response.go @@ -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格式响应 diff --git a/router/router.go b/router/router.go index 070b4de..fe8c98f 100644 --- a/router/router.go +++ b/router/router.go @@ -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 } diff --git a/security/security.go b/security/security.go index 151b097..38d5c7c 100644 --- a/security/security.go +++ b/security/security.go @@ -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) diff --git a/service/calculate/meters.go b/service/calculate/meters.go new file mode 100644 index 0000000..0b9bb1a --- /dev/null +++ b/service/calculate/meters.go @@ -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 +} diff --git a/service/calculate/mod.go b/service/calculate/mod.go new file mode 100644 index 0000000..f735e21 --- /dev/null +++ b/service/calculate/mod.go @@ -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) +} diff --git a/service/calculate/persist.go b/service/calculate/persist.go new file mode 100644 index 0000000..112a07f --- /dev/null +++ b/service/calculate/persist.go @@ -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 +} diff --git a/service/calculate/summary.go b/service/calculate/summary.go new file mode 100644 index 0000000..53385b2 --- /dev/null +++ b/service/calculate/summary.go @@ -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 +} diff --git a/service/calculate/tenement.go b/service/calculate/tenement.go new file mode 100644 index 0000000..436fcb2 --- /dev/null +++ b/service/calculate/tenement.go @@ -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 +} diff --git a/service/charge.go b/service/charge.go index d3fcead..5dfa2cc 100644 --- a/service/charge.go +++ b/service/charge.go @@ -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 } diff --git a/service/invoice.go b/service/invoice.go new file mode 100644 index 0000000..59b4716 --- /dev/null +++ b/service/invoice.go @@ -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 +} diff --git a/service/meter.go b/service/meter.go new file mode 100644 index 0000000..ff3be35 --- /dev/null +++ b/service/meter.go @@ -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 +} diff --git a/service/report.go b/service/report.go index da5043d..2380e83 100644 --- a/service/report.go +++ b/service/report.go @@ -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 } diff --git a/service/synchronize.go b/service/synchronize.go new file mode 100644 index 0000000..bd2a810 --- /dev/null +++ b/service/synchronize.go @@ -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 + +} diff --git a/service/tenement.go b/service/tenement.go new file mode 100644 index 0000000..ab56299 --- /dev/null +++ b/service/tenement.go @@ -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 +} diff --git a/service/user.go b/service/user.go index bc28c04..a81dc3f 100644 --- a/service/user.go +++ b/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 } diff --git a/settings.yaml b/settings.yaml index f926f74..4a0e548 100644 --- a/settings.yaml +++ b/settings.yaml @@ -19,4 +19,5 @@ Redis: Service: MaxSessionLife: 2h ItemsPageSize: 20 - CacheLifeTime: 5m \ No newline at end of file + CacheLifeTime: 5m + HostSerial: 5 diff --git a/tools/serial/algorithm.go b/tools/serial/algorithm.go new file mode 100644 index 0000000..91858e8 --- /dev/null +++ b/tools/serial/algorithm.go @@ -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 "" +} diff --git a/tools/utils.go b/tools/utils.go index 459f1dd..6312dd5 100644 --- a/tools/utils.go +++ b/tools/utils.go @@ -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])) +} diff --git a/types/date.go b/types/date.go new file mode 100644 index 0000000..ebfd690 --- /dev/null +++ b/types/date.go @@ -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()) +} diff --git a/types/daterange.go b/types/daterange.go new file mode 100644 index 0000000..b71b89c --- /dev/null +++ b/types/daterange.go @@ -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) +} diff --git a/types/datetime.go b/types/datetime.go new file mode 100644 index 0000000..7331040 --- /dev/null +++ b/types/datetime.go @@ -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()) +} diff --git a/types/datetimerange.go b/types/datetimerange.go new file mode 100644 index 0000000..971fcf9 --- /dev/null +++ b/types/datetimerange.go @@ -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) +} diff --git a/types/range.go b/types/range.go new file mode 100644 index 0000000..374db35 --- /dev/null +++ b/types/range.go @@ -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() +} diff --git a/vo/invoice.go b/vo/invoice.go new file mode 100644 index 0000000..79015e3 --- /dev/null +++ b/vo/invoice.go @@ -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"` +} diff --git a/vo/meter.go b/vo/meter.go new file mode 100644 index 0000000..eaedcbb --- /dev/null +++ b/vo/meter.go @@ -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"` +} diff --git a/vo/park.go b/vo/park.go new file mode 100644 index 0000000..209f066 --- /dev/null +++ b/vo/park.go @@ -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) +} diff --git a/vo/reading.go b/vo/reading.go new file mode 100644 index 0000000..1cbf30c --- /dev/null +++ b/vo/reading.go @@ -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, + } +} diff --git a/vo/report.go b/vo/report.go new file mode 100644 index 0000000..0615a57 --- /dev/null +++ b/vo/report.go @@ -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) + } +} diff --git a/vo/shares.go b/vo/shares.go new file mode 100644 index 0000000..19c7e16 --- /dev/null +++ b/vo/shares.go @@ -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 +} diff --git a/vo/synchronize.go b/vo/synchronize.go new file mode 100644 index 0000000..39edecb --- /dev/null +++ b/vo/synchronize.go @@ -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"` // 重试间隔,基础间隔时间,根据间隔算法不同会产生不同的间隔 +} diff --git a/vo/tenement.go b/vo/tenement.go new file mode 100644 index 0000000..1cc743f --- /dev/null +++ b/vo/tenement.go @@ -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"` +} diff --git a/vo/top_up.go b/vo/top_up.go new file mode 100644 index 0000000..5138de3 --- /dev/null +++ b/vo/top_up.go @@ -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"` +} diff --git a/vo/user.go b/vo/user.go new file mode 100644 index 0000000..c7d430a --- /dev/null +++ b/vo/user.go @@ -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, "") +}