beancount-gs/script/bql.go

978 lines
29 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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())
}
}