From 68c6a5802964fc8f5b577feb39a44d733ddc9038 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期一, 14 九月 2026 13:30:42 +0800
Subject: [PATCH] refactor(payment): 退役平台自动提现,改由斗拱自动结算并新增结算记录页

---
 talent-card-server/docs/huifu-payment-integration.md |  177 +++++-----------------------------------------------------
 1 files changed, 17 insertions(+), 160 deletions(-)

diff --git a/talent-card-server/docs/huifu-payment-integration.md b/talent-card-server/docs/huifu-payment-integration.md
index 02f031f..899a50f 100644
--- a/talent-card-server/docs/huifu-payment-integration.md
+++ b/talent-card-server/docs/huifu-payment-integration.md
@@ -161,61 +161,21 @@
 duplicate historical verification records as revoked (keeping the earliest one) before creating the
 unique key.
 
-## Daily withdrawal aggregation
+## Automatic settlement
 
-`AutoWithdrawalScheduler` runs `AutoWithdrawalAggregationService` every day at 00:10 Beijing time
-(`app.merchant.auto-withdrawal.cron`, zone `Asia/Shanghai`) for the previous calendar day. The service
-turns one merchant store-day into one `merchant_withdrawal` row and immediately hands the new records
-to the automatic encashment path described below. `AUTO_WITHDRAWAL_ENABLED` still defaults to `false`:
-the flag is turned on only after the sandbox and controlled real-amount acceptance of ticket 06.
+The platform no longer aggregates daily orders and no longer calls Huifu encashment. Delayed
+account plus transaction confirmation already keep unverified money away from the merchant, and
+payout is left to Huifu:
 
-| Rule | Implementation |
-| --- | --- |
-| Business day | Beijing time. The window is `verify_time < business day 24:00` and `>= business day - 30 days` |
-| Order eligibility | `order_status = 1`, `verify_status = 1`, `refund_status = 0`, `withdrawal_status = 0`, not deleted, plus an active `verify_record` |
-| Merchant store eligibility | Newest approved onboarding of the store, with a non-blank `provider_huifu_id` and an approved settlement card that carries a provider token |
-| Settlement card | The approval-time card when it has a token, otherwise the store's newest approved token card; the recorded `bank_card_id` and the token always come from that one card |
-| One record per merchant store-day | `merchant_withdrawal.withdrawal_date` (Beijing `yyyyMMdd`) plus unique key `uk_merchant_withdrawal_daily (merchant_id, store_key, withdrawal_date)`, where `store_key = ifnull(store_id, 0)` |
-| Source order links | `merchant_withdrawal_order`, one row per order, unique key on `order_id` |
-| Amount | Integer cents, the sum of `benefit_order.pay_amount` |
-| Deferral | Skip the day when the total is below `min-amount-cents`, when `total - fee-buffer-cents` is still below it, or when the merchant store has no dispatchable settlement configuration |
-| Idempotency | The day's record is looked up first; the unique keys on `(merchant_id, store_key, withdrawal_date)` and on `order_id` are the database-level guard |
-| Locking | Every aggregated order goes through `OrderFundBoundaryService.lockIntoWithdrawal`, so it can no longer be revoked, refunded or aggregated again |
+- onboarding opens automatic settlement for every merchant (settle_status=1, cycle D1,
+  batch settlement, no minimum and no retained amount), so a one-cent settlement is possible;
+- after verification confirms the delayed transaction, the money sits in the merchant's basic
+  account and Huifu settles it to the bound settlement card on the next natural day;
+- the platform stores no withdrawal rows. The admin **结算记录** page queries Huifu settlement
+  records (/v2/merchant/basicdata/settlement/query) to monitor how much was settled each day.
 
-The window's lower bound exists so a first deployment does not sweep years of history into one
-withdrawal; anything older than 30 days is left alone for manual review. Inside the window, an order
-that was skipped because its refund was still in flight, or because the merchant store had no usable
-settlement card, is picked up by the next run instead of being stranded. Aggregation is per merchant store, matching the store-scoped withdrawal
-pages and the store that collected the money.
-
-```text
-AUTO_WITHDRAWAL_ENABLED=false
-AUTO_WITHDRAWAL_CRON=0 10 0 * * *
-AUTO_WITHDRAWAL_MAX_ATTEMPTS=3
-AUTO_WITHDRAWAL_QUERY_AFTER_MINUTES=5
-AUTO_WITHDRAWAL_RESULT_CHECK_ENABLED=true
-AUTO_WITHDRAWAL_RESULT_CHECK_FIXED_DELAY_MS=300000
-```
-
-There is no minimum withdrawal amount and no fee buffer: Huifu's active encashment has no minimum,
-and encashment itself is free (the fee is deducted when the payment is collected), so any verified
-amount is withdrawn no matter how small. `AUTO_WITHDRAWAL_ENABLED` only gates the automatic nightly
-aggregation; result convergence (submitting pending rows and reconciling processing ones) is gated by
-`AUTO_WITHDRAWAL_RESULT_CHECK_ENABLED` alone, because it must keep working when aggregation is run
-manually from the admin endpoint.
-
-`AutoWithdrawalAggregationTests` covers the Beijing date boundary, the eligibility filters (each
-negative order carries an active verification record, so the filters themselves are exercised), the
-source order links, the minimum amount and fee-buffer deferral, repeated runs, the database uniqueness
-guard, the 30-day lookback, the approved settlement card, a merchant store with no dispatchable
-configuration, and per-store isolation.
-
-`AutoWithdrawalEndToEndAcceptanceTests` is the chain-level acceptance for ticket 06: it drives the
-real services from `submitOnboarding` through sub-merchant approval, the capability snapshot, order
-creation, miniprogram payment, the signed callback, merchant verification, daily aggregation, the
-encashment submission and the notification/query convergence, with every Huifu gateway stubbed. The
-sandbox and controlled real-amount procedure, the whitelist and amount-cap rules, and the items that
-still need provider confirmation are in `docs/automatic-withdrawal-acceptance.md`.
+Migration V140 drops merchant_withdrawal, merchant_withdrawal_order and the order's
+withdrawal_status.
 
 ## Reconciliation and exception handling
 
@@ -359,114 +319,11 @@
 ## Remaining provider-dependent work
 
 - Delayed-account confirmation must only be enabled after the corresponding Huifu product and confirmation API are provisioned and tested.
-- Merchant onboarding, bank-card verification, merchant settlement/withdrawal callbacks, platform bill download, and accounting reconciliation still require the exact Huifu product contracts and callback specifications for the merchant account.
 
-## Merchant withdrawal (encashment)
+## Settlement records
 
-Automatic withdrawal uses Huifu `POST /v2/trade/settlement/encashment` (主动取现), not the mini-program
-payment product, and it is submitted only after the provider values are recorded:
+HuifuSettlementRecordGateway queries Huifu settlement records for one sub-merchant over a date
+range. The admin endpoint GET /api/admin/management/merchant-settlement/settlements asks every
+approved onboarding for the range (default: last 30 days), merges the rows and returns them newest
+first. A merchant whose query fails is reported in ailures instead of failing the whole page.
 
-- the approved merchant's `huifu_id` on the onboarding application;
-- the approved settlement card's `token_no` from Huifu card binding.
-
-The admin settlement pages are read-only for withdrawals: there is no manual approval and no way to set
-a withdrawal to paid or failed by hand, on either the merchant or the admin side. A withdrawal state
-changes only when Huifu confirms it through the asynchronous notification or through the status query.
-Merchants can still view and replace the settlement card, because that is the account the automatic
-withdrawal pays into.
-
-### Submission
-
-`AutoWithdrawalResultService.submitPending` claims each `pending` auto-withdrawal row with a conditional
-update (`where status = 'pending'`), so a repeated task or a second instance can never submit the same
-record twice, then calls the provider with the record's own `withdrawal_no` as the idempotent request
-number (`req_seq_id`). The exact values the request used are stored on the row and reused for the later
-query: `provider_request_date` is the same `yyyyMMdd` that was sent as `req_date` (computed once, not
-read from the clock twice), and `provider_huifu_id` is the sub-merchant the request was submitted to.
-The query locates a payout by `huifu_id` + `org_req_date` + `org_req_seq_id`, so re-deriving the
-sub-merchant at query time would read a merchant that re-onboarded in between as "transaction does not
-exist" (`23000001`) and wrongly release an accepted payout. Rows written before migration `V136` carry
-no pinned merchant and fall back to the store's newest approved sub-merchant.
-
-| Provider answer | Local state | Meaning |
-| --- | --- | --- |
-| `trans_stat = S` and an accepted code | `paid` | The money left the sub-merchant account |
-| `trans_stat = P`, an accepted code, or a payload that cannot be read | `processing` | Submitted, result not decided |
-| `trans_stat = F`, or a non-accepted code with no state | `failed` | The provider rejected it; no money moved |
-| Gateway exception after the request may have been sent | `processing` | Unknown result; the query has to converge it |
-| Local failure before the request (`BusinessException`) | stays `pending` | Nothing was sent, the next run retries |
-
-The result mapping (`HuifuWithdrawalResult`) is intentionally biased towards `processing`: only a
-statement whose transaction state *and* response code agree may reach a final state, a contradictory
-statement is re-queried, and the duplicate-transaction code `20000000` is never read as a failure.
-Wrongly declaring a request failed would release its orders and pay the same money twice.
-
-### Confirmation
-
-A signature-verified notification and the scheduled query both go through the same idempotent entry
-point (`AutoWithdrawalResultService.apply`), which locks the withdrawal row, refuses to touch a record
-that is already `paid` or `failed`, and only then moves `pending`/`processing` to a final state. A
-repeated notification therefore changes nothing, and a late opposite result cannot reopen a terminal
-one. Huifu may send two notifications for one withdrawal (channel and accounting, `msg_type 01`/`02`);
-both carry the same `req_seq_id` and both converge on the same state.
-
-`failed` has to be terminal: the failure already released the source orders, so reopening the record on
-a late success would let the same money be withdrawn a second time. A statement that contradicts the
-recorded terminal state is therefore *not* applied and is written to the server log with the withdrawal
-number, state, outcome and response code, so an operator can reconcile it by hand.
-
-While a withdrawal is still `processing`, an accepted answer from the submission is evidence and is
-kept: the first non-blank response code and message are never overwritten by the temporary answers of
-later queries.
-
-The query uses `huifu_id` + `org_req_date` + `org_req_seq_id`, and waits
-`app.merchant.auto-withdrawal.query-after-minutes` (default 5) after submission. Querying too early
-would read the provider's "not found" (`23000001`) as a failure and wrongly release the orders; the
-grace period is also what makes `23000001` trustworthy, because a request the provider never accepted
-is exactly the case that has to be deferred again.
-
-### Failure deferral
-
-When Huifu confirms a failure, the reason is stored in `merchant_withdrawal.failure_reason` and each
-source order is released (`OrderFundBoundaryService.releaseFromWithdrawal`, association row marked with
-`released_at`), so the next daily aggregation combines it with the new day's orders and submits a fresh
-request. One order is retried at most `app.merchant.auto-withdrawal.max-attempts` times (default 3);
-after that it stays locked inside the failed record, which keeps the amount visible for manual
-handling instead of retrying forever. That stuck money is the one withdrawal state an operator still
-has to act on, so the management console shows a "提现失败待处理" notice counting failed withdrawals
-that still hold locked orders.
-
-The uniqueness rule of `merchant_withdrawal_order` is therefore "at most one *unreleased* association
-per order": the generated column `active` is `1` while `released_at` is null and `NULL` afterwards, and
-MySQL ignores `NULL` entries in the unique key `uk_withdrawal_order_active (order_id, active)`.
-Migrations: `V133`/`V134` (aggregation, day-scoped uniqueness), `V135` (release support, submit
-counter), `V136` (sub-merchant pinned on the withdrawal for the result query).
-
-### Configuration
-
-```text
-HUIFU_WITHDRAWAL_PRODUCT_ID=...          # Product that has encashment permission, may differ from payment product
-HUIFU_WITHDRAWAL_NOTIFY_URL=https://api.example.com/api/payments/huifu/withdrawal-notify
-HUIFU_WITHDRAWAL_INTO_ACCOUNT_DATE_TYPE=D0
-```
-
-`HUIFU_WITHDRAWAL_NOTIFY_URL` must be publicly reachable HTTPS and configured with Huifu. The callback
-verifies `resp_data` with Huifu's public key before a withdrawal is touched. `status=paid` is the only
-terminal success; `processing` never means funds arrived.
-
-### Statuses exposed to users
-
-`merchant_withdrawal.status` keeps four internal values, but both the merchant and the admin API collapse
-them to the three agreed states with `AutoWithdrawalResultService.displayStatus`: `pending` and
-`processing` are shown as 处理中, `paid` as 已到账 and `failed` as 提现失败.
-
-### Remaining work
-
-- Huifu's `re_exchange = Y` (已退汇, money returned after a payout) and `acct_status = B` (回账成功) are
-  not handled yet: a payout that is returned after being confirmed is not moved back to 提现失败 and the
-  amount is not deferred again.
-- The `query-after-minutes` grace period is the only thing standing between "the channel has not booked
-  it yet" and "the channel never accepted it". The sandbox acceptance of ticket 06 has to confirm how
-  quickly an accepted encashment becomes queryable before the flag is turned on.
-- The single-encashment amount limits are per merchant and per bank, so the sandbox acceptance of ticket
-  06 has to confirm the effective minimum, the daily cap and the fee deduction before enabling the flag.

--
Gitblit v1.7.1