295 lines
9.8 KiB
Go
295 lines
9.8 KiB
Go
package voiceroomredpacket
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"chatapp3-golang/internal/integration"
|
|
"chatapp3-golang/internal/model"
|
|
"chatapp3-golang/internal/utils"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
// Claim 领取语音房红包。
|
|
func (s *Service) Claim(ctx context.Context, user AuthUser, req ClaimRequest) (*ClaimResponse, error) {
|
|
packetNo := strings.TrimSpace(req.PacketNo)
|
|
if packetNo == "" {
|
|
return nil, NewAppError(http.StatusBadRequest, "missing_packet_no", "packetNo is required")
|
|
}
|
|
sysOrigin := normalizeSysOrigin(user.SysOrigin)
|
|
lockKey := fmt.Sprintf("voice_room:red_packet:claim_lock:%s:%d", packetNo, user.UserID)
|
|
lockOK, err := s.redis.SetNX(ctx, lockKey, "1", 15*time.Second).Result()
|
|
if err != nil {
|
|
return nil, NewAppError(http.StatusServiceUnavailable, "voice_room_red_packet_lock_failed", err.Error())
|
|
}
|
|
if !lockOK {
|
|
return nil, NewAppError(http.StatusConflict, "voice_room_red_packet_claim_in_progress", "red packet claim is in progress")
|
|
}
|
|
defer s.redis.Del(context.Background(), lockKey)
|
|
|
|
packet, err := s.loadPacketByNo(ctx, packetNo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if packet == nil {
|
|
return nil, NewAppError(http.StatusNotFound, "voice_room_red_packet_not_found", "red packet not found")
|
|
}
|
|
if existing, err := s.loadUserClaim(ctx, packet.ID, user.UserID); err != nil {
|
|
return nil, err
|
|
} else if existing != nil {
|
|
return s.resumeExistingClaim(ctx, *packet, *existing)
|
|
}
|
|
|
|
userRegion, err := s.resolveCachedUserRegion(ctx, sysOrigin, user.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var claim model.VoiceRoomRedPacketClaim
|
|
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var locked model.VoiceRoomRedPacket
|
|
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
Where("packet_no = ?", packetNo).
|
|
First(&locked).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return NewAppError(http.StatusNotFound, "voice_room_red_packet_not_found", "red packet not found")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := validateClaimable(locked, userRegion); err != nil {
|
|
return err
|
|
}
|
|
existing, err := loadUserClaimTx(tx, locked.ID, user.UserID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existing != nil {
|
|
claim = *existing
|
|
return nil
|
|
}
|
|
|
|
var slice model.VoiceRoomRedPacketSlice
|
|
err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
Where("packet_id = ? AND status = ?", locked.ID, sliceStatusUnclaimed).
|
|
Order("sort_no asc").
|
|
First(&slice).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
_ = tx.Model(&model.VoiceRoomRedPacket{}).
|
|
Where("id = ?", locked.ID).
|
|
Updates(map[string]any{"status": packetStatusFinished, "update_time": time.Now()}).Error
|
|
return NewAppError(http.StatusBadRequest, "voice_room_red_packet_finished", "red packet is finished")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
claimID, err := utils.NextID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
now := time.Now()
|
|
claim = model.VoiceRoomRedPacketClaim{
|
|
ID: claimID,
|
|
PacketID: locked.ID,
|
|
PacketNo: locked.PacketNo,
|
|
SliceID: slice.ID,
|
|
SysOrigin: locked.SysOrigin,
|
|
RoomID: locked.RoomID,
|
|
UserID: user.UserID,
|
|
UserRegionCode: userRegion,
|
|
Amount: slice.Amount,
|
|
WalletEventID: walletEventID(walletOriginClaim, locked.SysOrigin, locked.PacketNo, claimID),
|
|
WalletStatus: walletStatusPending,
|
|
ClaimTime: now,
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
}
|
|
if err := tx.Create(&claim).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Model(&model.VoiceRoomRedPacketSlice{}).
|
|
Where("id = ? AND status = ?", slice.ID, sliceStatusUnclaimed).
|
|
Updates(map[string]any{
|
|
"status": sliceStatusClaiming,
|
|
"claim_user_id": user.UserID,
|
|
"claim_record_id": claim.ID,
|
|
"update_time": now,
|
|
}).Error; err != nil {
|
|
return err
|
|
}
|
|
remainAmount := locked.RemainAmount - slice.Amount
|
|
remainCount := locked.RemainCount - 1
|
|
if remainAmount < 0 {
|
|
remainAmount = 0
|
|
}
|
|
if remainCount < 0 {
|
|
remainCount = 0
|
|
}
|
|
return tx.Model(&model.VoiceRoomRedPacket{}).
|
|
Where("id = ?", locked.ID).
|
|
Updates(map[string]any{
|
|
"remain_amount": remainAmount,
|
|
"remain_count": remainCount,
|
|
"update_time": now,
|
|
}).Error
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s.completeClaimWallet(ctx, claim)
|
|
}
|
|
|
|
func (s *Service) resumeExistingClaim(ctx context.Context, packet model.VoiceRoomRedPacket, claim model.VoiceRoomRedPacketClaim) (*ClaimResponse, error) {
|
|
if claim.WalletStatus == walletStatusSuccess {
|
|
return s.buildClaimResponse(ctx, claim)
|
|
}
|
|
if err := validateClaimableForRetry(packet); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.completeClaimWallet(ctx, claim)
|
|
}
|
|
|
|
func (s *Service) completeClaimWallet(ctx context.Context, claim model.VoiceRoomRedPacketClaim) (*ClaimResponse, error) {
|
|
if claim.WalletStatus != walletStatusSuccess {
|
|
err := s.wallet.ChangeGoldBalance(ctx, integration.GoldReceiptCommand{
|
|
ReceiptType: walletReceiptIncome,
|
|
UserID: claim.UserID,
|
|
SysOrigin: claim.SysOrigin,
|
|
EventID: claim.WalletEventID,
|
|
Remark: fmt.Sprintf("voice room red packet claim %s", claim.PacketNo),
|
|
Amount: integration.NewPennyAmountPayloadFromDollar(claim.Amount),
|
|
CloseDelayAsset: false,
|
|
OpUserType: "APP",
|
|
CustomizeOrigin: walletOriginClaim,
|
|
CustomizeOriginDesc: walletOriginClaimDesc,
|
|
})
|
|
if err != nil {
|
|
_ = s.db.WithContext(context.Background()).Model(&model.VoiceRoomRedPacketClaim{}).
|
|
Where("id = ?", claim.ID).
|
|
Updates(map[string]any{
|
|
"wallet_status": walletStatusFailed,
|
|
"error_message": trimErrorMessage(err.Error()),
|
|
"update_time": time.Now(),
|
|
}).Error
|
|
return nil, mapWalletError("voice_room_red_packet_wallet_income_failed", err)
|
|
}
|
|
now := time.Now()
|
|
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Model(&model.VoiceRoomRedPacketClaim{}).
|
|
Where("id = ?", claim.ID).
|
|
Updates(map[string]any{
|
|
"wallet_status": walletStatusSuccess,
|
|
"error_message": "",
|
|
"update_time": now,
|
|
}).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Model(&model.VoiceRoomRedPacketSlice{}).
|
|
Where("id = ?", claim.SliceID).
|
|
Updates(map[string]any{
|
|
"status": sliceStatusClaimed,
|
|
"update_time": now,
|
|
}).Error; err != nil {
|
|
return err
|
|
}
|
|
return tx.Model(&model.VoiceRoomRedPacket{}).
|
|
Where("id = ? AND remain_count = 0 AND status = ?", claim.PacketID, packetStatusActive).
|
|
Updates(map[string]any{
|
|
"status": packetStatusFinished,
|
|
"update_time": now,
|
|
}).Error
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
claim.WalletStatus = walletStatusSuccess
|
|
claim.UpdateTime = now
|
|
}
|
|
return s.buildClaimResponse(ctx, claim)
|
|
}
|
|
|
|
func validateClaimable(packet model.VoiceRoomRedPacket, userRegion string) error {
|
|
if packet.Status != packetStatusActive {
|
|
if packet.Status == packetStatusFinished {
|
|
return NewAppError(http.StatusBadRequest, "voice_room_red_packet_finished", "red packet is finished")
|
|
}
|
|
if packet.Status == packetStatusExpired || packet.Status == packetStatusRefunded || packet.Status == packetStatusRefundFailed {
|
|
return NewAppError(http.StatusBadRequest, "voice_room_red_packet_expired", "red packet is expired")
|
|
}
|
|
return NewAppError(http.StatusBadRequest, "voice_room_red_packet_unavailable", "red packet is unavailable")
|
|
}
|
|
now := time.Now()
|
|
if now.Before(packet.ClaimStartTime) {
|
|
return NewAppError(http.StatusBadRequest, "voice_room_red_packet_not_started", "red packet is not started")
|
|
}
|
|
if !now.Before(packet.ExpireTime) {
|
|
return NewAppError(http.StatusBadRequest, "voice_room_red_packet_expired", "red packet is expired")
|
|
}
|
|
if !strings.EqualFold(strings.TrimSpace(packet.SenderRegionCode), strings.TrimSpace(userRegion)) {
|
|
return NewAppError(http.StatusForbidden, "voice_room_region_not_match", "user region does not match")
|
|
}
|
|
if packet.RemainCount <= 0 || packet.RemainAmount <= 0 {
|
|
return NewAppError(http.StatusBadRequest, "voice_room_red_packet_finished", "red packet is finished")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateClaimableForRetry(packet model.VoiceRoomRedPacket) error {
|
|
if packet.Status == packetStatusExpired || packet.Status == packetStatusRefunded || packet.Status == packetStatusRefundFailed {
|
|
return NewAppError(http.StatusBadRequest, "voice_room_red_packet_expired", "red packet is expired")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) loadPacketByNo(ctx context.Context, packetNo string) (*model.VoiceRoomRedPacket, error) {
|
|
var row model.VoiceRoomRedPacket
|
|
err := s.db.WithContext(ctx).Where("packet_no = ?", packetNo).First(&row).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &row, nil
|
|
}
|
|
|
|
func (s *Service) loadUserClaim(ctx context.Context, packetID, userID int64) (*model.VoiceRoomRedPacketClaim, error) {
|
|
return loadUserClaimTx(s.db.WithContext(ctx), packetID, userID)
|
|
}
|
|
|
|
func loadUserClaimTx(tx *gorm.DB, packetID, userID int64) (*model.VoiceRoomRedPacketClaim, error) {
|
|
var row model.VoiceRoomRedPacketClaim
|
|
err := tx.Where("packet_id = ? AND user_id = ?", packetID, userID).First(&row).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &row, nil
|
|
}
|
|
|
|
func (s *Service) buildClaimResponse(ctx context.Context, claim model.VoiceRoomRedPacketClaim) (*ClaimResponse, error) {
|
|
packet, err := s.loadPacketByNo(ctx, claim.PacketNo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if packet == nil {
|
|
return nil, NewAppError(http.StatusNotFound, "voice_room_red_packet_not_found", "red packet not found")
|
|
}
|
|
return &ClaimResponse{
|
|
PacketNo: claim.PacketNo,
|
|
Amount: claim.Amount,
|
|
RemainCount: packet.RemainCount,
|
|
TotalCount: packet.TotalCount,
|
|
ClaimTime: formatTime(claim.ClaimTime),
|
|
Status: claim.WalletStatus,
|
|
}, nil
|
|
}
|