优化
This commit is contained in:
@@ -15154,7 +15154,7 @@ DELETE rp FROM iam_role_permission rp JOIN iam_permission p ON p.id=rp.permissio
|
||||
UPDATE iam_permission SET status='INACTIVE', deleted=1 WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code IN ('platform:finance:bill:view','platform:finance:account:view','platform:finance:fee-source:view','platform:finance:settlement-ar:view','platform:finance:settlement-ap:view');
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, r.id, p.id, CURRENT_TIMESTAMP(3) FROM iam_role r JOIN iam_permission p ON p.tenant_id=0 AND p.app_code='PLATFORM'
|
||||
WHERE r.tenant_id=0 AND r.app_code='PLATFORM' AND r.role_code='PLATFORM_ADMIN' AND p.permission_code IN ('platform:finance:customer-account:list','platform:finance:customer-arap:list','platform:finance:customer-statement:list','platform:finance:settlement-ar:list','platform:finance:settlement-ap:list','platform:finance:customer-account:export','platform:finance:customer-arap:export','platform:finance:customer-statement:export','platform:finance:settlement-ar:export','platform:finance:settlement-ap:export');
|
||||
WHERE r.tenant_id=0 AND r.app_code='PLATFORM' AND r.role_code='PLATFORM_ADMIN' AND p.permission_code IN ('platform:finance:customer-account:list','platform:finance:customer-arap:list','platform:finance:customer-statement:list','platform:finance:settlement-ar:list','platform:finance:settlement-ap:list','platform:finance:customer-account:export','platform:finance:customer-arap:export','platform:finance:customer-statement:export','platform:finance:settlement-ar:export','platform:finance:settlement-ap:export','platform:download:list');
|
||||
UPDATE iam_permission p JOIN (SELECT 'platform:finance:fee-source:review' code,5105041 parent_id UNION ALL SELECT 'platform:finance:fee-source:settlement',5105041 UNION ALL SELECT 'platform:finance:reconciliation:create',5105042 UNION ALL SELECT 'platform:finance:reconciliation:review',5105042 UNION ALL SELECT 'platform:finance:reconciliation:issue',5105042 UNION ALL SELECT 'platform:finance:reconciliation:adjust',5105042 UNION ALL SELECT 'platform:finance:reconciliation:void',5105042 UNION ALL SELECT 'platform:finance:settlement-ar:create',5105043 UNION ALL SELECT 'platform:finance:settlement-ap:create',5105044 UNION ALL SELECT 'platform:finance:payment:create',5105044) mapping ON mapping.code=p.permission_code SET p.parent_id=mapping.parent_id WHERE p.tenant_id=0 AND p.app_code='PLATFORM' AND p.permission_type='BUTTON';
|
||||
|
||||
INSERT INTO baiyue_iam.iam_permission
|
||||
@@ -15373,15 +15373,18 @@ DROP PROCEDURE IF EXISTS assert_finance_permissions;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE assert_finance_permissions()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 27 THEN
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 32 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:平台 Finance 权限不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=10001 AND app_code='MERCHANT' AND permission_code IN ('merchant:finance:statement:list','merchant:finance:statement:view') AND status='ACTIVE' AND deleted=0) <> 2 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:客户 Finance 只读权限不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000001 AND rp.tenant_id=0 AND (p.permission_code='platform:finance' OR p.permission_code LIKE 'platform:finance:%')) <> 28 THEN
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000001 AND rp.tenant_id=0 AND (p.permission_code='platform:finance' OR p.permission_code LIKE 'platform:finance:%')) <> 33 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:平台管理员 Finance 授权不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000001 AND rp.tenant_id=0 AND p.status='ACTIVE' AND p.deleted=0 AND p.permission_code IN ('platform:download:list','platform:finance:customer-account:export','platform:finance:customer-arap:export','platform:finance:customer-statement:export','platform:finance:settlement-ar:export','platform:finance:settlement-ap:export')) <> 6 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:平台管理员 Finance 导出和下载中心授权不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000101 AND rp.tenant_id=10001 AND (p.permission_code='merchant:finance:statement' OR p.permission_code LIKE 'merchant:finance:%')) <> 3 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:客户管理员 Finance 只读授权不完整';
|
||||
END IF;
|
||||
|
||||
@@ -48,6 +48,7 @@ public class FinanceExportProviderService {
|
||||
|
||||
private static final int SCHEMA_VERSION = 1;
|
||||
private static final int MAX_BATCH_SIZE = 500;
|
||||
private static final int QUERY_PAGE_SIZE = 200;
|
||||
private static final int MAX_SNAPSHOT_TOKEN_LENGTH = 512;
|
||||
private static final Set<String> TYPES = Set.of(CUSTOMER_ACCOUNT_LIST, CUSTOMER_ACCOUNT_LEDGER,
|
||||
FEE_SOURCE_LIST, RECONCILIATION_STATEMENT_LIST, RECEIVABLE_SETTLEMENT_LIST,
|
||||
@@ -148,13 +149,15 @@ public class FinanceExportProviderService {
|
||||
int limit = request.source().limit();
|
||||
Map<String, Object> p = request.parameters();
|
||||
if (CUSTOMER_ACCOUNT_LIST.equals(type)) {
|
||||
int page = offset / limit + 1;
|
||||
var result = accountService.list(page, limit, null, text(p, "status"), longValue(p, "customerId", false));
|
||||
int querySize = Math.min(limit, QUERY_PAGE_SIZE);
|
||||
int page = offset / querySize + 1;
|
||||
var result = accountService.list(page, querySize, null, text(p, "status"), longValue(p, "customerId", false));
|
||||
return new BatchRows(result.data().stream().map(this::accountRow).toList(), result.page().total());
|
||||
}
|
||||
if (CUSTOMER_ACCOUNT_LEDGER.equals(type)) {
|
||||
int page = offset / limit + 1;
|
||||
var result = operationService.listDerivedCustomerAccountLedger(longValue(p, "customerId", true), page, limit,
|
||||
int querySize = Math.min(limit, QUERY_PAGE_SIZE);
|
||||
int page = offset / querySize + 1;
|
||||
var result = operationService.listDerivedCustomerAccountLedger(longValue(p, "customerId", true), page, querySize,
|
||||
text(p, "businessType"), date(p, "startDate"), date(p, "endDate"));
|
||||
return new BatchRows(result.data().stream().map(this::ledgerRow).toList(), result.page().total());
|
||||
}
|
||||
|
||||
+23
-1
@@ -3,6 +3,7 @@ package com.baiyue.finance.service;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.core.export.ExportContracts.ExportBatchRequest;
|
||||
import com.baiyue.finance.dto.FinancePhase1Dtos.FeeSourceDetail;
|
||||
import com.baiyue.finance.dto.FinancePhase1Dtos.PageEnvelope;
|
||||
import com.baiyue.finance.dto.FinanceReadModels.Statement;
|
||||
import com.baiyue.finance.dto.ReconciliationStatementDtos.PaymentHistory;
|
||||
import com.baiyue.finance.dto.ReconciliationStatementDtos.ReconciliationDetail;
|
||||
@@ -23,6 +24,7 @@ import java.util.Map;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FinanceExportProviderServiceTest {
|
||||
@@ -87,7 +89,27 @@ class FinanceExportProviderServiceTest {
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("snapshotToken");
|
||||
}
|
||||
|
||||
@Test
|
||||
void capsFileBatchSizeToFinanceQueryPageLimitForAccountAndLedgerExports() {
|
||||
when(accountService.list(1, 200, null, null, 10001L))
|
||||
.thenReturn(PageEnvelope.success(List.of(), 1, 200, 0));
|
||||
when(operationService.listDerivedCustomerAccountLedger(10001L, 1, 200, null, null, null))
|
||||
.thenReturn(PageEnvelope.success(List.of(), 1, 200, 0));
|
||||
|
||||
service.exportBatch(FinanceExportProviderService.CUSTOMER_ACCOUNT_LIST,
|
||||
request(Map.of("customerId", "10001"), null, null, 500));
|
||||
service.exportBatch(FinanceExportProviderService.CUSTOMER_ACCOUNT_LEDGER,
|
||||
request(Map.of("customerId", "10001"), null, null, 500));
|
||||
|
||||
verify(accountService).list(1, 200, null, null, 10001L);
|
||||
verify(operationService).listDerivedCustomerAccountLedger(10001L, 1, 200, null, null, null);
|
||||
}
|
||||
|
||||
private ExportBatchRequest request(Map<String, Object> parameters, String cursor, String snapshot) {
|
||||
return new ExportBatchRequest("9001", "0", "PLATFORM", "7", 1, parameters, cursor, snapshot, 100);
|
||||
return request(parameters, cursor, snapshot, 100);
|
||||
}
|
||||
|
||||
private ExportBatchRequest request(Map<String, Object> parameters, String cursor, String snapshot, int limit) {
|
||||
return new ExportBatchRequest("9001", "0", "PLATFORM", "7", 1, parameters, cursor, snapshot, limit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,40 @@ public class FeeItemRepository {
|
||||
.set(InboundFeeDetailEntity::getUpdatedAt, now));
|
||||
}
|
||||
|
||||
public int supersedeFreight(long tenantId, long inboundId, long retainedSourceFeeId,
|
||||
long operatorId, LocalDateTime now) {
|
||||
return feeDetails.update(null, new LambdaUpdateWrapper<InboundFeeDetailEntity>()
|
||||
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
|
||||
.eq(InboundFeeDetailEntity::getInboundId, inboundId)
|
||||
.eq(InboundFeeDetailEntity::getSourceType, "FBA_PRICING")
|
||||
.in(InboundFeeDetailEntity::getPublicationStatus, List.of("DRAFT", "PUBLISHED"))
|
||||
.ne(InboundFeeDetailEntity::getSourceFeeId, retainedSourceFeeId)
|
||||
.set(InboundFeeDetailEntity::getPublicationStatus, "SUPERSEDED")
|
||||
.set(InboundFeeDetailEntity::getSettlementIncluded, false)
|
||||
.set(InboundFeeDetailEntity::getUpdatedBy, operatorId)
|
||||
.set(InboundFeeDetailEntity::getUpdatedAt, now));
|
||||
}
|
||||
|
||||
public int publishCalculatedFreight(long tenantId, long sourceFeeId, long operatorId,
|
||||
String operatorName, LocalDateTime now) {
|
||||
return feeDetails.update(null, new LambdaUpdateWrapper<InboundFeeDetailEntity>()
|
||||
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
|
||||
.eq(InboundFeeDetailEntity::getSourceType, "FBA_PRICING")
|
||||
.eq(InboundFeeDetailEntity::getSourceFeeId, sourceFeeId)
|
||||
.eq(InboundFeeDetailEntity::getPublicationStatus, "DRAFT")
|
||||
.set(InboundFeeDetailEntity::getReviewStatus, "APPROVED")
|
||||
.set(InboundFeeDetailEntity::getReviewerId, operatorId)
|
||||
.set(InboundFeeDetailEntity::getReviewerName, operatorName)
|
||||
.set(InboundFeeDetailEntity::getReviewedAt, now)
|
||||
.set(InboundFeeDetailEntity::getPublicationStatus, "PUBLISHED")
|
||||
.set(InboundFeeDetailEntity::getPublishedBy, operatorId)
|
||||
.set(InboundFeeDetailEntity::getPublishedByName, operatorName)
|
||||
.set(InboundFeeDetailEntity::getPublishedAt, now)
|
||||
.set(InboundFeeDetailEntity::getSettlementIncluded, true)
|
||||
.set(InboundFeeDetailEntity::getUpdatedBy, operatorId)
|
||||
.set(InboundFeeDetailEntity::getUpdatedAt, now));
|
||||
}
|
||||
|
||||
public void insertDetail(InboundFeeDetailEntity detail) {
|
||||
feeDetails.insert(detail);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ public class FbaPricingService {
|
||||
estimate.chargeUnit(), chargeable) != 1) {
|
||||
throw conflict("入库单状态已变化,费用试算未保存");
|
||||
}
|
||||
feeDetails.materializeCalculatedFreight(tenantId, inboundId, fee.getId(), operatorId, now);
|
||||
complete(token, fee.getId(), now);
|
||||
String nextStatus = isCompletedInbound(inbound.getProcessStatus())
|
||||
? inbound.getProcessStatus() : "FEE_CONFIRMING";
|
||||
@@ -215,6 +216,9 @@ public class FbaPricingService {
|
||||
feeDetails.publishFreight(tenantId, inboundId, adjusted.getId(), operatorId, null, now);
|
||||
financeOutbox.publishInbound(FinanceFeeSourceOutboxService.ADJUSTED,
|
||||
tenantId, inboundId, adjusted.getId(), nextVersion, now);
|
||||
} else {
|
||||
feeDetails.materializeCalculatedFreight(
|
||||
tenantId, inboundId, adjusted.getId(), operatorId, now);
|
||||
}
|
||||
complete(token, adjusted.getId(), now);
|
||||
return response(tenantId, adjusted.getId(), inbound.getProcessStatus());
|
||||
|
||||
@@ -48,7 +48,7 @@ public class InboundFeeDetailService {
|
||||
long operatorId, String operatorName,
|
||||
LocalDateTime publishedAt) {
|
||||
InboundFeeDetailEntity existing = repository.findBySourceFee(tenantId, "FBA_PRICING", feeId);
|
||||
if (existing != null) return existing;
|
||||
if (existing != null && "PUBLISHED".equals(existing.getPublicationStatus())) return existing;
|
||||
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
|
||||
InboundFeeEntity fee = pricingRepository.findFee(tenantId, feeId);
|
||||
if (fee == null || !Long.valueOf(inboundId).equals(fee.getInboundId())) {
|
||||
@@ -58,6 +58,14 @@ public class InboundFeeDetailService {
|
||||
throw conflict("只有已发送状态的计费快照才能生成客户运费明细");
|
||||
}
|
||||
LocalDateTime now = publishedAt == null ? LocalDateTime.now() : publishedAt;
|
||||
if (existing != null) {
|
||||
repository.supersedeFreight(tenantId, inboundId, feeId, operatorId, now);
|
||||
if (repository.publishCalculatedFreight(
|
||||
tenantId, feeId, operatorId, operatorName, now) != 1) {
|
||||
throw conflict("初算费用明细状态已变化,请刷新后重试");
|
||||
}
|
||||
return repository.findBySourceFee(tenantId, "FBA_PRICING", feeId);
|
||||
}
|
||||
FeeItemEntity feeItem = requiredFreightItem();
|
||||
InboundFeeItemEntity baseItem = pricingRepository.findBaseItem(tenantId, feeId);
|
||||
repository.supersedePublishedFreight(tenantId, inboundId, feeId, operatorId, now);
|
||||
@@ -121,6 +129,62 @@ public class InboundFeeDetailService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 初算成功后生成后台可见的草稿运费明细,不发布 Finance。 */
|
||||
@Transactional
|
||||
public InboundFeeDetailEntity materializeCalculatedFreight(long tenantId, long inboundId, long feeId,
|
||||
long operatorId, LocalDateTime calculatedAt) {
|
||||
InboundFeeDetailEntity existing = repository.findBySourceFee(tenantId, "FBA_PRICING", feeId);
|
||||
if (existing != null) return existing;
|
||||
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
|
||||
InboundFeeEntity fee = pricingRepository.findFee(tenantId, feeId);
|
||||
if (fee == null || !Long.valueOf(inboundId).equals(fee.getInboundId())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "待展示的入库计费快照不存在");
|
||||
}
|
||||
if (!"CALCULATED".equals(fee.getFeeStatus())) {
|
||||
throw conflict("只有初算状态的计费快照可生成草稿运费明细");
|
||||
}
|
||||
LocalDateTime now = calculatedAt == null ? LocalDateTime.now() : calculatedAt;
|
||||
repository.supersedeFreight(tenantId, inboundId, feeId, operatorId, now);
|
||||
InboundFeeDetailEntity detail = freightDetail(
|
||||
tenantId, inboundId, operatorId, null, now, inbound, fee, requiredFreightItem(), false);
|
||||
repository.insertDetail(detail);
|
||||
return detail;
|
||||
}
|
||||
|
||||
private InboundFeeDetailEntity freightDetail(long tenantId, long inboundId, long operatorId,
|
||||
String operatorName, LocalDateTime now,
|
||||
InboundOrderEntity inbound, InboundFeeEntity fee,
|
||||
FeeItemEntity feeItem, boolean published) {
|
||||
InboundFeeItemEntity baseItem = pricingRepository.findBaseItem(tenantId, fee.getId());
|
||||
InboundFeeDetailEntity detail = new InboundFeeDetailEntity();
|
||||
detail.setTenantId(tenantId); detail.setInboundId(inboundId); detail.setFeeItemId(feeItem.getId());
|
||||
detail.setSourceType("FBA_PRICING"); detail.setSourceFeeId(fee.getId());
|
||||
detail.setSourceFeeVersion(fee.getFeeVersion()); detail.setSourceFeeNo(fee.getFeeNo());
|
||||
detail.setInboundCode(inbound.getInboundCode()); detail.setDirection("RECEIVABLE");
|
||||
detail.setBusinessDate(now.toLocalDate()); detail.setCustomerId(inbound.getCustomerId());
|
||||
detail.setCustomerName(inbound.getCustomerName()); detail.setInvoiceTitle(inbound.getCompanyName());
|
||||
detail.setFeeCategorySnapshot(feeItem.getCategory()); detail.setTradeScopeSnapshot(feeItem.getTradeScope());
|
||||
detail.setFeeTypeNameSnapshot(feeItem.getFeeTypeName()); detail.setChannelCode(inbound.getChannelCode());
|
||||
detail.setChannelName(inbound.getChannelName()); detail.setDestinationCode(inbound.getDestinationCode());
|
||||
detail.setDestinationName(inbound.getDestinationName()); detail.setPackageCount(inbound.getTotalPackageCount());
|
||||
detail.setActualGrossWeightKg(fee.getActualWeightKg()); detail.setActualVolumeCbm(fee.getActualVolumeCbm());
|
||||
detail.setVolumeWeightKg(inbound.getVolumeWeightKg());
|
||||
detail.setChargeableWeightKg("KG".equals(fee.getChargeUnit()) ? fee.getChargeableQuantity() : inbound.getChargeableWeightKg());
|
||||
detail.setChargeableVolumeCbm("CBM".equals(fee.getChargeUnit()) ? fee.getChargeableQuantity() : inbound.getChargeableVolumeCbm());
|
||||
detail.setChargeableQuantity(fee.getChargeableQuantity()); detail.setChargeUnit(fee.getChargeUnit());
|
||||
detail.setUnitPrice(baseItem == null ? effectiveUnitPrice(fee) : baseItem.getUnitPrice());
|
||||
detail.setCurrency(fee.getCurrency()); detail.setAmount(fee.getTotalAmount().setScale(2, RoundingMode.HALF_UP));
|
||||
detail.setRemark("来源费用单:" + fee.getFeeNo());
|
||||
detail.setReviewStatus(published ? "APPROVED" : "PENDING");
|
||||
detail.setPublicationStatus(published ? "PUBLISHED" : "DRAFT");
|
||||
detail.setSettlementIncluded(published); detail.setCreatedBy(operatorId); detail.setUpdatedBy(operatorId);
|
||||
if (published) {
|
||||
detail.setReviewerId(operatorId); detail.setReviewerName(operatorName); detail.setReviewedAt(now);
|
||||
detail.setPublishedBy(operatorId); detail.setPublishedByName(operatorName); detail.setPublishedAt(now);
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundFeeDetailEntity create(long tenantId, long inboundId, long operatorId,
|
||||
SaveRequest request) {
|
||||
|
||||
@@ -143,6 +143,7 @@ class FbaPricingServiceTest {
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||
verify(repository).markInboundCalculated(TENANT, INBOUND, "WAIT_SPLIT", "KG",
|
||||
new BigDecimal("167.000"));
|
||||
verify(feeDetails).materializeCalculatedFreight(TENANT, INBOUND, 501L, OPERATOR, NOW);
|
||||
verify(financeOutbox, never()).publishInbound(any(), anyLong(), anyLong(), anyLong(), anyInt(), any());
|
||||
}
|
||||
|
||||
@@ -366,6 +367,7 @@ class FbaPricingServiceTest {
|
||||
assertThat(response.totalAmount()).isEqualByComparingTo("120.00");
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||
verify(repository).supersedeCurrent(TENANT, 501L, "CALCULATED");
|
||||
verify(feeDetails).materializeCalculatedFreight(TENANT, INBOUND, 503L, OPERATOR, NOW);
|
||||
verify(financeOutbox, never()).publishInbound(any(), anyLong(), anyLong(), anyLong(), anyInt(), any());
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ SELECT 0, r.id, p.id, CURRENT_TIMESTAMP(3)
|
||||
'platform:finance:settlement-ap:list','platform:finance:customer-account:export',
|
||||
'platform:finance:customer-arap:export','platform:finance:customer-statement:export',
|
||||
'platform:finance:settlement-ar:export','platform:finance:settlement-ap:export',
|
||||
'platform:download:list',
|
||||
'platform:firstleg:inbound:measure:view',
|
||||
'firstleg:inbound:measurement-review','platform:supplier:list',
|
||||
'platform:supplier:create','platform:supplier:edit','platform:supplier:status',
|
||||
|
||||
+15
-1
@@ -43,8 +43,22 @@ class AdminPrototypeMenuPermissionSqlContractTest {
|
||||
.doesNotContain("SELECT 'platform:finance:settlement-ap:view', 5105044")
|
||||
.contains("role_code='PLATFORM_ADMIN'");
|
||||
|
||||
int grantStart = sql.indexOf("role_code='PLATFORM_ADMIN'");
|
||||
String platformAdminGrant = sql.substring(grantStart, sql.indexOf(");", grantStart));
|
||||
for (String permission : FINANCE_EXPORTS) assertThat(platformAdminGrant).contains(permission);
|
||||
assertThat(platformAdminGrant).contains("platform:download:list");
|
||||
|
||||
String unified = Files.readString(resolveUnifiedSql(), StandardCharsets.UTF_8);
|
||||
for (String permission : FINANCE_EXPORTS) assertThat(unified).contains(permission);
|
||||
for (int index = 0; index < FINANCE_EXPORTS.length; index++) {
|
||||
assertThat(unified).contains("(" + (5105050 + index) + ",0,'PLATFORM','BUTTON'," + (5105040 + index) + ",'" + FINANCE_EXPORTS[index] + "'");
|
||||
}
|
||||
assertThat(unified)
|
||||
.contains("'platform:finance:settlement-ar:export','platform:finance:settlement-ap:export','platform:download:list'")
|
||||
.contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 32")
|
||||
.contains("(p.permission_code='platform:finance' OR p.permission_code LIKE 'platform:finance:%')) <> 33")
|
||||
.contains("Finance 导出和下载中心授权不完整")
|
||||
.satisfies(content -> assertThat(content.indexOf("(5105050,0,'PLATFORM','BUTTON'"))
|
||||
.isLessThan(content.indexOf("CREATE PROCEDURE assert_finance_permissions()")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ Dry-run 只检查根 `pom.xml` 和关键回归测试文件并打印 Maven 命令
|
||||
|
||||
### 本地 MySQL 全量验收
|
||||
|
||||
正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `1DBA47099AE501B21C4BEC22A50D065571BA46264C15C6C543CBE6F1CC597463` 和 15215 行,并静态确认中文注释升级采用校验入队、每表一次 ALTER、结构签名复核、九库覆盖断言及唯一最终 `PASS`。
|
||||
正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `00E8F9C24CE16FE6B4D2B6AF60460AE1AF1D264DFE110D4BDCE611F09EB69EAA` 和 15403 行,并静态确认中文注释升级采用校验入队、每表一次 ALTER、结构签名复核、九库覆盖断言及唯一最终 `PASS`。
|
||||
|
||||
```powershell
|
||||
.\scripts\local-db-full-acceptance.ps1 -DryRun
|
||||
|
||||
@@ -3,13 +3,18 @@ import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { EXPORT_SPECS, run } from './finance-downloads.mjs'
|
||||
import { EXPORT_SPECS, REQUIRED_PERMISSIONS, run } from './finance-downloads.mjs'
|
||||
import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs'
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const source = fs.readFileSync(path.join(here, 'finance-downloads.mjs'), 'utf8')
|
||||
|
||||
test('八类 Finance XLSX 契约均有正式表头并在完整链末尾执行', () => {
|
||||
assert.deepEqual(REQUIRED_PERMISSIONS, [
|
||||
'platform:download:list', 'platform:finance:customer-account:export',
|
||||
'platform:finance:customer-arap:export', 'platform:finance:customer-statement:export',
|
||||
'platform:finance:settlement-ar:export', 'platform:finance:settlement-ap:export',
|
||||
])
|
||||
assert.deepEqual(Object.keys(EXPORT_SPECS), [
|
||||
'FINANCE_CUSTOMER_ACCOUNT_LIST', 'FINANCE_CUSTOMER_ACCOUNT_LEDGER', 'FINANCE_FEE_SOURCE_LIST',
|
||||
'FINANCE_RECONCILIATION_STATEMENT_LIST', 'FINANCE_RECEIVABLE_SETTLEMENT_LIST',
|
||||
@@ -39,11 +44,17 @@ test('未部署时能力探测后记录部署前置,不借历史业务数据',
|
||||
const steps = []; const gaps = []
|
||||
const context = {
|
||||
marker: 'ADMIN-API-PROBE', createdResources: {},
|
||||
apiSuccess(module, name) { steps.push({ module, name, passed: false }); return null },
|
||||
apiSuccess(module, name, response) {
|
||||
const passed = response.http === 200 && String(response.body?.code) === '200'
|
||||
steps.push({ module, name, passed }); return passed ? response.body.data : null
|
||||
},
|
||||
assert(module, name) { steps.push({ module, name, passed: false }) },
|
||||
addStep(module, name, response, passed, evidence, options) { steps.push({ module, name, passed, evidence, options }) },
|
||||
addCoverageGap(...args) { gaps.push(args) },
|
||||
async request() { return { http: 200, body: { code: 'A0403', message: 'not deployed' } } },
|
||||
async request(method, path) {
|
||||
if (path.endsWith('/authorization/permissions')) return { http: 200, body: { code: '200', data: REQUIRED_PERMISSIONS } }
|
||||
return { http: 200, body: { code: 'A0403', message: 'not deployed' } }
|
||||
},
|
||||
}
|
||||
await run(context)
|
||||
assert.equal(context.createdResources.financeExportTasks.marker, context.marker)
|
||||
|
||||
@@ -3,6 +3,14 @@ import { inflateRawSync } from 'node:zlib'
|
||||
const MODULE = 'finance-downloads'
|
||||
const TERMINAL = new Set(['SUCCEEDED', 'FAILED', 'EXPIRED'])
|
||||
const OPTIONS = { scenario: 'finance-download-center', deploymentBlocker: true }
|
||||
export const REQUIRED_PERMISSIONS = Object.freeze([
|
||||
'platform:download:list',
|
||||
'platform:finance:customer-account:export',
|
||||
'platform:finance:customer-arap:export',
|
||||
'platform:finance:customer-statement:export',
|
||||
'platform:finance:settlement-ar:export',
|
||||
'platform:finance:settlement-ap:export',
|
||||
])
|
||||
|
||||
export const EXPORT_SPECS = Object.freeze({
|
||||
FINANCE_CUSTOMER_ACCOUNT_LIST: ['客户编码', '客户', '联系人', '联系电话', '待收(CNY)', '待开账(CNY)', '本期已收(CNY)', '待付(CNY)', '客户状态', '最近变动'],
|
||||
@@ -180,15 +188,26 @@ async function noPermission(context) {
|
||||
|
||||
export async function run(context) {
|
||||
context.createdResources.financeExportTasks = { marker: context.marker, tasks: {} }
|
||||
const permissionsResponse = await context.request('GET', '/api/platform/v1/iam/authorization/permissions')
|
||||
const permissionData = context.apiSuccess(MODULE, 'fresh login 权限快照', permissionsResponse, '', OPTIONS)
|
||||
const permissions = new Set(Array.isArray(permissionData) ? permissionData : [])
|
||||
const missingPermissions = REQUIRED_PERMISSIONS.filter((permission) => !permissions.has(permission))
|
||||
context.assert(MODULE, '下载中心与五项 Finance export 权限齐全', permissionsResponse,
|
||||
missingPermissions.length === 0, `missing=${missingPermissions.join(',') || 'none'}`, OPTIONS)
|
||||
if (missingPermissions.length > 0) {
|
||||
context.addCoverageGap(MODULE, 'finance-download-center', '八类 Finance 下载中心真实链',
|
||||
`fresh login 权限快照缺失:${missingPermissions.join(',')}`, 'deployment-prerequisite')
|
||||
return
|
||||
}
|
||||
const longMissingCustomerId = '9223372036854775806'
|
||||
const probe = await createTask(context, 'FINANCE_CUSTOMER_ACCOUNT_LIST',
|
||||
{ customerId: longMissingCustomerId }, 'CAPABILITY-PROBE', EXPORT_SPECS.FINANCE_CUSTOMER_ACCOUNT_LIST)
|
||||
context.createdResources.financeExportTasks.probe = probe
|
||||
if (!probe || probe.status !== 'SUCCEEDED') {
|
||||
context.addCoverageGap(MODULE, 'finance-download-center', '八类 Finance 下载中心真实链',
|
||||
'Finance/File/权限/配置尚未完整部署,能力探测未成功;部署后重跑完整链', 'deployment-prerequisite')
|
||||
return
|
||||
}
|
||||
context.createdResources.financeExportTasks.probe = probe
|
||||
context.assert(MODULE, '长 ID 字符串与空结果', null,
|
||||
probe.processedRows === 0, `customerIdType=string, customerId=${longMissingCustomerId}, rows=${probe.processedRows}`, OPTIONS)
|
||||
await noPermission(context)
|
||||
|
||||
@@ -11,8 +11,8 @@ $ErrorActionPreference = 'Stop'
|
||||
$Repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||
$Workspace = (Resolve-Path (Join-Path $Repo '..')).Path
|
||||
$Acceptance = Join-Path $Workspace '.codex-tmp\p23-04-acceptance'
|
||||
$ExpectedSha256 = '1DBA47099AE501B21C4BEC22A50D065571BA46264C15C6C543CBE6F1CC597463'
|
||||
$ExpectedLines = 15215
|
||||
$ExpectedSha256 = '00E8F9C24CE16FE6B4D2B6AF60460AE1AF1D264DFE110D4BDCE611F09EB69EAA'
|
||||
$ExpectedLines = 15403
|
||||
|
||||
if ($TargetHost -notin @('127.0.0.1', 'localhost')) {
|
||||
[Console]::Error.WriteLine('Refusing non-local database; only 127.0.0.1 or localhost is allowed.')
|
||||
|
||||
Reference in New Issue
Block a user