From d370d2e10a1d62f237f8b272d4390032f9ea3314 Mon Sep 17 00:00:00 2001 From: fish Date: Sat, 4 Jul 2026 14:51:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=8C=89=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E6=89=80=E5=90=8C=E6=AD=A5=E8=82=A1=E7=A5=A8=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=20SSE/SZSE/BSE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + backend/internal/datasource/tushare.go | 70 +++++++++++++++------- backend/internal/handlers/data_sync.go | 83 ++++++++++++++++++++++---- backend/internal/models/stock.go | 31 ++++++---- backend/internal/routes/routes.go | 1 + 5 files changed, 145 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 4b9e38a..dfe2280 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ open http://localhost:3018 | PUT | /api/admin/users/:id | 更新用户/角色 | admin / system_admin | | DELETE | /api/admin/users/:id | 删除用户 | 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 | ## 目录结构 diff --git a/backend/internal/datasource/tushare.go b/backend/internal/datasource/tushare.go index 9031d74..b1cf0bf 100644 --- a/backend/internal/datasource/tushare.go +++ b/backend/internal/datasource/tushare.go @@ -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 } diff --git a/backend/internal/handlers/data_sync.go b/backend/internal/handlers/data_sync.go index 2ddc43a..0b9c5d3 100644 --- a/backend/internal/handlers/data_sync.go +++ b/backend/internal/handlers/data_sync.go @@ -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] + "股票同步完成", + }, + }) +} diff --git a/backend/internal/models/stock.go b/backend/internal/models/stock.go index b143740..1a6ee61 100644 --- a/backend/internal/models/stock.go +++ b/backend/internal/models/stock.go @@ -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 迁移股票基础信息表。 diff --git a/backend/internal/routes/routes.go b/backend/internal/routes/routes.go index 729e396..5c0d495 100644 --- a/backend/internal/routes/routes.go +++ b/backend/internal/routes/routes.go @@ -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