This commit is contained in:
2026-08-17 18:32:38 +08:00
parent 5c22018edd
commit 5199146e97
13 changed files with 194 additions and 17 deletions
@@ -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());
}
@@ -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',
@@ -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
View File
@@ -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)
+2 -2
View File
@@ -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.')