Files
ruoyi-vue-pro/docs/education/migration/issues/EDU-021-legacy-pay-account-import.md

52 lines
5.7 KiB
Markdown

# EDU-021 — Import legacy tenant payment accounts into native Pay
- **Status:** done — bounded single-account import, audit, and native Pay UI entry implemented and verified
- **Type:** legacy data bridge / payment security / tenant isolation
- **Phase:** 4 / tenant operations
- **Blockers:** EDU-020, native Pay App/Channel runtime, PostgreSQL Flyway
## Problem
The legacy `tenant_payment_accounts` and `app_private.tenant_secrets` records cannot be copied directly into native Pay. Provider aliases, collection modes, channel variants, callback ownership, credential shapes, and status values are not one-to-one. Guessing any of them can route money or callbacks to the wrong party.
The migration also needs durable evidence without creating an Education payment shadow model or persisting a second plaintext credential copy.
## Delivered contract
- The bridge is Pay-owned and creates native `pay_app` and `pay_channel` rows through `PayAppService` and `PayChannelService`; Education owns neither a payment account nor a credential table.
- `POST /pay/legacy-account-import/import` imports exactly one explicitly reviewed manifest. It requires both `pay:app:create` and `pay:channel:create`.
- `GET /pay/legacy-account-import/page` exposes tenant-filtered audit history and requires both Pay App and Channel query permissions.
- The existing `pay/app/index` Vben page adds a **迁移旧支付账号** action and JSON manifest modal. The button uses explicit AND permission visibility, matching the controller.
- Import request-body logging is disabled for database access logs, non-production request logs, and unexpected-error logs so the manifest does not become a plaintext logging side channel.
- Only `tenant_collect` is accepted. `platform_collect` and `service_provider` fail closed because their settlement and merchant ownership semantics are not equivalent.
- Historical WeChat and Alipay aliases normalize to `wechat_pay` or `alipay`. Non-equivalent providers such as XPay/Xunhu fail closed.
- Operators must explicitly select a native channel such as `wx_lite`, `wx_pub`, or an Alipay variant. Provider family and channel family must match; the importer never guesses a WeChat client type.
- WeChat V3 and Alipay public-key configurations map into native Pay configuration objects. Multiple rotating WeChat platform keys require an explicit choice. Alipay accepts only the native production or sandbox official gateway.
- Old provider callbacks are not reused. The manifest must provide new business order/refund callbacks and may provide a transfer callback.
- `active` maps to enabled. `disabled` and `pending` map to disabled with an audit note.
- Within the current target tenant, `sourceAccountId` is the idempotency key. A replay with the same source SHA-256 returns the existing mapping; a different checksum is rejected rather than overwriting it.
## Audit and database contract
V4330 creates tenant-scoped `pay_legacy_account_import` with source identifiers/checksum, normalized provider/config digest, target App/Channel IDs, mapping notes, operator, and timestamp. It deliberately has no `config_public`, `secret_json`, raw config, or secret-value column.
Composite foreign keys `(tenant_id,target_app_id)` and `(tenant_id,target_channel_id)` prevent an audit row from pointing across tenants. A target tenant may import the same legacy UUID independently, while duplicate active source IDs inside one tenant are rejected. An existing global audit table without `tenant_id` causes migration failure and requires explicit disposition.
Credentials still enter native `pay_channel.config` using Pay's existing configuration storage. EDU-021 prevents an extra audit copy; it does not introduce generic encryption or key rotation for native Pay credentials.
## Verification
- Nine focused importer tests pass for WeChat/Alipay mapping, aliases, disabled-state mapping, replay, checksum conflict, unsupported modes/providers, channel mismatch, unsafe endpoints/rotating keys, and audit redaction. A controller contract verifies dual write permission and request-body logging suppression.
- The combined Pay selection passes 29 tests, including the prior tenant App/Channel contracts.
- All 45 PostgreSQL Flyway integration tests pass through V4380. V4330 coverage verifies tenant-independent legacy UUID reuse, no raw credential columns, cross-tenant composite-FK rejection, mode constraints, migration history, and fail-closed global-table adoption.
- The Vben `@vben/web-antd` typecheck passes with the import API, modal, and explicit dual-permission button.
## Explicitly open
- Audit history currently has a backend/Vben API contract but no dedicated history table in the account-import modal. EDU-023 provides its own recent transaction-import history table on the native order page.
- A controlled export job from the legacy database and operator runbook are still required before production bulk migration. The UI template contains placeholders and must never be submitted unchanged.
- Concurrent first imports of the same source account rely on the database unique constraint and transaction rollback; a friendly concurrent-replay response is not claimed.
- Platform/service-provider settlement, XPay/Xunhu replacement, generic credential encryption/rotation, and tenant PNVS require separate decisions.
- EDU-022 activates tenant-scoped native Pay order, refund, and notification ledgers; EDU-023 adds bounded terminal legacy transaction import; EDU-024 activates empty native Transfer/Wallet ledgers. Production bulk tooling and reviewed opening-balance migration remain open.
- EDU-025 delivers native Mall Product activation. Explicit legacy product import, Promotion/Trade, coupon import/redemption, purchase fulfillment, refunds-to-entitlement revocation, and reconciliation remain separate commercialization work.