init: 1.0.0

This commit is contained in:
Chang lue Tsen
2025-04-25 12:08:29 +09:00
commit 8addcc584b
1031 changed files with 76472 additions and 0 deletions
@@ -0,0 +1,13 @@
package order
import (
"github.com/perfect-panel/ppanel-server/internal/model/coupon"
)
func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 {
if couponInfo.Type == 1 {
return int64(float64(amount) * (float64(couponInfo.Discount) / float64(100)))
} else {
return min(couponInfo.Discount, amount)
}
}
@@ -0,0 +1,20 @@
package order
import "github.com/perfect-panel/ppanel-server/internal/model/payment"
func calculateFee(amount int64, config *payment.Payment) int64 {
var fee float64
switch config.FeeMode {
case 0:
return 0
case 1:
fee = float64(amount) * (float64(config.FeePercent) / float64(100))
case 2:
if amount > 0 {
fee = float64(config.FeeAmount)
}
case 3:
fee = float64(amount)*(float64(config.FeePercent)/float64(100)) + float64(config.FeeAmount)
}
return int64(fee)
}
@@ -0,0 +1,197 @@
package order
import (
"context"
"encoding/json"
"github.com/perfect-panel/ppanel-server/internal/model/user"
"github.com/perfect-panel/ppanel-server/pkg/payment/stripe"
"gorm.io/gorm"
"github.com/perfect-panel/ppanel-server/internal/model/order"
"github.com/perfect-panel/ppanel-server/internal/model/payment"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/internal/types"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/payment/alipay"
)
type CloseOrderLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewCloseOrderLogic Close order
func NewCloseOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CloseOrderLogic {
return &CloseOrderLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error {
// Find order information by order number
orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
if err != nil {
l.Errorw("[CloseOrder] Find order info failed",
logger.Field("error", err.Error()),
logger.Field("orderNo", req.OrderNo),
)
return nil
}
// If the order status is not 1, it means that the order has been closed or paid
if orderInfo.Status != 1 {
l.Infow("[CloseOrder] Order status is not 1",
logger.Field("orderNo", req.OrderNo),
logger.Field("status", orderInfo.Status),
)
return nil
}
err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
// update order status
err := tx.Model(&order.Order{}).Where("order_no = ?", req.OrderNo).Update("status", 3).Error
if err != nil {
l.Errorw("[CloseOrder] Update order status failed",
logger.Field("error", err.Error()),
logger.Field("orderNo", req.OrderNo),
)
return err
}
// If User ID is 0, it means that the order is a guest order and does not need to be refunded, the order can be deleted directly
if orderInfo.UserId == 0 {
err = tx.Model(&order.Order{}).Where("order_no = ?", req.OrderNo).Delete(&order.Order{}).Error
if err != nil {
l.Errorw("[CloseOrder] Delete order failed",
logger.Field("error", err.Error()),
logger.Field("orderNo", req.OrderNo),
)
return err
}
return nil
}
// refund deduction amount to user deduction balance
if orderInfo.GiftAmount > 0 {
userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, orderInfo.UserId)
if err != nil {
l.Errorw("[CloseOrder] Find user info failed",
logger.Field("error", err.Error()),
logger.Field("user_id", orderInfo.UserId),
)
return err
}
deduction := userInfo.GiftAmount + orderInfo.GiftAmount
err = tx.Model(&user.User{}).Where("id = ?", orderInfo.UserId).Update("deduction", deduction).Error
if err != nil {
l.Errorw("[CloseOrder] Refund deduction amount failed",
logger.Field("error", err.Error()),
logger.Field("uid", orderInfo.UserId),
logger.Field("deduction", orderInfo.GiftAmount),
)
return err
}
// Record the deduction refund log
giftAmountLog := &user.GiftAmountLog{
UserId: orderInfo.UserId,
OrderNo: orderInfo.OrderNo,
Amount: orderInfo.GiftAmount,
Type: 1,
Balance: deduction,
Remark: "Order cancellation refund",
}
err = tx.Model(&user.GiftAmountLog{}).Create(giftAmountLog).Error
if err != nil {
l.Errorw("[CloseOrder] Record cancellation refund log failed",
logger.Field("error", err.Error()),
logger.Field("uid", orderInfo.UserId),
logger.Field("deduction", orderInfo.GiftAmount),
)
return err
}
// update user cache
return l.svcCtx.UserModel.UpdateUserCache(l.ctx, userInfo)
}
return nil
})
if err != nil {
return err
}
return nil
}
// confirmationPayment Determine whether the payment is successful
//
//nolint:unused
func (l *CloseOrderLogic) confirmationPayment(order *order.Order) bool {
paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, order.PaymentId)
if err != nil {
l.Errorw("[CloseOrder] Find payment config failed", logger.Field("error", err.Error()), logger.Field("paymentMark", order.Method))
return false
}
switch order.Method {
case AlipayF2f:
if l.queryAlipay(paymentConfig, order.TradeNo) {
return true
}
case StripeAlipay:
if l.queryStripe(paymentConfig, order.TradeNo) {
return true
}
case StripeWeChatPay:
if l.queryStripe(paymentConfig, order.TradeNo) {
return true
}
default:
l.Infow("[CloseOrder] Unsupported payment method", logger.Field("paymentMethod", order.Method))
}
return false
}
// queryAlipay Query Alipay payment status
//
//nolint:unused
func (l *CloseOrderLogic) queryAlipay(paymentConfig *payment.Payment, TradeNo string) bool {
config := payment.AlipayF2FConfig{}
if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil {
l.Errorw("[CloseOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config))
return false
}
client := alipay.NewClient(alipay.Config{
AppId: config.AppId,
PrivateKey: config.PrivateKey,
PublicKey: config.PublicKey,
InvoiceName: config.InvoiceName,
})
status, err := client.QueryTrade(l.ctx, TradeNo)
if err != nil {
l.Errorw("[CloseOrder] Query trade failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo))
return false
}
if status == alipay.Success || status == alipay.Finished {
return true
}
return false
}
// queryStripe Query Stripe payment status
//
//nolint:unused
func (l *CloseOrderLogic) queryStripe(paymentConfig *payment.Payment, TradeNo string) bool {
config := payment.StripeConfig{}
if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil {
l.Errorw("[CloseOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config))
return false
}
client := stripe.NewClient(stripe.Config{
PublicKey: config.PublicKey,
SecretKey: config.SecretKey,
WebhookSecret: config.WebhookSecret,
})
status, err := client.QueryOrderStatus(TradeNo)
if err != nil {
l.Errorw("[CloseOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo))
return false
}
return status
}
+9
View File
@@ -0,0 +1,9 @@
package order
const (
Epay = "epay"
AlipayF2f = "alipay_f2f"
StripeAlipay = "stripe_alipay"
StripeWeChatPay = "stripe_wechat_pay"
Balance = "balance"
)
@@ -0,0 +1,14 @@
package order
import "github.com/perfect-panel/ppanel-server/internal/types"
func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 {
var finalDiscount int64 = 100
for _, discount := range discounts {
if inputMonths >= discount.Quantity && discount.Discount < finalDiscount {
finalDiscount = discount.Discount
}
}
return float64(finalDiscount) / float64(100)
}
@@ -0,0 +1,126 @@
package order
import (
"context"
"encoding/json"
"github.com/perfect-panel/ppanel-server/internal/model/order"
"github.com/perfect-panel/ppanel-server/pkg/tool"
"github.com/perfect-panel/ppanel-server/pkg/constant"
"github.com/perfect-panel/ppanel-server/internal/model/user"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/internal/types"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
)
type PreCreateOrderLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Pre create order
func NewPreCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreCreateOrderLogic {
return &PreCreateOrderLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (resp *types.PreOrderResponse, err error) {
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
logger.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
// find subscribe plan
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId)
if err != nil {
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error())
}
var discount float64 = 1
if sub.Discount != "" {
var dis []types.SubscribeDiscount
_ = json.Unmarshal([]byte(sub.Discount), &dis)
discount = getDiscount(dis, req.Quantity)
}
price := sub.UnitPrice * req.Quantity
amount := int64(float64(price) * discount)
discountAmount := price - amount
var couponAmount int64
if req.Coupon != "" {
couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error())
}
if couponInfo.Count <= couponInfo.UsedCount {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponAlreadyUsed), "coupon used")
}
var count int64
err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
return tx.Model(&order.Order{}).Where("user_id = ? and coupon = ?", u.Id, req.Coupon).Count(&count).Error
})
if err != nil {
l.Logger.Error("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id), logger.Field("coupon", req.Coupon))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error())
}
if count >= couponInfo.UserLimit {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon limit exceeded")
}
couponSub := tool.StringToInt64Slice(couponInfo.Subscribe)
if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match")
}
couponAmount = calculateCoupon(amount, couponInfo)
}
amount -= couponAmount
var deductionAmount int64
// Check user deduction amount
if u.GiftAmount > 0 {
if u.GiftAmount >= amount {
deductionAmount = amount
amount = 0
} else {
deductionAmount = u.GiftAmount
amount -= u.GiftAmount
}
}
var feeAmount int64
if req.Payment != 0 {
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
if err != nil {
l.Logger.Error("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error())
}
// Calculate the handling fee
if amount > 0 {
feeAmount = calculateFee(amount, payment)
}
amount += feeAmount
}
resp = &types.PreOrderResponse{
Price: price,
Amount: amount,
Discount: discountAmount,
GiftAmount: deductionAmount,
Coupon: req.Coupon,
CouponDiscount: couponAmount,
FeeAmount: feeAmount,
}
return
}
@@ -0,0 +1,229 @@
package order
import (
"context"
"encoding/json"
"time"
"github.com/perfect-panel/ppanel-server/pkg/constant"
"github.com/hibiken/asynq"
"github.com/perfect-panel/ppanel-server/internal/model/order"
"github.com/perfect-panel/ppanel-server/internal/model/user"
"github.com/perfect-panel/ppanel-server/pkg/tool"
"github.com/perfect-panel/ppanel-server/pkg/xerr"
queue "github.com/perfect-panel/ppanel-server/queue/types"
"github.com/pkg/errors"
"gorm.io/gorm"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/internal/types"
"github.com/perfect-panel/ppanel-server/pkg/logger"
)
type PurchaseLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
const (
CloseOrderTimeMinutes = 15
)
// NewPurchaseLogic purchase Subscription
func NewPurchaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PurchaseLogic {
return &PurchaseLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.PurchaseOrderResponse, err error) {
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
logger.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
// find user subscription
userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id)
if err != nil {
l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscription error: %v", err.Error())
}
if l.svcCtx.Config.Subscribe.SingleModel {
if len(userSub) > 0 {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserSubscribeExist), "user has subscription")
}
}
// find subscribe plan
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId)
if err != nil {
l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error())
}
// check subscribe plan status
if !*sub.Sell {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell")
}
// check subscribe plan limit
if sub.Quota > 0 {
var count int64
for _, v := range userSub {
if v.SubscribeId == req.SubscribeId {
count += 1
}
}
if count >= sub.Quota {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeQuotaLimit), "quota limit")
}
}
var discount float64 = 1
if sub.Discount != "" {
var dis []types.SubscribeDiscount
_ = json.Unmarshal([]byte(sub.Discount), &dis)
discount = getDiscount(dis, req.Quantity)
}
price := sub.UnitPrice * req.Quantity
// discount amount
amount := int64(float64(price) * discount)
discountAmount := price - amount
var coupon int64 = 0
// Calculate the coupon deduction
if req.Coupon != "" {
couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error())
}
if couponInfo.Count <= couponInfo.UsedCount {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used")
}
couponSub := tool.StringToInt64Slice(couponInfo.Subscribe)
if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match")
}
var count int64
err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
return tx.Model(&order.Order{}).Where("user_id = ? and coupon = ?", u.Id, req.Coupon).Count(&count).Error
})
if err != nil {
l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id), logger.Field("coupon", req.Coupon))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error())
}
if count >= couponInfo.UserLimit {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon limit exceeded")
}
coupon = calculateCoupon(amount, couponInfo)
}
// Calculate the handling fee
amount -= coupon
var deductionAmount int64
// Check user deduction amount
if u.GiftAmount > 0 {
if u.GiftAmount >= amount {
deductionAmount = amount
amount = 0
u.GiftAmount -= amount
} else {
deductionAmount = u.GiftAmount
amount -= u.GiftAmount
u.GiftAmount = 0
}
}
// find payment method
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
if err != nil {
l.Logger.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error())
}
var feeAmount int64
// Calculate the handling fee
if amount > 0 {
feeAmount = calculateFee(amount, payment)
}
// query user is new purchase or renewal
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
if err != nil {
l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user order error: %v", err.Error())
}
// create order
orderInfo := &order.Order{
UserId: u.Id,
OrderNo: tool.GenerateTradeNo(),
Type: 1,
Quantity: req.Quantity,
Price: price,
Amount: amount,
Discount: discountAmount,
GiftAmount: deductionAmount,
Coupon: req.Coupon,
CouponDiscount: coupon,
PaymentId: payment.Id,
Method: payment.Platform,
FeeAmount: feeAmount,
Status: 1,
IsNew: isNew,
SubscribeId: req.SubscribeId,
}
// Database transaction
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
// update user deduction && Pre deduction ,Return after canceling the order
if orderInfo.GiftAmount > 0 {
// update user deduction && Pre deduction ,Return after canceling the order
if e := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil {
l.Errorw("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u))
return e
}
// create deduction record
giftAmountLog := user.GiftAmountLog{
UserId: orderInfo.UserId,
OrderNo: orderInfo.OrderNo,
Amount: orderInfo.GiftAmount,
Type: 2,
Balance: u.GiftAmount,
Remark: "Purchase order deduction",
}
if e := db.Model(&user.GiftAmountLog{}).Create(&giftAmountLog).Error; e != nil {
l.Errorw("[Purchase] Database insert error",
logger.Field("error", err.Error()),
logger.Field("deductionLog", giftAmountLog),
)
return e
}
}
// insert order
return db.WithContext(l.ctx).Model(&order.Order{}).Create(&orderInfo).Error
})
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert order error: %v", err.Error())
}
// Deferred task
payload := queue.DeferCloseOrderPayload{
OrderNo: orderInfo.OrderNo,
}
val, err := json.Marshal(payload)
if err != nil {
l.Errorw("[CreateOrder] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload))
}
task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3))
taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute))
if err != nil {
l.Errorw("[CreateOrder] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task))
} else {
l.Infow("[CreateOrder] Enqueue task success", logger.Field("TaskID", taskInfo.ID))
}
return &types.PurchaseOrderResponse{
OrderNo: orderInfo.OrderNo,
}, nil
}
@@ -0,0 +1,40 @@
package order
import (
"context"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/internal/types"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/tool"
"github.com/perfect-panel/ppanel-server/pkg/xerr"
"github.com/pkg/errors"
)
type QueryOrderDetailLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Get order
func NewQueryOrderDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryOrderDetailLogic {
return &QueryOrderDetailLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryOrderDetailLogic) QueryOrderDetail(req *types.QueryOrderDetailRequest) (resp *types.OrderDetail, err error) {
orderInfo, err := l.svcCtx.OrderModel.FindOneDetailsByOrderNo(l.ctx, req.OrderNo)
if err != nil {
l.Errorw("[QueryOrderDetail] Database query error", logger.Field("error", err.Error()), logger.Field("order_no", req.OrderNo))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", err.Error())
}
resp = &types.OrderDetail{}
tool.DeepCopy(resp, orderInfo)
// Prevent commission amount leakage
resp.Commission = 0
return
}
@@ -0,0 +1,56 @@
package order
import (
"context"
"github.com/perfect-panel/ppanel-server/pkg/constant"
"github.com/perfect-panel/ppanel-server/internal/model/user"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/internal/types"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/tool"
"github.com/perfect-panel/ppanel-server/pkg/xerr"
"github.com/pkg/errors"
)
type QueryOrderListLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Get order list
func NewQueryOrderListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryOrderListLogic {
return &QueryOrderListLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryOrderListLogic) QueryOrderList(req *types.QueryOrderListRequest) (resp *types.QueryOrderListResponse, err error) {
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
logger.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
total, data, err := l.svcCtx.OrderModel.QueryOrderListByPage(l.ctx, req.Page, req.Size, 0, u.Id, 0, "")
if err != nil {
l.Errorw("[QueryOrderListLogic] Query order list failed", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query order list failed")
}
resp = &types.QueryOrderListResponse{
Total: total,
List: make([]types.OrderDetail, 0),
}
for _, item := range data {
var orderInfo types.OrderDetail
tool.DeepCopy(&orderInfo, item)
// Prevent commission amount leakage
orderInfo.Commission = 0
resp.List = append(resp.List, orderInfo)
}
return
}
@@ -0,0 +1,92 @@
package order
import (
"context"
"encoding/json"
"time"
"github.com/perfect-panel/ppanel-server/pkg/constant"
"github.com/perfect-panel/ppanel-server/pkg/xerr"
"github.com/hibiken/asynq"
"github.com/perfect-panel/ppanel-server/internal/model/order"
"github.com/perfect-panel/ppanel-server/internal/model/user"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/internal/types"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/tool"
queue "github.com/perfect-panel/ppanel-server/queue/types"
"github.com/pkg/errors"
)
type RechargeLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Recharge
func NewRechargeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RechargeLogic {
return &RechargeLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RechargeLogic) Recharge(req *types.RechargeOrderRequest) (resp *types.RechargeOrderResponse, err error) {
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
logger.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
// find payment method
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
if err != nil {
l.Errorw("[Recharge] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
return nil, errors.Wrapf(err, "find payment error: %v", err.Error())
}
// Calculate the handling fee
feeAmount := calculateFee(req.Amount, payment)
// query user is new purchase or renewal
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
if err != nil {
l.Errorw("[Recharge] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
return nil, errors.Wrapf(err, "query user error: %v", err.Error())
}
orderInfo := order.Order{
UserId: u.Id,
OrderNo: tool.GenerateTradeNo(),
Type: 4,
Price: req.Amount,
Amount: req.Amount + feeAmount,
FeeAmount: feeAmount,
PaymentId: payment.Id,
Method: payment.Platform,
Status: 1,
IsNew: isNew,
}
err = l.svcCtx.OrderModel.Insert(l.ctx, &orderInfo)
if err != nil {
l.Errorw("[Recharge] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo))
return nil, errors.Wrapf(err, "insert order error: %v", err.Error())
}
// Deferred task
payload := queue.DeferCloseOrderPayload{
OrderNo: orderInfo.OrderNo,
}
val, err := json.Marshal(payload)
if err != nil {
l.Errorw("[Recharge] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload))
}
task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3))
taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute))
if err != nil {
l.Errorw("[Recharge] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task))
} else {
l.Infow("[Recharge] Enqueue task success", logger.Field("TaskID", taskInfo.ID))
}
return &types.RechargeOrderResponse{
OrderNo: orderInfo.OrderNo,
}, nil
}
+197
View File
@@ -0,0 +1,197 @@
package order
import (
"context"
"encoding/json"
"time"
"github.com/perfect-panel/ppanel-server/pkg/constant"
"gorm.io/gorm"
"github.com/hibiken/asynq"
"github.com/perfect-panel/ppanel-server/internal/model/order"
"github.com/perfect-panel/ppanel-server/internal/model/user"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/internal/types"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/tool"
"github.com/perfect-panel/ppanel-server/pkg/xerr"
queue "github.com/perfect-panel/ppanel-server/queue/types"
"github.com/pkg/errors"
)
type RenewalLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Renewal Subscription
func NewRenewalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenewalLogic {
return &RenewalLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.RenewalOrderResponse, err error) {
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
logger.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
orderNo := tool.GenerateTradeNo()
// find user subscribe
userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscribe error: %v", err.Error())
}
// find subscription
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, userSubscribe.SubscribeId)
if err != nil {
l.Errorw("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", userSubscribe.SubscribeId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error())
}
// check subscribe plan status
if !*sub.Sell {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell")
}
var discount float64 = 1
if sub.Discount != "" {
var dis []types.SubscribeDiscount
_ = json.Unmarshal([]byte(sub.Discount), &dis)
discount = getDiscount(dis, req.Quantity)
}
price := sub.UnitPrice * req.Quantity
amount := int64(float64(price) * discount)
discountAmount := price - amount
var coupon int64 = 0
if req.Coupon != "" {
couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error())
}
if couponInfo.Count <= couponInfo.UsedCount {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used")
}
couponSub := tool.StringToInt64Slice(couponInfo.Subscribe)
if len(couponSub) > 0 && !tool.Contains(couponSub, sub.Id) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match")
}
var count int64
err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
return tx.Model(&order.Order{}).Where("user_id = ? and coupon = ?", u.Id, req.Coupon).Count(&count).Error
})
if err != nil {
l.Errorw("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id), logger.Field("coupon", req.Coupon))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error())
}
if count >= couponInfo.UserLimit {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon limit exceeded")
}
coupon = calculateCoupon(amount, couponInfo)
}
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
if err != nil {
l.Errorw("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
return nil, errors.Wrapf(err, "find payment error: %v", err.Error())
}
amount -= coupon
var deductionAmount int64
// Check user deduction amount
if u.GiftAmount > 0 {
if u.GiftAmount >= amount {
deductionAmount = amount
amount = 0
u.GiftAmount -= amount
} else {
deductionAmount = u.GiftAmount
amount -= u.GiftAmount
u.GiftAmount = 0
}
}
var feeAmount int64
// Calculate the handling fee
if amount > 0 {
feeAmount = calculateFee(amount, payment)
}
amount += feeAmount
// create order
orderInfo := order.Order{
UserId: u.Id,
ParentId: userSubscribe.OrderId,
OrderNo: orderNo,
Type: 2,
Quantity: req.Quantity,
Price: price,
Amount: amount,
GiftAmount: deductionAmount,
Discount: discountAmount,
Coupon: req.Coupon,
CouponDiscount: coupon,
PaymentId: payment.Id,
Method: payment.Platform,
FeeAmount: feeAmount,
Status: 1,
SubscribeId: userSubscribe.SubscribeId,
SubscribeToken: userSubscribe.Token,
}
// Database transaction
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
// update user deduction && Pre deduction ,Return after canceling the order
if orderInfo.GiftAmount > 0 {
// update user deduction && Pre deduction ,Return after canceling the order
if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil {
l.Errorw("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u))
return err
}
// create deduction record
deductionLog := user.GiftAmountLog{
UserId: orderInfo.UserId,
OrderNo: orderInfo.OrderNo,
Amount: orderInfo.GiftAmount,
Type: 2,
Balance: u.GiftAmount,
Remark: "Renewal order deduction",
}
if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil {
l.Errorw("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog))
return err
}
}
// insert order
return db.Model(&order.Order{}).Create(&orderInfo).Error
})
if err != nil {
l.Errorw("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo))
return nil, errors.Wrapf(err, "insert order error: %v", err.Error())
}
// Deferred task
payload := queue.DeferCloseOrderPayload{
OrderNo: orderInfo.OrderNo,
}
val, err := json.Marshal(payload)
if err != nil {
l.Errorw("[Renewal] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload))
}
task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3))
taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute))
if err != nil {
l.Errorw("[Renewal] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task))
} else {
l.Infow("[Renewal] Enqueue task success", logger.Field("TaskID", taskInfo.ID))
}
return &types.RenewalOrderResponse{
OrderNo: orderInfo.OrderNo,
}, nil
}
@@ -0,0 +1,145 @@
package order
import (
"context"
"encoding/json"
"time"
"github.com/perfect-panel/ppanel-server/pkg/constant"
"github.com/perfect-panel/ppanel-server/pkg/xerr"
"gorm.io/gorm"
"github.com/hibiken/asynq"
"github.com/perfect-panel/ppanel-server/internal/model/order"
"github.com/perfect-panel/ppanel-server/internal/model/user"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/internal/types"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/tool"
queue "github.com/perfect-panel/ppanel-server/queue/types"
"github.com/pkg/errors"
)
type ResetTrafficLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Reset traffic
func NewResetTrafficLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetTrafficLogic {
return &ResetTrafficLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ResetTrafficLogic) ResetTraffic(req *types.ResetTrafficOrderRequest) (resp *types.ResetTrafficOrderResponse, err error) {
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
logger.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
// find user subscription
userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID)
if err != nil {
l.Errorw("[ResetTraffic] Database query error", logger.Field("error", err.Error()), logger.Field("UserSubscribeID", req.UserSubscribeID))
return nil, errors.Wrapf(err, "find user subscribe error: %v", err.Error())
}
if userSubscribe.Subscribe == nil {
l.Errorw("[ResetTraffic] subscribe not found", logger.Field("UserSubscribeID", req.UserSubscribeID))
return nil, errors.New("subscribe not found")
}
amount := userSubscribe.Subscribe.Replacement
var deductionAmount int64
// Check user deduction amount
if u.GiftAmount > 0 {
if u.GiftAmount >= amount {
deductionAmount = amount
amount = 0
u.GiftAmount -= amount
} else {
deductionAmount = u.GiftAmount
amount -= u.GiftAmount
u.GiftAmount = 0
}
}
// find payment method
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
if err != nil {
l.Errorw("[ResetTraffic] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
return nil, errors.Wrapf(err, "find payment error: %v", err.Error())
}
var feeAmount int64
// Calculate the handling fee
if amount > 0 {
feeAmount = calculateFee(amount, payment)
}
// create order
orderInfo := order.Order{
Id: 0,
ParentId: userSubscribe.OrderId,
UserId: u.Id,
OrderNo: tool.GenerateTradeNo(),
Type: 3,
Price: userSubscribe.Subscribe.Replacement,
Amount: amount + feeAmount,
GiftAmount: deductionAmount,
FeeAmount: feeAmount,
PaymentId: payment.Id,
Method: payment.Platform,
Status: 1,
SubscribeId: userSubscribe.SubscribeId,
SubscribeToken: userSubscribe.Token,
}
// Database transaction
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
// update user deduction && Pre deduction ,Return after canceling the order
if orderInfo.GiftAmount > 0 {
// update user deduction && Pre deduction ,Return after canceling the order
if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil {
l.Errorw("[ResetTraffic] Database update error", logger.Field("error", err.Error()), logger.Field("user", u))
return err
}
// create deduction record
deductionLog := user.GiftAmountLog{
UserId: orderInfo.UserId,
OrderNo: orderInfo.OrderNo,
Amount: orderInfo.GiftAmount,
Type: 2,
Balance: u.GiftAmount,
Remark: "ResetTraffic order deduction",
}
if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil {
l.Errorw("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog))
return err
}
}
// insert order
return db.Model(&order.Order{}).Create(&orderInfo).Error
})
if err != nil {
l.Errorw("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo))
return nil, errors.Wrapf(err, "insert order error: %v", err.Error())
}
// Deferred task
payload := queue.DeferCloseOrderPayload{
OrderNo: orderInfo.OrderNo,
}
val, err := json.Marshal(payload)
if err != nil {
l.Errorw("[ResetTraffic] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload))
}
task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3))
taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute))
if err != nil {
l.Errorw("[ResetTraffic] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task))
} else {
l.Infow("[ResetTraffic] Enqueue task success", logger.Field("TaskID", taskInfo.ID))
}
return &types.ResetTrafficOrderResponse{
OrderNo: orderInfo.OrderNo,
}, nil
}