package qq import ( "bytes" "encoding/json" "fmt" "io" "net/http" "regexp" "strconv" "strings" "time" "ops-assistant/models" ) type subSessionPayload struct { Step string `json:"step"` Temp map[string]string `json:"temp"` } type subAPISettings struct { APIBase string Token string } const ( subStepName = "WAIT_NAME" subStepCategory = "WAIT_CATEGORY" subStepAcct = "WAIT_ACCOUNT" subStepAmount = "WAIT_AMOUNT" subStepCurrency = "WAIT_CURRENCY" subStepDate = "WAIT_DATE" subStepCycleUnit = "WAIT_CYCLE_UNIT" subStepCycleValue = "WAIT_CYCLE_VALUE" ) var dateRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) func (b *QQBot) handleSubFlow(userID string, text string) (string, bool) { raw := strings.TrimSpace(text) if !strings.HasPrefix(raw, "/sub") { if raw == "/cancel" { if b.hasSubSession(userID) { b.clearSubSession(userID) return "✅ 已取消订阅新增流程", true } return "", false } if !b.hasSubSession(userID) { return "", false } } else { if raw == "/sub" || strings.HasPrefix(raw, "/sub help") { if b.opsSvc != nil { if handled, out := b.opsSvc.Handle(DefaultUserID, raw); handled { return out, true } } return "订阅模块\n- /sub add\n- /sub list\n- /sub report\n- /sub report category\n- /sub report cycle\n- /sub report due <7|30|overdue>\n- /sub del \n- /sub cancel(或 /cancel)取消新增流程", true } if raw == "/sub cancel" { if b.hasSubSession(userID) { b.clearSubSession(userID) return "✅ 已取消订阅新增流程", true } return "", true } if raw == "/sub add" || strings.HasPrefix(raw, "/sub add ") { b.setSubSession(userID, &subSessionPayload{Step: subStepName, Temp: map[string]string{}}) return "1️⃣ 输入订阅名称(例如:Netflix)", true } return "", false } sess := b.getSubSession(userID) if sess == nil { return "", false } msg := raw if msg == "" { return "", true } if sess.Temp == nil { sess.Temp = map[string]string{} } switch sess.Step { case subStepName: sess.Temp["name"] = msg sess.Step = subStepCategory b.setSubSession(userID, sess) return "2️⃣ 输入分类(例如:通信/视频/游戏/工具/其他,可 /skip=未分类)", true case subStepCategory: if msg == "/skip" { sess.Temp["category"] = "未分类" } else { sess.Temp["category"] = msg } if strings.TrimSpace(sess.Temp["category"]) == "" { return "❌ 分类不能为空,请重新输入(可 /skip)", true } sess.Step = subStepAcct b.setSubSession(userID, sess) return "3️⃣ 输入账号/备注(可选,回复 /skip 跳过)", true case subStepAcct: if msg == "/skip" { sess.Temp["account"] = "" } else { sess.Temp["account"] = msg } sess.Step = subStepAmount b.setSubSession(userID, sess) return "4️⃣ 输入金额(数字,例如 9.99)", true case subStepAmount: amt, err := strconv.ParseFloat(msg, 64) if err != nil || amt < 0 { return "❌ 金额格式错误,请输入数字(例如 9.99)", true } sess.Temp["amount"] = fmt.Sprintf("%.2f", amt) sess.Step = subStepCurrency b.setSubSession(userID, sess) return "5️⃣ 输入币种(CNY/USD/EUR/GBP/HKD/JPY/MYR/SGD,或 /skip 默认 CNY)", true case subStepCurrency: cur := strings.ToUpper(msg) if msg == "/skip" { cur = "CNY" } if !isAllowedCurrency(cur) { return "❌ 币种不支持,请输入 CNY/USD/EUR/GBP/HKD/JPY/MYR/SGD", true } sess.Temp["currency"] = cur sess.Step = subStepDate b.setSubSession(userID, sess) return "6️⃣ 输入到期日(YYYY-MM-DD)", true case subStepDate: if !dateRe.MatchString(msg) { return "❌ 日期格式错误,请用 YYYY-MM-DD", true } sess.Temp["due_date"] = msg sess.Step = subStepCycleUnit b.setSubSession(userID, sess) return "7️⃣ 输入周期单位(day/month/year)", true case subStepCycleUnit: unit := strings.ToLower(msg) if !isAllowedCycleUnit(unit) { return "❌ 周期单位不支持,请输入 day/month/year", true } sess.Temp["cycle_unit"] = unit sess.Step = subStepCycleValue b.setSubSession(userID, sess) return "8️⃣ 输入周期数量(数字)", true case subStepCycleValue: cv, err := strconv.Atoi(msg) if err != nil || cv <= 0 { return "❌ 周期数量必须是正整数", true } sess.Temp["cycle_value"] = strconv.Itoa(cv) b.clearSubSession(userID) return b.subCreate(sess) default: b.clearSubSession(userID) return "❌ 会话已重置,请重新发送 /sub add", true } } func (b *QQBot) subCreate(sess *subSessionPayload) (string, bool) { cfg, err := b.loadSubSettings() if err != nil { return "❌ 未配置 sub_api_base/sub_api_token", true } payload := map[string]any{ "user_id": "1", "channel": "qq", "name": sess.Temp["name"], "category": sess.Temp["category"], "account": sess.Temp["account"], "amount": toFloat(sess.Temp["amount"]), "currency": sess.Temp["currency"], "due_date": sess.Temp["due_date"], "cycle_unit": sess.Temp["cycle_unit"], "cycle_value": toInt(sess.Temp["cycle_value"]), } body, _ := json.Marshal(payload) url := strings.TrimRight(cfg.APIBase, "/") + "/api/subscriptions" resp, status, err := doSubRequest(http.MethodPost, url, body, cfg.Token) if err != nil { return "❌ 创建失败: " + err.Error(), true } if status >= 400 { return fmt.Sprintf("❌ 创建失败: http %d %s", status, strings.TrimSpace(resp)), true } return "✅ 添加成功", true } func (b *QQBot) loadSubSettings() (*subAPISettings, error) { if b.db == nil { return nil, fmt.Errorf("db nil") } var base models.AppSetting if err := b.db.Where("key = ?", "sub_api_base").First(&base).Error; err != nil { return nil, err } var token models.AppSetting if err := b.db.Where("key = ?", "sub_api_token").First(&token).Error; err != nil { return nil, err } apiBase := strings.TrimSpace(base.Value) apiToken := strings.TrimSpace(token.Value) if apiBase == "" || apiToken == "" { return nil, fmt.Errorf("empty settings") } return &subAPISettings{APIBase: apiBase, Token: apiToken}, nil } func (b *QQBot) hasSubSession(userID string) bool { if b.db == nil { return false } var st models.SubSession if err := b.db.Where("user_id = ? AND platform = ?", userID, "qqbot_official").First(&st).Error; err != nil { return false } return strings.TrimSpace(st.Step) != "" } func (b *QQBot) getSubSession(userID string) *subSessionPayload { if b.db == nil { return nil } var st models.SubSession if err := b.db.Where("user_id = ? AND platform = ?", userID, "qqbot_official").First(&st).Error; err != nil { return nil } var payload subSessionPayload if err := json.Unmarshal([]byte(st.TempJSON), &payload.Temp); err != nil { payload.Temp = map[string]string{} } payload.Step = st.Step return &payload } func (b *QQBot) setSubSession(userID string, sess *subSessionPayload) { if b.db == nil { return } tmp, _ := json.Marshal(sess.Temp) var row models.SubSession if err := b.db.Where("user_id = ? AND platform = ?", userID, "qqbot_official").First(&row).Error; err == nil { row.Step = sess.Step row.TempJSON = string(tmp) _ = b.db.Save(&row).Error return } _ = b.db.Create(&models.SubSession{ UserID: userID, Platform: "qqbot_official", Step: sess.Step, TempJSON: string(tmp), }).Error } func (b *QQBot) clearSubSession(userID string) { if b.db == nil { return } _ = b.db.Where("user_id = ? AND platform = ?", userID, "qqbot_official").Delete(&models.SubSession{}).Error } func doSubRequest(method, url string, body []byte, token string) (string, int, error) { req, err := http.NewRequest(method, url, bytes.NewReader(body)) if err != nil { return "", 0, err } if token != "" { req.Header.Set("Authorization", "Bearer "+token) } if body != nil { req.Header.Set("Content-Type", "application/json") } client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(req) if err != nil { return "", 0, err } defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) return string(data), resp.StatusCode, nil } func toInt(s string) int { if s == "" { return 0 } v, _ := strconv.Atoi(s) return v } func toFloat(s string) float64 { if s == "" { return 0 } v, _ := strconv.ParseFloat(s, 64) return v } func isAllowedCurrency(c string) bool { switch strings.ToUpper(c) { case "CNY", "USD", "EUR", "GBP", "HKD", "JPY", "MYR", "SGD": return true default: return false } } func isAllowedCycleUnit(u string) bool { switch strings.ToLower(u) { case "day", "month", "year": return true default: return false } }