From 2146887f955cdcfc14778a1d91f847640e72a8e0 Mon Sep 17 00:00:00 2001 From: luorijun Date: Wed, 16 Apr 2025 18:50:55 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=B5=84=E6=BA=90=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E9=80=BB=E8=BE=91=EF=BC=8C=E6=95=B4=E5=90=88=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=AE=9D=EF=BC=8C=E4=BC=98=E5=8C=96=E5=A5=97=E9=A4=90?= =?UTF-8?q?=E7=94=9F=E6=88=90=E4=B8=8E=E4=BA=A4=E6=98=93=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +- pkg/env/env.go | 9 +- web/handlers/channel.go | 8 +- web/handlers/resource.go | 441 +++++++++++++++++++++++++++++++++++---- web/handlers/trade.go | 116 ---------- web/router.go | 8 +- 6 files changed, 416 insertions(+), 180 deletions(-) delete mode 100644 web/handlers/trade.go diff --git a/README.md b/README.md index 6138f6e..bce5fb3 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,8 @@ - [ ] jwt 签发 - [x] 鉴权 - [x] 实名认证 - - [ ] 对接接口 -- [x] 充值余额 - - [ ] 对接接口 -- [x] 选择套餐 + - [x] 对接接口 +- [x] 充值或购买 - [ ] 对接接口 - [ ] 提取记录 - [x] 提取 IP @@ -24,9 +22,11 @@ - [ ] Limiter - [ ] Compress -统一简化包导入别名 +统一套餐创建逻辑 -迁移 pkg 包下的代码,尽量放置在 web/globals 下,env 和 log 作为全局公共配置保留 +删除账单的状态字段,状态从关联表中计算获得 + +统一简化包导入别名 更新数据库填充 @@ -47,8 +47,6 @@ channel 优化: - 端口分配时加锁 - 数据存入顺序,数据库 > 缓存 > 外部接口 -remote 令牌问题 - 用对称加密处理密钥 考虑将鉴权逻辑放到 handler 里,统一动静态鉴权以及解耦服务层 diff --git a/pkg/env/env.go b/pkg/env/env.go index 1fc582a..e6b9149 100644 --- a/pkg/env/env.go +++ b/pkg/env/env.go @@ -193,25 +193,26 @@ var ( ) func loadAlipay() { - AlipayAppId := os.Getenv("ALIPAY_APP_ID") + AlipayAppId = os.Getenv("ALIPAY_APP_ID") if AlipayAppId == "" { panic("环境变量 ALIPAY_APP_ID 的值不能为空") } - AlipayAppPrivateKey := os.Getenv("ALIPAY_APP_PRIVATE_KEY") + AlipayAppPrivateKey = os.Getenv("ALIPAY_APP_PRIVATE_KEY") if AlipayAppPrivateKey == "" { panic("环境变量 ALIPAY_APP_PRIVATE_KEY 的值不能为空") } - AlipayPublicKey := os.Getenv("ALIPAY_PUBLIC_KEY") + AlipayPublicKey = os.Getenv("ALIPAY_PUBLIC_KEY") if AlipayPublicKey == "" { panic("环境变量 ALIPAY_PUBLIC_KEY 的值不能为空") } - AlipayEncryptKey := os.Getenv("ALIPAY_ENCRYPT_KEY") + AlipayEncryptKey = os.Getenv("ALIPAY_ENCRYPT_KEY") if AlipayEncryptKey == "" { panic("环境变量 ALIPAY_ENCRYPT_KEY 的值不能为空") } + _AlipayProduction := os.Getenv("ALIPAY_PRODUCTION") if _AlipayProduction != "" { value, err := strconv.ParseBool(_AlipayProduction) diff --git a/web/handlers/channel.go b/web/handlers/channel.go index 198430a..9b078e7 100644 --- a/web/handlers/channel.go +++ b/web/handlers/channel.go @@ -99,13 +99,13 @@ func RemoveChannels(c *fiber.Ctx) error { } // 获取用户信息 - auth, ok := c.Locals("auth").(*services.AuthContext) + authCtx, ok := c.Locals("auth").(*services.AuthContext) if !ok { return errors.New("user not found") } // 删除通道 - err := services.Channel.RemoveChannels(c.Context(), auth, req.ByIds...) + err := services.Channel.RemoveChannels(c.Context(), authCtx, req.ByIds...) if err != nil { return err } @@ -166,7 +166,7 @@ func CreateChannelGet(c *fiber.Ctx) error { return err } - auth := &services.AuthContext{ + authCtx := &services.AuthContext{ Payload: services.Payload{ Id: user.ID, Type: services.PayloadUser, @@ -188,7 +188,7 @@ func CreateChannelGet(c *fiber.Ctx) error { // 建立连接通道 result, err := services.Channel.CreateChannel( c.Context(), - auth, + authCtx, req.ResourceId, req.Protocol, req.AuthType, diff --git a/web/handlers/resource.go b/web/handlers/resource.go index b14e5e5..a296900 100644 --- a/web/handlers/resource.go +++ b/web/handlers/resource.go @@ -1,16 +1,24 @@ package handlers import ( + "context" + "encoding/json" "errors" + "fmt" + "platform/pkg/rds" "platform/pkg/u" "platform/web/auth" "platform/web/common" + g "platform/web/globals" m "platform/web/models" q "platform/web/queries" "platform/web/services" + "strconv" + "strings" "time" "github.com/gofiber/fiber/v2" + "github.com/smartwalle/alipay/v3" ) // region ListResourcePss @@ -26,7 +34,7 @@ type ListResourcePssReq struct { ExpireBefore *time.Time `json:"expire_before"` } -// ListResourcePss 获取资源列表 +// ListResourcePss 获取套餐列表 func ListResourcePss(c *fiber.Ctx) error { // 检查权限 authContext, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{}) @@ -40,7 +48,7 @@ func ListResourcePss(c *fiber.Ctx) error { return err } - // 查询资源列表 + // 查询套餐列表 do := q.Resource. Joins(q.Resource.Pss). Where(q.Resource.UserID.Eq(authContext.Payload.Id)) @@ -107,7 +115,7 @@ func AllResource(c *fiber.Ctx) error { return err } - // 查询资源列表 + // 查询套餐列表 pss := q.ResourcePss.As(q.Resource.Pss.Name()) do := q.Resource.Debug(). Joins(q.Resource.Pss). @@ -140,9 +148,9 @@ func AllResource(c *fiber.Ctx) error { // endregion -// region CreateResourceByBalance +// region CreateResource -type CreateResourceByBalanceReq struct { +type CreateResourceReq struct { Type int32 `json:"type" validate:"required"` Live int32 `json:"live" validate:"required"` Expire int32 `json:"expire" validate:"required"` @@ -150,8 +158,16 @@ type CreateResourceByBalanceReq struct { DailyLimit int32 `json:"daily_limit" validate:"required"` } -// CreateResourceByBalance 通过余额创建资源 -func CreateResourceByBalance(c *fiber.Ctx) error { +type CreateResourceResp struct { + TradeNo string `json:"trade_no"` + PayURL string `json:"pay_url"` +} + +type PaidCreateResourceReq struct { + TradeNo string `json:"trade_no" validate:"required"` +} + +func PrepareResourceByAlipay(c *fiber.Ctx) error { // 检查权限 authContext, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{}) @@ -160,66 +176,268 @@ func CreateResourceByBalance(c *fiber.Ctx) error { } // 解析请求参数 - req := new(CreateResourceByBalanceReq) + req := new(CreateResourceReq) + if err := c.BodyParser(req); err != nil { + return err + } + + // 生成订单 + amount, tradeNo, err := prepareResource(c.Context(), req) + if err != nil { + return err + } + + // 调用外部接口 + alipayResp, err := g.Alipay.TradePagePay(alipay.TradePagePay{ + QRPayMode: "4", + 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"), + }, + }) + if err != nil { + return err + } + + // 保存交易信息 + err = savePrepareResource(c.Context(), req, amount, tradeNo, authContext.Payload.Id, 1) + + // 返回结果 + return c.JSON(CreateResourceResp{ + TradeNo: tradeNo, + PayURL: alipayResp.String(), + }) +} + +func PrepareResourceByWechat(c *fiber.Ctx) error { + + // 检查权限 + authContext, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{}) + if err != nil { + return err + } + + // 解析请求参数 + req := new(CreateResourceReq) + if err := c.BodyParser(req); err != nil { + return err + } + + // 生成订单 + amount, tradeNo, err := prepareResource(c.Context(), req) + 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"), + }, + }) + if err != nil { + return err + } + + // 保存交易信息 + err = savePrepareResource(c.Context(), req, amount, tradeNo, authContext.Payload.Id, 2) + if err != nil { + return err + } + + // 返回结果 + return c.JSON(CreateResourceResp{ + TradeNo: tradeNo, + PayURL: alipayResp.String(), + }) +} + +func CreateResourceByAlipay(c *fiber.Ctx) error { + // 检查权限 + authCtx, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{}) + if err != nil { + return err + } + + // 解析请求参数 + req := new(PaidCreateResourceReq) + if err := c.BodyParser(req); err != nil { + return err + } + + // 验证支付结果 + alipayResp, err := g.Alipay.TradeQuery(c.Context(), alipay.TradeQuery{ + OutTradeNo: req.TradeNo, + }) + if err != nil { + return err + } + if alipayResp.TradeStatus != "TRADE_SUCCESS" { + return fiber.NewError(fiber.StatusBadRequest, "支付未完成,请确认后重试") + } + + payment, err := strconv.ParseFloat(alipayResp.ReceiptAmount, 64) + if err != nil { + return err + } + + paidAt, err := time.Parse("2006-01-02 15:04:05", alipayResp.SendPayDate) + if err != nil { + 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 + }) + if err != nil { + return err + } + + return nil +} + +func CreateResourceByWechat(c *fiber.Ctx) error { + // 检查权限 + authCtx, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{}) + if err != nil { + return err + } + + // 解析请求参数 + req := new(CreateResourceReq) if err := c.BodyParser(req); err != nil { return err } err = q.Q.Transaction(func(q *q.Query) error { - // 检查用户 - user, err := q.User.Where(q.User.ID.Eq(authContext.Payload.Id)).Take() + + // 保存套餐 + resource, err := saveResourceBalance(req, authCtx.Payload.Id, 0) if err != nil { return err } - // 计算价格 - var amount = 100 - var payment = 100 + // 更新订单状态 + // _, 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 + // } - // 检查余额 - if user.Balance < float64(amount)/100 { - return fiber.NewError(fiber.StatusBadRequest, "余额不足") - } - - // 更新用户余额 - user.Balance -= float64(payment) - _, err = q.User. - Where(q.User.ID.Eq(authContext.Payload.Id)). - Update(q.User.Balance, user.Balance) + // 更新账单状态 + _, err = q.Bill. + Where(q.Bill.TradeID.Eq(resource.ID)). + Updates(&m.Bill{ + ResourceID: resource.ID, + Status: 1, + }) if err != nil { return err } - // 创建资源 - resource := m.Resource{ - UserID: authContext.Payload.Id, - ResourceNo: services.ID.GenReadable("res"), - } - err = q.Resource.Create(&resource) - if err != nil { - return err - } - resourcePss := m.ResourcePss{ - ResourceID: resource.ID, - 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.ResourcePss.Create(&resourcePss) + return nil + }) + if err != nil { + return err + } + + return nil +} + +func CreateResourceByBalance(c *fiber.Ctx) error { + + // 检查权限 + authCtx, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{}) + if err != nil { + return err + } + + // 解析请求参数 + req := new(CreateResourceReq) + if err := c.BodyParser(req); err != nil { + 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: authContext.Payload.Id, + UserID: authCtx.Payload.Id, ResourceID: resource.ID, BillNo: services.ID.GenReadable("bil"), - Info: "购买套餐", + Info: "购买套餐 - " + resourceName(req), Type: 1, Status: 1, + Amount: amount, } err = q.Bill. Omit(q.Bill.TradeID, q.Bill.RefundID). @@ -237,6 +455,141 @@ func CreateResourceByBalance(c *fiber.Ctx) error { 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) + 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 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 @@ -251,7 +604,7 @@ func CreateResourceByAlipayCallback(c *fiber.Ctx) error { // 1. 支付宝或微信(即时支付) // - 更新订单状态 // - 生成账单 - // - 生成资源 + // - 生成套餐 return errors.New("not implemented") } diff --git a/web/handlers/trade.go b/web/handlers/trade.go deleted file mode 100644 index f39e624..0000000 --- a/web/handlers/trade.go +++ /dev/null @@ -1,116 +0,0 @@ -package handlers - -import ( - "net/url" - "platform/web/auth" - g "platform/web/globals" - m "platform/web/models" - q "platform/web/queries" - "platform/web/services" - "strconv" - - "github.com/gofiber/fiber/v2" - "github.com/smartwalle/alipay/v3" -) - -// region CreateTrade - -type CreateTradeReq struct { - Type int `json:"type" validate:"required"` // 交易类型:1.充值,2.购买 - Subject string `json:"subject" validate:"required"` - Remark string `json:"remark"` - Amount int `json:"amount" validate:"required"` - Method int `json:"method" validate:"required"` // 支付方式:1.支付宝,2.微信 -} - -func CreateTrade(c *fiber.Ctx) error { - // 检查权限 - authContext, err := auth.Protect(c, []services.PayloadType{services.PayloadUser}, []string{}) - if err != nil { - return err - } - - // 解析请求参数 - req := new(CreateTradeReq) - if err := c.BodyParser(req); err != nil { - return err - } - - // 调用外部接口 - - // 保存交易订单 - num, err := services.ID.GenSerial(c.Context()) - if err != nil { - return err - } - var trade = m.Trade{ - UserID: authContext.Payload.Id, - InnerNo: strconv.FormatUint(num, 10), - Subject: req.Subject, - Remark: req.Remark, - Amount: float64(req.Amount) / 100, - Method: int32(req.Method), - } - err = q.Trade.Create(&trade) - if err != nil { - return err - } - - // 保存用户帐单 - var info string - var t int32 - switch req.Type { - case 1: - info = "充值余额" - t = 0 - case 2: - info = "购买产品" - t = 1 - } - bill := m.Bill{ - UserID: authContext.Payload.Id, - TradeID: trade.ID, - BillNo: services.ID.GenReadable("bil"), - Info: info, - Type: t, - Status: 0, - } - err = q.Bill. - Omit(q.Bill.ResourceID, q.Bill.RefundID). - Create(&bill) - if err != nil { - return err - } - - // 返回结果,外部支付链接 - return nil -} - -func createTradeByAlipay() (*url.URL, error) { - - target, err := g.Alipay.TradePagePay(alipay.TradePagePay{ - Trade: alipay.Trade{}, - AuthToken: "", - QRPayMode: "", - QRCodeWidth: "", - }) - if err != nil { - return nil, err - } - - return target, nil -} - -func createTradeByWechat() error { - panic("unimplemented") -} - -// endregion - -// region TradeCallbackAlipay - -// endregion - -// region TradeCallbackWechat - -// endregion diff --git a/web/router.go b/web/router.go index 803ca8a..19b2a81 100644 --- a/web/router.go +++ b/web/router.go @@ -33,6 +33,10 @@ func ApplyRouters(app *fiber.App) { resource.Post("/list/pss", handlers.ListResourcePss) resource.Post("/all", handlers.AllResource) resource.Post("/create/balance", handlers.CreateResourceByBalance) + resource.Post("/prepare/alipay", handlers.PrepareResourceByAlipay) + resource.Post("/create/alipay", handlers.CreateResourceByAlipay) + resource.Post("/prepare/wechat", handlers.PrepareResourceByWechat) + resource.Post("/create/wechat", handlers.CreateResourceByWechat) // 用户 user := api.Group("/user") @@ -40,10 +44,6 @@ func ApplyRouters(app *fiber.App) { user.Post("/identify", handlers.Identify) user.Post("/identify/callback", handlers.IdentifyCallback) - // 支付 - trade := api.Group("/trade") - trade.Post("/create", handlers.CreateTrade) - // 账单 bill := api.Group("/bill") bill.Post("/list", handlers.ListBill)