1036 lines
28 KiB
Go
1036 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(¬ices).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 baseURL := requestBaseURL(c); baseURL != "" {
|
|
return baseURL
|
|
}
|
|
return ""
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|