From f6a97545c5ea6a32cd3514f2891deea2067325aa Mon Sep 17 00:00:00 2001 From: luorijun Date: Thu, 17 Apr 2025 18:29:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=BE=AE=E4=BF=A1=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E6=94=AF=E6=8C=81=EF=BC=8C=E9=87=8D=E6=9E=84=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E5=88=9B=E5=BB=BA=E9=80=BB=E8=BE=91=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=94=AF=E4=BB=98=E5=AE=9D=E7=9B=B8=E5=85=B3=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=EF=BC=8C=E7=A7=BB=E9=99=A4=E8=B4=A6=E5=8D=95=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 2 + cmd/playground/main.go | 6 +- go.mod | 1 + go.sum | 4 + pkg/env/env.go | 77 +++++++- scripts/sql/init.sql | 3 - web/globals/alipay.go | 2 +- web/globals/wechat.go | 47 +++++ web/handlers/bill.go | 4 - web/handlers/channel.go | 124 ------------- web/handlers/resource.go | 376 ++++++--------------------------------- web/handlers/temp.go | 22 --- web/handlers/trade.go | 117 ++++++++++++ web/models/bill.gen.go | 1 - web/queries/bill.gen.go | 6 +- web/router.go | 6 +- web/services/id.go | 9 +- web/services/resource.go | 293 ++++++++++++++++++++++++++++++ web/web.go | 1 + 20 files changed, 607 insertions(+), 495 deletions(-) create mode 100644 web/globals/wechat.go delete mode 100644 web/handlers/temp.go create mode 100644 web/handlers/trade.go create mode 100644 web/services/resource.go diff --git a/.gitignore b/.gitignore index 8f5fe8d..461a6d2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ bin/ +*.pem *.http \ No newline at end of file diff --git a/README.md b/README.md index bce5fb3..0c9fe57 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ - [ ] Limiter - [ ] Compress +callback 结果直接由 api 端提供,不通过前端转发 + 统一套餐创建逻辑 删除账单的状态字段,状态从关联表中计算获得 diff --git a/cmd/playground/main.go b/cmd/playground/main.go index 95d7622..7905807 100644 --- a/cmd/playground/main.go +++ b/cmd/playground/main.go @@ -1,9 +1,5 @@ package main func main() { - println('|') - println(':') - println('\t') - println('\r') - println('\n') + } diff --git a/go.mod b/go.mod index 2faeac7..94f50c6 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,7 @@ require ( github.com/stretchr/testify v1.8.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.59.0 // indirect + github.com/wechatpay-apiv3/wechatpay-go v0.2.20 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/sync v0.12.0 // indirect diff --git a/go.sum b/go.sum index 4dd7194..ceaf3dd 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0= @@ -99,12 +100,15 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= +github.com/wechatpay-apiv3/wechatpay-go v0.2.20 h1:gS8oFn1bHGnyapR2Zb4aqTV6l4kJWgbtqjCq6k1L9DQ= +github.com/wechatpay-apiv3/wechatpay-go v0.2.20/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/pkg/env/env.go b/pkg/env/env.go index e6b9149..c367bc4 100644 --- a/pkg/env/env.go +++ b/pkg/env/env.go @@ -188,7 +188,7 @@ var ( AlipayAppId string AlipayAppPrivateKey string AlipayPublicKey string - AlipayEncryptKey string + AlipayApiCert string AlipayProduction = false ) @@ -208,9 +208,9 @@ func loadAlipay() { panic("环境变量 ALIPAY_PUBLIC_KEY 的值不能为空") } - AlipayEncryptKey = os.Getenv("ALIPAY_ENCRYPT_KEY") - if AlipayEncryptKey == "" { - panic("环境变量 ALIPAY_ENCRYPT_KEY 的值不能为空") + AlipayApiCert = os.Getenv("ALIPAY_API_CERT") + if AlipayApiCert == "" { + panic("环境变量 ALIPAY_API_CERT 的值不能为空") } _AlipayProduction := os.Getenv("ALIPAY_PRODUCTION") @@ -225,6 +225,74 @@ func loadAlipay() { // endregion +// region wechatpay + +var ( + WechatPayAppId string + WechatPayMchId string + WechatPayMchPrivateKeySerial string + WechatPayMchPrivateKeyPath string + WechatPayPublicKeyId string + WechatPayPublicKeyPath string + WechatPayApiCert string + WechatPayCallbackUrl string + WechatPayProduction = false +) + +func loadWechatPay() { + + WechatPayAppId = os.Getenv("WECHATPAY_APP_ID") + if WechatPayAppId == "" { + panic("环境变量 WECHATPAY_APP_ID 的值不能为空") + } + + WechatPayMchId = os.Getenv("WECHATPAY_MCH_ID") + if WechatPayMchId == "" { + panic("环境变量 WECHATPAY_MCH_ID 的值不能为空") + } + + WechatPayMchPrivateKeySerial = os.Getenv("WECHATPAY_MCH_PRIVATE_KEY_SERIAL") + if WechatPayMchPrivateKeySerial == "" { + panic("环境变量 WECHATPAY_MCH_PRIVATE_KEY_SERIAL 的值不能为空") + } + + WechatPayMchPrivateKeyPath = os.Getenv("WECHATPAY_MCH_PRIVATE_KEY_PATH") + if WechatPayMchPrivateKeyPath == "" { + panic("环境变量 WECHATPAY_MCH_PRIVATE_KEY_PATH 的值不能为空") + } + + WechatPayPublicKeyId = os.Getenv("WECHATPAY_PUBLIC_KEY_ID") + if WechatPayPublicKeyId == "" { + panic("环境变量 WECHATPAY_PUBLIC_KEY_ID 的值不能为空") + } + + WechatPayPublicKeyPath = os.Getenv("WECHATPAY_PUBLIC_KEY_PATH") + if WechatPayPublicKeyPath == "" { + panic("环境变量 WECHATPAY_PUBLIC_KEY_PATH 的值不能为空") + } + + WechatPayApiCert = os.Getenv("WECHATPAY_API_CERT") + if WechatPayApiCert == "" { + panic("环境变量 WECHATPAY_API_CERT 的值不能为空") + } + + WechatPayCallbackUrl = os.Getenv("WECHATPAY_CALLBACK_URL") + if WechatPayCallbackUrl == "" { + panic("环境变量 WECHATPAY_CALLBACK_URL 的值不能为空") + } + + _WechatPayProduction := os.Getenv("WECHATPAY_PRODUCTION") + if _WechatPayProduction != "" { + value, err := strconv.ParseBool(_WechatPayProduction) + if err != nil { + panic("环境变量 WECHATPAY_PRODUCTION 的值不是布尔值") + } + WechatPayProduction = value + } +} + +// endregion + // region debug var ( @@ -272,4 +340,5 @@ func Init() { loadDebug() loadRemote() loadAlipay() + // loadWechatPay() } diff --git a/scripts/sql/init.sql b/scripts/sql/init.sql index 8e38840..44b802a 100644 --- a/scripts/sql/init.sql +++ b/scripts/sql/init.sql @@ -789,7 +789,6 @@ create table bill ( bill_no varchar(255) not null unique, info varchar(255), type int not null, - status int not null, amount decimal(12, 2) not null default 0, created_at timestamp default current_timestamp, updated_at timestamp default current_timestamp, @@ -801,7 +800,6 @@ create index bill_resource_id_index on bill (resource_id); create index bill_refund_id_index on bill (refund_id); create index bill_bill_no_index on bill (bill_no); create index bill_type_index on bill (type); -create index bill_status_index on bill (status); create index bill_deleted_at_index on bill (deleted_at); -- bill表字段注释 @@ -814,7 +812,6 @@ comment on column bill.refund_id is '退款ID'; comment on column bill.bill_no is '易读账单号'; comment on column bill.info is '产品可读信息'; comment on column bill.type is '账单类型:0-充值,1-消费,2-退款'; -comment on column bill.status is '账单状态:0-未完成,1-已完成,2-已作废'; comment on column bill.amount is '账单金额'; comment on column bill.created_at is '创建时间'; comment on column bill.updated_at is '更新时间'; diff --git a/web/globals/alipay.go b/web/globals/alipay.go index 3e44326..cbaad7e 100644 --- a/web/globals/alipay.go +++ b/web/globals/alipay.go @@ -23,7 +23,7 @@ func InitAlipay() { panic("加载支付宝公钥失败: " + err.Error()) } - err = client.SetEncryptKey(env.AlipayEncryptKey) + err = client.SetEncryptKey(env.AlipayApiCert) if err != nil { panic("设置支付宝加密密钥失败: " + err.Error()) } diff --git a/web/globals/wechat.go b/web/globals/wechat.go new file mode 100644 index 0000000..fce51ba --- /dev/null +++ b/web/globals/wechat.go @@ -0,0 +1,47 @@ +package globals + +import ( + "context" + "platform/pkg/env" + + "github.com/wechatpay-apiv3/wechatpay-go/core" + "github.com/wechatpay-apiv3/wechatpay-go/core/option" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native" + "github.com/wechatpay-apiv3/wechatpay-go/utils" +) + +var WechatPay *WechatPayClient + +type WechatPayClient struct { + Native *native.NativeApiService +} + +func InitWechatPay() { + + appPrivateKey, err := utils.LoadPrivateKey(env.WechatPayMchPrivateKeyPath) + if err != nil { + panic(err) + } + + wechatPublicKey, err := utils.LoadPublicKey(env.WechatPayPublicKeyPath) + if err != nil { + panic(err) + } + + client, err := core.NewClient(context.Background(), + option.WithWechatPayPublicKeyAuthCipher( + env.WechatPayMchId, + env.WechatPayMchPrivateKeySerial, + appPrivateKey, + env.WechatPayPublicKeyId, + wechatPublicKey, + ), + ) + if err != nil { + panic(err) + } + + WechatPay = &WechatPayClient{ + Native: &native.NativeApiService{Client: client}, + } +} diff --git a/web/handlers/bill.go b/web/handlers/bill.go index 50ee62d..7b392bf 100644 --- a/web/handlers/bill.go +++ b/web/handlers/bill.go @@ -16,7 +16,6 @@ type ListBillReq struct { common.PageReq BillNo *string `json:"bill_no"` Type *int `json:"type"` - Status *int `json:"status"` CreateAfter *time.Time `json:"create_after"` CreateBefore *time.Time `json:"create_before"` } @@ -39,9 +38,6 @@ func ListBill(c *fiber.Ctx) error { do := q.Bill. Where(q.Bill.UserID.Eq(authContext.Payload.Id)) - if req.Status != nil { - do = do.Where(q.Bill.Status.Eq(int32(*req.Status))) - } if req.Type != nil { do = do.Where(q.Bill.Type.Eq(int32(*req.Type))) } diff --git a/web/handlers/channel.go b/web/handlers/channel.go index 9b078e7..3e0e2ca 100644 --- a/web/handlers/channel.go +++ b/web/handlers/channel.go @@ -3,12 +3,9 @@ package handlers import ( "errors" "fmt" - "log/slog" "platform/web/auth" q "platform/web/queries" "platform/web/services" - "strconv" - "strings" "github.com/gofiber/fiber/v2" ) @@ -114,124 +111,3 @@ func RemoveChannels(c *fiber.Ctx) error { } // endregion - -// region CreateChannel(GET) - -type CreateChannelGetReq struct { - ResourceId int32 `query:"i" validate:"required"` - Protocol services.ChannelProtocol `query:"x" validate:"required,oneof=socks5 http https"` - AuthType services.ChannelAuthType `query:"t" validate:"required,oneof=0 1"` - Count int `query:"n" validate:"required"` - Prov string `query:"a" validate:"required"` - City string `query:"b" validate:"required"` - Isp string `query:"s" validate:"required"` - ResultType CreateChannelResultType `query:"rt" validate:"required,oneof=json text"` - ResultBreaker []rune `query:"rb"` - ResultSeparator []rune `query:"rs"` -} - -func CreateChannelGet(c *fiber.Ctx) error { - req := new(CreateChannelGetReq) - if err := c.QueryParser(req); err != nil { - return err - } - slog.Info("CreateChannelGet", "req", *req) - - // 验证用户身份 - resource, err := q.Resource.Debug().Where(q.Resource.ID.Eq(req.ResourceId)).Take() - if err != nil { - return err - } - whitelists, err := q.Whitelist.Debug().Where(q.Whitelist.UserID.Eq(resource.UserID)).Find() - if err != nil { - return err - } - if len(whitelists) == 0 { - return fiber.NewError(fiber.StatusForbidden, fmt.Sprintf("forbidden %s", c.IP())) - } - - var invalid bool - for _, whitelist := range whitelists { - invalid = whitelist.Host == c.IP() - if invalid { - break - } - } - if !invalid { - return fiber.NewError(fiber.StatusForbidden, fmt.Sprintf("forbidden %s", c.IP())) - } - - user, err := q.User.Debug().Where(q.User.ID.Eq(resource.UserID)).Take() - if err != nil { - return err - } - - authCtx := &services.AuthContext{ - Payload: services.Payload{ - Id: user.ID, - Type: services.PayloadUser, - Name: user.Name, - Avatar: user.Avatar, - }, - } - - if req.ResultType == "" { - req.ResultType = CreateChannelResultTypeText - } - if req.ResultBreaker == nil { - req.ResultBreaker = []rune("\r\n") - } - if req.ResultSeparator == nil { - req.ResultSeparator = []rune("|") - } - - // 建立连接通道 - result, err := services.Channel.CreateChannel( - c.Context(), - authCtx, - req.ResourceId, - req.Protocol, - req.AuthType, - req.Count, - services.NodeFilterConfig{ - Isp: req.Isp, - Prov: req.Prov, - City: req.City, - }, - ) - if err != nil { - return err - } - - var separator = string(req.ResultSeparator) - switch req.ResultType { - case CreateChannelResultTypeJson: - return c.JSON(fiber.Map{ - "code": 1, - "data": result, - }) - default: - var breaker = string(req.ResultBreaker) - var str = strings.Builder{} - for _, info := range result { - - str.WriteString(info.Host) - - str.WriteString(separator) - str.WriteString(strconv.Itoa(info.Port)) - - if info.Username != nil { - str.WriteString(separator) - str.WriteString(*info.Username) - } - if info.Password != nil { - str.WriteString(separator) - str.WriteString(*info.Password) - } - str.WriteString(breaker) - } - return c.SendString(str.String()) - } -} - -// endregion diff --git a/web/handlers/resource.go b/web/handlers/resource.go index a296900..d57f50b 100644 --- a/web/handlers/resource.go +++ b/web/handlers/resource.go @@ -1,24 +1,19 @@ package handlers import ( - "context" - "encoding/json" - "errors" - "fmt" - "platform/pkg/rds" + "platform/pkg/env" "platform/pkg/u" "platform/web/auth" "platform/web/common" g "platform/web/globals" - m "platform/web/models" q "platform/web/queries" - "platform/web/services" + s "platform/web/services" "strconv" - "strings" "time" "github.com/gofiber/fiber/v2" "github.com/smartwalle/alipay/v3" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native" ) // region ListResourcePss @@ -37,7 +32,7 @@ type ListResourcePssReq struct { // ListResourcePss 获取套餐列表 func ListResourcePss(c *fiber.Ctx) error { // 检查权限 - authContext, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{}) + authContext, err := auth.Protect(c, []s.PayloadType{s.PayloadUser}, []string{}) if err != nil { return err } @@ -110,7 +105,7 @@ type AllResourceReq struct { func AllResource(c *fiber.Ctx) error { // 检查权限 - authContext, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{}) + authContext, err := auth.Protect(c, []s.PayloadType{s.PayloadUser}, []string{}) if err != nil { return err } @@ -148,14 +143,10 @@ func AllResource(c *fiber.Ctx) error { // endregion -// region CreateResource +// region CreateResourcePrepared type CreateResourceReq struct { - Type int32 `json:"type" validate:"required"` - Live int32 `json:"live" validate:"required"` - Expire int32 `json:"expire" validate:"required"` - Quota int32 `json:"quota" validate:"required"` - DailyLimit int32 `json:"daily_limit" validate:"required"` + s.CreateResourceData } type CreateResourceResp struct { @@ -170,7 +161,7 @@ type PaidCreateResourceReq struct { func PrepareResourceByAlipay(c *fiber.Ctx) error { // 检查权限 - authContext, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{}) + authContext, err := auth.Protect(c, []s.PayloadType{s.PayloadUser}, []string{}) if err != nil { return err } @@ -182,7 +173,7 @@ func PrepareResourceByAlipay(c *fiber.Ctx) error { } // 生成订单 - amount, tradeNo, err := prepareResource(c.Context(), req) + tradeNo, err := s.ID.GenSerial(c.Context()) if err != nil { return err } @@ -192,8 +183,8 @@ func PrepareResourceByAlipay(c *fiber.Ctx) error { QRPayMode: "4", Trade: alipay.Trade{ OutTradeNo: tradeNo, - TotalAmount: strconv.FormatFloat(amount, 'f', 2, 64), - Subject: "购买套餐", + TotalAmount: strconv.FormatFloat(req.GetPrice(), 'f', 2, 64), + Subject: "购买套餐 - " + req.GetName(), ProductCode: "FAST_INSTANT_TRADE_PAY", TimeExpire: time.Now().Add(30 * time.Minute).Format("2006-01-02 15:04:05"), }, @@ -203,7 +194,7 @@ func PrepareResourceByAlipay(c *fiber.Ctx) error { } // 保存交易信息 - err = savePrepareResource(c.Context(), req, amount, tradeNo, authContext.Payload.Id, 1) + err = s.Resource.PrepareResource(c.Context(), &req.CreateResourceData, authContext.Payload.Id, tradeNo, 1) // 返回结果 return c.JSON(CreateResourceResp{ @@ -215,7 +206,7 @@ func PrepareResourceByAlipay(c *fiber.Ctx) error { func PrepareResourceByWechat(c *fiber.Ctx) error { // 检查权限 - authContext, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{}) + authContext, err := auth.Protect(c, []s.PayloadType{s.PayloadUser}, []string{}) if err != nil { return err } @@ -226,29 +217,27 @@ func PrepareResourceByWechat(c *fiber.Ctx) error { return err } - // 生成订单 - amount, tradeNo, err := prepareResource(c.Context(), req) + // 生成订单号 + tradeNo, err := s.ID.GenSerial(c.Context()) if err != nil { return err } // 调用外部接口 - alipayResp, err := g.Alipay.TradePagePay(alipay.TradePagePay{ - QRPayMode: "3", - Trade: alipay.Trade{ - OutTradeNo: tradeNo, - TotalAmount: strconv.FormatFloat(amount, 'f', 2, 64), - Subject: "购买套餐", - ProductCode: "FAST_INSTANT_TRADE_PAY", - TimeExpire: time.Now().Add(30 * time.Minute).Format("2006-01-02 15:04:05"), + wechatPayResp, _, err := g.WechatPay.Native.Prepay(c.Context(), native.PrepayRequest{ + Mchid: &env.WechatPayMchId, + Appid: &env.WechatPayAppId, + Description: u.P("购买套餐 - " + req.GetName()), + OutTradeNo: &tradeNo, + TimeExpire: u.P(time.Now().Add(30 * time.Minute)), + NotifyUrl: &env.WechatPayCallbackUrl, + Amount: &native.Amount{ + Total: u.P(int64(req.GetPrice() * 100)), }, }) - if err != nil { - return err - } // 保存交易信息 - err = savePrepareResource(c.Context(), req, amount, tradeNo, authContext.Payload.Id, 2) + err = s.Resource.PrepareResource(c.Context(), &req.CreateResourceData, authContext.Payload.Id, tradeNo, 2) if err != nil { return err } @@ -256,13 +245,13 @@ func PrepareResourceByWechat(c *fiber.Ctx) error { // 返回结果 return c.JSON(CreateResourceResp{ TradeNo: tradeNo, - PayURL: alipayResp.String(), + PayURL: *wechatPayResp.CodeUrl, }) } func CreateResourceByAlipay(c *fiber.Ctx) error { // 检查权限 - authCtx, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{}) + _, err := auth.Protect(c, []s.PayloadType{s.PayloadUser}, []string{}) if err != nil { return err } @@ -284,6 +273,7 @@ func CreateResourceByAlipay(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "支付未完成,请确认后重试") } + // 创建套餐 payment, err := strconv.ParseFloat(alipayResp.ReceiptAmount, 64) if err != nil { return err @@ -294,53 +284,7 @@ func CreateResourceByAlipay(c *fiber.Ctx) error { return err } - // 获取请求缓存 - reqStr, err := rds.Client.GetDel(c.Context(), req.TradeNo).Result() - if err != nil { - return err - } - - reqCreate := new(CreateResourceReq) - if err := json.Unmarshal([]byte(reqStr), reqCreate); err != nil { - return err - } - - // 保存交易信息 - err = q.Q.Transaction(func(q *q.Query) error { - - // 保存套餐 - resource, err := saveResourceBalance(reqCreate, authCtx.Payload.Id, 0) - if err != nil { - return err - } - - // 更新订单状态 - _, err = q.Trade. - Where(q.Trade.InnerNo). - Select(q.Trade.OuterNo, q.Trade.Payment, q.Trade.Status, q.Trade.PaidAt). - Updates(&m.Trade{ - OuterNo: alipayResp.TradeNo, - Payment: payment, - Status: 1, - PaidAt: common.LocalDateTime(paidAt), - }) - if err != nil { - return err - } - - // 更新账单状态 - _, err = q.Bill. - Where(q.Bill.TradeID.Eq(resource.ID)). - Updates(&m.Bill{ - ResourceID: resource.ID, - Status: 1, - }) - if err != nil { - return err - } - - return nil - }) + err = s.Resource.CreateResourcePrepared(c.Context(), req.TradeNo, alipayResp.TradeNo, payment, paidAt) if err != nil { return err } @@ -350,63 +294,50 @@ func CreateResourceByAlipay(c *fiber.Ctx) error { func CreateResourceByWechat(c *fiber.Ctx) error { // 检查权限 - authCtx, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{}) + _, err := auth.Protect(c, []s.PayloadType{s.PayloadUser}, []string{}) if err != nil { return err } // 解析请求参数 - req := new(CreateResourceReq) + req := new(PaidCreateResourceReq) if err := c.BodyParser(req); err != nil { return err } - err = q.Q.Transaction(func(q *q.Query) error { - - // 保存套餐 - resource, err := saveResourceBalance(req, authCtx.Payload.Id, 0) - if err != nil { - return err - } - - // 更新订单状态 - // _, err = q.Trade. - // Where(q.Trade.InnerNo). - // Select(q.Trade.OuterNo, q.Trade.Payment, q.Trade.Status, q.Trade.PaidAt). - // Updates(&m.Trade{ - // OuterNo: alipayResp.TradeNo, - // Payment: payment, - // Status: 1, - // PaidAt: common.LocalDateTime(paidAt), - // }) - // if err != nil { - // return err - // } - - // 更新账单状态 - _, err = q.Bill. - Where(q.Bill.TradeID.Eq(resource.ID)). - Updates(&m.Bill{ - ResourceID: resource.ID, - Status: 1, - }) - if err != nil { - return err - } - - return nil + // 验证支付结果 + wechatPayResp, _, err := g.WechatPay.Native.QueryOrderByOutTradeNo(c.Context(), native.QueryOrderByOutTradeNoRequest{ + OutTradeNo: &req.TradeNo, + Mchid: &env.WechatPayMchId, }) if err != nil { return err } + if *wechatPayResp.TradeState != "SUCCESS" { + return fiber.NewError(fiber.StatusBadRequest, "支付未完成,请确认后重试") + } + + // 创建套餐 + payment := float64(*wechatPayResp.Amount.PayerTotal) / 100 + + paidAt, err := time.Parse(time.RFC3339, *wechatPayResp.SuccessTime) + if err != nil { + return err + } + + err = s.Resource.CreateResourcePrepared(c.Context(), req.TradeNo, *wechatPayResp.OutTradeNo, payment, paidAt) + if err != nil { + return err + } + return nil } func CreateResourceByBalance(c *fiber.Ctx) error { // 检查权限 - authCtx, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{}) + authCtx, err := auth.Protect(c, []s.PayloadType{s.PayloadUser}, []string{}) if err != nil { return err } @@ -417,208 +348,13 @@ func CreateResourceByBalance(c *fiber.Ctx) error { return err } - // 计算价格 - var amount = calcResourcePrice(req) - - // 保存交易信息 - err = q.Q.Transaction(func(q *q.Query) error { - - // 保存套餐 - resource, err := saveResourceBalance(req, authCtx.Payload.Id, amount) - if err != nil { - return err - } - - // 生成账单 - bill := m.Bill{ - UserID: authCtx.Payload.Id, - ResourceID: resource.ID, - BillNo: services.ID.GenReadable("bil"), - Info: "购买套餐 - " + resourceName(req), - Type: 1, - Status: 1, - Amount: amount, - } - err = q.Bill. - Omit(q.Bill.TradeID, q.Bill.RefundID). - Create(&bill) - if err != nil { - return err - } - - return nil - }) - if err != nil { - return err - } - - return nil -} - -func calcResourcePrice(req *CreateResourceReq) float64 { - return 100 -} - -func prepareResource(ctx context.Context, req *CreateResourceReq) (amount float64, tradeNo string, err error) { - - // todo 计算价格 - amount = calcResourcePrice(req) - - // 生成订单号 - tradeNoUint, err := services.ID.GenSerial(ctx) - if err != nil { - return 0, "", err - } - tradeNo = strconv.FormatUint(tradeNoUint, 10) - - return amount, tradeNo, nil -} - -func savePrepareResource(ctx context.Context, req *CreateResourceReq, amount float64, tradeNo string, uid int32, method int32) error { - // 缓存交易信息 - reqStr, err := json.Marshal(req) - if err != nil { - return err - } - - err = rds.Client.Set(ctx, tradeNo, reqStr, 30*time.Minute).Err() - if err != nil { - return err - } - - // 保存到数据库 - err = q.Q.Transaction(func(q *q.Query) error { - // 创建交易订单 - var trade = m.Trade{ - UserID: uid, - InnerNo: tradeNo, - Subject: "购买套餐 - " + resourceName(req), - Method: method, - Type: 1, - Status: 0, - Amount: amount, - } - err = q.Trade.Create(&trade) - if err != nil { - return err - } - - // 保存用户帐单 - bill := m.Bill{ - UserID: uid, - TradeID: trade.ID, - BillNo: services.ID.GenReadable("bil"), - Info: "购买产品", - Type: 1, - Status: 0, - Amount: -amount, - } - err = q.Bill. - Omit(q.Bill.ResourceID, q.Bill.RefundID). - Create(&bill) - if err != nil { - return err - } - - return nil - }) - if err != nil { - return err - } - - return nil -} - -func saveResourceBalance(req *CreateResourceReq, uid int32, amount float64) (*m.Resource, error) { - // 检查用户 - user, err := q.User. - Where( - q.User.ID.Eq(uid), - q.User.Status.Eq(1), - ). - Take() - if err != nil { - return nil, err - } - - // 检查余额 - if user.Balance < amount { - return nil, fiber.NewError(fiber.StatusBadRequest, "余额不足") - } - // 创建套餐 - resource := &m.Resource{ - UserID: user.ID, - ResourceNo: services.ID.GenReadable("res"), - Active: true, - Type: 1, - Pss: &m.ResourcePss{ - Type: req.Type, - Live: req.Live, - Quota: req.Quota, - Expire: common.LocalDateTime(time.Now().Add(time.Duration(req.Expire) * time.Second)), - DailyLimit: req.DailyLimit, - }, - } - err = q.Resource.Create(resource) + err = s.Resource.CreateResourceImmediately(&req.CreateResourceData, authCtx.Payload.Id) if err != nil { - return nil, err + return err } - // 更新用户余额 - user.Balance -= amount - _, err = q.User. - Where(q.User.ID.Eq(uid)). - Update(q.User.Balance, user.Balance) - if err != nil { - return nil, err - } - - return resource, nil -} - -func resourceName(req *CreateResourceReq) string { - sb := strings.Builder{} - sb.WriteString("短效动态") - switch req.Type { - case 1: - sb.WriteString("包时 ") - case 2: - sb.WriteString("包量 ") - } - sb.WriteString(fmt.Sprintf("%d 分钟", req.Live/60)) - return sb.String() -} - -// endregion - -// region CreateResourceByAlipayCallback - -type CreateResourceByAlipayCallbackReq struct { -} - -// CreateResourceByAlipayCallback 支付宝支付回调 -func CreateResourceByAlipayCallback(c *fiber.Ctx) error { - - // 根据支付类型执行不同流程: - // 1. 支付宝或微信(即时支付) - // - 更新订单状态 - // - 生成账单 - // - 生成套餐 - - return errors.New("not implemented") -} - -// endregion - -// region CreateResourceByWechatCallback - -type CreateResourceByWechatCallbackReq struct { -} - -// CreateResourceByWechatCallback 微信支付回调 -func CreateResourceByWechatCallback(c *fiber.Ctx) error { - return errors.New("not implemented") + return nil } // endregion diff --git a/web/handlers/temp.go b/web/handlers/temp.go deleted file mode 100644 index aeb907a..0000000 --- a/web/handlers/temp.go +++ /dev/null @@ -1,22 +0,0 @@ -package handlers - -import ( - q "platform/web/queries" - "time" - - "github.com/gofiber/fiber/v2" -) - -func Temp(c *fiber.Ctx) error { - - channels, err := q.Channel.Debug().Where( - q.Channel.Expiration.Lt(time.Now().Add(3 * time.Minute)), - ).Find() - if err != nil { - return err - } - - return c.JSON(fiber.Map{ - "result": channels, - }) -} diff --git a/web/handlers/trade.go b/web/handlers/trade.go new file mode 100644 index 0000000..2632e0c --- /dev/null +++ b/web/handlers/trade.go @@ -0,0 +1,117 @@ +package handlers + +import ( + "net/http" + g "platform/web/globals" + s "platform/web/services" + "strconv" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/smartwalle/alipay/v3" +) + +// region AlipayCallback + +func AlipayCallback(c *fiber.Ctx) error { + + // 解析请求 + req := make(map[string][]string) + c.Context().QueryArgs().VisitAll(func(key, value []byte) { + req[string(key)] = append(req[string(key)], string(value)) + }) + c.Context().PostArgs().VisitAll(func(key, value []byte) { + req[string(key)] = append(req[string(key)], string(value)) + }) + + notification, err := g.Alipay.DecodeNotification(req) + if err != nil { + return err + } + + switch notification.NotifyType { + + // 支付成功 + case string(alipay.TradeStatusSuccess): + + if isRefund(notification) { + break + } + + payment, err := strconv.ParseFloat(notification.TotalAmount, 64) + if err != nil { + return err + } + + paidAt, err := time.Parse("2006-01-02 15:04:05", notification.GmtPayment) + if err != nil { + return err + } + + err = s.Resource.CreateResourcePrepared( + c.Context(), + notification.OutTradeNo, + notification.TradeNo, + payment, + paidAt, + ) + if err != nil { + return err + } + + // 支付关闭 + case string(alipay.TradeStatusClosed): + + if isRefund(notification) { + break + } + + cancelAt, err := time.Parse("2006-01-02 15:04:05", notification.GmtClose) + if err != nil { + return err + } + + err = s.Resource.CancelResource(c.Context(), notification.OutTradeNo, cancelAt) + if err != nil { + return err + } + + default: + } + + g.Alipay.ACKNotification(AdapterWriter{ + c: c, + }) + return nil +} + +type AdapterWriter struct { + c *fiber.Ctx +} + +func (a AdapterWriter) Header() http.Header { + panic("implement me") +} + +func (a AdapterWriter) Write(bytes []byte) (int, error) { + return a.c.Write(bytes) +} + +func (a AdapterWriter) WriteHeader(statusCode int) { + a.c.Status(statusCode) +} + +func isRefund(notification *alipay.Notification) bool { + return notification.OutBizNo != "" || notification.RefundFee != "" || notification.GmtRefund != "" +} + +// endregion + +// region WechatPayCallback + +func WechatPayCallback(c *fiber.Ctx) error { + + return nil +} + +// endregion diff --git a/web/models/bill.gen.go b/web/models/bill.gen.go index ad18279..3e070f0 100644 --- a/web/models/bill.gen.go +++ b/web/models/bill.gen.go @@ -25,7 +25,6 @@ type Bill struct { Type int32 `gorm:"column:type;not null" json:"type"` BillNo string `gorm:"column:bill_no;not null" json:"bill_no"` RefundID int32 `gorm:"column:refund_id" json:"refund_id"` - Status int32 `gorm:"column:status;not null" json:"status"` Amount float64 `gorm:"column:amount;not null" json:"amount"` Trade *Trade `gorm:"foreignKey:TradeID" json:"trade"` Refund *Refund `gorm:"foreignKey:RefundID" json:"refund"` diff --git a/web/queries/bill.gen.go b/web/queries/bill.gen.go index 2995857..4c62b47 100644 --- a/web/queries/bill.gen.go +++ b/web/queries/bill.gen.go @@ -38,7 +38,6 @@ func newBill(db *gorm.DB, opts ...gen.DOOption) bill { _bill.Type = field.NewInt32(tableName, "type") _bill.BillNo = field.NewString(tableName, "bill_no") _bill.RefundID = field.NewInt32(tableName, "refund_id") - _bill.Status = field.NewInt32(tableName, "status") _bill.Amount = field.NewFloat64(tableName, "amount") _bill.Trade = billBelongsToTrade{ db: db.Session(&gorm.Session{}), @@ -83,7 +82,6 @@ type bill struct { Type field.Int32 BillNo field.String RefundID field.Int32 - Status field.Int32 Amount field.Float64 Trade billBelongsToTrade @@ -117,7 +115,6 @@ func (b *bill) updateTableName(table string) *bill { b.Type = field.NewInt32(table, "type") b.BillNo = field.NewString(table, "bill_no") b.RefundID = field.NewInt32(table, "refund_id") - b.Status = field.NewInt32(table, "status") b.Amount = field.NewFloat64(table, "amount") b.fillFieldMap() @@ -135,7 +132,7 @@ func (b *bill) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (b *bill) fillFieldMap() { - b.fieldMap = make(map[string]field.Expr, 16) + b.fieldMap = make(map[string]field.Expr, 15) b.fieldMap["id"] = b.ID b.fieldMap["user_id"] = b.UserID b.fieldMap["info"] = b.Info @@ -147,7 +144,6 @@ func (b *bill) fillFieldMap() { b.fieldMap["type"] = b.Type b.fieldMap["bill_no"] = b.BillNo b.fieldMap["refund_id"] = b.RefundID - b.fieldMap["status"] = b.Status b.fieldMap["amount"] = b.Amount } diff --git a/web/router.go b/web/router.go index 19b2a81..1f063ca 100644 --- a/web/router.go +++ b/web/router.go @@ -48,7 +48,7 @@ func ApplyRouters(app *fiber.App) { bill := api.Group("/bill") bill.Post("/list", handlers.ListBill) - // 临时 - app.Get("/collect", handlers.CreateChannelGet) - app.Get("/temp", handlers.Temp) + // 交易 + trade := api.Group("/trade") + trade.Post("/callback/alipay", handlers.AlipayCallback) } diff --git a/web/services/id.go b/web/services/id.go index 70454ea..5ba63da 100644 --- a/web/services/id.go +++ b/web/services/id.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "platform/pkg/rds" + "strconv" "strings" "time" @@ -42,7 +43,7 @@ var ( ErrSequenceOverflow = errors.New("sequence overflow") ) -func (s *IdService) GenSerial(ctx context.Context) (uint64, error) { +func (s *IdService) GenSerial(ctx context.Context) (string, error) { // 构造Redis键 now := time.Now().Unix() key := idSerialKey(now) @@ -74,13 +75,15 @@ func (s *IdService) GenSerial(ctx context.Context) (uint64, error) { return err }, key) if err != nil { - return 0, err + return "", err } // 组装最终ID id := uint64((now << timestampShift) | sequence) - return id, nil + idStr := strconv.FormatUint(id, 10) + + return idStr, nil } // ParseSerial 解析ID,返回其组成部分 diff --git a/web/services/resource.go b/web/services/resource.go new file mode 100644 index 0000000..56b2332 --- /dev/null +++ b/web/services/resource.go @@ -0,0 +1,293 @@ +package services + +import ( + "context" + "encoding/json" + "fmt" + "platform/pkg/rds" + "platform/web/common" + m "platform/web/models" + q "platform/web/queries" + "strings" + "time" + + "github.com/gofiber/fiber/v2" +) + +var Resource = &resourceService{} + +type resourceService struct{} + +func (s *resourceService) PrepareResource(ctx context.Context, data *CreateResourceData, uid int32, tradeNo string, method int32) error { + amount := data.GetPrice() + + // 保存到数据库 + err := q.Q.Transaction(func(q *q.Query) error { + // 创建交易订单 + var trade = m.Trade{ + UserID: uid, + InnerNo: tradeNo, + Subject: "购买套餐 - " + data.GetName(), + Method: method, + Type: 1, + Status: 0, + Amount: amount, + } + err := q.Trade.Create(&trade) + if err != nil { + return err + } + + // 保存用户帐单 + bill := m.Bill{ + UserID: uid, + TradeID: trade.ID, + BillNo: ID.GenReadable("bil"), + Info: "购买套餐 - " + data.GetName(), + Type: 1, + Amount: -amount, + } + err = q.Bill. + Omit(q.Bill.ResourceID, q.Bill.RefundID). + Create(&bill) + if err != nil { + return err + } + + // 保存请求缓存 + cache := &CreateResourceCache{ + CreateResourceData: *data, + Uid: uid, + TradeId: trade.ID, + BillId: bill.ID, + } + + reqStr, err := json.Marshal(cache) + if err != nil { + return err + } + + err = rds.Client.Set(ctx, tradeNo, reqStr, 30*time.Minute).Err() + if err != nil { + return err + } + + return nil + }) + if err != nil { + return err + } + + return nil +} + +func (s *resourceService) CreateResourcePrepared(ctx context.Context, tradeNo string, outerTradeNo string, payment float64, at time.Time) error { + + // 获取请求缓存 + reqStr, err := rds.Client.Get(ctx, tradeNo).Result() + if err != nil { + return err + } + + cache := new(CreateResourceCache) + if err := json.Unmarshal([]byte(reqStr), cache); err != nil { + return err + } + + // 保存交易信息 + err = q.Q.Transaction(func(q *q.Query) error { + + // 保存套餐 + resource, err := createResource(&cache.CreateResourceData, cache.Uid) + if err != nil { + return err + } + + // 更新订单状态 + _, err = q.Trade.Debug(). + Select(q.Trade.OuterNo, q.Trade.Payment, q.Trade.Status, q.Trade.PaidAt). + Updates(&m.Trade{ + ID: cache.TradeId, + OuterNo: outerTradeNo, + Payment: payment, + Status: 1, + PaidAt: common.LocalDateTime(at), + }) + if err != nil { + return err + } + + // 更新账单状态 + _, err = q.Bill.Debug(). + Select(q.Bill.ResourceID). + Updates(&m.Bill{ + ID: cache.BillId, + ResourceID: resource.ID, + }) + if err != nil { + return err + } + + // 删除缓存 + err = rds.Client.Del(ctx, tradeNo).Err() + if err != nil { + return err + } + + return nil + }) + if err != nil { + return err + } + + return nil +} + +func (s *resourceService) CreateResourceImmediately(data *CreateResourceData, uid int32) error { + + // 保存交易信息 + err := q.Q.Transaction(func(q *q.Query) error { + + // 保存套餐 + resource, err := createResource(data, uid) + if err != nil { + return err + } + + // 生成账单 + bill := m.Bill{ + UserID: uid, + ResourceID: resource.ID, + BillNo: ID.GenReadable("bil"), + Info: "购买套餐 - " + data.GetName(), + Type: 1, + Amount: data.GetPrice(), + } + err = q.Bill. + Omit(q.Bill.TradeID, q.Bill.RefundID). + Create(&bill) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return err + } + + return nil +} + +type CreateResourceData struct { + Type int32 `json:"type" validate:"required"` + Live int32 `json:"live" validate:"required"` + Expire int32 `json:"expire" validate:"required"` + Quota int32 `json:"quota" validate:"required"` + DailyLimit int32 `json:"daily_limit" validate:"required"` + + name string + price float64 +} + +func (data *CreateResourceData) GetName() string { + if data.name == "" { + sb := strings.Builder{} + sb.WriteString("短效动态") + switch data.Type { + case 1: + sb.WriteString("包时 ") + case 2: + sb.WriteString("包量 ") + } + sb.WriteString(fmt.Sprintf("%d 分钟", data.Live/60)) + data.name = sb.String() + } + return data.name +} + +func (data *CreateResourceData) GetPrice() float64 { + if data.price == 0 { + data.price = 0.01 + } + return data.price +} + +type CreateResourceCache struct { + CreateResourceData `json:"data"` + Uid int32 `json:"uid"` + TradeId int32 `json:"trade_id"` + BillId int32 `json:"bill_id"` +} + +func createResource(data *CreateResourceData, uid int32) (*m.Resource, error) { + amount := data.GetPrice() + + // 检查用户 + user, err := q.User. + Where( + q.User.ID.Eq(uid), + q.User.Status.Eq(1), + ). + Take() + if err != nil { + return nil, err + } + + // 检查余额 + if user.Balance < amount { + return nil, fiber.NewError(fiber.StatusBadRequest, "余额不足") + } + + // 创建套餐 + resource := &m.Resource{ + UserID: user.ID, + ResourceNo: ID.GenReadable("res"), + Active: true, + Type: 1, + Pss: &m.ResourcePss{ + Type: data.Type, + Live: data.Live, + Quota: data.Quota, + Expire: common.LocalDateTime(time.Now().Add(time.Duration(data.Expire) * time.Second)), + DailyLimit: data.DailyLimit, + }, + } + err = q.Resource.Create(resource) + if err != nil { + return nil, err + } + + // 更新用户余额 + user.Balance -= amount + _, err = q.User. + Where(q.User.ID.Eq(uid)). + Update(q.User.Balance, user.Balance) + if err != nil { + return nil, err + } + + return resource, nil +} + +func (s *resourceService) CancelResource(ctx context.Context, tradeNo string, at time.Time) error { + // 获取请求缓存 + _, err := rds.Client.Del(ctx, tradeNo).Result() + if err != nil { + return err + } + + // 更新订单状态 + _, err = q.Trade. + Where(q.Trade.InnerNo.Eq(tradeNo)). + Select(q.Trade.Status, q.Trade.CancelAt). + Updates(m.Trade{ + Status: 2, + CancelAt: common.LocalDateTime(at), + }) + if err != nil { + return err + } + + return nil +} diff --git a/web/web.go b/web/web.go index bfc8dd6..bfc6f99 100644 --- a/web/web.go +++ b/web/web.go @@ -42,6 +42,7 @@ func (s *Server) Run() error { // inits g.InitBaiyin() g.InitAlipay() + // g.InitWechatPay() // config s.fiber = fiber.New(fiber.Config{