fix(invitation): 修复邀请关系手机号与邀请码筛选并预加载统计
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
package invitation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
)
|
||||
|
||||
func TestListAdminInvitationsFiltersPhoneAndInviteCode(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
users := []model.User{
|
||||
{Phone: "13812345678", Nickname: "邀请人甲", Status: "active", RealnameStatus: "verified"},
|
||||
{Phone: "13911112222", Nickname: "被邀请人甲", Status: "active", RealnameStatus: "verified"},
|
||||
{Phone: "13700001111", Nickname: "邀请人乙", Status: "active", RealnameStatus: "verified"},
|
||||
{Phone: "13622223333", Nickname: "被邀请人乙", Status: "active", RealnameStatus: "verified"},
|
||||
}
|
||||
for i := range users {
|
||||
if err := db.Create(&users[i]).Error; err != nil {
|
||||
t.Fatalf("创建测试用户失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
records := []model.UserInvitation{
|
||||
{
|
||||
InviterID: users[0].ID,
|
||||
InviteeID: users[1].ID,
|
||||
InviteCode: "alpha01",
|
||||
Status: StatusActive,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
InviterID: users[2].ID,
|
||||
InviteeID: users[3].ID,
|
||||
InviteCode: "beta02",
|
||||
Status: StatusActive,
|
||||
CreatedAt: now,
|
||||
},
|
||||
}
|
||||
for i := range records {
|
||||
if err := db.Create(&records[i]).Error; err != nil {
|
||||
t.Fatalf("创建邀请关系失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
query AdminInvitationQuery
|
||||
want int64
|
||||
}{
|
||||
{name: "通用手机号匹配邀请人", query: AdminInvitationQuery{Phone: "1234"}, want: 1},
|
||||
{name: "通用手机号匹配被邀请人", query: AdminInvitationQuery{Phone: "139111"}, want: 1},
|
||||
{name: "邀请码模糊匹配", query: AdminInvitationQuery{InviteCode: "beta"}, want: 1},
|
||||
{name: "邀请人手机号模糊匹配", query: AdminInvitationQuery{InviterPhone: "137"}, want: 1},
|
||||
{name: "被邀请人手机号模糊匹配", query: AdminInvitationQuery{InviteePhone: "1362"}, want: 1},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.query.Page = 1
|
||||
tc.query.PageSize = 20
|
||||
_, total, err := repo.ListAdminInvitations(ctx, tc.query)
|
||||
if err != nil {
|
||||
t.Fatalf("查询邀请关系失败: %v", err)
|
||||
}
|
||||
if total != tc.want {
|
||||
t.Fatalf("查询结果数量 = %d, want %d", total, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAdminRebateLedgersFiltersPartialPhoneAndOrderNo(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
users := []model.User{
|
||||
{Phone: "13812345678", Nickname: "邀请人甲", Status: "active", RealnameStatus: "verified"},
|
||||
{Phone: "13911112222", Nickname: "被邀请人甲", Status: "active", RealnameStatus: "verified"},
|
||||
{Phone: "13700001111", Nickname: "邀请人乙", Status: "active", RealnameStatus: "verified"},
|
||||
{Phone: "13622223333", Nickname: "被邀请人乙", Status: "active", RealnameStatus: "verified"},
|
||||
}
|
||||
for i := range users {
|
||||
if err := db.Create(&users[i]).Error; err != nil {
|
||||
t.Fatalf("创建测试用户失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
ledgers := []model.InvitationRebateLedger{
|
||||
{
|
||||
OrderID: 1001,
|
||||
OrderNo: "ORDER-ALPHA-1001",
|
||||
Source: "rental_order",
|
||||
InviterID: users[0].ID,
|
||||
InviteeID: users[1].ID,
|
||||
TradeRole: "buyer",
|
||||
RateApplied: 0.6,
|
||||
CycleStage: CycleFirstMonth,
|
||||
Status: "settled",
|
||||
RebateAmountCent: 100,
|
||||
SettledAt: now,
|
||||
},
|
||||
{
|
||||
OrderID: 1002,
|
||||
OrderNo: "ORDER-BETA-1002",
|
||||
Source: "pickup",
|
||||
InviterID: users[2].ID,
|
||||
InviteeID: users[3].ID,
|
||||
TradeRole: "seller",
|
||||
RateApplied: 0.3,
|
||||
CycleStage: CycleSecondMonth,
|
||||
Status: "settled",
|
||||
RebateAmountCent: 200,
|
||||
SettledAt: now,
|
||||
},
|
||||
}
|
||||
for i := range ledgers {
|
||||
if err := db.Create(&ledgers[i]).Error; err != nil {
|
||||
t.Fatalf("创建返利流水失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
query AdminRebateLedgerQuery
|
||||
want int64
|
||||
}{
|
||||
{name: "订单号模糊匹配", query: AdminRebateLedgerQuery{OrderNo: "ALPHA"}, want: 1},
|
||||
{name: "邀请人手机号模糊匹配", query: AdminRebateLedgerQuery{InviterPhone: "1381"}, want: 1},
|
||||
{name: "被邀请人手机号模糊匹配", query: AdminRebateLedgerQuery{InviteePhone: "1362"}, want: 1},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.query.Page = 1
|
||||
tc.query.PageSize = 20
|
||||
_, total, err := repo.ListAdminRebateLedgers(ctx, tc.query)
|
||||
if err != nil {
|
||||
t.Fatalf("查询返利流水失败: %v", err)
|
||||
}
|
||||
if total != tc.want {
|
||||
t.Fatalf("查询结果数量 = %d, want %d", total, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -76,8 +76,11 @@ type UpdateConfigRequest struct {
|
||||
|
||||
// AdminInvitationQuery 后台查询邀请关系
|
||||
type AdminInvitationQuery struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"page_size,default=20"`
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"page_size,default=20"`
|
||||
// Phone 为通用手机号搜索:同时匹配邀请人/被邀请人,支持模糊查询。
|
||||
Phone string `form:"phone"`
|
||||
InviteCode string `form:"invite_code"`
|
||||
InviterPhone string `form:"inviter_phone"`
|
||||
InviteePhone string `form:"invitee_phone"`
|
||||
Status string `form:"status"`
|
||||
|
||||
@@ -845,11 +845,18 @@ func (r *Repository) ListAdminInvitations(ctx context.Context, query AdminInvita
|
||||
Joins("LEFT JOIN users inviter ON inviter.id = ui.inviter_id").
|
||||
Joins("LEFT JOIN users invitee ON invitee.id = ui.invitee_id")
|
||||
|
||||
if p := strings.TrimSpace(query.Phone); p != "" {
|
||||
like := "%" + p + "%"
|
||||
db = db.Where("(inviter.phone LIKE ? OR invitee.phone LIKE ?)", like, like)
|
||||
}
|
||||
if p := strings.TrimSpace(query.InviterPhone); p != "" {
|
||||
db = db.Where("inviter.phone = ?", p)
|
||||
db = db.Where("inviter.phone LIKE ?", "%"+p+"%")
|
||||
}
|
||||
if p := strings.TrimSpace(query.InviteePhone); p != "" {
|
||||
db = db.Where("invitee.phone = ?", p)
|
||||
db = db.Where("invitee.phone LIKE ?", "%"+p+"%")
|
||||
}
|
||||
if code := strings.TrimSpace(query.InviteCode); code != "" {
|
||||
db = db.Where("ui.invite_code LIKE ?", "%"+code+"%")
|
||||
}
|
||||
if s := strings.TrimSpace(query.Status); s != "" {
|
||||
db = db.Where("ui.status = ?", s)
|
||||
@@ -928,13 +935,13 @@ func (r *Repository) ListAdminRebateLedgers(ctx context.Context, query AdminReba
|
||||
Joins("LEFT JOIN users invitee ON invitee.id = irl.invitee_id")
|
||||
|
||||
if o := strings.TrimSpace(query.OrderNo); o != "" {
|
||||
db = db.Where("irl.order_no = ?", o)
|
||||
db = db.Where("irl.order_no LIKE ?", "%"+o+"%")
|
||||
}
|
||||
if p := strings.TrimSpace(query.InviterPhone); p != "" {
|
||||
db = db.Where("inviter.phone = ?", p)
|
||||
db = db.Where("inviter.phone LIKE ?", "%"+p+"%")
|
||||
}
|
||||
if p := strings.TrimSpace(query.InviteePhone); p != "" {
|
||||
db = db.Where("invitee.phone = ?", p)
|
||||
db = db.Where("invitee.phone LIKE ?", "%"+p+"%")
|
||||
}
|
||||
if t := strings.TrimSpace(query.TradeRole); t != "" {
|
||||
db = db.Where("irl.trade_role = ?", t)
|
||||
|
||||
@@ -42,6 +42,13 @@ export interface AdminRebateRow {
|
||||
}
|
||||
|
||||
export interface AdminInvitationQuery {
|
||||
phone?: string
|
||||
invite_code?: string
|
||||
inviter_phone?: string
|
||||
invitee_phone?: string
|
||||
order_no?: string
|
||||
trade_role?: string
|
||||
distribution_site_id?: number
|
||||
inviter_id?: number
|
||||
invitee_id?: number
|
||||
page?: number
|
||||
|
||||
@@ -55,9 +55,9 @@ const rebateQuery = reactive({
|
||||
const rebates = ref<any[]>([])
|
||||
const rebatesTotal = ref(0)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSites()
|
||||
await loadConfig()
|
||||
onMounted(() => {
|
||||
// 三个页签的数据一起预加载,避免邀请关系/返利流水标签在首次进入时显示 0。
|
||||
void Promise.all([loadSites(), loadConfig(), loadInvitations(), loadRebates()])
|
||||
})
|
||||
|
||||
async function loadSites() {
|
||||
@@ -169,6 +169,16 @@ function handleTabChange(tab: 'config' | 'invitations' | 'rebates') {
|
||||
}
|
||||
}
|
||||
|
||||
function searchInvitations() {
|
||||
invitationQuery.page = 1
|
||||
void loadInvitations()
|
||||
}
|
||||
|
||||
function searchRebates() {
|
||||
rebateQuery.page = 1
|
||||
void loadRebates()
|
||||
}
|
||||
|
||||
function cycleLabel(cycle: string) {
|
||||
switch (cycle) {
|
||||
case 'first_month':
|
||||
@@ -295,20 +305,20 @@ function cycleLabel(cycle: string) {
|
||||
<div class="filter-bar">
|
||||
<el-input
|
||||
v-model="invitationQuery.phone"
|
||||
placeholder="搜索手机号"
|
||||
placeholder="邀请人/被邀请人手机号"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@keyup.enter="loadInvitations"
|
||||
style="width: 210px"
|
||||
@keyup.enter="searchInvitations"
|
||||
/>
|
||||
<el-input
|
||||
v-model="invitationQuery.invite_code"
|
||||
placeholder="搜索邀请码"
|
||||
placeholder="邀请码(支持模糊搜索)"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
@keyup.enter="loadInvitations"
|
||||
style="width: 180px"
|
||||
@keyup.enter="searchInvitations"
|
||||
/>
|
||||
<el-button type="primary" :icon="Search" @click="loadInvitations">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="loadInvitations">刷新</el-button>
|
||||
<el-button type="primary" :icon="Search" @click="searchInvitations">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="searchInvitations">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
@@ -374,24 +384,24 @@ function cycleLabel(cycle: string) {
|
||||
placeholder="订单号"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@keyup.enter="loadRebates"
|
||||
@keyup.enter="searchRebates"
|
||||
/>
|
||||
<el-input
|
||||
v-model="rebateQuery.inviter_phone"
|
||||
placeholder="邀请人手机号"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
@keyup.enter="loadRebates"
|
||||
@keyup.enter="searchRebates"
|
||||
/>
|
||||
<el-input
|
||||
v-model="rebateQuery.invitee_phone"
|
||||
placeholder="被邀请人手机号"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
@keyup.enter="loadRebates"
|
||||
@keyup.enter="searchRebates"
|
||||
/>
|
||||
<el-button type="primary" :icon="Search" @click="loadRebates">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="loadRebates">刷新</el-button>
|
||||
<el-button type="primary" :icon="Search" @click="searchRebates">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="searchRebates">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
|
||||
Reference in New Issue
Block a user