feat: 模块化命令框架 + runbook 严格断言与查询结果回传
- 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 任务、渠道管理、审计页
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user