299 lines
8.3 KiB
Go
299 lines
8.3 KiB
Go
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)
|
|
}
|