beancount-gs/script/bql.go

978 lines
29 KiB
Go
Raw Normal View History

2021-11-21 14:37:13 +00:00
package script
import (
"bytes"
"encoding/csv"
2021-11-22 08:47:49 +00:00
"encoding/json"
2021-11-21 14:37:13 +00:00
"fmt"
"log"
"os"
2021-11-21 14:37:13 +00:00
"os/exec"
"path/filepath"
2021-11-21 14:37:13 +00:00
"reflect"
"runtime"
2021-11-22 14:50:10 +00:00
"strconv"
2021-11-21 14:37:13 +00:00
"strings"
"github.com/gin-gonic/gin"
2021-11-21 14:37:13 +00:00
)
// 调试模式下的错误处理
// 修改 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 查询中的对应字段名
*/
2021-11-21 14:37:13 +00:00
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 // 启用日期范围查询
2021-11-21 14:37:13 +00:00
}
// 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"
2021-11-22 14:50:10 +00:00
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 {
2021-11-22 14:50:10 +00:00
queryParams.Year = val
DebugLogWithContext("GetQueryParams", "设置有效年份: %d", val)
} else {
WarnLogWithContext("GetQueryParams", "无效年份参数: %s", year)
2021-11-22 14:50:10 +00:00
}
}
if month := c.Query("month"); isValidParam(month) {
if val, err := strconv.Atoi(month); err == nil && val > 0 && val <= 12 {
2021-11-22 14:50:10 +00:00
queryParams.Month = val
DebugLogWithContext("GetQueryParams", "设置有效月份: %d", val)
} else {
WarnLogWithContext("GetQueryParams", "无效月份参数: %s", month)
2021-11-22 14:50:10 +00:00
}
}
// 字符串条件
setStringParam := func(param *string, queryKey string) {
if val := c.Query(queryKey); isValidParam(val) {
*param = val
DebugLogWithContext("GetQueryParams", "设置%s: %s", queryKey, val)
}
2021-11-24 10:04:02 +00:00
}
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)
2021-11-22 14:50:10 +00:00
}
// 处理排序参数
if orderBy := c.Query("orderBy"); isValidParam(orderBy) {
queryParams.OrderBy = orderBy
} else if queryParams.Year > 0 || queryParams.Month > 0 {
queryParams.OrderBy = "year desc, month desc"
2021-11-24 09:32:24 +00:00
}
// 处理limit参数
if limit := c.Query("limit"); isValidParam(limit) {
if val, err := strconv.Atoi(limit); err == nil && val > 0 {
queryParams.Limit = val
}
2024-10-24 15:26:07 +00:00
}
// 处理严格模式参数
if strictMode := c.Query("strictMode"); isValidParam(strictMode) {
if strict, err := strconv.ParseBool(strictMode); err == nil {
queryParams.StrictAccountMatch = strict
DebugLogWithContext("GetQueryParams", "设置严格模式: %t", strict)
}
2021-11-23 15:33:14 +00:00
}
// 验证必要参数
if queryParams.Year == 0 && queryParams.Month == 0 &&
queryParams.Account == "" && queryParams.AccountLike == "" {
WarnLogWithContext("GetQueryParams", "缺少必要查询参数")
}
queryParams.Where = queryParams.HasConditions()
2021-11-22 14:50:10 +00:00
return queryParams
}
2022-06-05 03:07:03 +00:00
//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
//}
2021-11-22 08:47:49 +00:00
2024-10-24 15:26:07 +00:00
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指向的变量中
2021-11-22 08:47:49 +00:00
func BQLQueryList(ledgerConfig *Config, queryParams *QueryParams, queryResultPtr interface{}) error {
// 调试模式设置默认为true开启调试模式
// 调试信息:函数开始执行
LogDebugDetailed(ledgerConfig.Mail, "BQLQueryList",
"函数开始执行\n----------------------\n输入查询参数: %+v\n----------------------\nqueryResultPtr 类型: %T",
queryParams, queryResultPtr)
2021-11-22 08:47:49 +00:00
assertQueryResultIsPointer(queryResultPtr)
// 调试信息执行bqlRawQuery前
LogDebugDetailed(ledgerConfig.Mail, "BQLQuery",
"正在执行 bqlRawQuery...")
2021-11-22 14:50:10 +00:00
output, err := bqlRawQuery(ledgerConfig, "", queryParams, queryResultPtr)
// 调试信息bqlRawQuery执行结果
2021-11-22 08:47:49 +00:00
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)
2021-11-22 08:47:49 +00:00
}
// 调试信息执行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)
2021-11-22 08:47:49 +00:00
}
return parseErr
2021-11-22 08:47:49 +00:00
}
2021-11-22 14:50:10 +00:00
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)
2021-11-22 14:50:10 +00:00
assertQueryResultIsPointer(queryResultPtr)
// 调试信息执行bqlRawQuery前
LogBQLQueryDebug(ledgerConfig.Mail, contextTag, "正在执行 bqlRawQuery...")
2021-11-22 14:50:10 +00:00
output, err := bqlRawQuery(ledgerConfig, selectBql, queryParams, queryResultPtr)
// 调试信息bqlRawQuery执行结果
2021-11-22 14:50:10 +00:00
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)
2021-11-22 08:47:49 +00:00
}
2021-11-22 14:50:10 +00:00
if err != nil {
errorMsg := fmt.Sprintf("BQL:%s - 自定义 BQL 查询失败: %v", contextTag, err)
LogError(ledgerConfig.Mail, errorMsg)
return fmt.Errorf("自定义 BQL 查询失败: %v", err)
2021-11-22 14:50:10 +00:00
}
// 调试信息执行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
2022-04-12 16:30:38 +00:00
}
2021-11-22 14:50:10 +00:00
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 部分
2021-11-22 14:50:10 +00:00
if selectBql == "" {
LogDebugDetailed(ledgerConfig.Mail, "BQLBuilder", "自动生成SELECT字段...")
bql.WriteString("SELECT ")
2021-11-22 14:50:10 +00:00
queryResultPtrType := reflect.TypeOf(queryResultPtr)
queryResultType := queryResultPtrType.Elem()
if queryResultType.Kind() == reflect.Slice {
queryResultType = queryResultType.Elem()
}
first := true
2021-11-22 14:50:10 +00:00
for i := 0; i < queryResultType.NumField(); i++ {
typeField := queryResultType.Field(i)
if b := typeField.Tag.Get("bql"); b != "" {
if !first {
bql.WriteString(", ")
}
2021-11-22 14:50:10 +00:00
if strings.Contains(b, "distinct") {
b = strings.ReplaceAll(b, "distinct", "")
bql.WriteString("DISTINCT ")
2021-11-22 14:50:10 +00:00
}
bql.WriteString(b)
// 保留反斜线作为分隔符
bql.WriteString(", '\\'")
first = false
2021-11-21 14:37:13 +00:00
}
2021-11-22 08:47:49 +00:00
}
LogDebugDetailed(ledgerConfig.Mail, "BQLBuilder", "生成的SELECT部分: %s", bql.String())
2021-11-22 14:50:10 +00:00
} else {
bql.WriteString(selectBql)
2021-11-22 08:47:49 +00:00
}
2021-11-22 14:50:10 +00:00
// 2. 记录WHERE条件构建
2021-11-22 08:47:49 +00:00
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 ")
2021-11-22 08:47:49 +00:00
}
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
2021-11-22 08:47:49 +00:00
}
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)
}
2021-12-02 14:48:45 +00:00
}
if len(notBlanks) > 0 {
temp[jsonName] = notBlanks
}
2021-11-22 08:47:49 +00:00
}
}
if len(temp) > 0 {
l = append(l, temp)
}
2021-11-22 08:47:49 +00:00
}
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
2021-11-22 08:47:49 +00:00
}
// 原v2版本格式数据导入函数
// 主解析函数 - 智能识别格式
2021-11-22 08:47:49 +00:00
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 {
2021-11-22 08:47:49 +00:00
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 // 没有标准标题格式
2021-11-22 08:47:49 +00:00
}
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
2021-11-22 08:47:49 +00:00
}
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)
2021-11-22 08:47:49 +00:00
}
}
if len(notBlanks) > 0 {
2021-11-22 08:47:49 +00:00
temp[jsonName] = notBlanks
}
default:
if IsDebugMode() {
panic(fmt.Sprintf("Unsupported field type: %s", field.Type.Kind()))
2021-11-22 08:47:49 +00:00
}
2021-11-21 14:37:13 +00:00
}
}
if len(temp) > 0 {
2021-11-22 08:47:49 +00:00
l = append(l, temp)
2021-11-21 14:37:13 +00:00
}
}
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 查询没有找到结果")
}
// 对于切片,返回空切片是合理的
}
2021-11-22 08:47:49 +00:00
var jsonBytes []byte
var err error
2021-11-22 08:47:49 +00:00
if selectOne {
if len(data) == 0 {
if IsDebugMode() {
panic("selectOne 查询没有找到结果")
}
return fmt.Errorf("selectOne 查询没有找到结果")
}
jsonBytes, err = json.Marshal(data[0])
2021-11-22 08:47:49 +00:00
} else {
jsonBytes, err = json.Marshal(data)
2021-11-22 08:47:49 +00:00
}
2021-11-22 08:47:49 +00:00
if err != nil {
handleError(err, "JSON 序列化失败")
2021-11-22 08:47:49 +00:00
return err
}
2021-11-22 08:47:49 +00:00
err = json.Unmarshal(jsonBytes, queryResultPtr)
2021-11-21 14:37:13 +00:00
if err != nil {
handleError(err, "JSON 反序列化失败")
2021-11-21 14:37:13 +00:00
return err
}
2021-11-21 14:37:13 +00:00
return nil
}
// 直接调用v3版本的bean-query命令返回beancount专用表格格式
2021-11-21 14:37:13 +00:00
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)
2021-11-21 14:37:13 +00:00
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("bean-query 执行错误: %v", err)
2021-11-21 14:37:13 +00:00
}
return string(output), nil
}
2021-11-22 08:47:49 +00:00
func assertQueryResultIsPointer(queryResult interface{}) {
k := reflect.TypeOf(queryResult).Kind()
if k != reflect.Ptr {
panic("QueryResult 类型必须是指针,当前是 " + k.String())
2021-11-22 08:47:49 +00:00
}
}