feat(points): 增加积分兑换联系信息
This commit is contained in:
@@ -56,7 +56,10 @@ type ProductRequest struct {
|
||||
}
|
||||
|
||||
type RedeemRequest struct {
|
||||
Quantity int `json:"quantity" binding:"required,min=1,max=10"`
|
||||
Quantity int `json:"quantity" binding:"required,min=1,max=10"`
|
||||
ContactName string `json:"contact_name" binding:"required,max=32"`
|
||||
ContactPhone string `json:"contact_phone" binding:"required,len=11,numeric"`
|
||||
CustomerRemark string `json:"customer_remark" binding:"max=500"`
|
||||
}
|
||||
|
||||
type CompleteRedemptionRequest struct {
|
||||
@@ -73,6 +76,9 @@ type RedemptionDTO struct {
|
||||
ProductTitle string `json:"product_title"`
|
||||
ProductCoverURL string `json:"product_cover_url"`
|
||||
RedemptionInstructions string `json:"redemption_instructions"`
|
||||
ContactName string `json:"contact_name"`
|
||||
ContactPhone string `json:"contact_phone"`
|
||||
CustomerRemark string `json:"customer_remark"`
|
||||
PointsPrice int64 `json:"points_price"`
|
||||
Quantity int `json:"quantity"`
|
||||
TotalPoints int64 `json:"total_points"`
|
||||
@@ -111,6 +117,9 @@ func toRedemptionDTO(item model.PointsMallRedemption) RedemptionDTO {
|
||||
ProductTitle: item.ProductTitle,
|
||||
ProductCoverURL: item.ProductCoverURL,
|
||||
RedemptionInstructions: item.RedemptionInstructions,
|
||||
ContactName: item.ContactName,
|
||||
ContactPhone: item.ContactPhone,
|
||||
CustomerRemark: item.CustomerRemark,
|
||||
PointsPrice: item.PointsPrice,
|
||||
Quantity: item.Quantity,
|
||||
TotalPoints: item.TotalPoints,
|
||||
|
||||
@@ -107,10 +107,10 @@ func (h *Handler) Redeem(c *gin.Context) {
|
||||
productID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
var req RedeemRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "兑换数量不正确")
|
||||
response.BadRequest(c, "请填写联系人、正确的手机号和兑换数量")
|
||||
return
|
||||
}
|
||||
item, err := h.service.Redeem(c.Request.Context(), userID, productID, req.Quantity)
|
||||
item, err := h.service.Redeem(c.Request.Context(), userID, productID, req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -18,6 +19,8 @@ type Repository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
var mobilePhonePattern = regexp.MustCompile(`^1[3-9]\d{9}$`)
|
||||
|
||||
func NewRepository(db *gorm.DB) *Repository {
|
||||
return &Repository{db: db}
|
||||
}
|
||||
@@ -115,8 +118,18 @@ func (r *Repository) findProduct(ctx context.Context, id uint64) (*ProductDTO, e
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) Redeem(ctx context.Context, userID, productID uint64, quantity int) (*RedemptionDTO, error) {
|
||||
if userID == 0 || productID == 0 || quantity <= 0 || quantity > 10 {
|
||||
func (r *Repository) Redeem(ctx context.Context, userID, productID uint64, req RedeemRequest) (*RedemptionDTO, error) {
|
||||
contactName := strings.TrimSpace(req.ContactName)
|
||||
contactPhone := strings.TrimSpace(req.ContactPhone)
|
||||
customerRemark := strings.TrimSpace(req.CustomerRemark)
|
||||
if userID == 0 ||
|
||||
productID == 0 ||
|
||||
req.Quantity <= 0 ||
|
||||
req.Quantity > 10 ||
|
||||
contactName == "" ||
|
||||
len([]rune(contactName)) > 32 ||
|
||||
!mobilePhonePattern.MatchString(contactPhone) ||
|
||||
len([]rune(customerRemark)) > 500 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
var result model.PointsMallRedemption
|
||||
@@ -125,7 +138,7 @@ func (r *Repository) Redeem(ctx context.Context, userID, productID uint64, quant
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&product, productID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if product.Status != "online" || product.Stock < int64(quantity) {
|
||||
if product.Status != "online" || product.Stock < int64(req.Quantity) {
|
||||
return ErrProductUnavailable
|
||||
}
|
||||
var redeemed int64
|
||||
@@ -135,14 +148,14 @@ func (r *Repository) Redeem(ctx context.Context, userID, productID uint64, quant
|
||||
Scan(&redeemed).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if redeemed+int64(quantity) > int64(product.PerUserLimit) {
|
||||
if redeemed+int64(req.Quantity) > int64(product.PerUserLimit) {
|
||||
return ErrRedeemLimitExceeded
|
||||
}
|
||||
var user model.User
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
totalPoints := product.PointsPrice * int64(quantity)
|
||||
totalPoints := product.PointsPrice * int64(req.Quantity)
|
||||
if totalPoints <= 0 || user.PointsAvailable < totalPoints {
|
||||
return ErrInsufficientPoints
|
||||
}
|
||||
@@ -154,8 +167,11 @@ func (r *Repository) Redeem(ctx context.Context, userID, productID uint64, quant
|
||||
ProductTitle: product.Title,
|
||||
ProductCoverURL: product.CoverURL,
|
||||
RedemptionInstructions: product.RedemptionInstructions,
|
||||
ContactName: contactName,
|
||||
ContactPhone: contactPhone,
|
||||
CustomerRemark: customerRemark,
|
||||
PointsPrice: product.PointsPrice,
|
||||
Quantity: quantity,
|
||||
Quantity: req.Quantity,
|
||||
TotalPoints: totalPoints,
|
||||
Status: "pending",
|
||||
CreatedAt: now,
|
||||
@@ -170,8 +186,8 @@ func (r *Repository) Redeem(ctx context.Context, userID, productID uint64, quant
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&product).Updates(map[string]any{
|
||||
"stock": gorm.Expr("stock - ?", quantity),
|
||||
"redeemed_count": gorm.Expr("redeemed_count + ?", quantity),
|
||||
"stock": gorm.Expr("stock - ?", req.Quantity),
|
||||
"redeemed_count": gorm.Expr("redeemed_count + ?", req.Quantity),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -27,13 +27,29 @@ func TestRedeemDeductsAvailablePointsAndStock(t *testing.T) {
|
||||
t.Fatalf("create product failed: %v", err)
|
||||
}
|
||||
repo := NewRepository(db)
|
||||
redemption, err := repo.Redeem(t.Context(), user.ID, product.ID, 2)
|
||||
req := RedeemRequest{
|
||||
Quantity: 2,
|
||||
ContactName: "张三",
|
||||
ContactPhone: "13800138000",
|
||||
CustomerRemark: "请发送到邮箱",
|
||||
}
|
||||
invalidReq := req
|
||||
invalidReq.ContactPhone = "123"
|
||||
if _, err := repo.Redeem(t.Context(), user.ID, product.ID, invalidReq); !errors.Is(err, ErrInvalidInput) {
|
||||
t.Fatalf("invalid contact phone error = %v, want ErrInvalidInput", err)
|
||||
}
|
||||
redemption, err := repo.Redeem(t.Context(), user.ID, product.ID, req)
|
||||
if err != nil {
|
||||
t.Fatalf("redeem failed: %v", err)
|
||||
}
|
||||
if redemption.TotalPoints != 200 {
|
||||
t.Fatalf("total points = %d, want 200", redemption.TotalPoints)
|
||||
}
|
||||
if redemption.ContactName != req.ContactName ||
|
||||
redemption.ContactPhone != req.ContactPhone ||
|
||||
redemption.CustomerRemark != req.CustomerRemark {
|
||||
t.Fatalf("redemption contact = %+v, want %+v", redemption, req)
|
||||
}
|
||||
var savedUser model.User
|
||||
if err := db.First(&savedUser, user.ID).Error; err != nil {
|
||||
t.Fatalf("find user failed: %v", err)
|
||||
@@ -48,7 +64,8 @@ func TestRedeemDeductsAvailablePointsAndStock(t *testing.T) {
|
||||
if savedProduct.Stock != 0 || savedProduct.RedeemedCount != 2 {
|
||||
t.Fatalf("product stock/redeemed = %d/%d", savedProduct.Stock, savedProduct.RedeemedCount)
|
||||
}
|
||||
if _, err := repo.Redeem(t.Context(), user.ID, product.ID, 1); !errors.Is(err, ErrRedeemLimitExceeded) && !errors.Is(err, ErrProductUnavailable) {
|
||||
req.Quantity = 1
|
||||
if _, err := repo.Redeem(t.Context(), user.ID, product.ID, req); !errors.Is(err, ErrRedeemLimitExceeded) && !errors.Is(err, ErrProductUnavailable) {
|
||||
t.Fatalf("repeat redeem error = %v, want limit or unavailable", err)
|
||||
}
|
||||
completed, err := repo.CompleteRedemption(t.Context(), 99, redemption.ID, "已发放")
|
||||
|
||||
@@ -34,8 +34,8 @@ func (s *Service) UpdateProduct(ctx context.Context, id uint64, req ProductReque
|
||||
return s.repo.UpdateProduct(ctx, id, req)
|
||||
}
|
||||
|
||||
func (s *Service) Redeem(ctx context.Context, userID, productID uint64, quantity int) (*RedemptionDTO, error) {
|
||||
return s.repo.Redeem(ctx, userID, productID, quantity)
|
||||
func (s *Service) Redeem(ctx context.Context, userID, productID uint64, req RedeemRequest) (*RedemptionDTO, error) {
|
||||
return s.repo.Redeem(ctx, userID, productID, req)
|
||||
}
|
||||
|
||||
func (s *Service) ListRedemptions(ctx context.Context, userID uint64, admin bool) ([]RedemptionDTO, error) {
|
||||
|
||||
Reference in New Issue
Block a user