新增按交易所同步股票功能,支持 SSE/SZSE/BSE
This commit is contained in:
@@ -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 |
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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] + "股票同步完成",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,9 +14,18 @@ type Stock struct {
|
|||||||
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"`
|
||||||
|
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"`
|
Market string `json:"market" gorm:"size:16"`
|
||||||
Exchange string `json:"exchange" 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"`
|
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"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user