init
This commit is contained in:
298
internal/handler/exchange_rate_handler_v2.go
Normal file
298
internal/handler/exchange_rate_handler_v2.go
Normal file
@@ -0,0 +1,298 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user