diff --git a/controller/park.go b/controller/park.go new file mode 100644 index 0000000..e959cd2 --- /dev/null +++ b/controller/park.go @@ -0,0 +1,373 @@ +package controller + +import ( + "electricity_bill_calc/logger" + "electricity_bill_calc/repository" + "electricity_bill_calc/response" + "electricity_bill_calc/security" + "electricity_bill_calc/vo" + "net/http" + + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +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", 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 listParksBelongsToCurrentUser(c *fiber.Ctx) error { + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + parkLog.Error("列出当前用的全部园区,无法获取当前用户的会话。") + return result.Unauthorized(err.Error()) + } + parkLog.Info("列出当前用户下的全部园区", zap.String("user id", session.Uid)) + parks, err := repository.ParkRepository.RetrieveParkbelongs(session.Uid) + if err != nil { + parkLog.Error("无法获取园区列表。", zap.String("user id", session.Uid)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + return result.Success("已获取到指定用户的下的园区", fiber.Map{"parks": parks}) +} + +// 列出隶属于指定用户的全部园区 +func listParksBelongsTo(c *fiber.Ctx) error { + result := response.NewResult(c) + userId := c.Params("userId") + parkLog.Info("列出指定用户下的全部园区", zap.String("user id", userId)) + parks, err := repository.ParkRepository.RetrieveParkbelongs(userId) + if err != nil { + parkLog.Error("无法获取园区列表。", zap.String("user id", userId)) + return result.Error(http.StatusInternalServerError, err.Error()) + } + return result.Success("已获取到指定用户的下的园区", fiber.Map{"parks": parks}) +} + +// 获取指定园区的详细信息 +func fetchParkDetail(c *fiber.Ctx) error { + result := response.NewResult(c) + 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.Success("已获取到指定园区的详细信息", fiber.Map{"park": park}) +} + +// 创建一个新的园区 +func createPark(c *fiber.Ctx) error { + result := response.NewResult(c) + session, err := _retreiveSession(c) + if err != nil { + parkLog.Error("创建一个新的园区,无法获取当前用户的会话。") + return result.Unauthorized(err.Error()) + } + 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()) + } + 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.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()) + } + 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("您无权访问该园区。") + } + 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()) + } + 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("您无权访问该园区。") + } + 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) + 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("您无权访问该园区。") + } + 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("已删除指定的园区") +} + +// 列出指定园区中已经登记的建筑 +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()) + } + 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("您无权访问该园区。") + } + 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()) + } + 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("您无权访问该园区。") + } + 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()) + } + 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("您无权访问该园区。") + } + 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()) + } + 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("您无权访问该园区。") + } + 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/model/enums.go b/model/enums.go new file mode 100644 index 0000000..bdbb9c1 --- /dev/null +++ b/model/enums.go @@ -0,0 +1,51 @@ +package model + +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 +) + +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 +) diff --git a/model/park.go b/model/park.go new file mode 100644 index 0000000..c020948 --- /dev/null +++ b/model/park.go @@ -0,0 +1,32 @@ +package model + +import ( + "time" + + "github.com/shopspring/decimal" +) + +type Park struct { + Id string `json:"id"` + UserId string `json:"userId"` + Name string `json:"name"` + Area decimal.NullDecimal `json:"area"` + TenementQuantity decimal.NullDecimal `json:"tenementQuantity"` + Capacity decimal.NullDecimal `json:"capacity"` + Category int16 `json:"category"` + MeterType int16 `json:"meter04kvType"` + PricePolicy int16 `json:"pricePolicy"` + BasicPooled int16 `json:"basicPooled"` + AdjustPooled int16 `json:"adjustPooled"` + LossPooled int16 `json:"lossPooled"` + PublicPooled int16 `json:"publicPooled"` + TaxRate decimal.NullDecimal `json:"taxRate"` + Region *string `json:"region"` + Address *string `json:"address"` + Contact *string `json:"contact"` + Phone *string `json:"phone"` + 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/repository/park.go b/repository/park.go new file mode 100644 index 0000000..eb51cc1 --- /dev/null +++ b/repository/park.go @@ -0,0 +1,473 @@ +package repository + +import ( + "electricity_bill_calc/cache" + "electricity_bill_calc/global" + "electricity_bill_calc/logger" + "electricity_bill_calc/model" + "electricity_bill_calc/tools" + "electricity_bill_calc/tools/serial" + "electricity_bill_calc/tools/time" + "fmt" + "strings" + + "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 _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)) + cacheConditions := []string{ + uid, + } + if parks, err := cache.RetrieveSearch[[]*model.Park]("park_belongs", cacheConditions...); err == nil && parks != nil { + pr.log.Info("已经从缓存获取到了指定用户下的所有园区。") + return *parks, nil + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + var parks []*model.Park + parkQuerySql, parkParams, _ := pr.ds. + From("park"). + 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 + } + + cache.CacheSearch(parks, []string{"park", fmt.Sprintf("park:%s", uid)}, "park_belongs", cacheConditions...) + + return parks, nil +} + +// 检查并确定指定园区的归属情况 +func (pr _ParkRepository) IsParkBelongs(pid, uid string) (bool, error) { + pr.log.Info("检查并确定指定园区的归属情况", zap.String("pid", pid), zap.String("uid", uid)) + cacheConditions := []string{ + pid, "belongs", uid, + } + if exists, err := cache.CheckExists("park", cacheConditions...); err == nil && exists { + pr.log.Info("已经从缓存获取到了指定园区的归属情况。") + return true, nil + } + 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 + } + + if count > 0 { + cache.CacheExists([]string{"park", fmt.Sprintf("park:%s", uid)}, "park", cacheConditions...) + } + + 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 := time.Now() + 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{ + serial.GeneratePrefixedUniqueSerialString("P"), + 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 + } + + cache.AbolishRelation("park") + + return rs.RowsAffected() > 0, nil +} + +// 获取指定园区的详细信息 +func (pr _ParkRepository) RetrieveParkDetail(pid string) (*model.Park, error) { + pr.log.Info("获取指定园区的详细信息", zap.String("pid", pid)) + if park, err := cache.RetrieveEntity[model.Park]("park", pid); err == nil && park != nil { + pr.log.Info("已经从缓存获取到了指定园区的详细信息。") + return park, nil + } + ctx, cancel := global.TimeoutContext() + defer cancel() + + var park model.Park + parkSql, parkArgs, _ := pr.ds. + From("park"). + 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 + } + + cache.CacheEntity(park, []string{"park", fmt.Sprintf("park:%s", pid)}, "park", pid) + + return &park, nil +} + +// 获取园区对应的用户ID +func (pr _ParkRepository) RetrieveParkbelongs(pid string) (string, error) { + pr.log.Info("获取园区对应的用户ID", zap.String("pid", pid)) + if uid, err := cache.RetrieveEntity[string]("park_belongs", pid); err == nil && uid != nil { + pr.log.Info("已经从缓存获取到了园区对应的用户ID。") + return *uid, nil + } + 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 + } + + cache.CacheEntity(uid, []string{"park", fmt.Sprintf("park:%s", pid)}, "park_belongs", pid) + + 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 := time.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 + } + + if ok.RowsAffected() > 0 { + cache.AbolishRelation("park") + cache.AbolishRelation(fmt.Sprintf("park:%s", pid)) + } + 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 := time.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 + } + + if ok.RowsAffected() > 0 { + cache.AbolishRelation("park") + cache.AbolishRelation(fmt.Sprintf("park:%s", pid)) + } + 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 := time.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 + } + + if ok.RowsAffected() > 0 { + cache.AbolishRelation("park") + cache.AbolishRelation(fmt.Sprintf("park:%s", pid)) + } + 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 + } + cacheConditions := []string{strings.Join(pids, ",")} + if parks, err := cache.RetrieveSearch[[]*model.Park]("park", cacheConditions...); err == nil && parks != nil { + pr.log.Info("已经从缓存获取到了给定的园区详细信息列表。") + return *parks, 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 + } + + cache.CacheSearch(parks, []string{"park", fmt.Sprintf("park:%s", strings.Join(pids, ":"))}, "park", cacheConditions...) + + return parks, nil +} + +// 获取指定园区中的建筑 +func (pr _ParkRepository) RetrieveParkBuildings(pid string) ([]*model.ParkBuilding, error) { + pr.log.Info("获取指定园区中的建筑", zap.String("pid", pid)) + if buildings, err := cache.RetrieveSearch[[]*model.ParkBuilding]("park_building", pid); err == nil && buildings != nil { + pr.log.Info("已经从缓存获取到了指定园区中的建筑。") + return *buildings, nil + } + 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 + } + + cache.CacheSearch(buildings, []string{"park_building", fmt.Sprintf("park_building:%s", pid)}, "park_building", pid) + + 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 := time.Now() + createSql, createArgs, _ := pr.ds. + Insert("park_building"). + Cols( + "id", "park_id", "name", "floors", "enabled", "created_at", "last_modified_at", + ). + Vals(goqu.Vals{ + serial.GeneratePrefixedUniqueSerialString("B"), + 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 + } + + if rs.RowsAffected() > 0 { + cache.AbolishRelation("park_building") + cache.AbolishRelation(fmt.Sprintf("park_building:%s", pid)) + } + + 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 := time.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 + } + + if rs.RowsAffected() > 0 { + cache.AbolishRelation("park_building") + cache.AbolishRelation(fmt.Sprintf("park_building:%s", pid)) + } + + 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 := time.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 + } + + if rs.RowsAffected() > 0 { + cache.AbolishRelation("park_building") + cache.AbolishRelation(fmt.Sprintf("park_building:%s", pid)) + } + + 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 := time.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 + } + + if rs.RowsAffected() > 0 { + cache.AbolishRelation("park_building") + cache.AbolishRelation(fmt.Sprintf("park_building:%s", pid)) + } + + return rs.RowsAffected() > 0, nil +} diff --git a/router/router.go b/router/router.go index def7992..73e779f 100644 --- a/router/router.go +++ b/router/router.go @@ -47,6 +47,7 @@ func App() *fiber.App { controller.InitializeUserHandlers(app) controller.InitializeRegionHandlers(app) controller.InitializeChargeHandlers(app) + controller.InitializeParkHandlers(app) return app } diff --git a/tools/utils.go b/tools/utils.go index a0333d9..544f904 100644 --- a/tools/utils.go +++ b/tools/utils.go @@ -7,6 +7,7 @@ import ( "github.com/mozillazg/go-pinyin" "github.com/samber/lo" + "github.com/shopspring/decimal" ) func ContainsInsensitive(element string, slice []string) bool { @@ -86,3 +87,27 @@ func CondFn[T, R any](exprFn func(val T) bool, value T, trueValue, falseValue R) 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 +} diff --git a/vo/park.go b/vo/park.go new file mode 100644 index 0000000..a557f4b --- /dev/null +++ b/vo/park.go @@ -0,0 +1,67 @@ +package vo + +import ( + "electricity_bill_calc/model" + "electricity_bill_calc/tools" +) + +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"` +} diff --git a/vo/shares.go b/vo/shares.go new file mode 100644 index 0000000..92d6f95 --- /dev/null +++ b/vo/shares.go @@ -0,0 +1,5 @@ +package vo + +type StateForm struct { + Enabled bool `json:"enabled"` +}