1
This commit is contained in:
@@ -14,6 +14,7 @@ logs/
|
|||||||
data/
|
data/
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
.env.development
|
||||||
!.env.example
|
!.env.example
|
||||||
!.env.*.example
|
!.env.*.example
|
||||||
application-local.yml
|
application-local.yml
|
||||||
|
|||||||
+10
-3
@@ -52,9 +52,16 @@ public class NotificationRecipientResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<IamMembership> byRole(CreateNotificationRequest request) {
|
private List<IamMembership> byRole(CreateNotificationRequest request) {
|
||||||
var role = roles.selectOne(new LambdaQueryWrapper<com.baiyue.iam.entity.IamRole>()
|
String appCode = request.recipientType() == com.baiyue.iam.notification.dto.NotificationDtos.RecipientType.TENANT_ROLE
|
||||||
.eq(com.baiyue.iam.entity.IamRole::getTenantId, request.tenantId())
|
? "MERCHANT" : "PLATFORM";
|
||||||
.eq(com.baiyue.iam.entity.IamRole::getRoleCode, request.roleCode()).eq(com.baiyue.iam.entity.IamRole::getStatus,"ACTIVE").last("LIMIT 1"));
|
// Merchant members can be assigned tenant-specific roles or platform-provided merchant roles (tenant 0).
|
||||||
|
var candidates = roles.selectList(new LambdaQueryWrapper<com.baiyue.iam.entity.IamRole>()
|
||||||
|
.in(com.baiyue.iam.entity.IamRole::getTenantId, request.tenantId(), 0L)
|
||||||
|
.eq(com.baiyue.iam.entity.IamRole::getAppCode, appCode)
|
||||||
|
.eq(com.baiyue.iam.entity.IamRole::getRoleCode, request.roleCode())
|
||||||
|
.eq(com.baiyue.iam.entity.IamRole::getStatus, "ACTIVE")
|
||||||
|
.orderByDesc(com.baiyue.iam.entity.IamRole::getTenantId));
|
||||||
|
var role = candidates.isEmpty() ? null : candidates.get(0);
|
||||||
if (role == null) fail("角色编码不存在,请检查角色编码是否正确");
|
if (role == null) fail("角色编码不存在,请检查角色编码是否正确");
|
||||||
List<Long> ids = membershipRoles.selectList(new LambdaQueryWrapper<com.baiyue.iam.entity.IamMembershipRole>()
|
List<Long> ids = membershipRoles.selectList(new LambdaQueryWrapper<com.baiyue.iam.entity.IamMembershipRole>()
|
||||||
.eq(com.baiyue.iam.entity.IamMembershipRole::getTenantId,request.tenantId())
|
.eq(com.baiyue.iam.entity.IamMembershipRole::getTenantId,request.tenantId())
|
||||||
|
|||||||
+9
-3
@@ -12,7 +12,9 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class NotificationTemplateRenderer {
|
public class NotificationTemplateRenderer {
|
||||||
private static final Pattern VARIABLE = Pattern.compile("\\{\\{([a-z][a-z0-9_]{0,31})}}" );
|
// ${name} is the format inserted by the template editor; retain {{name}} for existing templates.
|
||||||
|
private static final Pattern VARIABLE = Pattern.compile(
|
||||||
|
"\\$\\{([a-z][a-z0-9_]{0,31})}|\\{\\{([a-z][a-z0-9_]{0,31})}}" );
|
||||||
|
|
||||||
public String render(String template, Set<String> whitelist, Map<String, String> variables) {
|
public String render(String template, Set<String> whitelist, Map<String, String> variables) {
|
||||||
Set<String> used = placeholders(template);
|
Set<String> used = placeholders(template);
|
||||||
@@ -20,7 +22,11 @@ public class NotificationTemplateRenderer {
|
|||||||
if (!whitelist.containsAll(variables.keySet())) fail("NOTIFICATION_VARIABLE_NOT_ALLOWED");
|
if (!whitelist.containsAll(variables.keySet())) fail("NOTIFICATION_VARIABLE_NOT_ALLOWED");
|
||||||
if (!variables.keySet().containsAll(used)) fail("NOTIFICATION_VARIABLE_MISSING");
|
if (!variables.keySet().containsAll(used)) fail("NOTIFICATION_VARIABLE_MISSING");
|
||||||
String rendered = template;
|
String rendered = template;
|
||||||
for (String key : used) rendered = rendered.replace("{{" + key + "}}", variables.get(key));
|
for (String key : used) {
|
||||||
|
String value = variables.get(key);
|
||||||
|
rendered = rendered.replace("${" + key + "}", value)
|
||||||
|
.replace("{{" + key + "}}", value);
|
||||||
|
}
|
||||||
if (rendered.length() > 10_000) fail("NOTIFICATION_TEMPLATE_RENDER_TOO_LONG");
|
if (rendered.length() > 10_000) fail("NOTIFICATION_TEMPLATE_RENDER_TOO_LONG");
|
||||||
return rendered;
|
return rendered;
|
||||||
}
|
}
|
||||||
@@ -33,7 +39,7 @@ public class NotificationTemplateRenderer {
|
|||||||
|
|
||||||
private Set<String> placeholders(String value) {
|
private Set<String> placeholders(String value) {
|
||||||
Set<String> keys = new HashSet<>(); Matcher matcher = VARIABLE.matcher(value);
|
Set<String> keys = new HashSet<>(); Matcher matcher = VARIABLE.matcher(value);
|
||||||
while (matcher.find()) keys.add(matcher.group(1));
|
while (matcher.find()) keys.add(matcher.group(1) != null ? matcher.group(1) : matcher.group(2));
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ public class ProfileResourceService {
|
|||||||
if (tenant == null || !"MERCHANT".equals(tenant.getTenantType())) {
|
if (tenant == null || !"MERCHANT".equals(tenant.getTenantType())) {
|
||||||
throw new BizException(ResultCode.NOT_FOUND, "Enterprise not found / 企业客户不存在");
|
throw new BizException(ResultCode.NOT_FOUND, "Enterprise not found / 企业客户不存在");
|
||||||
}
|
}
|
||||||
if (!"PROFILE_REQUIRED".equals(tenant.getStatus()) && !"REJECTED".equals(tenant.getStatus())) {
|
if (!platformCanUpdateQualifications(tenant)) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质");
|
throw new BizException(ResultCode.CONFLICT, "Qualifications can only be uploaded before review / 仅待完善、已驳回或已审核客户可上传资质");
|
||||||
}
|
}
|
||||||
requireOptionalQualificationType(request.qualificationType());
|
requireOptionalQualificationType(request.qualificationType());
|
||||||
String certificateNo = resolveCertificateNo(tenant, request.qualificationType(), request.certificateNo());
|
String certificateNo = resolveCertificateNo(tenant, request.qualificationType(), request.certificateNo());
|
||||||
@@ -163,6 +163,7 @@ public class ProfileResourceService {
|
|||||||
IamTenantQualification qualification = createQualification(tenant.getId(), request.qualificationType(),
|
IamTenantQualification qualification = createQualification(tenant.getId(), request.qualificationType(),
|
||||||
certificateNo, request.documentUrl(), request.originalFileName());
|
certificateNo, request.documentUrl(), request.originalFileName());
|
||||||
qualification.setValidTo(request.validTo());
|
qualification.setValidTo(request.validTo());
|
||||||
|
markPlatformQualificationApproved(tenant, qualification);
|
||||||
repository.saveQualification(qualification);
|
repository.saveQualification(qualification);
|
||||||
return new PlatformQualificationRegistrationResponse(
|
return new PlatformQualificationRegistrationResponse(
|
||||||
qualification.getId(), null, tenant.getStatus(), replacedDocumentUrl);
|
qualification.getId(), null, tenant.getStatus(), replacedDocumentUrl);
|
||||||
@@ -173,7 +174,7 @@ public class ProfileResourceService {
|
|||||||
resourceId = "#request.tenantId", description = "上传企业必填资质包", captureAfter = false)
|
resourceId = "#request.tenantId", description = "上传企业必填资质包", captureAfter = false)
|
||||||
public RequiredQualificationPackageResponse submitRequiredQualificationPackage(
|
public RequiredQualificationPackageResponse submitRequiredQualificationPackage(
|
||||||
RequiredQualificationPackageRequest request) {
|
RequiredQualificationPackageRequest request) {
|
||||||
return registerRequiredQualificationPackage(request, "商户在送审前替换文件");
|
return registerRequiredQualificationPackage(request, "商户在送审前替换文件", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -181,16 +182,17 @@ public class ProfileResourceService {
|
|||||||
resourceId = "#request.tenantId", description = "平台代客户上传必填资质包", captureAfter = false)
|
resourceId = "#request.tenantId", description = "平台代客户上传必填资质包", captureAfter = false)
|
||||||
public RequiredQualificationPackageResponse registerPlatformRequiredQualificationPackage(
|
public RequiredQualificationPackageResponse registerPlatformRequiredQualificationPackage(
|
||||||
RequiredQualificationPackageRequest request) {
|
RequiredQualificationPackageRequest request) {
|
||||||
return registerRequiredQualificationPackage(request, "平台在送审前替换文件");
|
return registerRequiredQualificationPackage(request, "平台在送审前替换文件", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RequiredQualificationPackageResponse registerRequiredQualificationPackage(
|
private RequiredQualificationPackageResponse registerRequiredQualificationPackage(
|
||||||
RequiredQualificationPackageRequest request, String replacementComment) {
|
RequiredQualificationPackageRequest request, String replacementComment, boolean platformManaged) {
|
||||||
IamTenant tenant = tenantRepository.findTenantForUpdate(request.tenantId());
|
IamTenant tenant = tenantRepository.findTenantForUpdate(request.tenantId());
|
||||||
if (tenant == null || !"MERCHANT".equals(tenant.getTenantType())) {
|
if (tenant == null || !"MERCHANT".equals(tenant.getTenantType())) {
|
||||||
throw new BizException(ResultCode.NOT_FOUND, "Enterprise not found / 企业客户不存在");
|
throw new BizException(ResultCode.NOT_FOUND, "Enterprise not found / 企业客户不存在");
|
||||||
}
|
}
|
||||||
if (!"PROFILE_REQUIRED".equals(tenant.getStatus()) && !"REJECTED".equals(tenant.getStatus())) {
|
if (platformManaged ? !platformCanUpdateQualifications(tenant)
|
||||||
|
: (!"PROFILE_REQUIRED".equals(tenant.getStatus()) && !"REJECTED".equals(tenant.getStatus()))) {
|
||||||
throw new BizException(ResultCode.CONFLICT,
|
throw new BizException(ResultCode.CONFLICT,
|
||||||
"Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质");
|
"Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质");
|
||||||
}
|
}
|
||||||
@@ -212,12 +214,14 @@ public class ProfileResourceService {
|
|||||||
IamTenantQualification businessLicense = createQualification(tenant.getId(), "BUSINESS_LICENSE",
|
IamTenantQualification businessLicense = createQualification(tenant.getId(), "BUSINESS_LICENSE",
|
||||||
creditCode, request.businessLicenseDocumentUrl(), request.businessLicenseOriginalFileName());
|
creditCode, request.businessLicenseDocumentUrl(), request.businessLicenseOriginalFileName());
|
||||||
businessLicense.setValidTo(request.businessLicenseValidTo());
|
businessLicense.setValidTo(request.businessLicenseValidTo());
|
||||||
|
if (platformManaged) markPlatformQualificationApproved(tenant, businessLicense);
|
||||||
repository.saveQualification(businessLicense);
|
repository.saveQualification(businessLicense);
|
||||||
IamTenantQualification legalPersonId = null;
|
IamTenantQualification legalPersonId = null;
|
||||||
if (hasLegalPersonId) {
|
if (hasLegalPersonId) {
|
||||||
legalPersonId = createQualification(tenant.getId(), "LEGAL_PERSON_ID",
|
legalPersonId = createQualification(tenant.getId(), "LEGAL_PERSON_ID",
|
||||||
null, request.legalPersonIdDocumentUrl().trim(), request.legalPersonIdOriginalFileName());
|
null, request.legalPersonIdDocumentUrl().trim(), request.legalPersonIdOriginalFileName());
|
||||||
legalPersonId.setValidTo(request.legalPersonIdValidTo());
|
legalPersonId.setValidTo(request.legalPersonIdValidTo());
|
||||||
|
if (platformManaged) markPlatformQualificationApproved(tenant, legalPersonId);
|
||||||
repository.saveQualification(legalPersonId);
|
repository.saveQualification(legalPersonId);
|
||||||
}
|
}
|
||||||
if (!creditCode.equals(tenant.getUnifiedSocialCreditCode())) {
|
if (!creditCode.equals(tenant.getUnifiedSocialCreditCode())) {
|
||||||
@@ -314,6 +318,17 @@ public class ProfileResourceService {
|
|||||||
&& !"SUPERSEDED".equals(qualification.getStatus());
|
&& !"SUPERSEDED".equals(qualification.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean platformCanUpdateQualifications(IamTenant tenant) {
|
||||||
|
return "PROFILE_REQUIRED".equals(tenant.getStatus()) || "REJECTED".equals(tenant.getStatus())
|
||||||
|
|| "ACTIVE".equals(tenant.getStatus()) || "DISABLED".equals(tenant.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markPlatformQualificationApproved(IamTenant tenant, IamTenantQualification qualification) {
|
||||||
|
if ("ACTIVE".equals(tenant.getStatus()) || "DISABLED".equals(tenant.getStatus())) {
|
||||||
|
qualification.setStatus("APPROVED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void requireMerchant(long tenantId) {
|
private void requireMerchant(long tenantId) {
|
||||||
IamTenant tenant = tenantRepository.findTenant(tenantId);
|
IamTenant tenant = tenantRepository.findTenant(tenantId);
|
||||||
if (tenant == null || tenant.getId() == null || tenant.getId().longValue() != tenantId
|
if (tenant == null || tenant.getId() == null || tenant.getId().longValue() != tenantId
|
||||||
|
|||||||
+37
-3
@@ -2,6 +2,7 @@ package com.baiyue.iam.notification;
|
|||||||
|
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.iam.entity.IamMembership;
|
import com.baiyue.iam.entity.IamMembership;
|
||||||
|
import com.baiyue.iam.entity.IamMembershipRole;
|
||||||
import com.baiyue.iam.entity.IamRole;
|
import com.baiyue.iam.entity.IamRole;
|
||||||
import com.baiyue.iam.entity.IamUser;
|
import com.baiyue.iam.entity.IamUser;
|
||||||
import com.baiyue.iam.mapper.IamMembershipMapper;
|
import com.baiyue.iam.mapper.IamMembershipMapper;
|
||||||
@@ -22,7 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
class NotificationRecipientResolverTest {
|
class NotificationRecipientResolverTest {
|
||||||
private final IamMembershipMapper memberships = mock(IamMembershipMapper.class);
|
private final IamMembershipMapper memberships = mock(IamMembershipMapper.class);
|
||||||
@@ -70,7 +71,7 @@ class NotificationRecipientResolverTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void explainsWhenRoleCodeDoesNotExist() {
|
void explainsWhenRoleCodeDoesNotExist() {
|
||||||
when(roles.selectOne(any())).thenReturn(null);
|
when(roles.selectList(any())).thenReturn(List.of());
|
||||||
|
|
||||||
assertThatThrownBy(() -> resolver.resolve(roleRequest(RecipientType.PLATFORM_ROLE, "MISSING_ROLE")))
|
assertThatThrownBy(() -> resolver.resolve(roleRequest(RecipientType.PLATFORM_ROLE, "MISSING_ROLE")))
|
||||||
.isInstanceOf(BizException.class)
|
.isInstanceOf(BizException.class)
|
||||||
@@ -81,7 +82,7 @@ class NotificationRecipientResolverTest {
|
|||||||
void explainsWhenRoleHasNoActiveMembers() {
|
void explainsWhenRoleHasNoActiveMembers() {
|
||||||
IamRole role = new IamRole();
|
IamRole role = new IamRole();
|
||||||
role.setId(501L);
|
role.setId(501L);
|
||||||
when(roles.selectOne(any())).thenReturn(role);
|
when(roles.selectList(any())).thenReturn(List.of(role));
|
||||||
when(membershipRoles.selectList(any())).thenReturn(List.of());
|
when(membershipRoles.selectList(any())).thenReturn(List.of());
|
||||||
|
|
||||||
assertThatThrownBy(() -> resolver.resolve(roleRequest(RecipientType.PLATFORM_ROLE, "CUSTOMER_REVIEWER")))
|
assertThatThrownBy(() -> resolver.resolve(roleRequest(RecipientType.PLATFORM_ROLE, "CUSTOMER_REVIEWER")))
|
||||||
@@ -89,6 +90,39 @@ class NotificationRecipientResolverTest {
|
|||||||
.hasMessage("该角色下暂无可接收消息的有效用户,请先为角色分配启用中的成员");
|
.hasMessage("该角色下暂无可接收消息的有效用户,请先为角色分配启用中的成员");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolvesEnterpriseMemberAssignedToPlatformProvidedMerchantRole() {
|
||||||
|
IamRole role = new IamRole();
|
||||||
|
role.setId(501L);
|
||||||
|
role.setTenantId(0L);
|
||||||
|
role.setAppCode("MERCHANT");
|
||||||
|
role.setRoleCode("MERCHANT_OWNER");
|
||||||
|
IamMembershipRole assignment = new IamMembershipRole();
|
||||||
|
assignment.setTenantId(9L);
|
||||||
|
assignment.setRoleId(role.getId());
|
||||||
|
assignment.setMembershipId(301L);
|
||||||
|
IamMembership membership = new IamMembership();
|
||||||
|
membership.setId(301L);
|
||||||
|
membership.setTenantId(9L);
|
||||||
|
membership.setUserId(401L);
|
||||||
|
membership.setStatus("ACTIVE");
|
||||||
|
IamUser user = new IamUser();
|
||||||
|
user.setId(401L);
|
||||||
|
user.setDisplayName("企业管理员");
|
||||||
|
user.setStatus("ACTIVE");
|
||||||
|
when(roles.selectList(any())).thenReturn(List.of(role));
|
||||||
|
when(membershipRoles.selectList(any())).thenReturn(List.of(assignment));
|
||||||
|
when(memberships.selectBatchIds(List.of(301L))).thenReturn(List.of(membership));
|
||||||
|
when(users.selectById(401L)).thenReturn(user);
|
||||||
|
|
||||||
|
var recipients = resolver.resolve(new CreateNotificationRequest(9L, "ORDER_STATUS_CHANGED",
|
||||||
|
RecipientType.TENANT_ROLE, null, "MERCHANT_OWNER", Map.of(), null,
|
||||||
|
"notification-tenant-role-key", null, null));
|
||||||
|
|
||||||
|
assertThat(recipients).containsExactly(new NotificationRecipientResolver.ResolvedRecipient(
|
||||||
|
9L, 301L, 401L, "企业管理员", "MERCHANT"));
|
||||||
|
}
|
||||||
|
|
||||||
private CreateNotificationRequest request(RecipientType recipientType, List<Long> recipientIds) {
|
private CreateNotificationRequest request(RecipientType recipientType, List<Long> recipientIds) {
|
||||||
return new CreateNotificationRequest(9L, "ORDER_STATUS_CHANGED", recipientType, recipientIds,
|
return new CreateNotificationRequest(9L, "ORDER_STATUS_CHANGED", recipientType, recipientIds,
|
||||||
null, Map.of(), null, "notification-test-key", null, null);
|
null, Map.of(), null, "notification-test-key", null, null);
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ import java.util.Set;
|
|||||||
import static org.assertj.core.api.Assertions.*;
|
import static org.assertj.core.api.Assertions.*;
|
||||||
class NotificationTemplateRendererTest {
|
class NotificationTemplateRendererTest {
|
||||||
private final NotificationTemplateRenderer renderer=new NotificationTemplateRenderer();
|
private final NotificationTemplateRenderer renderer=new NotificationTemplateRenderer();
|
||||||
@Test void rendersOnlyWhitelistedVariables(){assertThat(renderer.render("Order {{order_no}} is {{status}}",Set.of("order_no","status"),Map.of("order_no","A1","status","ready"))).isEqualTo("Order A1 is ready");}
|
@Test void rendersOnlyWhitelistedVariables(){assertThat(renderer.render("Order ${order_no} is {{status}}",Set.of("order_no","status"),Map.of("order_no","A1","status","ready"))).isEqualTo("Order A1 is ready");}
|
||||||
@Test void rejectsMissingAndUnknownVariables(){assertThatThrownBy(()->renderer.render("{{order_no}}",Set.of("order_no"),Map.of())).isInstanceOf(BizException.class).hasMessageContaining("NOTIFICATION_VARIABLE_MISSING");assertThatThrownBy(()->renderer.render("ok",Set.of(),Map.of("secret","x"))).isInstanceOf(BizException.class).hasMessageContaining("NOTIFICATION_VARIABLE_NOT_ALLOWED");}
|
@Test void rejectsMissingAndUnknownVariables(){assertThatThrownBy(()->renderer.render("${order_no}",Set.of("order_no"),Map.of())).isInstanceOf(BizException.class).hasMessageContaining("NOTIFICATION_VARIABLE_MISSING");assertThatThrownBy(()->renderer.render("ok",Set.of(),Map.of("secret","x"))).isInstanceOf(BizException.class).hasMessageContaining("NOTIFICATION_VARIABLE_NOT_ALLOWED");}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.baiyue.iam.service;
|
package com.baiyue.iam.service;
|
||||||
|
|
||||||
import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationRequest;
|
import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationRequest;
|
||||||
|
import com.baiyue.iam.dto.OnboardingDtos.RequiredQualificationPackageRequest;
|
||||||
import com.baiyue.iam.entity.IamTenant;
|
import com.baiyue.iam.entity.IamTenant;
|
||||||
import com.baiyue.iam.entity.IamTenantQualification;
|
import com.baiyue.iam.entity.IamTenantQualification;
|
||||||
import com.baiyue.iam.repository.ProfileResourceRepository;
|
import com.baiyue.iam.repository.ProfileResourceRepository;
|
||||||
@@ -69,4 +70,51 @@ class PlatformQualificationRegistrationTest {
|
|||||||
assertEquals(existing.getDocumentUrl(), response.replacedDocumentUrl());
|
assertEquals(existing.getDocumentUrl(), response.replacedDocumentUrl());
|
||||||
assertEquals("PENDING", captor.getAllValues().get(1).getStatus());
|
assertEquals("PENDING", captor.getAllValues().get(1).getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void platformCanReplaceAnApprovedOptionalQualificationForAnActiveTenant() {
|
||||||
|
IamTenant tenant = new IamTenant();
|
||||||
|
tenant.setId(10001L); tenant.setTenantType("MERCHANT"); tenant.setStatus("ACTIVE");
|
||||||
|
IamTenantQualification existing = new IamTenantQualification();
|
||||||
|
existing.setId(88L); existing.setTenantId(10001L); existing.setQualificationType("BANK_ACCOUNT_LICENSE");
|
||||||
|
existing.setStatus("APPROVED");
|
||||||
|
existing.setDocumentUrl("/api/merchant/v1/files/qualifications/old-file.pdf");
|
||||||
|
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
||||||
|
when(repository.findQualificationsForUpdate(10001L)).thenReturn(List.of(existing));
|
||||||
|
|
||||||
|
var response = service.registerPlatformQualification(new PlatformQualificationRegistrationRequest(
|
||||||
|
10001L, "BANK_ACCOUNT_LICENSE", null, "/api/merchant/v1/files/qualifications/new-file.pdf",
|
||||||
|
"new-file.pdf", LocalDate.of(2031, 12, 31), true));
|
||||||
|
|
||||||
|
ArgumentCaptor<IamTenantQualification> captor = ArgumentCaptor.forClass(IamTenantQualification.class);
|
||||||
|
verify(repository, times(2)).saveQualification(captor.capture());
|
||||||
|
assertEquals("SUPERSEDED", existing.getStatus());
|
||||||
|
assertEquals("APPROVED", captor.getAllValues().get(1).getStatus());
|
||||||
|
assertEquals("ACTIVE", response.tenantStatus());
|
||||||
|
assertEquals(existing.getDocumentUrl(), response.replacedDocumentUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void platformCanReplaceAnApprovedBusinessLicenseForAnActiveTenant() {
|
||||||
|
IamTenant tenant = new IamTenant();
|
||||||
|
tenant.setId(10001L); tenant.setTenantType("MERCHANT"); tenant.setStatus("ACTIVE");
|
||||||
|
tenant.setUnifiedSocialCreditCode("91310101ABCDE12345");
|
||||||
|
IamTenantQualification existing = new IamTenantQualification();
|
||||||
|
existing.setId(88L); existing.setTenantId(10001L); existing.setQualificationType("BUSINESS_LICENSE");
|
||||||
|
existing.setStatus("APPROVED");
|
||||||
|
existing.setDocumentUrl("/api/merchant/v1/files/qualifications/old-license.pdf");
|
||||||
|
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
||||||
|
when(repository.findQualificationsForUpdate(10001L)).thenReturn(List.of(existing));
|
||||||
|
|
||||||
|
var response = service.registerPlatformRequiredQualificationPackage(new RequiredQualificationPackageRequest(
|
||||||
|
10001L, "91310101ABCDE12345", "/api/merchant/v1/files/qualifications/new-license.pdf",
|
||||||
|
"new-license.pdf", LocalDate.of(2031, 12, 31), true, null, null, null, null));
|
||||||
|
|
||||||
|
ArgumentCaptor<IamTenantQualification> captor = ArgumentCaptor.forClass(IamTenantQualification.class);
|
||||||
|
verify(repository, times(2)).saveQualification(captor.capture());
|
||||||
|
assertEquals("SUPERSEDED", existing.getStatus());
|
||||||
|
assertEquals("APPROVED", captor.getAllValues().get(1).getStatus());
|
||||||
|
assertEquals("ACTIVE", response.tenantStatus());
|
||||||
|
assertEquals(existing.getDocumentUrl(), response.replacedBusinessLicenseDocumentUrl());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ public class CustomerAddressService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Long create(long tenantId, long userId, CustomerAddressDtos.SaveRequest request) {
|
public Long create(long tenantId, long userId, CustomerAddressDtos.SaveRequest request) {
|
||||||
|
ensureNoDuplicate(tenantId, request, null);
|
||||||
CustomerAddressEntity value = new CustomerAddressEntity();
|
CustomerAddressEntity value = new CustomerAddressEntity();
|
||||||
value.setId(IdWorker.getId());
|
value.setId(IdWorker.getId());
|
||||||
value.setTenantId(tenantId);
|
value.setTenantId(tenantId);
|
||||||
@@ -51,6 +52,7 @@ public class CustomerAddressService {
|
|||||||
if (!value.getAddressType().equals(request.addressType())) {
|
if (!value.getAddressType().equals(request.addressType())) {
|
||||||
throw new BizException(ResultCode.VALIDATION_FAILED, "地址类型创建后不可修改");
|
throw new BizException(ResultCode.VALIDATION_FAILED, "地址类型创建后不可修改");
|
||||||
}
|
}
|
||||||
|
ensureNoDuplicate(tenantId, request, id);
|
||||||
if (request.defaultAddress()) clearDefault(tenantId, request.addressType(), id);
|
if (request.defaultAddress()) clearDefault(tenantId, request.addressType(), id);
|
||||||
apply(value, request);
|
apply(value, request);
|
||||||
value.setUpdatedBy(userId);
|
value.setUpdatedBy(userId);
|
||||||
@@ -99,6 +101,25 @@ public class CustomerAddressService {
|
|||||||
.set(CustomerAddressEntity::getDefaultAddress, false));
|
.set(CustomerAddressEntity::getDefaultAddress, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensureNoDuplicate(long tenantId, CustomerAddressDtos.SaveRequest request, Long excludedId) {
|
||||||
|
boolean duplicate = mapper.selectList(new LambdaQueryWrapper<CustomerAddressEntity>()
|
||||||
|
.eq(CustomerAddressEntity::getTenantId, tenantId)
|
||||||
|
.eq(CustomerAddressEntity::getActive, true)
|
||||||
|
.eq(CustomerAddressEntity::getAddressType, request.addressType())
|
||||||
|
.eq(CustomerAddressEntity::getCompanyName, request.companyName().trim())
|
||||||
|
.eq(CustomerAddressEntity::getContactName, request.contactName().trim())
|
||||||
|
.eq(CustomerAddressEntity::getPhone, request.phone().trim())
|
||||||
|
.eq(CustomerAddressEntity::getCountryCode, request.countryCode().trim().toUpperCase())
|
||||||
|
.eq(CustomerAddressEntity::getCity, request.city().trim())
|
||||||
|
.eq(CustomerAddressEntity::getAddressLine, request.addressLine().trim())
|
||||||
|
.ne(excludedId != null, CustomerAddressEntity::getId, excludedId))
|
||||||
|
.stream().findAny().isPresent();
|
||||||
|
if (duplicate) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "PICKUP".equals(request.addressType())
|
||||||
|
? "相同的企业提货地址已存在" : "相同的企业私人仓地址已存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void apply(CustomerAddressEntity value, CustomerAddressDtos.SaveRequest source) {
|
private static void apply(CustomerAddressEntity value, CustomerAddressDtos.SaveRequest source) {
|
||||||
value.setAddressType(source.addressType()); value.setAddressName(source.addressName().trim());
|
value.setAddressType(source.addressType()); value.setAddressName(source.addressName().trim());
|
||||||
value.setCompanyName(source.companyName().trim()); value.setContactName(source.contactName().trim());
|
value.setCompanyName(source.companyName().trim()); value.setContactName(source.contactName().trim());
|
||||||
|
|||||||
@@ -324,11 +324,11 @@ public class UnifiedOrderQueryService {
|
|||||||
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), null, null,
|
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), null, null,
|
||||||
order.getInboundCode(),
|
order.getInboundCode(),
|
||||||
order.getExpectedShipDate(), null, null, totalPackageCount(cargo), null, null,
|
order.getExpectedShipDate(), null, null, totalPackageCount(cargo), null, null,
|
||||||
order.getEstimatedVolumeCbm(), volumeCbm(cargo), null, null,
|
order.getEstimatedVolumeCbm(), volumeCbm(cargo), grossWeightKg(cargo), volumeCbm(cargo),
|
||||||
order.getExpectedArrivalAt(), null, null, null, order.getFulfillmentType(), order.getLxCode(),
|
order.getExpectedArrivalAt(), null, null, null, order.getFulfillmentType(), order.getLxCode(),
|
||||||
order.getPickupMethod(), order.getTaxMode(), order.getContainerMode(),
|
order.getPickupMethod(), order.getTaxMode(), order.getContainerMode(),
|
||||||
order.getCustomsInspection(), order.getDestinationType(), order.getAmazonWarehouseCode(),
|
order.getCustomsInspection(), order.getDestinationType(), order.getAmazonWarehouseCode(),
|
||||||
null, null, null, null, null, null, null, order.getExpressCompany(), order.getExpressTrackingNo(),
|
null, null, null, null, netWeightKg(cargo), null, null, order.getExpressCompany(), order.getExpressTrackingNo(),
|
||||||
StringUtils.hasText(creatorName) ? creatorName : string(order.getCreatedBy()), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo),
|
StringUtils.hasText(creatorName) ? creatorName : string(order.getCreatedBy()), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo),
|
||||||
null, null, null, null, null, coLoadGroup,
|
null, null, null, null, null, coLoadGroup,
|
||||||
order.getCustomerRemark(), latestInternalNote, order.getWarehouseRemark(), order.getCustomerServiceRemark(),
|
order.getCustomerRemark(), latestInternalNote, order.getWarehouseRemark(), order.getCustomerServiceRemark(),
|
||||||
@@ -348,13 +348,14 @@ public class UnifiedOrderQueryService {
|
|||||||
displayedPackageCount(order, cargo, inbound), null,
|
displayedPackageCount(order, cargo, inbound), null,
|
||||||
distinctLocationCodes(inbound.locationCodes()),
|
distinctLocationCodes(inbound.locationCodes()),
|
||||||
order.getEstimatedVolumeCbm(), volumeCbm(cargo),
|
order.getEstimatedVolumeCbm(), volumeCbm(cargo),
|
||||||
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(),
|
measuredOrDeclared(inbound.actualGrossWeightKg(), grossWeightKg(cargo)),
|
||||||
|
measuredOrDeclared(inbound.actualVolumeCbm(), volumeCbm(cargo)),
|
||||||
order.getExpectedArrivalAt() != null ? order.getExpectedArrivalAt() : inbound.expectedArrivalAt(), inbound.feeReviewStatus(),
|
order.getExpectedArrivalAt() != null ? order.getExpectedArrivalAt() : inbound.expectedArrivalAt(), inbound.feeReviewStatus(),
|
||||||
inbound.companyName(), inbound.businessType(), inbound.fulfillmentType(), inbound.lxCode(),
|
inbound.companyName(), inbound.businessType(), inbound.fulfillmentType(), inbound.lxCode(),
|
||||||
order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
|
order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
|
||||||
inbound.customsInspection(), inbound.destinationType(), inbound.destinationCode(), inbound.palletCount(),
|
inbound.customsInspection(), inbound.destinationType(), inbound.destinationCode(), inbound.palletCount(),
|
||||||
inbound.chargeableWeightKg(), inbound.chargeableVolumeCbm(), inbound.volumeWeightKg(),
|
inbound.chargeableWeightKg(), inbound.chargeableVolumeCbm(), inbound.volumeWeightKg(),
|
||||||
inbound.actualNetWeightKg(),
|
measuredOrDeclared(inbound.actualNetWeightKg(), netWeightKg(cargo)),
|
||||||
inbound.measureCompletedAt(), inbound.inboundedAt(), inbound.expressCompanyName(), inbound.expressTrackingNo(),
|
inbound.measureCompletedAt(), inbound.inboundedAt(), inbound.expressCompanyName(), inbound.expressTrackingNo(),
|
||||||
StringUtils.hasText(creatorName) ? creatorName : inbound.createdByName(), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo), measurement(inbound),
|
StringUtils.hasText(creatorName) ? creatorName : inbound.createdByName(), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo), measurement(inbound),
|
||||||
preload(summary.preload()), loading(summary.loading()),
|
preload(summary.preload()), loading(summary.loading()),
|
||||||
@@ -378,13 +379,14 @@ public class UnifiedOrderQueryService {
|
|||||||
inbound.id(), inbound.version(), inbound.inboundCode(), null, inbound.holdFlag(), inbound.currentHoldReason(),
|
inbound.id(), inbound.version(), inbound.inboundCode(), null, inbound.holdFlag(), inbound.currentHoldReason(),
|
||||||
displayedPackageCount(order, cargo, inbound), null, distinctLocationCodes(inbound.locationCodes()),
|
displayedPackageCount(order, cargo, inbound), null, distinctLocationCodes(inbound.locationCodes()),
|
||||||
order == null ? null : order.getEstimatedVolumeCbm(), volumeCbm(cargo),
|
order == null ? null : order.getEstimatedVolumeCbm(), volumeCbm(cargo),
|
||||||
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(),
|
measuredOrDeclared(inbound.actualGrossWeightKg(), grossWeightKg(cargo)),
|
||||||
|
measuredOrDeclared(inbound.actualVolumeCbm(), volumeCbm(cargo)),
|
||||||
order != null && order.getExpectedArrivalAt() != null ? order.getExpectedArrivalAt() : inbound.expectedArrivalAt(), inbound.feeReviewStatus(), inbound.companyName(),
|
order != null && order.getExpectedArrivalAt() != null ? order.getExpectedArrivalAt() : inbound.expectedArrivalAt(), inbound.feeReviewStatus(), inbound.companyName(),
|
||||||
inbound.businessType(), inbound.fulfillmentType(), inbound.lxCode(),
|
inbound.businessType(), inbound.fulfillmentType(), inbound.lxCode(),
|
||||||
order == null ? null : order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
|
order == null ? null : order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
|
||||||
inbound.customsInspection(), inbound.destinationType(), inbound.destinationCode(), inbound.palletCount(),
|
inbound.customsInspection(), inbound.destinationType(), inbound.destinationCode(), inbound.palletCount(),
|
||||||
inbound.chargeableWeightKg(), inbound.chargeableVolumeCbm(), inbound.volumeWeightKg(),
|
inbound.chargeableWeightKg(), inbound.chargeableVolumeCbm(), inbound.volumeWeightKg(),
|
||||||
inbound.actualNetWeightKg(),
|
measuredOrDeclared(inbound.actualNetWeightKg(), netWeightKg(cargo)),
|
||||||
inbound.measureCompletedAt(), inbound.inboundedAt(), inbound.expressCompanyName(), inbound.expressTrackingNo(),
|
inbound.measureCompletedAt(), inbound.inboundedAt(), inbound.expressCompanyName(), inbound.expressTrackingNo(),
|
||||||
inbound.createdByName(), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo),
|
inbound.createdByName(), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo),
|
||||||
measurement(inbound), preload(summary.preload()), loading(summary.loading()),
|
measurement(inbound), preload(summary.preload()), loading(summary.loading()),
|
||||||
@@ -490,6 +492,10 @@ public class UnifiedOrderQueryService {
|
|||||||
return cargo == null ? null : cargo.volumeCbm();
|
return cargo == null ? null : cargo.volumeCbm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BigDecimal measuredOrDeclared(BigDecimal measured, BigDecimal declared) {
|
||||||
|
return measured == null ? declared : measured;
|
||||||
|
}
|
||||||
|
|
||||||
private record CargoSummary(List<String> skuNos, List<String> shipmentIds, List<String> referenceIds,
|
private record CargoSummary(List<String> skuNos, List<String> shipmentIds, List<String> referenceIds,
|
||||||
Integer totalQuantity, Integer totalPackageCount, BigDecimal grossWeightKg,
|
Integer totalQuantity, Integer totalPackageCount, BigDecimal grossWeightKg,
|
||||||
BigDecimal netWeightKg, BigDecimal volumeCbm) {
|
BigDecimal netWeightKg, BigDecimal volumeCbm) {
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import com.baiyue.order.mapper.CustomerAddressMapper;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -38,4 +41,21 @@ class CustomerAddressServiceTest {
|
|||||||
assertThatThrownBy(() -> service.required(10001L, 90001L, "PICKUP"))
|
assertThatThrownBy(() -> service.required(10001L, 90001L, "PICKUP"))
|
||||||
.isInstanceOf(BizException.class).hasMessage("客户地址不存在或已停用");
|
.isInstanceOf(BizException.class).hasMessage("客户地址不存在或已停用");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsDuplicatePickupAddressBeforeCreatingAnotherRecord() {
|
||||||
|
CustomerAddressMapper mapper = mock(CustomerAddressMapper.class);
|
||||||
|
CustomerAddressEntity duplicate = new CustomerAddressEntity();
|
||||||
|
duplicate.setId(90001L);
|
||||||
|
when(mapper.selectList(any())).thenReturn(List.of(duplicate));
|
||||||
|
CustomerAddressService service = new CustomerAddressService();
|
||||||
|
ReflectionTestUtils.setField(service, "mapper", mapper);
|
||||||
|
var request = new CustomerAddressDtos.SaveRequest("PICKUP", "深圳工厂", "柏悦物流",
|
||||||
|
"惠万昌", "13229608341", null, "CN", "江西省", "抚州市", null,
|
||||||
|
"崇仁县三山乡长仁大队廉家村", null, false);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.create(10001L, 20001L, request))
|
||||||
|
.isInstanceOf(BizException.class).hasMessage("相同的企业提货地址已存在");
|
||||||
|
verify(mapper, never()).insert(any(CustomerAddressEntity.class));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class UnifiedOrderQueryServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void newlyCreatedOrderUsesItsOwnCargoPackageCountBeforeInboundExists() {
|
void newlyCreatedOrderUsesItsOwnCargoTotalsBeforeInboundExists() {
|
||||||
UnifiedOrderQuery query = query(null);
|
UnifiedOrderQuery query = query(null);
|
||||||
OrderEntity order = order(OrderStatus.APPROVED);
|
OrderEntity order = order(OrderStatus.APPROVED);
|
||||||
OrderCargoEntity cargo = new OrderCargoEntity();
|
OrderCargoEntity cargo = new OrderCargoEntity();
|
||||||
@@ -144,13 +144,39 @@ class UnifiedOrderQueryServiceTest {
|
|||||||
assertThat(service.page(10001L, query).records()).singleElement().satisfies(row -> {
|
assertThat(service.page(10001L, query).records()).singleElement().satisfies(row -> {
|
||||||
assertThat(row.totalPackageCount()).isEqualTo(100);
|
assertThat(row.totalPackageCount()).isEqualTo(100);
|
||||||
assertThat(row.expectedVolumeCbm()).isEqualByComparingTo("0.375");
|
assertThat(row.expectedVolumeCbm()).isEqualByComparingTo("0.375");
|
||||||
assertThat(row.actualGrossWeightKg()).isNull();
|
assertThat(row.actualGrossWeightKg()).isEqualByComparingTo("12.5");
|
||||||
assertThat(row.totalNetWeightKg()).isNull();
|
assertThat(row.totalNetWeightKg()).isEqualByComparingTo("11.2");
|
||||||
assertThat(row.actualVolumeCbm()).isNull();
|
assertThat(row.actualVolumeCbm()).isEqualByComparingTo("0.375");
|
||||||
});
|
});
|
||||||
verify(orders).findCargoByOrderIds(10001L, List.of(77L));
|
verify(orders).findCargoByOrderIds(10001L, List.of(77L));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unmeasuredInboundFallsBackToDeclaredCargoTotals() {
|
||||||
|
UnifiedOrderQuery query = query(null);
|
||||||
|
OrderEntity order = order(OrderStatus.APPROVED);
|
||||||
|
OrderCargoEntity cargo = new OrderCargoEntity();
|
||||||
|
cargo.setOrderId(77L);
|
||||||
|
cargo.setPackageCount(50);
|
||||||
|
cargo.setGrossWeightKg(new BigDecimal("1500"));
|
||||||
|
cargo.setNetWeightKg(new BigDecimal("25"));
|
||||||
|
cargo.setVolumeCbm(new BigDecimal("7.5"));
|
||||||
|
var summary = summary("WAIT_MEASURE", null);
|
||||||
|
when(summary.inbound().actualGrossWeightKg()).thenReturn(null);
|
||||||
|
when(summary.inbound().actualNetWeightKg()).thenReturn(null);
|
||||||
|
when(summary.inbound().actualVolumeCbm()).thenReturn(null);
|
||||||
|
when(orders.unifiedPage(eq(10001L), eq(query), any()))
|
||||||
|
.thenReturn(new PageResult<>(List.of(order), 1, 20, 1, 1));
|
||||||
|
when(firstleg.summaries(eq("internal-token"), any())).thenReturn(ApiResponse.success(List.of(summary)));
|
||||||
|
when(orders.findCargoByOrderIds(10001L, List.of(77L))).thenReturn(List.of(cargo));
|
||||||
|
|
||||||
|
assertThat(service.page(10001L, query).records()).singleElement().satisfies(row -> {
|
||||||
|
assertThat(row.actualGrossWeightKg()).isEqualByComparingTo("1500");
|
||||||
|
assertThat(row.totalNetWeightKg()).isEqualByComparingTo("25");
|
||||||
|
assertThat(row.actualVolumeCbm()).isEqualByComparingTo("7.5");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void dropshipMixedSkuRowsCountSharedCartonVolumeOnlyOnce() {
|
void dropshipMixedSkuRowsCountSharedCartonVolumeOnlyOnce() {
|
||||||
UnifiedOrderQuery query = query(null);
|
UnifiedOrderQuery query = query(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user