Compare commits
10 Commits
v2025.7.11
...
v2025.7.15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8db94da233 | ||
|
|
60e7d87918 | ||
|
|
615b4d231a | ||
|
|
490a3c0847 | ||
|
|
38f83674ef | ||
|
|
d26c4bc986 | ||
|
|
7e919376b5 | ||
|
|
1d9ef724e6 | ||
|
|
8e982d4430 | ||
|
|
a67559831a |
@@ -11,7 +11,7 @@
|
||||
|
||||
### 📈 交流群
|
||||
- QQ交流群2:[点击链接加入群聊【go-stock交流群2】:892666282](https://qm.qq.com/q/5mYiy6Yxh0)
|
||||
- QQ交流群:[点击链接加入群聊【go-stock交流群】:491605333(已满会定期清理,随缘入群)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333)
|
||||
- QQ交流群:[点击链接加入群聊【go-stock交流群】:491605333(定期清理,随缘入群)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333)
|
||||
|
||||
### ✨ 简介
|
||||
- 本项目基于Wails和NaiveUI开发,结合AI大模型构建的股票分析工具。
|
||||
@@ -23,7 +23,8 @@
|
||||
### 📦 立即体验
|
||||
- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
- 绿色版:[go-stock-windows-amd64.exe](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
- MACOS版:[go-stock-darwin-universal](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
- MACOS绿色版:[go-stock-darwin-universal](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
- MACOS安装版:[go-stock-darwin-universal.pkg](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
|
||||
|
||||
### 💬 支持大模型/平台
|
||||
|
||||
102
app.go
102
app.go
@@ -258,7 +258,7 @@ func (a *App) CheckUpdate() {
|
||||
"time": "发现新版本:" + releaseVersion.TagName,
|
||||
"isRed": false,
|
||||
"source": "go-stock",
|
||||
"content": fmt.Sprintf("当前版本:%s, 最新版本:%s,后台开始下载...", Version, releaseVersion.TagName),
|
||||
"content": fmt.Sprintf("%s", commit.Message),
|
||||
})
|
||||
resp, err := resty.New().R().Get(downloadUrl)
|
||||
if err != nil {
|
||||
@@ -266,7 +266,7 @@ func (a *App) CheckUpdate() {
|
||||
"time": "新版本:" + releaseVersion.TagName,
|
||||
"isRed": true,
|
||||
"source": "go-stock",
|
||||
"content": "新版本下载失败,请稍后重试或请前往 https://github.com/ArvinLovegood/go-stock/releases 手动下载替换文件。",
|
||||
"content": commit.Message + "\n新版本下载失败,请稍后重试或请前往 https://github.com/ArvinLovegood/go-stock/releases 手动下载替换文件。",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -277,7 +277,7 @@ func (a *App) CheckUpdate() {
|
||||
"time": "新版本:" + releaseVersion.TagName,
|
||||
"isRed": true,
|
||||
"source": "go-stock",
|
||||
"content": "新版本下载失败,请稍后重试或请前往 https://github.com/ArvinLovegood/go-stock/releases 手动下载替换文件。",
|
||||
"content": commit.Message + "\n新版本下载失败,请稍后重试或请前往 https://github.com/ArvinLovegood/go-stock/releases 手动下载替换文件。",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -301,21 +301,24 @@ func (a *App) CheckUpdate() {
|
||||
// domReady is called after front-end resources have been loaded
|
||||
func (a *App) domReady(ctx context.Context) {
|
||||
defer PanicHandler()
|
||||
defer func() {
|
||||
go runtime.EventsEmit(ctx, "loadingMsg", "done")
|
||||
}()
|
||||
|
||||
if stocksBin != nil && len(stocksBin) > 0 {
|
||||
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查A股基础信息...")
|
||||
go initStockData(a.ctx)
|
||||
}
|
||||
|
||||
if stocksBinHK != nil && len(stocksBinHK) > 0 {
|
||||
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查港股基础信息...")
|
||||
go initStockDataHK(a.ctx)
|
||||
}
|
||||
|
||||
if stocksBinUS != nil && len(stocksBinUS) > 0 {
|
||||
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查美股基础信息...")
|
||||
go initStockDataUS(a.ctx)
|
||||
}
|
||||
//if stocksBin != nil && len(stocksBin) > 0 {
|
||||
// go runtime.EventsEmit(a.ctx, "loadingMsg", "检查A股基础信息...")
|
||||
// go initStockData(a.ctx)
|
||||
//}
|
||||
//
|
||||
//if stocksBinHK != nil && len(stocksBinHK) > 0 {
|
||||
// go runtime.EventsEmit(a.ctx, "loadingMsg", "检查港股基础信息...")
|
||||
// go initStockDataHK(a.ctx)
|
||||
//}
|
||||
//
|
||||
//if stocksBinUS != nil && len(stocksBinUS) > 0 {
|
||||
// go runtime.EventsEmit(a.ctx, "loadingMsg", "检查美股基础信息...")
|
||||
// go initStockDataUS(a.ctx)
|
||||
//}
|
||||
updateBasicInfo()
|
||||
|
||||
// Add your action here
|
||||
@@ -424,6 +427,7 @@ func (a *App) domReady(ctx context.Context) {
|
||||
a.cron.AddFunc("30 05 8,12,20 * * *", func() {
|
||||
logger.SugaredLogger.Errorf("Checking for updates...")
|
||||
a.CheckUpdate()
|
||||
a.CheckStockBaseInfo()
|
||||
})
|
||||
|
||||
}()
|
||||
@@ -460,7 +464,71 @@ func (a *App) domReady(ctx context.Context) {
|
||||
logger.SugaredLogger.Infof("domReady-cronEntrys:%+v", a.cronEntrys)
|
||||
|
||||
}
|
||||
func (a *App) CheckStockBaseInfo() {
|
||||
defer PanicHandler()
|
||||
|
||||
stockBasics := &[]data.StockBasic{}
|
||||
resty.New().R().
|
||||
SetHeader("user", "go-stock").
|
||||
SetResult(stockBasics).
|
||||
Get("https://go-stock.sparkmemory.top/stock_basic.json")
|
||||
|
||||
for _, stock := range *stockBasics {
|
||||
stockInfo := &data.StockBasic{
|
||||
TsCode: stock.TsCode,
|
||||
Name: stock.Name,
|
||||
Symbol: stock.Symbol,
|
||||
BKCode: stock.BKCode,
|
||||
BKName: stock.BKName,
|
||||
}
|
||||
db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).First(stockInfo)
|
||||
if stockInfo.ID == 0 {
|
||||
db.Dao.Model(&data.StockBasic{}).Create(stockInfo)
|
||||
} else {
|
||||
db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).Updates(stockInfo)
|
||||
}
|
||||
}
|
||||
|
||||
stockHKBasics := &[]models.StockInfoHK{}
|
||||
resty.New().R().
|
||||
SetHeader("user", "go-stock").
|
||||
SetResult(stockHKBasics).
|
||||
Get("https://go-stock.sparkmemory.top/stock_base_info_hk.json")
|
||||
for _, stock := range *stockHKBasics {
|
||||
stockInfo := &models.StockInfoHK{
|
||||
Code: stock.Code,
|
||||
Name: stock.Name,
|
||||
BKName: stock.BKName,
|
||||
BKCode: stock.BKCode,
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stock.Code).First(stockInfo)
|
||||
if stockInfo.ID == 0 {
|
||||
db.Dao.Model(&models.StockInfoHK{}).Create(stockInfo)
|
||||
} else {
|
||||
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stock.Code).Updates(stockInfo)
|
||||
}
|
||||
}
|
||||
stockUSBasics := &[]models.StockInfoUS{}
|
||||
resty.New().R().
|
||||
SetHeader("user", "go-stock").
|
||||
SetResult(stockUSBasics).
|
||||
Get("https://go-stock.sparkmemory.top/stock_base_info_us.json")
|
||||
for _, stock := range *stockUSBasics {
|
||||
stockInfo := &models.StockInfoUS{
|
||||
Code: stock.Code,
|
||||
Name: stock.Name,
|
||||
BKName: stock.BKName,
|
||||
BKCode: stock.BKCode,
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stock.Code).First(stockInfo)
|
||||
if stockInfo.ID == 0 {
|
||||
db.Dao.Model(&models.StockInfoUS{}).Create(stockInfo)
|
||||
} else {
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stock.Code).Updates(stockInfo)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
func (a *App) NewsPush(news *[]models.Telegraph) {
|
||||
for _, telegraph := range *news {
|
||||
//if telegraph.IsRed {
|
||||
|
||||
21
app_test.go
21
app_test.go
@@ -1,7 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -23,3 +26,21 @@ func TestIsUSTradingTime(t *testing.T) {
|
||||
|
||||
t.Log(IsUSTradingTime(time.Now()))
|
||||
}
|
||||
|
||||
func TestCheckStockBaseInfo(t *testing.T) {
|
||||
db.Init("./data/stock.db")
|
||||
NewApp().CheckStockBaseInfo()
|
||||
}
|
||||
|
||||
func TestJson(t *testing.T) {
|
||||
db.Init("./data/stock.db")
|
||||
|
||||
jsonStr := "{\n\t\t\"id\" : 3334,\n\t\t\"created_at\" : \"2025-02-28 16:49:31.8342514+08:00\",\n\t\t\"updated_at\" : \"2025-02-28 16:49:31.8342514+08:00\",\n\t\t\"deleted_at\" : null,\n\t\t\"code\" : \"PUK.US\",\n\t\t\"name\" : \"英国保诚集团\",\n\t\t\"full_name\" : \"\",\n\t\t\"e_name\" : \"\",\n\t\t\"exchange\" : \"NASDAQ\",\n\t\t\"type\" : \"stock\",\n\t\t\"is_del\" : 0,\n\t\t\"bk_name\" : null,\n\t\t\"bk_code\" : null\n\t}"
|
||||
|
||||
v := &models.StockInfoUS{}
|
||||
json.Unmarshal([]byte(jsonStr), v)
|
||||
logger.SugaredLogger.Infof("v:%+v", v)
|
||||
|
||||
db.Dao.Model(v).Updates(v)
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"go-stock/backend/util"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -702,7 +703,9 @@ func (m MarketNewsApi) ClsCalendar() []any {
|
||||
return respMap["data"].([]any)
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetGDP() {
|
||||
func (m MarketNewsApi) GetGDP() *models.GDPResp {
|
||||
res := &models.GDPResp{}
|
||||
|
||||
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CDOMESTICL_PRODUCT_BASE%2CFIRST_PRODUCT_BASE%2CSECOND_PRODUCT_BASE%2CTHIRD_PRODUCT_BASE%2CSUM_SAME%2CFIRST_SAME%2CSECOND_SAME%2CTHIRD_SAME&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_GDP&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "datacenter-web.eastmoney.com").
|
||||
@@ -712,7 +715,7 @@ func (m MarketNewsApi) GetGDP() {
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
|
||||
return
|
||||
return res
|
||||
}
|
||||
body := resp.Body()
|
||||
logger.SugaredLogger.Debugf("GDP:%s", body)
|
||||
@@ -722,13 +725,135 @@ func (m MarketNewsApi) GetGDP() {
|
||||
val, err := vm.Run(body)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
|
||||
return
|
||||
return res
|
||||
}
|
||||
data, _ := val.Object().Value().Export()
|
||||
logger.SugaredLogger.Infof("GDP:%v", data)
|
||||
marshal, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
json.Unmarshal(marshal, &res)
|
||||
logger.SugaredLogger.Infof("GDP:%+v", res)
|
||||
return res
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetCPI() *models.CPIResp {
|
||||
res := &models.CPIResp{}
|
||||
|
||||
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CNATIONAL_SAME%2CNATIONAL_BASE%2CNATIONAL_SEQUENTIAL%2CNATIONAL_ACCUMULATE%2CCITY_SAME%2CCITY_BASE%2CCITY_SEQUENTIAL%2CCITY_ACCUMULATE%2CRURAL_SAME%2CRURAL_BASE%2CRURAL_SEQUENTIAL%2CRURAL_ACCUMULATE&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_CPI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "datacenter-web.eastmoney.com").
|
||||
SetHeader("Origin", "https://datacenter.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
|
||||
return res
|
||||
}
|
||||
body := resp.Body()
|
||||
logger.SugaredLogger.Debugf("GDP:%s", body)
|
||||
vm := otto.New()
|
||||
vm.Run("function data(res){return res};")
|
||||
|
||||
val, err := vm.Run(body)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
|
||||
return res
|
||||
}
|
||||
data, _ := val.Object().Value().Export()
|
||||
logger.SugaredLogger.Infof("GDP:%v", data)
|
||||
marshal, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
json.Unmarshal(marshal, &res)
|
||||
logger.SugaredLogger.Infof("GDP:%+v", res)
|
||||
return res
|
||||
}
|
||||
|
||||
// GetPPI PPI
|
||||
func (m MarketNewsApi) GetPPI() *models.PPIResp {
|
||||
res := &models.PPIResp{}
|
||||
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE,TIME,BASE,BASE_SAME,BASE_ACCUMULATE&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_PPI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "datacenter-web.eastmoney.com").
|
||||
SetHeader("Origin", "https://datacenter.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
|
||||
return res
|
||||
}
|
||||
body := resp.Body()
|
||||
vm := otto.New()
|
||||
vm.Run("function data(res){return res};")
|
||||
|
||||
val, err := vm.Run(body)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
data, _ := val.Object().Value().Export()
|
||||
marshal, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
json.Unmarshal(marshal, &res)
|
||||
return res
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetPMI() *models.PMIResp {
|
||||
res := &models.PMIResp{}
|
||||
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CMAKE_INDEX%2CMAKE_SAME%2CNMAKE_INDEX%2CNMAKE_SAME&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_PMI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "datacenter-web.eastmoney.com").
|
||||
SetHeader("Origin", "https://datacenter.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
body := resp.Body()
|
||||
vm := otto.New()
|
||||
vm.Run("function data(res){return res};")
|
||||
|
||||
val, err := vm.Run(body)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
data, _ := val.Object().Value().Export()
|
||||
marshal, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
json.Unmarshal(marshal, &res)
|
||||
return res
|
||||
|
||||
}
|
||||
func (m MarketNewsApi) GetIndustryReportInfo(infoCode string) {
|
||||
url := "https://data.eastmoney.com/report/zw_industry.jshtml?infocode=" + infoCode
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "data.eastmoney.com").
|
||||
SetHeader("Origin", "https://data.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/report/industry.jshtml").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GetIndustryReportInfo err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("GDP:%s", marshal)
|
||||
body := resp.Body()
|
||||
//logger.SugaredLogger.Debugf("GetIndustryReportInfo:%s", body)
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(body)))
|
||||
title, _ := doc.Find("div.c-title").Html()
|
||||
content, _ := doc.Find("div.ctx-content").Html()
|
||||
//logger.SugaredLogger.Infof("GetIndustryReportInfo:\n%s\n%s", title, content)
|
||||
markdown, err := util.HTMLToMarkdown(title + content)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("GetIndustryReportInfo markdown:\n%s", markdown)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/util"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -177,5 +178,29 @@ func TestClsCalendar(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetGDP(t *testing.T) {
|
||||
NewMarketNewsApi().GetGDP()
|
||||
res := NewMarketNewsApi().GetGDP()
|
||||
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
}
|
||||
func TestGetCPI(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetCPI()
|
||||
md := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res.CPIResult.Data)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
}
|
||||
|
||||
// PPI
|
||||
func TestGetPPI(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetPPI()
|
||||
md := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res.PPIResult.Data)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
}
|
||||
|
||||
// PMI
|
||||
func TestGetPMI(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetPMI()
|
||||
md := util.MarkdownTableWithTitle("采购经理人指数(PMI)", res.PMIResult.Data)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
}
|
||||
func TestGetIndustryReportInfo(t *testing.T) {
|
||||
NewMarketNewsApi().GetIndustryReportInfo("AP202507151709216483")
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"go-stock/backend/util"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -178,7 +179,34 @@ func (o OpenAi) NewSummaryStockNewsStreamWithTools(userQuestion string, sysPromp
|
||||
"content": "当前本地时间是:" + time.Now().Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
wg.Add(3)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var market strings.Builder
|
||||
res := NewMarketNewsApi().GetGDP()
|
||||
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
|
||||
market.WriteString(md)
|
||||
res2 := NewMarketNewsApi().GetCPI()
|
||||
md2 := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res2.CPIResult.Data)
|
||||
market.WriteString(md2)
|
||||
res3 := NewMarketNewsApi().GetPPI()
|
||||
md3 := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res3.PPIResult.Data)
|
||||
market.WriteString(md3)
|
||||
res4 := NewMarketNewsApi().GetPMI()
|
||||
md4 := util.MarkdownTableWithTitle("采购经理人指数(PMI)", res4.PMIResult.Data)
|
||||
market.WriteString(md4)
|
||||
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "国内宏观经济数据",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "\n# 国内宏观经济数据:\n" + market.String(),
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var market strings.Builder
|
||||
|
||||
@@ -153,6 +153,8 @@ type StockBasic struct {
|
||||
IsHs string `json:"is_hs"`
|
||||
ActName string `json:"act_name"`
|
||||
ActEntType string `json:"act_ent_type"`
|
||||
BKName string `json:"bk_name"`
|
||||
BKCode string `json:"bk_code"`
|
||||
}
|
||||
|
||||
type FollowedStock struct {
|
||||
@@ -1454,7 +1456,7 @@ func getSinaStockInfo(receiver StockDataApi, page, pageSize int) *[]models.SinaS
|
||||
func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
|
||||
//m:105,m:106,m:107 //美股
|
||||
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
|
||||
fs := ""
|
||||
fs := "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048"
|
||||
switch market {
|
||||
case "hk":
|
||||
fs = "m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2"
|
||||
@@ -1462,62 +1464,108 @@ func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
|
||||
fs = "m:105,m:106,m:107"
|
||||
}
|
||||
|
||||
url := "https://push2.eastmoney.com/api/qt/clist/get?cb=jQuery371047843066631541353_1745889398012&fs=%s&fields=f12,f13,f14,f19,f1,f2,f4,f3,f152,f17,f18,f15,f16,f5,f6&fid=f3&pn=%d&pz=%d&po=1&dect=1"
|
||||
url := "https://push2.eastmoney.com/api/qt/clist/get?np=1&fltt=1&invt=2&cb=data&fs=%s&fields=f12,f13,f14,f1,f2,f4,f3,f152,f5,f6,f7,f15,f18,f16,f17,f10,f8,f9,f23,f100,f265&fid=f3&pn=%d&pz=%d&po=1&dect=1&wbp2u=|0|0|0|web&_=%d"
|
||||
sprintfUrl := fmt.Sprintf(url, fs, page, pageSize, time.Now().UnixMilli())
|
||||
logger.SugaredLogger.Infof("url:%s", sprintfUrl)
|
||||
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "push2.eastmoney.com").
|
||||
SetHeader("Referer", "https://quote.eastmoney.com/center/gridlist.html").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(fmt.Sprintf(url, fs, page, pageSize))
|
||||
Get(sprintfUrl)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
body := string(resp.Body())
|
||||
body = strutil.ReplaceWithMap(body, map[string]string{
|
||||
"jQuery371047843066631541353_1745889398012(": "",
|
||||
");": "",
|
||||
})
|
||||
js := "var d=" + body
|
||||
logger.SugaredLogger.Infof("resp:%s", body)
|
||||
vm := otto.New()
|
||||
_, err = vm.Run(js)
|
||||
_, err = vm.Run("var data = JSON.stringify(d);")
|
||||
value, err := vm.Get("data")
|
||||
data := make(map[string]any)
|
||||
err = json.Unmarshal([]byte(value.String()), &data)
|
||||
vm.Run("function data(res){return res};")
|
||||
val, err := vm.Run(body)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("vm.Run error:%v", err.Error())
|
||||
}
|
||||
value, _ := val.Object().Value().Export()
|
||||
marshal, err := json.Marshal(value)
|
||||
data := make(map[string]any)
|
||||
err = json.Unmarshal(marshal, &data)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json:%s", value.String())
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
|
||||
}
|
||||
logger.SugaredLogger.Infof("resp:%s", data)
|
||||
if data["data"] != nil {
|
||||
datas := data["data"].(map[string]any)
|
||||
total := datas["total"].(float64)
|
||||
diff := datas["diff"].(map[string]any)
|
||||
diff := datas["diff"].([]any)
|
||||
logger.SugaredLogger.Infof("total:%d", int(total))
|
||||
for k, item := range diff {
|
||||
stock := item.(map[string]any)
|
||||
logger.SugaredLogger.Infof("k:%s,%s:%s", k, stock["f14"], stock["f12"])
|
||||
logger.SugaredLogger.Infof("k:%d,%s:%s:%s %s:%s", k, stock["f14"], stock["f12"], DCToTsCode(stock["f12"].(string)), stock["f100"], stock["f265"])
|
||||
|
||||
if market == "" {
|
||||
stockInfo := &StockBasic{
|
||||
Symbol: stock["f12"].(string),
|
||||
TsCode: DCToTsCode(stock["f12"].(string)),
|
||||
Name: stock["f14"].(string),
|
||||
BKName: stock["f100"].(string),
|
||||
BKCode: stock["f265"].(string),
|
||||
}
|
||||
db.Dao.Model(&StockBasic{}).Where("symbol = ?", stockInfo.Symbol).First(stockInfo)
|
||||
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
|
||||
if stockInfo.ID == 0 {
|
||||
db.Dao.Model(&StockBasic{}).Create(stockInfo)
|
||||
} else {
|
||||
stockInfo = &StockBasic{
|
||||
Symbol: stock["f12"].(string),
|
||||
TsCode: DCToTsCode(stock["f12"].(string)),
|
||||
Name: stock["f14"].(string),
|
||||
BKName: stock["f100"].(string),
|
||||
BKCode: stock["f265"].(string),
|
||||
}
|
||||
db.Dao.Model(&StockBasic{}).Where("symbol = ?", stockInfo.Symbol).Updates(stockInfo)
|
||||
}
|
||||
}
|
||||
|
||||
if market == "hk" {
|
||||
stockInfo := &models.StockInfoHK{
|
||||
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
|
||||
Name: stock["f14"].(string),
|
||||
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
|
||||
Name: stock["f14"].(string),
|
||||
BKName: stock["f100"].(string),
|
||||
BKCode: stock["f265"].(string),
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stockInfo.Code).First(stockInfo)
|
||||
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
|
||||
if stockInfo.ID == 0 {
|
||||
db.Dao.Model(&models.StockInfoHK{}).Create(stockInfo)
|
||||
} else {
|
||||
stockInfo = &models.StockInfoHK{
|
||||
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
|
||||
Name: stock["f14"].(string),
|
||||
BKName: stock["f100"].(string),
|
||||
BKCode: stock["f265"].(string),
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stockInfo.Code).Updates(stockInfo)
|
||||
}
|
||||
}
|
||||
|
||||
if market == "us" {
|
||||
stockInfo := &models.StockInfoUS{
|
||||
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
|
||||
Name: stock["f14"].(string),
|
||||
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
|
||||
Name: stock["f14"].(string),
|
||||
BKName: stock["f100"].(string),
|
||||
BKCode: stock["f265"].(string),
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stockInfo.Code).First(stockInfo)
|
||||
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
|
||||
if stockInfo.ID == 0 {
|
||||
db.Dao.Model(&models.StockInfoUS{}).Create(stockInfo)
|
||||
} else {
|
||||
stockInfo = &models.StockInfoUS{
|
||||
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
|
||||
Name: stock["f14"].(string),
|
||||
BKName: stock["f100"].(string),
|
||||
BKCode: stock["f265"].(string),
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stockInfo.Code).Updates(stockInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1526,6 +1574,25 @@ func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
|
||||
}
|
||||
}
|
||||
|
||||
func DCToTsCode(dcCode string) string {
|
||||
//北京证券交易所 8(83、87、88 等) 创新型中小企业(专精特新为主)
|
||||
//上海证券交易所 6(60、688 等) 大盘蓝筹、科创板(高新技术)
|
||||
//深圳证券交易所 0、3(000、002、30 等) 中小盘、创业板(成长型创新企业)
|
||||
switch dcCode[0:1] {
|
||||
case "8":
|
||||
return dcCode + ".BJ"
|
||||
case "9":
|
||||
return dcCode + ".BJ"
|
||||
case "6":
|
||||
return dcCode + ".SH"
|
||||
case "0":
|
||||
return dcCode + ".SZ"
|
||||
case "3":
|
||||
return dcCode + ".SZ"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) GetHKStockInfo(pageSize int) {
|
||||
url := "https://stock.gtimg.cn/data/hk_rank.php?board=main_all&metric=price&pageSize=%d&reqPage=1&order=desc&var_name=list_data"
|
||||
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
|
||||
@@ -110,7 +110,8 @@ func TestGetHKStockInfo(t *testing.T) {
|
||||
//NewStockDataApi().GetSinaHKStockInfo()
|
||||
//m:105,m:106,m:107 //美股
|
||||
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
|
||||
for i := 1; i <= 592; i++ {
|
||||
//287 224 605
|
||||
for i := 1; i <= 605; i++ {
|
||||
NewStockDataApi().getDCStockInfo("us", i, 20)
|
||||
time.Sleep(time.Duration(random.RandInt(1, 3)) * time.Second)
|
||||
}
|
||||
|
||||
@@ -172,6 +172,8 @@ type StockInfoHK struct {
|
||||
FullName string `json:"fullName"`
|
||||
EName string `json:"eName"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
BKName string `json:"bk_name"`
|
||||
BKCode string `json:"bk_code"`
|
||||
}
|
||||
|
||||
func (receiver StockInfoHK) TableName() string {
|
||||
@@ -187,6 +189,8 @@ type StockInfoUS struct {
|
||||
Exchange string `json:"exchange"`
|
||||
Type string `json:"type"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
BKName string `json:"bk_name"`
|
||||
BKCode string `json:"bk_code"`
|
||||
}
|
||||
|
||||
func (receiver StockInfoUS) TableName() string {
|
||||
@@ -383,3 +387,81 @@ type GDP struct {
|
||||
THIRDPRODUCTBASE float64 `json:"THIRD_PRODUCT_BASE" md:"第三产业(亿元)"`
|
||||
THIRDSAME float64 `json:"THIRD_SAME" md:"第三产业同比增长(%)"`
|
||||
}
|
||||
type CPI struct {
|
||||
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
|
||||
TIME string `json:"TIME" md:"报告期"`
|
||||
NATIONALBASE float64 `json:"NATIONAL_BASE" md:"全国当月"`
|
||||
NATIONALSAME float64 `json:"NATIONAL_SAME" md:"全国当月同比增长(%)"`
|
||||
NATIONALSEQUENTIAL float64 `json:"NATIONAL_SEQUENTIAL" md:"全国当月环比增长(%)"`
|
||||
NATIONALACCUMULATE float64 `json:"NATIONAL_ACCUMULATE" md:"全国当月累计"`
|
||||
CITYBASE float64 `json:"CITY_BASE" md:"城市当月"`
|
||||
CITYSAME float64 `json:"CITY_SAME" md:"城市当月同比增长(%)"`
|
||||
CITYSEQUENTIAL float64 `json:"CITY_SEQUENTIAL" md:"城市当月环比增长(%)"`
|
||||
CITYACCUMULATE int `json:"CITY_ACCUMULATE" md:"城市当月累计"`
|
||||
RURALBASE float64 `json:"RURAL_BASE" md:"农村当月"`
|
||||
RURALSAME float64 `json:"RURAL_SAME" md:"农村当月同比增长(%)"`
|
||||
RURALSEQUENTIAL int `json:"RURAL_SEQUENTIAL" md:"农村当月环比增长(%)"`
|
||||
RURALACCUMULATE float64 `json:"RURAL_ACCUMULATE" md:"农村当月累计"`
|
||||
}
|
||||
type PPI struct {
|
||||
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
|
||||
TIME string `json:"TIME" md:"报告期"`
|
||||
BASE float64 `json:"BASE" md:"当月"`
|
||||
BASESAME float64 `json:"BASE_SAME" md:"当月同比增长(%)"`
|
||||
BASEACCUMULATE float64 `json:"BASE_ACCUMULATE" md:"累计"`
|
||||
}
|
||||
type PMI struct {
|
||||
REPORTDATE string `md:"报告时间" json:"REPORT_DATE"`
|
||||
TIME string `md:"报告期" json:"TIME"`
|
||||
MAKEINDEX float64 `md:"制造业指数" json:"MAKE_INDEX"`
|
||||
MAKESAME float64 `md:"制造业指数同比增长(%)" json:"MAKE_SAME"`
|
||||
NMAKEINDEX float64 `md:"非制造业" json:"NMAKE_INDEX"`
|
||||
NMAKESAME float64 `md:"非制造业同比增长(%)" json:"NMAKE_SAME"`
|
||||
}
|
||||
|
||||
type DCResp struct {
|
||||
Version string `json:"version"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
type GDPResult struct {
|
||||
Pages int `json:"pages"`
|
||||
Data []GDP `json:"data"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
type CPIResult struct {
|
||||
Pages int `json:"pages"`
|
||||
Data []CPI `json:"data"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type PPIResult struct {
|
||||
Pages int `json:"pages"`
|
||||
Data []PPI `json:"data"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
type PMIResult struct {
|
||||
Pages int `json:"pages"`
|
||||
Data []PMI `json:"data"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
type GDPResp struct {
|
||||
DCResp
|
||||
GDPResult GDPResult `json:"result"`
|
||||
}
|
||||
|
||||
type CPIResp struct {
|
||||
DCResp
|
||||
CPIResult CPIResult `json:"result"`
|
||||
}
|
||||
|
||||
type PPIResp struct {
|
||||
DCResp
|
||||
PPIResult PPIResult `json:"result"`
|
||||
}
|
||||
type PMIResp struct {
|
||||
DCResp
|
||||
PMIResult PMIResult `json:"result"`
|
||||
}
|
||||
|
||||
221
backend/util/html_to_markdown.go
Normal file
221
backend/util/html_to_markdown.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package util
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/7/15 14:08
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"golang.org/x/net/html"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HTMLNode 表示HTML文档中的一个节点
|
||||
type HTMLNode struct {
|
||||
Type html.NodeType
|
||||
Data string
|
||||
Attr []html.Attribute
|
||||
Children []*HTMLNode
|
||||
}
|
||||
|
||||
// HTMLToMarkdown 将HTML转换为Markdown
|
||||
func HTMLToMarkdown(htmlContent string) (string, error) {
|
||||
doc, err := html.Parse(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
root := parseHTMLNode(doc)
|
||||
var buf bytes.Buffer
|
||||
convertNode(&buf, root, 0)
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// parseHTMLNode 递归解析HTML节点
|
||||
func parseHTMLNode(n *html.Node) *HTMLNode {
|
||||
node := &HTMLNode{
|
||||
Type: n.Type,
|
||||
Data: n.Data,
|
||||
Attr: n.Attr,
|
||||
}
|
||||
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
node.Children = append(node.Children, parseHTMLNode(c))
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
// convertNode 递归转换节点为Markdown
|
||||
func convertNode(buf *bytes.Buffer, node *HTMLNode, depth int) {
|
||||
switch node.Type {
|
||||
case html.ElementNode:
|
||||
convertElementNode(buf, node, depth)
|
||||
case html.TextNode:
|
||||
// 处理文本节点,去除多余的空白
|
||||
text := strings.TrimSpace(node.Data)
|
||||
if text != "" {
|
||||
buf.WriteString(text)
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理子节点
|
||||
for _, child := range node.Children {
|
||||
convertNode(buf, child, depth+1)
|
||||
}
|
||||
|
||||
// 处理需要在结束标签后添加内容的元素
|
||||
switch node.Data {
|
||||
case "p", "h1", "h2", "h3", "h4", "h5", "h6", "li":
|
||||
buf.WriteString("\n\n")
|
||||
case "blockquote":
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// convertElementNode 转换元素节点为Markdown
|
||||
func convertElementNode(buf *bytes.Buffer, node *HTMLNode, depth int) {
|
||||
switch node.Data {
|
||||
case "h1":
|
||||
buf.WriteString("# ")
|
||||
case "h2":
|
||||
buf.WriteString("## ")
|
||||
case "h3":
|
||||
buf.WriteString("### ")
|
||||
case "h4":
|
||||
buf.WriteString("#### ")
|
||||
case "h5":
|
||||
buf.WriteString("##### ")
|
||||
case "h6":
|
||||
buf.WriteString("###### ")
|
||||
case "p":
|
||||
// 段落标签不需要特殊标记,直接处理内容
|
||||
case "strong", "b":
|
||||
buf.WriteString("**")
|
||||
case "em", "i":
|
||||
buf.WriteString("*")
|
||||
case "u":
|
||||
buf.WriteString("<u>")
|
||||
case "s", "del":
|
||||
buf.WriteString("~~")
|
||||
case "a":
|
||||
//href := getAttrValue(node.Attr, "href")
|
||||
buf.WriteString("[")
|
||||
case "img":
|
||||
src := getAttrValue(node.Attr, "src")
|
||||
alt := getAttrValue(node.Attr, "alt")
|
||||
buf.WriteString(fmt.Sprintf("", alt, src))
|
||||
case "ul":
|
||||
// 无序列表不需要特殊标记,子项会处理
|
||||
case "ol":
|
||||
// 有序列表不需要特殊标记,子项会处理
|
||||
case "li":
|
||||
if isParentListType(node, "ul") {
|
||||
buf.WriteString("- ")
|
||||
} else {
|
||||
// 计算当前列表项的序号
|
||||
index := 1
|
||||
if parent := findParentList(node); parent != nil {
|
||||
for i, sibling := range parent.Children {
|
||||
if sibling == node {
|
||||
index = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("%d. ", index))
|
||||
}
|
||||
case "blockquote":
|
||||
buf.WriteString("> ")
|
||||
case "code":
|
||||
if isParentPre(node) {
|
||||
// 父节点是pre,使用代码块
|
||||
buf.WriteString("\n```\n")
|
||||
} else {
|
||||
// 行内代码
|
||||
buf.WriteString("`")
|
||||
}
|
||||
case "pre":
|
||||
// 前置代码块由子节点code处理
|
||||
case "br":
|
||||
buf.WriteString("\n")
|
||||
case "hr":
|
||||
buf.WriteString("\n---\n")
|
||||
}
|
||||
|
||||
// 处理闭合标签
|
||||
if needsClosingTag(node.Data) {
|
||||
defer func() {
|
||||
switch node.Data {
|
||||
case "strong", "b":
|
||||
buf.WriteString("**")
|
||||
case "em", "i":
|
||||
buf.WriteString("*")
|
||||
case "u":
|
||||
buf.WriteString("</u>")
|
||||
case "s", "del":
|
||||
buf.WriteString("~~")
|
||||
case "a":
|
||||
href := getAttrValue(node.Attr, "href")
|
||||
buf.WriteString(fmt.Sprintf("](%s)", href))
|
||||
case "code":
|
||||
if isParentPre(node) {
|
||||
buf.WriteString("\n```\n")
|
||||
} else {
|
||||
buf.WriteString("`")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// getAttrValue 获取属性值
|
||||
func getAttrValue(attrs []html.Attribute, key string) string {
|
||||
for _, attr := range attrs {
|
||||
if attr.Key == key {
|
||||
return attr.Val
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// isParentListType 检查父节点是否为指定类型的列表
|
||||
func isParentListType(node *HTMLNode, listType string) bool {
|
||||
parent := findParentList(node)
|
||||
return parent != nil && parent.Data == listType
|
||||
}
|
||||
|
||||
// findParentList 查找父列表节点
|
||||
func findParentList(node *HTMLNode) *HTMLNode {
|
||||
// 简化实现,实际应该递归查找父节点
|
||||
if node.Type == html.ElementNode && (node.Data == "ul" || node.Data == "ol") {
|
||||
return node
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isParentPre 检查父节点是否为pre
|
||||
func isParentPre(node *HTMLNode) bool {
|
||||
if len(node.Children) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, child := range node.Children {
|
||||
if child.Type == html.ElementNode && child.Data == "pre" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// needsClosingTag 判断元素是否需要闭合标签
|
||||
func needsClosingTag(tag string) bool {
|
||||
switch tag {
|
||||
case "img", "br", "hr", "input", "meta", "link":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
6
backend/util/html_to_markdown_test.go
Normal file
6
backend/util/html_to_markdown_test.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package util
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/7/15 14:08
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
@@ -29,6 +29,28 @@ func MarkdownTable(v interface{}) string {
|
||||
return "输入必须是结构体、结构体指针、结构体切片或数组"
|
||||
}
|
||||
|
||||
func MarkdownTableWithTitle(title string, v interface{}) string {
|
||||
value := reflect.ValueOf(v)
|
||||
if value.Kind() == reflect.Ptr {
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
// 处理单个结构体
|
||||
if value.Kind() == reflect.Struct {
|
||||
return markdownSingleStruct(value)
|
||||
}
|
||||
|
||||
// 处理结构体切片/数组
|
||||
if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
|
||||
if value.Len() == 0 {
|
||||
return "\n## " + title + "\n" + "无数据" + "\n"
|
||||
}
|
||||
return "\n## " + title + "\n" + markdownStructSlice(value) + "\n"
|
||||
}
|
||||
|
||||
return "\n## " + title + "\n" + "无数据" + "\n"
|
||||
}
|
||||
|
||||
// 处理单个结构体
|
||||
func markdownSingleStruct(value reflect.Value) string {
|
||||
t := value.Type()
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '../wailsjs/runtime'
|
||||
import {h, onBeforeMount, onBeforeUnmount, onMounted, ref} from "vue";
|
||||
import {RouterLink, useRouter} from 'vue-router'
|
||||
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,dateZhCN,zhCN} from 'naive-ui'
|
||||
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,NButton,dateZhCN,zhCN} from 'naive-ui'
|
||||
import {
|
||||
AlarmOutline,
|
||||
AnalyticsOutline,
|
||||
@@ -180,15 +180,15 @@ const menuOptions = ref([
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "指标行情",
|
||||
name: "重大指数",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '指标行情'})
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '重大指数'})
|
||||
},
|
||||
},
|
||||
{default: () => '指标行情',}
|
||||
{default: () => '重大指数',}
|
||||
),
|
||||
key: 'market3',
|
||||
icon: renderIcon(AnalyticsOutline),
|
||||
@@ -638,16 +638,24 @@ onMounted(() => {
|
||||
//type:"error",
|
||||
// avatar: () => h(NIcon,{component:Notifications,color:"red"}),
|
||||
title: data.time,
|
||||
content: () => h(NText,{type:"error"}, { default: () => data.content }),
|
||||
content: () => h('div',{type:"error",style:{
|
||||
"text-align":"left",
|
||||
"font-size":"14px",
|
||||
"color":"#f67979"
|
||||
}}, { default: () => data.content }),
|
||||
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
|
||||
duration:1000*40,
|
||||
})
|
||||
}else{
|
||||
notification.create({
|
||||
notification.create({
|
||||
//type:"info",
|
||||
//avatar: () => h(NIcon,{component:Notifications}),
|
||||
title: data.time,
|
||||
content: () => h(NText,{type:"info"}, { default: () => data.content }),
|
||||
content: () => h('div',{type:"info",style:{
|
||||
"text-align":"left",
|
||||
"font-size":"14px",
|
||||
"color":"#F98C24"
|
||||
}}, { default: () => data.content }),
|
||||
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
|
||||
duration:1000*30 ,
|
||||
})
|
||||
|
||||
@@ -378,7 +378,7 @@ function calculateMA(dayCount,values) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="kLineChartRef" style="width: 100%;height: auto;" :style="{height:chartHeight+'px'}"></div>
|
||||
<div ref="kLineChartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {computed, h, onBeforeMount, onBeforeUnmount, ref} from 'vue'
|
||||
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted, ref} from 'vue'
|
||||
import {
|
||||
GetAIResponseResult,
|
||||
GetConfig,
|
||||
@@ -84,8 +84,6 @@ function getIndex() {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
onBeforeMount(() => {
|
||||
nowTab.value = route.query.name
|
||||
stockCode.value = route.query.stockCode
|
||||
@@ -311,8 +309,8 @@ function ReFlesh(source) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card style="--wails-draggable:drag">
|
||||
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab">
|
||||
<n-card>
|
||||
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab" style="--wails-draggable:drag">
|
||||
<n-tab-pane name="市场快讯" tab="市场快讯">
|
||||
<n-grid :cols="2" :y-gap="0">
|
||||
<n-gi>
|
||||
@@ -390,10 +388,34 @@ function ReFlesh(source) {
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="指标行情" tab="指标行情" style="--wails-dragable:no-drag">
|
||||
<n-tab-pane name="重大指数" tab="重大指数">
|
||||
<n-tabs type="segment" animated>
|
||||
<n-tab-pane name="科创50" tab="科创50">
|
||||
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
|
||||
<n-tab-pane name="恒生科技指数" tab="恒生科技指数">
|
||||
<k-line-chart code="hkHSTECH" :chart-height="panelHeight" name="恒生科技指数" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="科创50" tab="科创50" >
|
||||
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="科创芯片" tab="科创芯片" >
|
||||
<k-line-chart code="sh000685" :chart-height="panelHeight" name="科创芯片" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="证券龙头" tab="证券龙头" >
|
||||
<k-line-chart code="sz399437" :chart-height="panelHeight" name="证券龙头" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="高端装备" tab="高端装备" >
|
||||
<k-line-chart code="sz399437" :chart-height="panelHeight" name="高端装备" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="中证银行" tab="中证银行">
|
||||
<k-line-chart code="sz399986" :chart-height="panelHeight" name="中证银行" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="上证医药" tab="上证医药">
|
||||
<k-line-chart code="sh000037" :chart-height="panelHeight" name="上证医药" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="沪深300" tab="沪深300">
|
||||
@@ -672,5 +694,4 @@ function ReFlesh(source) {
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -426,6 +426,8 @@ export namespace data {
|
||||
is_hs: string;
|
||||
act_name: string;
|
||||
act_ent_type: string;
|
||||
bk_name: string;
|
||||
bk_code: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new StockBasic(source);
|
||||
@@ -454,6 +456,8 @@ export namespace data {
|
||||
this.is_hs = source["is_hs"];
|
||||
this.act_name = source["act_name"];
|
||||
this.act_ent_type = source["act_ent_type"];
|
||||
this.bk_name = source["bk_name"];
|
||||
this.bk_code = source["bk_code"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
|
||||
2
go.mod
2
go.mod
@@ -21,6 +21,7 @@ require (
|
||||
github.com/tidwall/gjson v1.14.2
|
||||
github.com/wailsapp/wails/v2 v2.10.1
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/net v0.38.0
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/text v0.23.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
@@ -82,7 +83,6 @@ require (
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
|
||||
11
main.go
11
main.go
@@ -19,7 +19,6 @@ import (
|
||||
log "go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"os"
|
||||
goruntime "runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
@@ -91,11 +90,11 @@ func main() {
|
||||
//})
|
||||
//FileMenu.AddSeparator()
|
||||
|
||||
if goruntime.GOOS == "windows" {
|
||||
FileMenu.AddText("隐藏到托盘区", keys.CmdOrCtrl("z"), func(_ *menu.CallbackData) {
|
||||
runtime.WindowHide(app.ctx)
|
||||
})
|
||||
}
|
||||
//if goruntime.GOOS == "windows" {
|
||||
// FileMenu.AddText("隐藏到托盘区", keys.CmdOrCtrl("z"), func(_ *menu.CallbackData) {
|
||||
// runtime.WindowHide(app.ctx)
|
||||
// })
|
||||
//}
|
||||
|
||||
//FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
|
||||
// runtime.Quit(app.ctx)
|
||||
|
||||
Reference in New Issue
Block a user