feat: 添加仅已购用户筛选功能
This commit is contained in:
@@ -4,5 +4,7 @@ import "quyun/v2/app/requests"
|
|||||||
|
|
||||||
type UserListQuery struct {
|
type UserListQuery struct {
|
||||||
*requests.Pagination
|
*requests.Pagination
|
||||||
Keyword *string `query:"keyword"`
|
Keyword *string `query:"keyword"` // 关键词(模糊匹配手机号/用户名;若为数字且>0,则同时按用户 ID 精确匹配)
|
||||||
|
|
||||||
|
OnlyBought *bool `query:"onlyBought"` // 是否仅返回“购买数量>0”的用户(true=仅已购用户;false/空=全部用户)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,16 +50,57 @@ func (m *users) List(
|
|||||||
|
|
||||||
query = query.Order(tbl.ID.Desc())
|
query = query.Order(tbl.ID.Desc())
|
||||||
|
|
||||||
|
keyword := ""
|
||||||
if filter.Keyword != nil && *filter.Keyword != "" {
|
if filter.Keyword != nil && *filter.Keyword != "" {
|
||||||
|
keyword = strings.TrimSpace(*filter.Keyword)
|
||||||
query = query.
|
query = query.
|
||||||
Where(tbl.Phone.Like(database.WrapLike(*filter.Keyword))).
|
Where(tbl.Phone.Like(database.WrapLike(keyword))).
|
||||||
Or(tbl.Username.Like(database.WrapLike(*filter.Keyword)))
|
Or(tbl.Username.Like(database.WrapLike(keyword)))
|
||||||
|
|
||||||
if id, err := strconv.ParseInt(strings.TrimSpace(*filter.Keyword), 10, 64); err == nil && id > 0 {
|
if id, err := strconv.ParseInt(keyword, 10, 64); err == nil && id > 0 {
|
||||||
query = query.Or(tbl.ID.Eq(id))
|
query = query.Or(tbl.ID.Eq(id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if filter.OnlyBought != nil && *filter.OnlyBought {
|
||||||
|
// 仅返回“购买数量>0”的用户:通过 JOIN user_posts 做存在性过滤。
|
||||||
|
// 注意:FindByPage 内部用 Count(),在 GROUP BY 场景下可能不准确,这里改为手动 Count(DISTINCT users.id)。
|
||||||
|
tblUserPost, _ := models.UserPostQuery.QueryContext(ctx)
|
||||||
|
query = query.Join(tblUserPost, tbl.ID.EqCol(tblUserPost.UserID)).Group(tbl.ID)
|
||||||
|
|
||||||
|
offset := int(filter.Pagination.Offset())
|
||||||
|
limit := int(filter.Pagination.Limit)
|
||||||
|
|
||||||
|
items, err := query.Offset(offset).Limit(limit).Find()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "query users error")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := _db.WithContext(ctx).Model(&models.User{}).
|
||||||
|
Joins("JOIN user_posts ON user_posts.user_id = users.id")
|
||||||
|
if keyword != "" {
|
||||||
|
like := database.WrapLike(keyword)
|
||||||
|
args := []any{like, like}
|
||||||
|
where := "(users.phone LIKE ? OR users.username LIKE ?)"
|
||||||
|
if id, err := strconv.ParseInt(keyword, 10, 64); err == nil && id > 0 {
|
||||||
|
where = "(users.phone LIKE ? OR users.username LIKE ? OR users.id = ?)"
|
||||||
|
args = append(args, id)
|
||||||
|
}
|
||||||
|
db = db.Where(where, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cnt int64
|
||||||
|
if err := db.Distinct("users.id").Count(&cnt).Error; err != nil {
|
||||||
|
return nil, errors.Wrap(err, "count users error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &requests.Pager{
|
||||||
|
Items: items,
|
||||||
|
Total: cnt,
|
||||||
|
Pagination: *filter.Pagination,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
items, cnt, err := query.FindByPage(int(filter.Pagination.Offset()), int(filter.Pagination.Limit))
|
items, cnt, err := query.FindByPage(int(filter.Pagination.Offset()), int(filter.Pagination.Limit))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "query users error")
|
return nil, errors.Wrap(err, "query users error")
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import httpClient from './httpClient';
|
import httpClient from './httpClient';
|
||||||
|
|
||||||
export const userService = {
|
export const userService = {
|
||||||
getUsers({ page = 1, limit = 10, keyword = '' } = {}) {
|
getUsers({ page = 1, limit = 10, keyword = '', onlyBought = false } = {}) {
|
||||||
return httpClient.get('/users', {
|
return httpClient.get('/users', {
|
||||||
params: {
|
params: {
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
keyword: keyword.trim()
|
keyword: keyword.trim(),
|
||||||
|
onlyBought
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Column from 'primevue/column';
|
|||||||
import ConfirmDialog from 'primevue/confirmdialog';
|
import ConfirmDialog from 'primevue/confirmdialog';
|
||||||
import DataTable from 'primevue/datatable';
|
import DataTable from 'primevue/datatable';
|
||||||
import Dialog from 'primevue/dialog';
|
import Dialog from 'primevue/dialog';
|
||||||
|
import Dropdown from 'primevue/dropdown';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
import ProgressSpinner from 'primevue/progressspinner';
|
import ProgressSpinner from 'primevue/progressspinner';
|
||||||
import Toast from 'primevue/toast';
|
import Toast from 'primevue/toast';
|
||||||
@@ -25,6 +26,12 @@ const filters = ref({
|
|||||||
status: { value: null, matchMode: 'equals' }
|
status: { value: null, matchMode: 'equals' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const boughtFilterOptions = ref([
|
||||||
|
{ name: '全部用户', value: false },
|
||||||
|
{ name: '仅已购用户', value: true }
|
||||||
|
]);
|
||||||
|
const onlyBought = ref(false);
|
||||||
|
|
||||||
// Table state
|
// Table state
|
||||||
const users = ref({
|
const users = ref({
|
||||||
items: [],
|
items: [],
|
||||||
@@ -58,7 +65,8 @@ const fetchUsers = async () => {
|
|||||||
const response = await userService.getUsers({
|
const response = await userService.getUsers({
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
limit: rows.value,
|
limit: rows.value,
|
||||||
keyword: globalFilterValue.value
|
keyword: globalFilterValue.value,
|
||||||
|
onlyBought: onlyBought.value
|
||||||
});
|
});
|
||||||
users.value = response.data;
|
users.value = response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -188,6 +196,11 @@ const savePhone = async () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onOnlyBoughtChange = () => {
|
||||||
|
first.value = 0;
|
||||||
|
fetchUsers();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -263,7 +276,9 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="pb-10 flex">
|
<div class="pb-10 flex gap-3 items-center">
|
||||||
|
<Dropdown v-model="onlyBought" :options="boughtFilterOptions" optionLabel="name" optionValue="value"
|
||||||
|
class="w-48" @change="onOnlyBoughtChange" />
|
||||||
<InputText v-model="globalFilterValue" placeholder="搜索用户..." class="flex-1" @input="onSearch" />
|
<InputText v-model="globalFilterValue" placeholder="搜索用户..." class="flex-1" @input="onSearch" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user