实现带宽计费套餐

This commit is contained in:
GoEdgeLab
2022-01-23 19:16:52 +08:00
parent 6054e65c88
commit 9f129a2795
26 changed files with 752 additions and 143 deletions

View File

@@ -1,6 +1,6 @@
package models
// 区域计费设置
// NodePriceItem 区域计费设置
type NodePriceItem struct {
Id uint32 `field:"id"` // ID
IsOn uint8 `field:"isOn"` // 是否启用

View File

@@ -1,6 +1,6 @@
package models
// 节点区域
// NodeRegion 节点区域
type NodeRegion struct {
Id uint32 `field:"id"` // ID
AdminId uint32 `field:"adminId"` // 管理员ID

View File

@@ -1 +1,18 @@
package models
import "encoding/json"
func (this *NodeRegion) DecodePriceMap() map[int64]float64 {
var m = map[int64]float64{}
if len(this.Prices) == 0 {
return m
}
err := json.Unmarshal([]byte(this.Prices), &m)
if err != nil {
// 忽略错误
return m
}
return m
}

View File

@@ -11,7 +11,7 @@ func TestNodeTaskDAO_CreateNodeTask(t *testing.T) {
dbs.NotifyReady()
var tx *dbs.Tx
err := SharedNodeTaskDAO.CreateNodeTask(tx, nodeconfigs.NodeRoleNode, 1, 2, NodeTaskTypeConfigChanged, 0)
err := SharedNodeTaskDAO.CreateNodeTask(tx, nodeconfigs.NodeRoleNode, 1, 2, 0, NodeTaskTypeConfigChanged, 0)
if err != nil {
t.Fatal(err)
}
@@ -22,7 +22,7 @@ func TestNodeTaskDAO_CreateClusterTask(t *testing.T) {
dbs.NotifyReady()
var tx *dbs.Tx
err := SharedNodeTaskDAO.CreateClusterTask(tx, nodeconfigs.NodeRoleNode, 1, NodeTaskTypeConfigChanged)
err := SharedNodeTaskDAO.CreateClusterTask(tx, nodeconfigs.NodeRoleNode, 1, 0, NodeTaskTypeConfigChanged)
if err != nil {
t.Fatal(err)
}
@@ -33,7 +33,7 @@ func TestNodeTaskDAO_ExtractClusterTask(t *testing.T) {
dbs.NotifyReady()
var tx *dbs.Tx
err := SharedNodeTaskDAO.ExtractNodeClusterTask(tx, 1, NodeTaskTypeConfigChanged)
err := SharedNodeTaskDAO.ExtractNodeClusterTask(tx, 1, 0, NodeTaskTypeConfigChanged)
if err != nil {
t.Fatal(err)
}

View File

@@ -80,7 +80,17 @@ func (this *PlanDAO) FindPlanName(tx *dbs.Tx, id int64) (string, error) {
}
// CreatePlan 创建套餐
func (this *PlanDAO) CreatePlan(tx *dbs.Tx, name string, clusterId int64, trafficLimitJSON []byte, featuresJSON []byte, priceType serverconfigs.PlanPriceType, trafficPriceJSON []byte, monthlyPrice float32, seasonallyPrice float32, yearlyPrice float32) (int64, error) {
func (this *PlanDAO) CreatePlan(tx *dbs.Tx,
name string,
clusterId int64,
trafficLimitJSON []byte,
featuresJSON []byte,
priceType serverconfigs.PlanPriceType,
trafficPriceJSON []byte,
bandwidthPriceJSON []byte,
monthlyPrice float32,
seasonallyPrice float32,
yearlyPrice float32) (int64, error) {
var op = NewPlanOperator()
op.Name = name
op.ClusterId = clusterId
@@ -94,6 +104,9 @@ func (this *PlanDAO) CreatePlan(tx *dbs.Tx, name string, clusterId int64, traffi
if len(trafficPriceJSON) > 0 {
op.TrafficPrice = trafficPriceJSON
}
if len(bandwidthPriceJSON) > 0 {
op.BandwidthPrice = bandwidthPriceJSON
}
if monthlyPrice >= 0 {
op.MonthlyPrice = monthlyPrice
}
@@ -109,7 +122,19 @@ func (this *PlanDAO) CreatePlan(tx *dbs.Tx, name string, clusterId int64, traffi
}
// UpdatePlan 修改套餐
func (this *PlanDAO) UpdatePlan(tx *dbs.Tx, planId int64, name string, isOn bool, clusterId int64, trafficLimitJSON []byte, featuresJSON []byte, priceType serverconfigs.PlanPriceType, trafficPriceJSON []byte, monthlyPrice float32, seasonallyPrice float32, yearlyPrice float32) error {
func (this *PlanDAO) UpdatePlan(tx *dbs.Tx,
planId int64,
name string,
isOn bool,
clusterId int64,
trafficLimitJSON []byte,
featuresJSON []byte,
priceType serverconfigs.PlanPriceType,
trafficPriceJSON []byte,
bandwidthPriceJSON []byte,
monthlyPrice float32,
seasonallyPrice float32,
yearlyPrice float32) error {
if planId <= 0 {
return errors.New("invalid planId")
}
@@ -138,6 +163,9 @@ func (this *PlanDAO) UpdatePlan(tx *dbs.Tx, planId int64, name string, isOn bool
if len(trafficPriceJSON) > 0 {
op.TrafficPrice = trafficPriceJSON
}
if len(bandwidthPriceJSON) > 0 {
op.BandwidthPrice = bandwidthPriceJSON
}
if monthlyPrice >= 0 {
op.MonthlyPrice = monthlyPrice
} else {

View File

@@ -9,6 +9,7 @@ type Plan struct {
TrafficLimit string `field:"trafficLimit"` // 流量限制
Features string `field:"features"` // 允许的功能
TrafficPrice string `field:"trafficPrice"` // 流量价格设定
BandwidthPrice string `field:"bandwidthPrice"` // 带宽价格
MonthlyPrice float64 `field:"monthlyPrice"` // 月付
SeasonallyPrice float64 `field:"seasonallyPrice"` // 季付
YearlyPrice float64 `field:"yearlyPrice"` // 年付
@@ -25,6 +26,7 @@ type PlanOperator struct {
TrafficLimit interface{} // 流量限制
Features interface{} // 允许的功能
TrafficPrice interface{} // 流量价格设定
BandwidthPrice interface{} // 带宽价格
MonthlyPrice interface{} // 月付
SeasonallyPrice interface{} // 季付
YearlyPrice interface{} // 年付

View File

@@ -1 +1,38 @@
package models
import (
"encoding/json"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
)
// DecodeTrafficPrice 流量价格配置
func (this *Plan) DecodeTrafficPrice() *serverconfigs.PlanTrafficPriceConfig {
var config = &serverconfigs.PlanTrafficPriceConfig{}
if len(this.TrafficPrice) == 0 {
return config
}
err := json.Unmarshal([]byte(this.TrafficPrice), config)
if err != nil {
// 忽略错误
}
return config
}
// DecodeBandwidthPrice 带宽价格配置
func (this *Plan) DecodeBandwidthPrice() *serverconfigs.PlanBandwidthPriceConfig {
var config = &serverconfigs.PlanBandwidthPriceConfig{}
if len(this.BandwidthPrice) == 0 {
return config
}
err := json.Unmarshal([]byte(this.BandwidthPrice), config)
if err != nil {
// 忽略错误
}
return config
}

View File

@@ -0,0 +1,96 @@
package models
import (
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/maps"
"math"
"time"
)
type ServerBillDAO dbs.DAO
func NewServerBillDAO() *ServerBillDAO {
return dbs.NewDAO(&ServerBillDAO{
DAOObject: dbs.DAOObject{
DB: Tea.Env,
Table: "edgeServerBills",
Model: new(ServerBill),
PkName: "id",
},
}).(*ServerBillDAO)
}
var SharedServerBillDAO *ServerBillDAO
func init() {
dbs.OnReady(func() {
SharedServerBillDAO = NewServerBillDAO()
})
}
// CreateOrUpdateServerBill 创建账单
func (this *ServerBillDAO) CreateOrUpdateServerBill(tx *dbs.Tx, userId int64, serverId int64, month string, userPlanId int64, planId int64, totalTrafficBytes int64, bandwidthPercentileBytes int64, bandwidthPercentile int, fee float64) error {
fee = math.Floor(fee*100) / 100
return this.Query(tx).
InsertOrUpdateQuickly(maps.Map{
"userId": userId,
"serverId": serverId,
"month": month,
"amount": fee,
"userPlanId": userPlanId,
"planId": planId,
"totalTrafficBytes": totalTrafficBytes,
"bandwidthPercentileBytes": bandwidthPercentileBytes,
"bandwidthPercentile": bandwidthPercentile,
"createdAt": time.Now().Unix(),
}, maps.Map{
"userId": userId,
"amount": fee,
"userPlanId": userPlanId,
"planId": planId,
"totalTrafficBytes": totalTrafficBytes,
"bandwidthPercentileBytes": bandwidthPercentileBytes,
"bandwidthPercentile": bandwidthPercentile,
"createdAt": time.Now().Unix(),
})
}
// SumUserMonthlyAmount 计算总费用
func (this *ServerBillDAO) SumUserMonthlyAmount(tx *dbs.Tx, userId int64, month string) (float64, error) {
return this.Query(tx).
Attr("userId", userId).
Attr("month", month).
Sum("amount", 0)
}
// CountServerBills 计算总账单数量
func (this *ServerBillDAO) CountServerBills(tx *dbs.Tx, userId int64, month string) (int64, error) {
var query = this.Query(tx)
if userId > 0 {
query.Attr("userId", userId)
}
if len(month) > 0 {
query.Attr("month", month)
}
return query.Count()
}
// ListServerBills 列出单页账单
func (this *ServerBillDAO) ListServerBills(tx *dbs.Tx, userId int64, month string, offset int64, size int64) (result []*ServerBill, err error) {
var query = this.Query(tx)
if userId > 0 {
query.Attr("userId", userId)
}
if len(month) > 0 {
query.Attr("month", month)
}
_, err = query.
Desc("serverId").
Offset(offset).
Limit(size).
Slice(&result).
FindAll()
return
}

View File

@@ -0,0 +1,20 @@
package models
import (
_ "github.com/go-sql-driver/mysql"
_ "github.com/iwind/TeaGo/bootstrap"
"github.com/iwind/TeaGo/dbs"
timeutil "github.com/iwind/TeaGo/utils/time"
"testing"
)
func TestServerBillDAO_CreateOrUpdateServerBill(t *testing.T) {
var dao = NewServerBillDAO()
var tx *dbs.Tx
var month = timeutil.Format("Y02")
err := dao.CreateOrUpdateServerBill(tx, 1, 2, month, 4, 5, 6, 7, 95, 100)
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}

View File

@@ -0,0 +1,34 @@
package models
// ServerBill 服务账单
type ServerBill struct {
Id uint64 `field:"id"` // ID
UserId uint32 `field:"userId"` // 用户ID
ServerId uint32 `field:"serverId"` // 服务ID
Amount float64 `field:"amount"` // 金额
Month string `field:"month"` // 月份
CreatedAt uint64 `field:"createdAt"` // 创建时间
UserPlanId uint32 `field:"userPlanId"` // 用户套餐ID
PlanId uint32 `field:"planId"` // 套餐ID
TotalTrafficBytes uint64 `field:"totalTrafficBytes"` // 总流量
BandwidthPercentileBytes uint64 `field:"bandwidthPercentileBytes"` // 带宽百分位字节
BandwidthPercentile uint8 `field:"bandwidthPercentile"` // 带宽百分位
}
type ServerBillOperator struct {
Id interface{} // ID
UserId interface{} // 用户ID
ServerId interface{} // 服务ID
Amount interface{} // 金额
Month interface{} // 月份
CreatedAt interface{} // 创建时间
UserPlanId interface{} // 用户套餐ID
PlanId interface{} // 套餐ID
TotalTrafficBytes interface{} // 总流量
BandwidthPercentileBytes interface{} // 带宽百分位字节
BandwidthPercentile interface{} // 带宽百分位
}
func NewServerBillOperator() *ServerBillOperator {
return &ServerBillOperator{}
}

View File

@@ -0,0 +1 @@
package models

View File

@@ -12,7 +12,9 @@ import (
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/rands"
timeutil "github.com/iwind/TeaGo/utils/time"
"math"
"regexp"
"strings"
"time"
)
@@ -24,7 +26,7 @@ func init() {
var ticker = time.NewTicker(time.Duration(rands.Int(24, 48)) * time.Hour)
goman.New(func() {
for range ticker.C {
err := SharedServerDailyStatDAO.Clean(nil, 30) // 只保留N天
err := SharedServerDailyStatDAO.Clean(nil, 60) // 只保留 N 天,时间需要长一些,因为需要用来生成账单
if err != nil {
logs.Println("ServerDailyStatDAO", "clean expired data failed: "+err.Error())
}
@@ -130,15 +132,15 @@ func (this *ServerDailyStatDAO) SaveStats(tx *dbs.Tx, stats []*pb.ServerDailySta
return nil
}
// SumUserMonthly 根据用户计算某月合计
// SumServerMonthlyWithRegion 根据服务计算某月合计
// month 格式为YYYYMM
func (this *ServerDailyStatDAO) SumUserMonthly(tx *dbs.Tx, userId int64, regionId int64, month string) (int64, error) {
func (this *ServerDailyStatDAO) SumServerMonthlyWithRegion(tx *dbs.Tx, serverId int64, regionId int64, month string) (int64, error) {
query := this.Query(tx)
if regionId > 0 {
query.Attr("regionId", regionId)
}
return query.Between("day", month+"01", month+"32").
Attr("userId", userId).
Attr("serverId", serverId).
SumInt64("bytes", 0)
}
@@ -156,16 +158,6 @@ func (this *ServerDailyStatDAO) SumUserMonthlyWithoutPlan(tx *dbs.Tx, userId int
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 获取某月带宽峰值
// month 格式为YYYYMM
func (this *ServerDailyStatDAO) SumUserMonthlyPeek(tx *dbs.Tx, userId int64, regionId int64, month string) (int64, error) {
@@ -195,6 +187,15 @@ func (this *ServerDailyStatDAO) SumUserDaily(tx *dbs.Tx, userId int64, regionId
SumInt64("bytes", 0)
}
// SumUserMonthly 获取某月流量总和
// month 格式为YYYYMM
func (this *ServerDailyStatDAO) SumUserMonthly(tx *dbs.Tx, userId int64, month string) (int64, error) {
return this.Query(tx).
Between("day", month+"01", month+"31").
Attr("userId", userId).
SumInt64("bytes", 0)
}
// SumUserDailyPeek 获取某天带宽峰值
// day 格式为YYYYMMDD
func (this *ServerDailyStatDAO) SumUserDailyPeek(tx *dbs.Tx, userId int64, regionId int64, day string) (int64, error) {
@@ -339,6 +340,59 @@ func (this *ServerDailyStatDAO) SumMonthlyStat(tx *dbs.Tx, serverId int64, month
return
}
// SumMonthlyBytes 获取某月内的流量
// month 格式为YYYYMM
func (this *ServerDailyStatDAO) SumMonthlyBytes(tx *dbs.Tx, serverId int64, month string) (result int64, err error) {
if !regexp.MustCompile(`^\d{6}$`).MatchString(month) {
return
}
return this.Query(tx).
Result("SUM(bytes) AS bytes").
Attr("serverId", serverId).
Between("day", month+"01", month+"31").
FindInt64Col(0)
}
// FindMonthlyPercentile 获取某月内百分位
func (this *ServerDailyStatDAO) FindMonthlyPercentile(tx *dbs.Tx, serverId int64, month string, percentile int) (result int64, err error) {
if percentile <= 0 {
percentile = 95
}
if percentile > 100 {
percentile = 100
}
total, err := this.Query(tx).
Attr("serverId", serverId).
Between("day", month+"01", month+"31").
Count()
if err != nil {
return 0, err
}
if total == 0 {
return 0, nil
}
var offset int64
if total > 1 {
offset = int64(math.Ceil(float64(total) * float64(100-percentile) / 100))
}
result, err = this.Query(tx).
Result("bytes").
Attr("serverId", serverId).
Between("day", month+"01", month+"31").
Desc("bytes").
Offset(offset).
Limit(1).
FindInt64Col(0)
// 因为是5分钟统计所以需要除以300
result = result / 300
return
}
// FindDailyStats 按天统计
func (this *ServerDailyStatDAO) FindDailyStats(tx *dbs.Tx, serverId int64, dayFrom string, dayTo string) (result []*ServerDailyStat, err error) {
ones, err := this.Query(tx).
@@ -428,6 +482,25 @@ func (this *ServerDailyStatDAO) FindTopUserStats(tx *dbs.Tx, hourFrom string, ho
return
}
// FindDistinctServerIds 查找所有有流量的服务ID列表
// dayFrom YYYYMMDD
// dayTo YYYYMMDD
func (this *ServerDailyStatDAO) FindDistinctServerIds(tx *dbs.Tx, dayFrom string, dayTo string) (serverIds []int64, err error) {
dayFrom = strings.ReplaceAll(dayFrom, "-", "")
dayTo = strings.ReplaceAll(dayTo, "-", "")
ones, _, err := this.Query(tx).
Result("DISTINCT(serverId) AS serverId").
Between("day", dayFrom, dayTo).
FindOnes()
if err != nil {
return nil, err
}
for _, one := range ones {
serverIds = append(serverIds, one.GetInt64("serverId"))
}
return serverIds, nil
}
// UpdateStatFee 设置费用
func (this *ServerDailyStatDAO) UpdateStatFee(tx *dbs.Tx, statId int64, fee float32) error {
return this.Query(tx).

View File

@@ -46,7 +46,7 @@ func TestServerDailyStatDAO_SaveStats2(t *testing.T) {
func TestServerDailyStatDAO_SumUserMonthly(t *testing.T) {
dbs.NotifyReady()
var tx *dbs.Tx
bytes, err := NewServerDailyStatDAO().SumUserMonthly(tx, 1, 1, timeutil.Format("Ym"))
bytes, err := NewServerDailyStatDAO().SumUserMonthly(tx, 1, timeutil.Format("Ym"))
if err != nil {
t.Fatal(err)
}
@@ -68,9 +68,28 @@ func TestServerDailyStatDAO_SumMinutelyRequests(t *testing.T) {
dbs.NotifyReady()
var tx *dbs.Tx
stat, err := NewServerDailyStatDAO().SumMinutelyStat(tx, 23, timeutil.Format("Ymd") + "1435")
stat, err := NewServerDailyStatDAO().SumMinutelyStat(tx, 23, timeutil.Format("Ymd")+"1435")
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(stat, t)
}
func TestServerDailyStatDAO_FindDistinctPlanServerIdsBetweenDay(t *testing.T) {
var tx *dbs.Tx
serverIds, err := NewServerDailyStatDAO().FindDistinctServerIds(tx, timeutil.Format("Ym01"), timeutil.Format("Ymd"))
if err != nil {
t.Fatal(err)
}
t.Log(serverIds)
}
func TestServerDailyStatDAO_FindMonthlyPercentile(t *testing.T) {
var tx *dbs.Tx
var dao = NewServerDailyStatDAO()
result, err := dao.FindMonthlyPercentile(tx, 23, timeutil.Format("Ym"), 95)
if err != nil {
t.Fatal(err)
}
t.Log("result:", result)
}

View File

@@ -221,6 +221,7 @@ func (this *ServerDAO) CreateServer(tx *dbs.Tx,
op.DnsName = dnsName
op.UserPlanId = userPlanId
op.LastUserPlanId = userPlanId
op.Version = 1
op.IsOn = 1
@@ -2242,6 +2243,7 @@ func (this *ServerDAO) UpdateServerUserPlanId(tx *dbs.Tx, serverId int64, userPl
err = this.Query(tx).
Pk(serverId).
Set("userPlanId", userPlanId).
Set("lastUserPlanId", userPlanId).
Set("clusterId", plan.ClusterId).
UpdateQuickly()
if err != nil {
@@ -2250,6 +2252,19 @@ func (this *ServerDAO) UpdateServerUserPlanId(tx *dbs.Tx, serverId int64, userPl
return this.NotifyUpdate(tx, serverId)
}
// FindServerLastUserPlanIdAndUserId 查找最后使用的套餐
func (this *ServerDAO) FindServerLastUserPlanIdAndUserId(tx *dbs.Tx, serverId int64) (userPlanId int64, userId int64, err error) {
one, err := this.Query(tx).
Pk(serverId).
Result("lastUserPlanId", "userId").
Find()
if err != nil || one == nil {
return 0, 0, err
}
return int64(one.(*Server).LastUserPlanId), int64(one.(*Server).UserId), nil
}
// NotifyUpdate 同步集群
func (this *ServerDAO) NotifyUpdate(tx *dbs.Tx, serverId int64) error {
// 创建任务

View File

@@ -43,6 +43,7 @@ type Server struct {
TrafficLimitStatus string `field:"trafficLimitStatus"` // 流量限制状态
TotalTraffic float64 `field:"totalTraffic"` // 总流量
UserPlanId uint32 `field:"userPlanId"` // 所属套餐ID
LastUserPlanId uint32 `field:"lastUserPlanId"` // 上一次使用的套餐
}
type ServerOperator struct {
@@ -87,6 +88,7 @@ type ServerOperator struct {
TrafficLimitStatus interface{} // 流量限制状态
TotalTraffic interface{} // 总流量
UserPlanId interface{} // 所属套餐ID
LastUserPlanId interface{} // 上一次使用的套餐
}
func NewServerOperator() *ServerOperator {

View File

@@ -1,10 +1,9 @@
package models
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAPI/internal/goman"
"github.com/TeaOSLab/EdgeAPI/internal/remotelogs"
"github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeAPI/internal/utils"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
@@ -23,7 +22,7 @@ func init() {
goman.New(func() {
// 自动生成账单任务
var ticker = time.NewTicker(1 * time.Minute)
var ticker = time.NewTicker(1 * time.Hour)
for range ticker.C {
// 是否已经生成了,如果已经生成了就跳过
var lastMonth = timeutil.Format("Ym", time.Now().AddDate(0, -1, 0))
@@ -136,7 +135,7 @@ func (this *UserBillDAO) FindUnpaidBills(tx *dbs.Tx, size int64) (result []*User
}
// CreateBill 创建账单
func (this *UserBillDAO) CreateBill(tx *dbs.Tx, userId int64, billType BillType, description string, amount float32, month string) error {
func (this *UserBillDAO) CreateBill(tx *dbs.Tx, userId int64, billType BillType, description string, amount float64, month string, canPay bool) error {
code, err := this.GenerateBillCode(tx)
if err != nil {
return err
@@ -149,9 +148,11 @@ func (this *UserBillDAO) CreateBill(tx *dbs.Tx, userId int64, billType BillType,
"amount": amount,
"month": month,
"code": code,
"isPaid": false,
"isPaid": amount == 0,
"canPay": canPay,
}, maps.Map{
"amount": amount,
"canPay": canPay,
})
}
@@ -172,19 +173,16 @@ func (this *UserBillDAO) GenerateBills(tx *dbs.Tx, month string) error {
if err != nil {
return err
}
if len(regions) == 0 {
return nil
}
priceItems, err := SharedNodePriceItemDAO.FindAllEnabledRegionPrices(tx, NodePriceTypeTraffic)
var priceItems []*NodePriceItem
if len(regions) > 0 {
priceItems, err = SharedNodePriceItemDAO.FindAllEnabledRegionPrices(tx, NodePriceTypeTraffic)
if err != nil {
return err
}
if len(priceItems) == 0 {
return nil
}
// 计算套餐费用
// 计算服务套餐费用
plans, err := SharedPlanDAO.FindAllEnabledPlans(tx)
if err != nil {
return err
@@ -194,54 +192,169 @@ func (this *UserBillDAO) GenerateBills(tx *dbs.Tx, month string) error {
planMap[int64(plan.Id)] = plan
}
stats, err := SharedServerDailyStatDAO.FindMonthlyStatsWithPlan(tx, month)
var dayFrom = month + "01"
var dayTo = month + "32"
serverIds, err := SharedServerDailyStatDAO.FindDistinctServerIds(tx, dayFrom, dayTo)
if err != nil {
return err
}
for _, stat := range stats {
plan, ok := planMap[int64(stat.PlanId)]
var cacheMap = utils.NewCacheMap()
var userIds = []int64{}
for _, serverId := range serverIds {
// 套餐类型
userPlanId, userId, err := SharedServerDAO.FindServerLastUserPlanIdAndUserId(tx, serverId)
if err != nil {
return err
}
if userId == 0 {
continue
}
userIds = append(userIds, userId)
if userPlanId == 0 {
var fee float64
for _, region := range regions {
var regionId = int64(region.Id)
var pricesMap = region.DecodePriceMap()
if len(pricesMap) == 0 {
continue
}
trafficBytes, err := SharedServerDailyStatDAO.SumServerMonthlyWithRegion(tx, serverId, regionId, month)
if err != nil {
return err
}
if trafficBytes == 0 {
continue
}
var itemId = SharedNodePriceItemDAO.SearchItemsWithBytes(priceItems, trafficBytes)
if itemId == 0 {
continue
}
price, ok := pricesMap[itemId]
if !ok {
continue
}
if plan.PriceType != serverconfigs.PlanPriceTypeTraffic {
if price <= 0 {
continue
}
if len(plan.TrafficPrice) == 0 {
continue
var regionFee = float64(trafficBytes) / 1000 / 1000 / 1000 * 8 * price
fee += regionFee
}
var priceConfig = &serverconfigs.PlanTrafficPrice{}
err = json.Unmarshal([]byte(plan.TrafficPrice), priceConfig)
// 总流量
totalTrafficBytes, err := SharedServerDailyStatDAO.SumMonthlyBytes(tx, serverId, month)
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)
// 百分位
var percentile = 95
percentileBytes, err := SharedServerDailyStatDAO.FindMonthlyPercentile(tx, serverId, month, percentile)
if err != nil {
return err
}
err = SharedServerBillDAO.CreateOrUpdateServerBill(tx, userId, serverId, month, userPlanId, 0, totalTrafficBytes, percentileBytes, percentile, fee)
if err != nil {
return err
}
} else {
userPlan, err := SharedUserPlanDAO.FindUserPlanWithoutState(tx, userPlanId, cacheMap)
if err != nil {
return err
}
if userPlan == nil {
continue
}
plan, ok := planMap[int64(userPlan.PlanId)]
if !ok {
continue
}
// 总流量
totalTrafficBytes, err := SharedServerDailyStatDAO.SumMonthlyBytes(tx, serverId, month)
if err != nil {
return err
}
switch plan.PriceType {
case serverconfigs.PlanPriceTypePeriod:
// 已经在购买套餐的时候付过费,这里不再重复计费
var fee float64 = 0
// 百分位
var percentile = 95
percentileBytes, err := SharedServerDailyStatDAO.FindMonthlyPercentile(tx, serverId, month, percentile)
if err != nil {
return err
}
err = SharedServerBillDAO.CreateOrUpdateServerBill(tx, int64(userPlan.UserId), serverId, month, userPlanId, int64(userPlan.PlanId), totalTrafficBytes, percentileBytes, percentile, fee)
if err != nil {
return err
}
case serverconfigs.PlanPriceTypeTraffic:
var config = plan.DecodeTrafficPrice()
var fee float64 = 0
if config != nil && config.Base > 0 {
fee = float64(totalTrafficBytes) / 1024 / 1024 / 1024 * float64(config.Base)
}
// 百分位
var percentile = 95
percentileBytes, err := SharedServerDailyStatDAO.FindMonthlyPercentile(tx, serverId, month, percentile)
if err != nil {
return err
}
err = SharedServerBillDAO.CreateOrUpdateServerBill(tx, int64(userPlan.UserId), serverId, month, userPlanId, int64(userPlan.PlanId), totalTrafficBytes, percentileBytes, percentile, fee)
if err != nil {
return err
}
case serverconfigs.PlanPriceTypeBandwidth:
// 百分位
var percentile = 95
var config = plan.DecodeBandwidthPrice()
if config != nil {
percentile = config.Percentile
if percentile <= 0 {
percentile = 95
} else if percentile > 100 {
percentile = 100
}
}
percentileBytes, err := SharedServerDailyStatDAO.FindMonthlyPercentile(tx, serverId, month, percentile)
if err != nil {
return err
}
var mb = float32(percentileBytes) / 1024 / 1024
var price float32
if config != nil {
price = config.LookupPrice(mb)
}
var fee = float64(price)
err = SharedServerBillDAO.CreateOrUpdateServerBill(tx, int64(userPlan.UserId), serverId, month, userPlanId, int64(userPlan.PlanId), totalTrafficBytes, percentileBytes, percentile, 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 userId == 0 {
continue
}
amount, err := SharedServerBillDAO.SumUserMonthlyAmount(tx, userId, month)
if err != nil {
return err
}
err = SharedUserBillDAO.CreateBill(tx, userId, BillTypeTraffic, "流量带宽费用", amount, month, month < timeutil.Format("Ym"))
if err != nil {
return err
}
}
@@ -256,74 +369,11 @@ func (this *UserBillDAO) UpdateUserBillIsPaid(tx *dbs.Tx, billId int64, isPaid b
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 {
if len(region.Prices) == 0 || region.Prices == "null" {
continue
}
priceMap := map[string]float32{}
err := json.Unmarshal([]byte(region.Prices), &priceMap)
if err != nil {
return err
}
trafficBytes, err := SharedServerDailyStatDAO.SumUserMonthlyWithoutPlan(tx, userId, int64(region.Id), month)
if err != nil {
return err
}
if trafficBytes == 0 {
continue
}
itemId := SharedNodePriceItemDAO.SearchItemsWithBytes(priceItems, trafficBytes)
if itemId == 0 {
continue
}
price, ok := priceMap[numberutils.FormatInt64(itemId)]
if !ok {
continue
}
// 计算钱
// 这里采用1000进制
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 {
return nil
}
// 创建账单
return this.CreateBill(tx, userId, BillTypeTraffic, "按流量计费", cost, month)
}
// BillTypeName 获取账单类型名称
func (this *UserBillDAO) BillTypeName(billType BillType) string {
switch billType {
case BillTypeTraffic:
return "流量"
return "流量带宽"
}
return ""
}

View File

@@ -7,7 +7,10 @@ type UserBill struct {
Type string `field:"type"` // 消费类型
Description string `field:"description"` // 描述
Amount float64 `field:"amount"` // 消费数额
DayFrom string `field:"dayFrom"` // YYYYMMDD
DayTo string `field:"dayTo"` // YYYYMMDD
Month string `field:"month"` // 帐期YYYYMM
CanPay uint8 `field:"canPay"` // 是否可以支付
IsPaid uint8 `field:"isPaid"` // 是否已支付
PaidAt uint64 `field:"paidAt"` // 支付时间
Code string `field:"code"` // 账单编号
@@ -20,7 +23,10 @@ type UserBillOperator struct {
Type interface{} // 消费类型
Description interface{} // 描述
Amount interface{} // 消费数额
DayFrom interface{} // YYYYMMDD
DayTo interface{} // YYYYMMDD
Month interface{} // 帐期YYYYMM
CanPay interface{} // 是否可以支付
IsPaid interface{} // 是否已支付
PaidAt interface{} // 支付时间
Code interface{} // 账单编号

View File

@@ -95,6 +95,18 @@ func (this *UserDAO) FindEnabledBasicUser(tx *dbs.Tx, id int64) (*User, error) {
return result.(*User), err
}
// FindBasicUserWithoutState 查找用户基本信息,并忽略状态
func (this *UserDAO) FindBasicUserWithoutState(tx *dbs.Tx, id int64) (*User, error) {
result, err := this.Query(tx).
Pk(id).
Result("id", "fullname", "username").
Find()
if result == nil {
return nil, err
}
return result.(*User), err
}
// FindUserFullname 获取管理员名称
func (this *UserDAO) FindUserFullname(tx *dbs.Tx, userId int64) (string, error) {
return this.Query(tx).

View File

@@ -85,6 +85,31 @@ func (this *UserPlanDAO) FindEnabledUserPlan(tx *dbs.Tx, userPlanId int64, cache
return result.(*UserPlan), err
}
// FindUserPlanWithoutState 查找套餐,并不检查状态
// 防止因为删除套餐而导致计费失败
func (this *UserPlanDAO) FindUserPlanWithoutState(tx *dbs.Tx, userPlanId int64, cacheMap *utils.CacheMap) (*UserPlan, error) {
var cacheKey = this.Table + ":FindUserPlanWithoutState:" + types.String(userPlanId)
if cacheMap != nil {
cache, ok := cacheMap.Get(cacheKey)
if ok {
return cache.(*UserPlan), nil
}
}
result, err := this.Query(tx).
Pk(userPlanId).
Find()
if result == nil {
return nil, err
}
if cacheMap != nil {
cacheMap.Put(cacheKey, result)
}
return result.(*UserPlan), err
}
// CountAllEnabledUserPlans 计算套餐数量
func (this *UserPlanDAO) CountAllEnabledUserPlans(tx *dbs.Tx, userId int64, isAvailable bool, isExpired bool, expiringDays int32) (int64, error) {
var query = this.Query(tx).

View File

@@ -348,6 +348,11 @@ func (this *APINode) registerServices(server *grpc.Server) {
pb.RegisterUserBillServiceServer(server, instance)
this.rest(instance)
}
{
instance := this.serviceInstance(&services.ServerBillService{}).(*services.ServerBillService)
pb.RegisterServerBillServiceServer(server, instance)
this.rest(instance)
}
{
instance := this.serviceInstance(&services.UserNodeService{}).(*services.UserNodeService)
pb.RegisterUserNodeServiceServer(server, instance)

View File

@@ -0,0 +1,131 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package services
import (
"context"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/utils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
// ServerBillService 服务账单相关服务
type ServerBillService struct {
BaseService
}
// CountAllServerBills 查询服务账单数量
func (this *ServerBillService) CountAllServerBills(ctx context.Context, req *pb.CountAllServerBillsRequest) (*pb.RPCCountResponse, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, 0, 0)
if err != nil {
return nil, err
}
if userId > 0 {
req.UserId = userId
}
var tx = this.NullTx()
count, err := models.SharedServerBillDAO.CountServerBills(tx, req.UserId, req.Month)
if err != nil {
return nil, err
}
return this.SuccessCount(count)
}
// ListServerBills 查询服务账单列表
func (this *ServerBillService) ListServerBills(ctx context.Context, req *pb.ListServerBillsRequest) (*pb.ListServerBillsResponse, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, 0, 0)
if err != nil {
return nil, err
}
if userId > 0 {
req.UserId = userId
}
var tx = this.NullTx()
serverBills, err := models.SharedServerBillDAO.ListServerBills(tx, req.UserId, req.Month, req.Offset, req.Size)
if err != nil {
return nil, err
}
var pbServerBills = []*pb.ServerBill{}
var cacheMap = utils.NewCacheMap()
for _, bill := range serverBills {
// user
user, err := models.SharedUserDAO.FindBasicUserWithoutState(tx, int64(bill.UserId))
if err != nil {
return nil, err
}
var pbUser = &pb.User{Id: int64(bill.UserId)}
if user != nil {
pbUser = &pb.User{
Id: int64(bill.UserId),
Username: user.Username,
Fullname: user.Fullname,
}
}
// plan
var pbPlan *pb.Plan
if bill.PlanId > 0 {
plan, err := models.SharedPlanDAO.FindEnabledPlan(tx, int64(bill.PlanId))
if err != nil {
return nil, err
}
if plan != nil {
pbPlan = &pb.Plan{
Id: int64(plan.Id),
Name: plan.Name,
PriceType: plan.PriceType,
}
}
}
// user plan
var pbUserPlan *pb.UserPlan
if bill.UserPlanId > 0 {
userPlan, err := models.SharedUserPlanDAO.FindEnabledUserPlan(tx, int64(bill.UserPlanId), cacheMap)
if err != nil {
return nil, err
}
if userPlan != nil {
pbUserPlan = &pb.UserPlan{
Id: int64(userPlan.Id),
}
}
}
// server
var pbServer *pb.Server
if bill.ServerId > 0 {
server, err := models.SharedServerDAO.FindEnabledServerBasic(tx, int64(bill.ServerId))
if err != nil {
return nil, err
}
if server != nil {
pbServer = &pb.Server{Id: int64(bill.ServerId), Name: server.Name}
}
}
pbServerBills = append(pbServerBills, &pb.ServerBill{
Id: int64(bill.Id),
UserId: int64(bill.UserId),
ServerId: int64(bill.ServerId),
Amount: float32(bill.Amount),
CreatedAt: int64(bill.CreatedAt),
UserPlanId: int64(bill.UserPlanId),
PlanId: int64(bill.PlanId),
TotalTrafficBytes: int64(bill.TotalTrafficBytes),
BandwidthPercentileBytes: int64(bill.BandwidthPercentileBytes),
BandwidthPercentile: int32(bill.BandwidthPercentile),
User: pbUser,
Plan: pbPlan,
UserPlan: pbUserPlan,
Server: pbServer,
})
}
return &pb.ListServerBillsResponse{ServerBills: pbServerBills}, nil
}

View File

@@ -406,7 +406,7 @@ func (this *UserService) ComposeUserDashboard(ctx context.Context, req *pb.Compo
// 本月总流量
month := timeutil.Format("Ym")
monthlyTrafficBytes, err := models.SharedServerDailyStatDAO.SumUserMonthly(tx, req.UserId, 0, month)
monthlyTrafficBytes, err := models.SharedServerDailyStatDAO.SumUserMonthly(tx, req.UserId, month)
if err != nil {
return nil, err
}

View File

@@ -74,7 +74,7 @@ func (this *UserBillService) ListUserBills(ctx context.Context, req *pb.ListUser
}
result := []*pb.UserBill{}
for _, bill := range bills {
user, err := models.SharedUserDAO.FindEnabledBasicUser(tx, int64(bill.UserId))
user, err := models.SharedUserDAO.FindBasicUserWithoutState(tx, int64(bill.UserId))
if err != nil {
return nil, err
}
@@ -88,12 +88,14 @@ func (this *UserBillService) ListUserBills(ctx context.Context, req *pb.ListUser
Id: int64(bill.UserId),
Fullname: user.Fullname,
Username: user.Username,
IsDeleted: user.State == models.UserStateDisabled,
},
Type: bill.Type,
TypeName: models.SharedUserBillDAO.BillTypeName(bill.Type),
Description: bill.Description,
Amount: float32(bill.Amount),
Month: bill.Month,
CanPay: bill.CanPay == 1,
IsPaid: bill.IsPaid == 1,
PaidAt: int64(bill.PaidAt),
Code: bill.Code,
@@ -151,6 +153,7 @@ func (this *UserBillService) FindUserBill(ctx context.Context, req *pb.FindUserB
Description: bill.Description,
Amount: float32(bill.Amount),
Month: bill.Month,
CanPay: bill.CanPay == 1,
IsPaid: bill.IsPaid == 1,
PaidAt: int64(bill.PaidAt),
Code: bill.Code,
@@ -194,6 +197,10 @@ func (this *UserBillService) PayUserBill(ctx context.Context, req *pb.PayUserBil
return models.SharedUserBillDAO.UpdateUserBillIsPaid(tx, req.UserBillId, true)
}
if bill.CanPay == 0 {
return errors.New("can not pay now")
}
// 余额是否足够
account, err := accounts.SharedUserAccountDAO.FindUserAccountWithUserId(tx, userId)
if err != nil {

File diff suppressed because one or more lines are too long

View File

@@ -65,6 +65,9 @@ var upgradeFuncs = []*upgradeVersion{
{
"0.4.0", upgradeV0_4_0,
},
{
"0.4.1", upgradeV0_4_1,
},
}
// UpgradeSQLData 升级SQL数据
@@ -569,3 +572,14 @@ func upgradeV0_4_0(db *dbs.DB) error {
return nil
}
// v0.4.1
func upgradeV0_4_1(db *dbs.DB) error {
// 升级 servers.lastUserPlanId
_, err := db.Exec("UPDATE edgeServers SET lastUserPlanId=userPlanId WHERE userPlanId>0")
if err != nil {
return err
}
return nil
}

View File

@@ -85,7 +85,6 @@ func TestUpgradeSQLData_v0_3_7(t *testing.T) {
t.Log("ok")
}
func TestUpgradeSQLData_v0_4_0(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
@@ -101,3 +100,19 @@ func TestUpgradeSQLData_v0_4_0(t *testing.T) {
}
t.Log("ok")
}
func TestUpgradeSQLData_v0_4_1(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
t.Fatal(err)
}
err = upgradeV0_4_1(db)
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}