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
+110
View File
@@ -0,0 +1,110 @@
package service
import (
"context"
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
"git.sundynix.cn/Blizzard/Veyra/internal/model"
"github.com/google/uuid"
"regexp"
"strconv"
)
type TradePlanRepository interface {
Preview(context.Context, string, string, string, int64, int64, int64) (model.PlanPreview, error)
Confirm(context.Context, string, string, string) (model.TradePlan, error)
List(context.Context, string) ([]model.TradePlan, error)
Cancel(context.Context, string, string, int64, string) (model.TradePlan, error)
Budget(context.Context, string, string) (model.PlanBudget, error)
}
type TradePlanService struct {
repo TradePlanRepository
development bool
}
func NewTradePlanService(repo TradePlanRepository, development bool) *TradePlanService {
return &TradePlanService{repo, development}
}
func (s *TradePlanService) authorize(actor model.User) error {
if actor.Role != "USER" || actor.Status != "ACTIVE" {
return apperror.ErrForbidden
}
if s == nil || s.repo == nil || !s.development {
return apperror.ErrUnavailable
}
return nil
}
var quantityPattern = regexp.MustCompile(`^[1-9][0-9]{0,6}$`)
func ParseQuantity(value string) (int64, error) {
if !quantityPattern.MatchString(value) {
return 0, apperror.ErrInvalidInput
}
qty, err := strconv.ParseInt(value, 10, 64)
if err != nil || qty > 1000000 {
return 0, apperror.ErrInvalidInput
}
return qty, nil
}
type PlanPreviewInput struct {
AccountID string `json:"account_id"`
CandidateRevisionID string `json:"candidate_revision_id"`
ExpectedCandidateVersion int64 `json:"expected_candidate_version"`
Quantity string `json:"quantity"`
FeeBudget string `json:"fee_budget"`
}
func (s *TradePlanService) Preview(ctx context.Context, actor model.User, input PlanPreviewInput) (model.PlanPreview, error) {
if err := s.authorize(actor); err != nil {
return model.PlanPreview{}, err
}
if _, err := uuid.Parse(input.AccountID); err != nil {
return model.PlanPreview{}, apperror.ErrInvalidInput
}
if _, err := uuid.Parse(input.CandidateRevisionID); err != nil {
return model.PlanPreview{}, apperror.ErrInvalidInput
}
qty, err := ParseQuantity(input.Quantity)
if err != nil || qty%100 != 0 || input.ExpectedCandidateVersion < 1 {
return model.PlanPreview{}, apperror.ErrInvalidInput
}
fee, err := model.ParseMoney(input.FeeBudget)
if err != nil {
return model.PlanPreview{}, err
}
return s.repo.Preview(ctx, actor.ID, input.AccountID, input.CandidateRevisionID, input.ExpectedCandidateVersion, qty, fee)
}
func (s *TradePlanService) Confirm(ctx context.Context, actor model.User, id, requestID string) (model.TradePlan, error) {
if err := s.authorize(actor); err != nil {
return model.TradePlan{}, err
}
if _, err := uuid.Parse(id); err != nil {
return model.TradePlan{}, apperror.ErrInvalidInput
}
return s.repo.Confirm(ctx, actor.ID, id, requestID)
}
func (s *TradePlanService) List(ctx context.Context, actor model.User) ([]model.TradePlan, error) {
if err := s.authorize(actor); err != nil {
return nil, err
}
return s.repo.List(ctx, actor.ID)
}
func (s *TradePlanService) Cancel(ctx context.Context, actor model.User, id string, expected int64, requestID string) (model.TradePlan, error) {
if err := s.authorize(actor); err != nil {
return model.TradePlan{}, err
}
if _, err := uuid.Parse(id); err != nil || expected < 1 {
return model.TradePlan{}, apperror.ErrInvalidInput
}
return s.repo.Cancel(ctx, actor.ID, id, expected, requestID)
}
func (s *TradePlanService) Budget(ctx context.Context, actor model.User, id string) (model.PlanBudget, error) {
if err := s.authorize(actor); err != nil {
return model.PlanBudget{}, err
}
if _, err := uuid.Parse(id); err != nil {
return model.PlanBudget{}, apperror.ErrInvalidInput
}
return s.repo.Budget(ctx, actor.ID, id)
}