// 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, ®ionPrice{ 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, ®ionPrice{ 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, ®ionPrice{ 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:]+"账单,请登录查询详情 ${url.home}/finance/bills。") } } 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 }