Compare commits
No commits in common. "b614332e78d2f02125468c602cc91652bc0d96b3" and "55b9e144efd1151b33cb254623ab17bd08a9160f" have entirely different histories.
b614332e78
...
55b9e144ef
@ -1,65 +0,0 @@
|
|||||||
# fastjson 2.0.63 升级验证记录
|
|
||||||
|
|
||||||
日期:2026-07-29
|
|
||||||
|
|
||||||
## 背景
|
|
||||||
|
|
||||||
后端工程当前通过 `water-backend/sw-dependencies/pom.xml` 统一管理 `com.alibaba:fastjson` 版本。本次按要求将 `fastjson` 从 `2.0.62` 升级到 `2.0.63`,继续使用 `com.alibaba:fastjson` 的 fastjson2 兼容包,保持现有 `com.alibaba.fastjson.*` 调用方式不变。
|
|
||||||
|
|
||||||
## 变更
|
|
||||||
|
|
||||||
修改文件:
|
|
||||||
|
|
||||||
- `water-backend/sw-dependencies/pom.xml`
|
|
||||||
|
|
||||||
版本属性:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<fastjson.version>2.0.63</fastjson.version>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 验证
|
|
||||||
|
|
||||||
执行 fastjson 兼容性单测:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mvn -pl sw-framework/sw-common -am -Dtest=FastjsonCompatibilityTest test
|
|
||||||
```
|
|
||||||
|
|
||||||
结果:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
|
|
||||||
BUILD SUCCESS
|
|
||||||
```
|
|
||||||
|
|
||||||
执行依赖树确认:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mvn -pl sw-framework/sw-common -Dincludes=com.alibaba:fastjson dependency:tree
|
|
||||||
```
|
|
||||||
|
|
||||||
结果:
|
|
||||||
|
|
||||||
```text
|
|
||||||
com.alibaba:fastjson:jar:2.0.63:compile
|
|
||||||
BUILD SUCCESS
|
|
||||||
```
|
|
||||||
|
|
||||||
执行 diff 空白检查:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git diff --check -- sw-dependencies/pom.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
结果:无输出,命令退出码为 0。
|
|
||||||
|
|
||||||
## 已知提示
|
|
||||||
|
|
||||||
验证过程中 Maven 仍输出既有工程模型 warning,包括:
|
|
||||||
|
|
||||||
- `sw-module-iot-biz` 中 `sw-spring-boot-starter-biz-tenant` 依赖重复声明。
|
|
||||||
- `sw-module-iot-plugin-mqtt` 中 `maven-deploy-plugin` 缺少版本声明。
|
|
||||||
- `sw-business-server`、`sw-business-bank-server` 中 `spring-boot-maven-plugin` 缺少版本声明。
|
|
||||||
|
|
||||||
上述 warning 与本次 `fastjson` 版本升级无关,本次未修改这些模块。
|
|
||||||
@ -1,803 +0,0 @@
|
|||||||
# 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:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
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:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@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:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
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:
|
|
||||||
|
|
||||||
```text
|
|
||||||
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:
|
|
||||||
|
|
||||||
```text
|
|
||||||
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:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@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:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
InvoiceRouteContext resolveForApply(Long accountId, Short invoiceType, Integer supplier);
|
|
||||||
InvoicePlatformAccountConfig resolveByRecord(Long accountId, String platformCode, String platformAccountCode);
|
|
||||||
InvoicePlatformAccountConfig resolveByAccount(InvoicePlatformAccountDO accountDO);
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Resolver rules:
|
|
||||||
|
|
||||||
```text
|
|
||||||
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:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Schema(description = "发票平台编码,例如 NUONUO")
|
|
||||||
private String platformCode;
|
|
||||||
|
|
||||||
@Schema(description = "发票平台账号编码")
|
|
||||||
private String platformAccountCode;
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] In `InvoiceServiceImpl.createInvoice` and `updateInvoice`, validate route when either platform field is present:
|
|
||||||
|
|
||||||
```java
|
|
||||||
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:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Schema(description = "供应商模板编码,存在多个开票配置时必填")
|
|
||||||
private Integer supplier;
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Replace direct `InvoicePlatformClient platformClient` injection with:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Resource
|
|
||||||
private InvoicePlatformAccountResolver invoicePlatformAccountResolver;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private InvoicePlatformClientRegistry invoicePlatformClientRegistry;
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Update apply flow:
|
|
||||||
|
|
||||||
```java
|
|
||||||
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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
InvoicePlatformAccountConfig account = invoicePlatformAccountResolver.resolveByRecord(
|
|
||||||
record.getAccountId(), record.getPlatformCode(), record.getPlatformAccountCode());
|
|
||||||
InvoicePlatformClient client = invoicePlatformClientRegistry.getRequired(account.platformCode());
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Add tests:
|
|
||||||
|
|
||||||
```text
|
|
||||||
applyInvoice_shouldWritePlatformRouteToRecord
|
|
||||||
applyInvoice_shouldRejectAmbiguousSupplierRoute
|
|
||||||
queryInvoice_shouldUseRecordPlatformRoute
|
|
||||||
invalidateInvoice_shouldUseRecordPlatformRoute
|
|
||||||
redInkInvoice_shouldUseRecordPlatformRoute
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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()`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Override
|
|
||||||
public String platformCode() {
|
|
||||||
return "NUONUO";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Stop choosing Nuonuo account from static account map in normal calls. Use `ctx.platformAccount()`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
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:
|
|
||||||
|
|
||||||
```text
|
|
||||||
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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
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:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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:
|
|
||||||
|
|
||||||
```java
|
|
||||||
InvoicePlatformAccountTestRespVO testConnect(InvoicePlatformAccountTestReqVO reqVO);
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Implement by resolving account config and calling the platform client's query endpoint with a harmless nonexistent `applicationNo`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
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:
|
|
||||||
|
|
||||||
```text
|
|
||||||
testConnect_shouldReturnSuccessForEmptyNuonuoQuery
|
|
||||||
testConnect_shouldReturnFailureWhenClientThrowsException
|
|
||||||
testConnect_shouldNotExposeResolvedAppKey
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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:
|
|
||||||
|
|
||||||
```text
|
|
||||||
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:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mvn -pl sw-business/sw-business-server -am -Dtest='InvoiceRecordServiceImplTest,Nuonuo*Test,InvoiceControllerContractTest' test
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Run compile for all affected modules:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rg -n "happy\\^_\\^everyday" -S .
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: no matches.
|
|
||||||
|
|
||||||
- [ ] Check whitespace:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git diff --check
|
|
||||||
```
|
|
||||||
|
|
||||||
## Commit Guidance
|
|
||||||
|
|
||||||
- Commit implementation after Task 10 passes.
|
|
||||||
- Suggested backend commit message:
|
|
||||||
|
|
||||||
```text
|
|
||||||
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.
|
|
||||||
@ -1,284 +0,0 @@
|
|||||||
# 发票平台账号配置设计
|
|
||||||
|
|
||||||
## 背景
|
|
||||||
|
|
||||||
当前发票模块已经有三类配置和业务数据:
|
|
||||||
|
|
||||||
- `biz_invoice`:开票业务配置,按水司账户维护供应商、开票限额、开票类型、自动开票、扩展参数。
|
|
||||||
- `biz_invoice_taxrate`:税收分类编码、税率、规格单位等明细配置。
|
|
||||||
- `biz_cust_invoice`:客户发票抬头、税号、开户行、地址电话等客户资料。
|
|
||||||
|
|
||||||
这几类配置可以支撑“能不能开票、怎么组成发票明细、给谁开票”,但没有独立维护“对接哪个发票平台账号、平台地址、APPID、销方税号、回调地址、密钥引用”的能力。诺诺联调已经证明接口链路可通,但当前实现仍偏向代码或环境变量配置,不适合运营后台长期维护,也不适合未来接入方欣、百望、航信、用友等其他供应商。
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
|
|
||||||
1. 把发票平台账号做成通用配置,不绑定诺诺专有表结构。
|
|
||||||
2. 允许一个水司账户配置多个发票平台账号,并由 `biz_invoice` 业务配置选择默认账号。
|
|
||||||
3. 发票申请、查询、作废、冲红都能回溯当时使用的平台账号,避免后续配置变更影响已开票记录。
|
|
||||||
4. 密钥不落业务库,只在业务库保存 `secret_ref`,实际密钥从环境变量、Nacos、KMS 或 Vault 解析。
|
|
||||||
5. 保持现有 `/business/invoice` 配置语义,让业务开票规则和平台账号解耦。
|
|
||||||
|
|
||||||
## 不纳入本次范围
|
|
||||||
|
|
||||||
- 不一次性实现所有外部供应商客户端。
|
|
||||||
- 不重做发票税率、客户抬头、费用组成等既有配置。
|
|
||||||
- 不迁移历史发票记录的平台账号归属;历史记录可以继续按已有字段展示和查询。
|
|
||||||
- 不把真实 APPKEY、私钥、证书内容保存到数据库。
|
|
||||||
|
|
||||||
## 核心概念
|
|
||||||
|
|
||||||
| 概念 | 存储位置 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 供应商模板 `supplier` | `biz_invoice.supplier` | 现有枚举 `InvoiceTemplateEnum`,描述业务采用哪套开票模板或供应商类型。 |
|
|
||||||
| 平台编码 `platform_code` | 新平台账号表、`biz_invoice`、`biz_invoice_record` | 稳定技术路由键,例如 `NUONUO`、`FANGXIN`、`BAIWANG`、`HANGXIN`。 |
|
|
||||||
| 平台账号编码 `platform_account_code` | 新平台账号表、`biz_invoice`、`biz_invoice_record` | 同一平台下的账号标识,例如 `NUONUO_STAGE_339901999999142`。 |
|
|
||||||
| 平台密钥引用 `app_key_secret_ref` | 新平台账号表 | 指向外部密钥来源的引用值,不保存密钥明文。 |
|
|
||||||
|
|
||||||
供应商模板和平台编码不强制一一对应。比如 `NUONUO_QUANDIAN` 和 `NUONUO` 可以复用 `platform_code=NUONUO`,通过模板字段决定业务明细映射,通过平台编码决定调用哪个技术客户端。
|
|
||||||
|
|
||||||
## 数据模型
|
|
||||||
|
|
||||||
### 新增表 `biz_invoice_platform_account`
|
|
||||||
|
|
||||||
建议新增 PostgreSQL DDL:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
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;
|
|
||||||
```
|
|
||||||
|
|
||||||
字段说明:
|
|
||||||
|
|
||||||
| 字段 | 用途 |
|
|
||||||
| --- | --- |
|
|
||||||
| `account_id` | 水司账户 ID,和现有开票配置保持同一归属维度。 |
|
|
||||||
| `platform_code` | 技术平台编码,作为客户端注册和路由键。 |
|
|
||||||
| `platform_account_code` | 账号编码,业务配置和发票记录只引用这个编码。 |
|
|
||||||
| `base_url` | 平台环境地址,例如诺诺测试环境 `https://cmp-stage.nntest.cn`。 |
|
|
||||||
| `version` | 平台接口版本,诺诺当前使用 `1.0.0`。 |
|
|
||||||
| `app_id` | 平台分配的应用标识。 |
|
|
||||||
| `app_key_secret_ref` | 密钥引用,例如 `env:NUONUO_STAGE_APP_KEY` 或 `nacos:invoice.nuonuo.stage.app-key`。 |
|
|
||||||
| `seller_taxnum` | 销方税号。 |
|
|
||||||
| `company_code` | 平台侧企业编码,有的平台不需要时为空。 |
|
|
||||||
| `callback_url` | 平台回调地址。 |
|
|
||||||
| `extra_properties` | 平台差异化配置,JSON 字符串,例如门店编码、开票员、收款人、复核人。 |
|
|
||||||
| `status` | 启停状态,`1` 启用,`0` 停用。 |
|
|
||||||
|
|
||||||
### 扩展 `biz_invoice`
|
|
||||||
|
|
||||||
在现有开票业务配置表增加默认平台账号绑定:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
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;
|
|
||||||
```
|
|
||||||
|
|
||||||
`biz_invoice` 继续表示开票规则。新增字段只表示这条业务规则默认走哪个平台账号。
|
|
||||||
|
|
||||||
### 复用 `biz_invoice_record`
|
|
||||||
|
|
||||||
发票记录中已有或计划中的 `platform_code`、`platform_account_code` 字段继续保留。申请成功或进入平台处理中时,记录本次使用的平台编码和账号编码。后续查询、作废、冲红优先使用发票记录上的平台字段,不再根据当前业务配置重新选择账号。
|
|
||||||
|
|
||||||
## 密钥方案
|
|
||||||
|
|
||||||
数据库只保存 `app_key_secret_ref`,不保存真实密钥。后端增加统一解析接口:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public interface InvoicePlatformSecretProvider {
|
|
||||||
String resolve(String secretRef);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
第一阶段支持两种引用:
|
|
||||||
|
|
||||||
| 引用格式 | 解析来源 | 用途 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `env:NUONUO_STAGE_APP_KEY` | 系统环境变量 | 本地联调、流水线、临时测试。 |
|
|
||||||
| `nacos:invoice.nuonuo.stage.app-key` | 应用配置中心属性 | 测试环境、生产环境。 |
|
|
||||||
|
|
||||||
后续接入 KMS 或 Vault 时扩展该接口,不影响业务表结构和平台账号表结构。日志中禁止打印 `appKey`、解密后的密钥、`secret_ref` 解析结果。
|
|
||||||
|
|
||||||
## 路由流程
|
|
||||||
|
|
||||||
### 开票申请
|
|
||||||
|
|
||||||
1. 根据 `chargeIds` 校验费用归属,并解析水司账户 `accountId`。
|
|
||||||
2. 读取 `biz_invoice` 中启用的业务配置,匹配 `accountId`、`invoiceType`,并优先使用请求中的 `supplier`。
|
|
||||||
3. 如果请求没有传 `supplier`,且同一 `accountId + invoiceType` 只有一条启用配置,则使用该配置。
|
|
||||||
4. 如果请求没有传 `supplier`,且存在多条启用配置,返回明确业务异常,要求前端传供应商模板。
|
|
||||||
5. 使用业务配置中的 `platform_code`、`platform_account_code` 查询启用的平台账号。
|
|
||||||
6. 从 `InvoicePlatformClientRegistry` 按 `platform_code` 获取客户端。
|
|
||||||
7. 调用平台客户端,写入 `biz_invoice_record.platform_code`、`biz_invoice_record.platform_account_code`、`sys_request_no` 和平台返回状态。
|
|
||||||
|
|
||||||
### 查询、作废、冲红
|
|
||||||
|
|
||||||
1. 根据申请单号或发票记录 ID 读取 `biz_invoice_record`。
|
|
||||||
2. 优先使用记录中的 `platform_code`、`platform_account_code` 查询平台账号。
|
|
||||||
3. 如果历史记录缺少平台字段,且系统中只有一个可用平台账号,可以走兼容兜底;如果存在多个候选账号,返回需要人工确认的业务异常。
|
|
||||||
4. 按 `platform_code` 取客户端并执行对应操作。
|
|
||||||
5. 更新发票记录状态,不覆盖历史平台字段。
|
|
||||||
|
|
||||||
## 服务分层
|
|
||||||
|
|
||||||
新增后端结构:
|
|
||||||
|
|
||||||
```text
|
|
||||||
controller/admin/invoiceplatform/
|
|
||||||
InvoicePlatformAccountController.java
|
|
||||||
vo/InvoicePlatformAccountCreateReqVO.java
|
|
||||||
vo/InvoicePlatformAccountUpdateReqVO.java
|
|
||||||
vo/InvoicePlatformAccountPageReqVO.java
|
|
||||||
vo/InvoicePlatformAccountRespVO.java
|
|
||||||
vo/InvoicePlatformAccountTestReqVO.java
|
|
||||||
vo/InvoicePlatformAccountTestRespVO.java
|
|
||||||
|
|
||||||
dal/dataobject/invoiceplatform/
|
|
||||||
InvoicePlatformAccountDO.java
|
|
||||||
|
|
||||||
dal/mysql/invoiceplatform/
|
|
||||||
InvoicePlatformAccountMapper.java
|
|
||||||
|
|
||||||
service/invoice/platform/config/
|
|
||||||
InvoicePlatformAccountConfig.java
|
|
||||||
InvoicePlatformSecretProvider.java
|
|
||||||
EnvAndPropertyInvoicePlatformSecretProvider.java
|
|
||||||
|
|
||||||
service/invoice/platform/routing/
|
|
||||||
InvoicePlatformAccountResolver.java
|
|
||||||
InvoicePlatformClientRegistry.java
|
|
||||||
InvoiceRouteContext.java
|
|
||||||
```
|
|
||||||
|
|
||||||
管理后台 API:
|
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `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=` | 获取详情,返回 `secret_ref`,不返回密钥。 |
|
|
||||||
| `GET` | `/business/invoice-platform-account/page` | 分页查询。 |
|
|
||||||
| `POST` | `/business/invoice-platform-account/test-connect` | 使用账号配置发起平台连通性测试。 |
|
|
||||||
|
|
||||||
## 客户端注册
|
|
||||||
|
|
||||||
每个供应商客户端声明自己的平台编码:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public interface InvoicePlatformClient {
|
|
||||||
String platformCode();
|
|
||||||
ApplyResult apply(ApplyContext ctx);
|
|
||||||
QueryResult query(QueryContext ctx);
|
|
||||||
InvalidateResult invalidate(PostProcessContext ctx);
|
|
||||||
RedInkResult redInk(PostProcessContext ctx);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
注册表在启动时收集所有客户端:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Component
|
|
||||||
public class InvoicePlatformClientRegistry {
|
|
||||||
private final Map<String, InvoicePlatformClient> clients;
|
|
||||||
|
|
||||||
public InvoicePlatformClientRegistry(List<InvoicePlatformClient> candidates) {
|
|
||||||
this.clients = candidates.stream()
|
|
||||||
.collect(Collectors.toUnmodifiableMap(
|
|
||||||
client -> client.platformCode().toUpperCase(Locale.ROOT),
|
|
||||||
Function.identity()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public InvoicePlatformClient getRequired(String platformCode) {
|
|
||||||
InvoicePlatformClient client = clients.get(platformCode.toUpperCase(Locale.ROOT));
|
|
||||||
if (client == null) {
|
|
||||||
throw exception(INVOICE_PLATFORM_CLIENT_NOT_EXISTS);
|
|
||||||
}
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
这样平台选择从“Spring 只注入一个全局 `InvoicePlatformClient`”升级为“按记录或业务配置路由到对应客户端”。
|
|
||||||
|
|
||||||
## 诺诺账号示例
|
|
||||||
|
|
||||||
联调账号在平台账号表中表现为:
|
|
||||||
|
|
||||||
| 字段 | 值 |
|
|
||||||
| --- | --- |
|
|
||||||
| `account_id` | 对应水司账户 ID |
|
|
||||||
| `platform_code` | `NUONUO` |
|
|
||||||
| `platform_account_code` | `NUONUO_STAGE_339901999999142` |
|
|
||||||
| `account_name` | `诺诺测试账号` |
|
|
||||||
| `base_url` | `https://cmp-stage.nntest.cn` |
|
|
||||||
| `version` | `1.0.0` |
|
|
||||||
| `app_id` | `convert` |
|
|
||||||
| `app_key_secret_ref` | `env:NUONUO_STAGE_APP_KEY` |
|
|
||||||
| `seller_taxnum` | `339901999999142` |
|
|
||||||
|
|
||||||
真实密钥通过运行环境设置 `NUONUO_STAGE_APP_KEY`,不写入仓库、不写入数据库迁移脚本。
|
|
||||||
|
|
||||||
## 兼容策略
|
|
||||||
|
|
||||||
1. 现有 `biz_invoice.extra_properties` 保留,用于开票业务模板的扩展参数。
|
|
||||||
2. 现有诺诺 YAML 配置可以保留为本地联调兜底,但生产调用优先使用平台账号表。
|
|
||||||
3. 平台账号表上线后,新增发票记录必须写入 `platform_code` 和 `platform_account_code`。
|
|
||||||
4. 历史记录缺少平台字段时,只允许在唯一候选账号场景自动兜底。
|
|
||||||
5. `InvoiceApplyReqVO` 增加可选 `supplier` 字段,前端在多供应商场景必须传值。
|
|
||||||
|
|
||||||
## 测试策略
|
|
||||||
|
|
||||||
| 层级 | 覆盖点 |
|
|
||||||
| --- | --- |
|
|
||||||
| Mapper/Service 单测 | 平台账号创建、更新、禁用、唯一性、分页过滤。 |
|
|
||||||
| Secret Provider 单测 | `env:` 和 `nacos:` 引用解析,缺失密钥报错,日志不包含密钥值。 |
|
|
||||||
| Resolver 单测 | 单配置自动选择、多配置歧义报错、停用账号不可用、记录字段优先。 |
|
|
||||||
| Registry 单测 | 多客户端注册、重复平台编码启动失败、未知平台编码报错。 |
|
|
||||||
| InvoiceRecordService 单测 | 开票申请写入平台字段,查询/作废/冲红按历史记录路由。 |
|
|
||||||
| Nuonuo 合约测试 | 请求体使用账号表配置组装,查询接口兼容空数组、对象数组和失败响应。 |
|
|
||||||
| Stage Smoke | 使用 `NUONUO_STAGE_APP_KEY` 环境变量执行诺诺测试环境查询接口,默认跳过。 |
|
|
||||||
|
|
||||||
## 落地顺序
|
|
||||||
|
|
||||||
1. 新增平台账号表、DO、Mapper、VO、Controller、Service。
|
|
||||||
2. 增加 `biz_invoice` 默认平台账号字段,并更新创建、更新、返回 VO。
|
|
||||||
3. 新增密钥解析接口和运行时账号配置对象。
|
|
||||||
4. 新增平台账号解析器和平台客户端注册表。
|
|
||||||
5. 改造发票申请、查询、作废、冲红调用链。
|
|
||||||
6. 把诺诺客户端从固定属性读取改为接收运行时账号配置。
|
|
||||||
7. 增加单元测试、契约测试和默认跳过的联调 Smoke 测试。
|
|
||||||
Loading…
x
Reference in New Issue
Block a user