- core/module: 命令模板、门控(feature flag/confirm/dry-run)、ecode 错误标签、ResultFormatter - runbook executor: http.get/http.post/assert.json(expect_success 严格断言)、目标主机管理、任务落库、最终 stdout 直接回传 - cf 模块: /cf dns list 支持域名自动解析 zone_id;dns list/zones/workers list 格式化回传;API success:false 判真失败 - 全部 CF/CPA runbook 接入严格断言,杜绝假阳性 success - 新增 cpa/cfdav/mail/sub 模块与 runbooks;QQ 订阅到期提醒调度 - Web 后台: 模块设置(CF/CPA/cfdav/AI)、OPS 任务、渠道管理、审计页
128 lines
3.5 KiB
Go
128 lines
3.5 KiB
Go
package sub
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
type reportSummaryResp struct {
|
||
Success bool `json:"success"`
|
||
Data struct {
|
||
Total int `json:"total"`
|
||
Currency []struct {
|
||
Currency string `json:"currency"`
|
||
Cnt int `json:"cnt"`
|
||
Total float64 `json:"total"`
|
||
} `json:"currency"`
|
||
Due struct {
|
||
Overdue int `json:"overdue"`
|
||
Due7 int `json:"due7"`
|
||
Due30 int `json:"due30"`
|
||
} `json:"due"`
|
||
} `json:"data"`
|
||
Error string `json:"error"`
|
||
}
|
||
|
||
type reportListResp struct {
|
||
Success bool `json:"success"`
|
||
Data []map[string]any `json:"data"`
|
||
Error string `json:"error"`
|
||
}
|
||
|
||
func (m *Module) report(userID int64, args []string) (string, error) {
|
||
cfg, err := m.loadSettings()
|
||
if err != nil {
|
||
return "❌ " + err.Error(), nil
|
||
}
|
||
typeName := "summary"
|
||
days := ""
|
||
if len(args) >= 1 {
|
||
typeName = strings.ToLower(strings.TrimSpace(args[0]))
|
||
}
|
||
if len(args) >= 2 {
|
||
days = strings.TrimSpace(args[1])
|
||
}
|
||
|
||
base := strings.TrimRight(cfg.APIBase, "/") + "/api/report?user_id=1&channel=qq&type=" + typeName
|
||
if days != "" {
|
||
base += "&days=" + days
|
||
}
|
||
body, status, err := m.doRequest(http.MethodGet, base, nil, cfg)
|
||
if err != nil {
|
||
return "❌ 报表请求失败: " + err.Error(), nil
|
||
}
|
||
if status >= 400 {
|
||
return fmt.Sprintf("❌ 报表失败: http %d %s", status, body), nil
|
||
}
|
||
|
||
switch typeName {
|
||
case "summary":
|
||
var out reportSummaryResp
|
||
if err := json.Unmarshal([]byte(body), &out); err != nil {
|
||
return "❌ 报表解析失败", nil
|
||
}
|
||
if !out.Success {
|
||
return "❌ " + out.Error, nil
|
||
}
|
||
lines := []string{fmt.Sprintf("📊 订阅总览:%d 项", out.Data.Total)}
|
||
if len(out.Data.Currency) > 0 {
|
||
lines = append(lines, "💰 按币种汇总:")
|
||
for _, c := range out.Data.Currency {
|
||
lines = append(lines, fmt.Sprintf("- %s: %.2f(%d项)", c.Currency, c.Total, c.Cnt))
|
||
}
|
||
}
|
||
lines = append(lines, fmt.Sprintf("⏰ 到期:7天内=%d,30天内=%d,已逾期=%d", out.Data.Due.Due7, out.Data.Due.Due30, out.Data.Due.Overdue))
|
||
return strings.Join(lines, "\n"), nil
|
||
case "category", "cycle", "due":
|
||
var out reportListResp
|
||
if err := json.Unmarshal([]byte(body), &out); err != nil {
|
||
return "❌ 报表解析失败", nil
|
||
}
|
||
if !out.Success {
|
||
return "❌ " + out.Error, nil
|
||
}
|
||
if len(out.Data) == 0 {
|
||
return "📭 无数据", nil
|
||
}
|
||
lines := []string{fmt.Sprintf("📊 报表(%s):%d 条", typeName, len(out.Data))}
|
||
max := len(out.Data)
|
||
if max > 30 {
|
||
max = 30
|
||
}
|
||
for i := 0; i < max; i++ {
|
||
r := out.Data[i]
|
||
switch typeName {
|
||
case "category":
|
||
lines = append(lines, fmt.Sprintf("- %v | %v | 数量:%v | 金额:%.2f", r["category"], r["currency"], r["cnt"], toF64(r["total"])))
|
||
case "cycle":
|
||
lines = append(lines, fmt.Sprintf("- %v x%v | 数量:%v", r["cycle_unit"], r["cycle_value"], r["cnt"]))
|
||
case "due":
|
||
lines = append(lines, fmt.Sprintf("- %v | %v | 分类:%v | 到期:%v | 周期:%v x%v | %.2f %v", r["id"], r["name"], r["category"], r["due_date"], r["cycle_unit"], r["cycle_value"], toF64(r["amount"]), r["currency"]))
|
||
}
|
||
}
|
||
if len(out.Data) > max {
|
||
lines = append(lines, fmt.Sprintf("... 共%d条,仅显示前%d条", len(out.Data), max))
|
||
}
|
||
return strings.Join(lines, "\n"), nil
|
||
default:
|
||
return "❌ 用法:/sub report | /sub report category | /sub report cycle | /sub report due <7|30|overdue>", nil
|
||
}
|
||
}
|
||
|
||
func toF64(v any) float64 {
|
||
switch t := v.(type) {
|
||
case float64:
|
||
return t
|
||
case string:
|
||
f, _ := strconv.ParseFloat(t, 64)
|
||
return f
|
||
case int:
|
||
return float64(t)
|
||
default:
|
||
return 0
|
||
}
|
||
}
|