新增按交易所同步股票功能,支持 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
+49 -21
View File
@@ -25,11 +25,22 @@ type StockBasic struct {
Name string
Area string
Industry string
Fullname string
Enname string
Cnspell string
Market string
Exchange string
CurrType 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 调用。
type Client struct {
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 接口获取全部上市股票基础信息。
func (c *Client) ListStocks() ([]StockBasic, error) {
params := map[string]any{
"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)
if err != nil {
return nil, fmt.Errorf("stock_basic: %w", err)
}
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))
for _, row := range items {
code := stringAt(row, idxTsCode)
code := stringAt(row, col["ts_code"])
if code == "" {
continue
}
stocks = append(stocks, StockBasic{
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),
})
stocks = append(stocks, parseStockBasic(row, col))
}
return stocks, nil
}
+73 -10
View File
@@ -9,8 +9,37 @@ import (
"github.com/gin-gonic/gin"
"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 处理数据同步相关接口。
type DataSyncHandler struct {
DB *gorm.DB
@@ -40,16 +69,7 @@ func (h *DataSyncHandler) InitStocks(c *gin.Context) {
records := make([]models.Stock, 0, len(stocks))
for _, s := range stocks {
records = append(records, models.Stock{
TsCode: s.TsCode,
Symbol: s.Symbol,
Name: s.Name,
Area: s.Area,
Industry: s.Industry,
Market: s.Market,
Exchange: s.Exchange,
ListStatus: s.ListStatus,
})
records = append(records, toModelStock(s))
}
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 存储股票基础信息。
type Stock struct {
ID string `json:"id" gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
TsCode string `json:"ts_code" gorm:"size:32;not null;uniqueIndex"`
Symbol string `json:"symbol" gorm:"size:32;not null;index"`
Name string `json:"name" gorm:"size:128"`
Area string `json:"area" gorm:"size:64"`
Industry string `json:"industry" gorm:"size:64"`
Market string `json:"market" gorm:"size:16"`
Exchange string `json:"exchange" gorm:"size:16"`
ListStatus string `json:"list_status" gorm:"size:8"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID string `json:"id" gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
TsCode string `json:"ts_code" gorm:"size:32;not null;uniqueIndex"`
Symbol string `json:"symbol" gorm:"size:32;not null;index"`
Name string `json:"name" gorm:"size:128"`
Area string `json:"area" gorm:"size:64"`
Industry string `json:"industry" gorm:"size:64"`
Fullname string `json:"fullname" gorm:"size:256"`
Enname string `json:"enname" gorm:"size:256"`
Cnspell string `json:"cnspell" gorm:"size:64"`
Market string `json:"market" gorm:"size:16"`
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 迁移股票基础信息表。
+1
View File
@@ -52,6 +52,7 @@ func Setup(cfg *config.Config, db *gorm.DB) *gin.Engine {
admin.DELETE("/users/:id", adminHandler.DeleteUser)
admin.GET("/roles", adminHandler.ListRoles)
admin.POST("/data-sync/init-stocks", dataSyncHandler.InitStocks)
admin.POST("/data-sync/stocks/:exchange", dataSyncHandler.SyncStocksByExchange)
}
return r