自动生成账单,自动支付账单

This commit is contained in:
刘祥超
2021-11-11 08:30:53 +08:00
parent ed74adebac
commit 3e76bf6e5a
12 changed files with 372 additions and 101 deletions

View File

@@ -3,14 +3,36 @@ package accounts
import ( import (
"github.com/TeaOSLab/EdgeAPI/internal/db/models" "github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/errors" "github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/TeaOSLab/EdgeAPI/internal/remotelogs"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea" "github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs" "github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types" "github.com/iwind/TeaGo/types"
"time"
) )
func init() {
dbs.OnReadyDone(func() {
go func() {
// 自动支付账单任务
var ticker = time.NewTicker(12 * time.Hour)
for range ticker.C {
if SharedUserAccountDAO.Instance != nil {
err := SharedUserAccountDAO.Instance.RunTx(func(tx *dbs.Tx) error {
return SharedUserAccountDAO.PayBills(tx)
})
if err != nil {
remotelogs.Error("USER_ACCOUNT_DAO", "pay bills task failed: "+err.Error())
}
}
}
}()
})
}
type UserAccountDAO dbs.DAO type UserAccountDAO dbs.DAO
func NewUserAccountDAO() *UserAccountDAO { func NewUserAccountDAO() *UserAccountDAO {
@@ -170,3 +192,46 @@ func (this *UserAccountDAO) ListAccounts(tx *dbs.Tx, keyword string, offset int6
FindAll() FindAll()
return return
} }
// PayBills 尝试自动支付账单
func (this *UserAccountDAO) PayBills(tx *dbs.Tx) error {
bills, err := models.SharedUserBillDAO.FindUnpaidBills(tx, 10000)
if err != nil {
return err
}
// 先支付久远的
lists.Reverse(bills)
for _, bill := range bills {
if bill.Amount <= 0 {
err = models.SharedUserBillDAO.UpdateUserBillIsPaid(tx, int64(bill.Id), true)
if err != nil {
return err
}
continue
}
account, err := SharedUserAccountDAO.FindUserAccountWithUserId(tx, int64(bill.UserId))
if err != nil {
return err
}
if account == nil || account.Total < bill.Amount {
continue
}
// 扣款
err = SharedUserAccountDAO.UpdateUserAccount(tx, int64(account.Id), -float32(bill.Amount), userconfigs.AccountEventTypePayBill, "支付账单"+bill.Code, maps.Map{"billId": bill.Id})
if err != nil {
return err
}
// 改为已支付
err = models.SharedUserBillDAO.UpdateUserBillIsPaid(tx, int64(bill.Id), true)
if err != nil {
return err
}
}
return nil
}

View File

@@ -3,4 +3,16 @@ package accounts
import ( import (
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
_ "github.com/iwind/TeaGo/bootstrap" _ "github.com/iwind/TeaGo/bootstrap"
"github.com/iwind/TeaGo/dbs"
"testing"
) )
func TestUserAccountDAO_PayBills(t *testing.T) {
dbs.NotifyReady()
err := NewUserAccountDAO().PayBills(nil)
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}

View File

@@ -35,7 +35,7 @@ func init() {
}) })
} }
// 启用条目 // EnableNodePriceItem 启用条目
func (this *NodePriceItemDAO) EnableNodePriceItem(tx *dbs.Tx, id int64) error { func (this *NodePriceItemDAO) EnableNodePriceItem(tx *dbs.Tx, id int64) error {
_, err := this.Query(tx). _, err := this.Query(tx).
Pk(id). Pk(id).
@@ -44,7 +44,7 @@ func (this *NodePriceItemDAO) EnableNodePriceItem(tx *dbs.Tx, id int64) error {
return err return err
} }
// 禁用条目 // DisableNodePriceItem 禁用条目
func (this *NodePriceItemDAO) DisableNodePriceItem(tx *dbs.Tx, id int64) error { func (this *NodePriceItemDAO) DisableNodePriceItem(tx *dbs.Tx, id int64) error {
_, err := this.Query(tx). _, err := this.Query(tx).
Pk(id). Pk(id).
@@ -53,7 +53,7 @@ func (this *NodePriceItemDAO) DisableNodePriceItem(tx *dbs.Tx, id int64) error {
return err return err
} }
// 查找启用中的条目 // FindEnabledNodePriceItem 查找启用中的条目
func (this *NodePriceItemDAO) FindEnabledNodePriceItem(tx *dbs.Tx, id int64) (*NodePriceItem, error) { func (this *NodePriceItemDAO) FindEnabledNodePriceItem(tx *dbs.Tx, id int64) (*NodePriceItem, error) {
result, err := this.Query(tx). result, err := this.Query(tx).
Pk(id). Pk(id).
@@ -65,7 +65,7 @@ func (this *NodePriceItemDAO) FindEnabledNodePriceItem(tx *dbs.Tx, id int64) (*N
return result.(*NodePriceItem), err return result.(*NodePriceItem), err
} }
// 根据主键查找名称 // FindNodePriceItemName 根据主键查找名称
func (this *NodePriceItemDAO) FindNodePriceItemName(tx *dbs.Tx, id int64) (string, error) { func (this *NodePriceItemDAO) FindNodePriceItemName(tx *dbs.Tx, id int64) (string, error) {
return this.Query(tx). return this.Query(tx).
Pk(id). Pk(id).
@@ -73,7 +73,7 @@ func (this *NodePriceItemDAO) FindNodePriceItemName(tx *dbs.Tx, id int64) (strin
FindStringCol("") FindStringCol("")
} }
// 创建价格 // CreateItem 创建价格
func (this *NodePriceItemDAO) CreateItem(tx *dbs.Tx, name string, itemType string, bitsFrom, bitsTo int64) (int64, error) { func (this *NodePriceItemDAO) CreateItem(tx *dbs.Tx, name string, itemType string, bitsFrom, bitsTo int64) (int64, error) {
op := NewNodePriceItemOperator() op := NewNodePriceItemOperator()
op.Name = name op.Name = name
@@ -85,7 +85,7 @@ func (this *NodePriceItemDAO) CreateItem(tx *dbs.Tx, name string, itemType strin
return this.SaveInt64(tx, op) return this.SaveInt64(tx, op)
} }
// 修改价格 // UpdateItem 修改价格
func (this *NodePriceItemDAO) UpdateItem(tx *dbs.Tx, itemId int64, name string, bitsFrom, bitsTo int64) error { func (this *NodePriceItemDAO) UpdateItem(tx *dbs.Tx, itemId int64, name string, bitsFrom, bitsTo int64) error {
if itemId <= 0 { if itemId <= 0 {
return errors.New("invalid itemId") return errors.New("invalid itemId")
@@ -98,7 +98,7 @@ func (this *NodePriceItemDAO) UpdateItem(tx *dbs.Tx, itemId int64, name string,
return this.Save(tx, op) return this.Save(tx, op)
} }
// 列出某个区域的所有价格 // FindAllEnabledRegionPrices 列出某个区域的所有价格
func (this *NodePriceItemDAO) FindAllEnabledRegionPrices(tx *dbs.Tx, priceType string) (result []*NodePriceItem, err error) { func (this *NodePriceItemDAO) FindAllEnabledRegionPrices(tx *dbs.Tx, priceType string) (result []*NodePriceItem, err error) {
_, err = this.Query(tx). _, err = this.Query(tx).
Attr("type", priceType). Attr("type", priceType).
@@ -109,7 +109,7 @@ func (this *NodePriceItemDAO) FindAllEnabledRegionPrices(tx *dbs.Tx, priceType s
return return
} }
// 列出某个区域的所有启用的价格 // FindAllEnabledAndOnRegionPrices 列出某个区域的所有启用的价格
func (this *NodePriceItemDAO) FindAllEnabledAndOnRegionPrices(tx *dbs.Tx, priceType string) (result []*NodePriceItem, err error) { func (this *NodePriceItemDAO) FindAllEnabledAndOnRegionPrices(tx *dbs.Tx, priceType string) (result []*NodePriceItem, err error) {
_, err = this.Query(tx). _, err = this.Query(tx).
Attr("type", priceType). Attr("type", priceType).
@@ -121,7 +121,7 @@ func (this *NodePriceItemDAO) FindAllEnabledAndOnRegionPrices(tx *dbs.Tx, priceT
return return
} }
// 根据字节查找付费项目 // SearchItemsWithBytes 根据字节查找付费项目
func (this *NodePriceItemDAO) SearchItemsWithBytes(items []*NodePriceItem, bytes int64) int64 { func (this *NodePriceItemDAO) SearchItemsWithBytes(items []*NodePriceItem, bytes int64) int64 {
bytes *= 8 bytes *= 8

View File

@@ -35,7 +35,7 @@ func init() {
}) })
} }
// 启用条目 // EnableNodeRegion 启用条目
func (this *NodeRegionDAO) EnableNodeRegion(tx *dbs.Tx, id int64) error { func (this *NodeRegionDAO) EnableNodeRegion(tx *dbs.Tx, id int64) error {
_, err := this.Query(tx). _, err := this.Query(tx).
Pk(id). Pk(id).
@@ -44,7 +44,7 @@ func (this *NodeRegionDAO) EnableNodeRegion(tx *dbs.Tx, id int64) error {
return err return err
} }
// 禁用条目 // DisableNodeRegion 禁用条目
func (this *NodeRegionDAO) DisableNodeRegion(tx *dbs.Tx, id int64) error { func (this *NodeRegionDAO) DisableNodeRegion(tx *dbs.Tx, id int64) error {
_, err := this.Query(tx). _, err := this.Query(tx).
Pk(id). Pk(id).
@@ -53,7 +53,7 @@ func (this *NodeRegionDAO) DisableNodeRegion(tx *dbs.Tx, id int64) error {
return err return err
} }
// 查找启用中的条目 // FindEnabledNodeRegion 查找启用中的条目
func (this *NodeRegionDAO) FindEnabledNodeRegion(tx *dbs.Tx, id int64) (*NodeRegion, error) { func (this *NodeRegionDAO) FindEnabledNodeRegion(tx *dbs.Tx, id int64) (*NodeRegion, error) {
result, err := this.Query(tx). result, err := this.Query(tx).
Pk(id). Pk(id).
@@ -65,7 +65,7 @@ func (this *NodeRegionDAO) FindEnabledNodeRegion(tx *dbs.Tx, id int64) (*NodeReg
return result.(*NodeRegion), err return result.(*NodeRegion), err
} }
// 根据主键查找名称 // FindNodeRegionName 根据主键查找名称
func (this *NodeRegionDAO) FindNodeRegionName(tx *dbs.Tx, id int64) (string, error) { func (this *NodeRegionDAO) FindNodeRegionName(tx *dbs.Tx, id int64) (string, error) {
return this.Query(tx). return this.Query(tx).
Pk(id). Pk(id).
@@ -73,7 +73,7 @@ func (this *NodeRegionDAO) FindNodeRegionName(tx *dbs.Tx, id int64) (string, err
FindStringCol("") FindStringCol("")
} }
// 创建区域 // CreateRegion 创建区域
func (this *NodeRegionDAO) CreateRegion(tx *dbs.Tx, adminId int64, name string, description string) (int64, error) { func (this *NodeRegionDAO) CreateRegion(tx *dbs.Tx, adminId int64, name string, description string) (int64, error) {
op := NewNodeRegionOperator() op := NewNodeRegionOperator()
op.AdminId = adminId op.AdminId = adminId
@@ -84,7 +84,7 @@ func (this *NodeRegionDAO) CreateRegion(tx *dbs.Tx, adminId int64, name string,
return this.SaveInt64(tx, op) return this.SaveInt64(tx, op)
} }
// 修改区域 // UpdateRegion 修改区域
func (this *NodeRegionDAO) UpdateRegion(tx *dbs.Tx, regionId int64, name string, description string, isOn bool) error { func (this *NodeRegionDAO) UpdateRegion(tx *dbs.Tx, regionId int64, name string, description string, isOn bool) error {
if regionId <= 0 { if regionId <= 0 {
return errors.New("invalid regionId") return errors.New("invalid regionId")
@@ -97,7 +97,7 @@ func (this *NodeRegionDAO) UpdateRegion(tx *dbs.Tx, regionId int64, name string,
return this.Save(tx, op) return this.Save(tx, op)
} }
// 列出所有区域 // FindAllEnabledRegions 列出所有区域
func (this *NodeRegionDAO) FindAllEnabledRegions(tx *dbs.Tx) (result []*NodeRegion, err error) { func (this *NodeRegionDAO) FindAllEnabledRegions(tx *dbs.Tx) (result []*NodeRegion, err error) {
_, err = this.Query(tx). _, err = this.Query(tx).
State(NodeRegionStateEnabled). State(NodeRegionStateEnabled).
@@ -108,7 +108,7 @@ func (this *NodeRegionDAO) FindAllEnabledRegions(tx *dbs.Tx) (result []*NodeRegi
return return
} }
// 列出所有价格 // FindAllEnabledRegionPrices 列出所有价格
func (this *NodeRegionDAO) FindAllEnabledRegionPrices(tx *dbs.Tx) (result []*NodeRegion, err error) { func (this *NodeRegionDAO) FindAllEnabledRegionPrices(tx *dbs.Tx) (result []*NodeRegion, err error) {
_, err = this.Query(tx). _, err = this.Query(tx).
State(NodeRegionStateEnabled). State(NodeRegionStateEnabled).
@@ -120,7 +120,7 @@ func (this *NodeRegionDAO) FindAllEnabledRegionPrices(tx *dbs.Tx) (result []*Nod
return return
} }
// 列出所有启用的区域 // FindAllEnabledAndOnRegions 列出所有启用的区域
func (this *NodeRegionDAO) FindAllEnabledAndOnRegions(tx *dbs.Tx) (result []*NodeRegion, err error) { func (this *NodeRegionDAO) FindAllEnabledAndOnRegions(tx *dbs.Tx) (result []*NodeRegion, err error) {
_, err = this.Query(tx). _, err = this.Query(tx).
State(NodeRegionStateEnabled). State(NodeRegionStateEnabled).
@@ -132,7 +132,7 @@ func (this *NodeRegionDAO) FindAllEnabledAndOnRegions(tx *dbs.Tx) (result []*Nod
return return
} }
// 排序 // UpdateRegionOrders 排序
func (this *NodeRegionDAO) UpdateRegionOrders(tx *dbs.Tx, regionIds []int64) error { func (this *NodeRegionDAO) UpdateRegionOrders(tx *dbs.Tx, regionIds []int64) error {
order := len(regionIds) order := len(regionIds)
for _, regionId := range regionIds { for _, regionId := range regionIds {
@@ -148,7 +148,7 @@ func (this *NodeRegionDAO) UpdateRegionOrders(tx *dbs.Tx, regionIds []int64) err
return nil return nil
} }
// 修改价格项价格 // UpdateRegionItemPrice 修改价格项价格
func (this *NodeRegionDAO) UpdateRegionItemPrice(tx *dbs.Tx, regionId int64, itemId int64, price float32) error { func (this *NodeRegionDAO) UpdateRegionItemPrice(tx *dbs.Tx, regionId int64, itemId int64, price float32) error {
one, err := this.Query(tx). one, err := this.Query(tx).
Pk(regionId). Pk(regionId).

View File

@@ -194,6 +194,15 @@ func (this *PlanDAO) ListEnabledPlans(tx *dbs.Tx, offset int64, size int64) (res
return return
} }
// FindAllEnabledPlans 查找所有可用套餐
func (this *PlanDAO) FindAllEnabledPlans(tx *dbs.Tx) (result []*Plan, err error) {
_, err = this.Query(tx).
State(PlanStateEnabled).
Slice(&result).
FindAll()
return
}
// SortPlans 增加排序 // SortPlans 增加排序
func (this *PlanDAO) SortPlans(tx *dbs.Tx, planIds []int64) error { func (this *PlanDAO) SortPlans(tx *dbs.Tx, planIds []int64) error {
if len(planIds) == 0 { if len(planIds) == 0 {

View File

@@ -88,6 +88,7 @@ func (this *ServerDailyStatDAO) SaveStats(tx *dbs.Tx, stats []*pb.ServerDailySta
"countCachedRequests": stat.CountCachedRequests, "countCachedRequests": stat.CountCachedRequests,
"countAttackRequests": stat.CountAttackRequests, "countAttackRequests": stat.CountAttackRequests,
"attackBytes": stat.AttackBytes, "attackBytes": stat.AttackBytes,
"planId": stat.PlanId,
"day": day, "day": day,
"hour": hour, "hour": hour,
"timeFrom": timeFrom, "timeFrom": timeFrom,
@@ -99,6 +100,7 @@ func (this *ServerDailyStatDAO) SaveStats(tx *dbs.Tx, stats []*pb.ServerDailySta
"countCachedRequests": dbs.SQL("countCachedRequests+:countCachedRequests"), "countCachedRequests": dbs.SQL("countCachedRequests+:countCachedRequests"),
"countAttackRequests": dbs.SQL("countAttackRequests+:countAttackRequests"), "countAttackRequests": dbs.SQL("countAttackRequests+:countAttackRequests"),
"attackBytes": dbs.SQL("attackBytes+:attackBytes"), "attackBytes": dbs.SQL("attackBytes+:attackBytes"),
"planId": stat.PlanId,
}) })
if err != nil { if err != nil {
return err return err
@@ -139,6 +141,30 @@ func (this *ServerDailyStatDAO) SumUserMonthly(tx *dbs.Tx, userId int64, regionI
SumInt64("bytes", 0) SumInt64("bytes", 0)
} }
// SumUserMonthlyWithoutPlan 根据用户计算某月合计并排除套餐
// month 格式为YYYYMM
func (this *ServerDailyStatDAO) SumUserMonthlyWithoutPlan(tx *dbs.Tx, userId int64, regionId int64, month string) (int64, error) {
query := this.Query(tx)
if regionId > 0 {
query.Attr("regionId", regionId)
}
return query.
Attr("planId", 0).
Between("day", month+"01", month+"32").
Attr("userId", userId).
SumInt64("bytes", 0)
}
// SumUserMonthlyFee 计算用户某个月费用
// month 格式为YYYYMM
func (this *ServerDailyStatDAO) SumUserMonthlyFee(tx *dbs.Tx, userId int64, month string) (float64, error) {
return this.Query(tx).
Attr("userId", userId).
Between("day", month+"01", month+"32").
Gt("fee", 0).
Sum("fee", 0)
}
// SumUserMonthlyPeek 获取某月带宽峰值 // SumUserMonthlyPeek 获取某月带宽峰值
// month 格式为YYYYMM // month 格式为YYYYMM
func (this *ServerDailyStatDAO) SumUserMonthlyPeek(tx *dbs.Tx, userId int64, regionId int64, month string) (int64, error) { func (this *ServerDailyStatDAO) SumUserMonthlyPeek(tx *dbs.Tx, userId int64, regionId int64, month string) (int64, error) {
@@ -345,6 +371,17 @@ func (this *ServerDailyStatDAO) FindDailyStats(tx *dbs.Tx, serverId int64, dayFr
return return
} }
// FindMonthlyStatsWithPlan 查找某月有套餐的流量
// month YYYYMM
func (this *ServerDailyStatDAO) FindMonthlyStatsWithPlan(tx *dbs.Tx, month string) (result []*ServerDailyStat, err error) {
_, err = this.Query(tx).
Between("day", month+"01", month+"32").
Gt("planId", 0).
Slice(&result).
FindAll()
return
}
// FindHourlyStats 按小时统计 // FindHourlyStats 按小时统计
func (this *ServerDailyStatDAO) FindHourlyStats(tx *dbs.Tx, serverId int64, hourFrom string, hourTo string) (result []*ServerDailyStat, err error) { func (this *ServerDailyStatDAO) FindHourlyStats(tx *dbs.Tx, serverId int64, hourFrom string, hourTo string) (result []*ServerDailyStat, err error) {
ones, err := this.Query(tx). ones, err := this.Query(tx).
@@ -390,6 +427,14 @@ func (this *ServerDailyStatDAO) FindTopUserStats(tx *dbs.Tx, hourFrom string, ho
return return
} }
// UpdateStatFee 设置费用
func (this *ServerDailyStatDAO) UpdateStatFee(tx *dbs.Tx, statId int64, fee float32) error {
return this.Query(tx).
Pk(statId).
Set("fee", fee).
UpdateQuickly()
}
// Clean 清理历史数据 // Clean 清理历史数据
func (this *ServerDailyStatDAO) Clean(tx *dbs.Tx, days int) error { func (this *ServerDailyStatDAO) Clean(tx *dbs.Tx, days int) error {
var day = timeutil.Format("Ymd", time.Now().AddDate(0, 0, -days)) var day = timeutil.Format("Ymd", time.Now().AddDate(0, 0, -days))

View File

@@ -2,21 +2,23 @@ package models
// ServerDailyStat 计费流量统计 // ServerDailyStat 计费流量统计
type ServerDailyStat struct { type ServerDailyStat struct {
Id uint64 `field:"id"` // ID Id uint64 `field:"id"` // ID
UserId uint32 `field:"userId"` // 用户ID UserId uint32 `field:"userId"` // 用户ID
ServerId uint32 `field:"serverId"` // 服务ID ServerId uint32 `field:"serverId"` // 服务ID
RegionId uint32 `field:"regionId"` // 区域ID RegionId uint32 `field:"regionId"` // 区域ID
Bytes uint64 `field:"bytes"` // 流量 Bytes uint64 `field:"bytes"` // 流量
CachedBytes uint64 `field:"cachedBytes"` // 缓存的流量 CachedBytes uint64 `field:"cachedBytes"` // 缓存的流量
CountRequests uint64 `field:"countRequests"` // 请求数 CountRequests uint64 `field:"countRequests"` // 请求数
CountCachedRequests uint64 `field:"countCachedRequests"` // 缓存的请求数 CountCachedRequests uint64 `field:"countCachedRequests"` // 缓存的请求数
CountAttackRequests uint64 `field:"countAttackRequests"` // 攻击请求数 CountAttackRequests uint64 `field:"countAttackRequests"` // 攻击请求数
AttackBytes uint64 `field:"attackBytes"` // 攻击流量 AttackBytes uint64 `field:"attackBytes"` // 攻击流量
Day string `field:"day"` // 日期YYYYMMDD Day string `field:"day"` // 日期YYYYMMDD
Hour string `field:"hour"` // YYYYMMDDHH Hour string `field:"hour"` // YYYYMMDDHH
TimeFrom string `field:"timeFrom"` // 开始时间HHMMSS TimeFrom string `field:"timeFrom"` // 开始时间HHMMSS
TimeTo string `field:"timeTo"` // 结束时间 TimeTo string `field:"timeTo"` // 结束时间
IsCharged uint8 `field:"isCharged"` // 是否已计算费用 IsCharged uint8 `field:"isCharged"` // 是否已计算费用
PlanId uint64 `field:"planId"` // 套餐ID
Fee float64 `field:"fee"` // 费用
} }
type ServerDailyStatOperator struct { type ServerDailyStatOperator struct {
@@ -35,6 +37,8 @@ type ServerDailyStatOperator struct {
TimeFrom interface{} // 开始时间HHMMSS TimeFrom interface{} // 开始时间HHMMSS
TimeTo interface{} // 结束时间 TimeTo interface{} // 结束时间
IsCharged interface{} // 是否已计算费用 IsCharged interface{} // 是否已计算费用
PlanId interface{} // 套餐ID
Fee interface{} // 费用
} }
func NewServerDailyStatOperator() *ServerDailyStatOperator { func NewServerDailyStatOperator() *ServerDailyStatOperator {

View File

@@ -1060,6 +1060,9 @@ func (this *ServerDAO) ComposeServerConfig(tx *dbs.Tx, server *Server, cacheMap
if plan != nil { if plan != nil {
config.UserPlan = &serverconfigs.UserPlanConfig{ config.UserPlan = &serverconfigs.UserPlanConfig{
DayTo: userPlan.DayTo, DayTo: userPlan.DayTo,
Plan: &serverconfigs.PlanConfig{
Id: int64(plan.Id),
},
} }
if len(plan.TrafficLimit) > 0 && (config.TrafficLimit == nil || !config.TrafficLimit.IsOn) { if len(plan.TrafficLimit) > 0 && (config.TrafficLimit == nil || !config.TrafficLimit.IsOn) {

View File

@@ -2,12 +2,45 @@ package models
import ( import (
"encoding/json" "encoding/json"
"github.com/TeaOSLab/EdgeAPI/internal/remotelogs"
"github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils" "github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea" "github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs" "github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/types"
timeutil "github.com/iwind/TeaGo/utils/time"
"time"
) )
func init() {
dbs.OnReadyDone(func() {
var generatedMonths = []string{}
go func() {
// 自动生成账单任务
var ticker = time.NewTicker(1 * time.Minute)
for range ticker.C {
// 是否已经生成了,如果已经生成了就跳过
var lastMonth = timeutil.Format("Ym", time.Now().AddDate(0, -1, 0))
if lists.ContainsString(generatedMonths, lastMonth) {
continue
}
err := SharedUserBillDAO.GenerateBills(nil, lastMonth)
if err != nil {
remotelogs.Error("UserBillDAO", "generate bills failed: "+err.Error())
} else {
generatedMonths = append(generatedMonths, lastMonth)
}
}
}()
})
}
type BillType = string type BillType = string
const ( const (
@@ -35,7 +68,7 @@ func init() {
}) })
} }
// 计算账单数量 // CountAllUserBills 计算账单数量
func (this *UserBillDAO) CountAllUserBills(tx *dbs.Tx, isPaid int32, userId int64, month string) (int64, error) { func (this *UserBillDAO) CountAllUserBills(tx *dbs.Tx, isPaid int32, userId int64, month string) (int64, error) {
query := this.Query(tx) query := this.Query(tx)
if isPaid == 0 { if isPaid == 0 {
@@ -52,7 +85,7 @@ func (this *UserBillDAO) CountAllUserBills(tx *dbs.Tx, isPaid int32, userId int6
return query.Count() return query.Count()
} }
// 列出单页账单 // ListUserBills 列出单页账单
func (this *UserBillDAO) ListUserBills(tx *dbs.Tx, isPaid int32, userId int64, month string, offset, size int64) (result []*UserBill, err error) { func (this *UserBillDAO) ListUserBills(tx *dbs.Tx, isPaid int32, userId int64, month string, offset, size int64) (result []*UserBill, err error) {
query := this.Query(tx) query := this.Query(tx)
if isPaid == 0 { if isPaid == 0 {
@@ -75,19 +108,42 @@ func (this *UserBillDAO) ListUserBills(tx *dbs.Tx, isPaid int32, userId int64, m
return return
} }
// 创建账 // FindUnpaidBills 查找未支付订
func (this *UserBillDAO) CreateBill(tx *dbs.Tx, userId int64, billType BillType, description string, amount float32, month string) (int64, error) { func (this *UserBillDAO) FindUnpaidBills(tx *dbs.Tx, size int64) (result []*UserBill, err error) {
op := NewUserBillOperator() if size <= 0 {
op.UserId = userId size = 10000
op.Type = billType }
op.Description = description _, err = this.Query(tx).
op.Amount = amount Attr("isPaid", false).
op.Month = month Lt("month", timeutil.Format("Ym")). //当月的不能支付,因为当月还没过完
op.IsPaid = false Limit(size).
return this.SaveInt64(tx, op) DescPk().
Slice(&result).
FindAll()
return
} }
// 检查是否有当月账单 // CreateBill 创建账单
func (this *UserBillDAO) CreateBill(tx *dbs.Tx, userId int64, billType BillType, description string, amount float32, month string) error {
code, err := this.GenerateBillCode(tx)
if err != nil {
return err
}
return this.Query(tx).
InsertOrUpdateQuickly(maps.Map{
"userId": userId,
"type": billType,
"description": description,
"amount": amount,
"month": month,
"code": code,
"isPaid": false,
}, maps.Map{
"amount": amount,
})
}
// ExistBill 检查是否有当月账单
func (this *UserBillDAO) ExistBill(tx *dbs.Tx, userId int64, billType BillType, month string) (bool, error) { func (this *UserBillDAO) ExistBill(tx *dbs.Tx, userId int64, billType BillType, month string) (bool, error) {
return this.Query(tx). return this.Query(tx).
Attr("userId", userId). Attr("userId", userId).
@@ -96,47 +152,10 @@ func (this *UserBillDAO) ExistBill(tx *dbs.Tx, userId int64, billType BillType,
Exist() Exist()
} }
// 生成账单 // GenerateBills 生成账单
// month 格式YYYYMM // month 格式YYYYMM
func (this *UserBillDAO) GenerateBills(tx *dbs.Tx, month string) error { func (this *UserBillDAO) GenerateBills(tx *dbs.Tx, month string) error {
// 用户 // 区域价格
offset := int64(0)
size := int64(100) // 每次只查询N次防止由于执行时间过长而锁表
for {
userIds, err := SharedUserDAO.ListEnabledUserIds(tx, offset, size)
if err != nil {
return err
}
offset += size
if len(userIds) == 0 {
break
}
for _, userId := range userIds {
// CDN流量账单
err := this.generateTrafficBill(tx, userId, month)
if err != nil {
return err
}
}
}
return nil
}
// 生成CDN流量账单
// month 格式YYYYMM
func (this *UserBillDAO) generateTrafficBill(tx *dbs.Tx, userId int64, month string) error {
// 检查是否已经有账单了
b, err := this.ExistBill(tx, userId, BillTypeTraffic, month)
if err != nil {
return err
}
if b {
return nil
}
// TODO 优化使用缓存
regions, err := SharedNodeRegionDAO.FindAllEnabledRegionPrices(tx) regions, err := SharedNodeRegionDAO.FindAllEnabledRegionPrices(tx)
if err != nil { if err != nil {
return err return err
@@ -153,18 +172,104 @@ func (this *UserBillDAO) generateTrafficBill(tx *dbs.Tx, userId int64, month str
return nil return nil
} }
cost := float32(0) // 计算套餐费用
plans, err := SharedPlanDAO.FindAllEnabledPlans(tx)
if err != nil {
return err
}
var planMap = map[int64]*Plan{}
for _, plan := range plans {
planMap[int64(plan.Id)] = plan
}
stats, err := SharedServerDailyStatDAO.FindMonthlyStatsWithPlan(tx, month)
if err != nil {
return err
}
for _, stat := range stats {
plan, ok := planMap[int64(stat.PlanId)]
if !ok {
continue
}
if plan.PriceType != serverconfigs.PlanPriceTypeTraffic {
continue
}
if len(plan.TrafficPrice) == 0 {
continue
}
var priceConfig = &serverconfigs.PlanTrafficPrice{}
err = json.Unmarshal([]byte(plan.TrafficPrice), priceConfig)
if err != nil {
return err
}
if priceConfig.Base > 0 {
var fee = priceConfig.Base * (float32(stat.Bytes) / 1024 / 1024 / 1024)
err = SharedServerDailyStatDAO.UpdateStatFee(tx, int64(stat.Id), fee)
if err != nil {
return err
}
}
}
// 用户
offset := int64(0)
size := int64(100) // 每次只查询N次防止由于执行时间过长而锁表
for {
userIds, err := SharedUserDAO.ListEnabledUserIds(tx, offset, size)
if err != nil {
return err
}
offset += size
if len(userIds) == 0 {
break
}
for _, userId := range userIds {
// CDN流量账单
err := this.generateTrafficBill(tx, userId, month, regions, priceItems)
if err != nil {
return err
}
}
}
return nil
}
// UpdateUserBillIsPaid 设置账单已支付
func (this *UserBillDAO) UpdateUserBillIsPaid(tx *dbs.Tx, billId int64, isPaid bool) error {
return this.Query(tx).
Pk(billId).
Set("isPaid", isPaid).
UpdateQuickly()
}
// 生成CDN流量账单
// month 格式YYYYMM
func (this *UserBillDAO) generateTrafficBill(tx *dbs.Tx, userId int64, month string, regions []*NodeRegion, priceItems []*NodePriceItem) error {
// 检查是否已经有账单了
if month < timeutil.Format("Ym") {
b, err := this.ExistBill(tx, userId, BillTypeTraffic, month)
if err != nil {
return err
}
if b {
return nil
}
}
var cost = float32(0)
for _, region := range regions { for _, region := range regions {
if len(region.Prices) == 0 || region.Prices == "null" { if len(region.Prices) == 0 || region.Prices == "null" {
continue continue
} }
priceMap := map[string]float32{} priceMap := map[string]float32{}
err = json.Unmarshal([]byte(region.Prices), &priceMap) err := json.Unmarshal([]byte(region.Prices), &priceMap)
if err != nil { if err != nil {
return err return err
} }
trafficBytes, err := SharedServerDailyStatDAO.SumUserMonthly(tx, userId, int64(region.Id), month) trafficBytes, err := SharedServerDailyStatDAO.SumUserMonthlyWithoutPlan(tx, userId, int64(region.Id), month)
if err != nil { if err != nil {
return err return err
} }
@@ -187,16 +292,22 @@ func (this *UserBillDAO) generateTrafficBill(tx *dbs.Tx, userId int64, month str
cost += (float32(trafficBytes*8) / 1_000_000_000) * price cost += (float32(trafficBytes*8) / 1_000_000_000) * price
} }
// 套餐费用
planFee, err := SharedServerDailyStatDAO.SumUserMonthlyFee(tx, userId, month)
if err != nil {
return err
}
cost += float32(planFee)
if cost == 0 { if cost == 0 {
return nil return nil
} }
// 创建账单 // 创建账单
_, err = this.CreateBill(tx, userId, BillTypeTraffic, "按流量计费", cost, month) return this.CreateBill(tx, userId, BillTypeTraffic, "按流量计费", cost, month)
return err
} }
// 获取账单类型名称 // BillTypeName 获取账单类型名称
func (this *UserBillDAO) BillTypeName(billType BillType) string { func (this *UserBillDAO) BillTypeName(billType BillType) string {
switch billType { switch billType {
case BillTypeTraffic: case BillTypeTraffic:
@@ -204,3 +315,18 @@ func (this *UserBillDAO) BillTypeName(billType BillType) string {
} }
return "" return ""
} }
// GenerateBillCode 生成账单编号
func (this *UserBillDAO) GenerateBillCode(tx *dbs.Tx) (string, error) {
var code = timeutil.Format("YmdHis") + types.String(rands.Int(100000, 999999))
exists, err := this.Query(tx).
Attr("code", code).
Exist()
if err != nil {
return "", err
}
if !exists {
return code, nil
}
return this.GenerateBillCode(tx)
}

View File

@@ -1,6 +1,6 @@
package models package models
// 用户账单 // UserBill 用户账单
type UserBill struct { type UserBill struct {
Id uint64 `field:"id"` // ID Id uint64 `field:"id"` // ID
UserId uint32 `field:"userId"` // 用户ID UserId uint32 `field:"userId"` // 用户ID
@@ -10,6 +10,7 @@ type UserBill struct {
Month string `field:"month"` // 帐期YYYYMM Month string `field:"month"` // 帐期YYYYMM
IsPaid uint8 `field:"isPaid"` // 是否已支付 IsPaid uint8 `field:"isPaid"` // 是否已支付
PaidAt uint64 `field:"paidAt"` // 支付时间 PaidAt uint64 `field:"paidAt"` // 支付时间
Code string `field:"code"` // 账单编号
CreatedAt uint64 `field:"createdAt"` // 创建时间 CreatedAt uint64 `field:"createdAt"` // 创建时间
} }
@@ -22,6 +23,7 @@ type UserBillOperator struct {
Month interface{} // 帐期YYYYMM Month interface{} // 帐期YYYYMM
IsPaid interface{} // 是否已支付 IsPaid interface{} // 是否已支付
PaidAt interface{} // 支付时间 PaidAt interface{} // 支付时间
Code interface{} // 账单编号
CreatedAt interface{} // 创建时间 CreatedAt interface{} // 创建时间
} }

View File

@@ -9,12 +9,12 @@ import (
"regexp" "regexp"
) )
// 账单相关服务 // UserBillService 账单相关服务
type UserBillService struct { type UserBillService struct {
BaseService BaseService
} }
// 手工生成订单 // GenerateAllUserBills 手工生成订单
func (this *UserBillService) GenerateAllUserBills(ctx context.Context, req *pb.GenerateAllUserBillsRequest) (*pb.RPCSuccess, error) { func (this *UserBillService) GenerateAllUserBills(ctx context.Context, req *pb.GenerateAllUserBillsRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx, 0) _, err := this.ValidateAdmin(ctx, 0)
if err != nil { if err != nil {
@@ -39,7 +39,7 @@ func (this *UserBillService) GenerateAllUserBills(ctx context.Context, req *pb.G
return this.Success() return this.Success()
} }
// 计算所有账单数量 // CountAllUserBills 计算所有账单数量
func (this *UserBillService) CountAllUserBills(ctx context.Context, req *pb.CountAllUserBillsRequest) (*pb.RPCCountResponse, error) { func (this *UserBillService) CountAllUserBills(ctx context.Context, req *pb.CountAllUserBillsRequest) (*pb.RPCCountResponse, error) {
_, _, err := this.ValidateAdminAndUser(ctx, 0, req.UserId) _, _, err := this.ValidateAdminAndUser(ctx, 0, req.UserId)
if err != nil { if err != nil {
@@ -55,7 +55,7 @@ func (this *UserBillService) CountAllUserBills(ctx context.Context, req *pb.Coun
return this.SuccessCount(count) return this.SuccessCount(count)
} }
// 列出单页账单 // ListUserBills 列出单页账单
func (this *UserBillService) ListUserBills(ctx context.Context, req *pb.ListUserBillsRequest) (*pb.ListUserBillsResponse, error) { func (this *UserBillService) ListUserBills(ctx context.Context, req *pb.ListUserBillsRequest) (*pb.ListUserBillsResponse, error) {
_, _, err := this.ValidateAdminAndUser(ctx, 0, req.UserId) _, _, err := this.ValidateAdminAndUser(ctx, 0, req.UserId)
if err != nil { if err != nil {
@@ -70,16 +70,20 @@ func (this *UserBillService) ListUserBills(ctx context.Context, req *pb.ListUser
} }
result := []*pb.UserBill{} result := []*pb.UserBill{}
for _, bill := range bills { for _, bill := range bills {
userFullname, err := models.SharedUserDAO.FindUserFullname(tx, int64(bill.UserId)) user, err := models.SharedUserDAO.FindEnabledBasicUser(tx, int64(bill.UserId))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user == nil {
user = &models.User{Id: bill.UserId}
}
result = append(result, &pb.UserBill{ result = append(result, &pb.UserBill{
Id: int64(bill.Id), Id: int64(bill.Id),
User: &pb.User{ User: &pb.User{
Id: int64(bill.UserId), Id: int64(bill.UserId),
Fullname: userFullname, Fullname: user.Fullname,
Username: user.Username,
}, },
Type: bill.Type, Type: bill.Type,
TypeName: models.SharedUserBillDAO.BillTypeName(bill.Type), TypeName: models.SharedUserBillDAO.BillTypeName(bill.Type),
@@ -88,6 +92,7 @@ func (this *UserBillService) ListUserBills(ctx context.Context, req *pb.ListUser
Month: bill.Month, Month: bill.Month,
IsPaid: bill.IsPaid == 1, IsPaid: bill.IsPaid == 1,
PaidAt: int64(bill.PaidAt), PaidAt: int64(bill.PaidAt),
Code: bill.Code,
}) })
} }
return &pb.ListUserBillsResponse{UserBills: result}, nil return &pb.ListUserBillsResponse{UserBills: result}, nil

File diff suppressed because one or more lines are too long