feat: add wechat pay

This commit is contained in:
Rogee
2025-01-14 14:42:08 +08:00
parent 52c17b63bb
commit 9cd7659d14
32 changed files with 1431 additions and 110 deletions

View File

@@ -44,6 +44,11 @@ func New(code, statusCode int, message string) *Response {
} }
} }
func (r *Response) WithMsg(msg string) *Response {
r.Message = msg
return r
}
func (r *Response) Sql(sql string) *Response { func (r *Response) Sql(sql string) *Response {
r.sql = sql r.sql = sql
return r return r

View File

@@ -1,52 +0,0 @@
package orders
import (
"backend/app/requests"
"backend/database/models/qvyun_v2/public/model"
"backend/providers/jwt"
"github.com/gofiber/fiber/v3"
"github.com/jinzhu/copier"
"github.com/samber/lo"
log "github.com/sirupsen/logrus"
)
// @provider
type Controller struct {
svc *Service
log *log.Entry `inject:"false"`
}
func (c *Controller) Prepare() error {
c.log = log.WithField("module", "orders.Controller")
return nil
}
// Orders show user orders
// @Router /api/v1/orders [get]
// @Bind claim local
// @Bind pagination query
// @Bind filter query
func (c *Controller) List(ctx fiber.Ctx, claim *jwt.Claims, pagination *requests.Pagination, filter *UserOrderFilter) (*requests.Pager, error) {
pagination.Format()
pager := &requests.Pager{
Pagination: *pagination,
}
filter.UserID = claim.UserID
orders, total, err := c.svc.GetOrders(ctx.Context(), pagination, filter)
if err != nil {
return nil, err
}
pager.Total = total
pager.Items = lo.FilterMap(orders, func(item model.Orders, _ int) (UserOrder, bool) {
var o UserOrder
if err := copier.Copy(&o, item); err != nil {
return o, false
}
return o, true
})
return pager, nil
}

View File

@@ -0,0 +1,97 @@
package orders
import (
"backend/app/errorx"
"backend/app/http/posts"
"backend/app/http/tenants"
"backend/app/http/users"
"backend/app/requests"
"backend/database/models/qvyun_v2/public/model"
"backend/providers/jwt"
"github.com/gofiber/fiber/v3"
"github.com/jinzhu/copier"
"github.com/samber/lo"
log "github.com/sirupsen/logrus"
)
// @provider
type OrderController struct {
svc *Service
userSvc *users.Service
tenantSvc *tenants.Service
postSvc *posts.Service
log *log.Entry `inject:"false"`
}
func (c *OrderController) Prepare() error {
c.log = log.WithField("module", "orders.OrderController")
return nil
}
// Orders show user orders
// @Router /api/v1/orders [get]
// @Bind claim local
// @Bind pagination query
// @Bind filter query
func (c *OrderController) List(ctx fiber.Ctx, claim *jwt.Claims, pagination *requests.Pagination, filter *UserOrderFilter) (*requests.Pager, error) {
pagination.Format()
pager := &requests.Pager{
Pagination: *pagination,
}
filter.UserID = claim.UserID
orders, total, err := c.svc.GetOrders(ctx.Context(), pagination, filter)
if err != nil {
return nil, err
}
pager.Total = total
pager.Items = lo.FilterMap(orders, func(item model.Orders, _ int) (UserOrder, bool) {
var o UserOrder
if err := copier.Copy(&o, item); err != nil {
return o, false
}
return o, true
})
return pager, nil
}
// Create order
// @Router /api/v1/orders [post]
// @Bind claim local
// @Bind hash path
// @Bind tenantSlug cookie key(tenant)
func (c *OrderController) Create(ctx fiber.Ctx, claim *jwt.Claims, tenantSlug, hash string) (*UserOrder, error) {
user, err := c.userSvc.GetUserByID(ctx.Context(), claim.UserID)
if err != nil {
return nil, err
}
tenant, err := c.tenantSvc.GetTenantBySlug(ctx.Context(), tenantSlug)
if err != nil {
return nil, err
}
post, err := c.postSvc.GetPostByHash(ctx.Context(), tenant.ID, hash)
if err != nil {
return nil, err
}
if tenant.ID != post.TenantID {
return nil, errorx.BadRequest
}
order, err := c.svc.Create(ctx.Context(), user, post)
if err != nil {
return nil, err
}
var userOrder UserOrder
if err := copier.Copy(&userOrder, order); err != nil {
return nil, err
}
return &userOrder, nil
}

View File

@@ -0,0 +1,78 @@
package orders
import (
"backend/app/errorx"
"backend/app/http/posts"
"backend/app/http/tenants"
"backend/app/http/users"
"backend/database/fields"
"backend/database/models/qvyun_v2/public/model"
"backend/providers/jwt"
"backend/providers/pay"
"github.com/go-pay/gopay/wechat/v3"
"github.com/gofiber/fiber/v3"
log "github.com/sirupsen/logrus"
)
// @provider
type PayController struct {
svc *Service
pay *pay.Client
userSvc *users.Service
tenantSvc *tenants.Service
postSvc *posts.Service
log *log.Entry `inject:"false"`
}
func (c *PayController) Prepare() error {
c.log = log.WithField("module", "orders.Controller")
return nil
}
// JSPay
// @Router /api/v1/orders/pay/:orderID/js [get]
// @Bind claim local
// @Bind orderID path
func (ctl *PayController) JSPay(ctx fiber.Ctx, claim *jwt.Claims, orderID string) (*wechat.JSAPIPayParams, error) {
order, err := ctl.svc.GetUserOrderByOrderID(ctx.Context(), orderID, claim.UserID)
if err != nil {
return nil, err
}
if order.Status != fields.OrderStatusPending {
return nil, errorx.BadRequest.WithMsg("订单状态异常")
}
oauths, err := ctl.userSvc.GetUserOAuthChannels(ctx.Context(), claim.UserID)
if err != nil {
return nil, err
}
var oauth *model.UserOauths
for _, v := range oauths {
if v.Channel == fields.AuthChannelWeChat {
oauth = &v
break
}
}
if oauth == nil {
return nil, errorx.BadRequest.WithMsg("未绑定微信")
}
params, err := ctl.pay.WeChat_JSApiPayRequest(
ctx.Context(),
oauth.OpenID,
order.OrderSerial,
order.Title,
order.Amount,
1,
"/v1/orders/pay/wechat/notify",
)
if err != nil {
return nil, err
}
return params, nil
}

View File

@@ -3,6 +3,11 @@ package orders
import ( import (
"database/sql" "database/sql"
"backend/app/http/posts"
"backend/app/http/tenants"
"backend/app/http/users"
"backend/providers/pay"
"git.ipao.vip/rogeecn/atom" "git.ipao.vip/rogeecn/atom"
"git.ipao.vip/rogeecn/atom/container" "git.ipao.vip/rogeecn/atom/container"
"git.ipao.vip/rogeecn/atom/contracts" "git.ipao.vip/rogeecn/atom/contracts"
@@ -11,10 +16,16 @@ import (
func Provide(opts ...opt.Option) error { func Provide(opts ...opt.Option) error {
if err := container.Container.Provide(func( if err := container.Container.Provide(func(
postSvc *posts.Service,
svc *Service, svc *Service,
) (*Controller, error) { tenantSvc *tenants.Service,
obj := &Controller{ userSvc *users.Service,
) (*OrderController, error) {
obj := &OrderController{
postSvc: postSvc,
svc: svc, svc: svc,
tenantSvc: tenantSvc,
userSvc: userSvc,
} }
if err := obj.Prepare(); err != nil { if err := obj.Prepare(); err != nil {
return nil, err return nil, err
@@ -25,10 +36,34 @@ func Provide(opts ...opt.Option) error {
return err return err
} }
if err := container.Container.Provide(func( if err := container.Container.Provide(func(
controller *Controller, pay *pay.Client,
postSvc *posts.Service,
svc *Service,
tenantSvc *tenants.Service,
userSvc *users.Service,
) (*PayController, error) {
obj := &PayController{
pay: pay,
postSvc: postSvc,
svc: svc,
tenantSvc: tenantSvc,
userSvc: userSvc,
}
if err := obj.Prepare(); err != nil {
return nil, err
}
return obj, nil
}); err != nil {
return err
}
if err := container.Container.Provide(func(
orderController *OrderController,
payController *PayController,
) (contracts.HttpRoute, error) { ) (contracts.HttpRoute, error) {
obj := &Routes{ obj := &Routes{
controller: controller, orderController: orderController,
payController: payController,
} }
if err := obj.Prepare(); err != nil { if err := obj.Prepare(); err != nil {
return nil, err return nil, err

View File

@@ -16,7 +16,8 @@ import (
// @provider contracts.HttpRoute atom.GroupRoutes // @provider contracts.HttpRoute atom.GroupRoutes
type Routes struct { type Routes struct {
log *log.Entry `inject:"false"` log *log.Entry `inject:"false"`
controller *Controller orderController *OrderController
payController *PayController
} }
func (r *Routes) Prepare() error { func (r *Routes) Prepare() error {
@@ -29,12 +30,26 @@ func (r *Routes) Name() string {
} }
func (r *Routes) Register(router fiber.Router) { func (r *Routes) Register(router fiber.Router) {
// 注册路由组: Controller // 注册路由组: OrderController
router.Get("/api/v1/orders", DataFunc3( router.Get("/api/v1/orders", DataFunc3(
r.controller.List, r.orderController.List,
Local[*jwt.Claims]("claim"), Local[*jwt.Claims]("claim"),
Query[requests.Pagination]("pagination"), Query[requests.Pagination]("pagination"),
Query[UserOrderFilter]("filter"), Query[UserOrderFilter]("filter"),
)) ))
router.Post("/api/v1/orders", DataFunc3(
r.orderController.Create,
Local[*jwt.Claims]("claim"),
CookieParam("tenant"),
PathParam[string]("hash"),
))
// 注册路由组: PayController
router.Get("/api/v1/orders/pay/:orderID/js", DataFunc2(
r.payController.JSPay,
Local[*jwt.Claims]("claim"),
PathParam[string]("orderID"),
))
} }

View File

@@ -3,10 +3,13 @@ package orders
import ( import (
"context" "context"
"database/sql" "database/sql"
"time"
"backend/app/requests" "backend/app/requests"
"backend/database/fields"
"backend/database/models/qvyun_v2/public/model" "backend/database/models/qvyun_v2/public/model"
"backend/database/models/qvyun_v2/public/table" "backend/database/models/qvyun_v2/public/table"
"backend/pkg/utils"
"backend/providers/otel" "backend/providers/otel"
. "github.com/go-jet/jet/v2/postgres" . "github.com/go-jet/jet/v2/postgres"
@@ -75,3 +78,99 @@ func (svc *Service) GetOrders(ctx context.Context, pagination *requests.Paginati
} }
return orders, count.Cnt, nil return orders, count.Cnt, nil
} }
// CreateOrder
func (svc *Service) Create(ctx context.Context, user *model.Users, post *model.Posts) (*model.Orders, error) {
_, span := otel.Start(ctx, "users.service.CreateOrder")
defer span.End()
span.SetAttributes(
attribute.Int64("post.id", post.ID),
)
price := post.Price
if post.Discount != 100 {
price = post.Price * int64(post.Discount) / 100
}
m := model.Orders{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
TenantID: post.TenantID,
UserID: user.ID,
Type: fields.OrderTypeConsume,
Status: fields.OrderStatusPending,
OrderSerial: svc.generateCreateOrderSerial(ctx),
RemoteOrderSerial: "",
RefundSerial: "",
RemoteRefundSerial: "",
Amount: price,
Currency: "CNY",
Title: post.Title,
Description: new(string),
Meta: fields.ToJson(fields.OrderMeta{
ObjectID: post.ID,
Price: post.Price,
Discount: post.Discount,
Coupons: nil,
}),
}
tbl := table.Orders
stmt := tbl.INSERT(tbl.MutableColumns).MODEL(m).RETURNING(tbl.AllColumns)
span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql()))
var mm model.Orders
if err := stmt.QueryContext(ctx, svc.db, &mm); err != nil {
return nil, err
}
return &mm, nil
}
// generateCreateOrderSerial
func (svc *Service) generateCreateOrderSerial(ctx context.Context) string {
return utils.GenerateOrderSerial("O")
}
// generateRefundOrderSerial
func (svc *Service) generateRefundOrderSerial(ctx context.Context) string {
return utils.GenerateOrderSerial("R")
}
// GetByOrderID
func (svc *Service) GetByOrderID(ctx context.Context, orderID string) (*model.Orders, error) {
_, span := otel.Start(ctx, "users.service.GetByOrderID")
defer span.End()
span.SetAttributes(
attribute.String("order.id", orderID),
)
tbl := table.Orders
stmt := tbl.SELECT(tbl.AllColumns).WHERE(tbl.OrderSerial.EQ(String(orderID)))
span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql()))
var order model.Orders
if err := stmt.QueryContext(ctx, svc.db, &order); err != nil {
return nil, err
}
return &order, nil
}
// GetUserOrderByOrderID
func (svc *Service) GetUserOrderByOrderID(ctx context.Context, orderID string, userID int64) (*model.Orders, error) {
_, span := otel.Start(ctx, "users.service.GetUserOrderByOrderID")
defer span.End()
span.SetAttributes(
attribute.String("order.id", orderID),
attribute.Int64("user.id", userID),
)
tbl := table.Orders
stmt := tbl.SELECT(tbl.AllColumns).WHERE(tbl.OrderSerial.EQ(String(orderID)).AND(tbl.UserID.EQ(Int64(userID))))
span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql()))
var order model.Orders
if err := stmt.QueryContext(ctx, svc.db, &order); err != nil {
return nil, err
}
return &order, nil
}

View File

@@ -1,6 +1,7 @@
package posts package posts
import ( import (
"backend/app/http/tenants"
"backend/app/requests" "backend/app/requests"
"backend/database/models/qvyun_v2/public/model" "backend/database/models/qvyun_v2/public/model"
"backend/providers/jwt" "backend/providers/jwt"
@@ -13,6 +14,7 @@ import (
// @provider // @provider
type Controller struct { type Controller struct {
tenantSvc *tenants.Service
svc *Service svc *Service
log *log.Entry `inject:"false"` log *log.Entry `inject:"false"`
} }
@@ -24,16 +26,22 @@ func (c *Controller) Prepare() error {
// List show posts list // List show posts list
// @Router /api/v1/posts [get] // @Router /api/v1/posts [get]
// @Bind tenantSlug cookie key(tenant)
// @Bind claim local // @Bind claim local
// @Bind pagination query // @Bind pagination query
// @Bind filter query // @Bind filter query
func (c *Controller) List(ctx fiber.Ctx, claim *jwt.Claims, pagination *requests.Pagination, filter *UserPostFilter) (*requests.Pager, error) { func (c *Controller) List(ctx fiber.Ctx, tenantSlug string, claim *jwt.Claims, pagination *requests.Pagination, filter *UserPostFilter) (*requests.Pager, error) {
tenant, err := c.tenantSvc.GetTenantBySlug(ctx.Context(), tenantSlug)
if err != nil {
return nil, err
}
pagination.Format() pagination.Format()
pager := &requests.Pager{ pager := &requests.Pager{
Pagination: *pagination, Pagination: *pagination,
} }
filter.TenantID = *claim.TenantID filter.TenantID = tenant.ID
filter.UserID = claim.UserID filter.UserID = claim.UserID
orders, total, err := c.svc.GetPosts(ctx.Context(), pagination, filter) orders, total, err := c.svc.GetPosts(ctx.Context(), pagination, filter)
if err != nil { if err != nil {
@@ -51,3 +59,64 @@ func (c *Controller) List(ctx fiber.Ctx, claim *jwt.Claims, pagination *requests
return pager, nil return pager, nil
} }
// ListBought show user bought posts list
// @Router /api/v1/bought-posts [get]
// @Bind tenantSlug cookie key(tenant)
// @Bind claim local
// @Bind pagination query
// @Bind filter query
func (c *Controller) ListBought(ctx fiber.Ctx, tenantSlug string, claim *jwt.Claims, pagination *requests.Pagination, filter *UserPostFilter) (*requests.Pager, error) {
tenant, err := c.tenantSvc.GetTenantBySlug(ctx.Context(), tenantSlug)
if err != nil {
return nil, err
}
pagination.Format()
pager := &requests.Pager{
Pagination: *pagination,
}
filter.TenantID = tenant.ID
filter.UserID = claim.UserID
orders, total, err := c.svc.GetBoughtPosts(ctx.Context(), pagination, filter)
if err != nil {
return nil, err
}
pager.Total = total
pager.Items = lo.FilterMap(orders, func(item model.Posts, _ int) (UserPost, bool) {
var o UserPost
if err := copier.Copy(&o, item); err != nil {
return o, false
}
return o, true
})
return pager, nil
}
// Show show posts detail
// @Router /api/v1/show/:hash [get]
// @Bind claim local
// @Bind tenantSlug cookie key(tenant)
// @Bind hash path
func (c *Controller) Show(ctx fiber.Ctx, claim *jwt.Claims, tenantSlug, hash string) (*UserPost, error) {
userPost := &UserPost{}
tenant, err := c.tenantSvc.GetTenantBySlug(ctx.Context(), tenantSlug)
if err != nil {
return nil, err
}
post, err := c.svc.GetPostByHash(ctx.Context(), tenant.ID, hash)
if err != nil {
return nil, err
}
if err := copier.Copy(userPost, post); err != nil {
return nil, err
}
return userPost, nil
}

View File

@@ -3,6 +3,8 @@ package posts
import ( import (
"database/sql" "database/sql"
"backend/app/http/tenants"
"git.ipao.vip/rogeecn/atom" "git.ipao.vip/rogeecn/atom"
"git.ipao.vip/rogeecn/atom/container" "git.ipao.vip/rogeecn/atom/container"
"git.ipao.vip/rogeecn/atom/contracts" "git.ipao.vip/rogeecn/atom/contracts"
@@ -12,9 +14,11 @@ import (
func Provide(opts ...opt.Option) error { func Provide(opts ...opt.Option) error {
if err := container.Container.Provide(func( if err := container.Container.Provide(func(
svc *Service, svc *Service,
tenantSvc *tenants.Service,
) (*Controller, error) { ) (*Controller, error) {
obj := &Controller{ obj := &Controller{
svc: svc, svc: svc,
tenantSvc: tenantSvc,
} }
if err := obj.Prepare(); err != nil { if err := obj.Prepare(); err != nil {
return nil, err return nil, err

View File

@@ -30,11 +30,27 @@ func (r *Routes) Name() string {
func (r *Routes) Register(router fiber.Router) { func (r *Routes) Register(router fiber.Router) {
// 注册路由组: Controller // 注册路由组: Controller
router.Get("/api/v1/posts", DataFunc3( router.Get("/api/v1/posts", DataFunc4(
r.controller.List, r.controller.List,
CookieParam("tenant"),
Local[*jwt.Claims]("claim"), Local[*jwt.Claims]("claim"),
Query[requests.Pagination]("pagination"), Query[requests.Pagination]("pagination"),
Query[UserPostFilter]("filter"), Query[UserPostFilter]("filter"),
)) ))
router.Get("/api/v1/bought-posts", DataFunc4(
r.controller.ListBought,
CookieParam("tenant"),
Local[*jwt.Claims]("claim"),
Query[requests.Pagination]("pagination"),
Query[UserPostFilter]("filter"),
))
router.Get("/api/v1/show/:hash", DataFunc3(
r.controller.Show,
Local[*jwt.Claims]("claim"),
CookieParam("tenant"),
PathParam[string]("hash"),
))
} }

View File

@@ -11,6 +11,7 @@ import (
"backend/providers/otel" "backend/providers/otel"
. "github.com/go-jet/jet/v2/postgres" . "github.com/go-jet/jet/v2/postgres"
"github.com/samber/lo"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0" semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
@@ -28,6 +29,78 @@ func (svc *Service) Prepare() error {
return nil return nil
} }
// GetBoughtPosts
func (svc *Service) GetBoughtPosts(ctx context.Context, pagination *requests.Pagination, filter *UserPostFilter) ([]model.Posts, int64, error) {
_, span := otel.Start(ctx, "users.service.GetBoughtPosts")
defer span.End()
span.SetAttributes(
attribute.Int64("user.id", filter.UserID),
attribute.Int64("page.page", pagination.Page),
attribute.Int64("page.limit", pagination.Limit),
)
tbl := table.Posts
boughtIds, err := svc.GetUserBoughtIDs(ctx, filter.TenantID, filter.UserID)
if err != nil {
return nil, 0, err
}
if len(boughtIds) == 0 {
return nil, 0, nil
}
idExprs := lo.Map(boughtIds, func(id int64, _ int) Expression { return Int64(id) })
cond := tbl.ID.IN(
idExprs...,
).AND(
tbl.TenantID.EQ(Int64(filter.TenantID)),
).AND(
tbl.UserID.EQ(Int64(filter.UserID)),
)
if filter.CreatedAt != nil {
cond = cond.AND(tbl.CreatedAt.LT_EQ(TimestampT(*filter.CreatedAt)))
}
if filter.Keyword != nil {
cond = cond.AND(
tbl.Title.
LIKE(String(database.WrapLike(*filter.Keyword))).
OR(
tbl.Description.LIKE(String(database.WrapLike(*filter.Keyword))),
).
OR(
tbl.Content.LIKE(String(database.WrapLike(*filter.Keyword))),
),
)
}
cntStmt := tbl.SELECT(COUNT(tbl.ID).AS("cnt")).WHERE(cond)
span.SetAttributes(semconv.DBStatementKey.String(cntStmt.DebugSql()))
var count struct {
Cnt int64
}
if err := cntStmt.QueryContext(ctx, svc.db, &count); err != nil {
return nil, 0, err
}
stmt := tbl.
SELECT(tbl.AllColumns).
ORDER_BY(tbl.ID.DESC()).
LIMIT(pagination.Limit).
OFFSET(pagination.Offset())
span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql()))
var posts []model.Posts
if err := stmt.QueryContext(ctx, svc.db, &posts); err != nil {
return nil, 0, err
}
return posts, count.Cnt, nil
}
// GetPosts // GetPosts
func (svc *Service) GetPosts(ctx context.Context, pagination *requests.Pagination, filter *UserPostFilter) ([]model.Posts, int64, error) { func (svc *Service) GetPosts(ctx context.Context, pagination *requests.Pagination, filter *UserPostFilter) ([]model.Posts, int64, error) {
_, span := otel.Start(ctx, "users.service.GetPosts") _, span := otel.Start(ctx, "users.service.GetPosts")
@@ -89,3 +162,61 @@ func (svc *Service) GetPosts(ctx context.Context, pagination *requests.Paginatio
return posts, count.Cnt, nil return posts, count.Cnt, nil
} }
// GetPostByHash
func (svc *Service) GetPostByHash(ctx context.Context, tenantID int64, hash string) (*model.Posts, error) {
_, span := otel.Start(ctx, "users.service.GetPostByHash")
defer span.End()
span.SetAttributes(
attribute.String("hash", hash),
)
tbl := table.Posts
stmt := tbl.
SELECT(tbl.AllColumns).
WHERE(
tbl.Hash.EQ(String(hash)).AND(
tbl.TenantID.EQ(Int64(tenantID)),
),
)
span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql()))
var post model.Posts
if err := stmt.QueryContext(ctx, svc.db, &post); err != nil {
return nil, err
}
return &post, nil
}
// GetUserBoughtPosts
func (svc *Service) GetUserBoughtIDs(ctx context.Context, tenantID, userID int64) ([]int64, error) {
_, span := otel.Start(ctx, "users.service.GetUserBoughtIDs")
defer span.End()
span.SetAttributes(
attribute.Int64("tenant.id", tenantID),
attribute.Int64("user.id", userID),
)
tbl := table.UserBoughtPosts
stmt := tbl.
SELECT(tbl.PostID.AS("post_id")).
WHERE(
tbl.TenantID.EQ(Int64(tenantID)).AND(
tbl.UserID.EQ(Int64(userID)),
),
)
span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql()))
type tmp struct {
PostID int64
}
var results []tmp
if err := stmt.QueryContext(ctx, svc.db, &results); err != nil {
return nil, err
}
return lo.Map(results, func(item tmp, _ int) int64 {
return item.PostID
}), nil
}

View File

@@ -1,9 +1,6 @@
package tenants package tenants
import ( import (
"time"
"backend/app/consts"
"backend/providers/jwt" "backend/providers/jwt"
"backend/providers/otel" "backend/providers/otel"
@@ -40,20 +37,11 @@ func (c *Controller) Index(ctx fiber.Ctx, tenant string, claim *jwt.Claims) erro
return err return err
} }
if claim.TenantID == nil { // set tenant cookie
claim.TenantID = &tenantModel.ID
token, err := c.jwt.CreateToken(claim)
if err != nil {
return err
}
ctx.Cookie(&fiber.Cookie{ ctx.Cookie(&fiber.Cookie{
Name: consts.TokenTypeUser.String(), Name: "tenant",
Value: token, Value: tenantModel.Slug,
Expires: time.Now().Add(6 * time.Hour),
HTTPOnly: true,
}) })
}
// TODO: render page // TODO: render page
return nil return nil

View File

@@ -28,3 +28,13 @@ Salt = "LiXi.Y@140202"
Type = "local" Type = "local"
Path = "/mnt/yangpingliang/processed" Path = "/mnt/yangpingliang/processed"
Asset = "/projects/qvyun/frontend/dist" Asset = "/projects/qvyun/frontend/dist"
[Pay]
[Pay.WeChat]
AppId = "wx45745a8c51091ae0"
MechID = ""
SubMechID = ""
SerialNo = ""
ApiV3Key = ""
PrivateKey = ""

View File

@@ -0,0 +1,479 @@
// Code generated by go-enum DO NOT EDIT.
// Version: -
// Revision: -
// Build Date: -
// Built By: -
package fields
import (
"database/sql/driver"
"errors"
"fmt"
"strconv"
"strings"
)
const (
// OrderStatusPending is a OrderStatus of type Pending.
OrderStatusPending OrderStatus = iota
// OrderStatusPaid is a OrderStatus of type Paid.
OrderStatusPaid
// OrderStatusRefunding is a OrderStatus of type Refunding.
OrderStatusRefunding
// OrderStatusRefunded is a OrderStatus of type Refunded.
OrderStatusRefunded
// OrderStatusCancelled is a OrderStatus of type Cancelled.
OrderStatusCancelled
)
var ErrInvalidOrderStatus = fmt.Errorf("not a valid OrderStatus, try [%s]", strings.Join(_OrderStatusNames, ", "))
const _OrderStatusName = "PendingPaidRefundingRefundedCancelled"
var _OrderStatusNames = []string{
_OrderStatusName[0:7],
_OrderStatusName[7:11],
_OrderStatusName[11:20],
_OrderStatusName[20:28],
_OrderStatusName[28:37],
}
// OrderStatusNames returns a list of possible string values of OrderStatus.
func OrderStatusNames() []string {
tmp := make([]string, len(_OrderStatusNames))
copy(tmp, _OrderStatusNames)
return tmp
}
// OrderStatusValues returns a list of the values for OrderStatus
func OrderStatusValues() []OrderStatus {
return []OrderStatus{
OrderStatusPending,
OrderStatusPaid,
OrderStatusRefunding,
OrderStatusRefunded,
OrderStatusCancelled,
}
}
var _OrderStatusMap = map[OrderStatus]string{
OrderStatusPending: _OrderStatusName[0:7],
OrderStatusPaid: _OrderStatusName[7:11],
OrderStatusRefunding: _OrderStatusName[11:20],
OrderStatusRefunded: _OrderStatusName[20:28],
OrderStatusCancelled: _OrderStatusName[28:37],
}
// String implements the Stringer interface.
func (x OrderStatus) String() string {
if str, ok := _OrderStatusMap[x]; ok {
return str
}
return fmt.Sprintf("OrderStatus(%d)", x)
}
// IsValid provides a quick way to determine if the typed value is
// part of the allowed enumerated values
func (x OrderStatus) IsValid() bool {
_, ok := _OrderStatusMap[x]
return ok
}
var _OrderStatusValue = map[string]OrderStatus{
_OrderStatusName[0:7]: OrderStatusPending,
_OrderStatusName[7:11]: OrderStatusPaid,
_OrderStatusName[11:20]: OrderStatusRefunding,
_OrderStatusName[20:28]: OrderStatusRefunded,
_OrderStatusName[28:37]: OrderStatusCancelled,
}
// ParseOrderStatus attempts to convert a string to a OrderStatus.
func ParseOrderStatus(name string) (OrderStatus, error) {
if x, ok := _OrderStatusValue[name]; ok {
return x, nil
}
return OrderStatus(0), fmt.Errorf("%s is %w", name, ErrInvalidOrderStatus)
}
var errOrderStatusNilPtr = errors.New("value pointer is nil") // one per type for package clashes
// Scan implements the Scanner interface.
func (x *OrderStatus) Scan(value interface{}) (err error) {
if value == nil {
*x = OrderStatus(0)
return
}
// A wider range of scannable types.
// driver.Value values at the top of the list for expediency
switch v := value.(type) {
case int64:
*x = OrderStatus(v)
case string:
*x, err = ParseOrderStatus(v)
if err != nil {
// try parsing the integer value as a string
if val, verr := strconv.Atoi(v); verr == nil {
*x, err = OrderStatus(val), nil
}
}
case []byte:
*x, err = ParseOrderStatus(string(v))
if err != nil {
// try parsing the integer value as a string
if val, verr := strconv.Atoi(string(v)); verr == nil {
*x, err = OrderStatus(val), nil
}
}
case OrderStatus:
*x = v
case int:
*x = OrderStatus(v)
case *OrderStatus:
if v == nil {
return errOrderStatusNilPtr
}
*x = *v
case uint:
*x = OrderStatus(v)
case uint64:
*x = OrderStatus(v)
case *int:
if v == nil {
return errOrderStatusNilPtr
}
*x = OrderStatus(*v)
case *int64:
if v == nil {
return errOrderStatusNilPtr
}
*x = OrderStatus(*v)
case float64: // json marshals everything as a float64 if it's a number
*x = OrderStatus(v)
case *float64: // json marshals everything as a float64 if it's a number
if v == nil {
return errOrderStatusNilPtr
}
*x = OrderStatus(*v)
case *uint:
if v == nil {
return errOrderStatusNilPtr
}
*x = OrderStatus(*v)
case *uint64:
if v == nil {
return errOrderStatusNilPtr
}
*x = OrderStatus(*v)
case *string:
if v == nil {
return errOrderStatusNilPtr
}
*x, err = ParseOrderStatus(*v)
if err != nil {
// try parsing the integer value as a string
if val, verr := strconv.Atoi(*v); verr == nil {
*x, err = OrderStatus(val), nil
}
}
}
return
}
// Value implements the driver Valuer interface.
func (x OrderStatus) Value() (driver.Value, error) {
return int64(x), nil
}
// Set implements the Golang flag.Value interface func.
func (x *OrderStatus) Set(val string) error {
v, err := ParseOrderStatus(val)
*x = v
return err
}
// Get implements the Golang flag.Getter interface func.
func (x *OrderStatus) Get() interface{} {
return *x
}
// Type implements the github.com/spf13/pFlag Value interface.
func (x *OrderStatus) Type() string {
return "OrderStatus"
}
type NullOrderStatus struct {
OrderStatus OrderStatus
Valid bool
}
func NewNullOrderStatus(val interface{}) (x NullOrderStatus) {
x.Scan(val) // yes, we ignore this error, it will just be an invalid value.
return
}
// Scan implements the Scanner interface.
func (x *NullOrderStatus) Scan(value interface{}) (err error) {
if value == nil {
x.OrderStatus, x.Valid = OrderStatus(0), false
return
}
err = x.OrderStatus.Scan(value)
x.Valid = (err == nil)
return
}
// Value implements the driver Valuer interface.
func (x NullOrderStatus) Value() (driver.Value, error) {
if !x.Valid {
return nil, nil
}
// driver.Value accepts int64 for int values.
return int64(x.OrderStatus), nil
}
type NullOrderStatusStr struct {
NullOrderStatus
}
func NewNullOrderStatusStr(val interface{}) (x NullOrderStatusStr) {
x.Scan(val) // yes, we ignore this error, it will just be an invalid value.
return
}
// Value implements the driver Valuer interface.
func (x NullOrderStatusStr) Value() (driver.Value, error) {
if !x.Valid {
return nil, nil
}
return x.OrderStatus.String(), nil
}
const (
// OrderTypeCharge is a OrderType of type Charge.
OrderTypeCharge OrderType = iota
// OrderTypeConsume is a OrderType of type Consume.
OrderTypeConsume
// OrderTypeRefund is a OrderType of type Refund.
OrderTypeRefund
)
var ErrInvalidOrderType = fmt.Errorf("not a valid OrderType, try [%s]", strings.Join(_OrderTypeNames, ", "))
const _OrderTypeName = "ChargeConsumeRefund"
var _OrderTypeNames = []string{
_OrderTypeName[0:6],
_OrderTypeName[6:13],
_OrderTypeName[13:19],
}
// OrderTypeNames returns a list of possible string values of OrderType.
func OrderTypeNames() []string {
tmp := make([]string, len(_OrderTypeNames))
copy(tmp, _OrderTypeNames)
return tmp
}
// OrderTypeValues returns a list of the values for OrderType
func OrderTypeValues() []OrderType {
return []OrderType{
OrderTypeCharge,
OrderTypeConsume,
OrderTypeRefund,
}
}
var _OrderTypeMap = map[OrderType]string{
OrderTypeCharge: _OrderTypeName[0:6],
OrderTypeConsume: _OrderTypeName[6:13],
OrderTypeRefund: _OrderTypeName[13:19],
}
// String implements the Stringer interface.
func (x OrderType) String() string {
if str, ok := _OrderTypeMap[x]; ok {
return str
}
return fmt.Sprintf("OrderType(%d)", x)
}
// IsValid provides a quick way to determine if the typed value is
// part of the allowed enumerated values
func (x OrderType) IsValid() bool {
_, ok := _OrderTypeMap[x]
return ok
}
var _OrderTypeValue = map[string]OrderType{
_OrderTypeName[0:6]: OrderTypeCharge,
_OrderTypeName[6:13]: OrderTypeConsume,
_OrderTypeName[13:19]: OrderTypeRefund,
}
// ParseOrderType attempts to convert a string to a OrderType.
func ParseOrderType(name string) (OrderType, error) {
if x, ok := _OrderTypeValue[name]; ok {
return x, nil
}
return OrderType(0), fmt.Errorf("%s is %w", name, ErrInvalidOrderType)
}
var errOrderTypeNilPtr = errors.New("value pointer is nil") // one per type for package clashes
// Scan implements the Scanner interface.
func (x *OrderType) Scan(value interface{}) (err error) {
if value == nil {
*x = OrderType(0)
return
}
// A wider range of scannable types.
// driver.Value values at the top of the list for expediency
switch v := value.(type) {
case int64:
*x = OrderType(v)
case string:
*x, err = ParseOrderType(v)
if err != nil {
// try parsing the integer value as a string
if val, verr := strconv.Atoi(v); verr == nil {
*x, err = OrderType(val), nil
}
}
case []byte:
*x, err = ParseOrderType(string(v))
if err != nil {
// try parsing the integer value as a string
if val, verr := strconv.Atoi(string(v)); verr == nil {
*x, err = OrderType(val), nil
}
}
case OrderType:
*x = v
case int:
*x = OrderType(v)
case *OrderType:
if v == nil {
return errOrderTypeNilPtr
}
*x = *v
case uint:
*x = OrderType(v)
case uint64:
*x = OrderType(v)
case *int:
if v == nil {
return errOrderTypeNilPtr
}
*x = OrderType(*v)
case *int64:
if v == nil {
return errOrderTypeNilPtr
}
*x = OrderType(*v)
case float64: // json marshals everything as a float64 if it's a number
*x = OrderType(v)
case *float64: // json marshals everything as a float64 if it's a number
if v == nil {
return errOrderTypeNilPtr
}
*x = OrderType(*v)
case *uint:
if v == nil {
return errOrderTypeNilPtr
}
*x = OrderType(*v)
case *uint64:
if v == nil {
return errOrderTypeNilPtr
}
*x = OrderType(*v)
case *string:
if v == nil {
return errOrderTypeNilPtr
}
*x, err = ParseOrderType(*v)
if err != nil {
// try parsing the integer value as a string
if val, verr := strconv.Atoi(*v); verr == nil {
*x, err = OrderType(val), nil
}
}
}
return
}
// Value implements the driver Valuer interface.
func (x OrderType) Value() (driver.Value, error) {
return int64(x), nil
}
// Set implements the Golang flag.Value interface func.
func (x *OrderType) Set(val string) error {
v, err := ParseOrderType(val)
*x = v
return err
}
// Get implements the Golang flag.Getter interface func.
func (x *OrderType) Get() interface{} {
return *x
}
// Type implements the github.com/spf13/pFlag Value interface.
func (x *OrderType) Type() string {
return "OrderType"
}
type NullOrderType struct {
OrderType OrderType
Valid bool
}
func NewNullOrderType(val interface{}) (x NullOrderType) {
x.Scan(val) // yes, we ignore this error, it will just be an invalid value.
return
}
// Scan implements the Scanner interface.
func (x *NullOrderType) Scan(value interface{}) (err error) {
if value == nil {
x.OrderType, x.Valid = OrderType(0), false
return
}
err = x.OrderType.Scan(value)
x.Valid = (err == nil)
return
}
// Value implements the driver Valuer interface.
func (x NullOrderType) Value() (driver.Value, error) {
if !x.Valid {
return nil, nil
}
// driver.Value accepts int64 for int values.
return int64(x.OrderType), nil
}
type NullOrderTypeStr struct {
NullOrderType
}
func NewNullOrderTypeStr(val interface{}) (x NullOrderTypeStr) {
x.Scan(val) // yes, we ignore this error, it will just be an invalid value.
return
}
// Value implements the driver Valuer interface.
func (x NullOrderTypeStr) Value() (driver.Value, error) {
if !x.Valid {
return nil, nil
}
return x.OrderType.String(), nil
}

View File

@@ -0,0 +1,19 @@
package fields
// swagger:enum OrderType
// ENUM( Charge, Consume, Refund)
type OrderType int16
// swagger:enum OrderStatus
// ENUM( Pending, Paid, Refunding, Refunded, Cancelled)
type OrderStatus int16
type OrderMeta struct {
ObjectID int64 `json:"object_id"`
Price int64 `json:"price"`
Discount int16 `json:"discount"`
Coupons []struct {
ID int64 `json:"id"`
Description string `json:"description"`
} `json:"coupons"`
}

View File

@@ -11,7 +11,7 @@ CREATE TABLE
tenant_id INT8 NOT NULL, tenant_id INT8 NOT NULL,
user_id INT8 NOT NULL, user_id INT8 NOT NULL,
hash_id VARCHAR(128) NOT NULL, hash VARCHAR(128) NOT NULL UNIQUE,
title VARCHAR(128) NOT NULL, title VARCHAR(128) NOT NULL,
description VARCHAR(256) NOT NULL, description VARCHAR(256) NOT NULL,
poster VARCHAR(128) NOT NULL, poster VARCHAR(128) NOT NULL,
@@ -25,6 +25,31 @@ CREATE TABLE
meta jsonb default '{}'::jsonb, meta jsonb default '{}'::jsonb,
assets jsonb default '{}'::jsonb assets jsonb default '{}'::jsonb
); );
-- create indexes
CREATE INDEX posts_tenant_id_index ON posts (tenant_id);
CREATE INDEX posts_user_id_index ON posts (user_id);
CREATE INDEX posts_title_index ON posts (title);
-- create user bought posts
CREATE TABLE
user_bought_posts (
id SERIAL8 PRIMARY KEY,
created_at timestamp NOT NULL default now(),
updated_at timestamp NOT NULL default now(),
tenant_id INT8 NOT NULL,
user_id INT8 NOT NULL,
post_id INT8 NOT NULL,
price INT8 NOT NULL default 0,
discount INT2 NOT NULL default 100,
meta jsonb default '{}'::jsonb
);
-- create indexes
CREATE INDEX user_bought_posts_tenant_id_index ON user_bought_posts (tenant_id);
CREATE INDEX user_bought_posts_user_id_index ON user_bought_posts (user_id);
CREATE INDEX user_bought_posts_post_id_index ON user_bought_posts (post_id);
-- +goose StatementEnd -- +goose StatementEnd
-- +goose Down -- +goose Down

View File

@@ -6,6 +6,7 @@ CREATE TABLE medias (
created_at timestamp NOT NULL default now(), created_at timestamp NOT NULL default now(),
updated_at timestamp NOT NULL default now(), updated_at timestamp NOT NULL default now(),
tenant_id INT8 NOT NULL,
user_id INT8 NOT NULL, user_id INT8 NOT NULL,
post_id INT8 NOT NULL, post_id INT8 NOT NULL,
storage_id INT8 NOT NULL, storage_id INT8 NOT NULL,
@@ -15,6 +16,11 @@ CREATE TABLE medias (
size INT8 NOT NULL default 0, size INT8 NOT NULL default 0,
path VARCHAR(255) NOT NULL default '' path VARCHAR(255) NOT NULL default ''
); );
CREATE INDEX medias_tenant_id_index ON medias (tenant_id);
CREATE INDEX medias_user_id_index ON medias (user_id);
CREATE INDEX medias_post_id_index ON medias (post_id);
CREATE INDEX medias_storage_id_index ON medias (storage_id);
-- +goose StatementEnd -- +goose StatementEnd
-- +goose Down -- +goose Down

View File

@@ -15,6 +15,7 @@ type Medias struct {
ID int64 `sql:"primary_key" json:"id"` ID int64 `sql:"primary_key" json:"id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
PostID int64 `json:"post_id"` PostID int64 `json:"post_id"`
StorageID int64 `json:"storage_id"` StorageID int64 `json:"storage_id"`

View File

@@ -8,6 +8,7 @@
package model package model
import ( import (
"backend/database/fields"
"time" "time"
) )
@@ -18,8 +19,8 @@ type Orders struct {
DeletedAt *time.Time `json:"deleted_at"` DeletedAt *time.Time `json:"deleted_at"`
TenantID int64 `json:"tenant_id"` TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Type int16 `json:"type"` Type fields.OrderType `json:"type"`
Status int16 `json:"status"` Status fields.OrderStatus `json:"status"`
OrderSerial string `json:"order_serial"` OrderSerial string `json:"order_serial"`
RemoteOrderSerial string `json:"remote_order_serial"` RemoteOrderSerial string `json:"remote_order_serial"`
RefundSerial string `json:"refund_serial"` RefundSerial string `json:"refund_serial"`
@@ -28,5 +29,5 @@ type Orders struct {
Currency string `json:"currency"` Currency string `json:"currency"`
Title string `json:"title"` Title string `json:"title"`
Description *string `json:"description"` Description *string `json:"description"`
Meta *string `json:"meta"` Meta fields.Json[fields.OrderMeta] `json:"meta"`
} }

View File

@@ -18,7 +18,7 @@ type Posts struct {
DeletedAt *time.Time `json:"deleted_at"` DeletedAt *time.Time `json:"deleted_at"`
TenantID int64 `json:"tenant_id"` TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
HashID string `json:"hash_id"` Hash string `json:"hash"`
Title string `json:"title"` Title string `json:"title"`
Description string `json:"description"` Description string `json:"description"`
Poster string `json:"poster"` Poster string `json:"poster"`

View File

@@ -0,0 +1,24 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"time"
)
type UserBoughtPosts struct {
ID int64 `sql:"primary_key" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"`
PostID int64 `json:"post_id"`
Price int64 `json:"price"`
Discount int16 `json:"discount"`
Meta *string `json:"meta"`
}

View File

@@ -20,6 +20,7 @@ type mediasTable struct {
ID postgres.ColumnInteger ID postgres.ColumnInteger
CreatedAt postgres.ColumnTimestamp CreatedAt postgres.ColumnTimestamp
UpdatedAt postgres.ColumnTimestamp UpdatedAt postgres.ColumnTimestamp
TenantID postgres.ColumnInteger
UserID postgres.ColumnInteger UserID postgres.ColumnInteger
PostID postgres.ColumnInteger PostID postgres.ColumnInteger
StorageID postgres.ColumnInteger StorageID postgres.ColumnInteger
@@ -71,6 +72,7 @@ func newMediasTableImpl(schemaName, tableName, alias string) mediasTable {
IDColumn = postgres.IntegerColumn("id") IDColumn = postgres.IntegerColumn("id")
CreatedAtColumn = postgres.TimestampColumn("created_at") CreatedAtColumn = postgres.TimestampColumn("created_at")
UpdatedAtColumn = postgres.TimestampColumn("updated_at") UpdatedAtColumn = postgres.TimestampColumn("updated_at")
TenantIDColumn = postgres.IntegerColumn("tenant_id")
UserIDColumn = postgres.IntegerColumn("user_id") UserIDColumn = postgres.IntegerColumn("user_id")
PostIDColumn = postgres.IntegerColumn("post_id") PostIDColumn = postgres.IntegerColumn("post_id")
StorageIDColumn = postgres.IntegerColumn("storage_id") StorageIDColumn = postgres.IntegerColumn("storage_id")
@@ -79,8 +81,8 @@ func newMediasTableImpl(schemaName, tableName, alias string) mediasTable {
MimeTypeColumn = postgres.StringColumn("mime_type") MimeTypeColumn = postgres.StringColumn("mime_type")
SizeColumn = postgres.IntegerColumn("size") SizeColumn = postgres.IntegerColumn("size")
PathColumn = postgres.StringColumn("path") PathColumn = postgres.StringColumn("path")
allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, UserIDColumn, PostIDColumn, StorageIDColumn, NameColumn, UUIDColumn, MimeTypeColumn, SizeColumn, PathColumn} allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, TenantIDColumn, UserIDColumn, PostIDColumn, StorageIDColumn, NameColumn, UUIDColumn, MimeTypeColumn, SizeColumn, PathColumn}
mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, UserIDColumn, PostIDColumn, StorageIDColumn, NameColumn, UUIDColumn, MimeTypeColumn, SizeColumn, PathColumn} mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, TenantIDColumn, UserIDColumn, PostIDColumn, StorageIDColumn, NameColumn, UUIDColumn, MimeTypeColumn, SizeColumn, PathColumn}
) )
return mediasTable{ return mediasTable{
@@ -90,6 +92,7 @@ func newMediasTableImpl(schemaName, tableName, alias string) mediasTable {
ID: IDColumn, ID: IDColumn,
CreatedAt: CreatedAtColumn, CreatedAt: CreatedAtColumn,
UpdatedAt: UpdatedAtColumn, UpdatedAt: UpdatedAtColumn,
TenantID: TenantIDColumn,
UserID: UserIDColumn, UserID: UserIDColumn,
PostID: PostIDColumn, PostID: PostIDColumn,
StorageID: StorageIDColumn, StorageID: StorageIDColumn,

View File

@@ -23,7 +23,7 @@ type postsTable struct {
DeletedAt postgres.ColumnTimestamp DeletedAt postgres.ColumnTimestamp
TenantID postgres.ColumnInteger TenantID postgres.ColumnInteger
UserID postgres.ColumnInteger UserID postgres.ColumnInteger
HashID postgres.ColumnString Hash postgres.ColumnString
Title postgres.ColumnString Title postgres.ColumnString
Description postgres.ColumnString Description postgres.ColumnString
Poster postgres.ColumnString Poster postgres.ColumnString
@@ -82,7 +82,7 @@ func newPostsTableImpl(schemaName, tableName, alias string) postsTable {
DeletedAtColumn = postgres.TimestampColumn("deleted_at") DeletedAtColumn = postgres.TimestampColumn("deleted_at")
TenantIDColumn = postgres.IntegerColumn("tenant_id") TenantIDColumn = postgres.IntegerColumn("tenant_id")
UserIDColumn = postgres.IntegerColumn("user_id") UserIDColumn = postgres.IntegerColumn("user_id")
HashIDColumn = postgres.StringColumn("hash_id") HashColumn = postgres.StringColumn("hash")
TitleColumn = postgres.StringColumn("title") TitleColumn = postgres.StringColumn("title")
DescriptionColumn = postgres.StringColumn("description") DescriptionColumn = postgres.StringColumn("description")
PosterColumn = postgres.StringColumn("poster") PosterColumn = postgres.StringColumn("poster")
@@ -95,8 +95,8 @@ func newPostsTableImpl(schemaName, tableName, alias string) postsTable {
LikesColumn = postgres.IntegerColumn("likes") LikesColumn = postgres.IntegerColumn("likes")
MetaColumn = postgres.StringColumn("meta") MetaColumn = postgres.StringColumn("meta")
AssetsColumn = postgres.StringColumn("assets") AssetsColumn = postgres.StringColumn("assets")
allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, TenantIDColumn, UserIDColumn, HashIDColumn, TitleColumn, DescriptionColumn, PosterColumn, ContentColumn, StageColumn, StatusColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, MetaColumn, AssetsColumn} allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, TenantIDColumn, UserIDColumn, HashColumn, TitleColumn, DescriptionColumn, PosterColumn, ContentColumn, StageColumn, StatusColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, MetaColumn, AssetsColumn}
mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, TenantIDColumn, UserIDColumn, HashIDColumn, TitleColumn, DescriptionColumn, PosterColumn, ContentColumn, StageColumn, StatusColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, MetaColumn, AssetsColumn} mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, TenantIDColumn, UserIDColumn, HashColumn, TitleColumn, DescriptionColumn, PosterColumn, ContentColumn, StageColumn, StatusColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, MetaColumn, AssetsColumn}
) )
return postsTable{ return postsTable{
@@ -109,7 +109,7 @@ func newPostsTableImpl(schemaName, tableName, alias string) postsTable {
DeletedAt: DeletedAtColumn, DeletedAt: DeletedAtColumn,
TenantID: TenantIDColumn, TenantID: TenantIDColumn,
UserID: UserIDColumn, UserID: UserIDColumn,
HashID: HashIDColumn, Hash: HashColumn,
Title: TitleColumn, Title: TitleColumn,
Description: DescriptionColumn, Description: DescriptionColumn,
Poster: PosterColumn, Poster: PosterColumn,

View File

@@ -23,6 +23,7 @@ func UseSchema(schema string) {
TenantUserBalances = TenantUserBalances.FromSchema(schema) TenantUserBalances = TenantUserBalances.FromSchema(schema)
TenantUsers = TenantUsers.FromSchema(schema) TenantUsers = TenantUsers.FromSchema(schema)
Tenants = Tenants.FromSchema(schema) Tenants = Tenants.FromSchema(schema)
UserBoughtPosts = UserBoughtPosts.FromSchema(schema)
UserOauths = UserOauths.FromSchema(schema) UserOauths = UserOauths.FromSchema(schema)
Users = Users.FromSchema(schema) Users = Users.FromSchema(schema)
} }

View File

@@ -0,0 +1,99 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var UserBoughtPosts = newUserBoughtPostsTable("public", "user_bought_posts", "")
type userBoughtPostsTable struct {
postgres.Table
// Columns
ID postgres.ColumnInteger
CreatedAt postgres.ColumnTimestamp
UpdatedAt postgres.ColumnTimestamp
TenantID postgres.ColumnInteger
UserID postgres.ColumnInteger
PostID postgres.ColumnInteger
Price postgres.ColumnInteger
Discount postgres.ColumnInteger
Meta postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UserBoughtPostsTable struct {
userBoughtPostsTable
EXCLUDED userBoughtPostsTable
}
// AS creates new UserBoughtPostsTable with assigned alias
func (a UserBoughtPostsTable) AS(alias string) *UserBoughtPostsTable {
return newUserBoughtPostsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserBoughtPostsTable with assigned schema name
func (a UserBoughtPostsTable) FromSchema(schemaName string) *UserBoughtPostsTable {
return newUserBoughtPostsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserBoughtPostsTable with assigned table prefix
func (a UserBoughtPostsTable) WithPrefix(prefix string) *UserBoughtPostsTable {
return newUserBoughtPostsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserBoughtPostsTable with assigned table suffix
func (a UserBoughtPostsTable) WithSuffix(suffix string) *UserBoughtPostsTable {
return newUserBoughtPostsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserBoughtPostsTable(schemaName, tableName, alias string) *UserBoughtPostsTable {
return &UserBoughtPostsTable{
userBoughtPostsTable: newUserBoughtPostsTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserBoughtPostsTableImpl("", "excluded", ""),
}
}
func newUserBoughtPostsTableImpl(schemaName, tableName, alias string) userBoughtPostsTable {
var (
IDColumn = postgres.IntegerColumn("id")
CreatedAtColumn = postgres.TimestampColumn("created_at")
UpdatedAtColumn = postgres.TimestampColumn("updated_at")
TenantIDColumn = postgres.IntegerColumn("tenant_id")
UserIDColumn = postgres.IntegerColumn("user_id")
PostIDColumn = postgres.IntegerColumn("post_id")
PriceColumn = postgres.IntegerColumn("price")
DiscountColumn = postgres.IntegerColumn("discount")
MetaColumn = postgres.StringColumn("meta")
allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, TenantIDColumn, UserIDColumn, PostIDColumn, PriceColumn, DiscountColumn, MetaColumn}
mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, TenantIDColumn, UserIDColumn, PostIDColumn, PriceColumn, DiscountColumn, MetaColumn}
)
return userBoughtPostsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
CreatedAt: CreatedAtColumn,
UpdatedAt: UpdatedAtColumn,
TenantID: TenantIDColumn,
UserID: UserIDColumn,
PostID: PostIDColumn,
Price: PriceColumn,
Discount: DiscountColumn,
Meta: MetaColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@@ -14,3 +14,8 @@ types:
storages: storages:
type: StorageType type: StorageType
orders:
type: OrderType
status: OrderStatus
meta: Json[OrderMeta]

View File

@@ -10,6 +10,7 @@ require (
github.com/ThreeDotsLabs/watermill-redisstream v1.4.2 github.com/ThreeDotsLabs/watermill-redisstream v1.4.2
github.com/ThreeDotsLabs/watermill-sql/v3 v3.1.0 github.com/ThreeDotsLabs/watermill-sql/v3 v3.1.0
github.com/go-jet/jet/v2 v2.12.0 github.com/go-jet/jet/v2 v2.12.0
github.com/go-pay/gopay v1.5.107
github.com/gofiber/fiber/v3 v3.0.0-beta.4 github.com/gofiber/fiber/v3 v3.0.0-beta.4
github.com/gofiber/utils/v2 v2.0.0-beta.7 github.com/gofiber/utils/v2 v2.0.0-beta.7
github.com/golang-jwt/jwt/v4 v4.5.1 github.com/golang-jwt/jwt/v4 v4.5.1
@@ -81,6 +82,12 @@ require (
github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-pay/crypto v0.0.1 // indirect
github.com/go-pay/errgroup v0.0.3 // indirect
github.com/go-pay/smap v0.0.2 // indirect
github.com/go-pay/util v0.0.4 // indirect
github.com/go-pay/xlog v0.0.3 // indirect
github.com/go-pay/xtime v0.0.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gofiber/schema v1.2.0 // indirect github.com/gofiber/schema v1.2.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect

View File

@@ -83,6 +83,20 @@ github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-pay/crypto v0.0.1 h1:B6InT8CLfSLc6nGRVx9VMJRBBazFMjr293+jl0lLXUY=
github.com/go-pay/crypto v0.0.1/go.mod h1:41oEIvHMKbNcYlWUlRWtsnC6+ASgh7u29z0gJXe5bes=
github.com/go-pay/errgroup v0.0.3 h1:DB4s8e8oWYDyETKQ1y1riMJ7y29zE1uIsMCSjEOFSbU=
github.com/go-pay/errgroup v0.0.3/go.mod h1:0+4b8mvFMS71MIzsaC+gVvB4x37I93lRb2dqrwuU8x8=
github.com/go-pay/gopay v1.5.107 h1:BZauJyTijvvb2AIMJN0SqOWwjPzssmYkTsjn0NME4P4=
github.com/go-pay/gopay v1.5.107/go.mod h1:Kwv8YPKh9StrJAYMDgnoTkCrPW7f9/O+XgWWlCLvOKc=
github.com/go-pay/smap v0.0.2 h1:kKflYor5T5FgZltPFBMTFfjJvqYMHr5VnIFSEyhVTcA=
github.com/go-pay/smap v0.0.2/go.mod h1:HW9oAo0okuyDYsbpbj5fJFxnNj/BZorRGFw26SxrNWw=
github.com/go-pay/util v0.0.4 h1:TuwSU9o3Qd7m9v1PbzFuIA/8uO9FJnA6P7neG/NwPyk=
github.com/go-pay/util v0.0.4/go.mod h1:Tsdhs8Ib9J9b4+NKNO1PHh5hWHhlg98PthsX0ckq6PM=
github.com/go-pay/xlog v0.0.3 h1:avyMhCL/JgBHreoGx/am/kHxfs1udDOAeVqbmzP/Yes=
github.com/go-pay/xlog v0.0.3/go.mod h1:mH47xbobrdsSHWsmFtSF5agWbMHFP+tK0ZbVCk5OAEw=
github.com/go-pay/xtime v0.0.2 h1:7YR4/iuELsEHpJ6LUO0SVK80hQxDO9MLCfuVYIiTCRM=
github.com/go-pay/xtime v0.0.2/go.mod h1:W1yRbJaSt4CSBcdAtLBQ8xajiN/Pl5hquGczUcUE9xE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=

View File

@@ -0,0 +1,15 @@
package utils
import (
"fmt"
"math/rand"
"strings"
"time"
)
// GenerateOrderSerial generate order serial
func GenerateOrderSerial(prefix string) string {
timestamp := time.Now().Format("20060102150405")
randomSuffix := rand.Intn(10000)
return fmt.Sprintf("%s%s%04d", strings.ToUpper(prefix), timestamp, randomSuffix)
}

View File

@@ -0,0 +1,30 @@
package pay
import (
"git.ipao.vip/rogeecn/atom/container"
"git.ipao.vip/rogeecn/atom/utils/opt"
)
const DefaultPrefix = "Pay"
func DefaultProvider() container.ProviderContainer {
return container.ProviderContainer{
Provider: Provide,
Options: []opt.Option{
opt.Prefix(DefaultPrefix),
},
}
}
type Config struct {
WeChat *wechatPay
}
type wechatPay struct {
AppId string
MechID string
SubMechID string
SerialNo string
ApiV3Key string
PrivateKey string
}

View File

@@ -0,0 +1,48 @@
package pay
import (
"backend/providers/app"
"git.ipao.vip/rogeecn/atom/container"
"git.ipao.vip/rogeecn/atom/utils/opt"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/wechat/v3"
)
func Provide(opts ...opt.Option) error {
o := opt.New(opts...)
var config Config
if err := o.UnmarshalConfig(&config); err != nil {
return err
}
return container.Container.Provide(func(app *app.Config) (*Client, error) {
wechatPay, err := wechat.NewClientV3(
config.WeChat.MechID,
config.WeChat.SerialNo,
config.WeChat.ApiV3Key,
config.WeChat.PrivateKey,
)
if err != nil {
return nil, err
}
if err := wechatPay.AutoVerifySign(); err != nil {
return nil, err
}
if app.IsDevMode() {
wechatPay.DebugSwitch = gopay.DebugOn
}
return &Client{
conf: &config,
WeChat: wechatPay,
}, nil
}, o.DiOptions()...)
}
type Client struct {
conf *Config
WeChat *wechat.ClientV3
}

View File

@@ -0,0 +1,49 @@
package pay
import (
"context"
"errors"
"time"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/wechat/v3"
)
// get js pay prepay id
func (client *Client) WeChat_JSApiPayRequest(ctx context.Context, payerOpenID, orderNo, title string, price, amount int64, notifyUrl string) (*wechat.JSAPIPayParams, error) {
expire := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
// 初始化 BodyMap
bm := make(gopay.BodyMap)
bm.
Set("sp_appid", client.conf.WeChat.AppId).
Set("sp_mchid", client.conf.WeChat.MechID).
Set("sub_mchid", client.conf.WeChat.SubMechID).
Set("description", title).
Set("out_trade_no", orderNo).
Set("time_expire", expire).
Set("notify_url", notifyUrl).
SetBodyMap("amount", func(bm gopay.BodyMap) {
if amount == 0 {
amount = 1
}
bm.
Set("total", amount).
Set("currency", "CNY")
}).
SetBodyMap("payer", func(bm gopay.BodyMap) {
bm.Set("sp_openid", payerOpenID)
})
resp, err := client.WeChat.V3TransactionJsapi(ctx, bm)
if err != nil {
return nil, err
}
if resp.Code != 0 {
return nil, errors.New("获取预支付ID失败")
}
return client.WeChat.PaySignOfJSAPI(client.conf.WeChat.AppId, resp.Response.PrepayId)
}