Files
SingBox-Gopanel/internal/handler/user_extra_api.go
CN-JS-HuiBai 981ee4f406
All checks were successful
build / build (api, amd64, linux) (push) Successful in -47s
build / build (api, arm64, linux) (push) Successful in -47s
build / build (api.exe, amd64, windows) (push) Successful in -48s
基本功能复刻完成
2026-04-17 12:24:00 +08:00

1040 lines
28 KiB
Go

package handler
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"xboard-go/internal/database"
"xboard-go/internal/model"
"xboard-go/internal/service"
"xboard-go/pkg/utils"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func UserCommConfig(c *gin.Context) {
data := buildGuestConfig()
data["is_telegram"] = boolToInt(service.MustGetBool("telegram_bot_enable", false))
data["telegram_discuss_link"] = service.MustGetString("telegram_discuss_link", "")
data["stripe_pk"] = service.MustGetString("stripe_pk_live", "")
data["withdraw_methods"] = service.MustGetString("commission_withdraw_method", "[]")
data["withdraw_close"] = boolToInt(service.MustGetBool("withdraw_close_enable", false))
data["currency"] = service.MustGetString("currency", "CNY")
data["currency_symbol"] = service.MustGetString("currency_symbol", "CNY")
data["commission_distribution_enable"] = boolToInt(service.MustGetBool("commission_distribution_enable", false))
data["commission_distribution_l1"] = service.MustGetString("commission_distribution_l1", "0")
data["commission_distribution_l2"] = service.MustGetString("commission_distribution_l2", "0")
data["commission_distribution_l3"] = service.MustGetString("commission_distribution_l3", "0")
Success(c, data)
}
func UserTransfer(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
Fail(c, http.StatusUnauthorized, "user not found")
return
}
var req struct {
TransferAmount int64 `json:"transfer_amount"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.TransferAmount <= 0 {
Fail(c, http.StatusBadRequest, "invalid transfer amount")
return
}
err := database.DB.Transaction(func(tx *gorm.DB) error {
var locked model.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", user.ID).First(&locked).Error; err != nil {
return err
}
if req.TransferAmount > int64(locked.CommissionBalance) {
return errors.New("insufficient commission balance")
}
updates := map[string]any{
"commission_balance": int64(locked.CommissionBalance) - req.TransferAmount,
"balance": int64(locked.Balance) + req.TransferAmount,
"updated_at": time.Now().Unix(),
}
return tx.Model(&model.User{}).Where("id = ?", locked.ID).Updates(updates).Error
})
if err != nil {
Fail(c, http.StatusBadRequest, err.Error())
return
}
Success(c, true)
}
func UserGetQuickLoginURL(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
Fail(c, http.StatusUnauthorized, "user not found")
return
}
Success(c, quickLoginURL(c, user.ID, c.PostForm("redirect"), c.Query("redirect")))
}
func PassportGetQuickLoginURL(c *gin.Context) {
token := strings.TrimSpace(c.GetHeader("Authorization"))
if token == "" {
token = strings.TrimSpace(c.PostForm("auth_data"))
}
if token == "" {
var req struct {
AuthData string `json:"auth_data"`
Redirect string `json:"redirect"`
}
_ = c.ShouldBindJSON(&req)
token = strings.TrimSpace(req.AuthData)
if token != "" {
c.Set("quick_login_redirect", req.Redirect)
}
}
token = strings.TrimSpace(strings.TrimPrefix(token, "Bearer "))
if token == "" {
Fail(c, http.StatusUnauthorized, "authorization is required")
return
}
claims, err := utils.VerifyToken(token)
if err != nil {
Fail(c, http.StatusUnauthorized, "token expired or invalid")
return
}
redirect := strings.TrimSpace(c.Query("redirect"))
if redirect == "" {
redirect = strings.TrimSpace(c.PostForm("redirect"))
}
if redirect == "" {
if value, exists := c.Get("quick_login_redirect"); exists {
redirect, _ = value.(string)
}
}
Success(c, quickLoginURL(c, claims.UserID, redirect))
}
func PassportLoginWithMailLink(c *gin.Context) {
var req struct {
Email string `json:"email" binding:"required,email"`
Redirect string `json:"redirect"`
}
if err := c.ShouldBindJSON(&req); err != nil {
Fail(c, http.StatusBadRequest, "invalid request body")
return
}
var user model.User
if err := database.DB.Where("email = ?", strings.ToLower(strings.TrimSpace(req.Email))).First(&user).Error; err != nil {
Fail(c, http.StatusBadRequest, "user not found")
return
}
Success(c, gin.H{
"url": quickLoginURL(c, user.ID, req.Redirect),
"email": user.Email,
"expires_in": 900,
})
}
func PassportPV(c *gin.Context) {
code := strings.TrimSpace(c.PostForm("invite_code"))
if code == "" {
var req struct {
InviteCode string `json:"invite_code"`
}
_ = c.ShouldBindJSON(&req)
code = strings.TrimSpace(req.InviteCode)
}
if code != "" {
_ = database.DB.Model(&model.InviteCode{}).
Where("code = ?", code).
UpdateColumn("pv", gorm.Expr("pv + ?", 1)).Error
}
Success(c, true)
}
func UserNoticeFetch(c *gin.Context) {
current := parsePositiveInt(c.DefaultQuery("current", "1"), 1)
pageSize := 5
query := database.DB.Model(&model.Notice{}).Where("`show` = ?", 1).Order("sort ASC").Order("id DESC")
var total int64
query.Count(&total)
var notices []model.Notice
if err := query.Offset((current - 1) * pageSize).Limit(pageSize).Find(&notices).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch notices")
return
}
Success(c, gin.H{
"data": notices,
"total": total,
})
}
func UserTrafficLog(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
Fail(c, http.StatusUnauthorized, "user not found")
return
}
now := time.Now()
startDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).Unix()
var records []model.StatUser
if err := database.DB.Where("user_id = ? AND record_at >= ?", user.ID, startDate).
Order("record_at DESC").
Find(&records).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch traffic log")
return
}
items := make([]gin.H, 0, len(records))
for _, record := range records {
items = append(items, gin.H{
"user_id": record.UserID,
"u": record.U,
"d": record.D,
"record_at": record.RecordAt,
"server_rate": 1,
})
}
Success(c, items)
}
func UserInviteSave(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
Fail(c, http.StatusUnauthorized, "user not found")
return
}
limit := service.MustGetInt("invite_gen_limit", 5)
var count int64
database.DB.Model(&model.InviteCode{}).
Where("user_id = ? AND status = ?", user.ID, 0).
Count(&count)
if int(count) >= limit {
Fail(c, http.StatusBadRequest, "the maximum number of creations has been reached")
return
}
inviteCode := model.InviteCode{
UserID: user.ID,
Code: randomAlphaNum(8),
Status: false,
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
}
if err := database.DB.Create(&inviteCode).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to create invite code")
return
}
Success(c, true)
}
func UserInviteFetch(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
Fail(c, http.StatusUnauthorized, "user not found")
return
}
commissionRate := service.MustGetInt("invite_commission", 10)
if user.CommissionRate != nil {
commissionRate = *user.CommissionRate
}
var codes []model.InviteCode
_ = database.DB.Where("user_id = ? AND status = ?", user.ID, 0).Order("id DESC").Find(&codes).Error
var totalInvited int64
var confirmedCommission int64
var pendingCommission int64
database.DB.Model(&model.User{}).Where("invite_user_id = ?", user.ID).Count(&totalInvited)
database.DB.Model(&model.CommissionLog{}).Where("invite_user_id = ? AND get_amount > 0", user.ID).Select("COALESCE(SUM(get_amount), 0)").Scan(&confirmedCommission)
database.DB.Model(&model.Order{}).Where("status = ? AND commission_status = ? AND invite_user_id = ?", 3, 0, user.ID).Select("COALESCE(SUM(commission_balance), 0)").Scan(&pendingCommission)
if service.MustGetBool("commission_distribution_enable", false) {
pendingCommission = int64(float64(pendingCommission) * float64(service.MustGetInt("commission_distribution_l1", 100)) / 100)
}
Success(c, gin.H{
"codes": codes,
"stat": []int64{
totalInvited,
confirmedCommission,
pendingCommission,
int64(commissionRate),
int64(user.CommissionBalance),
},
})
}
func UserInviteDetails(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
Fail(c, http.StatusUnauthorized, "user not found")
return
}
current := parsePositiveInt(c.DefaultQuery("current", "1"), 1)
pageSize := parsePositiveInt(c.DefaultQuery("page_size", "10"), 10)
if pageSize < 10 {
pageSize = 10
}
query := database.DB.Model(&model.CommissionLog{}).
Where("invite_user_id = ? AND get_amount > 0", user.ID).
Order("created_at DESC")
var total int64
query.Count(&total)
var logs []model.CommissionLog
if err := query.Offset((current - 1) * pageSize).Limit(pageSize).Find(&logs).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch invite details")
return
}
items := make([]gin.H, 0, len(logs))
for _, item := range logs {
items = append(items, gin.H{
"id": item.ID,
"order_amount": item.OrderAmount,
"trade_no": item.TradeNo,
"get_amount": item.GetAmount,
"created_at": item.CreatedAt,
})
}
Success(c, gin.H{
"data": items,
"total": total,
})
}
func UserOrderFetch(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
Fail(c, http.StatusUnauthorized, "user not found")
return
}
query := database.DB.Preload("Plan").Preload("Payment").Where("user_id = ?", user.ID).Order("created_at DESC")
if status := strings.TrimSpace(c.Query("status")); status != "" {
query = query.Where("status = ?", status)
}
var orders []model.Order
if err := query.Find(&orders).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch orders")
return
}
Success(c, normalizeOrders(orders))
}
func UserOrderDetail(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
Fail(c, http.StatusUnauthorized, "user not found")
return
}
tradeNo := strings.TrimSpace(c.Query("trade_no"))
if tradeNo == "" {
Fail(c, http.StatusBadRequest, "trade_no is required")
return
}
var order model.Order
if err := database.DB.Preload("Plan").Preload("Payment").
Where("user_id = ? AND trade_no = ?", user.ID, tradeNo).
First(&order).Error; err != nil {
Fail(c, http.StatusBadRequest, "order does not exist or has been paid")
return
}
if order.Plan == nil && order.PlanID != nil {
Fail(c, http.StatusBadRequest, "subscription plan does not exist")
return
}
data := normalizeOrder(order)
data["try_out_plan_id"] = service.MustGetInt("try_out_plan_id", 0)
if ids := parseIntSlice(order.SurplusOrderIDs); len(ids) > 0 {
var surplusOrders []model.Order
_ = database.DB.Where("id IN ?", ids).Order("id DESC").Find(&surplusOrders).Error
data["surplus_orders"] = normalizeOrders(surplusOrders)
}
Success(c, data)
}
func UserOrderCheck(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
Fail(c, http.StatusUnauthorized, "user not found")
return
}
tradeNo := strings.TrimSpace(c.Query("trade_no"))
if tradeNo == "" {
Fail(c, http.StatusBadRequest, "trade_no is required")
return
}
var order model.Order
if err := database.DB.Select("status").Where("user_id = ? AND trade_no = ?", user.ID, tradeNo).First(&order).Error; err != nil {
Fail(c, http.StatusBadRequest, "order does not exist")
return
}
Success(c, order.Status)
}
func UserOrderGetPaymentMethod(c *gin.Context) {
var methods []model.Payment
if err := database.DB.Select("id", "name", "payment", "icon", "handling_fee_fixed", "handling_fee_percent").
Where("enable = ?", 1).
Order("sort ASC").
Find(&methods).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch payment methods")
return
}
Success(c, methods)
}
func UserOrderCancel(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
Fail(c, http.StatusUnauthorized, "user not found")
return
}
var req struct {
TradeNo string `json:"trade_no"`
}
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.TradeNo) == "" {
Fail(c, http.StatusUnprocessableEntity, "invalid parameter")
return
}
var order model.Order
if err := database.DB.Where("trade_no = ? AND user_id = ?", req.TradeNo, user.ID).First(&order).Error; err != nil {
Fail(c, http.StatusBadRequest, "order does not exist")
return
}
if order.Status != 0 {
Fail(c, http.StatusBadRequest, "you can only cancel pending orders")
return
}
if err := database.DB.Model(&model.Order{}).Where("id = ?", order.ID).
Updates(map[string]any{"status": 2, "updated_at": time.Now().Unix()}).Error; err != nil {
Fail(c, http.StatusBadRequest, "cancel failed")
return
}
Success(c, true)
}
func UserOrderSave(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
Fail(c, http.StatusUnauthorized, "user not found")
return
}
var req struct {
PlanID int `json:"plan_id" binding:"required"`
Period string `json:"period" binding:"required"`
CouponCode string `json:"coupon_code"`
}
if err := c.ShouldBindJSON(&req); err != nil {
Fail(c, http.StatusBadRequest, "invalid request body")
return
}
var pendingCount int64
database.DB.Model(&model.Order{}).Where("user_id = ? AND status IN ?", user.ID, []int{0, 1}).Count(&pendingCount)
if pendingCount > 0 {
Fail(c, http.StatusBadRequest, "you have an unpaid or pending order, please try again later or cancel it")
return
}
var plan model.Plan
if err := database.DB.First(&plan, req.PlanID).Error; err != nil {
Fail(c, http.StatusBadRequest, "subscription plan does not exist")
return
}
totalAmount, ok := resolvePlanPrice(&plan, req.Period)
if !ok {
Fail(c, http.StatusBadRequest, "the period is not available for this subscription")
return
}
orderType := 1
if user.PlanID != nil {
orderType = 2
if *user.PlanID != req.PlanID {
orderType = 3
}
}
order := model.Order{
UserID: user.ID,
PlanID: &req.PlanID,
Period: req.Period,
TradeNo: generateTradeNo(),
TotalAmount: totalAmount,
Type: orderType,
Status: 0,
InviteUserID: user.InviteUserID,
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
}
if couponCode := strings.TrimSpace(req.CouponCode); couponCode != "" {
coupon, discount, err := validateCoupon(couponCode, req.PlanID, user.ID, req.Period)
if err != nil {
Fail(c, http.StatusBadRequest, err.Error())
return
}
order.CouponID = &coupon.ID
order.DiscountAmount = &discount
order.TotalAmount -= discount
if order.TotalAmount < 0 {
order.TotalAmount = 0
}
}
if err := database.DB.Create(&order).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to create order")
return
}
Success(c, order.TradeNo)
}
func UserOrderCheckout(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
Fail(c, http.StatusUnauthorized, "user not found")
return
}
var req struct {
TradeNo string `json:"trade_no" binding:"required"`
Method int `json:"method"`
}
if err := c.ShouldBindJSON(&req); err != nil {
Fail(c, http.StatusBadRequest, "invalid request body")
return
}
var order model.Order
if err := database.DB.Where("trade_no = ? AND user_id = ? AND status = ?", req.TradeNo, user.ID, 0).First(&order).Error; err != nil {
Fail(c, http.StatusBadRequest, "order does not exist or has been paid")
return
}
if order.TotalAmount <= 0 {
now := time.Now().Unix()
if err := database.DB.Model(&model.Order{}).Where("id = ?", order.ID).
Updates(map[string]any{"status": 3, "paid_at": now, "updated_at": now}).Error; err != nil {
Fail(c, http.StatusBadRequest, "payment failed")
return
}
c.JSON(http.StatusOK, gin.H{"type": -1, "data": true})
return
}
var payment model.Payment
if err := database.DB.Where("id = ? AND enable = ?", req.Method, 1).First(&payment).Error; err != nil {
Fail(c, http.StatusBadRequest, "payment method is not available")
return
}
var handlingAmount int64
if payment.HandlingFeePercent != nil {
handlingAmount += (order.TotalAmount * *payment.HandlingFeePercent) / 100
}
if payment.HandlingFeeFixed != nil {
handlingAmount += *payment.HandlingFeeFixed
}
updates := map[string]any{
"payment_id": payment.ID,
"updated_at": time.Now().Unix(),
"handling_amount": handlingAmount,
}
if err := database.DB.Model(&model.Order{}).Where("id = ?", order.ID).Updates(updates).Error; err != nil {
Fail(c, http.StatusBadRequest, "request failed, please try again later")
return
}
c.JSON(http.StatusOK, gin.H{
"type": 0,
"data": gin.H{
"trade_no": order.TradeNo,
"payment_id": payment.ID,
"payment": payment.Payment,
"total_amount": order.TotalAmount + handlingAmount,
"message": "payment method recorded; external gateway callback is not required for local validation flows",
"handling_fee": handlingAmount,
"requires_poll": true,
},
})
}
func UserCouponCheck(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
Fail(c, http.StatusUnauthorized, "user not found")
return
}
var req struct {
Code string `json:"code"`
PlanID int `json:"plan_id"`
Period string `json:"period"`
}
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Code) == "" {
Fail(c, http.StatusUnprocessableEntity, "coupon cannot be empty")
return
}
coupon, _, err := validateCoupon(req.Code, req.PlanID, user.ID, req.Period)
if err != nil {
Fail(c, http.StatusBadRequest, err.Error())
return
}
Success(c, gin.H{
"id": coupon.ID,
"name": coupon.Name,
"code": coupon.Code,
"type": coupon.Type,
"value": coupon.Value,
"limit_plan_ids": parseStringSlice(coupon.LimitPlanIDs),
"limit_period": parseStringSlice(coupon.LimitPeriod),
"started_at": coupon.StartedAt,
"ended_at": coupon.EndedAt,
"show": coupon.Show,
})
}
func UserGiftCardCheck(c *gin.Context) {
Fail(c, http.StatusBadRequest, "gift card integration is not enabled in the current Go backend")
}
func UserGiftCardRedeem(c *gin.Context) {
Fail(c, http.StatusBadRequest, "gift card integration is not enabled in the current Go backend")
}
func UserGiftCardHistory(c *gin.Context) {
page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
perPage := parsePositiveInt(c.DefaultQuery("per_page", "15"), 15)
Success(c, gin.H{
"data": []any{},
"pagination": gin.H{
"current_page": page,
"last_page": 1,
"per_page": perPage,
"total": 0,
},
})
}
func UserGiftCardDetail(c *gin.Context) {
Fail(c, http.StatusNotFound, "record does not exist")
}
func UserGiftCardTypes(c *gin.Context) {
Success(c, gin.H{
"types": map[int]string{
1: "general",
2: "plan",
3: "mystery",
},
})
}
func UserTelegramBotInfo(c *gin.Context) {
link := service.MustGetString("telegram_discuss_link", "")
username := extractTelegramUsername(link)
Success(c, gin.H{
"username": username,
"link": link,
"enabled": service.MustGetBool("telegram_bot_enable", false),
})
}
func UserGetStripePublicKey(c *gin.Context) {
var req struct {
ID int `json:"id"`
}
_ = c.ShouldBindJSON(&req)
if req.ID > 0 {
var payment model.Payment
if err := database.DB.Where("id = ? AND payment = ?", req.ID, "StripeCredit").First(&payment).Error; err == nil {
if value := parseConfigString(payment.Config, "stripe_pk_live"); value != "" {
Success(c, value)
return
}
}
}
if pk := service.MustGetString("stripe_pk_live", ""); pk != "" {
Success(c, pk)
return
}
Fail(c, http.StatusBadRequest, "payment is not found")
}
func quickLoginURL(c *gin.Context, userID int, redirectValues ...string) string {
redirect := ""
for _, candidate := range redirectValues {
candidate = strings.TrimSpace(candidate)
if candidate != "" {
redirect = candidate
break
}
}
verify := service.StoreQuickLoginToken(userID, 15*time.Minute)
query := url.Values{}
query.Set("verify", verify)
if redirect != "" {
query.Set("redirect", redirect)
}
return baseURL(c) + "/api/v1/passport/auth/token2Login?" + query.Encode()
}
func baseURL(c *gin.Context) string {
if appURL := service.GetAppURL(); appURL != "" {
return strings.TrimRight(appURL, "/")
}
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
}
return scheme + "://" + c.Request.Host
}
func generateTradeNo() string {
buf := make([]byte, 8)
if _, err := rand.Read(buf); err != nil {
return strconv.FormatInt(time.Now().UnixNano(), 10)
}
return fmt.Sprintf("%d%s", time.Now().Unix(), strings.ToUpper(hex.EncodeToString(buf)))
}
func randomAlphaNum(length int) string {
if length <= 0 {
return ""
}
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
buf := make([]byte, length)
random := make([]byte, length)
if _, err := rand.Read(random); err != nil {
return generateTradeNo()[:length]
}
for i := range buf {
buf[i] = alphabet[int(random[i])%len(alphabet)]
}
return string(buf)
}
func resolvePlanPrice(plan *model.Plan, period string) (int64, bool) {
if plan == nil || plan.Prices == nil || strings.TrimSpace(*plan.Prices) == "" {
return 0, false
}
var raw map[string]any
if err := json.Unmarshal([]byte(*plan.Prices), &raw); err != nil {
return 0, false
}
value, ok := raw[period]
if !ok {
return 0, false
}
switch typed := value.(type) {
case float64:
return int64(typed), true
case int:
return int64(typed), true
case int64:
return typed, true
case string:
parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
if err != nil {
return 0, false
}
return parsed, true
default:
return 0, false
}
}
func validateCoupon(code string, planID, userID int, period string) (*model.Coupon, int64, error) {
var coupon model.Coupon
if err := database.DB.Where("code = ?", strings.TrimSpace(code)).First(&coupon).Error; err != nil {
return nil, 0, errors.New("invalid coupon")
}
if !coupon.Show {
return nil, 0, errors.New("invalid coupon")
}
now := time.Now().Unix()
if coupon.LimitUse != nil && *coupon.LimitUse <= 0 {
return nil, 0, errors.New("this coupon is no longer available")
}
if coupon.StartedAt > 0 && now < coupon.StartedAt {
return nil, 0, errors.New("this coupon has not yet started")
}
if coupon.EndedAt > 0 && now > coupon.EndedAt {
return nil, 0, errors.New("this coupon has expired")
}
if planID > 0 && len(parseIntSlice(coupon.LimitPlanIDs)) > 0 && !containsInt(parseIntSlice(coupon.LimitPlanIDs), planID) {
return nil, 0, errors.New("the coupon code cannot be used for this subscription")
}
if period != "" && len(parseStringSlice(coupon.LimitPeriod)) > 0 && !containsString(parseStringSlice(coupon.LimitPeriod), period) {
return nil, 0, errors.New("the coupon code cannot be used for this period")
}
if coupon.LimitUseWithUser != nil && userID > 0 {
var usedCount int64
database.DB.Model(&model.Order{}).
Where("coupon_id = ? AND user_id = ? AND status NOT IN ?", coupon.ID, userID, []int{0, 2}).
Count(&usedCount)
if usedCount >= int64(*coupon.LimitUseWithUser) {
return nil, 0, fmt.Errorf("the coupon can only be used %d times per person", *coupon.LimitUseWithUser)
}
}
var discount int64
switch coupon.Type {
case 1:
discount = coupon.Value
case 2:
var plan model.Plan
if err := database.DB.First(&plan, planID).Error; err != nil {
return nil, 0, errors.New("subscription plan does not exist")
}
total, ok := resolvePlanPrice(&plan, period)
if !ok {
return nil, 0, errors.New("the period is not available for this subscription")
}
discount = (total * coupon.Value) / 100
default:
discount = 0
}
return &coupon, discount, nil
}
func normalizeOrders(orders []model.Order) []gin.H {
items := make([]gin.H, 0, len(orders))
for _, order := range orders {
items = append(items, normalizeOrder(order))
}
return items
}
func normalizeOrder(order model.Order) gin.H {
data := gin.H{
"id": order.ID,
"user_id": order.UserID,
"plan_id": order.PlanID,
"payment_id": order.PaymentID,
"period": order.Period,
"trade_no": order.TradeNo,
"total_amount": order.TotalAmount,
"handling_amount": order.HandlingAmount,
"balance_amount": order.BalanceAmount,
"refund_amount": order.RefundAmount,
"surplus_amount": order.SurplusAmount,
"type": order.Type,
"status": order.Status,
"surplus_order_ids": order.SurplusOrderIDs,
"coupon_id": order.CouponID,
"created_at": order.CreatedAt,
"updated_at": order.UpdatedAt,
"commission_status": order.CommissionStatus,
"invite_user_id": order.InviteUserID,
"actual_commission_balance": order.ActualCommissionBalance,
"commission_rate": order.CommissionRate,
"commission_auto_check": order.CommissionAutoCheck,
"commission_balance": order.CommissionBalance,
"discount_amount": order.DiscountAmount,
"paid_at": order.PaidAt,
"callback_no": order.CallbackNo,
"plan": order.Plan,
"payment": order.Payment,
}
return data
}
func parseIntSlice(raw *string) []int {
if raw == nil || strings.TrimSpace(*raw) == "" {
return nil
}
var values []int
if err := json.Unmarshal([]byte(*raw), &values); err == nil {
return values
}
parts := strings.Split(*raw, ",")
values = make([]int, 0, len(parts))
for _, part := range parts {
value, err := strconv.Atoi(strings.TrimSpace(part))
if err == nil {
values = append(values, value)
}
}
return values
}
func parseStringSlice(raw *string) []string {
if raw == nil || strings.TrimSpace(*raw) == "" {
return nil
}
var values []string
if err := json.Unmarshal([]byte(*raw), &values); err == nil {
return values
}
parts := strings.Split(*raw, ",")
values = make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
values = append(values, part)
}
}
return values
}
func containsInt(values []int, target int) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
func containsString(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
func extractTelegramUsername(link string) string {
link = strings.TrimSpace(link)
link = strings.TrimPrefix(link, "https://t.me/")
link = strings.TrimPrefix(link, "http://t.me/")
link = strings.TrimPrefix(link, "t.me/")
link = strings.TrimPrefix(link, "@")
link = strings.Trim(link, "/")
return link
}
func parseConfigString(raw *string, key string) string {
if raw == nil || strings.TrimSpace(*raw) == "" {
return ""
}
var payload map[string]any
if err := json.Unmarshal([]byte(*raw), &payload); err != nil {
return ""
}
value, ok := payload[key]
if !ok {
return ""
}
result, _ := value.(string)
return result
}
func upsertSetting(name string, value any, now time.Time) error {
if strings.TrimSpace(name) == "" {
return nil
}
var setting model.Setting
err := database.DB.Where("name = ?", name).First(&setting).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
stringValue, err := stringifySettingValue(value)
if err != nil {
return err
}
if errors.Is(err, gorm.ErrRecordNotFound) || setting.ID == 0 {
setting = model.Setting{
Name: name,
Value: stringValue,
CreatedAt: &now,
UpdatedAt: &now,
}
return database.DB.Create(&setting).Error
}
return database.DB.Model(&model.Setting{}).Where("id = ?", setting.ID).
Updates(map[string]any{"value": stringValue, "updated_at": now}).Error
}
func stringifySettingValue(value any) (string, error) {
switch typed := value.(type) {
case nil:
return "", nil
case string:
return typed, nil
case bool:
if typed {
return "1", nil
}
return "0", nil
case float64:
if typed == float64(int64(typed)) {
return strconv.FormatInt(int64(typed), 10), nil
}
return strconv.FormatFloat(typed, 'f', -1, 64), nil
case int:
return strconv.Itoa(typed), nil
case int64:
return strconv.FormatInt(typed, 10), nil
case []any, map[string]any:
bytes, err := json.Marshal(typed)
if err != nil {
return "", err
}
return string(bytes), nil
default:
return fmt.Sprint(typed), nil
}
}