add: 新增虚拟环境支持和调试模式功能,优化日志系统

This commit is contained in:
cnb.asek4HHRAKA 2025-10-01 20:43:33 +08:00
parent 2d4f03b7cf
commit 284f4e39d7
6 changed files with 476 additions and 12 deletions

View File

@ -0,0 +1,44 @@
anyio==4.11.0
babel==2.17.0
beancount==3.2.0
beangulp==0.2.0
beanquery==0.2.0
beautifulsoup4==4.14.0
blinker==1.9.0
chardet==5.2.0
cheroot==10.0.1
click==8.3.0
dateparser==1.2.2
debugpy==1.8.16
fava==1.30.6
Flask==3.1.2
flask-babel==4.0.0
idna==3.10
iniconfig==2.1.0
itsdangerous==2.2.0
jaraco.functools==4.3.0
Jinja2==3.1.6
lxml==6.0.2
markdown2==2.5.4
MarkupSafe==3.0.3
more-itertools==10.8.0
packaging==25.0
pluggy==1.6.0
ply==3.11
pycryptodomex==3.23.0
Pygments==2.19.2
pytest==8.4.2
python-dateutil==2.9.0.post0
python-magic==0.4.27
pytz==2025.2
pyzipper==0.3.6
regex==2025.9.18
simplejson==3.20.2
six==1.17.0
sniffio==1.3.1
soupsieve==2.8
TatSu-LTS==5.13.2
typing_extensions==4.15.0
tzlocal==5.3.1
watchfiles==1.1.0
Werkzeug==3.1.3

View File

@ -6,8 +6,10 @@ import (
"os" "os"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
"github.com/beancount-gs/utils/venv" // 添加这个导入
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -20,6 +22,14 @@ var ledgerAccountTypesMap map[string]map[string]string
var ledgerCurrencyMap map[string][]LedgerCurrency var ledgerCurrencyMap map[string][]LedgerCurrency
var whiteList []string var whiteList []string
var (
// 添加虚拟环境相关的变量
venvPath string
venvExecutor *venv.VenvExecutor
venvPathLock sync.RWMutex
venvExecLock sync.RWMutex
)
type Config struct { type Config struct {
Id string `json:"id,omitempty"` Id string `json:"id,omitempty"`
Mail string `json:"mail,omitempty"` Mail string `json:"mail,omitempty"`
@ -30,6 +40,7 @@ type Config struct {
IsBak bool `json:"isBak"` IsBak bool `json:"isBak"`
OpeningBalances string `json:"openingBalances"` OpeningBalances string `json:"openingBalances"`
CreateDate string `json:"createDate,omitempty"` CreateDate string `json:"createDate,omitempty"`
DebugMode bool `json:"debugMode"`
} }
type Account struct { type Account struct {
@ -85,12 +96,15 @@ func GetServerConfig() Config {
func LoadServerConfig() error { func LoadServerConfig() error {
filePath := GetServerConfigFilePath() filePath := GetServerConfigFilePath()
LogSystemInfo("Load config file (" + filePath + ")")
if !FileIfExist(filePath) { if !FileIfExist(filePath) {
serverConfig = Config{ serverConfig = Config{
OpeningBalances: "Equity:OpeningBalances", OpeningBalances: "Equity:Opening-Balances",
OperatingCurrency: "CNY", OperatingCurrency: "CNY",
StartDate: "1970-01-01", StartDate: "1970-01-01",
IsBak: true, IsBak: true,
DebugMode: false, // 添加默认值
DataPath: GetDataPath(), // 添加默认值
} }
return nil return nil
} }
@ -131,15 +145,52 @@ func LoadServerConfig() error {
return nil return nil
} }
// 获取当前调试模式状态
func IsDebugMode() bool {
return serverConfig.DebugMode
}
// 设置调试模式并保存到配置文件
func SetDebugMode(debug bool) error {
serverConfig.DebugMode = debug
return UpdateServerConfig(serverConfig)
}
// 为方便使用,添加调试日志函数
func DebugLog(format string, args ...interface{}) {
if IsDebugMode() {
message := fmt.Sprintf(format, args...)
LogSystemInfo("[DEBUG] " + message)
}
}
// 添加带上下文信息的调试日志
func DebugLogWithContext(context string, format string, args ...interface{}) {
if IsDebugMode() {
message := fmt.Sprintf("[%s] "+format, append([]interface{}{context}, args...)...)
LogSystemInfo("[DEBUG] " + message)
}
}
// 添加带上下文信息的警告日志
func WarnLogWithContext(context string, format string, args ...interface{}) {
message := fmt.Sprintf("[%s] "+format, append([]interface{}{context}, args...)...)
LogSystemInfo("[WARN] " + message)
}
func UpdateServerConfig(config Config) error { func UpdateServerConfig(config Config) error {
bytes, err := json.Marshal(config) bytes, err := json.Marshal(config)
if err != nil { if err != nil {
return err return err
} }
err = WriteFile(GetServerConfigFilePath(), string(bytes))
// 使用新的带目录创建功能的写入函数
configPath := GetServerConfigFilePath()
err = WriteFileWithDir(configPath, string(bytes))
if err != nil { if err != nil {
return err return err
} }
serverConfig = config serverConfig = config
return nil return nil
} }
@ -335,15 +386,16 @@ func LoadLedgerAccounts(ledgerId string) error {
key := words[2] key := words[2]
temp = accountMap[key] temp = accountMap[key]
account := Account{Acc: key, Type: nil, StartDate: "", EndDate: ""} account := Account{Acc: key, Type: nil, StartDate: "", EndDate: ""}
if words[1] == "open" { switch words[1] {
case "open":
// 最晚的开户日期设置为账户开户日期 // 最晚的开户日期设置为账户开户日期
account.StartDate = getMaxDate(words[0], temp.StartDate) account.StartDate = getMaxDate(words[0], temp.StartDate)
// 货币单位 // 货币单位
if len(words) >= 4 { if len(words) >= 4 {
account.Currency = words[3] account.Currency = words[3]
} }
} else if words[1] == "close" { case "close":
//账户最晚的关闭日期设置为账户关闭日期 // 账户最晚的关闭日期设置为账户关闭日期
account.EndDate = getMaxDate(words[0], temp.EndDate) account.EndDate = getMaxDate(words[0], temp.EndDate)
} }
if account.EndDate != "" && account.StartDate == getMaxDate(account.StartDate, account.EndDate) { if account.EndDate != "" && account.StartDate == getMaxDate(account.StartDate, account.EndDate) {
@ -588,3 +640,31 @@ func GetAccountIconName(account string) string {
nodes := strings.Split(account, ":") nodes := strings.Split(account, ":")
return strings.Join(nodes, "_") return strings.Join(nodes, "_")
} }
// 新增函数:设置虚拟环境路径
func SetVenvPath(path string) {
venvPathLock.Lock()
defer venvPathLock.Unlock()
venvPath = path
}
// 新增函数:获取虚拟环境路径
func GetVenvPath() string {
venvPathLock.RLock()
defer venvPathLock.RUnlock()
return venvPath
}
// 新增函数:设置虚拟环境执行器
func SetVenvExecutor(executor *venv.VenvExecutor) {
venvExecLock.Lock()
defer venvExecLock.Unlock()
venvExecutor = executor
}
// 新增函数:获取虚拟环境执行器
func GetVenvExecutor() *venv.VenvExecutor {
venvExecLock.RLock()
defer venvExecLock.RUnlock()
return venvExecutor
}

View File

@ -295,10 +295,7 @@ func getAccountWithNumber(str string) string {
func IsComment(line string) bool { func IsComment(line string) bool {
trimmed := strings.TrimLeft(line, " ") trimmed := strings.TrimLeft(line, " ")
if strings.HasPrefix(trimmed, ";") { return strings.HasPrefix(trimmed, ";")
return true
}
return false
} }
// 删除指定行范围的内容 // 删除指定行范围的内容
@ -360,3 +357,20 @@ func WriteToFile(filePath string, lines []string) error {
LogSystemInfo("Success write content in file " + filePath) LogSystemInfo("Success write content in file " + filePath)
return writer.Flush() return writer.Flush()
} }
// EnsureDirExists 确保目录存在,如果不存在则创建
func EnsureDirExists(path string) error {
dir := filepath.Dir(path)
return os.MkdirAll(dir, 0755)
}
// WriteFileWithDir 写入文件,确保目录存在
func WriteFileWithDir(filename string, data string) error {
// 确保目录存在
if err := EnsureDirExists(filename); err != nil {
return err
}
// 写入文件
return WriteFile(filename, data)
}

View File

@ -5,18 +5,136 @@ import (
"time" "time"
) )
// Info级别日志函数组
// LogInfo 记录信息级别的日志
// ledgerName: 账本名称,用于标识日志来源
// message: 需要记录的日志信息
func LogInfo(ledgerName string, message string) { func LogInfo(ledgerName string, message string) {
fmt.Printf("[Info] [%s] [%s]: %s\n", time.Now().Format("2006-01-02 15:04:05"), ledgerName, message) fmt.Printf("[Info] [%s] [%s]: %s\n", time.Now().Format("2006-01-02 15:04:05"), ledgerName, message)
} }
// LogSystemInfo 记录系统信息日志
// message: 要记录的系统信息消息
func LogSystemInfo(message string) { func LogSystemInfo(message string) {
LogInfo("System", message) LogInfo("System", message)
} }
// Warn级别日志函数组
// LogWarn 记录警告级别的日志
// ledgerName: 账本名称,用于标识日志来源
// context: 日志上下文,提供更多分类信息
// format: 格式化字符串,定义日志消息格式
// args: 可变参数,用于填充格式化字符串
func LogWarn(ledgerName string, context string, format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
fmt.Printf("[Warn] [%s] [%s][%s]: %s\n",
time.Now().Format("2006-01-02 15:04:05"),
ledgerName,
context,
message)
}
// LogSystemWarn 记录系统级警告日志
// context: 日志上下文标识
// format: 格式化字符串
// args: 格式化参数
func LogSystemWarn(context string, format string, args ...interface{}) {
LogWarn("System", context, format, args...)
}
// Error级别日志函数组
// LogError 记录错误日志,格式为:[Error] [时间] [账本名称]: 错误信息
// ledgerName: 账本名称
// message: 需要记录的错误信息
func LogError(ledgerName string, message string) { func LogError(ledgerName string, message string) {
fmt.Printf("[Error] [%s] [%s]: %s\n", time.Now().Format("2006-01-02 15:04:05"), ledgerName, message) fmt.Printf("[Error] [%s] [%s]: %s\n", time.Now().Format("2006-01-02 15:04:05"), ledgerName, message)
} }
// LogErrorDetailed 记录带有详细信息的错误日志
// ledgerName: 账本名称,用于标识日志来源
// context: 上下文信息,帮助定位错误发生的场景
// format: 格式化字符串,用于构建错误信息
// args: 格式化字符串的参数
// 日志格式为:[Error] [时间] [账本名称][上下文]: 错误信息
func LogErrorDetailed(ledgerName string, context string, format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
fmt.Printf("[Error] [%s] [%s][%s]: %s\n",
time.Now().Format("2006-01-02 15:04:05"),
ledgerName,
context,
message)
}
// LogSystemError 记录系统级错误信息
// message: 错误描述信息
// 该函数是对LogError的封装专门用于记录系统模块的错误日志
func LogSystemError(message string) { func LogSystemError(message string) {
LogError("System", message) LogError("System", message)
} }
// LogSystemErrorDetailed 记录系统级错误日志,包含详细上下文信息
// context: 错误发生的上下文环境
// format: 格式化字符串,用于描述错误信息
// args: 格式化字符串的参数
func LogSystemErrorDetailed(context string, format string, args ...interface{}) {
LogErrorDetailed("System", context, format, args...)
}
// Debug级别日志函数组
// LogDebug 在调试模式下记录调试日志,格式为:[Debug] [时间] [账本名称]: 消息
// 仅当 IsDebugMode() 返回 true 时才会输出日志
func LogDebug(ledgerName string, message string) {
if IsDebugMode() {
fmt.Printf("[Debug] [%s] [%s]: %s\n", time.Now().Format("2006-01-02 15:04:05"), ledgerName, message)
}
}
// LogDebugDetailed 在调试模式下记录详细的调试日志
// ledgerName: 账本名称,用于标识日志来源
// context: 上下文信息,提供额外的日志分类
// format: 格式化字符串,用于构建日志消息
// args: 格式化字符串的参数
// 日志格式: [Debug] [时间] [账本名称][上下文]: 消息内容
// 注意: 仅在调试模式(IsDebugMode返回true)下输出日志
func LogDebugDetailed(ledgerName string, context string, format string, args ...interface{}) {
if IsDebugMode() {
message := fmt.Sprintf(format, args...)
fmt.Printf("[Debug] [%s] [%s][%s]: %s\n",
time.Now().Format("2006-01-02 15:04:05"),
ledgerName,
context,
message)
}
}
// LogSystemDebug 记录系统级别的调试日志
// message: 需要记录的调试信息
func LogSystemDebug(message string) {
LogDebug("System", message)
}
// LogSystemDebugDetailed 记录系统级调试日志,包含详细上下文信息
// context: 日志上下文标识
// format: 日志格式字符串
// args: 格式化参数
func LogSystemDebugDetailed(context string, format string, args ...interface{}) {
LogDebugDetailed("System", context, format, args...)
}
// 特殊功能日志函数
// LogBQLQueryDebug 在调试模式下记录BQL查询日志
// ledgerName: 账本名称
// context: 查询上下文信息
// format: 日志格式字符串
// args: 格式化参数
// 注意: 仅在调试模式开启时才会记录日志
func LogBQLQueryDebug(ledgerName string, context string, format string, args ...interface{}) {
if IsDebugMode() {
LogDebugDetailed(ledgerName, "BQL:"+context, format, args...)
}
}

View File

@ -3,14 +3,25 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"github.com/beancount-gs/script"
"github.com/beancount-gs/service"
"github.com/gin-gonic/gin"
"io" "io"
"net/http" "net/http"
"os" "os"
"github.com/beancount-gs/script"
"github.com/beancount-gs/service"
"github.com/beancount-gs/utils/venv"
"github.com/gin-gonic/gin"
) )
// 全局变量,方便其他模块使用
var venvExecutor *venv.VenvExecutor
var venvPath string // 新增:虚拟环境路径变量
/*
* 初始化服务器文件
* 检查账本目录是否存在如果不存在则创建
* 返回error表示操作是否成功
*/
func InitServerFiles() error { func InitServerFiles() error {
dataPath := script.GetServerConfig().DataPath dataPath := script.GetServerConfig().DataPath
// 账本目录不存在,则创建 // 账本目录不存在,则创建
@ -20,6 +31,11 @@ func InitServerFiles() error {
return nil return nil
} }
/*
* 加载服务器缓存
* 加载账本配置映射和账户映射
* 返回error表示加载过程中是否出错
*/
func LoadServerCache() error { func LoadServerCache() error {
err := script.LoadLedgerConfigMap() err := script.LoadLedgerConfigMap()
if err != nil { if err != nil {
@ -28,6 +44,11 @@ func LoadServerCache() error {
return script.LoadLedgerAccountsMap() return script.LoadLedgerAccountsMap()
} }
/*
* 授权中间件
* 检查请求头中的ledgerId是否有效
* 如果有效则继续处理请求否则返回未授权错误
*/
func AuthorizedHandler() gin.HandlerFunc { func AuthorizedHandler() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
ledgerId := c.GetHeader("ledgerId") ledgerId := c.GetHeader("ledgerId")
@ -42,18 +63,24 @@ func AuthorizedHandler() gin.HandlerFunc {
} }
} }
/*
* 注册路由
* 配置静态文件服务API路由和需要授权的路由组
*/
func RegisterRouter(router *gin.Engine) { func RegisterRouter(router *gin.Engine) {
// fix wildcard and static file router conflict, https://github.com/gin-gonic/gin/issues/360 // fix wildcard and static file router conflict, https://github.com/gin-gonic/gin/issues/360
router.GET("/", func(c *gin.Context) { router.GET("/", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/web") c.Redirect(http.StatusMovedPermanently, "/web")
}) })
router.StaticFS("/web", http.Dir("./public")) router.StaticFS("/web", http.Dir("./public"))
// 公开API路由无需授权
router.GET("/api/version", service.QueryVersion) router.GET("/api/version", service.QueryVersion)
router.POST("/api/check", service.CheckBeancount) router.POST("/api/check", service.CheckBeancount)
router.GET("/api/config", service.QueryServerConfig) router.GET("/api/config", service.QueryServerConfig)
router.POST("/api/config", service.UpdateServerConfig) router.POST("/api/config", service.UpdateServerConfig)
router.GET("/api/ledger", service.QueryLedgerList) router.GET("/api/ledger", service.QueryLedgerList)
router.POST("/api/ledger", service.OpenOrCreateLedger) router.POST("/api/ledger", service.OpenOrCreateLedger)
// 需要授权的API路由组
authorized := router.Group("/api/auth/") authorized := router.Group("/api/auth/")
authorized.Use(AuthorizedHandler()) authorized.Use(AuthorizedHandler())
{ {
@ -106,19 +133,71 @@ func RegisterRouter(router *gin.Engine) {
} }
} }
// initVenvExecutor 初始化虚拟环境执行器
func initVenvExecutor(venvDir string) {
venvPath = venvDir
script.SetVenvPath(venvDir) // 设置路径到 script 包
// 检查虚拟环境是否存在
if !venv.CheckVenvExists(venvPath) {
script.LogSystemError("虚拟环境不存在,请先运行 setup script: " + venvPath)
fmt.Println("警告: 虚拟环境不存在,某些功能可能无法正常工作")
fmt.Println("请运行: ./start_dev.sh 或手动创建虚拟环境")
return
}
venvExecutor = venv.NewVenvExecutor(venvPath)
script.SetVenvExecutor(venvExecutor) // 设置执行器到 script 包
// 测试 bean-query 是否可用
_, err := venvExecutor.GetCommandPath("bean-query")
if err != nil {
script.LogSystemError("bean-query 不可用: " + err.Error())
fmt.Println("警告: bean-query 命令不可用,价格查询功能将受限")
} else {
script.LogSystemInfo("虚拟环境初始化成功: bean-query 可用, 路径: " + venvPath)
fmt.Println("虚拟环境初始化成功: " + venvPath)
}
}
func main() { func main() {
var secret string var secret string
var port int var port int
var debugFlag bool
var venvDir string // 新增:虚拟环境目录参数
flag.StringVar(&secret, "secret", "", "服务器密钥") flag.StringVar(&secret, "secret", "", "服务器密钥")
flag.IntVar(&port, "p", 10000, "端口号") flag.IntVar(&port, "p", 10000, "端口号")
flag.BoolVar(&debugFlag, "debug", false, "调试模式")
flag.StringVar(&venvDir, "venv", ".env_beancount-v3", "虚拟环境目录名称,默认值为 .env_beancount-v3") // 新增参数
flag.Parse() flag.Parse()
// 初始化虚拟环境执行器
initVenvExecutor(venvDir)
// 读取配置文件 // 读取配置文件
err := script.LoadServerConfig() err := script.LoadServerConfig()
if err != nil { if err != nil {
script.LogSystemError("Failed to load server config, " + err.Error()) script.LogSystemError("Failed to load server config, " + err.Error())
return return
} }
// 如果命令行指定了debug参数覆盖配置文件中的设置
if debugFlag {
err = script.SetDebugMode(true)
if err != nil {
fmt.Println("Warning: Failed to set debug mode:", err)
}
}
// 现在可以在任何地方使用 script.IsDebugMode() 来检查调试模式
if script.IsDebugMode() {
fmt.Println("调试模式已启用")
} else {
fmt.Println("调试模式未启用")
}
serverConfig := script.GetServerConfig() serverConfig := script.GetServerConfig()
// 若 DataPath == "" 则配置未初始化 // 若 DataPath == "" 则配置未初始化
if serverConfig.DataPath != "" { if serverConfig.DataPath != "" {
@ -135,11 +214,13 @@ func main() {
return return
} }
} }
// gin 日志设置 // gin 日志设置
gin.DisableConsoleColor() gin.DisableConsoleColor()
fs, _ := os.Create("logs/gin.log") fs, _ := os.Create("logs/gin.log")
gin.DefaultWriter = io.MultiWriter(fs, os.Stdout) gin.DefaultWriter = io.MultiWriter(fs, os.Stdout)
router := gin.Default() router := gin.Default()
// 注册路由 // 注册路由
RegisterRouter(router) RegisterRouter(router)
@ -151,10 +232,13 @@ func main() {
startLog += " or http://" + ip + portStr startLog += " or http://" + ip + portStr
} }
script.LogSystemInfo(startLog) script.LogSystemInfo(startLog)
// 打开浏览器 // 打开浏览器
script.OpenBrowser(url) script.OpenBrowser(url)
// 打印密钥 // 打印密钥
script.LogSystemInfo("Secret token is " + script.GenerateServerSecret(secret)) script.LogSystemInfo("Secret token is " + script.GenerateServerSecret(secret))
// 启动服务 // 启动服务
err = router.Run(portStr) err = router.Run(portStr)
if err != nil { if err != nil {

124
utils/venv/venv.go Normal file
View File

@ -0,0 +1,124 @@
/*
* @Author: liangzai450
* @Date: 2025-09-17 20:50:11
* @LastEditors: liangzai liangzai450@qq.com
* @LastEditTime: 2025-09-18 08:15:42
* @FilePath: \\cnb-beancount\\utils\\venv\\venv.go
* @Description:
* Copyright (c) 2025 by ${git_name_email}, All Rights Reserved.
* ==============================================
*/
package venv
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
)
// VenvExecutor 虚拟环境执行器
type VenvExecutor struct {
venvPath string
}
// NewVenvExecutor 创建新的虚拟环境执行器
func NewVenvExecutor(venvPath string) *VenvExecutor {
return &VenvExecutor{venvPath: venvPath}
}
// GetCommandPath 获取虚拟环境中命令的完整路径
func (v *VenvExecutor) GetCommandPath(command string) (string, error) {
var cmdPath string
if runtime.GOOS == "windows" {
cmdPath = filepath.Join(v.venvPath, "Scripts", command+".exe")
} else {
cmdPath = filepath.Join(v.venvPath, "bin", command)
}
if _, err := os.Stat(cmdPath); os.IsNotExist(err) {
return "", fmt.Errorf("command %s not found at: %s", command, cmdPath)
}
return cmdPath, nil
}
// Execute 执行虚拟环境中的命令
func (v *VenvExecutor) Execute(command string, args ...string) error {
cmdPath, err := v.GetCommandPath(command)
if err != nil {
return err
}
cmd := exec.Command(cmdPath, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
return cmd.Run()
}
// ExecuteWithOutput 执行命令并返回输出
func (v *VenvExecutor) ExecuteWithOutput(command string, args ...string) ([]byte, []byte, error) {
cmdPath, err := v.GetCommandPath(command)
if err != nil {
return nil, nil, err
}
cmd := exec.Command(cmdPath, args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
return stdout.Bytes(), stderr.Bytes(), err
}
// BeanQueryStdout 执行 bean-query 命令,只返回纯净的标准输出
func (v *VenvExecutor) BeanQueryStdout(beancountFile, query string) ([]byte, error) {
stdout, stderr, err := v.ExecuteWithOutput("bean-query", beancountFile, query)
if err != nil {
// 你可以选择记录 stderr 或根据错误类型处理
fmt.Printf("Bean-query stderr: %s", string(stderr))
return nil, err
}
return stdout, nil
}
// BeanQuery 执行 bean-query 命令
// 保持原有的 BeanQuery 函数(如果需要兼容旧代码)
func (v *VenvExecutor) BeanQuery(beancountFile, query string) ([]byte, error) {
return v.BeanQueryStdout(beancountFile, query)
}
// BeanCheck 执行 bean-check 语法检查
func (v *VenvExecutor) BeanCheck(beancountFile string) error {
_, err := v.BeanQueryStdout("bean-check", beancountFile)
return err
}
// Fava 启动 fava 服务器
func (v *VenvExecutor) Fava(beancountFile string, port int) error {
return v.Execute("fava", beancountFile, "--port", fmt.Sprintf("%d", port))
}
// CheckVenvExists 检查虚拟环境是否存在
func CheckVenvExists(venvPath string) bool {
if runtime.GOOS == "windows" {
return dirExists(filepath.Join(venvPath, "Scripts"))
}
return dirExists(filepath.Join(venvPath, "bin"))
}
func dirExists(path string) bool {
info, err := os.Stat(path)
if os.IsNotExist(err) {
return false
}
return info.IsDir()
}