From 68313e6efe90a7f171e538d3de01dec321e305eb Mon Sep 17 00:00:00 2001 From: zizi Date: Fri, 29 May 2026 14:55:08 +0800 Subject: [PATCH] =?UTF-8?q?fix(p1r):=20=E5=AF=B9=E9=BD=90=20Account=20?= =?UTF-8?q?=E5=B8=82=E5=9C=BA=E8=AE=B0=E5=BD=95=E7=8A=B6=E6=80=81=E8=BF=87?= =?UTF-8?q?=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccountMarketRecordServiceImpl.java | 61 ++++++++++++++++--- .../convert/account/AccountConvert.java | 2 +- .../AccountRecordProjectionMapper.java | 4 +- .../AccountMarketRecordServiceTest.java | 59 +++++++++++++++--- .../convert/account/AccountConvertTest.java | 20 ++++++ 5 files changed, 123 insertions(+), 23 deletions(-) diff --git a/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/application/account/AccountMarketRecordServiceImpl.java b/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/application/account/AccountMarketRecordServiceImpl.java index 09d87b7e..525622d2 100644 --- a/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/application/account/AccountMarketRecordServiceImpl.java +++ b/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/application/account/AccountMarketRecordServiceImpl.java @@ -56,24 +56,24 @@ public class AccountMarketRecordServiceImpl implements AccountMarketRecordServic public AccountPageResult adminListPurchaseRecords(PageParam pageParam, Long accountUserId, String status) { - String normalizedStatus = normalizeFilter(status, ADMIN_PURCHASE_STATUSES, "status"); + List storageStatuses = toAdminPurchaseStorageStatuses(status); marketProjectionFacade.requireProjectionAvailable(RECORD_TYPE_PURCHASE); if (NON_NUMERIC_USER_ID_MARKER.equals(accountUserId)) { // 非数字 userId 代表合同 ID 当前无法映射到 member_user.id;保留过滤语义,不能退化成全量查询。 return AccountPageResult.of(Collections.emptyList(), 0L, pageParam); } requireUserIfPresent(accountUserId); - return pageByProjection(pageParam, RECORD_TYPE_PURCHASE, accountUserId, normalizedStatus, null, + return pageByProjection(pageParam, RECORD_TYPE_PURCHASE, accountUserId, storageStatuses, null, AccountConvert.INSTANCE::convertAdminPurchaseRecord); } @Override public AccountPageResult appListPurchases(Long accountUserId, PageParam pageParam, String status) { - String normalizedStatus = normalizeFilter(status, APP_PURCHASE_STATUSES, "status"); + List storageStatuses = toAppPurchaseStorageStatuses(status); marketProjectionFacade.requireProjectionAvailable(RECORD_TYPE_PURCHASE); requireUser(accountUserId); - return pageByProjection(pageParam, RECORD_TYPE_PURCHASE, accountUserId, normalizedStatus, null, + return pageByProjection(pageParam, RECORD_TYPE_PURCHASE, accountUserId, storageStatuses, null, AccountConvert.INSTANCE::convertAppPurchaseRecord); } @@ -81,10 +81,10 @@ public class AccountMarketRecordServiceImpl implements AccountMarketRecordServic public AccountPageResult appListLicenses(Long accountUserId, PageParam pageParam, String assetType, String licenseStatus) { String normalizedAssetType = normalizeFilter(assetType, ASSET_TYPES, "assetType"); - String normalizedStatus = normalizeFilter(licenseStatus, LICENSE_STATUSES, "licenseStatus"); + List storageStatuses = toSingleStorageStatus(licenseStatus, LICENSE_STATUSES, "licenseStatus"); marketProjectionFacade.requireProjectionAvailable(RECORD_TYPE_LICENSE); requireUser(accountUserId); - return pageByProjection(pageParam, RECORD_TYPE_LICENSE, accountUserId, normalizedStatus, normalizedAssetType, + return pageByProjection(pageParam, RECORD_TYPE_LICENSE, accountUserId, storageStatuses, normalizedAssetType, AccountConvert.INSTANCE::convertAppLicenseRecord); } @@ -92,19 +92,19 @@ public class AccountMarketRecordServiceImpl implements AccountMarketRecordServic public AccountPageResult appListPublishRecords(Long accountUserId, PageParam pageParam, String assetType, String reviewStatus) { String normalizedAssetType = normalizeFilter(assetType, ASSET_TYPES, "assetType"); - String normalizedStatus = normalizeFilter(reviewStatus, REVIEW_STATUSES, "reviewStatus"); + List storageStatuses = toPublishStorageStatuses(reviewStatus); marketProjectionFacade.requireProjectionAvailable(RECORD_TYPE_PUBLISH); requireUser(accountUserId); - return pageByProjection(pageParam, RECORD_TYPE_PUBLISH, accountUserId, normalizedStatus, normalizedAssetType, + return pageByProjection(pageParam, RECORD_TYPE_PUBLISH, accountUserId, storageStatuses, normalizedAssetType, AccountConvert.INSTANCE::convertAppPublishRecord); } private AccountPageResult pageByProjection(PageParam pageParam, String recordType, Long accountUserId, - String status, String assetType, + List storageStatuses, String assetType, Function converter) { // recordType 只能由各入口硬编码传入,不能暴露给请求参数,避免跨类型读取其它投影。 PageResult page = recordProjectionMapper.selectPageByFilters(pageParam, - recordType, accountUserId, status, assetType); + recordType, accountUserId, storageStatuses, assetType); List list = page.getList() == null ? Collections.emptyList() : page.getList().stream().map(converter).toList(); return AccountPageResult.of(list, page.getTotal(), pageParam); @@ -121,6 +121,47 @@ public class AccountMarketRecordServiceImpl implements AccountMarketRecordServic return normalized; } + private List toAdminPurchaseStorageStatuses(String status) { + String normalizedStatus = normalizeFilter(status, ADMIN_PURCHASE_STATUSES, "status"); + if (normalizedStatus == null) { + return null; + } + if ("processing".equals(normalizedStatus)) { + // Admin OpenAPI 没有 external_pending;所有会响应为 processing 的上游状态都必须一起命中过滤。 + return List.of("processing", "pending", "external_pending"); + } + return List.of(normalizedStatus); + } + + private List toAppPurchaseStorageStatuses(String status) { + String normalizedStatus = normalizeFilter(status, APP_PURCHASE_STATUSES, "status"); + if (normalizedStatus == null) { + return null; + } + if ("external_pending".equals(normalizedStatus)) { + // Market purchase 投影历史上可能写 pending;App 合同把它归一为 external_pending。 + return List.of("external_pending", "pending"); + } + return List.of(normalizedStatus); + } + + private List toPublishStorageStatuses(String reviewStatus) { + String normalizedStatus = normalizeFilter(reviewStatus, REVIEW_STATUSES, "reviewStatus"); + if (normalizedStatus == null) { + return null; + } + if ("under_review".equals(normalizedStatus)) { + // muse_market_publish_request 的默认状态是 pending_review;旧投影可能仍使用 pending/processing。 + return List.of("under_review", "pending_review", "pending", "processing"); + } + return List.of(normalizedStatus); + } + + private List toSingleStorageStatus(String status, Set allowedValues, String fieldName) { + String normalizedStatus = normalizeFilter(status, allowedValues, fieldName); + return normalizedStatus == null ? null : List.of(normalizedStatus); + } + private void requireUser(Long accountUserId) { if (accountUserId == null || memberUserMapper.selectById(accountUserId) == null) { throw new ServiceException(ACCOUNT_USER_NOT_EXISTS); diff --git a/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/convert/account/AccountConvert.java b/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/convert/account/AccountConvert.java index cf588476..457d03f4 100644 --- a/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/convert/account/AccountConvert.java +++ b/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/convert/account/AccountConvert.java @@ -321,7 +321,7 @@ public interface AccountConvert { } default String toReviewStatus(String status) { - if ("pending".equals(status) || "processing".equals(status)) { + if ("pending_review".equals(status) || "pending".equals(status) || "processing".equals(status)) { return "under_review"; } return statusOrFallback(status, REVIEW_STATUSES, "needs_supplement"); diff --git a/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/dal/mysql/account/AccountRecordProjectionMapper.java b/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/dal/mysql/account/AccountRecordProjectionMapper.java index 117f47ff..59607633 100644 --- a/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/dal/mysql/account/AccountRecordProjectionMapper.java +++ b/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/dal/mysql/account/AccountRecordProjectionMapper.java @@ -30,13 +30,13 @@ public interface AccountRecordProjectionMapper extends BaseMapperX selectPageByFilters(PageParam pageParam, String recordType, - Long accountUserId, String sourceStatus, + Long accountUserId, List sourceStatuses, String assetType) { LambdaQueryWrapperX query = new LambdaQueryWrapperX() // record_type 是 Account 侧隔离 purchase/license/publish 的硬边界,不能由请求方自由控制。 .eq(AccountRecordProjectionDO::getRecordType, recordType) .eqIfPresent(AccountRecordProjectionDO::getAccountUserId, accountUserId) - .eqIfPresent(AccountRecordProjectionDO::getSourceStatus, sourceStatus) + .inIfPresent(AccountRecordProjectionDO::getSourceStatus, sourceStatuses) .orderByDesc(AccountRecordProjectionDO::getOccurredAt) .orderByDesc(AccountRecordProjectionDO::getCreateTime); if (StringUtils.hasText(assetType)) { diff --git a/muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/application/account/AccountMarketRecordServiceTest.java b/muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/application/account/AccountMarketRecordServiceTest.java index 7c254aaa..9be83604 100644 --- a/muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/application/account/AccountMarketRecordServiceTest.java +++ b/muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/application/account/AccountMarketRecordServiceTest.java @@ -51,7 +51,7 @@ class AccountMarketRecordServiceTest extends BaseMockitoUnitTest { void should_adminListPurchaseRecords_queryPurchaseProjectionAndMaskExternalRef() { PageParam pageParam = page(2, 20); when(memberUserMapper.selectById(1001L)).thenReturn(MemberUserDO.builder().id(1001L).build()); - when(recordProjectionMapper.selectPageByFilters(pageParam, "purchase", 1001L, "completed", null)) + when(recordProjectionMapper.selectPageByFilters(pageParam, "purchase", 1001L, List.of("completed"), null)) .thenReturn(new PageResult<>(List.of(purchaseProjection("purchase-1", 1001L, "EXT-ORDER-SECRET-123456", "completed")), 3L)); @@ -68,14 +68,31 @@ class AccountMarketRecordServiceTest extends BaseMockitoUnitTest { assertEquals("completed", record.getStatus()); assertFalse(record.getExternalOrderRef().contains("SECRET")); verify(marketProjectionFacade).requireProjectionAvailable("purchase"); - verify(recordProjectionMapper).selectPageByFilters(pageParam, "purchase", 1001L, "completed", null); + verify(recordProjectionMapper).selectPageByFilters(pageParam, "purchase", 1001L, List.of("completed"), null); + } + + @Test + void should_adminListPurchaseRecords_mapProcessingFilterToRawPendingProcessingStatuses() { + PageParam pageParam = page(1, 20); + when(recordProjectionMapper.selectPageByFilters(pageParam, "purchase", null, + List.of("processing", "pending", "external_pending"), null)) + .thenReturn(new PageResult<>(List.of(purchaseProjection("purchase-pending", 1001L, + "EXT-ORDER-SECRET-222222", "pending")), 1L)); + + AccountPageResult page = + service.adminListPurchaseRecords(pageParam, null, "processing"); + + assertEquals("processing", page.getList().get(0).getStatus()); + verify(recordProjectionMapper).selectPageByFilters(pageParam, "purchase", null, + List.of("processing", "pending", "external_pending"), null); } @Test void should_appListPurchases_useLoginOwnerAndPurchaseTypeOnly() { PageParam pageParam = page(1, 20); when(memberUserMapper.selectById(1001L)).thenReturn(MemberUserDO.builder().id(1001L).build()); - when(recordProjectionMapper.selectPageByFilters(pageParam, "purchase", 1001L, "external_pending", null)) + when(recordProjectionMapper.selectPageByFilters(pageParam, "purchase", 1001L, + List.of("external_pending", "pending"), null)) .thenReturn(new PageResult<>(List.of(purchaseProjection("purchase-2", 1001L, "EXT-ORDER-SECRET-999999", "pending")), 1L)); @@ -86,15 +103,17 @@ class AccountMarketRecordServiceTest extends BaseMockitoUnitTest { assertEquals("purchase-2", record.getRecordId()); assertEquals("external_pending", record.getStatus()); assertFalse(record.getExternalOrderRef().contains("SECRET")); - verify(recordProjectionMapper).selectPageByFilters(pageParam, "purchase", 1001L, "external_pending", null); - verify(recordProjectionMapper, never()).selectPageByFilters(pageParam, "license", 1001L, "external_pending", null); + verify(recordProjectionMapper).selectPageByFilters(pageParam, "purchase", 1001L, + List.of("external_pending", "pending"), null); + verify(recordProjectionMapper, never()).selectPageByFilters(pageParam, "license", 1001L, + List.of("external_pending", "pending"), null); } @Test void should_appListLicenses_filterByAssetAndStatusWithoutMutatingLicenseStatus() { PageParam pageParam = page(1, 20); when(memberUserMapper.selectById(1001L)).thenReturn(MemberUserDO.builder().id(1001L).build()); - when(recordProjectionMapper.selectPageByFilters(pageParam, "license", 1001L, "bound", "agent")) + when(recordProjectionMapper.selectPageByFilters(pageParam, "license", 1001L, List.of("bound"), "agent")) .thenReturn(new PageResult<>(List.of(licenseProjection()), 1L)); AccountPageResult page = @@ -106,14 +125,14 @@ class AccountMarketRecordServiceTest extends BaseMockitoUnitTest { assertEquals("bound", record.getLicenseStatus()); assertEquals("allowed", record.getActionPolicy().getInstallPolicy()); assertEquals(List.of("source_version_changed"), record.getActionPolicy().getRecheckReasons()); - verify(recordProjectionMapper).selectPageByFilters(pageParam, "license", 1001L, "bound", "agent"); + verify(recordProjectionMapper).selectPageByFilters(pageParam, "license", 1001L, List.of("bound"), "agent"); } @Test void should_appListPublishRecords_filterByAssetAndReviewStatus() { PageParam pageParam = page(1, 20); when(memberUserMapper.selectById(1001L)).thenReturn(MemberUserDO.builder().id(1001L).build()); - when(recordProjectionMapper.selectPageByFilters(pageParam, "publish", 1001L, "submitted", "work")) + when(recordProjectionMapper.selectPageByFilters(pageParam, "publish", 1001L, List.of("submitted"), "work")) .thenReturn(new PageResult<>(List.of(publishProjection()), 1L)); AccountPageResult page = @@ -124,7 +143,23 @@ class AccountMarketRecordServiceTest extends BaseMockitoUnitTest { assertEquals("work", record.getAssetType()); assertEquals("submitted", record.getReviewStatus()); assertEquals("not_listed", record.getMarketStatus()); - verify(recordProjectionMapper).selectPageByFilters(pageParam, "publish", 1001L, "submitted", "work"); + verify(recordProjectionMapper).selectPageByFilters(pageParam, "publish", 1001L, List.of("submitted"), "work"); + } + + @Test + void should_appListPublishRecords_mapUnderReviewFilterToRawPendingReviewStatuses() { + PageParam pageParam = page(1, 20); + when(memberUserMapper.selectById(1001L)).thenReturn(MemberUserDO.builder().id(1001L).build()); + when(recordProjectionMapper.selectPageByFilters(pageParam, "publish", 1001L, + List.of("under_review", "pending_review", "pending", "processing"), "work")) + .thenReturn(new PageResult<>(List.of(publishProjection("pending_review")), 1L)); + + AccountPageResult page = + service.appListPublishRecords(1001L, pageParam, "work", "under_review"); + + assertEquals("under_review", page.getList().get(0).getReviewStatus()); + verify(recordProjectionMapper).selectPageByFilters(pageParam, "publish", 1001L, + List.of("under_review", "pending_review", "pending", "processing"), "work"); } @Test @@ -192,12 +227,16 @@ class AccountMarketRecordServiceTest extends BaseMockitoUnitTest { } private static AccountRecordProjectionDO publishProjection() { + return publishProjection("submitted"); + } + + private static AccountRecordProjectionDO publishProjection(String sourceStatus) { AccountRecordProjectionDO projection = AccountRecordProjectionDO.builder() .accountUserId(1001L) .recordType("publish") .recordId("publish-1") .title("Work Pro") - .sourceStatus("submitted") + .sourceStatus(sourceStatus) .projectionSnapshot("{\"assetType\":\"work\",\"version\":\"0.1.0\"," + "\"marketStatus\":\"not_listed\",\"blockingReason\":\"等待审核\"}") .build(); diff --git a/muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/convert/account/AccountConvertTest.java b/muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/convert/account/AccountConvertTest.java index 00375dfd..f5e1bc19 100644 --- a/muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/convert/account/AccountConvertTest.java +++ b/muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/convert/account/AccountConvertTest.java @@ -2,6 +2,7 @@ package cn.iocoder.muse.module.member.convert.account; import cn.iocoder.muse.framework.common.pojo.PageResult; import cn.iocoder.muse.module.member.controller.admin.account.vo.AdminPurchaseRecordRespVO; +import cn.iocoder.muse.module.member.controller.app.account.vo.AppPublishRecordRespVO; import cn.iocoder.muse.module.member.controller.app.account.vo.AppProfileRespVO; import cn.iocoder.muse.module.member.controller.app.account.vo.SecurityEventDetailRespVO; import cn.iocoder.muse.module.member.dal.dataobject.account.AccountProfileDO; @@ -110,6 +111,25 @@ class AccountConvertTest { assertEquals("Shanghai", vo.getDeviceInfo().getLocation()); } + @Test + void should_convertPublishPendingReviewToOpenApiUnderReview() { + AccountRecordProjectionDO projection = AccountRecordProjectionDO.builder() + .accountUserId(1001L) + .recordType("publish") + .recordId("publish-1") + .title("Work Pro") + .sourceStatus("pending_review") + .projectionSnapshot("{\"assetType\":\"work\",\"version\":\"0.1.0\",\"marketStatus\":\"not_listed\"}") + .build(); + projection.setOccurredAt(LocalDateTime.of(2026, 5, 28, 12, 0)); + + AppPublishRecordRespVO vo = convert.convertAppPublishRecord(projection); + + assertEquals("publish-1", vo.getRecordId()); + assertEquals("under_review", vo.getReviewStatus()); + assertEquals("not_listed", vo.getMarketStatus()); + } + @Test void should_applySecurityAcknowledgementFields_when_ackExists() { LocalDateTime acknowledgedAt = LocalDateTime.of(2026, 5, 28, 13, 0);