feat(charge):基本完成用户充值管理部分接口。

This commit is contained in:
徐涛 2023-06-03 11:26:36 +08:00
parent f8ef6aba98
commit 98f3bdec0a
7 changed files with 434 additions and 4 deletions

101
controller/charge.go Normal file
View File

@ -0,0 +1,101 @@
package controller
import (
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"electricity_bill_calc/response"
"electricity_bill_calc/service"
"net/http"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
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 searchCharges(c *fiber.Ctx) error {
chargeLog.Info("检索用户的充值记录列表。")
result := response.NewResult(c)
keyword := c.Query("keyword", "")
page := c.QueryInt("page", 1)
beginTime, err := model.ParseDate(c.Query("begin"))
if err != nil {
chargeLog.Error("无法解析查询起始时间。", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
endTime, err := model.ParseDate(c.Query("end"))
if err != nil {
chargeLog.Error("无法解析查询结束时间。", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
charges, total, err := repository.ChargeRepository.FindCharges(uint(page), &beginTime.Time, &endTime.Time, &keyword)
if err != nil {
chargeLog.Error("检索用户的充值记录列表失败。", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
return result.Success(
"已经获取到符合条件的计费记录。",
response.NewPagedResponse(page, total).ToMap(),
fiber.Map{"records": charges},
)
}
// 创建一条新的用户充值记录
func createNewUserChargeRecord(c *fiber.Ctx) error {
chargeLog.Info("创建一条新的用户充值记录。")
result := response.NewResult(c)
createionForm := new(model.ChargeRecordCreationForm)
if err := c.BodyParser(createionForm); err != nil {
chargeLog.Error("无法解析创建充值记录的请求数据。", zap.Error(err))
return result.Error(http.StatusBadRequest, err.Error())
}
ok, err := service.ChargeService.RecordUserCharge(
createionForm.UserId,
createionForm.Fee,
createionForm.Discount,
createionForm.Amount,
createionForm.ChargeTo,
true,
)
if err != nil {
chargeLog.Error("创建用户充值记录失败。", zap.Error(err))
return result.Error(http.StatusInternalServerError, err.Error())
}
if !ok {
chargeLog.Error("创建用户充值记录失败。")
return result.NotAccept("创建用户充值记录失败。")
} else {
return result.Success("创建用户充值记录成功, 指定用户的服务已延期。")
}
}
// 改变用户充值记录的状态
func modifyUserChargeState(c *fiber.Ctx) error {
chargeLog.Info("改变用户充值记录的状态。")
result := response.NewResult(c)
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())
}
if !ok {
chargeLog.Error("取消用户充值记录失败。")
return result.NotAccept("取消用户充值记录失败。")
} else {
return result.Success("取消用户充值记录成功。")
}
}

View File

@ -1,8 +1,10 @@
package logger
import (
"electricity_bill_calc/model"
"os"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
@ -130,3 +132,11 @@ 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 DateField(key string, val *model.Date) zap.Field {
return zap.String(key, val.Format("2006-01-02"))
}

30
model/charge.go Normal file
View File

@ -0,0 +1,30 @@
package model
import (
"time"
"github.com/shopspring/decimal"
)
type UserChargeDetail struct {
Seq int64 `json:"seq"`
UserId string `json:"user_id"`
Name string `json:"name"`
Fee *decimal.Decimal `json:"fee"`
Discount *decimal.Decimal `json:"discount"`
Amount *decimal.Decimal `json:"amount"`
ChargeTo Date `json:"charge_to"`
Settled bool `json:"settled"`
SettledAt *time.Time `json:"settled_at"`
Cancelled bool `json:"cancelled"`
CancelledAt *time.Time `json:"cancelled_at"`
CreatedAt time.Time `json:"created_at"`
}
type ChargeRecordCreationForm struct {
UserId string `json:"userId"`
Fee *decimal.Decimal `json:"fee"`
Discount *decimal.Decimal `json:"discount"`
Amount *decimal.Decimal `json:"amount"`
ChargeTo Date `json:"chargeTo"`
}

167
repository/charge.go Normal file
View File

@ -0,0 +1,167 @@
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/time"
"fmt"
st "time"
"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"
"github.com/shopspring/decimal"
"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 *st.Time, keyword *string) ([]*model.UserChargeDetail, int64, error) {
cr.log.Info("查询用户的充值记录。", zap.Timep("beginTime", beginTime), zap.Timep("endTime", endTime), zap.Stringp("keyword", keyword), zap.Uint("page", page))
cacheConditions := []string{
fmt.Sprintf("%d", page),
tools.DefaultTo(keyword, ""),
tools.CondFn(func(t *st.Time) bool { return t != nil }, beginTime, beginTime.Format("2006-01-02"), "UNDEF"),
tools.CondFn(func(t *st.Time) bool { return t != nil }, endTime, endTime.Format("2006-01-02"), "UNDEF"),
}
if charges, total, err := cache.RetrievePagedSearch[[]*model.UserChargeDetail]("charges", cacheConditions...); err == nil {
cr.log.Info("从缓存中获取用户的充值记录成功。", zap.Int("count", len(*charges)), zap.Int64("total", total))
return *charges, total, nil
}
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.user_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.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.user_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.charge_to").Gte(*beginTime))
countQuery = countQuery.Where(goqu.I("c.charge_to").Gte(*beginTime))
}
if endTime != nil {
chargeQuery = chargeQuery.Where(goqu.I("c.charge_to").Lte(*endTime))
countQuery = countQuery.Where(goqu.I("c.charge_to").Lte(*endTime))
}
chargeQuery = chargeQuery.Order(goqu.I("c.created_by").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
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
}
cache.CachePagedSearch(charges, total, []string{"charges"}, "charges", cacheConditions...)
return charges, total, nil
}
// 在用户充值记录中创建一条新的记录
func (cr _ChargeRepository) CreateChargeRecord(tx pgx.Tx, ctx context.Context, uid string, fee, discount, amount *decimal.Decimal, chargeTo model.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, time.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": time.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) (*model.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 []*model.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 *model.Date) bool { return a.Time.After(b.Time) })
return lastCharge, nil
}

View File

@ -1,6 +1,7 @@
package repository
import (
"context"
"electricity_bill_calc/cache"
"electricity_bill_calc/config"
"electricity_bill_calc/global"
@ -16,6 +17,7 @@ import (
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
"github.com/fufuok/utils"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
"github.com/samber/lo"
"go.uber.org/zap"
)
@ -449,10 +451,8 @@ func (ur _UserRepository) SearchUsersWithLimit(userType *int16, keyword *string,
}
// 更新指定用户的服务有效期限
func (ur _UserRepository) UpdateServiceExpiration(uid string, expiration stdTime.Time) (bool, error) {
func (ur _UserRepository) UpdateServiceExpiration(tx pgx.Tx, ctx context.Context, uid string, expiration stdTime.Time) (bool, error) {
ur.log.Info("更新指定用户的服务有效期限。", zap.String("user id", uid))
ctx, cancel := global.TimeoutContext()
defer cancel()
userDetailUpdateQuery := ur.ds.
Update("user_detail").
@ -462,7 +462,7 @@ func (ur _UserRepository) UpdateServiceExpiration(uid string, expiration stdTime
userDetailSql, userDetailParams, _ := userDetailUpdateQuery.
Prepared(true).ToSQL()
if res, err := global.DB.Exec(ctx, userDetailSql, userDetailParams...); err != nil {
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 {

View File

@ -46,6 +46,7 @@ func App() *fiber.App {
controller.InitializeUserHandlers(app)
controller.InitializeRegionHandlers(app)
controller.InitializeChargeHandlers(app)
return app
}

121
service/charge.go Normal file
View File

@ -0,0 +1,121 @@
package service
import (
"electricity_bill_calc/cache"
"electricity_bill_calc/global"
"electricity_bill_calc/logger"
"electricity_bill_calc/model"
"electricity_bill_calc/repository"
"fmt"
"github.com/shopspring/decimal"
"go.uber.org/zap"
)
type _ChargeService struct {
log *zap.Logger
}
var ChargeService = &_ChargeService{
log: logger.Named("Service", "Charge"),
}
// 创建一条新的用户充值记录,同时更新用户的服务期限
func (cs _ChargeService) RecordUserCharge(uid string, fee, discount, amount *decimal.Decimal, chargeTo model.Date, extendExpriationIgnoringSettle bool) (bool, error) {
cs.log.Info(
"创建一条新的用户充值记录。",
zap.String("uid", uid),
logger.DecimalField("fee", fee),
logger.DecimalField("discount", discount),
logger.DecimalField("amount", amount),
logger.DateField("chargeTo", &chargeTo),
zap.Bool("extendExpriationIgnoringSettle", extendExpriationIgnoringSettle),
)
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.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("未能成功更新用户服务期限")
}
}
err = tx.Commit(ctx)
if err != nil {
cs.log.Error("提交数据库事务失败。", zap.Error(err))
return false, err
}
cache.AbolishRelation("charge")
cache.AbolishRelation(fmt.Sprintf("user:%s", uid))
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
}
cache.AbolishRelation("charge")
cache.AbolishRelation(fmt.Sprintf("user:%s", uid))
return true, nil
}