fix(p1r): 对齐 Account 市场记录状态过滤

This commit is contained in:
zizi 2026-05-29 14:55:08 +08:00
parent ac81708f61
commit 68313e6efe
5 changed files with 123 additions and 23 deletions

View File

@ -56,24 +56,24 @@ public class AccountMarketRecordServiceImpl implements AccountMarketRecordServic
public AccountPageResult<AdminPurchaseRecordRespVO> adminListPurchaseRecords(PageParam pageParam,
Long accountUserId,
String status) {
String normalizedStatus = normalizeFilter(status, ADMIN_PURCHASE_STATUSES, "status");
List<String> 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<AppPurchaseRecordRespVO> appListPurchases(Long accountUserId, PageParam pageParam,
String status) {
String normalizedStatus = normalizeFilter(status, APP_PURCHASE_STATUSES, "status");
List<String> 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<AppLicenseRecordRespVO> appListLicenses(Long accountUserId, PageParam pageParam,
String assetType, String licenseStatus) {
String normalizedAssetType = normalizeFilter(assetType, ASSET_TYPES, "assetType");
String normalizedStatus = normalizeFilter(licenseStatus, LICENSE_STATUSES, "licenseStatus");
List<String> 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<AppPublishRecordRespVO> appListPublishRecords(Long accountUserId, PageParam pageParam,
String assetType, String reviewStatus) {
String normalizedAssetType = normalizeFilter(assetType, ASSET_TYPES, "assetType");
String normalizedStatus = normalizeFilter(reviewStatus, REVIEW_STATUSES, "reviewStatus");
List<String> 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 <T> AccountPageResult<T> pageByProjection(PageParam pageParam, String recordType, Long accountUserId,
String status, String assetType,
List<String> storageStatuses, String assetType,
Function<AccountRecordProjectionDO, T> converter) {
// recordType 只能由各入口硬编码传入不能暴露给请求参数避免跨类型读取其它投影
PageResult<AccountRecordProjectionDO> page = recordProjectionMapper.selectPageByFilters(pageParam,
recordType, accountUserId, status, assetType);
recordType, accountUserId, storageStatuses, assetType);
List<T> 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<String> 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<String> toAppPurchaseStorageStatuses(String status) {
String normalizedStatus = normalizeFilter(status, APP_PURCHASE_STATUSES, "status");
if (normalizedStatus == null) {
return null;
}
if ("external_pending".equals(normalizedStatus)) {
// Market purchase 投影历史上可能写 pendingApp 合同把它归一为 external_pending
return List.of("external_pending", "pending");
}
return List.of(normalizedStatus);
}
private List<String> 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<String> toSingleStorageStatus(String status, Set<String> 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);

View File

@ -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");

View File

@ -30,13 +30,13 @@ public interface AccountRecordProjectionMapper extends BaseMapperX<AccountRecord
}
default PageResult<AccountRecordProjectionDO> selectPageByFilters(PageParam pageParam, String recordType,
Long accountUserId, String sourceStatus,
Long accountUserId, List<String> sourceStatuses,
String assetType) {
LambdaQueryWrapperX<AccountRecordProjectionDO> query = new LambdaQueryWrapperX<AccountRecordProjectionDO>()
// 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)) {

View File

@ -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<AdminPurchaseRecordRespVO> 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<AppLicenseRecordRespVO> 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<AppPublishRecordRespVO> 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<AppPublishRecordRespVO> 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();

View File

@ -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);