package service import ( "context" "fmt" "time" "market-data-service/adapter" "market-data-service/api" "market-data-service/internal/repository" ) // StockServiceImpl 股票服务实现 type StockServiceImpl struct { repo *repository.StockRepository adapter adapter.DataSourceAdapter } // NewStockService 创建股票服务 func NewStockService(repo *repository.StockRepository, adapter adapter.DataSourceAdapter) StockService { return &StockServiceImpl{ repo: repo, adapter: adapter, } } // QueryKLines 查询K线数据 // 实现回源机制:先查数据库,如数据缺失则从数据源获取并保存 func (s *StockServiceImpl) QueryKLines(ctx context.Context, req *api.KLineQueryRequest) (*api.KLineData, error) { // 解析日期 start, err := time.Parse("20060102", req.Start) if err != nil { return nil, fmt.Errorf("invalid start date: %w", err) } end, err := time.Parse("20060102", req.End) if err != nil { return nil, fmt.Errorf("invalid end date: %w", err) } end = end.Add(24 * time.Hour).Add(-time.Second) // 包含结束日期全天 // 1. 先尝试从数据库获取数据 items, err := s.repo.GetKLines(ctx, req.Symbol, req.Freq, start, end, req.Adjust) if err != nil { return nil, err } // 2. 判断数据是否完整(简单策略:如果数据为空或数量明显不足,则触发回源) shouldFetchFromSource := len(items) == 0 || s.isDataIncomplete(req.Start, req.End, req.Freq, len(items)) // 3. 如果数据不完整且有配置适配器,则从数据源获取 if shouldFetchFromSource && s.adapter != nil { fetchedItems, err := s.fetchFromSourceAndSave(ctx, req, start, end) if err == nil && len(fetchedItems) > 0 { items = fetchedItems } } // 4. 处理复权 if req.Adjust != api.AdjustNone { items = s.applyAdjust(ctx, req.Symbol, items, req.Adjust) } return &api.KLineData{ Symbol: req.Symbol, Freq: req.Freq, Adjust: req.Adjust, Count: len(items), Items: items, }, nil } // isDataIncomplete 判断数据是否不完整(简单启发式判断) func (s *StockServiceImpl) isDataIncomplete(start, end string, freq api.Frequency, count int) bool { startDate, _ := time.Parse("20060102", start) endDate, _ := time.Parse("20060102", end) expectedDays := int(endDate.Sub(startDate).Hours()/24) + 1 switch freq { case api.Freq1Day: // 日线:预期交易日数量约为自然日数量的 70% expectedTradingDays := int(float64(expectedDays) * 0.7) return count < expectedTradingDays case api.Freq1Week: expectedWeeks := expectedDays / 7 return count < expectedWeeks case api.Freq1Month: expectedMonths := expectedDays / 30 return count < expectedMonths default: // 分钟线:不判断完整性,因为数量难以预估 return false } } // fetchFromSourceAndSave 从数据源获取数据并保存到数据库 func (s *StockServiceImpl) fetchFromSourceAndSave(ctx context.Context, req *api.KLineQueryRequest, start, end time.Time) ([]api.KLineItem, error) { // 从数据源获取 adapterData, err := s.adapter.FetchKLines(req.Symbol, req.Start, req.End, string(req.Freq)) if err != nil { return nil, fmt.Errorf("fetch from source failed: %w", err) } if len(adapterData) == 0 { return nil, nil } // 转换为 repository 格式并保存 saveItems := make([]api.KLineItem, len(adapterData)) resultItems := make([]api.KLineItem, len(adapterData)) for i, d := range adapterData { item := api.KLineItem{ Time: time.Unix(d.Time, 0), Open: d.Open, High: d.High, Low: d.Low, Close: d.Close, Volume: d.Volume, Amount: d.Amount, } saveItems[i] = item resultItems[i] = item } // 异步保存到数据库(不阻塞响应) go func() { saveCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := s.repo.SaveKLines(saveCtx, req.Freq, saveItems); err != nil { // 记录错误但不影响返回结果 fmt.Printf("save klines to db failed: %v\n", err) } }() return resultItems, nil } // applyAdjust 应用复权 func (s *StockServiceImpl) applyAdjust(ctx context.Context, symbol string, items []api.KLineItem, adjustType api.AdjustType) []api.KLineItem { // TODO: 实现复权计算 // 1. 从数据库获取复权系数 // 2. 根据前复权/后复权计算价格 return items } // ListSymbols 查询标的列表 func (s *StockServiceImpl) ListSymbols(ctx context.Context, req *api.SymbolListRequest) (*api.SymbolListData, error) { symbols, total, err := s.repo.ListSymbols(ctx, req) if err != nil { return nil, err } return &api.SymbolListData{ Total: total, Page: req.Page, Size: req.Size, Items: symbols, }, nil } // BatchQueryKLines 批量查询K线 func (s *StockServiceImpl) BatchQueryKLines(ctx context.Context, req *api.BatchKLineRequest) (*api.BatchKLineData, error) { results := make([]api.BatchKLineResult, len(req.Symbols)) for i, symbol := range req.Symbols { singleReq := &api.KLineQueryRequest{ Symbol: symbol, Start: req.Start, End: req.End, Freq: req.Freq, Adjust: req.Adjust, } data, err := s.QueryKLines(ctx, singleReq) results[i] = api.BatchKLineResult{ Symbol: symbol, Success: err == nil, } if err != nil { results[i].Error = err.Error() } else { results[i].Data = &api.KLineSubData{ Count: data.Count, Items: data.Items, } } } return &api.BatchKLineData{Results: results}, nil } // GetTradingDates 获取交易日历 func (s *StockServiceImpl) GetTradingDates(ctx context.Context, req *api.TradingDatesRequest) (*api.TradingDatesData, error) { return s.repo.GetTradingDates(ctx, req.Start, req.End) } // SyncSymbolsFromSource 从数据源同步标的列表 func (s *StockServiceImpl) SyncSymbolsFromSource(ctx context.Context, adapter interface{ FetchSymbols(assetType string) ([]struct { SymbolID string Name string Exchange string }, error) }) error { // TODO: 实现从Tushare同步标的列表 return nil }