package handler import ( "context" "errors" "strings" "accounting-app/internal/cache" "accounting-app/internal/service" "accounting-app/pkg/api" "github.com/gin-gonic/gin" ) // ExchangeRateHandlerV2 handles HTTP requests for the redesigned exchange rate API // Uses ExchangeRateServiceV2 with Redis caching and SyncScheduler type ExchangeRateHandlerV2 struct { service *service.ExchangeRateServiceV2 scheduler *service.SyncScheduler } // NewExchangeRateHandlerV2 creates a new ExchangeRateHandlerV2 instance func NewExchangeRateHandlerV2(service *service.ExchangeRateServiceV2, scheduler *service.SyncScheduler) *ExchangeRateHandlerV2 { return &ExchangeRateHandlerV2{ service: service, scheduler: scheduler, } } // ConvertInput represents the input for currency conversion type ConvertInput struct { Amount float64 `json:"amount" binding:"required"` FromCurrency string `json:"from_currency" binding:"required"` ToCurrency string `json:"to_currency" binding:"required"` } // AllRatesResponse represents the response for GET /api/exchange-rates type AllRatesResponse struct { Rates []service.ExchangeRateDTO `json:"rates"` BaseCurrency string `json:"base_currency"` SyncStatus *cache.SyncStatus `json:"sync_status,omitempty"` } // GetAllRates handles GET /api/exchange-rates // Returns all exchange rates relative to CNY with sync status // Supports query parameter: currencies=USD,EUR,JPY for batch query // Requirements: 2.1 func (h *ExchangeRateHandlerV2) GetAllRates(c *gin.Context) { ctx := c.Request.Context() // Check if specific currencies are requested currenciesParam := c.Query("currencies") var rates []service.ExchangeRateDTO var err error if currenciesParam != "" { // Batch query for specific currencies currencies := strings.Split(currenciesParam, ",") // Trim spaces for i := range currencies { currencies[i] = strings.TrimSpace(currencies[i]) } rates, err = h.service.GetRatesBatch(ctx, currencies) } else { // Get all rates rates, err = h.service.GetAllRates(ctx) } if err != nil { if errors.Is(err, service.ErrAPIUnavailable) { api.BadGateway(c, "汇率服务暂时不可用") return } if errors.Is(err, service.ErrCurrencyNotSupported) { api.Error(c, 400, "CURRENCY_NOT_SUPPORTED", err.Error()) return } if errors.Is(err, service.ErrRateNotFound) { api.NotFound(c, err.Error()) return } api.InternalError(c, "获取汇率失败: "+err.Error()) return } // Get sync status syncStatus, _ := h.service.GetSyncStatus(ctx) response := AllRatesResponse{ Rates: rates, BaseCurrency: "CNY", SyncStatus: syncStatus, } api.Success(c, response) } // GetRate handles GET /api/exchange-rates/:currency // Returns a single currency's exchange rate relative to CNY // Requirements: 2.2 func (h *ExchangeRateHandlerV2) GetRate(c *gin.Context) { currency := c.Param("currency") if currency == "" { api.BadRequest(c, "货币代码不能为空") return } ctx := c.Request.Context() rate, err := h.service.GetRate(ctx, currency) if err != nil { if errors.Is(err, service.ErrCurrencyNotSupported) { api.Error(c, 400, "CURRENCY_NOT_SUPPORTED", "货币 "+currency+" 不支持") return } if errors.Is(err, service.ErrRateNotFound) { api.NotFound(c, "未找到货币 "+currency+" 的汇率") return } if errors.Is(err, service.ErrAPIUnavailable) { api.BadGateway(c, "汇率服务暂时不可用") return } api.InternalError(c, "获取汇率失败: "+err.Error()) return } api.Success(c, rate) } // Convert handles POST /api/exchange-rates/convert // Converts an amount from one currency to another using CNY as intermediate // Requirements: 2.3 func (h *ExchangeRateHandlerV2) Convert(c *gin.Context) { var input ConvertInput if err := c.ShouldBindJSON(&input); err != nil { api.ValidationError(c, "无效的请求参数: "+err.Error()) return } ctx := c.Request.Context() result, err := h.service.ConvertCurrency(ctx, input.Amount, input.FromCurrency, input.ToCurrency) if err != nil { if errors.Is(err, service.ErrCurrencyNotSupported) { api.Error(c, 400, "CURRENCY_NOT_SUPPORTED", "不支持的货币: "+err.Error()) return } if errors.Is(err, service.ErrRateNotFound) { api.NotFound(c, "未找到所需货币的汇率") return } if errors.Is(err, service.ErrInvalidConversionAmount) { api.BadRequest(c, "无效的转换金额") return } if errors.Is(err, service.ErrAPIUnavailable) { api.BadGateway(c, "汇率服务暂时不可用") return } api.InternalError(c, "货币转换失败: "+err.Error()) return } api.Success(c, result) } // Refresh handles POST /api/exchange-rates/refresh // Manually triggers a refresh of exchange rates from YunAPI // Requirements: 2.5 func (h *ExchangeRateHandlerV2) Refresh(c *gin.Context) { if h.scheduler == nil { api.InternalError(c, "汇率同步服务未配置") return } ctx := c.Request.Context() // Trigger force sync err := h.scheduler.ForceSync(ctx) if err != nil { api.InternalError(c, "刷新汇率失败: "+err.Error()) return } // Get updated sync status syncStatus, _ := h.service.GetSyncStatus(ctx) result := service.SyncResultDTO{ Message: "汇率同步成功", RatesUpdated: 0, } if syncStatus != nil { result.RatesUpdated = syncStatus.RatesCount result.SyncTime = syncStatus.LastSyncTime } api.Success(c, result) } // GetSyncStatus handles GET /api/exchange-rates/sync-status // Returns the current synchronization status func (h *ExchangeRateHandlerV2) GetSyncStatus(c *gin.Context) { ctx := c.Request.Context() status, err := h.service.GetSyncStatus(ctx) if err != nil { api.InternalError(c, "获取同步状态失败: "+err.Error()) return } api.Success(c, status) } // HealthCheck handles GET /api/exchange-rates/health // Returns the health status of the exchange rate service func (h *ExchangeRateHandlerV2) HealthCheck(c *gin.Context) { ctx := c.Request.Context() health := map[string]interface{}{ "status": "healthy", } // Check cache connectivity cacheExists, err := h.service.GetCache().Exists(ctx) if err != nil { health["cache_status"] = "error" health["cache_error"] = err.Error() health["status"] = "degraded" } else if cacheExists { health["cache_status"] = "connected" } else { health["cache_status"] = "empty" } // Get sync status syncStatus, err := h.service.GetSyncStatus(ctx) if err != nil { health["sync_status"] = "unknown" } else if syncStatus != nil { health["sync_status"] = syncStatus.LastSyncStatus health["last_sync"] = syncStatus.LastSyncTime health["rates_count"] = syncStatus.RatesCount if syncStatus.LastSyncStatus == "failed" { health["status"] = "degraded" health["sync_error"] = syncStatus.ErrorMessage } } // Check API availability (optional - don't fail health check on this) apiURL := h.service.GetClient().GetAPIURL() health["api_url"] = apiURL // Determine overall status if health["status"] == "healthy" { api.Success(c, health) } else { c.JSON(200, gin.H{ "success": true, "data": health, }) } } // RegisterRoutes registers all exchange rate v2 routes to the given router group // This registers the new simplified API endpoints func (h *ExchangeRateHandlerV2) RegisterRoutes(rg *gin.RouterGroup) { exchangeRates := rg.Group("/exchange-rates") { // GET /api/exchange-rates - Get all rates with sync status // Supports query param: ?currencies=USD,EUR,JPY for batch query exchangeRates.GET("", h.GetAllRates) // POST /api/exchange-rates/convert - Currency conversion exchangeRates.POST("/convert", h.Convert) // POST /api/exchange-rates/refresh - Manual refresh exchangeRates.POST("/refresh", h.Refresh) // GET /api/exchange-rates/sync-status - Get sync status exchangeRates.GET("/sync-status", h.GetSyncStatus) // GET /api/exchange-rates/health - Health check exchangeRates.GET("/health", h.HealthCheck) // GET /api/exchange-rates/:currency - Get single currency rate // Note: This must be registered last to avoid conflicts with other routes exchangeRates.GET("/:currency", h.GetRate) } } // RegisterRoutesWithContext is an alternative registration method that accepts a context // for dependency injection in tests func (h *ExchangeRateHandlerV2) RegisterRoutesWithContext(rg *gin.RouterGroup, ctx context.Context) { h.RegisterRoutes(rg) }