31 KiB
Invoice Platform Account Config Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a generic invoice platform account configuration layer so invoice business settings can route to Nuonuo now and to other suppliers later without storing platform secrets in the database.
Architecture: Keep business invoice rules in biz_invoice, add biz_invoice_platform_account for technical platform credentials and endpoint configuration, route platform calls through InvoicePlatformAccountResolver and InvoicePlatformClientRegistry, and pass a runtime InvoicePlatformAccountConfig into each platform client call.
Tech Stack: Spring Boot, MyBatis Plus, PostgreSQL DDL, existing BaseMapperX, existing BeanUtils and JsonUtils, JUnit 5, Mockito, optional Nuonuo stage smoke test gated by environment variables.
Starting Point
Execute this plan in water-backend. It assumes the Nuonuo client work from branch feat/nuonuo-stage-smoke is present, because that work already adds:
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/InvoicePlatformClient.javasw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/mock/MockInvoicePlatformClient.javasw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/nuonuo/**sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/invoice/platform/nuonuo/**biz_invoice_record.platform_codebiz_invoice_record.platform_account_code
Do not commit real platform keys. Nuonuo stage smoke uses NUONUO_STAGE_APP_KEY from the process environment.
Task 1: Add Data Model and Error Codes
Files:
-
Create
sql/rev005/REV005_invoice_platform_account_config.sql -
Modify
sw-business/sw-business-api/src/main/java/cn/com/emsoft/sw/business/enums/ErrorCodeConstants.java -
Create
sw-business/sw-business-api/src/main/java/cn/com/emsoft/sw/business/enums/invoice/InvoicePlatformCodeEnum.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/dataobject/invoiceplatform/InvoicePlatformAccountDO.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/mysql/invoiceplatform/InvoicePlatformAccountMapper.java -
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/dataobject/invoice/InvoiceDO.java -
Modify
sql/rev005/REV005_invoice_base_tables_ddl.sql -
Add failing compile target by referencing the new DO and mapper from a new service test stub created in Task 2. Run:
mvn -pl sw-business/sw-business-server -am -Dtest=InvoicePlatformAccountServiceImplTest test
Expected result before implementation: test compilation fails because the classes do not exist.
- Add invoice platform error codes after
INVOICE_SYS008_ERROR:
ErrorCode INVOICE_PLATFORM_ACCOUNT_NOT_EXISTS = new ErrorCode(1_025_030_031, "发票平台账号不存在");
ErrorCode INVOICE_PLATFORM_ACCOUNT_DUPLICATE = new ErrorCode(1_025_030_032, "发票平台账号编码重复");
ErrorCode INVOICE_PLATFORM_ACCOUNT_DISABLED = new ErrorCode(1_025_030_033, "发票平台账号已停用");
ErrorCode INVOICE_PLATFORM_ROUTE_MISSING = new ErrorCode(1_025_030_034, "缺少发票平台路由配置");
ErrorCode INVOICE_PLATFORM_ROUTE_AMBIGUOUS = new ErrorCode(1_025_030_035, "存在多个开票配置,请指定供应商");
ErrorCode INVOICE_PLATFORM_CLIENT_NOT_EXISTS = new ErrorCode(1_025_030_036, "发票平台客户端不存在");
ErrorCode INVOICE_PLATFORM_SECRET_MISSING = new ErrorCode(1_025_030_037, "发票平台密钥未配置");
ErrorCode INVOICE_PLATFORM_CONNECT_FAILED = new ErrorCode(1_025_030_038, "发票平台连通性测试失败");
- Add platform enum:
package cn.com.emsoft.sw.business.enums.invoice;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum InvoicePlatformCodeEnum {
MOCK("MOCK", "模拟平台"),
NUONUO("NUONUO", "诺诺"),
FANGXIN("FANGXIN", "方欣"),
BAIWANG("BAIWANG", "百望"),
HANGXIN("HANGXIN", "航信"),
YONGYOU("YONGYOU", "用友");
private final String code;
private final String description;
}
- Add DDL:
CREATE TABLE IF NOT EXISTS biz_invoice_platform_account (
id BIGINT NOT NULL,
account_id BIGINT NOT NULL,
platform_code VARCHAR(32) NOT NULL,
platform_account_code VARCHAR(64) NOT NULL,
account_name VARCHAR(100) NOT NULL,
base_url VARCHAR(255) NOT NULL,
version VARCHAR(32),
app_id VARCHAR(128) NOT NULL,
app_key_secret_ref VARCHAR(255) NOT NULL,
seller_taxnum VARCHAR(50) NOT NULL,
company_code VARCHAR(64),
callback_url VARCHAR(512),
default_buyer_email VARCHAR(100),
default_buyer_mobile VARCHAR(20),
extra_properties TEXT,
status SMALLINT NOT NULL DEFAULT 1,
tenant_id BIGINT,
creator VARCHAR(64),
create_time TIMESTAMP NOT NULL DEFAULT now(),
updater VARCHAR(64),
update_time TIMESTAMP NOT NULL DEFAULT now(),
deleted SMALLINT NOT NULL DEFAULT 0,
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_invoice_platform_account_code
ON biz_invoice_platform_account(account_id, platform_code, platform_account_code)
WHERE deleted = 0;
CREATE INDEX IF NOT EXISTS idx_invoice_platform_account_account
ON biz_invoice_platform_account(account_id, status)
WHERE deleted = 0;
ALTER TABLE biz_invoice ADD COLUMN IF NOT EXISTS platform_code VARCHAR(32);
ALTER TABLE biz_invoice ADD COLUMN IF NOT EXISTS platform_account_code VARCHAR(64);
CREATE INDEX IF NOT EXISTS idx_biz_invoice_platform_route
ON biz_invoice(account_id, supplier, invoice_type, platform_code, platform_account_code)
WHERE deleted = 0;
- Add DO:
@TableName("biz_invoice_platform_account")
@KeySequence("biz_invoice_platform_account_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@GetterRef
public class InvoicePlatformAccountDO extends TenantBaseDO {
@TableId
private Long id;
private Long accountId;
private String platformCode;
private String platformAccountCode;
private String accountName;
private String baseUrl;
private String version;
private String appId;
private String appKeySecretRef;
private String sellerTaxnum;
private String companyCode;
private String callbackUrl;
private String defaultBuyerEmail;
private String defaultBuyerMobile;
private String extraProperties;
private Short status;
}
- Add mapper methods:
@Mapper
public interface InvoicePlatformAccountMapper extends BaseMapperX<InvoicePlatformAccountDO> {
default InvoicePlatformAccountDO selectByRoute(Long accountId, String platformCode, String platformAccountCode) {
return selectOne(InvoicePlatformAccountDO::getAccountId, accountId,
InvoicePlatformAccountDO::getPlatformCode, platformCode,
InvoicePlatformAccountDO::getPlatformAccountCode, platformAccountCode);
}
default PageResult<InvoicePlatformAccountDO> selectPage(InvoicePlatformAccountPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<InvoicePlatformAccountDO>()
.eqIfPresent(InvoicePlatformAccountDO::getAccountId, reqVO.getAccountId())
.eqIfPresent(InvoicePlatformAccountDO::getPlatformCode, reqVO.getPlatformCode())
.eqIfPresent(InvoicePlatformAccountDO::getStatus, reqVO.getStatus())
.orderByDesc(InvoicePlatformAccountDO::getUpdateTime));
}
}
-
Add
platformCodeandplatformAccountCodefields toInvoiceDO. -
Verify compile for API and server modules:
mvn -pl sw-business/sw-business-server -am -DskipTests compile
Task 2: Add Platform Account Admin API
Files:
-
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/invoiceplatform/InvoicePlatformAccountController.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/invoiceplatform/vo/InvoicePlatformAccountBaseVO.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/invoiceplatform/vo/InvoicePlatformAccountCreateReqVO.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/invoiceplatform/vo/InvoicePlatformAccountUpdateReqVO.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/invoiceplatform/vo/InvoicePlatformAccountPageReqVO.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/invoiceplatform/vo/InvoicePlatformAccountRespVO.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/invoiceplatform/vo/InvoicePlatformAccountTestReqVO.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/invoiceplatform/vo/InvoicePlatformAccountTestRespVO.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoiceplatform/InvoicePlatformAccountService.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoiceplatform/InvoicePlatformAccountServiceImpl.java -
Create
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/invoiceplatform/InvoicePlatformAccountServiceImplTest.java -
Write failing service tests first:
@ExtendWith(MockitoExtension.class)
class InvoicePlatformAccountServiceImplTest {
@Mock
private InvoicePlatformAccountMapper accountMapper;
@Mock
private CompanyAccountService companyAccountService;
@InjectMocks
private InvoicePlatformAccountServiceImpl service;
@Test
void createAccount_shouldRejectDuplicateRoute() {
InvoicePlatformAccountCreateReqVO req = new InvoicePlatformAccountCreateReqVO();
req.setAccountId(3001L);
req.setPlatformCode("NUONUO");
req.setPlatformAccountCode("NUONUO_STAGE_339901999999142");
req.setAccountName("诺诺测试账号");
req.setBaseUrl("https://cmp-stage.nntest.cn");
req.setAppId("convert");
req.setAppKeySecretRef("env:NUONUO_STAGE_APP_KEY");
req.setSellerTaxnum("339901999999142");
req.setStatus((short) 1);
when(companyAccountService.getCompanyAccount(3001L)).thenReturn(new CompanyAccountDO());
when(accountMapper.selectByRoute(3001L, "NUONUO", "NUONUO_STAGE_339901999999142"))
.thenReturn(InvoicePlatformAccountDO.builder().id(1L).build());
assertThrows(ServiceException.class, () -> service.createAccount(req));
}
}
- Add base VO with validation:
@Data
public class InvoicePlatformAccountBaseVO {
@NotNull
private Long accountId;
@NotBlank
private String platformCode;
@NotBlank
private String platformAccountCode;
@NotBlank
private String accountName;
@NotBlank
private String baseUrl;
private String version;
@NotBlank
private String appId;
@NotBlank
private String appKeySecretRef;
@NotBlank
private String sellerTaxnum;
private String companyCode;
private String callbackUrl;
private String defaultBuyerEmail;
private String defaultBuyerMobile;
private Map<String, Object> extraProperties;
@NotNull
private Short status;
}
- Implement service methods:
Long createAccount(InvoicePlatformAccountCreateReqVO reqVO);
void updateAccount(InvoicePlatformAccountUpdateReqVO reqVO);
void deleteAccount(Long id);
InvoicePlatformAccountDO getAccount(Long id);
PageResult<InvoicePlatformAccountDO> getAccountPage(InvoicePlatformAccountPageReqVO pageReqVO);
InvoicePlatformAccountDO getEnabledByRoute(Long accountId, String platformCode, String platformAccountCode);
-
Serialize
extraPropertieswith existingJsonUtils.toJsonStringbefore insert or update, and parse it back in response conversion. -
Implement controller endpoints:
POST /business/invoice-platform-account/create
PUT /business/invoice-platform-account/update
DELETE /business/invoice-platform-account/delete?id=
GET /business/invoice-platform-account/get?id=
GET /business/invoice-platform-account/page
POST /business/invoice-platform-account/test-connect
- Use permissions:
business:invoice-platform-account:create
business:invoice-platform-account:update
business:invoice-platform-account:delete
business:invoice-platform-account:query
business:invoice-platform-account:test-connect
- Run:
mvn -pl sw-business/sw-business-server -am -Dtest=InvoicePlatformAccountServiceImplTest test
Task 3: Add Runtime Config and Secret Resolution
Files:
-
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/config/InvoicePlatformAccountConfig.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/config/InvoicePlatformSecretProvider.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/config/EnvAndPropertyInvoicePlatformSecretProvider.java -
Create
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/invoice/platform/config/EnvAndPropertyInvoicePlatformSecretProviderTest.java -
Write failing tests for secret resolution:
@Test
void resolve_shouldReadSpringPropertyForNacosReference() {
MockEnvironment environment = new MockEnvironment()
.withProperty("invoice.nuonuo.stage.app-key", "0123456789abcdef");
EnvAndPropertyInvoicePlatformSecretProvider provider =
new EnvAndPropertyInvoicePlatformSecretProvider(environment);
assertEquals("0123456789abcdef", provider.resolve("nacos:invoice.nuonuo.stage.app-key"));
}
@Test
void resolve_shouldRejectMissingSecret() {
EnvAndPropertyInvoicePlatformSecretProvider provider =
new EnvAndPropertyInvoicePlatformSecretProvider(new MockEnvironment());
assertThrows(ServiceException.class, () -> provider.resolve("nacos:invoice.missing"));
}
- Add runtime config record:
public record InvoicePlatformAccountConfig(
Long id,
Long accountId,
String platformCode,
String platformAccountCode,
String accountName,
String baseUrl,
String version,
String appId,
String appKey,
String sellerTaxnum,
String companyCode,
String callbackUrl,
String defaultBuyerEmail,
String defaultBuyerMobile,
Map<String, Object> extraProperties
) {}
- Implement provider behavior:
@Component
public class EnvAndPropertyInvoicePlatformSecretProvider implements InvoicePlatformSecretProvider {
private final Environment environment;
public String resolve(String secretRef) {
String value;
if (secretRef.startsWith("env:")) {
value = System.getenv(secretRef.substring("env:".length()));
} else if (secretRef.startsWith("nacos:")) {
value = environment.getProperty(secretRef.substring("nacos:".length()));
} else {
value = environment.getProperty(secretRef);
}
if (StrUtil.isBlank(value)) {
throw exception(INVOICE_PLATFORM_SECRET_MISSING);
}
return value;
}
}
- Verify:
mvn -pl sw-business/sw-business-server -am -Dtest=EnvAndPropertyInvoicePlatformSecretProviderTest test
Task 4: Add Resolver and Client Registry
Files:
-
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/routing/InvoicePlatformClientRegistry.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/routing/InvoicePlatformAccountResolver.java -
Create
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/routing/InvoiceRouteContext.java -
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/InvoicePlatformClient.java -
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/mock/MockInvoicePlatformClient.java -
Create
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/invoice/platform/routing/InvoicePlatformClientRegistryTest.java -
Create
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/invoice/platform/routing/InvoicePlatformAccountResolverTest.java -
Write failing registry tests:
@Test
void getRequired_shouldReturnClientByUppercasePlatformCode() {
InvoicePlatformClient client = mock(InvoicePlatformClient.class);
when(client.platformCode()).thenReturn("NUONUO");
InvoicePlatformClientRegistry registry = new InvoicePlatformClientRegistry(List.of(client));
assertSame(client, registry.getRequired("nuonuo"));
}
@Test
void constructor_shouldRejectDuplicatePlatformCode() {
InvoicePlatformClient left = mock(InvoicePlatformClient.class);
InvoicePlatformClient right = mock(InvoicePlatformClient.class);
when(left.platformCode()).thenReturn("NUONUO");
when(right.platformCode()).thenReturn("nuonuo");
assertThrows(ServiceException.class, () -> new InvoicePlatformClientRegistry(List.of(left, right)));
}
- Extend client contract:
public interface InvoicePlatformClient {
String platformCode();
ApplyResult apply(ApplyContext ctx);
QueryResult query(QueryContext ctx);
InvalidateResult invalidate(PostProcessContext ctx);
RedInkResult redInk(PostProcessContext ctx);
record ApplyContext(
String applicationNo,
Long accountId,
String invoiceTitle,
String taxNo,
String invoiceType,
BigDecimal totalAmount,
String email,
String mobile,
InvoicePlatformAccountConfig platformAccount
) {}
}
Apply the same InvoicePlatformAccountConfig platformAccount addition to QueryContext and PostProcessContext.
- Implement
InvoicePlatformAccountResolvermethods:
InvoiceRouteContext resolveForApply(Long accountId, Short invoiceType, Integer supplier);
InvoicePlatformAccountConfig resolveByRecord(Long accountId, String platformCode, String platformAccountCode);
InvoicePlatformAccountConfig resolveByAccount(InvoicePlatformAccountDO accountDO);
- Resolver rules:
resolveForApply:
1. Query enabled biz_invoice rows by accountId and invoiceType.
2. If supplier is present, filter by supplier.
3. If no row remains, throw INVOICE_PLATFORM_ROUTE_MISSING.
4. If more than one row remains, throw INVOICE_PLATFORM_ROUTE_AMBIGUOUS.
5. Read enabled platform account by platform_code and platform_account_code.
6. Resolve appKey through InvoicePlatformSecretProvider.
7. Return InvoiceRouteContext with business config and runtime account config.
resolveByRecord:
1. Require platform_code and platform_account_code from biz_invoice_record.
2. Read enabled platform account by route.
3. Resolve appKey through InvoicePlatformSecretProvider.
- Run:
mvn -pl sw-business/sw-business-server -am -Dtest='InvoicePlatformClientRegistryTest,InvoicePlatformAccountResolverTest' test
Task 5: Bind Business Invoice Config to Platform Account
Files:
-
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/invoice/vo/InvoiceCreateReqVO.java -
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/invoice/vo/InvoiceSaveReqVO.java -
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/invoice/vo/InvoiceRespVO.java -
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/invoice/vo/InvoicePageRespVO.java -
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/InvoiceServiceImpl.java -
Modify
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/invoice/InvoiceServiceImplTest.javaor create it if the file does not exist. -
Add fields to create/update/response VO:
@Schema(description = "发票平台编码,例如 NUONUO")
private String platformCode;
@Schema(description = "发票平台账号编码")
private String platformAccountCode;
- In
InvoiceServiceImpl.createInvoiceandupdateInvoice, validate route when either platform field is present:
if (StrUtil.isNotBlank(reqVO.getPlatformCode()) || StrUtil.isNotBlank(reqVO.getPlatformAccountCode())) {
invoicePlatformAccountService.getEnabledByRoute(
reqVO.getAccountId(), reqVO.getPlatformCode(), reqVO.getPlatformAccountCode());
}
-
Require both route fields together. If only one is present, throw
INVOICE_PLATFORM_ROUTE_MISSING. -
Keep the existing upsert key
accountId + supplier, because it preserves the current management API behavior. -
Run:
mvn -pl sw-business/sw-business-server -am -Dtest=InvoiceServiceImplTest test
Task 6: Route Invoice Lifecycle Through Platform Account
Files:
-
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/invoice/vo/InvoiceApplyReqVO.java -
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/InvoiceRecordServiceImpl.java -
Modify
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/invoice/InvoiceRecordServiceImplTest.java -
Add optional supplier to invoice apply request:
@Schema(description = "供应商模板编码,存在多个开票配置时必填")
private Integer supplier;
- Replace direct
InvoicePlatformClient platformClientinjection with:
@Resource
private InvoicePlatformAccountResolver invoicePlatformAccountResolver;
@Resource
private InvoicePlatformClientRegistry invoicePlatformClientRegistry;
- Update apply flow:
InvoiceRouteContext route = invoicePlatformAccountResolver.resolveForApply(
accountId, reqVO.getInvoiceType(), reqVO.getSupplier());
InvoicePlatformClient client = invoicePlatformClientRegistry.getRequired(
route.platformAccount().platformCode());
ApplyResult platformResult = client.apply(new ApplyContext(
applicationNo,
accountId,
reqVO.getInvoiceTitle(),
reqVO.getTaxNo(),
String.valueOf(reqVO.getInvoiceType()),
totalAmount,
reqVO.getEmail(),
reqVO.getMobile(),
route.platformAccount()));
-
Persist
route.platformAccount().platformCode()androute.platformAccount().platformAccountCode()toInvoiceRecordDObefore returning the apply response. -
Update query, invalidate, and red-ink flow to resolve platform account from the invoice record:
InvoicePlatformAccountConfig account = invoicePlatformAccountResolver.resolveByRecord(
record.getAccountId(), record.getPlatformCode(), record.getPlatformAccountCode());
InvoicePlatformClient client = invoicePlatformClientRegistry.getRequired(account.platformCode());
- Add tests:
applyInvoice_shouldWritePlatformRouteToRecord
applyInvoice_shouldRejectAmbiguousSupplierRoute
queryInvoice_shouldUseRecordPlatformRoute
invalidateInvoice_shouldUseRecordPlatformRoute
redInkInvoice_shouldUseRecordPlatformRoute
- Run:
mvn -pl sw-business/sw-business-server -am -Dtest=InvoiceRecordServiceImplTest test
Task 7: Adapt Nuonuo Client to Runtime Account Config
Files:
-
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/nuonuo/NuonuoInvoicePlatformClient.java -
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/nuonuo/NuonuoTransportClient.java -
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/nuonuo/NuonuoInvoiceProperties.java -
Modify
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/invoice/platform/nuonuo/NuonuoInvoicePlatformClientTest.java -
Modify
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/invoice/platform/nuonuo/NuonuoTransportClientTest.java -
Modify
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/invoice/platform/nuonuo/NuonuoStageSmokeTest.java -
Add
platformCode():
@Override
public String platformCode() {
return "NUONUO";
}
- Stop choosing Nuonuo account from static account map in normal calls. Use
ctx.platformAccount():
InvoicePlatformAccountConfig account = ctx.platformAccount();
NuonuoEnvelope envelope = NuonuoEnvelope.of(
account.version(),
account.appId(),
requestIdGenerator.next(),
cryptoService.encrypt(payloadJson, account.appKey()));
NuonuoEnvelopeResponse response = transportClient.post(account.baseUrl(), endpoint, envelope);
-
Keep
NuonuoInvoicePropertiesonly for stage smoke bootstrap when the test constructs an in-memoryInvoicePlatformAccountConfig. -
Update client tests so they assert:
baseUrl comes from InvoicePlatformAccountConfig
appId comes from InvoicePlatformAccountConfig
appKey comes from InvoicePlatformAccountConfig
sellerTaxnum comes from InvoicePlatformAccountConfig
platformCode result remains NUONUO
- Update
NuonuoStageSmokeTestto create runtime config:
private InvoicePlatformAccountConfig stageAccount() {
return new InvoicePlatformAccountConfig(
1L,
1L,
"NUONUO",
"NUONUO_STAGE_339901999999142",
"诺诺测试账号",
"https://cmp-stage.nntest.cn",
"1.0.0",
"convert",
System.getenv("NUONUO_STAGE_APP_KEY"),
"339901999999142",
null,
null,
null,
null,
Map.of());
}
- Run:
mvn -pl sw-business/sw-business-server -am -Dtest='NuonuoInvoicePlatformClientTest,NuonuoTransportClientTest,NuonuoResponseMapperTest,NuonuoStageSmokeTest' test
Expected: NuonuoStageSmokeTest is skipped unless NUONUO_STAGE_SMOKE_ENABLED=true.
Task 8: Add Platform Account Test-Connect Operation
Files:
-
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoiceplatform/InvoicePlatformAccountService.java -
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoiceplatform/InvoicePlatformAccountServiceImpl.java -
Modify
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/invoiceplatform/InvoicePlatformAccountController.java -
Modify
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/invoiceplatform/InvoicePlatformAccountServiceImplTest.java -
Add service method:
InvoicePlatformAccountTestRespVO testConnect(InvoicePlatformAccountTestReqVO reqVO);
- Implement by resolving account config and calling the platform client's query endpoint with a harmless nonexistent
applicationNo:
InvoicePlatformAccountConfig account = resolveRuntimeConfig(accountDO);
InvoicePlatformClient client = invoicePlatformClientRegistry.getRequired(account.platformCode());
QueryResult result = client.query(new QueryContext(
null,
account.accountId(),
"CONNECT-" + System.currentTimeMillis(),
account.platformCode(),
account.platformAccountCode(),
account));
return new InvoicePlatformAccountTestRespVO(result.success(), result.errorMsg());
-
Treat successful transport with empty query result as connected. For Nuonuo stage, decrypted
[]is a valid connected response. -
Add tests:
testConnect_shouldReturnSuccessForEmptyNuonuoQuery
testConnect_shouldReturnFailureWhenClientThrowsException
testConnect_shouldNotExposeResolvedAppKey
- Run:
mvn -pl sw-business/sw-business-server -am -Dtest=InvoicePlatformAccountServiceImplTest test
Task 9: Controller Contract and Integration Coverage
Files:
-
Modify
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/controller/admin/invoice/InvoiceControllerContractTest.java -
Create
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/controller/admin/invoiceplatform/InvoicePlatformAccountControllerContractTest.java -
Add contract tests for new endpoints:
POST /business/invoice-platform-account/create validates required fields
GET /business/invoice-platform-account/page exposes platformCode and masks secret value
POST /business/invoice-platform-account/test-connect returns connected flag and message
POST /business/invoice/apply accepts optional supplier
-
Ensure response VO exposes
appKeySecretRefbut never exposesappKey. -
Run:
mvn -pl sw-business/sw-business-server -am -Dtest='InvoiceControllerContractTest,InvoicePlatformAccountControllerContractTest' test
Task 10: Full Verification
Files:
-
No source files beyond previous tasks.
-
Run focused unit and contract tests:
mvn -pl sw-business/sw-business-server -am -Dtest='InvoicePlatformAccountServiceImplTest,InvoicePlatformClientRegistryTest,InvoicePlatformAccountResolverTest,EnvAndPropertyInvoicePlatformSecretProviderTest,InvoiceRecordServiceImplTest,InvoiceServiceImplTest,NuonuoInvoicePlatformClientTest,NuonuoTransportClientTest,NuonuoResponseMapperTest,InvoiceControllerContractTest,InvoicePlatformAccountControllerContractTest' test
- Run existing invoice regression:
mvn -pl sw-business/sw-business-server -am -Dtest='InvoiceRecordServiceImplTest,Nuonuo*Test,InvoiceControllerContractTest' test
- Run compile for all affected modules:
mvn -pl sw-business/sw-business-server -am -DskipTests compile
- Run optional Nuonuo stage smoke:
Before running this command, export NUONUO_STAGE_APP_KEY in the shell from the approved secret source.
NUONUO_STAGE_SMOKE_ENABLED=true \
mvn -pl sw-business/sw-business-server -am -Dtest=NuonuoStageSmokeTest test
- Verify the real key is not in tracked files:
rg -n "happy\\^_\\^everyday" -S .
Expected: no matches.
- Check whitespace:
git diff --check
Commit Guidance
- Commit implementation after Task 10 passes.
- Suggested backend commit message:
feat(invoice): add platform account routing config
- Keep the Nuonuo stage key out of Git. Use
NUONUO_STAGE_APP_KEYlocally or platform-managed secret injection in deployed environments.