添加微信支付支持,重构资源创建逻辑,更新支付宝相关配置,移除账单状态字段

This commit is contained in:
2025-04-17 18:29:44 +08:00
parent 2146887f95
commit f6a97545c5
20 changed files with 607 additions and 495 deletions

1
.gitignore vendored
View File

@@ -7,4 +7,5 @@
bin/ bin/
*.pem
*.http *.http

View File

@@ -22,6 +22,8 @@
- [ ] Limiter - [ ] Limiter
- [ ] Compress - [ ] Compress
callback 结果直接由 api 端提供,不通过前端转发
统一套餐创建逻辑 统一套餐创建逻辑
删除账单的状态字段,状态从关联表中计算获得 删除账单的状态字段,状态从关联表中计算获得

View File

@@ -1,9 +1,5 @@
package main package main
func main() { func main() {
println('|')
println(':')
println('\t')
println('\r')
println('\n')
} }

1
go.mod
View File

@@ -49,6 +49,7 @@ require (
github.com/stretchr/testify v1.8.2 // indirect github.com/stretchr/testify v1.8.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.59.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 github.com/yuin/gopher-lua v1.1.1 // indirect
golang.org/x/mod v0.24.0 // indirect golang.org/x/mod v0.24.0 // indirect
golang.org/x/sync v0.12.0 // indirect golang.org/x/sync v0.12.0 // indirect

4
go.sum
View File

@@ -1,5 +1,6 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 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 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= 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= 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.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.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.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 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 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 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= 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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 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= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=

77
pkg/env/env.go vendored
View File

@@ -188,7 +188,7 @@ var (
AlipayAppId string AlipayAppId string
AlipayAppPrivateKey string AlipayAppPrivateKey string
AlipayPublicKey string AlipayPublicKey string
AlipayEncryptKey string AlipayApiCert string
AlipayProduction = false AlipayProduction = false
) )
@@ -208,9 +208,9 @@ func loadAlipay() {
panic("环境变量 ALIPAY_PUBLIC_KEY 的值不能为空") panic("环境变量 ALIPAY_PUBLIC_KEY 的值不能为空")
} }
AlipayEncryptKey = os.Getenv("ALIPAY_ENCRYPT_KEY") AlipayApiCert = os.Getenv("ALIPAY_API_CERT")
if AlipayEncryptKey == "" { if AlipayApiCert == "" {
panic("环境变量 ALIPAY_ENCRYPT_KEY 的值不能为空") panic("环境变量 ALIPAY_API_CERT 的值不能为空")
} }
_AlipayProduction := os.Getenv("ALIPAY_PRODUCTION") _AlipayProduction := os.Getenv("ALIPAY_PRODUCTION")
@@ -225,6 +225,74 @@ func loadAlipay() {
// endregion // 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 // region debug
var ( var (
@@ -272,4 +340,5 @@ func Init() {
loadDebug() loadDebug()
loadRemote() loadRemote()
loadAlipay() loadAlipay()
// loadWechatPay()
} }

View File

@@ -789,7 +789,6 @@ create table bill (
bill_no varchar(255) not null unique, bill_no varchar(255) not null unique,
info varchar(255), info varchar(255),
type int not null, type int not null,
status int not null,
amount decimal(12, 2) not null default 0, amount decimal(12, 2) not null default 0,
created_at timestamp default current_timestamp, created_at timestamp default current_timestamp,
updated_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_refund_id_index on bill (refund_id);
create index bill_bill_no_index on bill (bill_no); create index bill_bill_no_index on bill (bill_no);
create index bill_type_index on bill (type); 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); create index bill_deleted_at_index on bill (deleted_at);
-- bill表字段注释 -- bill表字段注释
@@ -814,7 +812,6 @@ comment on column bill.refund_id is '退款ID';
comment on column bill.bill_no is '易读账单号'; comment on column bill.bill_no is '易读账单号';
comment on column bill.info is '产品可读信息'; comment on column bill.info is '产品可读信息';
comment on column bill.type is '账单类型0-充值1-消费2-退款'; 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.amount is '账单金额';
comment on column bill.created_at is '创建时间'; comment on column bill.created_at is '创建时间';
comment on column bill.updated_at is '更新时间'; comment on column bill.updated_at is '更新时间';

View File

@@ -23,7 +23,7 @@ func InitAlipay() {
panic("加载支付宝公钥失败: " + err.Error()) panic("加载支付宝公钥失败: " + err.Error())
} }
err = client.SetEncryptKey(env.AlipayEncryptKey) err = client.SetEncryptKey(env.AlipayApiCert)
if err != nil { if err != nil {
panic("设置支付宝加密密钥失败: " + err.Error()) panic("设置支付宝加密密钥失败: " + err.Error())
} }

47
web/globals/wechat.go Normal file
View File

@@ -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},
}
}

View File

@@ -16,7 +16,6 @@ type ListBillReq struct {
common.PageReq common.PageReq
BillNo *string `json:"bill_no"` BillNo *string `json:"bill_no"`
Type *int `json:"type"` Type *int `json:"type"`
Status *int `json:"status"`
CreateAfter *time.Time `json:"create_after"` CreateAfter *time.Time `json:"create_after"`
CreateBefore *time.Time `json:"create_before"` CreateBefore *time.Time `json:"create_before"`
} }
@@ -39,9 +38,6 @@ func ListBill(c *fiber.Ctx) error {
do := q.Bill. do := q.Bill.
Where(q.Bill.UserID.Eq(authContext.Payload.Id)) 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 { if req.Type != nil {
do = do.Where(q.Bill.Type.Eq(int32(*req.Type))) do = do.Where(q.Bill.Type.Eq(int32(*req.Type)))
} }

View File

@@ -3,12 +3,9 @@ package handlers
import ( import (
"errors" "errors"
"fmt" "fmt"
"log/slog"
"platform/web/auth" "platform/web/auth"
q "platform/web/queries" q "platform/web/queries"
"platform/web/services" "platform/web/services"
"strconv"
"strings"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@@ -114,124 +111,3 @@ func RemoveChannels(c *fiber.Ctx) error {
} }
// endregion // 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

View File

@@ -1,24 +1,19 @@
package handlers package handlers
import ( import (
"context" "platform/pkg/env"
"encoding/json"
"errors"
"fmt"
"platform/pkg/rds"
"platform/pkg/u" "platform/pkg/u"
"platform/web/auth" "platform/web/auth"
"platform/web/common" "platform/web/common"
g "platform/web/globals" g "platform/web/globals"
m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
"platform/web/services" s "platform/web/services"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/smartwalle/alipay/v3" "github.com/smartwalle/alipay/v3"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
) )
// region ListResourcePss // region ListResourcePss
@@ -37,7 +32,7 @@ type ListResourcePssReq struct {
// ListResourcePss 获取套餐列表 // ListResourcePss 获取套餐列表
func ListResourcePss(c *fiber.Ctx) error { 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 { if err != nil {
return err return err
} }
@@ -110,7 +105,7 @@ type AllResourceReq struct {
func AllResource(c *fiber.Ctx) error { 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 { if err != nil {
return err return err
} }
@@ -148,14 +143,10 @@ func AllResource(c *fiber.Ctx) error {
// endregion // endregion
// region CreateResource // region CreateResourcePrepared
type CreateResourceReq struct { type CreateResourceReq struct {
Type int32 `json:"type" validate:"required"` s.CreateResourceData
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"`
} }
type CreateResourceResp struct { type CreateResourceResp struct {
@@ -170,7 +161,7 @@ type PaidCreateResourceReq struct {
func PrepareResourceByAlipay(c *fiber.Ctx) error { 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 { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@@ -192,8 +183,8 @@ func PrepareResourceByAlipay(c *fiber.Ctx) error {
QRPayMode: "4", QRPayMode: "4",
Trade: alipay.Trade{ Trade: alipay.Trade{
OutTradeNo: tradeNo, OutTradeNo: tradeNo,
TotalAmount: strconv.FormatFloat(amount, 'f', 2, 64), TotalAmount: strconv.FormatFloat(req.GetPrice(), 'f', 2, 64),
Subject: "购买套餐", Subject: "购买套餐 - " + req.GetName(),
ProductCode: "FAST_INSTANT_TRADE_PAY", ProductCode: "FAST_INSTANT_TRADE_PAY",
TimeExpire: time.Now().Add(30 * time.Minute).Format("2006-01-02 15:04:05"), 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{ return c.JSON(CreateResourceResp{
@@ -215,7 +206,7 @@ func PrepareResourceByAlipay(c *fiber.Ctx) error {
func PrepareResourceByWechat(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 { if err != nil {
return err return err
} }
@@ -226,29 +217,27 @@ func PrepareResourceByWechat(c *fiber.Ctx) error {
return err return err
} }
// 生成订单 // 生成订单
amount, tradeNo, err := prepareResource(c.Context(), req) tradeNo, err := s.ID.GenSerial(c.Context())
if err != nil { if err != nil {
return err return err
} }
// 调用外部接口 // 调用外部接口
alipayResp, err := g.Alipay.TradePagePay(alipay.TradePagePay{ wechatPayResp, _, err := g.WechatPay.Native.Prepay(c.Context(), native.PrepayRequest{
QRPayMode: "3", Mchid: &env.WechatPayMchId,
Trade: alipay.Trade{ Appid: &env.WechatPayAppId,
OutTradeNo: tradeNo, Description: u.P("购买套餐 - " + req.GetName()),
TotalAmount: strconv.FormatFloat(amount, 'f', 2, 64), OutTradeNo: &tradeNo,
Subject: "购买套餐", TimeExpire: u.P(time.Now().Add(30 * time.Minute)),
ProductCode: "FAST_INSTANT_TRADE_PAY", NotifyUrl: &env.WechatPayCallbackUrl,
TimeExpire: time.Now().Add(30 * time.Minute).Format("2006-01-02 15:04:05"), 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 { if err != nil {
return err return err
} }
@@ -256,13 +245,13 @@ func PrepareResourceByWechat(c *fiber.Ctx) error {
// 返回结果 // 返回结果
return c.JSON(CreateResourceResp{ return c.JSON(CreateResourceResp{
TradeNo: tradeNo, TradeNo: tradeNo,
PayURL: alipayResp.String(), PayURL: *wechatPayResp.CodeUrl,
}) })
} }
func CreateResourceByAlipay(c *fiber.Ctx) error { 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 { if err != nil {
return err return err
} }
@@ -284,6 +273,7 @@ func CreateResourceByAlipay(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "支付未完成,请确认后重试") return fiber.NewError(fiber.StatusBadRequest, "支付未完成,请确认后重试")
} }
// 创建套餐
payment, err := strconv.ParseFloat(alipayResp.ReceiptAmount, 64) payment, err := strconv.ParseFloat(alipayResp.ReceiptAmount, 64)
if err != nil { if err != nil {
return err return err
@@ -294,53 +284,7 @@ func CreateResourceByAlipay(c *fiber.Ctx) error {
return err return err
} }
// 获取请求缓存 err = s.Resource.CreateResourcePrepared(c.Context(), req.TradeNo, alipayResp.TradeNo, payment, paidAt)
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 { if err != nil {
return err return err
} }
@@ -350,63 +294,50 @@ func CreateResourceByAlipay(c *fiber.Ctx) error {
func CreateResourceByWechat(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 { if err != nil {
return err return err
} }
// 解析请求参数 // 解析请求参数
req := new(CreateResourceReq) req := new(PaidCreateResourceReq)
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return err return err
} }
err = q.Q.Transaction(func(q *q.Query) error { // 验证支付结果
wechatPayResp, _, err := g.WechatPay.Native.QueryOrderByOutTradeNo(c.Context(), native.QueryOrderByOutTradeNoRequest{
// 保存套餐 OutTradeNo: &req.TradeNo,
resource, err := saveResourceBalance(req, authCtx.Payload.Id, 0) Mchid: &env.WechatPayMchId,
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 { if err != nil {
return err 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 return nil
} }
func CreateResourceByBalance(c *fiber.Ctx) error { 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 { if err != nil {
return err return err
} }
@@ -417,208 +348,13 @@ func CreateResourceByBalance(c *fiber.Ctx) error {
return err 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{ err = s.Resource.CreateResourceImmediately(&req.CreateResourceData, authCtx.Payload.Id)
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 { if err != nil {
return nil, err return err
} }
// 更新用户余额 return nil
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")
} }
// endregion // endregion

View File

@@ -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,
})
}

117
web/handlers/trade.go Normal file
View File

@@ -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

View File

@@ -25,7 +25,6 @@ type Bill struct {
Type int32 `gorm:"column:type;not null" json:"type"` Type int32 `gorm:"column:type;not null" json:"type"`
BillNo string `gorm:"column:bill_no;not null" json:"bill_no"` BillNo string `gorm:"column:bill_no;not null" json:"bill_no"`
RefundID int32 `gorm:"column:refund_id" json:"refund_id"` 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"` Amount float64 `gorm:"column:amount;not null" json:"amount"`
Trade *Trade `gorm:"foreignKey:TradeID" json:"trade"` Trade *Trade `gorm:"foreignKey:TradeID" json:"trade"`
Refund *Refund `gorm:"foreignKey:RefundID" json:"refund"` Refund *Refund `gorm:"foreignKey:RefundID" json:"refund"`

View File

@@ -38,7 +38,6 @@ func newBill(db *gorm.DB, opts ...gen.DOOption) bill {
_bill.Type = field.NewInt32(tableName, "type") _bill.Type = field.NewInt32(tableName, "type")
_bill.BillNo = field.NewString(tableName, "bill_no") _bill.BillNo = field.NewString(tableName, "bill_no")
_bill.RefundID = field.NewInt32(tableName, "refund_id") _bill.RefundID = field.NewInt32(tableName, "refund_id")
_bill.Status = field.NewInt32(tableName, "status")
_bill.Amount = field.NewFloat64(tableName, "amount") _bill.Amount = field.NewFloat64(tableName, "amount")
_bill.Trade = billBelongsToTrade{ _bill.Trade = billBelongsToTrade{
db: db.Session(&gorm.Session{}), db: db.Session(&gorm.Session{}),
@@ -83,7 +82,6 @@ type bill struct {
Type field.Int32 Type field.Int32
BillNo field.String BillNo field.String
RefundID field.Int32 RefundID field.Int32
Status field.Int32
Amount field.Float64 Amount field.Float64
Trade billBelongsToTrade Trade billBelongsToTrade
@@ -117,7 +115,6 @@ func (b *bill) updateTableName(table string) *bill {
b.Type = field.NewInt32(table, "type") b.Type = field.NewInt32(table, "type")
b.BillNo = field.NewString(table, "bill_no") b.BillNo = field.NewString(table, "bill_no")
b.RefundID = field.NewInt32(table, "refund_id") b.RefundID = field.NewInt32(table, "refund_id")
b.Status = field.NewInt32(table, "status")
b.Amount = field.NewFloat64(table, "amount") b.Amount = field.NewFloat64(table, "amount")
b.fillFieldMap() b.fillFieldMap()
@@ -135,7 +132,7 @@ func (b *bill) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (b *bill) fillFieldMap() { 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["id"] = b.ID
b.fieldMap["user_id"] = b.UserID b.fieldMap["user_id"] = b.UserID
b.fieldMap["info"] = b.Info b.fieldMap["info"] = b.Info
@@ -147,7 +144,6 @@ func (b *bill) fillFieldMap() {
b.fieldMap["type"] = b.Type b.fieldMap["type"] = b.Type
b.fieldMap["bill_no"] = b.BillNo b.fieldMap["bill_no"] = b.BillNo
b.fieldMap["refund_id"] = b.RefundID b.fieldMap["refund_id"] = b.RefundID
b.fieldMap["status"] = b.Status
b.fieldMap["amount"] = b.Amount b.fieldMap["amount"] = b.Amount
} }

View File

@@ -48,7 +48,7 @@ func ApplyRouters(app *fiber.App) {
bill := api.Group("/bill") bill := api.Group("/bill")
bill.Post("/list", handlers.ListBill) bill.Post("/list", handlers.ListBill)
// 临时 // 交易
app.Get("/collect", handlers.CreateChannelGet) trade := api.Group("/trade")
app.Get("/temp", handlers.Temp) trade.Post("/callback/alipay", handlers.AlipayCallback)
} }

View File

@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"platform/pkg/rds" "platform/pkg/rds"
"strconv"
"strings" "strings"
"time" "time"
@@ -42,7 +43,7 @@ var (
ErrSequenceOverflow = errors.New("sequence overflow") ErrSequenceOverflow = errors.New("sequence overflow")
) )
func (s *IdService) GenSerial(ctx context.Context) (uint64, error) { func (s *IdService) GenSerial(ctx context.Context) (string, error) {
// 构造Redis键 // 构造Redis键
now := time.Now().Unix() now := time.Now().Unix()
key := idSerialKey(now) key := idSerialKey(now)
@@ -74,13 +75,15 @@ func (s *IdService) GenSerial(ctx context.Context) (uint64, error) {
return err return err
}, key) }, key)
if err != nil { if err != nil {
return 0, err return "", err
} }
// 组装最终ID // 组装最终ID
id := uint64((now << timestampShift) | sequence) id := uint64((now << timestampShift) | sequence)
return id, nil idStr := strconv.FormatUint(id, 10)
return idStr, nil
} }
// ParseSerial 解析ID返回其组成部分 // ParseSerial 解析ID返回其组成部分

293
web/services/resource.go Normal file
View File

@@ -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
}

View File

@@ -42,6 +42,7 @@ func (s *Server) Run() error {
// inits // inits
g.InitBaiyin() g.InitBaiyin()
g.InitAlipay() g.InitAlipay()
// g.InitWechatPay()
// config // config
s.fiber = fiber.New(fiber.Config{ s.fiber = fiber.New(fiber.Config{