Files
assetx/webapp-back/fundmarket/routers.go
default 2ee4553b71 init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
2026-03-27 11:26:43 +00:00

435 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package fundmarket
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/gothinkster/golang-gin-realworld-example-app/common"
"github.com/gothinkster/golang-gin-realworld-example-app/models"
)
// GetProducts returns all fund market products
func GetProducts(c *gin.Context) {
db := common.GetDB()
// Query assets with their performance data
// token_list=1: return all active assets (for dropdowns)
// default: exclude stablecoins (Fund Market product listing)
var assets []models.Asset
query := db.Where("is_active = ?", true)
if c.Query("token_list") != "1" {
query = query.Where("token_role != ?", "stablecoin")
}
err := query.Find(&assets).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"error": "Failed to fetch products",
})
return
}
// Get performance data for each asset
products := make([]models.ProductResponse, 0)
for _, asset := range assets {
var perf models.AssetPerformance
db.Where("asset_id = ?", asset.ID).
Order("snapshot_date DESC").
First(&perf)
riskLabel := asset.RiskLabel
if riskLabel == "" {
riskLabel = "--"
}
circulatingSupplyStr := "--"
if perf.CirculatingSupply > 0 {
circulatingSupplyStr = formatUSD(perf.CirculatingSupply)
}
poolCapStr := "--"
if asset.PoolCapUSD > 0 {
poolCapStr = formatUSD(asset.PoolCapUSD)
}
product := models.ProductResponse{
ID: int(asset.ID),
Name: asset.Name,
TokenSymbol: asset.TokenSymbol,
Decimals: asset.Decimals,
ContractAddress: asset.ContractAddress,
ChainID: asset.ChainID,
TokenRole: asset.TokenRole,
Category: asset.Category,
CategoryColor: asset.CategoryColor,
IconURL: asset.IconURL,
YieldAPY: fmt.Sprintf("%.1f%%", asset.TargetAPY),
PoolCap: poolCapStr,
Risk: riskLabel,
RiskLevel: asset.RiskLevel,
CirculatingSupply: circulatingSupplyStr,
PoolCapacityPercent: perf.PoolCapacityPercent,
}
products = append(products, product)
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": products,
})
}
// GetStats returns fund market statistics
func GetStats(c *gin.Context) {
db := common.GetDB()
// Total Value Locked = sum of latest tvlusd across all active assets
var totalTVL float64
db.Raw(`
SELECT COALESCE(SUM(ap.tvlusd), 0)
FROM asset_performance ap
INNER JOIN (
SELECT asset_id, MAX(snapshot_date) AS latest
FROM asset_performance
GROUP BY asset_id
) latest ON ap.asset_id = latest.asset_id AND ap.snapshot_date = latest.latest
INNER JOIN assets a ON a.id = ap.asset_id AND a.is_active = true
`).Scan(&totalTVL)
// Cumulative Yield = sum of cumulative_yield_usd from latest snapshots
var totalYield float64
db.Raw(`
SELECT COALESCE(SUM(ap.cumulative_yield_usd), 0)
FROM asset_performance ap
INNER JOIN (
SELECT asset_id, MAX(snapshot_date) AS latest
FROM asset_performance
GROUP BY asset_id
) latest ON ap.asset_id = latest.asset_id AND ap.snapshot_date = latest.latest
`).Scan(&totalYield)
stats := []models.StatsResponse{
{
Label: "Total Value Locked",
Value: formatUSD(totalTVL),
Change: "",
IsPositive: true,
},
{
Label: "Cumulative Yield",
Value: formatUSD(totalYield),
Change: "",
IsPositive: true,
},
{
Label: "Your Total Balance",
Value: "--",
Change: "",
IsPositive: true,
},
{
Label: "Your Total Earning",
Value: "--",
Change: "",
IsPositive: true,
},
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": stats,
})
}
// GetProductByID returns detailed product information by ID
func GetProductByID(c *gin.Context) {
db := common.GetDB()
id := c.Param("id")
var asset models.Asset
err := db.Where("id = ? AND is_active = ?", id, true).First(&asset).Error
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"error": "Product not found",
})
return
}
// Get performance data
var perf models.AssetPerformance
db.Where("asset_id = ?", asset.ID).
Order("snapshot_date DESC").
First(&perf)
// Calculate volume change vs 7-day average (excluding today)
var avg7dVolume float64
db.Raw(`
SELECT AVG(volume_24h_usd) FROM (
SELECT volume_24h_usd FROM asset_performance
WHERE asset_id = ? AND snapshot_date < CURDATE()
ORDER BY snapshot_date DESC
LIMIT 7
) t
`, asset.ID).Scan(&avg7dVolume)
volumeChangeVsAvg := 0.0
if avg7dVolume > 0 {
volumeChangeVsAvg = (perf.Volume24hUSD - avg7dVolume) / avg7dVolume * 100
}
// Get custody info
var custody models.AssetCustody
hasCustody := db.Where("asset_id = ?", asset.ID).First(&custody).Error == nil
// Get audit reports
var auditReports []models.AssetAuditReport
db.Where("asset_id = ? AND is_active = ?", asset.ID, true).
Order("report_date DESC").
Limit(10).
Find(&auditReports)
// Get product links
var productLinks []models.ProductLink
db.Where("asset_id = ? AND is_active = ?", asset.ID, true).
Order("display_order ASC, id ASC").
Find(&productLinks)
// Build response
detail := models.ProductDetailResponse{
// Basic Info
ID: int(asset.ID),
AssetCode: asset.AssetCode,
Name: asset.Name,
Subtitle: asset.Subtitle,
Description: asset.Description,
TokenSymbol: asset.TokenSymbol,
Decimals: asset.Decimals,
// Investment Parameters
UnderlyingAssets: asset.UnderlyingAssets,
PoolCapUSD: asset.PoolCapUSD,
RiskLevel: asset.RiskLevel,
RiskLabel: asset.RiskLabel,
TargetAPY: asset.TargetAPY,
// Contract Info
ContractAddress: asset.ContractAddress,
ChainID: asset.ChainID,
// Display Info
Category: asset.Category,
CategoryColor: asset.CategoryColor,
IconURL: asset.IconURL,
// Performance Data
CurrentAPY: perf.CurrentAPY,
TVLUSD: perf.TVLUSD,
Volume24hUSD: perf.Volume24hUSD,
VolumeChangeVsAvg: volumeChangeVsAvg,
CirculatingSupply: perf.CirculatingSupply,
PoolCapacityPercent: perf.PoolCapacityPercent,
CurrentPrice: perf.YTPrice,
}
// Add custody info if exists
if hasCustody {
lastAuditDate := ""
if custody.LastAuditDate.Time != nil {
lastAuditDate = custody.LastAuditDate.Time.Format("02 Jan 2006")
}
var additionalInfo map[string]interface{}
if custody.AdditionalInfo != "" {
_ = json.Unmarshal([]byte(custody.AdditionalInfo), &additionalInfo)
}
if additionalInfo == nil {
additionalInfo = map[string]interface{}{}
}
// 从 maturity_date 动态计算 days_remaining不再依赖存储值
if maturityStr, ok := additionalInfo["maturity_date"].(string); ok && maturityStr != "" {
if maturityDate, err := time.Parse("2006-01-02", maturityStr); err == nil {
days := int(time.Until(maturityDate).Hours() / 24)
additionalInfo["days_remaining"] = days
}
} else {
delete(additionalInfo, "days_remaining")
}
detail.Custody = &models.CustodyInfo{
CustodianName: custody.CustodianName,
CustodyType: custody.CustodyType,
CustodyLocation: custody.CustodyLocation,
AuditorName: custody.AuditorName,
LastAuditDate: lastAuditDate,
AuditReportURL: custody.AuditReportURL,
AdditionalInfo: additionalInfo,
}
}
// Add audit reports
detail.AuditReports = make([]models.AuditReportInfo, 0, len(auditReports))
for _, report := range auditReports {
detail.AuditReports = append(detail.AuditReports, models.AuditReportInfo{
ReportType: report.ReportType,
ReportTitle: report.ReportTitle,
ReportDate: report.ReportDate.Format("02 Jan 2006"),
AuditorName: report.AuditorName,
Summary: report.Summary,
ReportURL: report.ReportURL,
})
}
// Add product links
detail.ProductLinks = make([]models.ProductLinkInfo, 0, len(productLinks))
for _, link := range productLinks {
detail.ProductLinks = append(detail.ProductLinks, models.ProductLinkInfo{
LinkText: link.LinkText,
LinkURL: link.LinkURL,
Description: link.Description,
DisplayArea: link.DisplayArea,
DisplayOrder: link.DisplayOrder,
})
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": detail,
})
}
// GetProductHistory returns daily APY/price history for a product (one point per day)
func GetProductHistory(c *gin.Context) {
db := common.GetDB()
id := c.Param("id")
// Pick the last snapshot of each calendar day (UTC), up to 30 days
var snapshots []models.APYSnapshot
if err := db.Raw(`
SELECT ap.* FROM apy_snapshots ap
INNER JOIN (
SELECT DATE(snapshot_time) AS snap_date, MAX(snapshot_time) AS latest
FROM apy_snapshots
WHERE asset_id = ?
GROUP BY DATE(snapshot_time)
ORDER BY snap_date DESC
LIMIT 30
) d ON DATE(ap.snapshot_time) = d.snap_date AND ap.snapshot_time = d.latest
WHERE ap.asset_id = ?
ORDER BY ap.snapshot_time ASC
`, id, id).Scan(&snapshots).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"error": "Failed to fetch history",
})
return
}
type HistoryPoint struct {
Time string `json:"time"`
APY float64 `json:"apy"`
Price float64 `json:"price"`
}
points := make([]HistoryPoint, len(snapshots))
for i, s := range snapshots {
points[i] = HistoryPoint{
Time: s.SnapshotTime.UTC().Format(time.RFC3339),
APY: s.APYValue,
Price: s.Price,
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": points,
})
}
// Helper function to format USD values
func formatUSD(value float64) string {
if value >= 1000000 {
return fmt.Sprintf("$%.1fM", value/1000000)
} else if value >= 1000 {
return fmt.Sprintf("$%.1fK", value/1000)
}
return fmt.Sprintf("$%.2f", value)
}
// GetDailyReturns returns daily net return data for a product by month.
// Query params: year (int), month (int). Defaults to current month.
func GetDailyReturns(c *gin.Context) {
db := common.GetDB()
id := c.Param("id")
now := time.Now().UTC()
year := now.Year()
month := int(now.Month())
if y, err := strconv.Atoi(c.Query("year")); err == nil {
year = y
}
if m, err := strconv.Atoi(c.Query("month")); err == nil && m >= 1 && m <= 12 {
month = m
}
startDate := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
endDate := startDate.AddDate(0, 1, 0)
// Include one day before month start for delta calculation
queryStart := startDate.AddDate(0, 0, -1)
var perfs []models.AssetPerformance
db.Where("asset_id = ? AND snapshot_date >= ? AND snapshot_date < ?", id, queryStart, endDate).
Order("snapshot_date ASC").
Find(&perfs)
// Map date string -> performance
type perfEntry struct {
ytPrice float64
hasData bool
}
perfMap := make(map[string]perfEntry)
for _, p := range perfs {
perfMap[p.SnapshotDate.Format("2006-01-02")] = perfEntry{ytPrice: p.YTPrice, hasData: true}
}
type DayReturn struct {
Date string `json:"date"`
YTPrice float64 `json:"ytPrice"`
DailyReturn float64 `json:"dailyReturn"`
HasData bool `json:"hasData"`
}
today := now.Format("2006-01-02")
var results []DayReturn
for d := startDate; d.Before(endDate); d = d.AddDate(0, 0, 1) {
dateStr := d.Format("2006-01-02")
// Skip future days
if dateStr > today {
break
}
cur := perfMap[dateStr]
prev := perfMap[d.AddDate(0, 0, -1).Format("2006-01-02")]
dailyReturn := 0.0
if cur.hasData && prev.hasData && prev.ytPrice > 0 {
dailyReturn = (cur.ytPrice - prev.ytPrice) / prev.ytPrice * 100
}
results = append(results, DayReturn{
Date: dateStr,
YTPrice: cur.ytPrice,
DailyReturn: dailyReturn,
HasData: cur.hasData,
})
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": results,
})
}