同步前后端联调与文档更新

This commit is contained in:
2026-04-02 09:25:05 +08:00
parent af43beadb0
commit 6964e26ec9
113 changed files with 4317 additions and 293 deletions

View File

@@ -16,6 +16,10 @@ type SessionService struct {
store *postgres.Store
}
type sessionTokenPolicy struct {
AllowExpired bool
}
type SessionResult struct {
Session struct {
ID string `json:"id"`
@@ -99,57 +103,11 @@ func (s *SessionService) ListMySessions(ctx context.Context, userID string, limi
}
func (s *SessionService) StartSession(ctx context.Context, input SessionActionInput) (*SessionResult, error) {
session, err := s.validateSessionAction(ctx, input.SessionPublicID, input.SessionToken)
session, err := s.validateSessionAction(ctx, input.SessionPublicID, input.SessionToken, sessionTokenPolicy{})
if err != nil {
return nil, err
}
if session.Status == "finished" || session.Status == "cancelled" || session.Status == "failed" {
return nil, apperr.New(http.StatusConflict, "session_not_startable", "session cannot be started")
}
tx, err := s.store.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
locked, err := s.store.GetSessionByPublicIDForUpdate(ctx, tx, input.SessionPublicID)
if err != nil {
return nil, err
}
if locked == nil {
return nil, apperr.New(http.StatusNotFound, "session_not_found", "session not found")
}
if err := s.verifySessionToken(locked, input.SessionToken); err != nil {
return nil, err
}
if locked.Status == "finished" || locked.Status == "cancelled" || locked.Status == "failed" {
return nil, apperr.New(http.StatusConflict, "session_not_startable", "session cannot be started")
}
if err := s.store.StartSession(ctx, tx, locked.ID); err != nil {
return nil, err
}
updated, err := s.store.GetSessionByPublicIDForUpdate(ctx, tx, input.SessionPublicID)
if err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return nil, err
}
return buildSessionResult(updated), nil
}
func (s *SessionService) FinishSession(ctx context.Context, input FinishSessionInput) (*SessionResult, error) {
input.Status = normalizeFinishStatus(input.Status)
session, err := s.validateSessionAction(ctx, input.SessionPublicID, input.SessionToken)
if err != nil {
return nil, err
}
if session.Status == "finished" || session.Status == "cancelled" || session.Status == "failed" {
if session.Status == SessionStatusRunning || isSessionTerminalStatus(session.Status) {
return buildSessionResult(session), nil
}
@@ -166,11 +124,73 @@ func (s *SessionService) FinishSession(ctx context.Context, input FinishSessionI
if locked == nil {
return nil, apperr.New(http.StatusNotFound, "session_not_found", "session not found")
}
if err := s.verifySessionToken(locked, input.SessionToken); err != nil {
if err := s.verifySessionToken(locked, input.SessionToken, sessionTokenPolicy{}); err != nil {
return nil, err
}
if locked.Status == SessionStatusRunning || isSessionTerminalStatus(locked.Status) {
if err := tx.Commit(ctx); err != nil {
return nil, err
}
return buildSessionResult(locked), nil
}
if locked.Status == SessionStatusLaunched {
if err := s.store.StartSession(ctx, tx, locked.ID); err != nil {
return nil, err
}
}
updated, err := s.store.GetSessionByPublicIDForUpdate(ctx, tx, input.SessionPublicID)
if err != nil {
return nil, err
}
if updated == nil {
return nil, apperr.New(http.StatusNotFound, "session_not_found", "session not found")
}
if err := tx.Commit(ctx); err != nil {
return nil, err
}
return buildSessionResult(updated), nil
}
func (s *SessionService) FinishSession(ctx context.Context, input FinishSessionInput) (*SessionResult, error) {
status, err := normalizeFinishStatus(input.Status)
if err != nil {
return nil, err
}
input.Status = status
session, err := s.validateSessionAction(ctx, input.SessionPublicID, input.SessionToken, sessionTokenPolicy{
AllowExpired: input.Status == SessionStatusCancelled,
})
if err != nil {
return nil, err
}
if locked.Status == "finished" || locked.Status == "cancelled" || locked.Status == "failed" {
if isSessionTerminalStatus(session.Status) {
return buildSessionResult(session), nil
}
tx, err := s.store.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
locked, err := s.store.GetSessionByPublicIDForUpdate(ctx, tx, input.SessionPublicID)
if err != nil {
return nil, err
}
if locked == nil {
return nil, apperr.New(http.StatusNotFound, "session_not_found", "session not found")
}
if err := s.verifySessionToken(locked, input.SessionToken, sessionTokenPolicy{
AllowExpired: input.Status == SessionStatusCancelled || isSessionTerminalStatus(locked.Status),
}); err != nil {
return nil, err
}
if isSessionTerminalStatus(locked.Status) {
if err := tx.Commit(ctx); err != nil {
return nil, err
}
@@ -208,7 +228,7 @@ func (s *SessionService) FinishSession(ctx context.Context, input FinishSessionI
return buildSessionResult(updated), nil
}
func (s *SessionService) validateSessionAction(ctx context.Context, sessionPublicID, sessionToken string) (*postgres.Session, error) {
func (s *SessionService) validateSessionAction(ctx context.Context, sessionPublicID, sessionToken string, policy sessionTokenPolicy) (*postgres.Session, error) {
sessionPublicID = strings.TrimSpace(sessionPublicID)
sessionToken = strings.TrimSpace(sessionToken)
if sessionPublicID == "" || sessionToken == "" {
@@ -222,19 +242,19 @@ func (s *SessionService) validateSessionAction(ctx context.Context, sessionPubli
if session == nil {
return nil, apperr.New(http.StatusNotFound, "session_not_found", "session not found")
}
if err := s.verifySessionToken(session, sessionToken); err != nil {
if err := s.verifySessionToken(session, sessionToken, policy); err != nil {
return nil, err
}
return session, nil
}
func (s *SessionService) verifySessionToken(session *postgres.Session, sessionToken string) error {
if session.SessionTokenExpiresAt.Before(time.Now().UTC()) {
return apperr.New(http.StatusUnauthorized, "session_token_expired", "session token expired")
}
func (s *SessionService) verifySessionToken(session *postgres.Session, sessionToken string, policy sessionTokenPolicy) error {
if session.SessionTokenHash != security.HashText(sessionToken) {
return apperr.New(http.StatusUnauthorized, "invalid_session_token", "invalid session token")
}
if !policy.AllowExpired && session.SessionTokenExpiresAt.Before(time.Now().UTC()) {
return apperr.New(http.StatusUnauthorized, "session_token_expired", "session token expired")
}
return nil
}
@@ -265,14 +285,16 @@ func buildSessionResult(session *postgres.Session) *SessionResult {
return result
}
func normalizeFinishStatus(value string) string {
func normalizeFinishStatus(value string) (string, error) {
switch strings.TrimSpace(value) {
case "failed":
return "failed"
case "cancelled":
return "cancelled"
case "", SessionStatusFinished:
return SessionStatusFinished, nil
case SessionStatusFailed:
return SessionStatusFailed, nil
case SessionStatusCancelled:
return SessionStatusCancelled, nil
default:
return "finished"
return "", apperr.New(http.StatusBadRequest, "invalid_finish_status", "status must be finished, failed or cancelled")
}
}