feat: complete charge

This commit is contained in:
Rogee
2024-12-10 14:50:50 +08:00
parent e3ef31037c
commit db0eacbc46
12 changed files with 431 additions and 64 deletions

View File

@@ -1,14 +1,35 @@
package users
import "github.com/gofiber/fiber/v3"
import (
"backend/pkg/consts"
"backend/providers/jwt"
"github.com/gofiber/fiber/v3"
log "github.com/sirupsen/logrus"
hashids "github.com/speps/go-hashids/v2"
)
// @provider
type Controller struct {
svc *Service
svc *Service
hashIds *hashids.HashID
}
// List
func (c *Controller) List(ctx fiber.Ctx) error {
return ctx.SendString(ctx.Params("tenant", "no user"))
return ctx.JSON(nil)
}
// Charge
func (c *Controller) Charge(ctx fiber.Ctx) error {
claim := fiber.Locals[*jwt.Claims](ctx, consts.CtxKeyClaim)
log.Debug(claim)
// [tenantId, chargeAmount, timestamp]
code := ctx.Params("code")
if err := c.svc.Charge(ctx.Context(), claim, code); err != nil {
return err
}
return ctx.JSON(nil)
}

View File

@@ -7,14 +7,17 @@ import (
"git.ipao.vip/rogeecn/atom/container"
"git.ipao.vip/rogeecn/atom/contracts"
"git.ipao.vip/rogeecn/atom/utils/opt"
hashids "github.com/speps/go-hashids/v2"
)
func Provide(opts ...opt.Option) error {
if err := container.Container.Provide(func(
hashIds *hashids.HashID,
svc *Service,
) (*Controller, error) {
obj := &Controller{
svc: svc,
hashIds: hashIds,
svc: svc,
}
return obj, nil
}); err != nil {
@@ -37,9 +40,11 @@ func Provide(opts ...opt.Option) error {
if err := container.Container.Provide(func(
db *sql.DB,
hashIds *hashids.HashID,
) (*Service, error) {
obj := &Service{
db: db,
db: db,
hashIds: hashIds,
}
if err := obj.Prepare(); err != nil {
return nil, err

View File

@@ -27,4 +27,5 @@ func (r *Router) Name() string {
func (r *Router) Register(router fiber.Router) {
group := router.Group(r.Name())
group.Get("", r.controller.List)
group.Patch("charge/:code", r.controller.Charge)
}

View File

@@ -9,18 +9,22 @@ import (
"backend/database/models/qvyun/public/table"
"backend/pkg/consts"
"backend/pkg/db"
"backend/pkg/errorx"
"backend/pkg/pg"
"backend/providers/jwt"
. "github.com/go-jet/jet/v2/postgres"
"github.com/go-jet/jet/v2/qrm"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
hashids "github.com/speps/go-hashids/v2"
)
// @provider:except
type Service struct {
db *sql.DB
log *logrus.Entry `inject:"false"`
db *sql.DB
hashIds *hashids.HashID
log *logrus.Entry `inject:"false"`
}
func (svc *Service) Prepare() error {
@@ -255,3 +259,123 @@ func (svc *Service) SetTenantExpireAtBySlug(ctx context.Context, slug string, ex
return nil
}
func (svc *Service) GenerateChargeCode(ctx context.Context, tenantID, chargeAmount int64) (string, error) {
log := svc.log.WithField("method", "GenerateChargeCode")
timestamp := time.Now().Unix()
code, err := svc.hashIds.EncodeInt64([]int64{tenantID, chargeAmount, timestamp})
if err != nil {
return "", errors.Wrap(err, "failed to encode charge code")
}
log.Infof("generate charge code: %s", code)
return code, nil
}
// Charge
func (svc *Service) Charge(ctx context.Context, claim *jwt.Claims, code string) error {
log := svc.log.WithField("method", "Charge")
raw, err := svc.hashIds.DecodeInt64WithError(code)
if err != nil {
return errorx.InvalidChargeCode
}
if len(raw) != 3 {
return errorx.InvalidChargeCode
}
tenantId, chargeAmount, timestamp := raw[0], raw[1], raw[2]
if tenantId != claim.TenantID {
return errorx.InvalidChargeCode
}
generatedAt := time.Unix(timestamp, 0)
log.Infof("charge code %s generated at: %s", code, generatedAt)
if chargeAmount <= 0 {
return errorx.InvalidChargeCode
}
t := table.UserBalanceHistories
st := t.SELECT(COUNT(t.ID).AS("cnt")).WHERE(t.Code.EQ(String(code)))
log.Debug(st.DebugSql())
var result struct {
Cnt int64
}
if err := st.QueryContext(ctx, db.FromContext(ctx, svc.db), &result); err != nil {
return errors.Wrap(err, "failed to query charge code")
}
if result.Cnt > 0 {
return errorx.InvalidChargeCode
}
has, err := svc.TenantHasUser(ctx, claim.UserID, tenantId)
if err != nil {
return errors.Wrap(err, "failed to check user-tenant relation")
}
if !has {
return errorx.InvalidChargeCode
}
log.Infof("charge tenant: %d, user: %d, amount: %d", claim.TenantID, claim.UserID, chargeAmount)
tx, err := svc.db.BeginTx(ctx, nil)
if err != nil {
return errors.Wrap(err, "failed to begin transaction")
}
defer tx.Rollback()
// update user balance in users_tenants
tbl := table.UsersTenants
stmt := tbl.
UPDATE().
SET(
tbl.Balance.SET(
tbl.Balance.ADD(Int64(chargeAmount)),
),
).
WHERE(
tbl.UserID.EQ(Int64(claim.UserID)).AND(
tbl.TenantID.EQ(Int64(claim.TenantID)),
),
)
log.Debug(stmt.DebugSql())
if _, err := stmt.ExecContext(ctx, db.FromContext(ctx, svc.db)); err != nil {
return errors.Wrap(err, "failed to charge user balance")
}
// insert charge record
chargeTbl := table.UserBalanceHistories
chargeStmt := chargeTbl.
INSERT(
chargeTbl.UserID,
chargeTbl.TenantID,
chargeTbl.Balance,
chargeTbl.Target,
chargeTbl.Type,
chargeTbl.Code,
).
VALUES(
Int64(claim.UserID),
Int64(claim.TenantID),
Int64(chargeAmount),
Json(pg.BalanceTarget{}.MustValue()),
String(pg.BalanceTypeCharge.String()),
String(code),
)
log.Debug(chargeStmt.DebugSql())
if _, err := chargeStmt.ExecContext(ctx, db.FromContext(ctx, svc.db)); err != nil {
return errors.Wrap(err, "failed to insert charge record")
}
if err := tx.Commit(); err != nil {
return errors.Wrap(err, "failed to commit transaction")
}
return nil
}

View File

@@ -4,64 +4,56 @@ import (
"context"
"testing"
"backend/fixtures"
dbUtil "backend/pkg/db"
"backend/pkg/pg"
"backend/pkg/service/testx"
"backend/providers/hashids"
"backend/providers/jwt"
"backend/providers/postgres"
"backend/providers/storage"
log "github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/suite"
"go.uber.org/dig"
)
func TestService_GetOrNew(t *testing.T) {
FocusConvey("Test GetOrNew", t, func() {
// So(dbUtil.TruncateAllTables(context.TODO(), db, "users", "users_tenants"), ShouldBeNil)
db, err := fixtures.GetDB()
So(err, ShouldBeNil)
defer db.Close()
type ServiceInjectParams struct {
dig.In
Svc *Service
}
Convey("Test GetOrNew", func() {
svc := &Service{db: db}
So(svc.Prepare(), ShouldBeNil)
type ServiceTestSuite struct {
suite.Suite
ServiceInjectParams
}
user, err := svc.GetByOpenID(context.Background(), "hello")
So(err, ShouldBeNil)
func Test_DiscoverMedias(t *testing.T) {
log.SetLevel(log.DebugLevel)
So(user, ShouldNotBeNil)
So(user.OpenID, ShouldEqual, "hello")
})
providers := testx.Default(
postgres.DefaultProvider(),
storage.DefaultProvider(),
hashids.DefaultProvider(),
).With(
Provide,
)
FocusConvey("Test GetOrNew", func() {
svc := &Service{db: db}
So(svc.Prepare(), ShouldBeNil)
openid := "test_openid"
authInfo := pg.UserOAuth{
AccessToken: "test_access_token",
}
user, err := svc.GetOrNew(context.Background(), 1, openid, authInfo)
So(err, ShouldBeNil)
So(user.OpenID, ShouldEqual, openid)
})
testx.Serve(providers, t, func(params ServiceInjectParams) {
suite.Run(t, &ServiceTestSuite{ServiceInjectParams: params})
})
}
func TestService_CreateTenantUser(t *testing.T) {
FocusConvey("Test CreateTenantUser", t, func() {
db, err := fixtures.GetDB()
func (t *ServiceTestSuite) Test_Charge() {
Convey("Charge", t.T(), func() {
code, err := t.Svc.GenerateChargeCode(context.Background(), 1, 100)
So(err, ShouldBeNil)
defer db.Close()
code = "b8TDWf59wvPw"
So(dbUtil.TruncateAllTables(context.TODO(), db, "users", "users_tenants"), ShouldBeNil)
FocusConvey("Test Create", func() {
svc := &Service{db: db}
So(svc.Prepare(), ShouldBeNil)
err := svc.CreateTenantUser(context.Background(), 1, 1)
So(err, ShouldBeNil)
err = svc.CreateTenantUser(context.Background(), 1, 1)
So(err, ShouldBeNil)
})
err = t.Svc.Charge(context.Background(), &jwt.Claims{
BaseClaims: jwt.BaseClaims{
TenantID: 1,
UserID: 1,
},
}, code)
So(err, ShouldBeNil)
})
}