forked from all_user/stat_center
Initial commit
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package statutils
|
||||
|
||||
type DeviceName string
|
||||
|
||||
const (
|
||||
DeviceNameAndroid DeviceName = "android"
|
||||
DeviceNameIos DeviceName = "ios"
|
||||
DeviceNamePc DeviceName = "pc"
|
||||
DeviceNameLight DeviceName = "light"
|
||||
DeviceNameH5 DeviceName = "h5"
|
||||
DeviceNameH5Android DeviceName = "h5_android"
|
||||
DeviceNameH5Ios DeviceName = "h5_ios"
|
||||
DeviceNameH5Pc DeviceName = "h5_pc"
|
||||
)
|
||||
|
||||
func (d DeviceName) String() string {
|
||||
return string(d)
|
||||
}
|
||||
|
||||
const (
|
||||
StatProjectRecharge = "recharge"
|
||||
StatProjectRechargeOld = "recharge_old"
|
||||
StatProjectRechargeNew = "recharge_new"
|
||||
StatProjectRechargeGold = "recharge_gold"
|
||||
StatProjectRechargeVip = "recharge_vip"
|
||||
StatProjectOrder = "order"
|
||||
StatProjectRegister = "register"
|
||||
StatProjectVisit = "visit"
|
||||
StatProjectGame = "game"
|
||||
)
|
||||
|
||||
var StatProjectNameDict = map[string]string{
|
||||
StatProjectRecharge: "充值统计",
|
||||
StatProjectRechargeOld: "老用户充",
|
||||
StatProjectRechargeNew: "新用户充",
|
||||
StatProjectRechargeGold: "金币充值",
|
||||
StatProjectRechargeVip: "会员充值",
|
||||
StatProjectOrder: "订单统计",
|
||||
StatProjectRegister: "注册用户",
|
||||
StatProjectVisit: "访问统计",
|
||||
StatProjectGame: "游戏统计",
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package statutils
|
||||
|
||||
import (
|
||||
"stat/common/utils"
|
||||
"stat/models/syncrecord"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SyncItem struct {
|
||||
AppID int32 // 应用ID
|
||||
JobName string // 任务名称
|
||||
JobDesc string // 任务描述
|
||||
today time.Time // 内部日期
|
||||
todayCount int64 // 今日拉取条数
|
||||
CurrentCount int64 // 当前拉取条数
|
||||
RequestBegin time.Time // 请求开始时间
|
||||
RequestEnd time.Time // 请求结束时间
|
||||
ProcessBegin time.Time // 处理开始时间
|
||||
ProcessEnd time.Time // 处理结束时间
|
||||
jobBegin time.Time // 任务开始时间
|
||||
jobEnd time.Time // 任务结束时间
|
||||
DataLatest time.Time // 拉到的数据的最后时间
|
||||
Error error // 失败错误
|
||||
load bool // 是否载入redis数据
|
||||
}
|
||||
|
||||
func (s *SyncItem) IncCount(value int64) {
|
||||
s.todayCount += value
|
||||
s.CurrentCount += value
|
||||
}
|
||||
|
||||
func (s *SyncItem) TodayCount() int64 {
|
||||
return s.todayCount
|
||||
}
|
||||
|
||||
func (s *SyncItem) JobBegin() time.Time {
|
||||
return s.jobBegin
|
||||
}
|
||||
|
||||
func (s *SyncItem) SetJobBegin(start time.Time) {
|
||||
today := utils.BeginningOfDay(start)
|
||||
s.jobBegin = start
|
||||
if !s.load {
|
||||
s.load = true
|
||||
s.today = today
|
||||
GetTodayCount(s)
|
||||
}
|
||||
if !s.today.Equal(today) {
|
||||
s.today = today
|
||||
s.todayCount = 0
|
||||
s.CurrentCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SyncItem) JobEnd() time.Time {
|
||||
return s.jobEnd
|
||||
}
|
||||
|
||||
func (s *SyncItem) SetJobEnd(end time.Time) {
|
||||
s.jobEnd = end
|
||||
SetTodayCount(s)
|
||||
}
|
||||
|
||||
type SyncMonitor struct {
|
||||
mu sync.Mutex
|
||||
dict map[int32]map[string]*SyncItem
|
||||
}
|
||||
|
||||
func (s *SyncMonitor) SetData(appId int32, job string, setFunc func(item *SyncItem)) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
jobs := s.dict[appId]
|
||||
if jobs == nil {
|
||||
jobs = map[string]*SyncItem{}
|
||||
s.dict[appId] = jobs
|
||||
}
|
||||
item := jobs[job]
|
||||
if item == nil {
|
||||
item = &SyncItem{AppID: appId, JobName: job, JobDesc: syncrecord.SyncJobDescribe[job]}
|
||||
jobs[job] = item
|
||||
}
|
||||
item.AppID = appId
|
||||
item.JobName = job
|
||||
|
||||
setFunc(item)
|
||||
|
||||
// protect some item attr
|
||||
item.AppID = appId
|
||||
item.JobName = job
|
||||
item.JobDesc = syncrecord.SyncJobDescribe[job]
|
||||
}
|
||||
|
||||
func (s *SyncMonitor) Traverse(fn func(item *SyncItem)) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for _, jobs := range s.dict {
|
||||
for _, item := range jobs {
|
||||
fn(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
GlobalSyncMonitor = &SyncMonitor{
|
||||
dict: map[int32]map[string]*SyncItem{},
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
package statutils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"stat/common/redis"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var hash *redis.Hash
|
||||
|
||||
func getHash(key string) (*redis.Hash, error) {
|
||||
if hash != nil {
|
||||
if hash.Key() == key {
|
||||
return hash, nil
|
||||
} else {
|
||||
client := redis.StatRds.Client()
|
||||
if client != nil {
|
||||
hash = redis.NewHash(client, key)
|
||||
hash.Expire(48 * time.Hour)
|
||||
return hash, nil
|
||||
}
|
||||
return nil, errors.New("can not get redis client")
|
||||
}
|
||||
} else {
|
||||
client := redis.StatRds.Client()
|
||||
if client != nil {
|
||||
hash = redis.NewHash(client, key)
|
||||
hash.Expire(48 * time.Hour)
|
||||
return hash, nil
|
||||
}
|
||||
return nil, errors.New("can not get redis client")
|
||||
}
|
||||
}
|
||||
|
||||
func SetTodayCount(item *SyncItem) error {
|
||||
hashKey := fmt.Sprintf("sync-job-data-count-%s", item.JobBegin().Local().Format("2006-02-01"))
|
||||
jobKey := fmt.Sprintf("app:%d-job:%s", item.AppID, item.JobName)
|
||||
todayHash, err := getHash(hashKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = todayHash.Set(jobKey, item.todayCount)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetTodayCount(item *SyncItem) error {
|
||||
hashKey := fmt.Sprintf("sync-job-data-count-%s", item.JobBegin().Local().Format("2006-02-01"))
|
||||
jobKey := fmt.Sprintf("app:%d-job:%s", item.AppID, item.JobName)
|
||||
todayHash, err := getHash(hashKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
countS, err := todayHash.Get(jobKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
countI, err := strconv.ParseInt(countS, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.todayCount += countI
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package statutils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sort"
|
||||
"stat/common/utils"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var ServerMon = Monitor{}
|
||||
|
||||
type ErrorRecord struct {
|
||||
Error error `json:"error"`
|
||||
Occur time.Time `json:"occur"`
|
||||
}
|
||||
|
||||
type JobItem struct {
|
||||
Name string `json:"name"`
|
||||
Call string `json:"call"`
|
||||
Count int64 `json:"count"`
|
||||
Start time.Time `json:"start"`
|
||||
Stop time.Time `json:"stop"`
|
||||
Avg time.Duration `json:"avg"`
|
||||
Gap time.Duration `json:"gap"`
|
||||
Error error `json:"error"`
|
||||
SuccCount int64 `json:"succ_count"`
|
||||
ErrorCount int64 `json:"error_count"`
|
||||
Errors []*ErrorRecord `json:"errors"`
|
||||
}
|
||||
|
||||
func SpendTimeIcon(d time.Duration) string {
|
||||
if d < 5*time.Second {
|
||||
return "\U0001F7E2\U0001F7E2\U0001F7E2"
|
||||
} else if d < 10*time.Second {
|
||||
return "\U0001F7E2\U0001F7E2\U0001F7E0"
|
||||
} else if d < 30*time.Second {
|
||||
return "\U0001F7E2\U0001F7E0\U0001F7E0"
|
||||
} else if d < 80*time.Second {
|
||||
return "\U0001F7E0\U0001F7E0\U0001F7E0"
|
||||
} else if d < 220*time.Second {
|
||||
return "\U0001F7E0\U0001F7E0🔴"
|
||||
} else if d < 600*time.Second {
|
||||
return "\U0001F7E0🔴🔴"
|
||||
} else if d < 1640*time.Second {
|
||||
return "🔴🔴🔴"
|
||||
} else {
|
||||
return "🆘🆘🆘"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JobItem) Status() string {
|
||||
if s.Count > s.SuccCount+s.ErrorCount {
|
||||
return "💛执行中"
|
||||
} else {
|
||||
if s.Error != nil {
|
||||
return "💔执行出错"
|
||||
} else {
|
||||
return "💚执行成功"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JobItem) Display() string {
|
||||
text := fmt.Sprintf("【函数】%s\n", s.Name)
|
||||
text += fmt.Sprintf("【调用】%s\n", s.Call)
|
||||
text += fmt.Sprintf("【进入】%d次\n", s.Count)
|
||||
text += fmt.Sprintf("【状态】%s\n", s.Status())
|
||||
text += fmt.Sprintf("【开始】%s\n", s.Start.Format("15:04:05"))
|
||||
text += fmt.Sprintf("【结束】%s\n", s.Stop.Format("15:04:05"))
|
||||
text += fmt.Sprintf("【用时】%s\n", s.Stop.Sub(s.Start))
|
||||
text += fmt.Sprintf("【速度】%s\n", SpendTimeIcon(s.Stop.Sub(s.Start)))
|
||||
text += fmt.Sprintf("【平均】%s\n", s.Avg)
|
||||
text += fmt.Sprintf("【间隔】%s\n", s.Gap)
|
||||
text += fmt.Sprintf("【成功】%d次\n", s.SuccCount)
|
||||
text += fmt.Sprintf("【出错】%d次\n", s.ErrorCount)
|
||||
if len(s.Errors) > 0 {
|
||||
text += fmt.Sprintf("【最前出错信息】🔻%s\n", s.Errors[0].Error.Error())
|
||||
}
|
||||
if len(s.Errors) > 1 {
|
||||
text += fmt.Sprintf("【最后出错信息】🔺%s\n", s.Errors[len(s.Errors)-1].Error.Error())
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func newJob(name string, call string) *JobItem {
|
||||
now := time.Now()
|
||||
return &JobItem{
|
||||
Name: name,
|
||||
Call: call,
|
||||
Count: 1,
|
||||
Start: now,
|
||||
Stop: now,
|
||||
Errors: make([]*ErrorRecord, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JobItem) Begin() {
|
||||
now := time.Now()
|
||||
if s.Count > 0 {
|
||||
s.Gap = now.Sub(s.Start)
|
||||
}
|
||||
s.Start = now
|
||||
s.Count++
|
||||
}
|
||||
|
||||
func (s *JobItem) End(err error) {
|
||||
now := time.Now()
|
||||
s.Error = err
|
||||
s.Stop = now
|
||||
if err != nil {
|
||||
s.ErrorCount++
|
||||
s.Errors = append(s.Errors, &ErrorRecord{
|
||||
Error: err,
|
||||
Occur: now,
|
||||
})
|
||||
} else {
|
||||
s.SuccCount++
|
||||
s.Avg = time.Duration(int64(s.Avg+(s.Stop.Sub(s.Start))) / s.SuccCount)
|
||||
}
|
||||
}
|
||||
|
||||
type Monitor struct {
|
||||
lock sync.Mutex
|
||||
jobs map[string]*JobItem
|
||||
}
|
||||
|
||||
func (s *Monitor) call(callFunc func() error, funcName string, callName string) error {
|
||||
func() {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
if s.jobs == nil {
|
||||
s.jobs = make(map[string]*JobItem)
|
||||
}
|
||||
job, ok := s.jobs[funcName]
|
||||
if !ok {
|
||||
job = newJob(funcName, callName)
|
||||
s.jobs[funcName] = job
|
||||
} else {
|
||||
job.Begin()
|
||||
}
|
||||
}()
|
||||
err := callFunc()
|
||||
func() {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
job := s.jobs[funcName]
|
||||
if job != nil {
|
||||
job.End(err)
|
||||
}
|
||||
}()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Monitor) Run(fn func() error) error {
|
||||
defer utils.RecoverError(nil)
|
||||
callName := GetCallName(2)
|
||||
funcName := GetFuncName(fn)
|
||||
call := func() (err error) {
|
||||
defer utils.RecoverError(func(err1 error) {
|
||||
err = err1
|
||||
})
|
||||
err = fn()
|
||||
return err
|
||||
}
|
||||
return s.call(call, funcName, callName)
|
||||
}
|
||||
|
||||
func (s *Monitor) RunWithName(fn func() error, funcName string) error {
|
||||
defer utils.RecoverError(nil)
|
||||
callName := GetCallName(2)
|
||||
call := func() (err error) {
|
||||
defer utils.RecoverError(func(err1 error) {
|
||||
err = err1
|
||||
})
|
||||
err = fn()
|
||||
return err
|
||||
}
|
||||
return s.call(call, funcName, callName)
|
||||
}
|
||||
|
||||
func (s *Monitor) Run1(fn func(v time.Time) error, param time.Time) error {
|
||||
defer utils.RecoverError(nil)
|
||||
callName := GetCallName(2)
|
||||
funcName := GetFuncName(fn)
|
||||
call := func() (err error) {
|
||||
defer utils.RecoverError(func(err1 error) {
|
||||
err = err1
|
||||
})
|
||||
err = fn(param)
|
||||
return err
|
||||
}
|
||||
return s.call(call, funcName, callName)
|
||||
}
|
||||
|
||||
func (s *Monitor) ToJson() string {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
data, _ := json.Marshal(s.jobs)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (s *Monitor) Display() string {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
list := make(JobItemList, 0)
|
||||
for _, job := range s.jobs {
|
||||
list = append(list, job)
|
||||
}
|
||||
sort.Sort(list)
|
||||
|
||||
text := fmt.Sprintf("后台自动任务(%d个):\n", len(s.jobs))
|
||||
for _, job := range list {
|
||||
text += job.Display()
|
||||
text += "***** *****\n"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func GetCallName(skip int) string {
|
||||
pc, _, _, _ := runtime.Caller(skip)
|
||||
return runtime.FuncForPC(pc).Name()
|
||||
}
|
||||
|
||||
func GetFuncName(fn interface{}) string {
|
||||
funcPC := func(fn interface{}) uintptr {
|
||||
ptr := (*[2]uintptr)(unsafe.Pointer(&fn))[1]
|
||||
return **((**uintptr)(unsafe.Pointer(&ptr)))
|
||||
}
|
||||
pc := funcPC(fn)
|
||||
return runtime.FuncForPC(pc).Name()
|
||||
}
|
||||
|
||||
type JobItemList []*JobItem
|
||||
|
||||
func (a JobItemList) Len() int { return len(a) }
|
||||
func (a JobItemList) Less(i, j int) bool {
|
||||
return a[i].Name < a[j].Name
|
||||
}
|
||||
func (a JobItemList) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
@@ -0,0 +1,20 @@
|
||||
package statutils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func AAA() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TTT(fn func() error) string {
|
||||
return GetFuncName(fn)
|
||||
}
|
||||
|
||||
func TestGetFuncName(t *testing.T) {
|
||||
|
||||
s := TTT(AAA)
|
||||
t.Log("函数名:" + s)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,728 @@
|
||||
package statutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
type Value struct {
|
||||
Value int64 `json:"value" bson:"value"` // Value
|
||||
Count int64 `json:"count" bson:"count"` // Count
|
||||
}
|
||||
|
||||
func (s *Value) CountRef() *int64 {
|
||||
return &s.Count
|
||||
}
|
||||
|
||||
func (s *Value) ValueRef() *int64 {
|
||||
return &s.Value
|
||||
}
|
||||
|
||||
func (s *Value) Add(value int64) {
|
||||
s.Count++
|
||||
s.Value += value
|
||||
}
|
||||
|
||||
func (s *Value) Sub(d *Value) *Value {
|
||||
ret := &Value{
|
||||
Count: s.Count - d.Count,
|
||||
Value: s.Value - d.Value,
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Value) Plus(d *Value) *Value {
|
||||
ret := &Value{
|
||||
Count: s.Count + d.Count,
|
||||
Value: s.Value + d.Value,
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Value) ACC(d *Value) {
|
||||
s.Count += d.Count
|
||||
s.Value += d.Value
|
||||
}
|
||||
|
||||
func (s *Value) Percentage(den *Value) *Value {
|
||||
ret := &Value{
|
||||
Count: percentageDiv(s.Count, den.Count),
|
||||
Value: percentageDiv(s.Value, den.Value),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Value) Permil(den *Value) *Value {
|
||||
ret := &Value{
|
||||
Count: permilDiv(s.Count, den.Count),
|
||||
Value: permilDiv(s.Value, den.Value),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Value) Mul(v int64) *Value {
|
||||
ret := &Value{
|
||||
Count: s.Count * v,
|
||||
Value: s.Value * v,
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type Direct struct {
|
||||
Total Value `json:"total" bson:"total"` // Total
|
||||
Natural Value `json:"natural" bson:"natural"` // Natural
|
||||
}
|
||||
|
||||
func (s *Direct) CountRef() *int64 {
|
||||
return &s.Total.Count
|
||||
}
|
||||
|
||||
func (s *Direct) Count() int64 {
|
||||
return s.Total.Count
|
||||
}
|
||||
|
||||
func (s *Direct) ValueRef() *int64 {
|
||||
return &s.Total.Value
|
||||
}
|
||||
|
||||
func (s *Direct) Value() int64 {
|
||||
return s.Total.Value
|
||||
}
|
||||
|
||||
func (s *Direct) Add(value int64, device *ClientInfo) {
|
||||
s.Total.Add(value)
|
||||
if device.Natural {
|
||||
s.Natural.Add(value)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Direct) Sub(d *Direct) *Direct {
|
||||
ret := &Direct{
|
||||
Total: *s.Total.Sub(&d.Total),
|
||||
Natural: *s.Natural.Sub(&d.Natural),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Direct) Plus(d *Direct) *Direct {
|
||||
ret := &Direct{
|
||||
Total: *s.Total.Plus(&d.Total),
|
||||
Natural: *s.Natural.Plus(&d.Natural),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Direct) ACC(d *Direct) {
|
||||
s.Total.ACC(&d.Total)
|
||||
s.Natural.ACC(&d.Natural)
|
||||
}
|
||||
|
||||
func (s *Direct) Percentage(den *Direct) *Direct {
|
||||
ret := &Direct{
|
||||
Total: *s.Total.Percentage(&den.Total),
|
||||
Natural: *s.Total.Percentage(&den.Total),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Direct) Permil(den *Direct) *Direct {
|
||||
ret := &Direct{
|
||||
Total: *s.Total.Permil(&den.Total),
|
||||
Natural: *s.Natural.Permil(&den.Natural),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Direct) Mul(v int64) *Direct {
|
||||
ret := &Direct{
|
||||
Total: *s.Total.Mul(v),
|
||||
Natural: *s.Natural.Mul(v),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type From struct {
|
||||
Total Value `json:"total" bson:"total"` // Total
|
||||
// ---------------------分类1------------------------
|
||||
Web Value `json:"web" bson:"web"` // 网站
|
||||
Share Value `json:"share" bson:"share"` // 分享
|
||||
Normal Value `json:"normal" bson:"normal"` // 自来
|
||||
District Value `json:"district" bson:"district"` // 渠道
|
||||
// ---------------------分类2------------------------
|
||||
Direct Direct `json:"direct" bson:"direct"` // 直推
|
||||
Fission Value `json:"fission" bson:"fission"` // 裂变
|
||||
Deduction Value `json:"deduction" bson:"deduction"` // 扣量
|
||||
}
|
||||
|
||||
type StatH5 struct {
|
||||
// 总计
|
||||
Total From `json:"total" bson:"total"` // Total = Pc + Ios + Android
|
||||
// 分类1
|
||||
Pc From `json:"h5Pc" bson:"h5Pc"` // H5PC
|
||||
Ios From `json:"h5Ios" bson:"h5Ios"` // H5IOS
|
||||
Android From `json:"h5Android" bson:"h5Android"` // Android
|
||||
// 分类2
|
||||
Guest From `json:"h5Guest" bson:"h5Guest"` // Guest
|
||||
Formal From `json:"h5Formal" bson:"h5Formal"` // H5正式用户
|
||||
}
|
||||
|
||||
func (s *StatH5) Sub(d *StatH5) *StatH5 {
|
||||
ret := &StatH5{
|
||||
Total: *s.Total.Sub(&d.Total),
|
||||
Pc: *s.Pc.Sub(&d.Pc),
|
||||
Ios: *s.Ios.Sub(&d.Ios),
|
||||
Android: *s.Android.Sub(&d.Android),
|
||||
Guest: *s.Guest.Sub(&d.Guest),
|
||||
Formal: *s.Formal.Sub(&d.Formal),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *StatH5) Plus(d *StatH5) *StatH5 {
|
||||
ret := &StatH5{
|
||||
Total: *s.Total.Plus(&d.Total),
|
||||
Pc: *s.Pc.Plus(&d.Pc),
|
||||
Ios: *s.Ios.Plus(&d.Ios),
|
||||
Android: *s.Android.Plus(&d.Android),
|
||||
Guest: *s.Guest.Plus(&d.Guest),
|
||||
Formal: *s.Formal.Plus(&d.Formal),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *StatH5) ACC(d *StatH5) {
|
||||
s.Total.ACC(&d.Total)
|
||||
s.Pc.ACC(&d.Pc)
|
||||
s.Ios.ACC(&d.Ios)
|
||||
s.Android.ACC(&d.Android)
|
||||
s.Guest.ACC(&d.Guest)
|
||||
s.Formal.ACC(&d.Formal)
|
||||
}
|
||||
|
||||
func (s *StatH5) Mul(v int64) *StatH5 {
|
||||
ret := &StatH5{
|
||||
Total: *s.Total.Mul(v),
|
||||
Pc: *s.Pc.Mul(v),
|
||||
Ios: *s.Ios.Mul(v),
|
||||
Android: *s.Android.Mul(v),
|
||||
Guest: *s.Guest.Mul(v),
|
||||
Formal: *s.Formal.Mul(v),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *StatH5) Add(value int64, device *ClientInfo) {
|
||||
s.Total.Add(value, device)
|
||||
if device.H5Android {
|
||||
s.Android.Add(value, device)
|
||||
} else if device.H5Ios {
|
||||
s.Ios.Add(value, device)
|
||||
} else if device.H5Pc {
|
||||
s.Pc.Add(value, device)
|
||||
}
|
||||
if device.Formal {
|
||||
s.Formal.Add(value, device)
|
||||
} else {
|
||||
s.Guest.Add(value, device)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StatH5) CountRef() *int64 {
|
||||
return s.Total.CountRef()
|
||||
}
|
||||
|
||||
func (s *StatH5) Count() int64 {
|
||||
return s.Total.Count()
|
||||
}
|
||||
|
||||
func (s *StatH5) ValueRef() *int64 {
|
||||
return s.Total.ValueRef()
|
||||
}
|
||||
|
||||
func (s *StatH5) Value() int64 {
|
||||
return s.Total.Value()
|
||||
}
|
||||
|
||||
func (s *StatH5) Percentage(den *StatH5) *StatH5 {
|
||||
ret := &StatH5{
|
||||
Total: *s.Total.Percentage(&den.Total),
|
||||
Pc: *s.Pc.Percentage(&den.Pc),
|
||||
Ios: *s.Ios.Percentage(&den.Ios),
|
||||
Android: *s.Android.Percentage(&den.Android),
|
||||
Guest: *s.Guest.Percentage(&den.Guest),
|
||||
Formal: *s.Formal.Percentage(&den.Formal),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *StatH5) Permil(den *StatH5) *StatH5 {
|
||||
ret := &StatH5{
|
||||
Total: *s.Total.Permil(&den.Total),
|
||||
Pc: *s.Pc.Permil(&den.Pc),
|
||||
Ios: *s.Ios.Permil(&den.Ios),
|
||||
Android: *s.Android.Permil(&den.Android),
|
||||
Guest: *s.Guest.Permil(&den.Guest),
|
||||
Formal: *s.Formal.Permil(&den.Formal),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *StatH5) Rows(div int64, compare *StatH5) [][]string {
|
||||
if div == 0 {
|
||||
div = 1
|
||||
}
|
||||
ret := make([][]string, 0)
|
||||
if compare != nil {
|
||||
ret = append(ret, s.Total.Row("H5总计", div, &compare.Total))
|
||||
ret = append(ret, s.Android.Row("H5安卓", div, &compare.Android))
|
||||
ret = append(ret, s.Ios.Row("H5苹果", div, &compare.Ios))
|
||||
ret = append(ret, s.Pc.Row("H5电脑", div, &compare.Pc))
|
||||
ret = append(ret, s.Guest.Row("H5游客", div, &compare.Guest))
|
||||
ret = append(ret, s.Formal.Row("H5注册", div, &compare.Formal))
|
||||
} else {
|
||||
ret = append(ret, s.Total.Row("H5总计", div, nil))
|
||||
ret = append(ret, s.Android.Row("H5安卓", div, nil))
|
||||
ret = append(ret, s.Ios.Row("H5苹果", div, nil))
|
||||
ret = append(ret, s.Pc.Row("H5电脑", div, nil))
|
||||
ret = append(ret, s.Guest.Row("H5游客", div, nil))
|
||||
ret = append(ret, s.Formal.Row("H5注册", div, nil))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type Stat struct {
|
||||
Total From `json:"total" bson:"total"` // Total
|
||||
Android From `json:"android" bson:"android"` // Android
|
||||
Ios From `json:"ios" bson:"ios"` // IOS
|
||||
Light StatLight `json:"light" bson:"light"` // Light
|
||||
H5 StatH5 `json:"h5" bson:"h5"` // H5
|
||||
}
|
||||
|
||||
func (s *Stat) Mul(v int64) *Stat {
|
||||
ret := &Stat{
|
||||
Total: *s.Total.Mul(v),
|
||||
Android: *s.Android.Mul(v),
|
||||
Ios: *s.Ios.Mul(v),
|
||||
Light: *s.Light.Mul(v),
|
||||
H5: *s.H5.Mul(v),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Stat) Percentage(den *Stat) *Stat {
|
||||
ret := &Stat{
|
||||
Total: *s.Total.Percentage(&den.Total),
|
||||
Android: *s.Android.Percentage(&den.Android),
|
||||
Ios: *s.Ios.Percentage(&den.Ios),
|
||||
Light: *s.Light.Percentage(&den.Light),
|
||||
H5: *s.H5.Percentage(&den.H5),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Stat) Permil(den *Stat) *Stat {
|
||||
ret := &Stat{
|
||||
Total: *s.Total.Permil(&den.Total),
|
||||
Android: *s.Android.Permil(&den.Android),
|
||||
Ios: *s.Ios.Permil(&den.Ios),
|
||||
Light: *s.Light.Permil(&den.Light),
|
||||
H5: *s.H5.Permil(&den.H5),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Stat) Sub(d *Stat) *Stat {
|
||||
ret := &Stat{
|
||||
Total: *s.Total.Sub(&d.Total),
|
||||
Android: *s.Android.Sub(&d.Android),
|
||||
Ios: *s.Ios.Sub(&d.Ios),
|
||||
Light: *s.Light.Sub(&d.Light),
|
||||
H5: *s.H5.Sub(&d.H5),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Stat) Plus(d *Stat) *Stat {
|
||||
ret := &Stat{
|
||||
Total: *s.Total.Plus(&d.Total),
|
||||
Android: *s.Android.Plus(&d.Android),
|
||||
Ios: *s.Ios.Plus(&d.Ios),
|
||||
Light: *s.Light.Plus(&d.Light),
|
||||
H5: *s.H5.Plus(&d.H5),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Stat) ACC(d *Stat) {
|
||||
s.Total.ACC(&d.Total)
|
||||
s.Android.ACC(&d.Android)
|
||||
s.Ios.ACC(&d.Ios)
|
||||
s.Light.ACC(&d.Light)
|
||||
s.H5.ACC(&d.H5)
|
||||
}
|
||||
|
||||
func (s *Stat) Add(value int64, device *ClientInfo) {
|
||||
s.Total.Add(value, device)
|
||||
if device.Android {
|
||||
s.Android.Add(value, device)
|
||||
} else if device.Ios {
|
||||
s.Ios.Add(value, device)
|
||||
} else if device.Light {
|
||||
s.Light.Add(value, device)
|
||||
} else if device.H5 {
|
||||
s.H5.Add(value, device)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stat) CountRef() *int64 {
|
||||
return s.Total.CountRef()
|
||||
}
|
||||
|
||||
func (s *Stat) Count() int64 {
|
||||
return s.Total.Count()
|
||||
}
|
||||
|
||||
func (s *Stat) ValueRef() *int64 {
|
||||
return s.Total.ValueRef()
|
||||
}
|
||||
|
||||
func (s *Stat) Value() int64 {
|
||||
return s.Total.Value()
|
||||
}
|
||||
|
||||
func (s *Stat) Rows(div int64, compare *Stat) [][]string {
|
||||
if div == 0 {
|
||||
div = 1
|
||||
}
|
||||
ret := make([][]string, 0)
|
||||
if compare != nil {
|
||||
ret = append(ret, s.Android.Row("安卓", div, &compare.Android))
|
||||
ret = append(ret, s.Ios.Row("苹果", div, &compare.Ios))
|
||||
ret = append(ret, s.Light.Rows(div, &compare.Light)...)
|
||||
ret = append(ret, s.H5.Rows(div, &compare.H5)...)
|
||||
ret = append(ret, s.Total.Row("合计", div, &compare.Total))
|
||||
} else {
|
||||
ret = append(ret, s.Android.Row("安卓", div, nil))
|
||||
ret = append(ret, s.Ios.Row("苹果", div, nil))
|
||||
ret = append(ret, s.Light.Rows(div, nil)...)
|
||||
ret = append(ret, s.H5.Rows(div, nil)...)
|
||||
ret = append(ret, s.Total.Row("合计", div, nil))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *From) Sub(d *From) *From {
|
||||
ret := &From{
|
||||
Total: *s.Total.Sub(&d.Total),
|
||||
Web: *s.Web.Sub(&d.Web),
|
||||
Normal: *s.Normal.Sub(&d.Normal),
|
||||
District: *s.District.Sub(&d.District),
|
||||
Share: *s.Share.Sub(&d.Share),
|
||||
Direct: *s.Direct.Sub(&d.Direct),
|
||||
Fission: *s.Fission.Sub(&d.Fission),
|
||||
Deduction: *s.Deduction.Sub(&d.Deduction),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *From) Plus(d *From) *From {
|
||||
ret := &From{
|
||||
Total: *s.Total.Plus(&d.Total),
|
||||
Web: *s.Web.Plus(&d.Web),
|
||||
Normal: *s.Normal.Plus(&d.Normal),
|
||||
District: *s.District.Plus(&d.District),
|
||||
Share: *s.Share.Plus(&d.Share),
|
||||
Direct: *s.Direct.Plus(&d.Direct),
|
||||
Fission: *s.Fission.Plus(&d.Fission),
|
||||
Deduction: *s.Deduction.Plus(&d.Deduction),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *From) ACC(d *From) {
|
||||
s.Total.ACC(&d.Total)
|
||||
s.Web.ACC(&d.Web)
|
||||
s.Normal.ACC(&d.Normal)
|
||||
s.District.ACC(&d.District)
|
||||
s.Share.ACC(&d.Share)
|
||||
s.Direct.ACC(&d.Direct)
|
||||
s.Fission.ACC(&d.Fission)
|
||||
s.Deduction.ACC(&d.Deduction)
|
||||
}
|
||||
|
||||
func (s *From) Mul(v int64) *From {
|
||||
ret := &From{
|
||||
Total: *s.Total.Mul(v),
|
||||
Web: *s.Web.Mul(v),
|
||||
Normal: *s.Normal.Mul(v),
|
||||
District: *s.District.Mul(v),
|
||||
Share: *s.Share.Mul(v),
|
||||
Direct: *s.Direct.Mul(v),
|
||||
Fission: *s.Fission.Mul(v),
|
||||
Deduction: *s.Deduction.Mul(v),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *From) Percentage(den *From) *From {
|
||||
ret := &From{
|
||||
Total: *s.Total.Percentage(&den.Total),
|
||||
Web: *s.Web.Percentage(&den.Web),
|
||||
Normal: *s.Normal.Percentage(&den.Normal),
|
||||
District: *s.District.Percentage(&den.District),
|
||||
Share: *s.Share.Percentage(&den.Share),
|
||||
Direct: *s.Direct.Percentage(&den.Direct),
|
||||
Fission: *s.Fission.Percentage(&den.Fission),
|
||||
Deduction: *s.Deduction.Percentage(&den.Deduction),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *From) Permil(den *From) *From {
|
||||
ret := &From{
|
||||
Total: *s.Total.Permil(&den.Total),
|
||||
Web: *s.Web.Permil(&den.Web),
|
||||
Normal: *s.Normal.Permil(&den.Normal),
|
||||
District: *s.District.Permil(&den.District),
|
||||
Share: *s.Share.Permil(&den.Share),
|
||||
Direct: *s.Direct.Permil(&den.Direct),
|
||||
Fission: *s.Fission.Permil(&den.Fission),
|
||||
Deduction: *s.Deduction.Permil(&den.Deduction),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *From) Row(title string, div int64, compare *From) []string {
|
||||
if div == 0 {
|
||||
div = 1
|
||||
}
|
||||
if compare == nil {
|
||||
return []string{
|
||||
title,
|
||||
fmt.Sprintf("%d", s.Total.Value/div),
|
||||
fmt.Sprintf("%d", s.Web.Value/div),
|
||||
fmt.Sprintf("%d", s.Normal.Value/div),
|
||||
fmt.Sprintf("%d", s.District.Value/div),
|
||||
fmt.Sprintf("%d", s.Share.Value/div),
|
||||
fmt.Sprintf("%d", s.Direct.Total.Value/div),
|
||||
fmt.Sprintf("%d", s.Fission.Value/div),
|
||||
fmt.Sprintf("%d", s.Deduction.Value/div),
|
||||
}
|
||||
} else {
|
||||
diff := s.Sub(compare)
|
||||
rate := diff.Permil(compare)
|
||||
return []string{
|
||||
title,
|
||||
fmt.Sprintf("%d\n%s", s.Total.Value/div, DisplayPermilToPercentage(rate.Total.Value)),
|
||||
fmt.Sprintf("%d\n%s", s.Web.Value/div, DisplayPermilToPercentage(rate.Web.Value)),
|
||||
fmt.Sprintf("%d\n%s", s.Normal.Value/div, DisplayPermilToPercentage(rate.Normal.Value)),
|
||||
fmt.Sprintf("%d\n%s", s.District.Value/div, DisplayPermilToPercentage(rate.District.Value)),
|
||||
fmt.Sprintf("%d\n%s", s.Share.Value/div, DisplayPermilToPercentage(rate.Share.Value)),
|
||||
fmt.Sprintf("%d\n%s", s.Direct.Total.Value/div, DisplayPermilToPercentage(rate.Direct.Total.Value)),
|
||||
fmt.Sprintf("%d\n%s", s.Fission.Value/div, DisplayPermilToPercentage(rate.Fission.Value)),
|
||||
fmt.Sprintf("%d\n%s", s.Deduction.Value/div, DisplayPermilToPercentage(rate.Deduction.Value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *From) Header(title string) []string {
|
||||
return []string{title, "总量", "官网", "自来", "渠道", "分享", "直推", "裂变", "扣量"}
|
||||
}
|
||||
|
||||
func (s *From) Add(value int64, client *ClientInfo) {
|
||||
s.Total.Add(value)
|
||||
// 分类1统计
|
||||
if client.Web {
|
||||
s.Web.Add(value)
|
||||
} else if client.Share {
|
||||
s.Share.Add(value)
|
||||
} else if client.Normal {
|
||||
s.Normal.Add(value)
|
||||
} else if client.District {
|
||||
s.District.Add(value)
|
||||
}
|
||||
// 分类2统计
|
||||
if client.Deduction {
|
||||
s.Deduction.Add(value)
|
||||
} else {
|
||||
if client.Direct {
|
||||
s.Direct.Add(value, client)
|
||||
} else {
|
||||
s.Fission.Add(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *From) CountRef() *int64 {
|
||||
return s.Total.CountRef()
|
||||
}
|
||||
|
||||
func (s *From) Count() int64 {
|
||||
return s.Total.Count
|
||||
}
|
||||
|
||||
func (s *From) ValueRef() *int64 {
|
||||
return s.Total.ValueRef()
|
||||
}
|
||||
|
||||
func (s *From) Value() int64 {
|
||||
return s.Total.Value
|
||||
}
|
||||
|
||||
func percentageDiv(num, den int64) int64 {
|
||||
if num == 0 {
|
||||
return 0
|
||||
} else {
|
||||
if den == 0 {
|
||||
return math.MaxInt64
|
||||
} else {
|
||||
return num * 100 / den
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func permilDiv(num, den int64) int64 {
|
||||
if num == 0 {
|
||||
return 0
|
||||
} else {
|
||||
if den == 0 {
|
||||
return math.MaxInt64
|
||||
} else {
|
||||
return num * 1000 / den
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DisplayPermilToPercentage(v int64) string {
|
||||
var ret string
|
||||
var x float64
|
||||
if v == math.MaxInt64 {
|
||||
s := "##.##"
|
||||
ret = fmt.Sprintf("%s", s)
|
||||
return ret
|
||||
}
|
||||
if v < -99999 {
|
||||
s := "--.--"
|
||||
ret = fmt.Sprintf("%s", s)
|
||||
return ret
|
||||
}
|
||||
if v > 99999 {
|
||||
s := "++.++"
|
||||
ret = fmt.Sprintf("%s", s)
|
||||
return ret
|
||||
}
|
||||
if v < 0 {
|
||||
x = float64(v) / -10.0
|
||||
s := fmt.Sprintf("-%.1f%%", x)
|
||||
ret = fmt.Sprintf("%s", s)
|
||||
} else if v > 0 {
|
||||
x = float64(v) / 10.0
|
||||
s := fmt.Sprintf("+%.1f%%", x)
|
||||
ret = fmt.Sprintf("%s", s)
|
||||
} else {
|
||||
s := "00.0%"
|
||||
ret = fmt.Sprintf("%s", s)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type StatLight struct {
|
||||
// 总计
|
||||
Total From `json:"total" bson:"total"` // Total = Pc + Ios + Android
|
||||
// 分类
|
||||
Guest From `json:"guest" bson:"guest"` // Guest
|
||||
Formal From `json:"formal" bson:"formal"` // 正式用户
|
||||
}
|
||||
|
||||
func (s *StatLight) Sub(d *StatLight) *StatLight {
|
||||
ret := &StatLight{
|
||||
Total: *s.Total.Sub(&d.Total),
|
||||
Guest: *s.Guest.Sub(&d.Guest),
|
||||
Formal: *s.Formal.Sub(&d.Formal),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *StatLight) Plus(d *StatLight) *StatLight {
|
||||
ret := &StatLight{
|
||||
Total: *s.Total.Plus(&d.Total),
|
||||
Guest: *s.Guest.Plus(&d.Guest),
|
||||
Formal: *s.Formal.Plus(&d.Formal),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *StatLight) ACC(d *StatLight) {
|
||||
s.Total.ACC(&d.Total)
|
||||
s.Guest.ACC(&d.Guest)
|
||||
s.Formal.ACC(&d.Formal)
|
||||
}
|
||||
|
||||
func (s *StatLight) Mul(v int64) *StatLight {
|
||||
ret := &StatLight{
|
||||
Total: *s.Total.Mul(v),
|
||||
Guest: *s.Guest.Mul(v),
|
||||
Formal: *s.Formal.Mul(v),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *StatLight) Add(value int64, device *ClientInfo) {
|
||||
s.Total.Add(value, device)
|
||||
if device.Formal {
|
||||
s.Formal.Add(value, device)
|
||||
} else {
|
||||
s.Guest.Add(value, device)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StatLight) CountRef() *int64 {
|
||||
return s.Total.CountRef()
|
||||
}
|
||||
|
||||
func (s *StatLight) Count() int64 {
|
||||
return s.Total.Count()
|
||||
}
|
||||
|
||||
func (s *StatLight) ValueRef() *int64 {
|
||||
return s.Total.ValueRef()
|
||||
}
|
||||
|
||||
func (s *StatLight) Value() int64 {
|
||||
return s.Total.Value()
|
||||
}
|
||||
|
||||
func (s *StatLight) Percentage(den *StatLight) *StatLight {
|
||||
ret := &StatLight{
|
||||
Total: *s.Total.Percentage(&den.Total),
|
||||
Guest: *s.Guest.Percentage(&den.Guest),
|
||||
Formal: *s.Formal.Percentage(&den.Formal),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *StatLight) Permil(den *StatLight) *StatLight {
|
||||
ret := &StatLight{
|
||||
Total: *s.Total.Permil(&den.Total),
|
||||
Guest: *s.Guest.Permil(&den.Guest),
|
||||
Formal: *s.Formal.Permil(&den.Formal),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *StatLight) Rows(div int64, compare *StatLight) [][]string {
|
||||
if div == 0 {
|
||||
div = 1
|
||||
}
|
||||
ret := make([][]string, 0)
|
||||
if compare != nil {
|
||||
ret = append(ret, s.Total.Row("轻量合计", div, &compare.Total))
|
||||
ret = append(ret, s.Guest.Row("轻量安装", div, &compare.Guest))
|
||||
ret = append(ret, s.Formal.Row("轻量注册", div, &compare.Formal))
|
||||
} else {
|
||||
ret = append(ret, s.Total.Row("轻量合计", div, nil))
|
||||
ret = append(ret, s.Guest.Row("轻量安装", div, nil))
|
||||
ret = append(ret, s.Formal.Row("轻量注册", div, nil))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package statutils
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/vicanso/go-charts/v2"
|
||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||
|
||||
"github.com/golang/freetype"
|
||||
|
||||
"github.com/golang/freetype/truetype"
|
||||
)
|
||||
|
||||
//go:embed 微软雅黑.ttf
|
||||
var FontData []byte
|
||||
var Font *truetype.Font
|
||||
|
||||
func init() {
|
||||
f, err := freetype.ParseFont(FontData)
|
||||
if err == nil {
|
||||
Font = f
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stat) Table(title string, div int64, compare *Stat, filePath string) error {
|
||||
if div == 0 {
|
||||
div = 1
|
||||
}
|
||||
rows := make([][]string, 0)
|
||||
if compare != nil {
|
||||
rows = append(rows, s.Android.Row("安卓", div, &compare.Android))
|
||||
rows = append(rows, s.Ios.Row("苹果", div, &compare.Ios))
|
||||
rows = append(rows, s.Light.Rows(div, &compare.Light)...)
|
||||
rows = append(rows, s.H5.Rows(div, &compare.H5)...)
|
||||
rows = append(rows, s.Total.Row("合计", div, &compare.Total))
|
||||
} else {
|
||||
rows = append(rows, s.Android.Row("安卓", div, nil))
|
||||
rows = append(rows, s.Ios.Row("苹果", div, nil))
|
||||
rows = append(rows, s.Light.Rows(div, nil)...)
|
||||
rows = append(rows, s.H5.Rows(div, nil)...)
|
||||
rows = append(rows, s.Total.Row("合计", div, nil))
|
||||
}
|
||||
|
||||
charts.SetDefaultWidth(810)
|
||||
bgColor := drawing.ColorWhite
|
||||
p, err := charts.TableOptionRender(charts.TableChartOption{
|
||||
Header: s.Total.Header(title),
|
||||
BackgroundColor: bgColor,
|
||||
HeaderBackgroundColor: drawing.ColorFromHex("808080"),
|
||||
RowBackgroundColors: []charts.Color{
|
||||
bgColor,
|
||||
},
|
||||
HeaderFontColor: drawing.ColorBlack,
|
||||
FontColor: drawing.ColorBlack,
|
||||
Padding: charts.Box{
|
||||
Top: 15,
|
||||
Right: 10,
|
||||
Bottom: 15,
|
||||
Left: 10,
|
||||
},
|
||||
Data: rows,
|
||||
Font: Font,
|
||||
TextAligns: []string{
|
||||
charts.AlignCenter,
|
||||
charts.AlignLeft,
|
||||
},
|
||||
CellStyle: func(tc charts.TableCell) *charts.Style {
|
||||
text := strings.TrimSpace(tc.Text)
|
||||
style := &charts.Style{
|
||||
Padding: charts.Box{
|
||||
Bottom: 5,
|
||||
},
|
||||
}
|
||||
if strings.Contains(text, "-") {
|
||||
style.FontColor = drawing.ColorRed
|
||||
} else if strings.Contains(text, "+") {
|
||||
style.FontColor = drawing.ColorGreen
|
||||
}
|
||||
|
||||
if slices.Contains([]int{1, 2, 3}, tc.Row) {
|
||||
style.FillColor = drawing.ColorFromHex("ec8f75")
|
||||
} else if slices.Contains([]int{4, 5, 6}, tc.Row) {
|
||||
style.FillColor = drawing.ColorFromHex("788fc7")
|
||||
} else if slices.Contains([]int{7, 8}, tc.Row) {
|
||||
style.FillColor = drawing.ColorFromHex("ced6d2")
|
||||
} else if slices.Contains([]int{9}, tc.Row) {
|
||||
style.FillColor = drawing.ColorFromHex("808080")
|
||||
}
|
||||
return style
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf, err := p.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return WriteBytesToFile(buf, filePath)
|
||||
}
|
||||
|
||||
func WriteBytesToFile(buf []byte, filename string) error {
|
||||
err := os.WriteFile(filename, buf, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CommonTable(headers []string, rows [][]string, tableWidth int, filePath string) error {
|
||||
charts.SetDefaultWidth(tableWidth)
|
||||
bgColor := drawing.ColorWhite
|
||||
p, err := charts.TableOptionRender(charts.TableChartOption{
|
||||
Header: headers,
|
||||
BackgroundColor: bgColor,
|
||||
HeaderBackgroundColor: drawing.ColorFromHex("808080"),
|
||||
RowBackgroundColors: []charts.Color{
|
||||
bgColor,
|
||||
},
|
||||
HeaderFontColor: drawing.ColorBlack,
|
||||
FontColor: drawing.ColorBlack,
|
||||
Padding: charts.Box{
|
||||
Top: 15,
|
||||
Right: 10,
|
||||
Bottom: 15,
|
||||
Left: 10,
|
||||
},
|
||||
Data: rows,
|
||||
Font: Font,
|
||||
TextAligns: []string{
|
||||
charts.AlignCenter,
|
||||
charts.AlignLeft,
|
||||
},
|
||||
CellStyle: func(tc charts.TableCell) *charts.Style {
|
||||
style := &charts.Style{
|
||||
Padding: charts.Box{
|
||||
Bottom: 5,
|
||||
},
|
||||
}
|
||||
if tc.Row%2 == 0 {
|
||||
style.FillColor = drawing.ColorFromHex("f8f8f8")
|
||||
} else {
|
||||
style.FillColor = drawing.ColorWhite
|
||||
}
|
||||
return style
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf, err := p.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return WriteBytesToFile(buf, filePath)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package statutils
|
||||
|
||||
import (
|
||||
"stat/common/utils"
|
||||
"stat/models/channelmod"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DeviceCategory struct {
|
||||
Android bool
|
||||
Ios bool
|
||||
Light bool
|
||||
H5 bool
|
||||
H5Android bool
|
||||
H5Ios bool
|
||||
H5Pc bool
|
||||
}
|
||||
|
||||
type SourceCategory struct {
|
||||
Web bool // 是否官网
|
||||
Normal bool // 是否自来
|
||||
District bool // 是否渠道
|
||||
Share bool // 是否分享
|
||||
}
|
||||
|
||||
type OtherCategory struct {
|
||||
Direct bool // 是否直推
|
||||
Deduction bool // 是否扣量
|
||||
Natural bool // 是否自然
|
||||
Formal bool // H5正式用户
|
||||
}
|
||||
|
||||
type ClientInfo struct {
|
||||
//------设备------
|
||||
DeviceCategory
|
||||
//------来源------
|
||||
SourceCategory
|
||||
//------其他------
|
||||
OtherCategory
|
||||
}
|
||||
|
||||
func IsClientH5OrLight(devs ...string) bool {
|
||||
for _, name := range devs {
|
||||
dev := strings.ToLower(strings.TrimSpace(name))
|
||||
if dev == string(DeviceNameLight) || dev == string(DeviceNameH5) || dev == string(DeviceNameH5Android) || dev == string(DeviceNameH5Ios) || dev == string(DeviceNameH5Pc) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsClientH5(devs ...string) bool {
|
||||
for _, name := range devs {
|
||||
dev := strings.ToLower(strings.TrimSpace(name))
|
||||
if dev == string(DeviceNameH5) || dev == string(DeviceNameH5Android) || dev == string(DeviceNameH5Ios) || dev == string(DeviceNameH5Pc) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsClientPkg(devs ...string) bool {
|
||||
for _, name := range devs {
|
||||
dev := strings.ToLower(strings.TrimSpace(name))
|
||||
if dev == string(DeviceNameIos) || dev == string(DeviceNameAndroid) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsClientLight(devs ...string) bool {
|
||||
for _, name := range devs {
|
||||
dev := strings.ToLower(strings.TrimSpace(name))
|
||||
if dev == string(DeviceNameLight) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsClientAndroid(devs ...string) bool {
|
||||
for _, name := range devs {
|
||||
dev := strings.ToLower(strings.TrimSpace(name))
|
||||
if dev == string(DeviceNameAndroid) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsClientIos(devs ...string) bool {
|
||||
for _, name := range devs {
|
||||
dev := strings.ToLower(strings.TrimSpace(name))
|
||||
if dev == string(DeviceNameIos) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetDeviceInfo(dev1, dev2 string) DeviceCategory {
|
||||
ret := DeviceCategory{}
|
||||
ret.Android = utils.TowIsOR(dev1, dev2, string(DeviceNameAndroid))
|
||||
if !ret.Android {
|
||||
ret.Ios = utils.TowIsOR(dev1, dev2, string(DeviceNameIos))
|
||||
if !ret.Ios {
|
||||
ret.Light = utils.TowIsOR(dev1, dev2, string(DeviceNameLight))
|
||||
if !ret.Light {
|
||||
ret.H5 = true
|
||||
ret.H5Android = utils.TowIsOR(dev1, dev2, string(DeviceNameH5Android))
|
||||
if !ret.H5Android {
|
||||
ret.H5Ios = utils.TowIsOR(dev1, dev2, string(DeviceNameH5Ios))
|
||||
if !ret.H5Ios {
|
||||
ret.H5Pc = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func GetClientInfo(dev1, dev2 string, districtCode string, isDirect bool, isDeduction bool, isFormal bool) *ClientInfo {
|
||||
ret := &ClientInfo{
|
||||
DeviceCategory: GetDeviceInfo(dev1, dev2),
|
||||
}
|
||||
isOfficial := channelmod.IsOfficialChannelOrEmpty(districtCode)
|
||||
if districtCode == "modelweb" {
|
||||
ret.Web = true
|
||||
} else if districtCode == "h5share" {
|
||||
ret.Share = true
|
||||
} else if isOfficial || !isDirect {
|
||||
ret.Normal = true
|
||||
} else {
|
||||
ret.District = true
|
||||
}
|
||||
ret.Direct = isDirect
|
||||
if isDirect && isOfficial {
|
||||
ret.Natural = true
|
||||
}
|
||||
ret.Deduction = isDeduction
|
||||
ret.Formal = isFormal
|
||||
return ret
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package statutils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsDeviceH5(t *testing.T) {
|
||||
type args struct {
|
||||
devs []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "a",
|
||||
args: args{
|
||||
devs: []string{"andro", "android", "h5_android"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsClientH5(tt.args.devs...); got != tt.want {
|
||||
t.Errorf("IsClientH5() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDeviceAndroid(t *testing.T) {
|
||||
type args struct {
|
||||
devs []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "a",
|
||||
args: args{
|
||||
devs: []string{"andro", "android", "h5_android1"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsClientAndroid(tt.args.devs...); got != tt.want {
|
||||
t.Errorf("IsClientH5() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDeviceIos(t *testing.T) {
|
||||
type args struct {
|
||||
devs []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "a",
|
||||
args: args{
|
||||
devs: []string{"ios", "android", "h5_android1"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsClientIos(tt.args.devs...); got != tt.want {
|
||||
t.Errorf("IsClientH5() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user