feat: 完成模拟交易闭环、App 与 A 股行情接入
Checks / verify (push) Failing after 9m52s

统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
This commit is contained in:
2026-10-08 22:58:52 +08:00
parent bffdb3858b
commit 92329ddce2
145 changed files with 18593 additions and 587 deletions
+145
View File
@@ -0,0 +1,145 @@
package service
import (
"context"
"errors"
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
"git.sundynix.cn/Blizzard/Veyra/internal/model"
"go.uber.org/zap"
"strings"
"time"
"unicode/utf8"
)
type MarketRepository interface {
List(context.Context, string) ([]model.Instrument, error)
Save(context.Context, model.ProviderConfig, []model.Instrument, time.Time, string) error
Tasks(context.Context) ([]model.MarketSync, error)
}
type MarketAdapter interface {
Snapshot(context.Context, model.ProviderConfig) ([]model.Instrument, error)
History(context.Context, model.ProviderConfig, string, string, string, string) (model.MarketHistory, error)
}
type MarketService struct {
repo MarketRepository
providers ProviderRepository
adapter MarketAdapter
development bool
}
func NewMarketService(repo MarketRepository, providers ProviderRepository, adapter MarketAdapter, development bool) *MarketService {
return &MarketService{repo, providers, adapter, development}
}
func (s *MarketService) authorize(u model.User) error {
if u.Status != "ACTIVE" || (u.Role != "ADMIN" && u.Role != "USER") {
return apperror.ErrForbidden
}
if s == nil || s.repo == nil || s.providers == nil || !s.development {
return apperror.ErrUnavailable
}
return nil
}
func (s *MarketService) configuration(ctx context.Context) (model.ProviderConfig, error) {
c, e := s.providers.Get(ctx, "market_primary")
if e != nil {
return c, e
}
if !c.Enabled || (c.MarketAdapter != "eastmoney_public" && c.MarketAdapter != "sina_public") {
return c, apperror.ErrUnavailable
}
return c, nil
}
func (s *MarketService) List(ctx context.Context, u model.User, query string) ([]model.Instrument, error) {
if e := s.authorize(u); e != nil {
return nil, e
}
query = strings.TrimSpace(query)
if utf8.RuneCountInString(query) > 80 {
return nil, apperror.ErrInvalidInput
}
return s.repo.List(ctx, query)
}
func (s *MarketService) Tasks(ctx context.Context, u model.User) ([]model.MarketSync, error) {
if e := s.authorize(u); e != nil {
return nil, e
}
if u.Role != "ADMIN" {
return nil, apperror.ErrForbidden
}
return s.repo.Tasks(ctx)
}
func (s *MarketService) History(ctx context.Context, u model.User, symbol, exchange, start, end string) (model.MarketHistory, error) {
if e := s.authorize(u); e != nil {
return model.MarketHistory{}, e
}
if len(symbol) != 6 || (exchange != "SSE" && exchange != "SZSE") {
return model.MarketHistory{}, apperror.ErrInvalidInput
}
for _, r := range symbol {
if r < '0' || r > '9' {
return model.MarketHistory{}, apperror.ErrInvalidInput
}
}
a, e1 := time.Parse("2006-01-02", start)
b, e2 := time.Parse("2006-01-02", end)
if e1 != nil || e2 != nil || a.After(b) || b.Sub(a) > 1460*24*time.Hour || b.After(time.Now()) {
return model.MarketHistory{}, apperror.ErrInvalidInput
}
c, e := s.configuration(ctx)
if e != nil {
return model.MarketHistory{}, e
}
result, e := s.adapter.History(ctx, c, symbol, exchange, start, end)
if e != nil {
return result, apperror.ErrUnavailable
}
return result, nil
}
func (s *MarketService) Sync(ctx context.Context) error {
c, e := s.configuration(ctx)
if errors.Is(e, apperror.ErrNotFound) || errors.Is(e, apperror.ErrUnavailable) {
return nil
}
if e != nil {
return e
}
started := time.Now().UTC()
list, e := s.adapter.Snapshot(ctx, c)
if ctx.Err() != nil {
return ctx.Err()
}
message := ""
if e != nil {
message = "行情同步失败,保留上次快照并标明行情时间。"
}
saveError := s.repo.Save(ctx, c, list, started, message)
if saveError != nil {
return saveError
}
return e
}
func (s *MarketService) Run(ctx context.Context, logger *zap.Logger) {
if s == nil || s.repo == nil || s.providers == nil || s.adapter == nil || !s.development {
return
}
var lastAttempt time.Time
var lastVersion int64
for {
config, err := s.configuration(ctx)
if err == nil && (config.Version != lastVersion || time.Since(lastAttempt) >= 5*time.Minute) {
lastAttempt = time.Now()
lastVersion = config.Version
task, cancel := context.WithTimeout(ctx, 90*time.Second)
err = s.Sync(task)
cancel()
if err != nil && ctx.Err() == nil {
logger.Warn("market synchronization failed", zap.Error(err))
}
}
select {
case <-ctx.Done():
return
case <-time.After(10 * time.Second):
}
}
}