Initial commit

This commit is contained in:
2026-05-20 16:13:58 +08:00
commit ceffbe382e
500 changed files with 157469 additions and 0 deletions
+42
View File
@@ -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: "游戏统计",
}
+110
View File
@@ -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{},
}
)
+65
View File
@@ -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
}
+243
View File
@@ -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] }
+20
View File
@@ -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)
}
+728
View File
@@ -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
}
+162
View File
@@ -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)
}
+145
View File
@@ -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
}
+81
View File
@@ -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.