Files
Blizzard 92329ddce2
Checks / verify (push) Failing after 9m52s
feat: 完成模拟交易闭环、App 与 A 股行情接入
统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
2026-10-08 22:58:52 +08:00

108 lines
3.5 KiB
Go

package service
import (
"context"
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
"git.sundynix.cn/Blizzard/Veyra/internal/model"
"github.com/google/uuid"
"go.uber.org/zap"
"time"
)
type NotificationRepository interface {
ProcessNext(context.Context) (bool, error)
List(context.Context, string) ([]model.Notification, error)
MarkRead(context.Context, string, string) error
Tasks(context.Context) ([]model.NotificationTask, error)
SaveQuote(context.Context, model.QuoteSnapshot, string, string) (model.QuoteSnapshot, error)
Quotes(context.Context) ([]model.QuoteSnapshot, error)
}
type NotificationService struct {
repo NotificationRepository
development bool
}
func NewNotificationService(repo NotificationRepository, development bool) *NotificationService {
return &NotificationService{repo, development}
}
func (s *NotificationService) authorize(actor model.User, admin bool) error {
if actor.Status != "ACTIVE" || (admin && actor.Role != "ADMIN") || (!admin && actor.Role != "USER") {
return apperror.ErrForbidden
}
if s == nil || s.repo == nil || !s.development {
return apperror.ErrUnavailable
}
return nil
}
func (s *NotificationService) Run(ctx context.Context, logger *zap.Logger) {
if s == nil || s.repo == nil || !s.development {
return
}
timer := time.NewTicker(2 * time.Second)
defer timer.Stop()
for {
select {
case <-ctx.Done():
return
case <-timer.C:
for i := 0; i < 50; i++ {
worked, err := s.repo.ProcessNext(ctx)
if err != nil {
if ctx.Err() == nil {
logger.Error("notification processing failed")
}
break
}
if !worked {
break
}
}
}
}
}
func (s *NotificationService) List(ctx context.Context, actor model.User) ([]model.Notification, error) {
if err := s.authorize(actor, false); err != nil {
return nil, err
}
return s.repo.List(ctx, actor.ID)
}
func (s *NotificationService) MarkRead(ctx context.Context, actor model.User, id string) error {
if err := s.authorize(actor, false); err != nil {
return err
}
if _, err := uuid.Parse(id); err != nil {
return apperror.ErrInvalidInput
}
return s.repo.MarkRead(ctx, actor.ID, id)
}
func (s *NotificationService) Tasks(ctx context.Context, actor model.User) ([]model.NotificationTask, error) {
if err := s.authorize(actor, true); err != nil {
return nil, err
}
return s.repo.Tasks(ctx)
}
func (s *NotificationService) Quotes(ctx context.Context, actor model.User) ([]model.QuoteSnapshot, error) {
if err := s.authorize(actor, true); err != nil {
return nil, err
}
return s.repo.Quotes(ctx)
}
type SimulationQuoteInput struct {
Symbol string `json:"symbol"`
Exchange string `json:"exchange"`
Price string `json:"price"`
ObservedAt time.Time `json:"observed_at"`
}
func (s *NotificationService) SaveQuote(ctx context.Context, actor model.User, input SimulationQuoteInput, requestID string) (model.QuoteSnapshot, error) {
if err := s.authorize(actor, true); err != nil {
return model.QuoteSnapshot{}, err
}
price, err := model.ParseMoney(input.Price)
if err != nil || price == 0 || !fillSymbolPattern.MatchString(input.Symbol) || (input.Exchange != "SSE" && input.Exchange != "SZSE") || input.ObservedAt.Before(time.Now().Add(-time.Minute)) || input.ObservedAt.After(time.Now().Add(time.Minute)) {
return model.QuoteSnapshot{}, apperror.ErrInvalidInput
}
return s.repo.SaveQuote(ctx, model.QuoteSnapshot{Symbol: input.Symbol, Exchange: input.Exchange, PriceMinor: price, ObservedAt: input.ObservedAt, DataMode: "simulation"}, actor.ID, requestID)
}