diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserPageReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserPageReqVO.java index ee5bc15..596435b 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserPageReqVO.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserPageReqVO.java @@ -35,6 +35,9 @@ public class MemberUserPageReqVO extends PageParam { @Schema(description = "会员标签编号列表", example = "[1, 2]") private List tagIds; + @Schema(description = "用户标签类型列表,对应 member_recent_tag 字典,匹配任一所选标签", example = "[\"1\", \"4\"]") + private List 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 芋艿:登录用户类型; diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java index c8366c1..9952b3b 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java @@ -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 { 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() + LambdaQueryWrapper query = new LambdaQueryWrapperX() .likeIfPresent(MemberUserDO::getMobile, reqVO.getMobile()) .betweenIfPresent(MemberUserDO::getLoginDate, reqVO.getLoginDate()) .likeIfPresent(MemberUserDO::getNickname, reqVO.getNickname()) @@ -94,8 +96,19 @@ public interface MemberUserMapper extends BaseMapperX { .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) { diff --git a/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapperTest.java b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapperTest.java new file mode 100644 index 0000000..509b105 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapperTest.java @@ -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 result, long total, Long... ids) { + assertThat(result.getTotal()).isEqualTo(total); + assertThat(result.getList()).extracting(MemberUserDO::getId).containsExactly(ids); + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/test/resources/sql/member_user_page_filter_schema.sql b/yudao-module-member/yudao-module-member-biz/src/test/resources/sql/member_user_page_filter_schema.sql new file mode 100644 index 0000000..bc0c09e --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/test/resources/sql/member_user_page_filter_schema.sql @@ -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 +); diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index 3513e00..88c2785 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -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 diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index fc8e2fc..4477e61 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -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 diff --git a/yudao-server/src/main/resources/application-prod.yaml b/yudao-server/src/main/resources/application-prod.yaml index 840d3fe..cf48740 100644 --- a/yudao-server/src/main/resources/application-prod.yaml +++ b/yudao-server/src/main/resources/application-prod.yaml @@ -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 diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 3d3d25c..7ea06e5 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -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 # 基于域名获取租户,不许带租户编号