feat(stock):添加A股盘口数据解析和展示功能

- 在 stock.vue 中添加盘口数据展示组件
- 在 stock_data_api.go 中增加 A 股盘口数据解析逻辑
- 优化数据库自动迁移逻辑,提取到单独的函数中
- 更新测试用例以覆盖新的盘口数据解析功能
This commit is contained in:
ArvinLovegood 2025-05-09 11:44:13 +08:00
parent 58d93c76f6
commit 7e24424ea0
5 changed files with 171 additions and 26 deletions

View File

@ -53,6 +53,7 @@
| 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 |
## 👀 更新日志
### 2025.05.08 添加A股盘口数据解析和展示功能
### 2025.05.07 优化分时图的展示
### 2025.04.29 补全港股/美股基础数据,优化港股股价延迟问题,优化初始化逻辑
### 2025.04.25 市场资讯支持AI分析和总结让AI帮你读市场

View File

@ -295,12 +295,16 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
stockInfos := make([]StockInfo, 0)
hkcodes := slice.Filter(StockCodes, func(i int, s string) bool {
return strutil.HasPrefixAny(s, []string{"hk", "HK"})
return strutil.HasPrefixAny(s, []string{"hk", "HK", "sh", "sz"})
})
if hkcodes != nil && len(hkcodes) > 0 {
hkcodesStr := slice.JoinFunc(hkcodes, ",", func(s string) string {
return "r_" + strings.ToLower(s)
if strutil.HasPrefixAny(s, []string{"hk", "HK"}) {
return "r_" + strings.ToLower(s)
} else {
return strings.ToLower(s)
}
})
url := fmt.Sprintf(txStockUrl, time.Now().Unix(), hkcodesStr)
resp, err := receiver.client.R().
@ -336,7 +340,7 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
}
szzsusCodes := slice.Filter(StockCodes, func(i int, s string) bool {
return !strutil.HasPrefixAny(s, []string{"hk", "HK"})
return !strutil.HasPrefixAny(s, []string{"hk", "HK", "sh", "sz"})
})
codes := slice.JoinFunc(szzsusCodes, ",", func(s string) string {
@ -556,13 +560,16 @@ func GB18030ToUTF8(bs []byte) string {
func ParseTxStockData(data string) (*StockInfo, error) {
//v_r_hk09660="100~地平线机器人-W~09660~6.240~5.690~5.800~192659034.0~0~0~6.240~0~0~0~0~0~0~0~0~0~6.240~0~0~0~0~0~0~0~0~0~192659034.0~2025/04/29
//13:41:04~0.550~9.67~6.450~5.710~6.240~192659034.0~1180471843.140~0~32.51~~0~0~13.01~691.1364~823.6983~HORIZONROBOT-W~0.00~10.380~3.320~1.07~-16.03~0~0~0~0~0~32.51~6.40~1.74~600~73.33~17.96~GP~19.70~11.51~-0.95~-18.54~44.44~13200293682.00~11075904412.00~32.51~0.000~6.127~56.39~HKD~1~30";
//v_sz002241="51~歌尔股份~002241~22.26~22.27~0.00~0~0~0~22.26~1004~0.00~0~0.00~0~0.00~0~0.00~0~22.26~1004~0.00~558~0.00~0~0.00~0~0.00~0~~20250509092233~-0.01~-0.04~0.00~0.00~22.26/0/0~0~0~0.00~28.21~~0.00~0.00~0.00~686.46~777.09~2.31~24.50~20.04~0.00~-558~0.00~41.44~29.16~~~1.24~0.0000~0.0000~0~
//~GP-A~-13.75~6.76~1.09~8.18~3.39~30.63~15.70~6.87~17.47~-23.95~3083811231~3490989083~-21.75~12.02~3083811231~~~39.36~-0.04~~CNY~0~~0.00~0";
datas := strutil.SplitAndTrim(data, "=", "\"")
if len(datas) < 2 {
return nil, fmt.Errorf("invalid data format")
}
var result map[string]string
var err error
if strutil.ContainsAny(datas[0], []string{"v_r_hk", "v_hk"}) {
if strutil.ContainsAny(datas[0], []string{"v_r_hk", "v_hk", "v_sz", "v_sh"}) {
result, err = ParseTxHKStockData(datas)
}
@ -640,12 +647,50 @@ func ParseTxHKStockData(datas []string) (map[string]string, error) {
result["今日最高价"] = parts[33]
result["今日最低价"] = parts[34]
timestr := strutil.ReplaceWithMap(parts[30], map[string]string{
"/": "-",
})
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh"}) {
result["买一报价"] = parts[9]
result["买一申报"] = parts[10]
result["买二报价"] = parts[11]
result["买二申报"] = parts[12]
result["买三报价"] = parts[13]
result["买三申报"] = parts[14]
result["买四报价"] = parts[15]
result["买四申报"] = parts[16]
result["买五报价"] = parts[17]
result["买五申报"] = parts[18]
result["卖一报价"] = parts[19]
result["卖一申报"] = parts[20]
result["卖二报价"] = parts[21]
result["卖二申报"] = parts[22]
result["卖三报价"] = parts[23]
result["卖三申报"] = parts[24]
result["卖四报价"] = parts[25]
result["卖四申报"] = parts[26]
result["卖五报价"] = parts[27]
result["卖五申报"] = parts[28]
}
timestr := ""
if strutil.ContainsAny(parts[30], []string{"/"}) {
timestr = strutil.ReplaceWithMap(parts[30], map[string]string{
"/": "-",
"\n": " ",
})
result["日期"] = strutil.SplitAndTrim(timestr, " ", "")[0]
result["时间"] = strutil.SplitAndTrim(timestr, " ", "")[1]
} else {
result["日期"] = strutil.Trim(parts[29])[0:4] + "-" + strutil.Trim(parts[29])[4:6] + "-" + strutil.Trim(parts[29])[6:8]
result["时间"] = strutil.Trim(parts[29])[8:10] + ":" + strutil.Trim(parts[29])[10:12] + ":" + strutil.Trim(parts[29])[12:14]
result["今日最高价"] = parts[32]
result["今日最低价"] = parts[33]
}
//logger.SugaredLogger.Infof("股票数据解析完成 %s %s 时间: %s,%s", parts[1], parts[3], parts[29], parts[30])
//logger.SugaredLogger.Infof("股票数据解析完成 时间: %v", timestr)
result["日期"] = strutil.SplitAndTrim(timestr, " ", "")[0]
result["时间"] = strutil.SplitAndTrim(timestr, " ", "")[1]
//logger.SugaredLogger.Infof("股票数据解析完成: %v", result)

View File

@ -116,6 +116,9 @@ func TestGetHKStockInfo(t *testing.T) {
func TestParseTxStockData(t *testing.T) {
str := "v_r_hk09660=\"100~地平线机器人-W~09660~6.340~5.690~5.800~210980204.0~0~0~6.340~0~0~0~0~0~0~0~0~0~6.340~0~0~0~0~0~0~0~0~0~210980204.0~2025/04/29\n14:14:52~0.650~11.42~6.450~5.710~6.340~210980204.0~1295585259.040~0~33.03~~0~0~13.01~702.2123~836.8986~HORIZONROBOT-W~0.00~10.380~3.320~1.00~-53.74~0~0~0~0~0~33.03~6.50~1.90~600~76.11~19.85~GP~19.70~11.51~0.63~-17.23~46.76~13200293682.00~11075904412.00~33.03~0.000~6.141~58.90~HKD~1~30\";"
//str = "v_sz002241=\"51~歌尔股份~002241~22.26~22.27~0.00~0~0~0~22.26~1004~0.00~0~0.00~0~0.00~0~0.00~0~22.26~1004~0.00~558~0.00~0~0.00~0~0.00~0~~20250509092233~-0.01~-0.04~0.00~0.00~22.26/0/0~0~0~0.00~28.21~~0.00~0.00~0.00~686.46~777.09~2.31~24.50~20.04~0.00~-558~0.00~41.44~29.16~~~1.24~0.0000~0.0000~0~\n~GP-A~-13.75~6.76~1.09~8.18~3.39~30.63~15.70~6.87~17.47~-23.95~3083811231~3490989083~-21.75~12.02~3083811231~~~39.36~-0.04~~CNY~0~~0.00~0\";"
str = "v_sz002241=\"51~歌尔股份~002241~21.92~22.27~22.14~109872~40211~69642~21.91~25~21.90~961~21.89~257~21.88~748~21.87~665~21.92~86~21.93~168~21.94~556~21.95~171~21.96~85~~20250509094209~-0.35~-1.57~22.16~21.84~21.92/109872/241183171~109872~24118~0.36~27.78~~22.16~21.84~1.44~675.97~765.22~2.27~24.50~20.04~2.57~1590~21.95~40.80~28.71~~~1.24~24118.3171~0.0000~0~\n~GP-A~-15.07~5.13~1.11~8.18~3.39~30.63~15.70~5.23~15.67~-25.11~3083811231~3490989083~42.72~10.31~3083811231~~~37.23~0.18~~CNY~0~~21.85~1952\";"
//str = "v_r_hk09660=\"100~地平线机器人-W~09660~6.860~7.000~7.010~21157200.0~0~0~6.860~0~0~0~0~0~0~0~0~0~6.860~0~0~0~0~0~0~0~0~0~21157200.0~2025/05/09\n09:43:13~-0.140~-2.00~7.030~6.730~6.860~21157200.0~144331073.000~0~35.74~~0~0~4.29~759.8070~905.5401~HORIZONROBOT-W~0.00~10.380~3.320~2.93~11.10~0~0~0~0~0~35.74~7.04~0.19~600~90.56~4.73~GP~19.70~11.51~17.26~48.48~13.58~13200293682.00~11075904412.00~35.74~0.000~6.822~71.93~HKD~1~30\";"
info, _ := ParseTxStockData(str)
logger.SugaredLogger.Infof("%+#v", info)
}

View File

@ -632,7 +632,7 @@ function showFsChart(code, name) {
let option = {
title: {
subtext: "["+result.date+"] 开盘:"+openprice+" 收盘:"+closeprice+" 最高:"+max+" 最低:"+min,
subtext: "["+result.date+"] 开盘:"+openprice+" 最新:"+closeprice+" 最高:"+max+" 最低:"+min,
left: 'center',
top: '10',
textStyle: {
@ -1585,6 +1585,52 @@ function delStockGroup(code,name,groupId){
<n-text :type="'info'">{{"今开 "+result["今日开盘价"]}}</n-text>
</n-gi>
</n-grid>
<n-collapse accordion v-if="result['买一报价']>0">
<n-collapse-item title="盘口" name="1" v-if="result['买一报价']>0">
<template #header-extra>
<n-flex justify="space-between">
<n-text :type="'info'">{{"买一 "+result["买一报价"]+'('+result["买一申报"]+")"}}</n-text>
<n-text :type="'info'">{{"卖一 "+result["卖一报价"]+'('+result["卖一申报"]+")"}}</n-text>
</n-flex>
</template>
<n-grid :cols="2" :y-gap="4" :x-gap="4" >
<n-gi v-if="result['买一报价']>0">
<n-text :type="'info'">{{"买一 "+result["买一报价"]+'('+result["买一申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['卖一报价']>0">
<n-text :type="'info'">{{"卖一 "+result["卖一报价"]+'('+result["卖一申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['买二报价']>0">
<n-text :type="'info'">{{"买二 "+result["买二报价"]+'('+result["买二申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['卖二报价']>0">
<n-text :type="'info'">{{"卖二 "+result["卖二报价"]+'('+result["卖二申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['买三报价']>0">
<n-text :type="'info'">{{"买三 "+result["买三报价"]+'('+result["买三申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['卖三报价']>0">
<n-text :type="'info'">{{"买三 "+result["卖三报价"]+'('+result["卖三申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['买四报价']>0">
<n-text :type="'info'">{{"买四 "+result["买四报价"]+'('+result["买四申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['卖四报价']>0">
<n-text :type="'info'">{{"卖四 "+result["卖四报价"]+'('+result["卖四申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['买五报价']>0">
<n-text :type="'info'">{{"买五 "+result["买五报价"]+'('+result["买五申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['卖五报价']>0">
<n-text :type="'info'">{{"卖五 "+result["卖五报价"]+'('+result["卖五申报"]+")"}}</n-text>
</n-gi>
</n-grid>
</n-collapse-item>
</n-collapse>
<template #header-extra>
<n-tag size="small" :bordered="false">{{result['股票代码']}}</n-tag>&nbsp;
@ -1650,6 +1696,52 @@ function delStockGroup(code,name,groupId){
<n-text :type="'info'">{{"今开 "+result["今日开盘价"]}}</n-text>
</n-gi>
</n-grid>
<n-collapse accordion v-if="result['买一报价']>0">
<n-collapse-item title="盘口" name="1" v-if="result['买一报价']>0">
<template #header-extra>
<n-flex justify="space-between">
<n-text :type="'info'">{{"买一 "+result["买一报价"]+'('+result["买一申报"]+")"}}</n-text>
<n-text :type="'info'">{{"卖一 "+result["卖一报价"]+'('+result["卖一申报"]+")"}}</n-text>
</n-flex>
</template>
<n-grid :cols="2" :y-gap="4" :x-gap="4" >
<n-gi v-if="result['买一报价']>0">
<n-text :type="'info'">{{"买一 "+result["买一报价"]+'('+result["买一申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['卖一报价']>0">
<n-text :type="'info'">{{"卖一 "+result["卖一报价"]+'('+result["卖一申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['买二报价']>0">
<n-text :type="'info'">{{"买二 "+result["买二报价"]+'('+result["买二申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['卖二报价']>0">
<n-text :type="'info'">{{"卖二 "+result["卖二报价"]+'('+result["卖二申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['买三报价']>0">
<n-text :type="'info'">{{"买三 "+result["买三报价"]+'('+result["买三申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['卖三报价']>0">
<n-text :type="'info'">{{"买三 "+result["卖三报价"]+'('+result["卖三申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['买四报价']>0">
<n-text :type="'info'">{{"买四 "+result["买四报价"]+'('+result["买四申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['卖四报价']>0">
<n-text :type="'info'">{{"卖四 "+result["卖四报价"]+'('+result["卖四申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['买五报价']>0">
<n-text :type="'info'">{{"买五 "+result["买五报价"]+'('+result["买五申报"]+")"}}</n-text>
</n-gi>
<n-gi v-if="result['卖五报价']>0">
<n-text :type="'info'">{{"卖五 "+result["卖五报价"]+'('+result["卖五申报"]+")"}}</n-text>
</n-gi>
</n-grid>
</n-collapse-item>
</n-collapse>
<template #header-extra>
<n-tag size="small" :bordered="false">{{result['股票代码']}}</n-tag>&nbsp;

36
main.go
View File

@ -55,22 +55,7 @@ var VersionCommit string
func main() {
checkDir("data")
db.Init("")
db.Dao.AutoMigrate(&data.StockInfo{})
db.Dao.AutoMigrate(&data.StockBasic{})
db.Dao.AutoMigrate(&data.FollowedStock{})
db.Dao.AutoMigrate(&data.IndexBasic{})
db.Dao.AutoMigrate(&data.Settings{})
db.Dao.AutoMigrate(&models.AIResponseResult{})
db.Dao.AutoMigrate(&models.StockInfoHK{})
db.Dao.AutoMigrate(&models.StockInfoUS{})
db.Dao.AutoMigrate(&data.FollowedFund{})
db.Dao.AutoMigrate(&data.FundBasic{})
db.Dao.AutoMigrate(&models.PromptTemplate{})
db.Dao.AutoMigrate(&data.Group{})
db.Dao.AutoMigrate(&data.GroupStock{})
db.Dao.AutoMigrate(&models.Tags{})
db.Dao.AutoMigrate(&models.Telegraph{})
db.Dao.AutoMigrate(&models.TelegraphTags{})
go AutoMigrate()
//db.Dao.Model(&data.Group{}).Where("id = ?", 0).FirstOrCreate(&data.Group{
// Name: "默认分组",
@ -214,6 +199,25 @@ func main() {
}
func AutoMigrate() {
db.Dao.AutoMigrate(&data.StockInfo{})
db.Dao.AutoMigrate(&data.StockBasic{})
db.Dao.AutoMigrate(&data.FollowedStock{})
db.Dao.AutoMigrate(&data.IndexBasic{})
db.Dao.AutoMigrate(&data.Settings{})
db.Dao.AutoMigrate(&models.AIResponseResult{})
db.Dao.AutoMigrate(&models.StockInfoHK{})
db.Dao.AutoMigrate(&models.StockInfoUS{})
db.Dao.AutoMigrate(&data.FollowedFund{})
db.Dao.AutoMigrate(&data.FundBasic{})
db.Dao.AutoMigrate(&models.PromptTemplate{})
db.Dao.AutoMigrate(&data.Group{})
db.Dao.AutoMigrate(&data.GroupStock{})
db.Dao.AutoMigrate(&models.Tags{})
db.Dao.AutoMigrate(&models.Telegraph{})
db.Dao.AutoMigrate(&models.TelegraphTags{})
}
func initStockDataUS() {
var v []models.StockInfoUS
err := json.Unmarshal(stocksBinUS, &v)