package script import ( "bytes" "encoding/csv" "encoding/json" "fmt" "log" "os" "os/exec" "path/filepath" "reflect" "runtime" "strconv" "strings" "github.com/gin-gonic/gin" ) // 调试模式下的错误处理 // 修改 handleError 函数中的提示 // handleError 处理错误,根据调试模式决定处理方式: // - 调试模式下 panic 抛出错误 // - 生产环境下记录警告日志 // 参数: // // err: 错误对象 // message: 自定义错误信息 func handleError(err error, message string) { if IsDebugMode() { panic(fmt.Sprintf("%s: %v", message, err)) } else { // 生产环境下记录警告日志 log.Printf("警告: %s: %v", message, err) } } /* QueryParams 结构体定义了查询参数,包含多个字段用于构建查询条件。 每个字段都带有 bql 标签,用于指定在 BQL 查询中的对应字段名。 */ type QueryParams struct { From bool `bql:"From"` // 是否包含 From 子句 FromYear int `bql:"year ="` // From 子句中的年份条件 FromMonth int `bql:"month ="` // From 子句中的月份条件 Where bool `bql:"where"` // 是否包含 Where 子句 ID string `bql:"id ="` // ID 等于条件 IDList string `bql:"id in"` // ID 列表条件 Currency string `bql:"currency ="` // 货币等于条件 Year int `bql:"year ="` // 年份等于条件 Month int `bql:"month ="` // 月份等于条件 Tag string `bql:"in tags"` // 用于 tag in tags 条件 TagNotNull string `bql:"tags IS NOT NULL"` // 标签非空条件 Account string `bql:"account ="` // 账户等于条件 AccountLike string `bql:"account ~"` // 账户模糊匹配条件 StrictAccountMatch bool // true表示账户必须使用精确匹配,false表示使用模糊匹配 GroupBy string `bql:"group by"` // 分组条件 OrderBy string `bql:"order by"` // 排序条件 Limit int `bql:"limit"` // 限制结果数量 Path string // 查询路径 Offset int // 查询偏移量 DateRange bool // 启用日期范围查询 } // HasConditions 检查查询参数是否包含任何条件 // 返回 true 如果 Year, Month, Account, AccountLike, Tag, ID 或 Currency 任一字段不为零值 func (queryParams *QueryParams) HasConditions() bool { return queryParams.Year != 0 || queryParams.Month != 0 || queryParams.Account != "" || queryParams.AccountLike != "" || queryParams.Tag != "" || queryParams.ID != "" || queryParams.Currency != "" } // GetQueryParams 从 gin.Context 中解析查询参数并返回 QueryParams 结构体 // 支持以下查询参数: // - year/month: 数值型参数,用于时间范围筛选 // - tag/account/type/id/idList/currency/path: 字符串型参数 // - groupBy/orderBy: 分组和排序参数 // - limit: 限制返回结果数量 // - strictMode: 布尔值,控制账户匹配模式(精确/模糊) // // 默认值: // - StrictAccountMatch: true (精确匹配) // - OrderBy: "date desc" // - Limit: 100 // - Year/Month: 0 (表示不限制) // // 注意:所有参数都会经过有效性检查(非空且不为"undefined"/"null") func GetQueryParams(c *gin.Context) QueryParams { queryParams := QueryParams{ StrictAccountMatch: true, OrderBy: "date desc", Limit: 100, } // 辅助函数检查有效值 isValidParam := func(val string) bool { return val != "" && val != "undefined" && val != "null" && val != "None" } // 数值型条件 if year := c.Query("year"); isValidParam(year) { if val, err := strconv.Atoi(year); err == nil && val > 0 { queryParams.Year = val DebugLogWithContext("GetQueryParams", "设置有效年份: %d", val) } else { WarnLogWithContext("GetQueryParams", "无效年份参数: %s", year) } } if month := c.Query("month"); isValidParam(month) { if val, err := strconv.Atoi(month); err == nil && val > 0 && val <= 12 { queryParams.Month = val DebugLogWithContext("GetQueryParams", "设置有效月份: %d", val) } else { WarnLogWithContext("GetQueryParams", "无效月份参数: %s", month) } } // 字符串条件 setStringParam := func(param *string, queryKey string) { if val := c.Query(queryKey); isValidParam(val) { *param = val DebugLogWithContext("GetQueryParams", "设置%s: %s", queryKey, val) } } setStringParam(&queryParams.Tag, "tag") setStringParam(&queryParams.Account, "account") setStringParam(&queryParams.ID, "id") setStringParam(&queryParams.IDList, "idList") setStringParam(&queryParams.Currency, "currency") setStringParam(&queryParams.Path, "path") setStringParam(&queryParams.GroupBy, "groupBy") // 特殊处理type参数 if accType := c.Query("type"); isValidParam(accType) { queryParams.AccountLike = accType queryParams.StrictAccountMatch = false DebugLogWithContext("GetQueryParams", "设置账户类型(模糊匹配): %s", accType) } // 处理排序参数 if orderBy := c.Query("orderBy"); isValidParam(orderBy) { queryParams.OrderBy = orderBy } else if queryParams.Year > 0 || queryParams.Month > 0 { queryParams.OrderBy = "year desc, month desc" } // 处理limit参数 if limit := c.Query("limit"); isValidParam(limit) { if val, err := strconv.Atoi(limit); err == nil && val > 0 { queryParams.Limit = val } } // 处理严格模式参数 if strictMode := c.Query("strictMode"); isValidParam(strictMode) { if strict, err := strconv.ParseBool(strictMode); err == nil { queryParams.StrictAccountMatch = strict DebugLogWithContext("GetQueryParams", "设置严格模式: %t", strict) } } // 验证必要参数 if queryParams.Year == 0 && queryParams.Month == 0 && queryParams.Account == "" && queryParams.AccountLike == "" { WarnLogWithContext("GetQueryParams", "缺少必要查询参数") } queryParams.Where = queryParams.HasConditions() return queryParams } //func BQLQueryOne(ledgerConfig *Config, queryParams *QueryParams, queryResultPtr interface{}) error { // assertQueryResultIsPointer(queryResultPtr) // output, err := bqlRawQuery(ledgerConfig, "", queryParams, queryResultPtr) // if err != nil { // return err // } // err = parseResult(output, queryResultPtr, true) // if err != nil { // return err // } // return nil //} func BQLPrint(ledgerConfig *Config, transactionId string) (string, error) { // PRINT FROM id = 'xxx' output, err := queryByBQL(ledgerConfig, "PRINT FROM id = '"+transactionId+"'") if err != nil { return "", err } utf8, err := ConvertGBKToUTF8(output) if err != nil { return "", err } return utf8, nil } // BQLQueryList 执行BQL查询并将结果解析到指定的指针中 // // 参数: // // ledgerConfig: 账本配置信息 // queryParams: 查询参数 // queryResultPtr: 指向查询结果容器的指针,必须是指针类型 // // 返回值: // // error: 如果查询或解析过程中发生错误,返回错误信息 // // 注意: // 1. 函数内部包含详细的调试日志,可通过调试模式控制 // 2. queryResultPtr 必须是指针类型,否则会触发断言错误 // 3. 输出结果会被自动解析到queryResultPtr指向的变量中 func BQLQueryList(ledgerConfig *Config, queryParams *QueryParams, queryResultPtr interface{}) error { // 调试模式设置,默认为true(开启调试模式) // 调试信息:函数开始执行 LogDebugDetailed(ledgerConfig.Mail, "BQLQueryList", "函数开始执行\n----------------------\n输入查询参数: %+v\n----------------------\nqueryResultPtr 类型: %T", queryParams, queryResultPtr) assertQueryResultIsPointer(queryResultPtr) // 调试信息:执行bqlRawQuery前 LogDebugDetailed(ledgerConfig.Mail, "BQLQuery", "正在执行 bqlRawQuery...") output, err := bqlRawQuery(ledgerConfig, "", queryParams, queryResultPtr) // 调试信息:bqlRawQuery执行结果 if err != nil { LogError(ledgerConfig.Mail, fmt.Sprintf("bqlRawQuery执行失败: %v", err)) return fmt.Errorf("BQL查询失败: %v", err) } else { // 限制输出长度,避免日志过大 outputPreview := output if len(output) > 500 { outputPreview = output[:500] + "... (输出被截断)" } LogDebugDetailed(ledgerConfig.Mail, "BQLQuery", "bqlRawQuery执行成功,输出预览: %s", outputPreview) } // 调试信息:执行parseResult前 LogDebugDetailed(ledgerConfig.Mail, "BQLQuery", "正在使用parseResult解析查询结果...") parseErr := parseResult(output, queryResultPtr, false) // 调试信息:parseResult执行结果 if parseErr != nil { LogDebugDetailed(ledgerConfig.Mail, "BQLParse", "parseResult解析失败: %v", parseErr) } else { resultValue := reflect.ValueOf(queryResultPtr).Elem() logMessage := fmt.Sprintf("parseResult解析成功\n结果类型: %s\n种类: %s", resultValue.Type().String(), resultValue.Kind().String()) if resultValue.Kind() == reflect.Slice { logMessage += fmt.Sprintf("\n结果包含 %d 个项目", resultValue.Len()) } LogDebugDetailed(ledgerConfig.Mail, "BQLParse", logMessage) } return parseErr } func BQLQueryListByCustomSelect(ledgerConfig *Config, selectBql string, queryParams *QueryParams, queryResultPtr interface{}) error { const contextTag = "QueryListByCustomSelect" // 调试信息:函数开始执行 LogBQLQueryDebug(ledgerConfig.Mail, contextTag, "函数开始执行\n自定义 selectBql: %s\n输入查询参数: %+v\nqueryResultPtr 类型: %T", selectBql, queryParams, queryResultPtr) assertQueryResultIsPointer(queryResultPtr) // 调试信息:执行bqlRawQuery前 LogBQLQueryDebug(ledgerConfig.Mail, contextTag, "正在执行 bqlRawQuery...") output, err := bqlRawQuery(ledgerConfig, selectBql, queryParams, queryResultPtr) // 调试信息:bqlRawQuery执行结果 if err != nil { LogBQLQueryDebug(ledgerConfig.Mail, contextTag, "bqlRawQuery 执行失败: %v", err) } else { // 限制输出长度,避免日志过大 outputPreview := output if len(output) > 500 { outputPreview = output[:500] + "... (输出被截断)" } LogBQLQueryDebug(ledgerConfig.Mail, contextTag, "bqlRawQuery 执行成功,输出预览: %s", outputPreview) } if err != nil { errorMsg := fmt.Sprintf("BQL:%s - 自定义 BQL 查询失败: %v", contextTag, err) LogError(ledgerConfig.Mail, errorMsg) return fmt.Errorf("自定义 BQL 查询失败: %v", err) } // 调试信息:执行parseResult前 LogBQLQueryDebug(ledgerConfig.Mail, contextTag, "正在调用parseResult解析查询结果...") parseErr := parseResult(output, queryResultPtr, false) // 调试信息:parseResult执行结果 if parseErr != nil { LogBQLQueryDebug(ledgerConfig.Mail, contextTag, "parseResult 解析失败: %v", parseErr) } else { resultValue := reflect.ValueOf(queryResultPtr).Elem() LogBQLQueryDebug(ledgerConfig.Mail, contextTag, "parseResult 解析成功\n结果类型: %s\n种类: %s", resultValue.Type().String(), resultValue.Kind().String()) if resultValue.Kind() == reflect.Slice { LogBQLQueryDebug(ledgerConfig.Mail, contextTag, "结果包含 %d 个项目", resultValue.Len()) } } return parseErr } func bqlRawQuery(ledgerConfig *Config, selectBql string, queryParamsPtr *QueryParams, queryResultPtr interface{}) (string, error) { LogDebugDetailed(ledgerConfig.Mail, "BQLBuilder", "=== 开始构建BQL查询 ===\n输入参数: selectBql='%s'\nqueryParamsPtr=%+v", selectBql, queryParamsPtr) var bql strings.Builder // 1. SELECT 部分 if selectBql == "" { LogDebugDetailed(ledgerConfig.Mail, "BQLBuilder", "自动生成SELECT字段...") bql.WriteString("SELECT ") queryResultPtrType := reflect.TypeOf(queryResultPtr) queryResultType := queryResultPtrType.Elem() if queryResultType.Kind() == reflect.Slice { queryResultType = queryResultType.Elem() } first := true for i := 0; i < queryResultType.NumField(); i++ { typeField := queryResultType.Field(i) if b := typeField.Tag.Get("bql"); b != "" { if !first { bql.WriteString(", ") } if strings.Contains(b, "distinct") { b = strings.ReplaceAll(b, "distinct", "") bql.WriteString("DISTINCT ") } bql.WriteString(b) // 保留反斜线作为分隔符 bql.WriteString(", '\\'") first = false } } LogDebugDetailed(ledgerConfig.Mail, "BQLBuilder", "生成的SELECT部分: %s", bql.String()) } else { bql.WriteString(selectBql) } // 2. 记录WHERE条件构建 if queryParamsPtr != nil { LogDebugDetailed(ledgerConfig.Mail, "BQLBuilder", "正在解析和构建WHERE条件子句...") // queryParamsType := reflect.TypeOf(queryParamsPtr).Elem() // queryParamsValue := reflect.ValueOf(queryParamsPtr).Elem() hasConditions := false // firstCondition := true // 检查是否有实际条件 if (queryParamsPtr.Year != 0 || queryParamsPtr.Month != 0) || // 时间条件 (queryParamsPtr.AccountLike != "" || queryParamsPtr.Tag != "") || // 账户/标签条件 (queryParamsPtr.ID != "" || queryParamsPtr.Currency != "") { // ID/货币条件 hasConditions = true } if hasConditions { // 账户匹配方式验证 if queryParamsPtr.Account != "" && queryParamsPtr.AccountLike != "" { LogError(ledgerConfig.Mail, fmt.Sprintf("参数冲突: Account='%s' 和 AccountLike='%s' 不能同时指定", queryParamsPtr.Account, queryParamsPtr.AccountLike)) return "", fmt.Errorf("不能同时指定Account和AccountLike参数") } if queryParamsPtr.StrictAccountMatch && queryParamsPtr.AccountLike != "" { LogError(ledgerConfig.Mail, fmt.Sprintf("参数冲突: StrictAccountMatch=%v 但指定了AccountLike='%s'", queryParamsPtr.StrictAccountMatch, queryParamsPtr.AccountLike)) return "", fmt.Errorf("StrictAccountMatch模式下不能使用AccountLike") } if !queryParamsPtr.StrictAccountMatch && queryParamsPtr.Account != "" { LogError(ledgerConfig.Mail, fmt.Sprintf("参数冲突: StrictAccountMatch=%v 但指定了Account='%s' (应使用AccountLike)", queryParamsPtr.StrictAccountMatch, queryParamsPtr.Account)) return "", fmt.Errorf("非StrictAccountMatch模式下必须指定AccountLike") } LogDebugDetailed(ledgerConfig.Mail, "BQLBuilder", "构建前SQL: %s", bql.String()) bql.WriteString(" WHERE ") firstCondition := true // 辅助函数添加条件 addCondition := func(condition string) { if !firstCondition { bql.WriteString(" AND ") } bql.WriteString(condition) firstCondition = false } // 时间条件 if queryParamsPtr.Year != 0 { addCondition(fmt.Sprintf("year = %d", queryParamsPtr.Year)) } if queryParamsPtr.Month != 0 { addCondition(fmt.Sprintf("month = %d", queryParamsPtr.Month)) } // 账户条件 if queryParamsPtr.Account != "" { addCondition(fmt.Sprintf("account = '%s'", escapeSQLString(queryParamsPtr.Account))) } else if queryParamsPtr.AccountLike != "" { addCondition(fmt.Sprintf("account ~ '%s'", escapeSQLString(queryParamsPtr.AccountLike))) } // 其他条件 if queryParamsPtr.Tag != "" { addCondition("tag in tags") } if queryParamsPtr.TagNotNull != "" { addCondition("tags IS NOT NULL") } if queryParamsPtr.ID != "" { addCondition(fmt.Sprintf("id = '%s'", escapeSQLString(queryParamsPtr.ID))) } LogDebugDetailed(ledgerConfig.Mail, "BQLBuilder", "构建后SQL: %s", bql.String()) } } // 在构建GROUP BY子句前添加验证 if queryParamsPtr != nil && queryParamsPtr.GroupBy != "" { // 检查是否有聚合函数 hasAggregate := strings.Contains(bql.String(), "sum(") || strings.Contains(bql.String(), "count(") || strings.Contains(bql.String(), "avg(") || strings.Contains(bql.String(), "min(") || strings.Contains(bql.String(), "max(") if hasAggregate { LogDebugDetailed(ledgerConfig.Mail, "BQLBuilder-GroupBy", "BQL查询包含聚合函数,将添加GROUP BY子句") bql.WriteString(" GROUP BY ") bql.WriteString(queryParamsPtr.GroupBy) } else { LogDebugDetailed(ledgerConfig.Mail, "BQLBuilder-GroupBy", "BQL查询不包含聚合函数,但是指定了GROUP BY子句,将仍然添加GROUP BY子句") bql.WriteString(" GROUP BY ") bql.WriteString(queryParamsPtr.GroupBy) } } // 构建 ORDER BY 子句 if queryParamsPtr != nil && queryParamsPtr.OrderBy != "" { bql.WriteString(" ORDER BY ") orderBy := strings.ReplaceAll(queryParamsPtr.OrderBy, "'", "") orderBy = strings.ReplaceAll(orderBy, "\"", "") orderBy = strings.ReplaceAll(strings.ToLower(orderBy), "order by", "") orderBy = strings.TrimSpace(orderBy) bql.WriteString(orderBy) } // 构建 LIMIT 子句 if queryParamsPtr != nil && queryParamsPtr.Limit > 0 { bql.WriteString(" LIMIT ") bql.WriteString(strconv.Itoa(queryParamsPtr.Limit)) } finalQuery := bql.String() LogDebugDetailed(ledgerConfig.Mail, "BQLGenerator", "=== 最终生成的BQL ===\n%s", finalQuery) if strings.Contains(finalQuery, "WHERE") && strings.Contains(finalQuery, "GROUP BY") { if strings.Index(finalQuery, "WHERE") > strings.Index(finalQuery, "GROUP BY") { return "", fmt.Errorf("SQL语法错误: WHERE子句必须在GROUP BY之前") } } // 验证生成的SQL if strings.Contains(finalQuery, "WHERE AND") { LogError(ledgerConfig.Mail, "!!! 检测到非法WHERE条件: "+finalQuery) return "", fmt.Errorf("invalid WHERE clause") } LogDebugDetailed(ledgerConfig.Mail, "BQLGenerator", "=== 最终生成的BQL语句 ===\n%s", finalQuery) return queryByBQL(ledgerConfig, finalQuery) } // 辅助函数:安全转义 SQL 字符串 func escapeSQLString(s string) string { // 只转义单引号,不处理反斜杠 return strings.ReplaceAll(s, "'", "''") } // 修改 BeanReportAllPrices 函数 func BeanReportAllPrices(ledgerConfig *Config) []CommodityPrice { // 使用正确的 BQL 查询,不需要 FROM 子句 output, err := queryByBQL(ledgerConfig, "SELECT date, 'price', currency, price WHERE price is not NULL") if err != nil { LogError(ledgerConfig.Mail, "Failed to query prices: "+err.Error()) return nil } // 解析 CSV 输出 reader := csv.NewReader(strings.NewReader(output)) records, err := reader.ReadAll() if err != nil { LogError(ledgerConfig.Mail, "Failed to parse CSV: "+err.Error()) return nil } // 将 [][]string 转换为 []string var lines []string if len(records) > 0 { // 跳过标题行 for _, record := range records[1:] { lines = append(lines, strings.Join(record, " ")) } } return newCommodityPriceListFromString(lines) } // 修改 parseResult 函数中的相关部分 func parseCsvResult(output string, queryResultPtr interface{}, selectOne bool) error { queryResultPtrType := reflect.TypeOf(queryResultPtr) queryResultType := queryResultPtrType.Elem() if queryResultType.Kind() == reflect.Slice { queryResultType = queryResultType.Elem() } // 使用 csv 解析器处理输出 reader := csv.NewReader(strings.NewReader(output)) records, err := reader.ReadAll() if err != nil { return err } // 跳过标题行 if len(records) > 0 { records = records[1:] } if selectOne && len(records) > 0 { records = records[:1] } l := make([]map[string]interface{}, 0) for _, record := range records { if len(record) == 0 { continue } temp := make(map[string]interface{}) for i, val := range record { if i >= queryResultType.NumField() { continue } field := queryResultType.Field(i) jsonName := field.Tag.Get("json") if jsonName == "" { jsonName = field.Name } val = strings.TrimSpace(val) if val == "" { continue } switch field.Type.Kind() { case reflect.Int, reflect.Int32: if i, err := strconv.Atoi(val); err == nil { temp[jsonName] = i } case reflect.String: temp[jsonName] = val case reflect.Float32, reflect.Float64: if f, err := strconv.ParseFloat(val, 64); err == nil { temp[jsonName] = f } case reflect.Array, reflect.Slice: strArray := strings.Split(val, ",") notBlanks := make([]string, 0) for _, s := range strArray { if s = strings.TrimSpace(s); s != "" { notBlanks = append(notBlanks, s) } } if len(notBlanks) > 0 { temp[jsonName] = notBlanks } } } if len(temp) > 0 { l = append(l, temp) } } var jsonBytes []byte var jsonErr error // 修改变量名,避免重复声明 if selectOne && len(l) > 0 { jsonBytes, jsonErr = json.Marshal(l[0]) } else { jsonBytes, jsonErr = json.Marshal(l) } if jsonErr != nil { return jsonErr } err = json.Unmarshal(jsonBytes, queryResultPtr) // 使用外层的 err if err != nil { return err } return nil } // 原v2版本格式数据导入函数 // 主解析函数 - 智能识别格式 func parseResult(output string, queryResultPtr interface{}, selectOne bool) error { lines := strings.Split(strings.TrimSpace(output), "\n") if len(lines) == 0 { return nil // 空输出 } // 检测格式类型 isCustomFormat := false for _, line := range lines { if strings.Contains(line, "\\") || strings.Contains(line, "'") { isCustomFormat = true break } if IsDebugMode() { log.Printf("line: %s", line) log.Println("isCustomFormat:", isCustomFormat) } } if isCustomFormat { if IsDebugMode() { log.Println("使用自定义分隔符//解析逻辑") } // 使用自定义分隔符解析逻辑 return parseCustomFormat(output, queryResultPtr, selectOne) } else { if IsDebugMode() { log.Println("使用表格格式解析逻辑") } // 使用表格格式解析逻辑 return parseTableFormat(output, queryResultPtr, selectOne) } } // 解析自定义分隔符格式(原v2格式) func parseCustomFormat(output string, queryResultPtr interface{}, selectOne bool) error { queryResultPtrType := reflect.TypeOf(queryResultPtr) queryResultType := queryResultPtrType.Elem() if queryResultType.Kind() == reflect.Slice { queryResultType = queryResultType.Elem() } lines := strings.Split(output, "\n") // 跳过标题行(如果有) var dataLines []string if len(lines) >= 3 && strings.Contains(lines[1], "-") { dataLines = lines[2:] // 跳过前2行(标题和分隔线) } else { dataLines = lines // 没有标准标题格式 } l := make([]map[string]interface{}, 0) for _, line := range dataLines { line = strings.TrimSpace(line) if line == "" { continue } values := strings.Split(line, "\\") // 安全地去除首尾空元素 var cleanedValues []string for _, val := range values { trimmed := strings.TrimSpace(val) if trimmed != "" { cleanedValues = append(cleanedValues, trimmed) } } // 如果cleanedValues为空,跳过这行 if len(cleanedValues) == 0 { continue } temp := make(map[string]interface{}) for i, val := range cleanedValues { if i >= queryResultType.NumField() { break // 跳过多余的字段 } field := queryResultType.Field(i) jsonName := field.Tag.Get("json") if jsonName == "" { jsonName = field.Name } val = strings.TrimSpace(val) if val == "" { continue } // 添加tags/payee字段的专门调试 if jsonName == "tags" && IsDebugMode() { DebugLogWithContext("TAGS", "解析tags字段值: %s", val) } if jsonName == "payee" && IsDebugMode() { // DebugLogWithContext("PAYEE", "解析payee字段值: %s", val) } switch field.Type.Kind() { case reflect.Int, reflect.Int32: if intVal, err := strconv.Atoi(val); err != nil { handleError(err, fmt.Sprintf("解析整数值 '%s' 失败", val)) } else { temp[jsonName] = intVal } case reflect.String, reflect.Struct: temp[jsonName] = val case reflect.Array, reflect.Slice: strArray := strings.Split(val, ",") notBlanks := make([]string, 0) for _, s := range strArray { if trimmed := strings.TrimSpace(s); trimmed != "" { notBlanks = append(notBlanks, trimmed) } } if len(notBlanks) > 0 { temp[jsonName] = notBlanks } default: if IsDebugMode() { panic(fmt.Sprintf("Unsupported field type: %s", field.Type.Kind())) } } } if len(temp) > 0 { l = append(l, temp) } } return marshalAndUnmarshal(l, queryResultPtr, selectOne) } // 解析表格格式(bean-query默认格式) func parseTableFormat(output string, queryResultPtr interface{}, selectOne bool) error { queryResultPtrType := reflect.TypeOf(queryResultPtr) queryResultType := queryResultPtrType.Elem() if queryResultType.Kind() == reflect.Slice { queryResultType = queryResultType.Elem() } lines := strings.Split(strings.TrimSpace(output), "\n") // 检测并跳过标题行和分隔线 var dataLines []string if len(lines) >= 3 && strings.Contains(lines[1], "-") { dataLines = lines[2:] // 跳过前2行 } else if len(lines) >= 2 && strings.Contains(lines[0], "|") { dataLines = lines[1:] // 跳过标题行 } else { dataLines = lines // 没有标准标题 } l := make([]map[string]interface{}, 0) for _, line := range dataLines { line = strings.TrimSpace(line) if line == "" { continue } // 按 | 分割表格格式 values := strings.Split(line, "|") var cleanedValues []string for _, val := range values { trimmed := strings.TrimSpace(val) if trimmed != "" { cleanedValues = append(cleanedValues, trimmed) } } if len(cleanedValues) == 0 { continue } temp := make(map[string]interface{}) for i, val := range cleanedValues { if i >= queryResultType.NumField() { break } field := queryResultType.Field(i) jsonName := field.Tag.Get("json") if jsonName == "" { jsonName = field.Name } val = strings.TrimSpace(val) if val == "" { continue } switch field.Type.Kind() { case reflect.Int, reflect.Int32: if intVal, err := strconv.Atoi(val); err != nil { handleError(err, fmt.Sprintf("解析整数值 '%s' 失败", val)) } else { temp[jsonName] = intVal } case reflect.String, reflect.Struct: temp[jsonName] = val case reflect.Float32, reflect.Float64: if floatVal, err := strconv.ParseFloat(val, 64); err != nil { handleError(err, fmt.Sprintf("解析浮点数值 '%s' 失败", val)) } else { temp[jsonName] = floatVal } case reflect.Array, reflect.Slice: strArray := strings.Split(val, ",") notBlanks := make([]string, 0) for _, s := range strArray { if trimmed := strings.TrimSpace(s); trimmed != "" { notBlanks = append(notBlanks, trimmed) } } if len(notBlanks) > 0 { temp[jsonName] = notBlanks } default: if IsDebugMode() { panic(fmt.Sprintf("Unsupported field type: %s", field.Type.Kind())) } } } if len(temp) > 0 { l = append(l, temp) } } return marshalAndUnmarshal(l, queryResultPtr, selectOne) } // 通用的JSON序列化和反序列化 // 通用的JSON序列化和反序列化 func marshalAndUnmarshal(data []map[string]interface{}, queryResultPtr interface{}, selectOne bool) error { if len(data) == 0 { // 对于空结果,设置默认值 if selectOne { if IsDebugMode() { panic("selectOne 查询没有找到结果") } return fmt.Errorf("selectOne 查询没有找到结果") } // 对于切片,返回空切片是合理的 } var jsonBytes []byte var err error if selectOne { if len(data) == 0 { if IsDebugMode() { panic("selectOne 查询没有找到结果") } return fmt.Errorf("selectOne 查询没有找到结果") } jsonBytes, err = json.Marshal(data[0]) } else { jsonBytes, err = json.Marshal(data) } if err != nil { handleError(err, "JSON 序列化失败") return err } err = json.Unmarshal(jsonBytes, queryResultPtr) if err != nil { handleError(err, "JSON 反序列化失败") return err } return nil } // 直接调用v3版本的bean-query命令,返回beancount专用表格格式 func queryByBQL(ledgerConfig *Config, bql string) (string, error) { beanFilePath := ledgerConfig.DataPath + "/index.bean" LogInfo(ledgerConfig.Mail, fmt.Sprintf("[BQLExecution] 查询语句: %s", bql)) // 获取虚拟环境执行器 executor := GetVenvExecutor() if executor == nil { // 降级方案:使用原来的逻辑但修复路径 return queryByBQLFallback(beanFilePath, bql) } // 使用虚拟环境工具执行 bean-query output, err := executor.BeanQueryStdout(beanFilePath, bql) if err != nil { errorMsg := fmt.Sprintf("bean-query执行失败 - 错误详情: %v", err) LogError(ledgerConfig.Mail, errorMsg) return "", fmt.Errorf("bean-query 执行失败: %v", err) } return string(output), nil } // 降级方案,确保即使虚拟环境工具有问题也能工作 func queryByBQLFallback(beanFilePath, bql string) (string, error) { var cmdPath string if runtime.GOOS == "windows" { cmdPath = ".env_beancount-v3/Scripts/bean-query.exe" } else { cmdPath = ".env_beancount-v3/bin/bean-query" } // 检查文件是否存在 if _, err := os.Stat(cmdPath); os.IsNotExist(err) { return "", fmt.Errorf("bean-query 未找到: %s", cmdPath) } cmd := exec.Command(cmdPath, beanFilePath, bql) output, err := cmd.Output() if err != nil { return "", fmt.Errorf("bean-query 执行错误: %v", err) } return string(output), nil } func assertQueryResultIsPointer(queryResult interface{}) { k := reflect.TypeOf(queryResult).Kind() if k != reflect.Ptr { panic("QueryResult 类型必须是指针,当前是 " + k.String()) } }