新增按交易所同步股票功能,支持 SSE/SZSE/BSE

This commit is contained in:
2026-07-04 14:51:52 +08:00
parent 44fb305f8a
commit d370d2e10a
5 changed files with 145 additions and 42 deletions
+2
View File
@@ -62,6 +62,8 @@ open http://localhost:3018
| PUT | /api/admin/users/:id | 更新用户/角色 | admin / system_admin | | PUT | /api/admin/users/:id | 更新用户/角色 | admin / system_admin |
| DELETE | /api/admin/users/:id | 删除用户 | system_admin | | DELETE | /api/admin/users/:id | 删除用户 | system_admin |
| GET | /api/admin/roles | 角色列表 | admin / system_admin | | GET | /api/admin/roles | 角色列表 | admin / system_admin |
| POST | /api/admin/data-sync/init-stocks | 初始化全部上市股票 | admin / system_admin |
| POST | /api/admin/data-sync/stocks/:exchange | 按交易所同步股票(SSE/SZSE/BSE | admin / system_admin |
## 目录结构 ## 目录结构
+49 -21
View File
@@ -25,11 +25,22 @@ type StockBasic struct {
Name string Name string
Area string Area string
Industry string Industry string
Fullname string
Enname string
Cnspell string
Market string Market string
Exchange string Exchange string
CurrType string
ListStatus string ListStatus string
ListDate string
DelistDate string
IsHs string
ActName string
ActEntType string
} }
const stockBasicFields = "ts_code,symbol,name,area,industry,fullname,enname,cnspell,market,exchange,curr_type,list_status,list_date,delist_date,is_hs,act_name,act_ent_type"
// Client 封装 Tushare Pro HTTP API 调用。 // Client 封装 Tushare Pro HTTP API 调用。
type Client struct { type Client struct {
token string token string
@@ -46,43 +57,60 @@ func NewClient() *Client {
} }
} }
func parseStockBasic(row []any, col map[string]int) StockBasic {
return StockBasic{
TsCode: stringAt(row, col["ts_code"]),
Symbol: stringAt(row, col["symbol"]),
Name: stringAt(row, col["name"]),
Area: stringAt(row, col["area"]),
Industry: stringAt(row, col["industry"]),
Fullname: stringAt(row, col["fullname"]),
Enname: stringAt(row, col["enname"]),
Cnspell: stringAt(row, col["cnspell"]),
Market: stringAt(row, col["market"]),
Exchange: stringAt(row, col["exchange"]),
CurrType: stringAt(row, col["curr_type"]),
ListStatus: stringAt(row, col["list_status"]),
ListDate: stringAt(row, col["list_date"]),
DelistDate: stringAt(row, col["delist_date"]),
IsHs: stringAt(row, col["is_hs"]),
ActName: stringAt(row, col["act_name"]),
ActEntType: stringAt(row, col["act_ent_type"]),
}
}
// ListStocks 通过 stock_basic 接口获取全部上市股票基础信息。 // ListStocks 通过 stock_basic 接口获取全部上市股票基础信息。
func (c *Client) ListStocks() ([]StockBasic, error) { func (c *Client) ListStocks() ([]StockBasic, error) {
params := map[string]any{ params := map[string]any{
"list_status": "L", "list_status": "L",
"fields": "ts_code,symbol,name,area,industry,market,exchange,list_status", "fields": stockBasicFields,
} }
return c.listStockBasic(params)
}
// ListStocksByExchange 按交易所获取股票基础信息。
func (c *Client) ListStocksByExchange(exchange string) ([]StockBasic, error) {
params := map[string]any{
"exchange": exchange,
"fields": stockBasicFields,
}
return c.listStockBasic(params)
}
func (c *Client) listStockBasic(params map[string]any) ([]StockBasic, error) {
fields, items, err := c.call("stock_basic", params) fields, items, err := c.call("stock_basic", params)
if err != nil { if err != nil {
return nil, fmt.Errorf("stock_basic: %w", err) return nil, fmt.Errorf("stock_basic: %w", err)
} }
col := buildColumnMap(fields) col := buildColumnMap(fields)
idxTsCode := col["ts_code"]
idxSymbol := col["symbol"]
idxName := col["name"]
idxArea := col["area"]
idxIndustry := col["industry"]
idxMarket := col["market"]
idxExchange := col["exchange"]
idxListStatus := col["list_status"]
stocks := make([]StockBasic, 0, len(items)) stocks := make([]StockBasic, 0, len(items))
for _, row := range items { for _, row := range items {
code := stringAt(row, idxTsCode) code := stringAt(row, col["ts_code"])
if code == "" { if code == "" {
continue continue
} }
stocks = append(stocks, StockBasic{ stocks = append(stocks, parseStockBasic(row, col))
TsCode: code,
Symbol: stringAt(row, idxSymbol),
Name: stringAt(row, idxName),
Area: stringAt(row, idxArea),
Industry: stringAt(row, idxIndustry),
Market: stringAt(row, idxMarket),
Exchange: stringAt(row, idxExchange),
ListStatus: stringAt(row, idxListStatus),
})
} }
return stocks, nil return stocks, nil
} }
+73 -10
View File
@@ -9,8 +9,37 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
var validExchanges = map[string]string{
"SSE": "上交所",
"SZSE": "深交所",
"BSE": "北交所",
}
func toModelStock(s datasource.StockBasic) models.Stock {
return models.Stock{
TsCode: s.TsCode,
Symbol: s.Symbol,
Name: s.Name,
Area: s.Area,
Industry: s.Industry,
Fullname: s.Fullname,
Enname: s.Enname,
Cnspell: s.Cnspell,
Market: s.Market,
Exchange: s.Exchange,
CurrType: s.CurrType,
ListStatus: s.ListStatus,
ListDate: s.ListDate,
DelistDate: s.DelistDate,
IsHs: s.IsHs,
ActName: s.ActName,
ActEntType: s.ActEntType,
}
}
// DataSyncHandler 处理数据同步相关接口。 // DataSyncHandler 处理数据同步相关接口。
type DataSyncHandler struct { type DataSyncHandler struct {
DB *gorm.DB DB *gorm.DB
@@ -40,16 +69,7 @@ func (h *DataSyncHandler) InitStocks(c *gin.Context) {
records := make([]models.Stock, 0, len(stocks)) records := make([]models.Stock, 0, len(stocks))
for _, s := range stocks { for _, s := range stocks {
records = append(records, models.Stock{ records = append(records, toModelStock(s))
TsCode: s.TsCode,
Symbol: s.Symbol,
Name: s.Name,
Area: s.Area,
Industry: s.Industry,
Market: s.Market,
Exchange: s.Exchange,
ListStatus: s.ListStatus,
})
} }
if err := h.DB.Exec("TRUNCATE TABLE stocks RESTART IDENTITY").Error; err != nil { if err := h.DB.Exec("TRUNCATE TABLE stocks RESTART IDENTITY").Error; err != nil {
@@ -72,3 +92,46 @@ func (h *DataSyncHandler) InitStocks(c *gin.Context) {
}, },
}) })
} }
type syncStocksByExchangeResponse struct {
Exchange string `json:"exchange"`
Count int `json:"count"`
Message string `json:"message"`
}
// SyncStocksByExchange 按交易所从 Tushare 同步股票基础信息。
func (h *DataSyncHandler) SyncStocksByExchange(c *gin.Context) {
exchange := strings.ToUpper(strings.TrimSpace(c.Param("exchange")))
if _, ok := validExchanges[exchange]; !ok {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "无效的交易所代码,支持 SSE/SZSE/BSE"})
return
}
stocks, err := h.client.ListStocksByExchange(exchange)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
records := make([]models.Stock, 0, len(stocks))
for _, s := range stocks {
records = append(records, toModelStock(s))
}
if err := h.DB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "ts_code"}},
UpdateAll: true,
}).CreateInBatches(records, 500).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "写入股票列表失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": syncStocksByExchangeResponse{
Exchange: exchange,
Count: len(records),
Message: validExchanges[exchange] + "股票同步完成",
},
})
}
+20 -11
View File
@@ -8,17 +8,26 @@ import (
// Stock 存储股票基础信息。 // Stock 存储股票基础信息。
type Stock struct { type Stock struct {
ID string `json:"id" gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` ID string `json:"id" gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
TsCode string `json:"ts_code" gorm:"size:32;not null;uniqueIndex"` TsCode string `json:"ts_code" gorm:"size:32;not null;uniqueIndex"`
Symbol string `json:"symbol" gorm:"size:32;not null;index"` Symbol string `json:"symbol" gorm:"size:32;not null;index"`
Name string `json:"name" gorm:"size:128"` Name string `json:"name" gorm:"size:128"`
Area string `json:"area" gorm:"size:64"` Area string `json:"area" gorm:"size:64"`
Industry string `json:"industry" gorm:"size:64"` Industry string `json:"industry" gorm:"size:64"`
Market string `json:"market" gorm:"size:16"` Fullname string `json:"fullname" gorm:"size:256"`
Exchange string `json:"exchange" gorm:"size:16"` Enname string `json:"enname" gorm:"size:256"`
ListStatus string `json:"list_status" gorm:"size:8"` Cnspell string `json:"cnspell" gorm:"size:64"`
CreatedAt time.Time `json:"created_at"` Market string `json:"market" gorm:"size:16"`
UpdatedAt time.Time `json:"updated_at"` Exchange string `json:"exchange" gorm:"size:16;index"`
CurrType string `json:"curr_type" gorm:"size:16"`
ListStatus string `json:"list_status" gorm:"size:8"`
ListDate string `json:"list_date" gorm:"size:16"`
DelistDate string `json:"delist_date" gorm:"size:16"`
IsHs string `json:"is_hs" gorm:"size:8"`
ActName string `json:"act_name" gorm:"size:128"`
ActEntType string `json:"act_ent_type" gorm:"size:64"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
// AutoMigrateStocks 迁移股票基础信息表。 // AutoMigrateStocks 迁移股票基础信息表。
+1
View File
@@ -52,6 +52,7 @@ func Setup(cfg *config.Config, db *gorm.DB) *gin.Engine {
admin.DELETE("/users/:id", adminHandler.DeleteUser) admin.DELETE("/users/:id", adminHandler.DeleteUser)
admin.GET("/roles", adminHandler.ListRoles) admin.GET("/roles", adminHandler.ListRoles)
admin.POST("/data-sync/init-stocks", dataSyncHandler.InitStocks) admin.POST("/data-sync/init-stocks", dataSyncHandler.InitStocks)
admin.POST("/data-sync/stocks/:exchange", dataSyncHandler.SyncStocksByExchange)
} }
return r return r