[账单]显示账单、手动生成账单

This commit is contained in:
GoEdgeLab
2020-12-11 21:39:10 +08:00
parent d2f72d7e87
commit 641df4ccc5
13 changed files with 429 additions and 14 deletions

View File

@@ -13,6 +13,7 @@ type Log struct {
Ip string `field:"ip"` // IP地址
Type string `field:"type"` // 类型admin, user
Day string `field:"day"` // 日期
BillId uint32 `field:"billId"` // 账单ID
}
type LogOperator struct {
@@ -27,6 +28,7 @@ type LogOperator struct {
Ip interface{} // IP地址
Type interface{} // 类型admin, user
Day interface{} // 日期
BillId interface{} // 账单ID
}
func NewLogOperator() *LogOperator {

View File

@@ -10,6 +10,8 @@ import (
const (
NodePriceItemStateEnabled = 1 // 已启用
NodePriceItemStateDisabled = 0 // 已禁用
NodePriceTypeTraffic = "traffic" // 价格类型之流量
)
type NodePriceItemDAO dbs.DAO
@@ -118,3 +120,15 @@ func (this *NodePriceItemDAO) FindAllEnabledAndOnRegionPrices(priceType string)
FindAll()
return
}
// 根据字节查找付费项目
func (this *NodePriceItemDAO) SearchItemsWithBytes(items []*NodePriceItem, bytes int64) int64 {
bytes *= 8
for _, item := range items {
if bytes >= int64(item.BitsFrom) && (bytes < int64(item.BitsTo) || item.BitsTo == 0) {
return int64(item.Id)
}
}
return 0
}

View File

@@ -108,6 +108,18 @@ func (this *NodeRegionDAO) FindAllEnabledRegions() (result []*NodeRegion, err er
return
}
// 列出所有价格
func (this *NodeRegionDAO) FindAllEnabledRegionPrices() (result []*NodeRegion, err error) {
_, err = this.Query().
State(NodeRegionStateEnabled).
Desc("order").
AscPk().
Result("id", "prices").
Slice(&result).
FindAll()
return
}
// 列出所有启用的区域
func (this *NodeRegionDAO) FindAllEnabledAndOnRegions() (result []*NodeRegion, err error) {
_, err = this.Query().

View File

@@ -55,3 +55,14 @@ func (this *ServerDailyStatDAO) SaveStats(stats []*pb.ServerDailyStat) error {
}
return nil
}
// 根据用户计算某月合计
// month 格式为YYYYMM
func (this *ServerDailyStatDAO) SumUserMonthly(userId int64, regionId int64, month string) (int64, error) {
return this.Query().
Attr("regionId", regionId).
Between("day", month+"01", month+"32").
Where("serverId IN (SELECT id FROM "+SharedServerDAO.Table+" WHERE userId=:userId)").
Param("userId", userId).
SumInt64("bytes", 0)
}

View File

@@ -3,6 +3,8 @@ package models
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/dbs"
timeutil "github.com/iwind/TeaGo/utils/time"
"testing"
)
@@ -37,3 +39,12 @@ func TestServerDailyStatDAO_SaveStats2(t *testing.T) {
}
t.Log("ok")
}
func TestServerDailyStatDAO_SumUserMonthly(t *testing.T) {
dbs.NotifyReady()
bytes, err := NewServerDailyStatDAO().SumUserMonthly(1, 1, timeutil.Format("Ym"))
if err != nil {
t.Fatal(err)
}
t.Log("bytes:", bytes)
}

View File

@@ -2,23 +2,25 @@ package models
// 计费流量统计
type ServerDailyStat struct {
Id uint64 `field:"id"` // ID
ServerId uint32 `field:"serverId"` // 服务ID
RegionId uint32 `field:"regionId"` // 区域ID
Bytes uint64 `field:"bytes"` // 流量
Day string `field:"day"` // 日期YYYYMMDD
TimeFrom string `field:"timeFrom"` // 开始时间HHMMSS
TimeTo string `field:"timeTo"` // 结束时间
Id uint64 `field:"id"` // ID
ServerId uint32 `field:"serverId"` // 服务ID
RegionId uint32 `field:"regionId"` // 区域ID
Bytes uint64 `field:"bytes"` // 流量
Day string `field:"day"` // 日期YYYYMMDD
TimeFrom string `field:"timeFrom"` // 开始时间HHMMSS
TimeTo string `field:"timeTo"` // 结束时间
IsCharged uint8 `field:"isCharged"` // 是否已计算费用
}
type ServerDailyStatOperator struct {
Id interface{} // ID
ServerId interface{} // 服务ID
RegionId interface{} // 区域ID
Bytes interface{} // 流量
Day interface{} // 日期YYYYMMDD
TimeFrom interface{} // 开始时间HHMMSS
TimeTo interface{} // 结束时间
Id interface{} // ID
ServerId interface{} // 服务ID
RegionId interface{} // 区域ID
Bytes interface{} // 流量
Day interface{} // 日期YYYYMMDD
TimeFrom interface{} // 开始时间HHMMSS
TimeTo interface{} // 结束时间
IsCharged interface{} // 是否已计算费用
}
func NewServerDailyStatOperator() *ServerDailyStatOperator {

View File

@@ -0,0 +1,206 @@
package models
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils"
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs"
)
type BillType = string
const (
BillTypeTraffic BillType = "traffic" // 按流量计费
)
type UserBillDAO dbs.DAO
func NewUserBillDAO() *UserBillDAO {
return dbs.NewDAO(&UserBillDAO{
DAOObject: dbs.DAOObject{
DB: Tea.Env,
Table: "edgeUserBills",
Model: new(UserBill),
PkName: "id",
},
}).(*UserBillDAO)
}
var SharedUserBillDAO *UserBillDAO
func init() {
dbs.OnReady(func() {
SharedUserBillDAO = NewUserBillDAO()
})
}
// 计算账单数量
func (this *UserBillDAO) CountAllUserBills(isPaid int32, userId int64, month string) (int64, error) {
query := this.Query()
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)
}
return query.Count()
}
// 列出单页账单
func (this *UserBillDAO) ListUserBills(isPaid int32, userId int64, month string, offset, size int64) (result []*UserBill, err error) {
query := this.Query()
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.
Offset(offset).
Limit(size).
Slice(&result).
DescPk().
FindAll()
return
}
// 创建账单
func (this *UserBillDAO) CreateBill(userId int64, billType BillType, description string, amount float32, month string) (int64, error) {
op := NewUserBillOperator()
op.UserId = userId
op.Type = billType
op.Description = description
op.Amount = amount
op.Month = month
op.IsPaid = false
return this.SaveInt64(op)
}
// 检查是否有当月账单
func (this *UserBillDAO) ExistBill(userId int64, billType BillType, month string) (bool, error) {
return this.Query().
Attr("userId", userId).
Attr("month", month).
Attr("type", billType).
Exist()
}
// 生成账单
// month 格式YYYYMM
func (this *UserBillDAO) GenerateBills(month string) error {
// 用户
offset := int64(0)
size := int64(100) // 每次只查询N次防止由于执行时间过长而锁表
for {
userIds, err := SharedUserDAO.ListEnabledUserIds(offset, size)
if err != nil {
return err
}
offset += size
if len(userIds) == 0 {
break
}
for _, userId := range userIds {
// CDN流量账单
err := this.generateTrafficBill(userId, month)
if err != nil {
return err
}
}
}
return nil
}
// 生成CDN流量账单
// month 格式YYYYMM
func (this *UserBillDAO) generateTrafficBill(userId int64, month string) error {
// 检查是否已经有账单了
b, err := this.ExistBill(userId, BillTypeTraffic, month)
if err != nil {
return err
}
if b {
return nil
}
// TODO 优化使用缓存
regions, err := SharedNodeRegionDAO.FindAllEnabledRegionPrices()
if err != nil {
return err
}
if len(regions) == 0 {
return nil
}
priceItems, err := SharedNodePriceItemDAO.FindAllEnabledRegionPrices(NodePriceTypeTraffic)
if err != nil {
return err
}
if len(priceItems) == 0 {
return nil
}
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.SumUserMonthly(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
}
if cost == 0 {
return nil
}
// 创建账单
_, err = this.CreateBill(userId, BillTypeTraffic, "按流量计费", cost, month)
return err
}
// 获取账单类型名称
func (this *UserBillDAO) BillTypeName(billType BillType) string {
switch billType {
case BillTypeTraffic:
return "流量"
}
return ""
}

View File

@@ -0,0 +1,18 @@
package models
import (
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/dbs"
timeutil "github.com/iwind/TeaGo/utils/time"
"testing"
)
func TestUserBillDAO_GenerateBills(t *testing.T) {
dbs.NotifyReady()
err := SharedUserBillDAO.GenerateBills(timeutil.Format("Ym"))
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}

View File

@@ -0,0 +1,30 @@
package models
// 用户账单
type UserBill struct {
Id uint64 `field:"id"` // ID
UserId uint32 `field:"userId"` // 用户ID
Type string `field:"type"` // 消费类型
Description string `field:"description"` // 描述
Amount float64 `field:"amount"` // 消费数额
Month string `field:"month"` // 帐期YYYYMM
IsPaid uint8 `field:"isPaid"` // 是否已支付
PaidAt uint64 `field:"paidAt"` // 支付时间
CreatedAt uint64 `field:"createdAt"` // 创建时间
}
type UserBillOperator struct {
Id interface{} // ID
UserId interface{} // 用户ID
Type interface{} // 消费类型
Description interface{} // 描述
Amount interface{} // 消费数额
Month interface{} // 帐期YYYYMM
IsPaid interface{} // 是否已支付
PaidAt interface{} // 支付时间
CreatedAt interface{} // 创建时间
}
func NewUserBillOperator() *UserBillOperator {
return &UserBillOperator{}
}

View File

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

View File

@@ -160,3 +160,22 @@ func (this *UserDAO) ExistUser(userId int64, username string) (bool, error) {
Neq("id", userId).
Exist()
}
// 列出单页的用户ID
func (this *UserDAO) ListEnabledUserIds(offset, size int64) ([]int64, error) {
ones, _, err := this.Query().
ResultPk().
State(UserStateEnabled).
Offset(offset).
Limit(size).
AscPk().
FindOnes()
if err != nil {
return nil, err
}
result := []int64{}
for _, one := range ones {
result = append(result, one.GetInt64("id"))
}
return result, nil
}

View File

@@ -204,6 +204,7 @@ func (this *APINode) listenRPC(listener net.Listener, tlsConfig *tls.Config) err
pb.RegisterACMEAuthenticationServiceServer(rpcServer, &services.ACMEAuthenticationService{})
pb.RegisterUserServiceServer(rpcServer, &services.UserService{})
pb.RegisterServerDailyStatServiceServer(rpcServer, &services.ServerDailyStatService{})
pb.RegisterUserBillServiceServer(rpcServer, &services.UserBillService{})
err := rpcServer.Serve(listener)
if err != nil {
return errors.New("[API_NODE]start rpc failed: " + err.Error())

View File

@@ -0,0 +1,88 @@
package services
import (
"context"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
timeutil "github.com/iwind/TeaGo/utils/time"
"regexp"
)
// 账单相关服务
type UserBillService struct {
BaseService
}
// 手工生成订单
func (this *UserBillService) GenerateAllUserBills(ctx context.Context, req *pb.GenerateAllUserBillsRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx, 0)
if err != nil {
return nil, err
}
// 校验Month
if !regexp.MustCompile(`^\d{6}$`).MatchString(req.Month) {
return nil, errors.New("invalid month '" + req.Month + "'")
}
if req.Month >= timeutil.Format("Ym") {
return nil, errors.New("invalid month '" + req.Month + "'")
}
err = models.SharedUserBillDAO.GenerateBills(req.Month)
if err != nil {
return nil, err
}
return this.Success()
}
// 计算所有账单数量
func (this *UserBillService) CountAllUserBills(ctx context.Context, req *pb.CountAllUserBillsRequest) (*pb.RPCCountResponse, error) {
_, err := this.ValidateAdmin(ctx, 0)
if err != nil {
return nil, err
}
count, err := models.SharedUserBillDAO.CountAllUserBills(req.PaidFlag, req.UserId, req.Month)
if err != nil {
return nil, err
}
return this.SuccessCount(count)
}
// 列出单页账单
func (this *UserBillService) ListUserBills(ctx context.Context, req *pb.ListUserBillsRequest) (*pb.ListUserBillsResponse, error) {
_, err := this.ValidateAdmin(ctx, 0)
if err != nil {
return nil, err
}
bills, err := models.SharedUserBillDAO.ListUserBills(req.PaidFlag, req.UserId, req.Month, req.Offset, req.Size)
if err != nil {
return nil, err
}
result := []*pb.UserBill{}
for _, bill := range bills {
userFullname, err := models.SharedUserDAO.FindUserFullname(int64(bill.UserId))
if err != nil {
return nil, err
}
result = append(result, &pb.UserBill{
Id: int64(bill.Id),
User: &pb.User{
Id: int64(bill.UserId),
Fullname: userFullname,
},
Type: bill.Type,
TypeName: models.SharedUserBillDAO.BillTypeName(bill.Type),
Description: bill.Description,
Amount: float32(bill.Amount),
Month: bill.Month,
IsPaid: bill.IsPaid == 1,
PaidAt: int64(bill.PaidAt),
})
}
return &pb.ListUserBillsResponse{UserBills: result}, nil
}