diff --git a/controllers/platform_account_pool.go b/controllers/platform_account_pool.go index c9ebdb3..526cbf8 100644 --- a/controllers/platform_account_pool.go +++ b/controllers/platform_account_pool.go @@ -75,6 +75,64 @@ func validateCreateRow(row accountPoolCreateRow) error { return nil } +// accountPoolListWhere 列表筛选(与各号池表字段一致) +func accountPoolListWhere(dataType, status, keyword string) (where string, args []interface{}) { + var parts []string + if dataType != "" && isValidPoolType(dataType) { + parts = append(parts, "data_type = ?") + args = append(args, dataType) + } + switch status { + case "unused": + parts = append(parts, "is_extracted = ?") + args = append(args, int8(0)) + case "extracted": + parts = append(parts, "is_extracted IN (?, ?)") + args = append(args, int8(1), int8(2)) + } + if kw := strings.TrimSpace(keyword); kw != "" { + parts = append(parts, "account LIKE ?") + args = append(args, "%"+kw+"%") + } + if len(parts) == 0 { + return "1=1", args + } + return strings.Join(parts, " AND "), args +} + +func accountPoolCountMySQL(table, where string, whereArgs []interface{}) (int64, error) { + sqlStr := fmt.Sprintf("SELECT COUNT(*) AS cnt FROM `%s` WHERE %s", table, where) + var maps []orm.Params + _, err := models.Orm.Raw(sqlStr, whereArgs...).Values(&maps) + if err != nil { + return 0, err + } + if len(maps) == 0 { + return 0, nil + } + return paramsCellToInt64(maps[0]["cnt"]), nil +} + +func paramsCellToInt64(v interface{}) int64 { + if v == nil { + return 0 + } + switch x := v.(type) { + case []byte: + n, _ := strconv.ParseInt(strings.TrimSpace(string(x)), 10, 64) + return n + case int64: + return x + case int32: + return int64(x) + case int: + return int64(x) + default: + n, _ := strconv.ParseInt(strings.TrimSpace(fmt.Sprint(x)), 10, 64) + return n + } +} + func listPoolRows(c *beego.Controller, module string) { if _, err := requirePlatformAuth(c); err != nil { poolJSONErr(c, 401, 401, err.Error()) @@ -95,21 +153,8 @@ func listPoolRows(c *beego.Controller, module string) { dataType := strings.TrimSpace(c.GetString("type")) status := strings.TrimSpace(c.GetString("status")) - applyFilters := func(qs orm.QuerySeter) orm.QuerySeter { - if dataType != "" && isValidPoolType(dataType) { - qs = qs.Filter("data_type", dataType) - } - if status == "unused" { - qs = qs.Filter("is_extracted", 0) - } - if status == "extracted" { - qs = qs.Filter("is_extracted", 1) - } - if keyword != "" { - qs = qs.Filter("account__icontains", keyword) - } - return qs - } + where, whereArgs := accountPoolListWhere(dataType, status, keyword) + offset := (page - 1) * pageSize var list interface{} var total int64 @@ -117,14 +162,19 @@ func listPoolRows(c *beego.Controller, module string) { switch module { case "cursor": - qs := applyFilters(models.Orm.QueryTable(new(models.PlatformAccountPoolCursor))) - total, err = qs.Count() + table := (&models.PlatformAccountPoolCursor{}).TableName() + total, err = accountPoolCountMySQL(table, where, whereArgs) if err != nil { poolJSONErr(c, 500, 500, "获取列表失败: "+err.Error()) return } + sqlStr := fmt.Sprintf( + "SELECT * FROM `%s` WHERE %s ORDER BY FIELD(is_extracted, 0, 2, 1) ASC, id DESC LIMIT ? OFFSET ?", + table, where, + ) + args := append(append([]interface{}{}, whereArgs...), pageSize, offset) var rows []models.PlatformAccountPoolCursor - _, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows) + _, err = models.Orm.Raw(sqlStr, args...).QueryRows(&rows) if err != nil && err != orm.ErrNoRows { poolJSONErr(c, 500, 500, "cursor查询失败: "+err.Error()) return @@ -134,14 +184,19 @@ func listPoolRows(c *beego.Controller, module string) { } list = rows case "windsurf": - qs := applyFilters(models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf))) - total, err = qs.Count() + table := (&models.PlatformAccountPoolWindsurf{}).TableName() + total, err = accountPoolCountMySQL(table, where, whereArgs) if err != nil { poolJSONErr(c, 500, 500, "获取列表失败: "+err.Error()) return } + sqlStr := fmt.Sprintf( + "SELECT * FROM `%s` WHERE %s ORDER BY FIELD(is_extracted, 0, 2, 1) ASC, id DESC LIMIT ? OFFSET ?", + table, where, + ) + args := append(append([]interface{}{}, whereArgs...), pageSize, offset) var rows []models.PlatformAccountPoolWindsurf - _, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows) + _, err = models.Orm.Raw(sqlStr, args...).QueryRows(&rows) if err != nil && err != orm.ErrNoRows { poolJSONErr(c, 500, 500, "获取列表失败: "+err.Error()) return @@ -151,14 +206,19 @@ func listPoolRows(c *beego.Controller, module string) { } list = rows case "krio": - qs := applyFilters(models.Orm.QueryTable(new(models.PlatformAccountPoolKiro))) - total, err = qs.Count() + table := (&models.PlatformAccountPoolKiro{}).TableName() + total, err = accountPoolCountMySQL(table, where, whereArgs) if err != nil { poolJSONErr(c, 500, 500, "获取列表失败: "+err.Error()) return } + sqlStr := fmt.Sprintf( + "SELECT * FROM `%s` WHERE %s ORDER BY FIELD(is_extracted, 0, 2, 1) ASC, id DESC LIMIT ? OFFSET ?", + table, where, + ) + args := append(append([]interface{}{}, whereArgs...), pageSize, offset) var rows []models.PlatformAccountPoolKiro - _, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows) + _, err = models.Orm.Raw(sqlStr, args...).QueryRows(&rows) if err != nil && err != orm.ErrNoRows { poolJSONErr(c, 500, 500, "获取列表失败: "+err.Error()) return @@ -363,10 +423,11 @@ func extractPoolRow(c *beego.Controller, module string) { return } var payload struct { - ID uint64 `json:"id"` - Type string `json:"type"` - Platform string `json:"platform"` // local | xianyu | taobao | pinduoduo | jingdong | douyin | ziyoushangcheng - Remark string `json:"remark"` + ID uint64 `json:"id"` + Type string `json:"type"` + Platform string `json:"platform"` // local | xianyu | taobao | pinduoduo | jingdong | douyin | ziyoushangcheng + Remark string `json:"remark"` + Replenish bool `json:"replenish"` // true 时写入 is_extracted=2(补号),否则为 1(已提取) } if err := json.Unmarshal(raw, &payload); err != nil { poolJSONErr(c, 400, 400, "参数错误") @@ -390,6 +451,10 @@ func extractPoolRow(c *beego.Controller, module string) { now := time.Now() platform := payload.Platform remark := strings.TrimSpace(payload.Remark) + extractStatus := int8(1) + if payload.Replenish { + extractStatus = 2 + } switch module { case "cursor": @@ -405,15 +470,20 @@ func extractPoolRow(c *beego.Controller, module string) { return } _, err = models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).Filter("id", row.ID).Update(map[string]interface{}{ - "is_extracted": 1, - "extracted_time": now, + "is_extracted": extractStatus, + "extracted_time": now, "extracted_platform": platform, - "remark": remark, + "remark": remark, }) if err != nil { poolJSONErr(c, 500, 500, "提取失败: "+err.Error()) return } + row.IsExtracted = extractStatus + row.ExtractedTime = &now + pf := platform + row.ExtractedPlatform = &pf + row.Remark = remark c.Data["json"] = map[string]interface{}{"code": 200, "msg": "提取成功", "data": row} case "windsurf": var row models.PlatformAccountPoolWindsurf @@ -428,15 +498,20 @@ func extractPoolRow(c *beego.Controller, module string) { return } _, err = models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).Filter("id", row.ID).Update(map[string]interface{}{ - "is_extracted": 1, - "extracted_time": now, + "is_extracted": extractStatus, + "extracted_time": now, "extracted_platform": platform, - "remark": remark, + "remark": remark, }) if err != nil { poolJSONErr(c, 500, 500, "提取失败: "+err.Error()) return } + row.IsExtracted = extractStatus + row.ExtractedTime = &now + pf := platform + row.ExtractedPlatform = &pf + row.Remark = remark c.Data["json"] = map[string]interface{}{"code": 200, "msg": "提取成功", "data": row} case "krio": var row models.PlatformAccountPoolKiro @@ -451,15 +526,20 @@ func extractPoolRow(c *beego.Controller, module string) { return } _, err = models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).Filter("id", row.ID).Update(map[string]interface{}{ - "is_extracted": 1, - "extracted_time": now, + "is_extracted": extractStatus, + "extracted_time": now, "extracted_platform": platform, - "remark": remark, + "remark": remark, }) if err != nil { poolJSONErr(c, 500, 500, "提取失败: "+err.Error()) return } + row.IsExtracted = extractStatus + row.ExtractedTime = &now + pf := platform + row.ExtractedPlatform = &pf + row.Remark = remark c.Data["json"] = map[string]interface{}{"code": 200, "msg": "提取成功", "data": row} default: poolJSONErr(c, 400, 400, "无效模块") @@ -511,12 +591,12 @@ func replenishPoolRow(c *beego.Controller, module string) { return } if _, err = models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).Filter("id", row.ID).Update(map[string]interface{}{ - "is_extracted": 1, "extracted_time": now, "extracted_platform": platform, "remark": remark, + "is_extracted": int8(2), "extracted_time": now, "extracted_platform": platform, "remark": remark, }); err != nil { poolJSONErr(c, 500, 500, "补号失败: "+err.Error()) return } - row.IsExtracted = 1 + row.IsExtracted = 2 row.ExtractedTime = &now row.ExtractedPlatform = &platform row.Remark = remark @@ -530,12 +610,12 @@ func replenishPoolRow(c *beego.Controller, module string) { return } if _, err = models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).Filter("id", row.ID).Update(map[string]interface{}{ - "is_extracted": 1, "extracted_time": now, "extracted_platform": platform, "remark": remark, + "is_extracted": int8(2), "extracted_time": now, "extracted_platform": platform, "remark": remark, }); err != nil { poolJSONErr(c, 500, 500, "补号失败: "+err.Error()) return } - row.IsExtracted = 1 + row.IsExtracted = 2 row.ExtractedTime = &now row.ExtractedPlatform = &platform row.Remark = remark @@ -549,12 +629,12 @@ func replenishPoolRow(c *beego.Controller, module string) { return } if _, err = models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).Filter("id", row.ID).Update(map[string]interface{}{ - "is_extracted": 1, "extracted_time": now, "extracted_platform": platform, "remark": remark, + "is_extracted": int8(2), "extracted_time": now, "extracted_platform": platform, "remark": remark, }); err != nil { poolJSONErr(c, 500, 500, "补号失败: "+err.Error()) return } - row.IsExtracted = 1 + row.IsExtracted = 2 row.ExtractedTime = &now row.ExtractedPlatform = &platform row.Remark = remark diff --git a/controllers/platform_home.go b/controllers/platform_home.go new file mode 100644 index 0000000..96dc012 --- /dev/null +++ b/controllers/platform_home.go @@ -0,0 +1,190 @@ +package controllers + +import ( + "fmt" + "strconv" + "strings" + "time" + + "server/models" + + "github.com/beego/beego/v2/client/orm" + beego "github.com/beego/beego/v2/server/web" +) + +// PlatformHomeController 平台首页统计(需登录) +type PlatformHomeController struct { + beego.Controller +} + +func cellToDateKey(v interface{}) string { + if v == nil { + return "" + } + switch x := v.(type) { + case []byte: + s := strings.TrimSpace(string(x)) + if len(s) >= 10 { + return s[:10] + } + return s + case string: + s := strings.TrimSpace(x) + if len(s) >= 10 { + return s[:10] + } + return s + case time.Time: + if x.IsZero() { + return "" + } + return x.In(time.Local).Format("2006-01-02") + default: + s := strings.TrimSpace(fmt.Sprint(x)) + if len(s) >= 10 { + return s[:10] + } + return s + } +} + +func cellToInt64(v interface{}) int64 { + if v == nil { + return 0 + } + switch x := v.(type) { + case []byte: + n, _ := strconv.ParseInt(strings.TrimSpace(string(x)), 10, 64) + return n + case int64: + return x + case int32: + return int64(x) + case int: + return int64(x) + default: + n, _ := strconv.ParseInt(strings.TrimSpace(fmt.Sprint(x)), 10, 64) + return n + } +} + +func queryExtractedCountByDay(table string, start, endExclusive time.Time) (map[string]int64, error) { + // 不按 delete_time 过滤:部分库未删除行存 0000-00-00 或非 NULL,会导致统计全空。 + // Raw + QueryRows 对别名映射不稳定,改用 Values 解析 d/c。 + sql := fmt.Sprintf(` +SELECT DATE(extracted_time) AS d, COUNT(*) AS c +FROM %s +WHERE is_extracted IN (1, 2) + AND extracted_time IS NOT NULL + AND extracted_time >= ? + AND extracted_time < ? +GROUP BY DATE(extracted_time) +ORDER BY d +`, table) + var maps []orm.Params + _, err := models.Orm.Raw(sql, start, endExclusive).Values(&maps) + if err != nil { + return nil, err + } + out := make(map[string]int64, len(maps)) + for _, m := range maps { + var dk, ck interface{} + for _, k := range []string{"d", "D"} { + if v, ok := m[k]; ok { + dk = v + break + } + } + for _, k := range []string{"c", "C"} { + if v, ok := m[k]; ok { + ck = v + break + } + } + key := cellToDateKey(dk) + if key == "" { + continue + } + out[key] = cellToInt64(ck) + } + return out, nil +} + +// AccountPoolDailyExtract GET /platform/home/accountPoolDailyExtract?days=14 +// 按天统计各号池「已提取」数量,依据 extracted_time 落在当天的记录。 +func (c *PlatformHomeController) AccountPoolDailyExtract() { + if _, err := requirePlatformAuth(&c.Controller); err != nil { + poolJSONErr(&c.Controller, 401, 401, err.Error()) + return + } + n, _ := c.GetInt("days", 14) + if n < 1 { + n = 1 + } + if n > 90 { + n = 90 + } + + now := time.Now().In(time.Local) + today0 := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) + firstDay := today0.AddDate(0, 0, -(n - 1)) + endExclusive := today0.AddDate(0, 0, 1) + + cursorTable := (&models.PlatformAccountPoolCursor{}).TableName() + windsurfTable := (&models.PlatformAccountPoolWindsurf{}).TableName() + kiroTable := (&models.PlatformAccountPoolKiro{}).TableName() + + mCursor, err := queryExtractedCountByDay(cursorTable, firstDay, endExclusive) + if err != nil { + poolJSONErr(&c.Controller, 500, 500, "统计 Cursor 失败: "+err.Error()) + return + } + mWindsurf, err := queryExtractedCountByDay(windsurfTable, firstDay, endExclusive) + if err != nil { + poolJSONErr(&c.Controller, 500, 500, "统计 Windsurf 失败: "+err.Error()) + return + } + mKiro, err := queryExtractedCountByDay(kiroTable, firstDay, endExclusive) + if err != nil { + poolJSONErr(&c.Controller, 500, 500, "统计 Kiro 失败: "+err.Error()) + return + } + + dayKeys := make([]string, 0, n) + dayLabels := make([]string, 0, n) + cursorVals := make([]int64, 0, n) + windsurfVals := make([]int64, 0, n) + kiroVals := make([]int64, 0, n) + + for i := 0; i < n; i++ { + d := firstDay.AddDate(0, 0, i) + key := d.Format("2006-01-02") + dayKeys = append(dayKeys, key) + dayLabels = append(dayLabels, d.Format("01/02")) + cursorVals = append(cursorVals, mCursor[key]) + windsurfVals = append(windsurfVals, mWindsurf[key]) + kiroVals = append(kiroVals, mKiro[key]) + } + + c.Data["json"] = map[string]interface{}{ + "code": 200, + "msg": "success", + "data": map[string]interface{}{ + "days": dayLabels, + "dayKeys": dayKeys, + "cursor": int64SliceToInt(cursorVals), + "windsurf": int64SliceToInt(windsurfVals), + "kiro": int64SliceToInt(kiroVals), + "daysLength": n, + }, + } + _ = c.ServeJSON() +} + +func int64SliceToInt(in []int64) []int { + out := make([]int, len(in)) + for i, v := range in { + out[i] = int(v) + } + return out +} diff --git a/routers/platform/platform.go b/routers/platform/platform.go index 36ca890..3783a3e 100644 --- a/routers/platform/platform.go +++ b/routers/platform/platform.go @@ -158,6 +158,9 @@ func Register() { beego.Router("/platform/qiniu/token", &controllers.QiniuUploadController{}, "get:GetUploadToken") beego.Router("/platform/qiniu/save", &controllers.QiniuUploadController{}, "post:SaveFileRecord") + // 首页统计 + beego.Router("/platform/home/accountPoolDailyExtract", &controllers.PlatformHomeController{}, "get:AccountPoolDailyExtract") + // 账号池管理(cursor/windsurf/krio) beego.Router("/platform/accountPool/cursor/list", &controllers.PlatformAccountPoolCursorController{}, "get:List") beego.Router("/platform/accountPool/cursor/add", &controllers.PlatformAccountPoolCursorController{}, "post:Add") diff --git a/server.exe b/server.exe index 243db64..b2cc4f5 100644 Binary files a/server.exe and b/server.exe differ