fujian_water_biz_doc/docs/superpowers/plans/2026-07-22-invoice-platform-account-config.md

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.java
  • sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/invoice/platform/mock/MockInvoicePlatformClient.java
  • sw-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_code
  • biz_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 platformCode and platformAccountCode fields to InvoiceDO.

  • 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 extraProperties with existing JsonUtils.toJsonString before 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 InvoicePlatformAccountResolver methods:
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.java or 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.createInvoice and updateInvoice, 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 platformClient injection 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() and route.platformAccount().platformAccountCode() to InvoiceRecordDO before 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 NuonuoInvoiceProperties only for stage smoke bootstrap when the test constructs an in-memory InvoicePlatformAccountConfig.

  • 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 NuonuoStageSmokeTest to 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 appKeySecretRef but never exposes appKey.

  • 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_KEY locally or platform-managed secret injection in deployed environments.