优化表结构,重构模型,重新实现基于白银网关的提取节点流程

This commit is contained in:
2025-11-24 18:44:06 +08:00
parent 9a574f55cb
commit cb2a963a37
142 changed files with 6528 additions and 5808 deletions

View File

@@ -1,5 +1,11 @@
package services
import (
m "platform/web/models"
"github.com/shopspring/decimal"
)
var Bill = &billService{}
type billService struct{}
@@ -7,3 +13,31 @@ type billService struct{}
func (s *billService) GenNo() string {
return ID.GenReadable("bil")
}
func newForRecharge(uid int32, billNo string, info string, amount decimal.Decimal, trade *m.Trade) *m.Bill {
return &m.Bill{
UserID: uid,
BillNo: billNo,
TradeID: &trade.ID,
Type: m.BillTypeRecharge,
Info: &info,
Amount: amount,
}
}
func newForConsume(uid int32, billNo string, info string, amount decimal.Decimal, resource *m.Resource, trade ...*m.Trade) *m.Bill {
var bill = &m.Bill{
UserID: uid,
BillNo: billNo,
ResourceID: &resource.ID,
Type: m.BillTypeConsume,
Info: &info,
Amount: amount,
}
if len(trade) > 0 {
bill.TradeID = &trade[0].ID
}
return bill
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,308 @@
package services
import (
"database/sql/driver"
"encoding/json"
"fmt"
"log/slog"
"net/netip"
"platform/pkg/env"
"platform/pkg/u"
"platform/web/core"
e "platform/web/events"
g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models"
q "platform/web/queries"
"strings"
"time"
"github.com/hibiken/asynq"
)
type channelBaiyinService struct{}
func (s *channelBaiyinService) CreateChannels(source netip.Addr, userId int32, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) {
var filter *EdgeFilter = nil
if len(edgeFilter) > 0 {
filter = &edgeFilter[0]
}
now := time.Now()
batch := ID.GenReadable("bat")
// 获取用户白名单并检查用户 ip 地址
whitelists, err := q.Whitelist.Where(
q.Whitelist.UserID.Eq(userId),
).Find()
if err != nil {
return nil, err
}
whitelistIPs := make([]string, len(whitelists))
pass := false
for i, item := range whitelists {
whitelistIPs[i] = item.IP.String()
if item.IP.Compare(source) != 0 {
pass = true
}
}
if !pass {
return nil, core.NewBizErr("IP 地址不在白名单内")
}
// 获取用户套餐并检查
resource, err := findResource(q.Q, resourceId, userId, count, now)
if err != nil {
return nil, err
}
expire := now.Add(resource.Live)
// 获取可用通道
chans, err := lockChans(batch, count, expire)
if err != nil {
return nil, err
}
// 获取对应代理
ips := make([]driver.Valuer, 0)
findProxy := make(map[orm.Inet]*m.Proxy)
for _, ch := range chans {
ip := orm.Inet{Addr: ch.Addr()}
if _, ok := findProxy[ip]; !ok {
ips = append(ips, ip)
findProxy[ip] = nil
}
}
proxies, err := q.Proxy.Where(
q.Proxy.Type.Eq(int(m.ProxyTypeBaiYin)),
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
q.Proxy.IP.In(ips...),
).Find()
if err != nil {
return nil, core.NewBizErr("获取代理失败", err)
}
groups := make(map[*m.Proxy][]*m.Channel)
for _, proxy := range proxies {
findProxy[proxy.IP] = proxy
groups[proxy] = make([]*m.Channel, 0)
}
// 准备通道数据
actions := make([]*m.LogsUserUsage, len(chans))
channels := make([]*m.Channel, len(chans))
for i, ch := range chans {
if err != nil {
return nil, core.NewBizErr("解析通道地址失败", err)
}
// 使用记录
actions[i] = &m.LogsUserUsage{
UserID: userId,
ResourceID: resourceId,
Count: int32(count),
ISP: u.P(filter.Isp.String()),
Prov: filter.Prov,
City: filter.City,
}
// 通道数据
inet := orm.Inet{Addr: ch.Addr()}
channels[i] = &m.Channel{
UserID: userId,
ResourceID: resourceId,
BatchNo: batch,
ProxyID: findProxy[inet].ID,
Port: ch.Port(),
FilterISP: filter.Isp,
FilterProv: filter.Prov,
FilterCity: filter.City,
ExpiredAt: expire,
}
if authWhitelist {
channels[i].Whitelists = &orm.Slice[string]{Arr: whitelistIPs}
}
if authPassword {
username, password := genPassPair()
channels[i].Username = &username
channels[i].Password = &password
}
// 关联代理
proxy := findProxy[inet]
groups[proxy] = append(groups[proxy], channels[i])
}
// 保存数据
err = q.Q.Transaction(func(q *q.Query) error {
// 更新套餐用量
used := int32(count)
if u.IsSameDate(now, resource.DailyLast) {
used += resource.DailyUsed
}
switch resource.Type {
case m.ResourceTypeShort:
_, err = q.ResourceShort.
Where(
q.ResourceShort.ResourceID.Eq(resource.Id),
q.ResourceShort.Used.Eq(resource.Used),
q.ResourceShort.DailyUsed.Eq(resource.DailyUsed),
q.ResourceShort.DailyLast.Eq(resource.DailyLast),
).
UpdateSimple(
q.ResourceShort.Used.Add(int32(count)),
q.ResourceShort.DailyUsed.Value(used),
q.ResourceShort.DailyLast.Value(now),
)
case m.ResourceTypeLong:
_, err = q.ResourceLong.
Where(
q.ResourceLong.ResourceID.Eq(resource.Id),
q.ResourceLong.Used.Eq(resource.Used),
q.ResourceLong.DailyUsed.Eq(resource.DailyUsed),
q.ResourceLong.DailyLast.Eq(resource.DailyLast),
).
UpdateSimple(
q.ResourceLong.Used.Add(int32(count)),
q.ResourceLong.DailyUsed.Value(used),
q.ResourceLong.DailyLast.Value(now),
)
}
if err != nil {
return core.NewServErr("更新套餐使用记录失败", err)
}
// 保存通道和分配记录
err = q.Channel.Create(channels...)
if err != nil {
return core.NewServErr("保存通道失败", err)
}
err = q.LogsUserUsage.Create(actions...)
if err != nil {
return core.NewServErr("保存用户使用记录失败", err)
}
return nil
})
if err != nil {
return nil, err
}
// 提交异步任务关闭通道
_, err = g.Asynq.Enqueue(
e.NewRemoveChannel(e.RemoveChannelData{
Batch: batch,
IDs: core.GetIDs(channels),
}),
asynq.ProcessAt(expire),
)
if err != nil {
return nil, core.NewServErr("提交关闭通道任务失败", err)
}
// 提交配置
for proxy, chanels := range groups {
secret := strings.Split(u.Z(proxy.Secret), ":")
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
configs := make([]g.PortConfigsReq, len(chanels))
for i, channel := range chanels {
configs[i] = g.PortConfigsReq{
Port: int(channel.Port),
Status: true,
AutoEdgeConfig: &g.AutoEdgeConfig{
Isp: channel.FilterISP.String(),
Province: u.Z(channel.FilterProv),
City: u.Z(channel.FilterCity),
},
}
if authWhitelist {
configs[i].Whitelist = &channel.Whitelists.Arr
}
if authPassword {
configs[i].Userpass = u.P(fmt.Sprintf("%s:%s", *channel.Username, *channel.Password))
}
}
if env.DebugExternalChange {
err := gateway.GatewayPortConfigs(configs)
if err != nil {
return nil, core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err)
}
} else {
bytes, _ := json.Marshal(configs)
slog.Debug("提交代理端口配置", "config", string(bytes))
}
}
return channels, nil
}
func (s *channelBaiyinService) RemoveChannels(batch string, ids []int32) error {
// 获取连接数据
channels, err := q.Channel.Debug().
Preload(q.Channel.Proxy).
Where(q.Channel.ID.In(ids...)).
Find()
if err != nil {
return core.NewServErr("获取通道数据失败", err)
}
if len(channels) != len(ids) {
return core.NewServErr("获取通道数据不完整", err)
}
proxies := make(map[string]*m.Proxy, len(channels))
groups := make(map[string][]*m.Channel, len(channels))
chans := make([]string, len(channels))
for i, channel := range channels {
ip := channel.Proxy.IP.String()
groups[ip] = append(groups[ip], channel)
proxies[ip] = &channel.Proxy
chans[i] = fmt.Sprintf("%s:%d", ip, channel.Port)
}
addrs := make([]netip.AddrPort, len(channels))
for i, channel := range channels {
addrs[i] = netip.AddrPortFrom(channel.Proxy.IP.Addr, channel.Port)
}
// 释放端口
err = freeChans(batch, chans)
if err != nil {
return core.NewServErr("释放端口失败", err)
}
// 清空配置
for ip, channels := range groups {
proxy := proxies[ip]
secret := strings.Split(*proxy.Secret, ":")
gateway := g.NewGateway(ip, secret[0], secret[1])
configs := make([]g.PortConfigsReq, len(channels))
for i, channel := range channels {
configs[i] = g.PortConfigsReq{
Status: false,
Port: int(channel.Port),
Edge: &[]string{},
}
}
if env.DebugExternalChange {
err := gateway.GatewayPortConfigs(configs)
if err != nil {
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
}
} else {
bytes, _ := json.Marshal(configs)
slog.Debug("清除代理端口配置", "config", bytes)
}
}
return nil
}

View File

@@ -1,7 +1,6 @@
package services
import (
edge2 "platform/web/domains/edge"
m "platform/web/models"
q "platform/web/queries"
)
@@ -12,14 +11,14 @@ type edgeService struct{}
func (s *edgeService) AllEdges(count int, filter EdgeFilter) ([]*m.Edge, error) {
do := q.Edge.Where()
if filter.Prov != "" {
do = do.Where(q.Edge.Prov.Eq(filter.Prov))
if filter.Prov != nil {
do = do.Where(q.Edge.Prov.Eq(*filter.Prov))
}
if filter.City != "" {
do = do.Where(q.Edge.City.Eq(filter.City))
if filter.City != nil {
do = do.Where(q.Edge.City.Eq(*filter.City))
}
if filter.Isp != "" {
do = do.Where(q.Edge.Isp.Eq(int32(edge2.ISPFromStr(filter.Isp))))
if filter.Isp != nil {
do = do.Where(q.Edge.ISP.Eq(int(*filter.Isp)))
}
if count > 0 {
do = do.Limit(count)
@@ -33,11 +32,8 @@ func (s *edgeService) AllEdges(count int, filter EdgeFilter) ([]*m.Edge, error)
return edges, nil
}
type EdgeInfo struct {
}
type EdgeFilter struct {
Isp string `json:"isp"`
Prov string `json:"prov"`
City string `json:"city"`
Isp *m.EdgeISP `json:"isp"`
Prov *string `json:"prov"`
City *string `json:"city"`
}

25
web/services/proxy.go Normal file
View File

@@ -0,0 +1,25 @@
package services
import (
m "platform/web/models"
q "platform/web/queries"
"time"
)
var Proxy = &proxyService{}
type proxyService struct{}
func (s *proxyService) AllProxies(proxyType m.ProxyType, channels bool) ([]*m.Proxy, error) {
proxies, err := q.Proxy.Where(
q.Proxy.Type.Eq(int(proxyType)),
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
).Preload(
q.Proxy.Channels.On(q.Channel.ExpiredAt.Gte(time.Now())),
).Find()
if err != nil {
return nil, err
}
return proxies, nil
}

View File

@@ -5,11 +5,7 @@ import (
"fmt"
"platform/pkg/u"
"platform/web/core"
bill2 "platform/web/domains/bill"
resource2 "platform/web/domains/resource"
trade2 "platform/web/domains/trade"
g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models"
q "platform/web/queries"
"time"
@@ -24,7 +20,7 @@ type resourceService struct{}
func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data *CreateResourceData) error {
return g.Redsync.WithLock(userBalanceKey(uid), func() error {
return q.Q.Transaction(func(q *q.Query) error {
// 检查用户
// 找到用户
user, err := q.User.
Where(q.User.ID.Eq(uid)).
Take()
@@ -38,22 +34,22 @@ func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data
return ErrBalanceNotEnough
}
// 更新用户余额
_, err = q.User.
Where(q.User.ID.Eq(uid), q.User.Balance.Eq(user.Balance)).
UpdateSimple(q.User.Balance.Value(amount))
if err != nil {
return core.NewServErr("更新用户余额失败", err)
}
// 保存套餐
resource, err := createResource(q, uid, now, data)
if err != nil {
return core.NewServErr("创建套餐失败", err)
}
// 更新用户余额
_, err = q.User.
Where(q.User.ID.Eq(uid)).
UpdateSimple(q.User.Balance.Value(amount))
if err != nil {
return core.NewServErr("更新用户余额失败", err)
}
// 生成账单
err = q.Bill.Create(bill2.NewForConsume(uid, Bill.GenNo(), data.GetSubject(), data.GetAmount(), resource))
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), data.GetSubject(), data.GetAmount(), resource))
if err != nil {
return core.NewServErr("生成账单失败", err)
}
@@ -73,7 +69,7 @@ func (s *resourceService) CreateResourceByTrade(uid int32, now time.Time, data *
}
// 生成账单
err = q.Bill.Create(bill2.NewForConsume(uid, Bill.GenNo(), data.GetSubject(), data.GetAmount(), resource, trade))
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), data.GetSubject(), data.GetAmount(), resource, trade))
if err != nil {
return core.NewServErr("生成账单失败", err)
}
@@ -89,38 +85,37 @@ func createResource(q *q.Query, uid int32, now time.Time, data *CreateResourceDa
UserID: uid,
ResourceNo: u.P(ID.GenReadable("res")),
Active: true,
Type: data.Type,
}
switch data.Type {
// 短效套餐
case resource2.TypeShort:
case m.ResourceTypeShort:
var short = data.Short
if short == nil {
return nil, core.NewBizErr("短效套餐数据不能为空")
}
var duration = time.Duration(short.Expire) * 24 * time.Hour
resource.Type = int32(resource2.TypeShort)
resource.Short = &m.ResourceShort{
Type: short.Mode,
Live: short.Live,
Quota: &short.Quota,
Expire: u.P(orm.LocalDateTime(now.Add(duration))),
Expire: u.P(now.Add(duration)),
DailyLimit: short.DailyLimit,
}
// 长效套餐
case resource2.TypeLong:
case m.ResourceTypeLong:
var long = data.Long
if long == nil {
return nil, core.NewBizErr("长效套餐数据不能为空")
}
var duration = time.Duration(long.Expire) * 24 * time.Hour
resource.Type = int32(resource2.TypeLong)
resource.Long = &m.ResourceLong{
Type: long.Mode,
Live: long.Live,
Quota: &long.Quota,
Expire: u.P(orm.LocalDateTime(now.Add(duration))),
Expire: u.P(now.Add(duration)),
DailyLimit: long.DailyLimit,
}
default:
@@ -136,42 +131,42 @@ func createResource(q *q.Query, uid int32, now time.Time, data *CreateResourceDa
}
type CreateResourceData struct {
Type resource2.Type `json:"type" validate:"required"`
Type m.ResourceType `json:"type" validate:"required"`
Short *CreateShortResourceData `json:"short,omitempty"`
Long *CreateLongResourceData `json:"long,omitempty"`
}
type CreateShortResourceData struct {
Live int32 `json:"live" validate:"required,min=180"`
Mode int32 `json:"mode" validate:"required"`
Expire int32 `json:"expire"`
DailyLimit int32 `json:"daily_limit" validate:"min=2000"`
Quota int32 `json:"quota" validate:"min=10000"`
Live int32 `json:"live" validate:"required,min=180"`
Mode m.ResourceMode `json:"mode" validate:"required"`
Expire int32 `json:"expire"`
DailyLimit int32 `json:"daily_limit" validate:"min=2000"`
Quota int32 `json:"quota" validate:"min=10000"`
name string
price *decimal.Decimal
}
type CreateLongResourceData struct {
Live int32 `json:"live" validate:"required,oneof=1 4 8 12 24"`
Mode int32 `json:"mode" validate:"required,oneof=1 2"`
Expire int32 `json:"expire"`
DailyLimit int32 `json:"daily_limit" validate:"min=100"`
Quota int32 `json:"quota" validate:"min=500"`
Live int32 `json:"live" validate:"required,oneof=1 4 8 12 24"`
Mode m.ResourceMode `json:"mode" validate:"required,oneof=1 2"`
Expire int32 `json:"expire"`
DailyLimit int32 `json:"daily_limit" validate:"min=100"`
Quota int32 `json:"quota" validate:"min=500"`
name string
price *decimal.Decimal
}
func (c *CreateResourceData) GetType() trade2.Type {
return trade2.TypePurchase
func (c *CreateResourceData) GetType() m.TradeType {
return m.TradeTypePurchase
}
func (c *CreateResourceData) GetSubject() string {
switch c.Type {
case resource2.TypeShort:
case m.ResourceTypeShort:
return c.Short.GetSubject()
case resource2.TypeLong:
case m.ResourceTypeLong:
return c.Long.GetSubject()
}
panic("类型对应的数据为空")
@@ -179,9 +174,9 @@ func (c *CreateResourceData) GetSubject() string {
func (c *CreateResourceData) GetAmount() decimal.Decimal {
switch c.Type {
case resource2.TypeShort:
case m.ResourceTypeShort:
return c.Short.GetAmount()
case resource2.TypeLong:
case m.ResourceTypeLong:
return c.Long.GetAmount()
}
panic("类型对应的数据为空")
@@ -250,11 +245,11 @@ func (data *CreateLongResourceData) GetSubject() string {
func (data *CreateLongResourceData) GetAmount() decimal.Decimal {
if data.price == nil {
var factor int32 = 0
switch resource2.Mode(data.Mode) {
switch data.Mode {
case resource2.ModeTime:
case m.ResourceModeTime:
factor = data.Expire * data.DailyLimit
case resource2.ModeCount:
case m.ResourceModeQuota:
factor = data.Quota
}
@@ -283,14 +278,14 @@ func (data *CreateLongResourceData) GetAmount() decimal.Decimal {
type ResourceOnTradeComplete struct{}
func (r ResourceOnTradeComplete) Check(t trade2.Type) (trade2.ProductInfo, bool) {
if t == trade2.TypePurchase {
func (r ResourceOnTradeComplete) Check(t m.TradeType) (ProductInfo, bool) {
if t == m.TradeTypePurchase {
return &CreateResourceData{}, true
}
return nil, false
}
func (r ResourceOnTradeComplete) OnTradeComplete(info trade2.ProductInfo, trade *m.Trade) error {
func (r ResourceOnTradeComplete) OnTradeComplete(info ProductInfo, trade *m.Trade) error {
return Resource.CreateResourceByTrade(trade.UserID, time.Time(*trade.CompletedAt), info.(*CreateResourceData), trade)
}

View File

@@ -10,11 +10,8 @@ import (
"platform/pkg/env"
"platform/pkg/u"
"platform/web/core"
coupon2 "platform/web/domains/coupon"
trade2 "platform/web/domains/trade"
e "platform/web/events"
g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models"
q "platform/web/queries"
"time"
@@ -53,7 +50,7 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
coupon, err := q.Coupon.
Where(
q.Coupon.Code.Eq(*data.CouponCode),
q.Coupon.Status.Eq(int32(coupon2.StatusUnused)),
q.Coupon.Status.Eq(int(m.CouponStatusUnused)),
).
Take()
if err != nil {
@@ -67,7 +64,7 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
if !expireAt.IsZero() && expireAt.Before(now) {
_, err = q.Coupon.
Where(q.Coupon.ID.Eq(coupon.ID)).
Update(q.Coupon.Status, coupon2.StatusExpired)
Update(q.Coupon.Status, m.CouponStatusExpired)
if err != nil {
return nil, err
}
@@ -86,7 +83,7 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
if expireAt.IsZero() {
_, err = q.Coupon.
Where(q.Coupon.ID.Eq(coupon.ID)).
Update(q.Coupon.Status, int32(coupon2.StatusUsed))
Update(q.Coupon.Status, int(m.CouponStatusUsed))
if err != nil {
return nil, err
}
@@ -112,7 +109,7 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
switch {
// 支付宝 + 电脑网站
case method == trade2.MethodAlipay && platform == trade2.PlatformDesktop:
case method == m.TradeMethodAlipay && platform == m.TradePlatformPC:
resp, err := g.Alipay.TradePagePay(alipay.TradePagePay{
QRPayMode: "4",
QRCodeWidth: "196", // 二维码宽度需要-4支付宝页面布局有问题
@@ -130,7 +127,7 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
paymentUrl = resp.String()
// 微信 + 电脑网站
case method == trade2.MethodWeChat && platform == trade2.PlatformDesktop:
case method == m.TradeMethodWechat && platform == m.TradePlatformPC:
resp, _, err := g.WechatPay.Native.Prepay(context.Background(), native.PrepayRequest{
Appid: &env.WechatPayAppId,
Mchid: &env.WechatPayMchId,
@@ -149,14 +146,14 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
// 商福通 + 电脑网站
case
method == trade2.MethodSftAlipay && platform == trade2.PlatformDesktop,
method == trade2.MethodSftWeChat && platform == trade2.PlatformDesktop:
method == m.TradeMethodSftAlipay && platform == m.TradePlatformPC,
method == m.TradeMethodSftWechat && platform == m.TradePlatformPC:
var payType g.SftPayType
switch method {
case trade2.MethodSftAlipay:
case m.TradeMethodSftAlipay:
payType = g.SftAlipay
case trade2.MethodSftWeChat:
case m.TradeMethodSftWechat:
payType = g.SftWeChat
default:
panic("unhandled default case")
@@ -178,13 +175,13 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
// 商福通 + 手机网站
case
method == trade2.MethodSftAlipay && platform == trade2.PlatformMobile,
method == trade2.MethodSftWeChat && platform == trade2.PlatformMobile:
method == m.TradeMethodSftAlipay && platform == m.TradePlatformMobile,
method == m.TradeMethodSftWechat && platform == m.TradePlatformMobile:
var payType g.SftPayType
switch method {
case trade2.MethodSftAlipay:
case m.TradeMethodSftAlipay:
payType = g.SftAlipay
case trade2.MethodSftWeChat:
case m.TradeMethodSftWechat:
payType = g.SftWeChat
default:
panic("unhandled default case")
@@ -214,11 +211,11 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
err = q.Trade.Create(&m.Trade{
UserID: uid,
InnerNo: tradeNo,
Type: int32(tType),
Type: tType,
Subject: subject,
Amount: amount,
Method: int32(method),
Platform: int32(platform),
Method: method,
Platform: platform,
PaymentURL: &paymentUrl,
})
if err != nil {
@@ -266,11 +263,11 @@ func (s *tradeService) CompleteTrade(data *ModifyTradeData) error {
if err != nil {
return core.NewServErr("检查订单状态失败", err)
}
if result.Status != trade2.StatusSuccess {
if result.Status != m.TradeStatusSuccess {
switch result.Status {
case trade2.StatusPending:
case m.TradeStatusPending:
return core.NewBizErr("订单未支付")
case trade2.StatusCanceled:
case m.TradeStatusCanceled:
return core.NewBizErr("订单已过期")
}
}
@@ -331,22 +328,22 @@ func completeTrade(data *OnTradeCompletedData) (*m.Trade, error) {
}
// 检查交易状态
switch trade2.Status(trade.Status) {
case trade2.StatusCanceled:
switch trade.Status {
case m.TradeStatusCanceled:
return core.NewBizErr("交易已取消")
case trade2.StatusSuccess:
case m.TradeStatusSuccess:
return nil // 跳过更新交易信息
case trade2.StatusPending:
case m.TradeStatusPending:
}
// 更新交易信息
trade.Status = int32(trade2.StatusSuccess)
trade.Status = m.TradeStatusSuccess
trade.OuterNo = &transId
trade.Payment = payment
trade.Acquirer = u.P(int32(acquirer))
trade.CompletedAt = u.P(orm.LocalDateTime(paidAt))
trade.Acquirer = u.P(acquirer)
trade.CompletedAt = u.P(paidAt)
rs, err := q.Trade.
Where(q.Trade.InnerNo.Eq(tradeNo), q.Trade.Status.Eq(int32(trade2.StatusPending))).
Where(q.Trade.InnerNo.Eq(tradeNo), q.Trade.Status.Eq(int(m.TradeStatusPending))).
Updates(trade)
if rs.RowsAffected == 0 {
return core.NewBizErr("交易状态已发生变化")
@@ -372,13 +369,13 @@ func afterTradeComplete(trade *m.Trade) error {
}
// 执行资源创建
var ComplementEvents = []trade2.CompleteEvent{
var ComplementEvents = []CompleteEvent{
ResourceOnTradeComplete{},
UserOnTradeComplete{},
}
for _, event := range ComplementEvents {
info, ok := event.Check(trade2.Type(trade.Type))
info, ok := event.Check(trade.Type)
if !ok {
continue
}
@@ -405,7 +402,7 @@ func (s *tradeService) CancelTrade(data *ModifyTradeData, now time.Time) error {
return g.Redsync.WithLock(tradeLockKey(tradeNo), func() error {
switch method {
case trade2.MethodAlipay:
case m.TradeMethodAlipay:
resp, err := g.Alipay.TradeCancel(context.Background(), alipay.TradeCancel{
OutTradeNo: tradeNo,
})
@@ -417,7 +414,7 @@ func (s *tradeService) CancelTrade(data *ModifyTradeData, now time.Time) error {
return errors.New("上游取消交易失败")
}
case trade2.MethodWeChat:
case m.TradeMethodWechat:
resp, err := g.WechatPay.Native.CloseOrder(context.Background(), native.CloseOrderRequest{
Mchid: &env.WechatPayMchId,
OutTradeNo: &tradeNo,
@@ -435,7 +432,7 @@ func (s *tradeService) CancelTrade(data *ModifyTradeData, now time.Time) error {
return errors.New("上游取消交易失败")
}
case trade2.MethodSft, trade2.MethodSftAlipay, trade2.MethodSftWeChat:
case m.TradeMethodSft, m.TradeMethodSftAlipay, m.TradeMethodSftWechat:
_, err := g.SFTPay.OrderClose(&g.OrderCloseReq{
MchOrderNo: &tradeNo,
})
@@ -468,7 +465,7 @@ func (s *tradeService) OnTradeCanceled(tradeNo string, now time.Time) error {
func cancelTrade(tradeNo string, now time.Time) error {
return q.Q.Transaction(func(q *q.Query) error {
// 获取交易信息
var status trade2.Status
var status m.TradeStatus
err := q.Trade.
Where(q.Trade.InnerNo.Eq(tradeNo)).
Select(q.Trade.Status).
@@ -479,19 +476,19 @@ func cancelTrade(tradeNo string, now time.Time) error {
// 检查交易状态
switch status {
case trade2.StatusCanceled:
case m.TradeStatusCanceled:
return core.NewBizErr("交易已取消")
case trade2.StatusSuccess:
case m.TradeStatusSuccess:
return core.NewBizErr("交易已完成")
case trade2.StatusPending:
case m.TradeStatusPending:
}
// 更新交易状态
_, err = q.Trade.
Where(q.Trade.InnerNo.Eq(tradeNo)).
UpdateSimple(
q.Trade.Status.Value(int32(trade2.StatusCanceled)),
q.Trade.CanceledAt.Value(orm.LocalDateTime(now)),
q.Trade.Status.Value(int(m.TradeStatusCanceled)),
q.Trade.CanceledAt.Value(now),
)
if err != nil {
return core.NewServErr("更新交易状态失败", err)
@@ -518,7 +515,7 @@ func (s *tradeService) CheckTrade(data *ModifyTradeData) (*CheckTradeResult, err
switch method {
// 支付宝
case trade2.MethodAlipay:
case m.TradeMethodAlipay:
// 查询交易状态
resp, err := g.Alipay.TradeQuery(context.Background(), alipay.TradeQuery{
@@ -537,15 +534,15 @@ func (s *tradeService) CheckTrade(data *ModifyTradeData) (*CheckTradeResult, err
switch resp.TradeStatus {
case alipay.TradeStatusWaitBuyerPay:
result.Status = trade2.StatusPending
result.Status = m.TradeStatusPending
case alipay.TradeStatusClosed:
result.Status = trade2.StatusCanceled
result.Status = m.TradeStatusCanceled
case alipay.TradeStatusSuccess, alipay.TradeStatusFinished:
result.Status = trade2.StatusSuccess
result.Status = m.TradeStatusSuccess
result.Success = &TradeSuccessResult{}
result.Success.Acquirer = trade2.AcquirerAlipay
result.Success.Acquirer = m.TradeAcquirerAlipay
result.Success.Payment, err = decimal.NewFromString(resp.TotalAmount)
if err != nil {
return nil, err
@@ -557,7 +554,7 @@ func (s *tradeService) CheckTrade(data *ModifyTradeData) (*CheckTradeResult, err
}
// 微信
case trade2.MethodWeChat:
case m.TradeMethodWechat:
// 查询交易状态
resp, _, err := g.WechatPay.Native.QueryOrderByOutTradeNo(context.Background(), native.QueryOrderByOutTradeNoRequest{
@@ -583,15 +580,15 @@ func (s *tradeService) CheckTrade(data *ModifyTradeData) (*CheckTradeResult, err
switch *resp.TradeState {
case "NOTPAY":
result.Status = trade2.StatusPending
result.Status = m.TradeStatusPending
case "CLOSED":
result.Status = trade2.StatusCanceled
result.Status = m.TradeStatusCanceled
case "SUCCESS", "REFUND":
result.Status = trade2.StatusSuccess
result.Status = m.TradeStatusSuccess
result.Success = &TradeSuccessResult{}
result.Success.Acquirer = trade2.AcquirerWeChat
result.Success.Acquirer = m.TradeAcquirerWechat
result.Success.Payment = decimal.NewFromInt(*resp.Amount.PayerTotal).Div(decimal.NewFromInt(100))
result.Success.Time, err = time.Parse(time.RFC3339, *resp.SuccessTime)
if err != nil {
@@ -600,7 +597,7 @@ func (s *tradeService) CheckTrade(data *ModifyTradeData) (*CheckTradeResult, err
}
// 商福通
case trade2.MethodSft, trade2.MethodSftAlipay, trade2.MethodSftWeChat:
case m.TradeMethodSft, m.TradeMethodSftAlipay, m.TradeMethodSftWechat:
// 查询交易状态
resp, err := g.SFTPay.QueryTrade(&g.QueryTradeReq{
@@ -619,21 +616,21 @@ func (s *tradeService) CheckTrade(data *ModifyTradeData) (*CheckTradeResult, err
switch resp.State {
case g.SftInit, g.SftTradeAwait, g.SftTradeFail:
result.Status = trade2.StatusPending
result.Status = m.TradeStatusPending
case g.SftTradeClosed, g.SftTradeCancel:
result.Status = trade2.StatusCanceled
result.Status = m.TradeStatusCanceled
case g.SftTradeSuccess, g.SftTradeRefund, g.SftRefundIng:
result.Status = trade2.StatusSuccess
result.Status = m.TradeStatusSuccess
result.Success = &TradeSuccessResult{}
switch resp.PayType {
case "WECHAT":
result.Success.Acquirer = trade2.AcquirerWeChat
result.Success.Acquirer = m.TradeAcquirerWechat
case "ALIPAY":
result.Success.Acquirer = trade2.AcquirerAlipay
result.Success.Acquirer = m.TradeAcquirerAlipay
case "UNIONPAY":
result.Success.Acquirer = trade2.AcquirerUnionPay
result.Success.Acquirer = m.TradeAcquirerUnionPay
}
result.Success.Payment = decimal.NewFromInt(resp.Amount).Div(decimal.NewFromInt(100))
result.Success.Time, err = time.Parse("2006-01-02 15:04:05", *resp.PayTime)
@@ -659,10 +656,10 @@ func tradeLockKey(no string) string {
}
type CreateTradeData struct {
Platform trade2.Platform `json:"platform" validate:"required"`
Method trade2.Method `json:"method" validate:"required"`
Platform m.TradePlatform `json:"platform" validate:"required"`
Method m.TradeMethod `json:"method" validate:"required"`
CouponCode *string `json:"coupon_code"`
Product trade2.ProductInfo
Product ProductInfo
}
type CreateTradeResult struct {
@@ -672,17 +669,17 @@ type CreateTradeResult struct {
type ModifyTradeData struct {
TradeNo string `json:"trade_no" validate:"required"`
Method trade2.Method `json:"method" validate:"required"`
Method m.TradeMethod `json:"method" validate:"required"`
}
type CheckTradeResult struct {
TransId string
Status trade2.Status
Status m.TradeStatus
Success *TradeSuccessResult
}
type TradeSuccessResult struct {
Acquirer trade2.Acquirer
Acquirer m.TradeAcquirer
Payment decimal.Decimal
Time time.Time
}
@@ -693,6 +690,19 @@ type OnTradeCompletedData struct {
*TradeSuccessResult
}
type ProductInfo interface {
GetType() m.TradeType
GetSubject() string
GetAmount() decimal.Decimal
Serialize() (string, error)
Deserialize(str string) error
}
type CompleteEvent interface {
Check(t m.TradeType) (ProductInfo, bool)
OnTradeComplete(info ProductInfo, trade *m.Trade) error
}
type TradeErr string
func (e TradeErr) Error() string {

View File

@@ -4,8 +4,6 @@ import (
"encoding/json"
"fmt"
"platform/web/core"
bill2 "platform/web/domains/bill"
trade2 "platform/web/domains/trade"
g "platform/web/globals"
m "platform/web/models"
q "platform/web/queries"
@@ -27,7 +25,7 @@ func (s *userService) UpdateBalanceByTrade(uid int32, info *RechargeProductInfo,
}
// 生成账单
err = q.Bill.Create(bill2.NewForRecharge(uid, Bill.GenNo(), info.GetSubject(), info.GetAmount(), trade))
err = q.Bill.Create(newForRecharge(uid, Bill.GenNo(), info.GetSubject(), info.GetAmount(), trade))
if err != nil {
return core.NewServErr("生成账单失败", err)
}
@@ -73,8 +71,8 @@ type RechargeProductInfo struct {
Amount int `json:"amount"`
}
func (r *RechargeProductInfo) GetType() trade2.Type {
return trade2.TypeRecharge
func (r *RechargeProductInfo) GetType() m.TradeType {
return m.TradeTypeRecharge
}
func (r *RechargeProductInfo) GetSubject() string {
@@ -96,13 +94,13 @@ func (r *RechargeProductInfo) Deserialize(str string) error {
type UserOnTradeComplete struct{}
func (u UserOnTradeComplete) Check(t trade2.Type) (trade2.ProductInfo, bool) {
if t == trade2.TypeRecharge {
func (u UserOnTradeComplete) Check(t m.TradeType) (ProductInfo, bool) {
if t == m.TradeTypeRecharge {
return &RechargeProductInfo{}, true
}
return nil, false
}
func (u UserOnTradeComplete) OnTradeComplete(info trade2.ProductInfo, trade *m.Trade) error {
func (u UserOnTradeComplete) OnTradeComplete(info ProductInfo, trade *m.Trade) error {
return User.UpdateBalanceByTrade(trade.UserID, info.(*RechargeProductInfo), trade)
}