diff --git a/backend/app/http/orders.go b/backend/app/http/orders.go new file mode 100644 index 0000000..d02cfda --- /dev/null +++ b/backend/app/http/orders.go @@ -0,0 +1 @@ +package http diff --git a/backend/app/http/posts.go b/backend/app/http/posts.go index 1ec3e94..fb34b15 100644 --- a/backend/app/http/posts.go +++ b/backend/app/http/posts.go @@ -1,12 +1,16 @@ package http import ( + "time" + "quyun/app/models" "quyun/app/requests" "quyun/database/schemas/public/model" + "quyun/providers/wepay" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/log" + "github.com/pkg/errors" ) type ListQuery struct { @@ -14,7 +18,9 @@ type ListQuery struct { } // @provider -type posts struct{} +type posts struct { + wepay *wepay.Client +} // List posts // @Router /posts [get] @@ -40,3 +46,40 @@ func (ctl *posts) Mine(ctx fiber.Ctx, pagination *requests.Pagination, query *Li log.Infof("Fetching posts for user with pagination: %+v and keyword: %v", pagination, query.Keyword) return models.Users.PostList(ctx.Context(), 1, pagination, query.Keyword) } + +// Buy +// @Router /buy/:id [get] +// @Bind id path +func (ctl *posts) Buy(ctx fiber.Ctx, id int64) (*string, error) { + var userId int64 = 1 + + user, err := models.Users.GetByID(ctx.Context(), userId) + if err != nil { + return nil, errors.Wrapf(err, " failed to get user: %d", userId) + } + + post, err := models.Posts.GetByID(ctx.Context(), id) + if err != nil { + return nil, errors.Wrapf(err, " failed to get post: %d", id) + } + + // create order + order, err := models.Orders.Create(ctx.Context(), userId, post.ID) + if err != nil { + return nil, errors.Wrap(err, "订单创建失败") + } + + body := ctl.wepay. + BodyMap(). + Expire(30 * time.Minute). + Description(post.Title). + OutTradeNo(order.OrderNo). + Payer(user.OpenID) + + prePayResp, err := ctl.wepay.V3TransactionJsapi(ctx.Context(), body) + if err != nil { + log.Errorf("wepay.V3TransactionJsapi err: %v", err) + return nil, errors.Wrap(err, "微信支付失败") + } + return &prePayResp.Response.PrepayId, nil +} diff --git a/backend/app/service/http/http.go b/backend/app/service/http/http.go index 80b402f..246cdef 100644 --- a/backend/app/service/http/http.go +++ b/backend/app/service/http/http.go @@ -17,6 +17,7 @@ import ( "quyun/providers/job" "quyun/providers/jwt" "quyun/providers/postgres" + "quyun/providers/wepay" "go.ipao.vip/atom" "go.ipao.vip/atom/container" @@ -31,6 +32,7 @@ import ( func defaultProviders() container.Providers { return service.Default(container.Providers{ ali.DefaultProvider(), + wepay.DefaultProvider(), http.DefaultProvider(), postgres.DefaultProvider(), jwt.DefaultProvider(), diff --git a/backend/go.mod b/backend/go.mod index 2cdae7b..b329388 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -11,6 +11,7 @@ require ( github.com/ThreeDotsLabs/watermill-sql/v3 v3.1.0 github.com/aliyun/credentials-go v1.4.5 github.com/go-jet/jet/v2 v2.13.0 + github.com/go-pay/gopay v1.5.110 github.com/gofiber/fiber/v3 v3.0.0-beta.4 github.com/gofiber/utils/v2 v2.0.0-beta.7 github.com/golang-jwt/jwt/v4 v4.5.1 @@ -82,6 +83,12 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // 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-viper/mapstructure/v2 v2.2.1 // indirect github.com/gofiber/schema v1.2.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 24d1815..020f7da 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -82,6 +82,20 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +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.110 h1:K+S1goJu5235Bi94uxD0VIQXCOiC7taw5QkCBc2J7PE= +github.com/go-pay/gopay v1.5.110/go.mod h1:v2VLAEV2NI6SIRS3Qpyi826pBwlWthHvP/ZVVTjqxwU= +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.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= diff --git a/backend/providers/wepay/config.go b/backend/providers/wepay/config.go new file mode 100644 index 0000000..8a300a6 --- /dev/null +++ b/backend/providers/wepay/config.go @@ -0,0 +1,28 @@ +package wepay + +import ( + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" +) + +const DefaultPrefix = "WePay" + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +type Config struct { + Debug bool + NotifyURL string + + AppId string + MchId string + SerialNo string + APIv3Key string + PrivateKey string +} diff --git a/backend/providers/wepay/pay.go b/backend/providers/wepay/pay.go new file mode 100644 index 0000000..cfb2462 --- /dev/null +++ b/backend/providers/wepay/pay.go @@ -0,0 +1,127 @@ +package wepay + +import ( + "context" + "errors" + "time" + + "github.com/go-pay/gopay" + "github.com/go-pay/gopay/wechat/v3" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/opt" +) + +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() (*Client, error) { + // NewClientV3 初始化微信客户端 v3 + // mchid:商户ID 或者服务商模式的 sp_mchid + // serialNo:商户证书的证书序列号 + // apiV3Key:apiV3Key,商户平台获取 + // privateKey:私钥 apiclient_key.pem 读取后的内容 + client, err := wechat.NewClientV3(config.MchId, config.SerialNo, config.APIv3Key, config.PrivateKey) + if err != nil { + return nil, err + } + + client.DebugSwitch = gopay.DebugOff + if config.Debug { + client.DebugSwitch = gopay.DebugOn + } + + return &Client{ + payClient: client, + config: &config, + }, nil + }, o.DiOptions()...) +} + +type Client struct { + payClient *wechat.ClientV3 + config *Config +} + +func (c *Client) GetClient() *wechat.ClientV3 { + return c.payClient +} + +func (c *Client) V3TransactionJsapi(ctx context.Context, bm *BodyMap) (*wechat.PrepayRsp, error) { + resp, err := c.payClient.V3TransactionJsapi(ctx, bm.bm) + if err != nil { + return nil, err + } + + if resp.Code != wechat.Success { + return nil, errors.New(resp.Error) + } + + return resp, nil +} + +func (c *Client) BodyMap() *BodyMap { return NewBodyMap(c.config) } + +type BodyMap struct { + bm gopay.BodyMap +} + +func NewBodyMap(c *Config) *BodyMap { + bm := make(gopay.BodyMap) + bm.Set("sp_appid", c.AppId). + Set("sp_mchid", c.MchId). + Set("notify_url", c.NotifyURL). + Set("amount", func(bm gopay.BodyMap) { + bm.Set("total", 1). + Set("currency", "CNY") + }) + return &BodyMap{ + bm: bm, + } +} + +func (b *BodyMap) Set(key string, value interface{}) *BodyMap { + b.bm.Set(key, value) + return b +} + +func (b *BodyMap) SetBodyMap(key string, f func(bm gopay.BodyMap)) *BodyMap { + b.bm.SetBodyMap(key, f) + return b +} + +// Expire time +func (b *BodyMap) Expire(t time.Duration) *BodyMap { + return b.Set("time_expire", time.Now().Add(t).Format(time.RFC3339)) +} + +// Description +func (b *BodyMap) Description(desc string) *BodyMap { + return b.Set("description", desc) +} + +// OutTradeNo +func (b *BodyMap) OutTradeNo(outTradeNo string) *BodyMap { + return b.Set("out_trade_no", outTradeNo) +} + +// Amount +func (b *BodyMap) Amount(total int, currency string) *BodyMap { + return b.SetBodyMap("amount", func(bm gopay.BodyMap) { + bm.Set("total", total).Set("currency", currency) + }) +} + +// Payer +func (b *BodyMap) Payer(spOpenId string) *BodyMap { + return b.SetBodyMap("payer", func(bm gopay.BodyMap) { + bm.Set("sp_openid", spOpenId) + }) +} + +// SubMchId +func (b *BodyMap) SubMchId(subMchId string) *BodyMap { + return b.Set("sub_mchid", subMchId) +}