feat:会员管理增加搜索条件

This commit is contained in:
dzc committed 2026-10-29 18:46:19 +08:00
1 parent 9811d336e0
commit e04e1ca08b
8 files changed
+229 -17

No files matched your search

@@ -35,6 +35,9 @@ public class MemberUserPageReqVO extends PageParam {
@Schema(description = "会员标签编号列表", example = "[1, 2]")
private List<Long> tagIds;
@Schema(description = "用户标签类型列表,对应 member_recent_tag 字典,匹配任一所选标签", example = "[\"1\", \"4\"]")
private List<String> memberRecentTagList;
@Schema(description = "会员等级编号", example = "1")
private Long levelId;
@@ -44,9 +47,12 @@ public class MemberUserPageReqVO extends PageParam {
@Schema(description = "用户 Code(App 我的页面展示的 ID)", example = "924199")
private String userCode;
@Schema(description = "性别:1 男 2 女", example = "1")
@Schema(description = "性别:0 未知,1 男,2 女", example = "1")
private Integer sex;
@Schema(description = "是否禁用:0 未禁用,1 已禁用", example = "1")
private Integer status;
// TODO 芋艿:注册用户类型;
// TODO 芋艿:登录用户类型;
@@ -8,12 +8,14 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* 会员 User Mapper
@@ -82,10 +84,10 @@ public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
if (CollUtil.isNotEmpty(reqVO.getTagIds())) {
tagIdSql = reqVO.getTagIds().stream()
.map(tagId -> "FIND_IN_SET(" + tagId + ", tag_ids)")
.collect(Collectors.joining(" OR "));
.collect(Collectors.joining(" OR ", "(", ")"));
}
// 分页查询
return selectPage(reqVO, new LambdaQueryWrapperX<MemberUserDO>()
LambdaQueryWrapper<MemberUserDO> query = new LambdaQueryWrapperX<MemberUserDO>()
.likeIfPresent(MemberUserDO::getMobile, reqVO.getMobile())
.betweenIfPresent(MemberUserDO::getLoginDate, reqVO.getLoginDate())
.likeIfPresent(MemberUserDO::getNickname, reqVO.getNickname())
@@ -94,8 +96,19 @@ public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
.eqIfPresent(MemberUserDO::getGroupId, reqVO.getGroupId())
.eqIfPresent(MemberUserDO::getUserCode, reqVO.getUserCode())
.eqIfPresent(MemberUserDO::getSex, reqVO.getSex())
.eqIfPresent(MemberUserDO::getStatus, reqVO.getStatus())
.apply(StrUtil.isNotEmpty(tagIdSql), tagIdSql)
.orderByDesc(MemberUserDO::getId));
.orderByDesc(MemberUserDO::getId);
if (CollUtil.isNotEmpty(reqVO.getMemberRecentTagList())) {
String placeholders = IntStream.range(0, reqVO.getMemberRecentTagList().size())
.mapToObj(index -> "{" + index + "}")
.collect(Collectors.joining(", "));
// 与列表展示的用户标签使用同一张表;EXISTS 避免多标签或重复标签影响分页总数。
query.exists("SELECT 1 FROM member_recent_tag tag WHERE tag.member_id = member_user.id"
+ " AND tag.deleted = 0 AND tag.tag_biz_type IN (" + placeholders + ")",
reqVO.getMemberRecentTagList().toArray());
}
return selectPage(reqVO, query);
}
default Long selectCountByGroupId(Long groupId) {
@@ -0,0 +1,179 @@
package cn.iocoder.yudao.module.member.dal.mysql.user;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.web.bind.ServletRequestDataBinder;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:h2:mem:member_user_page_filters;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value;",
"spring.sql.init.schema-locations=classpath:/sql/create_tables.sql,classpath:/sql/member_user_page_filter_schema.sql"
})
class MemberUserMapperTest extends BaseDbUnitTest {
@Resource
private MemberUserMapper memberUserMapper;
@Resource
private DataSource dataSource;
private JdbcTemplate jdbcTemplate;
@BeforeEach
void setUpUsersAndTags() {
jdbcTemplate = new JdbcTemplate(dataSource);
int[] sexes = {1, 2, 1, 1, 1, 2, 0, 1};
for (int index = 0; index < sexes.length; index++) {
long id = index + 1L;
jdbcTemplate.update("INSERT INTO member_user"
+ " (id, user_code, nickname, sex, status, mobile, register_ip, tag_ids, deleted)"
+ " VALUES (?, ?, ?, ?, ?, ?, '127.0.0.1', ?, ?)",
id, String.format("%06d", id), "会员" + id, sexes[index],
id == 2 || id == 5 ? 1 : 0, "1390000000" + id, id == 4 ? "4" : null, id == 8);
}
insertTag(1L, "4", false);
insertTag(1L, "4", false); // 历史重复标签不能重复计算用户。
insertTag(1L, "1", false);
insertTag(2L, "4", false);
insertTag(3L, "5", false);
insertTag(5L, "4", true); // 已删除的标签不参与筛选。
insertTag(6L, "5", false);
insertTag(8L, "4", false); // 已删除的用户不参与筛选。
}
@AfterEach
void cleanRecentTags() {
jdbcTemplate.update("DELETE FROM member_recent_tag");
}
@Test
void selectPageWithoutFiltersIncludesUntaggedUsers() {
assertPage(memberUserMapper.selectPage(new MemberUserPageReqVO()), 7L,
7L, 6L, 5L, 4L, 3L, 2L, 1L);
}
@Test
void selectPageBySex() {
MemberUserPageReqVO request = new MemberUserPageReqVO();
request.setSex(1);
assertPage(memberUserMapper.selectPage(request), 4L, 5L, 4L, 3L, 1L);
}
@Test
void selectPageByUnknownSexDoesNotTreatZeroAsEmpty() {
MemberUserPageReqVO request = new MemberUserPageReqVO();
request.setSex(0);
assertPage(memberUserMapper.selectPage(request), 1L, 7L);
}
@Test
void selectPageByDisabledStatus() {
MemberUserPageReqVO request = new MemberUserPageReqVO();
request.setStatus(1);
assertPage(memberUserMapper.selectPage(request), 2L, 5L, 2L);
}
@Test
void selectPageByEnabledStatusDoesNotTreatZeroAsEmpty() {
MemberUserPageReqVO request = new MemberUserPageReqVO();
request.setStatus(0);
assertPage(memberUserMapper.selectPage(request), 5L, 7L, 6L, 4L, 3L, 1L);
}
@Test
void selectPageCombinesStatusSexAndRecentTags() {
MemberUserPageReqVO request = new MemberUserPageReqVO();
request.setSex(2);
request.setStatus(1);
request.setMemberRecentTagList(Arrays.asList("4", "5"));
assertPage(memberUserMapper.selectPage(request), 1L, 2L);
}
@Test
void selectPageByRecentTagIgnoresLegacyAndDeletedTags() {
MemberUserPageReqVO request = new MemberUserPageReqVO();
request.setMemberRecentTagList(Collections.singletonList("4"));
assertPage(memberUserMapper.selectPage(request), 2L, 2L, 1L);
}
@Test
void selectPageBySexAndAnySelectedTag() {
MemberUserPageReqVO request = new MemberUserPageReqVO();
request.setSex(1);
request.setMemberRecentTagList(Arrays.asList("4", "5"));
assertPage(memberUserMapper.selectPage(request), 2L, 3L, 1L);
}
@Test
void selectPageCombinesRecentTagsWithExistingFilters() {
MemberUserPageReqVO request = new MemberUserPageReqVO();
request.setSex(1);
request.setMobile("13900000001");
request.setMemberRecentTagList(Arrays.asList("4", "5"));
assertPage(memberUserMapper.selectPage(request), 1L, 1L);
}
@Test
void selectPageDoesNotDuplicateUsersOrInflateTotal() {
MemberUserPageReqVO request = new MemberUserPageReqVO();
request.setMemberRecentTagList(Arrays.asList("1", "4"));
request.setPageSize(1);
assertPage(memberUserMapper.selectPage(request), 2L, 2L);
request.setPageNo(2);
assertPage(memberUserMapper.selectPage(request), 2L, 1L);
}
@Test
void selectPageWithEmptyTagListDoesNotFilterUsers() {
MemberUserPageReqVO request = new MemberUserPageReqVO();
request.setMemberRecentTagList(Collections.emptyList());
assertPage(memberUserMapper.selectPage(request), 7L, 7L, 6L, 5L, 4L, 3L, 2L, 1L);
}
@Test
void selectPageTreatsUnknownAndSpecialTagValuesAsParameters() {
MemberUserPageReqVO request = new MemberUserPageReqVO();
request.setMemberRecentTagList(Arrays.asList("missing", "4') OR 1=1 --"));
assertPage(memberUserMapper.selectPage(request), 0L);
}
@Test
void bindIndexedTagParametersFromFrontendAndFilterPage() {
MockHttpServletRequest servletRequest = new MockHttpServletRequest();
servletRequest.addParameter("sex", "2");
servletRequest.addParameter("status", "1");
servletRequest.addParameter("memberRecentTagList[0]", "4");
servletRequest.addParameter("memberRecentTagList[1]", "5");
MemberUserPageReqVO request = new MemberUserPageReqVO();
ServletRequestDataBinder binder = new ServletRequestDataBinder(request);
binder.bind(servletRequest);
assertThat(binder.getBindingResult().hasErrors()).isFalse();
assertThat(request.getMemberRecentTagList()).containsExactly("4", "5");
assertPage(memberUserMapper.selectPage(request), 1L, 2L);
}
private void insertTag(Long memberId, String type, boolean deleted) {
jdbcTemplate.update("INSERT INTO member_recent_tag (member_id, tag_biz_type, deleted) VALUES (?, ?, ?)",
memberId, type, deleted);
}
private void assertPage(PageResult<MemberUserDO> result, long total, Long... ids) {
assertThat(result.getTotal()).isEqualTo(total);
assertThat(result.getList()).extracting(MemberUserDO::getId).containsExactly(ids);
}
}
@@ -0,0 +1,21 @@
ALTER TABLE member_user ADD COLUMN IF NOT EXISTS register_terminal int;
ALTER TABLE member_user ADD COLUMN IF NOT EXISTS app_refresh_date datetime;
ALTER TABLE member_user ADD COLUMN IF NOT EXISTS occ varchar(64);
ALTER TABLE member_user ADD COLUMN IF NOT EXISTS height double;
ALTER TABLE member_user ADD COLUMN IF NOT EXISTS weight varchar(32);
ALTER TABLE member_user ADD COLUMN IF NOT EXISTS description varchar(255);
ALTER TABLE member_user ADD COLUMN IF NOT EXISTS wechat_num varchar(64);
ALTER TABLE member_user ADD COLUMN IF NOT EXISTS wechat_qr_code varchar(255);
ALTER TABLE member_user ADD COLUMN IF NOT EXISTS lng double;
ALTER TABLE member_user ADD COLUMN IF NOT EXISTS lat double;
ALTER TABLE member_user ADD COLUMN IF NOT EXISTS province varchar(64);
ALTER TABLE member_user ADD COLUMN IF NOT EXISTS city varchar(64);
ALTER TABLE member_user ADD COLUMN IF NOT EXISTS district varchar(64);
ALTER TABLE member_user ADD COLUMN IF NOT EXISTS address varchar(255);
CREATE TABLE IF NOT EXISTS member_recent_tag (
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
member_id bigint NOT NULL,
tag_biz_type varchar(32),
deleted bit NOT NULL DEFAULT FALSE
);
@@ -246,8 +246,8 @@ tx-map:
app-secret: uYWt4DalxHgFnk6d9ul2KLdPVM1ABD9m
al-config:
access-key-id: LTAI5tBrkf3ynA5CMLb4TYbo
access-key-secret: u3Za5XigBfRXo0iJHSbPgOSZTDVN3C
access-key-id: LTAI5tAK9M3fnMhmsxVZnuTn
access-key-secret: 5J4EubEtDKHxvBytbzkgHTMQmzqpwd
face-scene-id: 1000016556
face-verify-callback-url: https://baac9a029ea84f36ba28c023fed4fc5f.hd.takin.cc
face-verify-callback-token: uVuYOzoiQBWPp4A5kPgTddZlKWoSQlzc
@@ -255,16 +255,9 @@ tx-map:
app-key: GGCBZ-SVPWT-IO6XL-LHZ3I-YPMBS-O6B6D
app-secret: uYWt4DalxHgFnk6d9ul2KLdPVM1ABD9m
#al-config:
# access-key-id: LTAI5tJ7sSxB96XaY9T3Vq3D
# access-key-secret: EVXWFTS2SB6UkjUVFLhTDBfIvIXgI3
# face-scene-id: 1000016045
# face-verify-callback-url: https://dzc001.fy.takin.cc
# face-verify-callback-token: uVuYOzoiQBWPp4A5kPgTddZlKWoSQlzc
al-config:
access-key-id: LTAI5tBrkf3ynA5CMLb4TYbo
access-key-secret: u3Za5XigBfRXo0iJHSbPgOSZTDVN3C
access-key-id: LTAI5tAK9M3fnMhmsxVZnuTn
access-key-secret: 5J4EubEtDKHxvBytbzkgHTMQmzqpwd
face-scene-id: 1000016556
face-verify-callback-url: https://baac9a029ea84f36ba28c023fed4fc5f.hd.takin.cc
face-verify-callback-token: uVuYOzoiQBWPp4A5kPgTddZlKWoSQlzc
@@ -248,7 +248,7 @@ tx-map:
al-config:
access-key-id: LTAI5tBrkf3ynA5CMLb4TYbo
access-key-secret: u3Za5XigBfRXo0iJHSbPgOSZTDVN3C
access-key-secret: 5J4EubEtDKHxvBytbzkgHTMQmzqpwd
face-scene-id: 1000016556
face-verify-callback-url: https://api.htysapp.com
face-verify-callback-token: uVuYOzoiQBWPp4A5kPgTddZlKWoSQlzc
@@ -278,7 +278,7 @@ yudao:
front-type: 20 # 前端模版的类型,参见 CodegenFrontTypeEnum 枚举类
unit-test-enable: false # 是否生成单元测试
tenant: # 多租户相关配置项
enable: true
enable: false
ignore-urls:
- /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号
- /admin-api/system/tenant/get-by-website # 基于域名获取租户,不许带租户编号