feat: 添加仅已购用户筛选功能

This commit is contained in:
2025-12-20 23:30:51 +08:00
parent 257c9a286a
commit 661a595fa7
4 changed files with 67 additions and 8 deletions

View File

@@ -4,5 +4,7 @@ import "quyun/v2/app/requests"
type UserListQuery struct {
*requests.Pagination
Keyword *string `query:"keyword"`
Keyword *string `query:"keyword"` // 关键词(模糊匹配手机号/用户名;若为数字且>0则同时按用户 ID 精确匹配)
OnlyBought *bool `query:"onlyBought"` // 是否仅返回“购买数量>0”的用户true=仅已购用户false/空=全部用户)
}

View File

@@ -50,16 +50,57 @@ func (m *users) List(
query = query.Order(tbl.ID.Desc())
keyword := ""
if filter.Keyword != nil && *filter.Keyword != "" {
keyword = strings.TrimSpace(*filter.Keyword)
query = query.
Where(tbl.Phone.Like(database.WrapLike(*filter.Keyword))).
Or(tbl.Username.Like(database.WrapLike(*filter.Keyword)))
Where(tbl.Phone.Like(database.WrapLike(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))
}
}
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))
if err != nil {
return nil, errors.Wrap(err, "query users error")

View File

@@ -1,12 +1,13 @@
import httpClient from './httpClient';
export const userService = {
getUsers({ page = 1, limit = 10, keyword = '' } = {}) {
getUsers({ page = 1, limit = 10, keyword = '', onlyBought = false } = {}) {
return httpClient.get('/users', {
params: {
page,
limit,
keyword: keyword.trim()
keyword: keyword.trim(),
onlyBought
}
});
},

View File

@@ -7,6 +7,7 @@ import Column from 'primevue/column';
import ConfirmDialog from 'primevue/confirmdialog';
import DataTable from 'primevue/datatable';
import Dialog from 'primevue/dialog';
import Dropdown from 'primevue/dropdown';
import InputText from 'primevue/inputtext';
import ProgressSpinner from 'primevue/progressspinner';
import Toast from 'primevue/toast';
@@ -25,6 +26,12 @@ const filters = ref({
status: { value: null, matchMode: 'equals' }
});
const boughtFilterOptions = ref([
{ name: '全部用户', value: false },
{ name: '仅已购用户', value: true }
]);
const onlyBought = ref(false);
// Table state
const users = ref({
items: [],
@@ -58,7 +65,8 @@ const fetchUsers = async () => {
const response = await userService.getUsers({
page: currentPage,
limit: rows.value,
keyword: globalFilterValue.value
keyword: globalFilterValue.value,
onlyBought: onlyBought.value
});
users.value = response.data;
} catch (error) {
@@ -188,6 +196,11 @@ const savePhone = async () => {
onMounted(() => {
fetchUsers();
});
const onOnlyBoughtChange = () => {
first.value = 0;
fetchUsers();
};
</script>
<template>
@@ -263,7 +276,9 @@ onMounted(() => {
</div>
<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" />
</div>