Browse Source

feat: 进件功能 — image.upload + create/query/cancel + 通知处理

AccountEntity 新增: onboardStatus, onboardOrderId, onboardOutBizNo
AlipayTransferService 新增4个方法:
  - uploadImage: alipay.fund.expandindirect.image.upload
  - createOnboard: alipay.fund.expandindirect.create
  - queryOnboard: alipay.fund.expandindirect.order.query
  - cancelOnboard: alipay.fund.expandindirect.order.cancel
Controller 新增: POST onboard/upload, POST onboard/create, GET onboard/query, POST onboard/cancel
AccountHandler: handleOnboardChanged 处理进件审核结果通知
alphaH 1 ngày trước cách đây
mục cha
commit
a6f1b191fd

+ 43 - 0
java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java

@@ -14,6 +14,7 @@ import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.io.IOException;
 import java.math.BigDecimal;
@@ -108,6 +109,48 @@ public class AccountController {
         return Result.ok(alipayTransferService.authorizeApply((String) b.get("enterprise_id")));
     }
 
+    // ==================== 进件 ====================
+
+    @PreAuthorize("@perm.hasAny('module_payment:account:authorize')")
+    @PostMapping("/onboard/upload")
+    public Result<Map<String, String>> uploadImage(@RequestParam("file") MultipartFile file,
+                                                    @RequestParam("enterprise_id") String enterpriseId) throws IOException {
+        String imageId = alipayTransferService.uploadImage(enterpriseId, file.getBytes(),
+                file.getOriginalFilename() != null ? file.getOriginalFilename() : "image.jpg");
+        return Result.ok(Map.of("image_id", imageId));
+    }
+
+    @PreAuthorize("@perm.hasAny('module_payment:account:authorize')")
+    @PostMapping("/onboard/create")
+    public Result<Map<String, String>> createOnboard(@RequestBody Map<String, Object> b) {
+        return Result.ok(alipayTransferService.createOnboard(
+                (String) b.get("enterprise_id"),
+                (String) b.get("scene_code"),
+                (String) b.get("scene_directions"),
+                (String) b.get("scene_image"),
+                (String) b.get("sites"),
+                (String) b.get("alipay_user_id")));
+    }
+
+    @PreAuthorize("@perm.hasAny('module_payment:account:authorize')")
+    @GetMapping("/onboard/query")
+    public Result<Map<String, String>> queryOnboard(
+            @RequestParam("enterprise_id") String enterpriseId,
+            @RequestParam("order_id") String orderId) {
+        return Result.ok(alipayTransferService.queryOnboard(enterpriseId, orderId));
+    }
+
+    @PreAuthorize("@perm.hasAny('module_payment:account:authorize')")
+    @PostMapping("/onboard/cancel")
+    public Result<Void> cancelOnboard(@RequestBody Map<String, Object> b) {
+        alipayTransferService.cancelOnboard(
+                (String) b.get("enterprise_id"),
+                (String) b.get("order_id"));
+        return Result.ok();
+    }
+
+    // ==================== 签约查询 ====================
+
     @PreAuthorize("@perm.hasAny('module_payment:account:authorize')")
     @GetMapping("/agreement/query")
     public Result<Map<String, String>> queryAgreement(

+ 6 - 0
java/src/main/java/com/payment/platform/module/payment/account/entity/AccountEntity.java

@@ -23,5 +23,11 @@ public class AccountEntity extends PaymentEnterpriseBaseEntity {
     private OffsetDateTime signTime;
     /** 签约协议号 — 安全发模式通过 alipay.user.agreement.page.sign 获取 */
     private String agreementNo;
+    /** 进件状态: PENDING/VALID/INVALID/DISUSE */
+    private String onboardStatus;
+    /** 进件单号(支付宝返回) */
+    private String onboardOrderId;
+    /** 进件外部单号(幂等) */
+    private String onboardOutBizNo;
     private String remark;
 }

+ 121 - 1
java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java

@@ -10,14 +10,23 @@ import com.alipay.api.domain.AlipayFundTransPagePayModel;
 import com.alipay.api.domain.AlipayFundTransUniTransferModel;
 import com.alipay.api.domain.AlipayUserAgreementPageSignModel;
 import com.alipay.api.domain.AlipayUserAgreementQueryModel;
+import com.alipay.api.domain.AlipayFundExpandindirectCreateModel;
+import com.alipay.api.domain.AlipayFundExpandindirectOrderQueryModel;
+import com.alipay.api.domain.AlipayFundExpandindirectOrderCancelModel;
 import com.alipay.api.domain.BankcardExtInfo;
 import com.alipay.api.domain.Participant;
+import com.alipay.api.domain.SecondaryPartnerInfo;
+import com.alipay.api.FileItem;
 import com.alipay.api.request.AlipayFundAccountbookCreateRequest;
 import com.alipay.api.request.AlipayFundAccountbookQueryRequest;
 import com.alipay.api.request.AlipayFundTransCommonQueryRequest;
 import com.alipay.api.request.AlipayFundTransPagePayRequest;
 import com.alipay.api.request.AlipayFundTransUniTransferRequest;
 import com.alipay.api.request.AlipayUserAgreementPageSignRequest;
+import com.alipay.api.request.AlipayFundExpandindirectCreateRequest;
+import com.alipay.api.request.AlipayFundExpandindirectImageUploadRequest;
+import com.alipay.api.request.AlipayFundExpandindirectOrderCancelRequest;
+import com.alipay.api.request.AlipayFundExpandindirectOrderQueryRequest;
 import com.alipay.api.request.AlipayUserAgreementQueryRequest;
 import com.alipay.api.response.AlipayFundAccountbookCreateResponse;
 import com.alipay.api.response.AlipayFundAccountbookQueryResponse;
@@ -25,6 +34,10 @@ import com.alipay.api.response.AlipayFundTransCommonQueryResponse;
 import com.alipay.api.response.AlipayFundTransPagePayResponse;
 import com.alipay.api.response.AlipayFundTransUniTransferResponse;
 import com.alipay.api.response.AlipayUserAgreementPageSignResponse;
+import com.alipay.api.response.AlipayFundExpandindirectCreateResponse;
+import com.alipay.api.response.AlipayFundExpandindirectImageUploadResponse;
+import com.alipay.api.response.AlipayFundExpandindirectOrderCancelResponse;
+import com.alipay.api.response.AlipayFundExpandindirectOrderQueryResponse;
 import com.alipay.api.response.AlipayUserAgreementQueryResponse;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.payment.platform.common.exception.BusinessException;
@@ -199,7 +212,114 @@ public class AlipayTransferService {
         }
     }
 
-    // ==================== 2. 开通记账本 ====================
+    // ==================== 2. 进件 ====================
+
+    private static final String ONBOARD_PRODUCT_CODE = "MERCHANT_TRANSFER_EXPAND_INDIRECT";
+    private static final String ONBOARD_BIZ_SCENE = "ENTRUST_SATF_TRANSFER";
+
+    /**
+     * alipay.fund.expandindirect.image.upload — 上传进件图片,返回 image_id 作为 scene_image 的 file_key
+     */
+    public String uploadImage(String enterpriseId, byte[] fileBytes, String fileName) {
+        try {
+            AlipayFundExpandindirectImageUploadRequest request = new AlipayFundExpandindirectImageUploadRequest();
+            request.setProductCode(ONBOARD_PRODUCT_CODE);
+            request.setBizScene(ONBOARD_BIZ_SCENE);
+            request.setImageType("jpg");
+            FileItem fileItem = new FileItem(fileName, fileBytes, fileName.substring(fileName.lastIndexOf('.') + 1));
+            request.setImageContent(fileItem);
+            AlipayFundExpandindirectImageUploadResponse response =
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
+            if (!response.isSuccess())
+                throw new BusinessException(400, "上传进件图片失败: " + response.getMsg());
+            return response.getImageId();
+        } catch (AlipayApiException e) {
+            throw new BusinessException(400, "上传进件图片失败: " + e.getMessage());
+        }
+    }
+
+    /** alipay.fund.expandindirect.create — 创建进件单 */
+    public Map<String, String> createOnboard(String enterpriseId, String sceneCode,
+                                              String sceneDirections, String sceneImageKeys,
+                                              String sites, String alipayUserId) {
+        String outBizNo = SnowflakeIdGenerator.nextIdStr();
+        try {
+            AlipayFundExpandindirectCreateModel model = new AlipayFundExpandindirectCreateModel();
+            model.setProductCode(ONBOARD_PRODUCT_CODE);
+            model.setBizScene(ONBOARD_BIZ_SCENE);
+            model.setOutBizNo(outBizNo);
+            model.setBizType("CREATE");
+            model.setSceneCode(sceneCode);
+            model.setSceneDirections(sceneDirections);
+            model.setSceneImage(sceneImageKeys);
+            if (sites != null && !sites.isBlank()) model.setSites(sites);
+            SecondaryPartnerInfo partner = new SecondaryPartnerInfo();
+            partner.setIdentity(alipayUserId);
+            partner.setIdentityType("ALIPAY_USER_ID");
+            model.setSecondaryPartnerInfo(partner);
+
+            AlipayFundExpandindirectCreateRequest request = new AlipayFundExpandindirectCreateRequest();
+            request.setBizModel(model);
+            AlipayFundExpandindirectCreateResponse response =
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
+            if (!response.isSuccess())
+                throw new BusinessException(400, "创建进件单失败: " + response.getMsg());
+
+            var accounts = accountMapper.selectList(
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<AccountEntity>()
+                            .eq(AccountEntity::getEnterpriseId, enterpriseId));
+            for (AccountEntity acc : accounts) {
+                acc.setOnboardOutBizNo(outBizNo);
+                acc.setOnboardOrderId(response.getOrderId());
+                acc.setOnboardStatus("PENDING");
+                accountMapper.updateById(acc);
+            }
+            return Map.of("order_id", response.getOrderId() != null ? response.getOrderId() : "",
+                    "out_biz_no", outBizNo, "status", "PENDING");
+        } catch (AlipayApiException e) {
+            throw new BusinessException(400, "创建进件单失败: " + e.getMessage());
+        }
+    }
+
+    /** alipay.fund.expandindirect.order.query — 查询进件状态 */
+    public Map<String, String> queryOnboard(String enterpriseId, String orderId) {
+        try {
+            AlipayFundExpandindirectOrderQueryModel model = new AlipayFundExpandindirectOrderQueryModel();
+            model.setProductCode(ONBOARD_PRODUCT_CODE);
+            model.setBizScene(ONBOARD_BIZ_SCENE);
+            model.setOrderId(orderId);
+            AlipayFundExpandindirectOrderQueryRequest request = new AlipayFundExpandindirectOrderQueryRequest();
+            request.setBizModel(model);
+            AlipayFundExpandindirectOrderQueryResponse response =
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
+            if (!response.isSuccess())
+                throw new BusinessException(400, "查询进件状态失败: " + response.getMsg());
+            return Map.of("order_id", orderId,
+                    "status", response.getStatus() != null ? response.getStatus() : "PENDING");
+        } catch (AlipayApiException e) {
+            throw new BusinessException(400, "查询进件状态失败: " + e.getMessage());
+        }
+    }
+
+    /** alipay.fund.expandindirect.order.cancel — 取消进件单 */
+    public void cancelOnboard(String enterpriseId, String orderId) {
+        try {
+            AlipayFundExpandindirectOrderCancelModel model = new AlipayFundExpandindirectOrderCancelModel();
+            model.setProductCode(ONBOARD_PRODUCT_CODE);
+            model.setBizScene(ONBOARD_BIZ_SCENE);
+            model.setOrderId(orderId);
+            AlipayFundExpandindirectOrderCancelRequest request = new AlipayFundExpandindirectOrderCancelRequest();
+            request.setBizModel(model);
+            AlipayFundExpandindirectOrderCancelResponse response =
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
+            if (!response.isSuccess())
+                throw new BusinessException(400, "取消进件单失败: " + response.getMsg());
+        } catch (AlipayApiException e) {
+            throw new BusinessException(400, "取消进件单失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 3. 开通记账本 ====================
 
     /**
      * alipay.fund.accountbook.create

+ 27 - 1
java/src/main/java/com/payment/platform/module/payment/notification/handler/AccountHandler.java

@@ -41,7 +41,8 @@ public class AccountHandler extends BaseNotifyHandler {
             "alipay.commerce.ec.trans.account.deposit",
             "alipay.commerce.ec.fund.change.notify",
             "alipay.commerce.ec.trans.authorize.notify",
-            "dut_user_sign"   // 安全发签约结果通知(notify_type,无 msg_method)
+            "dut_user_sign",   // 安全发签约结果通知(notify_type,无 msg_method)
+            "alipay.fund.expandindirect.order.changed"  // 进件状态通知
         };
     }
 
@@ -52,6 +53,8 @@ public class AccountHandler extends BaseNotifyHandler {
         try {
             if ("dut_user_sign".equals(msgMethod)) {
                 handleAgreementSign(params);
+            } else if ("alipay.fund.expandindirect.order.changed".equals(msgMethod)) {
+                handleOnboardChanged(params);
             } else if ("alipay.commerce.ec.fund.change.notify".equals(msgMethod)) {
                 handleFundChange(params);
             } else if ("alipay.commerce.ec.trans.authorize.notify".equals(msgMethod)) {
@@ -98,6 +101,29 @@ public class AccountHandler extends BaseNotifyHandler {
         }
     }
 
+    /**
+     * 进件状态通知 — alipay.fund.expandindirect.order.changed
+     * status: VALID(已生效)/ INVALID(不通过)/ DISUSE(已废弃)
+     */
+    private void handleOnboardChanged(Map<String, String> params) {
+        String orderId = params.get("order_id");
+        String outBizNo = params.get("out_biz_no");
+        String status = params.get("status");
+        String riskRemark = params.get("risk_review_remark");
+        log.info("进件状态通知: orderId={}, outBizNo={}, status={}, remark={}", orderId, outBizNo, status, riskRemark);
+
+        if (orderId == null) return;
+        var accounts = accountMapper.selectList(
+                new LambdaQueryWrapper<AccountEntity>()
+                        .eq(AccountEntity::getOnboardOrderId, orderId));
+        for (AccountEntity acc : accounts) {
+            acc.setOnboardStatus(status != null ? status : "PENDING");
+            accountMapper.updateById(acc);
+            log.info("进件状态更新: enterpriseId={}, orderId={}, status={}",
+                    acc.getEnterpriseId(), orderId, status);
+        }
+    }
+
     /**
      * 资金变更通知 -- 对应 Python _handle_fund_change
      *

+ 6 - 0
logs/payment-platform.log

@@ -1018,3 +1018,9 @@ Caused by: java.net.ConnectException: Connection refused: getsockopt
 2026-07-15 13:18:50.019 [scheduling-1] INFO  c.p.p.m.p.f.s.F2fTradePollScheduler - [收款轮询] 无待轮询交易
 2026-07-15 13:19:50.028 [scheduling-1] INFO  c.p.p.m.p.f.s.F2fTradePollScheduler - [收款轮询] 定时任务开始执行
 2026-07-15 13:19:50.072 [scheduling-1] INFO  c.p.p.m.p.f.s.F2fTradePollScheduler - [收款轮询] 无待轮询交易
+2026-07-15 13:20:50.078 [scheduling-1] INFO  c.p.p.m.p.f.s.F2fTradePollScheduler - [收款轮询] 定时任务开始执行
+2026-07-15 13:20:50.119 [scheduling-1] INFO  c.p.p.m.p.f.s.F2fTradePollScheduler - [收款轮询] 无待轮询交易
+2026-07-15 13:21:50.130 [scheduling-1] INFO  c.p.p.m.p.f.s.F2fTradePollScheduler - [收款轮询] 定时任务开始执行
+2026-07-15 13:21:50.176 [scheduling-1] INFO  c.p.p.m.p.f.s.F2fTradePollScheduler - [收款轮询] 无待轮询交易
+2026-07-15 13:22:50.190 [scheduling-1] INFO  c.p.p.m.p.f.s.F2fTradePollScheduler - [收款轮询] 定时任务开始执行
+2026-07-15 13:22:50.234 [scheduling-1] INFO  c.p.p.m.p.f.s.F2fTradePollScheduler - [收款轮询] 无待轮询交易