- 新增积分充值申请与审核:商户提交充值申请(凭证图片必填,1元=100积分), 管理员审核通过后事务内自动入账,幂等键防重复 - 新增通用图片上传组件 ImageUploader:canvas 压缩转 webp、预览、删除, 后端按魔数校验图片格式并持久化到 data/uploads - 管理员钱包调整改为按商户维度:移除商户侧调账接口,新增管理员 平台商户积分调整与钱包流水查看 - 低余额 Webhook 告警配置持久化,充值入账/调账后自动检查推送 - 平台商户操作列移除进入/成员,积分充值页接入真实接口并修复金额单位换算
149 lines
4.4 KiB
Go
149 lines
4.4 KiB
Go
package service
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"affiliate_dash/internal/model"
|
|
)
|
|
|
|
func TestCreateRechargeRequiresVoucher(t *testing.T) {
|
|
db := newServiceTestDB(t)
|
|
merchantID, _ := seedFulfillmentMerchant(t, db, "merchant-recharge-novoucher", 0, -1, 100)
|
|
svc := NewRechargeService(db, NewFulfillmentService(db, nil), t.TempDir())
|
|
|
|
if _, err := svc.CreateRecharge(CreateRechargeInput{
|
|
MerchantID: merchantID,
|
|
AmountCNY: 10000,
|
|
}); err == nil || !strings.Contains(err.Error(), "凭证") {
|
|
t.Fatalf("empty voucher should be rejected, got %v", err)
|
|
}
|
|
|
|
if _, err := svc.CreateRecharge(CreateRechargeInput{
|
|
MerchantID: merchantID,
|
|
AmountCNY: 100,
|
|
Vouchers: []string{"/uploads/a.png"},
|
|
}); err == nil || !strings.Contains(err.Error(), "10 元") {
|
|
t.Fatalf("amount below minimum should be rejected, got %v", err)
|
|
}
|
|
|
|
app, err := svc.CreateRecharge(CreateRechargeInput{
|
|
MerchantID: merchantID,
|
|
ActorUserID: 7,
|
|
AmountCNY: 50000,
|
|
Vouchers: []string{"/uploads/a.png"},
|
|
Note: "对公转账",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create recharge: %v", err)
|
|
}
|
|
if app.Status != model.RechargeStatusPending || app.PointsAmount != 50000 {
|
|
t.Fatalf("unexpected application: %+v", app)
|
|
}
|
|
}
|
|
|
|
func TestReviewRechargeCreditsWalletOnce(t *testing.T) {
|
|
db := newServiceTestDB(t)
|
|
merchantID, _ := seedFulfillmentMerchant(t, db, "merchant-recharge-review", 0, -1, 100)
|
|
fulfillment := NewFulfillmentService(db, nil)
|
|
svc := NewRechargeService(db, fulfillment, t.TempDir())
|
|
|
|
app, err := svc.CreateRecharge(CreateRechargeInput{
|
|
MerchantID: merchantID,
|
|
ActorUserID: 7,
|
|
AmountCNY: 100000, // 1000 元 = 100000 积分
|
|
Vouchers: []string{"/uploads/a.png"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create recharge: %v", err)
|
|
}
|
|
|
|
reviewed, err := svc.ReviewRecharge(ReviewRechargeInput{
|
|
ApplicationID: app.ID,
|
|
Approved: true,
|
|
ReviewNote: "已核实到账",
|
|
ActorUserID: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("review approve: %v", err)
|
|
}
|
|
if reviewed.Status != model.RechargeStatusApproved || reviewed.PointsAmount != 100000 {
|
|
t.Fatalf("unexpected reviewed app: %+v", reviewed)
|
|
}
|
|
|
|
wallet, err := fulfillment.GetWallet(merchantID)
|
|
if err != nil {
|
|
t.Fatalf("get wallet: %v", err)
|
|
}
|
|
if wallet.AvailableBalance != 100000 {
|
|
t.Fatalf("approved recharge should credit wallet, got %d", wallet.AvailableBalance)
|
|
}
|
|
|
|
// 重复审核应被拒绝且不再入账
|
|
if _, err := svc.ReviewRecharge(ReviewRechargeInput{
|
|
ApplicationID: app.ID,
|
|
Approved: true,
|
|
ActorUserID: 1,
|
|
}); err == nil || !strings.Contains(err.Error(), "已审核") {
|
|
t.Fatalf("repeat review should be rejected, got %v", err)
|
|
}
|
|
wallet, _ = fulfillment.GetWallet(merchantID)
|
|
if wallet.AvailableBalance != 100000 {
|
|
t.Fatalf("repeat review should not credit again, got %d", wallet.AvailableBalance)
|
|
}
|
|
}
|
|
|
|
func TestDetectImageExt(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
data []byte
|
|
want string
|
|
}{
|
|
{"jpeg", []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00}, "jpg"},
|
|
{"png", []byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, "png"},
|
|
{"gif", []byte{'G', 'I', 'F', '8', '9', 'a'}, "gif"},
|
|
{"webp", []byte{'R', 'I', 'F', 'F', 0x00, 0x00, 0x00, 0x00, 'W', 'E', 'B', 'P'}, "webp"},
|
|
{"fake", []byte{'<', 's', 'c', 'r', 'i', 'p', 't', '>'}, ""},
|
|
{"empty", []byte{}, ""},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := detectImageExt(tc.data); got != tc.want {
|
|
t.Fatalf("%s: detectImageExt = %q, want %q", tc.name, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReviewRechargeReject(t *testing.T) {
|
|
db := newServiceTestDB(t)
|
|
merchantID, _ := seedFulfillmentMerchant(t, db, "merchant-recharge-reject", 0, -1, 100)
|
|
fulfillment := NewFulfillmentService(db, nil)
|
|
svc := NewRechargeService(db, fulfillment, t.TempDir())
|
|
|
|
app, err := svc.CreateRecharge(CreateRechargeInput{
|
|
MerchantID: merchantID,
|
|
ActorUserID: 7,
|
|
AmountCNY: 10000,
|
|
Vouchers: []string{"/uploads/a.png"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create recharge: %v", err)
|
|
}
|
|
|
|
reviewed, err := svc.ReviewRecharge(ReviewRechargeInput{
|
|
ApplicationID: app.ID,
|
|
Approved: false,
|
|
ReviewNote: "凭证不清晰",
|
|
ActorUserID: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("review reject: %v", err)
|
|
}
|
|
if reviewed.Status != model.RechargeStatusRejected {
|
|
t.Fatalf("expected rejected, got %+v", reviewed)
|
|
}
|
|
wallet, _ := fulfillment.GetWallet(merchantID)
|
|
if wallet.AvailableBalance != 0 {
|
|
t.Fatalf("rejected recharge should not credit, got %d", wallet.AvailableBalance)
|
|
}
|
|
}
|