Files
waf-platform/EdgeAPI/internal/db/models/user_bill_dao_plus.go
2026-02-04 20:27:13 +08:00

1293 lines
37 KiB
Go

// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package models
import (
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/TeaOSLab/EdgeAPI/internal/goman"
"github.com/TeaOSLab/EdgeAPI/internal/remotelogs"
"github.com/TeaOSLab/EdgeAPI/internal/utils"
"github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeAPI/internal/utils/regexputils"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/types"
timeutil "github.com/iwind/TeaGo/utils/time"
"sort"
"strings"
"time"
)
type BillType = string
const (
BillTypeTraffic BillType = "traffic" // 按流量计费
BillTypeBandwidth BillType = "bandwidth" // 按带宽计费
BillTypeTrafficAndBandwidth BillType = "trafficAndBandwidth" // 但流量和带宽计费
)
const (
UserBillStateEnabled = 1
UserBillStateDisabled = 0
)
func init() {
dbs.OnReadyDone(func() {
goman.New(func() {
// 自动生成账单任务
var ticker = time.NewTicker(1 * time.Hour)
for range ticker.C {
if SharedAPINodeDAO.CheckAPINodeIsPrimaryWithoutErr() { // 只在主API节点上运行
// 如果是凌晨,则延迟,避免因为数据没有及时上传而导致的错误
var currentHour = time.Now().Hour()
if currentHour >= 1 {
err := SharedUserBillDAO.GenerateBills()
if err != nil {
remotelogs.Error("UserBillDAO", "generate bills failed: "+err.Error())
}
}
}
}
})
})
}
// 类型定义
type regionPrice struct {
regionId int64
price float64
amount float64
bandwidthMB float64
trafficGB float64
trafficPackageGB float64
userTrafficPackageIds []int64
}
// DeleteUserBill 删除账单
// 物理删除账单是为了避免账单表 unique key 冲突
func (this *UserBillDAO) DeleteUserBill(tx *dbs.Tx, billId int64) error {
return this.Query(tx).
Pk(billId).
DeleteQuickly()
}
// GenerateBills 生成月账单
func (this *UserBillDAO) GenerateBills() error {
var tx *dbs.Tx
// 默认计费方式
priceConfig, err := SharedSysSettingDAO.ReadUserPriceConfig(tx)
if err != nil {
return err
}
if priceConfig == nil || !priceConfig.IsOn {
return nil
}
// 使用套餐计费
if priceConfig.EnablePlans {
return this.GenerateBillsWithPlans(tx, timeutil.Format("Ym", time.Now().AddDate(0, -1, 0)), priceConfig)
}
// 生成流量和带宽账单
// 默认识别两个月内有流量的用户
var trafficDayFrom = timeutil.Format("Ym01", time.Now().AddDate(0, -1, 0))
var trafficDayTo = timeutil.Format("Ymd", time.Now().AddDate(0, 0, -1))
var billMonth = timeutil.Format("Ym", time.Now().AddDate(0, -1, 0))
var billDay = timeutil.Format("Ymd", time.Now().AddDate(0, 0, -1))
err = this.GenerateTrafficAndBandwidthBills(tx, trafficDayFrom, trafficDayTo, billMonth, billDay, priceConfig)
if err != nil {
return err
}
return nil
}
// GenerateTrafficAndBandwidthBills 根据流量或带宽计算账单
// trafficDayFrom 和 trafficDayTo 之间的时间间隔需要长一些,因为,可能是按月生成的
func (this *UserBillDAO) GenerateTrafficAndBandwidthBills(tx *dbs.Tx, trafficDayFrom string, trafficDayTo string, billMonth string, billDay string, priceConfig *userconfigs.UserPriceConfig) error {
if priceConfig == nil || !priceConfig.IsOn || priceConfig.EnablePlans /** 使用套餐 **/ {
return nil
}
if !regexputils.YYYYMMDD.MatchString(trafficDayFrom) {
return errors.New("invalid 'trafficDayFrom': " + trafficDayFrom)
}
if !regexputils.YYYYMMDD.MatchString(trafficDayTo) {
return errors.New("invalid 'trafficDayTo':" + trafficDayTo)
}
if len(billMonth) > 0 && !regexputils.YYYYMM.MatchString(billMonth) {
return errors.New("invalid 'billMonth':" + billMonth)
}
if len(billDay) > 0 && !regexputils.YYYYMMDD.MatchString(billDay) {
return errors.New("invalid 'billDay':" + billDay)
}
// 带宽用户
bandwidthUserIds, err := SharedUserBandwidthStatDAO.FindDistinctUserIds(tx, trafficDayFrom, trafficDayTo)
if err != nil {
return err
}
// 流量用户
trafficUserIds, err := SharedServerDailyStatDAO.FindDistinctUserIds(tx, trafficDayFrom, trafficDayTo)
if err != nil {
return err
}
// 区域价格设置
regions, err := SharedNodeRegionDAO.FindAllEnabledRegionPrices(tx)
if err != nil {
return err
}
var regionItemMaps = map[int64]map[int64]float64{} // regionId => { itemId => price }
var regionIds = []int64{0}
for _, region := range regions {
regionIds = append(regionIds, int64(region.Id))
regionItemMaps[int64(region.Id)] = region.DecodePriceMap()
}
// 价格区间
bandwidthPriceItems, err := SharedNodePriceItemDAO.FindAllEnabledAndOnRegionPrices(tx, userconfigs.PriceTypeBandwidth)
if err != nil {
return err
}
trafficPriceItems, err := SharedNodePriceItemDAO.FindAllEnabledAndOnRegionPrices(tx, userconfigs.PriceTypeTraffic)
if err != nil {
return err
}
var userIds = this.mergeUserIds([][]int64{bandwidthUserIds, trafficUserIds})
for _, userId := range userIds {
err := func(userId int64) error {
var month = timeutil.Format("Ym", time.Now().AddDate(0, -1, 0))
var day = timeutil.Format("Ymd", time.Now().AddDate(0, 0, -1))
if len(billMonth) > 0 {
month = billMonth
}
if len(billDay) > 0 {
day = billDay
}
// 生成用户账单
err := this.GenerateUserTrafficAndBandwidthBills(tx, userId, priceConfig, bandwidthPriceItems, trafficPriceItems, regionIds, regionItemMaps, month, day)
if err != nil {
return err
}
// 更新用户状态
_, err = SharedUserDAO.RenewUserServersState(tx, userId)
return err
}(userId)
if err != nil {
return err
}
}
return nil
}
// GenerateUserTrafficAndBandwidthBills 生成单个用户的流量和带宽账单
// month 强制生成某月
// day 强制生成某日
func (this *UserBillDAO) GenerateUserTrafficAndBandwidthBills(tx *dbs.Tx,
userId int64,
priceConfig *userconfigs.UserPriceConfig,
bandwidthPriceItems []*NodePriceItem,
trafficPriceItems []*NodePriceItem,
regionIds []int64,
regionItemMaps map[int64]map[int64]float64,
month string,
day string) error {
if len(month) > 0 && !regexputils.YYYYMM.MatchString(month) {
return errors.New("invalid 'month': " + month)
}
if len(day) > 0 && !regexputils.YYYYMMDD.MatchString(day) {
return errors.New("invalid 'day': " + day)
}
if len(month) == 0 {
month = timeutil.Format("Ym", time.Now().AddDate(0, -1, 0))
}
if len(day) == 0 {
day = timeutil.Format("Ymd", time.Now().AddDate(0, 0, -1))
}
// 计费方式和周期
var priceType = priceConfig.DefaultPriceType
var pricePeriod = priceConfig.DefaultPricePeriod
userPriceType, userPricePeriod, err := SharedUserDAO.FindUserPriceInfo(tx, userId)
if err != nil {
return err
}
if priceConfig.UserCanChangePriceType && (userPriceType == userconfigs.PriceTypeBandwidth || userPriceType == userconfigs.PriceTypeTraffic) {
priceType = userPriceType
}
if priceConfig.UserCanChangePricePeriod && userconfigs.IsValidPricePeriod(userPricePeriod) {
pricePeriod = userPricePeriod
}
// 用户最近一次账单
lastBill, err := this.FindUserLatestTrafficAndBandwidthBill(tx, userId)
if err != nil {
return err
}
var lastBillMonth = ""
var lastBillDay = ""
var lastPricePeriod = ""
var lastBillIsFinished = false
var lastBillType = ""
var lastBillId int64
if lastBill != nil {
lastBillId = int64(lastBill.Id)
lastBillMonth = lastBill.Month
lastPricePeriod = lastBill.PricePeriod
lastBillIsFinished = lastBill.CanPay
lastBillType = lastBill.Type
switch lastBill.PricePeriod {
case "daily":
lastBillDay = lastBill.DayTo
}
// 是否同当前一致
if lastBillIsFinished && lastPricePeriod == pricePeriod && ((pricePeriod == userconfigs.PricePeriodDaily && lastBillDay == day) || (pricePeriod == userconfigs.PricePeriodMonthly && lastBillMonth == month)) {
return nil
}
}
// 检查账单周期冲突
if pricePeriod == userconfigs.PricePeriodDaily { // 按天
if lastPricePeriod == userconfigs.PricePeriodMonthly && len(lastBillMonth) > 0 && strings.HasPrefix(day, lastBillMonth) /** 同月 **/ {
pricePeriod = userconfigs.PricePeriodMonthly
}
} else if pricePeriod == userconfigs.PricePeriodMonthly { // 按月
if lastPricePeriod == userconfigs.PricePeriodDaily && len(lastBillDay) > 0 && strings.HasPrefix(lastBillDay, month) /** 同月 **/ {
pricePeriod = userconfigs.PricePeriodDaily
}
}
var dayFrom string
var dayTo string
switch pricePeriod {
case userconfigs.PricePeriodDaily:
dayFrom = day
dayTo = day
// 如果和已生成的账单一致,则继续沿用计费方式
if lastBillDay == dayTo && lastPricePeriod == pricePeriod {
if lastBillIsFinished {
switch lastBillType {
case BillTypeTraffic:
priceType = userconfigs.PriceTypeTraffic
case BillTypeBandwidth:
priceType = userconfigs.PriceTypeBandwidth
}
} else {
err = this.DeleteUserBill(tx, lastBillId)
if err != nil {
return err
}
}
}
case userconfigs.PricePeriodMonthly:
dayFrom = month + "01"
dayTo, err = utils.FixMonthMaxDay(month + "31")
if err != nil {
return err
}
// 如果和已生成的账单一致,则继续沿用计费方式
if lastBillMonth == month && lastPricePeriod == pricePeriod {
if lastBillIsFinished {
switch lastBillType {
case BillTypeTraffic:
priceType = userconfigs.PriceTypeTraffic
case BillTypeBandwidth:
priceType = userconfigs.PriceTypeBandwidth
}
} else {
err = this.DeleteUserBill(tx, lastBillId)
if err != nil {
return err
}
}
}
default:
return errors.New("invalid price period '" + pricePeriod + "'")
}
switch priceType {
case userconfigs.PriceTypeBandwidth:
var bandwidthConfig = priceConfig.DefaultBandwidthPriceConfig
if bandwidthConfig == nil {
return nil
}
if bandwidthConfig.SupportRegions {
var totalAmount float64
var regionPrices []*regionPrice
for _, regionId := range regionIds {
var searchRegionId = regionId
if searchRegionId == 0 {
searchRegionId = -1 // 查询没有分区域的
}
stat, err := SharedUserBandwidthStatDAO.FindPercentileBetweenDays(tx, userId, searchRegionId, dayFrom, dayTo, int32(bandwidthConfig.Percentile), bandwidthConfig.SupportAvgBandwidth())
if err != nil {
return err
}
if stat == nil || stat.Bytes == 0 {
continue
}
var sizeMB = float64(stat.Bytes*8) / (1 << 20)
var price float64
var amount float64
var itemId = SharedNodePriceItemDAO.SearchItemsWithBits(bandwidthPriceItems, int64(stat.Bytes*8))
if itemId == 0 {
price, amount = bandwidthConfig.LookupPrice(sizeMB)
} else {
var found = false
priceMap, ok := regionItemMaps[regionId]
if ok {
itemPrice, ok := priceMap[itemId]
if ok {
found = true
price = itemPrice
amount = itemPrice * sizeMB
}
}
if !found {
price, amount = bandwidthConfig.LookupPrice(sizeMB)
}
}
regionPrices = append(regionPrices, &regionPrice{
regionId: regionId,
price: price,
amount: amount,
bandwidthMB: sizeMB,
trafficGB: 0,
})
totalAmount += amount
}
// 创建账单
billId, err := this.CreateUserTrafficBill(tx, userId, BillTypeBandwidth, pricePeriod, "带宽费用", totalAmount, month, dayFrom, dayTo, dayTo < timeutil.Format("Ymd"))
if err != nil {
return err
}
err = SharedUserTrafficBillDAO.DisableTrafficBills(tx, billId)
if err != nil {
return err
}
for _, regionPriceItem := range regionPrices {
err = SharedUserTrafficBillDAO.CreateTrafficBill(tx, billId, regionPriceItem.regionId, priceType, regionPriceItem.bandwidthMB, int32(bandwidthConfig.Percentile), 0, 0, nil, regionPriceItem.price, regionPriceItem.amount)
if err != nil {
return err
}
}
} else {
stat, err := SharedUserBandwidthStatDAO.FindPercentileBetweenDays(tx, userId, 0, dayFrom, dayTo, int32(bandwidthConfig.Percentile), bandwidthConfig.SupportAvgBandwidth())
if err != nil {
return err
}
if stat == nil || stat.Bytes == 0 {
_, err = this.CreateUserTrafficBill(tx, userId, BillTypeBandwidth, pricePeriod, "带宽费用", 0, month, dayFrom, dayTo, month < timeutil.Format("Ym"))
if err != nil {
return err
}
return nil
}
var sizeMB = float64(stat.Bytes*8) / (1 << 20)
price, amount := bandwidthConfig.LookupPrice(sizeMB)
billId, err := this.CreateUserTrafficBill(tx, userId, BillTypeBandwidth, pricePeriod, "带宽费用", amount, month, dayFrom, dayTo, dayTo < timeutil.Format("Ymd"))
if err != nil {
return err
}
// 删除老的子订单
err = SharedUserTrafficBillDAO.DisableTrafficBills(tx, billId)
if err != nil {
return err
}
// 新的子账单
err = SharedUserTrafficBillDAO.CreateTrafficBill(tx, billId, 0, priceType, sizeMB, int32(bandwidthConfig.Percentile), 0, 0, nil, price, amount)
if err != nil {
return err
}
}
case userconfigs.PriceTypeTraffic:
var trafficConfig = priceConfig.DefaultTrafficPriceConfig
if trafficConfig == nil {
return nil
}
if trafficConfig.SupportRegions || priceConfig.EnableTrafficPackages {
var totalAmount float64
var regionPrices []*regionPrice
for _, regionId := range regionIds {
var searchRegionId = regionId
if searchRegionId == 0 {
searchRegionId = -1 // 查询没有分区域的
}
totalBytes, err := SharedServerDailyStatDAO.SumUserTrafficBytesBetweenDays(tx, userId, searchRegionId, dayFrom, dayTo)
if err != nil {
return err
}
if totalBytes == 0 {
continue
}
var sizeGB = float64(totalBytes) / (1 << 30)
// 尝试从流量包中扣除
// 即使没有启用流量包,以往的流量包仍然可以使用
var trafficPackageGB float64
var userTrafficPackageIds []int64
if regionId > 0 && dayTo < timeutil.Format("Ymd") /** 只适用于过往日期 **/ { // TODO 将来支持不分区
userTrafficPackageIds2, leftBytes, err := SharedUserTrafficPackageDAO.CostUserPackages(tx, userId, regionId, totalBytes)
if err != nil {
return err
}
if len(userTrafficPackageIds2) > 0 {
userTrafficPackageIds = userTrafficPackageIds2
trafficPackageGB = float64(totalBytes-leftBytes) / (1 << 30)
totalBytes = leftBytes
sizeGB = float64(totalBytes) / (1 << 30)
if totalBytes == 0 {
regionPrices = append(regionPrices, &regionPrice{
regionId: regionId,
price: 0,
amount: 0,
bandwidthMB: 0,
trafficGB: sizeGB,
trafficPackageGB: trafficPackageGB,
userTrafficPackageIds: userTrafficPackageIds,
})
continue
}
}
}
var price float64
var amount float64
var itemId int64
if trafficConfig.SupportRegions {
itemId = SharedNodePriceItemDAO.SearchItemsWithBits(trafficPriceItems, totalBytes*8)
}
if itemId == 0 {
price, amount = trafficConfig.LookupPrice(sizeGB)
} else {
var found = false
priceMap, ok := regionItemMaps[regionId]
if ok {
itemPrice, ok := priceMap[itemId]
if ok {
found = true
price = itemPrice
amount = itemPrice * sizeGB
}
}
if !found {
price, amount = trafficConfig.LookupPrice(sizeGB)
}
}
regionPrices = append(regionPrices, &regionPrice{
regionId: regionId,
price: price,
amount: amount,
bandwidthMB: 0,
trafficGB: sizeGB,
trafficPackageGB: trafficPackageGB,
userTrafficPackageIds: userTrafficPackageIds,
})
totalAmount += amount
}
// 创建账单
billId, err := this.CreateUserTrafficBill(tx, userId, BillTypeTraffic, pricePeriod, "流量费用", totalAmount, month, dayFrom, dayTo, dayTo < timeutil.Format("Ymd"))
if err != nil {
return err
}
err = SharedUserTrafficBillDAO.DisableTrafficBills(tx, billId)
if err != nil {
return err
}
for _, regionPriceItem := range regionPrices {
err = SharedUserTrafficBillDAO.CreateTrafficBill(tx, billId, regionPriceItem.regionId, priceType, 0, 0, regionPriceItem.trafficGB, regionPriceItem.trafficPackageGB, regionPriceItem.userTrafficPackageIds, regionPriceItem.price, regionPriceItem.amount)
if err != nil {
return err
}
}
} else {
totalBytes, err := SharedServerDailyStatDAO.SumUserTrafficBytesBetweenDays(tx, userId, 0, dayFrom, dayTo)
if err != nil {
return err
}
if totalBytes == 0 {
_, err = this.CreateUserTrafficBill(tx, userId, BillTypeTraffic, pricePeriod, "流量费用", 0, month, dayFrom, dayTo, month < timeutil.Format("Ym"))
if err != nil {
return err
}
return nil
}
var sizeGB = float64(totalBytes) / (1 << 30)
price, amount := trafficConfig.LookupPrice(sizeGB)
billId, err := this.CreateUserTrafficBill(tx, userId, BillTypeTraffic, pricePeriod, "流量费用", amount, month, dayFrom, dayTo, dayTo < timeutil.Format("Ymd"))
if err != nil {
return err
}
// 删除老的子订单
err = SharedUserTrafficBillDAO.DisableTrafficBills(tx, billId)
if err != nil {
return err
}
// 新的子账单
err = SharedUserTrafficBillDAO.CreateTrafficBill(tx, billId, 0, priceType, 0, 0, sizeGB, 0, nil, price, amount)
if err != nil {
return err
}
}
}
return nil
}
func (this *UserBillDAO) mergeUserIds(userIdSlices [][]int64) []int64 {
var userIdMap = map[int64]bool{}
var userIds = []int64{}
for _, userIdSlice := range userIdSlices {
for _, userId := range userIdSlice {
if userId <= 0 {
continue
}
_, ok := userIdMap[userId]
if ok {
continue
}
userIds = append(userIds, userId)
userIdMap[userId] = true
}
}
// 排序
sort.Slice(userIds, func(i, j int) bool {
return userIds[i] < userIds[j]
})
return userIds
}
// GenerateBillsWithPlans 根据套餐计算账单
func (this *UserBillDAO) GenerateBillsWithPlans(tx *dbs.Tx, month string, userPriceConfig *userconfigs.UserPriceConfig) error {
// 区域价格
regions, err := SharedNodeRegionDAO.FindAllEnabledRegionPrices(tx)
if err != nil {
return err
}
var priceItems []*NodePriceItem
if len(regions) > 0 {
priceItems, err = SharedNodePriceItemDAO.FindAllEnabledRegionPrices(tx, NodePriceTypeTraffic)
if err != nil {
return err
}
}
// 计算服务套餐费用
plans, err := SharedPlanDAO.FindAllEnabledPlans(tx)
if err != nil {
return err
}
var planMap = map[int64]*Plan{}
for _, plan := range plans {
planMap[int64(plan.Id)] = plan
}
var dayFrom = month + "01"
var dayTo = month + "31"
userPlanIds, err := SharedUserPlanStatDAO.FindDistinctUserPlanIds(tx, dayFrom, dayTo)
if err != nil {
return err
}
var cacheMap = utils.NewCacheMap()
var userIds = []int64{}
// 套餐计费方式
for _, userPlanId := range userPlanIds {
if userPlanId <= 0 {
continue
}
userPlan, err := SharedUserPlanDAO.FindUserPlanWithoutState(tx, userPlanId, cacheMap)
if err != nil {
return err
}
if userPlan == nil {
continue
}
var userId = int64(userPlan.UserId)
if userId <= 0 {
continue
}
if !lists.ContainsInt64(userIds, userId) {
userIds = append(userIds, userId)
}
bandwidthAlgo, err := SharedUserDAO.FindUserBandwidthAlgoForFee(tx, userId, userPriceConfig)
if err != nil {
return err
}
plan, ok := planMap[int64(userPlan.PlanId)]
if !ok {
continue
}
// 总流量
totalTrafficBytes, err := SharedUserPlanBandwidthStatDAO.SumMonthlyBytes(tx, userPlanId, month)
if err != nil {
return err
}
switch plan.PriceType {
case serverconfigs.PlanPriceTypePeriod:
// 已经在购买套餐的时候付过费,这里不再重复计费
var fee float64 = 0
// 百分位
var percentile = 95
percentileBytes, err := SharedUserPlanBandwidthStatDAO.FindMonthlyPercentile(tx, userPlanId, month, percentile, bandwidthAlgo == systemconfigs.BandwidthAlgoAvg)
if err != nil {
return err
}
err = SharedServerBillDAO.CreateOrUpdateServerBill(tx, int64(userPlan.UserId), 0, month, userPlanId, int64(userPlan.PlanId), totalTrafficBytes, percentileBytes, percentile, plan.PriceType, 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) / (1 << 30) * config.Base
}
// 百分位
var percentile = 95
percentileBytes, err := SharedUserPlanBandwidthStatDAO.FindMonthlyPercentile(tx, userPlanId, month, percentile, bandwidthAlgo == systemconfigs.BandwidthAlgoAvg)
if err != nil {
return err
}
err = SharedServerBillDAO.CreateOrUpdateServerBill(tx, int64(userPlan.UserId), 0, month, userPlanId, int64(userPlan.PlanId), totalTrafficBytes, percentileBytes, percentile, plan.PriceType, 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 := SharedUserPlanBandwidthStatDAO.FindMonthlyPercentile(tx, userPlanId, month, percentile, bandwidthAlgo == systemconfigs.BandwidthAlgoAvg)
if err != nil {
return err
}
var mb = float64(percentileBytes) / (1 << 20)
var amount float64
if config != nil {
_, amount = config.LookupPrice(mb)
}
var fee = amount
err = SharedServerBillDAO.CreateOrUpdateServerBill(tx, int64(userPlan.UserId), 0, month, userPlanId, int64(userPlan.PlanId), totalTrafficBytes, percentileBytes, percentile, plan.PriceType, fee)
if err != nil {
return err
}
}
}
// 默认计费方式
for partitionIndex := 0; partitionIndex < SharedServerBandwidthStatDAO.CountPartitions(); partitionIndex++ {
serverIds, err := SharedServerBandwidthStatDAO.FindDistinctServerIdsWithoutPlanAtPartition(tx, partitionIndex, month)
if err != nil {
return err
}
for _, serverId := range serverIds {
userId, err := SharedServerDAO.FindServerUserId(tx, serverId)
if err != nil {
return err
}
if userId <= 0 {
continue
}
if !lists.ContainsInt64(userIds, userId) {
userIds = append(userIds, userId)
}
bandwidthAlgo, err := SharedUserDAO.FindUserBandwidthAlgoForFee(tx, userId, userPriceConfig)
if err != nil {
return err
}
// 总流量
totalTrafficBytes, err := SharedServerBandwidthStatDAO.SumMonthlyBytes(tx, serverId, month, true)
if err != nil {
return err
}
if userPriceConfig != nil && userPriceConfig.IsOn { // 默认计费方式
switch userPriceConfig.DefaultPriceType {
case serverconfigs.PlanPriceTypeTraffic:
var config = userPriceConfig.DefaultTrafficPriceConfig
var fee float64 = 0
if config != nil && config.Base > 0 {
fee = float64(totalTrafficBytes) / (1 << 30) * config.Base
}
// 百分位
var percentile = 95
percentileBytes, err := SharedServerBandwidthStatDAO.FindMonthlyPercentile(tx, serverId, month, percentile, bandwidthAlgo == systemconfigs.BandwidthAlgoAvg, true, 0)
if err != nil {
return err
}
err = SharedServerBillDAO.CreateOrUpdateServerBill(tx, userId, serverId, month, 0, 0, totalTrafficBytes, percentileBytes, percentile, userPriceConfig.DefaultPriceType, fee)
if err != nil {
return err
}
case serverconfigs.PlanPriceTypeBandwidth:
// 百分位
var percentile = 95
var config = userPriceConfig.DefaultBandwidthPriceConfig
if config != nil {
percentile = config.Percentile
if percentile <= 0 {
percentile = 95
} else if percentile > 100 {
percentile = 100
}
}
percentileBytes, err := SharedServerBandwidthStatDAO.FindMonthlyPercentile(tx, serverId, month, percentile, bandwidthAlgo == systemconfigs.BandwidthAlgoAvg, true, 10)
if err != nil {
return err
}
var mb = float64(percentileBytes) / (1 << 20)
var amount float64
if config != nil {
_, amount = config.LookupPrice(mb)
}
var fee = amount
err = SharedServerBillDAO.CreateOrUpdateServerBill(tx, userId, serverId, month, 0, 0, totalTrafficBytes, percentileBytes, percentile, userPriceConfig.DefaultPriceType, fee)
if err != nil {
return err
}
}
} else { // 区域流量计费
var fee float64
for _, region := range regions {
var regionId = int64(region.Id)
var pricesMap = region.DecodePriceMap()
if len(pricesMap) == 0 {
continue
}
trafficBytes, err := SharedServerBandwidthStatDAO.SumServerMonthlyWithRegion(tx, serverId, regionId, month, true)
if err != nil {
return err
}
if trafficBytes == 0 {
continue
}
var itemId = SharedNodePriceItemDAO.SearchItemsWithBits(priceItems, trafficBytes*8)
if itemId == 0 {
continue
}
price, ok := pricesMap[itemId]
if !ok {
continue
}
if price <= 0 {
continue
}
var regionFee = float64(trafficBytes) / 1000 / 1000 / 1000 * 8 * price
fee += regionFee
}
// 百分位
var percentile = 95
percentileBytes, err := SharedServerBandwidthStatDAO.FindMonthlyPercentile(tx, serverId, month, percentile, bandwidthAlgo == systemconfigs.BandwidthAlgoAvg, true, 0)
if err != nil {
return err
}
err = SharedServerBillDAO.CreateOrUpdateServerBill(tx, userId, serverId, month, 0, 0, totalTrafficBytes, percentileBytes, percentile, "", fee)
if err != nil {
return err
}
}
}
}
// 计算用户费用
for _, userId := range userIds {
if userId == 0 {
continue
}
amount, err := SharedServerBillDAO.SumUserMonthlyAmount(tx, userId, month)
if err != nil {
return err
}
lastDayInMonth, err := utils.LastDayInMonth(month)
if err != nil {
return err
}
_, err = SharedUserBillDAO.CreateUserTrafficBill(tx, userId, BillTypeTrafficAndBandwidth, userconfigs.PricePeriodMonthly, "流量带宽费用", amount, month, month+"01", lastDayInMonth, month < timeutil.Format("Ym"))
if err != nil {
return err
}
}
return nil
}
// CalculatePrice 计算价格
func (this *UserBillDAO) CalculatePrice(tx *dbs.Tx, priceType string, trafficGB float64, bandwidthMB float64, regionId int64) (amount float64, hasRegionPrice bool, err error) {
if !userconfigs.IsValidPriceType(priceType) {
return 0, false, errors.New("invalid priceType '" + priceType + "'")
}
// 整体配置
priceConfig, err := SharedSysSettingDAO.ReadUserPriceConfig(tx)
if err != nil {
return 0, false, err
}
if priceConfig == nil {
// 如果尚未设置计费就是0
return 0, false, nil
}
switch priceType {
case userconfigs.PriceTypeTraffic:
if trafficGB <= 0 {
return 0, false, nil
}
if priceConfig.DefaultTrafficPriceConfig == nil {
return 0, false, nil
}
_, amount = priceConfig.DefaultTrafficPriceConfig.LookupPrice(trafficGB)
case userconfigs.PriceTypeBandwidth:
if bandwidthMB <= 0 {
return 0, false, nil
}
if priceConfig.DefaultBandwidthPriceConfig == nil {
return 0, false, nil
}
_, amount = priceConfig.DefaultBandwidthPriceConfig.LookupPrice(bandwidthMB)
}
// 区域配置
if (priceType == userconfigs.PriceTypeTraffic && priceConfig.DefaultTrafficPriceConfig != nil && priceConfig.DefaultTrafficPriceConfig.SupportRegions) || (priceType == userconfigs.PriceTypeBandwidth && priceConfig.DefaultBandwidthPriceConfig != nil && priceConfig.DefaultBandwidthPriceConfig.SupportRegions) {
if regionId > 0 {
nodeRegion, err := SharedNodeRegionDAO.FindEnabledNodeRegion(tx, regionId)
if err != nil {
return 0, false, err
}
if nodeRegion == nil {
return 0, false, errors.New("invalid 'regionId': " + types.String(regionId))
}
var prices = nodeRegion.DecodePriceMap()
if len(prices) > 0 {
allPriceItems, err := SharedNodePriceItemDAO.FindAllEnabledAndOnRegionPrices(tx, priceType)
if err != nil {
return 0, false, err
}
var bits int64
switch priceType {
case userconfigs.PriceTypeTraffic:
bits = int64(trafficGB * (1 << 33)) // GBytes
case userconfigs.PriceTypeBandwidth:
bits = int64(bandwidthMB * (1 << 20)) // Mbps
}
var itemId = SharedNodePriceItemDAO.SearchItemsWithBits(allPriceItems, bits)
if itemId > 0 {
price, ok := prices[itemId]
if ok {
switch priceType {
case userconfigs.PriceTypeTraffic:
amount = numberutils.FloorFloat64(price*trafficGB, 2)
hasRegionPrice = true
case userconfigs.PriceTypeBandwidth:
amount = numberutils.FloorFloat64(price*bandwidthMB, 2)
hasRegionPrice = true
}
}
}
}
}
}
return
}
// CreateUserTrafficBill 创建流量账单
// month YYYYMM
// dayFrom YYYYMMDD
// dayTo YYYYMMDD
func (this *UserBillDAO) CreateUserTrafficBill(tx *dbs.Tx, userId int64, billType BillType, pricePeriod userconfigs.PricePeriod, description string, amount float64, month string, dayFrom string, dayTo string, canPay bool) (billId int64, err error) {
if !regexputils.YYYYMMDD.MatchString(dayFrom) {
return 0, errors.New("invalid dayFrom '" + dayFrom + "'")
}
if !regexputils.YYYYMMDD.MatchString(dayTo) {
return 0, errors.New("invalid dayTo")
}
dayFrom, err = utils.FixMonthMaxDay(dayFrom)
if err != nil {
return
}
dayTo, err = utils.FixMonthMaxDay(dayTo)
if err != nil {
return
}
if pricePeriod == userconfigs.PricePeriodDaily {
month = dayTo[:6]
}
// 账单是否存在
oldBillId, err := this.Query(tx).
State(UserBillStateEnabled).
Attr("userId", userId).
Attr("type", billType).
Attr("pricePeriod", pricePeriod).
Attr("month", month).
Attr("dayFrom", dayFrom).
Attr("dayTo", dayTo).
FindInt64Col(0)
if err != nil {
return 0, err
}
if oldBillId > 0 {
err = this.Query(tx).
Pk(oldBillId).
Set("amount", amount).
Set("canPay", canPay).
Set("description", description).
UpdateQuickly()
return oldBillId, err
}
// 生成账单号码
code, err := this.GenerateBillCode(tx)
if err != nil {
return 0, err
}
var op = NewUserBillOperator()
op.UserId = userId
op.Type = billType
op.PricePeriod = pricePeriod
op.Description = description
op.Amount = amount
op.Month = month
op.DayFrom = dayFrom
op.DayTo = dayTo
op.Code = code
op.IsPaid = amount == 0
op.CanPay = canPay
op.CreatedDay = timeutil.Format("Ymd")
op.State = UserStateEnabled
billId, err = this.SaveInt64(tx, op)
if err != nil {
return
}
// 发送邮件通知
if canPay {
// 不提示错误
if len(dayFrom) == 8 && len(dayTo) == 8 {
_ = SharedUserDAO.NotifyEmail(tx, userId, userconfigs.UserNotificationTypeBill, "[${product.name}]账单出账通知", "已为你生成"+dayFrom[:4]+"-"+dayFrom[4:6]+"-"+dayFrom[6:]+"到"+dayTo[:4]+"-"+dayTo[4:6]+"-"+dayTo[6:]+"账单,请登录查询详情 <a href=\"${url.home}/finance/bills\" target=\"_blank\">${url.home}/finance/bills</a>。")
}
}
return
}
// FindUserLatestTrafficAndBandwidthBill 查找最近一次账单
func (this *UserBillDAO) FindUserLatestTrafficAndBandwidthBill(tx *dbs.Tx, userId int64) (*UserBill, error) {
one, err := this.Query(tx).
State(UserBillStateEnabled).
Attr("userId", userId).
Attr("type", []string{BillTypeTraffic, BillTypeBandwidth}).
Result("id", "type", "month", "pricePeriod", "dayFrom", "dayTo", "canPay").
DescPk().
Find()
if err != nil || one == nil {
return nil, err
}
var userBill = one.(*UserBill)
if len(userBill.DayFrom) > 0 {
userBill.DayFrom, err = utils.FixMonthMaxDay(userBill.DayFrom)
if err != nil {
return nil, err
}
}
if len(userBill.DayTo) > 0 {
userBill.DayTo, err = utils.FixMonthMaxDay(userBill.DayTo)
if err != nil {
return nil, err
}
}
return userBill, nil
}
// FindUserBill 查找单个账单
func (this *UserBillDAO) FindUserBill(tx *dbs.Tx, billId int64) (*UserBill, error) {
one, err := this.Query(tx).
State(UserBillStateEnabled).
Pk(billId).
Find()
if err != nil || one == nil {
return nil, err
}
return one.(*UserBill), nil
}
// FindUserBillWithCode 根据账单号查找单个账单
func (this *UserBillDAO) FindUserBillWithCode(tx *dbs.Tx, code string) (*UserBill, error) {
if len(code) == 0 {
return nil, nil
}
one, err := this.Query(tx).
State(UserBillStateEnabled).
Attr("code", code).
Find()
if err != nil || one == nil {
return nil, err
}
return one.(*UserBill), nil
}
// CountAllUserBills 计算账单数量
func (this *UserBillDAO) CountAllUserBills(tx *dbs.Tx, isPaid int32, userId int64, month string, trafficRelated bool, minDailyBillDays int32, minMonthlyBillDays int32) (int64, error) {
var query = this.Query(tx)
query.State(UserBillStateEnabled)
if isPaid == 0 {
query.Attr("isPaid", 0)
} else if isPaid > 0 {
query.Attr("isPaid", 1)
}
if userId > 0 {
query.Attr("userId", userId)
}
if len(month) > 0 {
query.Attr("month", month)
}
if trafficRelated {
query.Attr("type", []string{BillTypeBandwidth, BillTypeTraffic, BillTypeTrafficAndBandwidth})
}
if minDailyBillDays > 0 || minMonthlyBillDays > 0 {
query.Attr("canPay", true)
if minDailyBillDays > 0 && minMonthlyBillDays > 0 {
var dailyTillDay = timeutil.Format("Ymd", time.Now().AddDate(0, 0, -int(minDailyBillDays)))
var monthTillDay = timeutil.Format("Ymd", time.Now().AddDate(0, 0, -int(minMonthlyBillDays)))
query.Where("((pricePeriod='daily' AND createdDay<:dailyTillDay) OR (pricePeriod='monthly' AND createdDay<:monthlyTillDay))")
query.Param("dailyTillDay", dailyTillDay)
query.Param("monthlyTillDay", monthTillDay)
} else if minDailyBillDays > 0 {
var dailyTillDay = timeutil.Format("Ymd", time.Now().AddDate(0, 0, -int(minDailyBillDays)))
query.Where("(pricePeriod='daily' AND createdDay<:dailyTillDay)")
query.Param("dailyTillDay", dailyTillDay)
} else if minMonthlyBillDays > 0 {
var monthTillDay = timeutil.Format("Ymd", time.Now().AddDate(0, 0, -int(minMonthlyBillDays)))
query.Where("(pricePeriod='monthly' AND createdDay<:monthlyTillDay)")
query.Param("monthlyTillDay", monthTillDay)
}
}
return query.Count()
}
// ListUserBills 列出单页账单
func (this *UserBillDAO) ListUserBills(tx *dbs.Tx, isPaid int32, userId int64, month string, offset, size int64) (result []*UserBill, err error) {
var query = this.Query(tx)
if isPaid == 0 {
query.Attr("isPaid", 0)
} else if isPaid > 0 {
query.Attr("isPaid", 1)
}
if userId > 0 {
query.Attr("userId", userId)
}
if len(month) > 0 {
query.Attr("month", month)
}
_, err = query.
State(UserBillStateEnabled).
Offset(offset).
Limit(size).
Slice(&result).
DescPk().
FindAll()
return
}
// FindUnpaidBills 查找未支付订单
func (this *UserBillDAO) FindUnpaidBills(tx *dbs.Tx, size int64) (result []*UserBill, err error) {
if size <= 0 {
size = 10000
}
_, err = this.Query(tx).
State(UserBillStateEnabled).
Attr("isPaid", false).
Where("((pricePeriod='monthly' AND `month`<:month) OR (pricePeriod='daily' AND dayTo<:day))").
Param("month", timeutil.Format("Ym")).
Param("day", timeutil.Format("Ymd")).
Where("(createdDay IS NULL OR createdDay>:minDay)").
Param("minDay", timeutil.Format("Ymd", time.Now().AddDate(0, -3, 0))). // 只读取最近 N 个月的账单,防止无限期等待
Limit(size).
DescPk().
Slice(&result).
FindAll()
return
}
// ExistBill 检查是否有当月账单
func (this *UserBillDAO) ExistBill(tx *dbs.Tx, userId int64, billType BillType, month string) (bool, error) {
return this.Query(tx).
Attr("userId", userId).
Attr("month", month).
Attr("type", billType).
State(UserBillStateEnabled).
Exist()
}
// UpdateUserBillIsPaid 设置账单已支付
func (this *UserBillDAO) UpdateUserBillIsPaid(tx *dbs.Tx, billId int64, isPaid bool) error {
err := this.Query(tx).
Pk(billId).
Set("isPaid", isPaid).
UpdateQuickly()
if err != nil {
return err
}
// 更新用户状态
userId, err := this.Query(tx).
Pk(billId).
Result("userId").
FindInt64Col(0)
if err != nil {
return err
}
if userId > 0 {
_, err = SharedUserDAO.RenewUserServersState(tx, userId)
return err
}
return nil
}
// BillTypeName 获取账单类型名称
func (this *UserBillDAO) BillTypeName(billType BillType) string {
switch billType {
case BillTypeTraffic:
return "流量计费"
case BillTypeBandwidth:
return "带宽计费"
case BillTypeTrafficAndBandwidth:
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)
}
// CheckUserBill 检查用户账单
func (this *UserBillDAO) CheckUserBill(tx *dbs.Tx, userId int64, billId int64) error {
if userId <= 0 || billId <= 0 {
return ErrNotFound
}
exists, err := this.Query(tx).
Pk(billId).
State(UserBillStateEnabled).
Attr("userId", userId).
Exist()
if err != nil {
return err
}
if !exists {
return ErrNotFound
}
return nil
}
// SumUnpaidUserBill 计算某个用户未支付总额
func (this *UserBillDAO) SumUnpaidUserBill(tx *dbs.Tx, userId int64) (float64, error) {
sum, err := this.Query(tx).
State(UserBillStateEnabled).
Attr("userId", userId).
Attr("isPaid", 0).
Sum("amount", 0)
if err != nil {
return 0, err
}
return sum, nil
}