init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
This commit is contained in:
111
webapp-back/admin/asset_audit.go
Normal file
111
webapp-back/admin/asset_audit.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/common"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||||
)
|
||||
|
||||
func ListAssetAuditReports(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
p := ParsePagination(c)
|
||||
|
||||
query := db.Model(&models.AssetAuditReport{})
|
||||
if v := c.Query("asset_id"); v != "" {
|
||||
query = query.Where("asset_id = ?", v)
|
||||
}
|
||||
if v := c.Query("report_type"); v != "" {
|
||||
query = query.Where("report_type = ?", v)
|
||||
}
|
||||
if v := c.Query("auditor_name"); v != "" {
|
||||
query = query.Where("auditor_name LIKE ?", "%"+v+"%")
|
||||
}
|
||||
if v := c.Query("is_active"); v != "" {
|
||||
query = query.Where("is_active = ?", v == "true")
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var items []models.AssetAuditReport
|
||||
if err := query.Order("asset_id ASC, display_order ASC, id DESC").Offset(p.Offset()).Limit(p.PageSize).Find(&items).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, "Failed to fetch audit reports")
|
||||
return
|
||||
}
|
||||
OKList(c, items, total)
|
||||
}
|
||||
|
||||
func GetAssetAuditReport(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.AssetAuditReport
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Audit report not found")
|
||||
return
|
||||
}
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func CreateAssetAuditReport(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.AssetAuditReport
|
||||
if err := BindJSONFlexTime(c, &item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = 0
|
||||
now := time.Now()
|
||||
if item.CreatedAt.IsZero() {
|
||||
item.CreatedAt = now
|
||||
}
|
||||
item.UpdatedAt = now
|
||||
if err := db.Create(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "create", "asset_audit_report", "create_audit_report", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func UpdateAssetAuditReport(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var existing models.AssetAuditReport
|
||||
if err := db.First(&existing, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Audit report not found")
|
||||
return
|
||||
}
|
||||
var item models.AssetAuditReport
|
||||
if err := BindJSONFlexTime(c, &item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = existing.ID
|
||||
if existing.CreatedAt.IsZero() {
|
||||
item.CreatedAt = time.Now()
|
||||
} else {
|
||||
item.CreatedAt = existing.CreatedAt
|
||||
}
|
||||
if err := db.Save(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "update", "asset_audit_report", "update_audit_report", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func DeleteAssetAuditReport(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.AssetAuditReport
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Audit report not found")
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "delete", "asset_audit_report", "delete_audit_report", &item.ID, nil)
|
||||
OK(c, gin.H{"message": "deleted"})
|
||||
}
|
||||
105
webapp-back/admin/asset_custody.go
Normal file
105
webapp-back/admin/asset_custody.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/common"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||||
)
|
||||
|
||||
func ListAssetCustody(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
p := ParsePagination(c)
|
||||
|
||||
query := db.Model(&models.AssetCustody{})
|
||||
if v := c.Query("asset_id"); v != "" {
|
||||
query = query.Where("asset_id = ?", v)
|
||||
}
|
||||
if v := c.Query("custodian_name"); v != "" {
|
||||
query = query.Where("custodian_name LIKE ?", "%"+v+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var items []models.AssetCustody
|
||||
if err := query.Order("id DESC").Offset(p.Offset()).Limit(p.PageSize).Find(&items).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, "Failed to fetch asset custody records")
|
||||
return
|
||||
}
|
||||
OKList(c, items, total)
|
||||
}
|
||||
|
||||
func GetAssetCustody(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.AssetCustody
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Asset custody record not found")
|
||||
return
|
||||
}
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func CreateAssetCustody(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.AssetCustody
|
||||
if err := BindJSONFlexTime(c, &item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = 0
|
||||
now := time.Now()
|
||||
if item.CreatedAt.IsZero() {
|
||||
item.CreatedAt = now
|
||||
}
|
||||
item.UpdatedAt = now
|
||||
if err := db.Create(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "create", "asset_custody", "create_asset_custody", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func UpdateAssetCustody(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var existing models.AssetCustody
|
||||
if err := db.First(&existing, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Asset custody record not found")
|
||||
return
|
||||
}
|
||||
var item models.AssetCustody
|
||||
if err := BindJSONFlexTime(c, &item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = existing.ID
|
||||
if existing.CreatedAt.IsZero() {
|
||||
item.CreatedAt = time.Now()
|
||||
} else {
|
||||
item.CreatedAt = existing.CreatedAt
|
||||
}
|
||||
if err := db.Save(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "update", "asset_custody", "update_asset_custody", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func DeleteAssetCustody(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.AssetCustody
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Asset custody record not found")
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "delete", "asset_custody", "delete_asset_custody", &item.ID, nil)
|
||||
OK(c, gin.H{"message": "deleted"})
|
||||
}
|
||||
127
webapp-back/admin/assets.go
Normal file
127
webapp-back/admin/assets.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/common"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||||
)
|
||||
|
||||
func ListAssets(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
p := ParsePagination(c)
|
||||
|
||||
query := db.Model(&models.Asset{})
|
||||
if v := c.Query("name"); v != "" {
|
||||
query = query.Where("name LIKE ?", "%"+v+"%")
|
||||
}
|
||||
if v := c.Query("asset_code"); v != "" {
|
||||
query = query.Where("asset_code LIKE ?", "%"+v+"%")
|
||||
}
|
||||
if v := c.Query("category"); v != "" {
|
||||
query = query.Where("category = ?", v)
|
||||
}
|
||||
if v := c.Query("is_active"); v != "" {
|
||||
query = query.Where("is_active = ?", v == "true")
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var items []models.Asset
|
||||
if err := query.Order("id DESC").Offset(p.Offset()).Limit(p.PageSize).Find(&items).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, "Failed to fetch assets")
|
||||
return
|
||||
}
|
||||
OKList(c, items, total)
|
||||
}
|
||||
|
||||
func GetAsset(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.Asset
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Asset not found")
|
||||
return
|
||||
}
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func CreateAsset(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.Asset
|
||||
if err := BindJSONFlexTime(c, &item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = 0
|
||||
// 合约地址唯一性校验(非空才校验)
|
||||
if item.ContractAddress != "" {
|
||||
var conflict models.Asset
|
||||
if err := db.Where("contract_address = ?", item.ContractAddress).First(&conflict).Error; err == nil {
|
||||
Fail(c, http.StatusConflict, "合约地址已被资产「"+conflict.Name+"」使用,请检查后重试")
|
||||
return
|
||||
}
|
||||
}
|
||||
now := time.Now()
|
||||
if item.CreatedAt.IsZero() {
|
||||
item.CreatedAt = now
|
||||
}
|
||||
item.UpdatedAt = now
|
||||
if err := db.Create(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "create", "asset", "create_asset", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func UpdateAsset(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var existing models.Asset
|
||||
if err := db.First(&existing, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Asset not found")
|
||||
return
|
||||
}
|
||||
var item models.Asset
|
||||
if err := BindJSONFlexTime(c, &item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = existing.ID
|
||||
// 合约地址唯一性校验(非空且与原值不同时校验)
|
||||
if item.ContractAddress != "" && item.ContractAddress != existing.ContractAddress {
|
||||
var conflict models.Asset
|
||||
if err := db.Where("contract_address = ? AND id != ?", item.ContractAddress, existing.ID).First(&conflict).Error; err == nil {
|
||||
Fail(c, http.StatusConflict, "合约地址已被资产「"+conflict.Name+"」使用,请检查后重试")
|
||||
return
|
||||
}
|
||||
}
|
||||
if existing.CreatedAt.IsZero() {
|
||||
item.CreatedAt = time.Now()
|
||||
} else {
|
||||
item.CreatedAt = existing.CreatedAt
|
||||
}
|
||||
if err := db.Save(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "update", "asset", "update_asset", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func DeleteAsset(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.Asset
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Asset not found")
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "delete", "asset", "delete_asset", &item.ID, nil)
|
||||
OK(c, gin.H{"message": "deleted"})
|
||||
}
|
||||
131
webapp-back/admin/common.go
Normal file
131
webapp-back/admin/common.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"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/middleware"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||||
)
|
||||
|
||||
// PaginationParams holds parsed ProTable pagination query params
|
||||
type PaginationParams struct {
|
||||
Current int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// ParsePagination reads ?current=N&pageSize=M from the request
|
||||
func ParsePagination(c *gin.Context) PaginationParams {
|
||||
current, _ := strconv.Atoi(c.DefaultQuery("current", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
||||
if current < 1 {
|
||||
current = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
return PaginationParams{Current: current, PageSize: pageSize}
|
||||
}
|
||||
|
||||
// Offset returns the DB offset for GORM queries
|
||||
func (p PaginationParams) Offset() int {
|
||||
return (p.Current - 1) * p.PageSize
|
||||
}
|
||||
|
||||
// OK responds with {success: true, data: ...}
|
||||
func OK(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": data})
|
||||
}
|
||||
|
||||
// OKList responds with {success: true, data: [...], total: N} — used by ProTable
|
||||
func OKList(c *gin.Context, data interface{}, total int64) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": data, "total": total})
|
||||
}
|
||||
|
||||
// Fail responds with {success: false, message: ...}
|
||||
func Fail(c *gin.Context, status int, msg string) {
|
||||
c.JSON(status, gin.H{"success": false, "message": msg})
|
||||
}
|
||||
|
||||
// flexTimeLayouts is the set of time formats accepted from the frontend.
|
||||
var flexTimeLayouts = []string{
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02T15:04",
|
||||
"2006-01-02 15:04",
|
||||
"2006-01-02",
|
||||
}
|
||||
|
||||
// BindJSONFlexTime works like c.ShouldBindJSON but also accepts non-RFC3339
|
||||
// time strings such as "2026-02-28 00:00:00" that Ant Design's DatePicker sends.
|
||||
// It reads the raw body, converts any date-like strings to RFC3339, then unmarshals.
|
||||
func BindJSONFlexTime(c *gin.Context, obj interface{}) error {
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
return json.Unmarshal(body, obj) // fallback: let normal errors surface
|
||||
}
|
||||
|
||||
for key, val := range raw {
|
||||
var s string
|
||||
if err := json.Unmarshal(val, &s); err != nil {
|
||||
continue // not a string, skip
|
||||
}
|
||||
for _, layout := range flexTimeLayouts {
|
||||
if t, err := time.Parse(layout, s); err == nil {
|
||||
converted, _ := json.Marshal(t) // produces RFC3339
|
||||
raw[key] = converted
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fixed, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(fixed, obj)
|
||||
}
|
||||
|
||||
// LogOp writes a record to operation_logs table
|
||||
func LogOp(c *gin.Context, opType, targetType, action string, targetID *uint, payload interface{}) {
|
||||
db := common.GetDB()
|
||||
user, _ := middleware.GetCurrentUser(c)
|
||||
|
||||
var userID *uint
|
||||
if user != nil {
|
||||
uid := uint(user.UserID)
|
||||
userID = &uid
|
||||
}
|
||||
|
||||
var changesStr string
|
||||
if payload != nil {
|
||||
b, _ := json.Marshal(payload)
|
||||
changesStr = string(b)
|
||||
}
|
||||
|
||||
db.Create(&models.OperationLog{
|
||||
UserID: userID,
|
||||
OperationType: opType,
|
||||
TargetType: targetType,
|
||||
Action: action,
|
||||
TargetID: targetID,
|
||||
Changes: changesStr,
|
||||
IPAddress: c.ClientIP(),
|
||||
UserAgent: c.GetHeader("User-Agent"),
|
||||
Status: "success",
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
85
webapp-back/admin/invite_codes.go
Normal file
85
webapp-back/admin/invite_codes.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/common"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||||
)
|
||||
|
||||
func ListInviteCodes(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
p := ParsePagination(c)
|
||||
|
||||
query := db.Model(&models.InviteCode{})
|
||||
if v := c.Query("wallet_address"); v != "" {
|
||||
query = query.Where("wallet_address LIKE ?", "%"+v+"%")
|
||||
}
|
||||
if v := c.Query("code"); v != "" {
|
||||
query = query.Where("code LIKE ?", "%"+v+"%")
|
||||
}
|
||||
if v := c.Query("is_active"); v != "" {
|
||||
query = query.Where("is_active = ?", v == "true")
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var items []models.InviteCode
|
||||
if err := query.Order("id DESC").Offset(p.Offset()).Limit(p.PageSize).Find(&items).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, "Failed to fetch invite codes")
|
||||
return
|
||||
}
|
||||
OKList(c, items, total)
|
||||
}
|
||||
|
||||
func ToggleInviteCode(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.InviteCode
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Invite code not found")
|
||||
return
|
||||
}
|
||||
|
||||
item.IsActive = !item.IsActive
|
||||
if item.CreatedAt.IsZero() {
|
||||
item.CreatedAt = time.Now()
|
||||
}
|
||||
if err := db.Save(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
LogOp(c, "update", "invite_code", "toggle_invite_code", &item.ID, gin.H{"is_active": item.IsActive})
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func UpdateInviteCode(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var existing models.InviteCode
|
||||
if err := db.First(&existing, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Invite code not found")
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
ExpiresAt models.NullTime `json:"expires_at"`
|
||||
}
|
||||
if err := BindJSONFlexTime(c, &body); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
existing.ExpiresAt = body.ExpiresAt
|
||||
if existing.CreatedAt.IsZero() {
|
||||
existing.CreatedAt = time.Now()
|
||||
}
|
||||
if err := db.Save(&existing).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "update", "invite_code", "update_invite_code", &existing.ID, gin.H{"expires_at": body.ExpiresAt})
|
||||
OK(c, existing)
|
||||
}
|
||||
121
webapp-back/admin/liquidation.go
Normal file
121
webapp-back/admin/liquidation.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
appcfg "github.com/gothinkster/golang-gin-realworld-example-app/config"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/common"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/lending"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||||
)
|
||||
|
||||
// ── Collateral Buyer Bot ───────────────────────────────────────────────────
|
||||
|
||||
func GetBuyerBotStatus(c *gin.Context) {
|
||||
OK(c, lending.GetBuyerBotStatus())
|
||||
}
|
||||
|
||||
func StartBuyerBot(c *gin.Context) {
|
||||
cfg := appcfg.AppConfig
|
||||
if cfg == nil {
|
||||
Fail(c, http.StatusInternalServerError, "Configuration not loaded")
|
||||
return
|
||||
}
|
||||
if cfg.CollateralBuyerPrivateKey == "" {
|
||||
Fail(c, http.StatusBadRequest, "COLLATERAL_BUYER_PRIVATE_KEY not configured on server")
|
||||
return
|
||||
}
|
||||
lending.StartCollateralBuyerBot(cfg)
|
||||
OK(c, gin.H{"message": "Collateral buyer bot start signal sent"})
|
||||
}
|
||||
|
||||
func StopBuyerBot(c *gin.Context) {
|
||||
lending.StopCollateralBuyerBot()
|
||||
OK(c, gin.H{"message": "Collateral buyer bot stop signal sent"})
|
||||
}
|
||||
|
||||
func ListBuyRecords(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
p := ParsePagination(c)
|
||||
|
||||
query := db.Model(&models.CollateralBuyRecord{})
|
||||
if v := c.Query("chain_id"); v != "" {
|
||||
if id, err := strconv.Atoi(v); err == nil {
|
||||
query = query.Where("chain_id = ?", id)
|
||||
}
|
||||
}
|
||||
if v := c.Query("status"); v != "" {
|
||||
query = query.Where("status = ?", v)
|
||||
}
|
||||
if v := c.Query("asset_addr"); v != "" {
|
||||
query = query.Where("asset_addr = ?", v)
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var records []models.CollateralBuyRecord
|
||||
if err := query.Order("created_at DESC").Offset(p.Offset()).Limit(p.PageSize).Find(&records).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, "Failed to fetch buy records")
|
||||
return
|
||||
}
|
||||
OKList(c, records, total)
|
||||
}
|
||||
|
||||
// GetLiquidationStatus returns the current liquidation bot status
|
||||
func GetLiquidationStatus(c *gin.Context) {
|
||||
status := lending.GetBotStatus()
|
||||
OK(c, status)
|
||||
}
|
||||
|
||||
// StartLiquidationBot starts the liquidation bot
|
||||
func StartLiquidationBot(c *gin.Context) {
|
||||
cfg := appcfg.AppConfig
|
||||
if cfg == nil {
|
||||
Fail(c, http.StatusInternalServerError, "Configuration not loaded")
|
||||
return
|
||||
}
|
||||
if cfg.LiquidatorPrivateKey == "" {
|
||||
Fail(c, http.StatusBadRequest, "LIQUIDATOR_PRIVATE_KEY not configured on server")
|
||||
return
|
||||
}
|
||||
lending.StartLiquidationBot(cfg)
|
||||
OK(c, gin.H{"message": "Liquidation bot start signal sent"})
|
||||
}
|
||||
|
||||
// StopLiquidationBot stops the liquidation bot
|
||||
func StopLiquidationBot(c *gin.Context) {
|
||||
lending.StopLiquidationBot()
|
||||
OK(c, gin.H{"message": "Liquidation bot stop signal sent"})
|
||||
}
|
||||
|
||||
// ListLiquidationRecords returns paginated liquidation history
|
||||
func ListLiquidationRecords(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
p := ParsePagination(c)
|
||||
|
||||
query := db.Model(&models.LiquidationRecord{})
|
||||
if v := c.Query("chain_id"); v != "" {
|
||||
if id, err := strconv.Atoi(v); err == nil {
|
||||
query = query.Where("chain_id = ?", id)
|
||||
}
|
||||
}
|
||||
if v := c.Query("status"); v != "" {
|
||||
query = query.Where("status = ?", v)
|
||||
}
|
||||
if v := c.Query("liquidator_addr"); v != "" {
|
||||
query = query.Where("liquidator_addr = ?", v)
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var records []models.LiquidationRecord
|
||||
if err := query.Order("created_at DESC").Offset(p.Offset()).Limit(p.PageSize).Find(&records).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, "Failed to fetch liquidation records")
|
||||
return
|
||||
}
|
||||
OKList(c, records, total)
|
||||
}
|
||||
108
webapp-back/admin/points_rules.go
Normal file
108
webapp-back/admin/points_rules.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/common"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||||
)
|
||||
|
||||
func ListPointsRules(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
p := ParsePagination(c)
|
||||
|
||||
query := db.Model(&models.PointsRule{})
|
||||
if v := c.Query("rule_name"); v != "" {
|
||||
query = query.Where("rule_name LIKE ?", "%"+v+"%")
|
||||
}
|
||||
if v := c.Query("rule_type"); v != "" {
|
||||
query = query.Where("rule_type = ?", v)
|
||||
}
|
||||
if v := c.Query("is_active"); v != "" {
|
||||
query = query.Where("is_active = ?", v == "true")
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var items []models.PointsRule
|
||||
if err := query.Order("priority DESC, id ASC").Offset(p.Offset()).Limit(p.PageSize).Find(&items).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, "Failed to fetch points rules")
|
||||
return
|
||||
}
|
||||
OKList(c, items, total)
|
||||
}
|
||||
|
||||
func GetPointsRule(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.PointsRule
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Points rule not found")
|
||||
return
|
||||
}
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func CreatePointsRule(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.PointsRule
|
||||
if err := BindJSONFlexTime(c, &item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = 0
|
||||
now := time.Now()
|
||||
if item.CreatedAt.IsZero() {
|
||||
item.CreatedAt = now
|
||||
}
|
||||
item.UpdatedAt = now
|
||||
if err := db.Create(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "create", "points_rule", "create_points_rule", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func UpdatePointsRule(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var existing models.PointsRule
|
||||
if err := db.First(&existing, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Points rule not found")
|
||||
return
|
||||
}
|
||||
var item models.PointsRule
|
||||
if err := BindJSONFlexTime(c, &item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = existing.ID
|
||||
if existing.CreatedAt.IsZero() {
|
||||
item.CreatedAt = time.Now()
|
||||
} else {
|
||||
item.CreatedAt = existing.CreatedAt
|
||||
}
|
||||
if err := db.Save(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "update", "points_rule", "update_points_rule", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func DeletePointsRule(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.PointsRule
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Points rule not found")
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "delete", "points_rule", "delete_points_rule", &item.ID, nil)
|
||||
OK(c, gin.H{"message": "deleted"})
|
||||
}
|
||||
108
webapp-back/admin/product_links.go
Normal file
108
webapp-back/admin/product_links.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/common"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||||
)
|
||||
|
||||
func ListProductLinks(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
p := ParsePagination(c)
|
||||
|
||||
query := db.Model(&models.ProductLink{})
|
||||
if v := c.Query("asset_id"); v != "" {
|
||||
query = query.Where("asset_id = ?", v)
|
||||
}
|
||||
if v := c.Query("link_text"); v != "" {
|
||||
query = query.Where("link_text LIKE ?", "%"+v+"%")
|
||||
}
|
||||
if v := c.Query("is_active"); v != "" {
|
||||
query = query.Where("is_active = ?", v == "true")
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var items []models.ProductLink
|
||||
if err := query.Order("asset_id ASC, display_order ASC, id ASC").Offset(p.Offset()).Limit(p.PageSize).Find(&items).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, "Failed to fetch product links")
|
||||
return
|
||||
}
|
||||
OKList(c, items, total)
|
||||
}
|
||||
|
||||
func GetProductLink(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.ProductLink
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Product link not found")
|
||||
return
|
||||
}
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func CreateProductLink(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.ProductLink
|
||||
if err := BindJSONFlexTime(c, &item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = 0
|
||||
now := time.Now()
|
||||
if item.CreatedAt.IsZero() {
|
||||
item.CreatedAt = now
|
||||
}
|
||||
item.UpdatedAt = now
|
||||
if err := db.Create(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "create", "product_link", "create_product_link", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func UpdateProductLink(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var existing models.ProductLink
|
||||
if err := db.First(&existing, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Product link not found")
|
||||
return
|
||||
}
|
||||
var item models.ProductLink
|
||||
if err := BindJSONFlexTime(c, &item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = existing.ID
|
||||
if existing.CreatedAt.IsZero() {
|
||||
item.CreatedAt = time.Now()
|
||||
} else {
|
||||
item.CreatedAt = existing.CreatedAt
|
||||
}
|
||||
if err := db.Save(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "update", "product_link", "update_product_link", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func DeleteProductLink(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.ProductLink
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Product link not found")
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "delete", "product_link", "delete_product_link", &item.ID, nil)
|
||||
OK(c, gin.H{"message": "deleted"})
|
||||
}
|
||||
96
webapp-back/admin/routes.go
Normal file
96
webapp-back/admin/routes.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/middleware"
|
||||
)
|
||||
|
||||
// RegisterRoutes mounts all /api/admin/* routes onto the provided parent group.
|
||||
// Call this BEFORE v1.Use(AuthMiddleware) in main.go so auth is applied here only.
|
||||
func RegisterRoutes(v1 *gin.RouterGroup) {
|
||||
g := v1.Group("/admin")
|
||||
g.Use(middleware.AuthMiddleware(true), middleware.RequireAdmin())
|
||||
{
|
||||
// P0 — Assets
|
||||
g.GET("/assets", ListAssets)
|
||||
g.GET("/assets/:id", GetAsset)
|
||||
g.POST("/assets", CreateAsset)
|
||||
g.PUT("/assets/:id", UpdateAsset)
|
||||
g.DELETE("/assets/:id", DeleteAsset)
|
||||
|
||||
// P0 — Asset Custody
|
||||
g.GET("/asset-custody", ListAssetCustody)
|
||||
g.GET("/asset-custody/:id", GetAssetCustody)
|
||||
g.POST("/asset-custody", CreateAssetCustody)
|
||||
g.PUT("/asset-custody/:id", UpdateAssetCustody)
|
||||
g.DELETE("/asset-custody/:id", DeleteAssetCustody)
|
||||
|
||||
// P0 — Asset Audit Reports
|
||||
g.GET("/asset-audit-reports", ListAssetAuditReports)
|
||||
g.GET("/asset-audit-reports/:id", GetAssetAuditReport)
|
||||
g.POST("/asset-audit-reports", CreateAssetAuditReport)
|
||||
g.PUT("/asset-audit-reports/:id", UpdateAssetAuditReport)
|
||||
g.DELETE("/asset-audit-reports/:id", DeleteAssetAuditReport)
|
||||
|
||||
// P1 — Points Rules
|
||||
g.GET("/points-rules", ListPointsRules)
|
||||
g.GET("/points-rules/:id", GetPointsRule)
|
||||
g.POST("/points-rules", CreatePointsRule)
|
||||
g.PUT("/points-rules/:id", UpdatePointsRule)
|
||||
g.DELETE("/points-rules/:id", DeletePointsRule)
|
||||
|
||||
// P1 — Seasons
|
||||
g.GET("/seasons", ListSeasons)
|
||||
g.GET("/seasons/:id", GetSeason)
|
||||
g.POST("/seasons", CreateSeason)
|
||||
g.PUT("/seasons/:id", UpdateSeason)
|
||||
g.DELETE("/seasons/:id", DeleteSeason)
|
||||
|
||||
// P1 — VIP Tiers
|
||||
g.GET("/vip-tiers", ListVIPTiers)
|
||||
g.GET("/vip-tiers/:id", GetVIPTier)
|
||||
g.POST("/vip-tiers", CreateVIPTier)
|
||||
g.PUT("/vip-tiers/:id", UpdateVIPTier)
|
||||
g.DELETE("/vip-tiers/:id", DeleteVIPTier)
|
||||
|
||||
// P2 — Users (list + edit only)
|
||||
g.GET("/users", ListUsers)
|
||||
g.GET("/users/:wallet", GetUser)
|
||||
g.PUT("/users/:wallet", UpdateUser)
|
||||
|
||||
// P2 — Invite Codes
|
||||
g.GET("/invite-codes", ListInviteCodes)
|
||||
g.PATCH("/invite-codes/:id", UpdateInviteCode)
|
||||
g.PUT("/invite-codes/:id/toggle", ToggleInviteCode)
|
||||
|
||||
// P3 — Product Links (per-asset links on product detail page)
|
||||
g.GET("/product-links", ListProductLinks)
|
||||
g.GET("/product-links/:id", GetProductLink)
|
||||
g.POST("/product-links", CreateProductLink)
|
||||
g.PUT("/product-links/:id", UpdateProductLink)
|
||||
g.DELETE("/product-links/:id", DeleteProductLink)
|
||||
|
||||
// System Contracts (central infrastructure contract registry)
|
||||
g.GET("/system-contracts", ListSystemContracts)
|
||||
g.GET("/system-contracts/:id", GetSystemContract)
|
||||
g.POST("/system-contracts", CreateSystemContract)
|
||||
g.PUT("/system-contracts/:id", UpdateSystemContract)
|
||||
g.DELETE("/system-contracts/:id", DeleteSystemContract)
|
||||
|
||||
// File Upload
|
||||
g.POST("/upload", UploadFile)
|
||||
g.DELETE("/upload", DeleteUploadedFile)
|
||||
|
||||
// Liquidation Bot
|
||||
g.GET("/liquidation/status", GetLiquidationStatus)
|
||||
g.POST("/liquidation/start", StartLiquidationBot)
|
||||
g.POST("/liquidation/stop", StopLiquidationBot)
|
||||
g.GET("/liquidation/records", ListLiquidationRecords)
|
||||
|
||||
// Collateral Buyer Bot
|
||||
g.GET("/buyer/status", GetBuyerBotStatus)
|
||||
g.POST("/buyer/start", StartBuyerBot)
|
||||
g.POST("/buyer/stop", StopBuyerBot)
|
||||
g.GET("/buyer/records", ListBuyRecords)
|
||||
}
|
||||
}
|
||||
108
webapp-back/admin/seasons.go
Normal file
108
webapp-back/admin/seasons.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/common"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||||
)
|
||||
|
||||
func ListSeasons(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
p := ParsePagination(c)
|
||||
|
||||
query := db.Model(&models.Season{})
|
||||
if v := c.Query("status"); v != "" {
|
||||
query = query.Where("status = ?", v)
|
||||
}
|
||||
if v := c.Query("is_active"); v != "" {
|
||||
query = query.Where("is_active = ?", v == "true")
|
||||
}
|
||||
if v := c.Query("season_name"); v != "" {
|
||||
query = query.Where("season_name LIKE ?", "%"+v+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var items []models.Season
|
||||
if err := query.Order("season_number DESC").Offset(p.Offset()).Limit(p.PageSize).Find(&items).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, "Failed to fetch seasons")
|
||||
return
|
||||
}
|
||||
OKList(c, items, total)
|
||||
}
|
||||
|
||||
func GetSeason(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.Season
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Season not found")
|
||||
return
|
||||
}
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func CreateSeason(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.Season
|
||||
if err := BindJSONFlexTime(c, &item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = 0
|
||||
now := time.Now()
|
||||
if item.CreatedAt.IsZero() {
|
||||
item.CreatedAt = now
|
||||
}
|
||||
item.UpdatedAt = now
|
||||
if err := db.Create(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "create", "season", "create_season", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func UpdateSeason(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var existing models.Season
|
||||
if err := db.First(&existing, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Season not found")
|
||||
return
|
||||
}
|
||||
var item models.Season
|
||||
if err := BindJSONFlexTime(c, &item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = existing.ID
|
||||
if existing.CreatedAt.IsZero() {
|
||||
item.CreatedAt = time.Now()
|
||||
} else {
|
||||
item.CreatedAt = existing.CreatedAt
|
||||
}
|
||||
if err := db.Save(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "update", "season", "update_season", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func DeleteSeason(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.Season
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "Season not found")
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "delete", "season", "delete_season", &item.ID, nil)
|
||||
OK(c, gin.H{"message": "deleted"})
|
||||
}
|
||||
138
webapp-back/admin/system_contracts.go
Normal file
138
webapp-back/admin/system_contracts.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/common"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||||
)
|
||||
|
||||
// ContractEntry is the contract address entry returned to the frontend.
|
||||
type ContractEntry struct {
|
||||
Name string `json:"name"`
|
||||
ChainID int `json:"chain_id"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
// GetContracts is a public endpoint that returns all active contract addresses.
|
||||
// Merges infrastructure contracts (system_contracts) and token contracts (assets).
|
||||
func GetContracts(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
|
||||
// Infrastructure contracts from system_contracts
|
||||
var sysContracts []models.SystemContract
|
||||
db.Where("is_active = ?", true).Find(&sysContracts)
|
||||
|
||||
// Token contracts from assets
|
||||
var assets []models.Asset
|
||||
db.Where("is_active = ? AND contract_address != ''", true).Find(&assets)
|
||||
|
||||
entries := make([]ContractEntry, 0, len(sysContracts)+len(assets))
|
||||
for _, sc := range sysContracts {
|
||||
if sc.Address != "" {
|
||||
entries = append(entries, ContractEntry{Name: sc.Name, ChainID: sc.ChainID, Address: sc.Address})
|
||||
}
|
||||
}
|
||||
for _, a := range assets {
|
||||
entries = append(entries, ContractEntry{Name: a.AssetCode, ChainID: a.ChainID, Address: a.ContractAddress})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"contracts": entries})
|
||||
}
|
||||
|
||||
// --- Admin CRUD for system_contracts ---
|
||||
|
||||
func ListSystemContracts(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
p := ParsePagination(c)
|
||||
|
||||
query := db.Model(&models.SystemContract{})
|
||||
if v := c.Query("name"); v != "" {
|
||||
query = query.Where("name LIKE ?", "%"+v+"%")
|
||||
}
|
||||
if v := c.Query("chain_id"); v != "" {
|
||||
query = query.Where("chain_id = ?", v)
|
||||
}
|
||||
if v := c.Query("is_active"); v != "" {
|
||||
query = query.Where("is_active = ?", v == "true")
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var items []models.SystemContract
|
||||
if err := query.Order("chain_id ASC, name ASC").Offset(p.Offset()).Limit(p.PageSize).Find(&items).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, "Failed to fetch system contracts")
|
||||
return
|
||||
}
|
||||
OKList(c, items, total)
|
||||
}
|
||||
|
||||
func GetSystemContract(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.SystemContract
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "System contract not found")
|
||||
return
|
||||
}
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func CreateSystemContract(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.SystemContract
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = 0
|
||||
now := time.Now()
|
||||
item.CreatedAt = now
|
||||
item.UpdatedAt = now
|
||||
if err := db.Create(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "create", "system_contract", "create_system_contract", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func UpdateSystemContract(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var existing models.SystemContract
|
||||
if err := db.First(&existing, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "System contract not found")
|
||||
return
|
||||
}
|
||||
var item models.SystemContract
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = existing.ID
|
||||
item.CreatedAt = existing.CreatedAt
|
||||
item.UpdatedAt = time.Now()
|
||||
if err := db.Save(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "update", "system_contract", "update_system_contract", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func DeleteSystemContract(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.SystemContract
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "System contract not found")
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "delete", "system_contract", "delete_system_contract", &item.ID, nil)
|
||||
OK(c, gin.H{"message": "deleted"})
|
||||
}
|
||||
99
webapp-back/admin/upload.go
Normal file
99
webapp-back/admin/upload.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var allowedCategories = map[string]bool{
|
||||
"reports": true,
|
||||
"custody": true,
|
||||
"links": true,
|
||||
}
|
||||
|
||||
var allowedExts = map[string]bool{
|
||||
".pdf": true,
|
||||
".png": true,
|
||||
".jpg": true,
|
||||
".jpeg": true,
|
||||
".doc": true,
|
||||
".docx": true,
|
||||
".txt": true,
|
||||
".xls": true,
|
||||
".xlsx": true,
|
||||
}
|
||||
|
||||
// UploadFile handles multipart file uploads, saves to ./uploads/{category}/, returns accessible URL.
|
||||
// Query param: category (reports|custody), defaults to "reports".
|
||||
func UploadFile(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
Fail(c, http.StatusBadRequest, "No file provided")
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(file.Filename))
|
||||
if !allowedExts[ext] {
|
||||
Fail(c, http.StatusBadRequest, "File type not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
category := c.DefaultQuery("category", "reports")
|
||||
if !allowedCategories[category] {
|
||||
Fail(c, http.StatusBadRequest, "Invalid category")
|
||||
return
|
||||
}
|
||||
|
||||
dir := "uploads/" + category
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
Fail(c, http.StatusInternalServerError, "Failed to create upload directory")
|
||||
return
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), filepath.Base(file.Filename))
|
||||
dst := filepath.Join(dir, filename)
|
||||
|
||||
if err := c.SaveUploadedFile(file, dst); err != nil {
|
||||
Fail(c, http.StatusInternalServerError, "Failed to save file")
|
||||
return
|
||||
}
|
||||
|
||||
url := "/uploads/" + category + "/" + filename
|
||||
OK(c, gin.H{"url": url})
|
||||
}
|
||||
|
||||
// DeleteUploadedFile deletes a previously uploaded file by its URL path.
|
||||
func DeleteUploadedFile(c *gin.Context) {
|
||||
urlPath := c.Query("path") // e.g. /uploads/reports/xxx.pdf or /uploads/custody/xxx.pdf
|
||||
if !strings.HasPrefix(urlPath, "/uploads/") {
|
||||
Fail(c, http.StatusBadRequest, "Invalid file path")
|
||||
return
|
||||
}
|
||||
if strings.Contains(urlPath, "..") {
|
||||
Fail(c, http.StatusBadRequest, "Invalid file path")
|
||||
return
|
||||
}
|
||||
// Verify the second segment is a known category
|
||||
parts := strings.SplitN(strings.TrimPrefix(urlPath, "/uploads/"), "/", 2)
|
||||
if len(parts) != 2 || !allowedCategories[parts[0]] {
|
||||
Fail(c, http.StatusBadRequest, "Invalid file path")
|
||||
return
|
||||
}
|
||||
|
||||
localPath := strings.TrimPrefix(urlPath, "/")
|
||||
if err := os.Remove(localPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
OK(c, gin.H{"message": "file not found, skipped"})
|
||||
return
|
||||
}
|
||||
Fail(c, http.StatusInternalServerError, "Failed to delete file")
|
||||
return
|
||||
}
|
||||
OK(c, gin.H{"message": "deleted"})
|
||||
}
|
||||
98
webapp-back/admin/users.go
Normal file
98
webapp-back/admin/users.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/common"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||||
)
|
||||
|
||||
func ListUsers(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
p := ParsePagination(c)
|
||||
|
||||
query := db.Model(&models.User{})
|
||||
if v := c.Query("wallet_address"); v != "" {
|
||||
query = query.Where("wallet_address LIKE ?", "%"+strings.ToLower(v)+"%")
|
||||
}
|
||||
if v := c.Query("member_tier"); v != "" {
|
||||
query = query.Where("member_tier = ?", v)
|
||||
}
|
||||
if v := c.Query("nickname"); v != "" {
|
||||
query = query.Where("nickname LIKE ?", "%"+v+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var items []models.User
|
||||
if err := query.Order("created_at DESC").Offset(p.Offset()).Limit(p.PageSize).Find(&items).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, "Failed to fetch users")
|
||||
return
|
||||
}
|
||||
OKList(c, items, total)
|
||||
}
|
||||
|
||||
func GetUser(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
wallet := strings.ToLower(c.Param("wallet"))
|
||||
var item models.User
|
||||
if err := db.Where("wallet_address = ?", wallet).First(&item).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "User not found")
|
||||
return
|
||||
}
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
// UpdateUser allows updating limited fields: nickname, member_tier, vip_level, total_points, global_rank
|
||||
func UpdateUser(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
wallet := strings.ToLower(c.Param("wallet"))
|
||||
|
||||
var existing models.User
|
||||
if err := db.Where("wallet_address = ?", wallet).First(&existing).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "User not found")
|
||||
return
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Nickname string `json:"nickname"`
|
||||
MemberTier string `json:"member_tier"`
|
||||
VIPLevel int `json:"vip_level"`
|
||||
TotalPoints int64 `json:"total_points"`
|
||||
GlobalRank *int `json:"global_rank"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
if payload.Nickname != "" {
|
||||
updates["nickname"] = payload.Nickname
|
||||
}
|
||||
if payload.MemberTier != "" {
|
||||
updates["member_tier"] = payload.MemberTier
|
||||
}
|
||||
if payload.VIPLevel > 0 {
|
||||
updates["vip_level"] = payload.VIPLevel
|
||||
}
|
||||
if payload.TotalPoints >= 0 {
|
||||
updates["total_points"] = payload.TotalPoints
|
||||
}
|
||||
if payload.GlobalRank != nil {
|
||||
updates["global_rank"] = payload.GlobalRank
|
||||
}
|
||||
|
||||
if err := db.Model(&existing).Updates(updates).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Reload updated user
|
||||
db.Where("wallet_address = ?", wallet).First(&existing)
|
||||
LogOp(c, "update", "user", "update_user", nil, updates)
|
||||
OK(c, existing)
|
||||
}
|
||||
105
webapp-back/admin/vip_tiers.go
Normal file
105
webapp-back/admin/vip_tiers.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/common"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||||
)
|
||||
|
||||
func ListVIPTiers(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
p := ParsePagination(c)
|
||||
|
||||
query := db.Model(&models.VIPTier{})
|
||||
if v := c.Query("is_active"); v != "" {
|
||||
query = query.Where("is_active = ?", v == "true")
|
||||
}
|
||||
if v := c.Query("tier_name"); v != "" {
|
||||
query = query.Where("tier_name LIKE ?", "%"+v+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var items []models.VIPTier
|
||||
if err := query.Order("tier_level ASC").Offset(p.Offset()).Limit(p.PageSize).Find(&items).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, "Failed to fetch VIP tiers")
|
||||
return
|
||||
}
|
||||
OKList(c, items, total)
|
||||
}
|
||||
|
||||
func GetVIPTier(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.VIPTier
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "VIP tier not found")
|
||||
return
|
||||
}
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func CreateVIPTier(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.VIPTier
|
||||
if err := BindJSONFlexTime(c, &item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = 0
|
||||
now := time.Now()
|
||||
if item.CreatedAt.IsZero() {
|
||||
item.CreatedAt = now
|
||||
}
|
||||
item.UpdatedAt = now
|
||||
if err := db.Create(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "create", "vip_tier", "create_vip_tier", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func UpdateVIPTier(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var existing models.VIPTier
|
||||
if err := db.First(&existing, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "VIP tier not found")
|
||||
return
|
||||
}
|
||||
var item models.VIPTier
|
||||
if err := BindJSONFlexTime(c, &item); err != nil {
|
||||
Fail(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = existing.ID
|
||||
if existing.CreatedAt.IsZero() {
|
||||
item.CreatedAt = time.Now()
|
||||
} else {
|
||||
item.CreatedAt = existing.CreatedAt
|
||||
}
|
||||
if err := db.Save(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "update", "vip_tier", "update_vip_tier", &item.ID, item)
|
||||
OK(c, item)
|
||||
}
|
||||
|
||||
func DeleteVIPTier(c *gin.Context) {
|
||||
db := common.GetDB()
|
||||
var item models.VIPTier
|
||||
if err := db.First(&item, c.Param("id")).Error; err != nil {
|
||||
Fail(c, http.StatusNotFound, "VIP tier not found")
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&item).Error; err != nil {
|
||||
Fail(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
LogOp(c, "delete", "vip_tier", "delete_vip_tier", &item.ID, nil)
|
||||
OK(c, gin.H{"message": "deleted"})
|
||||
}
|
||||
Reference in New Issue
Block a user