feat(stock):添加股票自动分析功能

- 在 App 结构中添加 cron 实例,用于定时任务调度
- 新增 SetStockAICron 函数,用于设置股票自动分析的 cron 表达式- 在前端 stock 组件中添加 cron 字段,允许用户输入定时任务规则
- 在后端 StockDataApi 中添加 SetStockAICron 方法,用于更新数据库中的 cron 信息
- 修改前端保存逻辑,当用户设置 cron 时,调用 SetStockAICron接口保存
This commit is contained in:
ArvinLovegood 2025-03-30 08:58:45 +08:00
parent e44bc55301
commit 54b0c7ccb3
9 changed files with 76 additions and 7 deletions

48
app.go
View File

@ -15,6 +15,7 @@ import (
"github.com/getlantern/systray" "github.com/getlantern/systray"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/go-toast/toast" "github.com/go-toast/toast"
"github.com/robfig/cron/v3"
"github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime" "github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data" "go-stock/backend/data"
@ -31,14 +32,18 @@ import (
type App struct { type App struct {
ctx context.Context ctx context.Context
cache *freecache.Cache cache *freecache.Cache
cron *cron.Cron
} }
// NewApp creates a new App application struct // NewApp creates a new App application struct
func NewApp() *App { func NewApp() *App {
cacheSize := 512 * 1024 cacheSize := 512 * 1024
cache := freecache.NewCache(cacheSize) cache := freecache.NewCache(cacheSize)
c := cron.New(cron.WithSeconds())
c.Start()
return &App{ return &App{
cache: cache, cache: cache,
cron: c,
} }
} }
@ -58,7 +63,6 @@ func (a *App) startup(ctx context.Context) {
}, func() { }, func() {
go onExit(a) go onExit(a)
}) })
} }
func (a *App) CheckUpdate() { func (a *App) CheckUpdate() {
@ -139,6 +143,11 @@ func (a *App) domReady(ctx context.Context) {
//检查新版本 //检查新版本
go func() { go func() {
a.CheckUpdate() a.CheckUpdate()
a.cron.AddFunc("30 05 8,12,20 * * *", func() {
logger.SugaredLogger.Errorf("Checking for updates...")
a.CheckUpdate()
})
}() }()
//检查谷歌浏览器 //检查谷歌浏览器
@ -158,6 +167,38 @@ func (a *App) domReady(ctx context.Context) {
// logger.SugaredLogger.Infof("Edge浏览器已安装路径为: %s", path) // logger.SugaredLogger.Infof("Edge浏览器已安装路径为: %s", path)
// } // }
//}() //}()
followList := data.NewStockDataApi().GetFollowList()
for _, follow := range *followList {
if follow.Cron == "" {
continue
}
logger.SugaredLogger.Errorf("添加自动分析任务:%s cron=%s", follow.Name, follow.Cron)
a.cron.AddFunc(follow.Cron, func() {
go runtime.EventsEmit(a.ctx, "warnMsg", "开始自动分析"+follow.Name+"_"+follow.StockCode)
ai := data.NewDeepSeekOpenAi(a.ctx)
msgs := ai.NewChatStream(follow.Name, follow.StockCode, "", nil)
var res strings.Builder
chatId := ""
question := ""
for msg := range msgs {
if msg["extraContent"] != nil {
res.WriteString(msg["extraContent"].(string) + "\n")
}
if msg["content"] != nil {
res.WriteString(msg["content"].(string))
}
if msg["chatId"] != nil {
chatId = msg["chatId"].(string)
}
if msg["question"] != nil {
question = msg["question"].(string)
}
}
data.NewDeepSeekOpenAi(a.ctx).SaveAIResponseResult(follow.StockCode, follow.Name, res.String(), chatId, question)
})
}
} }
func refreshTelegraphList() *[]string { func refreshTelegraphList() *[]string {
@ -480,6 +521,7 @@ func (a *App) shutdown(ctx context.Context) {
defer PanicHandler() defer PanicHandler()
// Perform your teardown here // Perform your teardown here
systray.Quit() systray.Quit()
a.cron.Stop()
os.Exit(0) os.Exit(0)
} }
@ -818,7 +860,9 @@ func (a *App) AddPrompt(prompt models.Prompt) string {
func (a *App) DelPrompt(id uint) string { func (a *App) DelPrompt(id uint) string {
return data.NewPromptTemplateApi().DelPrompt(id) return data.NewPromptTemplateApi().DelPrompt(id)
} }
func (a *App) SetStockAICron(cron, stockCode string) {
data.NewStockDataApi().SetStockAICron(cron, stockCode)
}
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) { func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
notification := toast.Notification{ notification := toast.Notification{
AppID: "go-stock", AppID: "go-stock",

View File

@ -114,8 +114,6 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
}() }()
defer close(ch) defer close(ch)
logger.SugaredLogger.Errorf("NewChatStream stock:%s stockCode:%s,sysPromptId:%d", stock, stockCode, *sysPromptId)
sysPrompt := "" sysPrompt := ""
if sysPromptId == nil || *sysPromptId == 0 { if sysPromptId == nil || *sysPromptId == 0 {
sysPrompt = o.Prompt sysPrompt = o.Prompt

View File

@ -8,7 +8,7 @@ import (
func TestNewDeepSeekOpenAiConfig(t *testing.T) { func TestNewDeepSeekOpenAiConfig(t *testing.T) {
//db.Init("../../data/stock.db") //db.Init("../../data/stock.db")
ai := NewDeepSeekOpenAi(context.TODO()) ai := NewDeepSeekOpenAi(context.TODO())
res := ai.NewChatStream("上海贝岭", "sh600171", "分析以上股票资金流入信息,找出适合买入的股票,给出具体操作建议") res := ai.NewChatStream("上海贝岭", "sh600171", "分析以上股票资金流入信息,找出适合买入的股票,给出具体操作建议", nil)
for { for {
select { select {
case msg := <-res: case msg := <-res:

View File

@ -163,6 +163,7 @@ type FollowedStock struct {
AlarmPrice float64 AlarmPrice float64
Time time.Time Time time.Time
Sort int64 Sort int64
Cron string
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"` IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
} }
@ -418,7 +419,14 @@ func (receiver StockDataApi) SetStockSort(sort int64, stockCode string) {
} }
db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).Update("sort", sort) db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).Update("sort", sort)
} }
func (receiver StockDataApi) SetStockAICron(cron string, stockCode string) {
if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
stockCode = strings.ToUpper(stockCode)
stockCode = strings.Replace(stockCode, "gb_", "us", 1)
stockCode = strings.Replace(stockCode, "GB_", "us", 1)
}
db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).Update("cron", cron)
}
func (receiver StockDataApi) GetFollowList() *[]FollowedStock { func (receiver StockDataApi) GetFollowList() *[]FollowedStock {
var result *[]FollowedStock var result *[]FollowedStock
db.Dao.Model(&FollowedStock{}).Order("sort asc,time desc").Find(&result) db.Dao.Model(&FollowedStock{}).Order("sort asc,time desc").Find(&result)

View File

@ -15,7 +15,7 @@ import {
SetCostPriceAndVolume, SetCostPriceAndVolume,
SetStockSort, SetStockSort,
UnFollow, UnFollow,
ShareAnalysis, SaveAsMarkdown, GetPromptTemplates ShareAnalysis, SaveAsMarkdown, GetPromptTemplates, SetStockAICron
} from '../../wailsjs/go/main/App' } from '../../wailsjs/go/main/App'
import { import {
NAvatar, NAvatar,
@ -81,6 +81,7 @@ const formModel = ref({
alarm: 0, alarm: 0,
alarmPrice:0, alarmPrice:0,
sort:999, sort:999,
cron:"",
}) })
const promptTemplates=ref([]) const promptTemplates=ref([])
const sysPromptOptions=ref([]) const sysPromptOptions=ref([])
@ -545,6 +546,7 @@ function setStock(code,name){
formModel.value.alarm=res[0].AlarmChangePercent formModel.value.alarm=res[0].AlarmChangePercent
formModel.value.alarmPrice=res[0].AlarmPrice formModel.value.alarmPrice=res[0].AlarmPrice
formModel.value.sort=res[0].Sort formModel.value.sort=res[0].Sort
formModel.value.cron=res[0].Cron
modalShow.value=true modalShow.value=true
} }
@ -587,6 +589,11 @@ function updateCostPriceAndVolumeNew(code,price,volume,alarm,formModel){
//message.success(result) //message.success(result)
}) })
} }
if(formModel.cron){
SetStockAICron(formModel.cron,code).then(result => {
//message.success(result)
})
}
if(alarm||formModel.alarmPrice){ if(alarm||formModel.alarmPrice){
SetAlarmChangePercent(alarm,formModel.alarmPrice,code).then(result => { SetAlarmChangePercent(alarm,formModel.alarmPrice,code).then(result => {
@ -975,6 +982,9 @@ function share(code,name){
<n-input-number v-model:value="formModel.sort" min="0" placeholder="请输入股价排序值" > <n-input-number v-model:value="formModel.sort" min="0" placeholder="请输入股价排序值" >
</n-input-number> </n-input-number>
</n-form-item> </n-form-item>
<n-form-item label="AI cron" path="cron">
<n-input v-model:value="formModel.cron" placeholder="请输入cron表达式" />
</n-form-item>
</n-form> </n-form>
<template #footer> <template #footer>
<n-button type="primary" @click="updateCostPriceAndVolumeNew(formModel.code,formModel.costPrice,formModel.volume,formModel.alarm,formModel)">保存</n-button> <n-button type="primary" @click="updateCostPriceAndVolumeNew(formModel.code,formModel.costPrice,formModel.volume,formModel.alarm,formModel)">保存</n-button>

View File

@ -47,6 +47,8 @@ export function SetAlarmChangePercent(arg1:number,arg2:number,arg3:string):Promi
export function SetCostPriceAndVolume(arg1:string,arg2:number,arg3:number):Promise<string>; export function SetCostPriceAndVolume(arg1:string,arg2:number,arg3:number):Promise<string>;
export function SetStockAICron(arg1:string,arg2:string):Promise<void>;
export function SetStockSort(arg1:number,arg2:string):Promise<void>; export function SetStockSort(arg1:number,arg2:string):Promise<void>;
export function ShareAnalysis(arg1:string,arg2:string):Promise<string>; export function ShareAnalysis(arg1:string,arg2:string):Promise<string>;

View File

@ -90,6 +90,10 @@ export function SetCostPriceAndVolume(arg1, arg2, arg3) {
return window['go']['main']['App']['SetCostPriceAndVolume'](arg1, arg2, arg3); return window['go']['main']['App']['SetCostPriceAndVolume'](arg1, arg2, arg3);
} }
export function SetStockAICron(arg1, arg2) {
return window['go']['main']['App']['SetStockAICron'](arg1, arg2);
}
export function SetStockSort(arg1, arg2) { export function SetStockSort(arg1, arg2) {
return window['go']['main']['App']['SetStockSort'](arg1, arg2); return window['go']['main']['App']['SetStockSort'](arg1, arg2);
} }

1
go.mod
View File

@ -13,6 +13,7 @@ require (
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-resty/resty/v2 v2.16.2 github.com/go-resty/resty/v2 v2.16.2
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/wailsapp/wails/v2 v2.10.1 github.com/wailsapp/wails/v2 v2.10.1
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0

2
go.sum
View File

@ -125,6 +125,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=